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: 3 additions & 3 deletions src/api/dashboard/overview.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ICommonResponse } from "@/types/common/common";
import type {
IBudgetsResponse,
IBudgetResponse,
IMetricsResponse,
IRoasRankingsParams,
IRoasRankingsResponse,
Expand All @@ -25,8 +25,8 @@ export const getOverview = async (
export const getBudget = async (
orgId: number,
providerType?: TProviderType,
): Promise<IBudgetsResponse> => {
const { data } = await axiosInstance.get<ICommonResponse<IBudgetsResponse>>(
): Promise<IBudgetResponse> => {
const { data } = await axiosInstance.get<ICommonResponse<IBudgetResponse>>(
`/api/dashboard/budgets`,
{ params: { orgId, ...(providerType ? { providerType } : {}) } },
);
Expand Down
16 changes: 7 additions & 9 deletions src/components/dashboard/charts/TrafficChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
useState,
} from "react";

import { parseMinuteToTimestamp } from "@/utils/dashboard/parseMinuteToTimestamp";

import { useClickStream } from "@/hooks/dashboard/useClickStream";

import { DropdownMenu } from "@/components/common/dropdownmenu/DropdownMenu";
Expand Down Expand Up @@ -122,15 +124,11 @@ const TrafficChart = memo(function TrafficChart() {

const chartData = items
.filter((d) => (d.minute?.length ?? 0) >= 12)
.map((d) => {
const year = parseInt(d.minute.slice(0, 4), 10);
const month = parseInt(d.minute.slice(4, 6), 10) - 1;
const day = parseInt(d.minute.slice(6, 8), 10);
const hour = parseInt(d.minute.slice(8, 10), 10);
const min = parseInt(d.minute.slice(10, 12), 10);
const x = new Date(year, month, day, hour, min).getTime();
return { x, y: d.count, minute: d.minute };
})
.map((d) => ({
x: parseMinuteToTimestamp(d.minute),
y: d.count,
minute: d.minute,
}))
.filter((p) => !Number.isNaN(p.x));

// 해당 일 00:00 (데이터 없으면 오늘)
Expand Down
17 changes: 6 additions & 11 deletions src/components/dashboard/platform/AllPlatformTrafficChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
PROVIDER_TYPES,
} from "@/types/dashboard/provider";

import { parseMinuteToTimestamp } from "@/utils/dashboard/parseMinuteToTimestamp";

import { Skeleton } from "@/components/common/skeleton/Skeleton";

import { platformTrafficMock } from "@/pages/dashboard/platform/platformDashboard.mock";
Expand All @@ -26,17 +28,10 @@ const AllPlatformTrafficChart = memo(function AllPlatformTrafficChart({
return {
name: PLATFORM_MAP[platform],
color: PLATFORM_CHART_COLORS[platform],
data: data.timeSeriesData.map((d) => {
const year = parseInt(d.minute.slice(0, 4), 10);
const month = parseInt(d.minute.slice(4, 6), 10) - 1;
const day = parseInt(d.minute.slice(6, 8), 10);
const hour = parseInt(d.minute.slice(8, 10), 10);
const min = parseInt(d.minute.slice(10, 12), 10);
return {
x: new Date(year, month, day, hour, min).getTime(),
y: d.count,
};
}),
data: data.timeSeriesData.map((d) => ({
x: parseMinuteToTimestamp(d.minute),
y: d.count,
})),
};
});
}, []);
Expand Down
23 changes: 10 additions & 13 deletions src/components/dashboard/platform/PlatformRoasTable.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,14 @@
import { memo, type ReactNode } from "react";
import { memo } from "react";

import type {
IPlatformRankingItem,
TProviderType,
} from "@/types/dashboard/overview";
import { PLATFORM_MAP } from "@/types/dashboard/provider";
import { PLATFORM_CIRCLE_LOGO_MAP } from "@/constants/dashboard/platformLogos";

import { TrendBadge } from "@/components/common/card/StatCard";

import GoogleLogo from "@/assets/logo/social-logo/circle/google-circle.svg?react";
import MetaLogo from "@/assets/logo/social-logo/circle/meta-circle.svg?react";
import NaverLogo from "@/assets/logo/social-logo/circle/naver-circle.svg?react";

const platformLogoMap: Record<TProviderType, ReactNode> = {
GOOGLE: <GoogleLogo className="h-7 w-auto" />,
NAVER: <NaverLogo className="h-7 w-auto" />,
META: <MetaLogo className="h-7 w-auto" />,
};

function toProviderType(provider: string): TProviderType | null {
const key = provider.toUpperCase();
if (key in PLATFORM_MAP) return key as TProviderType;
Expand All @@ -31,10 +22,16 @@ function getDisplayName(provider: string): string {

function getPlatformLogo(provider: string) {
const key = toProviderType(provider);
if (key) return platformLogoMap[key];
if (key) {
const Logo = PLATFORM_CIRCLE_LOGO_MAP[key];
return <Logo className="h-8 w-8" aria-hidden="true" />;
}
const name = getDisplayName(provider);
return (
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-surface-300 font-caption text-text-muted">
<span
aria-hidden="true"
className="flex h-8 w-8 items-center justify-center rounded-full bg-surface-300 font-caption text-text-muted"
>
{name[0]}
</span>
);
Expand Down
18 changes: 6 additions & 12 deletions src/components/dashboard/platform/PlatformTrafficChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import type { ApexOptions } from "apexcharts";
import type { TProviderType } from "@/types/dashboard/overview";
import { PLATFORM_CHART_COLORS } from "@/types/dashboard/provider";

import { parseMinuteToTimestamp } from "@/utils/dashboard/parseMinuteToTimestamp";

import { Skeleton } from "@/components/common/skeleton/Skeleton";

import type { IClickStreamResponse } from "@/pages/dashboard/platform/platformDashboard.mock";
Expand All @@ -20,20 +22,12 @@ const PlatformTrafficChart = memo(function PlatformTrafficChart({
platform,
isLoading,
}: IPlatformTrafficChartProps) {
// 데이터 변환: minute 문자열 -> 타임스탬프
const seriesData = useMemo(() => {
if (!data) return [];
return data.timeSeriesData.map((d) => {
const year = parseInt(d.minute.slice(0, 4), 10);
const month = parseInt(d.minute.slice(4, 6), 10) - 1;
const day = parseInt(d.minute.slice(6, 8), 10);
const hour = parseInt(d.minute.slice(8, 10), 10);
const min = parseInt(d.minute.slice(10, 12), 10);
return {
x: new Date(year, month, day, hour, min).getTime(),
y: d.count,
};
});
return data.timeSeriesData.map((d) => ({
x: parseMinuteToTimestamp(d.minute),
y: d.count,
}));
}, [data]);

// X축 범위 계산 (최근 60분)
Expand Down
45 changes: 10 additions & 35 deletions src/components/dashboard/platform/SinglePlatformView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import { twMerge } from "tailwind-merge";
import type { TProviderType } from "@/types/dashboard/overview";
import { PLATFORM_CHART_COLORS } from "@/types/dashboard/provider";

import { usePlatformBudget } from "@/hooks/dashboard/usePlatformBudget";
import { metricsToKpis } from "@/utils/dashboard/metricsToKpis";

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

import Badge from "@/components/common/badge/Badge";
import Card from "@/components/common/card/Card";
import StatCard, { type ITrend } from "@/components/common/card/StatCard";
import StatCard from "@/components/common/card/StatCard";
import ChartLegend from "@/components/common/chart/ChartLegend";
import { Skeleton } from "@/components/common/skeleton/Skeleton";
import DashboardAiSummarySection from "@/components/dashboard/ai-report/components/DashboardAiSummarySection";
Expand Down Expand Up @@ -52,45 +54,18 @@ export default function SinglePlatformView({
isError: isMetricsError,
} = usePlatformMetrics(platform);

const toTrend = (changeRate: number): ITrend => ({
direction: changeRate >= 0 ? "up" : "down",
value: `${Math.abs(changeRate).toFixed(2)}%`,
});

const kpis = useMemo(() => {
if (!platformData) return [];

return [
{
title: "노출수",
value: platformData.impressions.toLocaleString(),
trend: toTrend(platformData.impressionChangeRate),
},
{
title: "클릭수 (CTR)",
value: platformData.clicks.toLocaleString(),
trend: toTrend(platformData.clickChangeRate),
},
{
title: "전환율 (CVR)",
value: `${platformData.conversion}%`,
trend: toTrend(platformData.cvrChangeRate),
},
{
title: "광고비 대비 매출 (ROAS)",
value: `${platformData.ROAS}%`,
trend: toTrend(platformData.ROASChangeRate),
},
];
}, [platformData]);
const kpis = useMemo(
() => (platformData ? metricsToKpis(platformData) : []),
[platformData],
);

const logoInfo = PLATFORM_LOGOS[platform];

const {
data: budget,
isLoading: isBudgetLoading,
isError: isBudgetError,
} = usePlatformBudget(platform);
} = useBudget(platform);

const {
data: metricFacts,
Expand Down Expand Up @@ -133,7 +108,7 @@ export default function SinglePlatformView({
Array.from({ length: 4 }).map((_, i) => (
<div
key={i}
className="rounded-[24px] border border-surface-100/40 bg-surface-100/80 p-7 shadow-Soft backdrop-blur-sm flex flex-col gap-4"
className="rounded-3xl border border-surface-100/40 bg-surface-100/80 p-7 shadow-Soft backdrop-blur-sm flex flex-col gap-4"
>
<Skeleton className="h-4 w-16" />
<Skeleton className="h-8 w-24" />
Expand Down
35 changes: 15 additions & 20 deletions src/components/dashboard/platform/TopPerformanceList.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,42 @@
import React, { memo } from "react";
import { memo } from "react";

import type { IRoasRanking } from "@/types/dashboard/platform";
import { PLATFORM_MAP } from "@/types/dashboard/provider";
import { PLATFORM_MAP, type TProviderType } from "@/types/dashboard/provider";
import { PLATFORM_CIRCLE_LOGO_MAP } from "@/constants/dashboard/platformLogos";

import { TrendBadge } from "@/components/common/card/StatCard";

import GoogleLogo from "@/assets/logo/social-logo/circle/google-circle.svg?react";
import MetaLogo from "@/assets/logo/social-logo/circle/meta-circle.svg?react";
import NaverLogo from "@/assets/logo/social-logo/circle/naver-circle.svg?react";
function toProviderType(provider: string): TProviderType | null {
const key = provider.toUpperCase();
if (key in PLATFORM_MAP) return key as TProviderType;
return null;
}

interface ITopPerformanceListProps {
rankings: IRoasRanking[];
}

const PlatformInfo: Record<string, { name: string; logo: React.ReactNode }> = {
GOOGLE: {
name: PLATFORM_MAP.GOOGLE,
logo: <GoogleLogo className="w-8 h-8" />,
},
NAVER: { name: PLATFORM_MAP.NAVER, logo: <NaverLogo className="w-8 h-8" /> },
META: { name: PLATFORM_MAP.META, logo: <MetaLogo className="w-8 h-8" /> },
};

export const TopPerformanceList = memo(function TopPerformanceList({
rankings,
}: ITopPerformanceListProps) {
return (
<div className="flex-1 flex flex-col justify-center gap-6 w-full pt-3">
{rankings.map((item) => {
const info = PlatformInfo[item.provider] || {
name: item.provider,
logo: null,
};
const key = toProviderType(item.provider);
const Logo = key ? PLATFORM_CIRCLE_LOGO_MAP[key] : null;
const name = key ? PLATFORM_MAP[key] : item.provider;

return (
<div key={item.provider} className="flex items-center gap-4 w-full">
<div className="flex flex-1 items-center gap-4 min-w-0">
<span className="font-body1 text-text-muted w-4 shrink-0">
{item.rank}
</span>
<div className="shrink-0">{info.logo}</div>
<div className="shrink-0" aria-hidden="true">
{Logo && <Logo className="w-8 h-8" />}
</div>
Comment thread
Seojegyeong marked this conversation as resolved.
<span className="font-body1 text-text-title truncate">
{info.name}
{name}
</span>
</div>
<div className="flex items-center gap-4 shrink-0">
Expand Down
16 changes: 16 additions & 0 deletions src/constants/dashboard/platformLogos.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type React from "react";

import type { TProviderType } from "@/types/dashboard/provider";

import GoogleLogo from "@/assets/logo/social-logo/circle/google-circle.svg?react";
import MetaLogo from "@/assets/logo/social-logo/circle/meta-circle.svg?react";
import NaverLogo from "@/assets/logo/social-logo/circle/naver-circle.svg?react";

export const PLATFORM_CIRCLE_LOGO_MAP: Record<
TProviderType,
React.FC<React.SVGProps<SVGSVGElement>>
> = {
GOOGLE: GoogleLogo,
NAVER: NaverLogo,
META: MetaLogo,
};
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import type { TProviderType } from "@/types/dashboard/overview";

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

import { getBudget } from "@/api/dashboard/overview";
import useWorkspaceStore from "@/store/useWorkspaceStore";

// 예산 소진율 임계값
const WARNING_THRESHOLD = 50;
const DANGER_THRESHOLD = 75;

// 예산 소진 현황 조회
export function useOverviewBudget() {
export function useBudget(provider?: TProviderType) {
const orgId = useWorkspaceStore((s) => s.selectedOrgId);

return useCoreQuery(["overview", "budget", orgId], () => getBudget(orgId!), {
enabled: !!orgId,
const queryKey = provider
? ["platform", "budget", orgId, provider]
: ["overview", "budget", orgId];

return useCoreQuery(queryKey, () => getBudget(orgId!, provider), {
enabled: !!orgId && (provider ? !!provider : true),
select: (data) => ({
totalBudget: data.totalBudget,
spent: data.totalSpend,
Expand Down
Loading
Loading