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
1 change: 1 addition & 0 deletions app/routes/business/calendar/calendar-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ export default function CalendarContent() {
<CampaignCard
key={`${cp.campaignId}-${cp.proposalId}-${cp.status}`}
campaignId={cp.campaignId}
proposalId={cp.proposalId ?? undefined}
type={cp.type}
brand={cp.brandName}
title={cp.title}
Expand Down
8 changes: 5 additions & 3 deletions app/routes/business/components/CampaignCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,14 @@ export default function CampaignCard({
const id = proposalId || campaignId;

if (type === "APPLIED") {
// 지원한 경우의 캠페인 보기
navigate(`/business/proposal?type=applied&applicationId=${id}`);
} else if (type === "RECEIVED") {
navigate(`/business/proposal?type=received&proposalId=${id}`);
// 받은 제안 -> 받은 캠페인 보기
navigate(`/business/proposal?type=received-campaign&proposalId=${id}`);
} else {
// 기본값 또는 SENT
navigate(`/business/proposal?type=sent&proposalId=${id}`);
// 보낸 제안 (SENT) -> 보낸 캠페인 보기
navigate(`/business/proposal?type=sent-campaign&proposalId=${id}`);
}
};

Expand Down
22 changes: 16 additions & 6 deletions app/routes/business/proposal/api/proposal.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AxiosError } from "axios";
import axios, { AxiosError } from "axios";
import { axiosInstance } from "../../../../api/axios";
import type { BrandDetail } from "../../../../data/brand";

Expand Down Expand Up @@ -158,15 +158,25 @@ export const cancelCampaignApply = async (
campaignApplyId: string | number
): Promise<ApiResponse<string>> => {
try {

const id = Number(campaignApplyId);

const response = await axiosInstance.patch<ApiResponse<string>>(
`/v1/campaigns/apply/${campaignApplyId}/cancel`
`/v1/campaigns/apply/${id}/cancel`
);
return response.data;
} catch (error) {
console.error("지원 취소 실패:", error);
if (error instanceof AxiosError) {
const errorMessage = error.response?.data?.message || "지원 취소에 실패했습니다.";
throw new Error(errorMessage);
if (axios.isAxiosError(error)) {
// 서버에서 내려주는 구체적인 에러 객체 확인
const serverData = error.response?.data;
console.error("서버 에러 상세:", serverData);

// 403 에러일 경우 구체적인 메시지 처리
if (error.response?.status === 403) {
throw new Error(serverData?.message || "지원 취소 권한이 없습니다. 본인이 지원한 내역인지 확인해주세요.");
}

throw new Error(serverData?.message || "지원 취소 중 오류가 발생했습니다.");
}
throw error;
}
Expand Down
12 changes: 8 additions & 4 deletions app/routes/business/proposal/application-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export default function ApplicationContent() {
}

const profileResult = await getProfileCard();
setProfileCard(profileResult);
setProfileCard(profileResult);
} catch (error) {
console.error("최종 데이터 로드 실패:", error);
} finally {
Expand All @@ -83,7 +83,7 @@ export default function ApplicationContent() {
}

try {
const response = await cancelCampaignApply(applicationId);
const response = await cancelCampaignApply(data.campaignApplyId);
if (response.isSuccess) {
setModalStep("COMPLETE");
}
Expand Down Expand Up @@ -151,7 +151,11 @@ export default function ApplicationContent() {
{/* 제안 프로필 */}
<div className="flex flex-col gap-2">
<p className="text-title3 text-text-1">제안 프로필</p>
<div className="w-full p-4 bg-bluegray-2 rounded-2xl flex justify-between items-center border border-core-70">
<div
// 1. 클릭 시 마이페이지 프로필 카드로 이동하도록 추가
onClick={() => navigate("/mypage/profileCard")}
className="w-full p-4 bg-bluegray-2 rounded-2xl flex justify-between items-center border border-core-70 cursor-pointer active:bg-bluegray-3 transition-colors"
>
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-8 h-8">
<img src={profileIcon} alt="profile" className="w-full h-full" />
Expand Down Expand Up @@ -203,7 +207,7 @@ export default function ApplicationContent() {
{modalStep === "CONFIRM" ? (
<>
<h3 className="text-[20px] font-bold text-text-black mb-10 leading-snug">
제안을 취소하시겠습니까?
지원을 취소하시겠습니까?
</h3>
<div className="flex w-full gap-3">
<button
Expand Down
182 changes: 182 additions & 0 deletions app/routes/business/proposal/received-campaign-content.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { useState, useEffect } from "react";
import { useSearchParams, useNavigate } from "react-router-dom";
import Header from "../../../components/layout/Header";

import CampaignBrandCard from "../components/CampaignBrandCard";
import CampaignInfoGroup from "../components/CampaignInfoGroup";
import LoadingSpinner from "../../../components/common/LoadingSpinner";

import { getProposalDetail, type ProposalDetail } from "./api/proposal";
import { getBrandSummary, type BrandSummary } from "./api/brand";

import dropdownIcon from "../../../assets/arrow-down.svg";
import dropupIcon from "../../../assets/arrow-up.svg";
import arrowRightIcon from "../../../assets/icon/arrow-right.svg";
import chatIcon from "../../../assets/chat-icon.svg";

interface TagItem {
name: string;
}


export default function ReceivedCampaignContent() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const proposalId = searchParams.get("id") || searchParams.get("proposalId");

const [proposal, setProposal] = useState<ProposalDetail | null>(null);
const [brand, setBrand] = useState<BrandSummary | null>(null);

const [isLoading, setIsLoading] = useState(true);
//const [modalType, setModalType] = useState<"none" | "confirm" | "success" | "reject" | "rejectSuccess">("none");
const [isContentOpen, setIsContentOpen] = useState(false);
//const [isProcessing, setIsProcessing] = useState(false);
//const [rejectReason, setRejectReason] = useState("");

useEffect(() => {
const fetchData = async () => {
if (!proposalId) return;
try {
setIsLoading(true);
const proposalResult = await getProposalDetail(proposalId);
setProposal(proposalResult);

if (proposalResult.brandId) {
const brandResult = await getBrandSummary(proposalResult.brandId);
setBrand(brandResult);
}
} catch (error) {
console.error("데이터 로드 실패:", error);
} finally {
setIsLoading(false);
}
};
fetchData();
}, [proposalId]);


const formatTags = (tags: TagItem[] | undefined | null) =>
(!tags || tags.length === 0 ? "정보 없음" : tags.map((t: TagItem) => t.name).join(", "));
const formatDate = (dateStr: string) => (dateStr || "").replace(/-/g, ". ");

if (isLoading) return <LoadingSpinner className="py-10" />;
if (!proposal) return <div className="p-10 text-center text-text-gray3">데이터를 찾을 수 없습니다.</div>;

return (
<div className="flex flex-col w-full min-h-screen bg-[var(--color-bg-w)] font-pretendard relative">
<Header title="캠페인 보기" showBack={true} />

<main className="flex flex-col pb-24 bg-[var(--color-bluegray-1)]">
{/* 1. 상단 브랜드 정보 및 채팅 섹션 */}
<div className="bg-[var(--color-bg-w)] px-4 py-6 flex flex-col gap-6">
<CampaignBrandCard
showChatSection={false}
statusText="받은 제안"
brandName={brand?.brandName}
brandTags={brand?.brandTags}
brandImageUrl={brand?.brandImageUrl}
matchingRate={brand?.matchingRate}
/>

<div className="flex justify-between items-center">
<div className="flex flex-col gap-1">
<span className="text-core-1 text-title3">신규 캠페인</span>
<h2 className="text-title1 text-text-black">{proposal.title}</h2>
</div>
<button
onClick={() => navigate(`/chat/${proposal.brandId}`)}
className="flex items-center gap-1.5 px-4 py-2 bg-bluegray-2 rounded-lg text-text-gray1 text-caption1 active:bg-bluegray-3 transition-colors"
>
<img src={chatIcon} alt="chat" className="w-4 h-4" />
채팅하기
</button>
</div>
</div>

{/* 2. 캠페인 상세 정보 섹션 */}
<div className="px-4 py-8 flex flex-col gap-6">
{/* 캠페인 내용 (아코디언) */}
<CampaignInfoGroup
label="캠페인 내용"
right={
<button onClick={() => setIsContentOpen((prev) => !prev)}>
<img src={isContentOpen ? dropupIcon : dropdownIcon} alt="toggle" className="w-5 h-5" />
</button>
}
>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<p className="text-callout1 text-[var(--color-text-gray2)]">설명</p>
<div className="w-full min-h-[68px] px-4 py-2.5 bg-bg-w border border-text-gray5 rounded-lg text-callout1 text-text-gray1 leading-relaxed">
{proposal.description}
</div>
</div>

{isContentOpen && (
<div className="grid grid-cols-2 gap-4 animate-in fade-in duration-300">
<div className="col-span-2">
<ContentItem label="형식" value={formatTags(proposal.contentTags.formats)} />
</div>
<ContentItem label="종류" value={formatTags(proposal.contentTags.categories)} />
<ContentItem label="톤" value={formatTags(proposal.contentTags.tones)} />
<ContentItem label="관여도" value={formatTags(proposal.contentTags.involvements)} />
<ContentItem label="활용 범위" value={formatTags(proposal.contentTags.usageRanges)} />
</div>
)}
</div>
</CampaignInfoGroup>

{/* 협찬품 / 원고료 */}
<div className="grid grid-cols-2 gap-4">
<CampaignInfoGroup label="협찬품">
<div className="w-full h-9 px-4 bg-bg-w border border-text-gray5 rounded-lg text-callout1 text-text-gray1 flex justify-between items-center">
<span className="truncate">네이처 스크럽 바 1개</span>
<img src={arrowRightIcon} alt="arrow" className="w-4 h-4 opacity-30" />
</div>
</CampaignInfoGroup>

<CampaignInfoGroup label="원고료">
<div className="w-full h-9 px-4 bg-bg-w border border-text-gray5 rounded-lg text-callout1 text-text-gray1 flex justify-between items-center">
<span>{proposal.rewardAmount.toLocaleString()}</span>
<span className="ml-1">원</span>
</div>
</CampaignInfoGroup>
</div>

{/* 제작 기간 */}
<CampaignInfoGroup label="제작 기간">
<div className="flex items-center gap-2">
<div className="flex-1 h-9 px-4 flex items-center bg-bg-w border border-text-gray5 rounded-lg text-callout1 text-text-gray1">
{formatDate(proposal?.startDate || "")}
</div>
<span className="text-text-gray3">~</span>
<div className="flex-1 h-9 px-4 flex items-center bg-bg-w border border-text-gray5 rounded-lg text-callout1 text-text-gray1">
{formatDate(proposal?.endDate || "")}
</div>
</div>
</CampaignInfoGroup>

{/* 기타 협의 사항 */}
<CampaignInfoGroup
label="기타 협의 사항"
right={<img src={arrowRightIcon} alt="edit" className="w-4 h-4 opacity-40 rotate-[-45deg]" />}
>
<div className="w-full h-10 px-4 bg-bg-w border border-text-gray5 rounded-lg"></div>
</CampaignInfoGroup>
</div>
</main>
</div>
);
}

function ContentItem({ label, value }: { label: string; value: string }) {
return (
<div className="flex flex-col gap-2">
<p className="text-callout1 text-text-gray2">{label}</p>
<div className="w-full h-9 flex justify-between items-center bg-bg-w border border-text-gray5 rounded-lg">
<span className="flex-1 truncate pl-4 text-callout1 text-text-gray1">{value}</span>
<img src={arrowRightIcon} alt="arrow" className="w-4 h-4 mr-3 opacity-30" />
</div>
</div>
);
}
10 changes: 10 additions & 0 deletions app/routes/business/proposal/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { useSearchParams } from "react-router";
import ProposalContent from "./sent-proposal-content";
import ReceivedProposalContent from "./received-proposal-content";
import ApplicationContent from "./application-content";
import SentCampaignContent from "./sent-campaign-content";
import ReceivedCampaignContent from "./received-campaign-content";

export default function Proposal() {
const [searchParams] = useSearchParams();
Expand All @@ -15,5 +17,13 @@ export default function Proposal() {
return <ApplicationContent />;
}

if (type === "sent-campaign") {
return <SentCampaignContent />;
}

if (type === "received-campaign") {
return <ReceivedCampaignContent />;
}

return <ProposalContent />;
}
Loading
Loading