UI 수정

feature-dashboard
김성경 2026-03-09 12:47:28 +09:00
parent 3cbc48181b
commit 8be7b7adcf
4 changed files with 65 additions and 66 deletions

View File

@ -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);

View File

@ -132,6 +132,7 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
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<HTMLDivElement>(null);
const privacyDropdownRef = useRef<HTMLDivElement>(null);
@ -436,6 +437,10 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
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}` });
}}
/>
</div>
@ -453,7 +458,9 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
<p className="social-posting-video-title">
{video.store_name} {new Date(video.created_at).toLocaleString('ko-KR')}
</p>
<p className="social-posting-video-specs">{t('social.videoSpecs')}</p>
{videoSpecs && (
<p className="social-posting-video-specs">{videoSpecs.resolution} · {videoSpecs.duration}</p>
)}
</div>
{/* Channel Selector - Custom Dropdown */}

View File

@ -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<DashboardContentProps> = ({ onNavigate }) => {
const [dashboardData, setDashboardData] = useState<DashboardResponse | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [error, setError] = useState<DashboardError | null>(null);
const [showMockData, setShowMockData] = useState(false);
// 계정 관련 state
@ -576,12 +567,6 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ 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<DashboardContentProps> = ({ 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<DashboardContentProps> = ({ 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<DashboardContentProps> = ({ onNavigate }) => {
<svg className="w-5 h-5 mr-2 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
<div>
<p className="font-semibold"> .</p>
<p className="font-semibold">{error.message}</p>
</div>
</div>
{error.includes('YouTube') && (
{error.code === 'YOUTUBE_TOKEN_EXPIRED' && (
<button
onClick={() => onNavigate?.('내 정보')}
className="ml-4 px-4 py-2 bg-yellow-600 text-white rounded hover:bg-yellow-700 whitespace-nowrap"
>
</button>
)}
{error.code === 'YOUTUBE_NOT_CONNECTED' && (
<button
onClick={() => onNavigate?.('내 정보')}
className="ml-4 px-4 py-2 bg-yellow-600 text-white rounded hover:bg-yellow-700 whitespace-nowrap"

View File

@ -518,32 +518,8 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
</div>
</div>
{/* Generate Button */}
<button
onClick={handleGenerateMusic}
disabled={isGenerating}
className={`btn-generate-sound ${isGenerating ? 'disabled' : ''}`}
>
{isGenerating ? (
<>
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none"/>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/>
</svg>
{t('soundStudio.generating')}
</>
) : (
t('soundStudio.generateButton')
)}
</button>
{errorMessage && (
<div className="error-message-new">
{errorMessage}
</div>
)}
{isGenerating && statusMessage && (
{/* Generate Button / Status Message (교체) */}
{isGenerating && statusMessage ? (
<div className="status-message-new">
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none"/>
@ -551,6 +527,20 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
</svg>
{statusMessage}
</div>
) : (
<button
onClick={handleGenerateMusic}
disabled={isGenerating}
className={`btn-generate-sound ${isGenerating ? 'disabled' : ''}`}
>
{t('soundStudio.generateButton')}
</button>
)}
{errorMessage && (
<div className="error-message-new">
{errorMessage}
</div>
)}
</div>