"use client"; import React, { useState, useEffect } from "react"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Separator } from "@/components/ui/separator"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Checkbox } from "@/components/ui/checkbox"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { ChevronDown, Settings, Info, Database, Trash2, Copy, Palette, Monitor } from "lucide-react"; import { ComponentData, WebType, WidgetComponent, GroupComponent, DataTableComponent, TableInfo, LayoutComponent, FileComponent, AreaComponent, } from "@/types/screen"; import { ColumnSpanPreset, COLUMN_SPAN_PRESETS } from "@/lib/constants/columnSpans"; // 컬럼 스팬 숫자 배열 (1~12) // 동적으로 컬럼 수 배열 생성 (gridSettings.columns 기반) const generateColumnNumbers = (maxColumns: number) => { return Array.from({ length: maxColumns }, (_, i) => i + 1); }; import { cn } from "@/lib/utils"; import DataTableConfigPanel from "./DataTableConfigPanel"; import { WebTypeConfigPanel } from "./WebTypeConfigPanel"; import { FileComponentConfigPanel } from "./FileComponentConfigPanel"; import { useWebTypes } from "@/hooks/admin/useWebTypes"; import { isFileComponent } from "@/lib/utils/componentTypeUtils"; import { BaseInputType, BASE_INPUT_TYPE_OPTIONS, getBaseInputType, getDefaultDetailType, getDetailTypes, DetailTypeOption, } from "@/types/input-type-mapping"; // 새로운 컴포넌트 설정 패널들 import { ButtonConfigPanel } from "../config-panels/ButtonConfigPanel"; import { CardConfigPanel } from "../config-panels/CardConfigPanel"; import { DashboardConfigPanel } from "../config-panels/DashboardConfigPanel"; import { StatsCardConfigPanel } from "../config-panels/StatsCardConfigPanel"; // ComponentRegistry import (동적 ConfigPanel 가져오기용) import { ComponentRegistry } from "@/lib/registry/ComponentRegistry"; import { ProgressBarConfigPanel } from "../config-panels/ProgressBarConfigPanel"; import { ChartConfigPanel } from "../config-panels/ChartConfigPanel"; import { AlertConfigPanel } from "../config-panels/AlertConfigPanel"; import { BadgeConfigPanel } from "../config-panels/BadgeConfigPanel"; import { DynamicComponentConfigPanel } from "@/lib/utils/getComponentConfigPanel"; import StyleEditor from "../StyleEditor"; import ResolutionPanel from "./ResolutionPanel"; import { Slider } from "@/components/ui/slider"; import { Grid3X3, Eye, EyeOff, Zap } from "lucide-react"; interface UnifiedPropertiesPanelProps { selectedComponent?: ComponentData; tables: TableInfo[]; gridSettings?: { columns: number; gap: number; padding: number; snapToGrid: boolean; showGrid: boolean; gridColor?: string; gridOpacity?: number; }; onUpdateProperty: (componentId: string, path: string, value: any) => void; onGridSettingsChange?: (settings: any) => void; onDeleteComponent?: (componentId: string) => void; onCopyComponent?: (componentId: string) => void; currentTable?: TableInfo; currentTableName?: string; dragState?: any; // 스타일 관련 onStyleChange?: (style: any) => void; // 해상도 관련 currentResolution?: { name: string; width: number; height: number }; onResolutionChange?: (resolution: { name: string; width: number; height: number }) => void; // 🆕 플로우 위젯 감지용 allComponents?: ComponentData[]; // 🆕 메뉴 OBJID (코드/카테고리 스코프용) menuObjid?: number; // 🆕 현재 편집 중인 화면의 회사 코드 currentScreenCompanyCode?: string; } export const UnifiedPropertiesPanel: React.FC = ({ selectedComponent, tables, gridSettings, onUpdateProperty, onGridSettingsChange, onDeleteComponent, onCopyComponent, currentTable, currentTableName, currentScreenCompanyCode, dragState, onStyleChange, menuObjid, currentResolution, onResolutionChange, allComponents = [], // 🆕 기본값 빈 배열 }) => { const { webTypes } = useWebTypes({ active: "Y" }); const [localComponentDetailType, setLocalComponentDetailType] = useState(""); // 높이/너비 입력 로컬 상태 (자유 입력 허용) const [localHeight, setLocalHeight] = useState(""); const [localWidth, setLocalWidth] = useState(""); // 새로운 컴포넌트 시스템의 webType 동기화 useEffect(() => { if (selectedComponent?.type === "component") { const webType = selectedComponent.componentConfig?.webType; if (webType) { setLocalComponentDetailType(webType); } } }, [selectedComponent?.type, selectedComponent?.componentConfig?.webType, selectedComponent?.id]); // 높이 값 동기화 useEffect(() => { if (selectedComponent?.size?.height !== undefined) { setLocalHeight(String(selectedComponent.size.height)); } }, [selectedComponent?.size?.height, selectedComponent?.id]); // 너비 값 동기화 useEffect(() => { if (selectedComponent?.size?.width !== undefined) { setLocalWidth(String(selectedComponent.size.width)); } }, [selectedComponent?.size?.width, selectedComponent?.id]); // 격자 설정 업데이트 함수 (early return 이전에 정의) const updateGridSetting = (key: string, value: any) => { if (onGridSettingsChange && gridSettings) { onGridSettingsChange({ ...gridSettings, [key]: value, }); } }; // 격자 설정 렌더링 (early return 이전에 정의) const renderGridSettings = () => { if (!gridSettings || !onGridSettingsChange) return null; // 최대 컬럼 수 계산 const MIN_COLUMN_WIDTH = 30; const maxColumns = currentResolution ? Math.floor((currentResolution.width - gridSettings.padding * 2 + gridSettings.gap) / (MIN_COLUMN_WIDTH + gridSettings.gap)) : 24; const safeMaxColumns = Math.max(1, Math.min(maxColumns, 100)); // 최대 100개로 제한 return (

격자 설정

{/* 토글들 */}
{gridSettings.showGrid ? ( ) : ( )}
updateGridSetting("showGrid", checked)} />
updateGridSetting("snapToGrid", checked)} />
{/* 10px 단위 스냅 안내 */}

모든 컴포넌트는 10px 단위로 자동 배치됩니다.

); }; // 컴포넌트가 선택되지 않았을 때도 해상도 설정과 격자 설정은 표시 if (!selectedComponent) { return (
{/* 해상도 설정과 격자 설정 표시 */}
{/* 해상도 설정 */} {currentResolution && onResolutionChange && ( <>

해상도 설정

)} {/* 격자 설정 */} {renderGridSettings()} {/* 안내 메시지 */}

컴포넌트를 선택하여

속성을 편집하세요

); } const handleUpdate = (path: string, value: any) => { onUpdateProperty(selectedComponent.id, path, value); }; // 드래그 중일 때 실시간 위치 표시 const currentPosition = dragState?.isDragging && dragState?.draggedComponent?.id === selectedComponent.id ? dragState.currentPosition : selectedComponent.position; // 컴포넌트별 설정 패널 렌더링 함수 (DetailSettingsPanel의 로직) const renderComponentConfigPanel = () => { if (!selectedComponent) return null; const componentType = selectedComponent.componentConfig?.type || selectedComponent.type; const handleUpdateProperty = (path: string, value: any) => { onUpdateProperty(selectedComponent.id, path, value); }; const handleConfigChange = (newConfig: any) => { onUpdateProperty(selectedComponent.id, "componentConfig.config", newConfig); }; // 🆕 ComponentRegistry에서 ConfigPanel 가져오기 const componentId = selectedComponent.componentConfig?.type || selectedComponent.componentConfig?.id; if (componentId) { const definition = ComponentRegistry.getComponent(componentId); if (definition?.configPanel) { const ConfigPanelComponent = definition.configPanel; const currentConfig = selectedComponent.componentConfig || {}; console.log("✅ ConfigPanel 표시:", { componentId, definitionName: definition.name, hasConfigPanel: !!definition.configPanel, currentConfig, }); // 래퍼 컴포넌트: 새 ConfigPanel 인터페이스를 기존 패턴에 맞춤 const ConfigPanelWrapper = () => { const config = currentConfig.config || definition.defaultConfig || {}; const handleConfigChange = (newConfig: any) => { onUpdateProperty(selectedComponent.id, "componentConfig.config", newConfig); }; return (

