Skip to content

Conversation

@lepitaaar
Copy link
Contributor

@lepitaaar lepitaaar commented Dec 27, 2025

#️⃣연관된 이슈

#977

📝작업 내용

삭제되었던 하단 모집버튼을 복구하였습니다

image

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

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

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

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

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

🫡 참고사항

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 동아리 상세 페이지에 지원 버튼, 공유 버튼 및 하단 footer 추가
    • 여러 지원 옵션 선택 모달 및 외부/내부 지원 연동 지원
    • 버튼·아이콘·푸터의 레이아웃/스타일 개선(반응형 포함)
  • 테스트

    • 모집 일정 파싱 관련 테스트 업데이트 — 빈값/미정 처리 동작 반영

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

@lepitaaar lepitaaar self-assigned this Dec 27, 2025
@lepitaaar lepitaaar added 🐞 Bug Something isn't working 💻 FE Frontend 🛠Fix 기능이 의도한 대로 동작하지 않는 버그를 수정 🚀 hotfix 즉시 릴리즈할 부분 labels Dec 27, 2025
@vercel
Copy link

vercel bot commented Dec 27, 2025

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

Project Deployment Review Updated (UTC)
moadong Ready Ready Preview, Comment Dec 27, 2025 6:08am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 27, 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

클럽 상세 페이지에 sticky 푸터 컴포넌트 ClubDetailFooter를 추가하고, 지원/공유 버튼(및 스타일)을 새로 도입했습니다. 모집일 파서 반환 타입을 Date | null로 변경하고 ClubDetailrecruitmentStart/recruitmentEnd를 non-null string으로 변경했습니다.

Changes

코호트 / 파일(s) 요약
페이지 통합
frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
ClubDetailFooter를 import 후 렌더링; recruitmentStart/recruitmentEnd props 전달
푸터 컴포넌트
frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx, .../ClubDetailFooter.styles.ts
새 컴포넌트: 날짜 파싱(recruitmentDateParser), getDeadlineText 계산 후 ClubApplyButtondeadlineText 전달; sticky footer 스타일 추가
지원 버튼 컴포넌트
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx, .../ClubApplyButton.styles.ts
새 컴포넌트: 모집 종료 체크, 지원 폼 옵션 조회, 내부/외부 네비게이션, 모달 선택 로직; 관련 스타일(ApplyButtonContainer, ApplyButton, Separator) 추가
공유 버튼 컴포넌트
frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx, .../ShareButton.styles.ts
새 컴포넌트: Kakao SDK 기반 공유 로직 및 스타일(ShareButtonContainer, ShareButtonIcon) 추가
유틸 변경
frontend/src/utils/recruitmentDateParser.ts, frontend/src/utils/recruitmentDateParser.test.ts
반환 타입을 `Date
타입 변경
frontend/src/types/club.ts
ClubDetailrecruitmentStart, recruitmentEnd를 `string

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant ClubDetailPage
    participant ClubDetailFooter
    participant ClubApplyButton
    participant API as FormAPI
    participant Router
    participant Kakao as KakaoSDK

    User->>ClubDetailPage: 페이지 방문
    ClubDetailPage->>ClubDetailFooter: recruitmentStart, recruitmentEnd 전달
    ClubDetailFooter->>ClubDetailFooter: recruitmentDateParser -> getDeadlineText
    ClubDetailFooter->>ClubApplyButton: deadlineText 전달

    rect rgba(230,240,250,0.9)
    Note over User,ClubApplyButton: 지원 흐름
    User->>ClubApplyButton: 지원 버튼 클릭
    ClubApplyButton->>ClubApplyButton: 모집 종료 여부 확인
    alt 모집 종료됨
        ClubApplyButton->>User: 알림 표시
    else 모집 중
        ClubApplyButton->>API: 지원 옵션 조회
        API-->>ClubApplyButton: 옵션 목록
        alt 옵션 1개
            ClubApplyButton->>Router: 내부 폼 네비게이션 또는 외부 URL 오픈
        else 다수 옵션
            ClubApplyButton->>User: 모달 오픈
            User->>ClubApplyButton: 옵션 선택
            ClubApplyButton->>Router: 선택된 폼으로 이동 / 외부 URL 오픈
        end
    end
    end

    rect rgba(240,230,250,0.9)
    Note over User,Kakao: 공유 흐름
    User->>ClubDetailFooter: 공유 버튼 클릭
    ClubDetailFooter->>Kakao: Kakao.Share.sendDefault 호출
    Kakao->>User: 공유 UI 표시
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • seongwon030
  • oesnuj

