"use client"; import React, { useState, useEffect } from "react"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; import { Plus, X, Check, ChevronsUpDown } from "lucide-react"; import { ModalRepeaterTableProps, RepeaterColumnConfig, ColumnMapping, CalculationRule, DynamicDataSourceConfig, DynamicDataSourceOption, MultiTableJoinStep, ModalFilterConfig } from "./types"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog"; import { tableManagementApi } from "@/lib/api/tableManagement"; import { cn } from "@/lib/utils"; interface ModalRepeaterTableConfigPanelProps { config: Partial; onChange: (config: Partial) => void; onConfigChange?: (config: Partial) => void; // 하위 호환성 } // 소스 컬럼 선택기 (동적 테이블별 컬럼 로드) function SourceColumnSelector({ sourceTable, value, onChange, }: { sourceTable: string; value: string; onChange: (value: string) => void; }) { const [columns, setColumns] = useState<{ columnName: string; displayName?: string }[]>([]); const [isLoading, setIsLoading] = useState(false); useEffect(() => { const loadColumns = async () => { if (!sourceTable) { setColumns([]); return; } setIsLoading(true); try { const response = await tableManagementApi.getColumnList(sourceTable); if (response.success && response.data) { setColumns(response.data.columns); } } catch (error) { console.error("컬럼 로드 실패:", error); setColumns([]); } finally { setIsLoading(false); } }; loadColumns(); }, [sourceTable]); return ( ); } // 참조 컬럼 선택기 (동적 테이블별 컬럼 로드) function ReferenceColumnSelector({ referenceTable, value, onChange, }: { referenceTable: string; value: string; onChange: (value: string) => void; }) { const [columns, setColumns] = useState<{ columnName: string; displayName?: string }[]>([]); const [isLoading, setIsLoading] = useState(false); useEffect(() => { const loadColumns = async () => { if (!referenceTable) { setColumns([]); return; } setIsLoading(true); try { const response = await tableManagementApi.getColumnList(referenceTable); if (response.success && response.data) { setColumns(response.data.columns); } } catch (error) { console.error("참조 컬럼 로드 실패:", error); setColumns([]); } finally { setIsLoading(false); } }; loadColumns(); }, [referenceTable]); return ( ); } export function ModalRepeaterTableConfigPanel({ config, onChange, onConfigChange, }: ModalRepeaterTableConfigPanelProps) { // 하위 호환성: onConfigChange가 있으면 사용, 없으면 onChange 사용 const handleConfigChange = onConfigChange || onChange; // 초기 설정 정리: 계산 규칙과 컬럼 설정 동기화 const cleanupInitialConfig = (initialConfig: Partial): Partial => { // 계산 규칙이 없으면 모든 컬럼의 calculated 속성 제거 if (!initialConfig.calculationRules || initialConfig.calculationRules.length === 0) { const cleanedColumns = (initialConfig.columns || []).map((col) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { calculated, ...rest } = col; return { ...rest, editable: true }; }); return { ...initialConfig, columns: cleanedColumns }; } // 계산 규칙이 있으면 결과 필드만 calculated=true const resultFields = new Set(initialConfig.calculationRules.map((rule) => rule.result)); const cleanedColumns = (initialConfig.columns || []).map((col) => { if (resultFields.has(col.field)) { // 계산 결과 필드는 calculated=true, editable=false return { ...col, calculated: true, editable: false }; } else { // 나머지 필드는 calculated 제거, editable=true // eslint-disable-next-line @typescript-eslint/no-unused-vars const { calculated, ...rest } = col; return { ...rest, editable: true }; } }); return { ...initialConfig, columns: cleanedColumns }; }; const [localConfig, setLocalConfig] = useState(cleanupInitialConfig(config)); const [allTables, setAllTables] = useState<{ tableName: string; displayName?: string }[]>([]); const [tableColumns, setTableColumns] = useState<{ columnName: string; displayName?: string }[]>([]); const [targetTableColumns, setTargetTableColumns] = useState<{ columnName: string; displayName?: string }[]>([]); const [isLoadingTables, setIsLoadingTables] = useState(false); const [isLoadingColumns, setIsLoadingColumns] = useState(false); const [isLoadingTargetColumns, setIsLoadingTargetColumns] = useState(false); const [openTableCombo, setOpenTableCombo] = useState(false); const [openTargetTableCombo, setOpenTargetTableCombo] = useState(false); const [openUniqueFieldCombo, setOpenUniqueFieldCombo] = useState(false); // 동적 데이터 소스 설정 모달 const [dynamicSourceModalOpen, setDynamicSourceModalOpen] = useState(false); const [editingDynamicSourceColumnIndex, setEditingDynamicSourceColumnIndex] = useState(null); // config 변경 시 localConfig 동기화 (cleanupInitialConfig 적용) useEffect(() => { const cleanedConfig = cleanupInitialConfig(config); setLocalConfig(cleanedConfig); }, [config]); // 전체 테이블 목록 로드 useEffect(() => { const loadTables = async () => { setIsLoadingTables(true); try { const response = await tableManagementApi.getTableList(); if (response.success && response.data) { setAllTables(response.data); } } catch (error) { console.error("테이블 목록 로드 실패:", error); } finally { setIsLoadingTables(false); } }; loadTables(); }, []); // 소스 테이블의 컬럼 목록 로드 useEffect(() => { const loadColumns = async () => { if (!localConfig.sourceTable) { setTableColumns([]); return; } setIsLoadingColumns(true); try { const response = await tableManagementApi.getColumnList(localConfig.sourceTable); if (response.success && response.data) { setTableColumns(response.data.columns); } } catch (error) { console.error("컬럼 목록 로드 실패:", error); setTableColumns([]); } finally { setIsLoadingColumns(false); } }; loadColumns(); }, [localConfig.sourceTable]); // 저장 테이블의 컬럼 목록 로드 useEffect(() => { const loadTargetColumns = async () => { if (!localConfig.targetTable) { setTargetTableColumns([]); return; } setIsLoadingTargetColumns(true); try { const response = await tableManagementApi.getColumnList(localConfig.targetTable); if (response.success && response.data) { setTargetTableColumns(response.data.columns); } } catch (error) { console.error("저장 테이블 컬럼 로드 실패:", error); setTargetTableColumns([]); } finally { setIsLoadingTargetColumns(false); } }; loadTargetColumns(); }, [localConfig.targetTable]); const updateConfig = (updates: Partial) => { const newConfig = { ...localConfig, ...updates }; setLocalConfig(newConfig); handleConfigChange(newConfig); }; const addSourceColumn = () => { const columns = localConfig.sourceColumns || []; updateConfig({ sourceColumns: [...columns, ""] }); }; const updateSourceColumn = (index: number, value: string) => { const columns = [...(localConfig.sourceColumns || [])]; columns[index] = value; updateConfig({ sourceColumns: columns }); }; const removeSourceColumn = (index: number) => { const columns = [...(localConfig.sourceColumns || [])]; columns.splice(index, 1); updateConfig({ sourceColumns: columns }); }; const addSearchField = () => { const fields = localConfig.sourceSearchFields || []; updateConfig({ sourceSearchFields: [...fields, ""] }); }; const updateSearchField = (index: number, value: string) => { const fields = [...(localConfig.sourceSearchFields || [])]; fields[index] = value; updateConfig({ sourceSearchFields: fields }); }; const removeSearchField = (index: number) => { const fields = [...(localConfig.sourceSearchFields || [])]; fields.splice(index, 1); updateConfig({ sourceSearchFields: fields }); }; // 🆕 저장 테이블 컬럼에서 반복 테이블 컬럼 추가 const addRepeaterColumnFromTarget = (columnName: string) => { const columns = localConfig.columns || []; // 이미 존재하는 컬럼인지 확인 if (columns.some((col) => col.field === columnName)) { alert("이미 추가된 컬럼입니다."); return; } const targetCol = targetTableColumns.find((c) => c.columnName === columnName); const newColumn: RepeaterColumnConfig = { field: columnName, label: targetCol?.displayName || columnName, type: "text", width: "150px", editable: true, mapping: { type: "manual", referenceTable: localConfig.targetTable, referenceField: columnName, }, }; updateConfig({ columns: [...columns, newColumn] }); }; // 🆕 반복 테이블 컬럼 삭제 const removeRepeaterColumn = (index: number) => { const columns = [...(localConfig.columns || [])]; columns.splice(index, 1); updateConfig({ columns }); }; // 🆕 반복 테이블 컬럼 개별 수정 const updateRepeaterColumn = (index: number, updates: Partial) => { const columns = [...(localConfig.columns || [])]; columns[index] = { ...columns[index], ...updates }; updateConfig({ columns }); }; // 🆕 계산 규칙 관리 const addCalculationRule = () => { const rules = localConfig.calculationRules || []; const newRule: CalculationRule = { result: "", formula: "", dependencies: [], }; updateConfig({ calculationRules: [...rules, newRule] }); }; const updateCalculationRule = (index: number, updates: Partial) => { const rules = [...(localConfig.calculationRules || [])]; const oldRule = rules[index]; const newRule = { ...oldRule, ...updates }; // 결과 필드가 변경된 경우 if (updates.result !== undefined && oldRule.result !== updates.result) { const columns = [...(localConfig.columns || [])]; // 이전 결과 필드의 calculated 속성 제거 if (oldRule.result) { const oldResultIndex = columns.findIndex((c) => c.field === oldRule.result); if (oldResultIndex !== -1) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { calculated, ...rest } = columns[oldResultIndex]; columns[oldResultIndex] = { ...rest, editable: true }; } } // 새 결과 필드를 calculated=true, editable=false로 설정 if (updates.result) { const newResultIndex = columns.findIndex((c) => c.field === updates.result); if (newResultIndex !== -1) { columns[newResultIndex] = { ...columns[newResultIndex], calculated: true, editable: false, }; } } rules[index] = newRule; updateConfig({ calculationRules: rules, columns }); return; } rules[index] = newRule; updateConfig({ calculationRules: rules }); }; const removeCalculationRule = (index: number) => { const rules = [...(localConfig.calculationRules || [])]; const removedRule = rules[index]; // 결과 필드의 calculated 속성 제거 if (removedRule.result) { const columns = [...(localConfig.columns || [])]; const resultIndex = columns.findIndex((c) => c.field === removedRule.result); if (resultIndex !== -1) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { calculated, ...rest } = columns[resultIndex]; columns[resultIndex] = { ...rest, editable: true }; } rules.splice(index, 1); updateConfig({ calculationRules: rules, columns }); return; } rules.splice(index, 1); updateConfig({ calculationRules: rules }); }; // 동적 데이터 소스 설정 함수들 const openDynamicSourceModal = (columnIndex: number) => { setEditingDynamicSourceColumnIndex(columnIndex); setDynamicSourceModalOpen(true); }; const toggleDynamicDataSource = (columnIndex: number, enabled: boolean) => { const columns = [...(localConfig.columns || [])]; if (enabled) { columns[columnIndex] = { ...columns[columnIndex], dynamicDataSource: { enabled: true, options: [], }, }; } else { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { dynamicDataSource, ...rest } = columns[columnIndex]; columns[columnIndex] = rest; } updateConfig({ columns }); }; const addDynamicSourceOption = (columnIndex: number) => { const columns = [...(localConfig.columns || [])]; const col = columns[columnIndex]; const newOption: DynamicDataSourceOption = { id: `option_${Date.now()}`, label: "새 옵션", sourceType: "table", tableConfig: { tableName: "", valueColumn: "", joinConditions: [], }, }; columns[columnIndex] = { ...col, dynamicDataSource: { ...col.dynamicDataSource!, enabled: true, options: [...(col.dynamicDataSource?.options || []), newOption], }, }; updateConfig({ columns }); }; const updateDynamicSourceOption = (columnIndex: number, optionIndex: number, updates: Partial) => { const columns = [...(localConfig.columns || [])]; const col = columns[columnIndex]; const options = [...(col.dynamicDataSource?.options || [])]; options[optionIndex] = { ...options[optionIndex], ...updates }; columns[columnIndex] = { ...col, dynamicDataSource: { ...col.dynamicDataSource!, options, }, }; updateConfig({ columns }); }; const removeDynamicSourceOption = (columnIndex: number, optionIndex: number) => { const columns = [...(localConfig.columns || [])]; const col = columns[columnIndex]; const options = [...(col.dynamicDataSource?.options || [])]; options.splice(optionIndex, 1); columns[columnIndex] = { ...col, dynamicDataSource: { ...col.dynamicDataSource!, options, }, }; updateConfig({ columns }); }; const setDefaultDynamicSourceOption = (columnIndex: number, optionId: string) => { const columns = [...(localConfig.columns || [])]; const col = columns[columnIndex]; columns[columnIndex] = { ...col, dynamicDataSource: { ...col.dynamicDataSource!, defaultOptionId: optionId, }, }; updateConfig({ columns }); }; return (
{/* 소스/저장 테이블 설정 */}

