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

531 lines
20 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: "파일" },
];
export const PropertiesPanel: React.FC<PropertiesPanelProps> = ({
selectedComponent,
onUpdateProperty,
onDeleteComponent,
onCopyComponent,
onGroupComponents,
onUngroupComponents,
canGroup = false,
canUngroup = false,
}) => {
// 최신 값들의 참조를 유지
const selectedComponentRef = useRef(selectedComponent);
const onUpdatePropertyRef = useRef(onUpdateProperty);
2025-09-02 16:46:54 +09:00
// 입력 필드들의 로컬 상태 (실시간 타이핑 반영용)
const [localInputs, setLocalInputs] = useState({
placeholder: selectedComponent?.placeholder || "",
title: selectedComponent?.title || "",
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",
labelText: selectedComponent?.style?.labelText || selectedComponent?.label || "",
labelFontSize: selectedComponent?.style?.labelFontSize || "12px",
labelColor: selectedComponent?.style?.labelColor || "#374151",
labelMarginBottom: selectedComponent?.style?.labelMarginBottom || "4px",
required: selectedComponent?.required || false,
readonly: selectedComponent?.readonly || false,
});
2025-09-02 16:18:38 +09:00
useEffect(() => {
selectedComponentRef.current = selectedComponent;
onUpdatePropertyRef.current = onUpdateProperty;
});
2025-09-02 16:46:54 +09:00
// 선택된 컴포넌트가 변경될 때 로컬 입력 상태 업데이트
2025-09-02 16:18:38 +09:00
useEffect(() => {
if (selectedComponent) {
2025-09-02 16:46:54 +09:00
setLocalInputs({
2025-09-02 16:18:38 +09:00
placeholder: selectedComponent.placeholder || "",
title: selectedComponent.title || "",
2025-09-02 16:46:54 +09:00
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",
labelText: selectedComponent.style?.labelText || selectedComponent.label || "",
labelFontSize: selectedComponent.style?.labelFontSize || "12px",
labelColor: selectedComponent.style?.labelColor || "#374151",
labelMarginBottom: selectedComponent.style?.labelMarginBottom || "4px",
required: selectedComponent.required || false,
readonly: selectedComponent.readonly || false,
2025-09-02 16:18:38 +09:00
});
}
}, [selectedComponent]);
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">
{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"
2025-09-02 16:46:54 +09:00
value={localInputs.placeholder}
onChange={(e) => {
const newValue = e.target.value;
setLocalInputs((prev) => ({ ...prev, placeholder: newValue }));
onUpdateProperty("placeholder", newValue);
}}
2025-09-02 16:18:38 +09:00
placeholder="입력 힌트 텍스트"
className="mt-1"
/>
</div>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<Checkbox
id="required"
2025-09-02 16:46:54 +09:00
checked={localInputs.required}
onCheckedChange={(checked) => {
setLocalInputs((prev) => ({ ...prev, required: !!checked }));
onUpdateProperty("required", checked);
}}
2025-09-02 16:18:38 +09:00
/>
<Label htmlFor="required" className="text-sm">
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="readonly"
2025-09-02 16:46:54 +09:00
checked={localInputs.readonly}
onCheckedChange={(checked) => {
setLocalInputs((prev) => ({ ...prev, readonly: !!checked }));
onUpdateProperty("readonly", checked);
}}
2025-09-02 16:18:38 +09:00
/>
<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"
2025-09-02 16:46:54 +09:00
value={localInputs.positionX}
onChange={(e) => {
const newValue = e.target.value;
setLocalInputs((prev) => ({ ...prev, positionX: newValue }));
onUpdateProperty("position", { ...selectedComponent.position, x: Number(newValue) });
}}
2025-09-02 16:18:38 +09:00
className="mt-1"
/>
</div>
<div>
<Label htmlFor="positionY" className="text-sm font-medium">
Y
</Label>
<Input
id="positionY"
type="number"
2025-09-02 16:46:54 +09:00
value={localInputs.positionY}
onChange={(e) => {
const newValue = e.target.value;
setLocalInputs((prev) => ({ ...prev, positionY: newValue }));
onUpdateProperty("position", { ...selectedComponent.position, y: Number(newValue) });
}}
2025-09-02 16:18:38 +09:00
className="mt-1"
/>
</div>
<div>
<Label htmlFor="width" className="text-sm font-medium">
</Label>
<Input
id="width"
type="number"
2025-09-02 16:46:54 +09:00
value={localInputs.width}
onChange={(e) => {
const newValue = e.target.value;
setLocalInputs((prev) => ({ ...prev, width: newValue }));
onUpdateProperty("size", { ...selectedComponent.size, width: Number(newValue) });
}}
2025-09-02 16:18:38 +09:00
className="mt-1"
/>
</div>
<div>
<Label htmlFor="height" className="text-sm font-medium">
</Label>
<Input
id="height"
type="number"
2025-09-02 16:46:54 +09:00
value={localInputs.height}
onChange={(e) => {
const newValue = e.target.value;
setLocalInputs((prev) => ({ ...prev, height: newValue }));
onUpdateProperty("size", { ...selectedComponent.size, height: Number(newValue) });
}}
2025-09-02 16:18:38 +09:00
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"
2025-09-02 16:46:54 +09:00
value={localInputs.positionZ}
onChange={(e) => {
const newValue = e.target.value;
setLocalInputs((prev) => ({ ...prev, positionZ: newValue }));
onUpdateProperty("position", { ...selectedComponent.position, z: Number(newValue) });
}}
2025-09-02 16:18:38 +09:00
className="mt-1"
placeholder="1"
/>
</div>
</div>
</div>
2025-09-02 16:46:54 +09:00
<Separator />
{/* 라벨 스타일 */}
<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="flex items-center justify-between">
<Label htmlFor="labelDisplay" className="text-sm font-medium">
</Label>
<Checkbox
id="labelDisplay"
checked={selectedComponent.style?.labelDisplay !== false}
onCheckedChange={(checked) => onUpdateProperty("style.labelDisplay", checked)}
/>
</div>
{/* 라벨 텍스트 */}
<div>
<Label htmlFor="labelText" className="text-sm font-medium">
</Label>
<Input
id="labelText"
value={localInputs.labelText}
onChange={(e) => {
const newValue = e.target.value;
setLocalInputs((prev) => ({ ...prev, labelText: newValue }));
// 기본 라벨과 스타일 라벨을 모두 업데이트
onUpdateProperty("label", newValue);
onUpdateProperty("style.labelText", newValue);
}}
placeholder="라벨 텍스트"
className="mt-1"
/>
</div>
{/* 라벨 스타일 */}
<div className="grid grid-cols-2 gap-3">
<div>
<Label htmlFor="labelFontSize" className="text-sm font-medium">
</Label>
<Input
id="labelFontSize"
value={localInputs.labelFontSize}
onChange={(e) => {
const newValue = e.target.value;
setLocalInputs((prev) => ({ ...prev, labelFontSize: newValue }));
onUpdateProperty("style.labelFontSize", newValue);
}}
placeholder="12px"
className="mt-1"
/>
</div>
<div>
<Label htmlFor="labelColor" className="text-sm font-medium">
</Label>
<Input
id="labelColor"
type="color"
value={localInputs.labelColor}
onChange={(e) => {
const newValue = e.target.value;
setLocalInputs((prev) => ({ ...prev, labelColor: newValue }));
onUpdateProperty("style.labelColor", newValue);
}}
className="mt-1 h-8"
/>
</div>
<div>
<Label htmlFor="labelFontWeight" className="text-sm font-medium">
</Label>
<Select
value={selectedComponent.style?.labelFontWeight || "500"}
onValueChange={(value) => onUpdateProperty("style.labelFontWeight", value)}
>
<SelectTrigger className="mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="normal">Normal</SelectItem>
<SelectItem value="bold">Bold</SelectItem>
<SelectItem value="100">100</SelectItem>
<SelectItem value="200">200</SelectItem>
<SelectItem value="300">300</SelectItem>
<SelectItem value="400">400</SelectItem>
<SelectItem value="500">500</SelectItem>
<SelectItem value="600">600</SelectItem>
<SelectItem value="700">700</SelectItem>
<SelectItem value="800">800</SelectItem>
<SelectItem value="900">900</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="labelTextAlign" className="text-sm font-medium">
</Label>
<Select
value={selectedComponent.style?.labelTextAlign || "left"}
onValueChange={(value) => onUpdateProperty("style.labelTextAlign", value)}
>
<SelectTrigger className="mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="left"></SelectItem>
<SelectItem value="center"></SelectItem>
<SelectItem value="right"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* 라벨 여백 */}
<div>
<Label htmlFor="labelMarginBottom" className="text-sm font-medium">
</Label>
<Input
id="labelMarginBottom"
value={localInputs.labelMarginBottom}
onChange={(e) => {
const newValue = e.target.value;
setLocalInputs((prev) => ({ ...prev, labelMarginBottom: newValue }));
onUpdateProperty("style.labelMarginBottom", newValue);
}}
placeholder="4px"
className="mt-1"
/>
</div>
</div>
2025-09-02 16:18:38 +09:00
{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"
2025-09-02 16:46:54 +09:00
value={localInputs.title}
onChange={(e) => {
const newValue = e.target.value;
setLocalInputs((prev) => ({ ...prev, title: newValue }));
onUpdateProperty("title", newValue);
}}
2025-09-02 16:18:38 +09:00
placeholder="그룹 제목"
className="mt-1"
/>
</div>
</div>
</>
)}
</div>
</div>
);
};
export default PropertiesPanel;