"use client"; /** * 통합 설정 패널 (UnifiedConfigPanel) - Phase C: 스마트 Config * - 테이블 선택 → 컬럼 자동 로드 → webType 자동 감지 * - 메타 컴포넌트 타입별 심플/고급 모드 * - API 클라이언트 사용 (fetch 금지) */ import React, { useState, useEffect } from "react"; import { cn } from "@/lib/utils"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; import { Badge } from "@/components/ui/badge"; import { Checkbox } from "@/components/ui/checkbox"; import { Settings2, Eye, EyeOff, Plus, Trash2, RefreshCw, Database, Palette, Link2, Settings, Wand2, SlidersHorizontal } from "lucide-react"; import { MetaComponent, getFieldConfig } from "@/lib/api/metaComponent"; import { apiClient } from "@/lib/api/client"; import { toast } from "sonner"; interface UnifiedConfigPanelProps { component: MetaComponent | null; onChange: (config: any) => void; className?: string; } // 테이블 목록, 컬럼 목록 타입 interface TableInfo { tableName: string; displayName?: string; description?: string; } interface ColumnInfo { columnName: string; dataType: string; comment?: string; } // DB 타입 → webType 자동 매핑 function inferWebType(dbType: string): string { const type = dbType.toLowerCase(); if (type.includes("varchar") || type.includes("text") || type.includes("char")) return "text"; if (type.includes("int") || type.includes("serial") || type.includes("bigint")) return "number"; if (type.includes("numeric") || type.includes("decimal") || type.includes("float") || type.includes("double")) return "number"; if (type.includes("bool")) return "checkbox"; if (type === "date") return "date"; if (type.includes("timestamp")) return "datetime"; if (type.includes("json")) return "textarea"; return "text"; } export function UnifiedConfigPanel({ component, onChange, className }: UnifiedConfigPanelProps) { const [isAdvanced, setIsAdvanced] = useState(false); // Phase C: 테이블/컬럼 자동 로드 상태 const [tables, setTables] = useState([]); const [columns, setColumns] = useState([]); const [loadingTables, setLoadingTables] = useState(false); const [loadingColumns, setLoadingColumns] = useState(false); // Phase C: 컴포넌트 마운트 시 테이블 목록 로드 useEffect(() => { loadTables(); }, []); // Phase C: 테이블 선택 시 컬럼 로드 useEffect(() => { if (component?.config?.tableName) { loadColumns(component.config.tableName); } }, [component?.config?.tableName]); // 테이블 목록 가져오기 const loadTables = async () => { setLoadingTables(true); try { const response = await apiClient.get("/table-management/tables"); const data = response.data?.data || []; setTables(data); } catch (error: any) { console.error("테이블 목록 로드 실패:", error); toast.error("테이블 목록을 불러올 수 없습니다"); } finally { setLoadingTables(false); } }; // 특정 테이블의 컬럼 목록 가져오기 const loadColumns = async (tableName: string) => { if (!tableName) return; setLoadingColumns(true); try { const response = await apiClient.get(`/table-management/tables/${tableName}/columns?size=1000`); const data = response.data?.data || response.data; const cols = data.columns || data || []; setColumns(cols); } catch (error: any) { console.error("컬럼 목록 로드 실패:", error); toast.error("컬럼 목록을 불러올 수 없습니다"); setColumns([]); } finally { setLoadingColumns(false); } }; if (!component) { return (

컴포넌트를 선택하세요

캔버스에서 컴포넌트를 클릭하면 여기에서 설정할 수 있습니다

); } const { type, config } = component; // 설정 변경 핸들러 const handleConfigChange = (key: string, value: any) => { onChange({ ...config, [key]: value }); }; // 중첩 설정 변경 핸들러 (예: text.content) const handleNestedChange = (path: string[], value: any) => { const newConfig = { ...config }; let current: any = newConfig; for (let i = 0; i < path.length - 1; i++) { if (!current[path[i]]) current[path[i]] = {}; current = current[path[i]]; } current[path[path.length - 1]] = value; onChange(newConfig); }; // 공통 기본 설정 (모든 컴포넌트) const renderBasicTab = () => ( {/* 컴포넌트 유형 섹션 */}
{type}
{/* Field 컴포넌트 설정 - Phase C: 스마트 설정 */} {type === "meta-field" && ( <> {/* 심플 모드: 테이블 → 컬럼 → 라벨 → webType */}
{(config as any).tableName && (
)}
handleConfigChange("label", e.target.value)} placeholder="예: 고객명, 주문번호..." className="h-8 text-xs sm:h-10 sm:text-sm" />
{isAdvanced && ( <>
handleConfigChange("placeholder", e.target.value)} placeholder="입력 필드에 표시될 힌트 텍스트" className="h-8 text-xs sm:h-10 sm:text-sm" />
handleConfigChange("required", checked)} />
handleConfigChange("readonly", checked)} />
)} )} {/* DataView 컴포넌트 설정 - Phase C */} {type === "meta-dataview" && ( <> {/* 심플 모드: 테이블 → 페이지 크기 */}
handleConfigChange("pageSize", parseInt(e.target.value) || 10)} placeholder="10" className="h-8 text-xs sm:h-10 sm:text-sm" />
{isAdvanced && (config as any).tableName && ( <>

체크박스로 표시할 컬럼을 선택하세요

{loadingColumns ? (

로딩 중...

) : columns.length === 0 ? (

컬럼이 없습니다

) : ( columns.map((col) => { const selectedColumns = ((config as any).columns || "").split(",").filter(Boolean); const isChecked = selectedColumns.includes(col.columnName); return (
{ let updatedColumns = [...selectedColumns]; if (checked) { updatedColumns.push(col.columnName); } else { updatedColumns = updatedColumns.filter((c) => c !== col.columnName); } handleConfigChange("columns", updatedColumns.join(",")); }} />
); }) )}
)} )} {/* Action 컴포넌트 설정 - Phase C */} {type === "meta-action" && ( <> {/* 심플 모드: 액션 유형 → 대상 테이블 → 버튼 텍스트 → 버튼 스타일 */}
handleConfigChange("label", e.target.value)} placeholder="예: 저장, 삭제, 조회..." className="h-8 text-xs sm:h-10 sm:text-sm" />
{isAdvanced && ( <>
handleConfigChange("confirmDialog", checked)} />
)} )} {/* Layout 컴포넌트 설정 */} {type === "meta-layout" && ( <>
{isAdvanced && ( <>
handleConfigChange("gap", parseInt(e.target.value))} placeholder="4" className="h-8 text-xs sm:h-10 sm:text-sm" />
handleConfigChange("padding", parseInt(e.target.value))} placeholder="4" className="h-8 text-xs sm:h-10 sm:text-sm" />
handleConfigChange("bordered", checked)} />
)} )} {/* Display 컴포넌트 설정 */} {type === "meta-display" && ( <>
{(config as any).displayType === "text" && (
handleNestedChange(["text", "content"], e.target.value)} placeholder="표시할 텍스트를 입력하세요" className="h-8 text-xs sm:h-10 sm:text-sm" />
)} {(config as any).displayType === "heading" && ( <>
handleNestedChange(["heading", "content"], e.target.value)} placeholder="제목 텍스트를 입력하세요" className="h-8 text-xs sm:h-10 sm:text-sm" />
)} {(config as any).displayType === "stat" && ( <>
handleNestedChange(["stat", "value"], e.target.value)} placeholder="예: 1,234" className="h-8 text-xs sm:h-10 sm:text-sm" />
handleNestedChange(["stat", "label"], e.target.value)} placeholder="예: 총 판매액" className="h-8 text-xs sm:h-10 sm:text-sm" />
)} )} {/* Search 컴포넌트 설정 - Phase C */} {type === "meta-search" && ( <> {/* 심플 모드: 대상 DataView → 검색 필드 추가 */}
handleConfigChange("targetDataView", e.target.value)} placeholder="예: dataview-1" className="h-8 text-xs sm:h-10 sm:text-sm" />

같은 화면의 DataView 컴포넌트 ID를 입력하세요

검색할 컬럼을 추가하세요

{((config as any).searchFields || []).map((field: any, idx: number) => (
필드 {idx + 1}
{ const fields = [...(config as any).searchFields]; fields[idx].column = e.target.value; handleConfigChange("searchFields", fields); }} placeholder="컬럼명 (예: customer_name)" className="h-8 text-xs sm:h-10 sm:text-sm" /> { const fields = [...(config as any).searchFields]; fields[idx].label = e.target.value; handleConfigChange("searchFields", fields); }} placeholder="라벨 (예: 고객명)" className="h-8 text-xs sm:h-10 sm:text-sm" /> {isAdvanced && ( )}
))} )} {/* Modal 컴포넌트 설정 */} {type === "meta-modal" && ( <>
{(config as any).trigger === "button" && (
handleConfigChange("triggerLabel", e.target.value)} placeholder="예: 상세보기, 추가..." className="h-8 text-xs sm:h-10 sm:text-sm" />
)}
)}
); // 데이터 탭 const renderDataTab = () => { // DB 설정 가져오기 핸들러 (기존 유지) const handleFetchFieldConfig = async () => { const tableName = (config as any).tableName; const columnName = (config as any).binding; if (!tableName || !columnName) { toast.error("테이블명과 컬럼명을 먼저 입력하세요"); return; } try { const response = await getFieldConfig(tableName, columnName); if (response.success && response.data) { // DB 설정으로 config 자동 채우기 const dbConfig = response.data; onChange({ ...config, webType: dbConfig.webType || (config as any).webType, label: dbConfig.label || (config as any).label, required: dbConfig.required !== undefined ? dbConfig.required : (config as any).required, options: dbConfig.options || (config as any).options, join: dbConfig.join || (config as any).join, }); toast.success("DB 설정을 가져왔습니다"); } else { toast.error(response.error || "설정을 가져올 수 없습니다"); } } catch (error: any) { toast.error("DB 설정 가져오기 실패: " + error.message); } }; return ( {/* meta-field */} {type === "meta-field" && ( <>
handleConfigChange("defaultValue", e.target.value)} placeholder="필드 초기값" className="h-8 text-xs sm:h-10 sm:text-sm" />
)} {/* meta-dataview - Phase C: 데이터 탭에서는 정렬/페이징만 */} {type === "meta-dataview" && ( <>
handleConfigChange("defaultSortColumn", e.target.value)} placeholder="예: created_at" className="h-8 text-xs sm:h-10 sm:text-sm" />
)} {/* meta-modal (form 타입) - Phase C: 심플하게 */} {type === "meta-modal" && ( <>
{isAdvanced && ( <>

폼에 표시할 필드를 추가하세요

{((config as any).formColumns || []).map((col: any, idx: number) => (
컬럼 {idx + 1}
{ const cols = [...(config as any).formColumns]; cols[idx].column = e.target.value; handleConfigChange("formColumns", cols); }} placeholder="컬럼명" className="h-8 text-xs sm:h-10 sm:text-sm" /> { const cols = [...(config as any).formColumns]; cols[idx].label = e.target.value; handleConfigChange("formColumns", cols); }} placeholder="라벨" className="h-8 text-xs sm:h-10 sm:text-sm" />
))} )} )} {/* 기타 컴포넌트 - 데이터 설정 불필요 */} {!["meta-field", "meta-dataview", "meta-modal"].includes(type) && (

데이터 설정 불필요

이 컴포넌트는 데이터베이스 설정이 필요하지 않습니다.

)}
); }; // 표시 탭 const renderDisplayTab = () => ( {/* 공통 크기 설정 섹션 */}
handleConfigChange("width", e.target.value)} placeholder="예: 100%, 400px, auto" className="h-8 text-xs sm:h-10 sm:text-sm" />
handleConfigChange("height", e.target.value)} placeholder="예: 200px, auto" className="h-8 text-xs sm:h-10 sm:text-sm" />
{isAdvanced && ( <>
handleConfigChange("minWidth", e.target.value)} placeholder="예: 200px" className="h-8 text-xs sm:h-10 sm:text-sm" />
handleConfigChange("maxWidth", e.target.value)} placeholder="예: 1200px" className="h-8 text-xs sm:h-10 sm:text-sm" />
)}
{/* meta-field 전용 */} {type === "meta-field" && ( <>
{(config as any).labelPosition === "left" && (
handleConfigChange("labelWidth", e.target.value)} placeholder="예: 120px" className="h-8 text-xs sm:h-10 sm:text-sm" />
)}
)} {/* meta-display 전용 */} {type === "meta-display" && ( <>
)} {/* meta-layout 전용 */} {type === "meta-layout" && isAdvanced && ( <>
handleConfigChange("gap", parseInt(e.target.value))} placeholder="4" className="h-8 text-xs sm:h-10 sm:text-sm" />
handleConfigChange("padding", parseInt(e.target.value))} placeholder="4" className="h-8 text-xs sm:h-10 sm:text-sm" />
handleConfigChange("bordered", checked)} />
)} {/* meta-action 전용 */} {type === "meta-action" && ( <>
handleConfigChange("icon", e.target.value)} placeholder="예: Save, Edit, Trash2" className="h-8 text-xs sm:h-10 sm:text-sm" />
)}
); // 연동 탭 const renderBindingTab = () => { const bindings = (config as any).bindings || []; const addBinding = () => { handleConfigChange("bindings", [ ...bindings, { sourceComponentId: "", sourceEvent: "change", sourceField: "", targetComponentId: "", targetAction: "filter", targetField: "", }, ]); }; const removeBinding = (idx: number) => { const newBindings = [...bindings]; newBindings.splice(idx, 1); handleConfigChange("bindings", newBindings); }; const updateBinding = (idx: number, key: string, value: any) => { const newBindings = [...bindings]; newBindings[idx][key] = value; handleConfigChange("bindings", newBindings); }; return (

컴포넌트 간 이벤트-액션 연결 설정

{bindings.length === 0 && (

바인딩이 없습니다

"바인딩 추가" 버튼을 클릭하여 컴포넌트 간 연결을 설정하세요.

)} {bindings.map((binding: any, idx: number) => (
바인딩 {idx + 1}
{/* 소스 컴포넌트 */}
updateBinding(idx, "sourceComponentId", e.target.value)} placeholder="예: field-1" className="h-8 text-xs sm:h-10 sm:text-sm" />
updateBinding(idx, "sourceField", e.target.value)} placeholder="예: customer_code" className="h-8 text-xs sm:h-10 sm:text-sm" />
{/* 대상 컴포넌트 */}
updateBinding(idx, "targetComponentId", e.target.value)} placeholder="예: dataview-1" className="h-8 text-xs sm:h-10 sm:text-sm" />
updateBinding(idx, "targetField", e.target.value)} placeholder="예: customer_code" className="h-8 text-xs sm:h-10 sm:text-sm" />
))}
); }; // 조건 탭 const renderConditionTab = () => ( {/* 표시 조건 */}

특정 조건을 만족할 때만 컴포넌트 표시

{["when_field_equals", "when_field_not_empty", "custom"].includes( (config as any).visibilityMode ) && ( <>
handleConfigChange("visibilityFieldId", e.target.value)} placeholder="예: field-1" className="h-8 text-xs sm:h-10 sm:text-sm" />
{(config as any).visibilityMode === "when_field_equals" && (
handleConfigChange("visibilityValue", e.target.value)} placeholder="예: approved" className="h-8 text-xs sm:h-10 sm:text-sm" />
)} {(config as any).visibilityMode === "custom" && (
handleConfigChange("visibilityExpression", e.target.value)} placeholder="예: field1 === 'value' && field2 > 10" className="h-8 text-xs sm:h-10 sm:text-sm" />
)} )}
{/* 활성 조건 */}

특정 조건을 만족할 때만 컴포넌트 활성화 (입력/클릭 가능)

{["when_field_equals", "when_field_not_empty", "custom"].includes( (config as any).enabledMode ) && ( <>
handleConfigChange("enabledFieldId", e.target.value)} placeholder="예: field-1" className="h-8 text-xs sm:h-10 sm:text-sm" />
{(config as any).enabledMode === "when_field_equals" && (
handleConfigChange("enabledValue", e.target.value)} placeholder="예: edit" className="h-8 text-xs sm:h-10 sm:text-sm" />
)} {(config as any).enabledMode === "custom" && (
handleConfigChange("enabledExpression", e.target.value)} placeholder="예: mode === 'edit' && hasPermission" className="h-8 text-xs sm:h-10 sm:text-sm" />
)} )}
); return (
{/* 헤더 */}

컴포넌트 설정

{/* 간편/상세 토글 (세그먼트 컨트롤 스타일) */}
{/* Tabs */} 기본 데이터 표시 연동 조건
{renderBasicTab()} {renderDataTab()} {renderDisplayTab()} {renderBindingTab()} {renderConditionTab()}
); }