Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/assets/logo/social-logo/wordmark/meta-wordmark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions src/assets/logo/social-logo/wordmark/naver-wordmark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions src/components/dashboard/charts/BudgetGaugeChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ const BudgetGaugeChart = memo(function BudgetGaugeChart({
</div>
</div>

<div className="flex flex-col gap-3">
<div className="flex-1 flex flex-col gap-3">
<div className="flex flex-col gap-1 rounded-2xl border border-bg-disabled/25 bg-bg-surface/50 p-4">
<span className="font-caption font-medium text-text-sub">
남은 예산
Expand All @@ -142,7 +142,7 @@ const BudgetGaugeChart = memo(function BudgetGaugeChart({
</span>
</div>

<div className="flex items-center gap-3 rounded-2xl bg-chart-inactive px-5 py-4">
<div className="mt-auto flex items-center gap-3 rounded-2xl bg-chart-inactive px-5 py-4">
<WarnCircleIcon
className="block size-5 shrink-0 text-text-sub"
aria-hidden="true"
Expand Down
122 changes: 122 additions & 0 deletions src/components/dashboard/platform/AllPlatformView.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex flex-col gap-8">
<div className="grid grid-cols-3 tablet:grid-cols-1 gap-6">
{/* 성과 우수 플랫폼 */}
<Card
title="성과 우수 플랫폼"
RightElement={
isLoading ? (
<BadgeSkeleton className="w-28" />
) : (
<Badge variant="stopped" size="sm" className="text-text-auth-sub">
ROAS 기준 상위 3
</Badge>
)
}
className="flex-1 min-h-67 flex flex-col"
>
{isLoading ? (
<TopPerformanceListSkeleton />
) : (
<TopPerformanceList rankings={roasRankingMock} />
)}
</Card>

{/* 광고 소재 현황 */}
<Card
title="광고 소재 현황"
description={
<ChartLegend
className="[&_div]:rounded-none"
items={[
{ label: "Google", color: "#f9ab00" },
{ label: "NAVER", color: "#03c75a" },
{ label: "Meta", color: "#1877f2" },
]}
/>
}
RightElement={
isLoading ? (
<BadgeSkeleton className="w-14" />
) : (
<Badge variant="stopped" size="sm" className="text-text-auth-sub">
총 {adStatusMock.totalCount}개
</Badge>
)
}
className="flex-1 min-h-67 flex flex-col"
>
{isLoading ? (
<AdStatusChartSkeleton />
) : (
<AdStatusChart data={adStatusMock.providerCount} />
)}
</Card>

{/* 플랫폼별 성과 효율 비교 */}
<Card
title="플랫폼별 성과 효율 비교"
className="flex-1 min-h-67 flex flex-col pb-1"
description={
<ChartLegend
items={[
{ label: "클릭률", color: "#0084fe" },
{ label: "전환율", color: "#0a3d91" },
{ label: "노출수", color: "#4fc3f7" },
]}
/>
}
>
{isLoading ? (
<PerformanceEfficiencyChartSkeleton />
) : (
<PerformanceEfficiencyChart data={performanceEfficiencyMock} />
)}
</Card>
</div>

{/* 실시간 트래픽 변화 */}
<Card title="실시간 트래픽 변화" className="min-h-125">
{isLoading ? <TrafficChartSkeleton /> : null}
</Card>
Comment thread
YermIm marked this conversation as resolved.

{/* 개별 플랫폼 상세 */}
<div className="grid grid-cols-3 tablet:grid-cols-1 gap-6">
{isLoading
? Array.from({ length: 3 }).map((_, i) => (
<PlatformDetailCardSkeleton key={i} />
))
: performanceEfficiencyMock.map((platform) => (
<PlatformDetailCard key={platform.provider} data={platform} />
))}
</div>
</div>
);
}
157 changes: 157 additions & 0 deletions src/components/dashboard/platform/PlatformDetailTable.tsx
Original file line number Diff line number Diff line change
@@ -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]);
Comment thread
YermIm marked this conversation as resolved.

return (
<div className="mt-4 flex flex-col">
<style>{`
.custom-scrollbar {
scrollbar-gutter: stable;
}
.custom-scrollbar::-webkit-scrollbar {
width: 5px;
height: 5px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #D1D1D1;
border-radius: 10px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #B1B1B1;
}
`}</style>

<div className="overflow-auto max-h-125 relative custom-scrollbar border-t border-bg-disabled">
<table className="w-full text-left border-separate border-spacing-0 min-w-[800px] table-fixed">
<thead className="sticky top-0 z-20 bg-white">
<tr className="text-text-sub font-body2">
<th className="w-[14%] px-4 py-4 font-normal border-b border-bg-disabled/90">
날짜
</th>
<th className="w-[14%] px-4 py-4 font-normal text-right border-b border-bg-disabled">
비용(지출)
</th>
<th className="w-[12%] px-4 py-4 font-normal text-right border-b border-bg-disabled">
노출 수
</th>
<th className="w-[12%] px-4 py-4 font-normal text-right border-b border-bg-disabled">
클릭 수
</th>
<th className="w-[12%] px-4 py-4 font-normal text-right border-b border-bg-disabled">
CTR(클릭률)
</th>
<th className="w-[12%] px-4 py-4 font-normal text-right border-b border-bg-disabled">
CPC
</th>
<th className="w-[12%] px-4 py-4 font-normal text-right border-b border-bg-disabled">
전환 수
</th>
<th className="w-[12%] px-4 py-4 font-normal text-right border-b border-bg-disabled">
ROAS
</th>
Comment thread
YermIm marked this conversation as resolved.
</tr>
</thead>
<tbody className="text-text-main font-body2">
{/* 합계 행 */}
{total && (
<tr className="sticky top-13 z-10 bg-white/95 backdrop-blur-sm font-bold border-b-2 border-bg-disabled">
<td className="px-4 py-5 border-b border-bg-disabled">합계</td>
Comment thread
YermIm marked this conversation as resolved.
<td className="px-4 py-5 text-right tabular-nums border-b border-bg-disabled text-brand-main">
₩{total.spend.toLocaleString()}
</td>
<td className="px-4 py-5 text-right tabular-nums border-b border-bg-disabled">
{total.impressions.toLocaleString()}
</td>
<td className="px-4 py-5 text-right tabular-nums border-b border-bg-disabled">
{total.clicks.toLocaleString()}
</td>
<td className="px-4 py-5 text-right tabular-nums border-b border-bg-disabled">
{total.ctr.toFixed(2)}%
</td>
<td className="px-4 py-5 text-right tabular-nums border-b border-bg-disabled">
₩{Math.round(total.cpc).toLocaleString()}
</td>
<td className="px-4 py-5 text-right tabular-nums border-b border-bg-disabled">
{total.conversions.toLocaleString()}
</td>
<td className="px-4 py-5 text-right tabular-nums border-b border-bg-disabled text-brand-main">
{Math.round(total.roas)}%
</td>
</tr>
)}
{/* 일별 데이터 */}
{data.map((row, idx) => (
<tr
key={idx}
Comment thread
YermIm marked this conversation as resolved.
className="hover:bg-bg-surface/30 transition-colors group"
>
<td className="px-4 py-4 text-text-sub border-b border-bg-disabled/20 truncate">
{row.date}
</td>
<td className="px-4 py-4 text-right tabular-nums text-text-main border-b border-bg-disabled/20">
₩{row.spend.toLocaleString()}
</td>
<td className="px-4 py-4 text-right tabular-nums text-text-main border-b border-bg-disabled/20">
{row.impressions.toLocaleString()}
</td>
<td className="px-4 py-4 text-right tabular-nums text-text-main border-b border-bg-disabled/20">
{row.clicks.toLocaleString()}
</td>
<td className="px-4 py-4 text-right tabular-nums text-text-main border-b border-bg-disabled/20">
{row.ctr.toFixed(2)}%
</td>
<td className="px-4 py-4 text-right tabular-nums text-text-main border-b border-bg-disabled/20">
₩{row.cpc.toLocaleString()}
</td>
<td className="px-4 py-4 text-right tabular-nums text-text-main border-b border-bg-disabled/20">
{row.conversions.toLocaleString()}
</td>
<td className="px-4 py-4 text-right tabular-nums text-text-main font-medium border-b border-bg-disabled/20">
{row.roas}%
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}

export default PlatformDetailTable;
Loading
Loading