Skip to content

Comments

[release] FE v1.1.2 릴리즈#764

Merged
oesnuj merged 99 commits intomainfrom
develop-fe
Oct 12, 2025
Merged

[release] FE v1.1.2 릴리즈#764
oesnuj merged 99 commits intomainfrom
develop-fe

Conversation

@oesnuj
Copy link
Member

@oesnuj oesnuj commented Sep 26, 2025

📝작업 내용

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

🚀

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

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

🫡 참고사항

Summary by CodeRabbit

  • New Features

    • 소개 페이지 전면 리뉴얼: 다중 섹션, 애니메이션, 배경 요소, 반응형 카드·태그 추가
    • 메인 페이지 탭 네비게이션 도입(중앙동아리)
    • 카테고리 버튼 활성/비활성 아이콘 지원
    • 클럽 상세 하단 고정 푸터 및 지원 버튼 영역 개편, 공유 버튼 추가
    • 관리자 페이지 사이드바 고정 및 구분선 추가
    • 기기별 반응형 지원 강화(브레이크포인트 확장)
  • Style

    • 카테고리 버튼 크기·폰트·간격 조정, 레이아웃 패딩/정렬 개선
    • 공유 아이콘 교체, 일부 여백/보더 정리
  • Chores

    • 애니메이션 라이브러리 의존성 추가
  • Tests

    • 소개 페이지 테스트 제거

oesnuj and others added 30 commits September 6, 2025 17:25
애니메이션 구현을 위해 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로 번들 크기 최적화
- 이벤트 추적 위치 개선 및 수직선 구분자 추가
- 지원하기 버튼, 지원서 제출 성공 및 실패 로깅
suhyun113 and others added 6 commits September 25, 2025 19:41
…us-and-move-central-tab-MOA-259

[feature] 모집 상태 보기 기능을 제거하고 중앙동아리 태그를 이동한다
…n-MOA-197

[feature] 모아동 소개 페이지 HTML 전환 + 애니메이션/반응형 적용 (framer-motion, useDevice 훅 도입)
@vercel
Copy link

vercel bot commented Sep 26, 2025

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

Project Deployment Preview Comments Updated (UTC)
moadong Ready Ready Preview Comment Sep 26, 2025 2:22pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 26, 2025

Warning

.coderabbit.yaml has a parsing error

The CodeRabbit configuration file in this repository has a parsing error and default settings were used instead. Please fix the error(s) in the configuration file. You can initialize chat with CodeRabbit to get help with the configuration file.

💥 Parsing errors (1)
Validation error: Invalid regex pattern for base branch. Received: "**" at "reviews.auto_review.base_branches[0]"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Walkthrough

이 PR은 Introduce 페이지를 프레이머 모션 기반의 다중 섹션 구조로 전환하고 관련 애니메이션/스타일/데이터/훅을 신규 추가합니다. 클럽 상세의 지원 버튼/푸터/공유 버튼 흐름과 스타일을 수정하고, 메인 페이지에 탭 UI를 도입하며 카테고리 아이콘 체계를 맵 기반으로 정리합니다. 이벤트 상수/트래킹과 반응형 유틸, 자산 정리를 포함합니다.

Changes

Cohort / File(s) Change Summary
Dependency
frontend/package.json
framer-motion 신규 의존성 추가.
API & Utils
frontend/src/apis/application/getApplication.ts, frontend/src/utils/getDeadLineText.ts
로그 메시지/세미콜론 등 사소한 포맷 변경과 에러 로그 현지화. 마감 텍스트: days > 365 시 ‘상시 모집’ 조기 반환 추가.
Analytics
frontend/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 UI
frontend/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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@oesnuj oesnuj added 💻 FE Frontend 📈 release 릴리즈 배포 labels Sep 26, 2025
@oesnuj oesnuj changed the title [release] [release] FE v1.1.2 Sep 26, 2025
@oesnuj oesnuj requested a review from Zepelown September 26, 2025 14:27
@oesnuj oesnuj changed the title [release] FE v1.1.2 [release] FE v1.1.2 릴리즈 Sep 26, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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에서 DividerSidebarSectionDivider로 변경하고, AdminPage.styles.ts와 AdminPage.tsx의 import 및 JSX 사용(Styled.DividerStyled.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 guidelines

frontend/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;
`;
``;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

문법 오류: 불필요한 백틱 제거 필요

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.

Suggested change
``;
🤖 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.

Copy link
Member Author

Choose a reason for hiding this comment

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

뭐시여

Comment on lines +36 to +39
img {
font-size: 12px;
font-weight: 600;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

불필요한 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.

Suggested change
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).

Comment on lines +25 to 26
if (!clubId || !clubDetail) return;

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

컴포넌트는 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.

Suggested change
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).

Comment on lines +19 to +20
if (!clubDetail) return;

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

컴포넌트는 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.

Suggested change
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.

Comment on lines +108 to +116
{floatingClubs.map((club, index) => (
<Styled.CardImage
key={club.id}
variants={fadeUp}
{...cardPositions[index]}
>
<ClubCard club={club} />
</Styled.CardImage>
))}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Suggested change
{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.

Comment on lines +19 to +23
${media.mobile} {
width: 90vw;
margin: 0 auto 80px;
padding: 0 20px;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

모바일 가로 스크롤 위험: 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.

Suggested change
${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.

Comment on lines +57 to +59
${media.mobile} {
font-size: 14px
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

세미콜론 누락

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.

Suggested change
${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.

Copy link
Contributor

@lepitaaar lepitaaar left a comment

Choose a reason for hiding this comment

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

릴리즈 가보죠 수고하셨습니다~!!

Copy link
Collaborator

@suhyun113 suhyun113 left a comment

Choose a reason for hiding this comment

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

릴리즈 해여~~~ 수고했어요☺️

Copy link
Member

@seongwon030 seongwon030 left a comment

Choose a reason for hiding this comment

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

릴리즈 갑시다

@oesnuj oesnuj merged commit 45dede7 into main Oct 12, 2025
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💻 FE Frontend 📈 release 릴리즈 배포

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants