From c386467b6ca737b610429ced9861f6f650e2bee5 Mon Sep 17 00:00:00 2001 From: YermIm Date: Sun, 3 May 2026 05:18:27 +0900 Subject: [PATCH 1/8] =?UTF-8?q?refactor:=20=ED=94=8C=EB=9E=AB=ED=8F=BC?= =?UTF-8?q?=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/platform/AllPlatformView.tsx | 122 +++++++++++++++++ .../dashboard/platform/SinglePlatformView.tsx | 0 .../dashboard/platform/PlatformDashboard.tsx | 127 +----------------- 3 files changed, 129 insertions(+), 120 deletions(-) create mode 100644 src/components/dashboard/platform/AllPlatformView.tsx create mode 100644 src/components/dashboard/platform/SinglePlatformView.tsx diff --git a/src/components/dashboard/platform/AllPlatformView.tsx b/src/components/dashboard/platform/AllPlatformView.tsx new file mode 100644 index 00000000..5bb66c88 --- /dev/null +++ b/src/components/dashboard/platform/AllPlatformView.tsx @@ -0,0 +1,122 @@ +import Badge from "@/components/common/badge/Badge"; +import Card from "@/components/common/card/Card"; +import ChartLegend from "@/components/common/chart/ChartLegend"; +import AdStatusChart from "@/components/dashboard/charts/AdStatusChart"; +import PerformanceEfficiencyChart from "@/components/dashboard/charts/PerformanceEfficiencyChart"; +import PlatformDetailCard from "@/components/dashboard/platform/PlatformDetailCard"; +import { + AdStatusChartSkeleton, + BadgeSkeleton, + PerformanceEfficiencyChartSkeleton, + PlatformDetailCardSkeleton, + TopPerformanceListSkeleton, + TrafficChartSkeleton, +} from "@/components/dashboard/platform/skeleton/PlatformSkeleton"; +import TopPerformanceList from "@/components/dashboard/platform/TopPerformanceList"; + +import { + adStatusMock, + performanceEfficiencyMock, + roasRankingMock, +} from "@/pages/dashboard/platform/platformDashboard.mock"; + +interface IAllPlatformViewProps { + isLoading: boolean; +} + +export default function AllPlatformView({ isLoading }: IAllPlatformViewProps) { + return ( +
+
+ {/* 성과 우수 플랫폼 */} + + ) : ( + + ROAS 기준 상위 3 + + ) + } + className="flex-1 min-h-67 flex flex-col" + > + {isLoading ? ( + + ) : ( + + )} + + + {/* 광고 소재 현황 */} + + } + RightElement={ + isLoading ? ( + + ) : ( + + 총 {adStatusMock.totalCount}개 + + ) + } + className="flex-1 min-h-67 flex flex-col" + > + {isLoading ? ( + + ) : ( + + )} + + + {/* 플랫폼별 성과 효율 비교 */} + + } + > + {isLoading ? ( + + ) : ( + + )} + +
+ + {/* 실시간 트래픽 변화 */} + + {isLoading ? : null} + + + {/* 개별 플랫폼 상세 */} +
+ {isLoading + ? Array.from({ length: 3 }).map((_, i) => ( + + )) + : performanceEfficiencyMock.map((platform) => ( + + ))} +
+
+ ); +} diff --git a/src/components/dashboard/platform/SinglePlatformView.tsx b/src/components/dashboard/platform/SinglePlatformView.tsx new file mode 100644 index 00000000..e69de29b diff --git a/src/pages/dashboard/platform/PlatformDashboard.tsx b/src/pages/dashboard/platform/PlatformDashboard.tsx index 1bb4e86c..69501dbd 100644 --- a/src/pages/dashboard/platform/PlatformDashboard.tsx +++ b/src/pages/dashboard/platform/PlatformDashboard.tsx @@ -1,30 +1,10 @@ import { useEffect, useState } from "react"; import { twMerge } from "tailwind-merge"; -import Badge from "@/components/common/badge/Badge"; import Button from "@/components/common/button/Button"; -import Card from "@/components/common/card/Card"; -import ChartLegend from "@/components/common/chart/ChartLegend"; import { DropdownMenu } from "@/components/common/dropdownmenu/DropdownMenu"; import PageHeader from "@/components/common/PageHeader"; -import AdStatusChart from "@/components/dashboard/charts/AdStatusChart"; -import PerformanceEfficiencyChart from "@/components/dashboard/charts/PerformanceEfficiencyChart"; -import PlatformDetailCard from "@/components/dashboard/platform/PlatformDetailCard"; -import { - AdStatusChartSkeleton, - BadgeSkeleton, - PerformanceEfficiencyChartSkeleton, - PlatformDetailCardSkeleton, - TopPerformanceListSkeleton, - TrafficChartSkeleton, -} from "@/components/dashboard/platform/skeleton/PlatformSkeleton"; -import TopPerformanceList from "@/components/dashboard/platform/TopPerformanceList"; - -import { - adStatusMock, - performanceEfficiencyMock, - roasRankingMock, -} from "./platformDashboard.mock"; +import AllPlatformView from "@/components/dashboard/platform/AllPlatformView"; import ChevronDownIcon from "@/assets/icon/chevron/chevron-up.svg?react"; @@ -100,106 +80,13 @@ export default function PlatformDashboard() { } /> -
-
- {/* 성과 우수 플랫폼 */} - - ) : ( - - ROAS 기준 상위 3 - - ) - } - className="flex-1 min-h-67 flex flex-col" - > - {isLoading ? ( - - ) : ( - - )} - - - {/* 광고 소재 현황 */} - - } - RightElement={ - isLoading ? ( - - ) : ( - - 총 {adStatusMock.totalCount}개 - - ) - } - className="flex-1 min-h-67 flex flex-col" - > - {isLoading ? ( - - ) : ( - - )} - - - {/* 플랫폼별 성과 효율 비교 */} - - } - > - {isLoading ? ( - - ) : ( - - )} - -
- - {/* 실시간 트래픽 변화 */} - - {isLoading ? : null} - - - {/* 개별 플랫폼 상세 */} -
- {isLoading - ? Array.from({ length: 3 }).map((_, i) => ( - - )) - : performanceEfficiencyMock.map((platform) => ( - - ))} + {isAllView ? ( + + ) : ( +
+

{selectedPlatform}

-
+ )} ); } From 2e5a57f14b0b3bf131167e3be35f22b017fd4284 Mon Sep 17 00:00:00 2001 From: YermIm Date: Mon, 4 May 2026 23:03:30 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20=EB=A0=88=EC=9D=B4=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EB=B0=8F=20=EC=83=81=EB=8B=A8=20=EC=A7=80=ED=91=9C?= =?UTF-8?q?=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../social-logo/wordmark/meta-wordmark.svg | 6 + .../dashboard/platform/SinglePlatformView.tsx | 154 ++++++++++++++++++ .../dashboard/platform/PlatformDashboard.tsx | 5 +- 3 files changed, 162 insertions(+), 3 deletions(-) create mode 100644 src/assets/logo/social-logo/wordmark/meta-wordmark.svg diff --git a/src/assets/logo/social-logo/wordmark/meta-wordmark.svg b/src/assets/logo/social-logo/wordmark/meta-wordmark.svg new file mode 100644 index 00000000..cae85183 --- /dev/null +++ b/src/assets/logo/social-logo/wordmark/meta-wordmark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/dashboard/platform/SinglePlatformView.tsx b/src/components/dashboard/platform/SinglePlatformView.tsx index e69de29b..abfd77db 100644 --- a/src/components/dashboard/platform/SinglePlatformView.tsx +++ b/src/components/dashboard/platform/SinglePlatformView.tsx @@ -0,0 +1,154 @@ +import React, { useMemo } from "react"; +import { twMerge } from "tailwind-merge"; + +import Card from "@/components/common/card/Card"; +import StatCard from "@/components/common/card/StatCard"; +import { Skeleton } from "@/components/common/skeleton/Skeleton"; + +import AiButtonSvg from "@/assets/logo/service-logo/ai-요약버튼.svg?react"; +import GoogleLogo from "@/assets/logo/social-logo/wordmark/google-wordmark.svg?react"; +import MetaLogo from "@/assets/logo/social-logo/wordmark/meta-wordmark.svg?react"; +import NaverLogo from "@/assets/logo/social-logo/wordmark/naver-wordmark.svg?react"; +import { performanceEfficiencyMock } from "@/pages/dashboard/platform/platformDashboard.mock"; + +const PLATFORM_LOGOS: Record< + string, + { component: React.FC>; className: string } +> = { + GOOGLE: { component: GoogleLogo, className: "h-10" }, + NAVER: { component: NaverLogo, className: "h-6 ml-2" }, + META: { component: MetaLogo, className: "h-6 ml-2" }, +}; + +interface ISinglePlatformViewProps { + platform: string; + isLoading: boolean; +} + +export default function SinglePlatformView({ + platform, + isLoading, +}: ISinglePlatformViewProps) { + const platformData = useMemo(() => { + return performanceEfficiencyMock.find( + (p) => p.provider.toUpperCase() === platform.toUpperCase(), + ); + }, [platform]); + + const kpis = useMemo(() => { + if (!platformData) return []; + + return [ + { + title: "클릭수(CTR)", + value: platformData.clicks.toLocaleString(), + trend: { + direction: platformData.clickChangeRate >= 0 ? "up" : "down", + value: `${Math.abs(platformData.clickChangeRate * 100).toFixed(2)}%`, + }, + }, + { + title: "노출수", + value: platformData.impressions.toLocaleString(), + trend: { + direction: platformData.impressionChangeRate >= 0 ? "up" : "down", + value: `${Math.abs(platformData.impressionChangeRate * 100).toFixed(2)}%`, + }, + }, + { + title: "전환율(CVR)", + value: `${platformData.conversion}%`, + trend: { + direction: platformData.cvrChangeRate >= 0 ? "up" : "down", + value: `${Math.abs(platformData.cvrChangeRate * 100).toFixed(2)}%`, + }, + }, + { + title: "광고비 대비 매출(ROAS)", + value: `${platformData.ROAS}%`, + trend: { + direction: platformData.ROASChangeRate >= 0 ? "up" : "down", + value: `${Math.abs(platformData.ROASChangeRate * 100).toFixed(2)}%`, + }, + }, + ]; + }, [platformData]); + + const logoInfo = PLATFORM_LOGOS[platform.toUpperCase()]; + + return ( +
+ {/* platform header */} +
+
+ {logoInfo ? ( + + ) : ( +

{platform}

+ )} +
+ + +
+ + {/* top */} +
+ {isLoading + ? Array.from({ length: 4 }).map((_, i) => ( +
+ + + +
+ )) + : kpis.map((kpi) => ( + + ))} +
+ + {/* mid */} +
+ +
라인 차트
+
+ + +
게이지 차트
+
+
+ + {/* bottom */} + + + +
+ } + > +
테이블
+ +
+ ); +} diff --git a/src/pages/dashboard/platform/PlatformDashboard.tsx b/src/pages/dashboard/platform/PlatformDashboard.tsx index 69501dbd..5d4c481b 100644 --- a/src/pages/dashboard/platform/PlatformDashboard.tsx +++ b/src/pages/dashboard/platform/PlatformDashboard.tsx @@ -5,6 +5,7 @@ import Button from "@/components/common/button/Button"; import { DropdownMenu } from "@/components/common/dropdownmenu/DropdownMenu"; import PageHeader from "@/components/common/PageHeader"; import AllPlatformView from "@/components/dashboard/platform/AllPlatformView"; +import SinglePlatformView from "@/components/dashboard/platform/SinglePlatformView"; import ChevronDownIcon from "@/assets/icon/chevron/chevron-up.svg?react"; @@ -83,9 +84,7 @@ export default function PlatformDashboard() { {isAllView ? ( ) : ( -
-

{selectedPlatform}

-
+ )} ); From 881c5575dc83740088561ed3a91fba6ab4a5d25a Mon Sep 17 00:00:00 2001 From: YermIm Date: Tue, 5 May 2026 10:35:18 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20=EC=98=88=EC=82=B0=20=EC=86=8C?= =?UTF-8?q?=EC=A7=84=20=ED=98=84=ED=99=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../social-logo/wordmark/naver-wordmark.svg | 9 ++ .../dashboard/platform/SinglePlatformView.tsx | 88 ++++++++++++++++--- .../platform/platformDashboard.mock.ts | 26 ++++++ src/types/dashboard/platform.ts | 9 ++ 4 files changed, 121 insertions(+), 11 deletions(-) create mode 100644 src/assets/logo/social-logo/wordmark/naver-wordmark.svg diff --git a/src/assets/logo/social-logo/wordmark/naver-wordmark.svg b/src/assets/logo/social-logo/wordmark/naver-wordmark.svg new file mode 100644 index 00000000..a97c4b27 --- /dev/null +++ b/src/assets/logo/social-logo/wordmark/naver-wordmark.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/components/dashboard/platform/SinglePlatformView.tsx b/src/components/dashboard/platform/SinglePlatformView.tsx index 75a3f691..a049bac8 100644 --- a/src/components/dashboard/platform/SinglePlatformView.tsx +++ b/src/components/dashboard/platform/SinglePlatformView.tsx @@ -1,15 +1,24 @@ import React, { useMemo } from "react"; import { twMerge } from "tailwind-merge"; +import Badge from "@/components/common/badge/Badge"; import Card from "@/components/common/card/Card"; import StatCard from "@/components/common/card/StatCard"; +import ChartLegend from "@/components/common/chart/ChartLegend"; import { Skeleton } from "@/components/common/skeleton/Skeleton"; +import BudgetGaugeChart, { + getBudgetStatus, + statusBadgeVariant, +} from "@/components/dashboard/charts/BudgetGaugeChart"; import AiButtonSvg from "@/assets/logo/service-logo/ai-요약버튼.svg?react"; import GoogleLogo from "@/assets/logo/social-logo/wordmark/google-wordmark.svg?react"; import MetaLogo from "@/assets/logo/social-logo/wordmark/meta-wordmark.svg?react"; -import NaverLogo from "@/assets/logo/social-logo/wordmark/naver-wordmark.png"; -import { performanceEfficiencyMock } from "@/pages/dashboard/platform/platformDashboard.mock"; +import NaverLogo from "@/assets/logo/social-logo/wordmark/naver-wordmark.svg?react"; +import { + budgetStatusMock, + performanceEfficiencyMock, +} from "@/pages/dashboard/platform/platformDashboard.mock"; const PLATFORM_LOGOS: Record< string, @@ -76,6 +85,32 @@ export default function SinglePlatformView({ const logoInfo = PLATFORM_LOGOS[platform.toUpperCase()]; + const budget = useMemo(() => { + const data = budgetStatusMock.find( + (b) => b.providerType.toUpperCase() === platform.toUpperCase(), + ); + if (!data) return null; + + return { + totalBudget: data.totalBudget, + spent: data.totalSpend, + warningThreshold: 50, + dangerThreshold: 75, + }; + }, [platform]); + + const budgetPct = budget + ? Math.round((budget.spent / budget.totalBudget) * 100) + : 0; + + const budgetStatus = budget + ? getBudgetStatus( + budgetPct, + budget.warningThreshold, + budget.dangerThreshold, + ) + : null; + return (
{/* platform header */} @@ -90,11 +125,8 @@ export default function SinglePlatformView({ )}
- @@ -123,19 +155,53 @@ export default function SinglePlatformView({ {/* mid */}
- +
라인 차트
- -
게이지 차트
+ + } + RightElement={ + budgetStatus && ( + + {budgetStatus} + + ) + } + > + {budget ? ( +
+ +
+ ) : ( +
+ 데이터가 없습니다. +
+ )}
{/* bottom */} - } > -
테이블
+
); diff --git a/src/pages/dashboard/platform/platformDashboard.mock.ts b/src/pages/dashboard/platform/platformDashboard.mock.ts index 6df1fe68..02aa40d3 100644 --- a/src/pages/dashboard/platform/platformDashboard.mock.ts +++ b/src/pages/dashboard/platform/platformDashboard.mock.ts @@ -104,3 +104,74 @@ export const budgetStatusMock: IBudgetStatus[] = [ remainingBudget: 800000, }, ]; + +export interface IPlatformDailyPerformance { + date: string; + spend: number; + impressions: number; + clicks: number; + ctr: number; + cpc: number; + conversions: number; + roas: number; +} + +// 최근 30일 데이터 생성 함수 (5/5) +const generateRecentData = ( + baseSpend: number, + baseImp: number, + baseClicks: number, + baseConv: number, +) => { + const data: IPlatformDailyPerformance[] = []; + const days = ["일", "월", "화", "수", "목", "금", "토"]; + + const today = new Date(2026, 4, 5); // 5월 index 4 + + for (let i = 0; i < 30; i++) { + const targetDate = new Date(today); + targetDate.setDate(today.getDate() - i); + + const year = targetDate.getFullYear().toString().slice(-2); + const month = (targetDate.getMonth() + 1).toString().padStart(2, "0"); + const date = targetDate.getDate().toString().padStart(2, "0"); + const dayName = days[targetDate.getDay()]; + + const isWeekend = targetDate.getDay() === 0 || targetDate.getDay() === 6; + const multiplier = isWeekend ? 0.85 : 1.05; + + const spend = Math.floor( + baseSpend * multiplier * (0.9 + Math.random() * 0.2), + ); + const impressions = Math.floor( + baseImp * multiplier * (0.9 + Math.random() * 0.2), + ); + const clicks = Math.floor( + baseClicks * multiplier * (0.9 + Math.random() * 0.2), + ); + const conversions = Math.floor( + baseConv * multiplier * (0.9 + Math.random() * 0.2), + ); + + data.push({ + date: `${year}.${month}.${date}(${dayName})`, + spend, + impressions, + clicks, + ctr: Number(((clicks / impressions) * 100).toFixed(2)), + cpc: Math.floor(spend / clicks), + conversions, + roas: Math.floor(((conversions * 50000) / spend) * 100), + }); + } + return data; +}; + +export const platformDailyPerformanceMock: Record< + string, + IPlatformDailyPerformance[] +> = { + GOOGLE: generateRecentData(5000000, 1000000, 15000, 500), + NAVER: generateRecentData(2000000, 500000, 5000, 200), + META: generateRecentData(3000000, 300000, 18000, 300), +}; From a9672c5584c9cb3c65094cc8b324cd7475f28060 Mon Sep 17 00:00:00 2001 From: YermIm Date: Tue, 5 May 2026 12:47:21 +0900 Subject: [PATCH 5/8] =?UTF-8?q?feat:=20=EC=98=88=EC=82=B0=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/dashboard/charts/BudgetGaugeChart.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/dashboard/charts/BudgetGaugeChart.tsx b/src/components/dashboard/charts/BudgetGaugeChart.tsx index 5e99ac75..ffbd6f45 100644 --- a/src/components/dashboard/charts/BudgetGaugeChart.tsx +++ b/src/components/dashboard/charts/BudgetGaugeChart.tsx @@ -127,7 +127,7 @@ const BudgetGaugeChart = memo(function BudgetGaugeChart({ -
+
남은 예산 @@ -142,7 +142,7 @@ const BudgetGaugeChart = memo(function BudgetGaugeChart({
-
+
-
From bfa798fb9dea4df8d26ca3e65f8deb49b3929ad4 Mon Sep 17 00:00:00 2001 From: YermIm Date: Tue, 5 May 2026 14:35:53 +0900 Subject: [PATCH 8/8] =?UTF-8?q?feat:=20=EC=82=AC=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=EB=B0=94=20=ED=98=B8=EB=B2=84=20=EC=8B=9C=20=EC=84=9C=EB=B8=8C?= =?UTF-8?q?=EB=A9=94=EB=89=B4=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/sidebar/Sidebar.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/sidebar/Sidebar.tsx b/src/components/sidebar/Sidebar.tsx index bcf36de0..b0567c44 100644 --- a/src/components/sidebar/Sidebar.tsx +++ b/src/components/sidebar/Sidebar.tsx @@ -95,7 +95,10 @@ export default function Sidebar() {