대시보드 블러 추가

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); const root = ReactDOM.createRoot(rootElement);
root.render( root.render(
<React.StrictMode>
<App /> <App />
</React.StrictMode>
); );

View File

@ -7,6 +7,8 @@ import {
XAxis, CartesianGrid, Tooltip, XAxis, CartesianGrid, Tooltip,
} from 'recharts'; } from 'recharts';
// 환경변수에서 테스트 모드 확인
const isTestPage = import.meta.env.VITE_IS_TESTPAGE === 'true';
// ===================================================== // =====================================================
// Types // Types
// ===================================================== // =====================================================
@ -486,6 +488,7 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
const [dashboardData, setDashboardData] = useState<DashboardResponse | null>(null); const [dashboardData, setDashboardData] = useState<DashboardResponse | null>(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [showMockData, setShowMockData] = useState(false);
// 계정 관련 state // 계정 관련 state
const [accounts, setAccounts] = useState<ConnectedAccount[]>([]); const [accounts, setAccounts] = useState<ConnectedAccount[]>([]);
@ -612,6 +615,9 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
// hasUploads === false: 업로드 영상 없음 → 전체 mock 데이터 표시 + 안내 배너 // hasUploads === false: 업로드 영상 없음 → 전체 mock 데이터 표시 + 안내 배너
const isEmptyState = dashboardData?.hasUploads === false; const isEmptyState = dashboardData?.hasUploads === false;
// 블러 조건: 1)계정 미연결 2)업로드 영상 없음 3)데이터 없음
const isBlurred = accounts.length === 0 || isEmptyState || !dashboardData;
// API 데이터 우선 사용, 없거나 영상 없음(isEmptyState) 시 Mock 데이터로 폴백 // API 데이터 우선 사용, 없거나 영상 없음(isEmptyState) 시 Mock 데이터로 폴백
const contentMetrics = (!isEmptyState && dashboardData?.contentMetrics?.length) ? dashboardData.contentMetrics : MOCK_CONTENT_METRICS; const contentMetrics = (!isEmptyState && dashboardData?.contentMetrics?.length) ? dashboardData.contentMetrics : MOCK_CONTENT_METRICS;
const topContent = (!isEmptyState && dashboardData?.topContent?.length) ? dashboardData.topContent : MOCK_TOP_CONTENT; const topContent = (!isEmptyState && dashboardData?.topContent?.length) ? dashboardData.topContent : MOCK_TOP_CONTENT;
@ -649,7 +655,7 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
</svg> </svg>
<div> <div>
<p style={{ color: '#a6ffea', fontWeight: 600, marginBottom: '2px' }}> .</p> <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> </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" /> <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> </svg>
<div> <div>
<p className="font-semibold"> . .</p> <p className="font-semibold"> .</p>
</div> </div>
</div> </div>
{error.includes('YouTube') && ( {error.includes('YouTube') && (
@ -775,7 +781,10 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
{/* Content Performance Section */} {/* Content Performance Section */}
<AnimatedSection delay={100} className="dashboard-section"> <AnimatedSection delay={100} className="dashboard-section">
<div className="flex items-center justify-between mb-4"> <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"> <div className="mode-toggle">
<button <button
className={`mode-btn ${mode === 'month' ? 'active' : ''}`} className={`mode-btn ${mode === 'month' ? 'active' : ''}`}
@ -791,7 +800,7 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
</button> </button>
</div> </div>
</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) => ( {contentMetrics.map((metric: ContentMetric, index: number) => (
<AnimatedItem key={metric.id} index={index} baseDelay={200}> <AnimatedItem key={metric.id} index={index} baseDelay={200}>
<StatCard {...metric} /> <StatCard {...metric} />
@ -801,7 +810,7 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
</AnimatedSection> </AnimatedSection>
{/* Two Column Layout */} {/* Two Column Layout */}
<div className="dashboard-two-column"> <div className="dashboard-two-column" style={isBlurred && !showMockData ? { filter: 'blur(4px)', pointerEvents: 'none', userSelect: 'none' } : {}}>
{/* 추이 차트 섹션 (mode=month: 연간, mode=day: 일별) */} {/* 추이 차트 섹션 (mode=month: 연간, mode=day: 일별) */}
<AnimatedSection delay={600}> <AnimatedSection delay={600}>
<div className="yoy-chart-card"> <div className="yoy-chart-card">
@ -846,21 +855,13 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
{/* Audience Insights Section */} {/* Audience Insights Section */}
<AnimatedSection delay={1000} className="audience-section"> <AnimatedSection delay={1000} className="audience-section">
<h2 className="dashboard-section-title">{t('dashboard.audienceInsights')}</h2> <div style={{ marginBottom: '16px' }}>
{!isEmptyState && !hasRealAudienceData && ( <h2 className="dashboard-section-title" style={{ marginBottom: '2px' }}>{t('dashboard.audienceInsights')}</h2>
<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)' }}> <p style={{ fontSize: '12px', color: 'rgba(255,255,255,0.4)', margin: 0 }}> .</p>
<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>
)}
{audienceData ? ( {audienceData ? (
<div className="audience-cards"> <div style={{ position: 'relative' }}>
<div className="audience-cards" style={!hasRealAudienceData && !showMockData ? { filter: 'blur(4px)', pointerEvents: 'none', userSelect: 'none' } : {}}>
<AnimatedItem index={0} baseDelay={1100}> <AnimatedItem index={0} baseDelay={1100}>
<div className="audience-card"> <div className="audience-card">
<h3 className="audience-card-title">{t('dashboard.ageDistribution')}</h3> <h3 className="audience-card-title">{t('dashboard.ageDistribution')}</h3>
@ -880,12 +881,61 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
</div> </div>
</AnimatedItem> </AnimatedItem>
</div> </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>
)}
</div>
) : ( ) : (
<div style={{ textAlign: 'center', padding: '32px', opacity: 0.4 }}> <div style={{ textAlign: 'center', padding: '32px', opacity: 0.4 }}>
<span>{t('dashboard.noData') || '이 기간에 데이터가 없습니다.'}</span> <span>{t('dashboard.noData') || '이 기간에 데이터가 없습니다.'}</span>
</div> </div>
)} )}
</AnimatedSection> </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> </div>
); );
}; };