feature/v2-unified-renewal #379
|
|
@ -4,13 +4,24 @@ import { useState, useEffect } from "react";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||||
import { Palette, Type, Square } from "lucide-react";
|
import { Palette, Type, Square, ChevronDown } from "lucide-react";
|
||||||
import { ComponentStyle } from "@/types/screen";
|
import { ComponentStyle } from "@/types/screen";
|
||||||
import { ColorPickerWithTransparent } from "./common/ColorPickerWithTransparent";
|
import { ColorPickerWithTransparent } from "./common/ColorPickerWithTransparent";
|
||||||
|
|
||||||
|
interface StyleEditorProps {
|
||||||
|
style: ComponentStyle;
|
||||||
|
onStyleChange: (style: ComponentStyle) => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function StyleEditor({ style, onStyleChange, className }: StyleEditorProps) {
|
export default function StyleEditor({ style, onStyleChange, className }: StyleEditorProps) {
|
||||||
const [localStyle, setLocalStyle] = useState<ComponentStyle>(style || {});
|
const [localStyle, setLocalStyle] = useState<ComponentStyle>(style || {});
|
||||||
|
const [openSections, setOpenSections] = useState<Record<string, boolean>>({
|
||||||
|
border: false,
|
||||||
|
background: false,
|
||||||
|
text: false,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLocalStyle(style || {});
|
setLocalStyle(style || {});
|
||||||
|
|
@ -22,232 +33,255 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
|
||||||
onStyleChange(newStyle);
|
onStyleChange(newStyle);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const toggleSection = (section: string) => {
|
||||||
<div className={`space-y-4 p-3 ${className}`}>
|
setOpenSections((prev) => ({
|
||||||
{/* 테두리 섹션 */}
|
...prev,
|
||||||
<div className="space-y-2">
|
[section]: !prev[section],
|
||||||
<div className="flex items-center gap-2">
|
}));
|
||||||
<Square className="text-primary h-3.5 w-3.5" />
|
};
|
||||||
<h3 className="text-sm font-semibold">테두리</h3>
|
|
||||||
</div>
|
|
||||||
<Separator className="my-1.5" />
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label htmlFor="borderWidth" className="text-xs font-medium">
|
|
||||||
두께
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="borderWidth"
|
|
||||||
type="text"
|
|
||||||
placeholder="1px"
|
|
||||||
value={localStyle.borderWidth || ""}
|
|
||||||
onChange={(e) => handleStyleChange("borderWidth", e.target.value)}
|
|
||||||
className="h-6 w-full px-2 py-0 text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label htmlFor="borderStyle" className="text-xs font-medium">
|
|
||||||
스타일
|
|
||||||
</Label>
|
|
||||||
<Select
|
|
||||||
value={localStyle.borderStyle || "solid"}
|
|
||||||
onValueChange={(value) => handleStyleChange("borderStyle", value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className=" text-xsh-6 w-full px-2 py-0">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="solid" className="text-xs">
|
|
||||||
실선
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="dashed" className="text-xs">
|
|
||||||
파선
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="dotted" className="text-xs">
|
|
||||||
점선
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="none" className="text-xs">
|
|
||||||
없음
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
return (
|
||||||
<div className="space-y-1">
|
<div className={`space-y-2 ${className}`}>
|
||||||
<Label htmlFor="borderColor" className="text-xs font-medium">
|
{/* 테두리 섹션 */}
|
||||||
색상
|
<Collapsible open={openSections.border} onOpenChange={() => toggleSection("border")}>
|
||||||
</Label>
|
<CollapsibleTrigger className="flex w-full items-center justify-between rounded-md bg-gray-50 px-2 py-1.5 hover:bg-gray-100">
|
||||||
<ColorPickerWithTransparent
|
<div className="flex items-center gap-1.5">
|
||||||
id="borderColor"
|
<Square className="text-primary h-3 w-3" />
|
||||||
value={localStyle.borderColor}
|
<span className="text-xs font-medium">테두리</span>
|
||||||
onChange={(value) => handleStyleChange("borderColor", value)}
|
</div>
|
||||||
defaultColor="#e5e7eb"
|
<ChevronDown
|
||||||
placeholder="#e5e7eb"
|
className={`h-3 w-3 text-gray-500 transition-transform ${openSections.border ? "rotate-180" : ""}`}
|
||||||
/>
|
/>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent className="pt-2">
|
||||||
|
<div className="space-y-2 pl-1">
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label htmlFor="borderWidth" className="text-xs font-medium">
|
||||||
|
두께
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="borderWidth"
|
||||||
|
type="text"
|
||||||
|
placeholder="1px"
|
||||||
|
value={localStyle.borderWidth || ""}
|
||||||
|
onChange={(e) => handleStyleChange("borderWidth", e.target.value)}
|
||||||
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label htmlFor="borderStyle" className="text-xs font-medium">
|
||||||
|
스타일
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={localStyle.borderStyle || "solid"}
|
||||||
|
onValueChange={(value) => handleStyleChange("borderStyle", value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="solid" className="text-xs">
|
||||||
|
실선
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="dashed" className="text-xs">
|
||||||
|
파선
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="dotted" className="text-xs">
|
||||||
|
점선
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="none" className="text-xs">
|
||||||
|
없음
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
|
||||||
<Label htmlFor="borderRadius" className="text-xs font-medium">
|
<div className="space-y-2">
|
||||||
모서리
|
<div className="space-y-1">
|
||||||
</Label>
|
<Label htmlFor="borderColor" className="text-xs font-medium">
|
||||||
<Input
|
색상
|
||||||
id="borderRadius"
|
</Label>
|
||||||
type="text"
|
<ColorPickerWithTransparent
|
||||||
placeholder="5px"
|
id="borderColor"
|
||||||
value={localStyle.borderRadius || ""}
|
value={localStyle.borderColor}
|
||||||
onChange={(e) => handleStyleChange("borderRadius", e.target.value)}
|
onChange={(value) => handleStyleChange("borderColor", value)}
|
||||||
className="h-6 w-full px-2 py-0 text-xs"
|
defaultColor="#e5e7eb"
|
||||||
/>
|
placeholder="#e5e7eb"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label htmlFor="borderRadius" className="text-xs font-medium">
|
||||||
|
모서리
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="borderRadius"
|
||||||
|
type="text"
|
||||||
|
placeholder="5px"
|
||||||
|
value={localStyle.borderRadius || ""}
|
||||||
|
onChange={(e) => handleStyleChange("borderRadius", e.target.value)}
|
||||||
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CollapsibleContent>
|
||||||
</div>
|
</Collapsible>
|
||||||
|
|
||||||
{/* 배경 섹션 */}
|
{/* 배경 섹션 */}
|
||||||
<div className="space-y-2">
|
<Collapsible open={openSections.background} onOpenChange={() => toggleSection("background")}>
|
||||||
<div className="flex items-center gap-2">
|
<CollapsibleTrigger className="flex w-full items-center justify-between rounded-md bg-gray-50 px-2 py-1.5 hover:bg-gray-100">
|
||||||
<Palette className="text-primary h-3.5 w-3.5" />
|
<div className="flex items-center gap-1.5">
|
||||||
<h3 className="text-sm font-semibold">배경</h3>
|
<Palette className="text-primary h-3 w-3" />
|
||||||
</div>
|
<span className="text-xs font-medium">배경</span>
|
||||||
<Separator className="my-1.5" />
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label htmlFor="backgroundColor" className="text-xs font-medium">
|
|
||||||
색상
|
|
||||||
</Label>
|
|
||||||
<ColorPickerWithTransparent
|
|
||||||
id="backgroundColor"
|
|
||||||
value={localStyle.backgroundColor}
|
|
||||||
onChange={(value) => handleStyleChange("backgroundColor", value)}
|
|
||||||
defaultColor="#ffffff"
|
|
||||||
placeholder="#ffffff"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<ChevronDown
|
||||||
<div className="space-y-1">
|
className={`h-3 w-3 text-gray-500 transition-transform ${openSections.background ? "rotate-180" : ""}`}
|
||||||
<Label htmlFor="backgroundImage" className="text-xs font-medium">
|
/>
|
||||||
배경 이미지 (CSS)
|
</CollapsibleTrigger>
|
||||||
</Label>
|
<CollapsibleContent className="pt-2">
|
||||||
<Input
|
<div className="space-y-2 pl-1">
|
||||||
id="backgroundImage"
|
|
||||||
type="text"
|
|
||||||
placeholder="url('image.jpg')"
|
|
||||||
value={localStyle.backgroundImage || ""}
|
|
||||||
onChange={(e) => handleStyleChange("backgroundImage", e.target.value)}
|
|
||||||
className="h-6 w-full px-2 py-0 text-xs"
|
|
||||||
/>
|
|
||||||
<p className="text-[10px] text-muted-foreground">
|
|
||||||
위젯 배경 꾸미기용 (고급 사용자 전용)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 텍스트 섹션 */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Type className="text-primary h-3.5 w-3.5" />
|
|
||||||
<h3 className="text-sm font-semibold">텍스트</h3>
|
|
||||||
</div>
|
|
||||||
<Separator className="my-1.5" />
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label htmlFor="color" className="text-xs font-medium">
|
<Label htmlFor="backgroundColor" className="text-xs font-medium">
|
||||||
색상
|
색상
|
||||||
</Label>
|
</Label>
|
||||||
<ColorPickerWithTransparent
|
<ColorPickerWithTransparent
|
||||||
id="color"
|
id="backgroundColor"
|
||||||
value={localStyle.color}
|
value={localStyle.backgroundColor}
|
||||||
onChange={(value) => handleStyleChange("color", value)}
|
onChange={(value) => handleStyleChange("backgroundColor", value)}
|
||||||
defaultColor="#000000"
|
defaultColor="#ffffff"
|
||||||
placeholder="#000000"
|
placeholder="#ffffff"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label htmlFor="fontSize" className="text-xs font-medium">
|
<Label htmlFor="backgroundImage" className="text-xs font-medium">
|
||||||
크기
|
배경 이미지 (CSS)
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="fontSize"
|
id="backgroundImage"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="14px"
|
placeholder="url('image.jpg')"
|
||||||
value={localStyle.fontSize || ""}
|
value={localStyle.backgroundImage || ""}
|
||||||
onChange={(e) => handleStyleChange("fontSize", e.target.value)}
|
onChange={(e) => handleStyleChange("backgroundImage", e.target.value)}
|
||||||
className="h-6 w-full px-2 py-0 text-xs"
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
/>
|
/>
|
||||||
|
<p className="text-muted-foreground text-[10px]">위젯 배경 꾸미기용 (고급 사용자 전용)</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
{/* 텍스트 섹션 */}
|
||||||
<div className="space-y-1">
|
<Collapsible open={openSections.text} onOpenChange={() => toggleSection("text")}>
|
||||||
<Label htmlFor="fontWeight" className="text-xs font-medium">
|
<CollapsibleTrigger className="flex w-full items-center justify-between rounded-md bg-gray-50 px-2 py-1.5 hover:bg-gray-100">
|
||||||
굵기
|
<div className="flex items-center gap-1.5">
|
||||||
</Label>
|
<Type className="text-primary h-3 w-3" />
|
||||||
<Select
|
<span className="text-xs font-medium">텍스트</span>
|
||||||
value={localStyle.fontWeight || "normal"}
|
</div>
|
||||||
onValueChange={(value) => handleStyleChange("fontWeight", value)}
|
<ChevronDown
|
||||||
>
|
className={`h-3 w-3 text-gray-500 transition-transform ${openSections.text ? "rotate-180" : ""}`}
|
||||||
<SelectTrigger className=" text-xsh-6 w-full px-2 py-0">
|
/>
|
||||||
<SelectValue />
|
</CollapsibleTrigger>
|
||||||
</SelectTrigger>
|
<CollapsibleContent className="pt-2">
|
||||||
<SelectContent>
|
<div className="space-y-2 pl-1">
|
||||||
<SelectItem value="normal" className="text-xs">
|
<div className="space-y-2">
|
||||||
보통
|
<div className="space-y-1">
|
||||||
</SelectItem>
|
<Label htmlFor="color" className="text-xs font-medium">
|
||||||
<SelectItem value="bold" className="text-xs">
|
색상
|
||||||
굵게
|
</Label>
|
||||||
</SelectItem>
|
<ColorPickerWithTransparent
|
||||||
<SelectItem value="100" className="text-xs">
|
id="color"
|
||||||
100
|
value={localStyle.color}
|
||||||
</SelectItem>
|
onChange={(value) => handleStyleChange("color", value)}
|
||||||
<SelectItem value="400" className="text-xs">
|
defaultColor="#000000"
|
||||||
400
|
placeholder="#000000"
|
||||||
</SelectItem>
|
/>
|
||||||
<SelectItem value="500" className="text-xs">
|
</div>
|
||||||
500
|
<div className="space-y-1">
|
||||||
</SelectItem>
|
<Label htmlFor="fontSize" className="text-xs font-medium">
|
||||||
<SelectItem value="600" className="text-xs">
|
크기
|
||||||
600
|
</Label>
|
||||||
</SelectItem>
|
<Input
|
||||||
<SelectItem value="700" className="text-xs">
|
id="fontSize"
|
||||||
700
|
type="text"
|
||||||
</SelectItem>
|
placeholder="14px"
|
||||||
</SelectContent>
|
value={localStyle.fontSize || ""}
|
||||||
</Select>
|
onChange={(e) => handleStyleChange("fontSize", e.target.value)}
|
||||||
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
|
||||||
<Label htmlFor="textAlign" className="text-xs font-medium">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
정렬
|
<div className="space-y-1">
|
||||||
</Label>
|
<Label htmlFor="fontWeight" className="text-xs font-medium">
|
||||||
<Select
|
굵기
|
||||||
value={localStyle.textAlign || "left"}
|
</Label>
|
||||||
onValueChange={(value) => handleStyleChange("textAlign", value)}
|
<Select
|
||||||
>
|
value={localStyle.fontWeight || "normal"}
|
||||||
<SelectTrigger className=" text-xsh-6 w-full px-2 py-0">
|
onValueChange={(value) => handleStyleChange("fontWeight", value)}
|
||||||
<SelectValue />
|
>
|
||||||
</SelectTrigger>
|
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs">
|
||||||
<SelectContent>
|
<SelectValue />
|
||||||
<SelectItem value="left" className="text-xs">
|
</SelectTrigger>
|
||||||
왼쪽
|
<SelectContent>
|
||||||
</SelectItem>
|
<SelectItem value="normal" className="text-xs">
|
||||||
<SelectItem value="center" className="text-xs">
|
보통
|
||||||
가운데
|
</SelectItem>
|
||||||
</SelectItem>
|
<SelectItem value="bold" className="text-xs">
|
||||||
<SelectItem value="right" className="text-xs">
|
굵게
|
||||||
오른쪽
|
</SelectItem>
|
||||||
</SelectItem>
|
<SelectItem value="100" className="text-xs">
|
||||||
<SelectItem value="justify" className="text-xs">
|
100
|
||||||
양쪽
|
</SelectItem>
|
||||||
</SelectItem>
|
<SelectItem value="400" className="text-xs">
|
||||||
</SelectContent>
|
400
|
||||||
</Select>
|
</SelectItem>
|
||||||
|
<SelectItem value="500" className="text-xs">
|
||||||
|
500
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="600" className="text-xs">
|
||||||
|
600
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="700" className="text-xs">
|
||||||
|
700
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label htmlFor="textAlign" className="text-xs font-medium">
|
||||||
|
정렬
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={localStyle.textAlign || "left"}
|
||||||
|
onValueChange={(value) => handleStyleChange("textAlign", value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="left" className="text-xs">
|
||||||
|
왼쪽
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="center" className="text-xs">
|
||||||
|
가운데
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="right" className="text-xs">
|
||||||
|
오른쪽
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="justify" className="text-xs">
|
||||||
|
양쪽
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CollapsibleContent>
|
||||||
</div>
|
</Collapsible>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -228,18 +228,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
onUpdateProperty(selectedComponent.id, "componentConfig", { ...currentConfig, ...newConfig });
|
onUpdateProperty(selectedComponent.id, "componentConfig", { ...currentConfig, ...newConfig });
|
||||||
};
|
};
|
||||||
|
|
||||||
const unifiedNames: Record<string, string> = {
|
|
||||||
"unified-input": "통합 입력",
|
|
||||||
"unified-select": "통합 선택",
|
|
||||||
"unified-date": "통합 날짜",
|
|
||||||
"unified-list": "통합 목록",
|
|
||||||
"unified-layout": "통합 레이아웃",
|
|
||||||
"unified-group": "통합 그룹",
|
|
||||||
"unified-media": "통합 미디어",
|
|
||||||
"unified-biz": "통합 비즈니스",
|
|
||||||
"unified-hierarchy": "통합 계층",
|
|
||||||
};
|
|
||||||
|
|
||||||
// 컬럼의 inputType 가져오기 (entity 타입인지 확인용)
|
// 컬럼의 inputType 가져오기 (entity 타입인지 확인용)
|
||||||
const inputType = currentConfig.inputType || currentConfig.webType || (selectedComponent as any).inputType;
|
const inputType = currentConfig.inputType || currentConfig.webType || (selectedComponent as any).inputType;
|
||||||
|
|
||||||
|
|
@ -257,10 +245,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={selectedComponent.id} className="space-y-4">
|
<div key={selectedComponent.id} className="space-y-4">
|
||||||
<div className="flex items-center gap-2 border-b pb-2">
|
|
||||||
<Settings className="text-primary h-4 w-4" />
|
|
||||||
<h3 className="text-sm font-semibold">{unifiedNames[componentId] || componentId} 설정</h3>
|
|
||||||
</div>
|
|
||||||
<UnifiedConfigPanel config={currentConfig} onChange={handleUnifiedConfigChange} {...extraProps} />
|
<UnifiedConfigPanel config={currentConfig} onChange={handleUnifiedConfigChange} {...extraProps} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -301,10 +285,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={selectedComponent.id} className="space-y-4">
|
<div key={selectedComponent.id} className="space-y-4">
|
||||||
<div className="flex items-center gap-2 border-b pb-2">
|
|
||||||
<Settings className="text-primary h-4 w-4" />
|
|
||||||
<h3 className="text-sm font-semibold">{definition.name} 설정</h3>
|
|
||||||
</div>
|
|
||||||
<Suspense
|
<Suspense
|
||||||
fallback={
|
fallback={
|
||||||
<div className="flex items-center justify-center py-8">
|
<div className="flex items-center justify-center py-8">
|
||||||
|
|
@ -669,16 +649,89 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
const group = selectedComponent as GroupComponent;
|
const group = selectedComponent as GroupComponent;
|
||||||
const area = selectedComponent as AreaComponent;
|
const area = selectedComponent as AreaComponent;
|
||||||
|
|
||||||
|
// 라벨 설정이 표시될 입력 필드 타입들
|
||||||
|
const inputFieldTypes = [
|
||||||
|
"text",
|
||||||
|
"number",
|
||||||
|
"decimal",
|
||||||
|
"date",
|
||||||
|
"datetime",
|
||||||
|
"time",
|
||||||
|
"email",
|
||||||
|
"tel",
|
||||||
|
"url",
|
||||||
|
"password",
|
||||||
|
"textarea",
|
||||||
|
"select",
|
||||||
|
"dropdown",
|
||||||
|
"entity",
|
||||||
|
"code",
|
||||||
|
"checkbox",
|
||||||
|
"radio",
|
||||||
|
"boolean",
|
||||||
|
"file",
|
||||||
|
"autocomplete",
|
||||||
|
"text-input",
|
||||||
|
"number-input",
|
||||||
|
"date-input",
|
||||||
|
"textarea-basic",
|
||||||
|
"select-basic",
|
||||||
|
"checkbox-basic",
|
||||||
|
"radio-basic",
|
||||||
|
"entity-search-input",
|
||||||
|
"autocomplete-search-input",
|
||||||
|
// 새로운 통합 입력 컴포넌트
|
||||||
|
"unified-input",
|
||||||
|
"unified-select",
|
||||||
|
"unified-entity-select",
|
||||||
|
"unified-checkbox",
|
||||||
|
"unified-radio",
|
||||||
|
"unified-textarea",
|
||||||
|
"unified-date",
|
||||||
|
"unified-datetime",
|
||||||
|
"unified-time",
|
||||||
|
"unified-file",
|
||||||
|
];
|
||||||
|
|
||||||
|
// 현재 컴포넌트가 입력 필드인지 확인
|
||||||
|
const componentType = widget.widgetType || (widget as any).componentId || (widget as any).componentType;
|
||||||
|
const isInputField = inputFieldTypes.includes(componentType);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{/* 라벨 + 최소 높이 (같은 행) */}
|
{/* 너비 + 높이 (같은 행) */}
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs">라벨</Label>
|
<Label className="text-xs">너비 (px)</Label>
|
||||||
<Input
|
<Input
|
||||||
value={widget.label || ""}
|
type="number"
|
||||||
onChange={(e) => handleUpdate("label", e.target.value)}
|
min={10}
|
||||||
placeholder="라벨"
|
max={3840}
|
||||||
|
step="1"
|
||||||
|
value={localWidth}
|
||||||
|
onChange={(e) => {
|
||||||
|
setLocalWidth(e.target.value);
|
||||||
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
const value = parseInt(e.target.value) || 0;
|
||||||
|
if (value >= 10) {
|
||||||
|
const snappedValue = Math.round(value / 10) * 10;
|
||||||
|
handleUpdate("size.width", snappedValue);
|
||||||
|
setLocalWidth(String(snappedValue));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
const value = parseInt(e.currentTarget.value) || 0;
|
||||||
|
if (value >= 10) {
|
||||||
|
const snappedValue = Math.round(value / 10) * 10;
|
||||||
|
handleUpdate("size.width", snappedValue);
|
||||||
|
setLocalWidth(String(snappedValue));
|
||||||
|
}
|
||||||
|
e.currentTarget.blur();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="100"
|
||||||
className="h-6 w-full px-2 py-0 text-xs"
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -688,11 +741,9 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
type="number"
|
type="number"
|
||||||
value={localHeight}
|
value={localHeight}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
// 입력 중에는 로컬 상태만 업데이트 (자유 입력)
|
|
||||||
setLocalHeight(e.target.value);
|
setLocalHeight(e.target.value);
|
||||||
}}
|
}}
|
||||||
onBlur={(e) => {
|
onBlur={(e) => {
|
||||||
// 포커스를 잃을 때 10px 단위로 스냅
|
|
||||||
const value = parseInt(e.target.value) || 0;
|
const value = parseInt(e.target.value) || 0;
|
||||||
if (value >= 10) {
|
if (value >= 10) {
|
||||||
const snappedValue = Math.round(value / 10) * 10;
|
const snappedValue = Math.round(value / 10) * 10;
|
||||||
|
|
@ -701,7 +752,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
// Enter 키를 누르면 즉시 적용 (10px 단위로 스냅)
|
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
const value = parseInt(e.currentTarget.value) || 0;
|
const value = parseInt(e.currentTarget.value) || 0;
|
||||||
if (value >= 10) {
|
if (value >= 10) {
|
||||||
|
|
@ -709,7 +759,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
handleUpdate("size.height", snappedValue);
|
handleUpdate("size.height", snappedValue);
|
||||||
setLocalHeight(String(snappedValue));
|
setLocalHeight(String(snappedValue));
|
||||||
}
|
}
|
||||||
e.currentTarget.blur(); // 포커스 제거
|
e.currentTarget.blur();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
step={1}
|
step={1}
|
||||||
|
|
@ -719,19 +769,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Placeholder (widget만) */}
|
|
||||||
{selectedComponent.type === "widget" && (
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label className="text-xs">Placeholder</Label>
|
|
||||||
<Input
|
|
||||||
value={widget.placeholder || ""}
|
|
||||||
onChange={(e) => handleUpdate("placeholder", e.target.value)}
|
|
||||||
placeholder="입력 안내 텍스트"
|
|
||||||
className="h-6 w-full px-2 py-0 text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Title (group/area) */}
|
{/* Title (group/area) */}
|
||||||
{(selectedComponent.type === "group" || selectedComponent.type === "area") && (
|
{(selectedComponent.type === "group" || selectedComponent.type === "area") && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
|
|
@ -758,116 +795,74 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Width + Z-Index (같은 행) */}
|
{/* Z-Index */}
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="space-y-1">
|
||||||
<div className="space-y-1">
|
<Label className="text-xs">Z-Index</Label>
|
||||||
<Label className="text-xs">너비 (px)</Label>
|
<Input
|
||||||
<div className="flex items-center gap-1">
|
type="number"
|
||||||
<Input
|
step="1"
|
||||||
type="number"
|
value={currentPosition.z || 1}
|
||||||
min={10}
|
onChange={(e) => handleUpdate("position.z", parseInt(e.target.value) || 1)}
|
||||||
max={3840}
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
step="1"
|
/>
|
||||||
value={localWidth}
|
|
||||||
onChange={(e) => {
|
|
||||||
// 입력 중에는 로컬 상태만 업데이트 (자유 입력)
|
|
||||||
setLocalWidth(e.target.value);
|
|
||||||
}}
|
|
||||||
onBlur={(e) => {
|
|
||||||
// 포커스를 잃을 때 10px 단위로 스냅
|
|
||||||
const value = parseInt(e.target.value, 10);
|
|
||||||
if (!isNaN(value) && value >= 10) {
|
|
||||||
const snappedValue = Math.round(value / 10) * 10;
|
|
||||||
handleUpdate("size.width", snappedValue);
|
|
||||||
setLocalWidth(String(snappedValue));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
// Enter 키를 누르면 즉시 적용 (10px 단위로 스냅)
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
const value = parseInt(e.currentTarget.value, 10);
|
|
||||||
if (!isNaN(value) && value >= 10) {
|
|
||||||
const snappedValue = Math.round(value / 10) * 10;
|
|
||||||
handleUpdate("size.width", snappedValue);
|
|
||||||
setLocalWidth(String(snappedValue));
|
|
||||||
}
|
|
||||||
e.currentTarget.blur(); // 포커스 제거
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="h-6 w-full px-2 py-0 text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label className="text-xs">Z-Index</Label>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
step="1"
|
|
||||||
value={currentPosition.z || 1}
|
|
||||||
onChange={(e) => handleUpdate("position.z", parseInt(e.target.value) || 1)}
|
|
||||||
className="h-6 w-full px-2 py-0 text-xs"
|
|
||||||
className="text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 라벨 스타일 */}
|
{/* 라벨 스타일 - 입력 필드에서만 표시 */}
|
||||||
<Collapsible>
|
{isInputField && (
|
||||||
<CollapsibleTrigger className="flex w-full items-center justify-between rounded-lg bg-slate-50 p-2 text-xs font-medium hover:bg-slate-100">
|
<Collapsible>
|
||||||
라벨 스타일
|
<CollapsibleTrigger className="flex w-full items-center justify-between rounded-lg bg-slate-50 p-2 text-xs font-medium hover:bg-slate-100">
|
||||||
<ChevronDown className="h-3.5 w-3.5" />
|
라벨 스타일
|
||||||
</CollapsibleTrigger>
|
<ChevronDown className="h-3.5 w-3.5" />
|
||||||
<CollapsibleContent className="mt-2 space-y-2">
|
</CollapsibleTrigger>
|
||||||
<div className="space-y-1">
|
<CollapsibleContent className="mt-2 space-y-2">
|
||||||
<Label className="text-xs">라벨 텍스트</Label>
|
|
||||||
<Input
|
|
||||||
value={selectedComponent.style?.labelText || selectedComponent.label || ""}
|
|
||||||
onChange={(e) => handleUpdate("style.labelText", e.target.value)}
|
|
||||||
className="h-6 w-full px-2 py-0 text-xs"
|
|
||||||
className="text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs">크기</Label>
|
<Label className="text-xs">라벨 텍스트</Label>
|
||||||
<Input
|
<Input
|
||||||
value={selectedComponent.style?.labelFontSize || "12px"}
|
value={selectedComponent.style?.labelText || selectedComponent.label || ""}
|
||||||
onChange={(e) => handleUpdate("style.labelFontSize", e.target.value)}
|
onChange={(e) => handleUpdate("style.labelText", e.target.value)}
|
||||||
className="h-6 w-full px-2 py-0 text-xs"
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
className="text-xs"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<Label className="text-xs">색상</Label>
|
<div className="space-y-1">
|
||||||
<ColorPickerWithTransparent
|
<Label className="text-xs">크기</Label>
|
||||||
value={selectedComponent.style?.labelColor}
|
<Input
|
||||||
onChange={(value) => handleUpdate("style.labelColor", value)}
|
value={selectedComponent.style?.labelFontSize || "12px"}
|
||||||
defaultColor="#212121"
|
onChange={(e) => handleUpdate("style.labelFontSize", e.target.value)}
|
||||||
placeholder="#212121"
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs">색상</Label>
|
||||||
|
<ColorPickerWithTransparent
|
||||||
|
value={selectedComponent.style?.labelColor}
|
||||||
|
onChange={(value) => handleUpdate("style.labelColor", value)}
|
||||||
|
defaultColor="#212121"
|
||||||
|
placeholder="#212121"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="space-y-1">
|
||||||
<div className="space-y-1">
|
<Label className="text-xs">여백</Label>
|
||||||
<Label className="text-xs">여백</Label>
|
<Input
|
||||||
<Input
|
value={selectedComponent.style?.labelMarginBottom || "4px"}
|
||||||
value={selectedComponent.style?.labelMarginBottom || "4px"}
|
onChange={(e) => handleUpdate("style.labelMarginBottom", e.target.value)}
|
||||||
onChange={(e) => handleUpdate("style.labelMarginBottom", e.target.value)}
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
className="h-6 w-full px-2 py-0 text-xs"
|
/>
|
||||||
className="text-xs"
|
</div>
|
||||||
/>
|
<div className="flex items-center space-x-2 pt-5">
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedComponent.style?.labelDisplay !== false}
|
||||||
|
onCheckedChange={(checked) => handleUpdate("style.labelDisplay", checked)}
|
||||||
|
className="h-4 w-4"
|
||||||
|
/>
|
||||||
|
<Label className="text-xs">표시</Label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2 pt-5">
|
</CollapsibleContent>
|
||||||
<Checkbox
|
</Collapsible>
|
||||||
checked={selectedComponent.style?.labelDisplay !== false}
|
)}
|
||||||
onCheckedChange={(checked) => handleUpdate("style.labelDisplay", checked)}
|
|
||||||
className="h-4 w-4"
|
|
||||||
/>
|
|
||||||
<Label className="text-xs">표시</Label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CollapsibleContent>
|
|
||||||
</Collapsible>
|
|
||||||
|
|
||||||
{/* 옵션 */}
|
{/* 옵션 */}
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
|
|
||||||
|
|
@ -170,12 +170,6 @@ export const UnifiedListConfigPanel: React.FC<UnifiedListConfigPanelProps> = ({
|
||||||
updateConfig("columns", newColumns);
|
updateConfig("columns", newColumns);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 컬럼 너비 수정
|
|
||||||
const updateColumnWidth = (columnKey: string, width: string) => {
|
|
||||||
const newColumns = configColumns.map((col) => (col.key === columnKey ? { ...col, width } : col));
|
|
||||||
updateConfig("columns", newColumns);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 그룹별 컬럼 분리
|
// 그룹별 컬럼 분리
|
||||||
const baseColumns = useMemo(() => columns.filter((col) => !col.isJoinColumn), [columns]);
|
const baseColumns = useMemo(() => columns.filter((col) => !col.isJoinColumn), [columns]);
|
||||||
|
|
||||||
|
|
@ -209,21 +203,6 @@ export const UnifiedListConfigPanel: React.FC<UnifiedListConfigPanelProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* 데이터 소스 정보 (읽기 전용) */}
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label className="text-xs font-medium">데이터 소스</Label>
|
|
||||||
{tableName ? (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Database className="text-muted-foreground h-4 w-4" />
|
|
||||||
<span className="text-sm font-medium">{tableName}</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className="text-xs text-amber-600">화면에 테이블이 설정되지 않았습니다</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
{/* 뷰 모드 */}
|
{/* 뷰 모드 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-xs font-medium">표시 방식</Label>
|
<Label className="text-xs font-medium">표시 방식</Label>
|
||||||
|
|
@ -422,12 +401,6 @@ export const UnifiedListConfigPanel: React.FC<UnifiedListConfigPanelProps> = ({
|
||||||
placeholder="제목"
|
placeholder="제목"
|
||||||
className="h-6 flex-1 text-xs"
|
className="h-6 flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
<Input
|
|
||||||
value={column.width || ""}
|
|
||||||
onChange={(e) => updateColumnWidth(column.key, e.target.value)}
|
|
||||||
placeholder="너비"
|
|
||||||
className="h-6 w-14 text-xs"
|
|
||||||
/>
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue