Skip to content
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 페이지 CRUD 적용 #228

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
Draft

FAQ 페이지 CRUD 적용 #228

wants to merge 13 commits into from

Conversation

ujinsim
Copy link
Collaborator

@ujinsim ujinsim commented Feb 1, 2025

🔥 연관 이슈

🚀 작업 내용

저장버튼을 누르면 기존 FAQ를 지우거나 새로운 FAQ를 작성할 수 있도록 했습니다

🤔 고민했던 내용

기존에 내용을 수정하도록 해야될지 고민을 했었는데 수정버튼이 따로 있기에 한번에 추가와 수정을 같이 하는방향으로 결정하였습니다
그리고 새로운 데이터를 바로 반영하는 방식으로 refetch를 사용하였습니다

💬 리뷰 중점사항

Summary by CodeRabbit

  • 새로운 기능

    • FAQ 관리 기능 추가
    • FAQ 조회, 생성, 수정, 삭제 기능 구현
    • 관리자 페이지에서 FAQ 인터페이스 개선
  • 버그 수정

    • FAQ 데이터 처리 안정성 향상
  • 리팩터링

    • FAQ 관련 API, 훅, 컴포넌트 구조 최적화
    • 타입 정의 및 상수 관리 개선

Copy link

vercel bot commented Feb 1, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
ddingdong-fe ❌ Failed (Inspect) Feb 1, 2025 6:09am

Copy link

coderabbitai bot commented Feb 1, 2025

Walkthrough

이 풀 리퀘스트는 FAQ(자주 묻는 질문) 관리 기능을 구현하기 위한 포괄적인 변경 사항을 포함하고 있습니다. 주요 변경 사항은 FAQ 데이터를 가져오고, 생성하고, 업데이트하고, 삭제할 수 있는 API 엔드포인트와 관련 훅, 컴포넌트를 추가하는 것입니다. 이를 통해 관리자가 FAQ 콘텐츠를 동적으로 관리할 수 있는 인터페이스를 제공합니다.

Changes

파일 변경 요약
src/apis/index.ts FAQ 관련 API 메서드 추가 (getAllFaq, createFaq, deleteFaq, updateFaq)
src/components/faq/FAQActions.tsx FAQ 액션 컴포넌트 생성
src/components/faq/FAQList.tsx FAQ 목록 관리 컴포넌트 추가
src/components/ui/accordion.tsx AccordionTrigger에 선택적 화살표 속성 추가
src/constants/qna.ts QnA 상수 제거
src/hooks/api/faq/* FAQ 관련 React Query 훅 추가 (useAllFaq, useCreateFaq, useDeleteFaq, usePatchFaq)
src/pages/admin/FAQ/index.tsx FAQ 페이지 관리 로직 업데이트
src/types/faq.ts FAQ 관련 TypeScript 타입 정의

Assessment against linked issues

목표 해결 여부 설명
FAQ 페이지 CRUD 구현

Suggested labels

⚛️ 프론트엔드

Suggested reviewers

  • keemsebin
✨ Finishing Touches
  • 📝 Generate Docstrings (Beta)

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

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

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. (Beta)
  • @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.

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.

@ujinsim ujinsim changed the title FAQ페이지 CRUD 적용 FAQ 페이지 CRUD 적용 Feb 1, 2025
@ujinsim ujinsim added ⚛️ 프론트엔드 프론트엔드 관련 이슈 ✨ 기능 기능 개발 및 구현 labels Feb 1, 2025
@ujinsim ujinsim self-assigned this Feb 1, 2025
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: 7

🧹 Nitpick comments (8)
src/hooks/api/faq/useAllFaq.ts (2)

7-9: 에러 처리 개선이 필요합니다.

콘솔 로그만으로는 사용자에게 에러 상황을 알리기 어렵습니다. 토스트 메시지나 에러 상태를 상위 컴포넌트에 전달하는 방식으로 개선하는 것이 좋겠습니다.


4-11: 캐싱 전략과 재시도 로직 추가를 고려해보세요.

데이터 갱신이 빈번하지 않은 FAQ의 특성을 고려하여, 적절한 캐싱 전략을 설정하면 성능을 개선할 수 있습니다. 또한 네트워크 오류 시 자동 재시도 로직을 추가하면 좋겠습니다.

 export const useAllFaq = (token: string) => {
   return useQuery(['allFaqs'], () => getAllFaq(token), {
     enabled: !!token,
+    staleTime: 5 * 60 * 1000, // 5분
+    cacheTime: 30 * 60 * 1000, // 30분
+    retry: 3,
     onError: (error) => {
       console.error('FAQ 가져오기 실패', error);
     },
   });
 };
src/types/faq.ts (1)

1-27: 타입에 유효성 검사 제약 조건 추가를 고려해보세요.

문자열 길이 제한이나 필수값 표시 등의 제약 조건을 추가하면 타입 안정성을 더욱 높일 수 있습니다.

 export type FAQItem = {
-  question: string;
-  reply: string;
+  question: string & { minLength: 5; maxLength: 200 };
+  reply: string & { minLength: 10; maxLength: 1000 };
 };
src/hooks/api/faq/useDeleteFaq.ts (1)

24-26: 에러 처리 개선이 필요합니다.

에러 메시지를 콘솔에만 출력하는 것보다 사용자에게 표시하는 것이 좋습니다.

 onError: (error) => {
-  console.error('FAQ 삭제 실패:', error);
+  return error.response?.data?.message || 'FAQ 삭제에 실패했습니다.';
 },
src/components/faq/FAQActions.tsx (1)

20-45: 버튼 스타일 중복 코드를 개선할 수 있습니다.

취소, 저장하기, 수정하기 버튼에서 공통 스타일이 중복되어 있습니다. 이를 공통 컴포넌트로 추출하면 코드 재사용성이 향상됩니다.

+const ButtonBase = `h-10 rounded-lg px-4.5 py-2 text-sm font-bold`;
+const CancelButton = `${ButtonBase} bg-gray-100 text-gray-500 hover:bg-gray-200`;
+const SaveButton = `${ButtonBase} text-white bg-blue-500 hover:bg-blue-600`;
+const EditButton = `${ButtonBase} bg-blue-100 text-blue-500 hover:bg-blue-200`;

 <button
   onClick={() => setIsEditing(false)}
-  className="ml-3 h-10 rounded-lg bg-gray-100 px-4.5 py-2 text-sm font-bold text-gray-500 hover:bg-gray-200"
+  className={`ml-3 ${CancelButton}`}
 >
src/pages/admin/FAQ/index.tsx (1)

16-26: FAQ 관리 로직을 커스텀 훅으로 분리하는 것이 좋습니다.

상태 관리와 FAQ 추가 로직을 커스텀 훅으로 분리하면 코드의 재사용성과 테스트 용이성이 향상됩니다.

+// useFAQManagement.ts
+export function useFAQManagement() {
+  const [isEditing, setIsEditing] = useState(false);
+  const [newFAQs, setNewFAQs] = useState<{ question: string; reply: string }[]>([]);
+
+  const addFAQ = () => {
+    setNewFAQs([
+      ...newFAQs,
+      { question: '질문을 입력해주세요', reply: '답변을 입력해주세요' },
+    ]);
+  };
+
+  return { isEditing, setIsEditing, newFAQs, setNewFAQs, addFAQ };
+}
src/components/faq/FAQList.tsx (2)

35-36: 토큰 유효성 검사 추가 필요

토큰을 쿠키에서 추출할 때 유효성 검사가 누락되어 있습니다.

다음과 같이 토큰 유효성 검사를 추가해주세요:

  const [cookies] = useCookies(['token', 'role']);
  const { token } = cookies;
+ if (!token) {
+   toast.error('인증 정보가 없습니다.');
+   return null;
+ }

78-91: 접근성 속성 추가 필요

입력 필드에 접근성 속성이 누락되어 있어 스크린 리더 사용자의 접근성이 제한됩니다.

다음과 같이 접근성 속성을 추가해주세요:

  <input
    className="w-11/12"
    placeholder={item.question}
    value={item.question}
+   aria-label="FAQ 질문"
+   id={`faq-question-${index}`}
    onChange={(e) =>
      setNewFAQs((prev) =>
        prev.map((faq, i) =>
          i === index
            ? { ...faq, question: e.target.value }
            : faq,
        ),
      )
    }
  />
  ...
  <textarea
    className="w-full resize-none rounded-md border border-gray-200 bg-gray-50 p-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
    placeholder="답변을 입력해주세요"
    value={item.reply}
    rows={3}
+   aria-label="FAQ 답변"
+   id={`faq-reply-${index}`}
    onChange={(e) =>
      setNewFAQs((prev) =>
        prev.map((faq, i) =>
          i === index ? { ...faq, reply: e.target.value } : faq,
        ),
      )
    }
  />

Also applies to: 102-114

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5d34838 and 36cb6af.

📒 Files selected for processing (11)
  • src/apis/index.ts (2 hunks)
  • src/components/faq/FAQActions.tsx (1 hunks)
  • src/components/faq/FAQList.tsx (1 hunks)
  • src/components/ui/accordion.tsx (1 hunks)
  • src/constants/qna.ts (0 hunks)
  • src/hooks/api/faq/useAllFaq.ts (1 hunks)
  • src/hooks/api/faq/useCreateFaq.ts (1 hunks)
  • src/hooks/api/faq/useDeleteFaq.ts (1 hunks)
  • src/hooks/api/faq/usePatchFaq.ts (1 hunks)
  • src/pages/admin/FAQ/index.tsx (1 hunks)
  • src/types/faq.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • src/constants/qna.ts
🔇 Additional comments (2)
src/components/ui/accordion.tsx (1)

23-44: 구현이 깔끔하고 잘 되어있습니다.

  • isArrow prop의 기본값 설정이 적절합니다.
  • 조건부 렌더링이 명확하게 구현되어 있습니다.
src/apis/index.ts (1)

643-689: 구현이 잘 되었습니다!

FAQ 관련 API 함수들이 잘 구현되어 있습니다:

  • 적절한 타입 정의
  • 일관된 토큰 처리
  • 에러 처리 구현

Comment on lines +4 to +13
export const useCreateFaq = () => {
return useMutation(createFaq, {
onSuccess: () => {
console.log('FAQ 생성성공');
},
onError: (error) => {
console.error('FAQ 생성실패', error);
},
});
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

쿼리 무효화 및 사용자 피드백 개선이 필요합니다.

FAQ 생성 후 자동으로 목록을 갱신하고 사용자에게 적절한 피드백을 제공하도록 개선이 필요합니다.

+import { useQueryClient } from '@tanstack/react-query';
+import { toast } from 'react-toastify';

 export const useCreateFaq = () => {
+  const queryClient = useQueryClient();
   return useMutation(createFaq, {
     onSuccess: () => {
-      console.log('FAQ 생성성공');
+      queryClient.invalidateQueries(['allFaqs']);
+      toast.success('FAQ가 성공적으로 생성되었습니다.');
     },
     onError: (error) => {
-      console.error('FAQ 생성실패', error);
+      toast.error('FAQ 생성에 실패했습니다. 다시 시도해주세요.');
     },
   });
 };
📝 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 useCreateFaq = () => {
return useMutation(createFaq, {
onSuccess: () => {
console.log('FAQ 생성성공');
},
onError: (error) => {
console.error('FAQ 생성실패', error);
},
});
};
import { useQueryClient } from '@tanstack/react-query';
import { toast } from 'react-toastify';
export const useCreateFaq = () => {
const queryClient = useQueryClient();
return useMutation(createFaq, {
onSuccess: () => {
queryClient.invalidateQueries(['allFaqs']);
toast.success('FAQ가 성공적으로 생성되었습니다.');
},
onError: (error) => {
toast.error('FAQ 생성에 실패했습니다. 다시 시도해주세요.');
},
});
};

Comment on lines +4 to +13
export const usePatchFaq = () => {
return useMutation(updateFaq, {
onSuccess: () => {
console.log('FAQ 수정 성공');
},
onError: (error) => {
console.error('FAQ 수정 실패', error);
},
});
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

쿼리 무효화 및 에러 처리 개선이 필요합니다.

FAQ 수정 후 목록 갱신과 사용자 피드백 처리가 필요합니다.

+import { useQueryClient } from '@tanstack/react-query';
+import { toast } from 'react-toastify';

 export const usePatchFaq = () => {
+  const queryClient = useQueryClient();
   return useMutation(updateFaq, {
     onSuccess: () => {
-      console.log('FAQ 수정 성공');
+      queryClient.invalidateQueries(['allFaqs']);
+      toast.success('FAQ가 성공적으로 수정되었습니다.');
     },
     onError: (error) => {
-      console.error('FAQ 수정 실패', error);
+      toast.error('FAQ 수정에 실패했습니다. 다시 시도해주세요.');
     },
   });
 };
📝 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 usePatchFaq = () => {
return useMutation(updateFaq, {
onSuccess: () => {
console.log('FAQ 수정 성공');
},
onError: (error) => {
console.error('FAQ 수정 실패', error);
},
});
};
import { useQueryClient } from '@tanstack/react-query';
import { toast } from 'react-toastify';
export const usePatchFaq = () => {
const queryClient = useQueryClient();
return useMutation(updateFaq, {
onSuccess: () => {
queryClient.invalidateQueries(['allFaqs']);
toast.success('FAQ가 성공적으로 수정되었습니다.');
},
onError: (error) => {
toast.error('FAQ 수정에 실패했습니다. 다시 시도해주세요.');
},
});
};

Comment on lines +8 to +11
export type CreateFaq = {
token: string;
faqData: object;
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

CreateFaq 타입의 faqData를 구체화해야 합니다.

object 타입은 너무 일반적이며 타입 안정성을 보장하지 않습니다. FAQItem 타입을 재사용하는 것이 좋겠습니다.

 export type CreateFaq = {
   token: string;
-  faqData: object;
+  faqData: FAQItem;
 };
📝 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 type CreateFaq = {
token: string;
faqData: object;
};
export type CreateFaq = {
token: string;
faqData: FAQItem;
};

Comment on lines +57 to +87
<div className="flex items-end justify-between">
<Heading>FAQ 관리</Heading>
{isEditing ? (
<div>
<button
onClick={() => setIsEditing(false)}
className="ml-3 h-10 rounded-lg bg-gray-100 px-4.5 py-2 text-sm font-bold text-gray-500 hover:bg-gray-200"
>
취소
</button>
<button
onClick={saveFAQ}
className={`ml-3 h-10 rounded-lg px-4.5 py-2 text-sm font-bold text-white
${
isSaving
? 'cursor-not-allowed bg-gray-500'
: 'bg-blue-500 hover:bg-blue-600'
}`}
disabled={isSaving}
>
저장하기
</button>
</div>
) : (
<button
onClick={() => setIsEditing(true)}
className="ml-3 h-10 rounded-lg bg-blue-100 px-4.5 py-2 text-sm font-bold text-blue-500 hover:bg-blue-200"
>
수정하기
</button>
)}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

버튼 관련 코드를 FAQActions 컴포넌트로 이동하는 것이 좋습니다.

현재 버튼 관련 코드가 FAQActions 컴포넌트와 중복되어 있습니다. 이를 FAQActions 컴포넌트로 이동하여 코드 중복을 제거하고 재사용성을 높일 수 있습니다.

Comment on lines +28 to +49
const saveFAQ = async () => {
if (newFAQs.length === 0) return;

try {
await Promise.all(
newFAQs.map((faq) =>
createFaq(
{ token, ...faq },
{
onSuccess: () => {
refetch();
setNewFAQs([]);
toast.success('FAQ가 성공적으로 저장되었습니다');
},
},
),
),
);
} catch (error) {
toast.error('FAQ 저장에 실패하였습니다');
}
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

saveFAQ 함수의 에러 처리를 개선해야 합니다.

현재 구현에서는 일반적인 에러 메시지만 표시됩니다. 구체적인 에러 정보를 제공하면 사용자가 문제를 더 잘 이해할 수 있습니다.

 } catch (error) {
-  toast.error('FAQ 저장에 실패하였습니다');
+  if (error.response?.status === 400) {
+    toast.error('입력된 내용을 확인해주세요');
+  } else if (error.response?.status === 401) {
+    toast.error('권한이 없습니다');
+  } else {
+    toast.error('FAQ 저장에 실패하였습니다. 잠시 후 다시 시도해주세요');
+  }
 }
📝 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 saveFAQ = async () => {
if (newFAQs.length === 0) return;
try {
await Promise.all(
newFAQs.map((faq) =>
createFaq(
{ token, ...faq },
{
onSuccess: () => {
refetch();
setNewFAQs([]);
toast.success('FAQ가 성공적으로 저장되었습니다');
},
},
),
),
);
} catch (error) {
toast.error('FAQ 저장에 실패하였습니다');
}
};
const saveFAQ = async () => {
if (newFAQs.length === 0) return;
try {
await Promise.all(
newFAQs.map((faq) =>
createFaq(
{ token, ...faq },
{
onSuccess: () => {
refetch();
setNewFAQs([]);
toast.success('FAQ가 성공적으로 저장되었습니다');
},
},
),
),
);
} catch (error) {
if (error.response?.status === 400) {
toast.error('입력된 내용을 확인해주세요');
} else if (error.response?.status === 401) {
toast.error('권한이 없습니다');
} else {
toast.error('FAQ 저장에 실패하였습니다. 잠시 후 다시 시도해주세요');
}
}
};

Comment on lines +40 to +58
const isClickedDeleteButton = (questionId?: number) => {
if (!questionId || !token) {
toast.error('인증 정보가 없습니다.');
return;
}

deleteFaq(
{ questionId, token },
{
onSuccess: () => {
refetch();
toast.success('FAQ가 삭제되었습니다.');
},
onError: () => {
toast.error('FAQ 삭제에 실패했습니다.');
},
},
);
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

삭제 작업 전 확인 대화상자 추가 필요

FAQ 삭제 시 사용자 확인 없이 바로 삭제가 진행됩니다. 실수로 인한 삭제를 방지하기 위해 확인 절차가 필요합니다.

다음과 같이 확인 대화상자를 추가해주세요:

  const isClickedDeleteButton = (questionId?: number) => {
    if (!questionId || !token) {
      toast.error('인증 정보가 없습니다.');
      return;
    }

+   if (!window.confirm('정말로 이 FAQ를 삭제하시겠습니까?')) {
+     return;
+   }

    deleteFaq(
      { questionId, token },
      {
        onSuccess: () => {
📝 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 isClickedDeleteButton = (questionId?: number) => {
if (!questionId || !token) {
toast.error('인증 정보가 없습니다.');
return;
}
deleteFaq(
{ questionId, token },
{
onSuccess: () => {
refetch();
toast.success('FAQ가 삭제되었습니다.');
},
onError: () => {
toast.error('FAQ 삭제에 실패했습니다.');
},
},
);
};
const isClickedDeleteButton = (questionId?: number) => {
if (!questionId || !token) {
toast.error('인증 정보가 없습니다.');
return;
}
if (!window.confirm('정말로 이 FAQ를 삭제하시겠습니까?')) {
return;
}
deleteFaq(
{ questionId, token },
{
onSuccess: () => {
refetch();
toast.success('FAQ가 삭제되었습니다.');
},
onError: () => {
toast.error('FAQ 삭제에 실패했습니다.');
},
},
);
};

Comment on lines +18 to +24
interface FAQListProps {
FAQ: any;
newFAQs: FAQItem[];
setNewFAQs: React.Dispatch<React.SetStateAction<FAQItem[]>>;
isEditing: boolean;
refetch: () => void;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

FAQ 타입 안전성 개선 필요

FAQ prop의 타입이 any로 지정되어 있어 타입 안전성이 보장되지 않습니다.

다음과 같이 명시적인 타입을 지정해주세요:

interface FAQListProps {
-  FAQ: any;
+  FAQ: { data: FAQItem[] } | undefined;
  newFAQs: FAQItem[];
  setNewFAQs: React.Dispatch<React.SetStateAction<FAQItem[]>>;
  isEditing: boolean;
  refetch: () => void;
}
📝 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
interface FAQListProps {
FAQ: any;
newFAQs: FAQItem[];
setNewFAQs: React.Dispatch<React.SetStateAction<FAQItem[]>>;
isEditing: boolean;
refetch: () => void;
}
interface FAQListProps {
FAQ: { data: FAQItem[] } | undefined;
newFAQs: FAQItem[];
setNewFAQs: React.Dispatch<React.SetStateAction<FAQItem[]>>;
isEditing: boolean;
refetch: () => void;
}

Copy link
Contributor

@yougyung yougyung left a comment

Choose a reason for hiding this comment

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

수고 많으셨어요 👍

아래 리스트는 확인이 필요해 보여요 🙇‍♂️

  • 수정 기능
    • 수정 취소 -> 수정버튼 재클릭시, 이전 수정사항이 남아있는 부분(초기화 동작x)
    • 입력이 활성화되지 않는 부분
  • 일반 유저 화면에서는 데이터가 노출 되고 있지 않는 부분
스크린샷 2025-02-01 오후 4 39 34
  • 빌드 실패

Comment on lines +658 to +661
const formData = new URLSearchParams();
formData.append('question', question);
formData.append('reply', reply);

Copy link
Contributor

Choose a reason for hiding this comment

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

  1. searchParams을 사용하신 이유가 궁금해요!
  2. 네이밍은 formData이네요?!

const addFAQ = () => {
setNewFAQs([
...newFAQs,
{ question: '질문을 입력해주세요', reply: '답변을 입력해주세요' },
Copy link
Contributor

Choose a reason for hiding this comment

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

'' 빈값으로 처리하고 placeholder를 적용하는 것은 어떨까요?

  • 현재는 답변에만 placeholder가 적용되고 있어요.
  • 서비스 전체에서 focus시 border효과를 제거해둬서 요 부분도 통일이 필요해요
스크린샷 2025-02-01 오후 5 42 05


const { mutate: deleteFaq, isLoading } = useDeleteFaq();

const isClickedDeleteButton = (questionId?: number) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

figma상에 삭제 모달이 존재하는 것 같은데, 적용이 안되어져있는 것 같아요. UX차원에서 필요할 것 같다고 느껴져요 !
스크린샷 2025-02-01 오후 5 02 03

const { mutate: deleteFaq, isLoading } = useDeleteFaq();

const isClickedDeleteButton = (questionId?: number) => {
if (!questionId || !token) {
Copy link
Contributor

Choose a reason for hiding this comment

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

token이 없는 경우 login 페이지로 이동시키는 로직이 api에 rejectedResponse에 포함되어져있어요. 따로 대응할 필요가 없어서 참고해주세용

// src/api/index.ts

// interceptors로 일괄처리 적용
api.interceptors.response.use(fulfilledResponse, rejectedResponse);

// 401 경우 login화면 전환 로직 (expirationToken이 rejectedResponse하위로직으로 포함되어져있어요)
function expirationToken(error: AxiosError<ErrorType>) {
  removeToken();
  window.location.href = '/login';
  toast.error(error.response?.data?.message ?? `로그인 시간이 만료되었어요.`);

export const useCreateFaq = () => {
return useMutation(createFaq, {
onSuccess: () => {
console.log('FAQ 생성성공');
Copy link
Contributor

Choose a reason for hiding this comment

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

console은 개발 편의를 위해서 생성하신거라고 생각해요. console을 제거하고 toast를 여기에 띄우는 것은 어떨까요?

Suggested change
console.log('FAQ 생성성공');
toast.success('FAQ가 성공적으로 저장되었습니다');

Copy link
Contributor

Choose a reason for hiding this comment

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

생성하신 FAQActions component가 사용되고 있는 곳이 없고 중복코드인 것으로 보여요.
혹시 적용을 누락하신 것일까요??

@ujinsim ujinsim marked this pull request as draft February 1, 2025 11:58
Copy link
Collaborator

@keemsebin keemsebin left a comment

Choose a reason for hiding this comment

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

수고하셨습니다!

@@ -638,3 +639,51 @@ function rejectedResponse(error: AxiosError<ErrorType>) {
}

api.interceptors.response.use(fulfilledResponse, rejectedResponse);

export async function getAllFaq(
Copy link
Collaborator

Choose a reason for hiding this comment

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

명칭은 hooks에서는 Faq를 페이지,컴포넌트에서는 FAQ 요렇게 두가지를 사용하고 있는 것 같은데 둘 중 하나로 통일 시켜도 좋을 것 같습니다~!

@@ -0,0 +1,60 @@
interface FAQActionsProps {
Copy link
Collaborator

Choose a reason for hiding this comment

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

현재 type으로 컨벤션을 맞춰 진행하고 있었어요! 확인부탁드립니닷


export const useAllFaq = (token: string) => {
return useQuery(['allFaqs'], () => getAllFaq(token), {
enabled: !!token,
Copy link
Collaborator

Choose a reason for hiding this comment

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

FAQ는 일반 동아리원/총동아리연합회/동아리 회장님들 모두 사용 확인이 가능하기때문에,token이 없을 경우 FAQ를 확인하지 못하는 경우는 없을 것 같아요~!

const queryClient = useQueryClient();

return useMutation(
async ({ questionId, token }) => await deleteFaq({ questionId, token }), // ✅ async 함수 전달
Copy link
Collaborator

Choose a reason for hiding this comment

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

주석은 지워두는게 좋을 것 같습니다~!

addFAQ: () => void;
}

export default function FAQActions({
Copy link
Collaborator

Choose a reason for hiding this comment

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

이미 /admin/FAQ index.tsx 파일 내에 해당 내용이 포함되어 있는 것 같아요! 추후 삭제될 예정일까요??

Copy link
Collaborator

Choose a reason for hiding this comment

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

개인적으로 분리 안 했을 때 코드가 더 직관적이라면 굳이 나눌 필요는 없을 것 같다고 생각합니다!

} from '@/components/ui/accordion';
import { useDeleteFaq } from '@/hooks/api/faq/useDeleteFaq';

interface FAQItem {
Copy link
Collaborator

Choose a reason for hiding this comment

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

다시 선언할 필요없이 types/faq.ts 에서 사용중인 타입 가져다 써도 좋을 것 같아요~!

Comment on lines +47 to +55
{ questionId, token },
{
onSuccess: () => {
refetch();
toast.success('FAQ가 삭제되었습니다.');
},
onError: () => {
toast.error('FAQ 삭제에 실패했습니다.');
},
Copy link
Collaborator

Choose a reason for hiding this comment

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

삭제 로직은 훅 내부에서 처리하는게 좋을 것 같아요! 혹시 refetch를 사용한 이유가 있을까요??

async ({ questionId, token }) => await deleteFaq({ questionId, token }), // ✅ async 함수 전달
{
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['faqs'] });
Copy link
Collaborator

Choose a reason for hiding this comment

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

삭제와 전체 조회의 queryKey가 다른 이유가 궁금해요!

const { mutate: createFaq, isLoading: isSaving } = useCreateFaq();

const [isEditing, setIsEditing] = useState(false);
const [newFAQs, setNewFAQs] = useState<{ question: string; reply: string }[]>(
Copy link
Collaborator

Choose a reason for hiding this comment

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

변경 가능할 것 같아요~!

Suggested change
const [newFAQs, setNewFAQs] = useState<{ question: string; reply: string }[]>(
const [newFAQs, setNewFAQs] = useState<FAQItem[]>

};

const saveFAQ = async () => {
if (newFAQs.length === 0) return;
Copy link
Collaborator

Choose a reason for hiding this comment

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

요것도 좋지만, newFaqs.length가 0 일 경우 저장하기 버튼을 disable 처리하는게 사용자 경험 상 더 좋을 것 같아요~

@keemsebin
Copy link
Collaborator

저장하기 -> 수정하기 버튼을 누른 후 삭제를 진행하는데, 삭제 후에 저장하기 버튼은 남아있는 동시에 toast에 FAQ 삭제가 완료 되었다.는 문구가 뜨는게 사용자 입장에서는 삭제가 된 건지 저장하기를 눌러야 삭제가 되는 건지 애매할 수도 있다고 생각해요. 다른 분들 의견 궁금하네요!

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.

FAQ 페이지 CRUD 구현
3 participants