"use client"; import React, { useState, useEffect, useMemo, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Separator } from "@/components/ui/separator"; import { Badge } from "@/components/ui/badge"; 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, Search } from "lucide-react"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; // 타입 import import { FormSectionConfig, TableSectionConfig, TableColumnConfig, TablePreFilter, TableModalFilter, TableCalculationRule, ConditionalCalculationRule, ConditionalCalculationConfig, LookupOption, LookupCondition, ConditionalTableOption, TABLE_COLUMN_TYPE_OPTIONS, FILTER_OPERATOR_OPTIONS, MODAL_FILTER_TYPE_OPTIONS, LOOKUP_TYPE_OPTIONS, LOOKUP_CONDITION_SOURCE_OPTIONS, CONDITIONAL_TABLE_TRIGGER_OPTIONS, } from "../types"; import { defaultTableSectionConfig, defaultTableColumnConfig, defaultPreFilterConfig, defaultModalFilterConfig, defaultCalculationRuleConfig, defaultConditionalTableConfig, generateTableColumnId, generateFilterId, generateConditionalOptionId, } from "../config"; // 도움말 텍스트 컴포넌트 const HelpText = ({ children }: { children: React.ReactNode }) => (

{children}

); // 계산 규칙 편집 컴포넌트 (조건부 계산 지원) interface CalculationRuleEditorProps { calc: TableCalculationRule; index: number; columns: TableColumnConfig[]; sourceTableName?: string; // 소스 테이블명 추가 onUpdate: (updates: Partial) => void; onRemove: () => void; } const CalculationRuleEditor: React.FC = ({ calc, index, columns, sourceTableName, onUpdate, onRemove, }) => { const [categoryOptions, setCategoryOptions] = useState<{ value: string; label: string }[]>([]); const [loadingOptions, setLoadingOptions] = useState(false); const [categoryColumns, setCategoryColumns] = useState>({}); // 조건부 계산 활성화 여부 const isConditionalEnabled = calc.conditionalCalculation?.enabled ?? false; // 소스 테이블의 카테고리 컬럼 정보 로드 useEffect(() => { const loadCategoryColumns = async () => { if (!sourceTableName) { setCategoryColumns({}); return; } try { const { getCategoryColumns } = await import("@/lib/api/tableCategoryValue"); const result = await getCategoryColumns(sourceTableName); if (result && result.success && Array.isArray(result.data)) { const categoryMap: Record = {}; result.data.forEach((col: any) => { // API 응답은 camelCase (columnName) const colName = col.columnName || col.column_name; if (colName) { categoryMap[colName] = true; } }); setCategoryColumns(categoryMap); } } catch (error) { console.error("카테고리 컬럼 조회 실패:", error); } }; loadCategoryColumns(); }, [sourceTableName]); // 조건 필드가 선택되었을 때 옵션 로드 (테이블 타입 관리의 카테고리 기준) useEffect(() => { const loadConditionOptions = async () => { if (!isConditionalEnabled || !calc.conditionalCalculation?.conditionField) { setCategoryOptions([]); return; } const conditionField = calc.conditionalCalculation.conditionField; // 소스 필드(sourceField)가 있으면 해당 필드명 사용, 없으면 field명 사용 const selectedColumn = columns.find((col) => col.field === conditionField); const actualFieldName = selectedColumn?.sourceField || conditionField; // 소스 테이블에서 해당 컬럼이 카테고리 타입인지 확인 if (sourceTableName && categoryColumns[actualFieldName]) { try { setLoadingOptions(true); const { getCategoryValues } = await import("@/lib/api/tableCategoryValue"); const result = await getCategoryValues(sourceTableName, actualFieldName, false); if (result && result.success && Array.isArray(result.data)) { const options = result.data.map((item: any) => ({ // API 응답은 camelCase (valueCode, valueLabel) value: item.valueCode || item.value_code || item.value, label: item.valueLabel || item.displayLabel || item.display_label || item.label || item.valueCode || item.value_code || item.value, })); setCategoryOptions(options); } else { setCategoryOptions([]); } } catch (error) { console.error("카테고리 값 로드 실패:", error); setCategoryOptions([]); } finally { setLoadingOptions(false); } return; } // 카테고리 키가 직접 설정된 경우 (저장된 값) const categoryKey = calc.conditionalCalculation?.conditionFieldCategoryKey; if (categoryKey) { try { setLoadingOptions(true); const [tableName, columnName] = categoryKey.split("."); if (tableName && columnName) { const { getCategoryValues } = await import("@/lib/api/tableCategoryValue"); const result = await getCategoryValues(tableName, columnName, false); if (result && result.success && Array.isArray(result.data)) { setCategoryOptions( result.data.map((item: any) => ({ // API 응답은 camelCase (valueCode, valueLabel) value: item.valueCode || item.value_code || item.value, label: item.valueLabel || item.displayLabel || item.display_label || item.label || item.valueCode || item.value_code || item.value, })) ); } } } catch (error) { console.error("카테고리 옵션 로드 실패:", error); } finally { setLoadingOptions(false); } return; } // 그 외 타입은 옵션 없음 (직접 입력) setCategoryOptions([]); }; loadConditionOptions(); }, [isConditionalEnabled, calc.conditionalCalculation?.conditionField, calc.conditionalCalculation?.conditionFieldCategoryKey, columns, sourceTableName, categoryColumns]); // 조건부 계산 토글 const toggleConditionalCalculation = (enabled: boolean) => { onUpdate({ conditionalCalculation: enabled ? { enabled: true, conditionField: "", conditionFieldType: "static", rules: [], defaultFormula: calc.formula || "", } : undefined, }); }; // 조건 필드 변경 const updateConditionField = (field: string) => { const selectedColumn = columns.find((col) => col.field === field); const actualFieldName = selectedColumn?.sourceField || field; // 컬럼의 타입과 옵션 확인 (테이블 타입 관리의 카테고리 기준) let conditionFieldType: "static" | "code" | "table" = "static"; let conditionFieldCategoryKey = ""; // 소스 테이블에서 해당 컬럼이 카테고리 타입인지 확인 if (sourceTableName && categoryColumns[actualFieldName]) { conditionFieldType = "code"; conditionFieldCategoryKey = `${sourceTableName}.${actualFieldName}`; } onUpdate({ conditionalCalculation: { ...calc.conditionalCalculation!, conditionField: field, conditionFieldType, conditionFieldCategoryKey, rules: [], // 필드 변경 시 규칙 초기화 }, }); }; // 조건 규칙 추가 const addConditionRule = () => { const newRule: ConditionalCalculationRule = { conditionValue: "", formula: calc.formula || "", }; onUpdate({ conditionalCalculation: { ...calc.conditionalCalculation!, rules: [...(calc.conditionalCalculation?.rules || []), newRule], }, }); }; // 조건 규칙 업데이트 const updateConditionRule = (ruleIndex: number, updates: Partial) => { const newRules = [...(calc.conditionalCalculation?.rules || [])]; newRules[ruleIndex] = { ...newRules[ruleIndex], ...updates }; onUpdate({ conditionalCalculation: { ...calc.conditionalCalculation!, rules: newRules, }, }); }; // 조건 규칙 삭제 const removeConditionRule = (ruleIndex: number) => { onUpdate({ conditionalCalculation: { ...calc.conditionalCalculation!, rules: (calc.conditionalCalculation?.rules || []).filter((_, i) => i !== ruleIndex), }, }); }; // 기본 계산식 업데이트 const updateDefaultFormula = (formula: string) => { onUpdate({ conditionalCalculation: { ...calc.conditionalCalculation!, defaultFormula: formula, }, }); }; // 조건 필드로 사용 가능한 컬럼 (모든 컬럼) const availableColumns = columns.filter((col) => col.field); return (
{/* 기본 계산 규칙 */}
= onUpdate({ formula: e.target.value })} placeholder="수식 (예: qty * unit_price)" className="h-8 text-xs flex-1" disabled={isConditionalEnabled} />
{/* 조건부 계산 토글 */}
{availableColumns.length === 0 && !isConditionalEnabled && ( (컬럼 설정에서 먼저 컬럼을 추가하세요) )}
{/* 조건부 계산 설정 */} {isConditionalEnabled && (
{/* 조건 필드 선택 */}
{/* 조건별 계산식 목록 */} {calc.conditionalCalculation?.conditionField && (
{(calc.conditionalCalculation?.rules || []).map((rule, ruleIndex) => (
{/* 조건값 선택 */} {categoryOptions.length > 0 ? ( ) : ( updateConditionRule(ruleIndex, { conditionValue: e.target.value }) } placeholder="조건값" className="h-7 text-xs w-[120px]" /> )} updateConditionRule(ruleIndex, { formula: e.target.value }) } placeholder="계산식" className="h-7 text-xs flex-1" />
))} {/* 기본 계산식 */}
(기본값) updateDefaultFormula(e.target.value)} placeholder="기본 계산식 (조건 미해당 시)" className="h-7 text-xs flex-1" />
)} {loadingOptions && (

옵션 로딩 중...

)}
)}
); }; // 옵션 소스 설정 컴포넌트 (검색 가능한 Combobox) interface OptionSourceConfigProps { optionSource: { enabled: boolean; tableName: string; valueColumn: string; labelColumn: string; filterCondition?: string; }; tables: { table_name: string; comment?: string }[]; tableColumns: Record; onUpdate: (updates: Partial) => void; } const OptionSourceConfig: React.FC = ({ optionSource, tables, tableColumns, onUpdate, }) => { const [tableOpen, setTableOpen] = useState(false); const [valueColumnOpen, setValueColumnOpen] = useState(false); // 선택된 테이블의 컬럼 목록 const selectedTableColumns = useMemo(() => { return tableColumns[optionSource.tableName] || []; }, [tableColumns, optionSource.tableName]); return (
{/* 테이블 선택 Combobox */}
테이블을 찾을 수 없습니다. {tables.map((table) => ( { onUpdate({ tableName: table.table_name, valueColumn: "", // 테이블 변경 시 컬럼 초기화 labelColumn: "", }); setTableOpen(false); }} className="text-xs" >
{table.table_name} {table.comment && ( {table.comment} )}
))}
{/* 참조할 값 컬럼 선택 Combobox */}
컬럼을 찾을 수 없습니다. {selectedTableColumns.map((column) => ( { onUpdate({ valueColumn: column.column_name }); setValueColumnOpen(false); }} className="text-xs" >
{column.column_name} {column.comment && ( {column.comment} )}
))}
{/* 출력할 값 컬럼 선택 Combobox */}
컬럼을 찾을 수 없습니다. {/* 값 컬럼 사용 옵션 */} onUpdate({ labelColumn: "" })} className="text-xs text-muted-foreground" > (참조할 값과 동일) {selectedTableColumns.map((column) => ( onUpdate({ labelColumn: column.column_name })} className="text-xs" >
{column.column_name} {column.comment && ( {column.comment} )}
))}

비워두면 참조할 값을 그대로 표시

); }; // 부모 화면에서 전달 가능한 필드 타입 interface AvailableParentField { name: string; // 필드명 (columnName) label: string; // 표시 라벨 sourceComponent?: string; // 출처 컴포넌트 sourceTable?: string; // 출처 테이블명 } // 컬럼 설정 아이템 컴포넌트 interface ColumnSettingItemProps { col: TableColumnConfig; index: number; totalCount: number; saveTableColumns: { column_name: string; data_type: string; is_nullable: string; comment?: string; input_type?: string }[]; displayColumns: string[]; // 검색 설정에서 선택한 표시 컬럼 목록 sourceTableColumns: { column_name: string; data_type: string; is_nullable: string; comment?: string; input_type?: string }[]; // 소스 테이블 컬럼 sourceTableName: string; // 소스 테이블명 externalTableColumns: { column_name: string; data_type: string; is_nullable: string; comment?: string; input_type?: string }[]; // 외부 데이터 테이블 컬럼 externalTableName?: string; // 외부 데이터 테이블명 externalDataEnabled?: boolean; // 외부 데이터 소스 활성화 여부 tables: { table_name: string; comment?: string }[]; // 전체 테이블 목록 tableColumns: Record; // 테이블별 컬럼 sections: { id: string; title: string }[]; // 섹션 목록 formFields: { columnName: string; label: string; sectionId?: string }[]; // 폼 필드 목록 tableConfig: TableSectionConfig; // 현재 행 필드 목록 표시용 availableParentFields?: AvailableParentField[]; // 부모 화면에서 전달 가능한 필드 목록 onLoadTableColumns: (tableName: string) => void; onUpdate: (updates: Partial) => void; onMoveUp: () => void; onMoveDown: () => void; onRemove: () => void; } function ColumnSettingItem({ col, index, totalCount, saveTableColumns, displayColumns, sourceTableColumns, sourceTableName, externalTableColumns, externalTableName, externalDataEnabled, tables, tableColumns, sections, formFields, tableConfig, availableParentFields = [], onLoadTableColumns, onUpdate, onMoveUp, onMoveDown, onRemove, }: ColumnSettingItemProps) { const [fieldSearchOpen, setFieldSearchOpen] = useState(false); const [sourceFieldSearchOpen, setSourceFieldSearchOpen] = useState(false); const [externalFieldSearchOpen, setExternalFieldSearchOpen] = useState(false); const [parentFieldSearchOpen, setParentFieldSearchOpen] = useState(false); const [lookupTableOpenMap, setLookupTableOpenMap] = useState>({}); // 소스 필드 기준으로 카테고리 타입인지 확인 const actualSourceField = col.sourceField || col.field; const sourceColumnInfo = sourceTableColumns.find((c) => c.column_name === actualSourceField); const isCategoryColumn = sourceColumnInfo?.input_type === "category"; // 카테고리 컬럼인 경우 타입을 자동으로 category로 설정 useEffect(() => { if (isCategoryColumn && col.type !== "category") { onUpdate({ type: "category" }); } }, [isCategoryColumn, col.type, onUpdate]); // 조회 옵션 추가 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 (
{col.label || col.field || `컬럼 ${index + 1}`} {TABLE_COLUMN_TYPE_OPTIONS.find((t) => t.value === col.type)?.label || col.type} {col.calculated && 계산}
{/* 필드명 - Combobox (저장할 컬럼) */}
필드를 찾을 수 없습니다. {/* 선택 안 함 옵션 */} { onUpdate({ field: "" }); setFieldSearchOpen(false); }} className="text-xs text-muted-foreground" > (선택 안 함 - 저장하지 않음) {/* 실제 컬럼 목록 */} {saveTableColumns.map((column) => ( { onUpdate({ field: column.column_name, // 라벨이 비어있으면 comment로 자동 설정 ...((!col.label || col.label.startsWith("컬럼 ")) && column.comment ? { label: column.comment } : {}) }); setFieldSearchOpen(false); }} className="text-xs" >
{column.column_name} {column.comment || column.data_type}
))}
{/* 소스 필드 - Combobox (검색 모달에서 가져올 컬럼) */}
소스 필드를 찾을 수 없습니다. {/* 필드명과 동일 옵션 */} { onUpdate({ sourceField: undefined }); setSourceFieldSearchOpen(false); }} className="text-xs" > (필드명과 동일) {/* 표시 컬럼 목록 */} {displayColumns.map((colName) => { const colInfo = sourceTableColumns.find((c) => c.column_name === colName); return ( { onUpdate({ sourceField: colName }); setSourceFieldSearchOpen(false); }} className="text-xs" >
{colName} {colInfo?.comment && ( {colInfo.comment} )}
); })}
{/* 외부 필드 - Combobox (외부 데이터에서 가져올 컬럼) */} {externalDataEnabled && externalTableName && (
외부 필드를 찾을 수 없습니다. {/* 필드명과 동일 옵션 */} { onUpdate({ externalField: undefined }); setExternalFieldSearchOpen(false); }} className="text-xs" > (필드명과 동일) {/* 외부 테이블 컬럼 목록 */} {externalTableColumns.map((extCol) => ( { onUpdate({ externalField: extCol.column_name }); setExternalFieldSearchOpen(false); }} className="text-xs" >
{extCol.column_name} {extCol.comment && ( {extCol.comment} )}
))}

외부 데이터({externalTableName})에서 이 컬럼에 매핑할 필드

)} {/* 라벨 */}
onUpdate({ label: e.target.value })} placeholder="표시 라벨" className="h-8 text-xs mt-1" />
{/* 타입 */}
{isCategoryColumn && (

테이블 타입 관리에서 카테고리로 설정됨

)}
{/* 너비 */}
onUpdate({ width: e.target.value })} placeholder="150px" className="h-8 text-xs mt-1" />
{/* 옵션 스위치 */}
{/* 날짜 타입일 때만 일괄 적용 옵션 표시 */} {col.type === "date" && ( )}
{/* 부모에서 값 받기 설정 (부모값 ON일 때만 표시) */} {col.receiveFromParent && (

부모 화면에서 전달받을 필드를 선택하세요. 모든 행에 동일한 값이 적용됩니다.

{availableParentFields.length > 0 ? ( 사용 가능한 부모 필드가 없습니다. {/* 기본값 (필드명과 동일) */} { onUpdate({ parentFieldName: undefined }); setParentFieldSearchOpen(false); }} className="text-xs" > (기본: {col.field}) {/* 부모 필드 목록 */} {availableParentFields.map((pf) => ( { onUpdate({ parentFieldName: pf.name }); setParentFieldSearchOpen(false); }} className="text-xs" >
{pf.label || pf.name} {pf.sourceComponent && ( {pf.sourceComponent}{pf.sourceTable && ` (${pf.sourceTable})`} )}
))}
) : (
onUpdate({ parentFieldName: e.target.value })} placeholder={col.field} className="h-8 text-xs" />

비워두면 "{col.field}"를 사용합니다.

)}
)} {/* 조회 설정 (조회 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} ))}

찾을 컬럼

가져올 컬럼

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

비교 값 출처 (찾을 때 사용할 값)

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

{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" && "복합 조건: 다른 섹션 필드 + 현재 행 조합 조회"}

))}
)}
)} {/* 동적 Select 옵션 (소스 테이블 필터링이 활성화되고, 타입이 select일 때만 표시) */} {col.type === "select" && tableConfig.conditionalTable?.sourceFilter?.enabled && (

소스 테이블에서 옵션을 동적으로 로드합니다. 조건부 테이블 필터가 자동 적용됩니다.

{ onUpdate({ dynamicSelectOptions: checked ? { enabled: true, sourceField: "", distinct: true, } : undefined, }); }} className="scale-75" />
{col.dynamicSelectOptions?.enabled && (
{/* 소스 컬럼 선택 */}

드롭다운 옵션으로 사용할 컬럼

{sourceTableColumns.length > 0 ? ( ) : ( { onUpdate({ dynamicSelectOptions: { ...col.dynamicSelectOptions!, sourceField: e.target.value, }, }); }} placeholder="inspection_item" className="h-7 text-xs" /> )}

표시할 라벨 (비워두면 소스 컬럼과 동일)

{sourceTableColumns.length > 0 ? ( ) : ( { onUpdate({ dynamicSelectOptions: { ...col.dynamicSelectOptions!, labelField: e.target.value, }, }); }} placeholder="(비워두면 소스 컬럼과 동일)" className="h-7 text-xs" /> )}
{/* 행 선택 모드 */}

이 컬럼 선택 시 같은 소스 행의 다른 컬럼 값을 자동으로 채웁니다.

{col.dynamicSelectOptions.rowSelectionMode?.enabled && (
{(col.dynamicSelectOptions.rowSelectionMode?.autoFillColumns || []).length === 0 ? (

"매핑 추가" 버튼을 클릭하여 자동 채움 매핑을 추가하세요.

) : (
{(col.dynamicSelectOptions.rowSelectionMode?.autoFillColumns || []).map((mapping, mappingIndex) => (
{/* 소스 컬럼 */}
{sourceTableColumns.length > 0 ? ( ) : ( { const newMappings = [...(col.dynamicSelectOptions?.rowSelectionMode?.autoFillColumns || [])]; newMappings[mappingIndex] = { ...newMappings[mappingIndex], sourceColumn: e.target.value }; onUpdate({ dynamicSelectOptions: { ...col.dynamicSelectOptions!, rowSelectionMode: { ...col.dynamicSelectOptions!.rowSelectionMode!, autoFillColumns: newMappings, }, }, }); }} placeholder="소스 컬럼" className="h-6 text-[10px]" /> )}
{/* 타겟 필드 */}
{/* 삭제 버튼 */}
))}
)} {/* 매핑 설명 */} {(col.dynamicSelectOptions.rowSelectionMode?.autoFillColumns || []).length > 0 && (
{col.label || col.field} 선택 시:{" "} {(col.dynamicSelectOptions.rowSelectionMode?.autoFillColumns || []) .filter((m) => m.sourceColumn && m.targetField) .map((m) => { const targetCol = tableConfig.columns?.find((c) => c.field === m.targetField); return `${m.sourceColumn} → ${targetCol?.label || m.targetField}`; }) .join(", ")}
)}
)}
{/* 설정 요약 */} {col.dynamicSelectOptions.sourceField && (
{sourceTableName}.{col.dynamicSelectOptions.sourceField} {tableConfig.conditionalTable?.sourceFilter?.filterColumn && ( <> (조건: {tableConfig.conditionalTable.sourceFilter.filterColumn} = 선택된 검사유형) )}
)}
)}
)} {/* ============================================ */} {/* 저장 설정 섹션 */} {/* ============================================ */}

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

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

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

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

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

{/* 설정 요약 */} {col.saveConfig?.referenceDisplay?.referenceIdField && col.saveConfig?.referenceDisplay?.sourceColumn && (
설정 요약:
- 이 컬럼({col.label || col.field})은 저장되지 않습니다.
- 수정 화면에서 {col.saveConfig.referenceDisplay.referenceIdField}로{" "} {sourceTableName} 테이블을 조회하여{" "} {col.saveConfig.referenceDisplay.sourceColumn} 값을 표시합니다.
)}
)}
); } interface TableSectionSettingsModalProps { open: boolean; onOpenChange: (open: boolean) => void; section: FormSectionConfig; onSave: (updates: Partial) => void; tables: { table_name: string; comment?: string }[]; tableColumns: Record; onLoadTableColumns: (tableName: string) => void; // 카테고리 목록 (table_column_category_values에서 가져옴) categoryList?: { tableName: string; columnName: string; displayName?: string }[]; onLoadCategoryList?: () => void; // 전체 섹션 목록 (다른 섹션 필드 참조용) allSections?: FormSectionConfig[]; // 부모 화면에서 전달 가능한 필드 목록 availableParentFields?: AvailableParentField[]; } export function TableSectionSettingsModal({ open, onOpenChange, section, onSave, tables, tableColumns, onLoadTableColumns, categoryList = [], onLoadCategoryList, allSections = [], availableParentFields = [], }: TableSectionSettingsModalProps) { // 로컬 상태 const [title, setTitle] = useState(section.title); const [description, setDescription] = useState(section.description || ""); const [tableConfig, setTableConfig] = useState( section.tableConfig || { ...defaultTableSectionConfig } ); // 테이블 검색 Combobox 상태 const [tableSearchOpen, setTableSearchOpen] = useState(false); const [saveTableSearchOpen, setSaveTableSearchOpen] = useState(false); const [externalTableSearchOpen, setExternalTableSearchOpen] = useState(false); // 활성 탭 const [activeTab, setActiveTab] = useState("source"); // 사전 필터 카테고리 옵션 캐시 (컬럼명 -> 옵션 배열) const [preFilterCategoryOptions, setPreFilterCategoryOptions] = useState< Record >({}); // open이 변경될 때마다 데이터 동기화 useEffect(() => { if (open) { setTitle(section.title); setDescription(section.description || ""); setTableConfig(section.tableConfig || { ...defaultTableSectionConfig }); } }, [open, section]); // 소스 테이블 변경 시 컬럼 로드 useEffect(() => { if (tableConfig.source.tableName) { onLoadTableColumns(tableConfig.source.tableName); } }, [tableConfig.source.tableName, onLoadTableColumns]); // 저장 테이블 변경 시 컬럼 로드 useEffect(() => { if (tableConfig.saveConfig?.targetTable) { onLoadTableColumns(tableConfig.saveConfig.targetTable); } }, [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] || []; }, [tableColumns, tableConfig.source.tableName]); // 카테고리 옵션 로드 함수 const loadCategoryOptions = useCallback(async (columnName: string) => { if (!tableConfig.source.tableName || !columnName) return; // 이미 로드된 경우 스킵 if (preFilterCategoryOptions[columnName]) return; try { const response = await apiClient.get( `/table-categories/${tableConfig.source.tableName}/${columnName}/values` ); if (response.data?.success && response.data?.data) { const options = response.data.data.map((item: any) => ({ // value는 DB에 저장된 실제 값(valueCode)을 사용해야 필터링이 정상 작동 value: item.valueCode || item.value_code || item.valueLabel || item.value_label || "", // label은 사용자에게 보여질 라벨 label: item.valueLabel || item.value_label || item.valueCode || item.value_code || "", })); setPreFilterCategoryOptions((prev) => ({ ...prev, [columnName]: options, })); } } catch (error) { console.error(`카테고리 옵션 로드 실패 (${columnName}):`, error); } }, [tableConfig.source.tableName, preFilterCategoryOptions]); // 사전 필터에서 선택된 카테고리 컬럼들의 옵션 자동 로드 useEffect(() => { const preFilters = tableConfig.filters?.preFilters || []; preFilters.forEach((filter) => { if (filter.column) { const col = sourceTableColumns.find((c) => c.column_name === filter.column); if (col && col.input_type === "category") { loadCategoryOptions(filter.column); } } }); }, [tableConfig.filters?.preFilters, sourceTableColumns, loadCategoryOptions]); // 저장 테이블의 컬럼 목록 const saveTableColumns = useMemo(() => { // 저장 테이블이 지정되어 있으면 해당 테이블의 컬럼, 아니면 소스 테이블의 컬럼 사용 const targetTable = tableConfig.saveConfig?.targetTable; if (targetTable) { return tableColumns[targetTable] || []; } 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 })); }; const updateSource = (updates: Partial) => { updateTableConfig({ source: { ...tableConfig.source, ...updates }, }); }; const updateFilters = (updates: Partial) => { updateTableConfig({ filters: { ...tableConfig.filters, ...updates }, }); }; const updateUiConfig = (updates: Partial>) => { updateTableConfig({ uiConfig: { ...tableConfig.uiConfig, ...updates }, }); }; const updateSaveConfig = (updates: Partial>) => { updateTableConfig({ saveConfig: { ...tableConfig.saveConfig, ...updates }, }); }; const updateExternalDataSource = (updates: Partial>) => { updateTableConfig({ externalDataSource: { ...tableConfig.externalDataSource, enabled: false, tableName: "", ...updates }, }); }; // 외부 데이터 소스 테이블 컬럼 목록 const externalTableColumns = useMemo(() => { return tableColumns[tableConfig.externalDataSource?.tableName || ""] || []; }, [tableColumns, tableConfig.externalDataSource?.tableName]); // 외부 데이터 소스 테이블 변경 시 컬럼 로드 useEffect(() => { if (tableConfig.externalDataSource?.enabled && tableConfig.externalDataSource?.tableName) { onLoadTableColumns(tableConfig.externalDataSource.tableName); } }, [tableConfig.externalDataSource?.enabled, tableConfig.externalDataSource?.tableName, onLoadTableColumns]); // 저장 함수 const handleSave = () => { onSave({ title, description, tableConfig, }); onOpenChange(false); }; // 컬럼 추가 const addColumn = () => { const newColumn: TableColumnConfig = { ...defaultTableColumnConfig, field: `column_${(tableConfig.columns || []).length + 1}`, label: `컬럼 ${(tableConfig.columns || []).length + 1}`, }; updateTableConfig({ columns: [...(tableConfig.columns || []), newColumn], }); }; // 컬럼 삭제 const removeColumn = (index: number) => { updateTableConfig({ columns: (tableConfig.columns || []).filter((_, i) => i !== index), }); }; // 컬럼 업데이트 const updateColumn = (index: number, updates: Partial) => { updateTableConfig({ columns: (tableConfig.columns || []).map((col, i) => i === index ? { ...col, ...updates } : col ), }); }; // 컬럼 이동 const moveColumn = (index: number, direction: "up" | "down") => { const columns = [...(tableConfig.columns || [])]; if (direction === "up" && index > 0) { [columns[index - 1], columns[index]] = [columns[index], columns[index - 1]]; } else if (direction === "down" && index < columns.length - 1) { [columns[index], columns[index + 1]] = [columns[index + 1], columns[index]]; } updateTableConfig({ columns }); }; // 사전 필터 추가 const addPreFilter = () => { const newFilter: TablePreFilter = { ...defaultPreFilterConfig }; updateFilters({ preFilters: [...(tableConfig.filters?.preFilters || []), newFilter], }); }; // 사전 필터 삭제 const removePreFilter = (index: number) => { updateFilters({ preFilters: (tableConfig.filters?.preFilters || []).filter((_, i) => i !== index), }); }; // 사전 필터 업데이트 const updatePreFilter = (index: number, updates: Partial) => { updateFilters({ preFilters: (tableConfig.filters?.preFilters || []).map((f, i) => i === index ? { ...f, ...updates } : f ), }); }; // 모달 필터 추가 const addModalFilter = () => { const newFilter: TableModalFilter = { ...defaultModalFilterConfig }; updateFilters({ modalFilters: [...(tableConfig.filters?.modalFilters || []), newFilter], }); }; // 모달 필터 삭제 const removeModalFilter = (index: number) => { updateFilters({ modalFilters: (tableConfig.filters?.modalFilters || []).filter((_, i) => i !== index), }); }; // 모달 필터 업데이트 const updateModalFilter = (index: number, updates: Partial) => { updateFilters({ modalFilters: (tableConfig.filters?.modalFilters || []).map((f, i) => i === index ? { ...f, ...updates } : f ), }); }; // 계산 규칙 추가 const addCalculation = () => { const newCalc: TableCalculationRule = { ...defaultCalculationRuleConfig }; updateTableConfig({ calculations: [...(tableConfig.calculations || []), newCalc], }); }; // 계산 규칙 삭제 const removeCalculation = (index: number) => { updateTableConfig({ calculations: (tableConfig.calculations || []).filter((_, i) => i !== index), }); }; // 계산 규칙 업데이트 const updateCalculation = (index: number, updates: Partial) => { updateTableConfig({ calculations: (tableConfig.calculations || []).map((c, i) => i === index ? { ...c, ...updates } : c ), }); }; // 표시 컬럼 토글 const toggleDisplayColumn = (columnName: string) => { const current = tableConfig.source.displayColumns || []; if (current.includes(columnName)) { updateSource({ displayColumns: current.filter((c) => c !== columnName), }); } else { updateSource({ displayColumns: [...current, columnName], }); } }; // 검색 컬럼 토글 const toggleSearchColumn = (columnName: string) => { const current = tableConfig.source.searchColumns || []; if (current.includes(columnName)) { updateSource({ searchColumns: current.filter((c) => c !== columnName), }); } else { updateSource({ searchColumns: [...current, columnName], }); } }; // 표시 컬럼 순서 변경 const moveDisplayColumn = (index: number, direction: "up" | "down") => { const columns = [...(tableConfig.source.displayColumns || [])]; if (direction === "up" && index > 0) { [columns[index - 1], columns[index]] = [columns[index], columns[index - 1]]; } else if (direction === "down" && index < columns.length - 1) { [columns[index], columns[index + 1]] = [columns[index + 1], columns[index]]; } updateSource({ displayColumns: columns }); }; // 표시 컬럼 삭제 (순서 편집 영역에서) const removeDisplayColumn = (columnName: string) => { updateSource({ displayColumns: (tableConfig.source.displayColumns || []).filter((c) => c !== columnName), }); }; return ( 테이블 섹션 설정 테이블 형식의 데이터를 표시하고 편집하는 섹션을 설정합니다.
{/* 기본 정보 */}

기본 정보

setTitle(e.target.value)} placeholder="예: 품목 목록" className="h-9 text-sm" />
setDescription(e.target.value)} placeholder="섹션에 대한 설명" className="h-9 text-sm" />
{/* 탭 구성 */} 테이블 설정 컬럼 설정 검색 설정 고급 설정 {/* 테이블 설정 탭 */} {/* 소스 테이블 설정 */}

