fix: 이전 공유하기 기능 제거

This commit is contained in:
김성경 2026-08-18 17:23:42 +09:00
parent e400884eb7
commit 63d60caabd
3 changed files with 28 additions and 235 deletions

View File

@ -1,5 +1,4 @@
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; import React, { useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { API_URL, toggleVideoLike } from '../utils/api'; import { API_URL, toggleVideoLike } from '../utils/api';
import { buildVideoShareUrl, tryNativeShare } from '../utils/nativeShare'; import { buildVideoShareUrl, tryNativeShare } from '../utils/nativeShare';
@ -25,96 +24,29 @@ const ContentCardSocialActions: React.FC<ContentCardSocialActionsProps> = ({
const [likeCount, setLikeCount] = useState(initialLikeCount); const [likeCount, setLikeCount] = useState(initialLikeCount);
const [isLiked, setIsLiked] = useState(initialIsLiked); const [isLiked, setIsLiked] = useState(initialIsLiked);
const [copied, setCopied] = useState(false);
const [shareMenuOpen, setShareMenuOpen] = useState(false);
const [menuPos, setMenuPos] = useState<{ top: number; left: number } | null>(null);
const [anchorRect, setAnchorRect] = useState<{ top: number; bottom: number; left: number } | null>(null);
const shareBtnRef = useRef<HTMLButtonElement>(null);
const shareMenuRef = useRef<HTMLDivElement>(null);
const likeDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null); const likeDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const shareUrl = buildVideoShareUrl(API_URL, videoId); const shareUrl = buildVideoShareUrl(API_URL, videoId);
const shareTitle = storeName || t('videoDetail.kakaoDefaultTitle'); const shareTitle = storeName || t('videoDetail.kakaoDefaultTitle');
const shareDescription = t('videoDetail.kakaoDescription', { region: region ?? '' }); const shareDescription = t('videoDetail.kakaoDescription', { region: region ?? '' });
useEffect(() => { const handleShareBtnClick = async (e: React.MouseEvent) => {
if (!shareMenuOpen) return; e.stopPropagation();
const closeMenu = () => setShareMenuOpen(false); const handled = await tryNativeShare({
title: shareTitle,
const handleClickOutside = (e: MouseEvent) => { text: shareDescription,
if ( url: shareUrl,
shareMenuRef.current && !shareMenuRef.current.contains(e.target as Node) && });
shareBtnRef.current && !shareBtnRef.current.contains(e.target as Node) if (handled) {
) { return;
closeMenu();
}
};
document.addEventListener('mousedown', handleClickOutside);
window.addEventListener('scroll', closeMenu, true);
window.addEventListener('resize', closeMenu);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
window.removeEventListener('scroll', closeMenu, true);
window.removeEventListener('resize', closeMenu);
};
}, [shareMenuOpen]);
// 메뉴가 뷰포트 밖으로 넘어가지 않도록 실제 렌더된 크기 기준으로 위치 보정
useLayoutEffect(() => {
if (!shareMenuOpen || !anchorRect || !shareMenuRef.current) return;
const margin = 8;
const menuRect = shareMenuRef.current.getBoundingClientRect();
let left = anchorRect.left;
if (left + menuRect.width > window.innerWidth - margin) {
left = window.innerWidth - menuRect.width - margin;
} }
left = Math.max(margin, left);
let top = anchorRect.bottom + 6;
if (top + menuRect.height > window.innerHeight - margin) {
top = anchorRect.top - menuRect.height - 6;
}
top = Math.max(margin, top);
setMenuPos((prev) => (prev && prev.top === top && prev.left === left ? prev : { top, left }));
}, [shareMenuOpen, anchorRect]);
const handleCopyLink = async () => {
try { try {
await navigator.clipboard.writeText(shareUrl); await navigator.clipboard.writeText(shareUrl);
} catch { } catch {
// clipboard API 미지원 환경에서는 무시 // clipboard API 미지원 환경에서는 무시
} }
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const handleKakaoShare = () => {
const kakao = window.Kakao;
if (kakao?.Share) {
// 공유 URL(백엔드 OG 페이지)을 스크랩해 제목/설명/이미지를 자동으로 채운다.
// 다른 채널과 동일한 shareUrl을 쓰므로 카카오만을 위한 별도 콘텐츠 지정이 필요 없다.
kakao.Share.sendScrap({ requestUrl: shareUrl });
} else if (navigator.share) {
navigator.share({ title: shareTitle, text: shareDescription, url: shareUrl }).catch(() => {});
} else {
handleCopyLink();
}
setShareMenuOpen(false);
};
const handleFacebookShare = () => {
window.open(`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(shareUrl)}`, '_blank', 'noopener,width=600,height=600');
setShareMenuOpen(false);
};
const handleTwitterShare = () => {
window.open(`https://twitter.com/intent/tweet?url=${encodeURIComponent(shareUrl)}`, '_blank', 'noopener,width=600,height=600');
setShareMenuOpen(false);
}; };
const handleLikeClick = (e: React.MouseEvent) => { const handleLikeClick = (e: React.MouseEvent) => {
@ -140,28 +72,6 @@ const ContentCardSocialActions: React.FC<ContentCardSocialActionsProps> = ({
}, 500); }, 500);
}; };
const handleShareBtnClick = async (e: React.MouseEvent) => {
e.stopPropagation();
const handled = await tryNativeShare({
title: shareTitle,
text: shareDescription,
url: shareUrl,
});
if (handled) {
setShareMenuOpen(false);
return;
}
const rect = shareBtnRef.current?.getBoundingClientRect();
if (rect) {
// 초기 추정 위치(측정 전 첫 렌더용) — useLayoutEffect가 실제 크기 기준으로 다시 보정한다
setAnchorRect({ top: rect.top, bottom: rect.bottom, left: rect.left });
setMenuPos({ top: rect.bottom + 6, left: rect.left });
}
setShareMenuOpen((v) => !v);
};
return ( return (
<div className="content-card-social" onClick={(e) => e.stopPropagation()}> <div className="content-card-social" onClick={(e) => e.stopPropagation()}>
<button <button
@ -182,7 +92,6 @@ const ContentCardSocialActions: React.FC<ContentCardSocialActionsProps> = ({
</span> </span>
<button <button
ref={shareBtnRef}
className="content-card-share-btn" className="content-card-share-btn"
onClick={handleShareBtnClick} onClick={handleShareBtnClick}
title={t('videoDetail.share')} title={t('videoDetail.share')}
@ -192,42 +101,6 @@ const ContentCardSocialActions: React.FC<ContentCardSocialActionsProps> = ({
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49" /><line x1="15.41" y1="6.51" x2="8.59" y2="10.49" /> <line x1="8.59" y1="13.51" x2="15.42" y2="17.49" /><line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />
</svg> </svg>
</button> </button>
{shareMenuOpen && menuPos && createPortal(
<div
ref={shareMenuRef}
className="video-detail-share-menu"
style={{ position: 'fixed', top: menuPos.top, left: menuPos.left }}
>
<button className="video-detail-share-item" onClick={handleKakaoShare}>
<svg width="18" height="18" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="20" height="20" rx="4" fill="#FEE500" />
<path fillRule="evenodd" clipRule="evenodd" d="M10 3.5C6.134 3.5 3 6.01 3 9.1c0 1.98 1.2 3.72 3.01 4.76l-.74 2.75a.19.19 0 0 0 .28.21l3.37-2.23c.34.04.69.06 1.06.06 3.866 0 7-2.51 7-5.6S13.866 3.5 10 3.5z" fill="#3C1E1E" />
</svg>
{t('videoDetail.shareKakao')}
</button>
<button className="video-detail-share-item" onClick={handleFacebookShare}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="#1877F2">
<path d="M24 12.073C24 5.405 18.627 0 12 0S0 5.405 0 12.073C0 18.1 4.388 23.094 10.125 24v-8.437H7.078v-3.49h3.047V9.41c0-3.025 1.792-4.697 4.533-4.697 1.312 0 2.686.235 2.686.235v2.97h-1.513c-1.491 0-1.956.93-1.956 1.887v2.268h3.328l-.532 3.49h-2.796V24C19.612 23.094 24 18.1 24 12.073z" />
</svg>
{t('videoDetail.shareFacebook')}
</button>
<button className="video-detail-share-item" onClick={handleTwitterShare}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
{t('videoDetail.shareTwitter')}
</button>
<button className="video-detail-share-item" onClick={() => { handleCopyLink(); setShareMenuOpen(false); }}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<rect x="9" y="9" width="13" height="13" rx="2" />
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
</svg>
{copied ? t('videoDetail.copied') : t('videoDetail.copyUrl')}
</button>
</div>,
document.body
)}
</div> </div>
); );
}; };

View File

@ -32,7 +32,6 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
const [isLiked, setIsLiked] = useState(false); const [isLiked, setIsLiked] = useState(false);
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const [shareMenuOpen, setShareMenuOpen] = useState(false);
const [isLandscape, setIsLandscape] = useState(false); const [isLandscape, setIsLandscape] = useState(false);
const [showLoginModal, setShowLoginModal] = useState(false); const [showLoginModal, setShowLoginModal] = useState(false);
@ -107,42 +106,6 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
const shareTitle = video?.store_name ?? t('videoDetail.kakaoDefaultTitle'); const shareTitle = video?.store_name ?? t('videoDetail.kakaoDefaultTitle');
const shareDescription = t('videoDetail.kakaoDescription', { region: video?.region ?? '' }); const shareDescription = t('videoDetail.kakaoDescription', { region: video?.region ?? '' });
const handleCopyLink = async () => {
try {
await navigator.clipboard.writeText(shareUrl);
} catch {
// clipboard API 미지원 환경에서는 무시
}
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const handleKakaoShare = () => {
const kakao = window.Kakao;
if (kakao?.Share) {
// 공유 URL(백엔드 OG 페이지)을 스크랩해 제목/설명/이미지를 자동으로 채운다.
// 다른 채널과 동일한 shareUrl을 쓰므로 카카오만을 위한 별도 콘텐츠 지정이 필요 없다.
kakao.Share.sendScrap({ requestUrl: shareUrl });
} else if (navigator.share) {
navigator.share({ title: shareTitle, text: shareDescription, url: shareUrl }).catch(() => {});
} else {
handleCopyLink();
}
setShareMenuOpen(false);
};
const handleFacebookShare = () => {
window.open(`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(shareUrl)}`, '_blank', 'noopener,width=600,height=600');
setShareMenuOpen(false);
};
const handleTwitterShare = () => {
window.open(`https://twitter.com/intent/tweet?url=${encodeURIComponent(shareUrl)}`, '_blank', 'noopener,width=600,height=600');
setShareMenuOpen(false);
};
const shareMenuRef = React.useRef<HTMLDivElement>(null);
const handleShareButtonClick = async () => { const handleShareButtonClick = async () => {
const handled = await tryNativeShare({ const handled = await tryNativeShare({
title: shareTitle, title: shareTitle,
@ -150,24 +113,18 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
url: shareUrl, url: shareUrl,
}); });
if (handled) { if (handled) {
setShareMenuOpen(false);
return; return;
} }
setShareMenuOpen((open) => !open); try {
await navigator.clipboard.writeText(shareUrl);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
// clipboard API 미지원 환경에서는 무시
}
}; };
useEffect(() => {
if (!shareMenuOpen) return;
const handleClickOutside = (e: MouseEvent) => {
if (shareMenuRef.current && !shareMenuRef.current.contains(e.target as Node)) {
setShareMenuOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [shareMenuOpen]);
const likeDebounceRef = React.useRef<ReturnType<typeof setTimeout> | null>(null); const likeDebounceRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
const handleLike = () => { const handleLike = () => {
@ -292,52 +249,16 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
</svg> </svg>
{likeCount} {likeCount}
</button> </button>
<div style={{ position: 'relative' }} ref={shareMenuRef}> <button
<button className="video-detail-copy-btn"
className="video-detail-copy-btn" onClick={handleShareButtonClick}
onClick={handleShareButtonClick} >
> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/>
<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/> <line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/> </svg>
</svg> {copied ? t('videoDetail.copied') : t('videoDetail.share')}
{copied ? t('videoDetail.copied') : t('videoDetail.share')} </button>
</button>
{shareMenuOpen && (
<div className="video-detail-share-menu">
{/* 카카오톡 */}
<button className="video-detail-share-item" onClick={handleKakaoShare}>
<svg width="18" height="18" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="20" height="20" rx="4" fill="#FEE500"/>
<path fillRule="evenodd" clipRule="evenodd" d="M10 3.5C6.134 3.5 3 6.01 3 9.1c0 1.98 1.2 3.72 3.01 4.76l-.74 2.75a.19.19 0 0 0 .28.21l3.37-2.23c.34.04.69.06 1.06.06 3.866 0 7-2.51 7-5.6S13.866 3.5 10 3.5z" fill="#3C1E1E"/>
</svg>
{t('videoDetail.shareKakao')}
</button>
{/* 페이스북 */}
<button className="video-detail-share-item" onClick={handleFacebookShare}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="#1877F2">
<path d="M24 12.073C24 5.405 18.627 0 12 0S0 5.405 0 12.073C0 18.1 4.388 23.094 10.125 24v-8.437H7.078v-3.49h3.047V9.41c0-3.025 1.792-4.697 4.533-4.697 1.312 0 2.686.235 2.686.235v2.97h-1.513c-1.491 0-1.956.93-1.956 1.887v2.268h3.328l-.532 3.49h-2.796V24C19.612 23.094 24 18.1 24 12.073z"/>
</svg>
{t('videoDetail.shareFacebook')}
</button>
{/* X (트위터) */}
<button className="video-detail-share-item" onClick={handleTwitterShare}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
</svg>
{t('videoDetail.shareTwitter')}
</button>
{/* URL 복사 */}
<button className="video-detail-share-item" onClick={() => { handleCopyLink(); setShareMenuOpen(false); }}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<rect x="9" y="9" width="13" height="13" rx="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>
{copied ? t('videoDetail.copied') : t('videoDetail.copyUrl')}
</button>
</div>
)}
</div>
</div> </div>
{/* 댓글 섹션 */} {/* 댓글 섹션 */}

View File

@ -1,8 +1,7 @@
/** /**
* . * .
* *
* * .
* .
*/ */
export async function tryNativeShare(data: ShareData): Promise<boolean> { export async function tryNativeShare(data: ShareData): Promise<boolean> {
if (typeof navigator.share !== 'function') { if (typeof navigator.share !== 'function') {