"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 { Textarea } from "@/components/ui/textarea"; 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(""); // 🆕 전체 테이블 목록 (selected-items-detail-input 등에서 사용) const [allTables, setAllTables] = useState>([]); // 🆕 전체 테이블 목록 로드 useEffect(() => { const loadAllTables = async () => { try { const { tableManagementApi } = await import("@/lib/api/tableManagement"); const response = await tableManagementApi.getTableList(); if (response.success && response.data) { setAllTables(response.data); } } catch (error) { console.error("전체 테이블 목록 로드 실패:", error); } }; loadAllTables(); }, []); // 새로운 컴포넌트 시스템의 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; // 🎯 Section Card, Section Paper 등 신규 컴포넌트는 componentType에서 감지 const componentType = selectedComponent.componentType || // ⭐ 1순위: ScreenDesigner가 설정한 componentType (section-card 등) selectedComponent.componentConfig?.type || selectedComponent.componentConfig?.id || selectedComponent.type; const handleUpdateProperty = (path: string, value: any) => { onUpdateProperty(selectedComponent.id, path, value); }; const handleConfigChange = (newConfig: any) => { // 기존 config와 병합하여 다른 속성 유지 const currentConfig = selectedComponent.componentConfig?.config || {}; const mergedConfig = { ...currentConfig, ...newConfig }; onUpdateProperty(selectedComponent.id, "componentConfig.config", mergedConfig); }; // 🆕 ComponentRegistry에서 ConfigPanel 가져오기 시도 const componentId = selectedComponent.componentType || // ⭐ section-card 등 selectedComponent.componentConfig?.type || selectedComponent.componentConfig?.id || (selectedComponent.type === "component" ? selectedComponent.id : null); // 🆕 독립 컴포넌트 (table-search-widget 등) 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 = () => { // Section Card, Section Paper 등 신규 컴포넌트는 componentConfig 바로 아래에 설정 저장 const config = currentConfig || definition.defaultProps?.componentConfig || {}; const handleConfigChange = (newConfig: any) => { // componentConfig 전체를 업데이트 onUpdateProperty(selectedComponent.id, "componentConfig", newConfig); }; return (

{definition.name} 설정

); }; return ; } else { console.warn("⚠️ ComponentRegistry에서 ConfigPanel을 찾을 수 없음 - switch case로 이동:", { componentId, definitionName: definition?.name, hasDefinition: !!definition, }); // ConfigPanel이 없으면 아래 switch case로 넘어감 } } // 기존 하드코딩된 설정 패널들 (레거시) 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 ; case "section-card": return (

Section Card 설정

제목과 테두리가 있는 명확한 그룹화 컨테이너

{/* 헤더 표시 */}
{ handleUpdateProperty(selectedComponent.id, "componentConfig.showHeader", checked); }} />
{/* 제목 */} {selectedComponent.componentConfig?.showHeader !== false && (
{ handleUpdateProperty(selectedComponent.id, "componentConfig.title", e.target.value); }} placeholder="섹션 제목 입력" className="h-9 text-xs" />
)} {/* 설명 */} {selectedComponent.componentConfig?.showHeader !== false && (