{definition.name} 설정

); }; return ; } else { console.warn("⚠️ ConfigPanel 없음:", { componentId, definitionName: definition?.name, hasDefinition: !!definition, }); } } // 기존 하드코딩된 설정 패널들 (레거시) switch (componentType) { case "button": case "button-primary": case "button-secondary": // 🔧 component.id만 key로 사용 (unmount 방지) return ( ); case "card": return ; case "dashboard": return ; case "stats": case "stats-card": return ; case "progress": case "progress-bar": return ; case "chart": case "chart-basic": return ; case "alert": case "alert-info": return ; case "badge": case "badge-status": return ; default: // ConfigPanel이 없는 경우 경고 표시 return (

⚠️ 설정 패널 없음

컴포넌트 "{componentId || componentType}"에 대한 설정 패널이 없습니다.

); } }; // 기본 정보 탭 const renderBasicTab = () => { const widget = selectedComponent as WidgetComponent; const group = selectedComponent as GroupComponent; const area = selectedComponent as AreaComponent; return (
{/* 라벨 + 최소 높이 (같은 행) */}
handleUpdate("label", e.target.value)} placeholder="라벨" className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} style={{ fontSize: "12px" }} />
{ // 입력 중에는 로컬 상태만 업데이트 (자유 입력) setLocalHeight(e.target.value); }} onBlur={(e) => { // 포커스를 잃을 때 10px 단위로 스냅 const value = parseInt(e.target.value) || 0; if (value >= 10) { const snappedValue = Math.round(value / 10) * 10; handleUpdate("size.height", snappedValue); setLocalHeight(String(snappedValue)); } }} onKeyDown={(e) => { // Enter 키를 누르면 즉시 적용 (10px 단위로 스냅) if (e.key === "Enter") { const value = parseInt(e.currentTarget.value) || 0; if (value >= 10) { const snappedValue = Math.round(value / 10) * 10; handleUpdate("size.height", snappedValue); setLocalHeight(String(snappedValue)); } e.currentTarget.blur(); // 포커스 제거 } }} step={1} placeholder="10" className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} />
{/* Placeholder (widget만) */} {selectedComponent.type === "widget" && (
handleUpdate("placeholder", e.target.value)} placeholder="입력 안내 텍스트" className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} style={{ fontSize: "12px" }} />
)} {/* Title (group/area) */} {(selectedComponent.type === "group" || selectedComponent.type === "area") && (
handleUpdate("title", e.target.value)} placeholder="제목" className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} style={{ fontSize: "12px" }} />
)} {/* Description (area만) */} {selectedComponent.type === "area" && (
handleUpdate("description", e.target.value)} placeholder="설명" className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} style={{ fontSize: "12px" }} />
)} {/* Width + Z-Index (같은 행) */}
{ // 입력 중에는 로컬 상태만 업데이트 (자유 입력) setLocalWidth(e.target.value); }} onBlur={(e) => { // 포커스를 잃을 때 10px 단위로 스냅 const value = parseInt(e.target.value, 10); if (!isNaN(value) && value >= 10) { const snappedValue = Math.round(value / 10) * 10; handleUpdate("size.width", snappedValue); setLocalWidth(String(snappedValue)); } }} onKeyDown={(e) => { // Enter 키를 누르면 즉시 적용 (10px 단위로 스냅) if (e.key === "Enter") { const value = parseInt(e.currentTarget.value, 10); if (!isNaN(value) && value >= 10) { const snappedValue = Math.round(value / 10) * 10; handleUpdate("size.width", snappedValue); setLocalWidth(String(snappedValue)); } e.currentTarget.blur(); // 포커스 제거 } }} className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} />
handleUpdate("position.z", parseInt(e.target.value) || 1)} className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} style={{ fontSize: "12px" }} />
{/* 라벨 스타일 */} 라벨 스타일
handleUpdate("style.labelText", e.target.value)} className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} style={{ fontSize: "12px" }} />
handleUpdate("style.labelFontSize", e.target.value)} className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} style={{ fontSize: "12px" }} />
handleUpdate("style.labelColor", e.target.value)} className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} style={{ fontSize: "12px" }} />
handleUpdate("style.labelMarginBottom", e.target.value)} className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} style={{ fontSize: "12px" }} />
handleUpdate("style.labelDisplay", checked)} className="h-4 w-4" />
{/* 옵션 */}
{widget.required !== undefined && (
{ handleUpdate("required", checked); handleUpdate("componentConfig.required", checked); }} className="h-4 w-4" />
)} {widget.readonly !== undefined && (
{ handleUpdate("readonly", checked); handleUpdate("componentConfig.readonly", checked); }} className="h-4 w-4" />
)}
); }; // 상세 설정 탭 (DetailSettingsPanel의 전체 로직 통합) const renderDetailTab = () => { console.log("🔍 [renderDetailTab] selectedComponent.type:", selectedComponent.type); // 1. DataTable 컴포넌트 if (selectedComponent.type === "datatable") { console.log("✅ [renderDetailTab] DataTable 컴포넌트"); return ( { Object.entries(updates).forEach(([key, value]) => { handleUpdate(key, value); }); }} /> ); } // 3. 파일 컴포넌트 if (isFileComponent(selectedComponent)) { return ( ); } // 4. 새로운 컴포넌트 시스템 (button, card 등) const componentType = selectedComponent.componentConfig?.type || selectedComponent.type; const hasNewConfigPanel = componentType && [ "button", "button-primary", "button-secondary", "card", "dashboard", "stats", "stats-card", "progress", "progress-bar", "chart", "chart-basic", "alert", "alert-info", "badge", "badge-status", ].includes(componentType); if (hasNewConfigPanel) { const configPanel = renderComponentConfigPanel(); if (configPanel) { return
{configPanel}
; } } // 5. 새로운 컴포넌트 시스템 (type: "component") if (selectedComponent.type === "component") { console.log("✅ [renderDetailTab] Component 타입"); const componentId = (selectedComponent as any).componentType || selectedComponent.componentConfig?.type; const webType = selectedComponent.componentConfig?.webType; // 테이블 패널에서 드래그한 컴포넌트인지 확인 const isFromTablePanel = !!(selectedComponent.tableName && selectedComponent.columnName); if (!componentId) { return (

