Skip to content

[feature] 모바일 상세페이지 Footer UI 변경 #744

Merged
seongwon030 merged 19 commits intodevelop-fefrom
feature/#742-detaile-page-apply-button-position-MOA-234
Sep 21, 2025
Merged

[feature] 모바일 상세페이지 Footer UI 변경 #744
seongwon030 merged 19 commits intodevelop-fefrom
feature/#742-detaile-page-apply-button-position-MOA-234

Conversation

@seongwon030
Copy link
Member

@seongwon030 seongwon030 commented Sep 20, 2025

#️⃣연관된 이슈

ex) #742

📝작업 내용

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

상세페이지 푸터 일원화

  • 데드라인 컴포넌트를 제거하고 지원하기 버튼에 데드라인 텍스트를 포함시켰습니다.
  • 공유하기 버튼이 추가되었습니다.
  • 데스크탑 버전에서 지원하기 버튼과 카카오톡 공유 버튼을 제거했습니다.

데스크탑

스크린샷 2025-09-21 01 13 55

모바일

스크린샷 2025-09-21 01 14 49

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

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

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

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

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

🫡 참고사항

Summary by CodeRabbit

  • 신기능/개선

    • 지원 버튼에 마감 상태 표시("모집 마감") 및 마감 안내 텍스트 병기 기능 추가.
    • 1년 초과 모집의 경우 마감 텍스트가 "상시 모집"으로 표시됩니다.
  • UI/스타일

    • 공유 아이콘을 새 공용 아이콘으로 교체하고 공유 버튼 레이아웃 제약 완화.
    • 지원 버튼 스타일 및 푸터를 하단 고정(sticky) 레이아웃으로 개선.
  • 변경/리팩터

    • 클럽 상세 푸터가 모집 기간만 표시하도록 단순화(폼/연락처 노출 제거).
    • 일부 중복 공유 버튼 제거 및 공유 버튼 위치 통합.

@seongwon030 seongwon030 self-assigned this Sep 20, 2025
@seongwon030 seongwon030 added ✨ Feature 기능 개발 💻 FE Frontend labels Sep 20, 2025
@vercel
Copy link

vercel bot commented Sep 20, 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 21, 2025 2:37am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 20, 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

클럽 상세 페이지 하단 UI를 재구성하고 지원/공유 버튼의 위치·스타일·렌더 로직을 변경했으며, Footer 및 ApplyButton의 공개 props를 축소·재정의하고 데드라인 텍스트 처리와 일부 로그 메시지를 조정했습니다.

Changes

Cohort / File(s) Summary
페이지/하단 구조 및 API 호출 변경
frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx, frontend/src/apis/application/getApplication.ts
ClubDetailPage에서 데스크톱 ShareButton 제거; getApplication의 콘솔 에러 메시지 문구·세미콜론 스타일 변경(동작 불변).
Footer 인터페이스 및 스타일 변경
frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx, frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.styles.ts
ClubDetailFooterPropsrecruitmentPeriod만 받도록 축소(제거: recruitmentForm, presidentPhoneNumber), DeadlineBadge 제거, Footer를 하단에 고정(sticky)하는 스타일로 전환.
지원 버튼 리팩터링
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx, frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.styles.ts
deadlineText?: string prop 추가, 내부 로직 재구성(지원 가능/마감 판정 내재화), ShareButton 함께 렌더하도록 구조 변경, styled-components 기반 스타일 추가(ApplyButtonContainer, ApplyButton).
헤더/공유 버튼 조정
frontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.tsx, frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx, frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.styles.ts
헤더에서 ClubApplyButton 제거; ShareButton 아이콘을 KakaoIcon→ShareIcon으로 교체하고 컨테이너 width 제거, clubDetail 없을 시 조기 반환 추가 및 불필요한 import 제거.
데드라인 유틸 변경
frontend/src/utils/getDeadLineText.ts
남은 일수가 365일 초과면 '상시 모집'을 반환하는 분기 추가(시그니처 불변).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User as 사용자
  participant Page as ClubDetailPage
  participant Footer as ClubDetailFooter
  participant Apply as ClubApplyButton
  participant Share as ShareButton
  Note right of Page #DDDDFF: 페이지 렌더링 흐름

  User->>Page: 상세페이지 진입
  Page->>Footer: render(recruitmentPeriod)
  Footer->>Apply: render(deadlineText)

  User->>Apply: 지원 버튼 클릭
  Apply->>Apply: 입력값/deadlineText 확인
  alt 모집 마감(또는 CLOSED)
    Apply-->>User: "모집 마감" 표시 / 클릭 비활성 또는 alert
  else 모집 중
    Apply->>Share: 같은 컨테이너에서 ShareButton 렌더링(동시 노출)
    Apply-->>User: 내부 지원 페이지로 라우트(또는 외부 폼 오픈)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • lepitaaar
  • oesnuj
  • Zepelown

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning raw_summary를 보면 대부분의 변경은 푸터 UI 관련이지만 frontend/src/apis/application/getApplication.ts의 로깅 문구(영문→한글)와 세미콜론 추가처럼 기능적 관련이 적은 로깅/스타일 변경이 포함되어 있고 ClubDetailFooterProps에서 presidentPhoneNumber와 recruitmentForm을 제거한 것은 컴포넌트 API 변경으로 이슈에 명시되지 않았다면 범위밖일 가능성이 있습니다. 해결 권장: UI 관련 커밋만 이 PR에 남기고 getApplication.ts의 로깅 변경은 별도 PR로 분리하거나 변경 사유를 명확히 하며 ClubDetailFooter의 API 변경은 의도된 설계인지 팀과 확인하고 영향 범위를 조사해 호출부를 함께 수정하거나 호환성을 유지하는 방식으로 정리하십시오.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed 제목 "[feature] 모바일 상세페이지 Footer UI 변경"은 PR의 핵심 변경사항인 모바일 상세페이지 푸터 UI 개편, 지원하기 버튼 위치 변경 및 공유 버튼 추가를 간결하게 요약하고 있어 목적과 일치하며 불필요한 잡음이 없습니다.
Linked Issues Check ✅ Passed PR 변경사항은 MOA-234(상세페이지 지원하기 버튼 위치 변경)의 주요 목적과 일치합니다; 푸터 일원화, 데드라인 컴포넌트 제거 및 데드라인 텍스트를 지원하기 버튼에 포함, 공유 버튼 추가, 데스크탑에서 버튼 제거 등의 구현이 코드상 반영되어 있습니다.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/#742-detaile-page-apply-button-position-MOA-234

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@seongwon030 seongwon030 linked an issue Sep 20, 2025 that may be closed by this pull request
2 tasks
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: 0

Caution

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

⚠️ Outside diff range comments (3)
frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx (3)

16-16: 정적 import 남아있어 코드 분할/트리셰이킹 무력화. 반드시 제거 필요.

ShareButton을 더 이상 사용하지 않는데 정적 import가 남아 있어 관련 의존성이 번들에 항상 포함됩니다. PR 목표와 상충합니다.

-import ShareButton from '@/pages/ClubDetailPage/components/ShareButton/ShareButton';

37-43: 에러 처리 분기 순서 오류 (에러 UI가 절대 노출되지 않음).

현재는 !clubDetail이 먼저 반환되어 error가 있어도 null을 반환합니다. 에러 분기를 먼저 처리하세요.

-  if (!clubDetail) {
-    return null;
-  }
-
-  if (error) {
-    return <div>에러가 발생했습니다.</div>;
-  }
+  if (error) {
+    return <div>에러가 발생했습니다.</div>;
+  }
+
+  if (!clubDetail) {
+    return null;
+  }

1-83: ShareButton 정적 import 제거 및 Kakao SDK 지연 로딩 필요

  • 발견: ClubDetailPage가 ShareButton을 정적으로 import하고 있음 — frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx:16
  • ShareButton 내부에서 아이콘 정적 import 및 Kakao 직접 호출 존재 — frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx:4 (share_filled_icon.svg), 21~26 (window.Kakao / Kakao.Share)
  • 참고: ClubApplyButton은 동적 import로 처리됨 — frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx:66
  • Kakao SDK가 전역으로 로드됨(public/index.html) 및 init 호출 위치 — frontend/public/index.html:57, frontend/src/utils/initSDK.ts:52

권장 조치:

  • ClubDetailPage에서 ShareButton을 동적 import(React.lazy 또는 import())로 변경해 초기 번들에서 분리.
  • share_filled_icon.svg를 동적 로드하거나 inline SVG로 교체해 번들 영향 최소화.
  • public/index.html의 Kakao 스크립트 전역 포함을 제거하고, ShareButton 사용 시점에 스크립트 삽입 + initSDK로 초기화하도록 변경(전역 로딩이 의도라면 이유를 문서화).
🧹 Nitpick comments (10)
frontend/src/apis/application/getApplication.ts (2)

7-7: 에러 메시지 마지막 세미콜론이 의도치 않은 변경일 수 있음

Line 7에서 console.error 문의 마지막에 추가된 세미콜론은 실제로는 의미상 큰 변화는 없지만, 의도치 않은 변경이거나 단순한 포맷팅 수정일 수 있습니다.


