UI수정 및 블러처리 조건 변경
parent
440a6c8b72
commit
cab67c711a
19
index.css
19
index.css
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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 데이터 블러 해제 버튼 */}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue