Skip to content

[release] v1.1.0#676

Merged
lepitaaar merged 85 commits intomainfrom
develop-fe
Aug 19, 2025
Merged

[release] v1.1.0#676
lepitaaar merged 85 commits intomainfrom
develop-fe

Conversation

@seongwon030
Copy link
Member

@seongwon030 seongwon030 commented Aug 18, 2025

#️⃣연관된 이슈

ex) #이슈번호, #이슈번호

📝작업 내용

이번 PR에서 작업한 내용을 간략히 설명해주세요(이미지/동영상 첨부 가능)

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

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

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

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

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

🫡 참고사항

Summary by CodeRabbit

  • 신기능

    • 클럽연합회 소개 페이지 추가 및 헤더 내 이동 링크 추가
    • 클럽 상세에 추천 동아리 섹션(카드 UI) 추가
    • 관리자용 지원자 상세 편집 페이지(메모/상태, 이전/다음, 드롭다운) 재도입
    • 신청자 업데이트 API와 연동 훅 추가
  • 개선

    • 검색 UI 재구성(SearchField 도입, 페이지별 SearchBox 이동/통합)
    • 관리자 지원자 목록: 키워드 필터, 테이블/체크박스 및 상태 라벨 정리
    • 캘린더/시간 선택 UX 및 스타일 개선
    • 분석 이벤트명(EVENT_NAME) 통합 및 추적 호출 일원화
  • 버그 수정

    • 모집 기간 파싱에 UTC 반영
    • 모집 시작/종료 날짜 상호 동기화 및 초기값 보장
  • 작업

    • swiper 의존성 추가 및 빌드의 CSS 처리 범위 확장

lepitaaar and others added 30 commits August 11, 2025 22:02
- 지원자 탭에서 메모가 없을 때 "메모를 입력하지 않았습니다."라는 기본 메시지를 표시하도록 수정
- eventName 제거, 'categoryButton Clicked'로 통일
- category_id/category_name 속성으로 카테고리 정보 전달
- useMixpanelTrack로 공통 속성 자동 포함
- 이벤트명 상수화: CategoryButton Clicked, SearchBox Clicked, Photo Navigation 등
- 여러 컴포넌트에서 하드코딩된 이벤트명을 EVENT_NAME 상수로 치환
- 트래킹 네이밍 일관성 확보 및 오타/변경 비용 감소
- ApplicantsTab.tsx에서 사용하지 않는 useEffect 훅 제거로 코드 간소화
…ry-button-click-logging-MOA-144

[refactor] 카테고리 버튼 클릭 로깅 통일
…onstants-MOA-148

[refactor] 믹스패널 공통 이벤트 상수 도입
- SearchField.styles.ts 도입으로 페이지 전용 스타일 의존 제거
- SearchField가 공통 스타일을 직접 참조하도록 변경
- 검색 입력 UI의 재사용성/일관성 향상
- 지원자의 지원서 정보를 수정하는 updateApplicantDetail 함수 추가
- PUT 요청을 통해 메모와 상태를 업데이트하며, 오류 처리 로직 포함
- ApplicantsTab.tsx에서 상태 매핑 로직을 별도의 유틸리티 함수 mapStatusToGroup으로 분리하여 코드 가독성 향상
- 상태 매핑을 위한 상수 ApplicationStatus를 사용하여 코드의 명확성 및 유지보수성 개선
- 지원자 상세 페이지에서 메모 입력 및 상태 변경 기능을 추가하여 지원자 정보를 업데이트할 수 있도록 구현
- 메모 변경 시 debounce 처리로 API 호출 최적화
- 지원자 상태를 선택할 수 있는 드롭다운 추가 및 상태 매핑 로직 활용
- answers가 비어 있는 경우 대비해 String(item.answers?.[0]?.value ?? '') 사용
- null/undefined도 안전하게 문자열화 후 toLowerCase 적용
- 드물게 answers[0]가 없는 데이터로 인한 크래시 예방
…d-MOA-153

[refactor] 지원자 탭 확장을 위한 공통 검색창 통일
…OA-154

[feature] 지원자를 검색할 수 있다
…ange

[feature] 지원자의 상태와 메모를 변경한다.
DRAFT는 작성중으로 사용중인 지원상태가 아님
[feature] 지원서 폼 내에서 다른 지원서로 이동할 수 있다.
@seongwon030 seongwon030 self-assigned this Aug 18, 2025
@seongwon030 seongwon030 added 💻 FE Frontend 📈 release 릴리즈 배포 labels Aug 18, 2025
@vercel
Copy link

vercel bot commented Aug 18, 2025

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

Project Deployment Preview Comments Updated (UTC)
moadong Ready Ready Preview Comment Aug 18, 2025 3:42pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 18, 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

지원자 관리(상세 편집·목록 필터), 추천 동아리 섹션 및 카드, 총동아리연합회 페이지, 검색 컴포넌트 교체, 캘린더(시간 포함) 개선, 이벤트명 상수화, 헤더 리팩터, 빌드(deps/webpack) 변경, 타입·유틸 추가가 포함된 프론트엔드 변경입니다.

Changes

Cohort / File(s) Change Summary
Build & Deps
frontend/config/webpack.dev.ts, frontend/package.json
swiper CSS를 webpack include에 추가하고 swiper@^11.2.10 의존성 추가.
Routing & App
frontend/src/App.tsx, frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx
/club-union 라우트·임포트 추가, admin 사이드바 탭 라벨 포맷 변경 및 일부 import 경로 수정.
Applicants — API / Hook / Types / Util
frontend/src/apis/application/updateApplicantDetail.ts, frontend/src/hooks/queries/applicants/useUpdateApplicant.ts, frontend/src/types/applicants.ts, frontend/src/utils/mapStatusToGroup.ts
지원자 상세 업데이트 API 추가, React Query 뮤테이션 훅 추가, ApplicationStatus 열거형 축소, 상태→라벨 매핑 유틸 추가.
Applicants — UI (Detail) 재배치
.../ApplicantsTab/ApplicantDetailPage.tsx (삭제), frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx, .../ApplicantDetailPage/ApplicantDetailPage.styles.ts
상세 페이지 파일 이동/재구현: 메모·상태 편집(디바운스 업데이트), 이전/다음·드롭다운 이동, 로딩/오류 처리, 스타일 컴포넌트 추가.
Applicants — UI (List)
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx, .../ApplicantsTab/ApplicantsTab.styles.ts
키워드 라이브 필터링 추가, 체크박스 커스텀, 헤더/열 속성 확대(폭·borderLeft·isMemo), 상태 라벨 변경 및 행 클릭 네비게이션.
Header & Navigation
frontend/src/components/common/Header/Header.tsx, frontend/src/services/header/useHeaderService.ts, frontend/src/services/header/useMobileMenu.ts
헤더를 데이터 중심으로 리팩터(메모화, 공통 navLinks), 핸들러 useCallback화, 총동연 링크 추가 및 모바일 메뉴 이벤트명 상수화.
Analytics 이벤트 상수화
frontend/src/constants/eventName.ts, 다수 파일(.../usePhotoNavigation.ts, ClubDetailPage 컴포넌트들, CategoryButtonList, StatusRadioButton 등)
Mixpanel 이벤트명들을 EVENT_NAME 상수로 중앙집중화 및 기존 문자열 호출 교체.
검색 컴포넌트 교체
frontend/src/components/common/SearchBox/SearchBox.tsx (삭제), frontend/src/components/common/SearchField/SearchField.tsx, frontend/src/pages/MainPage/components/SearchBox/SearchBox.tsx
공용 SearchBox 제거, 재사용 가능한 SearchField 컴포넌트 추가, 메인용 SearchBox 재배치 및 검색 흐름(컨텍스트/네비게이션/트래킹) 구현.
Club Detail — 추천 동아리
frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx, frontend/src/pages/ClubDetailPage/components/RecommendedClubs/*, frontend/src/pages/ClubDetailPage/components/ClubCard/*, frontend/src/types/club.ts
추천 동아리 섹션·스타일·카드 컴포넌트 추가(최대 6개), 카드 클릭 트래킹 및 네비게이션, ClubDetailrecommendClubs? 필드 추가.
Club Union Page
frontend/src/pages/ClubUnionPage/ClubUnionPage.tsx, .../ClubUnionPage.styles.ts, frontend/src/constants/CLUB_UNION_INFO.ts
총동아리연합회 소개 페이지 컴포넌트·스타일·멤버 상수 추가.
Recruit Calendar & Date Parsing
frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx, .../Calendar/Calendar.tsx, .../Calendar/Calendar.styles.ts, frontend/src/utils/recruitmentPeriodParser.ts
시간 선택 포함된 DatePicker로 변경, 시작/종료 상호 보정 로직 추가, 스타일 토큰화, 파싱을 UTC로 처리(오프셋 포함), 초기값 보정.
기타 UI·접근성·스타일
여러 컴포넌트(예: QuestionTitle 변경, ClubCard 스타일 추가, PhotoList hook 등)
contentEditable 이벤트 핸들러 변경(onInput→onChange), 추천 카드/스타일 추가, 일부 트래킹 호출 상수화 등 다양한 UI 조정.

Sequence Diagram(s)

sequenceDiagram
  participant UI as ApplicantDetailPage
  participant Hook as useUpdateApplicant
  participant API as updateApplicantDetail
  participant Server as Backend

  UI->>Hook: mutate({ memo, status }) (debounced)
  Hook->>API: updateApplicantDetail(memo, status, clubId, applicantId)
  API->>Server: PUT /api/club/{clubId}/apply/{applicantId} {memo,status}
  Server-->>API: 200 OK { data }
  API-->>Hook: result.data
  Hook->>Hook: invalidateQueries(["clubApplicants"])
  Hook-->>UI: onSuccess
Loading
sequenceDiagram
  participant User
  participant Header as Header/useHeaderService
  participant Router as react-router
  participant Mix as Mixpanel

  User->>Header: Click Home/Introduce/ClubUnion/Menu
  Header->>Mix: track(EVENT_NAME.*)
  Header->>Router: navigate(/, /introduce, /club-union)
Loading
sequenceDiagram
  participant User
  participant Card as ClubCard
  participant Mix as Mixpanel
  participant Router as react-router

  User->>Card: Click recommended club
  Card->>Mix: track(EVENT_NAME.RECOMMENDED_CLUB_CLICKED, {club_id,...})
  Card->>Router: navigate(/club/{id})
Loading
sequenceDiagram
  participant User
  participant Search as Main SearchBox + SearchField
  participant Ctx as Contexts(Search/Category)
  participant Router as react-router
  participant Mix as Mixpanel

  User->>Search: Submit
  Search->>Router: navigate('/')
  Search->>Ctx: setKeyword(input), setSelectedCategory('all'), setIsSearching(true)
  Search->>Mix: track(EVENT_NAME.SEARCH_BOX_CLICKED or custom "Search Executed")
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • PororoAndFriends
  • oesnuj

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ 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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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
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: 11

🔭 Outside diff range comments (2)
frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx (1)

43-56: httpOnly 쿠키 환경에서 logout 미호출 위험: document.cookie 체크는 신뢰할 수 없습니다

document.cookie로 refreshToken 존재 여부를 판별하면, httpOnly로 설정된 경우 항상 읽지 못해 logout()이 호출되지 않을 수 있습니다. 이 경우 서버 측 리프레시 토큰/세션이 무효화되지 않아 “실제로는 로그아웃되지 않은 상태”가 남을 수 있습니다. logout은 무조건 시도하고, 실패해도 클라이언트 정리와 리디렉트를 보장하는 편이 안전합니다.

권장 수정안:

  • refreshToken 쿠키 존재 여부 조건을 제거하고 logout()을 항상 호출
  • 실패하더라도 finally에서 accessToken 정리 및 로그인 페이지로 이동

적용 예시:

-    try {
-      if (
-        document.cookie
-          .split(';')
-          .some((cookie) => cookie.trim().startsWith('refreshToken='))
-      ) {
-        await logout();
-      }
-      localStorage.removeItem('accessToken');
-      navigate('/admin/login', { replace: true });
-    } catch (error) {
-      alert('로그아웃에 실패했습니다.');
-    }
+    try {
+      // 서버가 보유한 쿠키/세션 무효화를 항상 시도합니다.
+      await logout();
+    } catch (error) {
+      // 실패하더라도 클라이언트 상태는 정리합니다.
+      console.error('logout() failed:', error);
+    } finally {
+      localStorage.removeItem('accessToken');
+      navigate('/admin/login', { replace: true });
+    }
frontend/src/pages/MainPage/components/StatusRadioButton/StatusRadioButton.tsx (1)

14-25: setState(updater) 내부 부수효과로 인한 중복 트래킹/콜백 호출 가능성

setIsActive((prev) => { ... onChange(); trackEvent(); return newStatus; }) 패턴은 React StrictMode에서 updater가 두 번 호출되어 트래킹·콜백이 중복 실행될 수 있습니다. 부수효과는 updater 밖으로 이동하세요.

-  const handleToggle = () => {
-    setIsActive((prev) => {
-      const newStatus = !prev;
-      onChange(newStatus);
-
-      trackEvent(EVENT_NAME.STATUS_RADIO_BUTTON_CLICKED, {
-        new_status: newStatus ? 'OPEN' : 'ALL',
-      });
-
-      return newStatus;
-    });
-  };
+  const handleToggle = () => {
+    const newStatus = !isActive;
+    setIsActive(newStatus);
+    onChange(newStatus);
+    trackEvent(EVENT_NAME.STATUS_RADIO_BUTTON_CLICKED, {
+      new_status: newStatus ? 'OPEN' : 'ALL',
+    });
+  };
🧹 Nitpick comments (64)
frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx (1)

68-75: 리스트 key는 label 대신 path를 사용해 라벨 변경 시 재마운트 방지

현재 key={tab.label}이라 라벨 카피 변경 시 React가 다른 요소로 인식하여 재마운트가 발생할 수 있습니다. path는 안정적인 식별자라 key로 더 적합합니다.

-            key={tab.label}
+            key={tab.path}
frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.styles.ts (5)

26-28: 컴포넌트명 오타(Tidle) → 의미 있는 이름으로 변경 제안

Tidle는 오타로 보이며 의미도 모호합니다. RangeSeparator 등 역할이 드러나는 이름이 가독성에 좋습니다.

-export const Tidle = styled.p`
+export const RangeSeparator = styled.p`
   font-size: 1.375rem;
 `;

Calendar.tsx 사용처도 함께 변경해 주세요(선택 영역 밖 참고 코드):

// Calendar.tsx
// ...
<Styled.RangeSeparator>~</Styled.RangeSeparator>
// ...

4-9: 반복 색상 토큰 보강: hover 배경 토큰화 권장

동일 색상 rgba(255, 84, 20, 0.12)가 여러 곳에서 반복됩니다. 토큰으로 승격해 유지보수성을 높이세요.

 const primary = 'rgba(255, 84, 20, 0.8)';
 const primaryHover = 'rgba(255, 84, 20, 0.95)';
 const white = '#fff';
 const gray = 'rgba(0,0,0,0.5)';
 const inputBg = 'rgba(0,0,0,0.05)';
+const primarySoft = 'rgba(255, 84, 20, 0.12)'; // hover 등 연한 배경

147-151: 반복된 하드코딩 색상 사용 제거(primarySoft 사용)

위에서 제안한 primarySoft를 적용하면 중복과 하드코딩을 줄일 수 있습니다.

 .react-datepicker__day:hover {
-  background: rgba(255, 84, 20, 0.12);
+  background: ${primarySoft};
 }
 .react-datepicker__time-list-item {
   ${cellBase};
   /* ... */
   &:hover {
-    background: rgba(255, 84, 20, 0.12);
+    background: ${primarySoft};
   }

