diff --git a/app/routes/business/calendar/api/calendar.ts b/app/routes/business/calendar/api/calendar.ts index 93d8330d..96bac9c3 100644 --- a/app/routes/business/calendar/api/calendar.ts +++ b/app/routes/business/calendar/api/calendar.ts @@ -7,7 +7,7 @@ export interface CampaignCollaboration { thumbnailUrl: string; title: string; status: "NONE" | "REVIEWING" | "MATCHED" | "REJECTED"; - startDate: string; // "2026-02-01" + startDate: string; endDate: string; type: "APPLIED" | "SENT" | "RECEIVED"; } @@ -26,57 +26,10 @@ export const getMyCollaborations = async (params?: { startDate?: string; endDate?: string; }) => { - // axios -> axiosInstance로 수정 + const response = await axiosInstance.get( "/v1/campaigns/collaborations/me", { params } ); return response.data.result; -}; - -/*export const MATCHING_DUMMY_DATA: CampaignCollaboration[] = [ - { - campaignId: 1, - proposalId: "p1", - brandName: "라운드랩", - thumbnailUrl: "", - title: "자작나무 수분크림 체험단", - status: "MATCHED", - startDate: "2026-02-02", - endDate: "2026-02-07", - type: "SENT", - }, - { - campaignId: 2, - proposalId: "p2", - brandName: "비플레인", - thumbnailUrl: "", - title: "'글로우업' 선크림 신제품 홍보", - status: "REVIEWING", - startDate: "2026-02-04", - endDate: "2025-02-05", - type: "RECEIVED", - }, - { - campaignId: 3, - proposalId: "p3", - brandName: "그레이스유", - thumbnailUrl: "", - title: "봄 신상 코디 콘텐츠 제작", - status: "REVIEWING", - startDate: "2026-02-23", - endDate: "2026-02-24", - type: "RECEIVED", - }, - { - campaignId: 4, - proposalId: "p4", - brandName: "이즈앤트리", - thumbnailUrl: "", - title: "비타민C 세럼 리뷰 캠페인", - status: "REJECTED", - startDate: "2026-02-01", - endDate: "2025-02-04", - type: "SENT", - }, -];*/ +}; \ No newline at end of file diff --git a/app/routes/business/calendar/calendar-content.tsx b/app/routes/business/calendar/calendar-content.tsx index 6d29ef4b..f5a50f56 100644 --- a/app/routes/business/calendar/calendar-content.tsx +++ b/app/routes/business/calendar/calendar-content.tsx @@ -11,7 +11,6 @@ import MatchingCard from "../components/MatchingCard"; import MatchingTabSection from "../components/MatchingTabSection"; import dropdownIcon from "../../../assets/arrow-down.svg"; import EmptyState from "../components/EmptyState"; -//import { MATCHING_DUMMY_DATA } from "../calendar/api/calendar"; export default function CalendarContent() { const navigate = useNavigate(); @@ -41,13 +40,12 @@ export default function CalendarContent() { fetchCampaigns(); }, [matchingSubTab]); - // 상태 변환 헬퍼 함수 const getStatusLabel = (status: CampaignCollaboration["status"]): "매칭" | "검토 중" | "거절" => { switch (status) { case "MATCHED": return "매칭"; case "REVIEWING": - case "NONE": + case "NONE": return "검토 중"; case "REJECTED": return "거절"; @@ -73,37 +71,44 @@ export default function CalendarContent() { const todayStr = new Date().toISOString().split('T')[0]; const currentMonthStr = todayStr.substring(0, 7); + const calendarEvents = campaigns.filter(item => item.status === "MATCHED"); const filteredList = campaigns.filter((item) => { + if (item.status !== "MATCHED") return false; + if (activeTab === "today") { return item.startDate <= todayStr && item.endDate >= todayStr; + } else { + const startMonth = item.startDate.substring(0, 7); + const endMonth = item.endDate.substring(0, 7); + return startMonth <= currentMonthStr && endMonth >= currentMonthStr; } - return item.startDate.includes(currentMonthStr) || item.endDate.includes(currentMonthStr); }); const handleCardClick = (item: CampaignCollaboration) => { - const proposalId = item.proposalId || item.campaignId; + const proposalId = item.proposalId || item.campaignId; + + if (item.status === "REJECTED") { + navigate(`/rejection?proposalId=${proposalId}`); + return; + } - // 1. 거절 상태일 때 - if (item.status === "REJECTED") { - navigate(`/rejection?proposalId=${proposalId}`); - return; - } + if (item.type === "APPLIED") { + navigate(`/business/proposal?type=applied&applicationId=${proposalId}`); + return; + } - // 2. 지원하기 타입일 때 - if (item.type === "APPLIED") { - navigate(`/business/proposal?type=applied&applicationId=${proposalId}`); - return; - } + if (item.type === "RECEIVED") { + navigate(`/business/proposal?type=received&proposalId=${proposalId}`); + return; + } - // 3. 그 외 기본 - navigate(`/business/proposal?proposalId=${proposalId}`); -}; + navigate(`/business/proposal?type=sent&proposalId=${proposalId}`); + }; return (
- {/* 탭 네비게이션 */}
) : ( - /* [B] 매칭 현황 */ + /* 매칭 현황 */
handleCardClick(item)} /> )) diff --git a/app/routes/business/campaign/$campaignId.tsx b/app/routes/business/campaign/$campaignId.tsx index 77301cbd..7f0419f8 100644 --- a/app/routes/business/campaign/$campaignId.tsx +++ b/app/routes/business/campaign/$campaignId.tsx @@ -37,10 +37,8 @@ export default function CampaignContent() { loadData(); }, [campaignId]); - // 데이터 로딩 전 if (!data) return
로딩 중...
; - // 태그들을 예쁘게 합쳐주는 함수 (예: ["릴스", "숏폼"] -> "릴스, 숏폼") const formatTags = (tags: { name: string }[]) => tags.map(t => t.name).join(", "); return ( diff --git a/app/routes/business/components/CampaignBrandCard.tsx b/app/routes/business/components/CampaignBrandCard.tsx index aedd1ce4..74046a25 100644 --- a/app/routes/business/components/CampaignBrandCard.tsx +++ b/app/routes/business/components/CampaignBrandCard.tsx @@ -1,20 +1,22 @@ -import brandLogo from "../../../assets/brand-logo.png"; import chatIcon from "../../../assets/chat-icon.svg"; import arrowRightIcon from "../../../assets/icon/arrow-right.svg"; interface CampaignBrandCardProps { showChatSection?: boolean; - statusText?: string; // '보낸 제안' 또는 '검토 중' + statusText?: string; brandName?: string; - brandTags?: string[]; - + brandTags?: string[]; + brandImageUrl?: string; + matchingRate?: number; } export default function CampaignBrandCard({ - showChatSection = true, - statusText = "보낸 제안", + showChatSection, + statusText, brandName, - brandTags + brandTags, + brandImageUrl, + matchingRate // Props }: CampaignBrandCardProps) { return (
@@ -23,11 +25,7 @@ export default function CampaignBrandCard({
{/* 브랜드 로고 박스 */}
- beplain + {brandName}
@@ -48,7 +46,7 @@ export default function CampaignBrandCard({
매칭률 - 99% + {matchingRate || 0}%
{statusText}
diff --git a/app/routes/business/components/CampaignCard.tsx b/app/routes/business/components/CampaignCard.tsx index 03dbffde..759cbf2f 100644 --- a/app/routes/business/components/CampaignCard.tsx +++ b/app/routes/business/components/CampaignCard.tsx @@ -12,6 +12,8 @@ interface CampaignCardProps { logo?: string; showButton?: boolean; campaignId?: string | number; + proposalId?: string | number; + type?: "SENT" | "RECEIVED" | "APPLIED"; } export default function CampaignCard({ @@ -21,12 +23,27 @@ export default function CampaignCard({ endDate, logo, showButton = true, - campaignId = 1, + campaignId, + proposalId, + type = "SENT", }: CampaignCardProps) { const navigate = useNavigate(); - // 로고 컴포넌트 + const handleDetailClick = () => { + // 사용자가 제공한 라우팅 규칙에 따른 분기 로직 + 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}`); + } else { + // 기본값 또는 SENT + navigate(`/business/proposal?type=sent&proposalId=${id}`); + } + }; + const Logo = (
- {/* 1. 컨테이너: 세로 배치, 간격 10px, 하단 정렬 */}
- {/* 2. 상단 정보 섹션 */}
{/* 브랜드명 */}
@@ -52,7 +67,7 @@ export default function CampaignCard({ 이동
- {/* 제목 및 날짜: 양 끝 정렬 */} + {/* 제목 및 날짜 */}

{title} @@ -64,20 +79,14 @@ export default function CampaignCard({

- {/* 3. 캠페인 보기 버튼: 디자인 규격에 맞춰 높이와 너비 조정 */} + {/* 캠페인 보기 버튼 */} {showButton && ( )}
diff --git a/app/routes/business/components/FilterBottomSheet.tsx b/app/routes/business/components/FilterBottomSheet.tsx index 59891c01..f7029a28 100644 --- a/app/routes/business/components/FilterBottomSheet.tsx +++ b/app/routes/business/components/FilterBottomSheet.tsx @@ -20,10 +20,8 @@ export default function FilterBottomSheet({ if (!isOpen) return null; return ( - /* 최상위 컨테이너 */
- - {/* 배경 오버레이 */} +
- {/* 1. 헤더 영역 */} + {/* 헤더 영역 */}
정렬 필터
- {/* 2. 필터 옵션 영역 */} + {/* 필터 옵션 영역 */}
{FILTERS.map((filter) => ( @@ -59,7 +57,7 @@ export default function FilterBottomSheet({
- {/* 3. 하단 적용하기 버튼 */} + {/* 하단 적용하기 버튼 */}
+ )} +
+ ); + })}
); diff --git a/app/routes/business/proposal/api/brand.ts b/app/routes/business/proposal/api/brand.ts new file mode 100644 index 00000000..a8351119 --- /dev/null +++ b/app/routes/business/proposal/api/brand.ts @@ -0,0 +1,29 @@ +import { axiosInstance } from "../../../../api/axios"; // 경로 확인 필요 +import type { ApiResponse } from "./proposal"; + +export interface BrandSummary { + brandId: number; + brandName: string; + brandImageUrl: string; + brandTags: string[]; + matchingRate: number; +} + +export const getBrandSummary = async (brandId: number): Promise => { + try { + // proposal.ts와 동일한 axiosInstance 사용 + const response = await axiosInstance.get>( + `/v1/brands/${brandId}/summary` + ); + + // 공통 응답 구조(isSuccess) 확인 + if (response.data.isSuccess) { + return response.data.result; + } + + throw new Error(response.data.message || "브랜드 요약 조회 실패"); + } catch (error) { + console.error("브랜드 요약 조회 실패:", error); + throw error; + } +}; \ No newline at end of file diff --git a/app/routes/business/proposal/api/proposal.ts b/app/routes/business/proposal/api/proposal.ts index 4841ca0b..b138ff63 100644 --- a/app/routes/business/proposal/api/proposal.ts +++ b/app/routes/business/proposal/api/proposal.ts @@ -55,7 +55,6 @@ export const getBrandDetail = async (brandId: number | string): Promise> => { + try { + const response = await axiosInstance.patch>( + `/v1/campaigns/proposal/${campaignProposalId}/approve` + ); + return response.data; + } catch (error) { + console.error("제안 수락 실패:", error); + throw error; + } +}; + +// 받은 캠페인 제안 거절 API + +export const rejectCampaignProposal = async ( + campaignProposalId: string | number, + rejectReason: string +): Promise> => { + try { + + const response = await axiosInstance.patch>( + `/v1/campaigns/proposal/${campaignProposalId}/reject`, + { rejectReason } + ); + return response.data; + } catch (error) { + console.error("제안 거절 실패:", error); + throw error; + } }; \ No newline at end of file diff --git a/app/routes/business/proposal/application-content.tsx b/app/routes/business/proposal/application-content.tsx index 2ca7d74a..94486041 100644 --- a/app/routes/business/proposal/application-content.tsx +++ b/app/routes/business/proposal/application-content.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from "react"; import { useSearchParams } from "react-router-dom"; import { getAppliedCampaignDetail, type AppliedCampaignDetail } from "./api/proposal"; - +import { getBrandSummary, type BrandSummary } from "./api/brand"; import Modal from "../../../components/common/Modal"; import Header from "../../../components/layout/Header"; @@ -17,6 +17,8 @@ import closeIcon from "../../../assets/icon/icon-close.svg"; export default function ApplicationContent() { const [searchParams] = useSearchParams(); const [data, setData] = useState(null); + + const [brand, setBrand] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isModalOpen, setIsModalOpen] = useState(false); @@ -40,8 +42,14 @@ export default function ApplicationContent() { setIsLoading(true); const result = await getAppliedCampaignDetail(applicationId as string); setData(result); + + + if (result.campaignId) { + const brandResult = await getBrandSummary(result.campaignId); + setBrand(brandResult); + } } catch (error) { - console.error("지원 상세 조회 실패:", error); + console.error("데이터 로드 실패:", error); } finally { setIsLoading(false); } @@ -81,8 +89,10 @@ export default function ApplicationContent() {
diff --git a/app/routes/business/proposal/received-proposal-content.tsx b/app/routes/business/proposal/received-proposal-content.tsx index f31a8f38..53040a01 100644 --- a/app/routes/business/proposal/received-proposal-content.tsx +++ b/app/routes/business/proposal/received-proposal-content.tsx @@ -5,8 +5,8 @@ import CampaignBrandCard from "../components/CampaignBrandCard"; import CampaignInfoGroup from "../components/CampaignInfoGroup"; import Modal from "../../../components/common/Modal"; -import { getProposalDetail, getBrandDetail, type ProposalDetail } from "./api/proposal"; -import type { BrandDetail } from "../../../data/brand"; +import { getProposalDetail, approveCampaignProposal, rejectCampaignProposal, 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"; @@ -20,24 +20,24 @@ export default function ReceivedProposalContent() { const proposalId = searchParams.get("id") || searchParams.get("proposalId"); const [proposal, setProposal] = useState(null); - const [brand, setBrand] = useState(null); + const [brand, setBrand] = useState(null); + const [isLoading, setIsLoading] = useState(true); const [modalType, setModalType] = useState<"none" | "confirm" | "success">("none"); const [isContentOpen, setIsContentOpen] = useState(false); + const [isProcessing, setIsProcessing] = useState(false); - // 1. 데이터 패칭 로직 useEffect(() => { const fetchData = async () => { if (!proposalId || typeof proposalId !== "string") return; try { setIsLoading(true); - // 1. 제안 상세 정보 가져오기 + const proposalResult = await getProposalDetail(proposalId); setProposal(proposalResult); - // 2. 제안 정보에 있는 brandId로 브랜드 상세 정보 가져오기 if (proposalResult.brandId) { - const brandResult = await getBrandDetail(proposalResult.brandId); + const brandResult = await getBrandSummary(proposalResult.brandId); setBrand(brandResult); } } catch (error) { @@ -50,12 +50,57 @@ export default function ReceivedProposalContent() { }, [proposalId]) const handleAcceptClick = () => setModalType("confirm"); - const handleConfirm = () => setModalType("success"); + const handleConfirm = async () => { + if (!proposalId) return; + + try { + setIsProcessing(true); + + const response = await approveCampaignProposal(proposalId); + + if (response.isSuccess) { + setModalType("success"); + + } else { + alert(response.message || "수락 처리 중 오류가 발생했습니다."); + } + } catch (error) { + console.error("수락 연동 에러:", error); + alert("서버와 통신 중 에러가 발생했습니다."); + } finally { + setIsProcessing(false); + } + }; const closeModal = () => setModalType("none"); + // 거절 처리 로직 + const handleRejectClick = async () => { + if (!proposalId) return; + + const reason = window.prompt("거절 사유를 입력해주세요.", "일정이 맞지 않습니다."); + + if (reason === null) return; + + try { + setIsProcessing(true); + const response = await rejectCampaignProposal(proposalId, reason); + + if (response.isSuccess) { + alert("제안을 거절했습니다."); + window.location.reload(); + } else { + alert(response.message || "거절 처리 중 오류가 발생했습니다."); + } + } catch (error) { + console.error("거절 연동 에러:", error); + alert("서버와 통신 중 에러가 발생했습니다."); + } finally { + setIsProcessing(false); + } + }; // 태그 배열을 문자열로 변환하는 헬퍼 함수 const formatTags = (tags: { name: string }[] | undefined | null) => { - if (!tags || tags.length === 0) return "정보 없음"; // 데이터가 없을 경우 처리 + if (!tags || tags.length === 0) return "정보 없음"; return tags.map(t => t.name).join(", "); }; @@ -75,7 +120,9 @@ export default function ReceivedProposalContent() { showChatSection={false} statusText={proposal.status === "MATCHED" ? "매칭 완료" : "검토 중"} brandName={brand?.brandName} - brandTags={brand?.brandTag} + brandTags={brand?.brandTags} + brandImageUrl={brand?.brandImageUrl} + matchingRate={brand?.matchingRate} />

{proposal.title}

@@ -138,7 +185,7 @@ export default function ReceivedProposalContent() {
- {/* 제작 기간: 36px 및 pl-4 통일 */} + {/* 제작 기간 */}
@@ -155,8 +202,12 @@ export default function ReceivedProposalContent() { {/* 하단 고정 버튼 영역 */}
-
diff --git a/app/routes/business/proposal/sent-proposal-content.tsx b/app/routes/business/proposal/sent-proposal-content.tsx index 38441347..0d753fc4 100644 --- a/app/routes/business/proposal/sent-proposal-content.tsx +++ b/app/routes/business/proposal/sent-proposal-content.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from "react"; import { useSearchParams } from "react-router-dom"; -import { getProposalDetail, type ProposalDetail} from "./api/proposal"; // 경로 확인 필요 -import type { BrandDetail } from "../../../data/brand"; +import { getProposalDetail, type ProposalDetail} from "./api/proposal"; +import { getBrandSummary, type BrandSummary } from "./api/brand"; import Header from "../../../components/layout/Header"; import CampaignBrandCard from "../components/CampaignBrandCard"; @@ -19,7 +19,7 @@ export default function ProposalContent() { // 데이터 상태 관리 const [data, setData] = useState(null); - const [brand] = useState(null); + const [brand, setBrand] = useState(null); const [isLoading, setIsLoading] = useState(true); const proposalId = searchParams.get("proposalId"); @@ -37,7 +37,11 @@ export default function ProposalContent() { const proposalResult = await getProposalDetail(proposalId); setData(proposalResult); - + // 2. 제안 정보에 brandId가 있다면 브랜드 요약 정보 조회 + if (proposalResult.brandId) { + const brandResult = await getBrandSummary(proposalResult.brandId); + setBrand(brandResult); + } } catch (error) { console.error("제안 상세 조회 실패:", error); @@ -64,8 +68,9 @@ export default function ProposalContent() {
diff --git a/app/routes/matching/brand/brand-content.tsx b/app/routes/matching/brand/brand-content.tsx index 084937aa..98662fe0 100644 --- a/app/routes/matching/brand/brand-content.tsx +++ b/app/routes/matching/brand/brand-content.tsx @@ -48,7 +48,7 @@ export default function BrandContent() { return response; }, initialPageParam: 0, - getNextPageParam: () => undefined, // 단일 페이지 + getNextPageParam: () => undefined, staleTime: 1000 * 60 * 1, // 1분간 캐시 유지 }); diff --git a/package.json b/package.json index 95abc3af..81388883 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@tanstack/react-query": "^5.90.16", "axios": "^1.13.4", "clsx": "^2.1.1", + "date-fns": "^4.1.0", "iconify": "^1.4.0", "isbot": "^5.1.17", "jwt-decode": "^4.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1679b36c..dbc7100a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + date-fns: + specifier: ^4.1.0 + version: 4.1.0 iconify: specifier: ^1.4.0 version: 1.4.0 @@ -1755,6 +1758,9 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -2121,6 +2127,7 @@ packages: glob@11.1.0: resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true globals@14.0.0: @@ -5292,6 +5299,8 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 + date-fns@4.1.0: {} + debug@2.6.9: dependencies: ms: 2.0.0