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({

("all"); const [budgetEditTarget, setBudgetEditTarget] = - useState(null); + useState(null); const [isBudgetEditOpen, setIsBudgetEditOpen] = useState(false); const clearAdSelection = useCallback(() => { @@ -193,13 +194,10 @@ export default function CampaignDetail() { [data?.providers, data?.platformBudgets], ); - const budgetByPlatform = useMemo(() => { - const map = new Map(); - for (const budget of platformBudgets) { - map.set(providerTypeToPlatform(budget.providerType), budget); - } - return map; - }, [platformBudgets]); + const budgetsByPlatform = useMemo( + () => groupPlatformBudgetsByPlatform(platformBudgets), + [platformBudgets], + ); const platformSections = useMemo(() => { if (!data) return []; @@ -207,9 +205,9 @@ export default function CampaignDetail() { const fromAds = groupAdsByPlatform(adsList, data.providers); const seen = new Set(fromAds.map((section) => section.platform)); - // 광고 0개여도 예산 mock/API 있으면 섹션 표시 + // 광고 0개여도 예산 API 있으면 섹션 표시 for (const budget of platformBudgets) { - const platform = providerTypeToPlatform(budget.providerType); + const platform = providerTypeToPlatform(budget.provider); if (!seen.has(platform)) { fromAds.push({ platform, ads: [] }); seen.add(platform); @@ -365,13 +363,10 @@ export default function CampaignDetail() { > { - const budget = budgetByPlatform.get(platform); - if (budget) { - setBudgetEditTarget(budget); - setIsBudgetEditOpen(true); - } + platformBudgets={budgetsByPlatform.get(platform)} + onEditBudget={(budget) => { + setBudgetEditTarget(budget); + setIsBudgetEditOpen(true); }} /> {platformAds.length > 0 ? ( diff --git a/src/types/ads/budget.ts b/src/types/ads/budget.ts index 4019609b..c3aa8880 100644 --- a/src/types/ads/budget.ts +++ b/src/types/ads/budget.ts @@ -19,10 +19,10 @@ export interface INaverBudgetUpdateRequest { dailyBudget: number; } -/** 일일 / 총 예산 구분 — 상세 API·수정 응답 공통 */ -export type TPlatformBudgetType = "DAILY" | "LIFETIME"; +/** 일일 / 총 예산 구분 — 상세·대시보드·수정 응답 공통 (OpenAPI: DAILY | TOTAL) */ +export type TPlatformBudgetType = "DAILY" | "TOTAL"; -/** Meta 상세 platformBudgets.activeBudgetType */ +/** @deprecated TPlatformBudgetType 사용 */ export type TMetaActiveBudgetType = TPlatformBudgetType; /** diff --git a/src/types/ads/campaign.ts b/src/types/ads/campaign.ts index c35bb128..ac1a3242 100644 --- a/src/types/ads/campaign.ts +++ b/src/types/ads/campaign.ts @@ -1,26 +1,25 @@ import type { TPlatformBudgetType } from "@/types/ads/budget"; -import type { IBudgetAmountSlice } from "@/types/dashboard/common"; export type TPlatform = "meta" | "google" | "naver"; //UI export type TProvider = "META" | "GOOGLE" | "NAVER"; //API export type TStatus = "ON_GOING" | "PAUSED" | "OVER"; -/** project 상세 — 플랫폼(매체 캠페인) 단위 예산 */ -export interface IPlatformProjectBudget { - providerType: TProvider; - /** Meta / Google 예산 수정 path param */ +/** + * GET /api/project/{orgId}/{projectId} — platformBudgets[] 항목 + * OpenAPI: PlatformBudgetSummary + */ +export interface IPlatformBudgetSummary { + provider: TProvider; + budgetType: TPlatformBudgetType; + budget: number; + spend: number; + remainingBudget: number; + remainingPercentage: number; + /** 예산 수정용 — 현재 OpenAPI 미포함, 내려오면 수정 활성화 */ adCampaignId?: number; adCampaignName?: string; - lifetime: IBudgetAmountSlice; - /** Meta / Google — activeBudgetType에 따라 표시 - * Naver — 일일 예산 */ - daily?: IBudgetAmountSlice | null; - /** Meta / Google — 기존 daily / lifetime 중 어떤 유형인지 */ - activeBudgetType?: TPlatformBudgetType; - /** Naver 일일 예산 수정 — /api/naver/{connectionId}/campaigns/{campaignId}/budget */ naverConnectionId?: number; naverCampaignId?: string; - /** 소유자 등 수정 가능 여부 */ canEditBudget?: boolean; } @@ -60,7 +59,7 @@ export interface ICampaignDetail extends ICampaign { budget: number; createdAt: string; ads: IAd[]; - platformBudgets?: IPlatformProjectBudget[]; + platformBudgets?: IPlatformBudgetSummary[]; } export interface IPlatformCampaign { diff --git a/src/utils/ads/budgetEdit.ts b/src/utils/ads/budgetEdit.ts index 9f6a27e6..b0ef8859 100644 --- a/src/utils/ads/budgetEdit.ts +++ b/src/utils/ads/budgetEdit.ts @@ -5,7 +5,7 @@ import type { TMetaGoogleBudgetUpdateRequest, TPlatformBudgetType, } from "@/types/ads/budget"; -import type { IPlatformProjectBudget } from "@/types/ads/campaign"; +import type { IPlatformBudgetSummary } from "@/types/ads/campaign"; /** 예산 수정 불가 사유 — canSubmitPlatformBudgetEdit 반환값 */ export type TBudgetEditBlockReason = @@ -36,7 +36,7 @@ export const dailyBudgetFormSchema = z.object({ dailyBudget: positiveBudget, }); -/** Meta / Google LIFETIME */ +/** Meta / Google TOTAL — 요청 body 필드명은 lifetimeBudget */ export const lifetimeBudgetFormSchema = z.object({ lifetimeBudget: positiveBudget, }); @@ -72,51 +72,47 @@ export interface IEffectivePlatformBudget { } /** - * activeBudgetType + daily/lifetime 데이터 존재 여부로 실제 활성 예산 결정 - * — DAILY인데 daily 없으면 LIFETIME fallback + * PlatformBudgetSummary row → 수정/표시용 effective 예산 + * — TOTAL 요청 body는 Meta/Google lifetimeBudget 필드 유지 */ export function resolveEffectivePlatformBudget( - budget: IPlatformProjectBudget, + budget: IPlatformBudgetSummary, ): IEffectivePlatformBudget { - if (budget.providerType === "NAVER") { - const daily = budget.daily ?? { totalBudget: 0, totalSpend: 0 }; + if (budget.provider === "NAVER") { return { - activeBudgetType: "DAILY", + activeBudgetType: budget.budgetType, fieldName: "dailyBudget", - label: "일일 예산", - totalBudget: daily.totalBudget, - totalSpend: daily.totalSpend, + label: budget.budgetType === "TOTAL" ? "전체 예산" : "일일 예산", + totalBudget: budget.budget, + totalSpend: budget.spend, }; } - const declaredType = - budget.activeBudgetType ?? (budget.daily ? "DAILY" : "LIFETIME"); - - if (declaredType === "DAILY" && budget.daily) { + if (budget.budgetType === "DAILY") { return { activeBudgetType: "DAILY", fieldName: "dailyBudget", label: "일일 예산", - totalBudget: budget.daily.totalBudget, - totalSpend: budget.daily.totalSpend, + totalBudget: budget.budget, + totalSpend: budget.spend, }; } return { - activeBudgetType: "LIFETIME", + activeBudgetType: "TOTAL", fieldName: "lifetimeBudget", label: "전체 예산", - totalBudget: budget.lifetime.totalBudget, - totalSpend: budget.lifetime.totalSpend, + totalBudget: budget.budget, + totalSpend: budget.spend, }; } /** * platformBudget → API 호출 가능 여부 - * BE 필드 없거나 mock이면 ok: false → 수정 버튼 disabled + * OpenAPI PlatformBudgetSummary에 수정용 ID가 없으면 disabled */ export function canSubmitPlatformBudgetEdit( - budget: IPlatformProjectBudget | undefined, + budget: IPlatformBudgetSummary | undefined, ): { ok: true } | { ok: false; reason: TBudgetEditBlockReason } { if (!budget) { return { ok: false, reason: "MISSING_PLATFORM_BUDGET" }; @@ -126,15 +122,15 @@ export function canSubmitPlatformBudgetEdit( return { ok: false, reason: "NOT_EDITABLE" }; } - switch (budget.providerType) { + switch (budget.provider) { case "META": - if (!budget.adCampaignId || !budget.activeBudgetType) { + if (!budget.adCampaignId) { return { ok: false, reason: "MISSING_META_CONTEXT" }; } return { ok: true }; case "GOOGLE": - if (!budget.adCampaignId || !budget.activeBudgetType) { + if (!budget.adCampaignId) { return { ok: false, reason: "MISSING_GOOGLE_CONTEXT" }; } return { ok: true }; @@ -143,9 +139,6 @@ export function canSubmitPlatformBudgetEdit( if (!budget.naverConnectionId || !budget.naverCampaignId) { return { ok: false, reason: "MISSING_NAVER_CONTEXT" }; } - if (!budget.daily) { - return { ok: false, reason: "MISSING_PLATFORM_BUDGET" }; - } return { ok: true }; default: @@ -153,7 +146,7 @@ export function canSubmitPlatformBudgetEdit( } } -/** Meta / Google — activeBudgetType에 맞는 필드 하나만 body에 */ +/** Meta / Google — budgetType에 맞는 필드 하나만 body에 */ export function buildMetaGoogleBudgetPayload( activeBudgetType: TPlatformBudgetType, values: { dailyBudget?: number; lifetimeBudget?: number }, @@ -179,10 +172,10 @@ export function buildNaverBudgetPayload( } /** - * 모달 — provider + activeBudgetType에 맞는 zod schema + * 모달 — provider + budgetType에 맞는 zod schema */ -export function resolveBudgetEditFormSchema(budget: IPlatformProjectBudget) { - if (budget.providerType === "NAVER") { +export function resolveBudgetEditFormSchema(budget: IPlatformBudgetSummary) { + if (budget.provider === "NAVER") { return naverBudgetFormSchema; } @@ -193,7 +186,7 @@ export function resolveBudgetEditFormSchema(budget: IPlatformProjectBudget) { } /** 모달 input — 필드명·라벨 (게이지 라벨과 동일) */ -export function resolveBudgetEditFieldMeta(budget: IPlatformProjectBudget): { +export function resolveBudgetEditFieldMeta(budget: IPlatformBudgetSummary): { fieldName: "dailyBudget" | "lifetimeBudget"; label: string; } { @@ -203,7 +196,7 @@ export function resolveBudgetEditFieldMeta(budget: IPlatformProjectBudget): { /** platformBudget → 폼 defaultValues */ export function resolveBudgetEditDefaultValues( - budget: IPlatformProjectBudget, + budget: IPlatformBudgetSummary, ): TBudgetEditModalFormValues { const { fieldName, totalBudget } = resolveEffectivePlatformBudget(budget); @@ -216,10 +209,10 @@ export function resolveBudgetEditDefaultValues( /** budget + form values → mutation variables */ export function buildUpdatePlatformBudgetVariables( - budget: IPlatformProjectBudget, + budget: IPlatformBudgetSummary, values: TBudgetEditModalFormValues, ): { - providerType: IPlatformProjectBudget["providerType"]; + providerType: IPlatformBudgetSummary["provider"]; adCampaignId?: number; activeBudgetType?: TPlatformBudgetType; naverConnectionId?: number; @@ -230,7 +223,7 @@ export function buildUpdatePlatformBudgetVariables( const { activeBudgetType } = resolveEffectivePlatformBudget(budget); const base = { - providerType: budget.providerType, + providerType: budget.provider, adCampaignId: budget.adCampaignId, activeBudgetType, naverConnectionId: budget.naverConnectionId, diff --git a/src/utils/ads/projectBudget.ts b/src/utils/ads/projectBudget.ts index b5e87542..42af4e25 100644 --- a/src/utils/ads/projectBudget.ts +++ b/src/utils/ads/projectBudget.ts @@ -1,16 +1,24 @@ +import type { TPlatformBudgetType } from "@/types/ads/budget"; import type { - IPlatformProjectBudget, + IPlatformBudgetSummary, TPlatform, TProvider, } from "@/types/ads/campaign"; import type { IBudgetGaugeProps } from "@/types/dashboard/budget"; -import { resolveEffectivePlatformBudget } from "@/utils/ads/budgetEdit"; import { buildBudgetGaugesFromSlices, supportsDailyBudget, } from "@/utils/dashboard/budget"; +const BUDGET_TYPE_LABEL: Record< + TPlatformBudgetType, + "전체 예산" | "일일 예산" +> = { + TOTAL: "전체 예산", + DAILY: "일일 예산", +}; + export function providerTypeToPlatform( provider: TProvider | string, ): TPlatform { @@ -25,67 +33,132 @@ export function platformToProviderType(platform: TPlatform): TProvider { return platform.toUpperCase() as TProvider; } -/** project detail — 플랫폼별 BudgetGaugeChart props */ -export function mapPlatformProjectBudgetToGauges( - budget: IPlatformProjectBudget, +/** + * 플랫폼별 표시할 budget rows + * — Naver: TOTAL 우선, TOTAL 없고 DAILY만 있으면 DAILY 표시 + */ +export function filterPlatformBudgetSummariesForDisplay( + summaries: IPlatformBudgetSummary[], +): IPlatformBudgetSummary[] { + const hasNaverTotal = summaries.some( + (row) => row.provider === "NAVER" && row.budgetType === "TOTAL", + ); + + return summaries.filter((row) => { + if (row.provider !== "NAVER") return true; + if (row.budgetType === "TOTAL") return true; + // TOTAL이 있을 때만 DAILY 숨김 (둘 다 오면 전체만) + if (row.budgetType === "DAILY") return !hasNaverTotal; + return true; + }); +} + +/** project detail — 플랫폼별 게이지 props */ +export function mapPlatformBudgetSummariesToGauges( + summaries: IPlatformBudgetSummary[], ): IBudgetGaugeProps[] { - const effective = resolveEffectivePlatformBudget(budget); + const visible = filterPlatformBudgetSummariesForDisplay(summaries); return buildBudgetGaugesFromSlices( - [ - { - label: effective.label, - totalBudget: effective.totalBudget, - spent: effective.totalSpend, - }, - ], + visible.map((row) => ({ + label: BUDGET_TYPE_LABEL[row.budgetType], + totalBudget: row.budget, + spent: row.spend, + })), { showInsight: true }, ); } -/** API 미준비 시 dev placeholder */ +/** 수정 모달용 — 플랫폼에서 대표 row 선택 */ +export function pickEditablePlatformBudget( + summaries: IPlatformBudgetSummary[], +): IPlatformBudgetSummary | null { + if (summaries.length === 0) return null; + + const provider = summaries[0]?.provider; + if (provider === "NAVER") { + return ( + summaries.find((row) => row.budgetType === "TOTAL") ?? + summaries[0] ?? + null + ); + } + + return ( + summaries.find((row) => row.budgetType === "DAILY") ?? + summaries.find((row) => row.budgetType === "TOTAL") ?? + summaries[0] ?? + null + ); +} + +/** API 미준비 시(platformBudgets 필드 자체 없음) dev placeholder */ export function buildPlaceholderPlatformBudgets( providers: TPlatform[], -): IPlatformProjectBudget[] { - return providers.map((platform, index) => { - const providerType = platformToProviderType(platform); +): IPlatformBudgetSummary[] { + const rows: IPlatformBudgetSummary[] = []; + + providers.forEach((platform, index) => { + const provider = platformToProviderType(platform); const totalBudget = 1_000_000 + index * 200_000; const totalSpend = Math.round(totalBudget * (0.2 + index * 0.15)); - const item: IPlatformProjectBudget = { - providerType, + rows.push({ + provider, + budgetType: "TOTAL", + budget: totalBudget, + spend: totalSpend, + remainingBudget: Math.max(0, totalBudget - totalSpend), + remainingPercentage: + totalBudget > 0 + ? Math.round(((totalBudget - totalSpend) / totalBudget) * 100) + : 100, adCampaignId: 1000 + index, - adCampaignName: `${providerType} 매체 캠페인 (mock)`, - lifetime: { totalBudget, totalSpend }, - }; - - if (providerType === "NAVER") { - item.daily = { - totalBudget: 50_000, - totalSpend: 12_000 + index * 2_000, - }; - item.naverConnectionId = 1; - item.naverCampaignId = `mock-campaign-${index}`; - item.canEditBudget = true; - } else if (supportsDailyBudget(providerType)) { - item.daily = { - totalBudget: 50_000, - totalSpend: 12_000 + index * 2_000, - }; - item.activeBudgetType = index % 2 === 0 ? "DAILY" : "LIFETIME"; - item.canEditBudget = true; - } + adCampaignName: `${provider} 매체 캠페인 (mock)`, + canEditBudget: false, + }); - return item; + if (supportsDailyBudget(provider)) { + const dailyBudget = 50_000; + const dailySpend = 12_000 + index * 2_000; + rows.push({ + provider, + budgetType: "DAILY", + budget: dailyBudget, + spend: dailySpend, + remainingBudget: Math.max(0, dailyBudget - dailySpend), + remainingPercentage: Math.round( + ((dailyBudget - dailySpend) / dailyBudget) * 100, + ), + adCampaignId: 1000 + index, + adCampaignName: `${provider} 매체 캠페인 (mock)`, + canEditBudget: false, + }); + } }); + + return rows; } -/** API 응답 + fallback (platformBudgets 없으면 mock) */ +/** API 응답 우선 — 필드가 있으면(빈 배열 포함) 그대로 사용 */ export function resolvePlatformBudgets(input: { providers: TPlatform[]; - platformBudgets?: IPlatformProjectBudget[]; -}): IPlatformProjectBudget[] { - if (input.platformBudgets?.length) return input.platformBudgets; + platformBudgets?: IPlatformBudgetSummary[]; +}): IPlatformBudgetSummary[] { + if (input.platformBudgets !== undefined) return input.platformBudgets; if (input.providers.length === 0) return []; return buildPlaceholderPlatformBudgets(input.providers); } + +export function groupPlatformBudgetsByPlatform( + budgets: IPlatformBudgetSummary[], +): Map { + const map = new Map(); + for (const budget of budgets) { + const platform = providerTypeToPlatform(budget.provider); + const list = map.get(platform) ?? []; + list.push(budget); + map.set(platform, list); + } + return map; +} From 03f60e57473aa8febf95ab59b7805119fbf7fd14 Mon Sep 17 00:00:00 2001 From: YermIm Date: Thu, 13 Aug 2026 02:18:16 +0900 Subject: [PATCH 4/8] =?UTF-8?q?refactor:=20=EC=98=88=EC=82=B0=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=EC=9A=A9=20ID=20=ED=95=84=EB=93=9C=EB=A5=BC=20?= =?UTF-8?q?=EC=8B=A4=20API=20=EC=8A=A4=ED=8E=99=EC=97=90=20=EB=A7=9E?= =?UTF-8?q?=EA=B2=8C=20=EB=A7=A4=ED=95=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/types/ads/campaign.ts | 13 +++++++++---- src/utils/ads/budgetEdit.ts | 13 ++++++++----- src/utils/ads/projectBudget.ts | 9 ++++++++- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/types/ads/campaign.ts b/src/types/ads/campaign.ts index ac1a3242..449df11c 100644 --- a/src/types/ads/campaign.ts +++ b/src/types/ads/campaign.ts @@ -8,6 +8,11 @@ export type TStatus = "ON_GOING" | "PAUSED" | "OVER"; * GET /api/project/{orgId}/{projectId} — platformBudgets[] 항목 * OpenAPI: PlatformBudgetSummary */ +export interface INaverBudgetTarget { + connectionId: number; + campaignId: string; +} + export interface IPlatformBudgetSummary { provider: TProvider; budgetType: TPlatformBudgetType; @@ -15,11 +20,11 @@ export interface IPlatformBudgetSummary { spend: number; remainingBudget: number; remainingPercentage: number; - /** 예산 수정용 — 현재 OpenAPI 미포함, 내려오면 수정 활성화 */ - adCampaignId?: number; + /** Meta / Google 예산 수정 path */ + adCampaignId?: number | null; adCampaignName?: string; - naverConnectionId?: number; - naverCampaignId?: string; + /** Naver 예산 수정 path */ + naverBudgetTarget?: INaverBudgetTarget | null; canEditBudget?: boolean; } diff --git a/src/utils/ads/budgetEdit.ts b/src/utils/ads/budgetEdit.ts index b0ef8859..f459e560 100644 --- a/src/utils/ads/budgetEdit.ts +++ b/src/utils/ads/budgetEdit.ts @@ -135,11 +135,13 @@ export function canSubmitPlatformBudgetEdit( } return { ok: true }; - case "NAVER": - if (!budget.naverConnectionId || !budget.naverCampaignId) { + case "NAVER": { + const target = budget.naverBudgetTarget; + if (!target?.connectionId || !target.campaignId) { return { ok: false, reason: "MISSING_NAVER_CONTEXT" }; } return { ok: true }; + } default: return { ok: false, reason: "MISSING_PLATFORM_BUDGET" }; @@ -221,13 +223,14 @@ export function buildUpdatePlatformBudgetVariables( lifetimeBudget?: number; } { const { activeBudgetType } = resolveEffectivePlatformBudget(budget); + const naverTarget = budget.naverBudgetTarget; const base = { providerType: budget.provider, - adCampaignId: budget.adCampaignId, + adCampaignId: budget.adCampaignId ?? undefined, activeBudgetType, - naverConnectionId: budget.naverConnectionId, - naverCampaignId: budget.naverCampaignId, + naverConnectionId: naverTarget?.connectionId, + naverCampaignId: naverTarget?.campaignId, }; if (values.lifetimeBudget !== undefined) { diff --git a/src/utils/ads/projectBudget.ts b/src/utils/ads/projectBudget.ts index 42af4e25..855fa436 100644 --- a/src/utils/ads/projectBudget.ts +++ b/src/utils/ads/projectBudget.ts @@ -113,7 +113,14 @@ export function buildPlaceholderPlatformBudgets( totalBudget > 0 ? Math.round(((totalBudget - totalSpend) / totalBudget) * 100) : 100, - adCampaignId: 1000 + index, + ...(provider === "NAVER" + ? { + naverBudgetTarget: { + connectionId: 1, + campaignId: `mock-campaign-${index}`, + }, + } + : { adCampaignId: 1000 + index }), adCampaignName: `${provider} 매체 캠페인 (mock)`, canEditBudget: false, }); From 6a9693adb2296e2b2c8c1559ed716cca30d3c24e Mon Sep 17 00:00:00 2001 From: JAESEON PARK Date: Thu, 13 Aug 2026 02:27:27 +0900 Subject: [PATCH 5/8] =?UTF-8?q?fix:=20discord=20webhook=20URL=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/setting/useSettingNotifications.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/hooks/setting/useSettingNotifications.ts b/src/hooks/setting/useSettingNotifications.ts index f532a6ec..6437820d 100644 --- a/src/hooks/setting/useSettingNotifications.ts +++ b/src/hooks/setting/useSettingNotifications.ts @@ -163,11 +163,21 @@ export default function useSettingNotifications() { setDiscordWebhookError("Webhook URL을 입력해주세요"); return; } - if (!url.startsWith("https://")) { + let parsed: URL; + try { + parsed = new URL(url); + } catch { setDiscordWebhookError("올바른 URL 형식으로 입력해주세요"); return; } - + const DISCORD_HOSTS = ["discord.com", "discordapp.com"]; + if ( + parsed.protocol !== "https:" || + !DISCORD_HOSTS.includes(parsed.hostname) + ) { + setDiscordWebhookError("디스코드 Webhook URL을 입력해주세요"); + return; + } setPendingOrgAction("discord"); try { await updateOrg.mutateAsync( From 7ab5ecc9b2ae58ffe3d3b1f2ec1384d910794309 Mon Sep 17 00:00:00 2001 From: YermIm Date: Thu, 13 Aug 2026 02:31:19 +0900 Subject: [PATCH 6/8] =?UTF-8?q?refactor:=20=EC=98=88=EC=82=B0=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B6=88=EA=B0=80=20=EC=82=AC=EC=9C=A0=20=EB=AC=B8?= =?UTF-8?q?=EA=B5=AC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ads/CampaignPlatformSection.tsx | 58 ++++++------------- 1 file changed, 17 insertions(+), 41 deletions(-) diff --git a/src/components/ads/CampaignPlatformSection.tsx b/src/components/ads/CampaignPlatformSection.tsx index d7808ebf..d76b69ae 100644 --- a/src/components/ads/CampaignPlatformSection.tsx +++ b/src/components/ads/CampaignPlatformSection.tsx @@ -3,10 +3,7 @@ import { twMerge } from "tailwind-merge"; import type { IPlatformBudgetSummary, TPlatform } from "@/types/ads/campaign"; -import { - BUDGET_EDIT_BLOCK_MESSAGES, - canSubmitPlatformBudgetEdit, -} from "@/utils/ads/budgetEdit"; +import { canSubmitPlatformBudgetEdit } from "@/utils/ads/budgetEdit"; import { mapPlatformBudgetSummariesToGauges, pickEditablePlatformBudget, @@ -47,49 +44,28 @@ export default function CampaignPlatformSection({ const editCheck = canSubmitPlatformBudgetEdit(editTarget ?? undefined); const isBudgetEditDisabled = !onEditBudget || !editTarget || !editCheck.ok; - const budgetEditDisabledReason = !onEditBudget - ? undefined - : !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 = ( -
- - {budgetEditDisabledReason ? ( -

- {budgetEditDisabledReason} -

- ) : null} -
+ ); return ( From f78903e679db0335a914451a681e60ad9455ce23 Mon Sep 17 00:00:00 2001 From: JAESEON PARK Date: Thu, 13 Aug 2026 02:50:55 +0900 Subject: [PATCH 7/8] =?UTF-8?q?fix:=20org=20=EC=A0=80=EC=9E=A5=EC=8B=9C=20?= =?UTF-8?q?alert=20draft=20=EA=B0=92=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/setting/useSettingNotifications.ts | 69 +++++++++++++------- src/hooks/setting/useSettingSave.ts | 22 ++++--- 2 files changed, 61 insertions(+), 30 deletions(-) diff --git a/src/hooks/setting/useSettingNotifications.ts b/src/hooks/setting/useSettingNotifications.ts index f532a6ec..620aac6f 100644 --- a/src/hooks/setting/useSettingNotifications.ts +++ b/src/hooks/setting/useSettingNotifications.ts @@ -86,6 +86,7 @@ export default function useSettingNotifications() { (isNotificationLoading || isNotificationRefetching); const buildOrgBody = ( + alerts: { alertClicks: boolean; alertReport: boolean }, overrides: Partial = {}, ): IUpdateOrgNotificationSettingsRequest => ({ isSlackEnabled: savedOrgNotif.slackEnabled, @@ -94,8 +95,8 @@ export default function useSettingNotifications() { isDiscordEnabled: savedOrgNotif.discordEnabled, discordWebhookUrl: "", disconnectDiscord: false, - alertClicks: savedWorkspaceNotif.clickAlarm ?? false, - alertReport: savedWorkspaceNotif.weeklyReport ?? false, + alertClicks: alerts.alertClicks, + alertReport: alerts.alertReport, ...overrides, }); @@ -121,11 +122,17 @@ export default function useSettingNotifications() { setPendingOrgAction("slack"); try { await updateOrg.mutateAsync( - buildOrgBody({ - isSlackEnabled: true, - slackWebhookUrl: url, - disconnectSlack: false, - }), + buildOrgBody( + { + alertClicks: draftWorkspaceNotif.clickAlarm, + alertReport: draftWorkspaceNotif.weeklyReport, + }, + { + isSlackEnabled: true, + slackWebhookUrl: url, + disconnectSlack: false, + }, + ), ); toast.success("슬랙이 연동되었습니다"); setSlackWebhookUrl(""); @@ -142,11 +149,17 @@ export default function useSettingNotifications() { setPendingOrgAction("slack"); try { await updateOrg.mutateAsync( - buildOrgBody({ - isSlackEnabled: false, - slackWebhookUrl: "", - disconnectSlack: true, - }), + buildOrgBody( + { + alertClicks: draftWorkspaceNotif.clickAlarm, + alertReport: draftWorkspaceNotif.weeklyReport, + }, + { + isSlackEnabled: false, + slackWebhookUrl: "", + disconnectSlack: true, + }, + ), ); toast.success("슬랙 연동이 해제되었습니다"); } catch (e) { @@ -171,11 +184,17 @@ export default function useSettingNotifications() { setPendingOrgAction("discord"); try { await updateOrg.mutateAsync( - buildOrgBody({ - isDiscordEnabled: true, - discordWebhookUrl: url, - disconnectDiscord: false, - }), + buildOrgBody( + { + alertClicks: draftWorkspaceNotif.clickAlarm, + alertReport: draftWorkspaceNotif.weeklyReport, + }, + { + isDiscordEnabled: true, + discordWebhookUrl: url, + disconnectDiscord: false, + }, + ), ); toast.success("디스코드가 연동되었습니다"); setDiscordWebhookUrl(""); @@ -192,11 +211,17 @@ export default function useSettingNotifications() { setPendingOrgAction("discord"); try { await updateOrg.mutateAsync( - buildOrgBody({ - isDiscordEnabled: false, - discordWebhookUrl: "", - disconnectDiscord: true, - }), + buildOrgBody( + { + alertClicks: draftWorkspaceNotif.clickAlarm, + alertReport: draftWorkspaceNotif.weeklyReport, + }, + { + isDiscordEnabled: false, + discordWebhookUrl: "", + disconnectDiscord: true, + }, + ), ); toast.success("디스코드 연동이 해제되었습니다"); } catch (e) { diff --git a/src/hooks/setting/useSettingSave.ts b/src/hooks/setting/useSettingSave.ts index a1fa0ca3..f661fa43 100644 --- a/src/hooks/setting/useSettingSave.ts +++ b/src/hooks/setting/useSettingSave.ts @@ -131,14 +131,20 @@ export default function useSettingSave({ } if (shouldSaveOrg) { await notifications.updateOrg.mutateAsync( - notifications.buildOrgBody({ - isSlackEnabled: notifications.draftOrgNotif.slackEnabled, - slackWebhookUrl: "", - disconnectSlack: false, - isDiscordEnabled: notifications.draftOrgNotif.discordEnabled, - discordWebhookUrl: "", - disconnectDiscord: false, - }), + notifications.buildOrgBody( + { + alertClicks: notifications.draftWorkspaceNotif.clickAlarm, + alertReport: notifications.draftWorkspaceNotif.weeklyReport, + }, + { + isSlackEnabled: notifications.draftOrgNotif.slackEnabled, + slackWebhookUrl: "", + disconnectSlack: false, + isDiscordEnabled: notifications.draftOrgNotif.discordEnabled, + discordWebhookUrl: "", + disconnectDiscord: false, + }, + ), ); notifications.setSavedOrgNotif((prev) => ({ ...prev, From 7e96534abfe3b226bcc412d743c70910f3d3073f Mon Sep 17 00:00:00 2001 From: YermIm Date: Thu, 13 Aug 2026 02:58:54 +0900 Subject: [PATCH 8/8] =?UTF-8?q?refactor:=20=EC=BA=A0=ED=8E=98=EC=9D=B8=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=EC=97=90=EC=84=9C=20=EC=98=88=EC=82=B0=20?= =?UTF-8?q?=EC=86=8C=EC=A7=84=20=EC=97=B4=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ads/CampaignRow.tsx | 21 ++++----------------- src/components/ads/CampaignTable.tsx | 16 ++-------------- src/components/ads/skeleton/AdsSkeleton.tsx | 13 ++----------- 3 files changed, 8 insertions(+), 42 deletions(-) diff --git a/src/components/ads/CampaignRow.tsx b/src/components/ads/CampaignRow.tsx index e02b37b0..7fd0033a 100644 --- a/src/components/ads/CampaignRow.tsx +++ b/src/components/ads/CampaignRow.tsx @@ -3,8 +3,6 @@ import { twMerge } from "tailwind-merge"; import type { TPlatform, TStatus } from "@/types/ads/campaign"; -import ProgressBar from "../common/progressbar/ProgressBar"; - 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"; @@ -14,7 +12,6 @@ interface ICampaignRowProps { name: string; providers: TPlatform[]; status: TStatus; - budgetUsageRate: number; isSelected: boolean; onToggleSelect: () => void; onClick?: () => void; @@ -22,11 +19,7 @@ interface ICampaignRowProps { /** 헤더·행·스켈레톤 플랫폼 열 — 모바일에서 로고 최대 3개 폭 예약 */ export const CAMPAIGN_PLATFORM_COL_CLASS = - "mr-24 w-28 shrink-0 tablet:mr-20 tablet:w-24 mobile:mr-2 mobile:w-16"; - -/** 헤더·행·스켈레톤 예산 열 */ -export const CAMPAIGN_BUDGET_COL_CLASS = - "w-[28%] shrink-0 tablet:w-[26%] mobile:w-[30%] mobile:max-w-28"; + "w-28 shrink-0 tablet:w-24 mobile:w-16"; const LogoMap: Record = { meta: ( @@ -44,7 +37,6 @@ export default function CampaignRow({ name, providers, status, - budgetUsageRate, isSelected, onToggleSelect, onClick, @@ -99,11 +91,12 @@ export default function CampaignRow({
{providers && providers.length > 0 ? ( -
+
{providers.map((p, idx) => ( {LogoMap[p.toLowerCase() as TPlatform] ?? ( @@ -116,12 +109,6 @@ export default function CampaignRow({
미연결
)}
- -
- -
); } diff --git a/src/components/ads/CampaignTable.tsx b/src/components/ads/CampaignTable.tsx index ed908553..0443dee4 100644 --- a/src/components/ads/CampaignTable.tsx +++ b/src/components/ads/CampaignTable.tsx @@ -3,10 +3,7 @@ import { twMerge } from "tailwind-merge"; import type { ICampaign } from "@/types/ads/campaign"; -import CampaignRow, { - CAMPAIGN_BUDGET_COL_CLASS, - CAMPAIGN_PLATFORM_COL_CLASS, -} from "./CampaignRow"; +import CampaignRow, { CAMPAIGN_PLATFORM_COL_CLASS } from "./CampaignRow"; interface ICampaignTableProps { campaigns: ICampaign[]; @@ -77,20 +74,11 @@ export default function CampaignTable({
플랫폼
-
- 예산 소진 현황 - 예산 -
    diff --git a/src/components/ads/skeleton/AdsSkeleton.tsx b/src/components/ads/skeleton/AdsSkeleton.tsx index db2f5d78..b0a8bdad 100644 --- a/src/components/ads/skeleton/AdsSkeleton.tsx +++ b/src/components/ads/skeleton/AdsSkeleton.tsx @@ -4,10 +4,7 @@ import { getAdListTableHeaderGridClass, getAdListTableRowGridClass, } from "@/components/ads/AdRow"; -import { - CAMPAIGN_BUDGET_COL_CLASS, - CAMPAIGN_PLATFORM_COL_CLASS, -} from "@/components/ads/CampaignRow"; +import { CAMPAIGN_PLATFORM_COL_CLASS } from "@/components/ads/CampaignRow"; import Card from "@/components/common/card/Card"; import { Skeleton, @@ -33,15 +30,12 @@ function CampaignTableRowSkeleton() {
    -
    - -
    ); } @@ -60,9 +54,6 @@ export function CampaignTableSkeleton() {
-
- -