diff --git a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx index 2e1cf659..f22eb497 100644 --- a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx @@ -1636,7 +1636,7 @@ export function ModalRepeaterTableConfigPanel({ - {(localConfig.columns || []).map((col) => ( + {(localConfig.columns || []).filter((col) => col.field && col.field !== "").map((col) => ( {col.label} ({col.field}) @@ -1900,7 +1900,7 @@ export function ModalRepeaterTableConfigPanel({ - {(localConfig.columns || []).map((col) => ( + {(localConfig.columns || []).filter((col) => col.field && col.field !== "").map((col) => ( {col.label} @@ -2056,7 +2056,7 @@ export function ModalRepeaterTableConfigPanel({ - {(localConfig.columns || []).map((col) => ( + {(localConfig.columns || []).filter((col) => col.field && col.field !== "").map((col) => ( {col.label} ({col.field}) @@ -2303,7 +2303,7 @@ export function ModalRepeaterTableConfigPanel({ - {(localConfig.columns || []).map((col) => ( + {(localConfig.columns || []).filter((col) => col.field && col.field !== "").map((col) => ( {col.label} diff --git a/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx b/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx index 88da4aef..70b15e7d 100644 --- a/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx @@ -481,7 +481,7 @@ export function RepeaterTable({ - {column.selectOptions?.map((option) => ( + {column.selectOptions?.filter((option) => option.value && option.value !== "").map((option) => ( {option.label} diff --git a/frontend/lib/registry/components/simple-repeater-table/SimpleRepeaterTableComponent.tsx b/frontend/lib/registry/components/simple-repeater-table/SimpleRepeaterTableComponent.tsx index 6303cdee..add34d5f 100644 --- a/frontend/lib/registry/components/simple-repeater-table/SimpleRepeaterTableComponent.tsx +++ b/frontend/lib/registry/components/simple-repeater-table/SimpleRepeaterTableComponent.tsx @@ -561,7 +561,7 @@ export function SimpleRepeaterTableComponent({ - {column.selectOptions?.map((option) => ( + {column.selectOptions?.filter((option) => option.value && option.value !== "").map((option) => ( {option.label} diff --git a/frontend/lib/registry/components/simple-repeater-table/SimpleRepeaterTableConfigPanel.tsx b/frontend/lib/registry/components/simple-repeater-table/SimpleRepeaterTableConfigPanel.tsx index 41e70e08..4ef47e39 100644 --- a/frontend/lib/registry/components/simple-repeater-table/SimpleRepeaterTableConfigPanel.tsx +++ b/frontend/lib/registry/components/simple-repeater-table/SimpleRepeaterTableConfigPanel.tsx @@ -1539,7 +1539,7 @@ export function SimpleRepeaterTableConfigPanel({ - {(localConfig.columns || []).filter(c => c.type === "number").map((col) => ( + {(localConfig.columns || []).filter(c => c.type === "number" && c.field && c.field !== "").map((col) => ( {col.label} diff --git a/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx b/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx index 64418541..ba03d2b9 100644 --- a/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx +++ b/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx @@ -498,11 +498,43 @@ export function TableSectionRenderer({ if (!hasDynamicSelectColumns) return; if (!conditionalConfig?.sourceFilter?.enabled) return; if (!activeConditionTab) return; + if (!tableConfig.source?.tableName) return; - // 조건 변경 시 캐시 리셋하고 다시 로드 + // 조건 변경 시 캐시 리셋하고 즉시 다시 로드 sourceDataLoadedRef.current = false; setSourceDataCache([]); - }, [activeConditionTab, hasDynamicSelectColumns, conditionalConfig?.sourceFilter?.enabled]); + + // 즉시 데이터 다시 로드 (기존 useEffect에 의존하지 않고 직접 호출) + const loadSourceData = async () => { + try { + const filterCondition: Record = {}; + filterCondition[conditionalConfig.sourceFilter!.filterColumn] = activeConditionTab; + + const response = await apiClient.post( + `/table-management/tables/${tableConfig.source!.tableName}/data`, + { + search: filterCondition, + size: 1000, + page: 1, + } + ); + + if (response.data.success && response.data.data?.data) { + setSourceDataCache(response.data.data.data); + sourceDataLoadedRef.current = true; + console.log("[TableSectionRenderer] 조건 탭 변경 - 소스 데이터 로드 완료:", { + tableName: tableConfig.source!.tableName, + rowCount: response.data.data.data.length, + filter: filterCondition, + }); + } + } catch (error) { + console.error("[TableSectionRenderer] 소스 데이터 로드 실패:", error); + } + }; + + loadSourceData(); + }, [activeConditionTab, hasDynamicSelectColumns, conditionalConfig?.sourceFilter?.enabled, conditionalConfig?.sourceFilter?.filterColumn, tableConfig.source?.tableName]); // 컬럼별 동적 Select 옵션 생성 const dynamicSelectOptionsMap = useMemo(() => { @@ -540,6 +572,45 @@ export function TableSectionRenderer({ return optionsMap; }, [sourceDataCache, tableConfig.columns]); + // 데이터 변경 핸들러 (날짜 일괄 적용 로직 포함) - 다른 함수에서 참조하므로 먼저 정의 + const handleDataChange = useCallback( + (newData: any[]) => { + let processedData = newData; + + // 날짜 일괄 적용 로직: batchApply가 활성화된 날짜 컬럼 처리 + const batchApplyColumns = tableConfig.columns.filter( + (col) => col.type === "date" && col.batchApply === true + ); + + for (const dateCol of batchApplyColumns) { + // 이미 일괄 적용된 컬럼은 건너뜀 + if (batchAppliedFields.has(dateCol.field)) continue; + + // 해당 컬럼에 값이 있는 행과 없는 행 분류 + const itemsWithDate = processedData.filter((item) => item[dateCol.field]); + const itemsWithoutDate = processedData.filter((item) => !item[dateCol.field]); + + // 조건: 정확히 1개만 날짜가 있고, 나머지는 모두 비어있을 때 + if (itemsWithDate.length === 1 && itemsWithoutDate.length > 0) { + const selectedDate = itemsWithDate[0][dateCol.field]; + + // 모든 행에 동일한 날짜 적용 + processedData = processedData.map((item) => ({ + ...item, + [dateCol.field]: selectedDate, + })); + + // 플래그 활성화 (이후 개별 수정 가능) + setBatchAppliedFields((prev) => new Set([...prev, dateCol.field])); + } + } + + setTableData(processedData); + onTableDataChange(processedData); + }, + [onTableDataChange, tableConfig.columns, batchAppliedFields] + ); + // 행 선택 모드: 드롭다운 값 변경 시 같은 소스 행의 다른 컬럼들 자동 채움 const handleDynamicSelectChange = useCallback( (rowIndex: number, columnField: string, selectedValue: string, conditionValue?: string) => { @@ -617,6 +688,91 @@ export function TableSectionRenderer({ [tableConfig.columns, sourceDataCache, tableData, conditionalTableData, isConditionalMode, handleDataChange, onConditionalTableDataChange] ); + // 참조 컬럼 값 조회 함수 (saveToTarget: false인 컬럼에 대해 소스 테이블 조회) + const loadReferenceColumnValues = useCallback(async (data: any[]) => { + // saveToTarget: false이고 referenceDisplay가 설정된 컬럼 찾기 + const referenceColumns = (tableConfig.columns || []).filter( + (col) => col.saveConfig?.saveToTarget === false && col.saveConfig?.referenceDisplay + ); + + if (referenceColumns.length === 0) return; + + const sourceTableName = tableConfig.source?.tableName; + if (!sourceTableName) { + console.warn("[TableSectionRenderer] 참조 조회를 위한 소스 테이블이 설정되지 않았습니다."); + return; + } + + // 참조 ID들 수집 (중복 제거) + const referenceIdSet = new Set(); + + for (const col of referenceColumns) { + const refDisplay = col.saveConfig!.referenceDisplay!; + + for (const row of data) { + const refId = row[refDisplay.referenceIdField]; + if (refId !== undefined && refId !== null && refId !== "") { + referenceIdSet.add(String(refId)); + } + } + } + + if (referenceIdSet.size === 0) return; + + try { + // 소스 테이블에서 참조 ID에 해당하는 데이터 조회 + const response = await apiClient.post( + `/table-management/tables/${sourceTableName}/data`, + { + search: { id: Array.from(referenceIdSet) }, // ID 배열로 조회 + size: 1000, + page: 1, + } + ); + + if (!response.data?.success || !response.data?.data?.data) { + console.warn("[TableSectionRenderer] 참조 데이터 조회 실패"); + return; + } + + const sourceData: any[] = response.data.data.data; + + // ID를 키로 하는 맵 생성 + const sourceDataMap: Record = {}; + for (const sourceRow of sourceData) { + sourceDataMap[String(sourceRow.id)] = sourceRow; + } + + // 각 행에 참조 컬럼 값 채우기 + const updatedData = data.map((row) => { + const newRow = { ...row }; + + for (const col of referenceColumns) { + const refDisplay = col.saveConfig!.referenceDisplay!; + const refId = row[refDisplay.referenceIdField]; + + if (refId !== undefined && refId !== null && refId !== "") { + const sourceRow = sourceDataMap[String(refId)]; + if (sourceRow) { + newRow[col.field] = sourceRow[refDisplay.sourceColumn]; + } + } + } + + return newRow; + }); + + console.log("[TableSectionRenderer] 참조 컬럼 값 조회 완료:", { + referenceColumns: referenceColumns.map((c) => c.field), + updatedRowCount: updatedData.length, + }); + + setTableData(updatedData); + } catch (error) { + console.error("[TableSectionRenderer] 참조 데이터 조회 실패:", error); + } + }, [tableConfig.columns, tableConfig.source?.tableName]); + // formData에서 초기 테이블 데이터 로드 (수정 모드에서 _groupedData 표시) useEffect(() => { // 이미 초기화되었으면 스킵 @@ -632,8 +788,11 @@ export function TableSectionRenderer({ }); setTableData(initialData); initialDataLoadedRef.current = true; + + // 참조 컬럼 값 조회 (saveToTarget: false인 컬럼) + loadReferenceColumnValues(initialData); } - }, [sectionId, formData]); + }, [sectionId, formData, loadReferenceColumnValues]); // RepeaterColumnConfig로 변환 (동적 Select 옵션 반영) const columns: RepeaterColumnConfig[] = useMemo(() => { @@ -691,45 +850,6 @@ export function TableSectionRenderer({ [calculateRow] ); - // 데이터 변경 핸들러 (날짜 일괄 적용 로직 포함) - const handleDataChange = useCallback( - (newData: any[]) => { - let processedData = newData; - - // 날짜 일괄 적용 로직: batchApply가 활성화된 날짜 컬럼 처리 - const batchApplyColumns = tableConfig.columns.filter( - (col) => col.type === "date" && col.batchApply === true - ); - - for (const dateCol of batchApplyColumns) { - // 이미 일괄 적용된 컬럼은 건너뜀 - if (batchAppliedFields.has(dateCol.field)) continue; - - // 해당 컬럼에 값이 있는 행과 없는 행 분류 - const itemsWithDate = processedData.filter((item) => item[dateCol.field]); - const itemsWithoutDate = processedData.filter((item) => !item[dateCol.field]); - - // 조건: 정확히 1개만 날짜가 있고, 나머지는 모두 비어있을 때 - if (itemsWithDate.length === 1 && itemsWithoutDate.length > 0) { - const selectedDate = itemsWithDate[0][dateCol.field]; - - // 모든 행에 동일한 날짜 적용 - processedData = processedData.map((item) => ({ - ...item, - [dateCol.field]: selectedDate, - })); - - // 플래그 활성화 (이후 개별 수정 가능) - setBatchAppliedFields((prev) => new Set([...prev, dateCol.field])); - } - } - - setTableData(processedData); - onTableDataChange(processedData); - }, - [onTableDataChange, tableConfig.columns, batchAppliedFields] - ); - // 행 변경 핸들러 (동적 Select 행 선택 모드 지원) const handleRowChange = useCallback( (index: number, newRow: any, conditionValue?: string) => { @@ -1377,9 +1497,10 @@ export function TableSectionRenderer({ const { triggerType } = conditionalConfig; // 정적 옵션과 동적 옵션 병합 (동적 옵션이 있으면 우선 사용) - const effectiveOptions = conditionalConfig.optionSource?.enabled && dynamicOptions.length > 0 + // 빈 value를 가진 옵션은 제외 (Select.Item은 빈 문자열 value를 허용하지 않음) + const effectiveOptions = (conditionalConfig.optionSource?.enabled && dynamicOptions.length > 0 ? dynamicOptions - : conditionalConfig.options || []; + : conditionalConfig.options || []).filter(opt => opt.value && opt.value.trim() !== ""); // 로딩 중이면 로딩 표시 if (dynamicOptionsLoading) { diff --git a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx index 16778725..b4921a51 100644 --- a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx +++ b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx @@ -102,11 +102,13 @@ const CascadingSelectField: React.FC = ({ : config.noOptionsMessage || "선택 가능한 항목이 없습니다"} ) : ( - options.map((option) => ( - - {option.label} - - )) + options + .filter((option) => option.value && option.value !== "") + .map((option) => ( + + {option.label} + + )) )} @@ -1081,6 +1083,14 @@ export function UniversalFormModalComponent({ // 공통 필드 병합 + 개별 품목 데이터 const itemToSave = { ...commonFieldsData, ...item }; + // saveToTarget: false인 컬럼은 저장에서 제외 + const columns = section.tableConfig?.columns || []; + for (const col of columns) { + if (col.saveConfig?.saveToTarget === false && col.field in itemToSave) { + delete itemToSave[col.field]; + } + } + // 메인 레코드와 연결이 필요한 경우 if (mainRecordId && config.saveConfig.primaryKeyColumn) { itemToSave[config.saveConfig.primaryKeyColumn] = mainRecordId; @@ -1680,11 +1690,13 @@ export function UniversalFormModalComponent({ {sourceData.length > 0 ? ( - sourceData.map((row, index) => ( - - {getDisplayText(row)} - - )) + sourceData + .filter((row) => row[valueColumn] !== null && row[valueColumn] !== undefined && String(row[valueColumn]) !== "") + .map((row, index) => ( + + {getDisplayText(row)} + + )) ) : ( {cachedData === undefined ? "데이터를 불러오는 중..." : "데이터가 없습니다"} @@ -2345,11 +2357,13 @@ function SelectField({ fieldId, value, onChange, optionConfig, placeholder, disa - {options.map((option) => ( - - {option.label} - - ))} + {options + .filter((option) => option.value && option.value !== "") + .map((option) => ( + + {option.label} + + ))} ); diff --git a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx index 61ad2016..27af68f1 100644 --- a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx +++ b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx @@ -728,13 +728,13 @@ export function UniversalFormModalConfigPanel({ config, onChange, allComponents {/* 테이블 컬럼 목록 (테이블 타입만) */} {section.type === "table" && section.tableConfig?.columns && section.tableConfig.columns.length > 0 && (
- {section.tableConfig.columns.slice(0, 4).map((col) => ( + {section.tableConfig.columns.slice(0, 4).map((col, idx) => ( - {col.label} + {col.label || col.field || `컬럼 ${idx + 1}`} ))} {section.tableConfig.columns.length > 4 && ( diff --git a/frontend/lib/registry/components/universal-form-modal/modals/TableSectionSettingsModal.tsx b/frontend/lib/registry/components/universal-form-modal/modals/TableSectionSettingsModal.tsx index f8b77aba..ebd16c44 100644 --- a/frontend/lib/registry/components/universal-form-modal/modals/TableSectionSettingsModal.tsx +++ b/frontend/lib/registry/components/universal-form-modal/modals/TableSectionSettingsModal.tsx @@ -450,10 +450,13 @@ function ColumnSettingItem({ variant="outline" role="combobox" aria-expanded={fieldSearchOpen} - className="h-8 w-full justify-between text-xs mt-1" + className={cn( + "h-8 w-full justify-between text-xs mt-1", + !col.field && "text-muted-foreground" + )} > - {col.field || "필드 선택..."} + {col.field || "(저장 안 함)"} @@ -466,6 +469,25 @@ function ColumnSettingItem({ 필드를 찾을 수 없습니다. + {/* 선택 안 함 옵션 */} + { + onUpdate({ field: "" }); + setFieldSearchOpen(false); + }} + className="text-xs text-muted-foreground" + > + + (선택 안 함 - 저장하지 않음) + + {/* 실제 컬럼 목록 */} {saveTableColumns.map((column) => ( )} + + {/* ============================================ */} + {/* 저장 설정 섹션 */} + {/* ============================================ */} +
+ +

+ 이 컬럼의 값을 DB에 저장할지 설정합니다. +

+ + {/* 저장 여부 라디오 버튼 */} +
+ {/* 저장함 옵션 */} + + + {/* 저장 안 함 옵션 */} + +
+ + {/* 참조 설정 패널 (저장 안 함 선택 시) */} + {col.saveConfig?.saveToTarget === false && ( +
+
+ + 참조 설정 +
+ + {/* Step 1: ID 컬럼 선택 */} +
+ + +

+ 이 컬럼에 저장된 ID로 소스 테이블을 조회합니다. +

+
+ + {/* Step 2: 소스 컬럼 선택 */} +
+ + {sourceTableColumns.length > 0 ? ( + + ) : ( + { + onUpdate({ + saveConfig: { + ...col.saveConfig!, + referenceDisplay: { + ...col.saveConfig!.referenceDisplay!, + sourceColumn: e.target.value, + }, + }, + }); + }} + placeholder="소스 컬럼명 입력" + className="h-7 text-xs" + /> + )} +

+ 조회된 행에서 이 컬럼의 값을 화면에 표시합니다. +

+
+ + {/* 설정 요약 */} + {col.saveConfig?.referenceDisplay?.referenceIdField && col.saveConfig?.referenceDisplay?.sourceColumn && ( +
+ 설정 요약: +
+ - 이 컬럼({col.label || col.field})은 저장되지 않습니다. +
+ - 수정 화면에서 {col.saveConfig.referenceDisplay.referenceIdField}로{" "} + {sourceTableName} 테이블을 조회하여{" "} + {col.saveConfig.referenceDisplay.sourceColumn} 값을 표시합니다. +
+ )} +
+ )} +
); } @@ -2826,11 +3027,13 @@ export function TableSectionSettingsModal({ 컬럼 설정에서 먼저 컬럼을 추가하세요
) : ( - (tableConfig.columns || []).map((col) => ( - - {col.label || col.field} - - )) + (tableConfig.columns || []) + .filter((col) => col.field) // 빈 필드명 제외 + .map((col, idx) => ( + + {col.label || col.field} + + )) )}
diff --git a/frontend/lib/registry/components/universal-form-modal/types.ts b/frontend/lib/registry/components/universal-form-modal/types.ts index 31388e96..1f2015eb 100644 --- a/frontend/lib/registry/components/universal-form-modal/types.ts +++ b/frontend/lib/registry/components/universal-form-modal/types.ts @@ -426,6 +426,31 @@ export interface TableColumnConfig { // 부모에서 값 받기 (모든 행에 동일한 값 적용) receiveFromParent?: boolean; // 부모에서 값 받기 활성화 parentFieldName?: string; // 부모 필드명 (미지정 시 field와 동일) + + // 저장 설정 (컬럼별 저장 여부 및 참조 표시) + saveConfig?: TableColumnSaveConfig; +} + +/** + * 테이블 컬럼 저장 설정 + * - 컬럼별로 저장 여부를 설정하고, 저장하지 않는 컬럼은 참조 ID로 조회하여 표시 + */ +export interface TableColumnSaveConfig { + // 저장 여부 (기본값: true) + // true: 사용자가 입력/선택한 값을 DB에 저장 + // false: 저장하지 않고, 참조 ID로 소스 테이블을 조회하여 표시만 함 + saveToTarget: boolean; + + // 참조 표시 설정 (saveToTarget이 false일 때 사용) + referenceDisplay?: { + // 참조할 ID 컬럼 (같은 테이블 내의 다른 컬럼) + // 예: "inspection_standard_id" + referenceIdField: string; + + // 소스 테이블에서 가져올 컬럼 + // 예: "inspection_item" → 소스 테이블의 inspection_item 값을 표시 + sourceColumn: string; + }; } // ============================================