/** * useFormStructure Hook * * API에서 품목 유형별 폼 구조를 로드하는 훅 * - 캐싱 지원 (5분 TTL) * - 에러 처리 및 재시도 * - Mock 데이터 폴백 (API 미구현 시) */ 'use client'; import { useState, useEffect, useCallback, useRef } from 'react'; import type { ItemType, PartType } from '@/types/item'; import type { FormStructure, FormStructureResponse, UseFormStructureReturn, DynamicSection, DynamicField, ConditionalSection, } from '../types'; // ===== 캐시 설정 ===== const CACHE_TTL = 5 * 60 * 1000; // 5분 const formStructureCache = new Map(); /** * 캐시 키 생성 */ function getCacheKey(itemType: ItemType, partType?: PartType): string { return partType ? `${itemType}_${partType}` : itemType; } /** * 캐시에서 데이터 가져오기 */ function getFromCache(key: string): FormStructure | null { const cached = formStructureCache.get(key); if (!cached) return null; const isExpired = Date.now() - cached.timestamp > CACHE_TTL; if (isExpired) { formStructureCache.delete(key); return null; } return cached.data; } /** * 캐시에 데이터 저장 */ function setToCache(key: string, data: FormStructure): void { formStructureCache.set(key, { data, timestamp: Date.now() }); } // ===== Mock 데이터 (API 미구현 시 사용) ===== /** * 제품(FG) Mock 폼 구조 */ function getMockFGFormStructure(): FormStructure { return { page: { id: 1, page_name: '제품 등록', item_type: 'FG', is_active: true, }, sections: [ { id: 101, title: '기본 정보', section_type: 'BASIC', order_no: 1, is_collapsible: false, is_default_open: true, fields: [ { id: 1001, field_name: '품목코드', field_key: 'item_code', field_type: 'textbox', order_no: 1, is_required: true, is_readonly: true, placeholder: '자동 생성', grid_row: 1, grid_col: 1, grid_span: 1, }, { id: 1002, field_name: '품목명', field_key: 'item_name', field_type: 'textbox', order_no: 2, is_required: true, placeholder: '품목명을 입력하세요', validation_rules: { maxLength: 100 }, grid_row: 1, grid_col: 2, grid_span: 2, }, { id: 1003, field_name: '제품 카테고리', field_key: 'product_category', field_type: 'dropdown', order_no: 3, is_required: true, dropdown_config: { options: [ { value: 'SCREEN', label: '스크린' }, { value: 'STEEL', label: '철재' }, ], placeholder: '카테고리 선택', }, grid_row: 1, grid_col: 4, grid_span: 1, }, { id: 1004, field_name: '단위', field_key: 'unit', field_type: 'dropdown', order_no: 4, is_required: true, dropdown_config: { options: [ { value: 'EA', label: 'EA (개)' }, { value: 'SET', label: 'SET (세트)' }, ], placeholder: '단위 선택', }, grid_row: 2, grid_col: 1, grid_span: 1, }, { id: 1005, field_name: '규격', field_key: 'specification', field_type: 'textbox', order_no: 5, is_required: false, placeholder: '규격을 입력하세요', grid_row: 2, grid_col: 2, grid_span: 2, }, { id: 1006, field_name: '활성 상태', field_key: 'is_active', field_type: 'switch', order_no: 6, is_required: false, default_value: true, grid_row: 2, grid_col: 4, grid_span: 1, }, ], }, { id: 102, title: '가격 정보', section_type: 'DETAIL', order_no: 2, is_collapsible: true, is_default_open: true, fields: [ { id: 1010, field_name: '판매 단가', field_key: 'sales_price', field_type: 'currency', order_no: 1, is_required: false, placeholder: '0', grid_row: 1, grid_col: 1, grid_span: 1, }, { id: 1011, field_name: '구매 단가', field_key: 'purchase_price', field_type: 'currency', order_no: 2, is_required: false, placeholder: '0', grid_row: 1, grid_col: 2, grid_span: 1, }, { id: 1012, field_name: '마진율 (%)', field_key: 'margin_rate', field_type: 'number', order_no: 3, is_required: false, placeholder: '0', validation_rules: { min: 0, max: 100 }, grid_row: 1, grid_col: 3, grid_span: 1, }, ], }, { id: 103, title: '부품 구성 (BOM)', section_type: 'BOM', order_no: 3, is_collapsible: true, is_default_open: true, fields: [], bom_config: { columns: [ { key: 'child_item_code', label: '품목코드', width: 150 }, { key: 'child_item_name', label: '품목명', width: 200 }, { key: 'specification', label: '규격', width: 150 }, { key: 'quantity', label: '수량', width: 80, type: 'number', editable: true }, { key: 'unit', label: '단위', width: 80 }, { key: 'note', label: '비고', width: 150, type: 'text', editable: true }, ], allow_search: true, search_endpoint: '/api/proxy/items/search', allow_add: true, allow_delete: true, allow_reorder: true, }, }, { id: 104, title: '인정 정보', section_type: 'CERTIFICATION', order_no: 4, is_collapsible: true, is_default_open: false, fields: [ { id: 1020, field_name: '인정번호', field_key: 'certification_number', field_type: 'textbox', order_no: 1, is_required: false, placeholder: '인정번호를 입력하세요', grid_row: 1, grid_col: 1, grid_span: 2, }, { id: 1021, field_name: '인정 시작일', field_key: 'certification_start_date', field_type: 'date', order_no: 2, is_required: false, grid_row: 1, grid_col: 3, grid_span: 1, }, { id: 1022, field_name: '인정 종료일', field_key: 'certification_end_date', field_type: 'date', order_no: 3, is_required: false, grid_row: 1, grid_col: 4, grid_span: 1, }, { id: 1023, field_name: '시방서', field_key: 'specification_file', field_type: 'file', order_no: 4, is_required: false, file_config: { accept: '.pdf,.doc,.docx', max_size: 10 * 1024 * 1024, // 10MB }, grid_row: 2, grid_col: 1, grid_span: 2, }, { id: 1024, field_name: '인정서', field_key: 'certification_file', field_type: 'file', order_no: 5, is_required: false, file_config: { accept: '.pdf,.doc,.docx', max_size: 10 * 1024 * 1024, }, grid_row: 2, grid_col: 3, grid_span: 2, }, ], }, ], conditionalSections: [], conditionalFields: [], }; } /** * 부품(PT) Mock 폼 구조 */ function getMockPTFormStructure(partType?: PartType): FormStructure { const baseFields: DynamicField[] = [ { id: 2001, field_name: '품목코드', field_key: 'item_code', field_type: 'textbox', order_no: 1, is_required: true, is_readonly: true, placeholder: '자동 생성', grid_row: 1, grid_col: 1, grid_span: 1, }, { id: 2002, field_name: '품목명', field_key: 'item_name', field_type: 'textbox', order_no: 2, is_required: true, placeholder: '품목명을 입력하세요', grid_row: 1, grid_col: 2, grid_span: 2, }, { id: 2003, field_name: '부품 유형', field_key: 'part_type', field_type: 'dropdown', order_no: 3, is_required: true, dropdown_config: { options: [ { value: 'ASSEMBLY', label: '조립 부품' }, { value: 'BENDING', label: '절곡 부품' }, { value: 'PURCHASED', label: '구매 부품' }, ], placeholder: '부품 유형 선택', }, grid_row: 1, grid_col: 4, grid_span: 1, }, { id: 2004, field_name: '단위', field_key: 'unit', field_type: 'dropdown', order_no: 4, is_required: true, dropdown_config: { options: [ { value: 'EA', label: 'EA (개)' }, { value: 'SET', label: 'SET (세트)' }, { value: 'M', label: 'M (미터)' }, ], }, grid_row: 2, grid_col: 1, grid_span: 1, }, ]; const sections: DynamicSection[] = [ { id: 201, title: '기본 정보', section_type: 'BASIC', order_no: 1, is_collapsible: false, is_default_open: true, fields: baseFields, }, ]; // 조립 부품 전용 섹션 const assemblySection: DynamicSection = { id: 202, title: '조립 부품 상세', section_type: 'DETAIL', order_no: 2, is_collapsible: true, is_default_open: true, display_condition: { field_key: 'part_type', operator: 'equals', value: 'ASSEMBLY', }, fields: [ { id: 2010, field_name: '설치 유형', field_key: 'installation_type', field_type: 'dropdown', order_no: 1, is_required: false, dropdown_config: { options: [ { value: 'WALL', label: '벽면형' }, { value: 'SIDE', label: '측면형' }, ], }, grid_row: 1, grid_col: 1, grid_span: 1, }, { id: 2011, field_name: '조립 종류', field_key: 'assembly_type', field_type: 'dropdown', order_no: 2, is_required: false, dropdown_config: { options: [ { value: 'M', label: 'M형' }, { value: 'T', label: 'T형' }, { value: 'C', label: 'C형' }, { value: 'D', label: 'D형' }, { value: 'S', label: 'S형' }, { value: 'U', label: 'U형' }, ], }, grid_row: 1, grid_col: 2, grid_span: 1, }, { id: 2012, field_name: '길이 (mm)', field_key: 'assembly_length', field_type: 'dropdown', order_no: 3, is_required: false, dropdown_config: { options: [ { value: '2438', label: '2438' }, { value: '3000', label: '3000' }, { value: '3500', label: '3500' }, { value: '4000', label: '4000' }, { value: '4300', label: '4300' }, ], }, grid_row: 1, grid_col: 3, grid_span: 1, }, ], }; // 절곡 부품 전용 섹션 const bendingSection: DynamicSection = { id: 203, title: '절곡 정보', section_type: 'BENDING', order_no: 2, is_collapsible: true, is_default_open: true, display_condition: { field_key: 'part_type', operator: 'equals', value: 'BENDING', }, fields: [ { id: 2020, field_name: '재질', field_key: 'material', field_type: 'dropdown', order_no: 1, is_required: true, dropdown_config: { options: [ { value: 'EGI_1.55T', label: 'EGI 1.55T' }, { value: 'SUS_1.2T', label: 'SUS 1.2T' }, { value: 'SUS_1.5T', label: 'SUS 1.5T' }, ], }, grid_row: 1, grid_col: 1, grid_span: 1, }, { id: 2021, field_name: '길이/목함 (mm)', field_key: 'bending_length', field_type: 'number', order_no: 2, is_required: false, placeholder: '길이 입력', grid_row: 1, grid_col: 2, grid_span: 1, }, { id: 2022, field_name: '전개도', field_key: 'bending_diagram', field_type: 'custom:drawing-canvas', order_no: 3, is_required: false, grid_row: 2, grid_col: 1, grid_span: 4, }, { id: 2023, field_name: '전개도 상세', field_key: 'bending_details', field_type: 'custom:bending-detail-table', order_no: 4, is_required: false, grid_row: 3, grid_col: 1, grid_span: 4, }, ], }; // 구매 부품 전용 섹션 const purchasedSection: DynamicSection = { id: 204, title: '구매 부품 상세', section_type: 'DETAIL', order_no: 2, is_collapsible: true, is_default_open: true, display_condition: { field_key: 'part_type', operator: 'equals', value: 'PURCHASED', }, fields: [ { id: 2030, field_name: '구매처', field_key: 'supplier', field_type: 'textbox', order_no: 1, is_required: false, placeholder: '구매처를 입력하세요', grid_row: 1, grid_col: 1, grid_span: 2, }, { id: 2031, field_name: '구매 단가', field_key: 'purchase_price', field_type: 'currency', order_no: 2, is_required: false, grid_row: 1, grid_col: 3, grid_span: 1, }, { id: 2032, field_name: '리드타임 (일)', field_key: 'lead_time', field_type: 'number', order_no: 3, is_required: false, placeholder: '0', grid_row: 1, grid_col: 4, grid_span: 1, }, ], }; sections.push(assemblySection, bendingSection, purchasedSection); // BOM 섹션 (조립/절곡 부품만) const bomSection: DynamicSection = { id: 205, title: '부품 구성 (BOM)', section_type: 'BOM', order_no: 3, is_collapsible: true, is_default_open: true, display_condition: { field_key: 'part_type', operator: 'in', value: ['ASSEMBLY', 'BENDING'], }, fields: [], bom_config: { columns: [ { key: 'child_item_code', label: '품목코드', width: 150 }, { key: 'child_item_name', label: '품목명', width: 200 }, { key: 'quantity', label: '수량', width: 80, type: 'number', editable: true }, { key: 'unit', label: '단위', width: 80 }, ], allow_search: true, search_endpoint: '/api/proxy/items/search', allow_add: true, allow_delete: true, }, }; sections.push(bomSection); return { page: { id: 2, page_name: '부품 등록', item_type: 'PT', part_type: partType, is_active: true, }, sections, conditionalSections: [], conditionalFields: [], }; } /** * 자재(RM/SM/CS) Mock 폼 구조 */ function getMockMaterialFormStructure(itemType: ItemType): FormStructure { const typeLabels: Record = { RM: '원자재', SM: '부자재', CS: '소모품', }; return { page: { id: itemType === 'RM' ? 3 : itemType === 'SM' ? 4 : 5, page_name: `${typeLabels[itemType]} 등록`, item_type: itemType, is_active: true, }, sections: [ { id: 301, title: '기본 정보', section_type: 'BASIC', order_no: 1, is_collapsible: false, is_default_open: true, fields: [ { id: 3001, field_name: '품목코드', field_key: 'item_code', field_type: 'textbox', order_no: 1, is_required: true, is_readonly: true, placeholder: '자동 생성', grid_row: 1, grid_col: 1, grid_span: 1, }, { id: 3002, field_name: '품목명', field_key: 'item_name', field_type: 'textbox', order_no: 2, is_required: true, placeholder: '품목명을 입력하세요', grid_row: 1, grid_col: 2, grid_span: 2, }, { id: 3003, field_name: '단위', field_key: 'unit', field_type: 'dropdown', order_no: 3, is_required: true, dropdown_config: { options: [ { value: 'EA', label: 'EA (개)' }, { value: 'KG', label: 'KG (킬로그램)' }, { value: 'M', label: 'M (미터)' }, { value: 'L', label: 'L (리터)' }, { value: 'BOX', label: 'BOX (박스)' }, ], }, grid_row: 1, grid_col: 4, grid_span: 1, }, { id: 3004, field_name: '규격', field_key: 'specification', field_type: 'textbox', order_no: 4, is_required: false, placeholder: '규격을 입력하세요', grid_row: 2, grid_col: 1, grid_span: 2, }, { id: 3005, field_name: '구매 단가', field_key: 'purchase_price', field_type: 'currency', order_no: 5, is_required: false, grid_row: 2, grid_col: 3, grid_span: 1, }, { id: 3006, field_name: '안전재고', field_key: 'safety_stock', field_type: 'number', order_no: 6, is_required: false, placeholder: '0', grid_row: 2, grid_col: 4, grid_span: 1, }, ], }, { id: 302, title: '구매 정보', section_type: 'DETAIL', order_no: 2, is_collapsible: true, is_default_open: false, fields: [ { id: 3010, field_name: '구매처', field_key: 'supplier', field_type: 'textbox', order_no: 1, is_required: false, placeholder: '구매처를 입력하세요', grid_row: 1, grid_col: 1, grid_span: 2, }, { id: 3011, field_name: '리드타임 (일)', field_key: 'lead_time', field_type: 'number', order_no: 2, is_required: false, placeholder: '0', grid_row: 1, grid_col: 3, grid_span: 1, }, { id: 3012, field_name: '비고', field_key: 'note', field_type: 'textarea', order_no: 3, is_required: false, placeholder: '비고를 입력하세요', grid_row: 2, grid_col: 1, grid_span: 4, }, ], }, ], conditionalSections: [], conditionalFields: [], }; } /** * Mock 데이터 가져오기 */ function getMockFormStructure(itemType: ItemType, partType?: PartType): FormStructure { switch (itemType) { case 'FG': return getMockFGFormStructure(); case 'PT': return getMockPTFormStructure(partType); case 'RM': case 'SM': case 'CS': return getMockMaterialFormStructure(itemType); default: return getMockFGFormStructure(); } } // ===== API 호출 ===== /** * 폼 구조 API 호출 */ async function fetchFormStructure( itemType: ItemType, partType?: PartType ): Promise { const endpoint = partType ? `/api/proxy/item-master/form-structure/${itemType}?part_type=${partType}` : `/api/proxy/item-master/form-structure/${itemType}`; try { const response = await fetch(endpoint); if (!response.ok) { // API가 404면 Mock 데이터 사용 if (response.status === 404) { console.warn(`[useFormStructure] API not found, using mock data for ${itemType}`); return getMockFormStructure(itemType, partType); } throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const result: FormStructureResponse = await response.json(); if (!result.success) { throw new Error(result.message || 'API returned unsuccessful response'); } // API 응답을 FormStructure 형식으로 변환 return { page: result.data.page, sections: result.data.sections, conditionalSections: result.data.conditional_sections || [], conditionalFields: result.data.conditional_fields || [], }; } catch (error) { console.warn(`[useFormStructure] API call failed, using mock data:`, error); // API 실패 시 Mock 데이터 폴백 return getMockFormStructure(itemType, partType); } } // ===== 훅 구현 ===== interface UseFormStructureOptions { itemType: ItemType; partType?: PartType; enabled?: boolean; useMock?: boolean; // 강제로 Mock 데이터 사용 } /** * useFormStructure Hook * * @param options - 훅 옵션 * @returns 폼 구조 데이터 및 상태 * * @example * const { formStructure, isLoading, error, refetch } = useFormStructure({ * itemType: 'FG', * }); * * @example * const { formStructure } = useFormStructure({ * itemType: 'PT', * partType: 'BENDING', * }); */ export function useFormStructure(options: UseFormStructureOptions): UseFormStructureReturn { const { itemType, partType, enabled = true, useMock = false } = options; const [formStructure, setFormStructure] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); // 이전 요청 취소용 const abortControllerRef = useRef(null); const cacheKey = getCacheKey(itemType, partType); /** * 폼 구조 로드 */ const loadFormStructure = useCallback(async () => { // 이전 요청 취소 if (abortControllerRef.current) { abortControllerRef.current.abort(); } abortControllerRef.current = new AbortController(); // 캐시 확인 const cached = getFromCache(cacheKey); if (cached) { setFormStructure(cached); setIsLoading(false); setError(null); return; } setIsLoading(true); setError(null); try { let data: FormStructure; if (useMock) { // 강제 Mock 모드 data = getMockFormStructure(itemType, partType); } else { // API 호출 (실패 시 자동으로 Mock 폴백) data = await fetchFormStructure(itemType, partType); } // 캐시에 저장 setToCache(cacheKey, data); setFormStructure(data); setError(null); } catch (err) { setError(err instanceof Error ? err : new Error('Unknown error')); } finally { setIsLoading(false); } }, [itemType, partType, cacheKey, useMock]); /** * 강제 새로고침 */ const refetch = useCallback(async () => { // 캐시 무효화 formStructureCache.delete(cacheKey); await loadFormStructure(); }, [cacheKey, loadFormStructure]); // 마운트 시 및 의존성 변경 시 로드 useEffect(() => { if (enabled) { loadFormStructure(); } return () => { // 언마운트 시 요청 취소 if (abortControllerRef.current) { abortControllerRef.current.abort(); } }; }, [enabled, loadFormStructure]); return { formStructure, isLoading, error, refetch, }; } // ===== 캐시 유틸리티 ===== /** * 폼 구조 캐시 초기화 */ export function clearFormStructureCache(): void { formStructureCache.clear(); } /** * 특정 품목 유형의 캐시 무효화 */ export function invalidateFormStructureCache(itemType: ItemType, partType?: PartType): void { const key = getCacheKey(itemType, partType); formStructureCache.delete(key); } export default useFormStructure;