검색용 소스 테이블

검색 모달에서 데이터를 가져올 테이블입니다.

테이블을 찾을 수 없습니다. {tables.map((table) => ( { updateSource({ tableName: table.table_name }); setTableSearchOpen(false); }} className="text-sm" >
{table.table_name} {table.comment && ( {table.comment} )}
))}
{/* 저장 테이블 설정 */}

저장용 테이블

테이블 섹션 데이터를 저장할 테이블입니다. 미설정 시 메인 테이블에 저장됩니다.

테이블을 찾을 수 없습니다. { updateSaveConfig({ targetTable: undefined }); setSaveTableSearchOpen(false); }} className="text-sm" > (메인 테이블과 동일) {tables.map((table) => ( { updateSaveConfig({ targetTable: table.table_name }); // 선택 즉시 컬럼 로드 요청 onLoadTableColumns(table.table_name); setSaveTableSearchOpen(false); }} className="text-sm" >
{table.table_name} {table.comment && ( {table.comment} )}
))}
updateSaveConfig({ uniqueField: e.target.value || undefined })} placeholder="예: item_id" className="h-9 text-sm mt-1" /> 동일 값이 있으면 추가하지 않습니다.
{/* 외부 데이터 소스 설정 */}

외부 데이터 소스

"데이터 전달 모달열기" 액션으로 전달받은 데이터를 테이블에 표시합니다.

