Conversation
…feature/#846-add-external-url-to-application-MOA-345
- 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되는 문제를 해결한다
- /admin으로 이동 - 트래킹 로직 추가
…ory-button-padding-MOA-392 [fix] 모바일 카테고리 버튼 양쪽에 생기는 여백을 제거한다
…-MOA-401 [refactor] LazyImage 컴포넌트 기능 개선 및 성능 최적화
…with-admin-MOA-407 [feature] 헤더 패치노트 링크를 관리자 로그인으로 변경한다
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
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 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. 📒 Files selected for processing (8)
Warning
|
| 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
- [feature] 외부 지원서 링크를 추가할 수 있게한다 #881 - 외부 지원 양식 지원 기능 추가 (ApplicationFormMode, externalApplicationUrl 타입 및 UI 변경)
- [feature] 대표 지원서 모아보기를 추가한다. #878 - 관리자 어플리케이션 목록 리팩토링 (ApplicationRowItem 추가, ApplicantsListTab 업데이트, 삭제/상태 토글 기능)
- [refactor] LazyImage 컴포넌트 기능 개선 및 성능 최적화 #913 - LazyImage 재구현 (공통 컴포넌트 제거 및 ClubDetailPage 범위 컴포넌트 추가)
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 | 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.
Comment @coderabbitai help to get the list of available commands and usage tips.
There was a problem hiding this comment.
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: sticky와z-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 구현은 적절하나 에러 처리/타입을 조금 더 보강할 수 있습니다현재 구현으로도 동작에는 문제가 없어 보이지만, 몇 가지 개선 여지가 있습니다.
에러 응답 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;형태로 만드는 편이 더 직관적입니다.
토글 로직 단순화
현재 로직은 의미상currentStatus !== 'ACTIVE'와 동일하므로, 아래처럼 표현하면 의도가 더 분명해집니다.
- const newStatus = currentStatus === 'ACTIVE' ? false : true;
- const newStatus = currentStatus !== 'ACTIVE';
- 공통 응답 타입 재사용
updateApplication과updateApplicationStatus모두result.data를 반환하므로, 공통 응답 타입을 정의해 두면 두 함수의 반환 타입을 일관되게 관리할 수 있습니다.Also applies to: 33-58
frontend/src/pages/AdminPage/components/ApplicationRow/ApplicationRowItem.style.ts (1)
3-9: 미사용 인터페이스 정리 및 버튼 포커스 스타일 보완 제안
미사용 타입 정리
interface MenuItemProps { $ActiveMenu?: boolean; } interface ExpandButtonProps { $isExpanded: boolean; }현재 파일 내에서는 사용되지 않고 있으니, 추후 사용 계획이 없다면 제거해 두는 편이 코드 가독성과 유지보수에 좋습니다.
-interface MenuItemProps {
- $ActiveMenu?: boolean;
-}-interface ExpandButtonProps {
- $isExpanded: boolean;
-}
+// TODO: 메뉴/확장 버튼 스타일에 prop 기반 분기가 필요해지면 다시 정의합니다.
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);이렇게 하면 혹시라도 다른 곳에서 빈 문자열을 전달하는 실수를 예방하고, 타입 의도와도 더 잘 맞습니다.
navigate호출 전 안전 가드 추가 고려onClick={() => navigate( `/admin/applicants-list/${applicationFormId}/${item.id}`, ) }현재 라우팅 구조상
ApplicantsTab가 렌더링되면applicationFormId가 항상 존재하는 것이 맞지만, 방어적으로 다음과 같이 가드를 두면 예외 상황에서/admin/applicants-list/undefined/...로 이동하는 케이스를 막을 수 있습니다.
<Styled.ApplicantTableRowkey={index}onClick={() =>navigate(`/admin/applicants-list/${applicationFormId}/${item.id}`,)}
<Styled.ApplicantTableRowkey={index}onClick={() => {if (!applicationFormId) return;navigate(`/admin/applicants-list/${applicationFormId}/${item.id}`,);}} >
(선택) 상태/필터/정렬 로직 분리
이 탭 컴포넌트에 상태와 이벤트 로직이 상당히 많아서, 장기적으로는
- 필터/정렬/선택 관련 로직을 커스텀 훅으로 분리하거나,
- 테이블/헤더/요약 카드 등을 소형 컴포넌트로 쪼개는 것
을 고려하면 가독성과 재사용성이 더 좋아질 것 같습니다. 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를 렌더링하도록 바꾼 부분은,ApplicantsTab이useParams로applicationFormId를 읽어 쓰는 현재 구조와 잘 맞습니다.application-list/:applicationFormId/edit와application-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함수 간소화를 고려해 주세요.현재
openByOption은goWithForm의 얇은 래퍼입니다.ApplicationSelectModal의onSelect에 직접 인라인 처리하거나,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제거 필요
onEditTitle가console.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: 중복된keyprop
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에서 직접updateApplicationStatusAPI를 호출하고 수동으로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.
⛔ Files ignored due to path filters (1)
frontend/src/assets/images/icons/ExpandArrow.svgis 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 withif/elseor 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.tsfrontend/src/components/ClubLogo/ClubLogo.tsxfrontend/src/constants/eventName.tsfrontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.styles.tsfrontend/src/apis/application/getApplication.tsfrontend/src/context/AdminClubContext.tsxfrontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.tsxfrontend/src/constants/INITIAL_FORM_DATA.tsfrontend/src/pages/AdminPage/AdminRoutes.tsxfrontend/src/apis/application/updateApplication.tsfrontend/src/hooks/useTrackPageView.tsfrontend/src/pages/AdminPage/components/ApplicationRow/ApplicationRowItem.tsxfrontend/src/components/common/Header/Header.tsxfrontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsxfrontend/src/pages/ClubDetailPage/ClubDetailPage.tsxfrontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsListTab/ApplicantsListTab.tsxfrontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.styles.tsfrontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationListTab.tsxfrontend/src/pages/IntroducePage/constants/mockData.tsfrontend/src/utils/parseRecruitmentPeriod.test.tsfrontend/src/utils/recruitmentPeriodParser.test.tsfrontend/src/pages/AdminPage/components/ApplicationRow/ApplicationRowItem.style.tsfrontend/src/hooks/queries/application/useDeleteApplication.tsfrontend/src/pages/ApplicationFormPage/ApplicationFormPage.tsxfrontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsxfrontend/src/mocks/data/mockData.tsfrontend/src/types/application.tsfrontend/src/pages/ClubDetailPage/components/ClubCard/ClubCard.tsxfrontend/src/hooks/Header/useHeaderNavigation.tsfrontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.test.tsxfrontend/src/apis/application/deleteApplication.tsfrontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsxfrontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsxfrontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.styles.tsfrontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationMenu.tsxfrontend/src/pages/ClubDetailPage/components/PhotoList/PhotoCardList/PhotoCardList.tsxfrontend/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.tsfrontend/src/components/ClubLogo/ClubLogo.tsxfrontend/src/constants/eventName.tsfrontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.styles.tsfrontend/src/apis/application/getApplication.tsfrontend/src/context/AdminClubContext.tsxfrontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.tsxfrontend/src/constants/INITIAL_FORM_DATA.tsfrontend/src/pages/AdminPage/AdminRoutes.tsxfrontend/src/apis/application/updateApplication.tsfrontend/src/hooks/useTrackPageView.tsfrontend/src/pages/AdminPage/components/ApplicationRow/ApplicationRowItem.tsxfrontend/src/components/common/Header/Header.tsxfrontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsxfrontend/src/pages/ClubDetailPage/ClubDetailPage.tsxfrontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsListTab/ApplicantsListTab.tsxfrontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.styles.tsfrontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationListTab.tsxfrontend/src/pages/IntroducePage/constants/mockData.tsfrontend/src/utils/parseRecruitmentPeriod.test.tsfrontend/src/utils/recruitmentPeriodParser.test.tsfrontend/src/pages/AdminPage/components/ApplicationRow/ApplicationRowItem.style.tsfrontend/src/hooks/queries/application/useDeleteApplication.tsfrontend/src/pages/ApplicationFormPage/ApplicationFormPage.tsxfrontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsxfrontend/src/mocks/data/mockData.tsfrontend/src/types/application.tsfrontend/src/pages/ClubDetailPage/components/ClubCard/ClubCard.tsxfrontend/src/hooks/Header/useHeaderNavigation.tsfrontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.test.tsxfrontend/src/apis/application/deleteApplication.tsfrontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsxfrontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsxfrontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.styles.tsfrontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationMenu.tsxfrontend/src/pages/ClubDetailPage/components/PhotoList/PhotoCardList/PhotoCardList.tsxfrontend/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.tsxfrontend/src/context/AdminClubContext.tsxfrontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.tsxfrontend/src/pages/AdminPage/AdminRoutes.tsxfrontend/src/pages/AdminPage/components/ApplicationRow/ApplicationRowItem.tsxfrontend/src/components/common/Header/Header.tsxfrontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsxfrontend/src/pages/ClubDetailPage/ClubDetailPage.tsxfrontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsListTab/ApplicantsListTab.tsxfrontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationListTab.tsxfrontend/src/pages/ApplicationFormPage/ApplicationFormPage.tsxfrontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsxfrontend/src/pages/ClubDetailPage/components/ClubCard/ClubCard.tsxfrontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.test.tsxfrontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsxfrontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsxfrontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationMenu.tsxfrontend/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.tsxfrontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsxfrontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsListTab/ApplicantsListTab.tsxfrontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationListTab.tsxfrontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsxfrontend/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.tsxfrontend/src/pages/ApplicationFormPage/ApplicationFormPage.tsxfrontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsxfrontend/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.tsxfrontend/src/pages/AdminPage/components/ApplicationRow/ApplicationRowItem.style.tsfrontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsxfrontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.styles.tsfrontend/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.tsfrontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.styles.tsfrontend/src/pages/ClubDetailPage/components/PhotoList/PhotoCardList/PhotoCardList.tsxfrontend/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.tsxfrontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsxfrontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsxfrontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsxfrontend/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.tsxfrontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsxfrontend/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.tsfrontend/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: UTCDate기대값 통일로 타임존 이슈 방지두 테스트 모두 기대값을
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!새로운
formMode와externalApplicationUrl필드가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 구현 확인삭제 기능이 올바르게 구현되었으며,
deleteApplicationAPI 엔드포인트는 적절한 에러 처리와 함께 DELETE 요청을 정확히 수행합니다.mutationFn과onSuccess콜백이 표준 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: 새로운 폼 모드 필드가 올바르게 추가되었습니다.
formMode와externalApplicationUrl필드가 타입 정의와 일치하게 추가되었으며, 초기값도 적절합니다.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이 비어있을 때 내부 폼으로 폴백되는 동작을 확인해 주세요.
formMode가EXTERNAL이지만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: Theactiveandstatusfields use different casing intentionally—they represent different API contracts.ApplicationFormData.active(lowercase) is the request payload format, whileApplicationFormItem.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와 조건부 렌더링이 명확하게 구현되어 있습니다. 버튼 컴포넌트 활용과
$activeprop을 통한 시각적 피드백도 적절합니다.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
$isExpandedprop에 따른 화살표 회전 애니메이션이css헬퍼를 사용하여 깔끔하게 구현되었습니다.
192-218: MenuItem의 조건부 스타일링 LGTM
$ActiveMenuprop을 통한 활성 상태 스타일링이 적절하게 구현되었습니다.
| 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; | ||
| } | ||
| } | ||
| }; | ||
| `; |
There was a problem hiding this comment.
styled-components를 렌더 함수 내부에 정의하면 성능 문제 발생
ActiveListBody와 ActiveApplicationRow가 컴포넌트 함수 내부에서 정의되어 있습니다. 이렇게 하면 매 렌더마다 새로운 스타일드 컴포넌트가 생성되어 불필요한 리렌더링과 스타일 재계산이 발생합니다.
파일 상단(컴포넌트 함수 외부) 또는 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.
| 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.
| const currentQuestions = | ||
| questions.length > 0 ? questions : INITIAL_FORM_DATA.questions!; | ||
|
|
||
| setApplicationFormMode(formMode); | ||
| setExternalApplicationUrl(externalApplicationUrl); | ||
| setNextId(Math.max(...currentQuestions.map((q) => q.id)) + 1); |
There was a problem hiding this comment.
빈 배열에 대한 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.
| 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.
| 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; | ||
| } | ||
| } | ||
| }; | ||
| `; |
There was a problem hiding this comment.
styled-components를 컴포넌트 내부에서 정의하면 안 됩니다
ActiveListBody와 ActiveApplicationRow가 컴포넌트 함수 내부에서 정의되어 있습니다. 이로 인해 매 렌더링마다 새로운 styled-component가 생성되어:
- 성능 저하 (불필요한 CSS 재계산)
- React reconciliation 문제 (컴포넌트 타입이 매번 달라짐)
- 상태 손실 가능성
컴포넌트 외부 또는 별도 스타일 파일로 이동해야 합니다.
+// 파일 상단 또는 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.
| } catch (e) { | ||
| const externalApplicationUrl = clubDetail.externalApplicationUrl?.trim(); | ||
| if (externalApplicationUrl) { | ||
| window.open(externalApplicationUrl, '_blank'); | ||
| return; | ||
| } | ||
| setOptions([]); | ||
| setIsOpen(true); | ||
| console.error('지원서 옵션 조회 중 오류가 발생했습니다.', e); | ||
| } |
There was a problem hiding this comment.
에러 발생 시 빈 모달을 표시하는 대신 에러 메시지를 보여주세요.
현재 에러 발생 시 빈 옵션 배열로 모달이 열리게 되어 사용자에게 빈 모달이 표시됩니다. 에러 상황에서는 모달을 열지 않고 사용자에게 에러 메시지를 표시하는 것이 더 나은 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.
| } 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.
…OA-397 [refactor] z-index 기준으로 리팩토링한다
suhyun113
left a comment
There was a problem hiding this comment.
테스트해보니 문제없는 것 같아요!
수고하셨습니당
#️⃣연관된 이슈
📝작업 내용
중점적으로 리뷰받고 싶은 부분(선택)
논의하고 싶은 부분(선택)
🫡 참고사항
Summary by CodeRabbit
릴리스 노트
새로운 기능
버그 수정
성능 개선
✏️ Tip: You can customize this high-level summary in your review settings.