Skip to content

[feature] 여러명의 지원상태를 변경할 수 있다.#710

Merged
lepitaaar merged 5 commits intodevelop-fefrom
feature/#701-change-multiple-status-applicants-MOA-199
Sep 1, 2025
Merged

[feature] 여러명의 지원상태를 변경할 수 있다.#710
lepitaaar merged 5 commits intodevelop-fefrom
feature/#701-change-multiple-status-applicants-MOA-199

Conversation

@lepitaaar
Copy link
Contributor

@lepitaaar lepitaaar commented Aug 28, 2025

#️⃣연관된 이슈

#701

📝작업 내용

코드 변경점

export interface UpdateApplicantParams {
  memo: string;
  status: ApplicationStatus;
  applicantId: string | undefined;
}

applicantId를 파라미터로 넘겨받게 하였습니다.

const { mutate: updateApplicant } = useUpdateApplicant(clubId!, questionId!);

이전코드는 useUpdateApplicant 훅을 초기화할때 현재 context의 applicantId 기준으로 초기화가 되어
updateApplicant의 함수가 호출될때 debounce 적용 시간(400ms) 이전에 페이지를 옮기게되면
훅이 바뀐 applicantId 기준으로 초기화 되어 이전 지원자를 변경하지 못하는 버그가 존재하였습니다.
따라서 applicantId를 함수 인자로 옮김에 따라 다른 페이지 변경시에도 이전 사용자를 호출하게 변경하였습니다


  useEffect(() => {
    const handleOutsideClick = (e: MouseEvent) => {
      const target = e.target as Node;

      if (
        open &&
        allSelectRef.current &&
        !allSelectRef.current.contains(target)
      ) {
        setOpen(false);
      }
      if (
        statusOpen &&
        statusSelectRef.current &&
        !statusSelectRef.current.contains(target)
      ) {
        setStatusOpen(false);
      }
    };

    document.addEventListener('mousedown', handleOutsideClick);
    return () => {
      document.removeEventListener('mousedown', handleOutsideClick);
    };
  }, [open, statusOpen]);

useRef로 드롭메뉴를 펼친후 다른 부분을 클릭했을때 꺼지게 변경하였습니다.


  const [isChecked, setIsChecked] = useState(false);

  useEffect(() => {
    ...
    setIsChecked(Array.from(checkedItem.values()).some(Boolean));
  }, [checkedItem]);

기존 disabled에 사용한 조건을 상태로 옮겼습니다.
disabled를 이용하는 컴포넌트가 3개로 많아짐에 따라 한개의 상태로 반복하는 코드를 줄였습니다.


  const updateAllApplicants = (status: ApplicationStatus) => {
    updateDetailApplicants(
      applicantsData!.applicants
        .filter((applicant) => checkedItem.get(applicant.id))
        .map(
          (applicant) => ({
            applicantId: applicant.id,
            memo: applicant.memo,
            status: status,
          })
        ),
    );
  };

현재 체크된 applicantId를 기준으로 필터 후 map을 이용해 UpdateApplicantParams 형식에 맞게 변환후 배열로 응답해줬습니다.


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

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

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

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

API를 호출하고 응답받는 딜레이가 존재하는데, Spinner를 추가해서 응답중임을 나타내는것이 좋을까요?

🫡 참고사항

Summary by CodeRabbit

  • New Features
    • 지원자 상태 일괄 변경: 여러 지원자를 선택해 서류검토/면접예정/합격/불합격을 한 번에 업데이트할 수 있습니다.
    • 배치 업데이트 방식 도입으로 여러 지원자에 대한 동시 수정이 가능해졌습니다.
  • Style
    • 상태 선택 UI를 커스텀 드롭다운으로 개선하고, 외부 클릭 시 자동으로 닫히도록 처리했습니다.
    • 항목 선택 시에만 상태 변경 메뉴와 삭제 버튼이 활성화되며, 전체선택/개별선택 흐름을 개선했습니다.

@lepitaaar lepitaaar self-assigned this Aug 28, 2025
@lepitaaar lepitaaar added ✨ Feature 기능 개발 📬 API 서버 API 통신 작업 💻 FE Frontend labels Aug 28, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 28, 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

Walkthrough

여러 지원자 상태/메모 업데이트를 단건에서 배치 방식으로 전환했다. API 엔드포인트와 요청 스키마를 변경했고, 관련 훅/페이지/스타일/타입을 업데이트했다. 성공 시 캐시 무효화는 유지된다.

Changes

Cohort / File(s) Change Summary
API 배치 업데이트 전환
frontend/src/apis/application/updateApplicantDetail.ts
단건 → 배치 시그니처로 변경(UpdateApplicantParams[]), 엔드포인트 /api/club/{clubId}/applicant 로 전환, 응답 ok 검사 및 result.data 반환, 예외 처리 및 기본 내보내기 추가
리액트 훅 업데이트
frontend/src/hooks/queries/applicants/useUpdateApplicant.ts
훅 파라미터에서 applicantId 제거, mutationFnUpdateApplicantParams[] 수용, 새로운 API 시그니처 사용, 성공 시 ['clubApplicants'] 캐시 무효화
페이지 로직(상세/목록) 반영
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx, frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx
mutate를 배열 페이로드로 호출하도록 변경(각 항목에 applicantId 포함), 체크박스 기반 배치 상태 변경 기능 및 외부 클릭으로 드롭다운 닫기 처리 추가, 선택 상태에 따른 버튼/드롭다운 활성화 제어
스타일/드롭다운 UI 교체
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts
StatusSelectselectp로 변경(비활성 처리), 커스텀 드롭다운(StatusSelectMenu, StatusSelectMenuItem) 추가 및 관련 스타일 변경
타입 추가
frontend/src/types/applicants.ts
UpdateApplicantParams 타입 신설({ memo, status, applicantId })

Sequence Diagram(s)

