fix: 품목기준관리 실시간 동기화 수정

- BOM 항목 추가/수정/삭제 시 섹션탭 즉시 반영
- 섹션 복제 시 UI 즉시 업데이트 (null vs undefined 이슈 해결)
- 항목 수정 기능 추가 (useTemplateManagement)
- 실시간 동기화 문서 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
byeongcheolryu
2025-11-27 22:19:50 +09:00
parent b73603822b
commit 65a8510c0b
130 changed files with 11031 additions and 2287 deletions

View File

@@ -46,6 +46,12 @@ export const handleApiError = async (response: Response): Promise<never> => {
// 422 Unprocessable Entity - Validation 에러
if (response.status === 422) {
// 상세 validation 에러 로그 출력
console.error('🔴 [API 422 Validation Error]', {
message: data.message,
errors: data.errors,
fullResponse: data
});
throw new ApiError(
422,
data.message || '입력값을 확인해주세요.',

View File

@@ -10,11 +10,19 @@ import type {
ItemSectionRequest,
ItemSectionResponse,
SectionReorderRequest,
IndependentSectionRequest,
SectionUsageResponse,
LinkSectionRequest,
ItemFieldRequest,
ItemFieldResponse,
FieldReorderRequest,
IndependentFieldRequest,
FieldUsageResponse,
LinkFieldRequest,
BomItemRequest,
BomItemResponse,
IndependentBomItemRequest,
PageStructureResponse,
SectionTemplateRequest,
SectionTemplateResponse,
MasterFieldRequest,
@@ -25,6 +33,11 @@ import type {
TabColumnUpdateRequest,
UnitOptionRequest,
UnitOptionResponse,
// 2025-11-27 추가: 엔티티 관계 타입
EntityRelationshipResponse,
LinkEntityRequest,
LinkBomRequest,
ReorderRelationshipsRequest,
} from '@/types/item-master-api';
import { getAuthHeaders } from './auth-headers';
import { handleApiError } from './error-handler';
@@ -216,6 +229,212 @@ export const itemMasterApi = {
throw error;
}
},
/**
* 페이지에 섹션 연결
* POST /v1/item-master/pages/{id}/link-section
* 2025-11-26 신규 API
*/
linkSection: async (pageId: number, data: LinkSectionRequest): Promise<ApiResponse<ItemSectionResponse>> => {
const startTime = apiLogger.logRequest('POST', `${BASE_URL}/item-master/pages/${pageId}/link-section`, data);
try {
const headers = getAuthHeaders();
const response = await fetch(`${BASE_URL}/item-master/pages/${pageId}/link-section`, {
method: 'POST',
headers,
body: JSON.stringify(data),
});
if (!response.ok) {
await handleApiError(response);
}
const result: ApiResponse<ItemSectionResponse> = await response.json();
apiLogger.logResponse('POST', `${BASE_URL}/item-master/pages/${pageId}/link-section`, response.status, result, startTime);
return result;
} catch (error) {
if (error instanceof TypeError) {
apiLogger.logError('POST', `${BASE_URL}/item-master/pages/${pageId}/link-section`, error, undefined, startTime);
throw new Error('네트워크 연결을 확인해주세요. 서버에 연결할 수 없습니다.');
}
apiLogger.logError('POST', `${BASE_URL}/item-master/pages/${pageId}/link-section`, error as Error, undefined, startTime);
throw error;
}
},
/**
* 페이지에서 섹션 연결 해제
* DELETE /v1/item-master/pages/{id}/unlink-section/{sectionId}
* 2025-11-26 신규 API
*/
unlinkSection: async (pageId: number, sectionId: number): Promise<ApiResponse<void>> => {
const startTime = apiLogger.logRequest('DELETE', `${BASE_URL}/item-master/pages/${pageId}/unlink-section/${sectionId}`);
try {
const headers = getAuthHeaders();
const response = await fetch(`${BASE_URL}/item-master/pages/${pageId}/unlink-section/${sectionId}`, {
method: 'DELETE',
headers,
});
if (!response.ok) {
await handleApiError(response);
}
const result: ApiResponse<void> = await response.json();
apiLogger.logResponse('DELETE', `${BASE_URL}/item-master/pages/${pageId}/unlink-section/${sectionId}`, response.status, result, startTime);
return result;
} catch (error) {
if (error instanceof TypeError) {
apiLogger.logError('DELETE', `${BASE_URL}/item-master/pages/${pageId}/unlink-section/${sectionId}`, error, undefined, startTime);
throw new Error('네트워크 연결을 확인해주세요. 서버에 연결할 수 없습니다.');
}
apiLogger.logError('DELETE', `${BASE_URL}/item-master/pages/${pageId}/unlink-section/${sectionId}`, error as Error, undefined, startTime);
throw error;
}
},
/**
* 페이지 전체 구조 조회
* GET /v1/item-master/pages/{id}/structure
* 2025-11-26 신규 API
*/
getStructure: async (pageId: number): Promise<PageStructureResponse> => {
const startTime = apiLogger.logRequest('GET', `${BASE_URL}/item-master/pages/${pageId}/structure`);
try {
const headers = getAuthHeaders();
const response = await fetch(`${BASE_URL}/item-master/pages/${pageId}/structure`, {
method: 'GET',
headers,
});
if (!response.ok) {
await handleApiError(response);
}
const result: ApiResponse<PageStructureResponse> = await response.json();
apiLogger.logResponse('GET', `${BASE_URL}/item-master/pages/${pageId}/structure`, response.status, result, startTime);
return result.data;
} catch (error) {
if (error instanceof TypeError) {
apiLogger.logError('GET', `${BASE_URL}/item-master/pages/${pageId}/structure`, error, undefined, startTime);
throw new Error('네트워크 연결을 확인해주세요. 서버에 연결할 수 없습니다.');
}
apiLogger.logError('GET', `${BASE_URL}/item-master/pages/${pageId}/structure`, error as Error, undefined, startTime);
throw error;
}
},
/**
* 페이지 관계 조회
* GET /v1/item-master/pages/{id}/relationships
* 2025-11-27 신규 API
*/
getRelationships: async (pageId: number): Promise<EntityRelationshipResponse[]> => {
const startTime = apiLogger.logRequest('GET', `${BASE_URL}/item-master/pages/${pageId}/relationships`);
try {
const headers = getAuthHeaders();
const response = await fetch(`${BASE_URL}/item-master/pages/${pageId}/relationships`, {
method: 'GET',
headers,
});
if (!response.ok) {
await handleApiError(response);
}
const result: ApiResponse<EntityRelationshipResponse[]> = await response.json();
apiLogger.logResponse('GET', `${BASE_URL}/item-master/pages/${pageId}/relationships`, response.status, result, startTime);
return result.data;
} catch (error) {
if (error instanceof TypeError) {
apiLogger.logError('GET', `${BASE_URL}/item-master/pages/${pageId}/relationships`, error, undefined, startTime);
throw new Error('네트워크 연결을 확인해주세요. 서버에 연결할 수 없습니다.');
}
apiLogger.logError('GET', `${BASE_URL}/item-master/pages/${pageId}/relationships`, error as Error, undefined, startTime);
throw error;
}
},
/**
* 페이지에 필드 직접 연결
* POST /v1/item-master/pages/{id}/link-field
* 2025-11-27 신규 API
*/
linkField: async (pageId: number, data: LinkEntityRequest): Promise<ApiResponse<ItemFieldResponse>> => {
const startTime = apiLogger.logRequest('POST', `${BASE_URL}/item-master/pages/${pageId}/link-field`, data);
try {
const headers = getAuthHeaders();
const response = await fetch(`${BASE_URL}/item-master/pages/${pageId}/link-field`, {
method: 'POST',
headers,
body: JSON.stringify(data),
});
if (!response.ok) {
await handleApiError(response);
}
const result: ApiResponse<ItemFieldResponse> = await response.json();
apiLogger.logResponse('POST', `${BASE_URL}/item-master/pages/${pageId}/link-field`, response.status, result, startTime);
return result;
} catch (error) {
if (error instanceof TypeError) {
apiLogger.logError('POST', `${BASE_URL}/item-master/pages/${pageId}/link-field`, error, undefined, startTime);
throw new Error('네트워크 연결을 확인해주세요. 서버에 연결할 수 없습니다.');
}
apiLogger.logError('POST', `${BASE_URL}/item-master/pages/${pageId}/link-field`, error as Error, undefined, startTime);
throw error;
}
},
/**
* 페이지에서 필드 연결 해제
* DELETE /v1/item-master/pages/{id}/unlink-field/{fieldId}
* 2025-11-27 신규 API
*/
unlinkField: async (pageId: number, fieldId: number): Promise<ApiResponse<void>> => {
const startTime = apiLogger.logRequest('DELETE', `${BASE_URL}/item-master/pages/${pageId}/unlink-field/${fieldId}`);
try {
const headers = getAuthHeaders();
const response = await fetch(`${BASE_URL}/item-master/pages/${pageId}/unlink-field/${fieldId}`, {
method: 'DELETE',
headers,
});
if (!response.ok) {
await handleApiError(response);
}
const result: ApiResponse<void> = await response.json();
apiLogger.logResponse('DELETE', `${BASE_URL}/item-master/pages/${pageId}/unlink-field/${fieldId}`, response.status, result, startTime);
return result;
} catch (error) {
if (error instanceof TypeError) {
apiLogger.logError('DELETE', `${BASE_URL}/item-master/pages/${pageId}/unlink-field/${fieldId}`, error, undefined, startTime);
throw new Error('네트워크 연결을 확인해주세요. 서버에 연결할 수 없습니다.');
}
apiLogger.logError('DELETE', `${BASE_URL}/item-master/pages/${pageId}/unlink-field/${fieldId}`, error as Error, undefined, startTime);
throw error;
}
},
},
// ============================================
@@ -223,7 +442,296 @@ export const itemMasterApi = {
// ============================================
sections: {
/**
* 섹션 생성
* 독립 섹션 목록 조회
* GET /v1/item-master/sections
* 2025-11-26 신규 API
* @param params.is_template - true: 템플릿만, false: 일반 섹션만, undefined: 전체
*/
list: async (params?: { is_template?: boolean }): Promise<ItemSectionResponse[]> => {
const queryParams = new URLSearchParams();
if (params?.is_template !== undefined) {
queryParams.set('is_template', String(params.is_template));
}
const queryString = queryParams.toString();
const url = `${BASE_URL}/item-master/sections${queryString ? `?${queryString}` : ''}`;
const startTime = apiLogger.logRequest('GET', url);
try {
const headers = getAuthHeaders();
const response = await fetch(url, { method: 'GET', headers });
if (!response.ok) {
await handleApiError(response);
}
const result: ApiResponse<ItemSectionResponse[]> = await response.json();
apiLogger.logResponse('GET', url, response.status, result, startTime);
return result.data;
} catch (error) {
if (error instanceof TypeError) {
apiLogger.logError('GET', url, error, undefined, startTime);
throw new Error('네트워크 연결을 확인해주세요. 서버에 연결할 수 없습니다.');
}
apiLogger.logError('GET', url, error as Error, undefined, startTime);
throw error;
}
},
/**
* 독립 섹션 생성 (페이지에 연결되지 않은 섹션)
* POST /v1/item-master/sections
* 2025-11-26 신규 API
*/
createIndependent: async (data: IndependentSectionRequest): Promise<ItemSectionResponse> => {
const startTime = apiLogger.logRequest('POST', `${BASE_URL}/item-master/sections`, data);
try {
const headers = getAuthHeaders();
const response = await fetch(`${BASE_URL}/item-master/sections`, {
method: 'POST',
headers,
body: JSON.stringify(data),
});
if (!response.ok) {
await handleApiError(response);
}
const result: ApiResponse<ItemSectionResponse> = await response.json();
apiLogger.logResponse('POST', `${BASE_URL}/item-master/sections`, response.status, result, startTime);
return result.data;
} catch (error) {
if (error instanceof TypeError) {
apiLogger.logError('POST', `${BASE_URL}/item-master/sections`, error, undefined, startTime);
throw new Error('네트워크 연결을 확인해주세요. 서버에 연결할 수 없습니다.');
}
apiLogger.logError('POST', `${BASE_URL}/item-master/sections`, error as Error, undefined, startTime);
throw error;
}
},
/**
* 섹션 복제
* POST /v1/item-master/sections/{id}/clone
* 2025-11-26 신규 API
*/
clone: async (id: number): Promise<ItemSectionResponse> => {
const startTime = apiLogger.logRequest('POST', `${BASE_URL}/item-master/sections/${id}/clone`);
try {
const headers = getAuthHeaders();
const response = await fetch(`${BASE_URL}/item-master/sections/${id}/clone`, { method: 'POST', headers });
if (!response.ok) {
await handleApiError(response);
}
const result: ApiResponse<ItemSectionResponse> = await response.json();
apiLogger.logResponse('POST', `${BASE_URL}/item-master/sections/${id}/clone`, response.status, result, startTime);
return result.data;
} catch (error) {
if (error instanceof TypeError) {
apiLogger.logError('POST', `${BASE_URL}/item-master/sections/${id}/clone`, error, undefined, startTime);
throw new Error('네트워크 연결을 확인해주세요. 서버에 연결할 수 없습니다.');
}
apiLogger.logError('POST', `${BASE_URL}/item-master/sections/${id}/clone`, error as Error, undefined, startTime);
throw error;
}
},
/**
* 섹션 사용처 조회
* GET /v1/item-master/sections/{id}/usage
* 2025-11-26 신규 API
*/
getUsage: async (id: number): Promise<SectionUsageResponse> => {
const startTime = apiLogger.logRequest('GET', `${BASE_URL}/item-master/sections/${id}/usage`);
try {
const headers = getAuthHeaders();
const response = await fetch(`${BASE_URL}/item-master/sections/${id}/usage`, { method: 'GET', headers });
if (!response.ok) {
await handleApiError(response);
}
const result: ApiResponse<SectionUsageResponse> = await response.json();
apiLogger.logResponse('GET', `${BASE_URL}/item-master/sections/${id}/usage`, response.status, result, startTime);
return result.data;
} catch (error) {
if (error instanceof TypeError) {
apiLogger.logError('GET', `${BASE_URL}/item-master/sections/${id}/usage`, error, undefined, startTime);
throw new Error('네트워크 연결을 확인해주세요. 서버에 연결할 수 없습니다.');
}
apiLogger.logError('GET', `${BASE_URL}/item-master/sections/${id}/usage`, error as Error, undefined, startTime);
throw error;
}
},
/**
* 섹션에 필드 연결
* POST /v1/item-master/sections/{id}/link-field
* 2025-11-26 신규 API
*/
linkField: async (sectionId: number, data: LinkFieldRequest): Promise<ApiResponse<ItemFieldResponse>> => {
const startTime = apiLogger.logRequest('POST', `${BASE_URL}/item-master/sections/${sectionId}/link-field`, data);
try {
const headers = getAuthHeaders();
const response = await fetch(`${BASE_URL}/item-master/sections/${sectionId}/link-field`, {
method: 'POST',
headers,
body: JSON.stringify(data),
});
if (!response.ok) {
await handleApiError(response);
}
const result: ApiResponse<ItemFieldResponse> = await response.json();
apiLogger.logResponse('POST', `${BASE_URL}/item-master/sections/${sectionId}/link-field`, response.status, result, startTime);
return result;
} catch (error) {
if (error instanceof TypeError) {
apiLogger.logError('POST', `${BASE_URL}/item-master/sections/${sectionId}/link-field`, error, undefined, startTime);
throw new Error('네트워크 연결을 확인해주세요. 서버에 연결할 수 없습니다.');
}
apiLogger.logError('POST', `${BASE_URL}/item-master/sections/${sectionId}/link-field`, error as Error, undefined, startTime);
throw error;
}
},
/**
* 섹션에서 필드 연결 해제
* DELETE /v1/item-master/sections/{id}/unlink-field/{fieldId}
* 2025-11-26 신규 API
*/
unlinkField: async (sectionId: number, fieldId: number): Promise<ApiResponse<void>> => {
const startTime = apiLogger.logRequest('DELETE', `${BASE_URL}/item-master/sections/${sectionId}/unlink-field/${fieldId}`);
try {
const headers = getAuthHeaders();
const response = await fetch(`${BASE_URL}/item-master/sections/${sectionId}/unlink-field/${fieldId}`, {
method: 'DELETE',
headers,
});
if (!response.ok) {
await handleApiError(response);
}
const result: ApiResponse<void> = await response.json();
apiLogger.logResponse('DELETE', `${BASE_URL}/item-master/sections/${sectionId}/unlink-field/${fieldId}`, response.status, result, startTime);
return result;
} catch (error) {
if (error instanceof TypeError) {
apiLogger.logError('DELETE', `${BASE_URL}/item-master/sections/${sectionId}/unlink-field/${fieldId}`, error, undefined, startTime);
throw new Error('네트워크 연결을 확인해주세요. 서버에 연결할 수 없습니다.');
}
apiLogger.logError('DELETE', `${BASE_URL}/item-master/sections/${sectionId}/unlink-field/${fieldId}`, error as Error, undefined, startTime);
throw error;
}
},
/**
* 섹션에 BOM 연결
* POST /v1/item-master/sections/{id}/link-bom
* 2025-11-27 신규 API
*/
linkBom: async (sectionId: number, data: LinkBomRequest): Promise<ApiResponse<BomItemResponse>> => {
const startTime = apiLogger.logRequest('POST', `${BASE_URL}/item-master/sections/${sectionId}/link-bom`, data);
try {
const headers = getAuthHeaders();
const response = await fetch(`${BASE_URL}/item-master/sections/${sectionId}/link-bom`, {
method: 'POST',
headers,
body: JSON.stringify(data),
});
if (!response.ok) {
await handleApiError(response);
}
const result: ApiResponse<BomItemResponse> = await response.json();
apiLogger.logResponse('POST', `${BASE_URL}/item-master/sections/${sectionId}/link-bom`, response.status, result, startTime);
return result;
} catch (error) {
if (error instanceof TypeError) {
apiLogger.logError('POST', `${BASE_URL}/item-master/sections/${sectionId}/link-bom`, error, undefined, startTime);
throw new Error('네트워크 연결을 확인해주세요. 서버에 연결할 수 없습니다.');
}
apiLogger.logError('POST', `${BASE_URL}/item-master/sections/${sectionId}/link-bom`, error as Error, undefined, startTime);
throw error;
}
},
/**
* 섹션에서 BOM 연결 해제
* DELETE /v1/item-master/sections/{id}/unlink-bom/{bomId}
* 2025-11-27 신규 API
*/
unlinkBom: async (sectionId: number, bomId: number): Promise<ApiResponse<void>> => {
const startTime = apiLogger.logRequest('DELETE', `${BASE_URL}/item-master/sections/${sectionId}/unlink-bom/${bomId}`);
try {
const headers = getAuthHeaders();
const response = await fetch(`${BASE_URL}/item-master/sections/${sectionId}/unlink-bom/${bomId}`, {
method: 'DELETE',
headers,
});
if (!response.ok) {
await handleApiError(response);
}
const result: ApiResponse<void> = await response.json();
apiLogger.logResponse('DELETE', `${BASE_URL}/item-master/sections/${sectionId}/unlink-bom/${bomId}`, response.status, result, startTime);
return result;
} catch (error) {
if (error instanceof TypeError) {
apiLogger.logError('DELETE', `${BASE_URL}/item-master/sections/${sectionId}/unlink-bom/${bomId}`, error, undefined, startTime);
throw new Error('네트워크 연결을 확인해주세요. 서버에 연결할 수 없습니다.');
}
apiLogger.logError('DELETE', `${BASE_URL}/item-master/sections/${sectionId}/unlink-bom/${bomId}`, error as Error, undefined, startTime);
throw error;
}
},
/**
* 섹션 관계 조회
* GET /v1/item-master/sections/{id}/relationships
* 2025-11-27 신규 API
*/
getRelationships: async (sectionId: number): Promise<EntityRelationshipResponse[]> => {
const startTime = apiLogger.logRequest('GET', `${BASE_URL}/item-master/sections/${sectionId}/relationships`);
try {
const headers = getAuthHeaders();
const response = await fetch(`${BASE_URL}/item-master/sections/${sectionId}/relationships`, {
method: 'GET',
headers,
});
if (!response.ok) {
await handleApiError(response);
}
const result: ApiResponse<EntityRelationshipResponse[]> = await response.json();
apiLogger.logResponse('GET', `${BASE_URL}/item-master/sections/${sectionId}/relationships`, response.status, result, startTime);
return result.data;
} catch (error) {
if (error instanceof TypeError) {
apiLogger.logError('GET', `${BASE_URL}/item-master/sections/${sectionId}/relationships`, error, undefined, startTime);
throw new Error('네트워크 연결을 확인해주세요. 서버에 연결할 수 없습니다.');
}
apiLogger.logError('GET', `${BASE_URL}/item-master/sections/${sectionId}/relationships`, error as Error, undefined, startTime);
throw error;
}
},
/**
* 섹션 생성 (페이지 하위)
* POST /v1/item-master/pages/{pageId}/sections
*/
create: async (pageId: number, data: ItemSectionRequest): Promise<ApiResponse<ItemSectionResponse>> => {
@@ -363,7 +871,127 @@ export const itemMasterApi = {
// ============================================
fields: {
/**
* 필드 생성
* 독립 필드 목록 조회
* GET /v1/item-master/fields
* 2025-11-26 신규 API
*/
list: async (): Promise<ItemFieldResponse[]> => {
const startTime = apiLogger.logRequest('GET', `${BASE_URL}/item-master/fields`);
try {
const headers = getAuthHeaders();
const response = await fetch(`${BASE_URL}/item-master/fields`, { method: 'GET', headers });
if (!response.ok) {
await handleApiError(response);
}
const result: ApiResponse<ItemFieldResponse[]> = await response.json();
apiLogger.logResponse('GET', `${BASE_URL}/item-master/fields`, response.status, result, startTime);
return result.data;
} catch (error) {
if (error instanceof TypeError) {
apiLogger.logError('GET', `${BASE_URL}/item-master/fields`, error, undefined, startTime);
throw new Error('네트워크 연결을 확인해주세요. 서버에 연결할 수 없습니다.');
}
apiLogger.logError('GET', `${BASE_URL}/item-master/fields`, error as Error, undefined, startTime);
throw error;
}
},
/**
* 독립 필드 생성
* POST /v1/item-master/fields
* 2025-11-26 신규 API
*/
createIndependent: async (data: IndependentFieldRequest): Promise<ItemFieldResponse> => {
const startTime = apiLogger.logRequest('POST', `${BASE_URL}/item-master/fields`, data);
try {
const headers = getAuthHeaders();
const response = await fetch(`${BASE_URL}/item-master/fields`, {
method: 'POST',
headers,
body: JSON.stringify(data),
});
if (!response.ok) {
await handleApiError(response);
}
const result: ApiResponse<ItemFieldResponse> = await response.json();
apiLogger.logResponse('POST', `${BASE_URL}/item-master/fields`, response.status, result, startTime);
return result.data;
} catch (error) {
if (error instanceof TypeError) {
apiLogger.logError('POST', `${BASE_URL}/item-master/fields`, error, undefined, startTime);
throw new Error('네트워크 연결을 확인해주세요. 서버에 연결할 수 없습니다.');
}
apiLogger.logError('POST', `${BASE_URL}/item-master/fields`, error as Error, undefined, startTime);
throw error;
}
},
/**
* 필드 복제
* POST /v1/item-master/fields/{id}/clone
* 2025-11-26 신규 API
*/
clone: async (id: number): Promise<ItemFieldResponse> => {
const startTime = apiLogger.logRequest('POST', `${BASE_URL}/item-master/fields/${id}/clone`);
try {
const headers = getAuthHeaders();
const response = await fetch(`${BASE_URL}/item-master/fields/${id}/clone`, { method: 'POST', headers });
if (!response.ok) {
await handleApiError(response);
}
const result: ApiResponse<ItemFieldResponse> = await response.json();
apiLogger.logResponse('POST', `${BASE_URL}/item-master/fields/${id}/clone`, response.status, result, startTime);
return result.data;
} catch (error) {
if (error instanceof TypeError) {
apiLogger.logError('POST', `${BASE_URL}/item-master/fields/${id}/clone`, error, undefined, startTime);
throw new Error('네트워크 연결을 확인해주세요. 서버에 연결할 수 없습니다.');
}
apiLogger.logError('POST', `${BASE_URL}/item-master/fields/${id}/clone`, error as Error, undefined, startTime);
throw error;
}
},
/**
* 필드 사용처 조회
* GET /v1/item-master/fields/{id}/usage
* 2025-11-26 신규 API
*/
getUsage: async (id: number): Promise<FieldUsageResponse> => {
const startTime = apiLogger.logRequest('GET', `${BASE_URL}/item-master/fields/${id}/usage`);
try {
const headers = getAuthHeaders();
const response = await fetch(`${BASE_URL}/item-master/fields/${id}/usage`, { method: 'GET', headers });
if (!response.ok) {
await handleApiError(response);
}
const result: ApiResponse<FieldUsageResponse> = await response.json();
apiLogger.logResponse('GET', `${BASE_URL}/item-master/fields/${id}/usage`, response.status, result, startTime);
return result.data;
} catch (error) {
if (error instanceof TypeError) {
apiLogger.logError('GET', `${BASE_URL}/item-master/fields/${id}/usage`, error, undefined, startTime);
throw new Error('네트워크 연결을 확인해주세요. 서버에 연결할 수 없습니다.');
}
apiLogger.logError('GET', `${BASE_URL}/item-master/fields/${id}/usage`, error as Error, undefined, startTime);
throw error;
}
},
/**
* 필드 생성 (섹션 하위)
* POST /v1/item-master/sections/{sectionId}/fields
*/
create: async (sectionId: number, data: ItemFieldRequest): Promise<ApiResponse<ItemFieldResponse>> => {
@@ -503,7 +1131,69 @@ export const itemMasterApi = {
// ============================================
bomItems: {
/**
* BOM 항목 생성
* 독립 BOM 목록 조회
* GET /v1/item-master/bom-items
* 2025-11-26 신규 API
*/
list: async (): Promise<BomItemResponse[]> => {
const startTime = apiLogger.logRequest('GET', `${BASE_URL}/item-master/bom-items`);
try {
const headers = getAuthHeaders();
const response = await fetch(`${BASE_URL}/item-master/bom-items`, { method: 'GET', headers });
if (!response.ok) {
await handleApiError(response);
}
const result: ApiResponse<BomItemResponse[]> = await response.json();
apiLogger.logResponse('GET', `${BASE_URL}/item-master/bom-items`, response.status, result, startTime);
return result.data;
} catch (error) {
if (error instanceof TypeError) {
apiLogger.logError('GET', `${BASE_URL}/item-master/bom-items`, error, undefined, startTime);
throw new Error('네트워크 연결을 확인해주세요. 서버에 연결할 수 없습니다.');
}
apiLogger.logError('GET', `${BASE_URL}/item-master/bom-items`, error as Error, undefined, startTime);
throw error;
}
},
/**
* 독립 BOM 항목 생성
* POST /v1/item-master/bom-items
* 2025-11-26 신규 API
*/
createIndependent: async (data: IndependentBomItemRequest): Promise<BomItemResponse> => {
const startTime = apiLogger.logRequest('POST', `${BASE_URL}/item-master/bom-items`, data);
try {
const headers = getAuthHeaders();
const response = await fetch(`${BASE_URL}/item-master/bom-items`, {
method: 'POST',
headers,
body: JSON.stringify(data),
});
if (!response.ok) {
await handleApiError(response);
}
const result: ApiResponse<BomItemResponse> = await response.json();
apiLogger.logResponse('POST', `${BASE_URL}/item-master/bom-items`, response.status, result, startTime);
return result.data;
} catch (error) {
if (error instanceof TypeError) {
apiLogger.logError('POST', `${BASE_URL}/item-master/bom-items`, error, undefined, startTime);
throw new Error('네트워크 연결을 확인해주세요. 서버에 연결할 수 없습니다.');
}
apiLogger.logError('POST', `${BASE_URL}/item-master/bom-items`, error as Error, undefined, startTime);
throw error;
}
},
/**
* BOM 항목 생성 (섹션 하위)
* POST /v1/item-master/sections/{sectionId}/bom-items
*/
create: async (sectionId: number, data: BomItemRequest): Promise<ApiResponse<BomItemResponse>> => {
@@ -745,11 +1435,56 @@ export const itemMasterApi = {
},
// ============================================
// 마스터 필드
// 엔티티 관계 (Entity Relationships)
// 2025-11-27 신규: 링크 테이블 기반 순서 변경
// ============================================
relationships: {
/**
* 관계 순서 변경
* POST /v1/item-master/relationships/reorder
* 2025-11-27 신규 API
*/
reorder: async (data: ReorderRelationshipsRequest): Promise<ApiResponse<EntityRelationshipResponse[]>> => {
const startTime = apiLogger.logRequest('POST', `${BASE_URL}/item-master/relationships/reorder`, data);
try {
const headers = getAuthHeaders();
const response = await fetch(`${BASE_URL}/item-master/relationships/reorder`, {
method: 'POST',
headers,
body: JSON.stringify(data),
});
if (!response.ok) {
await handleApiError(response);
}
const result: ApiResponse<EntityRelationshipResponse[]> = await response.json();
apiLogger.logResponse('POST', `${BASE_URL}/item-master/relationships/reorder`, response.status, result, startTime);
return result;
} catch (error) {
if (error instanceof TypeError) {
apiLogger.logError('POST', `${BASE_URL}/item-master/relationships/reorder`, error, undefined, startTime);
throw new Error('네트워크 연결을 확인해주세요. 서버에 연결할 수 없습니다.');
}
apiLogger.logError('POST', `${BASE_URL}/item-master/relationships/reorder`, error as Error, undefined, startTime);
throw error;
}
},
},
// ============================================
// 마스터 필드 (DEPRECATED)
// 2025-11-27: item_master_fields가 item_fields로 통합됨
// 독립 필드 = item_fields WHERE section_id IS NULL
// fields API 사용 권장 (fields.list, fields.createIndependent 등)
// ============================================
masterFields: {
/**
* 마스터 필드 목록 조회
* @deprecated 2025-11-27: fields.list() 사용 권장
* GET /v1/item-master/master-fields
* Note: init API에 포함되므로 일반적으로 직접 호출 불필요
*/
@@ -784,6 +1519,7 @@ export const itemMasterApi = {
/**
* 마스터 필드 생성
* @deprecated 2025-11-27: fields.createIndependent() 사용 권장
* POST /v1/item-master/master-fields
*/
create: async (data: MasterFieldRequest): Promise<ApiResponse<MasterFieldResponse>> => {
@@ -818,6 +1554,7 @@ export const itemMasterApi = {
/**
* 마스터 필드 수정
* @deprecated 2025-11-27: fields.update() 사용 권장
* PUT /v1/item-master/master-fields/{id}
*/
update: async (id: number, data: Partial<MasterFieldRequest>): Promise<ApiResponse<MasterFieldResponse>> => {
@@ -852,6 +1589,7 @@ export const itemMasterApi = {
/**
* 마스터 필드 삭제
* @deprecated 2025-11-27: fields.delete() 사용 권장
* DELETE /v1/item-master/master-fields/{id}
*/
delete: async (id: number): Promise<ApiResponse<void>> => {

View File

@@ -44,36 +44,16 @@ const SECTION_TYPE_REVERSE_MAP: Record<string, 'fields' | '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',
};
// 2025-11-26: field_type은 API와 Frontend가 동일한 값을 사용하므로 변환 불필요
// API & Frontend: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea'
type FieldType = 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea';
/**
* Frontend field_type → API field_type 변환
* 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',
const getFieldType = (type: string): FieldType => {
const validTypes: FieldType[] = ['textbox', 'number', 'dropdown', 'checkbox', 'date', 'textarea'];
return validTypes.includes(type as FieldType) ? (type as FieldType) : 'textbox';
};
// ============================================
@@ -91,8 +71,10 @@ export const transformPageResponse = (
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,
description: response.description ?? null, // 2025-11-26 추가
absolute_path: response.absolute_path || '', // null일 경우 빈 문자열
is_active: response.is_active,
order_no: response.order_no ?? 0, // 2025-11-26 추가
sections: response.sections?.map(transformSectionResponse) || [],
created_by: response.created_by,
updated_by: response.updated_by,
@@ -104,6 +86,7 @@ export const transformPageResponse = (
/**
* ItemSectionResponse → ItemSection 변환
* 주요 변환: type → section_type, 값 변환 (fields → BASIC, bom → BOM)
* 2025-11-26: group_id, is_template, is_default, description 추가 (section_templates 통합)
*/
export const transformSectionResponse = (
response: ItemSectionResponse
@@ -111,12 +94,17 @@ export const transformSectionResponse = (
return {
id: response.id,
tenant_id: response.tenant_id,
page_id: response.page_id,
group_id: response.group_id, // 2025-11-26 추가: 독립 섹션 그룹화용
page_id: response.page_id, // null이면 독립 섹션
title: response.title,
section_type: SECTION_TYPE_MAP[response.type] || 'BASIC', // 타입 값 변환
description: response.description, // 2025-11-26 추가
order_no: response.order_no,
is_template: response.is_template, // 2025-11-26 추가: 템플릿 여부 (section_templates 통합)
is_default: response.is_default, // 2025-11-26 추가: 기본 템플릿 여부
fields: response.fields?.map(transformFieldResponse) || [],
bom_items: response.bomItems?.map(transformBomItemResponse) || [],
// 2025-11-27: 백엔드가 bom_items (snake_case)로 반환하므로 둘 다 체크
bom_items: (response.bom_items || response.bomItems)?.map(transformBomItemResponse) || [],
created_by: response.created_by,
updated_by: response.updated_by,
created_at: response.created_at,
@@ -126,7 +114,7 @@ export const transformSectionResponse = (
/**
* ItemFieldResponse → ItemField 변환
* 주요 변환: field_type 값 변환 (textbox → TEXT, dropdown → SELECT 등)
* 2025-11-26: field_type은 API와 동일한 값 사용 (변환 불필요)
*/
export const transformFieldResponse = (
response: ItemFieldResponse
@@ -136,7 +124,7 @@ export const transformFieldResponse = (
tenant_id: response.tenant_id,
section_id: response.section_id,
field_name: response.field_name,
field_type: FIELD_TYPE_MAP[response.field_type] || 'TEXT', // 타입 값 변환
field_type: getFieldType(response.field_type), // API와 동일한 타입
order_no: response.order_no,
is_required: response.is_required,
placeholder: response.placeholder,
@@ -200,7 +188,7 @@ export const transformSectionTemplateResponse = (
/**
* MasterFieldResponse → ItemMasterField 변환
* 주요 변환: field_type 값 변환 (textbox → TEXT, dropdown → SELECT 등)
* 2025-11-26: field_type은 API와 동일한 값 사용, 속성명도 API와 통일
*/
export const transformMasterFieldResponse = (
response: MasterFieldResponse
@@ -209,11 +197,14 @@ export const transformMasterFieldResponse = (
id: response.id,
tenant_id: response.tenant_id,
field_name: response.field_name,
field_type: FIELD_TYPE_MAP[response.field_type] || 'TEXT', // 타입 값 변환
field_type: getFieldType(response.field_type), // API와 동일한 타입
category: response.category,
description: response.description,
default_validation: response.validation_rules, // 필드명 매핑
default_properties: response.properties, // 필드명 매핑
is_common: response.is_common ?? false, // 공통 필드 여부
default_value: response.default_value ?? null, // 기본값
options: response.options ?? null, // dropdown 옵션
validation_rules: response.validation_rules ?? null, // 검증 규칙
properties: response.properties ?? null, // 추가 속성
created_by: response.created_by,
updated_by: response.updated_by,
created_at: response.created_at,
@@ -242,14 +233,12 @@ export const transformSectionToRequest = (
/**
* ItemField → ItemFieldRequest 변환
* 주요 변환: field_type 값 역변환 (TEXT → textbox, SELECT → dropdown 등)
* 2025-11-26: field_type은 API와 동일한 값 사용 (변환 불필요)
*/
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',
field_type: field.field_type || 'textbox', // API와 동일한 타입
is_required: field.is_required ?? false,
placeholder: field.placeholder || null,
default_value: field.default_value || null,
@@ -295,23 +284,21 @@ export const transformSectionTemplateToRequest = (
/**
* ItemMasterField → MasterFieldRequest 변환
* 주요 변환: field_type 값 역변환, default_validation/properties 필드명 변환
* 2025-11-26: field_type과 속성명 모두 API와 동일하게 통일
*/
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',
field_type: field.field_type || 'textbox', // API와 동일한 타입
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, // 필드명 역변환
is_common: field.is_common ?? false,
default_value: field.default_value || undefined,
options: field.options || undefined,
validation_rules: field.validation_rules || undefined, // API와 동일
properties: field.properties || undefined, // API와 동일
};
};
@@ -323,8 +310,9 @@ export const transformMasterFieldToRequest = (
* 여러 페이지 응답을 한번에 변환
*/
export const transformPagesResponse = (
responses: ItemPageResponse[]
responses: ItemPageResponse[] | undefined | null
): ItemPage[] => {
if (!responses || !Array.isArray(responses)) return [];
return responses.map(transformPageResponse);
};
@@ -332,8 +320,9 @@ export const transformPagesResponse = (
* 여러 섹션 응답을 한번에 변환
*/
export const transformSectionsResponse = (
responses: ItemSectionResponse[]
responses: ItemSectionResponse[] | undefined | null
): ItemSection[] => {
if (!responses || !Array.isArray(responses)) return [];
return responses.map(transformSectionResponse);
};
@@ -341,8 +330,9 @@ export const transformSectionsResponse = (
* 여러 필드 응답을 한번에 변환
*/
export const transformFieldsResponse = (
responses: ItemFieldResponse[]
responses: ItemFieldResponse[] | undefined | null
): ItemField[] => {
if (!responses || !Array.isArray(responses)) return [];
return responses.map(transformFieldResponse);
};
@@ -350,8 +340,9 @@ export const transformFieldsResponse = (
* 여러 BOM 아이템 응답을 한번에 변환
*/
export const transformBomItemsResponse = (
responses: BomItemResponse[]
responses: BomItemResponse[] | undefined | null
): BOMItem[] => {
if (!responses || !Array.isArray(responses)) return [];
return responses.map(transformBomItemResponse);
};
@@ -359,8 +350,9 @@ export const transformBomItemsResponse = (
* 여러 섹션 템플릿 응답을 한번에 변환
*/
export const transformSectionTemplatesResponse = (
responses: SectionTemplateResponse[]
responses: SectionTemplateResponse[] | undefined | null
): SectionTemplate[] => {
if (!responses || !Array.isArray(responses)) return [];
return responses.map(transformSectionTemplateResponse);
};
@@ -368,8 +360,9 @@ export const transformSectionTemplatesResponse = (
* 여러 마스터 필드 응답을 한번에 변환
*/
export const transformMasterFieldsResponse = (
responses: MasterFieldResponse[]
responses: MasterFieldResponse[] | undefined | null
): ItemMasterField[] => {
if (!responses || !Array.isArray(responses)) return [];
return responses.map(transformMasterFieldResponse);
};
@@ -406,8 +399,9 @@ export const transformCustomTabResponse = (
* 여러 단위 옵션 응답을 한번에 변환
*/
export const transformUnitOptionsResponse = (
responses: UnitOptionResponse[]
responses: UnitOptionResponse[] | undefined | null
) => {
if (!responses || !Array.isArray(responses)) return [];
return responses.map(transformUnitOptionResponse);
};
@@ -415,7 +409,43 @@ export const transformUnitOptionsResponse = (
* 여러 커스텀 탭 응답을 한번에 변환
*/
export const transformCustomTabsResponse = (
responses: CustomTabResponse[]
responses: CustomTabResponse[] | undefined | null
) => {
if (!responses || !Array.isArray(responses)) return [];
return responses.map(transformCustomTabResponse);
};
/**
* ItemSectionResponse → SectionTemplate 변환
* 2025-11-26: 백엔드가 sectionTemplates 대신 sections를 반환하는 경우 사용
* is_template=true인 섹션을 SectionTemplate 형식으로 변환
*/
export const transformSectionTemplateFromSection = (
response: ItemSectionResponse
): SectionTemplate => {
return {
id: response.id,
tenant_id: response.tenant_id,
template_name: response.title, // title → template_name
section_type: SECTION_TYPE_MAP[response.type] || 'BASIC', // type → section_type
description: response.description,
default_fields: null, // API 응답에 없으므로 null
// 필드 변환은 별도 처리 필요 (fields가 있으면 TemplateField로 변환)
fields: response.fields?.map(field => ({
id: field.id.toString(),
name: field.field_name,
fieldKey: field.field_name.toLowerCase().replace(/\s+/g, '_'),
property: {
inputType: getFieldType(field.field_type),
required: field.is_required,
options: field.options?.map((opt: { label: string; value: string }) => opt.label || opt.value),
},
description: field.placeholder || undefined,
})),
bomItems: response.bomItems?.map(transformBomItemResponse),
created_by: response.created_by,
updated_by: response.updated_by,
created_at: response.created_at,
updated_at: response.updated_at,
};
};