Skip to content

[release] FE v1.1.1#733

Merged
oesnuj merged 74 commits intomainfrom
develop-fe
Sep 12, 2025
Merged

[release] FE v1.1.1#733
oesnuj merged 74 commits intomainfrom
develop-fe

Conversation

@oesnuj
Copy link
Member

@oesnuj oesnuj commented Sep 11, 2025

📝작업 내용

  • 5차 MVP 기능 및 개선사항을 main 브랜치에 병합합니다

Summary by CodeRabbit

  • 신기능

    • 지원자 탭에 다중 선택, 일괄 상태 변경, 일괄 삭제 기능 추가(체크박스/드롭다운/전체선택).
    • 계정 관리에 비밀번호 변경 기능 추가(유효성 검증·성공/오류 안내 포함).
    • 모집 상태 ‘상시모집’ 표시 지원.
  • 개선

    • 지원자 상태 배지·필터 UI 개선 및 새 상태(불합) 지원.
    • 입력 필드 성공 상태·비활성 버튼·텍스트에어리어 자동 높이 조절 등 스타일/사용성 향상.
    • 사이드바 카테고리화 및 아이콘 갱신.
    • 검색/카테고리 상태 유지 안정성 향상.
  • 버그 수정

    • 모집 기간 시간대(KST) 보정으로 날짜 표시/저장 정확도 개선.

seongwon030 and others added 30 commits August 17, 2025 14:28
- 라벨+이미지 구조
- flex로 변경
[fix] 모집기간 설정 시간 보정
[fix] 질문 제목 반응형 레이아웃 변경
- position relative로 부모 기준으로 위치설정
- right 설정으로 객관식 박스 안으로 들어가도록 설정
seongwon030 and others added 6 commits September 6, 2025 15:17
- CategoryButtonList, SearchBox, MainPage에서 쓰이던 것 변경
…render-solved-MOA-215

[refactor] 카테고리 context를 zustand기반으로 마이그레이션
[feature] 비밀번호 변경 페이지 추가
@vercel
Copy link

vercel bot commented Sep 11, 2025

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

Project Deployment Preview Comments Updated (UTC)
moadong Ready Ready Preview Comment Sep 11, 2025 1:14pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 11, 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

이 PR은 검색/카테고리 전역 상태를 React Context에서 Zustand 스토어로 마이그레이션하고, 지원자 관리 기능을 일괄 업데이트/삭제 중심으로 재구성합니다. 관리자 사이드바를 카테고리형 구조로 개편하고, 계정 탭을 비밀번호 변경 플로우로 교체합니다. 여러 UI 컴포넌트 스타일/상태 props가 보강되며, 모집 일정의 KST 시간 보정 로직이 추가됩니다.

Changes

Cohort / File(s) Change Summary
상태관리 마이그레이션 (Context → Zustand)
frontend/package.json, frontend/src/store/useSearchStore.ts, frontend/src/store/useCategoryStore.ts, frontend/src/context/SearchContext.tsx, frontend/src/context/CategoryContext.tsx, frontend/src/App.tsx, frontend/src/pages/MainPage/MainPage.tsx, frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx, frontend/src/pages/MainPage/components/SearchBox/SearchBox.tsx, frontend/src/services/header/useHeaderService.ts, frontend/src/components/common/Header/Header.stories.tsx
zustand 의존성 추가. Search/Category Context 삭제 → Zustand 스토어 추가 및 소비자 컴포넌트 전환. App에서 Provider 제거. 헤더 스토리에서 SearchProvider 제거.
지원자 관리 API/훅/화면 개편 (일괄 업데이트/삭제)
frontend/src/apis/application/updateApplicantDetail.ts, frontend/src/types/applicants.ts, frontend/src/constants/status.ts, frontend/src/apis/applicants/deleteApplicants.ts, frontend/src/hooks/queries/applicants/useUpdateApplicant.ts, frontend/src/hooks/queries/applicants/useDeleteApplicants.ts, frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx, frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts, frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx
업데이트 API를 단건→배치(UpdateApplicantParams[])로 변경. 삭제 API/훅 추가. useUpdateApplicant 시그니처 변경(clubId만). 상태 상수/사용처 정리(AVAILABLE_STATUSES, DECLINED 포함). ApplicantsTab에 다중 선택/일괄 상태 변경/일괄 삭제 UI·로직 추가. 스타일 컴포넌트 다수 추가/프로퍼티 확장.
관리자 사이드바 구조 개편
frontend/src/pages/AdminPage/components/SideBar/SideBar.styles.ts, frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx
탭 데이터를 카테고리+아이템 구조로 재편. Divider/SidebarButton 재정의, SidebarCategoryTitle 추가. 경로 항목 세분화(비밀번호 수정/회원탈퇴).
계정 편집 → 비밀번호 변경 플로우
frontend/src/apis/auth/changePassword.ts, frontend/src/pages/AdminPage/tabs/AccountEditTab/AccountEditTab.tsx, frontend/src/pages/AdminPage/tabs/AccountEditTab/AccountEditTab.styles.ts
비밀번호 변경 API 추가 및 탭 UI를 비밀번호 변경 플로우로 교체(검증/로딩/성공/에러 표시). 스타일 요소(SuccessMessage 등) 추가.
모집 일정 KST 보정
frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx
요청/응답 시각에 +9/-9시간 보정 함수 추가 및 적용.
공통 UI/스타일 보강
frontend/src/components/common/InputField/InputField.styles.ts, frontend/src/components/common/InputField/InputField.tsx, frontend/src/components/common/Button/Button.tsx, frontend/src/components/common/CustomTextArea/CustomTextArea.styles.ts, frontend/src/components/application/QuestionTitle/QuestionTitle.styles.ts, frontend/src/components/application/QuestionTitle/QuestionTitle.tsx, frontend/src/components/application/questionTypes/Choice.styles.ts, frontend/src/components/application/questionTypes/Choice.tsx, frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.styles.ts, frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.tsx, frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.styles.ts, frontend/src/components/ClubStateBox/ClubStateBox.tsx
InputField에 isSuccess prop 추가, Button에 disabled prop/스타일 추가, TextArea/QuestionTitle 자동 리사이즈 및 레이아웃 조정, Choice 삭제 버튼 아이콘화, QuestionBuilder 삭제 버튼/레이아웃 조정, ApplicationEditTab 폼 타이틀 스타일 수정, ClubStateBox에 ALWAYS 상태 스타일 추가.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Admin as 관리자
  participant Tab as ApplicantsTab
  participant Del as useDeleteApplicants
  participant Upd as useUpdateApplicant
  participant API as Server API

  rect rgb(245,248,255)
    note right of Tab: 다중 선택/드롭다운 조작
    Admin->>Tab: 행 체크/전체선택/상태 선택
    alt 일괄 삭제
      Tab->>Del: mutate({ applicantIds })
      Del->>API: DELETE /api/club/{clubId}/applicant { applicantIds }
      API-->>Del: 200 OK / Error
      Del-->>Tab: 성공/실패 콜백
      Tab->>Tab: 선택 초기화 / 오류 알림
    else 일괄 상태변경
      Tab->>Upd: mutate([{ applicantId, status, memo }, ...])
      Upd->>API: PUT /api/club/{clubId}/applicant [UpdateApplicantParams]
      API-->>Upd: 200 OK / Error
      Upd-->>Tab: 성공/실패 콜백
      Tab->>Tab: 선택 초기화/메뉴 닫기/알림
    end
  end
Loading
sequenceDiagram
  autonumber
  actor Admin as 관리자
  participant Page as AccountEditTab
  participant Auth as changePassword API
  participant API as Server API

  Admin->>Page: 새 비밀번호/확인 입력
  Page->>Page: 클라이언트 검증(정책, 일치 여부)
  alt 유효
    Page->>Auth: changePassword({ password })
    Auth->>API: PUT /auth/user/ { password }
    API-->>Auth: 204/200 또는 에러 메시지
    Auth-->>Page: 성공/에러
    Page->>Admin: 성공 메시지 또는 에러 안내
  else 무효
    Page->>Admin: 검증 에러 표시
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Possibly related PRs

Suggested labels

💻 FE, 🔨 Refactor, 📬 API

Suggested reviewers

  • seongwon030
  • lepitaaar

Pre-merge checks (2 passed, 1 warning)

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed 현재 제목 "[release] v1.1.1"은 짧고 이 PR이 릴리스 목적임을 명확히 표시하므로 변경 유형과 관련성이 있습니다. 그러나 제공된 변경 요약을 보면 컨텍스트 제거와 Zustand 도입, 다수의 API/훅/UI 변경 등 핵심 기술 변경이 포함되어 있어 제목만으로는 무엇이 주요 변경인지 파악하기 어렵습니다. 따라서 릴리스 PR로서 적절하나 변경의 핵심을 바로 전달하지는 않습니다.

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.

✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch develop-fe

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.

@oesnuj oesnuj requested review from Copilot and removed request for Copilot September 11, 2025 13:15
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: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
frontend/src/components/application/questionTypes/Choice.tsx (1)

110-118: 아이콘 전용 삭제 버튼 접근성 및 기본 submit 방지

현재 버튼 내부가 아이콘만 있어 스크린리더용 이름이 없습니다. 또한 form 내 배치 시 기본 submit 위험이 있어 type="button" 지정이 필요합니다.

다음과 같이 수정을 권장합니다.

-            {mode === 'builder' && items.length > MIN_ITEMS && (
-              <Styled.DeleteButton
-                onClick={(e) => {
+            {mode === 'builder' && items.length > MIN_ITEMS && (
+              <Styled.DeleteButton
+                type="button"
+                aria-label="항목 삭제"
+                onClick={(e) => {
                   e.stopPropagation();
                   handleDeleteItem(index);
                 }}
               >
-                <img src={DeleteIcon} />
+                <img src={DeleteIcon} alt="" aria-hidden="true" />
               </Styled.DeleteButton>
             )}
frontend/src/components/common/InputField/InputField.tsx (1)

68-79: 접근성 보강: aria 속성 및 HelperText 연결

에러/헬퍼 텍스트를 스크린리더가 인지하도록 aria-invalid/aria-describedby/role="alert"를 부여하는 것을 권장합니다.

적용 예시:

@@
-        <Styled.Input
+        <Styled.Input
           type={type === 'password' && !isPasswordVisible ? 'password' : 'text'}
           value={value}
           onChange={handleChange}
           placeholder={placeholder}
           maxLength={maxLength}
           disabled={disabled}
           hasError={isError}
           isSuccess={isSuccess}
           readOnly={readOnly}
+          aria-invalid={Boolean(isError) || undefined}
+          aria-describedby={isError && helperText ? 'input-helper-text' : undefined}
         />
@@
-      {isError && helperText && (
-        <Styled.HelperText>{helperText}</Styled.HelperText>
-      )}
+      {isError && helperText && (
+        <Styled.HelperText id='input-helper-text' role='alert' aria-live='assertive'>
+          {helperText}
+        </Styled.HelperText>
+      )}

여러 InputField가 동일 화면에 공존할 수 있다면, useId()로 고유 id를 생성해 id/aria-describedby를 매칭하는 형태로 확장해 주세요.

Also applies to: 95-97

frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.styles.ts (1)

56-76: 스타일드 컴포넌트 이름 변경 · 애니메이션 참조 수정 필요

  • frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.styles.ts: export const submitButton → SubmitButton(PascalCase)로 리네임하고 해당 import/사용부를 함께 수정하세요. 리포지토리에서 직접 사용은 검색되지 않았으나 DOM 커스텀 태그 충돌 위험이 있으므로 변경이 필요합니다.
  • 애니메이션: 현재 &:hover에서 "animation: pulse 0.4s ease-in-out;"을 사용하지만 전역 @Keyframes pulse 정의가 없습니다. 리포지토리에는 frontend/src/components/common/Button/Button.tsx에 "const pulse = keyframes..."로 정의되어 있고 해당 파일은 "animation: ${pulse} ..."로 사용 중입니다. 조치(택1): (a) pulse를 import해서 이 파일에서 "animation: ${pulse} 0.4s ease-in-out;"로 사용하거나, (b) 이 파일에 keyframes를 직접 선언하거나, (c) 전역 @Keyframes pulse를 추가하세요.
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts (2)

205-219: width 타입 처리 버그(문자열에 px 붙음)

width?: number | string인데 문자열(예: '20%')에도 px가 붙어 잘못된 CSS가 생성됩니다.

-  width: ${({ width }) => (width ? `${width}px` : 'auto')};
+  width: ${({ width }) =>
+    typeof width === 'number' ? `${width}px` : width ?? 'auto'};

337-354: 상태 라벨 오탈자(불합 → 불합격) 및 분기 간소화 권장

실제 상태 문자열과 불일치 시 배경색이 기본값으로 떨어집니다. 우선 오탈자를 바로잡고, 추후 맵 기반으로 단순화하는 것을 권장합니다.

-          : status === '불합'
+          : status === '불합격'
             ? '#FFE8E8'
             : '#eee'};

추가로 ApplicationStatus enum을 직접 사용하거나 STATUS_BG_MAP으로 치환하는 개선도 고려해주세요.

frontend/src/apis/application/updateApplicantDetail.ts (1)

21-23: 서버 에러 메시지 노출

서버가 전달하는 구체 메시지를 우선 사용하세요.

-    if (!response.ok) {
-      throw new Error('지원자의 지원서 정보 수정에 실패했습니다.');
-    }
+    if (!response.ok) {
+      let message = '지원자의 지원서 정보 수정에 실패했습니다.';
+      try {
+        const err = await response.json();
+        message = err?.message ?? message;
+      } catch (_) {
+        // noop
+      }
+      throw new Error(message);
+    }
🧹 Nitpick comments (61)
frontend/src/components/application/QuestionTitle/QuestionTitle.styles.ts (1)

33-33: flex-basis: 0%로 인한 레이아웃 충돌 가능성 (flex: 1 대신 flex: 1 1 auto 권장)

flex: 1flex: 1 1 0%로 해석되어 width: fit-content/max-width와 상충하고, 자식의 수평 수축을 막아 예기치 않은 오버플로우/줄바꿈을 유발할 수 있습니다. min-width: 0도 함께 지정해 자식이 정상적으로 줄어들 수 있게 해주세요.

적용 예시:

 export const QuestionTitleTextContainer = styled.div`
   display: flex;
   align-items: flex-start;
   gap: 4px;
-  width: fit-content;
-  max-width: calc(100% - 20px);
-  flex: 1;
+  /* flex item이 컨테이너 너비에 맞게 확장/수축 가능 */
+  flex: 1 1 auto;
+  /* 자식이 수평으로 줄어들 수 있게 허용 */
+  min-width: 0;
+  /* 필요시 캡을 둘 거라면 100% 캡으로 단순화 */
+  max-width: 100%;
 `;
frontend/src/components/application/QuestionTitle/QuestionTitle.tsx (1)

4-4: SSR 환경 경고 방지를 위한 “isomorphic layout effect” 적용 고려

Next.js 등 SSR 환경에서 useLayoutEffect는 경고를 유발할 수 있습니다. 현재 useEffect를 이미 임포트하고 있으니 래퍼 훅으로 감싸는 방식을 권장합니다.

-import { useEffect, useLayoutEffect, useRef } from 'react';
+import { useEffect, useLayoutEffect, useRef } from 'react';
+const useIsomorphicLayoutEffect =
+  typeof window !== 'undefined' ? useLayoutEffect : useEffect;
-  useLayoutEffect(() => {
+  useIsomorphicLayoutEffect(() => {
     // ...
-  }, [title]);
+  }, [title, mode]);
frontend/src/components/application/questionTypes/Choice.styles.ts (1)

23-24: 상대 위치 이동 대신 플렉스 레이아웃 활용 권장

position: relative; right: 45px;는 반응형에서 깨질 수 있습니다. 플렉스 컨테이너(ItemWrapper)를 활용해 오른쪽 정렬이 더 안전합니다.

-export const DeleteButton = styled.button`
-  position: relative;
-  right: 45px;
+export const DeleteButton = styled.button`
+  margin-left: auto;
frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.styles.ts (2)

78-89: 터치 목표 크기 상향 및 포커스 스타일

height: 20px는 터치/접근성 권장치(<~32–44px)에 못 미칩니다. 포커스 가시성도 보강해 주세요.

 export const DeleteButton = styled.button`
   display: flex;
   align-items: center;
   justify-content: center;
   gap: 4px;
   width: 100%;
-  height: 20px;
+  min-height: 32px;
   border-radius: 6px;
   border: 1px solid #ffded2;
   background: #fff;
   cursor: pointer;
+  &:focus-visible {
+    outline: 2px solid #ff5000;
+    outline-offset: 2px;
+  }
 `

91-103: 호버 효과 전이 및 키보드 포커스 동등성

호버 시 박스섀도 전이와 키보드 포커스 동등 효과를 추가하면 UX가 매끄럽습니다.

 export const QuestionWrapper = styled.div<{ readOnly?: boolean }>`
   display: flex;
   padding: 26px 20px;
   gap: 20px;
   border-radius: 10px;
   border: 1px solid #f0f0f0;
   pointer-events: ${({ readOnly }) => (readOnly ? 'none' : 'auto')};
   cursor: ${({ readOnly }) => (readOnly ? 'not-allowed' : 'auto')};
+  transition: box-shadow 0.2s ease;
 
   &:hover {
     box-shadow: 0 2px 10px 0 rgba(0, 0, 0, 0.1);
   }
+  &:focus-within {
+    box-shadow: 0 2px 10px 0 rgba(0, 0, 0, 0.1);
+  }
 `
frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.tsx (1)

139-141: 접근성 OK, CLS 방지용 아이콘 크기 지정 제안

텍스트 라벨 + 데코 아이콘 패턴은 적절합니다. 레이아웃 시프트 방지를 위해 고정 크기 지정 권장.

-          <Styled.DeleteButton type='button' onClick={() => onRemoveQuestion()}>
-            삭제 <img src={DeleteIcon} alt='' aria-hidden='true' />
+          <Styled.DeleteButton type='button' onClick={() => onRemoveQuestion()}>
+            삭제 <img src={DeleteIcon} alt='' aria-hidden='true' width="16" height="16" />
           </Styled.DeleteButton>

(아이콘 실제 크기에 맞춰 값 조정 필요)

frontend/src/components/common/InputField/InputField.styles.ts (4)

30-33: 중첩 삼항 제거 + 하드코딩 색상 테마/상수로 치환

가독성과 일관성을 위해 중첩 삼항을 if 블록으로 바꾸고, 색상은 테마 토큰(or 상수)로 관리하는 것을 권장합니다.

가능한 변경 예시(테마 있으면 우선 사용, 없으면 안전한 기본값 fallback):

-  border: 1px solid
-    ${({ hasError, isSuccess }) =>
-    hasError ? 'red' : isSuccess ? '#28a745' : '#c5c5c5'};
+  border: 1px solid
+    ${({ theme, hasError, isSuccess }) => {
+      if (hasError) return theme?.colors?.error ?? '#dc3545';
+      if (isSuccess) return theme?.colors?.success ?? '#28a745';
+      return theme?.colors?.border ?? '#c5c5c5';
+    }};

추가로, 현재 transition: background 0.2s;만 적용되어 있어 경계 색상 변경이 튀어 보입니다. border-color/box-shadow도 함께 트랜지션하는 것을 권장합니다.

-  transition: background 0.2s;
+  transition: background 0.2s, border-color 0.2s, box-shadow 0.2s;

49-56: focus 상태 색상 계산 로직 일원화 및 box-shadow 색상 명시

focus에서도 동일한 상태 색상 규칙을 재사용하면 유지보수성이 올라갑니다. 또한 box-shadow: 0 0 3px;는 색상이 명시되지 않아 브라우저 기본값에 의존합니다. 경계색과 동일하게 지정하는 것이 일관적입니다.

-    border-color: ${({ hasError, isSuccess, readOnly }) =>
-      readOnly
-        ? '#c5c5c5'
-        : hasError
-        ? 'red'
-        : isSuccess
-        ? '#28a745'
-        : '#007bff'};
+    border-color: ${({ theme, hasError, isSuccess, readOnly }) => {
+      if (readOnly) return theme?.colors?.border ?? '#c5c5c5';
+      if (hasError) return theme?.colors?.error ?? '#dc3545';
+      if (isSuccess) return theme?.colors?.success ?? '#28a745';
+      return theme?.colors?.primary ?? '#007bff';
+    }};

그리고 box-shadow 색상도 같은 규칙을 사용해 명시하는 것을 권장합니다(파일 상단 유틸 정의 후 사용 가능):

// 예시 유틸(파일 상단):
const stateColor = (theme: any, { readOnly, hasError, isSuccess }:
  { readOnly?: boolean; hasError?: boolean; isSuccess?: boolean; }) => {
  if (readOnly) return theme?.colors?.border ?? '#c5c5c5';
  if (hasError) return theme?.colors?.error ?? '#dc3545';
  if (isSuccess) return theme?.colors?.success ?? '#28a745';
  return theme?.colors?.primary ?? '#007bff';
};
-    box-shadow: 0 0 3px;
+    box-shadow: 0 0 3px
+      ${({ theme, hasError, isSuccess, readOnly }) =>
+        stateColor(theme, { readOnly, hasError, isSuccess })};

60-62: disabled 상태에서 테두리 색 중립화 여부 확인

현재 disabled에서도 에러/성공 테두리 색이 유지될 수 있습니다. 디자인 의도가 중립(회색)이라면 여기서 덮어써 주는 편이 명확합니다.

   &:disabled {
     background-color: rgba(0, 0, 0, 0.05);
+    border-color: ${({ theme }) => theme?.colors?.border ?? '#c5c5c5'};
   }

의도가 “상태 유지”라면 현 상태 유지가 맞습니다. 디자인 가이던스 확인 부탁드립니다.


26-26: 이름 불일치(hasError vs isError) 정리 제안(선택사항)

컴포넌트 외부 prop은 isError, 스타일 prop은 hasError로 혼재되어 있어 추론 비용이 있습니다. 차기 리팩토링에서 한쪽으로 통일하는 것을 권장합니다.

frontend/src/components/common/InputField/InputField.tsx (2)

69-70: 경미한 가독성 개선: 입력 type 계산 변수로 분리

인라인 삼항은 짧지만, 재사용성과 가독성을 위해 지역 변수로 빼면 테스트/디버깅이 쉬워집니다.

예시:

const inputType = type === 'password' && !isPasswordVisible ? 'password' : 'text';
...
<Styled.Input type={inputType} ... />

75-76: prop 네이밍 혼용(hasError vs isError) 정리 제안(선택사항)

외부 prop isError → 내부 스타일 prop hasError 매핑은 동작엔 문제없지만, 팀 컨벤션에 맞춰 한쪽으로 통일하면 추후 유지보수 시 혼란을 줄일 수 있습니다.

frontend/src/components/common/CustomTextArea/CustomTextArea.styles.ts (3)

12-17: 색상 매직 넘버를 디자인 토큰/변수로 치환하세요

디자인 일관성과 향후 테마 교체 용이성을 위해 하드코딩된 색(#787878)을 토큰/변수로 추상화하는 것을 권장합니다.

 export const Label = styled.label`
   font-size: 1.125rem;
   margin-bottom: 12px;
   font-weight: 600;
-  color: #787878;
+  color: var(--color-text-secondary, #787878);
 `;

25-32: 배경/테두리 색 중복 제거 및 명명된 변수로 정리

border와 background가 동일 색(#f5f5f5)을 각각 하드코딩하고 있어 유지보수성이 떨어집니다. CSS 커스텀 프로퍼티로 한 번 정의해 재사용하면 가독성과 변경 용이성이 좋아집니다. 경계가 흐려질 수 있으므로(동일 톤) 디자인 확인도 부탁드립니다.

 export const TextArea = styled.textarea<{ hasError?: boolean }>`
   flex: 1;
   height: 45px;
   padding: 12px 18px;
-  border: 1px solid ${({ hasError }) => (hasError ? 'red' : '#f5f5f5')};
-  border-radius: 10px;
-  background: var(--f5, #f5f5f5);
+  --textarea-bg: var(--f5, #f5f5f5);
+  --textarea-border: var(--f5, #f5f5f5);
+  border: 1px solid ${({ hasError }) => (hasError ? 'var(--color-error, red)' : 'var(--textarea-border)')};
+  border-radius: 10px;
+  background: var(--textarea-bg);
   outline: none;

39-44: 포커스 링/테두리 색상 토큰화 및 대비 확인 권장

포커스 색도 매직 넘버를 토큰/변수로 치환해 일관성 확보를 권장합니다. 신규 배경(#f5f5f5)과의 대비가 충분한지 함께 확인해 주세요.

   &:focus {
-    border-color: ${({ hasError }) => (hasError ? 'red' : '#007bff')};
+    border-color: ${({ hasError }) =>
+      hasError ? 'var(--color-error, red)' : 'var(--color-focus, #007bff)'};
     box-shadow: 0 0 3px
-      ${({ hasError }) =>
-        hasError ? 'rgba(255, 0, 0, 0.5)' : 'rgba(0, 123, 255, 0.5)'};
+      ${({ hasError }) =>
+        hasError
+          ? 'var(--focus-ring-error, rgba(255, 0, 0, 0.5))'
+          : 'var(--focus-ring, rgba(0, 123, 255, 0.5))'};
   }

다음 스크립트로 대비 비율을 빠르게 점검할 수 있습니다(참고용).

#!/bin/bash
python - <<'PY'
def hex_to_rgb(h):
    h=h.lstrip('#'); h=''.join(c*2 for c in h) if len(h)==3 else h
    return tuple(int(h[i:i+2],16) for i in (0,2,4))
def to_lin(v):
    s=v/255.0
    return s/12.92 if s<=0.04045 else ((s+0.055)/1.055)**2.4
def rel_lum(rgb):
    r,g,b=(to_lin(x) for x in rgb)
    return 0.2126*r+0.7152*g+0.0722*b
def contrast(fg,bg):
    L1,L2=rel_lum(hex_to_rgb(fg)),rel_lum(hex_to_rgb(bg))
    Lmax,Lmin=(L1,L2) if L1>=L2 else (L2,L1)
    return (Lmax+0.05)/(Lmin+0.05)
pairs=[
    ('#787878','#ffffff','label on white'),
    ('#007bff','#f5f5f5','focus ring vs bg'),
    ('#ff0000','#f5f5f5','error ring vs bg')
]
for fg,bg,name in pairs:
    print(f'{name}: {contrast(fg,bg):.2f}')
PY
frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx (2)

38-47: KST 고정 오프셋 매직 넘버 상수화 및 헬퍼 외부 추출

  • 9 * 60 * 60 * 1000은 상수로 정의해 의미를 드러내세요(가이드라인: 매직 넘버 제거).
  • null 체크 직후 date?.getTime()의 옵셔널 체이닝은 불필요합니다.
  • 재렌더마다 헬퍼가 재생성되지 않도록 컴포넌트 외부 유틸로 이동 권장.

현재 라인 한정 수정:

-  const correctRequestKoreanDate = (date: Date | null): Date | null => {
-    if (!date) return null;
-    return new Date(date?.getTime() + 9 * 60 * 60 * 1000);
-  }
+  const correctRequestKoreanDate = (date: Date | null): Date | null => {
+    if (!date) return null;
+    return new Date(date.getTime() + KST_OFFSET_MS);
+  }
 
-  const correctResponseKoreanDate = (date: Date | null): Date | null => {
-    if (!date) return null;
-    return new Date(date?.getTime() - 9 * 60 * 60 * 1000);
-  }
+  const correctResponseKoreanDate = (date: Date | null): Date | null => {
+    if (!date) return null;
+    return new Date(date.getTime() - KST_OFFSET_MS);
+  }

컴포넌트 밖에 추가(지원 코드):

// 파일 상단(컴포넌트 외부)
export const KST_OFFSET_MS = 9 * 60 * 60 * 1000 as const;
// 필요 시 utils/timezone.ts 등으로 이동해 재사용을 권장합니다.

원하시면 timezone 유틸(테스트 포함)로 분리하는 PR 보일러플레이트를 생성해드릴게요.


32-33: 초기값으로 현재 시각(now) 강제 세팅 금지 — null로 대체

  • 위험: now를 기본값으로 두면 의도치 않게 ‘지금’ 시각이 저장될 수 있음. 아래처럼 기본값을 null로 바꿔 명시적 입력을 유도하세요.
-    setRecruitmentStart((prev) => prev ?? correctResponseKoreanDate(initialStart) ?? now);
-    setRecruitmentEnd((prev) => prev ?? correctResponseKoreanDate(initialEnd) ?? now);
+    setRecruitmentStart((prev) => prev ?? correctResponseKoreanDate(initialStart) ?? null);
+    setRecruitmentEnd((prev) => prev ?? correctResponseKoreanDate(initialEnd) ?? null);

Line 30의 const now = new Date(); 삭제 가능.

  • 검증: 제공된 검색 결과에서 'Asia/Seoul', 'KST', 'utcToZonedTime', 'zonedTimeToUtc' 등 타임존 보정 관련 참조가 발견되지 않았으므로 parseRecruitmentPeriod가 별도 타임존 보정을 하는 것으로 보이지 않습니다(중복 보정 우려 낮음).

  • 권고: effect 내부에서 컴포넌트 하단에 정의된 헬퍼를 참조하고 있으면 exhaustive-deps 경고가 발생할 수 있으니 해당 헬퍼를 컴포넌트 밖으로 옮기거나 useCallback으로 안정화하세요.

frontend/src/apis/auth/changePassword.ts (2)

17-20: JSON이 아닌 에러 응답 안전 처리로 2차 예외 방지

서버가 빈 본문/텍스트를 반환하면 response.json()에서 또 다른 에러가 나며 UX가 불안정합니다. 콘텐츠 타입 확인 후 안전 파싱으로 보강하세요.

-  if (!response.ok) {
-    const errorData = await response.json();
-    throw new Error(errorData.message || '비밀번호 변경에 실패했습니다.');
-  }
+  if (!response.ok) {
+    let message = '비밀번호 변경에 실패했습니다.';
+    const contentType = response.headers.get('content-type') ?? '';
+    try {
+      if (contentType.includes('application/json')) {
+        const errorData = await response.json();
+        message = errorData?.message ?? message;
+      } else {
+        const text = await response.text();
+        if (text) message = text;
+      }
+    } catch {
+      // ignore parse errors and fall back to default message
+    }
+    throw new Error(message);
+  }

8-15: 취소/타임아웃 대응을 위한 AbortSignal 옵션 지원

호출자가 AbortController로 취소/타임아웃을 제어할 수 있도록 signal을 전달 가능하게 해두는 편이 안전합니다.

-export const changePassword = async (payload: ChangePasswordPayload): Promise<void> => {
+export const changePassword = async (
+  payload: ChangePasswordPayload,
+  opts?: { signal?: AbortSignal },
+): Promise<void> => {
   const response = await secureFetch(`${API_BASE_URL}/auth/user/`, {
     method: 'PUT',
     headers: {
       'Content-Type': 'application/json',
     },
-    body: JSON.stringify(payload),
+    body: JSON.stringify(payload),
+    signal: opts?.signal,
   });
frontend/src/pages/AdminPage/tabs/AccountEditTab/AccountEditTab.styles.ts (1)

16-22: 성공/오류 메시지에 ARIA 라이브 영역 적용

스크린 리더 공지 강화를 위해 role/aria-live를 부여해 주세요.

-export const SuccessMessage = styled.p`
+export const SuccessMessage = styled.p.attrs({
+  role: 'status',
+  'aria-live': 'polite',
+})`
   color: #28a745; /* 성공을 의미하는 긍정적인 녹색 */
   font-size: 0.9rem; /* 일반 텍스트보다 약간 작게 설정 */
   text-align: left; /* 메시지 좌측 정렬 */
   margin: 8px 0; /* 위아래로 적절한 여백 추가 */
   font-weight: 500; /* 살짝 굵게 하여 가독성 확보 */
 `;
 
-export const ErrorMessage = styled.p`
+export const ErrorMessage = styled.p.attrs({
+  role: 'alert',
+  'aria-live': 'assertive',
+})`
   color: #dc3545; /* 실패를 의미하는 명확한 빨간색 */
   font-size: 0.9rem;
   text-align: left;
   margin: 8px 0;
   font-weight: 500;
 `;

Also applies to: 24-30

frontend/src/pages/AdminPage/tabs/AccountEditTab/AccountEditTab.tsx (3)

14-16: alert 제거하고 인라인 오류 표시 + 제출 비활성화

차단형 alert 대신 오류 상태를 화면에 노출하고, 폼이 유효하지 않으면 버튼을 비활성화하세요. 접근성/UX 모두 개선됩니다.

   const [successMessage, setSuccessMessage] = useState('');
   const [isLoading, setIsLoading] = useState(false); // 1. 로딩 상태 추가
+  const [errorMessage, setErrorMessage] = useState('');
@@
   const handleChangePassword = async () => {
     if (isLoading) return;
 
     setSuccessMessage('');
+    setErrorMessage('');
 
     if (!newPassword || !confirmPassword) {
-      alert('새 비밀번호와 확인 필드를 모두 입력해주세요.');
+      setErrorMessage('새 비밀번호와 확인 필드를 모두 입력해주세요.');
       return;
     }
     if (!PASSWORD_REGEX.test(newPassword)) {
-      alert('비밀번호가 정책에 맞지 않습니다. (영문, 숫자, 특수문자 포함 8~20자)');
+      setErrorMessage('비밀번호가 정책에 맞지 않습니다.');
       return;
     }
     if (newPassword !== confirmPassword) {
-      alert('새 비밀번호가 일치하지 않습니다.');
+      setErrorMessage('새 비밀번호가 일치하지 않습니다.');
       return;
     }
@@
     try {
       await changePassword({ password: newPassword });
       setSuccessMessage('비밀번호가 성공적으로 변경되었습니다.');
       setNewPassword('');
       setConfirmPassword('');
     } catch (err) {
       if (err instanceof Error) {
-        alert(err.message);
+        setErrorMessage(err.message);
       } else {
-        alert('알 수 없는 오류가 발생했습니다.');
+        setErrorMessage('알 수 없는 오류가 발생했습니다.');
       }
     } finally {
       setIsLoading(false); 
     }
   };
@@
-      {successMessage && <Styled.SuccessMessage>{successMessage}</Styled.SuccessMessage>}
+      {errorMessage && <Styled.ErrorMessage>{errorMessage}</Styled.ErrorMessage>}
+      {successMessage && <Styled.SuccessMessage>{successMessage}</Styled.SuccessMessage>}
       <br />
@@
-      <Button
+      <Button
         width={'100%'}
         animated
         onClick={handleChangePassword}
-        disabled={isLoading}
+        disabled={
+          isLoading ||
+          !newPassword ||
+          !confirmPassword ||
+          !PASSWORD_REGEX.test(newPassword) ||
+          newPassword !== confirmPassword
+        }
       >

Also applies to: 21-37, 41-55, 96-106


70-80: 비밀번호 자동완성 힌트 추가

비밀번호 매니저 호환성을 위해 autoComplete="new-password"를 지정하세요.

       <InputField
         placeholder='새 비밀번호'
         type='password'
         value={newPassword}
         onChange={(e) => setNewPassword(e.target.value)}
         onClear={() => setNewPassword('')}
         maxLength={MAX_PASSWORD_LENGTH}
+        autoComplete="new-password"
         isError={isPasswordInvalid}
         isSuccess={newPassword.length > 0 && !isPasswordInvalid}
         helperText={isPasswordInvalid ? `영문, 숫자, 특수문자 포함 ${MIN_PASSWORD_LENGTH}~${MAX_PASSWORD_LENGTH}자` : ''}
       />
@@
       <InputField
         placeholder='새 비밀번호 재입력'
         type='password'
         value={confirmPassword}
         onChange={(e) => setConfirmPassword(e.target.value)}
         onClear={() => setConfirmPassword('')}
         maxLength={MAX_PASSWORD_LENGTH}
+        autoComplete="new-password"
         isError={isPasswordMismatch}
         isSuccess={confirmPassword.length > 0 && !isPasswordMismatch}
         helperText={isPasswordMismatch ? '비밀번호가 일치하지 않습니다.' : ''}
       />

Also applies to: 83-93


60-61: UI 간격에
남용은 지양

간격은 스타일로 제어하는 편이 일관됩니다. 간단한 마진 유틸이나 래퍼 컴포넌트를 고려해 주세요.

Also applies to: 81-82, 94-96

frontend/src/pages/AdminPage/components/SideBar/SideBar.styles.ts (4)

18-18: 매직 넘버(19px) 대신 토큰/상수 사용 권장

디자인 스케일과 일관성을 위해 테마 토큰(theme.space 등)이나 파일 상수로 추출해 주세요.


37-43: Divider 스타일: border-top 사용 및 color 제거

hrcolor로 테두리 색을 주는 것은 브라우저별 일관성이 떨어집니다. border-top으로 명시해 주세요. 마진도 축약형으로 정리하면 좋습니다.

 export const Divider = styled.hr`
   width: 100%;
-  border: 1px solid;
-  color: #C5C5C5;
-  height: 0;
-  margin: 16px 0px 16px 0px;
+  border: 0;
+  border-top: 1px solid #C5C5C5;
+  height: 0;
+  margin: 16px 0;
 `;

48-48: gap 16px도 토큰화 검토

반복되는 간격값은 테마/상수로 통합해 유지보수성을 높여 주세요.


52-58: 유효하지 않은 font-weight 값과 비효율 속성

font-weight: medium은 유효하지 않습니다(숫자 값 사용). 또한 align-itemsdisplay: flex가 없으면 무의미합니다.

 export const SidebarCategoryTitle = styled.p`
-  align-items: center;
+  display: flex;
+  align-items: center;
   padding: 6px 0px 6px 10px;
   font-size: 0.75rem;
-  font-weight: medium;
+  font-weight: 500;
   color: #989898;
 `;
frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx (5)

13-17: TabItem에 비활성화/메시지 필드 추가 제안

라벨 문자열 비교 대신 스키마로 제어하세요.

 interface TabItem {
   label: string;
   path: string;
+  disabled?: boolean;
+  disabledMessage?: string;
 }

23-49: 준비 중 기능은 데이터로 표현

'회원탈퇴' 가드 로직을 데이터로 옮기면 뷰/로직 분리가 좋아집니다.

       { label: '비밀번호 수정', path: '/admin/account-edit'},
-      { label: '회원탈퇴', path: '/admin/user-delete'},
+      { label: '회원탈퇴', path: '/admin/user-delete', disabled: true, disabledMessage: '회원탈퇴 기능은 아직 준비 중이에요. ☺️'},

55-59: active 계산: startsWith 대신 matchPath 사용 고려

프리픽스 충돌을 피하려면 matchPath가 더 안전합니다(중첩 라우트는 end: false).

-  const activeTab = useMemo(() => { 
-    return tabs.map((tab) =>
-      tab.items.findIndex((item) => location.pathname.startsWith(item.path)),
-    );
-  }, [location.pathname]);
+  const activeTab = useMemo(
+    () =>
+      tabs.map((tab) =>
+        tab.items.findIndex((item) => matchPath({ path: item.path, end: false }, location.pathname) != null),
+      ),
+    [location.pathname],
+  );

추가: 상단 import에 matchPath를 포함하세요.


61-71: 라벨 비교 가드 제거하고 스키마 기반으로 처리

문자열 의존은 취약합니다. 위에서 제안한 disabled 필드 활용:

-  const handleTabClick = (item: TabItem) => {
-    // if (item.label === '아이디/비밀번호 수정') {
-    //   alert('아이디/비밀번호 수정 기능은 아직 준비 중이에요. ☺️');
-    //   return;
-    // }
-    if (item.label === '회원탈퇴') {
-      alert('회원탈퇴 기능은 아직 준비 중이에요. ☺️');
-      return;
-    }
-    navigate(item.path);
-  };
+  const handleTabClick = (item: TabItem) => {
+    if (item.disabled) {
+      if (item.disabledMessage) alert(item.disabledMessage);
+      return;
+    }
+    navigate(item.path);
+  };

102-115: active 표시 접근성 강화(aria-current) 및 스타일링 훅 일원화

className='active' 유지하되 스크린리더 인지를 위해 aria-current를 추가하세요.

-                className={activeTab[tabIndex] === itemIndex ? 'active' : ''}
+                className={activeTab[tabIndex] === itemIndex ? 'active' : ''}
+                aria-current={activeTab[tabIndex] === itemIndex ? 'page' : undefined}

추가로, styled에서 $active prop이나 [aria-current="page"] 셀렉터로 스타일링을 일치시키는 방법도 고려해 보세요.

frontend/src/components/common/Header/Header.stories.tsx (1)

13-17: SearchProvider 제거에 따른 스토리 간 상태 누수 방지용 리셋 데코레이터를 추가하세요.

Zustand는 프로세스 단위 싱글턴이라 스토리 전환 시 검색 상태가 남을 수 있습니다. 데코레이터에서 resetSearch()를 호출해 독립성을 확보하는 것을 권장합니다.

적용 예시(diff):

 import Header from './Header';
 import { BrowserRouter } from 'react-router-dom';
+import { useSearchStore } from '../../../store/useSearchStore';

 ...

   decorators: [
-    (Story) => (
-      <BrowserRouter>
-        <Story />
-      </BrowserRouter>
-    ),
+    (Story) => {
+      // 각 스토리 렌더 전 검색 상태 초기화
+      useSearchStore.getState().resetSearch();
+      return (
+        <BrowserRouter>
+          <Story />
+        </BrowserRouter>
+      );
+    },
   ],
frontend/src/components/ClubStateBox/ClubStateBox.tsx (1)

22-26: ALWAYS 상태 추가는 👍. 타입 안전성 강화와 색상 토큰화 제안을 드립니다.

  • state: string 대신 리터럴 유니온으로 제한하면 오타/미지정 상태를 컴파일 타임에 차단할 수 있습니다.
  • 색상/문구는 상수로 분리해 재사용성과 일관성을 높여 주세요. (가이드라인의 “매직 넘버/상수화”)

보완 예시(추가 코드):

type ClubState = 'OPEN' | 'UPCOMING' | 'CLOSED' | 'ALWAYS';

const ALWAYS_BG = 'rgba(235, 250, 241, 1)';
const ALWAYS_FG = '#3ACD73' as const;
const ALWAYS_TEXT = '상시모집' as const;

const stateStyles: Record<ClubState, { backgroundColor: string; color: string; text: string }> = {
  /* ... */,
  ALWAYS: { backgroundColor: ALWAYS_BG, color: ALWAYS_FG, text: ALWAYS_TEXT },
};

interface ClubStateBoxProps {
  state: ClubState;
}

StyledBox의 width/height/padding 등 고정값도 테마/토큰으로 이동하면 유지보수에 유리합니다.

frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.styles.ts (1)

4-9: 불필요한 CSS 속성 제거 및 상수화 제안

  • styled.inputalign-items는 효과가 없습니다. 제거해 주세요.
  • 변경된 마진/여백 값은 상수(또는 테마 토큰)로 정의하여 재사용성을 높이세요. (가이드라인: 매직 넘버 상수화)

적용 diff:

 export const FormTitle = styled.input`
   width: 100%;
   padding: 10px 12px;
-  align-items: center;
   border-radius: 10px;
   background: var(--f5, #f5f5f5);
   font-size: 2.5rem;
   font-weight: 700;
   border: none;
   outline: none;
   margin: 60px 0px 35px 0px;

참고(추가 코드): 상수 선언 후 사용

const FORM_TITLE_MARGIN = '60px 0 35px 0';
const QUESTION_MARGIN_TOP = '40px';
-  margin-top: 40px;
+  margin-top: ${QUESTION_MARGIN_TOP};

Also applies to: 13-13, 28-28

frontend/src/components/common/Button/Button.tsx (2)

30-37: 비활성화 시 hover/active 효과 차단

&:hover가 비활성화에도 적용될 수 있습니다. :not(:disabled) 가드를 두어 시각적 일관성을 보장하세요.

적용 diff:

-  &:hover {
+  &:not(:disabled):hover {
     background-color: #333333;
     ${({ animated }) =>
       animated &&
       css`
         animation: ${pulse} 0.4s ease-in-out;
       `}
   }
 
-  &:active {
+  &:not(:disabled):active {
     transform: ${({ animated }) => (animated ? 'scale(0.95)' : 'none')};
   }

Also applies to: 43-49


3-10: props 타입 구체화 제안

  • type?: string'button' | 'submit' | 'reset'
  • onClick?: () => voidReact.MouseEventHandler<HTMLButtonElement> (이벤트 전달 필요 시)

적용 diff:

 export interface ButtonProps {
   width?: string;
   children: React.ReactNode;
-  type?: string;
-  onClick?: () => void;
+  type?: 'button' | 'submit' | 'reset';
+  onClick?: React.MouseEventHandler<HTMLButtonElement>;
   animated?: boolean;
   disabled?: boolean;
 }
frontend/src/store/useCategoryStore.ts (3)

17-19: 초기값 'all'은 매직 문자열입니다. 상수로 분리해 의미를 드러내세요.

가이드라인 상수화 원칙 준수 및 오타 방지에 도움 됩니다.

예시:

+const DEFAULT_CATEGORY = 'all' as const;
 ...
-        selectedCategory: 'all',
+        selectedCategory: DEFAULT_CATEGORY,

28-34: 셀렉터 훅 반환 구조는 👍. 필요 시 얕은 비교(shallow)로 리렌더 최소화도 고려해 보세요.

예시:

import { shallow } from 'zustand/shallow';
const { selectedCategory, setSelectedCategory } = useCategoryStore(
  (s) => ({ selectedCategory: s.selectedCategory, setSelectedCategory: s.setSelectedCategory }),
  shallow
);

21-23: SSR 환경에서 sessionStorage 접근 방어 필요

파일: frontend/src/store/useCategoryStore.ts (라인 21–23)

createJSONStorage(() => sessionStorage)는 SSR(서버 렌더링)에서 window/sessionStorage가 없어 ReferenceError 또는 hydration mismatch를 일으킬 수 있으므로 클라이언트 전용 보장 또는 서버용 noop storage로 방어하세요. 권장 대안(간단히 선택):

  • 클라이언트 전용: 모듈/컴포넌트 상단에 "use client"를 추가해 클라이언트에서만 로드.
  • 안전 팩토리(권장):
const getStorage = () =>
  typeof window === 'undefined'
    ? { getItem: () => null, setItem: () => {}, removeItem: () => {} }
    : sessionStorage;

storage: createJSONStorage(() => getStorage())
  • skipHydration 후 클라이언트에서 수동 rehydrate (hydration mismatch 회피):
// persist(..., { storage: createJSONStorage(() => sessionStorage), skipHydration: true })
useEffect(() => { store.persist.rehydrate(); }, []);

참고: sessionStorage는 클라이언트 전용이며 민감 정보 저장에 적합하지 않습니다.

frontend/src/store/useSearchStore.ts (1)

26-45: reset 전용 훅 추가하고 useSearchStore.getState() 직접 호출을 훅으로 대체하세요.

다음 파일들에서 직접 호출이 발견되었습니다 — 아래 훅을 export하고 해당 호출들을 대체하세요.

export const useSearchReset = () => useSearchStore((s) => s.resetSearch);

수정 필요 위치:

  • frontend/src/services/header/useHeaderService.ts — lines 19–20
  • frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx — lines 40–41

훅 추가 위치 제안: frontend/src/store/useSearchStore.ts

frontend/src/services/header/useHeaderService.ts (1)

19-23: 홈 이동 전 검색상태 초기화·트래킹 순서 조정 제안

라우팅 후 리셋 시 홈 진입 순간에 이전 검색 상태가 잠깐 보일 수 있습니다. 리셋/트래킹을 먼저 수행하고 마지막에 navigate를 호출하면 UX 플리커를 줄일 수 있습니다.

-      navigate('/');
-      const { resetSearch } = useSearchStore.getState();
-      resetSearch();
-      trackEvent(trackEventNames[device]);
+      const { resetSearch } = useSearchStore.getState();
+      resetSearch();
+      trackEvent(trackEventNames[device]);
+      navigate('/');
frontend/src/pages/MainPage/components/SearchBox/SearchBox.tsx (2)

21-32: 이벤트명 상수화 및 처리 순서 미세 조정

  • 트래킹 이벤트명을 상수로 통일하면 유지보수성이 좋아집니다.
  • 상태 업데이트를 라우팅보다 먼저 수행하면 홈 진입 시 깜빡임을 줄일 수 있습니다.
+import { EVENT_NAME } from '@/constants/eventName';
@@
-  const handleSearch = () => {
+  const handleSearch = () => {
     const currentPage = location.pathname;
-    redirectToHome();
     setKeyword(inputValue);
     setSelectedCategory('all');
     setIsSearching(true);
+    redirectToHome();
 
-    trackEvent('Search Executed', {
+    trackEvent(EVENT_NAME.SEARCH_EXECUTED, {
       inputValue: inputValue,
       page: currentPage,
     });
   };

25-26: 'all' 매직 스트링 상수화

카테고리 기본값 'all'은 상수(예: CATEGORY_ALL)로 노출해 사용처 간 일관성을 확보하세요.

frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts (2)

292-305: ApplicantAllSelectMenu의 open prop도 transient로 변경

일관성 유지.

-export const ApplicantAllSelectMenu = styled.div<{ open: boolean }>`
-  display: ${({ open }) => (open ? 'block' : 'none')};
+export const ApplicantAllSelectMenu = styled.div.withConfig({
+  shouldForwardProp: (prop) => prop !== '$open',
+})<{ $open: boolean }>`
+  display: ${({ $open }) => ($open ? 'block' : 'none')};

99-104: 세로 구분선 높이 지정

height: auto는 컨텍스트에 따라 기대 높이를 보장하지 않습니다. flex 컨테이너 내에서는 align-self: stretch가 안전합니다.

 export const VerticalLine = styled.div`
   width: 1px;
-  height: auto;
+  height: auto;
   background-color: #dcdcdc;
   margin: 8px 4px;
+  align-self: stretch;
 `;
frontend/src/App.tsx (2)

50-77: 중첩 Routes 구조 간결화 제안

<PrivateRoute> 내부에 별도의 <Routes>를 두기보다 아웃렛 패턴으로 합치는 것이 가독성과 유지보수에 유리합니다.

-          <Route
-            path='/admin/*'
-            element={
-              <AdminClubProvider>
-                <PrivateRoute>
-                  <Routes>
-                    <Route path='' element={<AdminPage />}>
+          <Route
+            path='/admin/*'
+            element={
+              <AdminClubProvider>
+                <PrivateRoute />
+              </AdminClubProvider>
+            }
+          >
+            <Route path='' element={<AdminPage />}>
                       <Route
                         index
                         element={<Navigate to='club-info' replace />}
                       />
                       <Route path='club-info' element={<ClubInfoEditTab />} />
                       <Route path='recruit-edit' element={<RecruitEditTab />} />
                       <Route path='photo-edit' element={<PhotoEditTab />} />
                       <Route path='account-edit' element={<AccountEditTab />} />
                       <Route
                         path='application-edit'
                         element={<ApplicationEditTab />}
                       />
                       <Route path='applicants' element={<ApplicantsTab />} />
                       <Route
                         path='applicants/:questionId'
                         element={<ApplicantDetailPage />}
                       />
-                    </Route>
-                  </Routes>
-                </PrivateRoute>
-              </AdminClubProvider>
-            }
-          />
+            </Route>
+          </Route>

18-21: import 경로 일관성

동일 레벨의 파일에서 절대 경로(@/)와 상대 경로가 혼재합니다. 하나로 통일하세요.

frontend/src/constants/status.ts (1)

3-8: 타입 명시로 의도 강화

as const도 가능하지만, 읽는 이에게 더 명확하도록 readonly ApplicationStatus[]를 명시해 주세요. 주석은 한글 라벨 맵으로 분리하는 것을 권장합니다.

-export const AVAILABLE_STATUSES = [
+export const AVAILABLE_STATUSES: readonly ApplicationStatus[] = [
   ApplicationStatus.SUBMITTED, // 서류검토 (SUBMITTED 포함)
   ApplicationStatus.INTERVIEW_SCHEDULED, // 면접예정
   ApplicationStatus.ACCEPTED, // 합격
   ApplicationStatus.DECLINED, // 불합격
 ] as const;
frontend/src/apis/application/updateApplicantDetail.ts (1)

5-8: 파라미터 네이밍과 입력 검증

배치 업데이트이므로 applicants 등 복수 형태가 더 명확합니다. 또한 applicantIdundefined인 항목을 사전에 차단하세요.

-export const updateApplicantDetail = async (
-  applicant: UpdateApplicantParams[],
+export const updateApplicantDetail = async (
+  applicants: UpdateApplicantParams[],
   clubId: string,
 ) => {
+  if (applicants.some((a) => !a.applicantId)) {
+    throw new Error('applicantId가 누락된 항목이 있습니다.');
+  }
frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx (1)

31-42: 컴포넌트 내부에서는 getState 대신 셀렉터로 액션 주입 권장

useSearchStore.getState()는 사용 가능하지만, 컴포넌트 내부에서는 셀렉터로 액션을 주입하는 편이 일관되고 테스트/타입 추론에도 유리합니다.

-  const { setSelectedCategory } = useSelectedCategory();
+  const { setSelectedCategory } = useSelectedCategory();
+  const resetSearch = useSearchStore((s) => s.resetSearch);

   const trackEvent = useMixpanelTrack();

   const handleCategoryClick = (category: Category) => {
@@
-    const { resetSearch } = useSearchStore.getState();
-    resetSearch();
+    resetSearch();
frontend/src/pages/MainPage/MainPage.tsx (2)

60-70: 중첩 삼항 연산자 제거로 가독성 향상

가이드라인(복잡한/중첩 삼항 지양)에 맞춰 분기 렌더링을 변수로 분리하세요.

-        <Styled.ContentWrapper>
-          {isLoading ? (
-            <Spinner />
-          ) : isEmpty ? (
-            <Styled.EmptyResult>
-              앗, 조건에 맞는 동아리가 없어요.
-              <br />
-              다른 키워드나 조건으로 다시 시도해보세요!
-            </Styled.EmptyResult>
-          ) : (
-            <Styled.CardList>{clubList}</Styled.CardList>
-          )}
-        </Styled.ContentWrapper>
+        <Styled.ContentWrapper>
+          {(() => {
+            if (isLoading) return <Spinner />;
+            if (isEmpty)
+              return (
+                <Styled.EmptyResult>
+                  앗, 조건에 맞는 동아리가 없어요.
+                  <br />
+                  다른 키워드나 조건으로 다시 시도해보세요!
+                </Styled.EmptyResult>
+              );
+            return <Styled.CardList>{clubList}</Styled.CardList>;
+          })()}
+        </Styled.ContentWrapper>

26-29: 매직 문자열 상수화(ALL/OPEN)로 오탈자/불일치 방지

여러 곳에서 반복되는 'all', 'OPEN'을 상수로 치환해 의도를 명확히 하세요. selectedCategoryundefined일 가능성 대비도 함께 권장합니다.

+const ALL = 'all' as const;
+const OPEN = 'OPEN' as const;
@@
-  const recruitmentStatus = isFilterActive ? 'OPEN' : 'all';
-  const division = 'all';
-  const searchCategory = isSearching ? 'all' : selectedCategory;
+  const recruitmentStatus = isFilterActive ? OPEN : ALL;
+  const division = ALL;
+  const searchCategory = isSearching ? ALL : (selectedCategory ?? ALL);
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx (4)

19-32: 색상 하드코딩을 상수 맵으로 단순화하세요

switch 대신 상태→색상 맵을 사용하면 확장성과 가독성이 좋아집니다. 또한 매직 컬러를 상수화하세요.

다음 변경을 제안합니다:

-const getStatusColor = (status: ApplicationStatus | undefined): string => {
-  switch (status) {
-    case ApplicationStatus.ACCEPTED:
-      return 'var(--f5, #F5F5F5)';
-    case ApplicationStatus.SUBMITTED:
-      return '#E5F6FF';
-    case ApplicationStatus.INTERVIEW_SCHEDULED:
-      return '#E9FFF1';
-    case ApplicationStatus.DECLINED:
-      return '#FFE8E8';
-    default:
-      return 'var(--f5, #F5F5F5)';
-  }
-};
+const DEFAULT_STATUS_BG = 'var(--f5, #F5F5F5)';
+const STATUS_BG: Record<ApplicationStatus, string> = {
+  [ApplicationStatus.ACCEPTED]: DEFAULT_STATUS_BG,
+  [ApplicationStatus.SUBMITTED]: '#E5F6FF',
+  [ApplicationStatus.INTERVIEW_SCHEDULED]: '#E9FFF1',
+  [ApplicationStatus.DECLINED]: '#FFE8E8',
+};
+
+const getStatusColor = (status: ApplicationStatus | undefined): string =>
+  status ? STATUS_BG[status] ?? DEFAULT_STATUS_BG : DEFAULT_STATUS_BG;

44-44: clubId non-null 단언 사용 주의

clubId가 아직 미할당인 초기 렌더에서 mutate가 호출될 가능성에 방어 로직을 추가해 주세요.

다음처럼 디바운스 내부에서 가드하는 것을 권장합니다(아래 디바운스 코멘트의 diff에 포함).


57-79: 디바운스 함수 의존성/정리 보완 및 매직넘버 상수화

  • updateApplicant를 deps에 포함해 잠재적 스테일 클로저를 방지하세요.
  • 400ms를 상수로 분리하세요.
  • 언마운트 시 대기 중 호출을 취소하세요(가능하면 debounce 유틸에 cancel 제공).
  • clubId/questionId 존재 확인을 디바운스 콜백 첫 줄에서 가드하세요.

다음 변경을 제안합니다:

-  debounce((memo, status) => {
+  debounce((memo, status) => {
+        if (!clubId || !questionId) return;
         function isApplicationStatus(v: unknown): v is ApplicationStatus {
           return (
             typeof v === 'string' &&
             Object.values(ApplicationStatus).includes(v as ApplicationStatus)
           );
         }
@@
-        updateApplicant([
+        updateApplicant([
           {
             memo,
             status,
             applicantId: questionId,
           },
         ]);
-      }, 400),
-    [clubId, questionId],
+      }, DEBOUNCE_MS),
+    [clubId, questionId, updateApplicant],
   );

추가로 파일 상단(컴포넌트 내부 적절한 위치)에 상수와 cleanup을 배치하세요:

const DEBOUNCE_MS = 400;

useEffect(() => {
  return () => {
    // debounce 유틸이 cancel을 제공한다면 호출
    (updateApplicantDetail as any)?.cancel?.();
  };
}, [updateApplicantDetail]);

60-65: 타입 가드 미세 최적화

Object.values(ApplicationStatus).includes는 호출마다 배열 생성/탐색이 발생합니다. Set을 사전 계산해 사용하세요.

예시:

const STATUS_SET = new Set(Object.values(ApplicationStatus));
function isApplicationStatus(v: unknown): v is ApplicationStatus {
  return typeof v === 'string' && STATUS_SET.has(v as ApplicationStatus);
}
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (4)

277-285: '전체선택' 체크박스 핸들러는 onChange 사용 권장

폼 컨트롤에는 onChange가 더 일관적이며 접근성 도구 호환이 좋습니다. stopPropagation은 동일하게 유지 가능합니다.

-                  <Styled.ApplicantTableAllSelectCheckbox
+                  <Styled.ApplicantTableAllSelectCheckbox
                     checked={selectAll}
-                    onClick={(e: React.MouseEvent<HTMLInputElement>) => {
+                    onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
                       e.stopPropagation();
                       selectApplicantsByStatus('all');
                     }}
                   />

349-354: 리스트 key로 index 대신 안정적인 id 사용

재정렬/필터링 시 불필요한 재마운트를 방지합니다.

-              <Styled.ApplicantTableRow
-                key={index}
+              <Styled.ApplicantTableRow
+                key={item.id}
                 onClick={() => navigate(`/admin/applicants/${item.id}`)}
                 style={{ cursor: 'pointer' }}
               >

252-263: 삭제 아이콘의 disabled 동작 확인 필요

Styled.DeleteButton이 button인지 img인지에 따라 disabled가 무시될 수 있습니다. img라면 클릭 차단/포커스 제거가 필요합니다.

가능한 대안:

  • button 요소로 구현하고 아이콘을 background/image로 사용
  • 또는 disabled 시 pointer-events: none; aria-disabled 적용 및 onClick에서 early return

392-401: 날짜 포맷 유틸로 분리 권장

IIFE/매직 포맷이 반복되기 쉬우므로 공용 유틸로 추출하세요.

예시:

export const formatDateYYYYMMDD = (iso: string) => {
  const d = new Date(iso);
  const y = d.getFullYear();
  const m = String(d.getMonth() + 1).padStart(2, '0');
  const day = String(d.getDate()).padStart(2, '0');
  return `${y}-${m}-${day}`;
};
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • 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 5439c6c and 96ef84c.

⛔ Files ignored due to path filters (7)
  • frontend/package-lock.json is excluded by !**/package-lock.json
  • frontend/src/assets/images/icons/applicant_delete.svg is excluded by !**/*.svg
  • frontend/src/assets/images/icons/applicant_delete_disabled.svg is excluded by !**/*.svg
  • frontend/src/assets/images/icons/applicant_delete_hover.svg is excluded by !**/*.svg
  • frontend/src/assets/images/icons/applicant_select_arrow.svg is excluded by !**/*.svg
  • frontend/src/assets/images/icons/delete_choice.svg is excluded by !**/*.svg
  • frontend/src/assets/images/icons/delete_question.svg is excluded by !**/*.svg
📒 Files selected for processing (38)
  • frontend/package.json (1 hunks)
  • frontend/src/App.tsx (2 hunks)
  • frontend/src/apis/applicants/deleteApplicants.ts (1 hunks)
  • frontend/src/apis/application/updateApplicantDetail.ts (1 hunks)
  • frontend/src/apis/auth/changePassword.ts (1 hunks)
  • frontend/src/components/ClubStateBox/ClubStateBox.tsx (1 hunks)
  • frontend/src/components/application/QuestionTitle/QuestionTitle.styles.ts (1 hunks)
  • frontend/src/components/application/QuestionTitle/QuestionTitle.tsx (3 hunks)
  • frontend/src/components/application/questionTypes/Choice.styles.ts (1 hunks)
  • frontend/src/components/application/questionTypes/Choice.tsx (2 hunks)
  • frontend/src/components/common/Button/Button.tsx (3 hunks)
  • frontend/src/components/common/CustomTextArea/CustomTextArea.styles.ts (3 hunks)
  • frontend/src/components/common/Header/Header.stories.tsx (1 hunks)
  • frontend/src/components/common/InputField/InputField.styles.ts (3 hunks)
  • frontend/src/components/common/InputField/InputField.tsx (3 hunks)
  • frontend/src/constants/status.ts (1 hunks)
  • frontend/src/context/CategoryContext.tsx (0 hunks)
  • frontend/src/context/SearchContext.tsx (0 hunks)
  • frontend/src/hooks/queries/applicants/useDeleteApplicants.ts (1 hunks)
  • frontend/src/hooks/queries/applicants/useUpdateApplicant.ts (1 hunks)
  • frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.styles.ts (1 hunks)
  • frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.tsx (2 hunks)
  • frontend/src/pages/AdminPage/components/SideBar/SideBar.styles.ts (2 hunks)
  • frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx (2 hunks)
  • frontend/src/pages/AdminPage/tabs/AccountEditTab/AccountEditTab.styles.ts (1 hunks)
  • frontend/src/pages/AdminPage/tabs/AccountEditTab/AccountEditTab.tsx (1 hunks)
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx (4 hunks)
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts (5 hunks)
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (5 hunks)
  • frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.styles.ts (2 hunks)
  • frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx (1 hunks)
  • frontend/src/pages/MainPage/MainPage.tsx (2 hunks)
  • frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx (3 hunks)
  • frontend/src/pages/MainPage/components/SearchBox/SearchBox.tsx (1 hunks)
  • frontend/src/services/header/useHeaderService.ts (2 hunks)
  • frontend/src/store/useCategoryStore.ts (1 hunks)
  • frontend/src/store/useSearchStore.ts (1 hunks)
  • frontend/src/types/applicants.ts (2 hunks)
💤 Files with no reviewable changes (2)
  • frontend/src/context/SearchContext.tsx
  • frontend/src/context/CategoryContext.tsx
🧰 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/components/common/InputField/InputField.tsx
  • frontend/src/components/application/QuestionTitle/QuestionTitle.styles.ts
  • frontend/src/components/common/CustomTextArea/CustomTextArea.styles.ts
  • frontend/src/components/ClubStateBox/ClubStateBox.tsx
  • frontend/src/apis/auth/changePassword.ts
  • frontend/src/components/common/Header/Header.stories.tsx
  • frontend/src/services/header/useHeaderService.ts
  • frontend/src/components/common/InputField/InputField.styles.ts
  • frontend/src/apis/applicants/deleteApplicants.ts
  • frontend/src/pages/AdminPage/tabs/AccountEditTab/AccountEditTab.styles.ts
  • frontend/src/store/useCategoryStore.ts
  • frontend/src/pages/MainPage/MainPage.tsx
  • frontend/src/constants/status.ts
  • frontend/src/types/applicants.ts
  • frontend/src/store/useSearchStore.ts
  • frontend/src/components/application/questionTypes/Choice.styles.ts
  • frontend/src/hooks/queries/applicants/useDeleteApplicants.ts
  • frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.styles.ts
  • frontend/src/components/common/Button/Button.tsx
  • frontend/src/pages/AdminPage/tabs/AccountEditTab/AccountEditTab.tsx
  • frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx
  • frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.styles.ts
  • frontend/src/components/application/QuestionTitle/QuestionTitle.tsx
  • frontend/src/pages/AdminPage/components/SideBar/SideBar.styles.ts
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx
  • frontend/src/hooks/queries/applicants/useUpdateApplicant.ts
  • frontend/src/pages/MainPage/components/SearchBox/SearchBox.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx
  • frontend/src/apis/application/updateApplicantDetail.ts
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts
  • frontend/src/components/application/questionTypes/Choice.tsx
  • frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx
  • frontend/src/App.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/components/QuestionBuilder/QuestionBuilder.tsx
  • frontend/src/components/common/InputField/InputField.tsx
  • frontend/src/components/ClubStateBox/ClubStateBox.tsx
  • frontend/src/components/common/Header/Header.stories.tsx
  • frontend/src/pages/MainPage/MainPage.tsx
  • frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx
  • frontend/src/components/common/Button/Button.tsx
  • frontend/src/pages/AdminPage/tabs/AccountEditTab/AccountEditTab.tsx
  • frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx
  • frontend/src/components/application/QuestionTitle/QuestionTitle.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx
  • frontend/src/pages/MainPage/components/SearchBox/SearchBox.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx
  • frontend/src/components/application/questionTypes/Choice.tsx
  • frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx
  • frontend/src/App.tsx
🧠 Learnings (2)
📚 Learning: 2025-07-19T05:09:10.702Z
Learnt from: CR
PR: Moadong/moadong#0
File: frontend/.cursorrules:0-0
Timestamp: 2025-07-19T05:09:10.702Z
Learning: Applies to frontend/**/*.tsx : Break down broad state management into smaller, focused hooks or contexts.

Applied to files:

  • frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx
  • frontend/src/pages/MainPage/components/SearchBox/SearchBox.tsx
📚 Learning: 2025-03-19T05:18:07.818Z
Learnt from: seongwon030
PR: Moadong/moadong#195
File: frontend/src/pages/AdminPage/AdminPage.tsx:7-7
Timestamp: 2025-03-19T05:18:07.818Z
Learning: AdminPage.tsx에서 현재 하드코딩된 클럽 ID('67d2e3b9b15c136c6acbf20b')는 로그인 기능 구현 후 동적으로 가져오는 방식으로 수정될 예정입니다.

Applied to files:

  • frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx
🧬 Code graph analysis (14)
frontend/src/apis/auth/changePassword.ts (1)
frontend/src/apis/auth/secureFetch.ts (1)
  • secureFetch (3-41)
frontend/src/services/header/useHeaderService.ts (1)
frontend/src/store/useSearchStore.ts (1)
  • useSearchStore (14-24)
frontend/src/apis/applicants/deleteApplicants.ts (1)
frontend/src/apis/auth/secureFetch.ts (1)
  • secureFetch (3-41)
frontend/src/pages/MainPage/MainPage.tsx (2)
frontend/src/store/useCategoryStore.ts (1)
  • useSelectedCategory (28-34)
frontend/src/store/useSearchStore.ts (2)
  • useSearchKeyword (26-30)
  • useSearchIsSearching (32-36)
frontend/src/types/applicants.ts (1)
frontend/src/types/application.ts (1)
  • AnswerItem (55-58)
frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx (2)
frontend/src/store/useCategoryStore.ts (1)
  • useSelectedCategory (28-34)
frontend/src/store/useSearchStore.ts (1)
  • useSearchStore (14-24)
frontend/src/pages/AdminPage/tabs/AccountEditTab/AccountEditTab.tsx (1)
frontend/src/apis/auth/changePassword.ts (1)
  • changePassword (8-21)
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (5)
frontend/src/context/AdminClubContext.tsx (1)
  • useAdminClubContext (30-37)
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/constants/status.ts (1)
  • AVAILABLE_STATUSES (3-8)
frontend/src/hooks/queries/applicants/useUpdateApplicant.ts (2)
frontend/src/types/applicants.ts (1)
  • UpdateApplicantParams (26-30)
frontend/src/apis/application/updateApplicantDetail.ts (1)
  • updateApplicantDetail (5-31)
frontend/src/pages/MainPage/components/SearchBox/SearchBox.tsx (2)
frontend/src/store/useSearchStore.ts (1)
  • useSearchInput (38-45)
frontend/src/store/useCategoryStore.ts (1)
  • useSelectedCategory (28-34)
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx (2)
frontend/src/hooks/queries/application/useGetApplication.ts (1)
  • useGetApplication (4-11)
frontend/src/hooks/queries/applicants/useUpdateApplicant.ts (1)
  • useUpdateApplicant (5-18)
frontend/src/apis/application/updateApplicantDetail.ts (2)
frontend/src/types/applicants.ts (1)
  • UpdateApplicantParams (26-30)
frontend/src/apis/auth/secureFetch.ts (1)
  • secureFetch (3-41)
frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx (1)
frontend/src/pages/ClubDetailPage/components/IntroduceBox/IntroduceBox.tsx (1)
  • li (41-43)
frontend/src/App.tsx (1)
frontend/src/context/AdminClubContext.tsx (1)
  • AdminClubProvider (15-28)
🪛 Biome (2.1.2)
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx

[error] 248-253: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)

🔇 Additional comments (29)
frontend/src/components/application/QuestionTitle/QuestionTitle.styles.ts (1)

39-39: textarea의 가로 폭 100% 적용은 적합합니다

컨테이너 확장 시 텍스트 영역이 레이아웃 그리드를 균일하게 채웁니다. 이후 JS 측 가변 폭 로직을 정리하면 더 일관됩니다.

frontend/src/components/application/QuestionTitle/QuestionTitle.tsx (1)

42-43: ref 전달과 rows={1} 설정은 적절합니다

자동 높이 조절과 잘 맞물립니다. 상단 수정대로 inline width를 정리하면 수평 레이아웃도 일관됩니다.

frontend/src/components/application/questionTypes/Choice.tsx (1)

8-8: 아이콘 도입으로 UI 일관성 향상

관리/빌더 영역의 삭제 컨트롤과 시각적 일관성이 맞춰집니다. 굿.

frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.tsx (1)

11-11: 아이콘 자산 분리 반영 좋습니다

불필요한 텍스트 대신 데코레이티브 아이콘으로 전환, 스타일 가이드와 일치합니다.

frontend/src/components/common/InputField/InputField.styles.ts (2)

26-26: isSuccess prop 추가 LGTM

성공 상태 스타일링을 위한 prop 추가는 명확하고 영향 범위가 제한적입니다.


128-129: 변경 없음

의미 없는 개행 추가는 무시 가능합니다.

frontend/src/components/common/InputField/InputField.tsx (1)

20-21: 성공 상태(isSuccess) prop 도입 및 전파 LGTM

스타일과 일관되게 성공 상태를 전달하고 있어 UI 피드백 확장에 유용합니다.

disabled/readonly와 isSuccess 동시 적용 시 기대 우선순위(중립 vs 성공 표시)를 디자인 측과 확인 부탁드립니다. 현재 구현은 disabled에서도 성공 테두리가 남을 수 있습니다.

Also applies to: 38-39, 76-76

frontend/src/apis/auth/changePassword.ts (1)

9-11: 엔드포인트/메서드 계약 확인 필요

비밀번호 변경이 PUT /auth/user/로 맞는지 백엔드 계약 확인 부탁드립니다. 일반적으로 전용 엔드포인트(예: PATCH /auth/password 혹은 /auth/user/password)를 분리하는 경우가 많습니다. 계약이 다르면 405/400이 발생합니다.

frontend/src/pages/AdminPage/tabs/AccountEditTab/AccountEditTab.tsx (1)

59-106: 전체 흐름은 명확하고 단순합니다

검증→로딩→API 호출→성공/에러 처리의 단계가 분리되어 있고, 상태 초기화도 적절합니다. 제안 사항만 반영되면 충분히 견고합니다.

frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx (3)

18-22: 카테고리 타입은 적절합니다

현재 모델링(카테고리/아이템)은 구조적으로 명확합니다.


99-99: Divider 위치 적절

시맨틱 블록 구분이 명확해졌습니다.


117-117: 하단 Divider도 OK

상/하 구분선 일관성 확보.

frontend/package.json (1)

41-43: Zustand v5: import 경로·React 19 호환 확인 — persist 동작 변경 검증 필요

  • import: persist, createJSONStorage, subscribeWithSelector는 v5에서도 'zustand/middleware'에서 동일하게 가져옴.
  • React: v5는 React 18 이상 요구하므로 React 19와 호환됨.
  • 주의: v5에서 persist는 생성 시 초기 상태를 자동으로 기록하지 않음(동작 변경). 코드가 이 동작에 의존하는지 확인하고, 필요하면 초기화/복원(rehydration) 로직을 조정하세요.
  • react-router-dom v7과의 특별한 충돌은 문서에 없음 — 프로젝트 내 RRD v7 사용 부분은 추가로 테스트/검증 권장.
frontend/src/types/applicants.ts (1)

26-31: applicantId를 undefined로 허용하지 마세요 — ID 필수 여부 확인 필요

frontend/src/types/applicants.ts (lines 26–31): UpdateApplicantParams.applicantId가 string | undefined로 정의돼 있어 업데이트 API에 undefined가 전송될 위험이 있습니다. 사용처가 모두 ID 필수인지 확인하세요.

권장 diff (ID 필수일 경우):

 export interface UpdateApplicantParams {
   memo: string;
   status: ApplicationStatus;
-  applicantId: string | undefined;
+  applicantId: string;
 }

또는 (ID 선택인 경우):

-  applicantId: string | undefined;
+  applicantId?: string;

검증 참고: 제출하신 스크립트 실행에서 rg가 "unrecognized file type: tsx" 오류를 반환해 코드 검색이 실패했습니다. 아래 명령으로 다시 검증하세요:

rg -n -C2 'UpdateApplicantParams'
rg -n -C2 '\bapplicantId\??\b'
frontend/src/store/useSearchStore.ts (1)

14-24: 스토어 구성이 단순·명확합니다.

초기값/액션 구조 적절하고 subscribeWithSelector 채택도 👍

frontend/src/apis/applicants/deleteApplicants.ts (1)

6-12: DELETE 바디 사용 계약 확인

일부 서버는 DELETE 요청 바디를 무시합니다. 백엔드 계약을 재확인해 주세요. 필요 시 쿼리스트링(예: ?ids=...)이나 POST+_method=DELETE로 대체를 검토하세요.

frontend/src/hooks/queries/applicants/useUpdateApplicant.ts (1)

12-12: 검증 결과 — 문제 없음: invalidateQueries는 접두사(부분) 매칭을 사용합니다. useGetApplicants.ts는 queryKey: ['clubApplicants', clubId]로 정의되어 있고, useUpdateApplicant.ts·useDeleteApplicants.ts는 queryClient.invalidateQueries({ queryKey: ['clubApplicants'] })를 호출합니다. TanStack Query의 invalidateQueries는 기본적으로 부분(접두사) 매칭을 하므로 ['clubApplicants']로도 ['clubApplicants', clubId] 쿼리가 무효화됩니다. (tanstack.com)

Likely an incorrect or invalid review comment.

frontend/src/pages/MainPage/MainPage.tsx (1)

22-29: 확인: selectedCategory 기본값이 'all'로 설정되어 있음

frontend/src/store/useCategoryStore.ts에서 selectedCategory가 'all'으로 초기화되어 있음 (line 17). searchCategory가 undefined가 될 위험 없음.

frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx (4)

17-17: 공통 상수로의 의존 전환 좋습니다

상태 소스 단일화로 유지보수성이 올라갑니다.


27-28: DECLINED 상태 색상 매핑 추가 LGTM

새 상태 대응이 누락되지 않았습니다.


38-40: 초기 상태 지정 포맷 변경 OK

기능 영향 없이 가독성만 개선되었습니다.


159-164: 공통 AVAILABLE_STATUSES 사용 좋습니다

레이블 매핑과 일관성이 유지됩니다.

frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (7)

19-21: 선택 상태를 Map으로 관리하는 선택 좋습니다

키 충돌/삭제에 강하고 성능도 충분합니다.


43-67: 바깥 클릭 닫힘 처리 적절

ref 포함 영역 밖 클릭 시 드롭다운 닫힘이 잘 동작합니다.


69-76: 필터 변경 시 선택 상태 초기화 OK

검색/필터 변경으로 잘못된 선택이 남지 않습니다.


77-83: selectAll/isChecked 파생 상태 계산 적절

렌더 최소화를 위해 메모이제이션은 현 구현으로 충분합니다.


86-106: 일괄 삭제 플로우 합리적

확인 창, 성공/실패 피드백, 상태 초기화 흐름이 명확합니다.


108-138: 상태 기반 다중 선택 로직 명확

'전체/필터' 케이스 분기가 이해하기 쉽습니다.


149-168: 일괄 상태 변경 mutate 페이로드 구성 적절

선택 항목만 필터링해 전송하고, 성공 시 UI 상태를 초기화합니다.

@oesnuj oesnuj removed the request for review from SeongHoonC September 11, 2025 16:36
Copy link
Contributor

@lepitaaar lepitaaar left a comment

Choose a reason for hiding this comment

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

수고하셨습니다.

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.

배포 갑시다

@oesnuj oesnuj merged commit cc2e5df into main Sep 12, 2025
4 checks passed
@oesnuj oesnuj changed the title [release] v1.1.1 [release] FE v1.1.1 Sep 15, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📈 release 릴리즈 배포

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

Comments