diff --git a/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx b/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx index c0327303..2aefb047 100644 --- a/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx +++ b/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx @@ -285,11 +285,14 @@ const RepeaterFieldGroupComponent: React.FC = (props) => // onChange 호출하여 부모에게 알림 if (onChange && items.length > 0) { + // 🆕 RepeaterFieldGroup이 관리하는 필드 목록 추출 + const repeaterFieldNames = (configRef.current.fields || []).map((f: any) => f.name); const dataWithMeta = items.map((item: any) => ({ ...item, _targetTable: targetTable, _originalItemIds: itemIds, // 🆕 원본 ID 목록도 함께 전달 _existingRecord: !!item.id, // 🆕 기존 레코드 플래그 (id가 있으면 기존 레코드) + _repeaterFields: repeaterFieldNames, // 🆕 품목 고유 필드 목록 })); onChange(dataWithMeta); } @@ -388,10 +391,13 @@ const RepeaterFieldGroupComponent: React.FC = (props) => // onChange 호출 (effectiveTargetTable 사용) if (onChange) { if (items.length > 0) { + // 🆕 RepeaterFieldGroup이 관리하는 필드 목록 추출 + const repeaterFieldNames = (configRef.current.fields || []).map((f: any) => f.name); const dataWithMeta = items.map((item: any) => ({ ...item, _targetTable: effectiveTargetTable, _existingRecord: !!item.id, + _repeaterFields: repeaterFieldNames, // 🆕 품목 고유 필드 목록 })); onChange(dataWithMeta); } else { @@ -673,26 +679,25 @@ const RepeaterFieldGroupComponent: React.FC = (props) => // 🆕 RepeaterInput에서 항목 변경 시 SplitPanelContext의 addedItemIds 동기화 const handleRepeaterChange = useCallback( (newValue: any[]) => { - // 🆕 분할 패널에서 우측인 경우, 새 항목에 FK 값과 targetTable 추가 - let valueWithMeta = newValue; + // 🆕 RepeaterFieldGroup이 관리하는 필드 목록 추출 + const repeaterFieldNames = (configRef.current.fields || []).map((f: any) => f.name); + + // 🆕 모든 항목에 메타데이터 추가 + let valueWithMeta = newValue.map((item: any) => ({ + ...item, + _targetTable: effectiveTargetTable || targetTable, + _existingRecord: !!item.id, + _repeaterFields: repeaterFieldNames, // 🆕 품목 고유 필드 목록 + })); - if (isRightPanel && effectiveTargetTable) { - valueWithMeta = newValue.map((item: any) => { - const itemWithMeta = { - ...item, - _targetTable: effectiveTargetTable, - }; - - // 🆕 FK 값이 있고 새 항목이면 FK 컬럼에 값 추가 - if (fkColumn && fkValue && item._isNewItem) { - itemWithMeta[fkColumn] = fkValue; - console.log("🔗 [RepeaterFieldGroup] 새 항목에 FK 값 추가:", { - fkColumn, - fkValue, - }); + // 🆕 분할 패널에서 우측인 경우, FK 값 추가 + if (isRightPanel && fkColumn && fkValue) { + valueWithMeta = valueWithMeta.map((item: any) => { + if (item._isNewItem) { + console.log("🔗 [RepeaterFieldGroup] 새 항목에 FK 값 추가:", { fkColumn, fkValue }); + return { ...item, [fkColumn]: fkValue }; } - - return itemWithMeta; + return item; }); } @@ -754,6 +759,7 @@ const RepeaterFieldGroupComponent: React.FC = (props) => screenContext?.updateFormData, isRightPanel, effectiveTargetTable, + targetTable, fkColumn, fkValue, fieldName, diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index f03774f1..c90cc741 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -690,6 +690,151 @@ export class ButtonActionExecutor { console.log("⚠️ [handleSave] formData 전체 내용:", context.formData); } + // 🆕 RepeaterFieldGroup JSON 문자열 파싱 및 저장 처리 + // formData에 JSON 배열 문자열이 저장된 경우 처리 (반복_필드_그룹 등) + const repeaterJsonKeys = Object.keys(context.formData).filter((key) => { + const value = context.formData[key]; + if (typeof value === "string" && value.startsWith("[") && value.endsWith("]")) { + try { + const parsed = JSON.parse(value); + return Array.isArray(parsed) && parsed.length > 0 && parsed[0]._targetTable; + } catch { + return false; + } + } + return false; + }); + + if (repeaterJsonKeys.length > 0) { + console.log("🔄 [handleSave] RepeaterFieldGroup JSON 문자열 감지:", repeaterJsonKeys); + + // 🆕 상단 폼 데이터(마스터 정보) 추출 + // RepeaterFieldGroup JSON과 컴포넌트 키를 제외한 나머지가 마스터 정보 + const masterFields: Record = {}; + Object.keys(context.formData).forEach((fieldKey) => { + // 제외 조건 + if (fieldKey.startsWith("comp_")) return; + if (fieldKey.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)) return; + if (fieldKey.endsWith("_label") || fieldKey.endsWith("_value_label")) return; + + const value = context.formData[fieldKey]; + + // JSON 배열 문자열 제외 (RepeaterFieldGroup 데이터) + if (typeof value === "string" && value.startsWith("[") && value.endsWith("]")) return; + + // 객체 타입인 경우 (범용_폼_모달 등) 내부 필드를 펼쳐서 추가 + if (typeof value === "object" && value !== null && !Array.isArray(value)) { + Object.entries(value).forEach(([innerKey, innerValue]) => { + if (innerKey.endsWith("_label") || innerKey.endsWith("_value_label")) return; + if (innerValue !== undefined && innerValue !== null && innerValue !== "") { + masterFields[innerKey] = innerValue; + } + }); + return; + } + + // 유효한 값만 포함 + if (value !== undefined && value !== null && value !== "") { + masterFields[fieldKey] = value; + } + }); + + console.log("📋 [handleSave] 상단 마스터 정보 (모든 품목에 적용):", masterFields); + + for (const key of repeaterJsonKeys) { + try { + const parsedData = JSON.parse(context.formData[key]); + const repeaterTargetTable = parsedData[0]?._targetTable; + + if (!repeaterTargetTable) { + console.warn(`⚠️ [handleSave] RepeaterFieldGroup targetTable 없음 (key: ${key})`); + continue; + } + + console.log(`📦 [handleSave] RepeaterFieldGroup 저장 시작: ${repeaterTargetTable}, ${parsedData.length}건`); + + // 🆕 품목 고유 필드 목록 (RepeaterFieldGroup 설정에서 가져옴) + // 첫 번째 아이템의 _repeaterFields에서 추출 + const repeaterFields: string[] = parsedData[0]?._repeaterFields || []; + const itemOnlyFields = new Set([...repeaterFields, 'id']); // id는 항상 포함 + + console.log("📋 [handleSave] RepeaterFieldGroup 품목 필드:", repeaterFields); + + for (const item of parsedData) { + // 메타 필드 제거 + const { _targetTable, _isNewItem, _existingRecord, _originalItemIds, _deletedItemIds, _repeaterFields, ...itemData } = item; + + // 🔧 품목 고유 필드만 추출 (RepeaterFieldGroup 설정 기반) + const itemOnlyData: Record = {}; + Object.keys(itemData).forEach((field) => { + if (itemOnlyFields.has(field)) { + itemOnlyData[field] = itemData[field]; + } + }); + + // 🔧 마스터 정보 + 품목 고유 정보 병합 + // masterFields: 상단 폼에서 수정한 최신 마스터 정보 + // itemOnlyData: 품목 고유 필드만 (품번, 품명, 수량 등) + const dataWithMeta: Record = { + ...masterFields, // 상단 마스터 정보 (최신) + ...itemOnlyData, // 품목 고유 필드만 + created_by: context.userId, + updated_by: context.userId, + company_code: context.companyCode, + }; + + // 불필요한 필드 제거 + Object.keys(dataWithMeta).forEach((field) => { + if (field.endsWith("_label") || field.endsWith("_value_label") || field.endsWith("_numberingRuleId")) { + delete dataWithMeta[field]; + } + }); + + // 새 레코드 vs 기존 레코드 판단 + const isNewRecord = _isNewItem || !item.id || item.id === "" || item.id === undefined; + + console.log(`📦 [handleSave] 저장할 데이터 (${isNewRecord ? 'INSERT' : 'UPDATE'}):`, { + id: item.id, + dataWithMeta, + }); + + if (isNewRecord) { + // INSERT - DynamicFormApi 사용하여 제어관리 실행 + delete dataWithMeta.id; + + const insertResult = await DynamicFormApi.saveFormData({ + screenId: context.screenId || 0, + tableName: repeaterTargetTable, + data: dataWithMeta as Record, + }); + console.log("✅ [handleSave] RepeaterFieldGroup INSERT 완료:", insertResult.data); + } else if (item.id && _existingRecord === true) { + // UPDATE - 기존 레코드 + const originalData = { id: item.id }; + const updatedData = { ...dataWithMeta, id: item.id }; + + const updateResult = await apiClient.put(`/table-management/tables/${repeaterTargetTable}/edit`, { + originalData, + updatedData, + }); + console.log("✅ [handleSave] RepeaterFieldGroup UPDATE 완료:", updateResult.data); + } + } + } catch (err) { + console.error(`❌ [handleSave] RepeaterFieldGroup 저장 실패 (key: ${key}):`, err); + } + } + + // RepeaterFieldGroup 저장 완료 후 새로고침 + console.log("✅ [handleSave] RepeaterFieldGroup 저장 완료"); + context.onRefresh?.(); + context.onFlowRefresh?.(); + window.dispatchEvent(new CustomEvent("closeEditModal")); + window.dispatchEvent(new CustomEvent("saveSuccessInModal")); + + return true; + } + // 🆕 Universal Form Modal 테이블 섹션 병합 저장 처리 // 범용_폼_모달 내부에 _tableSection_ 데이터가 있는 경우 공통 필드 + 개별 품목 병합 저장 const universalFormModalResult = await this.handleUniversalFormModalTableSectionSave(config, context, formData);