Skip to content

Commit a0251ab

Browse files
authored
Merge pull request #209 from RealMatchTeam/feat/ad-realmatch
[feat] 홈/매칭리스트 스켈레톤 UI 추가 및 광고 "리얼매치" 브랜드 추가
2 parents 255702e + 810fbc5 commit a0251ab

25 files changed

Lines changed: 686 additions & 214 deletions
94 KB
Loading
156 KB
Loading
3.93 KB
Loading
4.59 MB
Loading

app/routes/brand-detail/api/api.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import { apiClient } from "../../../api/axios";
2+
//import realmatchDetailLogo from "../../../assets/ad/ad-realmatch-detail-logo.png";
3+
//import realmatchDetailBanner from "../../../assets/ad/ad-realtmatch-detail-banner.png";
4+
//import realmatchDetailCampaign from "../../../assets/ad/ad-realmatch-detail-campagin.png";
25
import type {
36
BrandDomain,
47
BrandDetailData,
@@ -176,6 +179,31 @@ export async function fetchSponsorProductList(params: {
176179
}): Promise<SponsorProductsListDto[]> {
177180
const { brandId } = params;
178181

182+
/*if (brandId === "0") {
183+
return [
184+
{
185+
brandId: 0,
186+
brandName: "리얼매치",
187+
productId: 0,
188+
productName: "리얼이 캐릭터 크림",
189+
thumbnailImageUrl: realmatchDetailCampaign,
190+
productImageUrls: [realmatchDetailCampaign],
191+
categories: ["스킨케어"],
192+
sponsorInfo: {
193+
items: [
194+
{
195+
itemId: 0,
196+
availableType: "FULL",
197+
availableQuantity: 1,
198+
availableSize: 0,
199+
shippingType: "CREATOR_PAY",
200+
},
201+
],
202+
},
203+
},
204+
];
205+
}*/
206+
179207
const res = await apiClient.get<SponsorProductsApiResponse>(
180208
`/api/v1/brands/${brandId}/sponsor-products`,
181209
);
@@ -195,6 +223,28 @@ export async function fetchSponsorProductDetail(params: {
195223
}): Promise<SponsorProductDetailResult> {
196224
const { brandId, productId } = params;
197225

226+
/*if (brandId === "0" && String(productId) === "0") {
227+
return {
228+
brandId: 0,
229+
brandName: "리얼매치",
230+
productId: 0,
231+
productName: "리얼이 캐릭터 크림",
232+
productImageUrls: [realmatchDetailCampaign],
233+
categories: ["스킨케어"],
234+
sponsorInfo: {
235+
items: [
236+
{
237+
itemId: 0,
238+
availableType: "FULL",
239+
availableQuantity: 1,
240+
availableSize: 0,
241+
shippingType: "CREATOR_PAY",
242+
},
243+
],
244+
},
245+
};
246+
}*/
247+
198248
const res = await apiClient.get<SponsorProductDetailApiResponse>(
199249
`/api/v1/brands/${brandId}/sponsor-products/${productId}`,
200250
);
@@ -214,6 +264,38 @@ export async function fetchBrandDetail(params: {
214264
}): Promise<BrandDetailData> {
215265
const { brandId, domain } = params;
216266

267+
/*if (brandId === "0") {
268+
return {
269+
id: "0",
270+
userId: 0,
271+
brandUserId: undefined,
272+
domain: domain ?? "beauty",
273+
name: "리얼매치",
274+
matchRate: 0,
275+
heroImageUrl: realmatchDetailBanner,
276+
brandImages: [realmatchDetailBanner],
277+
logoText: "리얼매치",
278+
logoImageUrl: realmatchDetailLogo,
279+
homepageUrl: undefined,
280+
simpleIntro: "크리에이터와 브랜드를 정밀 매칭하는 플랫폼",
281+
hashtags: ["정밀매칭", "원스톱협업", "쌍방향제안"],
282+
description: "크리에이터와 브랜드를 정밀 매칭하는 플랫폼",
283+
categories: [],
284+
tagSections: [],
285+
isLiked: false,
286+
ongoingCampaigns: [],
287+
products: [
288+
{
289+
productId: 0,
290+
productName: "리얼이 캐릭터 크림",
291+
thumbnailImageUrl: realmatchDetailCampaign,
292+
},
293+
],
294+
histories: [],
295+
historiesHasNext: false,
296+
};
297+
}*/
298+
217299
const detailRes = await apiClient.get<BrandDetailApiResponse>(
218300
`/api/v1/brands/${brandId}`,
219301
);

app/routes/brand-detail/brand-detail-content.tsx

Lines changed: 88 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import HistoryRow from "./components/HistoryRow";
1111
import SponsorableProductSection from "./components/SponsorableProductSection";
1212

1313
import { tokenStorage } from "../../lib/token";
14-
import { toggleBrandLike } from "../matching/api/matching";
14+
import { toggleBrandLike, toggleCampaignLike } from "../matching/api/matching";
1515
import { useCampaignProposalStore } from "../../stores/campaign-proposal";
1616

1717
import { apiClient } from "../../api/axios";
@@ -143,7 +143,7 @@ const getNumberField = (
143143
const rec = obj as Record<string, unknown>;
144144
for (const k of keys) {
145145
const v = rec[k];
146-
if (typeof v === "number" && Number.isFinite(v) && v > 0) return v;
146+
if (typeof v === "number" && Number.isFinite(v) && v >= 0) return v;
147147
}
148148
return null;
149149
};
@@ -172,12 +172,14 @@ export default function BrandDetailContent({ data }: Props) {
172172
const navigate = useNavigate();
173173
const [searchParams] = useSearchParams();
174174
const brandId = Number(searchParams.get("brandId"));
175-
const validBrandId = Number.isFinite(brandId) && brandId > 0;
175+
const validBrandId = Number.isFinite(brandId) && brandId >= 0;
176176

177177
const setProposalData = useCampaignProposalStore(
178178
(state) => state.setProposalData,
179179
);
180180

181+
const isHardcodedBeauty = brandId === 0 && searchParams.get("domain") === "beauty";
182+
181183
const baseOngoingCampaigns = useMemo<OngoingCampaign[]>(
182184
() => data.ongoingCampaigns ?? [],
183185
[data.ongoingCampaigns],
@@ -187,19 +189,21 @@ export default function BrandDetailContent({ data }: Props) {
187189
Record<number, boolean>
188190
>({});
189191

192+
190193
const ongoingCampaigns = useMemo<OngoingCampaign[]>(() => {
191-
if (baseOngoingCampaigns.length === 0) return [];
192194
const overrides = ongoingLikeOverrides;
193195

194-
return baseOngoingCampaigns.map((c) => {
196+
const real = baseOngoingCampaigns.map((c) => {
195197
const cid = getCampaignIdFromOngoing(c);
196-
if (!cid) return c;
198+
if (cid === null) return c;
197199

198200
if (Object.prototype.hasOwnProperty.call(overrides, cid)) {
199201
return { ...(c as object), isLiked: overrides[cid] } as OngoingCampaign;
200202
}
201203
return c;
202204
});
205+
206+
return real;
203207
}, [baseOngoingCampaigns, ongoingLikeOverrides]);
204208

205209
const ongoingLikeInFlight = useRef<Set<number>>(new Set());
@@ -208,13 +212,17 @@ export default function BrandDetailContent({ data }: Props) {
208212
ProductMiniCardItem[]
209213
>([]);
210214

211-
const sponsorProducts = useMemo<ProductMiniCardItem[]>(
212-
() => (validBrandId ? sponsorProductsRaw : []),
213-
[validBrandId, sponsorProductsRaw],
214-
);
215+
const sponsorProducts = useMemo<ProductMiniCardItem[]>(() => {
216+
if (!validBrandId) return [];
217+
// brandId=0일 때는 data.products를 직접 사용
218+
if (brandId === 0) return data.products ?? [];
219+
return sponsorProductsRaw;
220+
}, [validBrandId, brandId, data.products, sponsorProductsRaw]);
215221

216222
useEffect(() => {
217223
if (!validBrandId) return;
224+
// brandId=0일 때는 API 호출 건너뛰기 (data.products 사용)
225+
//if (brandId === 0) return;
218226

219227
let alive = true;
220228

@@ -278,16 +286,53 @@ export default function BrandDetailContent({ data }: Props) {
278286

279287
const domain = searchParams.get("domain");
280288

281-
setProposalData({
282-
brandId,
283-
campaignId: 0,
284-
domain: domain || "beauty",
285-
brandName: data.name,
286-
products: sponsorProducts.map((p) => ({
287-
id: String(p.productId),
288-
name: p.productName,
289-
})),
290-
});
289+
// brandId=0일 때는 광고 캠페인 정보 포함
290+
if (brandId === 0) {
291+
setProposalData({
292+
brandId,
293+
campaignId: 0,
294+
domain: domain || "beauty",
295+
brandName: data.name,
296+
campaignTitle: "'리얼이 캐릭터 크림' 론칭 리뷰",
297+
campaignDescription: "'리얼이 캐릭터 크림'\n겟레디윗미 영상에서 자연스럽게 노출",
298+
rewardAmount: 200000,
299+
product: "리얼이 캐릭터 크림 1개",
300+
startDate: "2025-01-05",
301+
endDate: "2025-01-22",
302+
contentTags: {
303+
formats: [{ id: 3, name: "인스타 릴스" }],
304+
categories: [
305+
{ id: 6, name: "리뷰" },
306+
{ id: 7, name: "겟레디윗미" },
307+
],
308+
tones: [
309+
{ id: 16, name: "일상적인" },
310+
{ id: 17, name: "수다적인" },
311+
],
312+
usageRanges: [
313+
{ id: 24, name: "크리에이터 1차활용" },
314+
{ id: 25, name: "브랜드 2차활용" },
315+
],
316+
involvements: [{ id: 20, name: "가이드만 제공" }],
317+
},
318+
products: sponsorProducts.map((p) => ({
319+
id: String(p.productId),
320+
name: p.productName,
321+
})),
322+
});
323+
} else {
324+
// 일반 브랜드는 기존 로직 유지
325+
setProposalData({
326+
brandId,
327+
campaignId: 0,
328+
domain: domain || "beauty",
329+
brandName: data.name,
330+
products: sponsorProducts.map((p) => ({
331+
id: String(p.productId),
332+
name: p.productName,
333+
})),
334+
});
335+
}
291336

292337
navigate("/matching/suggest");
293338
};
@@ -306,7 +351,7 @@ export default function BrandDetailContent({ data }: Props) {
306351

307352
const handleSponsorableProductClick = (productId: number) => {
308353
if (!validBrandId) return;
309-
if (!Number.isFinite(productId) || productId <= 0) return;
354+
if (!Number.isFinite(productId)) return;
310355

311356
navigate(
312357
`/products/sponsorable/detail?brandId=${brandId}&productId=${productId}`,
@@ -335,8 +380,8 @@ export default function BrandDetailContent({ data }: Props) {
335380
};
336381

337382
const goOngoingCampaignDetail = (c: OngoingCampaign) => {
338-
const cid = getCampaignIdFromOngoing(c);
339-
if (!cid) return;
383+
const cid = getCampaignIdFromOngoing(c) ?? (c.campaignId === 0 ? 0 : null);
384+
if (cid === null) return;
340385

341386
const domainParam = searchParams.get("domain");
342387
const domain =
@@ -346,11 +391,11 @@ export default function BrandDetailContent({ data }: Props) {
346391

347392
const brandIdNum = validBrandId
348393
? brandId
349-
: Number.isFinite(Number(data.id)) && Number(data.id) > 0
394+
: Number.isFinite(Number(data.id)) && Number(data.id) >= 0
350395
? Number(data.id)
351396
: null;
352397

353-
if (!brandIdNum) return;
398+
if (brandIdNum === null) return;
354399

355400
navigate(
356401
`/campaign?brandId=${brandIdNum}&campaignId=${cid}&domain=${domain}`,
@@ -365,7 +410,7 @@ export default function BrandDetailContent({ data }: Props) {
365410
}
366411

367412
const clickedId = Number(id);
368-
if (!Number.isFinite(clickedId) || clickedId <= 0) return;
413+
if (!Number.isFinite(clickedId) || clickedId < 0) return;
369414

370415
const currentItem = ongoingCampaigns.find((c) => {
371416
const cid = getCampaignIdFromOngoing(c);
@@ -374,7 +419,7 @@ export default function BrandDetailContent({ data }: Props) {
374419
if (!currentItem) return;
375420

376421
const cid = getCampaignIdFromOngoing(currentItem);
377-
if (!cid) return;
422+
if (cid === null) return;
378423

379424
if (ongoingLikeInFlight.current.has(cid)) return;
380425
ongoingLikeInFlight.current.add(cid);
@@ -385,14 +430,24 @@ export default function BrandDetailContent({ data }: Props) {
385430

386431
setOngoingLikeOverrides((m) => ({ ...m, [cid]: next }));
387432

388-
ongoingLikeInFlight.current.delete(cid);
433+
try {
434+
await toggleCampaignLike(cid);
435+
} catch (error) {
436+
console.error("Failed to toggle campaign like:", error);
437+
setOngoingLikeOverrides((m) => ({ ...m, [cid]: prev }));
438+
} finally {
439+
ongoingLikeInFlight.current.delete(cid);
440+
}
389441
};
390442

391443
const PAGE_SIZE = 4;
392444
const GROUP_SIZE = 4;
393445

394-
const histories = data.histories ?? [];
395-
const hasNext = !!data.historiesHasNext;
446+
const histories = useMemo(() => {
447+
return data.histories ?? [];
448+
}, [data.histories]);
449+
450+
const hasNext = isHardcodedBeauty ? false : !!data.historiesHasNext;
396451

397452
const [page, setPage] = useState(1);
398453

@@ -457,8 +512,9 @@ export default function BrandDetailContent({ data }: Props) {
457512
<BrandInfo
458513
name={data.name}
459514
matchRate={data.matchRate}
460-
hashtags={(data.hashtags ?? []).slice(0, 2)}
515+
hashtags={data.hashtags ?? []}
461516
description={data.description}
517+
isAd={brandId === 0}
462518
/>
463519

464520
<div className="mt-3.5">
@@ -486,6 +542,7 @@ export default function BrandDetailContent({ data }: Props) {
486542

487543
<section>
488544
{(tagSections ?? []).map((sec, idx) => {
545+
489546
const showTitle = showSectionTitle;
490547

491548
return (

app/routes/brand-detail/components/BrandInfo.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,32 @@ type Props = {
33
matchRate: number;
44
hashtags: string[];
55
description: string;
6+
isAd?: boolean;
67
};
78

89
export default function BrandInfo({
910
name,
1011
matchRate,
1112
hashtags,
1213
description,
14+
isAd = false,
1315
}: Props) {
1416
return (
1517
<div className="pt-11.5">
1618
<div className="flex items-center justify-between">
1719
<div className="text-title text-text-black">{name}</div>
1820

1921
<div className="flex items-center gap-2">
20-
<span className="text-callout1 text-core-1 leading-none">매칭률</span>
21-
22-
<span className="text-title text-core-1 leading-none">
23-
{matchRate}%
24-
</span>
22+
{isAd ? (
23+
<span className="text-title text-core-1 leading-none">광고</span>
24+
) : (
25+
<>
26+
<span className="text-callout1 text-core-1 leading-none">매칭률</span>
27+
<span className="text-title text-core-1 leading-none">
28+
{matchRate}%
29+
</span>
30+
</>
31+
)}
2532
</div>
2633
</div>
2734

0 commit comments

Comments
 (0)