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
10 changes: 8 additions & 2 deletions app/components/common/NavigateHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import type { ReactNode } from "react";

type Props = {
title: string; // 텍스트
title: ReactNode; // 텍스트
onBack?: () => void; //onBack={() => history.back()} 뒤로가기
bgClassName?: string;
titleClassName?: string;
};

export default function NavigationHeader({
title,
onBack,
bgClassName,
titleClassName,
}: Props) {
const bg = bgClassName ?? "bg-[#FFFFFF]";

Expand Down Expand Up @@ -39,7 +43,9 @@ export default function NavigationHeader({
</button>

<div className="flex-1 text-center">
<div className="text-Title1 text-black">{title}</div>
<div className={`text-Title1 text-semiblod ${titleClassName ?? ""}`}>
{title}
</div>
</div>

{/* 오른쪽 여백 맞추기 */}
Expand Down
183 changes: 112 additions & 71 deletions app/routes/mypage/likes/likes-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,16 @@ const CAMPAIGN_SORT_PARAM_MAP: Record<string, string> = {
export default function MyPageLikes() {
useHideHeader(true);
const navigate = useNavigate();
const DEFAULT_SORT_LABEL = "매칭률 순";
const [activeTab, setActiveTab] = useState<"brand" | "campaign">("brand");
const [isFilterOpen, setIsFilterOpen] = useState(false);
const [brandSortOption, setBrandSortOption] = useState("정렬 필터");
const [campaignSortOption, setCampaignSortOption] = useState("정렬 필터");
const [pendingSort, setPendingSort] = useState("정렬 필터");
const [brandSortOption, setBrandSortOption] = useState(DEFAULT_SORT_LABEL);
const [campaignSortOption, setCampaignSortOption] = useState(
DEFAULT_SORT_LABEL,
);
const [brandSortApplied, setBrandSortApplied] = useState(false);
const [campaignSortApplied, setCampaignSortApplied] = useState(false);
const [pendingSort, setPendingSort] = useState(DEFAULT_SORT_LABEL);
const [loading, setLoading] = useState(false);
const [brandLikesApi, setBrandLikesApi] = useState<BrandLike[]>([]);
const [campaignLikesApi, setCampaignLikesApi] = useState<CampaignLike[]>([]);
Expand All @@ -107,15 +112,25 @@ export default function MyPageLikes() {

const currentSortOption =
activeTab === "brand" ? brandSortOption : campaignSortOption;
const currentSortApplied =
activeTab === "brand" ? brandSortApplied : campaignSortApplied;
const getSortButtonLabel = () => currentSortOption;
const isFiltered = currentSortOption !== "정렬 필터";
const isFiltered = currentSortApplied;

const brandSortOptions = useMemo(
() => ["매칭률 순", "인기 순", "신규 순"].map((label) => ({ label, value: label })),
() =>
["매칭률 순", "인기 순", "신규 순"].map((label) => ({
label,
value: label,
})),
[],
);
const campaignSortOptions = useMemo(
() => ["매칭률 순", "인기 순", "금액 순", "마감 순"].map((label) => ({ label, value: label })),
() =>
["매칭률 순", "인기 순", "금액 순", "마감 순"].map((label) => ({
label,
value: label,
})),
[],
);

Expand All @@ -124,11 +139,19 @@ export default function MyPageLikes() {
setIsFilterOpen(true);
};

useEffect(() => {
if (!isFilterOpen) {
setPendingSort(currentSortOption);
}
}, [currentSortOption, isFilterOpen]);

const applySort = () => {
if (activeTab === "brand") {
setBrandSortOption(pendingSort);
setBrandSortApplied(true);
} else {
setCampaignSortOption(pendingSort);
setCampaignSortApplied(true);
}
setIsFilterOpen(false);
};
Expand Down Expand Up @@ -202,7 +225,9 @@ export default function MyPageLikes() {
"/api/v1/users/me/favorites/campaign",
{
params: {
sort: CAMPAIGN_SORT_PARAM_MAP[campaignSortOption] ?? "MATCH_SCORE",
sort:
CAMPAIGN_SORT_PARAM_MAP[campaignSortOption] ??
"MATCH_SCORE",
},
},
);
Expand Down Expand Up @@ -269,14 +294,18 @@ export default function MyPageLikes() {

const toggleCampaignLike = async (campaignId: number) => {
setCampaignLikesApi((prev) =>
prev.map((c) => (c.id === campaignId ? { ...c, isLiked: !c.isLiked } : c)),
prev.map((c) =>
c.id === campaignId ? { ...c, isLiked: !c.isLiked } : c,
),
);
try {
await axiosInstance.post(`/api/v1/campaigns/${campaignId}/like`);
} catch (error) {
console.error("캠페인 좋아요 토글 실패:", error);
setCampaignLikesApi((prev) =>
prev.map((c) => (c.id === campaignId ? { ...c, isLiked: !c.isLiked } : c)),
prev.map((c) =>
c.id === campaignId ? { ...c, isLiked: !c.isLiked } : c,
),
);
}
};
Expand All @@ -285,7 +314,11 @@ export default function MyPageLikes() {
<div className="h-screen-full bg-gradient-to-b from-[#F6F6FF] via-[#F3F3FA] to-[#E8E8FB]">
<div className="w-full shadow-2xl flex flex-col">
<div className="h-[60px]">
<NavigationHeader title="내 찜" onBack={() => navigate(-1)} />
<NavigationHeader
title="내 찜"
titleClassName="font-semibold"
onBack={() => navigate(-1)}
/>
</div>

<div className="bg-white border-b border-[#E8E8FB]">
Expand Down Expand Up @@ -331,8 +364,8 @@ export default function MyPageLikes() {
onClick={openSortSheet}
className={`flex items-center w-fit h-7 pl-3 pr-1.5 rounded-full border text-[14px] font-Pretendard ${
isFiltered
? "border-core-70 text-core-1 bg-core-70"
: "border-core-2 text-gray-2 bg-white text-title3"
? "border-core-3 text-core-1 bg-core-2"
: "border-core-2 text-text-gray2 bg-white text-title3"
}`}
>
{getSortButtonLabel()}
Expand All @@ -356,63 +389,63 @@ export default function MyPageLikes() {
{loading ? (
<LoadingSpinner className="py-10" />
) : (
<div className="mt-4 space-y-4">
{activeTab === "brand"
? brandLikes.length > 0
? brandLikes.map((brand) => (
<div
key={brand.id}
className="bg-white rounded-[14px] px-[10px] py-[10px] flex gap-4 items-center h-[100px]"
>
<div className="w-[80px] h-[80px] rounded-[5px] border border-[#E6E6F3] grid place-items-center text-[#1D1D1F] text-[16px] font-semibold whitespace-pre text-center">
{brand.logoUrl ? (
<img
src={brand.logoUrl}
alt={brand.name}
className="w-full h-full object-contain rounded-[10px]"
/>
) : (
brand.name
)}
</div>
<div className="mt-4 space-y-4">
{activeTab === "brand" ? (
brandLikes.length > 0 ? (
brandLikes.map((brand) => (
<div
key={brand.id}
className="bg-white rounded-[14px] px-[10px] py-[10px] flex gap-4 items-center h-[100px]"
>
<div className="w-[80px] h-[80px] rounded-[5px] border border-[#E6E6F3] grid place-items-center text-[#1D1D1F] text-[16px] font-semibold whitespace-pre text-center">
{brand.logoUrl ? (
<img
src={brand.logoUrl}
alt={brand.name}
className="w-full h-full object-contain rounded-[10px]"
/>
) : (
brand.name
)}
</div>

<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2">
<div className="text-[18px] font-semibold text-[#171718] truncate">
{brand.name}
</div>
<div className="flex items-center gap-2 shrink-0">
<div className="text-[#6D6AFE]">
<span className="text-[12px] leading-[16px] font-medium">
매칭률 {/*수정*/}
</span>{" "}
<span className="text-[16px] leading-[20px] font-semibold">
{brand.matchRate}%
</span>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2">
<div className="text-[18px] font-semibold text-[#171718] truncate">
{brand.name}
</div>
<div className="flex items-center gap-2 shrink-0">
<div className="text-[#6D6AFE]">
<span className="text-[12px] leading-[16px] font-medium">
매칭률 {/*수정*/}
</span>{" "}
<span className="text-[16px] leading-[20px] font-semibold">
{brand.matchRate}%
</span>
</div>
<button
type="button"
className="text-[#B7B7F3] text-[24px]"
aria-label="찜 해제"
onClick={() => toggleBrandLike(brand.id)}
>
{brand.isLiked ? "♥" : "♡"}
</button>
</div>
<button
type="button"
className="text-[#B7B7F3] text-[24px]"
aria-label="찜 해제"
onClick={() => toggleBrandLike(brand.id)}
>
{brand.isLiked ? "♥" : "♡"}
</button>
</div>
</div>
<div className="mt-2 text-[13px] text-[#8B8D99] truncate">
{brand.tags.join(" ")}
<div className="mt-2 text-[13px] text-[#8B8D99] truncate">
{brand.tags.join(" ")}
</div>
</div>
</div>
</div>
))
: (
))
) : (
<div className="py-12 text-center text-[#9B9BA1] text-[14px]">
찜한 브랜드가 없습니다.
</div>
)
: campaignLikes.length > 0
? campaignLikes.map((campaign) => (
) : campaignLikes.length > 0 ? (
campaignLikes.map((campaign) => (
<div
key={campaign.id}
className="bg-white rounded-[10px] border border-[#E8E8FB] px-[10px] pt-[10px] pb-[6px] flex gap-4 items-start h-[120px]"
Expand All @@ -429,7 +462,9 @@ export default function MyPageLikes() {
campaign.brand
)}
</div>
<div className="flex items-start gap-1"> {/*수정*/}
<div className="flex items-start gap-1">
{" "}
{/*수정*/}
<span className="px-[2px] py-[1px] rounded-[5px] border border-[#6666E5] text-[#6666E5] text-[10px] leading-[14px]">
{campaign.dday}
</span>
Expand All @@ -447,7 +482,7 @@ export default function MyPageLikes() {
<div className="flex items-center gap-2 shrink-0">
<div className="text-[#6D6AFE]">
<span className="text-[12px] leading-[16px] font-medium">
매칭률 {/*수정*/}
매칭률 {/*수정*/}
</span>{" "}
<span className="text-[16px] leading-[20px] font-semibold">
{campaign.matchRate}%
Expand All @@ -463,21 +498,25 @@ export default function MyPageLikes() {
</button>
</div>
</div>
<div className="mt-[3px] text-[14px] leading-[20px] font-medium text-[#171718] truncate"> {/*수정*/}
<div className="mt-[3px] text-[14px] leading-[20px] font-medium text-[#171718] truncate">
{" "}
{/*수정*/}
{campaign.title}
</div>
<div className="mt-[1px] text-[11px] leading-[16px] text-[#6666E5] font-medium"> {/*수정*/}
<div className="mt-[1px] text-[11px] leading-[16px] text-[#6666E5] font-medium">
{" "}
{/*수정*/}
원고료: {campaign.reward.toLocaleString()}원
</div>
</div>
</div>
))
: (
<div className="py-12 text-center text-[#9B9BA1] text-[14px]">
찜한 캠페인이 없습니다.
</div>
)}
</div>
) : (
<div className="py-12 text-center text-[#9B9BA1] text-[14px]">
찜한 캠페인이 없습니다.
</div>
)}
</div>
)}
</div>
</div>
Expand All @@ -488,7 +527,9 @@ export default function MyPageLikes() {
onChange={setPendingSort}
onClose={() => setIsFilterOpen(false)}
onApply={applySort}
options={activeTab === "brand" ? brandSortOptions : campaignSortOptions}
options={
activeTab === "brand" ? brandSortOptions : campaignSortOptions
}
title="정렬 필터"
applyLabel="적용하기"
/>
Expand Down
Loading