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
64 changes: 64 additions & 0 deletions app/components/common/ConfirmModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import Modal from "./Modal";
import Button from "./Button";
import CheckCircleIcon from "../../assets/icon/icon-check-circle.svg";
import closeIcon from "../../assets/icon/icon-close.svg";


type ConfirmModalProps = {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
title: string;
};

export default function ConfirmModal({
isOpen,
onClose,
onConfirm,
title,
}: ConfirmModalProps) {
return (
<Modal isOpen={isOpen} onClose={onClose} className="w-[310px] h-[310px] rounded-[10px] p-6 relative">
{/* 상단 좌측 X 아이콘 추가 */}
<button
onClick={onClose}
className="absolute top-5 left-5 p-1 active:opacity-50 transition-opacity"
>
<img src={closeIcon} alt="close" className="w-5 h-5" />
</button>

<div className="flex flex-col items-center h-full">
<div className="flex-1 flex flex-col items-center justify-center w-full mt-4">
{/* 이미지 섹션 */}
<img src={CheckCircleIcon} alt="" className="w-[64px] h-[64px] mb-8" />

{/* 문구 섹션 */}
<h3 className="text-callout3 text-text-black text-center leading-tight">
{title}
</h3>
</div>

{/* 버튼 섹션 */}
<div className="w-full flex gap-3 mt-auto">
{/* '예' 버튼 */}
<Button
variant="outline"
className="w-[76px] h-[44px] text-[18px] font-semibold rounded-[16px] border-[#C5C7F9] text-[#6366F1] bg-white"
onClick={onConfirm}
>
</Button>

{/* '아니오' 버튼 */}
<Button
variant="primary"
className="w-[186px] h-[44px] text-[18px] font-semibold rounded-[16px] bg-[#6366F1] text-white border-none"
onClick={onClose}
>
아니오
</Button>
</div>
</div>
</Modal>
);
}
51 changes: 27 additions & 24 deletions app/routes/business/calendar/calendar-content.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState, useEffect, useMemo } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import { getMyCollaborations } from "./api/calendar";
import type { CampaignCollaboration } from "./api/calendar";
import { getMyCollaborations, searchCollaborations } 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 Down Expand Up @@ -30,25 +30,31 @@ export default function CalendarContent() {
const isFiltered = activeFilter !== "전체";

useEffect(() => {
const fetchAllCampaigns = async () => {
const fetchCampaigns = async () => {
try {
setIsLoading(true);

// 1. 키워드가 있을 때는 searchCollaborations API 사용
// 2. 키워드가 없을 때는 getMyCollaborations API 사용

const fetchFunction = keyword.trim() ? searchCollaborations : getMyCollaborations;

const [applied, sent, received] = await Promise.all([
getMyCollaborations({ type: "APPLIED", keyword: keyword.trim() || undefined }),
getMyCollaborations({ type: "SENT", keyword: keyword.trim() || undefined }),
getMyCollaborations({ type: "RECEIVED", keyword: keyword.trim() || undefined }),
fetchFunction({ type: "APPLIED", keyword: keyword.trim() || undefined }),
fetchFunction({ type: "SENT", keyword: keyword.trim() || undefined }),
fetchFunction({ type: "RECEIVED", keyword: keyword.trim() || undefined }),
]);

setCampaigns([...applied, ...sent, ...received]);
} catch (error) {
console.error("로드 실패:", error);
console.error("데이터 로드 실패:", error);
setCampaigns([]); // 에러 시 빈 배열 처리
} finally {
setIsLoading(false);
}
};

fetchAllCampaigns();
fetchCampaigns();
}, [keyword, location.key]);

// 날짜 계산을 별도 useMemo로 분리
Expand Down Expand Up @@ -92,27 +98,26 @@ export default function CalendarContent() {
}
};

// CalendarContent.tsx 내부 matchingList

const matchingList = useMemo(() => {
return campaigns.filter((item) => {
// 탭 필터링
const isCorrectSubTab =
matchingSubTab === "sent" ? item.type === "SENT" :
matchingSubTab === "received" ? item.type === "RECEIVED" :
item.type === "APPLIED";

if (!isCorrectSubTab) return false;

if (activeFilter === "전체") return true;

const statusMatches = getStatusLabel(item.status) === activeFilter;

const keywordMatches = keyword ? item.brandName.includes(keyword) : true;

return statusMatches && keywordMatches;


// 상태 필터링 (activeFilter가 "전체"가 아닐 때만 적용)
if (activeFilter !== "전체") {
return getStatusLabel(item.status) === activeFilter;
}

return true;
});
}, [campaigns, matchingSubTab, activeFilter, keyword]);
}, [campaigns, matchingSubTab, activeFilter]); // keyword는 이제 API 결과(campaigns)에 반영되어 있으므로 제외 가능

