Skip to content

Conversation

@YouD0313
Copy link
Collaborator

@YouD0313 YouD0313 commented May 9, 2025

구현내용

공지사항 ui, api연결

연관이슈

close #276

Summary by CodeRabbit

  • 신규 기능

    • 고객센터에 공지사항 목록 및 상세 조회 기능이 추가되었습니다.
    • 공지사항 상세 페이지에서 이전/다음 공지로 이동할 수 있는 네비게이션이 제공됩니다.
    • 공지사항 목록에서 검색 및 페이지네이션이 지원됩니다.
  • 스타일

    • 공지사항 및 상세, FAQ, 문의 등 고객센터 관련 UI 스타일이 개선되었습니다.
  • 버그 수정

    • 문의 카테고리 선택 시 아이콘 회전 동작이 정상적으로 동작하도록 수정되었습니다.
  • 리팩터

    • 페이지네이션 컴포넌트가 외부에서 제어 가능한 구조로 개선되었습니다.
  • 기타

    • React Query Devtools가 앱 전체에서 사용할 수 있도록 위치가 조정되었습니다.

@YouD0313 YouD0313 self-assigned this May 9, 2025
@YouD0313 YouD0313 added ⛏기능 기능 구현에 대한 라벨 🛠개선 성능 개선/리팩토링에 대한 라벨 labels May 9, 2025
@YouD0313 YouD0313 linked an issue May 9, 2025 that may be closed by this pull request
2 tasks
@coderabbitai
Copy link

coderabbitai bot commented May 9, 2025

"""

Walkthrough

공지사항(Notice) 기능이 새롭게 추가되었습니다. 공지사항 목록 및 상세 조회를 위한 API 연동, 관련 데이터 모델 정의, React 컴포넌트 및 스타일 컴포넌트 구현, React Query 기반 데이터 패칭 커스텀 훅, 라우트 추가 등이 포함되어 있습니다. 또한, Pagination 컴포넌트가 외부 제어형으로 리팩터링되었습니다.

Changes

파일/경로 요약 변경 요약
src/api/customerService.api.ts, src/hooks/useGetNotice.ts, src/hooks/useGetNoticeDetail.ts 공지사항 목록 및 상세 조회 API 함수와 이를 활용하는 React Query 기반 커스텀 훅 추가
src/models/customerService.ts 공지사항 목록, 상세, 검색 파라미터 등 관련 타입 및 인터페이스 추가
src/pages/customerService/notice/Notice.tsx, src/pages/customerService/notice/Notice.styled.ts 공지사항 페이지 UI 및 스타일 구현, 상태 관리 및 데이터 패칭, 페이지네이션 연동
src/pages/customerService/noticeDetail/NoticeDetail.tsx, src/pages/customerService/noticeDetail/NoticeDetail.styled.ts 공지사항 상세 페이지 및 스타일 컴포넌트 추가
src/components/customerService/notice/NoticeList.tsx, src/components/customerService/notice/NoticeList.styled.ts 공지사항 리스트 아이템 컴포넌트 및 스타일 추가
src/components/customerService/noticeDetail/** 공지사항 상세 헤더, 본문, 하단 내비게이션 등 상세 UI 컴포넌트 및 스타일 일괄 추가
src/routes/AppRoutes.tsx, src/constants/routes.ts 공지사항 상세 라우트 및 라우트 상수 추가
src/hooks/queries/keys.ts React Query 키에 noticeDetail 추가
src/components/common/pagination/Pagination.tsx Pagination 컴포넌트를 외부 제어형(props 기반)으로 리팩터링
src/components/home/projectCardLists/ProjectCardLists.tsx Pagination 컴포넌트 prop 변경에 따른 연동 코드 수정
src/App.tsx, src/pages/home/Home.tsx, src/pages/mypage/MyPage.tsx React Query Devtools 위치 조정 및 불필요한 import 제거
src/components/customerService/inquiry/Inquiry.styled.ts, src/components/customerService/faq/FAQContent.styled.ts 일부 스타일 prop/값 수정(내부 prop명 정정, padding/font-size 조정)
src/components/mypage/activityLog/inquiries/Inquiries.tsx, src/pages/customerService/faq/FAQ.tsx 기존 styled ContentBorder 컴포넌트를 공통 컴포넌트로 교체

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant NoticePage
    participant useGetNotice
    participant getNotice(API)
    participant NoticeList

    User->>NoticePage: 페이지 접속/검색/페이지 이동
    NoticePage->>useGetNotice: 공지사항 데이터 요청
    useGetNotice->>getNotice: API 호출
    getNotice->>useGetNotice: 데이터 반환
    useGetNotice->>NoticePage: noticeData, isLoading 반환
    NoticePage->>NoticeList: 데이터 전달 및 렌더링
Loading
sequenceDiagram
    participant User
    participant NoticeDetailPage
    participant useGetNoticeDetail
    participant getNoticeDetail(API)
    participant NoticeDetailBundle

    User->>NoticeDetailPage: 공지사항 상세 진입
    NoticeDetailPage->>useGetNoticeDetail: 상세 데이터 요청
    useGetNoticeDetail->>getNoticeDetail: API 호출
    getNoticeDetail->>useGetNoticeDetail: 상세 데이터 반환
    useGetNoticeDetail->>NoticeDetailPage: noticeDetail, isLoading 반환
    NoticeDetailPage->>NoticeDetailBundle: 상세 데이터 전달 및 렌더링
Loading

Assessment against linked issues

Objective (이슈 번호) Addressed Explanation
공지사항 UI 구현 (#276)
공지사항 API 연결 (#276)

Suggested reviewers

  • layout-SY
  • hyeongjun6364

Poem

🐇
새소식 전하는 공지의 길,
토끼가 뛰며 코드를 빚지.
목록과 상세, 페이지도 척척!
API도 척척, 스타일도 번쩍!
DevPals의 소식, 모두에게 쏙쏙!
📝✨
"""


📜 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 e9a730f and c9cffa0.

📒 Files selected for processing (2)
  • src/components/customerService/noticeDetail/NoticeDetailBundle.tsx (1 hunks)
  • src/components/customerService/noticeDetail/content/NoticeDetailContent.styled.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/components/customerService/noticeDetail/NoticeDetailBundle.tsx
  • src/components/customerService/noticeDetail/content/NoticeDetailContent.styled.ts
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: accessibility-test
✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 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.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

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 generate sequence diagram to generate a sequence diagram of the changes in 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.

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: 10

🧹 Nitpick comments (15)
src/components/customerService/noticeDetail/header/NoticeDetailHeader.tsx (1)

1-11: 헤더 컴포넌트가 잘 구현되었습니다.

NoticeDetailHeader 컴포넌트가 적절히 구현되었으나, 타이틀을 하드코딩하는 대신 props로 받는 것을 고려해 보세요. 이렇게 하면 다국어 지원이나 타이틀 변경이 필요할 때 더 유연하게 대응할 수 있습니다.

-export default function NoticeDetailHeader() {
+interface NoticeDetailHeaderProps {
+  title?: string;
+}
+
+export default function NoticeDetailHeader({ title = 'DevPals 공지사항' }: NoticeDetailHeaderProps) {
  return (
    <S.Container>
      <S.WrapperTitle>
-        <S.Title>DevPals 공지사항</S.Title>
+        <S.Title>{title}</S.Title>
      </S.WrapperTitle>
    </S.Container>
  );
}
src/components/customerService/notice/NoticeList.tsx (1)

5-7: props 네이밍을 개선해 주세요.

list라는 prop 이름은 배열을 의미하는 것 같지만 실제로는 단일 Notice 객체를 받고 있습니다. notice와 같은 더 명확한 이름으로 변경하는 것이 좋습니다.

interface NoticeProps {
-  list: Notice;
+  notice: Notice;
}
src/hooks/useGetNoticeDetail.ts (1)

5-11: 캐시 설정에 대한 고려 필요

staleTimegcTime을 모두 Infinity로 설정하면 데이터가 절대 새로고침되지 않고 메모리에서 제거되지 않습니다. 이는 의도된 설계일 수 있으나, 사용자가 많은 다른 공지사항을 조회할 경우 메모리 문제를 일으킬 수 있습니다.

적절한 타임아웃 값을 설정하거나, 특정 조건에서 쿼리를 무효화하는 로직을 추가하는 것이 좋습니다:

- staleTime: Infinity,
- gcTime: Infinity,
+ staleTime: 5 * 60 * 1000, // 5분
+ gcTime: 10 * 60 * 1000, // 10분
src/components/customerService/noticeDetail/header/NoticeDetailHeader.styled.ts (1)

16-16: 스타일 속성 추가 고려

현재 Title 컴포넌트에는 스타일 속성이 정의되어 있지 않습니다. 다른 파일의 Title 컴포넌트들에는 폰트 크기와 두께가 정의되어 있습니다. 일관성을 위해 스타일 속성 추가를 고려해보세요.

-export const Title = styled.h1``;
+export const Title = styled.h1`
+  font-size: 1.5rem;
+  font-weight: 700;
+`;
src/pages/customerService/notice/Notice.tsx (3)

12-14: 상태 관리 개선 가능성

현재 키워드 관련 상태를 두 개의 state 변수(keywordvalue)로 관리하고 있습니다. 이는 동기화 문제가 발생할 가능성이 있습니다. 하나의 상태로 통합하는 것이 더 안전할 수 있습니다.

-const [keyword, setKeyword] = useState<SearchKeyword>({ keyword: '' });
-const [value, setValue] = useState<string>('');
+const [search, setSearch] = useState<{query: SearchKeyword, inputValue: string}>({
+  query: { keyword: '' },
+  inputValue: ''
+});

// handleGetKeyword 함수도 수정
const handleGetKeyword = (keyword: string) => {
-  setKeyword({ keyword });
-  setValue(keyword);
+  setSearch({
+    query: { keyword },
+    inputValue: keyword
+  });
};

21-27: 로딩 상태 처리 개선 가능성

로딩 상태에서도 CustomerServiceHeader를 렌더링하는 것이 더 일관된 사용자 경험을 제공할 수 있습니다. 현재는 로딩 중일 때 헤더가 보이지 않습니다.

if (isLoading) {
  return (
-    <S.SpinnerWrapper>
-      <Spinner />
-    </S.SpinnerWrapper>
+    <>
+      <CustomerServiceHeader
+        title='공지사항'
+        keyword={value}
+        onGetKeyword={handleGetKeyword}
+      />
+      <S.Container>
+        <S.SpinnerWrapper>
+          <Spinner />
+        </S.SpinnerWrapper>
+      </S.Container>
+    </>
  );
}

4-4: 에러 처리 메커니즘 추가 고려

useGetNotice 훅에서 API 호출 실패 시 에러 처리를 위한 메커니즘이 없습니다. isErrorerror 값을 받아와서 에러 상태를 처리하는 로직을 추가하는 것이 좋을 것 같습니다.

-const { noticeData, isLoading } = useGetNotice(keyword);
+const { noticeData, isLoading, isError, error } = useGetNotice(keyword);

// 에러 처리 로직 추가
+if (isError) {
+  return (
+    <>
+      <CustomerServiceHeader
+        title='공지사항'
+        keyword={value}
+        onGetKeyword={handleGetKeyword}
+      />
+      <S.Container>
+        <S.ErrorMessage>데이터를 불러오는 중 오류가 발생했습니다. 다시 시도해주세요.</S.ErrorMessage>
+      </S.Container>
+    </>
+  );
+}

에러 메시지를 위한 스타일 컴포넌트도 추가해야 합니다.

src/components/customerService/notice/NoticeList.styled.ts (1)

24-26: 전역 Date 객체와 이름이 충돌됩니다.

Date라는 컴포넌트 이름은 JavaScript의 전역 Date 객체와 이름이 충돌될 수 있어 혼란을 일으킬 수 있습니다.

다음과 같이 더 명확한 이름을 사용하는 것이 좋습니다:

-export const Date = styled.span`
+export const DateText = styled.span`
  font-size: 1.1rem;
`;
🧰 Tools
🪛 Biome (1.9.4)

[error] 24-24: Do not shadow the global "Date" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)

src/pages/customerService/notice/Notice.styled.ts (1)

22-22: NoticeDetailLink 스타일링이 비어 있습니다.

NoticeDetailLink 컴포넌트에 스타일이 정의되어 있지 않습니다. 텍스트 색상, 호버 효과 등의 스타일링을 추가하는 것이 좋습니다.

-export const NoticeDetailLink = styled(Link)``;
+export const NoticeDetailLink = styled(Link)`
+  color: ${({ theme }) => theme.color.text};
+  text-decoration: none;
+  
+  &:hover {
+    text-decoration: underline;
+  }
+`;
src/components/customerService/noticeDetail/bottom/button/OtherNoticeButton.styled.ts (1)

26-26: OtherNoticeTitle 컴포넌트에 스타일 정의가 필요합니다.

OtherNoticeTitle 컴포넌트에 스타일이 정의되어 있지 않습니다. 필요한 스타일링(예: 말줄임표, 최대 너비 제한 등)을 추가하는 것이 좋습니다.

-export const OtherNoticeTitle = styled.span``;
+export const OtherNoticeTitle = styled.span`
+  max-width: 250px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+`;
src/components/customerService/noticeDetail/bottom/NoticeDetailBottom.styled.ts (2)

4-4: Container 컴포넌트에 스타일 정의가 필요합니다.

Container 컴포넌트에 스타일이 정의되어 있지 않습니다. nav 요소로 정의되어 있으므로 적절한 스타일링(예: 여백, 너비 등)을 추가하는 것이 좋습니다.

-export const Container = styled.nav``;
+export const Container = styled.nav`
+  width: 100%;
+  margin-top: 2rem;
+`;

37-37: ListTitle 컴포넌트에 스타일 정의가 필요합니다.

ListTitle 컴포넌트에 스타일이 정의되어 있지 않습니다. 필요한 스타일링(예: 폰트 크기, 굵기 등)을 추가하는 것이 좋습니다.

-export const ListTitle = styled.span``;
+export const ListTitle = styled.span`
+  font-size: 0.9rem;
+  font-weight: 500;
+`;
src/components/customerService/noticeDetail/content/NoticeDetailContent.styled.ts (3)

18-21: AdminImg 접근성 고려
이미지 크기를 2rem으로 고정한 것은 깔끔하지만, 사용할 때 반드시 alt 속성을 전달해 접근성을 보장하세요.


38-38: ViewWrapper 불필요 여부 검토
현재 스타일이 비어 있으므로 wrapper 역할이 모호합니다. 향후 스타일 추가가 없다면 제거를 고려하거나 필요한 스타일을 명시하세요.


58-64: ContentBorder 중복 추출 제안
다른 모듈(NoticeDetailBottom, Notice.styled.ts)에서도 동일한 ContentBorder 스타일을 정의하고 있습니다. 공통 컴포넌트로 추출하면 중복을 줄이고 유지보수성을 높일 수 있습니다.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 4c6fc37 and 664778d.

📒 Files selected for processing (23)
  • src/api/customerService.api.ts (2 hunks)
  • src/components/customerService/inquiry/Inquiry.styled.ts (1 hunks)
  • src/components/customerService/notice/NoticeList.styled.ts (1 hunks)
  • src/components/customerService/notice/NoticeList.tsx (1 hunks)
  • src/components/customerService/noticeDetail/NoticeDetailBundle.styled.ts (1 hunks)
  • src/components/customerService/noticeDetail/NoticeDetailBundle.tsx (1 hunks)
  • src/components/customerService/noticeDetail/bottom/NoticeDetailBottom.styled.ts (1 hunks)
  • src/components/customerService/noticeDetail/bottom/NoticeDetailBottom.tsx (1 hunks)
  • src/components/customerService/noticeDetail/bottom/button/OtherNoticeButton.styled.ts (1 hunks)
  • src/components/customerService/noticeDetail/bottom/button/OtherNoticeButton.tsx (1 hunks)
  • src/components/customerService/noticeDetail/content/NoticeDetailContent.styled.ts (1 hunks)
  • src/components/customerService/noticeDetail/content/NoticeDetailContent.tsx (1 hunks)
  • src/components/customerService/noticeDetail/header/NoticeDetailHeader.styled.ts (1 hunks)
  • src/components/customerService/noticeDetail/header/NoticeDetailHeader.tsx (1 hunks)
  • src/constants/routes.ts (1 hunks)
  • src/hooks/queries/keys.ts (1 hunks)
  • src/hooks/useGetNotice.ts (1 hunks)
  • src/hooks/useGetNoticeDetail.ts (1 hunks)
  • src/models/customerService.ts (1 hunks)
  • src/pages/customerService/notice/Notice.styled.ts (1 hunks)
  • src/pages/customerService/notice/Notice.tsx (1 hunks)
  • src/pages/customerService/noticeDetail/NoticeDetail.tsx (1 hunks)
  • src/routes/AppRoutes.tsx (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (17)
src/components/customerService/notice/NoticeList.tsx (2)
src/models/customerService.ts (1)
  • Notice (17-20)
src/pages/customerService/notice/Notice.tsx (1)
  • Notice (11-59)
src/components/customerService/noticeDetail/bottom/button/OtherNoticeButton.tsx (3)
src/components/customerService/noticeDetail/bottom/button/OtherNoticeButton.styled.ts (1)
  • OtherNotice (22-24)
src/models/customerService.ts (1)
  • OtherNotice (26-30)
src/constants/routes.ts (1)
  • ROUTES (1-31)
src/components/customerService/noticeDetail/NoticeDetailBundle.styled.ts (6)
src/pages/customerService/notice/Notice.styled.ts (2)
  • SpinnerWrapper (4-6)
  • Container (8-14)
src/components/customerService/notice/NoticeList.styled.ts (1)
  • Container (3-5)
src/components/customerService/noticeDetail/bottom/NoticeDetailBottom.styled.ts (1)
  • Container (4-4)
src/components/customerService/inquiry/Inquiry.styled.ts (1)
  • Container (3-8)
src/components/customerService/noticeDetail/header/NoticeDetailHeader.styled.ts (1)
  • Container (3-9)
src/components/customerService/noticeDetail/content/NoticeDetailContent.styled.ts (1)
  • Container (3-6)
src/components/customerService/notice/NoticeList.styled.ts (6)
src/components/customerService/noticeDetail/NoticeDetailBundle.styled.ts (1)
  • Container (7-10)
src/components/customerService/noticeDetail/bottom/NoticeDetailBottom.styled.ts (1)
  • Container (4-4)
src/components/customerService/inquiry/Inquiry.styled.ts (1)
  • Container (3-8)
src/components/customerService/noticeDetail/header/NoticeDetailHeader.styled.ts (2)
  • Container (3-9)
  • Title (16-16)
src/components/customerService/noticeDetail/content/NoticeDetailContent.styled.ts (2)
  • Container (3-6)
  • Title (12-12)
src/pages/customerService/notice/Notice.styled.ts (2)
  • Container (8-14)
  • Wrapper (16-20)
src/api/customerService.api.ts (2)
src/models/customerService.ts (3)
  • SearchKeyword (13-15)
  • ApiNotice (22-24)
  • ApiNoticeDetail (37-39)
src/api/http.api.ts (1)
  • httpClient (78-78)
src/components/customerService/noticeDetail/header/NoticeDetailHeader.styled.ts (5)
src/components/customerService/notice/NoticeList.styled.ts (2)
  • Container (3-5)
  • Title (19-22)
src/components/customerService/noticeDetail/NoticeDetailBundle.styled.ts (1)
  • Container (7-10)
src/components/customerService/noticeDetail/bottom/NoticeDetailBottom.styled.ts (1)
  • Container (4-4)
src/components/customerService/noticeDetail/content/NoticeDetailContent.styled.ts (2)
  • Container (3-6)
  • Title (12-12)
src/pages/customerService/notice/Notice.styled.ts (1)
  • Container (8-14)
src/hooks/useGetNoticeDetail.ts (2)
src/hooks/queries/keys.ts (1)
  • CustomerService (47-51)
src/api/customerService.api.ts (1)
  • getNoticeDetail (31-40)
src/components/customerService/noticeDetail/NoticeDetailBundle.tsx (4)
src/hooks/useGetNoticeDetail.ts (1)
  • useGetNoticeDetail (5-14)
src/components/customerService/noticeDetail/header/NoticeDetailHeader.tsx (1)
  • NoticeDetailHeader (3-11)
src/components/customerService/noticeDetail/content/NoticeDetailContent.tsx (1)
  • NoticeDetailContent (12-41)
src/components/customerService/noticeDetail/bottom/NoticeDetailBottom.tsx (1)
  • NoticeDetailBottom (11-47)
src/hooks/useGetNotice.ts (3)
src/models/customerService.ts (1)
  • SearchKeyword (13-15)
src/hooks/queries/keys.ts (1)
  • CustomerService (47-51)
src/api/customerService.api.ts (1)
  • getNotice (20-29)
src/routes/AppRoutes.tsx (2)
src/pages/customerService/noticeDetail/NoticeDetail.tsx (1)
  • NoticeDetail (3-5)
src/constants/routes.ts (1)
  • ROUTES (1-31)
src/pages/customerService/notice/Notice.tsx (6)
src/models/customerService.ts (2)
  • Notice (17-20)
  • SearchKeyword (13-15)
src/hooks/useGetNotice.ts (1)
  • useGetNotice (6-15)
src/components/customerService/CustomerServiceHeader.tsx (1)
  • CustomerServiceHeader (16-71)
src/constants/routes.ts (1)
  • ROUTES (1-31)
src/components/customerService/notice/NoticeList.tsx (1)
  • NoticeList (9-18)
src/components/common/noResult/NoResult.tsx (1)
  • NoResult (8-17)
src/components/customerService/noticeDetail/bottom/NoticeDetailBottom.tsx (3)
src/models/customerService.ts (1)
  • OtherNotice (26-30)
src/components/customerService/noticeDetail/bottom/button/OtherNoticeButton.tsx (1)
  • OtherNoticeButton (10-28)
src/constants/routes.ts (1)
  • ROUTES (1-31)
src/models/customerService.ts (4)
src/pages/customerService/notice/Notice.tsx (1)
  • Notice (11-59)
src/pages/customerService/faq/FAQ.tsx (1)
  • FAQ (10-53)
src/components/customerService/noticeDetail/bottom/button/OtherNoticeButton.styled.ts (1)
  • OtherNotice (22-24)
src/pages/customerService/noticeDetail/NoticeDetail.tsx (1)
  • NoticeDetail (3-5)
src/components/customerService/noticeDetail/bottom/button/OtherNoticeButton.styled.ts (1)
src/models/customerService.ts (1)
  • OtherNotice (26-30)
src/components/customerService/noticeDetail/bottom/NoticeDetailBottom.styled.ts (3)
src/components/customerService/notice/NoticeList.styled.ts (1)
  • Container (3-5)
src/components/customerService/noticeDetail/NoticeDetailBundle.styled.ts (1)
  • Container (7-10)
src/components/customerService/noticeDetail/content/NoticeDetailContent.styled.ts (2)
  • Container (3-6)
  • ContentBorder (58-64)
src/components/customerService/noticeDetail/content/NoticeDetailContent.styled.ts (5)
src/components/customerService/notice/NoticeList.styled.ts (2)
  • Container (3-5)
  • Title (19-22)
src/components/customerService/noticeDetail/NoticeDetailBundle.styled.ts (1)
  • Container (7-10)
src/components/customerService/noticeDetail/bottom/NoticeDetailBottom.styled.ts (2)
  • Container (4-4)
  • ContentBorder (39-45)
src/components/customerService/noticeDetail/header/NoticeDetailHeader.styled.ts (2)
  • Container (3-9)
  • Title (16-16)
src/pages/customerService/notice/Notice.styled.ts (2)
  • Container (8-14)
  • ContentBorder (24-30)
src/pages/customerService/notice/Notice.styled.ts (3)
src/components/customerService/noticeDetail/NoticeDetailBundle.styled.ts (2)
  • SpinnerWrapper (3-5)
  • Container (7-10)
src/pages/customerService/faq/FAQ.styled.ts (4)
  • SpinnerWrapper (3-5)
  • Container (7-13)
  • Wrapper (15-19)
  • ContentBorder (23-27)
src/components/customerService/noticeDetail/bottom/NoticeDetailBottom.styled.ts (2)
  • Container (4-4)
  • ContentBorder (39-45)
🪛 Biome (1.9.4)
src/components/customerService/notice/NoticeList.styled.ts

[error] 24-24: Do not shadow the global "Date" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)

src/components/customerService/noticeDetail/content/NoticeDetailContent.styled.ts

[error] 34-34: Do not shadow the global "Date" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: accessibility-test
🔇 Additional comments (38)
src/components/customerService/inquiry/Inquiry.styled.ts (1)

53-57: styled-components prop 이름 수정 적절
$isCategoryOpen으로 prop명을 타입 정의와 일치하게 변경하여, 이전에 발생할 수 있던 불일치 문제를 해소했습니다. 이로 인해 SVG 아이콘의 회전 애니메이션도 기대한 대로 작동할 것입니다.

src/constants/routes.ts (1)

29-29: 새로운 경로 상수 추가가 적절합니다.

공지사항 상세 페이지를 위한 noticeDetail 경로 상수가 적절히 추가되었습니다. 기존 경로 상수들과 네이밍 규칙이 일관되어 있습니다.

src/hooks/queries/keys.ts (1)

50-50: 쿼리 키 추가가 적절합니다.

CustomerService 객체에 noticeDetail 쿼리 키가 적절히 추가되었습니다. React Query를 사용하여 공지사항 상세 데이터를 가져오기 위한 키로 사용될 것으로 보입니다.

src/pages/customerService/noticeDetail/NoticeDetail.tsx (1)

1-5: 간단한 래퍼 컴포넌트가 적절히 구현되었습니다.

NoticeDetail 컴포넌트가 단순히 NoticeDetailBundle을 렌더링하는 래퍼로 구현되었습니다. 프로젝트의 라우팅 구조에 맞게 잘 작성되었습니다.

src/components/customerService/noticeDetail/NoticeDetailBundle.styled.ts (2)

3-5: 스타일링 일관성 확인 완료.

SpinnerWrapper의 높이 설정이 다른 파일(src/pages/customerService/notice/Notice.styled.ts)과 동일하게 설정되어 있어 일관성이 유지되었습니다.


7-10: 적절한 컨테이너 레이아웃 구현.

75% 너비와 중앙 정렬을 사용한 컨테이너 스타일링이 콘텐츠 표시에 적합합니다. 다른 컴포넌트의 Container와 비교했을 때 동일한 네이밍 패턴을 따르고 있습니다.

src/routes/AppRoutes.tsx (2)

26-28: 지연 로딩 구현 잘 되었습니다.

NoticeDetail 컴포넌트의 지연 로딩 구현이 다른 컴포넌트들과 동일한 패턴을 따르고 있어 일관성이 유지되었습니다.


149-152: 라우트 구성 적절하게 추가되었습니다.

공지사항 상세 페이지 라우트가 고객 서비스 섹션의 자식 라우트로 적절하게 추가되었습니다. 동적 매개변수 :noticeId를 사용하여 개별 공지사항에 접근할 수 있도록 구성한 점이 좋습니다.

src/components/customerService/noticeDetail/bottom/button/OtherNoticeButton.tsx (2)

6-8: 인터페이스 확장 방식이 적절합니다.

OtherNotice 인터페이스를 확장하여 navigation 속성을 추가한 방식이 효율적이고 타입 안전성을 보장합니다.


21-26: 네비게이션 및 날짜 포맷팅 구현이 잘 되었습니다.

이전/다음 공지사항으로 이동하는 네비게이션 링크와 날짜 포맷팅이 적절하게 구현되었습니다. formatDate 유틸리티 함수를 재사용한 점이 좋습니다.

src/hooks/useGetNoticeDetail.ts (1)

13-14: 반환 값 구조화가 적절합니다.

쿼리 결과와 로딩 상태를 객체로 묶어 반환하는 방식이 컴포넌트에서 사용하기 편리합니다.

src/hooks/useGetNotice.ts (1)

1-15: 잘 구현된 커스텀 훅입니다.

useGetNotice 훅은 React Query를 효과적으로 활용하여 공지사항 데이터를 가져오는 기능을 구현했습니다. 다만, staleTimegcTime이 모두 Infinity로 설정되어 있어 데이터가 자동으로 리패치되거나 가비지 컬렉션되지 않습니다. 공지사항이 자주 업데이트되는 경우 적절한 시간 값을 설정하는 것이 좋을 수 있습니다.

src/components/customerService/noticeDetail/bottom/NoticeDetailBottom.tsx (1)

40-44: 목록 이동 구현이 적절합니다.

목록으로 돌아가는 링크가 명확하게 구현되어 있습니다.

src/components/customerService/noticeDetail/header/NoticeDetailHeader.styled.ts (1)

3-9: 일관된 스타일링 구현입니다.

전체적으로 일관된 스타일링을 구현하였고, 시맨틱 HTML 요소(header)를 적절히 활용하고 있습니다.

src/pages/customerService/notice/Notice.tsx (1)

42-50: 공지사항 목록 렌더링 구현이 적절합니다.

공지사항 목록을 map 함수로 순회하여 각 항목을 NoticeList 컴포넌트로 렌더링하고, 각 항목에 상세 페이지 링크를 연결한 방식이 적절합니다. state를 통해 id를 전달하는 방식도 좋습니다.

src/api/customerService.api.ts (3)

1-6: 타입 임포트가 잘 정의되어 있습니다.

새로운 API 인터페이스를 위한 타입 임포트가 올바르게 추가되었습니다. ApiNotice, ApiNoticeDetail 타입을 가져와 API 응답 데이터 구조를 명확히 정의하고 있습니다.


20-29: 공지사항 목록 조회 API 함수 구현이 적절합니다.

getNotice 함수가 기존 getFAQ 함수와 일관된 패턴으로 구현되었습니다. 에러 처리 방식과 반환 형식도 적절합니다.


31-40: 공지사항 상세 조회 API 함수 구현이 적절합니다.

getNoticeDetail 함수가 ID를 매개변수로 받아 특정 공지사항의 상세 정보를 조회하는 구현이 잘 되어 있습니다. 에러 처리와 데이터 반환 방식이 일관성 있게 구현되었습니다.

src/components/customerService/noticeDetail/NoticeDetailBundle.tsx (1)

25-36: 코드 구조와 컴포넌트 조합이 잘 구현되었습니다.

공지사항 상세 페이지의 헤더, 컨텐츠, 하단 영역을 적절히 분리하여 구현했습니다. 필요한 props가 올바르게 전달되고 있으며 전체적인 구조가 깔끔합니다.

src/components/customerService/notice/NoticeList.styled.ts (2)

3-5: Container 스타일 컴포넌트가 적절하게 정의되었습니다.

Container가 nav 엘리먼트를 사용하여 목록 탐색을 위한 의미적으로 올바른 HTML 요소를 사용하고 있습니다.


7-17: Wrapper가 button 요소로 구현된 점을 검토해보세요.

목록 항목을 button 요소로 구현했는데, 이는 클릭 이벤트가 있다면 적절하지만 페이지 이동이 목적이라면 a 요소가 더 의미적으로 맞을 수 있습니다.

만약 페이지 이동이 목적이라면 다음과 같이 변경을 고려해보세요:

-export const Wrapper = styled.button`
+export const Wrapper = styled.a`
  width: 100%;
  padding: 1rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
+  text-decoration: none;
+  color: inherit;
+  cursor: pointer;

  &:hover {
    background: ${({ theme }) => theme.color.lightgrey};
  }
`;
src/components/customerService/noticeDetail/content/NoticeDetailContent.tsx (2)

6-10: Props 인터페이스가 잘 정의되었습니다.

NoticeDetailContentProps 인터페이스가 필요한 속성들을 명확하게 정의하고 있습니다.


35-38:

Details

❓ Verification inconclusive

컨텐츠 렌더링 방식 검토가 필요합니다.

현재 content를 일반 텍스트로 렌더링하고 있는데, 만약 컨텐츠에 HTML이 포함되어 있다면 정상적으로 표시되지 않을 수 있습니다.

만약 HTML 컨텐츠를 지원해야 한다면 다음과 같이 구현하는 것이 좋습니다:

<S.ContentWrapper>
-  <S.Content>{content}</S.Content>
+  <S.Content dangerouslySetInnerHTML={{ __html: content }} />
</S.ContentWrapper>

주의: dangerouslySetInnerHTML은 XSS 취약점이 발생할 수 있으므로, 백엔드에서 HTML 컨텐츠를 적절히 소독(sanitize)하고 있는지 확인해야 합니다.


🏁 Script executed:

#!/bin/bash
# 백엔드 API의 content 필드가 어떤 형식(HTML 또는 일반 텍스트)인지 확인

rg -n "interface.*NoticeDetail" --type ts

Length of output: 418


🏁 Script executed:

#!/bin/bash
# NoticeDetail 및 ApiNoticeDetail 인터페이스에서 content 필드 타입 확인
rg -n 'export interface NoticeDetail' -A5 -B0 src/models/customerService.ts
rg -n 'export interface ApiNoticeDetail' -A5 -B0 src/models/customerService.ts
rg -n 'content:' -C2 src/models/customerService.ts

Length of output: 499


컨텐츠 HTML 렌더링 여부 확인 필요
현재 src/models/customerService.tsNoticeDetail.content 필드가 string으로 정의되어 있어, 컴포넌트(NoticeDetailContent.tsx)에서 {content}로 단순 출력하고 있습니다.

  • 순수 텍스트만 반환된다면 현 구조로 문제가 없지만, HTML 마크업을 포함한 콘텐츠가 API에서 내려온다면 태그가 이스케이프되어 화면에 그대로 노출됩니다.
  • HTML 렌더링이 필요하다면 아래 예시처럼 dangerouslySetInnerHTML 사용을 검토하고, 반드시 백엔드 또는 프론트엔드에서 XSS 방지를 위한 sanitize 처리가 이루어지고 있는지 확인하세요.
<S.ContentWrapper>
-  <S.Content>{content}</S.Content>
+  <S.Content dangerouslySetInnerHTML={{ __html: content }} />
</S.ContentWrapper>

백엔드 API가 HTML 마크업을 포함한 내용을 반환하는지, 그리고 sanitize 처리 여부를 담당 팀에 확인 후 반영을 검토 바랍니다.

src/models/customerService.ts (1)

17-35: 인터페이스 구조가 명확하게 설계되었습니다.

계층적인 인터페이스 구조가 잘 설계되어 있어 타입 안정성을 확보하고 있습니다. NoticeFAQ를 확장하고, NoticeDetailNotice를 확장하며 이전/다음 공지사항 탐색 기능을 지원합니다.

src/pages/customerService/notice/Notice.styled.ts (1)

4-30: 스타일 컴포넌트 구조가 잘 구성되었습니다.

공지사항 목록 페이지의 스타일링이 다른 유사한 컴포넌트(FAQ 등)와 일관되게 유지되어 있어 사용자 경험의 일관성을 제공합니다.

src/components/customerService/noticeDetail/bottom/button/OtherNoticeButton.styled.ts (2)

4-31: 버튼 스타일링이 잘 구현되었습니다.

공지사항 이동 버튼 컴포넌트의 스타일링이 디자인 시스템과 일관되게 적용되어 있습니다. 호버 효과와 레이아웃 구성이 사용자 경험을 향상시킵니다.


28-31: OtherNoticeDate 사용 시 주의사항

models/customerService.ts에서 OtherNotice 인터페이스의 cratedAt 속성에 오타가 있습니다. 이 스타일드 컴포넌트를 사용하는 실제 컴포넌트에서 해당 속성을 사용할 때 주의가 필요합니다.

src/components/customerService/noticeDetail/bottom/NoticeDetailBottom.styled.ts (1)

6-45: 공지사항 상세 페이지 하단 영역 스타일링이 잘 구현되었습니다.

공지사항 상세 페이지의 하단 영역 스타일링이 테마 변수를 활용하여 일관된 디자인을 유지하고 있습니다. 특히 ListLink의 호버 효과와 트랜지션이 사용자 경험을 향상시킵니다.

src/components/customerService/noticeDetail/content/NoticeDetailContent.styled.ts (10)

3-6: Container 스타일 확인
Container가 너비 100%와 상하 여백(2rem)을 통해 레이아웃 여백을 적절히 확보하고 있습니다. 다른 모듈의 컨테이너와도 일관성이 있어 보입니다.


8-10: TitleWrapper 패딩 설정
TitleWrapper에 1rem 패딩을 주어 제목과 주변 요소 간 여유 공간을 확보한 점이 좋습니다.


12-12: Title 태그 사용 적절
Titleh2로 선언해 의미론적 구조를 잘 살렸습니다. 추가 스타일링이 필요 없을 경우 빈 템플릿 리터럴도 무방합니다.


14-16: AdminWrapper 역할 명확
관리자 정보 영역에 상하 패딩을 줘서 요소 간 간격을 분리한 점이 가독성 측면에서 좋습니다.


23-26: Admin 텍스트 정렬
vertical-align: middle로 이미지 옆 텍스트 정렬을 맞춘 점이 좋습니다.


28-32: InfoWrapper 플렉스 레이아웃
Flexbox와 gap을 활용해 날짜/조회수 요소 간 간격을 명확히 한 점이 우수합니다.


40-46: ViewIcon SVG 크기 지정
중첩된 svg에 width/height를 설정해 아이콘 크기를 제어한 구현이 적절합니다.


48-50: ViewCount 폰트 크기
조회수 텍스트에 0.8rem 폰트 크기를 지정해 시각적 위계를 잘 표현했습니다.


52-54: ContentWrapper 패딩
본문 영역에 1.5rem×1rem 패딩을 부여해 가독성을 확보한 점이 좋습니다.


56-56: Content 태그 활용
styled.p를 빈 템플릿으로 선언해 기본 단락 스타일을 유지한 것은 깔끔합니다.

Comment on lines 9 to 18
export default function NoticeList({ list }: NoticeProps) {
return (
<S.Container>
<S.Wrapper>
<S.Title>{list.title}</S.Title>
<S.Date>{formatDate(list.createdAt)}</S.Date>
</S.Wrapper>
</S.Container>
);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

컴포넌트 구현은 좋으나 몇 가지 개선 사항이 있습니다.

컴포넌트가 잘 구현되었지만, 다음과 같은 개선 사항을 고려해 보세요:

  1. props 이름을 list에서 notice로 변경
  2. createdAt 값이 없는 경우를 대비한 오류 처리 추가
-export default function NoticeList({ list }: NoticeProps) {
+export default function NoticeList({ notice }: NoticeProps) {
  return (
    <S.Container>
      <S.Wrapper>
-        <S.Title>{list.title}</S.Title>
-        <S.Date>{formatDate(list.createdAt)}</S.Date>
+        <S.Title>{notice.title}</S.Title>
+        <S.Date>{notice.createdAt ? formatDate(notice.createdAt) : '-'}</S.Date>
      </S.Wrapper>
    </S.Container>
  );
}

);
}

if (!noticeData) return;
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

데이터가 없을 때 처리 개선 필요

noticeData가 없을 때 아무것도 렌더링하지 않고 반환하는 것은 사용자 경험 측면에서 최적이 아닙니다. 에러 메시지나 로딩 상태를 보여주는 것이 더 좋을 수 있습니다.

-if (!noticeData) return;
+if (!noticeData) {
+  return (
+    <>
+      <CustomerServiceHeader
+        title='공지사항'
+        keyword={value}
+        onGetKeyword={handleGetKeyword}
+      />
+      <S.Container>
+        <S.Wrapper>
+          <S.ContentBorder></S.ContentBorder>
+          <NoResult height='20rem' />
+        </S.Wrapper>
+      </S.Container>
+    </>
+  );
+}
📝 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
if (!noticeData) return;
if (!noticeData) {
return (
<>
<CustomerServiceHeader
title='공지사항'
keyword={value}
onGetKeyword={handleGetKeyword}
/>
<S.Container>
<S.Wrapper>
<S.ContentBorder></S.ContentBorder>
<NoResult height='20rem' />
</S.Wrapper>
</S.Container>
</>
);
}

Comment on lines 15 to 23
if (!noticeDetailData) return;

if (isLoading) {
return (
<S.SpinnerWrapper>
<Spinner />
</S.SpinnerWrapper>
);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

오류 상태 처리가 누락되었습니다.

데이터가 없거나 로딩 중인 상태는 처리되어 있으나, API 요청 중 오류가 발생한 경우에 대한 처리가 없습니다.

다음과 같이 오류 상태를 처리하는 코드를 추가하세요:

-const { noticeDetailData, isLoading } = useGetNoticeDetail(String(id));
+const { noticeDetailData, isLoading, isError, error } = useGetNoticeDetail(String(id));

 if (!noticeDetailData) return;

+if (isError) {
+  return (
+    <S.ErrorWrapper>
+      <p>공지사항을 불러오는 중 오류가 발생했습니다.</p>
+      <p>{error instanceof Error ? error.message : '다시 시도해주세요.'}</p>
+    </S.ErrorWrapper>
+  );
+}
+
 if (isLoading) {
   return (
     <S.SpinnerWrapper>

그리고 관련 스타일 컴포넌트를 NoticeDetailBundle.styled.ts에 추가해야 합니다.

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 (1)
src/components/customerService/noticeDetail/NoticeDetailBundle.tsx (1)

15-25: 🛠️ Refactor suggestion

오류 상태 처리가 누락되었습니다.

현재 코드는 로딩 상태와 데이터 없음 상태는 처리하고 있지만, API 요청 중 오류가 발생한 경우에 대한 처리가 없습니다. 사용자 경험을 향상시키기 위해 오류 상태도 처리해야 합니다.

다음과 같이 오류 상태를 처리하는 코드를 추가하세요:

-const { noticeDetailData, isLoading } = useGetNoticeDetail(id);
+const { noticeDetailData, isLoading, isError, error } = useGetNoticeDetail(id);

-if (!noticeDetailData) return;
-
 if (isLoading) {
   return (
     <S.SpinnerWrapper>
       <Spinner />
     </S.SpinnerWrapper>
   );
 }
+
+if (isError) {
+  return (
+    <S.ErrorWrapper>
+      <p>공지사항을 불러오는 중 오류가 발생했습니다.</p>
+      <p>{error instanceof Error ? error.message : '다시 시도해주세요.'}</p>
+    </S.ErrorWrapper>
+  );
+}
+
+if (!noticeDetailData) return;

또한 useGetNoticeDetail 훅에서 오류 상태를 반환하도록 수정해야 합니다:

// src/hooks/useGetNoticeDetail.ts
export const useGetNoticeDetail = (id: string) => {
-  const { data: noticeDetailData, isLoading } = useQuery({
+  const { data: noticeDetailData, isLoading, isError, error } = useQuery({
     queryKey: [CustomerService.noticeDetail, id],
     queryFn: () => getNoticeDetail(id),
     staleTime: Infinity,
     gcTime: Infinity,
   });

-  return { noticeDetailData, isLoading };
+  return { noticeDetailData, isLoading, isError, error };
};

그리고 관련 스타일 컴포넌트를 NoticeDetailBundle.styled.ts에 추가해야 합니다.

🧹 Nitpick comments (1)
src/components/customerService/noticeDetail/NoticeDetailBundle.tsx (1)

9-40: 빈 응답 처리를 개선하세요.

현재 코드에서는 noticeDetailData가 없을 때 아무것도 렌더링하지 않고 return만 합니다. 이는 사용자에게 혼란을 줄 수 있습니다. 데이터가 없을 때 사용자에게 적절한 메시지를 보여주는 것이 좋습니다.

다음과 같이 수정하여 사용자에게 명확한 메시지를 제공하세요:

-  if (!noticeDetailData) return;
+  if (!noticeDetailData) {
+    return (
+      <S.ErrorWrapper>
+        <p>공지사항 정보를 찾을 수 없습니다.</p>
+        <S.BackLink to="/customer-service/notice">목록으로 돌아가기</S.BackLink>
+      </S.ErrorWrapper>
+    );
+  }
🧰 Tools
🪛 Biome (1.9.4)

[error] 13-13: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 664778d and bfa66e0.

📒 Files selected for processing (4)
  • src/components/customerService/noticeDetail/NoticeDetailBundle.tsx (1 hunks)
  • src/components/customerService/noticeDetail/content/NoticeDetailContent.styled.ts (1 hunks)
  • src/components/customerService/noticeDetail/content/NoticeDetailContent.tsx (1 hunks)
  • src/models/customerService.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/models/customerService.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/components/customerService/noticeDetail/content/NoticeDetailContent.tsx
  • src/components/customerService/noticeDetail/content/NoticeDetailContent.styled.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/components/customerService/noticeDetail/NoticeDetailBundle.tsx (4)
src/hooks/useGetNoticeDetail.ts (1)
  • useGetNoticeDetail (5-14)
src/components/customerService/noticeDetail/header/NoticeDetailHeader.tsx (1)
  • NoticeDetailHeader (3-11)
src/components/customerService/noticeDetail/content/NoticeDetailContent.tsx (1)
  • NoticeDetailContent (12-41)
src/components/customerService/noticeDetail/bottom/NoticeDetailBottom.tsx (1)
  • NoticeDetailBottom (11-47)
🪛 Biome (1.9.4)
src/components/customerService/noticeDetail/NoticeDetailBundle.tsx

[error] 13-13: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: accessibility-test

const location = useLocation();
const { noticeId } = useParams();

const id = noticeId || String(location.state && location.state.id);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

옵셔널 체이닝을 사용하여 코드를 개선하세요.

현재 코드에서는 location.state && location.state.id와 같은 방식으로 nullable 값을 처리하고 있습니다. 옵셔널 체이닝을 사용하면 더 간결하고 가독성이 높은 코드를 작성할 수 있습니다.

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

-  const id = noticeId || String(location.state && location.state.id);
+  const id = noticeId || String(location.state?.id);
🧰 Tools
🪛 Biome (1.9.4)

[error] 13-13: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

Comment on lines 13 to 15
const id = noticeId || String(location.state && location.state.id);

const { noticeDetailData, isLoading } = useGetNoticeDetail(id);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

ID 매개변수의 유효성 검사가 필요합니다.

현재 코드에서는 ID가 없는 경우에 대한 처리가 부족합니다. noticeIdlocation.state.id 모두 없는 경우, API 호출이 빈 문자열이나 "undefined" 문자열로 실행될 수 있으며 이는 오류를 발생시킬 수 있습니다.

다음과 같이 ID의 유효성을 검사하는 코드를 추가하세요:

  const { noticeId } = useParams();
-  const id = noticeId || String(location.state && location.state.id);
+  const stateId = location.state?.id;
+  const id = noticeId || (stateId ? String(stateId) : '');
+
+  // ID가 없는 경우 처리
+  if (!id) {
+    return (
+      <S.ErrorWrapper>
+        <p>공지사항 ID가 유효하지 않습니다.</p>
+        <S.BackLink to="/customer-service/notice">목록으로 돌아가기</S.BackLink>
+      </S.ErrorWrapper>
+    );
+  }
📝 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
const id = noticeId || String(location.state && location.state.id);
const { noticeDetailData, isLoading } = useGetNoticeDetail(id);
const location = useLocation();
const { noticeId } = useParams();
- const id = noticeId || String(location.state && location.state.id);
+ const stateId = location.state?.id;
+ const id = noticeId || (stateId ? String(stateId) : '');
+
+ // ID가 없는 경우 처리
+ if (!id) {
+ return (
+ <S.ErrorWrapper>
+ <p>공지사항 ID가 유효하지 않습니다.</p>
+ <S.BackLink to="/customer-service/notice">
+ 목록으로 돌아가기
+ </S.BackLink>
+ </S.ErrorWrapper>
+ );
+ }
const { noticeDetailData, isLoading } = useGetNoticeDetail(id);
🧰 Tools
🪛 Biome (1.9.4)

[error] 13-13: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

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: 9

♻️ Duplicate comments (5)
src/components/customerService/notice/NoticeList.tsx (2)

5-7: props 이름을 list에서 notice로 변경하세요.

단일 공지사항 항목을 표현하는 props 이름으로 list는 적절하지 않습니다. 이는 복수의 항목을 의미하는 이름인데 실제로는 단일 객체를 전달하고 있습니다. 다음과 같이 변경하는 것이 의미론적으로 명확합니다:

interface NoticeProps {
-  list: NoticeList;
+  notice: NoticeList;
}

9-18: createdAt 값이 없는 경우에 대한 처리가 필요합니다.

formatDate(list.createdAt)에서 createdAt 속성이 없거나 유효하지 않은 경우 오류가 발생할 수 있습니다. 다음과 같이 조건부 렌더링을 사용하여 안전하게 처리하세요:

export default function NoticeList({ list }: NoticeProps) {
  return (
    <S.Container>
      <S.Wrapper>
        <S.Title>{list.title}</S.Title>
-        <S.Date>{formatDate(list.createdAt)}</S.Date>
+        <S.Date>{list.createdAt ? formatDate(list.createdAt) : '-'}</S.Date>
      </S.Wrapper>
    </S.Container>
  );
}
🧰 Tools
🪛 Biome (1.9.4)

[error] 9-9: Shouldn't redeclare 'NoticeList'. Consider to delete it or rename it.

'NoticeList' is defined here:

(lint/suspicious/noRedeclare)

src/components/customerService/noticeDetail/NoticeDetailBundle.tsx (2)

12-12: 🛠️ Refactor suggestion

선택적 체이닝을 사용하여 코드를 간결하게 개선하세요.

현재 코드는 location.state && location.state.id와 같이 조건부 검사를 하고 있습니다. 선택적 체이닝(Optional Chaining)을 사용하면 더 간결하고 가독성이 좋은 코드를 작성할 수 있습니다.

-  const id = noticeId || String(location.state && location.state.id);
+  const id = noticeId || String(location.state?.id);
🧰 Tools
🪛 Biome (1.9.4)

[error] 12-12: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


16-24: 🛠️ Refactor suggestion

로딩 상태 확인 순서를 변경하세요.

현재 코드에서는 먼저 데이터 존재 여부를 확인한 후에 로딩 상태를 확인합니다. 로딩 중에는 데이터가 아직 없을 수 있으므로, 로직의 순서를 변경하는 것이 바람직합니다.

-  if (!noticeDetailData) return;
-
   if (isLoading) {
     return (
       <S.SpinnerWrapper>
         <Spinner />
       </S.SpinnerWrapper>
     );
   }
+
+  if (!noticeDetailData) return;
src/pages/customerService/notice/Notice.tsx (1)

36-36: 🛠️ Refactor suggestion

데이터가 없는 경우에 대한 처리를 개선하세요.

현재 코드에서는 noticeData가 없을 때 아무것도 렌더링하지 않고 있습니다. 이는 사용자 경험 측면에서 좋지 않습니다. 에러 메시지나 적절한 안내 문구를 보여주는 것이 바람직합니다.

-if (!noticeData) return;
+if (!noticeData) {
+  return (
+    <>
+      <CustomerServiceHeader
+        title='공지사항'
+        keyword={value}
+        onGetKeyword={handleGetKeyword}
+      />
+      <S.Container>
+        <S.Wrapper>
+          <S.ContentBorder></S.ContentBorder>
+          <NoResult height='20rem' text="공지사항을 불러올 수 없습니다." />
+        </S.Wrapper>
+      </S.Container>
+    </>
+  );
+}
🧹 Nitpick comments (7)
src/hooks/useGetNotice.ts (1)

9-14: 무한 staleTimegcTime 설정이 적절한지 검토가 필요합니다.

현재 설정은 데이터를 영구적으로 캐싱하고 있습니다. 공지사항은 업데이트될 수 있으므로, 일정 시간 후에 데이터를 새로 가져오는 방식이 더 적절할 수 있습니다.

const { data: noticeData, isLoading } = useQuery({
  queryKey: [CustomerService.notice, keyword, page],
  queryFn: () => getNotice(searchProperty),
-  staleTime: Infinity,
-  gcTime: Infinity,
+  staleTime: 5 * 60 * 1000, // 5분
+  gcTime: 10 * 60 * 1000, // 10분
});
src/components/customerService/notice/NoticeList.styled.ts (1)

3-5: nav 태그의 의미론적 사용이 적절한지 검토가 필요합니다.

Containernav 태그를 사용하고 있습니다. nav 태그는 네비게이션 링크의 주요 블록을 의미합니다. 하지만 이 컴포넌트가 단일 공지사항 항목을 표시하는 것이라면 li 또는 article이 더 적절할 수 있습니다.

-export const Container = styled.nav`
+export const Container = styled.article`
  width: 100%;
`;
src/components/customerService/noticeDetail/NoticeDetailBundle.tsx (1)

16-16: 데이터가 없는 경우 빈 화면보다 사용자 친화적인 메시지를 표시하세요.

현재 코드에서는 데이터가 없을 때 아무것도 표시하지 않고 있습니다. 이는 사용자 경험 측면에서 좋지 않습니다.

-  if (!noticeDetailData) return;
+  if (!noticeDetailData) {
+    return (
+      <S.ErrorWrapper>
+        <p>공지사항 정보를 찾을 수 없습니다.</p>
+        <S.BackLink to="/customer-service/notice">목록으로 돌아가기</S.BackLink>
+      </S.ErrorWrapper>
+    );
+  }
src/components/customerService/notice/noticePagination/NoticePagination.tsx (4)

71-71: 불필요한 삼항 연산자를 간결하게 변경하세요.

현재 $select 속성에 불필요하게 삼항 연산자를 사용하고 있습니다. 비교 결과 자체가 이미 불리언 값이므로 직접 할당하는 것이 더 간결합니다.

-            $select={currentPage === pageIndex ? true : false}
+            $select={currentPage === pageIndex}
🧰 Tools
🪛 Biome (1.9.4)

[error] 71-71: Unnecessary use of boolean literals in conditional expression.

Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with

(lint/complexity/noUselessTernary)


23-33: 페이지 범위 계산 로직을 개선하세요.

현재 페이지 범위 계산 로직에는 몇 가지 개선할 점이 있습니다:

  1. 마지막 페이지가 없는 경우 기본값을 설정하지 않고 바로 반환합니다.
  2. 페이지 수가 적을 때(예: 1-3 페이지) 불필요하게 복잡한 계산을 수행합니다.
   const calculatePageRange = () => {
-    if (!lastPage) return;
+    if (!lastPage) return { startPage: 1, endPage: 1 };
+    
+    // 페이지 수가 5개 이하인 경우 전체 페이지 표시
+    if (lastPage <= 5) {
+      return { startPage: 1, endPage: lastPage };
+    }
     
     const startPage = Math.max(1, currentPage - 2);
     const endPage = Math.min(lastPage, startPage + 4);
     const adjustedStartPage = Math.max(1, endPage - 4);
     return { startPage: adjustedStartPage, endPage };
   };
-  const { startPage, endPage } = calculatePageRange() || {
-    startPage: 1,
-    endPage: 5,
-  };
+  const { startPage, endPage } = calculatePageRange();

42-42: 마지막 페이지 정보가 없을 때 사용자 피드백을 제공하세요.

현재 lastPage가 없는 경우 컴포넌트가 아무것도 렌더링하지 않고 있습니다. 이는 사용자에게 혼란을 줄 수 있습니다.

-  if (!lastPage) return;
+  if (!lastPage) {
+    return (
+      <S.Container>
+        <S.Wrapper>
+          <S.NoPaginationMessage>
+            페이지 정보를 불러올 수 없습니다.
+          </S.NoPaginationMessage>
+        </S.Wrapper>
+      </S.Container>
+    );
+  }

그리고 관련 스타일 컴포넌트를 추가해야 합니다:

export const NoPaginationMessage = styled.p`
  text-align: center;
  color: #777;
  padding: 1rem;
`;

44-49: 이벤트 처리 중복 코드를 정리하세요.

handleMovePaginationClick 함수는 래퍼(S.Wrapper)에 연결되어 있지만, 각 페이지 번호 버튼은 이미 개별 클릭 핸들러를 가지고 있어 코드가 중복됩니다. 이 함수의 필요성을 재검토하고, 필요 없다면 제거하는 것을 권장합니다.

-  const handleMovePaginationClick = (e: React.MouseEvent<HTMLElement>) => {
-    const target = e.target as HTMLElement;
-    const dataId = target.dataset.id;
-    if (!dataId) return;
-    onChangePagination(Number(dataId));
-  };
+
   return (
     <S.Container>
-      <S.Wrapper onClick={handleMovePaginationClick}>
+      <S.Wrapper>

그리고 페이지 번호에도 클릭 핸들러를 직접 추가하세요:

        {pagination.map((pageIndex) => (
          <S.Pagination
            key={`pagination-${pageIndex}`}
-           data-id={pageIndex}
+           onClick={() => onChangePagination(pageIndex)}
            $select={currentPage === pageIndex}
          >
            {pageIndex}
          </S.Pagination>
        ))}
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between bfa66e0 and 829c70a.

📒 Files selected for processing (15)
  • src/api/customerService.api.ts (2 hunks)
  • src/components/customerService/faq/FAQContent.styled.ts (1 hunks)
  • src/components/customerService/notice/NoticeList.styled.ts (1 hunks)
  • src/components/customerService/notice/NoticeList.tsx (1 hunks)
  • src/components/customerService/notice/noticePagination/NoticePagination.styled.ts (1 hunks)
  • src/components/customerService/notice/noticePagination/NoticePagination.tsx (1 hunks)
  • src/components/customerService/noticeDetail/NoticeDetailBundle.styled.ts (1 hunks)
  • src/components/customerService/noticeDetail/NoticeDetailBundle.tsx (1 hunks)
  • src/components/customerService/noticeDetail/content/NoticeDetailContent.styled.ts (1 hunks)
  • src/components/customerService/noticeDetail/content/NoticeDetailContent.tsx (1 hunks)
  • src/hooks/useGetNotice.ts (1 hunks)
  • src/models/customerService.ts (1 hunks)
  • src/pages/customerService/notice/Notice.styled.ts (1 hunks)
  • src/pages/customerService/notice/Notice.tsx (1 hunks)
  • src/pages/customerService/noticeDetail/NoticeDetail.styled.ts (1 hunks)
✅ Files skipped from review due to trivial changes (4)
  • src/pages/customerService/noticeDetail/NoticeDetail.styled.ts
  • src/components/customerService/faq/FAQContent.styled.ts
  • src/components/customerService/notice/noticePagination/NoticePagination.styled.ts
  • src/components/customerService/noticeDetail/content/NoticeDetailContent.styled.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/api/customerService.api.ts
  • src/components/customerService/noticeDetail/NoticeDetailBundle.styled.ts
  • src/pages/customerService/notice/Notice.styled.ts
  • src/models/customerService.ts
🧰 Additional context used
🧬 Code Graph Analysis (4)
src/components/customerService/noticeDetail/NoticeDetailBundle.tsx (4)
src/hooks/useGetNoticeDetail.ts (1)
  • useGetNoticeDetail (5-14)
src/components/customerService/noticeDetail/header/NoticeDetailHeader.tsx (1)
  • NoticeDetailHeader (3-11)
src/components/customerService/noticeDetail/content/NoticeDetailContent.tsx (1)
  • NoticeDetailContent (13-43)
src/components/customerService/noticeDetail/bottom/NoticeDetailBottom.tsx (1)
  • NoticeDetailBottom (11-47)
src/hooks/useGetNotice.ts (3)
src/models/customerService.ts (1)
  • NoticeSearch (46-48)
src/hooks/queries/keys.ts (1)
  • CustomerService (47-51)
src/api/customerService.api.ts (1)
  • getNotice (21-32)
src/components/customerService/notice/NoticeList.styled.ts (6)
src/components/customerService/noticeDetail/NoticeDetailBundle.styled.ts (1)
  • Container (7-11)
src/components/customerService/noticeDetail/content/NoticeDetailContent.styled.ts (2)
  • Container (3-6)
  • Title (12-12)
src/pages/customerService/notice/Notice.styled.ts (2)
  • Container (8-15)
  • Wrapper (17-21)
src/components/customerService/noticeDetail/bottom/NoticeDetailBottom.styled.ts (1)
  • Container (4-4)
src/components/customerService/inquiry/Inquiry.styled.ts (1)
  • Container (3-8)
src/components/customerService/noticeDetail/header/NoticeDetailHeader.styled.ts (2)
  • Container (3-9)
  • Title (16-16)
src/components/customerService/notice/NoticeList.tsx (1)
src/models/customerService.ts (1)
  • NoticeList (13-15)
🪛 Biome (1.9.4)
src/components/customerService/noticeDetail/NoticeDetailBundle.tsx

[error] 12-12: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

src/components/customerService/notice/noticePagination/NoticePagination.tsx

[error] 71-71: Unnecessary use of boolean literals in conditional expression.

Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with

(lint/complexity/noUselessTernary)

src/components/customerService/notice/NoticeList.styled.ts

[error] 24-24: Do not shadow the global "Date" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)

src/components/customerService/notice/NoticeList.tsx

[error] 9-9: Shouldn't redeclare 'NoticeList'. Consider to delete it or rename it.

'NoticeList' is defined here:

(lint/suspicious/noRedeclare)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: accessibility-test

Comment on lines +9 to +17
const { data: noticeData, isLoading } = useQuery({
queryKey: [CustomerService.notice, keyword, page],
queryFn: () => getNotice(searchProperty),
staleTime: Infinity,
gcTime: Infinity,
});

return { noticeData, isLoading };
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

에러 상태와 처리 방법이 포함되어 있지 않습니다.

데이터 로딩 상태는 반환하고 있지만, 에러 상태는 포함되어 있지 않습니다. 에러 처리를 위해 errorisError 상태도 함께 반환하는 것이 좋습니다.

-const { data: noticeData, isLoading } = useQuery({
+const { data: noticeData, isLoading, error, isError } = useQuery({
  queryKey: [CustomerService.notice, keyword, page],
  queryFn: () => getNotice(searchProperty),
  staleTime: Infinity,
  gcTime: Infinity,
});

-return { noticeData, isLoading };
+return { noticeData, isLoading, error, isError };

Comment on lines +38 to +40
<S.ContentWrapper>
<S.Content>{content}</S.Content>
</S.ContentWrapper>
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

HTML 콘텐츠 삽입 시 보안 문제가 발생할 수 있습니다.

공지사항 내용이 HTML 마크업을 포함할 가능성이 있다면, 단순히 텍스트로 렌더링하는 것은 레이아웃 문제를 일으킬 수 있습니다. 반대로 HTML로 렌더링할 경우 XSS 취약점이 발생할 수 있습니다. 안전한 방식으로 HTML 렌더링을 고려하세요:

-<S.Content>{content}</S.Content>
+<S.Content dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(content) }} />

위 변경을 적용하려면 DOMPurify 라이브러리 설치 및 임포트가 필요합니다:

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

🌐 Web query:

React에서 HTML 콘텐츠를 안전하게 렌더링하는 방법

💡 Result:

React에서 HTML 콘텐츠를 안전하게 렌더링하는 방법에 대해 설명드리겠습니다.

React에서는 보통 JSX를 통해 UI를 렌더링하는데, 만약 외부에서 받은 HTML 문자열을 직접 렌더링해야 한다면, 보안상의 이유로 주의가 필요합니다. 특히, XSS(교차 사이트 스크립팅) 공격을 방지하기 위해 HTML을 안전하게 처리해야 합니다.

1. dangerouslySetInnerHTML 사용 (주의 필요)

React에서 HTML 문자열을 렌더링할 때 주로 사용하는 방법은 dangerouslySetInnerHTML 프로퍼티를 사용하는 것입니다. 아래 예시와 같이 사용합니다:

function MyComponent({ htmlContent }) {
  return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />;
}
  • 여기서 htmlContent는 HTML 문자열입니다.
  • 이 방법은 "위험하다"는 의미로 이름이 붙었으며, 이 HTML이 신뢰할 수 없는 출처라면 보안 문제가 생길 수 있습니다.

2. HTML 콘텐츠를 안전하게 렌더링하는 방법

  • 외부에서 받은 HTML 콘텐츠라면 반드시 신뢰할 수 있는 소스인지 확인해야 합니다.
  • 또는 HTML 문자열을 렌더링하기 전에 XSS 필터링 라이브러리를 사용해 악성 스크립트를 제거해야 합니다.

예를 들어, dompurify 라이브러리를 사용할 수 있습니다:

npm install dompurify
import DOMPurify from 'dompurify';

function MyComponent({ htmlContent }) {
  const cleanHtml = DOMPurify.sanitize(htmlContent);

  return <div dangerouslySetInnerHTML={{ __html: cleanHtml }} />;
}
  • DOMPurify.sanitize 함수는 HTML 문자열에서 잠재적 위험 요소(스크립트, 이벤트 핸들러 등)를 제거해 줍니다.

정리

  • 가능하면 HTML 문자열 대신 React 컴포넌트 구조로 데이터를 표현하세요.
  • 꼭 HTML 문자열이 필요할 경우 dangerouslySetInnerHTML을 사용하지만, 신뢰할 수 없는 콘텐츠는 반드시 dompurify 같은 XSS 필터링 도구로 정화한 뒤 사용하세요.

필요하면 관련 예제나 추가 설명도 드릴 수 있습니다!


XSS 방지를 위한 HTML 문자열 Sanitizing 적용 제안

대상 파일: src/components/customerService/noticeDetail/content/NoticeDetailContent.tsx
문제 위치: 38–40행

외부에서 전달된 content가 HTML 마크업을 포함할 경우, 단순 텍스트 렌더링 시 레이아웃이 깨질 수 있고, HTML로 직접 렌더링하면 XSS 취약점이 발생할 수 있습니다. 아래와 같이 DOMPurify로 sanitizing 후 dangerouslySetInnerHTML을 사용해 안전하게 렌더링하세요:

-       <S.ContentWrapper>
-         <S.Content>{content}</S.Content>
-       </S.ContentWrapper>
+       <S.ContentWrapper>
+         <S.Content
+           dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(content) }}
+         />
+       </S.ContentWrapper>

추가로 dompurify 설치 및 import를 잊지 마세요:

npm install dompurify
npm install --save-dev @types/dompurify
+ import DOMPurify from 'dompurify';
  • DOMPurify를 통해 잠재적 스크립트, 이벤트 핸들러 등을 제거해 XSS 공격을 방어합니다.
  • 가능하면 HTML 대신 React 컴포넌트 구조로 데이터를 전달하세요.

Comment on lines 12 to 16
const id = noticeId || String(location.state && location.state.id);

const { noticeDetailData, isLoading } = useGetNoticeDetail(id);

if (!noticeDetailData) return;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

ID의 유효성 검사와 오류 처리가 필요합니다.

현재 코드에서는 id가 없는 경우나 API 오류 발생 시 처리 로직이 누락되어 있습니다. 사용자가 URL을 통해 직접 접근할 경우 stateundefined일 수 있으며, API 호출 중 오류가 발생할 수 있습니다.

다음과 같이 ID 유효성 검사 및 오류 처리 로직을 추가하세요:

  const { noticeId } = useParams();
-  const id = noticeId || String(location.state && location.state.id);
+  const stateId = location.state?.id;
+  const id = noticeId || (stateId ? String(stateId) : '');
-  const { noticeDetailData, isLoading } = useGetNoticeDetail(id);
+  const { noticeDetailData, isLoading, isError, error } = useGetNoticeDetail(id);
+
+  // ID가 없는 경우 처리
+  if (!id) {
+    return (
+      <S.ErrorWrapper>
+        <p>공지사항 ID가 유효하지 않습니다.</p>
+        <S.BackLink to="/customer-service/notice">목록으로 돌아가기</S.BackLink>
+      </S.ErrorWrapper>
+    );
+  }
+
+  // API 오류 처리
+  if (isError) {
+    return (
+      <S.ErrorWrapper>
+        <p>공지사항을 불러오는 중 오류가 발생했습니다.</p>
+        <p>{error instanceof Error ? error.message : '다시 시도해주세요.'}</p>
+        <S.BackLink to="/customer-service/notice">목록으로 돌아가기</S.BackLink>
+      </S.ErrorWrapper>
+    );
+  }

관련 스타일 컴포넌트를 NoticeDetailBundle.styled.ts에 추가해야 합니다:

export const ErrorWrapper = styled.div`
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 300px;
  gap: 1rem;
`;

export const BackLink = styled(Link)`
  padding: 0.5rem 1rem;
  background-color: #f0f0f0;
  border-radius: 4px;
  text-decoration: none;
  color: #333;
  
  &:hover {
    background-color: #e0e0e0;
  }
`;
🧰 Tools
🪛 Biome (1.9.4)

[error] 12-12: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

page: 1,
});
const [value, setValue] = useState<string>('');
const { noticeData, isLoading } = useGetNotice(keyword);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

API 오류 처리 로직을 추가하세요.

현재 코드에서는 API 호출 실패 시 오류를 처리하는 로직이 누락되어 있습니다. 사용자에게 오류 발생 시 적절한 피드백을 제공하는 것이 중요합니다.

-  const { noticeData, isLoading } = useGetNotice(keyword);
+  const { noticeData, isLoading, isError, error } = useGetNotice(keyword);

+  if (isError) {
+    return (
+      <>
+        <CustomerServiceHeader
+          title='공지사항'
+          keyword={value}
+          onGetKeyword={handleGetKeyword}
+        />
+        <S.Container>
+          <S.Wrapper>
+            <S.ErrorMessage>
+              공지사항을 불러오는 중 오류가 발생했습니다.
+              {error instanceof Error && <p>{error.message}</p>}
+              <S.RetryButton onClick={() => setKeyword({...keyword})}>
+                다시 시도
+              </S.RetryButton>
+            </S.ErrorMessage>
+          </S.Wrapper>
+        </S.Container>
+      </>
+    );
+  }

이에 맞게 스타일 컴포넌트도 추가해야 합니다:

export const ErrorMessage = styled.div`
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 20rem;
  text-align: center;
  gap: 1rem;
`;

export const RetryButton = styled.button`
  padding: 0.5rem 1rem;
  background-color: #f0f0f0;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  margin-top: 1rem;
  
  &:hover {
    background-color: #e0e0e0;
  }
`;

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

♻️ Duplicate comments (5)
src/components/customerService/noticeDetail/content/NoticeDetailContent.tsx (3)

31-31: createdAt 값 유효성 검증 필요

createdAt 값이 유효하지 않은 경우 formatDate 함수 호출 시 오류가 발생할 수 있습니다. 조건부 렌더링을 사용하여 안전하게 처리하세요.

-<S.NoticeContentDate>{formatDate(createdAt)}</S.NoticeContentDate>
+<S.NoticeContentDate>{createdAt ? formatDate(createdAt) : '-'}</S.NoticeContentDate>

42-44: HTML 콘텐츠 보안 처리 누락

공지사항 내용이 HTML 마크업을 포함할 가능성이 있다면, 단순히 텍스트로 렌더링하는 것은 레이아웃 문제를 일으킬 수 있습니다. 반대로 HTML로 렌더링할 경우 XSS 취약점이 발생할 수 있습니다.

DOMPurify 라이브러리를 사용하여 HTML 콘텐츠를 안전하게 렌더링하세요:

-      <S.ContentWrapper>
-        <S.Content>{content}</S.Content>
-      </S.ContentWrapper>
+      <S.ContentWrapper>
+        <S.Content
+          dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(content) }}
+        />
+      </S.ContentWrapper>

DOMPurify 라이브러리 설치 및 임포트가 필요합니다:

npm install dompurify
npm install --save-dev @types/dompurify
+import DOMPurify from 'dompurify';
React에서 HTML 콘텐츠를 렌더링할 때 XSS 공격을 방지하는 방법

36-36: 🛠️ Refactor suggestion

viewCount 값 유효성 검증 필요

viewCount 값이 undefined일 경우 처리 로직이 없습니다. 이 속성을 옵셔널로 만들거나 기본값을 제공하여 안전하게 처리하세요.

-<S.ViewCount>{viewCount}</S.ViewCount>
+<S.ViewCount>{viewCount ?? 0}</S.ViewCount>

또는 인터페이스를 다음과 같이 수정하세요:

 interface NoticeDetailContentProps {
   id: number;
   title: string;
   content: string;
   createdAt: string;
-  viewCount: number;
+  viewCount?: number;
 }
src/components/customerService/noticeDetail/NoticeDetailBundle.tsx (2)

12-12: 옵셔널 체이닝 사용 권장

현재 코드는 location.state && location.state.id와 같은 방식으로 null 체크를 하고 있습니다. 옵셔널 체이닝을 사용하면 더 간결하고 가독성이 높은 코드를 작성할 수 있습니다.

-const id = noticeId || String(location.state && location.state.id);
+const id = noticeId || String(location.state?.id);
🧰 Tools
🪛 Biome (1.9.4)

[error] 12-12: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


12-14: ID 유효성 검사 및 오류 처리 필요

현재 코드에서는 id가 없거나 유효하지 않은 경우에 대한 처리가 부족합니다. noticeIdlocation.state.id 모두 없거나 유효하지 않은 경우, API 호출이 잘못된 매개변수로 실행될 수 있으며 이는 오류를 발생시킬 수 있습니다.

다음과 같이 ID의 유효성을 검사하고 오류 상태를 처리하는 코드를 추가하세요:

  const { noticeId } = useParams();
- const id = noticeId || String(location.state && location.state.id);
+ const stateId = location.state?.id;
+ const id = noticeId || (stateId ? String(stateId) : '');
- const { noticeDetail: noticeDetailData, isLoading } = useGetNoticeDetail(id);
+ const { noticeDetail: noticeDetailData, isLoading, isError, error } = useGetNoticeDetail(id);

+ // ID가 없는 경우 처리
+ if (!id) {
+   return (
+     <S.ErrorWrapper>
+       <p>공지사항 ID가 유효하지 않습니다.</p>
+       <S.BackLink to="/customer-service/notice">목록으로 돌아가기</S.BackLink>
+     </S.ErrorWrapper>
+   );
+ }
+
+ // API 오류 처리
+ if (isError) {
+   return (
+     <S.ErrorWrapper>
+       <p>공지사항을 불러오는 중 오류가 발생했습니다.</p>
+       <p>{error instanceof Error ? error.message : '다시 시도해주세요.'}</p>
+       <S.BackLink to="/customer-service/notice">목록으로 돌아가기</S.BackLink>
+     </S.ErrorWrapper>
+   );
+ }

관련 스타일 컴포넌트를 NoticeDetailBundle.styled.ts에 추가해야 합니다:

export const ErrorWrapper = styled.div`
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 300px;
  gap: 1rem;
`;

export const BackLink = styled(Link)`
  padding: 0.5rem 1rem;
  background-color: #f0f0f0;
  border-radius: 4px;
  text-decoration: none;
  color: #333;
  
  &:hover {
    background-color: #e0e0e0;
  }
`;
#!/bin/bash
# ID 검증 및 오류 처리 방식 확인

# useGetNoticeDetail 훅의 반환 값 확인
echo "useGetNoticeDetail 훅 구현 확인:"
rg -A 10 "export const useGetNoticeDetail" --type ts

# 에러 처리 관련 코드 확인
echo "비슷한 컴포넌트에서의 에러 처리 패턴 확인:"
rg -A 5 "isError" --type tsx
🧰 Tools
🪛 Biome (1.9.4)

[error] 12-12: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 829c70a and 18490c3.

📒 Files selected for processing (9)
  • src/App.tsx (2 hunks)
  • src/components/customerService/notice/NoticeList.styled.ts (1 hunks)
  • src/components/customerService/notice/NoticeList.tsx (1 hunks)
  • src/components/customerService/noticeDetail/NoticeDetailBundle.tsx (1 hunks)
  • src/components/customerService/noticeDetail/content/NoticeDetailContent.tsx (1 hunks)
  • src/hooks/useGetNoticeDetail.ts (1 hunks)
  • src/models/customerService.ts (1 hunks)
  • src/pages/home/Home.tsx (0 hunks)
  • src/pages/mypage/MyPage.tsx (0 hunks)
💤 Files with no reviewable changes (2)
  • src/pages/home/Home.tsx
  • src/pages/mypage/MyPage.tsx
✅ Files skipped from review due to trivial changes (2)
  • src/App.tsx
  • src/components/customerService/notice/NoticeList.styled.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/components/customerService/notice/NoticeList.tsx
  • src/hooks/useGetNoticeDetail.ts
  • src/models/customerService.ts
🧰 Additional context used
🪛 Biome (1.9.4)
src/components/customerService/noticeDetail/NoticeDetailBundle.tsx

[error] 12-12: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: accessibility-test

Comment on lines 27 to 39
<S.Admin>DevPals</S.Admin>
</S.AdminWrapper>
{id && (
<S.InfoWrapper>
<S.NoticeContentDate>{formatDate(createdAt)}</S.NoticeContentDate>
<S.ViewWrapper>
<S.ViewIcon>
<EyeIcon />
</S.ViewIcon>
<S.ViewCount>{viewCount}</S.ViewCount>
</S.ViewWrapper>
</S.InfoWrapper>
)}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

ID가 없을 때 정보 래퍼 조건부 렌더링 로직 재검토 필요

id 속성이 NoticeDetailContentProps 인터페이스에서 필수로 정의되어 있지만, 조건부 렌더링에 사용되고 있습니다. 이는 인터페이스 정의와 일치하지 않으며, 혼란을 줄 수 있습니다. 두 가지 방법으로 해결할 수 있습니다:

  1. 인터페이스에서 id를 선택적으로 변경하여 명시적으로 id가 없을 수 있음을 표시
  2. 조건부 렌더링을 제거하고 항상 정보를 표시
-  {id && (
+  {
     <S.InfoWrapper>
       <S.NoticeContentDate>{formatDate(createdAt)}</S.NoticeContentDate>
       <S.ViewWrapper>
         <S.ViewIcon>
           <EyeIcon />
         </S.ViewIcon>
         <S.ViewCount>{viewCount}</S.ViewCount>
       </S.ViewWrapper>
     </S.InfoWrapper>
-  )}
+  }

또는 인터페이스를 다음과 같이 수정하세요:

 interface NoticeDetailContentProps {
-  id: number;
+  id?: number;
   title: string;
   content: string;
   createdAt: string;
   viewCount: number;
 }

Comment on lines 24 to 34
if (!noticeDetailData) {
return (
<NoticeDetailContent
id={0}
title='공지사항ID가 없습니다.'
content='목록으로 돌아가세요.'
createdAt=''
viewCount={0}
/>
);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

데이터가 없을 때 사용자 친화적인 UI 제공 필요

현재 코드에서는 데이터가 없을 때 '공지사항ID가 없습니다'라는 메시지를 표시하는데, 이는 사용자에게 기술적인 정보를 노출하고 있습니다. 더 친화적인 UI와 함께 목록으로 돌아갈 수 있는 버튼을 제공하는 것이 좋습니다.

  if (!noticeDetailData) {
    return (
-     <NoticeDetailContent
-       id={0}
-       title='공지사항ID가 없습니다.'
-       content='목록으로 돌아가세요.'
-       createdAt=''
-       viewCount={0}
-     />
+     <S.ErrorWrapper>
+       <p>해당 공지사항을 찾을 수 없습니다.</p>
+       <S.BackLink to="/customer-service/notice">공지사항 목록으로 돌아가기</S.BackLink>
+     </S.ErrorWrapper>
    );
  }

S.ErrorWrapperS.BackLink 스타일 컴포넌트는 이전 제안에서 설명한 대로 NoticeDetailBundle.styled.ts에 추가해야 합니다.

📝 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
if (!noticeDetailData) {
return (
<NoticeDetailContent
id={0}
title='공지사항ID가 없습니다.'
content='목록으로 돌아가세요.'
createdAt=''
viewCount={0}
/>
);
}
if (!noticeDetailData) {
return (
<S.ErrorWrapper>
<p>해당 공지사항을 찾을 수 없습니다.</p>
<S.BackLink to="/customer-service/notice">
공지사항 목록으로 돌아가기
</S.BackLink>
</S.ErrorWrapper>
);
}

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

🧹 Nitpick comments (1)
src/pages/customerService/faq/FAQ.tsx (1)

44-44: 기존 스타일 컴포넌트에서 공통 컴포넌트로 변경

이전에 사용하던 로컬 스타일 컴포넌트 대신 공통 컴포넌트를 사용하도록 변경했습니다. 이는 코드 중복을 줄이고 디자인 시스템의 일관성을 높이는 좋은 변경입니다.

그러나 ContentBorder 컴포넌트가 실제로는 빈 요소를 반환하고 있습니다:

export default function ContentBorder() {
  return <S.ContentBorder></S.ContentBorder>;
}

이 컴포넌트는 children props를 받아 내용을 표시할 수 있도록 수정하거나, 현재처럼 단순 구분선 용도로만 사용한다면 더 의미 있는 이름(예: Divider)으로 변경하는 것이 좋을 수 있습니다.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 71068ce and e9a730f.

📒 Files selected for processing (17)
  • src/api/customerService.api.ts (2 hunks)
  • src/components/common/contentBorder/ContentBorder.styled.ts (1 hunks)
  • src/components/common/contentBorder/ContentBorder.tsx (1 hunks)
  • src/components/customerService/notice/NoticeList.styled.ts (1 hunks)
  • src/components/customerService/noticeDetail/NoticeDetailBundle.tsx (1 hunks)
  • src/components/customerService/noticeDetail/bottom/NoticeDetailBottom.styled.ts (1 hunks)
  • src/components/customerService/noticeDetail/bottom/NoticeDetailBottom.tsx (1 hunks)
  • src/components/customerService/noticeDetail/bottom/button/ListButton.styled.ts (1 hunks)
  • src/components/customerService/noticeDetail/bottom/button/ListButton.tsx (1 hunks)
  • src/components/customerService/noticeDetail/content/NoticeDetailContent.styled.ts (1 hunks)
  • src/components/customerService/noticeDetail/content/NoticeDetailContent.tsx (1 hunks)
  • src/components/mypage/activityLog/inquiries/Inquiries.styled.ts (0 hunks)
  • src/components/mypage/activityLog/inquiries/Inquiries.tsx (2 hunks)
  • src/pages/customerService/faq/FAQ.styled.ts (0 hunks)
  • src/pages/customerService/faq/FAQ.tsx (2 hunks)
  • src/pages/customerService/notice/Notice.styled.ts (1 hunks)
  • src/pages/customerService/notice/Notice.tsx (1 hunks)
💤 Files with no reviewable changes (2)
  • src/pages/customerService/faq/FAQ.styled.ts
  • src/components/mypage/activityLog/inquiries/Inquiries.styled.ts
✅ Files skipped from review due to trivial changes (5)
  • src/components/common/contentBorder/ContentBorder.tsx
  • src/components/customerService/noticeDetail/bottom/button/ListButton.tsx
  • src/components/common/contentBorder/ContentBorder.styled.ts
  • src/components/customerService/noticeDetail/bottom/button/ListButton.styled.ts
  • src/components/customerService/notice/NoticeList.styled.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/components/customerService/noticeDetail/bottom/NoticeDetailBottom.styled.ts
  • src/components/customerService/noticeDetail/bottom/NoticeDetailBottom.tsx
  • src/api/customerService.api.ts
  • src/components/customerService/noticeDetail/content/NoticeDetailContent.tsx
  • src/components/customerService/noticeDetail/NoticeDetailBundle.tsx
  • src/pages/customerService/notice/Notice.tsx
  • src/pages/customerService/notice/Notice.styled.ts
  • src/components/customerService/noticeDetail/content/NoticeDetailContent.styled.ts
🧰 Additional context used
🧬 Code Graph Analysis (2)
src/pages/customerService/faq/FAQ.tsx (2)
src/components/common/contentBorder/ContentBorder.styled.ts (1)
  • ContentBorder (3-7)
src/components/common/contentBorder/ContentBorder.tsx (1)
  • ContentBorder (3-5)
src/components/mypage/activityLog/inquiries/Inquiries.tsx (2)
src/components/common/contentBorder/ContentBorder.styled.ts (1)
  • ContentBorder (3-7)
src/components/common/contentBorder/ContentBorder.tsx (1)
  • ContentBorder (3-5)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Run Chromatic
🔇 Additional comments (2)
src/components/mypage/activityLog/inquiries/Inquiries.tsx (1)

2-2: ContentBorder 컴포넌트 재사용 구현이 잘 되었습니다.

스타일 컴포넌트 S.ContentBorder를 공통 컴포넌트 ContentBorder로 교체한 것은 재사용성과 일관성 측면에서 좋은 개선입니다. 이러한 변경은 코드베이스 전체에서 유사한 스타일링을 일관되게 유지하는 데 도움이 됩니다.

Also applies to: 34-34

src/pages/customerService/faq/FAQ.tsx (1)

9-9: 공통 컴포넌트 import 추가

ContentBorder 공통 컴포넌트를 추가함으로써 코드 재사용성을 향상시켰습니다. 컴포넌트 공유는 일관성 있는 UI를 유지하는 데 도움이 됩니다.

@YouD0313 YouD0313 merged commit 2db73aa into develop May 12, 2025
4 checks passed
@YouD0313 YouD0313 deleted the feat/#276 branch May 12, 2025 08:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⛏기능 기능 구현에 대한 라벨 🛠개선 성능 개선/리팩토링에 대한 라벨

Projects

None yet

Development

Successfully merging this pull request may close these issues.

공지사항

3 participants