diff --git a/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx b/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx index fb0878ef7..7d93128b8 100644 --- a/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx +++ b/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx @@ -54,6 +54,8 @@ const ClubDetailPage = () => { recruitmentPeriod={clubDetail.recruitmentPeriod} recruitmentForm={clubDetail.recruitmentForm} presidentPhoneNumber={clubDetail.presidentPhoneNumber} + clubId={clubId || ''} + description={clubDetail.description} /> diff --git a/frontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.styles.ts b/frontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.styles.ts index c8c169f55..d41b38bf9 100644 --- a/frontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.styles.ts +++ b/frontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.styles.ts @@ -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; + } + + @media (max-width: 500px) { + gap: 6px; + + .share-button { + padding: 4px 8px !important; + font-size: 11px !important; + min-width: auto !important; + border-radius: 4px !important; + } + } +`; diff --git a/frontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.tsx b/frontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.tsx index 028c5744f..534f02b06 100644 --- a/frontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.tsx +++ b/frontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.tsx @@ -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; @@ -12,6 +13,8 @@ interface ClubDetailHeaderProps { recruitmentPeriod: string; recruitmentForm?: string; presidentPhoneNumber?: string; + clubId: string; + description: string; } const ClubDetailHeader = ({ @@ -23,6 +26,8 @@ const ClubDetailHeader = ({ recruitmentPeriod, recruitmentForm, presidentPhoneNumber, + clubId, + description }: ClubDetailHeaderProps) => { const { recruitmentStart, recruitmentEnd } = parseRecruitmentPeriod(recruitmentPeriod); @@ -33,6 +38,10 @@ const ClubDetailHeader = ({ new Date(), ); + const shareUrl = `${window.location.origin}/club/${clubId}`; + const shareTitle = `${name} - 동아리 정보`; + const shareText = `${description}`; + return ( - + + + + ); }; diff --git a/frontend/src/pages/ClubDetailPage/components/Share/ShareButton/ShareButton.tsx b/frontend/src/pages/ClubDetailPage/components/Share/ShareButton/ShareButton.tsx new file mode 100644 index 000000000..35ed714d9 --- /dev/null +++ b/frontend/src/pages/ClubDetailPage/components/Share/ShareButton/ShareButton.tsx @@ -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 = ({ + url, + title, + text, + buttonText = '공유하기', + className = '' +}) => { + const [showModal, setShowModal] = useState(false); + + const isWebShareSupported = typeof navigator !== 'undefined' && 'share' in navigator; + + const handleShare = async (): Promise => { + 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 ( + <> + + {buttonText} + + + setShowModal(false)} + shareData={{ url, title, text }} + /> + > + ); +}; + + +export default ShareButton; diff --git a/frontend/src/pages/ClubDetailPage/components/Share/ShareModal/ShareModal.styles.ts b/frontend/src/pages/ClubDetailPage/components/Share/ShareModal/ShareModal.styles.ts new file mode 100644 index 000000000..8a590106c --- /dev/null +++ b/frontend/src/pages/ClubDetailPage/components/Share/ShareModal/ShareModal.styles.ts @@ -0,0 +1,91 @@ +export const overlayStyle: React.CSSProperties = { + 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' +}; diff --git a/frontend/src/pages/ClubDetailPage/components/Share/ShareModal/ShareModal.tsx b/frontend/src/pages/ClubDetailPage/components/Share/ShareModal/ShareModal.tsx new file mode 100644 index 000000000..608cde45e --- /dev/null +++ b/frontend/src/pages/ClubDetailPage/components/Share/ShareModal/ShareModal.tsx @@ -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 = ({ isOpen, onClose, shareData }) => { + const copyToClipboard = async (): Promise => { + try { + await navigator.clipboard.writeText(shareData.url); + alert('링크가 복사되었습니다'); + } catch (error) { + console.error('복사 실패:', error); + } + }; + + 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; + } + + if (shareUrl) { + window.open(shareUrl, '_blank', 'width=600,height=400'); + } + }; + + if (!isOpen) return null; + + const modalContent = ( + + e.stopPropagation()}> + {/* 헤더 */} + + 공유하기 + + ✕ + + + + {/* 공유 버튼들 */} + + shareToSocial('facebook')} style={styles.shareButtonStyle}> + 📘 Facebook에서 공유 + + + shareToSocial('x')} style={styles.shareButtonStyle}> + ❌ X(Twitter)에서 공유 + + + shareToSocial('instagram')} style={styles.shareButtonStyle}> + 📷 Instagram에서 공유 + + + + {/* 링크 복사 */} + + + 링크 복사 + + + + + 복사 + + + + + + ); + + return createPortal(modalContent, document.body); +}; + +export default ShareModal;