Skip to content

Conversation

@six-standard
Copy link
Member

@six-standard six-standard commented Mar 10, 2025

스크린샷 2025-03-10 오후 8 22 28

공지사항 기능을 드디어 완성했습니다!
따지고 보면 단순한 기능인데, UI 디자인 선정이랑 일정때문에 오늘에서야 완성되네요~

Summary by CodeRabbit

  • 새로운 기능
    • 헤더의 드롭다운 메뉴에 “공지사항” 버튼을 추가하여, 클릭 시 최신 알림을 확인할 수 있는 모달 창이 표시됩니다.
    • 최신 업데이트에 따라 알림 배너가 나타나, 중요한 공지사항을 즉시 전달합니다.
    • 알림을 표시하는 모달 컴포넌트가 추가되었습니다.
  • 스타일
    • 타이포그래피 플러그인 도입으로 앱의 텍스트 스타일과 전반적인 디자인이 향상되었습니다.

@coderabbitai
Copy link

coderabbitai bot commented Mar 10, 2025

Walkthrough

이번 PR은 여러 영역에 걸쳐 기능이 확장된 변경사항을 포함합니다. package.json에 두 개의 신규 디펜던시가 추가되고, API 관련 파일과 타입 파일에서 공지사항 데이터를 처리할 수 있도록 새로운 export와 함수, 타입이 도입되었습니다. 또한, 모달 기능을 제공하기 위한 ModalProvider와 관련된 컴포넌트 및 헤더, 레이아웃 업데이트가 포함되어 있으며, Tailwind CSS 설정에 typography 플러그인이 추가되었습니다.

Changes

파일 변경 요약
package.json 신규 의존성 @tailwindcss/typography (^0.5.16), zustand (^5.0.3) 추가
src/apis/index.ts
src/apis/notice.request.ts
notice.request 모듈 export 추가 및 notiList 비동기 함수 도입
src/constants/paths.constant.ts PATHS 객체에 NOTIS: '/notis' 경로 추가
src/types/index.ts
src/types/notice.type.ts
notice.type export 추가 및 NotiListDto 타입 정의 추가
src/app/layout.tsx ModalProvider 컴포넌트 추가 및 레이아웃에 통합
src/hooks/useModal.tsx 파일 상단에 'use client'; 디렉티브 추가
src/app/(auth-required)/components/header/index.tsx 공지사항 버튼 추가 및 useModal hook 활용으로 모달 오픈 기능 도입
src/app/(auth-required)/components/notice/Modal.tsx
src/app/(auth-required)/components/notice/index.tsx
새로운 ModalNotice 컴포넌트 추가, 공지사항 데이터 조회 및 배너/모달 UI 구현
src/app/(auth-required)/layout.tsx notiList prefetch 및 Notice 컴포넌트 렌더링을 위해 레이아웃 업데이트
src/components/Icon/icons/index.ts Close.svg 아이콘 export 추가
src/components/Providers/ModalProvider.tsx
src/components/Providers/index.ts
모달 관리를 위한 ModalProvider 컴포넌트 추가 및 Providers index에 export 업데이트
tailwind.config.ts Tailwind CSS에 typography 플러그인 추가, plugins 배열 업데이트

Sequence Diagram(s)

sequenceDiagram
  participant U as 사용자
  participant H as Header 컴포넌트
  participant M as useModal / Modal
  participant API as API (notiList)
  
  U->>H: 공지사항 버튼 클릭
  H->>M: ModalOpen 호출하여 모달 열기
  M->>M: 모달 렌더링 및 useQuery 호출
  M->>API: 공지사항 데이터 요청 (notiList)
  API-->>M: 공지사항 데이터 반환
  M->>U: 데이터 표시 (모달 내 공지사항)
  U->>M: Escape 키 또는 외부 클릭
  M->>M: 모달 닫힘 처리
Loading

Suggested labels

bug

Suggested reviewers

  • Nuung
  • Jihyun3478
  • cheonHwi

Poem

안녕, 나는 토끼야, 깡충깡충 뛰며
코드 변화 속을 즐겁게 달려가네.
모달과 공지사항 반짝이며
새 기능들이 춤추듯 펼쳐지네.
우리 함께 달려보자, 혁신의 길 위에서! 🐇✨

Tip

⚡🧪 Multi-step agentic review comment chat (experimental)
  • We're introducing multi-step agentic chat in review comments. This experimental feature enhances review discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments.
    - To enable this feature, set early_access to true under in the settings.

📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f62c912 and e3f4efd.

📒 Files selected for processing (1)
  • src/app/(auth-required)/components/notice/index.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/app/(auth-required)/components/notice/index.tsx

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@six-standard six-standard changed the title feat: 공지사항 제작 [25.03.10 / TASK-121] Feature - 공지사항 기능 구현 Mar 10, 2025
@six-standard six-standard self-assigned this Mar 10, 2025
@six-standard six-standard added the enhancement New feature or request label Mar 10, 2025
@notion-workspace
Copy link

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (6)
src/apis/notice.request.ts (1)

1-6: 공지사항 API 요청 함수 구현이 잘 되었습니다

공지사항 목록을 가져오는 API 함수가 간결하고 명확하게 구현되어 있습니다. 적절한 타입 지정과 함께 PATHS 상수를 활용하여 엔드포인트를 관리하는 방식이 좋습니다.

한 가지 제안사항으로, 에러 처리 로직을 추가하는 것을 고려해볼 수 있습니다. API 요청 실패 시 적절한 오류 메시지나 대체 UI를 제공할 수 있도록 하면 사용자 경험이 향상될 것입니다.

src/components/auth-required/notice/Modal.tsx (3)

16-16: 오타 수정이 필요합니다.

클래스명에 오타가 있습니다.

- <h2 className="text-TEXT-MAIN items-cenetr gap-3 text-T3">공지사항</h2>
+ <h2 className="text-TEXT-MAIN items-center gap-3 text-T3">공지사항</h2>

10-11: 로딩 및 에러 상태 처리가 필요합니다.

API 요청 중 로딩 상태와 오류 상태를 처리하는 코드가 없습니다. 사용자 경험 향상을 위해 이러한 상태를 처리하는 것이 좋습니다.

- const { data } = useQuery({ queryKey: [PATHS.NOTIS], queryFn: notiList });
+ const { data, isLoading, error } = useQuery({ queryKey: [PATHS.NOTIS], queryFn: notiList });

  return (
    <div className="w-[800px] h-[500px] overflow-auto flex flex-col gap-5 p-10 rounded-md bg-BG-SUB">
      <div className="flex items-center justify-between">
        <h2 className="text-TEXT-MAIN items-center gap-3 text-T3">공지사항</h2>
        <Icon name="Close" onClick={close} className="cursor-pointer" />
      </div>

+     {isLoading && (
+       <div className="flex justify-center items-center h-[400px]">
+         <p className="text-TEXT-ALT">로딩 중...</p>
+       </div>
+     )}
+
+     {error && (
+       <div className="flex justify-center items-center h-[400px]">
+         <p className="text-TEXT-ALT">데이터를 불러오는 중 오류가 발생했습니다.</p>
+       </div>
+     )}

-     {data?.posts?.map(({ content, created_at, id, title }) => (
+     {!isLoading && !error && data?.posts?.map(({ content, created_at, id, title }) => (

24-26: 날짜 형식 개선이 필요합니다.

현재 날짜는 T를 기준으로 분할하여 처리하고 있습니다. 더 견고한 날짜 형식 처리를 위해 Date 객체나 날짜 라이브러리 사용을 권장합니다.

- <h4 className="text-TEXT-ALT text-T5">
-   {created_at.split('T')[0]}
- </h4>
+ <h4 className="text-TEXT-ALT text-T5">
+   {new Date(created_at).toLocaleDateString('ko-KR', {
+     year: 'numeric',
+     month: 'long',
+     day: 'numeric'
+   })}
+ </h4>

또는 더 다양한 형식 지원이 필요하다면 date-fns 같은 라이브러리를 고려하세요:

+ import { format } from 'date-fns';
+ import { ko } from 'date-fns/locale';

- <h4 className="text-TEXT-ALT text-T5">
-   {created_at.split('T')[0]}
- </h4>
+ <h4 className="text-TEXT-ALT text-T5">
+   {format(new Date(created_at), 'yyyy년 MM월 dd일', { locale: ko })}
+ </h4>
src/components/auth-required/header/index.tsx (1)

15-16: Import 순서 수정 필요

import 순서가 ESLint 규칙에 맞지 않습니다. @/hooks/useModal과 관련 import는 상대 경로 import인 ./Section 이전에 와야 합니다.

- import { defaultStyle, Section, textStyle } from './Section';
- import { useModal } from '@/hooks/useModal';
- import { Modal } from '../notice/Modal';

+ import { useModal } from '@/hooks/useModal';
+ import { Modal } from '../notice/Modal';
+ import { defaultStyle, Section, textStyle } from './Section';
🧰 Tools
🪛 ESLint

[error] 15-15: @/hooks/useModal import should occur before import of ./Section

(import/order)

src/components/auth-required/notice/index.tsx (1)

29-53: 접근성 개선 제안

알림 배너의 구현이 잘 되었습니다. UI 접근성을 높이기 위해 ARIA 속성 추가를 고려해 보세요.

<div
  className={`transition-all shrink-0 duration-300 flex items-center justify-center gap-2 w-full overflow-hidden  bg-BORDER-SUB ${show ? 'h-[50px]' : 'h-[0px]'}`}
+ aria-hidden={!show}
+ role="alert"
>
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 60486b0 and d10f7ae.

⛔ Files ignored due to path filters (2)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • src/components/common/Icon/icons/Close.svg is excluded by !**/*.svg
📒 Files selected for processing (15)
  • package.json (1 hunks)
  • src/apis/index.ts (1 hunks)
  • src/apis/notice.request.ts (1 hunks)
  • src/app/(with-tracker)/(auth-required)/layout.tsx (2 hunks)
  • src/app/layout.tsx (2 hunks)
  • src/components/auth-required/header/index.tsx (3 hunks)
  • src/components/auth-required/notice/Modal.tsx (1 hunks)
  • src/components/auth-required/notice/index.tsx (1 hunks)
  • src/components/common/Icon/icons/index.ts (1 hunks)
  • src/components/common/ModalProvider.tsx (1 hunks)
  • src/components/common/index.ts (1 hunks)
  • src/constants/paths.constant.ts (1 hunks)
  • src/hooks/useModal.tsx (1 hunks)
  • src/types/index.ts (1 hunks)
  • src/types/notice.type.ts (1 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
src/components/auth-required/notice/Modal.tsx

[error] 30-30: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🪛 ESLint
src/components/auth-required/header/index.tsx

[error] 15-15: @/hooks/useModal import should occur before import of ./Section

(import/order)

🔇 Additional comments (14)
src/components/common/Icon/icons/index.ts (1)

7-7: 아이콘 추가가 잘 이루어졌습니다.

이 변경은 Close 아이콘을 내보내도록 추가했으며, 공지사항 모달에서 사용될 것으로 보입니다. 아이콘 네이밍이 명확하고 기존 패턴을 잘 따르고 있습니다.

src/types/index.ts (1)

4-4: 타입 내보내기가 잘 구현되었습니다.

공지사항 관련 타입(notice.type)을 내보내기 추가함으로써 애플리케이션 전체에서 이 타입들을 사용할 수 있게 되었습니다. 이는 NotiListDto 타입을 활용하는 API 호출 및 UI 컴포넌트와 잘 통합됩니다.

src/components/common/index.ts (1)

2-2: 모달 프로바이더 내보내기가 적절히 추가되었습니다.

ModalProvider를 내보내기에 추가함으로써 애플리케이션 전체에서 모달 기능을 사용할 수 있게 되었습니다. 이 변경은 공지사항 기능에 필요한 모달 관리를 위해 적절하게 구현되었습니다.

src/apis/index.ts (1)

3-3: 공지사항 API 내보내기가 잘 구현되었습니다.

notice.request 모듈을 내보내기에 추가함으로써 notiList 함수가 애플리케이션 전체에서 접근 가능하게 되었습니다. 이 API 함수는 공지사항 데이터를 가져오는 데 사용되며, 다른 컴포넌트들과 잘 통합되어 있습니다.

package.json (1)

34-35: 상태 관리를 위한 zustand 라이브러리 추가

zustand 라이브러리가 추가되었네요. 이 라이브러리는 React 애플리케이션에서 상태 관리를 간단하게 할 수 있게 해주는 도구로, 공지사항 모달 상태 관리에 사용될 것으로 보입니다.

src/types/notice.type.ts (1)

1-8: 알림 데이터 타입 정의가 잘 되었습니다

공지사항 데이터를 위한 타입 정의가 명확하게 되어 있습니다. 각 게시물(post)에 필요한 id, title, content, created_at 필드가 모두 포함되어 있어 타입 안정성을 확보할 수 있습니다.

src/constants/paths.constant.ts (1)

4-4: 공지사항 경로 상수가 적절히 추가되었습니다

공지사항 데이터를 가져오기 위한 경로 상수 'NOTIS'가 적절하게 추가되었습니다. 경로를 상수로 관리하는 것은 코드 유지보수성을 높이는 좋은 방법입니다.

src/hooks/useModal.tsx (1)

1-17: 잘 구현된 모달 관리 훅입니다!

Zustand를 활용한 모달 상태 관리 구현이 깔끔하고 효율적입니다. 다양한 컴포넌트에서 모달을 쉽게 열고 닫을 수 있는 직관적인 API를 제공합니다.

한 가지 제안으로, 타입 안전성을 더 높이기 위해 Modal 타입을 더 구체적으로 정의할 수 있습니다:

- interface ModalType {
-   Modal?: React.ReactNode;
-   // eslint-disable-next-line no-unused-vars
-   open: (Modal: React.ReactNode) => void;
-   close: () => void;
- }

+ interface ModalType {
+   Modal?: React.ReactNode;
+   // eslint-disable-next-line no-unused-vars
+   open: (Modal: React.ReactNode) => void;
+   close: () => void;
+ }
src/app/layout.tsx (1)

9-13: 모달 프로바이더가 적절하게 통합되었습니다.

레이아웃에 ModalProvider를 추가한 방식이 깔끔하게 구현되었습니다. 프로바이더 계층 구조 내에 올바르게 배치되어 있어 애플리케이션 전반에서 모달 기능을 사용할 수 있습니다.

Also applies to: 48-48

src/components/auth-required/header/index.tsx (1)

35-35: Modal hook 적용 잘 되었습니다.

useModal hook을 활용한 Modal 상태 관리가 잘 구현되었습니다.

src/components/auth-required/notice/index.tsx (1)

10-11: 상수 정의 적절히 되었습니다

재사용 가능한 시간 관련 상수가 잘 정의되었습니다.

src/app/(with-tracker)/(auth-required)/layout.tsx (3)

5-7: import 정리가 잘 되었습니다

새로운 기능을 위한 import 추가가 잘 되었습니다.


21-24: notiList prefetch 구현이 좋습니다

공지사항 데이터를 미리 가져오는 prefetch 구현이 적절하게 되었습니다. 사용자 경험 향상에 도움이 됩니다.


27-35: 레이아웃 구조 변경이 잘 이루어졌습니다

HydrationBoundary와 함께 Notice 컴포넌트를 추가하는 레이아웃 구조 변경이 깔끔하게 이루어졌습니다. 사용자가 항상 최신 공지를 볼 수 있도록 적절한 위치에 배치되었습니다.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🔭 Outside diff range comments (1)
src/components/auth-required/notice/Modal.tsx (1)

37-39: ⚠️ Potential issue

중복된 닫는 태그를 제거해 주세요.

파일 끝부분에 불필요한 중복 닫는 태그들이 있습니다. 이 태그들은 삭제되어야 합니다.

  );
};

- );
- };
-
♻️ Duplicate comments (1)
src/components/auth-required/notice/Modal.tsx (1)

28-31: ⚠️ Potential issue

보안 취약점: HTML 콘텐츠 삽입 시 XSS 공격 위험이 있습니다.

dangerouslySetInnerHTML을 사용할 때 HTML 콘텐츠를 삽입하기 전에 반드시 적절한 살균(sanitize) 처리가 필요합니다. 현재 구현은 XSS(Cross-Site Scripting) 공격에 취약합니다.

다음과 같이 DOMPurify 같은 라이브러리를 사용하여 HTML을 살균하세요:

+ import DOMPurify from 'dompurify';

  <div
    className="text-TEXT-MAIN text-ST5"
-   dangerouslySetInnerHTML={{ __html: content }}
+   dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(content) }}
  ></div>

필요한 패키지를 설치하려면:

npm install dompurify
npm install --save-dev @types/dompurify
🧹 Nitpick comments (3)
src/components/auth-required/notice/Modal.tsx (3)

16-16: 오타 수정: 'items-cenetr'를 'items-center'로 수정하세요.

className 속성에 오타가 있습니다.

- <h2 className="text-TEXT-MAIN items-cenetr gap-3 text-T3">공지사항</h2>
+ <h2 className="text-TEXT-MAIN items-center gap-3 text-T3">공지사항</h2>

20-33: 데이터 로딩 및 오류 상태 처리를 추가하세요.

현재 코드는 데이터가 로드되는 동안의 상태나 오류가 발생했을 때의 처리가 없습니다. 더 나은 사용자 경험을 위해 로딩 및 오류 상태에 대한 UI 처리를 추가하는 것이 좋습니다.

- {data?.posts?.map(({ content, created_at, id, title }) => (
+ {data?.posts?.length === 0 ? (
+   <div className="text-TEXT-MAIN text-center">공지사항이 없습니다.</div>
+ ) : data?.posts?.map(({ content, created_at, id, title }) => (

또한 로딩 상태 처리를 위해 다음과 같이 변경할 수 있습니다:

- const { data } = useQuery({ queryKey: [PATHS.NOTIS], queryFn: notiList });
+ const { data, isLoading, isError } = useQuery({ queryKey: [PATHS.NOTIS], queryFn: notiList });

...

+ {isLoading && <div className="text-TEXT-MAIN text-center">로딩 중...</div>}
+ {isError && <div className="text-TEXT-MAIN text-center">오류가 발생했습니다.</div>}
  {data?.posts?.length === 0 ? (
    <div className="text-TEXT-MAIN text-center">공지사항이 없습니다.</div>
  ) : data?.posts?.map(({ content, created_at, id, title }) => (
🧰 Tools
🪛 Biome (1.9.4)

[error] 32-32: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)


24-26: 날짜 포맷팅을 위한 별도 함수 또는 라이브러리 사용을 고려하세요.

현재 날짜 포맷팅을 위해 문자열 split을 사용하고 있습니다. 이 방식은 간단하지만, 더 복잡한 날짜 포맷팅이 필요하거나 국제화가 필요한 경우 확장성이 떨어집니다. date-fns 또는 dayjs와 같은 라이브러리를 사용하거나 별도의 포맷팅 함수를 만드는 것이 좋습니다.

+ import { format } from 'date-fns';
+ import { ko } from 'date-fns/locale';

...

- {created_at.split('T')[0]}
+ {format(new Date(created_at), 'yyyy년 MM월 dd일', { locale: ko })}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d10f7ae and 495434f.

📒 Files selected for processing (2)
  • src/components/auth-required/notice/Modal.tsx (1 hunks)
  • src/components/auth-required/notice/Modal.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/auth-required/notice/Modal.tsx
🧰 Additional context used
🪛 Biome (1.9.4)
src/components/auth-required/notice/Modal.tsx

[error] 32-32: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🔇 Additional comments (1)
src/components/auth-required/notice/Modal.tsx (1)

9-36: 전체 컴포넌트 구조가 잘 설계되어 있습니다.

Modal 컴포넌트가 깔끔하게 구성되어 있으며, 데이터 패칭과 표시를 적절히 처리하고 있습니다. React Query를 활용한 데이터 요청 패턴도 좋은 접근법입니다.

🧰 Tools
🪛 Biome (1.9.4)

[error] 32-32: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

Copy link

@Jihyun3478 Jihyun3478 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋았던 점

  • 크게 이견은 없습니다!

PS. 회의 때 보여주셨던 공지 디자인 배포하면 드디어 볼 수 있는건가요?!👀🤭

Copy link
Member

@Nuung Nuung left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋았던 점

  • 모달 독립 컴포넌트, 핵심 기능에 맞춰서 잘 기능 개발, 구현된 것 같아서 아주 좋았습니다.
  • src/components/auth-required/notice/Modal.tsxdangerouslySetInnerHTML={{ __html: content }} 부분과 같이 요구 사항과 align 잘 맞춰서 구현해주신 디테일 좋아요~~

아쉬운 점

  • 코멘트 체크 부탁드려요!, 레거시에서 빠진 부분들도 (이벤트 트래킹) 들어가 있는 것 같아서 해당 부분 아쉽네요!
  • 배포 전 꼭 게시글을 써야할 것 같은데, 이런 부분도 코멘트나, task 등의 수단으로 기록이 남겨져 있으면 좋을 것 같습니다!

@six-standard
Copy link
Member Author

좋았던 점

  • 모달 독립 컴포넌트, 핵심 기능에 맞춰서 잘 기능 개발, 구현된 것 같아서 아주 좋았습니다.
  • src/components/auth-required/notice/Modal.tsxdangerouslySetInnerHTML={{ __html: content }} 부분과 같이 요구 사항과 align 잘 맞춰서 구현해주신 디테일 좋아요~~

아쉬운 점

  • 코멘트 체크 부탁드려요!, 레거시에서 빠진 부분들도 (이벤트 트래킹) 들어가 있는 것 같아서 해당 부분 아쉽네요!
  • 배포 전 꼭 게시글을 써야할 것 같은데, 이런 부분도 코멘트나, task 등의 수단으로 기록이 남겨져 있으면 좋을 것 같습니다!

작업 브랜치들을 전부 메인에서 생성해서 그렇습니다..
이전에 Squash Merge 방식으로 머지하면 브랜치를 중첩해서 생성하면 이전 브랜치에서 이미 작업했음에도 현재 브랜치의 머지 커밋 내역에 들어가는 경우가 있었어서 그 부분 막으려고 이런 방식으로 작업했습니다.

사실 좀 애매하긴 한데, 당장은 리팩토링 브랜치 머지 후에 공지사항 브랜치 머지하면서 충돌 해결할 계획입니다~

@six-standard six-standard requested a review from Nuung March 11, 2025 13:21
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🔭 Outside diff range comments (1)
src/components/auth-required/notice/index.tsx (1)

55-56: ⚠️ Potential issue

구문 오류 수정 필요

파일 끝에 불필요한 중괄호와 세미콜론이 있습니다. 이로 인해 구문 오류가 발생할 수 있습니다.

};
-};
-
🧹 Nitpick comments (3)
src/components/auth-required/notice/index.tsx (3)

20-20: 타입 안전성 개선 필요

as string 타입 단언을 사용하는 것보다 옵셔널 체이닝과 조건부 렌더링으로 타입 안전성을 개선할 수 있습니다.

- data?.posts[0].created_at?.split('T')[0] as string,
+ (data?.posts[0]?.created_at?.split('T')[0] || new Date().toISOString().split('T')[0]),

31-36: 접근성 개선 필요

알림 배너에 접근성(a11y)을 위한 속성이 부족합니다. 스크린 리더 및 키보드 사용자를 위한 적절한 ARIA 속성과 역할을 추가하는 것이 좋습니다.

<div
- className={`transition-all shrink-0 duration-300 flex items-center justify-center gap-2 w-full overflow-hidden  bg-BORDER-SUB ${show ? 'h-[50px]' : 'h-[0px]'}`}
+ className={`transition-all shrink-0 duration-300 flex items-center justify-center gap-2 w-full overflow-hidden bg-BORDER-SUB ${show ? 'h-[50px]' : 'h-[0px]'}`}
+ role="alert"
+ aria-live="polite"
>
- <h1 className="text-TEXT-MAIN text-ST4 max-MBI:text-ST5">
+ <h2 className="text-TEXT-MAIN text-ST4 max-MBI:text-ST5">
  📣 새로운 업데이트를 확인해보세요!
- </h1>
+ </h2>

37-50: 버튼 접근성 및 로딩 상태 개선 필요

버튼에 접근성 속성(예: aria-label)을 추가하고, 모달 열기 작업이 진행 중일 때 버튼 상태를 표시하는 것이 좋습니다.

<button
  className="text-PRIMARY-MAIN hover:text-PRIMARY-SUB text-ST4 transition-all duration-300 max-MBI:text-ST5"
+ aria-label="새 업데이트 확인하기"
  onClick={() => {
    setShow(false);
    localStorage.setItem(
      'noti_expiry',
      JSON.stringify(new Date().getTime() + TTL),
    );

    open(<Modal />);
  }}
>
  확인하기
</button>
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b152e84 and a672797.

📒 Files selected for processing (2)
  • src/components/auth-required/notice/index.tsx (1 hunks)
  • src/components/auth-required/notice/index.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
src/components/auth-required/notice/index.tsx (1)
Learnt from: Nuung
PR: Check-Data-Out/velog-dashboard-v2-fe#23
File: src/components/auth-required/notice/index.tsx:18-27
Timestamp: 2025-03-11T12:38:34.012Z
Learning: React 컴포넌트에서 API 응답 데이터를 처리할 때는 데이터가 없거나 배열이 비어있는 경우(전체 기능 비활성화, DB 마이그레이션, DBMS 연결 문제 등)에 대비한 방어적 코딩이 필요하다. 특히 `data?.posts[0]`과 같이 배열의 요소에 접근하기 전에 `if (!data?.posts || data.posts.length === 0) return;`와 같은 가드 클로즈를 사용하여 런타임 오류를 방지해야 한다.
🔇 Additional comments (1)
src/components/auth-required/notice/index.tsx (1)

18-27: 데이터 안전성 개선 필요

현재 구현은 data?.posts[0]에 직접 접근하여 신규 공지를 확인합니다. 데이터가 없거나 posts 배열이 비어있을 경우 오류가 발생할 수 있습니다. 이전 리뷰에서도 지적된 부분입니다.

useEffect(() => {
+   if (!data?.posts || data.posts.length === 0) return;
    
    const lastUpdated = new Date(
      data?.posts[0].created_at?.split('T')[0] as string,
    ).getTime();
    const lastDate = Math.ceil((new Date().getTime() - lastUpdated) / DAY);
    if (lastDate <= 4) {
      const expiry = localStorage.getItem('noti_expiry');
      if (!expiry || JSON.parse(expiry) < new Date().getTime()) setShow(true);
    }
  }, [data]);

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🔭 Outside diff range comments (1)
src/components/auth-required/notice/Modal.tsx (1)

36-46: ⚠️ Potential issue

코드 중복: 36-46 라인에 중복된 코드가 있습니다

파일 끝부분에 중복된 코드가 있습니다. 이 부분은 제거되어야 합니다.

- </div>
-          <div
-            className="text-TEXT-MAIN text-I4"
-            dangerouslySetInnerHTML={{ __html: content }}
-          ></div>
-        </div>
-      ))}
-    </div>
-  );
-};
-
🧰 Tools
🪛 Biome (1.9.4)

[error] 40-40: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

♻️ Duplicate comments (1)
src/components/auth-required/notice/Modal.tsx (1)

28-31: ⚠️ Potential issue

보안 취약점: HTML 콘텐츠 삽입 시 XSS 공격 위험이 있습니다.

dangerouslySetInnerHTML을 사용할 때 HTML 콘텐츠를 삽입하기 전에 반드시 적절한 살균(sanitize) 처리가 필요합니다. 현재 구현은 XSS(Cross-Site Scripting) 공격에 취약합니다.

다음과 같이 DOMPurify 같은 라이브러리를 사용하여 HTML을 살균하세요:

+ import DOMPurify from 'dompurify';

  <div
    className="text-TEXT-MAIN text-ST5"
-   dangerouslySetInnerHTML={{ __html: content }}
+   dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(content) }}
  ></div>

필요한 패키지를 설치하려면:

npm install dompurify
npm install --save-dev @types/dompurify
🧹 Nitpick comments (3)
src/components/auth-required/notice/Modal.tsx (3)

1-12: 컴포넌트 구조 및 데이터 요청이 잘 설정되어 있습니다.

React Query를 사용한 데이터 요청 방식이 깔끔하게 구현되어 있습니다. 다만, 데이터 로딩 상태나 에러 처리 로직이 구현되어 있지 않아 사용자 경험에 영향을 줄 수 있습니다.

다음과 같이 로딩 상태와 에러 처리를 추가하는 것이 좋겠습니다:

  const { close } = useModal();
- const { data } = useQuery({ queryKey: [PATHS.NOTIS], queryFn: notiList });
+ const { data, isLoading, error } = useQuery({ queryKey: [PATHS.NOTIS], queryFn: notiList });

16-16: 오타가 있습니다: "items-cenetr"

className에 오타가 있습니다.

- <h2 className="text-TEXT-MAIN items-cenetr gap-3 text-T3">공지사항</h2>
+ <h2 className="text-TEXT-MAIN items-center gap-3 text-T3">공지사항</h2>

20-33: 데이터가 없는 경우의 처리가 필요합니다

API에서 데이터를 가져오지 못하거나 posts 배열이 비어있는 경우에 대한 처리가 되어 있지 않습니다.

다음과 같이 데이터가 없는 경우에 대한 처리를 추가하는 것이 좋겠습니다:

- {data?.posts?.map(({ content, created_at, id, title }) => (
+ {data?.posts?.length > 0 ? (
+   data.posts.map(({ content, created_at, id, title }) => (
      <div key={id}>
        <div className="flex items-center gap-3">
          <h3 className="text-TEXT-MAIN text-T4">{title}</h3>
          <h4 className="text-TEXT-ALT text-T5">
            {created_at.split('T')[0]}
          </h4>
        </div>
        <div
          className="text-TEXT-MAIN text-ST5"
          dangerouslySetInnerHTML={{ __html: content }}
        ></div>
      </div>
-   ))}
+   ))
+ ) : (
+   <div className="text-TEXT-ALT text-center py-10">공지사항이 없습니다.</div>
+ )}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a672797 and bb0e6a3.

📒 Files selected for processing (3)
  • src/components/auth-required/notice/Modal.tsx (1 hunks)
  • src/components/auth-required/notice/Modal.tsx (1 hunks)
  • src/components/auth-required/notice/Modal.tsx (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/auth-required/notice/Modal.tsx
🔇 Additional comments (2)
src/components/auth-required/notice/Modal.tsx (2)

13-19: 모달 헤더 구현이 잘 되어 있습니다.

모달 닫기 기능과 타이틀이 명확하게, 사용성 있게 구현되어 있습니다.


20-27: 날짜 형식 처리에 유틸리티 함수 사용 검토

created_at.split('T')[0]와 같은 날짜 처리 로직은 유틸리티 함수로 추출하는 것이 코드 재사용성과 가독성을 높입니다. 이전 리뷰 코멘트에서도 이 사항이 언급되었습니다.

날짜 포맷팅을 위한 유틸리티 함수를 만들어 사용하는 것이 좋겠습니다:

// src/utils/date.ts 파일 생성
+ export const formatDate = (isoString: string): string => {
+   return isoString.split('T')[0];
+ };

// Modal.tsx에서 사용
+ import { formatDate } from '@/utils/date';

// 사용 부분
- {created_at.split('T')[0]}
+ {formatDate(created_at)}

Copy link
Member

@Nuung Nuung left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이스케이프 처리 (이벤트 핸들링) / 그리고 trackUserEvent 빼야하는거 아녜유?

Copy link

@cheonHwi cheonHwi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋았던 점

  • 모달 로직이 독립적으로 설계되어서 유지보수에 어려움이 없을거 같네요!

저도 ESC를 통한 모달 닫기? 가 있으면 좋겠다고 생각했는데, 이미 대응하셨네요. 별 다른 이견 없습니다 고생하셨어요!

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (5)
src/components/Providers/ModalProvider.tsx (1)

6-26: 모달 구현 로직 검토

모달 컴포넌트가 잘 구현되었습니다. 모달 외부 클릭 시 닫히는 기능과 배경 어둡게 처리 등 기본적인 모달 기능이 모두 포함되어 있습니다.

몇 가지 개선점을 제안합니다:

  1. 접근성(a11y) 개선을 위해 Escape 키 처리와 포커스 트랩 추가를 고려해보세요:
'use client';

- import { useRef } from 'react';
+ import { useRef, useEffect } from 'react';
import { useModal } from '@/hooks/useModal';

export const ModalProvider = () => {
  const { Modal, close } = useModal();
  const ref = useRef<HTMLDivElement | null>(null);

+ useEffect(() => {
+   const handleKeyDown = (e: KeyboardEvent) => {
+     if (e.key === 'Escape') close();
+   };
+ 
+   window.addEventListener('keydown', handleKeyDown);
+   return () => window.removeEventListener('keydown', handleKeyDown);
+ }, [close]);

  if (typeof window !== 'undefined' && Modal) {
    return (
      <div
        className="w-full h-screen bg-[#000000AA] absolute z-[100] flex items-center justify-center"
        onMouseDown={({ target }) => {
          if (ref.current && !ref.current.contains(target as Node)) close();
        }}
+       role="dialog"
+       aria-modal="true"
      >
        <div className="w-fit h-fit" ref={ref}>
          {Modal}
        </div>
      </div>
    );
  } else {
    return <></>;
  }
};
src/app/(auth-required)/components/header/index.tsx (1)

129-134: 공지사항 버튼 구현이 잘 되었습니다.

공지사항 버튼이 기존 UI 스타일과 일관성 있게 추가되었고, 모달 열기 기능이 적절하게 구현되었습니다. 다만, 다음과 같은 개선사항을 고려해보세요:

  • 접근성 향상을 위해 버튼에 aria-label 속성 추가
  • 호버 효과가 로그아웃 버튼과 다른데, 일관성을 위해 동일한 스타일을 적용하는 것이 좋을 수 있습니다
<button
-  className="text-TEXT-MAIN text-I3 p-5 max-MBI:p-4 flex whitespace-nowrap w-auto hover:bg-BG-ALT"
+  className="text-TEXT-MAIN text-I3 p-5 max-MBI:p-4 flex whitespace-nowrap w-auto"
+  aria-label="공지사항 열기"
  onClick={() => ModalOpen(<Modal />)}
>
  공지사항
</button>
src/app/(auth-required)/components/notice/Modal.tsx (1)

10-13: 모달 컴포넌트 설계가 적절합니다.

useModal 훅의 close 함수와 useQuery를 사용하여 데이터를 가져오는 로직이 잘 구현되었습니다. 다만, 다음 개선사항을 고려해보세요:

  • API 호출 실패 시 에러 처리 로직 추가
  • 로딩 상태 처리 추가
export const Modal = () => {
  const { close } = useModal();
-  const { data } = useQuery({ queryKey: [PATHS.NOTIS], queryFn: notiList });
+  const { data, isLoading, isError } = useQuery({ 
+    queryKey: [PATHS.NOTIS], 
+    queryFn: notiList 
+  });
src/app/(auth-required)/components/notice/index.tsx (2)

13-17: Notice 컴포넌트의 구조가 적절합니다.

useQuery를 사용하여 데이터를 가져오고, 상태 관리를 위한 useState와 모달 열기 기능을 위한 useModal이 적절하게 사용되었습니다. 다만, 다음 개선사항을 고려해보세요:

  • API 호출 실패 시 에러 처리 로직 추가
  • 로딩 상태 처리 추가
export const Notice = () => {
-  const { data } = useQuery({ queryKey: [PATHS.NOTIS], queryFn: notiList });
+  const { data, isError } = useQuery({ 
+    queryKey: [PATHS.NOTIS], 
+    queryFn: notiList,
+    onError: (error) => {
+      console.error('공지사항 데이터 가져오기 실패:', error);
+    }
+  });
  const [show, setShow] = useState(false);
  const { open } = useModal();

30-54: 공지사항 배너 UI 및 로직이 잘 구현되었습니다.

공지사항 배너의 UI와 동작 로직이 잘 구현되었습니다. 다만, 다음과 같은 개선을 고려해보세요:

  1. 웹 접근성 향상을 위한 ARIA 속성 추가
  2. 로컬 스토리지 오류 처리 추가
  3. 확인하기 버튼에 키보드 접근성 개선
return (
  <>
    <div
+      role="alert"
+      aria-live="polite"
      className={`transition-all shrink-0 duration-300 flex items-center justify-center gap-2 w-full overflow-hidden  bg-BORDER-SUB ${show ? 'h-[50px]' : 'h-[0px]'}`}
    >
      <h1 className="text-TEXT-MAIN text-ST4 max-MBI:text-ST5">
        📣 새로운 업데이트를 확인해보세요!
      </h1>
      <button
        className="text-PRIMARY-MAIN hover:text-PRIMARY-SUB text-ST4 transition-all duration-300 max-MBI:text-ST5"
+        aria-label="공지사항 확인하기"
        onClick={() => {
          setShow(false);
+          try {
            localStorage.setItem(
              'noti_expiry',
              JSON.stringify(new Date().getTime() + TTL),
            );
+          } catch (error) {
+            console.error('로컬 스토리지 저장 중 오류 발생:', error);
+          }

          open(<Modal />);
        }}
+        onKeyDown={(e) => {
+          if (e.key === 'Enter' || e.key === ' ') {
+            e.preventDefault();
+            setShow(false);
+            try {
+              localStorage.setItem(
+                'noti_expiry',
+                JSON.stringify(new Date().getTime() + TTL),
+              );
+            } catch (error) {
+              console.error('로컬 스토리지 저장 중 오류 발생:', error);
+            }
+            open(<Modal />);
+          }
+        }}
      >
        확인하기
      </button>
    </div>
  </>
);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bb0e6a3 and e201492.

⛔ Files ignored due to path filters (1)
  • src/components/Icon/icons/Close.svg is excluded by !**/*.svg
📒 Files selected for processing (8)
  • src/app/(auth-required)/components/header/index.tsx (3 hunks)
  • src/app/(auth-required)/components/notice/Modal.tsx (1 hunks)
  • src/app/(auth-required)/components/notice/index.tsx (1 hunks)
  • src/app/(auth-required)/layout.tsx (2 hunks)
  • src/app/layout.tsx (2 hunks)
  • src/components/Icon/icons/index.ts (1 hunks)
  • src/components/Providers/ModalProvider.tsx (1 hunks)
  • src/components/Providers/index.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/app/layout.tsx
🧰 Additional context used
🪛 Biome (1.9.4)
src/app/(auth-required)/components/notice/Modal.tsx

[error] 40-40: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🔇 Additional comments (13)
src/components/Icon/icons/index.ts (1)

7-7: Close 아이콘 추가로 UI 확장성 향상

Close 아이콘을 추가함으로써 모달 기능 구현에 필요한 UI 요소가 적절히 추가되었습니다.

src/components/Providers/index.ts (1)

3-3: ModalProvider 내보내기 추가

ModalProvider를 index 파일에서 내보내도록 하여 다른 모듈에서 쉽게 접근할 수 있게 되었습니다. 기존의 Provider들과 일관성 있게 구현되었습니다.

src/app/(auth-required)/layout.tsx (3)

5-6: 공지사항 관련 모듈 추가

공지사항 기능 구현을 위한 적절한 모듈 import가 추가되었습니다.


18-21: 공지사항 데이터 prefetch 구현

공지사항 데이터를 prefetch하여 초기 로딩 시간을 최적화한 좋은 구현입니다.


24-32:

Details

❓ Verification inconclusive

레이아웃 구조 변경 및 Notice 컴포넌트 통합

레이아웃 구조가 개선되었고 Notice 컴포넌트가 적절히 통합되었습니다. HydrationBoundary 내에 컴포넌트들을 배치하여 서버 데이터를 클라이언트에서 효과적으로 활용할 수 있도록 했습니다.

다만, 이벤트 추적에 대한 PR 코멘트에서 언급된 내용이 확인되지 않습니다. 배포 전에 이벤트 추적 관련 코드가 필요한지 검토해 보세요.


🌐 Web query:

React에서 이벤트 추적을 위한 best practice

💡 Result:

React에서 이벤트 추적을 위한 베스트 프랙티스는 관심사 분리와 선언적 프로그래밍 패턴을 중심으로 구성됩니다. 주요 접근 방식은 다음과 같습니다:

1. 관심사 분리 원칙

  • 비즈니스 로직과 추적 로직 분리: 이벤트 핸들러 내에서 직접 분석 함수 호출 대신, 상위 컴포넌트에서 이벤트 가로채기 패턴 사용[4][14]
// 부모 컴포넌트에서 자식의 이벤트 핸들러 확장
const TrackingWrapper = ({ children, onTrack }) => {
  const enhancedChildren = React.Children.map(children, child => {
    return React.cloneElement(child, {
      onClick: (e) => {
        child.props.onClick?.(e);
        onTrack(e);
      }
    });
  });
  return enhancedChildren;
};

2. 선언적 이벤트 추적

  • 컴포넌트 프로퍼티를 통한 명시적 추적: onTrack 같은 프로퍼티를 통해 추적 로직 전달[4][12]
<Button 
  onClick={handlePayment} 
  onTrack={() => trackEvent('payment_initiated')}
/>

3. 이벤트 전파 활용

  • 이벤트 버블링 메커니즘 활용: 중앙 집중식 이벤트 핸들링[1][5]
// 상위 컨테이너에서 포괄적 이벤트 처리
<div onClickCapture={(e) => {
  if (e.target.dataset.tracking) {
    analytics.track(e.target.dataset.tracking);
  }
}}>
  <Button data-tracking="cta_click">Click</Button>
</div>

4. 커스텀 훅 활용

  • 추적 로직 추상화: useTracking 훅을 만들어 재사용성 확보[15][14]
const useTracking = (eventName) => {
  const track = useCallback((payload) => {
    analytics.track(eventName, payload);
  }, [eventName]);

  return track;
};

// 사용 예시
const trackButtonClick = useTracking('button_click');

5. 이벤트 객체 확장

  • SyntheticEvent 활용: React의 이벤트 래퍼 사용[12][3]
const handleClick = (e) => {
  e.persist(); // 비동기 이벤트 처리 시 필요
  trackEvent('click', {
    element: e.currentTarget.tagName,
    coordinates: {x: e.clientX, y: e.clientY}
  });
};

6. 이벤트 전파 제어

  • stopPropagation vs stopImmediatePropagation: 이벤트 전파 단계 제어[1][5]
const handleClick = (e) => {
  e.stopPropagation(); // 리액트 트리 전파만 중지
  e.nativeEvent.stopImmediatePropagation(); // 기본 DOM 전파 중지
};
패턴 장점 사용 사례
HOC 래퍼 기존 컴포넌트 수정 불필요 공통 추적 로직 적용
커스텀 훅 로직 재사용성 향상 복잡한 추적 시나리오
이벤트 버블링 중앙 집중식 관리 페이지 전체 이벤트 추적
데이터 속성 선언적 설정 가능 동적 이벤트 추적

이러한 패턴들은 코드의 가독성을 유지하면서도 확장 가능한 이벤트 추적 시스템을 구축할 수 있게 해줍니다. 특히 이벤트 전파 메커니즘과 React의 컴포넌트 구성 방식을 결합하면, 애플리케이션 전반에 걸친 통합 추적 시스템을 구현할 수 있습니다[5][12].

Citations:


레이아웃 통합은 잘 반영되었으나, 이벤트 추적 구현 여부를 재검토해 주세요

  • 레이아웃 구조 변경과 Notice 컴포넌트 통합, HydrationBoundary 적용은 적절히 개선되었습니다.
  • 다만, PR 코멘트에서 언급한 이벤트 추적 관련 구현은 확인되지 않았습니다.
    • React의 이벤트 추적 베스트 프랙티스(관심사 분리, 선언적 이벤트 추적, HOC 또는 커스텀 훅 활용 등)를 참고하여, 배포 전에 이벤트 추적에 필요한 코드 추가 여부를 검토해 주시기 바랍니다.
src/app/(auth-required)/components/header/index.tsx (3)

12-14: 새로운 의존성 추가가 적절합니다.

모달 기능에 필요한 useModal 훅과 Modal 컴포넌트를 가져오는 import 구문이 깔끔하게 추가되었습니다.


33-33: useModal 훅의 open 함수를 적절하게 활용했습니다.

open 함수를 ModalOpen으로 이름을 변경하여 사용함으로써 코드의 가독성을 높였습니다.


122-122: 스타일 변경이 적절하게 이루어졌습니다.

드롭다운 메뉴의 스타일 변경이 일관되게 적용되었습니다.

src/app/(auth-required)/components/notice/Modal.tsx (3)

3-9: 의존성 import가 잘 구성되었습니다.

필요한 의존성들이 깔끔하게 import 되었습니다.


14-19: Escape 키 이벤트 처리가 적절합니다.

모달을 닫기 위한 키보드 이벤트 처리가 잘 구현되었습니다. 클린업 함수도 올바르게 추가되었습니다.


21-28: 모달 헤더 스타일 및 구조가 잘 구현되었습니다.

모달 헤더의 레이아웃과 스타일이 잘 구현되었습니다. 클로즈 버튼도 추가되어 사용자 경험이 좋습니다.

src/app/(auth-required)/components/notice/index.tsx (2)

3-9: import 문이 잘 구성되어 있습니다.

필요한 의존성들이 깔끔하게 import 되었습니다.


10-12: 상수 정의가 명확합니다.

DAYTTL 상수를 활용해 코드의 가독성을 높였습니다.

@six-standard six-standard requested a review from Nuung March 15, 2025 02:34
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (3)
src/app/(auth-required)/components/notice/Modal.tsx (3)

12-12: 🛠️ Refactor suggestion

로딩 및 에러 상태 처리가 누락되었습니다.

API 호출 시 로딩 및 에러 상태에 대한 처리가 없습니다. 이로 인해 데이터 로딩 중이거나 오류 발생 시 사용자 경험이 저하될 수 있습니다.

useQuery에서 isLoading과 isError 상태를 추출하여 적절한 UI를 제공하세요:

- const { data } = useQuery({ queryKey: [PATHS.NOTIS], queryFn: notiList });
+ const { data, isLoading, isError } = useQuery({ queryKey: [PATHS.NOTIS], queryFn: notiList });

그리고 해당 상태에 따라 UI를 조건부로 렌더링하세요:

{isLoading && (
  <div className="flex items-center justify-center h-full">
    <p className="text-TEXT-ALT">로딩 중...</p>
  </div>
)}

{isError && (
  <div className="flex items-center justify-center h-full">
    <p className="text-DESTRUCTIVE-MAIN">공지사항을 불러오는 중 오류가 발생했습니다.</p>
  </div>
)}

{!isLoading && !isError && data?.posts?.length === 0 && (
  <div className="flex items-center justify-center h-full">
    <p className="text-TEXT-ALT">표시할 공지사항이 없습니다.</p>
  </div>
)}

21-28: 🛠️ Refactor suggestion

모달의 접근성을 개선해야 합니다.

현재 모달은 기본적인 접근성 속성이 누락되어 있습니다. 스크린 리더 사용자가 모달을 인식하고 제대로 상호작용할 수 있도록 ARIA 속성을 추가해야 합니다.

다음과 같이 수정하세요:

  return (
-   <div className="w-[800px] h-[500px] max-MBI:w-[450px] max-MBI:h-[200px] overflow-auto flex flex-col gap-3 p-10 max-MBI:p-7 rounded-md bg-BG-SUB">
+   <div 
+     role="dialog"
+     aria-labelledby="modal-title"
+     className="w-[800px] h-[500px] max-MBI:w-[450px] max-MBI:h-[200px] overflow-auto flex flex-col gap-3 p-10 max-MBI:p-7 rounded-md bg-BG-SUB">
      <div className="flex items-center justify-between">
-       <h2 className="text-TEXT-MAIN items-cenetr gap-3 text-T3 max-MBI:text-T4">
+       <h2 id="modal-title" className="text-TEXT-MAIN items-center gap-3 text-T3 max-MBI:text-T4">
          공지사항
        </h2>
-       <Icon name="Close" onClick={close} className="cursor-pointer" />
+       <Icon name="Close" onClick={close} className="cursor-pointer" aria-label="모달 닫기" />
      </div>

그리고 오타를 수정했습니다 (items-cenetritems-center).


30-43: ⚠️ Potential issue

XSS 취약점 및 데이터 존재 여부 확인이 필요합니다.

  1. dangerouslySetInnerHTML은 XSS 공격에 취약하므로 보안 이슈가 발생할 수 있습니다.
  2. data?.posts 확인 후 바로 map을 사용하면 posts가 빈 배열일 경우 UI 처리가 누락됩니다.

아래와 같이 변경하세요:

- {data?.posts?.map(({ content, created_at, id, title }) => (
+ {!isLoading && !isError && data?.posts?.length > 0 && data.posts.map(({ content, created_at, id, title }) => (
    <div key={id} className="flex flex-col gap-2">
      <div className="flex items-center gap-3">
        <h3 className="text-TEXT-MAIN text-T4 max-MBI:text-T5">{title}</h3>
        <h4 className="text-TEXT-ALT text-T5 max-MBI:text-ST5">
          {created_at.split('T')[0]}
        </h4>
      </div>
      <div
        className="text-TEXT-MAIN text-I4 prose"
-       dangerouslySetInnerHTML={{ __html: content }}
+       dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(content) }}
      />
    </div>
  ))}

DOMPurify를 사용하기 위해서는 패키지를 설치하고 임포트해야 합니다:

npm install dompurify
npm install @types/dompurify --save-dev
import DOMPurify from 'dompurify';

또는 html-react-parser를 사용하는 방법도 있습니다:

npm install html-react-parser
+ import parse from 'html-react-parser';

// ...

- <div
-   className="text-TEXT-MAIN text-I4 prose"
-   dangerouslySetInnerHTML={{ __html: content }}
- />
+ <div className="text-TEXT-MAIN text-I4 prose">
+   {parse(content)}
+ </div>

보안을 위해 API에서 받아오는 HTML 콘텐츠가 신뢰할 수 있는 소스인지 확인하세요.

🧰 Tools
🪛 Biome (1.9.4)

[error] 40-40: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🧹 Nitpick comments (1)
src/app/(auth-required)/components/notice/Modal.tsx (1)

24-26: 텍스트의 오타를 수정해야 합니다.

'items-cenetr'은 오타입니다. 'items-center'로 수정해야 합니다.

다음과 같이 오타를 수정하세요:

- <h2 className="text-TEXT-MAIN items-cenetr gap-3 text-T3 max-MBI:text-T4">
+ <h2 className="text-TEXT-MAIN items-center gap-3 text-T3 max-MBI:text-T4">
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e201492 and 7068c29.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (3)
  • package.json (2 hunks)
  • src/app/(auth-required)/components/notice/Modal.tsx (1 hunks)
  • tailwind.config.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • package.json
🧰 Additional context used
🪛 Biome (1.9.4)
src/app/(auth-required)/components/notice/Modal.tsx

[error] 40-40: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🔇 Additional comments (2)
tailwind.config.ts (1)

2-2: @tailwindcss/typography 플러그인 추가 승인

Typography 플러그인을 추가한 것은 좋은 선택입니다. 이 플러그인은 HTML 콘텐츠를 렌더링할 때 (특히 dangerouslySetInnerHTML을 사용할 때) 깔끔한 타이포그래피 스타일을 제공하므로 공지사항 텍스트 표시에 적합합니다.

Also applies to: 25-25

src/app/(auth-required)/components/notice/Modal.tsx (1)

22-22: 모달 백그라운드 오버레이가 누락되었습니다.

현재 모달은 컨테이너만 있고 배경 오버레이가 누락되어 있습니다. 이로 인해 사용자 경험이 저하되고 모달의 시각적 위계가 명확하지 않을 수 있습니다.

모달 컴포넌트를 수정하여 배경 오버레이를 추가하세요:

return (
+  <div 
+    className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
+    onClick={(e) => {
+      if (e.target === e.currentTarget) close();
+    }}
+  >
    <div className="w-[800px] h-[500px] max-MBI:w-[450px] max-MBI:h-[200px] overflow-auto flex flex-col gap-3 p-10 max-MBI:p-7 rounded-md bg-BG-SUB">
      {/* 기존 모달 내용 */}
    </div>
+  </div>
);

이렇게 하면 모달 외부를 클릭했을 때 모달이 닫히는 기능도 추가됩니다.

fontSize: FONTS,
},
plugins: [],
plugins: [typograpy()],
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

플러그인 사용에 오타가 있습니다.

import에서와 같은 오타가 여기에도 있습니다. 올바른 이름은 typography입니다.

다음과 같이 수정해주세요:

-  plugins: [typograpy()],
+  plugins: [typography()],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
plugins: [typograpy()],
plugins: [typography()],

@@ -1,4 +1,5 @@
import type { Config } from 'tailwindcss';
import typograpy from '@tailwindcss/typography';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

import 이름에 오타가 있습니다.

typograpy에 오타가 있습니다. 올바른 이름은 typography입니다.

다음과 같이 수정해주세요:

-import typograpy from '@tailwindcss/typography';
+import typography from '@tailwindcss/typography';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import typograpy from '@tailwindcss/typography';
import typography from '@tailwindcss/typography';

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/app/(auth-required)/components/notice/Modal.tsx (1)

39-40: ⚠️ Potential issue

XSS 보안 취약점이 있습니다.

dangerouslySetInnerHTML을 사용하면 XSS(Cross-Site Scripting) 공격에 취약해질 수 있습니다. API에서 받은 HTML 콘텐츠가 신뢰할 수 있는 소스에서 왔더라도, 안전한 렌더링을 위해 HTML 샌드박스 라이브러리를 사용하는 것이 좋습니다.

다음과 같이 개선해 보세요:

- <div
-   className="text-TEXT-MAIN text-I4 prose"
-   dangerouslySetInnerHTML={{ __html: content }}
- />
+ import DOMPurify from 'dompurify';
+ // 또는
+ import parse from 'html-react-parser';
+ 
+ // DOMPurify 사용 시
+ <div className="text-TEXT-MAIN text-I4 prose"
+   dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(content) }}
+ />
+ 
+ // html-react-parser 사용 시
+ <div className="text-TEXT-MAIN text-I4 prose">
+   {parse(content)}
+ </div>
🧰 Tools
🪛 Biome (1.9.4)

[error] 40-40: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🧹 Nitpick comments (3)
src/app/(auth-required)/components/notice/Modal.tsx (3)

35-35: 날짜 형식 처리 방식을 개선하세요.

created_at.split('T')[0]는 간단하지만 제한적인 방식입니다. 더 사용자 친화적인 날짜 형식을 위해 Intl.DateTimeFormat이나 날짜 라이브러리 사용을 고려하세요.

- {created_at.split('T')[0]}
+ {new Intl.DateTimeFormat('ko-KR', { 
+   year: 'numeric', 
+   month: 'long', 
+   day: 'numeric' 
+ }).format(new Date(created_at))}

12-12: API 요청 실패에 대한 오류 처리가 필요합니다.

현재 코드는 API 요청이 실패할 경우에 대한 처리가 없습니다. useQuery에서 제공하는 isLoading, isError 상태와 error 객체를 활용하여 더 완전한 사용자 경험을 제공하세요.

- const { data } = useQuery({ queryKey: [PATHS.NOTIS], queryFn: notiList });
+ const { data, isLoading, isError, error } = useQuery({ 
+   queryKey: [PATHS.NOTIS], 
+   queryFn: notiList,
+   retry: 1,
+   onError: (err) => {
+     console.error('공지사항 데이터 로딩 실패:', err);
+     // 선택적으로 에러 로깅 또는 알림 기능 추가
+   }
+ });

24-24: 오타가 발견되었습니다.

items-cenetr에 오타가 있습니다. 올바른 클래스명은 items-center입니다.

- <h2 className="text-TEXT-MAIN items-cenetr gap-3 text-T3 max-MBI:text-T4">
+ <h2 className="text-TEXT-MAIN items-center gap-3 text-T3 max-MBI:text-T4">
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7068c29 and f62c912.

📒 Files selected for processing (2)
  • src/app/(auth-required)/components/notice/Modal.tsx (1 hunks)
  • src/app/(auth-required)/components/notice/index.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/app/(auth-required)/components/notice/index.tsx
🧰 Additional context used
🪛 Biome (1.9.4)
src/app/(auth-required)/components/notice/Modal.tsx

[error] 40-40: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🔇 Additional comments (1)
src/app/(auth-required)/components/notice/Modal.tsx (1)

21-45: 모달 UI 개선 사항을 제안합니다.

현재 모달 구현에 다음과 같은 개선이 필요합니다:

  1. 로딩, 에러, 데이터 없음 상태에 대한 UI 처리가 없습니다.
  2. 웹 접근성을 위한 ARIA 속성이 누락되었습니다.
  3. 모달 배경 오버레이가 없어 UX가 저하됩니다.
return (
-  <div className="w-[800px] h-[500px] max-MBI:w-[450px] max-MBI:h-[200px] overflow-auto flex flex-col gap-3 p-10 max-MBI:p-7 rounded-md bg-BG-SUB">
+  <div 
+    role="dialog" 
+    aria-labelledby="modal-title"
+    className="w-[800px] h-[500px] max-MBI:w-[450px] max-MBI:h-[200px] overflow-auto flex flex-col gap-3 p-10 max-MBI:p-7 rounded-md bg-BG-SUB"
+  >
    <div className="flex items-center justify-between">
-      <h2 className="text-TEXT-MAIN items-cenetr gap-3 text-T3 max-MBI:text-T4">
+      <h2 id="modal-title" className="text-TEXT-MAIN items-center gap-3 text-T3 max-MBI:text-T4">
        공지사항
      </h2>
-      <Icon name="Close" onClick={close} className="cursor-pointer" />
+      <Icon name="Close" onClick={close} className="cursor-pointer" aria-label="모달 닫기" />
    </div>

+    {data === undefined && (
+      <div className="flex items-center justify-center h-full">
+        <p className="text-TEXT-ALT">로딩 중...</p>
+      </div>
+    )}

+    {data === null && (
+      <div className="flex items-center justify-center h-full">
+        <p className="text-DESTRUCTIVE-MAIN">공지사항을 불러오는 중 오류가 발생했습니다.</p>
+      </div>
+    )}

+    {data?.posts?.length === 0 && (
+      <div className="flex items-center justify-center h-full">
+        <p className="text-TEXT-ALT">표시할 공지사항이 없습니다.</p>
+      </div>
+    )}

-    {data?.posts?.map(({ content, created_at, id, title }) => (
+    {data?.posts && data.posts.length > 0 && data.posts.map(({ content, created_at, id, title }) => (
🧰 Tools
🪛 Biome (1.9.4)

[error] 40-40: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

@six-standard six-standard merged commit 038347f into main Mar 15, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants