"use client"; import React, { useState, useEffect, useCallback, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Badge } from "@/components/ui/badge"; import { Plus, Columns, AlignJustify, Trash2, Search } from "lucide-react"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; // 기존 ModalRepeaterTable 컴포넌트 재사용 import { RepeaterTable } from "../modal-repeater-table/RepeaterTable"; import { ItemSelectionModal } from "../modal-repeater-table/ItemSelectionModal"; import { RepeaterColumnConfig, CalculationRule } from "../modal-repeater-table/types"; // 타입 정의 import { TableSectionConfig, TableColumnConfig, TableJoinCondition, FormDataState, TableCalculationRule, } from "./types"; interface TableSectionRendererProps { sectionId: string; tableConfig: TableSectionConfig; formData: FormDataState; onFormDataChange: (field: string, value: any) => void; onTableDataChange: (data: any[]) => void; // 조건부 테이블용 콜백 (조건별 데이터 변경) onConditionalTableDataChange?: (conditionValue: string, data: any[]) => void; className?: string; } // 조건부 테이블 데이터 타입 interface ConditionalTableData { [conditionValue: string]: any[]; } /** * TableColumnConfig를 RepeaterColumnConfig로 변환 * columnModes 또는 lookup이 있으면 dynamicDataSource로 변환 */ function convertToRepeaterColumn(col: TableColumnConfig): RepeaterColumnConfig { const baseColumn: RepeaterColumnConfig = { field: col.field, label: col.label, type: col.type, editable: col.editable ?? true, calculated: col.calculated ?? false, width: col.width || "150px", required: col.required, hidden: col.hidden ?? false, defaultValue: col.defaultValue, selectOptions: col.selectOptions, // valueMapping은 별도로 처리 }; // lookup 설정을 dynamicDataSource로 변환 (새로운 조회 기능) if (col.lookup?.enabled && col.lookup.options && col.lookup.options.length > 0) { baseColumn.dynamicDataSource = { enabled: true, options: col.lookup.options.map((option) => ({ id: option.id, // "컬럼명 - 옵션라벨" 형식으로 헤더에 표시 label: option.displayLabel || option.label, // 헤더에 표시될 전체 라벨 (컬럼명 - 옵션라벨) headerLabel: `${col.label} - ${option.displayLabel || option.label}`, sourceType: "table" as const, tableConfig: { tableName: option.tableName, valueColumn: option.valueColumn, joinConditions: option.conditions.map((cond) => ({ sourceField: cond.sourceField, targetField: cond.targetColumn, // sourceType에 따른 데이터 출처 설정 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, })), }, // 조회 유형 정보 추가 lookupType: option.type, })), defaultOptionId: col.lookup.options.find((o) => o.isDefault)?.id || col.lookup.options[0]?.id, }; } // columnModes를 dynamicDataSource로 변환 (기존 로직 유지) else if (col.columnModes && col.columnModes.length > 0) { baseColumn.dynamicDataSource = { enabled: true, options: col.columnModes.map((mode) => ({ id: mode.id, label: mode.label, sourceType: "table" as const, // 실제 조회 로직은 TableSectionRenderer에서 처리 tableConfig: { tableName: mode.valueMapping?.externalRef?.tableName || "", valueColumn: mode.valueMapping?.externalRef?.valueColumn || "", joinConditions: (mode.valueMapping?.externalRef?.joinConditions || []).map((jc) => ({ sourceField: jc.sourceField, targetField: jc.targetColumn, })), }, })), 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 { return { result: calc.resultField, formula: calc.formula, dependencies: calc.dependencies, }; } /** * 값 변환 함수: 중간 테이블을 통해 값을 변환 * 예: 거래처 이름 "(무)테스트업체" → 거래처 코드 "CUST-0002" */ async function transformValue( value: any, transform: { tableName: string; matchColumn: string; resultColumn: string }, ): Promise { if (!value || !transform.tableName || !transform.matchColumn || !transform.resultColumn) { return value; } try { // 정확히 일치하는 검색 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]; return transformedValue; } console.warn(`변환 실패: ${transform.tableName}.${transform.matchColumn} = "${value}" 인 행을 찾을 수 없습니다.`); return undefined; } catch (error) { console.error("값 변환 오류:", error); return undefined; } } /** * 외부 테이블에서 조건 값을 조회하는 함수 * LookupCondition.sourceType이 "externalTable"인 경우 사용 */ async function fetchExternalLookupValue( externalLookup: { tableName: string; matchColumn: string; matchSourceType: "currentRow" | "sourceTable" | "sectionField"; matchSourceField: string; matchSectionId?: string; resultColumn: string; }, rowData: any, sourceData: any, formData: FormDataState, ): Promise { // 1. 비교 값 가져오기 let matchValue: any; if (externalLookup.matchSourceType === "currentRow") { matchValue = rowData[externalLookup.matchSourceField]; } else if (externalLookup.matchSourceType === "sourceTable") { matchValue = sourceData?.[externalLookup.matchSourceField]; } else { matchValue = formData[externalLookup.matchSourceField]; } if (matchValue === undefined || matchValue === null || matchValue === "") { 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, }); 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}" 인 행을 찾을 수 없습니다.`, ); return undefined; } catch (error) { console.error("외부 테이블 조회 오류:", error); return undefined; } } /** * 외부 테이블에서 값을 조회하는 함수 * * @param tableName - 조회할 테이블명 * @param valueColumn - 가져올 컬럼명 * @param joinConditions - 조인 조건 목록 * @param rowData - 현재 행 데이터 (설정된 컬럼 필드) * @param sourceData - 원본 소스 데이터 (_sourceData) * @param formData - 폼 데이터 (다른 섹션 필드) */ async function fetchExternalValue( tableName: string, valueColumn: string, joinConditions: TableJoinCondition[], rowData: any, sourceData: any, formData: FormDataState, ): Promise { if (joinConditions.length === 0) { return undefined; } try { const whereConditions: Record = {}; for (const condition of joinConditions) { let value: any; // 값 출처에 따라 가져오기 (4가지 소스 타입 지원) if (condition.sourceType === "row") { // 현재 행 데이터 (설정된 컬럼 필드) value = rowData[condition.sourceField]; } else if (condition.sourceType === "sourceData") { // 원본 소스 테이블 데이터 (_sourceData) value = sourceData?.[condition.sourceField]; } else if (condition.sourceType === "formData") { // formData에서 가져오기 (다른 섹션) value = formData[condition.sourceField]; } else if (condition.sourceType === "externalTable" && condition.externalLookup) { // 외부 테이블에서 조회하여 가져오기 value = await fetchExternalLookupValue(condition.externalLookup, rowData, sourceData, formData); } if (value === undefined || value === null || value === "") { return undefined; } // 값 변환이 필요한 경우 (예: 이름 → 코드) - 레거시 호환 if (condition.transform) { value = await transformValue(value, condition.transform); if (value === undefined) { return undefined; } } // 숫자형 ID 변환 let convertedValue = value; if (condition.targetColumn.endsWith("_id") || condition.targetColumn === "id") { const numValue = Number(value); if (!isNaN(numValue)) { convertedValue = numValue; } } // 정확히 일치하는 검색을 위해 operator: "equals" 사용 whereConditions[condition.targetColumn] = { value: convertedValue, operator: "equals", }; } // API 호출 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]; } return undefined; } catch (error) { console.error("외부 테이블 조회 오류:", error); return undefined; } } /** * 테이블 섹션 렌더러 * UniversalFormModal 내에서 테이블 형식의 데이터를 표시하고 편집 */ export function TableSectionRenderer({ sectionId, tableConfig, formData, onFormDataChange, onTableDataChange, onConditionalTableDataChange, className, }: 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); const dynamicOptionsLoadedRef = React.useRef(false); // 소스 테이블의 카테고리 타입 컬럼 목록 const [sourceCategoryColumns, setSourceCategoryColumns] = useState([]); // 소스 테이블의 카테고리 타입 컬럼 목록 로드 useEffect(() => { const loadCategoryColumns = async () => { if (!tableConfig.source.tableName) return; try { const response = await apiClient.get( `/table-categories/${tableConfig.source.tableName}/columns` ); if (response.data?.success && Array.isArray(response.data.data)) { const categoryColNames = response.data.data.map( (col: { columnName?: string; column_name?: string }) => col.columnName || col.column_name || "" ).filter(Boolean); setSourceCategoryColumns(categoryColNames); } } catch (error) { console.error("카테고리 컬럼 목록 조회 실패:", error); } }; loadCategoryColumns(); }, [tableConfig.source.tableName]); // 조건부 테이블: 동적 옵션 로드 (optionSource 설정이 있는 경우) useEffect(() => { 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, }); 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; 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; } } catch (error) { console.error("[TableSectionRenderer] 동적 옵션 로드 실패:", error); } finally { 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); }, [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, }); 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(); }, [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, }); 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(() => { 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; 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); 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) => { 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 })); onConditionalTableDataChange?.(conditionValue, newData); } else { const newData = [...tableData]; newData[rowIndex] = { ...newData[rowIndex], [columnField]: selectedValue }; handleDataChange(newData); } return; } // 행 선택 모드: 소스 데이터에서 해당 값을 가진 행 찾기 const { sourceField } = column.dynamicSelectOptions; const { autoFillColumns, sourceIdColumn, targetIdField } = column.dynamicSelectOptions.rowSelectionMode; const sourceRow = sourceDataCache.find((row) => String(row[sourceField]) === selectedValue); if (!sourceRow) { console.warn(`[TableSectionRenderer] 소스 행을 찾을 수 없음: ${sourceField} = ${selectedValue}`); return; } // 현재 행 데이터 가져오기 let currentData: any[]; if (conditionValue && isConditionalMode) { currentData = conditionalTableData[conditionValue] || []; } else { currentData = tableData; } const newData = [...currentData]; const updatedRow = { ...newData[rowIndex], [columnField]: selectedValue }; // 자동 채움 매핑 적용 if (autoFillColumns) { for (const mapping of autoFillColumns) { const sourceValue = sourceRow[mapping.sourceColumn]; if (sourceValue !== undefined) { updatedRow[mapping.targetField] = sourceValue; } } } // 소스 ID 저장 if (sourceIdColumn && targetIdField) { updatedRow[targetIdField] = sourceRow[sourceIdColumn]; } newData[rowIndex] = updatedRow; // 데이터 업데이트 if (conditionValue && isConditionalMode) { setConditionalTableData((prev) => ({ ...prev, [conditionValue]: newData })); onConditionalTableDataChange?.(conditionValue, newData); } else { handleDataChange(newData); } console.log("[TableSectionRenderer] 행 선택 모드 자동 채움:", { columnField, selectedValue, sourceRow, updatedRow, }); }, [ 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(() => { // 이미 초기화되었으면 스킵 if (initialDataLoadedRef.current) return; const tableSectionKey = `_tableSection_${sectionId}`; const initialData = formData[tableSectionKey]; if (Array.isArray(initialData) && initialData.length > 0) { // console.log("[TableSectionRenderer] 초기 데이터 로드:", { // sectionId, // itemCount: initialData.length, // }); setTableData(initialData); initialDataLoadedRef.current = true; // 참조 컬럼 값 조회 (saveToTarget: false인 컬럼) loadReferenceColumnValues(initialData); } }, [sectionId, formData, loadReferenceColumnValues]); // RepeaterColumnConfig로 변환 (동적 Select 옵션 반영) const columns: RepeaterColumnConfig[] = useMemo(() => { 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]); // 원본 계산 규칙 (조건부 계산 포함) const originalCalculationRules: TableCalculationRule[] = useMemo( () => tableConfig.calculations || [], [tableConfig.calculations], ); // 기본 계산 규칙 변환 (RepeaterTable용 - 조건부 계산이 없는 경우에 사용) // eslint-disable-next-line @typescript-eslint/no-unused-vars const calculationRules: CalculationRule[] = originalCalculationRules.map(convertToCalculationRule); // 조건부 계산 로직: 행의 조건 필드 값에 따라 적절한 계산식 선택 const getFormulaForRow = useCallback((rule: TableCalculationRule, row: Record): string => { // 조건부 계산이 활성화된 경우 if (rule.conditionalCalculation?.enabled && rule.conditionalCalculation.conditionField) { const conditionValue = row[rule.conditionalCalculation.conditionField]; // 조건값과 일치하는 규칙 찾기 const matchedRule = rule.conditionalCalculation.rules?.find((r) => r.conditionValue === conditionValue); if (matchedRule) { return matchedRule.formula; } // 일치하는 규칙이 없으면 기본 계산식 사용 if (rule.conditionalCalculation.defaultFormula) { return rule.conditionalCalculation.defaultFormula; } } // 조건부 계산이 비활성화되었거나 기본값이 없으면 원래 계산식 사용 return rule.formula; }, []); // 계산 로직 (조건부 계산 지원) const calculateRow = useCallback( (row: any): any => { if (originalCalculationRules.length === 0) return row; const updatedRow = { ...row }; for (const rule of originalCalculationRules) { try { // 조건부 계산에 따라 적절한 계산식 선택 let formula = getFormulaForRow(rule, row); if (!formula) continue; const fieldMatches = formula.match(/[a-zA-Z_][a-zA-Z0-9_]*/g) || []; const dependencies = rule.dependencies.length > 0 ? rule.dependencies : fieldMatches; for (const dep of dependencies) { if (dep === rule.resultField) continue; const value = parseFloat(row[dep]) || 0; formula = formula.replace(new RegExp(`\\b${dep}\\b`, "g"), value.toString()); } const result = new Function(`return ${formula}`)(); updatedRow[rule.resultField] = result; } catch (error) { console.error(`계산 오류 (${rule.formula}):`, error); updatedRow[rule.resultField] = 0; } } return updatedRow; }, [originalCalculationRules, getFormulaForRow], ); const calculateAll = useCallback( (data: any[]): any[] => { return data.map((row) => calculateRow(row)); }, [calculateRow], ); // 행 변경 핸들러 (동적 Select 행 선택 모드 지원) const handleRowChange = useCallback( (index: number, newRow: any, conditionValue?: string) => { const oldRow = conditionValue && isConditionalMode ? conditionalTableData[conditionValue]?.[index] || {} : tableData[index] || {}; // 변경된 필드 찾기 const changedFields: string[] = []; for (const key of Object.keys(newRow)) { if (oldRow[key] !== newRow[key]) { changedFields.push(key); } } // 동적 Select 컬럼의 행 선택 모드 확인 for (const changedField of changedFields) { 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 })); onConditionalTableDataChange?.(conditionValue, newData); } else { const newData = [...tableData]; newData[index] = calculatedRow; handleDataChange(newData); } }, [ tableData, conditionalTableData, isConditionalMode, tableConfig.columns, calculateRow, handleDataChange, handleDynamicSelectChange, onConditionalTableDataChange, ], ); // 행 삭제 핸들러 const handleRowDelete = useCallback( (index: number) => { const newData = tableData.filter((_, i) => i !== index); handleDataChange(newData); }, [tableData, handleDataChange], ); // 선택된 항목 일괄 삭제 const handleBulkDelete = useCallback(() => { if (selectedRows.size === 0) return; const newData = tableData.filter((_, index) => !selectedRows.has(index)); handleDataChange(newData); setSelectedRows(new Set()); // 데이터가 모두 삭제되면 일괄 적용 플래그도 리셋 if (newData.length === 0) { setBatchAppliedFields(new Set()); } }, [tableData, selectedRows, handleDataChange]); // 아이템 추가 핸들러 (모달에서 선택) const handleAddItems = useCallback( async (items: any[]) => { // 각 아이템에 대해 valueMapping 적용 const mappedItems = await Promise.all( items.map(async (sourceItem) => { const newItem: any = {}; for (const col of tableConfig.columns) { const mapping = col.valueMapping; // 0. lookup 설정이 있는 경우 (동적 조회) if (col.lookup?.enabled && col.lookup.options && col.lookup.options.length > 0) { // 현재 활성화된 옵션 또는 기본 옵션 사용 const activeOptionId = activeDataSources[col.field]; const defaultOption = col.lookup.options.find((o) => o.isDefault) || col.lookup.options[0]; const selectedOption = activeOptionId ? col.lookup.options.find((o) => o.id === activeOptionId) || defaultOption : defaultOption; if (selectedOption) { // sameTable 타입: 소스 데이터에서 직접 값 복사 if (selectedOption.type === "sameTable") { const value = sourceItem[selectedOption.valueColumn]; if (value !== undefined) { newItem[col.field] = value; } // _sourceData에 원본 저장 (나중에 다른 옵션으로 전환 시 사용) newItem._sourceData = sourceItem; continue; } // relatedTable, combinedLookup: 외부 테이블 조회 // 조인 조건 구성 (4가지 소스 타입 지원) const joinConditions: TableJoinCondition[] = selectedOption.conditions.map((cond) => { // sourceType 매핑 let sourceType: "row" | "sourceData" | "formData" | "externalTable"; if (cond.sourceType === "currentRow") { sourceType = "row"; } else if (cond.sourceType === "sourceTable") { sourceType = "sourceData"; } else if (cond.sourceType === "externalTable") { sourceType = "externalTable"; } else { sourceType = "formData"; } return { sourceType, sourceField: cond.sourceField, targetColumn: cond.targetColumn, // 외부 테이블 조회 설정 externalLookup: cond.externalLookup, // 값 변환 설정 전달 (레거시 호환) transform: cond.transform?.enabled ? { tableName: cond.transform.tableName, matchColumn: cond.transform.matchColumn, resultColumn: cond.transform.resultColumn, } : undefined, }; }); // 외부 테이블에서 값 조회 (sourceItem이 _sourceData 역할) const value = await fetchExternalValue( selectedOption.tableName, selectedOption.valueColumn, joinConditions, { ...sourceItem, ...newItem }, // rowData (현재 행) sourceItem, // sourceData (소스 테이블 원본) formData, ); if (value !== undefined) { newItem[col.field] = value; } // _sourceData에 원본 저장 newItem._sourceData = sourceItem; } continue; } // 1. 먼저 col.sourceField 확인 (간단 매핑) if (!mapping && col.sourceField) { // sourceField가 명시적으로 설정된 경우 if (sourceItem[col.sourceField] !== undefined) { newItem[col.field] = sourceItem[col.sourceField]; } continue; } if (!mapping) { // 매핑 없으면 소스에서 동일 필드명으로 복사 if (sourceItem[col.field] !== undefined) { newItem[col.field] = sourceItem[col.field]; } continue; } // 2. valueMapping이 있는 경우 (고급 매핑) switch (mapping.type) { case "source": // 소스 테이블에서 복사 const srcField = mapping.sourceField || col.sourceField || col.field; if (sourceItem[srcField] !== undefined) { newItem[col.field] = sourceItem[srcField]; } break; case "manual": // 사용자 입력 (빈 값 또는 기본값) newItem[col.field] = col.defaultValue ?? undefined; break; case "internal": // formData에서 값 가져오기 if (mapping.internalField) { newItem[col.field] = formData[mapping.internalField]; } break; case "external": // 외부 테이블에서 조회 if (mapping.externalRef) { const { tableName, valueColumn, joinConditions } = mapping.externalRef; const value = await fetchExternalValue( tableName, valueColumn, joinConditions, { ...sourceItem, ...newItem }, // rowData sourceItem, // sourceData formData, ); if (value !== undefined) { newItem[col.field] = value; } } break; } // 기본값 적용 if (col.defaultValue !== undefined && newItem[col.field] === undefined) { newItem[col.field] = col.defaultValue; } // 부모에서 값 받기 (receiveFromParent) if (col.receiveFromParent) { const parentField = col.parentFieldName || col.field; if (formData[parentField] !== undefined) { newItem[col.field] = formData[parentField]; } } } return newItem; }), ); // 계산 필드 업데이트 const calculatedItems = calculateAll(mappedItems); // 기존 데이터에 추가 const newData = [...tableData, ...calculatedItems]; handleDataChange(newData); }, [tableConfig.columns, formData, tableData, calculateAll, handleDataChange, activeDataSources], ); // 컬럼 모드/조회 옵션 변경 핸들러 const handleDataSourceChange = useCallback( async (columnField: string, optionId: string) => { setActiveDataSources((prev) => ({ ...prev, [columnField]: optionId, })); // 해당 컬럼의 모든 행 데이터 재조회 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); if (!selectedOption) return; // sameTable 타입: 현재 행의 소스 데이터에서 값 복사 (외부 조회 필요 없음) if (selectedOption.type === "sameTable") { const updatedData = tableData.map((row) => { // sourceField에서 값을 가져와 해당 컬럼에 복사 // row에 _sourceData가 있으면 거기서, 없으면 row 자체에서 가져옴 const sourceData = row._sourceData || row; const newValue = sourceData[selectedOption.valueColumn] ?? row[columnField]; return { ...row, [columnField]: newValue }; }); const calculatedData = calculateAll(updatedData); handleDataChange(calculatedData); return; } // 모든 행에 대해 새 값 조회 const updatedData = await Promise.all( tableData.map(async (row) => { let newValue: any = row[columnField]; // 조인 조건 구성 (4가지 소스 타입 지원) const joinConditions: TableJoinCondition[] = selectedOption.conditions.map((cond) => { // sourceType 매핑 let sourceType: "row" | "sourceData" | "formData" | "externalTable"; if (cond.sourceType === "currentRow") { sourceType = "row"; } else if (cond.sourceType === "sourceTable") { sourceType = "sourceData"; } else if (cond.sourceType === "externalTable") { sourceType = "externalTable"; } else { sourceType = "formData"; } return { sourceType, sourceField: cond.sourceField, targetColumn: cond.targetColumn, // 외부 테이블 조회 설정 externalLookup: cond.externalLookup, // 값 변환 설정 전달 (레거시 호환) transform: cond.transform?.enabled ? { tableName: cond.transform.tableName, matchColumn: cond.transform.matchColumn, resultColumn: cond.transform.resultColumn, } : undefined, }; }); // 외부 테이블에서 값 조회 (_sourceData 전달) const sourceData = row._sourceData || row; const value = await fetchExternalValue( selectedOption.tableName, selectedOption.valueColumn, joinConditions, row, sourceData, formData, ); if (value !== undefined) { newValue = value; } return { ...row, [columnField]: newValue }; }), ); // 계산 필드 업데이트 const calculatedData = calculateAll(updatedData); handleDataChange(calculatedData); return; } // 기존 columnModes 처리 (레거시 호환) if (!column?.columnModes) return; const selectedMode = column.columnModes.find((mode) => mode.id === optionId); if (!selectedMode) return; // 모든 행에 대해 새 값 조회 const updatedData = await Promise.all( tableData.map(async (row) => { const mapping = selectedMode.valueMapping; let newValue: any = row[columnField]; const sourceData = row._sourceData || row; if (mapping.type === "external" && mapping.externalRef) { const { tableName, valueColumn, joinConditions } = mapping.externalRef; const value = await fetchExternalValue(tableName, valueColumn, joinConditions, row, sourceData, formData); if (value !== undefined) { newValue = value; } } else if (mapping.type === "source" && mapping.sourceField) { newValue = row[mapping.sourceField]; } else if (mapping.type === "internal" && mapping.internalField) { newValue = formData[mapping.internalField]; } return { ...row, [columnField]: newValue }; }), ); // 계산 필드 업데이트 const calculatedData = calculateAll(updatedData); handleDataChange(calculatedData); }, [tableConfig.columns, tableData, formData, calculateAll, handleDataChange], ); // 소스 테이블 정보 const { source, filters, uiConfig } = tableConfig; const sourceTable = source.tableName; const sourceColumns = source.displayColumns; const sourceSearchFields = source.searchColumns; const columnLabels = source.columnLabels || {}; const modalTitle = uiConfig?.modalTitle || "항목 검색 및 선택"; 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에서 처리) // 연산자별로 특수 키 형식 사용: column__operator (예: division__in) const baseFilterCondition: Record = useMemo(() => { const condition: Record = {}; if (filters?.preFilters) { for (const filter of filters.preFilters) { if (!filter.column || filter.value === undefined || filter.value === "") continue; const operator = filter.operator || "="; if (operator === "=") { // 기본 등호 연산자는 그대로 전달 condition[filter.column] = filter.value; } else { // 다른 연산자는 특수 키 형식 사용: column__operator condition[`${filter.column}__${operator}`] = filter.value; } } } // console.log("[TableSectionRenderer] baseFilterCondition:", condition, "preFilters:", filters?.preFilters); 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 []; return filters.modalFilters.map((filter) => ({ column: filter.column, label: filter.label || filter.column, // category 타입을 select로 변환 (ModalFilterConfig 호환) type: filter.type === "category" ? ("select" as const) : (filter.type as "text" | "select"), options: filter.options, categoryRef: filter.categoryRef, defaultValue: filter.defaultValue, })); }, [filters?.modalFilters]); // ============================================ // 조건부 테이블 관련 핸들러 // ============================================ // 조건부 테이블: 조건 체크박스 토글 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; } }); }, [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 } : {}), }); } } 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 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 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; } // 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]; } } // 원본 소스 데이터 보존 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) => { setModalCondition(conditionValue); setModalOpen(true); }, []); // 조건부 테이블: 빈 행 추가 (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 handleSearchButtonClick = useCallback( (conditionValue: string) => { openConditionalModal(conditionValue); }, [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) { if (!grouped[conditionValue]) { grouped[conditionValue] = []; } grouped[conditionValue].push(row); conditions.add(conditionValue); } } setConditionalTableData(grouped); setSelectedConditions(Array.from(conditions)); // 첫 번째 조건을 활성 탭으로 설정 if (conditions.size > 0) { setActiveConditionTab(Array.from(conditions)[0]); } initialDataLoadedRef.current = true; } } }, [isConditionalMode, sectionId, formData, conditionalConfig?.conditionColumn]); // 조건부 테이블: 전체 항목 수 계산 const totalConditionalItems = useMemo(() => { return Object.values(conditionalTableData).reduce((sum, data) => sum + data.length, 0); }, [conditionalTableData]); // ============================================ // 조건부 테이블 렌더링 // ============================================ 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() !== ""); // 로딩 중이면 로딩 표시 if (dynamicOptionsLoading) { return (
조건 옵션을 불러오는 중...
); } return (
{/* 조건 선택 UI */} {triggerType === "checkbox" && (
{effectiveOptions.map((option) => ( ))}
{selectedConditions.length > 0 && (
{selectedConditions.length}개 유형 선택됨, 총 {totalConditionalItems}개 항목
)}
)} {triggerType === "dropdown" && (
유형 선택:
)} {/* 선택된 조건들의 테이블 (탭 형태) */} {selectedConditions.length > 0 && ( {selectedConditions.map((conditionValue) => { const option = effectiveOptions.find((o) => o.value === conditionValue); const itemCount = conditionalTableData[conditionValue]?.length || 0; return ( {option?.label || conditionValue} {itemCount > 0 && ( {itemCount} )} ); })} {selectedConditions.map((conditionValue) => { const data = conditionalTableData[conditionValue] || []; const selected = conditionalSelectedRows[conditionValue] || new Set(); return ( {/* 테이블 상단 컨트롤 */}
{data.length > 0 && `${data.length}개 항목`} {selected.size > 0 && ` (${selected.size}개 선택됨)`} {columns.length > 0 && ( )}
{selected.size > 0 && ( )} {showSearchButton && ( )} {showAddRowButton && ( )}
{/* 테이블 */} handleConditionalDataChange(conditionValue, newData)} onRowChange={(index, newRow) => handleConditionalRowChange(conditionValue, index, newRow)} onRowDelete={(index) => handleConditionalRowDelete(conditionValue, index)} activeDataSources={activeDataSources} onDataSourceChange={handleDataSourceChange} selectedRows={selected} onSelectionChange={(newSelected) => { setConditionalSelectedRows((prev) => ({ ...prev, [conditionValue]: newSelected, })); }} equalizeWidthsTrigger={widthTrigger} />
); })}
)} {/* tabs 모드: 모든 옵션을 탭으로 표시 (선택 UI 없음) */} {triggerType === "tabs" && effectiveOptions.length > 0 && ( {effectiveOptions.map((option) => { const itemCount = conditionalTableData[option.value]?.length || 0; return ( {option.label} {itemCount > 0 && ( {itemCount} )} ); })} {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}개 선택됨)`}
{selected.size > 0 && ( )} {showSearchButton && ( )} {showAddRowButton && ( )}
handleConditionalDataChange(option.value, newData)} onRowChange={(index, newRow) => handleConditionalRowChange(option.value, index, newRow)} onRowDelete={(index) => handleConditionalRowDelete(option.value, index)} activeDataSources={activeDataSources} onDataSourceChange={handleDataSourceChange} selectedRows={selected} onSelectionChange={(newSelected) => { setConditionalSelectedRows((prev) => ({ ...prev, [option.value]: newSelected, })); }} equalizeWidthsTrigger={widthTrigger} />
); })}
)} {/* 조건이 선택되지 않은 경우 안내 메시지 (checkbox/dropdown 모드에서만) */} {selectedConditions.length === 0 && triggerType !== "tabs" && (

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

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

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

)} {/* 항목 선택 모달 (조건부 테이블용) */} o.value === modalCondition)?.label || modalCondition} - ${modalTitle}`} alreadySelected={conditionalTableData[modalCondition] || []} uniqueField={tableConfig.saveConfig?.uniqueField} onSelect={handleConditionalAddItems} columnLabels={columnLabels} modalFilters={modalFiltersForModal} categoryColumns={sourceCategoryColumns} />
); } // ============================================ // 일반 테이블 렌더링 (기존 로직) // ============================================ return (
{/* 추가 버튼 영역 */}
{tableData.length > 0 && `${tableData.length}개 항목`} {selectedRows.size > 0 && ` (${selectedRows.size}개 선택됨)`} {columns.length > 0 && ( )}
{selectedRows.size > 0 && ( )} {showSearchButton && ( )} {showAddRowButton && ( )}
{/* Repeater 테이블 */} {/* 항목 선택 모달 */}
); }