Skip to content

[fix] 동아리 상세페이지 모집 버튼 푸터 삭제 복구#982

Merged
suhyun113 merged 4 commits intodevelop-fefrom
fix/delete-clubdetailfooter
Dec 28, 2025
Merged

[fix] 동아리 상세페이지 모집 버튼 푸터 삭제 복구#982
suhyun113 merged 4 commits intodevelop-fefrom
fix/delete-clubdetailfooter

Conversation

@lepitaaar
Copy link
Contributor

@lepitaaar lepitaaar commented Dec 28, 2025

#️⃣연관된 이슈

#977

📝작업 내용

상세페이지 푸터를 복구했습니더

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

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

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

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

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

🫡 참고사항

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 동아리 상세 페이지에 하단 푸터 추가로 모집 마감 상태 표시
    • 지원하기 버튼을 통한 원클릭 지원 기능 구현
    • 여러 지원 옵션이 있을 경우 선택 모달 제공
    • 동아리 정보 공유 버튼 추가 (카카오톡 공유)
  • Improvements

    • 모집 기간 데이터 처리 로직 개선 및 안정성 강화

✏️ Tip: You can customize this high-level summary in your review settings.

@lepitaaar lepitaaar self-assigned this Dec 28, 2025
@lepitaaar lepitaaar added 💻 FE Frontend 🛠Fix 기능이 의도한 대로 동작하지 않는 버그를 수정 labels Dec 28, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 28, 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.
  • You can also validate your configuration using the online YAML validator.
  • 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

ClubDetailPage에 새로운 ClubDetailFooter 컴포넌트를 추가하여 동적 모집 마감 텍스트와 함께 지원 버튼 및 공유 버튼을 렌더링합니다. ClubApplyButton, ShareButton, ClubDetailFooter를 신규 구현하고, 모집 일자 파서를 수정하여 지정되지 않은 날짜('미정')를 처리합니다.

Changes

Cohort / File(s) 요약
ClubDetailPage 통합
frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
ClubDetailFooter 컴포넌트 임포트 및 Footer 아래에 렌더링, 모집 시작/종료 날짜 전달
ClubApplyButton 컴포넌트
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx, ClubApplyButton.styles.ts
새 지원 버튼 컴포넌트: 모집 마감 확인, 지원 옵션 조회, 단일/다중 옵션 처리, 외부 URL 또는 내부 라우팅 지원, Mixpanel 이벤트 추적
ClubDetailFooter 컴포넌트
frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx, ClubDetailFooter.styles.ts
새 footer 컴포넌트: 모집 시작/종료 날짜 수신, 마감 텍스트 계산, ClubApplyButton 렌더링
ShareButton 컴포넌트
frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx, ShareButton.styles.ts
새 공유 버튼 컴포넌트: Kakao SDK 통합 피드 공유, Mixpanel 이벤트 추적
타입 및 유틸리티 수정
frontend/src/types/club.ts, frontend/src/utils/recruitmentDateParser.ts, frontend/src/utils/recruitmentDateParser.test.ts
ClubDetail의 recruitmentStart/recruitmentEnd를 nullable에서 non-nullable string으로 변경; recruitmentDateParser 반환 타입을 Date | null로 수정, '미정' 입력값 처리 추가

Sequence Diagram

