From 000f5208df1688ae1727796ec2db8aa3c89a80bf Mon Sep 17 00:00:00 2001
From: YermIm
Date: Wed, 12 Aug 2026 15:24:52 +0900
Subject: [PATCH 1/8] =?UTF-8?q?refactor:=20=EB=8C=80=EC=8B=9C=EB=B3=B4?=
=?UTF-8?q?=EB=93=9C=20=EC=98=88=EC=82=B0=20=EC=9D=91=EB=8B=B5=20=ED=83=80?=
=?UTF-8?q?=EC=9E=85=EC=9D=84=20groups=20=EC=8A=A4=ED=8E=99=EC=9C=BC?=
=?UTF-8?q?=EB=A1=9C=20=EA=B5=90=EC=B2=B4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/types/dashboard/budget.ts | 8 ++-----
src/types/dashboard/common.ts | 40 +++++++++++++++++++++++------------
2 files changed, 29 insertions(+), 19 deletions(-)
diff --git a/src/types/dashboard/budget.ts b/src/types/dashboard/budget.ts
index 6bca3904..d443c306 100644
--- a/src/types/dashboard/budget.ts
+++ b/src/types/dashboard/budget.ts
@@ -1,9 +1,5 @@
-/** 게이지 라벨 */
-export type TBudgetGaugeLabel =
- | "전체 예산"
- | "일일 예산"
- | "Google·Meta"
- | "NAVER";
+/** 게이지 라벨 — groups.budgetType(TOTAL/DAILY) 매핑 */
+export type TBudgetGaugeLabel = "전체 예산" | "일일 예산";
/** 게이지 1개 분량 */
export interface IBudgetSlice {
diff --git a/src/types/dashboard/common.ts b/src/types/dashboard/common.ts
index f8d861a6..7f7fead9 100644
--- a/src/types/dashboard/common.ts
+++ b/src/types/dashboard/common.ts
@@ -1,3 +1,5 @@
+import type { TProviderType } from "./provider";
+
// 공통 지표 응답 (overview/platform 공유)
export interface IMetricsResponse {
clicks: number;
@@ -10,25 +12,37 @@ export interface IMetricsResponse {
ROASChangeRate: number;
}
-// 예산 금액 단위 (API 분리 응답)
+// 예산 금액 단위
export interface IBudgetAmountSlice {
totalBudget: number;
totalSpend: number;
}
-// 예산 소진 현황
-export interface IBudgetResponse {
- providerType: string;
- usagePercentage: number;
- totalBudget: number;
- totalSpend: number;
+/** 대시보드 예산 group.budgetType */
+export type TDashboardBudgetType = "TOTAL" | "DAILY";
+
+/** 대시보드 예산 group.detail */
+export interface IBudgetGroupDetail {
+ budget: number;
+ spend: number;
remainingBudget: number;
- /** 통합 대시보드 — Google·Meta / Naver 분리 */
- googleMeta?: IBudgetAmountSlice;
- naver?: IBudgetAmountSlice;
- /** 플랫폼 Google/Meta — 전체 / 일일 분리 */
- lifetime?: IBudgetAmountSlice;
- daily?: IBudgetAmountSlice;
+ /** 0~100 */
+ remainingPercentage: number;
+ estimated: boolean;
+}
+
+/** 대시보드 예산 groups[] 항목 */
+export interface IBudgetGroup {
+ budgetType: TDashboardBudgetType;
+ providers: TProviderType[];
+ detail: IBudgetGroupDetail;
+}
+
+/** GET /api/dashboard/budgets 응답 data */
+export interface IBudgetResponse {
+ /** 통합: "ALL", 플랫폼: GOOGLE | META | NAVER */
+ providerType: TProviderType | "ALL";
+ groups: IBudgetGroup[];
}
// ROAS 순위 항목
From a28b3f5bc8f1bc611e311a896b36e73f9ac74107 Mon Sep 17 00:00:00 2001
From: YermIm
Date: Wed, 12 Aug 2026 15:31:11 +0900
Subject: [PATCH 2/8] =?UTF-8?q?refactor:=20=EC=98=88=EC=82=B0=20groups=20?=
=?UTF-8?q?=EC=9D=91=EB=8B=B5=EC=9D=84=20=EA=B2=8C=EC=9D=B4=EC=A7=80=20Vie?=
=?UTF-8?q?wModel=EB=A1=9C=20=EB=A7=A4=ED=95=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/utils/dashboard/budget.ts | 88 ++++++++++++-----------------------
1 file changed, 29 insertions(+), 59 deletions(-)
diff --git a/src/utils/dashboard/budget.ts b/src/utils/dashboard/budget.ts
index 9b2cd5b8..8d233ca9 100644
--- a/src/utils/dashboard/budget.ts
+++ b/src/utils/dashboard/budget.ts
@@ -4,8 +4,9 @@ import type {
IBudgetViewModel,
} from "@/types/dashboard/budget";
import type {
- IBudgetAmountSlice,
+ IBudgetGroup,
IBudgetResponse,
+ TDashboardBudgetType,
} from "@/types/dashboard/common";
import type { TProviderType } from "@/types/dashboard/provider";
@@ -15,7 +16,12 @@ const DANGER_THRESHOLD = 75;
/** BudgetGaugeChart showInsight 기본값 */
export const SHOW_BUDGET_GAUGE_INSIGHT = true;
-/** Google/Meta 플랫폼 — 일일 예산 게이지 */
+const BUDGET_TYPE_LABEL: Record = {
+ TOTAL: "전체 예산",
+ DAILY: "일일 예산",
+};
+
+/** Google/Meta 플랫폼 — 일일 예산 게이지 (skeleton·ads용) */
export function supportsDailyBudget(provider?: TProviderType): boolean {
return provider === "GOOGLE" || provider === "META";
}
@@ -54,26 +60,22 @@ export const statusPointClasses: Record = {
위험: "bg-info-red",
};
-function toAmountSlice(
- data: IBudgetResponse,
- slice?: IBudgetAmountSlice,
-): IBudgetAmountSlice {
- return (
- slice ?? {
- totalBudget: data.totalBudget,
- totalSpend: data.totalSpend,
- }
- );
+/** Naver 플랫폼만 DAILY 숨김 (통합 ALL은 TOTAL+DAILY 그대로) */
+function filterBudgetGroups(
+ groups: IBudgetGroup[],
+ provider?: TProviderType,
+): IBudgetGroup[] {
+ if (provider === "NAVER") {
+ return groups.filter((group) => group.budgetType === "TOTAL");
+ }
+ return groups;
}
-function toBudgetSlice(
- label: IBudgetSlice["label"],
- amount: IBudgetAmountSlice,
-): IBudgetSlice {
+function toBudgetSliceFromGroup(group: IBudgetGroup): IBudgetSlice {
return {
- label,
- totalBudget: amount.totalBudget,
- spent: amount.totalSpend,
+ label: BUDGET_TYPE_LABEL[group.budgetType],
+ totalBudget: group.detail.budget,
+ spent: group.detail.spend,
};
}
@@ -90,53 +92,21 @@ function toGaugeProps(
};
}
-/** 통합: Google·Meta + Naver (전체 예산 각 1개) */
-function mapOverviewBudgetViewModel(data: IBudgetResponse): IBudgetViewModel {
- const googleMeta = toAmountSlice(data, data.googleMeta);
- const naver = data.naver ?? { totalBudget: 0, totalSpend: 0 };
-
- return {
- slices: [
- toBudgetSlice("Google·Meta", googleMeta),
- toBudgetSlice("NAVER", naver),
- ],
- };
-}
-
-/** 플랫폼: Google/Meta → 전체+일일, Naver → 전체만 */
-function mapPlatformBudgetViewModel(
- data: IBudgetResponse,
- provider: TProviderType,
-): IBudgetViewModel {
- const lifetime = toBudgetSlice(
- "전체 예산",
- toAmountSlice(data, data.lifetime),
- );
-
- if (!supportsDailyBudget(provider)) {
- return { slices: [lifetime] };
- }
-
- const daily = toBudgetSlice("일일 예산", toAmountSlice(data, data.daily));
-
- return { slices: [lifetime, daily] };
-}
-
/**
- * API → UI 변환 (API 스펙 변경 시 여기만 수정)
+ * API → UI 변환
*
- * [통합] googleMeta + naver (legacy: googleMeta만 totalBudget/totalSpend fallback)
- * [Google/Meta] lifetime + daily
- * [Naver] lifetime만
+ * [통합] groups 그대로 (TOTAL + DAILY)
+ * [Google/Meta] groups 그대로
+ * [Naver] TOTAL만
*/
export function mapBudgetResponseToViewModel(
data: IBudgetResponse,
provider?: TProviderType,
): IBudgetViewModel {
- if (provider === undefined) {
- return mapOverviewBudgetViewModel(data);
- }
- return mapPlatformBudgetViewModel(data, provider);
+ const groups = filterBudgetGroups(data.groups, provider);
+ return {
+ slices: groups.map(toBudgetSliceFromGroup),
+ };
}
/** useBudget select에서 쓸 최종 형태 */
From e3483451073195c39ad0e13e158403d96befa66d Mon Sep 17 00:00:00 2001
From: YermIm
Date: Wed, 12 Aug 2026 15:59:55 +0900
Subject: [PATCH 3/8] =?UTF-8?q?fix:=20=EC=BA=A0=ED=8E=98=EC=9D=B8=20?=
=?UTF-8?q?=EC=83=81=EC=84=B8=20platformBudgets=EB=A5=BC=20PlatformBudgetS?=
=?UTF-8?q?ummary=20=EC=8A=A4=ED=8E=99=EC=97=90=20=EB=A7=9E=EA=B2=8C=20?=
=?UTF-8?q?=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../ads/CampaignPlatformSection.tsx | 49 +++---
.../ads/EditPlatformBudgetModal.tsx | 14 +-
src/hooks/ads/useUpdatePlatformBudget.ts | 4 +-
src/pages/ads/list/CampaignDetail.tsx | 31 ++--
src/types/ads/budget.ts | 6 +-
src/types/ads/campaign.ts | 27 ++-
src/utils/ads/budgetEdit.ts | 67 ++++----
src/utils/ads/projectBudget.ts | 161 +++++++++++++-----
8 files changed, 215 insertions(+), 144 deletions(-)
diff --git a/src/components/ads/CampaignPlatformSection.tsx b/src/components/ads/CampaignPlatformSection.tsx
index bd4c798f..d7808ebf 100644
--- a/src/components/ads/CampaignPlatformSection.tsx
+++ b/src/components/ads/CampaignPlatformSection.tsx
@@ -1,13 +1,16 @@
import type { ReactNode } from "react";
import { twMerge } from "tailwind-merge";
-import type { IPlatformProjectBudget, TPlatform } from "@/types/ads/campaign";
+import type { IPlatformBudgetSummary, TPlatform } from "@/types/ads/campaign";
import {
BUDGET_EDIT_BLOCK_MESSAGES,
canSubmitPlatformBudgetEdit,
} from "@/utils/ads/budgetEdit";
-import { mapPlatformProjectBudgetToGauges } from "@/utils/ads/projectBudget";
+import {
+ mapPlatformBudgetSummariesToGauges,
+ pickEditablePlatformBudget,
+} from "@/utils/ads/projectBudget";
import PlatformBudgetItem from "@/components/ads/PlatformBudgetItem";
import Button from "@/components/common/button/Button";
@@ -30,28 +33,33 @@ const PLATFORM_LABEL: Record = {
interface ICampaignPlatformSectionProps {
platform: TPlatform;
- platformBudget?: IPlatformProjectBudget;
- onEditBudget?: () => void;
+ platformBudgets?: IPlatformBudgetSummary[];
+ onEditBudget?: (budget: IPlatformBudgetSummary) => void;
}
export default function CampaignPlatformSection({
platform,
- platformBudget,
+ platformBudgets = [],
onEditBudget,
}: ICampaignPlatformSectionProps) {
- const gauges = platformBudget
- ? mapPlatformProjectBudgetToGauges(platformBudget)
- : [];
+ const gauges = mapPlatformBudgetSummariesToGauges(platformBudgets);
+ const editTarget = pickEditablePlatformBudget(platformBudgets);
- const editCheck = canSubmitPlatformBudgetEdit(platformBudget);
- const isBudgetEditDisabled = !onEditBudget || !editCheck.ok;
+ const editCheck = canSubmitPlatformBudgetEdit(editTarget ?? undefined);
+ const isBudgetEditDisabled = !onEditBudget || !editTarget || !editCheck.ok;
const budgetEditDisabledReason = !onEditBudget
? undefined
- : editCheck.ok
- ? undefined
- : BUDGET_EDIT_BLOCK_MESSAGES[editCheck.reason];
+ : !editTarget
+ ? BUDGET_EDIT_BLOCK_MESSAGES.MISSING_PLATFORM_BUDGET
+ : editCheck.ok
+ ? undefined
+ : BUDGET_EDIT_BLOCK_MESSAGES[editCheck.reason];
const budgetEditHintId = `budget-edit-hint-${platform}`;
+ const campaignName = platformBudgets.find(
+ (row) => row.adCampaignName,
+ )?.adCampaignName;
+
const budgetEditAction = (
@@ -115,12 +126,12 @@ export default function CampaignPlatformSection({
예산 정보가 없습니다.
) : (
-
- {gauges.map((gauge) => (
+
+ {gauges.map((gauge, index) => (
))}
diff --git a/src/components/ads/EditPlatformBudgetModal.tsx b/src/components/ads/EditPlatformBudgetModal.tsx
index 28a72a7a..dd64b3d5 100644
--- a/src/components/ads/EditPlatformBudgetModal.tsx
+++ b/src/components/ads/EditPlatformBudgetModal.tsx
@@ -8,7 +8,7 @@ import {
import { zodResolver } from "@hookform/resolvers/zod";
import { toast } from "sonner";
-import type { IPlatformProjectBudget } from "@/types/ads/campaign";
+import type { IPlatformBudgetSummary } from "@/types/ads/campaign";
import {
buildUpdatePlatformBudgetVariables,
@@ -89,7 +89,7 @@ const BudgetAmountInput = forwardRef
(
},
);
-const PROVIDER_LABEL: Record = {
+const PROVIDER_LABEL: Record = {
META: "Meta",
GOOGLE: "Google",
NAVER: "NAVER",
@@ -99,7 +99,7 @@ interface IEditPlatformBudgetModalProps {
isOpen: boolean;
onClose: () => void;
onClosed?: () => void;
- budget: IPlatformProjectBudget | null;
+ budget: IPlatformBudgetSummary | null;
orgId: number;
projectId: number;
}
@@ -112,7 +112,7 @@ export default function EditPlatformBudgetModal({
orgId,
projectId,
}: IEditPlatformBudgetModalProps) {
- const budgetRef = useRef(null);
+ const budgetRef = useRef(null);
if (budget) budgetRef.current = budget;
const activeBudget = budget ?? budgetRef.current;
@@ -184,12 +184,12 @@ export default function EditPlatformBudgetModal({
onExitComplete={onClosed}
size="md"
padding="lg"
- title={`${PROVIDER_LABEL[activeBudget.providerType]} 예산 수정`}
+ title={`${PROVIDER_LABEL[activeBudget.provider]} 예산 수정`}
disableOverlayClick={isPending}
>
- {PROVIDER_LABEL[activeBudget.providerType]} 예산 수정
+ {PROVIDER_LABEL[activeBudget.provider]} 예산 수정
{activeBudget.adCampaignName ? (
@@ -201,7 +201,7 @@ export default function EditPlatformBudgetModal({