Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/api/ads/ads.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { ICampaign } from "@/types/ads/campaign";

import type { IApiResult } from "../common/common";

import { axiosInstance } from "@/lib/axiosInstance";

export const getCampaignList = async (orgId: number): Promise<ICampaign[]> => {
const { data } = await axiosInstance.get<
IApiResult<{ projects: ICampaign[] }>
>(`/api/project/${orgId}`);
return data.data.projects;
};

export const updateAllCampaignStatus = async (
orgId: number,
status: "ON_GOING" | "PAUSED",
): Promise<void> => {
await axiosInstance.patch(`/api/project/${orgId}/status`, null, {
params: { status },
});
};
5 changes: 5 additions & 0 deletions src/api/common/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface IApiResult<T> {
Comment thread
YermIm marked this conversation as resolved.
Resolved
status: number;
message: string;
data: T;
}
55 changes: 33 additions & 22 deletions src/components/ads/CampaignRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,18 @@

import type { TCampaignStatus, TPlatform } from "@/types/ads/campaign";

import Badge from "../common/badge/Badge";
import ProgressBar from "../common/progressbar/ProgressBar";

import GoogleLogo from "@/assets/icon/ads/google-circle.svg?react";
import KakaoLogo from "@/assets/icon/ads/kakao-circle.svg?react";
import NaverLogo from "@/assets/icon/ads/naver-circle.svg?react";

interface ICampaignRowProps {
platforms: TPlatform[];
projectId: number;
name: string;
providers: TPlatform[];
status: TCampaignStatus;
statusText: string;
progress: number;
budgetUsageRate: number;
onClick?: () => void;
}
Comment thread
YermIm marked this conversation as resolved.
Resolved

Expand All @@ -25,16 +24,20 @@
};

export default function CampaignRow({
platforms,
projectId,

Check failure on line 27 in src/components/ads/CampaignRow.tsx

View workflow job for this annotation

GitHub Actions / Lint & Build

'projectId' is declared but its value is never read.

Check warning on line 27 in src/components/ads/CampaignRow.tsx

View workflow job for this annotation

GitHub Actions / Lint & Build

'projectId' is defined but never used

Check warning on line 27 in src/components/ads/CampaignRow.tsx

View workflow job for this annotation

GitHub Actions / Lint & Build

'projectId' is defined but never used
name,
providers,
status,
statusText,
progress,
budgetUsageRate,
onClick,
}: ICampaignRowProps) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
const isPaused = status == "PAUSED";
return (
<li
className="flex items-center px-7 py-5 border-b border-bg-disabled hover:bg-bg-surface hover:cursor-pointer transition-colors list-none"
className={`flex items-center px-7 py-5 border-b border-bg-disabled hover:bg-bg-surface hover:cursor-pointer transition-colors list-none ${
isPaused ? "bg-bg-surface" : "bg-white"
}
`}
onClick={onClick}
role="button"
tabIndex={onClick ? 0 : undefined}
Expand All @@ -47,31 +50,39 @@
>
{/* 플랫폼 */}
<div className="flex w-[20%] shrink-0">
{platforms.map((p, idx) => (
<div
key={idx}
className="flex h-8 w-8 mr-3 items-center justify-center rounded-full shadow-sm overflow-hidden shrink-0"
>
{LogoMap[p]}
</div>
))}
{providers && providers.length > 0 ? (
providers.map((p, idx) => (
<div
key={idx}
className="flex h-8 w-8 mr-3 items-center justify-center rounded-full shadow-sm overflow-hidden shrink-0"
>
{LogoMap[p]}
</div>
))
) : (
<div className="text-text-placeholder font-body2">미연결</div>
)}
</div>

{/* 캠페인 명 */}
<div className="w-[35%] min-w-0 pr-10 shrink-0">
<div className="font-body1 text-text-main truncate">{name}</div>
<div className="w-[50%] min-w-0 pr-10 shrink-0">
<div
className={`font-body1 truncate ${isPaused ? "text-text-sub" : "text-text-main"}`}
>
{name}
</div>
</div>

{/* 동기화 상태 */}
<div className="w-[15%] shrink-0 pr-10">
{/* <div className="w-[15%] shrink-0 pr-10">
<Badge variant={status} size="sm">
{statusText}
</Badge>
</div>
</div> */}

{/* 예산 소진 현황 */}
<div className="w-[30%] shrink-0">
<ProgressBar value={progress} />
<div className={`w-[30%] shrink-0 ${isPaused ? "opacity-80" : ""}`}>
<ProgressBar value={budgetUsageRate} />
</div>
</li>
);
Expand Down
36 changes: 25 additions & 11 deletions src/components/ads/CampaignTable.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,48 @@
import type { ICampaign } from "@/types/ads/campaign";

