diff --git a/app/api/axios.ts b/app/api/axios.ts index 99ed9f4..37cc1c7 100644 --- a/app/api/axios.ts +++ b/app/api/axios.ts @@ -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) { diff --git a/app/routes/matching/api/matching.ts b/app/routes/matching/api/matching.ts index dd03f74..46d8c31 100644 --- a/app/routes/matching/api/matching.ts +++ b/app/routes/matching/api/matching.ts @@ -1,4 +1,4 @@ -import { apiClient } from "../../../api/axios"; +import { axiosInstance } from "../../../api/axios"; import { tokenStorage } from "../../../lib/token"; // 매칭 분석 결과 조회 응답 타입 @@ -41,13 +41,16 @@ export const getMatchAnalysis = async (): Promise => { throw new MatchingTestRequiredError(); } - const response = await apiClient.get(`/api/v1/matches`); + const response = + await axiosInstance.get(`/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 || "매칭 분석 결과 조회 실패"); @@ -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(); @@ -213,7 +216,7 @@ export const getMatchingCampaigns = async ( sortBy, category, page, - size + size, }; if (tags && tags.length > 0) { @@ -224,16 +227,18 @@ export const getMatchingCampaigns = async ( params.keyword = keyword; } - const response = await apiClient.get( + const response = await axiosInstance.get( `/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 || "캠페인 목록 조회 실패"); @@ -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(); } @@ -288,7 +297,7 @@ 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(); @@ -296,20 +305,25 @@ export const getMatchingBrands = async ( throw new MatchingTestRequiredError(); } - const params: Record = { sortBy, category }; + const params: Record = { + sortBy, + category, + }; if (tags && tags.length > 0) { params.tags = tags; } - const response = await apiClient.get( + const response = await axiosInstance.get( `/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 || "브랜드 목록 조회 실패"); @@ -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(); } @@ -353,7 +371,9 @@ export const getMatchingBrands = async ( */ export const getBrandFilters = async (): Promise => { try { - const response = await apiClient.get('/api/v1/brands/filters'); + const response = await axiosInstance.get( + "/api/v1/brands/filters", + ); if (!response.data.isSuccess) { throw new Error(response.data.message || "브랜드 필터 조회 실패"); @@ -380,9 +400,9 @@ interface BrandLikeResponse { export const toggleBrandLike = async (brandId: number): Promise => { try { - const response = await apiClient.post( + const response = await axiosInstance.post( `/api/v1/brands/${brandId}/like`, - {} // 빈 객체 body 추가 + {}, // 빈 객체 body 추가 ); if (!response.data.isSuccess) { @@ -402,14 +422,15 @@ export const toggleBrandLike = async (brandId: number): Promise => { * @param category 카테고리 (BEAUTY, FASHION 등) * @returns 태그 이름 배열 */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export const getTagNamesByCategory = async (_category: string): Promise => { +export const getTagNamesByCategory = async ( + _category: string, +): Promise => { + void _category; // 카테고리별로 기본 태그를 반환하여 모든 태그 포함 // 실제로는 API에서 카테고리별 태그를 가져와야 할 수 있음 return []; }; - // 캠페인 제안 요청 타입 export interface CreateCampaignProposalRequest { brandId: number; @@ -441,11 +462,13 @@ interface CreateCampaignProposalResponse { /** * 캠페인 제안하기 (역제안) */ -export const createCampaignProposal = async (data: CreateCampaignProposalRequest): Promise => { +export const createCampaignProposal = async ( + data: CreateCampaignProposalRequest, +): Promise => { try { - const response = await apiClient.post( + const response = await axiosInstance.post( "/api/v1/campaigns/proposal", - data + data, ); if (!response.data.isSuccess) { @@ -503,11 +526,13 @@ export interface MatchRequestDto { * 매칭 테스트 결과 분석 요청 * POST /api/v1/matches */ -export const analyzeMatch = async (data: MatchRequestDto): Promise => { +export const analyzeMatch = async ( + data: MatchRequestDto, +): Promise => { try { - const response = await apiClient.post( + const response = await axiosInstance.post( "/api/v1/matches", - data + data, ); if (!response.data.isSuccess) { diff --git a/app/routes/matching/test/step2/route.tsx b/app/routes/matching/test/step2/route.tsx index 126fa31..6fc9bb3 100644 --- a/app/routes/matching/test/step2/route.tsx +++ b/app/routes/matching/test/step2/route.tsx @@ -15,6 +15,7 @@ const SECTIONS: Array<{ key: Step2SectionKey; title: string }> = [ ]; type ItemsBySection = Record; +const EMPTY_CATEGORIES: Record = {}; const pickCategory = ( categories: Record, @@ -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 { diff --git a/app/routes/matching/test/step3/step3-content.tsx b/app/routes/matching/test/step3/step3-content.tsx index 8d8d0b3..fc3d791 100644 --- a/app/routes/matching/test/step3/step3-content.tsx +++ b/app/routes/matching/test/step3/step3-content.tsx @@ -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); @@ -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),