16-16: 로그 메시지의 언어 일관성 검토가 필요함

Line 16에서 로그 메시지를 한국어로 변경한 것은 사용자 경험 측면에서는 좋지만, 기존 Line 7의 console.error는 여전히 영어로 남아있어 일관성이 부족합니다. 동적 import는 트리쉐이킹과 잘 작동한다고 알려져 있지만, 현재 PR의 ShareButton 동적 로딩 구현과 관련하여 로그 메시지의 일관성도 중요합니다.

Line 7의 에러 메시지도 한국어로 통일하는 것을 고려해보세요:

-      console.error(`Failed to fetch: ${response.statusText}`);
+      console.error(`지원서 조회 요청 실패: ${response.statusText}`);
frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx (2)

50-57: 키보드 접근성 보강 필요 (role='button' 사용 시 필수).

Space/Enter로 트리거 가능하도록 onKeyDown과 tabIndex를 추가해 주세요.

적용 diff:

-    <Styled.ShareButtonContainer
-      onClick={handleShare}
-      role='button'
-      aria-label='카카오톡으로 동아리 정보 공유하기'
-    >
+    <Styled.ShareButtonContainer
+      onClick={handleShare}
+      onKeyDown={(e) => {
+        if (e.key === 'Enter' || e.key === ' ') {
+          e.preventDefault();
+          handleShare();
+        }
+      }}
+      role='button'
+      tabIndex={0}
+      aria-label='카카오톡으로 동아리 정보 공유하기'
+    >

56-56: 아이콘과 대체 텍스트 의미 일치 제안.

일반 공유 아이콘이면 alt를 “공유하기 아이콘” 등으로 바꾸는 편이 명확합니다. 기능은 카카오 공유이므로 유지해도 되지만, UI 의미상 혼선을 줄이기 위해 권장합니다.

-      <img src={ShareIcon} alt='카카오톡 공유' />
+      <img src={ShareIcon} alt='공유하기 아이콘' />
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (4)

43-46: 불필요/무효한 스타일 제거.

img에 font-size/weight는 적용되지 않습니다. 불필요 rules 제거하세요.

-    img {
-      font-size: 12px;
-      font-weight: 600;
-    }

59-61: 타입 임포트 방식 개선 (React 네임스페이스 의존 제거).

React.ComponentType 대신 타입 전용 임포트를 사용하세요. 번들/TS 설정과 독립적으로 안전합니다.

-import { useState, useEffect } from 'react';
+import { useState, useEffect, type ComponentType } from 'react';

-  const [ShareButtonComponent, setShareButtonComponent] =
-    useState<React.ComponentType<{ clubId: string }> | null>(null);
+  const [ShareButtonComponent, setShareButtonComponent] =
+    useState<ComponentType<{ clubId: string }> | null>(null);

Also applies to: 9-9


64-71: 동적 로딩을 모바일에만 제한 + 예외 처리 추가.

PR 목적(모바일 전용 로딩)을 코드로 보장하고, 로딩 실패 시 안전하게 무시하세요.

-useEffect(() => {
-  if (deadlineText) {
-    import('../ShareButton/ShareButton').then((module) => {
-      setShareButtonComponent(() => module.default);
-    });
-  }
-}, [deadlineText]);
+useEffect(() => {
+  if (!deadlineText) return;
+  if (typeof window === 'undefined') return;
+  const isMobile = window.matchMedia('(max-width: 500px)').matches;
+  if (!isMobile) return;
+  import('../ShareButton/ShareButton')
+    .then((module) => setShareButtonComponent(() => module.default))
+    .catch(() => setShareButtonComponent(null));
+}, [deadlineText]);

80-89: 변수 섀도잉 제거 (가독성).

props의 deadlineText와 동일한 이름을 내부에서 다시 사용하고 있어 혼란을 줍니다.

