ERP-node/frontend/components/screen/panels/PropertiesPanel.tsx

457 lines
16 KiB
TypeScript
Raw Normal View History

2025-09-02 16:18:38 +09:00
"use client";
import React, { useState, useCallback, useEffect, useRef } 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 { Settings, Move, Resize, Type, Palette, Trash2, Copy, Group, Ungroup } from "lucide-react";
import { ComponentData, WebType } from "@/types/screen";
interface PropertiesPanelProps {
selectedComponent?: ComponentData;
onUpdateProperty: (path: string, value: any) => void;
onDeleteComponent: () => void;
onCopyComponent: () => void;
onGroupComponents?: () => void;
onUngroupComponents?: () => void;
canGroup?: boolean;
canUngroup?: boolean;
}
const webTypeOptions: { value: WebType; label: string }[] = [
{ value: "text", label: "텍스트" },
{ value: "email", label: "이메일" },
{ value: "tel", label: "전화번호" },
{ value: "number", label: "숫자" },
{ value: "decimal", label: "소수" },
{ value: "date", label: "날짜" },
{ value: "datetime", label: "날짜시간" },
{ value: "select", label: "선택박스" },
{ value: "dropdown", label: "드롭다운" },
{ value: "textarea", label: "텍스트영역" },
{ value: "boolean", label: "불린" },
{ value: "checkbox", label: "체크박스" },
{ value: "radio", label: "라디오" },
{ value: "code", label: "코드" },
{ value: "entity", label: "엔티티" },
{ value: "file", label: "파일" },
];
// Debounce hook for better performance
const useDebounce = (value: any, delay: number) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
};
export const PropertiesPanel: React.FC<PropertiesPanelProps> = ({
selectedComponent,
onUpdateProperty,
onDeleteComponent,
onCopyComponent,
onGroupComponents,
onUngroupComponents,
canGroup = false,
canUngroup = false,
}) => {
// 최신 값들의 참조를 유지
const selectedComponentRef = useRef(selectedComponent);
const onUpdatePropertyRef = useRef(onUpdateProperty);
useEffect(() => {
selectedComponentRef.current = selectedComponent;
onUpdatePropertyRef.current = onUpdateProperty;
});
// 로컬 상태 관리 (실시간 입력 반영용)
const [localValues, setLocalValues] = useState({
label: selectedComponent?.label || "",
placeholder: selectedComponent?.placeholder || "",
title: selectedComponent?.title || "",
positionX: selectedComponent?.position.x || 0,
positionY: selectedComponent?.position.y || 0,
positionZ: selectedComponent?.position.z || 1,
width: selectedComponent?.size.width || 0,
height: selectedComponent?.size.height || 0,
});
// 선택된 컴포넌트가 변경될 때 로컬 상태 업데이트
useEffect(() => {
if (selectedComponent) {
setLocalValues({
label: selectedComponent.label || "",
placeholder: selectedComponent.placeholder || "",
title: selectedComponent.title || "",
positionX: selectedComponent.position.x || 0,
positionY: selectedComponent.position.y || 0,
positionZ: selectedComponent.position.z || 1,
width: selectedComponent.size.width || 0,
height: selectedComponent.size.height || 0,
});
}
}, [selectedComponent]);
// Debounce된 값들
const debouncedLabel = useDebounce(localValues.label, 300);
const debouncedPlaceholder = useDebounce(localValues.placeholder, 300);
const debouncedTitle = useDebounce(localValues.title, 300);
const debouncedPositionX = useDebounce(localValues.positionX, 150);
const debouncedPositionY = useDebounce(localValues.positionY, 150);
const debouncedPositionZ = useDebounce(localValues.positionZ, 150);
const debouncedWidth = useDebounce(localValues.width, 150);
const debouncedHeight = useDebounce(localValues.height, 150);
// Debounce된 값이 변경될 때 실제 업데이트
useEffect(() => {
const currentComponent = selectedComponentRef.current;
const updateProperty = onUpdatePropertyRef.current;
if (currentComponent && debouncedLabel !== currentComponent.label && debouncedLabel) {
updateProperty("label", debouncedLabel);
}
}, [debouncedLabel]);
useEffect(() => {
const currentComponent = selectedComponentRef.current;
const updateProperty = onUpdatePropertyRef.current;
if (currentComponent && debouncedPlaceholder !== currentComponent.placeholder) {
updateProperty("placeholder", debouncedPlaceholder);
}
}, [debouncedPlaceholder]);
useEffect(() => {
const currentComponent = selectedComponentRef.current;
const updateProperty = onUpdatePropertyRef.current;
if (currentComponent && debouncedTitle !== currentComponent.title) {
updateProperty("title", debouncedTitle);
}
}, [debouncedTitle]);
useEffect(() => {
const currentComponent = selectedComponentRef.current;
const updateProperty = onUpdatePropertyRef.current;
if (currentComponent && debouncedPositionX !== currentComponent.position.x) {
updateProperty("position.x", debouncedPositionX);
}
}, [debouncedPositionX]);
useEffect(() => {
const currentComponent = selectedComponentRef.current;
const updateProperty = onUpdatePropertyRef.current;
if (currentComponent && debouncedPositionY !== currentComponent.position.y) {
updateProperty("position.y", debouncedPositionY);
}
}, [debouncedPositionY]);
useEffect(() => {
const currentComponent = selectedComponentRef.current;
const updateProperty = onUpdatePropertyRef.current;
if (currentComponent && debouncedPositionZ !== currentComponent.position.z) {
updateProperty("position.z", debouncedPositionZ);
}
}, [debouncedPositionZ]);
useEffect(() => {
const currentComponent = selectedComponentRef.current;
const updateProperty = onUpdatePropertyRef.current;
if (currentComponent && debouncedWidth !== currentComponent.size.width) {
updateProperty("size.width", debouncedWidth);
}
}, [debouncedWidth]);
useEffect(() => {
const currentComponent = selectedComponentRef.current;
const updateProperty = onUpdatePropertyRef.current;
if (currentComponent && debouncedHeight !== currentComponent.size.height) {
updateProperty("size.height", debouncedHeight);
}
}, [debouncedHeight]);
if (!selectedComponent) {
return (
<div className="flex h-full flex-col items-center justify-center p-6 text-center">
<Settings className="mb-4 h-12 w-12 text-gray-400" />
<h3 className="mb-2 text-lg font-medium text-gray-900"> </h3>
<p className="text-sm text-gray-500"> .</p>
</div>
);
}
return (
<div className="flex h-full flex-col">
{/* 헤더 */}
<div className="border-b border-gray-200 p-4">
<div className="mb-3 flex items-center justify-between">
<div className="flex items-center space-x-2">
<Settings className="h-4 w-4 text-gray-600" />
<h3 className="font-medium text-gray-900"> </h3>
</div>
<Badge variant="secondary" className="text-xs">
{selectedComponent.type}
</Badge>
</div>
{/* 액션 버튼들 */}
<div className="flex flex-wrap gap-2">
<Button size="sm" variant="outline" onClick={onCopyComponent} className="flex items-center space-x-1">
<Copy className="h-3 w-3" />
<span></span>
</Button>
{canGroup && (
<Button size="sm" variant="outline" onClick={onGroupComponents} className="flex items-center space-x-1">
<Group className="h-3 w-3" />
<span></span>
</Button>
)}
{canUngroup && (
<Button size="sm" variant="outline" onClick={onUngroupComponents} className="flex items-center space-x-1">
<Ungroup className="h-3 w-3" />
<span></span>
</Button>
)}
<Button size="sm" variant="destructive" onClick={onDeleteComponent} className="flex items-center space-x-1">
<Trash2 className="h-3 w-3" />
<span></span>
</Button>
</div>
</div>
{/* 속성 편집 영역 */}
<div className="flex-1 space-y-6 overflow-y-auto p-4">
{/* 기본 정보 */}
<div className="space-y-3">
<div className="flex items-center space-x-2">
<Type className="h-4 w-4 text-gray-600" />
<h4 className="font-medium text-gray-900"> </h4>
</div>
<div className="space-y-3">
<div>
<Label htmlFor="label" className="text-sm font-medium">
</Label>
<Input
id="label"
value={localValues.label}
onChange={(e) => setLocalValues((prev) => ({ ...prev, label: e.target.value }))}
placeholder="컴포넌트 라벨"
className="mt-1"
/>
</div>
{selectedComponent.type === "widget" && (
<>
<div>
<Label htmlFor="columnName" className="text-sm font-medium">
( )
</Label>
<Input
id="columnName"
value={selectedComponent.columnName || ""}
readOnly
placeholder="데이터베이스 컬럼명"
className="mt-1 bg-gray-50 text-gray-600"
title="컬럼명은 변경할 수 없습니다"
/>
</div>
<div>
<Label htmlFor="widgetType" className="text-sm font-medium">
</Label>
<Select
value={selectedComponent.widgetType || "text"}
onValueChange={(value) => onUpdateProperty("widgetType", value)}
>
<SelectTrigger className="mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
{webTypeOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="placeholder" className="text-sm font-medium">
</Label>
<Input
id="placeholder"
value={localValues.placeholder}
onChange={(e) => setLocalValues((prev) => ({ ...prev, placeholder: e.target.value }))}
placeholder="입력 힌트 텍스트"
className="mt-1"
/>
</div>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<Checkbox
id="required"
checked={selectedComponent.required || false}
onCheckedChange={(checked) => onUpdateProperty("required", checked)}
/>
<Label htmlFor="required" className="text-sm">
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="readonly"
checked={selectedComponent.readonly || false}
onCheckedChange={(checked) => onUpdateProperty("readonly", checked)}
/>
<Label htmlFor="readonly" className="text-sm">
</Label>
</div>
</div>
</>
)}
</div>
</div>
<Separator />
{/* 위치 및 크기 */}
<div className="space-y-3">
<div className="flex items-center space-x-2">
<Move className="h-4 w-4 text-gray-600" />
<h4 className="font-medium text-gray-900"> </h4>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label htmlFor="positionX" className="text-sm font-medium">
X
</Label>
<Input
id="positionX"
type="number"
value={localValues.positionX}
onChange={(e) => setLocalValues((prev) => ({ ...prev, positionX: Number(e.target.value) }))}
className="mt-1"
/>
</div>
<div>
<Label htmlFor="positionY" className="text-sm font-medium">
Y
</Label>
<Input
id="positionY"
type="number"
value={localValues.positionY}
onChange={(e) => setLocalValues((prev) => ({ ...prev, positionY: Number(e.target.value) }))}
className="mt-1"
/>
</div>
<div>
<Label htmlFor="width" className="text-sm font-medium">
</Label>
<Input
id="width"
type="number"
value={localValues.width}
onChange={(e) => setLocalValues((prev) => ({ ...prev, width: Number(e.target.value) }))}
className="mt-1"
/>
</div>
<div>
<Label htmlFor="height" className="text-sm font-medium">
</Label>
<Input
id="height"
type="number"
value={localValues.height}
onChange={(e) => setLocalValues((prev) => ({ ...prev, height: Number(e.target.value) }))}
className="mt-1"
/>
</div>
<div>
<Label htmlFor="zIndex" className="text-sm font-medium">
Z-Index ( )
</Label>
<Input
id="zIndex"
type="number"
min="0"
max="9999"
value={localValues.positionZ}
onChange={(e) => setLocalValues((prev) => ({ ...prev, positionZ: Number(e.target.value) }))}
className="mt-1"
placeholder="1"
/>
</div>
</div>
</div>
{selectedComponent.type === "group" && (
<>
<Separator />
{/* 그룹 설정 */}
<div className="space-y-3">
<div className="flex items-center space-x-2">
<Group className="h-4 w-4 text-gray-600" />
<h4 className="font-medium text-gray-900"> </h4>
</div>
<div>
<Label htmlFor="groupTitle" className="text-sm font-medium">
</Label>
<Input
id="groupTitle"
value={localValues.title}
onChange={(e) => setLocalValues((prev) => ({ ...prev, title: e.target.value }))}
placeholder="그룹 제목"
className="mt-1"
/>
</div>
</div>
</>
)}
</div>
</div>
);
};
export default PropertiesPanel;