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
5 changes: 4 additions & 1 deletion app/api/axios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ axiosInstance.interceptors.response.use(
};

// 400 또는 401 에러이고, 재시도하지 않은 요청인 경우
if ((error.response?.status === 401 || error.response?.status === 400) && !originalRequest._retry) {
if (
(error.response?.status === 401 || error.response?.status === 400) &&
!originalRequest._retry
) {
originalRequest._retry = true;

if (!isRefreshing) {
Expand Down
101 changes: 63 additions & 38 deletions app/routes/matching/api/matching.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { apiClient } from "../../../api/axios";
import { axiosInstance } from "../../../api/axios";
import { tokenStorage } from "../../../lib/token";

// 매칭 분석 결과 조회 응답 타입
Expand Down Expand Up @@ -41,13 +41,16 @@ export const getMatchAnalysis = async (): Promise<MatchResult> => {
throw new MatchingTestRequiredError();
}

const response = await apiClient.get<MatchResultResponse>(`/api/v1/matches`);
const response =
await axiosInstance.get<MatchResultResponse>(`/api/v1/matches`);

if (!response.data.isSuccess) {
// 매칭 테스트 미완료 에러 체크
if (response.data.code === "MATCH_TEST_NOT_COMPLETED" ||
if (
response.data.code === "MATCH_TEST_NOT_COMPLETED" ||
response.data.message.includes("매칭") ||
response.data.message.includes("테스트")) {
response.data.message.includes("테스트")
) {
throw new MatchingTestRequiredError();
}
throw new Error(response.data.message || "매칭 분석 결과 조회 실패");
Expand Down Expand Up @@ -201,7 +204,7 @@ export const getMatchingCampaigns = async (
tags?: string[],
keyword?: string,
page: number = 0,
size: number = 20
size: number = 20,
): Promise<{ campaigns: MatchingCampaign[]; count: number }> => {
try {
const userId = tokenStorage.getUserId();
Expand All @@ -213,7 +216,7 @@ export const getMatchingCampaigns = async (
sortBy,
category,
page,
size
size,
};

if (tags && tags.length > 0) {
Expand All @@ -224,16 +227,18 @@ export const getMatchingCampaigns = async (
params.keyword = keyword;
}

const response = await apiClient.get<MatchingCampaignResponse>(
const response = await axiosInstance.get<MatchingCampaignResponse>(
`/api/v1/matches/campaigns`,
{ params }
{ params },
);

if (!response.data.isSuccess) {
// 매칭 테스트 미완료 에러 체크
if (response.data.code === "MATCH_TEST_NOT_COMPLETED" ||
if (
response.data.code === "MATCH_TEST_NOT_COMPLETED" ||
response.data.message.includes("매칭") ||
response.data.message.includes("테스트")) {
response.data.message.includes("테스트")
) {
throw new MatchingTestRequiredError();
}
throw new Error(response.data.message || "캠페인 목록 조회 실패");
Expand All @@ -254,23 +259,27 @@ export const getMatchingCampaigns = async (
applicants: item.campaignTotalRecruit || 0,
isLiked: item.brandIsLiked || false,
logoUrl: item.brandLogoUrl,
dDay: item.campaignDDay
dDay: item.campaignDDay,
}));

return {
campaigns,
count: response.data.result.count || 0
count: response.data.result.count || 0,
};
} catch (error: unknown) {
if (error instanceof MatchingTestRequiredError) {
throw error;
}

const axiosError = error as { response?: { status: number; data?: { message?: string } } };
if (axiosError.response?.status === 404 ||
const axiosError = error as {
response?: { status: number; data?: { message?: string } };
};
if (
axiosError.response?.status === 404 ||
axiosError.response?.status === 400 ||
axiosError.response?.data?.message?.includes("매칭") ||
axiosError.response?.data?.message?.includes("테스트")) {
axiosError.response?.data?.message?.includes("테스트")
) {
throw new MatchingTestRequiredError();
}

Expand All @@ -288,28 +297,33 @@ export const getMatchingCampaigns = async (
export const getMatchingBrands = async (
sortBy: string = "MATCH_SCORE",
category: string = "ALL",
tags?: string[]
tags?: string[],
): Promise<{ brands: MatchingBrand[]; count: number }> => {
try {
const userId = tokenStorage.getUserId();
if (!userId) {
throw new MatchingTestRequiredError();
}

const params: Record<string, string | number | string[]> = { sortBy, category };
const params: Record<string, string | number | string[]> = {
sortBy,
category,
};
if (tags && tags.length > 0) {
params.tags = tags;
}

const response = await apiClient.get<MatchingBrandResponse>(
const response = await axiosInstance.get<MatchingBrandResponse>(
`/api/v1/matches/brands`,
{ params }
{ params },
);

if (!response.data.isSuccess) {
if (response.data.code === "MATCH_TEST_NOT_COMPLETED" ||
if (
response.data.code === "MATCH_TEST_NOT_COMPLETED" ||
response.data.message.includes("매칭") ||
response.data.message.includes("테스트")) {
response.data.message.includes("테스트")
) {
throw new MatchingTestRequiredError();
}
throw new Error(response.data.message || "브랜드 목록 조회 실패");
Expand All @@ -323,23 +337,27 @@ export const getMatchingBrands = async (
matchingRatio: item.matchingRatio,
isLiked: item.brandIsLiked || false,
category: item.category || category,
tags: item.tags || []
tags: item.tags || [],
}));

return {
brands,
count: response.data.result.count || 0
count: response.data.result.count || 0,
};
} catch (error: unknown) {
if (error instanceof MatchingTestRequiredError) {
throw error;
}

const axiosError = error as { response?: { status: number; data?: { message?: string } } };
if (axiosError.response?.status === 404 ||
const axiosError = error as {
response?: { status: number; data?: { message?: string } };
};
if (
axiosError.response?.status === 404 ||
axiosError.response?.status === 400 ||
axiosError.response?.data?.message?.includes("매칭") ||
axiosError.response?.data?.message?.includes("테스트")) {
axiosError.response?.data?.message?.includes("테스트")
) {
throw new MatchingTestRequiredError();
}

Expand All @@ -353,7 +371,9 @@ export const getMatchingBrands = async (
*/
export const getBrandFilters = async (): Promise<BrandFilterResponseDto[]> => {
try {
const response = await apiClient.get<BrandFilterResponse>('/api/v1/brands/filters');
const response = await axiosInstance.get<BrandFilterResponse>(
"/api/v1/brands/filters",
);

if (!response.data.isSuccess) {
throw new Error(response.data.message || "브랜드 필터 조회 실패");
Expand All @@ -380,9 +400,9 @@ interface BrandLikeResponse {

export const toggleBrandLike = async (brandId: number): Promise<boolean> => {
try {
const response = await apiClient.post<BrandLikeResponse>(
const response = await axiosInstance.post<BrandLikeResponse>(
`/api/v1/brands/${brandId}/like`,
{} // 빈 객체 body 추가
{}, // 빈 객체 body 추가
);

if (!response.data.isSuccess) {
Expand All @@ -402,14 +422,15 @@ export const toggleBrandLike = async (brandId: number): Promise<boolean> => {
* @param category 카테고리 (BEAUTY, FASHION 등)
* @returns 태그 이름 배열
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const getTagNamesByCategory = async (_category: string): Promise<string[]> => {
export const getTagNamesByCategory = async (
_category: string,
): Promise<string[]> => {
void _category;
// 카테고리별로 기본 태그를 반환하여 모든 태그 포함
// 실제로는 API에서 카테고리별 태그를 가져와야 할 수 있음
return [];
};


// 캠페인 제안 요청 타입
export interface CreateCampaignProposalRequest {
brandId: number;
Expand Down Expand Up @@ -441,11 +462,13 @@ interface CreateCampaignProposalResponse {
/**
* 캠페인 제안하기 (역제안)
*/
export const createCampaignProposal = async (data: CreateCampaignProposalRequest): Promise<number> => {
export const createCampaignProposal = async (
data: CreateCampaignProposalRequest,
): Promise<number> => {
try {
const response = await apiClient.post<CreateCampaignProposalResponse>(
const response = await axiosInstance.post<CreateCampaignProposalResponse>(
"/api/v1/campaigns/proposal",
data
data,
);

if (!response.data.isSuccess) {
Expand Down Expand Up @@ -503,11 +526,13 @@ export interface MatchRequestDto {
* 매칭 테스트 결과 분석 요청
* POST /api/v1/matches
*/
export const analyzeMatch = async (data: MatchRequestDto): Promise<MatchResult> => {
export const analyzeMatch = async (
data: MatchRequestDto,
): Promise<MatchResult> => {
try {
const response = await apiClient.post<MatchResultResponse>(
const response = await axiosInstance.post<MatchResultResponse>(
"/api/v1/matches",
data
data,
);

if (!response.data.isSuccess) {
Expand Down
6 changes: 5 additions & 1 deletion app/routes/matching/test/step2/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const SECTIONS: Array<{ key: Step2SectionKey; title: string }> = [
];

type ItemsBySection = Record<Step2SectionKey, TagItem[]>;
const EMPTY_CATEGORIES: Record<string, TagItem[]> = {};

const pickCategory = (
categories: Record<string, TagItem[]>,
Expand Down Expand Up @@ -49,7 +50,10 @@ export default function MatchingTestStep2Page() {
const fashionBody = useMatchingTestStore((s) => s.fashionBody);
const setFashionBody = useMatchingTestStore((s) => s.setFashionBody);

const categories = data?.categories ?? {};
const categories = useMemo(
() => data?.categories ?? EMPTY_CATEGORIES,
[data?.categories],
);

const itemsBySection = useMemo((): ItemsBySection => {
return {
Expand Down
41 changes: 33 additions & 8 deletions app/routes/matching/test/step3/step3-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type Props = {
};

type Sheet = null | "snsUrl" | "gender" | "ageGroup" | "videoLength" | "views";
const EMPTY_TAGS: TagItem[] = [];

const namesByIds = (ids: number[], options: TagItem[]) =>
options.filter((o) => ids.includes(o.id)).map((o) => o.name);
Expand Down Expand Up @@ -82,15 +83,39 @@ export default function MatchingTestStep3Content({
const open = (s: Sheet) => setSheet(s);
const close = () => setSheet(null);

const genderOptions = contentTags?.viewerGenders ?? [];
const ageOptions = contentTags?.viewerAges ?? [];
const videoLenOptions = contentTags?.avgVideoLengths ?? [];
const viewsOptions = contentTags?.avgVideoViews ?? [];
const genderOptions = useMemo(
() => contentTags?.viewerGenders ?? EMPTY_TAGS,
[contentTags?.viewerGenders],
);
const ageOptions = useMemo(
() => contentTags?.viewerAges ?? EMPTY_TAGS,
[contentTags?.viewerAges],
);
const videoLenOptions = useMemo(
() => contentTags?.avgVideoLengths ?? EMPTY_TAGS,
[contentTags?.avgVideoLengths],
);
const viewsOptions = useMemo(
() => contentTags?.avgVideoViews ?? EMPTY_TAGS,
[contentTags?.avgVideoViews],
);

const typeOptions = contentTags?.categories ?? [];
const toneOptions = contentTags?.tones ?? [];
const involvementOptions = contentTags?.involvements ?? [];
const coverageOptions = contentTags?.usageRanges ?? [];
const typeOptions = useMemo(
() => contentTags?.categories ?? EMPTY_TAGS,
[contentTags?.categories],
);
const toneOptions = useMemo(
() => contentTags?.tones ?? EMPTY_TAGS,
[contentTags?.tones],
);
const involvementOptions = useMemo(
() => contentTags?.involvements ?? EMPTY_TAGS,
[contentTags?.involvements],
);
const coverageOptions = useMemo(
() => contentTags?.usageRanges ?? EMPTY_TAGS,
[contentTags?.usageRanges],
);

const genderValue = useMemo(
() => joinNames(step3Selected.gender, genderOptions),
Expand Down