Skip to content

Commit 19f412a

Browse files
authored
Merge pull request #201 from RealMatchTeam/refactor/proposal-ui
Refactor/proposal UI
2 parents 59b8bdf + 82fdc14 commit 19f412a

5 files changed

Lines changed: 132 additions & 100 deletions

File tree

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import Modal from "./Modal";
2+
import Button from "./Button";
3+
import CheckCircleIcon from "../../assets/icon/icon-check-circle.svg";
4+
import closeIcon from "../../assets/icon/icon-close.svg";
5+
6+
7+
type ConfirmModalProps = {
8+
isOpen: boolean;
9+
onClose: () => void;
10+
onConfirm: () => void;
11+
title: string;
12+
};
13+
14+
export default function ConfirmModal({
15+
isOpen,
16+
onClose,
17+
onConfirm,
18+
title,
19+
}: ConfirmModalProps) {
20+
return (
21+
<Modal isOpen={isOpen} onClose={onClose} className="w-[310px] h-[310px] rounded-[10px] p-6 relative">
22+
{/* 상단 좌측 X 아이콘 추가 */}
23+
<button
24+
onClick={onClose}
25+
className="absolute top-5 left-5 p-1 active:opacity-50 transition-opacity"
26+
>
27+
<img src={closeIcon} alt="close" className="w-5 h-5" />
28+
</button>
29+
30+
<div className="flex flex-col items-center h-full">
31+
<div className="flex-1 flex flex-col items-center justify-center w-full mt-4">
32+
{/* 이미지 섹션 */}
33+
<img src={CheckCircleIcon} alt="" className="w-[64px] h-[64px] mb-8" />
34+
35+
{/* 문구 섹션 */}
36+
<h3 className="text-callout3 text-text-black text-center leading-tight">
37+
{title}
38+
</h3>
39+
</div>
40+
41+
{/* 버튼 섹션 */}
42+
<div className="w-full flex gap-3 mt-auto">
43+
{/* '예' 버튼 */}
44+
<Button
45+
variant="outline"
46+
className="w-[76px] h-[44px] text-[18px] font-semibold rounded-[16px] border-[#C5C7F9] text-[#6366F1] bg-white"
47+
onClick={onConfirm}
48+
>
49+
50+
</Button>
51+
52+
{/* '아니오' 버튼 */}
53+
<Button
54+
variant="primary"
55+
className="w-[186px] h-[44px] text-[18px] font-semibold rounded-[16px] bg-[#6366F1] text-white border-none"
56+
onClick={onClose}
57+
>
58+
아니오
59+
</Button>
60+
</div>
61+
</div>
62+
</Modal>
63+
);
64+
}

app/routes/business/calendar/calendar-content.tsx

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useState, useEffect, useMemo } from "react";
22
import { useNavigate, useLocation } from "react-router-dom";
3-
import { getMyCollaborations } from "./api/calendar";
4-
import type { CampaignCollaboration } from "./api/calendar";
3+
import { getMyCollaborations, searchCollaborations } from "./api/calendar";
4+
import type { CampaignCollaboration, } from "./api/calendar";
55
import FilterBottomSheet from "../components/FilterBottomSheet";
66
import WeeklyCalendar from "../components/WeeklyCalendar";
77
import MonthlyCalendar from "../components/MonthlyCalendar";
@@ -30,25 +30,31 @@ export default function CalendarContent() {
3030
const isFiltered = activeFilter !== "전체";
3131

3232
useEffect(() => {
33-
const fetchAllCampaigns = async () => {
33+
const fetchCampaigns = async () => {
3434
try {
3535
setIsLoading(true);
3636

37+
// 1. 키워드가 있을 때는 searchCollaborations API 사용
38+
// 2. 키워드가 없을 때는 getMyCollaborations API 사용
39+
40+
const fetchFunction = keyword.trim() ? searchCollaborations : getMyCollaborations;
41+
3742
const [applied, sent, received] = await Promise.all([
38-
getMyCollaborations({ type: "APPLIED", keyword: keyword.trim() || undefined }),
39-
getMyCollaborations({ type: "SENT", keyword: keyword.trim() || undefined }),
40-
getMyCollaborations({ type: "RECEIVED", keyword: keyword.trim() || undefined }),
43+
fetchFunction({ type: "APPLIED", keyword: keyword.trim() || undefined }),
44+
fetchFunction({ type: "SENT", keyword: keyword.trim() || undefined }),
45+
fetchFunction({ type: "RECEIVED", keyword: keyword.trim() || undefined }),
4146
]);
4247

4348
setCampaigns([...applied, ...sent, ...received]);
4449
} catch (error) {
45-
console.error("로드 실패:", error);
50+
console.error("데이터 로드 실패:", error);
51+
setCampaigns([]); // 에러 시 빈 배열 처리
4652
} finally {
4753
setIsLoading(false);
4854
}
4955
};
5056

51-
fetchAllCampaigns();
57+
fetchCampaigns();
5258
}, [keyword, location.key]);
5359

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

101+
// CalendarContent.tsx 내부 matchingList
102+
95103
const matchingList = useMemo(() => {
96104
return campaigns.filter((item) => {
105+
// 탭 필터링
97106
const isCorrectSubTab =
98107
matchingSubTab === "sent" ? item.type === "SENT" :
99108
matchingSubTab === "received" ? item.type === "RECEIVED" :
100109
item.type === "APPLIED";
101110

102111
if (!isCorrectSubTab) return false;
103112

104-
if (activeFilter === "전체") return true;
105-
106-
const statusMatches = getStatusLabel(item.status) === activeFilter;
107-
108-
const keywordMatches = keyword ? item.brandName.includes(keyword) : true;
109-
110-
return statusMatches && keywordMatches;
111-
112-
113+
// 상태 필터링 (activeFilter가 "전체"가 아닐 때만 적용)
114+
if (activeFilter !== "전체") {
115+
return getStatusLabel(item.status) === activeFilter;
116+
}
113117

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

117122
console.log("전체 데이터:", campaigns);
118123
console.log("필터된 데이터:", matchingList);
@@ -226,20 +231,18 @@ export default function CalendarContent() {
226231
<h2 className="text-title1 font-semibold text-text-black">매칭 현황</h2>
227232
<button
228233
onClick={() => setIsFilterOpen(true)}
229-
className={`flex items-center w-fit h-7 pl-3 pr-1.5 rounded-full border text-[14px] font-medium text-[#5B5D6B] ${
230-
isFiltered
234+
className={`flex items-center w-fit h-7 pl-3 pr-1.5 rounded-full border text-[14px] font-medium text-[#5B5D6B] ${isFiltered
231235
? "border-core-70 text-core-1 bg-core-70"
232236
: "border-core-2 text-gray-2 bg-white"
233-
}`}
237+
}`}
234238
>
235239
{activeFilter}
236240
<svg
237241
xmlns="http://www.w3.org/2000/svg"
238242
viewBox="0 0 20 20"
239243
fill="none"
240-
className={`w-6 h-6 ${
241-
isFiltered ? "text-core-1" : "text-text-gray2"
242-
}`}
244+
className={`w-6 h-6 ${isFiltered ? "text-core-1" : "text-text-gray2"
245+
}`}
243246
>
244247
<path
245248
d="M6 8L10 12L14 8"

app/routes/business/components/MatchingTabSection.tsx

Lines changed: 9 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
import { useState, useEffect, useCallback } from "react";
2-
import { searchCollaborations, type CampaignCollaboration } from "../calendar/api/calendar";
1+
import { useState } from "react";
32
import searchIcon from "../../../assets/search2.svg";
43
import closeIcon from "../../../assets/cancel.svg";
5-
import LoadingSpinner from "../../../components/common/LoadingSpinner";
64

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

3028
export default function MatchingTabSection({ subTab, setSubTab, receivedCount, keyword, setKeyword }: Props) {
3129
const [isSearching, setIsSearching] = useState(false);
32-
const [campaigns, setCampaigns] = useState<CampaignCollaboration[]>([]);
33-
const [isLoading, setIsLoading] = useState(false);
34-
35-
// 캠페인 검색 함수
36-
const fetchCampaigns = useCallback(async () => {
37-
if (!keyword.trim()) return;
38-
setIsLoading(true);
39-
try {
40-
const data = await searchCollaborations({
41-
keyword: keyword.trim(),
42-
type: subTab.toUpperCase() as "APPLIED" | "SENT" | "RECEIVED",
43-
});
44-
setCampaigns(data || []);
45-
} catch (error) {
46-
console.error("검색 실패:", error);
47-
} finally {
48-
setIsLoading(false);
49-
}
50-
}, [keyword, subTab]);
51-
52-
// 검색 상태에 따라 캠페인 검색
53-
useEffect(() => {
54-
if (isSearching && keyword.trim()) {
55-
fetchCampaigns();
56-
} else {
57-
setCampaigns([]);
58-
}
59-
}, [isSearching, keyword, subTab, fetchCampaigns]);
6030

6131
if (isSearching) {
6232
return (
63-
<div className="flex flex-col w-full animate-slide-up">
64-
{/* 검색바 섹션 */}
33+
<div className="flex flex-col w-full animate-slide-up border-b border-text-gray5">
6534
<div className="flex items-center w-full px-4 py-3">
6635
<div className="flex items-center w-full relative bg-bg-w border border-core-2 rounded-[8px] px-3 py-2">
6736
<img src={searchIcon} alt="search" className="w-4 h-4 opacity-40 flex-shrink-0" />
@@ -70,7 +39,7 @@ export default function MatchingTabSection({ subTab, setSubTab, receivedCount, k
7039
className="flex-1 bg-transparent mx-2 outline-none text-body1 text-center placeholder:text-text-gray3"
7140
placeholder="브랜드명 입력"
7241
value={keyword}
73-
onChange={(e) => setKeyword(e.target.value)}
42+
onChange={(e) => setKeyword(e.target.value)} // 부모의 setKeyword 호출
7443
/>
7544
<button
7645
onClick={() => { setIsSearching(false); setKeyword(""); }}
@@ -80,22 +49,6 @@ export default function MatchingTabSection({ subTab, setSubTab, receivedCount, k
8049
</button>
8150
</div>
8251
</div>
83-
84-
{/* 로딩 표시 */}
85-
{isLoading && <LoadingSpinner className="py-4" size={80} />}
86-
87-
{/* 검색 결과 리스트 */}
88-
<div className="px-4 overflow-y-auto">
89-
{!isLoading && campaigns.length > 0 ? (
90-
campaigns.map((item) => (
91-
<div key={item.campaignId} className="py-3 border-b border-text-gray5 text-[14px]">
92-
{item.brandName} - {item.title} {/* 브랜드명과 제목 표시 */}
93-
</div>
94-
))
95-
) : (
96-
!isLoading && keyword && <div className="text-center py-10 text-text-gray4">검색 결과가 없습니다.</div>
97-
)}
98-
</div>
9952
</div>
10053
);
10154
}
@@ -110,7 +63,7 @@ export default function MatchingTabSection({ subTab, setSubTab, receivedCount, k
11063
/>
11164
<button
11265
onClick={() => setSubTab("received")}
113-
className={`px-4 py-2 rounded-lg text-[14px] font-bold flex items-center gap-1 transition-all ${
66+
className={`px-4 py-2 rounded-lg text-[14px] font-semibold flex items-center gap-1 transition-all ${
11467
subTab === "received"
11568
? "bg-core-1 text-white"
11669
: "bg-white border border-text-gray5 text-text-gray3"
@@ -130,8 +83,11 @@ export default function MatchingTabSection({ subTab, setSubTab, receivedCount, k
13083
onClick={() => setSubTab("applied")}
13184
/>
13285
</div>
133-
<button onClick={() => setIsSearching(true)} className="p-1">
134-
<img src={searchIcon} alt="search" className="w-6 h-6" />
86+
<button
87+
onClick={() => setIsSearching(true)}
88+
className="w-[40px] h-[36px] flex items-center justify-center bg-white border border-text-gray5 rounded-[8px] active:bg-bluegray-2 transition-colors"
89+
>
90+
<img src={searchIcon} alt="search" className="w-5 h-5 opacity-40" />
13591
</button>
13692
</div>
13793
);

app/routes/business/proposal/application-content.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import { useLocation } from "react-router-dom";
55
import { getAppliedCampaignDetail, cancelCampaignApply, type AppliedCampaignDetail } from "./api/proposal";
66
import { getBrandSummary, type BrandSummary } from "./api/brand";
77
import { getProfileCard, type ProfileCard } from "./api/user";
8+
import { useHideHeader } from "../../../hooks/useHideHeader";
9+
import NavigationHeader from "../../../components/common/NavigateHeader";
810
import Modal from "../../../components/common/Modal";
9-
import Header from "../../../components/layout/Header";
1011
import CampaignBrandCard from "../components/CampaignBrandCard";
1112
import LoadingSpinner from "../../../components/common/LoadingSpinner";
1213
import CampaignInfoGroup from "../components/CampaignInfoGroup";
@@ -28,9 +29,12 @@ export default function ApplicationContent() {
2829
const [modalStep, setModalStep] = useState<"CONFIRM" | "COMPLETE">("CONFIRM");
2930

3031
const applicationId = searchParams.get("applicationId");
32+
const navigate = useNavigate();
3133
const location = useLocation();
3234
const brandIdFromList = location.state?.brandId;
3335

36+
useHideHeader(true);
37+
3438
const handleCloseModal = () => {
3539
setIsModalOpen(false);
3640
setTimeout(() => setModalStep("CONFIRM"), 300);
@@ -103,8 +107,6 @@ export default function ApplicationContent() {
103107
}
104108
};
105109

106-
const navigate = useNavigate();
107-
108110
const handleComplete = () => {
109111
setIsModalOpen(false);
110112
navigate(-1);
@@ -125,7 +127,9 @@ export default function ApplicationContent() {
125127

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

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

0 commit comments

Comments
 (0)