1. 소스 테이블 (데이터 검색)

항목 검색 모달에서 어떤 테이블의 어떤 컬럼 정보를 보여줄 건지 설정

{/* 소스 테이블 */}
테이블을 찾을 수 없습니다. {allTables.map((table) => ( { updateConfig({ sourceTable: table.tableName }); setOpenTableCombo(false); }} className="text-xs sm:text-sm" >
{table.displayName || table.tableName} {table.displayName && {table.tableName}}
))}

모달에서 검색할 데이터 테이블

2. 저장 테이블 (데이터 저장)

반복 테이블에 입력된 정보를 어떤 테이블에 저장할 건지 선택

{/* 저장 테이블 */}
테이블을 찾을 수 없습니다. {allTables.map((table) => ( { updateConfig({ targetTable: table.tableName }); setOpenTargetTableCombo(false); }} className="text-xs sm:text-sm" >
{table.displayName || table.tableName} {table.displayName && {table.tableName}}
))}

반복 테이블 데이터를 저장할 대상 테이블

{/* 소스 컬럼 */}

모달 테이블에 표시할 컬럼과 헤더 라벨을 설정합니다

{(localConfig.sourceColumns || []).map((column, index) => (
{/* 컬럼 선택 */}
{/* 라벨 입력 */}
{ const newLabels = { ...(localConfig.sourceColumnLabels || {}) }; if (e.target.value) { newLabels[column] = e.target.value; } else { delete newLabels[column]; } updateConfig({ sourceColumnLabels: newLabels }); }} placeholder={tableColumns.find(c => c.columnName === column)?.displayName || column || "라벨 입력"} className="h-8 text-xs" disabled={!column} />
))} {(localConfig.sourceColumns || []).length === 0 && (

"추가" 버튼을 클릭하여 모달에 표시할 컬럼을 추가하세요

)}
{/* 검색 필드 */}

