import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { getVideoById, getVideoComments, postVideoComment, deleteComment, toggleVideoLike, isLoggedIn, getUserMe, API_URL, } from '../utils/api'; import { VideoDetailItem, CommentItem, UserMeResponse } from '../types/api'; import { buildVideoShareUrl, tryNativeShare } from '../utils/nativeShare'; 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 [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 [currentUser, setCurrentUser] = useState(null); 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]); useEffect(() => { if (!authed) return; getUserMe().then(setCurrentUser).catch((err) => { console.error('Failed to fetch current user:', err); }); }, [authed]); const formatDate = (dateString: string) => { const date = new Date(dateString); return t('videoDetail.dateFormat', { year: date.getFullYear(), month: date.getMonth() + 1, day: 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 = buildVideoShareUrl(API_URL, videoId); const shareTitle = video?.title || video?.store_name || t('videoDetail.kakaoDefaultTitle'); const handleCopyLink = async () => { try { await navigator.clipboard.writeText(shareUrl); } catch { // clipboard API 미지원 환경에서는 무시 } setCopied(true); setTimeout(() => setCopied(false), 2000); }; const handleShareButtonClick = async () => { const handled = await tryNativeShare({ title: shareTitle, url: shareUrl, }); if (handled) { return; } await handleCopyLink(); }; 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()); setCommentInput(''); if (commentTextareaRef.current) { commentTextareaRef.current.style.height = 'auto'; } 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 {t('videoDetail.deletedComment')}; return content; }; return (
{/* 헤더 */}
{isModal ? ( ) : ( )}
{loading ? (

{t('ado2Contents.loading')}

) : error ? (

{error}

) : video ? (