컴포넌트 ID가 설정되지 않았습니다

); } // 현재 웹타입의 기본 입력 타입 추출 const currentBaseInputType = webType ? getBaseInputType(webType as any) : null; // 선택 가능한 세부 타입 목록 const availableDetailTypes = currentBaseInputType ? getDetailTypes(currentBaseInputType) : []; // 세부 타입 변경 핸들러 const handleDetailTypeChange = (newDetailType: string) => { setLocalComponentDetailType(newDetailType); handleUpdate("componentConfig.webType", newDetailType); }; return (
{/* 세부 타입 선택 - 테이블 패널에서 드래그한 컴포넌트만 표시 */} {isFromTablePanel && webType && availableDetailTypes.length > 1 && (
)} {/* DynamicComponentConfigPanel */} { console.log("🔄 DynamicComponentConfigPanel onChange:", newConfig); // 개별 속성별로 업데이트하여 다른 속성과의 충돌 방지 Object.entries(newConfig).forEach(([key, value]) => { handleUpdate(`componentConfig.${key}`, value); }); }} /> {/* 🆕 테이블 데이터 자동 입력 (component 타입용) */}

테이블 데이터 자동 입력

{/* 활성화 체크박스 */}
{ handleUpdate("autoFill", { ...selectedComponent.autoFill, enabled: Boolean(checked), }); }} />
{selectedComponent.autoFill?.enabled && ( <> {/* 조회할 테이블 */}
{/* 필터링할 컬럼 */}
{ handleUpdate("autoFill", { ...selectedComponent.autoFill, enabled: selectedComponent.autoFill?.enabled || false, filterColumn: e.target.value, }); }} placeholder="예: company_code" className="h-6 w-full px-2 py-0 text-xs" />
{/* 사용자 정보 필드 */}
{/* 표시할 컬럼 */}
{ handleUpdate("autoFill", { ...selectedComponent.autoFill, enabled: selectedComponent.autoFill?.enabled || false, displayColumn: e.target.value, }); }} placeholder="예: company_name" className="h-6 w-full px-2 py-0 text-xs" />
)}
); } // 6. Widget 컴포넌트 if (selectedComponent.type === "widget") { console.log("✅ [renderDetailTab] Widget 타입"); const widget = selectedComponent as WidgetComponent; console.log("🔍 [renderDetailTab] widget.widgetType:", widget.widgetType); // 새로운 컴포넌트 시스템 (widgetType이 button, card 등) - 먼저 체크 if ( widget.widgetType && ["button", "card", "dashboard", "stats-card", "progress-bar", "chart", "alert", "badge"].includes( widget.widgetType, ) ) { console.log("✅ [renderDetailTab] DynamicComponent 반환 (widgetType)"); return ( { console.log("🔄 DynamicComponentConfigPanel onChange (widget):", newConfig); // 전체 componentConfig를 업데이트 handleUpdate("componentConfig", newConfig); }} /> ); } // 일반 위젯 (webType 기반) console.log("✅ [renderDetailTab] 일반 위젯 렌더링 시작"); return (
{console.log("🔍 [UnifiedPropertiesPanel] widget.webType:", widget.webType, "widget:", widget)} {/* WebType 선택 (있는 경우만) */} {widget.webType && (
)} {/* 🆕 테이블 데이터 자동 입력 (모든 widget 컴포넌트) */}

