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
108 changes: 0 additions & 108 deletions src/shared/apis/base/baseFetcher.ts

This file was deleted.

109 changes: 109 additions & 0 deletions src/shared/apis/base/coreFetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { isRecord } from '@/shared/utils/errorGuards';

type ApiError = Error & {
status: number;
code?: string;
};

type AbortError = {
name: 'AbortError';
};

export const isAbortError = (error: unknown): error is AbortError => {
if (!isRecord(error)) {
return false;
}

return error.name === 'AbortError';
};

/**
* ### coreFetch
*
* @description
* URL과 RequestInit 옵션을 받아 fetch 요청을 실행하는 공통 실행기 함수입니다.
*
* @error
* - 요청이 abort된 경우(시간 초과 또는 외부 취소), Error를 throw 합니다.
* - HTTP 상태 코드가 2xx가 아닌 경우, status와 code를 포함한 ApiError를 throw 합니다.
*
* @param url
* - 완성된 요청 URL
* @param options
* - fetch에 전달할 RequestInit 옵션
* @param timeoutMs
* - 요청 제한 시간(ms), 기본값은 10초
*
* @returns
* 응답이 204(No Content)인 경우 undefined를 반환하고 그 외는 JSON 파싱 데이터 반환
*/
export const coreFetch = async <T>(
url: string,
options: RequestInit = {},
timeoutMs: number = 10_000
): Promise<T> => {
const isFormData = typeof FormData !== 'undefined' && options.body instanceof FormData;

const request = async (): Promise<Response> => {
const controller = new AbortController();
const headers = new Headers(options.headers);
let abortedByTimeout = false;

if (isFormData) {
headers.delete('Content-Type');
} else if (!headers.has('Content-Type')) {
headers.set('Content-Type', 'application/json');
}

let abortHandler: (() => void) | undefined;

if (options.signal) {
abortHandler = () => controller.abort();

if (options.signal.aborted) {
controller.abort();
} else {
options.signal.addEventListener('abort', abortHandler);
}
}

const timeoutId = setTimeout(() => {
abortedByTimeout = true;
controller.abort();
}, timeoutMs);

try {
const response = await fetch(url, {
...options,
signal: controller.signal,
headers,
});
return response;
} catch (error) {
if (isAbortError(error)) {
if (abortedByTimeout) {
throw new Error('요청 시간이 초과되었습니다. 다시 시도해주세요.');
}
throw new Error('요청이 취소되었습니다.');
}
throw error;
} finally {
clearTimeout(timeoutId);
if (options.signal && abortHandler) {
options.signal.removeEventListener('abort', abortHandler);
}
}
};

const response = await request();

if (!response.ok) {
const errorData: { message?: string; code?: string } = await response.json().catch(() => ({}));
const error = new Error(errorData.message || 'API 요청 중 오류가 발생했습니다.') as ApiError;
error.status = response.status;
error.code = errorData.code;

throw error;
}
return response.status === 204 ? (undefined as T) : response.json();
};
34 changes: 34 additions & 0 deletions src/shared/apis/base/publicFetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { coreFetch } from '@/shared/apis/base/coreFetch';