워크스루

클럽 상세 페이지에 새로운 ClubDetailFooter 컴포넌트를 추가하여 모집 기간을 파싱한 후 지원 버튼과 공유 버튼을 렌더링합니다. 모집 날짜 파서의 반환 타입을 Date | null로 변경하고 ClubDetail 인터페이스의 모집 필드를 필수 속성으로 업데이트합니다.

변경사항

코호트 / 파일 요약
ClubDetailPage 통합
frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
ClubDetailFooter 컴포넌트를 추가로 임포트하고 렌더링하며, 모집 시작/종료 날짜를 props로 전달
ClubDetailFooter 컴포넌트
frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx, .../ClubDetailFooter.styles.ts
새로운 함수형 컴포넌트로 모집 날짜를 파싱하여 deadline 텍스트를 계산한 후 ClubApplyButton에 전달; sticky 푸터 스타일 추가
ClubApplyButton 컴포넌트
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx, .../ClubApplyButton.styles.ts
새로운 컴포넌트로 지원 모달/선택 로직, 모집 종료 확인, 지원 폼 옵션 조회 및 내부/외부 네비게이션 처리; 스타일 정의 추가 (ApplyButtonContainer, ApplyButton, Separator)
ShareButton 컴포넌트
frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx, .../ShareButton.styles.ts
새로운 컴포넌트로 Kakao SDK를 사용한 클럽 공유 기능 구현; 스타일 정의 추가 (ShareButtonContainer, ShareButtonIcon)
타입 정의 업데이트
frontend/src/types/club.ts
ClubDetail 인터페이스의 recruitmentStart, recruitmentEnd를 `string
모집 날짜 파서 유틸리티
frontend/src/utils/recruitmentDateParser.ts, frontend/src/utils/recruitmentDateParser.test.ts
반환 타입을 `Date

시퀀스 다이어그램

sequenceDiagram
    participant User
    participant ClubDetailPage
    participant ClubDetailFooter
    participant ClubApplyButton
    participant Modal as ApplicationSelectModal
    participant API as Form API
    participant Router
    participant Kakao as Kakao SDK
    
    User->>ClubDetailPage: 클럽 상세 페이지 방문
    ClubDetailPage->>ClubDetailFooter: recruitmentStart/End 전달
    ClubDetailFooter->>ClubDetailFooter: recruitmentDateParser로 파싱
    ClubDetailFooter->>ClubDetailFooter: getDeadlineText 계산
    ClubDetailFooter->>ClubApplyButton: deadlineText 전달
    
    rect rgb(230, 240, 250)
    Note over User,ClubApplyButton: 지원 버튼 클릭 흐름
    User->>ClubApplyButton: 지원 버튼 클릭
    ClubApplyButton->>ClubApplyButton: 모집 종료 여부 확인
    alt 모집 종료됨
        ClubApplyButton->>User: 알림 표시
    else 모집 중
        ClubApplyButton->>API: 지원 폼 옵션 조회
        API-->>ClubApplyButton: 옵션 반환
        alt 옵션 1개
            ClubApplyButton->>Router: 폼 페이지로 직접 이동 또는 외부 URL 열기
        else 옵션 2개 이상
            ClubApplyButton->>Modal: 모달 오픈
            User->>Modal: 옵션 선택
            Modal->>Router: 선택된 폼으로 이동 또는 외부 URL 열기
        end
    end
    end
    
    rect rgb(240, 230, 250)
    Note over User,Kakao: 공유 버튼 클릭 흐름
    User->>ClubApplyButton: 공유 버튼 클릭
    ClubApplyButton->>Kakao: Kakao.Share.sendDefault 호출
    Kakao->>User: 공유 메뉴 표시
    end
Loading

예상 코드 리뷰 노력

🎯 3 (Moderate) | ⏱️ ~25분

관련된 가능성 있는 PR

  • PR #195: ClubDetail 타입(recruitmentStart/recruitmentEnd) 변경 관련 수정사항이 포함되어 있음.
  • PR #795: ClubDetailPage 푸터 및 관련 컴포넌트(ApplyButton, ShareButton) 스타일/구현과 중복 가능.
  • PR #972: ClubDetailPage 컴포넌트(푸터/지원 흐름)와 유사한 변경을 포함하여 충돌 가능성 있음.