sequenceDiagram
    actor User
    participant ClubApplyButton
    participant API as API (Applications)
    participant Modal as ApplicationSelectModal
    participant Form as Application Form
    participant Mixpanel
    
    User->>ClubApplyButton: 지원하기 버튼 클릭
    activate ClubApplyButton
    ClubApplyButton->>Mixpanel: 이벤트 추적 (클릭)
    ClubApplyButton->>ClubApplyButton: 모집 마감 확인
    
    alt 마감 상태
        ClubApplyButton-->>User: 아무 동작 없음
    else 모집 중
        ClubApplyButton->>API: 지원 옵션 조회
        activate API
        API-->>ClubApplyButton: 옵션 목록 반환
        deactivate API
        
        alt 옵션 0개
            ClubApplyButton-->>User: 아무 동작 없음
        else 옵션 1개
            ClubApplyButton->>ClubApplyButton: 폼 상세 정보 조회
            alt 외부 URL
                ClubApplyButton->>User: 새 탭에서 URL 열기
            else 내부 폼
                ClubApplyButton->>Form: 내부 라우팅 이동
            end
        else 옵션 2개 이상
            ClubApplyButton->>Modal: 모달 표시 (옵션 선택)
            activate Modal
            User->>Modal: 옵션 선택
            Modal-->>ClubApplyButton: 선택 옵션 반환
            deactivate Modal
            ClubApplyButton->>ClubApplyButton: 선택한 옵션으로 폼 열기
            ClubApplyButton->>Form: 라우팅 또는 URL 열기
        end
    end
    deactivate ClubApplyButton
Loading
sequenceDiagram
    actor User
    participant ShareButton
    participant Kakao as Kakao SDK
    participant Mixpanel
    
    User->>ShareButton: 공유 버튼 클릭
    activate ShareButton
    ShareButton->>Kakao: Kakao 초기화 확인
    
    alt Kakao 미초기화
        ShareButton-->>User: 공유 불가
    else Kakao 초기화됨
        ShareButton->>Kakao: 피드 공유 (클럽 정보)
        activate Kakao
        Kakao-->>User: 공유 완료
        deactivate Kakao
        ShareButton->>Mixpanel: 이벤트 추적 (SHARE_BUTTON_CLICKED)
    end
    deactivate ShareButton
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • oesnuj
  • seongwon030

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 '동아리 상세페이지 모집 버튼 푸터 삭제 복구'로, 변경사항의 핵심인 모집 푸터 컴포넌트 복구를 명확하게 요약하고 있습니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/delete-clubdetailfooter

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.

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

🧹 Nitpick comments (5)
frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.styles.ts (1)

7-7: z-index를 상수로 관리하는 것을 고려해주세요.

TODO 주석이 잘 작성되어 있습니다. Portal로 모달을 분리한 후, z-index 값들을 별도의 상수 파일(예: @/constants/zIndex.ts)에서 관리하면 헤더, 모달, 푸터 간의 레이어 순서를 일관되게 유지할 수 있습니다.

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

19-19: return null을 명시적으로 반환해주세요.

React 컴포넌트에서 아무것도 렌더링하지 않을 때는 undefined 대신 null을 명시적으로 반환하는 것이 관례입니다.

🔎 제안된 수정
-  if (!clubDetail) return;
+  if (!clubDetail) return null;
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.styles.ts (1)

20-20: 색상 값을 상수로 관리하는 것을 고려해주세요.

#ff7543(브랜드 오렌지)과 같은 색상 값이 하드코딩되어 있습니다. 프로젝트에 테마 또는 색상 상수 파일이 있다면, 해당 값을 사용하여 일관성을 유지하는 것이 좋습니다.

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

35-54: 함수 책임 분리 검토 권장

goWithForm 함수가 폼 데이터 조회, 외부 URL 처리, 내부 네비게이션을 모두 담당하고 있습니다. 단일 책임 원칙(SRP) 관점에서 여러 관심사가 혼재되어 있어 유지보수성이 저하될 수 있습니다.

외부 폼 처리와 내부 폼 네비게이션 로직을 별도 함수로 분리하는 것을 고려해보세요.

🔎 제안하는 리팩토링
+ const openExternalForm = (url: string) => {
+   window.open(url, '_blank', 'noopener,noreferrer');
+ };
+
+ const navigateToInternalForm = (formId: string, formDetail: any) => {
+   navigate(`/application/${clubId}/${formId}`, { state: { formDetail } });
+   setIsOpen(false);
+ };
+
  const goWithForm = async (formId: string) => {
    try {
      const formDetail = await getApplication(clubId, formId);
      if (formDetail?.formMode === ApplicationFormMode.EXTERNAL) {
        const externalApplicationUrl =
          formDetail.externalApplicationUrl?.trim();
        if (externalApplicationUrl) {
-         window.open(externalApplicationUrl, '_blank', 'noopener,noreferrer');
+         openExternalForm(externalApplicationUrl);
          return;
        }
      }
-     navigate(`/application/${clubId}/${formId}`, { state: { formDetail } });
-     setIsOpen(false);
+     navigateToInternalForm(formId, formDetail);
    } catch (error) {
      console.error('지원서 조회 중 오류가 발생했습니다', error);
      alert(
        '지원서 정보를 불러오는 중 오류가 발생했습니다. 다시 시도해주세요.',
      );
    }
  };

As per coding guidelines: "Avoid hidden side effects; functions should only perform actions implied by their signature (Single Responsibility Principle)"


62-88: 복잡한 로직을 커스텀 훅으로 추출 검토

handleClick 함수가 이벤트 추적, 마감 확인, 옵션 조회, 모달 제어 등 여러 책임을 담당하고 있습니다. 함수의 복잡도가 높아 테스트와 유지보수가 어려울 수 있습니다.

지원서 관련 로직을 useClubApplication 같은 커스텀 훅으로 추출하는 것을 고려해보세요.

💡 커스텀 훅 추출 예시
// useClubApplication.ts
const useClubApplication = (clubId: string, clubName: string) => {
  const [isModalOpen, setIsModalOpen] = useState(false);
  const [options, setOptions] = useState<ApplicationForm[]>([]);
  const navigate = useNavigate();

  const goWithForm = async (formId: string) => {
    // ... goWithForm 로직
  };

  const handleApplyClick = async (deadlineText?: string) => {
    if (deadlineText === RECRUITMENT_STATUS.CLOSED) {
      alert(`현재 ${clubName} 동아리는 모집 기간이 아닙니다.`);
      return;
    }

    try {
      const list = await getApplicationOptions(clubId);
      // ... 나머지 로직
    } catch (e) {
      // ... 에러 처리
    }
  };

  return {
    isModalOpen,
    setIsModalOpen,
    options,
    handleApplyClick,
    openByOption: (option?: ApplicationForm) => {
      if (!option) return;
      void goWithForm(option.id);
    },
  };
};

그런 다음 컴포넌트에서:

const ClubApplyButton = ({ deadlineText }: ClubApplyButtonProps) => {
  const { clubId } = useParams<{ clubId: string }>();
  const trackEvent = useMixpanelTrack();
  const { data: clubDetail } = useGetClubDetail(clubId!);
  
  const {
    isModalOpen,
    setIsModalOpen,
    options,
    handleApplyClick,
    openByOption,
  } = useClubApplication(clubId!, clubDetail?.name || '');

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

  const handleClick = async () => {
    trackEvent(USER_EVENT.CLUB_APPLY_BUTTON_CLICKED);
    await handleApplyClick(deadlineText);
  };

  // ... 렌더링 로직
};

As per coding guidelines: "Abstract complex logic/interactions into dedicated components/HOCs" 및 "Break down broad state management into smaller, focused hooks/contexts to reduce coupling"

📜 Review details

Configuration used: Organization 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 9f5691d and 73bc824.

📒 Files selected for processing (10)
  • frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.styles.ts
  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
  • frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.styles.ts
  • frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx
  • frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.styles.ts
  • frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx
  • frontend/src/types/club.ts
  • frontend/src/utils/recruitmentDateParser.test.ts
  • frontend/src/utils/recruitmentDateParser.ts
🧰 Additional context used
📓 Path-based instructions (3)
frontend/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (frontend/.cursorrules)

frontend/**/*.{ts,tsx,js,jsx}: Replace magic numbers with named constants for clarity
Replace complex/nested ternaries with if/else or IIFEs for readability
Assign complex boolean conditions to named variables for explicit meaning
Avoid hidden side effects; functions should only perform actions implied by their signature (Single Responsibility Principle)
Use unique and descriptive names for custom wrappers/functions to avoid ambiguity
Define constants near related logic or ensure names link them clearly to avoid silent failures
Break down broad state management into smaller, focused hooks/contexts to reduce coupling

Files:

  • frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.styles.ts
  • frontend/src/types/club.ts
  • frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx
  • frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.styles.ts
  • frontend/src/utils/recruitmentDateParser.ts
  • frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
  • frontend/src/utils/recruitmentDateParser.test.ts
  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.styles.ts
  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
  • frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx
frontend/**/*.{ts,tsx}

📄 CodeRabbit inference engine (frontend/.cursorrules)

Use consistent return types for similar functions/hooks

Files:

  • frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.styles.ts
  • frontend/src/types/club.ts
  • frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx
  • frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.styles.ts
  • frontend/src/utils/recruitmentDateParser.ts
  • frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
  • frontend/src/utils/recruitmentDateParser.test.ts
  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.styles.ts
  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
  • frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx
frontend/**/*.{tsx,jsx}

📄 CodeRabbit inference engine (frontend/.cursorrules)

frontend/**/*.{tsx,jsx}: Abstract complex logic/interactions into dedicated components/HOCs
Separate significantly different conditional UI/logic into distinct components
Colocate simple, localized logic or use inline definitions to reduce context switching
Choose field-level or form-level cohesion based on form requirements when using form libraries like react-hook-form
Use Component Composition instead of Props Drilling to reduce coupling

Files:

  • frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx
  • frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
  • frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx
🧠 Learnings (8)
📓 Common learnings
Learnt from: seongwon030
Repo: Moadong/moadong PR: 744
File: frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx:47-48
Timestamp: 2025-09-21T02:23:27.796Z
Learning: ClubApplyButton 컴포넌트에서 ShareButton은 항상 렌더링되어야 하므로 정적 import를 사용하는 것이 적절함. 동적 import는 불필요함.
Learnt from: seongwon030
Repo: Moadong/moadong PR: 195
File: frontend/src/pages/AdminPage/AdminPage.tsx:7-7
Timestamp: 2025-03-19T05:18:07.818Z
Learning: AdminPage.tsx에서 현재 하드코딩된 클럽 ID('67d2e3b9b15c136c6acbf20b')는 로그인 기능 구현 후 동적으로 가져오는 방식으로 수정될 예정입니다.
Learnt from: seongwon030
Repo: Moadong/moadong PR: 548
File: frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx:17-57
Timestamp: 2025-07-19T05:05:10.196Z
Learning: ClubDetailPage.tsx에서 notJoinedClubNames 배열의 하드코딩은 의도적인 설계 결정입니다. 개발자가 명시적으로 하드코딩을 선택했으므로 이에 대한 리팩토링 제안을 하지 않아야 합니다.
📚 Learning: 2025-09-21T02:23:27.796Z
Learnt from: seongwon030
Repo: Moadong/moadong PR: 744
File: frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx:47-48
Timestamp: 2025-09-21T02:23:27.796Z
Learning: ClubApplyButton 컴포넌트에서 ShareButton은 항상 렌더링되어야 하므로 정적 import를 사용하는 것이 적절함. 동적 import는 불필요함.

Applied to files:

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

Applied to files:

  • frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
  • frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx
📚 Learning: 2025-11-25T14:08:23.253Z
Learnt from: CR
Repo: Moadong/moadong PR: 0
File: frontend/.cursorrules:0-0
Timestamp: 2025-11-25T14:08:23.253Z
Learning: Applies to frontend/**/*.{tsx,jsx} : Separate significantly different conditional UI/logic into distinct components

Applied to files:

  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.styles.ts
  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
📚 Learning: 2025-11-25T14:08:23.253Z
Learnt from: CR
Repo: Moadong/moadong PR: 0
File: frontend/.cursorrules:0-0
Timestamp: 2025-11-25T14:08:23.253Z
Learning: Applies to frontend/**/*.{ts,tsx,js,jsx} : Replace magic numbers with named constants for clarity

Applied to files:

  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.styles.ts
📚 Learning: 2025-11-25T14:08:23.253Z
Learnt from: CR
Repo: Moadong/moadong PR: 0
File: frontend/.cursorrules:0-0
Timestamp: 2025-11-25T14:08:23.253Z
Learning: Applies to frontend/**/*.{tsx,jsx} : Abstract complex logic/interactions into dedicated components/HOCs

Applied to files:

  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
📚 Learning: 2025-11-25T14:08:23.253Z
Learnt from: CR
Repo: Moadong/moadong PR: 0
File: frontend/.cursorrules:0-0
Timestamp: 2025-11-25T14:08:23.253Z
Learning: Applies to frontend/**/*.{tsx,jsx} : Use Component Composition instead of Props Drilling to reduce coupling

Applied to files:

  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
📚 Learning: 2025-11-25T14:08:23.253Z
Learnt from: CR
Repo: Moadong/moadong PR: 0
File: frontend/.cursorrules:0-0
Timestamp: 2025-11-25T14:08:23.253Z
Learning: Applies to frontend/**/*.{tsx,jsx} : Choose field-level or form-level cohesion based on form requirements when using form libraries like react-hook-form

Applied to files:

  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
🧬 Code graph analysis (4)
frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx (1)
frontend/src/utils/recruitmentDateParser.ts (1)
  • recruitmentDateParser (3-18)
frontend/src/utils/recruitmentDateParser.test.ts (1)
frontend/src/utils/recruitmentDateParser.ts (1)
  • recruitmentDateParser (3-18)
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (3)
frontend/src/hooks/queries/club/useGetClubDetail.ts (1)
  • useGetClubDetail (6-20)
frontend/src/types/application.ts (1)
  • ApplicationForm (65-68)
frontend/src/constants/eventName.ts (1)
  • USER_EVENT (1-38)
frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx (2)
frontend/src/hooks/queries/club/useGetClubDetail.ts (1)
  • useGetClubDetail (6-20)
frontend/src/constants/eventName.ts (1)
  • USER_EVENT (1-38)
🔇 Additional comments (8)
frontend/src/utils/recruitmentDateParser.ts (1)

3-4: LGTM!

Date | null 반환 타입 변경과 '미정' 또는 빈 문자열에 대한 null 반환 처리가 적절합니다. 이를 통해 미정인 날짜를 예외 없이 우아하게 처리할 수 있습니다.

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

83-86: LGTM!

ClubDetailFooter 컴포넌트가 적절하게 통합되었습니다. recruitmentStartrecruitmentEnd props가 올바르게 전달되며, Footer 아래에 배치되어 sticky 푸터로서 정상 동작할 것입니다.

frontend/src/utils/recruitmentDateParser.test.ts (1)

37-45: LGTM!

테스트 케이스가 파서의 새로운 동작에 맞게 적절히 업데이트되었습니다. 빈 문자열과 '미정' 입력에 대한 null 반환을 명확하게 검증합니다.

frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.styles.ts (1)

1-12: LGTM!

ShareButton 스타일이 깔끔하게 정의되어 있습니다.

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

21-49: 공유 기능 구현이 잘 되어있습니다.

카카오 SDK 초기화 확인, 에러 처리, 접근성(aria-label), 그리고 Mixpanel 이벤트 추적이 적절히 구현되어 있습니다. Based on learnings, ShareButton은 항상 렌더링되어야 하므로 정적 import 사용이 올바릅니다.

frontend/src/types/club.ts (1)

27-28: 타입 차이는 의도된 설계 패턴입니다.

