-
Notifications
You must be signed in to change notification settings - Fork 3
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
base: staging
Are you sure you want to change the base?
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Walkthrough이번 PR은 패키지 의존성 추가와 함께 API, 컴포넌트, 타입, 커스텀 훅 및 유틸 함수 등 코드베이스 전반에 걸친 대대적인 기능 확장을 포함합니다. 특히 지원서 관리 및 어드민 인터페이스 기능이 확장되고, 이전 이벤트 관련 기능들이 제거되었습니다. Changes
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 등)
Possibly related PRs
Suggested labels
Suggested reviewers
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? 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
Documentation and Community
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
접근성 및 사용자 경험 개선이 필요합니다.
- 접근성 속성이 누락되어 있습니다.
- 체크박스의 크기가 모바일에서 너무 작을 수 있습니다.
<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버튼 핸들러와 접근성 속성이 누락되었습니다.
다음과 같은 개선이 필요합니다:
- 취소와 다음 버튼에 대한 클릭 핸들러가 구현되어 있지 않습니다.
- 버튼에 aria-label과 같은 접근성 속성이 누락되었습니다.
- 작업 중 상태(로딩)를 표시하는 기능이 없습니다.
다음과 같이 수정해주세요:
- <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
모달의 접근성과 외부 클릭 처리가 개선되어야 합니다.
- 모달에 대한 적절한 ARIA 속성이 누락되었습니다.
- 외부 클릭 시 모달을 닫는 기능이 없습니다.
- 키보드 접근성(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
중복된 인터페이스를 통합하세요.
QuestionField
와FormField
인터페이스가 동일한 구조를 가지고 있습니다. 코드 유지보수성을 높이기 위해 하나의 인터페이스로 통합하는 것이 좋습니다.다음과 같이 수정하는 것을 제안합니다:
-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
접근성 및 키보드 네비게이션 개선이 필요합니다.
드롭다운 메뉴의 접근성과 키보드 사용성을 개선해야 합니다.
- ARIA 속성 추가
- 키보드 네비게이션 지원
- 포커스 관리
{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 issueuseEffect 의존성 배열 검토 필요
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 issueuseMemo 의존성 배열 누락
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 issueTextArea 컴포넌트의 disabled 속성이 반대로 설정되어 있습니다.
편집 모드일 때 TextArea가 비활성화되고, 편집 모드가 아닐 때 활성화되는 것은 논리적으로 맞지 않습니다.
- disabled={isEditing} + disabled={!isEditing || isClosed}Also applies to: 149-154
29-31: 🛠️ Refactor suggestion
Props 인터페이스의 타입 안전성 개선이 필요합니다.
setFormField
와section.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 issuegetServerSideProps에서 타입 변환이 필요합니다.
쿼리 파라미터가 문자열로 전달되므로, 숫자로 변환하는 과정이 필요합니다.
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
상태 관리 및 로딩 처리 개선이 필요합니다.
현재 상태 관리와 로딩 처리가 다소 복잡하게 구현되어 있습니다. 다음과 같은 개선을 제안드립니다:
- 로딩 상태에 대한 사용자 피드백 추가
- 에러 상태 처리 추가
- 상태 관리 로직 단순화
다음과 같은 리팩토링을 제안합니다:
+ 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
섹션 추가 시 입력 검증 로직 강화가 필요합니다.
현재 섹션 이름 검증이 기본적인 수준에 머물러 있습니다. 다음과 같은 개선사항을 고려해보세요:
- 특수문자 및 공백 처리
- 최소 길이 검증
- 더 자세한 에러 메시지
다음과 같은 개선된 구현을 제안합니다:
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
상태 관리 최적화 및 성능 개선이 필요합니다.
현재 구현에서 다음과 같은 개선 포인트가 있습니다:
- 불필요한 리렌더링 발생 가능성
- 메모이제이션 필요
- 필터링 로직 최적화
다음과 같은 성능 최적화를 제안합니다:
+ 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개 이상의 질문 존재 여부 확인
🔥 연관 이슈
🚀 작업 내용
🤔 고민했던 내용
💬 리뷰 중점사항
Summary by CodeRabbit