From 8fd9cf2d40bb8286a8c1502e83e5b64cd8913e46 Mon Sep 17 00:00:00 2001 From: byeongcheolryu Date: Fri, 28 Nov 2025 19:57:52 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20field=5Fkey=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EB=B0=8F=20=ED=91=9C=EC=8B=9C=20=EA=B8=B0=EB=8A=A5=20=EC=99=84?= =?UTF-8?q?=EC=A0=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - field_key 저장 시 백엔드 형식({ID}_{사용자입력})으로 전송 - API 요청 전 field_key 유효성 검증 추가 - 계층구조 탭 필드 추가/수정 시 field_key 반영 - 섹션 탭에서 field_key 표시 시 사용자입력 부분만 추출 - sectionsAsTemplates useMemo에서 linkedSections/unlinkedSections 모두 수정 - 마스터 필드, 템플릿 필드 다이얼로그에서 field_key 입력 지원 - ItemMasterContext에 field_key 상태 업데이트 로직 추가 - transformers에서 field_key 변환 처리 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../items/ItemMasterDataManagement.tsx | 62 ++++++++++++------- .../dialogs/FieldDialog.tsx | 11 +++- .../dialogs/MasterFieldDialog.tsx | 11 +++- .../hooks/useFieldManagement.ts | 12 +++- .../hooks/useMasterFieldManagement.ts | 12 +++- .../hooks/useTemplateManagement.ts | 10 ++- src/contexts/ItemMasterContext.tsx | 34 +++++++++- src/lib/api/transformers.ts | 5 +- src/types/item-master-api.ts | 10 +++ 9 files changed, 133 insertions(+), 34 deletions(-) diff --git a/src/components/items/ItemMasterDataManagement.tsx b/src/components/items/ItemMasterDataManagement.tsx index 6c53c3e2..2faf7aed 100644 --- a/src/components/items/ItemMasterDataManagement.tsx +++ b/src/components/items/ItemMasterDataManagement.tsx @@ -329,18 +329,25 @@ export function ItemMasterDataManagement() { description: section.description || null, default_fields: null, // ItemField → TemplateField 변환 - fields: section.fields?.map(field => ({ - id: field.id.toString(), - name: field.field_name, - fieldKey: field.field_name.toLowerCase().replace(/\s+/g, '_'), - property: { - inputType: field.field_type, - // 2025-11-27: is_required와 properties.required 둘 다 체크 - required: field.is_required || field.properties?.required, - options: field.options?.map((opt: { label: string; value: string }) => opt.label || opt.value), - }, - description: field.placeholder || undefined, - } as TemplateField)), + // 2025-11-28: field_key에서 사용자입력 부분만 추출 (백엔드 형식: {ID}_{사용자입력}) + fields: section.fields?.map(field => { + const rawKey = field.field_key || ''; + const displayKey = rawKey.includes('_') + ? rawKey.substring(rawKey.indexOf('_') + 1) + : rawKey || field.field_name.toLowerCase().replace(/\s+/g, '_'); + return { + id: field.id.toString(), + name: field.field_name, + fieldKey: displayKey, + property: { + inputType: field.field_type, + // 2025-11-27: is_required와 properties.required 둘 다 체크 + required: field.is_required || field.properties?.required, + options: field.options?.map((opt: { label: string; value: string }) => opt.label || opt.value), + }, + description: field.placeholder || undefined, + } as TemplateField; + }), bomItems: section.bom_items, created_by: section.created_by || null, updated_by: section.updated_by || null, @@ -350,6 +357,7 @@ export function ItemMasterDataManagement() { ); // 2. 독립 섹션들 (page_id = null, 연결 해제된 섹션) + // 2025-11-28: field_key에서 사용자입력 부분만 추출 (백엔드 형식: {ID}_{사용자입력}) const unlinkedSections = independentSections.map(section => ({ id: section.id, tenant_id: section.tenant_id || 0, @@ -357,18 +365,24 @@ export function ItemMasterDataManagement() { section_type: section.section_type, description: section.description || null, default_fields: null, - fields: section.fields?.map(field => ({ - id: field.id.toString(), - name: field.field_name, - fieldKey: field.field_name.toLowerCase().replace(/\s+/g, '_'), - property: { - inputType: field.field_type, - // 2025-11-27: is_required와 properties.required 둘 다 체크 - required: field.is_required || field.properties?.required, - options: field.options?.map((opt: { label: string; value: string }) => opt.label || opt.value), - }, - description: field.placeholder || undefined, - } as TemplateField)), + fields: section.fields?.map(field => { + const rawKey = field.field_key || ''; + const displayKey = rawKey.includes('_') + ? rawKey.substring(rawKey.indexOf('_') + 1) + : rawKey || field.field_name.toLowerCase().replace(/\s+/g, '_'); + return { + id: field.id.toString(), + name: field.field_name, + fieldKey: displayKey, + property: { + inputType: field.field_type, + // 2025-11-27: is_required와 properties.required 둘 다 체크 + required: field.is_required || field.properties?.required, + options: field.options?.map((opt: { label: string; value: string }) => opt.label || opt.value), + }, + description: field.placeholder || undefined, + } as TemplateField; + }), bomItems: section.bom_items, created_by: section.created_by || null, updated_by: section.updated_by || null, diff --git a/src/components/items/ItemMasterDataManagement/dialogs/FieldDialog.tsx b/src/components/items/ItemMasterDataManagement/dialogs/FieldDialog.tsx index 18e6f459..a4f6b1e5 100644 --- a/src/components/items/ItemMasterDataManagement/dialogs/FieldDialog.tsx +++ b/src/components/items/ItemMasterDataManagement/dialogs/FieldDialog.tsx @@ -127,6 +127,9 @@ export function FieldDialog({ // 유효성 검사 const isNameEmpty = !newFieldName.trim(); const isKeyEmpty = !newFieldKey.trim(); + // 2025-11-28: field_key validation - 영문 시작, 영문+숫자+언더스코어만 허용 + const fieldKeyPattern = /^[a-zA-Z][a-zA-Z0-9_]*$/; + const isKeyInvalid = newFieldKey.trim() !== '' && !fieldKeyPattern.test(newFieldKey); const handleClose = () => { setIsSubmitted(false); @@ -288,11 +291,14 @@ export function FieldDialog({ value={newFieldKey} onChange={(e) => setNewFieldKey(e.target.value)} placeholder="예: itemName" - className={isSubmitted && isKeyEmpty ? 'border-red-500 focus-visible:ring-red-500' : ''} + className={(isSubmitted && isKeyEmpty) || isKeyInvalid ? 'border-red-500 focus-visible:ring-red-500' : ''} /> {isSubmitted && isKeyEmpty && (

필드 키를 입력해주세요

)} + {isKeyInvalid && ( +

영문으로 시작하고, 영문/숫자/언더스코어(_)만 사용 가능합니다

+ )} @@ -420,7 +426,8 @@ export function FieldDialog({ diff --git a/src/components/items/ItemMasterDataManagement/dialogs/MasterFieldDialog.tsx b/src/components/items/ItemMasterDataManagement/dialogs/MasterFieldDialog.tsx index a44499dd..071427d5 100644 --- a/src/components/items/ItemMasterDataManagement/dialogs/MasterFieldDialog.tsx +++ b/src/components/items/ItemMasterDataManagement/dialogs/MasterFieldDialog.tsx @@ -85,6 +85,9 @@ export function MasterFieldDialog({ // 유효성 검사 const isNameEmpty = !newMasterFieldName.trim(); const isKeyEmpty = !newMasterFieldKey.trim(); + // 2025-11-28: field_key validation - 영문 시작, 영문+숫자+언더스코어만 허용 + const fieldKeyPattern = /^[a-zA-Z][a-zA-Z0-9_]*$/; + const isKeyInvalid = newMasterFieldKey.trim() !== '' && !fieldKeyPattern.test(newMasterFieldKey); const handleClose = () => { setIsMasterFieldDialogOpen(false); @@ -105,7 +108,8 @@ export function MasterFieldDialog({ const handleSubmit = () => { setIsSubmitted(true); - if (!isNameEmpty && !isKeyEmpty) { + // 2025-11-28: field_key validation 추가 + if (!isNameEmpty && !isKeyEmpty && !isKeyInvalid) { if (editingMasterFieldId) { handleUpdateMasterField(); } else { @@ -147,11 +151,14 @@ export function MasterFieldDialog({ value={newMasterFieldKey} onChange={(e) => setNewMasterFieldKey(e.target.value)} placeholder="예: itemName" - className={isSubmitted && isKeyEmpty ? 'border-red-500 focus-visible:ring-red-500' : ''} + className={(isSubmitted && isKeyEmpty) || isKeyInvalid ? 'border-red-500 focus-visible:ring-red-500' : ''} /> {isSubmitted && isKeyEmpty && (

필드 키를 입력해주세요

)} + {isKeyInvalid && ( +

영문으로 시작하고, 영문/숫자/언더스코어(_)만 사용 가능합니다

+ )} diff --git a/src/components/items/ItemMasterDataManagement/hooks/useFieldManagement.ts b/src/components/items/ItemMasterDataManagement/hooks/useFieldManagement.ts index 7e198bb8..5c791f94 100644 --- a/src/components/items/ItemMasterDataManagement/hooks/useFieldManagement.ts +++ b/src/components/items/ItemMasterDataManagement/hooks/useFieldManagement.ts @@ -116,7 +116,8 @@ export function useFieldManagement(): UseFieldManagementReturn { const masterField = itemMasterFields.find(f => f.id === Number(selectedMasterFieldId)); if (masterField) { setNewFieldName(masterField.field_name); - setNewFieldKey(masterField.id.toString()); + // 2025-11-28: field_key 사용 (없으면 빈 문자열로 사용자가 입력하도록) + setNewFieldKey(''); setNewFieldInputType(masterField.field_type || 'textbox'); // properties에서 required 확인, 또는 validation_rules에서 확인 const isRequired = (masterField.properties as any)?.required || false; @@ -168,6 +169,7 @@ export function useFieldManagement(): UseFieldManagementReturn { section_id: Number(selectedSectionForField), master_field_id: masterFieldId, field_name: newFieldName, + field_key: newFieldKey, // 2025-11-28: field_key 추가 (백엔드에서 {ID}_{입력값} 형태로 저장) field_type: newFieldInputType, order_no: 0, is_required: newFieldRequired, @@ -229,7 +231,13 @@ export function useFieldManagement(): UseFieldManagementReturn { setSelectedSectionForField(Number(sectionId)); setEditingFieldId(field.id); setNewFieldName(field.field_name); - setNewFieldKey(field.id.toString()); + // 2025-11-28: field_key 사용 (없으면 빈 문자열) + // field_key 형식: {ID}_{사용자입력} → 사용자입력 부분만 추출해서 표시 + const fieldKeyValue = field.field_key || ''; + const userInputPart = fieldKeyValue.includes('_') + ? fieldKeyValue.substring(fieldKeyValue.indexOf('_') + 1) + : fieldKeyValue; + setNewFieldKey(userInputPart); setNewFieldInputType(field.field_type); // 2025-11-27: is_required와 properties.required 둘 다 체크 setNewFieldRequired(field.is_required || field.properties?.required || false); diff --git a/src/components/items/ItemMasterDataManagement/hooks/useMasterFieldManagement.ts b/src/components/items/ItemMasterDataManagement/hooks/useMasterFieldManagement.ts index 2b580242..a56a26fd 100644 --- a/src/components/items/ItemMasterDataManagement/hooks/useMasterFieldManagement.ts +++ b/src/components/items/ItemMasterDataManagement/hooks/useMasterFieldManagement.ts @@ -83,8 +83,10 @@ export function useMasterFieldManagement(): UseMasterFieldManagementReturn { } // ItemMasterField 타입에 맞게 필수 필드 포함 + // 2025-11-28: field_key 추가 (백엔드에서 {ID}_{입력값} 형태로 저장) const newMasterFieldData: Omit = { field_name: newMasterFieldName, + field_key: newMasterFieldKey, // 2025-11-28: field_key 추가 field_type: newMasterFieldInputType, category: newMasterFieldCategory || null, description: newMasterFieldDescription || null, @@ -109,10 +111,16 @@ export function useMasterFieldManagement(): UseMasterFieldManagementReturn { }; // 마스터 항목 수정 시작 + // 2025-11-28: field_key 추가 - {ID}_{사용자입력} 형식에서 사용자입력 부분만 추출 const handleEditMasterField = (field: ItemMasterField) => { setEditingMasterFieldId(field.id); setNewMasterFieldName(field.field_name); - setNewMasterFieldKey(field.id.toString()); + // 2025-11-28: field_key 형식 {ID}_{사용자입력}에서 사용자입력 부분만 추출 + const fieldKeyValue = field.field_key || ''; + const userInputPart = fieldKeyValue.includes('_') + ? fieldKeyValue.substring(fieldKeyValue.indexOf('_') + 1) + : fieldKeyValue; + setNewMasterFieldKey(userInputPart); setNewMasterFieldInputType(field.field_type || 'textbox'); setNewMasterFieldRequired((field.properties as any)?.required || false); setNewMasterFieldCategory(field.category || '공통'); @@ -132,8 +140,10 @@ export function useMasterFieldManagement(): UseMasterFieldManagementReturn { return; } + // 2025-11-28: field_key 수정 시에도 API 요청에 포함 const updateData: Partial = { field_name: newMasterFieldName, + field_key: newMasterFieldKey, // 2025-11-28: field_key 추가 field_type: newMasterFieldInputType, category: newMasterFieldCategory || null, description: newMasterFieldDescription || null, diff --git a/src/components/items/ItemMasterDataManagement/hooks/useTemplateManagement.ts b/src/components/items/ItemMasterDataManagement/hooks/useTemplateManagement.ts index 063dbf36..5cac426c 100644 --- a/src/components/items/ItemMasterDataManagement/hooks/useTemplateManagement.ts +++ b/src/components/items/ItemMasterDataManagement/hooks/useTemplateManagement.ts @@ -265,8 +265,10 @@ export function useTemplateManagement(): UseTemplateManagementReturn { try { // 수정 모드: 기존 필드 속성 업데이트 if (editingTemplateFieldId) { + // 2025-11-28: field_key 추가 (백엔드 요청에 포함) const updateData = { field_name: templateFieldName, + field_key: templateFieldKey, // 2025-11-28: field_key 추가 field_type: templateFieldInputType, is_required: templateFieldRequired, placeholder: templateFieldDescription || null, @@ -307,11 +309,17 @@ export function useTemplateManagement(): UseTemplateManagementReturn { }; // 템플릿 필드 수정 시작 + // 2025-11-28: field_key 형식 {ID}_{사용자입력}에서 사용자입력 부분만 추출 const handleEditTemplateField = (templateId: number, field: TemplateField) => { setCurrentTemplateId(templateId); setEditingTemplateFieldId(Number(field.id)); setTemplateFieldName(field.name); - setTemplateFieldKey(field.fieldKey); + // 2025-11-28: field_key 형식 {ID}_{사용자입력}에서 사용자입력 부분만 추출 + const fieldKeyValue = field.fieldKey || ''; + const userInputPart = fieldKeyValue.includes('_') + ? fieldKeyValue.substring(fieldKeyValue.indexOf('_') + 1) + : fieldKeyValue; + setTemplateFieldKey(userInputPart); setTemplateFieldInputType(field.property.inputType); setTemplateFieldRequired(field.property.required); setTemplateFieldOptions(field.property.options?.join(', ') || ''); diff --git a/src/contexts/ItemMasterContext.tsx b/src/contexts/ItemMasterContext.tsx index 89629ab3..0b579e45 100644 --- a/src/contexts/ItemMasterContext.tsx +++ b/src/contexts/ItemMasterContext.tsx @@ -261,6 +261,7 @@ export interface ItemMasterField { id: number; tenant_id: number; field_name: string; + field_key?: string | null; // 2025-11-28: field_key 추가 (형식: {ID}_{사용자입력}) field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea'; // API와 동일 category: string | null; description: string | null; @@ -296,6 +297,7 @@ export interface ItemField { section_id: number | null; // 외래키 - 섹션 ID (독립 필드는 null) master_field_id?: number | null; // 마스터 항목 ID (마스터에서 가져온 경우) field_name: string; // 항목명 (name → field_name) + field_key?: string | null; // 2025-11-28: 필드 키 (형식: {ID}_{사용자입력}, 백엔드에서 생성) field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea'; // 필드 타입 order_no: number; // 항목 순서 (order → order_no, required) is_required: boolean; // 필수 여부 @@ -305,6 +307,10 @@ export interface ItemField { validation_rules?: Record | null; // 검증 규칙 options?: Array<{ label: string; value: string }> | null; // dropdown 옵션 properties?: Record | null; // 추가 속성 + // 2025-11-28 추가: 잠금 기능 + is_locked?: boolean; // 잠금 여부 + locked_by?: number | null; // 잠금 설정자 + locked_at?: string | null; // 잠금 시간 created_by?: number | null; // 생성자 ID 추가 updated_by?: number | null; // 수정자 ID 추가 created_at: string; // 생성일 (camelCase → snake_case) @@ -879,8 +885,10 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) { try { // API 호출 - 2025-11-27: fields.createIndependent() 사용 (masterFields.create() deprecated) // Note: API가 ItemFieldResponse를 직접 반환 (wrapper 없음) + // 2025-11-28: field_key 추가 const response = await itemMasterApi.fields.createIndependent({ field_name: field.field_name, + field_key: field.field_key ?? undefined, // 2025-11-28: field_key 추가 field_type: field.field_type, category: field.category ?? undefined, description: field.description ?? undefined, @@ -893,10 +901,12 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) { // 응답 데이터 변환 및 state 업데이트 // 2025-11-27: API가 ItemFieldResponse를 직접 반환하므로 response를 직접 사용 + // 2025-11-28: field_key 추가 const newField: ItemMasterField = { id: response.id, tenant_id: response.tenant_id, field_name: response.field_name, + field_key: response.field_key, // 2025-11-28: field_key 추가 field_type: response.field_type, category: response.category, description: response.description, @@ -929,6 +939,7 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) { // API 호출 - 2025-11-27: fields.update() 사용 (masterFields.update() deprecated) const requestData: any = {}; if (updates.field_name !== undefined) requestData.field_name = updates.field_name; + if (updates.field_key !== undefined) requestData.field_key = updates.field_key; // 2025-11-28: field_key 추가 if (updates.field_type !== undefined) requestData.field_type = updates.field_type; if (updates.category !== undefined) requestData.category = updates.category; if (updates.description !== undefined) requestData.description = updates.description; @@ -944,11 +955,12 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) { throw new Error(response.message || '마스터 필드 수정 실패'); } - // state 업데이트 + // state 업데이트 - 2025-11-28: field_key 추가 setItemMasterFields(prev => prev.map(field => field.id === id ? { ...field, field_name: response.data!.field_name, + field_key: response.data!.field_key, // 2025-11-28: field_key 추가 field_type: response.data!.field_type, category: response.data!.category, description: response.data!.description, @@ -963,12 +975,14 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) { // 2025-11-27: 섹션탭/계층구조 실시간 반영 // sectionsAsTemplates useMemo가 [itemPages, independentSections] 둘 다 의존 + // 2025-11-28: field_key 추가 setIndependentSections(prev => prev.map(section => ({ ...section, fields: (section.fields || []).map(field => field.id === id ? { ...field, field_name: response.data!.field_name, + field_key: response.data!.field_key, // 2025-11-28: field_key 추가 field_type: response.data!.field_type, is_required: response.data!.is_required, default_value: response.data!.default_value, @@ -980,6 +994,7 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) { ) }))); + // 2025-11-28: field_key 추가 setItemPages(prev => prev.map(page => ({ ...page, sections: page.sections.map(section => ({ @@ -988,6 +1003,7 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) { field.id === id ? { ...field, field_name: response.data!.field_name, + field_key: response.data!.field_key, // 2025-11-28: field_key 추가 field_type: response.data!.field_type, is_required: response.data!.is_required, default_value: response.data!.default_value, @@ -1549,8 +1565,10 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) { const addFieldToSection = async (sectionId: number, fieldData: Omit) => { try { // API 호출 (null → undefined 변환) + // 2025-11-28: field_key 추가 const response = await itemMasterApi.fields.create(sectionId, { field_name: fieldData.field_name, + field_key: fieldData.field_key ?? undefined, // 2025-11-28: field_key 추가 field_type: fieldData.field_type, is_required: fieldData.is_required, default_value: fieldData.default_value ?? undefined, @@ -1566,12 +1584,14 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) { } // 응답 데이터 변환 및 state 업데이트 + // 2025-11-28: field_key 추가 const newField: ItemField = { id: response.data.id, tenant_id: response.data.tenant_id, section_id: response.data.section_id, master_field_id: response.data.master_field_id, field_name: response.data.field_name, + field_key: response.data.field_key, // 2025-11-28: field_key 추가 field_type: response.data.field_type, order_no: response.data.order_no, is_required: response.data.is_required, @@ -1627,10 +1647,12 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) { // 2025-11-27: 항목탭/속성탭 실시간 동기화 // ItemField → ItemMasterField 변환하여 추가 + // 2025-11-28: field_key 추가 const newMasterField: ItemMasterField = { id: newField.id, tenant_id: newField.tenant_id ?? 0, field_name: newField.field_name, + field_key: newField.field_key, // 2025-11-28: field_key 추가 field_type: newField.field_type, description: newField.placeholder ?? null, category: null, @@ -1660,6 +1682,8 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) { // API 호출 (null → undefined 변환) const requestData: any = {}; if (updates.field_name !== undefined) requestData.field_name = updates.field_name; + // 2025-11-28: field_key 추가 + if (updates.field_key !== undefined) requestData.field_key = updates.field_key ?? undefined; if (updates.field_type !== undefined) requestData.field_type = updates.field_type; if (updates.is_required !== undefined) requestData.is_required = updates.is_required; if (updates.default_value !== undefined) requestData.default_value = updates.default_value ?? undefined; @@ -1677,6 +1701,7 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) { } // state 업데이트 + // 2025-11-28: field_key 추가 setItemPages(prev => prev.map(page => ({ ...page, sections: page.sections.map(section => ({ @@ -1685,6 +1710,7 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) { field.id === fieldId ? { ...field, field_name: response.data!.field_name, + field_key: response.data!.field_key, // 2025-11-28: field_key 추가 field_type: response.data!.field_type, is_required: response.data!.is_required, default_value: response.data!.default_value, @@ -1701,10 +1727,12 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) { // 2025-11-27: itemMasterFields 동기화 (항목탭 실시간 반영) // ItemField → ItemMasterField 타입 매핑 + // 2025-11-28: field_key 추가 setItemMasterFields(prev => prev.map(mf => mf.id === fieldId ? { ...mf, field_name: response.data!.field_name, + field_key: response.data!.field_key, // 2025-11-28: field_key 추가 field_type: response.data!.field_type, is_required: response.data!.is_required, default_value: response.data!.default_value ?? null, @@ -1717,10 +1745,12 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) { )); // 2025-11-27: independentFields 동기화 (섹션에 연결되지 않은 필드) + // 2025-11-28: field_key 추가 setIndependentFields(prev => prev.map(field => field.id === fieldId ? { ...field, field_name: response.data!.field_name, + field_key: response.data!.field_key, // 2025-11-28: field_key 추가 field_type: response.data!.field_type, is_required: response.data!.is_required, default_value: response.data!.default_value, @@ -1735,12 +1765,14 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) { // 2025-11-27: independentSections 동기화 (섹션탭 실시간 반영) // sectionsAsTemplates useMemo가 [itemPages, independentSections] 둘 다 의존 + // 2025-11-28: field_key 추가 setIndependentSections(prev => prev.map(section => ({ ...section, fields: (section.fields || []).map(field => field.id === fieldId ? { ...field, field_name: response.data!.field_name, + field_key: response.data!.field_key, // 2025-11-28: field_key 추가 field_type: response.data!.field_type, is_required: response.data!.is_required, default_value: response.data!.default_value, diff --git a/src/lib/api/transformers.ts b/src/lib/api/transformers.ts index 2ebdd02d..795f8d1f 100644 --- a/src/lib/api/transformers.ts +++ b/src/lib/api/transformers.ts @@ -124,6 +124,7 @@ export const transformFieldResponse = ( tenant_id: response.tenant_id, section_id: response.section_id, field_name: response.field_name, + field_key: response.field_key, // 2025-11-28: field_key 추가 (형식: {ID}_{사용자입력}) field_type: getFieldType(response.field_type), // API와 동일한 타입 order_no: response.order_no, is_required: response.is_required, @@ -197,6 +198,7 @@ export const transformMasterFieldResponse = ( id: response.id, tenant_id: response.tenant_id, field_name: response.field_name, + field_key: response.field_key ?? null, // 2025-11-28: field_key 추가 (형식: {ID}_{사용자입력}) field_type: getFieldType(response.field_type), // API와 동일한 타입 category: response.category, description: response.description, @@ -431,10 +433,11 @@ export const transformSectionTemplateFromSection = ( description: response.description, default_fields: null, // API 응답에 없으므로 null // 필드 변환은 별도 처리 필요 (fields가 있으면 TemplateField로 변환) + // 2025-11-28: fieldKey를 실제 field_key 사용하도록 수정 (기존: field_name에서 생성) fields: response.fields?.map(field => ({ id: field.id.toString(), name: field.field_name, - fieldKey: field.field_name.toLowerCase().replace(/\s+/g, '_'), + fieldKey: field.field_key || field.field_name.toLowerCase().replace(/\s+/g, '_'), property: { inputType: getFieldType(field.field_type), required: field.is_required, diff --git a/src/types/item-master-api.ts b/src/types/item-master-api.ts index a9ab315a..a18aeceb 100644 --- a/src/types/item-master-api.ts +++ b/src/types/item-master-api.ts @@ -199,6 +199,7 @@ export interface LinkSectionRequest { */ export interface ItemFieldRequest { field_name: string; + field_key?: string; // 2025-11-28: 필드 키 (영문, 숫자, 언더스코어만 허용, 영문으로 시작) field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea'; is_required?: boolean; placeholder?: string; @@ -207,6 +208,7 @@ export interface ItemFieldRequest { validation_rules?: Record; // {"min": 0, "max": 100, "pattern": "regex"} options?: Array<{ label: string; value: string }>; // dropdown 옵션 properties?: Record; // {"unit": "mm", "precision": 2, "format": "YYYY-MM-DD"} + is_locked?: boolean; // 2025-11-28: 잠금 여부 } /** @@ -221,6 +223,7 @@ export interface ItemFieldResponse { section_id: number | null; // 섹션 ID (null이면 독립 필드/마스터 항목) master_field_id?: number | null; // 마스터 항목 ID (마스터에서 가져온 경우) field_name: string; + field_key: string | null; // 2025-11-28: 필드 키 (형식: {ID}_{사용자입력}) field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea'; order_no: number; is_required: boolean; @@ -234,6 +237,10 @@ export interface ItemFieldResponse { category: string | null; // 카테고리 (예: "공통", "완제품", "부품") description: string | null; // 필드 설명 is_common: boolean; // 공통 필드 여부 + // 2025-11-28 추가: 잠금 기능 + is_locked: boolean; // 잠금 여부 + locked_by: number | null; // 잠금 설정자 + locked_at: string | null; // 잠금 시간 created_by: number | null; updated_by: number | null; created_at: string; @@ -260,6 +267,7 @@ export interface FieldReorderRequest { export interface IndependentFieldRequest { group_id?: number; field_name: string; + field_key?: string; // 2025-11-28: 필드 키 (영문, 숫자, 언더스코어만 허용, 영문으로 시작) field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea'; is_required?: boolean; default_value?: string; @@ -272,6 +280,7 @@ export interface IndependentFieldRequest { category?: string; description?: string; is_common?: boolean; + is_locked?: boolean; // 2025-11-28: 잠금 여부 } /** @@ -522,6 +531,7 @@ export interface MasterFieldResponse { id: number; tenant_id: number; field_name: string; + field_key: string | null; // 2025-11-28: 필드 키 추가 (형식: {ID}_{사용자입력}) field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea'; category: string | null; description: string | null;