/**
* ### publicFetch
*
* @description
* - 비인증(public) API 호출을 위한 fetch 래퍼 함수입니다.
*
* @param endpoint
* - `/`로 시작하는 백엔드 API 엔드포인트
*
* @param options
* - fetch에 전달할 RequestInit 옵션
*
* @param timeoutMs
* - 요청 제한 시간(ms)
* - 지정하지 않으면 coreFetch의 기본 timeout을 사용합니다.
*
* @returns
* JSON 파싱된 응답 데이터
*/
export const publicFetch = async <T>(
endpoint: string,
options: RequestInit = {},
timeoutMs?: number
): Promise<T> => {
const BASE_URL = process.env.NEXT_PUBLIC_API_URL;
if (!BASE_URL) {
throw new Error('NEXT_PUBLIC_API_URL 환경 변수가 설정되지 않았습니다.');
}
const url = BASE_URL + endpoint;

return coreFetch<T>(url, options, timeoutMs);
};
16 changes: 8 additions & 8 deletions src/shared/apis/feature/activities.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { baseFetcher } from '@/shared/apis/base/baseFetcher';
import { publicFetch } from '@/shared/apis/base/publicFetch';
import type {
CreateActivityBodyDto,
CreateReservationBodyDto,
Expand All @@ -16,7 +16,7 @@ import { createQueryString } from '@/shared/utils/createQueryString';
*/
export const getActivities = (params: GetActivitiesParams) => {
const queryString = createQueryString(params);
return baseFetcher(`/activities${queryString}`, { method: 'GET' });
return publicFetch(`/activities${queryString}`, { method: 'GET' });
};

/**
Expand All @@ -26,7 +26,7 @@ export const getActivities = (params: GetActivitiesParams) => {
* @returns 체험 등록 API 응답 Promise
*/
export const createActivity = (data: CreateActivityBodyDto) => {
return baseFetcher(`/activities`, { method: 'POST', body: JSON.stringify(data) });
return publicFetch(`/activities`, { method: 'POST', body: JSON.stringify(data) });
};

/**
Expand All @@ -36,7 +36,7 @@ export const createActivity = (data: CreateActivityBodyDto) => {
* @returns 체험 상세 조회 API 응답 Promise
*/
export const getActivityDetail = (activityId: number) => {
return baseFetcher(`/activities/${activityId}`, { method: 'GET' });
return publicFetch(`/activities/${activityId}`, { method: 'GET' });
};

/**
Expand All @@ -48,7 +48,7 @@ export const getActivityDetail = (activityId: number) => {
*/
export const getActivitySchedules = (activityId: number, params: GetActivitySchedulesParams) => {
const queryString = createQueryString(params);
return baseFetcher(`/activities/${activityId}/available-schedule${queryString}`, {
return publicFetch(`/activities/${activityId}/available-schedule${queryString}`, {
method: 'GET',
});
};
Expand All @@ -62,7 +62,7 @@ export const getActivitySchedules = (activityId: number, params: GetActivitySche
*/
export const getActivityReviews = (activityId: number, params: GetActivityReviewsParams) => {
const queryString = createQueryString(params);
return baseFetcher(`/activities/${activityId}/reviews${queryString}`, {
return publicFetch(`/activities/${activityId}/reviews${queryString}`, {
method: 'GET',
});
};
Expand All @@ -75,7 +75,7 @@ export const getActivityReviews = (activityId: number, params: GetActivityReview
* @returns 체험 예약 신청 API 응답 Promise
*/
export const createActivityReservation = (activityId: number, data: CreateReservationBodyDto) => {
return baseFetcher(`/activities/${activityId}/reservations`, {
return publicFetch(`/activities/${activityId}/reservations`, {
method: 'POST',
body: JSON.stringify(data),
});
Expand All @@ -91,7 +91,7 @@ export const createActivityImage = (image: File) => {
const formData = new FormData();
formData.append('image', image);

return baseFetcher<{ imageUrl: string }>('/activities/images', {
return publicFetch<{ imageUrl: string }>('/activities/images', {
method: 'POST',
body: formData,
});
Expand Down
29 changes: 14 additions & 15 deletions src/shared/apis/feature/auth.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
import { baseFetcher } from '@/shared/apis/base/baseFetcher';
import type { LoginRequest } from '@/shared/types/auth.types';
// import type { LoginRequest } from '@/shared/types/auth.types';

/**
* 로그인 API
* 로그인 API (BFF)
*
* @param data - 로그인에 필요한 사용자 정보
* @returns 로그인 API 응답 Promise
*/
export const logIn = (data: LoginRequest) => {
return baseFetcher('/auth/login', {
method: 'POST',
body: JSON.stringify(data),
});
};
// export const logIn = (data: LoginRequest) => {
// return baseFetcher('/auth/login', {
// method: 'POST',
// body: JSON.stringify(data),
// });
// };

/**
* 토큰 재발급 API
* 토큰 재발급 API (BFF)
*
* @returns 토큰 재발급 API 응답 Promise
*/
export const refreshToken = () => {
return baseFetcher('/auth/tokens', {
method: 'POST',
});
};
// export const refreshToken = () => {
// return baseFetcher('/auth/tokens', {
// method: 'POST',
// });
// };
Loading