{ if (checked) { updateExternalDataSource({ enabled: true, tableName: "" }); } else { updateTableConfig({ externalDataSource: undefined }); } }} />
{tableConfig.externalDataSource?.enabled && (
테이블을 찾을 수 없습니다. {tables.map((table) => ( { updateExternalDataSource({ enabled: true, tableName: table.table_name }); onLoadTableColumns(table.table_name); setExternalTableSearchOpen(false); }} className="text-sm" >
{table.table_name} {table.comment && ( {table.comment} )}
))}
이전 화면에서 전달받을 데이터의 원본 테이블을 선택하세요. (예: 수주상세 데이터를 전달받는 경우 sales_order_detail)
{tableConfig.externalDataSource?.tableName && externalTableColumns.length > 0 && (

선택한 테이블 컬럼: {externalTableColumns.length}개

"컬럼 설정" 탭에서 각 컬럼의 "외부 필드"를 설정하여 전달받은 데이터의 컬럼을 매핑하세요.

)}
)}
{/* 컬럼 설정 탭 */} {/* 안내 메시지 */} {saveTableColumns.length === 0 && !tableConfig.saveConfig?.targetTable && !tableConfig.source.tableName && (

"테이블 설정" 탭에서 저장 테이블을 먼저 선택해주세요. 선택한 테이블의 컬럼을 여기서 설정할 수 있습니다.

)} {/* 테이블은 선택했지만 컬럼이 아직 로드되지 않은 경우 */} {saveTableColumns.length === 0 && (tableConfig.saveConfig?.targetTable || tableConfig.source.tableName) && (

테이블 "{tableConfig.saveConfig?.targetTable || tableConfig.source.tableName}" 의 컬럼을 불러오는 중입니다...

)}
{saveTableColumns.length > 0 && (

사용 가능한 컬럼: {saveTableColumns.length}개 ({tableConfig.saveConfig?.targetTable || tableConfig.source.tableName || "테이블 미선택"})

)}
{(tableConfig.columns || []).length === 0 ? (

컬럼이 없습니다

"컬럼 추가" 버튼으로 추가하세요

) : (
{(tableConfig.columns || []).map((col, index) => ( updateColumn(index, updates)} onMoveUp={() => moveColumn(index, "up")} onMoveDown={() => moveColumn(index, "down")} onRemove={() => removeColumn(index)} /> ))}
)}
{/* 검색 설정 탭 */} {/* 표시 컬럼 / 검색 컬럼 설정 */}