-  const deadlineText = getDeadlineText(
+  const computedDeadlineText = getDeadlineText(
     recruitmentStart,
     recruitmentEnd,
     new Date(),
   );

-  if (deadlineText === '모집 마감') {
+  if (computedDeadlineText === '모집 마감') {
     alert(`현재 ${clubDetail.name} 동아리는 모집 기간이 아닙니다.`);
     return;
   }
frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx (1)

75-77: 불필요 prop 전달 제거 제안.

ClubDetailFooter에서 recruitmentForm를 사용하지 않습니다. 호출부에서 제거해 인터페이스를 간결화하세요.

-      <ClubDetailFooter
-        recruitmentPeriod={clubDetail.recruitmentPeriod}
-        recruitmentForm={clubDetail.recruitmentForm}
-      />
+      <ClubDetailFooter
+        recruitmentPeriod={clubDetail.recruitmentPeriod}
+      />
frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx (1)

6-11: Footer 인터페이스 정리 (사용하지 않는 prop 제거).

recruitmentForm를 받지만 사용하지 않습니다. 호출부와 함께 제거해 API 명확성을 높이세요.

 interface ClubDetailFooterProps {
   recruitmentPeriod: string;
-  recruitmentForm: string;
 }
 
-const ClubDetailFooter = ({ recruitmentPeriod }: ClubDetailFooterProps) => {
+const ClubDetailFooter = ({ recruitmentPeriod }: ClubDetailFooterProps) => {
   const { recruitmentStart, recruitmentEnd } =
     parseRecruitmentPeriod(recruitmentPeriod);
@@
   return (
     <Styled.ClubDetailFooterContainer>
-      <ClubApplyButton deadlineText={deadlineText} />
+      <ClubApplyButton deadlineText={deadlineText} />
     </Styled.ClubDetailFooterContainer>
   );

호출부 수정은 ClubDetailPage.tsx 코멘트의 diff 참조.

Also applies to: 21-24

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b1ee1de and 00ed254.

⛔ Files ignored due to path filters (1)
  • frontend/src/assets/images/icons/share_filled_icon.svg is excluded by !**/*.svg
📒 Files selected for processing (7)
  • frontend/src/apis/application/getApplication.ts (2 hunks)
  • frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx (1 hunks)
  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (3 hunks)
  • frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx (2 hunks)
  • frontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.tsx (1 hunks)
  • frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.styles.ts (0 hunks)
  • frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx (2 hunks)
💤 Files with no reviewable changes (1)
  • frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.styles.ts
🧰 Additional context used
📓 Path-based instructions (2)
frontend/**/*.{ts,tsx}

📄 CodeRabbit inference engine (frontend/.cursorrules)

frontend/**/*.{ts,tsx}: Replace magic numbers with named constants for clarity.
Replace complex or nested ternary operators with if/else statements or IIFEs for readability.
Assign complex boolean conditions to named variables.
Use consistent return types for similar functions and hooks.
Avoid hidden side effects; functions should only perform actions implied by their signature (Single Responsibility Principle).
Use unique, descriptive names for custom wrappers and functions to avoid ambiguity.
Define constants near related logic or ensure names link them clearly.

Files:

  • frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx
  • frontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.tsx
  • frontend/src/apis/application/getApplication.ts
  • frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx
  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
  • frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
frontend/**/*.tsx

📄 CodeRabbit inference engine (frontend/.cursorrules)

frontend/**/*.tsx: Abstract complex logic/interactions into dedicated components or higher-order components (HOCs).
Separate significantly different conditional UI/logic into distinct components.
Colocate simple, localized logic or use inline definitions to reduce context switching.
Choose field-level or form-level cohesion based on form requirements.
Break down broad state management into smaller, focused hooks or contexts.
Use component composition instead of props drilling.

Files:

  • frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx
  • frontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.tsx
  • frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx
  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
  • frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
🧠 Learnings (2)
📓 Common learnings
Learnt from: seongwon030
PR: Moadong/moadong#195
File: frontend/src/pages/AdminPage/AdminPage.tsx:7-7
Timestamp: 2025-03-19T05:18:07.818Z
Learning: AdminPage.tsx에서 현재 하드코딩된 클럽 ID('67d2e3b9b15c136c6acbf20b')는 로그인 기능 구현 후 동적으로 가져오는 방식으로 수정될 예정입니다.
📚 Learning: 2025-03-19T05:18:07.818Z
Learnt from: seongwon030
PR: Moadong/moadong#195
File: frontend/src/pages/AdminPage/AdminPage.tsx:7-7
Timestamp: 2025-03-19T05:18:07.818Z
Learning: AdminPage.tsx에서 현재 하드코딩된 클럽 ID('67d2e3b9b15c136c6acbf20b')는 로그인 기능 구현 후 동적으로 가져오는 방식으로 수정될 예정입니다.

Applied to files:

  • frontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.tsx
  • frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
🧬 Code graph analysis (1)
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (2)
frontend/src/hooks/queries/club/useGetClubDetail.ts (1)
  • useGetClubDetail (6-20)
frontend/src/constants/eventName.ts (1)
  • EVENT_NAME (1-19)
🔇 Additional comments (5)
frontend/src/apis/application/getApplication.ts (1)

14-15: 확인 완료 — FIXME 주석의 응답 구조(statuscode / message / data)와 (await response.json()).message 접근 방식은 코드베이스 관례와 일치합니다.

frontend/src/apis/auth/login.ts의 LoginResponse 인터페이스 및 다수의 API 파일들이 동일 패턴을 사용합니다.

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

16-16: 주석 추가 확인.

데스크탑 전용 의도가 주석으로 명확합니다. 헤더의 ClubApplyButtondeadlineText 미전달 상태이므로 공유 버튼 로드가 되지 않습니다. 모바일에서 Footer에도 Apply 버튼이 있어 이중 노출/중복 인터랙션이 없는지 UI 확인 부탁드립니다.

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

15-19: 마감 텍스트 계산 및 전달 흐름 LGTM.

현재 시각 기준으로 deadlineText 계산 후 버튼으로 일원화한 구조가 명확합니다.

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

22-33: SSR 우려 불필요 — CSR SPA이므로 window 사용 안전
moadong은 순수 React + react-router-dom 기반의 CSR SPA(저장된 학습 정보 기준)라 useState에서 window를 직접 참조해도 SSR 크래시 우려가 없습니다. 제안된 변경 불필요.

Likely an incorrect or invalid review comment.

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

107-107: clubId non-null 단언 사용 검증 필요

useParams로 받아온 clubId를 clubId!로 단언해 사용하고 있습니다. 라우트가 항상 '/:clubId'를 보장하는지 확인하십시오. 보장되지 않으면 clubId가 없을 때 조건부 렌더링/early return 또는 null-safe 검사로 변경하세요.
위치: frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx — ShareButtonComponent 렌더() 및 useGetClubDetail(clubId!) 호출부.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.styles.ts (1)

1-45: 스타일 구성이 잘 정의되었지만 매직넘버 개선이 필요합니다.

styled-components를 사용한 컴포넌트 스타일링이 적절하게 구현되어 있으나, 코딩 가이드라인에 따라 매직넘버들을 명명된 상수로 교체하는 것을 권장합니다.

다음과 같이 매직넘버를 상수로 분리하여 개선할 수 있습니다:

import styled from 'styled-components';

+const BUTTON_CONFIG = {
+  BORDER_RADIUS: '10px',
+  TRANSITION_DURATION: '0.2s',
+  PADDING_VERTICAL: '10px',
+  PADDING_HORIZONTAL: '40px',
+  WIDTH_DESKTOP: '517px',
+  WIDTH_MOBILE: '280px',
+  HEIGHT: '44px',
+  FONT_SIZE: '16px',
+  FONT_WEIGHT: 500,
+  GAP: '10px',
+  HOVER_SCALE: 1.03,
+  MOBILE_BREAKPOINT: '500px'
+} as const;
+
+const COLORS = {
+  BACKGROUND: '#3a3a3a',
+  BACKGROUND_HOVER: '#555',
+  TEXT: '#fff',
+  SEPARATOR: '#787878'
+} as const;

export const ApplyButtonContainer = styled.div`
  width: 100%;
  display: flex;
  flex-direction: row;
  justify-content: center;
  text-align: center;
-  gap: 10px;
+  gap: ${BUTTON_CONFIG.GAP};
`;

export const ApplyButton = styled.button`
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
-  border-radius: 10px;
+  border-radius: ${BUTTON_CONFIG.BORDER_RADIUS};
  cursor: pointer;
-  transition: transform 0.2s ease-in-out;
+  transition: transform ${BUTTON_CONFIG.TRANSITION_DURATION} ease-in-out;
-  background-color: #3a3a3a;
+  background-color: ${COLORS.BACKGROUND};

-  padding: 10px 40px;
+  padding: ${BUTTON_CONFIG.PADDING_VERTICAL} ${BUTTON_CONFIG.PADDING_HORIZONTAL};
-  width: 517px;
+  width: ${BUTTON_CONFIG.WIDTH_DESKTOP};
-  height: 44px;
+  height: ${BUTTON_CONFIG.HEIGHT};
-  font-size: 16px;
+  font-size: ${BUTTON_CONFIG.FONT_SIZE};
  font-style: normal;
-  font-weight: 500;
+  font-weight: ${BUTTON_CONFIG.FONT_WEIGHT};
-  color: #fff;
+  color: ${COLORS.TEXT};
  text-align: center;

  &:hover {
-    background-color: #555;
+    background-color: ${COLORS.BACKGROUND_HOVER};
-    transform: scale(1.03);
+    transform: scale(${BUTTON_CONFIG.HOVER_SCALE});
  }

  img {
    font-size: 12px;
    font-weight: 600;
  }

-  @media (max-width: 500px) {
+  @media (max-width: ${BUTTON_CONFIG.MOBILE_BREAKPOINT}) {
-    width: 280px;
+    width: ${BUTTON_CONFIG.WIDTH_MOBILE};
  }
`;
🧹 Nitpick comments (3)
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (3)

20-20: 조기 반환에서 null 반환 고려

현재 조기 반환에서 undefined를 반환하는데, 명시적으로 null을 반환하는 것이 React 컴포넌트의 관례에 더 적합합니다.

-  if (!clubId || !clubDetail) return;
+  if (!clubId || !clubDetail) return null;

36-39: 외부 링크 검증 로직 개선

외부 링크가 없을 때의 에러 메시지가 적절하지만, 링크 유효성을 더 엄격하게 검증하는 것을 고려해보세요.

      const externalFormLink = clubDetail.externalApplicationUrl?.trim();

-      if (!externalFormLink) {
+      if (!externalFormLink || !isValidUrl(externalFormLink)) {
-        alert('동아리 모집 정보를 확인해주세요.');
+        alert('동아리 지원 링크가 올바르지 않습니다. 동아리에 직접 문의해주세요.');
        return;
      }

URL 유효성 검사 헬퍼 함수 추가:

const isValidUrl = (url: string): boolean => {
  try {
    new URL(url);
    return true;
  } catch {
    return false;
  }
};

55-55: 인라인 스타일 대신 styled-components 사용 권장

구분자 스타일을 인라인으로 정의하는 대신, 스타일 파일에서 관리하는 것이 일관성 있는 접근법입니다.

ClubApplyButton.styles.ts에 구분자 컴포넌트 추가:

export const Separator = styled.span`
  margin: 0 8px;
  color: #787878;
`;

그리고 컴포넌트에서 사용:

-                <span style={{ margin: '0 8px', color: '#787878' }}>|</span>
+                <Styled.Separator>|</Styled.Separator>
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 00ed254 and 5b9cef1.

📒 Files selected for processing (7)
  • frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx (1 hunks)
  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.styles.ts (1 hunks)
  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (1 hunks)
  • frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.styles.ts (1 hunks)
  • frontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.tsx (0 hunks)
  • frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx (3 hunks)
  • frontend/src/utils/getDeadLineText.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • frontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx
  • frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
🧰 Additional context used
📓 Path-based instructions (2)
frontend/**/*.{ts,tsx}

📄 CodeRabbit inference engine (frontend/.cursorrules)

frontend/**/*.{ts,tsx}: Replace magic numbers with named constants for clarity.
Replace complex or nested ternary operators with if/else statements or IIFEs for readability.
Assign complex boolean conditions to named variables.
Use consistent return types for similar functions and hooks.
Avoid hidden side effects; functions should only perform actions implied by their signature (Single Responsibility Principle).
Use unique, descriptive names for custom wrappers and functions to avoid ambiguity.
Define constants near related logic or ensure names link them clearly.

Files:

  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.styles.ts
  • frontend/src/utils/getDeadLineText.ts
  • frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.styles.ts
  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
frontend/**/*.tsx

📄 CodeRabbit inference engine (frontend/.cursorrules)

frontend/**/*.tsx: Abstract complex logic/interactions into dedicated components or higher-order components (HOCs).
Separate significantly different conditional UI/logic into distinct components.
Colocate simple, localized logic or use inline definitions to reduce context switching.
Choose field-level or form-level cohesion based on form requirements.
Break down broad state management into smaller, focused hooks or contexts.
Use component composition instead of props drilling.

Files:

  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
🧠 Learnings (1)
📓 Common learnings
Learnt from: seongwon030
PR: Moadong/moadong#195
File: frontend/src/pages/AdminPage/AdminPage.tsx:7-7
Timestamp: 2025-03-19T05:18:07.818Z
Learning: AdminPage.tsx에서 현재 하드코딩된 클럽 ID('67d2e3b9b15c136c6acbf20b')는 로그인 기능 구현 후 동적으로 가져오는 방식으로 수정될 예정입니다.
🧬 Code graph analysis (1)
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (2)
frontend/src/hooks/queries/club/useGetClubDetail.ts (1)
  • useGetClubDetail (6-20)
frontend/src/constants/eventName.ts (1)
  • EVENT_NAME (1-19)
🔇 Additional comments (4)
frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.styles.ts (2)

4-9: 스티키 포지셔닝 구현이 적절합니다.

하단 고정 레이아웃을 위한 스티키 포지셔닝과 z-index 설정이 모바일 Footer UI의 의도된 동작에 적합합니다.


11-14: Flexbox 레이아웃 구현이 잘 되었습니다.

버튼들 사이의 공간 배치를 위한 justify-content: space-between과 중앙 정렬을 위한 align-items: center 설정이 적절합니다. 16px gap도 적절한 간격을 제공합니다.

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

14-14: 새로운 조건부 로직이 올바르게 추가되었습니다.

365일을 초과하는 모집 기간에 대해 '상시 모집' 텍스트를 반환하는 로직이 적절하게 구현되었습니다. 기존 로직을 건드리지 않고 추가된 것도 좋습니다.

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

9-11: 인터페이스 정의가 적절합니다.

deadlineText를 선택적 prop으로 정의한 인터페이스가 명확하고 적절합니다.

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.

UI가 깔끔해졌네요~ 수고하셨습니다!!!

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: 1

🧹 Nitpick comments (5)
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (5)

25-25: 가드 분기 반환값은 null로 명시

React 컴포넌트는 명시적으로 null을 반환하는 것이 안전합니다(일관된 반환 타입).

-  if (!clubId || !clubDetail) return;
+  if (!clubId || !clubDetail) return null;

30-31: 마감 여부 비교 반복 → isRecruitmentClosed 변수로 추출

조건을 변수로 이름 붙이면 가독성과 변경 용이성이 좋아집니다.

@@
-    if (deadlineText === RECRUITMENT_STATUS.CLOSED) {
+    const isRecruitmentClosed =
+      deadlineText === RECRUITMENT_STATUS.CLOSED;
+    if (isRecruitmentClosed) {
@@
-    if (deadlineText === RECRUITMENT_STATUS.CLOSED) {
-      return RECRUITMENT_STATUS.CLOSED;
-    }
+    if (isRecruitmentClosed) return RECRUITMENT_STATUS.CLOSED;

Also applies to: 50-51


59-60: 인라인 스타일은 상수/스타일드로 추출 권장

매직 값(#787878, margin) 분리를 통해 일관성·테마 연동성을 높이세요.

-            <span style={{ margin: '0 8px', color: '#787878' }}>|</span>
+            <span style={SEPARATOR_STYLE}>|</span>

추가: 파일 상단에 상수 배치

const SEPARATOR_STYLE = { margin: '0 8px', color: '#787878' } as const;

28-28: 트래킹에 속성 추가 제안

분석 품질 향상을 위해 clubId/clubName 등 컨텍스트를 함께 전송하세요.

-    trackEvent(EVENT_NAME.CLUB_APPLY_BUTTON_CLICKED);
+    trackEvent(EVENT_NAME.CLUB_APPLY_BUTTON_CLICKED, {
+      clubId,
+      clubName: clubDetail?.name,
+    });

13-16: RECRUITMENT_STATUS에 as const 적용 권장

타입 추론과 타이포 방지를 위해 리터럴을 고정하세요. 전체 검색 결과 해당 문자열/심볼은 이 파일에서만 사용됩니다: frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx

-const RECRUITMENT_STATUS = {
+const RECRUITMENT_STATUS = {
   ALWAYS: '상시 모집',
   CLOSED: '모집 마감',
-};
+} as const;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5b9cef1 and 76d6389.

📒 Files selected for processing (1)
  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
frontend/**/*.{ts,tsx}

📄 CodeRabbit inference engine (frontend/.cursorrules)

frontend/**/*.{ts,tsx}: Replace magic numbers with named constants for clarity.
Replace complex or nested ternary operators with if/else statements or IIFEs for readability.
Assign complex boolean conditions to named variables.
Use consistent return types for similar functions and hooks.
Avoid hidden side effects; functions should only perform actions implied by their signature (Single Responsibility Principle).
Use unique, descriptive names for custom wrappers and functions to avoid ambiguity.
Define constants near related logic or ensure names link them clearly.

Files:

  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
frontend/**/*.tsx

📄 CodeRabbit inference engine (frontend/.cursorrules)

frontend/**/*.tsx: Abstract complex logic/interactions into dedicated components or higher-order components (HOCs).
Separate significantly different conditional UI/logic into distinct components.
Colocate simple, localized logic or use inline definitions to reduce context switching.
Choose field-level or form-level cohesion based on form requirements.
Break down broad state management into smaller, focused hooks or contexts.
Use component composition instead of props drilling.

Files:

  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
🧠 Learnings (5)
📓 Common learnings
Learnt from: seongwon030
PR: Moadong/moadong#195
File: frontend/src/pages/AdminPage/AdminPage.tsx:7-7
Timestamp: 2025-03-19T05:18:07.818Z
Learning: AdminPage.tsx에서 현재 하드코딩된 클럽 ID('67d2e3b9b15c136c6acbf20b')는 로그인 기능 구현 후 동적으로 가져오는 방식으로 수정될 예정입니다.
📚 Learning: 2025-07-19T05:05:10.196Z
Learnt from: seongwon030
PR: Moadong/moadong#548
File: frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx:17-57
Timestamp: 2025-07-19T05:05:10.196Z
Learning: ClubDetailPage.tsx에서 notJoinedClubNames 배열의 하드코딩은 의도적인 설계 결정입니다. 개발자가 명시적으로 하드코딩을 선택했으므로 이에 대한 리팩토링 제안을 하지 않아야 합니다.

Applied to files:

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

Applied to files:

  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
📚 Learning: 2025-07-19T05:09:10.702Z
Learnt from: CR
PR: Moadong/moadong#0
File: frontend/.cursorrules:0-0
Timestamp: 2025-07-19T05:09:10.702Z
Learning: Applies to frontend/**/*.tsx : Separate significantly different conditional UI/logic into distinct components.

Applied to files:

  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
📚 Learning: 2025-07-19T05:09:10.702Z
Learnt from: CR
PR: Moadong/moadong#0
File: frontend/.cursorrules:0-0
Timestamp: 2025-07-19T05:09:10.702Z
Learning: Applies to frontend/**/*.{ts,tsx} : Replace complex or nested ternary operators with if/else statements or IIFEs for readability.

Applied to files:

  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
🧬 Code graph analysis (1)
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (2)
frontend/src/hooks/queries/club/useGetClubDetail.ts (1)
  • useGetClubDetail (6-20)
frontend/src/constants/eventName.ts (1)
  • EVENT_NAME (1-19)
🔇 Additional comments (3)
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (3)

7-7: ShareButton 정적 import 유지 적절

항상 렌더링되는 컴포넌트라면 동적 로딩 이점이 적습니다. 현재 선택이 요구사항과 일치합니다.


49-65: 조건부 렌더링 분리 잘 했습니다

중첩 JSX를 함수로 뽑아 가독성이 개선되었습니다. 코딩 가이드라인과 합치됩니다.


45-46: window.open 보안 플래그 적용 OK

'noopener,noreferrer' 설정 적절합니다.

Comment on lines +41 to 46
if (!externalFormLink) {
alert('동아리 모집 정보를 확인해주세요.');
return;
}
window.open(externalFormLink, '_blank', 'noopener,noreferrer');
}
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

외부 링크 열기 전 프로토콜 검증 추가 필요(XSS/오용 방지)

backend/콘텐츠에서 잘못된 URL이 들어오면 javascript:, data:, file: 등으로 새 창을 열 수 있습니다. http/https만 허용하도록 검증하세요.

-      if (!externalFormLink) {
-        alert('동아리 모집 정보를 확인해주세요.');
-        return;
-      }
-      window.open(externalFormLink, '_blank', 'noopener,noreferrer');
+      if (!externalFormLink) {
+        alert('동아리 모집 정보를 확인해주세요.');
+        return;
+      }
+      try {
+        const url = new URL(externalFormLink);
+        const isHttp = url.protocol === 'http:' || url.protocol === 'https:';
+        if (!isHttp) {
+          alert('유효하지 않은 외부 링크입니다.');
+          return;
+        }
+        window.open(url.toString(), '_blank', 'noopener,noreferrer');
+      } catch {
+        alert('유효하지 않은 외부 링크입니다.');
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!externalFormLink) {
alert('동아리 모집 정보를 확인해주세요.');
return;
}
window.open(externalFormLink, '_blank', 'noopener,noreferrer');
}
if (!externalFormLink) {
alert('동아리 모집 정보를 확인해주세요.');
return;
}
try {
const url = new URL(externalFormLink);
const isHttp = url.protocol === 'http:' || url.protocol === 'https:';
if (!isHttp) {
alert('유효하지 않은 외부 링크입니다.');
return;
}
window.open(url.toString(), '_blank', 'noopener,noreferrer');
} catch {
alert('유효하지 않은 외부 링크입니다.');
}
}
🤖 Prompt for AI Agents
In
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
around lines 41-46, the code opens externalFormLink without validating its
protocol; fix by parsing the link with the URL constructor inside a try/catch
and only allow navigation when url.protocol is 'http:' or 'https:' (otherwise
alert the user and abort). If parsing fails or the protocol is disallowed, do
not call window.open; keep using target='_blank' and rel='noopener,noreferrer'
when opening allowed URLs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💻 FE Frontend ✨ Feature 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feature] MOA-234 상세페이지 지원하기 버튼 위치를 변경한다

2 participants

Comments