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
6 changes: 3 additions & 3 deletions app/api/axios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ axiosInstance.interceptors.request.use(
},
(error) => {
return Promise.reject(error);
}
},
);

// Response 인터셉터: 401 에러 시 토큰 갱신
Expand Down Expand Up @@ -76,7 +76,7 @@ axiosInstance.interceptors.response.use(
headers: {
RefreshToken: `Bearer ${refreshToken}`,
},
}
},
);

const { accessToken, refreshToken: newRefreshToken } =
Expand Down Expand Up @@ -117,5 +117,5 @@ axiosInstance.interceptors.response.use(
}

return Promise.reject(error);
}
},
);
22 changes: 22 additions & 0 deletions app/routes/matching/test/_shared/api/matches.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { axiosInstance } from "../../../../../api/axios";
import type { ApiResponse } from "../types/matches.types";
import type {
MatchesRequest,
MatchesResponseResult,
} from "../types/matches.types";

export async function postMatches(
payload: MatchesRequest,
): Promise<ApiResponse<MatchesResponseResult>> {
const res = await axiosInstance.post<ApiResponse<MatchesResponseResult>>(
"/api/v1/matches",
payload,
);

// 통신 자체는 성공했는데 서버가 실패 플래그 준 케이스
if (!res.data.isSuccess) {
throw new Error(res.data.message || "매칭 요청 실패");
}

return res.data;
}
128 changes: 128 additions & 0 deletions app/routes/matching/test/_shared/builders/build-match-payload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { useMatchingTestStore } from "../../../../../stores/matching-test";
import type { MatchesRequest } from "../types/matches.types";

function requireOne(arr: number[], fieldName: string): number {
const v = arr[0];
if (typeof v !== "number") {
throw new Error(`${fieldName} 값이 비어있습니다.`);
}
return v;
}

function requireNonEmpty(arr: number[], fieldName: string): number[] {
if (!Array.isArray(arr) || arr.length === 0) {
throw new Error(`${fieldName} 값이 비어있습니다.`);
}
return arr;
}

function requireNumber(v: number | null, fieldName: string): number {
if (typeof v !== "number") {
throw new Error(`${fieldName} 값이 비어있습니다.`);
}
return v;
}

export function buildMatchPayload(): MatchesRequest {
const s = useMatchingTestStore.getState();

const step1 = s.selected;
const step2 = s.step2Selected;
const body = s.fashionBody;

const step3Sel = s.step3Selected;
const step3Chips = s.step3Chips;

const interestStyleTags = requireNonEmpty(
step1.style,
"beauty.interestStyleTags",
);
const prefferedFunctionTags = requireNonEmpty(
step1.function,
"beauty.prefferedFunctionTags",
);

const skinTypeTags = requireOne(step1.skinType, "beauty.skinTypeTags");
const skinToneTags = requireOne(step1.skinTone, "beauty.skinToneTags");
const makeupStyleTags = requireOne(
step1.makeupStyle,
"beauty.makeupStyleTags",
);

const heightTag = requireNumber(body.heightTag, "fashion.heightTag");
const weightTypeTag = requireNumber(
body.weightTypeTag,
"fashion.weightTypeTag",
);
const topSizeTag = requireNumber(body.topSizeTag, "fashion.topSizeTag");
const bottomSizeTag = requireNumber(
body.bottomSizeTag,
"fashion.bottomSizeTag",
);

const url = s.snsUrl.trim();
if (!url) throw new Error("content.sns.url 값이 비어있습니다.");

return {
beauty: {
interestStyleTags,
prefferedFunctionTags,
skinTypeTags,
skinToneTags,
makeupStyleTags,
},
fashion: {
interestStyleTags: requireNonEmpty(
step2.fashionStyle,
"fashion.interestStyleTags",
),
preferredItemTags: requireNonEmpty(
step2.interestItem,
"fashion.preferredItemTags",
),
preferredBrandTags: requireNonEmpty(
step2.brandType,
"fashion.preferredBrandTags",
),
heightTag,
weightTypeTag,
topSizeTag,
bottomSizeTag,
},
content: {
sns: {
url,
mainAudience: {
genderTags: requireNonEmpty(
step3Sel.gender,
"content.sns.mainAudience.genderTags",
),
ageTags: requireNonEmpty(
step3Sel.ageGroup,
"content.sns.mainAudience.ageTags",
),
},
averageAudience: {
videoLengthTags: requireNonEmpty(
step3Sel.videoLength,
"content.sns.averageAudience.videoLengthTags",
),
videoViewsTags: requireNonEmpty(
step3Sel.views,
"content.sns.averageAudience.videoViewsTags",
),
},
},
typeTags: requireNonEmpty(step3Chips.contentType, "content.typeTags"),
toneTags: requireNonEmpty(step3Chips.contentTone, "content.toneTags"),
prefferedInvolvementTags: requireNonEmpty(
step3Chips.contentHardness,
"content.prefferedInvolvementTags",
),
prefferedCoverageTags: requireNonEmpty(
step3Chips.editingRange,
"content.prefferedCoverageTags",
),
},
};
}
24 changes: 24 additions & 0 deletions app/routes/matching/test/_shared/builders/build-matching-result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { MatchesResponseResult } from "../types/matches.types";

