"use client"; /** * UnifiedRepeater 설정 패널 * * 렌더링 모드별 설정: * - inline: 현재 화면 테이블 컬럼 직접 입력 * - modal: 엔티티 선택 + 추가 입력 (FK 저장 + 추가 컬럼 입력) */ import React, { useState, useEffect, useMemo, useCallback } from "react"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; import { Checkbox } from "@/components/ui/checkbox"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Database, Link2, Plus, Trash2, GripVertical, ArrowRight, Calculator, } from "lucide-react"; import { tableTypeApi } from "@/lib/api/screen"; import { cn } from "@/lib/utils"; import { UnifiedRepeaterConfig, RepeaterColumnConfig, DEFAULT_REPEATER_CONFIG, RENDER_MODE_OPTIONS, MODAL_SIZE_OPTIONS, COLUMN_WIDTH_OPTIONS, ColumnWidthOption, } from "@/types/unified-repeater"; interface UnifiedRepeaterConfigPanelProps { config: UnifiedRepeaterConfig; onChange: (config: UnifiedRepeaterConfig) => void; currentTableName?: string; screenTableName?: string; tableColumns?: any[]; } interface ColumnOption { columnName: string; displayName: string; inputType?: string; detailSettings?: { codeGroup?: string; referenceTable?: string; referenceColumn?: string; displayColumn?: string; format?: string; }; } interface EntityColumnOption { columnName: string; displayName: string; referenceTable?: string; referenceColumn?: string; displayColumn?: string; } interface CalculationRule { id: string; targetColumn: string; formula: string; label?: string; } export const UnifiedRepeaterConfigPanel: React.FC = ({ config: propConfig, onChange, currentTableName: propCurrentTableName, screenTableName, }) => { const currentTableName = screenTableName || propCurrentTableName; // config 안전하게 초기화 const config: UnifiedRepeaterConfig = useMemo(() => ({ ...DEFAULT_REPEATER_CONFIG, ...propConfig, renderMode: propConfig?.renderMode || DEFAULT_REPEATER_CONFIG.renderMode, dataSource: { ...DEFAULT_REPEATER_CONFIG.dataSource, ...propConfig?.dataSource, }, columns: propConfig?.columns || [], modal: { ...DEFAULT_REPEATER_CONFIG.modal, ...propConfig?.modal, }, features: { ...DEFAULT_REPEATER_CONFIG.features, ...propConfig?.features, }, }), [propConfig]); // 상태 관리 const [currentTableColumns, setCurrentTableColumns] = useState([]); // 현재 테이블 컬럼 const [entityColumns, setEntityColumns] = useState([]); // 엔티티 타입 컬럼 const [sourceTableColumns, setSourceTableColumns] = useState([]); // 소스(엔티티) 테이블 컬럼 const [calculationRules, setCalculationRules] = useState([]); const [loadingColumns, setLoadingColumns] = useState(false); const [loadingSourceColumns, setLoadingSourceColumns] = useState(false); // 설정 업데이트 헬퍼 const updateConfig = useCallback( (updates: Partial) => { onChange({ ...config, ...updates }); }, [config, onChange], ); const updateDataSource = useCallback( (field: string, value: any) => { updateConfig({ dataSource: { ...config.dataSource, [field]: value }, }); }, [config.dataSource, updateConfig], ); const updateModal = useCallback( (field: string, value: any) => { updateConfig({ modal: { ...config.modal, [field]: value }, }); }, [config.modal, updateConfig], ); const updateFeatures = useCallback( (field: string, value: boolean) => { updateConfig({ features: { ...config.features, [field]: value }, }); }, [config.features, updateConfig], ); // 현재 화면 테이블 컬럼 로드 + 엔티티 컬럼 감지 useEffect(() => { const loadCurrentTableColumns = async () => { if (!currentTableName) { setCurrentTableColumns([]); setEntityColumns([]); return; } setLoadingColumns(true); try { const columnData = await tableTypeApi.getColumns(currentTableName); const cols: ColumnOption[] = []; const entityCols: EntityColumnOption[] = []; for (const c of columnData) { // detailSettings 파싱 let detailSettings: any = null; if (c.detailSettings) { try { detailSettings = typeof c.detailSettings === "string" ? JSON.parse(c.detailSettings) : c.detailSettings; } catch (e) { console.warn("detailSettings 파싱 실패:", c.detailSettings); } } const col: ColumnOption = { columnName: c.columnName || c.column_name, displayName: c.displayName || c.columnLabel || c.columnName || c.column_name, inputType: c.inputType || c.input_type, detailSettings: detailSettings ? { codeGroup: detailSettings.codeGroup, referenceTable: detailSettings.referenceTable, referenceColumn: detailSettings.referenceColumn, displayColumn: detailSettings.displayColumn, format: detailSettings.format, } : undefined, }; cols.push(col); // 엔티티 타입 컬럼 감지 if (col.inputType === "entity") { const referenceTable = detailSettings?.referenceTable || c.referenceTable; const referenceColumn = detailSettings?.referenceColumn || c.referenceColumn || "id"; const displayColumn = detailSettings?.displayColumn || c.displayColumn; if (referenceTable) { entityCols.push({ columnName: col.columnName, displayName: col.displayName, referenceTable, referenceColumn, displayColumn, }); } } } setCurrentTableColumns(cols); setEntityColumns(entityCols); } catch (error) { console.error("현재 테이블 컬럼 로드 실패:", error); setCurrentTableColumns([]); setEntityColumns([]); } finally { setLoadingColumns(false); } }; loadCurrentTableColumns(); }, [currentTableName]); // 소스(엔티티) 테이블 컬럼 로드 (모달 모드일 때) useEffect(() => { const loadSourceTableColumns = async () => { const sourceTable = config.dataSource?.sourceTable; if (!sourceTable) { setSourceTableColumns([]); return; } setLoadingSourceColumns(true); try { const columnData = await tableTypeApi.getColumns(sourceTable); const cols: ColumnOption[] = columnData.map((c: any) => ({ columnName: c.columnName || c.column_name, displayName: c.displayName || c.columnLabel || c.columnName || c.column_name, inputType: c.inputType || c.input_type, })); setSourceTableColumns(cols); } catch (error) { console.error("소스 테이블 컬럼 로드 실패:", error); setSourceTableColumns([]); } finally { setLoadingSourceColumns(false); } }; if (config.renderMode === "modal") { loadSourceTableColumns(); } }, [config.dataSource?.sourceTable, config.renderMode]); // 컬럼 토글 (현재 테이블 컬럼 - 입력용) const toggleInputColumn = (column: ColumnOption) => { const existingIndex = config.columns.findIndex((c) => c.key === column.columnName); if (existingIndex >= 0) { const newColumns = config.columns.filter((c) => c.key !== column.columnName); updateConfig({ columns: newColumns }); } else { // 컬럼의 inputType과 detailSettings 정보 포함 const newColumn: RepeaterColumnConfig = { key: column.columnName, title: column.displayName, width: "auto", visible: true, editable: true, inputType: column.inputType || "text", detailSettings: column.detailSettings ? { codeGroup: column.detailSettings.codeGroup, referenceTable: column.detailSettings.referenceTable, referenceColumn: column.detailSettings.referenceColumn, displayColumn: column.detailSettings.displayColumn, format: column.detailSettings.format, } : undefined, }; updateConfig({ columns: [...config.columns, newColumn] }); } }; // 소스 컬럼 토글 (모달에 표시용 - 라벨 포함) const toggleSourceDisplayColumn = (column: ColumnOption) => { const sourceDisplayColumns = config.modal?.sourceDisplayColumns || []; const exists = sourceDisplayColumns.some(c => c.key === column.columnName); if (exists) { updateModal("sourceDisplayColumns", sourceDisplayColumns.filter(c => c.key !== column.columnName)); } else { updateModal("sourceDisplayColumns", [ ...sourceDisplayColumns, { key: column.columnName, label: column.displayName } ]); } }; const isColumnAdded = (columnName: string) => { return config.columns.some((c) => c.key === columnName); }; const isSourceColumnSelected = (columnName: string) => { return (config.modal?.sourceDisplayColumns || []).some(c => c.key === columnName); }; // 컬럼 속성 업데이트 const updateColumnProp = (key: string, field: keyof RepeaterColumnConfig, value: any) => { const newColumns = config.columns.map((col) => (col.key === key ? { ...col, [field]: value } : col)); updateConfig({ columns: newColumns }); }; // 계산 규칙 추가 const addCalculationRule = () => { setCalculationRules(prev => [ ...prev, { id: `calc_${Date.now()}`, targetColumn: "", formula: "" } ]); }; // 계산 규칙 삭제 const removeCalculationRule = (id: string) => { setCalculationRules(prev => prev.filter(r => r.id !== id)); }; // 계산 규칙 업데이트 const updateCalculationRule = (id: string, field: keyof CalculationRule, value: string) => { setCalculationRules(prev => prev.map(r => r.id === id ? { ...r, [field]: value } : r) ); }; // 엔티티 컬럼 선택 시 소스 테이블 자동 설정 const handleEntityColumnSelect = (columnName: string) => { const selectedEntity = entityColumns.find(c => c.columnName === columnName); if (selectedEntity) { console.log("엔티티 컬럼 선택:", selectedEntity); // 소스 테이블 컬럼에서 라벨 정보 찾기 const displayColInfo = sourceTableColumns.find(c => c.columnName === selectedEntity.displayColumn); const displayLabel = displayColInfo?.displayName || selectedEntity.displayColumn || ""; updateConfig({ dataSource: { ...config.dataSource, sourceTable: selectedEntity.referenceTable || "", foreignKey: selectedEntity.columnName, referenceKey: selectedEntity.referenceColumn || "id", displayColumn: selectedEntity.displayColumn, }, modal: { ...config.modal, searchFields: selectedEntity.displayColumn ? [selectedEntity.displayColumn] : [], // 라벨 포함 형식으로 저장 sourceDisplayColumns: selectedEntity.displayColumn ? [{ key: selectedEntity.displayColumn, label: displayLabel }] : [], }, }); } }; // 모드 여부 const isInlineMode = config.renderMode === "inline"; const isModalMode = config.renderMode === "modal"; // 엔티티 컬럼 제외한 입력 가능 컬럼 (FK 컬럼 제외) const inputableColumns = useMemo(() => { const fkColumn = config.dataSource?.foreignKey; return currentTableColumns.filter(col => col.columnName !== fkColumn && // FK 컬럼 제외 col.inputType !== "entity" // 다른 엔티티 컬럼도 제외 (필요시) ); }, [currentTableColumns, config.dataSource?.foreignKey]); return (
기본 컬럼 모달 {/* 기본 설정 탭 */} {/* 렌더링 모드 */}
{/* 현재 화면 정보 */}
{currentTableName ? (

{currentTableName}

컬럼 {currentTableColumns.length}개 / 엔티티 {entityColumns.length}개

) : (

화면에 테이블을 먼저 설정해주세요

)}
{/* 모달 모드: 엔티티 컬럼 선택 */} {isModalMode && ( <>

모달에서 검색할 엔티티를 선택하세요 (FK만 저장됨)

{entityColumns.length > 0 ? ( ) : (

{loadingColumns ? "로딩 중..." : "엔티티 타입 컬럼이 없습니다"}

)} {/* 선택된 엔티티 정보 */} {config.dataSource?.sourceTable && (

선택된 엔티티

검색 테이블: {config.dataSource.sourceTable}

저장 컬럼: {config.dataSource.foreignKey} (FK)

)}
)} {/* 기능 옵션 */}
updateFeatures("showAddButton", !!checked)} />
updateFeatures("showDeleteButton", !!checked)} />
updateFeatures("inlineEdit", !!checked)} />
updateFeatures("multiSelect", !!checked)} />
updateFeatures("showRowNumber", !!checked)} />
updateFeatures("selectable", !!checked)} />
{/* 컬럼 설정 탭 */} {/* 모달 모드: 모달에 표시할 컬럼 */} {isModalMode && config.dataSource?.sourceTable && ( <>

검색 모달에서 보여줄 컬럼 (보기용)

{loadingSourceColumns ? (

로딩 중...

) : sourceTableColumns.length === 0 ? (

컬럼 정보가 없습니다

) : (
{sourceTableColumns.map((column) => (
toggleSourceDisplayColumn(column)} > toggleSourceDisplayColumn(column)} className="pointer-events-none h-3.5 w-3.5" /> {column.displayName}
))}
)}
)} {/* 추가 입력 컬럼 (현재 테이블에서 FK 제외) */}

