Skip to content
Merged
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
303 changes: 141 additions & 162 deletions app/routes/brand/api/api.ts
Original file line number Diff line number Diff line change
@@ -1,233 +1,212 @@
import { apiClient } from "../../../api/axios";
import type { BrandDomain, BrandDetailData, TagGroup } from "../types";

// API 응답 타입
interface BrandSkinCareTagDto {
brandSkinType?: string[];
brandMainFunction?: string[];
}

interface BrandMakeUpTagDto {
brandMakeUpStyle?: string[];
brandMakeUpColor?: string[];
}

interface BrandOnGoingCampaignDto {
brandId: number;
type BeautyResponseDto = {
categories: string[];
skinType: string[];
mainFunction: string[];
makeUpStyle: string[];
};

type FashionResponseDto = {
categories: string[];
brandType: string[];
brandStyle: string[];
};

type BrandDetailItemDto = {
userId: number;
brandName: string;
recruitingTotalNumber: number;
recruitedNumber: number;
campaginDescription: string;
campaginManuscriptFee: string;
campaignDDay?: number;
logoUrl?: string;
isLiked?: boolean;
}
simpleIntro?: string;
detailIntro?: string;
homepageUrl?: string;

brandTag: string | null;
brandMatchingRatio: number;
brandIsLiked: boolean;
brandDescriptionTags: string[];

beautyResponse: BeautyResponseDto | null;
fashionResponse: FashionResponseDto | null;
};

// 진행 중인 캠페인 API 응답 (api.md 1839줄)
interface RecruitingCampaignCardDto {
type BrandDetailApiResponse = {
isSuccess: boolean;
code: string;
message: string;
result: BrandDetailItemDto[];
};

type RecruitingCampaignCardDto = {
campaignId: number;
brandName: string;
title: string;
recruitQuota: number;
rewardAmount: number;
imageUrl?: string;
dday: number;
}

interface RecruitingCampaignsApiResponse {
isSuccess: boolean;
code: string;
message: string;
result: {
campaigns: RecruitingCampaignCardDto[];
};
}

interface AvailableSponsorProdDto {
productId: number;
productName: string;
productImageUrl?: string;
availableType?: string;
availableQuantity?: number;
availableSize?: number;
}
};

interface BrandDetailResponseDto {
brandName: string;
brandTag?: string[];
brandDescription?: string;
brandMatchingRatio?: number;
brandIsLiked?: boolean;
brandCategory?: string[];
brandSkinCareTag?: BrandSkinCareTagDto;
brandMakeUpTag?: BrandMakeUpTagDto;
brandOnGoingCampaign?: BrandOnGoingCampaignDto[];
availableSponsorProd?: AvailableSponsorProdDto[];
}

interface BrandDetailApiResponse {
type RecruitingCampaignsApiResponse = {
isSuccess: boolean;
code: string;
message: string;
result: BrandDetailResponseDto[];
}
result: { campaigns: RecruitingCampaignCardDto[] };
};

interface SponsorProductListResponseDto {
type SponsorProductListResponseDto = {
id: number;
name: string;
thumbnailImageUrl: string;
totalCount: number;
currentCount: number;
}
};

interface SponsorProductListApiResponse {
type SponsorProductListApiResponse = {
isSuccess: boolean;
code: string;
message: string;
result: SponsorProductListResponseDto[];
}
};

interface BrandCampaignResponseDto {
type BrandCampaignResponseDto = {
campaignId: number;
title: string;
recruitStartDate: string;
recruitEndDate: string;
status: "UPCOMING" | "RECRUITING" | "CLOSED";
}
};

interface BrandCampaignSliceResponse {
type BrandCampaignSliceResponse = {
campaigns: BrandCampaignResponseDto[];
nextCursor?: number;
}
};

interface BrandCampaignApiResponse {
type BrandCampaignApiResponse = {
isSuccess: boolean;
code: string;
message: string;
result: BrandCampaignSliceResponse;
}
};

// 날짜 포맷팅 헬퍼
function formatHistoryDate(campaign: BrandCampaignResponseDto): { text: string; highlight: boolean } {
function formatHistoryDate(
campaign: BrandCampaignResponseDto
): { text: string; highlight: boolean } {
if (campaign.status === "UPCOMING" || campaign.status === "RECRUITING") {
const date = new Date(campaign.recruitStartDate);
const month = date.getMonth() + 1;
const day = date.getDate();
return {
text: `${month}월 ${day}일 진행예정`,
highlight: true
};
} else {
const date = new Date(campaign.recruitEndDate);
const month = date.getMonth() + 1;
const day = date.getDate();
const year = date.getFullYear().toString().slice(2);
return {
text: `${month}/${day}/${year} 완료`,
highlight: false
};
return { text: `${month}월 ${day}일 진행예정`, highlight: true };
}
const date = new Date(campaign.recruitEndDate);
const month = date.getMonth() + 1;
const day = date.getDate();
const year = date.getFullYear().toString().slice(2);
return { text: `${month}/${day}/${year} 완료`, highlight: false };
}

function inferDomain(item: BrandDetailItemDto): BrandDomain {
return item.fashionResponse ? "fashion" : "beauty";
}

export async function fetchBrandDetail(params: {
brandId: string;
domain?: BrandDomain;
}): Promise<BrandDetailData> {
const { brandId, domain } = params;
function buildCategories(domain: BrandDomain): string[] {
if (domain === "fashion") return ["패션"];
return ["스킨케어", "메이크업"];
}

// 네 API 병렬 호출 (상세, 협찬제품, 캠페인 내역, 진행중인 캠페인)
const [detailResponse, productsResponse, campaignsResponse, recruitingResponse] = await Promise.all([
apiClient.get<BrandDetailApiResponse>(`/api/v1/brands/${brandId}`),
// 협찬 가능 제품 리스트 별도 호출 (api.md line 1652)
apiClient.get<SponsorProductListApiResponse>(`/api/v1/brands/${brandId}/sponsor-products`),
// 캠페인 내역 호출 (api.md line 1785)
apiClient.get<BrandCampaignApiResponse>(`/api/v1/brands/${brandId}/campaigns`),
// 진행 중인 캠페인 호출 (api.md line 1839)
apiClient.get<RecruitingCampaignsApiResponse>(`/api/v1/brands/${brandId}/campaigns/recruiting`)
]);
function buildTagSections(domain: BrandDomain, item: BrandDetailItemDto): Array<{ title: string; groups: TagGroup[] }> {
if (domain === "fashion") {
const f = item.fashionResponse;
if (!f) return [];

const styleGroups: TagGroup[] = [];

if (!detailResponse.data.isSuccess || !detailResponse.data.result?.length) {
throw new Error("브랜드 상세 조회 실패");
if (f.categories?.length) styleGroups.push({ label: "카테고리", chips: f.categories });
if (f.brandType?.length) styleGroups.push({ label: "브랜드 타입", chips: f.brandType });
if (f.brandStyle?.length) styleGroups.push({ label: "브랜드 스타일", chips: f.brandStyle });

return styleGroups.length ? [{ title: "스타일", groups: styleGroups }] : [];
}

const data = detailResponse.data.result[0];
const b = item.beautyResponse;
if (!b) return [];

// 협찬 제품 리스트
const productList = productsResponse.data.isSuccess ? productsResponse.data.result : [];
const styleGroups: TagGroup[] = [];

// 캠페인 내역 리스트
const historyList = campaignsResponse.data.isSuccess ? campaignsResponse.data.result.campaigns : [];
if (b.categories?.length) styleGroups.push({ label: "카테고리", chips: b.categories });
if (b.skinType?.length) styleGroups.push({ label: "피부타입", chips: b.skinType });
if (b.mainFunction?.length) styleGroups.push({ label: "주요 기능", chips: b.mainFunction });
if (b.makeUpStyle?.length) styleGroups.push({ label: "메이크업 스타일", chips: b.makeUpStyle });

// 진행 중인 캠페인 리스트
const recruitingList = recruitingResponse.data.isSuccess ? recruitingResponse.data.result.campaigns : [];
return styleGroups.length ? [{ title: "스타일", groups: styleGroups }] : [];
}

export async function fetchBrandDetail(params: { brandId: string; domain?: BrandDomain }): Promise<BrandDetailData> {
const { brandId, domain } = params;

// 태그 섹션 구성
const tagSections: Array<{ title: string; groups: TagGroup[] }> = [];
const [detailRes, productsRes, campaignsRes, recruitingRes] = await Promise.all([
apiClient.get<BrandDetailApiResponse>(`/api/v1/brands/${brandId}`),
apiClient.get<SponsorProductListApiResponse>(`/api/v1/brands/${brandId}/sponsor-products`),
apiClient.get<BrandCampaignApiResponse>(`/api/v1/brands/${brandId}/campaigns`),
apiClient.get<RecruitingCampaignsApiResponse>(`/api/v1/brands/${brandId}/campaigns/recruiting`),
]);

if (data.brandSkinCareTag) {
const groups: TagGroup[] = [];
if (data.brandSkinCareTag.brandSkinType?.length) {
groups.push({ label: "피부타입", chips: data.brandSkinCareTag.brandSkinType });
}
if (data.brandSkinCareTag.brandMainFunction?.length) {
groups.push({ label: "주요기능", chips: data.brandSkinCareTag.brandMainFunction });
}
if (groups.length) {
tagSections.push({ title: "스킨케어 태그", groups });
}
}
const detail = detailRes.data;
if (!detail.isSuccess || !detail.result?.length) throw new Error("브랜드 상세 조회 실패");

if (data.brandMakeUpTag) {
const groups: TagGroup[] = [];
if (data.brandMakeUpTag.brandMakeUpStyle?.length) {
groups.push({ label: "메이크업 스타일", chips: data.brandMakeUpTag.brandMakeUpStyle });
}
if (data.brandMakeUpTag.brandMakeUpColor?.length) {
groups.push({ label: "컬러", chips: data.brandMakeUpTag.brandMakeUpColor });
}
if (groups.length) {
tagSections.push({ title: "메이크업 태그", groups });
}
}
const item = detail.result[0];
const resolvedDomain = domain ?? inferDomain(item);

const safeDomain: BrandDomain =
resolvedDomain === "fashion" && !item.fashionResponse
? "beauty"
: resolvedDomain === "beauty" && !item.beautyResponse
? "fashion"
: resolvedDomain;

const productList = productsRes.data.isSuccess ? productsRes.data.result : [];
const historyList = campaignsRes.data.isSuccess ? campaignsRes.data.result.campaigns : [];
const recruitingList = recruitingRes.data.isSuccess ? recruitingRes.data.result.campaigns : [];

return {
id: brandId,
domain: domain || "beauty",
name: data.brandName,
matchRate: data.brandMatchingRatio || 0,
heroImageUrl: "", // API에 없으면 빈 값
logoText: data.brandName,
hashtags: data.brandTag || [],
description: data.brandDescription || "",
categories: data.brandCategory || [],
tagSections,
ongoingCampaigns: recruitingList.map((campaign) => ({
campaignId: campaign.campaignId,
brandName: campaign.brandName,
title: campaign.title,
recruitQuota: campaign.recruitQuota,
rewardAmount: campaign.rewardAmount,
imageUrl: campaign.imageUrl,
dday: campaign.dday,
userId: item.userId,
domain: safeDomain,

name: item.brandName,
matchRate: item.brandMatchingRatio ?? 0,

heroImageUrl: "",
logoText: item.brandName,
logoImageUrl: item.logoUrl,

hashtags: item.brandDescriptionTags ?? [],
description: item.simpleIntro ?? "",

categories: buildCategories(safeDomain),
tagSections: buildTagSections(safeDomain, item),

ongoingCampaigns: recruitingList.map((c) => ({
campaignId: c.campaignId,
brandName: c.brandName,
title: c.title,
recruitQuota: c.recruitQuota,
rewardAmount: c.rewardAmount,
imageUrl: c.imageUrl,
dday: c.dday,
isLiked: false,
})),
products: productList.map((product) => ({
id: String(product.id),
title: product.name,
imageUrl: product.thumbnailImageUrl || "",

products: productList.map((p) => ({
id: String(p.id),
title: p.name,
imageUrl: p.thumbnailImageUrl || "",
})),
// 캠페인 내역 매핑
histories: historyList.map(campaign => {
const { text, highlight } = formatHistoryDate(campaign);
return {
id: String(campaign.campaignId),
title: campaign.title,
rightText: text,
highlight
};

histories: historyList.map((c) => {
const { text, highlight } = formatHistoryDate(c);
return { id: String(c.campaignId), title: c.title, rightText: text, highlight };
}),
};
}
Loading
Loading