[feature] 모바일 상세페이지 Footer UI 변경 #744
Conversation
- Club 타입 제거 - ClubDetailFooter의 presidentPhoneNumber prop제거
- deadlineText prop으로 변경하여 중복 계산 제거 - ShareButton 동적 import로 번들 크기 최적화 - 이벤트 추적 위치 개선 및 수직선 구분자 추가
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning
|
| Cohort / File(s) | Summary |
|---|---|
페이지/하단 구조 및 API 호출 변경frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx, frontend/src/apis/application/getApplication.ts |
ClubDetailPage에서 데스크톱 ShareButton 제거; getApplication의 콘솔 에러 메시지 문구·세미콜론 스타일 변경(동작 불변). |
Footer 인터페이스 및 스타일 변경frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx, frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.styles.ts |
ClubDetailFooterProps를 recruitmentPeriod만 받도록 축소(제거: recruitmentForm, presidentPhoneNumber), DeadlineBadge 제거, Footer를 하단에 고정(sticky)하는 스타일로 전환. |
지원 버튼 리팩터링frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx, frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.styles.ts |
deadlineText?: string prop 추가, 내부 로직 재구성(지원 가능/마감 판정 내재화), ShareButton 함께 렌더하도록 구조 변경, styled-components 기반 스타일 추가(ApplyButtonContainer, ApplyButton). |
헤더/공유 버튼 조정frontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.tsx, frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx, frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.styles.ts |
헤더에서 ClubApplyButton 제거; ShareButton 아이콘을 KakaoIcon→ShareIcon으로 교체하고 컨테이너 width 제거, clubDetail 없을 시 조기 반환 추가 및 불필요한 import 제거. |
데드라인 유틸 변경frontend/src/utils/getDeadLineText.ts |
남은 일수가 365일 초과면 '상시 모집'을 반환하는 분기 추가(시그니처 불변). |
Sequence Diagram(s)
sequenceDiagram
autonumber
actor User as 사용자
participant Page as ClubDetailPage
participant Footer as ClubDetailFooter
participant Apply as ClubApplyButton
participant Share as ShareButton
Note right of Page #DDDDFF: 페이지 렌더링 흐름
User->>Page: 상세페이지 진입
Page->>Footer: render(recruitmentPeriod)
Footer->>Apply: render(deadlineText)
User->>Apply: 지원 버튼 클릭
Apply->>Apply: 입력값/deadlineText 확인
alt 모집 마감(또는 CLOSED)
Apply-->>User: "모집 마감" 표시 / 클릭 비활성 또는 alert
else 모집 중
Apply->>Share: 같은 컨테이너에서 ShareButton 렌더링(동시 노출)
Apply-->>User: 내부 지원 페이지로 라우트(또는 외부 폼 오픈)
end
Estimated code review effort
🎯 3 (Moderate) | ⏱️ ~25 minutes
Possibly related issues
- [feature] MOA-234 상세페이지 지원하기 버튼 위치를 변경한다 #742 — 상세페이지의 지원 버튼 위치 변경 요구와 하단/버튼 위치 조정 작업이 본 PR 변경 사항과 직접적으로 일치함.
Possibly related PRs
- [release] v1.0.9 #607 — ClubDetailPage 관련 컴포넌트(ApplyButton, Footer 등)들의 props·동작 재정의와 코드 영역이 겹침.
- [feature] 지원서 제출 후 메시지 개선 및 리다이렉트 처리 추가 #592 — ClubApplyButton의 클릭/네비게이션 처리 변경 사항과 높은 연관성(라우팅/행동 로직 수정).
- [feature] 공유버튼을 카카오톡 이미지로 변경한다 #597 — ShareButton의 아이콘·렌더 변경과 직접적인 코드 레벨 유사성 존재.
Suggested reviewers
- lepitaaar
- oesnuj
- Zepelown
Pre-merge checks and finishing touches
❌ Failed checks (1 warning)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Out of Scope Changes Check | raw_summary를 보면 대부분의 변경은 푸터 UI 관련이지만 frontend/src/apis/application/getApplication.ts의 로깅 문구(영문→한글)와 세미콜론 추가처럼 기능적 관련이 적은 로깅/스타일 변경이 포함되어 있고 ClubDetailFooterProps에서 presidentPhoneNumber와 recruitmentForm을 제거한 것은 컴포넌트 API 변경으로 이슈에 명시되지 않았다면 범위밖일 가능성이 있습니다. | 해결 권장: UI 관련 커밋만 이 PR에 남기고 getApplication.ts의 로깅 변경은 별도 PR로 분리하거나 변경 사유를 명확히 하며 ClubDetailFooter의 API 변경은 의도된 설계인지 팀과 확인하고 영향 범위를 조사해 호출부를 함께 수정하거나 호환성을 유지하는 방식으로 정리하십시오. |
✅ Passed checks (4 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
| Title Check | ✅ Passed | 제목 "[feature] 모바일 상세페이지 Footer UI 변경"은 PR의 핵심 변경사항인 모바일 상세페이지 푸터 UI 개편, 지원하기 버튼 위치 변경 및 공유 버튼 추가를 간결하게 요약하고 있어 목적과 일치하며 불필요한 잡음이 없습니다. |
| Linked Issues Check | ✅ Passed | PR 변경사항은 MOA-234(상세페이지 지원하기 버튼 위치 변경)의 주요 목적과 일치합니다; 푸터 일원화, 데드라인 컴포넌트 제거 및 데드라인 텍스트를 지원하기 버튼에 포함, 공유 버튼 추가, 데스크탑에서 버튼 제거 등의 구현이 코드상 반영되어 있습니다. |
| Docstring Coverage | ✅ Passed | No functions found in the changes. Docstring coverage check skipped. |
✨ Finishing touches
- 📝 Generate Docstrings
🧪 Generate unit tests
- Create PR with unit tests
- Post copyable unit tests in a comment
- Commit unit tests in branch
feature/#742-detaile-page-apply-button-position-MOA-234
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: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx (3)
16-16: 정적 import 남아있어 코드 분할/트리셰이킹 무력화. 반드시 제거 필요.
ShareButton을 더 이상 사용하지 않는데 정적 import가 남아 있어 관련 의존성이 번들에 항상 포함됩니다. PR 목표와 상충합니다.-import ShareButton from '@/pages/ClubDetailPage/components/ShareButton/ShareButton';
37-43: 에러 처리 분기 순서 오류 (에러 UI가 절대 노출되지 않음).현재는
!clubDetail이 먼저 반환되어error가 있어도 null을 반환합니다. 에러 분기를 먼저 처리하세요.- if (!clubDetail) { - return null; - } - - if (error) { - return <div>에러가 발생했습니다.</div>; - } + if (error) { + return <div>에러가 발생했습니다.</div>; + } + + if (!clubDetail) { + return null; + }
1-83: ShareButton 정적 import 제거 및 Kakao SDK 지연 로딩 필요
- 발견: ClubDetailPage가 ShareButton을 정적으로 import하고 있음 — frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx:16
- ShareButton 내부에서 아이콘 정적 import 및 Kakao 직접 호출 존재 — frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx:4 (share_filled_icon.svg), 21~26 (window.Kakao / Kakao.Share)
- 참고: ClubApplyButton은 동적 import로 처리됨 — frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx:66
- Kakao SDK가 전역으로 로드됨(public/index.html) 및 init 호출 위치 — frontend/public/index.html:57, frontend/src/utils/initSDK.ts:52
권장 조치:
- ClubDetailPage에서 ShareButton을 동적 import(React.lazy 또는 import())로 변경해 초기 번들에서 분리.
- share_filled_icon.svg를 동적 로드하거나 inline SVG로 교체해 번들 영향 최소화.
- public/index.html의 Kakao 스크립트 전역 포함을 제거하고, ShareButton 사용 시점에 스크립트 삽입 + initSDK로 초기화하도록 변경(전역 로딩이 의도라면 이유를 문서화).
🧹 Nitpick comments (10)
frontend/src/apis/application/getApplication.ts (2)
7-7: 에러 메시지 마지막 세미콜론이 의도치 않은 변경일 수 있음Line 7에서
console.error문의 마지막에 추가된 세미콜론은 실제로는 의미상 큰 변화는 없지만, 의도치 않은 변경이거나 단순한 포맷팅 수정일 수 있습니다.
16-16: 로그 메시지의 언어 일관성 검토가 필요함Line 16에서 로그 메시지를 한국어로 변경한 것은 사용자 경험 측면에서는 좋지만, 기존 Line 7의
console.error는 여전히 영어로 남아있어 일관성이 부족합니다. 동적 import는 트리쉐이킹과 잘 작동한다고 알려져 있지만, 현재 PR의 ShareButton 동적 로딩 구현과 관련하여 로그 메시지의 일관성도 중요합니다.Line 7의 에러 메시지도 한국어로 통일하는 것을 고려해보세요:
- console.error(`Failed to fetch: ${response.statusText}`); + console.error(`지원서 조회 요청 실패: ${response.statusText}`);frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx (2)
50-57: 키보드 접근성 보강 필요 (role='button' 사용 시 필수).Space/Enter로 트리거 가능하도록 onKeyDown과 tabIndex를 추가해 주세요.
적용 diff:
- <Styled.ShareButtonContainer - onClick={handleShare} - role='button' - aria-label='카카오톡으로 동아리 정보 공유하기' - > + <Styled.ShareButtonContainer + onClick={handleShare} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleShare(); + } + }} + role='button' + tabIndex={0} + aria-label='카카오톡으로 동아리 정보 공유하기' + >
56-56: 아이콘과 대체 텍스트 의미 일치 제안.일반 공유 아이콘이면 alt를 “공유하기 아이콘” 등으로 바꾸는 편이 명확합니다. 기능은 카카오 공유이므로 유지해도 되지만, UI 의미상 혼선을 줄이기 위해 권장합니다.
- <img src={ShareIcon} alt='카카오톡 공유' /> + <img src={ShareIcon} alt='공유하기 아이콘' />frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (4)
43-46: 불필요/무효한 스타일 제거.img에 font-size/weight는 적용되지 않습니다. 불필요 rules 제거하세요.
- img { - font-size: 12px; - font-weight: 600; - }
59-61: 타입 임포트 방식 개선 (React 네임스페이스 의존 제거).
React.ComponentType대신 타입 전용 임포트를 사용하세요. 번들/TS 설정과 독립적으로 안전합니다.-import { useState, useEffect } from 'react'; +import { useState, useEffect, type ComponentType } from 'react'; - const [ShareButtonComponent, setShareButtonComponent] = - useState<React.ComponentType<{ clubId: string }> | null>(null); + const [ShareButtonComponent, setShareButtonComponent] = + useState<ComponentType<{ clubId: string }> | null>(null);Also applies to: 9-9
64-71: 동적 로딩을 모바일에만 제한 + 예외 처리 추가.PR 목적(모바일 전용 로딩)을 코드로 보장하고, 로딩 실패 시 안전하게 무시하세요.
-useEffect(() => { - if (deadlineText) { - import('../ShareButton/ShareButton').then((module) => { - setShareButtonComponent(() => module.default); - }); - } -}, [deadlineText]); +useEffect(() => { + if (!deadlineText) return; + if (typeof window === 'undefined') return; + const isMobile = window.matchMedia('(max-width: 500px)').matches; + if (!isMobile) return; + import('../ShareButton/ShareButton') + .then((module) => setShareButtonComponent(() => module.default)) + .catch(() => setShareButtonComponent(null)); +}, [deadlineText]);
80-89: 변수 섀도잉 제거 (가독성).props의
deadlineText와 동일한 이름을 내부에서 다시 사용하고 있어 혼란을 줍니다.- const deadlineText = getDeadlineText( + const computedDeadlineText = getDeadlineText( recruitmentStart, recruitmentEnd, new Date(), ); - if (deadlineText === '모집 마감') { + if (computedDeadlineText === '모집 마감') { alert(`현재 ${clubDetail.name} 동아리는 모집 기간이 아닙니다.`); return; }frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx (1)
75-77: 불필요 prop 전달 제거 제안.
ClubDetailFooter에서recruitmentForm를 사용하지 않습니다. 호출부에서 제거해 인터페이스를 간결화하세요.- <ClubDetailFooter - recruitmentPeriod={clubDetail.recruitmentPeriod} - recruitmentForm={clubDetail.recruitmentForm} - /> + <ClubDetailFooter + recruitmentPeriod={clubDetail.recruitmentPeriod} + />frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx (1)
6-11: Footer 인터페이스 정리 (사용하지 않는 prop 제거).
recruitmentForm를 받지만 사용하지 않습니다. 호출부와 함께 제거해 API 명확성을 높이세요.interface ClubDetailFooterProps { recruitmentPeriod: string; - recruitmentForm: string; } -const ClubDetailFooter = ({ recruitmentPeriod }: ClubDetailFooterProps) => { +const ClubDetailFooter = ({ recruitmentPeriod }: ClubDetailFooterProps) => { const { recruitmentStart, recruitmentEnd } = parseRecruitmentPeriod(recruitmentPeriod); @@ return ( <Styled.ClubDetailFooterContainer> - <ClubApplyButton deadlineText={deadlineText} /> + <ClubApplyButton deadlineText={deadlineText} /> </Styled.ClubDetailFooterContainer> );호출부 수정은
ClubDetailPage.tsx코멘트의 diff 참조.Also applies to: 21-24
📜 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/share_filled_icon.svgis excluded by!**/*.svg
📒 Files selected for processing (7)
frontend/src/apis/application/getApplication.ts(2 hunks)frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx(1 hunks)frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx(3 hunks)frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx(2 hunks)frontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.tsx(1 hunks)frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.styles.ts(0 hunks)frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx(2 hunks)
💤 Files with no reviewable changes (1)
- frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.styles.ts
🧰 Additional context used
📓 Path-based instructions (2)
frontend/**/*.{ts,tsx}
📄 CodeRabbit inference engine (frontend/.cursorrules)
frontend/**/*.{ts,tsx}: Replace magic numbers with named constants for clarity.
Replace complex or nested ternary operators with if/else statements or IIFEs for readability.
Assign complex boolean conditions to named variables.
Use consistent return types for similar functions and hooks.
Avoid hidden side effects; functions should only perform actions implied by their signature (Single Responsibility Principle).
Use unique, descriptive names for custom wrappers and functions to avoid ambiguity.
Define constants near related logic or ensure names link them clearly.
Files:
frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsxfrontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.tsxfrontend/src/apis/application/getApplication.tsfrontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsxfrontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsxfrontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
frontend/**/*.tsx
📄 CodeRabbit inference engine (frontend/.cursorrules)
frontend/**/*.tsx: Abstract complex logic/interactions into dedicated components or higher-order components (HOCs).
Separate significantly different conditional UI/logic into distinct components.
Colocate simple, localized logic or use inline definitions to reduce context switching.
Choose field-level or form-level cohesion based on form requirements.
Break down broad state management into smaller, focused hooks or contexts.
Use component composition instead of props drilling.
Files:
frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsxfrontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.tsxfrontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsxfrontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsxfrontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
🧠 Learnings (2)
📓 Common learnings
Learnt from: seongwon030
PR: Moadong/moadong#195
File: frontend/src/pages/AdminPage/AdminPage.tsx:7-7
Timestamp: 2025-03-19T05:18:07.818Z
Learning: AdminPage.tsx에서 현재 하드코딩된 클럽 ID('67d2e3b9b15c136c6acbf20b')는 로그인 기능 구현 후 동적으로 가져오는 방식으로 수정될 예정입니다.
📚 Learning: 2025-03-19T05:18:07.818Z
Learnt from: seongwon030
PR: Moadong/moadong#195
File: frontend/src/pages/AdminPage/AdminPage.tsx:7-7
Timestamp: 2025-03-19T05:18:07.818Z
Learning: AdminPage.tsx에서 현재 하드코딩된 클럽 ID('67d2e3b9b15c136c6acbf20b')는 로그인 기능 구현 후 동적으로 가져오는 방식으로 수정될 예정입니다.
Applied to files:
frontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.tsxfrontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
🧬 Code graph analysis (1)
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (2)
frontend/src/hooks/queries/club/useGetClubDetail.ts (1)
useGetClubDetail(6-20)frontend/src/constants/eventName.ts (1)
EVENT_NAME(1-19)
🔇 Additional comments (5)
frontend/src/apis/application/getApplication.ts (1)
14-15: 확인 완료 — FIXME 주석의 응답 구조(statuscode / message / data)와 (await response.json()).message 접근 방식은 코드베이스 관례와 일치합니다.frontend/src/apis/auth/login.ts의 LoginResponse 인터페이스 및 다수의 API 파일들이 동일 패턴을 사용합니다.
frontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.tsx (1)
16-16: 주석 추가 확인.데스크탑 전용 의도가 주석으로 명확합니다. 헤더의
ClubApplyButton은deadlineText미전달 상태이므로 공유 버튼 로드가 되지 않습니다. 모바일에서 Footer에도 Apply 버튼이 있어 이중 노출/중복 인터랙션이 없는지 UI 확인 부탁드립니다.frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx (1)
15-19: 마감 텍스트 계산 및 전달 흐름 LGTM.현재 시각 기준으로
deadlineText계산 후 버튼으로 일원화한 구조가 명확합니다.frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx (1)
22-33: SSR 우려 불필요 — CSR SPA이므로 window 사용 안전
moadong은 순수 React + react-router-dom 기반의 CSR SPA(저장된 학습 정보 기준)라 useState에서 window를 직접 참조해도 SSR 크래시 우려가 없습니다. 제안된 변경 불필요.Likely an incorrect or invalid review comment.
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (1)
107-107: clubId non-null 단언 사용 검증 필요useParams로 받아온 clubId를 clubId!로 단언해 사용하고 있습니다. 라우트가 항상 '/:clubId'를 보장하는지 확인하십시오. 보장되지 않으면 clubId가 없을 때 조건부 렌더링/early return 또는 null-safe 검사로 변경하세요.
위치: frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx — ShareButtonComponent 렌더() 및 useGetClubDetail(clubId!) 호출부.
- handleShare 내부 → 컴포넌트 상단으로 조기 반환 위치 변경 - TypeError 방지
- deadlineText를 prop으로 받아 중복 계산 제거 - parseRecruitmentPeriod, getDeadlineText import 제거 - 상시 모집 시 수직선과 데드라인 텍스트 제거 - 컴포넌트 책임 분리 및 성능 개선
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/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.styles.ts (1)
1-45: 스타일 구성이 잘 정의되었지만 매직넘버 개선이 필요합니다.styled-components를 사용한 컴포넌트 스타일링이 적절하게 구현되어 있으나, 코딩 가이드라인에 따라 매직넘버들을 명명된 상수로 교체하는 것을 권장합니다.
다음과 같이 매직넘버를 상수로 분리하여 개선할 수 있습니다:
import styled from 'styled-components'; +const BUTTON_CONFIG = { + BORDER_RADIUS: '10px', + TRANSITION_DURATION: '0.2s', + PADDING_VERTICAL: '10px', + PADDING_HORIZONTAL: '40px', + WIDTH_DESKTOP: '517px', + WIDTH_MOBILE: '280px', + HEIGHT: '44px', + FONT_SIZE: '16px', + FONT_WEIGHT: 500, + GAP: '10px', + HOVER_SCALE: 1.03, + MOBILE_BREAKPOINT: '500px' +} as const; + +const COLORS = { + BACKGROUND: '#3a3a3a', + BACKGROUND_HOVER: '#555', + TEXT: '#fff', + SEPARATOR: '#787878' +} as const; export const ApplyButtonContainer = styled.div` width: 100%; display: flex; flex-direction: row; justify-content: center; text-align: center; - gap: 10px; + gap: ${BUTTON_CONFIG.GAP}; `; export const ApplyButton = styled.button` display: flex; align-items: center; justify-content: center; border: none; - border-radius: 10px; + border-radius: ${BUTTON_CONFIG.BORDER_RADIUS}; cursor: pointer; - transition: transform 0.2s ease-in-out; + transition: transform ${BUTTON_CONFIG.TRANSITION_DURATION} ease-in-out; - background-color: #3a3a3a; + background-color: ${COLORS.BACKGROUND}; - padding: 10px 40px; + padding: ${BUTTON_CONFIG.PADDING_VERTICAL} ${BUTTON_CONFIG.PADDING_HORIZONTAL}; - width: 517px; + width: ${BUTTON_CONFIG.WIDTH_DESKTOP}; - height: 44px; + height: ${BUTTON_CONFIG.HEIGHT}; - font-size: 16px; + font-size: ${BUTTON_CONFIG.FONT_SIZE}; font-style: normal; - font-weight: 500; + font-weight: ${BUTTON_CONFIG.FONT_WEIGHT}; - color: #fff; + color: ${COLORS.TEXT}; text-align: center; &:hover { - background-color: #555; + background-color: ${COLORS.BACKGROUND_HOVER}; - transform: scale(1.03); + transform: scale(${BUTTON_CONFIG.HOVER_SCALE}); } img { font-size: 12px; font-weight: 600; } - @media (max-width: 500px) { + @media (max-width: ${BUTTON_CONFIG.MOBILE_BREAKPOINT}) { - width: 280px; + width: ${BUTTON_CONFIG.WIDTH_MOBILE}; } `;
🧹 Nitpick comments (3)
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (3)
20-20: 조기 반환에서 null 반환 고려현재 조기 반환에서
undefined를 반환하는데, 명시적으로null을 반환하는 것이 React 컴포넌트의 관례에 더 적합합니다.- if (!clubId || !clubDetail) return; + if (!clubId || !clubDetail) return null;
36-39: 외부 링크 검증 로직 개선외부 링크가 없을 때의 에러 메시지가 적절하지만, 링크 유효성을 더 엄격하게 검증하는 것을 고려해보세요.
const externalFormLink = clubDetail.externalApplicationUrl?.trim(); - if (!externalFormLink) { + if (!externalFormLink || !isValidUrl(externalFormLink)) { - alert('동아리 모집 정보를 확인해주세요.'); + alert('동아리 지원 링크가 올바르지 않습니다. 동아리에 직접 문의해주세요.'); return; }URL 유효성 검사 헬퍼 함수 추가:
const isValidUrl = (url: string): boolean => { try { new URL(url); return true; } catch { return false; } };
55-55: 인라인 스타일 대신 styled-components 사용 권장구분자 스타일을 인라인으로 정의하는 대신, 스타일 파일에서 관리하는 것이 일관성 있는 접근법입니다.
ClubApplyButton.styles.ts에 구분자 컴포넌트 추가:export const Separator = styled.span` margin: 0 8px; color: #787878; `;그리고 컴포넌트에서 사용:
- <span style={{ margin: '0 8px', color: '#787878' }}>|</span> + <Styled.Separator>|</Styled.Separator>
📜 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 selected for processing (7)
frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx(1 hunks)frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.styles.ts(1 hunks)frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx(1 hunks)frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.styles.ts(1 hunks)frontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.tsx(0 hunks)frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx(3 hunks)frontend/src/utils/getDeadLineText.ts(1 hunks)
💤 Files with no reviewable changes (1)
- frontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
- frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx
- frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
🧰 Additional context used
📓 Path-based instructions (2)
frontend/**/*.{ts,tsx}
📄 CodeRabbit inference engine (frontend/.cursorrules)
frontend/**/*.{ts,tsx}: Replace magic numbers with named constants for clarity.
Replace complex or nested ternary operators with if/else statements or IIFEs for readability.
Assign complex boolean conditions to named variables.
Use consistent return types for similar functions and hooks.
Avoid hidden side effects; functions should only perform actions implied by their signature (Single Responsibility Principle).
Use unique, descriptive names for custom wrappers and functions to avoid ambiguity.
Define constants near related logic or ensure names link them clearly.
Files:
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.styles.tsfrontend/src/utils/getDeadLineText.tsfrontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.styles.tsfrontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
frontend/**/*.tsx
📄 CodeRabbit inference engine (frontend/.cursorrules)
frontend/**/*.tsx: Abstract complex logic/interactions into dedicated components or higher-order components (HOCs).
Separate significantly different conditional UI/logic into distinct components.
Colocate simple, localized logic or use inline definitions to reduce context switching.
Choose field-level or form-level cohesion based on form requirements.
Break down broad state management into smaller, focused hooks or contexts.
Use component composition instead of props drilling.
Files:
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
🧠 Learnings (1)
📓 Common learnings
Learnt from: seongwon030
PR: Moadong/moadong#195
File: frontend/src/pages/AdminPage/AdminPage.tsx:7-7
Timestamp: 2025-03-19T05:18:07.818Z
Learning: AdminPage.tsx에서 현재 하드코딩된 클럽 ID('67d2e3b9b15c136c6acbf20b')는 로그인 기능 구현 후 동적으로 가져오는 방식으로 수정될 예정입니다.
🧬 Code graph analysis (1)
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (2)
frontend/src/hooks/queries/club/useGetClubDetail.ts (1)
useGetClubDetail(6-20)frontend/src/constants/eventName.ts (1)
EVENT_NAME(1-19)
🔇 Additional comments (4)
frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.styles.ts (2)
4-9: 스티키 포지셔닝 구현이 적절합니다.하단 고정 레이아웃을 위한 스티키 포지셔닝과 z-index 설정이 모바일 Footer UI의 의도된 동작에 적합합니다.
11-14: Flexbox 레이아웃 구현이 잘 되었습니다.버튼들 사이의 공간 배치를 위한
justify-content: space-between과 중앙 정렬을 위한align-items: center설정이 적절합니다. 16px gap도 적절한 간격을 제공합니다.frontend/src/utils/getDeadLineText.ts (1)
14-14: 새로운 조건부 로직이 올바르게 추가되었습니다.365일을 초과하는 모집 기간에 대해 '상시 모집' 텍스트를 반환하는 로직이 적절하게 구현되었습니다. 기존 로직을 건드리지 않고 추가된 것도 좋습니다.
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (1)
9-11: 인터페이스 정의가 적절합니다.
deadlineText를 선택적 prop으로 정의한 인터페이스가 명확하고 적절합니다.
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
Outdated
Show resolved
Hide resolved
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
Show resolved
Hide resolved
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
Outdated
Show resolved
Hide resolved
frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.styles.ts
Show resolved
Hide resolved
suhyun113
left a comment
There was a problem hiding this comment.
UI가 깔끔해졌네요~ 수고하셨습니다!!!
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (5)
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (5)
25-25: 가드 분기 반환값은 null로 명시React 컴포넌트는 명시적으로 null을 반환하는 것이 안전합니다(일관된 반환 타입).
- if (!clubId || !clubDetail) return; + if (!clubId || !clubDetail) return null;
30-31: 마감 여부 비교 반복 → isRecruitmentClosed 변수로 추출조건을 변수로 이름 붙이면 가독성과 변경 용이성이 좋아집니다.
@@ - if (deadlineText === RECRUITMENT_STATUS.CLOSED) { + const isRecruitmentClosed = + deadlineText === RECRUITMENT_STATUS.CLOSED; + if (isRecruitmentClosed) { @@ - if (deadlineText === RECRUITMENT_STATUS.CLOSED) { - return RECRUITMENT_STATUS.CLOSED; - } + if (isRecruitmentClosed) return RECRUITMENT_STATUS.CLOSED;Also applies to: 50-51
59-60: 인라인 스타일은 상수/스타일드로 추출 권장매직 값(#787878, margin) 분리를 통해 일관성·테마 연동성을 높이세요.
- <span style={{ margin: '0 8px', color: '#787878' }}>|</span> + <span style={SEPARATOR_STYLE}>|</span>추가: 파일 상단에 상수 배치
const SEPARATOR_STYLE = { margin: '0 8px', color: '#787878' } as const;
28-28: 트래킹에 속성 추가 제안분석 품질 향상을 위해 clubId/clubName 등 컨텍스트를 함께 전송하세요.
- trackEvent(EVENT_NAME.CLUB_APPLY_BUTTON_CLICKED); + trackEvent(EVENT_NAME.CLUB_APPLY_BUTTON_CLICKED, { + clubId, + clubName: clubDetail?.name, + });
13-16: RECRUITMENT_STATUS에 as const 적용 권장타입 추론과 타이포 방지를 위해 리터럴을 고정하세요. 전체 검색 결과 해당 문자열/심볼은 이 파일에서만 사용됩니다: frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
-const RECRUITMENT_STATUS = { +const RECRUITMENT_STATUS = { ALWAYS: '상시 모집', CLOSED: '모집 마감', -}; +} 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 selected for processing (1)
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx(1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
frontend/**/*.{ts,tsx}
📄 CodeRabbit inference engine (frontend/.cursorrules)
frontend/**/*.{ts,tsx}: Replace magic numbers with named constants for clarity.
Replace complex or nested ternary operators with if/else statements or IIFEs for readability.
Assign complex boolean conditions to named variables.
Use consistent return types for similar functions and hooks.
Avoid hidden side effects; functions should only perform actions implied by their signature (Single Responsibility Principle).
Use unique, descriptive names for custom wrappers and functions to avoid ambiguity.
Define constants near related logic or ensure names link them clearly.
Files:
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
frontend/**/*.tsx
📄 CodeRabbit inference engine (frontend/.cursorrules)
frontend/**/*.tsx: Abstract complex logic/interactions into dedicated components or higher-order components (HOCs).
Separate significantly different conditional UI/logic into distinct components.
Colocate simple, localized logic or use inline definitions to reduce context switching.
Choose field-level or form-level cohesion based on form requirements.
Break down broad state management into smaller, focused hooks or contexts.
Use component composition instead of props drilling.
Files:
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
🧠 Learnings (5)
📓 Common learnings
Learnt from: seongwon030
PR: Moadong/moadong#195
File: frontend/src/pages/AdminPage/AdminPage.tsx:7-7
Timestamp: 2025-03-19T05:18:07.818Z
Learning: AdminPage.tsx에서 현재 하드코딩된 클럽 ID('67d2e3b9b15c136c6acbf20b')는 로그인 기능 구현 후 동적으로 가져오는 방식으로 수정될 예정입니다.
📚 Learning: 2025-07-19T05:05:10.196Z
Learnt from: seongwon030
PR: Moadong/moadong#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/ClubApplyButton/ClubApplyButton.tsx
📚 Learning: 2025-09-21T02:23:27.784Z
Learnt from: seongwon030
PR: Moadong/moadong#744
File: frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx:47-48
Timestamp: 2025-09-21T02:23:27.784Z
Learning: ClubApplyButton 컴포넌트에서 ShareButton은 항상 렌더링되어야 하므로 정적 import를 사용하는 것이 적절함. 동적 import는 불필요함.
Applied to files:
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
📚 Learning: 2025-07-19T05:09:10.702Z
Learnt from: CR
PR: Moadong/moadong#0
File: frontend/.cursorrules:0-0
Timestamp: 2025-07-19T05:09:10.702Z
Learning: Applies to frontend/**/*.tsx : Separate significantly different conditional UI/logic into distinct components.
Applied to files:
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
📚 Learning: 2025-07-19T05:09:10.702Z
Learnt from: CR
PR: Moadong/moadong#0
File: frontend/.cursorrules:0-0
Timestamp: 2025-07-19T05:09:10.702Z
Learning: Applies to frontend/**/*.{ts,tsx} : Replace complex or nested ternary operators with if/else statements or IIFEs for readability.
Applied to files:
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
🧬 Code graph analysis (1)
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (2)
frontend/src/hooks/queries/club/useGetClubDetail.ts (1)
useGetClubDetail(6-20)frontend/src/constants/eventName.ts (1)
EVENT_NAME(1-19)
🔇 Additional comments (3)
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (3)
7-7: ShareButton 정적 import 유지 적절항상 렌더링되는 컴포넌트라면 동적 로딩 이점이 적습니다. 현재 선택이 요구사항과 일치합니다.
49-65: 조건부 렌더링 분리 잘 했습니다중첩 JSX를 함수로 뽑아 가독성이 개선되었습니다. 코딩 가이드라인과 합치됩니다.
45-46: window.open 보안 플래그 적용 OK'noopener,noreferrer' 설정 적절합니다.
| if (!externalFormLink) { | ||
| alert('동아리 모집 정보를 확인해주세요.'); | ||
| return; | ||
| } | ||
| window.open(externalFormLink, '_blank', 'noopener,noreferrer'); | ||
| } |
There was a problem hiding this comment.
외부 링크 열기 전 프로토콜 검증 추가 필요(XSS/오용 방지)
backend/콘텐츠에서 잘못된 URL이 들어오면 javascript:, data:, file: 등으로 새 창을 열 수 있습니다. http/https만 허용하도록 검증하세요.
- if (!externalFormLink) {
- alert('동아리 모집 정보를 확인해주세요.');
- return;
- }
- window.open(externalFormLink, '_blank', 'noopener,noreferrer');
+ if (!externalFormLink) {
+ alert('동아리 모집 정보를 확인해주세요.');
+ return;
+ }
+ try {
+ const url = new URL(externalFormLink);
+ const isHttp = url.protocol === 'http:' || url.protocol === 'https:';
+ if (!isHttp) {
+ alert('유효하지 않은 외부 링크입니다.');
+ return;
+ }
+ window.open(url.toString(), '_blank', 'noopener,noreferrer');
+ } catch {
+ 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.
| if (!externalFormLink) { | |
| alert('동아리 모집 정보를 확인해주세요.'); | |
| return; | |
| } | |
| window.open(externalFormLink, '_blank', 'noopener,noreferrer'); | |
| } | |
| if (!externalFormLink) { | |
| alert('동아리 모집 정보를 확인해주세요.'); | |
| return; | |
| } | |
| try { | |
| const url = new URL(externalFormLink); | |
| const isHttp = url.protocol === 'http:' || url.protocol === 'https:'; | |
| if (!isHttp) { | |
| alert('유효하지 않은 외부 링크입니다.'); | |
| return; | |
| } | |
| window.open(url.toString(), '_blank', 'noopener,noreferrer'); | |
| } catch { | |
| alert('유효하지 않은 외부 링크입니다.'); | |
| } | |
| } |
🤖 Prompt for AI Agents
In
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
around lines 41-46, the code opens externalFormLink without validating its
protocol; fix by parsing the link with the URL constructor inside a try/catch
and only allow navigation when url.protocol is 'http:' or 'https:' (otherwise
alert the user and abort). If parsing fails or the protocol is disallowed, do
not call window.open; keep using target='_blank' and rel='noopener,noreferrer'
when opening allowed URLs.
#️⃣연관된 이슈
📝작업 내용
상세페이지 푸터 일원화
데스크탑
모바일
중점적으로 리뷰받고 싶은 부분(선택)
논의하고 싶은 부분(선택)
🫡 참고사항
Summary by CodeRabbit
신기능/개선
UI/스타일
변경/리팩터