-
Notifications
You must be signed in to change notification settings - Fork 3
[feature] 여러명의 지원상태를 변경할 수 있다. #710
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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!), | ||||||||||||||||||||||||||||||||
|
Comment on lines
+9
to
+10
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 배치 페이로드 검증 및 널 단언 제거
- 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);
+ },또한, 디버깅 편의를 위해 return useMutation({
+ mutationKey: ['updateApplicants', clubId],📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| onSuccess: () => { | ||||||||||||||||||||||||||||||||
| queryClient.invalidateQueries({ queryKey: ["clubApplicants"] }); | ||||||||||||||||||||||||||||||||
| queryClient.invalidateQueries({ queryKey: ['clubApplicants'] }); | ||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chain쿼리 무효화 범위 축소 필요 (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 tsxLength 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 쿼리 무효화에 두 곳의 invalidateQueries 호출에서
- queryClient.invalidateQueries({ queryKey: ['clubApplicants'] });
+ queryClient.invalidateQueries({ queryKey: ['clubApplicants', clubId] });📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||
| onError: (error) => { | ||||||||||||||||||||||||||||||||
| console.log(`Error updating applicant detail: ${error}`); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+106
to
+128
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
lepitaaar marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+145
to
154
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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'; | ||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.jsonLength 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 구조분해 사용 - 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| 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<HTMLDivElement | null>(null); | ||||||||||||||||||||||||||||||
| const statusSelectRef = useRef<HTMLDivElement | null>(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<string, boolean>(); | ||||||||||||||||||||||||||||||
| 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)); | ||||||||||||||||||||||||||||||
lepitaaar marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||
| }, [checkedItem]); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if (!clubId) return null; | ||||||||||||||||||||||||||||||
|
|
@@ -80,7 +113,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); | ||||||||||||||||||||||||||||||
lepitaaar marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if (mode === 'all') { | ||||||||||||||||||||||||||||||
| newMap.forEach((_, key) => newMap.set(key, !isAllChecked)); | ||||||||||||||||||||||||||||||
|
|
@@ -112,6 +145,27 @@ 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 ( | ||||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||||
| <Styled.ApplicationHeader> | ||||||||||||||||||||||||||||||
|
|
@@ -171,18 +225,52 @@ const ApplicantsTab = () => { | |||||||||||||||||||||||||||||
| <Styled.Arrow src={selectIcon} /> | ||||||||||||||||||||||||||||||
| </Styled.SelectWrapper> | ||||||||||||||||||||||||||||||
| <Styled.VerticalLine /> | ||||||||||||||||||||||||||||||
| <Styled.SelectWrapper> | ||||||||||||||||||||||||||||||
| <Styled.StatusSelect | ||||||||||||||||||||||||||||||
| disabled={!Array.from(checkedItem.values()).some(Boolean)} | ||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||
| <option value='상태변경'>상태변경</option> | ||||||||||||||||||||||||||||||
| <Styled.SelectWrapper | ||||||||||||||||||||||||||||||
| ref={statusSelectRef} | ||||||||||||||||||||||||||||||
| onClick={() => { | ||||||||||||||||||||||||||||||
| if (!isChecked) return; | ||||||||||||||||||||||||||||||
| setStatusOpen((prev) => !prev); | ||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||
| <Styled.StatusSelect disabled={!isChecked}> | ||||||||||||||||||||||||||||||
| 상태변경 | ||||||||||||||||||||||||||||||
| </Styled.StatusSelect> | ||||||||||||||||||||||||||||||
|
Comment on lines
+228
to
237
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||||||||||||||||||||||||||
| <Styled.Arrow width={8} height={8} src={selectIcon} /> | ||||||||||||||||||||||||||||||
| <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> | ||||||||||||||||||||||||||||||
|
Comment on lines
+239
to
+268
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 상태 항목들이 중복되어 코드가 길어지는 느낌이에용 |
||||||||||||||||||||||||||||||
| </Styled.SelectWrapper> | ||||||||||||||||||||||||||||||
| <Styled.DeleteButton | ||||||||||||||||||||||||||||||
| src={deleteIcon} | ||||||||||||||||||||||||||||||
| alt='삭제' | ||||||||||||||||||||||||||||||
| disabled={!Array.from(checkedItem.values()).some(Boolean)} | ||||||||||||||||||||||||||||||
| disabled={!isChecked} | ||||||||||||||||||||||||||||||
| onClick={() => { | ||||||||||||||||||||||||||||||
| const toBeDeleted = Array.from(checkedItem.entries()) | ||||||||||||||||||||||||||||||
| .filter(([_, isChecked]) => isChecked) | ||||||||||||||||||||||||||||||
|
|
@@ -205,7 +293,7 @@ const ApplicantsTab = () => { | |||||||||||||||||||||||||||||
| <Styled.ApplicantTableHeaderWrapper> | ||||||||||||||||||||||||||||||
| <Styled.ApplicantTableRow> | ||||||||||||||||||||||||||||||
| <Styled.ApplicantTableHeader width={55}> | ||||||||||||||||||||||||||||||
| <Styled.ApplicantAllSelectWrapper> | ||||||||||||||||||||||||||||||
| <Styled.ApplicantAllSelectWrapper ref={allSelectRef}> | ||||||||||||||||||||||||||||||
| <Styled.ApplicantTableAllSelectCheckbox | ||||||||||||||||||||||||||||||
| checked={selectAll} | ||||||||||||||||||||||||||||||
| onClick={(e: React.MouseEvent<HTMLInputElement>) => { | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
배치 업데이트 입력 검증 추가 + 파라미터 명확화.
빈 배열/누락된 applicantId를 조기에 차단하면 서버 4xx를 줄일 수 있습니다. 또한 변수명을 updates로 명확화하세요.
📝 Committable suggestion
🤖 Prompt for AI Agents