From ecdb9d78de22c15858bfc866128b675c63d792bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=84=B8=EC=9C=A4?= <2ne1jenna@naver.com> Date: Thu, 12 Feb 2026 14:40:49 +0900 Subject: [PATCH 1/6] =?UTF-8?q?refactor:=20=EC=BA=A0=ED=8E=98=EC=9D=B8=20?= =?UTF-8?q?=EC=A0=9C=EC=95=88=ED=95=98=EA=B8=B0=20api=20=ED=83=9C=EA=B7=B8?= =?UTF-8?q?=EA=B0=92=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/data/proposalTags.ts | 85 +++++++++++++++++++ .../chat/resuggest/resuggest-content.tsx | 61 ++++--------- .../create/create-campaign-content.tsx | 68 +++++++-------- .../suggest/matching-suggest-content.tsx | 77 ++++++++++++++--- app/stores/campaign-proposal.ts | 18 ++++ 5 files changed, 216 insertions(+), 93 deletions(-) create mode 100644 app/data/proposalTags.ts diff --git a/app/data/proposalTags.ts b/app/data/proposalTags.ts new file mode 100644 index 00000000..794fdd39 --- /dev/null +++ b/app/data/proposalTags.ts @@ -0,0 +1,85 @@ +export interface ProposalTag { + id: number; + name: string; +} + +export interface ProposalTagGroup { + sort: string; + sortKorName: string; + tags: ProposalTag[]; +} + +export const PROPOSAL_TAGS: ProposalTagGroup[] = [ + { + sort: "FORMAT", + sortKorName: "형식", + tags: [ + { id: 1, name: "인스타 스토리" }, + { id: 2, name: "인스타 포스트" }, + { id: 3, name: "인스타 릴스" }, + { id: 4, name: "기타" }, + ], + }, + { + sort: "CATEGORY", + sortKorName: "종류", + tags: [ + { id: 5, name: "브이로그" }, + { id: 6, name: "리뷰" }, + { id: 7, name: "겟레디윗미" }, + { id: 8, name: "비포&애프터" }, + { id: 9, name: "스토리/썰" }, + { id: 10, name: "챌린지" }, + { id: 11, name: "기타" }, + ], + }, + { + sort: "TONE", + sortKorName: "톤", + tags: [ + { id: 12, name: "전문적인" }, + { id: 13, name: "감성적인" }, + { id: 14, name: "유쾌/재밌는" }, + { id: 15, name: "트렌디한" }, + { id: 16, name: "일상적인" }, + { id: 17, name: "수다적인" }, + { id: 18, name: "기타" }, + ], + }, + { + sort: "INVOLVEMENT", + sortKorName: "관여도", + tags: [ + { id: 19, name: "관여안함" }, + { id: 20, name: "가이드만 제공" }, + { id: 21, name: "대본 일부 제공" }, + { id: 22, name: "모든 연출 관여" }, + { id: 23, name: "기타" }, + ], + }, + { + sort: "USAGE_RANGE", + sortKorName: "활용 범위", + tags: [ + { id: 24, name: "크리에이터 1차활용" }, + { id: 25, name: "브랜드 2차활용" }, + { id: 26, name: "기타" }, + ], + }, +]; + +const findGroup = (sort: string) => + PROPOSAL_TAGS.find((g) => g.sort === sort)!; + +export const FORMAT_TAGS = findGroup("FORMAT").tags; +export const CATEGORY_TAGS = findGroup("CATEGORY").tags; +export const TONE_TAGS = findGroup("TONE").tags; +export const INVOLVEMENT_TAGS = findGroup("INVOLVEMENT").tags; +export const USAGE_RANGE_TAGS = findGroup("USAGE_RANGE").tags; + +/** name → id 매핑 (전체) */ +export const PROPOSAL_TAG_ID_BY_NAME: Record = + PROPOSAL_TAGS.flatMap((g) => g.tags).reduce( + (acc, t) => ({ ...acc, [t.name]: t.id }), + {} as Record, + ); diff --git a/app/routes/chat/resuggest/resuggest-content.tsx b/app/routes/chat/resuggest/resuggest-content.tsx index d64fe16f..e6e6ed2b 100644 --- a/app/routes/chat/resuggest/resuggest-content.tsx +++ b/app/routes/chat/resuggest/resuggest-content.tsx @@ -30,9 +30,13 @@ import { type CampaignFormData, } from "../../matching/suggest/create/schema"; import { - CONTENT_FILTER, -} from "../../../data/filter"; -import { TAG_NAME_BY_ID } from "../../../data/tagNameById"; + FORMAT_TAGS, + CATEGORY_TAGS, + TONE_TAGS, + INVOLVEMENT_TAGS, + USAGE_RANGE_TAGS, + PROPOSAL_TAG_ID_BY_NAME, +} from "../../../data/proposalTags"; export default function ReSuggestContent() { const navigate = useNavigate(); @@ -73,20 +77,6 @@ export default function ReSuggestContent() { mode: "onChange", }); - // 태그 이름으로 ID를 찾는 맵 생성 - const ID_BY_TAG_NAME: Record = Object.entries(TAG_NAME_BY_ID).reduce( - (acc, [id, name]) => ({ ...acc, [name]: Number(id) }), - {} - ); - - // 태그 매핑 보정 - const getMappedId = (name: string) => { - if (name === "인스타 포스트" || name === "인스타 포스터") return 172; - if (name === "스토리&썰" || name === "스토리/썰") return 178; - if (name === "가이드만 제공" || name === "가이드 라인만 제공") return 187; - return ID_BY_TAG_NAME[name]; - }; - // 초기 값 채우기 useEffect(() => { if (proposalData) { @@ -95,27 +85,27 @@ export default function ReSuggestContent() { if (proposalData.contentTags?.formats && proposalData.contentTags.formats.length > 0) { const t = proposalData.contentTags.formats[0]; - const id = t.id ?? getMappedId(t.name); + const id = t.id ?? PROPOSAL_TAG_ID_BY_NAME[t.name]; if (id) setValue("format", String(id)); } if (proposalData.contentTags?.categories && proposalData.contentTags.categories.length > 0) { const t = proposalData.contentTags.categories[0]; - const id = t.id ?? getMappedId(t.name); + const id = t.id ?? PROPOSAL_TAG_ID_BY_NAME[t.name]; if (id) setValue("category", String(id)); } if (proposalData.contentTags?.tones && proposalData.contentTags.tones.length > 0) { const t = proposalData.contentTags.tones[0]; - const id = t.id ?? getMappedId(t.name); + const id = t.id ?? PROPOSAL_TAG_ID_BY_NAME[t.name]; if (id) setValue("tone", String(id)); } if (proposalData.contentTags?.involvements && proposalData.contentTags.involvements.length > 0) { const t = proposalData.contentTags.involvements[0]; - const id = t.id ?? getMappedId(t.name); + const id = t.id ?? PROPOSAL_TAG_ID_BY_NAME[t.name]; if (id) setValue("involvement", String(id)); } if (proposalData.contentTags?.usageRanges && proposalData.contentTags.usageRanges.length > 0) { const t = proposalData.contentTags.usageRanges[0]; - const id = t.id ?? getMappedId(t.name); + const id = t.id ?? PROPOSAL_TAG_ID_BY_NAME[t.name]; if (id) setValue("usageScope", String(id)); } @@ -140,18 +130,11 @@ export default function ReSuggestContent() { const formValues = useWatch({ control, defaultValue: defaultCampaignFormValues }); - const getOptions = (filterKeys: readonly string[]) => { - return filterKeys.map((name) => ({ - value: String(getMappedId(name) || name), - label: name, - })); - }; - - const formatOptions = getOptions(CONTENT_FILTER.형식); - const categoryOptions = getOptions(CONTENT_FILTER.종류); - const toneOptions = getOptions(CONTENT_FILTER.톤); - const involvementOptions = getOptions(CONTENT_FILTER.관여도); - const usageScopeOptions = getOptions(CONTENT_FILTER["활용 범위"]); + const formatOptions = FORMAT_TAGS.map((t) => ({ value: String(t.id), label: t.name })); + const categoryOptions = CATEGORY_TAGS.map((t) => ({ value: String(t.id), label: t.name })); + const toneOptions = TONE_TAGS.map((t) => ({ value: String(t.id), label: t.name })); + 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 }] @@ -160,15 +143,7 @@ export default function ReSuggestContent() { const findLabel = (options: { value: string; label: string }[], value?: string) => { if (!value) return undefined; const option = options.find((opt) => opt.value === value); - if (option) return option.label; - - // Fallback to global tag mapping if value is a numeric ID - const numericId = Number(value); - if (!isNaN(numericId) && TAG_NAME_BY_ID[numericId]) { - return TAG_NAME_BY_ID[numericId]; - } - - return ""; + return option?.label || ""; }; const onSubmit = () => { diff --git a/app/routes/matching/suggest/create/create-campaign-content.tsx b/app/routes/matching/suggest/create/create-campaign-content.tsx index d6d6accc..ceb8aff5 100644 --- a/app/routes/matching/suggest/create/create-campaign-content.tsx +++ b/app/routes/matching/suggest/create/create-campaign-content.tsx @@ -21,8 +21,15 @@ import ProfileSelector from "../../components/ProfileSelector"; import SelectBottomSheet from "./components/SelectBottomSheet"; import DatePickerBottomSheet from "./components/DatePickerBottomSheet"; import ProposalModal from "../../components/ProposalModal"; -import { CONTENT_FILTER } from "../../../../data/filter"; -import { TAG_NAME_BY_ID } from "../../../../data/tagNameById"; +import { + FORMAT_TAGS, + CATEGORY_TAGS, + TONE_TAGS, + INVOLVEMENT_TAGS, + USAGE_RANGE_TAGS, + PROPOSAL_TAG_ID_BY_NAME, + type ProposalTag, +} from "../../../../data/proposalTags"; import { campaignFormSchema, defaultCampaignFormValues, @@ -62,6 +69,7 @@ export default function CreateCampaignContent() { const { control, setValue, + reset, handleSubmit, formState: { errors }, } = useForm({ @@ -71,14 +79,18 @@ export default function CreateCampaignContent() { // 폼 초기화 - 모든 데이터 소스를 한 번에 처리 useEffect(() => { - if (type !== "existing") return; + // 신규 제안 시 폼 초기화 + if (type !== "existing") { + reset(defaultCampaignFormValues); + return; + } let alive = true; // proposalData가 있으면 우선 사용 if (proposalData) { - setValue("campaignName", proposalData.campaignTitle || ""); - setValue("description", proposalData.campaignDescription || ""); + if (proposalData.campaignTitle) setValue("campaignName", proposalData.campaignTitle); + if (proposalData.campaignDescription) setValue("description", proposalData.campaignDescription); // 태그 매핑 (ID를 문자열로 변환하여 사용) if (proposalData.contentTags?.formats && proposalData.contentTags.formats.length > 0) { @@ -97,14 +109,16 @@ export default function CreateCampaignContent() { setValue("usageScope", String(proposalData.contentTags.usageRanges[0].id)); } - setValue("fee", proposalData.rewardAmount?.toString() || ""); - setValue("sponsorProduct", proposalData.product || ""); - setValue("startDate", proposalData.startDate || ""); - setValue("endDate", proposalData.endDate || ""); + 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 파라미터로 캠페인 조회 + // URL 파라미터로 캠페인 조회 (기존 캠페인 제안 시) const brandIdParam = searchParams.get("brandId"); const campaignIdParam = searchParams.get("campaignId"); @@ -144,45 +158,27 @@ export default function CreateCampaignContent() { return () => { alive = false; }; - // proposalData와 searchParams만 의존성에 포함 (location.state 제거) }, [type, proposalData, searchParams, setValue]); const formValues = useWatch({ control, defaultValue: defaultCampaignFormValues }); const tags = proposalData?.contentTags; - // 태그 이름으로 ID를 찾는 맵 생성 - const ID_BY_TAG_NAME: Record = Object.entries(TAG_NAME_BY_ID).reduce( - (acc, [id, name]) => ({ ...acc, [name]: Number(id) }), - {} - ); - - // 태그 매핑 보정 - const getMappedId = (name: string) => { - if (name === "인스타 포스트") return 172; - if (name === "스토리&썰") return 178; - if (name === "가이드만 제공") return 187; - return ID_BY_TAG_NAME[name]; - }; - - const getOptions = (campaignTags: { id?: number; name: string }[] | undefined, filterKeys: readonly string[]) => { + const toOptions = (defaultTags: ProposalTag[], campaignTags?: { id?: number; name: string }[]) => { if (campaignTags && campaignTags.length > 0) { return campaignTags.map((t) => ({ - value: String((t.id ?? getMappedId(t.name)) || t.name), + value: String(t.id ?? PROPOSAL_TAG_ID_BY_NAME[t.name] ?? t.name), label: t.name, })); } - return filterKeys.map((name) => ({ - value: String(getMappedId(name) || name), - label: name, - })); + return defaultTags.map((t) => ({ value: String(t.id), label: t.name })); }; - const formatOptions = getOptions(tags?.formats, CONTENT_FILTER.형식); - const categoryOptions = getOptions(tags?.categories, CONTENT_FILTER.종류); - const toneOptions = getOptions(tags?.tones, CONTENT_FILTER.톤); - const involvementOptions = getOptions(tags?.involvements, CONTENT_FILTER.관여도); - const usageScopeOptions = getOptions(tags?.usageRanges, CONTENT_FILTER["활용 범위"]); + const formatOptions = toOptions(FORMAT_TAGS, tags?.formats); + const categoryOptions = toOptions(CATEGORY_TAGS, tags?.categories); + const toneOptions = toOptions(TONE_TAGS, tags?.tones); + 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 }] diff --git a/app/routes/matching/suggest/matching-suggest-content.tsx b/app/routes/matching/suggest/matching-suggest-content.tsx index 0eac6ff5..fad5d9d4 100644 --- a/app/routes/matching/suggest/matching-suggest-content.tsx +++ b/app/routes/matching/suggest/matching-suggest-content.tsx @@ -6,21 +6,24 @@ import { CheckIcon } from "../../auth/components/CheckIcon"; import NewSuggestIcon from "../../../assets/icon/new-suggest.svg"; import ExistSuggestIcon from "../../../assets/icon/exist-suggest.svg"; import LoadingSpinner from "../../../components/common/LoadingSpinner"; -import { useCampaignProposalStore } from "../../../stores/campaign-proposal"; +import { useCampaignProposalStore, type RecruitingCampaignItem } from "../../../stores/campaign-proposal"; import { getRecruitingCampaigns, - type RecruitingCampaign, } from "../api/matching"; +import { apiClient } from "../../../api/axios"; +import type { CampaignDetailApiResponse } from "../../campaign-detail/types"; import { toast } from "sonner"; import { useHideBottomTab } from "../../../hooks/useHideBottomTab"; export default function MatchingSuggestContent() { const navigate = useNavigate(); const proposalData = useCampaignProposalStore((state) => state.proposalData); + const storedRecruitingCampaigns = useCampaignProposalStore((state) => state.recruitingCampaigns); + const setProposalData = useCampaignProposalStore((state) => state.setProposalData); const [isSheetOpen, setIsSheetOpen] = useState(false); const [recruitingCampaigns, setRecruitingCampaigns] = useState< - RecruitingCampaign[] + RecruitingCampaignItem[] >([]); const [selectedCampaignId, setSelectedCampaignId] = useState( null, @@ -30,14 +33,28 @@ export default function MatchingSuggestContent() { useHideBottomTab(isSheetOpen); const handleNewCampaign = () => { - navigate("/matching/suggest/create?type=new"); + const brandId = proposalData?.brandId; + const domain = proposalData?.domain || "beauty"; + + if (brandId) { + navigate(`/matching/suggest/create?type=new&brandId=${brandId}&domain=${domain}`); + } else { + navigate("/matching/suggest/create?type=new"); + } }; const handleExistingCampaign = async () => { if (proposalData?.brandId) { - setIsLoading(true); setIsSheetOpen(true); + // store에 이미 저장된 모집중 캠페인이 있으면 재사용 + if (storedRecruitingCampaigns.length > 0) { + setRecruitingCampaigns(storedRecruitingCampaigns); + return; + } + + // 없으면 API 호출 + setIsLoading(true); try { const campaigns = await getRecruitingCampaigns(proposalData.brandId); setRecruitingCampaigns(campaigns); @@ -59,21 +76,53 @@ export default function MatchingSuggestContent() { setSelectedCampaignId(id); }; - const handleSheetSubmit = () => { + const handleSheetSubmit = async () => { if (!selectedCampaignId) { toast.error("캠페인을 선택해주세요"); return; } - const selectedCampaign = recruitingCampaigns.find( - (c) => c.campaignId === selectedCampaignId, - ); - if (!selectedCampaign || !proposalData) return; + if (!proposalData) return; + + // 선택한 캠페인의 상세 정보를 조회하여 proposalData에 매핑 + setIsLoading(true); + try { + const res = await apiClient.get( + `/api/v1/campaigns/${selectedCampaignId}`, + ); + + if (!res.data?.isSuccess) { + toast.error(res.data?.message || "캠페인 정보를 불러오지 못했어요."); + return; + } - // 선택한 캠페인 정보와 함께 페이지 이동 - navigate( - `/matching/suggest/create?type=existing&brandId=${proposalData.brandId}&campaignId=${selectedCampaignId}&domain=${proposalData.domain}`, - ); + const campaign = res.data.result; + + setProposalData({ + ...proposalData, + campaignId: selectedCampaignId, + campaignTitle: campaign.title, + campaignDescription: campaign.description, + rewardAmount: campaign.rewardAmount, + product: campaign.product, + startDate: campaign.startDate, + endDate: campaign.endDate, + contentTags: { + formats: campaign.contentTags?.formats, + categories: campaign.contentTags?.categories, + tones: campaign.contentTags?.tones, + involvements: campaign.contentTags?.involvements, + usageRanges: campaign.contentTags?.usageRanges, + }, + }); + + setIsSheetOpen(false); + navigate("/matching/suggest/create?type=existing"); + } catch { + toast.error("캠페인 정보를 불러오지 못했어요."); + } finally { + setIsLoading(false); + } }; const handleSheetClose = () => { diff --git a/app/stores/campaign-proposal.ts b/app/stores/campaign-proposal.ts index ed603adf..be8a27d9 100644 --- a/app/stores/campaign-proposal.ts +++ b/app/stores/campaign-proposal.ts @@ -1,5 +1,16 @@ import { create } from "zustand"; +export interface RecruitingCampaignItem { + campaignId: number; + brandName: string; + title: string; + recruitQuota: number; + rewardAmount: number; + imageUrl?: string; + dday: number; + like?: boolean; +} + export interface CampaignProposalData { brandId: number; campaignId?: number; @@ -28,6 +39,10 @@ type CampaignProposalStore = { setProposalData: (data: CampaignProposalData) => void; clearProposalData: () => void; + // 브랜드 상세에서 조회한 모집중 캠페인 목록 + recruitingCampaigns: RecruitingCampaignItem[]; + setRecruitingCampaigns: (campaigns: RecruitingCampaignItem[]) => void; + // 사용자 프로필 정보 snsAccount: string | null; setSnsAccount: (account: string) => void; @@ -38,6 +53,9 @@ export const useCampaignProposalStore = create((set) => ( setProposalData: (data) => set({ proposalData: data }), clearProposalData: () => set({ proposalData: null }), + recruitingCampaigns: [], + setRecruitingCampaigns: (campaigns) => set({ recruitingCampaigns: campaigns }), + snsAccount: null, setSnsAccount: (account) => set({ snsAccount: account }), })); From 6845dd8a7c045dc65ecc33e0dcecaf0b7db5921d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=84=B8=EC=9C=A4?= <2ne1jenna@naver.com> Date: Thu, 12 Feb 2026 15:04:34 +0900 Subject: [PATCH 2/6] =?UTF-8?q?refactor:=20=EC=A0=9C=EC=95=88=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=ED=8F=BC=20=EB=B0=94=ED=85=80=EC=8B=9C=ED=8A=B8=20?= =?UTF-8?q?=ED=83=9C=EA=B7=B8=EA=B0=92=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routes/chat/components/CampaignListBottomSheet.tsx | 1 - app/routes/chat/resuggest/resuggest-content.tsx | 5 ++++- .../suggest/create/components/SelectBottomSheet.tsx | 2 +- .../matching/suggest/create/create-campaign-content.tsx | 9 +++++++-- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/app/routes/chat/components/CampaignListBottomSheet.tsx b/app/routes/chat/components/CampaignListBottomSheet.tsx index 9a3a38ac..48c8551d 100644 --- a/app/routes/chat/components/CampaignListBottomSheet.tsx +++ b/app/routes/chat/components/CampaignListBottomSheet.tsx @@ -92,7 +92,6 @@ export default function CampaignListBottomSheet({ isOpen, onClose, onSelect }: P selectedValues={[]} onSubmit={handleSubmit} multiSelect={false} - hasCustomInput={false} /> ); } diff --git a/app/routes/chat/resuggest/resuggest-content.tsx b/app/routes/chat/resuggest/resuggest-content.tsx index e6e6ed2b..97b0c480 100644 --- a/app/routes/chat/resuggest/resuggest-content.tsx +++ b/app/routes/chat/resuggest/resuggest-content.tsx @@ -138,7 +138,9 @@ export default function ReSuggestContent() { const sponsorProductOptions = proposalData?.product ? [{ value: proposalData.product, label: proposalData.product }] - : (proposalData?.products ?? []).map((p) => ({ value: String(p.id), label: p.name })); + : (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; @@ -441,6 +443,7 @@ export default function ReSuggestContent() { selectedValues={formValues.sponsorProduct ? [formValues.sponsorProduct] : []} onSubmit={(values) => setValue("sponsorProduct", values[0] || "", { shouldValidate: true })} multiSelect={false} + hasCustomInput={true} /> (selectedValues); const [customInput, setCustomInput] = useState(""); diff --git a/app/routes/matching/suggest/create/create-campaign-content.tsx b/app/routes/matching/suggest/create/create-campaign-content.tsx index ceb8aff5..6b504370 100644 --- a/app/routes/matching/suggest/create/create-campaign-content.tsx +++ b/app/routes/matching/suggest/create/create-campaign-content.tsx @@ -162,7 +162,7 @@ export default function CreateCampaignContent() { const formValues = useWatch({ control, defaultValue: defaultCampaignFormValues }); - const tags = proposalData?.contentTags; + const tags = type === "new" ? undefined : proposalData?.contentTags; const toOptions = (defaultTags: ProposalTag[], campaignTags?: { id?: number; name: string }[]) => { if (campaignTags && campaignTags.length > 0) { @@ -182,7 +182,11 @@ export default function CreateCampaignContent() { const sponsorProductOptions = proposalData?.product ? [{ value: proposalData.product, label: proposalData.product }] - : (proposalData?.products ?? []).map((p) => ({ value: String(p.id), label: p.name })); + : 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) => { @@ -482,6 +486,7 @@ export default function CreateCampaignContent() { selectedValues={formValues.sponsorProduct ? [formValues.sponsorProduct] : []} onSubmit={(values) => setValue("sponsorProduct", values[0] || "")} multiSelect={false} + hasCustomInput={true} /> {/* 시작 날짜 선택 바텀시트 */} From 03bfd511ae53d1325bc63d144abc3ae1da0d725a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=84=B8=EC=9C=A4?= <2ne1jenna@naver.com> Date: Thu, 12 Feb 2026 15:21:50 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20`useEffect`=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EB=B0=B0=EC=97=B4=EC=97=90=20`reset`=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routes/matching/suggest/create/create-campaign-content.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routes/matching/suggest/create/create-campaign-content.tsx b/app/routes/matching/suggest/create/create-campaign-content.tsx index 6b504370..544bc31c 100644 --- a/app/routes/matching/suggest/create/create-campaign-content.tsx +++ b/app/routes/matching/suggest/create/create-campaign-content.tsx @@ -158,7 +158,7 @@ export default function CreateCampaignContent() { return () => { alive = false; }; - }, [type, proposalData, searchParams, setValue]); + }, [type, proposalData, searchParams, setValue, reset]); const formValues = useWatch({ control, defaultValue: defaultCampaignFormValues }); From d0a8e77c276a63d32c5aefcc8637b34bf767ef29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=84=B8=EC=9C=A4?= <2ne1jenna@naver.com> Date: Thu, 12 Feb 2026 16:00:50 +0900 Subject: [PATCH 4/6] =?UTF-8?q?refactor:=20SelectBottomSheet=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../matching/suggest/create/components/SelectBottomSheet.tsx | 2 +- .../matching/suggest/create/create-campaign-content.tsx | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/app/routes/matching/suggest/create/components/SelectBottomSheet.tsx b/app/routes/matching/suggest/create/components/SelectBottomSheet.tsx index ba1d2e44..e5ff4b8d 100644 --- a/app/routes/matching/suggest/create/components/SelectBottomSheet.tsx +++ b/app/routes/matching/suggest/create/components/SelectBottomSheet.tsx @@ -112,7 +112,7 @@ export default function SelectBottomSheet({ variant="primary" size="lg" onClick={handleSubmit} - className="text-title7 w-[327px] h-[44px] flex items-center justify-center gap-[10px]" + className="text-title7 w-full h-[44px] flex items-center justify-center gap-[10px]" > 선택 완료 diff --git a/app/routes/matching/suggest/create/create-campaign-content.tsx b/app/routes/matching/suggest/create/create-campaign-content.tsx index 544bc31c..9e956de2 100644 --- a/app/routes/matching/suggest/create/create-campaign-content.tsx +++ b/app/routes/matching/suggest/create/create-campaign-content.tsx @@ -77,7 +77,6 @@ export default function CreateCampaignContent() { defaultValues: defaultCampaignFormValues, }); - // 폼 초기화 - 모든 데이터 소스를 한 번에 처리 useEffect(() => { // 신규 제안 시 폼 초기화 if (type !== "existing") { @@ -195,7 +194,6 @@ export default function CreateCampaignContent() { return found?.label || value; }; - const onSubmit = () => { // 폼 검증 후 확인 다이얼로그 표시 setIsConfirmDialogOpen(true); @@ -238,11 +236,9 @@ export default function CreateCampaignContent() { endDate: formData.endDate || "", }; - // 낙관적 UI: 즉시 완료 모달로 전환 setIsConfirmDialogOpen(false); setIsSuccessModalOpen(true); - // 백그라운드에서 API 호출 createCampaignProposal(requestData).catch((error) => { console.error("캠페인 제안 실패:", error); toast.error("캠페인 제안에 실패했습니다. 다시 시도해주세요."); From 908a524de135672333682dbb6a48a02872d0c02f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=84=B8=EC=9C=A4?= <2ne1jenna@naver.com> Date: Thu, 12 Feb 2026 16:30:34 +0900 Subject: [PATCH 5/6] =?UTF-8?q?refactor:=20=EB=B0=94=ED=85=80=20=EC=8B=9C?= =?UTF-8?q?=ED=8A=B8=20UI=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=20=EB=A1=9C=EC=A7=81=20=EB=B0=8F=20UI=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/common/FilterButton.tsx | 9 +- app/routes/matching/brand/brand-content.tsx | 10 +-- .../matching/campaign/campaign-content.tsx | 10 +-- .../matching/components/ProposalModal.tsx | 7 +- .../create/components/SelectBottomSheet.tsx | 2 +- .../create/create-campaign-content.tsx | 58 +++++++------ .../suggest/matching-suggest-content.tsx | 83 ++++--------------- 7 files changed, 67 insertions(+), 112 deletions(-) diff --git a/app/components/common/FilterButton.tsx b/app/components/common/FilterButton.tsx index 0ae62f95..3bb5f340 100644 --- a/app/components/common/FilterButton.tsx +++ b/app/components/common/FilterButton.tsx @@ -11,11 +11,10 @@ const truncateLabel = (text: string, maxLength = 10) => { export default function FilterButton({ label, isActive, className = "", ...props }: FilterButtonProps) { return ( diff --git a/app/routes/matching/suggest/create/create-campaign-content.tsx b/app/routes/matching/suggest/create/create-campaign-content.tsx index 9e956de2..76ac8151 100644 --- a/app/routes/matching/suggest/create/create-campaign-content.tsx +++ b/app/routes/matching/suggest/create/create-campaign-content.tsx @@ -3,7 +3,7 @@ import { useNavigate, useSearchParams } from "react-router"; import { useForm, useWatch } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { toast } from "sonner"; -import { createCampaignProposal, getRecruitingCampaigns, type RecruitingCampaign } from "../../api/matching"; +import { createCampaignProposal, getCampaignDetail } from "../../api/matching"; import { tokenStorage } from "../../../../lib/token"; import { useCampaignProposalStore } from "../../../../stores/campaign-proposal"; import { useAuthStore } from "../../../../stores/auth-store"; @@ -45,7 +45,6 @@ export default function CreateCampaignContent() { const me = useAuthStore((state) => state.me); // 바텀시트 상태 - const [selectedCampaign, setSelectedCampaign] = useState(null); // 각 필드별 바텀시트 상태 const [isFormatSheetOpen, setIsFormatSheetOpen] = useState(false); @@ -118,37 +117,42 @@ export default function CreateCampaignContent() { } // URL 파라미터로 캠페인 조회 (기존 캠페인 제안 시) - const brandIdParam = searchParams.get("brandId"); const campaignIdParam = searchParams.get("campaignId"); - if (brandIdParam && campaignIdParam) { - const brandId = Number(brandIdParam); + if (campaignIdParam) { const campaignId = Number(campaignIdParam); - if (Number.isFinite(brandId) && brandId > 0 && Number.isFinite(campaignId) && campaignId > 0) { + if (Number.isFinite(campaignId) && campaignId > 0) { (async () => { try { - const campaigns = await getRecruitingCampaigns(brandId); + const detail = await getCampaignDetail(campaignId); if (!alive) return; - const selectedCampaign = campaigns.find((c) => c.campaignId === campaignId); - if (!selectedCampaign) return; - - // 선택한 캠페인 정보를 폼에 반영 - setValue("campaignName", selectedCampaign.title); - setValue("fee", selectedCampaign.rewardAmount.toString()); - - // dday를 사용해 종료 날짜 계산 - const today = new Date(); - const endDate = new Date(today); - endDate.setDate(today.getDate() + selectedCampaign.dday); - - setValue("startDate", today.toISOString().split("T")[0]); - setValue("endDate", endDate.toISOString().split("T")[0]); - - setSelectedCampaign(selectedCampaign); + setValue("campaignName", detail.title); + setValue("description", detail.description); + setValue("fee", detail.rewardAmount.toString()); + setValue("sponsorProduct", 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)); + } + if (detail.contentTags?.categories?.length > 0) { + setValue("category", String(detail.contentTags.categories[0].id)); + } + if (detail.contentTags?.tones?.length > 0) { + setValue("tone", String(detail.contentTags.tones[0].id)); + } + if (detail.contentTags?.involvements?.length > 0) { + setValue("involvement", String(detail.contentTags.involvements[0].id)); + } + if (detail.contentTags?.usageRanges?.length > 0) { + setValue("usageScope", String(detail.contentTags.usageRanges[0].id)); + } } catch (error) { - console.error("모집중인 캠페인 조회 실패:", error); + console.error("캠페인 상세 조회 실패:", error); + toast.error("캠페인 정보를 불러오지 못했습니다"); } })(); } @@ -216,7 +220,7 @@ export default function CreateCampaignContent() { : proposalData?.brandId || 1; const campaignId = type === "existing" - ? (campaignIdParam ? Number(campaignIdParam) : (proposalData?.campaignId || selectedCampaign?.campaignId || null)) + ? (campaignIdParam ? Number(campaignIdParam) : (proposalData?.campaignId || null)) : null; const requestData = { @@ -246,7 +250,7 @@ export default function CreateCampaignContent() { }; // 선택된 캠페인 이름 가져오기 - const selectedCampaignName = selectedCampaign?.title; + const selectedCampaignName = formValues.campaignName; const title = type === "existing" && selectedCampaignName @@ -505,6 +509,7 @@ export default function CreateCampaignContent() { setIsConfirmDialogOpen(false)} onConfirm={handleConfirmSubmit} /> @@ -513,6 +518,7 @@ export default function CreateCampaignContent() { navigate("/business/calendar")} /> diff --git a/app/routes/matching/suggest/matching-suggest-content.tsx b/app/routes/matching/suggest/matching-suggest-content.tsx index fad5d9d4..812e6f2b 100644 --- a/app/routes/matching/suggest/matching-suggest-content.tsx +++ b/app/routes/matching/suggest/matching-suggest-content.tsx @@ -6,24 +6,21 @@ import { CheckIcon } from "../../auth/components/CheckIcon"; import NewSuggestIcon from "../../../assets/icon/new-suggest.svg"; import ExistSuggestIcon from "../../../assets/icon/exist-suggest.svg"; import LoadingSpinner from "../../../components/common/LoadingSpinner"; -import { useCampaignProposalStore, type RecruitingCampaignItem } from "../../../stores/campaign-proposal"; +import { useCampaignProposalStore } from "../../../stores/campaign-proposal"; import { getRecruitingCampaigns, + type RecruitingCampaign, } from "../api/matching"; -import { apiClient } from "../../../api/axios"; -import type { CampaignDetailApiResponse } from "../../campaign-detail/types"; import { toast } from "sonner"; import { useHideBottomTab } from "../../../hooks/useHideBottomTab"; export default function MatchingSuggestContent() { const navigate = useNavigate(); const proposalData = useCampaignProposalStore((state) => state.proposalData); - const storedRecruitingCampaigns = useCampaignProposalStore((state) => state.recruitingCampaigns); - const setProposalData = useCampaignProposalStore((state) => state.setProposalData); const [isSheetOpen, setIsSheetOpen] = useState(false); const [recruitingCampaigns, setRecruitingCampaigns] = useState< - RecruitingCampaignItem[] + RecruitingCampaign[] >([]); const [selectedCampaignId, setSelectedCampaignId] = useState( null, @@ -33,28 +30,14 @@ export default function MatchingSuggestContent() { useHideBottomTab(isSheetOpen); const handleNewCampaign = () => { - const brandId = proposalData?.brandId; - const domain = proposalData?.domain || "beauty"; - - if (brandId) { - navigate(`/matching/suggest/create?type=new&brandId=${brandId}&domain=${domain}`); - } else { - navigate("/matching/suggest/create?type=new"); - } + navigate("/matching/suggest/create?type=new"); }; const handleExistingCampaign = async () => { if (proposalData?.brandId) { + setIsLoading(true); setIsSheetOpen(true); - // store에 이미 저장된 모집중 캠페인이 있으면 재사용 - if (storedRecruitingCampaigns.length > 0) { - setRecruitingCampaigns(storedRecruitingCampaigns); - return; - } - - // 없으면 API 호출 - setIsLoading(true); try { const campaigns = await getRecruitingCampaigns(proposalData.brandId); setRecruitingCampaigns(campaigns); @@ -76,53 +59,21 @@ export default function MatchingSuggestContent() { setSelectedCampaignId(id); }; - const handleSheetSubmit = async () => { + const handleSheetSubmit = () => { if (!selectedCampaignId) { toast.error("캠페인을 선택해주세요"); return; } - if (!proposalData) return; - - // 선택한 캠페인의 상세 정보를 조회하여 proposalData에 매핑 - setIsLoading(true); - try { - const res = await apiClient.get( - `/api/v1/campaigns/${selectedCampaignId}`, - ); - - if (!res.data?.isSuccess) { - toast.error(res.data?.message || "캠페인 정보를 불러오지 못했어요."); - return; - } + const selectedCampaign = recruitingCampaigns.find( + (c) => c.campaignId === selectedCampaignId, + ); + if (!selectedCampaign || !proposalData) return; - const campaign = res.data.result; - - setProposalData({ - ...proposalData, - campaignId: selectedCampaignId, - campaignTitle: campaign.title, - campaignDescription: campaign.description, - rewardAmount: campaign.rewardAmount, - product: campaign.product, - startDate: campaign.startDate, - endDate: campaign.endDate, - contentTags: { - formats: campaign.contentTags?.formats, - categories: campaign.contentTags?.categories, - tones: campaign.contentTags?.tones, - involvements: campaign.contentTags?.involvements, - usageRanges: campaign.contentTags?.usageRanges, - }, - }); - - setIsSheetOpen(false); - navigate("/matching/suggest/create?type=existing"); - } catch { - toast.error("캠페인 정보를 불러오지 못했어요."); - } finally { - setIsLoading(false); - } + // 선택한 캠페인 정보와 함께 페이지 이동 + navigate( + `/matching/suggest/create?type=existing&brandId=${proposalData.brandId}&campaignId=${selectedCampaignId}&domain=${proposalData.domain}`, + ); }; const handleSheetClose = () => { @@ -203,13 +154,13 @@ export default function MatchingSuggestContent() { )} - {/* 선택 완료 버튼 (바닥 고정) */} -
+ {/* 선택 완료 버튼 */} +
From eeb142786631db1c7fd5da28e0f3668d01dfe548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=84=B8=EC=9C=A4?= <2ne1jenna@naver.com> Date: Thu, 12 Feb 2026 17:40:41 +0900 Subject: [PATCH 6/6] =?UTF-8?q?refactor:=20=ED=95=84=ED=84=B0=20UI=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routes/matching/brand/brand-content.tsx | 1 + app/routes/matching/campaign/campaign-content.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/app/routes/matching/brand/brand-content.tsx b/app/routes/matching/brand/brand-content.tsx index b30558eb..569b7da4 100644 --- a/app/routes/matching/brand/brand-content.tsx +++ b/app/routes/matching/brand/brand-content.tsx @@ -179,6 +179,7 @@ export default function BrandContent() { label={getFilterButtonLabel()} isActive={selectedTags.length > 0} onClick={() => { setFilterOpenTab("filter"); setIsFilterOpen(true); }} + className="bg-transparent border-core-1" />
diff --git a/app/routes/matching/campaign/campaign-content.tsx b/app/routes/matching/campaign/campaign-content.tsx index 2a8fcb61..83c4d380 100644 --- a/app/routes/matching/campaign/campaign-content.tsx +++ b/app/routes/matching/campaign/campaign-content.tsx @@ -187,6 +187,7 @@ export default function CampaignContent() { label={getFilterButtonLabel()} isActive={selectedTags.length > 0} onClick={() => setIsFilterOpen(true)} + className="bg-transparent border-core-1" />