diff --git a/app/api/axios.ts b/app/api/axios.ts index 7d77ebb7..bfd944af 100644 --- a/app/api/axios.ts +++ b/app/api/axios.ts @@ -121,5 +121,4 @@ axiosInstance.interceptors.response.use( }, ); -// apiClient alias for backward compatibility export const apiClient = axiosInstance; diff --git a/app/components/common/Modal.tsx b/app/components/common/Modal.tsx index 5f2c5c1d..a7a118c2 100644 --- a/app/components/common/Modal.tsx +++ b/app/components/common/Modal.tsx @@ -11,11 +11,11 @@ export default function Modal({ isOpen, onClose, children }: ModalProps) {
{/* 배경 레이어 */}
{/* 모달 본체 */} -
+
{children}
diff --git a/app/components/layout/Header.tsx b/app/components/layout/Header.tsx index 81cad4b5..8880859f 100644 --- a/app/components/layout/Header.tsx +++ b/app/components/layout/Header.tsx @@ -12,7 +12,7 @@ export default function Header({ rightElement, }: HeaderProps) { return ( -
+
{/* 왼쪽: 뒤로가기 버튼 */}
{showBack && ( diff --git a/app/routes/business/calendar/api/calendar.ts b/app/routes/business/calendar/api/calendar.ts index f79c2d82..93d8330d 100644 --- a/app/routes/business/calendar/api/calendar.ts +++ b/app/routes/business/calendar/api/calendar.ts @@ -28,7 +28,7 @@ export const getMyCollaborations = async (params?: { }) => { // axios -> axiosInstance로 수정 const response = await axiosInstance.get( - "/api/v1/campaigns/collaborations/me", + "/v1/campaigns/collaborations/me", { params } ); return response.data.result; diff --git a/app/routes/business/calendar/calendar-content.tsx b/app/routes/business/calendar/calendar-content.tsx index e12ef5f0..6d29ef4b 100644 --- a/app/routes/business/calendar/calendar-content.tsx +++ b/app/routes/business/calendar/calendar-content.tsx @@ -13,7 +13,6 @@ 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(); const [mainTab, setMainTab] = useState<"collaboration" | "matching">("collaboration"); @@ -22,8 +21,6 @@ export default function CalendarContent() { const [isFilterOpen, setIsFilterOpen] = useState(false); const [activeFilter, setActiveFilter] = useState("전체"); - //const hasData = true; - const [campaigns, setCampaigns] = useState([]); const [isLoading, setIsLoading] = useState(true); @@ -31,7 +28,6 @@ export default function CalendarContent() { const fetchCampaigns = async () => { try { setIsLoading(true); - // matchingSubTab 값에 따라 대문자로 변환하여 API 요청 const data = await getMyCollaborations({ type: matchingSubTab.toUpperCase() as "APPLIED" | "SENT" | "RECEIVED" }); @@ -43,7 +39,7 @@ export default function CalendarContent() { } }; fetchCampaigns(); - }, [matchingSubTab]); // 탭 클릭 시마다 API 다시 호출 + }, [matchingSubTab]); // 상태 변환 헬퍼 함수 const getStatusLabel = (status: CampaignCollaboration["status"]): "매칭" | "검토 중" | "거절" => { @@ -51,17 +47,15 @@ export default function CalendarContent() { case "MATCHED": return "매칭"; case "REVIEWING": - case "NONE": // NONE 상태도 '검토 중'으로 처리하거나 기획에 맞게 할당 + case "NONE": return "검토 중"; case "REJECTED": return "거절"; default: - // API에서 예상치 못한 값이 올 경우 기본값으로 "검토 중"을 반환하여 에러 방지 return "검토 중"; } }; - const matchingList = campaigns.filter((item) => { const isCorrectSubTab = matchingSubTab === "sent" ? item.type === "SENT" : @@ -70,7 +64,6 @@ export default function CalendarContent() { if (!isCorrectSubTab) return false; - // activeFilter가 "전체"가 아닐 때 데이터가 사라지는지 확인 if (activeFilter === "전체") return true; return getStatusLabel(item.status) === activeFilter; }); @@ -86,22 +79,27 @@ export default function CalendarContent() { if (activeTab === "today") { return item.startDate <= todayStr && item.endDate >= todayStr; } - // 이번달 기준 (시작일이나 종료일이 이번 달에 포함된 경우) return item.startDate.includes(currentMonthStr) || item.endDate.includes(currentMonthStr); }); const handleCardClick = (item: CampaignCollaboration) => { - // 1. 거절된 상태일 경우 거절 사유 페이지로 이동 - if (item.status === "REJECTED") { - navigate(`/rejection?id=${item.campaignId || item.proposalId}`); - } - // 2. 그 외(매칭, 검토 중) 상태일 경우 제안 상세 조회 페이지로 이동 - else { - // API 명세에 따른 campaignProposalId (item의 proposalId 혹은 campaignId)를 경로에 전달 - const proposalId = item.proposalId || item.campaignId; - navigate(`/business/proposal?id=${proposalId}`); - } - }; + const proposalId = item.proposalId || item.campaignId; + + // 1. 거절 상태일 때 + if (item.status === "REJECTED") { + navigate(`/rejection?proposalId=${proposalId}`); + return; + } + + // 2. 지원하기 타입일 때 + if (item.type === "APPLIED") { + navigate(`/business/proposal?type=applied&applicationId=${proposalId}`); + return; + } + + // 3. 그 외 기본 + navigate(`/business/proposal?proposalId=${proposalId}`); +}; return (
@@ -191,7 +189,7 @@ export default function CalendarContent() { c.type === "RECEIVED").length} /> @@ -218,7 +216,6 @@ export default function CalendarContent() { status={getStatusLabel(item.status)} date={item.startDate.split('-').slice(1).join('.') + "." + item.startDate.split('-')[0].slice(2)} actionLabel={item.status === "REJECTED" ? "거절 사유 보기" : "제안 보기"} - // [수정된 부분] item 객체 자체를 넘겨서 상태에 따라 분기 처리 onClick={() => handleCardClick(item)} /> )) diff --git a/app/routes/business/components/MatchingCard.tsx b/app/routes/business/components/MatchingCard.tsx index 62868e53..90f593af 100644 --- a/app/routes/business/components/MatchingCard.tsx +++ b/app/routes/business/components/MatchingCard.tsx @@ -44,7 +44,7 @@ export default function MatchingCard({
-
+
{brand} @@ -54,13 +54,13 @@ export default function MatchingCard({
- {/* 제안 보기 버튼 */} + {/* 액션 버튼 (제안 보기 / 지원 보기 / 거절 사유 보기) */} {/* 채팅 버튼 */} diff --git a/app/routes/business/proposal/api/proposal.ts b/app/routes/business/proposal/api/proposal.ts index 7cca7bf2..4841ca0b 100644 --- a/app/routes/business/proposal/api/proposal.ts +++ b/app/routes/business/proposal/api/proposal.ts @@ -1,7 +1,7 @@ +import { AxiosError } from "axios"; import { axiosInstance } from "../../../../api/axios"; import type { BrandDetail } from "../../../../data/brand"; -// 1. 응답 데이터의 공통 포맷 정의 (isSuccess 등을 포함) export interface ApiResponse { isSuccess: boolean; code: string; @@ -33,7 +33,7 @@ export interface ProposalDetail { export const getProposalDetail = async (proposalId: string): Promise => { try { const response = await axiosInstance.get>( - `/api/v1/campaigns/proposal/${proposalId}` + `/v1/campaigns/proposal/${proposalId}` ); if (response.data.isSuccess) { @@ -51,7 +51,7 @@ export const getProposalDetail = async (proposalId: string): Promise => { try { const response = await axiosInstance.get>( - `/api/v1/brands/${brandId}` + `/v1/brands/${brandId}` ); if (response.data.isSuccess) { @@ -64,4 +64,39 @@ export const getBrandDetail = async (brandId: number | string): Promise => { + try { + const response = await axiosInstance.get( + `/v1/campaigns/${campaignId}/apply/me` + ); + + if (response.data) { + return response.data; + } + + throw new Error("데이터를 가져오지 못했습니다."); + } catch (error: unknown) { + console.error("지원 상세 조회 실패:", error); + + if (error instanceof AxiosError) { + const errorMessage = error.response?.data?.message || "지원 상세 데이터 로드 실패"; + throw new Error(errorMessage); + } + + throw new 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 new file mode 100644 index 00000000..2ca7d74a --- /dev/null +++ b/app/routes/business/proposal/application-content.tsx @@ -0,0 +1,185 @@ +import { useState, useEffect } from "react"; +import { useSearchParams } from "react-router-dom"; +import { getAppliedCampaignDetail, type AppliedCampaignDetail } from "./api/proposal"; + +import Modal from "../../../components/common/Modal"; + +import Header from "../../../components/layout/Header"; +import CampaignBrandCard from "../components/CampaignBrandCard"; +import CampaignInfoGroup from "../components/CampaignInfoGroup"; + +import arrowPurpleIcon from "../../../assets/arrow-purple.svg"; +import profileIcon from "../../../assets/icon-profile.svg"; + +import checkIcon from "../../../assets/icon/icon-check-circle.svg"; +import closeIcon from "../../../assets/icon/icon-close.svg"; + +export default function ApplicationContent() { + const [searchParams] = useSearchParams(); + const [data, setData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalStep, setModalStep] = useState<"CONFIRM" | "COMPLETE">("CONFIRM"); + + const applicationId = searchParams.get("applicationId"); + + const handleCloseModal = () => { + setIsModalOpen(false); + setTimeout(() => setModalStep("CONFIRM"), 300); + }; + + useEffect(() => { + if (!applicationId) { + setIsLoading(false); + return; + } + + const fetchData = async () => { + try { + setIsLoading(true); + const result = await getAppliedCampaignDetail(applicationId as string); + setData(result); + } catch (error) { + console.error("지원 상세 조회 실패:", error); + } finally { + setIsLoading(false); + } + }; + fetchData(); + }, [applicationId]); + + const handleCancelSubmit = async () => { + try { + console.log("지원 취소 프로세스 시작"); + setModalStep("COMPLETE"); + } catch (error) { + console.error("취소 실패:", error); + } + }; + + if (isLoading) return
로딩 중...
; + if (!data) return
데이터를 찾을 수 없습니다.
; + + const getStatusLabel = (status: string) => { + switch (status) { + case "REVIEWING": return "검토 중"; + case "MATCHED": return "수락됨"; + case "REJECTED": return "거절됨"; + case "CANCELED": return "취소됨"; + default: return "상태 미정"; + } + }; + + return ( +
+
+ +
+ {/* 상단 브랜드 정보 섹션 */} +
+ + +
+ {/* 캠페인 제목 */} +
+

+ ‘{data.campaignTitle}’ +

+ link +
+ + {/* 제안 프로필 */} +
+

제안 프로필

+
+
+
+ profile +
+ + @{data.creatorId || "ivveeee"} + +
+ arrow +
+
+
+
+ + {/* 지원 이유 섹션 */} +
+ +
+ {data.campaignReason || "작성된 지원 이유가 없습니다."} +
+
+
+ + {/* 하단 버튼 영역 */} +
+ +
+
+ + +
+ {/* 닫기 버튼 (X) */} + + + {/* 체크 아이콘 */} +
+ check +
+ + {modalStep === "CONFIRM" ? ( + <> +

+ 제안을 취소하시겠습니까? +

+
+ + +
+ + ) : ( + <> +

취소하기 완료

+

+ 브랜드에게 보낸
제안이 취소되었습니다 +

+ + + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/app/routes/business/proposal/route.tsx b/app/routes/business/proposal/route.tsx index fe2c90b1..9ba7927e 100644 --- a/app/routes/business/proposal/route.tsx +++ b/app/routes/business/proposal/route.tsx @@ -1,6 +1,7 @@ import { useSearchParams } from "react-router"; import ProposalContent from "./sent-proposal-content"; import ReceivedProposalContent from "./received-proposal-content"; +import ApplicationContent from "./application-content"; export default function Proposal() { const [searchParams] = useSearchParams(); @@ -10,5 +11,9 @@ export default function Proposal() { return ; } + if (type === "applied") { + return ; + } + return ; } diff --git a/app/routes/business/proposal/sent-proposal-content.tsx b/app/routes/business/proposal/sent-proposal-content.tsx index 6b0cc332..38441347 100644 --- a/app/routes/business/proposal/sent-proposal-content.tsx +++ b/app/routes/business/proposal/sent-proposal-content.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; import { useSearchParams } from "react-router-dom"; -import { getProposalDetail, getBrandDetail, type ProposalDetail} from "./api/proposal"; // 경로 확인 필요 +import { getProposalDetail, type ProposalDetail} from "./api/proposal"; // 경로 확인 필요 import type { BrandDetail } from "../../../data/brand"; import Header from "../../../components/layout/Header"; @@ -19,23 +19,25 @@ export default function ProposalContent() { // 데이터 상태 관리 const [data, setData] = useState(null); - const [brand, setBrand] = useState(null); + const [brand] = useState(null); const [isLoading, setIsLoading] = useState(true); - const proposalId = searchParams.get("proposalId") || "29"; + const proposalId = searchParams.get("proposalId"); useEffect(() => { + if (!proposalId) { + setIsLoading(false); + return; + } + const fetchData = async () => { + console.log("실제 넘길 ID:", proposalId); try { setIsLoading(true); const proposalResult = await getProposalDetail(proposalId); setData(proposalResult); - // 브랜드 상세 정보 - if (proposalResult.brandId) { - const brandResult = await getBrandDetail(proposalResult.brandId); - setBrand(brandResult); - } + } catch (error) { console.error("제안 상세 조회 실패:", error);