diff --git a/index.html b/index.html index 156c788..5a8da49 100755 --- a/index.html +++ b/index.html @@ -50,7 +50,6 @@ } } - diff --git a/package-lock.json b/package-lock.json index b35197e..5a3baef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,8 @@ }, "devDependencies": { "@types/node": "^22.14.0", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", "@vitejs/plugin-react": "^5.0.0", "typescript": "~5.8.2", "vite": "^6.2.0" @@ -52,6 +54,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1288,10 +1291,32 @@ "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", @@ -1349,6 +1374,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -1400,6 +1426,13 @@ "dev": true, "license": "MIT" }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, "node_modules/d3-array": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", @@ -1691,6 +1724,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4" }, @@ -1811,6 +1845,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -1852,6 +1887,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -1861,6 +1897,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -1907,6 +1944,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -1969,7 +2007,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -2083,6 +2122,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2166,6 +2206,7 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", diff --git a/src/components/ContentCardSocialActions.tsx b/src/components/ContentCardSocialActions.tsx index 585d271..b2d1114 100644 --- a/src/components/ContentCardSocialActions.tsx +++ b/src/components/ContentCardSocialActions.tsx @@ -14,6 +14,8 @@ interface ContentCardSocialActionsProps { contentType?: ContentType; storeName: string; region?: string; + title?: string | null; + description?: string | null; commentCount: number; initialLikeCount: number; initialIsLiked?: boolean; @@ -24,6 +26,8 @@ const ContentCardSocialActions: React.FC = ({ contentType = 'video', storeName, region, + title, + description, commentCount, initialLikeCount, initialIsLiked = false, @@ -36,8 +40,8 @@ const ContentCardSocialActions: React.FC = ({ const likeDebounceRef = useRef | null>(null); const shareUrl = buildContentShareUrl(API_URL, videoId, contentType); - const shareTitle = storeName || t('videoDetail.kakaoDefaultTitle'); - const shareDescription = t('videoDetail.kakaoDescription', { region: region ?? '' }); + const shareTitle = title || storeName || t('videoDetail.kakaoDefaultTitle'); + const shareDescription = description || t('videoDetail.kakaoDescription', { region: region ?? '' }); const handleShareBtnClick = async (e: React.MouseEvent) => { e.stopPropagation(); diff --git a/src/components/VideoDetailContent.tsx b/src/components/VideoDetailContent.tsx index d21370b..e0989dd 100644 --- a/src/components/VideoDetailContent.tsx +++ b/src/components/VideoDetailContent.tsx @@ -32,7 +32,6 @@ const VideoDetailContent: React.FC = ({ videoId, isModa 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); @@ -104,8 +103,8 @@ const VideoDetailContent: React.FC = ({ videoId, isModa }; const shareUrl = buildVideoShareUrl(API_URL, videoId); - const shareTitle = video?.store_name ?? t('videoDetail.kakaoDefaultTitle'); - const shareDescription = t('videoDetail.kakaoDescription', { region: video?.region ?? '' }); + const shareTitle = video?.title || video?.store_name || t('videoDetail.kakaoDefaultTitle'); + const shareDescription = video?.description || t('videoDetail.kakaoDescription', { region: video?.region ?? '' }); const handleCopyLink = async () => { try { @@ -126,53 +125,9 @@ const VideoDetailContent: React.FC = ({ videoId, isModa if (handled) { return; } - setShareMenuOpen(v => !v); + await handleCopyLink(); }; - const handleKakaoShare = () => { - const kakao = window.Kakao; - if (kakao?.Share) { - kakao.Share.sendDefault({ - objectType: 'feed', - content: { - title: video?.store_name ?? t('videoDetail.kakaoDefaultTitle'), - description: t('videoDetail.kakaoDescription', { region: video?.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(); - } - 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 = () => { @@ -302,52 +257,17 @@ const VideoDetailContent: React.FC = ({ videoId, isModa {likeCount} -
- - {shareMenuOpen && ( -
- {/* 카카오톡 */} - - {/* 페이스북 */} - - {/* X (트위터) */} - - {/* URL 복사 */} - -
- )} -
+ {/* 댓글 섹션 */} diff --git a/src/index.tsx b/src/index.tsx index 894893b..0e5fdde 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -5,17 +5,6 @@ import './i18n'; import './styles/index.css'; import App from './App'; -declare global { - interface Window { - Kakao: any; - } -} - -const kakaoKey = import.meta.env.VITE_KAKAO_JS_KEY as string | undefined; -if (kakaoKey && window.Kakao && !window.Kakao.isInitialized()) { - window.Kakao.init(kakaoKey); -} - const rootElement = document.getElementById('root'); if (!rootElement) { throw new Error("Could not find root element to mount to"); diff --git a/src/locales/en.json b/src/locales/en.json index da1555b..c99c241 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -575,10 +575,6 @@ "closeAriaLabel": "Close", "share": "Share", "copied": "Copied!", - "shareKakao": "KakaoTalk", - "shareFacebook": "Facebook", - "shareTwitter": "X (Twitter)", - "copyUrl": "Copy URL", "commentsTitle": "Comments", "commentPlaceholder": "Write a comment...", "commentLoginRequired": "Please log in to write a comment", diff --git a/src/locales/ko.json b/src/locales/ko.json index 21b3e35..1f44cd2 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -575,10 +575,6 @@ "closeAriaLabel": "닫기", "share": "공유하기", "copied": "복사됨!", - "shareKakao": "카카오톡", - "shareFacebook": "페이스북", - "shareTwitter": "X (트위터)", - "copyUrl": "URL 복사", "commentsTitle": "댓글", "commentPlaceholder": "댓글을 입력하세요...", "commentLoginRequired": "로그인 후 댓글을 작성할 수 있습니다", @@ -620,7 +616,7 @@ "searching": "검색 중", "placeLoading": "가게를 찾는 중… (몇 초 걸려요)", "hintUrlOk": "네이버 링크로 이 가게를 직접 크롤링합니다.", - "hintDefault": "가게를 선택하면 그 가게의 사진과 정보를 크롤링해 썰박스에 씁니다.", + "hintDefault": "업체의 사진과 정보를 크롤링해 썰박스에 사용합니다.", "hintScenario": "{{name}} 시나리오로 약 5~6분 소요됩니다.", "noResult": "검색 결과가 없어요. 이름을 바꾸거나 네이버 지도 링크를 붙여넣어 주세요.", "cost": "생성 비용", @@ -634,7 +630,7 @@ "step2": "스토리보드 구성 중", "step3": "이미지·목소리 생성 중", "step4": "영상 합성 중", - "takesTime": "약 5~6분 소요됩니다. 다른 작업을 하셔도 계속 진행됩니다.", + "takesTime": "약 5~6분 소요됩니다.", "failTitle": "생성에 실패했어요", "failDefault": "잠시 후 다시 시도해주세요.", "refunded": "차감된 크레딧은 환불되었습니다.", diff --git a/src/pages/Dashboard/ADO2ContentsPage.tsx b/src/pages/Dashboard/ADO2ContentsPage.tsx index 9d9b44d..a013f90 100644 --- a/src/pages/Dashboard/ADO2ContentsPage.tsx +++ b/src/pages/Dashboard/ADO2ContentsPage.tsx @@ -200,7 +200,7 @@ const ADO2ContentsPage: React.FC = () => {

{video.store_name}

-
+

{formatDate(video.created_at)}

{/* 좋아요·댓글 API 가 type 을 받으므로 썰박스에도 노출한다. contentType 이 없으면 id 가 겹치는 다른 영상에 반영된다. */} @@ -209,6 +209,8 @@ const ADO2ContentsPage: React.FC = () => { 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/MyContentsPage.tsx b/src/pages/Dashboard/MyContentsPage.tsx index aab2752..c101266 100644 --- a/src/pages/Dashboard/MyContentsPage.tsx +++ b/src/pages/Dashboard/MyContentsPage.tsx @@ -299,7 +299,7 @@ const MyContentsPage: React.FC = ({ onNavigate, embedded })

{formatTitle(video.store_name, video.created_at)}

-
+

{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 = ({ {commentsTotal}
- {authed && ( + {authed && currentUser && (
- {t('videoDetail.changeAvatarTitle')} - setCommentNickname(e.target.value)} - maxLength={20} - /> + {currentUser.profile_image_url && ( + {currentUser.nickname} + )} + {currentUser.nickname}
)} @@ -458,11 +367,13 @@ const SsulDetailContent: React.FC = ({
    {comments.map((c) => (
  • - avatar + {c.profile_image_url && ( + {c.nickname + )}
    {c.nickname || t('videoDetail.anonymous')} @@ -486,11 +397,13 @@ const SsulDetailContent: React.FC = ({
      {c.replies.map((r) => (
    • - avatar + {r.profile_image_url && ( + {r.nickname + )}
      {r.nickname || t('videoDetail.anonymous')} diff --git a/src/styles/contents-social.css b/src/styles/contents-social.css index 55f6d99..c02e273 100644 --- a/src/styles/contents-social.css +++ b/src/styles/contents-social.css @@ -359,6 +359,7 @@ border-radius: 20px; overflow: hidden; background-color: #034A4D; + min-width: 0; } .content-card-thumbnail { @@ -435,6 +436,16 @@ display: flex; flex-direction: column; gap: 2px; + min-width: 0; +} + +.content-card-meta { + display: flex; + justify-content: space-between; + align-items: center; + gap: 4px 8px; + min-width: 0; + flex-wrap: wrap; } .content-card-title { @@ -458,7 +469,7 @@ color: #9BCACC; white-space: nowrap; flex-shrink: 0; - margin-left: 8px; + margin-left: auto; } .content-card-like-btn { @@ -696,6 +707,26 @@ min-height: 0; overflow-y: auto; padding: 0 32px 24px; + color-scheme: dark; + scrollbar-width: thin; + scrollbar-color: #046266 transparent; +} + +.video-detail-modal-content > .video-detail-body::-webkit-scrollbar { + width: 6px; +} + +.video-detail-modal-content > .video-detail-body::-webkit-scrollbar-track { + background: transparent; +} + +.video-detail-modal-content > .video-detail-body::-webkit-scrollbar-thumb { + background: #046266; + border-radius: 3px; +} + +.video-detail-modal-content > .video-detail-body::-webkit-scrollbar-thumb:hover { + background: #067C80; } .video-detail-page-content { diff --git a/src/types/api.ts b/src/types/api.ts index 63901e3..7cd5ead 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -305,6 +305,8 @@ export interface VideoDetailItem { poster_url?: string | null; store_name: string; region: string; + title?: string | null; + description?: string | null; created_at: string; like_count: number; is_liked_by_me: boolean; diff --git a/src/utils/nativeShare.ts b/src/utils/nativeShare.ts index 1e27e9d..50b8a96 100644 --- a/src/utils/nativeShare.ts +++ b/src/utils/nativeShare.ts @@ -20,20 +20,20 @@ export async function tryNativeShare(data: ShareData): Promise { } } -/** API 서버가 제공하는 영상별 Open Graph 공유 페이지 URL을 만듭니다. */ +/** API 서버의 영상 Open Graph 공유 페이지 URL을 만듭니다. */ export function buildVideoShareUrl(apiBaseUrl: string, videoId: number | string): string { return `${apiBaseUrl.replace(/\/$/, '')}/video/share/${encodeURIComponent(String(videoId))}`; } -/** 콘텐츠 종류별 공개 공유 URL. video.id 와 ssul_content.id 가 겹치므로 경로를 갈라야 한다. */ +/** 콘텐츠 종류별 공유 URL. video.id 와 ssul_content.id 가 겹치므로 경로를 갈라야 한다. */ export function buildContentShareUrl( apiBaseUrl: string, contentId: number | string, contentType: 'video' | 'ssul' = 'video', ): string { + const base = apiBaseUrl.replace(/\/$/, ''); if (contentType === 'ssul') { - const origin = typeof window !== 'undefined' ? window.location.origin : ''; - return `${origin}/ssul/${encodeURIComponent(String(contentId))}`; + return `${base}/ssul/share/${encodeURIComponent(String(contentId))}`; } - return buildVideoShareUrl(apiBaseUrl, contentId); + return buildVideoShareUrl(base, contentId); }