모달에서 검색 가능한 필드들

{(localConfig.sourceSearchFields || []).map((field, index) => (
))}
{/* 중복 체크 필드 */}
필드를 찾을 수 없습니다. {tableColumns.map((column) => ( { updateConfig({ uniqueField: column.columnName }); setOpenUniqueFieldCombo(false); }} className="text-xs sm:text-sm" >
{column.displayName || column.columnName} {column.displayName && {column.columnName}}
))}

중복 추가를 방지할 고유 필드 (예: 품목 코드)

{/* 모달 설정 */}

모달 설정

updateConfig({ modalTitle: e.target.value })} placeholder="항목 검색 및 선택" className="h-8 text-xs sm:h-10 sm:text-sm" />
updateConfig({ modalButtonText: e.target.value })} placeholder="항목 검색" className="h-8 text-xs sm:h-10 sm:text-sm" />
updateConfig({ multiSelect: checked }) } />
{/* 모달 필터 설정 */}

모달에서 드롭다운으로 필터링할 컬럼을 설정합니다. 소스 테이블의 해당 컬럼에서 고유 값들이 자동으로 표시됩니다.

{(localConfig.modalFilters || []).length > 0 && (
{(localConfig.modalFilters || []).map((filter, index) => (
{ const filters = [...(localConfig.modalFilters || [])]; filters[index] = { ...filters[index], label: e.target.value }; updateConfig({ modalFilters: filters }); }} placeholder="라벨" className="h-8 text-xs w-[100px]" />
))}
)}
{/* 반복 테이블 컬럼 관리 */}

3. 반복 테이블 컬럼 (헤더 구성)

반복 테이블 헤더에 표시할 컬럼 옵션 - 어떤 컬럼을 화면에 보여줄지 선택

{/* 저장 테이블 컬럼 선택 */} {localConfig.targetTable && targetTableColumns.length > 0 && (
{targetTableColumns.map((col) => ( ))}
)} {/* 추가된 컬럼 목록 */} {localConfig.columns && localConfig.columns.length > 0 && (
{localConfig.columns.map((col, index) => (
{col.label} ({col.mapping?.type === "source" ? "소스" : col.mapping?.type === "reference" ? "참조" : "수동입력"})
))}
{/* 개별 컬럼 설정 - 세로 레이아웃 */}
{localConfig.columns.map((col, index) => (

{col.label}

{/* 세로 레이아웃: 모든 항목 동일한 너비/높이 */}
{/* 1. 품목명 */}
updateRepeaterColumn(index, { label: e.target.value })} placeholder="화면에 표시될 이름" className="h-10 text-sm w-full" />

반복 테이블 헤더에 표시될 이름

{/* 2. 컬럼명 */}
updateRepeaterColumn(index, { field: e.target.value })} placeholder="데이터베이스 필드명" className="h-10 text-sm w-full" />

저장 테이블의 실제 컬럼명

{/* 3. 컬럼 타입 */}

입력 필드 타입

{/* 4. 매핑 설정 */}

{col.mapping?.type === "source" && "모달에서 선택한 항목의 값을 가져옴"} {col.mapping?.type === "reference" && "다른 테이블의 값을 참조"} {col.mapping?.type === "manual" && "사용자가 직접 입력"}

{/* 5. 소스 정보 */}
{col.mapping?.type === "source" && (

소스 테이블에서 복사

모달 검색에서 선택한 항목(소스 테이블)의 컬럼 값을 가져옴

{/* 소스 테이블 선택 */}

값을 가져올 소스 테이블

{/* 소스 컬럼 선택 */}
updateRepeaterColumn(index, { mapping: { ...col.mapping, type: "source", sourceField: value } as ColumnMapping })} />

가져올 컬럼명

)} {col.mapping?.type === "reference" && (

외부 테이블 참조

다른 테이블의 컬럼 값을 조인하여 가져옴 (조인 조건 필요)

{/* 참조 테이블 선택 */}

참조할 외부 테이블

{/* 참조 컬럼 선택 */}
updateRepeaterColumn(index, { mapping: { ...col.mapping, type: "reference", referenceField: value } as ColumnMapping })} />

가져올 컬럼명

{/* 조인 조건 설정 */}

소스 테이블과 참조 테이블을 어떻게 매칭할지 설정

{/* 조인 조건 목록 */}
{(col.mapping?.joinCondition || []).map((condition, condIndex) => (
조인 조건 {condIndex + 1}
{/* 소스 테이블 선택 */}

반복 테이블 = 저장 테이블 컬럼 사용

{/* 소스 필드 */}

{(!condition.sourceTable || condition.sourceTable === "target") ? "반복 테이블에 이미 추가된 컬럼" : "모달에서 선택한 원본 데이터의 컬럼"}

{/* 연산자 */}
{/* 대상 필드 */}
{ const currentConditions = [...(col.mapping?.joinCondition || [])]; currentConditions[condIndex] = { ...currentConditions[condIndex], targetField: value }; updateRepeaterColumn(index, { mapping: { ...col.mapping, type: "reference", joinCondition: currentConditions } as ColumnMapping }); }} />
{/* 조인 조건 미리보기 */} {condition.sourceField && condition.targetField && (
{condition.sourceTable === "source" ? localConfig.sourceTable : localConfig.targetTable || "저장테이블"} .{condition.sourceField} {condition.operator || "="} {col.mapping?.referenceTable} .{condition.targetField}
)}
))} {/* 조인 조건 없을 때 안내 */} {(!col.mapping?.joinCondition || col.mapping.joinCondition.length === 0) && (

조인 조건이 없습니다

"조인 조건 추가" 버튼을 클릭하여 매칭 조건을 설정하세요

)}
{/* 조인 조건 예시 */} {col.mapping?.referenceTable && (

조인 조건 예시

예) 거래처별 품목 단가 조회:

• item_code = item_code

• customer_code = customer_code

)}
)} {col.mapping?.type === "manual" && (

화면에서 입력 (수동)

사용자가 반복 테이블에서 직접 입력

)}
{/* 6. 동적 데이터 소스 설정 */}
toggleDynamicDataSource(index, checked)} />