검색 모달 컬럼 설정

검색 모달에서 보여줄 컬럼과 검색 대상 컬럼을 설정합니다.

{/* 소스 테이블 미선택 시 안내 */} {!tableConfig.source.tableName && (

"테이블 설정" 탭에서 검색용 소스 테이블을 먼저 선택해주세요.

)} {/* 표시 컬럼 선택 */} {sourceTableColumns.length > 0 && (
{sourceTableColumns.map((col) => ( ))}
선택된 컬럼: {(tableConfig.source.displayColumns || []).length}개 {/* 선택된 컬럼 순서 편집 */} {(tableConfig.source.displayColumns || []).length > 0 && (
{(tableConfig.source.displayColumns || []).map((colName, index) => { const colInfo = sourceTableColumns.find((c) => c.column_name === colName); return (
{colName} {colInfo?.comment && ( {colInfo.comment} )}
); })}
)}
)} {/* 검색 컬럼 선택 */} {sourceTableColumns.length > 0 && (
{sourceTableColumns.map((col) => ( ))}
검색 컬럼: {(tableConfig.source.searchColumns || []).length}개
)}
{/* 사전 필터 */}

항상 적용되는 필터 조건입니다.

{(tableConfig.filters?.preFilters || []).map((filter, index) => { // 선택된 컬럼의 정보 조회 const selectedColumn = filter.column ? sourceTableColumns.find((c) => c.column_name === filter.column) : null; const isCategory = selectedColumn?.input_type === "category"; const categoryOptions = isCategory && filter.column ? preFilterCategoryOptions[filter.column] || [] : []; return (
{/* 카테고리 컬럼인 경우 Select Box로 값 선택 */} {isCategory ? ( ) : ( updatePreFilter(index, { value: e.target.value })} placeholder="값" className="h-8 text-xs flex-1" /> )}
); })}
{/* 모달 필터 */}

