From 11e52883f416b8954aca74e66ee23c6d71b90179 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9C=EC=A0=9C=EA=B2=BD?= Date: Sat, 21 Mar 2026 19:59:45 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EA=B4=91=EA=B3=A0=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20store=20=EC=82=AC=EC=9A=A9=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20orgId=20=EA=B4=80=EB=A6=AC=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/dashboard/overview.ts | 12 ++++ src/api/dashboard/platform.ts | 18 ++++++ src/api/workspace/org.ts | 24 ++++--- src/hooks/ads/useCampaignDetail.ts | 44 +++---------- src/hooks/dashboard/useOverviewMetrics.ts | 58 +++++++++++++++++ src/layout/main/MainLayout.tsx | 12 ++++ src/pages/ads/list/AdsListPage.tsx | 63 +++++-------------- src/pages/ads/list/CampaignDetail.tsx | 2 +- .../dashboard/overview/OverviewDashboard.tsx | 7 ++- src/pages/dashboard/overview/overview.mock.ts | 26 -------- src/store/useWorkspaceStore.ts | 13 ++++ src/types/dashboard/overview.ts | 22 +++---- src/types/workspace/workspace.ts | 4 -- 13 files changed, 161 insertions(+), 144 deletions(-) create mode 100644 src/api/dashboard/overview.ts create mode 100644 src/api/dashboard/platform.ts create mode 100644 src/hooks/dashboard/useOverviewMetrics.ts delete mode 100644 src/pages/dashboard/overview/overview.mock.ts create mode 100644 src/store/useWorkspaceStore.ts diff --git a/src/api/dashboard/overview.ts b/src/api/dashboard/overview.ts new file mode 100644 index 00000000..474e457f --- /dev/null +++ b/src/api/dashboard/overview.ts @@ -0,0 +1,12 @@ +import type { ICommonResponse } from "@/types/common/common"; +import type { IMetricsResponse } from "@/types/dashboard/overview"; + +import { axiosInstance } from "@/lib/axiosInstance"; + +// 대시보드 - 전체 지표 집계 API +export const getOverview = async (orgId: number): Promise => { + const { data } = await axiosInstance.get>( + `/api/dashboard/${orgId}/metrics`, + ); + return data.data; +}; diff --git a/src/api/dashboard/platform.ts b/src/api/dashboard/platform.ts new file mode 100644 index 00000000..45fdc33c --- /dev/null +++ b/src/api/dashboard/platform.ts @@ -0,0 +1,18 @@ +import type { ICommonResponse } from "@/types/common/common"; +import type { IMetricsResponse } from "@/types/dashboard/overview"; + +import { axiosInstance } from "@/lib/axiosInstance"; + +// 대시보드 - 각 플랫폼 별 지표 집계 +export const getOverview = async ( + orgId: number, + providerType: "KAKAO" | "NAVER" | "GOOGLE", +): Promise => { + const { data } = await axiosInstance.get>( + `/api/dashboard/${orgId}/metrics`, + { + params: { providerType }, + }, + ); + return data.data; +}; diff --git a/src/api/workspace/org.ts b/src/api/workspace/org.ts index 7f1f8d56..9bcf46c5 100644 --- a/src/api/workspace/org.ts +++ b/src/api/workspace/org.ts @@ -1,5 +1,5 @@ +import type { ICommonResponse } from "@/types/common/common"; import { - type TApiResult, type TCreateOrgRequest, type TCreateOrgResponse, type TGetOrgResponse, @@ -14,24 +14,23 @@ import { axiosInstance } from "@/lib/axiosInstance"; export const getMyWorkspaces = async (): Promise => { const { data } = - await axiosInstance.get>("/api/org/my"); + await axiosInstance.get>("/api/org/my"); return data.data.organizations; }; export const createWorkspace = async ( body: TCreateOrgRequest, ): Promise => { - const { data } = await axiosInstance.post>( - "/api/org/create", - body, - ); + const { data } = await axiosInstance.post< + ICommonResponse + >("/api/org/create", body); return data.data; }; export const getWorkspace = async ( orgId: number, ): Promise => { - const { data } = await axiosInstance.get>( + const { data } = await axiosInstance.get>( `/api/org/${orgId}`, ); @@ -42,19 +41,18 @@ export const updateWorkspace = async ( orgId: number, body: TUpdateWorkspaceRequest, ): Promise => { - await axiosInstance.patch>(`/api/org/${orgId}`, body); + await axiosInstance.patch>(`/api/org/${orgId}`, body); }; export const deleteWorkspace = async (orgId: number): Promise => { - await axiosInstance.delete>(`/api/org/${orgId}`); + await axiosInstance.delete>(`/api/org/${orgId}`); }; export const uploadImage = async (file: File): Promise => { const formData = new FormData(); formData.append("image", file); - const { data } = await axiosInstance.post>( - `/api/images/upload`, - formData, - ); + const { data } = await axiosInstance.post< + ICommonResponse + >(`/api/images/upload`, formData); return data.data.url; }; diff --git a/src/hooks/ads/useCampaignDetail.ts b/src/hooks/ads/useCampaignDetail.ts index b7cd3eb2..f355205c 100644 --- a/src/hooks/ads/useCampaignDetail.ts +++ b/src/hooks/ads/useCampaignDetail.ts @@ -1,43 +1,17 @@ -import { useEffect, useState } from "react"; import { useParams } from "react-router-dom"; -import { toast } from "sonner"; -import type { ICampaignDetail } from "@/types/ads/campaign"; +import { useCoreQuery } from "@/hooks/customQuery"; import { getCampaignDetail } from "@/api/ads/ads"; +import useWorkspaceStore from "@/store/useWorkspaceStore"; -export const useCampaignDetail = (orgId: number | null) => { +export const useCampaignDetail = () => { + const orgId = useWorkspaceStore((s) => s.selectedOrgId); const { projectId } = useParams<{ projectId: string }>(); - const [data, setData] = useState(null); - - const [isLoading, setIsLoading] = useState(true); - - const fetchDetail = async () => { - if (!orgId || !projectId) { - console.log("데이터 부족"); - setIsLoading(false); - return; - } - - try { - setIsLoading(true); - const detailData = await getCampaignDetail(orgId, Number(projectId)); - setData(detailData); - } catch { - toast.error(" 캠페인 상세 정보를 불러오지 못했습니다."); - } finally { - setIsLoading(false); - } - }; - - useEffect(() => { - fetchDetail(); - }, [orgId, projectId]); - - return { - data, - isLoading, - refetch: fetchDetail, - }; + return useCoreQuery( + ["campaignDetail", orgId, projectId], + () => getCampaignDetail(orgId!, Number(projectId)), + { enabled: !!orgId && !!projectId }, + ); }; diff --git a/src/hooks/dashboard/useOverviewMetrics.ts b/src/hooks/dashboard/useOverviewMetrics.ts new file mode 100644 index 00000000..3203c56e --- /dev/null +++ b/src/hooks/dashboard/useOverviewMetrics.ts @@ -0,0 +1,58 @@ +import type { IMetricsResponse } from "@/types/dashboard/overview"; + +import { useCoreQuery } from "@/hooks/customQuery"; + +import type { IStatCardProps } from "@/components/common/card/StatCard"; + +import { getOverview } from "@/api/dashboard/overview"; +import useWorkspaceStore from "@/store/useWorkspaceStore"; + +function toKpis(metrics: IMetricsResponse): IStatCardProps[] { + return [ + { + title: "클릭수", + value: metrics.clicks.toLocaleString(), + trend: { + direction: metrics.clickChangeRate >= 0 ? "up" : "down", + value: `${Math.abs(metrics.clickChangeRate)}%`, + }, + }, + { + title: "노출수", + value: metrics.impressions.toLocaleString(), + trend: { + direction: metrics.impressionChangeRate >= 0 ? "up" : "down", + value: `${Math.abs(metrics.impressionChangeRate)}%`, + }, + }, + { + title: "전환수", + value: metrics.conversion.toLocaleString(), + trend: { + direction: metrics.cvrChangeRate >= 0 ? "up" : "down", + value: `${Math.abs(metrics.cvrChangeRate)}%`, + }, + }, + { + title: "ROAS", + value: `${metrics.ROAS.toLocaleString()}%`, + trend: { + direction: metrics.ROASChangeRate >= 0 ? "up" : "down", + value: `${Math.abs(metrics.ROASChangeRate)}%`, + }, + }, + ]; +} + +export function useOverviewMetrics() { + const orgId = useWorkspaceStore((s) => s.selectedOrgId); + + return useCoreQuery( + ["overview", "metrics", orgId], + () => getOverview(orgId!), + { + enabled: !!orgId, + select: (res) => toKpis(res), + }, + ); +} diff --git a/src/layout/main/MainLayout.tsx b/src/layout/main/MainLayout.tsx index 0c1984c5..26597b93 100644 --- a/src/layout/main/MainLayout.tsx +++ b/src/layout/main/MainLayout.tsx @@ -1,3 +1,4 @@ +import { useEffect } from "react"; import { Outlet } from "react-router-dom"; import { useCoreQuery } from "@/hooks/customQuery"; @@ -5,9 +6,20 @@ import { useCoreQuery } from "@/hooks/customQuery"; import Sidebar from "@/components/Sidebar/Sidebar"; import { getMyInfo } from "@/api/auth/auth"; +import { getMyWorkspaces } from "@/api/workspace/org"; +import useWorkspaceStore from "@/store/useWorkspaceStore"; export default function MainLayout() { useCoreQuery(["myInfo"], getMyInfo); + + const setSelectedOrgId = useWorkspaceStore((s) => s.setSelectedOrgId); + const { data: workspaces } = useCoreQuery(["workspaces"], getMyWorkspaces); + + useEffect(() => { + if (workspaces && workspaces.length > 0) { + setSelectedOrgId(workspaces[0].orgId); + } + }, [workspaces, setSelectedOrgId]); return (
diff --git a/src/pages/ads/list/AdsListPage.tsx b/src/pages/ads/list/AdsListPage.tsx index caa772ca..9b8fa397 100644 --- a/src/pages/ads/list/AdsListPage.tsx +++ b/src/pages/ads/list/AdsListPage.tsx @@ -1,10 +1,10 @@ -import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { toast } from "sonner"; import type { ICampaign } from "@/types/ads/campaign"; import { useControlModal } from "@/hooks/ads/useControlModal"; +import { useCoreQuery } from "@/hooks/customQuery"; import CampaignTable from "@/components/ads/CampaignTable"; import Card from "@/components/common/card/Card"; @@ -14,54 +14,24 @@ import ModalContent from "@/components/common/modal/ModalContent"; import PageHeader from "@/components/common/PageHeader"; import { getCampaignList, updateAllCampaignStatus } from "@/api/ads/ads"; -import { getMyWorkspaces } from "@/api/workspace/org"; import WarnCircleIcon from "@/assets/icon/common/warn-circle.svg?react"; +import useWorkspaceStore from "@/store/useWorkspaceStore"; export default function AdsListPage() { - const [campaigns, setCampaigns] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [currentOrgId, setCurrentOrgId] = useState(null); //orgId - - useEffect(() => { - const initData = async () => { - try { - setIsLoading(true); - const workspaces = await getMyWorkspaces(); - - if (workspaces && workspaces.length > 0) { - // 워크스페이스 ID 임시 지정 -> 추후 선택한 워크스페이스 api 연결 예정 - const TemporaryOrg = workspaces[0]; - const orgId = TemporaryOrg.orgId; - - setCurrentOrgId(orgId); - - // 캠페인 목록 API 호출 - const campaignData = await getCampaignList(orgId); - - setCampaigns(campaignData); - } else { - toast.error("조직이 없습니다."); - } - } catch { - toast.error("데이터를 불러오는 중 오류가 발생하였습니다."); - } finally { - setIsLoading(false); - } - }; - initData(); - }, []); - const navigate = useNavigate(); + const orgId = useWorkspaceStore((s) => s.selectedOrgId); + + const { data: campaigns = [], isLoading } = useCoreQuery( + ["campaigns", orgId], + () => getCampaignList(orgId!), + { enabled: !!orgId }, + ); const stopAll = useControlModal({ successMessage: "전체 캠페인의 모든 광고 노출이 중단되었습니다.", errorMessage: "중단 처리에 실패하였습니다.", onSuccess: () => { - setCampaigns((prev) => - prev.map((c) => - c.status === "ON_GOING" ? { ...c, status: "PAUSED" } : c, - ), - ); + toast.success("전체 캠페인의 모든 광고 노출이 중단되었습니다."); }, }); @@ -69,16 +39,12 @@ export default function AdsListPage() { successMessage: "전체 캠페인의 광고 노출이 재개되었습니다.", errorMessage: "재개 처리에 실패하였습니다.", onSuccess: () => { - setCampaigns((prev) => - prev.map((c) => - c.status === "PAUSED" ? { ...c, status: "ON_GOING" } : c, - ), - ); + toast.success("전체 캠페인의 광고 노출이 재개되었습니다."); }, }); const handleCampaignClick = (id: number) => { - navigate(`/ads/${currentOrgId}/${id}`); + navigate(`/ads/${orgId}/${id}`); }; const handleCampaignGroupClick = () => { @@ -86,7 +52,6 @@ export default function AdsListPage() { }; const hasCampaigns = campaigns.length > 0; - const hasActiveCampaign = campaigns.some((c) => c.status === "ON_GOING"); if (isLoading) { @@ -169,7 +134,7 @@ export default function AdsListPage() { buttonText="중단하기" onConfirm={() => stopAll.handleConfirm(() => - updateAllCampaignStatus(currentOrgId!, "PAUSED"), + updateAllCampaignStatus(orgId!, "PAUSED"), ) } isLoading={stopAll.isLoading} @@ -190,7 +155,7 @@ export default function AdsListPage() { buttonText="시작하기" onConfirm={() => resumeAll.handleConfirm(() => - updateAllCampaignStatus(currentOrgId!, "ON_GOING"), + updateAllCampaignStatus(orgId!, "ON_GOING"), ) } isLoading={resumeAll.isLoading} diff --git a/src/pages/ads/list/CampaignDetail.tsx b/src/pages/ads/list/CampaignDetail.tsx index 795c6fa3..fb7f93e9 100644 --- a/src/pages/ads/list/CampaignDetail.tsx +++ b/src/pages/ads/list/CampaignDetail.tsx @@ -19,7 +19,7 @@ export default function CampaignDetail() { orgId: string; projectId: string; }>(); - const { data, isLoading, refetch } = useCampaignDetail(Number(orgId)); + const { data, isLoading, refetch } = useCampaignDetail(); const stopControl = useControlModal({ successMessage: "해당 캠페인의 모든 광고 운영이 중단되었습니다.", diff --git a/src/pages/dashboard/overview/OverviewDashboard.tsx b/src/pages/dashboard/overview/OverviewDashboard.tsx index a72af643..8ce418dc 100644 --- a/src/pages/dashboard/overview/OverviewDashboard.tsx +++ b/src/pages/dashboard/overview/OverviewDashboard.tsx @@ -4,6 +4,8 @@ import { toast } from "sonner"; import { printAsPdf } from "@/utils/download"; +import { useOverviewMetrics } from "@/hooks/dashboard/useOverviewMetrics"; + import Badge from "@/components/common/badge/Badge"; import Button from "@/components/common/button/Button"; import Card from "@/components/common/card/Card"; @@ -21,8 +23,6 @@ import TrafficChart, { } from "@/components/dashboard/charts/TrafficChart"; import PlatformRoasTable from "@/components/dashboard/platform/PlatformRoasTable"; -import { overviewMockData } from "./overview.mock"; - import ChevronDoubleRightIcon from "@/assets/icon/chevron/chervon-double-right.svg?react"; import DownloadIcon from "@/assets/icon/common/download.svg?react"; import LinkIcon from "@/assets/icon/common/link.svg?react"; @@ -34,6 +34,7 @@ const OverviewAiReportPanel = lazy(() => import("./OverviewAiReportPanel")); export default function OverviewDashboard() { const navigate = useNavigate(); const [isAiPanelOpen, setIsAiPanelOpen] = useState(false); + const { data: kpis } = useOverviewMetrics(); const [currentDate] = useState(() => new Date().toLocaleString("ko-KR", { year: "numeric", @@ -75,7 +76,7 @@ export default function OverviewDashboard() { />
- {overviewMockData.kpis.map((kpi) => ( + {(kpis ?? []).map((kpi) => ( ))}
diff --git a/src/pages/dashboard/overview/overview.mock.ts b/src/pages/dashboard/overview/overview.mock.ts deleted file mode 100644 index de2d40f5..00000000 --- a/src/pages/dashboard/overview/overview.mock.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { IOverviewData } from "@/types/dashboard/overview"; - -export const overviewMockData: IOverviewData = { - kpis: [ - { - title: "클릭수(CTR)", - value: "1,280", - trend: { direction: "up", value: "13.77%" }, - }, - { - title: "노출수", - value: "3,175,630", - trend: { direction: "down", value: "13.77%" }, - }, - { - title: "전환율(CVR)", - value: "27.09%", - trend: { direction: "up", value: "13.77%" }, - }, - { - title: "광고비 대비 매출", - value: "37.58%", - trend: { direction: "down", value: "13.77%" }, - }, - ], -}; diff --git a/src/store/useWorkspaceStore.ts b/src/store/useWorkspaceStore.ts new file mode 100644 index 00000000..48bdfbfe --- /dev/null +++ b/src/store/useWorkspaceStore.ts @@ -0,0 +1,13 @@ +import { create } from "zustand"; + +interface IWorkspaceState { + selectedOrgId: number | null; + setSelectedOrgId: (orgId: number) => void; +} + +const useWorkspaceStore = create((set) => ({ + selectedOrgId: null, + setSelectedOrgId: (orgId) => set({ selectedOrgId: orgId }), +})); + +export default useWorkspaceStore; diff --git a/src/types/dashboard/overview.ts b/src/types/dashboard/overview.ts index 90334ff0..a328d35b 100644 --- a/src/types/dashboard/overview.ts +++ b/src/types/dashboard/overview.ts @@ -1,14 +1,10 @@ -interface ITrend { - direction: "up" | "down"; - value: string; -} - -export interface IKpiMetric { - title: string; - value: string | number; - trend?: ITrend; -} - -export interface IOverviewData { - kpis: IKpiMetric[]; +export interface IMetricsResponse { + clicks: number; + clickChangeRate: number; + impressions: number; + impressionChangeRate: number; + conversion: number; + cvrChangeRate: number; + ROAS: number; + ROASChangeRate: number; } diff --git a/src/types/workspace/workspace.ts b/src/types/workspace/workspace.ts index f15fd47b..a293302c 100644 --- a/src/types/workspace/workspace.ts +++ b/src/types/workspace/workspace.ts @@ -38,10 +38,6 @@ export type TUpdateWorkspaceRequest = { description: string; logoUrl: string | null; }; -export type TApiResult = { - status: string; - data: T; -}; export type TUploadImageResponse = { url: string; From 7dfefaa7e694718c2e03f55860e0ceefd7ba7a5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9C=EC=A0=9C=EA=B2=BD?= Date: Sat, 21 Mar 2026 22:33:03 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20getOverview=20API=20=ED=8C=8C?= =?UTF-8?q?=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20?= =?UTF-8?q?changeRate=20%=20=EB=B3=80=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/dashboard/overview.ts | 8 +++++++- src/api/dashboard/platform.ts | 18 ------------------ src/hooks/dashboard/useOverviewMetrics.ts | 18 ++++++++++-------- 3 files changed, 17 insertions(+), 27 deletions(-) delete mode 100644 src/api/dashboard/platform.ts diff --git a/src/api/dashboard/overview.ts b/src/api/dashboard/overview.ts index 474e457f..eb321898 100644 --- a/src/api/dashboard/overview.ts +++ b/src/api/dashboard/overview.ts @@ -3,10 +3,16 @@ import type { IMetricsResponse } from "@/types/dashboard/overview"; import { axiosInstance } from "@/lib/axiosInstance"; +type TProviderType = "KAKAO" | "NAVER" | "GOOGLE"; + // 대시보드 - 전체 지표 집계 API -export const getOverview = async (orgId: number): Promise => { +export const getOverview = async ( + orgId: number, + providerType?: TProviderType, +): Promise => { const { data } = await axiosInstance.get>( `/api/dashboard/${orgId}/metrics`, + { params: providerType ? { providerType } : undefined }, ); return data.data; }; diff --git a/src/api/dashboard/platform.ts b/src/api/dashboard/platform.ts deleted file mode 100644 index 45fdc33c..00000000 --- a/src/api/dashboard/platform.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { ICommonResponse } from "@/types/common/common"; -import type { IMetricsResponse } from "@/types/dashboard/overview"; - -import { axiosInstance } from "@/lib/axiosInstance"; - -// 대시보드 - 각 플랫폼 별 지표 집계 -export const getOverview = async ( - orgId: number, - providerType: "KAKAO" | "NAVER" | "GOOGLE", -): Promise => { - const { data } = await axiosInstance.get>( - `/api/dashboard/${orgId}/metrics`, - { - params: { providerType }, - }, - ); - return data.data; -}; diff --git a/src/hooks/dashboard/useOverviewMetrics.ts b/src/hooks/dashboard/useOverviewMetrics.ts index 3203c56e..8bf3368a 100644 --- a/src/hooks/dashboard/useOverviewMetrics.ts +++ b/src/hooks/dashboard/useOverviewMetrics.ts @@ -7,6 +7,8 @@ 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) * 100).toFixed(1)}%`; + function toKpis(metrics: IMetricsResponse): IStatCardProps[] { return [ { @@ -14,7 +16,7 @@ function toKpis(metrics: IMetricsResponse): IStatCardProps[] { value: metrics.clicks.toLocaleString(), trend: { direction: metrics.clickChangeRate >= 0 ? "up" : "down", - value: `${Math.abs(metrics.clickChangeRate)}%`, + value: toRate(metrics.clickChangeRate), }, }, { @@ -22,23 +24,23 @@ function toKpis(metrics: IMetricsResponse): IStatCardProps[] { value: metrics.impressions.toLocaleString(), trend: { direction: metrics.impressionChangeRate >= 0 ? "up" : "down", - value: `${Math.abs(metrics.impressionChangeRate)}%`, + value: toRate(metrics.impressionChangeRate), }, }, { - title: "전환수", - value: metrics.conversion.toLocaleString(), + title: "전환율", + value: `${(metrics.conversion * 100).toFixed(1)}%`, trend: { direction: metrics.cvrChangeRate >= 0 ? "up" : "down", - value: `${Math.abs(metrics.cvrChangeRate)}%`, + value: toRate(metrics.cvrChangeRate), }, }, { title: "ROAS", - value: `${metrics.ROAS.toLocaleString()}%`, + value: `${(metrics.ROAS * 100).toFixed(1)}%`, trend: { direction: metrics.ROASChangeRate >= 0 ? "up" : "down", - value: `${Math.abs(metrics.ROASChangeRate)}%`, + value: toRate(metrics.ROASChangeRate), }, }, ]; @@ -52,7 +54,7 @@ export function useOverviewMetrics() { () => getOverview(orgId!), { enabled: !!orgId, - select: (res) => toKpis(res), + select: toKpis, }, ); } From c55daf477b1771efd150784d6d5ca1c843382ab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9C=EC=A0=9C=EA=B2=BD?= Date: Sat, 21 Mar 2026 22:36:36 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=ED=85=8C=EC=9D=BC=EC=9C=88=EB=93=9C?= =?UTF-8?q?=20=EA=B2=BD=EA=B3=A0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/workspace/InviteMemberModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/workspace/InviteMemberModal.tsx b/src/components/workspace/InviteMemberModal.tsx index 29c15abe..32f31309 100644 --- a/src/components/workspace/InviteMemberModal.tsx +++ b/src/components/workspace/InviteMemberModal.tsx @@ -67,7 +67,7 @@ export default function InviteMemberModal({ size="lg" padding="none" title="팀원 초대하기" - className="w-full max-w-[760px] overflow-hidden" + className="w-full max-w-190 overflow-hidden" >
@@ -98,7 +98,7 @@ export default function InviteMemberModal({ size="big" onClick={handleInvite} disabled={isInviteDisabled} - className="min-w-[88px]" + className="min-w-22" > 초대 From 7de869150a6b0ba9a49f70d92874fd08e9529f4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9C=EC=A0=9C=EA=B2=BD?= Date: Sat, 21 Mar 2026 23:08:30 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=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 --- src/hooks/ads/useCampaignDetail.ts | 24 ++++++++++++----- src/pages/ads/list/AdsListPage.tsx | 15 ++++++----- .../dashboard/overview/OverviewDashboard.tsx | 27 ++++++++++++++++--- 3 files changed, 49 insertions(+), 17 deletions(-) diff --git a/src/hooks/ads/useCampaignDetail.ts b/src/hooks/ads/useCampaignDetail.ts index f355205c..edd607ea 100644 --- a/src/hooks/ads/useCampaignDetail.ts +++ b/src/hooks/ads/useCampaignDetail.ts @@ -3,15 +3,27 @@ import { useParams } from "react-router-dom"; import { useCoreQuery } from "@/hooks/customQuery"; import { getCampaignDetail } from "@/api/ads/ads"; -import useWorkspaceStore from "@/store/useWorkspaceStore"; export const useCampaignDetail = () => { - const orgId = useWorkspaceStore((s) => s.selectedOrgId); - const { projectId } = useParams<{ projectId: string }>(); + const { orgId, projectId } = useParams<{ + orgId: string; + projectId: string; + }>(); + + // URL 파라미터를 숫자로 변환 + const parsedOrgId = Number(orgId); + const parsedProjectId = Number(projectId); + + // 유효한 ID일 때만 요청 + const isValid = + Number.isFinite(parsedOrgId) && + parsedOrgId > 0 && + Number.isFinite(parsedProjectId) && + parsedProjectId > 0; return useCoreQuery( - ["campaignDetail", orgId, projectId], - () => getCampaignDetail(orgId!, Number(projectId)), - { enabled: !!orgId && !!projectId }, + ["campaignDetail", parsedOrgId, parsedProjectId], + () => getCampaignDetail(parsedOrgId, parsedProjectId), + { enabled: isValid }, ); }; diff --git a/src/pages/ads/list/AdsListPage.tsx b/src/pages/ads/list/AdsListPage.tsx index 9b8fa397..b89325ad 100644 --- a/src/pages/ads/list/AdsListPage.tsx +++ b/src/pages/ads/list/AdsListPage.tsx @@ -1,5 +1,5 @@ import { useNavigate } from "react-router-dom"; -import { toast } from "sonner"; +import { useQueryClient } from "@tanstack/react-query"; import type { ICampaign } from "@/types/ads/campaign"; @@ -19,6 +19,7 @@ import useWorkspaceStore from "@/store/useWorkspaceStore"; export default function AdsListPage() { const navigate = useNavigate(); + const queryClient = useQueryClient(); const orgId = useWorkspaceStore((s) => s.selectedOrgId); const { data: campaigns = [], isLoading } = useCoreQuery( @@ -27,20 +28,20 @@ export default function AdsListPage() { { enabled: !!orgId }, ); + const invalidateCampaigns = () => { + queryClient.invalidateQueries({ queryKey: ["campaigns", orgId] }); + }; + const stopAll = useControlModal({ successMessage: "전체 캠페인의 모든 광고 노출이 중단되었습니다.", errorMessage: "중단 처리에 실패하였습니다.", - onSuccess: () => { - toast.success("전체 캠페인의 모든 광고 노출이 중단되었습니다."); - }, + onSuccess: invalidateCampaigns, }); const resumeAll = useControlModal({ successMessage: "전체 캠페인의 광고 노출이 재개되었습니다.", errorMessage: "재개 처리에 실패하였습니다.", - onSuccess: () => { - toast.success("전체 캠페인의 광고 노출이 재개되었습니다."); - }, + onSuccess: invalidateCampaigns, }); const handleCampaignClick = (id: number) => { diff --git a/src/pages/dashboard/overview/OverviewDashboard.tsx b/src/pages/dashboard/overview/OverviewDashboard.tsx index 8ce418dc..90f97366 100644 --- a/src/pages/dashboard/overview/OverviewDashboard.tsx +++ b/src/pages/dashboard/overview/OverviewDashboard.tsx @@ -34,7 +34,11 @@ const OverviewAiReportPanel = lazy(() => import("./OverviewAiReportPanel")); export default function OverviewDashboard() { const navigate = useNavigate(); const [isAiPanelOpen, setIsAiPanelOpen] = useState(false); - const { data: kpis } = useOverviewMetrics(); + const { + data: kpis, + isLoading: isKpisLoading, + isError: isKpisError, + } = useOverviewMetrics(); const [currentDate] = useState(() => new Date().toLocaleString("ko-KR", { year: "numeric", @@ -76,9 +80,24 @@ export default function OverviewDashboard() { />
- {(kpis ?? []).map((kpi) => ( - - ))} + {isKpisError ? ( +
+ 지표 데이터를 불러오지 못했습니다. 잠시 후 다시 시도해 주세요. +
+ ) : isKpisLoading ? ( + Array.from({ length: 4 }).map((_, i) => ( +
+
+
+
+
+ )) + ) : ( + (kpis ?? []).map((kpi) => ) + )}