[feature] 지원서 상태에 따라 분류하고 이름, 제출 순으로 정렬 할 수 있다.#754
Conversation
Compound 패턴으로 보다 확장성있게 추가하였습니다
ApplicatnsTab에서 ALL 속성을 사용하기위해 변경했습니다.
useRef를 array 로 초기화하여 ref를 여러개 선언하지않고 한개의 ref로 여러개의 엘리먼트를 핸들 할 수 있습니다.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning
|
| Cohort / File(s) | Summary |
|---|---|
CustomDropDown 리팩터링frontend/src/components/common/CustomDropDown/CustomDropDown.tsx, frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts |
컴포넌트를 제네릭() 외부 제어 API로 변경(Props: open, onToggle, selected, onSelect, options). 내부 상태 제거, 컨텍스트 도입, 합성 서브컴포넌트 Trigger/Menu/Item 추가. 스타일 변경: Selected, Icon 제거; OptionList에 $top/$width/$right props 도입; OptionItem prop명 isSelected→$isSelected 및 레이아웃/시각 업데이트. |
QuestionBuilder 적용 및 스타일 추가frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.tsx, frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.styles.ts |
기존 CustomDropdown 사용을 새 CustomDropDown(Trigger/Menu/Item) 구조로 교체. 로컬 isDropdownOpen으로 제어하고 드롭다운 아이콘 추가. 스타일 파일에 Selected(open prop) 및 Icon 컴포넌트 추가. |
ApplicantsTab 통합 및 리스트 로직 추가frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx, frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts |
상태/필터/정렬 드롭다운을 CustomDropDown으로 전환. 다중 드롭다운(open) 상태 관리 및 외부 클릭으로 일괄 닫기 처리(refs 배열). 키워드 검색, 상태 필터링, 이름/제출일 정렬 로직 추가. 스타일: ApplicantFilterSelect를 styled.select→styled.div로 변경 및 화살표 위치 조정. |
유틸: 상태→그룹 매핑 변경frontend/src/utils/mapStatusToGroup.ts |
임포트 포맷 정리 및 기본(default) 분기 라벨을 '전체'로 변경(기능적 변경). 함수 시그니처는 유지됨. |
Sequence Diagram(s)
sequenceDiagram
autonumber
actor U as 사용자
participant DD as CustomDropDown (Provider)
participant Tr as Trigger
participant Mn as Menu
participant It as Item
participant C as Caller (QuestionBuilder / ApplicantsTab)
U->>Tr: 클릭
Tr-->>DD: onToggle(isOpen)
DD-->>Mn: open=true 이면 렌더
U->>It: 항목 선택
It-->>DD: handleSelect(value)
DD-->>C: onSelect(value)
C-->>DD: onToggle(false)
sequenceDiagram
autonumber
actor A as 관리자
participant AT as ApplicantsTab
participant DDs as CustomDropDowns(Status/Filter/Sort)
participant Utl as mapStatusToGroup
A->>DDs: 옵션 선택
DDs-->>AT: onSelect(value)
AT->>AT: 키워드 필터 적용
AT->>AT: 상태/필터 적용
AT->>Utl: mapStatusToGroup(status)
Utl-->>AT: {status,label}
AT->>AT: 정렬(이름/제출일)
AT-->>A: 필터/정렬된 목록 렌더
Estimated code review effort
🎯 4 (Complex) | ⏱️ ~60 minutes
Possibly related issues
- [feature] MOA-216 지원서 상태에 따라 분류하고 이름, 제출 순으로 정렬 할 수 있다. #721: ApplicantsTab에 상태 기반 필터링 및 이름/제출일 정렬을 추가하는 요구사항과 직접적으로 일치함 — 본 PR의 ApplicantsTab 필터/정렬/검색 구현이 해당 이슈 목표를 충족할 가능성 높음.
Possibly related PRs
- [feature] CustomDropdown 컴포넌트 구현 및 질문 입력 필드 UX 개선 #466: 동일한 CustomDropDown 컴포넌트 파일들(styles + 구현)에 대한 API/스타일 재구성과 직접적인 코드 수준 연관.
- [Release] v1.0.9 #627: CustomDropDown를 제네릭·컨텍스트 방식으로 도입하는 리팩터와 기능 및 파일 변경이 중첩됨.
- [feature] 지원자 목록 디자인 적용 #654: ApplicantsTab의 필터 셀렉트 스타일/위치 조정 및 드롭다운 사용 변경과 직접적 관련.
Suggested labels
🔨 Refactor
Suggested reviewers
- oesnuj
- suhyun113
- seongwon030
- Zepelown
Pre-merge checks and finishing touches
❌ Failed checks (1 warning)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Out of Scope Changes Check | 이 PR은 MOA-216의 필터링·정렬 기능 범위를 넘어 CustomDropDown 컴포넌트 전반의 리팩터링과 QuestionBuilder 및 기타 스타일 모듈 변경 등을 포함하여 이슈 범위와 직접 관련 없는 대규모 구조 변경이 섞여 있습니다. | 리팩터링된 CustomDropDown 및 스타일 변경은 별도 PR로 분리하여 MOA-216 이슈의 필터링·정렬 기능 구현과 구조 변경을 명확히 구분하시기를 권장합니다. |
✅ Passed checks (4 passed)
| Check name | Status | Explanation |
|---|---|---|
| Title Check | ✅ Passed | 제목이 PR의 주요 변경 사항인 지원서 상태별 필터링과 이름·제출 순 정렬 기능을 명확히 요약하고 있어 맥락에 부합하며 깔끔합니다. |
| Linked Issues Check | ✅ Passed | ApplicantsTab 컴포넌트에서 지원서 상태별 필터링과 이름·제출 순 정렬 로직을 MOA-216 체크리스트에 맞게 구현하여 링크된 이슈의 주요 요구사항을 충족하고 있습니다. |
| Docstring Coverage | ✅ Passed | No functions found in the changes. Docstring coverage check skipped. |
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
✨ Finishing touches
- 📝 Generate docstrings
🧪 Generate unit tests (beta)
- Create PR with unit tests
- Post copyable unit tests in a comment
- Commit unit tests in branch
feature/#721-sort-applicants-MOA-216
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands and usage tips.
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.tsx (1)
29-31: Hook가 조건부로 호출됩니다 — 즉시 수정 필요frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.tsx (라인 29–31): 해당 early return 때문에 라인 37(useState)과 라인 39(useEffect)가 조건부로 호출됩니다. React 훅 규칙 위반으로 런타임 오류가 발생할 수 있습니다.
수정(권장 순):
- 권장: guard 제거.
- 대안: 모든 hook 선언(예: useState, useEffect) 이후로 guard를 이동.
권장 변경 예시:
- if (!(type in QUESTION_LABEL_MAP)) { - return null; - } + // 타입은 QuestionType으로 보장되지만, 방어가 필요하면 hooks 이후에 처리하세요.혹은 hooks 선언 후:
// hooks 선언들... if (!(type in QUESTION_LABEL_MAP)) return null;frontend/src/components/common/CustomDropDown/CustomDropDown.tsx (1)
80-109: TS: 함수 선언에 정적 서브컴포넌트 부착(Trigger/Menu/Item)은 타입이 노출되지 않아 사용처에서 TS2339 발생 가능function 선언에 프로퍼티를 동적으로 붙이면 타입 시스템에는 반영되지 않습니다. 조합형 컴포넌트는 구현 함수와 정적 프로퍼티를 Object.assign으로 결합하고, 교차 타입을 부여해야 합니다.
아래처럼 구현 함수를 분리하고, 정적 필드를 타입 안전하게 결합해 주세요.
-export function CustomDropDown<T extends string | number = string>({ +function CustomDropDownImpl<T extends string | number = string>({ children, options, selected, onSelect, open, onToggle, ...rest -}: CustomDropDownProps<T>) { +}: CustomDropDownProps<T>) { @@ - const value = useMemo( - () => ({ open, selected, options, onToggle, handleSelect }), - [open, selected, options, onToggle, onSelect], - ); + const contextValue = useMemo( + () => ({ open, selected, options, onToggle, handleSelect }), + [open, selected, options, onToggle, handleSelect], + ); @@ - <CustomDropDownContext.Provider value={value}> + <CustomDropDownContext.Provider value={contextValue}> <Styled.DropDownWrapper {...rest}>{children}</Styled.DropDownWrapper> </CustomDropDownContext.Provider> ); } -CustomDropDown.Trigger = Trigger; -CustomDropDown.Menu = Menu; -CustomDropDown.Item = Item; +type CustomDropDownComposition = { + Trigger: typeof Trigger; + Menu: typeof Menu; + Item: typeof Item; +}; +export type CustomDropDownComponent = (<T extends string | number = string>( + props: CustomDropDownProps<T> +) => JSX.Element) & CustomDropDownComposition; + +export const CustomDropDown = Object.assign(CustomDropDownImpl, { + Trigger, + Menu, + Item, +}) as CustomDropDownComponent;
🧹 Nitpick comments (11)
frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.styles.ts (1)
78-101: 아이콘 기준점 누락 및 DOM 속성 누출 가능성
Icon이position: absolute인데Selected에position: relative가 없어 배치 기준이 상위 컨텍스트로 밀릴 수 있습니다.open(boolean) 커스텀 prop이 그대로 DOM으로 전달될 수 있습니다. styled-components에서는 transient prop($open)을 권장합니다.아래처럼 수정 제안:
-export const Selected = styled.div<{ open: boolean }>` +export const Selected = styled.div<{ $open: boolean }>` + position: relative; padding: 12px 16px; border-radius: 0.375rem; - background: ${({ open }) => (open ? '#fff' : '#f5f5f5')}; + background: ${({ $open }) => ($open ? '#fff' : '#f5f5f5')}; color: #787878; font-size: 0.875rem; font-weight: 600; cursor: pointer; - border: 1px solid ${({ open }) => (open ? '#c5c5c5' : 'transparent')}; + border: 1px solid ${({ $open }) => ($open ? '#c5c5c5' : 'transparent')}; transition: border-color 0.2s ease, background-color 0.2s ease; user-select: none; `;사용처에서도
open→$open으로 교체해야 합니다.frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts (2)
156-166:styled.div에 불필요한 form 전용 스타일이 포함되어 있습니다
-webkit-appearance,-moz-appearance,appearance는 form 요소 대상 속성입니다.div에서는 무의미하며 제거 권장. 또한 클릭 트리거 용도라면cursor: pointer추가를 권장합니다.export const ApplicantFilterSelect = styled.div` display: flex; align-items: center; height: 35px; padding: 4px 32px 4px 14px; border-radius: 8px; border: none; background: var(--f5, #f5f5f5); font-size: 16px; color: #000; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; + cursor: pointer; &:hover { background: #ebebeb; } `;
288-290: 아이콘 절대 위치 값 매직 넘버 정리 제안
right: -18px; top: -7px;는 컨테이너 변경 시 쉽게 깨집니다. 상수로 분리하거나 컨테이너 기준(패딩/라인-height) 기반 계산으로 전환을 고려하세요.frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts (1)
30-38: 중복된 padding 선언
padding: 10px;와padding: 8px 13px;가 동시에 존재합니다. 후자가 최종 적용되지만 혼동을 유발합니다. 하나로 통일하세요.export const OptionItem = styled.li<{ isSelected: boolean }>` text-align: center; - padding: 10px; + /* padding: 10px; 제거 */ font-weight: 600; color: #787878; background-color: ${({ isSelected }) => (isSelected ? '#f5f5f5' : '#fff')}; cursor: pointer; padding: 8px 13px;frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.tsx (2)
124-128: 라벨 안전값(fallback) 추가 제안옵션 조회 실패 시
selectedLabel이undefined가 될 수 있습니다. 기본 라벨을 지정해 UI 깜빡임을 방지하세요.- const selectedLabel = DROPDOWN_OPTIONS.find( + const selectedLabel = DROPDOWN_OPTIONS.find( (option) => option.value === selectedType, )?.label; + const displayLabel = selectedLabel ?? '질문 유형';그리고 사용처에서
selectedLabel→displayLabel로 교체.
147-155: 드롭다운 트리거 접근성 보완트리거가
div여서 스크린리더/키보드 접근성이 떨어집니다.role="button",tabIndex=0,aria-expanded/aria-haspopup를 부여하세요.- <Styled.Selected - open={isDropdownOpen} - onClick={() => setIsDropdownOpen((prev) => !prev)} - > + <Styled.Selected + $open={isDropdownOpen} + onClick={() => setIsDropdownOpen((prev) => !prev)} + role="button" + tabIndex={0} + aria-haspopup="listbox" + aria-expanded={isDropdownOpen} + >(
$open으로의 prop명 변경은 styles 쪽 수정과 함께 적용)frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (2)
294-325: 정렬 선택 상태는 값만 보관하고 라벨은 매핑으로 표시객체 형태 보관 대신
SortValue만 상태로 저장하면 단순/안전합니다.- onSelect={(value) => { - const selected = sortOptions.find( - (option) => option.value === value, - ); - if (selected) { - setSelectedSort(selected); - } - setIsSortOpen(false); - }} + onSelect={(value) => { + setSelectedSort(value as SortValue); + setIsSortOpen(false); + }} ... - {selectedSort.label} + {selectedSort === 'date' ? '제출순' : '이름순'}Also applies to: 311-314
50-50: 오타:dropdwonRef→dropdownRef의미 전달과 검색 편의성을 위해 변수명을 정정하세요.
- const dropdwonRef = useRef<Array<HTMLDivElement | null>>([]); + const dropdownRef = useRef<Array<HTMLDivElement | null>>([]);사용처 전체 치환 필요.
frontend/src/components/common/CustomDropDown/CustomDropDown.tsx (3)
1-1: 불필요한 default React import 및 누락된 유틸 import 정리JSX 자동 런타임이면 React default import는 불필요합니다. 동시에 Trigger 개선에 필요한
cloneElement,isValidElement,useCallback이 필요합니다. 위 다른 코멘트의 diff에 포함되어 있습니다.
9-15: Context value에 options 포함 — 불필요한 재렌더 트리거
options가 컨텍스트에 있지만 하위 컴포넌트에서 직접 사용하지 않습니다. 큰 배열의 식별자가 바뀔 때마다 Provider 이하가 전부 리렌더됩니다. 필요 없다면 컨텍스트에서 제거하고, 필요 시 상위에서 map하여 Item을 생성하세요.변경 전후로 ApplicantsTab/QuestionBuilder에서
useDropDownContext()를 통해 options를 직접 참조하는지 확인해 주세요.Also applies to: 94-97
23-24: API 명확성: onToggle → onOpenChange(boolean) 고려토글 기반 API는 외부 상태와 경쟁 조건(연속 토글)으로 인해 의도치 않은 열림 상태를 만들 수 있습니다.
onOpenChange(next: boolean)로의 전환을 추천합니다. 당장 바꾸기 어렵다면 추후 deprecate 계획을 남겨 주세요.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (7)
frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts(1 hunks)frontend/src/components/common/CustomDropDown/CustomDropDown.tsx(1 hunks)frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.styles.ts(1 hunks)frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.tsx(4 hunks)frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts(2 hunks)frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx(5 hunks)frontend/src/utils/mapStatusToGroup.ts(1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
frontend/**/*.{ts,tsx}
📄 CodeRabbit inference engine (frontend/.cursorrules)
frontend/**/*.{ts,tsx}: Replace magic numbers with named constants for clarity.
Replace complex or nested ternary operators with if/else statements or IIFEs for readability.
Assign complex boolean conditions to named variables.
Use consistent return types for similar functions and hooks.
Avoid hidden side effects; functions should only perform actions implied by their signature (Single Responsibility Principle).
Use unique, descriptive names for custom wrappers and functions to avoid ambiguity.
Define constants near related logic or ensure names link them clearly.
Files:
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.tsfrontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.styles.tsfrontend/src/components/common/CustomDropDown/CustomDropDown.tsxfrontend/src/components/common/CustomDropDown/CustomDropDown.styles.tsfrontend/src/utils/mapStatusToGroup.tsfrontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsxfrontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.tsx
frontend/**/*.tsx
📄 CodeRabbit inference engine (frontend/.cursorrules)
frontend/**/*.tsx: Abstract complex logic/interactions into dedicated components or higher-order components (HOCs).
Separate significantly different conditional UI/logic into distinct components.
Colocate simple, localized logic or use inline definitions to reduce context switching.
Choose field-level or form-level cohesion based on form requirements.
Break down broad state management into smaller, focused hooks or contexts.
Use component composition instead of props drilling.
Files:
frontend/src/components/common/CustomDropDown/CustomDropDown.tsxfrontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsxfrontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.tsx
🧬 Code graph analysis (2)
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (5)
frontend/src/constants/status.ts (1)
AVAILABLE_STATUSES(3-8)frontend/src/hooks/queries/applicants/useDeleteApplicants.ts (1)
useDeleteApplicants(4-17)frontend/src/hooks/queries/applicants/useUpdateApplicant.ts (1)
useUpdateApplicant(5-18)frontend/src/types/applicants.ts (1)
Applicant(18-24)frontend/src/components/common/CustomDropDown/CustomDropDown.tsx (1)
CustomDropDown(80-104)
frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.tsx (3)
frontend/src/constants/APPLICATION_FORM.ts (1)
DROPDOWN_OPTIONS(40-43)frontend/src/components/common/CustomDropDown/CustomDropDown.tsx (1)
CustomDropDown(80-104)frontend/src/types/application.ts (1)
QuestionType(3-3)
🪛 Biome (2.1.2)
frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.tsx
[error] 37-37: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
🔇 Additional comments (2)
frontend/src/utils/mapStatusToGroup.ts (1)
19-19: 'ALL' 라벨 처리 — mapStatusToGroup의 default에 의존 금지.
- 조치: ApplicantsTab에서 'ALL'은 별도 라벨링 처리하고, mapStatusToGroup은 ApplicationStatus(enum)만 받도록 유지하세요.
- 잘못된 입력 처리: mapStatusToGroup은 기본값으로 상태를 왜곡하지 말고, 필요 시 명시적 기본값('서류검토')으로 되돌리거나 잘못된 입력에 대해 예외를 던지도록 변경하세요.
- 위치: frontend/src/utils/mapStatusToGroup.ts (라인 19) — 현재: return { status: ApplicationStatus.SUBMITTED, label: '전체' }.
- 검증: 제공한 검색 명령(rg) 실행 결과 코드베이스에서 'ALL'을 인자로 넘기는 호출을 찾지 못했습니다(출력 없음). ApplicantsTab.tsx 등 관련 파일에서 수동으로 한 번 더 확인하세요.
frontend/src/components/common/CustomDropDown/CustomDropDown.tsx (1)
80-88: 검증 완료 — CustomDropDown 전환 반영됨레포 검색 결과 기존 'CustomDropdown' 사용 흔적은 없고 CustomDropDown 및 서브컴포넌트(Trigger/Menu/Item)를 사용하는 파일들이 확인되었습니다.
frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts
Outdated
Show resolved
Hide resolved
frontend/src/components/common/CustomDropDown/CustomDropDown.tsx
Outdated
Show resolved
Hide resolved
frontend/src/components/common/CustomDropDown/CustomDropDown.tsx
Outdated
Show resolved
Hide resolved
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx
Outdated
Show resolved
Hide resolved
seongwon030
left a comment
There was a problem hiding this comment.
커스텀 드롭다운을 위해 많이 고민하셨을 것 같습니다! 특히 컴파운드 패턴을 사용한 것이 좋았습니다.
PR 읽다가 설명이 좀 있었으면 좋았을 것들 생각나서 적어봐요.
- 전역 상태 사용
- 컴파운트 패턴을 사용한 동기
고생하셨습미당
frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts
Outdated
Show resolved
Hide resolved
frontend/src/components/common/CustomDropDown/CustomDropDown.tsx
Outdated
Show resolved
Hide resolved
| const CustomDropDownContext = createContext< | ||
| CustomDropDownContextProps<any> | undefined | ||
| >(undefined); |
There was a problem hiding this comment.
context 쓰신 이유가 각각의 드롭다운 상태를 독립적으로 유지하기 위해서인가요?
There was a problem hiding this comment.
컴포넌트안 공통된 상태들을 공유하기 위해서입니다!
There was a problem hiding this comment.
그럼 각각 다른 드롭다운 컴포넌트 두개를 생성하면 서로 상태를 공유하나요?
There was a problem hiding this comment.
서로 상태를 공유하지않고 각자 상태를 가지게됩니다
- onToggle에 isOpen인자를 주어 여닫을시 변경된 상태에 의존하지않고 그시점의 값을 보존하여 독립적으로 관리하게 변경하였습니다. - onToggle을 Trigger를 클릭시에 실행되게 변경하여 Trigger 컴포넌트 추가시 추가적인 event 핸들러 없이 여닫는게 가능하도록 재설계하였습니다
- 드롭다운 메뉴는 open 상태변화가 잦기때문에 메모이제이션을 하는것은 불필요한 계산을 야기함
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.tsx (1)
29-37: Hook을 조기 반환 이후에 호출하면 안 됩니다.29-31번 줄의 조기 반환 이후 37번 줄에서
useState를 호출하고 있습니다. React는 모든 렌더링에서 동일한 순서로 Hook이 호출되어야 하므로 이는 Rules of Hooks를 위반합니다.다음과 같이 수정하세요:
const QuestionBuilder = ({ ... }: QuestionBuilderProps) => { + const [selectionType, setSelectionType] = useState<'single' | 'multi'>( + type === 'MULTI_CHOICE' ? 'multi' : 'single', + ); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + if (!(type in QUESTION_LABEL_MAP)) { return null; } - const [selectionType, setSelectionType] = useState<'single' | 'multi'>( - type === 'MULTI_CHOICE' ? 'multi' : 'single', - ); - - const [isDropdownOpen, setIsDropdownOpen] = useState(false);
♻️ Duplicate comments (3)
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (2)
22-30: 'ALL' 값을 ApplicationStatus로 캐스팅하는 것은 타입 불일치입니다.26-28번 줄에서
'ALL'을ApplicationStatus로 캐스팅하고 있습니다.'ALL'은ApplicationStatus열거형의 일부가 아니므로 타입 안전성이 저하됩니다.다음과 같이 수정하세요:
+ type FilterValue = 'ALL' | ApplicationStatus; - const filterOptions = ['ALL', ...Object.values(ApplicationStatus)].map( - (status) => ({ - value: status, - label: - status === 'ALL' - ? '전체' - : mapStatusToGroup(status as ApplicationStatus).label, - }), - ); + const filterOptions: { value: FilterValue; label: string }[] = [ + { value: 'ALL', label: '전체' }, + ...Object.values(ApplicationStatus).map((status) => ({ + value: status, + label: mapStatusToGroup(status).label, + })), + ];
48-52: 상태 타입을 명시적인 유니온 타입으로 정의하세요.
selectedFilter와selectedSort가string과 객체로 관리되어 타입 안전성이 부족합니다.다음과 같이 수정하세요:
+ type FilterValue = 'ALL' | ApplicationStatus; + type SortValue = 'date' | 'name'; - const [selectedFilter, setSelectedFilter] = useState('ALL'); + const [selectedFilter, setSelectedFilter] = useState<FilterValue>('ALL'); const [isSortOpen, setIsSortOpen] = useState(false); - const [selectedSort, setSelectedSort] = useState< - (typeof sortOptions)[number] - >(sortOptions[0]); + const [selectedSort, setSelectedSort] = useState<SortValue>('date');이후 84번 줄의 조건을
selectedSort === 'name'으로, 325번 줄의 라벨 표시를sortOptions.find(o => o.value === selectedSort)?.label로 수정하세요.frontend/src/components/common/CustomDropDown/CustomDropDown.tsx (1)
76-92: Item에 키보드 선택 핸들러를 추가하세요.
role="option"은 추가되었으나 키보드(Enter/Space)로 항목을 선택할 수 없어 접근성이 제한됩니다.다음과 같이 개선하세요:
return ( <Styled.OptionItem role='option' + tabIndex={-1} + aria-selected={value === selected} $isSelected={value === selected} onClick={() => handleSelect(value)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleSelect(value); + } + }} style={style} > {children} </Styled.OptionItem> );
🧹 Nitpick comments (2)
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (1)
84-93: 정렬 로직에서 안전한 배열 접근을 개선하세요.86번 줄에서 옵셔널 체이닝을 추가한 것은 좋으나,
localeCompare의 인자가undefined일 수 있습니다.다음과 같이 더 안전하게 처리하세요:
+ const getName = (a: Applicant) => a.answers?.[0]?.value ?? ''; if (selectedSort.value === 'name') { applicants.sort((a, b) => - a.answers?.[0]?.value.localeCompare(b.answers?.[0]?.value), + getName(a).localeCompare(getName(b)), ); } else {frontend/src/components/common/CustomDropDown/CustomDropDown.tsx (1)
67-74: Menu에 aria-labelledby 또는 aria-label을 추가하세요.
role="listbox"는 추가되었으나, 스크린리더가 드롭다운의 목적을 이해할 수 있도록 레이블 연결이 필요합니다.부모 요소에 ID를 부여하고
aria-labelledby로 연결하거나, 간단히aria-label을 추가하세요:return open ? ( <Styled.OptionList role='listbox' + aria-label="옵션 선택" $top={top} $width={width} $right={right} > {children} </Styled.OptionList> ) : null;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts(1 hunks)frontend/src/components/common/CustomDropDown/CustomDropDown.tsx(1 hunks)frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.tsx(4 hunks)frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx(5 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
frontend/**/*.{ts,tsx}
📄 CodeRabbit inference engine (frontend/.cursorrules)
frontend/**/*.{ts,tsx}: Replace magic numbers with named constants for clarity.
Replace complex or nested ternary operators with if/else statements or IIFEs for readability.
Assign complex boolean conditions to named variables.
Use consistent return types for similar functions and hooks.
Avoid hidden side effects; functions should only perform actions implied by their signature (Single Responsibility Principle).
Use unique, descriptive names for custom wrappers and functions to avoid ambiguity.
Define constants near related logic or ensure names link them clearly.
Files:
frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.tsxfrontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsxfrontend/src/components/common/CustomDropDown/CustomDropDown.tsxfrontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts
frontend/**/*.tsx
📄 CodeRabbit inference engine (frontend/.cursorrules)
frontend/**/*.tsx: Abstract complex logic/interactions into dedicated components or higher-order components (HOCs).
Separate significantly different conditional UI/logic into distinct components.
Colocate simple, localized logic or use inline definitions to reduce context switching.
Choose field-level or form-level cohesion based on form requirements.
Break down broad state management into smaller, focused hooks or contexts.
Use component composition instead of props drilling.
Files:
frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.tsxfrontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsxfrontend/src/components/common/CustomDropDown/CustomDropDown.tsx
🧬 Code graph analysis (2)
frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.tsx (3)
frontend/src/constants/APPLICATION_FORM.ts (1)
DROPDOWN_OPTIONS(40-43)frontend/src/components/common/CustomDropDown/CustomDropDown.tsx (1)
CustomDropDown(94-115)frontend/src/types/application.ts (1)
QuestionType(3-3)
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (5)
frontend/src/constants/status.ts (1)
AVAILABLE_STATUSES(3-8)frontend/src/hooks/queries/applicants/useDeleteApplicants.ts (1)
useDeleteApplicants(4-17)frontend/src/hooks/queries/applicants/useUpdateApplicant.ts (1)
useUpdateApplicant(5-18)frontend/src/types/applicants.ts (1)
Applicant(18-24)frontend/src/components/common/CustomDropDown/CustomDropDown.tsx (1)
CustomDropDown(94-115)
🪛 Biome (2.1.2)
frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.tsx
[error] 37-37: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
🔇 Additional comments (10)
frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.tsx (2)
124-127: LGTM!드롭다운 라벨 표시를 위한 계산된 값이 명확하고 정확합니다.
138-167: LGTM!새로운 CustomDropDown 컴포넌트와 Compound 패턴 통합이 잘 구현되었습니다. 상태 관리와 이벤트 핸들링이 명확합니다.
frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts (2)
9-27: LGTM!Transient props (
$top,$width,$right)를 올바르게 사용하여 DOM 속성 누출을 방지했습니다. 과거 리뷰 피드백이 잘 반영되었습니다.
29-47: LGTM!
$isSelectedtransient prop을 사용하여 선택 상태를 안전하게 스타일링합니다.frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (5)
76-82: LGTM!옵셔널 체이닝(
?.)을 사용하여answers배열 접근을 안전하게 처리했습니다. 과거 리뷰 피드백이 반영되었습니다.
264-298: LGTM!필터 드롭다운이 새로운 CustomDropDown 컴포넌트를 사용하여 잘 구현되었습니다. 상태 관리와 외부 클릭 처리가 적절합니다.
305-340: LGTM!정렬 드롭다운 통합이 잘 구현되었습니다.
348-377: LGTM!상태 변경 드롭다운이 체크 여부에 따라 조건부로 활성화되는 로직이 명확합니다.
417-453: LGTM!전체 선택 드롭다운의 구현이 적절하며, transient props를 사용하여 메뉴 위치를 정확하게 지정했습니다.
frontend/src/components/common/CustomDropDown/CustomDropDown.tsx (1)
94-115: LGTM!제네릭 타입 처리와 Context Provider 구조가 잘 구현되었습니다. Compound 패턴을 통해 재사용성과 유연성이 향상되었습니다.
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (1)
498-498: 런타임 에러 위험: 안전하지 않은 배열 접근
item.answers[0].value는answers가 비어있거나answers[0]이 없을 때 런타임 에러를 발생시킵니다. 옵셔널 체이닝 또는 기본값을 사용하세요.다음과 같이 수정하세요:
<Styled.ApplicantTableCol> - {item.answers[0].value} + {item.answers?.[0]?.value ?? '이름 없음'} </Styled.ApplicantTableCol>
♻️ Duplicate comments (4)
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (4)
442-442: transient prop 이름 업데이트 필요
CustomDropDown.Menu가 transient props를 사용하도록 변경되었다면,top,width,right대신$top,$width,$right를 사용해야 합니다.다음과 같이 수정하세요:
- <CustomDropDown.Menu top='16px' width='110px' right='-84px'> + <CustomDropDown.Menu $top='16px' $width='110px' $right='-84px'>다른 위치(lines 287, 329)도 동일하게 확인하세요.
22-30: 타입 불일치:'ALL'을ApplicationStatus로 캐스팅
filterOptions에서'ALL'을ApplicationStatus로 캐스팅하는 것은 타입 안전성을 해칩니다.'ALL'옵션은 별도로 정의하고,ApplicationStatus값들만mapStatusToGroup에 전달하세요.다음과 같이 수정하세요:
- const filterOptions = ['ALL', ...Object.values(ApplicationStatus)].map( - (status) => ({ - value: status, - label: - status === 'ALL' - ? '전체' - : mapStatusToGroup(status as ApplicationStatus).label, - }), - ); + type FilterValue = 'ALL' | ApplicationStatus; + const filterOptions: { value: FilterValue; label: string }[] = [ + { value: 'ALL', label: '전체' }, + ...Object.values(ApplicationStatus).map((status) => ({ + value: status, + label: mapStatusToGroup(status).label, + })), + ];
45-52: 타입 정의 개선 필요
selectedFilter는string타입으로 정의되어 있어 불필요한 캐스팅과 타입 불안정성을 유발합니다.FilterValue유니온 타입으로 정의하세요.selectedSort는 현재 타입이 작동하지만,'date' | 'name'유니온으로 단순화하면 더 명확합니다.다음과 같이 수정하세요:
+ type FilterValue = 'ALL' | ApplicationStatus; + type SortValue = 'date' | 'name'; const [isFilterOpen, setIsFilterOpen] = useState(false); - const [selectedFilter, setSelectedFilter] = useState('ALL'); + const [selectedFilter, setSelectedFilter] = useState<FilterValue>('ALL'); const [isSortOpen, setIsSortOpen] = useState(false); - const [selectedSort, setSelectedSort] = useState< - (typeof sortOptions)[number] - >(sortOptions[0]); + const [selectedSort, setSelectedSort] = useState<SortValue>('date');아래 사용처에서도
selectedSort.value대신selectedSort를 직접 사용하고, 라벨은 매핑 객체로 표시하세요.
65-96: 정렬 로직에서 안전성 보완 필요
answers?.[0]?.value가undefined일 때localeCompare호출이 실패할 수 있습니다.String()래핑이나 기본값을 추가하여 안전성을 확보하세요. 또한,selectedSort.value대신selectedSort를 직접 사용하도록 리팩터링하세요 (위 타입 개선 코멘트 참조).다음과 같이 수정하세요:
const filteredApplicants = useMemo(() => { if (!applicantsData?.applicants) return []; let applicants = [...applicantsData.applicants]; if (selectedFilter !== 'ALL') { applicants = applicants.filter( (applicant) => applicant.status === selectedFilter, ); } + const getName = (a: Applicant) => a.answers?.[0]?.value ?? ''; if (keyword.trim()) { + const q = keyword.trim().toLowerCase(); applicants = applicants.filter((user: Applicant) => - user.answers?.[0]?.value - ?.toLowerCase() - .includes(keyword.trim().toLowerCase()), + getName(user).toLowerCase().includes(q), ); } - if (selectedSort.value === 'name') { + if (selectedSort === 'name') { applicants.sort((a, b) => - a.answers?.[0]?.value.localeCompare(b.answers?.[0]?.value), + String(getName(a)).localeCompare(String(getName(b))), ); } else { applicants.sort( (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), ); } return applicants; - }, [applicantsData, keyword, selectedFilter, selectedSort.value]); + }, [applicantsData, keyword, selectedFilter, selectedSort]);
🧹 Nitpick comments (1)
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (1)
300-341: 정렬 드롭다운 로직 리팩터링 필요
selectedSort를'date' | 'name'타입으로 변경한 후,onSelect에서 값만 설정하고 라벨은 매핑 객체로 표시하세요 (위 타입 개선 코멘트 참조).다음과 같이 수정하세요:
+ const sortLabelMap: Record<SortValue, string> = { + date: '제출순', + name: '이름순', + }; <CustomDropDown options={sortOptions} - onSelect={(value) => { - const selected = sortOptions.find( - (option) => option.value === value, - ); - if (selected) { - setSelectedSort(selected); - } - }} + onSelect={(value) => setSelectedSort(value as SortValue)} open={isSortOpen} - selected={selectedSort.value} + selected={selectedSort} onToggle={(isOpen) => { closeAllDropdowns(); setIsSortOpen(!isOpen); }} style={{ width: '101px' }} > <CustomDropDown.Trigger> <Styled.ApplicantFilterSelect> - {selectedSort.label} + {sortLabelMap[selectedSort]} </Styled.ApplicantFilterSelect> <Styled.Arrow src={selectIcon} /> </CustomDropDown.Trigger> <CustomDropDown.Menu top='115%'> {sortOptions.map(({ value, label }) => ( <CustomDropDown.Item key={value} value={value} style={{ fontSize: '12px' }} > {label} </CustomDropDown.Item> ))} </CustomDropDown.Menu> </CustomDropDown>
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx(5 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
frontend/**/*.{ts,tsx}
📄 CodeRabbit inference engine (frontend/.cursorrules)
frontend/**/*.{ts,tsx}: Replace magic numbers with named constants for clarity.
Replace complex or nested ternary operators with if/else statements or IIFEs for readability.
Assign complex boolean conditions to named variables.
Use consistent return types for similar functions and hooks.
Avoid hidden side effects; functions should only perform actions implied by their signature (Single Responsibility Principle).
Use unique, descriptive names for custom wrappers and functions to avoid ambiguity.
Define constants near related logic or ensure names link them clearly.
Files:
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx
frontend/**/*.tsx
📄 CodeRabbit inference engine (frontend/.cursorrules)
frontend/**/*.tsx: Abstract complex logic/interactions into dedicated components or higher-order components (HOCs).
Separate significantly different conditional UI/logic into distinct components.
Colocate simple, localized logic or use inline definitions to reduce context switching.
Choose field-level or form-level cohesion based on form requirements.
Break down broad state management into smaller, focused hooks or contexts.
Use component composition instead of props drilling.
Files:
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx
🧬 Code graph analysis (1)
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (5)
frontend/src/constants/status.ts (1)
AVAILABLE_STATUSES(3-8)frontend/src/hooks/queries/applicants/useDeleteApplicants.ts (1)
useDeleteApplicants(4-17)frontend/src/hooks/queries/applicants/useUpdateApplicant.ts (1)
useUpdateApplicant(5-18)frontend/src/types/applicants.ts (1)
Applicant(18-24)frontend/src/components/common/CustomDropDown/CustomDropDown.tsx (1)
CustomDropDown(94-115)
🔇 Additional comments (7)
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (7)
17-20: LGTM!
statusOptions정의가 명확하고 타입 안전합니다.
32-35: LGTM!
sortOptions정의가 명확하고as const로 타입이 좁혀져 있습니다.
55-60: LGTM!
closeAllDropdowns로직이 명확합니다.
63-63: LGTM!변수명 오타가 수정되었습니다.
98-113: LGTM!외부 클릭 처리 로직이 올바르게 구현되었습니다.
259-299: 필터 드롭다운 로직 확인
filterOptions타입이 수정되면 이 드롭다운도 타입 안전하게 작동합니다. 현재 로직은 올바릅니다.
filterOptions타입 수정 후 이 영역이 타입 안전하게 작동하는지 확인하세요.
343-378: LGTM!상태 변경 드롭다운 로직이 올바르게 구현되었습니다.
suhyun113
left a comment
There was a problem hiding this comment.
컴파운드 패턴에 대해 코드를 보고 많이 배웠습니다ㅏ
컴파운드 패턴에서도 하위 컴포넌트 간 상태 전달에서 Props를 쓰는 경우가 있나요? 주로 Context api를 사용하는 것 같은데 어떤 이유로 사용하셨는지 궁금합니다!
oesnuj
left a comment
There was a problem hiding this comment.
빡빡한 일정 속에 리뷰가 많이 늦었는데 기다려주셔서 감사합니다 🙏
컴파운드 패턴 정말 잘 구현하셨네요!
UI 라이브러리들도 비슷한 느낌으로 만들어져 있을 것 같네요
코드 보면서 많이 배워갑니다~
좋았던 부분이랑 개선하면 좋을 것 같은 부분 몇 가지 코멘트 남겨뒀습니다
수고하셨습니다 😊
| style={{ width: '101px' }} | ||
| > | ||
| <CustomDropDown.Trigger> | ||
| <Styled.ApplicantFilterSelect> | ||
| {selectedSort.label} | ||
| </Styled.ApplicantFilterSelect> | ||
| <Styled.Arrow src={selectIcon} /> | ||
| </CustomDropDown.Trigger> | ||
| <CustomDropDown.Menu top='115%'> | ||
| {sortOptions.map(({ value, label }) => ( | ||
| <CustomDropDown.Item | ||
| key={value} | ||
| value={value} | ||
| style={{ fontSize: '12px' }} |
There was a problem hiding this comment.
이런 식으로 width를 여러 곳에서 인라인으로 넣고 있는데 className, 상수 등으로 스타일을 별도로 관리하면 어떨까요?
물론 지금 방식도 충분히 괜찮긴 한데 한번 고려해보시면 좋을 것 같아서 남겨봅니다 😊
| const [statusOpen, setStatusOpen] = useState(false); | ||
| const [isStatusDropdownOpen, setIsStatusDropdownOpen] = useState(false); | ||
| const [isChecked, setIsChecked] = useState(false); | ||
| const [isFilterOpen, setIsFilterOpen] = useState(false); |
There was a problem hiding this comment.
요기 페이지 드랍다운이 많아서 커스텀 드랍다운 만든 효과가 좋네요~
한 페이지에 드롭다운이 4개나 되니까 상태 관리가 좀 복잡해 보이네요
지금은 각 드롭다운마다 closeAllDropdowns() + setState 패턴이 반복되고 있는데 4개의 open/close 상태를 하나의 상태로 관리하면 코드가 훨씬 깔끔해질 것 같아요!
const [activeDropdown, setActiveDropdown] = useState<string | null>(null);
const toggleDropdown = (name: string) =>
setActiveDropdown(prev => prev === name ? null : name);이렇게 바꾸면
closeAllDropdowns()제거onToggle콜백 함수 간소화useEffect의존성 배열: 상태 4개 → 1개
코드량도 줄고 유지보수도 쉬워질 것 같아서 제안드립니다! 😊
There was a problem hiding this comment.
string 형태로 활성 드롭다운 관리 좋은거같네요! 다음 리팩토링 작업으로 추가하겠습니다~
하위 컴포넌트가 존재하면 Prop을 넘길 수 있겠지만 기본적으로 Compound 패턴은 각각의 독립적인 컴포넌트를 하나로 묶는것이기때문에 상태공류를 위해 Context Api를 이용해야 상태를 각 하위 컴포넌트끼리 공유할 수 있습니다! |
#️⃣연관된 이슈
📝작업 내용
지원자 목록에 볼수 있는 옵션 리스트 (전체, 서류검토, 면접예정, 합격, 불합)
정렬 옵션 기능 추가(제출순, 이름순)
기존에 흩어져 있던 드롭다운 컴포넌트들을
CustomDropdown으로 분리하여 통일했습니다.또한
CustomDropdown을 Compound 패턴으로 구현해 재사용성을 높였습니다.CustomDropdown은 Trigger, Menu, Item을 자유롭게 조합하여 사용할 수 있습니다.기존 드롭다운 메뉴들도 디자인이 통일되었습니다.
(QuestionBuilder)
<이전>

<이후>

왜 Compound 패턴을 사용했는가
드롭다운 컴포넌트의 초기 설계 목적은 확장성 있는 구조를 만드는 것이었습니다.
또한, 드롭다운을 사용할 때마다 복잡한 로직을 반복적으로 작성하는 것을 피하고 싶었고, 이러한 요구에 Compound 패턴이 잘 맞았습니다.
Compound 패턴은 내부적으로 Context API를 활용하여 컴포넌트 간 상태를 공유할 수 있습니다.
이를 통해 드롭다운에서 반복되는 로직을 컴포넌트 내부에서 처리할 수 있었고, 상태 공유를 기반으로 드롭다운을 Trigger, Menu, Item이라는 세 가지 핵심 단위로 나누어 확장성 있게 설계할 수 있었습니다.
이러한 구조는 공통적인 UI 요소를 분리하여 디자인 재사용성과 유지보수성을 높여줍니다.
또한, 특정 부분만 커스터마이징하여 다양한 드롭다운 UI를 쉽게 구현할 수 있으며, 컴포넌트 간 의존성을 줄여 전체적인 확장성을 강화했습니다.
컴파운드 패턴 상태공유
컴파운드 패턴은 하나의 컴포넌트로 구성된것 처럼 보이지만
내부적으로는 여러개의 나누어진 컴포넌트로 구성되어있습니다
그렇기에 부모의 상태를 ContextAPI를 이용하여 캡슐화된 컴포넌트에게 전달할 수 있게됩니다
provider
전역 상태 사용
중점적으로 리뷰받고 싶은 부분(선택)
논의하고 싶은 부분(선택)
🫡 참고사항
Summary by CodeRabbit
신규 기능
버그 수정
스타일
리팩터
기타