Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
18 changes: 17 additions & 1 deletion src/api/dashboard/platform.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type { ICommonResponse } from "@/types/common/common";
import type { IAdCountParams, IAdStatusData } from "@/types/dashboard/platform";
import type {
IAdCountParams,
IAdStatusData,
IMetricFactsParams,
IMetricFactsResponse,
} from "@/types/dashboard/platform";

import { axiosInstance } from "@/lib/axiosInstance";

Expand All @@ -14,3 +19,14 @@ export const getAdCount = async (
);
return data.data;
};

// 광고 현황 상세 일별 조회 API
export const getMetricFacts = async (
orgId: number,
params?: IMetricFactsParams,
): Promise<IMetricFactsResponse> => {
const { data } = await axiosInstance.get<
ICommonResponse<IMetricFactsResponse>
>(`/api/dashboard/${orgId}/metric-facts`, { params });
return data.data;
};
12 changes: 9 additions & 3 deletions src/components/dashboard/platform/PlatformDetailTable.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { useMemo } from "react";

import type { IPlatformDailyPerformance } from "@/pages/dashboard/platform/platformDashboard.mock";
import type { IPlatformDailyPerformance } from "@/types/dashboard/platform";

interface IPlatformDetailTableProps {
data: IPlatformDailyPerformance[];
total?: IPlatformDailyPerformance | null;
}

function PlatformDetailTable({ data }: IPlatformDetailTableProps) {
function PlatformDetailTable({
data,
total: totalFromApi,
}: IPlatformDetailTableProps) {
// 합계 계산
const total = useMemo(() => {
const computedTotal = useMemo(() => {
if (!data.length) return null;
const totalSpend = data.reduce((acc, curr) => acc + curr.spend, 0);
const totalImpressions = data.reduce(
Expand All @@ -35,6 +39,8 @@ function PlatformDetailTable({ data }: IPlatformDetailTableProps) {
};
}, [data]);

const total = totalFromApi ?? computedTotal;

return (
<div className="mt-4 flex flex-col">
<div className="overflow-auto max-h-125 relative platform-table-scrollbar border-t border-surface-400">
Expand Down
40 changes: 29 additions & 11 deletions src/components/dashboard/platform/SinglePlatformView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { twMerge } from "tailwind-merge";
import type { TPlatformProvider } from "@/types/dashboard/platform";

import { usePlatformBudget } from "@/hooks/dashboard/usePlatformBudget";
import { usePlatformMetricFacts } from "@/hooks/dashboard/usePlatformMetricFacts";
import { usePlatformMetrics } from "@/hooks/dashboard/usePlatformMetrics";

import Badge from "@/components/common/badge/Badge";
Expand All @@ -21,10 +22,7 @@ import PlatformTrafficChart from "@/components/dashboard/platform/PlatformTraffi
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 {
platformDailyPerformanceMock,
platformTrafficMock,
} from "@/pages/dashboard/platform/platformDashboard.mock";
import { platformTrafficMock } from "@/pages/dashboard/platform/platformDashboard.mock";

const PLATFORM_LOGOS: Record<
string,
Expand Down Expand Up @@ -92,6 +90,15 @@ export default function SinglePlatformView({
isError: isBudgetError,
} = usePlatformBudget(platform.toUpperCase() as TPlatformProvider);

const {
data: metricFacts,
isLoading: isMetricFactsLoading,
isError: isMetricFactsError,
} = usePlatformMetricFacts(
platform.toUpperCase() as TPlatformProvider,
viewRange,
);

const budgetPct = budget
? Math.round((budget.spent / budget.totalBudget) * 100)
: 0;
Expand All @@ -104,12 +111,6 @@ export default function SinglePlatformView({
)
: null;

const dailyData = useMemo(() => {
const key = platform?.toUpperCase();
const allData = key ? platformDailyPerformanceMock[key] || [] : [];
return allData.slice(0, viewRange);
}, [platform, viewRange]);

const PLATFORM_THEME_COLORS: Record<string, string> = {
GOOGLE: "#f9ab00",
NAVER: "#03c75a",
Expand Down Expand Up @@ -264,7 +265,24 @@ export default function SinglePlatformView({
</div>
}
>
<PlatformDetailTable data={dailyData} />
{isLoading || isMetricFactsLoading ? (
<div className="flex items-center justify-center py-16">
<Skeleton className="h-40 w-full rounded-2xl" />
</div>
) : isMetricFactsError ? (
<div className="flex items-center justify-center py-16 text-center font-body2 text-info-red">
광고 현황 데이터를 불러오지 못했습니다.
</div>
) : !metricFacts?.dailyRows.length ? (
<div className="flex items-center justify-center py-16 text-center font-body2 text-text-muted">
표시할 광고 현황 데이터가 없습니다.
</div>
) : (
<PlatformDetailTable
data={metricFacts.dailyRows}
total={metricFacts.totalRow}
/>
)}
</Card>
</div>
);
Expand Down
80 changes: 80 additions & 0 deletions src/hooks/dashboard/usePlatformMetricFacts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type { TProviderType } from "@/types/dashboard/overview";
import type {
IMetricFactsResponse,
IMetricFactsRow,
IPlatformDailyPerformance,
TPlatformProvider,
} from "@/types/dashboard/platform";

import { useCoreQuery } from "@/hooks/customQuery";

import { getMetricFacts } from "@/api/dashboard/platform";
import useWorkspaceStore from "@/store/useWorkspaceStore";

const DAY_LABELS = ["일", "월", "화", "수", "목", "금", "토"] as const;

function formatTableDate(iso: string): string {
const d = new Date(`${iso}T00:00:00`);
if (Number.isNaN(d.getTime())) return iso;

const yy = String(d.getFullYear()).slice(-2);
const mm = String(d.getMonth() + 1).padStart(2, "0");
const dd = String(d.getDate()).padStart(2, "0");
return `${yy}.${mm}.${dd}(${DAY_LABELS[d.getDay()]})`;
}

function mapRow(row: IMetricFactsRow): IPlatformDailyPerformance {
return {
date: formatTableDate(row.date),
spend: row.spend,
impressions: row.impressions,
clicks: row.clicks,
ctr: row.ctr,
cpc: row.cpa,
conversions: row.conversions,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
roas: row.roas,
};
}

function mapTotalRow(total: IMetricFactsRow): IPlatformDailyPerformance {
return {
date: "합계",
spend: total.spend,
impressions: total.impressions,
clicks: total.clicks,
ctr: total.ctr,
cpc: total.cpa,
conversions: total.conversions,
roas: total.roas,
};
}

export type TMetricFactsViewModel = {
dailyRows: IPlatformDailyPerformance[];
totalRow: IPlatformDailyPerformance;
};

export function usePlatformMetricFacts(
provider: TPlatformProvider,
days: 7 | 30,
) {
const orgId = useWorkspaceStore((s) => s.selectedOrgId);

return useCoreQuery<IMetricFactsResponse, TMetricFactsViewModel>(
["platform", "metricFacts", orgId, provider, days],
() =>
getMetricFacts(orgId!, {
providerType: provider as TProviderType,
days,
}),
{
enabled: !!orgId && !!provider,
select: (data) => ({
dailyRows: [...data.dailyMetrics]
.sort((a, b) => b.date.localeCompare(a.date))
.map(mapRow),
totalRow: mapTotalRow(data.total),
}),
},
);
}
72 changes: 0 additions & 72 deletions src/pages/dashboard/platform/platformDashboard.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,79 +94,7 @@ export const budgetStatusMock: IBudgetStatus[] = [
},
];

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),
};

// 실시간 트래픽 데이터 추가

export interface ITimeSeriesData {
minute: string; // YYYYMMDDHHmm
count: number;
Expand Down
39 changes: 39 additions & 0 deletions src/types/dashboard/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,42 @@ export interface IBudgetStatus {
totalSpend: number;
remainingBudget: number;
}

// API 일자별·합계
export interface IMetricFactsRow {
date: string;
impressions: number;
clicks: number;
spend: number;
conversions: number;
revenue: number;
ctr: number;
cpa: number;
roas: number;
}

// /api/dashboard/{orgId}/metric-facts 응답 data
export interface IMetricFactsResponse {
providerType: string;
startDate: string;
endDate: string;
total: IMetricFactsRow;
dailyMetrics: IMetricFactsRow[];
}

export interface IMetricFactsParams {
providerType: TProviderType;
days?: number;
}

// 테이블 UI row
export interface IPlatformDailyPerformance {
date: string;
spend: number;
impressions: number;
clicks: number;
ctr: number;
cpc: number;
conversions: number;
roas: number;
}
Loading