From f9a745d5c01bc6654f0356b1655630469eca14d2 Mon Sep 17 00:00:00 2001 From: YermIm Date: Sun, 24 May 2026 06:42:52 +0900 Subject: [PATCH 01/88] =?UTF-8?q?feat:=20=ED=94=8C=EB=9E=AB=ED=8F=BC=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99=20=EB=AA=A9=EB=A1=9D=20mock=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EB=B0=8F=20=ED=83=80=EC=9E=85=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/icon/sidebar/connect.svg | 3 +++ .../integration/platformIntegrations.mock.ts | 20 +++++++++++++++++++ src/types/integration/platformConnection.ts | 19 ++++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 src/assets/icon/sidebar/connect.svg create mode 100644 src/pages/integration/platformIntegrations.mock.ts create mode 100644 src/types/integration/platformConnection.ts diff --git a/src/assets/icon/sidebar/connect.svg b/src/assets/icon/sidebar/connect.svg new file mode 100644 index 00000000..776084e1 --- /dev/null +++ b/src/assets/icon/sidebar/connect.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/pages/integration/platformIntegrations.mock.ts b/src/pages/integration/platformIntegrations.mock.ts new file mode 100644 index 00000000..35586680 --- /dev/null +++ b/src/pages/integration/platformIntegrations.mock.ts @@ -0,0 +1,20 @@ +import type { IPlatformConnectionItem } from "@/types/integration/platformConnection"; + +export const platformConnectionsMock: IPlatformConnectionItem[] = [ + { + provider: "META", + status: "connected", + accountLabel: "Meta Ads · 메인 계정", + lastSyncedAt: "2026-05-18T14:32:00", + }, + { + provider: "GOOGLE", + status: "disconnected", + }, + { + provider: "NAVER", + status: "error", + accountLabel: "네이버 검색광고", + errorMessage: "토큰이 만료되었습니다. 다시 연동해 주세요.", + }, +]; diff --git a/src/types/integration/platformConnection.ts b/src/types/integration/platformConnection.ts new file mode 100644 index 00000000..9659aec2 --- /dev/null +++ b/src/types/integration/platformConnection.ts @@ -0,0 +1,19 @@ +import type { TProvider } from "@/types/ads/campaign"; + +export type TIntegrationProvider = TProvider; + +export type TPlatformConnectionStatus = + | "disconnected" + | "connected" + | "error" + | "syncing"; + +export interface IPlatformConnectionItem { + provider: TIntegrationProvider; + status: TPlatformConnectionStatus; + /** 연동된 광고 계정 표시명 */ + accountLabel?: string; + /** ISO 문자열 또는 화면용 문자열 */ + lastSyncedAt?: string; + errorMessage?: string; +} From 48f146461e268975bfbc540f1d3c1e6b3122ea80 Mon Sep 17 00:00:00 2001 From: YermIm Date: Sun, 24 May 2026 20:59:44 +0900 Subject: [PATCH 02/88] =?UTF-8?q?feat:=20=ED=94=8C=EB=9E=AB=ED=8F=BC=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99=20UI=20=EB=B0=8F=20=EB=9D=BC=EC=9A=B0?= =?UTF-8?q?=EB=93=9C=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../integration/PlatformIntegrationCard.tsx | 164 ++++++++++++++++++ src/constants/sidebarNav.ts | 7 + src/layout/main/MainLayout.tsx | 16 +- .../integration/PlatformIntegrationsPage.tsx | 27 +++ .../integration/platformIntegrations.mock.ts | 3 +- src/routes/MainRoutes.tsx | 7 + src/types/integration/platformConnection.ts | 2 - 7 files changed, 215 insertions(+), 11 deletions(-) create mode 100644 src/components/integration/PlatformIntegrationCard.tsx create mode 100644 src/pages/integration/PlatformIntegrationsPage.tsx diff --git a/src/components/integration/PlatformIntegrationCard.tsx b/src/components/integration/PlatformIntegrationCard.tsx new file mode 100644 index 00000000..bbe5009e --- /dev/null +++ b/src/components/integration/PlatformIntegrationCard.tsx @@ -0,0 +1,164 @@ +import { memo, type ReactNode } from "react"; + +import { PLATFORM_MAP } from "@/types/dashboard/platform"; +import type { + IPlatformConnectionItem, + TIntegrationProvider, + TPlatformConnectionStatus, +} from "@/types/integration/platformConnection"; + +import Badge, { type TBadgeVariant } from "@/components/common/badge/Badge"; +import Button from "@/components/common/button/Button"; + +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 PLATFORM_LOGOS: Record = { + GOOGLE: , + NAVER: , + META: , +}; + +const STATUS_LABEL: Record = { + disconnected: "미연동", + connected: "연동됨", + error: "연동 오류", + syncing: "동기화 중", +}; + +/** 안정=infoBlue · 주의=infoYellow · 위험=infoRed · 중립=surface (Badge variant 추가 없음) */ +const CONNECTION_STATUS_BADGE: Record< + TPlatformConnectionStatus, + TBadgeVariant +> = { + connected: "infoBlue", + syncing: "infoYellow", + error: "infoRed", + disconnected: "surface", +}; + +type TProps = IPlatformConnectionItem & { + onConnect?: () => void; + onReconnect?: () => void; + onDisconnect?: () => void; +}; + +function formatSyncedAt(iso?: string) { + if (!iso) return null; + const date = new Date(iso); + if (Number.isNaN(date.getTime())) return iso; + return date.toLocaleString("ko-KR", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }); +} + +function PlatformIntegrationCard({ + provider, + status, + lastSyncedAt, + errorMessage, + onConnect, + onReconnect, + onDisconnect, +}: TProps) { + const label = PLATFORM_MAP[provider] ?? provider; + const syncedLabel = formatSyncedAt(lastSyncedAt); + + return ( +
+
+
+
{PLATFORM_LOGOS[provider]}
+

+ {label} +

+
+ + {STATUS_LABEL[status]} + +
+ + {syncedLabel ? ( +

+ 마지막 동기화 · {syncedLabel} +

+ ) : null} + + {status === "error" && errorMessage ? ( +

+ {errorMessage} +

+ ) : null} + +
+ +
+ {status === "disconnected" ? ( +

+ 광고 계정을 연동하면 대시보드와 캠페인에서 데이터를 확인할 수 + 있습니다. +

+ ) : null} + +
+ {status === "disconnected" ? ( + + ) : null} + + {status === "connected" ? ( + <> + + + + ) : null} + + {status === "error" ? ( + + ) : null} + + {status === "syncing" ? ( + + ) : null} +
+
+
+ ); +} + +export default memo(PlatformIntegrationCard); diff --git a/src/constants/sidebarNav.ts b/src/constants/sidebarNav.ts index 67ed1f5d..ddae5a7f 100644 --- a/src/constants/sidebarNav.ts +++ b/src/constants/sidebarNav.ts @@ -1,6 +1,7 @@ import type { INavItem } from "@/types/navigation/navItem"; import AdsIcon from "@/assets/icon/sidebar/ads.svg?react"; +import ConnectIcon from "@/assets/icon/sidebar/connect.svg?react"; import DashboardIcon from "@/assets/icon/sidebar/dashboard.svg?react"; import SettingsIcon from "@/assets/icon/sidebar/setting.svg?react"; import WorkspaceIcon from "@/assets/icon/sidebar/workspace.svg?react"; @@ -80,6 +81,12 @@ export const mainNav: INavItem[] = [ ]; export const footerNav: INavItem[] = [ + { + id: "integrations", + label: "플랫폼 연동", + icon: ConnectIcon, + path: "/integrations", + }, { id: "settings", label: "설정", diff --git a/src/layout/main/MainLayout.tsx b/src/layout/main/MainLayout.tsx index b3bb8d6b..2900b24e 100644 --- a/src/layout/main/MainLayout.tsx +++ b/src/layout/main/MainLayout.tsx @@ -199,13 +199,15 @@ export default function MainLayout() {
{headerRight}
-
- +
+
+ +
diff --git a/src/pages/integration/PlatformIntegrationsPage.tsx b/src/pages/integration/PlatformIntegrationsPage.tsx new file mode 100644 index 00000000..bb5ab974 --- /dev/null +++ b/src/pages/integration/PlatformIntegrationsPage.tsx @@ -0,0 +1,27 @@ +import { toast } from "sonner"; + +import PlatformIntegrationCard from "@/components/integration/PlatformIntegrationCard"; + +import { platformConnectionsMock } from "@/pages/integration/platformIntegrations.mock"; + +export default function PlatformIntegrationsPage() { + return ( +
+
    + {platformConnectionsMock.map((item) => ( +
  • + toast.message("연동하기 (mock)")} + onReconnect={() => toast.message("재연동 (mock)")} + onDisconnect={() => toast.message("연결 해제 (mock)")} + /> +
  • + ))} +
+
+ ); +} diff --git a/src/pages/integration/platformIntegrations.mock.ts b/src/pages/integration/platformIntegrations.mock.ts index 35586680..be679758 100644 --- a/src/pages/integration/platformIntegrations.mock.ts +++ b/src/pages/integration/platformIntegrations.mock.ts @@ -4,7 +4,6 @@ export const platformConnectionsMock: IPlatformConnectionItem[] = [ { provider: "META", status: "connected", - accountLabel: "Meta Ads · 메인 계정", lastSyncedAt: "2026-05-18T14:32:00", }, { @@ -14,7 +13,7 @@ export const platformConnectionsMock: IPlatformConnectionItem[] = [ { provider: "NAVER", status: "error", - accountLabel: "네이버 검색광고", errorMessage: "토큰이 만료되었습니다. 다시 연동해 주세요.", + lastSyncedAt: "2026-05-10T12:09:00", }, ]; diff --git a/src/routes/MainRoutes.tsx b/src/routes/MainRoutes.tsx index 964ffbb7..e6cdee4b 100644 --- a/src/routes/MainRoutes.tsx +++ b/src/routes/MainRoutes.tsx @@ -41,6 +41,9 @@ const MemberManagement = loadable( const Billing = loadable(lazy(() => import("@/pages/workspace/Billing"))); const Setting = loadable(lazy(() => import("@/pages/setting/Setting"))); +const PlatformIntegrationsPage = loadable( + lazy(() => import("@/pages/integration/PlatformIntegrationsPage")), +); const MainRoutes: RouteObject[] = [ { @@ -86,6 +89,10 @@ const MainRoutes: RouteObject[] = [ { path: "billing", element: }, ], }, + { + path: "integrations", + element: , + }, { path: "setting", element: , diff --git a/src/types/integration/platformConnection.ts b/src/types/integration/platformConnection.ts index 9659aec2..5b988ba5 100644 --- a/src/types/integration/platformConnection.ts +++ b/src/types/integration/platformConnection.ts @@ -11,8 +11,6 @@ export type TPlatformConnectionStatus = export interface IPlatformConnectionItem { provider: TIntegrationProvider; status: TPlatformConnectionStatus; - /** 연동된 광고 계정 표시명 */ - accountLabel?: string; /** ISO 문자열 또는 화면용 문자열 */ lastSyncedAt?: string; errorMessage?: string; From f3b4391f323c42aeaeef75b49694202713b92a4c Mon Sep 17 00:00:00 2001 From: YermIm Date: Sun, 24 May 2026 21:01:02 +0900 Subject: [PATCH 03/88] =?UTF-8?q?feat:=20=ED=94=8C=EB=9E=AB=ED=8F=BC=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99=20UI=20=EB=B0=8F=20=EB=9D=BC=EC=9A=B0?= =?UTF-8?q?=EB=93=9C=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/integration/PlatformIntegrationsPage.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/integration/PlatformIntegrationsPage.tsx b/src/pages/integration/PlatformIntegrationsPage.tsx index bb5ab974..5264fb83 100644 --- a/src/pages/integration/PlatformIntegrationsPage.tsx +++ b/src/pages/integration/PlatformIntegrationsPage.tsx @@ -15,9 +15,9 @@ export default function PlatformIntegrationsPage() { > toast.message("연동하기 (mock)")} - onReconnect={() => toast.message("재연동 (mock)")} - onDisconnect={() => toast.message("연결 해제 (mock)")} + onConnect={() => toast.message("연동하기")} + onReconnect={() => toast.message("재연동")} + onDisconnect={() => toast.message("연결 해제")} /> ))} From ce93ffd98c33d2dc1b297d5c68fea5d92932376c Mon Sep 17 00:00:00 2001 From: YermIm Date: Sun, 24 May 2026 21:38:48 +0900 Subject: [PATCH 04/88] =?UTF-8?q?feat:=20=EC=82=AC=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=EB=B0=94=EC=97=90=20=EB=B1=83=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../integration/PlatformIntegrationCard.tsx | 2 +- src/components/sidebar/Sidebar.tsx | 19 +++++++++ src/components/sidebar/SidebarItem.tsx | 41 +++++++++---------- .../integration/usePlatformConnections.ts | 25 +++++++++++ .../integration/PlatformIntegrationsPage.tsx | 8 ++-- 5 files changed, 70 insertions(+), 25 deletions(-) create mode 100644 src/hooks/integration/usePlatformConnections.ts diff --git a/src/components/integration/PlatformIntegrationCard.tsx b/src/components/integration/PlatformIntegrationCard.tsx index bbe5009e..b15b4785 100644 --- a/src/components/integration/PlatformIntegrationCard.tsx +++ b/src/components/integration/PlatformIntegrationCard.tsx @@ -80,7 +80,7 @@ function PlatformIntegrationCard({ {STATUS_LABEL[status]} diff --git a/src/components/sidebar/Sidebar.tsx b/src/components/sidebar/Sidebar.tsx index a6ca1ffc..3084b7bc 100644 --- a/src/components/sidebar/Sidebar.tsx +++ b/src/components/sidebar/Sidebar.tsx @@ -10,8 +10,14 @@ import { isPathMatch } from "@/utils/navigation/pathMatch"; import { applyWorkspacePathsToNav } from "@/utils/navigation/workspaceNavPaths"; import { useComingSoon } from "@/hooks/common/useComingSoon"; +import { + needsIntegrationAttention, + usePlatformConnections, +} from "@/hooks/integration/usePlatformConnections"; import { useSidebar } from "@/hooks/sidebar/useSidebar"; +import Badge from "@/components/common/badge/Badge"; + import { SidebarItem } from "./SidebarItem"; import { SubMenu } from "./SubMenu"; import { WorkspaceSwitcher } from "./WorkspaceSwitcher"; @@ -71,6 +77,12 @@ export default function Sidebar() { const { showComingSoon } = useComingSoon(); const selectedOrgId = useWorkspaceStore((s) => s.selectedOrgId); + const { data: platformConnections } = usePlatformConnections(); + const showIntegrationsAttention = useMemo( + () => needsIntegrationAttention(platformConnections), + [platformConnections], + ); + const mainNavWithWorkspace = useMemo( () => applyWorkspacePathsToNav(mainNav, selectedOrgId), [selectedOrgId], @@ -198,6 +210,13 @@ export default function Sidebar() { isCollapsed={isCollapsed} className="w-full h-full" onClick={handleFooterItemClick} + trailing={ + item.id === "integrations" && + showIntegrationsAttention && + !isCollapsed ? ( + 연동 필요 + ) : undefined + } /> ); diff --git a/src/components/sidebar/SidebarItem.tsx b/src/components/sidebar/SidebarItem.tsx index a416b570..4ec70844 100644 --- a/src/components/sidebar/SidebarItem.tsx +++ b/src/components/sidebar/SidebarItem.tsx @@ -1,4 +1,4 @@ -import { memo } from "react"; +import { memo, type ReactNode } from "react"; import { NavLink } from "react-router-dom"; import { twMerge } from "tailwind-merge"; @@ -10,6 +10,7 @@ interface ISidebarItemProps { isOpen?: boolean; className: string; onClick: (id: string, hasChildren: boolean) => void; + trailing?: ReactNode; } export const SidebarItem = memo(function SidebarItem({ @@ -18,30 +19,28 @@ export const SidebarItem = memo(function SidebarItem({ isOpen, className, onClick, + trailing, }: ISidebarItemProps) { const hasChildren = !!item.children?.length; const Icon = item.icon; - const content = ( -
- {Icon && ( - - )} - + const itemClassName = twMerge( + className, + "flex items-center", + isCollapsed ? "justify-center" : "", + ); + + const content = isCollapsed ? ( + Icon ? ( + + ) : null + ) : ( +
+ {Icon ? : null} + {item.label} + {trailing ? {trailing} : null}
); @@ -49,7 +48,7 @@ export const SidebarItem = memo(function SidebarItem({ return ( { if (e.defaultPrevented) return; onClick(item.id, hasChildren); @@ -65,7 +64,7 @@ export const SidebarItem = memo(function SidebarItem({ type="button" aria-haspopup={hasChildren ? "menu" : undefined} aria-expanded={hasChildren ? isOpen : undefined} - className={twMerge(className, "flex items-center text-left")} + className={twMerge(itemClassName, "text-left")} onClick={(e) => { if (e.defaultPrevented) return; onClick(item.id, hasChildren); diff --git a/src/hooks/integration/usePlatformConnections.ts b/src/hooks/integration/usePlatformConnections.ts new file mode 100644 index 00000000..c1a29d43 --- /dev/null +++ b/src/hooks/integration/usePlatformConnections.ts @@ -0,0 +1,25 @@ +import type { IPlatformConnectionItem } from "@/types/integration/platformConnection"; + +import { useCoreQuery } from "@/hooks/customQuery"; + +import { platformConnectionsMock } from "@/pages/integration/platformIntegrations.mock"; +import useWorkspaceStore from "@/store/useWorkspaceStore"; + +export function needsIntegrationAttention( + items: IPlatformConnectionItem[] | undefined, +): boolean { + return items?.some((item) => item.status === "error") ?? false; +} + +export function usePlatformConnections() { + const orgId = useWorkspaceStore((s) => s.selectedOrgId); + + return useCoreQuery( + ["platform-connections", orgId], + async () => { + // TODO: GET /api/orgs/{orgId}/integrations + return platformConnectionsMock; + }, + { enabled: orgId != null }, + ); +} diff --git a/src/pages/integration/PlatformIntegrationsPage.tsx b/src/pages/integration/PlatformIntegrationsPage.tsx index 5264fb83..f2881b04 100644 --- a/src/pages/integration/PlatformIntegrationsPage.tsx +++ b/src/pages/integration/PlatformIntegrationsPage.tsx @@ -1,14 +1,16 @@ import { toast } from "sonner"; -import PlatformIntegrationCard from "@/components/integration/PlatformIntegrationCard"; +import { usePlatformConnections } from "@/hooks/integration/usePlatformConnections"; -import { platformConnectionsMock } from "@/pages/integration/platformIntegrations.mock"; +import PlatformIntegrationCard from "@/components/integration/PlatformIntegrationCard"; export default function PlatformIntegrationsPage() { + const { data: platformConnections = [] } = usePlatformConnections(); + return (
    - {platformConnectionsMock.map((item) => ( + {platformConnections.map((item) => (
  • Date: Sun, 24 May 2026 22:14:30 +0900 Subject: [PATCH 05/88] feat: skeleton ui --- .../skeleton/PlatformIntegrationsSkeleton.tsx | 49 +++++++++++++++++++ .../integration/usePlatformConnections.ts | 4 +- .../integration/PlatformIntegrationsPage.tsx | 49 +++++++++++++------ 3 files changed, 85 insertions(+), 17 deletions(-) create mode 100644 src/components/integration/skeleton/PlatformIntegrationsSkeleton.tsx diff --git a/src/components/integration/skeleton/PlatformIntegrationsSkeleton.tsx b/src/components/integration/skeleton/PlatformIntegrationsSkeleton.tsx new file mode 100644 index 00000000..36261325 --- /dev/null +++ b/src/components/integration/skeleton/PlatformIntegrationsSkeleton.tsx @@ -0,0 +1,49 @@ +import { + Skeleton, + SkeletonCircle, +} from "@/components/common/skeleton/Skeleton"; + +const SKELETON_COUNT = 3; + +export function PlatformIntegrationCardSkeleton() { + return ( +
    +
    +
    + + +
    + +
    + +
    + +
    + +
    + +
    + +
    +
    + ); +} + +export default function PlatformIntegrationsPageSkeleton() { + return ( +
      + {Array.from({ length: SKELETON_COUNT }, (_, i) => ( +
    • + +
    • + ))} +
    + ); +} diff --git a/src/hooks/integration/usePlatformConnections.ts b/src/hooks/integration/usePlatformConnections.ts index c1a29d43..0feb67f8 100644 --- a/src/hooks/integration/usePlatformConnections.ts +++ b/src/hooks/integration/usePlatformConnections.ts @@ -17,7 +17,9 @@ export function usePlatformConnections() { return useCoreQuery( ["platform-connections", orgId], async () => { - // TODO: GET /api/orgs/{orgId}/integrations + await new Promise((resolve) => { + setTimeout(resolve, 800); + }); return platformConnectionsMock; }, { enabled: orgId != null }, diff --git a/src/pages/integration/PlatformIntegrationsPage.tsx b/src/pages/integration/PlatformIntegrationsPage.tsx index f2881b04..e42936f2 100644 --- a/src/pages/integration/PlatformIntegrationsPage.tsx +++ b/src/pages/integration/PlatformIntegrationsPage.tsx @@ -3,27 +3,44 @@ import { toast } from "sonner"; import { usePlatformConnections } from "@/hooks/integration/usePlatformConnections"; import PlatformIntegrationCard from "@/components/integration/PlatformIntegrationCard"; +import PlatformIntegrationsPageSkeleton from "@/components/integration/skeleton/PlatformIntegrationsSkeleton"; export default function PlatformIntegrationsPage() { - const { data: platformConnections = [] } = usePlatformConnections(); + const { + data: platformConnections = [], + isLoading, + isError, + error, + } = usePlatformConnections(); return (
    -
      - {platformConnections.map((item) => ( -
    • - toast.message("연동하기")} - onReconnect={() => toast.message("재연동")} - onDisconnect={() => toast.message("연결 해제")} - /> -
    • - ))} -
    + {isLoading ? ( + + ) : isError ? ( +
    +

    + {error?.message ?? + "플랫폼 연동 정보를 불러오지 못했습니다. 잠시 후 다시 시도해 주세요."} +

    +
    + ) : ( +
      + {platformConnections.map((item) => ( +
    • + toast.message("연동하기")} + onReconnect={() => toast.message("재연동")} + onDisconnect={() => toast.message("연결 해제")} + /> +
    • + ))} +
    + )}
    ); } From 3986318d7f5e3f641021f19feaf1d306a0ad2c31 Mon Sep 17 00:00:00 2001 From: JAESEON PARK Date: Wed, 27 May 2026 16:11:00 +0900 Subject: [PATCH 06/88] =?UTF-8?q?fix:=20=EC=9B=8C=ED=81=AC=EC=8A=A4?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EC=84=A4=EC=A0=95=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20MEMBER=EC=97=90=EA=B2=8C=EB=8A=94=20?= =?UTF-8?q?=EC=9D=BD=EA=B8=B0=EC=A0=84=EC=9A=A9=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=B4=EC=9D=B4=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/workspace/WorkspaceSetting.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/WorkspaceSetting.tsx b/src/pages/workspace/WorkspaceSetting.tsx index c1527591..074c086b 100644 --- a/src/pages/workspace/WorkspaceSetting.tsx +++ b/src/pages/workspace/WorkspaceSetting.tsx @@ -5,7 +5,7 @@ import { toast } from "sonner"; import type { IApiErrorResponse } from "@/types/common/common"; -import useIsAdmin from "@/hooks/auth/useIsAdmin"; +import { useCoreQuery } from "@/hooks/customQuery"; import Button from "@/components/common/button/Button"; import Card from "@/components/common/card/Card"; @@ -17,6 +17,7 @@ import WorkspaceSettingLoading from "@/components/workspace/WorkspaceSettingLoad import { deleteWorkspace, + getMyWorkspaces, getWorkspace, updateWorkspace, } from "@/api/workspace/org"; @@ -25,9 +26,15 @@ import WarnIcon from "@/assets/icon/common/warn-circle.svg?react"; import { getImageUrl } from "@/lib/getImageUrl"; export default function WorkspaceSetting() { - const isAdmin = useIsAdmin(); const navigate = useNavigate(); const { workspaceId } = useParams(); + + const { data: workspaces } = useCoreQuery(["my-workspaces"], getMyWorkspaces); + const isAdmin = useMemo(() => { + if (!workspaceId || !workspaces) return false; + const workspace = workspaces.find((w) => w.orgId === Number(workspaceId)); + return workspace?.myRole === "ADMIN"; + }, [workspaceId, workspaces]); const queryClient = useQueryClient(); const orgId = useMemo(() => { From c46e3d6b0e88f16673b4ffed97fcfe13a8136053 Mon Sep 17 00:00:00 2001 From: JAESEON PARK Date: Wed, 27 May 2026 16:12:08 +0900 Subject: [PATCH 07/88] =?UTF-8?q?fix:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20useIsAdmin=20=ED=9B=85=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/auth/useIsAdmin.ts | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 src/hooks/auth/useIsAdmin.ts diff --git a/src/hooks/auth/useIsAdmin.ts b/src/hooks/auth/useIsAdmin.ts deleted file mode 100644 index 25e5c2aa..00000000 --- a/src/hooks/auth/useIsAdmin.ts +++ /dev/null @@ -1,9 +0,0 @@ -import useWorkspaceStore from "@/store/useWorkspaceStore"; - -/*useWorkspaceStore에서 myRole을 읽어서 booelan으로 반환하는 훅*/ -function useIsAdmin(): boolean { - const myRole = useWorkspaceStore((s) => s.myRole); - return myRole === "ADMIN"; -} - -export default useIsAdmin; From 917c1f364057694995fc9a68329d25261f822ea9 Mon Sep 17 00:00:00 2001 From: JAESEON PARK Date: Wed, 27 May 2026 16:15:33 +0900 Subject: [PATCH 08/88] =?UTF-8?q?style:=20disabled=EC=8B=9C=20Textarea=20?= =?UTF-8?q?=ED=85=8D=EC=8A=A4=ED=8A=B8=EC=BB=AC=EB=9F=AC=EB=A5=BC=20Input?= =?UTF-8?q?=EC=BB=AC=EB=9F=AC=EC=99=80=20=EB=8F=99=EC=9D=BC=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/textarea/TextareaField.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/common/textarea/TextareaField.tsx b/src/components/common/textarea/TextareaField.tsx index e8117a55..6b4d1c78 100644 --- a/src/components/common/textarea/TextareaField.tsx +++ b/src/components/common/textarea/TextareaField.tsx @@ -47,7 +47,7 @@ export default function TextareaField({ rows={minRows} value={value} className={twMerge( - "w-full rounded-2xl px-5 py-4 ring-1 ring-primary-400/30 outline-none transition-colors duration-200 ease-out overflow-hidden placeholder:text-text-placeholder text-text-title font-body1", + "w-full rounded-2xl px-5 py-4 ring-1 ring-primary-400/30 outline-none transition-colors duration-200 ease-out overflow-hidden placeholder:text-text-placeholder text-text-title disabled:text-text-muted font-body1", disabled ? "bg-surface-200 cursor-not-allowed" : "hover:bg-surface-200 hover:ring-primary-400/40 focus-within:bg-surface-100 focus-within:ring-2 focus-within:ring-primary-400/50", From a8683850a5c634f93349c8031e94a95d80325864 Mon Sep 17 00:00:00 2001 From: JAESEON PARK Date: Wed, 27 May 2026 16:27:21 +0900 Subject: [PATCH 09/88] =?UTF-8?q?fix:=20=EC=82=AC=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=EB=B0=94=20MEMBER/ADMIN=20=EB=A9=94=EB=89=B4=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/sidebar/Sidebar.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/sidebar/Sidebar.tsx b/src/components/sidebar/Sidebar.tsx index 1ea18460..3a17be1b 100644 --- a/src/components/sidebar/Sidebar.tsx +++ b/src/components/sidebar/Sidebar.tsx @@ -1,5 +1,6 @@ import type { Dispatch, FocusEvent, SetStateAction } from "react"; import { useCallback, useMemo } from "react"; +import { useParams } from "react-router-dom"; import { AnimatePresence, motion } from "framer-motion"; import { twMerge } from "tailwind-merge"; @@ -12,12 +13,14 @@ import { isPathMatch } from "@/utils/navigation/pathMatch"; import { applyWorkspacePathsToNav } from "@/utils/navigation/workspaceNavPaths"; import { useComingSoon } from "@/hooks/common/useComingSoon"; +import { useCoreQuery } from "@/hooks/customQuery"; import { useSidebar } from "@/hooks/sidebar/useSidebar"; import { SidebarItem } from "./SidebarItem"; import { SubMenu } from "./SubMenu"; import { WorkspaceSwitcher } from "./WorkspaceSwitcher"; +import { getMyWorkspaces } from "@/api/workspace/org"; import CollapseIcon from "@/assets/icon/chevron/chervon-left.svg?react"; import ChevronIcon from "@/assets/icon/chevron/chevron-up.svg?react"; import useWorkspaceStore from "@/store/useWorkspaceStore"; @@ -87,7 +90,15 @@ export default function Sidebar() { const { showComingSoon } = useComingSoon(); const selectedOrgId = useWorkspaceStore((s) => s.selectedOrgId); - const myRole = useWorkspaceStore((s) => s.myRole); + const myRoleFromStore = useWorkspaceStore((s) => s.myRole); + const { workspaceId } = useParams<{ workspaceId: string }>(); + const { data: workspaces } = useCoreQuery(["my-workspaces"], getMyWorkspaces); + + const myRole = useMemo(() => { + if (!workspaceId || !workspaces) return null; + const workspace = workspaces.find((w) => w.orgId === Number(workspaceId)); + return workspace?.myRole ?? myRoleFromStore; + }, [workspaceId, workspaces, myRoleFromStore]); const mainNavWithWorkspace = useMemo( () => filterNavByRole(applyWorkspacePathsToNav(mainNav, selectedOrgId), myRole), From 6d7c294e599db6a388bce87efe35234bb2dc5e2f Mon Sep 17 00:00:00 2001 From: YermIm Date: Tue, 2 Jun 2026 21:27:24 +0900 Subject: [PATCH 10/88] =?UTF-8?q?feat:=20=EC=B9=B4=EB=93=9C=20=EC=84=B8?= =?UTF-8?q?=EB=B6=80=EC=82=AC=ED=95=AD=20=EB=B0=8F=20=EC=B6=94=ED=9B=84=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99=20=ED=94=8C=EB=9E=AB=ED=8F=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../integration/PlatformIntegrationCard.tsx | 105 ++++++++++++--- .../integration/UpcomingPlatformCard.tsx | 95 +++++++++++++ .../skeleton/PlatformIntegrationsSkeleton.tsx | 6 +- .../integration/usePlatformConnections.ts | 6 +- .../integration/PlatformIntegrationsPage.tsx | 52 ++++--- .../integration/platformIntegrations.mock.ts | 32 +++-- src/types/integration/platformConnection.ts | 26 +++- src/utils/integration/mapPlatformAccounts.ts | 127 ++++++++++++++++++ 8 files changed, 399 insertions(+), 50 deletions(-) create mode 100644 src/components/integration/UpcomingPlatformCard.tsx create mode 100644 src/utils/integration/mapPlatformAccounts.ts diff --git a/src/components/integration/PlatformIntegrationCard.tsx b/src/components/integration/PlatformIntegrationCard.tsx index b15b4785..5877b1ab 100644 --- a/src/components/integration/PlatformIntegrationCard.tsx +++ b/src/components/integration/PlatformIntegrationCard.tsx @@ -7,6 +7,12 @@ import type { TPlatformConnectionStatus, } from "@/types/integration/platformConnection"; +import { + formatConnectionDate, + formatConnectionDateTime, + getTokenExpireTone, +} from "@/utils/integration/mapPlatformAccounts"; + import Badge, { type TBadgeVariant } from "@/components/common/badge/Badge"; import Button from "@/components/common/button/Button"; @@ -38,36 +44,98 @@ const CONNECTION_STATUS_BADGE: Record< disconnected: "surface", }; +const TOKEN_EXPIRE_TEXT: Record< + ReturnType, + string +> = { + default: "text-text-title", + warning: "text-info-yellow", + expired: "text-info-red/80", +}; + type TProps = IPlatformConnectionItem & { onConnect?: () => void; onReconnect?: () => void; onDisconnect?: () => void; }; -function formatSyncedAt(iso?: string) { - if (!iso) return null; - const date = new Date(iso); - if (Number.isNaN(date.getTime())) return iso; - return date.toLocaleString("ko-KR", { - year: "numeric", - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - }); +function PlatformConnectionMeta({ + status, + syncedAt, + externalAccountId, + tokenExpireAt, +}: Pick< + IPlatformConnectionItem, + "status" | "syncedAt" | "externalAccountId" | "tokenExpireAt" +>) { + const syncedLabel = formatConnectionDateTime(syncedAt); + const expireLabel = formatConnectionDate(tokenExpireAt); + const expireTone = getTokenExpireTone(tokenExpireAt); + + return ( +
    + {status === "disconnected" ? ( + <> +

    + 마지막 동기화 · + +

    +

    + 연동 계정 · + +

    +

    + 토큰 만료 예정 · + +

    + + ) : ( + <> + {syncedLabel ? ( +

    + 마지막 동기화 · + {syncedLabel} +

    + ) : null} + + {externalAccountId ? ( +

    + 연동 계정 · + + {externalAccountId} + +

    + ) : null} + + {expireLabel ? ( +

    + 토큰 만료 예정 · + + {expireLabel} + +

    + ) : null} + + )} +
    + ); } function PlatformIntegrationCard({ provider, status, - lastSyncedAt, + syncedAt, + externalAccountId, + tokenExpireAt, errorMessage, onConnect, onReconnect, onDisconnect, }: TProps) { const label = PLATFORM_MAP[provider] ?? provider; - const syncedLabel = formatSyncedAt(lastSyncedAt); return (
    @@ -86,11 +154,12 @@ function PlatformIntegrationCard({
    - {syncedLabel ? ( -

    - 마지막 동기화 · {syncedLabel} -

    - ) : null} + {status === "error" && errorMessage ? (

    diff --git a/src/components/integration/UpcomingPlatformCard.tsx b/src/components/integration/UpcomingPlatformCard.tsx new file mode 100644 index 00000000..51f2b05d --- /dev/null +++ b/src/components/integration/UpcomingPlatformCard.tsx @@ -0,0 +1,95 @@ +import type { ReactNode } from "react"; + +import Badge from "@/components/common/badge/Badge"; +import Button from "@/components/common/button/Button"; + +import KakaoLogo from "@/assets/logo/social-logo/circle/kakao-circle.svg?react"; + +const UPCOMING_CARD_SHELL_CLASS = + "flex h-full min-h-70 w-full rounded-3xl bg-surface-100 p-8 shadow-Soft tablet:p-8"; +const UPCOMING_CARD_DISABLED_CLASS = + "pointer-events-none select-none opacity-70 grayscale"; + +type TProps = { + title: string; + badgeText: string; + description: string; + icon?: ReactNode; + disabled?: boolean; +}; + +export default function UpcomingPlatformCard({ + title, + badgeText, + description, + icon, + disabled = true, +}: TProps) { + return ( +

    +
    +
    +
    + {icon ?? ( + + ? + + )} +
    +

    + {title} +

    +
    + + + {badgeText} + +
    + +

    {description}

    + +
    + +
    +
    + ); +} + +export function KakaoUpcomingCard() { + return ( + } + /> + ); +} + +export function ComingSoonUpcomingCard() { + return ( +
    +
    +

    Coming soon…

    +
    +
    + ); +} diff --git a/src/components/integration/skeleton/PlatformIntegrationsSkeleton.tsx b/src/components/integration/skeleton/PlatformIntegrationsSkeleton.tsx index 36261325..7699c2a5 100644 --- a/src/components/integration/skeleton/PlatformIntegrationsSkeleton.tsx +++ b/src/components/integration/skeleton/PlatformIntegrationsSkeleton.tsx @@ -19,8 +19,10 @@ export function PlatformIntegrationCardSkeleton() {
    -
    - +
    + + +
    diff --git a/src/hooks/integration/usePlatformConnections.ts b/src/hooks/integration/usePlatformConnections.ts index 0feb67f8..acba2196 100644 --- a/src/hooks/integration/usePlatformConnections.ts +++ b/src/hooks/integration/usePlatformConnections.ts @@ -1,8 +1,10 @@ import type { IPlatformConnectionItem } from "@/types/integration/platformConnection"; +import { mapPlatformAccountsToConnections } from "@/utils/integration/mapPlatformAccounts"; + import { useCoreQuery } from "@/hooks/customQuery"; -import { platformConnectionsMock } from "@/pages/integration/platformIntegrations.mock"; +import { platformAccountsApiMock } from "@/pages/integration/platformIntegrations.mock"; import useWorkspaceStore from "@/store/useWorkspaceStore"; export function needsIntegrationAttention( @@ -20,7 +22,7 @@ export function usePlatformConnections() { await new Promise((resolve) => { setTimeout(resolve, 800); }); - return platformConnectionsMock; + return mapPlatformAccountsToConnections(platformAccountsApiMock); }, { enabled: orgId != null }, ); diff --git a/src/pages/integration/PlatformIntegrationsPage.tsx b/src/pages/integration/PlatformIntegrationsPage.tsx index e42936f2..072f12e1 100644 --- a/src/pages/integration/PlatformIntegrationsPage.tsx +++ b/src/pages/integration/PlatformIntegrationsPage.tsx @@ -4,6 +4,10 @@ import { usePlatformConnections } from "@/hooks/integration/usePlatformConnectio import PlatformIntegrationCard from "@/components/integration/PlatformIntegrationCard"; import PlatformIntegrationsPageSkeleton from "@/components/integration/skeleton/PlatformIntegrationsSkeleton"; +import { + ComingSoonUpcomingCard, + KakaoUpcomingCard, +} from "@/components/integration/UpcomingPlatformCard"; export default function PlatformIntegrationsPage() { const { @@ -25,21 +29,39 @@ export default function PlatformIntegrationsPage() {

    ) : ( -
      - {platformConnections.map((item) => ( -
    • - toast.message("연동하기")} - onReconnect={() => toast.message("재연동")} - onDisconnect={() => toast.message("연결 해제")} - /> -
    • - ))} -
    + <> +
      + {platformConnections.map((item) => ( +
    • + toast.message("연동하기")} + onReconnect={() => toast.message("재연동")} + onDisconnect={() => toast.message("연결 해제")} + /> +
    • + ))} +
    + +
    +

    + 더 많은 플랫폼 연동을 준비하고 있어요. 지원 범위는 변경될 수 + 있습니다. +

    + +
      +
    • + +
    • +
    • + +
    • +
    +
    + )}
); diff --git a/src/pages/integration/platformIntegrations.mock.ts b/src/pages/integration/platformIntegrations.mock.ts index be679758..2017c7df 100644 --- a/src/pages/integration/platformIntegrations.mock.ts +++ b/src/pages/integration/platformIntegrations.mock.ts @@ -1,19 +1,29 @@ -import type { IPlatformConnectionItem } from "@/types/integration/platformConnection"; +import type { IPlatformAccountApi } from "@/types/integration/platformConnection"; -export const platformConnectionsMock: IPlatformConnectionItem[] = [ +import { mapPlatformAccountsToConnections } from "@/utils/integration/mapPlatformAccounts"; + +/** 목록 API `data.platformAccounts` mock */ +export const platformAccountsApiMock: IPlatformAccountApi[] = [ { + platformAccountId: 1, + externalAccountId: "act_2847193056", provider: "META", - status: "connected", - lastSyncedAt: "2026-05-18T14:32:00", - }, - { - provider: "GOOGLE", - status: "disconnected", + authType: "OAUTH", + status: "ACTIVE", + tokenExpireAt: "2026-08-18", + syncedAt: "2026-05-18T14:32:00", }, { + platformAccountId: 3, + externalAccountId: "naver-ad-882910", provider: "NAVER", - status: "error", - errorMessage: "토큰이 만료되었습니다. 다시 연동해 주세요.", - lastSyncedAt: "2026-05-10T12:09:00", + authType: "OAUTH", + status: "EXPIRED", + tokenExpireAt: "2026-05-20", + syncedAt: "2026-05-10T12:09:00", }, ]; + +export const platformConnectionsMock = mapPlatformAccountsToConnections( + platformAccountsApiMock, +); diff --git a/src/types/integration/platformConnection.ts b/src/types/integration/platformConnection.ts index 5b988ba5..8076fdca 100644 --- a/src/types/integration/platformConnection.ts +++ b/src/types/integration/platformConnection.ts @@ -2,6 +2,26 @@ import type { TProvider } from "@/types/ads/campaign"; export type TIntegrationProvider = TProvider; +/** 목록 API `platformAccounts[].status` */ +export type TPlatformAccountApiStatus = "ACTIVE" | "EXPIRED" | "INACTIVE"; + +export type TPlatformAuthType = "OAUTH"; + +export interface IPlatformAccountApi { + platformAccountId: number; + externalAccountId: string; + provider: TIntegrationProvider; + authType: TPlatformAuthType; + status: TPlatformAccountApiStatus; + tokenExpireAt?: string; + syncedAt?: string; +} + +export interface IPlatformAccountsResponseData { + platformAccounts: IPlatformAccountApi[]; +} + +/** 카드 UI용 연동 상태 */ export type TPlatformConnectionStatus = | "disconnected" | "connected" @@ -11,7 +31,9 @@ export type TPlatformConnectionStatus = export interface IPlatformConnectionItem { provider: TIntegrationProvider; status: TPlatformConnectionStatus; - /** ISO 문자열 또는 화면용 문자열 */ - lastSyncedAt?: string; + platformAccountId?: number; + externalAccountId?: string; + syncedAt?: string; + tokenExpireAt?: string; errorMessage?: string; } diff --git a/src/utils/integration/mapPlatformAccounts.ts b/src/utils/integration/mapPlatformAccounts.ts new file mode 100644 index 00000000..e5c05da5 --- /dev/null +++ b/src/utils/integration/mapPlatformAccounts.ts @@ -0,0 +1,127 @@ +import type { + IPlatformAccountApi, + IPlatformConnectionItem, + TIntegrationProvider, + TPlatformConnectionStatus, +} from "@/types/integration/platformConnection"; + +const INTEGRATION_PROVIDERS: TIntegrationProvider[] = [ + "META", + "GOOGLE", + "NAVER", +]; + +const TOKEN_EXPIRE_WARNING_DAYS = 7; + +function parseDate(value: string): Date | null { + const date = new Date(value); + return Number.isNaN(date.getTime()) ? null : date; +} + +function startOfDay(date: Date): Date { + return new Date(date.getFullYear(), date.getMonth(), date.getDate()); +} + +export function isTokenExpired(tokenExpireAt?: string): boolean { + if (!tokenExpireAt) return false; + const expire = parseDate(tokenExpireAt); + if (!expire) return false; + return startOfDay(expire).getTime() < startOfDay(new Date()).getTime(); +} + +export function isTokenExpiringSoon(tokenExpireAt?: string): boolean { + if (!tokenExpireAt || isTokenExpired(tokenExpireAt)) return false; + const expire = parseDate(tokenExpireAt); + if (!expire) return false; + const diffMs = + startOfDay(expire).getTime() - startOfDay(new Date()).getTime(); + const diffDays = diffMs / (1000 * 60 * 60 * 24); + return diffDays <= TOKEN_EXPIRE_WARNING_DAYS; +} + +export type TTokenExpireTone = "default" | "warning" | "expired"; + +export function getTokenExpireTone(tokenExpireAt?: string): TTokenExpireTone { + if (!tokenExpireAt) return "default"; + if (isTokenExpired(tokenExpireAt)) return "expired"; + if (isTokenExpiringSoon(tokenExpireAt)) return "warning"; + return "default"; +} + +export function formatConnectionDateTime(value?: string): string | null { + if (!value) return null; + const date = parseDate(value); + if (!date) return value; + if (value.includes("T")) { + return date.toLocaleString("ko-KR", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }); + } + return date.toLocaleDateString("ko-KR", { + year: "numeric", + month: "2-digit", + day: "2-digit", + }); +} + +export function formatConnectionDate(value?: string): string | null { + if (!value) return null; + const date = parseDate(value); + if (!date) return value; + return date.toLocaleDateString("ko-KR", { + year: "numeric", + month: "2-digit", + day: "2-digit", + }); +} + +function mapApiStatusToUi( + account: IPlatformAccountApi, +): TPlatformConnectionStatus { + if (account.status === "EXPIRED" || isTokenExpired(account.tokenExpireAt)) { + return "error"; + } + if (account.status === "ACTIVE") { + return "connected"; + } + return "disconnected"; +} + +function mapAccountToConnection( + account: IPlatformAccountApi, +): IPlatformConnectionItem { + const status = mapApiStatusToUi(account); + const base: IPlatformConnectionItem = { + provider: account.provider, + status, + platformAccountId: account.platformAccountId, + externalAccountId: account.externalAccountId, + syncedAt: account.syncedAt, + tokenExpireAt: account.tokenExpireAt, + }; + + if (status === "error") { + return { + ...base, + errorMessage: "토큰이 만료되었습니다. 다시 연동해 주세요.", + }; + } + + return base; +} + +export function mapPlatformAccountsToConnections( + accounts: IPlatformAccountApi[], +): IPlatformConnectionItem[] { + return INTEGRATION_PROVIDERS.map((provider) => { + const account = accounts.find((item) => item.provider === provider); + if (!account) { + return { provider, status: "disconnected" }; + } + return mapAccountToConnection(account); + }); +} From 276e9557de3d12e610a652c0e53bf10278d44c92 Mon Sep 17 00:00:00 2001 From: YermIm Date: Sun, 7 Jun 2026 06:16:35 +0900 Subject: [PATCH 11/88] =?UTF-8?q?chore:=20=EC=A3=BC=EC=84=9D=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/integration/PlatformIntegrationCard.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/integration/PlatformIntegrationCard.tsx b/src/components/integration/PlatformIntegrationCard.tsx index a1b48831..c7f0bf1e 100644 --- a/src/components/integration/PlatformIntegrationCard.tsx +++ b/src/components/integration/PlatformIntegrationCard.tsx @@ -33,7 +33,6 @@ const STATUS_LABEL: Record = { syncing: "동기화 중", }; -/** 안정=infoBlue · 주의=infoYellow · 위험=infoRed · 중립=surface (Badge variant 추가 없음) */ const CONNECTION_STATUS_BADGE: Record< TPlatformConnectionStatus, TBadgeVariant From e3a8a6b586abdc0b8a07bdfdb6222e1ab8df65d6 Mon Sep 17 00:00:00 2001 From: YermIm Date: Sun, 7 Jun 2026 18:14:49 +0900 Subject: [PATCH 12/88] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EB=B9=97=20=EB=A6=AC=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../integration/PlatformIntegrationCard.tsx | 8 -------- .../integration/UpcomingPlatformCard.tsx | 2 +- src/types/integration/platformConnection.ts | 6 +----- src/utils/integration/mapPlatformAccounts.ts | 18 ++++++++++++++++++ 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/components/integration/PlatformIntegrationCard.tsx b/src/components/integration/PlatformIntegrationCard.tsx index c7f0bf1e..5190f520 100644 --- a/src/components/integration/PlatformIntegrationCard.tsx +++ b/src/components/integration/PlatformIntegrationCard.tsx @@ -30,7 +30,6 @@ const STATUS_LABEL: Record = { disconnected: "미연동", connected: "연동됨", error: "연동 오류", - syncing: "동기화 중", }; const CONNECTION_STATUS_BADGE: Record< @@ -38,7 +37,6 @@ const CONNECTION_STATUS_BADGE: Record< TBadgeVariant > = { connected: "infoBlue", - syncing: "infoYellow", error: "infoRed", disconnected: "surface", }; @@ -217,12 +215,6 @@ function PlatformIntegrationCard({ 재연동 ) : null} - - {status === "syncing" ? ( - - ) : null}
diff --git a/src/components/integration/UpcomingPlatformCard.tsx b/src/components/integration/UpcomingPlatformCard.tsx index 51f2b05d..5ee3d3c5 100644 --- a/src/components/integration/UpcomingPlatformCard.tsx +++ b/src/components/integration/UpcomingPlatformCard.tsx @@ -58,7 +58,7 @@ export default function UpcomingPlatformCard({

{description}

-
diff --git a/src/types/integration/platformConnection.ts b/src/types/integration/platformConnection.ts index 8076fdca..9080d6a8 100644 --- a/src/types/integration/platformConnection.ts +++ b/src/types/integration/platformConnection.ts @@ -22,11 +22,7 @@ export interface IPlatformAccountsResponseData { } /** 카드 UI용 연동 상태 */ -export type TPlatformConnectionStatus = - | "disconnected" - | "connected" - | "error" - | "syncing"; +export type TPlatformConnectionStatus = "disconnected" | "connected" | "error"; export interface IPlatformConnectionItem { provider: TIntegrationProvider; diff --git a/src/utils/integration/mapPlatformAccounts.ts b/src/utils/integration/mapPlatformAccounts.ts index e5c05da5..ff6280e4 100644 --- a/src/utils/integration/mapPlatformAccounts.ts +++ b/src/utils/integration/mapPlatformAccounts.ts @@ -12,8 +12,26 @@ const INTEGRATION_PROVIDERS: TIntegrationProvider[] = [ ]; const TOKEN_EXPIRE_WARNING_DAYS = 7; +const DATE_ONLY_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/; +/** date-only(YYYY-MM-DD)는 UTC가 아닌 로컬 달력 날짜로 파싱 */ function parseDate(value: string): Date | null { + const dateOnly = DATE_ONLY_PATTERN.exec(value); + if (dateOnly) { + const year = Number(dateOnly[1]); + const month = Number(dateOnly[2]); + const day = Number(dateOnly[3]); + const date = new Date(year, month - 1, day); + if ( + date.getFullYear() !== year || + date.getMonth() !== month - 1 || + date.getDate() !== day + ) { + return null; + } + return date; + } + const date = new Date(value); return Number.isNaN(date.getTime()) ? null : date; } From 72ba2cfe8334bc0b6a6e488c81bb77afb28d864f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9C=EC=A0=9C=EA=B2=BD?= Date: Wed, 17 Jun 2026 15:06:16 +0900 Subject: [PATCH 13/88] =?UTF-8?q?refactor:=20IRoasRanking=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=EC=9D=84=20provider.ts=EB=A1=9C=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit overview.ts, platform.ts에 중복 선언되어 있던 IRoasRanking을 provider.ts 단일 출처로 이동하고 각 파일에서 re-export --- src/types/dashboard/overview.ts | 13 +++---------- src/types/dashboard/platform.ts | 13 ++----------- src/types/dashboard/provider.ts | 12 ++++++++++++ 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/types/dashboard/overview.ts b/src/types/dashboard/overview.ts index 4c4ad15d..fe6e0e15 100644 --- a/src/types/dashboard/overview.ts +++ b/src/types/dashboard/overview.ts @@ -1,4 +1,7 @@ +import type { IRoasRanking } from "./provider"; + export type { TProviderType } from "./provider"; +export type { IRoasRanking }; // 전체 지표 집계 응답 export interface IMetricsResponse { @@ -34,16 +37,6 @@ export interface IRoasRankingsResponse { rankings: IRoasRanking[]; } -// ROAS 순위 항목 -export interface IRoasRanking { - rank: number; - provider: string; - roas: number; - diffRate: number | null; - revenue: number; - adSpend: number; -} - // 플랫폼별 ROAS 순위 + 지표(CTR/CVR) 통합 항목 export interface IPlatformRankingItem extends IRoasRanking { clickRate?: number; // CTR (%) diff --git a/src/types/dashboard/platform.ts b/src/types/dashboard/platform.ts index ff514821..6d7fb0ed 100644 --- a/src/types/dashboard/platform.ts +++ b/src/types/dashboard/platform.ts @@ -1,16 +1,7 @@ import type { TProviderType } from "./provider"; -export type { TProviderType } from "./provider"; - -// ROAS 성과 순위 -export interface IRoasRanking { - rank: number; - provider: string; - roas: number; - diffRate: number | null; - revenue: number; - adSpend: number; -} +export type { TProviderType }; +export type { IRoasRanking } from "./provider"; // 플랫폼 광고 소재 개수 export interface IAdCount { diff --git a/src/types/dashboard/provider.ts b/src/types/dashboard/provider.ts index b4f53d24..3931c4b2 100644 --- a/src/types/dashboard/provider.ts +++ b/src/types/dashboard/provider.ts @@ -1,10 +1,22 @@ /* API 플랫폼 코드 목록 */ export const PROVIDER_TYPES = ["GOOGLE", "NAVER", "META"] as const; + /* 단일 플랫폼 (GOOGLE | NAVER | META) */ export type TProviderType = (typeof PROVIDER_TYPES)[number]; + /* AI 분석용 — 플랫폼 하나 또는 통합(ALL) */ export type TAiAnalysisProvider = TProviderType | "ALL"; +/* ROAS 순위 항목 */ +export interface IRoasRanking { + rank: number; + provider: string; + roas: number; + diffRate: number | null; + revenue: number; + adSpend: number; +} + /* 화면에 보이는 이름 */ export const PLATFORM_MAP: Record = { GOOGLE: "Google", From 8a844160467310d5d6f5d244287603e818f7fab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9C=EC=A0=9C=EA=B2=BD?= Date: Wed, 17 Jun 2026 15:11:45 +0900 Subject: [PATCH 14/88] =?UTF-8?q?refactor:=20=EA=B3=B5=ED=86=B5=20dashboar?= =?UTF-8?q?d=20=ED=83=80=EC=9E=85=EC=9D=84=20common.ts=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IMetricsResponse, IRoasRanking을 common.ts 단일 출처로 이동 IPlatformPerformance는 IMetricsResponse를 extend해 중복 필드 제거 overview.ts, platform.ts는 common.ts에서 re-export --- src/types/dashboard/common.ts | 21 +++++++++++++++++++++ src/types/dashboard/overview.ts | 16 ++-------------- src/types/dashboard/platform.ts | 13 +++---------- src/types/dashboard/provider.ts | 10 ---------- 4 files changed, 26 insertions(+), 34 deletions(-) create mode 100644 src/types/dashboard/common.ts diff --git a/src/types/dashboard/common.ts b/src/types/dashboard/common.ts new file mode 100644 index 00000000..146f1add --- /dev/null +++ b/src/types/dashboard/common.ts @@ -0,0 +1,21 @@ +// 공통 지표 응답 (overview/platform 공유) +export interface IMetricsResponse { + clicks: number; + clickChangeRate: number; + impressions: number; + impressionChangeRate: number; + conversion: number; + cvrChangeRate: number; + ROAS: number; + ROASChangeRate: number; +} + +// ROAS 순위 항목 +export interface IRoasRanking { + rank: number; + provider: string; + roas: number; + diffRate: number | null; + revenue: number; + adSpend: number; +} diff --git a/src/types/dashboard/overview.ts b/src/types/dashboard/overview.ts index fe6e0e15..149b1ca6 100644 --- a/src/types/dashboard/overview.ts +++ b/src/types/dashboard/overview.ts @@ -1,19 +1,7 @@ -import type { IRoasRanking } from "./provider"; +import type { IMetricsResponse, IRoasRanking } from "./common"; +export type { IMetricsResponse, IRoasRanking }; export type { TProviderType } from "./provider"; -export type { IRoasRanking }; - -// 전체 지표 집계 응답 -export interface IMetricsResponse { - clicks: number; - clickChangeRate: number; - impressions: number; - impressionChangeRate: number; - conversion: number; - cvrChangeRate: number; - ROAS: number; - ROASChangeRate: number; -} // 예산 집계 응답 export interface IBudgetsResponse { diff --git a/src/types/dashboard/platform.ts b/src/types/dashboard/platform.ts index 6d7fb0ed..7f894f40 100644 --- a/src/types/dashboard/platform.ts +++ b/src/types/dashboard/platform.ts @@ -1,7 +1,8 @@ +import type { IMetricsResponse } from "./common"; import type { TProviderType } from "./provider"; +export type { IMetricsResponse, IRoasRanking } from "./common"; export type { TProviderType }; -export type { IRoasRanking } from "./provider"; // 플랫폼 광고 소재 개수 export interface IAdCount { @@ -24,16 +25,8 @@ export interface IAdCountParams { } // 플랫폼별 성과 -export interface IPlatformPerformance { +export interface IPlatformPerformance extends IMetricsResponse { provider: TProviderType; - clicks: number; - clickChangeRate: number; - impressions: number; - impressionChangeRate: number; - conversion: number; - cvrChangeRate: number; - ROAS: number; - ROASChangeRate: number; } // 플랫폼별 성과 효율 비교 diff --git a/src/types/dashboard/provider.ts b/src/types/dashboard/provider.ts index 3931c4b2..2c94e276 100644 --- a/src/types/dashboard/provider.ts +++ b/src/types/dashboard/provider.ts @@ -7,16 +7,6 @@ export type TProviderType = (typeof PROVIDER_TYPES)[number]; /* AI 분석용 — 플랫폼 하나 또는 통합(ALL) */ export type TAiAnalysisProvider = TProviderType | "ALL"; -/* ROAS 순위 항목 */ -export interface IRoasRanking { - rank: number; - provider: string; - roas: number; - diffRate: number | null; - revenue: number; - adSpend: number; -} - /* 화면에 보이는 이름 */ export const PLATFORM_MAP: Record = { GOOGLE: "Google", From 332bef639c7a584769f6f84a22e516c997b8f4e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9C=EC=A0=9C=EA=B2=BD?= Date: Wed, 17 Jun 2026 15:16:27 +0900 Subject: [PATCH 15/88] =?UTF-8?q?refactor:=20IBudgetsResponse/IBudgetStatu?= =?UTF-8?q?s=EB=A5=BC=20IBudgetResponse=EB=A1=9C=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 중복된 예산 타입 2개를 common.ts의 IBudgetResponse 단일 타입으로 통합 소비 파일(api, mock) import명 일괄 수정 --- src/api/dashboard/overview.ts | 6 +++--- .../dashboard/platform/platformDashboard.mock.ts | 4 ++-- src/types/dashboard/common.ts | 9 +++++++++ src/types/dashboard/overview.ts | 13 ++----------- src/types/dashboard/platform.ts | 11 +---------- 5 files changed, 17 insertions(+), 26 deletions(-) diff --git a/src/api/dashboard/overview.ts b/src/api/dashboard/overview.ts index d5208be3..67900223 100644 --- a/src/api/dashboard/overview.ts +++ b/src/api/dashboard/overview.ts @@ -1,6 +1,6 @@ import type { ICommonResponse } from "@/types/common/common"; import type { - IBudgetsResponse, + IBudgetResponse, IMetricsResponse, IRoasRankingsParams, IRoasRankingsResponse, @@ -25,8 +25,8 @@ export const getOverview = async ( export const getBudget = async ( orgId: number, providerType?: TProviderType, -): Promise => { - const { data } = await axiosInstance.get>( +): Promise => { + const { data } = await axiosInstance.get>( `/api/dashboard/budgets`, { params: { orgId, ...(providerType ? { providerType } : {}) } }, ); diff --git a/src/pages/dashboard/platform/platformDashboard.mock.ts b/src/pages/dashboard/platform/platformDashboard.mock.ts index d5a5f72d..28d4ca28 100644 --- a/src/pages/dashboard/platform/platformDashboard.mock.ts +++ b/src/pages/dashboard/platform/platformDashboard.mock.ts @@ -1,5 +1,5 @@ import type { - IBudgetStatus, + IBudgetResponse, IPlatformPerformance, IRoasRanking, } from "@/types/dashboard/platform"; @@ -70,7 +70,7 @@ export const performanceEfficiencyMock: IPlatformPerformance[] = [ ]; // 예산 소진 현황 -export const budgetStatusMock: IBudgetStatus[] = [ +export const budgetStatusMock: IBudgetResponse[] = [ { providerType: "GOOGLE", usagePercentage: 0.75, diff --git a/src/types/dashboard/common.ts b/src/types/dashboard/common.ts index 146f1add..a5c97f92 100644 --- a/src/types/dashboard/common.ts +++ b/src/types/dashboard/common.ts @@ -10,6 +10,15 @@ export interface IMetricsResponse { ROASChangeRate: number; } +// 예산 소진 현황 +export interface IBudgetResponse { + providerType: string; + usagePercentage: number; + totalBudget: number; + totalSpend: number; + remainingBudget: number; +} + // ROAS 순위 항목 export interface IRoasRanking { rank: number; diff --git a/src/types/dashboard/overview.ts b/src/types/dashboard/overview.ts index 149b1ca6..ab084707 100644 --- a/src/types/dashboard/overview.ts +++ b/src/types/dashboard/overview.ts @@ -1,17 +1,8 @@ -import type { IMetricsResponse, IRoasRanking } from "./common"; +import type { IBudgetResponse, IMetricsResponse, IRoasRanking } from "./common"; -export type { IMetricsResponse, IRoasRanking }; +export type { IBudgetResponse, IMetricsResponse, IRoasRanking }; export type { TProviderType } from "./provider"; -// 예산 집계 응답 -export interface IBudgetsResponse { - providerType: string; - usagePercentage: number; - totalBudget: number; - totalSpend: number; - remainingBudget: number; -} - // ROAS 순위 조회 요청 파라미터 export interface IRoasRankingsParams { startDate?: string; diff --git a/src/types/dashboard/platform.ts b/src/types/dashboard/platform.ts index 7f894f40..22051e60 100644 --- a/src/types/dashboard/platform.ts +++ b/src/types/dashboard/platform.ts @@ -1,7 +1,7 @@ import type { IMetricsResponse } from "./common"; import type { TProviderType } from "./provider"; -export type { IMetricsResponse, IRoasRanking } from "./common"; +export type { IBudgetResponse, IMetricsResponse, IRoasRanking } from "./common"; export type { TProviderType }; // 플랫폼 광고 소재 개수 @@ -35,15 +35,6 @@ export interface IPlatformEfficiencyData { data: IPlatformPerformance[]; } -// 예산 소진 현황 -export interface IBudgetStatus { - providerType: string; - usagePercentage: number; - totalBudget: number; - totalSpend: number; - remainingBudget: number; -} - // API 일자별·합계 export interface IMetricFactsRow { date: string; From e46b561d1e30cc5d76911b49a779b004eea47c18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9C=EC=A0=9C=EA=B2=BD?= Date: Wed, 17 Jun 2026 15:25:39 +0900 Subject: [PATCH 16/88] =?UTF-8?q?refactor:=20useOverviewBudget/usePlatform?= =?UTF-8?q?Budget=EB=A5=BC=20useBudget=EC=9C=BC=EB=A1=9C=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit provider 유무로 query key 분기해 캐시 네임스페이스 유지 중복된 상수(WARNING/DANGER_THRESHOLD)와 select 로직 단일화 --- .../dashboard/platform/SinglePlatformView.tsx | 4 +-- .../{useOverviewBudget.ts => useBudget.ts} | 14 ++++++---- src/hooks/dashboard/usePlatformBudget.ts | 28 ------------------- .../dashboard/overview/OverviewDashboard.tsx | 4 +-- .../sections/OverviewBudgetSection.tsx | 4 +-- 5 files changed, 15 insertions(+), 39 deletions(-) rename src/hooks/dashboard/{useOverviewBudget.ts => useBudget.ts} (57%) delete mode 100644 src/hooks/dashboard/usePlatformBudget.ts diff --git a/src/components/dashboard/platform/SinglePlatformView.tsx b/src/components/dashboard/platform/SinglePlatformView.tsx index af89a9ad..1ab38afb 100644 --- a/src/components/dashboard/platform/SinglePlatformView.tsx +++ b/src/components/dashboard/platform/SinglePlatformView.tsx @@ -4,7 +4,7 @@ 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 { useBudget } from "@/hooks/dashboard/useBudget"; import { usePlatformMetricFacts } from "@/hooks/dashboard/usePlatformMetricFacts"; import { usePlatformMetrics } from "@/hooks/dashboard/usePlatformMetrics"; @@ -90,7 +90,7 @@ export default function SinglePlatformView({ data: budget, isLoading: isBudgetLoading, isError: isBudgetError, - } = usePlatformBudget(platform); + } = useBudget(platform); const { data: metricFacts, diff --git a/src/hooks/dashboard/useOverviewBudget.ts b/src/hooks/dashboard/useBudget.ts similarity index 57% rename from src/hooks/dashboard/useOverviewBudget.ts rename to src/hooks/dashboard/useBudget.ts index 239709f9..65308645 100644 --- a/src/hooks/dashboard/useOverviewBudget.ts +++ b/src/hooks/dashboard/useBudget.ts @@ -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, diff --git a/src/hooks/dashboard/usePlatformBudget.ts b/src/hooks/dashboard/usePlatformBudget.ts deleted file mode 100644 index f8af08bb..00000000 --- a/src/hooks/dashboard/usePlatformBudget.ts +++ /dev/null @@ -1,28 +0,0 @@ -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 usePlatformBudget(provider: TProviderType) { - const orgId = useWorkspaceStore((s) => s.selectedOrgId); - - return useCoreQuery( - ["platform", "budget", orgId, provider], - () => getBudget(orgId!, provider), - { - enabled: !!orgId && !!provider, - select: (data) => ({ - totalBudget: data.totalBudget, - spent: data.totalSpend, - warningThreshold: WARNING_THRESHOLD, - dangerThreshold: DANGER_THRESHOLD, - }), - }, - ); -} diff --git a/src/pages/dashboard/overview/OverviewDashboard.tsx b/src/pages/dashboard/overview/OverviewDashboard.tsx index fb28c120..80254208 100644 --- a/src/pages/dashboard/overview/OverviewDashboard.tsx +++ b/src/pages/dashboard/overview/OverviewDashboard.tsx @@ -1,6 +1,6 @@ import { useNavigate } from "react-router-dom"; -import { useOverviewBudget } from "@/hooks/dashboard/useOverviewBudget"; +import { useBudget } from "@/hooks/dashboard/useBudget"; import { useOverviewMetrics } from "@/hooks/dashboard/useOverviewMetrics"; import { useOverviewRoasRankings } from "@/hooks/dashboard/useOverviewRoasRankings"; @@ -25,7 +25,7 @@ export default function OverviewDashboard() { isLoading: isBudgetLoading, isError: isBudgetError, error: budgetError, - } = useOverviewBudget(); + } = useBudget(); const { data: roasRankingsData, isLoading: isRankingsLoading, diff --git a/src/pages/dashboard/overview/sections/OverviewBudgetSection.tsx b/src/pages/dashboard/overview/sections/OverviewBudgetSection.tsx index 99e846c2..0953aabb 100644 --- a/src/pages/dashboard/overview/sections/OverviewBudgetSection.tsx +++ b/src/pages/dashboard/overview/sections/OverviewBudgetSection.tsx @@ -1,6 +1,6 @@ import type { IApiErrorResponse } from "@/types/common/common"; -import type { useOverviewBudget } from "@/hooks/dashboard/useOverviewBudget"; +import type { useBudget } from "@/hooks/dashboard/useBudget"; import Badge from "@/components/common/badge/Badge"; import Card from "@/components/common/card/Card"; @@ -28,7 +28,7 @@ export function OverviewBudgetSection({ budgetError, budgetStatus, }: { - budget: ReturnType["data"]; + budget: ReturnType["data"]; isBudgetLoading: boolean; isBudgetError: boolean; budgetError: IApiErrorResponse | null; From 4cd18927448d29b295b68758734582309a1b97dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9C=EC=A0=9C=EA=B2=BD?= Date: Wed, 17 Jun 2026 15:33:38 +0900 Subject: [PATCH 17/88] =?UTF-8?q?refactor:=20KPI=20=EB=B3=80=ED=99=98=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=EC=9D=84=20metricsToKpis=20=EC=9C=A0?= =?UTF-8?q?=ED=8B=B8=EB=A1=9C=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit useOverviewMetrics의 toKpis, SinglePlatformView의 인라인 변환 제거 타이틀 문자열과 소수점(2자리) 통일 --- .../dashboard/platform/SinglePlatformView.tsx | 39 +++------------ src/hooks/dashboard/useOverviewMetrics.ts | 48 +------------------ src/utils/dashboard/metricsToKpis.ts | 42 ++++++++++++++++ 3 files changed, 51 insertions(+), 78 deletions(-) create mode 100644 src/utils/dashboard/metricsToKpis.ts diff --git a/src/components/dashboard/platform/SinglePlatformView.tsx b/src/components/dashboard/platform/SinglePlatformView.tsx index 1ab38afb..39588700 100644 --- a/src/components/dashboard/platform/SinglePlatformView.tsx +++ b/src/components/dashboard/platform/SinglePlatformView.tsx @@ -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 { 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"; @@ -52,37 +54,10 @@ 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]; diff --git a/src/hooks/dashboard/useOverviewMetrics.ts b/src/hooks/dashboard/useOverviewMetrics.ts index f61c249b..e3b44840 100644 --- a/src/hooks/dashboard/useOverviewMetrics.ts +++ b/src/hooks/dashboard/useOverviewMetrics.ts @@ -1,54 +1,10 @@ -import type { IMetricsResponse } from "@/types/dashboard/overview"; +import { metricsToKpis } from "@/utils/dashboard/metricsToKpis"; import { useCoreQuery } from "@/hooks/customQuery"; -import type { IStatCardProps } from "@/components/common/card/StatCard"; - import { getOverview } from "@/api/dashboard/overview"; import useWorkspaceStore from "@/store/useWorkspaceStore"; -// 변화율 퍼센트 문자열로 변환 -const toRate = (rate: number) => `${Math.abs(rate).toFixed(1)}%`; - -// API 응답 KPI 카드 형식으로 변환 -function toKpis(metrics: IMetricsResponse): IStatCardProps[] { - return [ - { - title: "클릭수", - value: metrics.clicks.toLocaleString(), - trend: { - direction: metrics.clickChangeRate >= 0 ? "up" : "down", - value: toRate(metrics.clickChangeRate), - }, - }, - { - title: "노출수", - value: metrics.impressions.toLocaleString(), - trend: { - direction: metrics.impressionChangeRate >= 0 ? "up" : "down", - value: toRate(metrics.impressionChangeRate), - }, - }, - { - title: "전환율", - value: `${metrics.conversion.toFixed(1)}%`, - trend: { - direction: metrics.cvrChangeRate >= 0 ? "up" : "down", - value: toRate(metrics.cvrChangeRate), - }, - }, - { - title: "ROAS", - value: `${metrics.ROAS.toFixed(1)}%`, - trend: { - direction: metrics.ROASChangeRate >= 0 ? "up" : "down", - value: toRate(metrics.ROASChangeRate), - }, - }, - ]; -} - -// 통합 지표 조회 export function useOverviewMetrics() { const orgId = useWorkspaceStore((s) => s.selectedOrgId); @@ -57,7 +13,7 @@ export function useOverviewMetrics() { () => getOverview(orgId!), { enabled: !!orgId, - select: toKpis, + select: metricsToKpis, }, ); } diff --git a/src/utils/dashboard/metricsToKpis.ts b/src/utils/dashboard/metricsToKpis.ts new file mode 100644 index 00000000..20a233fc --- /dev/null +++ b/src/utils/dashboard/metricsToKpis.ts @@ -0,0 +1,42 @@ +import type { IMetricsResponse } from "@/types/dashboard/common"; + +import type { IStatCardProps } from "@/components/common/card/StatCard"; + +const toRate = (rate: number) => `${Math.abs(rate).toFixed(2)}%`; + +export function metricsToKpis(metrics: IMetricsResponse): IStatCardProps[] { + return [ + { + title: "클릭수", + value: metrics.clicks.toLocaleString(), + trend: { + direction: metrics.clickChangeRate >= 0 ? "up" : "down", + value: toRate(metrics.clickChangeRate), + }, + }, + { + title: "노출수", + value: metrics.impressions.toLocaleString(), + trend: { + direction: metrics.impressionChangeRate >= 0 ? "up" : "down", + value: toRate(metrics.impressionChangeRate), + }, + }, + { + title: "전환율", + value: `${metrics.conversion.toFixed(2)}%`, + trend: { + direction: metrics.cvrChangeRate >= 0 ? "up" : "down", + value: toRate(metrics.cvrChangeRate), + }, + }, + { + title: "ROAS", + value: `${metrics.ROAS.toFixed(2)}%`, + trend: { + direction: metrics.ROASChangeRate >= 0 ? "up" : "down", + value: toRate(metrics.ROASChangeRate), + }, + }, + ]; +} From b47597ba7164c175a65e8aa428a44080a72e09f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9C=EC=A0=9C=EA=B2=BD?= Date: Wed, 17 Jun 2026 15:39:41 +0900 Subject: [PATCH 18/88] =?UTF-8?q?refactor:=20=ED=94=8C=EB=9E=AB=ED=8F=BC?= =?UTF-8?q?=20circle=20=EB=A1=9C=EA=B3=A0=20=EB=A7=B5=EC=9D=84=20PLATFORM?= =?UTF-8?q?=5FCIRCLE=5FLOGO=5FMAP=20=EC=83=81=EC=88=98=EB=A1=9C=20?= =?UTF-8?q?=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PlatformRoasTable, TopPerformanceList의 중복 로고 선언 제거 각 컴포넌트는 공통 상수에서 컴포넌트 레퍼런스를 가져와 고유 className 적용 --- .../dashboard/platform/PlatformRoasTable.tsx | 18 ++++------ .../dashboard/platform/TopPerformanceList.tsx | 33 ++++++++----------- src/constants/dashboard/platformLogos.tsx | 16 +++++++++ 3 files changed, 35 insertions(+), 32 deletions(-) create mode 100644 src/constants/dashboard/platformLogos.tsx diff --git a/src/components/dashboard/platform/PlatformRoasTable.tsx b/src/components/dashboard/platform/PlatformRoasTable.tsx index bab1ab84..de5b7e91 100644 --- a/src/components/dashboard/platform/PlatformRoasTable.tsx +++ b/src/components/dashboard/platform/PlatformRoasTable.tsx @@ -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 = { - GOOGLE: , - NAVER: , - META: , -}; - function toProviderType(provider: string): TProviderType | null { const key = provider.toUpperCase(); if (key in PLATFORM_MAP) return key as TProviderType; @@ -31,7 +22,10 @@ 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 ; + } const name = getDisplayName(provider); return ( diff --git a/src/components/dashboard/platform/TopPerformanceList.tsx b/src/components/dashboard/platform/TopPerformanceList.tsx index 7950089a..ace4dfb6 100644 --- a/src/components/dashboard/platform/TopPerformanceList.tsx +++ b/src/components/dashboard/platform/TopPerformanceList.tsx @@ -1,37 +1,28 @@ -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_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"; - interface ITopPerformanceListProps { rankings: IRoasRanking[]; } -const PlatformInfo: Record = { - GOOGLE: { - name: PLATFORM_MAP.GOOGLE, - logo: , - }, - NAVER: { name: PLATFORM_MAP.NAVER, logo: }, - META: { name: PLATFORM_MAP.META, logo: }, -}; - export const TopPerformanceList = memo(function TopPerformanceList({ rankings, }: ITopPerformanceListProps) { return (
{rankings.map((item) => { - const info = PlatformInfo[item.provider] || { - name: item.provider, - logo: null, - }; + const Logo = + PLATFORM_CIRCLE_LOGO_MAP[ + item.provider as keyof typeof PLATFORM_CIRCLE_LOGO_MAP + ]; + const name = + PLATFORM_MAP[item.provider as keyof typeof PLATFORM_MAP] ?? + item.provider; return (
@@ -39,9 +30,11 @@ export const TopPerformanceList = memo(function TopPerformanceList({ {item.rank} -
{info.logo}
+
+ {Logo && } +
- {info.name} + {name}
diff --git a/src/constants/dashboard/platformLogos.tsx b/src/constants/dashboard/platformLogos.tsx new file mode 100644 index 00000000..b78a0593 --- /dev/null +++ b/src/constants/dashboard/platformLogos.tsx @@ -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> +> = { + GOOGLE: GoogleLogo, + NAVER: NaverLogo, + META: MetaLogo, +}; From a31f4c4688544e60208508a27675e6cd962c0a68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9C=EC=A0=9C=EA=B2=BD?= Date: Wed, 17 Jun 2026 15:41:06 +0900 Subject: [PATCH 19/88] =?UTF-8?q?style:=20=ED=94=8C=EB=9E=AB=ED=8F=BC=20?= =?UTF-8?q?=EB=A1=9C=EA=B3=A0=20=ED=81=AC=EA=B8=B0=20h-8=20w-8=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/dashboard/platform/PlatformRoasTable.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/dashboard/platform/PlatformRoasTable.tsx b/src/components/dashboard/platform/PlatformRoasTable.tsx index de5b7e91..0310c5be 100644 --- a/src/components/dashboard/platform/PlatformRoasTable.tsx +++ b/src/components/dashboard/platform/PlatformRoasTable.tsx @@ -24,11 +24,11 @@ function getPlatformLogo(provider: string) { const key = toProviderType(provider); if (key) { const Logo = PLATFORM_CIRCLE_LOGO_MAP[key]; - return ; + return ; } const name = getDisplayName(provider); return ( - + {name[0]} ); From 9f0f161b01c90f78598696db8322b228ddf59b3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9C=EC=A0=9C=EA=B2=BD?= Date: Wed, 17 Jun 2026 15:42:45 +0900 Subject: [PATCH 20/88] =?UTF-8?q?style:=20rounded-[24px]=EB=A5=BC=20Tailwi?= =?UTF-8?q?nd=20=ED=86=A0=ED=81=B0=20rounded-3xl=EB=A1=9C=20=EA=B5=90?= =?UTF-8?q?=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/dashboard/platform/SinglePlatformView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/dashboard/platform/SinglePlatformView.tsx b/src/components/dashboard/platform/SinglePlatformView.tsx index 39588700..6c650a23 100644 --- a/src/components/dashboard/platform/SinglePlatformView.tsx +++ b/src/components/dashboard/platform/SinglePlatformView.tsx @@ -108,7 +108,7 @@ export default function SinglePlatformView({ Array.from({ length: 4 }).map((_, i) => (
From 2feeb92381134892ed9a6b30f71742444f46dd1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9C=EC=A0=9C=EA=B2=BD?= Date: Wed, 17 Jun 2026 15:45:53 +0900 Subject: [PATCH 21/88] =?UTF-8?q?refactor:=20minute=20=EB=AC=B8=EC=9E=90?= =?UTF-8?q?=EC=97=B4=20=ED=8C=8C=EC=8B=B1=20=EB=A1=9C=EC=A7=81=EC=9D=84=20?= =?UTF-8?q?parseMinuteToTimestamp=20=EC=9C=A0=ED=8B=B8=EB=A1=9C=20?= =?UTF-8?q?=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TrafficChart, PlatformTrafficChart의 YYYYMMDDHHmm → timestamp 변환 중복 제거 --- .../dashboard/charts/TrafficChart.tsx | 16 +++++++--------- .../dashboard/platform/PlatformTrafficChart.tsx | 17 ++++++----------- src/utils/dashboard/parseMinuteToTimestamp.ts | 9 +++++++++ 3 files changed, 22 insertions(+), 20 deletions(-) create mode 100644 src/utils/dashboard/parseMinuteToTimestamp.ts diff --git a/src/components/dashboard/charts/TrafficChart.tsx b/src/components/dashboard/charts/TrafficChart.tsx index 54676bbd..cbe0ab79 100644 --- a/src/components/dashboard/charts/TrafficChart.tsx +++ b/src/components/dashboard/charts/TrafficChart.tsx @@ -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"; @@ -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 (데이터 없으면 오늘) diff --git a/src/components/dashboard/platform/PlatformTrafficChart.tsx b/src/components/dashboard/platform/PlatformTrafficChart.tsx index 80854cb7..2ec7e2ed 100644 --- a/src/components/dashboard/platform/PlatformTrafficChart.tsx +++ b/src/components/dashboard/platform/PlatformTrafficChart.tsx @@ -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"; @@ -23,17 +25,10 @@ const PlatformTrafficChart = memo(function PlatformTrafficChart({ // 데이터 변환: 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분) diff --git a/src/utils/dashboard/parseMinuteToTimestamp.ts b/src/utils/dashboard/parseMinuteToTimestamp.ts new file mode 100644 index 00000000..d1de3aaa --- /dev/null +++ b/src/utils/dashboard/parseMinuteToTimestamp.ts @@ -0,0 +1,9 @@ +// 'YYYYMMDDHHmm' 형태 문자열을 로컬 타임스탬프(ms)로 변환 +export function parseMinuteToTimestamp(minute: string): number { + const year = parseInt(minute.slice(0, 4), 10); + const month = parseInt(minute.slice(4, 6), 10) - 1; + const day = parseInt(minute.slice(6, 8), 10); + const hour = parseInt(minute.slice(8, 10), 10); + const min = parseInt(minute.slice(10, 12), 10); + return new Date(year, month, day, hour, min).getTime(); +} From c25d9cd92f78edcdf02af80450517ef525abb9fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9C=EC=A0=9C=EA=B2=BD?= Date: Wed, 17 Jun 2026 15:48:01 +0900 Subject: [PATCH 22/88] =?UTF-8?q?style:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=A3=BC=EC=84=9D=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/dashboard/platform/PlatformTrafficChart.tsx | 1 - src/utils/dashboard/parseMinuteToTimestamp.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/dashboard/platform/PlatformTrafficChart.tsx b/src/components/dashboard/platform/PlatformTrafficChart.tsx index 2ec7e2ed..be5dfe52 100644 --- a/src/components/dashboard/platform/PlatformTrafficChart.tsx +++ b/src/components/dashboard/platform/PlatformTrafficChart.tsx @@ -22,7 +22,6 @@ const PlatformTrafficChart = memo(function PlatformTrafficChart({ platform, isLoading, }: IPlatformTrafficChartProps) { - // 데이터 변환: minute 문자열 -> 타임스탬프 const seriesData = useMemo(() => { if (!data) return []; return data.timeSeriesData.map((d) => ({ diff --git a/src/utils/dashboard/parseMinuteToTimestamp.ts b/src/utils/dashboard/parseMinuteToTimestamp.ts index d1de3aaa..db6ae9a8 100644 --- a/src/utils/dashboard/parseMinuteToTimestamp.ts +++ b/src/utils/dashboard/parseMinuteToTimestamp.ts @@ -1,4 +1,4 @@ -// 'YYYYMMDDHHmm' 형태 문자열을 로컬 타임스탬프(ms)로 변환 +// 문자열을 로컬 타임스탬프로 변환 export function parseMinuteToTimestamp(minute: string): number { const year = parseInt(minute.slice(0, 4), 10); const month = parseInt(minute.slice(4, 6), 10) - 1; From d5334cc789237b8e36651ab8dcafbc316344b19b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9C=EC=A0=9C=EA=B2=BD?= Date: Wed, 17 Jun 2026 15:55:06 +0900 Subject: [PATCH 23/88] =?UTF-8?q?refactor:=20AllPlatformTrafficChart=20par?= =?UTF-8?q?seMinuteToTimestamp=20=EC=A0=81=EC=9A=A9,=20usePlatformRoasRank?= =?UTF-8?q?ings=20=EB=82=A0=EC=A7=9C=20=EC=83=81=EC=88=98=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AllPlatformTrafficChart의 인라인 파싱 로직을 parseMinuteToTimestamp 유틸로 교체 usePlatformRoasRankings의 하드코딩 날짜를 OVERVIEW_DAILY_METRICS_RANGE 상수로 교체 --- .../platform/AllPlatformTrafficChart.tsx | 17 ++++++----------- src/hooks/dashboard/usePlatformRoasRankings.ts | 7 ++----- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/src/components/dashboard/platform/AllPlatformTrafficChart.tsx b/src/components/dashboard/platform/AllPlatformTrafficChart.tsx index 0fd8f9e1..05ca96f8 100644 --- a/src/components/dashboard/platform/AllPlatformTrafficChart.tsx +++ b/src/components/dashboard/platform/AllPlatformTrafficChart.tsx @@ -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"; @@ -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, + })), }; }); }, []); diff --git a/src/hooks/dashboard/usePlatformRoasRankings.ts b/src/hooks/dashboard/usePlatformRoasRankings.ts index a32b12ec..8257aa79 100644 --- a/src/hooks/dashboard/usePlatformRoasRankings.ts +++ b/src/hooks/dashboard/usePlatformRoasRankings.ts @@ -1,4 +1,5 @@ import type { IRoasRanking } from "@/types/dashboard/overview"; +import { OVERVIEW_DAILY_METRICS_RANGE } from "@/constants/dashboard/overviewMetricsRange"; import { useCoreQuery } from "@/hooks/customQuery"; @@ -10,11 +11,7 @@ export function usePlatformRoasRankings() { const orgId = useWorkspaceStore((s) => s.selectedOrgId); return useCoreQuery( ["platform", "roasRankings", orgId], - () => - getRoasRankings(orgId!, { - startDate: "2026-01-22", - endDate: "2026-03-22", - }), + () => getRoasRankings(orgId!, OVERVIEW_DAILY_METRICS_RANGE), { enabled: !!orgId, select: (data): IRoasRanking[] => data.rankings, From 1d2b58639a015660898ad566c0f9ead8cada5168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9C=EC=A0=9C=EA=B2=BD?= Date: Wed, 17 Jun 2026 16:51:28 +0900 Subject: [PATCH 24/88] =?UTF-8?q?fix:=20provider=20=ED=82=A4=20=EC=A0=95?= =?UTF-8?q?=EA=B7=9C=ED=99=94=20=EB=B0=8F=20parseMinuteToTimestamp=20?= =?UTF-8?q?=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=A6=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80,=20=EC=9E=A5=EC=8B=9D=EC=9A=A9=20=EB=A1=9C=EA=B3=A0?= =?UTF-8?q?=20aria-hidden=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/platform/PlatformRoasTable.tsx | 7 +++++-- .../dashboard/platform/TopPerformanceList.tsx | 20 ++++++++++--------- src/utils/dashboard/parseMinuteToTimestamp.ts | 13 +++++++++--- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/components/dashboard/platform/PlatformRoasTable.tsx b/src/components/dashboard/platform/PlatformRoasTable.tsx index 0310c5be..bd91c85e 100644 --- a/src/components/dashboard/platform/PlatformRoasTable.tsx +++ b/src/components/dashboard/platform/PlatformRoasTable.tsx @@ -24,11 +24,14 @@ function getPlatformLogo(provider: string) { const key = toProviderType(provider); if (key) { const Logo = PLATFORM_CIRCLE_LOGO_MAP[key]; - return ; + return