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
17 changes: 17 additions & 0 deletions app/data/brand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
}
46 changes: 38 additions & 8 deletions app/routes/business/calendar/calendar-content.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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");
Expand Down Expand Up @@ -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 (
<div className="flex flex-col w-full min-h-screen bg-bluegray-1">
{/* 탭 네비게이션 */}
Expand Down Expand Up @@ -156,17 +162,41 @@ export default function CalendarContent() {
<div className="flex flex-col gap-4">
{matchingSubTab === "sent" ? (
<>
<MatchingCard brand="라운드랩" status="매칭" date="12.23.25" actionLabel="제안 보기" />
<MatchingCard brand="비플레인" status="검토 중" date="12.23.25" actionLabel="제안 보기" />
<MatchingCard brand="땡큐파머" status="검토 중" date="12.23.25" actionLabel="제안 보기" />
<MatchingCard brand="이즈트리" status="거절" date="12.23.25" actionLabel="거절 사유 보기" />
<MatchingCard
brand="라운드랩" status="매칭" date="12.23.25" actionLabel="제안 보기"
onClick={() => handleCardClick("sent")} // 4. 핸들러 연결
/>
<MatchingCard
brand="비플레인" status="검토 중" date="12.23.25" actionLabel="제안 보기"
onClick={() => handleCardClick("sent")}
/>
<MatchingCard
brand="땡큐파머" status="검토 중" date="12.23.25" actionLabel="제안 보기"
onClick={() => handleCardClick("sent")}
/>
<MatchingCard
brand="이즈트리" status="거절" date="12.23.25" actionLabel="거절 사유 보기"
onClick={() => handleCardClick("sent")}
/>
</>
) : (
<>
<MatchingCard brand="라운드랩" status="매칭" date="12.23.25" actionLabel="제안 보기" />
<MatchingCard brand="비플레인" status="검토 중" date="12.23.25" actionLabel="제안 보기" />
<MatchingCard brand="그레이스유" status="검토 중" date="12.23.25" actionLabel="제안 보기" />
<MatchingCard brand="이즈트리" status="거절" date="12.23.25" actionLabel="거절 사유 보기" />
<MatchingCard
brand="라운드랩" status="매칭" date="12.23.25" actionLabel="제안 보기"
onClick={() => handleCardClick("received")} // 4. 핸들러 연결
/>
<MatchingCard
brand="비플레인" status="검토 중" date="12.23.25" actionLabel="제안 보기"
onClick={() => handleCardClick("received")}
/>
<MatchingCard
brand="그레이스유" status="검토 중" date="12.23.25" actionLabel="제안 보기"
onClick={() => handleCardClick("received")}
/>
<MatchingCard
brand="이즈트리" status="거절" date="12.23.25" actionLabel="거절 사유 보기"
onClick={() => handleCardClick("received")}
/>
</>
)}
</div>
Expand Down
17 changes: 13 additions & 4 deletions app/routes/business/components/CampaignBrandCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<section className="bg-bg-w p-5 flex flex-col gap-4 -mx-4 -mt-6">
Expand All @@ -27,11 +32,15 @@ export default function CampaignBrandCard({

<div className="flex flex-col gap-1">
<div className="flex items-center gap-1">
<h2 className="text-title1 text-text-black">비플레인</h2>
<h2 className="text-title1 text-text-black">
{brandName || "브랜드명"}
</h2>
<img src={arrowRightIcon} alt="arrow"/>
</div>
<p className="text-callout1 text-text-gray3 mt-3">
#저자극 #천연재료 #민감성피부
{brandTags && brandTags.length > 0
? brandTags.map(tag => `#${tag}`).join(" ")
: "#태그정보없음"}
</p>
</div>
</div>
Expand Down
7 changes: 6 additions & 1 deletion app/routes/business/components/MatchingCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ interface MatchingCardProps {
status: "매칭" | "검토 중" | "거절";
date: string;
actionLabel: string;
onClick?: () => void;
}

export default function MatchingCard({
brand,
status,
date,
actionLabel,
onClick,
}: MatchingCardProps) {
// 상태에 따른 텍스트 컬러
const getStatusStyle = () => {
Expand Down Expand Up @@ -53,7 +55,10 @@ export default function MatchingCard({

<div className="flex gap-2 items-center">
{/* 제안 보기 버튼 */}
<button className="flex-2 py-2 flex items-center justify-center gap-1.5 bg-bluegray-2 rounded-lg transition-opacity hover:opacity-90">
<button
onClick={onClick}
className="flex-2 py-2 flex items-center justify-center gap-1.5 bg-bluegray-2 rounded-lg transition-opacity hover:opacity-90 active:scale-[0.98]"
>
<img src={searchIcon} alt="조회" className="w-3.5 h-3.5" />
<span className="text-title7 text-text-gray2">{actionLabel}</span>
</button>
Expand Down
92 changes: 92 additions & 0 deletions app/routes/business/proposal/api/proposal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import axios from "axios";
import { tokenStorage } from "../../../../lib/token";
import type { BrandDetail } from "../../../../data/brand";

// 1. 응답 데이터의 공통 포맷 정의 (isSuccess 등을 포함)
export interface ApiResponse<T> {
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<ProposalDetail> => {
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 삭제
if (axios.isAxiosError(error)) { // axios 에러인지 확인하는 가드 추가 (권장)
if (error.response?.status === 401) {
console.error("401 에러: 토큰이 유효하지 않거나 로그인이 필요합니다.");
}
}
throw error;
}
};

// 브랜드 상세 정보를 가져오는 API 함수 예시
export const getBrandDetail = async (brandId: number | string): Promise<BrandDetail> => {
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;
}
};
Loading