/** * API 유틸리티 함수 * @description API 호출에 필요한 유틸리티 함수를 제공합니다. */ import type { HttpMethod, FetchOptions, FetchRequestOptions, FetchErrorObject, RequestObject, } from '#layers/types/api/common' /** * 로딩 상태를 시작하는 헬퍼 함수 */ const startLoading = ( loadingStore: ReturnType, loading: FetchOptions['loading'] ) => { if (!loadingStore) return if (typeof loading === 'object' && loading.localId) { loadingStore.startLocalLoading(loading.localId) } } /** * 로딩 상태를 종료하는 헬퍼 함수 */ const stopLoading = ( loadingStore: ReturnType, loading: FetchOptions['loading'] ) => { if (!loadingStore) return if (typeof loading === 'object' && loading.localId) { loadingStore.stopLocalLoading(loading.localId) return } if (loading === true) { loadingStore.finishApiLoading() } } /** * 요청 옵션을 구성하는 헬퍼 함수 */ const buildRequestOptions = ( method: HttpMethod, url: string, options: FetchOptions ): FetchRequestOptions => { let stoveGameId = '' let callerDetail = '' if (import.meta.client) { try { const gameDataStore = useGameDataStore() stoveGameId = gameDataStore.gameData?.game_id || '' } catch { stoveGameId = '' } callerDetail = useCookie('sgs_da_uuid').value || '' } const requestOptions: FetchRequestOptions = { method, headers: { 'Content-Type': 'application/json;charset=UTF-8', }, } // 쿼리 파라미터 추가 if (options.query) { requestOptions.query = options.query } // 플랫폼 환경 API 호출 시 Caller 헤더 추가 if (url.includes('.onstove.com') || url.includes('.gate8.com')) { requestOptions.headers = { ...requestOptions.headers, 'Caller-Id': stoveGameId as string, 'Caller-Detail': callerDetail as string, } } // 사용자 정의 헤더 추가 if (options.headers) { requestOptions.headers = { ...requestOptions.headers, ...(options.headers as Record), } } // 본문 데이터 추가 if (options.body) { requestOptions.body = options.body } // 캐시 키 추가 if (options.key) { requestOptions.key = options.key } return requestOptions } /** * 공통 API 호출 함수 (리팩토링된 버전) * * @param method - HTTP 메소드 * @param url - API URL * @param options - 요청 옵션 * @returns API 응답 데이터 */ export const commonFetch = async ( method: HttpMethod = 'GET', url: string, options: FetchOptions = {} ): Promise => { let loadingStore = null if (import.meta.client) { try { loadingStore = useLoadingStore() } catch { loadingStore = null } } startLoading(loadingStore, options.loading) try { const requestOptions = buildRequestOptions(method, url, options) return await $fetch(url, requestOptions) } catch (error) { console.error('[Exception] apiUtil.commonFetch:', error) const fetchError = error as FetchErrorObject return ( fetchError.data || { code: fetchError.statusCode, message: fetchError.statusMessage, } ) } finally { stopLoading(loadingStore, options.loading) } } /** * 사용자 IP 조회 함수 (리팩토링된 버전) * * @param request - 요청 객체 * @returns 클라이언트 IP 주소 */ export const getTrueClientIp = (request: RequestObject): string => { const { headers, socket } = request // IP 헤더 우선순위 목록 const ipHeaders = [ 'True-Client-IP', 'X-Real-IP', 'X-Forwarded-For', 'Proxy-Client-IP', 'WL-Proxy-Client-IP', 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', ] // 헤더에서 IP 추출 for (const header of ipHeaders) { const ip = headers[header] || headers[header.toLowerCase()] if (ip?.trim()) { // 여러 IP가 있는 경우 첫 번째 IP 반환 return ip.includes(',') ? ip.split(',')[0].trim() : ip.trim() } } // 소켓에서 직접 IP 추출 if (socket.remoteAddress?.trim()) { return socket.remoteAddress.trim() } return '' }