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

지원자 결과 페이지 파일 다운로드, 시트 컴포넌트 연결 #236

Open
wants to merge 5 commits into
base: staging
Choose a base branch
from

Conversation

keemsebin
Copy link
Collaborator

@keemsebin keemsebin commented Feb 20, 2025

🔥 연관 이슈

🚀 작업 내용

  • 시트 컴포넌트 연결했습니다~
  • 지원자 결과 페이지 파일 다운로드 -> 백엔드 이슈로 현재는 다운로드가 불가하고, file name이 존재하지 않습니다!
image

🤔 고민했던 내용

💬 리뷰 중점사항

Summary by CodeRabbit

  • 신규 기능
    • 개선된 신청서 작성 및 관리 인터페이스 도입 (단계별 폼, 동적 질문 구성, 제출 후 애니메이션 효과 포함)
    • 관리자 페이지에서 지원자 상세 정보 조회, 상태 업데이트 및 결과 이메일 발송 기능 추가
    • 통계 대시보드를 통해 지원 현황과 데이터 시각화를 지원하며, 클럽 모집 정보도 간소화됨
  • 개선/정리
    • 기존 이벤트 관련 기능(스탬프 수집, 드로우 등) 제거로 사용자 경험이 일관되고 깔끔하게 개선됨

@keemsebin keemsebin added the ⚛️ 프론트엔드 프론트엔드 관련 이슈 label Feb 20, 2025
@keemsebin keemsebin self-assigned this Feb 20, 2025
Copy link

vercel bot commented Feb 20, 2025

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

Name Status Preview Comments Updated (UTC)
ddingdong-fe ✅ Ready (Inspect) Visit Preview 💬 Add feedback Feb 20, 2025 3:12pm

@keemsebin keemsebin changed the base branch from main to staging February 20, 2025 12:28
Copy link

coderabbitai bot commented Feb 20, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

이번 PR은 패키지 의존성 추가와 함께 API, 컴포넌트, 타입, 커스텀 훅 및 유틸 함수 등 코드베이스 전반에 걸친 대대적인 기능 확장을 포함합니다. 특히 지원서 관리 및 어드민 인터페이스 기능이 확장되고, 이전 이벤트 관련 기능들이 제거되었습니다.

Changes