Also applies to: 207-210


50-54: 매직 넘버(120px) 상수화: 의미 있는 이름으로 치환

시간 패널 폭은 재사용/조정 가능성이 높으므로 상수로 분리하세요.

추가(상단 토큰 인접):

+const TIME_PANEL_WIDTH = '120px';

치환:

 .react-datepicker__time-container {
-  flex: 0 0 120px;
+  flex: 0 0 ${TIME_PANEL_WIDTH};
   border-left: 1px solid rgba(0, 0, 0, 0.08);
   background: ${white};
 }

21-24: transition duration 값 통일 제안(0.08s vs 0.1s 혼재)

미세하게 다른 duration이 혼재되어 있어 인터랙션 감각이 일관되지 않을 수 있습니다. 상수/토큰으로 통일을 권장합니다(예: TRANSITION_FAST = '0.1s ease').

Also applies to: 134-136

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

41-48: 서버 전송 시 undefined 대신 null로 명시 전송 권장

toISOString() 앞의 옵셔널 체이닝으로 인해 값이 없으면 undefined가 됩니다. 많은 API/백엔드는 명시적 null을 선호하거나 undefined를 무시합니다. 의도한 동작(필드 초기화/유지)에 따라 명확히 보내세요.

 const updatedData = {
   id: clubDetail.id,
-  recruitmentStart: recruitmentStart?.toISOString(),
-  recruitmentEnd: recruitmentEnd?.toISOString(),
+  recruitmentStart: recruitmentStart ? recruitmentStart.toISOString() : null,
+  recruitmentEnd: recruitmentEnd ? recruitmentEnd.toISOString() : null,
   recruitmentTarget: recruitmentTarget,
   description: description,
   externalApplicationUrl: clubDetail.externalApplicationUrl ?? '',
 };
frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.tsx (2)

73-84: 매직 넘버/문자열 상수화 및 공통 핸들러 추출

  • timeIntervals={30}가 두 곳에서 반복됩니다.
  • dateFormat 문자열도 중복.
  • onChangeRaw={(e: any) => e.preventDefault()}를 공통 핸들러로 추출하고 타입을 명시하세요.

변경 예시:

추가(파일 상단, 선택 영역 밖):

const TIME_INTERVAL_MINUTES = 30;
const DATE_TIME_FORMAT = 'yyyy년 MM월 dd일 (eee) HH:mm';
const preventRawInput = (e: React.SyntheticEvent<HTMLInputElement>) => e.preventDefault();

치환:

 <DatePicker
   locale={ko}
-  selected={recruitmentStart}
+  selected={recruitmentStart}
   onChange={handleStartChange}
-  showTimeSelect
-  timeIntervals={30}
+  showTimeSelect
+  timeIntervals={TIME_INTERVAL_MINUTES}
   timeCaption='시간'
-  dateFormat='yyyy년 MM월 dd일 (eee) HH:mm'
+  dateFormat={DATE_TIME_FORMAT}
   shouldCloseOnSelect={false}
   popperPlacement='bottom-start'
   renderCustomHeader={(props) => <CustomHeader {...props} />}
-  onChangeRaw={(e: any) => e.preventDefault()}
+  onChangeRaw={preventRawInput}
 />
 <DatePicker
   locale={ko}
-  selected={recruitmentEnd}
+  selected={recruitmentEnd}
   onChange={handleEndChange}
-  showTimeSelect
-  timeIntervals={30}
+  showTimeSelect
+  timeIntervals={TIME_INTERVAL_MINUTES}
   timeCaption='시간'
-  dateFormat='yyyy년 MM월 dd일 (eee) HH:mm'
+  dateFormat={DATE_TIME_FORMAT}
   shouldCloseOnSelect={false}
   popperPlacement='bottom-start'
   renderCustomHeader={(props) => <CustomHeader {...props} />}
-  onChangeRaw={(e: any) => e.preventDefault()}
+  onChangeRaw={preventRawInput}
 />

Also applies to: 87-98


3-3: 중복 CSS 임포트 정리 고려

react-datepicker/dist/react-datepicker.css는 styles 파일에서도 임포트되고 있습니다. 한 곳으로 일원화해 중복 주입을 피하는 것이 깔끔합니다.

frontend/src/types/club.ts (2)

27-28: 세미콜론 누락 — 팀 컨벤션에 맞춰 일관성 유지 필요

동일 인터페이스 내 다른 필드들은 세미콜론으로 끝나는데, 본 줄만 누락돼 있습니다. Lint 규칙에 따라 경고가 발생할 수 있습니다.

-  recommendClubs?: Club[]
+  recommendClubs?: Club[];

27-27: 필드 명명 검토: recommendClubs → recommendedClubs(문법적) 여부

이미 사용처(ClubDetailPage 등)와 연동되어 있어 지금 릴리스에서는 그대로 가는 게 안전합니다. 다만 도메인 용어 통일을 위해 후속 PR에서 리팩터(혹은 API 응답 필드와의 정합성 확인)를 고려해 주세요.

frontend/src/constants/CLUB_UNION_INFO.ts (3)

30-32: 주석의 “글자 수 제한”을 상수로 노출해 재사용성/일관성 확보

UI 컴포넌트에서 동일 기준을 사용하려면 상수화를 권장합니다. 팀 가이드와도 부합합니다(매직 넘버 제거).

-// 개발자 가이드: description 필드는 UI가 깨지지 않도록 글자 수를 제한합니다.
-// (권장) 모바일: 50자 이내, 데스크톱: 100자 이내
+// 개발자 가이드: description 필드는 UI가 깨지지 않도록 글자 수를 제한합니다.
+// (권장) 모바일: 50자 이내, 데스크톱: 100자 이내
+export const CLUB_UNION_DESC_MAX_MOBILE = 50;
+export const CLUB_UNION_DESC_MAX_DESKTOP = 100;

32-122: 배열 불변성/타입 안전성 강화: satisfies + ReadonlyArray 사용 제안

런타임 변이를 방지하고 선언-사용 간 타입 검사를 강화하려면 satisfies를 활용해 읽기 전용 배열로 선언하는 방식을 권장합니다(이미 TS 5.x 사용).

-export const CLUB_UNION_MEMBERS: ClubUnionMember[] = [
+export const CLUB_UNION_MEMBERS = [
   {
     id: 1,
     name: '이정은',
     role: '회장',
     description: '부경대학교의 중앙동아리, 온 총동아리연합회가 책임지겠습니다.',
     imageSrc: MEMBER_AVATARS.PRESIDENT,
   },
   ...
-];
+] satisfies ReadonlyArray<ClubUnionMember>;

33-121: 정적 개인 정보(이름) 노출 — 공개 범위 확인

이름/직책/메시지가 정적으로 번들링됩니다. 공개 동의 및 최신성 관리(변경 시 배포 필요) 측면에서 의도대로인지 확인 부탁드립니다. 가능하면 CMS/백오피스 연동을 roadmap에 올려 데이터 갱신 비용을 줄이는 것도 고려해 주세요.

frontend/src/pages/ClubDetailPage/components/ClubCard/ClubCard.styles.ts (3)

9-16: 중복된 border-radius 선언 제거

9, 14행에서 border-radius가 중복 선언되어 후자가 전자를 덮어씁니다. 중복 제거로 가독성 향상 및 오해 방지.

   display: flex;
   flex-direction: column;
-  border-radius: 14px;
   padding: 20px;
   background-color: #fff;
   width: 100%;
   height: 170px;
   border-radius: 18px;
   border: 1px solid #dcdcdc;

32-34: 매직 넘버(300px) 상수화 — 가이드라인 준수

브레이크포인트는 의미 있는 이름의 상수로 분리해 주세요.

-import styled from 'styled-components';
+import styled from 'styled-components';
+
+const VERY_SMALL_MAX_WIDTH = 300; // px

 ...
