130 lines
4.2 KiB
TypeScript
130 lines
4.2 KiB
TypeScript
import React, { useRef, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { API_URL, toggleVideoLike, isLoggedIn } from '../utils/api';
|
|
import { buildContentShareUrl, tryNativeShare } from '../utils/nativeShare';
|
|
import LoginPromptModal from './LoginPromptModal';
|
|
|
|
interface ContentCardSocialActionsProps {
|
|
videoId: number;
|
|
/**
|
|
* 콘텐츠 종류. video.id 와 ssul_content.id 가 겹치므로 좋아요 API 와
|
|
* 공유 URL(/video/{id} vs /ssul/{id}) 분기에 반드시 필요하다.
|
|
* ContentType 전체가 아니라 이 둘로 좁힌 이유: 좋아요·공유 축은 video·ssul
|
|
* 에만 있다 — P2V 항목은 이 컴포넌트를 렌더하지 않는다(MyContentsPage 분기).
|
|
*/
|
|
contentType?: 'video' | 'ssul';
|
|
storeName: string;
|
|
region?: string;
|
|
title?: string | null;
|
|
description?: string | null;
|
|
commentCount: number;
|
|
initialLikeCount: number;
|
|
initialIsLiked?: boolean;
|
|
}
|
|
|
|
const ContentCardSocialActions: React.FC<ContentCardSocialActionsProps> = ({
|
|
videoId,
|
|
contentType = 'video',
|
|
storeName,
|
|
region,
|
|
title,
|
|
description,
|
|
commentCount,
|
|
initialLikeCount,
|
|
initialIsLiked = false,
|
|
}) => {
|
|
const { t } = useTranslation();
|
|
|
|
const [likeCount, setLikeCount] = useState(initialLikeCount);
|
|
const [isLiked, setIsLiked] = useState(initialIsLiked);
|
|
const [showLoginModal, setShowLoginModal] = useState(false);
|
|
const likeDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
const shareUrl = buildContentShareUrl(API_URL, videoId, contentType);
|
|
const shareTitle = title || storeName || t('videoDetail.kakaoDefaultTitle');
|
|
|
|
const handleShareBtnClick = async (e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
|
|
const handled = await tryNativeShare({
|
|
title: shareTitle,
|
|
url: shareUrl,
|
|
});
|
|
if (handled) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await navigator.clipboard.writeText(shareUrl);
|
|
} catch {
|
|
// clipboard API 미지원 환경에서는 무시
|
|
}
|
|
};
|
|
|
|
const handleLikeClick = (e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
|
|
if (!isLoggedIn()) {
|
|
setShowLoginModal(true);
|
|
return;
|
|
}
|
|
|
|
const prevLiked = isLiked;
|
|
const prevCount = likeCount;
|
|
|
|
setIsLiked(!prevLiked);
|
|
setLikeCount(prevLiked ? prevCount - 1 : prevCount + 1);
|
|
|
|
if (likeDebounceRef.current) clearTimeout(likeDebounceRef.current);
|
|
likeDebounceRef.current = setTimeout(async () => {
|
|
try {
|
|
const res = await toggleVideoLike(String(videoId), contentType);
|
|
setIsLiked(res.is_liked);
|
|
setLikeCount(res.like_count);
|
|
} catch (err) {
|
|
console.error('Failed to toggle like:', err);
|
|
setIsLiked(prevLiked);
|
|
setLikeCount(prevCount);
|
|
}
|
|
}, 500);
|
|
};
|
|
|
|
return (
|
|
<div className="content-card-social" onClick={(e) => e.stopPropagation()}>
|
|
<button
|
|
className={`content-card-like-btn ${isLiked ? 'liked' : ''}`}
|
|
onClick={handleLikeClick}
|
|
>
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill={isLiked ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="2">
|
|
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
|
|
</svg>
|
|
{likeCount}
|
|
</button>
|
|
|
|
<span className="content-card-comment">
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
|
</svg>
|
|
{commentCount}
|
|
</span>
|
|
|
|
<button
|
|
className="content-card-share-btn"
|
|
onClick={handleShareBtnClick}
|
|
title={t('videoDetail.share')}
|
|
>
|
|
<svg width="15" height="15" 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" />
|
|
<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>
|
|
</button>
|
|
|
|
{showLoginModal && (
|
|
<LoginPromptModal onClose={() => setShowLoginModal(false)} />
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ContentCardSocialActions;
|