From 162ab1280641ff65bd4652a1d126a4bfbb16b36a Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 2 Sep 2025 16:46:54 +0900 Subject: [PATCH] =?UTF-8?q?=EB=9D=BC=EB=B2=A8=20=ED=91=9C=EC=8B=9C?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/screen/RealtimePreview.tsx | 88 ++-- frontend/components/screen/ScreenDesigner.tsx | 14 + .../screen/panels/PropertiesPanel.tsx | 376 +++++++++++------- frontend/types/screen.ts | 13 + 4 files changed, 316 insertions(+), 175 deletions(-) diff --git a/frontend/components/screen/RealtimePreview.tsx b/frontend/components/screen/RealtimePreview.tsx index e23de4f5..ea3bec21 100644 --- a/frontend/components/screen/RealtimePreview.tsx +++ b/frontend/components/screen/RealtimePreview.tsx @@ -244,15 +244,33 @@ export const RealtimePreview: React.FC = ({ } : style; + // 라벨 스타일 계산 + const shouldShowLabel = component.style?.labelDisplay !== false && (component.label || component.style?.labelText); + const labelText = component.style?.labelText || component.label || ""; + + // 라벨 하단 여백 값 추출 (px 단위 숫자로 변환) + const labelMarginBottomValue = parseInt(component.style?.labelMarginBottom || "4px", 10); + + const labelStyle: React.CSSProperties = { + fontSize: component.style?.labelFontSize || "12px", + color: component.style?.labelColor || "#374151", + fontWeight: component.style?.labelFontWeight || "500", + fontFamily: component.style?.labelFontFamily || "inherit", + textAlign: component.style?.labelTextAlign || "left", + backgroundColor: component.style?.labelBackgroundColor || "transparent", + padding: component.style?.labelPadding || "0", + borderRadius: component.style?.labelBorderRadius || "0", + }; + return (
{ @@ -267,31 +285,53 @@ export const RealtimePreview: React.FC = ({ e.stopPropagation(); }} > - {type === "container" && ( -
-
- -
-
{label}
-
{tableName}
+ {/* 라벨 표시 */} + {shouldShowLabel && ( +
+ {labelText} + {component.required && *} +
+ )} + + {/* 컴포넌트 내용 */} +
+ {type === "container" && ( +
+
+ +
+
{label}
+
{tableName}
+
-
- )} + )} - {type === "group" && ( -
- {/* 그룹 박스/헤더 제거: 투명 컨테이너 */} -
{children}
-
- )} + {type === "group" && ( +
+ {/* 그룹 박스/헤더 제거: 투명 컨테이너 */} +
{children}
+
+ )} - {type === "widget" && ( -
- {/* 위젯 본체 - 실제 웹 위젯처럼 보이도록 */} -
{renderWidget(component)}
-
- )} + {type === "widget" && ( +
+ {/* 위젯 본체 - 실제 웹 위젯처럼 보이도록 */} +
{renderWidget(component)}
+
+ )} +
); }; diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index d8ea08bd..6b534882 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -431,6 +431,13 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD tableName: table.tableName, position: { x, y, z: 1 } as Position, size: { width: 300, height: 200 }, + style: { + labelDisplay: true, + labelFontSize: "14px", + labelColor: "#374151", + labelFontWeight: "600", + labelMarginBottom: "8px", + }, }; } else if (type === "column") { // 격자 기반 컬럼 너비 계산 @@ -449,6 +456,13 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD readonly: false, // 누락된 속성 추가 position: { x, y, z: 1 } as Position, size: { width: columnWidth, height: 40 }, + style: { + labelDisplay: true, + labelFontSize: "12px", + labelColor: "#374151", + labelFontWeight: "500", + labelMarginBottom: "6px", + }, }; } else { return; diff --git a/frontend/components/screen/panels/PropertiesPanel.tsx b/frontend/components/screen/panels/PropertiesPanel.tsx index 0507fd73..98002bee 100644 --- a/frontend/components/screen/panels/PropertiesPanel.tsx +++ b/frontend/components/screen/panels/PropertiesPanel.tsx @@ -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 = ({ selectedComponent, onUpdateProperty, @@ -73,122 +56,49 @@ export const PropertiesPanel: React.FC = ({ 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 (
@@ -251,19 +161,6 @@ export const PropertiesPanel: React.FC = ({
-
- - setLocalValues((prev) => ({ ...prev, label: e.target.value }))} - placeholder="컴포넌트 라벨" - className="mt-1" - /> -
- {selectedComponent.type === "widget" && ( <>
@@ -307,8 +204,12 @@ export const PropertiesPanel: React.FC = ({ 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 = ({
onUpdateProperty("required", checked)} + checked={localInputs.required} + onCheckedChange={(checked) => { + setLocalInputs((prev) => ({ ...prev, required: !!checked })); + onUpdateProperty("required", checked); + }} />
@@ -385,8 +300,12 @@ export const PropertiesPanel: React.FC = ({ 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" />
@@ -398,8 +317,12 @@ export const PropertiesPanel: React.FC = ({ 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" />
@@ -413,8 +336,12 @@ export const PropertiesPanel: React.FC = ({ 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 = ({
+ + + {/* 라벨 스타일 */} +
+
+ +

라벨 설정

+
+ + {/* 라벨 표시 토글 */} +
+ + onUpdateProperty("style.labelDisplay", checked)} + /> +
+ + {/* 라벨 텍스트 */} +
+ + { + const newValue = e.target.value; + setLocalInputs((prev) => ({ ...prev, labelText: newValue })); + // 기본 라벨과 스타일 라벨을 모두 업데이트 + onUpdateProperty("label", newValue); + onUpdateProperty("style.labelText", newValue); + }} + placeholder="라벨 텍스트" + className="mt-1" + /> +
+ + {/* 라벨 스타일 */} +
+
+ + { + const newValue = e.target.value; + setLocalInputs((prev) => ({ ...prev, labelFontSize: newValue })); + onUpdateProperty("style.labelFontSize", newValue); + }} + placeholder="12px" + className="mt-1" + /> +
+ +
+ + { + const newValue = e.target.value; + setLocalInputs((prev) => ({ ...prev, labelColor: newValue })); + onUpdateProperty("style.labelColor", newValue); + }} + className="mt-1 h-8" + /> +
+ +
+ + +
+ +
+ + +
+
+ + {/* 라벨 여백 */} +
+ + { + const newValue = e.target.value; + setLocalInputs((prev) => ({ ...prev, labelMarginBottom: newValue })); + onUpdateProperty("style.labelMarginBottom", newValue); + }} + placeholder="4px" + className="mt-1" + /> +
+
+ {selectedComponent.type === "group" && ( <> @@ -439,8 +509,12 @@ export const PropertiesPanel: React.FC = ({ 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" /> diff --git a/frontend/types/screen.ts b/frontend/types/screen.ts index cc26e785..18a05cb6 100644 --- a/frontend/types/screen.ts +++ b/frontend/types/screen.ts @@ -114,6 +114,19 @@ export interface ComponentStyle { cursor?: string; transition?: string; transform?: string; + + // 라벨 스타일 + labelDisplay?: boolean; // 라벨 표시 여부 + labelText?: string; // 라벨 텍스트 (기본값은 label 속성 사용) + labelFontSize?: string | number; // 라벨 폰트 크기 + labelColor?: string; // 라벨 색상 + labelFontWeight?: "normal" | "bold" | "100" | "200" | "300" | "400" | "500" | "600" | "700" | "800" | "900"; // 라벨 폰트 굵기 + labelFontFamily?: string; // 라벨 폰트 패밀리 + labelTextAlign?: "left" | "center" | "right"; // 라벨 텍스트 정렬 + labelMarginBottom?: string | number; // 라벨과 컴포넌트 사이의 간격 + labelBackgroundColor?: string; // 라벨 배경색 + labelPadding?: string; // 라벨 패딩 + labelBorderRadius?: string | number; // 라벨 모서리 둥글기 } // BaseComponent에 스타일 속성 추가