import CampaignRow from "./CampaignRow";

import { MOCK_CAMPAIGNS } from "@/pages/ads/list/campaign.mock";
//import { MOCK_CAMPAIGNS } from "@/pages/ads/list/campaign.mock";

interface ICampaignTableProps {
campaigns: ICampaign[];
onRowClick?: (id: number) => void;
}

export default function CampaignTable({ onRowClick }: ICampaignTableProps) {
export default function CampaignTable({
campaigns,
onRowClick,
}: ICampaignTableProps) {
return (
<div className="w-full bg-white overflow-x-auto">
<div className="min-w-180">
{/* Table Header */}
<div className="flex items-center px-7 py-3 border-b border-bg-disabled">
<div className="w-[20%] font-body1 text-text-main">플랫폼</div>
<div className="w-[35%] font-body1 text-text-main">캠페인 명</div>
<div className="w-[15%] font-body1 text-text-main">동기화 상태</div>
<div className="w-[50%] font-body1 text-text-main">캠페인 명</div>
{/* <div className="w-[15%] font-body1 text-text-main">동기화 상태</div> */}
<div className="w-[30%] font-body1 text-text-main">
예산 소진 현황
</div>
</div>

{/* Row */}
<ul className="divide-y divide-bg-disabled">
{MOCK_CAMPAIGNS.map((campaign) => (
<CampaignRow
key={campaign.id}
{...campaign}
onClick={() => onRowClick?.(campaign.id)}
/>
))}
{campaigns && campaigns.length > 0 ? (
campaigns
.filter((project) => project.status !== "OVER")
.map((project) => (
<CampaignRow
key={project.projectId}
{...project}
onClick={() => onRowClick?.(project.projectId)}
/>
))
) : (
<div className="py-20 text-center font-body2 text-text-placeholder">
현재 캠페인이 없습니다.
</div>
Comment thread
YermIm marked this conversation as resolved.
Resolved
)}
</ul>
</div>
</div>
Expand Down
108 changes: 92 additions & 16 deletions src/pages/ads/list/AdsListPage.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,52 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";

import type { ICampaign } from "@/types/ads/campaign";

import CampaignTable from "@/components/ads/CampaignTable";
import ControlBox from "@/components/common/controlbox/ControlBox";
import Modal from "@/components/common/modal/Modal";
import ModalContent from "@/components/common/modal/ModalContent";

import { getCampaignList, updateAllCampaignStatus } from "@/api/ads/ads";
import { getMyWorkspaces } from "@/api/workspace/org";
import WarningIcon from "@/assets/icon/workspace/message-circle-warning.svg?react";

export default function AdsListPage() {
const [campaigns, setCampaigns] = useState<ICampaign[]>([]);
const [isLoading, setIsLoading] = useState(true);

Check failure on line 18 in src/pages/ads/list/AdsListPage.tsx

View workflow job for this annotation

GitHub Actions / Lint & Build

'isLoading' is declared but its value is never read.

Check warning on line 18 in src/pages/ads/list/AdsListPage.tsx

View workflow job for this annotation

GitHub Actions / Lint & Build

'isLoading' is assigned a value but never used

Check warning on line 18 in src/pages/ads/list/AdsListPage.tsx

View workflow job for this annotation

GitHub Actions / Lint & Build

'isLoading' is assigned a value but never used
const [currentOrgId, setCurrentOrgId] = useState<number | null>(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();
}, []);
Comment thread
YermIm marked this conversation as resolved.
Resolved
Comment thread
YermIm marked this conversation as resolved.
Resolved

const navigate = useNavigate();

const [stopAllOpen, setStopAllOpen] = useState(false);
Expand All @@ -26,28 +63,49 @@
navigate("/ads/campaignGroup");
};

const onStopAll = () => {
const onStopAll = async () => {
if (!currentOrgId) return;
setIsStopping(true);

try {
await updateAllCampaignStatus(currentOrgId, "PAUSED");

setCampaigns((prev) =>
prev.map((c) =>
c.status === "ON_GOING" ? { ...c, status: "PAUSED" } : c,
),
);
toast.success("전체 캠페인의 모든 광고 노출이 중단되었습니다.");
setStopAllOpen(false);
} catch {
toast.error("중단 처리에 실패하였습니다.");
} finally {
setIsStopping(false);
}
};

const onResumeAll = () => {
const onResumeAll = async () => {
if (!currentOrgId) return;
setIsResuming(true);

try {
await updateAllCampaignStatus(currentOrgId, "ON_GOING");
setCampaigns((prev) =>
prev.map((c) =>
c.status === "PAUSED" ? { ...c, status: "ON_GOING" } : c,
),
);
toast.success("전체 캠페인의 광고 노출이 재개되었습니다.");
setResumeOpen(false);
} catch {
toast.error("재개 처리에 실패하였습니다.");
} finally {
setIsResuming(false);
}
};

const hasActiveCampaign = campaigns.some((c) => c.status === "ON_GOING");

return (
<section className="flex flex-col overflow-hidden bg-white rounded-component-lg min-h-[90vh]">
<div className="flex-1 overflow-x-auto py-15 px-10 md:px-18 lg:px-25">
Expand All @@ -61,7 +119,10 @@
</header>
{/* 테이블 */}
<div className="w-full flex flex-col mb-10">
<CampaignTable onRowClick={(id) => handleCampaignClick(id)} />
<CampaignTable
campaigns={campaigns}
onRowClick={(id) => handleCampaignClick(id)}
/>
</div>
{/* 하단 배너 */}
<div className="w-full flex flex-col gap-7">
Comment thread
YermIm marked this conversation as resolved.
Resolved
Expand All @@ -77,18 +138,33 @@
buttonSize="big"
buttonClassName="font-body1"
/>
<ControlBox
title="전체 캠페인을 완전히 중단할 수 있어요"
description="모든 광고 노출이 즉시 멈추고, 연결된 플랫폼에서도 더 이상 광고가 집행되지 않아요."
buttonText="중단하기"
onButtonClick={() => setStopAllOpen(true)}
buttonDisabled={isStopping}
containerClassName="bg-status-red/7 border-status-red px-6 py-4 min-w-[650px] shrink-0"
titleClassName="text-status-red font-heading3"
descriptionClassName="font-body2 text-text-sub"
buttonSize="big"
buttonClassName="font-body1 bg-status-red"
/>
{hasActiveCampaign ? (
<ControlBox
title="전체 캠페인을 완전히 중단할 수 있어요"
description="모든 광고 노출이 즉시 멈추고, 연결된 플랫폼에서도 더 이상 광고가 집행되지 않아요."
buttonText="중단하기"
onButtonClick={() => setStopAllOpen(true)}
buttonDisabled={isStopping}
containerClassName="bg-status-red/7 border-status-red px-6 py-4 min-w-[650px] shrink-0"
titleClassName="text-status-red font-heading3"
descriptionClassName="font-body2 text-text-sub"
buttonSize="big"
buttonClassName="font-body1 bg-status-red"
/>
) : (
<ControlBox
title="중단된 캠페인을 다시 시작할 수 있어요"
description="중단되었던 모든 캠페인의 광고 노출이 즉시 재개되며, 다시 활성화됩니다."
buttonText="시작하기"
onButtonClick={() => setResumeOpen(true)}
buttonDisabled={isResuming}
containerClassName="bg-status-blue/7 border-status-blue px-6 py-4 min-w-[650px] shrink-0"
titleClassName="text-status-blue font-heading3"
descriptionClassName="font-body2 text-text-sub"
buttonSize="big"
buttonClassName="font-body1 bg-status-blue"
/>
)}
</div>
</div>
</div>
Expand Down
35 changes: 19 additions & 16 deletions src/types/ads/campaign.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
export type TPlatform = "kakao" | "google" | "naver";
export type TCampaignStatus = "inactive" | "syncing" | "success";
export type TCampaignStatus = "ON_GOING" | "PAUSED" | "OVER";

// Campaign List
export interface ICampaign {
projectId: number;
name: string;
providers: TPlatform[];
status: TCampaignStatus;
description?: string;
budgetUsageRate: number;
}

// Campaign Detail
export interface ICampaignDetail extends ICampaign {
budget: number;
createAt: string;
ads: IAd[];
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}

// Ad List
export interface IAd {
id: number;
name: string;
Expand All @@ -11,18 +29,3 @@ export interface IAd {
tags: string[];
link: string;
}

export interface ICampaign {
id: number;
name: string;
platforms: TPlatform[];
status: TCampaignStatus;
statusText: string;
progress: number;
runStatus: "running" | "stopped";
runStatusText: string;
budget: string;
startDate: string;
description?: string;
ads: IAd[];
}
Loading