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

652 lines
25 KiB
TypeScript
Raw Normal View History

2025-09-02 16:18:38 +09:00
"use client";
2025-09-03 11:32:09 +09:00
import React, { useState, useEffect, useRef } from "react";
2025-09-02 16:18:38 +09:00
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";
2025-09-03 11:32:09 +09:00
import { Settings, Move, Type, Trash2, Copy, Group, Ungroup } from "lucide-react";
2025-09-03 15:23:12 +09:00
import { ComponentData, WebType, WidgetComponent, GroupComponent, DataTableComponent, TableInfo } from "@/types/screen";
import DataTableConfigPanel from "./DataTableConfigPanel";
2025-09-02 16:18:38 +09:00
interface PropertiesPanelProps {
selectedComponent?: ComponentData;
2025-09-03 15:23:12 +09:00
tables?: TableInfo[];
2025-09-03 11:32:09 +09:00
onUpdateProperty: (path: string, value: unknown) => void;
2025-09-02 16:18:38 +09:00
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,
2025-09-03 15:23:12 +09:00
tables = [],
2025-09-02 16:18:38 +09:00
onUpdateProperty,
onDeleteComponent,
onCopyComponent,
onGroupComponents,
onUngroupComponents,
canGroup = false,
canUngroup = false,
}) => {
2025-09-03 18:23:47 +09:00
// 데이터테이블 설정 탭 상태를 여기서 관리
const [dataTableActiveTab, setDataTableActiveTab] = useState("basic");
2025-09-02 16:18:38 +09:00
// 최신 값들의 참조를 유지
const selectedComponentRef = useRef(selectedComponent);
const onUpdatePropertyRef = useRef(onUpdateProperty);
2025-09-02 16:46:54 +09:00
// 입력 필드들의 로컬 상태 (실시간 타이핑 반영용)
const [localInputs, setLocalInputs] = useState({
2025-09-03 11:32:09 +09:00
placeholder: (selectedComponent?.type === "widget" ? (selectedComponent as WidgetComponent).placeholder : "") || "",
title: (selectedComponent?.type === "group" ? (selectedComponent as GroupComponent).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",
2025-09-03 11:32:09 +09:00
gridColumns: selectedComponent?.gridColumns?.toString() || "1",
2025-09-02 16:46:54 +09:00
labelText: selectedComponent?.style?.labelText || selectedComponent?.label || "",
labelFontSize: selectedComponent?.style?.labelFontSize || "12px",
labelColor: selectedComponent?.style?.labelColor || "#374151",
labelMarginBottom: selectedComponent?.style?.labelMarginBottom || "4px",
2025-09-03 11:32:09 +09:00
required: (selectedComponent?.type === "widget" ? (selectedComponent as WidgetComponent).required : false) || false,
readonly: (selectedComponent?.type === "widget" ? (selectedComponent as WidgetComponent).readonly : false) || false,
2025-09-03 15:23:12 +09:00
labelDisplay: selectedComponent?.style?.labelDisplay !== false,
2025-09-02 16:46:54 +09:00
});
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-03 11:32:09 +09:00
const widget = selectedComponent.type === "widget" ? (selectedComponent as WidgetComponent) : null;
const group = selectedComponent.type === "group" ? (selectedComponent as GroupComponent) : null;
2025-09-03 15:23:12 +09:00
console.log("🔄 PropertiesPanel: 컴포넌트 변경 감지", {
componentId: selectedComponent.id,
componentType: selectedComponent.type,
currentValues: {
placeholder: widget?.placeholder,
title: group?.title,
positionX: selectedComponent.position.x,
labelText: selectedComponent.style?.labelText || selectedComponent.label,
},
});
2025-09-02 16:46:54 +09:00
setLocalInputs({
2025-09-03 11:32:09 +09:00
placeholder: widget?.placeholder || "",
title: group?.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",
2025-09-03 11:32:09 +09:00
gridColumns: selectedComponent.gridColumns?.toString() || "1",
2025-09-02 16:46:54 +09:00
labelText: selectedComponent.style?.labelText || selectedComponent.label || "",
labelFontSize: selectedComponent.style?.labelFontSize || "12px",
labelColor: selectedComponent.style?.labelColor || "#374151",
labelMarginBottom: selectedComponent.style?.labelMarginBottom || "4px",
2025-09-03 11:32:09 +09:00
required: widget?.required || false,
readonly: widget?.readonly || false,
2025-09-03 15:23:12 +09:00
labelDisplay: selectedComponent.style?.labelDisplay !== false,
2025-09-02 16:18:38 +09:00
});
}
2025-09-03 15:23:12 +09:00
}, [
selectedComponent,
selectedComponent?.position,
selectedComponent?.size,
selectedComponent?.style,
selectedComponent?.label,
]);
2025-09-02 16:18:38 +09:00
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>
);
}
2025-09-03 15:23:12 +09:00
// 데이터 테이블 컴포넌트인 경우 전용 패널 사용
if (selectedComponent.type === "datatable") {
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-5 w-5 text-gray-600" />
<span className="text-lg font-semibold"> </span>
</div>
<Badge variant="secondary" className="text-xs">
{selectedComponent.type}
</Badge>
</div>
{/* 액션 버튼들 */}
<div className="flex flex-wrap gap-2">
<Button variant="outline" size="sm" onClick={onCopyComponent}>
<Copy className="mr-1 h-4 w-4" />
</Button>
<Button variant="destructive" size="sm" onClick={onDeleteComponent}>
<Trash2 className="mr-1 h-4 w-4" />
</Button>
</div>
</div>
{/* 데이터 테이블 설정 패널 */}
<div className="flex-1 overflow-y-auto">
<DataTableConfigPanel
key={`datatable-${selectedComponent.id}-${selectedComponent.columns.length}-${selectedComponent.filters.length}-${JSON.stringify(selectedComponent.columns.map((c) => c.id))}-${JSON.stringify(selectedComponent.filters.map((f) => f.columnName))}`}
component={selectedComponent as DataTableComponent}
tables={tables}
2025-09-03 18:23:47 +09:00
activeTab={dataTableActiveTab}
onTabChange={setDataTableActiveTab}
2025-09-03 15:23:12 +09:00
onUpdateComponent={(updates) => {
console.log("🔄 DataTable 컴포넌트 업데이트:", updates);
console.log("🔄 업데이트 항목들:", Object.keys(updates));
// 각 속성을 개별적으로 업데이트
Object.entries(updates).forEach(([key, value]) => {
console.log(` - ${key}:`, value);
if (key === "columns") {
console.log(` 컬럼 개수: ${Array.isArray(value) ? value.length : 0}`);
}
if (key === "filters") {
console.log(` 필터 개수: ${Array.isArray(value) ? value.length : 0}`);
}
onUpdateProperty(key, value);
});
console.log("✅ DataTable 컴포넌트 업데이트 완료");
}}
/>
</div>
</div>
);
}
2025-09-02 16:18:38 +09:00
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;
2025-09-03 15:23:12 +09:00
console.log("🔄 placeholder 변경:", newValue);
2025-09-02 16:46:54 +09:00
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>
2025-09-03 11:32:09 +09:00
<div>
<Label htmlFor="gridColumns" className="text-sm font-medium">
(1-12)
</Label>
<Input
id="gridColumns"
type="number"
min="1"
max="12"
value={localInputs.gridColumns}
onChange={(e) => {
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"
/>
<div className="mt-1 text-xs text-gray-500">
(기본: 1)
</div>
</div>
2025-09-02 16:18:38 +09:00
</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"
2025-09-03 15:23:12 +09:00
checked={localInputs.labelDisplay}
onCheckedChange={(checked) => {
console.log("🔄 라벨 표시 변경:", checked);
setLocalInputs((prev) => ({ ...prev, labelDisplay: checked as boolean }));
onUpdateProperty("style.labelDisplay", checked);
}}
2025-09-02 16:46:54 +09:00
/>
</div>
{/* 라벨 텍스트 */}
<div>
<Label htmlFor="labelText" className="text-sm font-medium">
</Label>
<Input
id="labelText"
value={localInputs.labelText}
onChange={(e) => {
const newValue = e.target.value;
2025-09-03 15:23:12 +09:00
console.log("🔄 라벨 텍스트 변경:", newValue);
2025-09-02 16:46:54 +09:00
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;