From bfec85b0d52bea46b0a6f444d2f09da9438722d8 Mon Sep 17 00:00:00 2001 From: yoonyoungyang Date: Sat, 31 Jan 2026 00:55:50 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A7=A4=EC=B9=AD=20=ED=9B=84=20=ED=99=88=20-?= =?UTF-8?q?=20=EB=B8=8C=EB=9E=9C=EB=93=9C=20=EC=83=81=EC=84=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=A0=9C=EC=9E=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routeTree.gen.ts | 100 +++ src/routes/_main/_home/brand/api.ts | 17 + .../_home/brand/brand-detail-content.tsx | 145 +++++ .../_home/brand/components/BrandActionBar.tsx | 39 ++ .../_home/brand/components/BrandHero.tsx | 37 ++ .../_home/brand/components/BrandInfo.tsx | 44 ++ .../_home/brand/components/HistoryRow.tsx | 33 + .../components/OngoingCampaignSection.tsx | 44 ++ .../_main/_home/brand/components/PillChip.tsx | 22 + .../brand/components/ProductMiniCard.tsx | 31 + .../_main/_home/brand/components/TagGroup.tsx | 23 + .../_home/brand/components/toCampaignItem.ts | 25 + src/routes/_main/_home/brand/index.tsx | 41 ++ src/routes/_main/_home/brand/mock.ts | 588 ++++++++++++++++++ src/routes/_main/_home/brand/query.ts | 11 + src/routes/_main/_home/brand/route.tsx | 9 + src/routes/_main/_home/brand/types.ts | 58 ++ src/routes/_main/_home/campaign/index.tsx | 20 + src/routes/_main/_home/campaign/route.tsx | 5 + .../_main/_home/components/BrandCard.tsx | 86 +-- .../_main/_home/components/CampaignCard.tsx | 57 +- .../_main/_home/components/HeartButton.tsx | 5 +- .../_main/_home/components/SectionHeader.tsx | 21 +- src/routes/_main/_home/home-after-match.tsx | 62 +- src/routes/_main/_home/types.ts | 13 +- 25 files changed, 1453 insertions(+), 83 deletions(-) create mode 100644 src/routes/_main/_home/brand/api.ts create mode 100644 src/routes/_main/_home/brand/brand-detail-content.tsx create mode 100644 src/routes/_main/_home/brand/components/BrandActionBar.tsx create mode 100644 src/routes/_main/_home/brand/components/BrandHero.tsx create mode 100644 src/routes/_main/_home/brand/components/BrandInfo.tsx create mode 100644 src/routes/_main/_home/brand/components/HistoryRow.tsx create mode 100644 src/routes/_main/_home/brand/components/OngoingCampaignSection.tsx create mode 100644 src/routes/_main/_home/brand/components/PillChip.tsx create mode 100644 src/routes/_main/_home/brand/components/ProductMiniCard.tsx create mode 100644 src/routes/_main/_home/brand/components/TagGroup.tsx create mode 100644 src/routes/_main/_home/brand/components/toCampaignItem.ts create mode 100644 src/routes/_main/_home/brand/index.tsx create mode 100644 src/routes/_main/_home/brand/mock.ts create mode 100644 src/routes/_main/_home/brand/query.ts create mode 100644 src/routes/_main/_home/brand/route.tsx create mode 100644 src/routes/_main/_home/brand/types.ts create mode 100644 src/routes/_main/_home/campaign/index.tsx create mode 100644 src/routes/_main/_home/campaign/route.tsx diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 740785c..5380619 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -22,7 +22,11 @@ import { Route as AuthSignupPurposeRouteRouteImport } from './routes/auth/signup import { Route as AuthSignupInfoMoreRouteRouteImport } from './routes/auth/signup/info-more/route' import { Route as AuthSignupInfoRouteRouteImport } from './routes/auth/signup/info/route' import { Route as MainMatchingTestMatchingResultRouteRouteImport } from './routes/_main/matching-test/matching-result/route' +import { Route as MainHomeCampaignRouteRouteImport } from './routes/_main/_home/campaign/route' +import { Route as MainHomeBrandRouteRouteImport } from './routes/_main/_home/brand/route' import { Route as MainBusinessCalendarRouteRouteImport } from './routes/_main/_business/calendar/route' +import { Route as MainHomeCampaignIndexRouteImport } from './routes/_main/_home/campaign/index' +import { Route as MainHomeBrandIndexRouteImport } from './routes/_main/_home/brand/index' import { Route as MainMatchingTestMatchingTestStep3RouteRouteImport } from './routes/_main/matching-test/matching-test/step3/route' import { Route as MainMatchingTestMatchingTestStep2RouteRouteImport } from './routes/_main/matching-test/matching-test/step2/route' import { Route as MainMatchingTestMatchingTestStep1RouteRouteImport } from './routes/_main/matching-test/matching-test/step1/route' @@ -92,12 +96,32 @@ const MainMatchingTestMatchingResultRouteRoute = path: '/matching-test/matching-result', getParentRoute: () => MainRoute, } as any) +const MainHomeCampaignRouteRoute = MainHomeCampaignRouteRouteImport.update({ + id: '/_home/campaign', + path: '/campaign', + getParentRoute: () => MainRoute, +} as any) +const MainHomeBrandRouteRoute = MainHomeBrandRouteRouteImport.update({ + id: '/_home/brand', + path: '/brand', + getParentRoute: () => MainRoute, +} as any) const MainBusinessCalendarRouteRoute = MainBusinessCalendarRouteRouteImport.update({ id: '/_business/calendar', path: '/calendar', getParentRoute: () => MainRoute, } as any) +const MainHomeCampaignIndexRoute = MainHomeCampaignIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => MainHomeCampaignRouteRoute, +} as any) +const MainHomeBrandIndexRoute = MainHomeBrandIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => MainHomeBrandRouteRoute, +} as any) const MainMatchingTestMatchingTestStep3RouteRoute = MainMatchingTestMatchingTestStep3RouteRouteImport.update({ id: '/matching-test/matching-test/step3', @@ -121,6 +145,8 @@ export interface FileRoutesByFullPath { '/chat': typeof MainChatRouteRouteWithChildren '/auth/login': typeof AuthLoginRouteRoute '/calendar': typeof MainBusinessCalendarRouteRoute + '/brand': typeof MainHomeBrandRouteRouteWithChildren + '/campaign': typeof MainHomeCampaignRouteRouteWithChildren '/matching-test/matching-result': typeof MainMatchingTestMatchingResultRouteRoute '/auth/signup/info': typeof AuthSignupInfoRouteRoute '/auth/signup/info-more': typeof AuthSignupInfoMoreRouteRoute @@ -134,6 +160,8 @@ export interface FileRoutesByFullPath { '/matching-test/matching-test/step1': typeof MainMatchingTestMatchingTestStep1RouteRoute '/matching-test/matching-test/step2': typeof MainMatchingTestMatchingTestStep2RouteRoute '/matching-test/matching-test/step3': typeof MainMatchingTestMatchingTestStep3RouteRoute + '/brand/': typeof MainHomeBrandIndexRoute + '/campaign/': typeof MainHomeCampaignIndexRoute } export interface FileRoutesByTo { '/chat': typeof MainChatRouteRouteWithChildren @@ -152,6 +180,8 @@ export interface FileRoutesByTo { '/matching-test/matching-test/step1': typeof MainMatchingTestMatchingTestStep1RouteRoute '/matching-test/matching-test/step2': typeof MainMatchingTestMatchingTestStep2RouteRoute '/matching-test/matching-test/step3': typeof MainMatchingTestMatchingTestStep3RouteRoute + '/brand': typeof MainHomeBrandIndexRoute + '/campaign': typeof MainHomeCampaignIndexRoute } export interface FileRoutesById { __root__: typeof rootRouteImport @@ -159,6 +189,8 @@ export interface FileRoutesById { '/_main/chat': typeof MainChatRouteRouteWithChildren '/auth/login': typeof AuthLoginRouteRoute '/_main/_business/calendar': typeof MainBusinessCalendarRouteRoute + '/_main/_home/brand': typeof MainHomeBrandRouteRouteWithChildren + '/_main/_home/campaign': typeof MainHomeCampaignRouteRouteWithChildren '/_main/matching-test/matching-result': typeof MainMatchingTestMatchingResultRouteRoute '/auth/signup/info': typeof AuthSignupInfoRouteRoute '/auth/signup/info-more': typeof AuthSignupInfoMoreRouteRoute @@ -172,6 +204,8 @@ export interface FileRoutesById { '/_main/matching-test/matching-test/step1': typeof MainMatchingTestMatchingTestStep1RouteRoute '/_main/matching-test/matching-test/step2': typeof MainMatchingTestMatchingTestStep2RouteRoute '/_main/matching-test/matching-test/step3': typeof MainMatchingTestMatchingTestStep3RouteRoute + '/_main/_home/brand/': typeof MainHomeBrandIndexRoute + '/_main/_home/campaign/': typeof MainHomeCampaignIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath @@ -179,6 +213,8 @@ export interface FileRouteTypes { | '/chat' | '/auth/login' | '/calendar' + | '/brand' + | '/campaign' | '/matching-test/matching-result' | '/auth/signup/info' | '/auth/signup/info-more' @@ -192,6 +228,8 @@ export interface FileRouteTypes { | '/matching-test/matching-test/step1' | '/matching-test/matching-test/step2' | '/matching-test/matching-test/step3' + | '/brand/' + | '/campaign/' fileRoutesByTo: FileRoutesByTo to: | '/chat' @@ -210,12 +248,16 @@ export interface FileRouteTypes { | '/matching-test/matching-test/step1' | '/matching-test/matching-test/step2' | '/matching-test/matching-test/step3' + | '/brand' + | '/campaign' id: | '__root__' | '/_main' | '/_main/chat' | '/auth/login' | '/_main/_business/calendar' + | '/_main/_home/brand' + | '/_main/_home/campaign' | '/_main/matching-test/matching-result' | '/auth/signup/info' | '/auth/signup/info-more' @@ -229,6 +271,8 @@ export interface FileRouteTypes { | '/_main/matching-test/matching-test/step1' | '/_main/matching-test/matching-test/step2' | '/_main/matching-test/matching-test/step3' + | '/_main/_home/brand/' + | '/_main/_home/campaign/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -335,6 +379,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof MainMatchingTestMatchingResultRouteRouteImport parentRoute: typeof MainRoute } + '/_main/_home/campaign': { + id: '/_main/_home/campaign' + path: '/campaign' + fullPath: '/campaign' + preLoaderRoute: typeof MainHomeCampaignRouteRouteImport + parentRoute: typeof MainRoute + } + '/_main/_home/brand': { + id: '/_main/_home/brand' + path: '/brand' + fullPath: '/brand' + preLoaderRoute: typeof MainHomeBrandRouteRouteImport + parentRoute: typeof MainRoute + } '/_main/_business/calendar': { id: '/_main/_business/calendar' path: '/calendar' @@ -342,6 +400,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof MainBusinessCalendarRouteRouteImport parentRoute: typeof MainRoute } + '/_main/_home/campaign/': { + id: '/_main/_home/campaign/' + path: '/' + fullPath: '/campaign/' + preLoaderRoute: typeof MainHomeCampaignIndexRouteImport + parentRoute: typeof MainHomeCampaignRouteRoute + } + '/_main/_home/brand/': { + id: '/_main/_home/brand/' + path: '/' + fullPath: '/brand/' + preLoaderRoute: typeof MainHomeBrandIndexRouteImport + parentRoute: typeof MainHomeBrandRouteRoute + } '/_main/matching-test/matching-test/step3': { id: '/_main/matching-test/matching-test/step3' path: '/matching-test/matching-test/step3' @@ -378,9 +450,35 @@ const MainChatRouteRouteWithChildren = MainChatRouteRoute._addFileChildren( MainChatRouteRouteChildren, ) +interface MainHomeBrandRouteRouteChildren { + MainHomeBrandIndexRoute: typeof MainHomeBrandIndexRoute +} + +const MainHomeBrandRouteRouteChildren: MainHomeBrandRouteRouteChildren = { + MainHomeBrandIndexRoute: MainHomeBrandIndexRoute, +} + +const MainHomeBrandRouteRouteWithChildren = + MainHomeBrandRouteRoute._addFileChildren(MainHomeBrandRouteRouteChildren) + +interface MainHomeCampaignRouteRouteChildren { + MainHomeCampaignIndexRoute: typeof MainHomeCampaignIndexRoute +} + +const MainHomeCampaignRouteRouteChildren: MainHomeCampaignRouteRouteChildren = { + MainHomeCampaignIndexRoute: MainHomeCampaignIndexRoute, +} + +const MainHomeCampaignRouteRouteWithChildren = + MainHomeCampaignRouteRoute._addFileChildren( + MainHomeCampaignRouteRouteChildren, + ) + interface MainRouteChildren { MainChatRouteRoute: typeof MainChatRouteRouteWithChildren MainBusinessCalendarRouteRoute: typeof MainBusinessCalendarRouteRoute + MainHomeBrandRouteRoute: typeof MainHomeBrandRouteRouteWithChildren + MainHomeCampaignRouteRoute: typeof MainHomeCampaignRouteRouteWithChildren MainMatchingTestMatchingResultRouteRoute: typeof MainMatchingTestMatchingResultRouteRoute MainHomePreRoute: typeof MainHomePreRoute MainHomeIndexRoute: typeof MainHomeIndexRoute @@ -392,6 +490,8 @@ interface MainRouteChildren { const MainRouteChildren: MainRouteChildren = { MainChatRouteRoute: MainChatRouteRouteWithChildren, MainBusinessCalendarRouteRoute: MainBusinessCalendarRouteRoute, + MainHomeBrandRouteRoute: MainHomeBrandRouteRouteWithChildren, + MainHomeCampaignRouteRoute: MainHomeCampaignRouteRouteWithChildren, MainMatchingTestMatchingResultRouteRoute: MainMatchingTestMatchingResultRouteRoute, MainHomePreRoute: MainHomePreRoute, diff --git a/src/routes/_main/_home/brand/api.ts b/src/routes/_main/_home/brand/api.ts new file mode 100644 index 0000000..3da6b11 --- /dev/null +++ b/src/routes/_main/_home/brand/api.ts @@ -0,0 +1,17 @@ +import type { BrandDomain, BrandDetailData } from "./types"; +import { BRAND_DETAIL_MOCK } from "./mock"; // 너 프로젝트에 맞는 mock import로 조정 + +export async function fetchBrandDetail(params: { + brandId: string; + domain?: BrandDomain; +}): Promise { + const { brandId } = params; + + const data = BRAND_DETAIL_MOCK[brandId]; + if (!data) { + // ✅ 여기서 바로 잡히면 home에서 넘어오는 id가 잘못된 것 + throw new Error(`Unknown brandId: ${brandId}`); + } + + return data; +} diff --git a/src/routes/_main/_home/brand/brand-detail-content.tsx b/src/routes/_main/_home/brand/brand-detail-content.tsx new file mode 100644 index 0000000..9d5b876 --- /dev/null +++ b/src/routes/_main/_home/brand/brand-detail-content.tsx @@ -0,0 +1,145 @@ +import BrandHero from "./components/BrandHero"; +import BrandInfo from "./components/BrandInfo"; +import BrandActionBar from "./components/BrandActionBar"; +import PillChip from "./components/PillChip"; +import TagGroup from "./components/TagGroup"; +import OngoingCampaignSection from "./components/OngoingCampaignSection"; +import ProductMiniCard from "./components/ProductMiniCard"; +import HistoryRow from "./components/HistoryRow"; + +import type { BrandDetailData } from "./types"; + +type Props = { + data: BrandDetailData; +}; + +export default function BrandDetailContent({ data }: Props) { + return ( +
+
+ + +
+ + + console.log("채팅하기")} + onSuggest={() => console.log("제안하기")} + onToggleHeart={() => console.log("하트")} + /> + + + + {/* 카테고리 */} +
+
카테고리
+
+ {data.categories.map((c) => ( + + {c} + + ))} +
+
+ +
+ + {/* 태그 섹션(뷰티/패션 공통 렌더링) */} +
+ {data.tagSections.map((sec, idx) => ( +
+
{sec.title}
+
+ {sec.groups.map((g) => ( + + ))} +
+
+ ))} +
+ + + + {/* 진행 중인 캠페인 */} + console.log("캠페인 더보기")} + /> + + + + {/* 협찬 가능 제품 */} +
+
+
협찬 가능 제품
+ +
+ +
+
+ {data.products.map((p) => ( + + ))} +
+
+
+ + + + {/* 캠페인 내역 */} +
+
캠페인 내역
+ +
+ {data.histories.map((h) => ( + + ))} + +
+
+
+ +
+
+
+
+
+
+
+ ); +} + +function DividerBlock() { + return ( +
+ ); +} diff --git a/src/routes/_main/_home/brand/components/BrandActionBar.tsx b/src/routes/_main/_home/brand/components/BrandActionBar.tsx new file mode 100644 index 0000000..19a7cb0 --- /dev/null +++ b/src/routes/_main/_home/brand/components/BrandActionBar.tsx @@ -0,0 +1,39 @@ +import HeartButton from "../../components/HeartButton"; + +type Props = { + isHearted: boolean; + onChat: () => void; + onSuggest: () => void; + onToggleHeart: (next: boolean) => void; +}; + +export default function BrandActionBar({ + isHearted, + onChat, + onSuggest, + onToggleHeart, +}: Props) { + return ( +
+ + + + +
+ +
+
+ ); +} diff --git a/src/routes/_main/_home/brand/components/BrandHero.tsx b/src/routes/_main/_home/brand/components/BrandHero.tsx new file mode 100644 index 0000000..0445567 --- /dev/null +++ b/src/routes/_main/_home/brand/components/BrandHero.tsx @@ -0,0 +1,37 @@ +import { useRouter } from "@tanstack/react-router"; + +type Props = { + heroImageUrl: string; + logoText: string; +}; + +export default function BrandHero({ heroImageUrl, logoText }: Props) { + const router = useRouter(); + + return ( + // ✅ overflow-hidden 제거 (로고가 잘리지 않게) +
+ {/* ✅ 이미지만 overflow 처리 */} +
+ +
+ + {/* back */} + + + {/* ✅ 로고: 배너 밖으로 내려와도 안 잘림 */} +
+
+ {logoText} +
+
+
+ ); +} diff --git a/src/routes/_main/_home/brand/components/BrandInfo.tsx b/src/routes/_main/_home/brand/components/BrandInfo.tsx new file mode 100644 index 0000000..4414edf --- /dev/null +++ b/src/routes/_main/_home/brand/components/BrandInfo.tsx @@ -0,0 +1,44 @@ +const PRIMARY = "#6666E5"; + +type Props = { + name: string; + matchRate: number; + hashtags: string[]; + description: string; +}; + +export default function BrandInfo({ + name, + matchRate, + hashtags, + description, +}: Props) { + return ( +
+
+
+ {name} +
+ + {/* ✅ 기존 한 줄 유지 + 숫자만 크게 */} +
+ 매칭률 + + {matchRate}% + +
+
+ +
+ {hashtags.join(" ")} +
+ +
+ {description} +
+
+ ); +} diff --git a/src/routes/_main/_home/brand/components/HistoryRow.tsx b/src/routes/_main/_home/brand/components/HistoryRow.tsx new file mode 100644 index 0000000..8137d9f --- /dev/null +++ b/src/routes/_main/_home/brand/components/HistoryRow.tsx @@ -0,0 +1,33 @@ +const PRIMARY = "#6666E5"; + +export type HistoryRowItem = { + id: string; + title: string; + rightText: string; + highlight?: boolean; +}; + +type Props = { + item: HistoryRowItem; +}; + +export default function HistoryRow({ item }: Props) { + return ( +
+ {/* 좌측 컬럼 */} +
+ {item.title} +
+ + {/* ✅ 우측 컬럼: 고정폭 박스 안에서 우측정렬 */} +
+ + {item.rightText} + +
+
+ ); +} diff --git a/src/routes/_main/_home/brand/components/OngoingCampaignSection.tsx b/src/routes/_main/_home/brand/components/OngoingCampaignSection.tsx new file mode 100644 index 0000000..78388a9 --- /dev/null +++ b/src/routes/_main/_home/brand/components/OngoingCampaignSection.tsx @@ -0,0 +1,44 @@ +import CampaignCard from "../../components/CampaignCard"; +import { toCampaignItem, type BrandOngoingCampaign } from "./toCampaignItem"; + +type Props = { + campaigns: BrandOngoingCampaign[]; + onMore?: () => void; +}; + +export default function OngoingCampaignSection({ campaigns, onMore }: Props) { + return ( +
+
+
진행 중인 캠페인
+ + {onMore ? ( + + ) : ( +
+ )} +
+ +
+
+ {campaigns.map((c) => ( + + ))} +
+
+
+ ); +} diff --git a/src/routes/_main/_home/brand/components/PillChip.tsx b/src/routes/_main/_home/brand/components/PillChip.tsx new file mode 100644 index 0000000..c11db73 --- /dev/null +++ b/src/routes/_main/_home/brand/components/PillChip.tsx @@ -0,0 +1,22 @@ +type Props = { + children: React.ReactNode; + variant?: "filled" | "outline"; +}; + +export default function PillChip({ children, variant = "outline" }: Props) { + if (variant === "filled") { + // 카테고리 칩(스크샷 느낌) + return ( + + {children} + + ); + } + + // ✅ 태그 칩(더 작게) + return ( + + {children} + + ); +} diff --git a/src/routes/_main/_home/brand/components/ProductMiniCard.tsx b/src/routes/_main/_home/brand/components/ProductMiniCard.tsx new file mode 100644 index 0000000..c91930b --- /dev/null +++ b/src/routes/_main/_home/brand/components/ProductMiniCard.tsx @@ -0,0 +1,31 @@ +export type ProductMiniCardItem = { + id: string; + title: string; + imageUrl: string; +}; + +type Props = { + item: ProductMiniCardItem; +}; + +function ellipsis10(text: string) { + return text.length > 10 ? `${text.slice(0, 10)}...` : text; +} + +export default function ProductMiniCard({ item }: Props) { + return ( +
+
+ {item.title} +
+ +
+ {ellipsis10(item.title)} +
+
+ ); +} diff --git a/src/routes/_main/_home/brand/components/TagGroup.tsx b/src/routes/_main/_home/brand/components/TagGroup.tsx new file mode 100644 index 0000000..1b93eb7 --- /dev/null +++ b/src/routes/_main/_home/brand/components/TagGroup.tsx @@ -0,0 +1,23 @@ +import PillChip from "./PillChip"; + +type Props = { + label: string; + chips: string[]; +}; + +export default function TagGroup({ label, chips }: Props) { + return ( +
+
+ {label} +
+
+ {chips.map((c) => ( + + {c} + + ))} +
+
+ ); +} diff --git a/src/routes/_main/_home/brand/components/toCampaignItem.ts b/src/routes/_main/_home/brand/components/toCampaignItem.ts new file mode 100644 index 0000000..042462b --- /dev/null +++ b/src/routes/_main/_home/brand/components/toCampaignItem.ts @@ -0,0 +1,25 @@ +import type { CampaignItem } from "../../types"; + +export type BrandOngoingCampaign = { + id: string; + brandName: string; + startAt: string; + ddayLabel: string; + matchRate: number; + descText: string; + rewardText: string; + isLiked: boolean; +}; + +export function toCampaignItem(src: BrandOngoingCampaign): CampaignItem { + return { + id: src.id, + brandName: src.brandName, + startAt: src.startAt, + ddayLabel: src.ddayLabel, + matchRate: src.matchRate, + descText: src.descText, + rewardText: src.rewardText, + isLiked: src.isLiked, + }; +} diff --git a/src/routes/_main/_home/brand/index.tsx b/src/routes/_main/_home/brand/index.tsx new file mode 100644 index 0000000..13d6b2f --- /dev/null +++ b/src/routes/_main/_home/brand/index.tsx @@ -0,0 +1,41 @@ +import { createFileRoute } from "@tanstack/react-router"; +import type { BrandDomain } from "./types"; +import { useBrandDetail } from "./query"; +import BrandDetailContent from "./brand-detail-content"; + +export const Route = createFileRoute("/_main/_home/brand/")({ + validateSearch: (search: Record) => { + const brandId = + typeof search.brandId === "string" && search.brandId.length > 0 + ? search.brandId + : "beplain"; + + const domain: BrandDomain | undefined = + search.domain === "beauty" || search.domain === "fashion" + ? (search.domain as BrandDomain) + : undefined; + + return { brandId, domain }; + }, + component: BrandDetailPage, +}); + +function BrandDetailPage() { + const { brandId, domain } = Route.useSearch(); + console.log("BRAND SEARCH", brandId, domain); + const { data, isLoading, isError } = useBrandDetail(brandId, domain); + + if (isLoading) { + return
로딩중…
; + } + + if (isError || !data) { + return ( +
+ 데이터를 불러오지 못했어요. +
+ ); + } + + return ; +} diff --git a/src/routes/_main/_home/brand/mock.ts b/src/routes/_main/_home/brand/mock.ts new file mode 100644 index 0000000..0d8fcbf --- /dev/null +++ b/src/routes/_main/_home/brand/mock.ts @@ -0,0 +1,588 @@ +import type { BrandDetailData } from "./types"; + +export const BRAND_DETAIL_MOCK: Record = { + // -------- beauty 3 -------- + beplain: { + id: "beplain", + domain: "beauty", + name: "비플레인", + matchRate: 98, + heroImageUrl: + "https://images.unsplash.com/photo-1522335789203-aabd1fc54bc9?auto=format&fit=crop&w=1200&q=80", + logoText: "beplain", + hashtags: ["#저자극", "#천연보습", "#민감성피부"], + description: "천연 유래 성분으로 민감 피부를 위한 저자극 스킨케어 브랜드", + categories: ["스킨케어", "메이크업"], + tagSections: [ + { + title: "스킨케어 태그", + groups: [ + { label: "피부타입", chips: ["건성", "지성", "복합성"] }, + { label: "주요 기능", chips: ["수분/보습", "진정"] }, + ], + }, + { + title: "메이크업 태그", + groups: [ + { label: "피부 타입", chips: ["건성", "민감성"] }, + { label: "메이크업 스타일", chips: ["내추럴", "글로우"] }, + ], + }, + ], + ongoingCampaigns: [ + { + id: "c1", + brandName: "beplain", + startAt: "7/10", + ddayLabel: "D-DAY", + matchRate: 98, + descText: "신제품 체험단 모집", + rewardText: "리워드 200,000원", + isLiked: false, + }, + { + id: "c2", + brandName: "beplain", + startAt: "5/10", + ddayLabel: "D-3", + matchRate: 98, + descText: "신제품 체험단 모집", + rewardText: "리워드 200,000원", + isLiked: true, + }, + { + id: "c3", + brandName: "beplain", + startAt: "4/1", + ddayLabel: "D-5", + matchRate: 98, + descText: "신제품 체험단 모집", + rewardText: "리워드 200,000원", + isLiked: false, + }, + ], + products: [ + { + id: "p1", + title: "녹두 약산성 클렌징폼", + imageUrl: + "https://images.unsplash.com/photo-1585232351009-aa87416fca90?auto=format&fit=crop&w=900&q=80", + }, + { + id: "p2", + title: "팔 콜라겐 팩투폼", + imageUrl: + "https://images.unsplash.com/photo-1611930022073-84f8f49f6f17?auto=format&fit=crop&w=900&q=80", + }, + { + id: "p3", + title: "레몬씨 글루타치온 톤업 크림", + imageUrl: + "https://images.unsplash.com/photo-1612810436541-336d6f2f1fd3?auto=format&fit=crop&w=900&q=80", + }, + ], + histories: [ + { + id: "h1", + title: "‘녹두 세럼’ 체험단 모집", + rightText: "1월 15일 진행예정", + highlight: true, + }, + { + id: "h2", + title: "‘녹두 토너’ 체험단 모집", + rightText: "1월 25일 진행예정", + highlight: true, + }, + { + id: "h3", + title: "‘레몬씨 글루타치온 톤업 크림’", + rightText: "12/15/24 완료", + }, + { + id: "h4", + title: "‘녹두 약산성 클렌징젤’ 체험단 모집", + rightText: "8/15/24 완료", + }, + ], + }, + + isntree: { + id: "isntree", + domain: "beauty", + name: "이즈앤트리", + matchRate: 98, + heroImageUrl: + "https://images.unsplash.com/photo-1611930022073-84f8f49f6f17?auto=format&fit=crop&w=1200&q=80", + logoText: "Isntree", + hashtags: ["#클린뷰티", "#저자극", "#성분 중심"], + description: "자연 유래 성분으로 피부의 힘을 키우는 비건 스킨케어 브랜드", + categories: ["스킨케어"], + tagSections: [ + { + title: "스킨케어 태그", + groups: [ + { label: "피부타입", chips: ["건성", "지성"] }, + { label: "주요 기능", chips: ["수분/보습", "트러블"] }, + ], + }, + ], + ongoingCampaigns: [ + { + id: "c1", + brandName: "Isntree", + startAt: "8/10", + ddayLabel: "D-DAY", + matchRate: 98, + descText: "어니언 뉴페어리 라인…", + rewardText: "리워드 200,000원", + isLiked: false, + }, + { + id: "c2", + brandName: "Isntree", + startAt: "6/10", + ddayLabel: "D-3", + matchRate: 98, + descText: "초저분자 히알루론…", + rewardText: "리워드 100,000원", + isLiked: true, + }, + { + id: "c3", + brandName: "Isntree", + startAt: "3/1", + ddayLabel: "D-5", + matchRate: 98, + descText: "하이알루론산 토너…", + rewardText: "리워드 200,000원", + isLiked: false, + }, + ], + products: [ + { + id: "p1", + title: "어니언 뉴페어리 젤", + imageUrl: + "https://images.unsplash.com/photo-1585232351009-aa87416fca90?auto=format&fit=crop&w=900&q=80", + }, + { + id: "p2", + title: "초저분자 히알루론", + imageUrl: + "https://images.unsplash.com/photo-1522335789203-aabd1fc54bc9?auto=format&fit=crop&w=900&q=80", + }, + { + id: "p3", + title: "하이알루론산 토너", + imageUrl: + "https://images.unsplash.com/photo-1612810436541-336d6f2f1fd3?auto=format&fit=crop&w=900&q=80", + }, + ], + histories: [ + { + id: "h1", + title: "‘어니언 뉴페어리 세럼’ 체험단…", + rightText: "1월 20일 진행예정", + highlight: true, + }, + { + id: "h2", + title: "‘어니언 뉴페어리 세럼’ 리뷰…", + rightText: "1월 25일 진행예정", + highlight: true, + }, + { + id: "h3", + title: "‘초저분자 히알루론 크림’…", + rightText: "12/15/24 완료", + }, + { + id: "h4", + title: "‘하이알루론산 토너’ 체험단…", + rightText: "8/15/24 완료", + }, + ], + }, + + roundlab: { + id: "roundlab", + domain: "beauty", + name: "라운드랩", + matchRate: 98, + heroImageUrl: + "https://images.unsplash.com/photo-1522335789203-aabd1fc54bc9?auto=format&fit=crop&w=1200&q=80", + logoText: "ROUND LAB", + hashtags: ["#정착템", "#저자극", "#심플한감성"], + description: "정직한 자연 성분으로 안심하고 쓸 수 있는 클린 뷰티 브랜드", + categories: ["스킨케어"], + tagSections: [ + { + title: "스킨케어 태그", + groups: [ + { label: "피부타입", chips: ["건성", "민감성"] }, + { label: "주요 기능", chips: ["수분/보습", "미백"] }, + ], + }, + ], + ongoingCampaigns: [ + { + id: "c1", + brandName: "ROUND LAB", + startAt: "9/10", + ddayLabel: "D-DAY", + matchRate: 98, + descText: "베스트 라인 체험단", + rewardText: "리워드 100,000원", + isLiked: false, + }, + { + id: "c2", + brandName: "ROUND LAB", + startAt: "4/5", + ddayLabel: "D-3", + matchRate: 98, + descText: "진정 라인 체험단", + rewardText: "리워드 100,000원", + isLiked: true, + }, + { + id: "c3", + brandName: "ROUND LAB", + startAt: "4/1", + ddayLabel: "D-5", + matchRate: 98, + descText: "수분 라인 체험단", + rewardText: "리워드 100,000원", + isLiked: false, + }, + ], + products: [ + { + id: "p1", + title: "비타 나이아신", + imageUrl: + "https://images.unsplash.com/photo-1585232351009-aa87416fca90?auto=format&fit=crop&w=900&q=80", + }, + { + id: "p2", + title: "자작나무 수분", + imageUrl: + "https://images.unsplash.com/photo-1611930022073-84f8f49f6f17?auto=format&fit=crop&w=900&q=80", + }, + { + id: "p3", + title: "1025 독도 토너", + imageUrl: + "https://images.unsplash.com/photo-1612810436541-336d6f2f1fd3?auto=format&fit=crop&w=900&q=80", + }, + ], + histories: [ + { + id: "h1", + title: "‘비타 나이아신 글로우’ 체험단…", + rightText: "1월 25일 진행예정", + highlight: true, + }, + { + id: "h2", + title: "‘자작나무 수분’ 더블…", + rightText: "1월 25일 진행예정", + highlight: true, + }, + { + id: "h3", + title: "‘1025 독도 토너’ 체험단…", + rightText: "12/15/24 완료", + }, + { + id: "h4", + title: "‘1025 독도 토너’ 체험단…", + rightText: "8/15/24 완료", + }, + ], + }, + + // -------- fashion 3 -------- + graceu: { + id: "graceu", + domain: "fashion", + name: "그레이스유", + matchRate: 98, + heroImageUrl: + "https://images.unsplash.com/photo-1520975958225-2b9d35f2f6f3?auto=format&fit=crop&w=1200&q=80", + logoText: "GRACE U", + hashtags: ["#데일리스", "#클래식", "#우아함"], + description: "전체를 실루엣과 고급 소재로 완성하는 미니멀 여성복 브랜드", + categories: ["의류"], + tagSections: [ + { + title: "의류 태그", + groups: [ + { label: "브랜드 종류", chips: ["디자이너 브랜드"] }, + { label: "브랜드 스타일", chips: ["페미닌", "미니멀"] }, + ], + }, + ], + ongoingCampaigns: [ + { + id: "c1", + brandName: "GRACE U", + startAt: "10/10", + ddayLabel: "D-DAY", + matchRate: 98, + descText: "가을 신상 체험단", + rewardText: "리워드 300,000원", + isLiked: false, + }, + { + id: "c2", + brandName: "GRACE U", + startAt: "4/5", + ddayLabel: "D-3", + matchRate: 98, + descText: "니트 라인 체험단", + rewardText: "리워드 300,000원", + isLiked: true, + }, + { + id: "c3", + brandName: "GRACE U", + startAt: "8/1", + ddayLabel: "D-5", + matchRate: 98, + descText: "원피스 라인 체험단", + rewardText: "리워드 300,000원", + isLiked: false, + }, + ], + products: [ + { + id: "p1", + title: "Lucy Tie Jacket", + imageUrl: + "https://images.unsplash.com/photo-1520975958225-2b9d35f2f6f3?auto=format&fit=crop&w=900&q=80", + }, + { + id: "p2", + title: "Lavina Knit Cardigan", + imageUrl: + "https://images.unsplash.com/photo-1520975867597-0df1b0d1f24f?auto=format&fit=crop&w=900&q=80", + }, + { + id: "p3", + title: "Anais Off-shoulder", + imageUrl: + "https://images.unsplash.com/photo-1520975682071-4f3f909cc053?auto=format&fit=crop&w=900&q=80", + }, + ], + histories: [ + { + id: "h1", + title: "Lucy Tie Cardigan 리뷰…", + rightText: "1월 15일 진행예정", + highlight: true, + }, + { + id: "h2", + title: "Lucy Tie Shirt 리뷰 테스트…", + rightText: "2월 25일 진행예정", + highlight: true, + }, + { + id: "h3", + title: "Lavina Knit Shirt 체험단…", + rightText: "12/15/24 완료", + }, + { + id: "h4", + title: "Anais Knit Skirt 체험단…", + rightText: "10/15/24 완료", + }, + ], + }, + + thetis: { + id: "thetis", + domain: "fashion", + name: "더티스", + matchRate: 88, + heroImageUrl: + "https://images.unsplash.com/photo-1485968579580-b6d095142e6e?auto=format&fit=crop&w=1200&q=80", + logoText: "TheTis", + hashtags: ["#러블리", "#트렌디", "#더티스_중심"], + description: "유니크한 디자이너 스토리로 무드를 담은 감각적인 패션 브랜드", + categories: ["의류"], + tagSections: [ + { + title: "의류 태그", + groups: [ + { label: "브랜드 종류", chips: ["디자이너 브랜드"] }, + { label: "브랜드 스타일", chips: ["페미닌", "러블리"] }, + ], + }, + ], + ongoingCampaigns: [ + { + id: "c1", + brandName: "TheTis", + startAt: "9/10", + ddayLabel: "D-DAY", + matchRate: 88, + descText: "TEDDY HOOD 체험단", + rewardText: "리워드 250,000원", + isLiked: false, + }, + { + id: "c2", + brandName: "TheTis", + startAt: "7/10", + ddayLabel: "D-3", + matchRate: 88, + descText: "ANGEL CABLE 체험단", + rewardText: "리워드 250,000원", + isLiked: true, + }, + { + id: "c3", + brandName: "TheTis", + startAt: "6/1", + ddayLabel: "D-5", + matchRate: 88, + descText: "BOUQUET 체험단", + rewardText: "리워드 250,000원", + isLiked: false, + }, + ], + products: [ + { + id: "p1", + title: "TEDDY HOOD…", + imageUrl: + "https://images.unsplash.com/photo-1485968579580-b6d095142e6e?auto=format&fit=crop&w=900&q=80", + }, + { + id: "p2", + title: "ANGEL CABL…", + imageUrl: + "https://images.unsplash.com/photo-1520975867597-0df1b0d1f24f?auto=format&fit=crop&w=900&q=80", + }, + { + id: "p3", + title: "BOUQUET L…", + imageUrl: + "https://images.unsplash.com/photo-1520975682071-4f3f909cc053?auto=format&fit=crop&w=900&q=80", + }, + ], + histories: [ + { + id: "h1", + title: "‘TEDDY HOOD FUR COA…", + rightText: "1월 15일 진행예정", + highlight: true, + }, + { + id: "h2", + title: "‘ANGEL CABLE SKIRT’ 체…", + rightText: "2월 25일 진행예정", + highlight: true, + }, + { id: "h3", title: "‘BOUQUET LAYERED SHI…", rightText: "12/15/24 완료" }, + { id: "h4", title: "‘BOUQUET LAYERED PA…", rightText: "8/15/24 완료" }, + ], + }, + + glowny: { + id: "glowny", + domain: "fashion", + name: "글로니", + matchRate: 78, + heroImageUrl: + "https://images.unsplash.com/photo-1520975916090-3105956dac38?auto=format&fit=crop&w=1200&q=80", + logoText: "GLOWNY", + hashtags: ["#클래식", "#편안함", "#러블리"], + description: "클래식한 실루엣에 트렌드를 더한 문턱 낮은 패션 브랜드", + categories: ["의류"], + tagSections: [ + { + title: "의류 태그", + groups: [ + { label: "브랜드 종류", chips: ["디자이너 브랜드", "중가 브랜드"] }, + { label: "브랜드 스타일", chips: ["러블리", "캐주얼"] }, + ], + }, + ], + ongoingCampaigns: [ + { + id: "c1", + brandName: "GLOWNY", + startAt: "9/10", + ddayLabel: "D-DAY", + matchRate: 78, + descText: "WILD TUBE TOP 체험단", + rewardText: "리워드 400,000원", + isLiked: false, + }, + { + id: "c2", + brandName: "GLOWNY", + startAt: "8/10", + ddayLabel: "D-3", + matchRate: 78, + descText: "SUGAR PUFF…", + rewardText: "리워드 400,000원", + isLiked: true, + }, + { + id: "c3", + brandName: "GLOWNY", + startAt: "5/1", + ddayLabel: "D-5", + matchRate: 78, + descText: "PEEKABOO…", + rewardText: "리워드 400,000원", + isLiked: false, + }, + ], + products: [ + { + id: "p1", + title: "WILD TUBE T…", + imageUrl: + "https://images.unsplash.com/photo-1520975916090-3105956dac38?auto=format&fit=crop&w=900&q=80", + }, + { + id: "p2", + title: "SUGAR PUFF …", + imageUrl: + "https://images.unsplash.com/photo-1520975867597-0df1b0d1f24f?auto=format&fit=crop&w=900&q=80", + }, + { + id: "p3", + title: "PEEKABOO …", + imageUrl: + "https://images.unsplash.com/photo-1520975682071-4f3f909cc053?auto=format&fit=crop&w=900&q=80", + }, + ], + histories: [ + { + id: "h1", + title: "‘WILD TUBE PANTS’ 체험…", + rightText: "1월 15일 진행예정", + highlight: true, + }, + { + id: "h2", + title: "‘WILD TUBE SKIRT’ 체험…", + rightText: "3월 25일 진행예정", + highlight: true, + }, + { id: "h3", title: "‘SUGAR PUFF SHIRT’ 리…", rightText: "12/15/24 완료" }, + { id: "h4", title: "‘SUGAR PUFF SHIRT’ 리…", rightText: "7/15/24 완료" }, + ], + }, +}; + +export function getBrandDetailMock(brandId: string): BrandDetailData { + return BRAND_DETAIL_MOCK[brandId] ?? BRAND_DETAIL_MOCK.beplain; +} diff --git a/src/routes/_main/_home/brand/query.ts b/src/routes/_main/_home/brand/query.ts new file mode 100644 index 0000000..623f85a --- /dev/null +++ b/src/routes/_main/_home/brand/query.ts @@ -0,0 +1,11 @@ +import { useQuery } from "@tanstack/react-query"; +import type { BrandDomain } from "./types"; +import { fetchBrandDetail } from "./api"; + +export function useBrandDetail(brandId: string, domain?: BrandDomain) { + return useQuery({ + queryKey: ["brandDetail", brandId, domain], + queryFn: () => fetchBrandDetail({ brandId, domain }), + staleTime: 60_000, + }); +} diff --git a/src/routes/_main/_home/brand/route.tsx b/src/routes/_main/_home/brand/route.tsx new file mode 100644 index 0000000..750c45d --- /dev/null +++ b/src/routes/_main/_home/brand/route.tsx @@ -0,0 +1,9 @@ +import { createFileRoute, Outlet } from "@tanstack/react-router"; + +export const Route = createFileRoute("/_main/_home/brand")({ + component: BrandLayout, +}); + +function BrandLayout() { + return ; +} diff --git a/src/routes/_main/_home/brand/types.ts b/src/routes/_main/_home/brand/types.ts new file mode 100644 index 0000000..1a223d9 --- /dev/null +++ b/src/routes/_main/_home/brand/types.ts @@ -0,0 +1,58 @@ +export type BrandDomain = "beauty" | "fashion"; + +export type TagGroup = { + label: string; // TagGroup 컴포넌트 prop과 동일 (label/chips) + chips: string[]; +}; + +export type BrandOngoingCampaign = { + id: string; + brandName: string; + startAt: string; // "7/10" + ddayLabel: string; // "D-3" + matchRate: number; + descText: string; + rewardText: string; + isLiked: boolean; +}; + +export type ProductMiniCardItem = { + id: string; + title: string; + imageUrl: string; +}; + +export type HistoryRowItem = { + id: string; + title: string; + rightText: string; + highlight?: boolean; +}; + +export type BrandDetailData = { + id: string; + domain: BrandDomain; + + name: string; + matchRate: number; + + heroImageUrl: string; + logoText?: string; // 텍스트 로고 (뷰티에서 사용) + logoImageUrl?: string; // 이미지 로고 (패션에서 사용 가능) + + hashtags: string[]; + description: string; + + // 카테고리(뷰티: 스킨케어/메이크업, 패션: 의류 등) + categories: string[]; + + // 섹션 단위 태그 (뷰티: 2개 섹션, 패션: 1개 섹션) + tagSections: Array<{ + title: string; // "스킨케어 태그" / "메이크업 태그" / "의류 태그" + groups: TagGroup[]; + }>; + + ongoingCampaigns: BrandOngoingCampaign[]; + products: ProductMiniCardItem[]; + histories: HistoryRowItem[]; +}; diff --git a/src/routes/_main/_home/campaign/index.tsx b/src/routes/_main/_home/campaign/index.tsx new file mode 100644 index 0000000..55d46b5 --- /dev/null +++ b/src/routes/_main/_home/campaign/index.tsx @@ -0,0 +1,20 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/_main/_home/campaign/")({ + validateSearch: (search: Record) => ({ + campaignId: typeof search.campaignId === "string" ? search.campaignId : "", + }), + component: CampaignTempDetail, +}); + +function CampaignTempDetail() { + const { campaignId } = Route.useSearch(); + return ( +
+
캠페인 상세(임시)
+
+ campaignId: {campaignId || "(없음)"} +
+
+ ); +} diff --git a/src/routes/_main/_home/campaign/route.tsx b/src/routes/_main/_home/campaign/route.tsx new file mode 100644 index 0000000..e0f1fa4 --- /dev/null +++ b/src/routes/_main/_home/campaign/route.tsx @@ -0,0 +1,5 @@ +import { createFileRoute, Outlet } from "@tanstack/react-router"; + +export const Route = createFileRoute("/_main/_home/campaign")({ + component: () => , +}); diff --git a/src/routes/_main/_home/components/BrandCard.tsx b/src/routes/_main/_home/components/BrandCard.tsx index 3c7caa4..0b5e488 100644 --- a/src/routes/_main/_home/components/BrandCard.tsx +++ b/src/routes/_main/_home/components/BrandCard.tsx @@ -1,58 +1,62 @@ -// src/routes/_home/components/BrandCard.tsx import type { BrandItem } from "../types"; import HeartButton from "./HeartButton"; -import BadgePill from "./BadgePill"; -const PRIMARY = "#5B5DEB"; +type Props = { + item: BrandItem; + onClick?: () => void; +}; -type Props = { item: BrandItem }; - -export default function BrandCard({ item }: Props) { +export default function BrandCard({ item, onClick }: Props) { return ( -
-
- {/* 상단: 좌 배지 / 우 하트 */} -
-
- {/* ✅ Brand 배지도 Campaign 배지와 동일 컴포넌트 */} - {item.badgeText ? : null} +
{ + if (!onClick) return; + if (e.key === "Enter" || e.key === " ") onClick(); + }} + className="shrink-0 text-left" + > + {/* ✅ 아래 UI는 기존 BrandCard 그대로 유지 */} +
+
+
+ {/* ✅ 하트 클릭이 카드 클릭으로 전파되지 않게 HeartButton에서 stopPropagation 처리 필요 */} + console.log("toggle like brand", item.id, v)} + />
- console.log("toggle like brand", item.id, v)} - /> +
+ {item.logoUrl ? ( + {item.name} + ) : ( +
+ {item.name} +
+ )} +
- {/* 중앙 로고 */} -
- {item.logoUrl ? ( - {item.name} - ) : ( -
+
+
+
{item.name}
- )} -
-
- - {/* 카드 아래 텍스트 */} -
-
-
- {item.name} +
+ {item.matchRate}% +
-
- {item.matchRate}% +
+ {item.subText ?? ""}
-
- {item.subText ?? ""} -
); diff --git a/src/routes/_main/_home/components/CampaignCard.tsx b/src/routes/_main/_home/components/CampaignCard.tsx index d262b55..dc941db 100644 --- a/src/routes/_main/_home/components/CampaignCard.tsx +++ b/src/routes/_main/_home/components/CampaignCard.tsx @@ -1,4 +1,3 @@ -// src/routes/_home/components/CampaignCard.tsx import type { CampaignItem } from "../types"; import HeartButton from "./HeartButton"; import BadgePill from "./BadgePill"; @@ -8,22 +7,51 @@ const PRIMARY = "#5B5DEB"; type Props = { item: CampaignItem; variant: "top" | "popular"; + showStartAt?: boolean; + rightTextMode?: "progress" | "matchRate"; + onClick?: () => void; }; -export default function CampaignCard({ item, variant }: Props) { +export default function CampaignCard({ + item, + variant, + showStartAt, + rightTextMode, + onClick, +}: Props) { + const showStart = showStartAt ?? variant === "top"; + + const mode = + rightTextMode ?? (variant === "popular" ? "progress" : "matchRate"); + const rightText = - variant === "popular" - ? `${item.progressText}명` + mode === "progress" + ? item.progressText + ? `${item.progressText}명` + : "" : item.matchRate != null ? `${item.matchRate}%` : ""; + const clickableProps = onClick + ? { + role: "button" as const, + tabIndex: 0, + onClick, + onKeyDown: (e: React.KeyboardEvent) => { + if (e.key === "Enter" || e.key === " ") onClick(); + }, + } + : {}; + return ( -
+
- {variant === "top" && item.startAt ? : null} + {showStart && item.startAt ? ( + + ) : null} {item.ddayLabel ? : null}
@@ -41,23 +69,32 @@ export default function CampaignCard({ item, variant }: Props) { className="max-h-[28px] w-auto object-contain" /> ) : ( -
{item.brandName}
+
+ {item.brandName} +
)}
-
{item.brandName}
+
+ {item.brandName} +
{rightText}
-
{item.descText ?? ""}
+
+ {item.descText ?? ""} +
{item.rewardText ? ( -
+
{item.rewardText}
) : null} diff --git a/src/routes/_main/_home/components/HeartButton.tsx b/src/routes/_main/_home/components/HeartButton.tsx index 5cb72c2..071d0ad 100644 --- a/src/routes/_main/_home/components/HeartButton.tsx +++ b/src/routes/_main/_home/components/HeartButton.tsx @@ -16,7 +16,8 @@ export default function HeartButton({ }: Props) { const [pressed, setPressed] = useState(defaultPressed); - const toggle = () => { + const toggle = (e: React.MouseEvent) => { + e.stopPropagation(); // ✅ 핵심: 카드 클릭으로 전파 방지 setPressed((prev) => { const next = !prev; onChange?.(next); @@ -34,7 +35,7 @@ export default function HeartButton({ className, ].join(" ")} > - {/* ✅ 하트 아이콘 크기 고정(항상 유지) */} + {/* 하트 아이콘 크기 고정 */} ); diff --git a/src/routes/_main/_home/components/SectionHeader.tsx b/src/routes/_main/_home/components/SectionHeader.tsx index 9e37226..149377e 100644 --- a/src/routes/_main/_home/components/SectionHeader.tsx +++ b/src/routes/_main/_home/components/SectionHeader.tsx @@ -12,18 +12,27 @@ export default function SectionHeader({
{title}
- {subtitle ?
{subtitle}
: null} + {subtitle ? ( +
{subtitle}
+ ) : null}
{onMore ? ( - +
) : null}
); diff --git a/src/routes/_main/_home/home-after-match.tsx b/src/routes/_main/_home/home-after-match.tsx index 1d77d10..3e8e3ce 100644 --- a/src/routes/_main/_home/home-after-match.tsx +++ b/src/routes/_main/_home/home-after-match.tsx @@ -1,4 +1,5 @@ import { useMemo, useState } from "react"; +import { useNavigate } from "@tanstack/react-router"; import type { CategoryKey, HomeAfterMatchCategoryData } from "./types"; import { HOME_AFTER_MATCH_MOCK } from "./home.mock"; import HeroCarousel from "./components/HeroCarousel"; @@ -8,7 +9,12 @@ import BrandCard from "./components/BrandCard"; import CampaignCard from "./components/CampaignCard"; import CreatorProfileCard from "./components/CreatorProfileCard"; +// ✅ index.tsx를 직접 import (barrel 금지) +import { Route as BrandIndexRoute } from "./brand/index"; +import { Route as CampaignIndexRoute } from "./campaign/index"; + export default function HomeAfterMatchPage() { + const navigate = useNavigate(); const [category, setCategory] = useState("beauty"); const data: HomeAfterMatchCategoryData = useMemo(() => { @@ -18,19 +24,13 @@ export default function HomeAfterMatchPage() { }, [category]); return ( - // ✅ 전체 화면 배경 흰색 고정
- {/* Hero 영역 */} - {/* 본문 */}
- {/* 카테고리 탭 */} - {/* =============================== - 매칭률 높은 브랜드 - =============================== */} + {/* 매칭률 높은 브랜드 */}
{data.topBrands.map((brand) => ( - + { + console.log("CLICK BRAND", brand.id, brand.domain); + navigate({ + to: BrandIndexRoute.to, + search: () => ({ + brandId: brand.id, + domain: brand.domain, + }), + }); + }} + /> ))}
- {/* =============================== - 매칭률 높은 캠페인 - =============================== */} + {/* 매칭률 높은 캠페인 */}
{data.topCampaigns.map((campaign) => ( - + { + navigate({ + to: CampaignIndexRoute.to, + search: () => ({ + campaignId: campaign.id, + }), + }); + }} + /> ))}
- {/* =============================== - 크리에이터 프로필 - =============================== */}
- {/* =============================== - 인기 캠페인 - =============================== */} + {/* 인기 캠페인 */}
{ + navigate({ + to: CampaignIndexRoute.to, + search: () => ({ + campaignId: campaign.id, + }), + }); + }} /> ))}
diff --git a/src/routes/_main/_home/types.ts b/src/routes/_main/_home/types.ts index 3a5bce7..27dee61 100644 --- a/src/routes/_main/_home/types.ts +++ b/src/routes/_main/_home/types.ts @@ -1,6 +1,7 @@ // src/routes/_home/types.ts export type CategoryKey = "beauty" | "fashion"; +export type BrandDomain = "beauty" | "fashion"; export interface HeroItem { id: string; @@ -15,8 +16,8 @@ export interface BrandItem { matchRate: number; // 0~100 isLiked?: boolean; subText?: string; // ✅ 카드 아래 회색 설명 (예: "세럼 / 선크림") - badgeText?: string; // ✅ "모집중" 같은 상태 표시 텍스트(백엔드 연동용) - + badgeText?: string; // ✅ "모집중" 같은 상태 표시 텍스트(백엔드 연동용) + domain: BrandDomain; // ✅ 추가 } export interface CampaignItem { @@ -25,15 +26,15 @@ export interface CampaignItem { logoUrl?: string; // 카드 상단 배지용 - startAt?: string; // top 캠페인에서는 날짜 배지로 쓸 수 있음(예: 7/10) + startAt?: string; // top 캠페인에서는 날짜 배지로 쓸 수 있음(예: 7/10) ddayLabel?: string; // D-3, D-DAY 등 // 카드 아래 오른쪽 텍스트 - matchRate?: number; // top 캠페인: 98% - progressText?: string; // ✅ 인기 캠페인: 7/10, 1/5 등 + matchRate?: number; // top 캠페인: 98% + progressText?: string; // ✅ 인기 캠페인: 7/10, 1/5 등 // 카드 아래 텍스트 - descText?: string; // 회색 설명 (예: "신제품 체험단 모집") + descText?: string; // 회색 설명 (예: "신제품 체험단 모집") rewardText?: string; // ✅ 보상 텍스트(보라색) isLiked?: boolean;