Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/api/customerService.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ApiFAQ } from '../models/customerService';
import { httpClient } from './http.api';

export const getFAQ = async () => {
try {
const response = await httpClient.get<ApiFAQ>(`/faq`);

return response.data.data;
} catch (e) {
console.error(e);
throw e;
}
};
26 changes: 26 additions & 0 deletions src/components/customerService/faq/FAQ.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import styled from 'styled-components';

export const SpinnerWrapper = styled.div`
height: 60vh;
`;

export const Container = styled.section`
margin-top: 2rem;
margin-bottom: 5rem;
width: 100%;
display: flex;
justify-content: center;
`;

export const Wrapper = styled.div`
width: 75%;
display: flex;
flex-direction: column;
gap: 1.5rem;
`;

export const ContentBorder = styled.div`
width: 100%;
height: 0.5px;
background: ${({ theme }) => theme.color.placeholder};
`;
35 changes: 35 additions & 0 deletions src/components/customerService/faq/FAQ.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Fragment } from 'react/jsx-runtime';
import { useGetFAQ } from '../../../hooks/useGetFAQ';
import CustomerServiceHeader from '../../../pages/customerService/CustomerServiceHeader';
import Spinner from '../../mypage/Spinner';
import * as S from './FAQ.styled';
import FAQContent from './FAQContent';

export default function FAQ() {
const { faqData, isLoading } = useGetFAQ();

if (!faqData || faqData.length === 0) 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

데이터 부재 시 처리 로직을 개선하세요.

현재 코드는 데이터가 없을 때 아무것도 반환하지 않습니다(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.

Suggested change
if (!faqData || faqData.length === 0) return;
if (!faqData || faqData.length === 0)
return <S.Container><S.Wrapper>등록된 FAQ가 없습니다.</S.Wrapper></S.Container>;


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>
</>
);
}
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 요청 오류를 처리하는 로직이 없습니다. 사용자에게 적절한 오류 메시지를 표시하여 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.

Suggested change
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>
</>
);
}

48 changes: 48 additions & 0 deletions src/components/customerService/faq/FAQContent.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import styled, { css } from 'styled-components';

export const ListContainer = styled.div``;

export const ListWrapper = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
`;

export const ListTitle = styled.div`
font-size: 1.3rem;
padding-left: 1.5rem;
font-weight: bold;
`;

export const ListPlusButton = styled.button<{ $isOpen: boolean }>`
transition: transform 500ms ease-in-out;
${({ $isOpen }) =>
$isOpen &&
css`
transform: rotate(45deg);
`}
svg {
width: 1.5rem;
height: 1.5rem;
}
`;

export const ListContentWrapper = styled.div`
margin: 0.5rem;
display: flex;
gap: 0.5rem;
`;

export const ListButtonWrapper = styled.div`
margin-top: 0.1rem;

svg {
width: 1.5rem;
height: 1.5rem;
}
`;

export const ListContent = styled.div`
font-size: 1.1rem;
padding-right: 1.5rem;
`;
31 changes: 31 additions & 0 deletions src/components/customerService/faq/FAQContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ChevronRightIcon, PlusIcon } from '@heroicons/react/24/outline';
import { FAQ } from '../../../models/customerService';
import * as S from './FAQContent.styled';
import { useState } from 'react';

interface FAQContentProps {
list: FAQ;
}

export default function FAQContent({ list }: FAQContentProps) {
const [isFAQContentOpen, setIsFAQContentOpen] = useState<boolean>(false);

return (
<S.ListContainer>
<S.ListWrapper onClick={() => setIsFAQContentOpen((prev) => !prev)}>
<S.ListTitle>{list.title}</S.ListTitle>
<S.ListPlusButton type='button' $isOpen={isFAQContentOpen}>
<PlusIcon />
</S.ListPlusButton>
</S.ListWrapper>
Copy link

@coderabbitai coderabbitai bot May 6, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

접근성 개선이 필요합니다.

현재 FAQ를 토글하는 기능이 divonClick 이벤트로 구현되어 있어 키보드 접근성 문제가 있습니다. 또한 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.

Suggested change
<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>

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

button 으로 바꿨잖아

Copy link

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 FAQ interface used as the type for the list prop in FAQContent.
  • Contains id, title, and content fields.

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 FAQContent components.
  • Passes each FAQ item as list prop to FAQContent.
  • 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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

다시 봐바

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

@YouD0313 죄송합니다. 제가 코드를 자세히 확인하지 않았네요. 다시 살펴본 결과, 이미 접근성을 고려하여 올바르게 구현하셨습니다:

  1. ListWrapperstyled.button으로 구현되어 있음
  2. type='button' 속성이 추가되어 있음
  3. 컴포넌트 이름도 적절하게 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 네, 죄송합니다. 코드를 다시 자세히 확인해보니 이미 접근성을 고려하여 올바르게 구현되어 있습니다:

  1. ListWrapperstyled.button으로 정의되어 있음
  2. type='button' 속성이 이미 추가되어 있음
  3. 버튼 클릭으로 토글 기능이 제대로 구현되어 있음

이미 시맨틱 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 FAQ interface used as the type for the list prop in FAQContent.
  • Contains id, title, and content fields, 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 useGetFAQ hook.
  • Displays a loading spinner while data is loading.
  • If no data, returns nothing.
  • Maps over faqData and renders FAQContent for each FAQ item.
  • Shows how FAQContent is used and how the list prop is passed.

These snippets provide context on the data structure (FAQ interface) and usage of the FAQContent component within the FAQ page.

{isFAQContentOpen && (
<S.ListContentWrapper>
<S.ListButtonWrapper>
<ChevronRightIcon />
</S.ListButtonWrapper>
<S.ListContent>{list.content}</S.ListContent>
</S.ListContentWrapper>
)}
</S.ListContainer>
);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import CustomerServiceHeader from '../CustomerServiceHeader';
import CustomerServiceHeader from '../../../pages/customerService/CustomerServiceHeader';
import * as S from './Notice.styled';

export default function Notice() {
Expand Down
3 changes: 0 additions & 3 deletions src/components/mypage/myProfile/MyProfile.styled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,5 @@ export const Container = styled.div`
`;

export const SectionContainer = styled.section`
background-color: ${({ theme }) => theme.color.lightgrey};
border-radius: ${({ theme }) => theme.borderRadius.large} 0 0
${({ theme }) => theme.borderRadius.large};
padding: 2rem;
`;
5 changes: 5 additions & 0 deletions src/hooks/queries/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,8 @@ export const ActivityLog = {
myComments: ['MyComments'],
myInquiries: ['MyInquiries'],
};

export const CustomerService = {
faq: ['faq'],
notice: ['notice'],
};
12 changes: 12 additions & 0 deletions src/hooks/useGetFAQ.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useQuery } from '@tanstack/react-query';
import { getFAQ } from '../api/customerService.api';
import { CustomerService } from './queries/keys';

export const useGetFAQ = () => {
const { data: faqData, isLoading } = useQuery({
queryKey: [CustomerService.faq],
queryFn: () => getFAQ(),
});

return { faqData, isLoading };
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

쿼리 키 형식을 수정하세요.

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.

Suggested change
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 };
};

11 changes: 11 additions & 0 deletions src/models/customerService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ApiCommonType } from './apiCommon';

export interface FAQ {
id: number;
title: string;
content: string;
}

export interface ApiFAQ extends ApiCommonType {
data: FAQ[];
}
3 changes: 0 additions & 3 deletions src/pages/customerService/faq/FAQ.styled.ts

This file was deleted.

11 changes: 0 additions & 11 deletions src/pages/customerService/faq/FAQ.tsx

This file was deleted.

10 changes: 7 additions & 3 deletions src/routes/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,13 @@ const ChangePassword = lazy(
const Main = lazy(() => import('../pages/main/Index'));
const Layout = lazy(() => import('../components/common/layout/Layout'));
const Home = lazy(() => import('../pages/home/Home'));
const FAQ = lazy(() => import('../pages/customerService/faq/FAQ'));
const Notice = lazy(() => import('../pages/customerService/notice/Notice'));
const Inquiry = lazy(() => import('../pages/customerService/inquiry/Inquiry'));
const FAQ = lazy(() => import('../components/customerService/faq/FAQ'));
const Notice = lazy(
() => import('../components/customerService/notice/Notice')
);
const Inquiry = lazy(
() => import('../components/customerService/inquiry/Inquiry')
);
const MyPage = lazy(() => import('../pages/mypage/MyPage'));
const UserPage = lazy(() => import('../pages/userpage/UserPage'));
const Apply = lazy(() => import('../pages/apply/ApplyStep'));
Expand Down