Skip to content

[release] FE v1.1.7#918

Merged
oesnuj merged 84 commits intomainfrom
develop-fe
Dec 16, 2025
Merged

[release] FE v1.1.7#918
oesnuj merged 84 commits intomainfrom
develop-fe

Conversation

@seongwon030
Copy link
Member

@seongwon030 seongwon030 commented Dec 7, 2025

#️⃣연관된 이슈

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

📝작업 내용

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

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

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

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

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

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

🫡 참고사항

Summary by CodeRabbit

릴리스 노트

새로운 기능

  • 관리자 페이지 네비게이션 추가
  • 지원서 삭제 기능 활성화
  • 지원서 상태 토글 기능 (공개/미공개)
  • 내부/외부 지원서 양식 모드 지원
  • 지원자 관리 탭 개선 및 확장 리스트 표시

버그 수정

  • 지원자 상세 페이지 네비게이션 경로 수정
  • 이미지 URL 처리 개선

성능 개선

  • 사진 목록에 이미지 지연 로딩 적용

✏️ Tip: You can customize this high-level summary in your review settings.

lepitaaar and others added 30 commits November 17, 2025 15:16
- applyHandlers.test.ts → applyHandlers.test.skip.ts로 변경
- 테스트 목록에서 제외
- 테스트의 예상값 Date 객체에 UTC 표시(Z) 추가
- parseRecruitmentPeriod.test.ts, recruitmentPeriodParser.test.ts 수정
- 로컬 타임존과 무관하게 테스트가 통과하도록 개선
[fix] Mixpanel 활성 호스트에 www 도메인 추가
…d-test-timezone-MOA-371

[test] 날짜 파싱 테스트 타임존 문제를 해결한다
- useTrackPageView 훅에 skip 파라미터추가하여 트래킹 제어 기능 구현
- ClubDetailPage에서 데이터 로딩 완료 시점까지 트래킹 지연 처리
- useEffect 의존성 배열에 clubName 추가하여 데이터 업데이트 반영
- clubName을 useRef로 관리하여 useEffect 의존성 문제 해결 및 최신 값 보장
- 트래킹 시작 시점(skip 해제 시)에 상태 초기화 로직 추가하여 정확도 향상
[fix] 카드 목록에 기본 이미지가 안불러 와지는 문제 수정
…king-MOA-386

[fix] 상세페이지에서 clubName이 undefined되는 문제를 해결한다
@seongwon030 seongwon030 self-assigned this Dec 7, 2025
@seongwon030 seongwon030 added 💻 FE Frontend 📈 release 릴리즈 배포 labels Dec 7, 2025
@vercel
Copy link

vercel bot commented Dec 7, 2025

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

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 7, 2025

Warning

Rate limit exceeded

@suhyun113 has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 17 minutes and 16 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 09acb46 and 3dcf13d.

📒 Files selected for processing (8)
  • frontend/src/components/common/Header/Header.styles.ts (2 hunks)
  • frontend/src/components/common/Modal/Modal.styles.ts (2 hunks)
  • frontend/src/components/common/ScrollToTopButton/ScrollToTopButton.styles.ts (1 hunks)
  • frontend/src/pages/AdminPage/auth/LoginTab/LoginTab.styles.ts (0 hunks)
  • frontend/src/pages/ClubDetailPage/components/BackNavigationBar/BackNavigationBar.styles.ts (1 hunks)
  • frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.styles.ts (1 hunks)
  • frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoModal/PhotoModal.styles.ts (2 hunks)
  • frontend/src/styles/zIndex.ts (1 hunks)

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.
  • You can also validate your configuration using the online YAML validator.
  • 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은 어플리케이션 폼 관리 시스템을 확장합니다. 삭제 기능을 활성화하고, API 메서드를 업데이트하며, 내부/외부 지원 양식 모드를 도입하고, 관리자 UI를 리팩토링하며, 공통 LazyImage 컴포넌트를 클럽 상세 페이지로 이전합니다.

Changes

Cohort / File(s) 변경 요약
Application API 활성화 및 업데이트
frontend/src/apis/application/deleteApplication.ts, frontend/src/apis/application/getApplication.ts, frontend/src/apis/application/updateApplication.ts
deleteApplication 구현 활성화, getApplication 반환 타입을 Promise로 명시, updateApplication을 PUT에서 PATCH로 변경, updateApplicationStatus 함수 추가
Application 타입 및 상수 확장
frontend/src/types/application.ts, frontend/src/constants/INITIAL_FORM_DATA.ts, frontend/src/mocks/data/mockData.ts
ApplicationFormMode (INTERNAL/EXTERNAL) 추가, ApplicationFormData에 formMode와 externalApplicationUrl 필드 추가, questions을 선택적으로 변경, status 값을 대문자로 변경
Hooks 및 유틸 업데이트
frontend/src/hooks/queries/application/useDeleteApplication.ts, frontend/src/hooks/useTrackPageView.ts, frontend/src/hooks/Header/useHeaderNavigation.ts
useDeleteApplication 구현 활성화, useTrackPageView에 skip 파라미터 추가, handleAdminClick 핸들러 추가
헤더 네비게이션 변경
frontend/src/components/common/Header/Header.tsx, frontend/src/constants/eventName.ts
패치노트 네비게이션을 관리자 페이지로 변경, ADMIN_BUTTON_CLICKED 이벤트 추가
LazyImage 컴포넌트 재배치
frontend/src/components/common/LazyImage/LazyImage.tsx (삭제), frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/* (신규)
공통 LazyImage 컴포넌트 제거, ClubDetailPage 하위에 새로운 LazyImage 구현 추가 (isEager, placeholder 지원), PhotoCardList에서 eager loading 적용
Admin Context 리팩토링
frontend/src/context/AdminClubContext.tsx
applicationFormId 및 setter 제거
Admin 라우팅 및 UI
frontend/src/pages/AdminPage/AdminRoutes.tsx, frontend/src/pages/AdminPage/tabs/ApplicantsTab/*, frontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationListTab.tsx
ApplicantsTab 라우트 추가, 중첩 라우트 application-list/:applicationFormId/edit 추가, 어플리케이션 목록 UI 리팩토링
Application 행 컴포넌트 신규
frontend/src/pages/AdminPage/components/ApplicationRow/ApplicationRowItem.tsx, frontend/src/pages/AdminPage/components/ApplicationRow/ApplicationRowItem.style.ts
새로운 ApplicationRowItem 컴포넌트 및 스타일 추가
ApplicationEditTab 확장
frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx, frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.styles.ts
내부/외부 양식 모드 지원, 외부 URL 입력 UI 추가, 모드별 제출 로직 구현
ApplicantsListTab 리팩토링
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsListTab/ApplicantsListTab.tsx, frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsListTab/ApplicantsListTab.styles.ts (삭제)
상태 토글 기능 추가, 확장 가능한 목록 구현, 삭제 확인 플로우 추가, ApplicationRowItem 사용
ApplicationListTab 업데이트
frontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationListTab.tsx, frontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationListTab.styles.ts, frontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationMenu.tsx
활성 양식 섹션 추가, 상태 토글 기능 구현, 메뉴 컴포넌트 props 업데이트
클럽 상세 페이지 업데이트
frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx, frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
외부 양식 모드 처리 추가, MobileWindowWidth 상수화, useTrackPageView 호출 업데이트
Application 폼 페이지
frontend/src/pages/ApplicationFormPage/ApplicationFormPage.tsx
formData.questions 가드 강화
UI 스타일 및 설정 업데이트
frontend/src/components/ClubLogo/ClubLogo.tsx, frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.styles.ts, frontend/src/utils/initSDK.ts, frontend/src/tsconfig.json
CSS url() 따옴표 추가, 미디어 쿼리 헬퍼 사용, PRODUCTION_HOSTNAMES 배열로 변경
테스트 및 유틸리티
frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.test.tsx, frontend/src/utils/parseRecruitmentPeriod.test.ts, frontend/src/utils/recruitmentPeriodParser.test.ts
LazyImage 테스트 업데이트, 날짜 파싱 테스트 UTC 형식으로 변경

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 분

주의가 필요한 영역:

  • ApplicationEditTab.tsx - 내부/외부 양식 모드 분기 로직, 페이로드 구성 및 검증 로직의 정확성 확인 필요
  • ApplicantsListTab.tsx - 상태 토글, 삭제 확인, 확장/축소 로직과 useQueryClient 활용의 정확성 확인
  • ApplicationListTab.tsx - ApplicationRowItem 통합, 활성 양식 섹션 렌더링 로직과 메뉴 토글 상태 관리 확인
  • AdminClubContext 제거 - 기존 코드에서 이 context를 사용하는 다른 부분이 없는지 확인 필요
  • API 타입 변경 - updateApplication의 PUT→PATCH 변경 및 status 값 대문자 변경이 백엔드와 일치하는지 확인
  • LazyImage 마이그레이션 - 공통 컴포넌트 제거 후 기존 사용처가 모두 새 경로로 업데이트되었는지 확인

Possibly related PRs

Suggested labels

📬 API, 🎨 UI, 🔧 Admin

Suggested reviewers

  • lepitaaar
  • oesnuj

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
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.
Title check ❓ Inconclusive PR 제목 '[release] v1.1.5'는 변경 사항의 주요 내용을 명확하게 설명하지 못합니다. 실제 PR에는 삭제된 컴포넌트, API 구현, 관리자 페이지 기능 추가 등 다양한 변경 사항이 포함되어 있지만, 제목은 단순히 버전 릴리스만 나타냅니다. 제목을 더 구체적으로 개선하거나, 릴리스 노트에 실제 변경 사항을 명확히 문서화하는 것이 좋습니다. 예: '[release] v1.1.5: 관리자 페이지 기능 추가 및 컴포넌트 리팩토링'
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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.

@seongwon030 seongwon030 changed the title [release] v1.1.5 [release] FE v1.1.7 Dec 7, 2025
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: 4

Caution

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

⚠️ Outside diff range comments (1)
frontend/src/hooks/useTrackPageView.ts (1)

45-56: visibilitychange 이벤트 리스너가 제대로 정리되지 않습니다.

visibilitychange 이벤트 리스너에 인라인 화살표 함수를 사용하고 있어서, cleanup 시 removeEventListener가 동일한 함수 참조를 가리키지 않아 리스너가 제거되지 않습니다. 이로 인해 메모리 누수와 중복 트래킹이 발생할 수 있습니다.

+   const handleVisibilityChange = () => {
+     if (document.hidden) {
+       trackPageDuration();
+     }
+   };
+
    window.addEventListener('beforeunload', trackPageDuration);
-   document.addEventListener('visibilitychange', () => {
-     if (document.hidden) {
-       trackPageDuration();
-     }
-   });
+   document.addEventListener('visibilitychange', handleVisibilityChange);

    return () => {
      trackPageDuration();
      window.removeEventListener('beforeunload', trackPageDuration);
-     document.removeEventListener('visibilitychange', trackPageDuration);
+     document.removeEventListener('visibilitychange', handleVisibilityChange);
    };
🧹 Nitpick comments (24)
frontend/src/utils/initSDK.ts (1)

5-16: Mixpanel 프로덕션 호스트 화이트리스트 로직 – SSR 안전성과 도메인 추가 시 관리 포인트 확인 제안

  • PRODUCTION_HOSTNAMES 상수로 허용 호스트를 관리하는 방식은 매직 스트링 제거 + 가독성 측면에서 좋습니다.
  • 다만 initializeMixpanel 내부에서 바로 window.location.hostname을 참조하고 있어서, 이 유틸이 SSR 환경(예: 서버 렌더링, 일부 테스트 환경 등)에서 호출되면 window가 없어 런타임 에러가 날 수 있습니다. 현재 완전히 클라이언트 전용으로만 호출된다는 전제가 아니라면, 아래처럼 가드를 추가하는 걸 권장합니다.
 export function initializeMixpanel() {
+  if (typeof window === 'undefined') {
+    return;
+  }
+
   if (import.meta.env.VITE_MIXPANEL_TOKEN) {
     mixpanel.init(import.meta.env.VITE_MIXPANEL_TOKEN, {
       ignore_dnt: true,
       debug: false,
     });
   }
 
   const isProductionHost = PRODUCTION_HOSTNAMES.includes(window.location.hostname);
   if (!isProductionHost) {
     mixpanel.disable();
   }
 }
  • 현재 화이트리스트에 moadong.com, www.moadong.com 두 개만 포함되어 있어서, 추후 app.moadong.com, beta.moadong.com 같은 새 프로덕션/서브도메인을 도입하면 Mixpanel 이벤트가 조용히 꺼질 수 있습니다.
    • 도메인이 늘어날 가능성이 있다면,
      • (1) 주석으로 “프로덕션 트래킹 허용 호스트 목록”임을 명시하거나
      • (2) 환경변수 기반 설정(예: VITE_MIXPANEL_ALLOWED_HOSTS)으로 빼서 릴리즈마다 재빌드 없이 조정할 수 있게 하는 방식을 한 번 고려해 보셔도 좋겠습니다.
frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.styles.ts (3)

10-23: 모바일 sticky 컨테이너 매직 넘버 및 레이아웃 영향 확인 제안

  • top: 56px;는 상단 헤더 높이와 직접 연결된 매직 넘버로 보이는데, 헤더 높이가 바뀌면 같이 깨질 여지가 있습니다. 공용 레이아웃 상수(예: HEADER_HEIGHT)가 있다면 그걸 참조하는 형태로 맞춰 두면 유지보수에 더 안전할 것 같습니다. (코딩 가이드라인의 매직 넘버 최소화 취지와도 맞습니다.)
  • margin: 0px -20px;, margin: -10px -10px;처럼 음수 마진으로 전체 가로폭을 채우는 패턴은 의도된 디자인 같지만, 특정 기기에서 수평 스크롤바가 생기지 않는지 한 번만 실제 디바이스/에뮬레이터에서 확인해 보시면 좋겠습니다.
  • 모바일 구간에서 position: stickyz-index: 1을 쓰고 있어서, 다른 상단 고정 컴포넌트(예: 글로벌 헤더, 플로팅 버튼 등)와 z-index 충돌이 없는지도 함께 체크해 주세요.

36-38: 모바일에서 버튼 padding 제거로 인한 탭 타겟 크기 확인 권장

데스크톱 기본 스타일에서 padding: 10px 0px;을 주다가 모바일 구간에서 padding: 0px;으로 바꾸면, 실제 탭 가능한 영역이 아이콘/텍스트 영역에 거의 밀착되어 줄어들 수 있습니다. 아이콘 높이를 줄이고 있긴 하지만, 모바일 접근성 측면에서 최소 탭 타겟 높이(보통 40–44px 이상)를 만족하는지 한 번 실제 UI에서 확인해 보시면 좋겠습니다.

문제 없다면 그대로 두셔도 되지만, 여유가 있다면 모바일에서도 약간의 상하 패딩을 남겨 두는 쪽을 고려해 볼 만합니다.


67-82: 텍스트 반응형 스타일 중복/매직 넘버 정리 여지

동작에는 문제가 없지만, 유지보수 관점에서 약간의 정리가 가능해 보입니다.

  • span의 기본 font-size가 이미 14px인데, tablet 구간에서도 다시 font-size: 14px;를 선언하고 있어 기능적으로는 중복입니다. 의도를 드러내기 위해 명시하신 것 아니라면 제거해도 될 것 같습니다.
  • mobile, mini_mobile 구간에서 line-height: normal;을 둘 다 지정하고 있는데, 상위(모바일)에서 한 번만 지정하고 미니 모바일에서는 font-size/margin-top만 덮어써도 됩니다.
  • 14/12/10px, margin-top 8/10/4/2px처럼 반복되는 숫자들은 디자인 토큰이나 상수(예: CATEGORY_LABEL_FONT_SIZE_MOBILE, CATEGORY_LABEL_MARGIN_TOP_MINI_MOBILE)로 빼 두면 의미 부여가 되어 나중에 조정하기 수월해집니다. (코딩 가이드라인 상 매직 넘버 정리 측면)

큰 변경은 아니라 후순위로 미뤄도 되지만, 다음 스타일 리팩터 때 한 번에 정리해 두시면 좋겠습니다.

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

1-23: 삭제 API 구현은 적절하나 에러 처리/타입을 조금 더 보강할 수 있습니다

현재 구현으로도 동작에는 문제가 없어 보이지만, 몇 가지 개선 여지가 있습니다.

  1. 에러 응답 JSON 파싱 실패 대비

    throw new Error((await response.json()).message);

    백엔드가 항상 JSON { message: string } 형태를 보장하지 않으면, 여기서 한 번 더 예외가 발생해 HTTP 상태 정보가 가려질 수 있습니다. 다음처럼 try/catch로 보완하면 디버깅이 더 수월합니다.

  • if (!response.ok) {
  •  console.error(`Failed to delete: ${response.statusText}`);
    
  •  throw new Error((await response.json()).message);
    
  • }
  • if (!response.ok) {
  •  console.error(`Failed to delete: ${response.status} ${response.statusText}`);
    
  •  let message = '지원서 삭제에 실패했습니다.';
    
  •  try {
    
  •    const errorBody = await response.json();
    
  •    if (errorBody?.message) message = errorBody.message;
    
  •  } catch {
    
  •    // JSON이 아닐 경우 statusText만 사용
    
  •    if (response.statusText) message = response.statusText;
    
  •  }
    
  •  throw new Error(message);
    
  • }

2. **반환 타입 명시**  
`return result.data;`를 사용하고 있으므로, `DeleteApplicationResponse` 같은 타입을 정의해 `Promise<DeleteApplicationResponse>` 형태로 명시해 두면 이후 사용처에서 타입 안전성이 높아집니다. (기존 update/get API와도 일관성이 생깁니다.)




Also applies to: 25-25

</blockquote></details>
<details>
<summary>frontend/src/apis/application/updateApplication.ts (1)</summary><blockquote>

`13-13`: **상태 토글 API의 파라미터/로직을 더 타입 안전하게 만드는 것이 좋겠습니다**

기능 자체는 정상 동작할 것으로 보이지만, 유지보수 관점에서 몇 가지 개선을 제안드립니다.

1. **`currentStatus` 타입 구체화 / boolean 기반으로 변경**  
```ts
export const updateApplicationStatus = async (
  applicationFormId: string,
  currentStatus: string,
) => {
  const newStatus = currentStatus === 'ACTIVE' ? false : true;
  • 단순 string으로 받으면 오타나 서버 스펙 변경에 취약합니다.
  • @/types/application에 상태 관련 타입(예: ApplicationFormStatus 또는 ApplicationFormActive 등)이 있다면 그 타입을 사용하거나,
  • 아예 currentActive: boolean을 인자로 받고 const newStatus = !currentActive; 형태로 만드는 편이 더 직관적입니다.
  1. 토글 로직 단순화
    현재 로직은 의미상 currentStatus !== 'ACTIVE'와 동일하므로, 아래처럼 표현하면 의도가 더 분명해집니다.

  • const newStatus = currentStatus === 'ACTIVE' ? false : true;
  • const newStatus = currentStatus !== 'ACTIVE';
    
    
  1. 공통 응답 타입 재사용
    updateApplicationupdateApplicationStatus 모두 result.data를 반환하므로, 공통 응답 타입을 정의해 두면 두 함수의 반환 타입을 일관되게 관리할 수 있습니다.

Also applies to: 33-58

frontend/src/pages/AdminPage/components/ApplicationRow/ApplicationRowItem.style.ts (1)

3-9: 미사용 인터페이스 정리 및 버튼 포커스 스타일 보완 제안

  1. 미사용 타입 정리

    interface MenuItemProps {
      $ActiveMenu?: boolean;
    }
    
    interface ExpandButtonProps {
      $isExpanded: boolean;
    }

    현재 파일 내에서는 사용되지 않고 있으니, 추후 사용 계획이 없다면 제거해 두는 편이 코드 가독성과 유지보수에 좋습니다.

-interface MenuItemProps {

  • $ActiveMenu?: boolean;
    -}

-interface ExpandButtonProps {

  • $isExpanded: boolean;
    -}
    +// TODO: 메뉴/확장 버튼 스타일에 prop 기반 분기가 필요해지면 다시 정의합니다.
    
    
  1. MoreButton 키보드 포커스 스타일
    MoreButton에 hover 스타일은 있지만 키보드 포커스(:focus-visible)에 대한 시각적 피드백이 없습니다. 접근성을 위해 아래처럼 최소한의 포커스 스타일을 추가하는 것을 권장합니다.

    export const MoreButton = styled.button`
      ...
      border-radius: 50%;
      &:hover {
        background-color: #DCDCDC;
      }
  • &:focus-visible {
  •  outline: 2px solid #ff5414;
    
  •  outline-offset: 2px;
    
  • }
    `;




Also applies to: 77-92

</blockquote></details>
<details>
<summary>frontend/src/pages/IntroducePage/constants/mockData.ts (1)</summary><blockquote>

`20-20`: **`recruitmentStatus: 'ALWAYS'` 값의 타입/도메인 일관성 확인 필요**

목업 데이터에 `recruitmentStatus: 'ALWAYS'`를 추가한 것은 의도된 새로운 상태로 보이는데,

- `Club['recruitmentStatus']` 타입이 `'ALWAYS'`를 포함하고 있는지,
- 다른 곳(상태 매핑 util, UI 표시, 필터 조건 등)에서 `'OPEN' | 'CLOSED'`만 가정하고 있지는 않은지

한 번만 같이 확인해 주시면 좋겠습니다. 가능하다면 문자열 리터럴 대신 공용 enum/상수(예: `RecruitmentStatus.ALWAYS`)를 사용하는 것도 오타를 줄이는 데 도움이 됩니다.

</blockquote></details>
<details>
<summary>frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (1)</summary><blockquote>

`5-5`: **URL 파라미터 처리와 훅 인자 전달 방식을 조금 더 단순/안전하게 만들 수 있습니다**

1. **훅 인자에서 `?? ''` 제거 권장**  
```ts
const { applicationFormId } = useParams<{ applicationFormId: string }>();
...
const {
  data: fetchData,
  isLoading,
  isError,
} = useGetApplicants(applicationFormId ?? '');
...
const { mutate: updateDetailApplicants } = useUpdateApplicant(
  applicationFormId ?? '',
);

useGetApplicants / useUpdateApplicant 두 훅 모두 시그니처가 string | undefined를 기대하고, 내부에서 enabled: !!applicationFormId 또는 null 체크를 하고 있으므로 그냥 applicationFormId를 그대로 넘겨도 충분합니다.

-  } = useGetApplicants(applicationFormId ?? '');
+  } = useGetApplicants(applicationFormId);
...
-  const { mutate: updateDetailApplicants } = useUpdateApplicant(
-    applicationFormId ?? '',
-  );
+  const { mutate: updateDetailApplicants } =
+    useUpdateApplicant(applicationFormId);

이렇게 하면 혹시라도 다른 곳에서 빈 문자열을 전달하는 실수를 예방하고, 타입 의도와도 더 잘 맞습니다.

  1. navigate 호출 전 안전 가드 추가 고려

    onClick={() =>
      navigate(
        `/admin/applicants-list/${applicationFormId}/${item.id}`,
      )
    }

    현재 라우팅 구조상 ApplicantsTab가 렌더링되면 applicationFormId가 항상 존재하는 것이 맞지만, 방어적으로 다음과 같이 가드를 두면 예외 상황에서 /admin/applicants-list/undefined/...로 이동하는 케이스를 막을 수 있습니다.

  •          <Styled.ApplicantTableRow
    
  •            key={index}
    
  •            onClick={() =>
    
  •              navigate(
    
  •                `/admin/applicants-list/${applicationFormId}/${item.id}`,
    
  •              )
    
  •            }
    
  •          <Styled.ApplicantTableRow
    
  •            key={index}
    
  •            onClick={() => {
    
  •              if (!applicationFormId) return;
    
  •              navigate(
    
  •                `/admin/applicants-list/${applicationFormId}/${item.id}`,
    
  •              );
    
  •            }}
             >
    
    
    
  1. (선택) 상태/필터/정렬 로직 분리
    이 탭 컴포넌트에 상태와 이벤트 로직이 상당히 많아서, 장기적으로는

    • 필터/정렬/선택 관련 로직을 커스텀 훅으로 분리하거나,
    • 테이블/헤더/요약 카드 등을 소형 컴포넌트로 쪼개는 것

    을 고려하면 가독성과 재사용성이 더 좋아질 것 같습니다. As per coding guidelines, ...

Also applies to: 18-19, 41-47, 76-78, 490-494

frontend/src/pages/AdminPage/AdminRoutes.tsx (1)

12-12: 신규 라우트 구성은 합리적으로 보이지만, edit 라우트 중복 여부를 한 번만 점검해 주세요

  • applicants-list/:applicationFormId 경로에서 ApplicantsTab를 렌더링하도록 바꾼 부분은, ApplicantsTabuseParamsapplicationFormId를 읽어 쓰는 현재 구조와 잘 맞습니다.
  • application-list/:applicationFormId/editapplication-list/edit 두 개의 edit 라우트가 공존하고 있는데,
    • 둘 다 실제로 사용되는지,
    • 하나는 과거 URL 호환용인지
      를 한 번만 정리해 두면 향후 라우팅 구조를 이해하는 데 도움이 될 것 같습니다. 불필요한 라우트라면 제거를 고려해 볼 수 있습니다.

또한 다른 탭들은 절대 경로 import(@/pages/...)를 쓰고, ApplicantsTab만 상대 경로(./tabs/...)를 쓰고 있어 코드 스타일 측면에서 통일해 두면 더 깔끔할 것 같습니다.

Also applies to: 24-27, 32-32

frontend/src/hooks/Header/useHeaderNavigation.ts (1)

39-42: 코드 포매팅 일관성 권장

기능적으로는 문제없이 구현되었습니다. 다만, 다른 핸들러들과의 일관성을 위해 화살표 함수 주변 공백을 맞추는 것을 권장합니다.

-  const handleAdminClick = useCallback(()=>{
+  const handleAdminClick = useCallback(() => {
     navigate('/admin');
     trackEvent(USER_EVENT.ADMIN_BUTTON_CLICKED);
-  },[navigate, trackEvent]);
+  }, [navigate, trackEvent]);
frontend/src/hooks/queries/application/useDeleteApplication.ts (1)

15-17: 사용자 피드백 및 에러 처리 개선 권장

현재 에러 발생 시 console.error만 사용하고 있어, 사용자에게 삭제 실패를 알리지 않습니다. 삭제 성공/실패 시 토스트 메시지나 알림을 추가하는 것을 권장합니다.

+    onSuccess: () => {
+      queryClient.invalidateQueries({
+        queryKey: ['applicationForm'],
+      });
+      // TODO: 성공 토스트 메시지 추가
+    },
     onError: (error) => {
       console.error(`Error delete application detail: ${error}`);
+      // TODO: 사용자에게 에러 피드백 제공 (토스트 또는 알림)
     },
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (1)

56-60: openByOption 함수 간소화를 고려해 주세요.

현재 openByOptiongoWithForm의 얇은 래퍼입니다. ApplicationSelectModalonSelect에 직접 인라인 처리하거나, goWithForm 내부에서 null 체크를 수행하는 방식으로 간소화할 수 있습니다.

-  // url 존재 시 외부, 내부 지원서 옵션에 따른 처리
-  const openByOption = (option?: ApplicationForm) => {
-    if (!option) return;
-    void goWithForm(option.id);
-  };

모달의 onSelect를 다음과 같이 변경:

onSelect={(option) => option && void goWithForm(option.id)}
frontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationMenu.tsx (2)

17-17: 개발용 console.log 제거 필요

onEditTitleconsole.log만 호출하는 stub으로 남아있습니다. 프로덕션 코드에서는 실제 구현을 추가하거나, 기능이 준비되지 않았다면 메뉴 항목을 숨기는 것이 좋습니다.


10-11: onToggleStatus가 optional이지만 항상 사용됨

onToggleStatus가 optional로 선언되었지만 Line 22에서 항상 onClick 핸들러로 사용됩니다. undefined일 경우 클릭 시 아무 동작도 하지 않아 사용자에게 혼란을 줄 수 있습니다.

실제 사용처(ApplicationRowItem.tsx)에서 항상 전달하고 있다면 required로 변경하는 것이 타입 안전성에 좋습니다.

 interface ApplicationMenuProps {
   isActive: boolean;
   onDelete: () => void;
-  onToggleStatus?: () => void;
+  onToggleStatus: () => void;
 }

Also applies to: 22-22

frontend/src/pages/AdminPage/components/ApplicationRow/ApplicationRowItem.tsx (2)

36-49: formatDateTime 함수를 컴포넌트 외부로 추출 권장

formatDateTime이 컴포넌트 내부에 정의되어 있어 매 렌더링마다 재생성됩니다. 외부 상태에 의존하지 않으므로 컴포넌트 외부로 추출하면 불필요한 함수 재생성을 방지할 수 있습니다.

+const formatDateTime = (dateTimeString: string) => {
+  const now = new Date();
+  const date = new Date(dateTimeString);
+  const isToday =
+    now.getFullYear() === date.getFullYear() &&
+    now.getMonth() === date.getMonth() &&
+    now.getDate() === date.getDate();
+  
+  const options: Intl.DateTimeFormatOptions = isToday
+    ? { hour: 'numeric', minute: '2-digit', hour12: true }
+    : { year: 'numeric', month: '2-digit', day: '2-digit' };
+
+  return date.toLocaleString('ko-KR', options);
+};
+
 const ApplicationRowItem = ({
   ...
 }: ApplicationRowItemProps) => {
-  const formatDateTime = (dateTimeString: string) => {
-    ...
-  };

52-52: 중복된 key prop

key={application.id}가 컴포넌트 내부에 설정되어 있지만, 부모 컴포넌트(ApplicationListTab.tsx)에서 이미 key={application.id}로 매핑하고 있습니다. 내부의 key는 제거해도 됩니다.

-    <Styled.ApplicationRow className={className} key={application.id}>
+    <Styled.ApplicationRow className={className}>
frontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationListTab.tsx (3)

44-56: 상태 변경 로직을 mutation hook으로 리팩터링 권장

handleToggleClick에서 직접 updateApplicationStatus API를 호출하고 수동으로 invalidateQueries를 실행하고 있습니다. 같은 파일에서 useDeleteApplication 훅은 useMutation 패턴을 사용하고 있어 일관성이 없습니다.

useUpdateApplicationStatus 훅을 생성하면 로딩 상태 관리, 에러 처리, 캐시 무효화가 자동으로 처리됩니다.

// hooks/queries/application/useUpdateApplicationStatus.ts
export const useUpdateApplicationStatus = () => {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: ({ id, status }: { id: string; status: string }) =>
      updateApplicationStatus(id, status),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['applicationForm'] });
    },
  });
};

21-22: 상수를 컴포넌트 외부로 이동

MAX_INITIAL_ITEMS가 컴포넌트 내부에 선언되어 있습니다. 코딩 가이드라인에 따라 상수는 관련 로직 근처 또는 파일 상단에 정의하는 것이 좋습니다.

+const MAX_INITIAL_ITEMS = 3;
+
 const ApplicationListTab = () => {
   const { data: allforms, isLoading, isError, error } = useGetApplicationlist();
   const queryClient = useQueryClient();
   const navigate = useNavigate();
   const { mutate: deleteApplication } = useDeleteApplication();
   const [isExpanded, setIsExpanded] = useState(false);
-  const MAX_INITIAL_ITEMS = 3;

125-125: 사소한 포맷팅: 연산자 주변 공백

activeForms.length >0에서 > 앞에 공백이 누락되었습니다.

-        {activeForms.length >0 ? (
+        {activeForms.length > 0 ? (
frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx (3)

126-148: URL 허용 목록을 상수로 추출하세요

코딩 가이드라인에 따라 매직 값(magic numbers/strings)은 명명된 상수로 추출해야 합니다. externalApplicationUrlAllowed 배열을 파일 상단이나 constants 파일로 이동하면 재사용성과 유지보수성이 향상됩니다.

파일 상단 또는 constants 파일에 추가:

const ALLOWED_EXTERNAL_FORM_URLS = [
  'https://forms.gle',
  'https://docs.google.com/forms',
  'https://form.naver.com',
  'https://naver.me',
] as const;

그 후 handleSubmit 내에서 해당 상수를 참조:

-      const externalApplicationUrlAllowed = [
-        'https://forms.gle',
-        'https://docs.google.com/forms',
-        'https://form.naver.com',
-        'https://naver.me',
-      ];
-
-      const isValidUrl = externalApplicationUrlAllowed.some((url) =>
+      const isValidUrl = ALLOWED_EXTERNAL_FORM_URLS.some((url) =>
         externalApplicationUrl.startsWith(url),
       );

219-346: InternalApplicationComponent를 별도 파일로 분리 고려

코딩 가이드라인에서 복잡한 로직/상호작용은 전용 컴포넌트로 추상화하고, 상당히 다른 조건부 UI/로직은 별도 컴포넌트로 분리하도록 권장합니다. 이 컴포넌트는 약 130줄로 자체적인 상태 핸들러와 렌더링 로직을 포함하고 있어 별도 파일로 추출하면 가독성과 테스트 용이성이 향상됩니다.


348-372: ExternalApplicationComponent도 별도 파일로 분리 고려

동일한 가이드라인에 따라, 이 컴포넌트도 별도 파일로 추출하면 모듈 구조가 개선됩니다. 다만, 현재 크기가 작으므로 선택적 개선 사항입니다.

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

25-108: 색상 값을 테마 변수나 상수로 추출 고려

#FF7543, #FF5414, #787878, #DCDCDC, #F2F2F2 등 여러 색상이 하드코딩되어 있습니다. 코딩 가이드라인에서 매직 값은 명명된 상수로 대체하도록 권장합니다. 프로젝트에 테마 시스템이 있다면 활용하거나, 최소한 색상 상수를 별도로 관리하면 일관성 유지에 도움이 됩니다.

예시:

// colors.ts 또는 theme.ts
export const COLORS = {
  primary: '#FF7543',
  primaryDark: '#FF5414',
  gray: '#787878',
  border: '#DCDCDC',
  backgroundHover: '#F2F2F2',
} as const;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f7c7411 and 09acb46.

⛔ Files ignored due to path filters (1)
  • frontend/src/assets/images/icons/ExpandArrow.svg is excluded by !**/*.svg
📒 Files selected for processing (40)
  • frontend/src/apis/application/deleteApplication.ts (1 hunks)
  • frontend/src/apis/application/getApplication.ts (1 hunks)
  • frontend/src/apis/application/updateApplication.ts (2 hunks)
  • frontend/src/components/ClubLogo/ClubLogo.tsx (1 hunks)
  • frontend/src/components/common/Header/Header.tsx (2 hunks)
  • frontend/src/components/common/LazyImage/LazyImage.tsx (0 hunks)
  • frontend/src/constants/INITIAL_FORM_DATA.ts (2 hunks)
  • frontend/src/constants/eventName.ts (1 hunks)
  • frontend/src/context/AdminClubContext.tsx (2 hunks)
  • frontend/src/hooks/Header/useHeaderNavigation.ts (2 hunks)
  • frontend/src/hooks/queries/application/useDeleteApplication.ts (1 hunks)
  • frontend/src/hooks/useTrackPageView.ts (3 hunks)
  • frontend/src/mocks/data/mockData.ts (2 hunks)
  • frontend/src/pages/AdminPage/AdminRoutes.tsx (2 hunks)
  • frontend/src/pages/AdminPage/components/ApplicationRow/ApplicationRowItem.style.ts (1 hunks)
  • frontend/src/pages/AdminPage/components/ApplicationRow/ApplicationRowItem.tsx (1 hunks)
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx (4 hunks)
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsListTab/ApplicantsListTab.styles.ts (0 hunks)
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsListTab/ApplicantsListTab.tsx (3 hunks)
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (6 hunks)
  • frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.styles.ts (3 hunks)
  • frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx (6 hunks)
  • frontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationListTab.styles.ts (5 hunks)
  • frontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationListTab.tsx (3 hunks)
  • frontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationMenu.tsx (1 hunks)
  • frontend/src/pages/ApplicationFormPage/ApplicationFormPage.tsx (4 hunks)
  • frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx (1 hunks)
  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (3 hunks)
  • frontend/src/pages/ClubDetailPage/components/ClubCard/ClubCard.tsx (2 hunks)
  • frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.styles.ts (1 hunks)
  • frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.test.tsx (2 hunks)
  • frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.tsx (1 hunks)
  • frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoCardList/PhotoCardList.tsx (3 hunks)
  • frontend/src/pages/IntroducePage/constants/mockData.ts (1 hunks)
  • frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.styles.ts (4 hunks)
  • frontend/src/types/application.ts (2 hunks)
  • frontend/src/utils/initSDK.ts (3 hunks)
  • frontend/src/utils/parseRecruitmentPeriod.test.ts (1 hunks)
  • frontend/src/utils/recruitmentPeriodParser.test.ts (1 hunks)
  • frontend/tsconfig.json (1 hunks)
