"use client"; import React, { useState, useEffect, useRef, useMemo } 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 { Settings, Move, Type, Trash2, Copy, Group, Ungroup } from "lucide-react"; import { ComponentData, WebType, WidgetComponent, GroupComponent, DataTableComponent, AreaComponent, AreaLayoutType, TableInfo, } from "@/types/screen"; import DataTableConfigPanel from "./DataTableConfigPanel"; import { DynamicConfigPanel } from "@/lib/registry"; import { useWebTypes } from "@/hooks/admin/useWebTypes"; // DataTableConfigPanel을 위한 안정화된 래퍼 컴포넌트 const DataTableConfigPanelWrapper: React.FC<{ selectedComponent: DataTableComponent; tables: TableInfo[]; activeTab: string; onTabChange: (tab: string) => void; onUpdateProperty: (property: string, value: any) => void; }> = React.memo( ({ selectedComponent, tables, activeTab, onTabChange, onUpdateProperty }) => { // 안정화된 업데이트 핸들러 const handleUpdateComponent = React.useCallback( (updates: Partial) => { console.log("🔄 DataTable 래퍼 컴포넌트 업데이트:", updates); // 변경사항이 있는지 확인 (간단한 비교로 성능 향상) const hasChanges = Object.entries(updates).some(([key, value]) => { const currentValue = (selectedComponent as any)[key]; // 배열의 경우 길이만 비교 if (Array.isArray(currentValue) && Array.isArray(value)) { return currentValue.length !== value.length; } // 기본값 비교 return currentValue !== value; }); if (!hasChanges) { console.log("⏭️ 래퍼: 변경사항 없음, 업데이트 스킵"); return; } // 각 속성을 개별적으로 업데이트 Object.entries(updates).forEach(([key, value]) => { onUpdateProperty(key, value); }); }, [selectedComponent.id, onUpdateProperty], ); // ID만 의존성으로 사용 return ( ); }, (prevProps, nextProps) => { // 컴포넌트 ID가 다르면 리렌더링 if (prevProps.selectedComponent.id !== nextProps.selectedComponent.id) { return false; } // 테이블 목록이 변경되면 리렌더링 if (prevProps.tables.length !== nextProps.tables.length) { return false; } // 활성 탭이 변경되면 리렌더링 if (prevProps.activeTab !== nextProps.activeTab) { return false; } // 그 외의 경우는 리렌더링하지 않음 return true; }, ); interface PropertiesPanelProps { selectedComponent?: ComponentData; tables?: TableInfo[]; onUpdateProperty: (path: string, value: unknown) => void; onDeleteComponent: () => void; onCopyComponent: () => void; onGroupComponents?: () => void; onUngroupComponents?: () => void; canGroup?: boolean; canUngroup?: boolean; } // 동적 웹타입 옵션은 컴포넌트 내부에서 useWebTypes 훅으로 가져옵니다 const PropertiesPanelComponent: React.FC = ({ selectedComponent, tables = [], onUpdateProperty, onDeleteComponent, onCopyComponent, onGroupComponents, onUngroupComponents, canGroup = false, canUngroup = false, }) => { // 동적 웹타입 목록 가져오기 - API에서 직접 조회 const { webTypes, isLoading: isWebTypesLoading } = useWebTypes({ active: "Y" }); // 웹타입 옵션 생성 - 데이터베이스 기반 const webTypeOptions = webTypes.map((webType) => ({ value: webType.web_type as WebType, label: webType.type_name, })); // 데이터테이블 설정 탭 상태를 여기서 관리 const [dataTableActiveTab, setDataTableActiveTab] = useState("basic"); // 최신 값들의 참조를 유지 const selectedComponentRef = useRef(selectedComponent); const onUpdatePropertyRef = useRef(onUpdateProperty); // 입력 필드들의 로컬 상태 (실시간 타이핑 반영용) const [localInputs, setLocalInputs] = useState({ placeholder: (selectedComponent?.type === "widget" ? (selectedComponent as WidgetComponent).placeholder : "") || "", title: (selectedComponent?.type === "group" ? (selectedComponent as GroupComponent).title : selectedComponent?.type === "area" ? (selectedComponent as AreaComponent).title : "") || "", description: (selectedComponent?.type === "area" ? (selectedComponent as AreaComponent).description : "") || "", positionX: selectedComponent?.position.x?.toString() || "0", positionY: selectedComponent?.position.y?.toString() || "0", positionZ: selectedComponent?.position.z?.toString() || "1", width: selectedComponent?.size.width?.toString() || "0", height: selectedComponent?.size.height?.toString() || "0", gridColumns: selectedComponent?.gridColumns?.toString() || "1", labelText: selectedComponent?.style?.labelText || selectedComponent?.label || "", labelFontSize: selectedComponent?.style?.labelFontSize || "12px", labelColor: selectedComponent?.style?.labelColor || "#374151", labelMarginBottom: selectedComponent?.style?.labelMarginBottom || "4px", required: (selectedComponent?.type === "widget" ? (selectedComponent as WidgetComponent).required : false) || false, readonly: (selectedComponent?.type === "widget" ? (selectedComponent as WidgetComponent).readonly : false) || false, labelDisplay: selectedComponent?.style?.labelDisplay !== false, // widgetType도 로컬 상태로 관리 widgetType: (selectedComponent?.type === "widget" ? (selectedComponent as WidgetComponent).widgetType : "text") || "text", }); useEffect(() => { selectedComponentRef.current = selectedComponent; onUpdatePropertyRef.current = onUpdateProperty; }); // 선택된 컴포넌트가 변경될 때 로컬 입력 상태 업데이트 useEffect(() => { if (selectedComponent) { const widget = selectedComponent.type === "widget" ? (selectedComponent as WidgetComponent) : null; const group = selectedComponent.type === "group" ? (selectedComponent as GroupComponent) : null; const area = selectedComponent.type === "area" ? (selectedComponent as AreaComponent) : null; console.log("🔄 PropertiesPanel: 컴포넌트 변경 감지", { componentId: selectedComponent.id, componentType: selectedComponent.type, currentValues: { placeholder: widget?.placeholder, title: group?.title || area?.title, description: area?.description, positionX: selectedComponent.position.x, labelText: selectedComponent.style?.labelText || selectedComponent.label, }, }); setLocalInputs({ placeholder: widget?.placeholder || "", title: group?.title || area?.title || "", description: area?.description || "", positionX: selectedComponent.position.x?.toString() || "0", positionY: selectedComponent.position.y?.toString() || "0", positionZ: selectedComponent.position.z?.toString() || "1", width: selectedComponent.size.width?.toString() || "0", height: selectedComponent.size.height?.toString() || "0", gridColumns: selectedComponent.gridColumns?.toString() || "1", labelText: selectedComponent.style?.labelText || selectedComponent.label || "", labelFontSize: selectedComponent.style?.labelFontSize || "12px", labelColor: selectedComponent.style?.labelColor || "#374151", labelMarginBottom: selectedComponent.style?.labelMarginBottom || "4px", required: widget?.required || false, readonly: widget?.readonly || false, labelDisplay: selectedComponent.style?.labelDisplay !== false, // widgetType 동기화 widgetType: widget?.widgetType || "text", }); } }, [ selectedComponent?.id, // ID만 감지하여 컴포넌트 변경 시에만 업데이트 ]); if (!selectedComponent) { return (

컴포넌트를 선택하세요

캔버스에서 컴포넌트를 클릭하면 속성을 편집할 수 있습니다.

); } // 데이터 테이블 컴포넌트인 경우 전용 패널 사용 if (selectedComponent.type === "datatable") { return (
{/* 헤더 */}
데이터 테이블 설정
{selectedComponent.type}
{/* 액션 버튼들 */}
{/* 데이터 테이블 설정 패널 */}
); } return (
{/* 헤더 */}

속성 편집

{selectedComponent.type}
{/* 액션 버튼들 */}
{canGroup && ( )} {canUngroup && ( )}
{/* 속성 편집 영역 */}
{/* 기본 정보 */}

기본 정보

{selectedComponent.type === "widget" && ( <>
{/* 동적 웹타입 설정 패널 */} {selectedComponent.widgetType && (
{ // 컴포넌트 전체 업데이트 Object.keys(updatedComponent).forEach((key) => { if (key !== "id") { onUpdateProperty(key, updatedComponent[key]); } }); }} onUpdateProperty={(property, value) => { // 웹타입 설정 업데이트 if (property === "webTypeConfig") { onUpdateProperty("webTypeConfig", value); } else { onUpdateProperty(property, value); } }} />
)}
{ const newValue = e.target.value; console.log("🔄 placeholder 변경:", newValue); setLocalInputs((prev) => ({ ...prev, placeholder: newValue })); onUpdateProperty("placeholder", newValue); }} placeholder="입력 힌트 텍스트" className="mt-1" />
{ setLocalInputs((prev) => ({ ...prev, required: e.target.checked })); onUpdateProperty("required", e.target.checked); }} className="border-input bg-background text-primary focus:ring-ring h-4 w-4 rounded border focus:ring-2 focus:ring-offset-2" />
{ setLocalInputs((prev) => ({ ...prev, readonly: e.target.checked })); onUpdateProperty("readonly", e.target.checked); }} className="border-input bg-background text-primary focus:ring-ring h-4 w-4 rounded border focus:ring-2 focus:ring-offset-2" />
)}
{/* 위치 및 크기 */}

위치 및 크기

