diff --git a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx index a605c45e..cebd8aa6 100644 --- a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx +++ b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx @@ -197,6 +197,10 @@ export function UniversalFormModalComponent({ // 로딩 상태 const [saving, setSaving] = useState(false); + // 채번규칙 원본 값 추적 (수동 모드 감지용) + // key: columnName, value: 자동 생성된 원본 값 + const [numberingOriginalValues, setNumberingOriginalValues] = useState>({}); + // 🆕 수정 모드: 원본 그룹 데이터 (INSERT/UPDATE/DELETE 추적용) const [originalGroupedData, setOriginalGroupedData] = useState([]); const groupedDataInitializedRef = useRef(false); @@ -221,15 +225,14 @@ export function UniversalFormModalComponent({ // hasInitialized: hasInitialized.current, // lastInitializedId: lastInitializedId.current, // }); - + // initialData에서 ID 값 추출 (id, ID, objid 등) const currentId = initialData?.id || initialData?.ID || initialData?.objid; const currentIdString = currentId !== undefined ? String(currentId) : undefined; - + // 생성 모드에서 부모로부터 전달받은 데이터 해시 (ID가 없을 때만) - const createModeDataHash = !currentIdString && initialData && Object.keys(initialData).length > 0 - ? JSON.stringify(initialData) - : undefined; + const createModeDataHash = + !currentIdString && initialData && Object.keys(initialData).length > 0 ? JSON.stringify(initialData) : undefined; // 이미 초기화되었고, ID가 동일하고, 생성 모드 데이터도 동일하면 스킵 if (hasInitialized.current && lastInitializedId.current === currentIdString) { @@ -241,7 +244,7 @@ export function UniversalFormModalComponent({ return; } } - + // 🆕 컴포넌트 remount 감지: hasInitialized가 true인데 formData가 비어있으면 재초기화 // (React의 Strict Mode나 EmbeddedScreen 리렌더링으로 인한 remount) if (hasInitialized.current && !currentIdString) { @@ -435,7 +438,7 @@ export function UniversalFormModalComponent({ // console.log("[채번] 섹션 검사:", section.title, { type: section.type, repeatable: section.repeatable, fieldsCount: section.fields?.length }); if (section.repeatable || section.type === "table") continue; - for (const field of (section.fields || [])) { + for (const field of section.fields || []) { // generateOnOpen은 기본값 true (undefined일 경우 true로 처리) const shouldGenerateOnOpen = field.numberingRule?.generateOnOpen !== false; // console.log("[채번] 필드 검사:", field.columnName, { @@ -457,12 +460,19 @@ export function UniversalFormModalComponent({ // generateOnOpen: 미리보기만 표시 (DB 시퀀스 증가 안 함) const response = await previewNumberingCode(field.numberingRule.ruleId); if (response.success && response.data?.generatedCode) { - updatedData[field.columnName] = response.data.generatedCode; + const generatedCode = response.data.generatedCode; + updatedData[field.columnName] = generatedCode; // 저장 시 실제 할당을 위해 ruleId 저장 (TextInput과 동일한 키 형식) const ruleIdKey = `${field.columnName}_numberingRuleId`; updatedData[ruleIdKey] = field.numberingRule.ruleId; + // 원본 채번 값 저장 (수동 모드 감지용) + setNumberingOriginalValues((prev) => ({ + ...prev, + [field.columnName]: generatedCode, + })); + hasChanges = true; numberingGeneratedRef.current = true; // 생성 완료 표시 // console.log( @@ -534,7 +544,7 @@ export function UniversalFormModalComponent({ continue; } else { // 일반 섹션 필드 초기화 - for (const field of (section.fields || [])) { + for (const field of section.fields || []) { // 기본값 설정 let value = field.defaultValue ?? ""; @@ -556,14 +566,16 @@ export function UniversalFormModalComponent({ if (section.optionalFieldGroups) { for (const group of section.optionalFieldGroups) { const key = `${section.id}-${group.id}`; - + // 수정 모드: triggerField 값이 triggerValueOnAdd와 일치하면 그룹 자동 활성화 if (effectiveInitialData && group.triggerField && group.triggerValueOnAdd !== undefined) { const triggerValue = effectiveInitialData[group.triggerField]; if (triggerValue === group.triggerValueOnAdd) { newActivatedGroups.add(key); - console.log(`[initializeForm] 옵셔널 그룹 자동 활성화: ${key}, triggerField=${group.triggerField}, value=${triggerValue}`); - + console.log( + `[initializeForm] 옵셔널 그룹 자동 활성화: ${key}, triggerField=${group.triggerField}, value=${triggerValue}`, + ); + // 활성화된 그룹의 필드값도 초기화 for (const field of group.fields || []) { let value = field.defaultValue ?? ""; @@ -575,7 +587,7 @@ export function UniversalFormModalComponent({ } } } - + // 신규 등록 모드: triggerValueOnRemove를 기본값으로 설정 if (group.triggerField && group.triggerValueOnRemove !== undefined) { // effectiveInitialData에 해당 값이 없는 경우에만 기본값 설정 @@ -595,7 +607,7 @@ export function UniversalFormModalComponent({ sectionsCount: config.sections.length, effectiveInitialDataKeys: Object.keys(effectiveInitialData), }); - + for (const section of config.sections) { if (section.type !== "table" || !section.tableConfig) { continue; @@ -634,67 +646,71 @@ export function UniversalFormModalComponent({ // 마스터 테이블명 확인 (saveConfig에서) // 1. customApiSave.multiTable.mainTable.tableName (다중 테이블 저장) // 2. saveConfig.tableName (단일 테이블 저장) - const masterTable = config.saveConfig?.customApiSave?.multiTable?.mainTable?.tableName - || config.saveConfig?.tableName; - + const masterTable = + config.saveConfig?.customApiSave?.multiTable?.mainTable?.tableName || config.saveConfig?.tableName; + // 디테일 테이블의 컬럼 목록 조회 const columnsResponse = await apiClient.get(`/table-management/tables/${detailTable}/columns`); - + if (columnsResponse.data?.success && columnsResponse.data?.data) { // API 응답 구조: { success, data: { columns: [...], total, page, ... } } const columnsArray = columnsResponse.data.data.columns || columnsResponse.data.data || []; const detailColumnsData = Array.isArray(columnsArray) ? columnsArray : []; const detailColumns = detailColumnsData.map((col: any) => col.column_name || col.columnName); const masterKeys = Object.keys(effectiveInitialData); - + console.log(`[initializeForm] 테이블 섹션 ${section.id}: 연결 필드 자동 감지`, { masterTable, detailTable, detailColumnsCount: detailColumnsData.length, }); - + // 방법 1: 엔티티 관계 기반 감지 (정확) // 디테일 테이블에서 마스터 테이블을 참조하는 엔티티 컬럼 찾기 if (masterTable) { for (const col of detailColumnsData) { const colName = col.column_name || col.columnName; const inputType = col.input_type || col.inputType; - + // 엔티티 타입 컬럼 확인 if (inputType === "entity") { // reference_table 또는 detail_settings에서 참조 테이블 확인 let refTable = col.reference_table || col.referenceTable; - + // detail_settings에서 referenceTable 확인 if (!refTable && col.detail_settings) { try { - const settings = typeof col.detail_settings === "string" - ? JSON.parse(col.detail_settings) - : col.detail_settings; + const settings = + typeof col.detail_settings === "string" + ? JSON.parse(col.detail_settings) + : col.detail_settings; refTable = settings.referenceTable; } catch { // JSON 파싱 실패 무시 } } - + // 마스터 테이블을 참조하는 컬럼 발견 if (refTable === masterTable) { // 참조 컬럼 확인 (마스터 테이블의 어떤 컬럼을 참조하는지) let refColumn = col.reference_column || col.referenceColumn; if (!refColumn && col.detail_settings) { try { - const settings = typeof col.detail_settings === "string" - ? JSON.parse(col.detail_settings) - : col.detail_settings; + const settings = + typeof col.detail_settings === "string" + ? JSON.parse(col.detail_settings) + : col.detail_settings; refColumn = settings.referenceColumn; } catch { // JSON 파싱 실패 무시 } } - + // 마스터 데이터에 해당 컬럼 값이 있는지 확인 if (refColumn && effectiveInitialData[refColumn]) { - console.log(`[initializeForm] 테이블 섹션 ${section.id}: 엔티티 관계 감지 - ${colName} → ${masterTable}.${refColumn}`); + console.log( + `[initializeForm] 테이블 섹션 ${section.id}: 엔티티 관계 감지 - ${colName} → ${masterTable}.${refColumn}`, + ); linkColumn = { masterField: refColumn, detailField: colName }; break; } @@ -702,18 +718,21 @@ export function UniversalFormModalComponent({ } } } - + // 방법 2: 공통 컬럼 패턴 기반 감지 (폴백) // 엔티티 관계가 없으면 공통 컬럼명 패턴으로 찾기 if (!linkColumn) { const priorityPatterns = ["_no", "_number", "_code", "_id"]; - + for (const pattern of priorityPatterns) { for (const masterKey of masterKeys) { - if (masterKey.endsWith(pattern) && - detailColumns.includes(masterKey) && - effectiveInitialData[masterKey] && - masterKey !== "id" && masterKey !== "company_code") { + if ( + masterKey.endsWith(pattern) && + detailColumns.includes(masterKey) && + effectiveInitialData[masterKey] && + masterKey !== "id" && + masterKey !== "company_code" + ) { console.log(`[initializeForm] 테이블 섹션 ${section.id}: 공통 컬럼 패턴 감지 - ${masterKey}`); linkColumn = { masterField: masterKey, detailField: masterKey }; break; @@ -722,14 +741,17 @@ export function UniversalFormModalComponent({ if (linkColumn) break; } } - + // 방법 3: 일반 공통 컬럼 (마지막 폴백) if (!linkColumn) { for (const masterKey of masterKeys) { - if (detailColumns.includes(masterKey) && - effectiveInitialData[masterKey] && - masterKey !== "id" && masterKey !== "company_code" && - !masterKey.startsWith("__")) { + if ( + detailColumns.includes(masterKey) && + effectiveInitialData[masterKey] && + masterKey !== "id" && + masterKey !== "company_code" && + !masterKey.startsWith("__") + ) { console.log(`[initializeForm] 테이블 섹션 ${section.id}: 공통 컬럼 감지 - ${masterKey}`); linkColumn = { masterField: masterKey, detailField: masterKey }; break; @@ -750,7 +772,9 @@ export function UniversalFormModalComponent({ // 마스터 테이블의 연결 필드 값 가져오기 const masterValue = effectiveInitialData[linkColumn.masterField]; if (!masterValue) { - console.log(`[initializeForm] 테이블 섹션 ${section.id}: masterField(${linkColumn.masterField}) 값 없음, 스킵`); + console.log( + `[initializeForm] 테이블 섹션 ${section.id}: masterField(${linkColumn.masterField}) 값 없음, 스킵`, + ); continue; } @@ -767,23 +791,30 @@ export function UniversalFormModalComponent({ [linkColumn.detailField]: { value: masterValue, operator: "equals" }, }; - console.log(`[initializeForm] 테이블 섹션 ${section.id}: API 요청 - URL: /table-management/tables/${detailTable}/data`); - console.log(`[initializeForm] 테이블 섹션 ${section.id}: API 요청 - search:`, JSON.stringify(searchCondition)); + console.log( + `[initializeForm] 테이블 섹션 ${section.id}: API 요청 - URL: /table-management/tables/${detailTable}/data`, + ); + console.log( + `[initializeForm] 테이블 섹션 ${section.id}: API 요청 - search:`, + JSON.stringify(searchCondition), + ); const response = await apiClient.post(`/table-management/tables/${detailTable}/data`, { - search: searchCondition, // filters가 아닌 search로 전달 + search: searchCondition, // filters가 아닌 search로 전달 page: 1, - size: 1000, // pageSize가 아닌 size로 전달 - autoFilter: { enabled: true }, // 멀티테넌시 필터 적용 + size: 1000, // pageSize가 아닌 size로 전달 + autoFilter: { enabled: true }, // 멀티테넌시 필터 적용 }); - console.log(`[initializeForm] 테이블 섹션 ${section.id}: API 응답 - success: ${response.data?.success}, total: ${response.data?.data?.total}, dataLength: ${response.data?.data?.data?.length}`); + console.log( + `[initializeForm] 테이블 섹션 ${section.id}: API 응답 - success: ${response.data?.success}, total: ${response.data?.data?.total}, dataLength: ${response.data?.data?.data?.length}`, + ); if (response.data?.success) { // 다양한 응답 구조 처리 let items: any[] = []; const data = response.data.data; - + if (Array.isArray(data)) { items = data; } else if (data?.items && Array.isArray(data.items)) { @@ -793,7 +824,7 @@ export function UniversalFormModalComponent({ } else if (data?.data && Array.isArray(data.data)) { items = data.data; } - + console.log(`[initializeForm] 테이블 섹션 ${section.id}: ${items.length}건 로드됨`, items); // 테이블 섹션 데이터를 formData에 저장 (TableSectionRenderer에서 사용) @@ -818,35 +849,35 @@ export function UniversalFormModalComponent({ if (multiTable && effectiveInitialData) { const pkColumn = multiTable.mainTable?.primaryKeyColumn; const pkValue = effectiveInitialData[pkColumn]; - + // PK 값이 있으면 수정 모드로 판단 if (pkValue) { console.log("[initializeForm] 수정 모드 - 서브 테이블 데이터 로드 시작"); - + for (const subTableConfig of multiTable.subTables || []) { // loadOnEdit 옵션이 활성화된 경우에만 로드 if (!subTableConfig.enabled || !subTableConfig.options?.loadOnEdit) { continue; } - + const { tableName, linkColumn, repeatSectionId, fieldMappings, options } = subTableConfig; if (!tableName || !linkColumn?.subColumn || !repeatSectionId) { continue; } - + try { // 서브 테이블에서 데이터 조회 const filters: Record = { [linkColumn.subColumn]: pkValue, }; - + // 서브 항목만 로드 (메인 항목 제외) if (options?.loadOnlySubItems && options?.mainMarkerColumn) { filters[options.mainMarkerColumn] = options.subMarkerValue ?? false; } - + console.log(`[initializeForm] 서브 테이블 ${tableName} 조회:`, filters); - + const response = await apiClient.get(`/table-management/tables/${tableName}/data`, { params: { filters: JSON.stringify(filters), @@ -854,11 +885,11 @@ export function UniversalFormModalComponent({ pageSize: 100, }, }); - + if (response.data?.success && response.data?.data?.items) { const subItems = response.data.data.items; console.log(`[initializeForm] 서브 테이블 ${tableName} 데이터 ${subItems.length}건 로드됨`); - + // 역매핑: 서브 테이블 데이터 → 반복 섹션 데이터 const repeatItems: RepeatSectionItem[] = subItems.map((item: any, index: number) => { const repeatItem: RepeatSectionItem = { @@ -866,17 +897,17 @@ export function UniversalFormModalComponent({ _index: index, _originalData: item, // 원본 데이터 보관 (수정 시 필요) }; - + // 필드 매핑 역변환 (targetColumn → formField) for (const mapping of fieldMappings || []) { if (mapping.formField && mapping.targetColumn) { repeatItem[mapping.formField] = item[mapping.targetColumn]; } } - + return repeatItem; }); - + // 반복 섹션에 데이터 설정 newRepeatSections[repeatSectionId] = repeatItems; setRepeatSections({ ...newRepeatSections }); @@ -903,7 +934,7 @@ export function UniversalFormModalComponent({ _index: index, }; - for (const field of (section.fields || [])) { + for (const field of section.fields || []) { item[field.columnName] = field.defaultValue ?? ""; } @@ -913,8 +944,42 @@ export function UniversalFormModalComponent({ // 필드 값 변경 핸들러 const handleFieldChange = useCallback( (columnName: string, value: any) => { + // 채번규칙 필드의 수동 모드 감지 + const originalNumberingValue = numberingOriginalValues[columnName]; + const ruleIdKey = `${columnName}_numberingRuleId`; + + // 해당 필드의 채번규칙 설정 찾기 + let fieldConfig: FormFieldConfig | undefined; + for (const section of config.sections) { + if (section.type === "table" || section.repeatable) continue; + fieldConfig = section.fields?.find((f) => f.columnName === columnName); + if (fieldConfig) break; + // 옵셔널 필드 그룹에서도 찾기 + for (const group of section.optionalFieldGroups || []) { + fieldConfig = group.fields?.find((f) => f.columnName === columnName); + if (fieldConfig) break; + } + if (fieldConfig) break; + } + setFormData((prev) => { const newData = { ...prev, [columnName]: value }; + + // 채번규칙이 활성화된 필드이고, "사용자 수정 가능"이 ON인 경우 + if (fieldConfig?.numberingRule?.enabled && fieldConfig?.numberingRule?.editable && originalNumberingValue) { + // 사용자가 값을 수정했으면 (원본과 다르면) ruleId 제거 → 수동 모드 + if (value !== originalNumberingValue) { + delete newData[ruleIdKey]; + console.log(`[채번 수동 모드] ${columnName}: 사용자가 값 수정 → ruleId 제거`); + } else { + // 원본 값으로 복구하면 ruleId 복구 → 자동 모드 + if (fieldConfig.numberingRule.ruleId) { + newData[ruleIdKey] = fieldConfig.numberingRule.ruleId; + console.log(`[채번 자동 모드] ${columnName}: 원본 값 복구 → ruleId 복구`); + } + } + } + // onChange는 렌더링 외부에서 호출해야 함 (setTimeout 사용) if (onChange) { setTimeout(() => onChange(newData), 0); @@ -922,7 +987,7 @@ export function UniversalFormModalComponent({ return newData; }); }, - [onChange], + [onChange, numberingOriginalValues, config.sections], ); // 반복 섹션 필드 값 변경 핸들러 @@ -995,47 +1060,53 @@ export function UniversalFormModalComponent({ }, []); // 옵셔널 필드 그룹 활성화 - const activateOptionalFieldGroup = useCallback((sectionId: string, groupId: string) => { - const section = config.sections.find((s) => s.id === sectionId); - const group = section?.optionalFieldGroups?.find((g) => g.id === groupId); - if (!group) return; + const activateOptionalFieldGroup = useCallback( + (sectionId: string, groupId: string) => { + const section = config.sections.find((s) => s.id === sectionId); + const group = section?.optionalFieldGroups?.find((g) => g.id === groupId); + if (!group) return; - const key = `${sectionId}-${groupId}`; - setActivatedOptionalFieldGroups((prev) => { - const newSet = new Set(prev); - newSet.add(key); - return newSet; - }); + const key = `${sectionId}-${groupId}`; + setActivatedOptionalFieldGroups((prev) => { + const newSet = new Set(prev); + newSet.add(key); + return newSet; + }); - // 연동 필드 값 변경 (추가 시) - if (group.triggerField && group.triggerValueOnAdd !== undefined) { - handleFieldChange(group.triggerField, group.triggerValueOnAdd); - } - }, [config, handleFieldChange]); + // 연동 필드 값 변경 (추가 시) + if (group.triggerField && group.triggerValueOnAdd !== undefined) { + handleFieldChange(group.triggerField, group.triggerValueOnAdd); + } + }, + [config, handleFieldChange], + ); // 옵셔널 필드 그룹 비활성화 - const deactivateOptionalFieldGroup = useCallback((sectionId: string, groupId: string) => { - const section = config.sections.find((s) => s.id === sectionId); - const group = section?.optionalFieldGroups?.find((g) => g.id === groupId); - if (!group) return; + const deactivateOptionalFieldGroup = useCallback( + (sectionId: string, groupId: string) => { + const section = config.sections.find((s) => s.id === sectionId); + const group = section?.optionalFieldGroups?.find((g) => g.id === groupId); + if (!group) return; - const key = `${sectionId}-${groupId}`; - setActivatedOptionalFieldGroups((prev) => { - const newSet = new Set(prev); - newSet.delete(key); - return newSet; - }); + const key = `${sectionId}-${groupId}`; + setActivatedOptionalFieldGroups((prev) => { + const newSet = new Set(prev); + newSet.delete(key); + return newSet; + }); - // 연동 필드 값 변경 (제거 시) - if (group.triggerField && group.triggerValueOnRemove !== undefined) { - handleFieldChange(group.triggerField, group.triggerValueOnRemove); - } + // 연동 필드 값 변경 (제거 시) + if (group.triggerField && group.triggerValueOnRemove !== undefined) { + handleFieldChange(group.triggerField, group.triggerValueOnRemove); + } - // 옵셔널 필드 그룹 필드 값 초기화 - (group.fields || []).forEach((field) => { - handleFieldChange(field.columnName, field.defaultValue || ""); - }); - }, [config, handleFieldChange]); + // 옵셔널 필드 그룹 필드 값 초기화 + (group.fields || []).forEach((field) => { + handleFieldChange(field.columnName, field.defaultValue || ""); + }); + }, + [config, handleFieldChange], + ); // Select 옵션 로드 const loadSelectOptions = useCallback( @@ -1081,13 +1152,11 @@ export function UniversalFormModalComponent({ // categoryKey 형식: "tableName.columnName" const [categoryTable, categoryColumn] = optionConfig.categoryKey.split("."); if (categoryTable && categoryColumn) { - const response = await apiClient.get( - `/table-categories/${categoryTable}/${categoryColumn}/values` - ); + const response = await apiClient.get(`/table-categories/${categoryTable}/${categoryColumn}/values`); if (response.data?.success && response.data?.data) { - // 라벨값을 DB에 저장 (화면에 표시되는 값 그대로 저장) + // 코드값을 DB에 저장하고 라벨값을 화면에 표시 options = response.data.data.map((item: any) => ({ - value: item.valueLabel || item.value_label, + value: item.valueCode || item.value_code, label: item.valueLabel || item.value_label, })); } @@ -1162,7 +1231,7 @@ export function UniversalFormModalComponent({ for (const section of config.sections) { if (section.repeatable || section.type === "table") continue; // 반복 섹션 및 테이블 섹션은 별도 검증 - for (const field of (section.fields || [])) { + for (const field of section.fields || []) { if (field.required && !field.hidden && !field.numberingRule?.hidden) { const value = formData[field.columnName]; if (value === undefined || value === null || value === "") { @@ -1178,7 +1247,7 @@ export function UniversalFormModalComponent({ // 단일 행 저장 const saveSingleRow = useCallback(async () => { const dataToSave = { ...formData }; - + // 테이블 섹션 데이터 추출 (별도 저장용) const tableSectionData: Record = {}; @@ -1194,19 +1263,45 @@ export function UniversalFormModalComponent({ } }); - // 저장 시점 채번규칙 처리 (generateOnSave만 처리) + // 저장 시점 채번규칙 처리 for (const section of config.sections) { // 테이블 타입 섹션은 건너뛰기 if (section.type === "table") continue; - - for (const field of (section.fields || [])) { - if (field.numberingRule?.enabled && field.numberingRule?.generateOnSave && field.numberingRule?.ruleId) { - const response = await allocateNumberingCode(field.numberingRule.ruleId); - if (response.success && response.data?.generatedCode) { - dataToSave[field.columnName] = response.data.generatedCode; - console.log(`[채번 할당] ${field.columnName} = ${response.data.generatedCode}`); + + for (const field of section.fields || []) { + if (field.numberingRule?.enabled && field.numberingRule?.ruleId) { + const ruleIdKey = `${field.columnName}_numberingRuleId`; + const hasRuleId = dataToSave[ruleIdKey]; // 사용자가 수정하지 않았으면 ruleId 유지됨 + + // 채번 규칙 할당 조건 + const shouldAllocate = + // 1. generateOnSave가 ON인 경우: 항상 저장 시점에 할당 + field.numberingRule.generateOnSave || + // 2. editable이 OFF인 경우: 사용자 입력 무시하고 채번 규칙으로 덮어씌움 + !field.numberingRule.editable || + // 3. editable이 ON이고 사용자가 수정하지 않은 경우 (ruleId 유지됨): 실제 번호 할당 + (field.numberingRule.editable && hasRuleId); + + if (shouldAllocate) { + const response = await allocateNumberingCode(field.numberingRule.ruleId); + if (response.success && response.data?.generatedCode) { + dataToSave[field.columnName] = response.data.generatedCode; + let reason = "(알 수 없음)"; + if (field.numberingRule.generateOnSave) { + reason = "(generateOnSave)"; + } else if (!field.numberingRule.editable) { + reason = "(editable=OFF, 강제 덮어씌움)"; + } else if (hasRuleId) { + reason = "(editable=ON, 사용자 미수정)"; + } + console.log(`[채번 할당] ${field.columnName} = ${response.data.generatedCode} ${reason}`); + } else { + console.error(`[채번 실패] ${field.columnName}:`, response.error); + } } else { - console.error(`[채번 실패] ${field.columnName}:`, response.error); + console.log( + `[채번 스킵] ${field.columnName}: 사용자가 직접 입력한 값 유지 = ${dataToSave[field.columnName]}`, + ); } } } @@ -1214,22 +1309,30 @@ export function UniversalFormModalComponent({ // 별도 테이블에 저장해야 하는 테이블 섹션 목록 const tableSectionsForSeparateTable = config.sections.filter( - (s) => s.type === "table" && - s.tableConfig?.saveConfig?.targetTable && - s.tableConfig.saveConfig.targetTable !== config.saveConfig.tableName + (s) => + s.type === "table" && + s.tableConfig?.saveConfig?.targetTable && + s.tableConfig.saveConfig.targetTable !== config.saveConfig.tableName, ); - + // 테이블 섹션이 있고 메인 테이블에 품목별로 저장하는 경우 (공통 + 개별 병합 저장) // targetTable이 없거나 메인 테이블과 같은 경우 const tableSectionsForMainTable = config.sections.filter( - (s) => s.type === "table" && - (!s.tableConfig?.saveConfig?.targetTable || - s.tableConfig.saveConfig.targetTable === config.saveConfig.tableName) + (s) => + s.type === "table" && + (!s.tableConfig?.saveConfig?.targetTable || + s.tableConfig.saveConfig.targetTable === config.saveConfig.tableName), ); console.log("[saveSingleRow] 메인 테이블:", config.saveConfig.tableName); - console.log("[saveSingleRow] 메인 테이블에 저장할 테이블 섹션:", tableSectionsForMainTable.map(s => s.id)); - console.log("[saveSingleRow] 별도 테이블에 저장할 테이블 섹션:", tableSectionsForSeparateTable.map(s => s.id)); + console.log( + "[saveSingleRow] 메인 테이블에 저장할 테이블 섹션:", + tableSectionsForMainTable.map((s) => s.id), + ); + console.log( + "[saveSingleRow] 별도 테이블에 저장할 테이블 섹션:", + tableSectionsForSeparateTable.map((s) => s.id), + ); console.log("[saveSingleRow] 테이블 섹션 데이터 키:", Object.keys(tableSectionData)); console.log("[saveSingleRow] dataToSave 키:", Object.keys(dataToSave)); @@ -1237,58 +1340,58 @@ export function UniversalFormModalComponent({ // 공통 저장 필드 수집 (sectionSaveModes 설정에 따라) const commonFieldsData: Record = {}; const { sectionSaveModes } = config.saveConfig; - + // 필드 타입 섹션에서 공통 저장 필드 수집 for (const section of config.sections) { if (section.type === "table") continue; - + const sectionMode = sectionSaveModes?.find((s) => s.sectionId === section.id); const defaultMode = "common"; // 필드 타입 섹션의 기본값은 공통 저장 const sectionSaveMode = sectionMode?.saveMode || defaultMode; - + if (section.fields) { for (const field of section.fields) { const fieldOverride = sectionMode?.fieldOverrides?.find((f) => f.fieldName === field.columnName); const fieldSaveMode = fieldOverride?.saveMode || sectionSaveMode; - + if (fieldSaveMode === "common" && dataToSave[field.columnName] !== undefined) { commonFieldsData[field.columnName] = dataToSave[field.columnName]; } } } } - + // 각 테이블 섹션의 품목 데이터에 공통 필드 병합하여 저장 for (const tableSection of tableSectionsForMainTable) { const sectionData = tableSectionData[tableSection.id] || []; - + if (sectionData.length > 0) { // 품목별로 행 저장 for (const item of sectionData) { const rowToSave = { ...commonFieldsData, ...item }; - + // _sourceData 등 내부 메타데이터 제거 Object.keys(rowToSave).forEach((key) => { if (key.startsWith("_")) { delete rowToSave[key]; } }); - + const response = await apiClient.post( `/table-management/tables/${config.saveConfig.tableName}/add`, - rowToSave + rowToSave, ); - + if (!response.data?.success) { throw new Error(response.data?.message || "품목 저장 실패"); } } - + // 이미 저장했으므로 아래 로직에서 다시 저장하지 않도록 제거 delete tableSectionData[tableSection.id]; } } - + // 품목이 없으면 공통 데이터만 저장하지 않음 (품목이 필요한 화면이므로) // 다른 테이블 섹션이 있는 경우에만 메인 데이터 저장 const hasOtherTableSections = Object.keys(tableSectionData).length > 0; @@ -1303,7 +1406,7 @@ export function UniversalFormModalComponent({ if (!response.data?.success) { throw new Error(response.data?.message || "저장 실패"); } - + // 테이블 섹션 데이터 저장 (별도 테이블에) for (const section of config.sections) { if (section.type === "table" && section.tableConfig?.saveConfig?.targetTable) { @@ -1311,35 +1414,35 @@ export function UniversalFormModalComponent({ if (sectionData && sectionData.length > 0) { // 메인 레코드 ID가 필요한 경우 (response.data에서 가져오기) const mainRecordId = response.data?.data?.id; - + // 공통 저장 필드 수집: 다른 섹션(필드 타입)에서 공통 저장으로 설정된 필드 값 // 기본값: 필드 타입 섹션은 'common', 테이블 타입 섹션은 'individual' const commonFieldsData: Record = {}; const { sectionSaveModes } = config.saveConfig; - + // 다른 섹션에서 공통 저장으로 설정된 필드 값 수집 for (const otherSection of config.sections) { if (otherSection.id === section.id) continue; // 현재 테이블 섹션은 건너뛰기 - + const sectionMode = sectionSaveModes?.find((s) => s.sectionId === otherSection.id); // 기본값: 필드 타입 섹션은 'common', 테이블 타입 섹션은 'individual' const defaultMode = otherSection.type === "table" ? "individual" : "common"; const sectionSaveMode = sectionMode?.saveMode || defaultMode; - + // 필드 타입 섹션의 필드들 처리 if (otherSection.type !== "table" && otherSection.fields) { for (const field of otherSection.fields) { // 필드별 오버라이드 확인 const fieldOverride = sectionMode?.fieldOverrides?.find((f) => f.fieldName === field.columnName); const fieldSaveMode = fieldOverride?.saveMode || sectionSaveMode; - + // 공통 저장이면 formData에서 값을 가져와 모든 품목에 적용 if (fieldSaveMode === "common" && formData[field.columnName] !== undefined) { commonFieldsData[field.columnName] = formData[field.columnName]; } } } - + // 🆕 선택적 필드 그룹 (optionalFieldGroups)도 처리 if (otherSection.optionalFieldGroups && otherSection.optionalFieldGroups.length > 0) { for (const optGroup of otherSection.optionalFieldGroups) { @@ -1354,13 +1457,13 @@ export function UniversalFormModalComponent({ } } } - + console.log("[saveSingleRow] 별도 테이블 저장 - 공통 필드:", Object.keys(commonFieldsData)); - + for (const item of sectionData) { // 공통 필드 병합 + 개별 품목 데이터 const itemToSave = { ...commonFieldsData, ...item }; - + // saveToTarget: false인 컬럼은 저장에서 제외 const columns = section.tableConfig?.columns || []; for (const col of columns) { @@ -1368,24 +1471,24 @@ export function UniversalFormModalComponent({ delete itemToSave[col.field]; } } - + // _sourceData 등 내부 메타데이터 제거 Object.keys(itemToSave).forEach((key) => { if (key.startsWith("_")) { delete itemToSave[key]; } }); - + // 메인 레코드와 연결이 필요한 경우 if (mainRecordId && config.saveConfig.primaryKeyColumn) { itemToSave[config.saveConfig.primaryKeyColumn] = mainRecordId; } - + const saveResponse = await apiClient.post( `/table-management/tables/${section.tableConfig.saveConfig.targetTable}/add`, - itemToSave + itemToSave, ); - + if (!saveResponse.data?.success) { throw new Error(saveResponse.data?.message || `${section.title || "테이블 섹션"} 저장 실패`); } @@ -1393,7 +1496,13 @@ export function UniversalFormModalComponent({ } } } - }, [config.sections, config.saveConfig.tableName, config.saveConfig.primaryKeyColumn, config.saveConfig.sectionSaveModes, formData]); + }, [ + config.sections, + config.saveConfig.tableName, + config.saveConfig.primaryKeyColumn, + config.saveConfig.sectionSaveModes, + formData, + ]); // 다중 행 저장 (겸직 등) const saveMultipleRows = useCallback(async () => { @@ -1469,7 +1578,7 @@ export function UniversalFormModalComponent({ for (const section of config.sections) { if (section.repeatable || section.type === "table") continue; - for (const field of (section.fields || [])) { + for (const field of section.fields || []) { if (field.numberingRule?.enabled && field.numberingRule?.ruleId) { // generateOnSave 또는 generateOnOpen 모두 저장 시 실제 순번 할당 const shouldAllocate = field.numberingRule.generateOnSave || field.numberingRule.generateOnOpen; @@ -1525,7 +1634,7 @@ export function UniversalFormModalComponent({ } }); }); - + // 1-0. receiveFromParent 필드 값도 mainData에 추가 (서브 테이블 저장용) // 이 필드들은 메인 테이블에는 저장되지 않지만, 서브 테이블 저장 시 필요할 수 있음 config.sections.forEach((section) => { @@ -1544,7 +1653,7 @@ export function UniversalFormModalComponent({ for (const section of config.sections) { if (section.repeatable || section.type === "table") continue; - for (const field of (section.fields || [])) { + for (const field of section.fields || []) { // 채번규칙이 활성화된 필드 처리 if (field.numberingRule?.enabled && field.numberingRule?.ruleId) { // 신규 생성이거나 값이 없는 경우에만 채번 @@ -1589,7 +1698,7 @@ export function UniversalFormModalComponent({ } const subItems: Record[] = []; - + // 반복 섹션이 있는 경우에만 반복 데이터 처리 if (subTableConfig.repeatSectionId) { const repeatData = repeatSections[subTableConfig.repeatSectionId] || []; @@ -1902,10 +2011,8 @@ export function UniversalFormModalComponent({ // 메인 표시 컬럼 (displayColumn) const mainDisplayVal = row[lfg.displayColumn || ""] || ""; // 서브 표시 컬럼 (subDisplayColumn이 있으면 사용, 없으면 valueColumn 사용) - const subDisplayVal = lfg.subDisplayColumn - ? (row[lfg.subDisplayColumn] || "") - : (row[valueColumn] || ""); - + const subDisplayVal = lfg.subDisplayColumn ? row[lfg.subDisplayColumn] || "" : row[valueColumn] || ""; + switch (lfg.displayFormat) { case "code_name": // 서브 - 메인 형식 @@ -1923,7 +2030,10 @@ export function UniversalFormModalComponent({ matches.forEach((match) => { const columnName = match.slice(1, -1); // { } 제거 const columnValue = row[columnName]; - result = result.replace(match, columnValue !== undefined && columnValue !== null ? String(columnValue) : ""); + result = result.replace( + match, + columnValue !== undefined && columnValue !== null ? String(columnValue) : "", + ); }); } return result; @@ -1980,7 +2090,12 @@ export function UniversalFormModalComponent({ {sourceData.length > 0 ? ( sourceData - .filter((row) => row[valueColumn] !== null && row[valueColumn] !== undefined && String(row[valueColumn]) !== "") + .filter( + (row) => + row[valueColumn] !== null && + row[valueColumn] !== undefined && + String(row[valueColumn]) !== "", + ) .map((row, index) => ( {getDisplayText(row)} @@ -2240,13 +2355,11 @@ export function UniversalFormModalComponent({ ), )} - + {/* 옵셔널 필드 그룹 렌더링 */} {section.optionalFieldGroups && section.optionalFieldGroups.length > 0 && (
- {section.optionalFieldGroups.map((group) => - renderOptionalFieldGroup(section, group, sectionColumns) - )} + {section.optionalFieldGroups.map((group) => renderOptionalFieldGroup(section, group, sectionColumns))}
)} @@ -2274,7 +2387,7 @@ export function UniversalFormModalComponent({ const renderOptionalFieldGroup = ( section: FormSectionConfig, group: OptionalFieldGroupConfig, - sectionColumns: number + sectionColumns: number, ) => { const key = `${section.id}-${group.id}`; const isActivated = activatedOptionalFieldGroups.has(key); @@ -2293,9 +2406,7 @@ export function UniversalFormModalComponent({

{group.title}

- {group.description && ( -

{group.description}

- )} + {group.description &&

{group.description}

}
@@ -2373,8 +2478,8 @@ export function UniversalFormModalComponent({ formData[field.columnName], (value) => handleFieldChange(field.columnName, value), `${section.id}-${group.id}-${field.id}`, - groupColumns - ) + groupColumns, + ), )}
@@ -2388,9 +2493,7 @@ export function UniversalFormModalComponent({

{group.title}

- {group.description && ( -

{group.description}

- )} + {group.description &&

{group.description}

}
@@ -2546,7 +2649,8 @@ export function UniversalFormModalComponent({

{config.modal.title || "범용 폼 모달"}

- {config.sections.length}개 섹션 |{config.sections.reduce((acc, s) => acc + (s.fields?.length || 0), 0)}개 필드 + {config.sections.length}개 섹션 |{config.sections.reduce((acc, s) => acc + (s.fields?.length || 0), 0)}개 + 필드

저장 테이블: {config.saveConfig.tableName || "(미설정)"}