Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion src/assets/icon/chevron/chervon-left.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
107 changes: 43 additions & 64 deletions src/components/ads/AdDetailContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,8 @@ export default function AdDetailContent({
});

const trackControl = useControlModal({
successMessage: "트래킹이 활성화되었습니다.",
errorMessage: "트래킹 활성화에 실패했습니다.",
onSuccess: refetchAds,
});

const stopTrackControl = useControlModal({
successMessage: "트래킹이 중단되었습니다.",
errorMessage: "트래킹 중단에 실패했습니다.",
successMessage: "트래킹 링크가 발급되었습니다.",
errorMessage: "트래킹 링크 발급에 실패했습니다.",
onSuccess: refetchAds,
});

Expand Down Expand Up @@ -75,25 +69,27 @@ export default function AdDetailContent({
};

return (
<div className="px-5 pt-2 pb-6 flex flex-col gap-6 transition-all animate-fade-in overflow-hidden">
<div className="flex w-full">
<div className="w-[15%] shrink-0 mr-2" />

<div className="w-[70%] flex flex-col gap-4 pl-2">
{/* description */}
<div className="pt-5 pb-5 flex flex-col gap-7 bg-bg-surface/30 transition-all animate-fade-in overflow-hidden border-t border-bg-disabled">
<div className="flex flex-col w-full px-8 gap-6 tablet:flex-col tablet:gap-8">
{/* description */}
<div className="flex-1 flex flex-col gap-2 min-w-0">
<span className="text-text-placeholder font-caption">광고 소재</span>
<p className="font-body2 text-text-sub whitespace-pre-line leading-relaxed">
{ad.description}
</p>
</div>

{/* tags */}
{/* tags */}
<div className="w-[30%] flex flex-col gap-2 shrink-0 tablet:w-full">
<span className="text-text-placeholder font-caption">타겟</span>
<div className="flex flex-wrap gap-2">
{tags.length > 0 ? (
tags.map((tag, idx) => (
<Badge
key={idx}
variant="running"
size="sm"
className="border border-bg-text-sub bg-bg-surface text-text-sub font-normal px-3"
className="border border-bg-disabled bg-white text-text-sub px-3"
>
#{tag}
</Badge>
Expand All @@ -104,10 +100,13 @@ export default function AdDetailContent({
</span>
)}
</div>
</div>

{/* link */}
<div className="relative w-full max-w-150 group">
<div className="flex items-center justify-between w-full h-10 px-4 py-2 bg-white border border-text-placeholder rounded-component-sm group-hover:border-primary-light transition-all">
{/* link */}
<div className="flex flex-col gap-2">
<span className="text-text-placeholder font-caption">랜딩 URL</span>
<div className="relative w-full max-w-160">
<div className="flex items-center justify-between w-full h-9 px-4 py-2 bg-white border border-bg-disabled rounded-component-sm group-hover:border-primary-light transition-all">
<span className="font-body2 text-text-auth-sub truncate pr-10 select-all">
{ad.landingUrl}
</span>
Expand All @@ -126,35 +125,33 @@ export default function AdDetailContent({
</div>
</div>

<div className="flex w-full">
<div className="w-[15%] shrink-0 mr-2" />
<div className="flex-1 min-w-0 flex flex-col gap-6 pl-2">
<div className="flex w-full px-8">
<div className="flex-1 min-w-0 flex flex-col gap-6">
<ControlBox
title={
isTrackingActive
? "트래킹이 활성화되어 데이터 수집 중입니다"
: "트래킹 활성화 시 실시간 성과 수집이 시작돼요"
? "트래킹 링크가 발급되었습니다"
: "트래킹 링크를 발급받아 성과를 수집해보세요"
}
description={
isTrackingActive
? "트래킹을 중단하면 더 이상 광고 성과 리포트가 갱신되지 않습니다."
: "광고 클릭 데이터를 실시간으로 수집하여 성과 분석에 반영합니다."
? "발급된 트래킹 링크를 해당 광고 플랫폼에 등록해 주세요."
: "링크를 광고 플랫폼에 등록하면 클릭 성과가 리포트에 반영됩니다."
}
buttonText={isTrackingActive ? "트래킹 중단하기" : "트래킹 활성화"}
buttonText={isTrackingActive ? "링크 복사하기" : "링크 발급하기"}
onButtonClick={
isTrackingActive
? stopTrackControl.openModal
? () => handleCopy(ad.trackingUrl!)
: trackControl.openModal
}
buttonDisabled={false}
containerClassName={`${isTrackingActive ? "bg-status-red/7 border-status-red" : "bg-chart-3/7 border-chart-3"} px-6 py-4 tablet:flex-col tablet:items-start tablet:gap-4`}
titleClassName={`${isTrackingActive ? "text-status-red" : "text-chart-3"} font-heading3`}
containerClassName="bg-status-blue/7 border-status-blue px-6 py-4"
titleClassName="text-status-blue font-heading4 -mb-1"
descriptionClassName="font-caption text-text-sub"
buttonSize="big"
buttonClassName={`${
isTrackingActive ? "bg-status-red" : "bg-chart-3"
} font-body1 tablet:w-full `}
buttonSize="small"
buttonClassName="bg-status-blue font-body2 tablet:w-full"
/>

<ControlBox
title={
ad.status === "PAUSED"
Expand All @@ -178,38 +175,41 @@ export default function AdDetailContent({
? "bg-status-blue/7 border-status-blue"
: "bg-status-red/7 border-status-red"
}`}
titleClassName={`font-heading3 ${
titleClassName={`font-heading4 -mb-1 ${
ad.status === "PAUSED" ? "text-status-blue" : "text-status-red"
}`}
descriptionClassName="font-caption text-text-sub"
buttonSize="big"
buttonClassName={`font-body1 tablet:w-full ${
buttonSize="small"
buttonClassName={`font-body2 tablet:w-full ${
ad.status === "PAUSED" ? "bg-status-blue" : "bg-status-red"
}`}
/>
</div>
</div>

{/* 트래킹 활성화 */}
{/* 트래킹 링크 발급 */}
<Modal
isOpen={trackControl.isOpen}
onClose={trackControl.closeModal}
title="트래킹 활성화"
title="트래킹 링크 발급"
>
<ModalContent
icon={<WarnCircleIcon className="text-status-blue" />}
title="트래킹을 활성화하시겠습니까?"
title="트래킹 링크를 발급하시겠습니까?"
description={
<>
실시간 광고 성과 데이터를 수집하여
발급된 링크를 복사하여 광고 플랫폼에 등록하면
<br />
분석 리포트에 즉시 반영합니다.
트래킹을 활성화할 수 있습니다.
</>
}
buttonText="시작하기"
buttonText="발급하기"
onConfirm={() =>
trackControl.handleConfirm(async () => {
if (!ad.landingUrl) {
toast.error(
"광고에 등록된 랜딩 URL이 없어 발급이 불가능합니다.",
);
throw new Error("랜딩 URL이 없습니다.");
}
await createTrackingUrl(Number(orgId), ad.id, ad.landingUrl);
Expand All @@ -220,27 +220,6 @@ export default function AdDetailContent({
/>
</Modal>

{/* 트래킹 중단 */}
<Modal
isOpen={stopTrackControl.isOpen}
onClose={stopTrackControl.closeModal}
title="트래킹 중단"
>
<ModalContent
icon={<WarnCircleIcon className="text-status-red" />}
title="트래킹을 중단하시겠습니까?"
description="데이터 수집이 중단되어 분석 리포트에 공백이 생길 수 있습니다."
buttonText="중단하기"
onConfirm={() =>
stopTrackControl.handleConfirm(async () => {
/* API 호출 */
})
}
isLoading={stopTrackControl.isLoading}
variant="danger"
/>
</Modal>

{/* 광고 소재 중단 */}
<Modal
isOpen={stopControl.isOpen}
Expand Down
16 changes: 8 additions & 8 deletions src/components/ads/AdRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export default function AdRow({
}
>
{/* status badge */}
<div className="w-[15%] shrink-0 mr-2">
<div className="w-21 shrink-0 mr-3">
<Badge
variant={runStatus === "running" ? "running" : "stopped"}
size="sm"
Expand All @@ -51,20 +51,20 @@ export default function AdRow({
</Badge>
</div>

{/* title */}
<div className="w-[65%] shrink-0 pl-2">
<span className="font-body1 truncate block">{name}</span>
</div>

{/* platform */}
<div className="w-[10%] shrink-0 flex justify-end mr-5">
<div className="w-[6%] shrink-0 flex items-center justify-center mr-3">
<div className="flex h-8 w-8 items-center justify-center rounded-full shadow-sm overflow-hidden shrink-0">
{LogoMap[platform]}
</div>
</div>

{/* title */}
<div className="flex-1 min-w-0 pl-3">
<span className="font-body1 truncate block">{name}</span>
</div>

{/* chevron */}
<div className="w-[5%] shrink-0 flex justify-end">
<div className="w-[5%] shrink-0 flex justify-end ml-4">
<ChevronIcon
className={`w-4 h-4 transition-transform duration-200 text-text-auth-sub ${
isOpen ? "rotate-0" : "rotate-180"
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/modal/ModalContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export default function ModalContent({
variant={variant}
size="big"
onClick={onConfirm}
className="w-auto px-12 tablet:w-full"
className="w-auto px-12 tablet:w-full cursor-pointer"
disabled={isLoading}
>
{isLoading ? "처리 중.." : buttonText}
Expand Down
105 changes: 68 additions & 37 deletions src/pages/ads/list/CampaignDetail.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useParams } from "react-router-dom";
import React from "react";
import { useNavigate, useParams } from "react-router-dom";

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

Expand All @@ -7,15 +8,23 @@ import { useCampaignDetail } from "@/hooks/ads/useCampaignDetail";
import { useControlModal } from "@/hooks/ads/useControlModal";

import AdListTable from "@/components/ads/AdListTable";
import CampaignInfoCard from "@/components/ads/CampaignInfoCard";
import PlatformCard from "@/components/ads/PlatformCard";
import Badge from "@/components/common/badge/Badge";
import ControlBox from "@/components/common/controlbox/ControlBox";
import Modal from "@/components/common/modal/Modal";
import ModalContent from "@/components/common/modal/ModalContent";

import { updateCampaignStatus } from "@/api/ads/ads";
import LeftChevronIcon from "@/assets/icon/chevron/chervon-left.svg?react";
import WarnCircleIcon from "@/assets/icon/common/warn-circle.svg?react";
import GoogleLogo from "@/assets/logo/social-logo/circle/google-circle.svg?react";
import KakaoLogo from "@/assets/logo/social-logo/circle/kakao-circle.svg?react";
import NaverLogo from "@/assets/logo/social-logo/circle/naver-circle.svg?react";

const LogoMap: Record<TPlatform, React.ReactNode> = {
kakao: <KakaoLogo className="w-6 h-6" />,
google: <GoogleLogo className="w-6 h-6" />,
naver: <NaverLogo className="w-6 h-6" />,
};

export default function CampaignDetail() {
const { orgId, projectId } = useParams<{
Expand All @@ -29,6 +38,8 @@ export default function CampaignDetail() {
projectId ? Number(projectId) : null,
);

const navigate = useNavigate();

const stopControl = useControlModal({
successMessage: "해당 캠페인의 모든 광고 운영이 중단되었습니다.",
errorMessage: "중단 처리에 실패하였습니다.",
Expand Down Expand Up @@ -67,46 +78,66 @@ export default function CampaignDetail() {

return (
<section className="flex flex-col justify-start bg-white rounded-component-lg min-h-[90vh] overflow-x-auto">
<div className="flex-1 py-15 px-25 tablet:px-10">
<div className="flex-1 py-10 px-20 tablet:px-10">
<div className="flex flex-col gap-10 w-full">
{/* header */}
<header className="flex flex-col gap-5 w-full">
<div className="flex items-center gap-4 flex-nowrap whitespace-nowrap overflow-hidden w-full">
<h1 className="font-heading2 text-text-main mr-3">{data.name}</h1>
<Badge
variant={data.status === "ON_GOING" ? "running" : "stopped"}
size="sm"
>
{data.status === "ON_GOING"
? "운영 중"
: data.status === "PAUSED"
? "중단"
: "종료"}
</Badge>
</div>
{data.description && (
<div className="border-l-3 border-text-auth-sub pl-4 py-1">
<p className="text-text-auth-sub font-body1 whitespace-pre-line leading-relaxed">
{data.description}
</p>
<button
onClick={() => navigate("/ads")}
className="flex items-center gap-2 text-text-sub font-body2 hover:text-text-main transition-colors w-fit mb-3"
>
<LeftChevronIcon className="w-4 h-4" />
Comment thread
YermIm marked this conversation as resolved.
광고 운영 관리
</button>
Comment thread
YermIm marked this conversation as resolved.

<div className="flex flex-col gap-5">
<div className="flex items-center justify-between w-full tablet:flex-col tablet:items-start tablet:gap-2">
<div className="flex items-center gap-5">
<h1 className="font-heading2 text-text-main">{data.name}</h1>
<Badge
variant={data.status === "ON_GOING" ? "running" : "stopped"}
size="sm"
>
{data.status === "ON_GOING"
? "운영 중"
: data.status === "PAUSED"
? "중단"
: "종료"}
</Badge>
</div>
<span className="text-text-placeholder font-body2">
{data.createdAt.replaceAll("-", ".")} 등록
</span>
</div>
)}
</header>

{/* card section */}
<div className="flex flex-wrap gap-7 w-full">
<CampaignInfoCard
budget={data.budget.toLocaleString()}
date={data.createdAt.replaceAll("-", ".")}
className="flex-1 min-w-[320px] w-full"
/>
<PlatformCard
platforms={data.providers.map(
(p) => p.toLowerCase() as TPlatform,
<div className="flex items-center gap-4 text-text-sub font-body2">
<span>예산 {data.budget.toLocaleString()}원</span>
<span className="text-text-placeholder">·</span>
<div className="flex items-center gap-1">
{data.providers.map((provider) => {
const platform = provider.toLowerCase() as TPlatform;
return (
<div
key={provider}
title={provider}
className="flex items-center justify-center w-6 h-6 rounded-full overflow-hidden shadow-sm"
>
{LogoMap[platform] || null}
</div>
);
})}
</div>
</div>

{data.description && (
<div className="flex flex-col gap-1">
<p className="text-text-sub font-body2 whitespace-pre-line leading-relaxed">
{data.description}
</p>
</div>
)}
className="flex-1 min-w-[320px] w-full"
/>
</div>
</div>
</header>

<div className="w-full">
{/* ads list */}
Expand Down
Loading