{ const newValue = e.target.value; setLocalInputs((prev) => ({ ...prev, positionX: newValue })); onUpdateProperty("position", { ...selectedComponent.position, x: Number(newValue) }); }} className="mt-1" />
{ const newValue = e.target.value; setLocalInputs((prev) => ({ ...prev, positionY: newValue })); onUpdateProperty("position", { ...selectedComponent.position, y: Number(newValue) }); }} className="mt-1" />
{ const newValue = e.target.value; setLocalInputs((prev) => ({ ...prev, width: newValue })); onUpdateProperty("size", { ...selectedComponent.size, width: Number(newValue) }); }} className="mt-1" />
{ const newValue = e.target.value; setLocalInputs((prev) => ({ ...prev, height: newValue })); onUpdateProperty("size", { ...selectedComponent.size, height: Number(newValue) }); }} className="mt-1" />
{ const newValue = e.target.value; setLocalInputs((prev) => ({ ...prev, positionZ: newValue })); onUpdateProperty("position", { ...selectedComponent.position, z: Number(newValue) }); }} className="mt-1" placeholder="1" />
{ const newValue = e.target.value; const numValue = Number(newValue); if (numValue >= 1 && numValue <= 12) { setLocalInputs((prev) => ({ ...prev, gridColumns: newValue })); onUpdateProperty("gridColumns", numValue); } }} placeholder="1" className="mt-1" />
이 컴포넌트가 차지할 그리드 컬럼 수를 설정합니다 (기본: 1)
{/* 라벨 스타일 */}

라벨 설정

{/* 라벨 표시 토글 */}
{ console.log("🔄 라벨 표시 변경:", e.target.checked); setLocalInputs((prev) => ({ ...prev, labelDisplay: e.target.checked })); onUpdateProperty("style.labelDisplay", e.target.checked); }} className="border-input bg-background text-primary focus:ring-ring h-4 w-4 rounded border focus:ring-2 focus:ring-offset-2" />
{/* 라벨 텍스트 */}
{ const newValue = e.target.value; console.log("🔄 라벨 텍스트 변경:", newValue); setLocalInputs((prev) => ({ ...prev, labelText: newValue })); // 기본 라벨과 스타일 라벨을 모두 업데이트 onUpdateProperty("label", newValue); onUpdateProperty("style.labelText", newValue); }} placeholder="라벨 텍스트" className="mt-1" />
{/* 라벨 스타일 */}
{ const newValue = e.target.value; setLocalInputs((prev) => ({ ...prev, labelFontSize: newValue })); onUpdateProperty("style.labelFontSize", newValue); }} placeholder="12px" className="mt-1" />
{ const newValue = e.target.value; setLocalInputs((prev) => ({ ...prev, labelColor: newValue })); onUpdateProperty("style.labelColor", newValue); }} className="mt-1 h-8" />
{/* 라벨 여백 */}
{ const newValue = e.target.value; setLocalInputs((prev) => ({ ...prev, labelMarginBottom: newValue })); onUpdateProperty("style.labelMarginBottom", newValue); }} placeholder="4px" className="mt-1" />
{selectedComponent.type === "group" && ( <> {/* 그룹 설정 */}

그룹 설정

{ const newValue = e.target.value; setLocalInputs((prev) => ({ ...prev, title: newValue })); onUpdateProperty("title", newValue); }} placeholder="그룹 제목" className="mt-1" />
)} {selectedComponent.type === "area" && ( <> {/* 영역 설정 */}

영역 설정

{ const newValue = e.target.value; setLocalInputs((prev) => ({ ...prev, title: newValue })); onUpdateProperty("title", newValue); }} placeholder="영역 제목" className="mt-1" />
{ const newValue = e.target.value; setLocalInputs((prev) => ({ ...prev, description: newValue })); onUpdateProperty("description", newValue); }} placeholder="영역 설명 (선택사항)" className="mt-1" />
{/* 레이아웃별 상세 설정 */} {(selectedComponent as AreaComponent).layoutType === "grid" && (
그리드 설정
{ const value = Number(e.target.value); onUpdateProperty("layoutConfig.gridColumns", value); }} className="mt-1" />
{ const value = Number(e.target.value); onUpdateProperty("layoutConfig.gridGap", value); }} className="mt-1" />
)} {((selectedComponent as AreaComponent).layoutType === "flex-row" || (selectedComponent as AreaComponent).layoutType === "flex-column") && (
플렉스 설정
{ const value = Number(e.target.value); onUpdateProperty("layoutConfig.gap", value); }} className="mt-1" />
)} {(selectedComponent as AreaComponent).layoutType === "sidebar" && (
사이드바 설정
{ const value = Number(e.target.value); onUpdateProperty("layoutConfig.sidebarWidth", value); }} className="mt-1" />
)}
)}
); }; // React.memo로 감싸서 불필요한 리렌더링 방지 export const PropertiesPanel = React.memo(PropertiesPanelComponent, (prevProps, nextProps) => { // 선택된 컴포넌트 ID가 다르면 리렌더링 if (prevProps.selectedComponent?.id !== nextProps.selectedComponent?.id) { return false; } // 선택된 컴포넌트가 없는 상태에서 있는 상태로 변경되거나 그 반대인 경우 if (!prevProps.selectedComponent !== !nextProps.selectedComponent) { return false; } // 테이블 목록이 변경되면 리렌더링 if (prevProps.tables.length !== nextProps.tables.length) { return false; } // 그룹 관련 props가 변경되면 리렌더링 if (prevProps.canGroup !== nextProps.canGroup || prevProps.canUngroup !== nextProps.canUngroup) { return false; } // 그 외의 경우는 리렌더링하지 않음 return true; }); export default PropertiesPanel;