💤 Files with no reviewable changes (2)
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsListTab/ApplicantsListTab.styles.ts
  • frontend/src/components/common/LazyImage/LazyImage.tsx
🧰 Additional context used
📓 Path-based instructions (3)
frontend/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (frontend/.cursorrules)

frontend/**/*.{ts,tsx,js,jsx}: Replace magic numbers with named constants for clarity
Replace complex/nested ternaries with if/else or IIFEs for readability
Assign complex boolean conditions to named variables for explicit meaning
Avoid hidden side effects; functions should only perform actions implied by their signature (Single Responsibility Principle)
Use unique and descriptive names for custom wrappers/functions to avoid ambiguity
Define constants near related logic or ensure names link them clearly to avoid silent failures
Break down broad state management into smaller, focused hooks/contexts to reduce coupling

Files:

  • frontend/src/utils/initSDK.ts
  • frontend/src/components/ClubLogo/ClubLogo.tsx
  • frontend/src/constants/eventName.ts
  • frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.styles.ts
  • frontend/src/apis/application/getApplication.ts
  • frontend/src/context/AdminClubContext.tsx
  • frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.tsx
  • frontend/src/constants/INITIAL_FORM_DATA.ts
  • frontend/src/pages/AdminPage/AdminRoutes.tsx
  • frontend/src/apis/application/updateApplication.ts
  • frontend/src/hooks/useTrackPageView.ts
  • frontend/src/pages/AdminPage/components/ApplicationRow/ApplicationRowItem.tsx
  • frontend/src/components/common/Header/Header.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx
  • frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsListTab/ApplicantsListTab.tsx
  • frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.styles.ts
  • frontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationListTab.tsx
  • frontend/src/pages/IntroducePage/constants/mockData.ts
  • frontend/src/utils/parseRecruitmentPeriod.test.ts
  • frontend/src/utils/recruitmentPeriodParser.test.ts
  • frontend/src/pages/AdminPage/components/ApplicationRow/ApplicationRowItem.style.ts
  • frontend/src/hooks/queries/application/useDeleteApplication.ts
  • frontend/src/pages/ApplicationFormPage/ApplicationFormPage.tsx
  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
  • frontend/src/mocks/data/mockData.ts
  • frontend/src/types/application.ts
  • frontend/src/pages/ClubDetailPage/components/ClubCard/ClubCard.tsx
  • frontend/src/hooks/Header/useHeaderNavigation.ts
  • frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.test.tsx
  • frontend/src/apis/application/deleteApplication.ts
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.styles.ts
  • frontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationMenu.tsx
  • frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoCardList/PhotoCardList.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationListTab.styles.ts
