UI수정 및 블러처리 조건 변경

feature-dashboard
김성경 2026-03-03 15:26:26 +09:00
parent 440a6c8b72
commit cab67c711a
3 changed files with 59 additions and 65 deletions

View File

@ -5597,14 +5597,14 @@
} }
.dashboard-description { .dashboard-description {
font-size: 10px; font-size: 15px;
color: var(--color-text-gray-500); color: var(--color-text-gray-500);
margin-top: 0.125rem; margin-top: 0.125rem;
} }
@media (min-width: 640px) { @media (min-width: 640px) {
.dashboard-description { .dashboard-description {
font-size: 10px; font-size: 15px;
} }
} }
@ -5898,7 +5898,6 @@
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: flex-start;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
margin-left: 2.5rem;
} }
@media (min-width: 768px) { @media (min-width: 768px) {
@ -6096,16 +6095,19 @@
/* Mode Toggle */ /* Mode Toggle */
.mode-toggle { .mode-toggle {
display: flex; display: flex;
flex-shrink: 0;
border: 1px solid var(--color-border-gray-700); border: 1px solid var(--color-border-gray-700);
border-radius: var(--radius-full); border-radius: var(--radius-full);
overflow: hidden; overflow: hidden;
} }
.mode-btn { .mode-btn {
min-width: 48px;
padding: 0.375rem 0.875rem; padding: 0.375rem 0.875rem;
background-color: transparent; background-color: transparent;
color: var(--color-text-gray-400); color: var(--color-text-gray-400);
font-size: var(--text-sm); font-size: var(--text-sm);
white-space: nowrap;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
border: none; border: none;
@ -6503,23 +6505,16 @@
/* Stats Grid 8 Columns */ /* Stats Grid 8 Columns */
.stats-grid-8 { .stats-grid-8 {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 0.5rem; gap: 0.5rem;
} }
@media (min-width: 640px) { @media (min-width: 640px) {
.stats-grid-8 { .stats-grid-8 {
grid-template-columns: repeat(4, 1fr);
gap: 0.75rem; gap: 0.75rem;
} }
} }
@media (min-width: 1024px) {
.stats-grid-8 {
grid-template-columns: repeat(8, 1fr);
}
}
/* Platform Metrics Grid 8 Columns */ /* Platform Metrics Grid 8 Columns */
.platform-metrics-grid-8 { .platform-metrics-grid-8 {
display: grid; display: grid;
@ -6758,7 +6753,7 @@
.audience-bar-label { .audience-bar-label {
font-size: var(--text-xs); font-size: var(--text-xs);
color: var(--color-text-gray-400); color: var(--color-text-gray-400);
width: 40px; width: 60px;
flex-shrink: 0; flex-shrink: 0;
} }

View File

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

View File

@ -1,4 +1,3 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { authenticatedFetch, API_URL } from '../../utils/api'; import { authenticatedFetch, API_URL } from '../../utils/api';
@ -9,6 +8,7 @@ import {
// 환경변수에서 테스트 모드 확인 // 환경변수에서 테스트 모드 확인
const isTestPage = import.meta.env.VITE_IS_TESTPAGE === 'true'; const isTestPage = import.meta.env.VITE_IS_TESTPAGE === 'true';
// ===================================================== // =====================================================
// Types // Types
// ===================================================== // =====================================================
@ -132,7 +132,7 @@ const MOCK_TOP_CONTENT: TopContent[] = [
const MOCK_AUDIENCE_DATA: AudienceData = { const MOCK_AUDIENCE_DATA: AudienceData = {
ageGroups: [ ageGroups: [
{ label: '18-24', percentage: 12 }, { label: '13-24', percentage: 12 },
{ label: '25-34', percentage: 35 }, { label: '25-34', percentage: 35 },
{ label: '35-44', percentage: 28 }, { label: '35-44', percentage: 28 },
{ label: '45-54', percentage: 18 }, { label: '45-54', percentage: 18 },
@ -140,11 +140,11 @@ const MOCK_AUDIENCE_DATA: AudienceData = {
], ],
gender: { male: 42, female: 58 }, gender: { male: 42, female: 58 },
topRegions: [ topRegions: [
{ region: '서울', percentage: 32 }, { region: '한국', percentage: 77 },
{ region: '경기', percentage: 24 }, { region: '일본', percentage: 5 },
{ region: '부산', percentage: 12 }, { region: '중국', percentage: 4 },
{ region: '인천', percentage: 8 }, { region: '미국', percentage: 2 },
{ region: '대구', percentage: 6 }, { region: '인도', percentage: 1 },
], ],
}; };
@ -338,7 +338,7 @@ const YearOverYearChart: React.FC<{
tick={{ fill: 'rgba(255,255,255,0.4)', fontSize: 12 }} tick={{ fill: 'rgba(255,255,255,0.4)', fontSize: 12 }}
axisLine={false} axisLine={false}
tickLine={false} tickLine={false}
interval={mode === 'day' ? 1 : 0} interval={mode === 'day' ? 2 : 0}
/> />
<Tooltip content={<CustomTooltip />} /> <Tooltip content={<CustomTooltip />} />
<Area <Area
@ -347,8 +347,8 @@ const YearOverYearChart: React.FC<{
stroke="#a6ffea" stroke="#a6ffea"
strokeWidth={3} strokeWidth={3}
fill="url(#thisYearGradient)" fill="url(#thisYearGradient)"
dot={{ fill: '#a6ffea', r: 4, filter: 'drop-shadow(0 0 6px rgba(166,255,234,0.6))' }} dot={{ fill: '#a6ffea', r: 2, filter: 'drop-shadow(0 0 6px rgba(166,255,234,0.6))' }}
activeDot={{ r: 6, fill: '#a6ffea' }} activeDot={{ r: 4, fill: '#a6ffea' }}
isAnimationActive={true} isAnimationActive={true}
/> />
<Line <Line
@ -621,7 +621,10 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
// 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;
const hasRealAudienceData = !isEmptyState && !!dashboardData?.audienceData?.ageGroups?.length; const hasRealAgeGroups = !isEmptyState && !!dashboardData?.audienceData?.ageGroups?.some(g => g.percentage > 0);
const hasRealGender = !isEmptyState && ((dashboardData?.audienceData?.gender?.male ?? 0) + (dashboardData?.audienceData?.gender?.female ?? 0)) > 0;
const hasRealTopRegions = !isEmptyState && !!dashboardData?.audienceData?.topRegions?.some(r => r.percentage > 0);
const hasRealAudienceData = hasRealAgeGroups && hasRealGender && hasRealTopRegions;
const audienceData = hasRealAudienceData ? dashboardData!.audienceData : MOCK_AUDIENCE_DATA; const audienceData = hasRealAudienceData ? dashboardData!.audienceData : MOCK_AUDIENCE_DATA;
// mode별 차트 데이터를 ChartDataPoint 통합 형식으로 변환 // mode별 차트 데이터를 ChartDataPoint 통합 형식으로 변환
@ -783,7 +786,7 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<div> <div>
<h2 className="dashboard-section-title" style={{ marginBottom: '2px' }}>{t('dashboard.contentPerformance')}</h2> <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> <p style={{ fontSize: '12px', color: 'rgba(255,255,255,0.4)', margin: 0 }}>ADO2 30 .</p>
</div> </div>
<div className="mode-toggle"> <div className="mode-toggle">
<button <button
@ -859,49 +862,43 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
<h2 className="dashboard-section-title" style={{ marginBottom: '2px' }}>{t('dashboard.audienceInsights')}</h2> <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> <p style={{ fontSize: '12px', color: 'rgba(255,255,255,0.4)', margin: 0 }}> .</p>
</div> </div>
{audienceData ? ( <div style={{ position: 'relative' }}>
<div style={{ position: 'relative' }}> <div className="audience-cards" style={!hasRealAudienceData && !showMockData ? { filter: 'blur(4px)', pointerEvents: 'none', userSelect: 'none' } : {}}>
<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> <AudienceBarChart data={audienceData.ageGroups} delay={1200} />
<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> </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>
) : ( {dashboardData && !isEmptyState && !hasRealAudienceData && (
<div style={{ textAlign: 'center', padding: '32px', opacity: 0.4 }}> <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', pointerEvents: 'none' }}>
<span>{t('dashboard.noData') || '이 기간에 데이터가 없습니다.'}</span> <div style={{
</div> 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>
</AnimatedSection> </AnimatedSection>
{/* 개발자 모드 전용: mock 데이터 블러 해제 버튼 */} {/* 개발자 모드 전용: mock 데이터 블러 해제 버튼 */}