대시보드 블러 추가

feature-dashboard
김성경 2026-02-27 14:03:01 +09:00
parent ba4d143189
commit 440a6c8b72
2 changed files with 122 additions and 74 deletions

View File

@ -11,7 +11,5 @@ if (!rootElement) {
const root = ReactDOM.createRoot(rootElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

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