사용자가 선택할 수 있는 필터입니다.

{(tableConfig.filters?.modalFilters || []).map((filter, index) => (
{/* 컬럼 선택 */} {/* 라벨 */} updateModalFilter(index, { label: e.target.value })} placeholder="라벨" className="h-8 text-xs w-[100px]" /> {/* 타입 */} {/* 카테고리 선택 (타입이 category일 때만 표시) */} {filter.type === "category" && ( )} {/* 삭제 버튼 */}
))}
{/* 고급 설정 탭 */} {/* UI 설정 */}

UI 설정

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

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

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

기존 데이터에서 선택

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

빈 행 직접 입력

{/* 검색 버튼 텍스트 */}
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?.showSearchButton ?? true)} />
{/* 테이블 최대 높이 */}
updateUiConfig({ maxHeight: e.target.value })} placeholder="400px" className="h-8 text-xs mt-1" />
{/* 다중 선택 허용 */}
{/* 계산 규칙 */}

계산 규칙

다른 컬럼 값을 기반으로 자동 계산합니다.

{(tableConfig.calculations || []).map((calc, index) => ( updateCalculation(index, updates)} onRemove={() => removeCalculation(index)} /> ))}
{/* 조건부 테이블 설정 */}

조건부 테이블

조건(검사유형 등)에 따라 다른 데이터를 표시하고 저장합니다.

{ setTableConfig({ ...tableConfig, conditionalTable: checked ? { ...defaultConditionalTableConfig, enabled: true } : { ...defaultConditionalTableConfig, enabled: false }, }); }} className="scale-75" />
{tableConfig.conditionalTable?.enabled && (
{/* 트리거 유형 및 조건 컬럼 */}
체크박스: 다중 선택 후 탭으로 표시 / 드롭다운: 단일 선택 / 탭: 모든 옵션 표시
저장 시 각 행에 조건 값이 이 컬럼에 자동 저장됩니다.
{/* 조건 옵션 목록 */}
{/* 옵션 목록 */}
{(tableConfig.conditionalTable?.options || []).map((option, index) => (
{ const newOptions = [...(tableConfig.conditionalTable?.options || [])]; newOptions[index] = { ...newOptions[index], value: e.target.value }; // label이 비어있으면 value와 동일하게 설정 if (!newOptions[index].label) { newOptions[index].label = e.target.value; } setTableConfig({ ...tableConfig, conditionalTable: { ...tableConfig.conditionalTable!, options: newOptions, }, }); }} placeholder="저장 값 (예: 입고검사)" className="h-8 text-xs flex-1" /> { const newOptions = [...(tableConfig.conditionalTable?.options || [])]; newOptions[index] = { ...newOptions[index], label: e.target.value }; setTableConfig({ ...tableConfig, conditionalTable: { ...tableConfig.conditionalTable!, options: newOptions, }, }); }} placeholder="표시 라벨 (예: 입고검사)" className="h-8 text-xs flex-1" />
))} {(tableConfig.conditionalTable?.options || []).length === 0 && (
조건 옵션을 추가하세요. (예: 입고검사, 공정검사, 출고검사 등)
)}
{/* 테이블에서 옵션 로드 설정 */}
{ setTableConfig({ ...tableConfig, conditionalTable: { ...tableConfig.conditionalTable!, optionSource: { ...tableConfig.conditionalTable?.optionSource, enabled: checked, tableName: tableConfig.conditionalTable?.optionSource?.tableName || "", valueColumn: tableConfig.conditionalTable?.optionSource?.valueColumn || "", labelColumn: tableConfig.conditionalTable?.optionSource?.labelColumn || "", }, }, }); }} className="scale-75" />
{tableConfig.conditionalTable?.optionSource?.enabled && ( { setTableConfig({ ...tableConfig, conditionalTable: { ...tableConfig.conditionalTable!, optionSource: { ...tableConfig.conditionalTable?.optionSource!, ...updates, }, }, }); }} /> )}
{/* 소스 테이블 필터링 설정 */}
{ setTableConfig({ ...tableConfig, conditionalTable: { ...tableConfig.conditionalTable!, sourceFilter: { enabled: checked, filterColumn: tableConfig.conditionalTable?.sourceFilter?.filterColumn || "", }, }, }); }} className="scale-75" />

조건 선택 시 소스 테이블에서 해당 조건으로 필터링합니다

{tableConfig.conditionalTable?.sourceFilter?.enabled && (

소스 테이블({tableConfig.source?.tableName || "미설정"})에서 조건값으로 필터링할 컬럼

{sourceTableColumns.length > 0 ? ( 컬럼을 찾을 수 없습니다. {sourceTableColumns.map((col) => ( { setTableConfig({ ...tableConfig, conditionalTable: { ...tableConfig.conditionalTable!, sourceFilter: { ...tableConfig.conditionalTable?.sourceFilter!, filterColumn: col.column_name, }, }, }); }} className="text-xs" > {col.column_name} {col.comment && ( ({col.comment}) )} ))} ) : ( { setTableConfig({ ...tableConfig, conditionalTable: { ...tableConfig.conditionalTable!, sourceFilter: { ...tableConfig.conditionalTable?.sourceFilter!, filterColumn: e.target.value, }, }, }); }} placeholder="inspection_type" className="h-7 text-xs" /> )}

예: 검사유형 "입고검사" 선택 시 → inspection_type = '입고검사' 조건 적용

)} {/* 사용 가이드 */}

사용 가이드

1. 소스 테이블 필터링 활성화 후:

  • 항목 검색: 검색 모달에서 필터링된 데이터만 표시
  • 빈 행 추가: 드롭다운 옵션이 필터링된 데이터로 제한

2. 컬럼 설정에서 추가 설정:

  • 컬럼 타입을 "선택(드롭다운)"으로 변경
  • "동적 드롭다운 옵션" 섹션이 나타남
  • 소스 컬럼 선택 → 해당 컬럼 값이 드롭다운 옵션으로 표시
  • "행 선택 모드" 활성화 시 → 선택한 값의 같은 행 데이터를 다른 컬럼에 자동 채움

3. 예시 (품목검사정보):

  • "입고검사" 체크박스 선택 → 테이블 탭 표시
  • "항목 추가" 클릭 → 빈 행 생성
  • "검사항목" 드롭다운 → inspection_type='입고검사'인 항목만 표시
  • 검사항목 선택 시 → 검사기준, 검사방법 자동 채움 (행 선택 모드)
)}
); }