File(s) Change Summary
package.json 새로운 의존성 추가: @radix-ui/react-select, @radix-ui/react-tooltip, chart.js, js-confetti.
src/apis/index.ts 신규 API 함수 추가 (지원서 통계, 등록, 정보 조회, 업데이트 등) 및 이벤트 관련 함수(getMyCollects, getMyQrCode, collectStamp) 제거.
src/components/admin-club/ClubInfoForm.tsx 모집 기간과 관련된 속성(parsedRecruitPeriod, formUrl) 제거하여 클럽 정보 폼 업데이트.
src/components/apply/*.tsx 지원서 관련 신규 및 업데이트된 컴포넌트 추가 (예: ApplicantCard, ApplicantInfo, ApplicantList, ApplyForm, ApplyContent 등).
src/components/event/*.tsx 이벤트 관련 컴포넌트 전체 제거 (예: BoothPlace, DrawFooter, DrawMessage, DrawForm, EventCard, LocalUserForm, StampBoard, StampDetail, UploadCertificate).
src/components/(feed, home, ui)/*.tsx UI 및 스타일 업데이트: TabsdefaultIndex 추가, SearchBartype 프로퍼티 추가, 신규 UI 컴포넌트 도입 (예: OptionModal, BarChart, LineChart, PieChart, Select 등) 및 dropdown-menu, tooltip 스타일 개선.
src/constants/*, src/hooks/api/*, src/types/*, src/utils/filter.ts 지원서 및 클럽 관리와 관련한 상수, 타입, 커스텀 훅, 유틸 함수 추가/수정. 이벤트 관련 상수 및 타입 제거.
src/middleware.ts 미들웨어 적용 경로에 /apply/apply/:path* 추가.
src/pages/* 어드민 지원서 페이지(상세, 이메일, 통계, 신규 등록 등)와 사용자 지원서 페이지의 업데이트 및 신규 컴포넌트 추가, 클럽 페이지의 모집 로직 단순화.

Sequence Diagram(s)

sequenceDiagram
    participant U as 지원자
    participant AF as ApplyForm 컴포넌트
    participant API as API 레이어 (useSubmitApply)
    participant QC as Query Cache

    U->>AF: 폼 입력 및 제출
    AF->>API: submitApplicationForm() 호출
    API-->>AF: 성공/오류 응답 전달
    AF->>QC: 관련 쿼리 무효화 (Invalidate)
    QC-->>AF: 최신 데이터 반영
    AF->>U: 결과 알림 (toast 등)
Loading

Possibly related PRs

Suggested labels

✨ 기능

Suggested reviewers

  • yougyung
  • ujinsim

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.

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.

Caution

Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments.

🛑 Comments failed to post (53)
src/hooks/api/apply/useAdminAllForms.ts (1)

9-11: 🛠️ Refactor suggestion

에러 타입 처리를 개선해 주세요.

현재 any 타입을 사용하여 에러를 처리하고 있습니다. 타입 안전성을 높이기 위해 구체적인 에러 타입을 정의하는 것이 좋습니다.

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

-      const errorMessage =
-        (error as any).response?.data?.message ||
-        '폼 정보 수정에 실패했습니다.';
+      type ApiError = {
+        response?: {
+          data?: {
+            message?: string;
+          };
+        };
+      };
+      const errorMessage =
+        (error as ApiError).response?.data?.message ||
+        '폼 정보 수정에 실패했습니다.';
📝 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.

      type ApiError = {
        response?: {
          data?: {
            message?: string;
          };
        };
      };
      const errorMessage =
        (error as ApiError).response?.data?.message ||
        '폼 정보 수정에 실패했습니다.';
src/hooks/api/apply/useAdminForm.ts (1)

13-13: 🛠️ Refactor suggestion

에러 처리 방식을 개선해 주세요.

현재 에러를 직접 문자열로 변환하고 있습니다. API 응답의 구조를 고려한 더 구체적인 에러 처리가 필요합니다.

-      toast.error(error as string);
+      const errorMessage =
+        (error as { response?: { data?: { message?: string } } })
+          .response?.data?.message || '폼 정보를 불러오는데 실패했습니다.';
+      toast.error(errorMessage);
📝 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 errorMessage =
        (error as { response?: { data?: { message?: string } } })
          .response?.data?.message || '폼 정보를 불러오는데 실패했습니다.';
      toast.error(errorMessage);
src/hooks/api/apply/useFormDetail.ts (1)

11-11: 🛠️ Refactor suggestion

에러 메시지 처리를 개선해 주세요.

현재 에러 메시지와 에러 객체를 문자열로 직접 연결하고 있습니다. 이는 가독성이 떨어지고 에러 정보가 불필요하게 노출될 수 있습니다.

-      toast.error('폼지 조회에 문제가 생겼습니다' + (error as string));
+      const errorMessage =
+        (error as { response?: { data?: { message?: string } } })
+          .response?.data?.message || '폼지 조회에 실패했습니다.';
+      toast.error(errorMessage);
📝 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 errorMessage =
        (error as { response?: { data?: { message?: string } } })
          .response?.data?.message || '폼지 조회에 실패했습니다.';
      toast.error(errorMessage);
src/hooks/api/apply/useAllApplication.ts (1)

6-16: 🛠️ Refactor suggestion

에러 처리 로직 추가 필요

현재 구현에는 에러 처리 로직이 없습니다. 사용자 경험 향상을 위해 에러 처리를 추가하는 것이 좋습니다.

다음과 같이 에러 처리를 추가하는 것을 제안합니다:

 export function useAllApplication(formId: number, token: string) {
   return useQuery<
     unknown,
     AxiosError,
     AxiosResponse<Application, unknown>,
     [string, number]
   >({
     queryKey: ['apply', formId],
     queryFn: () => getAllApplication(formId, token),
+    onError: (error) => {
+      const errorMessage =
+        error instanceof AxiosError
+          ? error.response?.data?.message ?? error.message
+          : '알 수 없는 오류가 발생했습니다';
+      toast.error(`지원서 조회 중 오류가 발생했습니다: ${errorMessage}`);
+    },
   });
 }
📝 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 function useAllApplication(formId: number, token: string) {
  return useQuery<
    unknown,
    AxiosError,
    AxiosResponse<Application, unknown>,
    [string, number]
  >({
    queryKey: ['apply', formId],
    queryFn: () => getAllApplication(formId, token),
    onError: (error) => {
      const errorMessage =
        error instanceof AxiosError
          ? error.response?.data?.message ?? error.message
          : '알 수 없는 오류가 발생했습니다';
      toast.error(`지원서 조회 중 오류가 발생했습니다: ${errorMessage}`);
    },
  });
}
src/hooks/api/apply/useApplyStatistics.ts (1)

6-16: 🛠️ Refactor suggestion

에러 처리 로직 추가 필요

통계 데이터 조회 시 발생할 수 있는 에러에 대한 처리가 필요합니다.

다음과 같이 에러 처리를 추가하는 것을 제안합니다:

 export function useApplyStatistics(applyId: number, token: string) {
   return useQuery<
     unknown,
     AxiosError,
     AxiosResponse<ApplyStatistics, unknown>,
     [string, number]
   >({
     queryKey: ['statistics', applyId],
     queryFn: () => getApplyStatistics(applyId, token),
+    onError: (error) => {
+      const errorMessage =
+        error instanceof AxiosError
+          ? error.response?.data?.message ?? error.message
+          : '알 수 없는 오류가 발생했습니다';
+      toast.error(`통계 데이터 조회 중 오류가 발생했습니다: ${errorMessage}`);
+    },
   });
 }
📝 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 function useApplyStatistics(applyId: number, token: string) {
  return useQuery<
    unknown,
    AxiosError,
    AxiosResponse<ApplyStatistics, unknown>,
    [string, number]
  >({
    queryKey: ['statistics', applyId],
    queryFn: () => getApplyStatistics(applyId, token),
    onError: (error) => {
      const errorMessage =
        error instanceof AxiosError
          ? error.response?.data?.message ?? error.message
          : '알 수 없는 오류가 발생했습니다';
      toast.error(`통계 데이터 조회 중 오류가 발생했습니다: ${errorMessage}`);
    },
  });
}
src/hooks/api/apply/useAllSections.ts (1)

7-8: 🛠️ Refactor suggestion

캐시 키 개선 필요

현재 queryKey가 정적인 'Sections'로 설정되어 있어, 다른 섹션 데이터와 캐시가 충돌할 수 있습니다.

다음과 같이 id를 포함하도록 수정하는 것을 제안합니다:

-    queryKey: ['Sections'],
+    queryKey: ['sections', id],
📝 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.

    queryKey: ['sections', id],
    queryFn: () => getSections(id),
src/hooks/api/apply/useMultipleAnswer.ts (1)

11-21: 🛠️ Refactor suggestion

에러 처리 로직 추가 필요

답변 데이터 조회 시 발생할 수 있는 에러에 대한 처리가 필요합니다.

다음과 같이 에러 처리를 추가하는 것을 제안합니다:

 export function useMultipleAnswer(questionId: number, token: string) {
   return useQuery<
     unknown,
     AxiosError,
     AxiosResponse<Props, unknown>,
     [string, number]
   >({
     queryKey: ['question', questionId],
     queryFn: () => getMultipleAnswer(questionId, token),
+    onError: (error) => {
+      const errorMessage =
+        error instanceof AxiosError
+          ? error.response?.data?.message ?? error.message
+          : '알 수 없는 오류가 발생했습니다';
+      toast.error(`답변 데이터 조회 중 오류가 발생했습니다: ${errorMessage}`);
+    },
   });
 }
📝 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 function useMultipleAnswer(questionId: number, token: string) {
  return useQuery<
    unknown,
    AxiosError,
    AxiosResponse<Props, unknown>,
    [string, number]
  >({
    queryKey: ['question', questionId],
    queryFn: () => getMultipleAnswer(questionId, token),
    onError: (error) => {
      const errorMessage =
        error instanceof AxiosError
          ? error.response?.data?.message ?? error.message
          : '알 수 없는 오류가 발생했습니다';
      toast.error(`답변 데이터 조회 중 오류가 발생했습니다: ${errorMessage}`);
    },
  });
}
src/hooks/api/apply/useApplicantInfo.ts (1)

18-19: 🛠️ Refactor suggestion

쿼리 키 구조 검토 필요

formId가 쿼리 키에 포함되어 있지 않아 동일한 applicantId를 가진 다른 폼의 지원자 정보가 캐시에서 잘못 반환될 수 있습니다.

-   queryKey: ['apply', applicantId],
+   queryKey: ['apply', formId, applicantId],
📝 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.

    queryKey: ['apply', formId, applicantId],
    queryFn: () => getApplicantInfo(formId, applicantId, token),
src/hooks/api/apply/useSingleAnswer.ts (1)

15-23: 🛠️ Refactor suggestion

에러 처리 및 타입 안전성 개선 필요

쿼리 응답 타입을 더 명확하게 정의하고, 에러 처리를 추가하면 좋을 것 같습니다.

   return useQuery<
-    unknown,
+    AnswerResponse,
     AxiosError,
-    AxiosResponse<Props, unknown>,
+    AxiosResponse<AnswerResponse>,
     [string, number]
   >({
     queryKey: ['question', questionId],
     queryFn: () => getSingleAnswer(questionId, token),
+    onError: (error) => {
+      console.error('답변 조회 중 오류 발생:', 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.

  return useQuery<
    AnswerResponse,
    AxiosError,
    AxiosResponse<AnswerResponse>,
    [string, number]
  >({
    queryKey: ['question', questionId],
    queryFn: () => getSingleAnswer(questionId, token),
    onError: (error) => {
      console.error('답변 조회 중 오류 발생:', error);
    }
  });
src/components/common/CheckBox.tsx (1)

18-28: 🛠️ Refactor suggestion

접근성 및 사용자 경험 개선이 필요합니다.

  1. 접근성 속성이 누락되어 있습니다.
  2. 체크박스의 크기가 모바일에서 너무 작을 수 있습니다.
 <input
   checked={value}
   onChange={onChange}
   type="checkbox"
   disabled={disabled}
+  aria-checked={value}
+  role="checkbox"
   className={cn(
-    "size-4 appearance-none rounded-sm border-2 border-gray-300 bg-contain bg-center bg-no-repeat outline-none checked:border-blue-500 checked:border-transparent checked:bg-blue-500 checked:bg-[url('/check-white.svg')] checked:bg-[length:80%] checked:hover:bg-blue-600 md:size-5",
+    "size-5 appearance-none rounded-sm border-2 border-gray-300 bg-contain bg-center bg-no-repeat outline-none checked:border-blue-500 checked:border-transparent checked:bg-blue-500 checked:bg-[url('/check-white.svg')] checked:bg-[length:80%] checked:hover:bg-blue-600",
     className,
   )}
   {...props}
 />
📝 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.

    <input
      checked={value}
      onChange={onChange}
      type="checkbox"
      disabled={disabled}
      aria-checked={value}
      role="checkbox"
      className={cn(
        "size-5 appearance-none rounded-sm border-2 border-gray-300 bg-contain bg-center bg-no-repeat outline-none checked:border-blue-500 checked:border-transparent checked:bg-blue-500 checked:bg-[url('/check-white.svg')] checked:bg-[length:80%] checked:hover:bg-blue-600",
        className,
      )}
      {...props}
    />
src/pages/admin/apply/[id]/new.tsx (1)

10-13: 🛠️ Refactor suggestion

토큰 및 라우터 파라미터 처리 개선 필요

토큰과 라우터 파라미터 처리에 대한 안전성을 높일 수 있습니다:

  • 토큰이 없는 경우에 대한 처리
  • id 파라미터의 유효성 검증

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

  const { id } = router.query;
  const [cookies] = useCookies();
  const token = cookies.token;
+ 
+ if (!token) {
+   router.push('/login');
+   return null;
+ }
+
+ if (!id || Array.isArray(id)) {
+   router.push('/404');
+   return null;
+ }
📝 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 { id } = router.query;
  const [cookies] = useCookies();
  const token = cookies.token;
  
  if (!token) {
    router.push('/login');
    return null;
  }
  
  if (!id || Array.isArray(id)) {
    router.push('/404');
    return null;
  }
src/components/apply/StatisticsSections.tsx (1)

16-29: 🛠️ Refactor suggestion

접근성 및 사용성 개선 필요

탭 인터페이스의 접근성과 사용성을 개선할 수 있습니다:

  • ARIA 속성 추가
  • 키보드 네비게이션 지원

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

- <div className="relative mt-7 flex items-center gap-1 border-b-0 px-4 font-semibold">
+ <div 
+   role="tablist" 
+   className="relative mt-7 flex items-center gap-1 border-b-0 px-4 font-semibold"
+ >
    {sections?.map((name: string) => (
      <span
        key={name}
+       role="tab"
+       tabIndex={0}
+       aria-selected={focusSection === name}
        className={`cursor-pointer rounded-md rounded-b-none border border-b-0 border-gray-200 px-3 py-1 ${
          focusSection === name
            ? 'bg-blue-50 text-blue-500'
            : 'bg-white text-gray-500 hover:bg-gray-50'
        }`}
        onClick={() => setFocusSection(name)}
+       onKeyDown={(e) => {
+         if (e.key === 'Enter' || e.key === ' ') {
+           e.preventDefault();
+           setFocusSection(name);
+         }
+       }}
      >
        {name}
      </span>
    ))}
  </div>
📝 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.

    <div 
      role="tablist" 
      className="relative mt-7 flex items-center gap-1 border-b-0 px-4 font-semibold"
    >
      {sections?.map((name: string) => (
        <span
          key={name}
          role="tab"
          tabIndex={0}
          aria-selected={focusSection === name}
          className={`cursor-pointer rounded-md rounded-b-none border border-b-0 border-gray-200 px-3 py-1 ${
            focusSection === name
              ? 'bg-blue-50 text-blue-500'
              : 'bg-white text-gray-500 hover:bg-gray-50'
          }`}
          onClick={() => setFocusSection(name)}
          onKeyDown={(e) => {
            if (e.key === 'Enter' || e.key === ' ') {
              e.preventDefault();
              setFocusSection(name);
            }
          }}
        >
          {name}
        </span>
      ))}
    </div>
src/components/apply/SelectSection.tsx (1)

29-34: ⚠️ Potential issue

버튼 핸들러와 접근성 속성이 누락되었습니다.

다음과 같은 개선이 필요합니다:

  1. 취소와 다음 버튼에 대한 클릭 핸들러가 구현되어 있지 않습니다.
  2. 버튼에 aria-label과 같은 접근성 속성이 누락되었습니다.
  3. 작업 중 상태(로딩)를 표시하는 기능이 없습니다.

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

-        <button className="rounded bg-gray-300 px-4 py-2">취소</button>
-        <button className="rounded bg-blue-500 px-4 py-2 text-white">
-          다음
-        </button>
+        <button
+          onClick={onCancel}
+          aria-label="취소"
+          className="rounded bg-gray-300 px-4 py-2"
+        >
+          취소
+        </button>
+        <button
+          onClick={onNext}
+          aria-label="다음"
+          disabled={isLoading}
+          className="rounded bg-blue-500 px-4 py-2 text-white disabled:opacity-50"
+        >
+          {isLoading ? '처리중...' : '다음'}
+        </button>
📝 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.

      <div className="flex gap-4">
        <button
          onClick={onCancel}
          aria-label="취소"
          className="rounded bg-gray-300 px-4 py-2"
        >
          취소
        </button>
        <button
          onClick={onNext}
          aria-label="다음"
          disabled={isLoading}
          className="rounded bg-blue-500 px-4 py-2 text-white disabled:opacity-50"
        >
          {isLoading ? '처리중...' : '다음'}
        </button>
      </div>
src/components/ui/OptionModal.tsx (2)

9-37: 🛠️ Refactor suggestion

모달의 접근성과 외부 클릭 처리가 개선되어야 합니다.

  1. 모달에 대한 적절한 ARIA 속성이 누락되었습니다.
  2. 외부 클릭 시 모달을 닫는 기능이 없습니다.
  3. 키보드 접근성(Esc 키로 닫기 등)이 구현되어 있지 않습니다.

다음과 같은 라이브러리 사용을 추천드립니다:

  • @radix-ui/react-dialog
  • @headlessui/react

이러한 라이브러리들은 모달의 접근성과 상호작용을 자동으로 처리해줍니다.


18-24: 🛠️ Refactor suggestion

map 함수에서 index를 key로 사용하는 것은 피해야 합니다.

배열의 index를 key로 사용하면 React의 재조정(reconciliation) 과정에서 문제가 발생할 수 있습니다. 가능하면 고유한 식별자를 key로 사용해주세요.

-            <li className="p-1" key={index}>
+            <li className="p-1" key={`${label}-${index}`}>
📝 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.

        {labels.map((label, index) => {
          return (
            <li className="p-1" key={`${label}-${index}`}>
              {label}
            </li>
          );
        })}
src/components/apply/QuestionMultipleContent.tsx (1)

20-21: 🛠️ Refactor suggestion

데이터 로딩 및 에러 상태 처리 필요

데이터 페칭 시 로딩 상태와 에러 상태에 대한 처리가 누락되어 있습니다. 사용자 경험 향상을 위해 이를 추가하는 것이 좋겠습니다.

  const [{ token }] = useCookies();
- const { data } = useMultipleAnswer(id, token);
+ const { data, isLoading, error } = useMultipleAnswer(id, token);
+
+ if (isLoading) return <div>로딩 중...</div>;
+ if (error) return <div>데이터를 불러오는데 실패했습니다.</div>;
📝 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 [{ token }] = useCookies();
  const { data, isLoading, error } = useMultipleAnswer(id, token);

  if (isLoading) return <div>로딩 중...</div>;
  if (error) return <div>데이터를 불러오는데 실패했습니다.</div>;
src/components/apply/FormBlock.tsx (1)

12-15: 🛠️ Refactor suggestion

접근성 및 키보드 네비게이션 개선 필요

클릭 이벤트만 사용하고 있어 키보드 접근성이 부족합니다. 적절한 시맨틱 요소와 키보드 이벤트를 추가하는 것이 좋겠습니다.

- <div
+ <button
  className="flex cursor-pointer items-center justify-between rounded-lg border border-gray-200 bg-white p-4 text-lg"
  onClick={onClick}
+ onKeyDown={(e) => e.key === 'Enter' && onClick()}
+ role="button"
+ tabIndex={0}
>
📝 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.

    <button
      className="flex cursor-pointer items-center justify-between rounded-lg border border-gray-200 bg-white p-4 text-lg"
      onClick={onClick}
      onKeyDown={(e) => e.key === 'Enter' && onClick()}
      role="button"
      tabIndex={0}
    >
src/components/apply/BaseInput.tsx (2)

12-12: 🛠️ Refactor suggestion

Props 타입 안정성 개선 필요

[key: string]: any 타입은 타입 안정성을 저해할 수 있습니다. 허용되는 props를 명시적으로 정의하는 것이 좋겠습니다.

- [key: string]: any;
+ type HTMLInputProps = React.InputHTMLAttributes<HTMLInputElement>;
+ type HTMLTextAreaProps = React.TextAreaHTMLAttributes<HTMLTextAreaElement>;
+ type AdditionalProps = Partial<HTMLInputProps & HTMLTextAreaProps>;

Committable suggestion skipped: line range outside the PR's diff.


28-32: 🛠️ Refactor suggestion

레이블 접근성 개선 필요

label과 input/textarea 요소 간의 연결이 누락되어 있습니다. htmlFor 속성을 추가하여 접근성을 개선하는 것이 좋겠습니다.

+ const id = props.id || `base-input-${Math.random().toString(36).substr(2, 9)}`;

  {label && !disabled && (
-   <label className="mb-3 block px-1 text-lg font-bold text-blue-500 md:text-xl">
+   <label 
+     htmlFor={id}
+     className="mb-3 block px-1 text-lg font-bold text-blue-500 md:text-xl"
+   >
      {label}
    </label>
  )}
📝 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 id = props.id || `base-input-${Math.random().toString(36).substr(2, 9)}`;

      {label && !disabled && (
-        <label className="mb-3 block px-1 text-lg font-bold text-blue-500 md:text-xl">
+        <label 
+          htmlFor={id}
+          className="mb-3 block px-1 text-lg font-bold text-blue-500 md:text-xl"
+        >
           {label}
         </label>
      )}
src/components/apply/QuestionList.tsx (1)

19-24: ⚠️ Potential issue

데이터 필터링 로직에 대한 에러 처리가 필요합니다.

data가 undefined일 경우에 대한 처리가 없어 런타임 에러가 발생할 수 있습니다.

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

  useEffect(() => {
+   if (!data?.data.fieldStatistics.fields) return;
    const filteredData = data?.data.fieldStatistics.fields.filter(
      (item: ApplyQuestion) => item.section === fields,
    );
    setFieldsData(filteredData);
  }, [fields, data]);
📝 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.

  useEffect(() => {
    if (!data?.data.fieldStatistics.fields) return;
    const filteredData = data?.data.fieldStatistics.fields.filter(
      (item: ApplyQuestion) => item.section === fields,
    );
    setFieldsData(filteredData);
  }, [fields, data]);
src/pages/admin/apply/[id]/statistics/index.tsx (2)

21-21: ⚠️ Potential issue

관리자 경로 수정이 필요합니다.

현재 뒤로 가기 링크가 /apply/${applyId}로 설정되어 있는데, 이는 관리자 페이지에서 일반 지원 페이지로 이동하게 됩니다. 관리자 경로를 유지하기 위해 수정이 필요합니다.

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

-        <Link href={`/apply/${applyId}`}>
+        <Link href={`/admin/apply/${applyId}`}>
📝 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.

        <Link href={`/admin/apply/${applyId}`}>

16-16: 🛠️ Refactor suggestion

데이터 로딩 상태 및 에러 처리가 필요합니다.

useApplyStatistics 훅에서 반환되는 데이터의 로딩 상태와 에러 상태를 처리하지 않고 있습니다.

다음과 같이 로딩 상태와 에러 처리를 추가하는 것을 추천드립니다:

-  const { data } = useApplyStatistics(applyId, token);
+  const { data, isLoading, error } = useApplyStatistics(applyId, token);
+
+  if (isLoading) return <div>로딩 중...</div>;
+  if (error) return <div>데이터를 불러오는 중 오류가 발생했습니다.</div>;
+  if (!data) return <div>데이터가 없습니다.</div>;
📝 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 { data, isLoading, error } = useApplyStatistics(applyId, token);

  if (isLoading) return <div>로딩 중...</div>;
  if (error) return <div>데이터를 불러오는 중 오류가 발생했습니다.</div>;
  if (!data) return <div>데이터가 없습니다.</div>;
src/middleware.ts (1)

55-56: ⚠️ Potential issue

중복된 경로 설정을 제거해주세요.

matcher 배열에 '/apply'와 '/apply/:path*' 경로가 중복되어 있습니다. 중복된 항목은 제거되어야 합니다.

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

    '/apply',
    '/apply/:path*',
    '/admin/:path*',
    '/report/:path*',
    '/feeds/:path*',
    '/feed/:path*',
-   '/apply/:path*',
-   '/apply',

Also applies to: 61-62

src/types/form.ts (2)

24-31: 🛠️ Refactor suggestion

중복된 인터페이스를 통합하세요.

QuestionFieldFormField 인터페이스가 동일한 구조를 가지고 있습니다. 코드 유지보수성을 높이기 위해 하나의 인터페이스로 통합하는 것이 좋습니다.

다음과 같이 수정하는 것을 제안합니다:

-export interface QuestionField {
-  question: string;
-  type: QuestionType;
-  options: string[];
-  required: boolean;
-  order: number;
-  section: string;
-}

-export interface FormField {
-  question: string;
-  type: QuestionType;
-  options: string[];
-  required: boolean;
-  order: number;
-  section: string;
-}

+export interface FormField {
+  question: string;
+  type: QuestionType;
+  options: string[];
+  required: boolean;
+  order: number;
+  section: string;
+}

+export type QuestionField = FormField;

Also applies to: 40-47


19-22: 🛠️ Refactor suggestion

타입 재사용을 통해 일관성을 유지하세요.

ApplyData 인터페이스의 formAnswers 배열이 FormAnswer 인터페이스와 동일한 구조를 가지고 있습니다. 타입의 일관성을 유지하기 위해 FormAnswer 인터페이스를 재사용하는 것이 좋습니다.

다음과 같이 수정하는 것을 제안합니다:

export interface ApplyData {
  name: string;
  studentNumber: string;
  department: string;
  email: string;
  phoneNumber: string;
-  formAnswers: {
-    fieldId: string | number;
-    value: string | string[];
-  }[];
+  formAnswers: FormAnswer[];
}

Also applies to: 64-74

src/components/apply/Dropdown.tsx (1)

59-72: 🛠️ Refactor suggestion

배열 인덱스를 key로 사용하지 마세요.

React의 리렌더링 최적화를 위해 배열의 인덱스 대신 고유한 값을 key로 사용해야 합니다. QuestionType은 이미 고유한 값이므로 이를 key로 사용할 수 있습니다.

다음과 같이 수정하는 것을 제안합니다:

-key={index}
+key={item}
📝 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.

          {contents.map((item, index) => (
            <div
              key={item}
              className={`cursor-pointer px-4 py-3 transition hover:bg-gray-100 ${
                index === 0 ? 'hover:rounded-t-lg' : ''
              } ${index === contents.length - 1 ? 'hover:rounded-b-lg' : ''}`}
              onClick={() => {
                setSelected(item);
                setOpenDropdown(false);
              }}
            >
              {convertToKorean(item)}
            </div>
          ))}
src/components/apply/ApplyResult.tsx (1)

48-62: ⚠️ Potential issue

파일 다운로드 기능의 오류 처리를 개선하세요.

파일 다운로드 기능에 대한 오류 처리가 없습니다. 파일이 존재하지 않거나 다운로드에 실패할 경우를 대비한 처리가 필요합니다.

다음과 같이 수정하는 것을 제안합니다:

if (type === 'FILE') {
+  if (!value || !value.length) {
+    return <div className="text-red-500">파일을 찾을 수 없습니다.</div>;
+  }
  return (
    <div className="flex items-center gap-2">
      <a
        download
        href={value[0]}
        target="_blank"
+       rel="noopener noreferrer"
        className="flex items-center text-lg font-semibold text-gray-700"
+       onClick={(e) => {
+         if (!value[0]) {
+           e.preventDefault();
+           alert('파일을 찾을 수 없습니다.');
+         }
+       }}
      >
📝 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 (type === 'FILE') {
      if (!value || !value.length) {
        return <div className="text-red-500">파일을 찾을 수 없습니다.</div>;
      }
      return (
        <div className="flex items-center gap-2">
          <a
            download
            href={value[0]}
            target="_blank"
            rel="noopener noreferrer"
            className="flex items-center text-lg font-semibold text-gray-700"
            onClick={(e) => {
              if (!value[0]) {
                e.preventDefault();
                alert('파일을 찾을 수 없습니다.');
              }
            }}
          >
            {value[0]}
            <Image src={DownLoad} width={20} height={20} alt="file" />
          </a>
        </div>
      );
    }
src/components/common/Prompt.tsx (1)

80-91: 🛠️ Refactor suggestion

버튼 타입 속성 누락

확인 및 취소 버튼에 type 속성이 누락되어 있습니다. 폼 제출 시 의도하지 않은 동작을 방지하기 위해 추가해야 합니다.

 <button
+  type="button"
   className="rounded-xl bg-gray-100 px-3 py-2 text-sm font-semibold text-gray-500 hover:bg-gray-200"
   onClick={closeModal}
 >
   {cancelText}
 </button>
 <button
+  type="button"
   className="rounded-xl bg-blue-500 px-8 text-sm font-semibold text-white hover:bg-blue-600"
   onClick={handleSubmit}
 >
   {confirmText}
 </button>
📝 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.

        <button
          type="button"
          className="rounded-xl bg-gray-100 px-3 py-2 text-sm font-semibold text-gray-500 hover:bg-gray-200"
          onClick={closeModal}
        >
          {cancelText}
        </button>
        <button
          type="button"
          className="rounded-xl bg-blue-500 px-8 text-sm font-semibold text-white hover:bg-blue-600"
          onClick={handleSubmit}
        >
          {confirmText}
        </button>
src/components/apply/FileUpload.tsx (1)

48-54: ⚠️ Potential issue

파일 업로드 제한 추가 필요

파일 크기와 타입에 대한 제한이 없습니다. 보안과 성능을 위해 이러한 제한을 추가해야 합니다.

 <input
   type="file"
   multiple
+  accept=".pdf,.doc,.docx,.txt"
   className="absolute left-0 top-0 h-full w-full cursor-pointer opacity-0"
   onChange={handleFileChange}
   disabled={disabled || isLoading}
 />

추가로 handleFileChange 함수에서 파일 크기를 검증하는 로직을 추가하세요:

const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB

const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
  if (!event.target.files) return;

  const selectedFiles = Array.from(event.target.files);
  
  // 파일 크기 검증
  const oversizedFiles = selectedFiles.filter(file => file.size > MAX_FILE_SIZE);
  if (oversizedFiles.length > 0) {
    toast.error('파일 크기는 5MB를 초과할 수 없습니다.');
    return;
  }

  const updatedFiles = [...files, ...selectedFiles];
  setFiles(updatedFiles);
  // ... 나머지 코드
};
src/components/apply/StepDropdown.tsx (1)

58-83: 🛠️ Refactor suggestion

접근성 및 키보드 네비게이션 개선이 필요합니다.

드롭다운 메뉴의 접근성과 키보드 사용성을 개선해야 합니다.

  1. ARIA 속성 추가
  2. 키보드 네비게이션 지원
  3. 포커스 관리
{openDropdown && !disabled && (
  <div 
+   role="listbox"
+   tabIndex={0}
+   onKeyDown={(e) => {
+     if (e.key === 'Escape') setOpenDropdown(false);
+   }}
    className="absolute left-0 top-full z-10 max-h-60 w-full overflow-y-auto rounded-lg border border-gray-200 bg-white shadow-lg">
    {Object.entries(contents).map(([category, items], categoryIndex) => (
      <div key={categoryIndex} className="flex flex-col">
        <div 
+         role="group"
+         aria-label={category}
          className="cursor-default border-b border-gray-200 px-5 py-3 font-semibold text-gray-300">
          {category}
        </div>
        {items.map((item, key) => (
          <div
            key={key}
+           role="option"
+           aria-selected={item === selectedContent}
+           tabIndex={0}
            className="cursor-pointer px-5 py-3 hover:bg-gray-100"
            onClick={() => {
              selectItem(item);
              setOpenDropdown(false);
            }}
+           onKeyPress={(e) => {
+             if (e.key === 'Enter' || e.key === ' ') {
+               selectItem(item);
+               setOpenDropdown(false);
+             }
+           }}
          >
            {item}
          </div>
        ))}
      </div>
    ))}
  </div>
)}
📝 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.

        {openDropdown && !disabled && (
          <div 
            role="listbox"
            tabIndex={0}
            onKeyDown={(e) => {
              if (e.key === 'Escape') setOpenDropdown(false);
            }}
            className="absolute left-0 top-full z-10 max-h-60 w-full overflow-y-auto rounded-lg border border-gray-200 bg-white shadow-lg">
            {Object.entries(contents).map(([category, items], categoryIndex) => (
              <div key={categoryIndex} className="flex flex-col">
                <div 
                  role="group"
                  aria-label={category}
                  className="cursor-default border-b border-gray-200 px-5 py-3 font-semibold text-gray-300">
                  {category}
                </div>
                {items.map((item, key) => (
                  <div
                    key={key}
                    role="option"
                    aria-selected={item === selectedContent}
                    tabIndex={0}
                    className="cursor-pointer px-5 py-3 hover:bg-gray-100"
                    onClick={() => {
                      selectItem(item);
                      setOpenDropdown(false);
                    }}
                    onKeyPress={(e) => {
                      if (e.key === 'Enter' || e.key === ' ') {
                        selectItem(item);
                        setOpenDropdown(false);
                      }
                    }}
                  >
                    {item}
                  </div>
                ))}
              </div>
            ))}
          </div>
        )}
src/components/ui/pie-chart.tsx (2)

102-109: ⚠️ Potential issue

useEffect 의존성 배열 검토 필요

useEffect 훅에서 renderChart 함수를 의존성 배열에 포함시키지 않았습니다. 이로 인해 메모리 누수나 예기치 않은 동작이 발생할 수 있습니다.

-  useEffect(() => {
-    renderChart();
-    window.addEventListener('resize', renderChart);
-    return () => {
-      chartInstance?.destroy();
-      window.removeEventListener('resize', renderChart);
-    };
-  }, [passedData]);
+  useEffect(() => {
+    const handleRender = () => {
+      renderChart();
+    };
+    handleRender();
+    window.addEventListener('resize', handleRender);
+    return () => {
+      chartInstance?.destroy();
+      window.removeEventListener('resize', handleRender);
+    };
+  }, [passedData, renderChart]);
📝 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.

  useEffect(() => {
    const handleRender = () => {
      renderChart();
    };
    handleRender();
    window.addEventListener('resize', handleRender);
    return () => {
      chartInstance?.destroy();
      window.removeEventListener('resize', handleRender);
    };
  }, [passedData, renderChart]);

31-48: 🛠️ Refactor suggestion

라벨 처리 로직 개선 필요

라벨 문자열 처리 로직에서 매직 넘버(7)를 사용하고 있습니다. 이를 상수로 분리하고, 라벨 처리 로직을 별도의 유틸리티 함수로 분리하면 좋을 것 같습니다.

+const LABEL_MAX_LENGTH = 7;
+
+const formatChartLabel = (label: string, count: number) => {
+  const truncatedLabel = label.length > LABEL_MAX_LENGTH 
+    ? `${label.slice(0, LABEL_MAX_LENGTH - 1)}...` 
+    : label;
+  return `${truncatedLabel} (${count}명)`;
+};

Committable suggestion skipped: line range outside the PR's diff.

src/components/apply/CommnQuestion.tsx (2)

6-20: 🛠️ Refactor suggestion

인터페이스 네이밍 및 타입 안정성 개선 필요

인터페이스 이름이 일반적이어서 다른 컴포넌트와 충돌할 수 있습니다. 또한, 필수 필드에 대한 유효성 검사 타입이 누락되어 있습니다.

-interface RequiredQuestions {
+interface CommonQuestionFormData {
   name: string;
   studentNumber: string;
   department: string;
   phoneNumber: string;
   email: string;
+  isValid?: {
+    name: boolean;
+    studentNumber: boolean;
+    phoneNumber: boolean;
+    email: boolean;
+  };
 }

-interface CommonQuestionProps {
+interface CommonQuestionComponentProps {
   disabled?: boolean;
-  requiredQuestions?: RequiredQuestions;
-  setRequiredQuestions?: React.Dispatch<React.SetStateAction<RequiredQuestions>>;
+  requiredQuestions?: CommonQuestionFormData;
+  setRequiredQuestions?: React.Dispatch<React.SetStateAction<CommonQuestionFormData>>;
 }
📝 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.

interface CommonQuestionFormData {
  name: string;
  studentNumber: string;
  department: string;
  phoneNumber: string;
  email: string;
  isValid?: {
    name: boolean;
    studentNumber: boolean;
    phoneNumber: boolean;
    email: boolean;
  };
}

interface CommonQuestionComponentProps {
  disabled?: boolean;
  requiredQuestions?: CommonQuestionFormData;
  setRequiredQuestions?: React.Dispatch<React.SetStateAction<CommonQuestionFormData>>;
}

32-45: 🛠️ Refactor suggestion

입력값 유효성 검사 로직 추가 필요

현재 handleBlur 함수는 단순히 값을 저장만 하고 있습니다. 각 필드에 대한 유효성 검사 로직을 추가하면 좋을 것 같습니다.

+const validateField = (field: keyof CommonQuestionFormData, value: string) => {
+  switch (field) {
+    case 'email':
+      return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
+    case 'phoneNumber':
+      return /^[0-9]{10,11}$/.test(value);
+    case 'studentNumber':
+      return /^[0-9]{8}$/.test(value);
+    default:
+      return value.length > 0;
+  }
+};

 const handleBlur = useCallback(
-  (field: keyof RequiredQuestions, value: string) => {
+  (field: keyof CommonQuestionFormData, value: string) => {
     if (setRequiredQuestions) {
       setRequiredQuestions((prev) => ({
         ...prev,
         [field]: value ?? '',
+        isValid: {
+          ...prev.isValid,
+          [field]: validateField(field, value)
+        }
       }));
     }
   },
   [setRequiredQuestions],
 );
📝 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 validateField = (field: keyof CommonQuestionFormData, value: string) => {
  switch (field) {
    case 'email':
      return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
    case 'phoneNumber':
      return /^[0-9]{10,11}$/.test(value);
    case 'studentNumber':
      return /^[0-9]{8}$/.test(value);
    default:
      return value.length > 0;
  }
};

const handleBlur = useCallback(
  (field: keyof CommonQuestionFormData, value: string) => {
    if (setRequiredQuestions) {
      setRequiredQuestions((prev) => ({
        ...prev,
        [field]: value ?? '',
        isValid: {
          ...prev.isValid,
          [field]: validateField(field, value)
        }
      }));
    }
  },
  [setRequiredQuestions],
);
src/pages/admin/apply/index.tsx (1)

13-23: ⚠️ Potential issue

에러 처리 및 로딩 상태 개선 필요

useAllForms 훅에서 반환되는 error를 처리하지 않고 있습니다. 또한 로딩 상태에 대한 UI 피드백이 부족합니다.

+import ErrorMessage from '@/components/common/ErrorMessage';
+import LoadingSpinner from '@/components/common/LoadingSpinner';

 const { data, isLoading, error } = useAllForms(token);

+if (error) {
+  return <ErrorMessage message="지원서 목록을 불러오는데 실패했습니다" />;
+}
+
+if (isLoading) {
+  return <LoadingSpinner />;
+}

 const forms: FormBlockData[] = data?.data || [];
📝 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.

import ErrorMessage from '@/components/common/ErrorMessage';
import LoadingSpinner from '@/components/common/LoadingSpinner';

const { data, isLoading, error } = useAllForms(token);

if (error) {
  return <ErrorMessage message="지원서 목록을 불러오는데 실패했습니다" />;
}

if (isLoading) {
  return <LoadingSpinner />;
}

const forms: FormBlockData[] = data?.data || [];

const formCounts = {
  전체: forms.length,
  '진행 전': forms.filter((form) => form.formStatus === '진행 전').length,
  '진행 중': forms.filter((form) => form.formStatus === '진행 중').length,
  마감: forms.filter((form) => form.formStatus === '마감').length,
};
src/components/ui/line-chart.tsx (2)

111-130: 🛠️ Refactor suggestion

커스텀 플러그인 최적화 필요

custom-text-plugin의 afterDatasetsDraw 함수가 매 프레임마다 실행되어 성능에 영향을 줄 수 있습니다. 또한 하드코딩된 스타일값들을 상수로 분리하면 좋을 것 같습니다.

+const CHART_TEXT_STYLES = {
+  current: '#3B82F6',
+  previous: '#6B7280',
+  font: 'bold 12px Arial',
+  offset: -15,
+};
+
 plugins: [
   {
     id: 'custom-text-plugin',
     afterDatasetsDraw: (chart) => {
       const { ctx, data } = chart;
       const dataset = data.datasets[0].data as number[];
+      
+      if (!chart.tooltip?.getActiveElements().length) {
+        return; // 툴팁이 활성화되지 않은 경우에만 텍스트 그리기
+      }

       dataset.forEach((value, index) => {
         const meta = chart.getDatasetMeta(0);
         const bar = meta.data[index];
-        ctx.fillStyle =
-          index === dataset.length - 1 ? '#3B82F6' : '#6B7280';
-        ctx.font = 'bold 12px Arial';
+        ctx.fillStyle =
+          index === dataset.length - 1
+            ? CHART_TEXT_STYLES.current
+            : CHART_TEXT_STYLES.previous;
+        ctx.font = CHART_TEXT_STYLES.font;
         ctx.textAlign = 'center';
-        ctx.fillText(`${value}명`, bar.x, bar.y - 15);
+        ctx.fillText(
+          `${value}명`,
+          bar.x,
+          bar.y + CHART_TEXT_STYLES.offset
+        );
         ctx.restore();
       });
     },
   },
 ],
📝 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 CHART_TEXT_STYLES = {
  current: '#3B82F6',
  previous: '#6B7280',
  font: 'bold 12px Arial',
  offset: -15,
};

plugins: [
  {
    id: 'custom-text-plugin',
    afterDatasetsDraw: (chart) => {
      const { ctx, data } = chart;
      const dataset = data.datasets[0].data as number[];

      if (!chart.tooltip?.getActiveElements().length) {
        return; // 툴팁이 활성화되지 않은 경우에만 텍스트 그리기
      }

      dataset.forEach((value, index) => {
        const meta = chart.getDatasetMeta(0);
        const bar = meta.data[index];
        ctx.fillStyle =
          index === dataset.length - 1
            ? CHART_TEXT_STYLES.current
            : CHART_TEXT_STYLES.previous;
        ctx.font = CHART_TEXT_STYLES.font;
        ctx.textAlign = 'center';
        ctx.fillText(
          `${value}명`,
          bar.x,
          bar.y + CHART_TEXT_STYLES.offset
        );
        ctx.restore();
      });
    },
  },
],

47-74: ⚠️ Potential issue

데이터 처리 로직 개선 필요

getChartData 함수에서 localStorage 접근과 MOCK_APPLYCANT 사용이 하드코딩되어 있습니다. 이를 props나 환경 설정으로 관리하면 좋을 것 같습니다.

-const getChartData = () => {
+const getChartData = (mockData?: boolean) => {
-  const club =
-    typeof window !== 'undefined'
-      ? JSON.parse(localStorage.getItem('club') ?? '')
-      : '';
-  const clubName = club.state?.club.name.toUpperCase() ?? '';
+  const clubData = useClubData(); // 커스텀 훅으로 분리
+  const clubName = clubData?.name?.toUpperCase() ?? '';

   const parsedApplicantData = [
-    MOCK_APPLYCANT[clubName],
-    calculateCompared(MOCK_APPLYCANT[clubName], passedData[2]),
+    mockData ? MOCK_APPLYCANT[clubName] : passedData[0],
+    calculateCompared(
+      mockData ? MOCK_APPLYCANT[clubName] : passedData[0],
+      passedData[2]
+    ),
   ];
📝 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 getChartData = (mockData?: boolean) => {
    const clubData = useClubData(); // 커스텀 훅으로 분리
    const clubName = clubData?.name?.toUpperCase() ?? '';

    const parsedApplicantData = [
      mockData ? MOCK_APPLYCANT[clubName] : passedData[0],
      calculateCompared(
        mockData ? MOCK_APPLYCANT[clubName] : passedData[0],
        passedData[2]
      ),
    ];

    const labels = parsedApplicantData.map((item) => item?.label);
    const datas = parsedApplicantData.map((item) => item?.count);
    const rates = parsedApplicantData.map(
      (item) => item?.comparedToBefore.ratio,
    );
    return {
      labels,
      rates,
      datasets: [
        {
          data: datas,
          ...lineChartStyle,
        },
      ],
    };
  };
src/components/ui/bar-chart.tsx (3)

111-114: 🛠️ Refactor suggestion

차트 정리(cleanup) 로직 개선 필요

차트 인스턴스 정리가 올바르게 이루어지지 않을 수 있습니다. chartInstanceRef를 사용하여 정리 로직을 개선해야 합니다.

   useEffect(() => {
     renderChart();
-    return () => chartInstance?.destroy();
+    return () => {
+      if (chartInstanceRef.current) {
+        chartInstanceRef.current.destroy();
+        chartInstanceRef.current = null;
+      }
+    };
   }, [passedData, barThickness]);
📝 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.

  useEffect(() => {
    renderChart();
    return () => {
      if (chartInstanceRef.current) {
        chartInstanceRef.current.destroy();
        chartInstanceRef.current = null;
      }
    };
  }, [passedData, barThickness]);

14-15: 🛠️ Refactor suggestion

차트 인스턴스 관리 개선 필요

차트 인스턴스가 let으로 선언되어 있어 예기치 않은 동작이 발생할 수 있습니다. useRef를 사용하여 차트 인스턴스를 관리하는 것이 더 안전합니다.

-  let chartInstance: ChartJS | null = null;
+  const chartInstanceRef = useRef<ChartJS | null>(null);
📝 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 chartInstanceRef = useRef<ChartJS | null>(null);

43-45: ⚠️ Potential issue

useMemo 의존성 배열 누락

useMemo에 window.innerWidth에 대한 의존성이 누락되어 있어 창 크기 변경 시 메모이제이션된 값이 업데이트되지 않을 수 있습니다.

-  }, []);
+  }, [typeof window !== 'undefined' ? window.innerWidth : undefined]);
📝 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 getBarThickness = useMemo(() => {
    return typeof window !== 'undefined' && window.innerWidth >= 768 ? 30 : 20;
  }, [typeof window !== 'undefined' ? window.innerWidth : undefined]);
src/components/apply/Question.tsx (1)

13-14: ⚠️ Potential issue

타입 안전성 개선 필요

questions 배열이 any 타입으로 선언되어 있어 타입 안전성이 보장되지 않습니다. 명시적인 타입을 정의해야 합니다.

 interface Section {
   section: string;
-  questions: any[];
+  questions: QuestionData[];
 }
📝 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.

interface Section {
  section: string;
  questions: QuestionData[];
}
src/pages/admin/apply/[id]/email/index.tsx (2)

51-59: ⚠️ Potential issue

폼 유효성 검사 누락

이메일 전송 전 제목과 내용의 유효성 검사가 누락되어 있습니다. 빈 값 체크와 적절한 사용자 피드백이 필요합니다.

   const handleSubmit = () => {
+    if (!title.trim()) {
+      toast.error('이메일 제목을 입력해주세요.');
+      return;
+    }
+    if (!message.trim()) {
+      toast.error('이메일 내용을 입력해주세요.');
+      return;
+    }
     mutation.mutate({
       formId: Number(id),
       title,
       target: target,
       message: message,
       token,
     });
   };
📝 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 handleSubmit = () => {
    if (!title.trim()) {
      toast.error('이메일 제목을 입력해주세요.');
      return;
    }
    if (!message.trim()) {
      toast.error('이메일 내용을 입력해주세요.');
      return;
    }
    mutation.mutate({
      formId: Number(id),
      title,
      target: target,
      message: message,
      token,
    });
  };

131-136: 🛠️ Refactor suggestion

전송 중 상태 처리 필요

이메일 전송 버튼이 전송 중에도 클릭 가능한 상태로 유지됩니다. 중복 전송을 방지하기 위한 처리가 필요합니다.

         <button
           onClick={handleSubmit}
+          disabled={mutation.isLoading}
           className="rounded-xl bg-blue-500 px-10 py-3 text-lg font-bold text-white hover:bg-blue-600 md:px-[60px] md:py-3.5"
         >
-          전송하기
+          {mutation.isLoading ? '전송중...' : '전송하기'}
         </button>
📝 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.

        <button
          onClick={handleSubmit}
          disabled={mutation.isLoading}
          className="rounded-xl bg-blue-500 px-10 py-3 text-lg font-bold text-white hover:bg-blue-600 md:px-[60px] md:py-3.5"
        >
          {mutation.isLoading ? '전송중...' : '전송하기'}
        </button>
src/components/apply/ApplyForm.tsx (1)

98-100: ⚠️ Potential issue

타입 안전성 개선 필요

formFields 필터링에서 any 타입이 사용되고 있습니다. 명시적인 타입을 사용하여 타입 안전성을 개선해야 합니다.

     const requiredFields = formData.data.formFields.filter(
-      (field: any) => field.required,
+      (field) => field.required,
     );

Committable suggestion skipped: line range outside the PR's diff.

src/components/apply/Content.tsx (2)

143-148: ⚠️ Potential issue

TextArea 컴포넌트의 disabled 속성이 반대로 설정되어 있습니다.

편집 모드일 때 TextArea가 비활성화되고, 편집 모드가 아닐 때 활성화되는 것은 논리적으로 맞지 않습니다.

- disabled={isEditing}
+ disabled={!isEditing || isClosed}

Also applies to: 149-154


29-31: 🛠️ Refactor suggestion

Props 인터페이스의 타입 안전성 개선이 필요합니다.

setFormFieldsection.questions에서 any 타입을 사용하고 있습니다. 타입 안전성을 높이기 위해 명시적인 타입을 정의해주세요.

- setFormField: (update: any) => void;
- section: { section: string; questions: any[] };
+ setFormField: (update: FormField[]) => void;
+ section: { section: string; questions: QuestionData[] };
📝 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.

  setFormField: (update: FormField[]) => void;
  section: { section: string; questions: QuestionData[] };
  isClosed: boolean;
src/components/apply/ApplyContent.tsx (1)

64-74: ⚠️ Potential issue

파일 업로드 에러 처리가 필요합니다.

파일 업로드 과정에서 발생할 수 있는 에러에 대한 처리가 누락되어 있습니다. try-catch 블록을 추가하여 에러를 적절히 처리해주세요.

 const handleFileUpload = async (files: File[] | string[]) => {
   if (files.length === 0) return;

   if (typeof files[0] === 'string') {
     handleFormAnswer(files as string[]);
   } else {
+    try {
       const uploadedFiles = await getPresignedIds(files as File[]);
       const fileIds = uploadedFiles.map((file) => file.id);
       handleFormAnswer(fileIds);
+    } catch (error) {
+      console.error('파일 업로드 중 오류가 발생했습니다:', 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.

  const handleFileUpload = async (files: File[] | string[]) => {
    if (files.length === 0) return;

    if (typeof files[0] === 'string') {
      handleFormAnswer(files as string[]);
    } else {
      try {
        const uploadedFiles = await getPresignedIds(files as File[]);
        const fileIds = uploadedFiles.map((file) => file.id);
        handleFormAnswer(fileIds);
      } catch (error) {
        console.error('파일 업로드 중 오류가 발생했습니다:', error);
        // 에러 처리 로직 추가
      }
    }
  };
src/pages/admin/apply/[id]/[applicantId]/index.tsx (1)

151-159: ⚠️ Potential issue

getServerSideProps에서 타입 변환이 필요합니다.

쿼리 파라미터가 문자열로 전달되므로, 숫자로 변환하는 과정이 필요합니다.

 export const getServerSideProps: GetServerSideProps = async (context) => {
   const { id, applicantId } = context.query;
   return {
     props: {
-      id: id,
-      applicantId: applicantId,
+      id: Number(id),
+      applicantId: Number(applicantId),
     },
   };
 };
📝 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 getServerSideProps: GetServerSideProps = async (context) => {
  const { id, applicantId } = context.query;
  return {
    props: {
      id: Number(id),
      applicantId: Number(applicantId),
    },
  };
};
src/pages/apply/[id]/index.tsx (1)

17-44: 🛠️ Refactor suggestion

상태 관리 및 로딩 처리 개선이 필요합니다.

현재 상태 관리와 로딩 처리가 다소 복잡하게 구현되어 있습니다. 다음과 같은 개선을 제안드립니다:

  1. 로딩 상태에 대한 사용자 피드백 추가
  2. 에러 상태 처리 추가
  3. 상태 관리 로직 단순화

다음과 같은 리팩토링을 제안합니다:

+  const [error, setError] = useState<string | null>(null);
   const { data: sectionsData, isLoading } = useAllSections(formId);
   const [selectedRadio, setSelectedRadio] = useState<string>('');
   const [step, setStep] = useState<'SECTION' | 'QUESTION' | 'SUBMITTED'>('SECTION');

   useEffect(() => {
     if (!isLoading) {
+      try {
         if (sections && sections.length > 2) {
           setStep('SECTION');
         } else {
           setStep('QUESTION');
           setSelectedRadio('공통');
         }
+      } catch (e) {
+        setError(e instanceof Error ? e.message : '알 수 없는 오류가 발생했습니다.');
+      }
     }
   }, [sections, isLoading]);

+  if (isLoading) {
+    return <div className="flex justify-center items-center h-screen">로딩 중...</div>;
+  }

+  if (error) {
+    return <div className="text-red-500 text-center">{error}</div>;
+  }

Committable suggestion skipped: line range outside the PR's diff.

src/components/apply/Sections.tsx (1)

83-110: 🛠️ Refactor suggestion

섹션 추가 시 입력 검증 로직 강화가 필요합니다.

현재 섹션 이름 검증이 기본적인 수준에 머물러 있습니다. 다음과 같은 개선사항을 고려해보세요:

  1. 특수문자 및 공백 처리
  2. 최소 길이 검증
  3. 더 자세한 에러 메시지

다음과 같은 개선된 구현을 제안합니다:

   const addNewSection = (sectionName: string) => {
     const trimmedName = sectionName.trim();
 
+    const validateSectionName = (name: string) => {
+      if (name.length < 2) {
+        return '섹션 이름은 최소 2자 이상이어야 합니다.';
+      }
+      if (name.length > 10) {
+        return '섹션 이름은 최대 10자까지 가능합니다.';
+      }
+      if (!/^[가-힣a-zA-Z0-9\s]+$/.test(name)) {
+        return '섹션 이름은 한글, 영문, 숫자만 사용 가능합니다.';
+      }
+      if (sections.includes(name)) {
+        return '이미 존재하는 섹션입니다.';
+      }
+      return null;
+    };

     if (!trimmedName) {
       toast.error('섹션 이름을 입력해주세요.');
       return;
     }

-    if (sections.includes(trimmedName)) {
-      toast.error('이미 존재하는 섹션입니다.');
+    const error = validateSectionName(trimmedName);
+    if (error) {
+      toast.error(error);
       return;
     }
📝 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 addNewSection = (sectionName: string) => {
    const trimmedName = sectionName.trim();

    const validateSectionName = (name: string) => {
      if (name.length < 2) {
        return '섹션 이름은 최소 2자 이상이어야 합니다.';
      }
      if (name.length > 10) {
        return '섹션 이름은 최대 10자까지 가능합니다.';
      }
      if (!/^[가-힣a-zA-Z0-9\s]+$/.test(name)) {
        return '섹션 이름은 한글, 영문, 숫자만 사용 가능합니다.';
      }
      if (sections.includes(name)) {
        return '이미 존재하는 섹션입니다.';
      }
      return null;
    };

    if (!trimmedName) {
      toast.error('섹션 이름을 입력해주세요.');
      return;
    }

    const error = validateSectionName(trimmedName);
    if (error) {
      toast.error(error);
      return;
    }

    setSections([...sections, trimmedName]);

    setFormField((prev) => [
      ...prev,
      {
        section: trimmedName,
        questions: baseQuestion.map((q) => ({
          ...q,
          section: trimmedName,
        })),
      },
    ]);

    setFocusSection(trimmedName);
  };
src/components/apply/ApplicantList.tsx (1)

27-66: 🛠️ Refactor suggestion

상태 관리 최적화 및 성능 개선이 필요합니다.

현재 구현에서 다음과 같은 개선 포인트가 있습니다:

  1. 불필요한 리렌더링 발생 가능성
  2. 메모이제이션 필요
  3. 필터링 로직 최적화

다음과 같은 성능 최적화를 제안합니다:

+  const filteredApplicants = useMemo(() => {
+    return data
+      .filter((applicant) => applicant.name.includes(keyword))
+      .filter((applicant) => {
+        const statusFiltered = filterApplicantsByStatus(
+          [applicant],
+          type,
+          filterType,
+        );
+        return statusFiltered.length > 0;
+      });
+  }, [data, keyword, type, filterType]);

   useEffect(() => {
-    const filtered = data
-      .filter((applicant) => applicant.name.includes(keyword))
-      .filter((applicant) => {
-        const statusFiltered = filterApplicantsByStatus(
-          [applicant],
-          type,
-          filterType,
-        );
-        return statusFiltered.length > 0;
-      });
-    setApplicants(filtered);
+    setApplicants(filteredApplicants);
   }, [keyword, filterType, type, data]);
📝 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 ApplicantList({ type = 'DOCUMENT', data }: Props) {
  const [{ token }] = useCookies(['token']);
  const [allChecked, setAllChecked] = useState<boolean>(false);
  const [selectedApplicants, setSelectedApplicants] = useState<{
    DOCUMENT: Set<number>;
    INTERVIEW: Set<number>;
  }>({ DOCUMENT: new Set(), INTERVIEW: new Set() });
  const [keyword, setKeyword] = useState<string>('');
  const [filterType, setFilterType] = useState<'ALL' | 'PASS' | 'FAIL'>('ALL');
  const [applicants, setApplicants] = useState<Applicant[]>(data);
  const [passedApplicants, setPassedApplicants] = useState<Applicant[]>([]);
  const [failedApplicants, setFailedApplicants] = useState<Applicant[]>([]);

  const mutation = useUpdateApplicantStatus();

  useEffect(() => {
    setFilterType('ALL');
  }, [type]);

  useEffect(() => {
    const passed = filterPassedApplicants(data, type);
    const failed = filterFailedApplicants(data, type);
    setPassedApplicants(passed);
    setFailedApplicants(failed);
  }, [data, type]);

  const filteredApplicants = useMemo(() => {
    return data
      .filter((applicant) => applicant.name.includes(keyword))
      .filter((applicant) => {
        const statusFiltered = filterApplicantsByStatus(
          [applicant],
          type,
          filterType,
        );
        return statusFiltered.length > 0;
      });
  }, [data, keyword, type, filterType]);

  useEffect(() => {
    setApplicants(filteredApplicants);
  }, [keyword, filterType, type, data]);

  // ... remaining component code if any
}
src/pages/admin/apply/[id]/index.tsx (1)

49-49: 🛠️ Refactor suggestion

불필요한 리네이밍을 제거해주세요.

data: data와 같은 불필요한 리네이밍은 코드를 더 복잡하게 만듭니다.

다음과 같이 수정하는 것을 추천드립니다:

-const { data: data, isLoading } = useAllApplication(Number(id), token);
+const { data, isLoading } = useAllApplication(Number(id), token);
📝 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 { data, isLoading } = useAllApplication(Number(id), token);
🧰 Tools
🪛 Biome (1.9.4)

[error] 49-49: Useless rename.

Safe fix: Remove the renaming.

(lint/complexity/noUselessRename)

src/components/apply/ManageForm.tsx (1)

69-77: 🛠️ Refactor suggestion

폼 생성 시 추가적인 유효성 검사가 필요해 보입니다.

현재는 설명 길이만 검사하고 있습니다. 제목이나 날짜 등 다른 필수 필드들도 검사가 필요합니다.

다음과 같은 추가 검증을 제안드립니다:

  • 제목 필수 입력 확인
  • 모집 기간 유효성 검사
  • 섹션별 최소 1개 이상의 질문 존재 여부 확인

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.

지원자 결과 페이지 파일 다운로드, 시트 컴포넌트 연결
1 participant