export type MatchingApiViewModel = {
userType: string;
typeTag: string[];
brands: Array<{
brandId: number;
brandName: string;
matchingRatio: number;
logoUrl?: string;
}>;
count: number;
};

export function buildMatchingResult(
api: MatchesResponseResult,
): MatchingApiViewModel {
return {
userType: api.userType,
typeTag: api.typeTag,
brands: api.highMatchingBrandList.brands,
count: api.highMatchingBrandList.count,
};
}
28 changes: 28 additions & 0 deletions app/routes/matching/test/_shared/tags/tags.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { axiosInstance } from "../../../../../api/axios";
import type { ApiResponse } from "../types/matches.types";
import type { BeautyTags, FashionTags, ContentTags } from "./tags.types";

export async function fetchBeautyTags(): Promise<BeautyTags> {
const res = await axiosInstance.get<ApiResponse<BeautyTags>>(
"/api/v1/tags/beauty",
);
if (!res.data.isSuccess)
throw new Error(res.data.message || "beauty 태그 조회 실패");
return res.data.result;
}
export async function fetchFashionTags(): Promise<FashionTags> {
const res = await axiosInstance.get<ApiResponse<FashionTags>>(
"/api/v1/tags/fashion",
);
if (!res.data.isSuccess)
throw new Error(res.data.message || "fashion 태그 조회 실패");
return res.data.result;
}
export async function fetchContentTags(): Promise<ContentTags> {
const res = await axiosInstance.get<ApiResponse<ContentTags>>(
"/api/v1/tags/content",
);
if (!res.data.isSuccess)
throw new Error(res.data.message || "content 태그 조회 실패");
return res.data.result;
}
32 changes: 32 additions & 0 deletions app/routes/matching/test/_shared/tags/tags.query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useQuery } from "@tanstack/react-query";
import {
fetchBeautyTags,
fetchFashionTags,
fetchContentTags,
} from "./tags.api";
import type { BeautyTags, FashionTags, ContentTags } from "./tags.types";

const STALE = 1000 * 60 * 10;

export function useBeautyTags() {
return useQuery<BeautyTags, Error>({
queryKey: ["tags", "beauty"],
queryFn: fetchBeautyTags,
staleTime: STALE,
});
}

export function useFashionTags() {
return useQuery<FashionTags, Error>({
queryKey: ["tags", "fashion"],
queryFn: fetchFashionTags,
staleTime: STALE,
});
}
export function useContentTags() {
return useQuery<ContentTags, Error>({
queryKey: ["tags", "content"],
queryFn: fetchContentTags,
staleTime: STALE,
});
}
27 changes: 27 additions & 0 deletions app/routes/matching/test/_shared/tags/tags.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export type TagItem = {
id: number;
name: string;
};

export type BeautyTags = {
tagType: string;
categories: Record<string, TagItem[]>;
};

export type FashionTags = {
tagType: string;
categories: Record<string, TagItem[]>;
};

export type ContentTags = {
viewerGenders: TagItem[];
viewerAges: TagItem[];
avgVideoLengths: TagItem[];
avgVideoViews: TagItem[];

formats: TagItem[];
categories: TagItem[];
tones: TagItem[];
involvements: TagItem[];
usageRanges: TagItem[];
};
60 changes: 60 additions & 0 deletions app/routes/matching/test/_shared/types/matches.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
export type ApiResponse<T> = {
isSuccess: boolean;
code: string;
message: string;
result: T;
};

export type MatchesRequest = {
beauty: {
interestStyleTags: number[];
prefferedFunctionTags: number[];
skinTypeTags: number;
skinToneTags: number;
makeupStyleTags: number;
};
fashion: {
interestStyleTags: number[];
preferredItemTags: number[];
preferredBrandTags: number[];
heightTag: number;
weightTypeTag: number;
topSizeTag: number;
bottomSizeTag: number;
};
content: {
sns: {
url: string;
mainAudience: {
genderTags: number[];
ageTags: number[];
};
averageAudience: {
videoLengthTags: number[];
videoViewsTags: number[];
};
};
typeTags: number[];
toneTags: number[];
prefferedInvolvementTags: number[];
prefferedCoverageTags: number[];
};
};

export type MatchedBrand = {
brandId: number;
brandName: string;
matchingRatio: number;
logoUrl?: string;
};

export type HighMatchingBrandList = {
count: number;
brands: MatchedBrand[];
};

export type MatchesResponseResult = {
userType: string;
typeTag: string[];
highMatchingBrandList: HighMatchingBrandList;
};
22 changes: 22 additions & 0 deletions app/routes/matching/test/_shared/types/matching-result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export type ApiResponse<T> = {
isSuccess: boolean;
code: string;
message: string;
result: T;
};

export type Brand = {
brandId: number;
brandName: string;
logoUrl?: string;
matchingRatio: number;
};

export type MatchingResultApi = {
userType: string;
typeTag: string[];
highMatchingBrandList: {
count: number;
brands: Brand[];
};
};
Loading