테이블 데이터 자동 입력

{/* 활성화 체크박스 */}
{ handleUpdate("autoFill", { ...widget.autoFill, enabled: Boolean(checked), }); }} />
{widget.autoFill?.enabled && ( <> {/* 조회할 테이블 */}
{/* 필터링할 컬럼 */}
{ handleUpdate("autoFill", { ...widget.autoFill, enabled: widget.autoFill?.enabled || false, filterColumn: e.target.value, }); }} placeholder="예: company_code" className="h-6 w-full px-2 py-0 text-xs" />
{/* 사용자 정보 필드 */}
{/* 표시할 컬럼 */}
{ handleUpdate("autoFill", { ...widget.autoFill, enabled: widget.autoFill?.enabled || false, displayColumn: e.target.value, }); }} placeholder="예: company_name" className="h-6 w-full px-2 py-0 text-xs" />
)}
); } // 기본 메시지 return (

이 컴포넌트는 추가 설정이 없습니다

); }; return (
{/* 헤더 - 간소화 */}
{selectedComponent.type === "widget" && (
{(selectedComponent as WidgetComponent).label || selectedComponent.id}
)}
{/* 통합 컨텐츠 (탭 제거) */}
{/* 해상도 설정 - 항상 맨 위에 표시 */} {currentResolution && onResolutionChange && ( <>

해상도 설정

)} {/* 격자 설정 - 해상도 설정 아래 표시 */} {renderGridSettings()} {gridSettings && onGridSettingsChange && } {/* 기본 설정 */} {renderBasicTab()} {/* 상세 설정 */} {renderDetailTab()} {/* 스타일 설정 */} {selectedComponent && ( <>

컴포넌트 스타일

{ if (onStyleChange) { onStyleChange(style); } else { handleUpdate("style", style); } }} />
)}
); }; export default UnifiedPropertiesPanel;