+
+
+ {/* 성과 우수 플랫폼 */}
+
+ ) : (
+
+ 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/PlatformDetailTable.tsx b/src/components/dashboard/platform/PlatformDetailTable.tsx
new file mode 100644
index 00000000..7291f782
--- /dev/null
+++ b/src/components/dashboard/platform/PlatformDetailTable.tsx
@@ -0,0 +1,157 @@
+import { useMemo } from "react";
+
+import type { IPlatformDailyPerformance } from "@/pages/dashboard/platform/platformDashboard.mock";
+
+interface IPlatformDetailTableProps {
+ data: IPlatformDailyPerformance[];
+}
+
+function PlatformDetailTable({ data }: IPlatformDetailTableProps) {
+ // 합계 계산
+ const total = useMemo(() => {
+ if (!data.length) return null;
+ const totalSpend = data.reduce((acc, curr) => acc + curr.spend, 0);
+ const totalImpressions = data.reduce(
+ (acc, curr) => acc + curr.impressions,
+ 0,
+ );
+ const totalClicks = data.reduce((acc, curr) => acc + curr.clicks, 0);
+ const totalConversions = data.reduce(
+ (acc, curr) => acc + curr.conversions,
+ 0,
+ );
+ return {
+ spend: totalSpend,
+ impressions: totalImpressions,
+ clicks: totalClicks,
+ ctr: totalImpressions > 0 ? (totalClicks / totalImpressions) * 100 : 0,
+ cpc: totalClicks > 0 ? totalSpend / totalClicks : 0,
+ conversions: totalConversions,
+ roas:
+ totalSpend > 0
+ ? data.reduce((acc, curr) => acc + curr.roas * curr.spend, 0) /
+ totalSpend
+ : 0,
+ };
+ }, [data]);
+
+ return (
+
+
+
+
+
+
+
+ |
+ 날짜
+ |
+
+ 비용(지출)
+ |
+
+ 노출 수
+ |
+
+ 클릭 수
+ |
+
+ CTR(클릭률)
+ |
+
+ CPC
+ |
+
+ 전환 수
+ |
+
+ ROAS
+ |
+
+
+
+ {/* 합계 행 */}
+ {total && (
+
+ | 합계 |
+
+ ₩{total.spend.toLocaleString()}
+ |
+
+ {total.impressions.toLocaleString()}
+ |
+
+ {total.clicks.toLocaleString()}
+ |
+
+ {total.ctr.toFixed(2)}%
+ |
+
+ ₩{Math.round(total.cpc).toLocaleString()}
+ |
+
+ {total.conversions.toLocaleString()}
+ |
+
+ {Math.round(total.roas)}%
+ |
+
+ )}
+ {/* 일별 데이터 */}
+ {data.map((row, idx) => (
+
+ |
+ {row.date}
+ |
+
+ ₩{row.spend.toLocaleString()}
+ |
+
+ {row.impressions.toLocaleString()}
+ |
+
+ {row.clicks.toLocaleString()}
+ |
+
+ {row.ctr.toFixed(2)}%
+ |
+
+ ₩{row.cpc.toLocaleString()}
+ |
+
+ {row.conversions.toLocaleString()}
+ |
+
+ {row.roas}%
+ |
+
+ ))}
+
+
+
+
+ );
+}
+
+export default PlatformDetailTable;
diff --git a/src/components/dashboard/platform/SinglePlatformView.tsx b/src/components/dashboard/platform/SinglePlatformView.tsx
new file mode 100644
index 00000000..2cd89011
--- /dev/null
+++ b/src/components/dashboard/platform/SinglePlatformView.tsx
@@ -0,0 +1,252 @@
+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 PlatformDetailTable from "@/components/dashboard/platform/PlatformDetailTable";
+
+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 {
+ budgetStatusMock,
+ performanceEfficiencyMock,
+ platformDailyPerformanceMock,
+} 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 [viewRange, setViewRange] = React.useState<7 | 30>(7);
+
+ 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()];
+
+ 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;
+
+ const dailyData = useMemo(() => {
+ const key = platform?.toUpperCase();
+ const allData = key ? platformDailyPerformanceMock[key] || [] : [];
+ return allData.slice(0, viewRange);
+ }, [platform, viewRange]);
+
+ return (
+
+ {/* platform header */}
+
+
+ {logoInfo ? (
+
+ ) : (
+
{platform}
+ )}
+
+
+
+
+
+ {/* top */}
+
+ {isLoading
+ ? Array.from({ length: 4 }).map((_, i) => (
+
+
+
+
+
+ ))
+ : kpis.map((kpi) => (
+
+ ))}
+
+
+ {/* mid */}
+
+
+
+
+
+
+ }
+ RightElement={
+ budgetStatus && (
+
+ {budgetStatus}
+
+ )
+ }
+ >
+ {budget ? (
+
+
+
+ ) : (
+
+ 데이터가 없습니다.
+
+ )}
+
+
+
+ {/* bottom */}
+
+
+
+
+ }
+ >
+
+
+
+ );
+}
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() {