Skip to content

Comments

[feature] 지원자의 상태와 메모를 변경한다.#651

Merged
seongwon030 merged 9 commits intodevelop-fefrom
feature/applicant-memo-status-change
Aug 13, 2025
Merged

[feature] 지원자의 상태와 메모를 변경한다.#651
seongwon030 merged 9 commits intodevelop-fefrom
feature/applicant-memo-status-change

Conversation

@lepitaaar
Copy link
Contributor

@lepitaaar lepitaaar commented Aug 13, 2025

#️⃣연관된 이슈

#633

📝작업 내용

지원자 메모를 변경할 수 있습니다
지원자의 지원상태를 변경할 수 있습니다.

중점적으로 리뷰받고 싶은 부분(선택)

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

ex) 메서드 XXX의 이름을 더 잘 짓고 싶은데 혹시 좋은 명칭이 있을까요?

논의하고 싶은 부분(선택)

논의하고 싶은 부분이 있다면 작성해주세요.

🫡 참고사항

Summary by CodeRabbit

  • 신기능

    • 지원자 상세 페이지에서 메모와 상태를 직접 편집하고 자동 저장(지연 적용)됩니다.
    • 상태 드롭다운 추가(서류검토/면접예정/합격)로 가독성이 향상되었습니다.
    • 지원자 목록에 키워드 검색이 추가되어 답변 기반 필터링이 가능합니다.
    • 목록에서 메모가 없을 경우 안내 문구를 표시합니다.
  • 리팩터링

    • 상태 배지를 통합 매핑 로직으로 일원화해 표시 일관성을 강화했습니다.
    • 지원자 조회와 필터링을 메모이제이션하여 렌더링 효율을 개선했습니다.

- 지원자 탭에서 메모가 없을 때 "메모를 입력하지 않았습니다."라는 기본 메시지를 표시하도록 수정
- ApplicantsTab.tsx에서 사용하지 않는 useEffect 훅 제거로 코드 간소화
- 지원자의 지원서 정보를 수정하는 updateApplicantDetail 함수 추가
- PUT 요청을 통해 메모와 상태를 업데이트하며, 오류 처리 로직 포함
- ApplicantsTab.tsx에서 상태 매핑 로직을 별도의 유틸리티 함수 mapStatusToGroup으로 분리하여 코드 가독성 향상
- 상태 매핑을 위한 상수 ApplicationStatus를 사용하여 코드의 명확성 및 유지보수성 개선
- 지원자 상세 페이지에서 메모 입력 및 상태 변경 기능을 추가하여 지원자 정보를 업데이트할 수 있도록 구현
- 메모 변경 시 debounce 처리로 API 호출 최적화
- 지원자 상태를 선택할 수 있는 드롭다운 추가 및 상태 매핑 로직 활용
@lepitaaar lepitaaar requested a review from seongwon030 August 13, 2025 08:28
@lepitaaar lepitaaar self-assigned this Aug 13, 2025
@lepitaaar lepitaaar added ✨ Feature 기능 개발 💻 FE Frontend labels Aug 13, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 13, 2025

Warning

.coderabbit.yaml has a parsing error

The CodeRabbit configuration file in this repository has a parsing error and default settings were used instead. Please fix the error(s) in the configuration file. You can initialize chat with CodeRabbit to get help with the configuration file.

💥 Parsing errors (1)
Validation error: Invalid regex pattern for base branch. Received: "**" at "reviews.auto_review.base_branches[0]"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Warning

Rate limit exceeded

@lepitaaar has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 16 minutes and 10 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between e726da9 and 7952a99.

📒 Files selected for processing (2)
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (2 hunks)
  • frontend/src/utils/mapStatusToGroup.ts (1 hunks)

Walkthrough

지원자 메모/상태 수정 기능을 위한 API 클라이언트를 추가하고, 관리자용 지원자 상세 페이지에 메모/상태 편집과 디바운스 저장 로직을 도입했습니다. 목록 페이지는 상태 배지 표시를 공용 유틸로 통일하고 키워드 기반 필터링을 추가했습니다. 상태 그룹 매핑 유틸을 신규 도입했습니다.

Changes

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 상태 동기화
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Possibly related PRs

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.

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

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.

@vercel
Copy link

vercel bot commented Aug 13, 2025

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

Project Deployment Preview Comments Updated (UTC)
moadong Ready Preview Comment Aug 13, 2025 8:42am

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 65501d3 and e726da9.

📒 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.ts
  • frontend/src/apis/application/updateApplicantDetail.ts
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage.tsx
  • frontend/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.tsx
  • frontend/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';
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 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"
fi

Length 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.tsx

Length 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.

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

Comment on lines +26 to +31
if (!response.ok) {
throw new Error('지원자의 지원서 정보 수정에 실패했습니다.');
}

const result = await response.json();
return result.data;
Copy link
Contributor

Choose a reason for hiding this comment

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

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

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

Comment on lines +37 to +43
useEffect(() => {
if (applicant) {
setAppMemo(applicant.memo);
setApplicantStatus(mapStatusToGroup(applicant.status).status);
}
}, [applicant]);

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

초기 선택값 미정의로 인한 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.

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

Comment on lines +44 to +51
const updateApplicantDetail = useMemo(
() =>
debounce((memo: any, status: any) => {
updateApplicantMemo(memo as string, status as ApplicationStatus, clubId!, questionId!);
}, 400),
[clubId, questionId],
);

Copy link
Contributor

Choose a reason for hiding this comment

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

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

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

Comment on lines +71 to +75
const handleMemoChange = (e: any) => {
const newMemo = e.target.value;
setAppMemo(newMemo);
updateApplicantDetail(newMemo, applicantStatus);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ 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).

Comment on lines 14 to 23
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]);

Copy link
Contributor

Choose a reason for hiding this comment

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

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

Comment on lines +1 to +3
import { ApplicationStatus } from "@/types/applicants";

const mapStatusToGroup = (status: ApplicationStatus): { status: ApplicationStatus, label: string } => {
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 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 0

Length of output: 2905


모든 ApplicationStatus에 대한 매핑 케이스 추가 필요

frontend/src/utils/mapStatusToGroup.tsmapStatusToGroup 함수가 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.

Copy link
Member

@seongwon030 seongwon030 left a comment

Choose a reason for hiding this comment

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

고생하셨습니다 ! 리뷰 천천히 달아주세요~

const { data: formData, isLoading, isError } = useGetApplication(clubId!);

const applicant = useMemo(
() => applicantsData?.applicants.find((a) => a.id === questionId),
Copy link
Member

Choose a reason for hiding this comment

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

applicants 가 새로 만들어지면서 참조가 바뀌는데 성능 부분에서 큰 문제는 없을까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

실제로 useMemo를 이용한 성능향상이 없는거같네요. 어렵네요

() =>
debounce((memo: any, status: any) => {
updateApplicantMemo(memo as string, status as ApplicationStatus, clubId!, questionId!);
}, 400),
Copy link
Member

Choose a reason for hiding this comment

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

400으로 설정하신 이유가 따로 있나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

직접쳐봤는데 400이 적당하더라고요

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💻 FE Frontend ✨ Feature 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants