diff --git a/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx b/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx index 172e7037..106c1c18 100644 --- a/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx +++ b/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx @@ -31,7 +31,7 @@ interface TableSectionRendererProps { /** * TableColumnConfig를 RepeaterColumnConfig로 변환 - * columnModes가 있으면 dynamicDataSource로 변환 + * columnModes 또는 lookup이 있으면 dynamicDataSource로 변환 */ function convertToRepeaterColumn(col: TableColumnConfig): RepeaterColumnConfig { const baseColumn: RepeaterColumnConfig = { @@ -47,8 +47,43 @@ function convertToRepeaterColumn(col: TableColumnConfig): RepeaterColumnConfig { // valueMapping은 별도로 처리 }; - // columnModes를 dynamicDataSource로 변환 - if (col.columnModes && col.columnModes.length > 0) { + // 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, + // displayLabel이 있으면 그것을 사용, 없으면 원래 label 사용 + 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) => ({ @@ -58,12 +93,10 @@ function convertToRepeaterColumn(col: TableColumnConfig): RepeaterColumnConfig { // 실제 조회 로직은 TableSectionRenderer에서 처리 tableConfig: { tableName: mode.valueMapping?.externalRef?.tableName || "", - valueField: mode.valueMapping?.externalRef?.valueColumn || "", + valueColumn: mode.valueMapping?.externalRef?.valueColumn || "", joinConditions: (mode.valueMapping?.externalRef?.joinConditions || []).map((jc) => ({ - sourceTable: jc.sourceType === "row" ? "target" : "source", sourceField: jc.sourceField, targetField: jc.targetColumn, - operator: jc.operator || "=", })), }, })), @@ -85,18 +118,110 @@ function convertToCalculationRule(calc: { resultField: string; formula: string; }; } +/** + * 값 변환 함수: 중간 테이블을 통해 값을 변환 + * 예: 거래처 이름 "(무)테스트업체" → 거래처 코드 "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 }, 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]: matchValue }, 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 { + console.log("📡 [fetchExternalValue] 시작:", { tableName, valueColumn, joinConditions }); + if (joinConditions.length === 0) { - console.warn("조인 조건이 없습니다."); + console.warn("📡 [fetchExternalValue] 조인 조건이 없습니다."); return undefined; } @@ -106,20 +231,44 @@ async function fetchExternalValue( for (const condition of joinConditions) { let value: any; - // 값 출처에 따라 가져오기 + console.log("📡 [fetchExternalValue] 조건 처리:", { condition, rowData, sourceData, formData }); + + // 값 출처에 따라 가져오기 (4가지 소스 타입 지원) if (condition.sourceType === "row") { - // 현재 행에서 가져오기 + // 현재 행 데이터 (설정된 컬럼 필드) value = rowData[condition.sourceField]; + console.log("📡 [fetchExternalValue] row에서 값 가져옴:", { field: condition.sourceField, value }); + } else if (condition.sourceType === "sourceData") { + // 원본 소스 테이블 데이터 (_sourceData) + value = sourceData?.[condition.sourceField]; + console.log("📡 [fetchExternalValue] sourceData에서 값 가져옴:", { field: condition.sourceField, value }); } else if (condition.sourceType === "formData") { - // formData에서 가져오기 (핵심 기능!) + // formData에서 가져오기 (다른 섹션) value = formData[condition.sourceField]; + console.log("📡 [fetchExternalValue] formData에서 값 가져옴:", { field: condition.sourceField, value }); + } else if (condition.sourceType === "externalTable" && condition.externalLookup) { + // 외부 테이블에서 조회하여 가져오기 + console.log("📡 [fetchExternalValue] externalTable 조회 시작:", condition.externalLookup); + value = await fetchExternalLookupValue(condition.externalLookup, rowData, sourceData, formData); + console.log("📡 [fetchExternalValue] externalTable 조회 결과:", { value }); } - if (value === undefined || value === null) { - console.warn(`조인 조건의 필드 "${condition.sourceField}" 값이 없습니다. (sourceType: ${condition.sourceType})`); + if (value === undefined || value === null || value === "") { + console.warn(`📡 [fetchExternalValue] 조인 조건의 필드 "${condition.sourceField}" 값이 없습니다. (sourceType: ${condition.sourceType})`); return undefined; } + // 값 변환이 필요한 경우 (예: 이름 → 코드) - 레거시 호환 + if (condition.transform) { + console.log("📡 [fetchExternalValue] 값 변환 시작:", { originalValue: value, transform: condition.transform }); + value = await transformValue(value, condition.transform); + console.log("📡 [fetchExternalValue] 값 변환 결과:", { transformedValue: value }); + if (value === undefined) { + console.warn(`📡 [fetchExternalValue] 값 변환 후 결과가 없습니다. 원본 값: "${formData[condition.sourceField]}"`); + return undefined; + } + } + // 숫자형 ID 변환 let convertedValue = value; if (condition.targetColumn.endsWith("_id") || condition.targetColumn === "id") { @@ -129,22 +278,33 @@ async function fetchExternalValue( } } - whereConditions[condition.targetColumn] = convertedValue; + // 정확히 일치하는 검색을 위해 operator: "equals" 사용 + whereConditions[condition.targetColumn] = { + value: convertedValue, + operator: "equals" + }; + console.log("📡 [fetchExternalValue] WHERE 조건 추가:", { targetColumn: condition.targetColumn, value: convertedValue }); } // API 호출 + console.log("📡 [fetchExternalValue] API 호출:", { tableName, whereConditions }); const response = await apiClient.post( `/table-management/tables/${tableName}/data`, { search: whereConditions, size: 1, page: 1 } ); + console.log("📡 [fetchExternalValue] API 응답:", response.data); + if (response.data.success && response.data.data?.data?.length > 0) { - return response.data.data.data[0][valueColumn]; + const result = response.data.data.data[0][valueColumn]; + console.log("📡 [fetchExternalValue] 최종 결과:", { valueColumn, result }); + return result; } + console.warn("📡 [fetchExternalValue] 조회 결과 없음"); return undefined; } catch (error) { - console.error("외부 테이블 조회 오류:", error); + console.error("📡 [fetchExternalValue] 외부 테이블 조회 오류:", error); return undefined; } } @@ -269,6 +429,77 @@ export function TableSectionRenderer({ 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가 명시적으로 설정된 경우 @@ -316,7 +547,8 @@ export function TableSectionRenderer({ tableName, valueColumn, joinConditions, - { ...sourceItem, ...newItem }, // 현재까지 빌드된 아이템 + { ...sourceItem, ...newItem }, // rowData + sourceItem, // sourceData formData ); if (value !== undefined) { @@ -343,12 +575,14 @@ export function TableSectionRenderer({ const newData = [...tableData, ...calculatedItems]; handleDataChange(newData); }, - [tableConfig.columns, formData, tableData, calculateAll, handleDataChange] + [tableConfig.columns, formData, tableData, calculateAll, handleDataChange, activeDataSources] ); - // 컬럼 모드 변경 핸들러 + // 컬럼 모드/조회 옵션 변경 핸들러 const handleDataSourceChange = useCallback( async (columnField: string, optionId: string) => { + console.log("🔍 [handleDataSourceChange] 시작:", { columnField, optionId }); + setActiveDataSources((prev) => ({ ...prev, [columnField]: optionId, @@ -356,6 +590,109 @@ export function TableSectionRenderer({ // 해당 컬럼의 모든 행 데이터 재조회 const column = tableConfig.columns.find((col) => col.field === columnField); + console.log("🔍 [handleDataSourceChange] 컬럼 찾기:", { column: column?.field, hasLookup: column?.lookup?.enabled }); + + // lookup 설정이 있는 경우 (새로운 조회 기능) + if (column?.lookup?.enabled && column.lookup.options) { + const selectedOption = column.lookup.options.find((opt) => opt.id === optionId); + console.log("🔍 [handleDataSourceChange] 선택된 옵션:", { selectedOption, optionId }); + if (!selectedOption) return; + + // sameTable 타입: 현재 행의 소스 데이터에서 값 복사 (외부 조회 필요 없음) + if (selectedOption.type === "sameTable") { + console.log("🔍 [handleDataSourceChange] sameTable 타입 - 소스 데이터에서 복사"); + const updatedData = tableData.map((row) => { + // sourceField에서 값을 가져와 해당 컬럼에 복사 + // row에 _sourceData가 있으면 거기서, 없으면 row 자체에서 가져옴 + const sourceData = row._sourceData || row; + const newValue = sourceData[selectedOption.valueColumn] ?? row[columnField]; + console.log("🔍 [handleDataSourceChange] sameTable 값 복사:", { valueColumn: selectedOption.valueColumn, sourceData, newValue }); + return { ...row, [columnField]: newValue }; + }); + + const calculatedData = calculateAll(updatedData); + handleDataChange(calculatedData); + return; + } + + // 모든 행에 대해 새 값 조회 + console.log("🔍 [handleDataSourceChange] 외부 테이블 조회 시작:", { + type: selectedOption.type, + tableName: selectedOption.tableName, + valueColumn: selectedOption.valueColumn, + conditions: selectedOption.conditions, + tableDataLength: tableData.length, + }); + + const updatedData = await Promise.all( + tableData.map(async (row, rowIndex) => { + 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, + }; + }); + + console.log(`🔍 [handleDataSourceChange] 행 ${rowIndex} 조회:`, { + rowData: row, + sourceData: row._sourceData, + formData, + joinConditions, + }); + + // 외부 테이블에서 값 조회 (_sourceData 전달) + const sourceData = row._sourceData || row; + const value = await fetchExternalValue( + selectedOption.tableName, + selectedOption.valueColumn, + joinConditions, + row, + sourceData, + formData + ); + + console.log(`🔍 [handleDataSourceChange] 행 ${rowIndex} 조회 결과:`, { value }); + + if (value !== undefined) { + newValue = value; + } + + return { ...row, [columnField]: newValue }; + }) + ); + + // 계산 필드 업데이트 + const calculatedData = calculateAll(updatedData); + handleDataChange(calculatedData); + console.log("🔍 [handleDataSourceChange] 완료:", { calculatedData }); + return; + } + + // 기존 columnModes 처리 (레거시 호환) if (!column?.columnModes) return; const selectedMode = column.columnModes.find((mode) => mode.id === optionId); @@ -366,10 +703,11 @@ export function TableSectionRenderer({ 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, formData); + const value = await fetchExternalValue(tableName, valueColumn, joinConditions, row, sourceData, formData); if (value !== undefined) { newValue = value; } @@ -417,10 +755,10 @@ export function TableSectionRenderer({ return filters.modalFilters.map((filter) => ({ column: filter.column, label: filter.label || filter.column, - type: filter.type, + // category 타입을 select로 변환 (ModalFilterConfig 호환) + type: filter.type === "category" ? "select" as const : filter.type as "text" | "select", options: filter.options, categoryRef: filter.categoryRef, - booleanRef: filter.booleanRef, defaultValue: filter.defaultValue, })); }, [filters?.modalFilters]); diff --git a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx index 37c76407..4ef28d6f 100644 --- a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx +++ b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx @@ -689,6 +689,7 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor ]) )} onLoadTableColumns={loadTableColumns} + allSections={config.sections as FormSectionConfig[]} /> )} diff --git a/frontend/lib/registry/components/universal-form-modal/modals/TableColumnSettingsModal.tsx b/frontend/lib/registry/components/universal-form-modal/modals/TableColumnSettingsModal.tsx index 15d8d733..797bce55 100644 --- a/frontend/lib/registry/components/universal-form-modal/modals/TableColumnSettingsModal.tsx +++ b/frontend/lib/registry/components/universal-form-modal/modals/TableColumnSettingsModal.tsx @@ -22,9 +22,14 @@ import { ValueMappingConfig, ColumnModeConfig, TableJoinCondition, + LookupConfig, + LookupOption, + LookupCondition, VALUE_MAPPING_TYPE_OPTIONS, JOIN_SOURCE_TYPE_OPTIONS, TABLE_COLUMN_TYPE_OPTIONS, + LOOKUP_TYPE_OPTIONS, + LOOKUP_CONDITION_SOURCE_OPTIONS, } from "../types"; import { @@ -42,8 +47,10 @@ interface TableColumnSettingsModalProps { open: boolean; onOpenChange: (open: boolean) => void; column: TableColumnConfig; + sourceTableName: string; // 소스 테이블명 sourceTableColumns: { column_name: string; data_type: string; comment?: string }[]; - formFields: { columnName: string; label: string }[]; // formData 필드 목록 + formFields: { columnName: string; label: string; sectionId?: string; sectionTitle?: string }[]; // formData 필드 목록 (섹션 정보 포함) + sections: { id: string; title: string }[]; // 섹션 목록 onSave: (updatedColumn: TableColumnConfig) => void; tables: { table_name: string; comment?: string }[]; tableColumns: Record; @@ -54,8 +61,10 @@ export function TableColumnSettingsModal({ open, onOpenChange, column, + sourceTableName, sourceTableColumns, formFields, + sections, onSave, tables, tableColumns, @@ -67,6 +76,9 @@ export function TableColumnSettingsModal({ // 외부 테이블 검색 상태 const [externalTableOpen, setExternalTableOpen] = useState(false); + // 조회 테이블 검색 상태 (옵션별) + const [lookupTableOpenMap, setLookupTableOpenMap] = useState>({}); + // 활성 탭 const [activeTab, setActiveTab] = useState("basic"); @@ -174,6 +186,101 @@ export function TableColumnSettingsModal({ }); }; + // ============================================ + // 조회(Lookup) 관련 함수들 + // ============================================ + + // 조회 설정 업데이트 + const updateLookup = (updates: Partial) => { + const current = localColumn.lookup || { enabled: false, options: [] }; + updateColumn({ + lookup: { ...current, ...updates }, + }); + }; + + // 조회 옵션 추가 + const addLookupOption = () => { + const newOption: LookupOption = { + id: `lookup_${Date.now()}`, + label: `조회 옵션 ${(localColumn.lookup?.options || []).length + 1}`, + type: "sameTable", + tableName: sourceTableName, // 기본값: 소스 테이블 + valueColumn: "", + conditions: [], + isDefault: (localColumn.lookup?.options || []).length === 0, // 첫 번째 옵션은 기본값 + }; + updateLookup({ + options: [...(localColumn.lookup?.options || []), newOption], + }); + }; + + // 조회 옵션 삭제 + const removeLookupOption = (index: number) => { + const newOptions = (localColumn.lookup?.options || []).filter((_, i) => i !== index); + // 삭제 후 기본 옵션이 없으면 첫 번째를 기본으로 + if (newOptions.length > 0 && !newOptions.some(opt => opt.isDefault)) { + newOptions[0].isDefault = true; + } + updateLookup({ options: newOptions }); + }; + + // 조회 옵션 업데이트 + const updateLookupOption = (index: number, updates: Partial) => { + updateLookup({ + options: (localColumn.lookup?.options || []).map((opt, i) => + i === index ? { ...opt, ...updates } : opt + ), + }); + }; + + // 조회 조건 추가 + const addLookupCondition = (optionIndex: number) => { + const option = localColumn.lookup?.options?.[optionIndex]; + if (!option) return; + + const newCondition: LookupCondition = { + sourceType: "currentRow", + sourceField: "", + targetColumn: "", + }; + updateLookupOption(optionIndex, { + conditions: [...(option.conditions || []), newCondition], + }); + }; + + // 조회 조건 삭제 + const removeLookupCondition = (optionIndex: number, conditionIndex: number) => { + const option = localColumn.lookup?.options?.[optionIndex]; + if (!option) return; + + updateLookupOption(optionIndex, { + conditions: option.conditions.filter((_, i) => i !== conditionIndex), + }); + }; + + // 조회 조건 업데이트 + const updateLookupCondition = (optionIndex: number, conditionIndex: number, updates: Partial) => { + const option = localColumn.lookup?.options?.[optionIndex]; + if (!option) return; + + updateLookupOption(optionIndex, { + conditions: option.conditions.map((c, i) => + i === conditionIndex ? { ...c, ...updates } : c + ), + }); + }; + + // 조회 옵션의 테이블 컬럼 로드 + useEffect(() => { + if (localColumn.lookup?.enabled) { + localColumn.lookup.options?.forEach(option => { + if (option.tableName) { + onLoadTableColumns(option.tableName); + } + }); + } + }, [localColumn.lookup?.enabled, localColumn.lookup?.options, onLoadTableColumns]); + // 저장 함수 const handleSave = () => { onSave(localColumn); @@ -432,8 +539,9 @@ export function TableColumnSettingsModal({
- + 기본 설정 + 조회 설정 값 매핑 컬럼 모드 @@ -595,6 +703,376 @@ export function TableColumnSettingsModal({ )} + {/* 조회 설정 탭 */} + + {/* 조회 여부 토글 */} +
+
+ +

+ 다른 테이블에서 값을 조회하여 가져옵니다. +

+
+ { + if (checked) { + updateLookup({ enabled: true, options: [] }); + } else { + updateColumn({ lookup: undefined }); + } + }} + /> +
+ + {/* 조회 설정 (활성화 시) */} + {localColumn.lookup?.enabled && ( +
+ + +
+
+ +

+ 헤더에서 선택 가능한 조회 방식을 정의합니다. +

+
+ +
+ + {(localColumn.lookup?.options || []).length === 0 ? ( +
+

조회 옵션이 없습니다

+

+ "옵션 추가" 버튼을 클릭하여 조회 방식을 추가하세요. +

+
+ ) : ( +
+ {(localColumn.lookup?.options || []).map((option, optIndex) => ( +
+ {/* 옵션 헤더 */} +
+
+ {option.label || `옵션 ${optIndex + 1}`} + {option.isDefault && ( + 기본 + )} +
+ +
+ + {/* 기본 설정 */} +
+
+ + updateLookupOption(optIndex, { label: e.target.value })} + placeholder="예: 기준단가" + className="h-8 text-xs mt-1" + /> +
+
+ + +
+
+ + {/* 조회 테이블 선택 */} +
+
+ + {option.type === "sameTable" ? ( + + ) : ( + setLookupTableOpenMap(prev => ({ ...prev, [option.id]: open }))} + > + + + + + + + + + 테이블을 찾을 수 없습니다. + + + {tables.map((table) => ( + { + updateLookupOption(optIndex, { tableName: table.table_name }); + onLoadTableColumns(table.table_name); + setLookupTableOpenMap(prev => ({ ...prev, [option.id]: false })); + }} + className="text-xs" + > + + {table.table_name} + + ))} + + + + + + )} +
+
+ + +
+
+ + {/* 기본 옵션 체크박스 */} +
+ { + if (checked) { + // 기본 옵션은 하나만 + updateLookup({ + options: (localColumn.lookup?.options || []).map((opt, i) => ({ + ...opt, + isDefault: i === optIndex, + })), + }); + } else { + updateLookupOption(optIndex, { isDefault: false }); + } + }} + className="scale-75" + /> + 기본 옵션으로 설정 +
+ + + + {/* 조회 조건 */} +
+
+ + +
+ + {(option.conditions || []).length === 0 ? ( +

+ 조회 조건을 추가하세요. +

+ ) : ( +
+ {option.conditions.map((condition, condIndex) => ( +
+ {/* 소스 타입 */} + + + {/* 섹션 선택 (sectionField일 때) */} + {condition.sourceType === "sectionField" && ( + + )} + + {/* 소스 필드 */} + + + = + + {/* 타겟 컬럼 */} + + + +
+ ))} +
+ )} + + {/* 조회 유형별 설명 */} +
+ {option.type === "sameTable" && ( + <> + 동일 테이블 조회: 검색 모달에서 선택한 행의 다른 컬럼 값을 가져옵니다. +
예: 품목 선택 시 → 품목 테이블의 기준단가 + + )} + {option.type === "relatedTable" && ( + <> + 연관 테이블 조회: 현재 행 데이터를 기준으로 다른 테이블에서 값을 조회합니다. +
예: 품목코드로 → 품목별단가 테이블에서 단가 조회 + + )} + {option.type === "combinedLookup" && ( + <> + 복합 조건 조회: 다른 섹션 필드와 현재 행을 조합하여 조회합니다. +
예: 거래처(섹션1) + 품목(현재행) → 거래처별단가 테이블 + + )} +
+
+
+ ))} +
+ )} +
+ )} +
+ {/* 값 매핑 탭 */}
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 0976e1a4..542d4467 100644 --- a/frontend/lib/registry/components/universal-form-modal/modals/TableSectionSettingsModal.tsx +++ b/frontend/lib/registry/components/universal-form-modal/modals/TableSectionSettingsModal.tsx @@ -13,7 +13,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Plus, Trash2, GripVertical, ChevronUp, ChevronDown, Settings, Check, ChevronsUpDown, Filter, Table as TableIcon } from "lucide-react"; +import { Plus, Trash2, GripVertical, ChevronUp, ChevronDown, Settings, Check, ChevronsUpDown, Filter, Table as TableIcon, Search } from "lucide-react"; import { cn } from "@/lib/utils"; // 타입 import @@ -24,9 +24,13 @@ import { TablePreFilter, TableModalFilter, TableCalculationRule, + LookupOption, + ExternalTableLookup, TABLE_COLUMN_TYPE_OPTIONS, FILTER_OPERATOR_OPTIONS, MODAL_FILTER_TYPE_OPTIONS, + LOOKUP_TYPE_OPTIONS, + LOOKUP_CONDITION_SOURCE_OPTIONS, } from "../types"; import { @@ -52,6 +56,13 @@ interface ColumnSettingItemProps { saveTableColumns: { column_name: string; data_type: string; is_nullable: string; comment?: string }[]; displayColumns: string[]; // 검색 설정에서 선택한 표시 컬럼 목록 sourceTableColumns: { column_name: string; data_type: string; is_nullable: string; comment?: string }[]; // 소스 테이블 컬럼 + sourceTableName: string; // 소스 테이블명 + tables: { table_name: string; comment?: string }[]; // 전체 테이블 목록 + tableColumns: Record; // 테이블별 컬럼 + sections: { id: string; title: string }[]; // 섹션 목록 + formFields: { columnName: string; label: string; sectionId?: string }[]; // 폼 필드 목록 + tableConfig: TableSectionConfig; // 현재 행 필드 목록 표시용 + onLoadTableColumns: (tableName: string) => void; onUpdate: (updates: Partial) => void; onMoveUp: () => void; onMoveDown: () => void; @@ -65,6 +76,13 @@ function ColumnSettingItem({ saveTableColumns, displayColumns, sourceTableColumns, + sourceTableName, + tables, + tableColumns, + sections, + formFields, + tableConfig, + onLoadTableColumns, onUpdate, onMoveUp, onMoveDown, @@ -72,6 +90,86 @@ function ColumnSettingItem({ }: ColumnSettingItemProps) { const [fieldSearchOpen, setFieldSearchOpen] = useState(false); const [sourceFieldSearchOpen, setSourceFieldSearchOpen] = useState(false); + const [lookupTableOpenMap, setLookupTableOpenMap] = useState>({}); + + // 조회 옵션 추가 + const addLookupOption = () => { + const newOption: LookupOption = { + id: `lookup_${Date.now()}`, + label: `조회 옵션 ${(col.lookup?.options || []).length + 1}`, + type: "sameTable", + tableName: sourceTableName, + valueColumn: "", + conditions: [], + isDefault: (col.lookup?.options || []).length === 0, + }; + onUpdate({ + lookup: { + enabled: true, + options: [...(col.lookup?.options || []), newOption], + }, + }); + }; + + // 조회 옵션 삭제 + const removeLookupOption = (optIndex: number) => { + const newOptions = (col.lookup?.options || []).filter((_, i) => i !== optIndex); + if (newOptions.length > 0 && !newOptions.some((opt) => opt.isDefault)) { + newOptions[0].isDefault = true; + } + onUpdate({ + lookup: { + enabled: col.lookup?.enabled ?? false, + options: newOptions, + }, + }); + }; + + // 조회 옵션 업데이트 + const updateLookupOption = (optIndex: number, updates: Partial) => { + onUpdate({ + lookup: { + enabled: col.lookup?.enabled ?? false, + options: (col.lookup?.options || []).map((opt, i) => + i === optIndex ? { ...opt, ...updates } : opt + ), + }, + }); + }; + + // 조회 조건 추가 + const addLookupCondition = (optIndex: number) => { + const option = col.lookup?.options?.[optIndex]; + if (!option) return; + const newCondition: LookupCondition = { + sourceType: "currentRow", + sourceField: "", + targetColumn: "", + }; + updateLookupOption(optIndex, { + conditions: [...(option.conditions || []), newCondition], + }); + }; + + // 조회 조건 삭제 + const removeLookupCondition = (optIndex: number, condIndex: number) => { + const option = col.lookup?.options?.[optIndex]; + if (!option) return; + updateLookupOption(optIndex, { + conditions: option.conditions.filter((_, i) => i !== condIndex), + }); + }; + + // 조회 조건 업데이트 + const updateLookupCondition = (optIndex: number, condIndex: number, updates: Partial) => { + const option = col.lookup?.options?.[optIndex]; + if (!option) return; + updateLookupOption(optIndex, { + conditions: option.conditions.map((c, i) => + i === condIndex ? { ...c, ...updates } : c + ), + }); + }; return (
@@ -304,7 +402,695 @@ function ColumnSettingItem({ /> 필수 +
+ + {/* 조회 설정 (조회 ON일 때만 표시) */} + {col.lookup?.enabled && ( +
+
+ + +
+ + {(col.lookup?.options || []).length === 0 ? ( +

+ "옵션 추가" 버튼을 클릭하여 조회 방식을 추가하세요. +

+ ) : ( +
+ {(col.lookup?.options || []).map((option, optIndex) => ( +
+ {/* 옵션 헤더 */} +
+
+ {option.label || `옵션 ${optIndex + 1}`} + {option.isDefault && ( + 기본 + )} +
+ +
+ + {/* 기본 설정 - 첫 번째 줄: 옵션명, 표시 라벨 */} +
+
+ + updateLookupOption(optIndex, { label: e.target.value })} + placeholder="예: 기준단가" + className="h-7 text-xs mt-0.5" + /> +
+
+ + updateLookupOption(optIndex, { displayLabel: e.target.value })} + placeholder={`예: 단가 (${option.label || "옵션명"})`} + className="h-7 text-xs mt-0.5" + /> +

+ 비워두면 옵션명만 표시 +

+
+
+ + {/* 기본 설정 - 두 번째 줄: 조회 유형, 테이블, 가져올 컬럼 */} +
+
+ + +
+
+ + {option.type === "sameTable" ? ( + + ) : ( + setLookupTableOpenMap((prev) => ({ ...prev, [option.id]: open }))} + > + + + + + + + + 없음 + + {tables.map((table) => ( + { + updateLookupOption(optIndex, { tableName: table.table_name }); + onLoadTableColumns(table.table_name); + setLookupTableOpenMap((prev) => ({ ...prev, [option.id]: false })); + }} + className="text-xs" + > + + {table.table_name} + + ))} + + + + + + )} +
+
+ + +
+
+ + {/* 기본 옵션 & 조회 조건 */} +
+ + +
+ + {/* 조회 조건 목록 */} + {(option.conditions || []).length > 0 && ( +
+ {option.conditions.map((cond, condIndex) => ( +
+ {/* 기본 조건 행 */} +
+ + + {/* 다른 섹션 선택 시 - 섹션 드롭다운 */} + {cond.sourceType === "sectionField" && ( + + )} + + {/* 현재 행 / 소스 테이블 / 다른 섹션 - 필드 선택 */} + {cond.sourceType !== "externalTable" && ( +
+ + {cond.sourceField && ( +

+ {cond.sourceType === "currentRow" + ? `rowData.${cond.sourceField}` + : cond.sourceType === "sourceTable" + ? `${sourceTableName}.${cond.sourceField}` + : `formData.${cond.sourceField}` + } +

+ )} +
+ )} + + {/* 현재 행 / 소스 테이블 / 다른 섹션일 때 = 기호와 조회 컬럼 */} + {cond.sourceType !== "externalTable" && ( + <> + = + +
+ + {cond.targetColumn && option.tableName && ( +

+ {option.tableName}.{cond.targetColumn} +

+ )} +
+ + )} + + +
+ + {/* 외부 테이블 조회 설정 */} + {cond.sourceType === "externalTable" && cond.externalLookup && ( +
+

외부 테이블에서 조건 값 조회

+ + {/* 1행: 조회 테이블 선택 */} +
+
+

조회 테이블

+ setLookupTableOpenMap(prev => ({ ...prev, [`ext_${optIndex}_${condIndex}`]: open }))} + > + + + + + + + + 테이블을 찾을 수 없습니다. + + {tables.map((table) => ( + { + onLoadTableColumns(table.table_name); + updateLookupCondition(optIndex, condIndex, { + externalLookup: { ...cond.externalLookup!, tableName: table.table_name, matchColumn: "", resultColumn: "" } + }); + setLookupTableOpenMap(prev => ({ ...prev, [`ext_${optIndex}_${condIndex}`]: false })); + }} + className="text-xs" + > + + {table.table_name} + + ))} + + + + + +
+ +
+

비교 컬럼 (WHERE)

+ +
+ +
+

가져올 값 (SELECT)

+ +
+
+ + {/* 2행: 비교 값 출처 */} +
+

비교 값 출처

+
+ + + {cond.externalLookup.matchSourceType === "sectionField" && ( + + )} + + +
+
+ + {/* 3행: 최종 조회 컬럼 */} +
+ 조회된 값 + = + +
+ + {/* 설명 텍스트 */} + {cond.externalLookup.tableName && cond.externalLookup.matchColumn && cond.externalLookup.resultColumn && ( +

+ {cond.externalLookup.tableName}에서 {cond.externalLookup.matchColumn} = 입력값 인 행의{" "} + {cond.externalLookup.resultColumn} 값을 가져와 {option.tableName}.{cond.targetColumn}와 비교 +

+ )} +
+ )} + + {/* 값 변환 설정 (다른 섹션일 때만 표시) */} + {cond.sourceType === "sectionField" && ( +
+ + + {cond.transform?.enabled && ( +
+
+
+ + + + + + + + + + 없음 + + {tables.map((table) => ( + { + updateLookupCondition(optIndex, condIndex, { + transform: { ...cond.transform!, tableName: table.table_name, matchColumn: "", resultColumn: "" } + }); + onLoadTableColumns(table.table_name); + }} + className="text-xs" + > + + {table.table_name} + + ))} + + + + + +
+
+ + +
+
+ + +
+
+ {cond.transform.tableName && cond.transform.matchColumn && cond.transform.resultColumn && ( +

+ {cond.transform.tableName}에서 {cond.transform.matchColumn} = 입력값 인 행의 {cond.transform.resultColumn} 값으로 변환 +

+ )} +
+ )} +
+ )} +
+ ))} +
+ )} + + {/* 조회 유형 설명 */} +

+ {option.type === "sameTable" && "동일 테이블: 검색 모달에서 선택한 행의 다른 컬럼 값"} + {option.type === "relatedTable" && "연관 테이블: 현재 행 데이터로 다른 테이블 조회"} + {option.type === "combinedLookup" && "복합 조건: 다른 섹션 필드 + 현재 행 조합 조회"} +

+
+ ))} +
+ )} +
+ )}
); } @@ -320,6 +1106,8 @@ interface TableSectionSettingsModalProps { // 카테고리 목록 (table_column_category_values에서 가져옴) categoryList?: { tableName: string; columnName: string; displayName?: string }[]; onLoadCategoryList?: () => void; + // 전체 섹션 목록 (다른 섹션 필드 참조용) + allSections?: FormSectionConfig[]; } export function TableSectionSettingsModal({ @@ -332,6 +1120,7 @@ export function TableSectionSettingsModal({ onLoadTableColumns, categoryList = [], onLoadCategoryList, + allSections = [], }: TableSectionSettingsModalProps) { // 로컬 상태 const [title, setTitle] = useState(section.title); @@ -370,6 +1159,38 @@ export function TableSectionSettingsModal({ } }, [tableConfig.saveConfig?.targetTable, onLoadTableColumns]); + // 조회 설정에 있는 테이블들의 컬럼 로드 (모달 열릴 때) + useEffect(() => { + if (open && tableConfig.columns) { + const tablesToLoad = new Set(); + + // 각 컬럼의 lookup 설정에서 테이블 수집 + tableConfig.columns.forEach((col) => { + if (col.lookup?.enabled && col.lookup.options) { + col.lookup.options.forEach((option) => { + // 조회 테이블 + if (option.tableName) { + tablesToLoad.add(option.tableName); + } + // 변환 테이블 + option.conditions?.forEach((cond) => { + if (cond.transform?.enabled && cond.transform.tableName) { + tablesToLoad.add(cond.transform.tableName); + } + }); + }); + } + }); + + // 수집된 테이블들의 컬럼 로드 + tablesToLoad.forEach((tableName) => { + if (!tableColumns[tableName]) { + onLoadTableColumns(tableName); + } + }); + } + }, [open, tableConfig.columns, tableColumns, onLoadTableColumns]); + // 소스 테이블의 컬럼 목록 const sourceTableColumns = useMemo(() => { return tableColumns[tableConfig.source.tableName] || []; @@ -385,6 +1206,30 @@ export function TableSectionSettingsModal({ return sourceTableColumns; }, [tableColumns, tableConfig.saveConfig?.targetTable, sourceTableColumns]); + // 다른 섹션 목록 (현재 섹션 제외, 테이블 타입이 아닌 섹션만) + const otherSections = useMemo(() => { + return allSections + .filter((s) => s.id !== section.id && s.type !== "table") + .map((s) => ({ id: s.id, title: s.title })); + }, [allSections, section.id]); + + // 다른 섹션의 필드 목록 + const otherSectionFields = useMemo(() => { + const fields: { columnName: string; label: string; sectionId: string }[] = []; + allSections + .filter((s) => s.id !== section.id && s.type !== "table") + .forEach((s) => { + (s.fields || []).forEach((f) => { + fields.push({ + columnName: f.columnName, + label: f.label, + sectionId: s.id, + }); + }); + }); + return fields; + }, [allSections, section.id]); + // 설정 업데이트 함수 const updateTableConfig = (updates: Partial) => { setTableConfig((prev) => ({ ...prev, ...updates })); @@ -831,6 +1676,13 @@ export function TableSectionSettingsModal({ saveTableColumns={saveTableColumns} displayColumns={tableConfig.source.displayColumns || []} sourceTableColumns={sourceTableColumns} + sourceTableName={tableConfig.source.tableName} + tables={tables} + tableColumns={tableColumns} + sections={otherSections} + formFields={otherSectionFields} + tableConfig={tableConfig} + onLoadTableColumns={onLoadTableColumns} onUpdate={(updates) => updateColumn(index, updates)} onMoveUp={() => moveColumn(index, "up")} onMoveDown={() => moveColumn(index, "down")} diff --git a/frontend/lib/registry/components/universal-form-modal/types.ts b/frontend/lib/registry/components/universal-form-modal/types.ts index 899b1ddd..80938d65 100644 --- a/frontend/lib/registry/components/universal-form-modal/types.ts +++ b/frontend/lib/registry/components/universal-form-modal/types.ts @@ -328,6 +328,97 @@ export interface TableColumnConfig { // 컬럼 모드 전환 (동적 데이터 소스) columnModes?: ColumnModeConfig[]; + + // 조회 설정 (동적 값 조회) + lookup?: LookupConfig; +} + +// ============================================ +// 조회(Lookup) 설정 관련 타입 정의 +// ============================================ + +/** + * 조회 유형 + * - sameTable: 동일 테이블 조회 (소스 테이블에서 다른 컬럼 값) + * - relatedTable: 연관 테이블 조회 (현재 행 기준으로 다른 테이블에서) + * - combinedLookup: 복합 조건 조회 (다른 섹션 필드 + 현재 행 조합) + */ +export type LookupType = "sameTable" | "relatedTable" | "combinedLookup"; + +/** + * 값 변환 설정 + * 예: 거래처 이름 → 거래처 코드로 변환 + */ +export interface LookupTransform { + enabled: boolean; // 변환 사용 여부 + tableName: string; // 변환 테이블 (예: customer_mng) + matchColumn: string; // 찾을 컬럼 (예: customer_name) + resultColumn: string; // 가져올 컬럼 (예: customer_code) +} + +/** + * 외부 테이블 조회 설정 + * 다른 테이블에서 조건 값을 조회하여 사용 (이름→코드 변환 등) + */ +export interface ExternalTableLookup { + tableName: string; // 조회할 테이블 + matchColumn: string; // 조회 조건 컬럼 (WHERE 절에서 비교할 컬럼) + matchSourceType: "currentRow" | "sourceTable" | "sectionField"; // 비교값 출처 + matchSourceField: string; // 비교값 필드명 + matchSectionId?: string; // sectionField인 경우 섹션 ID + resultColumn: string; // 가져올 컬럼 (SELECT 절) +} + +/** + * 조회 조건 설정 + * + * sourceType 설명: + * - "currentRow": 테이블에 설정된 컬럼 필드값 (rowData에서 가져옴, 예: part_code, quantity) + * - "sourceTable": 원본 소스 테이블의 컬럼값 (_sourceData에서 가져옴, 예: item_number, company_code) + * - "sectionField": 폼의 다른 섹션 필드값 (formData에서 가져옴, 예: partner_id) + * - "externalTable": 외부 테이블에서 조회한 값 (다른 테이블에서 값을 조회해서 조건으로 사용) + */ +export interface LookupCondition { + sourceType: "currentRow" | "sourceTable" | "sectionField" | "externalTable"; // 값 출처 + sourceField: string; // 출처의 필드명 (참조할 필드) + sectionId?: string; // sectionField인 경우 섹션 ID + targetColumn: string; // 조회 테이블의 컬럼 + + // 외부 테이블 조회 설정 (sourceType이 "externalTable"인 경우) + externalLookup?: ExternalTableLookup; + + // 값 변환 설정 (선택) - 이름→코드 등 변환이 필요할 때 (레거시 호환) + transform?: LookupTransform; +} + +/** + * 조회 옵션 설정 + * 하나의 컬럼에 여러 조회 방식을 정의하고 헤더에서 선택 가능 + */ +export interface LookupOption { + id: string; // 옵션 고유 ID + label: string; // 옵션 라벨 (예: "기준단가", "거래처별 단가") + displayLabel?: string; // 헤더 드롭다운에 표시될 텍스트 (예: "기준단가" → "단가 (기준단가)") + type: LookupType; // 조회 유형 + + // 조회 테이블 설정 + tableName: string; // 조회할 테이블 + valueColumn: string; // 가져올 컬럼 + + // 조회 조건 (여러 조건 AND로 결합) + conditions: LookupCondition[]; + + // 기본 옵션 여부 + isDefault?: boolean; +} + +/** + * 컬럼 조회 설정 + */ +export interface LookupConfig { + enabled: boolean; // 조회 사용 여부 + options: LookupOption[]; // 조회 옵션 목록 + defaultOptionId?: string; // 기본 선택 옵션 ID } /** @@ -354,12 +445,28 @@ export interface ValueMappingConfig { /** * 테이블 조인 조건 * 외부 테이블 조회 시 사용하는 조인 조건 + * + * sourceType 설명: + * - "row": 현재 행의 설정된 컬럼 (rowData) + * - "sourceData": 원본 소스 테이블 데이터 (_sourceData) + * - "formData": 폼의 다른 섹션 필드 (formData) + * - "externalTable": 외부 테이블에서 조회한 값 */ export interface TableJoinCondition { - sourceType: "row" | "formData"; // 값 출처 (현재 행 또는 폼 데이터) + sourceType: "row" | "sourceData" | "formData" | "externalTable"; // 값 출처 sourceField: string; // 출처의 필드명 targetColumn: string; // 조회 테이블의 컬럼 operator?: "=" | "!=" | ">" | "<" | ">=" | "<="; // 연산자 (기본: "=") + + // 외부 테이블 조회 설정 (sourceType이 "externalTable"인 경우) + externalLookup?: ExternalTableLookup; + + // 값 변환 설정 (선택) - 이름→코드 등 중간 변환이 필요할 때 (레거시 호환) + transform?: { + tableName: string; // 변환 테이블 (예: customer_mng) + matchColumn: string; // 찾을 컬럼 (예: customer_name) + resultColumn: string; // 가져올 컬럼 (예: customer_code) + }; } /** @@ -665,3 +772,18 @@ export const MODAL_FILTER_TYPE_OPTIONS = [ { value: "category", label: "테이블 조회" }, { value: "text", label: "텍스트 입력" }, ] as const; + +// 조회 유형 옵션 +export const LOOKUP_TYPE_OPTIONS = [ + { value: "sameTable", label: "동일 테이블 조회" }, + { value: "relatedTable", label: "연관 테이블 조회" }, + { value: "combinedLookup", label: "복합 조건 조회" }, +] as const; + +// 조회 조건 소스 타입 옵션 +export const LOOKUP_CONDITION_SOURCE_OPTIONS = [ + { value: "currentRow", label: "현재 행" }, + { value: "sourceTable", label: "소스 테이블" }, + { value: "sectionField", label: "다른 섹션" }, + { value: "externalTable", label: "외부 테이블" }, +] as const;