-  @media (max-width: 300px) {
+  @media (max-width: ${VERY_SMALL_MAX_WIDTH}px) {
     height: auto;
   }

3-6: 사용되지 않는 스타일 프롭 ($state) 정리 또는 활용 권장

$state가 스타일에 반영되지 않습니다. 불필요하면 제거(타입/호출부 포함)하거나, 상태별 스타일(예: 모집 상태에 따른 border 색상)로 활용해 주세요. DOM 전파는 transient prop라 문제 없지만, 유지보수 관점에서 의도를 명확히 하는 게 좋습니다.

frontend/src/pages/ClubUnionPage/ClubUnionPage.styles.ts (4)

5-16: 매직 넘버/색상을 디자인 토큰(상수)로 치환 권장

여러 곳에서 픽셀/색상 리터럴이 반복됩니다. 팀 컨벤션(테마/디자인 토큰)이 있다면 상수로 치환해 가독성과 일관성을 높이면 좋겠습니다. 아래는 패턴 예시입니다.

아래 상수를 파일 상단(import 아래)에 추가:

const COLORS = {
  textStrong: '#222',
  textMuted: '#555',
  overlayBg: 'rgba(0, 0, 0, 0.65)',
  badgeBg: 'rgba(255, 255, 255, 0.8)',
  accent: '#ff5414',
} as const;

const SPACING = {
  titleMt: '100px',
  titleMb: '40px',
  titleMtMobile: '80px',
  titleMbMobile: '30px',
} as const;

해당 구간 적용 예:

-  margin-top: 100px;
-  margin-bottom: 40px;
+  margin-top: ${SPACING.titleMt};
+  margin-bottom: ${SPACING.titleMb};
-  color: #222;
+  color: ${COLORS.textStrong};

   ${media.mobile} {
-    margin-top: 80px;
-    margin-bottom: 30px;
+    margin-top: ${SPACING.titleMtMobile};
+    margin-bottom: ${SPACING.titleMbMobile};
   }

49-67: 오버레이 쌓임 순서 안정화: z-index 명시

브라우저/컨텍스트에 따라 stacking 이슈를 방지하기 위해 z-index를 명시해두는 것이 안전합니다.

 export const InfoOverlay = styled.div`
   position: absolute;
   top: 0;
   left: 0;
   width: 100%;
   height: 100%;
   background-color: rgba(0, 0, 0, 0.65);
   color: white;
   display: flex;
   flex-direction: column;
   align-items: center;
   justify-content: center;
   text-align: center;
   padding: 15px;
   opacity: 0;
   transition: opacity 0.3s ease;
   border-radius: 50%;
+  z-index: 1;
   box-sizing: border-box;
 `

69-76: 이미지 하단 간격 제거를 위한 display: block 권장

인라인 이미지는 baseline 간격이 생길 수 있습니다. display: block을 추가하면 불필요한 하단 갭을 예방할 수 있습니다.

 export const ProfileImage = styled.img`
   width: 100%;
   height: 100%;
+  display: block;
   object-fit: cover;
   transition:
     transform 0.3s ease,
     filter 0.3s ease;
 `

93-101: 클릭 동작이 없다면 cursor: pointer 제거 권장

현재 카드에 onClick 동작이 없는데 pointer 커서가 노출됩니다. 사용자 혼동을 줄이기 위해 제거하거나(비클릭) 클릭 동작/역할을 부여(클릭)하는 방향으로 통일해주세요.

 export const ProfileCardContainer = styled.div`
   position: relative;
   aspect-ratio: 1 / 1;
   border-radius: 50%;
   overflow: hidden;
-  cursor: pointer;
   box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);

클릭 가능 요소로 유지하려면 Role/키보드 접근성(버튼 역할)도 함께 고려가 필요합니다. 원하시면 해당 방향으로도 패치 제안드리겠습니다.

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

20-23: 프로필 이미지에 지연 로딩/디코딩 추가로 LCP 개선

이미지 그리드에 lazy/async를 추가해 페인트/네트워크 효율을 높일 수 있습니다.

               <Styled.ProfileImage
                 src={member.imageSrc}
                 alt={`${member.name} 프로필`}
+                loading='lazy'
+                decoding='async'
               />
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (4)

49-49: 트래킹에 컨텍스트(클럽 정보) 함께 포함 권장

분석 가치를 높이기 위해 clubId/clubName을 속성으로 함께 보내는 것을 권장합니다.

-    trackEvent(EVENT_NAME.CLUB_APPLY_BUTTON_CLICKED);
+    trackEvent(EVENT_NAME.CLUB_APPLY_BUTTON_CLICKED, {
+      clubId,
+      clubName: clubDetail.name,
+    });

32-36: 브레이크포인트 상수 사용으로 일관성 확보

이 파일에서는 500px 매직 넘버를 쓰고 있는데, 프로젝트에는 mediaQuery 유틸이 존재합니다. 동일한 기준을 사용하도록 교체해주세요.

-  @media (max-width: 500px) {
+  ${media.mobile} {
     width: 256px;
     height: 44px;
     font-size: 1rem;
   }

아래 import를 상단에 추가:

import { media } from '@/styles/mediaQuery';

44-44: non-null 단언(clubId!) 사용 재검토

라우트 파라미터가 항상 존재한다면 문제 없으나, 타입 안정성과 초기 렌더 안전성을 위해 hook에 enabled 옵션(React Query 패턴) 또는 가드 로직을 도입하는 것이 바람직합니다. useGetClubDetail 시그니처를 확인해 enabled를 지원하는지 검토 부탁드립니다.

원하시면 enabled 패턴을 사용하는 대체 구현도 제안드리겠습니다.


10-36: 버튼 스타일의 매직 넘버/색상 토큰화 고려

width/height/font-size/background-color 등 하드코딩 값이 반복됩니다. 테마/디자인 토큰으로 추출하면 유지보수성과 일관성이 개선됩니다.

원하시면 프로젝트 테마 구조를 기준으로 맞춘 예시 패치도 제공할게요.

frontend/src/pages/ClubDetailPage/components/BackNavigationBar/BackNavigationBar.tsx (1)

19-19: 접근성: 이미지 클릭 요소에 대체 텍스트/키보드 접근 추가

현재 img에 onClick만 있어 키보드 접근이 어렵고 alt도 없습니다. role/tabIndex/onKeyDown을 추가해 접근성을 개선하세요.

-        <Styled.BackImage src={BackIcon} onClick={handleBackClick} />
+        <Styled.BackImage
+          src={BackIcon}
+          alt='뒤로가기'
+          role='button'
+          tabIndex={0}
+          onClick={handleBackClick}
+          onKeyDown={(e) => {
+            if (e.key === 'Enter' || e.key === ' ') handleBackClick();
+          }}
+        />

가능하다면 실제 button 요소 사용을 검토하면 더 자연스러운 접근성을 보장할 수 있습니다.

frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx (1)

51-55: 접근성: 커스텀 버튼에 키보드 접근 추가

role='button'만으로는 키보드 활성화가 보장되지 않습니다. tabIndex 및 Enter/Space 핸들링을 추가해주세요.

   return (
     <Styled.ShareButtonContainer
       onClick={handleShare}
       role='button'
       aria-label='카카오톡으로 동아리 정보 공유하기'
+      tabIndex={0}
+      onKeyDown={(e) => {
+        if (e.key === 'Enter' || e.key === ' ') handleShare();
+      }}
     >
frontend/src/services/header/useMobileMenu.ts (1)

18-21: useCallback 의존성 배열에 trackEvent 포함 권장

현재 closeMenu는 빈 의존성 배열([])을 사용하면서 내부에서 trackEvent를 참조합니다. hook 구현에 따라 안정적일 수 있지만, eslint-plugin-react의 exhaustive-deps 규칙과 stale closure 회피 차원에서 trackEvent를 의존성에 넣는 것이 안전합니다.

적용 예시:

-  const closeMenu = useCallback(() => {
-    setIsMenuOpen(false);
-    trackEvent(EVENT_NAME.MOBILE_MENU_DELETE_BUTTON_CLICKED);
-  }, []);
+  const closeMenu = useCallback(() => {
+    setIsMenuOpen(false);
+    trackEvent(EVENT_NAME.MOBILE_MENU_DELETE_BUTTON_CLICKED);
+  }, [trackEvent]);
frontend/src/pages/ClubDetailPage/components/InfoTabs/InfoTabs.tsx (2)

6-10: 매직 넘버(0) 상수화 제안

가이드라인(프론트 매직 넘버 치환)에 따라 기본 활성 탭 인덱스 0을 의미 있는 상수로 치환하면 가독성이 좋아집니다.

 const tabLabels = ['모집정보', '동아리정보', '소개글', '활동사진'];

+const DEFAULT_ACTIVE_TAB = 0;
+
 const InfoTabs = ({ onTabClick }: { onTabClick: (index: number) => void }) => {
-  const [activeTab, setActiveTab] = useState(0);
+  const [activeTab, setActiveTab] = useState(DEFAULT_ACTIVE_TAB);

23-31: 탭 접근성(ARIA) 속성 추가 권장

역할/상태를 명시하면 보조기기 사용성이 개선됩니다. 최소한 role="tablist"와 각 버튼의 role="tab", aria-selected를 추가해 주세요.

-    <Styled.InfoTabWrapper>
+    <Styled.InfoTabWrapper role="tablist" aria-label="동아리 상세 정보 탭">
       {tabLabels.map((label, index) => (
         <Styled.InfoTabButton
           key={label}
           className={activeTab === index ? 'active' : ''}
-          onClick={() => handleTabClick(index)}
+          role="tab"
+          aria-selected={activeTab === index}
+          type="button"
+          onClick={() => handleTabClick(index)}
         >
           {label}
         </Styled.InfoTabButton>
       ))}
frontend/src/pages/ClubDetailPage/components/RecommendedClubs/RecommendedClubs.tsx (2)

11-12: 매직 넘버(6) 상수화 권장

프론트 가이드라인에 따라 표시 개수 6을 명명된 상수로 치환하면 의도가 명확합니다. 관련 로직 근처에 정의해 주세요.

-  const displayClubs = clubs.slice(0, 6);
+  const MAX_RECOMMENDED_CLUBS = 6;
+  const displayClubs = clubs.slice(0, MAX_RECOMMENDED_CLUBS);

4-4: 불필요한 swiper/css import 제거 권장

  • 파일: frontend/src/pages/ClubDetailPage/components/RecommendedClubs/RecommendedClubs.tsx
    4행의 import 'swiper/css';는 이 컴포넌트 내에서 Swiper를 사용하지 않으므로 제거해 주세요.
- import 'swiper/css';
frontend/src/apis/application/updateApplicantDetail.ts (1)

26-31: 서버 에러 메시지 보존을 위한 에러 처리 개선 제안

!response.ok에서 고정 문구만 던지면 서버가 준 상세 메시지를 잃습니다. 가능하면 JSON을 파싱해 메시지를 보존하세요.

-    if (!response.ok) {
-      throw new Error('지원자의 지원서 정보 수정에 실패했습니다.');
-    }
+    if (!response.ok) {
+      let message = '지원자의 지원서 정보 수정에 실패했습니다.';
+      try {
+        const errJson = await response.json();
+        message = errJson?.message ?? message;
+      } catch {
+        // 바디가 없거나 JSON 파싱 실패 시 기본 메시지 유지
+      }
+      throw new Error(message);
+    }
frontend/src/hooks/queries/applicants/useUpdateApplicant.ts (3)

12-13: 쿼리 무효화 키 범위 축소를 고려해 주세요

현재 invalidateQueries({ queryKey: ["clubApplicants"] })는 동일 prefix를 가진 모든 쿼리를 무효화합니다. 의도적으로 전체 목록을 리프레시하려는 목적이면 괜찮지만, 클럽별로 키를 분리해 운용 중이라면 과도한 무효화를 유발할 수 있습니다. clubId를 포함해 스코프를 좁히는 방식을 추천합니다.

아래처럼 변경을 제안드립니다(실제 조회 키 체계에 맞게 조정 필요):

-        queryClient.invalidateQueries({ queryKey: ["clubApplicants"] });
+        queryClient.invalidateQueries({ queryKey: ["clubApplicants", clubId] });

8-11: 제네릭과 payload 타입 도입으로 타입 안정성 강화

useMutation 제네릭과 payload 타입을 명시하면 DX/안정성이 좋아집니다. 반환 타입은 updateApplicantDetail의 반환을 활용할 수 있습니다.

아래와 같이 수정을 제안드립니다:

+ type UpdateApplicantPayload = { memo: string; status: ApplicationStatus };
+
   return useMutation({
-    mutationFn: ({memo, status}: { memo: string, status: ApplicationStatus }) => 
+    mutationFn: ({ memo, status }: UpdateApplicantPayload) =>
       updateApplicantDetail(memo, status, clubId, applicantId),

파일 범위 외 추가 변경(선택):

// 제네릭 추가 예시
return useMutation<Awaited<ReturnType<typeof updateApplicantDetail>>, Error, UpdateApplicantPayload>({
  // ...
});

14-16: 에러 로깅은 console.error로, 에러 객체는 분리 전달

문자열 보간으로 에러를 문자열화하기보다 console.error('...', error) 형태가 분석/수집에 유리합니다.

-      console.log(`Error updating applicant detail: ${error}`);
+      console.error('Error updating applicant detail:', error);
frontend/src/utils/mapStatusToGroup.ts (1)

3-16: switch 대신 Record 매핑으로 ‘완전성(Exhaustiveness)’ 확보

향후 ApplicationStatus가 추가될 경우 누락을 컴파일 타임에 잡기 위해 Record<ApplicationStatus, ...> 기반 매핑을 추천합니다. 기본값 분기를 제거/축소할 수 있고 가독성도 좋아집니다.

-import { ApplicationStatus } from "@/types/applicants";
-
-const mapStatusToGroup = (status: ApplicationStatus): { status: ApplicationStatus, label: string } => {
-  switch (status) {
-    case ApplicationStatus.SUBMITTED:
-      return { status: ApplicationStatus.SUBMITTED, label: '서류검토' };
-    case ApplicationStatus.INTERVIEW_SCHEDULED:
-      return { status: ApplicationStatus.INTERVIEW_SCHEDULED, label: '면접예정' };
-    case ApplicationStatus.ACCEPTED:
-      return { status: ApplicationStatus.ACCEPTED, label: '합격' };
-    case ApplicationStatus.DECLINED:
-      return { status: ApplicationStatus.DECLINED, label: '불합' };
-    default:
-      return { status: ApplicationStatus.SUBMITTED, label: '서류검토'};
-  }
-}
+import { ApplicationStatus } from "@/types/applicants";
+
+type StatusGroup = { status: ApplicationStatus; label: string };
+
+const STATUS_LABELS: Record<ApplicationStatus, StatusGroup> = {
+  [ApplicationStatus.SUBMITTED]: { status: ApplicationStatus.SUBMITTED, label: '서류검토' },
+  [ApplicationStatus.INTERVIEW_SCHEDULED]: { status: ApplicationStatus.INTERVIEW_SCHEDULED, label: '면접예정' },
+  [ApplicationStatus.ACCEPTED]: { status: ApplicationStatus.ACCEPTED, label: '합격' },
+  [ApplicationStatus.DECLINED]: { status: ApplicationStatus.DECLINED, label: '불합' },
+};
+
+const mapStatusToGroup = (status: ApplicationStatus): StatusGroup => {
+  // 런타임 안전망(이상치가 들어와도 기본값으로 매핑)
+  return STATUS_LABELS[status] ?? STATUS_LABELS[ApplicationStatus.SUBMITTED];
+};
frontend/src/pages/MainPage/components/SearchBox/SearchBox.tsx (2)

24-25: 카테고리 초기값 'all' 하드코딩 개선 제안

카테고리 초기값이 여러 곳에서 사용된다면 상수(DEFAULT_CATEGORY 등)로 추출해 관리하는 편이 안전하고 검색/변경이 용이합니다.


27-30: 객체 프로퍼티 단축 표기 사용

불필요한 중복을 줄여 가독성을 조금 개선할 수 있습니다.

-    trackEvent('Search Executed', {
-      inputValue: inputValue,
-      page: currentPage,
-    });
+    trackEvent('Search Executed', {
+      inputValue,
+      page: currentPage,
+    });
frontend/src/pages/ClubDetailPage/components/RecommendedClubs/RecommendedClubs.styles.tsx (1)

3-12: 매직 넘버/색상은 테마 토큰으로 추출 권장

여러 컴포넌트에서 재사용될 수 있는 spacing(30px/34px/16px), border(1px), radius(18px), color(#dcdcdc), breakpoint(600px)는 theme 혹은 상수로 관리하면 유지보수/전역 스타일 일관성에 유리합니다.

예시:

  • theme.space.xl, theme.radius.lg, theme.colors.border, theme.breakpoints.sm 등
frontend/src/components/common/SearchField/SearchField.tsx (3)

48-49: 아이콘은 장식 요소로 처리해 중복 낭독 방지

버튼에 aria-label='검색'이 이미 있으므로 내부 아이콘은 장식 목적으로 스크린리더에서 숨기는 것이 접근성에 더 적합합니다.

-        <img src={searchButtonIcon} alt='Search Button' />
+        <img src={searchButtonIcon} alt='' aria-hidden='true' />

25-29: 기능 설계 확인: 빈 문자열 검색 허용 여부

현재는 빈 문자열도 그대로 setKeyword되어 검색이 수행됩니다. “전체 보기”를 의도했다면 OK, 아니라면 inputValue.trim().length === 0일 때 early-return 또는 가이드 표시를 고려해 주세요.


31-33: Styled.SearchBoxContainer가 styled.form으로 정의되어 있어 onSubmit이 정상 작동합니다.

  • frontend/src/components/common/SearchField/SearchField.styles.ts: SearchBoxContainerstyled.form<{ $isFocused: boolean }>으로 선언되어 있습니다.
  • (선택) frontend/src/components/common/SearchField/SearchField.tsx: 컨테이너에 role="search"를 추가하여 접근성을 개선할 수 있습니다.

권장 변경 예시:

-  <Styled.SearchBoxContainer $isFocused={isFocused} onSubmit={handleSubmit}>
+  <Styled.SearchBoxContainer $isFocused={isFocused} onSubmit={handleSubmit} role="search">
frontend/src/pages/ClubDetailPage/components/ClubCard/ClubCard.tsx (1)

27-31: 클릭 지연 150ms 매직 넘버 상수화

가독성과 추후 조정 용이성을 위해 150ms를 명명된 상수로 추출해 주세요.

다음 변경을 권장합니다:

@@
-    setTimeout(() => {
+    setTimeout(() => {
       setIsClicked(false);
       navigate(`/club/${club.id}`);
-    }, 150);
+    }, CLICK_FEEDBACK_DURATION_MS);

그리고 파일 상단(선언부 바로 위)에 상수를 추가합니다:

@@
-import { EVENT_NAME } from '@/constants/eventName';
+import { EVENT_NAME } from '@/constants/eventName';
+
+const CLICK_FEEDBACK_DURATION_MS = 150 as const;
frontend/src/components/common/Header/Header.tsx (1)

114-152: 메모이제이션 무력화 요인 제거: navLinks/useCallback 도입

현재 navLinks 배열을 렌더마다 새로 생성하고(onHomeClick도 인라인 함수) 있어 memo로 감싼 하위 컴포넌트(DesktopHeader, MobileHeader, MobileMenuDrawer)의 메모이제이션 이점이 줄어듭니다. navLinks를 useMemo로, onHomeClick 래퍼를 useCallback으로 감싸 참조 안정성을 확보해 주세요.

적용 예시:

@@
-import { memo } from 'react';
+import { memo, useCallback, useMemo } from 'react';
@@
   const {
     handleHomeClick,
     handleIntroduceClick,
     handleClubUnionClick,
     handleMenuClick,
   } = useHeaderService();
@@
-  const navLinks: NavLinkData[] = [
-    { label: '모아동 소개', handler: handleIntroduceClick },
-    { label: '총동아리연합회 소개', handler: handleClubUnionClick },
-  ];
+  const navLinks: NavLinkData[] = useMemo(
+    () => [
+      { label: '모아동 소개', handler: handleIntroduceClick },
+      { label: '총동아리연합회 소개', handler: handleClubUnionClick },
+    ],
+    [handleIntroduceClick, handleClubUnionClick],
+  );
+
+  const handleDesktopHomeClick = useCallback(
+    () => handleHomeClick('desktop'),
+    [handleHomeClick],
+  );
+  const handleMobileHomeClick = useCallback(
+    () => handleHomeClick('mobile'),
+    [handleHomeClick],
+  );
@@
-      {isMobile ? (
-        <MobileHeader
-          onHomeClick={() => handleHomeClick('mobile')}
-          onMenuClick={openMenu}
-        />
-      ) : (
-        <DesktopHeader
-          isAdminPage={isAdminPage}
-          navLinks={navLinks}
-          onHomeClick={() => handleHomeClick('desktop')}
-        />
-      )}
+      {isMobile ? (
+        <MobileHeader onHomeClick={handleMobileHomeClick} onMenuClick={openMenu} />
+      ) : (
+        <DesktopHeader
+          isAdminPage={isAdminPage}
+          navLinks={navLinks}
+          onHomeClick={handleDesktopHomeClick}
+        />
+      )}
@@
-        onHomeClick={() => {
-          handleHomeClick('mobile');
-          closeMenu();
-        }}
+        onHomeClick={() => {
+          handleMobileHomeClick();
+          closeMenu();
+        }}
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx (3)

58-76: 디바운스 지연값(400ms) 상수화 및 의존성 보강

  • 400ms 매직 넘버를 상수로 추출해 주세요.
  • updateApplicant를 의존성에 포함해 최신 mutate 참조를 유지하는 것이 안전합니다.
@@
-const AVAILABLE_STATUSES = [
+const AVAILABLE_STATUSES = [
   ApplicationStatus.SUBMITTED, // 서류검토 (SUBMITTED 포함)
   ApplicationStatus.INTERVIEW_SCHEDULED, // 면접예정
   ApplicationStatus.ACCEPTED, // 합격
 ] as const;
 
+const UPDATE_DEBOUNCE_MS = 400 as const;
@@
-  const updateApplicantDetail = useMemo(
+  const updateApplicantDetail = useMemo(
     () =>
-      debounce((memo, status) => {
+      debounce((memo, status) => {
@@
-        );
-      }, 400),
-    [clubId, questionId],
+        );
+      }, UPDATE_DEBOUNCE_MS),
+    [clubId, questionId, updateApplicant],
   );

추가로, debounce 유틸이 cancel을 제공한다면 언마운트 시 잔여 호출을 취소해 사이드이펙트를 줄일 수 있습니다.

useEffect(() => {
  return () => {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (updateApplicantDetail as any)?.cancel?.();
  };
}, [updateApplicantDetail]);

38-49: 파라미터 명칭 정합성 개선: questionId → applicantId

URL이 /admin/applicants/:id인데 로컬 변수명이 questionId라 혼동 여지가 큽니다. 명확성을 위해 applicantId로 변경을 권장합니다.

-  const { questionId } = useParams<{ questionId: string }>();
+  const { applicantId } = useParams<{ applicantId: string }>();
@@
-  const { mutate: updateApplicant } = useUpdateApplicant(clubId!, questionId!);
+  const { mutate: updateApplicant } = useUpdateApplicant(clubId!, applicantId!);
@@
-  const applicantIndex =
-    applicantsData?.applicants.findIndex((a) => a.id === questionId) ?? -1;
+  const applicantIndex =
+    applicantsData?.applicants.findIndex((a) => a.id === applicantId) ?? -1;

메모이제이션 의존성도 함께 갱신해 주세요.

-    [clubId, questionId],
+    [clubId, applicantId],

168-171: Textarea 이벤트 onInput → onChange로 통일

React의 통상적인 제어 컴포넌트 패턴에 맞춰 onInput 대신 onChange 사용을 권장합니다. 기능상 차이는 없지만 일관성이 좋아집니다.

-          <Styled.MemoTextarea
-            onInput={handleMemoChange}
+          <Styled.MemoTextarea
+            onChange={handleMemoChange}
             placeholder='메모를 입력해주세요'
             value={applicantMemo}
           ></Styled.MemoTextarea>
frontend/src/constants/eventName.ts (2)

1-19: 타입 세이프티 보강: EventName 유틸 타입 노출 제안

소비 측에서 이벤트 문자열을 안전하게 받도록 유틸 타입을 함께 export하면 좋습니다.

export type EventName = (typeof EVENT_NAME)[keyof typeof EVENT_NAME];

9-17: 이벤트 값 언어/문구 일관성

영문/한글이 혼재하고 "Mobile Menubar delete Button Clicked"처럼 표현도 다소 일관성이 떨어집니다. 분석 체계화를 위해 차기 PR에서 언어 통일 및 문구 교정(예: 'Mobile Menubar Delete Button Clicked')을 고려해 주세요.

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

117-123: 리스트 key에 index 사용 지양 → 안정 키(id)로 변경

필터/정렬 시 DOM 재활용 오류와 퍼포먼스 저하가 발생할 수 있습니다. 고유 id를 key로 사용해 주세요.

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

21-25: answers[0] 접근 방어 및 널 세이프 필터링

일부 데이터에서 answers가 비거나 0번 인덱스가 없을 수 있습니다. optional chaining으로 방어하면 안전합니다.

-    return applicantsData.applicants.filter((user: Applicant) =>
-      user.answers[0].value
-        .toLowerCase()
-        .includes(keyword.trim().toLowerCase()),
-    );
+    return applicantsData.applicants.filter((user: Applicant) => {
+      const name = user.answers?.[0]?.value ?? '';
+      return name.toLowerCase().includes(keyword.trim().toLowerCase());
+    });

73-86: 드롭다운 화살표 아이콘 대체 텍스트 처리

장식용 이미지에는 빈 alt를 지정해 스크린리더가 불필요하게 읽지 않도록 해 주세요.

-              <Styled.Arrow src={selectIcon} />
+              <Styled.Arrow src={selectIcon} alt='' />
@@
-              <Styled.Arrow src={selectIcon} />
+              <Styled.Arrow src={selectIcon} alt='' />

100-112: 컬럼 폭 매직 넘버 상수화

가독성과 유지보수성을 위해 컬럼 폭 숫자를 상수로 추출해 주세요.

상수 선언(파일 상단에 인접 선언 권장):

@@
-import React, { useMemo, useState } from 'react';
+import React, { useMemo, useState } from 'react';
+
+const COL_WIDTH = {
+  checkbox: 40,
+  status: 120,
+  name: 80,
+  date: 140,
+} as const;

사용처 교체:

-              <Styled.ApplicantTableHeader
-                width={40}
-              ></Styled.ApplicantTableHeader>
-              <Styled.ApplicantTableHeader width={120}>
+              <Styled.ApplicantTableHeader width={COL_WIDTH.checkbox}></Styled.ApplicantTableHeader>
+              <Styled.ApplicantTableHeader width={COL_WIDTH.status}>
                 현재상태
               </Styled.ApplicantTableHeader>
-              <Styled.ApplicantTableHeader width={80} borderLeft={true}>
+              <Styled.ApplicantTableHeader width={COL_WIDTH.name} borderLeft={true}>
                 이름
               </Styled.ApplicantTableHeader>
@@
-              <Styled.ApplicantTableHeader width={140} borderLeft={true}>
+              <Styled.ApplicantTableHeader width={COL_WIDTH.date} borderLeft={true}>
                 제출날짜
               </Styled.ApplicantTableHeader>

151-163: 날짜 포맷 IIFE 제거 → 헬퍼 함수로 분리

가독성을 위해 즉시실행함수 대신 포맷터 함수를 사용해 주세요.

-                  {
-                    // createdAt을 yyyy-mm-dd 형식으로 변환
-                    // 임시로.. 나중에 변경해야함
-                    (() => {
-                      const date = new Date(item.createdAt);
-                      const year = date.getFullYear();
-                      const month = String(date.getMonth() + 1).padStart(
-                        2,
-                        '0',
-                      );
-                      const day = String(date.getDate()).padStart(2, '0');
-                      return `${year}-${month}-${day}`;
-                    })()
-                  }
+                  {formatDate(item.createdAt)}

파일 상단에 헬퍼 함수 추가:

function formatDate(iso: string) {
  const date = new Date(iso);
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, '0');
  const day = String(date.getDate()).padStart(2, '0');
  return `${year}-${month}-${day}`;
}
frontend/src/services/header/useHeaderService.ts (2)

19-19: 경로 문자열 하드코딩을 상수로 치환 제안

여러 곳에서 사용하는 경로 문자열('/', '/introduce', '/club-union')은 상수로 모아 의미를 드러내면 변경과 오타 대응이 쉬워집니다.

아래처럼 치환을 제안합니다.

-      navigate('/');
+      navigate(ROUTES.home);
-    navigate('/introduce');
+    navigate(ROUTES.introduce);
-    navigate('/club-union');
+    navigate(ROUTES.clubUnion);

상수 정의(파일 상단 인접 위치 권장):

const ROUTES = {
  home: '/',
  introduce: '/introduce',
  clubUnion: '/club-union',
} as const;

Also applies to: 28-28, 33-33


41-46: 반환 객체의 참조 안정화(useMemo)로 불필요한 리렌더 방지

훅 소비처가 반환 객체 자체를 의존성에 둘 경우 매 렌더마다 새로운 객체가 생성됩니다. useMemo로 감싸 참조를 안정화하는 것을 권장합니다.

   return {
-    handleHomeClick: navigateToHome,
-    handleIntroduceClick,
-    handleClubUnionClick,
-    handleMenuClick,
-  };
+    return useMemo(
+      () => ({
+        handleHomeClick: navigateToHome,
+        handleIntroduceClick,
+        handleClubUnionClick,
+        handleMenuClick,
+      }),
+      [
+        navigateToHome,
+        handleIntroduceClick,
+        handleClubUnionClick,
+        handleMenuClick,
+      ],
+    );

추가: import 보완

import { useCallback, useMemo } from 'react';
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.styles.ts (3)

115-118: 중복된 height 선언 정리

동일 블록 내 height가 두 번 선언되어 마지막 값(14px)만 적용됩니다. 중복을 제거해 혼란을 줄이세요.

-    height: 40%;
     width: 4px;
-    height: 14px;
+    height: 14px;

34-49: ApplicantContainer 내 전역 select 스타일의 범위 오염 가능성

컨테이너 내부 모든 select에 화살표/appearance 리셋이 강제 적용됩니다. 특정 컴포넌트(StatusSelect 등)만 대상으로 스타일링하거나, 별도 래퍼/클래스로 스코프를 좁히는 편이 안전합니다.


27-29: 가변 레이아웃에서 gap: 240px + width: 82%는 축소 화면에서 깨질 수 있음

관리자 화면이라도 브라우저 크기 축소 시 레이아웃 붕괴가 발생할 수 있습니다. media query 또는 clamp를 사용한 유연값으로 보완을 권장합니다.

예:

gap: clamp(24px, 8vw, 240px);
width: min(82%, 1200px);
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts (3)

207-216: 중첩 3항 연산자 → switch로 가독성 개선

스타일 계산 로직은 간결해야 합니다. 가이드라인에도 따라 switch/IIFE로 치환을 권장합니다.

-  background: ${({ status }) =>
-    status === '서류검토'
-      ? '#E6F4FB'
-      : status === '면접예정'
-        ? '#E6FBF0'
-        : status === '합격'
-          ? '#F5F5F5'
-          : '#eee'};
+  background: ${({ status }) => {
+    switch (status) {
+      case '서류검토':
+        return '#E6F4FB';
+      case '면접예정':
+        return '#E6FBF0';
+      case '합격':
+        return '#F5F5F5';
+      default:
+        return '#eee';
+    }
+  }};

(아래 color의 단일 3항은 유지해도 무방합니다.)


176-184: 메모 컬럼 말줄임 처리 동작 확인 및 table-layout 고정 제안

max-width: 0은 컬럼 폭 제약이 없으면 기대대로 동작하지 않을 수 있습니다. 테이블에 table-layout: fixed를 적용하는 것을 고려해 주세요.

테이블 스타일 보완(선택):

 export const ApplicantTable = styled.table`
   width: 100%;
   border-collapse: collapse;
   background: #fff;
+  table-layout: fixed;
 `;

빠른 확인 방법:

  • 메모 컬럼에 긴 문자열 입력 후 말줄임(...)이 기대대로 표시되는지 UI에서 확인

96-107: 상수 도입으로 숫자 매직값 정리 제안

셀렉트 높이(35), 체크박스 크기/아이콘(24), 화살표(12) 등이 여러 곳에서 사용됩니다. 의미 있는 상수로 치환하면 유지보수성이 좋아집니다.

예시(파일 상단):

const CHECKBOX_SIZE = 24;
const FILTER_ARROW_SIZE = 12;
const FILTER_SELECT_HEIGHT = 35;

사용처에서 ${CHECKBOX_SIZE}px 등으로 교체

Also applies to: 186-199

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear 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 0b9327b and 9c2ed7b.

⛔ Files ignored due to path filters (6)
  • frontend/package-lock.json is excluded by !**/package-lock.json
  • frontend/src/assets/images/icons/applicant_drop.svg is excluded by !**/*.svg
  • frontend/src/assets/images/icons/checkBox.svg is excluded by !**/*.svg
  • frontend/src/assets/images/icons/next_applicant.svg is excluded by !**/*.svg
  • frontend/src/assets/images/icons/prev_applicant.svg is excluded by !**/*.svg
  • frontend/src/assets/images/icons/selectArrow.svg is excluded by !**/*.svg
📒 Files selected for processing (41)
  • frontend/config/webpack.dev.ts (1 hunks)
  • frontend/package.json (1 hunks)
  • frontend/src/App.tsx (3 hunks)
  • frontend/src/apis/application/updateApplicantDetail.ts (1 hunks)
  • frontend/src/components/common/Header/Header.tsx (1 hunks)
  • frontend/src/components/common/SearchBox/SearchBox.tsx (0 hunks)
  • frontend/src/components/common/SearchField/SearchField.tsx (1 hunks)
  • frontend/src/constants/CLUB_UNION_INFO.ts (1 hunks)
  • frontend/src/constants/eventName.ts (1 hunks)
  • frontend/src/hooks/PhotoList/usePhotoNavigation.ts (2 hunks)
  • frontend/src/hooks/queries/applicants/useUpdateApplicant.ts (1 hunks)
  • frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx (2 hunks)
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage.tsx (0 hunks)
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.styles.ts (1 hunks)
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx (1 hunks)
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts (3 hunks)
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (2 hunks)
  • frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx (2 hunks)
  • frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.styles.ts (3 hunks)
  • frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.tsx (4 hunks)
  • frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx (2 hunks)
  • frontend/src/pages/ClubDetailPage/components/BackNavigationBar/BackNavigationBar.tsx (1 hunks)
  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (2 hunks)
  • frontend/src/pages/ClubDetailPage/components/ClubCard/ClubCard.styles.ts (1 hunks)
  • frontend/src/pages/ClubDetailPage/components/ClubCard/ClubCard.tsx (1 hunks)
  • frontend/src/pages/ClubDetailPage/components/InfoTabs/InfoTabs.tsx (3 hunks)
  • frontend/src/pages/ClubDetailPage/components/RecommendedClubs/RecommendedClubs.styles.tsx (1 hunks)
  • frontend/src/pages/ClubDetailPage/components/RecommendedClubs/RecommendedClubs.tsx (1 hunks)
  • frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx (2 hunks)
  • frontend/src/pages/ClubDetailPage/components/SnsLinkIcons/SnsLinkIcons.tsx (2 hunks)
  • frontend/src/pages/ClubUnionPage/ClubUnionPage.styles.ts (1 hunks)
  • frontend/src/pages/ClubUnionPage/ClubUnionPage.tsx (1 hunks)
  • frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx (2 hunks)
  • frontend/src/pages/MainPage/components/SearchBox/SearchBox.tsx (1 hunks)
  • frontend/src/pages/MainPage/components/StatusRadioButton/StatusRadioButton.tsx (2 hunks)
  • frontend/src/services/header/useHeaderService.ts (1 hunks)
  • frontend/src/services/header/useMobileMenu.ts (2 hunks)
  • frontend/src/types/applicants.ts (0 hunks)
  • frontend/src/types/club.ts (1 hunks)
  • frontend/src/utils/mapStatusToGroup.ts (1 hunks)
  • frontend/src/utils/recruitmentPeriodParser.ts (1 hunks)
💤 Files with no reviewable changes (3)
  • frontend/src/components/common/SearchBox/SearchBox.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage.tsx
  • frontend/src/types/applicants.ts
🧰 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/types/club.ts
  • frontend/src/components/common/SearchField/SearchField.tsx
  • frontend/src/pages/ClubDetailPage/components/BackNavigationBar/BackNavigationBar.tsx
  • frontend/src/services/header/useMobileMenu.ts
  • frontend/src/pages/ClubUnionPage/ClubUnionPage.tsx
  • frontend/src/constants/CLUB_UNION_INFO.ts
  • frontend/src/pages/MainPage/components/SearchBox/SearchBox.tsx
  • frontend/src/apis/application/updateApplicantDetail.ts
  • frontend/src/utils/recruitmentPeriodParser.ts
  • frontend/src/pages/MainPage/components/StatusRadioButton/StatusRadioButton.tsx
  • frontend/src/pages/ClubDetailPage/components/ClubCard/ClubCard.styles.ts
  • frontend/src/utils/mapStatusToGroup.ts
  • frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx
  • frontend/src/pages/ClubDetailPage/components/InfoTabs/InfoTabs.tsx
  • frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
  • frontend/src/pages/ClubDetailPage/components/RecommendedClubs/RecommendedClubs.styles.tsx
  • frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx
  • frontend/src/hooks/queries/applicants/useUpdateApplicant.ts
  • frontend/src/pages/ClubDetailPage/components/ClubCard/ClubCard.tsx
  • frontend/src/hooks/PhotoList/usePhotoNavigation.ts
  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
  • frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx
  • frontend/src/pages/ClubDetailPage/components/RecommendedClubs/RecommendedClubs.tsx
  • frontend/src/pages/ClubUnionPage/ClubUnionPage.styles.ts
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.styles.ts
  • frontend/config/webpack.dev.ts
  • frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx
  • frontend/src/constants/eventName.ts
  • frontend/src/pages/ClubDetailPage/components/SnsLinkIcons/SnsLinkIcons.tsx
  • frontend/src/App.tsx
  • frontend/src/components/common/Header/Header.tsx
  • frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.styles.ts
  • frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.tsx
  • frontend/src/services/header/useHeaderService.ts
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts
  • 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/components/common/SearchField/SearchField.tsx
  • frontend/src/pages/ClubDetailPage/components/BackNavigationBar/BackNavigationBar.tsx
  • frontend/src/pages/ClubUnionPage/ClubUnionPage.tsx
  • frontend/src/pages/MainPage/components/SearchBox/SearchBox.tsx
  • frontend/src/pages/MainPage/components/StatusRadioButton/StatusRadioButton.tsx
  • frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx
  • frontend/src/pages/ClubDetailPage/components/InfoTabs/InfoTabs.tsx
  • frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
  • frontend/src/pages/ClubDetailPage/components/RecommendedClubs/RecommendedClubs.styles.tsx
  • frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx
  • frontend/src/pages/ClubDetailPage/components/ClubCard/ClubCard.tsx
  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
  • frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx
  • frontend/src/pages/ClubDetailPage/components/RecommendedClubs/RecommendedClubs.tsx
  • frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx
  • frontend/src/pages/ClubDetailPage/components/SnsLinkIcons/SnsLinkIcons.tsx
  • frontend/src/App.tsx
  • frontend/src/components/common/Header/Header.tsx
  • frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx
🧠 Learnings (4)
📚 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/**/*.{ts,tsx} : Replace magic numbers with named constants for clarity.

Applied to files:

  • frontend/src/pages/MainPage/components/StatusRadioButton/StatusRadioButton.tsx
  • frontend/src/constants/eventName.ts
📚 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 : Separate significantly different conditional UI/logic into distinct components.

Applied to files:

  • frontend/src/pages/ClubDetailPage/components/RecommendedClubs/RecommendedClubs.styles.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/ClubDetailPage/components/ClubCard/ClubCard.tsx
  • frontend/src/App.tsx
📚 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/**/*.{ts,tsx} : Define constants near related logic or ensure names link them clearly.

Applied to files:

  • frontend/src/constants/eventName.ts
🧬 Code Graph Analysis (19)
frontend/src/pages/ClubDetailPage/components/BackNavigationBar/BackNavigationBar.tsx (1)
frontend/src/constants/eventName.ts (1)
  • EVENT_NAME (1-19)
frontend/src/services/header/useMobileMenu.ts (1)
frontend/src/constants/eventName.ts (1)
  • EVENT_NAME (1-19)
frontend/src/pages/ClubUnionPage/ClubUnionPage.tsx (1)
frontend/src/constants/CLUB_UNION_INFO.ts (1)
  • CLUB_UNION_MEMBERS (32-122)
frontend/src/pages/MainPage/components/SearchBox/SearchBox.tsx (2)
frontend/src/context/SearchContext.tsx (1)
  • useSearch (39-45)
frontend/src/context/CategoryContext.tsx (1)
  • useCategory (11-18)
frontend/src/apis/application/updateApplicantDetail.ts (1)
frontend/src/apis/auth/secureFetch.ts (1)
  • secureFetch (3-41)
frontend/src/pages/MainPage/components/StatusRadioButton/StatusRadioButton.tsx (1)
frontend/src/constants/eventName.ts (1)
  • EVENT_NAME (1-19)
frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx (1)
frontend/src/constants/eventName.ts (1)
  • EVENT_NAME (1-19)
frontend/src/pages/ClubDetailPage/components/InfoTabs/InfoTabs.tsx (1)
frontend/src/constants/eventName.ts (1)
  • EVENT_NAME (1-19)
frontend/src/hooks/queries/applicants/useUpdateApplicant.ts (1)
frontend/src/apis/application/updateApplicantDetail.ts (1)
  • updateApplicantDetail (5-36)
frontend/src/pages/ClubDetailPage/components/ClubCard/ClubCard.tsx (4)
frontend/src/types/club.ts (1)
  • Club (3-12)
frontend/src/constants/eventName.ts (1)
  • EVENT_NAME (1-19)
frontend/src/pages/MainPage/components/ClubCard/ClubCard.tsx (1)
  • club (11-55)
frontend/src/pages/MainPage/MainPage.tsx (1)
  • club (39-39)
frontend/src/hooks/PhotoList/usePhotoNavigation.ts (1)
frontend/src/constants/eventName.ts (1)
  • EVENT_NAME (1-19)
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (1)
frontend/src/constants/eventName.ts (1)
  • EVENT_NAME (1-19)
frontend/src/pages/ClubDetailPage/components/RecommendedClubs/RecommendedClubs.tsx (1)
frontend/src/types/club.ts (1)
  • Club (3-12)
frontend/src/pages/ClubUnionPage/ClubUnionPage.styles.ts (1)
frontend/src/styles/mediaQuery.ts (1)
  • media (6-9)
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx (5)
frontend/src/context/AdminClubContext.tsx (1)
  • useAdminClubContext (30-37)
frontend/src/hooks/queries/application/useGetApplication.ts (1)
  • useGetApplication (4-10)
frontend/src/hooks/queries/applicants/useUpdateApplicant.ts (1)
  • useUpdateApplicant (5-18)
frontend/src/apis/application/updateApplicantDetail.ts (1)
  • updateApplicantDetail (5-36)
frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.styles.ts (1)
  • QuestionContainer (20-25)
frontend/src/pages/ClubDetailPage/components/SnsLinkIcons/SnsLinkIcons.tsx (1)
frontend/src/constants/eventName.ts (1)
  • EVENT_NAME (1-19)
frontend/src/components/common/Header/Header.tsx (1)
frontend/src/components/common/Header/Header.styles.ts (1)
  • MobileMainIcon (112-124)
frontend/src/services/header/useHeaderService.ts (2)
frontend/src/constants/eventName.ts (1)
  • EVENT_NAME (1-19)
frontend/src/context/SearchContext.tsx (1)
  • useSearch (39-45)
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (2)
frontend/src/context/AdminClubContext.tsx (1)
  • useAdminClubContext (30-37)
frontend/src/types/applicants.ts (1)
  • Applicant (18-24)
🪛 Biome (2.1.2)
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx

[error] 16-16: 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 (36)
frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx (1)

18-19: 탭 라벨 변경(‘지원자 현황’)은 적절합니다

경로는 그대로이고 카피만 바뀌어 UX 의도가 명확해졌습니다. 라벨 기반으로 요소를 찾는 E2E 테스트나 애널리틱스가 있다면 함께 업데이트만 확인하세요.

frontend/src/utils/recruitmentPeriodParser.ts (1)

10-16: UI 표시 시 UTC→로컬 변환에 따른 시간 이동 여부 확인 필요

parseRecruitmentDateString/parseRecruitmentPeriod이 입력을 UTC(“+0000”)로 해석하기 때문에, 각 컴포넌트에서 Date 객체를 렌더링할 때 로컬 타임존(KST 등)으로 변환되어 예기치 않게 날짜·시간이 이동(예: 09:00 UTC → 18:00 KST 전일/익일 경계)될 수 있습니다. 아래 사용처를 중심으로 UX 기대치와 서버 저장 포맷(ISO UTC) 간 일관성이 유지되는지 검증해주세요:

  • AdminPage/tabs/RecruitEditTab (RecruitEditTab.tsx)의 DatePicker 초기값
  • ClubDetailPage/components/ClubDetailFooter (ClubDetailFooter.tsx)의 마감 시간 표시
  • ClubDetailPage/components/ClubApplyButton (ClubApplyButton.tsx)의 마감 시간 텍스트
frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx (1)

30-33: 초기값 now 폴백은 합리적이나 타임존 UX 확인 필요

parseRecruitmentPeriod가 UTC로 해석되도록 바뀐 상황에서, 로컬 DatePicker와 조합 시 표시 시간 이동이 없는지 확인이 필요합니다. 현재 폴백(now)이 로컬 기준인 점도 함께 고려해 주세요.

frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.tsx (2)

26-27: 마우스 다운 기본 동작 방지로 포커스/블러 이슈 예방 LGTM

헤더 네비게이션 버튼의 onMouseDown 기본 동작 방지는 입력 포커스 손실을 막는 데 유효합니다.

Also applies to: 37-38


50-58: 시작/종료 교차 동기화 로직 단순·명확 LGTM

범위를 벗어나는 선택 시 서로를 보정하는 접근은 UX에 부합합니다. 의도치 않은 루프도 없어 보입니다.

Also applies to: 60-68

frontend/src/types/club.ts (1)

16-28: 서버/모킹 데이터 스키마 검증 필요
프론트 타입에 recommendClubs가 추가되었으니, 실제 백엔드 응답 스키마(또는 mock)에도 동일 필드가 존재하는지 꼭 확인해주세요. 불일치 시 undefined 접근으로 런타임 에러가 발생할 수 있습니다.

아래 스크립트 예시를 참고해 전체 코드베이스(및 모킹 데이터)에서 빠르게 점검해보세요:

# 전체 코드베이스에서 recommendClubs 사용처 확인
rg -nP '\brecommendClubs\b' -C2

# ClubDetailPage → RecommendedClubs 전달부 점검
rg -nP 'ClubDetailPage' -C3

# 모킹 데이터 디렉토리에서 recommendClubs 포함 여부 확인
rg -nP '\brecommendClubs\b' -C2 -g '*mock*'

필요하다면 백엔드 GraphQL 스키마나 REST API 문서도 함께 검토해 주세요.

frontend/package.json (1)

40-42: webpack.prod.ts에 swiper CSS 로더 설정 추가 필요

webpack.dev.ts에는 아래와 같이 swiper/css 처리를 위한 CSS loader(include: /node_modules\/swiper/) 설정이 반영되어 있으나,
frontend/config/webpack.prod.ts에는 동일한 설정이 누락되어 있습니다.
프로덕션 빌드 시 import 'swiper/css'가 무시되어 스타일이 적용되지 않을 수 있으니, dev 설정을 참고하여 prod 설정에도 CSS loader를 추가해 주세요.

  • 설정 파일: frontend/config/webpack.prod.ts

  • 추가 예시:

      // webpack.prod.ts 내 모듈 룰 배열(rule[] ) 중
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
    +   include: /node_modules\/swiper/,
      },

Likely an incorrect or invalid review comment.

frontend/config/webpack.dev.ts (1)

58-61: 벤더 CSS include 배열화(LGTM)

react-datepicker와 swiper의 CSS를 명시적으로 include하여 node_modules CSS 로딩 문제를 방지한 점 좋습니다. 개발 환경에서의 스타일 깨짐 가능성을 줄여줍니다.

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

7-41: 데이터 기반 렌더링과 스타일 분리 구조 👍

정적인 소개 섹션 + 멤버 그리드 구성, key로 id 사용, 스타일 모듈 분리 모두 깔끔합니다. 큰 이슈 없습니다.

frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (1)

8-8: 이벤트명 상수화 적용 잘 되었어요

하드코딩 문자열을 EVENT_NAME으로 치환해 추적 일관성과 오타 리스크를 줄였습니다.

Also applies to: 49-49

frontend/src/pages/ClubDetailPage/components/BackNavigationBar/BackNavigationBar.tsx (1)

5-5: 이벤트명 상수화 적용 확인

BACK_BUTTON_CLICKED 상수 사용으로 추적 일관성 확보된 점 좋습니다.

Also applies to: 12-12

frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx (1)

6-6: 이벤트명 상수화 적용 확인

공유 버튼 트래킹 문자열을 상수로 통일한 점 좋습니다.

Also applies to: 47-47

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

3-3: 이벤트 이름 상수화 적용: 일관성/오타 방지에 좋아요

하드코딩된 문자열 대신 EVENT_NAME 상수 사용으로 분석 이벤트 네이밍이 표준화되었습니다. 유지보수성 향상에 기여합니다.

frontend/src/pages/ClubDetailPage/components/SnsLinkIcons/SnsLinkIcons.tsx (2)

6-6: EVENT_NAME 상수 도입 LGTM

분석 이벤트 문자열을 상수로 통일했습니다. 오타/변경 관리에 유리합니다.


30-34: trackEvent의 이벤트 키 상수화 반영 적절

페이로드 변경 없이 키만 상수로 치환되어, 기존 이벤트 분석 흐름을 유지하면서 표준화를 달성했습니다.

frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx (2)

17-19: 추천 동아리 섹션 도입: 구성/의존성 주입 적절

RecommendedClubs 컴포넌트를 분리해서 가져오는 방향이 명확합니다. 타입/경로 의존성도 적절히 정리되어 있습니다.


73-75: 빈 배열 fallback으로 안전성 확보

recommendClubs가 없을 때 빈 배열로 전달해 컴포넌트 내부 분기(null 반환)와 잘 맞물립니다. 런타임 예외 가능성을 줄여줍니다.

frontend/src/hooks/PhotoList/usePhotoNavigation.ts (2)

3-3: EVENT_NAME 상수 사용으로 분석 이벤트 표준화 👍

하드코딩 제거로 네이밍 일관성/변경 용이성이 향상되었습니다.


38-41: 이벤트 키 치환 외 동작 동일 — 회귀 위험 낮음

페이로드와 흐름은 유지되고 이벤트 키만 상수로 대체되었습니다. 기존 대시보드/리포팅에도 영향이 없을 것으로 보입니다.

Also applies to: 47-50

frontend/src/App.tsx (3)

21-23: 경로/구조 변경 반영된 import 경로 업데이트 LGTM

ApplicantDetailPage의 새 구조에 맞춘 import 경로와 ClubUnionPage 추가가 명확합니다.


81-88: 어드민 상세 라우트 가독성 개선

멀티라인으로 정리되어 읽기 쉬워졌고, 라우팅 구조/권한 보호 흐름은 그대로 유지됩니다.


99-99: 신규 라우트(/club-union) 추가 승인

별도 페이지로 분리된 ClubUnionPage 라우팅이 명확합니다. 404 처리와 다른 경로들과의 충돌도 없어 보입니다.

frontend/src/pages/ClubDetailPage/components/InfoTabs/InfoTabs.tsx (1)

16-19: 이벤트 상수로의 전환 좋습니다

EVENT_NAME.TAB_CLICKED 사용으로 트래킹 키 중앙화가 잘 반영되었습니다. 페이로드 구조도 기존과 동일해 안정적입니다.

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

19-21: 이벤트 상수 사용 👍

EVENT_NAME.STATUS_RADIO_BUTTON_CLICKED로 치환되어 이벤트 키 관리가 일관적입니다.

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

1-1: 믹스패널 훅과 이벤트 상수 도입 잘 적용됨

직접 mixpanel 호출을 훅(useMixpanelTrack)과 상수(EVENT_NAME.CATEGORY_BUTTON_CLICKED)로 치환하여 추적 로직이 일관되고 테스트 용이해졌습니다. 페이로드도 간결하고 충분합니다.

Also applies to: 12-13, 33-39

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

1-3: secureFetch 활용 좋습니다

토큰 만료 처리까지 고려된 secureFetch를 재사용하여 보안·일관성 측면에서 적절합니다.

frontend/src/utils/mapStatusToGroup.ts (1)

11-12: 라벨 표기 확인 요청: ‘불합’ vs ‘불합격’

제품/카피 가이드에 따라 ‘불합(불합격의 약칭)’이 의도된 표현인지 확인해 주세요. 리스트/상세 등 다른 화면과 표기가 일관되어야 합니다.

frontend/src/pages/ClubDetailPage/components/ClubCard/ClubCard.tsx (1)

18-26: 이벤트 트래킹 표준화 적용 잘 되었습니다

useMixpanelTrack와 EVENT_NAME 상수를 활용한 이벤트 로깅 구성은 일관성이 있고 유지보수에 용이합니다. 페이로드 키도 구체적이라 분석 용이성이 좋습니다.

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

95-104: 모바일 헤더 아이콘 접근성 처리 적절

모바일 메인 아이콘과 메뉴 버튼에 적절한 aria-label과 alt 처리가 되어 있어 접근성 측면에서 양호합니다.


95-102: 관리자 페이지의 검색창 노출 의도 확인 요청

데스크톱에서는 isAdminPage일 때 SearchBox 렌더를 숨기지만(Line 59), 모바일 헤더에서는 항상(SearchBox가 조건 없이) 노출됩니다. 의도된 UX인지 확인 부탁드립니다. 일관성 있게 숨기려면 모바일에서도 isAdminPage 조건을 고려해야 합니다.

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

91-95: answers[0] 존재 가정에 대한 안전장치 확인

지원자 답변 배열에 항상 첫 요소가 존재한다는 가정이 곳곳에 있습니다. 데이터가 비어 있을 경우 런타임 오류 위험이 있습니다. 최소한 optional chaining으로 방어하거나, 서버 계약을 문서화해 주세요.

필요 시 다음과 같이 방어 코드를 적용할 수 있습니다.

-  const getAnswerByQuestionId = (qId: number) => {
-    return applicant.answers
-      .filter((ans) => ans.id === qId)
-      .map((ans) => ans.value);
-  };
+  const getAnswerByQuestionId = (qId: number) => {
+    return applicant.answers
+      ?.filter((ans) => ans.id === qId)
+      ?.map((ans) => ans.value) ?? [];
+  };
frontend/src/constants/eventName.ts (1)

1-19: 이벤트 네이밍 상수화 매우 좋습니다

단일 소스(EVENT_NAME)로 표준화되어 추적/변경 관리가 쉬워졌습니다.

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

70-95: 필터/정렬 UI 마크업 구성 양호

FilterContainer/SelectWrapper/Arrow로 구조화하여 향후 정렬/필터 로직 확장에 유리합니다. 접근성 속성(SearchField ariaLabel 등)도 적절합니다.

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

5-5: 이벤트 상수화 + 핸들러 useCallback 전환: 👍

하드코딩된 이벤트명을 EVENT_NAME으로 표준화하고, 모든 핸들러를 useCallback으로 메모이즈한 점 좋습니다. 의존성 배열도 정확합니다.

Also applies to: 17-25, 27-39


32-35: 라우트 정의 확인 완료

/club-union 경로는 이미 frontend/src/App.tsx 99번째 줄의

<Route path='/club-union' element={<ClubUnionPage />} />

로 정의되어 있어 네비게이션 오류가 없습니다.

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

74-95: 필터 셀렉트 래퍼 추가 및 화살표 오버레이 구성: 👍

FilterContainer/SelectWrapper/Arrow 분리로 재사용성과 스타일 제어가 쉬워졌습니다. 셀렉트의 padding-right 조정과도 일관적입니다.

Comment on lines +58 to +61
include: [
path.resolve(__dirname, '../node_modules/react-datepicker/dist'),
path.resolve(__dirname, '../node_modules/swiper'),
],
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

❓ Verification inconclusive

prod 설정과의 정합성 유지 권장

동일 예외 설정이 webpack.prod.ts에도 적용되어야 합니다. 미적용 시 로컬(dev)에서는 정상, 배포 빌드에서는 swiper 스타일이 누락되는 이슈가 재현됩니다.

빠른 점검 스크립트:


🏁 Script executed:

#!/bin/bash
# prod 설정에서 swiper css 예외 처리 검색
fd webpack.prod.ts | xargs -r -I {} rg -n "node_modules/react-datepicker/dist|node_modules/swiper" {}

Length of output: 107


webpack.prod.ts에도 React Datepicker·Swiper 스타일 예외 설정 추가 필요

빠른 점검 스크립트(fd webpack.prod.ts | xargs -r -I {} rg -n "node_modules/react-datepicker/dist|node_modules/swiper" {}) 결과, prod용 Webpack 설정에 해당 include 예외가 존재하지 않음을 확인했습니다. 로컬(dev)에서는 정상적으로 스타일이 로드되지만, 배포(build) 시 Swiper 스타일이 누락될 수 있으므로 아래와 같이 webpack.prod.ts에도 동일한 설정을 적용해주세요.

  • 대상 파일: frontend/config/webpack.prod.ts
  • 위치: CSS 로더 설정의 include 배열
   include: [
     path.resolve(__dirname, '../src'),
+    path.resolve(__dirname, '../node_modules/react-datepicker/dist'),
+    path.resolve(__dirname, '../node_modules/swiper'),
   ],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
include: [
path.resolve(__dirname, '../node_modules/react-datepicker/dist'),
path.resolve(__dirname, '../node_modules/swiper'),
],
include: [
path.resolve(__dirname, '../src'),
path.resolve(__dirname, '../node_modules/react-datepicker/dist'),
path.resolve(__dirname, '../node_modules/swiper'),
],
🤖 Prompt for AI Agents
In frontend/config/webpack.prod.ts around the CSS loader's include array, the
prod config is missing the same node_modules exceptions for react-datepicker and
swiper present in webpack.dev.ts; update the include array to add
path.resolve(__dirname, '../node_modules/react-datepicker/dist') and
path.resolve(__dirname, '../node_modules/swiper') so production builds also
process those packages' styles.

Comment on lines +12 to +13
queryClient.invalidateQueries({ queryKey: ["clubApplicants"] });
},
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

클럽별 쿼리 키 사용 여부 확인 필요

실제 목록 쿼리에서 어떤 키를 사용 중인지 확인해 동일 체계를 유지해 주세요. 전역/클럽별 키 혼재 시 캐시 무효화 누락/과다 무효화가 발생할 수 있습니다.

다음 스크립트로 clubApplicants 키 사용처를 확인할 수 있습니다(레포 루트에서 실행).


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== clubApplicants 쿼리 키 사용처 탐색 =="
# clubApplicants 단독 사용
rg -n -C2 --type=ts --type=tsx "queryKey\s*:\s*\[\s*['\"]clubApplicants['\"]\s*\]" || true

# clubId 등과 함께 조합된 사용
rg -n -C2 --type=ts --type=tsx "queryKey\s*:\s*\[\s*['\"]clubApplicants['\"].*?\]" || true

echo "== useMutation에서 mutationKey 사용 여부(참고) =="
rg -n -C2 --type=ts --type=tsx "useMutation\s*\(\s*\{\s*[^}]*mutationKey" || true

Length of output: 546


클럽별 쿼리 키 사용 일관성 확인을 위해 전체 코드베이스에서 clubApplicants 키의 사용처를 다시 검색해주세요. 아래 스크립트를 레포지토리 루트에서 실행 후 결과를 공유해 주시면, invalidateQueries 호출이 올바른 키 구조를 따라가고 있는지 검증할 수 있습니다.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== clubApplicants 키 전체 사용처 검색 =="
rg -n -C2 "clubApplicants" || true

Length of output: 1112


일관된 쿼리 키 사용: invalidateQueries에 clubId 포함 필요
useGetApplicants 훅에서는 ['clubApplicants', clubId] 키를 사용 중이지만, useUpdateApplicant의 onSuccess에서는 ['clubApplicants']만 전달하고 있어 특정 클럽 캐시가 제대로 무효화되지 않거나 모든 클럽 캐시가 과도하게 무효화될 수 있습니다. 아래처럼 clubId를 포함한 키로 호출하도록 수정해주세요.

  • 대상: frontend/src/hooks/queries/applicants/useUpdateApplicant.ts, onSuccess 콜백
  • 수정 전:
    queryClient.invalidateQueries({ queryKey: ['clubApplicants'] });
  • 수정 후:
    queryClient.invalidateQueries({ queryKey: ['clubApplicants', clubId] });
🤖 Prompt for AI Agents
In frontend/src/hooks/queries/applicants/useUpdateApplicant.ts around lines
12-13, the onSuccess handler calls queryClient.invalidateQueries with queryKey
['clubApplicants'] which doesn't include the clubId used elsewhere; change the
invalidateQueries call to use the same key shape as useGetApplicants by passing
['clubApplicants', clubId] so only the specific club's cache is invalidated.

Comment on lines +77 to +80
&:focus {
outline: none;
}
`;
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

포커스 아웃라인 제거로 인한 접근성 저하: focus-visible 스타일 추가 권장

현재 outline을 제거만 하고 있어 키보드 사용자에게 포커스가 보이지 않습니다. 최소한 focus-visible에 커스텀 아웃라인을 제공하세요.

StatusSelect:

-  &:focus {
-    outline: none;
-  }
+  &:focus-visible {
+    outline: 2px solid var(--primary, #0B5FFF);
+    outline-offset: 2px;
+  }

MemoTextarea:

-  &:focus {
-    outline: none;
-  }
+  &:focus-visible {
+    outline: 2px solid var(--primary, #0B5FFF);
+    outline-offset: 2px;
+  }

Also applies to: 135-137

🤖 Prompt for AI Agents
In
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.styles.ts
around lines 77-80 and also apply same change at 135-137, the current styles
remove the focus outline entirely which hides focus for keyboard users; instead
add a :focus-visible rule that preserves or replaces the outline with an
accessible custom focus style (e.g., visible 2px solid or box-shadow with
high-contrast color and sufficient focus ring spacing), keep outline: none only
for :focus but ensure :focus-visible sets the visible outline, and apply the
same pattern to the StatusSelect and MemoTextarea style blocks so keyboard focus
is clearly visible.

background: #fff;
border-radius: 10px;
padding: 6px 14px;
color: var(--, #111);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

CSS 변수 사용 오류: var(--, #111)는 유효하지 않습니다

커스텀 프로퍼티 이름이 비어 있어 전체 속성이 무효화됩니다. 즉시 수정이 필요합니다.

-  color: var(--, #111);
+  color: #111;

디자인 토큰을 사용한다면 예: var(--text-primary, #111)로 교체할 수 있습니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
color: var(--, #111);
color: #111;
🤖 Prompt for AI Agents
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.styles.ts
around line 105: the CSS uses an invalid custom property syntax `var(--, #111)`
which invalidates the declaration; replace it with a valid variable name (for
example `var(--text-primary, #111)`) or the literal color `#111`, ensuring you
use the correct design token name if your project defines one.

Comment on lines +130 to +145
export const ApplicantTableHeader = styled.th<{
width?: number | string;
borderLeft?: boolean;
isMemo?: boolean;
}>`
position: relative;
background: #fafafa;
padding: 12px 8px;
font-size: 16px;
font-weight: 500;
color: #888;
text-align: center;
font-size: 12px;
font-style: normal;
font-weight: 600;
color: var(--78, #787878);
width: ${({ width }) => (width ? `${width}px` : 'auto')};
text-align: ${({ isMemo }) => (isMemo ? 'left' : 'center')};
padding-left: ${({ isMemo }) => (isMemo ? '30px' : '8px')};

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

문자열 너비에 px가 덧붙는 버그 수정 필요

width?: number | string인데 현재 구현은 무조건 px를 붙여 "100%px" 같은 잘못된 CSS를 생성할 수 있습니다.

-  width: ${({ width }) => (width ? `${width}px` : 'auto')};
+  width: ${({ width }) =>
+    width !== undefined
+      ? typeof width === 'number'
+        ? `${width}px`
+        : width
+      : 'auto'};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const ApplicantTableHeader = styled.th<{
width?: number | string;
borderLeft?: boolean;
isMemo?: boolean;
}>`
position: relative;
background: #fafafa;
padding: 12px 8px;
font-size: 16px;
font-weight: 500;
color: #888;
text-align: center;
font-size: 12px;
font-style: normal;
font-weight: 600;
color: var(--78, #787878);
width: ${({ width }) => (width ? `${width}px` : 'auto')};
text-align: ${({ isMemo }) => (isMemo ? 'left' : 'center')};
padding-left: ${({ isMemo }) => (isMemo ? '30px' : '8px')};
export const ApplicantTableHeader = styled.th<{
width?: number | string;
borderLeft?: boolean;
isMemo?: boolean;
}>`
position: relative;
background: #fafafa;
padding: 12px 8px;
font-size: 12px;
font-style: normal;
font-weight: 600;
color: var(--78, #787878);
width: ${({ width }) =>
width !== undefined
? typeof width === 'number'
? `${width}px`
: width
: 'auto'};
text-align: ${({ isMemo }) => (isMemo ? 'left' : 'center')};
padding-left: ${({ isMemo }) => (isMemo ? '30px' : '8px')};
🤖 Prompt for AI Agents
In frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.ts
around lines 130-145, the width interpolation always appends "px" which can
produce invalid values like "100%px"; change the logic to only append "px" when
width is a number and use the string value as-is when width is a string (e.g. if
width === undefined use 'auto', else if typeof width === 'number' return
`${width}px`, else return width).

Comment on lines 14 to +16
if (!clubId) return null;

const filteredApplicants = useMemo(() => {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Hooks 규칙 위반: early return 이전에 모든 hook 호출 필요

현재 if (!clubId) return null;이 useMemo보다 앞에 있어 렌더 간 hook 호출 순서가 달라집니다. Biome 경고도 동일 이슈를 지적하고 있습니다. early return을 모든 hook 선언 이후로 이동해 주세요.

아래처럼 수정하면 경고가 해소됩니다.

@@
-  const [keyword, setKeyword] = useState('');
-  if (!clubId) return null;
+  const [keyword, setKeyword] = useState('');
@@
-  const filteredApplicants = useMemo(() => {
+  const filteredApplicants = useMemo(() => {
     if (!applicantsData?.applicants) return [];
@@
   }, [applicantsData, keyword]);
+
+  if (!clubId) return null;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!clubId) return null;
const filteredApplicants = useMemo(() => {
const [keyword, setKeyword] = useState('');
const filteredApplicants = useMemo(() => {
if (!applicantsData?.applicants) return [];
// …other filtering logic…
}, [applicantsData, keyword]);
if (!clubId) return null;
🧰 Tools
🪛 Biome (2.1.2)

[error] 16-16: 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)

🤖 Prompt for AI Agents
In frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx around
lines 14 to 16, the early return "if (!clubId) return null;" appears before
hooks are declared which breaks the rules-of-hooks; move that early return to
after all hook calls (e.g., useMemo, useState, useEffect, etc.) so that every
render calls hooks in the same order, i.e., declare and initialize all hooks
first, then do "if (!clubId) return null;" to short-circuit rendering.

Comment on lines +59 to +65
const Introduction = styled.p`
font-size: 0.875rem;
margin: 22px 3px 22px 5px;
color: rgba(129, 129, 129, 1);
line-height: 16px;
white-space: nowrap;
`;
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

한 줄 처리 시 overflow/ellipsis 누락 — 레이아웃 깨짐 예방

white-space: nowrap만 있으면 긴 텍스트가 컨테이너 밖으로 흘러나올 수 있습니다. ellipsis 처리를 추가해 주세요.

 const Introduction = styled.p`
   font-size: 0.875rem;
   margin: 22px 3px 22px 5px;
   color: rgba(129, 129, 129, 1);
   line-height: 16px;
   white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
 `;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const Introduction = styled.p`
font-size: 0.875rem;
margin: 22px 3px 22px 5px;
color: rgba(129, 129, 129, 1);
line-height: 16px;
white-space: nowrap;
`;
const Introduction = styled.p`
font-size: 0.875rem;
margin: 22px 3px 22px 5px;
color: rgba(129, 129, 129, 1);
line-height: 16px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
🤖 Prompt for AI Agents
In frontend/src/pages/ClubDetailPage/components/ClubCard/ClubCard.styles.ts
around lines 59 to 65, the Introduction paragraph only uses white-space: nowrap
so long text can overflow the container; update the styles to enable single-line
truncation by adding overflow: hidden and text-overflow: ellipsis (and ensure
the element is a block-level or inline-block element so ellipsis works, e.g.,
keep as display: block or set display: inline-block if needed); this will
prevent layout breakage by truncating overflowed text with an ellipsis.

Comment on lines +20 to +30
// 반응형 그리드 리스트
export const GridList = styled.div`
display: grid;
grid-template-columns: repeat(2, 1fr); // 기본 2열
gap: 16px;

// 화면 넓이 600px 이하일 경우 1열로 변경
@media (max-width: 600px) {
grid-template-columns: 1fr;
}
`;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

styled-components 내 // 주석은 CSS 파싱 오류 가능성 있음

템플릿 리터럴 안에서는 CSS 문법이 적용되므로 // 주석은 유효하지 않을 수 있습니다. /* ... */로 변경하거나 주석을 제거해 주세요. 현재 형태는 스타일 누락/깨짐을 유발할 수 있어 우선 수정 필요입니다.

-// 반응형 그리드 리스트
 export const GridList = styled.div`
   display: grid;
-  grid-template-columns: repeat(2, 1fr); // 기본 2열
+  grid-template-columns: repeat(2, 1fr); /* 기본 2열 */
   gap: 16px;
 
-  // 화면 넓이 600px 이하일 경우 1열로 변경
+  /* 화면 넓이 600px 이하일 경우 1열로 변경 */
   @media (max-width: 600px) {
     grid-template-columns: 1fr;
   }
 `;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 반응형 그리드 리스트
export const GridList = styled.div`
display: grid;
grid-template-columns: repeat(2, 1fr); // 기본 2열
gap: 16px;
// 화면 넓이 600px 이하일 경우 1열로 변경
@media (max-width: 600px) {
grid-template-columns: 1fr;
}
`;
export const GridList = styled.div`
display: grid;
grid-template-columns: repeat(2, 1fr); /* 기본 2열 */
gap: 16px;
/* 화면 넓이 600px 이하일 경우 1열로 변경 */
@media (max-width: 600px) {
grid-template-columns: 1fr;
}
`;
🤖 Prompt for AI Agents
In
frontend/src/pages/ClubDetailPage/components/RecommendedClubs/RecommendedClubs.styles.tsx
around lines 20 to 30, the styled-components template literal contains
JavaScript-style // comments which are invalid in CSS and can cause parsing
errors; replace those // comments with CSS block comments /* ... */ or remove
them entirely inside the template literal (e.g., change "// 기본 2열" and "// 화면 넓이
600px 이하일 경우 1열로 변경" to "/* 기본 2열 */" and "/* 화면 넓이 600px 이하일 경우 1열로 변경 */"),
then run a quick build or style check to ensure no other inline // comments
remain in any styled template.

Comment on lines +27 to +31
trackEvent('Search Executed', {
inputValue: inputValue,
page: currentPage,
});
};
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

이벤트명 하드코딩 대신 상수 사용 필요 (PR 요약과 불일치)

PR 요약에서 “이벤트명 상수화”를 언급했지만 이 파일은 'Search Executed' 문자열을 직접 사용하고 있습니다. 공용 상수(예: constants/eventName.ts)를 사용해 일관성을 맞춰 주세요.

필요 시 상수 파일 경로/식별자명을 확인한 뒤 아래와 같이 교체해 주세요(실제 상수명에 맞게 조정 필요):

-    trackEvent('Search Executed', {
-      inputValue: inputValue,
-      page: currentPage,
-    });
+    trackEvent(EVENT_NAME.SEARCH_EXECUTED, {
+      inputValue,
+      page: currentPage,
+    });

다음 스크립트로 이벤트명 상수 파일과 키를 확인할 수 있습니다.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== eventName.ts 파일 검색 =="
fd -t f "eventName.ts" frontend || true

echo "== 이벤트 상수 정의/사용 검색 =="
rg -n -C2 --type=ts --type=tsx "eventName|EVENT_NAME|Search Executed|SEARCH_EXECUTED" frontend || true

Length of output: 331


다음 스크립트로 eventName.ts 파일 내에 SEARCH_EXECUTED 상수가 정의되어 있는지 확인해 주세요.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== frontend/src/constants/eventName.ts 파일 내용 =="
cat frontend/src/constants/eventName.ts

Length of output: 1178


이벤트명 상수 추가 및 적용 필요

PR 요약에 맞춰 아래 두 곳을 수정해 주세요.

  • frontend/src/constants/eventName.ts
    • SEARCH_EXECUTED 상수를 추가해야 합니다.
  • frontend/src/pages/MainPage/components/SearchBox/SearchBox.tsx
    • 직접 하드코딩된 'Search Executed'를 상수로 교체하고, 객체 프로퍼티 축약 문법을 사용해 주세요.
--- a/frontend/src/constants/eventName.ts
+++ b/frontend/src/constants/eventName.ts
@@ export const EVENT_NAME = {
   CLUB_UNION_BUTTON_CLICKED: 'Club Union Button Clicked' as const,
+  SEARCH_EXECUTED: 'Search Executed' as const,
 } as const;
--- a/frontend/src/pages/MainPage/components/SearchBox/SearchBox.tsx
+++ b/frontend/src/pages/MainPage/components/SearchBox/SearchBox.tsx
@@ const onSearch = () => {
-  trackEvent('Search Executed', {
-    inputValue: inputValue,
-    page: currentPage,
-  });
+  trackEvent(EVENT_NAME.SEARCH_EXECUTED, {
+    inputValue,
+    page: currentPage,
+  });
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
trackEvent('Search Executed', {
inputValue: inputValue,
page: currentPage,
});
};
trackEvent(EVENT_NAME.SEARCH_EXECUTED, {
inputValue,
page: currentPage,
});
🤖 Prompt for AI Agents
In frontend/src/pages/MainPage/components/SearchBox/SearchBox.tsx around lines
27 to 31 and frontend/src/constants/eventName.ts, the event name is hardcoded as
'Search Executed' and object properties are not using shorthand; add a new
exported constant SEARCH_EXECUTED to frontend/src/constants/eventName.ts and
replace the hardcoded string in SearchBox.tsx with that constant, importing it,
and change the trackEvent call to use property shorthand
(trackEvent(SEARCH_EXECUTED, { inputValue, page: currentPage }) or rename
currentPage to page before calling to use { inputValue, page }).

);
}
const date = parse(s, 'yyyy.MM.dd HH:mm', new Date());
const date = parse(s + ' +0000', 'yyyy.MM.dd HH:mm X', new Date());
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

UTC 오프셋 파싱 토큰 불일치로 인해 파싱 실패 가능성 (X ↔ XX/XXX)

+0000 문자열은 XX(±HHmm) 또는 xxxx 패턴과 매칭됩니다. 현재 포맷 'yyyy.MM.dd HH:mm X'+0000을 붙이면 파싱 실패할 수 있습니다. 안정적으로 UTC로 해석하려면 +00:00XXX(±HH:mm)를 함께 사용하는 것이 안전합니다.

아래처럼 교체를 권장합니다.

- const date = parse(s + ' +0000', 'yyyy.MM.dd HH:mm X', new Date());
+ const date = parse(s + ' +00:00', 'yyyy.MM.dd HH:mm XXX', new Date());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const date = parse(s + ' +0000', 'yyyy.MM.dd HH:mm X', new Date());
const date = parse(s + ' +00:00', 'yyyy.MM.dd HH:mm XXX', new Date());
🤖 Prompt for AI Agents
In frontend/src/utils/recruitmentPeriodParser.ts around line 10, the parse call
appends '+0000' but uses the format token 'X' which doesn't match '+0000' and
can cause parse failures; change the appended offset to '+00:00' and the format
string to use the 'XXX' token (e.g. parse(s + ' +00:00', 'yyyy.MM.dd HH:mm XXX',
new Date())) so the UTC offset is parsed reliably as ±HH:mm.

Copy link
Member

@Zepelown Zepelown left a comment

Choose a reason for hiding this comment

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

이번 MVP 고생하셨습니다

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.

다들 수고했습니다
이제 리펙토링 가시죠 🚀

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

🧹 Nitpick comments (1)
frontend/src/components/application/QuestionTitle/QuestionTitle.tsx (1)

31-36: contentEditable에서 onChange로 변경 시 실시간 입력 누락/지연 가능성 — onInput 유지(또는 병행) 권장

contentEditable 요소는 브라우저/React 버전에 따라 onChange가 즉시(키 입력마다) 발생하지 않거나, IME(한글/일본어 등) 조합 중 동작이 불안정할 수 있습니다. 기존 onInput은 입력/붙여넣기 타이밍을 안정적으로 포착합니다. 현재 변경으로 빌더 모드에서 제목 변경이 실시간 반영되지 않거나, 최대 길이 검증 타이밍이 늦어질 위험이 있습니다.

변경 제안(diff):

-          onChange={(e) => {
+          onInput={(e) => {
             const value = e.currentTarget.textContent || '';
             if (value.length <= APPLICATION_FORM.QUESTION_TITLE.maxLength) {
               onTitleChange?.(value);
             }
           }}

추가 개선(선택): IME 조합 중에는 길이 검증/상태 반영을 스킵하고 조합 종료 시 반영하면 UX가 개선됩니다. 예시:

// 파일 상단: 필요 시 추가
import { useRef, useCallback } from 'react';

// 컴포넌트 내부
const isComposingRef = useRef(false);

const handleInput = useCallback((e: React.FormEvent<HTMLElement>) => {
  if (isComposingRef.current) return;
  const value = (e.currentTarget as HTMLElement).textContent || '';
  if (value.length <= APPLICATION_FORM.QUESTION_TITLE.maxLength) {
    onTitleChange?.(value);
  }
}, [onTitleChange]);

// JSX
<Styled.QuestionTitleText
  contentEditable={mode !== 'answer'}
  suppressContentEditableWarning={true}
  onCompositionStart={() => { isComposingRef.current = true; }}
  onCompositionEnd={(e) => {
    isComposingRef.current = false;
    const value = (e.currentTarget as HTMLElement).textContent || '';
    if (value.length <= APPLICATION_FORM.QUESTION_TITLE.maxLength) {
      onTitleChange?.(value);
    }
  }}
  onInput={handleInput}
  data-placeholder={title ? '' : APPLICATION_FORM.QUESTION_TITLE.placeholder}
>
  {title}
</Styled.QuestionTitleText>

검증 요청:

  • 데스크톱(Chrome/Safari/Firefox)과 모바일에서 다음을 확인해 주세요.
    • 한글/일본어 IME 조합 시 입력 중간에 onTitleChange가 과도하게 호출되지 않는지.
    • 일반 타이핑/붙여넣기 시 즉시 반영되는지.
    • 최대 길이 초과 입력 후 삭제로 길이 이하가 되면 정상 반영되는지(현재 로직은 초과 시 저장만 막고 UI는 넘길 수 있습니다).
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear 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 9c2ed7b and 580ebff.

📒 Files selected for processing (1)
  • frontend/src/components/application/QuestionTitle/QuestionTitle.tsx (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/components/application/QuestionTitle/QuestionTitle.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/application/QuestionTitle/QuestionTitle.tsx

@lepitaaar lepitaaar merged commit a46485e into main Aug 19, 2025
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💻 FE Frontend 📈 release 릴리즈 배포

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

Comments