From c8fef26519298a15e6165f2069d9775468fa8bce Mon Sep 17 00:00:00 2001 From: Tekiter Date: Fri, 28 Apr 2023 21:06:57 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20TypedAxios=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20(#674)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Endpoint 객체 생성 * feat: 에러 발생 추가 * feat: makers 엔드포인트 적용 * feat: 검색 엔드포인트에 적용 * refactor: 폴더 추가 * feat: 코드리뷰 반영 --- api/endpoint/makers/index.ts | 31 +++++++++++++ api/endpoint/members/index.ts | 22 +++++++++ api/endpoint_LEGACY/hooks/members.ts | 4 +- api/typedAxios.ts | 46 +++++++++++++++++++ .../upload/hooks/useGetMembersByNameQuery.ts | 4 +- pages/makers/index.tsx | 5 +- 6 files changed, 105 insertions(+), 7 deletions(-) create mode 100644 api/endpoint/makers/index.ts create mode 100644 api/endpoint/members/index.ts create mode 100644 api/typedAxios.ts diff --git a/api/endpoint/makers/index.ts b/api/endpoint/makers/index.ts new file mode 100644 index 000000000..e42ad3721 --- /dev/null +++ b/api/endpoint/makers/index.ts @@ -0,0 +1,31 @@ +import { z } from 'zod'; + +import { createEndpoint } from '@/api/typedAxios'; + +export const getMakersProfile = createEndpoint({ + request: { + method: 'GET', + url: `makers/profile`, + }, + serverResponseScheme: z.array( + z.object({ + id: z.number(), + name: z.string(), + profileImage: z.string().nullable(), + activities: z.array( + z.object({ + id: z.number(), + generation: z.number(), + }), + ), + careers: z.array( + z.object({ + id: z.number(), + companyName: z.string(), + title: z.string(), + isCurrent: z.boolean(), + }), + ), + }), + ), +}); diff --git a/api/endpoint/members/index.ts b/api/endpoint/members/index.ts new file mode 100644 index 000000000..e68ecfd85 --- /dev/null +++ b/api/endpoint/members/index.ts @@ -0,0 +1,22 @@ +import { z } from 'zod'; + +import { createEndpoint } from '@/api/typedAxios'; + +export const getMembersSearchByName = createEndpoint({ + request: (name: string) => ({ + method: 'GET', + url: `api/v1/members/search?name=${encodeURIComponent(name)}`, + }), + serverResponseScheme: z.array( + z.object({ + id: z.number(), + name: z.string(), + generation: z.number(), + hasProfile: z.boolean(), + profileImage: z + .string() + .nullable() + .transform((str) => str ?? ''), + }), + ), +}); diff --git a/api/endpoint_LEGACY/hooks/members.ts b/api/endpoint_LEGACY/hooks/members.ts index ee2088905..ab26993a5 100644 --- a/api/endpoint_LEGACY/hooks/members.ts +++ b/api/endpoint_LEGACY/hooks/members.ts @@ -1,11 +1,11 @@ import { useMutation, useQuery, UseQueryOptions } from '@tanstack/react-query'; import { AxiosError } from 'axios'; +import { getMembersSearchByName } from '@/api/endpoint/members'; import { getMemberOfMe, getMemberProfileById, getMemberProfileOfMe, - getMembersSearchByName, postMemberCoffeeChat, } from '@/api/endpoint_LEGACY/members'; import { PostMemberCoffeeChatVariables, ProfileDetail } from '@/api/endpoint_LEGACY/members/type'; @@ -74,7 +74,7 @@ export const useGetMembersSearchByName = (name: string) => { return useQuery( ['getMembersSearchByName', name], async () => { - const data = await getMembersSearchByName(name); + const data = await getMembersSearchByName.request(name); return data; }, { diff --git a/api/typedAxios.ts b/api/typedAxios.ts new file mode 100644 index 000000000..23982436e --- /dev/null +++ b/api/typedAxios.ts @@ -0,0 +1,46 @@ +import { AxiosRequestConfig } from 'axios'; +import { z } from 'zod'; + +import { axiosInstance } from '@/api'; + +interface Endpoint { + request(...params: Params): Promise; +} + +export function createEndpoint< + Validator extends z.ZodType, + Param extends unknown[] = [], + Transformed = z.infer, +>(config: { + request: AxiosRequestConfig | ((...params: Param) => AxiosRequestConfig); + serverResponseScheme: Validator; + transformer?: (original: z.infer) => Transformed; +}): Endpoint { + return { + async request(...params) { + const getConfig = () => { + if (typeof config.request === 'function') { + return config.request(...params); + } + return config.request; + }; + + const axiosConfig = getConfig(); + + const { data } = await axiosInstance.request(axiosConfig); + + const res = config.serverResponseScheme.safeParse(data); + + if (!res.success) { + const zodError = String(res.error).slice(0, 1000) + '\n...'; + const message = `서버 타입 검증에 실패했습니다. (${axiosConfig.method} ${axiosConfig.url})\n${zodError}`; + console.error(zodError); + throw new Error(message); + } + + return res.data; + }, + }; +} + +export type GetResponseType = T extends Endpoint ? R : never; diff --git a/components/projects/upload/hooks/useGetMembersByNameQuery.ts b/components/projects/upload/hooks/useGetMembersByNameQuery.ts index 124ebcaef..361e76253 100644 --- a/components/projects/upload/hooks/useGetMembersByNameQuery.ts +++ b/components/projects/upload/hooks/useGetMembersByNameQuery.ts @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query'; -import { getMembersSearchByName } from '@/api/endpoint_LEGACY/members'; +import { getMembersSearchByName } from '@/api/endpoint/members'; interface GetMembersByNameQueryVariables { name: string; } @@ -12,7 +12,7 @@ const useGetMembersByNameQuery = (variables: GetMembersByNameQueryVariables) => if (!name) { return; } - const data = await getMembersSearchByName(name); + const data = await getMembersSearchByName.request(name); return data; }, { diff --git a/pages/makers/index.tsx b/pages/makers/index.tsx index 613d0fb5c..d0930315a 100644 --- a/pages/makers/index.tsx +++ b/pages/makers/index.tsx @@ -3,7 +3,7 @@ import { GetStaticProps } from 'next'; import { FC } from 'react'; import { RemoveScroll } from 'react-remove-scroll'; -import { getMakersProfile } from '@/api/endpoint_LEGACY/makers'; +import { getMakersProfile } from '@/api/endpoint/makers'; import Footer from '@/components/common/Footer'; import SwitchableHeader from '@/components/common/Header/SwitchableHeader'; import AboutMakers from '@/components/makers/AboutMakers'; @@ -32,11 +32,10 @@ const MakersPage: FC = ({ memberMetadataList }) => { }; export const getStaticProps: GetStaticProps = async () => { - const memberList = await getMakersProfile(); + const memberList = await getMakersProfile.request(); const memberMetadataList = memberList.map((member) => { const sortedCareers = member.careers.filter((career) => career.isCurrent); - sortedCareers.sort((a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime()); const currentCompany = sortedCareers.length > 0 ? sortedCareers.at(-1)?.companyName ?? null : null; const generations = member.activities.map((value) => value.generation).sort((a, b) => a - b);