From 6150063364704accae426665de6fdbd2c01b124f Mon Sep 17 00:00:00 2001 From: yoonyoungyang Date: Thu, 12 Feb 2026 05:57:26 +0900 Subject: [PATCH 01/15] =?UTF-8?q?=EC=BA=A0=ED=8E=98=EC=9D=B8=20UI=202?= =?UTF-8?q?=EC=B0=A8=20=EC=88=98=EC=A0=95(=ED=8C=A8=EB=94=A9,=EB=A7=88?= =?UTF-8?q?=EC=A7=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/OngoingCampaignSection.tsx | 20 ++++- .../campaign-detail/campaign-detail.tsx | 82 ++++++++++++++++++- 2 files changed, 95 insertions(+), 7 deletions(-) diff --git a/app/routes/brand-detail/components/OngoingCampaignSection.tsx b/app/routes/brand-detail/components/OngoingCampaignSection.tsx index 8dd4263..f76c60d 100644 --- a/app/routes/brand-detail/components/OngoingCampaignSection.tsx +++ b/app/routes/brand-detail/components/OngoingCampaignSection.tsx @@ -5,15 +5,20 @@ import type { BrandOngoingCampaign } from "../types"; type Props = { campaigns: BrandOngoingCampaign[]; onMore?: () => void; + onCampaignClick?: (c: BrandOngoingCampaign) => void; }; -export default function OngoingCampaignSection({ campaigns, onMore }: Props) { +export default function OngoingCampaignSection({ + campaigns, + onMore, + onCampaignClick, +}: Props) { const isEmpty = campaigns.length === 0; return ( -
+
{isEmpty ? ( -
+
캠페인 내역
@@ -50,7 +55,14 @@ export default function OngoingCampaignSection({ campaigns, onMore }: Props) {
{campaigns.map((c) => ( - + ))}
diff --git a/app/routes/campaign-detail/campaign-detail.tsx b/app/routes/campaign-detail/campaign-detail.tsx index 9db30e0..f2edafa 100644 --- a/app/routes/campaign-detail/campaign-detail.tsx +++ b/app/routes/campaign-detail/campaign-detail.tsx @@ -25,6 +25,8 @@ type Props = { campaignId: number; }; +type OngoingCampaign = NonNullable[number]; + const fmtMoney = (n?: number) => Number.isFinite(n) ? `${Number(n).toLocaleString()}원` : "-"; @@ -49,6 +51,37 @@ const toDdayText = (dday?: number) => { return `D-${dday}`; }; +const getNumberField = ( + obj: unknown, + keys: readonly string[], +): number | null => { + if (!obj || typeof obj !== "object") return null; + const rec = obj as Record; + for (const k of keys) { + const v = rec[k]; + if (typeof v === "number" && Number.isFinite(v) && v > 0) return v; + } + return null; +}; + +const getNestedNumberField = ( + obj: unknown, + outerKey: string, + innerKeys: readonly string[], +): number | null => { + if (!obj || typeof obj !== "object") return null; + const rec = obj as Record; + const nested = rec[outerKey]; + return getNumberField(nested, innerKeys); +}; + +const getCampaignIdFromOngoing = (c: OngoingCampaign): number | null => + getNumberField(c, ["campaignId", "campaign_id", "id"]); + +const getBrandIdFromOngoing = (c: OngoingCampaign): number | null => + getNumberField(c, ["brandId", "brand_id"]) ?? + getNestedNumberField(c, "brand", ["brandId", "id"]); + export default function CampaignDetailContent({ brandData, campaignId, @@ -85,6 +118,15 @@ export default function CampaignDetailContent({ setCampaign(res.data.result); setCampaignError(null); + + const liked = (() => { + const r: unknown = res.data.result; + if (!r || typeof r !== "object") return false; + const rec = r as Record; + return rec["isLiked"] === true; + })(); + + setIsCampaignLiked(liked); } catch { if (!alive) return; setCampaignError("캠페인 정보를 불러오지 못했어요."); @@ -139,6 +181,7 @@ export default function CampaignDetailContent({ } navigate(`/rooms/brand/${brandData.id}`); }; + const handleToggleHeart = async (next: boolean) => { const accessToken = tokenStorage.getAccessToken(); if (!accessToken) { @@ -237,6 +280,35 @@ export default function CampaignDetailContent({ navigate("/matching/apply"); }; + const goOngoingCampaignDetail = (c: OngoingCampaign) => { + const cid = getCampaignIdFromOngoing(c); + if (!cid) return; + + const domainParam = searchParams.get("domain"); + const domain = + domainParam === "fashion" || domainParam === "beauty" + ? domainParam + : "beauty"; + + const bidFromItem = getBrandIdFromOngoing(c); + const brandIdFromQuery = Number(searchParams.get("brandId")); + const fallbackBrandId = Number(brandData.id); + + const brandIdNum = + bidFromItem ?? + (Number.isFinite(brandIdFromQuery) && brandIdFromQuery > 0 + ? brandIdFromQuery + : Number.isFinite(fallbackBrandId) && fallbackBrandId > 0 + ? fallbackBrandId + : null); + + if (!brandIdNum) return; + + navigate( + `/campaign?brandId=${brandIdNum}&campaignId=${cid}&domain=${domain}`, + ); + }; + if (campaignError) { return (
@@ -315,7 +387,7 @@ export default function CampaignDetailContent({
-
+
상세 설명
{detailRows.map((row) => ( @@ -328,7 +400,7 @@ export default function CampaignDetailContent({
-
+
콘텐츠
@@ -384,7 +456,11 @@ export default function CampaignDetailContent({
- {}} /> + {}} + onCampaignClick={goOngoingCampaignDetail} + />
From f5bc081b55d0196060173fcb47ec4b7025ad1307 Mon Sep 17 00:00:00 2001 From: yoonyoungyang Date: Thu, 12 Feb 2026 07:05:09 +0900 Subject: [PATCH 02/15] =?UTF-8?q?=EC=BA=A0=ED=8E=98=EC=9D=B8=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=ED=8E=98=EC=9D=B4=EC=A7=80=20UI=20=EC=99=84=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../brand-detail/components/BrandHero.tsx | 2 +- .../components/OngoingCampaignSection.tsx | 97 +++++++++++-------- .../campaign-detail/campaign-detail.tsx | 94 +++++++++++++++--- .../components/CampaignActionBar.tsx | 10 +- app/routes/home/components/HeartButton.tsx | 7 +- 5 files changed, 153 insertions(+), 57 deletions(-) diff --git a/app/routes/brand-detail/components/BrandHero.tsx b/app/routes/brand-detail/components/BrandHero.tsx index 6876ed8..acd28b9 100644 --- a/app/routes/brand-detail/components/BrandHero.tsx +++ b/app/routes/brand-detail/components/BrandHero.tsx @@ -28,7 +28,7 @@ export default function BrandHero({ + ) : ( +
+ )} +
-
-
+ {isEmpty ? ( +
+
+
진행한 캠페인이 없어요 @@ -34,39 +63,31 @@ export default function OngoingCampaignSection({
) : ( - <> -
-
진행 중인 캠페인
- - {onMore ? ( - - ) : ( -
- )} -
- -
-
- {campaigns.map((c) => ( - - ))} -
+ onLikeToggle={onLikeToggle} + /> +
+ ))}
- +
)}
); diff --git a/app/routes/campaign-detail/campaign-detail.tsx b/app/routes/campaign-detail/campaign-detail.tsx index f2edafa..beef326 100644 --- a/app/routes/campaign-detail/campaign-detail.tsx +++ b/app/routes/campaign-detail/campaign-detail.tsx @@ -1,9 +1,9 @@ -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useState, useRef } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; import MiniLogo from "../../assets/logo/mini-logo.svg"; import BrandHero from "../brand-detail/components/BrandHero"; import BrandInfo from "../brand-detail/components/BrandInfo"; -import CampaingActionBar from "./components/CampaignActionBar"; +import CampaignActionBar from "./components/CampaignActionBar"; import OngoingCampaignSection from "../brand-detail/components/OngoingCampaignSection"; import { tokenStorage } from "../../lib/token"; @@ -97,6 +97,16 @@ export default function CampaignDetailContent({ const [campaign, setCampaign] = useState(null); const [campaignError, setCampaignError] = useState(null); + const [ongoingCampaigns, setOngoingCampaigns] = useState( + brandData.ongoingCampaigns ?? [], + ); + + useEffect(() => { + setOngoingCampaigns(brandData.ongoingCampaigns ?? []); + }, [brandData.ongoingCampaigns]); + + const ongoingLikeInFlight = useRef>(new Set()); + useEffect(() => { let alive = true; @@ -171,8 +181,6 @@ export default function CampaignDetailContent({ ]; }, [campaign]); - const ongoing = useMemo(() => brandData.ongoingCampaigns ?? [], [brandData]); - const handleChat = () => { const accessToken = tokenStorage.getAccessToken(); if (!accessToken) { @@ -207,6 +215,67 @@ export default function CampaignDetailContent({ } }; + const handleOngoingLikeToggle = async (id: string) => { + const accessToken = tokenStorage.getAccessToken(); + if (!accessToken) { + navigate("/auth/login"); + return; + } + + const clickedId = Number(id); + if (!Number.isFinite(clickedId) || clickedId <= 0) return; + + const currentItem = ongoingCampaigns.find((c) => { + const cid = getCampaignIdFromOngoing(c); + return cid === clickedId; + }); + if (!currentItem) return; + + const cid = getCampaignIdFromOngoing(currentItem); + if (!cid) return; + + if (ongoingLikeInFlight.current.has(cid)) return; + ongoingLikeInFlight.current.add(cid); + + const prev = + (currentItem as unknown as { isLiked?: boolean }).isLiked ?? false; + const next = !prev; + + setOngoingCampaigns((prevList) => + prevList.map((c) => { + const eachId = getCampaignIdFromOngoing(c); + if (eachId !== clickedId) return c; + return { ...(c as object), isLiked: next } as OngoingCampaign; + }), + ); + + try { + const serverStatus = await toggleCampaignLike(cid); + if (typeof serverStatus === "boolean") { + setOngoingCampaigns((prevList) => + prevList.map((c) => { + const eachId = getCampaignIdFromOngoing(c); + if (eachId !== clickedId) return c; + return { + ...(c as object), + isLiked: serverStatus, + } as OngoingCampaign; + }), + ); + } + } catch { + setOngoingCampaigns((prevList) => + prevList.map((c) => { + const eachId = getCampaignIdFromOngoing(c); + if (eachId !== clickedId) return c; + return { ...(c as object), isLiked: prev } as OngoingCampaign; + }), + ); + } finally { + ongoingLikeInFlight.current.delete(cid); + } + }; + const handleSuggest = () => { const accessToken = tokenStorage.getAccessToken(); if (!accessToken) { @@ -363,7 +432,7 @@ export default function CampaignDetailContent({ - -
+
{campaign.title}
-
+
상세 설명
-
+
{detailRows.map((row) => (
-
+
콘텐츠
-
+
{contentRows.map((row) => (
@@ -442,7 +511,7 @@ export default function CampaignDetailContent({
diff --git a/app/routes/campaign-detail/components/CampaignActionBar.tsx b/app/routes/campaign-detail/components/CampaignActionBar.tsx index 00bdc60..7b19f82 100644 --- a/app/routes/campaign-detail/components/CampaignActionBar.tsx +++ b/app/routes/campaign-detail/components/CampaignActionBar.tsx @@ -7,18 +7,18 @@ type Props = { onToggleHeart: (next: boolean) => void; }; -export default function BrandActionBar({ +export default function CampaignActionBar({ isHearted, onChat, onSuggest, onToggleHeart, }: Props) { return ( -
+
@@ -26,12 +26,12 @@ export default function BrandActionBar({ -
+
diff --git a/app/routes/home/components/HeartButton.tsx b/app/routes/home/components/HeartButton.tsx index 222f59e..f2a29b0 100644 --- a/app/routes/home/components/HeartButton.tsx +++ b/app/routes/home/components/HeartButton.tsx @@ -13,10 +13,15 @@ export default function HeartButton({ onChange?.(!pressed); }; + const handleClick = (e: React.MouseEvent) => { + e.stopPropagation(); + toggle(); + }; + return ( + )} + + + +
+ {displayPages.map((p) => { + const disabledPage = p > totalPages && !hasNext; + const active = p === page; + + return ( -
- - {!data.products || data.products.length === 0 ? ( -
-
- 협찬 가능한 제품이 없어요. -
-
- ) : ( -
-
-
- {data.products.map((p) => ( - - ))} -
-
-
- )} -
- - - -
-
캠페인 내역
+ ); + })} +
- {histories.length === 0 ? ( -
-
- 진행한 캠페인이 없어요 -
-
- ) : ( - <> -
- {pageItems.map((h) => ( - - ))} -
- -
- {page > GROUP_SIZE && ( - - )} - - - -
- {displayPages.map((p) => { - const disabledPage = p > totalPages && !hasNext; - const active = p === page; - - return ( - - ); - })} -
+ + + +
+ + )} +
- - - - - - )} - @@ -433,5 +615,5 @@ export default function BrandDetailContent({ data }: Props) { } function DividerBlock() { - return
; + return
; } diff --git a/app/routes/brand-detail/components/BrandActionBar.tsx b/app/routes/brand-detail/components/BrandActionBar.tsx index 7aabbfa..4b8a85d 100644 --- a/app/routes/brand-detail/components/BrandActionBar.tsx +++ b/app/routes/brand-detail/components/BrandActionBar.tsx @@ -14,11 +14,11 @@ export default function BrandActionBar({ onToggleHeart, }: Props) { return ( -
+
@@ -26,12 +26,12 @@ export default function BrandActionBar({ -
+
diff --git a/app/routes/brand-detail/components/BrandHero.tsx b/app/routes/brand-detail/components/BrandHero.tsx index acd28b9..6876ed8 100644 --- a/app/routes/brand-detail/components/BrandHero.tsx +++ b/app/routes/brand-detail/components/BrandHero.tsx @@ -28,7 +28,7 @@ export default function BrandHero({ + ) : null} +
+ + {isEmpty ? ( +
+
+
+
+
+ 협찬 가능한 제품이 없어요 +
+
+
+
+
+ ) : ( +
+
+
+ {products.map((p, idx) => ( + onProductClick?.(p.productId)} + /> + ))} +
+
+
+ )} + + ); +} diff --git a/app/routes/brand-detail/components/TagGroup.tsx b/app/routes/brand-detail/components/TagGroup.tsx index 300d616..56a0937 100644 --- a/app/routes/brand-detail/components/TagGroup.tsx +++ b/app/routes/brand-detail/components/TagGroup.tsx @@ -7,11 +7,11 @@ type Props = { export default function TagGroup({ label, chips }: Props) { return ( -
-
+
+
{label}
-
+
{chips.map((c) => ( {c.startsWith("#") ? c : `#${c}`} diff --git a/app/routes/brand-detail/types.ts b/app/routes/brand-detail/types.ts index de59e9c..b4e134f 100644 --- a/app/routes/brand-detail/types.ts +++ b/app/routes/brand-detail/types.ts @@ -17,9 +17,9 @@ export type BrandOngoingCampaign = { }; export type ProductMiniCardItem = { - id: string; - title: string; - imageUrl: string; + productId: number; + productName: string; + thumbnailImageUrl: string; }; export type HistoryRowItem = { @@ -99,6 +99,7 @@ export type BrandDetailData = { histories: HistoryRowItem[]; historiesHasNext?: boolean; }; + export type BrandCampaignStatus = "UPCOMING" | "RECRUITING" | "CLOSED"; export type BrandCampaignDto = { @@ -119,32 +120,36 @@ export type BrandCampaignsApiResponse = { }; }; -export type SponsorProductDto = { - id: number; - name: string; - thumbnailImageUrl: string; - totalCount: number; - currentCount: number; -}; - -export type SponsorProductsApiResponse = { - isSuccess: boolean; - code: string; - message: string; - result: SponsorProductDto[]; -}; - export type SponsorAvailableItem = { itemId: number; - availableType: string; // 백엔드 enum 나오면 union으로 좁히기 + availableType: string; availableQuantity: number; availableSize: number; - sizeUnit: string; // "ml", "g" 등 + sizeUnit?: string; + shippingType?: string; }; export type SponsorInfo = { items: SponsorAvailableItem[]; - shippingType: string; // 백엔드 enum 나오면 union으로 좁히기 + shippingType?: string; +}; + +export type SponsorProductsListDto = { + thumbnailImageUrl: string; + brandId: number; + brandName: string; + productId: number; + productName: string; + productImageUrls: string[]; + categories: string[]; + sponsorInfo: SponsorInfo; +}; + +export type SponsorProductsApiResponse = { + isSuccess: boolean; + code: string; + message: string; + result: SponsorProductsListDto[]; }; export type SponsorProductAction = { diff --git a/package.json b/package.json index abae70c..f11a919 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,6 @@ "axios": "^1.13.4", "clsx": "^2.1.1", "date-fns": "^4.1.0", - "firebase": "^12.9.0", "iconify": "^1.4.0", "isbot": "^5.1.17", "jwt-decode": "^4.0.0", @@ -46,7 +45,6 @@ "@tailwindcss/postcss": "^4.1.18", "@tailwindcss/vite": "^4.1.18", "@tanstack/router-plugin": "^1.157.16", - "@types/firebase": "^3.2.3", "@types/node": "^24.10.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8fea317..d18ba24 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,9 +41,6 @@ importers: date-fns: specifier: ^4.1.0 version: 4.1.0 - firebase: - specifier: ^12.9.0 - version: 12.9.0 iconify: specifier: ^1.4.0 version: 1.4.0 @@ -111,9 +108,6 @@ importers: '@tanstack/router-plugin': specifier: ^1.157.16 version: 1.157.16(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)) - '@types/firebase': - specifier: ^3.2.3 - version: 3.2.3 '@types/node': specifier: ^24.10.1 version: 24.10.9 @@ -897,225 +891,6 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@firebase/ai@2.8.0': - resolution: {integrity: sha512-grWYGFPsSo+pt+6CYeKR0kWnUfoLLS3xgWPvNrhAS5EPxl6xWq7+HjDZqX24yLneETyl45AVgDsTbVgxeWeRfg==} - engines: {node: '>=20.0.0'} - peerDependencies: - '@firebase/app': 0.x - '@firebase/app-types': 0.x - - '@firebase/analytics-compat@0.2.25': - resolution: {integrity: sha512-fdzoaG0BEKbqksRDhmf4JoyZf16Wosrl0Y7tbZtJyVDOOwziE0vrFjmZuTdviL0yhak+Nco6rMsUUbkbD+qb6Q==} - peerDependencies: - '@firebase/app-compat': 0.x - - '@firebase/analytics-types@0.8.3': - resolution: {integrity: sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg==} - - '@firebase/analytics@0.10.19': - resolution: {integrity: sha512-3wU676fh60gaiVYQEEXsbGS4HbF2XsiBphyvvqDbtC1U4/dO4coshbYktcCHq+HFaGIK07iHOh4pME0hEq1fcg==} - peerDependencies: - '@firebase/app': 0.x - - '@firebase/app-check-compat@0.4.0': - resolution: {integrity: sha512-UfK2Q8RJNjYM/8MFORltZRG9lJj11k0nW84rrffiKvcJxLf1jf6IEjCIkCamykHE73C6BwqhVfhIBs69GXQV0g==} - engines: {node: '>=20.0.0'} - peerDependencies: - '@firebase/app-compat': 0.x - - '@firebase/app-check-interop-types@0.3.3': - resolution: {integrity: sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==} - - '@firebase/app-check-types@0.5.3': - resolution: {integrity: sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng==} - - '@firebase/app-check@0.11.0': - resolution: {integrity: sha512-XAvALQayUMBJo58U/rxW02IhsesaxxfWVmVkauZvGEz3vOAjMEQnzFlyblqkc2iAaO82uJ2ZVyZv9XzPfxjJ6w==} - engines: {node: '>=20.0.0'} - peerDependencies: - '@firebase/app': 0.x - - '@firebase/app-compat@0.5.8': - resolution: {integrity: sha512-4De6SUZ36zozl9kh5rZSxKWULpgty27rMzZ6x+xkoo7+NWyhWyFdsdvhFsWhTw/9GGj0wXIcbTjwHYCUIUuHyg==} - engines: {node: '>=20.0.0'} - - '@firebase/app-types@0.9.3': - resolution: {integrity: sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==} - - '@firebase/app@0.14.8': - resolution: {integrity: sha512-WiE9uCGRLUnShdjb9iP20sA3ToWrBbNXr14/N5mow7Nls9dmKgfGaGX5cynLvrltxq2OrDLh1VDNaUgsnS/k/g==} - engines: {node: '>=20.0.0'} - - '@firebase/auth-compat@0.6.2': - resolution: {integrity: sha512-8UhCzF6pav9bw/eXA8Zy1QAKssPRYEYXaWagie1ewLTwHkXv6bKp/j6/IwzSYQP67sy/BMFXIFaCCsoXzFLr7A==} - engines: {node: '>=20.0.0'} - peerDependencies: - '@firebase/app-compat': 0.x - - '@firebase/auth-interop-types@0.2.4': - resolution: {integrity: sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==} - - '@firebase/auth-types@0.13.0': - resolution: {integrity: sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg==} - peerDependencies: - '@firebase/app-types': 0.x - '@firebase/util': 1.x - - '@firebase/auth@1.12.0': - resolution: {integrity: sha512-zkvLpsrxynWHk07qGrUDfCSqKf4AvfZGEqJ7mVCtYGjNNDbGE71k0Yn84rg8QEZu4hQw1BC0qDEHzpNVBcSVmA==} - engines: {node: '>=20.0.0'} - peerDependencies: - '@firebase/app': 0.x - '@react-native-async-storage/async-storage': ^2.2.0 - peerDependenciesMeta: - '@react-native-async-storage/async-storage': - optional: true - - '@firebase/component@0.7.0': - resolution: {integrity: sha512-wR9En2A+WESUHexjmRHkqtaVH94WLNKt6rmeqZhSLBybg4Wyf0Umk04SZsS6sBq4102ZsDBFwoqMqJYj2IoDSg==} - engines: {node: '>=20.0.0'} - - '@firebase/data-connect@0.3.12': - resolution: {integrity: sha512-baPddcoNLj/+vYo+HSJidJUdr5W4OkhT109c5qhR8T1dJoZcyJpkv/dFpYlw/VJ3dV66vI8GHQFrmAZw/xUS4g==} - peerDependencies: - '@firebase/app': 0.x - - '@firebase/database-compat@2.1.0': - resolution: {integrity: sha512-8nYc43RqxScsePVd1qe1xxvWNf0OBnbwHxmXJ7MHSuuTVYFO3eLyLW3PiCKJ9fHnmIz4p4LbieXwz+qtr9PZDg==} - engines: {node: '>=20.0.0'} - - '@firebase/database-types@1.0.16': - resolution: {integrity: sha512-xkQLQfU5De7+SPhEGAXFBnDryUWhhlFXelEg2YeZOQMCdoe7dL64DDAd77SQsR+6uoXIZY5MB4y/inCs4GTfcw==} - - '@firebase/database@1.1.0': - resolution: {integrity: sha512-gM6MJFae3pTyNLoc9VcJNuaUDej0ctdjn3cVtILo3D5lpp0dmUHHLFN/pUKe7ImyeB1KAvRlEYxvIHNF04Filg==} - engines: {node: '>=20.0.0'} - - '@firebase/firestore-compat@0.4.5': - resolution: {integrity: sha512-yVX1CkVvqBI4qbA56uZo42xFA4TNU0ICQ+9AFDvYq9U9Xu6iAx9lFDAk/tN+NGereQQXXCSnpISwc/oxsQqPLA==} - engines: {node: '>=20.0.0'} - peerDependencies: - '@firebase/app-compat': 0.x - - '@firebase/firestore-types@3.0.3': - resolution: {integrity: sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q==} - peerDependencies: - '@firebase/app-types': 0.x - '@firebase/util': 1.x - - '@firebase/firestore@4.11.0': - resolution: {integrity: sha512-Zb88s8rssBd0J2Tt+NUXMPt2sf+Dq7meatKiJf5t9oto1kZ8w9gK59Koe1uPVbaKfdgBp++N/z0I4G/HamyEhg==} - engines: {node: '>=20.0.0'} - peerDependencies: - '@firebase/app': 0.x - - '@firebase/functions-compat@0.4.1': - resolution: {integrity: sha512-AxxUBXKuPrWaVNQ8o1cG1GaCAtXT8a0eaTDfqgS5VsRYLAR0ALcfqDLwo/QyijZj1w8Qf8n3Qrfy/+Im245hOQ==} - engines: {node: '>=20.0.0'} - peerDependencies: - '@firebase/app-compat': 0.x - - '@firebase/functions-types@0.6.3': - resolution: {integrity: sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg==} - - '@firebase/functions@0.13.1': - resolution: {integrity: sha512-sUeWSb0rw5T+6wuV2o9XNmh9yHxjFI9zVGFnjFi+n7drTEWpl7ZTz1nROgGrSu472r+LAaj+2YaSicD4R8wfbw==} - engines: {node: '>=20.0.0'} - peerDependencies: - '@firebase/app': 0.x - - '@firebase/installations-compat@0.2.19': - resolution: {integrity: sha512-khfzIY3EI5LePePo7vT19/VEIH1E3iYsHknI/6ek9T8QCozAZshWT9CjlwOzZrKvTHMeNcbpo/VSOSIWDSjWdQ==} - peerDependencies: - '@firebase/app-compat': 0.x - - '@firebase/installations-types@0.5.3': - resolution: {integrity: sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA==} - peerDependencies: - '@firebase/app-types': 0.x - - '@firebase/installations@0.6.19': - resolution: {integrity: sha512-nGDmiwKLI1lerhwfwSHvMR9RZuIH5/8E3kgUWnVRqqL7kGVSktjLTWEMva7oh5yxQ3zXfIlIwJwMcaM5bK5j8Q==} - peerDependencies: - '@firebase/app': 0.x - - '@firebase/logger@0.5.0': - resolution: {integrity: sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g==} - engines: {node: '>=20.0.0'} - - '@firebase/messaging-compat@0.2.23': - resolution: {integrity: sha512-SN857v/kBUvlQ9X/UjAqBoQ2FEaL1ZozpnmL1ByTe57iXkmnVVFm9KqAsTfmf+OEwWI4kJJe9NObtN/w22lUgg==} - peerDependencies: - '@firebase/app-compat': 0.x - - '@firebase/messaging-interop-types@0.2.3': - resolution: {integrity: sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q==} - - '@firebase/messaging@0.12.23': - resolution: {integrity: sha512-cfuzv47XxqW4HH/OcR5rM+AlQd1xL/VhuaeW/wzMW1LFrsFcTn0GND/hak1vkQc2th8UisBcrkVcQAnOnKwYxg==} - peerDependencies: - '@firebase/app': 0.x - - '@firebase/performance-compat@0.2.22': - resolution: {integrity: sha512-xLKxaSAl/FVi10wDX/CHIYEUP13jXUjinL+UaNXT9ByIvxII5Ne5150mx6IgM8G6Q3V+sPiw9C8/kygkyHUVxg==} - peerDependencies: - '@firebase/app-compat': 0.x - - '@firebase/performance-types@0.2.3': - resolution: {integrity: sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ==} - - '@firebase/performance@0.7.9': - resolution: {integrity: sha512-UzybENl1EdM2I1sjYm74xGt/0JzRnU/0VmfMAKo2LSpHJzaj77FCLZXmYQ4oOuE+Pxtt8Wy2BVJEENiZkaZAzQ==} - peerDependencies: - '@firebase/app': 0.x - - '@firebase/remote-config-compat@0.2.21': - resolution: {integrity: sha512-9+lm0eUycxbu8GO25JfJe4s6R2xlDqlVt0CR6CvN9E6B4AFArEV4qfLoDVRgIEB7nHKwvH2nYRocPWfmjRQTnw==} - peerDependencies: - '@firebase/app-compat': 0.x - - '@firebase/remote-config-types@0.5.0': - resolution: {integrity: sha512-vI3bqLoF14L/GchtgayMiFpZJF+Ao3uR8WCde0XpYNkSokDpAKca2DxvcfeZv7lZUqkUwQPL2wD83d3vQ4vvrg==} - - '@firebase/remote-config@0.8.0': - resolution: {integrity: sha512-sJz7C2VACeE257Z/3kY9Ap2WXbFsgsDLfaGfZmmToKAK39ipXxFan+vzB9CSbF6mP7bzjyzEnqPcMXhAnYE6fQ==} - peerDependencies: - '@firebase/app': 0.x - - '@firebase/storage-compat@0.4.0': - resolution: {integrity: sha512-vDzhgGczr1OfcOy285YAPur5pWDEvD67w4thyeCUh6Ys0izN9fNYtA1MJERmNBfqjqu0lg0FM5GLbw0Il21M+g==} - engines: {node: '>=20.0.0'} - peerDependencies: - '@firebase/app-compat': 0.x - - '@firebase/storage-types@0.8.3': - resolution: {integrity: sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg==} - peerDependencies: - '@firebase/app-types': 0.x - '@firebase/util': 1.x - - '@firebase/storage@0.14.0': - resolution: {integrity: sha512-xWWbb15o6/pWEw8H01UQ1dC5U3rf8QTAzOChYyCpafV6Xki7KVp3Yaw2nSklUwHEziSWE9KoZJS7iYeyqWnYFA==} - engines: {node: '>=20.0.0'} - peerDependencies: - '@firebase/app': 0.x - - '@firebase/util@1.13.0': - resolution: {integrity: sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ==} - engines: {node: '>=20.0.0'} - - '@firebase/webchannel-wrapper@1.0.5': - resolution: {integrity: sha512-+uGNN7rkfn41HLO0vekTFhTxk61eKa8mTpRGLO0QSqlQdKvIoGAvLp3ppdVIWbTGYJWM6Kp0iN+PjMIOcnVqTw==} - - '@grpc/grpc-js@1.9.15': - resolution: {integrity: sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==} - engines: {node: ^8.13.0 || >=10.10.0} - - '@grpc/proto-loader@0.7.15': - resolution: {integrity: sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==} - engines: {node: '>=6'} - hasBin: true - '@hookform/resolvers@5.2.2': resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==} peerDependencies: @@ -1187,36 +962,6 @@ packages: '@mjackson/node-fetch-server@0.2.0': resolution: {integrity: sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng==} - '@protobufjs/aspromise@1.1.2': - resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} - - '@protobufjs/base64@1.1.2': - resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} - - '@protobufjs/codegen@2.0.4': - resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} - - '@protobufjs/eventemitter@1.1.0': - resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} - - '@protobufjs/fetch@1.1.0': - resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} - - '@protobufjs/float@1.0.2': - resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} - - '@protobufjs/inquire@1.1.0': - resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} - - '@protobufjs/path@1.1.2': - resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} - - '@protobufjs/pool@1.1.0': - resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} - - '@protobufjs/utf8@1.1.0': - resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} - '@react-router/dev@7.13.0': resolution: {integrity: sha512-0vRfTrS6wIXr9j0STu614Cv2ytMr21evnv1r+DXPv5cJ4q0V2x2kBAXC8TAqEXkpN5vdhbXBlbGQ821zwOfhvg==} engines: {node: '>=20.0.0'} @@ -1635,10 +1380,6 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/firebase@3.2.3': - resolution: {integrity: sha512-fId22ajZ7CkTMnm/FveFcEGb7Trv97JsUk1Dvc1vIFDiwpVSGKeTUyBOA2Tr2riXe53ZP526G9WDCrq3ulE7tw==} - deprecated: This is a stub types definition. firebase provides its own type definitions, so you do not need this installed. - '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1942,10 +1683,6 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -2333,9 +2070,6 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} - firebase@12.9.0: - resolution: {integrity: sha512-CwwTYoqZg6KxygPOaaJqIc4aoLvo0RCRrXoln9GoxLE8QyAwTydBaSLGVlR4WPcuOgN3OEL0tJLT1H4IU/dv7w==} - flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -2402,10 +2136,6 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -2853,9 +2583,6 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lodash.camelcase@4.3.0: - resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} - lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} @@ -2868,9 +2595,6 @@ packages: lodash@4.17.23: resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} - long@5.3.2: - resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} - lru-cache@11.2.5: resolution: {integrity: sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==} engines: {node: 20 || >=22} @@ -3098,10 +2822,6 @@ packages: resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} engines: {node: ^14.13.1 || >=16.0.0} - protobufjs@7.5.4: - resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} - engines: {node: '>=12.0.0'} - proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -3227,10 +2947,6 @@ packages: resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==} hasBin: true - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -3710,9 +3426,6 @@ packages: resolution: {integrity: sha512-3WFqGEgSXIyGhOmAFtlicJNMjEps8b1MG31NCA0/vOF9+nKMUW1ckhi9cnNHmf88Rzw5V+dwIwsm2C7X8k9aQg==} engines: {node: '>=12'} - web-vitals@4.2.4: - resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} - webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} @@ -3852,21 +3565,9 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} - yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -4729,336 +4430,6 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 - '@firebase/ai@2.8.0(@firebase/app-types@0.9.3)(@firebase/app@0.14.8)': - dependencies: - '@firebase/app': 0.14.8 - '@firebase/app-check-interop-types': 0.3.3 - '@firebase/app-types': 0.9.3 - '@firebase/component': 0.7.0 - '@firebase/logger': 0.5.0 - '@firebase/util': 1.13.0 - tslib: 2.8.1 - - '@firebase/analytics-compat@0.2.25(@firebase/app-compat@0.5.8)(@firebase/app@0.14.8)': - dependencies: - '@firebase/analytics': 0.10.19(@firebase/app@0.14.8) - '@firebase/analytics-types': 0.8.3 - '@firebase/app-compat': 0.5.8 - '@firebase/component': 0.7.0 - '@firebase/util': 1.13.0 - tslib: 2.8.1 - transitivePeerDependencies: - - '@firebase/app' - - '@firebase/analytics-types@0.8.3': {} - - '@firebase/analytics@0.10.19(@firebase/app@0.14.8)': - dependencies: - '@firebase/app': 0.14.8 - '@firebase/component': 0.7.0 - '@firebase/installations': 0.6.19(@firebase/app@0.14.8) - '@firebase/logger': 0.5.0 - '@firebase/util': 1.13.0 - tslib: 2.8.1 - - '@firebase/app-check-compat@0.4.0(@firebase/app-compat@0.5.8)(@firebase/app@0.14.8)': - dependencies: - '@firebase/app-check': 0.11.0(@firebase/app@0.14.8) - '@firebase/app-check-types': 0.5.3 - '@firebase/app-compat': 0.5.8 - '@firebase/component': 0.7.0 - '@firebase/logger': 0.5.0 - '@firebase/util': 1.13.0 - tslib: 2.8.1 - transitivePeerDependencies: - - '@firebase/app' - - '@firebase/app-check-interop-types@0.3.3': {} - - '@firebase/app-check-types@0.5.3': {} - - '@firebase/app-check@0.11.0(@firebase/app@0.14.8)': - dependencies: - '@firebase/app': 0.14.8 - '@firebase/component': 0.7.0 - '@firebase/logger': 0.5.0 - '@firebase/util': 1.13.0 - tslib: 2.8.1 - - '@firebase/app-compat@0.5.8': - dependencies: - '@firebase/app': 0.14.8 - '@firebase/component': 0.7.0 - '@firebase/logger': 0.5.0 - '@firebase/util': 1.13.0 - tslib: 2.8.1 - - '@firebase/app-types@0.9.3': {} - - '@firebase/app@0.14.8': - dependencies: - '@firebase/component': 0.7.0 - '@firebase/logger': 0.5.0 - '@firebase/util': 1.13.0 - idb: 7.1.1 - tslib: 2.8.1 - - '@firebase/auth-compat@0.6.2(@firebase/app-compat@0.5.8)(@firebase/app-types@0.9.3)(@firebase/app@0.14.8)': - dependencies: - '@firebase/app-compat': 0.5.8 - '@firebase/auth': 1.12.0(@firebase/app@0.14.8) - '@firebase/auth-types': 0.13.0(@firebase/app-types@0.9.3)(@firebase/util@1.13.0) - '@firebase/component': 0.7.0 - '@firebase/util': 1.13.0 - tslib: 2.8.1 - transitivePeerDependencies: - - '@firebase/app' - - '@firebase/app-types' - - '@react-native-async-storage/async-storage' - - '@firebase/auth-interop-types@0.2.4': {} - - '@firebase/auth-types@0.13.0(@firebase/app-types@0.9.3)(@firebase/util@1.13.0)': - dependencies: - '@firebase/app-types': 0.9.3 - '@firebase/util': 1.13.0 - - '@firebase/auth@1.12.0(@firebase/app@0.14.8)': - dependencies: - '@firebase/app': 0.14.8 - '@firebase/component': 0.7.0 - '@firebase/logger': 0.5.0 - '@firebase/util': 1.13.0 - tslib: 2.8.1 - - '@firebase/component@0.7.0': - dependencies: - '@firebase/util': 1.13.0 - tslib: 2.8.1 - - '@firebase/data-connect@0.3.12(@firebase/app@0.14.8)': - dependencies: - '@firebase/app': 0.14.8 - '@firebase/auth-interop-types': 0.2.4 - '@firebase/component': 0.7.0 - '@firebase/logger': 0.5.0 - '@firebase/util': 1.13.0 - tslib: 2.8.1 - - '@firebase/database-compat@2.1.0': - dependencies: - '@firebase/component': 0.7.0 - '@firebase/database': 1.1.0 - '@firebase/database-types': 1.0.16 - '@firebase/logger': 0.5.0 - '@firebase/util': 1.13.0 - tslib: 2.8.1 - - '@firebase/database-types@1.0.16': - dependencies: - '@firebase/app-types': 0.9.3 - '@firebase/util': 1.13.0 - - '@firebase/database@1.1.0': - dependencies: - '@firebase/app-check-interop-types': 0.3.3 - '@firebase/auth-interop-types': 0.2.4 - '@firebase/component': 0.7.0 - '@firebase/logger': 0.5.0 - '@firebase/util': 1.13.0 - faye-websocket: 0.11.4 - tslib: 2.8.1 - - '@firebase/firestore-compat@0.4.5(@firebase/app-compat@0.5.8)(@firebase/app-types@0.9.3)(@firebase/app@0.14.8)': - dependencies: - '@firebase/app-compat': 0.5.8 - '@firebase/component': 0.7.0 - '@firebase/firestore': 4.11.0(@firebase/app@0.14.8) - '@firebase/firestore-types': 3.0.3(@firebase/app-types@0.9.3)(@firebase/util@1.13.0) - '@firebase/util': 1.13.0 - tslib: 2.8.1 - transitivePeerDependencies: - - '@firebase/app' - - '@firebase/app-types' - - '@firebase/firestore-types@3.0.3(@firebase/app-types@0.9.3)(@firebase/util@1.13.0)': - dependencies: - '@firebase/app-types': 0.9.3 - '@firebase/util': 1.13.0 - - '@firebase/firestore@4.11.0(@firebase/app@0.14.8)': - dependencies: - '@firebase/app': 0.14.8 - '@firebase/component': 0.7.0 - '@firebase/logger': 0.5.0 - '@firebase/util': 1.13.0 - '@firebase/webchannel-wrapper': 1.0.5 - '@grpc/grpc-js': 1.9.15 - '@grpc/proto-loader': 0.7.15 - tslib: 2.8.1 - - '@firebase/functions-compat@0.4.1(@firebase/app-compat@0.5.8)(@firebase/app@0.14.8)': - dependencies: - '@firebase/app-compat': 0.5.8 - '@firebase/component': 0.7.0 - '@firebase/functions': 0.13.1(@firebase/app@0.14.8) - '@firebase/functions-types': 0.6.3 - '@firebase/util': 1.13.0 - tslib: 2.8.1 - transitivePeerDependencies: - - '@firebase/app' - - '@firebase/functions-types@0.6.3': {} - - '@firebase/functions@0.13.1(@firebase/app@0.14.8)': - dependencies: - '@firebase/app': 0.14.8 - '@firebase/app-check-interop-types': 0.3.3 - '@firebase/auth-interop-types': 0.2.4 - '@firebase/component': 0.7.0 - '@firebase/messaging-interop-types': 0.2.3 - '@firebase/util': 1.13.0 - tslib: 2.8.1 - - '@firebase/installations-compat@0.2.19(@firebase/app-compat@0.5.8)(@firebase/app-types@0.9.3)(@firebase/app@0.14.8)': - dependencies: - '@firebase/app-compat': 0.5.8 - '@firebase/component': 0.7.0 - '@firebase/installations': 0.6.19(@firebase/app@0.14.8) - '@firebase/installations-types': 0.5.3(@firebase/app-types@0.9.3) - '@firebase/util': 1.13.0 - tslib: 2.8.1 - transitivePeerDependencies: - - '@firebase/app' - - '@firebase/app-types' - - '@firebase/installations-types@0.5.3(@firebase/app-types@0.9.3)': - dependencies: - '@firebase/app-types': 0.9.3 - - '@firebase/installations@0.6.19(@firebase/app@0.14.8)': - dependencies: - '@firebase/app': 0.14.8 - '@firebase/component': 0.7.0 - '@firebase/util': 1.13.0 - idb: 7.1.1 - tslib: 2.8.1 - - '@firebase/logger@0.5.0': - dependencies: - tslib: 2.8.1 - - '@firebase/messaging-compat@0.2.23(@firebase/app-compat@0.5.8)(@firebase/app@0.14.8)': - dependencies: - '@firebase/app-compat': 0.5.8 - '@firebase/component': 0.7.0 - '@firebase/messaging': 0.12.23(@firebase/app@0.14.8) - '@firebase/util': 1.13.0 - tslib: 2.8.1 - transitivePeerDependencies: - - '@firebase/app' - - '@firebase/messaging-interop-types@0.2.3': {} - - '@firebase/messaging@0.12.23(@firebase/app@0.14.8)': - dependencies: - '@firebase/app': 0.14.8 - '@firebase/component': 0.7.0 - '@firebase/installations': 0.6.19(@firebase/app@0.14.8) - '@firebase/messaging-interop-types': 0.2.3 - '@firebase/util': 1.13.0 - idb: 7.1.1 - tslib: 2.8.1 - - '@firebase/performance-compat@0.2.22(@firebase/app-compat@0.5.8)(@firebase/app@0.14.8)': - dependencies: - '@firebase/app-compat': 0.5.8 - '@firebase/component': 0.7.0 - '@firebase/logger': 0.5.0 - '@firebase/performance': 0.7.9(@firebase/app@0.14.8) - '@firebase/performance-types': 0.2.3 - '@firebase/util': 1.13.0 - tslib: 2.8.1 - transitivePeerDependencies: - - '@firebase/app' - - '@firebase/performance-types@0.2.3': {} - - '@firebase/performance@0.7.9(@firebase/app@0.14.8)': - dependencies: - '@firebase/app': 0.14.8 - '@firebase/component': 0.7.0 - '@firebase/installations': 0.6.19(@firebase/app@0.14.8) - '@firebase/logger': 0.5.0 - '@firebase/util': 1.13.0 - tslib: 2.8.1 - web-vitals: 4.2.4 - - '@firebase/remote-config-compat@0.2.21(@firebase/app-compat@0.5.8)(@firebase/app@0.14.8)': - dependencies: - '@firebase/app-compat': 0.5.8 - '@firebase/component': 0.7.0 - '@firebase/logger': 0.5.0 - '@firebase/remote-config': 0.8.0(@firebase/app@0.14.8) - '@firebase/remote-config-types': 0.5.0 - '@firebase/util': 1.13.0 - tslib: 2.8.1 - transitivePeerDependencies: - - '@firebase/app' - - '@firebase/remote-config-types@0.5.0': {} - - '@firebase/remote-config@0.8.0(@firebase/app@0.14.8)': - dependencies: - '@firebase/app': 0.14.8 - '@firebase/component': 0.7.0 - '@firebase/installations': 0.6.19(@firebase/app@0.14.8) - '@firebase/logger': 0.5.0 - '@firebase/util': 1.13.0 - tslib: 2.8.1 - - '@firebase/storage-compat@0.4.0(@firebase/app-compat@0.5.8)(@firebase/app-types@0.9.3)(@firebase/app@0.14.8)': - dependencies: - '@firebase/app-compat': 0.5.8 - '@firebase/component': 0.7.0 - '@firebase/storage': 0.14.0(@firebase/app@0.14.8) - '@firebase/storage-types': 0.8.3(@firebase/app-types@0.9.3)(@firebase/util@1.13.0) - '@firebase/util': 1.13.0 - tslib: 2.8.1 - transitivePeerDependencies: - - '@firebase/app' - - '@firebase/app-types' - - '@firebase/storage-types@0.8.3(@firebase/app-types@0.9.3)(@firebase/util@1.13.0)': - dependencies: - '@firebase/app-types': 0.9.3 - '@firebase/util': 1.13.0 - - '@firebase/storage@0.14.0(@firebase/app@0.14.8)': - dependencies: - '@firebase/app': 0.14.8 - '@firebase/component': 0.7.0 - '@firebase/util': 1.13.0 - tslib: 2.8.1 - - '@firebase/util@1.13.0': - dependencies: - tslib: 2.8.1 - - '@firebase/webchannel-wrapper@1.0.5': {} - - '@grpc/grpc-js@1.9.15': - dependencies: - '@grpc/proto-loader': 0.7.15 - '@types/node': 24.10.9 - - '@grpc/proto-loader@0.7.15': - dependencies: - lodash.camelcase: 4.3.0 - long: 5.3.2 - protobufjs: 7.5.4 - yargs: 17.7.2 - '@hookform/resolvers@5.2.2(react-hook-form@7.71.1(react@19.2.4))': dependencies: '@standard-schema/utils': 0.3.0 @@ -5130,29 +4501,6 @@ snapshots: '@mjackson/node-fetch-server@0.2.0': {} - '@protobufjs/aspromise@1.1.2': {} - - '@protobufjs/base64@1.1.2': {} - - '@protobufjs/codegen@2.0.4': {} - - '@protobufjs/eventemitter@1.1.0': {} - - '@protobufjs/fetch@1.1.0': - dependencies: - '@protobufjs/aspromise': 1.1.2 - '@protobufjs/inquire': 1.1.0 - - '@protobufjs/float@1.0.2': {} - - '@protobufjs/inquire@1.1.0': {} - - '@protobufjs/path@1.1.2': {} - - '@protobufjs/pool@1.1.0': {} - - '@protobufjs/utf8@1.1.0': {} - '@react-router/dev@7.13.0(@react-router/serve@7.13.0(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3))(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0))': dependencies: '@babel/core': 7.28.6 @@ -5552,12 +4900,6 @@ snapshots: '@types/estree@1.0.8': {} - '@types/firebase@3.2.3': - dependencies: - firebase: 12.9.0 - transitivePeerDependencies: - - '@react-native-async-storage/async-storage' - '@types/json-schema@7.0.15': {} '@types/node@24.10.9': @@ -5927,12 +5269,6 @@ snapshots: dependencies: readdirp: 4.1.2 - cliui@8.0.1: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - clsx@2.1.1: {} color-convert@2.0.1: @@ -6407,39 +5743,6 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 - firebase@12.9.0: - dependencies: - '@firebase/ai': 2.8.0(@firebase/app-types@0.9.3)(@firebase/app@0.14.8) - '@firebase/analytics': 0.10.19(@firebase/app@0.14.8) - '@firebase/analytics-compat': 0.2.25(@firebase/app-compat@0.5.8)(@firebase/app@0.14.8) - '@firebase/app': 0.14.8 - '@firebase/app-check': 0.11.0(@firebase/app@0.14.8) - '@firebase/app-check-compat': 0.4.0(@firebase/app-compat@0.5.8)(@firebase/app@0.14.8) - '@firebase/app-compat': 0.5.8 - '@firebase/app-types': 0.9.3 - '@firebase/auth': 1.12.0(@firebase/app@0.14.8) - '@firebase/auth-compat': 0.6.2(@firebase/app-compat@0.5.8)(@firebase/app-types@0.9.3)(@firebase/app@0.14.8) - '@firebase/data-connect': 0.3.12(@firebase/app@0.14.8) - '@firebase/database': 1.1.0 - '@firebase/database-compat': 2.1.0 - '@firebase/firestore': 4.11.0(@firebase/app@0.14.8) - '@firebase/firestore-compat': 0.4.5(@firebase/app-compat@0.5.8)(@firebase/app-types@0.9.3)(@firebase/app@0.14.8) - '@firebase/functions': 0.13.1(@firebase/app@0.14.8) - '@firebase/functions-compat': 0.4.1(@firebase/app-compat@0.5.8)(@firebase/app@0.14.8) - '@firebase/installations': 0.6.19(@firebase/app@0.14.8) - '@firebase/installations-compat': 0.2.19(@firebase/app-compat@0.5.8)(@firebase/app-types@0.9.3)(@firebase/app@0.14.8) - '@firebase/messaging': 0.12.23(@firebase/app@0.14.8) - '@firebase/messaging-compat': 0.2.23(@firebase/app-compat@0.5.8)(@firebase/app@0.14.8) - '@firebase/performance': 0.7.9(@firebase/app@0.14.8) - '@firebase/performance-compat': 0.2.22(@firebase/app-compat@0.5.8)(@firebase/app@0.14.8) - '@firebase/remote-config': 0.8.0(@firebase/app@0.14.8) - '@firebase/remote-config-compat': 0.2.21(@firebase/app-compat@0.5.8)(@firebase/app@0.14.8) - '@firebase/storage': 0.14.0(@firebase/app@0.14.8) - '@firebase/storage-compat': 0.4.0(@firebase/app-compat@0.5.8)(@firebase/app-types@0.9.3)(@firebase/app@0.14.8) - '@firebase/util': 1.13.0 - transitivePeerDependencies: - - '@react-native-async-storage/async-storage' - flat-cache@4.0.1: dependencies: flatted: 3.3.3 @@ -6499,8 +5802,6 @@ snapshots: gensync@1.0.0-beta.2: {} - get-caller-file@2.0.5: {} - get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -6940,8 +6241,6 @@ snapshots: dependencies: p-locate: 5.0.0 - lodash.camelcase@4.3.0: {} - lodash.debounce@4.0.8: {} lodash.merge@4.6.2: {} @@ -6950,8 +6249,6 @@ snapshots: lodash@4.17.23: {} - long@5.3.2: {} - lru-cache@11.2.5: {} lru-cache@5.1.1: @@ -7137,21 +6434,6 @@ snapshots: pretty-bytes@6.1.1: {} - protobufjs@7.5.4: - dependencies: - '@protobufjs/aspromise': 1.1.2 - '@protobufjs/base64': 1.1.2 - '@protobufjs/codegen': 2.0.4 - '@protobufjs/eventemitter': 1.1.0 - '@protobufjs/fetch': 1.1.0 - '@protobufjs/float': 1.0.2 - '@protobufjs/inquire': 1.1.0 - '@protobufjs/path': 1.1.2 - '@protobufjs/pool': 1.1.0 - '@protobufjs/utf8': 1.1.0 - '@types/node': 24.10.9 - long: 5.3.2 - proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -7279,8 +6561,6 @@ snapshots: dependencies: jsesc: 3.1.0 - require-directory@2.1.1: {} - require-from-string@2.0.2: {} requires-port@1.0.0: {} @@ -7822,8 +7102,6 @@ snapshots: dependencies: xml-name-validator: 4.0.0 - web-vitals@4.2.4: {} - webidl-conversions@4.0.2: {} webidl-conversions@7.0.0: {} @@ -8038,22 +7316,8 @@ snapshots: xmlchars@2.2.0: {} - y18n@5.0.8: {} - yallist@3.1.1: {} - yargs-parser@21.1.1: {} - - yargs@17.7.2: - dependencies: - cliui: 8.0.1 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 - yocto-queue@0.1.0: {} zod-validation-error@4.0.2(zod@3.25.76): From 167ac428acb953d2bad08b1633208ae1b4fe621a Mon Sep 17 00:00:00 2001 From: Yoonyoung Yang Date: Thu, 12 Feb 2026 15:00:55 +0900 Subject: [PATCH 04/15] =?UTF-8?q?=ED=98=91=EC=B0=AC=20=EC=83=81=EC=84=B8?= =?UTF-8?q?=20=ED=8E=98=EC=9D=B4=EC=A7=80=20UI=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routes/brand-detail/api/api.ts | 76 +++--- .../components/ProductListCard.tsx | 24 +- .../detail/sponsorable-detail-content.tsx | 232 +++++++++++------- .../sponsorable/sponsorable-content.tsx | 114 +++++---- app/routes/brand-detail/types.ts | 13 +- 5 files changed, 260 insertions(+), 199 deletions(-) diff --git a/app/routes/brand-detail/api/api.ts b/app/routes/brand-detail/api/api.ts index 9a24331..7c63805 100644 --- a/app/routes/brand-detail/api/api.ts +++ b/app/routes/brand-detail/api/api.ts @@ -6,6 +6,9 @@ import type { BrandCampaignsApiResponse, SponsorProductDetailApiResponse, SponsorProductDetailResult, + SponsorProductsApiResponse, + SponsorProductsListDto, + ProductMiniCardItem, } from "../types"; type BeautyResponseDto = { @@ -60,21 +63,6 @@ type RecruitingCampaignsApiResponse = { result: { campaigns: RecruitingCampaignCardDto[] }; }; -export type SponsorProductListResponseDto = { - id: number; - name: string; - thumbnailImageUrl: string; - totalCount: number; - currentCount: number; -}; - -type SponsorProductListApiResponse = { - isSuccess: boolean; - code: string; - message: string; - result: SponsorProductListResponseDto[]; -}; - type BrandCampaignDto = BrandCampaignsApiResponse["result"]["campaigns"][number]; @@ -113,10 +101,7 @@ function inferDomain(item: BrandDetailItemDto): BrandDomain { return item.fashionResponse ? "fashion" : "beauty"; } -function buildCategories( - domain: BrandDomain, - item: BrandDetailItemDto, -): string[] { +function buildCategories(domain: BrandDomain, item: BrandDetailItemDto): string[] { if (domain === "fashion") { const cats = unique(stripHash(item.fashionResponse?.categories)); return cats.length ? cats : ["의류", "가방", "신발", "주얼리", "패션 소품"]; @@ -137,10 +122,8 @@ function buildTagSections( const brandStyle = unique(stripHash(f.brandStyle)); const groups: TagGroup[] = []; - if (brandType.length) - groups.push({ label: "브랜드 종류", chips: brandType }); - if (brandStyle.length) - groups.push({ label: "브랜드 스타일", chips: brandStyle }); + if (brandType.length) groups.push({ label: "브랜드 종류", chips: brandType }); + if (brandStyle.length) groups.push({ label: "브랜드 스타일", chips: brandStyle }); return groups.length ? [{ title: "의류 태그", groups }] : []; } @@ -158,25 +141,21 @@ function buildTagSections( if (cats.includes("스킨케어") || cats.includes("바디")) { const groups: TagGroup[] = []; if (skinType.length) groups.push({ label: "피부타입", chips: skinType }); - if (mainFunction.length) - groups.push({ label: "주요기능", chips: mainFunction }); + if (mainFunction.length) groups.push({ label: "주요기능", chips: mainFunction }); if (groups.length) sections.push({ title: "스킨케어 태그", groups }); } if (cats.includes("메이크업")) { const groups: TagGroup[] = []; - if (makeUpStyle.length) - groups.push({ label: "메이크업 스타일", chips: makeUpStyle }); + if (makeUpStyle.length) groups.push({ label: "메이크업 스타일", chips: makeUpStyle }); if (groups.length) sections.push({ title: "메이크업 태그", groups }); } if (!sections.length) { const groups: TagGroup[] = []; if (skinType.length) groups.push({ label: "피부타입", chips: skinType }); - if (mainFunction.length) - groups.push({ label: "주요기능", chips: mainFunction }); - if (makeUpStyle.length) - groups.push({ label: "메이크업 스타일", chips: makeUpStyle }); + if (mainFunction.length) groups.push({ label: "주요기능", chips: mainFunction }); + if (makeUpStyle.length) groups.push({ label: "메이크업 스타일", chips: makeUpStyle }); return groups.length ? [{ title: "태그", groups }] : []; } @@ -193,10 +172,10 @@ async function safeGet(p: Promise): Promise { export async function fetchSponsorProductList(params: { brandId: string; -}): Promise { +}): Promise { const { brandId } = params; - const res = await apiClient.get( + const res = await apiClient.get( `/api/v1/brands/${brandId}/sponsor-products`, ); @@ -254,7 +233,7 @@ export async function fetchBrandDetail(params: { : resolvedDomain; const productsRes = await safeGet( - apiClient.get( + apiClient.get( `/api/v1/brands/${brandId}/sponsor-products`, ), ); @@ -271,9 +250,8 @@ export async function fetchBrandDetail(params: { ), ); - const productList = productsRes?.data?.isSuccess - ? productsRes.data.result - : []; + const productList: SponsorProductsListDto[] = + productsRes?.data?.isSuccess ? productsRes.data.result ?? [] : []; const historyList = campaignsRes?.data?.isSuccess ? campaignsRes.data.result.campaigns @@ -286,6 +264,19 @@ export async function fetchBrandDetail(params: { ? recruitingRes.data.result.campaigns : []; + const products: ProductMiniCardItem[] = productList.map((p) => { + const fallback = + (p.productImageUrls ?? []).find(Boolean) ?? + (p.thumbnailImageUrl ?? "") ?? + ""; + + return { + productId: p.productId, + productName: p.productName ?? "", + thumbnailImageUrl: (p.thumbnailImageUrl ?? "") || fallback, + }; + }); + return { id: brandId, userId: item.userId, @@ -300,12 +291,17 @@ export async function fetchBrandDetail(params: { logoText: item.brandName, logoImageUrl: item.logoUrl, + homepageUrl: item.homepageUrl, + simpleIntro: item.simpleIntro, + hashtags: unique(stripHash(item.brandDescriptionTags)), description: item.simpleIntro ?? "", categories: buildCategories(safeDomain, item), tagSections: buildTagSections(safeDomain, item), + isLiked: item.brandIsLiked, + ongoingCampaigns: recruitingList.map((c) => ({ campaignId: c.campaignId, brandName: c.brandName, @@ -317,11 +313,7 @@ export async function fetchBrandDetail(params: { isLiked: false, })), - products: productList.map((p) => ({ - id: String(p.id), - title: p.name, - imageUrl: p.thumbnailImageUrl || "", - })), + products, histories: historyList.map((c) => { const { text, highlight } = formatHistoryDate(c); diff --git a/app/routes/brand-detail/components/ProductListCard.tsx b/app/routes/brand-detail/components/ProductListCard.tsx index 7e9323f..a9a6875 100644 --- a/app/routes/brand-detail/components/ProductListCard.tsx +++ b/app/routes/brand-detail/components/ProductListCard.tsx @@ -1,9 +1,9 @@ -interface Props { +type Props = { title: string; subtitle: string; imageUrl: string; onClick?: () => void; -} +}; export default function ProductListCard({ title, @@ -13,24 +13,18 @@ export default function ProductListCard({ }: Props) { return ( +
+ )}
)} - - {showButton && ( -
- -
- )}
); diff --git a/app/routes/brand-detail/sponsorable/sponsorable-content.tsx b/app/routes/brand-detail/sponsorable/sponsorable-content.tsx index b3b7eaa..e7e1693 100644 --- a/app/routes/brand-detail/sponsorable/sponsorable-content.tsx +++ b/app/routes/brand-detail/sponsorable/sponsorable-content.tsx @@ -6,11 +6,13 @@ import { LayoutContext } from "../../layout-context"; import { fetchSponsorProductList } from "../../brand-detail/api/api"; import LoadingSpinner from "../../../components/common/LoadingSpinner"; -type SponsorProduct = { - id: number; - title: string; - subtitle: string; +import type { SponsorProductsListDto } from "../../brand-detail/types"; + +type UiSponsorProduct = { + productId: number; + productName: string; imageUrl: string; + subtitle: string; }; type NavState = { @@ -18,20 +20,53 @@ type NavState = { brandName?: string; }; -type SponsorProductListResponseDto = { - id: number; - name: string; - thumbnailImageUrl: string; - totalCount: number; - currentCount: number; +const mapType = (t: string) => { + switch (t) { + case "FULL": + return "본품"; + case "SAMPLE": + return "샘플"; + default: + return t; + } +}; + +const buildSubtitle = (dto: SponsorProductsListDto) => { + const name = (dto.productName ?? "").toString(); + const items = dto.sponsorInfo?.items ?? []; + + if (!items.length) return ""; + + const parts = items + .map((it) => { + const type = mapType((it.availableType ?? "").toString().trim()); + + const qty = + typeof it.availableQuantity === "number" && it.availableQuantity > 0 + ? `${it.availableQuantity}개` + : ""; + + const size = + typeof it.availableSize === "number" && it.availableSize > 0 + ? `${it.availableSize}ml` + : ""; + + const left = [name, type, qty].filter(Boolean).join(" ").trim(); + const right = size.trim(); + + return [left, right].filter(Boolean).join(" / "); + }) + .filter(Boolean); + + return parts.join(" / "); }; -function toUiProducts(list: SponsorProductListResponseDto[]): SponsorProduct[] { - return list.map((p) => ({ - id: p.id, - title: p.name, - subtitle: `${p.currentCount}/${p.totalCount}개 남음`, - imageUrl: p.thumbnailImageUrl, +function toUiProducts(list: SponsorProductsListDto[]): UiSponsorProduct[] { + return (list ?? []).map((p) => ({ + productId: p.productId, + productName: p.productName ?? "", + imageUrl: p.thumbnailImageUrl ?? "", + subtitle: buildSubtitle(p), })); } @@ -48,7 +83,7 @@ export default function SponsorableContent() { (Number.isFinite(brandIdFromQuery) ? brandIdFromQuery : undefined); const [brandName] = useState(state.brandName ?? ""); - const [products, setProducts] = useState([]); + const [products, setProducts] = useState([]); const [loading, setLoading] = useState(false); const [errorText, setErrorText] = useState(null); @@ -59,7 +94,6 @@ export default function SponsorableContent() { [brandId], ); - // ✅ 헤더 숨김: “헤더만” 숨기고 화면(Outlet)은 계속 보여야 함 useEffect(() => { if (!layout?.setHideHeader) return; @@ -80,15 +114,14 @@ export default function SponsorableContent() { const list = await fetchSponsorProductList({ brandId: String(brandId), }); + if (cancelled) return; - setProducts(toUiProducts(list)); + setProducts(toUiProducts(list as SponsorProductsListDto[])); } catch (e: unknown) { if (cancelled) return; setErrorText( - e instanceof Error - ? e.message - : "협찬 가능 제품을 불러오지 못했어요.", + e instanceof Error ? e.message : "협찬 가능 제품을 불러오지 못했어요.", ); } finally { if (!cancelled) setLoading(false); @@ -100,47 +133,44 @@ export default function SponsorableContent() { }; }, [brandId, canFetch]); - const goDetail = (product: SponsorProduct) => { + const goDetail = (product: UiSponsorProduct) => { if (!brandId || !Number.isFinite(brandId) || brandId <= 0) return; - if (!Number.isFinite(product.id) || product.id <= 0) return; + if (!Number.isFinite(product.productId) || product.productId <= 0) return; navigate( - `/products/sponsorable/detail?brandId=${brandId}&productId=${product.id}`, + `/products/sponsorable/detail?brandId=${brandId}&productId=${product.productId}`, { state: { brandId, brandName, heroImageUrl: product.imageUrl, - productName: product.title, + productName: product.productName, }, }, ); }; return ( -
-
+
+
- navigate(-1)} - /> + navigate(-1)} />
-
+
{brandName || "브랜드"}
-
- 협찬 가능 리스트 +
+
+ 협찬 가능 리스트 +
-
-
- {loading && ( - - )} +
+
+ {loading && } {!loading && errorText && (
@@ -158,8 +188,8 @@ export default function SponsorableContent() { !errorText && products.map((p) => ( goDetail(p)} diff --git a/app/routes/brand-detail/types.ts b/app/routes/brand-detail/types.ts index b4e134f..e9fc2e5 100644 --- a/app/routes/brand-detail/types.ts +++ b/app/routes/brand-detail/types.ts @@ -120,18 +120,18 @@ export type BrandCampaignsApiResponse = { }; }; +export type SponsorShippingType = "CREATOR_PAY" | "BRAND_PAY" | string; + export type SponsorAvailableItem = { itemId: number; availableType: string; availableQuantity: number; availableSize: number; - sizeUnit?: string; - shippingType?: string; + shippingType: SponsorShippingType; }; export type SponsorInfo = { items: SponsorAvailableItem[]; - shippingType?: string; }; export type SponsorProductsListDto = { @@ -152,24 +152,17 @@ export type SponsorProductsApiResponse = { result: SponsorProductsListDto[]; }; -export type SponsorProductAction = { - canProposeCampaign: boolean; - proposeCampaignCtaText: string; -}; - export type SponsorProductDetailResult = { brandId: number; brandName: string; productId: number; productName: string; - productDescription: string; productImageUrls: string[]; categories: string[]; sponsorInfo: SponsorInfo; - action: SponsorProductAction; }; export type SponsorProductDetailApiResponse = { From 7bbfe3c44e69a2db3cb28d9748157009909d86fc Mon Sep 17 00:00:00 2001 From: Yoonyoung Yang Date: Thu, 12 Feb 2026 15:16:04 +0900 Subject: [PATCH 05/15] =?UTF-8?q?CI=20=EC=98=A4=EB=A5=98=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../brand-detail/brand-detail-content.tsx | 255 +++++++++--------- .../components/OngoingCampaignSection.tsx | 4 +- .../components/SponsorableProductSection.tsx | 2 +- 3 files changed, 136 insertions(+), 125 deletions(-) diff --git a/app/routes/brand-detail/brand-detail-content.tsx b/app/routes/brand-detail/brand-detail-content.tsx index 3f6f5c1..149062a 100644 --- a/app/routes/brand-detail/brand-detail-content.tsx +++ b/app/routes/brand-detail/brand-detail-content.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; import BrandHero from "./components/BrandHero"; @@ -172,29 +172,49 @@ export default function BrandDetailContent({ data }: Props) { const navigate = useNavigate(); const [searchParams] = useSearchParams(); const brandId = Number(searchParams.get("brandId")); + const validBrandId = Number.isFinite(brandId) && brandId > 0; + const setProposalData = useCampaignProposalStore( (state) => state.setProposalData, ); - const [ongoingCampaigns, setOngoingCampaigns] = useState( - data.ongoingCampaigns ?? [], + const baseOngoingCampaigns = useMemo( + () => data.ongoingCampaigns ?? [], + [data.ongoingCampaigns], ); - useEffect(() => { - setOngoingCampaigns(data.ongoingCampaigns ?? []); - }, [data.ongoingCampaigns]); + const [ongoingLikeOverrides, setOngoingLikeOverrides] = useState< + Record + >({}); + + const ongoingCampaigns = useMemo(() => { + if (baseOngoingCampaigns.length === 0) return []; + const overrides = ongoingLikeOverrides; + + return baseOngoingCampaigns.map((c) => { + const cid = getCampaignIdFromOngoing(c); + if (!cid) return c; + + if (Object.prototype.hasOwnProperty.call(overrides, cid)) { + return { ...(c as object), isLiked: overrides[cid] } as OngoingCampaign; + } + return c; + }); + }, [baseOngoingCampaigns, ongoingLikeOverrides]); const ongoingLikeInFlight = useRef>(new Set()); - const [sponsorProducts, setSponsorProducts] = useState( - [], + const [sponsorProductsRaw, setSponsorProductsRaw] = useState< + ProductMiniCardItem[] + >([]); + + const sponsorProducts = useMemo( + () => (validBrandId ? sponsorProductsRaw : []), + [validBrandId, sponsorProductsRaw], ); useEffect(() => { - if (!Number.isFinite(brandId) || brandId <= 0) { - setSponsorProducts([]); - return; - } + if (!validBrandId) return; let alive = true; @@ -207,7 +227,7 @@ export default function BrandDetailContent({ data }: Props) { if (!alive) return; if (!res.data?.isSuccess) { - setSponsorProducts([]); + setSponsorProductsRaw([]); return; } @@ -226,17 +246,17 @@ export default function BrandDetailContent({ data }: Props) { }, ); - setSponsorProducts(mapped); + setSponsorProductsRaw(mapped); } catch { if (!alive) return; - setSponsorProducts([]); + setSponsorProductsRaw([]); } })(); return () => { alive = false; }; - }, [brandId]); + }, [brandId, validBrandId]); const handleChat = () => { const accessToken = tokenStorage.getAccessToken(); @@ -244,7 +264,7 @@ export default function BrandDetailContent({ data }: Props) { navigate("/auth/login"); return; } - if (!Number.isFinite(brandId) || brandId <= 0) return; + if (!validBrandId) return; navigate(`/rooms/brand/${brandId}`); }; @@ -254,7 +274,7 @@ export default function BrandDetailContent({ data }: Props) { navigate("/auth/login"); return; } - if (!Number.isFinite(brandId) || brandId <= 0) return; + if (!validBrandId) return; const domain = searchParams.get("domain"); @@ -263,7 +283,7 @@ export default function BrandDetailContent({ data }: Props) { campaignId: 0, domain: domain || "beauty", brandName: data.name, - products: (sponsorProducts ?? []).map((p) => ({ + products: sponsorProducts.map((p) => ({ id: String(p.productId), name: p.productName, })), @@ -273,7 +293,7 @@ export default function BrandDetailContent({ data }: Props) { }; const handleGoSponsorableProducts = () => { - if (!Number.isFinite(brandId) || brandId <= 0) return; + if (!validBrandId) return; navigate(`/products/sponsorable?brandId=${brandId}`, { state: { @@ -285,7 +305,7 @@ export default function BrandDetailContent({ data }: Props) { }; const handleSponsorableProductClick = (productId: number) => { - if (!Number.isFinite(brandId) || brandId <= 0) return; + if (!validBrandId) return; if (!Number.isFinite(productId) || productId <= 0) return; navigate( @@ -300,7 +320,7 @@ export default function BrandDetailContent({ data }: Props) { }; const handleToggleHeart = async () => { - if (!Number.isFinite(brandId) || brandId <= 0) return; + if (!validBrandId) return; const prev = isHearted; const next = !prev; @@ -325,7 +345,7 @@ export default function BrandDetailContent({ data }: Props) { : "beauty"; const brandIdNum = - Number.isFinite(brandId) && brandId > 0 + validBrandId ? brandId : Number.isFinite(Number(data.id)) && Number(data.id) > 0 ? Number(data.id) @@ -364,13 +384,7 @@ export default function BrandDetailContent({ data }: Props) { (currentItem as unknown as { isLiked?: boolean }).isLiked ?? false; const next = !prev; - setOngoingCampaigns((prevList) => - prevList.map((c) => { - const eachId = getCampaignIdFromOngoing(c); - if (eachId !== clickedId) return c; - return { ...(c as object), isLiked: next } as OngoingCampaign; - }), - ); + setOngoingLikeOverrides((m) => ({ ...m, [cid]: next })); ongoingLikeInFlight.current.delete(cid); }; @@ -457,7 +471,7 @@ export default function BrandDetailContent({ data }: Props) { />
-
+
@@ -483,9 +497,7 @@ export default function BrandDetailContent({ data }: Props) {
) : null} -
+
{(sec.groups ?? []).map((g, gi) => (
-
캠페인 내역
- - {histories.length === 0 ? ( -
-
-
-
-
- 진행한 캠페인이 없어요 -
-
-
-
-
- ) : ( - <> -
- {pageItems.map((h) => ( - - ))} -
- -
- {page > GROUP_SIZE && ( - - )} - - - -
- {displayPages.map((p) => { - const disabledPage = p > totalPages && !hasNext; - const active = p === page; - - return ( - - ); - })} -
- - - - -
- - )} -
+
캠페인 내역
+ + {histories.length === 0 ? ( +
+
+
+
+
+ 진행한 캠페인이 없어요 +
+
+
+
+
+ ) : ( + <> +
+ {pageItems.map((h) => ( + + ))} +
+ +
+ {page > GROUP_SIZE && ( + + )} + + + +
+ {displayPages.map((p) => { + const disabledPage = p > totalPages && !hasNext; + const active = p === page; + + return ( + + ); + })} +
+ + + +
+ + )} +
@@ -615,5 +626,5 @@ export default function BrandDetailContent({ data }: Props) { } function DividerBlock() { - return
; + return
; } diff --git a/app/routes/brand-detail/components/OngoingCampaignSection.tsx b/app/routes/brand-detail/components/OngoingCampaignSection.tsx index b529ffa..e58f4c0 100644 --- a/app/routes/brand-detail/components/OngoingCampaignSection.tsx +++ b/app/routes/brand-detail/components/OngoingCampaignSection.tsx @@ -55,8 +55,8 @@ export default function OngoingCampaignSection({
-
- 진행한 캠페인이 없어요 +
+ 진행 중인 캠페인이 없어요
diff --git a/app/routes/brand-detail/components/SponsorableProductSection.tsx b/app/routes/brand-detail/components/SponsorableProductSection.tsx index 5c42b59..2502e1b 100644 --- a/app/routes/brand-detail/components/SponsorableProductSection.tsx +++ b/app/routes/brand-detail/components/SponsorableProductSection.tsx @@ -44,7 +44,7 @@ export default function SponsorableProductSection({
-
+
협찬 가능한 제품이 없어요
From 7a29c196a193a9590b73c7dff4c3f128268eafbb Mon Sep 17 00:00:00 2001 From: Yoonchulchung Date: Thu, 12 Feb 2026 13:59:19 +0900 Subject: [PATCH 06/15] =?UTF-8?q?fix:=20=EB=B0=B0=EB=84=88=20UI=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routes/home/components/BannerCarousel.tsx | 35 ++++++++++++++----- app/routes/home/components/CategoryTabs.tsx | 4 +-- app/routes/home/home-after-match.tsx | 2 +- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/app/routes/home/components/BannerCarousel.tsx b/app/routes/home/components/BannerCarousel.tsx index fb32f71..6c14f36 100644 --- a/app/routes/home/components/BannerCarousel.tsx +++ b/app/routes/home/components/BannerCarousel.tsx @@ -1,19 +1,38 @@ import { useEffect, useRef, useState, useCallback } from "react"; +import type { CategoryKey } from "../types"; import bannerBeauty from "../../../assets/home-banner/banner-beauty.svg"; import bannerFashion from "../../../assets/home-banner/banner-fashion.svg"; const INTERVAL = 3000; -export default function BannerCarousel() { - const banners = [ - { src: bannerBeauty, alt: "뷰티 배너" }, - { src: bannerFashion, alt: "패션 배너" }, - { src: null, alt: "준비중 배너" }, - ]; +interface BannerItem { + src: string | null; + alt: string; +} + +const beautyBanners: BannerItem[] = [ + { src: bannerBeauty, alt: "뷰티 배너 1" }, + { src: null, alt: "뷰티 배너 2" }, + { src: null, alt: "뷰티 배너 3" }, +]; + +const fashionBanners: BannerItem[] = [ + { src: bannerFashion, alt: "패션 배너 1" }, + { src: null, alt: "패션 배너 2" }, + { src: null, alt: "패션 배너 3" }, +]; + +export default function BannerCarousel({ category }: { category: CategoryKey }) { + const banners = category === "beauty" ? beautyBanners : fashionBanners; const [current, setCurrent] = useState(0); const timerRef = useRef | null>(null); + // 카테고리 변경 시 첫 번째 배너로 리셋 + useEffect(() => { + setCurrent(0); + }, [category]); + const start = useCallback(() => { timerRef.current = setInterval(() => { setCurrent((prev) => (prev + 1) % banners.length); @@ -40,7 +59,7 @@ export default function BannerCarousel() { style={{ transform: `translateX(-${current * 100}%)` }} > {banners.map((banner, i) => ( -
+
{banner.src ? (
); -} \ No newline at end of file +} diff --git a/app/routes/home/components/CategoryTabs.tsx b/app/routes/home/components/CategoryTabs.tsx index 6a01923..397c75b 100644 --- a/app/routes/home/components/CategoryTabs.tsx +++ b/app/routes/home/components/CategoryTabs.tsx @@ -14,7 +14,7 @@ export default function CategoryTabs({ ]; return ( -
+
{tabs.map((t) => { const active = t.key === value; @@ -37,7 +37,7 @@ export default function CategoryTabs({
-
+
- + From aef0a8cb325ed650735300cdf99938f926fdf91b Mon Sep 17 00:00:00 2001 From: Yoonchulchung Date: Thu, 12 Feb 2026 14:02:25 +0900 Subject: [PATCH 07/15] =?UTF-8?q?fix:=20lint=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routes/home/components/BannerCarousel.tsx | 5 ----- app/routes/home/home-after-match.tsx | 2 +- app/routes/home/index.tsx | 2 +- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/app/routes/home/components/BannerCarousel.tsx b/app/routes/home/components/BannerCarousel.tsx index 6c14f36..26e21d0 100644 --- a/app/routes/home/components/BannerCarousel.tsx +++ b/app/routes/home/components/BannerCarousel.tsx @@ -28,11 +28,6 @@ export default function BannerCarousel({ category }: { category: CategoryKey }) const [current, setCurrent] = useState(0); const timerRef = useRef | null>(null); - // 카테고리 변경 시 첫 번째 배너로 리셋 - useEffect(() => { - setCurrent(0); - }, [category]); - const start = useCallback(() => { timerRef.current = setInterval(() => { setCurrent((prev) => (prev + 1) % banners.length); diff --git a/app/routes/home/home-after-match.tsx b/app/routes/home/home-after-match.tsx index f2124e2..54b24ec 100644 --- a/app/routes/home/home-after-match.tsx +++ b/app/routes/home/home-after-match.tsx @@ -441,7 +441,7 @@ export default function HomeAfterMatchPage() { return (
- + diff --git a/app/routes/home/index.tsx b/app/routes/home/index.tsx index fab13cb..73ec63c 100644 --- a/app/routes/home/index.tsx +++ b/app/routes/home/index.tsx @@ -52,7 +52,7 @@ export default function HomeIndex() { return () => { isMounted = false; }; - }, [hasTokens, setMe]); + }, [hasTokens, setMe, me?.matchingTestDone]); // 토큰 없으면 비로그인 홈 if (!hasTokens) { From bb37b071c1c31d46735d83f5057c272f277c8aa0 Mon Sep 17 00:00:00 2001 From: Yoonchulchung Date: Thu, 12 Feb 2026 14:10:37 +0900 Subject: [PATCH 08/15] =?UTF-8?q?fix:=20=EB=82=B4=20=ED=8A=B9=EC=84=B1=20?= =?UTF-8?q?=ED=8C=9D=EC=97=85=EC=B0=BD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routes/mypage/components/profileCard/TraitModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routes/mypage/components/profileCard/TraitModal.tsx b/app/routes/mypage/components/profileCard/TraitModal.tsx index aadbdf3..7517cf1 100644 --- a/app/routes/mypage/components/profileCard/TraitModal.tsx +++ b/app/routes/mypage/components/profileCard/TraitModal.tsx @@ -100,7 +100,7 @@ export default function TraitModal({
{/* sections */} -
+
{trait.sections.map((section, i) => (
From 92e419a375469040e8000f3aa4c3bfc4e8026070 Mon Sep 17 00:00:00 2001 From: yerimi00 Date: Thu, 12 Feb 2026 13:38:54 +0900 Subject: [PATCH 09/15] =?UTF-8?q?feat=20:=20=EC=B1=84=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EC=97=AC=EB=8A=94=EC=A4=91=20ui=20=EB=A1=9C=ED=8B=B0=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routes/room/$brandId.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/routes/room/$brandId.tsx b/app/routes/room/$brandId.tsx index 33d4677..12f168e 100644 --- a/app/routes/room/$brandId.tsx +++ b/app/routes/room/$brandId.tsx @@ -3,6 +3,7 @@ import { useNavigate, useParams } from "react-router-dom"; import { tokenStorage } from "../../lib/token"; import { createOrGetDirectRoom } from "./api/rooms"; import NavigationHeader from "../../components/common/NavigateHeader"; +import { DotLottieReact } from "@lottiefiles/dotlottie-react"; export default function BrandEntry() { const navigate = useNavigate(); @@ -39,7 +40,14 @@ export default function BrandEntry() { return (
history.back()} /> -
채팅방을 여는 중...
+
+ +
); } From e42c57aef559ca93f2c7d51dce2c3abcfe86166b Mon Sep 17 00:00:00 2001 From: yerimi00 Date: Thu, 12 Feb 2026 14:13:23 +0900 Subject: [PATCH 10/15] =?UTF-8?q?feat=20:=20=EC=99=84=EB=A3=8C=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/common/SuccessModal.tsx | 49 +++++++++++++++++++ app/routes/mypage/edit/edit-content.tsx | 13 ++++- .../notification/notifications-content.tsx | 9 ++++ .../profileCard/profileCard-content.tsx | 9 ++-- 4 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 app/components/common/SuccessModal.tsx diff --git a/app/components/common/SuccessModal.tsx b/app/components/common/SuccessModal.tsx new file mode 100644 index 0000000..b4c0096 --- /dev/null +++ b/app/components/common/SuccessModal.tsx @@ -0,0 +1,49 @@ +import Modal from "./Modal"; +import Button from "./Button"; +import CheckCircleIcon from "../../assets/icon/icon-check-circle.svg"; + +type SuccessModalProps = { + isOpen: boolean; + onClose: () => void; + title: string; + description?: string; + buttonLabel?: string; +}; + +export default function SuccessModal({ + isOpen, + onClose, + title, + description, + buttonLabel = "완료하기", +}: SuccessModalProps) { + return ( + +
+
+ + +

{title}

+ + {description ? ( +

+ {description} +

+ ) : null} +
+ +
+ +
+
+
+ ); +} diff --git a/app/routes/mypage/edit/edit-content.tsx b/app/routes/mypage/edit/edit-content.tsx index 1f7a0c3..7cb5bad 100644 --- a/app/routes/mypage/edit/edit-content.tsx +++ b/app/routes/mypage/edit/edit-content.tsx @@ -6,6 +6,7 @@ import { useHideHeader } from "../../../hooks/useHideHeader"; import { useHideBottomTab } from "../../../hooks/useHideBottomTab"; import FilterBottomSheet from "../../../components/common/FilterBottomSheet"; import { axiosInstance } from "../../../api/axios"; +import SuccessModal from "../../../components/common/SuccessModal"; type DaumPostcodeData = { address?: string; @@ -77,7 +78,9 @@ export default function MyPageEdit() { const [isNickChecking, setIsNickChecking] = useState(false); const [isAddressSaving, setIsAddressSaving] = useState(false); const [isNickSaving, setIsNickSaving] = useState(false); - useHideBottomTab(isNickSheetOpen); + const [successMessage, setSuccessMessage] = useState(null); + const isSuccessOpen = Boolean(successMessage); + useHideBottomTab(isNickSheetOpen || isSuccessOpen); useEffect(() => { const fetchMyEditInfo = async () => { @@ -338,6 +341,7 @@ export default function MyPageEdit() { setOriginalNickname(nextNickname); setOriginalAddress(nextAddress); setOriginalDetailAddress(nextDetailAddress); + setSuccessMessage("주소 변경 완료"); } catch (error) { console.error("Failed to update address:", error); toast.error("주소 변경에 실패했습니다."); @@ -481,6 +485,7 @@ export default function MyPageEdit() { setOriginalAddress(nextAddress); setOriginalDetailAddress(nextDetailAddress); setIsNickSheetOpen(false); + setSuccessMessage("닉네임 변경 완료"); } catch (error) { console.error("Failed to update nickname:", error); } finally { @@ -492,6 +497,12 @@ export default function MyPageEdit() {
+ + setSuccessMessage(null)} + title={successMessage ?? ""} + />
); } diff --git a/app/routes/mypage/notification/notifications-content.tsx b/app/routes/mypage/notification/notifications-content.tsx index 964a1e3..4f26881 100644 --- a/app/routes/mypage/notification/notifications-content.tsx +++ b/app/routes/mypage/notification/notifications-content.tsx @@ -3,6 +3,7 @@ import { useNavigate } from "react-router"; import NavigationHeader from "../../../components/common/NavigateHeader"; import { useHideHeader } from "../../../hooks/useHideHeader"; import { axiosInstance } from "../../../api/axios"; +import SuccessModal from "../../../components/common/SuccessModal"; type NotificationSettingResponse = { marketingConsent: boolean; @@ -31,6 +32,7 @@ export default function MyPageNotifications() { const [appPush, setAppPush] = useState(true); const [emailPush, setEmailPush] = useState(false); const [isSaving, setIsSaving] = useState(false); + const [successMessage, setSuccessMessage] = useState(null); useEffect(() => { const fetchSetting = async () => { @@ -193,6 +195,7 @@ export default function MyPageNotifications() { if (!response.data.isSuccess) { throw new Error(response.data.message || "알림 설정 변경 실패"); } + setSuccessMessage("알림 설정 완료"); } catch (error) { console.error("Failed to update notification setting:", error); } finally { @@ -204,6 +207,12 @@ export default function MyPageNotifications() {
+ + setSuccessMessage(null)} + title={successMessage ?? ""} + />
); } diff --git a/app/routes/mypage/profileCard/profileCard-content.tsx b/app/routes/mypage/profileCard/profileCard-content.tsx index 204f722..42bdcef 100644 --- a/app/routes/mypage/profileCard/profileCard-content.tsx +++ b/app/routes/mypage/profileCard/profileCard-content.tsx @@ -157,10 +157,11 @@ export default function ProfileCard() { throw new Error(response.data.message || "프로필 이미지 변경 실패"); } - setProfileCard((prev) => - prev - ? { ...prev, profileImageUrl: uploaded.accessUrl } - : { profileImageUrl: uploaded.accessUrl }, + const profileRes = await axiosInstance.get( + "/api/v1/users/me/profile-card", + ); + setProfileCard( + profileRes.data?.isSuccess ? profileRes.data.result : null, ); } catch (error) { console.error("프로필 이미지 업로드 실패:", error); From 44168f98765042a89422d2aade8aef59ea4113e3 Mon Sep 17 00:00:00 2001 From: yerimi00 Date: Thu, 12 Feb 2026 14:15:58 +0900 Subject: [PATCH 11/15] =?UTF-8?q?fix=20:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=95=8C=EB=A6=BC=20=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B4=EB=84=88=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/notifications-content.tsx | 113 +++++++++--------- 1 file changed, 54 insertions(+), 59 deletions(-) diff --git a/app/routes/mypage/notification/notifications-content.tsx b/app/routes/mypage/notification/notifications-content.tsx index 4f26881..6c3305e 100644 --- a/app/routes/mypage/notification/notifications-content.tsx +++ b/app/routes/mypage/notification/notifications-content.tsx @@ -1,9 +1,8 @@ -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { useNavigate } from "react-router"; import NavigationHeader from "../../../components/common/NavigateHeader"; import { useHideHeader } from "../../../hooks/useHideHeader"; import { axiosInstance } from "../../../api/axios"; -import SuccessModal from "../../../components/common/SuccessModal"; type NotificationSettingResponse = { marketingConsent: boolean; @@ -32,7 +31,7 @@ export default function MyPageNotifications() { const [appPush, setAppPush] = useState(true); const [emailPush, setEmailPush] = useState(false); const [isSaving, setIsSaving] = useState(false); - const [successMessage, setSuccessMessage] = useState(null); + const initialSettingsRef = useRef(null); useEffect(() => { const fetchSetting = async () => { @@ -50,6 +49,11 @@ export default function MyPageNotifications() { setBenefitPush(Boolean(data.marketingConsent)); setAppPush(Boolean(data.appPushEnabled)); setEmailPush(Boolean(data.emailEnabled)); + initialSettingsRef.current = { + marketingConsent: Boolean(data.marketingConsent), + appPushEnabled: Boolean(data.appPushEnabled), + emailEnabled: Boolean(data.emailEnabled), + }; } catch (error) { console.error("Failed to load notification setting:", error); } @@ -58,14 +62,59 @@ export default function MyPageNotifications() { fetchSetting(); }, []); + const saveSettings = async () => { + const payload = { + marketingConsent: benefitPush, + appPushEnabled: appPush, + emailEnabled: emailPush, + }; + + const response = await axiosInstance.put( + "/api/v1/users/me/notification-settings", + payload, + ); + + if (!response.data.isSuccess) { + throw new Error(response.data.message || "알림 설정 변경 실패"); + } + + initialSettingsRef.current = payload; + }; + + const handleBack = async () => { + if (isSaving) return; + + const initial = initialSettingsRef.current; + const hasChanges = initial + ? initial.marketingConsent !== benefitPush || + initial.appPushEnabled !== appPush || + initial.emailEnabled !== emailPush + : false; + + if (!hasChanges) { + navigate(-1); + return; + } + + try { + setIsSaving(true); + await saveSettings(); + navigate(-1); + } catch (error) { + console.error("Failed to update notification setting:", error); + } finally { + setIsSaving(false); + } + }; + return (
- navigate(-1)} /> +
-
+
{/* 혜택 푸시 */}
@@ -158,61 +207,7 @@ export default function MyPageNotifications() {
-
- -
- - setSuccessMessage(null)} - title={successMessage ?? ""} - />
); } From 0f57a419cffdf6e7b7935eea0bd7724dc04235ea Mon Sep 17 00:00:00 2001 From: yeahsel Date: Thu, 12 Feb 2026 14:49:50 +0900 Subject: [PATCH 12/15] =?UTF-8?q?=EB=82=B4=20=EC=B0=9C,=20=EB=B9=84?= =?UTF-8?q?=EC=A6=88=EB=8B=88=EC=8A=A4=20UI=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business/calendar/calendar-content.tsx | 38 ++++++++++++++----- .../business/components/FilterBottomSheet.tsx | 8 ++-- .../business/components/MatchingCard.tsx | 8 ++-- .../components/MatchingTabSection.tsx | 2 +- .../business/components/SectionTitle.tsx | 2 +- .../proposal/received-proposal-content.tsx | 10 ++++- app/routes/chat/components/ChatListHeader.tsx | 4 +- .../chat/components/ChatSortFilterSheet.tsx | 6 +-- app/routes/mypage/likes/likes-content.tsx | 19 ++++------ 9 files changed, 60 insertions(+), 37 deletions(-) diff --git a/app/routes/business/calendar/calendar-content.tsx b/app/routes/business/calendar/calendar-content.tsx index 06e49fd..934959c 100644 --- a/app/routes/business/calendar/calendar-content.tsx +++ b/app/routes/business/calendar/calendar-content.tsx @@ -9,7 +9,6 @@ import CampaignCard from "../components/CampaignCard"; import SectionTitle from "../components/SectionTitle"; import MatchingCard from "../components/MatchingCard"; import MatchingTabSection from "../components/MatchingTabSection"; -import dropdownIcon from "../../../assets/arrow-down.svg"; import EmptyState from "../components/EmptyState"; import LoadingSpinner from "../../../components/common/LoadingSpinner"; @@ -27,6 +26,8 @@ export default function CalendarContent() { const [campaigns, setCampaigns] = useState([]); const [isLoading, setIsLoading] = useState(true); + const isFiltered = activeFilter !== "전체"; + useEffect(() => { const fetchAllCampaigns = async () => { try { @@ -144,7 +145,7 @@ export default function CalendarContent() {
diff --git a/app/routes/business/components/FilterBottomSheet.tsx b/app/routes/business/components/FilterBottomSheet.tsx index be80edc..5ae832f 100644 --- a/app/routes/business/components/FilterBottomSheet.tsx +++ b/app/routes/business/components/FilterBottomSheet.tsx @@ -49,8 +49,8 @@ export default function FilterBottomSheet({
{/* 필터 옵션 영역 */} -
-
+
+
{filterOptions.map((filter) => ( diff --git a/app/routes/business/components/MatchingCard.tsx b/app/routes/business/components/MatchingCard.tsx index bfa9e98..a83705d 100644 --- a/app/routes/business/components/MatchingCard.tsx +++ b/app/routes/business/components/MatchingCard.tsx @@ -69,10 +69,10 @@ export default function MatchingCard({
- + {brand} - + {status}
@@ -81,14 +81,14 @@ export default function MatchingCard({ {/* 제안 보기 / 지원 보기 / 거절 사유 보기 */} {/* 채팅 버튼 */} -
diff --git a/app/routes/business/components/MatchingTabSection.tsx b/app/routes/business/components/MatchingTabSection.tsx index b44cced..9d1c31f 100644 --- a/app/routes/business/components/MatchingTabSection.tsx +++ b/app/routes/business/components/MatchingTabSection.tsx @@ -16,7 +16,7 @@ function TabButton({ label, active, onClick }: { label: string; active: boolean; return ( @@ -90,7 +90,7 @@ function SortOptionButton({ return ( diff --git a/app/routes/mypage/likes/likes-content.tsx b/app/routes/mypage/likes/likes-content.tsx index 61063a0..c4cbe8f 100644 --- a/app/routes/mypage/likes/likes-content.tsx +++ b/app/routes/mypage/likes/likes-content.tsx @@ -416,7 +416,7 @@ export default function MyPageLikes() {
- 매칭률 {/*수정*/} + 매칭률 {" "} {brand.matchRate}% @@ -451,7 +451,7 @@ export default function MyPageLikes() { key={campaign.id} className="bg-white rounded-[10px] border border-[#E8E8FB] px-[10px] pt-[10px] pb-[6px] flex gap-4 items-start h-[120px]" > -
+
{campaign.logoUrl ? (
{" "} - {/*수정*/} - + {campaign.dday} - + {campaign.applicants}
-
+
{campaign.brand}
-
+
- 매칭률 {/*수정*/} + 매칭률 {" "} {campaign.matchRate}% @@ -491,7 +490,7 @@ export default function MyPageLikes() {
{" "} - {/*수정*/} {campaign.title}
{" "} - {/*수정*/} 원고료: {campaign.reward.toLocaleString()}원
From 07dd633d59fa3b8b5ccb4c59054ec17b06650707 Mon Sep 17 00:00:00 2001 From: Yoonyoung Yang Date: Thu, 12 Feb 2026 13:44:44 +0900 Subject: [PATCH 13/15] =?UTF-8?q?=EB=B8=8C=EB=9E=9C=EB=93=9C=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20UI=20=EC=99=84?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../brand-detail/brand-detail-content.tsx | 173 +++++++++--------- app/routes/brand-detail/types.ts | 7 + 2 files changed, 94 insertions(+), 86 deletions(-) diff --git a/app/routes/brand-detail/brand-detail-content.tsx b/app/routes/brand-detail/brand-detail-content.tsx index 149062a..4aa0686 100644 --- a/app/routes/brand-detail/brand-detail-content.tsx +++ b/app/routes/brand-detail/brand-detail-content.tsx @@ -532,93 +532,94 @@ export default function BrandDetailContent({ data }: Props) {
-
캠페인 내역
- - {histories.length === 0 ? ( -
-
-
-
-
- 진행한 캠페인이 없어요 -
-
-
-
-
- ) : ( - <> -
- {pageItems.map((h) => ( - - ))} -
- -
- {page > GROUP_SIZE && ( - - )} - - - -
- {displayPages.map((p) => { - const disabledPage = p > totalPages && !hasNext; - const active = p === page; - - return ( - - ); - })} -
+
캠페인 내역
+ + {histories.length === 0 ? ( +
+
+
+
+
+ 진행한 캠페인이 없어요 +
+
+
+
+
+ ) : ( + <> +
+ {pageItems.map((h) => ( + + ))} +
+ +
+ {page > GROUP_SIZE && ( + + )} + + + +
+ {displayPages.map((p) => { + const disabledPage = p > totalPages && !hasNext; + const active = p === page; + + return ( + + ); + })} +
+ + + + +
+ + )} +
- - - -
- - )} -
diff --git a/app/routes/brand-detail/types.ts b/app/routes/brand-detail/types.ts index e9fc2e5..e46908e 100644 --- a/app/routes/brand-detail/types.ts +++ b/app/routes/brand-detail/types.ts @@ -127,11 +127,13 @@ export type SponsorAvailableItem = { availableType: string; availableQuantity: number; availableSize: number; + sizeUnit?: string; shippingType: SponsorShippingType; }; export type SponsorInfo = { items: SponsorAvailableItem[]; + shippingType?: string; }; export type SponsorProductsListDto = { @@ -152,6 +154,11 @@ export type SponsorProductsApiResponse = { result: SponsorProductsListDto[]; }; +export type SponsorProductAction = { + canProposeCampaign: boolean; + proposeCampaignCtaText: string; +}; + export type SponsorProductDetailResult = { brandId: number; brandName: string; From 002a94cfd3a79fd0c757419db519b10159e7ccfa 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:37:43 +0900 Subject: [PATCH 14/15] =?UTF-8?q?fix:=20=EB=A6=B0=ED=8A=B8=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routes/brand-detail/api/api.ts | 3 +-- app/routes/brand-detail/brand-detail-content.tsx | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/routes/brand-detail/api/api.ts b/app/routes/brand-detail/api/api.ts index 7c63805..1281482 100644 --- a/app/routes/brand-detail/api/api.ts +++ b/app/routes/brand-detail/api/api.ts @@ -267,8 +267,7 @@ export async function fetchBrandDetail(params: { const products: ProductMiniCardItem[] = productList.map((p) => { const fallback = (p.productImageUrls ?? []).find(Boolean) ?? - (p.thumbnailImageUrl ?? "") ?? - ""; + (p.thumbnailImageUrl ?? ""); return { productId: p.productId, diff --git a/app/routes/brand-detail/brand-detail-content.tsx b/app/routes/brand-detail/brand-detail-content.tsx index 4aa0686..42e0c9c 100644 --- a/app/routes/brand-detail/brand-detail-content.tsx +++ b/app/routes/brand-detail/brand-detail-content.tsx @@ -235,8 +235,7 @@ export default function BrandDetailContent({ data }: Props) { (p) => { const fallback = (p.productImageUrls ?? []).find(Boolean) ?? - (p.thumbnailImageUrl ?? "") ?? - ""; + (p.thumbnailImageUrl ?? ""); return { productId: p.productId, From ced980a7803c90b6d0a37d8b3f0cac69017f411e Mon Sep 17 00:00:00 2001 From: yoonyoungyang <153082394+yoonyoungyang@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:26:53 +0900 Subject: [PATCH 15/15] Update MatchingTabSection.tsx --- .../components/MatchingTabSection.tsx | 101 ++++++++++-------- 1 file changed, 55 insertions(+), 46 deletions(-) diff --git a/app/routes/business/components/MatchingTabSection.tsx b/app/routes/business/components/MatchingTabSection.tsx index 9d1c31f..30b540a 100644 --- a/app/routes/business/components/MatchingTabSection.tsx +++ b/app/routes/business/components/MatchingTabSection.tsx @@ -1,25 +1,32 @@ -import { useState, useEffect } from "react"; -import { searchCollaborations, type CampaignCollaboration } from "../calendar/api/calendar"; +import { useCallback, useEffect, useState } from "react"; +import { searchCollaborations, type CampaignCollaboration } from "../calendar/api/calendar"; import searchIcon from "../../../assets/search2.svg"; -import closeIcon from "../../../assets/cancel.svg"; +import closeIcon from "../../../assets/cancel.svg"; import LoadingSpinner from "../../../components/common/LoadingSpinner"; interface Props { subTab: "sent" | "received" | "applied"; setSubTab: (tab: "sent" | "received" | "applied") => void; - receivedCount?: number; - keyword: string; + receivedCount?: number; + keyword: string; setKeyword: (keyword: string) => void; } -function TabButton({ label, active, onClick }: { label: string; active: boolean; onClick: () => void }) { +function TabButton({ + label, + active, + onClick, +}: { + label: string; + active: boolean; + onClick: () => void; +}) { return (
- {/* 로딩 표시 */} {isLoading && } - {/* 검색 결과 리스트 */}
{!isLoading && campaigns.length > 0 ? ( campaigns.map((item) => (
- {item.brandName} - {item.title} {/* 브랜드명과 제목 표시 */} + {item.brandName} - {item.title}
)) - ) : ( - !isLoading && keyword &&
검색 결과가 없습니다.
- )} + ) : !isLoading && keyword ? ( +
검색 결과가 없습니다.
+ ) : null}
); @@ -103,34 +118,28 @@ export default function MatchingTabSection({ subTab, setSubTab, receivedCount, k return (
- setSubTab("sent")} - /> - - - setSubTab("applied")} - /> + setSubTab("applied")} />
-