Skip to content

[feature] 지원서 상태에 따라 분류하고 이름, 제출 순으로 정렬 할 수 있다.#754

Merged
lepitaaar merged 28 commits intodevelop-fefrom
feature/#721-sort-applicants-MOA-216
Oct 19, 2025
Merged

[feature] 지원서 상태에 따라 분류하고 이름, 제출 순으로 정렬 할 수 있다.#754
lepitaaar merged 28 commits intodevelop-fefrom
feature/#721-sort-applicants-MOA-216

Conversation

@lepitaaar
Copy link
Contributor

@lepitaaar lepitaaar commented Sep 21, 2025

#️⃣연관된 이슈

#721

📝작업 내용

스크린샷 2025-09-21 20 16 39

지원자 목록에 볼수 있는 옵션 리스트 (전체, 서류검토, 면접예정, 합격, 불합)
정렬 옵션 기능 추가(제출순, 이름순)

기존에 흩어져 있던 드롭다운 컴포넌트들을 CustomDropdown으로 분리하여 통일했습니다.
또한 CustomDropdown을 Compound 패턴으로 구현해 재사용성을 높였습니다.
CustomDropdown은 Trigger, Menu, Item을 자유롭게 조합하여 사용할 수 있습니다.

기존 드롭다운 메뉴들도 디자인이 통일되었습니다.
(QuestionBuilder)

<이전>
image

<이후>
스크린샷 2025-09-21 20 34 17

왜 Compound 패턴을 사용했는가

드롭다운 컴포넌트의 초기 설계 목적은 확장성 있는 구조를 만드는 것이었습니다.
또한, 드롭다운을 사용할 때마다 복잡한 로직을 반복적으로 작성하는 것을 피하고 싶었고, 이러한 요구에 Compound 패턴이 잘 맞았습니다.
Compound 패턴은 내부적으로 Context API를 활용하여 컴포넌트 간 상태를 공유할 수 있습니다.
이를 통해 드롭다운에서 반복되는 로직을 컴포넌트 내부에서 처리할 수 있었고, 상태 공유를 기반으로 드롭다운을 Trigger, Menu, Item이라는 세 가지 핵심 단위로 나누어 확장성 있게 설계할 수 있었습니다.
이러한 구조는 공통적인 UI 요소를 분리하여 디자인 재사용성과 유지보수성을 높여줍니다.
또한, 특정 부분만 커스터마이징하여 다양한 드롭다운 UI를 쉽게 구현할 수 있으며, 컴포넌트 간 의존성을 줄여 전체적인 확장성을 강화했습니다.

컴파운드 패턴 상태공유

컴파운드 패턴은 하나의 컴포넌트로 구성된것 처럼 보이지만
내부적으로는 여러개의 나누어진 컴포넌트로 구성되어있습니다
그렇기에 부모의 상태를 ContextAPI를 이용하여 캡슐화된 컴포넌트에게 전달할 수 있게됩니다

const useDropDownContext = () => {
  const context = useContext(CustomDropDownContext);
  if (!context) {
    throw new Error(
      'useDropDownContext는 CustomDropDownContextProvider 내부에서 사용할 수 있습니다.',
    );
  }
  return context;
};

provider

  const value = { open, selected, options, onToggle, handleSelect };

  return (
    <CustomDropDownContext.Provider value={value}>
      <Styled.DropDownWrapper style={style}>{children}</Styled.DropDownWrapper>
    </CustomDropDownContext.Provider>
  );

전역 상태 사용

const Trigger = ({ children }: { children: ReactNode }) => {
  const { onToggle, open } = useDropDownContext();
  return (
    <div
      onClick={() => {
        onToggle(open);
      }}
    >
      {children}
    </div>
  );
};

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

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

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

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

논의하고 싶은 부분이 있다면 작성해주세요.

🫡 참고사항

Summary by CodeRabbit

  • 신규 기능

    • CustomDropDown 컴포넌트가 외부 제어(컨트롤) 방식과 하위 트리거, 메뉴, 아이템 구성을 지원하도록 업데이트되었습니다.
    • 관리 페이지(어드민)에서 신청자 필터/정렬/상태 등 다양한 드롭다운 컨트롤이 새로운 방식으로 통합되었습니다.
  • 버그 수정

    • 드롭다운 외부 클릭 시 모든 드롭다운 메뉴가 올바르게 닫히도록 개선되었습니다.
  • 스타일

    • 드롭다운과 관련된 UI의 시각적 스타일, 호버, 선택 효과가 개선되었습니다.
    • 관리 페이지의 선택 영역 스타일이 일관되게 변경되었습니다.
  • 리팩터

    • 기존 드롭다운 구현을 새로운 커스텀 방식으로 코드 구조를 개편하였습니다.
  • 기타

    • 일부 기본값 및 내부 처리 로직이 변경되었습니다(예: 상태 레이블 명칭).

@lepitaaar lepitaaar self-assigned this Sep 21, 2025
@lepitaaar lepitaaar added ✨ Feature 기능 개발 💻 FE Frontend labels Sep 21, 2025
@vercel
Copy link

vercel bot commented Sep 21, 2025

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

Project Deployment Preview Comments Updated (UTC)
moadong Ready Ready Preview Comment Oct 3, 2025 1:21pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 21, 2025

Warning

.coderabbit.yaml has a parsing error

The CodeRabbit configuration file in this repository has a parsing error and default settings were used instead. Please fix the error(s) in the configuration file. You can initialize chat with CodeRabbit to get help with the configuration file.

💥 Parsing errors (1)
Validation error: Invalid regex pattern for base branch. Received: "**" at "reviews.auto_review.base_branches[0]"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Walkthrough

제네릭 컨텍스트 기반의 외부 제어(composed) CustomDropDown로 전면 리팩터링하고, 이를 Admin의 QuestionBuilder 및 ApplicantsTab에 통합했습니다. 스타일 컴포넌트 일부가 추가/제거/변경되었고, ApplicantsTab에는 클라이언트 필터링·검색·정렬 및 다중 드롭다운 외부클릭 처리 로직이 추가되었습니다.

Changes

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 배열). 키워드 검색, 상태 필터링, 이름/제출일 정렬 로직 추가. 스타일: ApplicantFilterSelectstyled.selectstyled.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)
Loading
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: 필터/정렬된 목록 렌더
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

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 ⚠️ Warning 이 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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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 속성 누출 가능성

  • Iconposition: absolute인데 Selectedposition: 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) 추가 제안

옵션 조회 실패 시 selectedLabelundefined가 될 수 있습니다. 기본 라벨을 지정해 UI 깜빡임을 방지하세요.

-  const selectedLabel = DROPDOWN_OPTIONS.find(
+  const selectedLabel = DROPDOWN_OPTIONS.find(
     (option) => option.value === selectedType,
   )?.label;
+  const displayLabel = selectedLabel ?? '질문 유형';

그리고 사용처에서 selectedLabeldisplayLabel로 교체.


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: 오타: dropdwonRefdropdownRef

의미 전달과 검색 편의성을 위해 변수명을 정정하세요.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 0955be2 and 19dbdba.

📒 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.ts
  • frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.styles.ts
  • frontend/src/components/common/CustomDropDown/CustomDropDown.tsx
  • frontend/src/components/common/CustomDropDown/CustomDropDown.styles.ts
  • frontend/src/utils/mapStatusToGroup.ts
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx
  • frontend/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.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx
  • frontend/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)를 사용하는 파일들이 확인되었습니다.

Copy link
Member

@seongwon030 seongwon030 left a comment

Choose a reason for hiding this comment

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

커스텀 드롭다운을 위해 많이 고민하셨을 것 같습니다! 특히 컴파운드 패턴을 사용한 것이 좋았습니다.
PR 읽다가 설명이 좀 있었으면 좋았을 것들 생각나서 적어봐요.

  1. 전역 상태 사용
  2. 컴파운트 패턴을 사용한 동기

고생하셨습미당

Comment on lines +33 to +35
const CustomDropDownContext = createContext<
CustomDropDownContextProps<any> | undefined
>(undefined);
Copy link
Member

Choose a reason for hiding this comment

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

context 쓰신 이유가 각각의 드롭다운 상태를 독립적으로 유지하기 위해서인가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

컴포넌트안 공통된 상태들을 공유하기 위해서입니다!

Copy link
Member

Choose a reason for hiding this comment

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

그럼 각각 다른 드롭다운 컴포넌트 두개를 생성하면 서로 상태를 공유하나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

서로 상태를 공유하지않고 각자 상태를 가지게됩니다

- onToggle에 isOpen인자를 주어 여닫을시 변경된 상태에 의존하지않고 그시점의 값을 보존하여 독립적으로 관리하게 변경하였습니다.

- onToggle을 Trigger를 클릭시에 실행되게 변경하여 Trigger 컴포넌트 추가시 추가적인 event 핸들러 없이 여닫는게 가능하도록 재설계하였습니다
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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: 상태 타입을 명시적인 유니온 타입으로 정의하세요.

selectedFilterselectedSortstring과 객체로 관리되어 타입 안전성이 부족합니다.

다음과 같이 수정하세요:

+  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.

📥 Commits

Reviewing files that changed from the base of the PR and between 19dbdba and 972d708.

📒 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.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx
  • frontend/src/components/common/CustomDropDown/CustomDropDown.tsx
  • frontend/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.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx
  • frontend/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!

$isSelected transient 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 패턴을 통해 재사용성과 유연성이 향상되었습니다.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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].valueanswers가 비어있거나 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: 타입 정의 개선 필요

selectedFilterstring 타입으로 정의되어 있어 불필요한 캐스팅과 타입 불안정성을 유발합니다. 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]?.valueundefined일 때 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 972d708 and 3af21b0.

📒 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!

상태 변경 드롭다운 로직이 올바르게 구현되었습니다.

Copy link
Collaborator

@suhyun113 suhyun113 left a comment

Choose a reason for hiding this comment

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

컴파운드 패턴에 대해 코드를 보고 많이 배웠습니다ㅏ

컴파운드 패턴에서도 하위 컴포넌트 간 상태 전달에서 Props를 쓰는 경우가 있나요? 주로 Context api를 사용하는 것 같은데 어떤 이유로 사용하셨는지 궁금합니다!

Copy link
Member

@oesnuj oesnuj left a comment

Choose a reason for hiding this comment

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

빡빡한 일정 속에 리뷰가 많이 늦었는데 기다려주셔서 감사합니다 🙏

컴파운드 패턴 정말 잘 구현하셨네요!
UI 라이브러리들도 비슷한 느낌으로 만들어져 있을 것 같네요
코드 보면서 많이 배워갑니다~

좋았던 부분이랑 개선하면 좋을 것 같은 부분 몇 가지 코멘트 남겨뒀습니다
수고하셨습니다 😊

Comment on lines +321 to +334
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' }}
Copy link
Member

Choose a reason for hiding this comment

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

이런 식으로 width를 여러 곳에서 인라인으로 넣고 있는데 className, 상수 등으로 스타일을 별도로 관리하면 어떨까요?
물론 지금 방식도 충분히 괜찮긴 한데 한번 고려해보시면 좋을 것 같아서 남겨봅니다 😊

Comment on lines -24 to +47
const [statusOpen, setStatusOpen] = useState(false);
const [isStatusDropdownOpen, setIsStatusDropdownOpen] = useState(false);
const [isChecked, setIsChecked] = useState(false);
const [isFilterOpen, setIsFilterOpen] = useState(false);
Copy link
Member

Choose a reason for hiding this comment

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

요기 페이지 드랍다운이 많아서 커스텀 드랍다운 만든 효과가 좋네요~

한 페이지에 드롭다운이 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개

코드량도 줄고 유지보수도 쉬워질 것 같아서 제안드립니다! 😊

Copy link
Contributor Author

Choose a reason for hiding this comment

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

string 형태로 활성 드롭다운 관리 좋은거같네요! 다음 리팩토링 작업으로 추가하겠습니다~

@lepitaaar lepitaaar merged commit b04c7b2 into develop-fe Oct 19, 2025
5 checks passed
@lepitaaar
Copy link
Contributor Author

컴파운드 패턴에 대해 코드를 보고 많이 배웠습니다ㅏ

컴파운드 패턴에서도 하위 컴포넌트 간 상태 전달에서 Props를 쓰는 경우가 있나요? 주로 Context api를 사용하는 것 같은데 어떤 이유로 사용하셨는지 궁금합니다!

하위 컴포넌트가 존재하면 Prop을 넘길 수 있겠지만 기본적으로 Compound 패턴은 각각의 독립적인 컴포넌트를 하나로 묶는것이기때문에 상태공류를 위해 Context Api를 이용해야 상태를 각 하위 컴포넌트끼리 공유할 수 있습니다!

@lepitaaar lepitaaar deleted the feature/#721-sort-applicants-MOA-216 branch November 11, 2025 04:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💻 FE Frontend ✨ Feature 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

Comments