From 8be7b7adcf690b07814cd76ae38a37646042eb3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=84=B1=EA=B2=BD?= Date: Mon, 9 Mar 2026 12:47:28 +0900 Subject: [PATCH] =?UTF-8?q?UI=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.css | 20 +++++--- src/components/SocialPostingModal.tsx | 9 +++- src/pages/Dashboard/DashboardContent.tsx | 60 +++++++++++----------- src/pages/Dashboard/SoundStudioContent.tsx | 42 ++++++--------- 4 files changed, 65 insertions(+), 66 deletions(-) diff --git a/index.css b/index.css index dbea805..742040d 100644 --- a/index.css +++ b/index.css @@ -2049,19 +2049,23 @@ display: flex; align-items: center; gap: 4px; - padding: 8px 20px 8px 8px; - background: #462E64; + background-color: #462E64; + color: #CFABFB; + font-size: 0.875rem; + font-weight: 600; + padding: 0 1.25rem 0 0.5rem; + height: 36px; border: 1px solid #694596; border-radius: 999px; - color: #CFABFB; - font-size: 14px; - font-weight: 600; cursor: pointer; - transition: opacity 0.2s; + white-space: nowrap; + transition: background-color 0.2s; + line-height: 1.19; + letter-spacing: -0.006em; } .comp2-back-btn:hover { - opacity: 0.85; + background-color: #5a3a80; } .comp2-container { @@ -7857,7 +7861,7 @@ .lyrics-textarea { width: 100%; - height: 200px; + min-height: 200px; background: transparent; border: none; color: var(--color-text-white); diff --git a/src/components/SocialPostingModal.tsx b/src/components/SocialPostingModal.tsx index a0f56c1..6a8762c 100644 --- a/src/components/SocialPostingModal.tsx +++ b/src/components/SocialPostingModal.tsx @@ -132,6 +132,7 @@ const SocialPostingModal: React.FC = ({ const [isPrivacyDropdownOpen, setIsPrivacyDropdownOpen] = useState(false); const [isLoadingAutoDescription, setIsLoadingAutoDescription] = useState(false); const [isHorizontalVideo, setIsHorizontalVideo] = useState(false); + const [videoSpecs, setVideoSpecs] = useState<{ resolution: string; duration: string } | null>(null); const channelDropdownRef = useRef(null); const privacyDropdownRef = useRef(null); @@ -436,6 +437,10 @@ const SocialPostingModal: React.FC = ({ onLoadedMetadata={(e) => { const v = e.currentTarget; setIsHorizontalVideo(v.videoWidth > v.videoHeight); + const totalSec = Math.floor(v.duration); + const m = Math.floor(totalSec / 60).toString().padStart(2, '0'); + const s = (totalSec % 60).toString().padStart(2, '0'); + setVideoSpecs({ resolution: `${v.videoWidth}×${v.videoHeight}`, duration: `${m}:${s}` }); }} /> @@ -453,7 +458,9 @@ const SocialPostingModal: React.FC = ({

{video.store_name} {new Date(video.created_at).toLocaleString('ko-KR')}

-

{t('social.videoSpecs')}

