From 825f164bdeddcbfeeedbd28f2c172416b77cfc27 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Mon, 16 Mar 2026 17:15:12 +0900 Subject: [PATCH] 22 --- .../screen/ResponsiveGridRenderer.tsx | 405 +----------------- frontend/package-lock.json | 42 +- 2 files changed, 47 insertions(+), 400 deletions(-) diff --git a/frontend/components/screen/ResponsiveGridRenderer.tsx b/frontend/components/screen/ResponsiveGridRenderer.tsx index c718c70e..1322ee99 100644 --- a/frontend/components/screen/ResponsiveGridRenderer.tsx +++ b/frontend/components/screen/ResponsiveGridRenderer.tsx @@ -2,8 +2,6 @@ import React, { useRef, useState, useEffect } from "react"; import { ComponentData } from "@/types/screen"; -import { useResponsive } from "@/lib/hooks/useResponsive"; -import { cn } from "@/lib/utils"; interface ResponsiveGridRendererProps { components: ComponentData[]; @@ -12,60 +10,6 @@ interface ResponsiveGridRendererProps { renderComponent: (component: ComponentData) => React.ReactNode; } -const FULL_WIDTH_TYPES = new Set([ - "table-list", - "v2-table-list", - "table-search-widget", - "v2-table-search-widget", - "conditional-container", - "split-panel-layout", - "split-panel-layout2", - "v2-split-panel-layout", - "screen-split-panel", - "v2-split-line", - "flow-widget", - "v2-tab-container", - "tab-container", - "tabs-widget", - "v2-tabs-widget", -]); - -const FLEX_GROW_TYPES = new Set([ - "table-list", - "v2-table-list", - "split-panel-layout", - "split-panel-layout2", - "v2-split-panel-layout", - "screen-split-panel", - "v2-tab-container", - "tab-container", - "tabs-widget", - "v2-tabs-widget", -]); - -function groupComponentsIntoRows( - components: ComponentData[], - threshold: number = 30 -): ComponentData[][] { - if (components.length === 0) return []; - const sorted = [...components].sort((a, b) => a.position.y - b.position.y); - const rows: ComponentData[][] = []; - let currentRow: ComponentData[] = []; - let currentRowY = -Infinity; - - for (const comp of sorted) { - if (comp.position.y - currentRowY > threshold) { - if (currentRow.length > 0) rows.push(currentRow); - currentRow = [comp]; - currentRowY = comp.position.y; - } else { - currentRow.push(comp); - } - } - if (currentRow.length > 0) rows.push(currentRow); - return rows.map((row) => row.sort((a, b) => a.position.x - b.position.x)); -} - function getComponentTypeId(component: ComponentData): string { const direct = (component as any).componentType || (component as any).widgetType; @@ -78,132 +22,10 @@ function getComponentTypeId(component: ComponentData): string { return component.type || ""; } -function isButtonComponent(component: ComponentData): boolean { - return getComponentTypeId(component).includes("button"); -} - -function isFullWidthComponent(component: ComponentData): boolean { - return FULL_WIDTH_TYPES.has(getComponentTypeId(component)); -} - -function shouldFlexGrow(component: ComponentData): boolean { - return FLEX_GROW_TYPES.has(getComponentTypeId(component)); -} - -function getPercentageWidth(componentWidth: number, canvasWidth: number): number { - const pct = (componentWidth / canvasWidth) * 100; - return pct >= 95 ? 100 : pct; -} - -function getRowGap(row: ComponentData[], canvasWidth: number): number { - if (row.length < 2) return 0; - const totalW = row.reduce((s, c) => s + (c.size?.width || 100), 0); - const gap = canvasWidth - totalW; - const cnt = row.length - 1; - if (gap <= 0 || cnt <= 0) return 8; - return Math.min(Math.max(Math.round(gap / cnt), 4), 24); -} - -interface ProcessedRow { - type: "normal" | "fullwidth"; - mainComponent?: ComponentData; - overlayComps: ComponentData[]; - normalComps: ComponentData[]; - rowMinY?: number; - rowMaxBottom?: number; -} - -function FullWidthOverlayRow({ - main, - overlayComps, - canvasWidth, - renderComponent, -}: { - main: ComponentData; - overlayComps: ComponentData[]; - canvasWidth: number; - renderComponent: (component: ComponentData) => React.ReactNode; -}) { - const containerRef = useRef(null); - const [containerW, setContainerW] = useState(0); - - useEffect(() => { - const el = containerRef.current; - if (!el) return; - const ro = new ResizeObserver((entries) => { - const w = entries[0]?.contentRect.width; - if (w && w > 0) setContainerW(w); - }); - ro.observe(el); - return () => ro.disconnect(); - }, []); - - const compFlexGrow = shouldFlexGrow(main); - const mainY = main.position.y; - const scale = containerW > 0 ? containerW / canvasWidth : 1; - - const minButtonY = Math.min(...overlayComps.map((c) => c.position.y)); - const rawYOffset = minButtonY - mainY; - const maxBtnH = Math.max( - ...overlayComps.map((c) => c.size?.height || 40) - ); - const yOffset = rawYOffset + (maxBtnH / 2) * (1 - scale); - - return ( -
-
- {renderComponent(main)} -
- - {overlayComps.length > 0 && containerW > 0 && ( -
- {overlayComps.map((comp) => ( -
- {renderComponent(comp)} -
- ))} -
- )} -
- ); -} - +/** + * 디자이너 절대좌표를 캔버스 대비 비율로 변환하여 렌더링. + * 화면이 줄어들면 비율에 맞게 축소, 늘어나면 확대. + */ function ProportionalRenderer({ components, canvasWidth, @@ -270,220 +92,13 @@ export function ResponsiveGridRenderer({ canvasHeight, renderComponent, }: ResponsiveGridRendererProps) { - const { isMobile } = useResponsive(); - - const topLevel = components.filter((c) => !c.parentId); - const hasFullWidthComponent = topLevel.some((c) => isFullWidthComponent(c)); - - if (!isMobile && !hasFullWidthComponent) { - return ( - - ); - } - - const rows = groupComponentsIntoRows(topLevel); - const processedRows: ProcessedRow[] = []; - - for (const row of rows) { - const fullWidthComps: ComponentData[] = []; - const normalComps: ComponentData[] = []; - - for (const comp of row) { - if (isFullWidthComponent(comp)) { - fullWidthComps.push(comp); - } else { - normalComps.push(comp); - } - } - - const allComps = [...fullWidthComps, ...normalComps]; - const rowMinY = allComps.length > 0 ? Math.min(...allComps.map(c => c.position.y)) : 0; - const rowMaxBottom = allComps.length > 0 ? Math.max(...allComps.map(c => c.position.y + (c.size?.height || 40))) : 0; - - if (fullWidthComps.length > 0 && normalComps.length > 0) { - for (const fwComp of fullWidthComps) { - processedRows.push({ - type: "fullwidth", - mainComponent: fwComp, - overlayComps: normalComps, - normalComps: [], - rowMinY, - rowMaxBottom, - }); - } - } else if (fullWidthComps.length > 0) { - for (const fwComp of fullWidthComps) { - processedRows.push({ - type: "fullwidth", - mainComponent: fwComp, - overlayComps: [], - normalComps: [], - rowMinY, - rowMaxBottom, - }); - } - } else { - processedRows.push({ - type: "normal", - overlayComps: [], - normalComps, - rowMinY, - rowMaxBottom, - }); - } - } - return ( -
- {processedRows.map((processedRow, rowIndex) => { - const rowMarginTop = (() => { - if (rowIndex === 0) return 0; - const prevRow = processedRows[rowIndex - 1]; - const prevBottom = prevRow.rowMaxBottom ?? 0; - const currTop = processedRow.rowMinY ?? 0; - const designGap = currTop - prevBottom; - if (designGap <= 0) return 0; - return Math.min(Math.max(Math.round(designGap * 0.5), 4), 48); - })(); - - if (processedRow.type === "fullwidth" && processedRow.mainComponent) { - return ( -
0 ? `${rowMarginTop}px` : undefined }}> - -
- ); - } - - const { normalComps } = processedRow; - const allButtons = normalComps.every((c) => isButtonComponent(c)); - - // 데스크톱에서 버튼만 있는 행: 디자이너의 x, width를 비율로 적용 - if (allButtons && normalComps.length > 0 && !isMobile) { - const rowHeight = Math.max(...normalComps.map(c => c.size?.height || 40)); - - return ( -
0 ? `${rowMarginTop}px` : undefined, - }} - > - {normalComps.map((component) => { - const typeId = getComponentTypeId(component); - const leftPct = (component.position.x / canvasWidth) * 100; - const widthPct = ((component.size?.width || 90) / canvasWidth) * 100; - - return ( -
- {renderComponent(component)} -
- ); - })} -
- ); - } - - const gap = isMobile ? 8 : getRowGap(normalComps, canvasWidth); - - const hasFlexHeightComp = normalComps.some((c) => { - const h = c.size?.height || 0; - return h / canvasHeight >= 0.8; - }); - - return ( -
0 ? `${rowMarginTop}px` : undefined }} - > - {normalComps.map((component) => { - const typeId = getComponentTypeId(component); - const isButton = isButtonComponent(component); - const isFullWidth = isMobile && !isButton; - - if (isButton) { - return ( -
- {renderComponent(component)} -
- ); - } - - const percentWidth = isFullWidth - ? 100 - : getPercentageWidth(component.size?.width || 100, canvasWidth); - const flexBasis = isFullWidth - ? "100%" - : `calc(${percentWidth}% - ${gap}px)`; - - const heightPct = (component.size?.height || 0) / canvasHeight; - const useFlexHeight = heightPct >= 0.8; - - return ( -
- {renderComponent(component)} -
- ); - })} -
- ); - })} -
+ ); } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 230d3139..85329c8b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -266,6 +266,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -307,6 +308,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -340,6 +342,7 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", + "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -3055,6 +3058,7 @@ "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.4.0.tgz", "integrity": "sha512-k4iu1R6e5D54918V4sqmISUkI5OgTw3v7/sDRKEC632Wd5g2WBtUS5gyG63X0GJO/HZUj1tsjSXfyzwrUHZl1g==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.17.8", "@types/react-reconciler": "^0.32.0", @@ -3708,6 +3712,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.6.tgz", "integrity": "sha512-gB1sljYjcobZKxjPbKSa31FUTyr+ROaBdoH+wSSs9Dk+yDCmMs+TkTV3PybRRVLC7ax7q0erJ9LvRWnMktnRAw==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/query-core": "5.90.6" }, @@ -3802,6 +3807,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.27.1.tgz", "integrity": "sha512-nkerkl8syHj44ZzAB7oA2GPmmZINKBKCa79FuNvmGJrJ4qyZwlkDzszud23YteFZEytbc87kVd/fP76ROS6sLg==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -4115,6 +4121,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.27.1.tgz", "integrity": "sha512-ijKo3+kIjALthYsnBmkRXAuw2Tswd9gd7BUR5OMfIcjGp8v576vKxOxrRfuYiUM78GPt//P0sVc1WV82H5N0PQ==", "license": "MIT", + "peer": true, "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", @@ -6615,6 +6622,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -6625,6 +6633,7 @@ "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -6667,6 +6676,7 @@ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.180.0.tgz", "integrity": "sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg==", "license": "MIT", + "peer": true, "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", @@ -6749,6 +6759,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -7381,6 +7392,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -8531,7 +8543,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/d3": { "version": "7.9.0", @@ -8853,6 +8866,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -9612,6 +9626,7 @@ "integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -9700,6 +9715,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -9801,6 +9817,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -10972,6 +10989,7 @@ "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -11752,7 +11770,8 @@ "version": "1.9.4", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "peer": true }, "node_modules/levn": { "version": "0.4.1", @@ -13091,6 +13110,7 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -13384,6 +13404,7 @@ "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==", "license": "MIT", + "peer": true, "dependencies": { "orderedmap": "^2.0.0" } @@ -13413,6 +13434,7 @@ "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", "license": "MIT", + "peer": true, "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0", @@ -13461,6 +13483,7 @@ "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.4.tgz", "integrity": "sha512-WkKgnyjNncri03Gjaz3IFWvCAE94XoiEgvtr0/r2Xw7R8/IjK3sKLSiDoCHWcsXSAinVaKlGRZDvMCsF1kbzjA==", "license": "MIT", + "peer": true, "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", @@ -13664,6 +13687,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -13733,6 +13757,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -13783,6 +13808,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz", "integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -13815,7 +13841,8 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-leaflet": { "version": "5.0.0", @@ -14123,6 +14150,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -14145,7 +14173,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/recharts/node_modules/redux-thunk": { "version": "3.1.0", @@ -15175,7 +15204,8 @@ "version": "0.180.0", "resolved": "https://registry.npmjs.org/three/-/three-0.180.0.tgz", "integrity": "sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/three-mesh-bvh": { "version": "0.8.3", @@ -15263,6 +15293,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -15611,6 +15642,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver"