+
{formatDate(video.created_at)}
@@ -310,6 +310,8 @@ const MyContentsPage: React.FC
= ({ onNavigate, embedded })
contentType={video.type}
storeName={video.store_name}
region={video.region}
+ title={video.title}
+ description={video.description}
commentCount={video.comment_count ?? 0}
initialLikeCount={video.like_count ?? 0}
initialIsLiked={video.is_liked_by_me}
diff --git a/src/pages/Dashboard/MyInfoContent.tsx b/src/pages/Dashboard/MyInfoContent.tsx
index ec96981..51617d1 100644
--- a/src/pages/Dashboard/MyInfoContent.tsx
+++ b/src/pages/Dashboard/MyInfoContent.tsx
@@ -298,13 +298,13 @@ const MyInfoContent: React.FC = ({ initialTab, onNavigate })
{isConnecting === 'youtube' ? t('myInfo.youtubeConnecting') : t('myInfo.youtubeConnect')}
-
+ */}
{/* 연결된 계정 목록 */}
diff --git a/src/pages/Ssulbox/SsulDetailContent.tsx b/src/pages/Ssulbox/SsulDetailContent.tsx
index f2636cb..00e9af0 100644
--- a/src/pages/Ssulbox/SsulDetailContent.tsx
+++ b/src/pages/Ssulbox/SsulDetailContent.tsx
@@ -1,15 +1,18 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
+ API_URL,
deleteComment,
getSsulContentById,
+ getUserMe,
getVideoComments,
isLoggedIn,
postVideoComment,
toggleVideoLike,
} from '../../utils/api';
import type { SsulDetailItem } from '../../utils/api';
-import { CommentItem } from '../../types/api';
+import { CommentItem, UserMeResponse } from '../../types/api';
+import { buildContentShareUrl, tryNativeShare } from '../../utils/nativeShare';
import LoginPromptModal from '../../components/LoginPromptModal';
interface SsulDetailContentProps {
@@ -22,11 +25,11 @@ interface SsulDetailContentProps {
* 썰박스 콘텐츠 상세 — castad `VideoDetailContent` 와 같은 3층 구조의 내용부.
* (SsulViewerModal 이 모달로, SsulDetailPage 가 공유 링크 페이지로 감싼다)
*
- * UI·동작(좋아요 낙관 갱신·댓글·대댓글·공유 메뉴·비로그인 게이트)은
+ * UI·동작(좋아요 낙관 갱신·댓글·대댓글·공유·비로그인 게이트)은
* `VideoDetailContent` 와 동일하고, 다른 점은 셋뿐이다:
* 1. 상세 조회가 `GET /ssul/{id}` (video 조회를 쓰면 id 가 겹치는 남의 영상이 열린다)
* 2. 좋아요·댓글 API 에 `contentType='ssul'` 을 얹는다
- * 3. 공유 URL 이 `/ssul/{id}` 다
+ * 3. 공유 URL 이 `/ssul/share/{id}` 다
*/
const SsulDetailContent: React.FC
= ({
contentId,
@@ -44,8 +47,6 @@ const SsulDetailContent: React.FC = ({
const [isLiked, setIsLiked] = useState(false);
const [copied, setCopied] = useState(false);
- const [shareMenuOpen, setShareMenuOpen] = useState(false);
- const [shareMenuUpward, setShareMenuUpward] = useState(false);
const [isLandscape, setIsLandscape] = useState(false);
const [showLoginModal, setShowLoginModal] = useState(false);
@@ -58,16 +59,7 @@ const SsulDetailContent: React.FC = ({
const [commentSubmitting, setCommentSubmitting] = useState(false);
const commentTextareaRef = useRef(null);
- const [commentNickname, setCommentNickname] = useState('');
- const [commentAvatarSeedIdx, setCommentAvatarSeedIdx] = useState(0);
-
- // castad 와 동일한 고정 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 [currentUser, setCurrentUser] = useState(null);
const fetchComments = useCallback(
async (page: number, append = false) => {
@@ -112,6 +104,13 @@ const SsulDetailContent: React.FC = ({
fetchComments(1);
}, [contentId, fetchComments, t]);
+ 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', {
@@ -129,7 +128,7 @@ const SsulDetailContent: React.FC = ({
};
// 썰박스 공개 페이지 경로. /video/{id} 를 쓰면 id 가 겹치는 남의 영상이 열린다.
- const shareUrl = `${window.location.origin}/ssul/${contentId}`;
+ const shareUrl = buildContentShareUrl(API_URL, contentId, 'ssul');
const handleCopyLink = async () => {
try {
@@ -141,65 +140,16 @@ const SsulDetailContent: React.FC = ({
setTimeout(() => setCopied(false), 2000);
};
- const handleKakaoShare = () => {
- const kakao = window.Kakao;
- if (kakao?.Share) {
- kakao.Share.sendDefault({
- objectType: 'feed',
- content: {
- title: content?.store_name ?? t('videoDetail.kakaoDefaultTitle'),
- description: t('videoDetail.kakaoDescription', { region: content?.region ?? '' }),
- imageUrl: 'https://ado2.o2osolution.ai/favicon_48.svg',
- link: { mobileWebUrl: shareUrl, webUrl: shareUrl },
- },
- buttons: [{ title: t('videoDetail.kakaoButtonTitle'), link: { mobileWebUrl: shareUrl, webUrl: shareUrl } }],
- });
- } else if (navigator.share) {
- navigator.share({ url: shareUrl }).catch(() => {});
- } else {
- handleCopyLink();
+ const handleShareButtonClick = async () => {
+ const handled = await tryNativeShare({
+ title: content?.store_name ?? t('videoDetail.kakaoDefaultTitle'),
+ text: t('videoDetail.kakaoDescription', { region: content?.region ?? '' }),
+ url: shareUrl,
+ });
+ if (handled) {
+ return;
}
- 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 = 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]);
-
- // 모달 내부는 스크롤 영역(.video-detail-body)이 곧 화면 경계라, 버튼이 그 하단에
- // 가까우면 아래로 펼치는 메뉴가 잘려서 안 보인다. 열기 직전 남은 공간을 재서 뒤집는다.
- const handleToggleShareMenu = () => {
- if (!shareMenuOpen) {
- const btnRect = shareMenuRef.current?.getBoundingClientRect();
- if (btnRect) {
- const scrollBox = shareMenuRef.current?.closest('.video-detail-body');
- const boundaryBottom = scrollBox
- ? scrollBox.getBoundingClientRect().bottom
- : window.innerHeight;
- const ESTIMATED_MENU_HEIGHT = 220;
- setShareMenuUpward(btnRect.bottom + ESTIMATED_MENU_HEIGHT > boundaryBottom);
- }
- }
- setShareMenuOpen(v => !v);
+ await handleCopyLink();
};
const likeDebounceRef = useRef | null>(null);
@@ -252,8 +202,6 @@ const SsulDetailContent: React.FC = ({
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 ssul comment:', err);
@@ -349,48 +297,17 @@ const SsulDetailContent: React.FC = ({
{likeCount}
-
-
- {shareMenuOpen && (
-
-
-
-
-
-
- )}
-
+
{/* 댓글 섹션 */}
@@ -400,24 +317,16 @@ const SsulDetailContent: React.FC