Conversation
- 지원자 탭에서 메모가 없을 때 "메모를 입력하지 않았습니다."라는 기본 메시지를 표시하도록 수정
- ApplicantsTab.tsx에서 사용하지 않는 useEffect 훅 제거로 코드 간소화
- 지원자의 지원서 정보를 수정하는 updateApplicantDetail 함수 추가 - PUT 요청을 통해 메모와 상태를 업데이트하며, 오류 처리 로직 포함
- ApplicantsTab.tsx에서 상태 매핑 로직을 별도의 유틸리티 함수 mapStatusToGroup으로 분리하여 코드 가독성 향상 - 상태 매핑을 위한 상수 ApplicationStatus를 사용하여 코드의 명확성 및 유지보수성 개선
- 지원자 상세 페이지에서 메모 입력 및 상태 변경 기능을 추가하여 지원자 정보를 업데이트할 수 있도록 구현 - 메모 변경 시 debounce 처리로 API 호출 최적화 - 지원자 상태를 선택할 수 있는 드롭다운 추가 및 상태 매핑 로직 활용
|
Warning
|
| Cohort / File(s) | Summary |
|---|---|
API 클라이언트 추가frontend/src/apis/application/updateApplicantDetail.ts |
지원자 메모·상태 수정 PUT 요청 함수 updateApplicantDetail 추가. 실패 시 에러 처리 및 재throw, 성공 시 result.data 반환. |
지원자 상세 페이지 편집 로직frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage.tsx |
메모/상태 로컬 상태 관리, 초기 동기화, 디바운스 저장 호출 추가. 상태 드롭다운과 텍스트영역 편집 UI 도입. mapStatusToGroup 기반 라벨 사용. |
지원자 목록 페이지 개선frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx |
상태 배지 표기를 mapStatusToGroup(...).label로 변경. 키워드 기반 필터링(answers[0].value)과 제어형 입력 추가. 메모 미입력 시 플레이스홀더 표시. |
상태 그룹핑 유틸 추가frontend/src/utils/mapStatusToGroup.ts |
상세 상태를 3개 그룹(서류검토/면접예정/합격)으로 매핑하여 {status,label} 반환. 기본값 포함. |
Sequence Diagram(s)
sequenceDiagram
participant Admin as Admin User
participant Detail as ApplicantDetailPage
participant Debounce as Debounced Saver
participant API as updateApplicantDetail()
participant BE as Backend
Admin->>Detail: 메모/상태 편집
Detail->>Debounce: 변경 이벤트 전달
Debounce->>API: PUT /api/club/{clubId}/apply/{appId} {memo,status}
API->>BE: 요청
BE-->>API: 200 OK + data
API-->>Detail: result.data
Detail-->>Admin: UI 상태 동기화
Estimated code review effort
🎯 3 (Moderate) | ⏱️ ~20 minutes
Possibly related issues
- [feature] MOA-140 동아리 지원서 메모와 상태를 변경할 수 있다. #633: 지원자 메모/상태 변경 UI와 PUT API 연동 요청 사항을 이 PR이 구현합니다.
Possibly related PRs
- [feautre] 지원서 관리 API #622: 백엔드 PUT /api/club/{clubId}/apply/{appId} 엔드포인트 추가와 매칭되는 프런트 저장 호출을 본 PR이 도입.
- [Release] v1.0.9 #627: 동일한 ApplicantsTab/ApplicantDetailPage 흐름을 확장하며, 본 PR이 편집/저장 기능을 추가.
- [feature] 지원자를 검색할 수 있다 #650: ApplicantsTab의 클라이언트 검색 관련 변경과 직접적으로 연관(본 PR도 키워드 검색 도입).
Suggested labels
📬 API
Suggested reviewers
- Zepelown
- oesnuj
- seongwon030
✨ Finishing Touches
- 📝 Generate Docstrings
🧪 Generate unit tests
- Create PR with unit tests
- Post copyable unit tests in a comment
- Commit unit tests in branch
feature/applicant-memo-status-change
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
🪧 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.Open a follow-up GitHub issue for this discussion.
- Files and specific lines of code (under the "Files changed" tab): Tag
@coderabbitaiin a new review comment at the desired location with your query. - PR comments: Tag
@coderabbitaiin 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 the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
Support
Need help? Create a ticket on our support page for assistance with any issues or questions.
CodeRabbit Commands (Invoked using PR/Issue comments)
Type @coderabbitai help to get the list of available commands.
Other keywords and placeholders
- Add
@coderabbitai ignoreanywhere in the PR description to prevent this PR from being reviewed. - Add
@coderabbitai summaryto generate the high-level summary at a specific location in the PR description. - Add
@coderabbitaianywhere in the PR title to generate the title automatically.
Status, Documentation and Community
- Visit our Status Page to check the current availability of CodeRabbit.
- 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.
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
There was a problem hiding this comment.
Actionable comments posted: 9
🔭 Outside diff range comments (1)
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (1)
104-112: 리스트 key로 index 사용 지양React key는 안정적인 고유값을 사용해야 합니다. 지원자 id를 사용하는 것이 적합합니다.
- (item: Applicant, index: number) => ( + (item: Applicant) => ( <Styled.ApplicantTableRow - key={index} + key={item.id} style={{ cursor: 'pointer' }} onClick={() => navigate(`/admin/applicants/${item.id}`) } >
🧹 Nitpick comments (5)
frontend/src/apis/application/updateApplicantDetail.ts (1)
15-23: 요청 취소(AbortSignal) 지원으로 디바운스/연속 입력 시 중복 요청 방지디바운스 저장 플로우에서 이전 요청을 취소할 수 있으면 서버 부하와 레이스 컨디션을 줄일 수 있습니다. RequestInit.signal을 허용하도록 시그니처 확장을 권장합니다.
-export const updateApplicantDetail = async ( +export const updateApplicantDetail = async ( memo: string, status: ApplicationStatus, clubId: string, appId: string, -): Promise<Applicant> => { + signal?: AbortSignal, +): Promise<Applicant> => { @@ - { + { method: 'PUT', headers: { 'Content-Type': 'application/json', }, + signal, body: JSON.stringify({ memo, status }) },frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage.tsx (3)
17-21: 매직 넘버(400ms) 상수화디바운스 지연 시간을 의미 있는 이름의 상수로 추출하세요. 유지보수성과 가독성이 좋아집니다.
-const AVAILABLE_STATUSES = [ +const UPDATE_DEBOUNCE_MS = 400; +const AVAILABLE_STATUSES = [ ApplicationStatus.SCREENING, // 서류검토 ApplicationStatus.INTERVIEW_SCHEDULED, // 면접예정 ApplicationStatus.ACCEPTED, // 합격 ];
77-81: 상태 변경 핸들러의 타입 안정성 및 디바운스 함수명 사용 통일로컬 상태 업데이트 후 저장 호출 시 동일한 디바운스 함수를 사용하도록 통일하세요.
const handleStatusChange = (e: React.ChangeEvent<HTMLSelectElement>) => { const newStatus = e.target.value as ApplicationStatus; setApplicantStatus(newStatus); - updateApplicantDetail(applicantMemo, newStatus); + debouncedUpdateApplicant(applicantMemo, newStatus); };
24-27: 네이밍 혼동: questionId는 applicantId에 가깝습니다URL 파라미터가 실질적으로 지원자 식별자이므로 questionId라는 이름은 오해를 부릅니다. 추후 리팩터링 시 applicantId로 변경을 권장합니다.
원하시면 이 파일과 연관 라우트/네비게이션 사용처까지 포함한 일괄 리네이밍 패치를 준비하겠습니다.
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (1)
139-147: 날짜 포맷팅 로직 분리IIFE로 즉석 포맷팅하기보다 유틸 함수로 분리하거나 상단에 헬퍼 함수를 정의하면 가독성이 좋아집니다.
예시:
const formatYMD = (iso: string) => { const d = new Date(iso); const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); return `${y}-${m}-${day}`; };사용:
-(() => { - const date = new Date(item.createdAt); - ... -})() +formatYMD(item.createdAt)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
frontend/src/apis/application/updateApplicantDetail.ts(1 hunks)frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage.tsx(4 hunks)frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx(4 hunks)frontend/src/utils/mapStatusToGroup.ts(1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
frontend/**/*.{ts,tsx}
📄 CodeRabbit Inference Engine (frontend/.cursorrules)
frontend/**/*.{ts,tsx}: Replace magic numbers with named constants for clarity.
Replace complex or nested ternary operators with if/else statements or IIFEs for readability.
Assign complex boolean conditions to named variables.
Use consistent return types for similar functions and hooks.
Avoid hidden side effects; functions should only perform actions implied by their signature (Single Responsibility Principle).
Use unique, descriptive names for custom wrappers and functions to avoid ambiguity.
Define constants near related logic or ensure names link them clearly.
Files:
frontend/src/utils/mapStatusToGroup.tsfrontend/src/apis/application/updateApplicantDetail.tsfrontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage.tsxfrontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx
frontend/**/*.tsx
📄 CodeRabbit Inference Engine (frontend/.cursorrules)
frontend/**/*.tsx: Abstract complex logic/interactions into dedicated components or higher-order components (HOCs).
Separate significantly different conditional UI/logic into distinct components.
Colocate simple, localized logic or use inline definitions to reduce context switching.
Choose field-level or form-level cohesion based on form requirements.
Break down broad state management into smaller, focused hooks or contexts.
Use component composition instead of props drilling.
Files:
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage.tsxfrontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx
🧬 Code Graph Analysis (3)
frontend/src/apis/application/updateApplicantDetail.ts (1)
frontend/src/apis/auth/secureFetch.ts (1)
secureFetch(3-41)
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage.tsx (3)
frontend/src/context/AdminClubContext.tsx (1)
useAdminClubContext(30-37)frontend/src/hooks/queries/application/useGetApplication.ts (1)
useGetApplication(4-10)frontend/src/apis/application/updateApplicantDetail.ts (1)
updateApplicantDetail(5-36)
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (2)
frontend/src/context/AdminClubContext.tsx (1)
useAdminClubContext(30-37)frontend/src/types/applicants.ts (1)
Applicant(27-33)
🪛 Biome (2.1.2)
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx
[error] 14-14: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
🔇 Additional comments (1)
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (1)
123-124: 상태 배지 prop에 label 전달 여부 확인status prop에 label(예: '서류검토')을 넘기고 있습니다. Styled.ApplicantStatusBadge가 label 문자열을 기준으로 스타일링한다면 OK이지만, 내부에서 상태 코드를 기대한다면 색상 매핑이 어긋납니다. 의도한 컨벤션인지 확인 바랍니다.
필요 시 status 코드와 label을 각각 넘기도록 컴포넌트 API를 정리하는 리팩터를 제안할 수 있습니다.
| @@ -0,0 +1,38 @@ | |||
| import API_BASE_URL from '@/constants/api'; | |||
| import { secureFetch } from '@/apis/auth/secureFetch'; | |||
| import { ApplicationStatus } from '@/types/applicants'; | |||
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
반환 타입 명시 및 응답 타입 안정화
현재 반환 타입이 암시적(any)입니다. 호출부에서 안전하게 사용할 수 있도록 Applicant 타입을 반환하도록 명시하세요.
타입 정의가 맞다면 아래와 같이 적용 가능합니다. 만약 API가 다른 형태를 반환한다면 해당 DTO 타입을 새로 정의해 주세요.
-import { ApplicationStatus } from '@/types/applicants';
+import { ApplicationStatus, Applicant } from '@/types/applicants';
-export const updateApplicantDetail = async (
+export const updateApplicantDetail = async (
memo: string,
status: ApplicationStatus,
clubId: string,
appId: string,
-) => {
+): Promise<Applicant> => {
@@
- const result = await response.json();
- return result.data;
+ const result = (await response.json()) as { data: Applicant };
+ return result.data;호출부 타입 오류가 없는지 확인 부탁드립니다. 필요시 제가 검색 스크립트를 제공하여 호출부를 점검할 수 있습니다.
Also applies to: 5-10, 30-31
🏁 Script executed:
#!/bin/bash
set -e
echo "------ Showing updateApplicantDetail.ts ------"
sed -n '1,200p' frontend/src/apis/application/updateApplicantDetail.ts || echo "File not found"
echo
echo "------ Listing files under src/types matching applicants ------"
fd 'applicants' -t f src/types || echo "No matching files"
echo
echo "------ Displaying content of first matching types file ------"
TFILE=$(fd 'applicants' -t f src/types | head -n1)
if [ -n "$TFILE" ]; then
sed -n '1,200p' "$TFILE"
else
echo "No type definition file found for applicants"
fiLength of output: 1783
🏁 Script executed:
#!/bin/bash
set -e
echo "Searching for types/applicants.ts in frontend/src"
fd 'applicants.ts' -t f frontend/src || echo "No applicants.ts found under frontend/src"
echo
echo "Displaying content of found applicants.ts"
for file in $(fd 'applicants.ts' -t f frontend/src); do
echo "----- $file -----"
sed -n '1,200p' "$file"
done
echo
echo "Searching for ApplicationStatus references"
rg 'ApplicationStatus' -n frontend/src || echo "No ApplicationStatus references found"Length of output: 5130
🏁 Script executed:
#!/bin/bash
set -e
echo "Searching for updateApplicantDetail references in frontend/src"
rg 'updateApplicantDetail' -n frontend/src || echo "No references found"
echo
echo "Searching for updateApplicantMemo references in frontend/src"
rg 'updateApplicantMemo' -n frontend/src || echo "No references found"Length of output: 1409
🏁 Script executed:
#!/bin/bash
set -e
echo "----- frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage.tsx -----"
sed -n '1,200p' frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage.tsxLength of output: 4882
API 응답 타입 명시: updateApplicantDetail에 Applicant 반환 타입 지정 필요
updateApplicantDetail 함수가 현재 암시적 any를 반환하므로, API 호출부에서 타입 안정성을 확보하려면 Applicant 타입을 명시해주세요.
- 파일:
frontend/src/apis/application/updateApplicantDetail.ts
적용 예시(diff):
-import { ApplicationStatus } from '@/types/applicants';
+import { ApplicationStatus, Applicant } from '@/types/applicants';
-export const updateApplicantDetail = async (
+export const updateApplicantDetail = async (
memo: string,
status: ApplicationStatus,
clubId: string,
appId: string,
-): Promise<any> => {
+): Promise<Applicant> => {
@@
- const result = await response.json();
- return result.data;
+ const result = (await response.json()) as { data: Applicant };
+ return result.data;변경 후 호출부(updateApplicantDetail 또는 updateApplicantMemo)에서 반환값을 사용하고 있지는 않으므로, 별도의 타입 오류는 발생하지 않습니다.
필요시 추가 검토 부탁드립니다.
📝 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 { ApplicationStatus } from '@/types/applicants'; | |
| import { ApplicationStatus, Applicant } from '@/types/applicants'; | |
| export const updateApplicantDetail = async ( | |
| memo: string, | |
| status: ApplicationStatus, | |
| clubId: string, | |
| appId: string, | |
| ): Promise<Applicant> => { | |
| const response = await fetch( | |
| `/api/applications/${clubId}/${appId}`, | |
| { | |
| method: 'PUT', | |
| body: JSON.stringify({ memo, status }), | |
| } | |
| ); | |
| const result = (await response.json()) as { data: Applicant }; | |
| return result.data; | |
| }; |
🤖 Prompt for AI Agents
In frontend/src/apis/application/updateApplicantDetail.ts around line 3, the
function updateApplicantDetail currently returns an implicit any; import the
Applicant type from the correct types file and annotate the function (and its
Promise) to return Promise<Applicant> (or Promise<Applicant | null> if the API
can return null), update any related helper types/signatures (and similarly
annotate updateApplicantMemo if applicable), and run typecheck to ensure callers
remain valid.
| if (!response.ok) { | ||
| throw new Error('지원자의 지원서 정보 수정에 실패했습니다.'); | ||
| } | ||
|
|
||
| const result = await response.json(); | ||
| return result.data; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
서버 에러 메시지 파싱 및 상세 에러 전달 필요
response.ok가 false일 때 서버가 내려주는 에러 메시지를 무시하고 고정 문구로 던지고 있습니다. 서버 메시지를 우선 파싱해서 전달하면 디버깅과 사용자 피드백에 유리합니다.
아래처럼 서버 에러를 시도-파싱 후 fallback 하도록 수정 제안드립니다.
- if (!response.ok) {
- throw new Error('지원자의 지원서 정보 수정에 실패했습니다.');
- }
-
- const result = await response.json();
+ if (!response.ok) {
+ let message = '지원자의 지원서 정보 수정에 실패했습니다.';
+ try {
+ const err = await response.json();
+ if (err?.message) message = err.message;
+ } catch {
+ // body가 없거나 JSON 파싱 실패 시 fallback
+ }
+ throw new Error(message);
+ }
+
+ const result = (await response.json());
return result.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.
| if (!response.ok) { | |
| throw new Error('지원자의 지원서 정보 수정에 실패했습니다.'); | |
| } | |
| const result = await response.json(); | |
| return result.data; | |
| if (!response.ok) { | |
| let message = '지원자의 지원서 정보 수정에 실패했습니다.'; | |
| try { | |
| const err = await response.json(); | |
| if (err?.message) message = err.message; | |
| } catch { | |
| // body가 없거나 JSON 파싱 실패 시 fallback | |
| } | |
| throw new Error(message); | |
| } | |
| const result = (await response.json()); | |
| return result.data; |
🤖 Prompt for AI Agents
In frontend/src/apis/application/updateApplicantDetail.ts around lines 26 to 31,
the code throws a fixed error when response.ok is false and ignores the
server-provided error payload; update it to await and attempt to parse
response.json() (in a try/catch), extract a meaningful message or error field
from the parsed body, and throw a new Error containing that server message with
a fallback to the existing static message if parsing fails or the expected
fields are missing; ensure you only consume the response body once and handle
JSON parse errors gracefully.
| useEffect(() => { | ||
| if (applicant) { | ||
| setAppMemo(applicant.memo); | ||
| setApplicantStatus(mapStatusToGroup(applicant.status).status); | ||
| } | ||
| }, [applicant]); | ||
|
|
There was a problem hiding this comment.
초기 선택값 미정의로 인한 select 경고 및 비일관 상태
첫 렌더에서 applicantStatus가 undefined라 select value가 매칭되지 않아 React 경고가 발생할 수 있습니다. 빈 문자열을 허용하고 placeholder option을 추가해 안전하게 처리하세요. 또한 mapStatusToGroup 변경(서류검토 → SCREENING 반환)이 반영되어야 드롭다운과 값이 일치합니다.
- const [applicantStatus, setApplicantStatus] = useState<ApplicationStatus>();
+ const [applicantStatus, setApplicantStatus] = useState<ApplicationStatus | ''>('');
@@
- setApplicantStatus(mapStatusToGroup(applicant.status).status);
+ setApplicantStatus(mapStatusToGroup(applicant.status).status);그리고 select 영역을 아래와 같이 수정합니다(placeholder 추가 및 안전한 value 적용).
- <select id="statusSelect" value={applicantStatus} onChange={handleStatusChange}>
+ <select id="statusSelect" value={applicantStatus ?? ''} onChange={handleStatusChange}>
+ <option value="" disabled>상태를 선택하세요</option>
{AVAILABLE_STATUSES.map((status) => (
<option key={status} value={status}>
{mapStatusToGroup(status).label}
</option>
))}
</select>📝 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 (applicant) { | |
| setAppMemo(applicant.memo); | |
| setApplicantStatus(mapStatusToGroup(applicant.status).status); | |
| } | |
| }, [applicant]); | |
| // --- State initialization (add empty‐string as a valid initial state) --- | |
| - const [applicantStatus, setApplicantStatus] = useState<ApplicationStatus>(); | |
| + const [applicantStatus, setApplicantStatus] = useState<ApplicationStatus | ''>(''); | |
| // --- useEffect (no change) --- | |
| useEffect(() => { | |
| if (applicant) { | |
| setAppMemo(applicant.memo); | |
| setApplicantStatus(mapStatusToGroup(applicant.status).status); | |
| } | |
| }, [applicant]); | |
| // --- In the JSX where the <select> lives --- | |
| - <select id="statusSelect" value={applicantStatus} onChange={handleStatusChange}> | |
| + <select id="statusSelect" value={applicantStatus ?? ''} onChange={handleStatusChange}> | |
| + <option value="" disabled>상태를 선택하세요</option> | |
| {AVAILABLE_STATUSES.map((status) => ( | |
| <option key={status} value={status}> | |
| {mapStatusToGroup(status).label} | |
| </option> | |
| ))} | |
| </select> |
| const updateApplicantDetail = useMemo( | ||
| () => | ||
| debounce((memo: any, status: any) => { | ||
| updateApplicantMemo(memo as string, status as ApplicationStatus, clubId!, questionId!); | ||
| }, 400), | ||
| [clubId, questionId], | ||
| ); | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
디바운스된 업데이트 함수의 명확한 명명 및 타입 명시
any 사용을 제거하고, 함수명이 역할을 드러내도록 바꾸면 가독성이 향상됩니다. 또한 내부 Promise rejection이 처리되지 않아 Unhandled Rejection이 발생할 수 있으니 명시적으로 처리하세요.
- const updateApplicantDetail = useMemo(
- () =>
- debounce((memo: any, status: any) => {
- updateApplicantMemo(memo as string, status as ApplicationStatus, clubId!, questionId!);
- }, 400),
- [clubId, questionId],
- );
+ const debouncedUpdateApplicant = useMemo(
+ () =>
+ debounce((memo: string, status: ApplicationStatus) => {
+ void updateApplicantMemo(memo, status, clubId!, questionId!).catch((err) => {
+ console.error('지원자 정보 업데이트 실패:', err);
+ });
+ }, UPDATE_DEBOUNCE_MS),
+ [clubId, questionId],
+ );📝 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 updateApplicantDetail = useMemo( | |
| () => | |
| debounce((memo: any, status: any) => { | |
| updateApplicantMemo(memo as string, status as ApplicationStatus, clubId!, questionId!); | |
| }, 400), | |
| [clubId, questionId], | |
| ); | |
| const debouncedUpdateApplicant = useMemo( | |
| () => | |
| debounce((memo: string, status: ApplicationStatus) => { | |
| void updateApplicantMemo(memo, status, clubId!, questionId!).catch((err) => { | |
| console.error('지원자 정보 업데이트 실패:', err); | |
| }); | |
| }, UPDATE_DEBOUNCE_MS), | |
| [clubId, questionId], | |
| ); |
🤖 Prompt for AI Agents
In frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage.tsx
around lines 44-51, rename the debounced function to a clearer name (e.g.,
debouncedUpdateApplicantDetail), replace the any types with explicit types
(memo: string, status: ApplicationStatus) and keep clubId and questionId
typed/non-null assertions as needed, and ensure the Promise returned by
updateApplicantMemo is handled to avoid unhandled rejections by appending a
.catch(...) that logs or handles the error; keep the debounce timeout and
dependency array as-is.
| const handleMemoChange = (e: any) => { | ||
| const newMemo = e.target.value; | ||
| setAppMemo(newMemo); | ||
| updateApplicantDetail(newMemo, applicantStatus); | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
입력 핸들러 타입 명시 및 onChange 사용 권장
textarea에는 onChange 사용이 일반적이며 타입은 React.ChangeEvent로 명시하세요. 또한 applicantStatus가 확정되지 않은 경우 서버 호출을 건너뛰도록 가드가 필요합니다.
- const handleMemoChange = (e: any) => {
+ const handleMemoChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newMemo = e.target.value;
setAppMemo(newMemo);
- updateApplicantDetail(newMemo, applicantStatus);
+ if (applicantStatus) {
+ debouncedUpdateApplicant(newMemo, applicantStatus as ApplicationStatus);
+ }
};그리고 textarea 속성을 onInput → onChange로 변경합니다.
- <textarea onInput={handleMemoChange} placeholder='메모를 입력해주세요' value={applicantMemo}></textarea>
+ <textarea onChange={handleMemoChange} placeholder='메모를 입력해주세요' value={applicantMemo}></textarea>Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage.tsx
around lines 71-75, the textarea input handler currently uses a loosely typed
parameter and triggers updateApplicantDetail even when applicantStatus may be
unset; change the handler signature to accept event:
React.ChangeEvent<HTMLTextAreaElement>, switch the textarea prop from onInput to
onChange, and add a guard so updateApplicantDetail is only called when
applicantStatus is present/defined (still call setAppMemo unconditionally).
| const filteredApplicants = useMemo(() => { | ||
| const lower = keyword.trim().toLowerCase(); | ||
| if (!lower) return applicants; // 검색어가 없는 경우 모든 지원자 반환 | ||
| return applicants.filter((item) => { | ||
| const name = String(item.answers?.[0]?.value ?? '').toLowerCase(); | ||
| return name.includes(lower); | ||
| }); | ||
| }, [applicants, keyword]); | ||
| if (!applicantsData?.applicants) return []; | ||
|
|
||
| if (!clubId) return null; | ||
| if (!keyword.trim()) return applicantsData.applicants; | ||
|
|
||
| return applicantsData.applicants.filter((user: Applicant) => | ||
| user.answers[0].value.toLowerCase().includes(keyword.trim().toLowerCase()) | ||
| ); | ||
| }, [applicantsData, keyword]); | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
검색 필터의 안전성 강화(answers가 비어있는 경우 대비)
answers[0]에 직접 접근하고 있어 빈 배열일 때 런타임 에러가 발생합니다. 안전하게 접근하도록 수정하세요.
- return applicantsData.applicants.filter((user: Applicant) =>
- user.answers[0].value.toLowerCase().includes(keyword.trim().toLowerCase())
- );
+ const key = keyword.trim().toLowerCase();
+ return applicantsData.applicants.filter((user: Applicant) => {
+ const name = user.answers?.[0]?.value?.toString().toLowerCase() ?? '';
+ return name.includes(key);
+ });🧰 Tools
🪛 Biome (2.1.2)
[error] 14-14: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
🤖 Prompt for AI Agents
In frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx around
lines 14 to 23, the filter accesses user.answers[0] directly which can throw if
answers is undefined or empty; update the predicate to safely check that
user.answers is an array with at least one element and that answers[0].value is
a string (use Array.isArray(user.answers) && user.answers.length > 0 and typeof
user.answers[0].value === 'string' or use optional chaining with a fallback like
(user.answers?.[0]?.value ?? '') before calling toLowerCase), then apply the
keyword match against that safe string so empty or missing answers won’t cause
runtime errors.
| import { ApplicationStatus } from "@/types/applicants"; | ||
|
|
||
| const mapStatusToGroup = (status: ApplicationStatus): { status: ApplicationStatus, label: string } => { |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
상태 값 전체 목록 검증 필요
정의되지 않은 상태(예: REJECTED, WITHDRAWN 등)가 존재한다면 현재 default가 '서류검토'로 떨어져 잘못된 라벨을 표시할 수 있습니다. 실제 ApplicationStatus 정의를 확인해 누락된 상태가 없는지 검증해 주세요.
필요 시 다음 스크립트로 상태 정의를 검색해 목록을 확인할 수 있습니다.
🏁 Script executed:
#!/bin/bash
# ApplicationStatus 정의와 사용처 점검
rg -n "export (enum|type) ApplicationStatus" -A 50
rg -n "ApplicationStatus\." -A 0Length of output: 2905
모든 ApplicationStatus에 대한 매핑 케이스 추가 필요
frontend/src/utils/mapStatusToGroup.ts의 mapStatusToGroup 함수가 ApplicationStatus enum의 일부 상태만 처리하고 나머지는 default로 ‘서류검토’로 떨어뜨리고 있습니다.
frontend/src/types/applicants.ts에 정의된 전체 상태는 다음과 같습니다:
- DRAFT
- SUBMITTED
- SCREENING
- SCREENING_PASSED
- SCREENING_FAILED
- INTERVIEW_SCHEDULED
- INTERVIEW_IN_PROGRESS
- INTERVIEW_PASSED
- INTERVIEW_FAILED
- OFFERED
- ACCEPTED
- DECLINED
- CANCELED_BY_APPLICANT
현 매핑 함수에 누락된 상태:
- SUBMITTED
- SCREENING
- SCREENING_PASSED
- SCREENING_FAILED
- INTERVIEW_IN_PROGRESS
- INTERVIEW_PASSED
- INTERVIEW_FAILED
- OFFERED
- DECLINED
- CANCELED_BY_APPLICANT
이들 상태가 default 케이스로 떨어지면 잘못된 라벨이 표시될 수 있으므로, 각 상태에 맞는 그룹 라벨을 추가하거나, default에서 예외를 던지도록 수정해 주세요.
🤖 Prompt for AI Agents
In frontend/src/utils/mapStatusToGroup.ts around lines 1-3, the mapStatusToGroup
function currently handles only some ApplicationStatus values and falls back to
a default '서류검토'; update the function to handle every ApplicationStatus
explicitly (DRAFT, SUBMITTED, SCREENING, SCREENING_PASSED, SCREENING_FAILED,
INTERVIEW_SCHEDULED, INTERVIEW_IN_PROGRESS, INTERVIEW_PASSED, INTERVIEW_FAILED,
OFFERED, ACCEPTED, DECLINED, CANCELED_BY_APPLICANT) by adding explicit cases
that return appropriate labels (e.g., DRAFT:'임시', SUBMITTED:'제출됨',
SCREENING:'서류검토', SCREENING_PASSED:'서류합격', SCREENING_FAILED:'서류탈락',
INTERVIEW_SCHEDULED:'면접일정', INTERVIEW_IN_PROGRESS:'면접진행',
INTERVIEW_PASSED:'면접합격', INTERVIEW_FAILED:'면접불합격', OFFERED:'오퍼발행',
ACCEPTED:'수락', DECLINED:'거절', CANCELED_BY_APPLICANT:'지원자취소'); remove the
catch-all default that mislabels states and optionally add a final throw or
exhaustive check to fail fast if an unknown status is passed.
| const { data: formData, isLoading, isError } = useGetApplication(clubId!); | ||
|
|
||
| const applicant = useMemo( | ||
| () => applicantsData?.applicants.find((a) => a.id === questionId), |
There was a problem hiding this comment.
applicants 가 새로 만들어지면서 참조가 바뀌는데 성능 부분에서 큰 문제는 없을까요?
There was a problem hiding this comment.
실제로 useMemo를 이용한 성능향상이 없는거같네요. 어렵네요
| () => | ||
| debounce((memo: any, status: any) => { | ||
| updateApplicantMemo(memo as string, status as ApplicationStatus, clubId!, questionId!); | ||
| }, 400), |
There was a problem hiding this comment.
직접쳐봤는데 400이 적당하더라고요
#️⃣연관된 이슈
#633
📝작업 내용
지원자 메모를 변경할 수 있습니다
지원자의 지원상태를 변경할 수 있습니다.
중점적으로 리뷰받고 싶은 부분(선택)
논의하고 싶은 부분(선택)
🫡 참고사항
Summary by CodeRabbit
신기능
리팩터링