"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 } from "./types"; 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); // 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 }); }; 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) => (
))}
{/* 검색 필드 */}

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

{(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 }) } />
{/* 반복 테이블 컬럼 관리 */}

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" && (

화면에서 입력 (수동)

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

)}
))}
)}
{/* 계산 규칙 (자동 계산) */}

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
); }