+ {videoSpecs && ( +

{videoSpecs.resolution} · {videoSpecs.duration}

+ )} {/* Channel Selector - Custom Dropdown */} diff --git a/src/pages/Dashboard/DashboardContent.tsx b/src/pages/Dashboard/DashboardContent.tsx index 027e118..b4e551e 100755 --- a/src/pages/Dashboard/DashboardContent.tsx +++ b/src/pages/Dashboard/DashboardContent.tsx @@ -34,21 +34,6 @@ interface DailyData { lastPeriod: number; } -// interface PlatformMetric { // 미사용 — platform_data 기능 예정 -// id: string; -// label: string; -// value: string; -// unit?: string; -// trend: number; -// trendDirection: 'up' | 'down'; -// } - -// interface PlatformData { // 미사용 — platform_data 기능 예정 -// platform: 'youtube' | 'instagram'; -// displayName: string; -// metrics: PlatformMetric[]; -// } - interface TopContent { id: string; title: string; @@ -65,6 +50,12 @@ interface AudienceData { topRegions: { region: string; percentage: number }[]; } +interface DashboardError { + code: string; + message: string; + reconnect_url?: string; +} + interface DashboardResponse { contentMetrics: ContentMetric[]; monthlyData: MonthlyData[]; @@ -72,7 +63,7 @@ interface DashboardResponse { topContent: TopContent[]; audienceData: AudienceData; hasUploads: boolean; // 업로드 영상 존재 여부 (false 시 mock 데이터 + 안내 메시지 표시) - // platformData: PlatformData[]; // 미사용 + error: DashboardError | null; } interface ConnectedAccount { @@ -487,7 +478,7 @@ const DashboardContent: React.FC = ({ onNavigate }) => { const [dashboardData, setDashboardData] = useState(null); const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); + const [error, setError] = useState(null); const [showMockData, setShowMockData] = useState(false); // 계정 관련 state @@ -576,12 +567,6 @@ const DashboardContent: React.FC = ({ onNavigate }) => { if (!response.ok) { const errorData = await response.json(); - - if (errorData.code === 'YOUTUBE_NOT_CONNECTED') { - setError('YouTube 계정을 연동하여 데이터를 확인하세요.'); - setDashboardData(null); - return; - } if (errorData.code === 'YOUTUBE_ACCOUNT_SELECTION_REQUIRED') { setDashboardData(null); return; @@ -590,6 +575,13 @@ const DashboardContent: React.FC = ({ onNavigate }) => { } const data: DashboardResponse = await response.json(); + + if (data.error) { + setError(data.error); + setDashboardData(data); + return; + } + setDashboardData(data); setError(null); } catch (err) { @@ -612,11 +604,11 @@ const DashboardContent: React.FC = ({ onNavigate }) => { ); } - // hasUploads === false: 업로드 영상 없음 → 전체 mock 데이터 표시 + 안내 배너 - const isEmptyState = dashboardData?.hasUploads === false; + // hasUploads === false이고 error 없음: 업로드 영상 없음 → 전체 mock 데이터 표시 + 안내 배너 + const isEmptyState = dashboardData?.hasUploads === false && !dashboardData?.error; - // 블러 조건: 1)계정 미연결 2)업로드 영상 없음 3)데이터 없음 - const isBlurred = accounts.length === 0 || isEmptyState || !dashboardData; + // 블러 조건: 1)계정 미연결 2)업로드 영상 없음 3)데이터 없음 4)에러 있음 + const isBlurred = accounts.length === 0 || isEmptyState || !dashboardData || !!error; // API 데이터 우선 사용, 없거나 영상 없음(isEmptyState) 시 Mock 데이터로 폴백 const contentMetrics = (!isEmptyState && dashboardData?.contentMetrics?.length) ? dashboardData.contentMetrics : MOCK_CONTENT_METRICS; @@ -672,11 +664,17 @@ const DashboardContent: React.FC = ({ onNavigate }) => { -
-

실제 데이터를 보려면 계정을 연동하세요.

-
+

{error.message}

- {error.includes('YouTube') && ( + {error.code === 'YOUTUBE_TOKEN_EXPIRED' && ( + + )} + {error.code === 'YOUTUBE_NOT_CONNECTED' && ( - - {errorMessage && ( -
- {errorMessage} -
- )} - - {isGenerating && statusMessage && ( + {/* Generate Button / Status Message (교체) */} + {isGenerating && statusMessage ? (
@@ -551,6 +527,20 @@ const SoundStudioContent: React.FC = ({ {statusMessage}
+ ) : ( + + )} + + {errorMessage && ( +
+ {errorMessage} +
)}