From a155a98767815b98dbcc7b63f5a295d03b4015c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=84=B1=EA=B2=BD?= Date: Tue, 12 May 2026 15:22:19 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B0=B0=EA=B2=BD=EC=9D=8C=EC=95=85=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.css | 49 +++++++++ public/google60b514c02fd6af4e.html | 1 + ...naver33dfe258205b0af1416aa0ac18c8c0b3.html | 1 + src/components/SocialPostingModal.tsx | 36 ++++++- src/locales/en.json | 4 +- src/locales/ko.json | 4 +- src/pages/Dashboard/ADO2ContentsPage.tsx | 88 +++++++++++++-- src/pages/Dashboard/CompletionContent.tsx | 4 +- .../Dashboard/ContentCalendarContent.tsx | 2 +- src/pages/Dashboard/SoundStudioContent.tsx | 102 ++++++++++-------- src/types/api.ts | 6 +- src/utils/api.ts | 17 ++- 12 files changed, 248 insertions(+), 66 deletions(-) create mode 100644 public/google60b514c02fd6af4e.html create mode 100644 public/naver33dfe258205b0af1416aa0ac18c8c0b3.html diff --git a/index.css b/index.css index 314c217..a9aad74 100644 --- a/index.css +++ b/index.css @@ -8046,6 +8046,51 @@ background: #379599; } +.lyrics-paragraphs { + width: 100%; + overflow-y: auto; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 1.25rem; + scrollbar-color: #046266 transparent; +} + +.lyrics-paragraphs::-webkit-scrollbar { + width: 6px; +} + +.lyrics-paragraphs::-webkit-scrollbar-track { + background: transparent; +} + +.lyrics-paragraphs::-webkit-scrollbar-thumb { + background: #046266; + border-radius: 3px; +} + +.lyrics-section { + display: flex; + flex-direction: column; + gap: 0.4rem; +} + +.lyrics-tag { + font-size: var(--text-xs); + font-weight: 700; + color: #379599; + letter-spacing: 0.05em; +} + +.lyrics-paragraph { + font-size: var(--text-sm); + font-weight: 400; + color: #CEE5E6; + line-height: 1.9; + white-space: pre-line; + margin: 0; +} + .lyrics-placeholder { width: 100%; display: flex; @@ -10572,6 +10617,10 @@ transform: rotate(180deg); } +.social-posting-channel-placeholder { + color: rgba(255, 255, 255, 0.35); +} + .social-posting-channel-menu { position: absolute; top: 100%; diff --git a/public/google60b514c02fd6af4e.html b/public/google60b514c02fd6af4e.html new file mode 100644 index 0000000..968a1e3 --- /dev/null +++ b/public/google60b514c02fd6af4e.html @@ -0,0 +1 @@ +google-site-verification: google60b514c02fd6af4e.html \ No newline at end of file diff --git a/public/naver33dfe258205b0af1416aa0ac18c8c0b3.html b/public/naver33dfe258205b0af1416aa0ac18c8c0b3.html new file mode 100644 index 0000000..e37cfc0 --- /dev/null +++ b/public/naver33dfe258205b0af1416aa0ac18c8c0b3.html @@ -0,0 +1 @@ +naver-site-verification: naver33dfe258205b0af1416aa0ac18c8c0b3.html \ No newline at end of file diff --git a/src/components/SocialPostingModal.tsx b/src/components/SocialPostingModal.tsx index fad79d9..486f377 100644 --- a/src/components/SocialPostingModal.tsx +++ b/src/components/SocialPostingModal.tsx @@ -142,6 +142,10 @@ const SocialPostingModal: React.FC = ({ const channelDropdownRef = useRef(null); const privacyDropdownRef = useRef(null); const hasBeenOpenedRef = useRef(false); + const loadedForTaskIdRef = useRef(null); + const loadedAtRef = useRef(0); + const seoCache = useRef>(new Map()); + const SEO_CACHE_TTL = 50 * 60 * 1000; // Upload progress modal state const [showUploadProgress, setShowUploadProgress] = useState(false); @@ -207,11 +211,28 @@ const SocialPostingModal: React.FC = ({ // 소셜 계정 로드 useEffect(() => { - if (isOpen) { - loadSocialAccounts(); + if (!isOpen) return; + + const now = Date.now(); + + loadSocialAccounts(); + + const taskId = video?.task_id ?? null; + const expired = now - loadedAtRef.current > SEO_CACHE_TTL; + + if (taskId && (taskId !== loadedForTaskIdRef.current || expired)) { + loadedForTaskIdRef.current = taskId; + loadedAtRef.current = now; loadAutocomplete(); + } else if (taskId) { + const cached = seoCache.current.get(taskId); + if (cached) { + setTitle(cached.title); + setDescription(cached.description); + setTags(cached.tags); + } } - }, [isOpen, video]); + }, [isOpen, video?.task_id]); const loadSocialAccounts = async () => { setIsLoadingAccounts(true); @@ -252,6 +273,11 @@ const SocialPostingModal: React.FC = ({ if (autoSeoResponse.title) setTitle(autoSeoResponse.title); if (autoSeoResponse.description) setDescription(autoSeoResponse.description); if (autoSeoResponse.keywords) setTags(autoSeoResponse.keywords.join(',')); + seoCache.current.set(video.task_id, { + title: autoSeoResponse.title || '', + description: autoSeoResponse.description || '', + tags: autoSeoResponse.keywords?.join(',') || '', + }); } catch (error) { console.error('Failed to load autocomplete:', error); // 실패해도 사용자에게 별도 알림 없이 조용히 처리 @@ -530,7 +556,7 @@ const SocialPostingModal: React.FC = ({ onClick={() => setIsChannelDropdownOpen(!isChannelDropdownOpen)} >
- {selectedAccount && ( + {selectedAccount ? ( <> = ({ /> {selectedAccount.display_name} + ) : ( + {t('social.selectChannel')} )}
diff --git a/src/locales/en.json b/src/locales/en.json index 143dfb0..96ab94f 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -208,7 +208,7 @@ "imageAlt": "Image", "uploadBadge": "Uploaded", "imageUpload": "Image Upload", - "dragAndDrop": "Drag and drop\nimages to upload", + "dragAndDrop": "Drag & drop or\nclick to upload", "videoRatio": "Video Ratio", "minImages": "Min. 5 images", "youtubeShorts": "YouTube Shorts", @@ -234,6 +234,7 @@ "lyricsColumn": "Lyrics", "lyricsHint": "Select the lyrics area to edit", "lyricsPlaceholder": "Lyrics will be displayed when sound is generated.", + "lyricsPlaceholderBGM": "Background music is generated without lyrics.", "generateButton": "Generate Sound", "regenerateButton": "Regenerate", "regenerateHint": "Press the regenerate button to create new lyrics and music.", @@ -306,6 +307,7 @@ "resolution": "Resolution", "lyrics": "Lyrics", "sampleLyrics": "Loading lyrics...", + "noLyricsBGM": "Background music is generated without lyrics.", "downloading": "Downloading...", "download": "Download", "uploadToSocial": "Upload to Social" diff --git a/src/locales/ko.json b/src/locales/ko.json index c1888ad..577d754 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -208,7 +208,7 @@ "imageAlt": "이미지", "uploadBadge": "업로드", "imageUpload": "이미지 업로드", - "dragAndDrop": "이미지를 드래그하여\n업로드", + "dragAndDrop": "이미지를 끌어다 놓거나\n클릭하여 업로드", "videoRatio": "영상 비율", "minImages": "최소 5장", "youtubeShorts": "유튜브 쇼츠", @@ -234,6 +234,7 @@ "lyricsColumn": "가사", "lyricsHint": "가사 영역을 선택해서 수정 가능해요", "lyricsPlaceholder": "사운드 생성 시 가사 표시됩니다.", + "lyricsPlaceholderBGM": "배경음악은 가사 없이 생성됩니다.", "generateButton": "사운드 생성", "regenerateButton": "재생성 하기", "regenerateHint": "재생성 버튼을 누르면 가사와 음악을 다시 만들 수 있어요.", @@ -306,6 +307,7 @@ "resolution": "규격", "lyrics": "가사", "sampleLyrics": "가사를 불러오는 중...", + "noLyricsBGM": "배경음악은 가사 없이 생성됩니다.", "downloading": "다운로드 중...", "download": "다운로드", "uploadToSocial": "소셜 채널 업로드" diff --git a/src/pages/Dashboard/ADO2ContentsPage.tsx b/src/pages/Dashboard/ADO2ContentsPage.tsx index d3537d5..06e57cd 100644 --- a/src/pages/Dashboard/ADO2ContentsPage.tsx +++ b/src/pages/Dashboard/ADO2ContentsPage.tsx @@ -1,10 +1,87 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { getVideosList, deleteVideo } from '../../utils/api'; import { VideoListItem } from '../../types/api'; import SocialPostingModal from '../../components/SocialPostingModal'; +const VideoPreviewCard: React.FC<{ src: string; className?: string }> = ({ src, className }) => { + const videoRef = useRef(null); + const isPlayingRef = useRef(false); + const pendingPlayRef = useRef(false); + + useEffect(() => { + const video = videoRef.current; + if (!video) return; + + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + video.preload = 'auto'; + } else { + video.preload = 'none'; + } + }, + { threshold: 0.1 } + ); + + observer.observe(video); + return () => observer.disconnect(); + }, []); + + const safePlay = useCallback((video: HTMLVideoElement) => { + const playPromise = video.play(); + if (playPromise !== undefined) { + playPromise + .then(() => { isPlayingRef.current = true; }) + .catch((error) => { + if (error.name !== 'AbortError') console.error(error); + }); + } + }, []); + + const handleCanPlay = useCallback(() => { + if (!pendingPlayRef.current || !videoRef.current) return; + pendingPlayRef.current = false; + safePlay(videoRef.current); + }, [safePlay]); + + const handleMouseEnter = useCallback(() => { + const video = videoRef.current; + if (!video) return; + if (video.readyState >= 3) { + safePlay(video); + } else { + pendingPlayRef.current = true; + } + }, [safePlay]); + + const handleMouseLeave = useCallback(() => { + const video = videoRef.current; + if (!video) return; + pendingPlayRef.current = false; + if (isPlayingRef.current) { + video.pause(); + video.currentTime = 0; + isPlayingRef.current = false; + } + }, []); + + return ( + + ); +}; + interface ADO2ContentsPageProps { onBack: () => void; onNavigate?: (item: string) => void; @@ -166,16 +243,9 @@ const ADO2ContentsPage: React.FC = ({ onBack, onNavigate {/* Video Thumbnail */}
{video.result_movie_url ? ( -