From 8af0cf5a44c9d2311f1e6e1b592227077b5243ad Mon Sep 17 00:00:00 2001 From: sohot8653 Date: Fri, 29 Aug 2025 10:32:21 +0900 Subject: [PATCH] =?UTF-8?q?[API=20=EA=B0=9C=EC=84=A0]=20useApi=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20=EB=B0=8F=20?= =?UTF-8?q?API=20=ED=94=8C=EB=9F=AC=EA=B7=B8=EC=9D=B8=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=EB=A1=9C=20API=20=ED=98=B8=EC=B6=9C=20=EB=B0=A9=EC=8B=9D=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- composables/useApi.ts | 70 +++++++++++++++++++++---------------------- plugins/api.ts | 44 +++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 35 deletions(-) create mode 100644 plugins/api.ts diff --git a/composables/useApi.ts b/composables/useApi.ts index 05d2b6a..ce66a77 100644 --- a/composables/useApi.ts +++ b/composables/useApi.ts @@ -1,36 +1,36 @@ -import { useRuntimeConfig } from "#imports"; - -export const useApi = async ( - path: string, - options: { - method?: "get" | "post" | "put" | "delete"; - body?: any; - query?: Record; - headers?: HeadersInit; - credentials?: RequestCredentials; - } = {} -): Promise => { - const config = useRuntimeConfig(); - const method = options.method ? options.method.toUpperCase() : "GET"; - - try { - const response = await $fetch( - `${config.public.apiBase}${config.public.contextPath}${path}`, - { - method: method as any, - body: options.body, - query: options.query, - credentials: options.credentials || "include", // 쿠키 자동 전송 - headers: { - "Content-Type": "application/json", - ...options.headers, - }, - } - ); - - return response; - } catch (error) { - console.error("API 호출 실패:", error); - throw error; - } +/** + * API 호출을 위한 편의 함수 + * + * @template T - 응답 데이터의 타입 + * @param path - API 엔드포인트 경로 (예: '/users', '/users/1') + * @param opts - 요청 옵션 (method, body, headers 등) + * @returns Promise - API 응답 데이터 + * + * @example + * // GET 요청 + * const users = await useApi('/users') + * + * // POST 요청 + * const newUser = await useApi('/users', { + * method: 'POST', + * body: { name: 'John', email: 'john@example.com' } + * }) + * + * // PUT 요청 + * const updatedUser = await useApi('/users/1', { + * method: 'PUT', + * body: { name: 'John Updated' } + * }) + * + * // DELETE 요청 + * await useApi('/users/1', { method: 'DELETE' }) + * + * // FormData 업로드 + * const formData = new FormData() + * formData.append('file', file) + * await useApi('/upload', { method: 'POST', body: formData }) + */ +export const useApi = (path: string, opts?: any): Promise => { + const { $api } = useNuxtApp(); + return ($api as any)(path, opts); }; diff --git a/plugins/api.ts b/plugins/api.ts new file mode 100644 index 0000000..79093ca --- /dev/null +++ b/plugins/api.ts @@ -0,0 +1,44 @@ +export default defineNuxtPlugin(() => { + const config = useRuntimeConfig(); + const baseURL = `${config.public.apiBase}${config.public.contextPath}`; + + const api = $fetch.create({ + baseURL, + credentials: "include", + onRequest({ request, options }) { + // 1) GET/HEAD가 아니면 body만 넣기 (GET에 body 금지) + const method = (options.method ?? "GET").toUpperCase(); + if (method === "GET" || method === "HEAD") { + delete (options as any).body; + } + + // 2) FormData면 Content-Type 자동 지정 금지 + const isFormData = + typeof FormData !== "undefined" && options.body instanceof FormData; + options.headers = { + ...(isFormData ? {} : { "Content-Type": "application/json" }), + ...(options.headers || {}), + }; + + // 3) SSR 쿠키 포워딩 + if (import.meta.server) { + const cookie = useRequestHeaders(["cookie"])?.cookie; + // request가 절대 URL이면 호스트 비교 + const reqUrl = typeof request === "string" ? request : String(request); + const isBackendApi = + !reqUrl.startsWith("http") || // 상대경로면 내 API + reqUrl.startsWith(baseURL); // 혹은 baseURL과 동일 + + if (cookie && isBackendApi) { + options.headers = { ...(options.headers || {}), cookie } as any; + } + } + }, + onResponseError({ response }) { + // 공통 로깅 + console.error("[API ERROR]", response.status, response._data); + }, + }); + + return { provide: { api } }; +});