Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/routes/_main/_home/brand/api.ts
Original file line number Diff line number Diff line change
@@ -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<BrandDetailData> {
const { brandId } = params;

const data = BRAND_DETAIL_MOCK[brandId];
if (!data) {
// ✅ 여기서 바로 잡히면 home에서 넘어오는 id가 잘못된 것
throw new Error(`Unknown brandId: ${brandId}`);
}

return data;
}
145 changes: 145 additions & 0 deletions src/routes/_main/_home/brand/brand-detail-content.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="min-h-screen bg-white">
<div className="mx-auto min-h-screen max-w-[430px] bg-white">
<BrandHero
heroImageUrl={data.heroImageUrl}
logoText={data.logoText ?? ""}
/>

<div className="px-5 pb-10">
<BrandInfo
name={data.name}
matchRate={data.matchRate}
hashtags={data.hashtags}
description={data.description}
/>

<BrandActionBar
isHearted={false}
onChat={() => console.log("채팅하기")}
onSuggest={() => console.log("제안하기")}
onToggleHeart={() => console.log("하트")}
/>

<DividerBlock />

{/* 카테고리 */}
<section className="py-5">
<div className="text-title7 text-text-black">카테고리</div>
<div className="mt-3 flex flex-wrap gap-2">
{data.categories.map((c) => (
<PillChip key={c} variant="filled">
{c}
</PillChip>
))}
</div>
</section>

<div className="h-px bg-bluegray-2" />

{/* 태그 섹션(뷰티/패션 공통 렌더링) */}
<section className="py-5">
{data.tagSections.map((sec, idx) => (
<div
key={`${sec.title}-${idx}`}
className={idx === 0 ? "" : "mt-6"}
>
<div className="text-title7 text-text-black">{sec.title}</div>
<div className="mt-4 space-y-4">
{sec.groups.map((g) => (
<TagGroup
key={`${sec.title}-${g.label}`}
label={g.label}
chips={g.chips}
/>
))}
</div>
</div>
))}
</section>

<DividerBlock />

{/* 진행 중인 캠페인 */}
<OngoingCampaignSection
campaigns={data.ongoingCampaigns}
onMore={() => console.log("캠페인 더보기")}
/>

<DividerBlock />

{/* 협찬 가능 제품 */}
<section className="py-5">
<div className="flex items-center justify-between">
<div className="text-title7 text-text-black">협찬 가능 제품</div>
<button
type="button"
className="text-[18px] text-text-gray3"
aria-label="more"
>
</button>
</div>

<div className="mt-4 -mx-5 overflow-x-auto px-5 scrollbar-hide">
<div className="flex gap-3">
{data.products.map((p) => (
<ProductMiniCard key={p.id} item={p} />
))}
</div>
</div>
</section>

<DividerBlock />

{/* 캠페인 내역 */}
<section className="py-5">
<div className="text-title7 text-text-black">캠페인 내역</div>

<div className="mt-3">
{data.histories.map((h) => (
<HistoryRow key={h.id} item={h} />
))}

<div className="flex items-center border-t border-bluegray-2 py-3">
<div className="flex-1" />
<div className="w-[140px] shrink-0 text-right">
<button
type="button"
onClick={() => console.log("더보기")}
className="inline-block bg-transparent p-0 text-[13px] font-medium text-text-gray3 outline-none"
aria-label="캠페인 내역 더보기"
>
+ 더보기
</button>
</div>
</div>
</div>
</section>
</div>
</div>
</div>
);
}

function DividerBlock() {
return (
<div className="relative left-1/2 mt-5 h-2 w-screen -translate-x-1/2 bg-bluegray-1" />
);
}
39 changes: 39 additions & 0 deletions src/routes/_main/_home/brand/components/BrandActionBar.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="mt-4 flex items-center gap-2">
<button
type="button"
onClick={onChat}
className="h-9 flex-1 rounded-xl bg-[#F5F6FA] text-[13px] font-medium text-text-black"
>
채팅하기
</button>

<button
type="button"
onClick={onSuggest}
className="h-9 flex-1 rounded-xl bg-[#F5F6FA] text-[13px] font-medium text-text-black"
>
제안하기
</button>

<div className="grid h-9 w-9 place-items-center rounded-xl border border-bluegray-2 bg-bg-w">
<HeartButton defaultPressed={isHearted} onChange={onToggleHeart} />
</div>
</div>
);
}
37 changes: 37 additions & 0 deletions src/routes/_main/_home/brand/components/BrandHero.tsx
Original file line number Diff line number Diff line change
@@ -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 제거 (로고가 잘리지 않게)
<div className="relative h-[210px] w-full bg-bluegray-2">
{/* ✅ 이미지만 overflow 처리 */}
<div className="h-full w-full overflow-hidden">
<img src={heroImageUrl} alt="" className="h-full w-full object-cover" />
</div>

{/* back */}
<button
type="button"
onClick={() => router.history.back()}
className="absolute left-4 top-4 grid h-9 w-9 place-items-center rounded-full bg-white/80"
aria-label="back"
>
</button>

{/* ✅ 로고: 배너 밖으로 내려와도 안 잘림 */}
<div className="absolute -bottom-8 left-5 z-10">
<div className="grid h-16 w-16 place-items-center rounded-full bg-white shadow-[0_8px_18px_rgba(0,0,0,0.12)]">
<span className="text-[14px] font-semibold">{logoText}</span>
</div>
</div>
</div>
);
}
44 changes: 44 additions & 0 deletions src/routes/_main/_home/brand/components/BrandInfo.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="pt-10">
<div className="flex items-start justify-between">
<div className="text-[20px] font-semibold tracking-tight text-text-black">
{name}
</div>

{/* ✅ 기존 한 줄 유지 + 숫자만 크게 */}
<div
className="text-[14px] font-semibold leading-none"
style={{ color: PRIMARY }}
>
<span>매칭률 </span>
<span className="text-[24px] font-extrabold tracking-tight">
{matchRate}%
</span>
</div>
</div>

<div className="mt-1 text-[12px] text-text-gray3">
{hashtags.join(" ")}
</div>

<div className="mt-2 text-[12px] leading-5 text-text-gray2">
{description}
</div>
</div>
);
}
33 changes: 33 additions & 0 deletions src/routes/_main/_home/brand/components/HistoryRow.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex items-center border-b border-bluegray-2 py-3">
{/* 좌측 컬럼 */}
<div className="min-w-0 flex-1 truncate text-[13px] text-text-black">
{item.title}
</div>

{/* ✅ 우측 컬럼: 고정폭 박스 안에서 우측정렬 */}
<div className="w-[140px] shrink-0 text-right">
<span
className="text-[13px] font-medium"
style={{ color: item.highlight ? PRIMARY : "#9B9BA1" }}
>
{item.rightText}
</span>
</div>
</div>
);
}
44 changes: 44 additions & 0 deletions src/routes/_main/_home/brand/components/OngoingCampaignSection.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<section className="py-5">
<div className="flex items-center justify-between">
<div className="text-title7 text-text-black">진행 중인 캠페인</div>

{onMore ? (
<button
type="button"
onClick={onMore}
className="grid h-8 w-8 place-items-center rounded-full text-text-gray2"
aria-label="more"
>
<span className="text-[18px] leading-none">›</span>
</button>
) : (
<div className="h-8 w-8" />
)}
</div>

<div className="mt-4 -mx-5 overflow-x-auto px-5 scrollbar-hide">
<div className="flex gap-3">
{campaigns.map((c) => (
<CampaignCard
key={c.id}
item={toCampaignItem(c)}
variant="popular"
showStartAt
rightTextMode="matchRate"
/>
))}
</div>
</div>
</section>
);
}
22 changes: 22 additions & 0 deletions src/routes/_main/_home/brand/components/PillChip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
type Props = {
children: React.ReactNode;
variant?: "filled" | "outline";
};

export default function PillChip({ children, variant = "outline" }: Props) {
if (variant === "filled") {
// 카테고리 칩(스크샷 느낌)
return (
<span className="inline-flex h-7 items-center rounded-full bg-[#ECECFF] px-3 text-[12px] font-medium text-core-1">
{children}
</span>
);
}

// ✅ 태그 칩(더 작게)
return (
<span className="inline-flex h-6 items-center rounded-full border border-[#E3E5F1] px-2.5 text-[11px] font-medium text-text-gray2">
{children}
</span>
);
}
Loading