sequenceDiagram
    actor Admin as 관리자 UI
    participant Page as ApplicantsTab / ApplicantDetailPage
    participant Hook as useUpdateApplicant
    participant API as updateApplicantDetail
    participant BE as 서버 (/api/club/{clubId}/applicant)
    participant Cache as Query Cache

    Admin->>Page: 여러 항목 선택 후 상태/메모 변경
    Page->>Hook: mutate([ { applicantId, status, memo }, ... ])
    Hook->>API: updateApplicantDetail(payload[], clubId)
    API->>BE: PUT /api/club/{clubId}/applicant (JSON Array)
    BE-->>API: 200 OK { data }
    API-->>Hook: result.data
    Hook->>Cache: invalidate(['clubApplicants'])
    Cache-->>Page: 데이터 재패치
    Page-->>Admin: UI 갱신
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Assessment against linked issues

Objective Addressed Explanation
여러명의 지원서를 변경할 수 있다 (MOA-199)
상태변경 API 연동 (MOA-199)

Possibly related issues

Possibly related PRs

Suggested reviewers

  • oesnuj
  • seongwon030
  • Zepelown
  • PororoAndFriends

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ 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/#701-change-multiple-status-applicants-MOA-199

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 @coderabbit in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbit 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:
    • @coderabbit gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbit 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 @coderabbit help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbit ignore or @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbit summary or @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbit or @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 28, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
moadong Ready Ready Preview Comment Aug 28, 2025 0:24am

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: 7

🧹 Nitpick comments (9)
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts (1)

130-143: 드롭다운 컨텐트 스크롤/레이어 우선순위 보강.

옵션 수가 많을 때 뷰포트 밖으로 넘칠 수 있습니다. max-height/스크롤 및 z-index 상향을 권장합니다.

 export const StatusSelectMenu = styled.div<{ open: boolean }>`
   display: ${({ open }) => (open ? 'block' : 'none')};
   position: absolute;
   top: 100%;
   height: auto;
   background: #fff;
   left: 0;
   border: 1px solid #dcdcdc;
   border-radius: 6px;
-  box-shadow: 0px 1px 8px 0px #0000001f;
-  z-index: 10;
+  box-shadow: 0px 1px 8px 0px #0000001f;
+  z-index: 1000;
   padding: 8px 0;
   color: #787878;
+  min-width: 120px;
+  max-height: 320px;
+  overflow-y: auto;
 `
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx (1)

60-82: 디바운스 상수화 + 의존성 보강(+ 취소 처리 권장).

  • 매직넘버 400을 상수로 분리하세요.
  • mutate는 안정적이지만 의존성 배열에 포함하는 편이 안전합니다.
  • 디바운스 유틸이 cancel을 제공한다면 언마운트 시 취소를 권장합니다(페이지 전환 중 늦은 요청 방지).
-      }, 400),
-    [clubId, questionId],
+      }, DEBOUNCE_MS),
+    [clubId, questionId, updateApplicant],
   );

추가(파일 상단 인접 영역):

const DEBOUNCE_MS = 400;

선택(유틸이 지원한다면):

useEffect(() => () => updateApplicantDetail.cancel?.(), [updateApplicantDetail]);
frontend/src/apis/application/updateApplicantDetail.ts (1)

17-18: 요청 본문 변수명 동기화.

-        body: JSON.stringify(applicant),
+        body: JSON.stringify(updates),
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (3)

26-27: 요청 중 UI 비활성화(+작성자 질문 답변: 로딩 표시 권장).

배치 변경 요청 동안 버튼/삭제를 잠시 비활성화하면 중복 요청을 방지합니다. 필요 시 버튼 내부에 작은 스피너를 노출해도 좋습니다.

-  const { mutate: updateDetailApplicants } = useUpdateApplicant(clubId!);
+  const { mutate: updateDetailApplicants, isPending: isUpdatingStatus } =
+    useUpdateApplicant(clubId!);
-              <Styled.StatusSelect disabled={!isChecked}>
+              <Styled.StatusSelect disabled={!isChecked || isUpdatingStatus}>
                 상태변경
               </Styled.StatusSelect>
-              disabled={!isChecked}
+              disabled={!isChecked || isUpdatingStatus}

원하시면 인라인 스피너 컴포넌트 삽입 코드도 제공하겠습니다.

Also applies to: 237-237, 275-275


42-67: 바깥 클릭 핸들러 등록/해제 최적화 여지.

open/statusOpen에 따라 매 렌더마다 리스너를 재등록합니다. 한 번 등록하고 ref로 상태를 추적하거나, 포커스 아웃(onBlur/capture) 방식으로 단순화할 수 있습니다. 체감 이슈가 없다면 유지해도 무방합니다.


116-116: isAllChecked 계산 의미 명확화(가독성).

이미 prev에서 every(Boolean) 결과를 사용하므로 변수명을 willToggleAll 등으로 의미를 드러내면 읽기 쉬워집니다. 동작 문제는 없습니다.

frontend/src/hooks/queries/applicants/useUpdateApplicant.ts (3)

1-3: 타입 전용 임포트로 미세 최적화

UpdateApplicantParams는 타입이므로 import type 사용을 권장합니다.

-import { UpdateApplicantParams } from '@/types/applicants';
+import type { UpdateApplicantParams } from '@/types/applicants';

5-5: 훅 이름이 실제 동작(배치 업데이트)과 불일치

단건 → 배치로 변경되었으니 가독성을 위해 useUpdateApplicants(또는 useBulkUpdateApplicants)로의 리네이밍을 고려해 주세요. 시그니처는 그대로여도 의미 전달이 선명해집니다.