frontend/**/*.{ts,tsx}

📄 CodeRabbit inference engine (frontend/.cursorrules)

Use consistent return types for similar functions/hooks

Files:

  • frontend/src/utils/initSDK.ts
  • frontend/src/components/ClubLogo/ClubLogo.tsx
  • frontend/src/constants/eventName.ts
  • frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.styles.ts
  • frontend/src/apis/application/getApplication.ts
  • frontend/src/context/AdminClubContext.tsx
  • frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.tsx
  • frontend/src/constants/INITIAL_FORM_DATA.ts
  • frontend/src/pages/AdminPage/AdminRoutes.tsx
  • frontend/src/apis/application/updateApplication.ts
  • frontend/src/hooks/useTrackPageView.ts
  • frontend/src/pages/AdminPage/components/ApplicationRow/ApplicationRowItem.tsx
  • frontend/src/components/common/Header/Header.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx
  • frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsListTab/ApplicantsListTab.tsx
  • frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.styles.ts
  • frontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationListTab.tsx
  • frontend/src/pages/IntroducePage/constants/mockData.ts
  • frontend/src/utils/parseRecruitmentPeriod.test.ts
  • frontend/src/utils/recruitmentPeriodParser.test.ts
  • frontend/src/pages/AdminPage/components/ApplicationRow/ApplicationRowItem.style.ts
  • frontend/src/hooks/queries/application/useDeleteApplication.ts
  • frontend/src/pages/ApplicationFormPage/ApplicationFormPage.tsx
  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
  • frontend/src/mocks/data/mockData.ts
  • frontend/src/types/application.ts
  • frontend/src/pages/ClubDetailPage/components/ClubCard/ClubCard.tsx
  • frontend/src/hooks/Header/useHeaderNavigation.ts
  • frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.test.tsx
  • frontend/src/apis/application/deleteApplication.ts
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.styles.ts
  • frontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationMenu.tsx
  • frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoCardList/PhotoCardList.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationListTab.styles.ts
frontend/**/*.{tsx,jsx}

📄 CodeRabbit inference engine (frontend/.cursorrules)

frontend/**/*.{tsx,jsx}: Abstract complex logic/interactions into dedicated 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 when using form libraries like react-hook-form
Use Component Composition instead of Props Drilling to reduce coupling

Files:

  • frontend/src/components/ClubLogo/ClubLogo.tsx
  • frontend/src/context/AdminClubContext.tsx
  • frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.tsx
  • frontend/src/pages/AdminPage/AdminRoutes.tsx
  • frontend/src/pages/AdminPage/components/ApplicationRow/ApplicationRowItem.tsx
  • frontend/src/components/common/Header/Header.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx
  • frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsListTab/ApplicantsListTab.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationListTab.tsx
  • frontend/src/pages/ApplicationFormPage/ApplicationFormPage.tsx
  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
  • frontend/src/pages/ClubDetailPage/components/ClubCard/ClubCard.tsx
  • frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.test.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationMenu.tsx
  • frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoCardList/PhotoCardList.tsx
🧠 Learnings (12)
📓 Common learnings
Learnt from: seongwon030
Repo: Moadong/moadong PR: 195
File: frontend/src/pages/AdminPage/AdminPage.tsx:7-7
Timestamp: 2025-03-19T05:18:07.818Z
Learning: AdminPage.tsx에서 현재 하드코딩된 클럽 ID('67d2e3b9b15c136c6acbf20b')는 로그인 기능 구현 후 동적으로 가져오는 방식으로 수정될 예정입니다.
📚 Learning: 2025-11-25T14:08:23.253Z
Learnt from: CR
Repo: Moadong/moadong PR: 0
File: frontend/.cursorrules:0-0
Timestamp: 2025-11-25T14:08:23.253Z
Learning: Applies to frontend/**/*.{ts,tsx,js,jsx} : Define constants near related logic or ensure names link them clearly to avoid silent failures

Applied to files:

  • frontend/src/utils/initSDK.ts
📚 Learning: 2025-11-25T14:08:23.253Z
Learnt from: CR
Repo: Moadong/moadong PR: 0
File: frontend/.cursorrules:0-0
Timestamp: 2025-11-25T14:08:23.253Z
Learning: Applies to frontend/**/*.{ts,tsx,js,jsx} : Break down broad state management into smaller, focused hooks/contexts to reduce coupling

Applied to files:

  • frontend/src/context/AdminClubContext.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsListTab/ApplicantsListTab.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationListTab.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationListTab.styles.ts
📚 Learning: 2025-03-19T05:18:07.818Z
Learnt from: seongwon030
Repo: Moadong/moadong PR: 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/ClubDetailPage.tsx
  • frontend/src/pages/ApplicationFormPage/ApplicationFormPage.tsx
  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
  • frontend/src/pages/ClubDetailPage/components/ClubCard/ClubCard.tsx
📚 Learning: 2025-11-25T14:08:23.253Z
Learnt from: CR
Repo: Moadong/moadong PR: 0
File: frontend/.cursorrules:0-0
Timestamp: 2025-11-25T14:08:23.253Z
Learning: Applies to frontend/**/*.{tsx,jsx} : Separate significantly different conditional UI/logic into distinct components

Applied to files:

  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsListTab/ApplicantsListTab.tsx
  • frontend/src/pages/AdminPage/components/ApplicationRow/ApplicationRowItem.style.ts
  • frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.styles.ts
  • frontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationListTab.styles.ts
📚 Learning: 2025-11-25T14:08:23.253Z
Learnt from: CR
Repo: Moadong/moadong PR: 0
File: frontend/.cursorrules:0-0
Timestamp: 2025-11-25T14:08:23.253Z
Learning: Applies to frontend/**/*.{ts,tsx,js,jsx} : Replace magic numbers with named constants for clarity

Applied to files:

  • frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.styles.ts
  • frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.styles.ts
  • frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoCardList/PhotoCardList.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationListTab.styles.ts
📚 Learning: 2025-11-25T14:08:23.253Z
Learnt from: CR
Repo: Moadong/moadong PR: 0
File: frontend/.cursorrules:0-0
Timestamp: 2025-11-25T14:08:23.253Z
Learning: Applies to frontend/**/*.{tsx,jsx} : Choose field-level or form-level cohesion based on form requirements when using form libraries like react-hook-form

Applied to files:

  • frontend/src/pages/ApplicationFormPage/ApplicationFormPage.tsx
  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.styles.ts
📚 Learning: 2025-11-25T14:08:23.253Z
Learnt from: CR
Repo: Moadong/moadong PR: 0
File: frontend/.cursorrules:0-0
Timestamp: 2025-11-25T14:08:23.253Z
Learning: Applies to frontend/**/*.{tsx,jsx} : Abstract complex logic/interactions into dedicated components/HOCs

Applied to files:

  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx
  • frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.styles.ts
📚 Learning: 2025-09-21T02:23:27.796Z
Learnt from: seongwon030
Repo: Moadong/moadong PR: 744
File: frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx:47-48
Timestamp: 2025-09-21T02:23:27.796Z
Learning: ClubApplyButton 컴포넌트에서 ShareButton은 항상 렌더링되어야 하므로 정적 import를 사용하는 것이 적절함. 동적 import는 불필요함.

Applied to files:

  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
📚 Learning: 2025-09-30T05:26:41.788Z
Learnt from: alsdddk
Repo: Moadong/moadong PR: 765
File: backend/src/main/java/moadong/club/service/ClubApplyService.java:431-435
Timestamp: 2025-09-30T05:26:41.788Z
Learning: In the Moadong codebase's club application feature (backend/src/main/java/moadong/club/), multiple ClubApplicationForm entities can have ACTIVE status for the same clubId, semesterYear, and semesterTerm simultaneously. There is no uniqueness constraint requiring only one ACTIVE form per semester.

Applied to files:

  • frontend/src/mocks/data/mockData.ts
  • frontend/src/types/application.ts
📚 Learning: 2025-07-19T05:05:10.196Z
Learnt from: seongwon030
Repo: Moadong/moadong PR: 548
File: frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx:17-57
Timestamp: 2025-07-19T05:05:10.196Z
Learning: ClubDetailPage.tsx에서 notJoinedClubNames 배열의 하드코딩은 의도적인 설계 결정입니다. 개발자가 명시적으로 하드코딩을 선택했으므로 이에 대한 리팩토링 제안을 하지 않아야 합니다.

Applied to files:

  • frontend/src/pages/ClubDetailPage/components/ClubCard/ClubCard.tsx
📚 Learning: 2025-08-25T14:43:52.320Z
Learnt from: lepitaaar
Repo: Moadong/moadong PR: 703
File: backend/src/main/java/moadong/club/controller/ClubApplyController.java:84-84
Timestamp: 2025-08-25T14:43:52.320Z
Learning: In the Moadong codebase, questionId and clubId are equivalent identifiers that represent the same entity. The ClubApplicationRepository.findAllByIdInAndQuestionId method correctly uses clubId as the questionId parameter for filtering club applications.

Applied to files:

  • frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx
🧬 Code graph analysis (16)
frontend/src/apis/application/getApplication.ts (1)
frontend/src/types/application.ts (1)
  • ApplicationFormData (49-58)
frontend/src/context/AdminClubContext.tsx (1)
frontend/src/types/applicants.ts (1)
  • ApplicantsInfo (10-16)
frontend/src/constants/INITIAL_FORM_DATA.ts (1)
frontend/src/types/application.ts (2)
  • ApplicationFormMode (83-86)
  • ApplicationFormMode (88-89)
frontend/src/apis/application/updateApplication.ts (1)
frontend/src/apis/auth/secureFetch.ts (1)
  • secureFetch (3-41)
frontend/src/pages/AdminPage/components/ApplicationRow/ApplicationRowItem.tsx (1)
frontend/src/types/application.ts (1)
  • ApplicationFormItem (70-75)
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx (3)
frontend/src/context/AdminClubContext.tsx (1)
  • useAdminClubContext (39-46)
frontend/src/hooks/queries/applicants/useGetApplicants.ts (1)
  • useGetApplicants (4-11)
frontend/src/hooks/queries/applicants/useUpdateApplicant.ts (1)
  • useUpdateApplicant (5-24)
frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx (2)
frontend/src/hooks/queries/club/useGetClubDetail.ts (1)
  • useGetClubDetail (6-20)
frontend/src/constants/eventName.ts (1)
  • PAGE_VIEW (75-89)
frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.styles.ts (1)
frontend/src/styles/mediaQuery.ts (1)
  • media (8-14)
frontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationListTab.tsx (4)
frontend/src/hooks/queries/application/useGetApplicationlist.ts (1)
  • useGetApplicationlist (4-10)
frontend/src/hooks/queries/application/useDeleteApplication.ts (1)
  • useDeleteApplication (4-19)
frontend/src/apis/application/updateApplication.ts (1)
  • updateApplicationStatus (33-58)
frontend/src/types/application.ts (2)
  • ApplicationFormItem (70-75)
  • SemesterGroup (77-81)
frontend/src/utils/recruitmentPeriodParser.test.ts (1)
frontend/src/utils/recruitmentPeriodParser.ts (1)
  • parseRecruitmentDateString (3-17)
frontend/src/pages/ApplicationFormPage/ApplicationFormPage.tsx (1)
frontend/src/hooks/useValidateAnswers.ts (1)
  • validateAnswers (3-14)
frontend/src/mocks/data/mockData.ts (2)
frontend/src/types/application.ts (3)
  • ApplicationFormMode (83-86)
  • ApplicationFormMode (88-89)
  • ApplicationForm (65-68)
frontend/src/mocks/constants/clubApi.ts (4)
  • CLUB_BOB (6-6)
  • CLUB_IVF (5-5)
  • CLUB_BACK (7-7)
  • CLUB_TEST (8-8)
frontend/src/types/application.ts (1)
frontend/src/mocks/data/mockData.ts (1)
  • Question (21-28)
frontend/src/hooks/Header/useHeaderNavigation.ts (1)
frontend/src/constants/eventName.ts (1)
  • USER_EVENT (1-31)
frontend/src/apis/application/deleteApplication.ts (1)
frontend/src/apis/auth/secureFetch.ts (1)
  • secureFetch (3-41)
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx (4)
frontend/src/context/AdminClubContext.tsx (1)
  • useAdminClubContext (39-46)
frontend/src/hooks/queries/applicants/useUpdateApplicant.ts (1)
  • useUpdateApplicant (5-24)
frontend/src/types/application.ts (1)
  • Question (5-14)
frontend/src/mocks/data/mockData.ts (1)
  • Question (21-28)
🔇 Additional comments (42)
frontend/tsconfig.json (1)

16-32: 포맷팅 변경사항 확인됨.

설정 파일의 가독성 개선을 위한 포맷팅 변경입니다. 기능적 변화는 없습니다.

frontend/src/components/ClubLogo/ClubLogo.tsx (1)

32-32: CSS URL 인용 부호 추가 - 좋은 개선사항입니다.

url() 함수 내부의 이미지 경로를 인용 부호로 감싼 것은 CSS 모범 사례를 따르는 변경입니다. 이를 통해 특수 문자나 공백이 포함된 URL을 더 안전하게 처리할 수 있습니다.

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

59-59: EOF 변경(중괄호 라인)으로 인한 동작 영향 없음

Line 59의 변경은 initializeKakaoSDK 종료 중괄호/개행 처리 수준으로 보이며, 함수 로직에는 영향이 없어 보입니다. 이대로 두셔도 무방해 보입니다.

frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.styles.ts (2)

2-2: 공통 media 헬퍼 도입 방향 👍

@/styles/mediaQuery에서 제공하는 media 객체를 가져와 사용하는 방식이 다른 스타일 파일들과도 일관적이고, 반응형 룰을 중앙집중식으로 관리하는 방향으로 잘 맞습니다. 별도 수정 없이 그대로 유지해도 될 것 같습니다.


49-56: 아이콘 이미지 반응형 사이즈 조절은 구조적으로 적절함

img에 대해 기본 56px → 모바일 45px → 미니 모바일 40px으로 점진적으로 축소하는 구조가 max-width 미디어쿼리 특성과 선언 순서(모바일 후 미니 모바일)와 잘 맞습니다. 작은 뷰포트일수록 더 작은 사이즈가 우선 적용되므로, 동작 측면에서는 문제 없어 보입니다.

frontend/src/utils/parseRecruitmentPeriod.test.ts (1)

8-9: UTC 기반 기대값으로 테스트 일관성 확보

parseRecruitmentPeriod가 UTC 기준 Date 객체를 반환하는 구현과 맞춰 기대값을 new Date('...Z')로 변경한 부분 좋아 보입니다. 브라우저/런타임별 로컬 타임존 차이로 인한 테스트 흔들림을 줄여줄 것 같습니다.

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

6-6: UTC Date 기대값 통일로 타임존 이슈 방지

두 테스트 모두 기대값을 new Date('...Z') 형태로 맞춘 것은, 실제 파서가 UTC 기준으로 파싱하는 동작과 일치해서 타임존 환경에 따른 오차를 줄여줍니다. 변경 그대로 유지해도 무방해 보입니다.

Also applies to: 12-12

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

11-11: LGTM!

새로운 이벤트 상수가 네비게이션 섹션에 적절하게 추가되었고, 기존 명명 규칙을 따르고 있습니다.

frontend/src/mocks/data/mockData.ts (2)

1-5: LGTM!

ApplicationFormMode 타입이 적절하게 import되었고, mock 데이터에서 새로운 폼 모드 기능을 지원하기 위해 올바르게 사용되고 있습니다.


130-131: LGTM!

새로운 formModeexternalApplicationUrl 필드가 INITIAL_FORM_DATA.ts의 기본값과 일관성 있게 추가되었습니다.

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

17-37: LGTM!

포매팅 변경만 있으며, 클릭 핸들러와 네비게이션 로직이 올바르게 유지되고 있습니다.

frontend/src/hooks/queries/application/useDeleteApplication.ts (1)

4-18: 삭제 mutation 구현 확인