컬럼 헤더 클릭으로 데이터 소스 전환 (예: 거래처별 단가, 품목별 단가)

{col.dynamicDataSource?.enabled && (
{col.dynamicDataSource.options.length}개 옵션 설정됨
{/* 옵션 미리보기 */} {col.dynamicDataSource.options.length > 0 && (
{col.dynamicDataSource.options.map((opt) => ( {opt.label} {col.dynamicDataSource?.defaultOptionId === opt.id && " (기본)"} ))}
)}
)}
))}
)}
{/* 계산 규칙 (자동 계산) */}

4. 계산 규칙 (자동 계산)

반복 테이블 컬럼들을 조합하여 자동 계산 (예: 수량 x 단가 = 금액)

중요 안내

  • 결과 필드는 수정 불가 (자동 계산됨)
  • • 계산 공식에 사용된 컬럼은 수정 가능
  • • 예시: order_qty * unit_price = total_amount
{localConfig.calculationRules && localConfig.calculationRules.length > 0 ? (
{localConfig.calculationRules.map((rule, index) => (
{index + 1}

계산 규칙 {index + 1}

{/* 결과 필드 */}

이 컬럼은 자동 계산되어 수정 불가능합니다

{/* 계산 공식 */}
updateCalculationRule(index, { formula: e.target.value })} placeholder="예: order_qty * unit_price" className="h-10 text-sm w-full font-mono" />

사용 가능한 연산자: +, -, *, /, (), 필드명 사용

{/* 현재 설정 표시 */} {rule.result && rule.formula && (

현재 설정

{localConfig.columns?.find((c) => c.field === rule.result)?.label || rule.result} = {rule.formula}
)}
))}
) : (

계산 규칙이 없습니다

자동 계산이 필요한 컬럼이 있다면 규칙을 추가하세요

)} {localConfig.calculationRules && localConfig.calculationRules.length > 0 && ( )}
{/* 설정 순서 안내 */}

설정 순서:

  1. 소스 테이블 (검색용): 모달에서 검색할 데이터 테이블
  2. 저장 테이블 (저장용): 선택한 항목을 저장할 테이블
  3. 반복 테이블 컬럼: 저장 테이블의 컬럼 선택 → 화면에 표시
  4. 컬럼 매핑 설정: 각 컬럼의 초기값을 어디서 가져올지 설정
  5. 계산 규칙: 자동 계산이 필요한 컬럼 설정 (선택사항)

실제 사용 예시:

  • 수주 등록 화면
  • - 소스 테이블: item_info (품목 검색)
  • - 저장 테이블: sales_order_mng (수주 저장)
  • - 컬럼: part_name, quantity, unit_price, amount...
  • - 매핑: item_name → part_name (소스에서 복사)
  • - 계산: amount = quantity * unit_price
{/* 동적 데이터 소스 설정 모달 */} 동적 데이터 소스 설정 {editingDynamicSourceColumnIndex !== null && localConfig.columns?.[editingDynamicSourceColumnIndex] && ( ({localConfig.columns[editingDynamicSourceColumnIndex].label}) )} 컬럼 헤더 클릭 시 선택할 수 있는 데이터 소스 옵션을 설정합니다 {editingDynamicSourceColumnIndex !== null && localConfig.columns?.[editingDynamicSourceColumnIndex] && (
{/* 옵션 목록 */}
{(localConfig.columns[editingDynamicSourceColumnIndex].dynamicDataSource?.options || []).map((option, optIndex) => (
옵션 {optIndex + 1} {localConfig.columns![editingDynamicSourceColumnIndex].dynamicDataSource?.defaultOptionId === option.id && ( 기본 )}
{localConfig.columns![editingDynamicSourceColumnIndex].dynamicDataSource?.defaultOptionId !== option.id && ( )}
{/* 옵션 라벨 */}
updateDynamicSourceOption(editingDynamicSourceColumnIndex, optIndex, { label: e.target.value })} placeholder="예: 거래처별 단가" className="h-8 text-xs" />
{/* 소스 타입 */}
{/* 테이블 직접 조회 설정 */} {option.sourceType === "table" && (

테이블 조회 설정

{/* 참조 테이블 */}
{/* 값 컬럼 */}
updateDynamicSourceOption(editingDynamicSourceColumnIndex, optIndex, { tableConfig: { ...option.tableConfig!, valueColumn: value }, })} />
{/* 조인 조건 */}
{(option.tableConfig?.joinConditions || []).map((cond, condIndex) => (
= { const newConditions = [...(option.tableConfig?.joinConditions || [])]; newConditions[condIndex] = { ...newConditions[condIndex], targetField: value }; updateDynamicSourceOption(editingDynamicSourceColumnIndex, optIndex, { tableConfig: { ...option.tableConfig!, joinConditions: newConditions }, }); }} />
))}
)} {/* 테이블 복합 조인 설정 (2개 이상 테이블) */} {option.sourceType === "multiTable" && (

복합 조인 설정

여러 테이블을 순차적으로 조인합니다

{/* 조인 체인 */}
{/* 시작점 안내 */}

시작: 현재 행 데이터

첫 번째 조인은 현재 행의 필드에서 시작합니다

{/* 조인 단계들 */} {(option.multiTableConfig?.joinChain || []).map((step, stepIndex) => (
{stepIndex + 1}
조인 단계 {stepIndex + 1}
{/* 조인할 테이블 */}
{/* 조인 조건 */}
{stepIndex === 0 ? ( ) : ( { const newChain = [...(option.multiTableConfig?.joinChain || [])]; newChain[stepIndex] = { ...newChain[stepIndex], joinCondition: { ...newChain[stepIndex].joinCondition, fromField: e.target.value } }; updateDynamicSourceOption(editingDynamicSourceColumnIndex, optIndex, { multiTableConfig: { ...option.multiTableConfig!, joinChain: newChain }, }); }} placeholder={option.multiTableConfig?.joinChain[stepIndex - 1]?.outputField || "이전 출력 필드"} className="h-8 text-xs" /> )}
=
{ const newChain = [...(option.multiTableConfig?.joinChain || [])]; newChain[stepIndex] = { ...newChain[stepIndex], joinCondition: { ...newChain[stepIndex].joinCondition, toField: value } }; updateDynamicSourceOption(editingDynamicSourceColumnIndex, optIndex, { multiTableConfig: { ...option.multiTableConfig!, joinChain: newChain }, }); }} />
{/* 다음 단계로 전달할 필드 */}
{ const newChain = [...(option.multiTableConfig?.joinChain || [])]; newChain[stepIndex] = { ...newChain[stepIndex], outputField: value }; updateDynamicSourceOption(editingDynamicSourceColumnIndex, optIndex, { multiTableConfig: { ...option.multiTableConfig!, joinChain: newChain }, }); }} />

{stepIndex < (option.multiTableConfig?.joinChain.length || 0) - 1 ? "다음 조인 단계에서 사용할 필드" : "마지막 단계면 비워두세요"}

{/* 조인 미리보기 */} {step.tableName && step.joinCondition.fromField && step.joinCondition.toField && (
{stepIndex === 0 ? "현재행" : option.multiTableConfig?.joinChain[stepIndex - 1]?.tableName} .{step.joinCondition.fromField} = {step.tableName} .{step.joinCondition.toField} {step.outputField && ( → {step.outputField} )}
)}
))} {/* 조인 체인이 없을 때 안내 */} {(!option.multiTableConfig?.joinChain || option.multiTableConfig.joinChain.length === 0) && (

조인 체인이 없습니다

"조인 추가" 버튼을 클릭하여 테이블 조인을 설정하세요

)}
{/* 최종 값 컬럼 */} {option.multiTableConfig?.joinChain && option.multiTableConfig.joinChain.length > 0 && (
{ updateDynamicSourceOption(editingDynamicSourceColumnIndex, optIndex, { multiTableConfig: { ...option.multiTableConfig!, valueColumn: value }, }); }} />

마지막 테이블에서 가져올 값

)} {/* 전체 조인 경로 미리보기 */} {option.multiTableConfig?.joinChain && option.multiTableConfig.joinChain.length > 0 && (

조인 경로 미리보기

{option.multiTableConfig.joinChain.map((step, idx) => (
{idx === 0 && ( <> 현재행 .{step.joinCondition.fromField} )} {step.tableName} .{step.joinCondition.toField} {step.outputField && idx < option.multiTableConfig!.joinChain.length - 1 && ( <> {step.outputField} )}
))} {option.multiTableConfig.valueColumn && (
최종 값: {option.multiTableConfig.joinChain[option.multiTableConfig.joinChain.length - 1]?.tableName}.{option.multiTableConfig.valueColumn}
)}
)}
)} {/* API 호출 설정 */} {option.sourceType === "api" && (

API 호출 설정

복잡한 다중 조인은 백엔드 API로 처리합니다

{/* API 엔드포인트 */}
updateDynamicSourceOption(editingDynamicSourceColumnIndex, optIndex, { apiConfig: { ...option.apiConfig!, endpoint: e.target.value }, })} placeholder="/api/price/customer" className="h-8 text-xs font-mono" />
{/* HTTP 메서드 */}
{/* 파라미터 매핑 */}
{(option.apiConfig?.parameterMappings || []).map((mapping, mapIndex) => (
{ const newMappings = [...(option.apiConfig?.parameterMappings || [])]; newMappings[mapIndex] = { ...newMappings[mapIndex], paramName: e.target.value }; updateDynamicSourceOption(editingDynamicSourceColumnIndex, optIndex, { apiConfig: { ...option.apiConfig!, parameterMappings: newMappings }, }); }} placeholder="파라미터명" className="h-7 text-[10px] flex-1" /> =
))}
{/* 응답 값 필드 */}
updateDynamicSourceOption(editingDynamicSourceColumnIndex, optIndex, { apiConfig: { ...option.apiConfig!, responseValueField: e.target.value }, })} placeholder="price (또는 data.price)" className="h-8 text-xs font-mono" />

API 응답에서 값을 가져올 필드 (중첩 경로 지원: data.price)

)}
))} {/* 옵션 추가 버튼 */}
{/* 안내 */}

사용 예시

  • - 거래처별 단가: customer_item_price 테이블에서 조회
  • - 품목별 단가: item_info 테이블에서 기준 단가 조회
  • - 계약 단가: 전용 API로 복잡한 조인 처리
)}
); }