"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 { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; import { Plus, X, Check, ChevronsUpDown } from "lucide-react"; import { SimpleRepeaterTableProps, SimpleRepeaterColumnConfig, CalculationRule, ColumnSourceConfig, ColumnTargetConfig, InitialDataConfig, DataFilterCondition, SummaryConfig, SummaryFieldConfig, } from "./types"; import { tableManagementApi } from "@/lib/api/tableManagement"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { cn } from "@/lib/utils"; interface SimpleRepeaterTableConfigPanelProps { config: Partial; onChange: (config: Partial) => void; onConfigChange?: (config: Partial) => void; // 하위 호환성 } // 🆕 검색 가능한 컬럼 선택기 (Combobox) function SourceColumnSelector({ sourceTable, value, onChange, showTableName = false, }: { sourceTable: string; value: string; onChange: (value: string) => void; showTableName?: boolean; }) { const [columns, setColumns] = useState<{ columnName: string; displayName?: string }[]>([]); const [isLoading, setIsLoading] = useState(false); const [open, setOpen] = 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]); const selectedColumn = columns.find((col) => col.columnName === value); const displayText = selectedColumn ? showTableName ? `${sourceTable}.${selectedColumn.displayName || selectedColumn.columnName}` : selectedColumn.displayName || selectedColumn.columnName : "컬럼 선택"; return ( 컬럼을 찾을 수 없습니다. {columns.map((col) => { const label = showTableName ? `${sourceTable}.${col.displayName || col.columnName}` : col.displayName || col.columnName; return ( { onChange(col.columnName); setOpen(false); }} className="text-xs sm:text-sm" >
{label} {col.displayName && col.displayName !== col.columnName && ( {col.columnName} )}
); })}
); } // 🆕 FormData 필드 선택기 (Combobox) function FormDataFieldSelector({ sourceTable, value, onChange, }: { sourceTable: string; value: string; onChange: (value: string) => void; }) { const [columns, setColumns] = useState<{ columnName: string; displayName?: string }[]>([]); const [isLoading, setIsLoading] = useState(false); const [open, setOpen] = 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]); const selectedColumn = columns.find((col) => col.columnName === value); const displayText = selectedColumn ? `formData["${selectedColumn.columnName}"]` : value ? `formData["${value}"]` : "formData 필드 선택"; return ( 필드를 찾을 수 없습니다. {columns.map((col) => { return ( { onChange(col.columnName); setOpen(false); }} className="text-xs" >
formData["{col.columnName}"] {col.displayName && ( {col.displayName} )}
); })}
); } export function SimpleRepeaterTableConfigPanel({ config, onChange, onConfigChange, }: SimpleRepeaterTableConfigPanelProps) { // 하위 호환성: onConfigChange가 있으면 사용, 없으면 onChange 사용 const handleConfigChange = onConfigChange || onChange; const [localConfig, setLocalConfig] = useState(config); const [allTables, setAllTables] = useState<{ tableName: string; displayName?: string }[]>([]); const [isLoadingTables, setIsLoadingTables] = useState(false); // config 변경 시 localConfig 동기화 useEffect(() => { setLocalConfig(config); }, [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(); }, []); // 🆕 즉시 업데이트 (Select, Switch 등) const updateConfig = (updates: Partial) => { const newConfig = { ...localConfig, ...updates }; setLocalConfig(newConfig); handleConfigChange(newConfig); }; // 컬럼 추가 const addColumn = () => { const columns = localConfig.columns || []; const newColumn: SimpleRepeaterColumnConfig = { field: "", label: "", type: "text", editable: true, width: "150px", sourceConfig: { type: "manual", }, targetConfig: { saveEnabled: true, }, }; updateConfig({ columns: [...columns, newColumn] }); }; // 컬럼 삭제 const removeColumn = (index: number) => { const columns = [...(localConfig.columns || [])]; columns.splice(index, 1); updateConfig({ columns }); }; // 컬럼 수정 const updateColumn = (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) { 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) { 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 addFilterCondition = () => { const initialConfig = localConfig.initialDataConfig || {} as InitialDataConfig; const conditions = initialConfig.filterConditions || []; const newCondition: DataFilterCondition = { field: "", operator: "=", value: "", }; updateConfig({ initialDataConfig: { ...initialConfig, filterConditions: [...conditions, newCondition], }, }); }; // 🆕 초기 데이터 필터 조건 수정 const updateFilterCondition = (index: number, updates: Partial) => { const initialConfig = localConfig.initialDataConfig || {} as InitialDataConfig; const conditions = [...(initialConfig.filterConditions || [])]; conditions[index] = { ...conditions[index], ...updates }; updateConfig({ initialDataConfig: { ...initialConfig, filterConditions: conditions, }, }); }; // 🆕 초기 데이터 필터 조건 삭제 const removeFilterCondition = (index: number) => { const initialConfig = localConfig.initialDataConfig || {} as InitialDataConfig; const conditions = [...(initialConfig.filterConditions || [])]; conditions.splice(index, 1); updateConfig({ initialDataConfig: { ...initialConfig, filterConditions: conditions, }, }); }; return (
{/* 기본 설정 */}

기본 설정

updateConfig({ readOnly: checked })} />

활성화 시 모든 필드가 편집 불가능합니다

updateConfig({ showRowNumber: checked })} />

테이블 왼쪽에 행 번호를 표시합니다

updateConfig({ allowDelete: checked })} />

각 행에 삭제 버튼을 표시합니다

updateConfig({ allowAdd: checked })} />

사용자가 새 행을 추가할 수 있습니다

{localConfig.allowAdd && ( <>
updateConfig({ addButtonText: e.target.value })} placeholder="행 추가" className="h-8 text-xs sm:h-10 sm:text-sm" />
updateConfig({ minRows: parseInt(e.target.value) || 0 })} className="h-8 text-xs sm:h-10 sm:text-sm" />

0이면 제한 없음

updateConfig({ maxRows: e.target.value ? parseInt(e.target.value) : undefined })} placeholder="무제한" className="h-8 text-xs sm:h-10 sm:text-sm" />

비워두면 무제한

)}
updateConfig({ maxHeight: e.target.value })} placeholder="240px" className="h-8 text-xs sm:h-10 sm:text-sm" />

테이블의 최대 높이 (예: 240px, 400px, 50vh)

{/* 🆕 초기 데이터 로드 설정 */}

🔄 초기 데이터 로드 설정

컴포넌트가 로드될 때 어떤 테이블에서 어떤 조건으로 데이터를 가져올지 설정

선택 안 하면 빈 테이블로 시작합니다 (새 데이터 입력용)

{localConfig.initialDataConfig?.sourceTable && ( <>

데이터를 필터링할 조건 (예: order_no = 선택한 수주번호)

{localConfig.initialDataConfig?.filterConditions && localConfig.initialDataConfig.filterConditions.length > 0 && (
{localConfig.initialDataConfig.filterConditions.map((condition, index) => (
조건 {index + 1}
updateFilterCondition(index, { field: value })} showTableName={true} />
updateFilterCondition(index, { valueFromField: value })} />

선택한 수주번호, 품목코드 등을 formData에서 가져옴

{condition.field && condition.valueFromField && (
WHERE {condition.field} {condition.operator} formData["{condition.valueFromField}"]
)}
))}
)} {(!localConfig.initialDataConfig?.filterConditions || localConfig.initialDataConfig.filterConditions.length === 0) && (

필터 조건이 없습니다. 모든 데이터를 가져옵니다.

)}

사용 예시

• 수주관리 리스트에서 클릭한 수주번호(order_no)를 기준으로 데이터 로드

• 필터 조건: order_no = formData["order_no"]

)}
{/* 컬럼 설정 */}

컬럼 설정

테이블에 표시할 컬럼을 설정합니다

{localConfig.columns && localConfig.columns.length > 0 ? (
{localConfig.columns.map((col, index) => (

컬럼 {index + 1}

{/* 필드명 */}
{localConfig.initialDataConfig?.sourceTable ? ( { // 필드명 선택 시 자동으로 라벨도 업데이트 try { const response = await tableManagementApi.getColumnList(localConfig.initialDataConfig!.sourceTable); if (response.success && response.data) { const selectedCol = response.data.columns.find((c: any) => c.columnName === value); if (selectedCol) { updateColumn(index, { field: value, label: selectedCol.displayName || value }); } else { updateColumn(index, { field: value }); } } } catch (error) { console.error("컬럼 정보 로드 실패:", error); updateColumn(index, { field: value }); } }} showTableName={false} /> ) : ( updateColumn(index, { field: e.target.value })} placeholder="먼저 소스 테이블을 선택하세요" className="h-8 text-xs sm:h-10 sm:text-sm" disabled /> )}

{localConfig.initialDataConfig?.sourceTable ? "데이터 객체의 필드명 (소스 테이블 컬럼에서 선택)" : "초기 데이터 로드 설정에서 소스 테이블을 먼저 선택하세요"}

{/* 라벨 */}
updateColumn(index, { label: e.target.value })} placeholder="테이블 헤더명" className="h-8 text-xs sm:h-10 sm:text-sm" />

테이블 헤더에 표시될 이름 (필드 선택 시 자동으로 입력됨)

{/* 타입 */}

입력 필드의 타입

{/* Select 옵션 (타입이 select일 때만) */} {col.type === "select" && (
{(col.selectOptions || []).map((option, optIndex) => (
{ const newOptions = [...(col.selectOptions || [])]; newOptions[optIndex] = { ...newOptions[optIndex], value: e.target.value }; updateColumn(index, { selectOptions: newOptions }); }} placeholder="값" className="h-8 text-xs flex-1" /> { const newOptions = [...(col.selectOptions || [])]; newOptions[optIndex] = { ...newOptions[optIndex], label: e.target.value }; updateColumn(index, { selectOptions: newOptions }); }} placeholder="라벨" className="h-8 text-xs flex-1" />
))}
)} {/* 🆕 데이터 소스 설정 (어디서 조회할지) */}

{col.sourceConfig?.type === "direct" && "소스 테이블에서 직접 가져오기"} {col.sourceConfig?.type === "join" && "다른 테이블과 조인하여 가져오기"} {col.sourceConfig?.type === "manual" && "사용자가 직접 입력"}

{col.sourceConfig?.type === "direct" && (

직접 조회 설정

값을 가져올 테이블

updateColumn(index, { sourceConfig: { ...col.sourceConfig, type: "direct", sourceColumn: value } as ColumnSourceConfig })} showTableName={true} />

가져올 컬럼명

)} {col.sourceConfig?.type === "join" && (

조인 조회 설정

updateColumn(index, { sourceConfig: { ...col.sourceConfig, type: "join", joinKey: value } as ColumnSourceConfig })} showTableName={true} />

현재 테이블에서 조인에 사용할 컬럼 (예: sales_order_id)

updateColumn(index, { sourceConfig: { ...col.sourceConfig, type: "join", joinRefKey: value } as ColumnSourceConfig })} showTableName={true} />

조인 테이블에서 매칭할 컬럼 (예: id)

updateColumn(index, { sourceConfig: { ...col.sourceConfig, type: "join", joinColumn: value } as ColumnSourceConfig })} showTableName={true} />

조인 조건은 향후 추가 예정

현재는 기본 키 기반 자동 조인

)}
{/* 🆕 데이터 타겟 설정 - 부모-자식 모드면 숨김 */} {localConfig.parentChildConfig?.enabled ? ( // 부모-자식 모드: 간단한 안내만 표시

부모-자식 모드
{localConfig.parentChildConfig.childTable || "자식 테이블"}.{col.field || "필드명"} 에 저장

) : ( // 일반 모드: 타겟 설정 (선택사항)

선택 안 하면 이 컬럼은 저장되지 않습니다

{col.targetConfig?.targetTable && col.targetConfig.targetTable !== "" && (
updateColumn(index, { targetConfig: { ...col.targetConfig, targetColumn: value } as ColumnTargetConfig })} showTableName={true} />
)}
)} {/* 편집 가능 여부 */}
updateColumn(index, { editable: checked })} disabled={col.calculated} />