권장 검토자

  • 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: 6

🧹 Nitpick comments (6)
frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx (1)

19-19: 명시적으로 null을 반환하도록 개선하세요.

조기 반환 시 암묵적으로 undefined를 반환하고 있습니다. React 컴포넌트에서는 명시적으로 null을 반환하는 것이 모범 사례입니다.

🔎 권장 수정사항
-  if (!clubDetail) return;
+  if (!clubDetail) return null;
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (5)

26-26: non-null 단언 대신 타입 가드 이후 사용 패턴을 명확히 하세요.

Line 32에서 clubId를 체크하고 있기 때문에 line 26의 non-null 단언(clubId!)은 불필요합니다. 훅 호출 순서 규칙을 준수하면서도 타입 안정성을 높이려면, clubId가 undefined일 수 있는 경우 early return 이전에 빈 문자열 등으로 기본값을 제공하거나, enabled 옵션을 활용하는 것이 더 안전한 패턴입니다.

🔎 더 안전한 패턴 제안
  const { clubId } = useParams<{ clubId: string }>();
  const navigate = useNavigate();
  const trackEvent = useMixpanelTrack();
-  const { data: clubDetail } = useGetClubDetail(clubId!);
+  const { data: clubDetail } = useGetClubDetail(clubId || '');

  // 모달 옵션 상태
  const [isOpen, setIsOpen] = useState(false);
  const [options, setOptions] = useState<ApplicationForm[]>([]);

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

35-54: 복잡한 로직을 분리하여 단일 책임 원칙을 준수하세요.

goWithForm 함수가 여러 책임을 가지고 있습니다:

  1. 폼 데이터 조회
  2. 외부/내부 폼 모드 판단
  3. 외부 URL 처리 (window.open)
  4. 내부 라우팅 처리 (navigate)
  5. 모달 상태 업데이트

코딩 가이드라인에 따르면 복잡한 로직은 전용 함수나 HOC로 추상화해야 합니다. 특히 lines 38-45의 중첩된 조건 로직은 별도 헬퍼 함수로 분리하는 것을 권장합니다.

🔎 리팩토링 제안
// 외부 URL 처리를 별도 함수로 분리
const handleExternalForm = (externalUrl: string | undefined) => {
  const trimmedUrl = externalUrl?.trim();
  if (trimmedUrl) {
    window.open(trimmedUrl, '_blank', 'noopener,noreferrer');
    return true;
  }
  return false;
};

const goWithForm = async (formId: string) => {
  try {
    const formDetail = await getApplication(clubId, formId);
    
    if (formDetail?.formMode === ApplicationFormMode.EXTERNAL) {
      if (handleExternalForm(formDetail.externalApplicationUrl)) {
        return;
      }
    }
    
    navigate(`/application/${clubId}/${formId}`, { state: { formDetail } });
    setIsOpen(false);
  } catch (error) {
    console.error('지원서 조회 중 오류가 발생했습니다', error);
    alert('지원서 정보를 불러오는 중 오류가 발생했습니다. 다시 시도해주세요.');
  }
};

Based on coding guidelines: "Abstract complex logic/interactions into dedicated components/HOCs" and "Avoid hidden side effects".


57-60: 비일관적인 비동기 처리 패턴을 통일하세요.

Line 59에서 void goWithForm(option.id)를 사용하지만, line 78에서는 await goWithForm(list[0].id)로 동일한 함수를 await하고 있습니다. 이러한 비일관성은 에러 처리와 실행 순서에 혼란을 줄 수 있습니다.

코딩 가이드라인에 따르면 유사한 함수/훅에 대해 일관된 반환 타입과 사용 패턴을 유지해야 합니다.

🔎 일관성 있는 패턴 제안
  const openByOption = (option?: ApplicationForm) => {
    if (!option) return;
-    void goWithForm(option.id);
+    // handleClick 내부의 await 패턴과 일관성 유지
+    goWithForm(option.id).catch((error) => {
+      console.error('지원서 처리 중 오류:', error);
+    });
  };

Based on coding guidelines: "Use consistent return types for similar functions/hooks".


62-88: 복잡한 handleClick 함수를 더 작은 단위로 분리하세요.

handleClick 함수가 너무 많은 책임을 가지고 있습니다:

  1. 이벤트 추적
  2. 마감 상태 검증
  3. 지원서 옵션 조회
  4. 단일/다중 옵션 분기 처리
  5. 에러 핸들링
  6. 모달 상태 관리

코딩 가이드라인에 따르면 복잡한 로직은 전용 컴포넌트나 커스텀 훅으로 분리해야 합니다. 특히 지원서 옵션 조회 및 처리 로직은 별도의 커스텀 훅(예: useApplicationFlow)으로 추출하는 것을 권장합니다.

🔎 커스텀 훅으로 분리하는 예시
// useApplicationFlow.ts
const useApplicationFlow = (clubId: string) => {
  const navigate = useNavigate();
  
  const handleSingleOption = async (formId: string) => {
    const formDetail = await getApplication(clubId, formId);
    // ... 폼 처리 로직
  };
  
  const fetchAndProcessOptions = async () => {
    const list = await getApplicationOptions(clubId);
    
    if (list.length === 0) {
      alert('현재 이용 가능한 지원서가 없습니다.');
      return { shouldOpenModal: false, options: [] };
    }
    
    if (list.length === 1) {
      await handleSingleOption(list[0].id);
      return { shouldOpenModal: false, options: [] };
    }
    
    return { shouldOpenModal: true, options: list };
  };
  
  return { fetchAndProcessOptions, handleSingleOption };
};

// ClubApplyButton.tsx에서 사용
const handleClick = async () => {
  trackEvent(USER_EVENT.CLUB_APPLY_BUTTON_CLICKED);
  
  if (deadlineText === RECRUITMENT_STATUS.CLOSED) {
    alert(`현재 ${clubDetail.name} 동아리는 모집 기간이 아닙니다.`);
    return;
  }
  
  try {
    const { shouldOpenModal, options: newOptions } = await fetchAndProcessOptions();
    if (shouldOpenModal) {
      setOptions(newOptions);
      setIsOpen(true);
    }
  } catch (e) {
    console.error('지원서 옵션 조회 중 오류가 발생했습니다.', e);
    alert('지원서 옵션을 불러오는 중 오류가 발생했습니다. 다시 시도해주세요.');
  }
};

Based on coding guidelines: "Abstract complex logic/interactions into dedicated components/HOCs" and "Break down broad state management into smaller, focused hooks/contexts to reduce coupling".


90-106: 중첩된 조건문을 단순화하고 boolean 조건을 명확한 변수명으로 추출하세요.

코딩 가이드라인에 따르면:

  1. 복잡한/중첩된 삼항 연산자는 if/else나 IIFE로 교체해야 합니다
  2. 복잡한 boolean 조건은 명확한 의미를 가진 명명된 변수로 할당해야 합니다

Lines 98-103의 중첩된 조건 로직은 가독성을 저해합니다.

🔎 리팩토링 제안
const renderButtonContent = () => {
  if (deadlineText === RECRUITMENT_STATUS.CLOSED) {
    return RECRUITMENT_STATUS.CLOSED;
  }

  const shouldShowDeadline = 
    deadlineText && deadlineText !== RECRUITMENT_STATUS.ALWAYS;

  return (
    <>
      지원하기
      {shouldShowDeadline && (
        <>
          <Styled.Separator />
          {deadlineText}
        </>
      )}
    </>
  );
};

Based on coding guidelines: "Replace complex/nested ternaries with if/else or IIFEs for readability" and "Assign complex boolean conditions to named variables for explicit meaning".

📜 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 0cdff28 and 66315d4.

📒 Files selected for processing (9)
  • 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.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/ClubApplyButton/ClubApplyButton.styles.ts
  • frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.styles.ts
  • frontend/src/utils/recruitmentDateParser.ts
  • frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx
  • frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx
  • frontend/src/utils/recruitmentDateParser.test.ts
  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
  • frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
  • frontend/src/types/club.ts
frontend/**/*.{ts,tsx}

📄 CodeRabbit inference engine (frontend/.cursorrules)

Use consistent return types for similar functions/hooks

Files:

  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.styles.ts
  • frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.styles.ts
  • frontend/src/utils/recruitmentDateParser.ts
  • frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx
  • frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx
  • frontend/src/utils/recruitmentDateParser.test.ts
  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
  • frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
  • frontend/src/types/club.ts
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/components/ShareButton/ShareButton.tsx
  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
  • frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
