// lib/api/index.ts // API 클라이언트 배럴 익스포트 export { ApiClient, withTokenRefresh } from './client'; export { serverFetch } from './fetch-wrapper'; export { AUTH_CONFIG } from './auth/auth-config'; // 공용 API 타입 및 페이지네이션 유틸리티 export { toPaginationMeta, type PaginatedApiResponse, type PaginationMeta, type PaginatedResult, type SelectOption, } from './types'; // 쿼리 파라미터 빌더 (신규 코드용) export { buildQueryParams, buildApiUrl } from './query-params'; // 페이지네이션 조회 래퍼 (신규 코드용) export { executePaginatedAction, type PaginatedActionResult } from './execute-paginated-action'; // 공용 룩업 헬퍼 (거래처/계좌 조회) export { fetchVendorOptions, fetchBankAccountOptions, fetchBankAccountDetailOptions, type BankAccountOption, } from './shared-lookups'; // 공통 코드 타입 및 유틸리티 export { toCommonCodeOptions, getCodeLabel, type CommonCode, type CommonCodeOption, } from './common-codes'; // Server-side API 클라이언트 // 서버 액션에서 쿠키 기반 Bearer 토큰 자동 포함 import { cookies } from 'next/headers'; import { redirect } from 'next/navigation'; import { AUTH_CONFIG } from './auth/auth-config'; import { authenticatedFetch } from './authenticated-fetch'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; /** * Server Actions 전용 API 클라이언트 * * 특징: * - 쿠키에서 access_token 자동 읽기 * - X-API-KEY + Bearer 토큰 자동 포함 * - 401 발생 시 authenticatedFetch 게이트웨이를 통한 자동 갱신 */ class ServerApiClient { private baseURL: string; private apiKey: string; constructor() { const apiUrl = AUTH_CONFIG.apiUrl.replace(/\/$/, ''); this.baseURL = `${apiUrl}/api/v1`; this.apiKey = process.env.API_KEY || ''; } /** * 쿠키에서 인증 헤더 생성 (async) */ private async getAuthHeaders(token?: string): Promise> { const cookieStore = await cookies(); const accessToken = token || cookieStore.get('access_token')?.value; return { 'Accept': 'application/json', 'Content-Type': 'application/json', 'X-API-KEY': this.apiKey, ...(accessToken ? { 'Authorization': `Bearer ${accessToken}` } : {}), }; } /** * 토큰 쿠키 삭제 (인증 실패 시) */ private async clearTokenCookies() { const cookieStore = await cookies(); cookieStore.delete('access_token'); cookieStore.delete('refresh_token'); cookieStore.delete('token_refreshed_at'); cookieStore.delete('is_authenticated'); } /** * 새 토큰을 쿠키에 저장 */ private async setNewTokenCookies(tokens: { accessToken?: string; refreshToken?: string; expiresIn?: number; }) { const cookieStore = await cookies(); const isProduction = process.env.NODE_ENV === 'production'; if (tokens.accessToken) { cookieStore.set('access_token', tokens.accessToken, { httpOnly: true, secure: isProduction, sameSite: 'lax', path: '/', maxAge: tokens.expiresIn || 7200, }); cookieStore.set('token_refreshed_at', Date.now().toString(), { httpOnly: false, secure: isProduction, sameSite: 'lax', path: '/', maxAge: 60, }); } if (tokens.refreshToken) { cookieStore.set('refresh_token', tokens.refreshToken, { httpOnly: true, secure: isProduction, sameSite: 'lax', path: '/', maxAge: 604800, }); } } /** * HTTP 요청 실행 (authenticatedFetch 게이트웨이를 통한 자동 갱신) */ private async request( endpoint: string, options?: RequestInit & { skipAuthRetry?: boolean } ): Promise { try { const cookieStore = await cookies(); const refreshToken = cookieStore.get('refresh_token')?.value; const headers = await this.getAuthHeaders(); const url = `${this.baseURL}${endpoint}`; // authenticatedFetch 게이트웨이로 요청 실행 // skipAuthRetry=true면 refreshToken을 넘기지 않아 401 시 갱신 안 함 const { response, newTokens, authFailed } = await authenticatedFetch( url, { ...options, headers: { ...headers, ...options?.headers, }, cache: 'no-store', }, options?.skipAuthRetry ? undefined : refreshToken, 'ServerApiClient' ); // 새 토큰 → 쿠키 저장 if (newTokens) { await this.setNewTokenCookies(newTokens); } // 인증 실패 → 쿠키 삭제 + 리다이렉트 if (authFailed && !options?.skipAuthRetry) { await this.clearTokenCookies(); redirect('/login'); } if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw { status: response.status, message: errorData.message || 'An error occurred', errors: errorData.errors, code: errorData.code, }; } if (response.status === 204) { return undefined as T; } return await response.json(); } catch (error) { if (isNextRedirectError(error)) throw error; throw error; } } /** * GET 요청 */ async get(endpoint: string, options?: { params?: Record }): Promise { let url = endpoint; if (options?.params) { const searchParams = new URLSearchParams(options.params); url = `${endpoint}?${searchParams.toString()}`; } return this.request(url, { method: 'GET' }); } /** * POST 요청 */ async post(endpoint: string, data?: unknown): Promise { return this.request(endpoint, { method: 'POST', body: data ? JSON.stringify(data) : undefined, }); } /** * PUT 요청 */ async put(endpoint: string, data?: unknown): Promise { return this.request(endpoint, { method: 'PUT', body: data ? JSON.stringify(data) : undefined, }); } /** * PATCH 요청 */ async patch(endpoint: string, data?: unknown): Promise { return this.request(endpoint, { method: 'PATCH', body: data ? JSON.stringify(data) : undefined, }); } /** * DELETE 요청 */ async delete(endpoint: string, options?: { data?: unknown }): Promise { return this.request(endpoint, { method: 'DELETE', body: options?.data ? JSON.stringify(options.data) : undefined, }); } } // 서버 액션용 API 클라이언트 인스턴스 export const apiClient = new ServerApiClient();