From 6bab76f270a36b21bab5e1d6bb41a50a8dbd8fb9 Mon Sep 17 00:00:00 2001 From: Jio Date: Thu, 19 Feb 2026 00:35:08 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=EC=BA=A0=ED=8E=98=EC=9D=B8=20?= =?UTF-8?q?=EB=B3=B4=EA=B8=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routes/business/campaign/$campaignId.tsx | 2 +- app/routes/mypage/components/profileCard/CampaignsSection.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/routes/business/campaign/$campaignId.tsx b/app/routes/business/campaign/$campaignId.tsx index 724768e..8854e99 100644 --- a/app/routes/business/campaign/$campaignId.tsx +++ b/app/routes/business/campaign/$campaignId.tsx @@ -118,7 +118,7 @@ export default function CampaignContent() {
- 상품 ID: {data.productId} + {data.productId} arrow
diff --git a/app/routes/mypage/components/profileCard/CampaignsSection.tsx b/app/routes/mypage/components/profileCard/CampaignsSection.tsx index 1166153..293d790 100644 --- a/app/routes/mypage/components/profileCard/CampaignsSection.tsx +++ b/app/routes/mypage/components/profileCard/CampaignsSection.tsx @@ -143,7 +143,7 @@ export default function CampaignsSection() { ].join(" ")} onClick={ campaignId - ? () => navigate(`/business/campaign/${campaignId}`) + ? () => navigate(`/business/campaign/${campaignId}?type=sent-campaign`) : undefined } role={campaignId ? "button" : undefined} From 82df4becedf9f00e54c2246fe01e791a4c56c542 Mon Sep 17 00:00:00 2001 From: Jio Date: Thu, 19 Feb 2026 02:17:00 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=EB=B3=B4=EB=82=B8=20=EC=A0=9C?= =?UTF-8?q?=EC=95=88=20=EC=BA=A0=ED=8E=98=EC=9D=B8=20=EB=B3=B4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routes.ts | 5 +- app/routes/business/campaign/$campaignId.tsx | 101 +++++++++++------- app/routes/business/proposal/api/proposal.ts | 3 + .../profileCard/CampaignsSection.tsx | 2 +- 4 files changed, 68 insertions(+), 43 deletions(-) diff --git a/app/routes.ts b/app/routes.ts index 3ce45f9..abd3bc8 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -55,6 +55,7 @@ export default [ route("calendar", "routes/business/calendar/route.tsx"), route("proposal", "routes/business/proposal/route.tsx"), route("rejection", "routes/business/rejection/route.tsx"), + route("campaign/:campaignId", "routes/business/campaign/$campaignId.tsx"), ]), @@ -94,10 +95,10 @@ export default [ ]), // Campaign detail route (without main layout) - route( + /*route( "business/campaign/:campaignId", "routes/business/campaign/$campaignId.tsx", - ), + ),*/ // 404 Catch-all route("*", "routes/not-found.tsx"), diff --git a/app/routes/business/campaign/$campaignId.tsx b/app/routes/business/campaign/$campaignId.tsx index 8854e99..1136b14 100644 --- a/app/routes/business/campaign/$campaignId.tsx +++ b/app/routes/business/campaign/$campaignId.tsx @@ -1,25 +1,24 @@ import { useParams, useNavigate } from "react-router"; import { useState, useEffect } from "react"; -import Button from "../../../components/common/Button"; import LoadingSpinner from "../../../components/common/LoadingSpinner"; - import CampaignBrandCard from "../components/CampaignBrandCard"; import CampaignInfoGroup from "../components/CampaignInfoGroup"; +import { getProposalDetail, type ProposalDetail } from "../proposal/api/proposal"; +import { useHideHeader } from "../../../hooks/useHideHeader"; +import { getBrandSummary, type BrandSummary } from "../proposal/api/brand"; -import { type ProposalDetail, getProposalDetail } from "../../../data/campaign"; - -import editIcon from "../../../assets/icon-edit.svg"; import dropdownIcon from "../../../assets/arrow-down.svg"; import dropupIcon from "../../../assets/arrow-up.svg"; import arrowRightIcon from "../../../assets/icon/arrow-right.svg"; -import calendarIcon from "../../../assets/icon-calender.svg"; +import chatIcon from "../../../assets/chat-icon.svg"; // 채팅 아이콘 경로 확인 필요 export default function CampaignContent() { const { campaignId } = useParams(); const navigate = useNavigate(); + useHideHeader(true); const [isContentOpen, setIsContentOpen] = useState(false); - const [data, setData] = useState(null); + const [brand, setBrand] = useState(null); useEffect(() => { console.log("1. 현재 주소창에서 가져온 ID:", campaignId); @@ -28,10 +27,14 @@ export default function CampaignContent() { const loadData = async () => { try { const res = await getProposalDetail(campaignId); - console.log("2. 서버에서 받은 데이터:", res); setData(res); + + if (res.brandId) { + const brandResult = await getBrandSummary(res.brandId); + setBrand(brandResult); + } } catch (err) { - console.error("3. API 호출 중 발생한 에러:", err); + console.error("데이터 호출 에러:", err); } }; @@ -40,10 +43,19 @@ export default function CampaignContent() { if (!data) return ; + const STATUS_TEXT_MAP: Record = { + REVIEWING: "검토 중", + MATCHED: "매칭 완료", + REJECTED: "거절됨", + CANCELED: "취소됨", + }; + const formatTags = (tags: { name: string }[]) => tags.map(t => t.name).join(", "); + const isExistingCampaign = data.campaignId !== null && data.campaignId !== undefined; + return ( -
+
{/* Header */}
@@ -61,14 +73,45 @@ export default function CampaignContent() {
-
- - -
+
+ + {/* 상단 통합 영역: 브랜드 카드 + 캠페인 제목 + 채팅하기 */} +
+ + +
+
+ + {isExistingCampaign ? "기존 캠페인" : "신규 캠페인"} + +

+ {isExistingCampaign ? data.campaignName : data.title} +

+
+ + +
+
+ +
{/* 캠페인명 */} } >
{data.title} @@ -118,7 +161,7 @@ export default function CampaignContent() {
- {data.productId} + {data.product || "상품 정보 없음"} arrow
@@ -133,43 +176,21 @@ export default function CampaignContent() { {/* 제작 기간 */} } >
- {data.startDate.replace(/-/g, '. ')} + {(data?.startDate || "").replace(/-/g, '. ')}
~
- {data.endDate.replace(/-/g, '. ')} + {(data?.endDate || "").replace(/-/g, '. ')}
- - {/* 기타 협의 사항 */} - } - > -
- {data.refusalReason || "기타 협의 사항이 없습니다."} -
-
- -
- -
); } diff --git a/app/routes/business/proposal/api/proposal.ts b/app/routes/business/proposal/api/proposal.ts index 6cc0cc9..824cc47 100644 --- a/app/routes/business/proposal/api/proposal.ts +++ b/app/routes/business/proposal/api/proposal.ts @@ -13,10 +13,13 @@ export interface ProposalDetail { proposalId: number; brandId: number; creatorId: number; + campaignId: number | null; + campaignName: string | null; title: string; description: string; rewardAmount: number; productId: number; + product?: string; startDate: string | null; endDate: string | null; status: string; diff --git a/app/routes/mypage/components/profileCard/CampaignsSection.tsx b/app/routes/mypage/components/profileCard/CampaignsSection.tsx index 293d790..1166153 100644 --- a/app/routes/mypage/components/profileCard/CampaignsSection.tsx +++ b/app/routes/mypage/components/profileCard/CampaignsSection.tsx @@ -143,7 +143,7 @@ export default function CampaignsSection() { ].join(" ")} onClick={ campaignId - ? () => navigate(`/business/campaign/${campaignId}?type=sent-campaign`) + ? () => navigate(`/business/campaign/${campaignId}`) : undefined } role={campaignId ? "button" : undefined} From 5ccd6f81c98cab5654a7d4feffb32eebc858b31e Mon Sep 17 00:00:00 2001 From: Jio Date: Thu, 19 Feb 2026 02:39:45 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=A7=84=ED=96=89=ED=95=9C=20=EC=BA=A0?= =?UTF-8?q?=ED=8E=98=EC=9D=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routes/business/campaign/$campaignId.tsx | 21 +++++----- .../profileCard/CampaignsSection.tsx | 40 +++++++++++-------- 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/app/routes/business/campaign/$campaignId.tsx b/app/routes/business/campaign/$campaignId.tsx index 1136b14..615e6fe 100644 --- a/app/routes/business/campaign/$campaignId.tsx +++ b/app/routes/business/campaign/$campaignId.tsx @@ -1,4 +1,4 @@ -import { useParams, useNavigate } from "react-router"; +import { useParams, useNavigate, useSearchParams } from "react-router"; import { useState, useEffect } from "react"; import LoadingSpinner from "../../../components/common/LoadingSpinner"; import CampaignBrandCard from "../components/CampaignBrandCard"; @@ -15,6 +15,10 @@ import chatIcon from "../../../assets/chat-icon.svg"; // 채팅 아이콘 경로 export default function CampaignContent() { const { campaignId } = useParams(); const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + + const isReceived = searchParams.get("type") === "received-campaign"; + useHideHeader(true); const [isContentOpen, setIsContentOpen] = useState(false); const [data, setData] = useState(null); @@ -109,14 +113,13 @@ export default function CampaignContent() {
- {/* 캠페인명 */} - -
- {data.title} -
-
+ {!isReceived && ( + +
+ {data.title} +
+
+ )} {/* 캠페인 내용 */} = { + SENT: "sent-campaign", + RECEIVED: "received-campaign", + APPLIED: "applied-campaign", + }; + useEffect(() => { let isMounted = true; const fetchData = async () => { @@ -121,19 +127,24 @@ export default function CampaignsSection() {
{pageItems.map((item, idx) => { const typeLabel = item.type ? typeLabelMap[item.type] : ""; - const statusLabel = item.status - ? statusLabelMap[item.status] - : ""; - const dateLabel = formatDate( - item.endDate ?? item.startDate ?? undefined, - ); - const rightLabel = [dateLabel, statusLabel] - .filter(Boolean) - .join(" "); + const statusLabel = item.status ? statusLabelMap[item.status] : ""; + const dateLabel = formatDate(item.endDate ?? item.startDate ?? undefined); + const rightLabel = [dateLabel, statusLabel].filter(Boolean).join(" "); const title = item.title ?? ""; const brand = item.brandName ? `${item.brandName} - ` : ""; - + const idToUse = item.proposalId ?? item.campaignId ?? null; const campaignId = item.campaignId ?? null; + + const handleNavigate = () => { + if (!idToUse) return; + + // type에 따른 쿼리 스트링 매핑 + const typeQuery = item.type ? typeQueryMap[item.type] : "sent-campaign"; + + // proposalId + navigate(`/business/campaign/${idToUse}?type=${typeQuery}`); + }; + return (
navigate(`/business/campaign/${campaignId}`) - : undefined - } + onClick={idToUse ? handleNavigate : undefined} role={campaignId ? "button" : undefined} tabIndex={campaignId ? 0 : -1} > @@ -153,8 +160,7 @@ export default function CampaignsSection() { {typeLabel}
- {brand} - {title} + {brand}{title}
{rightLabel} From c9ec0a14f952d7c80cf7d6a80f016fc5075c0f79 Mon Sep 17 00:00:00 2001 From: Jio Date: Thu, 19 Feb 2026 03:31:27 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=EC=A7=80=EC=9B=90=ED=95=9C=20?= =?UTF-8?q?=EC=BA=A0=ED=8E=98=EC=9D=B8=20=EB=B3=B4=EA=B8=B0=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routes/business/campaign/$campaignId.tsx | 224 ++++++++++-------- .../profileCard/CampaignsSection.tsx | 8 +- 2 files changed, 127 insertions(+), 105 deletions(-) diff --git a/app/routes/business/campaign/$campaignId.tsx b/app/routes/business/campaign/$campaignId.tsx index 615e6fe..29eb35a 100644 --- a/app/routes/business/campaign/$campaignId.tsx +++ b/app/routes/business/campaign/$campaignId.tsx @@ -3,7 +3,7 @@ import { useState, useEffect } from "react"; import LoadingSpinner from "../../../components/common/LoadingSpinner"; import CampaignBrandCard from "../components/CampaignBrandCard"; import CampaignInfoGroup from "../components/CampaignInfoGroup"; -import { getProposalDetail, type ProposalDetail } from "../proposal/api/proposal"; +import { getProposalDetail, getAppliedCampaignDetail, type ProposalDetail, type AppliedCampaignDetail } from "../proposal/api/proposal"; import { useHideHeader } from "../../../hooks/useHideHeader"; import { getBrandSummary, type BrandSummary } from "../proposal/api/brand"; @@ -17,11 +17,13 @@ export default function CampaignContent() { const navigate = useNavigate(); const [searchParams] = useSearchParams(); - const isReceived = searchParams.get("type") === "received-campaign"; + const campaignType = searchParams.get("type"); + const isApplied = campaignType === "applied-campaign"; + const isReceived = campaignType === "received-campaign"; useHideHeader(true); const [isContentOpen, setIsContentOpen] = useState(false); - const [data, setData] = useState(null); + const [data, setData] = useState(null); // ProposalDetail | AppliedCampaignDetail const [brand, setBrand] = useState(null); useEffect(() => { @@ -30,11 +32,18 @@ export default function CampaignContent() { const loadData = async () => { try { - const res = await getProposalDetail(campaignId); + let res; + if (isApplied) { + // 지원한 캠페인 API 호출 + res = await getAppliedCampaignDetail(campaignId); + } else { + // 보낸/받은 제안 API 호출 + res = await getProposalDetail(campaignId); + } setData(res); if (res.brandId) { - const brandResult = await getBrandSummary(res.brandId); + const brandResult = await getBrandSummary(Number(res.brandId)); setBrand(brandResult); } } catch (err) { @@ -43,20 +52,23 @@ export default function CampaignContent() { }; loadData(); - }, [campaignId]); + }, [campaignId, isApplied]); if (!data) return ; - const STATUS_TEXT_MAP: Record = { - REVIEWING: "검토 중", - MATCHED: "매칭 완료", - REJECTED: "거절됨", - CANCELED: "취소됨", + const getStatusLabel = (status: string) => { + const MAP: Record = { + REVIEWING: "검토 중", MATCHED: "완료", REJECTED: "거절됨", CANCELED: "취소됨", + }; + return MAP[status] || status; }; const formatTags = (tags: { name: string }[]) => tags.map(t => t.name).join(", "); - const isExistingCampaign = data.campaignId !== null && data.campaignId !== undefined; + const proposalData = !isApplied ? (data as ProposalDetail) : null; + const appliedData = isApplied ? (data as AppliedCampaignDetail) : null; + + //const isExistingCampaign = data.campaignId !== null && data.campaignId !== undefined; return (
@@ -78,32 +90,33 @@ export default function CampaignContent() {
- + {/* 상단 통합 영역: 브랜드 카드 + 캠페인 제목 + 채팅하기 */}
- +
- {isExistingCampaign ? "기존 캠페인" : "신규 캠페인"} + {isApplied ? "기존 캠페인" : (proposalData?.campaignId ? "기존 캠페인" : "신규 캠페인")} -

- {isExistingCampaign ? data.campaignName : data.title} +

+ {/* 각 타입에 맞는 필드명을 조건부로 출력 */} + {appliedData ? `‘${appliedData.campaignTitle}’` : (proposalData?.campaignId ? proposalData.campaignName : proposalData?.title)}

- -
- -
- {!isReceived && ( - -
- {data.title} -
-
- )} - - {/* 캠페인 내용 */} - setIsContentOpen(prev => !prev)}> - toggle - - } - > -
- {/* 설명 */} -
-

- 설명 -

-
- {data.description} {/* data.description으로 변경 */} -
-
- - {/* dropdown 열렸을 때 */} - {isContentOpen && ( -
-
- -
- - - - + {isApplied && appliedData ? ( + /* 1. 지원한 캠페인 뷰 */ +
+
+ +
+ {appliedData.campaignReason || "작성된 지원 이유가 없습니다."}
- )} +
- - - {/* 협찬품 / 원고료 */} -
- -
- {data.product || "상품 정보 없음"} - arrow -
-
+
+ ) : proposalData ? ( + /* 2. 제안 뷰 (proposalData가 확실히 있을 때) */ +
+ {!isReceived && ( + +
+ {proposalData.title} +
+
+ )} + + {/* 캠페인 내용 */} + setIsContentOpen(prev => !prev)}> + toggle + + } + > +
+ {/* 설명 */} +
+

+ 설명 +

+
+ {proposalData.description} +
+
- -
- {data.rewardAmount.toLocaleString()} + {/* dropdown 열렸을 때 */} + {isContentOpen && proposalData.contentTags && ( +
+
+ +
+ + + + +
+ )}
-
- {/* 제작 기간 */} - -
-
- {(data?.startDate || "").replace(/-/g, '. ')} -
+ {/* 협찬품 / 원고료 */} +
+ +
+ {proposalData.product || "상품 정보 없음"} + arrow +
+
- ~ + +
+ {proposalData.rewardAmount?.toLocaleString()} +
+
+
-
- {(data?.endDate || "").replace(/-/g, '. ')} + {/* 제작 기간 */} + +
+
+ {(proposalData.startDate || "").replace(/-/g, '. ')} +
+ ~ +
+ {(proposalData.endDate || "").replace(/-/g, '. ')} +
-
- -
+
+
+ ) : null}
); diff --git a/app/routes/mypage/components/profileCard/CampaignsSection.tsx b/app/routes/mypage/components/profileCard/CampaignsSection.tsx index f843285..d764634 100644 --- a/app/routes/mypage/components/profileCard/CampaignsSection.tsx +++ b/app/routes/mypage/components/profileCard/CampaignsSection.tsx @@ -133,7 +133,7 @@ export default function CampaignsSection() { const title = item.title ?? ""; const brand = item.brandName ? `${item.brandName} - ` : ""; const idToUse = item.proposalId ?? item.campaignId ?? null; - const campaignId = item.campaignId ?? null; + //const campaignId = item.campaignId ?? null; const handleNavigate = () => { if (!idToUse) return; @@ -150,11 +150,11 @@ export default function CampaignsSection() { key={`${item.proposalId ?? item.campaignId ?? idx}`} className={[ "flex items-center gap-3 py-3", - campaignId ? "cursor-pointer" : "", + idToUse ? "cursor-pointer" : "", ].join(" ")} onClick={idToUse ? handleNavigate : undefined} - role={campaignId ? "button" : undefined} - tabIndex={campaignId ? 0 : -1} + role={idToUse ? "button" : undefined} + tabIndex={idToUse ? 0 : -1} >
{typeLabel} From cd0d458884fb3bfdc3494340abec0b76f84507ff Mon Sep 17 00:00:00 2001 From: Jio Date: Thu, 19 Feb 2026 03:35:07 +0900 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20=EB=A6=B0=ED=8A=B8=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routes/business/campaign/$campaignId.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/routes/business/campaign/$campaignId.tsx b/app/routes/business/campaign/$campaignId.tsx index 29eb35a..6d76a1d 100644 --- a/app/routes/business/campaign/$campaignId.tsx +++ b/app/routes/business/campaign/$campaignId.tsx @@ -94,15 +94,15 @@ export default function CampaignContent() { {/* 상단 통합 영역: 브랜드 카드 + 캠페인 제목 + 채팅하기 */}
+ showChatSection={false} + statusText={getStatusLabel(data.status)} + brandName={brand?.brandName} + brandTags={brand?.brandTags || []} + brandImageUrl={brand?.brandImageUrl} + matchingRate={brand?.matchingRate} + brandId={brand?.brandId || (data).brandId} + category={proposalData?.contentTags?.categories?.[0]?.name || "beauty"} + />