[feat]: Item Master 데이터 관리 기능 구현 및 타입 에러 수정
- 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>
This commit is contained in:
48
src/lib/api/auth-headers.ts
Normal file
48
src/lib/api/auth-headers.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
// 인증 헤더 유틸리티
|
||||
// API 요청 시 자동으로 인증 헤더 추가
|
||||
|
||||
/**
|
||||
* API 요청에 사용할 인증 헤더 생성
|
||||
* - Content-Type: application/json
|
||||
* - X-API-KEY: 환경변수에서 로드
|
||||
* - Authorization: Bearer 토큰 (쿠키에서 추출)
|
||||
*/
|
||||
export const getAuthHeaders = (): HeadersInit => {
|
||||
// TODO: 실제 프로젝트의 토큰 저장 방식에 맞춰 수정 필요
|
||||
// 현재는 쿠키에서 'auth_token' 추출하는 방식
|
||||
const token = typeof window !== 'undefined'
|
||||
? document.cookie.split('; ').find(row => row.startsWith('auth_token='))?.split('=')[1]
|
||||
: '';
|
||||
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Multipart/form-data 요청에 사용할 헤더 생성
|
||||
* - Content-Type은 브라우저가 자동으로 설정 (boundary 포함)
|
||||
* - X-API-KEY와 Authorization만 포함
|
||||
*/
|
||||
export const getMultipartHeaders = (): HeadersInit => {
|
||||
const token = typeof window !== 'undefined'
|
||||
? document.cookie.split('; ').find(row => row.startsWith('auth_token='))?.split('=')[1]
|
||||
: '';
|
||||
|
||||
return {
|
||||
'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
// Content-Type은 명시하지 않음 (multipart/form-data; boundary=... 자동 설정)
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 토큰 존재 여부 확인
|
||||
*/
|
||||
export const hasAuthToken = (): boolean => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
const token = document.cookie.split('; ').find(row => row.startsWith('auth_token='))?.split('=')[1];
|
||||
return !!token;
|
||||
};
|
||||
85
src/lib/api/error-handler.ts
Normal file
85
src/lib/api/error-handler.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
// API 에러 핸들링 헬퍼 유틸리티
|
||||
// API 요청 실패 시 에러 처리 및 사용자 친화적 메시지 생성
|
||||
|
||||
/**
|
||||
* API 에러 클래스
|
||||
* - 표준 Error를 확장하여 HTTP 상태 코드와 validation errors 포함
|
||||
*/
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
public status: number,
|
||||
public message: string,
|
||||
public errors?: Record<string, string[]>
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'ApiError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API 응답 에러를 처리하고 ApiError를 throw
|
||||
* @param response - fetch Response 객체
|
||||
* @throws {ApiError} HTTP 상태 코드, 메시지, validation errors 포함
|
||||
*/
|
||||
export const handleApiError = async (response: Response): Promise<never> => {
|
||||
const data = await response.json().catch(() => ({}));
|
||||
|
||||
// 401 Unauthorized - 토큰 만료 또는 인증 실패
|
||||
if (response.status === 401) {
|
||||
// 로그인 페이지로 리다이렉트
|
||||
if (typeof window !== 'undefined') {
|
||||
// 현재 페이지 URL을 저장 (로그인 후 돌아오기 위함)
|
||||
const currentPath = window.location.pathname + window.location.search;
|
||||
sessionStorage.setItem('redirectAfterLogin', currentPath);
|
||||
|
||||
// 로그인 페이지로 이동
|
||||
window.location.href = '/login?session=expired';
|
||||
}
|
||||
|
||||
throw new ApiError(
|
||||
401,
|
||||
'인증이 만료되었습니다. 다시 로그인해주세요.',
|
||||
data.errors
|
||||
);
|
||||
}
|
||||
|
||||
// 403 Forbidden - 권한 없음
|
||||
if (response.status === 403) {
|
||||
throw new ApiError(
|
||||
403,
|
||||
data.message || '접근 권한이 없습니다.',
|
||||
data.errors
|
||||
);
|
||||
}
|
||||
|
||||
// 422 Unprocessable Entity - Validation 에러
|
||||
if (response.status === 422) {
|
||||
throw new ApiError(
|
||||
422,
|
||||
data.message || '입력값을 확인해주세요.',
|
||||
data.errors
|
||||
);
|
||||
}
|
||||
|
||||
// 기타 에러
|
||||
throw new ApiError(
|
||||
response.status,
|
||||
data.message || '서버 오류가 발생했습니다',
|
||||
data.errors
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 에러 객체에서 사용자 친화적인 메시지 추출
|
||||
* @param error - 발생한 에러 객체 (ApiError, Error, unknown)
|
||||
* @returns 사용자에게 표시할 에러 메시지
|
||||
*/
|
||||
export const getErrorMessage = (error: unknown): string => {
|
||||
if (error instanceof ApiError) {
|
||||
return error.message;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return '알 수 없는 오류가 발생했습니다';
|
||||
};
|
||||
1184
src/lib/api/item-master.ts
Normal file
1184
src/lib/api/item-master.ts
Normal file
File diff suppressed because it is too large
Load Diff
360
src/lib/api/logger.ts
Normal file
360
src/lib/api/logger.ts
Normal file
@@ -0,0 +1,360 @@
|
||||
// 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'
|
||||
);
|
||||
}
|
||||
449
src/lib/api/mock-data.ts
Normal file
449
src/lib/api/mock-data.ts
Normal file
@@ -0,0 +1,449 @@
|
||||
// API Mock 데이터
|
||||
// 백엔드 API 준비 전 프론트엔드 개발용 Mock 데이터
|
||||
|
||||
import type {
|
||||
ItemPageResponse,
|
||||
ItemSectionResponse,
|
||||
ItemFieldResponse,
|
||||
BomItemResponse,
|
||||
SectionTemplateResponse,
|
||||
MasterFieldResponse,
|
||||
InitResponse,
|
||||
} from '@/types/item-master-api';
|
||||
|
||||
// ============================================
|
||||
// Mock Pages
|
||||
// ============================================
|
||||
|
||||
export const mockPages: ItemPageResponse[] = [
|
||||
{
|
||||
id: 1,
|
||||
tenant_id: 1,
|
||||
page_name: '완제품(FG)',
|
||||
item_type: 'FG',
|
||||
absolute_path: '/item-master/FG',
|
||||
is_active: true,
|
||||
sections: [],
|
||||
created_by: 1,
|
||||
updated_by: 1,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
tenant_id: 1,
|
||||
page_name: '반제품(PT)',
|
||||
item_type: 'PT',
|
||||
absolute_path: '/item-master/PT',
|
||||
is_active: true,
|
||||
sections: [],
|
||||
created_by: 1,
|
||||
updated_by: 1,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
tenant_id: 1,
|
||||
page_name: '원자재(RM)',
|
||||
item_type: 'RM',
|
||||
absolute_path: '/item-master/RM',
|
||||
is_active: true,
|
||||
sections: [],
|
||||
created_by: 1,
|
||||
updated_by: 1,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// Mock Sections
|
||||
// ============================================
|
||||
|
||||
export const mockSections: ItemSectionResponse[] = [
|
||||
{
|
||||
id: 1,
|
||||
tenant_id: 1,
|
||||
page_id: 1,
|
||||
title: '기본 정보',
|
||||
type: 'fields',
|
||||
order_no: 1,
|
||||
fields: [],
|
||||
bomItems: [],
|
||||
created_by: 1,
|
||||
updated_by: 1,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
tenant_id: 1,
|
||||
page_id: 1,
|
||||
title: 'BOM',
|
||||
type: 'bom',
|
||||
order_no: 2,
|
||||
fields: [],
|
||||
bomItems: [],
|
||||
created_by: 1,
|
||||
updated_by: 1,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// Mock Fields
|
||||
// ============================================
|
||||
|
||||
export const mockFields: ItemFieldResponse[] = [
|
||||
{
|
||||
id: 1,
|
||||
tenant_id: 1,
|
||||
section_id: 1,
|
||||
field_name: '품목코드',
|
||||
field_type: 'textbox',
|
||||
order_no: 1,
|
||||
is_required: true,
|
||||
placeholder: '품목코드를 입력하세요',
|
||||
default_value: null,
|
||||
display_condition: null,
|
||||
validation_rules: { maxLength: 50 },
|
||||
options: null,
|
||||
properties: null,
|
||||
created_by: 1,
|
||||
updated_by: 1,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
tenant_id: 1,
|
||||
section_id: 1,
|
||||
field_name: '품목명',
|
||||
field_type: 'textbox',
|
||||
order_no: 2,
|
||||
is_required: true,
|
||||
placeholder: '품목명을 입력하세요',
|
||||
default_value: null,
|
||||
display_condition: null,
|
||||
validation_rules: { maxLength: 100 },
|
||||
options: null,
|
||||
properties: null,
|
||||
created_by: 1,
|
||||
updated_by: 1,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
tenant_id: 1,
|
||||
section_id: 1,
|
||||
field_name: '단위',
|
||||
field_type: 'dropdown',
|
||||
order_no: 3,
|
||||
is_required: true,
|
||||
placeholder: '단위를 선택하세요',
|
||||
default_value: null,
|
||||
display_condition: null,
|
||||
validation_rules: null,
|
||||
options: ['EA', 'KG', 'L', 'M', 'SET'],
|
||||
properties: null,
|
||||
created_by: 1,
|
||||
updated_by: 1,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
tenant_id: 1,
|
||||
section_id: 1,
|
||||
field_name: '수량',
|
||||
field_type: 'number',
|
||||
order_no: 4,
|
||||
is_required: false,
|
||||
placeholder: '수량을 입력하세요',
|
||||
default_value: '0',
|
||||
display_condition: null,
|
||||
validation_rules: { min: 0 },
|
||||
options: null,
|
||||
properties: null,
|
||||
created_by: 1,
|
||||
updated_by: 1,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// Mock BOM Items
|
||||
// ============================================
|
||||
|
||||
export const mockBomItems: BomItemResponse[] = [
|
||||
{
|
||||
id: 1,
|
||||
tenant_id: 1,
|
||||
section_id: 2,
|
||||
item_code: 'RM-001',
|
||||
item_name: '철판',
|
||||
quantity: 5,
|
||||
unit: 'KG',
|
||||
unit_price: 10000,
|
||||
total_price: 50000,
|
||||
spec: 'SUS304 2T',
|
||||
note: null,
|
||||
created_by: 1,
|
||||
updated_by: 1,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
tenant_id: 1,
|
||||
section_id: 2,
|
||||
item_code: 'PT-001',
|
||||
item_name: '플레이트',
|
||||
quantity: 2,
|
||||
unit: 'EA',
|
||||
unit_price: 25000,
|
||||
total_price: 50000,
|
||||
spec: '200x200mm',
|
||||
note: '표면처리 필요',
|
||||
created_by: 1,
|
||||
updated_by: 1,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// Mock Section Templates
|
||||
// ============================================
|
||||
|
||||
export const mockSectionTemplates: SectionTemplateResponse[] = [
|
||||
{
|
||||
id: 1,
|
||||
tenant_id: 1,
|
||||
title: '기본정보 템플릿',
|
||||
type: 'fields',
|
||||
description: '품목 기본 정보 입력용 템플릿',
|
||||
is_default: true,
|
||||
created_by: 1,
|
||||
updated_by: 1,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
tenant_id: 1,
|
||||
title: 'BOM 템플릿',
|
||||
type: 'bom',
|
||||
description: 'BOM 관리용 템플릿',
|
||||
is_default: true,
|
||||
created_by: 1,
|
||||
updated_by: 1,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// Mock Master Fields
|
||||
// ============================================
|
||||
|
||||
export const mockMasterFields: MasterFieldResponse[] = [
|
||||
{
|
||||
id: 1,
|
||||
tenant_id: 1,
|
||||
field_name: '품목코드',
|
||||
field_type: 'textbox',
|
||||
category: 'basic',
|
||||
description: '품목 고유 코드',
|
||||
is_common: true,
|
||||
default_value: null,
|
||||
options: null,
|
||||
validation_rules: { required: true, maxLength: 50 },
|
||||
properties: null,
|
||||
created_by: 1,
|
||||
updated_by: 1,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
tenant_id: 1,
|
||||
field_name: '품목명',
|
||||
field_type: 'textbox',
|
||||
category: 'basic',
|
||||
description: '품목 명칭',
|
||||
is_common: true,
|
||||
default_value: null,
|
||||
options: null,
|
||||
validation_rules: { required: true, maxLength: 100 },
|
||||
properties: null,
|
||||
created_by: 1,
|
||||
updated_by: 1,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
tenant_id: 1,
|
||||
field_name: '단위',
|
||||
field_type: 'dropdown',
|
||||
category: 'basic',
|
||||
description: '수량 단위',
|
||||
is_common: true,
|
||||
default_value: 'EA',
|
||||
options: ['EA', 'KG', 'L', 'M', 'SET'],
|
||||
validation_rules: { required: true },
|
||||
properties: null,
|
||||
created_by: 1,
|
||||
updated_by: 1,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// Mock Init Response
|
||||
// ============================================
|
||||
|
||||
export const mockInitResponse: InitResponse = {
|
||||
pages: mockPages,
|
||||
sections: mockSections,
|
||||
fields: mockFields,
|
||||
bom_items: mockBomItems,
|
||||
section_templates: mockSectionTemplates,
|
||||
master_fields: mockMasterFields,
|
||||
custom_tabs: [
|
||||
{
|
||||
id: 1,
|
||||
tenant_id: 1,
|
||||
tab_name: '사용자 정의 탭',
|
||||
item_type: 'FG',
|
||||
order_no: 10,
|
||||
is_active: true,
|
||||
created_by: 1,
|
||||
updated_by: 1,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
],
|
||||
unit_options: [
|
||||
{
|
||||
id: 1,
|
||||
tenant_id: 1,
|
||||
option_type: 'unit',
|
||||
option_value: 'EA',
|
||||
display_name: '개',
|
||||
order_no: 1,
|
||||
is_active: true,
|
||||
created_by: 1,
|
||||
updated_by: 1,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
tenant_id: 1,
|
||||
option_type: 'unit',
|
||||
option_value: 'KG',
|
||||
display_name: '킬로그램',
|
||||
order_no: 2,
|
||||
is_active: true,
|
||||
created_by: 1,
|
||||
updated_by: 1,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
tenant_id: 1,
|
||||
option_type: 'unit',
|
||||
option_value: 'L',
|
||||
display_name: '리터',
|
||||
order_no: 3,
|
||||
is_active: true,
|
||||
created_by: 1,
|
||||
updated_by: 1,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
],
|
||||
material_options: [
|
||||
{
|
||||
id: 4,
|
||||
tenant_id: 1,
|
||||
option_type: 'material',
|
||||
option_value: 'SUS304',
|
||||
display_name: '스테인리스 304',
|
||||
order_no: 1,
|
||||
is_active: true,
|
||||
created_by: 1,
|
||||
updated_by: 1,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
tenant_id: 1,
|
||||
option_type: 'material',
|
||||
option_value: 'AL',
|
||||
display_name: '알루미늄',
|
||||
order_no: 2,
|
||||
is_active: true,
|
||||
created_by: 1,
|
||||
updated_by: 1,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
],
|
||||
surface_treatment_options: [
|
||||
{
|
||||
id: 6,
|
||||
tenant_id: 1,
|
||||
option_type: 'surface_treatment',
|
||||
option_value: 'ANODIZING',
|
||||
display_name: '아노다이징',
|
||||
order_no: 1,
|
||||
is_active: true,
|
||||
created_by: 1,
|
||||
updated_by: 1,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
tenant_id: 1,
|
||||
option_type: 'surface_treatment',
|
||||
option_value: 'PAINTING',
|
||||
display_name: '도장',
|
||||
order_no: 2,
|
||||
is_active: true,
|
||||
created_by: 1,
|
||||
updated_by: 1,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// Mock 모드 활성화 플래그
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Mock 모드 활성화 여부
|
||||
* - true: Mock 데이터 사용 (백엔드 없이 프론트엔드 개발)
|
||||
* - false: 실제 API 호출
|
||||
*/
|
||||
export const MOCK_MODE = process.env.NEXT_PUBLIC_MOCK_MODE === 'true';
|
||||
|
||||
/**
|
||||
* Mock API 응답 시뮬레이션 (네트워크 지연 재현)
|
||||
*/
|
||||
export const simulateNetworkDelay = async (ms: number = 500) => {
|
||||
if (!MOCK_MODE) return;
|
||||
await new Promise((resolve) => setTimeout(resolve, ms));
|
||||
};
|
||||
97
src/lib/api/php-proxy.ts
Normal file
97
src/lib/api/php-proxy.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
/**
|
||||
* PHP 백엔드 프록시 유틸리티
|
||||
*
|
||||
* 역할:
|
||||
* - Next.js API Routes → PHP Backend 단순 프록시
|
||||
* - HttpOnly 쿠키의 access_token을 Bearer token으로 전달
|
||||
* - PHP 응답을 그대로 프론트엔드로 반환
|
||||
*
|
||||
* 보안:
|
||||
* - tenant.id 검증은 PHP 백엔드에서 수행
|
||||
* - Next.js는 단순히 요청/응답 전달만
|
||||
*/
|
||||
|
||||
/**
|
||||
* PHP 백엔드로 프록시 요청 전송
|
||||
*
|
||||
* @param request NextRequest 객체
|
||||
* @param phpEndpoint PHP 백엔드 엔드포인트 (예: '/api/v1/tenants/282/item-master-config')
|
||||
* @param options fetch options (method, body 등)
|
||||
* @returns NextResponse
|
||||
*/
|
||||
export async function proxyToPhpBackend(
|
||||
request: NextRequest,
|
||||
phpEndpoint: string,
|
||||
options?: RequestInit
|
||||
): Promise<NextResponse> {
|
||||
try {
|
||||
// 1. 쿠키에서 access_token 추출
|
||||
const accessToken = request.cookies.get('access_token')?.value;
|
||||
|
||||
if (!accessToken) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
code: 'UNAUTHORIZED',
|
||||
message: '인증이 필요합니다.',
|
||||
},
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// 2. PHP 백엔드 URL 생성
|
||||
const phpUrl = `${process.env.NEXT_PUBLIC_API_URL}${phpEndpoint}`;
|
||||
|
||||
// 3. PHP 백엔드 호출
|
||||
const response = await fetch(phpUrl, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '',
|
||||
...options?.headers,
|
||||
},
|
||||
});
|
||||
|
||||
// 4. PHP 응답을 그대로 반환
|
||||
const data = await response.json().catch(() => ({}));
|
||||
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
} catch (error) {
|
||||
console.error('[PHP Proxy Error]', error);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
code: 'SERVER_ERROR',
|
||||
message: '서버 오류가 발생했습니다.',
|
||||
},
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query Parameters를 URL에 추가하는 헬퍼
|
||||
*
|
||||
* @param baseUrl 기본 URL
|
||||
* @param searchParams URLSearchParams
|
||||
* @returns Query string이 추가된 URL
|
||||
*/
|
||||
export function appendQueryParams(baseUrl: string, searchParams: URLSearchParams): string {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
searchParams.forEach((value, key) => {
|
||||
params.append(key, value);
|
||||
});
|
||||
|
||||
const queryString = params.toString();
|
||||
return queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
||||
}
|
||||
421
src/lib/api/transformers.ts
Normal file
421
src/lib/api/transformers.ts
Normal file
@@ -0,0 +1,421 @@
|
||||
// API 응답 데이터 변환 헬퍼
|
||||
// API 응답 (snake_case + 특정 값) ↔ Frontend State (snake_case + 변환된 값)
|
||||
|
||||
import type {
|
||||
ItemPageResponse,
|
||||
ItemSectionResponse,
|
||||
ItemFieldResponse,
|
||||
BomItemResponse,
|
||||
SectionTemplateResponse,
|
||||
MasterFieldResponse,
|
||||
UnitOptionResponse,
|
||||
CustomTabResponse,
|
||||
} from '@/types/item-master-api';
|
||||
|
||||
import type {
|
||||
ItemPage,
|
||||
ItemSection,
|
||||
ItemField,
|
||||
BOMItem,
|
||||
SectionTemplate,
|
||||
ItemMasterField,
|
||||
} from '@/contexts/ItemMasterContext';
|
||||
|
||||
// ============================================
|
||||
// 타입 값 변환 매핑
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* API section type → Frontend section_type 변환
|
||||
* API: 'fields' | 'bom'
|
||||
* Frontend: 'BASIC' | 'BOM' | 'CUSTOM'
|
||||
*/
|
||||
const SECTION_TYPE_MAP: Record<string, 'BASIC' | 'BOM' | 'CUSTOM'> = {
|
||||
fields: 'BASIC',
|
||||
bom: 'BOM',
|
||||
};
|
||||
|
||||
/**
|
||||
* Frontend section_type → API section type 변환
|
||||
*/
|
||||
const SECTION_TYPE_REVERSE_MAP: Record<string, 'fields' | 'bom'> = {
|
||||
BASIC: 'fields',
|
||||
BOM: 'bom',
|
||||
CUSTOM: 'fields', // CUSTOM은 fields로 매핑
|
||||
};
|
||||
|
||||
/**
|
||||
* API field_type → Frontend field_type 변환
|
||||
* API: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea'
|
||||
* Frontend: 'TEXT' | 'NUMBER' | 'DATE' | 'SELECT' | 'TEXTAREA' | 'CHECKBOX'
|
||||
*/
|
||||
const FIELD_TYPE_MAP: Record<
|
||||
string,
|
||||
'TEXT' | 'NUMBER' | 'DATE' | 'SELECT' | 'TEXTAREA' | 'CHECKBOX'
|
||||
> = {
|
||||
textbox: 'TEXT',
|
||||
number: 'NUMBER',
|
||||
dropdown: 'SELECT',
|
||||
checkbox: 'CHECKBOX',
|
||||
date: 'DATE',
|
||||
textarea: 'TEXTAREA',
|
||||
};
|
||||
|
||||
/**
|
||||
* Frontend field_type → API field_type 변환
|
||||
*/
|
||||
const FIELD_TYPE_REVERSE_MAP: Record<
|
||||
string,
|
||||
'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea'
|
||||
> = {
|
||||
TEXT: 'textbox',
|
||||
NUMBER: 'number',
|
||||
SELECT: 'dropdown',
|
||||
CHECKBOX: 'checkbox',
|
||||
DATE: 'date',
|
||||
TEXTAREA: 'textarea',
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// API Response → Frontend State 변환
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* ItemPageResponse → ItemPage 변환
|
||||
*/
|
||||
export const transformPageResponse = (
|
||||
response: ItemPageResponse
|
||||
): ItemPage => {
|
||||
return {
|
||||
id: response.id,
|
||||
tenant_id: response.tenant_id,
|
||||
page_name: response.page_name,
|
||||
item_type: response.item_type as 'FG' | 'PT' | 'SM' | 'RM' | 'CS',
|
||||
absolute_path: response.absolute_path,
|
||||
is_active: response.is_active,
|
||||
sections: response.sections?.map(transformSectionResponse) || [],
|
||||
created_by: response.created_by,
|
||||
updated_by: response.updated_by,
|
||||
created_at: response.created_at,
|
||||
updated_at: response.updated_at,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* ItemSectionResponse → ItemSection 변환
|
||||
* 주요 변환: type → section_type, 값 변환 (fields → BASIC, bom → BOM)
|
||||
*/
|
||||
export const transformSectionResponse = (
|
||||
response: ItemSectionResponse
|
||||
): ItemSection => {
|
||||
return {
|
||||
id: response.id,
|
||||
tenant_id: response.tenant_id,
|
||||
page_id: response.page_id,
|
||||
title: response.title,
|
||||
section_type: SECTION_TYPE_MAP[response.type] || 'BASIC', // 타입 값 변환
|
||||
order_no: response.order_no,
|
||||
fields: response.fields?.map(transformFieldResponse) || [],
|
||||
bom_items: response.bomItems?.map(transformBomItemResponse) || [],
|
||||
created_by: response.created_by,
|
||||
updated_by: response.updated_by,
|
||||
created_at: response.created_at,
|
||||
updated_at: response.updated_at,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* ItemFieldResponse → ItemField 변환
|
||||
* 주요 변환: field_type 값 변환 (textbox → TEXT, dropdown → SELECT 등)
|
||||
*/
|
||||
export const transformFieldResponse = (
|
||||
response: ItemFieldResponse
|
||||
): ItemField => {
|
||||
return {
|
||||
id: response.id,
|
||||
tenant_id: response.tenant_id,
|
||||
section_id: response.section_id,
|
||||
field_name: response.field_name,
|
||||
field_type: FIELD_TYPE_MAP[response.field_type] || 'TEXT', // 타입 값 변환
|
||||
order_no: response.order_no,
|
||||
is_required: response.is_required,
|
||||
placeholder: response.placeholder,
|
||||
default_value: response.default_value,
|
||||
display_condition: response.display_condition,
|
||||
validation_rules: response.validation_rules,
|
||||
options: response.options,
|
||||
properties: response.properties,
|
||||
created_by: response.created_by,
|
||||
updated_by: response.updated_by,
|
||||
created_at: response.created_at,
|
||||
updated_at: response.updated_at,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* BomItemResponse → BOMItem 변환
|
||||
*/
|
||||
export const transformBomItemResponse = (
|
||||
response: BomItemResponse
|
||||
): BOMItem => {
|
||||
return {
|
||||
id: response.id,
|
||||
tenant_id: response.tenant_id,
|
||||
section_id: response.section_id,
|
||||
item_code: response.item_code,
|
||||
item_name: response.item_name,
|
||||
quantity: response.quantity,
|
||||
unit: response.unit,
|
||||
unit_price: response.unit_price,
|
||||
total_price: response.total_price,
|
||||
spec: response.spec,
|
||||
note: response.note,
|
||||
created_by: response.created_by,
|
||||
updated_by: response.updated_by,
|
||||
created_at: response.created_at,
|
||||
updated_at: response.updated_at,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* SectionTemplateResponse → SectionTemplate 변환
|
||||
* 주요 변환: title → template_name, type → section_type, 값 변환
|
||||
*/
|
||||
export const transformSectionTemplateResponse = (
|
||||
response: SectionTemplateResponse
|
||||
): SectionTemplate => {
|
||||
return {
|
||||
id: response.id,
|
||||
tenant_id: response.tenant_id,
|
||||
template_name: response.title, // 필드명 변환
|
||||
section_type: SECTION_TYPE_MAP[response.type] || 'BASIC', // 타입 값 변환
|
||||
description: response.description,
|
||||
default_fields: null, // API 응답에 없으므로 null
|
||||
created_by: response.created_by,
|
||||
updated_by: response.updated_by,
|
||||
created_at: response.created_at,
|
||||
updated_at: response.updated_at,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* MasterFieldResponse → ItemMasterField 변환
|
||||
* 주요 변환: field_type 값 변환 (textbox → TEXT, dropdown → SELECT 등)
|
||||
*/
|
||||
export const transformMasterFieldResponse = (
|
||||
response: MasterFieldResponse
|
||||
): ItemMasterField => {
|
||||
return {
|
||||
id: response.id,
|
||||
tenant_id: response.tenant_id,
|
||||
field_name: response.field_name,
|
||||
field_type: FIELD_TYPE_MAP[response.field_type] || 'TEXT', // 타입 값 변환
|
||||
category: response.category,
|
||||
description: response.description,
|
||||
default_validation: response.validation_rules, // 필드명 매핑
|
||||
default_properties: response.properties, // 필드명 매핑
|
||||
created_by: response.created_by,
|
||||
updated_by: response.updated_by,
|
||||
created_at: response.created_at,
|
||||
updated_at: response.updated_at,
|
||||
};
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// Frontend State → API Request 변환
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* ItemSection → ItemSectionRequest 변환
|
||||
* 주요 변환: section_type → type, 값 역변환 (BASIC → fields, BOM → bom)
|
||||
*/
|
||||
export const transformSectionToRequest = (
|
||||
section: Partial<ItemSection>
|
||||
): { title: string; type: 'fields' | 'bom' } => {
|
||||
return {
|
||||
title: section.title || '',
|
||||
type: section.section_type
|
||||
? SECTION_TYPE_REVERSE_MAP[section.section_type] || 'fields'
|
||||
: 'fields',
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* ItemField → ItemFieldRequest 변환
|
||||
* 주요 변환: field_type 값 역변환 (TEXT → textbox, SELECT → dropdown 등)
|
||||
*/
|
||||
export const transformFieldToRequest = (field: Partial<ItemField>) => {
|
||||
return {
|
||||
field_name: field.field_name || '',
|
||||
field_type: field.field_type
|
||||
? FIELD_TYPE_REVERSE_MAP[field.field_type] || 'textbox'
|
||||
: 'textbox',
|
||||
is_required: field.is_required ?? false,
|
||||
placeholder: field.placeholder || null,
|
||||
default_value: field.default_value || null,
|
||||
display_condition: field.display_condition || null,
|
||||
validation_rules: field.validation_rules || null,
|
||||
options: field.options || null,
|
||||
properties: field.properties || null,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* BOMItem → BomItemRequest 변환
|
||||
*/
|
||||
export const transformBomItemToRequest = (bomItem: Partial<BOMItem>) => {
|
||||
return {
|
||||
item_code: bomItem.item_code || undefined,
|
||||
item_name: bomItem.item_name || '',
|
||||
quantity: bomItem.quantity || 0,
|
||||
unit: bomItem.unit || undefined,
|
||||
unit_price: bomItem.unit_price || undefined,
|
||||
total_price: bomItem.total_price || undefined,
|
||||
spec: bomItem.spec || undefined,
|
||||
note: bomItem.note || undefined,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* SectionTemplate → SectionTemplateRequest 변환
|
||||
* 주요 변환: template_name → title, section_type → type, 값 역변환
|
||||
*/
|
||||
export const transformSectionTemplateToRequest = (
|
||||
template: Partial<SectionTemplate>
|
||||
) => {
|
||||
return {
|
||||
title: template.template_name || '', // 필드명 역변환
|
||||
type: template.section_type
|
||||
? SECTION_TYPE_REVERSE_MAP[template.section_type] || 'fields'
|
||||
: 'fields',
|
||||
description: template.description || undefined,
|
||||
is_default: false, // 기본값
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* ItemMasterField → MasterFieldRequest 변환
|
||||
* 주요 변환: field_type 값 역변환, default_validation/properties 필드명 변환
|
||||
*/
|
||||
export const transformMasterFieldToRequest = (
|
||||
field: Partial<ItemMasterField>
|
||||
) => {
|
||||
return {
|
||||
field_name: field.field_name || '',
|
||||
field_type: field.field_type
|
||||
? FIELD_TYPE_REVERSE_MAP[field.field_type] || 'textbox'
|
||||
: 'textbox',
|
||||
category: field.category || undefined,
|
||||
description: field.description || undefined,
|
||||
is_common: false, // 기본값
|
||||
default_value: undefined,
|
||||
options: undefined,
|
||||
validation_rules: field.default_validation || undefined, // 필드명 역변환
|
||||
properties: field.default_properties || undefined, // 필드명 역변환
|
||||
};
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 배치 변환 헬퍼
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 여러 페이지 응답을 한번에 변환
|
||||
*/
|
||||
export const transformPagesResponse = (
|
||||
responses: ItemPageResponse[]
|
||||
): ItemPage[] => {
|
||||
return responses.map(transformPageResponse);
|
||||
};
|
||||
|
||||
/**
|
||||
* 여러 섹션 응답을 한번에 변환
|
||||
*/
|
||||
export const transformSectionsResponse = (
|
||||
responses: ItemSectionResponse[]
|
||||
): ItemSection[] => {
|
||||
return responses.map(transformSectionResponse);
|
||||
};
|
||||
|
||||
/**
|
||||
* 여러 필드 응답을 한번에 변환
|
||||
*/
|
||||
export const transformFieldsResponse = (
|
||||
responses: ItemFieldResponse[]
|
||||
): ItemField[] => {
|
||||
return responses.map(transformFieldResponse);
|
||||
};
|
||||
|
||||
/**
|
||||
* 여러 BOM 아이템 응답을 한번에 변환
|
||||
*/
|
||||
export const transformBomItemsResponse = (
|
||||
responses: BomItemResponse[]
|
||||
): BOMItem[] => {
|
||||
return responses.map(transformBomItemResponse);
|
||||
};
|
||||
|
||||
/**
|
||||
* 여러 섹션 템플릿 응답을 한번에 변환
|
||||
*/
|
||||
export const transformSectionTemplatesResponse = (
|
||||
responses: SectionTemplateResponse[]
|
||||
): SectionTemplate[] => {
|
||||
return responses.map(transformSectionTemplateResponse);
|
||||
};
|
||||
|
||||
/**
|
||||
* 여러 마스터 필드 응답을 한번에 변환
|
||||
*/
|
||||
export const transformMasterFieldsResponse = (
|
||||
responses: MasterFieldResponse[]
|
||||
): ItemMasterField[] => {
|
||||
return responses.map(transformMasterFieldResponse);
|
||||
};
|
||||
|
||||
/**
|
||||
* UnitOptionResponse → MasterOption 변환 (Frontend의 MasterOption 타입에 맞춤)
|
||||
*/
|
||||
export const transformUnitOptionResponse = (
|
||||
response: UnitOptionResponse
|
||||
): { id: string; value: string; label: string; isActive: boolean } => {
|
||||
return {
|
||||
id: response.id.toString(), // number → string 변환
|
||||
value: response.value,
|
||||
label: response.label,
|
||||
isActive: true, // API에 없으므로 기본값
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* CustomTabResponse → Frontend customTabs 타입 변환
|
||||
*/
|
||||
export const transformCustomTabResponse = (
|
||||
response: CustomTabResponse
|
||||
): { id: string; label: string; icon: string; isDefault: boolean; order: number } => {
|
||||
return {
|
||||
id: response.id.toString(), // number → string 변환
|
||||
label: response.label,
|
||||
icon: response.icon || 'FileText', // null이면 기본 아이콘
|
||||
isDefault: response.is_default,
|
||||
order: response.order_no,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 여러 단위 옵션 응답을 한번에 변환
|
||||
*/
|
||||
export const transformUnitOptionsResponse = (
|
||||
responses: UnitOptionResponse[]
|
||||
) => {
|
||||
return responses.map(transformUnitOptionResponse);
|
||||
};
|
||||
|
||||
/**
|
||||
* 여러 커스텀 탭 응답을 한번에 변환
|
||||
*/
|
||||
export const transformCustomTabsResponse = (
|
||||
responses: CustomTabResponse[]
|
||||
) => {
|
||||
return responses.map(transformCustomTabResponse);
|
||||
};
|
||||
Reference in New Issue
Block a user