From 2f29c2af7bbf1c6061ecf85b409a0e9a39935bc8 Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Mon, 15 Sep 2025 16:02:41 +0900 Subject: [PATCH 01/28] =?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=8B=A4=EC=9A=B4=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.=20Compound=20=ED=8C=A8=ED=84=B4=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=B4=EB=8B=A4=20=ED=99=95=EC=9E=A5=EC=84=B1=EC=9E=88?= =?UTF-8?q?=EA=B2=8C=20=EC=B6=94=EA=B0=80=ED=95=98=EC=98=80=EC=8A=B5?= =?UTF-8?q?=EB=8B=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/CustomDropDown/CustomDropDown.tsx | 134 +++++++++++++----- 1 file changed, 99 insertions(+), 35 deletions(-) diff --git a/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx b/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx index e5b7a437a..252a7415a 100644 --- a/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx +++ b/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx @@ -1,50 +1,114 @@ -import { useState } from 'react'; +import React, { + createContext, + useContext, + useMemo, + ReactNode, + useEffect, +} from 'react'; import * as Styled from './CustomDropDown.styles'; -import dropdown_icon from '@/assets/images/icons/drop_button_icon.svg'; -interface DropdownOption { +interface DropdownOption { label: string; - value: string; + value: TValue; } -interface DropdownProps { - options: DropdownOption[]; - selected: string; - onSelect: (value: string) => void; +interface CustomDropDownContextProps { + open: boolean; + selected: TValue; + options: DropdownOption[]; + onToggle: () => void; + handleSelect: (value: TValue) => void; } -const CustomDropdown = ({ options, selected, onSelect }: DropdownProps) => { - const [open, setOpen] = useState(false); +interface CustomDropDownProps + extends Omit, 'onSelect'> { + children: ReactNode; + options: DropdownOption[]; + selected?: TValue; + onSelect: (value: TValue) => void; + open: boolean; + onToggle: () => void; +} + +interface ItemProps { + value: TValue; + children: ReactNode; + style?: React.CSSProperties; +} + +const CustomDropDownContext = createContext< + CustomDropDownContextProps | undefined +>(undefined); + +const useDropDownContext = () => { + const context = useContext(CustomDropDownContext); + if (!context) { + throw new Error( + 'useDropDownContext는 CustomDropDownContextProvider 내부에서 사용할 수 있습니다.', + ); + } + return context; +}; + +const Trigger = ({ children }: { children: ReactNode }) => { + return <>{children}; +}; + +interface MenuProps { + children: ReactNode; + top?: string; + width?: string; + right?: string; +} - const handleSelect = (value: string) => { +const Menu = ({ children, top, width, right }: MenuProps) => { + const { open } = useDropDownContext(); + return open ? ( + + {children} + + ) : null; +}; + +const Item = ({ value, children, style }: ItemProps) => { + const { selected, handleSelect } = useDropDownContext(); + return ( + handleSelect(value)} + style={style} + > + {children} + + ); +}; + +export function CustomDropDown({ + children, + options, + selected, + onSelect, + open, + onToggle, + ...rest +}: CustomDropDownProps) { + const handleSelect = (value: T) => { onSelect(value); - setOpen(false); + onToggle(); }; - const selectedLabel = - options.find((option) => option.value === selected)?.label || '선택하세요'; + const value = useMemo( + () => ({ open, selected, options, onToggle, handleSelect }), + [open, selected, options, onToggle, onSelect], + ); return ( - - setOpen((prev) => !prev)} open={open}> - {selectedLabel} - - - {open && ( - - {options.map(({ label, value }) => ( - handleSelect(value)} - > - {label} - - ))} - - )} - + + {children} + ); -}; +} -export default CustomDropdown; +CustomDropDown.Trigger = Trigger; +CustomDropDown.Menu = Menu; +CustomDropDown.Item = Item; From a137afb782453d5abc0108797edc0e431062a12f Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Mon, 15 Sep 2025 16:04:31 +0900 Subject: [PATCH 02/28] =?UTF-8?q?feat:=20=EB=93=9C=EB=A1=AD=EB=8B=A4?= =?UTF-8?q?=EC=9A=B4=20=EB=A9=94=EB=89=B4=20=EC=8A=A4=ED=83=80=EC=9D=BC?= =?UTF-8?q?=EC=9D=84=20=EC=9E=90=EC=9C=A0=EB=A1=AD=EA=B2=8C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=ED=95=A0=20=EC=88=98=20=EC=9E=88=EA=B2=8C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CustomDropDown/CustomDropDown.styles.ts | 37 +++++-------------- 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts b/frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts index 4a54f5049..bc6ba1a17 100644 --- a/frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts +++ b/frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts @@ -5,27 +5,18 @@ export const DropDownWrapper = styled.div` width: 100%; `; -export const Selected = styled.div<{ open: boolean }>` - padding: 12px 16px; - border-radius: 0.375rem; - background: ${({ open }) => (open ? '#fff' : '#f5f5f5')}; - color: #787878; - font-size: 0.875rem; - font-weight: 600; - cursor: pointer; - border: 1px solid ${({ open }) => (open ? '#c5c5c5' : 'transparent')}; - transition: - border-color 0.2s ease, - background-color 0.2s ease; - - user-select: none; -`; +interface OptionListProps { + top?: string; + width?: string; + right?: string; +} -export const OptionList = styled.ul` +export const OptionList = styled.ul` position: absolute; - top: 100%; - left: 0; - width: 100%; + top: ${({ top }) => top || '100%'}; + left: ${({ right }) => (right ? 'auto' : '0')}; + width: ${({ width }) => width || '100%'}; + right: ${({ right }) => right || 'auto'}; background: #fff; border-radius: 6px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05); @@ -50,11 +41,3 @@ export const OptionItem = styled.li<{ isSelected: boolean }>` transition: background-color 0.2s ease; user-select: none; `; - -export const Icon = styled.img` - position: absolute; - top: 50%; - right: 19px; - transform: translateY(-50%); - pointer-events: none; -`; From e2f589cb3123dfec98630a6755461957df43d305 Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Mon, 15 Sep 2025 16:04:56 +0900 Subject: [PATCH 03/28] =?UTF-8?q?feat:=20=EB=93=9C=EB=A1=AD=EB=8B=A4?= =?UTF-8?q?=EC=9A=B4=20=EB=A9=94=EB=89=B4=EB=A5=BC=20=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=ED=85=80=20=EB=93=9C=EB=A1=AD=EB=8B=A4=EC=9A=B4=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EA=B5=90=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QuestionBuilder/QuestionBuilder.styles.ts | 24 +++++++++++++ .../QuestionBuilder/QuestionBuilder.tsx | 35 ++++++++++++++++--- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.styles.ts b/frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.styles.ts index 37cff899a..eed7b824f 100644 --- a/frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.styles.ts +++ b/frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.styles.ts @@ -75,6 +75,30 @@ export const SelectionToggleButton = styled.button<{ active: boolean }>` color 0.2s ease; `; +export const Selected = styled.div<{ open: boolean }>` + padding: 12px 16px; + border-radius: 0.375rem; + background: ${({ open }) => (open ? '#fff' : '#f5f5f5')}; + color: #787878; + font-size: 0.875rem; + font-weight: 600; + cursor: pointer; + border: 1px solid ${({ open }) => (open ? '#c5c5c5' : 'transparent')}; + transition: + border-color 0.2s ease, + background-color 0.2s ease; + + user-select: none; +`; + +export const Icon = styled.img` + position: absolute; + top: 50%; + right: 19px; + transform: translateY(-50%); + pointer-events: none; +`; + export const DeleteButton = styled.button` display: flex; align-items: center; diff --git a/frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.tsx b/frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.tsx index 6c553a69b..e889dc833 100644 --- a/frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.tsx +++ b/frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.tsx @@ -7,8 +7,9 @@ import { QuestionBuilderProps } from '@/types/application'; import { QUESTION_LABEL_MAP } from '@/constants/APPLICATION_FORM'; import { DROPDOWN_OPTIONS } from '@/constants/APPLICATION_FORM'; import * as Styled from './QuestionBuilder.styles'; -import CustomDropdown from '@/components/common/CustomDropDown/CustomDropDown'; import DeleteIcon from '@/assets/images/icons/delete_question.svg'; +import { CustomDropDown } from '@/components/common/CustomDropDown/CustomDropDown'; +import dropdown_icon from '@/assets/images/icons/drop_button_icon.svg'; const QuestionBuilder = ({ id, @@ -33,6 +34,8 @@ const QuestionBuilder = ({ type === 'MULTI_CHOICE' ? 'multi' : 'single', ); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + useEffect(() => { if (type === 'MULTI_CHOICE') { setSelectionType('multi'); @@ -118,6 +121,11 @@ const QuestionBuilder = ({ ); }; + const selectedType = type === 'MULTI_CHOICE' ? 'CHOICE' : type; + const selectedLabel = DROPDOWN_OPTIONS.find( + (option) => option.value === selectedType, + )?.label; + return ( @@ -127,13 +135,32 @@ const QuestionBuilder = ({ 답변 필수 - { onTypeChange?.(value as QuestionType); }} - /> + open={isDropdownOpen} + onToggle={() => setIsDropdownOpen((prev) => !prev)} + > + + setIsDropdownOpen((prev) => !prev)} + > + {selectedLabel} + + + + + {DROPDOWN_OPTIONS.map(({ label, value }) => ( + + {label} + + ))} + + {renderSelectionToggle()} {!readOnly && ( onRemoveQuestion()}> From f17ebbeeb1df8e956e70ae54e7e8d9f604460579 Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Mon, 15 Sep 2025 16:05:49 +0900 Subject: [PATCH 04/28] =?UTF-8?q?refactor:=20default=20=EA=B0=92=EC=9D=84?= =?UTF-8?q?=20=EC=A0=84=EC=B2=B4=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20Applicatn?= =?UTF-8?q?sTab=EC=97=90=EC=84=9C=20ALL=20=EC=86=8D=EC=84=B1=EC=9D=84=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EA=B8=B0=EC=9C=84=ED=95=B4=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=ED=96=88=EC=8A=B5=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/utils/mapStatusToGroup.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/frontend/src/utils/mapStatusToGroup.ts b/frontend/src/utils/mapStatusToGroup.ts index 9f57824e7..f1a95ad29 100644 --- a/frontend/src/utils/mapStatusToGroup.ts +++ b/frontend/src/utils/mapStatusToGroup.ts @@ -1,18 +1,23 @@ -import { ApplicationStatus } from "@/types/applicants"; +import { ApplicationStatus } from '@/types/applicants'; -const mapStatusToGroup = (status: ApplicationStatus): { status: ApplicationStatus, label: string } => { +const mapStatusToGroup = ( + status: ApplicationStatus, +): { status: ApplicationStatus; label: string } => { switch (status) { case ApplicationStatus.SUBMITTED: return { status: ApplicationStatus.SUBMITTED, label: '서류검토' }; case ApplicationStatus.INTERVIEW_SCHEDULED: - return { status: ApplicationStatus.INTERVIEW_SCHEDULED, label: '면접예정' }; + return { + status: ApplicationStatus.INTERVIEW_SCHEDULED, + label: '면접예정', + }; case ApplicationStatus.ACCEPTED: return { status: ApplicationStatus.ACCEPTED, label: '합격' }; case ApplicationStatus.DECLINED: return { status: ApplicationStatus.DECLINED, label: '불합' }; default: - return { status: ApplicationStatus.SUBMITTED, label: '서류검토'}; + return { status: ApplicationStatus.SUBMITTED, label: '전체' }; } -} +}; -export default mapStatusToGroup; \ No newline at end of file +export default mapStatusToGroup; From 7954ef8ee463c827024d175e606cb6a71b11af45 Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Mon, 15 Sep 2025 16:06:21 +0900 Subject: [PATCH 05/28] =?UTF-8?q?refactor:=20=EA=B8=B0=EC=A1=B4=20?= =?UTF-8?q?=EB=93=9C=EB=A1=AD=EB=8B=A4=EC=9A=B4=20=EB=A9=94=EB=89=B4?= =?UTF-8?q?=EB=A5=BC=20CustomDropDown=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=EB=A1=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ApplicantsTab/ApplicantsTab.styles.ts | 3 +- .../tabs/ApplicantsTab/ApplicantsTab.tsx | 130 ++++++++++++------ 2 files changed, 89 insertions(+), 44 deletions(-) diff --git a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts index 0526d0461..408594a41 100644 --- a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts +++ b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts @@ -282,7 +282,8 @@ export const ApplicantAllSelectWrapper = styled.div` export const ApplicantAllSelectArrow = styled.img` position: absolute; - right: -1px; + right: -18px; + top: -7px; width: 16px; height: 16px; object-fit: none; diff --git a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx index 8d04ff0f7..91f3c57f3 100644 --- a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx +++ b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx @@ -11,6 +11,7 @@ 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'; import { AVAILABLE_STATUSES } from '@/constants/status'; +import { CustomDropDown } from '@/components/common/CustomDropDown/CustomDropDown'; const ApplicantsTab = () => { const navigate = useNavigate(); @@ -21,12 +22,11 @@ const ApplicantsTab = () => { ); const [selectAll, setSelectAll] = useState(false); const [open, setOpen] = useState(false); - const [statusOpen, setStatusOpen] = useState(false); + const [isStatusDropdownOpen, setIsStatusDropdownOpen] = 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 []; @@ -51,20 +51,13 @@ const ApplicantsTab = () => { ) { setOpen(false); } - if ( - statusOpen && - statusSelectRef.current && - !statusSelectRef.current.contains(target) - ) { - setStatusOpen(false); - } }; document.addEventListener('mousedown', handleOutsideClick); return () => { document.removeEventListener('mousedown', handleOutsideClick); }; - }, [open, statusOpen]); + }, [open]); useEffect(() => { const newMap = new Map(); @@ -132,8 +125,6 @@ const ApplicantsTab = () => { }); return newMap; }); - - if (open) setOpen(false); }; const checkoutAllApplicants = () => { @@ -158,7 +149,6 @@ const ApplicantsTab = () => { { onSuccess: () => { checkoutAllApplicants(); - setStatusOpen(false); }, onError: () => { alert('지원자 상태 변경에 실패했습니다. 다시 시도해주세요.'); @@ -167,15 +157,27 @@ const ApplicantsTab = () => { ); }; + const statusOptions = AVAILABLE_STATUSES.map((status) => ({ + value: status, + label: mapStatusToGroup(status).label, + })); + + const filterOptions = ['ALL', ...Object.values(ApplicationStatus)].map( + (status) => ({ + value: status, + label: mapStatusToGroup(status as ApplicationStatus).label, + }), + ); + return ( <> 지원 현황 - {/* + {/* + - ...다른 학기 */ - /*{' '} - */} + + */} @@ -226,28 +228,42 @@ const ApplicantsTab = () => { - { - if (!isChecked) return; - setStatusOpen((prev) => !prev); - }} - > - - 상태변경 - - - - {AVAILABLE_STATUSES.map((status) => ( - + + updateAllApplicants(status as ApplicationStatus) + } + open={isStatusDropdownOpen} + onToggle={() => { + if (!isChecked) return; + setIsStatusDropdownOpen((prev) => !prev); + }} + > + + { - updateAllApplicants(status); + if (!isChecked) return; + setIsStatusDropdownOpen((prev) => !prev); }} > - {mapStatusToGroup(status).label} - - ))} - + 상태변경 + + + + + {statusOptions.map(({ value, label }) => ( + + {label} + + ))} + + { selectApplicantsByStatus('all'); }} /> - setOpen((prev) => !prev)} - /> - + { + if (status === 'ALL') { + selectApplicantsByStatus('all'); + } else { + selectApplicantsByStatus( + 'filter', + status as ApplicationStatus, + ); + } + }} + onToggle={() => { + setOpen((prev) => !prev); + }} + open={open} + style={{ width: '0' }} + > + + setOpen((prev) => !prev)} + /> + + + {filterOptions.map(({ value, label }) => ( + + {label} + + ))} + + + {/* { if (selectAll) { @@ -328,7 +372,7 @@ const ApplicantsTab = () => { > 선택해제 - + */} From 6462c46e8fb573cfa41be96ae44d16776de1e8dc Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Tue, 16 Sep 2025 20:29:28 +0900 Subject: [PATCH 06/28] feat: click outside close dropdown menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit useRef를 array 로 초기화하여 ref를 여러개 선언하지않고 한개의 ref로 여러개의 엘리먼트를 핸들 할 수 있습니다. --- .../tabs/ApplicantsTab/ApplicantsTab.tsx | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx index 91f3c57f3..4143b6674 100644 --- a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx +++ b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx @@ -26,7 +26,7 @@ const ApplicantsTab = () => { const [isChecked, setIsChecked] = useState(false); const { mutate: deleteApplicants } = useDeleteApplicants(clubId!); const { mutate: updateDetailApplicants } = useUpdateApplicant(clubId!); - const allSelectRef = useRef(null); + const dropdwonRef = useRef>([]); const filteredApplicants = useMemo(() => { if (!applicantsData?.applicants) return []; @@ -43,13 +43,12 @@ const ApplicantsTab = () => { useEffect(() => { const handleOutsideClick = (e: MouseEvent) => { const target = e.target as Node; - if ( - open && - allSelectRef.current && - !allSelectRef.current.contains(target) + dropdwonRef.current && + !dropdwonRef.current.some((ref) => ref && ref.contains(target)) ) { - setOpen(false); + if (open) setOpen(false); + if (isStatusDropdownOpen) setIsStatusDropdownOpen(false); } }; @@ -228,7 +227,11 @@ const ApplicantsTab = () => { - + { + dropdwonRef.current[0] = el; + }} + > @@ -291,7 +294,11 @@ const ApplicantsTab = () => { - + { + dropdwonRef.current[1] = el; + }} + > ) => { From 8433910d634fd58b6e0f766d7ff7245690f6de19 Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Sun, 21 Sep 2025 09:44:23 +0900 Subject: [PATCH 07/28] =?UTF-8?q?feat:=20prevent=20=EC=84=A0=ED=83=9D=20?= =?UTF-8?q?=EB=A9=94=EB=89=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts | 1 + .../src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts index 408594a41..0a0eb983f 100644 --- a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts +++ b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts @@ -160,6 +160,7 @@ export const ApplicantFilterSelect = styled.select` border: none; background: var(--f5, #f5f5f5); font-size: 16px; + color: #000; -webkit-appearance: none; -moz-appearance: none; diff --git a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx index 4143b6674..138a33748 100644 --- a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx +++ b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx @@ -215,13 +215,13 @@ const ApplicantsTab = () => { - + - + From a698eb9ad4ddd813b8891c1315036cd80b98dce3 Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Sun, 21 Sep 2025 10:06:01 +0900 Subject: [PATCH 08/28] =?UTF-8?q?feat:=20=EC=A0=84=EC=B2=B4,=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=EC=88=9C=20=EC=A0=95=EB=A0=AC=20=EC=98=B5=EC=85=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tabs/ApplicantsTab/ApplicantsTab.tsx | 200 +++++++++++------- 1 file changed, 129 insertions(+), 71 deletions(-) diff --git a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx index 138a33748..00c525620 100644 --- a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx +++ b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx @@ -14,6 +14,23 @@ import { AVAILABLE_STATUSES } from '@/constants/status'; import { CustomDropDown } from '@/components/common/CustomDropDown/CustomDropDown'; const ApplicantsTab = () => { + const statusOptions = AVAILABLE_STATUSES.map((status) => ({ + value: status, + label: mapStatusToGroup(status).label, + })); + + const filterOptions = ['ALL', ...Object.values(ApplicationStatus)].map( + (status) => ({ + value: status, + label: mapStatusToGroup(status as ApplicationStatus).label, + }), + ); + + const sortOptions = [ + { value: 'date', label: '제출순' }, + { value: 'name', label: '이름순' }, + ]; + const navigate = useNavigate(); const { clubId, applicantsData } = useAdminClubContext(); const [keyword, setKeyword] = useState(''); @@ -24,6 +41,10 @@ const ApplicantsTab = () => { const [open, setOpen] = useState(false); const [isStatusDropdownOpen, setIsStatusDropdownOpen] = useState(false); const [isChecked, setIsChecked] = useState(false); + const [isFilterOpen, setIsFilterOpen] = useState(false); + const [selectedFilter, setSelectedFilter] = useState('ALL'); + const [isSortOpen, setIsSortOpen] = useState(false); + const [selectedSort, setSelectedSort] = useState(sortOptions[0]); const { mutate: deleteApplicants } = useDeleteApplicants(clubId!); const { mutate: updateDetailApplicants } = useUpdateApplicant(clubId!); const dropdwonRef = useRef>([]); @@ -31,14 +52,35 @@ const ApplicantsTab = () => { const filteredApplicants = useMemo(() => { if (!applicantsData?.applicants) return []; - if (!keyword.trim()) return applicantsData.applicants; + let applicants = [...applicantsData.applicants]; - return applicantsData.applicants.filter((user: Applicant) => - user.answers[0].value - .toLowerCase() - .includes(keyword.trim().toLowerCase()), - ); - }, [applicantsData, keyword]); + if (selectedFilter !== 'ALL') { + applicants = applicants.filter( + (applicant) => applicant.status === selectedFilter, + ); + } + + if (keyword.trim()) { + applicants = applicants.filter((user: Applicant) => + user.answers[0].value + .toLowerCase() + .includes(keyword.trim().toLowerCase()), + ); + } + + if (selectedSort.value === 'name') { + applicants.sort((a, b) => + a.answers[0].value.localeCompare(b.answers[0].value), + ); + } else { + applicants.sort( + (a, b) => + new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), + ); + } + + return applicants; + }, [applicantsData, keyword, selectedFilter, selectedSort.value]); useEffect(() => { const handleOutsideClick = (e: MouseEvent) => { @@ -49,6 +91,8 @@ const ApplicantsTab = () => { ) { if (open) setOpen(false); if (isStatusDropdownOpen) setIsStatusDropdownOpen(false); + if (isFilterOpen) setIsFilterOpen(false); + if (isSortOpen) setIsSortOpen(false); } }; @@ -56,7 +100,7 @@ const ApplicantsTab = () => { return () => { document.removeEventListener('mousedown', handleOutsideClick); }; - }, [open]); + }, [open, isStatusDropdownOpen, isFilterOpen, isSortOpen]); useEffect(() => { const newMap = new Map(); @@ -156,18 +200,6 @@ const ApplicantsTab = () => { ); }; - const statusOptions = AVAILABLE_STATUSES.map((status) => ({ - value: status, - label: mapStatusToGroup(status).label, - })); - - const filterOptions = ['ALL', ...Object.values(ApplicationStatus)].map( - (status) => ({ - value: status, - label: mapStatusToGroup(status as ApplicationStatus).label, - }), - ); - return ( <> @@ -214,17 +246,84 @@ const ApplicantsTab = () => { 지원자 목록 - - - - - + { + dropdwonRef.current[2] = el; + }} + > + { + setSelectedFilter(value as string); + setIsFilterOpen(false); + }} + open={isFilterOpen} + onToggle={() => setIsFilterOpen((prev) => !prev)} + > + + setIsFilterOpen((prev) => !prev)} + > + { + filterOptions.find( + (option) => option.value === selectedFilter, + )?.label + } + + setIsFilterOpen((prev) => !prev)} + /> + + + {filterOptions.map(({ value, label }) => ( + + {label} + + ))} + + - - - - - + { + dropdwonRef.current[3] = el; + }} + > + { + const selected = sortOptions.find( + (option) => option.value === value, + ); + if (selected) { + setSelectedSort(selected); + } + setIsSortOpen(false); + }} + open={isSortOpen} + onToggle={() => setIsSortOpen((prev) => !prev)} + > + + setIsSortOpen((prev) => !prev)} + > + {selectedSort.label} + + setIsSortOpen((prev) => !prev)} + /> + + + {sortOptions.map(({ value, label }) => ( + + {label} + + ))} + + { ))} - {/* - { - if (selectAll) { - setOpen(false); - return; - } - selectApplicantsByStatus('all'); - }} - > - 전체선택 - - { - selectApplicantsByStatus( - 'filter', - ApplicationStatus.SUBMITTED, - ); - }} - > - 서류 검토 필요 - - { - selectApplicantsByStatus( - 'filter', - ApplicationStatus.INTERVIEW_SCHEDULED, - ); - }} - > - 면접예정 - - { - setOpen(false); - checkoutAllApplicants(); - }} - > - 선택해제 - - */} From 875de8d21782f19936e331e716e712a66f61b93d Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Sun, 21 Sep 2025 10:16:37 +0900 Subject: [PATCH 09/28] =?UTF-8?q?feat:=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EA=B0=80=EC=9A=B4=EB=8D=B0=20=EC=A0=95=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ApplicantsTab/ApplicantsTab.styles.ts | 4 +++- .../tabs/ApplicantsTab/ApplicantsTab.tsx | 24 +++++++++---------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts index 0a0eb983f..e7aa8798a 100644 --- a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts +++ b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts @@ -153,7 +153,9 @@ export const StatusSelectMenuItem = styled.div` } `; -export const ApplicantFilterSelect = styled.select` +export const ApplicantFilterSelect = styled.div` + display: flex; + align-items: center; height: 35px; padding: 4px 32px 4px 14px; border-radius: 8px; diff --git a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx index 00c525620..930b6043f 100644 --- a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx +++ b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx @@ -262,7 +262,6 @@ const ApplicantsTab = () => { > setIsFilterOpen((prev) => !prev)} > { @@ -271,14 +270,15 @@ const ApplicantsTab = () => { )?.label } - setIsFilterOpen((prev) => !prev)} - /> + {filterOptions.map(({ value, label }) => ( - + {label} ))} @@ -306,19 +306,19 @@ const ApplicantsTab = () => { > setIsSortOpen((prev) => !prev)} > {selectedSort.label} - setIsSortOpen((prev) => !prev)} - /> + {sortOptions.map(({ value, label }) => ( - + {label} ))} From 228bde8b86ca6ddaa0672fe5f55ccc3bddca9fc8 Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Sun, 21 Sep 2025 14:34:24 +0900 Subject: [PATCH 10/28] feat: change figma style guide --- .../common/CustomDropDown/CustomDropDown.styles.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts b/frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts index bc6ba1a17..099712bcc 100644 --- a/frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts +++ b/frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts @@ -17,25 +17,27 @@ export const OptionList = styled.ul` left: ${({ right }) => (right ? 'auto' : '0')}; width: ${({ width }) => width || '100%'}; right: ${({ right }) => right || 'auto'}; - background: #fff; border-radius: 6px; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05); + border: 1px solid #dcdcdc; + background: #fff; + box-shadow: 0 1px 8px 0 rgba(0, 0, 0, 0.12); + padding: 6px 0; z-index: 10; + height: auto; list-style: none; `; export const OptionItem = styled.li<{ isSelected: boolean }>` text-align: center; padding: 10px; - margin: 4px; font-weight: 600; - border-radius: 6px; color: #787878; background-color: ${({ isSelected }) => (isSelected ? '#DCDCDC' : '#fff')}; cursor: pointer; + padding: 8px 13px; &:hover { - background-color: #dcdcdc; + background-color: #f5f5f5; } transition: background-color 0.2s ease; From a96c1ef1ea70df4507468a1d7c70bfdb946351c7 Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Sun, 21 Sep 2025 20:07:36 +0900 Subject: [PATCH 11/28] =?UTF-8?q?feat:=20figma=20=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B8=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/CustomDropDown/CustomDropDown.styles.ts | 2 +- .../components/common/CustomDropDown/CustomDropDown.tsx | 8 +------- .../pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx | 4 ++-- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts b/frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts index 099712bcc..6b0f2cae3 100644 --- a/frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts +++ b/frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts @@ -13,7 +13,7 @@ interface OptionListProps { export const OptionList = styled.ul` position: absolute; - top: ${({ top }) => top || '100%'}; + top: ${({ top }) => top || '110%'}; left: ${({ right }) => (right ? 'auto' : '0')}; width: ${({ width }) => width || '100%'}; right: ${({ right }) => right || 'auto'}; diff --git a/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx b/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx index 252a7415a..46211d9e0 100644 --- a/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx +++ b/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx @@ -1,10 +1,4 @@ -import React, { - createContext, - useContext, - useMemo, - ReactNode, - useEffect, -} from 'react'; +import React, { createContext, useContext, useMemo, ReactNode } from 'react'; import * as Styled from './CustomDropDown.styles'; interface DropdownOption { diff --git a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx index 930b6043f..09a2b2467 100644 --- a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx +++ b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx @@ -272,7 +272,7 @@ const ApplicantsTab = () => { - + {filterOptions.map(({ value, label }) => ( { - + {sortOptions.map(({ value, label }) => ( Date: Sun, 21 Sep 2025 20:10:00 +0900 Subject: [PATCH 12/28] =?UTF-8?q?feat:=20=EC=84=A0=ED=83=9D=EB=90=98?= =?UTF-8?q?=EC=97=88=EC=9D=84=EB=95=8C=EC=9D=98=20=EC=83=89=EC=83=81=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/common/CustomDropDown/CustomDropDown.styles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts b/frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts index 6b0f2cae3..ab5e0d2e6 100644 --- a/frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts +++ b/frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts @@ -32,7 +32,7 @@ export const OptionItem = styled.li<{ isSelected: boolean }>` padding: 10px; font-weight: 600; color: #787878; - background-color: ${({ isSelected }) => (isSelected ? '#DCDCDC' : '#fff')}; + background-color: ${({ isSelected }) => (isSelected ? '#f5f5f5' : '#fff')}; cursor: pointer; padding: 8px 13px; From 69f0f7b1d81005b1aff36c9ef1480231df2eb2e4 Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Fri, 3 Oct 2025 20:26:09 +0900 Subject: [PATCH 13/28] =?UTF-8?q?style:=20=EB=94=94=EC=9E=90=EC=9D=B8?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/CustomDropDown/CustomDropDown.styles.ts | 5 ++++- .../src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts b/frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts index ab5e0d2e6..33c8dec26 100644 --- a/frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts +++ b/frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts @@ -28,13 +28,16 @@ export const OptionList = styled.ul` `; export const OptionItem = styled.li<{ isSelected: boolean }>` + display: flex; + align-items: center; + justify-content: center; text-align: center; - padding: 10px; font-weight: 600; color: #787878; background-color: ${({ isSelected }) => (isSelected ? '#f5f5f5' : '#fff')}; cursor: pointer; padding: 8px 13px; + height: 27px; &:hover { background-color: #f5f5f5; diff --git a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx index 09a2b2467..1a820148b 100644 --- a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx +++ b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx @@ -259,6 +259,7 @@ const ApplicantsTab = () => { }} open={isFilterOpen} onToggle={() => setIsFilterOpen((prev) => !prev)} + style={{ width: '101px' }} > { }} open={isSortOpen} onToggle={() => setIsSortOpen((prev) => !prev)} + style={{ width: '101px' }} > { {filterOptions.map(({ value, label }) => ( - + {label} ))} From 5165a86e96128cd4c497cae155e2d8e73572fdb7 Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Fri, 3 Oct 2025 20:39:06 +0900 Subject: [PATCH 14/28] =?UTF-8?q?fix:=20=EC=97=AC=EB=9F=AC=EA=B0=9C?= =?UTF-8?q?=EC=9D=98=20=EB=93=9C=EB=A1=AD=EB=8B=A4=EC=9A=B4=20=EB=A9=94?= =?UTF-8?q?=EB=89=B4=EA=B0=80=20=ED=8E=BC=EC=B3=90=EC=A7=80=EB=8A=94=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tabs/ApplicantsTab/ApplicantsTab.tsx | 51 +++++++++++++------ 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx index 1a820148b..6d9212c43 100644 --- a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx +++ b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx @@ -45,6 +45,14 @@ const ApplicantsTab = () => { const [selectedFilter, setSelectedFilter] = useState('ALL'); const [isSortOpen, setIsSortOpen] = useState(false); const [selectedSort, setSelectedSort] = useState(sortOptions[0]); + + // 모든 드롭다운을 닫는 함수 + const closeAllDropdowns = () => { + if (open) setOpen(false); + if (isStatusDropdownOpen) setIsStatusDropdownOpen(false); + if (isFilterOpen) setIsFilterOpen(false); + if (isSortOpen) setIsSortOpen(false); + }; const { mutate: deleteApplicants } = useDeleteApplicants(clubId!); const { mutate: updateDetailApplicants } = useUpdateApplicant(clubId!); const dropdwonRef = useRef>([]); @@ -89,10 +97,7 @@ const ApplicantsTab = () => { dropdwonRef.current && !dropdwonRef.current.some((ref) => ref && ref.contains(target)) ) { - if (open) setOpen(false); - if (isStatusDropdownOpen) setIsStatusDropdownOpen(false); - if (isFilterOpen) setIsFilterOpen(false); - if (isSortOpen) setIsSortOpen(false); + closeAllDropdowns(); } }; @@ -254,16 +259,21 @@ const ApplicantsTab = () => { { - setSelectedFilter(value as string); - setIsFilterOpen(false); + setSelectedFilter(value); }} open={isFilterOpen} - onToggle={() => setIsFilterOpen((prev) => !prev)} + onToggle={() => { + closeAllDropdowns(); + setIsFilterOpen(true); + }} style={{ width: '101px' }} > setIsFilterOpen((prev) => !prev)} + onClick={() => { + closeAllDropdowns(); + setIsFilterOpen(true); + }} > { filterOptions.find( @@ -300,15 +310,20 @@ const ApplicantsTab = () => { if (selected) { setSelectedSort(selected); } - setIsSortOpen(false); }} open={isSortOpen} - onToggle={() => setIsSortOpen((prev) => !prev)} + onToggle={() => { + closeAllDropdowns(); + setIsSortOpen(true); + }} style={{ width: '101px' }} > setIsSortOpen((prev) => !prev)} + onClick={() => { + closeAllDropdowns(); + setIsSortOpen(true); + }} > {selectedSort.label} @@ -341,7 +356,8 @@ const ApplicantsTab = () => { open={isStatusDropdownOpen} onToggle={() => { if (!isChecked) return; - setIsStatusDropdownOpen((prev) => !prev); + closeAllDropdowns(); + setIsStatusDropdownOpen(true); }} > @@ -349,7 +365,8 @@ const ApplicantsTab = () => { disabled={!isChecked} onClick={() => { if (!isChecked) return; - setIsStatusDropdownOpen((prev) => !prev); + closeAllDropdowns(); + setIsStatusDropdownOpen(true); }} > 상태변경 @@ -420,7 +437,8 @@ const ApplicantsTab = () => { } }} onToggle={() => { - setOpen((prev) => !prev); + closeAllDropdowns(); + setOpen(true); }} open={open} style={{ width: '0' }} @@ -429,7 +447,10 @@ const ApplicantsTab = () => { setOpen((prev) => !prev)} + onClick={() => { + closeAllDropdowns(); + setOpen(true); + }} /> From 9b6c2e545993ab7f51e1c8f7c672b1a38685f5ef Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Fri, 3 Oct 2025 20:42:51 +0900 Subject: [PATCH 15/28] =?UTF-8?q?refactor:=20=EA=B3=BC=ED=95=9C=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=EC=A0=95=EC=9D=98=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/CustomDropDown/CustomDropDown.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx b/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx index 46211d9e0..74fa93190 100644 --- a/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx +++ b/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx @@ -14,14 +14,14 @@ interface CustomDropDownContextProps { handleSelect: (value: TValue) => void; } -interface CustomDropDownProps - extends Omit, 'onSelect'> { +interface CustomDropDownProps { children: ReactNode; options: DropdownOption[]; selected?: TValue; onSelect: (value: TValue) => void; open: boolean; onToggle: () => void; + style?: React.CSSProperties; } interface ItemProps { @@ -84,7 +84,7 @@ export function CustomDropDown({ onSelect, open, onToggle, - ...rest + style, }: CustomDropDownProps) { const handleSelect = (value: T) => { onSelect(value); @@ -98,7 +98,9 @@ export function CustomDropDown({ return ( - {children} + + {children} + ); } From f63681d6132fe7562557f7b00d3a69ddd6af1db6 Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Fri, 3 Oct 2025 21:10:28 +0900 Subject: [PATCH 16/28] =?UTF-8?q?refactor:=20onToggle=20=ED=95=A8=EC=88=98?= =?UTF-8?q?=EC=97=90=20isOpen=20=EC=9D=B8=EC=9E=90=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=EC=8B=A4=ED=96=89=20=EC=9C=84=EC=B9=98=20=EC=9E=AC?= =?UTF-8?q?=EC=84=A4=EA=B3=84=20-=20onToggle=EC=97=90=20isOpen=EC=9D=B8?= =?UTF-8?q?=EC=9E=90=EB=A5=BC=20=EC=A3=BC=EC=96=B4=20=EC=97=AC=EB=8B=AB?= =?UTF-8?q?=EC=9D=84=EC=8B=9C=20=EB=B3=80=EA=B2=BD=EB=90=9C=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EC=97=90=20=EC=9D=98=EC=A1=B4=ED=95=98=EC=A7=80?= =?UTF-8?q?=EC=95=8A=EA=B3=A0=20=EA=B7=B8=EC=8B=9C=EC=A0=90=EC=9D=98=20?= =?UTF-8?q?=EA=B0=92=EC=9D=84=20=EB=B3=B4=EC=A1=B4=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=EB=8F=85=EB=A6=BD=EC=A0=81=EC=9C=BC=EB=A1=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=ED=95=98=EA=B2=8C=20=EB=B3=80=EA=B2=BD=ED=95=98?= =?UTF-8?q?=EC=98=80=EC=8A=B5=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - onToggle을 Trigger를 클릭시에 실행되게 변경하여 Trigger 컴포넌트 추가시 추가적인 event 핸들러 없이 여닫는게 가능하도록 재설계하였습니다 --- .../common/CustomDropDown/CustomDropDown.tsx | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx b/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx index 74fa93190..b40a0b580 100644 --- a/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx +++ b/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx @@ -10,7 +10,7 @@ interface CustomDropDownContextProps { open: boolean; selected: TValue; options: DropdownOption[]; - onToggle: () => void; + onToggle: (isOpen: boolean) => void; handleSelect: (value: TValue) => void; } @@ -20,7 +20,7 @@ interface CustomDropDownProps { selected?: TValue; onSelect: (value: TValue) => void; open: boolean; - onToggle: () => void; + onToggle: (isOpen: boolean) => void; style?: React.CSSProperties; } @@ -45,7 +45,16 @@ const useDropDownContext = () => { }; const Trigger = ({ children }: { children: ReactNode }) => { - return <>{children}; + const { onToggle, open } = useDropDownContext(); + return ( +
{ + onToggle(open); + }} + > + {children} +
+ ); }; interface MenuProps { @@ -88,7 +97,7 @@ export function CustomDropDown({ }: CustomDropDownProps) { const handleSelect = (value: T) => { onSelect(value); - onToggle(); + onToggle(open); }; const value = useMemo( @@ -98,9 +107,7 @@ export function CustomDropDown({ return ( - - {children} - + {children} ); } From 1cd6e7f14054a37af8696cef13c20c6253cc30c8 Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Fri, 3 Oct 2025 21:10:50 +0900 Subject: [PATCH 17/28] =?UTF-8?q?style:=20=EB=93=9C=EB=A1=AD=EB=8B=A4?= =?UTF-8?q?=EC=9A=B4=20=EB=A9=94=EB=89=B4=20=ED=98=B8=EB=B2=84=EC=8B=9C=20?= =?UTF-8?q?=EB=A7=88=EC=9A=B0=EC=8A=A4=ED=8F=AC=EC=9D=B8=ED=84=B0=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/common/CustomDropDown/CustomDropDown.styles.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts b/frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts index 33c8dec26..9679508bf 100644 --- a/frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts +++ b/frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts @@ -3,6 +3,7 @@ import styled from 'styled-components'; export const DropDownWrapper = styled.div` position: relative; width: 100%; + cursor: pointer; `; interface OptionListProps { From b24b4cbecb9b1ca66fbd829e08f735efffbc4c41 Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Fri, 3 Oct 2025 21:11:06 +0900 Subject: [PATCH 18/28] =?UTF-8?q?refactor:=20=EC=9E=AC=EC=84=A4=EA=B3=84?= =?UTF-8?q?=EB=90=9C=20dropdown=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tabs/ApplicantsTab/ApplicantsTab.tsx | 51 +++++++------------ 1 file changed, 18 insertions(+), 33 deletions(-) diff --git a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx index 6d9212c43..5ebac7526 100644 --- a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx +++ b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx @@ -262,19 +262,15 @@ const ApplicantsTab = () => { setSelectedFilter(value); }} open={isFilterOpen} - onToggle={() => { + onToggle={(isOpen) => { closeAllDropdowns(); - setIsFilterOpen(true); + setIsFilterOpen(!isOpen); }} + selected={selectedFilter} style={{ width: '101px' }} > - { - closeAllDropdowns(); - setIsFilterOpen(true); - }} - > + { filterOptions.find( (option) => option.value === selectedFilter, @@ -312,19 +308,15 @@ const ApplicantsTab = () => { } }} open={isSortOpen} - onToggle={() => { + selected={selectedSort.value} + onToggle={(isOpen) => { closeAllDropdowns(); - setIsSortOpen(true); + setIsSortOpen(!isOpen); }} style={{ width: '101px' }} > - { - closeAllDropdowns(); - setIsSortOpen(true); - }} - > + {selectedSort.label} @@ -354,21 +346,14 @@ const ApplicantsTab = () => { updateAllApplicants(status as ApplicationStatus) } open={isStatusDropdownOpen} - onToggle={() => { + onToggle={(isOpen) => { if (!isChecked) return; closeAllDropdowns(); - setIsStatusDropdownOpen(true); + setIsStatusDropdownOpen(!isOpen); }} > - { - if (!isChecked) return; - closeAllDropdowns(); - setIsStatusDropdownOpen(true); - }} - > + 상태변경 @@ -436,9 +421,9 @@ const ApplicantsTab = () => { ); } }} - onToggle={() => { + onToggle={(isOpen) => { closeAllDropdowns(); - setOpen(true); + setOpen(!isOpen); }} open={open} style={{ width: '0' }} @@ -447,15 +432,15 @@ const ApplicantsTab = () => { { - closeAllDropdowns(); - setOpen(true); - }} /> {filterOptions.map(({ value, label }) => ( - + {label} ))} From 9bc9af13312365ee6922f2653730ece92f925ee7 Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Fri, 3 Oct 2025 21:17:12 +0900 Subject: [PATCH 19/28] =?UTF-8?q?refactor:=20=EC=9E=AC=EC=84=A4=EA=B3=84?= =?UTF-8?q?=EB=90=9C=20dropdown=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QuestionBuilder/QuestionBuilder.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.tsx b/frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.tsx index e889dc833..1298cc08c 100644 --- a/frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.tsx +++ b/frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.tsx @@ -142,20 +142,24 @@ const QuestionBuilder = ({ onTypeChange?.(value as QuestionType); }} open={isDropdownOpen} - onToggle={() => setIsDropdownOpen((prev) => !prev)} + onToggle={(isOpen) => setIsDropdownOpen(!isOpen)} > - setIsDropdownOpen((prev) => !prev)} - > + {selectedLabel} {DROPDOWN_OPTIONS.map(({ label, value }) => ( - + {label} ))} From e3711ec6391d9c4dc1aa473bd03bb210b99c11de Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Fri, 3 Oct 2025 21:23:38 +0900 Subject: [PATCH 20/28] refactor: refactor to transient props --- .../CustomDropDown/CustomDropDown.styles.ts | 24 +++++++++---------- .../common/CustomDropDown/CustomDropDown.tsx | 4 ++-- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts b/frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts index 9679508bf..022011b75 100644 --- a/frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts +++ b/frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts @@ -6,18 +6,16 @@ export const DropDownWrapper = styled.div` cursor: pointer; `; -interface OptionListProps { - top?: string; - width?: string; - right?: string; -} - -export const OptionList = styled.ul` +export const OptionList = styled.ul<{ + $top?: string; + $width?: string; + $right?: string; +}>` position: absolute; - top: ${({ top }) => top || '110%'}; - left: ${({ right }) => (right ? 'auto' : '0')}; - width: ${({ width }) => width || '100%'}; - right: ${({ right }) => right || 'auto'}; + top: ${({ $top }) => $top || '110%'}; + left: ${({ $right }) => ($right ? 'auto' : '0')}; + width: ${({ $width }) => $width || '100%'}; + right: ${({ $right }) => $right || 'auto'}; border-radius: 6px; border: 1px solid #dcdcdc; background: #fff; @@ -28,14 +26,14 @@ export const OptionList = styled.ul` list-style: none; `; -export const OptionItem = styled.li<{ isSelected: boolean }>` +export const OptionItem = styled.li<{ $isSelected: boolean }>` display: flex; align-items: center; justify-content: center; text-align: center; font-weight: 600; color: #787878; - background-color: ${({ isSelected }) => (isSelected ? '#f5f5f5' : '#fff')}; + background-color: ${({ $isSelected }) => ($isSelected ? '#f5f5f5' : '#fff')}; cursor: pointer; padding: 8px 13px; height: 27px; diff --git a/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx b/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx index b40a0b580..e72f1c3ff 100644 --- a/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx +++ b/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx @@ -67,7 +67,7 @@ interface MenuProps { const Menu = ({ children, top, width, right }: MenuProps) => { const { open } = useDropDownContext(); return open ? ( - + {children} ) : null; @@ -77,7 +77,7 @@ const Item = ({ value, children, style }: ItemProps) => { const { selected, handleSelect } = useDropDownContext(); return ( handleSelect(value)} style={style} > From 65c6fc9d2289923bbe6731e1ddcde5d8553ccb27 Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Fri, 3 Oct 2025 21:27:41 +0900 Subject: [PATCH 21/28] =?UTF-8?q?fix:=20dropdowncontextprop=EA=B3=BC=20dro?= =?UTF-8?q?pdownprop=EC=9D=98=20type=20=EB=B6=88=EC=9D=BC=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/common/CustomDropDown/CustomDropDown.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx b/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx index e72f1c3ff..5e8c7d293 100644 --- a/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx +++ b/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx @@ -8,7 +8,7 @@ interface DropdownOption { interface CustomDropDownContextProps { open: boolean; - selected: TValue; + selected?: TValue; options: DropdownOption[]; onToggle: (isOpen: boolean) => void; handleSelect: (value: TValue) => void; From bdc96aca6e0dcf37da19c4a6bd01c909d5fb7941 Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Fri, 3 Oct 2025 21:31:15 +0900 Subject: [PATCH 22/28] refactor: add default aria --- .../src/components/common/CustomDropDown/CustomDropDown.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx b/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx index 5e8c7d293..b2f378be0 100644 --- a/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx +++ b/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx @@ -67,7 +67,7 @@ interface MenuProps { const Menu = ({ children, top, width, right }: MenuProps) => { const { open } = useDropDownContext(); return open ? ( - + {children} ) : null; From d8843caf4a046bc34a4f88db8cf193d076061139 Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Fri, 3 Oct 2025 21:36:33 +0900 Subject: [PATCH 23/28] =?UTF-8?q?refactor:=20aria=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=EC=A0=9C=EB=84=A4=EB=A6=AD=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=A0=9C=ED=95=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/common/CustomDropDown/CustomDropDown.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx b/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx index b2f378be0..db9461ab7 100644 --- a/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx +++ b/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx @@ -73,10 +73,15 @@ const Menu = ({ children, top, width, right }: MenuProps) => { ) : null; }; -const Item = ({ value, children, style }: ItemProps) => { +const Item = ({ + value, + children, + style, +}: ItemProps) => { const { selected, handleSelect } = useDropDownContext(); return ( handleSelect(value)} style={style} From ac9ab83020fd29f25d458b9cfe9b99cb5c30eccf Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Fri, 3 Oct 2025 21:45:38 +0900 Subject: [PATCH 24/28] =?UTF-8?q?refactor:=20remove=20memoization=20-=20?= =?UTF-8?q?=EB=93=9C=EB=A1=AD=EB=8B=A4=EC=9A=B4=20=EB=A9=94=EB=89=B4?= =?UTF-8?q?=EB=8A=94=20open=20=EC=83=81=ED=83=9C=EB=B3=80=ED=99=94?= =?UTF-8?q?=EA=B0=80=20=EC=9E=A6=EA=B8=B0=EB=95=8C=EB=AC=B8=EC=97=90=20?= =?UTF-8?q?=EB=A9=94=EB=AA=A8=EC=9D=B4=EC=A0=9C=EC=9D=B4=EC=85=98=EC=9D=84?= =?UTF-8?q?=20=ED=95=98=EB=8A=94=EA=B2=83=EC=9D=80=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EA=B3=84=EC=82=B0=EC=9D=84=20=EC=95=BC?= =?UTF-8?q?=EA=B8=B0=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/common/CustomDropDown/CustomDropDown.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx b/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx index db9461ab7..73610f76f 100644 --- a/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx +++ b/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx @@ -1,4 +1,4 @@ -import React, { createContext, useContext, useMemo, ReactNode } from 'react'; +import React, { createContext, useContext, ReactNode } from 'react'; import * as Styled from './CustomDropDown.styles'; interface DropdownOption { @@ -105,10 +105,7 @@ export function CustomDropDown({ onToggle(open); }; - const value = useMemo( - () => ({ open, selected, options, onToggle, handleSelect }), - [open, selected, options, onToggle, onSelect], - ); + const value = { open, selected, options, onToggle, handleSelect }; return ( From a1c03519a90ef3ba113fbd4a42841674d4636711 Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Fri, 3 Oct 2025 21:51:39 +0900 Subject: [PATCH 25/28] =?UTF-8?q?refactor:=20=EC=9E=98=EB=AA=BB=EB=90=9C?= =?UTF-8?q?=20=ED=83=80=EC=9E=85=EC=BA=90=EC=8A=A4=ED=8C=85=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx index 5ebac7526..755b98d22 100644 --- a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx +++ b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx @@ -22,7 +22,10 @@ const ApplicantsTab = () => { const filterOptions = ['ALL', ...Object.values(ApplicationStatus)].map( (status) => ({ value: status, - label: mapStatusToGroup(status as ApplicationStatus).label, + label: + status === 'ALL' + ? '전체' + : mapStatusToGroup(status as ApplicationStatus).label, }), ); From e476c11596a6db54a658dd96417b9c7210ab6cf1 Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Fri, 3 Oct 2025 22:06:42 +0900 Subject: [PATCH 26/28] =?UTF-8?q?refactor:=20as=20const=20=EB=A5=BC=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EC=97=AC=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=95=88=EC=A0=84=EC=84=B1=20=EC=A6=9D=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/common/CustomDropDown/CustomDropDown.tsx | 4 ++-- .../pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx b/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx index 73610f76f..df53542ce 100644 --- a/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx +++ b/frontend/src/components/common/CustomDropDown/CustomDropDown.tsx @@ -9,14 +9,14 @@ interface DropdownOption { interface CustomDropDownContextProps { open: boolean; selected?: TValue; - options: DropdownOption[]; + options: readonly DropdownOption[]; onToggle: (isOpen: boolean) => void; handleSelect: (value: TValue) => void; } interface CustomDropDownProps { children: ReactNode; - options: DropdownOption[]; + options: readonly DropdownOption[]; selected?: TValue; onSelect: (value: TValue) => void; open: boolean; diff --git a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx index 755b98d22..220e06f86 100644 --- a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx +++ b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx @@ -32,7 +32,7 @@ const ApplicantsTab = () => { const sortOptions = [ { value: 'date', label: '제출순' }, { value: 'name', label: '이름순' }, - ]; + ] as const; const navigate = useNavigate(); const { clubId, applicantsData } = useAdminClubContext(); @@ -47,7 +47,9 @@ const ApplicantsTab = () => { const [isFilterOpen, setIsFilterOpen] = useState(false); const [selectedFilter, setSelectedFilter] = useState('ALL'); const [isSortOpen, setIsSortOpen] = useState(false); - const [selectedSort, setSelectedSort] = useState(sortOptions[0]); + const [selectedSort, setSelectedSort] = useState< + (typeof sortOptions)[number] + >(sortOptions[0]); // 모든 드롭다운을 닫는 함수 const closeAllDropdowns = () => { From 972d708bf092640b79f0227a575b307a4d27c848 Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Fri, 3 Oct 2025 22:13:33 +0900 Subject: [PATCH 27/28] refactor: add optional chaining --- .../pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx index 220e06f86..f2f65bfe8 100644 --- a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx +++ b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx @@ -75,15 +75,15 @@ const ApplicantsTab = () => { if (keyword.trim()) { applicants = applicants.filter((user: Applicant) => - user.answers[0].value - .toLowerCase() + user.answers?.[0]?.value + ?.toLowerCase() .includes(keyword.trim().toLowerCase()), ); } if (selectedSort.value === 'name') { applicants.sort((a, b) => - a.answers[0].value.localeCompare(b.answers[0].value), + a.answers?.[0]?.value.localeCompare(b.answers?.[0]?.value), ); } else { applicants.sort( From 3af21b05758f33c2003dc66f883a15de13c21583 Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Fri, 3 Oct 2025 22:21:01 +0900 Subject: [PATCH 28/28] =?UTF-8?q?refactor:=20dropdownRef=20=EC=98=A4?= =?UTF-8?q?=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx index f2f65bfe8..513b4d229 100644 --- a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx +++ b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx @@ -60,7 +60,7 @@ const ApplicantsTab = () => { }; const { mutate: deleteApplicants } = useDeleteApplicants(clubId!); const { mutate: updateDetailApplicants } = useUpdateApplicant(clubId!); - const dropdwonRef = useRef>([]); + const dropdownRef = useRef>([]); const filteredApplicants = useMemo(() => { if (!applicantsData?.applicants) return []; @@ -99,8 +99,8 @@ const ApplicantsTab = () => { const handleOutsideClick = (e: MouseEvent) => { const target = e.target as Node; if ( - dropdwonRef.current && - !dropdwonRef.current.some((ref) => ref && ref.contains(target)) + dropdownRef.current && + !dropdownRef.current.some((ref) => ref && ref.contains(target)) ) { closeAllDropdowns(); } @@ -258,7 +258,7 @@ const ApplicantsTab = () => { { - dropdwonRef.current[2] = el; + dropdownRef.current[2] = el; }} > { { - dropdwonRef.current[3] = el; + dropdownRef.current[3] = el; }} > { { - dropdwonRef.current[0] = el; + dropdownRef.current[0] = el; }} > { { - dropdwonRef.current[1] = el; + dropdownRef.current[1] = el; }} >