🧠 Learnings (4)
📚 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-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/ShareButton/ShareButton.tsx
  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.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/components/ShareButton/ShareButton.tsx
  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
  • frontend/src/pages/ClubDetailPage/ClubDetailPage.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} : Abstract complex logic/interactions into dedicated components/HOCs

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/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)
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)
🔇 Additional comments (7)
frontend/src/utils/recruitmentDateParser.test.ts (1)

37-45: LGTM!

빈 문자열과 '미정' 입력에 대한 null 반환 케이스가 적절히 추가되었습니다. 기존 에러 케이스들도 잘 유지되어 있어 함수의 모든 동작이 검증되고 있습니다.

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

3-4: LGTM!

'미정' 또는 빈 값에 대한 null 반환 처리가 적절합니다. 조기 반환 패턴을 사용하여 코드 가독성도 좋고, 반환 타입 Date | null로 타입 안전성도 확보되었습니다.

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

1-12: LGTM!

ShareButton 컴포넌트의 스타일 정의가 명확하고 간결합니다. 컨테이너와 아이콘을 분리하여 관리하는 구조가 적절합니다.

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

83-86: LGTM!

ClubDetailFooter 컴포넌트가 적절한 위치에 추가되었고, 필요한 props도 올바르게 전달되고 있습니다. clubDetail의 null 체크도 이미 상위에서 수행되고 있어 안전합니다.

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

11-13: LGTM!

모듈 레벨에 상수를 정의하여 코드의 가독성과 유지보수성을 높였습니다.


22-25: LGTM!

Kakao SDK 초기화 상태를 확인하는 방어 로직이 적절히 구현되어 있습니다.

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

12-39: 반응형 디자인이 잘 구현되어 있습니다.

버튼 스타일이 명확하고, 500px 이하 화면에서 적절한 너비로 조정되는 반응형 디자인이 잘 적용되어 있습니다.

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하는데, 현재 에러 처리가 없어 컴포넌트가 크래시할 수 있습니다. 예를 들어, recruitmentStartrecruitmentEnd가 빈 문자열이나 '미정'이 아니면서 잘못된 형식(예: "2025-05-25")일 경우 에러가 발생합니다.

사용자 경험 개선을 위해 try-catch로 에러를 처리하고 적절한 fallback UI를 표시하거나, 최소한 에러 로깅을 추가하는 것을 권장합니다.

🔎 에러 처리 추가 예시
 const ClubDetailFooter = ({
   recruitmentStart,
   recruitmentEnd,
 }: ClubDetailFooterProps) => {
+  let deadlineText: string;
+  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 (
     <Styled.ClubDetailFooterContainer>
       <ClubApplyButton deadlineText={deadlineText} />
     </Styled.ClubDetailFooterContainer>
   );
 };
📝 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
const deadlineText = getDeadlineText(
recruitmentDateParser(recruitmentStart),
recruitmentDateParser(recruitmentEnd),
new Date(),
);
const ClubDetailFooter = ({
recruitmentStart,
recruitmentEnd,
}: ClubDetailFooterProps) => {
let deadlineText: string;
try {
deadlineText = getDeadlineText(
recruitmentDateParser(recruitmentStart),
recruitmentDateParser(recruitmentEnd),
new Date(),
);
} catch (error) {
console.error('모집 기간 파싱 오류:', error);
deadlineText = '모집 기간 확인 불가';
}
return (
<Styled.ClubDetailFooterContainer>
<ClubApplyButton deadlineText={deadlineText} />
</Styled.ClubDetailFooterContainer>
);
};
🤖 Prompt for AI Agents
In
frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx
around lines 15-19, the calls to recruitmentDateParser can throw on invalid
input and currently crash the component; wrap the parsing and getDeadlineText
call in a try-catch, log the error (console.error or a logger) and provide a
safe fallback (e.g., nulls or a "날짜 정보 없음" deadlineText) so the component
renders without breaking; ensure getDeadlineText is only called with valid Date
objects and the fallback path renders appropriate UI or hides the deadline
section.

