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;
|