-export const useUpdateApplicant = (clubId: string) => {
+export const useUpdateApplicants = (clubId: string) => {

추가로, 제네릭을 지정해 반환/에러/변수 타입을 명시하면 사용처에서 이점이 큽니다.

-  return useMutation({
+  return useMutation<unknown, Error, UpdateApplicantParams[]>({

15-16: 에러 로깅 개선 및 안전한 출력

템플릿 리터럴로 unknown 에러를 문자열화하면 [object Object]가 될 수 있습니다. 원본 객체를 함께 로깅하세요.

-      console.log(`Error updating applicant detail: ${error}`);
+      console.error('Error updating applicant detail:', error);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6be7d37 and 4fdd93c.

📒 Files selected for processing (6)
  • frontend/src/apis/application/updateApplicantDetail.ts (1 hunks)
  • frontend/src/hooks/queries/applicants/useUpdateApplicant.ts (1 hunks)
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx (2 hunks)
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts (1 hunks)
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (9 hunks)
  • frontend/src/types/applicants.ts (2 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/types/applicants.ts
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx
  • frontend/src/hooks/queries/applicants/useUpdateApplicant.ts
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx
  • frontend/src/apis/application/updateApplicantDetail.ts
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/ApplicantDetailPage.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx
🧬 Code graph analysis (5)
frontend/src/types/applicants.ts (1)
frontend/src/types/application.ts (1)
  • AnswerItem (55-58)
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx (3)
frontend/src/context/AdminClubContext.tsx (1)
  • useAdminClubContext (30-37)
frontend/src/hooks/queries/application/useGetApplication.ts (1)
  • useGetApplication (4-11)
frontend/src/hooks/queries/applicants/useUpdateApplicant.ts (1)
  • useUpdateApplicant (5-18)
frontend/src/hooks/queries/applicants/useUpdateApplicant.ts (2)
frontend/src/types/applicants.ts (1)
  • UpdateApplicantParams (26-30)
frontend/src/apis/application/updateApplicantDetail.ts (1)
  • updateApplicantDetail (5-31)
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (2)
frontend/src/hooks/queries/applicants/useDeleteApplicants.ts (1)
  • useDeleteApplicants (4-17)
frontend/src/hooks/queries/applicants/useUpdateApplicant.ts (1)
  • useUpdateApplicant (5-18)
frontend/src/apis/application/updateApplicantDetail.ts (2)
frontend/src/types/applicants.ts (1)
  • UpdateApplicantParams (26-30)
frontend/src/apis/auth/secureFetch.ts (1)
  • secureFetch (3-41)
🔇 Additional comments (3)
frontend/src/types/applicants.ts (1)

26-30: UpdateApplicantParams 수정: applicantId 필수, memo 옵션 처리
타입을 아래와 같이 변경하고, 백엔드 스펙이 해당 변경을 지원하는지 확인 바랍니다.

-export interface UpdateApplicantParams {
-  memo: string;
-  status: ApplicationStatus;
-  applicantId: string | undefined;
-}
+export interface UpdateApplicantParams {
+  applicantId: string;
+  status: ApplicationStatus;
+  memo?: string;
+}
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx (1)

18-23: 상세 페이지 상태 옵션에 DECLINED 누락 — 목록 탭과 불일치.

ApplicantsTab.tsx에서는 불합격(DECLINED) 변경을 제공하지만 상세 페이지 드롭다운에는 없습니다. 의도된 제한인지 확인 바랍니다.

frontend/src/hooks/queries/applicants/useUpdateApplicant.ts (1)

17-18: useUpdateApplicant 호출부 인자 검토 완료
전체 코드베이스에 2개 인자 사용 호출 패턴이 존재하지 않음을 확인했습니다.

Comment on lines 5 to 9
export const updateApplicantDetail = async (
memo: string,
status: ApplicationStatus,
applicant: UpdateApplicantParams[],
clubId: string,
applicantId: string,
) => {
try {
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

⚠️ Potential issue

배치 업데이트 입력 검증 추가 + 파라미터 명확화.

빈 배열/누락된 applicantId를 조기에 차단하면 서버 4xx를 줄일 수 있습니다. 또한 변수명을 updates로 명확화하세요.

-export const updateApplicantDetail = async (
-  applicant: UpdateApplicantParams[],
+export const updateApplicantDetail = async (
+  updates: UpdateApplicantParams[],
   clubId: string,
 ) => {
   try {
+    if (!Array.isArray(updates) || updates.length === 0) {
+      return [];
+    }
+    if (updates.some((u) => !u.applicantId)) {
+      throw new Error('유효하지 않은 요청: 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.

Suggested change
export const updateApplicantDetail = async (
memo: string,
status: ApplicationStatus,
applicant: UpdateApplicantParams[],
clubId: string,
applicantId: string,
) => {
try {
export const updateApplicantDetail = async (
updates: UpdateApplicantParams[],
clubId: string,
) => {
try {
if (!Array.isArray(updates) || updates.length === 0) {
return [];
}
if (updates.some((u) => !u.applicantId)) {
throw new Error('유효하지 않은 요청: applicantId 누락');
}
// ...rest of implementation
🤖 Prompt for AI Agents
In frontend/src/apis/application/updateApplicantDetail.ts around lines 5 to 9,
the function accepts applicant: UpdateApplicantParams[] without validating
inputs and uses an unclear parameter name; rename the parameter to updates and
add early validation that throws or returns a 4xx-friendly error when updates is
missing or an empty array, and iterate the array to verify each entry contains a
non-empty applicantId (and any required fields) before making the request;
return or throw a clear validation error message so callers get immediate
feedback and server-side 4xx calls are reduced.

Comment on lines +9 to +10
mutationFn: (applicant: UpdateApplicantParams[]) =>
updateApplicantDetail(applicant, clubId!),
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

배치 페이로드 검증 및 널 단언 제거

  • 변수명을 복수형으로 변경해 의도를 명확히.
  • clubId!는 매개변수가 string이므로 불필요합니다.
  • UpdateApplicantParams.applicantIdstring | undefined이므로 런타임 검증이 필요합니다. 서버로 undefined가 전달되면 실패합니다.
-    mutationFn: (applicant: UpdateApplicantParams[]) =>
-      updateApplicantDetail(applicant, clubId!),
+    mutationFn: (applicants: UpdateApplicantParams[]) => {
+      if (applicants.length === 0) {
+        throw new Error('업데이트할 지원자가 없습니다.');
+      }
+      if (applicants.some(a => !a.applicantId)) {
+        throw new Error('applicantId가 없는 항목이 있습니다.');
+      }
+      return updateApplicantDetail(applicants, clubId);
+    },

또한, 디버깅 편의를 위해 mutationKey를 추가하는 것을 권장합니다.

   return useMutation({
+    mutationKey: ['updateApplicants', clubId],
📝 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
mutationFn: (applicant: UpdateApplicantParams[]) =>
updateApplicantDetail(applicant, clubId!),
return useMutation({
mutationKey: ['updateApplicants', clubId],
mutationFn: (applicants: UpdateApplicantParams[]) => {
if (applicants.length === 0) {
throw new Error('업데이트할 지원자가 없습니다.');
}
if (applicants.some(a => !a.applicantId)) {
throw new Error('applicantId가 없는 항목이 있습니다.');
}
return updateApplicantDetail(applicants, clubId);
},
// …other options (onSuccess, onError, etc.) remain unchanged
});
🤖 Prompt for AI Agents
In frontend/src/hooks/queries/applicants/useUpdateApplicant.ts around lines
9–10, the mutation currently uses a singular variable name, a non-null assertion
on clubId, and can send undefined applicantId to the server; rename the
parameter to a plural (e.g., applicants), remove the unnecessary clubId!
non-null assertion, validate the batch payload before calling
updateApplicantDetail by checking it's a non-empty array and that each item has
a defined applicantId (either filter out invalid entries or throw a clear
error), and add a descriptive mutationKey to the hook options for easier
debugging.

updateApplicantDetail(applicant, clubId!),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["clubApplicants"] });
queryClient.invalidateQueries({ queryKey: ['clubApplicants'] });
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

쿼리 무효화 범위 축소 필요 (clubId 키 포함 권장)

다중 클럽 환경에서 불필요한 광범위 무효화를 막기 위해 키에 clubId를 포함하세요. 기존 조회 훅의 키 형식과도 일치시켜 주세요.

-      queryClient.invalidateQueries({ queryKey: ['clubApplicants'] });
+      queryClient.invalidateQueries({ queryKey: ['clubApplicants', clubId] });

검증 스크립트: 현재 조회 훅이 어떤 키를 쓰는지 확인하여 키 형식을 맞춰 주세요.


🏁 Script executed:

#!/bin/bash
# 'clubApplicants' 관련 useQuery 키 사용 패턴 확인
rg -nP -C2 "(useQuery|useInfiniteQuery)\s*\(\s*\{[^\}]*queryKey\s*:\s*\[[^\]]*'clubApplicants'[^\]]*\]" --type ts --type tsx

Length of output: 160


🏁 Script executed:

#!/bin/bash
# 'clubApplicants' 사용 파일 목록 검색
rg -l "clubApplicants" -g "*.ts*"
# 'clubApplicants' 쿼리Key 패턴 확인
rg -nP -C2 "queryKey\s*:\s*\[[^]]*['\"]clubApplicants['\"]" -g "*.ts*"

Length of output: 1758


쿼리 무효화에 clubId 추가 필요

두 곳의 invalidateQueries 호출에서 clubApplicants만 사용하여 모든 클럽의 캐시를 무효화하고 있습니다. useGetApplicants 훅의 키와 동일하게 범위를 좁히려면 clubId를 함께 전달하세요.

  • frontend/src/hooks/queries/applicants/useUpdateApplicant.ts (12행)
  • frontend/src/hooks/queries/applicants/useDeleteApplicants.ts (11행)
-      queryClient.invalidateQueries({ queryKey: ['clubApplicants'] });
+      queryClient.invalidateQueries({ queryKey: ['clubApplicants', clubId] });
📝 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
queryClient.invalidateQueries({ queryKey: ['clubApplicants'] });
queryClient.invalidateQueries({ queryKey: ['clubApplicants', clubId] });
🤖 Prompt for AI Agents
In frontend/src/hooks/queries/applicants/useUpdateApplicant.ts around line 12,
the call to queryClient.invalidateQueries({ queryKey: ['clubApplicants'] }) is
too broad and should include the clubId to match useGetApplicants' key; update
the invalidateQueries call to pass the same key shape, e.g. ['clubApplicants',
clubId], and apply the same change in
frontend/src/hooks/queries/applicants/useDeleteApplicants.ts (around line 11) so
only the specific club's applicants cache is invalidated.

Comment on lines +106 to +128
export const StatusSelect = styled.p<{ disabled: boolean }>`
height: 30px;
border: 1px solid #dcdcdc;
background: #fff;
border-radius: 55px;
padding: 0px 22px 0px 8px;
margin: 5px 0px 5px 0px;
font-weight: 700;
color: ${({ disabled }) => (disabled ? '#DCDCDC' : '#000')};
padding: 0 22px 0 8px;
margin: 5px 0;

-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
${({ disabled }) =>
disabled
? 'color: #DCDCDC'
: 'color: #000; &:hover { background: #f5f5f5; }'};

&:not(:disabled):hover {
font-size: 12px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: default;

display: inline-flex;
align-items: center;
`;
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

StatusSelect는 인터랙션 요소입니다. p → button으로 전환하고 접근성/포커스/비활성 처리를 명시하세요.

현재 styled.p에 클릭 동작을 부여하는 패턴은 접근성/키보드 내비게이션에 취약합니다. button으로 전환하고 disabled 시 pointer-events 차단, focus-visible 스타일을 추가하는 편이 안전합니다. ApplicantsTab.tsx에서 onClick을 래퍼가 아닌 버튼에 부여하는 변경과 세트로 적용해 주세요.

-export const StatusSelect = styled.p<{ disabled: boolean }>`
+export const StatusSelect = styled.button<{ disabled: boolean }>`
   height: 30px;
   border: 1px solid #dcdcdc;
   background: #fff;
   border-radius: 55px;
   padding: 0 22px 0 8px;
   margin: 5px 0;
-  ${({ disabled }) =>
-    disabled
-      ? 'color: #DCDCDC'
-      : 'color: #000; &:hover { background: #f5f5f5; }'};
+  ${({ disabled }) =>
+    disabled
+      ? `
+        color: #DCDCDC;
+        pointer-events: none;
+      `
+      : `
+        color: #000;
+        &:hover { background: #f5f5f5; }
+        &:focus-visible { outline: 2px solid #99c2ff; outline-offset: 2px; }
+      `};
   font-size: 12px;
   font-weight: 600;
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
-  cursor: default;
+  cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
   display: inline-flex;
   align-items: center;
 `
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const StatusSelect = styled.p<{ disabled: boolean }>`
height: 30px;
border: 1px solid #dcdcdc;
background: #fff;
border-radius: 55px;
padding: 0px 22px 0px 8px;
margin: 5px 0px 5px 0px;
font-weight: 700;
color: ${({ disabled }) => (disabled ? '#DCDCDC' : '#000')};
padding: 0 22px 0 8px;
margin: 5px 0;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
${({ disabled }) =>
disabled
? 'color: #DCDCDC'
: 'color: #000; &:hover { background: #f5f5f5; }'};
&:not(:disabled):hover {
font-size: 12px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: default;
display: inline-flex;
align-items: center;
`;
export const StatusSelect = styled.button<{ disabled: boolean }>`
height: 30px;
border: 1px solid #dcdcdc;
background: #fff;
border-radius: 55px;
padding: 0 22px 0 8px;
margin: 5px 0;
${({ disabled }) =>
disabled
? `
color: #DCDCDC;
pointer-events: none;
`
: `
color: #000;
&:hover { background: #f5f5f5; }
&:focus-visible { outline: 2px solid #99c2ff; outline-offset: 2px; }
`};
font-size: 12px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: inline-flex;
align-items: center;
`
🤖 Prompt for AI Agents
In frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts
around lines 106 to 128, the StatusSelect is currently a styled.p used as an
interactive control; change it to a styled.button, ensure it accepts and applies
the disabled prop properly (use disabled attribute instead of custom prop where
possible), add CSS to disable pointer-events and set aria-disabled/opacity for
disabled state, and add visible focus styles (focus-visible outline/box-shadow)
for keyboard users; also update ApplicantsTab.tsx so onClick is attached
directly to this button (not a wrapper) and ensure the component renders with
role/button semantics and proper disabled handling.

Comment on lines +145 to 154
export const StatusSelectMenuItem = styled.div`
font-size: 12px;
font-weight: 600;
padding: 8px 13px;
cursor: pointer;
text-align: left;
&:hover {
background: #f5f5f5;
}
`;
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

메뉴 항목도 div → button으로 전환해 키보드 접근성을 확보하세요.

role/button semantics 없이 div 클릭은 스크린리더/키보드 접근성이 떨어집니다.

-export const StatusSelectMenuItem = styled.div`
+export const StatusSelectMenuItem = styled.button`
   font-size: 12px;
   font-weight: 600;
   padding: 8px 13px;
   cursor: pointer;
   text-align: left;
+  width: 100%;
+  border: 0;
+  background: transparent;
   &:hover {
     background: #f5f5f5;
   }
+  &:focus-visible {
+    outline: 2px solid #99c2ff;
+    outline-offset: 2px;
+  }
 `
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const StatusSelectMenuItem = styled.div`
font-size: 12px;
font-weight: 600;
padding: 8px 13px;
cursor: pointer;
text-align: left;
&:hover {
background: #f5f5f5;
}
`;
export const StatusSelectMenuItem = styled.button`
font-size: 12px;
font-weight: 600;
padding: 8px 13px;
cursor: pointer;
text-align: left;
width: 100%;
border: 0;
background: transparent;
&:hover {
background: #f5f5f5;
}
&:focus-visible {
outline: 2px solid #99c2ff;
outline-offset: 2px;
}
`;
🤖 Prompt for AI Agents
In frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts
around lines 145 to 154, the StatusSelectMenuItem is a div which harms keyboard
and screen-reader accessibility; change it to a styled button element, add
type="button" where it’s rendered, reset default browser button styles
(background, border, padding) to preserve current visual styling, ensure cursor
and hover styles remain, and add visible focus styles (outline or box-shadow)
and appropriate ARIA attributes if this item functions as a selectable menu
option.

Comment on lines +230 to 239
<Styled.SelectWrapper
ref={statusSelectRef}
onClick={() => {
if (!isChecked) return;
setStatusOpen((prev) => !prev);
}}
>
<Styled.StatusSelect disabled={!isChecked}>
상태변경
</Styled.StatusSelect>
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

클릭 핸들러를 래퍼에서 버튼으로 이동하고 ARIA를 부여하세요.

래퍼 div에 onClick을 두면 스크린리더/키보드 접근성이 떨어집니다. 버튼에 aria-haspopup/aria-expanded를 명시하고 메뉴/항목에 role을 부여하세요. styles에서 button 전환과 함께 적용하세요.

-            <Styled.SelectWrapper
-              ref={statusSelectRef}
-              onClick={() => {
-                if (!isChecked) return;
-                setStatusOpen((prev) => !prev);
-              }}
-            >
-              <Styled.StatusSelect disabled={!isChecked}>
+            <Styled.SelectWrapper ref={statusSelectRef}>
+              <Styled.StatusSelect
+                disabled={!isChecked || isUpdatingStatus}
+                aria-haspopup="menu"
+                aria-expanded={statusOpen}
+                type="button"
+                onClick={() => setStatusOpen((prev) => !prev)}
+              >
                 상태변경
               </Styled.StatusSelect>
               <Styled.Arrow width={8} height={8} src={selectIcon} />
-              <Styled.StatusSelectMenu open={statusOpen}>
-                <Styled.StatusSelectMenuItem
+              <Styled.StatusSelectMenu open={statusOpen} role="menu">
+                <Styled.StatusSelectMenuItem
+                  role="menuitem"
                   onClick={() => {
                     updateAllApplicants(ApplicationStatus.SUBMITTED);
                   }}
                 >
                   서류검토
                 </Styled.StatusSelectMenuItem>
-                <Styled.StatusSelectMenuItem
+                <Styled.StatusSelectMenuItem
+                  role="menuitem"
                   onClick={() => {
                     updateAllApplicants(
                       ApplicationStatus.INTERVIEW_SCHEDULED,
                     );
                   }}
                 >
                   면접예정
                 </Styled.StatusSelectMenuItem>
-                <Styled.StatusSelectMenuItem
+                <Styled.StatusSelectMenuItem
+                  role="menuitem"
                   onClick={() => {
                     updateAllApplicants(ApplicationStatus.ACCEPTED);
                   }}
                 >
                   합격
                 </Styled.StatusSelectMenuItem>
-                <Styled.StatusSelectMenuItem
+                <Styled.StatusSelectMenuItem
+                  role="menuitem"
                   onClick={() => {
                     updateAllApplicants(ApplicationStatus.DECLINED);
                   }}
                 >
                   불합격
                 </Styled.StatusSelectMenuItem>
               </Styled.StatusSelectMenu>
             </Styled.SelectWrapper>

Also applies to: 241-269

🤖 Prompt for AI Agents
In frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx around
lines 230-239 (also apply same changes to 241-269), move the click handler from
the wrapping Styled.SelectWrapper div to an actual button element, update styles
to render the button visually identical to the current wrapper, and add proper
ARIA attributes: aria-haspopup="menu" and aria-expanded={statusOpen} on the
button; ensure the menu and each option have appropriate roles (e.g.,
role="menu" and role="menuitem") and keyboard handlers (Enter/Space to
toggle/open and arrow/escape support as needed); remove onClick from the wrapper
and ensure the button is disabled when !isChecked so it is keyboard and
screen-reader accessible.

Copy link
Collaborator

@suhyun113 suhyun113 left a comment

Choose a reason for hiding this comment

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

수고하셨으여~~~~~~

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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (4)

30-40: 검색 필터에서 answers[0] 가정으로 인한 런타임 크래시 가능성

answers가 비어있거나 value가 없는 경우 NPE 발생 여지.

다음과 같이 옵셔널 체이닝/기본값으로 방어하세요.

-  return applicantsData.applicants.filter((user: Applicant) =>
-    user.answers[0].value
-      .toLowerCase()
-      .includes(keyword.trim().toLowerCase()),
-  );
+  return applicantsData.applicants.filter((user: Applicant) => {
+    const name = user.answers?.[0]?.value ?? '';
+    return name.toLowerCase().includes(keyword.trim().toLowerCase());
+  });

299-303: 체크박스는 onChange 사용 권장

폼 컨트롤에는 onChange가 표준이며 보조기기 호환성이 더 높습니다.

-                    onClick={(e: React.MouseEvent<HTMLInputElement>) => {
+                    onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
                       e.stopPropagation();
                       selectApplicantsByStatus('all');
                     }}

367-372: 리스트 key에 index 사용 지양 → id 사용

리렌더링/필터링 시 의도치 않은 재마운트 방지.

-              <Styled.ApplicantTableRow
-                key={index}
+              <Styled.ApplicantTableRow
+                key={item.id}
                 onClick={() => navigate(`/admin/applicants/${item.id}`)}
                 style={{ cursor: 'pointer' }}
               >

375-384: 개별 행 체크박스도 onChange로 전환

일관성과 접근성 향상.

-                  <Styled.ApplicantTableCheckbox
+                  <Styled.ApplicantTableCheckbox
                     checked={checkedItem.get(item.id)}
-                    onClick={(e: React.MouseEvent<HTMLInputElement>) => {
+                    onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
                       e.stopPropagation();
                       setCheckedItem((prev) => {
                         const newMap = new Map(prev);
                         newMap.set(item.id, !newMap.get(item.id));
                         return newMap;
                       });
                     }}
                   />
♻️ Duplicate comments (2)
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (2)

148-166: mutate 옵션(thisArg) 전달 버그가 해결되었습니다.

옵션 객체가 map의 thisArg로 가는 문제는 더 이상 없고, onSuccess/onError가 정상 동작합니다. 굿 픽스.


228-269: 상태 변경 트리거를 wrapper div에서 button으로 이동 + ARIA 롤/키보드 지원 추가

현재 onClick이 래퍼 div에 있어 접근성이 떨어집니다. 버튼에 포커스/키보드 연동과 aria 속성을 부여하세요. 또한 업데이트 중 비활성화/aria-busy 적용을 권장합니다.

-            <Styled.SelectWrapper
-              ref={statusSelectRef}
-              onClick={() => {
-                if (!isChecked) return;
-                setStatusOpen((prev) => !prev);
-              }}
-            >
-              <Styled.StatusSelect disabled={!isChecked}>
+            <Styled.SelectWrapper ref={statusSelectRef}>
+              <Styled.StatusSelect
+                type="button"
+                disabled={!isChecked || isUpdatingStatus}
+                aria-haspopup="menu"
+                aria-expanded={statusOpen}
+                aria-busy={isUpdatingStatus}
+                onClick={() => setStatusOpen((prev) => !prev)}
+              >
                 상태변경
               </Styled.StatusSelect>
               <Styled.Arrow width={8} height={8} src={selectIcon} />
-              <Styled.StatusSelectMenu open={statusOpen}>
-                <Styled.StatusSelectMenuItem
-                  onClick={() => {
+              <Styled.StatusSelectMenu open={statusOpen} role="menu">
+                <Styled.StatusSelectMenuItem role="menuitem"
+                  onClick={() => {
                     updateAllApplicants(ApplicationStatus.SUBMITTED);
                   }}
                 >
                   서류검토
                 </Styled.StatusSelectMenuItem>
-                <Styled.StatusSelectMenuItem
-                  onClick={() => {
+                <Styled.StatusSelectMenuItem role="menuitem"
+                  onClick={() => {
                     updateAllApplicants(ApplicationStatus.INTERVIEW_SCHEDULED);
                   }}
                 >
                   면접예정
                 </Styled.StatusSelectMenuItem>
-                <Styled.StatusSelectMenuItem
-                  onClick={() => {
+                <Styled.StatusSelectMenuItem role="menuitem"
+                  onClick={() => {
                     updateAllApplicants(ApplicationStatus.ACCEPTED);
                   }}
                 >
                   합격
                 </Styled.StatusSelectMenuItem>
-                <Styled.StatusSelectMenuItem
-                  onClick={() => {
+                <Styled.StatusSelectMenuItem role="menuitem"
+                  onClick={() => {
                     updateAllApplicants(ApplicationStatus.DECLINED);
                   }}
                 >
                   불합격
                 </Styled.StatusSelectMenuItem>
               </Styled.StatusSelectMenu>
             </Styled.SelectWrapper>

키보드 핸들러(Enter/Space, Escape, Arrow)도 후속으로 추가하세요.

🧹 Nitpick comments (6)
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (6)

42-67: 외부 클릭 리스너: pointer 이벤트와 1회 바인딩 고려

모바일/펜 입력 대응성과 불필요한 바인딩/언바인딩을 줄이기 위해 pointerdown 사용 및 의존성 축소를 권장합니다.

예시:

-  useEffect(() => {
+  useEffect(() => {
     const handleOutsideClick = (e: MouseEvent) => {
       const target = e.target as Node;
@@
-    document.addEventListener('mousedown', handleOutsideClick);
+    document.addEventListener('pointerdown', handleOutsideClick as any, { passive: true });
     return () => {
-      document.removeEventListener('mousedown', handleOutsideClick);
+      document.removeEventListener('pointerdown', handleOutsideClick as any);
     };
-  }, [open, statusOpen]);
+  }, [open, statusOpen]);

참고: 상태 의존성을 없애고 ref.current와 setState만 쓰는 방식으로 고정 리스너 1회 등록도 가능합니다.


68-75: 필터 변경 시 선택 초기화 UX 확인 필요

검색어 변경으로 filteredApplicants가 바뀔 때 선택이 전부 초기화됩니다. 의도라면 OK, 아니라면 원본 선택 상태를 유지하는 구조(예: 전역 selection Map) 검토 필요.

원하시면 선택 상태를 뷰(필터)와 분리하는 리팩터를 제안하겠습니다.


150-156: 선택된 대상 집합 축소를 위해 filteredApplicants 사용 권장

전체 applicants를 순회할 필요 없이 표시/관리 중인 집합만 대상으로 충분합니다.

-      applicantsData!.applicants
+      filteredApplicants
         .filter((applicant) => checkedItem.get(applicant.id))
         .map((applicant) => ({
           applicantId: applicant.id,
           memo: applicant.memo,
-          status: status,
+          status,
         })),

또한 memo가 undefined일 수 있다면 memo: applicant.memo ?? ''로 방어하세요.

UpdateApplicantParams.memo가 필수 문자열인지 확인 바랍니다.


271-278: 삭제 버튼도 삭제 진행 중 비활성화 및 접근성 속성 부여

중복 클릭 방지 및 SR 노출 개선.

-            <Styled.DeleteButton
+            <Styled.DeleteButton
               src={deleteIcon}
               alt='삭제'
-              disabled={!isChecked}
+              disabled={!isChecked || isDeleting}
+              aria-busy={isDeleting}
               onClick={() => {
                 const toBeDeleted = Array.from(checkedItem.entries())
                   .filter(([_, isChecked]) => isChecked)
                   .map(([id, _]) => id);

(상단 코멘트의 isDeleting 구조분해가 전제됩니다.)


407-419: 날짜 포맷 간소화 및 매직 넘버 제거

IIFE/패딩 매직 넘버 대신 ISO 슬라이스로 단순화.

-                    (() => {
-                      const date = new Date(item.createdAt);
-                      const year = date.getFullYear();
-                      const month = String(date.getMonth() + 1).padStart(
-                        2,
-                        '0',
-                      );
-                      const day = String(date.getDate()).padStart(2, '0');
-                      return `${year}-${month}-${day}`;
-                    })()
+                    (() => new Date(item.createdAt).toISOString().slice(0, 10))()

타임존 보정이 필요하면 dayjs/date-fns 등 공용 유틸로 대체를 권장합니다.


148-166: 로딩 표시(Spinner) 제안에 대한 답변

예, 권장합니다. StatusSelect 내부 우측에 인라인 스피너를 배치하거나 메뉴 항목 클릭 시 로딩 오버레이를 표시하세요. 위 코멘트의 isUpdatingStatus를 사용해 제어하면 됩니다.

원하시면 공용 Spinner 컴포넌트 연동용 패치도 드리겠습니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4fdd93c and be30f26.

📒 Files selected for processing (1)
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (9 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/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/ApplicantsTab.tsx
🧬 Code graph analysis (1)
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (2)
frontend/src/hooks/queries/applicants/useDeleteApplicants.ts (1)
  • useDeleteApplicants (4-17)
frontend/src/hooks/queries/applicants/useUpdateApplicant.ts (1)
  • useUpdateApplicant (5-18)
🔇 Additional comments (4)
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (4)

3-3: 불필요 코멘트 없음 — 적절한 React 훅 임포트

현재 사용 범위에 맞는 임포트입니다.


23-29: 상태 플래그와 ref 추가 적절

statusOpen/isChecked 및 외부 클릭 감지를 위한 refs 추가는 목적에 부합합니다.


76-81: 선택 상태 파생 계산 LGTM

selectAll/isChecked 계산이 명확합니다.


107-136: isAllChecked 파생 값 계산 적절

토글 전/후 일관성 확보에 기여합니다.

import selectIcon from '@/assets/images/icons/selectArrow.svg';
import deleteIcon from '@/assets/images/icons/applicant_delete.svg';
import selectAllIcon from '@/assets/images/icons/applicant_select_arrow.svg';
import { useUpdateApplicant } from '@/hooks/queries/applicants/useUpdateApplicant';
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

상태 변경 중 UI 비활성화/스피너 연계를 위해 mutation 로딩 상태를 노출하세요.

로직 중복 클릭 방지와 접근성(aria-busy) 강화를 위해 isPending(or isLoading)을 구조분해 해 사용 권장.

다음 패치를 적용하세요(React Query v5 기준 isPending, v4면 isLoading으로 교체 필요).

-const { mutate: deleteApplicants } = useDeleteApplicants(clubId!);
-const { mutate: updateDetailApplicants } = useUpdateApplicant(clubId!);
+const { mutate: deleteApplicants, isPending: isDeleting } = useDeleteApplicants(clubId!);
+const { mutate: updateDetailApplicants, isPending: isUpdatingStatus } = useUpdateApplicant(clubId!);

React Query 버전(v4/v5) 확인 후 isPending/isLoading 네이밍을 맞춰주세요.

Also applies to: 25-26


🏁 Script executed:

#!/bin/bash
# React Query 버전 확인 (v4/v5 여부 결정)
grep -E '"(@tanstack/react-query|react-query)":' -n package.json

Length of output: 112


🏁 Script executed:

#!/bin/bash
# package.json 위치 검색 및 React Query 버전 추출
pkg_file=$(find . -maxdepth 3 -type f -name "package.json" | head -n1)
if [ -z "$pkg_file" ]; then
  echo "package.json 파일을 찾을 수 없습니다."
  exit 1
fi
grep -E '"(@tanstack/react-query|react-query)"' -n "$pkg_file"

Length of output: 252


React Query v5 isPending 구조분해 사용
변경 중 스피너/aria-busy 연계를 위해 mutate 호출부에서 isPending을 구조분해하여 사용하세요.

- const { mutate: deleteApplicants } = useDeleteApplicants(clubId!);
- const { mutate: updateDetailApplicants } = useUpdateApplicant(clubId!);
+ const { mutate: deleteApplicants, isPending: isDeleting } = useDeleteApplicants(clubId!);
+ const { mutate: updateDetailApplicants, isPending: isUpdatingStatus } = useUpdateApplicant(clubId!);

위치: frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (12, 25–26)

📝 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 { useUpdateApplicant } from '@/hooks/queries/applicants/useUpdateApplicant';
// at the top of ApplicantsTab.tsx
import { useDeleteApplicants } from '@/hooks/queries/applicants/useDeleteApplicants';
import { useUpdateApplicant } from '@/hooks/queries/applicants/useUpdateApplicant';
const {
mutate: deleteApplicants,
isPending: isDeleting
} = useDeleteApplicants(clubId!);
const {
mutate: updateDetailApplicants,
isPending: isUpdatingStatus
} = useUpdateApplicant(clubId!);
🤖 Prompt for AI Agents
In frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx around
lines 12 and 25–26, the React Query v5 isPending flag from useUpdateApplicant
should be destructured and used to drive the UI during mutation; change the hook
usage to const { mutate, isPending } = useUpdateApplicant() (or equivalent) and
then use isPending at the mutate call site to control the spinner/aria-busy
state (replace any current boolean or indirect loading checks with isPending so
the spinner and aria-busy reflect the actual mutation pending state).

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.

리뷰 달았어용 고생하셨습니다~

@lepitaaar lepitaaar merged commit a138c7e into develop-fe Sep 1, 2025
5 checks passed
Copy link
Member

@oesnuj oesnuj left a comment

Choose a reason for hiding this comment

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

수고하셨습니다~~

Comment on lines +239 to +268
<Styled.StatusSelectMenu open={statusOpen}>
<Styled.StatusSelectMenuItem
onClick={() => {
updateAllApplicants(ApplicationStatus.SUBMITTED);
}}
>
서류검토
</Styled.StatusSelectMenuItem>
<Styled.StatusSelectMenuItem
onClick={() => {
updateAllApplicants(ApplicationStatus.INTERVIEW_SCHEDULED);
}}
>
면접예정
</Styled.StatusSelectMenuItem>
<Styled.StatusSelectMenuItem
onClick={() => {
updateAllApplicants(ApplicationStatus.ACCEPTED);
}}
>
합격
</Styled.StatusSelectMenuItem>
<Styled.StatusSelectMenuItem
onClick={() => {
updateAllApplicants(ApplicationStatus.DECLINED);
}}
>
불합격
</Styled.StatusSelectMenuItem>
</Styled.StatusSelectMenu>
Copy link
Member

Choose a reason for hiding this comment

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

상태 항목들이 중복되어 코드가 길어지는 느낌이에용
상태 옵션을 상수로 분리해두고 map 렌더링 하는 방식은 어떤가용?
한번 같이 생각해봐요

@coderabbitai coderabbitai bot mentioned this pull request Sep 11, 2025
@lepitaaar lepitaaar deleted the feature/#701-change-multiple-status-applicants-MOA-199 branch September 20, 2025 01:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📬 API 서버 API 통신 작업 💻 FE Frontend ✨ Feature 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

Comments