Conversation
애니메이션 구현을 위해 framer-motion 의존성을 추가합니다. 향후 컴포넌트 등장, 인터랙션 등 다양한 동적 UI를 구현하는 데 사용될 예정입니다.
소개 페이지의 배경 및 시각적 장식에 사용될 SVG 파일을 추가합니다. 추가된 파일: - background-circle-large.svg - background-circle-small.svg - background-twist-left.svg - background-twist-right.svg
framer-motion을 기반으로 페이지 배경을 꾸미는 Twist 및 Circle 애니메이션 컴포넌트 4종을 추가합니다.
styled-components를 사용하여 소개 페이지의 전체적인 레이아웃과 각 섹션(인트로, 페인포인트, 슬로건 등)에 대한 기본 스타일링을 추가합니다. 이는 초기 버전이며, 추후 반응형 및 디테일한 스타일이 적용될 예정입니다.
소개 페이지의 최상단 히어로 섹션을 구현합니다.
…MOA-213 [feature] 사이드바가 스크롤을 따라온다
…MOA-230 [feature] 글 영역 박스를 제거하고 레이아웃 구분선을 추가한다
…ner-MOA-233 [feature] 메인페이지 배너를 변경한다
- Club 타입 제거 - ClubDetailFooter의 presidentPhoneNumber prop제거
- deadlineText prop으로 변경하여 중복 계산 제거 - ShareButton 동적 import로 번들 크기 최적화 - 이벤트 추적 위치 개선 및 수직선 구분자 추가
- 지원하기 버튼, 지원서 제출 성공 및 실패 로깅
…us-and-move-central-tab-MOA-259 [feature] 모집 상태 보기 기능을 제거하고 중앙동아리 태그를 이동한다
…ithub.com/Moadong/moadong into feature/#697-intro-html-animation-MOA-197
…n-MOA-197 [feature] 모아동 소개 페이지 HTML 전환 + 애니메이션/반응형 적용 (framer-motion, useDevice 훅 도입)
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning
|
| Cohort / File(s) | Change Summary |
|---|---|
Dependencyfrontend/package.json |
framer-motion 신규 의존성 추가. |
API & Utilsfrontend/src/apis/application/getApplication.ts, frontend/src/utils/getDeadLineText.ts |
로그 메시지/세미콜론 등 사소한 포맷 변경과 에러 로그 현지화. 마감 텍스트: days > 365 시 ‘상시 모집’ 조기 반환 추가. |
Analyticsfrontend/src/constants/eventName.ts, frontend/src/pages/ApplicationFormPage/ApplicationFormPage.tsx |
EVENT_NAME에 APPLICATION_FORM_SUBMITTED 추가 및 폼 제출 시 트래킹 호출 추가. |
Introduce: 구성요소/애니메이션/데이터frontend/src/pages/IntroducePage/IntroducePage.tsx, frontend/src/pages/IntroducePage/IntroducePage.styles.ts, frontend/src/pages/IntroducePage/components/** (BackgroundShapes 및 sections//.tsx, *.styles.ts), frontend/src/pages/IntroducePage/constants/*, frontend/src/assets/images/introduce/features/index.ts, frontend/src/hooks/useDevice.ts |
싱글 이미지/스피너 제거, 섹션 1–7 컴포넌트/스타일/애니메이션/모션 상수/모의 데이터/디바이스 훅 추가. 피처 이미지 배열(데스크탑/모바일) 공개 export 추가. |
Introduce: 테스트frontend/src/pages/IntroducePage/IntroducePage.test.tsx |
Introduce 페이지 기존 테스트 파일 삭제. |
Club Detail: 지원/푸터/공유/헤더/스타일frontend/src/pages/ClubDetailPage/components/ClubApplyButton/*, .../ClubDetailFooter/*, .../ShareButton/*, .../ClubDetailHeader/ClubDetailHeader.tsx, frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx |
ApplyButton 컴포넌트에 deadlineText prop 도입 및 로직 재구성(상태 텍스트 상수, 렌더 가드, 클릭 시 트래킹→신청서 조회/네비게이션/대체 링크/알림). 푸터는 sticky 바로 스타일 수정, DeadlineBadge 제거, props 축소. ShareButton 아이콘 변경 및 로드 가드 추가. 헤더의 ApplyButton 제거. 신규 스타일 파일 추가. |
Main Page: 탭/카테고리/반응형frontend/src/pages/MainPage/MainPage.tsx, frontend/src/pages/MainPage/MainPage.styles.ts, frontend/src/pages/MainPage/components/CategoryButtonList/*, frontend/src/store/useCategoryStore.ts, frontend/src/styles/mediaQuery.ts |
상태 필터 UI 제거, 단일 탭 UI 추가(SectionTabs/Tab). 카테고리 버튼은 active/inactive 아이콘 맵 기반으로 선택 상태 렌더링. 스타일/브레이크포인트 정리(미니 모바일/랩탑 추가). 불필요한 FilterWrapper 제거. |
Icons & Constants 연결frontend/src/assets/images/icons/category_button/index.ts, frontend/src/constants/CLUB_UNION_INFO.ts |
카테고리 비활성/활성 아이콘 맵 export 추가. CLUB_UNION_INFO는 개별 아바타 import 대신 inactiveCategoryIcons 참조로 매핑 변경. |
Admin Page 레이아웃frontend/src/pages/AdminPage/AdminPage.styles.ts, frontend/src/pages/AdminPage/AdminPage.tsx, frontend/src/pages/AdminPage/components/SideBar/SideBar.styles.ts |
사이드바 sticky 속성 및 Divider 추가. 컨테이너 정렬/패딩 조정. 페이지에 Divider 렌더 추가. |
Common UIfrontend/src/components/ClubTag/ClubTag.tsx, frontend/src/components/common/Footer/Footer.styles.ts |
ClubTag에 className prop 추가 및 전달. Footer 상단 마진 제거 및 미세 정리. |
Sequence Diagram(s)
sequenceDiagram
autonumber
actor U as User
participant CAB as ClubApplyButton
participant TR as Track (Mixpanel)
participant API as getApplication()
participant NAV as Router
participant EXT as External URL
U->>CAB: Click "지원하기"
CAB->>TR: track(APPLICATION_BUTTON_CLICKED) [if implemented]
alt 모집 마감/상시 외 조건
CAB-->>U: alert("모집이 마감되었습니다") / no-op
else 진행 가능
CAB->>API: fetch application by clubId
alt Application exists
API-->>CAB: ok + applicationId
CAB->>NAV: navigate(`/application/${id}`)
else 외부 지원 링크 존재
API-->>CAB: not found
CAB->>EXT: window.open(externalApplicationUrl)
else 미존재
CAB-->>U: alert("지원서가 준비되지 않았습니다")
end
end
sequenceDiagram
autonumber
actor U as User
participant AFP as ApplicationFormPage
participant TR as Track (Mixpanel)
participant VAL as Validator
participant API as Submit API
participant NAV as Router
U->>AFP: Submit form
AFP->>TR: track(APPLICATION_FORM_SUBMITTED, { clubName })
AFP->>VAL: validate()
alt validation ok
AFP->>API: submit()
API-->>AFP: result
AFP->>NAV: navigate(...)
else validation fail
VAL-->>AFP: errors
AFP-->>U: show validation errors
end
Estimated code review effort
🎯 4 (Complex) | ⏱️ ~75 minutes
Possibly related PRs
- [feature] 모바일 상세페이지 Footer UI 변경 #744: 동일 영역(ApplyButton/ShareButton/데드라인 텍스트/getApplication 로그)의 흐름과 스타일을 함께 변경.
- [feature] 카테고리 버튼을 리디자인하고 선택 효과를 적용한다 #751: 카테고리 아이콘 맵 도입과 CategoryButtonList 로직/스타일 변경이 직접적으로 겹침.
- [feature] 사용자 지원서에 믹스패널 이벤트를 추가한다 #746: APPLICATION_FORM_SUBMITTED 이벤트 추가 및 ApplicationFormPage 트래킹 연동과 동일 주제.
Suggested labels
📈 release, 💻 FE, ✨ Feature, 🎨 Design
Suggested reviewers
- seongwon030
- lepitaaar
- Zepelown
- suhyun113
Pre-merge checks and finishing touches
❌ Failed checks (1 inconclusive)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Title Check | ❓ Inconclusive | PR 제목 “[release]”은 변경 사항의 핵심을 설명하지 않고 일반적인 레이블만 포함하고 있어 무슨 작업이 이루어졌는지 동료가 빠르게 파악하기 어렵습니다. | 주요 기능 변경이나 추가된 항목을 간결하게 요약하는 제목(예: “카테고리 버튼 활성화 및 애니메이션 추가” 또는 “의존성 framer-motion 추가 및 소개 페이지 섹션 분리”)으로 수정해주세요. |
✅ Passed checks (2 passed)
| Check name | Status | Explanation |
|---|---|---|
| Docstring Coverage | ✅ Passed | No functions found in the changes. Docstring coverage check skipped. |
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
✨ Finishing touches
- 📝 Generate Docstrings
🧪 Generate unit tests
- Create PR with unit tests
- Post copyable unit tests in a comment
- Commit unit tests in branch
develop-fe
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands and usage tips.
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
frontend/src/pages/ApplicationFormPage/ApplicationFormPage.tsx (1)
91-111: 이벤트 트래킹 시점을 성공 제출 이후로 옮겨주세요Line 91에서
trackEvent가 검증·API 호출 전에 실행돼 제출이 실패하더라도"Application Form Submitted"이벤트가 수집됩니다. 실제 제출 여부와 로그가 어긋나 Mixpanel 데이터 품질이 크게 훼손되므로, 성공적으로applyToClub가 완료된 뒤에만 이벤트를 남기도록 해야 합니다.- trackEvent(EVENT_NAME.APPLICATION_FORM_SUBMITTED, { - clubName: clubDetail?.name, - }); - const invalidIds = validateAnswers(formData.questions, getAnswersById); if (invalidIds.length > 0) { setInvalidQuestionIds(invalidIds); handleScrollToInvalid(invalidIds); return; } try { await applyToClub(clubId, answers); + trackEvent(EVENT_NAME.APPLICATION_FORM_SUBMITTED, { + clubName: clubDetail?.name, + }); localStorage.removeItem(STORAGE_KEY);frontend/src/pages/AdminPage/AdminPage.tsx (1)
14-19: 에러 처리 우선순위 역전: error를 먼저 처리하세요현재는
clubDetail이 falsy면 즉시null을 반환해 에러 메시지가 숨겨질 수 있습니다. 사용자 경험 및 디버깅을 위해 에러를 우선 표출하세요.다음과 같이 순서를 조정:
- if (!clubDetail) { - return null; - } - - if (error) return <p>Error: {error.message}</p>; + if (error) return <p>Error: {error.message}</p>; + if (!clubDetail) { + return null; + }frontend/src/pages/AdminPage/components/SideBar/SideBar.styles.ts (2)
3-7: align-items: left는 유효한 CSS 값이 아닙니다 → flex-start로 수정 필요브라우저가 속성을 무시해 의도한 레이아웃이 적용되지 않을 수 있습니다.
- align-items: left; + align-items: flex-start;
55-62: font-weight: medium은 유효하지 않습니다 → 숫자 값(예: 500) 사용브라우저가 무시하여 의도한 굵기가 적용되지 않습니다.
export const SidebarCategoryTitle = styled.p` @@ - font-weight: medium; + font-weight: 500; @@ export const SidebarButton = styled.button` @@ - font-weight: medium; + font-weight: 500; @@ &.active { background-color: rgba(255, 117, 67, 1); color: white; - font-weight: medium; + font-weight: 500; }Also applies to: 63-86
frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx (1)
52-58: 키보드 접근성 보완 및 대체 텍스트 정교화div+role="button"은 Enter/Space 키 처리가 필요합니다. 또한 아이콘이 일반 “공유” 아이콘이라면 alt를 더 일반적으로 표현하는 것이 좋습니다.
- <Styled.ShareButtonContainer - onClick={handleShare} - role='button' - aria-label='카카오톡으로 동아리 정보 공유하기' - > - <img src={ShareIcon} alt='카카오톡 공유' /> + <Styled.ShareButtonContainer + onClick={handleShare} + role='button' + tabIndex={0} + aria-label='카카오톡으로 동아리 정보 공유하기' + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleShare(); + } + }} + > + <img src={ShareIcon} alt='공유하기' />
🧹 Nitpick comments (65)
frontend/src/pages/AdminPage/AdminPage.styles.ts (2)
9-17: Divider 명칭 충돌 및 헤더 높이(98px) 하드코딩 중복 — 명확한 네이밍+CSS 변수로 통일 권장SideBar.styles.ts에도
Divider가 있어 혼동 여지가 큽니다. 또한98px이 여러 파일에서 반복됩니다.VerticalDivider같은 구체적 이름과--header-height변수 사용으로 중복/결합도를 낮추세요. As per coding guidelines.적용 예시:
-export const Divider = styled.div` - position: sticky; - top: 98px; - width: 1px; - height: calc(100vh - 98px); - background-color: #dcdcdc; - margin: 0 34px; - flex-shrink: 0; -`; +export const VerticalDivider = styled.div` + position: sticky; + top: var(--header-height, 98px); + width: 1px; + height: calc(100vh - var(--header-height, 98px)); + background-color: #dcdcdc; + margin: 0 34px; + flex-shrink: 0; +`;추가로 전역 스타일 또는 상위 컨테이너에
--header-height: 98px;를 선언해 두면 재사용이 쉬워집니다.
23-23: 패딩값 변경 OK. 다만 테마/토큰화 고려 권장하드코딩 대신 theme spacing 또는 CSS 변수로 관리하면 일관성 유지와 리팩토링이 쉬워집니다.
frontend/src/pages/AdminPage/AdminPage.tsx (2)
8-8: 사용되지 않는 Divider import 제거
<Styled.Divider />(또는 제안된<Styled.VerticalDivider />)만 사용되고 있어 해당 import는 불필요합니다.다음과 같이 삭제하세요:
-import { Divider } from './components/SideBar/SideBar.styles';
29-29: Styled.Divider 명확화: VerticalDivider로 이름 변경 권장동일한 이름의 Divider가 여러 곳에 존재해 혼동됩니다. styles에서의 제안(VerticalDivider)과 맞춰 사용을 갱신하세요. As per coding guidelines.
- <Styled.Divider /> + <Styled.VerticalDivider />frontend/src/pages/AdminPage/components/SideBar/SideBar.styles.ts (2)
11-13: 헤더 높이(98px) 하드코딩 중복 — CSS 변수로 통일AdminPage.styles.ts와 중복됩니다.
--header-height를 공통 사용하면 유지보수가 수월합니다.- top: 98px; + top: var(--header-height, 98px);
40-46: Divider → SidebarSectionDivider로 일관되게 리네이밍 및 사용처 업데이트SideBar.styles.ts에서
Divider를SidebarSectionDivider로 변경하고, AdminPage.styles.ts와 AdminPage.tsx의 import 및 JSX 사용(Styled.Divider→Styled.SidebarSectionDivider)도 함께 수정하세요.frontend/src/store/useCategoryStore.ts (1)
28-31: Zustand 셀렉터를 하나로 묶어 불필요 렌더링 방지동일 컴포넌트에서 셀렉터를 두 번 호출하면 각각의 변경에 반응해 렌더가 2회 발생할 수 있습니다. 하나의 셀렉터로 묶고 shallow 비교를 적용해 렌더를 최소화하세요.
아래처럼 변경을 제안합니다.
+import { shallow } from 'zustand/shallow'; @@ - const selectedCategory = useCategoryStore((state) => state.selectedCategory); - const setSelectedCategory = useCategoryStore((state) => state.setSelectedCategory); - return { selectedCategory, setSelectedCategory }; + const { selectedCategory, setSelectedCategory } = useCategoryStore( + (state) => ({ + selectedCategory: state.selectedCategory, + setSelectedCategory: state.setSelectedCategory, + }), + shallow, + ); + return { selectedCategory, setSelectedCategory };As per coding guidelines
frontend/package.json (1)
41-41: styled-components 패치 픽업 확인6.1.14 → 6.1.15에서 React 19 관련 보완이 있으니, 락파일이 6.1.15를 해석하도록 업데이트 되었는지 확인해주세요(의존성 설치 시 ^ 범위로 자동 픽업되지만, 기존 lock이 고정돼 있으면 갱신 필요).
Based on learnings
frontend/src/assets/images/introduce/features/index.ts (1)
13-25: 데이터 불변성 및 타입 안정성 부여features 배열에 as const를 부여해 불변성과 리터럴 타입을 확보하면 사용처에서 오타/누락을 예방할 수 있습니다. 필요 시 FeatureItem 타입도 노출하세요.
-export const desktopFeatures = [ +export const desktopFeatures = [ { src: feature_category_mockup_desktop, alt: '분과별 카테고리' }, { src: feature_recruitment_mockup_desktop, alt: '모집 상태 확인' }, { src: feature_info_mockup_desktop, alt: '지원/정보 확인' }, { src: feature_introduction_mockup_desktop, alt: '동아리 소개' }, -]; +] as const; @@ -export const mobileFeatures = [ +export const mobileFeatures = [ { src: feature_category_mockup_mobile, alt: '분과별 카테고리' }, { src: feature_recruitment_mockup_mobile, alt: '모집 상태 확인' }, { src: feature_info_mockup_mobile, alt: '지원/정보 확인' }, { src: feature_introduction_mockup_mobile, alt: '동아리 소개' }, -]; +] as const;추가로 필요하다면 아래 타입을 외부에 노출할 수 있습니다:
export type FeatureItem = typeof desktopFeatures[number];frontend/src/pages/IntroducePage/components/BackgroundShapes.tsx (2)
7-53: 장식용 이미지 접근성 개선(스크린리더 노이즈 제거)배경 장식 이미지는 의미가 없으므로 alt를 빈 문자열로 두고 aria-hidden/role로 숨기는 것이 좋습니다. 또한 지연 로딩을 권장합니다.
- alt='Background Twist Left' + alt="" + aria-hidden="true" + role="presentation" + loading="lazy" + decoding="async" @@ - alt='Background Twist Right' + alt="" + aria-hidden="true" + role="presentation" + loading="lazy" + decoding="async" @@ - alt='Background Circle Small' + alt="" + aria-hidden="true" + role="presentation" + loading="lazy" + decoding="async" @@ - alt='Background Circle Large' + alt="" + aria-hidden="true" + role="presentation" + loading="lazy" + decoding="async"
7-53: 중복 애니메이션/사이즈 상수화 및 감소된 모션 선호 반영애니메이션 prop이 반복되므로 공통 상수/variants로 추출하고, 사용자 환경설정(prefers-reduced-motion)을 존중해 애니메이션을 끌 수 있게 하세요.
예시:
-import { motion } from 'framer-motion'; +import { motion, useReducedMotion } from 'framer-motion'; @@ -export const BackgroundTwistLeft = () => ( - <motion.img +const fadeScale = { + initial: { opacity: 0, scale: 0.5 }, + animate: { opacity: 1, scale: 1 }, +}; + +export const BackgroundTwistLeft = () => { + const reduce = useReducedMotion(); + return ( + <motion.img src={TwistLeft} width={496} height={439} - initial={{ opacity: 0, scale: 0.5 }} - animate={{ opacity: 1, scale: 1 }} - transition={{ duration: 1, ease: 'easeInOut' }} + initial={reduce ? undefined : fadeScale.initial} + animate={reduce ? undefined : fadeScale.animate} + transition={reduce ? undefined : { duration: 1, ease: 'easeInOut' }} /> -); +);}다른 컴포넌트에도 동일 패턴 적용 권장.
As per coding guidelinesfrontend/src/pages/IntroducePage/IntroducePage.styles.ts (1)
3-18: 색상/보더/패딩 매직 넘버를 토큰화#fff, #eee 등 매직 값은 테마 토큰 또는 CSS 변수로 치환해 일관성/다크모드 대응성을 높이세요. 패딩/간격 값도 공통 spacing 스케일을 권장합니다.
예시:
-export const IntroducePageHeader = styled.header` - width: 100%; - background: #fff; +export const IntroducePageHeader = styled.header` + width: 100%; + background: var(--color-bg, #fff); @@ -export const IntroducePageFooter = styled.footer` - background: #fff; - border-top: 1px solid #eee; +export const IntroducePageFooter = styled.footer` + background: var(--color-bg, #fff); + border-top: 1px solid var(--color-border, #eee); @@ -export const Main = styled.main` - background: #fff; +export const Main = styled.main` + background: var(--color-bg, #fff);As per coding guidelines
frontend/src/styles/mediaQuery.ts (1)
2-14: 브레이크포인트/미디어 토큰에 불변성 부여 및 타입 안정성 향상as const를 적용해 키/값을 리터럴로 고정하면 오타와 타입 오류를 줄일 수 있습니다.
-export const BREAKPOINT = { +export const BREAKPOINT = { mini_mobile: 375, // ≤ 375 mobile: 500, // ≤ 500 tablet: 700, // ≤ 700 laptop: 1280, // ≤ 1280 -}; +} as const; @@ -export const media = { +export const media = { mini_mobile: `@media (max-width: ${BREAKPOINT.mini_mobile}px)`, mobile: `@media (max-width: ${BREAKPOINT.mobile}px)`, tablet: `@media (max-width: ${BREAKPOINT.tablet}px)`, laptop: `@media (max-width: ${BREAKPOINT.laptop}px)`, // 1281px 이상은 기본 스타일 (desktop) -}; +} as const;As per coding guidelines
frontend/src/hooks/useDevice.ts (1)
5-11: SSR 안전성: 초기 렌더에서 window 참조 회피SSR 환경에서
useState(window.innerWidth)는 ReferenceError를 유발합니다. lazy initializer로 가드하고, 마운트 시 1회 동기화하세요.- const [width, setWidth] = useState(window.innerWidth); + const [width, setWidth] = useState(() => + typeof window === 'undefined' ? BREAKPOINT.laptop + 1 : window.innerWidth, + ); @@ - useEffect(() => { - const onResize = () => setWidth(window.innerWidth); - window.addEventListener('resize', onResize); - return () => window.removeEventListener('resize', onResize); - }, []); + useEffect(() => { + if (typeof window === 'undefined') return; + const onResize = () => setWidth(window.innerWidth); + onResize(); // 마운트 시 현재 폭 동기화 + window.addEventListener('resize', onResize); + return () => window.removeEventListener('resize', onResize); + }, []);SSR을 사용하지 않는 SPA라면 영향은 없지만, 공용 훅이므로 안전 가드를 권장합니다. As per coding guidelines
frontend/src/pages/IntroducePage/components/sections/3.QuestionSection/QuestionSection.styles.ts (3)
5-27: 섹션 패딩 매직 넘버 토큰화150/120/100/80px 등 패딩 값을 공통 spacing 스케일(예: theme.spacing.xl/lg/md/sm)로 치환해 유지보수성과 일관성을 높이세요.
As per coding guidelines
29-48: 타이포 스케일/컬러 토큰 사용 권장font-size 30/26/22/18px, color #333 같은 매직 값은 타이포/컬러 토큰으로 대체하면 다크모드 및 전역 스타일 변경이 용이합니다.
As per coding guidelines
50-77: 장식용 배경 요소 접근성 및 퍼포먼스 고려BackgroundQuestionMark는 장식 목적이므로 사용 컴포넌트에서
aria-hidden을 부여해 보조기기의 노출을 막는 것을 권장합니다. 또한 blur(19px)는 비용이 크니 모바일에서 강도를 낮추거나 이미지 자산으로 대체 가능성을 검토해주세요.As per coding guidelines
frontend/src/pages/MainPage/MainPage.tsx (3)
24-31: 탭 상태가 실제 쿼리에 반영되지 않습니다division이 'all'로 고정되어 있고, active 탭 변경은 useGetCardList 인자에 영향을 주지 않아 현재 UI만 바뀌고 데이터는 그대로입니다. 의도라면 OK, 아니라면 DivisionKey를 정의해 탭과 division을 연결하세요. TODO와도 일치하는 작업입니다.
As per coding guidelines
67-77: 중첩 3항 연산자 → if/else(IIFE)로 가독성 개선가독성 향상을 위해 IIFE로 치환을 권장합니다.
As per coding guidelines
- {isLoading ? ( - <Spinner /> - ) : isEmpty ? ( - <Styled.EmptyResult> - 앗, 조건에 맞는 동아리가 없어요. - <br /> - 다른 키워드나 조건으로 다시 시도해보세요! - </Styled.EmptyResult> - ) : ( - <Styled.CardList>{clubList}</Styled.CardList> - )} + {(() => { + if (isLoading) return <Spinner />; + if (isEmpty) { + return ( + <Styled.EmptyResult> + 앗, 조건에 맞는 동아리가 없어요. + <br /> + 다른 키워드나 조건으로 다시 시도해보세요! + </Styled.EmptyResult> + ); + } + return <Styled.CardList>{clubList}</Styled.CardList>; + })()}
58-65: 탭 ARIA 롤/키보드 접근성 보완 권장역할/선택 상태를 명시해 키보드 내비게이션 친화적으로 만드세요.
As per coding guidelines
- <Styled.SectionTabs> + <Styled.SectionTabs role="tablist" aria-label="분과 탭"> {tabs .map((tab) =>( - <Styled.Tab key={tab} $active={active===tab} onClick={() => setActive(tab)}> + <Styled.Tab + key={tab} + $active={active===tab} + role="tab" + aria-selected={active===tab} + tabIndex={active===tab ? 0 : -1} + onClick={() => setActive(tab)} + > {tab} </Styled.Tab> ))} </Styled.SectionTabs>frontend/src/constants/CLUB_UNION_INFO.ts (1)
11-22: 아이콘 키 타입 안전성 강화 제안inactiveCategoryIcons가 Record<string, string>라 키 오타를 컴파일 타임에 잡지 못합니다. 아이콘 모듈에서 as const와 키 타입을 export하여 참조 측에서 keyof로 제한해 주세요.
추가(아이콘 모듈 수정 예시, 파일 외):
// frontend/src/assets/images/icons/category_button/index.ts export const inactiveCategoryIcons = { all: iconAll, volunteer: iconVolunteer, religion: iconReligion, hobby: iconHobby, study: iconStudy, sport: iconSport, performance: iconPerformance, } as const; export const activeCategoryIcons = { all: iconAllActive, volunteer: iconVolunteerActive, religion: iconReligionActive, hobby: iconHobbyActive, study: iconStudyActive, sport: iconSportActive, performance: iconPerformanceActive, } as const; export type CategoryIconKey = keyof typeof inactiveCategoryIcons;그 후 이 파일/사용처에서 CategoryIconKey를 활용해 키를 제한하세요.
frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx (1)
8-22: 카테고리 type을 아이콘 키 유니온으로 제한type: string은 안전하지 않습니다. 아이콘 레코드의 키 유니온으로 제한하여 오타를 방지하세요.
As per coding guidelines
interface Category { id: string; name: string; - type: string; + type: keyof typeof inactiveCategoryIcons; }frontend/src/pages/IntroducePage/components/sections/6.ConvenienceSection/ConvenienceSection.tsx (1)
22-56: 중복 렌더링을 map으로 단순화카드 4개 렌더링을 인덱스 기반 map으로 줄여 유지보수성 향상을 권장합니다.
- <Styled.FeatureGrid> - <Styled.Card1 - src={features[0].src} - alt={features[0].alt} - variants={cardVariants.top} - initial='hidden' - whileInView='show' - viewport={VIEWPORT_CONFIG} - /> - <Styled.Card2 - src={features[1].src} - alt={features[1].alt} - variants={cardVariants.left} - initial='hidden' - whileInView='show' - viewport={VIEWPORT_CONFIG} - /> - <Styled.Card3 - src={features[2].src} - alt={features[2].alt} - variants={cardVariants.right} - initial='hidden' - whileInView='show' - viewport={VIEWPORT_CONFIG} - /> - <Styled.Card4 - src={features[3].src} - alt={features[3].alt} - variants={cardVariants.bottom} - initial='hidden' - whileInView='show' - viewport={VIEWPORT_CONFIG} - /> - </Styled.FeatureGrid> + <Styled.FeatureGrid> + {features.map((f, i) => { + const Cards = [Styled.Card1, Styled.Card2, Styled.Card3, Styled.Card4] as const; + const variants = [cardVariants.top, cardVariants.left, cardVariants.right, cardVariants.bottom] as const; + const Card = Cards[i]; + return ( + <Card + key={f.alt} + src={f.src} + alt={f.alt} + variants={variants[i]} + initial='hidden' + whileInView='show' + viewport={VIEWPORT_CONFIG} + /> + ); + })} + </Styled.FeatureGrid>frontend/src/components/ClubTag/ClubTag.tsx (1)
3-19: 타입 안전성 강화: 색상 맵/props를 유니온으로 제한TagColors를 as const로, type을 keyof로 제한하면 오타를 컴파일 타임에 방지할 수 있습니다.
-const TagColors: Record<string, string> = { +const TagColors = { 중동: 'rgba(230, 247, 255, 1)', 봉사: 'rgba(255, 187, 187, 0.5)', 종교: 'rgba(255, 230, 147, 0.5)', 취미교양: 'rgba(159, 220, 214, 0.48)', 학술: 'rgba(177, 189, 241, 0.5)', 운동: 'rgba(253, 173, 60, 0.4)', 공연: 'rgba(205, 241, 165, 0.5)', 자유: 'rgba(237, 237, 237, 0.8)', -}; +} as const; -interface TagProps { - type: string; +type TagType = keyof typeof TagColors; +interface TagProps { + type: TagType; children?: React.ReactNode; className?: string; } const StyledTag = styled.span<{ color: string }>` @@ -const ClubTag = ({ type, children, className }: TagProps) => { +const ClubTag = ({ type, children, className }: TagProps) => { const backgroundColor = TagColors[type] || 'rgba(237, 237, 237, 1)'; return ( <StyledTag color={backgroundColor} className={className} >{`#${children || type}`}</StyledTag> ); }Also applies to: 31-38
frontend/src/pages/IntroducePage/components/sections/7.ContactSection/ContactSection.tsx (1)
24-35: 장식용 배경 이미지의 대체 텍스트 처리해당 배경 도형은 장식용이라면 스크린리더 노이즈를 줄이기 위해 alt="" 및 aria-hidden을 권장합니다(컴포넌트 정의부 변경).
추가(BackgroundShapes.tsx 수정 예시, 파일 외):
// frontend/src/pages/IntroducePage/components/BackgroundShapes.tsx export const BackgroundTwistLeft = () => ( <motion.img src={TwistLeft} width={496} height={439} alt="" aria-hidden="true" initial={{ opacity: 0, scale: 0.5 }} animate={{ opacity: 1, scale: 1 }} transition={{ duration: 1, ease: 'easeInOut' }} /> ); // 나머지 3개 컴포넌트도 동일하게 alt="" 및 aria-hidden="true" 적용As per coding guidelines
frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.styles.ts (3)
9-15: 중앙화된 미디어 토큰으로 교체 권장직접 숫자 브레이크포인트(500px, 360px)를 사용하고 있습니다. 프로젝트 전반에서 mediaQuery 유틸을 사용하는 것으로 보이니 동일하게 통일하면 유지보수성이 좋아집니다. 또한 background-color 등 색상도 테마 토큰을 고려해 주세요.
As per coding guidelines
- @media (max-width: 500px) { - margin: 16px 0 12px 0; - background-color: white; + ${media.mobile} { + margin: 16px 0 12px 0; + background-color: ${({theme}) => theme.colors.background?.default ?? '#fff'}; position: sticky; top: 56px; z-index: 1; } ... - @media (max-width: 500px) { + ${media.mobile} { width: 40px; height: 40px; } - @media (max-width: 360px) { + ${media.mini_mobile} { width: 23px; height: 23px; }Also applies to: 37-44
18-31: 버튼 키보드 접근성: focus-visible 스타일 추가 권장키보드 포커스 가시성이 정의되어 있지 않습니다. 사용자 에이전트 기본값을 덮어쓸 수 있으니 명시적 focus-visible 스타일을 추가해 주세요.
As per coding guidelines
export const CategoryButton = styled.button` display: flex; flex-direction: column; align-items: center; border: none; background: none; cursor: pointer; padding: 8px 0px; transition: transform 0.1s ease; + &:focus-visible { + outline: 2px solid currentColor; + outline-offset: 2px; + border-radius: 6px; + }
47-71: 색상/타이포 토큰화 권장span의 color, font-size, line-height가 하드코딩되어 있습니다. 테마/타이포 스케일과 색상 토큰을 사용하면 일관성과 다크모드 대응성이 좋아집니다.
As per coding guidelines
- font-size: 14px; + font-size: ${({theme}) => theme.typography?.labelSm?.fontSize ?? '14px'}; - color: #787878; + color: ${({theme}) => theme.colors.text?.secondary ?? '#787878'}; - line-height: 30px; + line-height: ${({theme}) => theme.typography?.labelSm?.lineHeight ?? '30px'};frontend/src/pages/IntroducePage/components/sections/5.FeatureSection/FeatureSection.tsx (2)
27-32: 검색 버튼 접근성 이름 명확화이미지 alt로도 이름이 전달되긴 하지만, 버튼에 aria-label을 직접 지정하면 더 명확합니다.
- <Styled.SearchButton> + <Styled.SearchButton aria-label='검색'> <img src={search_button_icon} alt='검색 아이콘' /> </Styled.SearchButton>
37-41: map key로 index 단독 사용 지양중복 배열을 펼치고 있어 index 단독 key는 재정렬 시 오동작 위험이 있습니다. 내용 기반의 안정적인 key를 사용하세요.
As per coding guidelines
- {[...tagsRow1, ...tagsRow1].map((tag, index) => ( - <Styled.CustomTag key={index} type={tag.type}> + {[...tagsRow1, ...tagsRow1].map((tag, index) => ( + <Styled.CustomTag key={`${tag.type}-${tag.label}-${index}`} type={tag.type}> {tag.label} </Styled.CustomTag> ))}그리고 동일하게 아래 tagsRow2에도 적용해 주세요.
Also applies to: 47-51
frontend/src/assets/images/icons/category_button/index.ts (1)
16-34: 아이콘 매핑 타입 안정성 강화Record<string, string>은 오탈자에 취약합니다. 키를 유니온 타입으로 한정하고 객체를 as const로 고정하면 안전합니다.
As per coding guidelines
+export type CategoryKey = + | 'all' + | 'volunteer' + | 'religion' + | 'hobby' + | 'study' + | 'sport' + | 'performance'; + -export const inactiveCategoryIcons : Record<string, string> = { - all : iconAll, - volunteer : iconVolunteer, - religion : iconReligion, - hobby : iconHobby, - study : iconStudy, - sport : iconSport, - performance : iconPerformance -} +export const inactiveCategoryIcons = { + all: iconAll, + volunteer: iconVolunteer, + religion: iconReligion, + hobby: iconHobby, + study: iconStudy, + sport: iconSport, + performance: iconPerformance, +} as const satisfies Record<CategoryKey, string>; -export const activeCategoryIcons : Record<string, string> = { - all : iconAllActive, - volunteer : iconVolunteerActive, - religion : iconReligionActive, - hobby : iconHobbyActive, - study : iconStudyActive, - sport : iconSportActive, - performance : iconPerformanceActive -} +export const activeCategoryIcons = { + all: iconAllActive, + volunteer: iconVolunteerActive, + religion: iconReligionActive, + hobby: iconHobbyActive, + study: iconStudyActive, + sport: iconSportActive, + performance: iconPerformanceActive, +} as const satisfies Record<CategoryKey, string>;frontend/src/pages/IntroducePage/components/sections/3.QuestionSection/QuestionSection.tsx (1)
12-14: 장식용 문자 스크린리더 제외배경의 물음표는 장식이므로 스크린리더에서 숨기는 것이 좋습니다.
- <Styled.BackgroundQuestionMark variants={fade}> + <Styled.BackgroundQuestionMark variants={fade} aria-hidden> ? </Styled.BackgroundQuestionMark>frontend/src/pages/IntroducePage/components/sections/1.IntroSection/IntroSection.tsx (1)
78-82: 배경 장식 이미지 접근성BackgroundShapes 내부 motion.img에 alt가 설정되어 있어 스크린리더가 읽을 수 있습니다. 장식 요소라면 해당 컴포넌트에서 alt=""와 aria-hidden을 적용해 주세요. 필요시 제가 패치 드릴 수 있습니다.
frontend/src/pages/IntroducePage/constants/mockData.ts (1)
47-67: 카테고리 타입 일치성 확보다른 곳에서 카테고리를 영문 슬러그로 관리(예: 'hobby', 'study')한다면, 한글/영문 문자열 혼용은 매핑 실수를 유발합니다. 카테고리 유니온 타입을 정의하고 전역적으로 재사용하는 것을 권장합니다.
As per coding guidelines
// 예) types/category.ts export type Category = | '운동' | '취미교양' | '공연' | '자유' | '봉사' | '학술' | '종교'; // 혹은 영문 슬러그로 통일하고 i18n으로 라벨 분리frontend/src/pages/MainPage/MainPage.styles.ts (2)
33-60: 탭 접근성 및 대비 개선
- 키보드 포커스 가시성 추가
- WCAG 대비(특히 모바일 14px에서 #787878)는 경계값입니다. 약간 더 진한 톤을 권장합니다.
As per coding guidelines
export const Tab = styled.button<{$active?: boolean}>` display: flex; position: relative; font-size: 24px; font-weight: bold; - color: ${({$active}) => $active ? '#787878' : '#DCDCDC'}; + color: ${({$active}) => $active ? '#6A6A6A' : '#D0D0D0'}; border: none; background: none; cursor: pointer; + &:focus-visible { + outline: 2px solid currentColor; + outline-offset: 3px; + border-radius: 4px; + } + &::after { content: ''; position: absolute; left: 0; bottom: -4px; width: 100%; height: 1.5px; background: #787878; border-radius: 1.5px; transform: ${({$active}) => $active ? 'scaleX(1)' : 'scaleX(0)'}; transform-origin: center; transition: transform 0.2s ease; } ${media.mobile} { - font-size: 14px + font-size: 14px; } `;
73-79: 미디어 쿼리 유틸 통일${media.laptop}와 별도의 하드코딩된 750px가 혼재합니다. 한 곳(mediaQuery 유틸)으로 통일하면 예측 가능성이 올라갑니다.
As per coding guidelines
- @media (max-width: 750px) { - grid-template-columns: repeat(1, 1fr); - } + ${media.tablet} { + grid-template-columns: repeat(1, 1fr); + }Also applies to: 81-84
frontend/src/pages/IntroducePage/components/sections/7.ContactSection/ContactSection.styles.ts (3)
58-89: 접근성: 키보드 포커스 표시를 명시적으로 추가하세요키보드 사용자에게 시각적 포커스가 보장되지 않을 수 있습니다. :focus-visible 스타일을 추가해 접근성을 확보하세요.
export const ContactButton = styled.a` display: inline-block; text-decoration: none; cursor: pointer; z-index: 1; background: #ffffff; color: #ff5414; font-size: 1rem; font-weight: bold; padding: 14px 64px; border: 1px solid #ff5414; border-radius: 50px; transition: all 0.3s ease; + &:focus-visible { + outline: 3px solid rgba(255, 84, 20, 0.5); + outline-offset: 2px; + } &:hover { background: #ff5414; color: #ffffff; }As per coding guidelines
91-103: 중복/일관성: Shape를 Intro 섹션과 동일 스펙으로 확장하거나 공통화하세요IntroSection의 Shape는 breakpoint별 위치 오버라이드를 지원합니다. 본 섹션의 Shape도 동일 옵션(laptop/tablet/mobile)과 scale 처리까지 지원하거나, 공통 파일로 추출해 중복/드리프트를 방지하세요.
-export const Shape = styled.div<{ - top?: string; - left?: string; - right?: string; - bottom?: string; -}>` - position: absolute; - z-index: 0; - top: ${({ top }) => top || 'auto'}; - left: ${({ left }) => left || 'auto'}; - right: ${({ right }) => right || 'auto'}; - bottom: ${({ bottom }) => bottom || 'auto'}; -`; +export const Shape = styled.div<{ + top?: string; + left?: string; + right?: string; + bottom?: string; + laptop?: { top?: string; left?: string; right?: string; bottom?: string }; + tablet?: { top?: string; left?: string; right?: string; bottom?: string }; + mobile?: { top?: string; left?: string; right?: string; bottom?: string }; +}>` + position: absolute; + z-index: 0; + top: ${({ top }) => top || 'auto'}; + left: ${({ left }) => left || 'auto'}; + right: ${({ right }) => right || 'auto'}; + bottom: ${({ bottom }) => bottom || 'auto'}; + + ${media.laptop} { + transform: scale(0.85); + top: ${({ laptop }) => laptop?.top || 'auto'}; + left: ${({ laptop }) => laptop?.left || 'auto'}; + right: ${({ laptop }) => laptop?.right || 'auto'}; + bottom: ${({ laptop }) => laptop?.bottom || 'auto'}; + } + + ${media.tablet} { + transform: scale(0.7); + top: ${({ tablet }) => tablet?.top || 'auto'}; + left: ${({ tablet }) => tablet?.left || 'auto'}; + right: ${({ tablet }) => tablet?.right || 'auto'}; + bottom: ${({ tablet }) => tablet?.bottom || 'auto'}; + } + + ${media.mobile} { + transform: scale(0.5); + top: ${({ mobile }) => mobile?.top || 'auto'}; + left: ${({ mobile }) => mobile?.left || 'auto'}; + right: ${({ mobile }) => mobile?.right || 'auto'}; + bottom: ${({ mobile }) => mobile?.bottom || 'auto'}; + } +`;
5-33: 디자인 토큰화: 반복 색상/수치 상수화 권장#ff5414, gap/height 픽셀 값 등 매직넘버가 다수 존재합니다. theme(ThemeProvider) 또는 상수로 추출해 섹션 간 일관성과 유지보수를 개선하세요. 특히 브랜드 컬러(#ff5414)와 배경 tint(rgba(255, 84, 20, 0.08))는 공통 토큰으로 정의하는 것을 권장합니다.
As per coding guidelines
Also applies to: 35-56
frontend/src/pages/IntroducePage/components/sections/5.FeatureSection/FeatureSection.styles.ts (3)
198-206: 엣지 그라디언트가 클릭 가로채기 가능성 있음 → 포인터 이벤트 비활성화TagWindow의 ::before/::after가 z-index:2로 덮여 있어 양 끝의 태그(클릭 가능 시)를 가로챌 수 있습니다. pointer-events: none을 추가해 상호작용을 방해하지 않도록 하세요.
해당 태그(ClubTag)가 클릭 가능 요소인지 확인 부탁드립니다. 클릭 가능하다면 아래 패치를 적용하세요.
&::before, &::after { content: ''; position: absolute; top: 0; width: 80px; height: 100%; z-index: 2; + pointer-events: none; }As per coding guidelines
Also applies to: 218-223, 225-230, 232-237
102-121: 접근성: 검색 버튼에 :focus-visible 스타일 추가키보드 탐색 시 포커스 표시가 부족합니다. :focus-visible을 추가해 접근성을 강화하세요.
export const SearchButton = styled.button` background: none; border: none; cursor: pointer; margin-left: 8px; + + &:focus-visible { + outline: 3px solid rgba(255, 84, 20, 0.5); + outline-offset: 2px; + border-radius: 6px; + } img { width: 20px; height: 20px;As per coding guidelines
169-189: 디자인 토큰/일관 단위 사용 권장
- 브랜드 컬러/보더 컬러/하이라이트(#ff5414 등)와 그림자, gap 값 등은 토큰화해 재사용성을 높이세요.
- 폰트 크기 단위(px, rem 혼용)를 일관화하세요(예: 전역 기준 rem 사용).
As per coding guidelines
Also applies to: 124-145, 31-76
frontend/src/pages/IntroducePage/constants/animations.ts (5)
1-1: 타입 보완: Viewport 타입을 함께 임포트VIEWPORT_CONFIG에 정확한 타입을 부여하려면 Viewport 타입을 임포트하세요.
-import type { Variants, Transition } from 'framer-motion'; +import type { Variants, Transition, Viewport } from 'framer-motion';As per coding guidelines
15-18: 일관된 타입 선언: fadeIn에 Variants 타입 지정다른 variants들과 동일하게 타입을 명시해 유지보수성을 높이세요.
-export const fadeIn = { +export const fadeIn: Variants = { hidden: { opacity: 0 }, show: { opacity: 1, transition: { duration: 0.5 } }, };As per coding guidelines
36-53: 타입 좁히기: cardVariants 키를 유니온으로 제한Record<string, Variants> 대신 명시적 키 유니온을 사용해 오타/누락을 방지하세요.
-export const cardVariants: Record<string, Variants> = { +export const cardVariants: Record<'left' | 'right' | 'top' | 'bottom', Variants> = { left: { hidden: { opacity: 0, x: -100 }, show: { opacity: 1, x: 0, transition: transDefault }, }, right: { hidden: { opacity: 0, x: 100 }, show: { opacity: 1, x: 0, transition: transDefault }, }, top: { hidden: { opacity: 0, y: -100 }, show: { opacity: 1, y: 0, transition: transDefault }, }, bottom: { hidden: { opacity: 0, y: 100 }, show: { opacity: 1, y: 0, transition: transDefault }, }, };As per coding guidelines
55-58: 타입 명시 및 상수화: VIEWPORT_CONFIG에 Viewport 타입 적용명시적 타입과 const 단언으로 안정성을 높이세요.
TS 4.9+에서 satisfies 사용 가능 여부를 확인해주세요.
-export const VIEWPORT_CONFIG = { - once: true, - amount: 0.2, -}; +export const VIEWPORT_CONFIG = { + once: true, + amount: 0.2, +} as const satisfies Viewport;As per coding guidelines
3-13: 선호도 고려: Reduced Motion 사용자 지원애니메이션 기본 지속시간, 스크롤 마퀴(duration: 20s) 등은 모션 민감 사용자에게 부담이 될 수 있습니다. useReducedMotion을 적용해 지속시간을 0으로 낮추는 분기(또는 variants 교체)를 고려하세요.
Based on learnings
Also applies to: 20-34
frontend/src/pages/IntroducePage/components/sections/2.ProblemSection/ProblemSection.styles.ts (1)
26-46: 디자인 토큰화 및 단위 일관성폰트, 마진, gap 등 수치가 섹션별로 매직넘버로 산재해 있습니다. 공통 토큰(폰트 스케일, 간격 스케일)을 도입하고 rem 기준으로 단위를 일관화하면 유지보수성이 크게 향상됩니다.
As per coding guidelines
Also applies to: 74-100
frontend/src/pages/IntroducePage/components/sections/4.CatchphraseSection/CatchphraseSection.styles.ts (2)
32-43: 장식 이미지의 상호작용 차단 및 접근성 고려배경 장식 이미지는 상호작용이 불필요합니다. pointer-events: none을 추가해 이벤트 가로채기를 방지하고, 실제 컴포넌트에서 alt=""(장식용) 또는 aria-hidden을 설정했는지 확인하세요.
export const BackgroundBrandImage = styled(motion.img)` position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 0; width: 100%; max-width: 1400px; opacity: 0.05; user-select: none; + pointer-events: none; `;
5-30: 브랜드 컬러/스페이싱 토큰화 권장배경색 rgba(255, 84, 20, 0.08)과 텍스트 컬러 #ff5414 등 반복 값은 theme로 승격해 재사용하세요. 섹션 전반의 padding/margin도 간격 스케일로 일관화하면 좋습니다.
As per coding guidelines
Also applies to: 53-70, 72-88
frontend/src/pages/IntroducePage/components/sections/6.ConvenienceSection/ConvenienceSection.styles.ts (1)
42-46: 이미지의 인라인 간격 제거: display: block 권장motion.img는 기본적으로 인라인 요소여서 하단에 여백이 생길 수 있습니다. display: block을 지정해 레이아웃 왜곡을 방지하세요.
export const FeatureCard = styled(motion.img)` + display: block; ${media.laptop} { width: 100%; } `;frontend/src/pages/IntroducePage/components/sections/1.IntroSection/IntroSection.styles.ts (3)
131-181: 접근성: CTA 버튼 :focus-visible 추가키보드 포커스 표시를 추가해 접근성을 개선하세요.
export const IntroButton = styled(motion.button)` display: flex; align-items: center; gap: 12px; padding: 14px 46px 14px 50px; background: #ff5414; color: #fff; font-weight: bold; font-size: 16px; border: none; border-radius: 50px; margin-top: 28px; cursor: pointer; + &:focus-visible { + outline: 3px solid rgba(255, 84, 20, 0.5); + outline-offset: 2px; + } + .icon { width: 14px; height: 14px; filter: brightness(0) invert(1); }As per coding guidelines
35-74: 색상/간격 토큰화 및 단위 일관성 권장
- #ff5414, rgba(255,84,20,…) 등 색상과 폰트/패딩 수치를 theme/토큰으로 관리해 다른 섹션과 일관성을 확보하세요.
- px/rem 혼용은 한 가지 기준(rem 권장)으로 통일을 고려하세요.
As per coding guidelines
Also applies to: 95-110, 112-129, 195-210
35-74: 공통 컴포넌트화 제안: Shape를 별도 공용 styled로 추출본 파일의 Shape는 다른 섹션(예: ContactSection)의 Shape와 스펙이 유사합니다. components/common/Shape.ts 등으로 추출해 재사용하면 유지보수성과 표현 일관성이 좋아집니다.
Also applies to: 91-103
frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.styles.ts (1)
4-9: 매직 넘버를 상수/테마 토큰으로 치환height(65px), padding(10px 40px), gap(16px), 색상 값 등이 하드코딩되어 있습니다. styled-components 테마 또는 파일 상단 상수로 추출해 가독성과 일관성을 높이세요.
As per coding guidelines
예시(파일 상단):
const FOOTER_HEIGHT = 65; const FOOTER_PADDING_Y = 10; const FOOTER_PADDING_X = 40; const FOOTER_GAP = 16; const FOOTER_BG = '#fff'; const FOOTER_BORDER = '#cdcdcd';적용:
- height: 65px; - padding: 10px 40px; + height: ${FOOTER_HEIGHT}px; + padding: ${FOOTER_PADDING_Y}px ${FOOTER_PADDING_X}px; - gap: 16px; + gap: ${FOOTER_GAP}px; - background-color: white; - border-top: 1px solid #cdcdcd; + background-color: ${FOOTER_BG}; + border-top: 1px solid ${FOOTER_BORDER};Also applies to: 11-17
frontend/src/utils/getDeadLineText.ts (1)
14-15: 임계값(365일) 상수화 및 상태 문자열 중복 제거 제안
- 365는 매직 넘버입니다. 상수로 추출하세요.
- '상시 모집' / '모집 마감' 문자열은 ClubApplyButton에서도 사용됩니다. 상수 모듈로 중앙집중화하세요.
As per coding guidelines
상수 선언(파일 상단 또는 공용 constants 모듈):
export const ALWAYS_OPEN_THRESHOLD_DAYS = 365; export const RECRUITMENT_STATUS = { ALWAYS: '상시 모집', CLOSED: '모집 마감', BEFORE: '모집 전', } as const;적용:
- if (days > 365) return '상시 모집'; + if (days > ALWAYS_OPEN_THRESHOLD_DAYS) return RECRUITMENT_STATUS.ALWAYS;frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.styles.ts (2)
31-34: 키보드 포커스 가시성 추가hover만 있고 focus 스타일이 없어 키보드 사용자 접근성이 떨어집니다.
:focus-visible스타일을 추가하세요.&:hover { background-color: #555; transform: scale(1.03); } + + &:focus-visible { + outline: 2px solid #2684FF; + outline-offset: 2px; + }
4-10: 매직 넘버/색상 토큰화여러 픽셀/색상 값이 하드코딩되어 있습니다. 테마 토큰 또는 파일 상단 상수로 추출해 유지보수성을 높이세요.
As per coding guidelines
Also applies to: 22-25, 41-43
frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx (2)
6-9: 미사용 prop 정리 제안
recruitmentForm가 인터페이스에 선언되어 있으나 컴포넌트에서 사용되지 않습니다. 사용 계획이 없다면 인터페이스 및 호출부(ClubDetailPage)에서 제거해 타입 일관성을 맞추세요.
4-4: 파일/식별자 네이밍 일관성
getDeadLineText(DeadLine) 네이밍이 일반적으로는getDeadlineText와 다릅니다. 전역적으로 일관된 표기를 권장합니다. 추후 리팩터 때 모듈/식별자명을 통일하세요.frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (5)
13-16: 상태 상수 중앙집중화 및 BEFORE 상태 추가 제안'상시 모집'/'모집 마감' 문자열이 utils에도 존재합니다. 공용 constants로 이동하고 '모집 전' 상태도 포함하면 분기 로직 일관성이 좋아집니다.
-const RECRUITMENT_STATUS = { - ALWAYS: '상시 모집', - CLOSED: '모집 마감', -}; +const RECRUITMENT_STATUS = { + ALWAYS: '상시 모집', + CLOSED: '모집 마감', + BEFORE: '모집 전', +} as const;
23-24: non-null 단언 대신 안전한 인자 전달
clubId!대신 빈 문자열로 전달해 훅 사용을 일관화하고 런타임 위험을 줄이세요. 동일 코드베이스의 다른 사용처(ClubDetailPage)와도 맞춥니다.- const { data: clubDetail } = useGetClubDetail(clubId!); + const { data: clubDetail } = useGetClubDetail(clubId || '');
49-65: 표시 텍스트 분기 일관화닫힘/사전 모집 상태 표기를 버튼 레이블에도 반영하면 UX가 명확해집니다.
const renderButtonContent = () => { - if (deadlineText === RECRUITMENT_STATUS.CLOSED) { - return RECRUITMENT_STATUS.CLOSED; - } + if (deadlineText === RECRUITMENT_STATUS.CLOSED) + return RECRUITMENT_STATUS.CLOSED; + if (deadlineText === RECRUITMENT_STATUS.BEFORE) + return RECRUITMENT_STATUS.BEFORE;
70-72: 버튼 type, 비활성/접근성 속성 추가폼 내 사용 시 의도치 않은 submit을 막기 위해
type="button"을 지정하고, 닫힘/사전 모집 시 disabled/aria-disabled 처리를 권장합니다.- <Styled.ApplyButton onClick={handleClick}> + <Styled.ApplyButton + type="button" + onClick={handleClick} + disabled={ + deadlineText === RECRUITMENT_STATUS.CLOSED || + deadlineText === RECRUITMENT_STATUS.BEFORE + } + aria-disabled={ + deadlineText === RECRUITMENT_STATUS.CLOSED || + deadlineText === RECRUITMENT_STATUS.BEFORE + } + > {renderButtonContent()} </Styled.ApplyButton>
58-62: 인라인 스타일 제거 제안
<span style={{ ... }}>|</span>인라인 스타일은 스타일 파일로 이전해 재사용/일관성을 높이세요.As per coding guidelines
| font-weight: 600; | ||
| color: #3a3a3a; | ||
| `; | ||
| ``; |
There was a problem hiding this comment.
문법 오류: 불필요한 백틱 제거 필요
Styled 컴포넌트 정의 뒤의 "``;"는 문법 에러를 유발합니다. 제거하세요.
-``;📝 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.
| ``; |
🤖 Prompt for AI Agents
In frontend/src/components/ClubTag/ClubTag.tsx around line 29, there is an
extraneous backtick sequence ("``;") after the styled-component definition
causing a syntax error; remove the stray backticks and trailing semicolon so the
file ends with the styled-component export/definition only, and run a quick
TypeScript/ESLint check to confirm the syntax error is resolved.
| img { | ||
| font-size: 12px; | ||
| font-weight: 600; | ||
| } |
There was a problem hiding this comment.
불필요한 img 타이포그래피 스타일 제거
img에 font-size, font-weight는 효과가 없습니다. 제거하세요.
- img {
- font-size: 12px;
- font-weight: 600;
- }📝 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.
| img { | |
| font-size: 12px; | |
| font-weight: 600; | |
| } | |
| // (Lines 36–39 removed: the entire `img { font-size: 12px; font-weight: 600; }` block) |
🤖 Prompt for AI Agents
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.styles.ts
around lines 36 to 39: remove the unnecessary typography rules from the img
selector — delete the font-size and font-weight properties under img so the
selector only contains relevant styles (or remove the entire img block if it
becomes empty).
| if (!clubId || !clubDetail) return; | ||
|
|
There was a problem hiding this comment.
컴포넌트는 null을 반환
return; 대신 return null;로 명시하세요.
- if (!clubId || !clubDetail) return;
+ if (!clubId || !clubDetail) return null;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (!clubId || !clubDetail) return; | |
| if (!clubId || !clubDetail) return null; |
🤖 Prompt for AI Agents
In
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
around lines 25-26, the component currently uses a bare `return;` which returns
undefined; change it to explicitly `return null;` so the React component returns
a valid null render value; update the line to `return null;` and, if applicable,
adjust the component's TypeScript return type to allow null (e.g.,
React.ReactElement | null).
| if (!clubDetail) return; | ||
|
|
There was a problem hiding this comment.
컴포넌트는 undefined 대신 null을 반환하세요
React 컴포넌트에서 return;은 undefined를 반환합니다. 명시적으로 null을 반환하는 편이 타입/일관성 측면에서 안전합니다.
- if (!clubDetail) return;
+ if (!clubDetail) return null;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (!clubDetail) return; | |
| if (!clubDetail) return null; |
🤖 Prompt for AI Agents
In frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx
around lines 19-20, the component currently uses "if (!clubDetail) return;"
which returns undefined; change this to explicitly return null (e.g., "if
(!clubDetail) return null;") so the component consistently returns a valid React
node and aligns with typing/consistency expectations.
| {floatingClubs.map((club, index) => ( | ||
| <Styled.CardImage | ||
| key={club.id} | ||
| variants={fadeUp} | ||
| {...cardPositions[index]} | ||
| > | ||
| <ClubCard club={club} /> | ||
| </Styled.CardImage> | ||
| ))} |
There was a problem hiding this comment.
cardPositions 인덱스 안전성 보장
floatingClubs 길이가 cardPositions보다 길어질 경우 스프레드에 undefined가 들어가 런타임 오류 위험이 있습니다. 모듈로 인덱싱 등으로 방어해 주세요.
As per coding guidelines
- {floatingClubs.map((club, index) => (
+ {floatingClubs.map((club, index) => {
+ const pos = cardPositions[index % cardPositions.length];
+ return (
<Styled.CardImage
key={club.id}
variants={fadeUp}
- {...cardPositions[index]}
+ {...pos}
>
<ClubCard club={club} />
</Styled.CardImage>
- ))}
+ )})}📝 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.
| {floatingClubs.map((club, index) => ( | |
| <Styled.CardImage | |
| key={club.id} | |
| variants={fadeUp} | |
| {...cardPositions[index]} | |
| > | |
| <ClubCard club={club} /> | |
| </Styled.CardImage> | |
| ))} | |
| {floatingClubs.map((club, index) => { | |
| const pos = cardPositions[index % cardPositions.length]; | |
| return ( | |
| <Styled.CardImage | |
| key={club.id} | |
| variants={fadeUp} | |
| {...pos} | |
| > | |
| <ClubCard club={club} /> | |
| </Styled.CardImage> | |
| ); | |
| })} |
🤖 Prompt for AI Agents
In
frontend/src/pages/IntroducePage/components/sections/1.IntroSection/IntroSection.tsx
around lines 108 to 116, the code spreads cardPositions[index] which can be
undefined if floatingClubs is longer than cardPositions; make indexing safe by
computing a guarded position (e.g., use index % cardPositions.length or fallback
to a default position object) and spread that instead so undefined is never
passed and runtime errors are avoided.
| ${media.mobile} { | ||
| width: 90vw; | ||
| margin: 0 auto 80px; | ||
| padding: 0 20px; | ||
| } |
There was a problem hiding this comment.
모바일 가로 스크롤 위험: width 90vw + padding 40px 조합
글로벌 box-sizing이 border-box가 아니라면 90vw + 40px로 뷰포트 초과 가능성이 큽니다. 모바일에서 가로 스크롤이 생길 수 있으니 width: 100%와 box-sizing을 지정하는 편이 안전합니다.
${media.mobile} {
- width: 90vw;
+ width: 100%;
margin: 0 auto 80px;
- padding: 0 20px;
+ padding: 0 20px;
+ box-sizing: border-box;
}As per coding guidelines
📝 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.
| ${media.mobile} { | |
| width: 90vw; | |
| margin: 0 auto 80px; | |
| padding: 0 20px; | |
| } | |
| ${media.mobile} { | |
| width: 100%; | |
| margin: 0 auto 80px; | |
| padding: 0 20px; | |
| box-sizing: border-box; | |
| } |
🤖 Prompt for AI Agents
In
frontend/src/pages/IntroducePage/components/sections/2.ProblemSection/ProblemSection.styles.ts
around lines 19-23, the mobile rule uses width: 90vw plus horizontal padding
which can exceed the viewport and cause horizontal scrolling if box-sizing isn't
border-box; change the rule to use width: 100% (or width: calc(100% - 40px) if
you must keep padding) and set box-sizing: border-box for this selector (or
ensure a global border-box is applied) so padding is included in the width and
prevents overflow.
| ${media.mobile} { | ||
| font-size: 14px | ||
| } |
There was a problem hiding this comment.
세미콜론 누락
CSS 속성 뒤에 세미콜론이 빠졌습니다.
- font-size: 14px
+ font-size: 14px;📝 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.
| ${media.mobile} { | |
| font-size: 14px | |
| } | |
| ${media.mobile} { | |
| font-size: 14px; | |
| } |
🤖 Prompt for AI Agents
In frontend/src/pages/MainPage/MainPage.styles.ts around lines 57 to 59, the CSS
rule inside the mobile media query is missing a semicolon after the font-size
declaration; add a trailing semicolon to the font-size line (font-size: 14px;)
and verify surrounding CSS rules follow the project's formatting conventions.
📝작업 내용
🚀
[feature] 글 영역 박스를 제거하고 레이아웃 구분선을 추가한다 #738
[feature] 메인페이지 배너를 변경한다 #741
[feature] 모바일 상세페이지 Footer UI 변경 #744
[feature] 사용자 지원서에 믹스패널 이벤트를 추가한다 #746
[feature] 카테고리 버튼을 리디자인하고 선택 효과를 적용한다 #751
[feature] 모아동 소개 페이지 HTML 전환 + 애니메이션/반응형 적용 (framer-motion, useDevice 훅 도입) #752
논의하고 싶은 부분(선택)
🫡 참고사항
Summary by CodeRabbit
New Features
Style
Chores
Tests