라벨 표시기능

This commit is contained in:
kjs
2025-09-02 16:46:54 +09:00
parent 9af3cdea01
commit 162ab12806
4 changed files with 316 additions and 175 deletions

View File

@@ -42,23 +42,6 @@ const webTypeOptions: { value: WebType; label: string }[] = [
{ 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,
@@ -73,122 +56,49 @@ export const PropertiesPanel: React.FC<PropertiesPanelProps> = ({
const selectedComponentRef = useRef(selectedComponent);
const onUpdatePropertyRef = useRef(onUpdateProperty);
// 입력 필드들의 로컬 상태 (실시간 타이핑 반영용)
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,
});
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 || "",
setLocalInputs({
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,
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,
});
}
}, [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">
@@ -251,19 +161,6 @@ export const PropertiesPanel: React.FC<PropertiesPanelProps> = ({
</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>
@@ -307,8 +204,12 @@ export const PropertiesPanel: React.FC<PropertiesPanelProps> = ({
</Label>
<Input
id="placeholder"
value={localValues.placeholder}
onChange={(e) => setLocalValues((prev) => ({ ...prev, placeholder: e.target.value }))}
value={localInputs.placeholder}
onChange={(e) => {
const newValue = e.target.value;
setLocalInputs((prev) => ({ ...prev, placeholder: newValue }));
onUpdateProperty("placeholder", newValue);
}}
placeholder="입력 힌트 텍스트"
className="mt-1"
/>
@@ -318,8 +219,11 @@ export const PropertiesPanel: React.FC<PropertiesPanelProps> = ({
<div className="flex items-center space-x-2">
<Checkbox
id="required"
checked={selectedComponent.required || false}
onCheckedChange={(checked) => onUpdateProperty("required", checked)}
checked={localInputs.required}
onCheckedChange={(checked) => {
setLocalInputs((prev) => ({ ...prev, required: !!checked }));
onUpdateProperty("required", checked);
}}
/>
<Label htmlFor="required" className="text-sm">
@@ -329,8 +233,11 @@ export const PropertiesPanel: React.FC<PropertiesPanelProps> = ({
<div className="flex items-center space-x-2">
<Checkbox
id="readonly"
checked={selectedComponent.readonly || false}
onCheckedChange={(checked) => onUpdateProperty("readonly", checked)}
checked={localInputs.readonly}
onCheckedChange={(checked) => {
setLocalInputs((prev) => ({ ...prev, readonly: !!checked }));
onUpdateProperty("readonly", checked);
}}
/>
<Label htmlFor="readonly" className="text-sm">
@@ -359,8 +266,12 @@ export const PropertiesPanel: React.FC<PropertiesPanelProps> = ({
<Input
id="positionX"
type="number"
value={localValues.positionX}
onChange={(e) => setLocalValues((prev) => ({ ...prev, positionX: Number(e.target.value) }))}
value={localInputs.positionX}
onChange={(e) => {
const newValue = e.target.value;
setLocalInputs((prev) => ({ ...prev, positionX: newValue }));
onUpdateProperty("position", { ...selectedComponent.position, x: Number(newValue) });
}}
className="mt-1"
/>
</div>
@@ -372,8 +283,12 @@ export const PropertiesPanel: React.FC<PropertiesPanelProps> = ({
<Input
id="positionY"
type="number"
value={localValues.positionY}
onChange={(e) => setLocalValues((prev) => ({ ...prev, positionY: Number(e.target.value) }))}
value={localInputs.positionY}
onChange={(e) => {
const newValue = e.target.value;
setLocalInputs((prev) => ({ ...prev, positionY: newValue }));
onUpdateProperty("position", { ...selectedComponent.position, y: Number(newValue) });
}}
className="mt-1"
/>
</div>
@@ -385,8 +300,12 @@ export const PropertiesPanel: React.FC<PropertiesPanelProps> = ({
<Input
id="width"
type="number"
value={localValues.width}
onChange={(e) => setLocalValues((prev) => ({ ...prev, width: Number(e.target.value) }))}
value={localInputs.width}
onChange={(e) => {
const newValue = e.target.value;
setLocalInputs((prev) => ({ ...prev, width: newValue }));
onUpdateProperty("size", { ...selectedComponent.size, width: Number(newValue) });
}}
className="mt-1"
/>
</div>
@@ -398,8 +317,12 @@ export const PropertiesPanel: React.FC<PropertiesPanelProps> = ({
<Input
id="height"
type="number"
value={localValues.height}
onChange={(e) => setLocalValues((prev) => ({ ...prev, height: Number(e.target.value) }))}
value={localInputs.height}
onChange={(e) => {
const newValue = e.target.value;
setLocalInputs((prev) => ({ ...prev, height: newValue }));
onUpdateProperty("size", { ...selectedComponent.size, height: Number(newValue) });
}}
className="mt-1"
/>
</div>
@@ -413,8 +336,12 @@ export const PropertiesPanel: React.FC<PropertiesPanelProps> = ({
type="number"
min="0"
max="9999"
value={localValues.positionZ}
onChange={(e) => setLocalValues((prev) => ({ ...prev, positionZ: Number(e.target.value) }))}
value={localInputs.positionZ}
onChange={(e) => {
const newValue = e.target.value;
setLocalInputs((prev) => ({ ...prev, positionZ: newValue }));
onUpdateProperty("position", { ...selectedComponent.position, z: Number(newValue) });
}}
className="mt-1"
placeholder="1"
/>
@@ -422,6 +349,149 @@ export const PropertiesPanel: React.FC<PropertiesPanelProps> = ({
</div>
</div>
<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>
{selectedComponent.type === "group" && (
<>
<Separator />
@@ -439,8 +509,12 @@ export const PropertiesPanel: React.FC<PropertiesPanelProps> = ({
</Label>
<Input
id="groupTitle"
value={localValues.title}
onChange={(e) => setLocalValues((prev) => ({ ...prev, title: e.target.value }))}
value={localInputs.title}
onChange={(e) => {
const newValue = e.target.value;
setLocalInputs((prev) => ({ ...prev, title: newValue }));
onUpdateProperty("title", newValue);
}}
placeholder="그룹 제목"
className="mt-1"
/>