Compare commits

..

No commits in common. "7c3c0b4508fa3a65627938b1adbf91c60e0948b5" and "eb55383de5d7bba043607d9ed770fed0fca24c4a" have entirely different histories.

7 changed files with 87 additions and 135 deletions

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": []
}

View File

@ -860,39 +860,6 @@
background: rgba(255, 255, 255, 0.15);
}
/* Sidebar Footer Actions */
.sidebar-footer-actions {
display: flex;
flex-direction: column;
}
.sidebar-footer-actions .logout-btn {
width: 100%;
}
.sidebar-inquiry-btn {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
color: var(--color-text-gray-400);
text-decoration: none;
transition: color var(--transition-normal);
white-space: nowrap;
font-size: var(--text-sm);
font-weight: 700;
flex-shrink: 0;
}
.sidebar-inquiry-btn:hover {
color: var(--color-text-white);
}
.sidebar-inquiry-btn.collapsed {
justify-content: center;
padding: 0.75rem;
}
/* Logout Button */
.logout-btn {
width: 100%;
@ -2075,9 +2042,31 @@
height: 64px;
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
}
.comp2-inquiry-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 8px;
color: rgba(255, 255, 255, 0.8);
font-size: 14px;
text-decoration: none;
cursor: pointer;
transition: background 0.2s, border-color 0.2s, color 0.2s;
}
.comp2-inquiry-btn:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.6);
color: #fff;
}
.comp2-back-btn {
display: flex;
align-items: center;
@ -10523,27 +10512,3 @@
cursor: not-allowed;
transform: none;
}
/* Calendar panel scrollbar */
.calendar-panel-scroll::-webkit-scrollbar {
width: 4px;
}
.calendar-panel-scroll::-webkit-scrollbar-track {
background: transparent;
}
.calendar-panel-scroll::-webkit-scrollbar-thumb {
background: #067C80;
border-radius: 999px;
}
.calendar-panel-scroll::-webkit-scrollbar-thumb:hover {
background: #088a8e;
}
/* Firefox */
.calendar-panel-scroll {
scrollbar-width: thin;
scrollbar-color: #067C80 transparent;
}

View File

@ -175,30 +175,16 @@ const Sidebar: React.FC<SidebarProps> = ({ activeItem, onNavigate, onHome, userI
)}
</div>
<div className="sidebar-footer-actions">
<button
className={`logout-btn ${isCollapsed ? 'collapsed' : ''}`}
onClick={handleLogout}
disabled={isLoggingOut}
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/>
</svg>
{!isCollapsed && <span className="logout-btn-label">{isLoggingOut ? t('sidebar.loggingOut') : t('sidebar.logout')}</span>}
</button>
<a
href="https://forms.gle/4a8mGebBYtdesvby9"
target="_blank"
rel="noopener noreferrer"
className={`sidebar-inquiry-btn ${isCollapsed ? 'collapsed' : ''}`}
title="고객의견"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
</svg>
{!isCollapsed && <span></span>}
</a>
</div>
<button
className={`logout-btn ${isCollapsed ? 'collapsed' : ''}`}
onClick={handleLogout}
disabled={isLoggingOut}
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/>
</svg>
{!isCollapsed && <span className="logout-btn-label">{isLoggingOut ? t('sidebar.loggingOut') : t('sidebar.logout')}</span>}
</button>
</div>
</div>
</>

View File

@ -331,6 +331,17 @@ const CompletionContent: React.FC<CompletionContentProps> = ({
</svg>
<span>{t('completion.back')}</span>
</button>
<a
href="https://forms.gle/4a8mGebBYtdesvby9"
target="_blank"
rel="noopener noreferrer"
className="comp2-inquiry-btn"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
</svg>
<span></span>
</a>
</div>
<div className="comp2-title-row">

View File

@ -643,8 +643,7 @@ const ContentCalendarContent: React.FC<ContentCalendarContentProps> = ({ onNavig
return (
<div
ref={panelRef}
className="calendar-panel-scroll"
style={{ flex: 1, overflowY: 'auto', padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 20, maxHeight: 900 }}
style={{ flex: 1, overflowY: 'auto', padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 20 }}
>
{sortedDateKeys.map(dateKey => (
<div

View File

@ -34,6 +34,21 @@ 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;
@ -50,12 +65,6 @@ interface AudienceData {
topRegions: { region: string; percentage: number }[];
}
interface DashboardError {
code: string;
message: string;
reconnect_url?: string;
}
interface DashboardResponse {
contentMetrics: ContentMetric[];
monthlyData: MonthlyData[];
@ -63,7 +72,7 @@ interface DashboardResponse {
topContent: TopContent[];
audienceData: AudienceData;
hasUploads: boolean; // 업로드 영상 존재 여부 (false 시 mock 데이터 + 안내 메시지 표시)
error: DashboardError | null;
// platformData: PlatformData[]; // 미사용
}
interface ConnectedAccount {
@ -478,9 +487,8 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
const [dashboardData, setDashboardData] = useState<DashboardResponse | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<DashboardError | null>(null);
const [error, setError] = useState<string | null>(null);
const [showMockData, setShowMockData] = useState(false);
const [retryTrigger, setRetryTrigger] = useState(0);
// 계정 관련 state
const [accounts, setAccounts] = useState<ConnectedAccount[]>([]);
@ -568,32 +576,25 @@ 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;
}
if (errorData.code === 'YOUTUBE_API_FAILED') {
setError({ code: 'YOUTUBE_API_FAILED', message: 'YouTube Analytics API 호출에 실패했습니다.' });
setDashboardData(null);
return;
}
setError({ code: errorData.code || 'API_ERROR', message: errorData.detail || `API Error: ${response.status}`, reconnect_url: errorData.reconnect_url });
return;
throw new Error(errorData.detail || `API Error: ${response.status}`);
}
const data: DashboardResponse = await response.json();
if (data.error) {
setError(data.error);
setDashboardData(data);
return;
}
setDashboardData(data);
setError(null);
} catch (err) {
console.error('Dashboard API Error:', err);
setError({ code: 'UNKNOWN', message: err instanceof Error ? err.message : 'Unknown error' });
setError(err instanceof Error ? err.message : 'Unknown error');
setDashboardData(null);
} finally {
setIsLoading(false);
@ -601,7 +602,7 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
};
fetchDashboardData();
}, [mode, selectedAccountId, accountsLoaded, retryTrigger]);
}, [mode, selectedAccountId, accountsLoaded]);
if (isLoading) {
return (
@ -611,11 +612,11 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
);
}
// hasUploads === false이고 error 없음: 업로드 영상 없음 → 전체 mock 데이터 표시 + 안내 배너
const isEmptyState = dashboardData?.hasUploads === false && !dashboardData?.error;
// hasUploads === false: 업로드 영상 없음 → 전체 mock 데이터 표시 + 안내 배너
const isEmptyState = dashboardData?.hasUploads === false;
// 블러 조건: 1)계정 미연결 2)업로드 영상 없음 3)데이터 없음 4)에러 있음
const isBlurred = accounts.length === 0 || isEmptyState || !dashboardData || !!error;
// 블러 조건: 1)계정 미연결 2)업로드 영상 없음 3)데이터 없음
const isBlurred = accounts.length === 0 || isEmptyState || !dashboardData;
// showMockData=true면 전체 mock 강제, 아니면 API 우선 / isEmptyState 시 mock 폴백
const useReal = !showMockData && !isEmptyState;
@ -672,17 +673,11 @@ 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>
<p className="font-semibold">{error.message}</p>
<div>
<p className="font-semibold"> .</p>
</div>
</div>
{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' && (
{error.includes('YouTube') && (
<button
onClick={() => onNavigate?.('내 정보')}
className="ml-4 px-4 py-2 bg-yellow-600 text-white rounded hover:bg-yellow-700 whitespace-nowrap"
@ -690,14 +685,6 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
</button>
)}
{(error.code === 'YOUTUBE_API_FAILED' || error.code === 'DASHBOARD_DATA_ERROR') && (
<button
onClick={() => { setError(null); setRetryTrigger((n: number) => n + 1); }}
className="ml-4 px-4 py-2 bg-yellow-600 text-white rounded hover:bg-yellow-700 whitespace-nowrap"
>
</button>
)}
</div>
</div>
)}

View File

@ -266,12 +266,13 @@ const UrlInputContent: React.FC<UrlInputContentProps> = ({ onAnalyze, onAutocomp
</div>
)}
</div>
{/* 검색 버튼 */}
<button type="submit" className="url-input-button">
{t('landing.hero.analyzeButton')}
</button>
</div>
{/* 검색 버튼 */}
<button type="submit" className="url-input-button">
{t('urlInput.searchButton')}
</button>
{/* 에러 메시지 */}
{error && (
<p className="url-input-error">{error}</p>