{isModalMode ? "엔티티 선택 후 추가로 입력받을 컬럼 (수량, 단가 등)" : "직접 입력받을 컬럼을 선택하세요" }

{loadingColumns ? (

로딩 중...

) : inputableColumns.length === 0 ? (

{isModalMode ? "추가 입력 가능한 컬럼이 없습니다" : "컬럼 정보가 없습니다"}

) : (
{inputableColumns.map((column) => (
toggleInputColumn(column)} > toggleInputColumn(column)} className="pointer-events-none h-3.5 w-3.5" /> {column.displayName} {column.inputType}
))}
)}
{/* 선택된 컬럼 상세 설정 */} {config.columns.length > 0 && ( <>
{config.columns.map((col) => (
updateColumnProp(col.key, "title", e.target.value)} placeholder="제목" className="h-6 flex-1 text-xs" /> updateColumnProp(col.key, "editable", !!checked)} title="편집 가능" />
))}
)} {/* 계산 규칙 */} {(isModalMode || isInlineMode) && config.columns.length > 0 && ( <>

예: 금액 = 수량 * 단가

{calculationRules.map((rule) => (
= updateCalculationRule(rule.id, "formula", e.target.value)} placeholder="quantity * unit_price" className="h-7 flex-1 text-xs" />
))} {calculationRules.length === 0 && (

계산 규칙이 없습니다

)}
)}
{/* 모달 설정 탭 */} {isModalMode ? ( <> {/* 모달 크기 */}
{/* 모달 제목 */}
updateModal("title", e.target.value)} placeholder="예: 품목 검색" className="h-8 text-xs" />
{/* 버튼 텍스트 */}
updateModal("buttonText", e.target.value)} placeholder="예: 품목 검색" className="h-8 text-xs" />
{/* 검색 필드 */}

검색어 입력 시 검색할 필드

{sourceTableColumns.map((column) => { const searchFields = config.modal?.searchFields || []; const isChecked = searchFields.includes(column.columnName); return (
{ if (isChecked) { updateModal("searchFields", searchFields.filter(f => f !== column.columnName)); } else { updateModal("searchFields", [...searchFields, column.columnName]); } }} > {column.displayName}
); })}
) : (

모달 또는 혼합 모드에서만 설정할 수 있습니다

)}
); }; UnifiedRepeaterConfigPanel.displayName = "UnifiedRepeaterConfigPanel"; export default UnifiedRepeaterConfigPanel;