diff --git a/src/api/dashboard/platform.ts b/src/api/dashboard/platform.ts new file mode 100644 index 00000000..a5652967 --- /dev/null +++ b/src/api/dashboard/platform.ts @@ -0,0 +1,16 @@ +import type { ICommonResponse } from "@/types/common/common"; +import type { IAdCountParams, IAdStatusData } from "@/types/dashboard/platform"; + +import { axiosInstance } from "@/lib/axiosInstance"; + +// 광고 소재 현황 조회 API +export const getAdCount = async ( + orgId: number, + params?: IAdCountParams, +): Promise => { + const { data } = await axiosInstance.get>( + `/api/dashboard/${orgId}/ad-count`, + { params }, + ); + return data.data; +}; diff --git a/src/components/dashboard/charts/performanceEfficiencyChart.config.ts b/src/components/dashboard/charts/performanceEfficiencyChart.config.ts index 0e105122..fc40a138 100644 --- a/src/components/dashboard/charts/performanceEfficiencyChart.config.ts +++ b/src/components/dashboard/charts/performanceEfficiencyChart.config.ts @@ -40,7 +40,7 @@ export const getMixedChartOptions = (categories: string[]): ApexOptions => ({ }, yaxis: [ { - seriesName: "클릭률", + seriesName: "클릭률(CTR)", labels: { formatter: (val) => `${val.toFixed(1)}%`, style: { @@ -49,7 +49,10 @@ export const getMixedChartOptions = (categories: string[]): ApexOptions => ({ }, }, }, - { show: false, seriesName: "전환율" }, + { + seriesName: "클릭률(CTR)", + show: false, + }, { opposite: true, seriesName: "노출수", diff --git a/src/components/dashboard/platform/AllPlatformView.tsx b/src/components/dashboard/platform/AllPlatformView.tsx index eec98854..0b683f6e 100644 --- a/src/components/dashboard/platform/AllPlatformView.tsx +++ b/src/components/dashboard/platform/AllPlatformView.tsx @@ -1,3 +1,7 @@ +import { usePlatformAdCount } from "@/hooks/dashboard/usePlatformAdCount"; +import { usePlatformPerformance } from "@/hooks/dashboard/usePlatformPerformance"; +import { usePlatformRoasRankings } from "@/hooks/dashboard/usePlatformRoasRankings"; + import Badge from "@/components/common/badge/Badge"; import Card from "@/components/common/card/Card"; import ChartLegend from "@/components/common/chart/ChartLegend"; @@ -14,17 +18,29 @@ import { } 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) { + const { + data: roasRankings, + isLoading: isRankingsLoading, + isError: isRankingsError, + } = usePlatformRoasRankings(); + + const { + data: adStatus, + isLoading: isAdStatusLoading, + isError: isAdStatusError, + } = usePlatformAdCount(); + + const { + data: platformPerformance, + isLoading: isPerformanceLoading, + isError: isPerformanceError, + } = usePlatformPerformance(); + return (
@@ -42,10 +58,18 @@ export default function AllPlatformView({ isLoading }: IAllPlatformViewProps) { } className="flex-1 min-h-67 flex flex-col" > - {isLoading ? ( + {isLoading || isRankingsLoading ? ( + ) : isRankingsError || !roasRankings ? ( +
+ 데이터를 불러오지 못했습니다. +
+ ) : roasRankings.length === 0 ? ( +
+ 표시할 순위 데이터가 없습니다. +
) : ( - + )} @@ -63,20 +87,28 @@ export default function AllPlatformView({ isLoading }: IAllPlatformViewProps) { /> } RightElement={ - isLoading ? ( + isLoading || isAdStatusLoading ? ( - ) : ( + ) : adStatus ? ( - 총 {adStatusMock.totalCount}개 + 총 {adStatus.totalCount}개 - ) + ) : null } className="flex-1 min-h-67 flex flex-col" > - {isLoading ? ( + {isLoading || isAdStatusLoading ? ( + ) : isAdStatusError || !adStatus ? ( +
+ 데이터를 불러오지 못했습니다. +
+ ) : adStatus.providerCount.length === 0 ? ( +
+ 표시할 광고 소재가 없습니다. +
) : ( - + )} @@ -94,10 +126,18 @@ export default function AllPlatformView({ isLoading }: IAllPlatformViewProps) { /> } > - {isLoading ? ( + {isLoading || isPerformanceLoading ? ( + ) : isPerformanceError || !platformPerformance ? ( +
+ 데이터를 불러오지 못했습니다. +
+ ) : platformPerformance.length === 0 ? ( +
+ 표시할 성과 데이터가 없습니다. +
) : ( - + )}
@@ -123,13 +163,23 @@ export default function AllPlatformView({ isLoading }: IAllPlatformViewProps) { {/* 개별 플랫폼 상세 */}
- {isLoading - ? Array.from({ length: 3 }).map((_, i) => ( - - )) - : performanceEfficiencyMock.map((platform) => ( - - ))} + {isLoading || isPerformanceLoading ? ( + Array.from({ length: 3 }).map((_, i) => ( + + )) + ) : isPerformanceError || !platformPerformance ? ( +
+ 데이터를 불러오지 못했습니다. +
+ ) : platformPerformance.length === 0 ? ( +
+ 표시할 플랫폼 데이터가 없습니다. +
+ ) : ( + platformPerformance.map((platform) => ( + + )) + )}
); diff --git a/src/components/dashboard/platform/PlatformDetailCard.tsx b/src/components/dashboard/platform/PlatformDetailCard.tsx index ccfd3ddd..1621d251 100644 --- a/src/components/dashboard/platform/PlatformDetailCard.tsx +++ b/src/components/dashboard/platform/PlatformDetailCard.tsx @@ -52,7 +52,7 @@ export const PlatformDetailCard = memo( impressionChangeRate !== 0 ? { direction: impressionChangeRate > 0 ? "up" : "down", - value: `${Math.abs(impressionChangeRate * 100).toFixed(1)}%`, + value: `${Math.abs(impressionChangeRate).toFixed(1)}%`, } : undefined } @@ -65,7 +65,7 @@ export const PlatformDetailCard = memo( clickChangeRate !== 0 ? { direction: clickChangeRate > 0 ? "up" : "down", - value: `${Math.abs(clickChangeRate * 100).toFixed(1)}%`, + value: `${Math.abs(clickChangeRate).toFixed(1)}%`, } : undefined } @@ -78,7 +78,7 @@ export const PlatformDetailCard = memo( cvrChangeRate !== 0 ? { direction: cvrChangeRate > 0 ? "up" : "down", - value: `${Math.abs(cvrChangeRate * 100).toFixed(1)}%`, + value: `${Math.abs(cvrChangeRate).toFixed(1)}%`, } : undefined } @@ -91,7 +91,7 @@ export const PlatformDetailCard = memo( ROASChangeRate !== 0 ? { direction: ROASChangeRate > 0 ? "up" : "down", - value: `${Math.abs(ROASChangeRate * 100).toFixed(1)}%`, + value: `${Math.abs(ROASChangeRate).toFixed(1)}%`, } : undefined } diff --git a/src/hooks/dashboard/usePlatformAdCount.ts b/src/hooks/dashboard/usePlatformAdCount.ts new file mode 100644 index 00000000..93dc3024 --- /dev/null +++ b/src/hooks/dashboard/usePlatformAdCount.ts @@ -0,0 +1,33 @@ +import type { + IAdStatusData, + TPlatformProvider, +} from "@/types/dashboard/platform"; + +import { useCoreQuery } from "@/hooks/customQuery"; + +import { getAdCount } from "@/api/dashboard/platform"; +import useWorkspaceStore from "@/store/useWorkspaceStore"; + +// TODO: 추후 제거 +const normalizeProvider = (provider: string): TPlatformProvider => + provider === "KAKAO" ? "META" : (provider as TPlatformProvider); + +// 광고 소재 현황 +export function usePlatformAdCount() { + const orgId = useWorkspaceStore((s) => s.selectedOrgId); + + return useCoreQuery( + ["platform", "adCount", orgId], + () => getAdCount(orgId!), + { + enabled: !!orgId, + select: (data): IAdStatusData => ({ + ...data, + providerCount: data.providerCount.map((item) => ({ + ...item, + provider: normalizeProvider(item.provider), + })), + }), + }, + ); +} diff --git a/src/hooks/dashboard/usePlatformPerformance.ts b/src/hooks/dashboard/usePlatformPerformance.ts new file mode 100644 index 00000000..3c73e39e --- /dev/null +++ b/src/hooks/dashboard/usePlatformPerformance.ts @@ -0,0 +1,48 @@ +import type { TProviderType } from "@/types/dashboard/overview"; +import type { + IPlatformPerformance, + TPlatformProvider, +} from "@/types/dashboard/platform"; + +import { useCoreQuery } from "@/hooks/customQuery"; + +import { getOverview } from "@/api/dashboard/overview"; +import useWorkspaceStore from "@/store/useWorkspaceStore"; + +// TODO: 추후 제거 +const normalizeProvider = (provider: string): TPlatformProvider => + provider === "KAKAO" ? "META" : (provider as TPlatformProvider); + +const PROVIDERS: TProviderType[] = ["GOOGLE", "NAVER", "KAKAO"]; + +// 플랫폼별 성과 효율 (3개 플랫폼 병렬 조회 후 병합) +export function usePlatformPerformance() { + const orgId = useWorkspaceStore((s) => s.selectedOrgId); + + return useCoreQuery( + ["platform", "performance", orgId], + async (): Promise => { + const settled = await Promise.allSettled( + PROVIDERS.map((provider) => + getOverview(orgId!, provider).then((metrics) => ({ + ...metrics, + provider: normalizeProvider(provider), + })), + ), + ); + + const success = settled + .filter( + (r): r is PromiseFulfilledResult => + r.status === "fulfilled", + ) + .map((r) => r.value); + + if (success.length !== PROVIDERS.length) { + throw new Error("일부 플랫폼 성과 데이터를 불러오지 못했습니다."); + } + return success; + }, + { enabled: !!orgId }, + ); +} diff --git a/src/hooks/dashboard/usePlatformRoasRankings.ts b/src/hooks/dashboard/usePlatformRoasRankings.ts new file mode 100644 index 00000000..589b5a10 --- /dev/null +++ b/src/hooks/dashboard/usePlatformRoasRankings.ts @@ -0,0 +1,30 @@ +import type { IRoasRanking } from "@/types/dashboard/overview"; + +import { useCoreQuery } from "@/hooks/customQuery"; + +import { getRoasRankings } from "@/api/dashboard/overview"; +import useWorkspaceStore from "@/store/useWorkspaceStore"; +// TODO: 추후 제거 +const normalizeProvider = (provider: string): string => + provider === "KAKAO" ? "META" : provider; +// ROAS 성과 순위 (상위 3개) +export function usePlatformRoasRankings() { + const orgId = useWorkspaceStore((s) => s.selectedOrgId); + return useCoreQuery( + ["platform", "roasRankings", orgId], + // TODO: 추후 제거 + () => + getRoasRankings(orgId!, { + startDate: "2026-01-22", + endDate: "2026-03-22", + }), + { + enabled: !!orgId, + select: (data): IRoasRanking[] => + data.rankings.map((item) => ({ + ...item, + provider: normalizeProvider(item.provider), + })), + }, + ); +} diff --git a/src/pages/dashboard/platform/platformDashboard.mock.ts b/src/pages/dashboard/platform/platformDashboard.mock.ts index 7cdbbfd2..b9d665c0 100644 --- a/src/pages/dashboard/platform/platformDashboard.mock.ts +++ b/src/pages/dashboard/platform/platformDashboard.mock.ts @@ -1,5 +1,4 @@ import type { - IAdStatusData, IBudgetStatus, IPlatformPerformance, IRoasRanking, @@ -33,16 +32,6 @@ export const roasRankingMock: IRoasRanking[] = [ }, ]; -// 광고 소재 현황 -export const adStatusMock: IAdStatusData = { - totalCount: 14, - providerCount: [ - { provider: "GOOGLE", count: 7 }, - { provider: "NAVER", count: 5 }, - { provider: "META", count: 2 }, - ], -}; - // 플랫폼별 성과 효율 비교 export const performanceEfficiencyMock: IPlatformPerformance[] = [ { diff --git a/src/types/dashboard/platform.ts b/src/types/dashboard/platform.ts index a6f4258d..0a5a0fd8 100644 --- a/src/types/dashboard/platform.ts +++ b/src/types/dashboard/platform.ts @@ -26,10 +26,18 @@ export interface IAdCount { // 광고 소재 현황 export interface IAdStatusData { + startDate: string; + endDate: string; totalCount: number; providerCount: IAdCount[]; } +//광고 소재 현황 조회 요청 파라미터 +export interface IAdCountParams { + startDate?: string; + endDate?: string; +} + // 플랫폼별 성과 export interface IPlatformPerformance { provider: TPlatformProvider;