o2o-castad-frontend/src/components/ContentCardSocialActions.tsx
2026-08-26 13:53:10 +09:00

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;