From 81f0338f8009d89d0a0f2d6918171710b51bc546 Mon Sep 17 00:00:00 2001
From: seyun31 <2ne1jenna@naver.com>
Date: Wed, 18 Feb 2026 19:13:13 +0900
Subject: [PATCH 1/2] =?UTF-8?q?refactor:=20=EC=A0=9C=EC=95=88=ED=95=98?=
=?UTF-8?q?=EA=B8=B0/=EC=9E=AC=EC=A0=9C=EC=95=88=ED=95=98=EA=B8=B0=20?=
=?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 제안하기 항목 중복 선택 기능 수정
- 협찬 가능 제품 -> 개별 제품 -> 제안하기 기능 추가
- 재제안하기 페이지 UI 수정
---
app/components/form/SelectField.tsx | 9 +-
.../detail/sponsorable-detail-content.tsx | 22 +-
.../chat/resuggest/resuggest-content.tsx | 193 +++++++++---------
app/routes/home/components/BannerCarousel.tsx | 96 +++++++--
.../matching/components/ProposalModal.tsx | 6 +-
.../create/components/SelectBottomSheet.tsx | 6 +-
.../create/create-campaign-content.tsx | 178 ++++++++--------
app/routes/matching/suggest/create/schema.ts | 24 +--
8 files changed, 321 insertions(+), 213 deletions(-)
diff --git a/app/components/form/SelectField.tsx b/app/components/form/SelectField.tsx
index e72e1f4c..db1f4f99 100644
--- a/app/components/form/SelectField.tsx
+++ b/app/components/form/SelectField.tsx
@@ -2,13 +2,18 @@ interface SelectFieldProps {
placeholder: string;
value?: string;
onClick: () => void;
+ noTruncate?: boolean;
}
export default function SelectField({
placeholder,
value,
onClick,
+ noTruncate = false,
}: SelectFieldProps) {
+ // 12글자 이상이면 ...으로 표시 (noTruncate가 false일 때만)
+ const displayValue = !noTruncate && value && value.length > 12 ? `${value.slice(0, 12)}...` : value;
+
return (
);
diff --git a/app/routes/brand-detail/sponsorable/detail/sponsorable-detail-content.tsx b/app/routes/brand-detail/sponsorable/detail/sponsorable-detail-content.tsx
index 056ab11b..0411b13b 100644
--- a/app/routes/brand-detail/sponsorable/detail/sponsorable-detail-content.tsx
+++ b/app/routes/brand-detail/sponsorable/detail/sponsorable-detail-content.tsx
@@ -12,6 +12,7 @@ import { LayoutContext } from "../../../layout-context";
import Button from "../../../../components/common/Button";
import { fetchSponsorProductDetail } from "../../api/api";
import LoadingSpinner from "../../../../components/common/LoadingSpinner";
+import { useCampaignProposalStore } from "../../../../stores/campaign-proposal";
const INTERVAL = 3000;
@@ -152,6 +153,7 @@ export default function SponsorableDetailContent() {
const navigate = useNavigate();
const location = useLocation();
const [sp] = useSearchParams();
+ const setProposalData = useCampaignProposalStore((state) => state.setProposalData);
const layout = useContext(LayoutContext);
const state = (location.state ?? {}) as NavState;
@@ -404,16 +406,16 @@ export default function SponsorableDetailContent() {
variant="primary"
size="lg"
fullWidth
- onClick={() =>
- navigate("/matching/suggest/create", {
- state: {
- brandId: data?.brandId,
- productId: data?.productId,
- brandName: data?.brandName,
- productName: data?.productName,
- },
- })
- }
+ onClick={() => {
+ setProposalData({
+ brandId: data?.brandId ?? 0,
+ brandName: data?.brandName,
+ product: data?.productName,
+ productId: data?.productId,
+ domain: "BEAUTY",
+ });
+ navigate("/matching/suggest/create?type=new");
+ }}
>
{buttonText}
diff --git a/app/routes/chat/resuggest/resuggest-content.tsx b/app/routes/chat/resuggest/resuggest-content.tsx
index 4ac903ce..b43431a0 100644
--- a/app/routes/chat/resuggest/resuggest-content.tsx
+++ b/app/routes/chat/resuggest/resuggest-content.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect } from "react";
+import { useState, useEffect, useMemo } from "react";
import { useNavigate } from "react-router";
import { useForm, useWatch } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -84,36 +84,26 @@ export default function ReSuggestContent() {
setValue("description", proposalData.campaignDescription || "");
if (proposalData.contentTags?.formats && proposalData.contentTags.formats.length > 0) {
- const t = proposalData.contentTags.formats[0];
- const id = t.id ?? PROPOSAL_TAG_ID_BY_NAME[t.name];
- if (id) setValue("format", String(id));
+ setValue("format", proposalData.contentTags.formats.map(f => String(f.id ?? PROPOSAL_TAG_ID_BY_NAME[f.name])));
}
if (proposalData.contentTags?.categories && proposalData.contentTags.categories.length > 0) {
- const t = proposalData.contentTags.categories[0];
- const id = t.id ?? PROPOSAL_TAG_ID_BY_NAME[t.name];
- if (id) setValue("category", String(id));
+ setValue("category", proposalData.contentTags.categories.map(c => String(c.id ?? PROPOSAL_TAG_ID_BY_NAME[c.name])));
}
if (proposalData.contentTags?.tones && proposalData.contentTags.tones.length > 0) {
- const t = proposalData.contentTags.tones[0];
- const id = t.id ?? PROPOSAL_TAG_ID_BY_NAME[t.name];
- if (id) setValue("tone", String(id));
+ setValue("tone", proposalData.contentTags.tones.map(t => String(t.id ?? PROPOSAL_TAG_ID_BY_NAME[t.name])));
}
if (proposalData.contentTags?.involvements && proposalData.contentTags.involvements.length > 0) {
- const t = proposalData.contentTags.involvements[0];
- const id = t.id ?? PROPOSAL_TAG_ID_BY_NAME[t.name];
- if (id) setValue("involvement", String(id));
+ setValue("involvement", proposalData.contentTags.involvements.map(i => String(i.id ?? PROPOSAL_TAG_ID_BY_NAME[i.name])));
}
if (proposalData.contentTags?.usageRanges && proposalData.contentTags.usageRanges.length > 0) {
- const t = proposalData.contentTags.usageRanges[0];
- const id = t.id ?? PROPOSAL_TAG_ID_BY_NAME[t.name];
- if (id) setValue("usageScope", String(id));
+ setValue("usageScope", proposalData.contentTags.usageRanges.map(u => String(u.id ?? PROPOSAL_TAG_ID_BY_NAME[u.name])));
}
setValue("fee", proposalData.rewardAmount?.toString() || "");
const productIdValue = proposalData.productId && proposalData.productId > 0
? proposalData.productId.toString()
- : (proposalData.product || "");
- setValue("sponsorProduct", productIdValue || "");
+ : (proposalData.product && proposalData.product !== "0" ? proposalData.product : "");
+ setValue("sponsorProduct", productIdValue ? [productIdValue] : []);
setValue("startDate", proposalData.startDate || "");
setValue("endDate", proposalData.endDate || "");
}
@@ -136,16 +126,31 @@ export default function ReSuggestContent() {
const involvementOptions = INVOLVEMENT_TAGS.map((t) => ({ value: String(t.id), label: t.name }));
const usageScopeOptions = USAGE_RANGE_TAGS.map((t) => ({ value: String(t.id), label: t.name }));
- const sponsorProductOptions = proposalData?.product
- ? [{ value: proposalData.product, label: proposalData.product }]
- : (proposalData?.products ?? [])
- .filter((p) => p.id && p.name)
- .map((p) => ({ value: String(p.id), label: p.name }));
-
- const findLabel = (options: { value: string; label: string }[], value?: string) => {
- if (!value) return undefined;
- const option = options.find((opt) => opt.value === value);
- return option?.label || "";
+ const sponsorProductOptions = useMemo(() => {
+ const baseOptions = proposalData?.product && proposalData.product !== "0"
+ ? [{ value: proposalData.product, label: proposalData.product }]
+ : (proposalData?.products ?? [])
+ .filter((p) => p.id && p.name && String(p.id) !== "0")
+ .map((p) => ({ value: String(p.id), label: p.name }));
+
+ // formValues.sponsorProduct 배열에 있는데 options에 없는 항목들 추가 (0 제외)
+ const missingOptions = (formValues.sponsorProduct || [])
+ .filter(sp => sp !== "0" && !baseOptions.find(opt => opt.value === sp))
+ .map(sp => ({ value: sp, label: sp }));
+
+ return [...missingOptions, ...baseOptions];
+ }, [proposalData, formValues.sponsorProduct]);
+
+ // ID 배열로 label들 찾기 헬퍼 함수
+ const findLabels = (options: { value: string; label: string }[], values?: string[]) => {
+ if (!values || values.length === 0) return undefined;
+ const labels = values
+ .map(value => {
+ const found = options.find((opt) => opt.value === value);
+ return found?.label || value;
+ })
+ .filter(Boolean);
+ return labels.length > 0 ? labels.join(", ") : undefined;
};
const onSubmit = () => {
@@ -167,13 +172,13 @@ export default function ReSuggestContent() {
campaignId: proposalData?.campaignId || null,
campaignName: formData.campaignName || "",
description: formData.description || "",
- formats: formData.format ? [{ id: Number(formData.format) }] : [],
- categories: formData.category ? [{ id: Number(formData.category) }] : [],
- tones: formData.tone ? [{ id: Number(formData.tone) }] : [],
- involvements: formData.involvement ? [{ id: Number(formData.involvement) }] : [],
- usageRanges: formData.usageScope ? [{ id: Number(formData.usageScope) }] : [],
+ formats: formData.format?.map(f => ({ id: Number(f) })) || [],
+ categories: formData.category?.map(c => ({ id: Number(c) })) || [],
+ tones: formData.tone?.map(t => ({ id: Number(t) })) || [],
+ involvements: formData.involvement?.map(i => ({ id: Number(i) })) || [],
+ usageRanges: formData.usageScope?.map(u => ({ id: Number(u) })) || [],
rewardAmount: Number(formData.fee) || 0,
- productId: Number(formData.sponsorProduct) || 0,
+ productId: Number(formData.sponsorProduct?.[0]) || 0,
startDate: formData.startDate || "",
endDate: formData.endDate || "",
};
@@ -188,46 +193,47 @@ export default function ReSuggestContent() {
};
return (
-
navigate(-1)} />
+ navigate(-1)} />
{/* 커스텀 브랜드 카드 디자인 */}
-
-
- {/* 로고 영역 */}
-
-

-
+
+ {/* 로고 영역 */}
+
+

+
- {/* 정보 영역 */}
-
-
+ {/* 정보 영역 */}
+
+ {/* 첫 번째 줄: 브랜드명 및 매칭률 */}
+
+
{brandDetail?.name || proposalData?.brandName || ""}
-
-
-
- {(brandDetail?.hashtags || []).map((tag) => (
- #{tag}
- ))}
-
-
검토 중
+
+ 매칭률
+ {brandDetail?.matchRate ?? 99}%
-
- {/* 매칭률 영역 */}
-
-
매칭률
-
{brandDetail?.matchRate ?? 99}%
+ {/* 해시태그 및 상태값 */}
+
+
+ {(brandDetail?.hashtags || []).map((tag) => (
+ #{tag}
+ ))}
+
+
검토 중
+
@@ -249,7 +255,7 @@ export default function ReSuggestContent() {
{/* 캠페인명 */}
-
@@ -314,7 +324,7 @@ export default function ReSuggestContent() {
관여도
setIsInvolvementSheetOpen(true)}
/>
@@ -322,7 +332,7 @@ export default function ReSuggestContent() {
활용 범위
setIsUsageScopeSheetOpen(true)}
/>
@@ -333,17 +343,17 @@ export default function ReSuggestContent() {
- 협찬품*
+ 협찬품
setIsSponsorProductSheetOpen(true)}
/>
- 원고료*
+ 원고료
- 제작 기간*
+ 제작 기간
setIsFormatSheetOpen(false)}
title="형식"
options={formatOptions}
- selectedValues={formValues.format ? [formValues.format] : []}
- onSubmit={(values) => setValue("format", values[0] || "", { shouldValidate: true })}
- multiSelect={false}
+ selectedValues={formValues.format || []}
+ onSubmit={(values) => setValue("format", values, { shouldValidate: true })}
+ multiSelect={true}
/>
setIsCategorySheetOpen(false)}
title="종류"
options={categoryOptions}
- selectedValues={formValues.category ? [formValues.category] : []}
- onSubmit={(values) => setValue("category", values[0] || "", { shouldValidate: true })}
- multiSelect={false}
+ selectedValues={formValues.category || []}
+ onSubmit={(values) => setValue("category", values, { shouldValidate: true })}
+ multiSelect={true}
/>
setIsToneSheetOpen(false)}
title="톤"
options={toneOptions}
- selectedValues={formValues.tone ? [formValues.tone] : []}
- onSubmit={(values) => setValue("tone", values[0] || "", { shouldValidate: true })}
- multiSelect={false}
+ selectedValues={formValues.tone || []}
+ onSubmit={(values) => setValue("tone", values, { shouldValidate: true })}
+ multiSelect={true}
/>
setIsInvolvementSheetOpen(false)}
title="관여도"
options={involvementOptions}
- selectedValues={formValues.involvement ? [formValues.involvement] : []}
- onSubmit={(values) => setValue("involvement", values[0] || "", { shouldValidate: true })}
- multiSelect={false}
+ selectedValues={formValues.involvement || []}
+ onSubmit={(values) => setValue("involvement", values, { shouldValidate: true })}
+ multiSelect={true}
/>
setIsUsageScopeSheetOpen(false)}
title="활용 범위"
options={usageScopeOptions}
- selectedValues={formValues.usageScope ? [formValues.usageScope] : []}
- onSubmit={(values) => setValue("usageScope", values[0] || "", { shouldValidate: true })}
- multiSelect={false}
+ selectedValues={formValues.usageScope || []}
+ onSubmit={(values) => setValue("usageScope", values, { shouldValidate: true })}
+ multiSelect={true}
/>
setIsSponsorProductSheetOpen(false)}
title="협찬품 선택"
options={sponsorProductOptions}
- selectedValues={formValues.sponsorProduct ? [formValues.sponsorProduct] : []}
- onSubmit={(values) => setValue("sponsorProduct", values[0] || "", { shouldValidate: true })}
- multiSelect={false}
+ selectedValues={formValues.sponsorProduct || []}
+ onSubmit={(values) => setValue("sponsorProduct", values, { shouldValidate: true })}
+ multiSelect={true}
hasCustomInput={true}
/>
setIsConfirmDialogOpen(false)}
onConfirm={handleConfirmSubmit}
/>
diff --git a/app/routes/home/components/BannerCarousel.tsx b/app/routes/home/components/BannerCarousel.tsx
index 68dd739d..26932ff4 100644
--- a/app/routes/home/components/BannerCarousel.tsx
+++ b/app/routes/home/components/BannerCarousel.tsx
@@ -32,13 +32,19 @@ export default function BannerCarousel({
category: CategoryKey;
}) {
const banners = category === "beauty" ? beautyBanners : fashionBanners;
+ const displayBanners = [banners[banners.length - 1], ...banners, banners[0]];
- const [current, setCurrent] = useState(0);
+ const [current, setCurrent] = useState(1);
+ const [isDragging, setIsDragging] = useState(false);
+ const [isSilentJumping, setIsSilentJumping] = useState(false);
+ const [startX, setStartX] = useState(0);
+ const [dragOffset, setDragOffset] = useState(0);
const timerRef = useRef | null>(null);
const start = useCallback(() => {
+ stop();
timerRef.current = setInterval(() => {
- setCurrent((prev) => (prev + 1) % banners.length);
+ setCurrent((prev) => prev + 1);
}, INTERVAL);
}, [banners.length]);
@@ -54,19 +60,83 @@ export default function BannerCarousel({
return stop;
}, [start, stop]);
+ const handleTransitionEnd = () => {
+ if (current === 0) {
+ setIsSilentJumping(true);
+ setCurrent(banners.length);
+ } else if (current === banners.length + 1) {
+ setIsSilentJumping(true);
+ setCurrent(1);
+ }
+ };
+
+ useEffect(() => {
+ if (isSilentJumping) {
+ const timeout = setTimeout(() => {
+ setIsSilentJumping(false);
+ }, 50);
+ return () => clearTimeout(timeout);
+ }
+ }, [isSilentJumping]);
+
+ const handleStart = (clientX: number) => {
+ stop();
+ setIsDragging(true);
+ setStartX(clientX);
+ setDragOffset(0);
+ };
+
+ const handleMove = (clientX: number) => {
+ if (!isDragging) return;
+ const offset = clientX - startX;
+ setDragOffset(offset);
+ };
+
+ const handleEnd = () => {
+ if (!isDragging) return;
+
+ const threshold = 50;
+ if (dragOffset > threshold) {
+ setCurrent((prev) => prev - 1);
+ } else if (dragOffset < -threshold) {
+ setCurrent((prev) => prev + 1);
+ }
+
+ setIsDragging(false);
+ setDragOffset(0);
+ start();
+ };
+
+ const activeDotIndex = (current - 1 + banners.length) % banners.length;
+
return (
-
+
handleStart(e.clientX)}
+ onMouseMove={(e) => handleMove(e.clientX)}
+ onMouseUp={handleEnd}
+ onMouseLeave={handleEnd}
+ onTouchStart={(e) => handleStart(e.touches[0].clientX)}
+ onTouchMove={(e) => handleMove(e.touches[0].clientX)}
+ onTouchEnd={handleEnd}
+ >
- {banners.map((banner, i) => (
-
+ {displayBanners.map((banner, i) => (
+
))}
@@ -77,14 +147,14 @@ export default function BannerCarousel({
diff --git a/app/routes/matching/components/ProposalModal.tsx b/app/routes/matching/components/ProposalModal.tsx
index ff997880..47df9e21 100644
--- a/app/routes/matching/components/ProposalModal.tsx
+++ b/app/routes/matching/components/ProposalModal.tsx
@@ -44,8 +44,8 @@ export default function ProposalModal({
{isConfirm
- ? (isSuggest ? "캠페인을 제안하시겠습니까?" : "지원하시겠습니까?")
- : (isSuggest ? "제안하기 완료" : "지원 완료")}
+ ? (isSuggest ? "제안하시겠습니까?" : "지원하시겠습니까?")
+ : (isSuggest ? "제안 완료" : "지원 완료")}
{!isConfirm && (
@@ -72,7 +72,7 @@ export default function ProposalModal({
className="flex-[4] h-11 text-title7 rounded-xl"
onClick={onConfirm}
>
- 지원하기
+ {isSuggest ? "제안하기" : "지원하기"}
>
) : (
diff --git a/app/routes/matching/suggest/create/components/SelectBottomSheet.tsx b/app/routes/matching/suggest/create/components/SelectBottomSheet.tsx
index 04193bc8..74f89c78 100644
--- a/app/routes/matching/suggest/create/components/SelectBottomSheet.tsx
+++ b/app/routes/matching/suggest/create/components/SelectBottomSheet.tsx
@@ -1,4 +1,4 @@
-import { useState } from "react";
+import { useState, useEffect } from "react";
import FilterBottomSheet from "../../../../../components/common/FilterBottomSheet";
import Button from "../../../../../components/common/Button";
import { CheckIcon } from "../../../../auth/components/CheckIcon";
@@ -32,6 +32,10 @@ export default function SelectBottomSheet({
const [selected, setSelected] = useState
(selectedValues);
const [customInput, setCustomInput] = useState("");
+ useEffect(() => {
+ setSelected(selectedValues);
+ }, [selectedValues, isOpen]);
+
const handleToggle = (value: string) => {
if (multiSelect) {
setSelected((prev) =>
diff --git a/app/routes/matching/suggest/create/create-campaign-content.tsx b/app/routes/matching/suggest/create/create-campaign-content.tsx
index 17c4fba7..a64aefd4 100644
--- a/app/routes/matching/suggest/create/create-campaign-content.tsx
+++ b/app/routes/matching/suggest/create/create-campaign-content.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect } from "react";
+import { useState, useEffect, useMemo } from "react";
import { useNavigate, useSearchParams } from "react-router";
import { useForm, useWatch } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -85,37 +85,6 @@ export default function CreateCampaignContent() {
let alive = true;
- // proposalData가 있으면 우선 사용
- if (proposalData) {
- if (proposalData.campaignTitle) setValue("campaignName", proposalData.campaignTitle);
- if (proposalData.campaignDescription) setValue("description", proposalData.campaignDescription);
-
- // 태그 매핑 (ID를 문자열로 변환하여 사용)
- if (proposalData.contentTags?.formats && proposalData.contentTags.formats.length > 0) {
- setValue("format", String(proposalData.contentTags.formats[0].id));
- }
- if (proposalData.contentTags?.categories && proposalData.contentTags.categories.length > 0) {
- setValue("category", String(proposalData.contentTags.categories[0].id));
- }
- if (proposalData.contentTags?.tones && proposalData.contentTags.tones.length > 0) {
- setValue("tone", String(proposalData.contentTags.tones[0].id));
- }
- if (proposalData.contentTags?.involvements && proposalData.contentTags.involvements.length > 0) {
- setValue("involvement", String(proposalData.contentTags.involvements[0].id));
- }
- if (proposalData.contentTags?.usageRanges && proposalData.contentTags.usageRanges.length > 0) {
- setValue("usageScope", String(proposalData.contentTags.usageRanges[0].id));
- }
-
- const reward = proposalData.rewardAmount?.toString();
- if (reward) setValue("fee", reward);
-
- if (proposalData.product) setValue("sponsorProduct", proposalData.product);
- if (proposalData.startDate) setValue("startDate", proposalData.startDate);
- if (proposalData.endDate) setValue("endDate", proposalData.endDate);
- return;
- }
-
// URL 파라미터로 캠페인 조회 (기존 캠페인 제안 시)
const campaignIdParam = searchParams.get("campaignId");
@@ -131,24 +100,24 @@ export default function CreateCampaignContent() {
setValue("campaignName", detail.title);
setValue("description", detail.description);
setValue("fee", detail.rewardAmount.toString());
- setValue("sponsorProduct", detail.product);
+ setValue("sponsorProduct", detail.product ? [detail.product] : []);
if (detail.startDate) setValue("startDate", detail.startDate);
if (detail.endDate) setValue("endDate", detail.endDate);
if (detail.contentTags?.formats?.length > 0) {
- setValue("format", String(detail.contentTags.formats[0].id));
+ setValue("format", detail.contentTags.formats.map(f => String(f.id)));
}
if (detail.contentTags?.categories?.length > 0) {
- setValue("category", String(detail.contentTags.categories[0].id));
+ setValue("category", detail.contentTags.categories.map(c => String(c.id)));
}
if (detail.contentTags?.tones?.length > 0) {
- setValue("tone", String(detail.contentTags.tones[0].id));
+ setValue("tone", detail.contentTags.tones.map(t => String(t.id)));
}
if (detail.contentTags?.involvements?.length > 0) {
- setValue("involvement", String(detail.contentTags.involvements[0].id));
+ setValue("involvement", detail.contentTags.involvements.map(i => String(i.id)));
}
if (detail.contentTags?.usageRanges?.length > 0) {
- setValue("usageScope", String(detail.contentTags.usageRanges[0].id));
+ setValue("usageScope", detail.contentTags.usageRanges.map(u => String(u.id)));
}
} catch (error) {
console.error("캠페인 상세 조회 실패:", error);
@@ -156,6 +125,38 @@ export default function CreateCampaignContent() {
}
})();
}
+ return () => {
+ alive = false;
+ };
+ }
+
+ if (proposalData) {
+ if (proposalData.campaignTitle) setValue("campaignName", proposalData.campaignTitle);
+ if (proposalData.campaignDescription) setValue("description", proposalData.campaignDescription);
+
+ // 태그 매핑 (ID를 문자열로 변환하여 배열로 사용)
+ if (proposalData.contentTags?.formats && proposalData.contentTags.formats.length > 0) {
+ setValue("format", proposalData.contentTags.formats.map(f => String(f.id)));
+ }
+ if (proposalData.contentTags?.categories && proposalData.contentTags.categories.length > 0) {
+ setValue("category", proposalData.contentTags.categories.map(c => String(c.id)));
+ }
+ if (proposalData.contentTags?.tones && proposalData.contentTags.tones.length > 0) {
+ setValue("tone", proposalData.contentTags.tones.map(t => String(t.id)));
+ }
+ if (proposalData.contentTags?.involvements && proposalData.contentTags.involvements.length > 0) {
+ setValue("involvement", proposalData.contentTags.involvements.map(i => String(i.id)));
+ }
+ if (proposalData.contentTags?.usageRanges && proposalData.contentTags.usageRanges.length > 0) {
+ setValue("usageScope", proposalData.contentTags.usageRanges.map(u => String(u.id)));
+ }
+
+ const reward = proposalData.rewardAmount?.toString();
+ if (reward) setValue("fee", reward);
+
+ if (proposalData.product) setValue("sponsorProduct", [proposalData.product]);
+ if (proposalData.startDate) setValue("startDate", proposalData.startDate);
+ if (proposalData.endDate) setValue("endDate", proposalData.endDate);
}
return () => {
@@ -183,19 +184,33 @@ export default function CreateCampaignContent() {
const involvementOptions = toOptions(INVOLVEMENT_TAGS, tags?.involvements);
const usageScopeOptions = toOptions(USAGE_RANGE_TAGS, tags?.usageRanges);
- const sponsorProductOptions = proposalData?.product
- ? [{ value: proposalData.product, label: proposalData.product }]
- : type === "new"
- ? []
- : (proposalData?.products ?? [])
- .filter((p) => String(p.id).trim() && String(p.name).trim())
- .map((p) => ({ value: String(p.id), label: String(p.name).trim() }));
-
- // ID로 label 찾기 헬퍼 함수
- const findLabel = (options: { value: string; label: string }[], value?: string) => {
- if (!value) return undefined;
- const found = options.find((opt) => opt.value === value);
- return found?.label || value;
+ const sponsorProductOptions = useMemo(() => {
+ const baseOptions = proposalData?.product
+ ? [{ value: proposalData.product, label: proposalData.product }]
+ : type === "new"
+ ? []
+ : (proposalData?.products ?? [])
+ .filter((p) => String(p.id).trim() && String(p.name).trim())
+ .map((p) => ({ value: String(p.id), label: String(p.name).trim() }));
+
+ // formValues.sponsorProduct 배열에 있는데 options에 없는 항목들 추가
+ const missingOptions = (formValues.sponsorProduct || [])
+ .filter(sp => !baseOptions.find(opt => opt.value === sp))
+ .map(sp => ({ value: sp, label: sp }));
+
+ return [...missingOptions, ...baseOptions];
+ }, [proposalData, type, formValues.sponsorProduct]);
+
+ // ID 배열로 label들 찾기 헬퍼 함수
+ const findLabels = (options: { value: string; label: string }[], values?: string[]) => {
+ if (!values || values.length === 0) return undefined;
+ const labels = values
+ .map(value => {
+ const found = options.find((opt) => opt.value === value);
+ return found?.label || value;
+ })
+ .filter(Boolean);
+ return labels.length > 0 ? labels.join(", ") : undefined;
};
const onSubmit = () => {
@@ -229,13 +244,13 @@ export default function CreateCampaignContent() {
campaignId,
campaignName: formData.campaignName || "",
description: formData.description || "",
- formats: formData.format ? [{ id: Number(formData.format) }] : [],
- categories: formData.category ? [{ id: Number(formData.category) }] : [],
- tones: formData.tone ? [{ id: Number(formData.tone) }] : [],
- involvements: formData.involvement ? [{ id: Number(formData.involvement) }] : [],
- usageRanges: formData.usageScope ? [{ id: Number(formData.usageScope) }] : [],
+ formats: formData.format?.map(f => ({ id: Number(f) })) || [],
+ categories: formData.category?.map(c => ({ id: Number(c) })) || [],
+ tones: formData.tone?.map(t => ({ id: Number(t) })) || [],
+ involvements: formData.involvement?.map(i => ({ id: Number(i) })) || [],
+ usageRanges: formData.usageScope?.map(u => ({ id: Number(u) })) || [],
rewardAmount: Number(formData.fee) || 0,
- productId: Number(formData.sponsorProduct) || 0,
+ productId: Number(formData.sponsorProduct?.[0]) || 0,
startDate: formData.startDate || "",
endDate: formData.endDate || "",
};
@@ -324,8 +339,9 @@ export default function CreateCampaignContent() {
형식
setIsFormatSheetOpen(true)}
+ noTruncate={true}
/>
{/* 종류 / 톤 */}
@@ -334,7 +350,7 @@ export default function CreateCampaignContent() {
종류
setIsCategorySheetOpen(true)}
/>
@@ -342,7 +358,7 @@ export default function CreateCampaignContent() {
톤
setIsToneSheetOpen(true)}
/>
@@ -354,7 +370,7 @@ export default function CreateCampaignContent() {
관여도
setIsInvolvementSheetOpen(true)}
/>
@@ -362,7 +378,7 @@ export default function CreateCampaignContent() {
활용 범위
setIsUsageScopeSheetOpen(true)}
/>
@@ -377,7 +393,7 @@ export default function CreateCampaignContent() {
setIsSponsorProductSheetOpen(true)}
/>
@@ -434,9 +450,9 @@ export default function CreateCampaignContent() {
onClose={() => setIsFormatSheetOpen(false)}
title="형식"
options={formatOptions}
- selectedValues={formValues.format ? [formValues.format] : []}
- onSubmit={(values) => setValue("format", values[0] || "")}
- multiSelect={false}
+ selectedValues={formValues.format || []}
+ onSubmit={(values) => setValue("format", values)}
+ multiSelect={true}
/>
{/* 종류 선택 바텀시트 */}
@@ -445,9 +461,9 @@ export default function CreateCampaignContent() {
onClose={() => setIsCategorySheetOpen(false)}
title="종류"
options={categoryOptions}
- selectedValues={formValues.category ? [formValues.category] : []}
- onSubmit={(values) => setValue("category", values[0] || "")}
- multiSelect={false}
+ selectedValues={formValues.category || []}
+ onSubmit={(values) => setValue("category", values)}
+ multiSelect={true}
/>
{/* 톤 선택 바텀시트 */}
@@ -456,9 +472,9 @@ export default function CreateCampaignContent() {
onClose={() => setIsToneSheetOpen(false)}
title="톤"
options={toneOptions}
- selectedValues={formValues.tone ? [formValues.tone] : []}
- onSubmit={(values) => setValue("tone", values[0] || "")}
- multiSelect={false}
+ selectedValues={formValues.tone || []}
+ onSubmit={(values) => setValue("tone", values)}
+ multiSelect={true}
/>
{/* 관여도 선택 바텀시트 */}
@@ -467,9 +483,9 @@ export default function CreateCampaignContent() {
onClose={() => setIsInvolvementSheetOpen(false)}
title="관여도"
options={involvementOptions}
- selectedValues={formValues.involvement ? [formValues.involvement] : []}
- onSubmit={(values) => setValue("involvement", values[0] || "")}
- multiSelect={false}
+ selectedValues={formValues.involvement || []}
+ onSubmit={(values) => setValue("involvement", values)}
+ multiSelect={true}
/>
{/* 활용 범위 선택 바텀시트 */}
@@ -478,9 +494,9 @@ export default function CreateCampaignContent() {
onClose={() => setIsUsageScopeSheetOpen(false)}
title="활용 범위"
options={usageScopeOptions}
- selectedValues={formValues.usageScope ? [formValues.usageScope] : []}
- onSubmit={(values) => setValue("usageScope", values[0] || "")}
- multiSelect={false}
+ selectedValues={formValues.usageScope || []}
+ onSubmit={(values) => setValue("usageScope", values)}
+ multiSelect={true}
/>
{/* 협찬품 선택 바텀시트 */}
@@ -489,9 +505,9 @@ export default function CreateCampaignContent() {
onClose={() => setIsSponsorProductSheetOpen(false)}
title="협찬품 선택"
options={sponsorProductOptions}
- selectedValues={formValues.sponsorProduct ? [formValues.sponsorProduct] : []}
- onSubmit={(values) => setValue("sponsorProduct", values[0] || "")}
- multiSelect={false}
+ selectedValues={formValues.sponsorProduct || []}
+ onSubmit={(values) => setValue("sponsorProduct", values)}
+ multiSelect={true}
hasCustomInput={true}
/>
diff --git a/app/routes/matching/suggest/create/schema.ts b/app/routes/matching/suggest/create/schema.ts
index c860c23f..3bc73a69 100644
--- a/app/routes/matching/suggest/create/schema.ts
+++ b/app/routes/matching/suggest/create/schema.ts
@@ -9,12 +9,12 @@ export const campaignFormSchema = z.object({
.string()
.min(1, "설명을 입력해주세요")
.max(300, "설명은 300자 이내로 입력해주세요"),
- format: z.string().min(1, "형식을 선택해주세요"),
- category: z.string().min(1, "종류를 선택해주세요"),
- tone: z.string().min(1, "톤을 선택해주세요"),
- involvement: z.string().min(1, "관여도를 선택해주세요"),
- usageScope: z.string().min(1, "활용 범위를 선택해주세요"),
- sponsorProduct: z.string().min(1, "협찬품을 선택해주세요"),
+ format: z.array(z.string()).min(1, "형식을 선택해주세요"),
+ category: z.array(z.string()).min(1, "종류를 선택해주세요"),
+ tone: z.array(z.string()).min(1, "톤을 선택해주세요"),
+ involvement: z.array(z.string()).min(1, "관여도를 선택해주세요"),
+ usageScope: z.array(z.string()).min(1, "활용 범위를 선택해주세요"),
+ sponsorProduct: z.array(z.string()).min(1, "협찬품을 선택해주세요"),
fee: z
.string()
.min(1, "원고료를 입력해주세요")
@@ -34,12 +34,12 @@ export type CampaignFormData = z.infer;
export const defaultCampaignFormValues: CampaignFormData = {
campaignName: "",
description: "",
- format: "",
- category: "",
- tone: "",
- involvement: "",
- usageScope: "",
- sponsorProduct: "",
+ format: [],
+ category: [],
+ tone: [],
+ involvement: [],
+ usageScope: [],
+ sponsorProduct: [],
fee: "",
startDate: "",
endDate: "",
From 60d087bcf541a54e0c11664d30095b03ac451411 Mon Sep 17 00:00:00 2001
From: seyun31 <2ne1jenna@naver.com>
Date: Wed, 18 Feb 2026 19:25:00 +0900
Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=EB=A6=B0=ED=8A=B8=20=EC=98=A4?=
=?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../business/components/MatchingTabSection.tsx | 8 ++++----
app/routes/campaign-detail/route.tsx | 2 +-
app/routes/home/components/BannerCarousel.tsx | 14 +++++++-------
.../create/components/SelectBottomSheet.tsx | 15 +++++++--------
app/routes/notification/notification-content.tsx | 8 ++++----
5 files changed, 23 insertions(+), 24 deletions(-)
diff --git a/app/routes/business/components/MatchingTabSection.tsx b/app/routes/business/components/MatchingTabSection.tsx
index 9d1c31f0..40103390 100644
--- a/app/routes/business/components/MatchingTabSection.tsx
+++ b/app/routes/business/components/MatchingTabSection.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect } from "react";
+import { useState, useEffect, useCallback } from "react";
import { searchCollaborations, type CampaignCollaboration } from "../calendar/api/calendar";
import searchIcon from "../../../assets/search2.svg";
import closeIcon from "../../../assets/cancel.svg";
@@ -33,7 +33,7 @@ export default function MatchingTabSection({ subTab, setSubTab, receivedCount, k
const [isLoading, setIsLoading] = useState(false);
// 캠페인 검색 함수
- const fetchCampaigns = async () => {
+ const fetchCampaigns = useCallback(async () => {
if (!keyword.trim()) return;
setIsLoading(true);
try {
@@ -47,7 +47,7 @@ export default function MatchingTabSection({ subTab, setSubTab, receivedCount, k
} finally {
setIsLoading(false);
}
- };
+ }, [keyword, subTab]);
// 검색 상태에 따라 캠페인 검색
useEffect(() => {
@@ -56,7 +56,7 @@ export default function MatchingTabSection({ subTab, setSubTab, receivedCount, k
} else {
setCampaigns([]);
}
- }, [isSearching, keyword, subTab]);
+ }, [isSearching, keyword, subTab, fetchCampaigns]);
if (isSearching) {
return (
diff --git a/app/routes/campaign-detail/route.tsx b/app/routes/campaign-detail/route.tsx
index 919d626c..e44e2375 100644
--- a/app/routes/campaign-detail/route.tsx
+++ b/app/routes/campaign-detail/route.tsx
@@ -124,7 +124,7 @@ export default function CampaignDetailRoute() {
return () => {
alive = false;
};
- }, [resolvedBrandId, resolvedDomain]);
+ }, [resolvedBrandId, resolvedDomain, matchRateParam]);
if (!campaignId) {
return (
diff --git a/app/routes/home/components/BannerCarousel.tsx b/app/routes/home/components/BannerCarousel.tsx
index 26932ff4..ec3739d8 100644
--- a/app/routes/home/components/BannerCarousel.tsx
+++ b/app/routes/home/components/BannerCarousel.tsx
@@ -41,13 +41,6 @@ export default function BannerCarousel({
const [dragOffset, setDragOffset] = useState(0);
const timerRef = useRef | null>(null);
- const start = useCallback(() => {
- stop();
- timerRef.current = setInterval(() => {
- setCurrent((prev) => prev + 1);
- }, INTERVAL);
- }, [banners.length]);
-
const stop = useCallback(() => {
if (timerRef.current) {
clearInterval(timerRef.current);
@@ -55,6 +48,13 @@ export default function BannerCarousel({
}
}, []);
+ const start = useCallback(() => {
+ stop();
+ timerRef.current = setInterval(() => {
+ setCurrent((prev) => prev + 1);
+ }, INTERVAL);
+ }, [stop]);
+
useEffect(() => {
start();
return stop;
diff --git a/app/routes/matching/suggest/create/components/SelectBottomSheet.tsx b/app/routes/matching/suggest/create/components/SelectBottomSheet.tsx
index 74f89c78..bd9ee5f5 100644
--- a/app/routes/matching/suggest/create/components/SelectBottomSheet.tsx
+++ b/app/routes/matching/suggest/create/components/SelectBottomSheet.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect } from "react";
+import { useState } from "react";
import FilterBottomSheet from "../../../../../components/common/FilterBottomSheet";
import Button from "../../../../../components/common/Button";
import { CheckIcon } from "../../../../auth/components/CheckIcon";
@@ -19,7 +19,7 @@ interface SelectBottomSheetProps {
hasCustomInput?: boolean;
}
-export default function SelectBottomSheet({
+function SelectBottomSheetInner({
isOpen,
onClose,
title,
@@ -32,10 +32,6 @@ export default function SelectBottomSheet({
const [selected, setSelected] = useState(selectedValues);
const [customInput, setCustomInput] = useState("");
- useEffect(() => {
- setSelected(selectedValues);
- }, [selectedValues, isOpen]);
-
const handleToggle = (value: string) => {
if (multiSelect) {
setSelected((prev) =>
@@ -57,8 +53,6 @@ export default function SelectBottomSheet({
};
const handleClose = () => {
- setSelected(selectedValues);
- setCustomInput("");
onClose();
};
@@ -124,3 +118,8 @@ export default function SelectBottomSheet({
);
}
+
+export default function SelectBottomSheet(props: SelectBottomSheetProps) {
+ // Reset component state when bottom sheet opens by changing key
+ return ;
+}
diff --git a/app/routes/notification/notification-content.tsx b/app/routes/notification/notification-content.tsx
index 05d69037..b43e1ad7 100644
--- a/app/routes/notification/notification-content.tsx
+++ b/app/routes/notification/notification-content.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect } from "react";
+import { useState, useEffect, useCallback } from "react";
import { fetchNotifications, readNotification, readAllNotifications, type NotificationItem as NotificationType } from "./api/notification";
import { useHideHeader } from "../../hooks/useHideHeader";
import NavigationHeader from "../../components/common/NavigateHeader";
@@ -29,7 +29,7 @@ export default function NotificationContent() {
useHideHeader(true);
- const fetchNotificationsData = async () => {
+ const fetchNotificationsData = useCallback(async () => {
setLoading(true);
try {
const data = await fetchNotifications(activeTab);
@@ -42,11 +42,11 @@ export default function NotificationContent() {
} finally {
setLoading(false);
}
- };
+ }, [activeTab]);
useEffect(() => {
fetchNotificationsData();
- }, [activeTab]);
+ }, [fetchNotificationsData]);
// 전체 읽기 핸들러
const handleReadAll = async () => {