import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { getVideoById, getVideoComments, postVideoComment, deleteComment, toggleVideoLike, isLoggedIn, } from '../utils/api'; import { VideoDetailItem, CommentItem } from '../types/api'; import LoginPromptModal from './LoginPromptModal'; interface VideoDetailContentProps { videoId: string; isModal?: boolean; onClose?: () => void; } const VideoDetailContent: React.FC = ({ videoId, isModal = false, onClose }) => { const { t } = useTranslation(); const authed = isLoggedIn(); const [video, setVideo] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [likeCount, setLikeCount] = useState(0); const [isLiked, setIsLiked] = useState(false); const [copied, setCopied] = useState(false); const [shareMenuOpen, setShareMenuOpen] = useState(false); const [isLandscape, setIsLandscape] = useState(false); const [showLoginModal, setShowLoginModal] = useState(false); const [comments, setComments] = useState([]); const [commentsTotal, setCommentsTotal] = useState(0); const [commentsPage, setCommentsPage] = useState(1); const [commentsHasNext, setCommentsHasNext] = useState(false); const [commentsLoading, setCommentsLoading] = useState(false); const [commentInput, setCommentInput] = useState(''); const [commentSubmitting, setCommentSubmitting] = useState(false); const commentTextareaRef = useRef(null); const [commentNickname, setCommentNickname] = useState(''); const [commentAvatarSeedIdx, setCommentAvatarSeedIdx] = useState(0); // 고정 seed 목록: 브라우저가 캐싱하여 중복 요청 없음 const AVATAR_SEEDS = ['42', '77', '123', '256', '512', '888', '1024', '2048', '3141', '9999']; const commentAvatarSeed = AVATAR_SEEDS[commentAvatarSeedIdx % AVATAR_SEEDS.length]; const handleChangeAvatar = useCallback(() => { setCommentAvatarSeedIdx(prev => (prev + 1) % AVATAR_SEEDS.length); }, []); const fetchComments = useCallback(async (page: number, append = false) => { setCommentsLoading(true); try { const res = await getVideoComments(videoId, page, 20); const sorted = [...res.items].sort( (a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime() ); setComments(prev => append ? [...prev, ...sorted] : sorted); setCommentsTotal(res.total); setCommentsHasNext(res.has_next); setCommentsPage(page); } catch (err) { console.error('Failed to fetch comments:', err); } finally { setCommentsLoading(false); } }, [videoId]); useEffect(() => { const fetchVideo = async () => { setLoading(true); setError(null); try { const data = await getVideoById(videoId); setVideo(data); setLikeCount(data.like_count); setIsLiked(data.is_liked_by_me); } catch (err) { console.error('Failed to fetch video:', err); setError(t('ado2Contents.loadFailed')); } finally { setLoading(false); } }; fetchVideo(); fetchComments(1); }, [videoId, fetchComments]); const formatDate = (dateString: string) => { const date = new Date(dateString); return `${date.getFullYear()}년 ${date.getMonth() + 1}월 ${date.getDate()}일`; }; const formatCommentDate = (dateString: string) => { const date = new Date(dateString); return `${date.getFullYear()}.${String(date.getMonth() + 1).padStart(2, '0')}.${String(date.getDate()).padStart(2, '0')}`; }; const shareUrl = `${window.location.origin}/video/${videoId}`; 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) { kakao.Share.sendDefault({ objectType: 'feed', content: { title: video?.store_name ?? 'ADO2 영상', description: `${video?.region ?? ''} · ADO2 AI 마케팅 영상`, imageUrl: 'https://demo.castad.net/favicon_48.svg', link: { mobileWebUrl: shareUrl, webUrl: shareUrl }, }, buttons: [{ title: '영상 보기', link: { mobileWebUrl: shareUrl, webUrl: shareUrl } }], }); } else if (navigator.share) { navigator.share({ 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(null); 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 | null>(null); const handleLike = () => { if (!authed) { setShowLoginModal(true); return; } // 1. UI 즉시 업데이트 (Optimistic) setIsLiked(prev => !prev); setLikeCount(prev => isLiked ? prev - 1 : prev + 1); // 2. 기존 debounce 타이머 취소 후 재설정 if (likeDebounceRef.current) clearTimeout(likeDebounceRef.current); likeDebounceRef.current = setTimeout(async () => { const prevLiked = isLiked; const prevCount = likeCount; try { await toggleVideoLike(videoId); } catch (err) { // 3. 실패 시 롤백 console.error('Failed to toggle like:', err); setIsLiked(prevLiked); setLikeCount(prevCount); } }, 500); }; const handleHeaderAction = () => { if (!isModal && !authed) { setShowLoginModal(true); return; } onClose?.(); }; const handleCommentFocus = () => { if (!authed) setShowLoginModal(true); }; const handleCommentSubmit = async () => { if (!authed) { setShowLoginModal(true); return; } if (!commentInput.trim() || commentSubmitting) return; setCommentSubmitting(true); try { await postVideoComment(videoId, commentInput.trim(), commentNickname); setCommentInput(''); if (commentTextareaRef.current) { commentTextareaRef.current.style.height = 'auto'; } setCommentNickname(''); setCommentAvatarSeedIdx(prev => (prev + 1) % AVATAR_SEEDS.length); await fetchComments(1); } catch (err) { console.error('Failed to post comment:', err); } finally { setCommentSubmitting(false); } }; const handleDeleteComment = async (commentId: number) => { try { await deleteComment(commentId); await fetchComments(commentsPage); } catch (err) { console.error('Failed to delete comment:', err); } }; const renderCommentContent = (content: string | null, isDeleted: boolean) => { if (isDeleted) return (삭제된 댓글입니다.); return content; }; return (
{/* 헤더 */}
{isModal ? ( ) : ( )}
{loading ? (

{t('ado2Contents.loading')}

) : error ? (

{error}

) : video ? (