diff --git a/.husky/prepare-commit-msg b/.husky/prepare-commit-msg index 9b8879a1..6af55186 100755 --- a/.husky/prepare-commit-msg +++ b/.husky/prepare-commit-msg @@ -5,7 +5,7 @@ commit_msg_file=$1 commit_msg=$(cat "$1") second_line=$(echo "$commit_msg" | sed -n '2p') -commit_msg_title_regex='(feat|fix|refactor|chore|test|docs|style|rename|setting|remove|build): .{1,100}?$' +commit_msg_title_regex='(feat|fix|refactor|chore|test|docs|style|rename|setting|remove|build): .{1,100}?' # 제목 diff --git a/app/business/services/user/user.command.ts b/app/business/services/user/user.command.ts index a37ce90b..72cc370c 100644 --- a/app/business/services/user/user.command.ts +++ b/app/business/services/user/user.command.ts @@ -9,7 +9,7 @@ import { UserDeleteRequestBody, ResetPasswordRequestBody, } from './user.type'; -import { httpErrorHandler } from '@/app/utils/http/http-error-handler'; +import { fetchAxErrorHandler, httpErrorHandler } from '@/app/utils/http/http-error-handler'; import { BadRequestError, UnauthorizedError } from '@/app/utils/http/http-error'; import { SignUpFormSchema, @@ -23,6 +23,8 @@ import { cookies } from 'next/headers'; import { isValidation } from '@/app/utils/zod/validation.util'; import { redirect } from 'next/navigation'; import { fetchUser } from './user.query'; +import fetchAX from 'fetch-ax'; +import { instance } from '@/app/utils/api/instance'; function deleteCookies() { cookies().delete('accessToken'); @@ -41,21 +43,10 @@ export async function deleteUser(prevState: FormState, formData: FormData): Prom password: formData.get('password') as string, }; - const response = await fetch(`${API_PATH.user}/me`, { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${cookies().get('accessToken')?.value}`, - }, - body: JSON.stringify(body), + await instance.delete(`${API_PATH.user}/me`, { + data: body, }); - - if (response.status !== 200) { - const result = await response.json(); - httpErrorHandler(response, result); - } } catch (error) { - console.log(error); if (error instanceof BadRequestError) { // 잘못된 요청 처리 로직 return { @@ -91,27 +82,16 @@ export async function authenticate(prevState: FormState, formData: FormData): Pr const body: SignInRequestBody = { ...validatedFields.data, }; - try { - const response = await fetch(`${API_PATH.auth}/sign-in`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(body), - }); - - const result = await response.json(); - - httpErrorHandler(response, result); + const { data } = await instance.post(`${API_PATH.auth}/sign-in`, body); - if (isValidation(result, SignInResponseSchema)) { - cookies().set('accessToken', result.accessToken, { + if (isValidation(data, SignInResponseSchema)) { + cookies().set('accessToken', data.accessToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', path: '/', }); - cookies().set('refreshToken', result.refreshToken, { + cookies().set('refreshToken', data.refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', path: '/', @@ -154,20 +134,18 @@ export async function authenticate(prevState: FormState, formData: FormData): Pr export async function refreshToken(): Promise { const refreshToken = cookies().get('refreshToken')?.value; try { - const response = await fetch(`${API_PATH.auth}/token`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', + const { data } = await fetchAX.post( + `${API_PATH.auth}/token`, + { refreshToken }, + { + responseRejectedInterceptor: (error) => { + fetchAxErrorHandler(error); + }, }, - body: JSON.stringify({ refreshToken }), - }); - - const result = await response.json(); + ); - httpErrorHandler(response, result); - - if (isValidation(result, ValidateTokenResponseSchema)) { - return result; + if (isValidation(data, ValidateTokenResponseSchema)) { + return data; } else { throw 'Invalid token response schema.'; } @@ -207,18 +185,9 @@ export async function createUser(prevState: FormState, formData: FormData): Prom }; try { - const response = await fetch(`${API_PATH.user}/sign-up`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(body), + await instance.post(`${API_PATH.user}/sign-up`, body, { + responseType: 'text', }); - - if (response.status !== 200) { - const result = await response.json(); - httpErrorHandler(response, result); - } } catch (error) { if (error instanceof BadRequestError) { // 잘못된 요청 처리 로직 @@ -263,12 +232,8 @@ export async function resetPassword(prevState: FormState, formData: FormData): P passwordCheck, }; try { - const response = await fetch(`${API_PATH.user}/password`, { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(body), + await instance.patch(`${API_PATH.user}/password`, body, { + responseType: 'text', }); return { diff --git a/app/business/services/user/user.query.ts b/app/business/services/user/user.query.ts index 81ae96d1..feab0b4e 100644 --- a/app/business/services/user/user.query.ts +++ b/app/business/services/user/user.query.ts @@ -1,9 +1,7 @@ 'use server'; import { BadRequestError, UnauthorizedError } from '@/app/utils/http/http-error'; -import { httpErrorHandler } from '@/app/utils/http/http-error-handler'; import { isValidation } from '@/app/utils/zod/validation.util'; -import { cookies } from 'next/headers'; import { API_PATH } from '../../api-path'; import { InitUserInfoResponse, UserInfoResponse } from './user.type'; import { @@ -13,6 +11,7 @@ import { FindIdResponseSchema, } from './user.validation'; import { FormState } from '@/app/ui/view/molecule/form/form-root'; +import { instance } from '@/app/utils/api/instance'; export async function auth(): Promise { try { @@ -28,17 +27,10 @@ export async function auth(): Promise { try { - const response = await fetch(`${API_PATH.user}/me`, { - headers: { - Authorization: `Bearer ${cookies().get('accessToken')?.value}`, - }, - }); - const result = await response.json(); + const { data } = await instance.get(`${API_PATH.user}/me`); - httpErrorHandler(response, result); - - if (isValidation(result, UserInfoResponseSchema) || isValidation(result, InitUserInfoResponseSchema)) { - return result; + if (isValidation(data, UserInfoResponseSchema) || isValidation(data, InitUserInfoResponseSchema)) { + return data; } else { throw 'Invalid user info response schema.'; } @@ -63,28 +55,22 @@ export async function findUserToStudentNumber(prevState: FormState, formData: Fo try { const { studentNumber } = validatedFields.data; - const response = await fetch(`${API_PATH.user}/${studentNumber}/auth-id`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); + const response = await instance.get(`${API_PATH.user}/${studentNumber}/auth-id`); - const result = await response.json(); if (response.status === 200) return { isSuccess: true, isFailure: false, validationError: {}, message: '', - value: result, + value: response.data, }; else return { isSuccess: false, isFailure: true, validationError: {}, - message: result.message, + message: response.data, }; } catch (error) { if (error instanceof BadRequestError) { @@ -92,7 +78,7 @@ export async function findUserToStudentNumber(prevState: FormState, formData: Fo isSuccess: false, isFailure: true, validationError: {}, - message: error.message, + message: '해당 사용자를 찾을 수 없습니다.', }; } else { throw error; @@ -117,15 +103,9 @@ export async function validateUser(prevState: FormState, formData: FormData): Pr try { const { studentNumber, authId } = validatedFields.data; - const response = await fetch(`${API_PATH.user}/${studentNumber}/validate?auth-id=${authId}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); - const result = await response.json(); + const { data } = await instance.get(`${API_PATH.user}/${studentNumber}/validate?auth-id=${authId}`); - if (result.passedUserValidation) + if (data.passedUserValidation) return { isSuccess: true, isFailure: false, diff --git a/app/ui/user/sign-up-form/sign-up-form.stories.tsx b/app/ui/user/sign-up-form/sign-up-form.stories.tsx index ea609777..ff48dfad 100644 --- a/app/ui/user/sign-up-form/sign-up-form.stories.tsx +++ b/app/ui/user/sign-up-form/sign-up-form.stories.tsx @@ -90,7 +90,7 @@ export const FailureSenarioWithDuplicatedStudentNumber: Story = { await step('회원가입에 실패한다.', async () => { await waitFor(() => { expect(args.onSuccess).not.toHaveBeenCalled(); - expect(canvas.getByText('이미 가입된 학번입니다.')).toBeInTheDocument(); + expect(canvas.getByText('Bad Request')).toBeInTheDocument(); }); }); }, diff --git a/app/utils/api/instance.ts b/app/utils/api/instance.ts new file mode 100644 index 00000000..037e70bd --- /dev/null +++ b/app/utils/api/instance.ts @@ -0,0 +1,22 @@ +import fetchAx, { FetchAxError } from 'fetch-ax'; +import { cookies } from 'next/headers'; +import { fetchAxErrorHandler } from '../http/http-error-handler'; + +export const instance = fetchAx.create({ + headers: { + 'Content-Type': 'application/json', + }, + responseRejectedInterceptor: (error) => { + fetchAxErrorHandler(error); + }, + requestInterceptor: (config) => { + const accessToken = cookies().get('accessToken')?.value; + if (accessToken) { + config.headers = { + ...config.headers, + Authorization: `Bearer ${accessToken}`, + }; + } + return config; + }, +}); diff --git a/app/utils/api/setup-url.util.ts b/app/utils/api/setup-url.util.ts index f990052b..1bd67792 100644 --- a/app/utils/api/setup-url.util.ts +++ b/app/utils/api/setup-url.util.ts @@ -1,6 +1,6 @@ const API_URLS = { - BASE_URL: process.env.NEXT_PUBLIC_API_PATH ?? "", - PARSE_API_URL: process.env.NEXT_PUBLIC_PARSE_API_PATH ?? "", + BASE_URL: process.env.NEXT_PUBLIC_API_PATH ?? 'http://localhost:9090', + PARSE_API_URL: process.env.NEXT_PUBLIC_PARSE_API_PATH ?? 'http://localhost:9090/parsePDFtoText', }; const MOCK_API_URLS = { diff --git a/app/utils/http/http-error-handler.ts b/app/utils/http/http-error-handler.ts index 3a86eef4..5f3a5a46 100644 --- a/app/utils/http/http-error-handler.ts +++ b/app/utils/http/http-error-handler.ts @@ -7,6 +7,7 @@ import { UnauthorizedError, InternetServerError, } from './http-error'; +import { FetchAxError } from 'fetch-ax'; export interface ErrorResponseData { status: number; @@ -54,3 +55,48 @@ export const httpErrorHandler = (response: Response, result: ErrorResponseData) } } }; + +interface ErrorData { + errorCode: string; +} + +export const fetchAxErrorHandler = (error: FetchAxError) => { + const status = error.statusCode; + const message = error.response.data.errorCode; // 여기서 errorcode를 message로 변환 필요 + const response = error.response; + + switch (status) { + case HttpStatusCode.Unauthorized: + throw new UnauthorizedError({ + message, + response, + }); + + case HttpStatusCode.BadRequest: + throw new BadRequestError({ + message, + response, + }); + + case HttpStatusCode.Forbidden: + throw new ForbiddenError({ + message, + response, + }); + + case HttpStatusCode.NotFound: + throw new NotFoundError({ + message, + response, + }); + + case HttpStatusCode.InternalServerError: + throw new InternetServerError({ + message, + response, + }); + + default: + throw new HttpError(status, message, response); + } +}; diff --git a/app/utils/http/http-error.ts b/app/utils/http/http-error.ts index a605703a..31fec692 100644 --- a/app/utils/http/http-error.ts +++ b/app/utils/http/http-error.ts @@ -1,7 +1,7 @@ type ErrorConstrutor = { message?: string; statusCode?: number; - response?: Response; + response?: Partial; }; // Refactor: fetch가 NetworkError랑 TimeoutError 또 자동으로 에러로 던져서 처리가 더 애매하다. @@ -23,7 +23,7 @@ export class HttpError extends Error { constructor( readonly statusCode?: number, message?: string, - readonly response?: Response, + readonly response?: Partial, ) { super(message); } diff --git a/package-lock.json b/package-lock.json index 7eb16d7a..508228b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "express": "^4.18.2", + "fetch-ax": "^1.0.10", "jotai": "^2.7.0", "lucide-react": "^0.336.0", "next": "14.1.0", @@ -12757,6 +12758,11 @@ "pend": "~1.2.0" } }, + "node_modules/fetch-ax": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fetch-ax/-/fetch-ax-1.0.10.tgz", + "integrity": "sha512-9qXCkrOBDDNodVQmZJFQ3b3A3J6OMr6XFFQXgFuXiwEmcifxZPx2GR80++OVbF7M5EjkgQnZYiiwDOsYHuBoXQ==" + }, "node_modules/fetch-retry": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-5.0.6.tgz", diff --git a/package.json b/package.json index 68e0cc1b..66ab23e6 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "express": "^4.18.2", + "fetch-ax": "^1.0.10", "jotai": "^2.7.0", "lucide-react": "^0.336.0", "next": "14.1.0",