/** * ๐Ÿ“„ API Utility * API ํ˜ธ์ถœ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํŒŒ์ผ * * - ๋‹ค์–‘ํ•œ HTTP ๋ฉ”์†Œ๋“œ์˜ API ํ˜ธ์ถœ ํ•จ์ˆ˜ (GET, POST, PUT, DELETE) * - ์ฟ ํ‚ค ๊ธฐ๋ฐ˜ ์ธ์ฆ ํ† ํฐ ๊ด€๋ฆฌ ๋ฐ ์‚ฌ์šฉ์ž ์กฐํšŒ * - ํŠน์ • ์„œ๋น„์Šค (DC, CMS, GeoServer) ํ˜ธ์ถœ ์ง€์› * - Axios ๋ฐ Fetch ๊ธฐ๋ฐ˜ HTTP ์š”์ฒญ ์ฒ˜๋ฆฌ * - ํƒ€์ž… ์•ˆ์ „์„ฑ ๋ณด์žฅ์„ ์œ„ํ•œ ์ธํ„ฐํŽ˜์ด์Šค ํ™œ์šฉ * - ์—๋Ÿฌ ํ•ธ๋“ค๋ง ๋ฐ ์‘๋‹ต ์ฒ˜๋ฆฌ ํ‘œ์ค€ํ™” */ import { useEffect, useState } from "react"; import axios from 'axios'; import { AxiosError } from 'axios'; import { CLAIM_NAME, SITE_ID } from "@/shared/config"; import { tokenManager } from "@/entities/auth/lib/token-manager"; import { getRemainingMs } from "@common/auth"; const BASE_URL = '/sewerage-service/api'; const DEFAULT_HEADERS = { "X-Site-Id": SITE_ID, }; const LOGIN_PAGE_URL = process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "/auth/login"; const withSiteIdHeaders = (headers: HeadersInit = {}) => ({ ...headers, ...DEFAULT_HEADERS, }); const resolveApiUrl = (url: string) => url.startsWith('/') ? `${url}` : `${BASE_URL}/${url}`; const buildInvalidJsonResponseError = (url: string, text: string, contentType?: string | null) => { const preview = text.trim().slice(0, 200); const responseType = contentType ? ` (${contentType})` : ""; return new Error( `JSON ์‘๋‹ต์ด ์•„๋‹Œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•˜์Šต๋‹ˆ๋‹ค${responseType}: ${url}${preview ? ` - ${preview}` : ""}`, ); }; const buildRequestError = ( fallbackMessage: string, status?: number, body?: unknown, ) => { const bodyMessage = typeof body === "object" && body !== null ? ( (body as { message?: unknown }).message ?? (body as { error?: { message?: unknown } }).error?.message ) : undefined; const message = typeof bodyMessage === "string" && bodyMessage.trim() ? bodyMessage : fallbackMessage; const error = new Error(message) as Error & { status?: number; body?: unknown }; error.status = status; error.body = body; return error; }; const handleUnauthorizedResponse = () => { if (typeof window === "undefined") return; const accessToken = tokenManager.getToken(CLAIM_NAME); const remainingMs = accessToken ? getRemainingMs(accessToken) : null; const isExpired = remainingMs === null || remainingMs <= 0; if (!isExpired) return; tokenManager.clearAuthToken(); const isLoginPage = window.location.pathname === "/auth/login" || window.location.pathname.startsWith("/auth/login/"); if (!isLoginPage) { window.location.assign(LOGIN_PAGE_URL); } }; export const getData = async (url: string): Promise => { const res = await fetch(resolveApiUrl(url), { method: "GET", headers: withSiteIdHeaders(), }); console.log(res); if (res.status === 401) { handleUnauthorizedResponse(); } if (res.ok) { try { // ์‘๋‹ต์ด ๋น„์–ด์žˆ๋Š”์ง€ ํ™•์ธ const contentLength = res.headers.get('content-length'); if (contentLength === '0') { return null; } // ์‘๋‹ต ํ…์ŠคํŠธ๋ฅผ ๋จผ์ € ๊ฐ€์ ธ์™€์„œ ๋นˆ ๋ฌธ์ž์—ด์ธ์ง€ ํ™•์ธ const text = await res.text(); if (!text || text.trim() === '') { return null; } // JSON ํŒŒ์‹ฑ ์‹œ๋„ const data = JSON.parse(text); return data.data; } catch (error) { console.error('JSON parsing error:', error); // JSON ํŒŒ์‹ฑ์— ์‹คํŒจํ•œ ๊ฒฝ์šฐ, ์›๋ณธ ํ…์ŠคํŠธ ๋˜๋Š” null ๋ฐ˜ํ™˜ throw new Error('Invalid JSON response from server'); } } const text = await res.text().catch(() => ""); let body: unknown = null; if (text.trim()) { try { body = JSON.parse(text); } catch { body = text; } } throw buildRequestError( `Failed to fetch data: ${res.status} ${res.statusText}`, res.status, body, ); }; export const postData = async (url: string, data: object): Promise => { const options: RequestInit = { method: "POST", headers: withSiteIdHeaders({ "Content-Type": "application/json", }), body: JSON.stringify(data), }; const res = await fetch(`${BASE_URL}/${url}`, options); if (res.status === 401) { handleUnauthorizedResponse(); } if (res.ok) { const resData = await res.json(); console.log('postData response:', resData); return resData.data; } }; export const postRawJson = async (url: string, data: object): Promise => { const res = await fetch(resolveApiUrl(url), { method: "POST", headers: withSiteIdHeaders({ "Content-Type": "application/json", }), body: JSON.stringify(data), }); if (res.status === 401) { handleUnauthorizedResponse(); } const text = await res.text(); const body = text && text.trim() ? JSON.parse(text) : null; if (!res.ok) { const message = body?.message ?? `Failed to post data: ${res.status} ${res.statusText}`; const error = new Error(message) as Error & { status?: number; body?: any }; error.status = res.status; error.body = body; throw error; } return body; }; /** ๊ณตํ†ต ์‘๋‹ต ๋ž˜ํผ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” JSON PUT ์š”์ฒญ์„ ์ „์†กํ•˜๊ณ  data ํ•„๋“œ๋งŒ ๋ฐ˜ํ™˜ํ•œ๋‹ค. */ export const putData = async (url: string, data: object): Promise => { const res = await fetch(resolveApiUrl(url), { method: "PUT", headers: withSiteIdHeaders({ "Content-Type": "application/json", }), body: JSON.stringify(data), }); if (res.status === 401) { handleUnauthorizedResponse(); } const text = await res.text(); let body: unknown = null; if (text.trim()) { try { body = JSON.parse(text); } catch { throw buildInvalidJsonResponseError(url, text, res.headers.get("content-type")); } } if (!res.ok) { throw buildRequestError( `Failed to update data: ${res.status} ${res.statusText}`, res.status, body, ); } if (typeof body === "object" && body !== null && "data" in body) { return (body as { data?: unknown }).data ?? null; } return body; }; const parseDownloadFileName = (contentDisposition: string | null) => { if (!contentDisposition) return undefined; const utf8Match = contentDisposition.match(/filename\*\s*=\s*UTF-8''([^;]+)/i); if (utf8Match?.[1]) { return decodeURIComponent(utf8Match[1].trim()); } const plainMatch = contentDisposition.match(/filename\s*=\s*"?([^\";]+)"?/i); return plainMatch?.[1]?.trim(); }; export const postBlobData = async ( url: string, data: object, ): Promise<{ blob: Blob; fileName?: string }> => { const res = await fetch(resolveApiUrl(url), { method: "POST", headers: withSiteIdHeaders({ "Content-Type": "application/json", }), body: JSON.stringify(data), }); if (res.status === 401) { handleUnauthorizedResponse(); } if (!res.ok) { const errorText = await res.text().catch(() => ""); throw new Error( `Failed to fetch blob: ${res.status} ${res.statusText}${errorText ? ` - ${errorText.slice(0, 200)}` : ""} (${url})`, ); } return { blob: await res.blob(), fileName: parseDownloadFileName(res.headers.get("content-disposition")), }; }; export const postMutiPart = async (url: string, data: FormData): Promise => { try { const res = await axios.post(resolveApiUrl(url), data, { headers: DEFAULT_HEADERS, timeout: 600000, // 10๋ถ„ }); console.log('postMultiPart response:', res); return res.data.data; } catch (error) { if (error instanceof AxiosError) { const responseBody = error.response?.data; const serverMessage = typeof responseBody?.message === 'string' && responseBody.message.trim() ? responseBody.message.trim() : typeof responseBody?.error?.message === 'string' && responseBody.error.message.trim() ? responseBody.error.message.trim() : typeof responseBody === 'string' && responseBody.trim() ? responseBody.trim() : null; const nextError = new Error( serverMessage ?? `Multipart request failed: ${error.response?.status ?? 'UNKNOWN'} ${error.response?.statusText ?? ''}`.trim(), ) as Error & { status?: number; body?: unknown }; nextError.status = error.response?.status; nextError.body = responseBody; throw nextError; } throw error; } }; export const deleteData = async (url: string, data?: object): Promise => { const options: RequestInit = { method: "DELETE", headers: withSiteIdHeaders({ "Content-Type": "application/json", }), body: data ? JSON.stringify(data) : undefined, }; const res = await fetch(resolveApiUrl(url), options); if (res.status === 401) { handleUnauthorizedResponse(); } if (res.status === 204) return true return res; }; // GET ์‘๋‹ต์„ ๊ณตํ†ต data wrapper ์—†์ด ์›๋ฌธ JSON์œผ๋กœ ์ฝ๋Š”๋‹ค. init์€ ์ง€๋„ bbox ์š”์ฒญ์ฒ˜๋Ÿผ AbortSignal์ด ํ•„์š”ํ•œ ํ˜ธ์ถœ์—์„œ ์‚ฌ์šฉํ•œ๋‹ค. export const getRawJson = async (url: string, init: RequestInit = {}): Promise => { const res = await fetch(resolveApiUrl(url), { ...init, method: "GET", headers: withSiteIdHeaders(init.headers), }); if (res.status === 401) { handleUnauthorizedResponse(); } if (!res.ok) { const errorText = await res.text().catch(() => ""); throw new Error( `Failed to fetch data: ${res.status} ${res.statusText}${errorText ? ` - ${errorText.slice(0, 200)}` : ""}`, ); } const contentLength = res.headers.get('content-length'); if (contentLength === '0') { return null; } const text = await res.text(); if (!text || text.trim() === '') { return null; } const trimmed = text.trim(); if (trimmed.startsWith("<")) { throw buildInvalidJsonResponseError(url, text, res.headers.get("content-type")); } try { return JSON.parse(text); } catch { throw buildInvalidJsonResponseError(url, text, res.headers.get("content-type")); } }; export const getBlobData = async (url: string): Promise => { const res = await fetch(resolveApiUrl(url), { method: "GET", headers: withSiteIdHeaders(), }); if (res.status === 401) { handleUnauthorizedResponse(); } if (!res.ok) { const errorText = await res.text().catch(() => ""); throw new Error( `Failed to fetch blob: ${res.status} ${res.statusText}${errorText ? ` - ${errorText.slice(0, 200)}` : ""} (${url})`, ); } return res.blob(); }; export const getBlobDataWithHeaders = async ( url: string, ): Promise<{ blob: Blob; fileName?: string; headers: Headers }> => { const res = await fetch(resolveApiUrl(url), { method: "GET", headers: withSiteIdHeaders(), }); if (res.status === 401) { handleUnauthorizedResponse(); } if (!res.ok) { const errorText = await res.text().catch(() => ""); throw new Error( `Failed to fetch blob: ${res.status} ${res.statusText}${errorText ? ` - ${errorText.slice(0, 200)}` : ""} (${url})`, ); } return { blob: await res.blob(), fileName: parseDownloadFileName(res.headers.get("content-disposition")), headers: res.headers, }; }; // DC API ํ˜ธ์ถœ export const getDcData = async (url: string, resType: string = "json"): Promise => { const res = await fetch('/dc-service' + url, { method: "GET", headers: withSiteIdHeaders(), }); if (res.status === 401) { handleUnauthorizedResponse(); } if (res.ok) { let data; if (resType === "json") data = await res.json(); else if (resType === "blob") data = await res.blob(); return data; } throw new Error('Failed to fetch data'); }; // CMS API ํ˜ธ์ถœ export const getCmsData = async (url: string, data?: object): Promise => { const res = await fetch('/cms-service' + url, { method: "GET", headers: withSiteIdHeaders(), }); if (res.status === 401) { handleUnauthorizedResponse(); } if (res.ok) { const data = await res.json(); return data; } throw new Error('Failed to fetch data'); }; // GeoServer API ํ˜ธ์ถœ export const getGeoData = async (url: string, data?: object): Promise => { const res = await fetch('/geoserver' + url, { method: "GET", headers: withSiteIdHeaders(), }); if (res.status === 401) { handleUnauthorizedResponse(); } if (res.ok) { const data = await res.json(); return data; } throw new Error('Failed to fetch data'); }; export function getCookie(name: string) { const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)')); if (match && match[2]) { return decodeURIComponent(match[2]); } return null; } export const useCookieAuth = () => { const [data, setData] = useState(""); useEffect(() => { const fetch = async () => { const result = getCookie("Authorization"); if (result) { setData(result); } }; fetch(); console.log("useCookieAuth ํ˜ธ์ถœ"); }, []); return data; };