ClubDetailClubDescriptionrecruitmentStartrecruitmentEnd 타입 차이는 읽기(API 응답)와 쓰기(업데이트 요청) 작업에 대한 의도적인 분리입니다. RecruitEditTab에서 확인할 수 있듯이, ClubDetail의 값을 ClubDescription 형식으로 변환할 때 toISOString() ?? null을 통해 null 가능성을 명시적으로 처리하고 있습니다. 이는 API 설계의 일반적인 패턴이므로 현재 상태를 유지하는 것이 적절합니다.

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

17-20: LGTM!

상수 객체를 사용하여 매직 스트링을 제거한 점이 좋습니다. 코딩 가이드라인을 잘 준수하고 있습니다.


108-121: LGTM!

ShareButton과의 컴포넌트 조합이 적절하게 구현되어 있습니다. 학습된 내용에 따르면 ShareButton은 항상 렌더링되어야 하므로 정적 import를 사용한 점이 올바릅니다.

Based on learnings: "ClubApplyButton 컴포넌트에서 ShareButton은 항상 렌더링되어야 하므로 정적 import를 사용하는 것이 적절함"

Comment on lines +31 to +34
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.

🛠️ Refactor suggestion | 🟠 Major

img 요소에 대한 불필요한 폰트 스타일링을 제거해주세요.

font-sizefont-weightimg 요소에 적용되지 않습니다. 이 스타일은 의도된 동작을 하지 않으므로 제거하는 것이 좋습니다.

🔎 제안된 수정
-  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;
}
🤖 Prompt for AI Agents
In
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.styles.ts
around lines 31 to 34, the img selector contains font-size and font-weight which
do not apply to images; remove those two properties from the img rule so the
selector only contains applicable styles (or delete the entire img rule if it
becomes empty).

Comment on lines +15 to +19
const deadlineText = getDeadlineText(
recruitmentDateParser(recruitmentStart),
recruitmentDateParser(recruitmentEnd),
new Date(),
);
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

날짜 파싱 오류 처리 필요

recruitmentDateParser는 날짜 형식이 유효하지 않을 경우 에러를 throw합니다. 백엔드에서 예상치 못한 형식의 데이터가 전달되면 컴포넌트 렌더링이 실패할 수 있습니다.

try-catch로 감싸거나 Error Boundary를 활용하여 오류를 처리하는 것을 권장합니다.

🔎 제안하는 수정안
 const ClubDetailFooter = ({
   recruitmentStart,
   recruitmentEnd,
 }: ClubDetailFooterProps) => {
+  let deadlineText = '';
+  try {
-  const deadlineText = getDeadlineText(
-    recruitmentDateParser(recruitmentStart),
-    recruitmentDateParser(recruitmentEnd),
-    new Date(),
-  );
+    deadlineText = getDeadlineText(
+      recruitmentDateParser(recruitmentStart),
+      recruitmentDateParser(recruitmentEnd),
+      new Date(),
+    );
+  } catch (error) {
+    console.error('날짜 파싱 오류:', error);
+    deadlineText = ''; // 또는 적절한 기본값
+  }

   return (
🤖 Prompt for AI Agents
In
frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx
around lines 15 to 19, the direct calls to recruitmentDateParser can throw on
unexpected date formats and crash rendering; wrap each parser call in a
try-catch (or use a safe parsing helper that returns null/undefined on failure),
log the parsing error, and pass guarded values (e.g., null) into getDeadlineText
or use a fallback deadline string so the component can render gracefully when
parsing fails; ensure the UI shows a sensible default (like "날짜 정보 없음" or
similar) instead of crashing.

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.

빠른 수정 좋아여~

@suhyun113 suhyun113 merged commit a45a02b into develop-fe Dec 28, 2025
3 checks passed
@lepitaaar lepitaaar deleted the fix/delete-clubdetailfooter branch January 4, 2026 07:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💻 FE Frontend 🛠Fix 기능이 의도한 대로 동작하지 않는 버그를 수정

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants