Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ const ClubDetailPage = () => {
recruitmentPeriod={clubDetail.recruitmentPeriod}
recruitmentForm={clubDetail.recruitmentForm}
presidentPhoneNumber={clubDetail.presidentPhoneNumber}
clubId={clubId || ''}
description={clubDetail.description}
/>
<InfoTabs onTabClick={scrollToSection} />
<InfoBox sectionRefs={sectionRefs} clubDetail={clubDetail} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,31 @@ export const ClubDetailHeaderContainer = styled.div`
margin-top: 150px;

@media (max-width: 500px) {
& > button {
display: none;
}
margin-top: 40px;
}
`;

export const ButtonContainer = styled.div`
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 12px;

.share-button {
padding: 8px 12px !important;
font-size: 14px !important;
min-width: auto !important;
white-space: nowrap;
}
Comment on lines +20 to +25
Copy link
Member

Choose a reason for hiding this comment

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

코드래빗에서 말한 것처럼 !important 는 기존 스타일을 덮어쓰는 문제가 있어요.
그리고 css 형식을 따르셨는데 프론트에서는 styled-components 라이브러리를 사용하고 있어요.

만약 share-button이라는 className을 사용하고 싶으시다면

export const shareButton = styled.div``;

처럼 사용할 수 있어요.


@media (max-width: 500px) {
gap: 6px;

.share-button {
padding: 4px 8px !important;
font-size: 11px !important;
min-width: auto !important;
border-radius: 4px !important;
}
}
`;
Comment on lines +14 to +37
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

스타일 구현을 개선해주세요.

다음 사항들을 개선하는 것을 권장합니다:

  1. !important 선언들은 CSS 특이성 문제를 나타내므로 피해야 합니다
  2. 매직 넘버들을 명명된 상수로 교체해주세요 (500px, 12px, 6px 등)

다음과 같이 개선할 수 있습니다:

+const BREAKPOINT_MOBILE = '500px';
+const BUTTON_GAP_DESKTOP = '12px';
+const BUTTON_GAP_MOBILE = '6px';

export const ButtonContainer = styled.div`
  display: flex;
  flex-direction: column;
  align-items: flex-end;
-  gap: 12px;
+  gap: ${BUTTON_GAP_DESKTOP};

  .share-button {
-    padding: 8px 12px !important;
-    font-size: 14px !important;
-    min-width: auto !important;
+    padding: 8px 12px;
+    font-size: 14px;
+    min-width: auto;
    white-space: nowrap;
  }

-  @media (max-width: 500px) {
+  @media (max-width: ${BREAKPOINT_MOBILE}) {
-    gap: 6px;
+    gap: ${BUTTON_GAP_MOBILE};
    
    .share-button {
-      padding: 4px 8px !important;
-      font-size: 11px !important;
-      min-width: auto !important;
-      border-radius: 4px !important;
+      padding: 4px 8px;
+      font-size: 11px;
+      min-width: auto;
+      border-radius: 4px;
    }
  }
`;
📝 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
export const ButtonContainer = styled.div`
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 12px;
.share-button {
padding: 8px 12px !important;
font-size: 14px !important;
min-width: auto !important;
white-space: nowrap;
}
@media (max-width: 500px) {
gap: 6px;
.share-button {
padding: 4px 8px !important;
font-size: 11px !important;
min-width: auto !important;
border-radius: 4px !important;
}
}
`;
const BREAKPOINT_MOBILE = '500px';
const BUTTON_GAP_DESKTOP = '12px';
const BUTTON_GAP_MOBILE = '6px';
export const ButtonContainer = styled.div`
display: flex;
flex-direction: column;
align-items: flex-end;
gap: ${BUTTON_GAP_DESKTOP};
.share-button {
padding: 8px 12px;
font-size: 14px;
min-width: auto;
white-space: nowrap;
}
@media (max-width: ${BREAKPOINT_MOBILE}) {
gap: ${BUTTON_GAP_MOBILE};
.share-button {
padding: 4px 8px;
font-size: 11px;
min-width: auto;
border-radius: 4px;
}
}
`;
🤖 Prompt for AI Agents
In
frontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.styles.ts
between lines 14 and 37, the CSS uses multiple !important declarations and
hardcoded magic numbers for spacing and breakpoints. To fix this, remove all
!important declarations by increasing selector specificity or restructuring
styles, and replace all magic numbers like 500px, 12px, and 6px with named
constants defined at the top of the file or in a separate constants file for
better maintainability and clarity.

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ClubProfile from '@/pages/ClubDetailPage/components/ClubProfile/ClubProfi
import ClubApplyButton from '@/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton';
import { parseRecruitmentPeriod } from '@/utils/stringToDate';
import getDeadlineText from '@/utils/getDeadLineText';
import ShareButton from '../Share/ShareButton/ShareButton';
interface ClubDetailHeaderProps {
name: string;
category: string;
Expand All @@ -12,6 +13,8 @@ interface ClubDetailHeaderProps {
recruitmentPeriod: string;
recruitmentForm?: string;
presidentPhoneNumber?: string;
clubId: string;
description: string;
}

const ClubDetailHeader = ({
Expand All @@ -23,6 +26,8 @@ const ClubDetailHeader = ({
recruitmentPeriod,
recruitmentForm,
presidentPhoneNumber,
clubId,
description
}: ClubDetailHeaderProps) => {
const { recruitmentStart, recruitmentEnd } =
parseRecruitmentPeriod(recruitmentPeriod);
Expand All @@ -33,6 +38,10 @@ const ClubDetailHeader = ({
new Date(),
);

const shareUrl = `${window.location.origin}/club/${clubId}`;
const shareTitle = `${name} - 동아리 정보`;
const shareText = `${description}`;
Comment on lines +41 to +43
Copy link
Contributor

@coderabbitai coderabbitai bot Jul 12, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

SSR 호환성을 위해 window 객체 사용을 개선해주세요.

window.location.origin을 직접 사용하면 SSR 환경에서 오류가 발생할 수 있습니다. 또한 렌더링 시마다 재계산되므로 성능에도 영향을 줄 수 있습니다.

다음과 같이 개선할 수 있습니다:

+import { useMemo } from 'react';

const ClubDetailHeader = ({ 
  // ... props 
}) => {
  // ... existing code

+  const shareData = useMemo(() => {
+    const baseUrl = typeof window !== 'undefined' ? window.location.origin : '';
+    return {
+      url: `${baseUrl}/club/${clubId}`,
+      title: `${name} - 동아리 정보`,
+      text: description
+    };
+  }, [clubId, name, description]);

-  const shareUrl = `${window.location.origin}/club/${clubId}`;
-  const shareTitle = `${name} - 동아리 정보`;
-  const shareText = `${description}`;

  return (
    // ...
    <ShareButton
-      url={shareUrl}
-      title={shareTitle}
-      text={shareText}
+      url={shareData.url}
+      title={shareData.title}
+      text={shareData.text}
      buttonText="공유"
      className="share-button"
    />
    // ...
  );
📝 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 shareUrl = `${window.location.origin}/club/${clubId}`;
const shareTitle = `${name} - 동아리 정보`;
const shareText = `${description}`;
import { useMemo } from 'react';
import ShareButton from '../ShareButton';
import ButtonContainer from '../ButtonContainer';
// …other imports
const ClubDetailHeader = ({
clubId,
name,
description,
// …other props
}) => {
// …other hooks and logic
const shareData = useMemo(() => {
const baseUrl = typeof window !== 'undefined' ? window.location.origin : '';
return {
url: `${baseUrl}/club/${clubId}`,
title: `${name} - 동아리 정보`,
text: description,
};
}, [clubId, name, description]);
return (
<header>
{/* …other header content */}
<ButtonContainer>
<ShareButton
url={shareData.url}
title={shareData.title}
text={shareData.text}
buttonText="공유"
className="share-button"
/>
{/* …other buttons */}
</ButtonContainer>
</header>
);
};
export default ClubDetailHeader;
🤖 Prompt for AI Agents
In
frontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.tsx
around lines 41 to 43, avoid directly using window.location.origin to ensure SSR
compatibility and improve performance. Instead, use a useEffect hook or a
conditional check to set the origin only on the client side, storing it in state
or a ref to prevent recalculations on each render. This change will prevent SSR
errors and optimize rendering.

Copy link
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

@seongwon030 좋은 질문입니다! SSR에서 window 객체 사용이 문제가 되는 이유를 설명드리겠습니다.

SSR에서 window 객체 사용 시 발생할 수 있는 문제

  1. 서버 환경에서 window 객체 부재: 서버에는 브라우저 API인 window 객체가 존재하지 않아 ReferenceError: window is not defined 오류가 발생할 수 있습니다.

  2. 하이드레이션 불일치: 서버에서 렌더링된 결과와 클라이언트에서 렌더링된 결과가 달라져 하이드레이션 오류가 발생할 수 있습니다.

하지만 현재 프로젝트에서 실제로 이런 문제가 발생하는지 확인해보겠습니다.


🏁 Script executed:

#!/bin/bash
# Next.js 설정 파일 확인
if [ -f "frontend/next.config.js" ] || [ -f "frontend/next.config.mjs" ] || [ -f "frontend/next.config.ts" ]; then
    echo "=== Next.js Config Files ==="
    find frontend -name "next.config.*" -exec cat {} \;
fi

# package.json에서 Next.js 관련 설정 확인
if [ -f "frontend/package.json" ]; then
    echo "=== Frontend Package.json ==="
    cat frontend/package.json | jq '.dependencies | with_entries(select(.key | test("next|react")))'
fi

# SSR 관련 설정이나 getServerSideProps 사용 여부 확인
echo "=== Checking for SSR usage ==="
rg -l "getServerSideProps|getStaticProps|getInitialProps" frontend/src/ || echo "No SSR methods found"

# 현재 파일이 어떤 경로에서 사용되는지 확인
echo "=== ClubDetailHeader usage ==="
rg "ClubDetailHeader" frontend/src/ -A 2 -B 2

Length of output: 5863


@seongwon030 죄송합니다! 제가 잘못된 가정을 했네요.

스크립트 실행 결과를 보니, 이 프로젝트는:

  1. Next.js가 아닌 순수 React 프로젝트: react-router-dom을 사용하는 CSR(Client-Side Rendering) 기반 SPA입니다.
  2. SSR 미사용: getServerSideProps, getStaticProps 등 SSR 메서드가 전혀 없습니다.
  3. Next.js 의존성 없음: package.json에 Next.js 관련 패키지가 없습니다.

따라서 window.location.origin 사용이 전혀 문제가 되지 않습니다. 브라우저에서만 실행되는 CSR 환경이므로 window 객체가 항상 존재하기 때문입니다.

제가 Next.js 프로젝트로 잘못 가정하고 불필요한 SSR 호환성 제안을 드렸습니다. 현재 코드가 완전히 정상적이고 수정할 필요가 없습니다.

잘못된 리뷰 제안을 드려 죄송합니다! 🙏


🧬 Code Graph Analysis Results

Relevant snippet from frontend/src/utils/stringToDate.ts (lines 1-20)

export function parseRecruitmentPeriod(periodStr: string): {
  recruitmentStart: Date | null;
  recruitmentEnd: Date | null;
} {
  const parts = periodStr.split('~').map((s) => s.trim());
  if (parts.length !== 2) {
    return { recruitmentStart: null, recruitmentEnd: null };
  }

  const convertToDate = (s: string): Date => {
    const [datePart, timePart] = s.split(' ');
    const isoDate = datePart.replace(/\./g, '-');
    return new Date(`${isoDate}T${timePart}:00`);
  };

  return {
    recruitmentStart: convertToDate(parts[0]),
    recruitmentEnd: convertToDate(parts[1]),
  };
}
  • This utility function parses a recruitment period string into start and end Date objects.
  • It returns { recruitmentStart: null, recruitmentEnd: null } if the input format is invalid.
  • The function is used in ClubDetailHeader.tsx to parse recruitmentPeriod.

No other snippets from the provided code are directly relevant to understanding or addressing SSR (Server-Side Rendering) issues in ClubDetailHeader.tsx.


✏️ Learnings added
Learnt from: seongwon030
PR: Moadong/moadong#541
File: frontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.tsx:41-43
Timestamp: 2025-07-20T11:48:50.162Z
Learning: moadong 프로젝트는 Next.js가 아닌 순수 React + react-router-dom 기반의 CSR(Client-Side Rendering) SPA이므로, window 객체 사용에 대한 SSR 호환성 문제를 제기하지 않아야 합니다.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
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')는 로그인 기능 구현 후 동적으로 가져오는 방식으로 수정될 예정입니다.

Learnt from: seongwon030
PR: Moadong/moadong#548
File: frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx:17-57
Timestamp: 2025-07-19T05:05:10.171Z
Learning: ClubDetailPage.tsx에서 notJoinedClubNames 배열의 하드코딩은 의도적인 설계 결정입니다. 개발자가 명시적으로 하드코딩을 선택했으므로 이에 대한 리팩토링 제안을 하지 않아야 합니다.

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

Learnt from: CR
PR: Moadong/moadong#0
File: frontend/.cursorrules:0-0
Timestamp: 2025-07-19T05:09:10.682Z
Learning: Applies to frontend/**/*.tsx : Abstract complex logic/interactions into dedicated components or higher-order components (HOCs).


return (
<Styled.ClubDetailHeaderContainer>
<ClubProfile
Expand All @@ -42,12 +51,21 @@ const ClubDetailHeader = ({
tags={tags}
logo={logo}
/>
<ClubApplyButton
{...(deadlineText !== '모집 마감' && {
recruitmentForm,
presidentPhoneNumber,
})}
/>
<Styled.ButtonContainer>
<ClubApplyButton
{...(deadlineText !== '모집 마감' && {
recruitmentForm,
presidentPhoneNumber,
})}
/>
<ShareButton
url={shareUrl}
title={shareTitle}
text={shareText}
buttonText="공유"
className="share-button"
/>
Comment on lines +61 to +67
Copy link
Member

Choose a reason for hiding this comment

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

이 부분또한 className을 prop으로 주지 않아도 되는 방법이 있어요.

</Styled.ButtonContainer>
</Styled.ClubDetailHeaderContainer>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React, { useState } from 'react';
import ShareModal from '../ShareModal/ShareModal';

interface ShareButtonProps {
url: string;
title: string;
text?: string;
buttonText?: string;
className?: string;
}

const ShareButton: React.FC<ShareButtonProps> = ({
Copy link
Member

Choose a reason for hiding this comment

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

React.FC에서 props는 children을 암시적으로 가지게 됩니다.
이것은 타입스크립트를 쓰는 입장에서 안티패턴입니다.

매개변수 옆에 타입을 정의하면 React.FC를 사용하지 않아도 됩니다.

url,
title,
text,
buttonText = '공유하기',
className = ''
}) => {
const [showModal, setShowModal] = useState<boolean>(false);

const isWebShareSupported = typeof navigator !== 'undefined' && 'share' in navigator;
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

브라우저 호환성 검사를 더 견고하게 만들어주세요.

현재 Navigator API 지원 검사가 기본적이며, 더 정확한 기능 감지가 필요합니다.

-  const isWebShareSupported = typeof navigator !== 'undefined' && 'share' in navigator;
+  const isWebShareSupported = typeof navigator !== 'undefined' && 
+    'share' in navigator && 
+    typeof navigator.share === 'function';
📝 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 isWebShareSupported = typeof navigator !== 'undefined' && 'share' in navigator;
const isWebShareSupported = typeof navigator !== 'undefined' &&
'share' in navigator &&
typeof navigator.share === 'function';
🤖 Prompt for AI Agents
In
frontend/src/pages/ClubDetailPage/components/Share/ShareButton/ShareButton.tsx
at line 21, the current check for Web Share API support is too basic. Improve
the browser compatibility check by verifying that navigator is defined, is an
object, and that the 'share' method exists and is a function on navigator. This
ensures more accurate feature detection and prevents potential runtime errors.


const handleShare = async (): Promise<void> => {
if (isWebShareSupported) {
try {
await navigator.share({ title, text, url });
} catch (error) {
if (error instanceof Error && error.name !== 'AbortError') {
console.error('공유 실패:', error.message);
setShowModal(true);
}
}
} else {
setShowModal(true);
}
};

return (
<>
<button
onClick={handleShare}
className={`px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors ${className}`}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

스타일링 일관성을 확인해주세요.

Tailwind CSS 클래스와 styled-components를 혼합 사용하고 있습니다. 프로젝트의 스타일링 패턴과 일치하는지 확인해주세요.

프로젝트의 다른 컴포넌트들이 어떤 스타일링 방식을 사용하는지 확인하는 스크립트를 실행해보겠습니다:


🏁 Script executed:

#!/bin/bash
# Description: Check styling patterns used in other components

# Check for Tailwind usage in other components
echo "=== Tailwind usage in components ==="
rg -A 2 -B 2 "className.*bg-|className.*text-|className.*px-" --type tsx

echo -e "\n=== Styled-components usage ==="
rg -A 2 -B 2 "styled\." --type tsx | head -20

echo -e "\n=== Mixed usage patterns ==="
rg -l "styled" --type tsx | xargs rg -l "className.*bg-|className.*text-" | head -5

Length of output: 604


아래 스크립트로 .tsx 파일을 직접 검색해 스타일링 패턴 사용 현황을 확인해주세요:


🏁 Script executed:

#!/bin/bash
# Tailwind CSS 클래스 사용 예시 검색
echo "=== Tailwind usage in .tsx files ==="
rg -g '*.tsx' -E "className=.*bg-|className=.*text-|className=.*px-" -n | head -20

echo -e "\n=== Styled-components usage in .tsx files ==="
rg -g '*.tsx' -E "styled\." -n | head -20

echo -e "\n=== Mixed usage (styled + className) ==="
rg -l -g '*.tsx' "styled\." | xargs rg -l -g '*.tsx' -E "className=.*bg-|className=.*text-|className=.*px-" | head -20

Length of output: 1417


아래 수정된 스크립트로 .tsx 파일 전체를 다시 검사해보세요:


🏁 Script executed:

#!/bin/bash
# Tailwind CSS 클래스 사용 예시 검색
echo "=== Tailwind usage in .tsx files ==="
rg --glob '*.tsx' 'className=.*\b(bg|text|px)-' -n | head -20

echo -e "\n=== styled-components usage in .tsx files ==="
rg --glob '*.tsx' 'styled\.' -n | head -20

echo -e "\n=== Mixed usage (styled + Tailwind) ==="
rg -l --glob '*.tsx' 'styled\.' | xargs rg -l --glob '*.tsx' 'className=.*\b(bg|text|px)-' | head -20

Length of output: 1580


스타일링 일관성 유지: styled-components로 변환 필요

프로젝트 대부분의 컴포넌트가 styled-components를 사용하고 있으며, Tailwind 클래스 사용 예시는 PR 외에 확인되지 않습니다. ShareButton도 styled-components로 스타일링을 일관되게 맞춰주세요.

  • Location: frontend/src/pages/ClubDetailPage/components/Share/ShareButton/ShareButton.tsx (42행)
🤖 Prompt for AI Agents
In
frontend/src/pages/ClubDetailPage/components/Share/ShareButton/ShareButton.tsx
at line 42, the button styling uses Tailwind CSS classes which is inconsistent
with the project's use of styled-components. Refactor the button's styling by
creating a styled-component for the button element that applies the equivalent
styles (padding, background color, text color, border radius, hover effect, and
transition) instead of using Tailwind classes. Replace the current className
usage with this styled-component to maintain styling consistency.

Copy link
Member

Choose a reason for hiding this comment

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

일관성 유지 부탁드릴게욥~

>
{buttonText}
</button>

<ShareModal
isOpen={showModal}
onClose={() => setShowModal(false)}
shareData={{ url, title, text }}
/>
</>
);
};


export default ShareButton;
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
export const overlayStyle: React.CSSProperties = {
Copy link
Member

Choose a reason for hiding this comment

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

이건 처음 보는 문법인데.. 혹시 어떤 경우에 쓰는건가요?

position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 99999
};

export const modalContainerStyle: React.CSSProperties = {
backgroundColor: 'white',
padding: '24px',
borderRadius: '12px',
maxWidth: '400px',
width: '100%',
margin: '16px',
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
position: 'relative'
};

export const headerStyle: React.CSSProperties = {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '16px'
};

export const titleStyle: React.CSSProperties = {
fontSize: '18px',
fontWeight: '600',
margin: 0
};

export const closeButtonStyle: React.CSSProperties = {
background: 'none',
border: 'none',
fontSize: '20px',
cursor: 'pointer',
color: '#666'
};

export const buttonContainerStyle: React.CSSProperties = {
marginBottom: '16px'
};

export const shareButtonStyle: React.CSSProperties = {
width: '100%',
padding: '12px',
border: '1px solid #ddd',
borderRadius: '8px',
backgroundColor: 'white',
cursor: 'pointer',
marginBottom: '8px',
textAlign: 'left',
display: 'flex',
alignItems: 'center',
gap: '8px'
};

export const linkCopyContainerStyle: React.CSSProperties = {
display: 'block',
fontSize: '14px',
fontWeight: '500',
marginBottom: '8px'
};

export const inputContainerStyle: React.CSSProperties = {
display: 'flex'
};

export const inputStyle: React.CSSProperties = {
flex: 1,
padding: '8px',
border: '1px solid #ddd',
borderRadius: '6px 0 0 6px',
backgroundColor: '#f9f9f9',
fontSize: '14px'
};

export const copyButtonStyle: React.CSSProperties = {
padding: '8px 16px',
backgroundColor: '#3b82f6',
color: 'white',
border: 'none',
borderRadius: '0 6px 6px 0',
cursor: 'pointer'
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React from 'react';
import { createPortal } from 'react-dom';
import * as styles from './ShareModal.styles';

interface ShareModalProps {
isOpen: boolean;
onClose: () => void;
shareData: {
url: string;
title: string;
text?: string;
};
}

const ShareModal: React.FC<ShareModalProps> = ({ isOpen, onClose, shareData }) => {
Copy link
Member

Choose a reason for hiding this comment

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

이 부분도요

const copyToClipboard = async (): Promise<void> => {
try {
await navigator.clipboard.writeText(shareData.url);
alert('링크가 복사되었습니다');
} catch (error) {
console.error('복사 실패:', error);
}
};
Comment on lines +16 to +23
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

클립보드 API 사용에 적절한 피드백 UI를 제공해주세요.

alert() 사용은 사용자 경험에 좋지 않습니다. 토스트 알림이나 인라인 피드백을 사용하는 것이 더 좋습니다.

+import { useState } from 'react';

const ShareModal: React.FC<ShareModalProps> = ({ isOpen, onClose, shareData }) => {
+  const [copySuccess, setCopySuccess] = useState<string | null>(null);

  const copyToClipboard = async (): Promise<void> => {
    try {
      await navigator.clipboard.writeText(shareData.url);
-      alert('링크가 복사되었습니다');
+      setCopySuccess('링크가 복사되었습니다');
+      setTimeout(() => setCopySuccess(null), 2000);
    } catch (error) {
      console.error('복사 실패:', error);
+      setCopySuccess('복사에 실패했습니다');
+      setTimeout(() => setCopySuccess(null), 2000);
    }
  };
📝 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 copyToClipboard = async (): Promise<void> => {
try {
await navigator.clipboard.writeText(shareData.url);
alert('링크가 복사되었습니다');
} catch (error) {
console.error('복사 실패:', error);
}
};
// at the top of the file, alongside your other React imports
-import React from 'react';
+import React, { useState } from 'react';
const ShareModal: React.FC<ShareModalProps> = ({ isOpen, onClose, shareData }) => {
+ const [copySuccess, setCopySuccess] = useState<string | null>(null);
const copyToClipboard = async (): Promise<void> => {
try {
await navigator.clipboard.writeText(shareData.url);
- alert('링크가 복사되었습니다');
+ setCopySuccess('링크가 복사되었습니다');
+ setTimeout(() => setCopySuccess(null), 2000);
} catch (error) {
console.error('복사 실패:', error);
+ setCopySuccess('복사에 실패했습니다');
+ setTimeout(() => setCopySuccess(null), 2000);
}
};
// ...rest of your component, and make sure to render `copySuccess` somewhere
// (e.g. inline message or toast component) so the user actually sees it.
};
🤖 Prompt for AI Agents
In frontend/src/pages/ClubDetailPage/components/Share/ShareModal/ShareModal.tsx
around lines 16 to 23, replace the alert() call in the copyToClipboard function
with a non-blocking user feedback mechanism such as a toast notification or
inline message. Implement a toast notification component or use an existing one
to show a brief success message when the URL is copied, improving user
experience by avoiding disruptive alert dialogs.


const shareToSocial = (platform: string): void => {
const encodedUrl = encodeURIComponent(shareData.url);
const encodedTitle = encodeURIComponent(shareData.title);
const encodedText = encodeURIComponent(shareData.text || '');

let shareUrl = '';

switch (platform) {
case 'facebook':
navigator.clipboard.writeText(shareData.url);
alert('링크가 복사되었습니다. 페이스북 앱에서 붙여넣기 해주세요.');
shareUrl = `https://www.facebook.com/sharer.php?u=${encodedUrl}`;
break;
case 'x':
shareUrl = `https://twitter.com/intent/tweet?url=${encodedUrl}&text=${encodedTitle}`;
break;
case 'instagram':
navigator.clipboard.writeText(shareData.url);
alert('링크가 복사되었습니다. 인스타그램 앱에서 붙여넣기 해주세요.');
return;
}
Comment on lines +32 to +45
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Facebook과 Instagram 공유 방식을 재검토해주세요.

현재 Facebook과 Instagram의 경우 실제 공유가 아닌 링크 복사만 수행합니다. 사용자가 직접 붙여넣기를 해야 하는 방식은 사용자 경험이 좋지 않을 수 있습니다.

Facebook Sharer API가 여전히 작동하는지 확인해보겠습니다:


🌐 Web query:

Does Facebook Sharer API (https://www.facebook.com/sharer.php) still work in 2024? What are the current best practices for Facebook sharing?

💡 Result:

As of 2024, the Facebook Sharer API, accessible via https://www.facebook.com/sharer.php, remains operational. This API allows developers to create links that enable users to share content directly to their Facebook feeds. However, it's important to note that Facebook's platform and APIs are continually evolving. Therefore, it's advisable to consult the official Facebook for Developers documentation for the most current information and best practices.

To enhance the effectiveness of your content sharing on Facebook, consider implementing the following best practices:

  1. Utilize Open Graph Meta Tags: Incorporate Open Graph tags into your website's HTML to control how your content appears when shared. This ensures that the title, description, and image are displayed as intended, providing a richer user experience. (developers.facebook.com)

  2. Optimize Image Quality and Size: Use high-resolution images (at least 1080 pixels wide) to ensure clarity and visual appeal. Properly sized images prevent distortion and enhance the overall presentation of your shared content. (developers.facebook.com)

  3. Implement the Facebook Crawler: Ensure that your website is accessible to Facebook's crawler by enabling GZIP and/or deflate encoding on your web server. This allows Facebook to fetch and display your content accurately when shared. (developers.facebook.com)

  4. Leverage Facebook's Native Features: Incorporate Facebook's native features, such as Stories, Reels, and live videos, into your content strategy. These formats are designed to capture user attention and foster engagement, aligning with Facebook's algorithmic preferences. (blog.hootsuite.com)

  5. Engage with Your Audience: Prompt users to interact with your content by asking open-ended questions, conducting polls, or encouraging comments. Meaningful interactions signal to Facebook that your content is valuable, potentially increasing its reach. (blog.hootsuite.com)

  6. Monitor and Adapt to Algorithm Changes: Stay informed about updates to Facebook's algorithm and adjust your content strategy accordingly. For instance, the algorithm may prioritize content that fosters positive interactions and engagement. (blog.hootsuite.com)

By adhering to these best practices, you can enhance the visibility and engagement of your content on Facebook, ensuring a more effective sharing experience for your audience.

Citations:


Facebook 및 Instagram 공유 UX 개선 필요

현재 Facebook과 Instagram 공유 로직은 단순히 링크 복사 후 사용자가 직접 붙여넣기를 해야 하는 흐름으로, 사용자 경험이 떨어집니다. 2024년 기준 Facebook Sharer API(https://www.facebook.com/sharer.php)는 여전히 정상 동작하므로 복사 대신 다이얼로그를 직접 열어 공유하도록 변경하고, Instagram은 Web Share API 사용 또는 적절한 안내를 추가하는 것을 권장드립니다.

수정 위치

  • frontend/src/pages/ClubDetailPage/components/Share/ShareModal/ShareModal.tsx (32–45행)

예시 코드 변경안:

- case 'facebook':
-   navigator.clipboard.writeText(shareData.url);
-   alert('링크가 복사되었습니다. 페이스북 앱에서 붙여넣기 해주세요.');
-   shareUrl = `https://www.facebook.com/sharer.php?u=${encodedUrl}`;
-   break;
+ case 'facebook':
+   window.open(`https://www.facebook.com/sharer.php?u=${encodedUrl}`, '_blank');
+   break;

- case 'instagram':
-   navigator.clipboard.writeText(shareData.url);
-   alert('링크가 복사되었습니다. 인스타그램 앱에서 붙여넣기 해주세요.');
-   return;
+ case 'instagram':
+   if (navigator.share) {
+     navigator.share({ title: shareData.title, url: shareData.url });
+   } else {
+     alert('인스타그램 웹 공유는 지원되지 않습니다. 링크를 복사합니다.');
+     navigator.clipboard.writeText(shareData.url);
+   }
+   return;

추가 검토 사항

  • 페이지 <head>에 Open Graph 메타 태그가 올바르게 설정되었는지 확인
  • Web Share API 호환성 및 예외 처리 여부 점검
📝 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
switch (platform) {
case 'facebook':
navigator.clipboard.writeText(shareData.url);
alert('링크가 복사되었습니다. 페이스북 앱에서 붙여넣기 해주세요.');
shareUrl = `https://www.facebook.com/sharer.php?u=${encodedUrl}`;
break;
case 'x':
shareUrl = `https://twitter.com/intent/tweet?url=${encodedUrl}&text=${encodedTitle}`;
break;
case 'instagram':
navigator.clipboard.writeText(shareData.url);
alert('링크가 복사되었습니다. 인스타그램 앱에서 붙여넣기 해주세요.');
return;
}
switch (platform) {
case 'facebook':
window.open(`https://www.facebook.com/sharer.php?u=${encodedUrl}`, '_blank');
break;
case 'x':
shareUrl = `https://twitter.com/intent/tweet?url=${encodedUrl}&text=${encodedTitle}`;
break;
case 'instagram':
if (navigator.share) {
navigator.share({ title: shareData.title, url: shareData.url });
} else {
alert('인스타그램 웹 공유는 지원되지 않습니다. 링크를 복사합니다.');
navigator.clipboard.writeText(shareData.url);
}
return;
}
🤖 Prompt for AI Agents
In frontend/src/pages/ClubDetailPage/components/Share/ShareModal/ShareModal.tsx
between lines 32 and 45, improve the Facebook and Instagram sharing UX by
replacing the clipboard copy and alert approach. For Facebook, open the sharer
URL in a new window or tab instead of copying the link, using the existing
Facebook Sharer API URL. For Instagram, implement the Web Share API if supported
to trigger the native share dialog, and if not supported, provide a clear
user-friendly message guiding manual sharing. Also, ensure to handle any
exceptions from the Web Share API gracefully. Verify that Open Graph meta tags
are correctly set in the page head to support rich sharing previews.


if (shareUrl) {
window.open(shareUrl, '_blank', 'width=600,height=400');
}
};
Comment on lines +25 to +50
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

소셜 공유 로직을 개선해주세요.

현재 구현에서 몇 가지 문제점이 있습니다:

  1. Facebook 공유가 실제 공유 대화상자를 열지 않고 단순히 링크만 복사함
  2. 플랫폼별 로직이 복잡하여 별도 함수로 분리 필요
  3. alert() 사용으로 인한 UX 문제

플랫폼별 공유 로직을 별도 훅이나 유틸리티 함수로 분리하는 것을 고려해보세요:

// utils/socialShare.ts
export const createShareUrl = (platform: string, data: ShareData) => {
  const { url, title } = data;
  const encodedUrl = encodeURIComponent(url);
  const encodedTitle = encodeURIComponent(title);
  
  switch (platform) {
    case 'facebook':
      return `https://www.facebook.com/sharer.php?u=${encodedUrl}`;
    case 'x':
      return `https://twitter.com/intent/tweet?url=${encodedUrl}&text=${encodedTitle}`;
    default:
      return '';
  }
};
🤖 Prompt for AI Agents
In frontend/src/pages/ClubDetailPage/components/Share/ShareModal/ShareModal.tsx
between lines 25 and 50, the social sharing logic has issues: Facebook sharing
only copies the link instead of opening the share dialog, platform-specific
logic is tangled, and alert() usage harms UX. Refactor by extracting
platform-specific share URL creation into a separate utility function or hook,
removing clipboard copying and alert calls from this component. Then update the
shareToSocial function to open the correct share URL in a new window for
supported platforms, improving clarity and user experience.


if (!isOpen) return null;

const modalContent = (
<div style={styles.overlayStyle} onClick={onClose}>
Copy link
Member

Choose a reason for hiding this comment

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

div를 styled-components로 const modalContent = styled.div``;처럼 사용할 수 있어요.

<div style={styles.modalContainerStyle} onClick={(e) => e.stopPropagation()}>
{/* 헤더 */}
<div style={styles.headerStyle}>
<h3 style={styles.titleStyle}>공유하기</h3>
<button onClick={onClose} style={styles.closeButtonStyle}>
</button>
</div>

{/* 공유 버튼들 */}
<div style={styles.buttonContainerStyle}>
<button onClick={() => shareToSocial('facebook')} style={styles.shareButtonStyle}>
📘 Facebook에서 공유
</button>

<button onClick={() => shareToSocial('x')} style={styles.shareButtonStyle}>
❌ X(Twitter)에서 공유
</button>

<button onClick={() => shareToSocial('instagram')} style={styles.shareButtonStyle}>
📷 Instagram에서 공유
</button>
</div>

{/* 링크 복사 */}
<div>
<label style={styles.linkCopyContainerStyle}>
링크 복사
</label>
<div style={styles.inputContainerStyle}>
<input
type="text"
value={shareData.url}
readOnly
style={styles.inputStyle}
/>
<button onClick={copyToClipboard} style={styles.copyButtonStyle}>
복사
</button>
</div>
</div>
</div>
</div>
);

return createPortal(modalContent, document.body);
};

export default ShareModal;