-
Notifications
You must be signed in to change notification settings - Fork 0
FAQ 구현 (#issue 274) #275
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
FAQ 구현 (#issue 274) #275
Conversation
|
""" WalkthroughFAQ 사용자 페이지의 UI와 API 연동이 새롭게 구현되었습니다. FAQ 데이터 모델, API 함수, 쿼리 훅, 스타일드 컴포넌트, FAQ 리스트 및 아이템 컴포넌트가 추가되었으며, 라우팅 구조와 관련 상수도 변경되었습니다. 기존 FAQ 헤더 컴포넌트는 제거되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant FAQPage
participant useGetFAQ
participant getFAQ (API)
participant Server
User->>FAQPage: 페이지 접근/검색어 입력
FAQPage->>useGetFAQ: keyword 전달
useGetFAQ->>getFAQ (API): getFAQ(keyword)
getFAQ (API)->>Server: GET /faq?keyword=...
Server-->>getFAQ (API): FAQ 데이터 반환
getFAQ (API)-->>useGetFAQ: FAQ 데이터 반환
useGetFAQ-->>FAQPage: FAQ 데이터 및 로딩 상태 반환
FAQPage->>FAQContent: FAQ 리스트 전달 및 렌더링
Assessment against linked issues
Suggested labels
Poem
""" Tip ⚡️ Faster reviews with caching
Enjoy the performance boost—your workflow just got faster. ✨ Finishing Touches
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed 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)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
🧹 Nitpick comments (12)
src/components/customerService/notice/Notice.tsx (1)
1-1: 경로 변경이 올바르게 이루어졌습니다!CustomerServiceHeader의 임포트 경로가 올바르게 업데이트되었습니다. 다만, 향후에는 CustomerServiceHeader도 components 디렉토리로 이동하여 관련 컴포넌트들을 한 곳에서 관리하는 것이 좋을 수 있습니다.
src/components/customerService/faq/FAQ.tsx (1)
25-30: 매개변수 이름을 명확하게 변경하세요.현재
list라는 매개변수 이름은 혼란을 줄 수 있습니다. 각 항목은 실제로 단일 FAQ 항목이지 목록이 아닙니다.- {faqData.map((list) => ( - <Fragment key={list.id}> - <FAQContent list={list} /> - <S.ContentBorder></S.ContentBorder> - </Fragment> - ))} + {faqData.map((faqItem) => ( + <Fragment key={faqItem.id}> + <FAQContent list={faqItem} /> + <S.ContentBorder></S.ContentBorder> + </Fragment> + ))}src/components/customerService/faq/FAQ.styled.ts (2)
3-5: Spinner를 수직으로 중앙 정렬하세요.현재
SpinnerWrapper는 높이만 설정하고 있어 스피너가 수직으로 중앙에 위치하지 않을 수 있습니다.export const SpinnerWrapper = styled.div` height: 60vh; + display: flex; + align-items: center; + justify-content: center; `;
15-20: 반응형 디자인을 고려하세요.현재
Wrapper컴포넌트는 고정된 너비 퍼센트(75%)를 사용하고 있어 모든 화면 크기에서 최적으로 표시되지 않을 수 있습니다.export const Wrapper = styled.div` - width: 75%; + width: 75%; + max-width: 1200px; + padding: 0 1rem; display: flex; flex-direction: column; gap: 1.5rem; `;또는 미디어 쿼리를 사용하여 더 세밀한 반응형 디자인을 구현할 수 있습니다:
export const Wrapper = styled.div` - width: 75%; + width: 90%; + max-width: 1200px; display: flex; flex-direction: column; gap: 1.5rem; + + @media (min-width: 768px) { + width: 75%; + } `;src/components/customerService/faq/FAQContent.tsx (2)
21-28: 콘텐츠 영역에 접근성 및 의미적 구조 개선이 필요합니다.FAQ 콘텐츠 영역에 ARIA 속성이 없어 스크린 리더 사용자에게 상태 변화가 제대로 전달되지 않습니다. 또한 콘텐츠 영역의 의미적 구조를 개선할 수 있습니다.
{isFAQContentOpen && ( - <S.ListContentWrapper> + <S.ListContentWrapper aria-live="polite" role="region"> <S.ListButtonWrapper> <ChevronRightIcon /> </S.ListButtonWrapper> - <S.ListContent>{list.content}</S.ListContent> + <S.ListContent>{list.content}</S.ListContent> </S.ListContentWrapper> )}
1-31: HTML 시맨틱 구조 개선 제안현재 구현은 작동하지만, HTML의
<details>와<summary>태그를 사용하면 내장된 접근성과 토글 기능을 활용할 수 있습니다. 전체 컴포넌트를 시맨틱 구조로 리팩토링하는 방안을 고려해 보세요.export default function FAQContent({ list }: FAQContentProps) { const [isFAQContentOpen, setIsFAQContentOpen] = useState<boolean>(false); const handleToggle = (e: React.MouseEvent<HTMLDetailsElement>) => { // details 요소의 open 속성이 변경되면 상태 업데이트 setIsFAQContentOpen(e.currentTarget.open); }; return ( <S.ListContainer as="details" open={isFAQContentOpen} onToggle={handleToggle}> <S.ListWrapper as="summary"> <S.ListTitle>{list.title}</S.ListTitle> <S.ListPlusButton type='button' $isOpen={isFAQContentOpen}> <PlusIcon /> </S.ListPlusButton> </S.ListWrapper> <S.ListContentWrapper> <S.ListButtonWrapper> <ChevronRightIcon /> </S.ListButtonWrapper> <S.ListContent>{list.content}</S.ListContent> </S.ListContentWrapper> </S.ListContainer> ); }src/components/customerService/faq/FAQContent.styled.ts (6)
3-3: 빈 스타일드 컴포넌트
ListContainer에 스타일 속성이 없습니다. 필요한 스타일이 없다면 이 컴포넌트가 필요한지 검토해 보세요. 만약 위치나 여백 등의 스타일이 필요하다면 추가하는 것이 좋겠습니다.-export const ListContainer = styled.div``; +export const ListContainer = styled.div` + width: 100%; + margin-bottom: 1rem; +`;
5-9: 클릭 가능한 UI 힌트 추가 필요
ListWrapper는 클릭 가능한 영역이지만 시각적으로 이를 나타내는 힌트(커서 스타일)가 없습니다. 사용자에게 클릭 가능함을 알리는 스타일을 추가해 보세요.export const ListWrapper = styled.div` display: flex; justify-content: space-between; align-items: center; + cursor: pointer; + padding: 1rem 0; + + &:hover { + background-color: #f9f9f9; + } `;
17-28: 버튼 스타일 개선 사항
ListPlusButton은 type='button'을 가진 버튼이지만, 버튼의 기본 스타일이 재설정되지 않았습니다. 또한 클릭 영역이 작을 수 있습니다.export const ListPlusButton = styled.button<{ $isOpen: boolean }>` + background: none; + border: none; + cursor: pointer; + padding: 0.5rem; + display: flex; + align-items: center; + justify-content: center; transition: transform 500ms ease-in-out; ${({ $isOpen }) => $isOpen && css` transform: rotate(45deg); `} svg { width: 1.5rem; height: 1.5rem; } `;
36-43: 불필요한 여백 제거
ListButtonWrapper의margin-top값이 매우 작습니다(0.1rem). 이것이 의도적인 미세 조정인지 확인하세요. 아이콘을 수직 정렬하는 데align-items: center를 사용하는 것이 더 나을 수 있습니다.export const ListButtonWrapper = styled.div` - margin-top: 0.1rem; + display: flex; + align-items: center; svg { width: 1.5rem; height: 1.5rem; } `;
30-48: 반응형 디자인 고려 필요현재 스타일은 고정된
rem값을 사용하고 있어 다양한 화면 크기에 대응하기 어려울 수 있습니다. 특히 모바일 환경에서 콘텐츠가 적절히 표시되는지 확인하고, 필요에 따라 반응형 스타일을 추가하세요.다음과 같은 미디어 쿼리 추가를 고려해보세요:
export const ListContentWrapper = styled.div` margin: 0.5rem; display: flex; gap: 0.5rem; + + @media (max-width: 768px) { + margin: 0.25rem; + gap: 0.25rem; + } `; // ... (중략) ... export const ListContent = styled.div` font-size: 1.1rem; padding-right: 1.5rem; + + @media (max-width: 768px) { + font-size: 1rem; + padding-right: 1rem; + } `;
1-48: 다크 모드 지원 고려현재 스타일은 색상이 명시적으로 정의되어 있지 않아 시스템 기본값을 따를 것으로 보입니다. 앱에서 다크 모드를 지원한다면, 테마에 따른 색상 변경을 고려해보세요.
예시:
export const ListWrapper = styled.div` display: flex; justify-content: space-between; align-items: center; cursor: pointer; padding: 1rem 0; color: ${({ theme }) => theme.colors.text}; background-color: ${({ theme }) => theme.colors.background}; &:hover { background-color: ${({ theme }) => theme.colors.backgroundHover}; } `;이 방식은 앱에서 스타일드 컴포넌트의 ThemeProvider를 사용하고 있다는 가정하에 적용 가능합니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
src/api/customerService.api.ts(1 hunks)src/components/customerService/faq/FAQ.styled.ts(1 hunks)src/components/customerService/faq/FAQ.tsx(1 hunks)src/components/customerService/faq/FAQContent.styled.ts(1 hunks)src/components/customerService/faq/FAQContent.tsx(1 hunks)src/components/customerService/notice/Notice.tsx(1 hunks)src/components/mypage/myProfile/MyProfile.styled.ts(0 hunks)src/hooks/queries/keys.ts(1 hunks)src/hooks/useGetFAQ.ts(1 hunks)src/models/customerService.ts(1 hunks)src/pages/customerService/faq/FAQ.styled.ts(0 hunks)src/pages/customerService/faq/FAQ.tsx(0 hunks)src/routes/AppRoutes.tsx(1 hunks)
💤 Files with no reviewable changes (3)
- src/pages/customerService/faq/FAQ.styled.ts
- src/components/mypage/myProfile/MyProfile.styled.ts
- src/pages/customerService/faq/FAQ.tsx
🧰 Additional context used
🧬 Code Graph Analysis (5)
src/models/customerService.ts (2)
src/components/customerService/faq/FAQ.tsx (1)
FAQ(8-35)src/models/apiCommon.ts (1)
ApiCommonType(1-4)
src/hooks/useGetFAQ.ts (2)
src/hooks/queries/keys.ts (1)
CustomerService(47-50)src/api/customerService.api.ts (1)
getFAQ(4-13)
src/components/customerService/faq/FAQ.tsx (4)
src/models/customerService.ts (1)
FAQ(3-7)src/hooks/useGetFAQ.ts (1)
useGetFAQ(5-12)src/pages/customerService/CustomerServiceHeader.tsx (1)
CustomerServiceHeader(9-31)src/components/customerService/faq/FAQContent.tsx (1)
FAQContent(10-31)
src/components/customerService/faq/FAQContent.tsx (2)
src/components/customerService/faq/FAQ.tsx (1)
FAQ(8-35)src/models/customerService.ts (1)
FAQ(3-7)
src/components/customerService/faq/FAQ.styled.ts (1)
src/components/customerService/inquiry/Inquiry.styled.ts (1)
Container(3-8)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: accessibility-test
🔇 Additional comments (6)
src/hooks/queries/keys.ts (1)
47-50: 코드 추가가 잘 되었습니다!CustomerService 객체와 그 안의 faq, notice 키가 기존 코드 패턴을 잘 따르고 있습니다. 이 키들은 React Query를 통해 FAQ와 공지사항 데이터를 캐싱하는 데 사용될 것입니다.
src/routes/AppRoutes.tsx (1)
24-30: 경로 변경이 적절히 이루어졌습니다!FAQ, Notice, Inquiry 컴포넌트의 임포트 경로가 pages에서 components로 변경되었습니다. 이는 해당 요소들을 재사용 가능한 UI 컴포넌트로 다루는 구조적 변화를 잘 반영하고 있습니다.
src/api/customerService.api.ts (1)
1-13: API 구현이 잘 되었습니다!FAQ 데이터를 가져오는 API 함수가 적절하게 구현되었습니다. try/catch를 통한 오류 처리, 적절한 타입 지정(ApiFAQ), 그리고 응답 데이터의 올바른 반환이 잘 이루어졌습니다. 오류 발생 시 콘솔에 로깅하고 예외를 다시 throw하는 패턴도 좋습니다.
src/models/customerService.ts (1)
3-11: 적절한 인터페이스 설계입니다.FAQ 및 API 응답 구조를 위한 인터페이스가 깔끔하게 설계되었습니다. 각 FAQ 항목의 필수 속성(
id,title,content)이 명확하게 정의되어 있으며, API 응답 구조도 기존ApiCommonType을 확장하여 일관성 있게 구현되었습니다.src/components/customerService/faq/FAQContent.tsx (2)
6-8: Props 타입 정의가 명확합니다.FAQ 인터페이스를 활용한 props 타입 정의가 명확하고 잘 구성되어 있습니다.
10-12: useState 활용이 적절합니다.컴포넌트 상태 관리를 위한 useState 훅 사용이 적절합니다. boolean 타입을 명시적으로 지정한 것도 좋습니다.
src/hooks/useGetFAQ.ts
Outdated
| export const useGetFAQ = () => { | ||
| const { data: faqData, isLoading } = useQuery({ | ||
| queryKey: [CustomerService.faq], | ||
| queryFn: () => getFAQ(), | ||
| }); | ||
|
|
||
| return { faqData, isLoading }; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
쿼리 키 형식을 수정하세요.
queryKey 설정에 이슈가 있습니다. 현재 코드는 [CustomerService.faq]로 설정되어 있는데, 이는 사실상 [['faq']] 중첩 배열이 됩니다.
- queryKey: [CustomerService.faq],
+ queryKey: CustomerService.faq,또한 오류 상태 처리를 제공하면 컴포넌트에서 더 완전한 상태 관리가 가능합니다:
export const useGetFAQ = () => {
- const { data: faqData, isLoading } = useQuery({
+ const { data: faqData, isLoading, error } = useQuery({
queryKey: CustomerService.faq,
queryFn: () => getFAQ(),
});
- return { faqData, isLoading };
+ return { faqData, isLoading, error };
};📝 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.
| export const useGetFAQ = () => { | |
| const { data: faqData, isLoading } = useQuery({ | |
| queryKey: [CustomerService.faq], | |
| queryFn: () => getFAQ(), | |
| }); | |
| return { faqData, isLoading }; | |
| }; | |
| export const useGetFAQ = () => { | |
| const { data: faqData, isLoading, error } = useQuery({ | |
| queryKey: CustomerService.faq, | |
| queryFn: () => getFAQ(), | |
| }); | |
| return { faqData, isLoading, error }; | |
| }; |
| export default function FAQ() { | ||
| const { faqData, isLoading } = useGetFAQ(); | ||
|
|
||
| if (!faqData || faqData.length === 0) return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
데이터 부재 시 처리 로직을 개선하세요.
현재 코드는 데이터가 없을 때 아무것도 반환하지 않습니다(return;). 이는 컴포넌트에서 undefined를 반환하게 되어 잠재적인 렌더링 문제를 일으킬 수 있습니다.
- if (!faqData || faqData.length === 0) return;
+ if (!faqData || faqData.length === 0) return <S.Container><S.Wrapper>등록된 FAQ가 없습니다.</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.
| if (!faqData || faqData.length === 0) return; | |
| if (!faqData || faqData.length === 0) | |
| return <S.Container><S.Wrapper>등록된 FAQ가 없습니다.</S.Wrapper></S.Container>; |
| export default function FAQ() { | ||
| const { faqData, isLoading } = useGetFAQ(); | ||
|
|
||
| if (!faqData || faqData.length === 0) return; | ||
|
|
||
| if (isLoading) | ||
| return ( | ||
| <S.SpinnerWrapper> | ||
| <Spinner /> | ||
| </S.SpinnerWrapper> | ||
| ); | ||
|
|
||
| return ( | ||
| <> | ||
| <CustomerServiceHeader title='FAQ' /> | ||
| <S.Container> | ||
| <S.Wrapper> | ||
| {faqData.map((list) => ( | ||
| <Fragment key={list.id}> | ||
| <FAQContent list={list} /> | ||
| <S.ContentBorder></S.ContentBorder> | ||
| </Fragment> | ||
| ))} | ||
| </S.Wrapper> | ||
| </S.Container> | ||
| </> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
오류 처리 로직을 추가하세요.
현재 컴포넌트는 API 요청 오류를 처리하는 로직이 없습니다. 사용자에게 적절한 오류 메시지를 표시하여 UX를 개선할 수 있습니다.
export default function FAQ() {
- const { faqData, isLoading } = useGetFAQ();
+ const { faqData, isLoading, error } = useGetFAQ();
+ if (error) {
+ return (
+ <S.Container>
+ <S.Wrapper>
+ <div>FAQ 데이터를 불러오는 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.</div>
+ </S.Wrapper>
+ </S.Container>
+ );
+ }위 수정을 적용하려면 useGetFAQ 훅에서 error 상태도 반환해야 합니다.
📝 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.
| export default function FAQ() { | |
| const { faqData, isLoading } = useGetFAQ(); | |
| if (!faqData || faqData.length === 0) return; | |
| if (isLoading) | |
| return ( | |
| <S.SpinnerWrapper> | |
| <Spinner /> | |
| </S.SpinnerWrapper> | |
| ); | |
| return ( | |
| <> | |
| <CustomerServiceHeader title='FAQ' /> | |
| <S.Container> | |
| <S.Wrapper> | |
| {faqData.map((list) => ( | |
| <Fragment key={list.id}> | |
| <FAQContent list={list} /> | |
| <S.ContentBorder></S.ContentBorder> | |
| </Fragment> | |
| ))} | |
| </S.Wrapper> | |
| </S.Container> | |
| </> | |
| ); | |
| } | |
| export default function FAQ() { | |
| const { faqData, isLoading, error } = useGetFAQ(); | |
| if (error) { | |
| return ( | |
| <S.Container> | |
| <S.Wrapper> | |
| <div>FAQ 데이터를 불러오는 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.</div> | |
| </S.Wrapper> | |
| </S.Container> | |
| ); | |
| } | |
| if (!faqData || faqData.length === 0) return; | |
| if (isLoading) | |
| return ( | |
| <S.SpinnerWrapper> | |
| <Spinner /> | |
| </S.SpinnerWrapper> | |
| ); | |
| return ( | |
| <> | |
| <CustomerServiceHeader title='FAQ' /> | |
| <S.Container> | |
| <S.Wrapper> | |
| {faqData.map((list) => ( | |
| <Fragment key={list.id}> | |
| <FAQContent list={list} /> | |
| <S.ContentBorder></S.ContentBorder> | |
| </Fragment> | |
| ))} | |
| </S.Wrapper> | |
| </S.Container> | |
| </> | |
| ); | |
| } |
| <S.ListWrapper onClick={() => setIsFAQContentOpen((prev) => !prev)}> | ||
| <S.ListTitle>{list.title}</S.ListTitle> | ||
| <S.ListPlusButton type='button' $isOpen={isFAQContentOpen}> | ||
| <PlusIcon /> | ||
| </S.ListPlusButton> | ||
| </S.ListWrapper> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
접근성 개선이 필요합니다.
현재 FAQ를 토글하는 기능이 div에 onClick 이벤트로 구현되어 있어 키보드 접근성 문제가 있습니다. 또한 ARIA 속성이 없어 스크린 리더 사용자에게 상태 변화가 제대로 전달되지 않습니다.
-<S.ListWrapper onClick={() => setIsFAQContentOpen((prev) => !prev)}>
+<S.ListWrapper
+ onClick={() => setIsFAQContentOpen((prev) => !prev)}
+ role="button"
+ tabIndex={0}
+ aria-expanded={isFAQContentOpen}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ setIsFAQContentOpen((prev) => !prev);
+ }
+ }}
+>
<S.ListTitle>{list.title}</S.ListTitle>
<S.ListPlusButton type='button' $isOpen={isFAQContentOpen}>
<PlusIcon />
</S.ListPlusButton>
</S.ListWrapper>📝 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.
| <S.ListWrapper onClick={() => setIsFAQContentOpen((prev) => !prev)}> | |
| <S.ListTitle>{list.title}</S.ListTitle> | |
| <S.ListPlusButton type='button' $isOpen={isFAQContentOpen}> | |
| <PlusIcon /> | |
| </S.ListPlusButton> | |
| </S.ListWrapper> | |
| <S.ListWrapper | |
| onClick={() => setIsFAQContentOpen((prev) => !prev)} | |
| role="button" | |
| tabIndex={0} | |
| aria-expanded={isFAQContentOpen} | |
| onKeyDown={(e) => { | |
| if (e.key === 'Enter' || e.key === ' ') { | |
| e.preventDefault(); | |
| setIsFAQContentOpen((prev) => !prev); | |
| } | |
| }} | |
| > | |
| <S.ListTitle>{list.title}</S.ListTitle> | |
| <S.ListPlusButton type='button' $isOpen={isFAQContentOpen}> | |
| <PlusIcon /> | |
| </S.ListPlusButton> | |
| </S.ListWrapper> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
button 으로 바꿨잖아
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
import styled from 'styled-components';
export const ListContainer = styled.div`
padding: 2rem 0;
`;
export const ListWrapper = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
`;
export const ListTitle = styled.p`
font-size: 1.2rem;
font-weight: 700;
padding: 0 2rem;
`;
export const ListPlusButton = styled.button<{ $isOpen: boolean }>`
display: flex;
justify-content: center;
align-items: center;
margin-right: 2rem;
transform: ${({ $isOpen }) => ($isOpen ? 'rotate(45deg)' : 'none')};
transition: all 500ms;
svg {
width: 1.2rem;
height: 1.2rem;
}
`;
export const ListContentWrapper = styled.div`
display: flex;
justify-content: flex-start;
align-items: flex-start;
margin: 1rem 2rem;
gap: 1rem;
`;
export const ListButtonWrapper = styled.div`
display: flex;
justify-content: center;
align-items: center;
svg {
width: 1.2rem;
height: 1.2rem;
}
`;
export const ListContent = styled.p`
font-size: 1rem;
padding-right: 2rem;
`;
@YouD0313 네, ListPlusButton은 이미 버튼으로 구현되어 있는 것을 확인했습니다. 하지만 CodeRabbit의 제안은 ListWrapper 컴포넌트(현재 div로 구현됨)에 대한 접근성 개선이었습니다.
토글 기능을 담당하는 div에 role="button", tabIndex={0}, aria-expanded 속성 및 키보드 이벤트(onKeyDown)를 추가하면 키보드만으로도 FAQ를 열고 닫을 수 있어 접근성이 향상됩니다.
🏁 Script executed:
#!/bin/bash
# Check the definition of styled components in FAQContent.styled.ts
cat src/components/customerService/faq/FAQContent.styled.ts
Length of output: 965
@YouD0313 type='button' 속성을 추가하셨지만, 이것만으로는 완전한 접근성 개선이 되지 않습니다. ListPlusButton은 여전히 styled.div로 정의되어 있고, 주요 클릭 영역(ListWrapper)에는 접근성 속성이 없습니다.
CodeRabbit의 제안은 클릭 핸들러가 있는 ListWrapper에 키보드 접근성(onKeyDown)과 ARIA 속성(role, aria-expanded)을 추가하는 것이었습니다. 이는 키보드 사용자와 스크린 리더 사용자에게 필수적인 접근성 기능입니다.
진정한 접근성 개선을 위해서는 styled.div를 styled.button으로 변경하거나, CodeRabbit이 제안한 ARIA 속성과 키보드 이벤트 핸들러를 추가하는 것이 좋습니다.
🧬 Code Graph Analysis Results
Relevant snippet from src/models/customerService.ts (lines 3-7)
export interface FAQ {
id: number;
title: string;
content: string;
}- Defines the
FAQinterface used as the type for thelistprop inFAQContent. - Contains
id,title, andcontentfields.
Relevant snippet from src/components/customerService/faq/FAQ.tsx (lines 8-36)
export default function FAQ() {
const { faqData, isLoading } = useGetFAQ();
if (isLoading) {
return (
<S.SpinnerWrapper>
<Spinner />
</S.SpinnerWrapper>
);
}
if (!faqData || faqData.length === 0) return;
return (
<>
<CustomerServiceHeader title='FAQ' />
<S.Container>
<S.Wrapper>
{faqData.map((list) => (
<S.ToggleWrapper key={list.id}>
<FAQContent list={list} />
<S.ContentBorder></S.ContentBorder>
</S.ToggleWrapper>
))}
</S.Wrapper>
</S.Container>
</>
);
}- Fetches FAQ data and renders a list of
FAQContentcomponents. - Passes each FAQ item as
listprop toFAQContent. - Shows loading spinner while data is loading.
- Returns nothing if no FAQ data is available.
These snippets provide context on the data structure (FAQ interface) and how FAQContent is used within the FAQ list rendering component. This is relevant to understanding the current implementation of FAQContent.tsx and the user comment about changing to a button.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
다시 봐바
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
@YouD0313 죄송합니다. 제가 코드를 자세히 확인하지 않았네요. 다시 살펴본 결과, 이미 접근성을 고려하여 올바르게 구현하셨습니다:
ListWrapper가styled.button으로 구현되어 있음type='button'속성이 추가되어 있음- 컴포넌트 이름도 적절하게
ListPlusIcon으로 변경되어 있음
이미 시맨틱 HTML 버튼 요소를 사용하여 접근성 문제를 해결하셨습니다. 이전 리뷰 코멘트는 오래된 코드를 기반으로 했던 것 같습니다. 수정해 주셔서 감사합니다!
(__/)
(='.'=)
(")_(")
🏁 Script executed:
#!/bin/bash
# Check the current implementation of FAQContent.tsx and styled components
echo "=== FAQContent.tsx ==="
cat src/components/customerService/faq/FAQContent.tsx
echo -e "\n\n=== FAQContent.styled.ts ==="
cat src/components/customerService/faq/FAQContent.styled.ts
Length of output: 2235
@YouD0313 네, 죄송합니다. 코드를 다시 자세히 확인해보니 이미 접근성을 고려하여 올바르게 구현되어 있습니다:
ListWrapper가styled.button으로 정의되어 있음type='button'속성이 이미 추가되어 있음- 버튼 클릭으로 토글 기능이 제대로 구현되어 있음
이미 시맨틱 HTML을 사용하여 접근성을 확보하셨는데, 제가 제대로 확인하지 않고 코멘트를 남겼습니다. 지적해주셔서 감사합니다.
(__/)
(='.'=)
(")_(")
🧬 Code Graph Analysis Results
Relevant snippet from src/models/customerService.ts (lines 3-7)
export interface FAQ {
id: number;
title: string;
content: string;
}- Defines the
FAQinterface used as the type for thelistprop inFAQContent. - Contains
id,title, andcontentfields, all required.
Relevant snippet from src/components/customerService/faq/FAQ.tsx (lines 7-35)
export default function FAQ() {
const { faqData, isLoading } = useGetFAQ();
if (isLoading) {
return (
<S.SpinnerWrapper>
<Spinner />
</S.SpinnerWrapper>
);
}
if (!faqData || faqData.length === 0) return;
return (
<>
<CustomerServiceHeader title='FAQ' />
<S.Container>
<S.Wrapper>
{faqData.map((list) => (
<S.ToggleWrapper key={list.id}>
<FAQContent list={list} />
<S.ContentBorder></S.ContentBorder>
</S.ToggleWrapper>
))}
</S.Wrapper>
</S.Container>
</>
);
}- This component fetches FAQ data using
useGetFAQhook. - Displays a loading spinner while data is loading.
- If no data, returns nothing.
- Maps over
faqDataand rendersFAQContentfor each FAQ item. - Shows how
FAQContentis used and how thelistprop is passed.
These snippets provide context on the data structure (FAQ interface) and usage of the FAQContent component within the FAQ page.
우게 수정, profile 문구수정&constant관리
There was a problem hiding this 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 (2)
src/components/customerService/faq/FAQ.tsx (2)
19-19: 데이터 부재 시 처리 로직을 개선하세요.현재 코드는 데이터가 없을 때 아무것도 반환하지 않습니다(
return;). 이는 컴포넌트에서undefined를 반환하게 되어 잠재적인 렌더링 문제를 일으킬 수 있습니다.- if (!faqData || faqData.length === 0) return; + if (!faqData || faqData.length === 0) return <S.Container><S.Wrapper>등록된 FAQ가 없습니다.</S.Wrapper></S.Container>;
8-9: 오류 처리 로직을 추가하세요.현재 컴포넌트는 API 요청 오류를 처리하는 로직이 없습니다. 사용자에게 적절한 오류 메시지를 표시하여 UX를 개선할 수 있습니다.
export default function FAQ() { - const { faqData, isLoading } = useGetFAQ(); + const { faqData, isLoading, error } = useGetFAQ(); + if (error) { + return ( + <S.Container> + <S.Wrapper> + <div>FAQ 데이터를 불러오는 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.</div> + </S.Wrapper> + </S.Container> + ); + }
🧹 Nitpick comments (1)
src/components/customerService/faq/FAQ.tsx (1)
1-1: 사용되지 않는 import가 있습니다.
Fragment가 import 되었지만 사용되지 않고 있습니다. 불필요한 import는 제거하는 것이 좋습니다.-import { Fragment } from 'react/jsx-runtime';🧰 Tools
🪛 ESLint
[error] 1-1: 'Fragment' is defined but never used.
(@typescript-eslint/no-unused-vars)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (15)
src/components/customerService/faq/FAQ.styled.ts(1 hunks)src/components/customerService/faq/FAQ.tsx(1 hunks)src/components/customerService/faq/FAQContent.styled.ts(1 hunks)src/components/customerService/faq/FAQContent.tsx(1 hunks)src/components/mypage/ScrollWrapper.styled.ts(1 hunks)src/components/mypage/activityLog/inquiries/Inquiries.styled.ts(2 hunks)src/components/mypage/activityLog/inquiries/Inquiries.tsx(1 hunks)src/components/mypage/activityLog/inquiries/inquiry/Inquiry.styled.ts(1 hunks)src/components/mypage/activityLog/inquiries/inquiry/Inquiry.tsx(1 hunks)src/components/mypage/myProfile/profile/Profile.tsx(4 hunks)src/components/mypage/notifications/all/All.tsx(1 hunks)src/constants/myPageProfile.ts(1 hunks)src/hooks/queries/keys.ts(1 hunks)src/hooks/useGetFAQ.ts(1 hunks)src/hooks/useMyInfo.ts(1 hunks)
✅ Files skipped from review due to trivial changes (4)
- src/components/mypage/myProfile/profile/Profile.tsx
- src/components/mypage/activityLog/inquiries/Inquiries.tsx
- src/components/mypage/ScrollWrapper.styled.ts
- src/constants/myPageProfile.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- src/hooks/useGetFAQ.ts
- src/components/customerService/faq/FAQContent.tsx
- src/components/customerService/faq/FAQ.styled.ts
- src/components/customerService/faq/FAQContent.styled.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/components/mypage/activityLog/inquiries/Inquiries.styled.ts (1)
src/components/customerService/faq/FAQ.styled.ts (1)
ContentBorder(25-29)
🪛 ESLint
src/components/customerService/faq/FAQ.tsx
[error] 1-1: 'Fragment' is defined but never used.
(@typescript-eslint/no-unused-vars)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: accessibility-test
🔇 Additional comments (14)
src/components/mypage/activityLog/inquiries/inquiry/Inquiry.tsx (1)
25-28: 시맨틱 HTML 개선 적용 👍InquiryTitleWrapper를 버튼으로 변경하고 type='button' 속성을 추가하여 웹 접근성과 시맨틱 마크업이 향상되었습니다. 이 변경은 사용자 경험과 스크린 리더 호환성을 개선합니다.
src/components/mypage/activityLog/inquiries/inquiry/Inquiry.styled.ts (2)
7-10: 버튼 스타일 및 시맨틱 개선 👍div에서 button으로 변경하고 적절한 스타일을 적용하여 UI와 접근성이 개선되었습니다. 텍스트 정렬과 너비 설정이 적절합니다.
12-12: 그리드 레이아웃 비율 조정그리드 컬럼 비율이 Inquiries.styled.ts의 InquiriesTableHeadWrapper와 일치하도록 조정되어 일관된 UI를 제공합니다.
src/components/mypage/activityLog/inquiries/Inquiries.styled.ts (5)
5-5: 패딩 조정으로 UI 개선컨테이너에 수평 패딩을 추가하여 콘텐츠의 가독성과 시각적 여백이, 개선되었습니다.
12-18: 고정 헤더 기능 추가 👍테이블 헤더를 sticky 포지션으로 설정하여 사용자 경험이 크게 향상되었습니다. 스크롤 시에도 항상 헤더가 보이므로 내용 파악이 용이해집니다.
23-23: 일관된 그리드 레이아웃 적용테이블 헤더의 그리드 컬럼 비율을 Inquiry 컴포넌트와 일치시켜 레이아웃의 일관성을 유지했습니다.
29-33: 구분선 추가로 시각적 계층 구조 개선ContentBorder 컴포넌트를 추가하여 시각적으로 명확한 구분을 제공합니다. 이 스타일은 customerService/faq 모듈의 스타일과 일관성 있게 구현되었습니다.
53-53: 여백 조정으로 레이아웃 개선InquiriesWrapper의 상단 여백을 조정하여 콘텐츠 간 적절한 간격을 유지하여 가독성이 향상되었습니다.
src/hooks/queries/keys.ts (1)
47-50: 고객 서비스 관련 쿼리 키가 적절하게 추가되었습니다.FAQ 기능 구현을 위한 쿼리 키 추가가 잘 이루어졌습니다. 기존 키 구조와 일관성을 유지하면서 확장성 있게 구현되었습니다.
src/hooks/useMyInfo.ts (1)
28-28: 캐시 관리 설정이 적절하게 추가되었습니다.
gcTime: Infinity설정이 추가되어staleTime: Infinity와 함께 사용자 프로필 데이터가 메모리에서 제거되지 않도록 구성되었습니다. 이는 사용자 경험 향상에 도움이 될 것입니다.src/components/customerService/faq/FAQ.tsx (2)
11-17: 로딩 상태 처리가 잘 구현되었습니다.로딩 상태에서 Spinner 컴포넌트를 표시하는 것은 사용자 경험을 향상시키는 좋은 방법입니다.
21-35: FAQ 컴포넌트 렌더링 구조가 잘 구현되었습니다.고객 서비스 헤더와 FAQ 콘텐츠를 적절히 배치하고, map 함수를 사용하여 데이터를 동적으로 렌더링하는 구조가 잘 구현되었습니다. 각 FAQ 항목마다 고유한 key를 부여하는 것도 좋은 방식입니다.
src/components/mypage/notifications/all/All.tsx (2)
35-43: 필터링된 결과 계산 로직이 적절하게 구현되었습니다.
filterLength변수를 통해 현재 필터 조건에 맞는 알림 개수를 계산하는 로직이 잘 구현되었습니다. 이 방식은 코드 중복을 피하고 관심사를 분리하여 가독성을 높였습니다.
44-44: 빈 결과 처리 로직이 개선되었습니다.필터링 결과가 없는 경우에도 '콘텐츠 없음' 상태를 표시하도록 조건이 추가되어 사용자 경험이 향상되었습니다. 이는 사용자가 필터링 후 결과가 없을 때 명확한 피드백을 제공합니다.
There was a problem hiding this 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 (1)
src/hooks/useGetFAQ.ts (1)
6-16: 🛠️ Refactor suggestion에러 상태 처리를 추가하세요.
useGetFAQ 훅에서 isLoading 상태는 반환하고 있지만, 에러 상태는 반환하고 있지 않습니다. 컴포넌트에서 API 호출 실패 시 적절한 에러 처리를 위해 에러 상태를 추가하는 것이 좋습니다.
export const useGetFAQ = (keyword: SearchKeyword) => { - const { data: faqData, isLoading } = useQuery({ + const { data: faqData, isLoading, error } = useQuery({ // keyword 조회시 keyword 키 추가 queryKey: [CustomerService.faq, keyword], queryFn: () => getFAQ(keyword), staleTime: Infinity, gcTime: Infinity, }); - return { faqData, isLoading }; + return { faqData, isLoading, error }; };
🧹 Nitpick comments (4)
src/hooks/useGetFAQ.ts (1)
11-13: 캐시 갱신 전략을 재검토하세요.현재 staleTime과 gcTime 모두 Infinity로 설정되어 있어 데이터가 자동으로 갱신되지 않습니다. FAQ 데이터가 자주 업데이트되지 않는다면 문제가 없을 수 있지만, 데이터 업데이트를 고려해 적절한 시간 값으로 설정하는 것이 좋습니다.
export const useGetFAQ = (keyword: SearchKeyword) => { const { data: faqData, isLoading } = useQuery({ // keyword 조회시 keyword 키 추가 queryKey: [CustomerService.faq, keyword], queryFn: () => getFAQ(keyword), - staleTime: Infinity, - gcTime: Infinity, + staleTime: 1000 * 60 * 30, // 30분 + gcTime: 1000 * 60 * 60, // 1시간 }); return { faqData, isLoading }; };src/pages/customerService/faq/FAQ.tsx (2)
11-12: 타입 명시를 추가하세요.value 상태에 대한 타입이 명시적으로 지정되어 있지 않습니다. 코드 일관성을 위해 타입을 명시적으로 지정하는 것이 좋습니다.
const [keyword, setKeyword] = useState<SearchKeyword>({ keyword: '' }); - const [value, setValue] = useState<string>(''); + const [value, setValue] = useState<string>('');
38-43: 리스트 렌더링 시 의미있는 변수명을 사용하세요.map 함수에서 사용하는 변수명 'list'는 실제로 개별 FAQ 항목을 나타내므로 'item' 또는 'faq'와 같은 더 의미있는 이름을 사용하는 것이 좋습니다.
- {faqData.map((list) => ( - <S.ToggleWrapper key={list.id}> - <FAQContent list={list} /> - <S.ContentBorder></S.ContentBorder> - </S.ToggleWrapper> - ))} + {faqData.map((faqItem) => ( + <S.ToggleWrapper key={faqItem.id}> + <FAQContent list={faqItem} /> + <S.ContentBorder></S.ContentBorder> + </S.ToggleWrapper> + ))}src/components/customerService/CustomerServiceHeader.tsx (1)
48-49: 검색어 입력 필드의 value 처리 로직이 개선될 수 있습니다.현재
value={inputValue || keyword}를 사용하고 있는데, 이 로직이 사용자 경험에 혼란을 줄 수 있습니다. 예를 들어, 사용자가 keyword에 기반한 검색 결과를 보고 있는 상태에서 새로운 검색어를 입력할 때 입력 필드가 갑자기 빈 값으로 변할 수 있습니다.다음과 같이 컴포넌트 초기화 시 keyword를 inputValue에 설정하는 것이 좋습니다:
- const [inputValue, setInputValue] = useState<string>(''); + const [inputValue, setInputValue] = useState<string>(keyword); // useEffect를 추가하여 외부 keyword 변경 시 inputValue도 업데이트 + useEffect(() => { + setInputValue(keyword); + }, [keyword]); // 그리고 value 속성 수정 - value={inputValue || keyword} + value={inputValue}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
src/api/customerService.api.ts(1 hunks)src/components/common/header/Header.tsx(1 hunks)src/components/customerService/CustomerServiceHeader.styled.ts(1 hunks)src/components/customerService/CustomerServiceHeader.tsx(1 hunks)src/components/customerService/faq/FAQContent.styled.ts(1 hunks)src/constants/routes.ts(1 hunks)src/hooks/useGetFAQ.ts(1 hunks)src/models/customerService.ts(1 hunks)src/pages/customerService/CustomerServiceHeader.tsx(0 hunks)src/pages/customerService/faq/FAQ.styled.ts(1 hunks)src/pages/customerService/faq/FAQ.tsx(1 hunks)src/pages/customerService/notice/Notice.tsx(0 hunks)src/routes/AppRoutes.tsx(2 hunks)
💤 Files with no reviewable changes (2)
- src/pages/customerService/notice/Notice.tsx
- src/pages/customerService/CustomerServiceHeader.tsx
✅ Files skipped from review due to trivial changes (3)
- src/components/common/header/Header.tsx
- src/components/customerService/CustomerServiceHeader.styled.ts
- src/constants/routes.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- src/routes/AppRoutes.tsx
- src/components/customerService/faq/FAQContent.styled.ts
🧰 Additional context used
🧬 Code Graph Analysis (4)
src/models/customerService.ts (2)
src/pages/customerService/faq/FAQ.tsx (1)
FAQ(9-48)src/models/apiCommon.ts (1)
ApiCommonType(1-4)
src/hooks/useGetFAQ.ts (3)
src/models/customerService.ts (1)
SearchKeyword(13-15)src/hooks/queries/keys.ts (1)
CustomerService(47-50)src/api/customerService.api.ts (1)
getFAQ(4-13)
src/pages/customerService/faq/FAQ.styled.ts (3)
src/components/customerService/CustomerServiceHeader.styled.ts (1)
Container(3-9)src/pages/customerService/notice/Notice.styled.ts (1)
Container(3-3)src/components/customerService/inquiry/Inquiry.styled.ts (1)
Container(3-8)
src/components/customerService/CustomerServiceHeader.tsx (1)
src/components/customerService/MoveInquiredLink.tsx (1)
MovedInquiredLink(5-13)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: accessibility-test
🔇 Additional comments (17)
src/api/customerService.api.ts (1)
1-13: API 함수가 잘 구현되었습니다.getFAQ 함수가 SearchKeyword 파라미터를 받아 FAQ 데이터를 가져오는 로직이 명확하게 구현되어 있습니다. 에러 처리도 적절하게 되어 있어 좋습니다.
src/models/customerService.ts (1)
3-15: 인터페이스 정의가 명확히 구현되었습니다.FAQ, ApiFAQ, SearchKeyword 인터페이스가 잘 정의되어 있습니다. 타입 구조가 간결하고 명확해서 코드 가독성과 유지보수성이 좋아 보입니다.
src/pages/customerService/faq/FAQ.tsx (2)
10-17: 상태 관리가 잘 구현되어 있습니다.검색 키워드 상태 관리와 핸들러 함수가 잘 구현되어 있습니다. value 상태는 input 값을 관리하고, keyword 상태는 API 요청에 사용되는 구조로 적절히 분리되었습니다.
19-25: 로딩 상태 처리가 잘 구현되어 있습니다.API 호출 중에는 Spinner 컴포넌트를 보여주어 사용자에게 로딩 상태를 시각적으로 잘 표현하고 있습니다.
src/pages/customerService/faq/FAQ.styled.ts (5)
3-5: 로딩 상태를 위한 스피너 래퍼 컴포넌트가 추가되었습니다.스피너를 위한 고정 높이(60vh)를 가진 컨테이너로, 로딩 상태 표시를 위한 적절한 스타일링입니다.
7-13: FAQ 컨테이너의 레이아웃 개선이 적절합니다.이전에 빈 컨테이너였던 것과 달리, 마진과 중앙 정렬을 포함한 명확한 스타일링이 추가되었습니다. 상단(2rem)과 하단(5rem) 마진으로 적절한 간격을 제공하고, 플렉스박스 중앙 정렬을 통해 콘텐츠를 깔끔하게 배치합니다.
15-19: 내용 영역 너비를 제한하여 가독성을 향상시켰습니다.75% 너비 제한과 세로 방향 플렉스 레이아웃을 사용하여 FAQ 항목을 효과적으로 정렬합니다. 이는 사용자 경험 측면에서 좋은 접근 방식입니다.
21-21: 토글 래퍼가 스타일 없이 정의되었습니다.현재 스타일이 적용되지 않은 빈 래퍼 컴포넌트입니다. 향후 확장/축소 기능을 위한 것으로 보이지만, 현재는 사용되지 않는 것 같습니다.
이 컴포넌트가 현재 어떻게 사용되고 있는지, 아니면 향후 사용 예정인지 확인해주세요.
23-27: 구분선 스타일이 적절하게 정의되었습니다.테마의 placeholder 색상을 사용하여 미세한 높이(0.5px)의 가로 구분선을 만듭니다. FAQ 항목 사이의 시각적 구분을 위한 좋은 디자인 요소입니다.
src/components/customerService/CustomerServiceHeader.tsx (8)
1-9: 필요한 라이브러리와 컴포넌트 임포트가 잘 정의되었습니다.Heroicons에서 필요한 아이콘을 가져오고, 스타일 컴포넌트와 라우터 관련 컴포넌트를 적절히 임포트했습니다. React의 useState 훅을 사용하여 상태 관리를 준비했습니다.
10-14: 인터페이스 정의가 명확합니다.컴포넌트 props에 대한 타입 정의가 명확하게 되어 있습니다. title, keyword, onGetKeyword 함수를 포함하여 검색 기능을 위한 필수 속성들이 잘 설계되었습니다.
16-22: 컴포넌트와 상태 초기화가 적절합니다.props를 구조 분해 할당으로 받아오고, inputValue 상태를 useState로 초기화하여 사용자 입력을 관리합니다. 검색어 상태 관리를 위한 좋은 접근법입니다.
23-36: 이벤트 핸들러 함수들이 잘 구현되었습니다.폼 제출, 입력값 변경, 검색어 초기화를 위한 세 가지 핸들러 함수가 명확하게 구현되었습니다:
handleSubmitKeyword: 폼 제출 시 기본 동작을 방지하고 검색어를 상위 컴포넌트에 전달handleChangeValue: 입력 필드 값 변경 시 로컬 상태 업데이트handleReset: 검색어 초기화 시 로컬 상태와 상위 컴포넌트의 상태 모두 리셋각 함수의 책임이 명확하게 분리되어 있습니다.
38-43: 타이틀 섹션이 적절하게 구성되었습니다.헤더의 타이틀 부분에서 "DevPals"를 접두어로 사용하여 브랜드 아이덴티티를 강화하고, props로 전달받은 title을 함께 표시합니다.
44-50: 검색 폼 구성이 적절합니다.검색 폼과 입력 필드가 잘 구성되어 있습니다. value 속성에
inputValue || keyword를 사용하여 로컬 상태와 props 값 사이의 우선순위를 적절히 처리했습니다.
51-65: 버튼 영역이 조건부 렌더링으로 최적화되었습니다.버튼 영역에서:
- 검색어가 있을 때만 초기화 버튼이 표시되도록 조건부 렌더링을 사용
- 접근성을 위한 aria-label 속성 추가
- 적절한 아이콘 사용으로 직관적인 UI 제공
이는 사용자 경험을 향상시키는 좋은 디자인 패턴입니다.
66-69: 추가 링크와 중첩 라우팅 처리가 적절합니다.문의하기 링크(
MovedInquiredLink)와 중첩 라우팅을 위한Outlet컴포넌트가 적절히 배치되어 있습니다. 이를 통해 라우터 구조를 효율적으로 활용할 수 있습니다.
| export default function FAQ() { | ||
| const [keyword, setKeyword] = useState<SearchKeyword>({ keyword: '' }); | ||
| const [value, setValue] = useState<string>(''); | ||
| const { faqData, isLoading } = useGetFAQ(keyword); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
에러 처리를 추가하세요.
현재 useGetFAQ 훅에서 반환된 에러 상태를 처리하는 로직이 없습니다. API 호출 실패 시 사용자에게 알려주는 에러 처리를 추가하세요.
- const { faqData, isLoading } = useGetFAQ(keyword);
+ const { faqData, isLoading, error } = useGetFAQ(keyword);
+ // 에러 처리 추가
+ if (error) {
+ return (
+ <>
+ <CustomerServiceHeader
+ title='FAQ'
+ keyword={value}
+ onGetKeyword={handleGetKeyword}
+ />
+ <S.Container>
+ <S.Wrapper>
+ <S.ErrorMessage>
+ 데이터를 불러오는 중 오류가 발생했습니다. 다시 시도해 주세요.
+ </S.ErrorMessage>
+ </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.
| const { faqData, isLoading } = useGetFAQ(keyword); | |
| const { faqData, isLoading, error } = useGetFAQ(keyword); | |
| // 에러 처리 추가 | |
| if (error) { | |
| return ( | |
| <> | |
| <CustomerServiceHeader | |
| title='FAQ' | |
| keyword={value} | |
| onGetKeyword={handleGetKeyword} | |
| /> | |
| <S.Container> | |
| <S.Wrapper> | |
| <S.ErrorMessage> | |
| 데이터를 불러오는 중 오류가 발생했습니다. 다시 시도해 주세요. | |
| </S.ErrorMessage> | |
| </S.Wrapper> | |
| </S.Container> | |
| </> | |
| ); | |
| } |
There was a problem hiding this 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 (2)
src/pages/customerService/faq/FAQ.tsx (2)
13-13: 에러 처리를 추가하세요.현재 useGetFAQ 훅에서 반환된 에러 상태를 처리하는 로직이 없습니다. API 호출 실패 시 사용자에게 알려주는 에러 처리를 추가하세요.
- const { faqData, isLoading } = useGetFAQ(keyword); + const { faqData, isLoading, error } = useGetFAQ(keyword); + // 에러 처리 추가 + if (error) { + return ( + <> + <CustomerServiceHeader + title='FAQ' + keyword={value} + onGetKeyword={handleGetKeyword} + /> + <S.Container> + <S.Wrapper> + <S.ErrorMessage> + 데이터를 불러오는 중 오류가 발생했습니다. 다시 시도해 주세요. + </S.ErrorMessage> + </S.Wrapper> + </S.Container> + </> + ); + }
28-28: 🛠️ Refactor suggestion데이터 null/undefined 처리를 개선하세요.
현재 faqData가 없을 때 아무것도 반환하지 않는데, 이는 사용자에게 아무런 정보를 제공하지 않는 UI를 만듭니다. 적절한 메시지나 UI를 표시하는 것이 좋습니다.
- if (!faqData) return; + if (!faqData) { + return ( + <> + <CustomerServiceHeader + title='FAQ' + keyword={value} + onGetKeyword={handleGetKeyword} + /> + <S.Container> + <S.Wrapper> + <S.NoDataMessage> + FAQ 데이터를 불러올 수 없습니다. 다시 시도해 주세요. + </S.NoDataMessage> + </S.Wrapper> + </S.Container> + </> + ); + }
🧹 Nitpick comments (2)
src/pages/customerService/faq/FAQ.tsx (2)
40-45: 성능 최적화를 위한 제안.faqData를 map으로 순회할 때 key로 list.id를 사용하고 있어 좋습니다. 하지만 리스트가 많아질 경우 성능 최적화를 위해 메모이제이션을 고려해볼 수 있습니다.
+ // faqData가 변경될 때만 리렌더링 + const renderFAQItems = React.useMemo(() => { + return faqData.map((list) => ( <S.ToggleWrapper key={list.id}> <FAQContent list={list} /> <S.ContentBorder></S.ContentBorder> </S.ToggleWrapper> + )); + }, [faqData]); {faqData.length > 0 ? ( - faqData.map((list) => ( - <S.ToggleWrapper key={list.id}> - <FAQContent list={list} /> - <S.ContentBorder></S.ContentBorder> - </S.ToggleWrapper> - )) + renderFAQItems ) : ( <NoResult height='20rem' /> )}
15-18: useCallback을 사용하여 함수 메모이제이션 추가.handleGetKeyword 함수는 자식 컴포넌트로 전달되므로, useCallback을 사용하여 메모이제이션하는 것이 좋습니다. 이렇게 하면 불필요한 리렌더링을 방지할 수 있습니다.
- const handleGetKeyword = (keyword: string) => { + const handleGetKeyword = useCallback((keyword: string) => { setKeyword({ keyword }); setValue(keyword); - }; + }, []);이 변경을 적용하려면 import 문에 useCallback을 추가해야 합니다:
- import { useState } from 'react'; + import { useState, useCallback } from 'react';
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
src/components/common/emptyLoading/EmptyLoading.styled.ts(1 hunks)src/components/common/emptyLoading/EmptyLoading.tsx(1 hunks)src/components/common/noResult/NoResult.styled.ts(1 hunks)src/components/common/noResult/NoResult.tsx(1 hunks)src/components/home/projectCardLists/ProjectCardLists.tsx(3 hunks)src/pages/customerService/faq/FAQ.tsx(1 hunks)
✅ Files skipped from review due to trivial changes (5)
- src/components/common/noResult/NoResult.styled.ts
- src/components/common/emptyLoading/EmptyLoading.styled.ts
- src/components/home/projectCardLists/ProjectCardLists.tsx
- src/components/common/noResult/NoResult.tsx
- src/components/common/emptyLoading/EmptyLoading.tsx
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: accessibility-test
🔇 Additional comments (5)
src/pages/customerService/faq/FAQ.tsx (5)
11-13: 상태 관리 로직이 잘 구현되었습니다.검색 키워드를 관리하는 상태와 입력값을 관리하는 상태가 명확하게 분리되어 있어 좋습니다. useGetFAQ 훅을 사용한 데이터 페칭 방식도 적절합니다.
15-18: 검색 기능이 적절하게 구현되었습니다.키워드 검색 기능이 명확하게 구현되어 있습니다. 검색어를 입력하면 키워드 상태와 입력값 상태가 모두 업데이트되어 UI와 데이터 요청이 동기화됩니다.
20-26: 로딩 상태 처리가 잘 구현되었습니다.로딩 중에 스피너를 표시하는 부분이 잘 구현되어 있습니다. 사용자 경험 측면에서 데이터 로딩 중임을 명확하게 보여주고 있습니다.
32-36: 헤더 컴포넌트 구현이 적절합니다.CustomerServiceHeader 컴포넌트에 필요한 제목, 검색어, 검색 콜백 함수를 전달하고 있어 잘 구현되었습니다.
39-48: 조건부 렌더링이 적절히 구현되었습니다.데이터가 있을 때는 FAQ 목록을 표시하고, 없을 때는 NoResult 컴포넌트를 표시하는 조건부 렌더링이 잘 구현되어 있습니다. 검색 결과가 없을 때 사용자에게 시각적 피드백을 제공하는 것은 좋은 UX 실천입니다.
구현내용
FAQ api 연결, ui 구현
연관이슈
close #274
Summary by CodeRabbit
신규 기능
스타일
버그 수정
리팩터
기타