From 4781a17b71a614175dc053591316fd44610c6d3e Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 21 Jan 2026 09:33:44 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A6=AC=EC=82=AC=EC=9D=B4=EC=A6=88=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EA=B4=80=EB=A6=AC=20=EA=B0=9C=EC=84=A0:=20Realtime?= =?UTF-8?q?PreviewDynamic=20=EB=B0=8F=20TabsWidget=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=A6=AC=EC=82=AC?= =?UTF-8?q?=EC=9D=B4=EC=A6=88=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=98=EA=B3=A0,=20=EB=A6=AC=EC=82=AC=EC=9D=B4?= =?UTF-8?q?=EC=A6=88=20=EC=83=81=ED=83=9C=EB=A5=BC=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EB=A1=9C=EC=A7=81=EC=9D=84=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=ED=95=98=EC=97=AC=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EA=B2=BD=ED=97=98=EC=9D=84=20=ED=96=A5=EC=83=81=EC=8B=9C?= =?UTF-8?q?=EC=BC=B0=EC=8A=B5=EB=8B=88=EB=8B=A4.=20=EC=9D=B4=EB=A5=BC=20?= =?UTF-8?q?=ED=86=B5=ED=95=B4=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=ED=81=AC=EA=B8=B0=20=EC=A1=B0=EC=A0=95=20=EC=8B=9C=20=EB=8D=94?= =?UTF-8?q?=20=EB=82=98=EC=9D=80=20=EB=B0=98=EC=9D=91=EC=84=B1=EA=B3=BC=20?= =?UTF-8?q?=EC=A0=95=ED=99=95=EC=84=B1=EC=9D=84=20=EC=A0=9C=EA=B3=B5?= =?UTF-8?q?=ED=95=98=EA=B2=8C=20=EB=90=98=EC=97=88=EC=8A=B5=EB=8B=88?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/RealtimePreviewDynamic.tsx | 150 +++++++++- frontend/components/screen/ScreenDesigner.tsx | 271 +++++++++++------- .../v2-tabs-widget/tabs-component.tsx | 178 +++++++++++- 3 files changed, 482 insertions(+), 117 deletions(-) diff --git a/frontend/components/screen/RealtimePreviewDynamic.tsx b/frontend/components/screen/RealtimePreviewDynamic.tsx index 61c7e77c..75eec128 100644 --- a/frontend/components/screen/RealtimePreviewDynamic.tsx +++ b/frontend/components/screen/RealtimePreviewDynamic.tsx @@ -39,6 +39,7 @@ interface RealtimePreviewProps { onUpdateComponent?: (updatedComponent: any) => void; // ๐Ÿ†• ์ปดํฌ๋„ŒํŠธ ์—…๋ฐ์ดํŠธ ์ฝœ๋ฐฑ onSelectTabComponent?: (tabId: string, compId: string, comp: any) => void; // ๐Ÿ†• ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ์ฝœ๋ฐฑ selectedTabComponentId?: string; // ๐Ÿ†• ์„ ํƒ๋œ ํƒญ ์ปดํฌ๋„ŒํŠธ ID + onResize?: (componentId: string, newSize: { width: number; height: number }) => void; // ๐Ÿ†• ๋ฆฌ์‚ฌ์ด์ฆˆ ์ฝœ๋ฐฑ // ๋ฒ„ํŠผ ์•ก์…˜์„ ์œ„ํ•œ props screenId?: number; @@ -139,6 +140,7 @@ const RealtimePreviewDynamicComponent: React.FC = ({ onUpdateComponent, // ๐Ÿ†• ์ปดํฌ๋„ŒํŠธ ์—…๋ฐ์ดํŠธ ์ฝœ๋ฐฑ onSelectTabComponent, // ๐Ÿ†• ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ์ฝœ๋ฐฑ selectedTabComponentId, // ๐Ÿ†• ์„ ํƒ๋œ ํƒญ ์ปดํฌ๋„ŒํŠธ ID + onResize, // ๐Ÿ†• ๋ฆฌ์‚ฌ์ด์ฆˆ ์ฝœ๋ฐฑ }) => { // ๐Ÿ†• ํ™”๋ฉด ๋‹ค๊ตญ์–ด ์ปจํ…์ŠคํŠธ const { getTranslatedText } = useScreenMultiLang(); @@ -146,6 +148,102 @@ const RealtimePreviewDynamicComponent: React.FC = ({ const [actualHeight, setActualHeight] = React.useState(null); const contentRef = React.useRef(null); const lastUpdatedHeight = React.useRef(null); + + // ๐Ÿ†• ๋ฆฌ์‚ฌ์ด์ฆˆ ์ƒํƒœ + const [isResizing, setIsResizing] = React.useState(false); + const [resizeSize, setResizeSize] = React.useState<{ width: number; height: number } | null>(null); + const rafRef = React.useRef(null); + + // ๐Ÿ†• size๊ฐ€ ์—…๋ฐ์ดํŠธ๋˜๋ฉด resizeSize ์ดˆ๊ธฐํ™” (๋ ˆ์ด์•„์›ƒ ์ƒํƒœ๊ฐ€ props์— ๋ฐ˜์˜๋˜์—ˆ์Œ) + React.useEffect(() => { + if (resizeSize && !isResizing) { + // component.size๊ฐ€ resizeSize์™€ ๊ฐ™์•„์ง€๋ฉด resizeSize ์ดˆ๊ธฐํ™” + if (component.size?.width === resizeSize.width && component.size?.height === resizeSize.height) { + setResizeSize(null); + } + } + }, [component.size?.width, component.size?.height, resizeSize, isResizing]); + + // 10px ๋‹จ์œ„ ์Šค๋ƒ… ํ•จ์ˆ˜ + const snapTo10 = (value: number) => Math.round(value / 10) * 10; + + // ๐Ÿ†• ๋ฆฌ์‚ฌ์ด์ฆˆ ํ•ธ๋“ค๋Ÿฌ + const handleResizeStart = React.useCallback( + (e: React.MouseEvent, direction: "e" | "s" | "se") => { + e.stopPropagation(); + e.preventDefault(); + + const startMouseX = e.clientX; + const startMouseY = e.clientY; + const startWidth = component.size?.width || 200; + const startHeight = component.size?.height || 100; + + setIsResizing(true); + setResizeSize({ width: startWidth, height: startHeight }); + + const handleMouseMove = (moveEvent: MouseEvent) => { + if (rafRef.current) { + cancelAnimationFrame(rafRef.current); + } + + rafRef.current = requestAnimationFrame(() => { + const deltaX = moveEvent.clientX - startMouseX; + const deltaY = moveEvent.clientY - startMouseY; + + let newWidth = startWidth; + let newHeight = startHeight; + + if (direction === "e" || direction === "se") { + newWidth = snapTo10(Math.max(50, startWidth + deltaX)); + } + if (direction === "s" || direction === "se") { + newHeight = snapTo10(Math.max(20, startHeight + deltaY)); + } + + setResizeSize({ width: newWidth, height: newHeight }); + }); + }; + + const handleMouseUp = (upEvent: MouseEvent) => { + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + + if (rafRef.current) { + cancelAnimationFrame(rafRef.current); + rafRef.current = null; + } + + const deltaX = upEvent.clientX - startMouseX; + const deltaY = upEvent.clientY - startMouseY; + + let newWidth = startWidth; + let newHeight = startHeight; + + if (direction === "e" || direction === "se") { + newWidth = snapTo10(Math.max(50, startWidth + deltaX)); + } + if (direction === "s" || direction === "se") { + newHeight = snapTo10(Math.max(20, startHeight + deltaY)); + } + + // ๐Ÿ†• ๋ฆฌ์‚ฌ์ด์ฆˆ ์ƒํƒœ๋Š” ์œ ์ง€ํ•œ ์ฑ„๋กœ ํฌ๊ธฐ ๋ณ€๊ฒฝ ์ฝœ๋ฐฑ ํ˜ธ์ถœ + // resizeSize๋Š” null๋กœ ์„ค์ •ํ•˜์ง€ ์•Š๊ณ  ๋งˆ์ง€๋ง‰ ํฌ๊ธฐ ์œ ์ง€ + // (component.size๊ฐ€ ์—…๋ฐ์ดํŠธ๋˜๋ฉด ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์˜ฌ๋ฐ”๋ฅธ ํฌ๊ธฐ ํ‘œ์‹œ) + + // ๐Ÿ†• ํฌ๊ธฐ ๋ณ€๊ฒฝ ์ฝœ๋ฐฑ ํ˜ธ์ถœํ•˜์—ฌ ๋ ˆ์ด์•„์›ƒ ์ƒํƒœ ์—…๋ฐ์ดํŠธ + if (onResize) { + onResize(component.id, { width: newWidth, height: newHeight }); + } + + // ๐Ÿ†• ๋ฆฌ์‚ฌ์ด์ฆˆ ํ”Œ๋ž˜๊ทธ๋งŒ ํ•ด์ œ (resizeSize๋Š” ๋งˆ์ง€๋ง‰ ํฌ๊ธฐ ์œ ์ง€) + setIsResizing(false); + }; + + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + }, + [component.id, component.size, onResize] + ); // ํ”Œ๋กœ์šฐ ์œ„์ ฏ์˜ ์‹ค์ œ ๋†’์ด ์ธก์ • React.useEffect(() => { @@ -249,18 +347,27 @@ const RealtimePreviewDynamicComponent: React.FC = ({ return `${actualHeight}px`; } - // 1์ˆœ์œ„: style.height๊ฐ€ ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ (๋ฌธ์ž์—ด ๊ทธ๋Œ€๋กœ ๋˜๋Š” ์ˆซ์ž+px) + // ๐Ÿ†• 1์ˆœ์œ„: size.height๊ฐ€ ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ (๋ ˆ์ด์•„์›ƒ์—์„œ ๊ด€๋ฆฌ๋˜๋Š” ์‹ค์ œ ํฌ๊ธฐ) + // size๋Š” ๋ ˆ์ด์•„์›ƒ ์ƒํƒœ์—์„œ ์ง์ ‘ ๊ด€๋ฆฌ๋˜๋ฉฐ ๋ฆฌ์‚ฌ์ด์ฆˆ๋กœ ๋ณ€๊ฒฝ๋จ + if (size?.height && size.height > 0) { + if (component.componentConfig?.type === "table-list") { + return `${Math.max(size.height, 200)}px`; + } + return `${size.height}px`; + } + + // 2์ˆœ์œ„: componentStyle.height (์ปดํฌ๋„ŒํŠธ ์ •์˜์—์„œ ์˜จ ๊ธฐ๋ณธ ์Šคํƒ€์ผ) if (componentStyle?.height) { return typeof componentStyle.height === "number" ? `${componentStyle.height}px` : componentStyle.height; } - // 2์ˆœ์œ„: size.height (ํ”ฝ์…€) + // 3์ˆœ์œ„: ๊ธฐ๋ณธ๊ฐ’ if (component.componentConfig?.type === "table-list") { - return `${Math.max(size?.height || 200, 200)}px`; + return "200px"; } - // size.height๊ฐ€ ์žˆ์œผ๋ฉด ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ, ์—†์œผ๋ฉด ์ตœ์†Œ 10px - return `${size?.height || 10}px`; + // ๊ธฐ๋ณธ ๋†’์ด + return "10px"; }; // layout ํƒ€์ž… ์ปดํฌ๋„ŒํŠธ์ธ์ง€ ํ™•์ธ @@ -405,16 +512,22 @@ const RealtimePreviewDynamicComponent: React.FC = ({ const { adjustedPositionX, isOnSplitPanel, isDraggingSplitPanel } = calculateButtonPosition(); + // ๐Ÿ†• ๋ฆฌ์‚ฌ์ด์ฆˆ ํฌ๊ธฐ๊ฐ€ ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ + // (size๊ฐ€ ์—…๋ฐ์ดํŠธ๋˜๋ฉด ์œ„ useEffect์—์„œ resizeSize๋ฅผ null๋กœ ์„ค์ •) + const displayWidth = resizeSize ? `${resizeSize.width}px` : getWidth(); + const displayHeight = resizeSize ? `${resizeSize.height}px` : getHeight(); + const baseStyle = { left: `${adjustedPositionX}px`, // ๐Ÿ†• ์กฐ์ •๋œ X ์ขŒํ‘œ ์‚ฌ์šฉ top: `${position.y}px`, ...componentStyle, // componentStyle ์ „์ฒด ์ ์šฉ (DynamicComponentRenderer์—์„œ ์ด๋ฏธ size๊ฐ€ ๋ณ€ํ™˜๋จ) - width: getWidth(), // getWidth() ์šฐ์„  (table-list ๋“ฑ ํŠน์ˆ˜ ์ผ€์ด์Šค) - height: getHeight(), // getHeight() ์šฐ์„  (flow-widget ๋“ฑ ํŠน์ˆ˜ ์ผ€์ด์Šค) + width: displayWidth, // ๐Ÿ†• ๋ฆฌ์‚ฌ์ด์ฆˆ ์ค‘์ด๋ฉด resizeSize ์‚ฌ์šฉ + height: displayHeight, // ๐Ÿ†• ๋ฆฌ์‚ฌ์ด์ฆˆ ์ค‘์ด๋ฉด resizeSize ์‚ฌ์šฉ zIndex: component.type === "layout" ? 1 : position.z || 2, right: undefined, - // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„ ๋“œ๋ž˜๊ทธ ์ค‘์—๋Š” ํŠธ๋žœ์ง€์…˜ ์—†์ด ์ฆ‰์‹œ ์ด๋™ + // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„ ๋“œ๋ž˜๊ทธ ์ค‘์—๋Š” ํŠธ๋žœ์ง€์…˜ ์—†์ด ์ฆ‰์‹œ ์ด๋™, ๋ฆฌ์‚ฌ์ด์ฆˆ ์ค‘์—๋„ ํŠธ๋žœ์ง€์…˜ ์—†์Œ transition: + isResizing ? "none" : isOnSplitPanel && isButtonComponent ? (isDraggingSplitPanel ? "none" : "left 0.1s ease-out") : undefined, }; @@ -546,6 +659,27 @@ const RealtimePreviewDynamicComponent: React.FC = ({ )} )} + + {/* ๐Ÿ†• ๋ฆฌ์‚ฌ์ด์ฆˆ ๊ฐ€์žฅ์ž๋ฆฌ ์˜์—ญ - ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ + ๋””์ž์ธ ๋ชจ๋“œ์—์„œ๋งŒ ํ‘œ์‹œ */} + {isSelected && isDesignMode && onResize && ( + <> + {/* ์˜ค๋ฅธ์ชฝ ๊ฐ€์žฅ์ž๋ฆฌ (๋„ˆ๋น„ ์กฐ์ ˆ) */} +
handleResizeStart(e, "e")} + /> + {/* ์•„๋ž˜ ๊ฐ€์žฅ์ž๋ฆฌ (๋†’์ด ์กฐ์ ˆ) */} +
handleResizeStart(e, "s")} + /> + {/* ์˜ค๋ฅธ์ชฝ ์•„๋ž˜ ๋ชจ์„œ๋ฆฌ (๋„ˆ๋น„+๋†’์ด ์กฐ์ ˆ) */} +
handleResizeStart(e, "se")} + /> + + )}
); }; diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index e8561387..410ed46e 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -4905,115 +4905,155 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD - {/* ๐Ÿ†• ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์„ ํƒ๋œ ๊ฒฝ์šฐ ๋ณ„๋„ ํŒจ๋„ ํ‘œ์‹œ */} + {/* ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ์‹œ์—๋„ UnifiedPropertiesPanel ์‚ฌ์šฉ */} {selectedTabComponentInfo ? ( -
-
-
-

ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ค์ •

-

- {selectedTabComponentInfo.component.label || selectedTabComponentInfo.component.componentType} -

-
- -
- {/* DynamicComponentConfigPanel ๋ Œ๋”๋ง */} -
- {(() => { - const DynamicConfigPanel = require("@/lib/utils/getComponentConfigPanel").DynamicComponentConfigPanel; - const tabComp = selectedTabComponentInfo.component; - - // ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ผ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ - const componentForConfig = { - id: tabComp.id, - type: "component", - componentType: tabComp.componentType, - label: tabComp.label, - position: tabComp.position, - size: tabComp.size, - componentConfig: tabComp.componentConfig || {}, - style: tabComp.style || {}, - inputType: tabComp.inputType || tabComp.componentConfig?.inputType, // ๐Ÿ†• inputType ์ถ”๊ฐ€ - widgetType: tabComp.widgetType || tabComp.componentConfig?.widgetType, // ๐Ÿ†• widgetType ์ถ”๊ฐ€ + (() => { + const tabComp = selectedTabComponentInfo.component; + + // ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ComponentData ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ + const tabComponentAsComponentData: ComponentData = { + id: tabComp.id, + type: "component", + componentType: tabComp.componentType, + label: tabComp.label, + position: tabComp.position || { x: 0, y: 0 }, + size: tabComp.size || { width: 200, height: 100 }, + componentConfig: tabComp.componentConfig || {}, + style: tabComp.style || {}, + } as ComponentData; + + // ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ์šฉ ์†์„ฑ ์—…๋ฐ์ดํŠธ ํ•ธ๋“ค๋Ÿฌ + const updateTabComponentProperty = (componentId: string, path: string, value: any) => { + const { tabsComponentId, tabId } = selectedTabComponentInfo; + + setLayout((prevLayout) => { + const tabsComponent = prevLayout.components.find((c) => c.id === tabsComponentId); + if (!tabsComponent) return prevLayout; + + const currentConfig = (tabsComponent as any).componentConfig || {}; + const tabs = currentConfig.tabs || []; + + const updatedTabs = tabs.map((tab: any) => { + if (tab.id === tabId) { + return { + ...tab, + components: (tab.components || []).map((comp: any) => { + if (comp.id !== componentId) return comp; + + // path๋ฅผ ํŒŒ์‹ฑํ•˜์—ฌ ์ค‘์ฒฉ ์†์„ฑ ์—…๋ฐ์ดํŠธ + const pathParts = path.split("."); + const newComp = { ...comp }; + let current: any = newComp; + + for (let i = 0; i < pathParts.length - 1; i++) { + const part = pathParts[i]; + if (!current[part]) { + current[part] = {}; + } else { + current[part] = { ...current[part] }; + } + current = current[part]; + } + current[pathParts[pathParts.length - 1]] = value; + + return newComp; + }), + }; + } + return tab; + }); + + const updatedComponent = { + ...tabsComponent, + componentConfig: { ...currentConfig, tabs: updatedTabs }, }; - return ( - 0 ? tables[0].columns : []} - menuObjid={selectedScreen?.menuObjid} - currentComponent={componentForConfig} - onChange={(newConfig: any) => { - // componentConfig ์ „์ฒด ์—…๋ฐ์ดํŠธ - ํ•จ์ˆ˜ํ˜• ์—…๋ฐ์ดํŠธ๋กœ ํด๋กœ์ € ๋ฌธ์ œ ํ•ด๊ฒฐ - const { tabsComponentId, tabId, componentId } = selectedTabComponentInfo; - - setLayout((prevLayout) => { - const tabsComponent = prevLayout.components.find((c) => c.id === tabsComponentId); - if (!tabsComponent) return prevLayout; + // ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ ์ •๋ณด ์—…๋ฐ์ดํŠธ + const updatedComp = updatedTabs + .find((t: any) => t.id === tabId) + ?.components?.find((c: any) => c.id === componentId); + if (updatedComp) { + setSelectedTabComponentInfo((prev) => + prev ? { ...prev, component: updatedComp } : null + ); + } - const currentConfig = (tabsComponent as any).componentConfig || {}; - const tabs = currentConfig.tabs || []; + return { + ...prevLayout, + components: prevLayout.components.map((c) => + c.id === tabsComponentId ? updatedComponent : c + ), + }; + }); + }; - const updatedTabs = tabs.map((tab: any) => { - if (tab.id === tabId) { - return { - ...tab, - components: (tab.components || []).map((comp: any) => - comp.id === componentId - ? { ...comp, componentConfig: newConfig } - : comp - ), - }; - } - return tab; - }); + // ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์‚ญ์ œ ํ•ธ๋“ค๋Ÿฌ + const deleteTabComponent = (componentId: string) => { + const { tabsComponentId, tabId } = selectedTabComponentInfo; + + setLayout((prevLayout) => { + const tabsComponent = prevLayout.components.find((c) => c.id === tabsComponentId); + if (!tabsComponent) return prevLayout; - const updatedComponent = { - ...tabsComponent, - componentConfig: { - ...currentConfig, - tabs: updatedTabs, - }, - }; + const currentConfig = (tabsComponent as any).componentConfig || {}; + const tabs = currentConfig.tabs || []; - const newLayout = { - ...prevLayout, - components: prevLayout.components.map((c) => - c.id === tabsComponentId ? updatedComponent : c - ), - }; + const updatedTabs = tabs.map((tab: any) => { + if (tab.id === tabId) { + return { + ...tab, + components: (tab.components || []).filter((c: any) => c.id !== componentId), + }; + } + return tab; + }); - // ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ ์ •๋ณด ์—…๋ฐ์ดํŠธ - const updatedComp = updatedTabs - .find((t: any) => t.id === tabId) - ?.components?.find((c: any) => c.id === componentId); - if (updatedComp) { - setSelectedTabComponentInfo((prev) => - prev ? { ...prev, component: updatedComp } : null - ); - } + const updatedComponent = { + ...tabsComponent, + componentConfig: { ...currentConfig, tabs: updatedTabs }, + }; - return newLayout; - }); - }} - screenTableName={selectedScreen?.tableName} - tableColumns={tables.length > 0 ? tables[0].columns : []} + setSelectedTabComponentInfo(null); + + return { + ...prevLayout, + components: prevLayout.components.map((c) => + c.id === tabsComponentId ? updatedComponent : c + ), + }; + }); + }; + + return ( +
+
+ ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ + +
+
+ 0 ? tables[0] : undefined} + currentTableName={selectedScreen?.tableName} + currentScreenCompanyCode={selectedScreen?.companyCode} + onStyleChange={(style) => { + updateTabComponentProperty(tabComp.id, "style", style); + }} allComponents={layout.components} - menuObjid={selectedScreen?.menuObjid} + menuObjid={menuObjid} /> - ); - })()} -
-
+
+
+ ); + })() ) : ( { + setLayout((prevLayout) => { + const updatedComponents = prevLayout.components.map((comp) => + comp.id === componentId + ? { ...comp, size: newSize } + : comp + ); + + const newLayout = { + ...prevLayout, + components: updatedComponents, + }; + + // saveToHistory๋Š” ๋ณ„๋„๋กœ ํ˜ธ์ถœ (prevLayout ๊ธฐ๋ฐ˜) + setTimeout(() => saveToHistory(newLayout), 0); + return newLayout; + }); + }} // ๐Ÿ†• ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ํ•ธ๋“ค๋Ÿฌ onSelectTabComponent={(tabId, compId, comp) => handleSelectTabComponent(component.id, tabId, compId, comp) @@ -5476,6 +5535,24 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD // console.log("๐Ÿ“ค ์ž์‹ ์ปดํฌ๋„ŒํŠธ ์„ค์ • ๋ณ€๊ฒฝ์„ ์ƒ์„ธ์„ค์ •์— ์•Œ๋ฆผ:", config); // TODO: ์‹ค์ œ ๊ตฌํ˜„์€ DetailSettingsPanel๊ณผ์˜ ์—ฐ๋™ ํ•„์š” }} + // ๐Ÿ†• ์ž์‹ ์ปดํฌ๋„ŒํŠธ ๋ฆฌ์‚ฌ์ด์ฆˆ ํ•ธ๋“ค๋Ÿฌ + onResize={(componentId, newSize) => { + setLayout((prevLayout) => { + const updatedComponents = prevLayout.components.map((comp) => + comp.id === componentId + ? { ...comp, size: newSize } + : comp + ); + + const newLayout = { + ...prevLayout, + components: updatedComponents, + }; + + setTimeout(() => saveToHistory(newLayout), 0); + return newLayout; + }); + }} /> ); })} diff --git a/frontend/lib/registry/components/v2-tabs-widget/tabs-component.tsx b/frontend/lib/registry/components/v2-tabs-widget/tabs-component.tsx index 6aa29cb9..91965aa8 100644 --- a/frontend/lib/registry/components/v2-tabs-widget/tabs-component.tsx +++ b/frontend/lib/registry/components/v2-tabs-widget/tabs-component.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useRef, useCallback } from "react"; +import React, { useState, useRef, useCallback, useEffect } from "react"; import { ComponentRegistry } from "../../ComponentRegistry"; import { ComponentCategory } from "@/types/component"; import { Folder, Plus, Move, Settings, Trash2 } from "lucide-react"; @@ -21,8 +21,26 @@ const TabsDesignEditor: React.FC<{ const [dragPosition, setDragPosition] = useState<{ x: number; y: number } | null>(null); const containerRef = useRef(null); const rafRef = useRef(null); + + // ๋ฆฌ์‚ฌ์ด์ฆˆ ์ƒํƒœ + const [resizingCompId, setResizingCompId] = useState(null); + const [resizeSize, setResizeSize] = useState<{ width: number; height: number } | null>(null); + const [lastResizedCompId, setLastResizedCompId] = useState(null); const activeTab = tabs.find((t) => t.id === activeTabId); + + // ๐Ÿ†• ํƒญ ์ปดํฌ๋„ŒํŠธ size๊ฐ€ ์—…๋ฐ์ดํŠธ๋˜๋ฉด resizeSize ์ดˆ๊ธฐํ™” + useEffect(() => { + if (resizeSize && lastResizedCompId && !resizingCompId) { + const targetComp = activeTab?.components?.find(c => c.id === lastResizedCompId); + if (targetComp && + targetComp.size?.width === resizeSize.width && + targetComp.size?.height === resizeSize.height) { + setResizeSize(null); + setLastResizedCompId(null); + } + } + }, [tabs, activeTab, resizeSize, lastResizedCompId, resizingCompId]); const getTabStyle = (tab: TabItem) => { const isActive = tab.id === activeTabId; @@ -157,6 +175,110 @@ const TabsDesignEditor: React.FC<{ [activeTabId, component, onUpdateComponent, tabs] ); + // 10px ๋‹จ์œ„ ์Šค๋ƒ… ํ•จ์ˆ˜ + const snapTo10 = useCallback((value: number) => Math.round(value / 10) * 10, []); + + // ๋ฆฌ์‚ฌ์ด์ฆˆ ์‹œ์ž‘ ํ•ธ๋“ค๋Ÿฌ + const handleResizeStart = useCallback( + (e: React.MouseEvent, comp: TabInlineComponent, direction: "e" | "s" | "se") => { + e.stopPropagation(); + e.preventDefault(); + + const startMouseX = e.clientX; + const startMouseY = e.clientY; + const startWidth = comp.size?.width || 200; + const startHeight = comp.size?.height || 100; + + setResizingCompId(comp.id); + setResizeSize({ width: startWidth, height: startHeight }); + + const handleMouseMove = (moveEvent: MouseEvent) => { + if (rafRef.current) { + cancelAnimationFrame(rafRef.current); + } + + rafRef.current = requestAnimationFrame(() => { + const deltaX = moveEvent.clientX - startMouseX; + const deltaY = moveEvent.clientY - startMouseY; + + let newWidth = startWidth; + let newHeight = startHeight; + + if (direction === "e" || direction === "se") { + newWidth = snapTo10(Math.max(50, startWidth + deltaX)); + } + if (direction === "s" || direction === "se") { + newHeight = snapTo10(Math.max(20, startHeight + deltaY)); + } + + setResizeSize({ width: newWidth, height: newHeight }); + }); + }; + + const handleMouseUp = (upEvent: MouseEvent) => { + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + + if (rafRef.current) { + cancelAnimationFrame(rafRef.current); + rafRef.current = null; + } + + const deltaX = upEvent.clientX - startMouseX; + const deltaY = upEvent.clientY - startMouseY; + + let newWidth = startWidth; + let newHeight = startHeight; + + if (direction === "e" || direction === "se") { + newWidth = snapTo10(Math.max(50, startWidth + deltaX)); + } + if (direction === "s" || direction === "se") { + newHeight = snapTo10(Math.max(20, startHeight + deltaY)); + } + + // ๐Ÿ†• ํƒญ ์ปดํฌ๋„ŒํŠธ ํฌ๊ธฐ ์—…๋ฐ์ดํŠธ ๋จผ์ € ์‹คํ–‰ + if (onUpdateComponent) { + const updatedTabs = tabs.map((tab) => { + if (tab.id === activeTabId) { + return { + ...tab, + components: (tab.components || []).map((c) => + c.id === comp.id + ? { + ...c, + size: { + width: newWidth, + height: newHeight, + }, + } + : c + ), + }; + } + return tab; + }); + + onUpdateComponent({ + ...component, + componentConfig: { + ...component.componentConfig, + tabs: updatedTabs, + }, + }); + } + + // ๐Ÿ†• ๋ฆฌ์‚ฌ์ด์ฆˆ ์ƒํƒœ ํ•ด์ œ (resizeSize๋Š” ๋งˆ์ง€๋ง‰ ํฌ๊ธฐ ์œ ์ง€, lastResizedCompId ์„ค์ •) + setLastResizedCompId(comp.id); + setResizingCompId(null); + }; + + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + }, + [activeTabId, component, onUpdateComponent, tabs] + ); + return (
{/* ํƒญ ํ—ค๋” */} @@ -205,6 +327,15 @@ const TabsDesignEditor: React.FC<{ {activeTab.components.map((comp: TabInlineComponent) => { const isSelected = selectedTabComponentId === comp.id; const isDragging = draggingCompId === comp.id; + const isResizing = resizingCompId === comp.id; + + // ๋“œ๋ž˜๊ทธ/๋ฆฌ์‚ฌ์ด์ฆˆ ์ค‘ ํ‘œ์‹œํ•  ํฌ๊ธฐ + // resizeSize๊ฐ€ ์žˆ๊ณ  ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ์ด๋ฉด resizeSize ์šฐ์„  ์‚ฌ์šฉ (๋ ˆ์ด์•„์›ƒ ์—…๋ฐ์ดํŠธ ๋ฐ˜์˜ ์ „๊นŒ์ง€) + const compWidth = comp.size?.width || 200; + const compHeight = comp.size?.height || 100; + const isResizingThis = (resizingCompId === comp.id || lastResizedCompId === comp.id) && resizeSize; + const displayWidth = isResizingThis ? resizeSize!.width : compWidth; + const displayHeight = isResizingThis ? resizeSize!.height : compHeight; // ์ปดํฌ๋„ŒํŠธ ๋ฐ์ดํ„ฐ๋ฅผ DynamicComponentRenderer ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ const componentData = { @@ -213,7 +344,7 @@ const TabsDesignEditor: React.FC<{ componentType: comp.componentType, label: comp.label, position: comp.position || { x: 0, y: 0 }, - size: comp.size || { width: 200, height: 100 }, + size: { width: displayWidth, height: displayHeight }, componentConfig: comp.componentConfig || {}, style: comp.style || {}, }; @@ -279,23 +410,46 @@ const TabsDesignEditor: React.FC<{ {/* ์‹ค์ œ ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง - ํ•ธ๋“ค ์•„๋ž˜์— ๋ณ„๋„ ์˜์—ญ */}
- +
+ +
+ + {/* ๋ฆฌ์‚ฌ์ด์ฆˆ ๊ฐ€์žฅ์ž๋ฆฌ ์˜์—ญ - ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ์—๋งŒ ํ‘œ์‹œ */} + {isSelected && ( + <> + {/* ์˜ค๋ฅธ์ชฝ ๊ฐ€์žฅ์ž๋ฆฌ (๋„ˆ๋น„ ์กฐ์ ˆ) */} +
handleResizeStart(e, comp, "e")} + /> + {/* ์•„๋ž˜ ๊ฐ€์žฅ์ž๋ฆฌ (๋†’์ด ์กฐ์ ˆ) */} +
handleResizeStart(e, comp, "s")} + /> + {/* ์˜ค๋ฅธ์ชฝ ์•„๋ž˜ ๋ชจ์„œ๋ฆฌ (๋„ˆ๋น„+๋†’์ด ์กฐ์ ˆ) */} +
handleResizeStart(e, comp, "se")} + /> + + )}
);