console.log("전체 데이터:", campaigns);
console.log("필터된 데이터:", matchingList);
Expand Down Expand Up @@ -226,20 +231,18 @@ export default function CalendarContent() {
<h2 className="text-title1 font-semibold text-text-black">매칭 현황</h2>
<button
onClick={() => setIsFilterOpen(true)}
className={`flex items-center w-fit h-7 pl-3 pr-1.5 rounded-full border text-[14px] font-medium text-[#5B5D6B] ${
isFiltered
className={`flex items-center w-fit h-7 pl-3 pr-1.5 rounded-full border text-[14px] font-medium text-[#5B5D6B] ${isFiltered
? "border-core-70 text-core-1 bg-core-70"
: "border-core-2 text-gray-2 bg-white"
}`}
}`}
>
{activeFilter}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="none"
className={`w-6 h-6 ${
isFiltered ? "text-core-1" : "text-text-gray2"
}`}
className={`w-6 h-6 ${isFiltered ? "text-core-1" : "text-text-gray2"
}`}
>
<path
d="M6 8L10 12L14 8"
Expand Down
62 changes: 9 additions & 53 deletions app/routes/business/components/MatchingTabSection.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { useState, useEffect, useCallback } from "react";
import { searchCollaborations, type CampaignCollaboration } from "../calendar/api/calendar";
import { useState } from "react";
import searchIcon from "../../../assets/search2.svg";
import closeIcon from "../../../assets/cancel.svg";
import LoadingSpinner from "../../../components/common/LoadingSpinner";

interface Props {
subTab: "sent" | "received" | "applied";
Expand All @@ -29,39 +27,10 @@ function TabButton({ label, active, onClick }: { label: string; active: boolean;

export default function MatchingTabSection({ subTab, setSubTab, receivedCount, keyword, setKeyword }: Props) {
const [isSearching, setIsSearching] = useState(false);
const [campaigns, setCampaigns] = useState<CampaignCollaboration[]>([]);
const [isLoading, setIsLoading] = useState(false);

// 캠페인 검색 함수
const fetchCampaigns = useCallback(async () => {
if (!keyword.trim()) return;
setIsLoading(true);
try {
const data = await searchCollaborations({
keyword: keyword.trim(),
type: subTab.toUpperCase() as "APPLIED" | "SENT" | "RECEIVED",
});
setCampaigns(data || []);
} catch (error) {
console.error("검색 실패:", error);
} finally {
setIsLoading(false);
}
}, [keyword, subTab]);

// 검색 상태에 따라 캠페인 검색
useEffect(() => {
if (isSearching && keyword.trim()) {
fetchCampaigns();
} else {
setCampaigns([]);
}
}, [isSearching, keyword, subTab, fetchCampaigns]);

if (isSearching) {
return (
<div className="flex flex-col w-full animate-slide-up">
{/* 검색바 섹션 */}
<div className="flex flex-col w-full animate-slide-up border-b border-text-gray5">
<div className="flex items-center w-full px-4 py-3">
<div className="flex items-center w-full relative bg-bg-w border border-core-2 rounded-[8px] px-3 py-2">
<img src={searchIcon} alt="search" className="w-4 h-4 opacity-40 flex-shrink-0" />
Expand All @@ -70,7 +39,7 @@ export default function MatchingTabSection({ subTab, setSubTab, receivedCount, k
className="flex-1 bg-transparent mx-2 outline-none text-body1 text-center placeholder:text-text-gray3"
placeholder="브랜드명 입력"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
onChange={(e) => setKeyword(e.target.value)} // 부모의 setKeyword 호출
/>
<button
onClick={() => { setIsSearching(false); setKeyword(""); }}
Expand All @@ -80,22 +49,6 @@ export default function MatchingTabSection({ subTab, setSubTab, receivedCount, k
</button>
</div>
</div>

{/* 로딩 표시 */}
{isLoading && <LoadingSpinner className="py-4" size={80} />}

{/* 검색 결과 리스트 */}
<div className="px-4 overflow-y-auto">
{!isLoading && campaigns.length > 0 ? (
campaigns.map((item) => (
<div key={item.campaignId} className="py-3 border-b border-text-gray5 text-[14px]">
{item.brandName} - {item.title} {/* 브랜드명과 제목 표시 */}
</div>
))
) : (
!isLoading && keyword && <div className="text-center py-10 text-text-gray4">검색 결과가 없습니다.</div>
)}
</div>
</div>
);
}
Expand All @@ -110,7 +63,7 @@ export default function MatchingTabSection({ subTab, setSubTab, receivedCount, k
/>
<button
onClick={() => setSubTab("received")}
className={`px-4 py-2 rounded-lg text-[14px] font-bold flex items-center gap-1 transition-all ${
className={`px-4 py-2 rounded-lg text-[14px] font-semibold flex items-center gap-1 transition-all ${
subTab === "received"
? "bg-core-1 text-white"
: "bg-white border border-text-gray5 text-text-gray3"
Expand All @@ -130,8 +83,11 @@ export default function MatchingTabSection({ subTab, setSubTab, receivedCount, k
onClick={() => setSubTab("applied")}
/>
</div>
<button onClick={() => setIsSearching(true)} className="p-1">
<img src={searchIcon} alt="search" className="w-6 h-6" />
<button
onClick={() => setIsSearching(true)}
className="w-[40px] h-[36px] flex items-center justify-center bg-white border border-text-gray5 rounded-[8px] active:bg-bluegray-2 transition-colors"
>
<img src={searchIcon} alt="search" className="w-5 h-5 opacity-40" />
</button>
</div>
);
Expand Down
12 changes: 8 additions & 4 deletions app/routes/business/proposal/application-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import { useLocation } from "react-router-dom";
import { getAppliedCampaignDetail, cancelCampaignApply, type AppliedCampaignDetail } from "./api/proposal";
import { getBrandSummary, type BrandSummary } from "./api/brand";
import { getProfileCard, type ProfileCard } from "./api/user";
import { useHideHeader } from "../../../hooks/useHideHeader";
import NavigationHeader from "../../../components/common/NavigateHeader";
import Modal from "../../../components/common/Modal";
import Header from "../../../components/layout/Header";
import CampaignBrandCard from "../components/CampaignBrandCard";
import LoadingSpinner from "../../../components/common/LoadingSpinner";
import CampaignInfoGroup from "../components/CampaignInfoGroup";
Expand All @@ -28,9 +29,12 @@ export default function ApplicationContent() {
const [modalStep, setModalStep] = useState<"CONFIRM" | "COMPLETE">("CONFIRM");

const applicationId = searchParams.get("applicationId");
const navigate = useNavigate();
const location = useLocation();
const brandIdFromList = location.state?.brandId;

useHideHeader(true);

const handleCloseModal = () => {
setIsModalOpen(false);
setTimeout(() => setModalStep("CONFIRM"), 300);
Expand Down Expand Up @@ -103,8 +107,6 @@ export default function ApplicationContent() {
}
};

const navigate = useNavigate();

const handleComplete = () => {
setIsModalOpen(false);
navigate(-1);
Expand All @@ -125,7 +127,9 @@ export default function ApplicationContent() {

return (
<div className="flex flex-col w-full min-h-screen bg-bg-w font-pretendard">
<Header title="지원 상세 보기" />
<div className="min-h-[60px]">
<NavigationHeader title="지원 보기" onBack={() => navigate(-1)} />
</div>

<main className="flex flex-col pb-24">
{/* 상단 브랜드 정보 섹션 */}
Expand Down
Loading