UI 수정
parent
3cbc48181b
commit
8be7b7adcf
20
index.css
20
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);
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</div>
|
||||
<p className="font-semibold">{error.message}</p>
|
||||
</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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue