From c625679fa1bb17dba83728e1e2f04d812267bf30 Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Tue, 26 Aug 2025 23:18:40 +0900 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20=EC=9D=B4=EC=A0=84=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EB=A5=BC=20=EB=B3=B4=EC=9E=A5=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx index 11cfe7ed2..537ce6992 100644 --- a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx +++ b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx @@ -80,7 +80,7 @@ const ApplicantsTab = () => { setCheckedItem((prev) => { const newMap = new Map(prev); - const isAllChecked = Array.from(checkedItem.values()).every(Boolean); + const isAllChecked = Array.from(prev.values()).every(Boolean); if (mode === 'all') { newMap.forEach((_, key) => newMap.set(key, !isAllChecked)); From d3815ea9e0db1a09ecbe961dd44d7b9a8d4733f7 Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Thu, 28 Aug 2025 13:31:57 +0900 Subject: [PATCH 2/5] refactor: update applicant detail api --- .../apis/application/updateApplicantDetail.ts | 13 ++++-------- .../queries/applicants/useUpdateApplicant.ts | 20 +++++++++---------- frontend/src/types/applicants.ts | 16 ++++++++++----- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/frontend/src/apis/application/updateApplicantDetail.ts b/frontend/src/apis/application/updateApplicantDetail.ts index 4e99bec42..f1f9926c8 100644 --- a/frontend/src/apis/application/updateApplicantDetail.ts +++ b/frontend/src/apis/application/updateApplicantDetail.ts @@ -1,25 +1,20 @@ import API_BASE_URL from '@/constants/api'; import { secureFetch } from '@/apis/auth/secureFetch'; -import { ApplicationStatus } from '@/types/applicants'; +import { UpdateApplicantParams } from '@/types/applicants'; export const updateApplicantDetail = async ( - memo: string, - status: ApplicationStatus, + applicant: UpdateApplicantParams[], clubId: string, - applicantId: string, ) => { try { const response = await secureFetch( - `${API_BASE_URL}/api/club/${clubId}/apply/${applicantId}`, + `${API_BASE_URL}/api/club/${clubId}/applicant`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ - memo, - status - }) + body: JSON.stringify(applicant), }, ); diff --git a/frontend/src/hooks/queries/applicants/useUpdateApplicant.ts b/frontend/src/hooks/queries/applicants/useUpdateApplicant.ts index bf6d34ad9..f053734c2 100644 --- a/frontend/src/hooks/queries/applicants/useUpdateApplicant.ts +++ b/frontend/src/hooks/queries/applicants/useUpdateApplicant.ts @@ -1,18 +1,18 @@ -import { updateApplicantDetail } from "@/apis/application/updateApplicantDetail"; -import { ApplicationStatus } from "@/types/applicants"; -import { useMutation, useQueryClient } from "@tanstack/react-query" +import { updateApplicantDetail } from '@/apis/application/updateApplicantDetail'; +import { UpdateApplicantParams } from '@/types/applicants'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; -export const useUpdateApplicant = (clubId: string, applicantId: string) => { +export const useUpdateApplicant = (clubId: string) => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: ({memo, status}: { memo: string, status: ApplicationStatus }) => - updateApplicantDetail(memo, status, clubId, applicantId), + mutationFn: (applicant: UpdateApplicantParams[]) => + updateApplicantDetail(applicant, clubId!), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["clubApplicants"] }); + queryClient.invalidateQueries({ queryKey: ['clubApplicants'] }); }, onError: (error) => { console.log(`Error updating applicant detail: ${error}`); - } - }) -} \ No newline at end of file + }, + }); +}; diff --git a/frontend/src/types/applicants.ts b/frontend/src/types/applicants.ts index 7d4d6e6a5..8850420e8 100644 --- a/frontend/src/types/applicants.ts +++ b/frontend/src/types/applicants.ts @@ -1,4 +1,4 @@ -import { AnswerItem } from "./application"; +import { AnswerItem } from './application'; export enum ApplicationStatus { SUBMITTED = 'SUBMITTED', // 제출 완료 @@ -11,14 +11,20 @@ export interface ApplicantsInfo { total: number; reviewRequired: number; scheduledInterview: number; - accepted: number; - applicants: Applicant[] + accepted: number; + applicants: Applicant[]; } export interface Applicant { id: string; status: ApplicationStatus; - answers: AnswerItem[] + answers: AnswerItem[]; memo: string; createdAt: string; -} \ No newline at end of file +} + +export interface UpdateApplicantParams { + memo: string; + status: ApplicationStatus; + applicantId: string | undefined; +} From 55bd9552f6f1d08843d0353d7cf8e53171ffab07 Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Thu, 28 Aug 2025 19:23:43 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=EC=BB=A4=EC=8A=A4=ED=85=80=20?= =?UTF-8?q?=EB=93=9C=EB=A1=AD=EB=A9=94=EB=89=B4=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=ED=85=9C=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ApplicantsTab/ApplicantsTab.styles.ts | 49 +++++++++++++++---- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts index e29efd8a2..344fc5de6 100644 --- a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts +++ b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts @@ -103,21 +103,52 @@ export const VerticalLine = styled.div` margin: 8px 4px; `; -export const StatusSelect = styled.select` +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 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; + padding: 8px 0; + color: #787878; +`; + +export const StatusSelectMenuItem = styled.div` + font-size: 12px; + font-weight: 600; + padding: 8px 13px; + cursor: pointer; + text-align: left; + &:hover { background: #f5f5f5; } `; From 4fdd93c738adf74a2c6772bd054a49c2cf7ead1d Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Thu, 28 Aug 2025 19:24:01 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=EC=97=AC=EB=9F=AC=EB=AA=85=20?= =?UTF-8?q?=EC=A7=80=EC=9B=90=EC=83=81=ED=83=9C=20=EB=B3=80=EA=B2=BD=20api?= =?UTF-8?q?=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ApplicantDetailPage.tsx | 23 ++-- .../tabs/ApplicantsTab/ApplicantsTab.tsx | 106 ++++++++++++++++-- 2 files changed, 112 insertions(+), 17 deletions(-) diff --git a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx index 0841beae2..8ee8b776e 100644 --- a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx +++ b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx @@ -38,11 +38,13 @@ const ApplicantDetailPage = () => { const { questionId } = useParams<{ questionId: string }>(); const navigate = useNavigate(); const [applicantMemo, setAppMemo] = useState(''); - const [applicantStatus, setApplicantStatus] = useState(ApplicationStatus.SUBMITTED); + const [applicantStatus, setApplicantStatus] = useState( + ApplicationStatus.SUBMITTED, + ); const { applicantsData, clubId } = useAdminClubContext(); const { data: formData, isLoading, isError } = useGetApplication(clubId!); - const { mutate: updateApplicant } = useUpdateApplicant(clubId!, questionId!); + const { mutate: updateApplicant } = useUpdateApplicant(clubId!); const applicantIndex = applicantsData?.applicants.findIndex((a) => a.id === questionId) ?? -1; @@ -58,20 +60,23 @@ const ApplicantDetailPage = () => { const updateApplicantDetail = useMemo( () => debounce((memo, status) => { - function isApplicationStatus(v: unknown): v is ApplicationStatus { - return typeof v === 'string' && Object.values(ApplicationStatus).includes(v as ApplicationStatus); + return ( + typeof v === 'string' && + Object.values(ApplicationStatus).includes(v as ApplicationStatus) + ); } if (typeof memo !== 'string') return; if (!isApplicationStatus(status)) return; - updateApplicant( + updateApplicant([ { - memo, - status - } - ); + memo, + status, + applicantId: questionId, + }, + ]); }, 400), [clubId, questionId], ); diff --git a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx index 537ce6992..198427a7f 100644 --- a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx +++ b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx @@ -1,6 +1,6 @@ import { useAdminClubContext } from '@/context/AdminClubContext'; import { Applicant, ApplicationStatus } from '@/types/applicants'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import * as Styled from './ApplicantsTab.styles'; import { useNavigate } from 'react-router-dom'; import { useDeleteApplicants } from '@/hooks/queries/applicants/useDeleteApplicants'; @@ -9,6 +9,7 @@ import mapStatusToGroup from '@/utils/mapStatusToGroup'; 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'; const ApplicantsTab = () => { const navigate = useNavigate(); @@ -19,7 +20,12 @@ const ApplicantsTab = () => { ); const [selectAll, setSelectAll] = useState(false); const [open, setOpen] = useState(false); + const [statusOpen, setStatusOpen] = useState(false); + const [isChecked, setIsChecked] = useState(false); const { mutate: deleteApplicants } = useDeleteApplicants(clubId!); + const { mutate: updateDetailApplicants } = useUpdateApplicant(clubId!); + const allSelectRef = useRef(null); + const statusSelectRef = useRef(null); const filteredApplicants = useMemo(() => { if (!applicantsData?.applicants) return []; @@ -33,6 +39,32 @@ const ApplicantsTab = () => { ); }, [applicantsData, keyword]); + 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]); + useEffect(() => { const newMap = new Map(); filteredApplicants.forEach((user: Applicant) => { @@ -45,6 +77,7 @@ const ApplicantsTab = () => { const all = checkedItem.size > 0 && Array.from(checkedItem.values()).every(Boolean); setSelectAll(all); + setIsChecked(Array.from(checkedItem.values()).some(Boolean)); }, [checkedItem]); if (!clubId) return null; @@ -112,6 +145,29 @@ const ApplicantsTab = () => { }); }; + const updateAllApplicants = (status: ApplicationStatus) => { + updateDetailApplicants( + applicantsData!.applicants + .filter((applicant) => checkedItem.get(applicant.id)) + .map( + (applicant) => ({ + applicantId: applicant.id, + memo: applicant.memo, + status: status, + }), + { + onSuccess: () => { + checkoutAllApplicants(); + setStatusOpen(false); + }, + onError: () => { + alert('지원자 상태 변경에 실패했습니다. 다시 시도해주세요.'); + }, + }, + ), + ); + }; + return ( <> @@ -171,18 +227,52 @@ const ApplicantsTab = () => { - - - + { + if (!isChecked) return; + setStatusOpen((prev) => !prev); + }} + > + + 상태변경 + + { + updateAllApplicants(ApplicationStatus.SUBMITTED); + }} + > + 서류검토 + + { + updateAllApplicants(ApplicationStatus.INTERVIEW_SCHEDULED); + }} + > + 면접예정 + + { + updateAllApplicants(ApplicationStatus.ACCEPTED); + }} + > + 합격 + + { + updateAllApplicants(ApplicationStatus.DECLINED); + }} + > + 불합격 + + { const toBeDeleted = Array.from(checkedItem.entries()) .filter(([_, isChecked]) => isChecked) @@ -205,7 +295,7 @@ const ApplicantsTab = () => { - + ) => { From be30f2629f6b642a29db56dd8e1869ea72596f55 Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Thu, 28 Aug 2025 21:23:18 +0900 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20mutate=EC=9D=98=20=EC=98=B5=EC=85=98?= =?UTF-8?q?=20=EA=B0=9D=EC=B2=B4=EA=B0=80=20=EC=A0=84=EB=8B=AC=EB=90=98?= =?UTF-8?q?=EC=A7=80=EC=95=8A=EB=8A=94=20=EB=AC=B8=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tabs/ApplicantsTab/ApplicantsTab.tsx | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx index 198427a7f..347c59736 100644 --- a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx +++ b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx @@ -149,22 +149,20 @@ const ApplicantsTab = () => { updateDetailApplicants( applicantsData!.applicants .filter((applicant) => checkedItem.get(applicant.id)) - .map( - (applicant) => ({ - applicantId: applicant.id, - memo: applicant.memo, - status: status, - }), - { - onSuccess: () => { - checkoutAllApplicants(); - setStatusOpen(false); - }, - onError: () => { - alert('지원자 상태 변경에 실패했습니다. 다시 시도해주세요.'); - }, - }, - ), + .map((applicant) => ({ + applicantId: applicant.id, + memo: applicant.memo, + status: status, + })), + { + onSuccess: () => { + checkoutAllApplicants(); + setStatusOpen(false); + }, + onError: () => { + alert('지원자 상태 변경에 실패했습니다. 다시 시도해주세요.'); + }, + }, ); };