삭제 기능이 올바르게 구현되었으며, deleteApplication API 엔드포인트는 적절한 에러 처리와 함께 DELETE 요청을 정확히 수행합니다. mutationFnonSuccess 콜백이 표준 react-query 패턴을 따르고 있으며, 다른 mutation 훅들과 일관된 반환 타입을 유지합니다. Query invalidation을 통해 UI가 적절히 업데이트됩니다.

frontend/src/constants/INITIAL_FORM_DATA.ts (2)

7-7: 학기 계산 로직 변경 확인이 필요합니다.

비교 연산자가 <에서 <=로 변경되어 6월(currentMonth === 5)이 1학기에 포함됩니다. 이전에는 15월이 1학기, 612월이 2학기였으나, 이제는 16월이 1학기, 712월이 2학기가 됩니다. 이 변경이 의도된 것인지 확인해주세요.


41-42: 새로운 폼 모드 필드가 올바르게 추가되었습니다.

formModeexternalApplicationUrl 필드가 타입 정의와 일치하게 추가되었으며, 초기값도 적절합니다.

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

1-32: 타입 안정성이 개선되었습니다.

명시적인 ApplicationFormData 반환 타입 추가와 멀티라인 포맷팅으로 코드 가독성과 타입 안정성이 향상되었습니다.

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

65-65: 선택적 questions 필드를 올바르게 처리합니다.

formData.questions가 선택적 필드로 변경됨에 따라 렌더 가드가 적절하게 추가되었습니다. 이후 코드에서 사용되는 non-null assertion은 이 가드 덕분에 안전합니다.

frontend/src/context/AdminClubContext.tsx (1)

4-37: 컨텍스트 단순화로 결합도가 감소했습니다.

applicationFormId를 컨텍스트에서 제거하고 라우트 파라미터로 관리하는 방식으로 변경하여, 컨텍스트의 책임이 줄어들고 결합도가 낮아졌습니다. 코딩 가이드라인에 따라 상태 관리를 더 작고 집중된 단위로 분리한 좋은 개선입니다.

Based on learnings, 상태 관리를 작은 단위로 분리하여 결합도를 줄이는 것이 권장됩니다.

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

35-38: 라우트 파라미터 기반 아키텍처로 올바르게 마이그레이션되었습니다.

applicationFormId를 컨텍스트 대신 라우트 파라미터에서 가져오도록 변경하여, 데이터 소스가 일관되고 명확해졌습니다.


153-155: 내비게이션 경로 오타가 수정되었습니다.

/admin/applicants-lsit/에서 /admin/applicants-list/로 경로 오타가 수정되어 정상적인 내비게이션이 가능해졌습니다.


195-195: 선택적 체이닝으로 안전한 접근을 보장합니다.

formData.questions?.map으로 변경하여 questions가 undefined일 때 안전하게 처리됩니다.

frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoCardList/PhotoCardList.tsx (2)

11-11: 매직 넘버를 명명된 상수로 대체했습니다.

IMAGE_EAGER_LOADING_COUNT 상수를 사용하여 첫 4개 이미지의 즉시 로딩 임계값을 명확하게 표현했습니다. 코딩 가이드라인에 따른 좋은 개선입니다.

As per coding guidelines, 매직 넘버는 명명된 상수로 대체하여 명확성을 높여야 합니다.


28-28: 성능 최적화를 위한 즉시 로딩 로직이 추가되었습니다.

첫 4개 이미지를 즉시 로딩하여 초기 페이지 로드 성능이 개선됩니다.

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

26-26: 헤더 내비게이션이 관리자 페이지로 업데이트되었습니다.

패치노트 항목이 관리자 페이지로 교체되었으며, 이전 항목은 주석으로 보존되어 있습니다. 핸들러와 경로가 일관되게 업데이트되었습니다.

Also applies to: 36-37

frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.test.tsx (3)

22-24: 테스트 코드에 유용한 JSDoc 주석이 추가되었습니다.

MockIntersectionObserver의 역할을 명확히 설명하는 주석이 추가되어 테스트 코드의 이해도가 향상되었습니다.


57-75: 비동기 테스트 패턴이 개선되었습니다.

페이크 타이머를 제거하고 실제 비동기 흐름과 waitFor를 사용하여 더 견고하고 유지보수하기 쉬운 테스트 패턴으로 개선되었습니다.


77-95: 비동기 DOM 쿼리로 테스트 안정성이 향상되었습니다.

findByRole을 사용하여 이미지 요소가 렌더링될 때까지 기다린 후 에러 이벤트를 디스패치하도록 변경하여, 타이밍 이슈를 방지하고 테스트의 안정성이 높아졌습니다.

frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.styles.ts (1)

1-18: LGTM!

styled-components 모범 사례를 잘 따르고 있습니다. transient props ($isLoaded, $placeholder)를 사용하여 DOM으로 props가 전달되는 것을 방지하고, 일관된 0.3s 트랜지션으로 부드러운 로딩 효과를 구현했습니다.

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

38-45: 외부 폼 URL이 비어있을 때 내부 폼으로 폴백되는 동작을 확인해 주세요.

formModeEXTERNAL이지만 externalApplicationUrl이 비어있거나 공백만 있는 경우, 코드가 내부 지원서 경로로 이동합니다. 이것이 의도된 동작인지 확인이 필요합니다. 의도하지 않은 동작이라면 사용자에게 에러 메시지를 표시하는 것이 더 적절할 수 있습니다.

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

19-19: LGTM!

매직 넘버 500을 MobileWindowWidth 상수로 추출하여 코드의 가독성과 유지보수성을 향상시켰습니다. 코딩 가이드라인을 잘 따르고 있습니다.


37-37: LGTM!

!clubDetail을 skip 파라미터로 전달하여 데이터 로딩 전 불필요한 트래킹을 방지합니다. clubName이 undefined인 상태에서의 트래킹 문제를 해결하는 좋은 접근입니다.

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

78-120: LGTM!

ApplicationFormChangeButton의 조건부 스타일링이 transient prop ($active)과 css 헬퍼를 사용하여 깔끔하게 구현되었습니다. CSS 변수를 활용한 테마 적용도 일관성 있게 적용되어 있습니다.


122-163: LGTM!

외부 지원서 폼 관련 스타일 컴포넌트들이 일관된 패턴으로 잘 구현되었습니다. CSS 변수 사용과 placeholder 스타일링이 적절합니다.

frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.tsx (1)

1-64: 잘 구현된 LazyImage 컴포넌트입니다.

IntersectionObserver를 활용한 지연 로딩과 eager 모드 지원이 잘 구현되어 있습니다. cleanup 함수에서 observer.disconnect()를 호출하여 메모리 누수를 방지한 점이 좋습니다.

한 가지 참고 사항: Line 56의 loading 속성은 이미 IntersectionObserver로 visibility를 제어하고 있어 실질적으로 중복됩니다. isVisible이 false일 때 <img>가 렌더링되지 않으므로, 브라우저의 native lazy loading은 작동하지 않습니다. 기능적 문제는 아니지만 코드 명확성을 위해 제거해도 무방합니다.

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

83-89: as const 패턴이 적절하게 사용되었습니다.

동일한 이름으로 상수와 타입을 정의하는 패턴은 TypeScript에서 enum-like 동작을 구현하는 관용적인 방식입니다.


55-57: The active and status fields use different casing intentionally—they represent different API contracts. ApplicationFormData.active (lowercase) is the request payload format, while ApplicationFormItem.status (uppercase) is the response format. This pattern correctly reflects the backend API's expectations and does not require changes.

Likely an incorrect or invalid review comment.

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

160-209: 전체적인 구조와 모드 전환 로직 LGTM

내부/외부 모드 전환 UI와 조건부 렌더링이 명확하게 구현되어 있습니다. 버튼 컴포넌트 활용과 $active prop을 통한 시각적 피드백도 적절합니다.

frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsListTab/ApplicantsListTab.tsx (3)

30-41: 삭제 확인 및 에러 처리 로직 LGTM

window.confirm을 통한 사용자 확인과 onSuccess 콜백에서의 알림 처리가 적절합니다. 다만, onError 핸들러 추가를 고려해보세요.

선택적으로 에러 처리 추가:

      deleteApplication(applicationFormId, {
        onSuccess: () => {
          setOpenMenuId(null);
          alert('삭제되었습니다.');
        },
+       onError: (error) => {
+         console.error('삭제 실패:', error);
+         alert('삭제에 실패했습니다.');
+       },
      });

163-192: 학기별 그룹 렌더링 로직 LGTM

groupUniqueKeyPrefix를 사용하여 각 그룹 내 메뉴 상태를 고유하게 관리하는 접근 방식이 좋습니다. ApplicationRowItem 컴포넌트로의 추상화도 적절합니다.


43-55: 쿼리 키 사용이 의도적이며 올바릅니다. React Query의 부분 키 매칭(partial key matching) 기능을 활용하고 있습니다: ['applicationForm']으로 무효화하면 ['applicationForm'] (전체 목록)과 ['applicationForm', clubId, formId] (상세 정보) 모두가 자동으로 무효화됩니다. 상태 변경 시 관련된 모든 쿼리를 한 번에 갱신하는 정확한 패턴입니다.

frontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationListTab.styles.ts (3)

3-9: 새로운 인터페이스 정의 LGTM

$ActiveMenu$isExpanded에 transient props($ 접두사)를 사용하여 DOM으로 전달되지 않도록 올바르게 처리했습니다. styled-components 모범 사례를 잘 따르고 있습니다.


70-76: 조건부 스타일링 구현 LGTM

$isExpanded prop에 따른 화살표 회전 애니메이션이 css 헬퍼를 사용하여 깔끔하게 구현되었습니다.


192-218: MenuItem의 조건부 스타일링 LGTM

$ActiveMenu prop을 통한 활성 상태 스타일링이 적절하게 구현되었습니다.

Comment on lines +105 to +115
const ActiveListBody = styled(Styled.ApplicationList)`
border-top-left-radius: 0;
`;
const ActiveApplicationRow = styled(ApplicationRowItem)`
&:hover {
background-color: #f8f9fa;
&:first-child {
border-top-right-radius: 20px;
}
}
};
`;
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 | 🟠 Major

styled-components를 렌더 함수 내부에 정의하면 성능 문제 발생

ActiveListBodyActiveApplicationRow가 컴포넌트 함수 내부에서 정의되어 있습니다. 이렇게 하면 매 렌더마다 새로운 스타일드 컴포넌트가 생성되어 불필요한 리렌더링과 스타일 재계산이 발생합니다.

파일 상단(컴포넌트 함수 외부) 또는 styles 파일로 이동하세요:

+import styled from 'styled-components';
+import ApplicationRowItem from '@/pages/AdminPage/components/ApplicationRow/ApplicationRowItem';

+const ActiveListBody = styled(Styled.ApplicationList)`
+  border-top-left-radius: 0;
+`;
+
+const ActiveApplicationRow = styled(ApplicationRowItem)`
+  &:hover {
+    background-color: #f8f9fa;
+    &:first-child {
+      border-top-right-radius: 20px;
+    }
+  }
+`;

const ApplicationListTab = () => {
  // ... component body without styled definitions
-  const ActiveListBody = styled(Styled.ApplicationList)`
-    border-top-left-radius: 0;
-  `;
-  const ActiveApplicationRow = styled(ApplicationRowItem)`
-    &:hover {
-      background-color: #f8f9fa;
-      &:first-child {
-        border-top-right-radius: 20px;
-      }
-    }
-  `;
📝 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 ActiveListBody = styled(Styled.ApplicationList)`
border-top-left-radius: 0;
`;
const ActiveApplicationRow = styled(ApplicationRowItem)`
&:hover {
background-color: #f8f9fa;
&:first-child {
border-top-right-radius: 20px;
}
}
};
`;
// Component body without styled-component definitions
// The styled components are now defined outside at module level
return (
// ... rest of component implementation using ActiveListBody and ActiveApplicationRow
);
🤖 Prompt for AI Agents
In
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsListTab/ApplicantsListTab.tsx
around lines 105 to 115, the styled-components ActiveListBody and
ActiveApplicationRow are declared inside the component function which recreates
them on every render; move these styled definitions out of the component (to the
top of this file or into a dedicated styles file), update imports/exports if
moved, and if they depend on props/state capture those values via props passed
into the component rather than closing over local variables so the styled
components can be defined once at module scope.

Comment on lines +54 to +59
const currentQuestions =
questions.length > 0 ? questions : INITIAL_FORM_DATA.questions!;

setApplicationFormMode(formMode);
setExternalApplicationUrl(externalApplicationUrl);
setNextId(Math.max(...currentQuestions.map((q) => q.id)) + 1);
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 | 🟡 Minor

빈 배열에 대한 Math.max 호출 시 -Infinity 반환 가능

currentQuestions가 빈 배열일 경우 Math.max(...[])-Infinity를 반환하여 nextId가 예상치 못한 값이 됩니다. INITIAL_FORM_DATA.questions에 항상 요소가 있다고 가정하지만, 방어적 코딩이 권장됩니다.

-    setNextId(Math.max(...currentQuestions.map((q) => q.id)) + 1);
+    const maxId = currentQuestions.length > 0 
+      ? Math.max(...currentQuestions.map((q) => q.id)) 
+      : 0;
+    setNextId(maxId + 1);
📝 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 currentQuestions =
questions.length > 0 ? questions : INITIAL_FORM_DATA.questions!;
setApplicationFormMode(formMode);
setExternalApplicationUrl(externalApplicationUrl);
setNextId(Math.max(...currentQuestions.map((q) => q.id)) + 1);
const currentQuestions =
questions.length > 0 ? questions : INITIAL_FORM_DATA.questions!;
setApplicationFormMode(formMode);
setExternalApplicationUrl(externalApplicationUrl);
const maxId = currentQuestions.length > 0
? Math.max(...currentQuestions.map((q) => q.id))
: 0;
setNextId(maxId + 1);
🤖 Prompt for AI Agents
In frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx
around lines 54 to 59, computing nextId uses Math.max(...currentQuestions.map(q
=> q.id)) which returns -Infinity for an empty array; update the logic to
defensively handle empty currentQuestions (e.g., compute maxId =
currentQuestions.length ? Math.max(...currentQuestions.map(q => q.id)) : 0) and
then call setNextId(maxId + 1) so nextId is a sensible positive integer when
there are no questions.

Comment on lines +106 to +116
const ActiveListBody = styled(Styled.ApplicationList)`
border-top-left-radius: 0;
`;
const ActiveApplicationRow = styled(ApplicationRowItem)`
&:hover {
background-color: #F2F2F2;
&:first-child {
border-top-right-radius: 20px;
}
}
};
`;
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 | 🟠 Major

styled-components를 컴포넌트 내부에서 정의하면 안 됩니다

ActiveListBodyActiveApplicationRow가 컴포넌트 함수 내부에서 정의되어 있습니다. 이로 인해 매 렌더링마다 새로운 styled-component가 생성되어:

  1. 성능 저하 (불필요한 CSS 재계산)
  2. React reconciliation 문제 (컴포넌트 타입이 매번 달라짐)
  3. 상태 손실 가능성

컴포넌트 외부 또는 별도 스타일 파일로 이동해야 합니다.

+// 파일 상단 또는 ApplicationListTab.styles.ts에 추가
+const ActiveListBody = styled(Styled.ApplicationList)`
+  border-top-left-radius: 0;
+`;
+
+const ActiveApplicationRow = styled(ApplicationRowItem)`
+  &:hover {
+    background-color: #F2F2F2;
+    &:first-child {
+      border-top-right-radius: 20px;
+    }
+  }
+`;

 const ApplicationListTab = () => {
   // ...
-  const ActiveListBody = styled(Styled.ApplicationList)`
-    border-top-left-radius: 0;
-  `;
-  const ActiveApplicationRow = styled(ApplicationRowItem)`
-    &:hover {
-      background-color: #F2F2F2;
-      &:first-child {
-        border-top-right-radius: 20px;
-      }
-    }
-  `;
🤖 Prompt for AI Agents
In frontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationListTab.tsx
around lines 106 to 116, the styled-components ActiveListBody and
ActiveApplicationRow are currently defined inside the component causing new
component creation each render; move these styled definitions to the top-level
of the module (or a separate styles file) so they are created once, ensure any
props or variables they need are passed in explicitly (via props or
styled.attrs) rather than closed over, and update imports/exports accordingly so
the component uses the externally defined styled-components.

Comment on lines 83 to 87
} catch (e) {
const externalApplicationUrl = clubDetail.externalApplicationUrl?.trim();
if (externalApplicationUrl) {
window.open(externalApplicationUrl, '_blank');
return;
}
setOptions([]);
setIsOpen(true);
console.error('지원서 옵션 조회 중 오류가 발생했습니다.', e);
}
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 | 🟡 Minor

에러 발생 시 빈 모달을 표시하는 대신 에러 메시지를 보여주세요.

현재 에러 발생 시 빈 옵션 배열로 모달이 열리게 되어 사용자에게 빈 모달이 표시됩니다. 에러 상황에서는 모달을 열지 않고 사용자에게 에러 메시지를 표시하는 것이 더 나은 UX입니다.

    } catch (e) {
-     setOptions([]);
-     setIsOpen(true);
      console.error('지원서 옵션 조회 중 오류가 발생했습니다.', e);
+     alert('지원서 옵션을 불러오는 중 오류가 발생했습니다. 다시 시도해주세요.');
    }
📝 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
} catch (e) {
const externalApplicationUrl = clubDetail.externalApplicationUrl?.trim();
if (externalApplicationUrl) {
window.open(externalApplicationUrl, '_blank');
return;
}
setOptions([]);
setIsOpen(true);
console.error('지원서 옵션 조회 중 오류가 발생했습니다.', e);
}
} catch (e) {
console.error('지원서 옵션 조회 중 오류가 발생했습니다.', e);
alert('지원서 옵션을 불러오는 중 오류가 발생했습니다. 다시 시도해주세요.');
}
🤖 Prompt for AI Agents
In
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
around lines 83 to 87, when fetching options fails the code currently clears
options and opens the modal causing an empty modal to appear; instead, do not
open the modal on error and surface an error message to the user (e.g.,
setIsOpen(false) or leave it unchanged and call a user-facing error handler such
as a toast or set an error state that the component renders); keep the
console.error for diagnostics but remove or replace setIsOpen(true) and ensure
the UI displays the error text so users see the failure rather than an empty
modal.

Copy link
Collaborator

@suhyun113 suhyun113 left a comment

Choose a reason for hiding this comment

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

테스트해보니 문제없는 것 같아요!
수고하셨습니당

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

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

수고하셨습니다
릴리즈 가시죠

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.

5 participants

Comments