{col.calculated ? "계산 필드는 자동으로 편집 불가능합니다" : "비활성화 시 해당 컬럼은 읽기 전용입니다"}

{/* 필수 입력 */}
updateColumn(index, { required: checked })} />

활성화 시 헤더에 * 표시

{/* 컬럼 너비 */}

컬럼의 표시 너비 선택

{/* 기본값 */}
updateColumn(index, { defaultValue: e.target.value })} placeholder="기본값 (선택사항)" className="h-8 text-xs sm:h-10 sm:text-sm" />

새 행 추가 시 자동으로 입력될 값

))}
) : (

컬럼이 없습니다

테이블에 표시할 컬럼을 추가하세요

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

계산 규칙 (자동 계산)

반복 테이블 컬럼들을 조합하여 자동 계산 (예: 수량 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 && ( )}
{/* 합계 설정 */}

합계 설정

테이블 하단에 합계를 표시합니다

updateConfig({ summaryConfig: { ...localConfig.summaryConfig, enabled: checked, fields: localConfig.summaryConfig?.fields || [], } })} />
{localConfig.summaryConfig?.enabled && ( <>
updateConfig({ summaryConfig: { ...localConfig.summaryConfig, enabled: true, title: e.target.value, fields: localConfig.summaryConfig?.fields || [], } })} placeholder="합계" className="h-8 text-xs sm:h-10 sm:text-sm" />
{localConfig.summaryConfig?.fields && localConfig.summaryConfig.fields.length > 0 ? (
{localConfig.summaryConfig.fields.map((field, index) => (
합계 필드 {index + 1}
{ const fields = [...(localConfig.summaryConfig?.fields || [])]; fields[index] = { ...fields[index], label: e.target.value }; updateConfig({ summaryConfig: { ...localConfig.summaryConfig, enabled: true, fields, } }); }} placeholder="합계 라벨" className="h-8 text-xs" />
{ const fields = [...(localConfig.summaryConfig?.fields || [])]; fields[index] = { ...fields[index], highlight: checked }; updateConfig({ summaryConfig: { ...localConfig.summaryConfig, enabled: true, fields, } }); }} />
))}
) : (

합계 필드를 추가하세요

)}

사용 예시

• 공급가액 합계: supply_amount 필드의 SUM

• 세액 합계: tax_amount 필드의 SUM

• 총액: supply_amount + tax_amount (수식 필드)

)}
{/* 사용 안내 */}

SimpleRepeaterTable 사용법:

  • 주어진 데이터를 표시하고 편집하는 경량 테이블입니다
  • 행 추가 허용 옵션으로 사용자가 새 행을 추가할 수 있습니다
  • 주로 EditModal과 함께 사용되며, 선택된 데이터를 일괄 수정할 때 유용합니다
  • readOnly 옵션으로 전체 테이블을 읽기 전용으로 만들 수 있습니다
  • 자동 계산 규칙을 통해 수량 * 단가 = 금액 같은 계산을 자동화할 수 있습니다
  • 합계 설정으로 테이블 하단에 합계/평균 등을 표시할 수 있습니다
); }