153 lines
3.3 KiB
TypeScript
153 lines
3.3 KiB
TypeScript
|
|
// lib/api/client.ts
|
||
|
|
import { AUTH_CONFIG } from './auth/auth-config';
|
||
|
|
import type { AuthMode } from './auth/types';
|
||
|
|
|
||
|
|
interface ClientConfig {
|
||
|
|
mode: AuthMode;
|
||
|
|
apiKey?: string; // API Key 모드용
|
||
|
|
getToken?: () => string | null; // Bearer 모드용
|
||
|
|
}
|
||
|
|
|
||
|
|
interface ApiErrorResponse {
|
||
|
|
message: string;
|
||
|
|
errors?: Record<string, string[]>;
|
||
|
|
code?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export class ApiClient {
|
||
|
|
private baseURL: string;
|
||
|
|
private mode: AuthMode;
|
||
|
|
private apiKey?: string;
|
||
|
|
private getToken?: () => string | null;
|
||
|
|
|
||
|
|
constructor(config: ClientConfig) {
|
||
|
|
this.baseURL = AUTH_CONFIG.apiUrl;
|
||
|
|
this.mode = config.mode;
|
||
|
|
this.apiKey = config.apiKey;
|
||
|
|
this.getToken = config.getToken;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 인증 헤더 생성
|
||
|
|
*/
|
||
|
|
private getAuthHeaders(): Record<string, string> {
|
||
|
|
const headers: Record<string, string> = {
|
||
|
|
'Accept': 'application/json',
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
};
|
||
|
|
|
||
|
|
// API Key는 모든 모드에서 기본으로 포함 (PHP API 요구사항)
|
||
|
|
if (this.apiKey) {
|
||
|
|
headers['X-API-KEY'] = this.apiKey;
|
||
|
|
}
|
||
|
|
|
||
|
|
switch (this.mode) {
|
||
|
|
case 'api-key':
|
||
|
|
// API Key만 사용 (이미 위에서 추가됨)
|
||
|
|
break;
|
||
|
|
|
||
|
|
case 'bearer':
|
||
|
|
const token = this.getToken?.();
|
||
|
|
if (token) {
|
||
|
|
headers['Authorization'] = `Bearer ${token}`;
|
||
|
|
}
|
||
|
|
// API Key도 함께 전송 (이미 위에서 추가됨)
|
||
|
|
break;
|
||
|
|
|
||
|
|
case 'sanctum':
|
||
|
|
// 쿠키 기반 - 별도 헤더 불필요
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
return headers;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* HTTP 요청 실행
|
||
|
|
*/
|
||
|
|
async request<T>(
|
||
|
|
endpoint: string,
|
||
|
|
options?: RequestInit
|
||
|
|
): Promise<T> {
|
||
|
|
const url = `${this.baseURL}${endpoint}`;
|
||
|
|
const headers = {
|
||
|
|
...this.getAuthHeaders(),
|
||
|
|
...options?.headers,
|
||
|
|
};
|
||
|
|
|
||
|
|
const config: RequestInit = {
|
||
|
|
...options,
|
||
|
|
headers,
|
||
|
|
};
|
||
|
|
|
||
|
|
// Sanctum 모드는 쿠키 포함
|
||
|
|
if (this.mode === 'sanctum') {
|
||
|
|
config.credentials = 'include';
|
||
|
|
}
|
||
|
|
|
||
|
|
const response = await fetch(url, config);
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
await this.handleError(response);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 204 No Content 처리
|
||
|
|
if (response.status === 204) {
|
||
|
|
return undefined as T;
|
||
|
|
}
|
||
|
|
|
||
|
|
return await response.json();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* GET 요청
|
||
|
|
*/
|
||
|
|
async get<T>(endpoint: string): Promise<T> {
|
||
|
|
return this.request<T>(endpoint, { method: 'GET' });
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* POST 요청
|
||
|
|
*/
|
||
|
|
async post<T>(endpoint: string, data?: unknown): Promise<T> {
|
||
|
|
return this.request<T>(endpoint, {
|
||
|
|
method: 'POST',
|
||
|
|
body: data ? JSON.stringify(data) : undefined,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* PUT 요청
|
||
|
|
*/
|
||
|
|
async put<T>(endpoint: string, data?: unknown): Promise<T> {
|
||
|
|
return this.request<T>(endpoint, {
|
||
|
|
method: 'PUT',
|
||
|
|
body: data ? JSON.stringify(data) : undefined,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* DELETE 요청
|
||
|
|
*/
|
||
|
|
async delete<T>(endpoint: string): Promise<T> {
|
||
|
|
return this.request<T>(endpoint, { method: 'DELETE' });
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 에러 처리
|
||
|
|
*/
|
||
|
|
private async handleError(response: Response): Promise<never> {
|
||
|
|
const data = await response.json().catch(() => ({}));
|
||
|
|
|
||
|
|
const error: ApiErrorResponse = {
|
||
|
|
message: data.message || 'An error occurred',
|
||
|
|
errors: data.errors,
|
||
|
|
code: data.code,
|
||
|
|
};
|
||
|
|
|
||
|
|
throw {
|
||
|
|
status: response.status,
|
||
|
|
...error,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|