From ca9719b4a4d0ed1d3f9fe2d21d5f35fbda329e98 Mon Sep 17 00:00:00 2001 From: Jio Date: Mon, 2 Feb 2026 00:39:10 +0900 Subject: [PATCH 1/3] =?UTF-8?q?rename:=20=EC=A0=9C=EC=95=88=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0=20=EB=B2=84=ED=8A=BC=20=EB=9D=BC=EC=9A=B0=ED=8A=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business/calendar/calendar-content.tsx | 46 +++++++++++++++---- .../business/components/MatchingCard.tsx | 7 ++- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/app/routes/business/calendar/calendar-content.tsx b/app/routes/business/calendar/calendar-content.tsx index e8b9930..d1f8722 100644 --- a/app/routes/business/calendar/calendar-content.tsx +++ b/app/routes/business/calendar/calendar-content.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; import { getMyCollaborations } from "./api/calendar"; import type { CampaignCollaboration } from "./api/calendar"; import FilterBottomSheet from "../components/FilterBottomSheet"; @@ -13,6 +14,7 @@ import EmptyState from "../components/EmptyState"; export default function CalendarContent() { + const navigate = useNavigate(); const [mainTab, setMainTab] = useState<"collaboration" | "matching">("collaboration"); const [activeTab, setActiveTab] = useState<"thisMonth" | "today">("thisMonth"); const [matchingSubTab, setMatchingSubTab] = useState<"sent" | "received">("sent"); @@ -54,6 +56,10 @@ export default function CalendarContent() { return item.startDate.includes(currentMonthStr) || item.endDate.includes(currentMonthStr); }); + const handleCardClick = (type: "sent" | "received") => { + navigate(`/business/proposal?type=${type}`); + }; + return (
{/* 탭 네비게이션 */} @@ -156,17 +162,41 @@ export default function CalendarContent() {
{matchingSubTab === "sent" ? ( <> - - - - + handleCardClick("sent")} // 4. 핸들러 연결 + /> + handleCardClick("sent")} + /> + handleCardClick("sent")} + /> + handleCardClick("sent")} + /> ) : ( <> - - - - + handleCardClick("received")} // 4. 핸들러 연결 + /> + handleCardClick("received")} + /> + handleCardClick("received")} + /> + handleCardClick("received")} + /> )}
diff --git a/app/routes/business/components/MatchingCard.tsx b/app/routes/business/components/MatchingCard.tsx index 97fb2fa..62868e5 100644 --- a/app/routes/business/components/MatchingCard.tsx +++ b/app/routes/business/components/MatchingCard.tsx @@ -6,6 +6,7 @@ interface MatchingCardProps { status: "매칭" | "검토 중" | "거절"; date: string; actionLabel: string; + onClick?: () => void; } export default function MatchingCard({ @@ -13,6 +14,7 @@ export default function MatchingCard({ status, date, actionLabel, + onClick, }: MatchingCardProps) { // 상태에 따른 텍스트 컬러 const getStatusStyle = () => { @@ -53,7 +55,10 @@ export default function MatchingCard({
{/* 제안 보기 버튼 */} - From 0264007b116d613852027b783e2ac247872fe160 Mon Sep 17 00:00:00 2001 From: Jio Date: Mon, 2 Feb 2026 03:32:38 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=EB=B3=B4=EB=82=B8=20=EC=A0=9C?= =?UTF-8?q?=EC=95=88=20-=20=EC=A0=9C=EC=95=88=20=EB=B3=B4=EA=B8=B0=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/data/brand.ts | 17 ++++ .../business/components/CampaignBrandCard.tsx | 17 +++- app/routes/business/proposal/api/proposal.ts | 91 +++++++++++++++++++ .../proposal/sent-proposal-content.tsx | 82 +++++++++++++---- 4 files changed, 183 insertions(+), 24 deletions(-) create mode 100644 app/routes/business/proposal/api/proposal.ts diff --git a/app/data/brand.ts b/app/data/brand.ts index 0f5afbc..3a0e344 100644 --- a/app/data/brand.ts +++ b/app/data/brand.ts @@ -20,3 +20,20 @@ export const BRAND_DATA: Brand[] = [ { id: 5, name: "디스이즈네버댓", matchRate: 88, tags: ["스트릿", "캐주얼", "트렌디"], isLiked: true, category: "FASHION" }, { id: 6, name: "커버낫", matchRate: 82, tags: ["아메리칸 캐주얼", "빈티지", "유니섹스"], isLiked: false, category: "FASHION" }, ]; + +// 캠페인 보기에서 사용하는 브랜드 +export interface BrandDetail { + brandName: string; + brandTag: string[]; + brandDescription: string; + brandMatchingRatio: number; + brandIsLiked: boolean; + brandCategory: string[]; +} + +export interface BrandResponse { + isSuccess: boolean; + code: string; + message: string; + result: BrandDetail[]; +} diff --git a/app/routes/business/components/CampaignBrandCard.tsx b/app/routes/business/components/CampaignBrandCard.tsx index aac6572..aedd1ce 100644 --- a/app/routes/business/components/CampaignBrandCard.tsx +++ b/app/routes/business/components/CampaignBrandCard.tsx @@ -3,13 +3,18 @@ import chatIcon from "../../../assets/chat-icon.svg"; import arrowRightIcon from "../../../assets/icon/arrow-right.svg"; interface CampaignBrandCardProps { - showChatSection?: boolean; // 채팅 섹션 표시 여부 + showChatSection?: boolean; statusText?: string; // '보낸 제안' 또는 '검토 중' + brandName?: string; + brandTags?: string[]; + } export default function CampaignBrandCard({ showChatSection = true, - statusText = "보낸 제안" + statusText = "보낸 제안", + brandName, + brandTags }: CampaignBrandCardProps) { return (
@@ -27,11 +32,15 @@ export default function CampaignBrandCard({
-

비플레인

+

+ {brandName || "브랜드명"} +

arrow

- #저자극 #천연재료 #민감성피부 + {brandTags && brandTags.length > 0 + ? brandTags.map(tag => `#${tag}`).join(" ") + : "#태그정보없음"}

diff --git a/app/routes/business/proposal/api/proposal.ts b/app/routes/business/proposal/api/proposal.ts new file mode 100644 index 0000000..cbe0635 --- /dev/null +++ b/app/routes/business/proposal/api/proposal.ts @@ -0,0 +1,91 @@ +import axios from "axios"; +import { tokenStorage } from "../../../../lib/token"; +import type { BrandDetail } from "../../../../data/brand"; + +// 1. 응답 데이터의 공통 포맷 정의 (isSuccess 등을 포함) +export interface ApiResponse { + isSuccess: boolean; + code: string; + message: string; + result: T; +} + +export interface ProposalDetail { + proposalId: number; + brandId: number; + creatorId: number; + title: string; + description: string; + rewardAmount: number; + productId: number; + startDate: string; + endDate: string; + status: string; + refusalReason: string | null; + contentTags: { + formats: { id: number; name: string }[]; + categories: { id: number; name: string }[]; + tones: { id: number; name: string }[]; + involvements: { id: number; name: string }[]; + usageRanges: { id: number; name: string }[]; + }; +} + +export const getProposalDetail = async (proposalId: string): Promise => { + const BASE_URL = "https://api.realmatch.co.kr"; + + // 1. tokenStorage 유틸을 사용하여 안전하게 토큰을 가져옵니다. + const token = tokenStorage.getAccessToken(); + + console.log("현재 보관된 토큰:", token); + console.log("토큰 만료 여부:", tokenStorage.isTokenExpired()); + console.log("현재 사용자 ID:", tokenStorage.getUserId()); + console.log("현재 사용자 역할(Role):", tokenStorage.getRole()); + + try { + const response = await axios.get(`${BASE_URL}/api/v1/campaigns/proposal/${proposalId}`, { + headers: { + // 2. 토큰이 있을 때만 Authorization 헤더를 추가합니다. + ...(token && { Authorization: `Bearer ${token}` }), + "accept": "*/*" + } + }); + + if (response.data.isSuccess) { + return response.data.result; + } + + throw new Error(response.data.message || "데이터 로드 실패"); + } catch (error: any) { + // 401 에러(인증 실패) 시 로그 확인 + if (error.response?.status === 401) { + console.error("401 에러: 토큰이 유효하지 않거나 로그인이 필요합니다."); + } + throw error; + } +}; + +// 브랜드 상세 정보를 가져오는 API 함수 예시 +export const getBrandDetail = async (brandId: number | string): Promise => { + const BASE_URL = "https://api.realmatch.co.kr"; + const token = tokenStorage.getAccessToken(); + + try { + const response = await axios.get(`${BASE_URL}/api/v1/brands/${brandId}`, { + headers: { + ...(token && { Authorization: `Bearer ${token}` }), + "accept": "*/*" + } + }); + + if (response.data.isSuccess) { + // 스웨거 응답 구조상 result가 배열이므로 첫 번째 요소를 반환 + return response.data.result[0]; + } + + throw new Error(response.data.message || "브랜드 정보 로드 실패"); + } catch (error: any) { + console.error("브랜드 상세 조회 실패:", error); + throw error; + } +}; \ No newline at end of file diff --git a/app/routes/business/proposal/sent-proposal-content.tsx b/app/routes/business/proposal/sent-proposal-content.tsx index 0a1975b..9ad2c7b 100644 --- a/app/routes/business/proposal/sent-proposal-content.tsx +++ b/app/routes/business/proposal/sent-proposal-content.tsx @@ -1,4 +1,7 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; +import { useSearchParams } from "react-router-dom"; +import { getProposalDetail, getBrandDetail, type ProposalDetail} from "./api/proposal"; // 경로 확인 필요 +import type { BrandDetail } from "../../../data/brand"; import Header from "../../../components/layout/Header"; import CampaignBrandCard from "../components/CampaignBrandCard"; @@ -11,45 +14,85 @@ import arrowPurpleIcon from "../../../assets/arrow-purple.svg"; import profileIcon from "../../../assets/icon-profile.svg"; export default function ProposalContent() { + const [searchParams] = useSearchParams(); const [isContentOpen, setIsContentOpen] = useState(false); + // 데이터 상태 관리 + const [data, setData] = useState(null); + const [brand, setBrand] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + const proposalId = searchParams.get("proposalId") || "1"; + + useEffect(() => { + const fetchData = async () => { + 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); + } finally { + setIsLoading(false); + } + }; + fetchData(); + }, [proposalId]); + + if (isLoading) return
로딩 중...
; + if (!data) return
데이터를 찾을 수 없습니다.
; + + // 태그 배열을 문자열로 변환하는 헬퍼 함수 + const getTagNames = (tags: { name: string }[]) => tags.map(t => t.name).join(", "); + return (
- {/* 1. 상단 섹션: 브랜드 카드 및 제안 프로필 */} + {/* 브랜드 카드 및 제안 프로필 */}
-

신규 캠페인

+

{data.title}

제안 프로필

- +
profile
- @ivveeee + + @{data.creatorId || "unknown"} +
- arrow + arrow
- {/* 2. 상세 정보 섹션 */} + {/* 상세 정보 섹션 */}
{/* 캠페인명 */}
- 비플레인 클렌징 및 세럼 리뷰 콘텐츠 + {data.title}
@@ -70,8 +113,7 @@ export default function ProposalContent() {

설명

- 안녕하세요 크리에이터 비비 입니다! 비플레인의 가치가 제 채널에서 소개하는 - 뷰티 콘텐츠와 잘 맞닿아 있다고 생각되어 협찬을 제안드립니다. + {data.description}
@@ -79,12 +121,12 @@ export default function ProposalContent() { {isContentOpen && (
- +
- - - - + + + +
)}
@@ -94,14 +136,14 @@ export default function ProposalContent() {
- 글로우 크림 1개 + 상품 ID: {data.productId} arrow
- 200,000 + {data.rewardAmount.toLocaleString()}
@@ -110,11 +152,11 @@ export default function ProposalContent() {
- 2025년 1월 20일 + {(data?.startDate || "").replace(/-/g, '. ')}
~
- 2025년 1월 30일 + {(data?.endDate || "").replace(/-/g, '. ')}
From e8dd41d7cb84e5aab8e0cdc556d6924adb83ccd7 Mon Sep 17 00:00:00 2001 From: Jio Date: Mon, 2 Feb 2026 14:12:24 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20any=20type=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routes/business/proposal/api/proposal.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/app/routes/business/proposal/api/proposal.ts b/app/routes/business/proposal/api/proposal.ts index cbe0635..cfca5e8 100644 --- a/app/routes/business/proposal/api/proposal.ts +++ b/app/routes/business/proposal/api/proposal.ts @@ -33,7 +33,7 @@ export interface ProposalDetail { export const getProposalDetail = async (proposalId: string): Promise => { const BASE_URL = "https://api.realmatch.co.kr"; - + // 1. tokenStorage 유틸을 사용하여 안전하게 토큰을 가져옵니다. const token = tokenStorage.getAccessToken(); @@ -54,12 +54,13 @@ export const getProposalDetail = async (proposalId: string): Promise