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
16 changes: 16 additions & 0 deletions src/api/dashboard/platform.ts
Original file line number Diff line number Diff line change
@@ -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<IAdStatusData> => {
const { data } = await axiosInstance.get<ICommonResponse<IAdStatusData>>(
`/api/dashboard/${orgId}/ad-count`,
{ params },
);
return data.data;
};
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const getMixedChartOptions = (categories: string[]): ApexOptions => ({
},
yaxis: [
{
seriesName: "클릭률",
seriesName: "클릭률(CTR)",
labels: {
formatter: (val) => `${val.toFixed(1)}%`,
style: {
Expand All @@ -49,7 +49,10 @@ export const getMixedChartOptions = (categories: string[]): ApexOptions => ({
},
},
},
{ show: false, seriesName: "전환율" },
{
seriesName: "클릭률(CTR)",
show: false,
},
{
opposite: true,
seriesName: "노출수",
Expand Down
96 changes: 73 additions & 23 deletions src/components/dashboard/platform/AllPlatformView.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 (
<div className="flex flex-col gap-8">
<div className="grid grid-cols-3 tablet:grid-cols-1 gap-6">
Expand All @@ -42,10 +58,18 @@ export default function AllPlatformView({ isLoading }: IAllPlatformViewProps) {
}
className="flex-1 min-h-67 flex flex-col"
>
{isLoading ? (
{isLoading || isRankingsLoading ? (
<TopPerformanceListSkeleton />
) : isRankingsError || !roasRankings ? (
Comment thread
YermIm marked this conversation as resolved.
<div className="flex flex-1 items-center justify-center font-body2 text-text-sub">
데이터를 불러오지 못했습니다.
</div>
) : roasRankings.length === 0 ? (
<div className="flex flex-1 items-center justify-center font-body2 text-text-sub">
표시할 순위 데이터가 없습니다.
</div>
) : (
<TopPerformanceList rankings={roasRankingMock} />
<TopPerformanceList rankings={roasRankings} />
)}
</Card>

Expand All @@ -63,20 +87,28 @@ export default function AllPlatformView({ isLoading }: IAllPlatformViewProps) {
/>
}
RightElement={
isLoading ? (
isLoading || isAdStatusLoading ? (
<BadgeSkeleton className="w-14" />
) : (
) : adStatus ? (
<Badge variant="stopped" size="sm" className="text-text-auth-sub">
총 {adStatusMock.totalCount}개
총 {adStatus.totalCount}개
</Badge>
)
) : null
}
className="flex-1 min-h-67 flex flex-col"
>
{isLoading ? (
{isLoading || isAdStatusLoading ? (
<AdStatusChartSkeleton />
) : isAdStatusError || !adStatus ? (
<div className="flex flex-1 items-center justify-center font-body2 text-text-sub">
데이터를 불러오지 못했습니다.
</div>
) : adStatus.providerCount.length === 0 ? (
<div className="flex flex-1 items-center justify-center font-body2 text-text-sub">
표시할 광고 소재가 없습니다.
</div>
) : (
<AdStatusChart data={adStatusMock.providerCount} />
<AdStatusChart data={adStatus.providerCount} />
)}
</Card>

Expand All @@ -94,10 +126,18 @@ export default function AllPlatformView({ isLoading }: IAllPlatformViewProps) {
/>
}
>
{isLoading ? (
{isLoading || isPerformanceLoading ? (
<PerformanceEfficiencyChartSkeleton />
) : isPerformanceError || !platformPerformance ? (
<div className="flex flex-1 items-center justify-center font-body2 text-text-sub">
데이터를 불러오지 못했습니다.
</div>
) : platformPerformance.length === 0 ? (
<div className="flex flex-1 items-center justify-center font-body2 text-text-sub">
표시할 성과 데이터가 없습니다.
</div>
) : (
<PerformanceEfficiencyChart data={performanceEfficiencyMock} />
<PerformanceEfficiencyChart data={platformPerformance} />
)}
</Card>
</div>
Expand All @@ -123,13 +163,23 @@ export default function AllPlatformView({ isLoading }: IAllPlatformViewProps) {

{/* 개별 플랫폼 상세 */}
<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} />
))}
{isLoading || isPerformanceLoading ? (
Array.from({ length: 3 }).map((_, i) => (
<PlatformDetailCardSkeleton key={i} />
))
) : isPerformanceError || !platformPerformance ? (
<div className="col-span-3 tablet:col-span-1 flex items-center justify-center font-body2 text-text-sub py-16">
데이터를 불러오지 못했습니다.
</div>
) : platformPerformance.length === 0 ? (
<div className="col-span-3 tablet:col-span-1 flex items-center justify-center font-body2 text-text-sub py-16">
표시할 플랫폼 데이터가 없습니다.
</div>
) : (
platformPerformance.map((platform) => (
<PlatformDetailCard key={platform.provider} data={platform} />
))
)}
</div>
</div>
);
Expand Down
8 changes: 4 additions & 4 deletions src/components/dashboard/platform/PlatformDetailCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand Down
33 changes: 33 additions & 0 deletions src/hooks/dashboard/usePlatformAdCount.ts
Original file line number Diff line number Diff line change
@@ -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<IAdStatusData>(
["platform", "adCount", orgId],
() => getAdCount(orgId!),
{
enabled: !!orgId,
select: (data): IAdStatusData => ({
...data,
providerCount: data.providerCount.map((item) => ({
...item,
provider: normalizeProvider(item.provider),
})),
}),
},
);
}
48 changes: 48 additions & 0 deletions src/hooks/dashboard/usePlatformPerformance.ts
Original file line number Diff line number Diff line change
@@ -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<IPlatformPerformance[]> => {
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<IPlatformPerformance> =>
r.status === "fulfilled",
)
.map((r) => r.value);

if (success.length !== PROVIDERS.length) {
throw new Error("일부 플랫폼 성과 데이터를 불러오지 못했습니다.");
}
return success;
},
{ enabled: !!orgId },
);
}
30 changes: 30 additions & 0 deletions src/hooks/dashboard/usePlatformRoasRankings.ts
Original file line number Diff line number Diff line change
@@ -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",
}),
Comment thread
YermIm marked this conversation as resolved.
{
enabled: !!orgId,
select: (data): IRoasRanking[] =>
data.rankings.map((item) => ({
...item,
provider: normalizeProvider(item.provider),
})),
},
);
}
11 changes: 0 additions & 11 deletions src/pages/dashboard/platform/platformDashboard.mock.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type {
IAdStatusData,
IBudgetStatus,
IPlatformPerformance,
IRoasRanking,
Expand Down Expand Up @@ -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[] = [
{
Expand Down
8 changes: 8 additions & 0 deletions src/types/dashboard/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading