- ItemMasterDataManagement 컴포넌트 구조화 (tabs, dialogs, components 분리) - HierarchyTab 타입 에러 수정 (BOMItem section_id, updated_at 추가) - API 클라이언트 구현 (item-master.ts, 13개 엔드포인트) - ItemMasterContext 구현 (상태 관리 및 데이터 흐름) - 백엔드 요구사항 문서 작성 (CORS 설정, API 스펙 등) - SSR 호환성 수정 (navigator API typeof window 체크) - 미사용 변수 ESLint 에러 해결 - Context 리팩토링 (AuthContext, RootProvider 추가) - API 유틸리티 추가 (error-handler, logger, transformers) 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
361 lines
7.7 KiB
TypeScript
361 lines
7.7 KiB
TypeScript
// API 호출 로깅 유틸리티
|
|
// 개발 중 API 요청/응답을 추적하고 디버깅하기 위한 로거
|
|
|
|
/**
|
|
* 로그 레벨
|
|
*/
|
|
export enum LogLevel {
|
|
DEBUG = 'DEBUG',
|
|
INFO = 'INFO',
|
|
WARN = 'WARN',
|
|
ERROR = 'ERROR',
|
|
}
|
|
|
|
/**
|
|
* API 로그 항목 인터페이스
|
|
*/
|
|
interface ApiLogEntry {
|
|
timestamp: string;
|
|
level: LogLevel;
|
|
method: string;
|
|
url: string;
|
|
requestData?: any;
|
|
responseData?: any;
|
|
statusCode?: number;
|
|
error?: Error;
|
|
duration?: number;
|
|
}
|
|
|
|
/**
|
|
* API Logger 클래스
|
|
*/
|
|
class ApiLogger {
|
|
private enabled: boolean;
|
|
private logs: ApiLogEntry[] = [];
|
|
private maxLogs: number = 100;
|
|
|
|
constructor() {
|
|
// 개발 환경에서만 로깅 활성화
|
|
this.enabled =
|
|
process.env.NODE_ENV === 'development' ||
|
|
process.env.NEXT_PUBLIC_API_LOGGING === 'true';
|
|
}
|
|
|
|
/**
|
|
* 로깅 활성화 여부 설정
|
|
*/
|
|
setEnabled(enabled: boolean) {
|
|
this.enabled = enabled;
|
|
}
|
|
|
|
/**
|
|
* 로그 최대 개수 설정
|
|
*/
|
|
setMaxLogs(max: number) {
|
|
this.maxLogs = max;
|
|
}
|
|
|
|
/**
|
|
* API 요청 시작 로그
|
|
*/
|
|
logRequest(method: string, url: string, data?: any): number {
|
|
if (!this.enabled) return Date.now();
|
|
|
|
const startTime = Date.now();
|
|
const entry: ApiLogEntry = {
|
|
timestamp: new Date().toISOString(),
|
|
level: LogLevel.INFO,
|
|
method,
|
|
url,
|
|
requestData: data,
|
|
};
|
|
|
|
console.group(`🚀 API Request: ${method} ${url}`);
|
|
console.log('⏰ Time:', entry.timestamp);
|
|
if (data) {
|
|
console.log('📤 Request Data:', data);
|
|
}
|
|
console.groupEnd();
|
|
|
|
this.addLog(entry);
|
|
return startTime;
|
|
}
|
|
|
|
/**
|
|
* API 응답 성공 로그
|
|
*/
|
|
logResponse(
|
|
method: string,
|
|
url: string,
|
|
statusCode: number,
|
|
data: any,
|
|
startTime: number
|
|
) {
|
|
if (!this.enabled) return;
|
|
|
|
const duration = Date.now() - startTime;
|
|
const entry: ApiLogEntry = {
|
|
timestamp: new Date().toISOString(),
|
|
level: LogLevel.INFO,
|
|
method,
|
|
url,
|
|
responseData: data,
|
|
statusCode,
|
|
duration,
|
|
};
|
|
|
|
console.group(`✅ API Response: ${method} ${url}`);
|
|
console.log('⏰ Time:', entry.timestamp);
|
|
console.log('📊 Status:', statusCode);
|
|
console.log('⏱️ Duration:', `${duration}ms`);
|
|
console.log('📥 Response Data:', data);
|
|
console.groupEnd();
|
|
|
|
this.addLog(entry);
|
|
}
|
|
|
|
/**
|
|
* API 에러 로그
|
|
*/
|
|
logError(
|
|
method: string,
|
|
url: string,
|
|
error: Error,
|
|
statusCode?: number,
|
|
startTime?: number
|
|
) {
|
|
if (!this.enabled) return;
|
|
|
|
const duration = startTime ? Date.now() - startTime : undefined;
|
|
const entry: ApiLogEntry = {
|
|
timestamp: new Date().toISOString(),
|
|
level: LogLevel.ERROR,
|
|
method,
|
|
url,
|
|
error,
|
|
statusCode,
|
|
duration,
|
|
};
|
|
|
|
console.group(`❌ API Error: ${method} ${url}`);
|
|
console.log('⏰ Time:', entry.timestamp);
|
|
if (statusCode) {
|
|
console.log('📊 Status:', statusCode);
|
|
}
|
|
if (duration) {
|
|
console.log('⏱️ Duration:', `${duration}ms`);
|
|
}
|
|
console.error('💥 Error:', error);
|
|
console.groupEnd();
|
|
|
|
this.addLog(entry);
|
|
}
|
|
|
|
/**
|
|
* 경고 로그
|
|
*/
|
|
logWarning(message: string, data?: any) {
|
|
if (!this.enabled) return;
|
|
|
|
const entry: ApiLogEntry = {
|
|
timestamp: new Date().toISOString(),
|
|
level: LogLevel.WARN,
|
|
method: 'WARN',
|
|
url: message,
|
|
requestData: data,
|
|
};
|
|
|
|
console.warn(`⚠️ API Warning: ${message}`, data);
|
|
this.addLog(entry);
|
|
}
|
|
|
|
/**
|
|
* 디버그 로그
|
|
*/
|
|
logDebug(message: string, data?: any) {
|
|
if (!this.enabled) return;
|
|
|
|
const entry: ApiLogEntry = {
|
|
timestamp: new Date().toISOString(),
|
|
level: LogLevel.DEBUG,
|
|
method: 'DEBUG',
|
|
url: message,
|
|
requestData: data,
|
|
};
|
|
|
|
console.debug(`🔍 API Debug: ${message}`, data);
|
|
this.addLog(entry);
|
|
}
|
|
|
|
/**
|
|
* 로그 추가 및 최대 개수 관리
|
|
*/
|
|
private addLog(entry: ApiLogEntry) {
|
|
this.logs.push(entry);
|
|
if (this.logs.length > this.maxLogs) {
|
|
this.logs.shift(); // 가장 오래된 로그 제거
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 모든 로그 조회
|
|
*/
|
|
getLogs(): ApiLogEntry[] {
|
|
return [...this.logs];
|
|
}
|
|
|
|
/**
|
|
* 특정 레벨의 로그만 조회
|
|
*/
|
|
getLogsByLevel(level: LogLevel): ApiLogEntry[] {
|
|
return this.logs.filter((log) => log.level === level);
|
|
}
|
|
|
|
/**
|
|
* 에러 로그만 조회
|
|
*/
|
|
getErrors(): ApiLogEntry[] {
|
|
return this.getLogsByLevel(LogLevel.ERROR);
|
|
}
|
|
|
|
/**
|
|
* 모든 로그 삭제
|
|
*/
|
|
clearLogs() {
|
|
this.logs = [];
|
|
console.log('🗑️ API logs cleared');
|
|
}
|
|
|
|
/**
|
|
* 로그 통계 조회
|
|
*/
|
|
getStats() {
|
|
const stats = {
|
|
total: this.logs.length,
|
|
byLevel: {
|
|
[LogLevel.DEBUG]: 0,
|
|
[LogLevel.INFO]: 0,
|
|
[LogLevel.WARN]: 0,
|
|
[LogLevel.ERROR]: 0,
|
|
},
|
|
averageDuration: 0,
|
|
errorRate: 0,
|
|
};
|
|
|
|
let totalDuration = 0;
|
|
let countWithDuration = 0;
|
|
|
|
this.logs.forEach((log) => {
|
|
stats.byLevel[log.level]++;
|
|
if (log.duration) {
|
|
totalDuration += log.duration;
|
|
countWithDuration++;
|
|
}
|
|
});
|
|
|
|
if (countWithDuration > 0) {
|
|
stats.averageDuration = totalDuration / countWithDuration;
|
|
}
|
|
|
|
if (stats.total > 0) {
|
|
stats.errorRate =
|
|
(stats.byLevel[LogLevel.ERROR] / stats.total) * 100;
|
|
}
|
|
|
|
return stats;
|
|
}
|
|
|
|
/**
|
|
* 로그 통계 출력
|
|
*/
|
|
printStats() {
|
|
const stats = this.getStats();
|
|
console.group('📊 API Logger Statistics');
|
|
console.log('Total Logs:', stats.total);
|
|
console.log('By Level:', stats.byLevel);
|
|
console.log(
|
|
'Average Duration:',
|
|
`${stats.averageDuration.toFixed(2)}ms`
|
|
);
|
|
console.log('Error Rate:', `${stats.errorRate.toFixed(2)}%`);
|
|
console.groupEnd();
|
|
}
|
|
|
|
/**
|
|
* 로그를 JSON으로 내보내기
|
|
*/
|
|
exportLogs(): string {
|
|
return JSON.stringify(this.logs, null, 2);
|
|
}
|
|
|
|
/**
|
|
* 로그를 콘솔에 테이블로 출력
|
|
*/
|
|
printLogsAsTable() {
|
|
if (this.logs.length === 0) {
|
|
console.log('📭 No logs available');
|
|
return;
|
|
}
|
|
|
|
const tableData = this.logs.map((log) => ({
|
|
Timestamp: log.timestamp,
|
|
Level: log.level,
|
|
Method: log.method,
|
|
URL: log.url,
|
|
Status: log.statusCode || '-',
|
|
Duration: log.duration ? `${log.duration}ms` : '-',
|
|
Error: log.error?.message || '-',
|
|
}));
|
|
|
|
console.table(tableData);
|
|
}
|
|
}
|
|
|
|
// 싱글톤 인스턴스 생성
|
|
export const apiLogger = new ApiLogger();
|
|
|
|
/**
|
|
* API 호출 래퍼 함수
|
|
* 자동으로 요청/응답을 로깅합니다
|
|
*/
|
|
export async function loggedFetch<T>(
|
|
method: string,
|
|
url: string,
|
|
options?: RequestInit
|
|
): Promise<T> {
|
|
const startTime = apiLogger.logRequest(method, url, options?.body);
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
...options,
|
|
method,
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
apiLogger.logResponse(method, url, response.status, data, startTime);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(data.message || 'API request failed');
|
|
}
|
|
|
|
return data;
|
|
} catch (error) {
|
|
apiLogger.logError(method, url, error as Error, undefined, startTime);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// 개발 도구를 window에 노출 (브라우저 콘솔에서 사용 가능)
|
|
if (typeof window !== 'undefined' && process.env.NODE_ENV === 'development') {
|
|
(window as any).apiLogger = apiLogger;
|
|
console.log(
|
|
'💡 API Logger is available in console as "apiLogger"\n' +
|
|
' - apiLogger.getLogs() - View all logs\n' +
|
|
' - apiLogger.getErrors() - View errors only\n' +
|
|
' - apiLogger.printStats() - View statistics\n' +
|
|
' - apiLogger.printLogsAsTable() - View logs as table\n' +
|
|
' - apiLogger.clearLogs() - Clear all logs'
|
|
);
|
|
}
|