Comment on lines +27 to +37
window.Kakao.Share.sendDefault({
objectType: 'feed',
content: {
title: clubDetail.name,
description: clubDetail.description,
imageUrl: clubDetail.logo ? clubDetail.logo : DEFAULT_IMAGE_URL,
link: {
mobileWebUrl: `${MOADONG_BASE_URL}${clubDetail.id}`,
webUrl: `${MOADONG_BASE_URL}${clubDetail.id}`,
},
},
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

카카오 공유 description에 잘못된 타입이 전달되고 있습니다.

Line 31에서 clubDetail.description을 카카오 공유 description으로 사용하고 있지만, ClubDetail 타입의 descriptionDetailedDescription 객체입니다(문자열이 아님). 이로 인해 카카오톡 공유 시 [object Object]가 표시되거나 런타임 에러가 발생할 수 있습니다.

clubDetail.introduction 또는 clubDetail.description.introDescription을 사용해야 합니다.

🔎 수정 제안
     window.Kakao.Share.sendDefault({
       objectType: 'feed',
       content: {
         title: clubDetail.name,
-        description: clubDetail.description,
+        description: clubDetail.introduction,
         imageUrl: clubDetail.logo ? clubDetail.logo : DEFAULT_IMAGE_URL,
         link: {
           mobileWebUrl: `${MOADONG_BASE_URL}${clubDetail.id}`,
           webUrl: `${MOADONG_BASE_URL}${clubDetail.id}`,
         },
       },
📝 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
window.Kakao.Share.sendDefault({
objectType: 'feed',
content: {
title: clubDetail.name,
description: clubDetail.description,
imageUrl: clubDetail.logo ? clubDetail.logo : DEFAULT_IMAGE_URL,
link: {
mobileWebUrl: `${MOADONG_BASE_URL}${clubDetail.id}`,
webUrl: `${MOADONG_BASE_URL}${clubDetail.id}`,
},
},
window.Kakao.Share.sendDefault({
objectType: 'feed',
content: {
title: clubDetail.name,
description: clubDetail.introduction,
imageUrl: clubDetail.logo ? clubDetail.logo : DEFAULT_IMAGE_URL,
link: {
mobileWebUrl: `${MOADONG_BASE_URL}${clubDetail.id}`,
webUrl: `${MOADONG_BASE_URL}${clubDetail.id}`,
},
},
🤖 Prompt for AI Agents
In frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx
around lines 27 to 37, the Kakao share payload uses clubDetail.description which
is a DetailedDescription object, causing “[object Object]” or runtime errors;
replace it with a string field such as clubDetail.introduction or
clubDetail.description.introDescription (prefer introduction if available), and
ensure you coerce to a string with a safe fallback (e.g., empty string or
DEFAULT_DESCRIPTION) so the Kakao description always receives a plain string.

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

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

3-17: 매직 넘버를 상수로 추출하는 것을 고려해보세요.

스타일 값들(10px, 40px, 16px, #cdcdcd)이 하드코딩되어 있습니다. 디자인 시스템이나 테마에서 관리하거나 명명된 상수로 추출하면 유지보수성이 향상됩니다.

또한 z-index: 1050은 상당히 높은 값인데, TODO 주석에서 이미 조정이 필요함을 인지하고 계신 것으로 보입니다. 모달 분리 작업 시 함께 개선하시면 좋을 것 같습니다.

🔎 제안하는 리팩토링 예시

테마나 상수 파일에서 값을 관리하는 방식:

+const FOOTER_Z_INDEX = 1050; // TODO: Portal로 모달 분리 후 header보다 낮게 재조정
+const FOOTER_PADDING = '10px 40px';
+const FOOTER_GAP = '16px';
+const BORDER_COLOR = '#cdcdcd'; // 또는 theme.colors.gray300
+
 export const ClubDetailFooterContainer = styled.div`
   position: sticky;
   bottom: 0;
   width: 100%;
-  z-index: 1050; // TODO: Portal로 모달 분리 후 header보다 낮게 재조정
+  z-index: ${FOOTER_Z_INDEX};
-  padding: 10px 40px;
+  padding: ${FOOTER_PADDING};

   display: flex;
   align-items: center;
   justify-content: space-between;
-  gap: 16px;
+  gap: ${FOOTER_GAP};

   background-color: white;
-  border-top: 1px solid #cdcdcd;
+  border-top: 1px solid ${BORDER_COLOR};
 `;
📜 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 66315d4 and e6cda42.

📒 Files selected for processing (1)
  • frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.styles.ts
🧰 Additional context used
📓 Path-based instructions (2)
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/**/*.{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
🧠 Learnings (1)
📓 Common learnings
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')는 로그인 기능 구현 후 동적으로 가져오는 방식으로 수정될 예정입니다.

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

Labels

🐞 Bug Something isn't working 💻 FE Frontend 🛠Fix 기능이 의도한 대로 동작하지 않는 버그를 수정 🚀 hotfix 즉시 릴리즈할 부분

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants