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
35 changes: 35 additions & 0 deletions app/routes/business/calendar/api/calendar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { axiosInstance } from "../../../../api/axios";

export interface CampaignCollaboration {
campaignId: number;
proposalId: string | null;
brandName: string;
thumbnailUrl: string;
title: string;
status: "NONE" | "REVIEWING" | "MATCHED" | "REJECTED";
startDate: string; // "2026-02-01"
endDate: string;
type: "APPLIED" | "SENT" | "RECEIVED";
}

interface CollaborationResponse {
isSuccess: boolean;
code: string;
message: string;
result: CampaignCollaboration[];
}

// 협업 내역 조회 API
export const getMyCollaborations = async (params?: {
type?: "APPLIED" | "SENT" | "RECEIVED";
status?: "NONE" | "REVIEWING" | "MATCHED" | "REJECTED";
startDate?: string;
endDate?: string;
}) => {
// axios -> axiosInstance로 수정
const response = await axiosInstance.get<CollaborationResponse>(
"/api/v1/campaigns/collaborations/me",
{ params }
);
return response.data.result;
};
68 changes: 62 additions & 6 deletions app/routes/business/calendar/calendar-content.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState } from "react";

import { useState, useEffect } from "react";
import { getMyCollaborations } from "./api/calendar";
import type { CampaignCollaboration } from "./api/calendar";
import FilterBottomSheet from "../components/FilterBottomSheet";
import WeeklyCalendar from "../components/WeeklyCalendar";
import MonthlyCalendar from "../components/MonthlyCalendar";
Expand All @@ -10,6 +11,7 @@ import MatchingTabSection from "../components/MatchingTabSection";
import dropdownIcon from "../../../assets/arrow-down.svg";
import EmptyState from "../components/EmptyState";


export default function CalendarContent() {
const [mainTab, setMainTab] = useState<"collaboration" | "matching">("collaboration");
const [activeTab, setActiveTab] = useState<"thisMonth" | "today">("thisMonth");
Expand All @@ -19,6 +21,39 @@ export default function CalendarContent() {

const hasData = true;

// API 데이터 상태
const [campaigns, setCampaigns] = useState<CampaignCollaboration[]>([]);
const [isLoading, setIsLoading] = useState(true);
// 데이터 로드
useEffect(() => {
const fetchCampaigns = async () => {
try {
setIsLoading(true);
// 협업 현황 조회를 위해 전체 데이터를 가져옵니다.
const data = await getMyCollaborations();
setCampaigns(data);
} catch (error) {
console.error("캠페인 로드 실패:", error);
} finally {
setIsLoading(false);
}
};

fetchCampaigns();
}, []);

// [필터링 로직]
const todayStr = new Date().toISOString().split('T')[0];
const currentMonthStr = todayStr.substring(0, 7); // "2026-02"

const filteredList = campaigns.filter((item) => {
if (activeTab === "today") {
return item.startDate <= todayStr && item.endDate >= todayStr;
}
// 이번달 기준 (시작일이나 종료일이 이번 달에 포함된 경우)
return item.startDate.includes(currentMonthStr) || item.endDate.includes(currentMonthStr);
});

return (
<div className="flex flex-col w-full min-h-screen bg-bluegray-1">
{/* 탭 네비게이션 */}
Expand Down Expand Up @@ -49,15 +84,18 @@ export default function CalendarContent() {
{mainTab === "collaboration" ? (
/* [A] 협업 현황 */
<div className="flex flex-col gap-6 px-4 py-6">
{/* 주간 캘린더 연동 */}
<section className="flex flex-col gap-3">
<SectionTitle title="진행 중인 협업" />
<p className="text-title1 font-bold text-text-black">이번주 일정</p>
<WeeklyCalendar />
<WeeklyCalendar events={campaigns} />
</section>
<section className="flex flex-col gap-3">
<p className="text-title1 font-bold text-text-black">이번달 일정</p>
<MonthlyCalendar />
<MonthlyCalendar events={campaigns} />
</section>

{/* 하단 리스트 섹션 */}
<section className="flex flex-col gap-4">
<div className="flex items-center gap-2">
<button
Expand All @@ -75,8 +113,26 @@ export default function CalendarContent() {
</button>
</div>
<div className="flex flex-col gap-4">
<CampaignCard brand="비플레인" title="비플레인 클렌징 및 세럼 리뷰" startDate="12.21" endDate="12.24" />
<CampaignCard brand="비플레인" title="비플레인 클렌징 및 세럼 리뷰" startDate="12.23" endDate="12.26" />
{isLoading ? (
<p className="text-center py-10 text-text-gray3">로딩 중...</p>
) : filteredList.length > 0 ? (
filteredList.map((cp) => (
<CampaignCard
key={cp.campaignId || cp.proposalId}
campaignId={cp.campaignId}
brand={cp.brandName}
title={cp.title}
logo={cp.thumbnailUrl}
// 날짜 포맷 변경 (2026-02-01 -> 02.01)
startDate={cp.startDate.split('-').slice(1).join('.')}
endDate={cp.endDate.split('-').slice(1).join('.')}
/>
))
) : (
<div className="py-10">
<EmptyState message="해당하는 캠페인 일정이 없어요" />
</div>
)}
</div>
</section>
</div>
Expand Down
33 changes: 17 additions & 16 deletions app/routes/business/components/CampaignCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ export default function CampaignCard({
campaignId = 1,
}: CampaignCardProps) {
const navigate = useNavigate();
const BUTTON_WIDTH = '280px';

// 로고 컴포넌트
const Logo = (
Expand All @@ -39,39 +38,41 @@ export default function CampaignCard({

return (
<Card image={Logo}>
<div
className="flex flex-col h-[72px] justify-between"
style={{ width: BUTTON_WIDTH }}
>
{/* 브랜드 + 제목/날짜 */}
<div className="flex flex-col w-full">
{/* 1. 컨테이너: 세로 배치, 간격 10px, 하단 정렬 */}
<div className="flex flex-col justify-end items-end gap-[10px] w-full self-stretch">

{/* 2. 상단 정보 섹션 */}
<div className="flex flex-col w-full gap-1">
{/* 브랜드명 */}
<div className="flex items-center gap-1">
<span className="text-[17px] font-bold text-text-black leading-tight">{brand}</span>
<span className="text-[17px] font-bold text-text-black leading-tight">
{brand}
</span>
<img src={ArrowRight} alt="이동" className="w-4 h-4 object-contain" />
</div>

{/* 제목 및 날짜: 양 끝 정렬 */}
<div className="flex justify-between items-start w-full">
<p className="text-[13px] text-text-gray2 truncate flex-1 leading-tight">
<p className="text-[13px] text-text-gray2 truncate flex-1 leading-tight mr-2">
{title}
</p>
<div className="flex flex-col items-end text-[10px] text-text-gray3 leading-tight ml-2 flex-shrink-0">
<span>{startDate}.25</span>
<span>{endDate}.25</span>
<div className="flex flex-col items-end text-[10px] text-text-gray3 leading-tight flex-shrink-0">
<span>{startDate}</span>
<span>{endDate}</span>
</div>
</div>
</div>

{/* 캠페인 보기 버튼 */}
{/* 3. 캠페인 보기 버튼: 디자인 규격에 맞춰 높이와 너비 조정 */}
{showButton && (
<button
onClick={() => navigate(`/business/campaign/${campaignId}`)}
className="relative flex items-center justify-center bg-[#EBEEFB] rounded-[6px] transition-colors hover:bg-[#DEE2F5]"
style={{ width: BUTTON_WIDTH, height: '50px' }}
className="flex items-center justify-center bg-[#EBEEFB] rounded-[6px] transition-colors hover:bg-[#DEE2F5] w-full h-[32px] gap-[4px]"
>
<img
src={SearchIcon}
alt="돋보기"
className="absolute left-[12px] w-3.5 h-3.5 object-contain"
className="w-3.5 h-3.5 object-contain"
/>
<span className="text-[13px] font-medium text-text-black">
캠페인 보기
Expand Down
131 changes: 76 additions & 55 deletions app/routes/business/components/MonthlyCalendar.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,58 @@
import { useState, useMemo } from "react";
import type { CampaignCollaboration } from "../calendar/api/calendar";
import ArrowLeftIcon from "../../../assets/icon/arrow-left.svg";
import ArrowRightIcon from "../../../assets/icon/arrow-right.svg";

interface MonthlyCalendarProps {
events: CampaignCollaboration[];
}

const WEEK_DAYS = ["일", "월", "화", "수", "목", "금", "토"];

export default function MonthlyCalendar() {
const emptyDays = Array(2).fill(null);
const days = Array.from({ length: 31 }, (_, i) => i + 1);
const allSlots = [...emptyDays, ...days];
export default function MonthlyCalendar({ events }: MonthlyCalendarProps) {
// 1. 현재 표시할 날짜 상태 (누락되었던 부분 추가)
const [currentDate, setCurrentDate] = useState(new Date());

// 2. 해당 월의 정보 계산
const { year, month, allSlots } = useMemo(() => {
const y = currentDate.getFullYear();
const m = currentDate.getMonth(); // 0 ~ 11

const firstDay = new Date(y, m, 1).getDay();
const lastDate = new Date(y, m + 1, 0).getDate();

const emptyDays = Array(firstDay).fill(null);
const days = Array.from({ length: lastDate }, (_, i) => i + 1);

return { year: y, month: m + 1, allSlots: [...emptyDays, ...days] };
}, [currentDate]);

// 월 이동 핸들러
const handlePrevMonth = () => setCurrentDate(new Date(year, month - 2, 1));
const handleNextMonth = () => setCurrentDate(new Date(year, month, 1));

// 3. 특정 날짜에 이벤트가 있는지 확인하는 함수
const getEventsForDay = (day: number | null) => {
if (!day) return [];
// YYYY-MM-DD 형식으로 비교
const targetStr = `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;

return events.filter(event => {
return event.startDate <= targetStr && event.endDate >= targetStr;
});
};

return (
<div className="flex flex-col gap-6 p-4 bg-white rounded-xl shadow-sm">
{/* 상단 월 이동 */}
<div className="flex items-center justify-center gap-1">
<button type="button" className="p-1 active:opacity-60">
<button onClick={handlePrevMonth} type="button" className="p-1 active:opacity-60">
<img src={ArrowLeftIcon} alt="이전 달" className="w-5 h-5" />
</button>
<span className="mx-2 text-[17px] font-bold text-text-black">
2025년 07월
{year}년 {String(month).padStart(2, "0")}월
</span>
<button type="button" className="p-1 active:opacity-60">
<button onClick={handleNextMonth} type="button" className="p-1 active:opacity-60">
<img src={ArrowRightIcon} alt="다음 달" className="w-5 h-5" />
</button>
</div>
Expand All @@ -33,57 +67,44 @@ export default function MonthlyCalendar() {

{/* 날짜 그리드 */}
<div className="grid grid-cols-7 text-center border-t border-gray-50">
{allSlots.map((day, index) => (
<div key={index} className="h-28 relative pt-2">
<div className="h-8 flex items-center justify-center">
{day && (
<span
className={`inline-flex items-center justify-center w-8 h-8 rounded-full text-[15px]
${day === 11
? "bg-[var(--color-core-1)] text-white"
: "text-text-black"
}`}
>
{day}
</span>
)}
</div>
</div>
))}
</div>
{allSlots.map((day, index) => {
const dayEvents = getEventsForDay(day);
const isToday = day === new Date().getDate() && month === (new Date().getMonth() + 1) && year === new Date().getFullYear();

{/* 일정 바 */}
<div className="absolute top-[85px] left-0 w-full pointer-events-none">
{/* 비플레인 */}
<div
className="absolute flex items-center justify-center text-[11px] text-white shadow-sm bg-grad-1"
style={{
left: "28.57%",
width: "57.14%",
height: "20px",
borderRadius: "20px",
top: "0px",
}}
>
비플레인
</div>
return (
<div key={index} className="h-20 border-b border-gray-50 relative pt-2 flex flex-col items-center">
{/* 날짜 표시 */}
<div className="h-7 flex items-center justify-center mb-1">
{day && (
<span className={`text-[14px] w-6 h-6 flex items-center justify-center rounded-full ${
isToday ? "bg-core-1 text-white font-bold" : "text-text-black"
}`}>
{day}
</span>
)}
</div>

{/* 라운드랩 */}
<div
className="absolute flex items-center justify-center text-[11px] text-white shadow-sm bg-grad-2"
style={{
left: "57.14%",
width: "42.85%",
height: "20px",
borderRadius: "20px",
top: "25px",
}}
>
라운드랩
</div>
{/* 해당 날짜의 일정 바 (이벤트가 있을 때만 표시) */}
<div className="w-full px-1 flex flex-col gap-0.5">
{dayEvents.slice(0, 2).map((event, idx) => (
<div
key={event.campaignId || event.proposalId}
className={`h-3 w-full rounded-sm text-[8px] text-white truncate px-1 ${
idx % 2 === 0 ? "bg-grad-1" : "bg-grad-2"
}`}
>
{event.brandName}
</div>
))}
{dayEvents.length > 2 && (
<span className="text-[8px] text-text-gray3">+{dayEvents.length - 2}</span>
)}
</div>
</div>
);
})}
</div>

</div>
</div>
);
}
}
Loading