대시보드 블러 추가
parent
ba4d143189
commit
440a6c8b72
|
|
@ -11,7 +11,5 @@ if (!rootElement) {
|
|||
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import {
|
|||
XAxis, CartesianGrid, Tooltip,
|
||||
} from 'recharts';
|
||||
|
||||
// 환경변수에서 테스트 모드 확인
|
||||
const isTestPage = import.meta.env.VITE_IS_TESTPAGE === 'true';
|
||||
// =====================================================
|
||||
// Types
|
||||
// =====================================================
|
||||
|
|
@ -486,6 +488,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 [showMockData, setShowMockData] = useState(false);
|
||||
|
||||
// 계정 관련 state
|
||||
const [accounts, setAccounts] = useState<ConnectedAccount[]>([]);
|
||||
|
|
@ -612,6 +615,9 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
|
|||
// hasUploads === false: 업로드 영상 없음 → 전체 mock 데이터 표시 + 안내 배너
|
||||
const isEmptyState = dashboardData?.hasUploads === false;
|
||||
|
||||
// 블러 조건: 1)계정 미연결 2)업로드 영상 없음 3)데이터 없음
|
||||
const isBlurred = accounts.length === 0 || isEmptyState || !dashboardData;
|
||||
|
||||
// API 데이터 우선 사용, 없거나 영상 없음(isEmptyState) 시 Mock 데이터로 폴백
|
||||
const contentMetrics = (!isEmptyState && dashboardData?.contentMetrics?.length) ? dashboardData.contentMetrics : MOCK_CONTENT_METRICS;
|
||||
const topContent = (!isEmptyState && dashboardData?.topContent?.length) ? dashboardData.topContent : MOCK_TOP_CONTENT;
|
||||
|
|
@ -649,7 +655,7 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
|
|||
</svg>
|
||||
<div>
|
||||
<p style={{ color: '#a6ffea', fontWeight: 600, marginBottom: '2px' }}>아직 업로드된 영상이 없습니다.</p>
|
||||
<p style={{ color: 'rgba(255,255,255,0.5)', fontSize: '13px' }}>ADO2에서 영상을 업로드하면 실제 통계가 표시됩니다. 현재 샘플 데이터입니다.</p>
|
||||
<p style={{ color: 'rgba(255,255,255,0.5)', fontSize: '13px' }}>ADO2에서 영상을 업로드하면 실제 통계가 표시됩니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -664,7 +670,7 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
|
|||
<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">실제 데이터를 보려면 계정을 연동하세요.</p>
|
||||
</div>
|
||||
</div>
|
||||
{error.includes('YouTube') && (
|
||||
|
|
@ -775,7 +781,10 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
|
|||
{/* Content Performance Section */}
|
||||
<AnimatedSection delay={100} className="dashboard-section">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="dashboard-section-title" style={{ marginBottom: 0 }}>{t('dashboard.contentPerformance')}</h2>
|
||||
<div>
|
||||
<h2 className="dashboard-section-title" style={{ marginBottom: '2px' }}>{t('dashboard.contentPerformance')}</h2>
|
||||
<p style={{ fontSize: '12px', color: 'rgba(255,255,255,0.4)', margin: 0 }}>ADO2에서 업로드한 영상의 통계가 표시됩니다.</p>
|
||||
</div>
|
||||
<div className="mode-toggle">
|
||||
<button
|
||||
className={`mode-btn ${mode === 'month' ? 'active' : ''}`}
|
||||
|
|
@ -791,7 +800,7 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stats-grid-8">
|
||||
<div className="stats-grid-8" style={isBlurred && !showMockData ? { filter: 'blur(4px)', pointerEvents: 'none', userSelect: 'none' } : {}}>
|
||||
{contentMetrics.map((metric: ContentMetric, index: number) => (
|
||||
<AnimatedItem key={metric.id} index={index} baseDelay={200}>
|
||||
<StatCard {...metric} />
|
||||
|
|
@ -801,84 +810,92 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
|
|||
</AnimatedSection>
|
||||
|
||||
{/* Two Column Layout */}
|
||||
<div className="dashboard-two-column">
|
||||
{/* 추이 차트 섹션 (mode=month: 연간, mode=day: 일별) */}
|
||||
<AnimatedSection delay={600}>
|
||||
<div className="yoy-chart-card">
|
||||
<div className="yoy-chart-header">
|
||||
<h2 className="dashboard-section-title">{chartSectionTitle}</h2>
|
||||
<div className="chart-legend-dual">
|
||||
<div className="chart-legend-item">
|
||||
<span className="chart-legend-line solid"></span>
|
||||
<span className="chart-legend-text">{chartCurrentLabel}</span>
|
||||
</div>
|
||||
<div className="chart-legend-item">
|
||||
<span className="chart-legend-line dashed"></span>
|
||||
<span className="chart-legend-text">{chartPreviousLabel}</span>
|
||||
<div className="dashboard-two-column" style={isBlurred && !showMockData ? { filter: 'blur(4px)', pointerEvents: 'none', userSelect: 'none' } : {}}>
|
||||
{/* 추이 차트 섹션 (mode=month: 연간, mode=day: 일별) */}
|
||||
<AnimatedSection delay={600}>
|
||||
<div className="yoy-chart-card">
|
||||
<div className="yoy-chart-header">
|
||||
<h2 className="dashboard-section-title">{chartSectionTitle}</h2>
|
||||
<div className="chart-legend-dual">
|
||||
<div className="chart-legend-item">
|
||||
<span className="chart-legend-line solid"></span>
|
||||
<span className="chart-legend-text">{chartCurrentLabel}</span>
|
||||
</div>
|
||||
<div className="chart-legend-item">
|
||||
<span className="chart-legend-line dashed"></span>
|
||||
<span className="chart-legend-text">{chartPreviousLabel}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="yoy-chart-container">
|
||||
<YearOverYearChart
|
||||
data={chartData}
|
||||
currentLabel={chartCurrentLabel}
|
||||
previousLabel={chartPreviousLabel}
|
||||
mode={mode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="yoy-chart-container">
|
||||
<YearOverYearChart
|
||||
data={chartData}
|
||||
currentLabel={chartCurrentLabel}
|
||||
previousLabel={chartPreviousLabel}
|
||||
mode={mode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AnimatedSection>
|
||||
</AnimatedSection>
|
||||
|
||||
{/* Top Content Section */}
|
||||
<AnimatedSection delay={700}>
|
||||
<div className="top-content-card">
|
||||
<h2 className="dashboard-section-title">{t('dashboard.popularContent')}</h2>
|
||||
<div className="top-content-list">
|
||||
{topContent.map((content, index) => (
|
||||
<AnimatedItem key={content.id} index={index} baseDelay={800}>
|
||||
<TopContentItem {...content} />
|
||||
</AnimatedItem>
|
||||
))}
|
||||
{/* Top Content Section */}
|
||||
<AnimatedSection delay={700}>
|
||||
<div className="top-content-card">
|
||||
<h2 className="dashboard-section-title">{t('dashboard.popularContent')}</h2>
|
||||
<div className="top-content-list">
|
||||
{topContent.map((content, index) => (
|
||||
<AnimatedItem key={content.id} index={index} baseDelay={800}>
|
||||
<TopContentItem {...content} />
|
||||
</AnimatedItem>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AnimatedSection>
|
||||
</div>
|
||||
</AnimatedSection>
|
||||
</div>
|
||||
|
||||
{/* Audience Insights Section */}
|
||||
<AnimatedSection delay={1000} className="audience-section">
|
||||
<h2 className="dashboard-section-title">{t('dashboard.audienceInsights')}</h2>
|
||||
{!isEmptyState && !hasRealAudienceData && (
|
||||
<div className="mb-4 p-3 border-l-4 rounded" style={{ background: 'rgba(255,200,0,0.06)', borderColor: 'rgba(255,200,0,0.5)' }}>
|
||||
<div className="flex items-center gap-2">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="rgba(255,200,0,0.8)" strokeWidth="2" style={{ flexShrink: 0 }}>
|
||||
<circle cx="12" cy="12" r="10" /><line x1="12" y1="8" x2="12" y2="12" /><line x1="12" y1="16" x2="12.01" y2="16" />
|
||||
</svg>
|
||||
<p style={{ color: 'rgba(255,255,255,0.5)', fontSize: '13px' }}>
|
||||
누적 데이터가 부족하여 실제 시청자 데이터가 없습니다. 현재 샘플 데이터로 표시됩니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<h2 className="dashboard-section-title" style={{ marginBottom: '2px' }}>{t('dashboard.audienceInsights')}</h2>
|
||||
<p style={{ fontSize: '12px', color: 'rgba(255,255,255,0.4)', margin: 0 }}>선택한 채널의 통계가 표시됩니다.</p>
|
||||
</div>
|
||||
{audienceData ? (
|
||||
<div className="audience-cards">
|
||||
<AnimatedItem index={0} baseDelay={1100}>
|
||||
<div className="audience-card">
|
||||
<h3 className="audience-card-title">{t('dashboard.ageDistribution')}</h3>
|
||||
<AudienceBarChart data={audienceData.ageGroups} delay={1200} />
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div className="audience-cards" style={!hasRealAudienceData && !showMockData ? { filter: 'blur(4px)', pointerEvents: 'none', userSelect: 'none' } : {}}>
|
||||
<AnimatedItem index={0} baseDelay={1100}>
|
||||
<div className="audience-card">
|
||||
<h3 className="audience-card-title">{t('dashboard.ageDistribution')}</h3>
|
||||
<AudienceBarChart data={audienceData.ageGroups} delay={1200} />
|
||||
</div>
|
||||
</AnimatedItem>
|
||||
<AnimatedItem index={1} baseDelay={1100}>
|
||||
<div className="audience-card">
|
||||
<h3 className="audience-card-title">{t('dashboard.genderDistribution')}</h3>
|
||||
<GenderChart male={audienceData.gender.male} female={audienceData.gender.female} delay={1300} />
|
||||
</div>
|
||||
</AnimatedItem>
|
||||
<AnimatedItem index={2} baseDelay={1100}>
|
||||
<div className="audience-card">
|
||||
<h3 className="audience-card-title">{t('dashboard.topRegions')}</h3>
|
||||
<AudienceBarChart data={audienceData.topRegions.map(r => ({ label: r.region, percentage: r.percentage }))} delay={1400} />
|
||||
</div>
|
||||
</AnimatedItem>
|
||||
</div>
|
||||
{dashboardData && !isEmptyState && !hasRealAudienceData && (
|
||||
<div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', pointerEvents: 'none' }}>
|
||||
<div style={{
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '10px',
|
||||
background: 'rgba(0,0,0,0.45)',
|
||||
backdropFilter: 'blur(2px)',
|
||||
borderRadius: '12px',
|
||||
padding: '20px 28px',
|
||||
}}>
|
||||
<p style={{ color: 'rgba(255,255,255,0.85)', fontSize: '14px', fontWeight: 500, margin: 0, textAlign: 'center' }}>
|
||||
누적 데이터가 부족하여 실제 시청자 데이터가 없습니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</AnimatedItem>
|
||||
<AnimatedItem index={1} baseDelay={1100}>
|
||||
<div className="audience-card">
|
||||
<h3 className="audience-card-title">{t('dashboard.genderDistribution')}</h3>
|
||||
<GenderChart male={audienceData.gender.male} female={audienceData.gender.female} delay={1300} />
|
||||
</div>
|
||||
</AnimatedItem>
|
||||
<AnimatedItem index={2} baseDelay={1100}>
|
||||
<div className="audience-card">
|
||||
<h3 className="audience-card-title">{t('dashboard.topRegions')}</h3>
|
||||
<AudienceBarChart data={audienceData.topRegions.map(r => ({ label: r.region, percentage: r.percentage }))} delay={1400} />
|
||||
</div>
|
||||
</AnimatedItem>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ textAlign: 'center', padding: '32px', opacity: 0.4 }}>
|
||||
|
|
@ -886,6 +903,39 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
|
|||
</div>
|
||||
)}
|
||||
</AnimatedSection>
|
||||
|
||||
{/* 개발자 모드 전용: mock 데이터 블러 해제 버튼 */}
|
||||
{isTestPage && isEmptyState && (
|
||||
<button
|
||||
onClick={() => setShowMockData(prev => !prev)}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
bottom: '24px',
|
||||
right: '24px',
|
||||
zIndex: 9999,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
background: showMockData ? 'rgba(255,100,100,0.15)' : 'rgba(166,255,234,0.12)',
|
||||
border: `1px solid ${showMockData ? 'rgba(255,100,100,0.4)' : 'rgba(166,255,234,0.4)'}`,
|
||||
borderRadius: '10px',
|
||||
color: showMockData ? 'rgba(255,150,150,0.9)' : '#a6ffea',
|
||||
padding: '8px 14px',
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
backdropFilter: 'blur(8px)',
|
||||
}}
|
||||
>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
{showMockData
|
||||
? <><path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94"/><path d="M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19"/><line x1="1" y1="1" x2="23" y2="23"/></>
|
||||
: <><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></>
|
||||
}
|
||||
</svg>
|
||||
{showMockData ? 'Mock 숨기기' : 'Mock 보기'} [DEV]
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue