diff --git a/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx b/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx index ba03d2b9..a1c0bd76 100644 --- a/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx +++ b/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx @@ -16,12 +16,7 @@ import { ItemSelectionModal } from "../modal-repeater-table/ItemSelectionModal"; import { RepeaterColumnConfig, CalculationRule } from "../modal-repeater-table/types"; // 타입 정의 -import { - TableSectionConfig, - TableColumnConfig, - TableJoinCondition, - FormDataState, -} from "./types"; +import { TableSectionConfig, TableColumnConfig, TableJoinCondition, FormDataState } from "./types"; interface TableSectionRendererProps { sectionId: string; @@ -56,7 +51,7 @@ function convertToRepeaterColumn(col: TableColumnConfig): RepeaterColumnConfig { selectOptions: col.selectOptions, // valueMapping은 별도로 처리 }; - + // lookup 설정을 dynamicDataSource로 변환 (새로운 조회 기능) if (col.lookup?.enabled && col.lookup.options && col.lookup.options.length > 0) { baseColumn.dynamicDataSource = { @@ -75,17 +70,19 @@ function convertToRepeaterColumn(col: TableColumnConfig): RepeaterColumnConfig { sourceField: cond.sourceField, targetField: cond.targetColumn, // sourceType에 따른 데이터 출처 설정 - sourceType: cond.sourceType, // "currentRow" | "sectionField" | "externalTable" + sourceType: cond.sourceType, // "currentRow" | "sectionField" | "externalTable" fromFormData: cond.sourceType === "sectionField", sectionId: cond.sectionId, // 외부 테이블 조회 설정 (sourceType이 "externalTable"인 경우) externalLookup: cond.externalLookup, // 값 변환 설정 전달 (레거시 호환) - transform: cond.transform?.enabled ? { - tableName: cond.transform.tableName, - matchColumn: cond.transform.matchColumn, - resultColumn: cond.transform.resultColumn, - } : undefined, + transform: cond.transform?.enabled + ? { + tableName: cond.transform.tableName, + matchColumn: cond.transform.matchColumn, + resultColumn: cond.transform.resultColumn, + } + : undefined, })), }, // 조회 유형 정보 추가 @@ -115,14 +112,18 @@ function convertToRepeaterColumn(col: TableColumnConfig): RepeaterColumnConfig { defaultOptionId: col.columnModes.find((m) => m.isDefault)?.id || col.columnModes[0]?.id, }; } - + return baseColumn; } /** * TableCalculationRule을 CalculationRule로 변환 */ -function convertToCalculationRule(calc: { resultField: string; formula: string; dependencies: string[] }): CalculationRule { +function convertToCalculationRule(calc: { + resultField: string; + formula: string; + dependencies: string[]; +}): CalculationRule { return { result: calc.resultField, formula: calc.formula, @@ -136,7 +137,7 @@ function convertToCalculationRule(calc: { resultField: string; formula: string; */ async function transformValue( value: any, - transform: { tableName: string; matchColumn: string; resultColumn: string } + transform: { tableName: string; matchColumn: string; resultColumn: string }, ): Promise { if (!value || !transform.tableName || !transform.matchColumn || !transform.resultColumn) { return value; @@ -144,19 +145,16 @@ async function transformValue( try { // 정확히 일치하는 검색 - const response = await apiClient.post( - `/table-management/tables/${transform.tableName}/data`, - { - search: { - [transform.matchColumn]: { - value: value, - operator: "equals" - } - }, - size: 1, - page: 1 - } - ); + const response = await apiClient.post(`/table-management/tables/${transform.tableName}/data`, { + search: { + [transform.matchColumn]: { + value: value, + operator: "equals", + }, + }, + size: 1, + page: 1, + }); if (response.data.success && response.data.data?.data?.length > 0) { const transformedValue = response.data.data.data[0][transform.resultColumn]; @@ -186,7 +184,7 @@ async function fetchExternalLookupValue( }, rowData: any, sourceData: any, - formData: FormDataState + formData: FormDataState, ): Promise { // 1. 비교 값 가져오기 let matchValue: any; @@ -199,31 +197,32 @@ async function fetchExternalLookupValue( } if (matchValue === undefined || matchValue === null || matchValue === "") { - console.warn(`외부 테이블 조회: 비교 값이 없습니다. (${externalLookup.matchSourceType}.${externalLookup.matchSourceField})`); + console.warn( + `외부 테이블 조회: 비교 값이 없습니다. (${externalLookup.matchSourceType}.${externalLookup.matchSourceField})`, + ); return undefined; } // 2. 외부 테이블에서 값 조회 (정확히 일치하는 검색) try { - const response = await apiClient.post( - `/table-management/tables/${externalLookup.tableName}/data`, - { - search: { - [externalLookup.matchColumn]: { - value: matchValue, - operator: "equals" - } - }, - size: 1, - page: 1 - } - ); + const response = await apiClient.post(`/table-management/tables/${externalLookup.tableName}/data`, { + search: { + [externalLookup.matchColumn]: { + value: matchValue, + operator: "equals", + }, + }, + size: 1, + page: 1, + }); if (response.data.success && response.data.data?.data?.length > 0) { return response.data.data.data[0][externalLookup.resultColumn]; } - console.warn(`외부 테이블 조회: ${externalLookup.tableName}.${externalLookup.matchColumn} = "${matchValue}" 인 행을 찾을 수 없습니다.`); + console.warn( + `외부 테이블 조회: ${externalLookup.tableName}.${externalLookup.matchColumn} = "${matchValue}" 인 행을 찾을 수 없습니다.`, + ); return undefined; } catch (error) { console.error("외부 테이블 조회 오류:", error); @@ -233,7 +232,7 @@ async function fetchExternalLookupValue( /** * 외부 테이블에서 값을 조회하는 함수 - * + * * @param tableName - 조회할 테이블명 * @param valueColumn - 가져올 컬럼명 * @param joinConditions - 조인 조건 목록 @@ -247,7 +246,7 @@ async function fetchExternalValue( joinConditions: TableJoinCondition[], rowData: any, sourceData: any, - formData: FormDataState + formData: FormDataState, ): Promise { if (joinConditions.length === 0) { return undefined; @@ -298,15 +297,16 @@ async function fetchExternalValue( // 정확히 일치하는 검색을 위해 operator: "equals" 사용 whereConditions[condition.targetColumn] = { value: convertedValue, - operator: "equals" + operator: "equals", }; } // API 호출 - const response = await apiClient.post( - `/table-management/tables/${tableName}/data`, - { search: whereConditions, size: 1, page: 1 } - ); + const response = await apiClient.post(`/table-management/tables/${tableName}/data`, { + search: whereConditions, + size: 1, + page: 1, + }); if (response.data.success && response.data.data?.data?.length > 0) { return response.data.data.data[0][valueColumn]; @@ -334,42 +334,42 @@ export function TableSectionRenderer({ }: TableSectionRendererProps) { // 테이블 데이터 상태 (일반 모드) const [tableData, setTableData] = useState([]); - + // 조건부 테이블 데이터 상태 (조건별로 분리) const [conditionalTableData, setConditionalTableData] = useState({}); - + // 조건부 테이블: 선택된 조건들 (체크박스 모드) const [selectedConditions, setSelectedConditions] = useState([]); - + // 조건부 테이블: 현재 활성 탭 const [activeConditionTab, setActiveConditionTab] = useState(""); - + // 조건부 테이블: 현재 모달이 열린 조건 (어떤 조건의 테이블에 추가할지) const [modalCondition, setModalCondition] = useState(""); - + // 모달 상태 const [modalOpen, setModalOpen] = useState(false); - + // 체크박스 선택 상태 (조건별로 분리) const [selectedRows, setSelectedRows] = useState>(new Set()); const [conditionalSelectedRows, setConditionalSelectedRows] = useState>>({}); - + // 너비 조정 트리거 (홀수: 자동맞춤, 짝수: 균등분배) const [widthTrigger, setWidthTrigger] = useState(0); - + // 동적 데이터 소스 활성화 상태 const [activeDataSources, setActiveDataSources] = useState>({}); - + // 날짜 일괄 적용 완료 플래그 (컬럼별로 한 번만 적용) const [batchAppliedFields, setBatchAppliedFields] = useState>(new Set()); // 초기 데이터 로드 완료 플래그 (무한 루프 방지) const initialDataLoadedRef = React.useRef(false); - + // 조건부 테이블 설정 const conditionalConfig = tableConfig.conditionalTable; const isConditionalMode = conditionalConfig?.enabled ?? false; - + // 조건부 테이블: 동적 옵션 로드 상태 const [dynamicOptions, setDynamicOptions] = useState<{ id: string; value: string; label: string }[]>([]); const [dynamicOptionsLoading, setDynamicOptionsLoading] = useState(false); @@ -380,51 +380,48 @@ export function TableSectionRenderer({ if (!isConditionalMode) return; if (!conditionalConfig?.optionSource?.enabled) return; if (dynamicOptionsLoadedRef.current) return; - + const { tableName, valueColumn, labelColumn, filterCondition } = conditionalConfig.optionSource; - + if (!tableName || !valueColumn) return; - + const loadDynamicOptions = async () => { setDynamicOptionsLoading(true); try { // DISTINCT 값을 가져오기 위한 API 호출 - const response = await apiClient.post( - `/table-management/tables/${tableName}/data`, - { - search: filterCondition ? { _raw: filterCondition } : {}, - size: 1000, - page: 1, - } - ); - + const response = await apiClient.post(`/table-management/tables/${tableName}/data`, { + search: filterCondition ? { _raw: filterCondition } : {}, + size: 1000, + page: 1, + }); + if (response.data.success && response.data.data?.data) { const rows = response.data.data.data; - + // 중복 제거하여 고유 값 추출 const uniqueValues = new Map(); for (const row of rows) { const value = row[valueColumn]; if (value && !uniqueValues.has(value)) { - const label = labelColumn ? (row[labelColumn] || value) : value; + const label = labelColumn ? row[labelColumn] || value : value; uniqueValues.set(value, label); } } - + // 옵션 배열로 변환 const options = Array.from(uniqueValues.entries()).map(([value, label], index) => ({ id: `dynamic_${index}`, value, label, })); - + console.log("[TableSectionRenderer] 동적 옵션 로드 완료:", { tableName, valueColumn, optionCount: options.length, options, }); - + setDynamicOptions(options); dynamicOptionsLoadedRef.current = true; } @@ -434,48 +431,45 @@ export function TableSectionRenderer({ setDynamicOptionsLoading(false); } }; - + loadDynamicOptions(); }, [isConditionalMode, conditionalConfig?.optionSource]); // ============================================ // 동적 Select 옵션 (소스 테이블에서 드롭다운 옵션 로드) // ============================================ - + // 소스 테이블 데이터 캐시 (동적 Select 옵션용) const [sourceDataCache, setSourceDataCache] = useState([]); const sourceDataLoadedRef = React.useRef(false); - + // 동적 Select 옵션이 있는 컬럼 확인 const hasDynamicSelectColumns = useMemo(() => { - return tableConfig.columns?.some(col => col.dynamicSelectOptions?.enabled); + return tableConfig.columns?.some((col) => col.dynamicSelectOptions?.enabled); }, [tableConfig.columns]); - + // 소스 테이블 데이터 로드 (동적 Select 옵션용) useEffect(() => { if (!hasDynamicSelectColumns) return; if (sourceDataLoadedRef.current) return; if (!tableConfig.source?.tableName) return; - + const loadSourceData = async () => { try { // 조건부 테이블 필터 조건 적용 const filterCondition: Record = {}; - + // 소스 필터가 활성화되어 있고 조건이 선택되어 있으면 필터 적용 if (conditionalConfig?.sourceFilter?.enabled && activeConditionTab) { filterCondition[conditionalConfig.sourceFilter.filterColumn] = activeConditionTab; } - - const response = await apiClient.post( - `/table-management/tables/${tableConfig.source.tableName}/data`, - { - search: filterCondition, - size: 1000, - page: 1, - } - ); - + + 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; @@ -489,36 +483,33 @@ export function TableSectionRenderer({ console.error("[TableSectionRenderer] 소스 데이터 로드 실패:", error); } }; - + loadSourceData(); }, [hasDynamicSelectColumns, tableConfig.source?.tableName, conditionalConfig?.sourceFilter, activeConditionTab]); - + // 조건 탭 변경 시 소스 데이터 다시 로드 useEffect(() => { if (!hasDynamicSelectColumns) return; if (!conditionalConfig?.sourceFilter?.enabled) return; if (!activeConditionTab) return; if (!tableConfig.source?.tableName) return; - + // 조건 변경 시 캐시 리셋하고 즉시 다시 로드 sourceDataLoadedRef.current = false; setSourceDataCache([]); - + // 즉시 데이터 다시 로드 (기존 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, - } - ); - + + 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; @@ -532,96 +523,100 @@ export function TableSectionRenderer({ console.error("[TableSectionRenderer] 소스 데이터 로드 실패:", error); } }; - + loadSourceData(); - }, [activeConditionTab, hasDynamicSelectColumns, conditionalConfig?.sourceFilter?.enabled, conditionalConfig?.sourceFilter?.filterColumn, tableConfig.source?.tableName]); - + }, [ + activeConditionTab, + hasDynamicSelectColumns, + conditionalConfig?.sourceFilter?.enabled, + conditionalConfig?.sourceFilter?.filterColumn, + tableConfig.source?.tableName, + ]); + // 컬럼별 동적 Select 옵션 생성 const dynamicSelectOptionsMap = useMemo(() => { const optionsMap: Record = {}; - + if (!sourceDataCache.length) return optionsMap; - + for (const col of tableConfig.columns || []) { if (!col.dynamicSelectOptions?.enabled) continue; - + const { sourceField, labelField, distinct = true } = col.dynamicSelectOptions; - + if (!sourceField) continue; - + // 소스 데이터에서 옵션 추출 const seenValues = new Set(); const options: { value: string; label: string }[] = []; - + for (const row of sourceDataCache) { const value = row[sourceField]; if (value === undefined || value === null || value === "") continue; - + const stringValue = String(value); - + if (distinct && seenValues.has(stringValue)) continue; seenValues.add(stringValue); - - const label = labelField ? (row[labelField] || stringValue) : stringValue; + + const label = labelField ? row[labelField] || stringValue : stringValue; options.push({ value: stringValue, label: String(label) }); } - + optionsMap[col.field] = options; } - + 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 - ); - + 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] + [onTableDataChange, tableConfig.columns, batchAppliedFields], ); - + // 행 선택 모드: 드롭다운 값 변경 시 같은 소스 행의 다른 컬럼들 자동 채움 const handleDynamicSelectChange = useCallback( (rowIndex: number, columnField: string, selectedValue: string, conditionValue?: string) => { - const column = tableConfig.columns?.find(col => col.field === columnField); + const column = tableConfig.columns?.find((col) => col.field === columnField); if (!column?.dynamicSelectOptions?.rowSelectionMode?.enabled) { // 행 선택 모드가 아니면 일반 값 변경만 if (conditionValue && isConditionalMode) { const currentData = conditionalTableData[conditionValue] || []; const newData = [...currentData]; newData[rowIndex] = { ...newData[rowIndex], [columnField]: selectedValue }; - setConditionalTableData(prev => ({ ...prev, [conditionValue]: newData })); + setConditionalTableData((prev) => ({ ...prev, [conditionValue]: newData })); onConditionalTableDataChange?.(conditionValue, newData); } else { const newData = [...tableData]; @@ -630,18 +625,18 @@ export function TableSectionRenderer({ } return; } - + // 행 선택 모드: 소스 데이터에서 해당 값을 가진 행 찾기 const { sourceField } = column.dynamicSelectOptions; const { autoFillColumns, sourceIdColumn, targetIdField } = column.dynamicSelectOptions.rowSelectionMode; - - const sourceRow = sourceDataCache.find(row => String(row[sourceField]) === selectedValue); - + + const sourceRow = sourceDataCache.find((row) => String(row[sourceField]) === selectedValue); + if (!sourceRow) { console.warn(`[TableSectionRenderer] 소스 행을 찾을 수 없음: ${sourceField} = ${selectedValue}`); return; } - + // 현재 행 데이터 가져오기 let currentData: any[]; if (conditionValue && isConditionalMode) { @@ -649,10 +644,10 @@ export function TableSectionRenderer({ } else { currentData = tableData; } - + const newData = [...currentData]; const updatedRow = { ...newData[rowIndex], [columnField]: selectedValue }; - + // 자동 채움 매핑 적용 if (autoFillColumns) { for (const mapping of autoFillColumns) { @@ -662,22 +657,22 @@ export function TableSectionRenderer({ } } } - + // 소스 ID 저장 if (sourceIdColumn && targetIdField) { updatedRow[targetIdField] = sourceRow[sourceIdColumn]; } - + newData[rowIndex] = updatedRow; - + // 데이터 업데이트 if (conditionValue && isConditionalMode) { - setConditionalTableData(prev => ({ ...prev, [conditionValue]: newData })); + setConditionalTableData((prev) => ({ ...prev, [conditionValue]: newData })); onConditionalTableDataChange?.(conditionValue, newData); } else { handleDataChange(newData); } - + console.log("[TableSectionRenderer] 행 선택 모드 자동 채움:", { columnField, selectedValue, @@ -685,93 +680,101 @@ export function TableSectionRenderer({ updatedRow, }); }, - [tableConfig.columns, sourceDataCache, tableData, conditionalTableData, isConditionalMode, handleDataChange, onConditionalTableDataChange] + [ + 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)); + 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`, - { + + 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; } - ); - - 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]; + + 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]); + + 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(() => { @@ -788,7 +791,7 @@ export function TableSectionRenderer({ }); setTableData(initialData); initialDataLoadedRef.current = true; - + // 참조 컬럼 값 조회 (saveToTarget: false인 컬럼) loadReferenceColumnValues(initialData); } @@ -796,14 +799,14 @@ export function TableSectionRenderer({ // RepeaterColumnConfig로 변환 (동적 Select 옵션 반영) const columns: RepeaterColumnConfig[] = useMemo(() => { - return (tableConfig.columns || []).map(col => { + return (tableConfig.columns || []).map((col) => { const baseColumn = convertToRepeaterColumn(col); - + // 동적 Select 옵션이 있으면 적용 if (col.dynamicSelectOptions?.enabled && dynamicSelectOptionsMap[col.field]) { baseColumn.selectOptions = dynamicSelectOptionsMap[col.field]; } - + return baseColumn; }); }, [tableConfig.columns, dynamicSelectOptionsMap]); @@ -840,23 +843,24 @@ export function TableSectionRenderer({ return updatedRow; }, - [calculationRules] + [calculationRules], ); const calculateAll = useCallback( (data: any[]): any[] => { return data.map((row) => calculateRow(row)); }, - [calculateRow] + [calculateRow], ); // 행 변경 핸들러 (동적 Select 행 선택 모드 지원) const handleRowChange = useCallback( (index: number, newRow: any, conditionValue?: string) => { - const oldRow = conditionValue && isConditionalMode - ? (conditionalTableData[conditionValue]?.[index] || {}) - : (tableData[index] || {}); - + const oldRow = + conditionValue && isConditionalMode + ? conditionalTableData[conditionValue]?.[index] || {} + : tableData[index] || {}; + // 변경된 필드 찾기 const changedFields: string[] = []; for (const key of Object.keys(newRow)) { @@ -864,25 +868,25 @@ export function TableSectionRenderer({ changedFields.push(key); } } - + // 동적 Select 컬럼의 행 선택 모드 확인 for (const changedField of changedFields) { - const column = tableConfig.columns?.find(col => col.field === changedField); + const column = tableConfig.columns?.find((col) => col.field === changedField); if (column?.dynamicSelectOptions?.rowSelectionMode?.enabled) { // 행 선택 모드 처리 (자동 채움) handleDynamicSelectChange(index, changedField, newRow[changedField], conditionValue); return; // 행 선택 모드에서 처리 완료 } } - + // 일반 행 변경 처리 const calculatedRow = calculateRow(newRow); - + if (conditionValue && isConditionalMode) { const currentData = conditionalTableData[conditionValue] || []; const newData = [...currentData]; newData[index] = calculatedRow; - setConditionalTableData(prev => ({ ...prev, [conditionValue]: newData })); + setConditionalTableData((prev) => ({ ...prev, [conditionValue]: newData })); onConditionalTableDataChange?.(conditionValue, newData); } else { const newData = [...tableData]; @@ -890,7 +894,16 @@ export function TableSectionRenderer({ handleDataChange(newData); } }, - [tableData, conditionalTableData, isConditionalMode, tableConfig.columns, calculateRow, handleDataChange, handleDynamicSelectChange, onConditionalTableDataChange] + [ + tableData, + conditionalTableData, + isConditionalMode, + tableConfig.columns, + calculateRow, + handleDataChange, + handleDynamicSelectChange, + onConditionalTableDataChange, + ], ); // 행 삭제 핸들러 @@ -899,7 +912,7 @@ export function TableSectionRenderer({ const newData = tableData.filter((_, i) => i !== index); handleDataChange(newData); }, - [tableData, handleDataChange] + [tableData, handleDataChange], ); // 선택된 항목 일괄 삭제 @@ -908,7 +921,7 @@ export function TableSectionRenderer({ const newData = tableData.filter((_, index) => !selectedRows.has(index)); handleDataChange(newData); setSelectedRows(new Set()); - + // 데이터가 모두 삭제되면 일괄 적용 플래그도 리셋 if (newData.length === 0) { setBatchAppliedFields(new Set()); @@ -931,7 +944,7 @@ export function TableSectionRenderer({ // 현재 활성화된 옵션 또는 기본 옵션 사용 const activeOptionId = activeDataSources[col.field]; const defaultOption = col.lookup.options.find((o) => o.isDefault) || col.lookup.options[0]; - const selectedOption = activeOptionId + const selectedOption = activeOptionId ? col.lookup.options.find((o) => o.id === activeOptionId) || defaultOption : defaultOption; @@ -969,11 +982,13 @@ export function TableSectionRenderer({ // 외부 테이블 조회 설정 externalLookup: cond.externalLookup, // 값 변환 설정 전달 (레거시 호환) - transform: cond.transform?.enabled ? { - tableName: cond.transform.tableName, - matchColumn: cond.transform.matchColumn, - resultColumn: cond.transform.resultColumn, - } : undefined, + transform: cond.transform?.enabled + ? { + tableName: cond.transform.tableName, + matchColumn: cond.transform.matchColumn, + resultColumn: cond.transform.resultColumn, + } + : undefined, }; }); @@ -982,15 +997,15 @@ export function TableSectionRenderer({ selectedOption.tableName, selectedOption.valueColumn, joinConditions, - { ...sourceItem, ...newItem }, // rowData (현재 행) - sourceItem, // sourceData (소스 테이블 원본) - formData + { ...sourceItem, ...newItem }, // rowData (현재 행) + sourceItem, // sourceData (소스 테이블 원본) + formData, ); - + if (value !== undefined) { newItem[col.field] = value; } - + // _sourceData에 원본 저장 newItem._sourceData = sourceItem; } @@ -1045,8 +1060,8 @@ export function TableSectionRenderer({ valueColumn, joinConditions, { ...sourceItem, ...newItem }, // rowData - sourceItem, // sourceData - formData + sourceItem, // sourceData + formData, ); if (value !== undefined) { newItem[col.field] = value; @@ -1070,7 +1085,7 @@ export function TableSectionRenderer({ } return newItem; - }) + }), ); // 계산 필드 업데이트 @@ -1080,7 +1095,7 @@ export function TableSectionRenderer({ const newData = [...tableData, ...calculatedItems]; handleDataChange(newData); }, - [tableConfig.columns, formData, tableData, calculateAll, handleDataChange, activeDataSources] + [tableConfig.columns, formData, tableData, calculateAll, handleDataChange, activeDataSources], ); // 컬럼 모드/조회 옵션 변경 핸들러 @@ -1093,7 +1108,7 @@ export function TableSectionRenderer({ // 해당 컬럼의 모든 행 데이터 재조회 const column = tableConfig.columns.find((col) => col.field === columnField); - + // lookup 설정이 있는 경우 (새로운 조회 기능) if (column?.lookup?.enabled && column.lookup.options) { const selectedOption = column.lookup.options.find((opt) => opt.id === optionId); @@ -1140,11 +1155,13 @@ export function TableSectionRenderer({ // 외부 테이블 조회 설정 externalLookup: cond.externalLookup, // 값 변환 설정 전달 (레거시 호환) - transform: cond.transform?.enabled ? { - tableName: cond.transform.tableName, - matchColumn: cond.transform.matchColumn, - resultColumn: cond.transform.resultColumn, - } : undefined, + transform: cond.transform?.enabled + ? { + tableName: cond.transform.tableName, + matchColumn: cond.transform.matchColumn, + resultColumn: cond.transform.resultColumn, + } + : undefined, }; }); @@ -1156,15 +1173,15 @@ export function TableSectionRenderer({ joinConditions, row, sourceData, - formData + formData, ); - + if (value !== undefined) { newValue = value; } return { ...row, [columnField]: newValue }; - }) + }), ); // 계산 필드 업데이트 @@ -1199,14 +1216,14 @@ export function TableSectionRenderer({ } return { ...row, [columnField]: newValue }; - }) + }), ); // 계산 필드 업데이트 const calculatedData = calculateAll(updatedData); handleDataChange(calculatedData); }, - [tableConfig.columns, tableData, formData, calculateAll, handleDataChange] + [tableConfig.columns, tableData, formData, calculateAll, handleDataChange], ); // 소스 테이블 정보 @@ -1216,10 +1233,16 @@ export function TableSectionRenderer({ const sourceSearchFields = source.searchColumns; const columnLabels = source.columnLabels || {}; const modalTitle = uiConfig?.modalTitle || "항목 검색 및 선택"; - const addButtonType = uiConfig?.addButtonType || "search"; - const addButtonText = uiConfig?.addButtonText || (addButtonType === "addRow" ? "항목 추가" : "항목 검색"); const multiSelect = uiConfig?.multiSelect ?? true; + // 버튼 표시 설정 (두 버튼 동시 표시 가능) + // 레거시 호환: 기존 addButtonType 설정이 있으면 그에 맞게 변환 + const legacyAddButtonType = uiConfig?.addButtonType; + const showSearchButton = legacyAddButtonType === "addRow" ? false : (uiConfig?.showSearchButton ?? true); + const showAddRowButton = legacyAddButtonType === "addRow" ? true : (uiConfig?.showAddRowButton ?? false); + const searchButtonText = uiConfig?.searchButtonText || uiConfig?.addButtonText || "품목 검색"; + const addRowButtonText = uiConfig?.addRowButtonText || "직접 입력"; + // 기본 필터 조건 생성 (사전 필터만 - 모달 필터는 ItemSelectionModal에서 처리) const baseFilterCondition: Record = useMemo(() => { const condition: Record = {}; @@ -1233,19 +1256,19 @@ export function TableSectionRenderer({ } return condition; }, [filters?.preFilters]); - + // 조건부 테이블용 필터 조건 생성 (선택된 조건값으로 소스 테이블 필터링) const conditionalFilterCondition = useMemo(() => { const filter = { ...baseFilterCondition }; - + // 조건부 테이블의 소스 필터 설정이 있고, 모달에서 선택된 조건이 있으면 적용 if (conditionalConfig?.sourceFilter?.enabled && modalCondition) { filter[conditionalConfig.sourceFilter.filterColumn] = modalCondition; } - + return filter; }, [baseFilterCondition, conditionalConfig?.sourceFilter, modalCondition]); - + // 모달 필터 설정을 ItemSelectionModal에 전달할 형식으로 변환 const modalFiltersForModal = useMemo(() => { if (!filters?.modalFilters) return []; @@ -1253,7 +1276,7 @@ export function TableSectionRenderer({ column: filter.column, label: filter.label || filter.column, // category 타입을 select로 변환 (ModalFilterConfig 호환) - type: filter.type === "category" ? "select" as const : filter.type as "text" | "select", + type: filter.type === "category" ? ("select" as const) : (filter.type as "text" | "select"), options: filter.options, categoryRef: filter.categoryRef, defaultValue: filter.defaultValue, @@ -1265,138 +1288,156 @@ export function TableSectionRenderer({ // ============================================ // 조건부 테이블: 조건 체크박스 토글 - const handleConditionToggle = useCallback((conditionValue: string, checked: boolean) => { - setSelectedConditions((prev) => { - if (checked) { - const newConditions = [...prev, conditionValue]; - // 첫 번째 조건 선택 시 해당 탭 활성화 - if (prev.length === 0) { - setActiveConditionTab(conditionValue); + const handleConditionToggle = useCallback( + (conditionValue: string, checked: boolean) => { + setSelectedConditions((prev) => { + if (checked) { + const newConditions = [...prev, conditionValue]; + // 첫 번째 조건 선택 시 해당 탭 활성화 + if (prev.length === 0) { + setActiveConditionTab(conditionValue); + } + return newConditions; + } else { + const newConditions = prev.filter((c) => c !== conditionValue); + // 현재 활성 탭이 제거된 경우 다른 탭으로 전환 + if (activeConditionTab === conditionValue && newConditions.length > 0) { + setActiveConditionTab(newConditions[0]); + } + return newConditions; } - return newConditions; - } else { - const newConditions = prev.filter((c) => c !== conditionValue); - // 현재 활성 탭이 제거된 경우 다른 탭으로 전환 - if (activeConditionTab === conditionValue && newConditions.length > 0) { - setActiveConditionTab(newConditions[0]); - } - return newConditions; - } - }); - }, [activeConditionTab]); + }); + }, + [activeConditionTab], + ); // 조건부 테이블: 조건별 데이터 변경 - const handleConditionalDataChange = useCallback((conditionValue: string, newData: any[]) => { - setConditionalTableData((prev) => ({ - ...prev, - [conditionValue]: newData, - })); - - // 부모에게 조건별 데이터 변경 알림 - if (onConditionalTableDataChange) { - onConditionalTableDataChange(conditionValue, newData); - } - - // 전체 데이터를 flat array로 변환하여 onTableDataChange 호출 - // (저장 시 조건 컬럼 값이 자동으로 추가됨) - const conditionColumn = conditionalConfig?.conditionColumn; - const allData: any[] = []; - - // 현재 변경된 조건의 데이터 업데이트 - const updatedConditionalData = { ...conditionalTableData, [conditionValue]: newData }; - - for (const [condition, data] of Object.entries(updatedConditionalData)) { - for (const row of data) { - allData.push({ - ...row, - ...(conditionColumn ? { [conditionColumn]: condition } : {}), - }); + const handleConditionalDataChange = useCallback( + (conditionValue: string, newData: any[]) => { + setConditionalTableData((prev) => ({ + ...prev, + [conditionValue]: newData, + })); + + // 부모에게 조건별 데이터 변경 알림 + if (onConditionalTableDataChange) { + onConditionalTableDataChange(conditionValue, newData); } - } - - onTableDataChange(allData); - }, [conditionalTableData, conditionalConfig?.conditionColumn, onConditionalTableDataChange, onTableDataChange]); + + // 전체 데이터를 flat array로 변환하여 onTableDataChange 호출 + // (저장 시 조건 컬럼 값이 자동으로 추가됨) + const conditionColumn = conditionalConfig?.conditionColumn; + const allData: any[] = []; + + // 현재 변경된 조건의 데이터 업데이트 + const updatedConditionalData = { ...conditionalTableData, [conditionValue]: newData }; + + for (const [condition, data] of Object.entries(updatedConditionalData)) { + for (const row of data) { + allData.push({ + ...row, + ...(conditionColumn ? { [conditionColumn]: condition } : {}), + }); + } + } + + onTableDataChange(allData); + }, + [conditionalTableData, conditionalConfig?.conditionColumn, onConditionalTableDataChange, onTableDataChange], + ); // 조건부 테이블: 조건별 행 변경 - const handleConditionalRowChange = useCallback((conditionValue: string, index: number, newRow: any) => { - const calculatedRow = calculateRow(newRow); - const currentData = conditionalTableData[conditionValue] || []; - const newData = [...currentData]; - newData[index] = calculatedRow; - handleConditionalDataChange(conditionValue, newData); - }, [conditionalTableData, calculateRow, handleConditionalDataChange]); + const handleConditionalRowChange = useCallback( + (conditionValue: string, index: number, newRow: any) => { + const calculatedRow = calculateRow(newRow); + const currentData = conditionalTableData[conditionValue] || []; + const newData = [...currentData]; + newData[index] = calculatedRow; + handleConditionalDataChange(conditionValue, newData); + }, + [conditionalTableData, calculateRow, handleConditionalDataChange], + ); // 조건부 테이블: 조건별 행 삭제 - const handleConditionalRowDelete = useCallback((conditionValue: string, index: number) => { - const currentData = conditionalTableData[conditionValue] || []; - const newData = currentData.filter((_, i) => i !== index); - handleConditionalDataChange(conditionValue, newData); - }, [conditionalTableData, handleConditionalDataChange]); + const handleConditionalRowDelete = useCallback( + (conditionValue: string, index: number) => { + const currentData = conditionalTableData[conditionValue] || []; + const newData = currentData.filter((_, i) => i !== index); + handleConditionalDataChange(conditionValue, newData); + }, + [conditionalTableData, handleConditionalDataChange], + ); // 조건부 테이블: 조건별 선택 행 일괄 삭제 - const handleConditionalBulkDelete = useCallback((conditionValue: string) => { - const selected = conditionalSelectedRows[conditionValue] || new Set(); - if (selected.size === 0) return; - - const currentData = conditionalTableData[conditionValue] || []; - const newData = currentData.filter((_, index) => !selected.has(index)); - handleConditionalDataChange(conditionValue, newData); - - // 선택 상태 초기화 - setConditionalSelectedRows((prev) => ({ - ...prev, - [conditionValue]: new Set(), - })); - }, [conditionalTableData, conditionalSelectedRows, handleConditionalDataChange]); + const handleConditionalBulkDelete = useCallback( + (conditionValue: string) => { + const selected = conditionalSelectedRows[conditionValue] || new Set(); + if (selected.size === 0) return; + + const currentData = conditionalTableData[conditionValue] || []; + const newData = currentData.filter((_, index) => !selected.has(index)); + handleConditionalDataChange(conditionValue, newData); + + // 선택 상태 초기화 + setConditionalSelectedRows((prev) => ({ + ...prev, + [conditionValue]: new Set(), + })); + }, + [conditionalTableData, conditionalSelectedRows, handleConditionalDataChange], + ); // 조건부 테이블: 아이템 추가 (특정 조건에) - const handleConditionalAddItems = useCallback(async (items: any[]) => { - if (!modalCondition) return; - - // 기존 handleAddItems 로직을 재사용하여 매핑된 아이템 생성 - const mappedItems = await Promise.all( - items.map(async (sourceItem) => { - const newItem: any = {}; - - for (const col of tableConfig.columns) { - const mapping = col.valueMapping; - - // 소스 필드에서 값 복사 (기본) - if (!mapping) { - const sourceField = col.sourceField || col.field; - if (sourceItem[sourceField] !== undefined) { - newItem[col.field] = sourceItem[sourceField]; + const handleConditionalAddItems = useCallback( + async (items: any[]) => { + if (!modalCondition) return; + + // 기존 handleAddItems 로직을 재사용하여 매핑된 아이템 생성 + const mappedItems = await Promise.all( + items.map(async (sourceItem) => { + const newItem: any = {}; + + for (const col of tableConfig.columns) { + const mapping = col.valueMapping; + + // 소스 필드에서 값 복사 (기본) + if (!mapping) { + const sourceField = col.sourceField || col.field; + if (sourceItem[sourceField] !== undefined) { + newItem[col.field] = sourceItem[sourceField]; + } + continue; } - continue; - } - - // valueMapping 처리 - if (mapping.type === "source" && mapping.sourceField) { - const value = sourceItem[mapping.sourceField]; - if (value !== undefined) { - newItem[col.field] = value; + + // valueMapping 처리 + if (mapping.type === "source" && mapping.sourceField) { + const value = sourceItem[mapping.sourceField]; + if (value !== undefined) { + newItem[col.field] = value; + } + } else if (mapping.type === "manual") { + newItem[col.field] = col.defaultValue || ""; + } else if (mapping.type === "internal" && mapping.internalField) { + newItem[col.field] = formData[mapping.internalField]; } - } else if (mapping.type === "manual") { - newItem[col.field] = col.defaultValue || ""; - } else if (mapping.type === "internal" && mapping.internalField) { - newItem[col.field] = formData[mapping.internalField]; } - } - - // 원본 소스 데이터 보존 - newItem._sourceData = sourceItem; - - return newItem; - }) - ); - - // 현재 조건의 데이터에 추가 - const currentData = conditionalTableData[modalCondition] || []; - const newData = [...currentData, ...mappedItems]; - handleConditionalDataChange(modalCondition, newData); - - setModalOpen(false); - }, [modalCondition, tableConfig.columns, formData, conditionalTableData, handleConditionalDataChange]); + + // 원본 소스 데이터 보존 + newItem._sourceData = sourceItem; + + return newItem; + }), + ); + + // 현재 조건의 데이터에 추가 + const currentData = conditionalTableData[modalCondition] || []; + const newData = [...currentData, ...mappedItems]; + handleConditionalDataChange(modalCondition, newData); + + setModalOpen(false); + }, + [modalCondition, tableConfig.columns, formData, conditionalTableData, handleConditionalDataChange], + ); // 조건부 테이블: 모달 열기 (특정 조건에 대해) const openConditionalModal = useCallback((conditionValue: string) => { @@ -1405,62 +1446,68 @@ export function TableSectionRenderer({ }, []); // 조건부 테이블: 빈 행 추가 (addRow 모드에서 사용) - const addEmptyRowToCondition = useCallback((conditionValue: string) => { - const newRow: Record = {}; - - // 각 컬럼의 기본값으로 빈 행 생성 - for (const col of tableConfig.columns) { - if (col.defaultValue !== undefined) { - newRow[col.field] = col.defaultValue; - } else if (col.type === "number") { - newRow[col.field] = 0; - } else if (col.type === "checkbox") { - newRow[col.field] = false; - } else { - newRow[col.field] = ""; - } - } - - // 조건 컬럼에 현재 조건 값 설정 - if (conditionalConfig?.conditionColumn) { - newRow[conditionalConfig.conditionColumn] = conditionValue; - } - - // 현재 조건의 데이터에 추가 - const currentData = conditionalTableData[conditionValue] || []; - const newData = [...currentData, newRow]; - handleConditionalDataChange(conditionValue, newData); - }, [tableConfig.columns, conditionalConfig?.conditionColumn, conditionalTableData, handleConditionalDataChange]); + const addEmptyRowToCondition = useCallback( + (conditionValue: string) => { + const newRow: Record = {}; - // 버튼 클릭 핸들러 (addButtonType에 따라 다르게 동작) - const handleAddButtonClick = useCallback((conditionValue: string) => { - const addButtonType = tableConfig.uiConfig?.addButtonType || "search"; - - if (addButtonType === "addRow") { - // 빈 행 직접 추가 - addEmptyRowToCondition(conditionValue); - } else { - // 검색 모달 열기 + // 각 컬럼의 기본값으로 빈 행 생성 + for (const col of tableConfig.columns) { + if (col.defaultValue !== undefined) { + newRow[col.field] = col.defaultValue; + } else if (col.type === "number") { + newRow[col.field] = 0; + } else if (col.type === "checkbox") { + newRow[col.field] = false; + } else { + newRow[col.field] = ""; + } + } + + // 조건 컬럼에 현재 조건 값 설정 + if (conditionalConfig?.conditionColumn) { + newRow[conditionalConfig.conditionColumn] = conditionValue; + } + + // 현재 조건의 데이터에 추가 + const currentData = conditionalTableData[conditionValue] || []; + const newData = [...currentData, newRow]; + handleConditionalDataChange(conditionValue, newData); + }, + [tableConfig.columns, conditionalConfig?.conditionColumn, conditionalTableData, handleConditionalDataChange], + ); + + // 검색 버튼 클릭 핸들러 + const handleSearchButtonClick = useCallback( + (conditionValue: string) => { openConditionalModal(conditionValue); - } - }, [tableConfig.uiConfig?.addButtonType, addEmptyRowToCondition, openConditionalModal]); + }, + [openConditionalModal], + ); + + // 행 추가 버튼 클릭 핸들러 + const handleAddRowButtonClick = useCallback( + (conditionValue: string) => { + addEmptyRowToCondition(conditionValue); + }, + [addEmptyRowToCondition], + ); // 조건부 테이블: 초기 데이터 로드 (수정 모드) useEffect(() => { if (!isConditionalMode) return; if (initialDataLoadedRef.current) return; - + const tableSectionKey = `_tableSection_${sectionId}`; const initialData = formData[tableSectionKey]; - + if (Array.isArray(initialData) && initialData.length > 0) { const conditionColumn = conditionalConfig?.conditionColumn; - + if (conditionColumn) { // 조건별로 데이터 그룹핑 const grouped: ConditionalTableData = {}; const conditions = new Set(); - + for (const row of initialData) { const conditionValue = row[conditionColumn] || ""; if (conditionValue) { @@ -1471,15 +1518,15 @@ export function TableSectionRenderer({ conditions.add(conditionValue); } } - + setConditionalTableData(grouped); setSelectedConditions(Array.from(conditions)); - + // 첫 번째 조건을 활성 탭으로 설정 if (conditions.size > 0) { setActiveConditionTab(Array.from(conditions)[0]); } - + initialDataLoadedRef.current = true; } } @@ -1495,27 +1542,29 @@ export function TableSectionRenderer({ // ============================================ if (isConditionalMode && conditionalConfig) { const { triggerType } = conditionalConfig; - + // 정적 옵션과 동적 옵션 병합 (동적 옵션이 있으면 우선 사용) // 빈 value를 가진 옵션은 제외 (Select.Item은 빈 문자열 value를 허용하지 않음) - const effectiveOptions = (conditionalConfig.optionSource?.enabled && dynamicOptions.length > 0 - ? dynamicOptions - : conditionalConfig.options || []).filter(opt => opt.value && opt.value.trim() !== ""); - + const effectiveOptions = ( + conditionalConfig.optionSource?.enabled && dynamicOptions.length > 0 + ? dynamicOptions + : conditionalConfig.options || [] + ).filter((opt) => opt.value && opt.value.trim() !== ""); + // 로딩 중이면 로딩 표시 if (dynamicOptionsLoading) { return (
-
+
-
+
조건 옵션을 불러오는 중...
); } - + return (
{/* 조건 선택 UI */} @@ -1525,7 +1574,7 @@ export function TableSectionRenderer({ {effectiveOptions.map((option) => (
- + {selectedConditions.length > 0 && ( -
+
{selectedConditions.length}개 유형 선택됨, 총 {totalConditionalItems}개 항목
)}
)} - + {triggerType === "dropdown" && (
유형 선택: @@ -1566,7 +1615,7 @@ export function TableSectionRenderer({ {effectiveOptions.map((option) => ( {option.label} - {conditionalTableData[option.value]?.length > 0 && + {conditionalTableData[option.value]?.length > 0 && ` (${conditionalTableData[option.value].length})`} ))} @@ -1574,7 +1623,7 @@ export function TableSectionRenderer({
)} - + {/* 선택된 조건들의 테이블 (탭 형태) */} {selectedConditions.length > 0 && ( @@ -1594,17 +1643,17 @@ export function TableSectionRenderer({ ); })} - + {selectedConditions.map((conditionValue) => { const data = conditionalTableData[conditionValue] || []; const selected = conditionalSelectedRows[conditionValue] || new Set(); - + return ( {/* 테이블 상단 컨트롤 */}
- + {data.length > 0 && `${data.length}개 항목`} {selected.size > 0 && ` (${selected.size}개 선택됨)`} @@ -1642,20 +1691,25 @@ export function TableSectionRenderer({ 선택 삭제 ({selected.size}) )} - + {searchButtonText} + + )} + {showAddRowButton && ( + + )}
- + {/* 테이블 */} )} - + {/* tabs 모드: 모든 옵션을 탭으로 표시 (선택 UI 없음) */} {triggerType === "tabs" && effectiveOptions.length > 0 && ( - @@ -1702,16 +1756,16 @@ export function TableSectionRenderer({ ); })} - + {effectiveOptions.map((option) => { const data = conditionalTableData[option.value] || []; const selected = conditionalSelectedRows[option.value] || new Set(); - + return (
- + {data.length > 0 && `${data.length}개 항목`} {selected.size > 0 && ` (${selected.size}개 선택됨)`} @@ -1728,20 +1782,25 @@ export function TableSectionRenderer({ 선택 삭제 ({selected.size}) )} - + {searchButtonText} + + )} + {showAddRowButton && ( + + )}
- + )} - + {/* 조건이 선택되지 않은 경우 안내 메시지 (checkbox/dropdown 모드에서만) */} {selectedConditions.length === 0 && triggerType !== "tabs" && (
-

- {triggerType === "checkbox" - ? "위에서 유형을 선택하여 검사항목을 추가하세요." - : "유형을 선택하세요."} +

+ {triggerType === "checkbox" ? "위에서 유형을 선택하여 검사항목을 추가하세요." : "유형을 선택하세요."}

)} - + {/* 옵션이 없는 경우 안내 메시지 */} {effectiveOptions.length === 0 && (
-

- 조건 옵션이 설정되지 않았습니다. -

+

조건 옵션이 설정되지 않았습니다.

)} - + {/* 항목 선택 모달 (조건부 테이블용) */} {/* 추가 버튼 영역 */} -
+
- + {tableData.length > 0 && `${tableData.length}개 항목`} {selectedRows.size > 0 && ` (${selectedRows.size}개 선택됨)`} @@ -1822,17 +1877,17 @@ export function TableSectionRenderer({ variant="outline" size="sm" onClick={() => setWidthTrigger((prev) => prev + 1)} - className="h-7 text-xs px-2" + className="h-7 px-2 text-xs" title={widthTrigger % 2 === 0 ? "내용에 맞게 자동 조정" : "균등 분배"} > {widthTrigger % 2 === 0 ? ( <> - + 자동 맞춤 ) : ( <> - + 균등 분배 )} @@ -1841,17 +1896,20 @@ export function TableSectionRenderer({
{selectedRows.size > 0 && ( - )} - + )} + {showAddRowButton && ( + + }} + className="h-8 text-xs sm:h-10 sm:text-sm" + > + + {addRowButtonText} + + )}
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 ebd16c44..d82db59b 100644 --- a/frontend/lib/registry/components/universal-form-modal/modals/TableSectionSettingsModal.tsx +++ b/frontend/lib/registry/components/universal-form-modal/modals/TableSectionSettingsModal.tsx @@ -2928,54 +2928,74 @@ export function TableSectionSettingsModal({ {/* UI 설정 */}

UI 설정

-
-
- - + + {/* 버튼 표시 설정 */} +
+ +

+ 두 버튼을 동시에 표시할 수 있습니다. +

+
+
+ updateUiConfig({ showSearchButton: checked })} + className="scale-75" + /> +
+ 검색 버튼 +

기존 데이터에서 선택

+
+
+
+ updateUiConfig({ showAddRowButton: checked })} + className="scale-75" + /> +
+ 행 추가 버튼 +

빈 행 직접 입력

+
+
+
+ +
+ {/* 검색 버튼 텍스트 */}
- + updateUiConfig({ addButtonText: e.target.value })} - placeholder={tableConfig.uiConfig?.addButtonType === "addRow" ? "항목 추가" : "항목 검색"} + value={tableConfig.uiConfig?.searchButtonText || ""} + onChange={(e) => updateUiConfig({ searchButtonText: e.target.value })} + placeholder="품목 검색" className="h-8 text-xs mt-1" + disabled={!(tableConfig.uiConfig?.showSearchButton ?? true)} />
+ {/* 행 추가 버튼 텍스트 */}
- + + updateUiConfig({ addRowButtonText: e.target.value })} + placeholder="직접 입력" + className="h-8 text-xs mt-1" + disabled={!tableConfig.uiConfig?.showAddRowButton} + /> +
+ {/* 모달 제목 */} +
+ updateUiConfig({ modalTitle: e.target.value })} placeholder="항목 검색 및 선택" className="h-8 text-xs mt-1" - disabled={tableConfig.uiConfig?.addButtonType === "addRow"} + disabled={!(tableConfig.uiConfig?.showSearchButton ?? true)} /> - {tableConfig.uiConfig?.addButtonType === "addRow" && ( -

빈 행 추가 모드에서는 모달이 열리지 않습니다

- )}
+ {/* 테이블 최대 높이 */}
+ {/* 다중 선택 허용 */}
diff --git a/frontend/lib/registry/components/universal-form-modal/types.ts b/frontend/lib/registry/components/universal-form-modal/types.ts index 1f2015eb..a07feed6 100644 --- a/frontend/lib/registry/components/universal-form-modal/types.ts +++ b/frontend/lib/registry/components/universal-form-modal/types.ts @@ -253,15 +253,19 @@ export interface TableSectionConfig { // 6. UI 설정 uiConfig?: { - addButtonText?: string; // 추가 버튼 텍스트 (기본: "품목 검색") modalTitle?: string; // 모달 제목 (기본: "항목 검색 및 선택") multiSelect?: boolean; // 다중 선택 허용 (기본: true) maxHeight?: string; // 테이블 최대 높이 (기본: "400px") - // 추가 버튼 타입 - // - search: 검색 모달 열기 (기본값) - 기존 데이터에서 선택 - // - addRow: 빈 행 직접 추가 - 새 데이터 직접 입력 + // 버튼 표시 설정 (동시 표시 가능) + showSearchButton?: boolean; // 검색 버튼 표시 (기본: true) + showAddRowButton?: boolean; // 행 추가 버튼 표시 (기본: false) + searchButtonText?: string; // 검색 버튼 텍스트 (기본: "품목 검색") + addRowButtonText?: string; // 행 추가 버튼 텍스트 (기본: "직접 입력") + + // 레거시 호환용 (deprecated) addButtonType?: "search" | "addRow"; + addButtonText?: string; }; // 7. 조건부 테이블 설정 (고급)