diff --git a/.cursorrules b/.cursorrules index cf9eaae9..3b0c3833 100644 --- a/.cursorrules +++ b/.cursorrules @@ -1,22 +1,5 @@ # Cursor Rules for ERP-node Project -## ๐Ÿ”ฅ ํ•„์ˆ˜ ํ™•์ธ ๊ทœ์น™ (์ž‘์—… ์‹œ์ž‘ ์ „ & ์™„๋ฃŒ ํ›„) - -**AI ์—์ด์ „ํŠธ๋Š” ๋ชจ๋“  ์ž‘์—…์„ ์‹œ์ž‘ํ•˜๊ธฐ ์ „๊ณผ ์™„๋ฃŒํ•œ ํ›„์— ๋ฐ˜๋“œ์‹œ ๋‹ค์Œ ํŒŒ์ผ์„ ํ™•์ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:** -- [AI-๊ฐœ๋ฐœ์ž ํ˜‘์—… ์ž‘์—… ์ˆ˜์น™](.cursor/rules/ai-developer-collaboration-rules.mdc) - -**ํ•ต์‹ฌ 3์›์น™:** -1. **ํ™•์ธ ์šฐ์„ ** ๐Ÿ” - ์ถ”์ธกํ•˜์ง€ ๋ง๊ณ , ํ•ญ์ƒ ํ™•์ธํ•˜๊ณ  ์ž‘์—… -2. **ํ•œ ๋ฒˆ์— ํ•˜๋‚˜** ๐ŸŽฏ - ์—ฌ๋Ÿฌ ๋ฌธ์ œ๋ฅผ ๋™์‹œ์— ํ•ด๊ฒฐํ•˜๋ ค ํ•˜์ง€ ๋ง๊ธฐ -3. **์ฒ ์ €ํ•œ ๋งˆ๋ฌด๋ฆฌ** โœจ - ๋กœ๊ทธ ์ œ๊ฑฐ, ํ…Œ์ŠคํŠธ, ๋ช…ํ™•ํ•œ ์„ค๋ช… - -**์ ˆ๋Œ€ ๊ธˆ์ง€:** -- โŒ ํ™•์ธ ์—†์ด "์™„๋ฃŒํ–ˆ์Šต๋‹ˆ๋‹ค" ๋งํ•˜๊ธฐ -- โŒ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ปฌ๋Ÿผ๋ช… ์ถ”์ธกํ•˜๊ธฐ (๋ฐ˜๋“œ์‹œ MCP๋กœ ํ™•์ธ) -- โŒ ๋””๋ฒ„๊น… ๋กœ๊ทธ๋ฅผ ๋‚จ๊ฒจ๋‘” ์ฑ„ ์ž‘์—… ์ข…๋ฃŒ - ---- - ## ๐Ÿšจ ์ตœ์šฐ์„  ๋ณด์•ˆ ๊ทœ์น™: ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ **๋ชจ๋“  ์ฝ”๋“œ ์ž‘์„ฑ/์ˆ˜์ • ์™„๋ฃŒ ํ›„ ๋ฐ˜๋“œ์‹œ ๋‹ค์Œ ํŒŒ์ผ์„ ํ™•์ธํ•˜์„ธ์š”:** diff --git a/.gitignore b/.gitignore index e6e30135..a771d2c9 100644 --- a/.gitignore +++ b/.gitignore @@ -286,5 +286,4 @@ uploads/ *.hwp *.hwpx -claude.md -.cursor/rules/ai-developer-collaboration-rules.mdc +claude.md \ No newline at end of file diff --git a/frontend/components/screen/RealtimePreview.tsx b/frontend/components/screen/RealtimePreview.tsx index 86a2f357..ab8cc3ae 100644 --- a/frontend/components/screen/RealtimePreview.tsx +++ b/frontend/components/screen/RealtimePreview.tsx @@ -57,7 +57,7 @@ interface RealtimePreviewProps { isSelected?: boolean; isDesignMode?: boolean; onClick?: (e?: React.MouseEvent) => void; - onDragStart?: (e: React.MouseEvent | React.DragEvent) => void; // MouseEvent๋„ ํ—ˆ์šฉ + onDragStart?: (e: React.DragEvent) => void; onDragEnd?: () => void; onGroupToggle?: (groupId: string) => void; // ๊ทธ๋ฃน ์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ children?: React.ReactNode; // ๊ทธ๋ฃน ๋‚ด ์ž์‹ ์ปดํฌ๋„ŒํŠธ๋“ค @@ -247,13 +247,6 @@ export const RealtimePreviewDynamic: React.FC = ({ }) => { const { user } = useAuth(); const { type, id, position, size, style = {} } = component; - - // ๐Ÿ” [๋””๋ฒ„๊น…] ๋ Œ๋”๋ง ์‹œ ํฌ๊ธฐ ๋กœ๊ทธ - console.log("๐ŸŽจ [RealtimePreview] ๋ Œ๋”๋ง", { - componentId: id, - size, - position, - }); const [fileUpdateTrigger, setFileUpdateTrigger] = useState(0); const [actualHeight, setActualHeight] = useState(null); const contentRef = React.useRef(null); @@ -465,17 +458,7 @@ export const RealtimePreviewDynamic: React.FC = ({ onClick?.(e); }; - const handleMouseDown = (e: React.MouseEvent) => { - // ๋””์ž์ธ ๋ชจ๋“œ์—์„œ๋งŒ ๋“œ๋ž˜๊ทธ ์‹œ์ž‘ (์บ”๋ฒ„์Šค ๋‚ด ์ด๋™์šฉ) - if (isDesignMode && onDragStart) { - e.stopPropagation(); - // MouseEvent๋ฅผ ๊ทธ๋Œ€๋กœ ์ „๋‹ฌ - onDragStart(e); - } - }; - const handleDragStart = (e: React.DragEvent) => { - // HTML5 Drag API (ํŒ”๋ ˆํŠธ์—์„œ ์บ”๋ฒ„์Šค๋กœ ๋“œ๋ž˜๊ทธ์šฉ) e.stopPropagation(); onDragStart?.(e); }; @@ -490,9 +473,8 @@ export const RealtimePreviewDynamic: React.FC = ({ className="absolute cursor-pointer" style={{ ...componentStyle, ...selectionStyle }} onClick={handleClick} - onMouseDown={isDesignMode ? handleMouseDown : undefined} - draggable={!isDesignMode} // ๋””์ž์ธ ๋ชจ๋“œ๊ฐ€ ์•„๋‹ ๋•Œ๋งŒ draggable (ํŒ”๋ ˆํŠธ์šฉ) - onDragStart={!isDesignMode ? handleDragStart : undefined} + draggable + onDragStart={handleDragStart} onDragEnd={handleDragEnd} > {/* ์ปดํฌ๋„ŒํŠธ ํƒ€์ž…๋ณ„ ๋ Œ๋”๋ง */} diff --git a/frontend/components/screen/RealtimePreviewDynamic.tsx b/frontend/components/screen/RealtimePreviewDynamic.tsx index 80d577d6..679ed5a8 100644 --- a/frontend/components/screen/RealtimePreviewDynamic.tsx +++ b/frontend/components/screen/RealtimePreviewDynamic.tsx @@ -264,9 +264,6 @@ export const RealtimePreviewDynamic: React.FC = ({ height: getHeight(), zIndex: component.type === "layout" ? 1 : position.z || 2, ...componentStyle, - // ๐Ÿ”ฅ ์ค‘์š”: componentStyle.width๋ฅผ ๋ฎ์–ด์“ฐ๊ธฐ ์œ„ํ•ด ๋‹ค์‹œ ์„ค์ • - width: getWidth(), // size.width ๊ธฐ๋ฐ˜ ํ”ฝ์…€ ๊ฐ’์œผ๋กœ ๊ฐ•์ œ - height: getHeight(), // size.height ๊ธฐ๋ฐ˜ ํ”ฝ์…€ ๊ฐ’์œผ๋กœ ๊ฐ•์ œ right: undefined, }; diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 5c5e4dd2..7db03da6 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -29,9 +29,13 @@ import { snapToGrid, snapSizeToGrid, generateGridLines, + updateSizeFromGridColumns, + adjustGridColumnsFromSize, alignGroupChildrenToGrid, calculateOptimalGroupSize, normalizeGroupChildPositions, + calculateWidthFromColumns, + GridSettings as GridUtilSettings, } from "@/lib/utils/gridUtils"; import { GroupingToolbar } from "./GroupingToolbar"; import { screenApi, tableTypeApi } from "@/lib/api/screen"; @@ -103,8 +107,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD const [layout, setLayout] = useState({ components: [], gridSettings: { - snapToGrid: true, // ๊ฒฉ์ž ์Šค๋ƒ… ON - showGrid: false, // ๊ฒฉ์ž ํ‘œ์‹œ OFF + columns: 12, + gap: 16, + padding: 0, + snapToGrid: true, + showGrid: false, // ๊ธฐ๋ณธ๊ฐ’ false๋กœ ๋ณ€๊ฒฝ gridColor: "#d1d5db", gridOpacity: 0.5, }, @@ -533,31 +540,107 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD gridInfo && newComp.type !== "group" ) { - // ๐Ÿ”ฅ 10px ๊ณ ์ • ๊ฒฉ์ž๋กœ ์Šค๋ƒ… - const currentGridInfo = calculateGridInfo( - screenResolution.width, - screenResolution.height, - prevLayout.gridSettings, + // ํ˜„์žฌ ํ•ด์ƒ๋„์— ๋งž๋Š” ๊ฒฉ์ž ์ •๋ณด๋กœ ์Šค๋ƒ… ์ ์šฉ + const currentGridInfo = calculateGridInfo(screenResolution.width, screenResolution.height, { + columns: prevLayout.gridSettings.columns, + gap: prevLayout.gridSettings.gap, + padding: prevLayout.gridSettings.padding, + snapToGrid: prevLayout.gridSettings.snapToGrid || false, + }); + const snappedSize = snapSizeToGrid( + newComp.size, + currentGridInfo, + prevLayout.gridSettings as GridUtilSettings, ); - const snappedSize = snapSizeToGrid(newComp.size, currentGridInfo, prevLayout.gridSettings); newComp.size = snappedSize; + + // ํฌ๊ธฐ ๋ณ€๊ฒฝ ์‹œ gridColumns๋„ ์ž๋™ ์กฐ์ • + const adjustedColumns = adjustGridColumnsFromSize( + newComp, + currentGridInfo, + prevLayout.gridSettings as GridUtilSettings, + ); + if (newComp.gridColumns !== adjustedColumns) { + newComp.gridColumns = adjustedColumns; + } } - // ๐Ÿ—‘๏ธ gridColumns ๋กœ์ง ์ œ๊ฑฐ: 10px ๊ณ ์ • ๊ฒฉ์ž์—์„œ๋Š” ๋ถˆํ•„์š” + // gridColumns ๋ณ€๊ฒฝ ์‹œ ํฌ๊ธฐ๋ฅผ ๊ฒฉ์ž์— ๋งž๊ฒŒ ์ž๋™ ์กฐ์ • + if (path === "gridColumns" && prevLayout.gridSettings?.snapToGrid && newComp.type !== "group") { + const currentGridInfo = calculateGridInfo(screenResolution.width, screenResolution.height, { + columns: prevLayout.gridSettings.columns, + gap: prevLayout.gridSettings.gap, + padding: prevLayout.gridSettings.padding, + snapToGrid: prevLayout.gridSettings.snapToGrid || false, + }); + + // gridColumns์— ๋งž๋Š” ์ •ํ™•ํ•œ ๋„ˆ๋น„ ๊ณ„์‚ฐ + const newWidth = calculateWidthFromColumns( + newComp.gridColumns, + currentGridInfo, + prevLayout.gridSettings as GridUtilSettings, + ); + newComp.size = { + ...newComp.size, + width: newWidth, + }; + } // ์œ„์น˜ ๋ณ€๊ฒฝ ์‹œ ๊ฒฉ์ž ์Šค๋ƒ… ์ ์šฉ (๊ทธ๋ฃน ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ํฌํ•จ) if ( (path === "position.x" || path === "position.y" || path === "position") && layout.gridSettings?.snapToGrid ) { - // ๐Ÿ”ฅ 10px ๊ณ ์ • ๊ฒฉ์ž - const currentGridInfo = calculateGridInfo( - screenResolution.width, - screenResolution.height, - layout.gridSettings, - ); - const snappedPosition = snapToGrid(newComp.position, currentGridInfo, layout.gridSettings); - newComp.position = snappedPosition; + // ํ˜„์žฌ ํ•ด์ƒ๋„์— ๋งž๋Š” ๊ฒฉ์ž ์ •๋ณด ๊ณ„์‚ฐ + const currentGridInfo = calculateGridInfo(screenResolution.width, screenResolution.height, { + columns: layout.gridSettings.columns, + gap: layout.gridSettings.gap, + padding: layout.gridSettings.padding, + snapToGrid: layout.gridSettings.snapToGrid || false, + }); + + // ๊ทธ๋ฃน ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ์ธ ๊ฒฝ์šฐ ํŒจ๋”ฉ์„ ๊ณ ๋ คํ•œ ๊ฒฉ์ž ์Šค๋ƒ… ์ ์šฉ + if (newComp.parentId && currentGridInfo) { + const { columnWidth } = currentGridInfo; + const { gap } = layout.gridSettings; + + // ๊ทธ๋ฃน ๋‚ด๋ถ€ ํŒจ๋”ฉ ๊ณ ๋ คํ•œ ๊ฒฉ์ž ์ •๋ ฌ + const padding = 16; + const effectiveX = newComp.position.x - padding; + const columnIndex = Math.round(effectiveX / (columnWidth + (gap || 16))); + const snappedX = padding + columnIndex * (columnWidth + (gap || 16)); + + // Y ์ขŒํ‘œ๋Š” 10px ๋‹จ์œ„๋กœ ์Šค๋ƒ… + const effectiveY = newComp.position.y - padding; + const rowIndex = Math.round(effectiveY / 10); + const snappedY = padding + rowIndex * 10; + + // ํฌ๊ธฐ๋„ ์™ธ๋ถ€ ๊ฒฉ์ž์™€ ๋™์ผํ•˜๊ฒŒ ์Šค๋ƒ… + const fullColumnWidth = columnWidth + (gap || 16); // ์™ธ๋ถ€ ๊ฒฉ์ž์™€ ๋™์ผํ•œ ํฌ๊ธฐ + const widthInColumns = Math.max(1, Math.round(newComp.size.width / fullColumnWidth)); + const snappedWidth = widthInColumns * fullColumnWidth - (gap || 16); // gap ์ œ๊ฑฐํ•˜์—ฌ ์‹ค์ œ ์ปดํฌ๋„ŒํŠธ ํฌ๊ธฐ + // ๋†’์ด๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ๊ฐ’ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ (์Šค๋ƒ… ์ œ๊ฑฐ) + const snappedHeight = Math.max(10, newComp.size.height); + + newComp.position = { + x: Math.max(padding, snappedX), // ํŒจ๋”ฉ๋งŒํผ ์ตœ์†Œ ์—ฌ๋ฐฑ ํ™•๋ณด + y: Math.max(padding, snappedY), + z: newComp.position.z || 1, + }; + + newComp.size = { + width: snappedWidth, + height: snappedHeight, + }; + } else if (newComp.type !== "group") { + // ๊ทธ๋ฃน์ด ์•„๋‹Œ ์ผ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ๋งŒ ๊ฒฉ์ž ์Šค๋ƒ… ์ ์šฉ + const snappedPosition = snapToGrid( + newComp.position, + currentGridInfo, + layout.gridSettings as GridUtilSettings, + ); + newComp.position = snappedPosition; + } } return newComp; @@ -820,21 +903,20 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD const { convertLayoutComponents } = await import("@/lib/utils/webTypeConfigConverter"); const convertedComponents = convertLayoutComponents(layoutToUse.components); - // ๐Ÿ”ฅ 10px ๊ณ ์ • ๊ฒฉ์ž ์‹œ์Šคํ…œ์œผ๋กœ ์ž๋™ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ - // ์ด์ „ columns, gap, padding ์„ค์ •์„ ์ œ๊ฑฐํ•˜๊ณ  ์ƒˆ ์‹œ์Šคํ…œ์œผ๋กœ ๋ณ€ํ™˜ + // ๊ธฐ๋ณธ ๊ฒฉ์ž ์„ค์ • ๋ณด์žฅ (๊ฒฉ์ž ํ‘œ์‹œ์™€ ์Šค๋ƒ… ๊ธฐ๋ณธ ํ™œ์„ฑํ™”) const layoutWithDefaultGrid = { ...layoutToUse, components: convertedComponents, // ๋ณ€ํ™˜๋œ ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉ gridSettings: { - // ๐Ÿ—‘๏ธ ์ œ๊ฑฐ: columns, gap, padding (๋” ์ด์ƒ ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ) + columns: layoutToUse.gridSettings?.columns || 12, // DB ๊ฐ’ ์šฐ์„ , ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ 12 + gap: layoutToUse.gridSettings?.gap ?? 16, // DB ๊ฐ’ ์šฐ์„ , ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ 16 + padding: 0, // padding์€ ํ•ญ์ƒ 0์œผ๋กœ ๊ฐ•์ œ snapToGrid: layoutToUse.gridSettings?.snapToGrid ?? true, // DB ๊ฐ’ ์šฐ์„  showGrid: layoutToUse.gridSettings?.showGrid ?? false, // DB ๊ฐ’ ์šฐ์„  gridColor: layoutToUse.gridSettings?.gridColor || "#d1d5db", gridOpacity: layoutToUse.gridSettings?.gridOpacity ?? 0.5, }, }; - - console.log("โœ… ๊ฒฉ์ž ์„ค์ • ๋กœ๋“œ (10px ๊ณ ์ •):", layoutWithDefaultGrid.gridSettings); // ์ €์žฅ๋œ ํ•ด์ƒ๋„ ์ •๋ณด๊ฐ€ ์žˆ์œผ๋ฉด ์ ์šฉ, ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ if (layoutToUse.screenResolution) { @@ -992,12 +1074,51 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD }; }, [MIN_ZOOM, MAX_ZOOM]); - // ๊ฒฉ์ž ์„ค์ • ์—…๋ฐ์ดํŠธ (10px ๊ณ ์ • ๊ฒฉ์ž - ์ž๋™ ์Šค๋ƒ… ์ œ๊ฑฐ) + // ๊ฒฉ์ž ์„ค์ • ์—…๋ฐ์ดํŠธ ๋ฐ ์ปดํฌ๋„ŒํŠธ ์ž๋™ ์Šค๋ƒ… const updateGridSettings = useCallback( (newGridSettings: GridSettings) => { - // ๋‹จ์ˆœํžˆ ๊ฒฉ์ž ์„ค์ •๋งŒ ์—…๋ฐ์ดํŠธ (์ปดํฌ๋„ŒํŠธ ์ž๋™ ์ด๋™ ์—†์Œ) const newLayout = { ...layout, gridSettings: newGridSettings }; - + + // ๊ฒฉ์ž ์Šค๋ƒ…์ด ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ, ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ƒˆ๋กœ์šด ๊ฒฉ์ž์— ๋งž๊ฒŒ ์กฐ์ • + if (newGridSettings.snapToGrid && screenResolution.width > 0) { + // ์ƒˆ๋กœ์šด ๊ฒฉ์ž ์„ค์ •์œผ๋กœ ๊ฒฉ์ž ์ •๋ณด ์žฌ๊ณ„์‚ฐ (ํ•ด์ƒ๋„ ๊ธฐ์ค€) + const newGridInfo = calculateGridInfo(screenResolution.width, screenResolution.height, { + columns: newGridSettings.columns, + gap: newGridSettings.gap, + padding: newGridSettings.padding, + snapToGrid: newGridSettings.snapToGrid || false, + }); + + const gridUtilSettings = { + columns: newGridSettings.columns, + gap: newGridSettings.gap, + padding: newGridSettings.padding, + snapToGrid: newGridSettings.snapToGrid, + }; + + const adjustedComponents = layout.components.map((comp) => { + const snappedPosition = snapToGrid(comp.position, newGridInfo, gridUtilSettings); + const snappedSize = snapSizeToGrid(comp.size, newGridInfo, gridUtilSettings); + + // gridColumns๊ฐ€ ์—†๊ฑฐ๋‚˜ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚˜๋ฉด ์ž๋™ ์กฐ์ • + let adjustedGridColumns = comp.gridColumns; + if (!adjustedGridColumns || adjustedGridColumns < 1 || adjustedGridColumns > newGridSettings.columns) { + adjustedGridColumns = adjustGridColumnsFromSize({ size: snappedSize }, newGridInfo, gridUtilSettings); + } + + return { + ...comp, + position: snappedPosition, + size: snappedSize, + gridColumns: adjustedGridColumns, // gridColumns ์†์„ฑ ์ถ”๊ฐ€/์กฐ์ • + }; + }); + + newLayout.components = adjustedComponents; + // console.log("๊ฒฉ์ž ์„ค์ • ๋ณ€๊ฒฝ์œผ๋กœ ์ปดํฌ๋„ŒํŠธ ์œ„์น˜ ๋ฐ ํฌ๊ธฐ ์ž๋™ ์กฐ์ •:", adjustedComponents.length, "๊ฐœ"); + // console.log("์ƒˆ๋กœ์šด ๊ฒฉ์ž ์ •๋ณด:", newGridInfo); + } + setLayout(newLayout); saveToHistory(newLayout); }, @@ -1094,13 +1215,18 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD const snappedPosition = snapToGrid(comp.position, newGridInfo, gridUtilSettings); const snappedSize = snapSizeToGrid(comp.size, newGridInfo, gridUtilSettings); + // gridColumns ์žฌ๊ณ„์‚ฐ + const adjustedGridColumns = adjustGridColumnsFromSize({ size: snappedSize }, newGridInfo, gridUtilSettings); + return { ...comp, position: snappedPosition, size: snappedSize, + gridColumns: adjustedGridColumns, }; }); + console.log("๐Ÿงฒ ๊ฒฉ์ž ์Šค๋ƒ… ์ ์šฉ ์™„๋ฃŒ"); } const updatedLayout = { @@ -1159,10 +1285,17 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD const snappedPosition = snapToGrid(comp.position, currentGridInfo, gridUtilSettings); const snappedSize = snapSizeToGrid(comp.size, currentGridInfo, gridUtilSettings); + // gridColumns๊ฐ€ ์—†๊ฑฐ๋‚˜ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚˜๋ฉด ์ž๋™ ์กฐ์ • + let adjustedGridColumns = comp.gridColumns; + if (!adjustedGridColumns || adjustedGridColumns < 1 || adjustedGridColumns > layout.gridSettings!.columns) { + adjustedGridColumns = adjustGridColumnsFromSize({ size: snappedSize }, currentGridInfo, gridUtilSettings); + } + return { ...comp, position: snappedPosition, size: snappedSize, + gridColumns: adjustedGridColumns, }; }); @@ -1321,8 +1454,24 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD : { x: absoluteX, y: absoluteY, z: 1 }; if (templateComp.type === "container") { - // ๐Ÿ”ฅ 10px ๊ณ ์ • ๊ฒฉ์ž: ๊ธฐ๋ณธ ๋„ˆ๋น„ ์‚ฌ์šฉ - const calculatedSize = { width: 400, height: templateComp.size.height }; + // ๊ทธ๋ฆฌ๋“œ ์ปฌ๋Ÿผ ๊ธฐ๋ฐ˜ ํฌ๊ธฐ ๊ณ„์‚ฐ + const gridColumns = + typeof templateComp.size.width === "number" && templateComp.size.width <= 12 ? templateComp.size.width : 4; // ๊ธฐ๋ณธ 4์ปฌ๋Ÿผ + + const calculatedSize = + currentGridInfo && layout.gridSettings?.snapToGrid + ? (() => { + const newWidth = calculateWidthFromColumns( + gridColumns, + currentGridInfo, + layout.gridSettings as GridUtilSettings, + ); + return { + width: newWidth, + height: templateComp.size.height, + }; + })() + : { width: 400, height: templateComp.size.height }; // ํด๋ฐฑ ํฌ๊ธฐ return { id: componentId, @@ -1346,11 +1495,21 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD // ๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ” ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ const gridColumns = 6; // ๊ธฐ๋ณธ๊ฐ’: 6์ปฌ๋Ÿผ (50% ๋„ˆ๋น„) - // ๐Ÿ”ฅ 10px ๊ณ ์ • ๊ฒฉ์ž: ๊ธฐ๋ณธ ํฌ๊ธฐ ์‚ฌ์šฉ - const calculatedSize = { - width: 800, // ๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ” ๊ธฐ๋ณธ ๋„ˆ๋น„ - height: templateComp.size.height, - }; + // gridColumns์— ๋งž๋Š” ํฌ๊ธฐ ๊ณ„์‚ฐ + const calculatedSize = + currentGridInfo && layout.gridSettings?.snapToGrid + ? (() => { + const newWidth = calculateWidthFromColumns( + gridColumns, + currentGridInfo, + layout.gridSettings as GridUtilSettings, + ); + return { + width: newWidth, + height: templateComp.size.height, // ๋†’์ด๋Š” ํ…œํ”Œ๋ฆฟ ๊ฐ’ ์œ ์ง€ + }; + })() + : templateComp.size; console.log("๐Ÿ“Š ๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ” ์ƒ์„ฑ ์‹œ ํฌ๊ธฐ ๊ณ„์‚ฐ:", { gridColumns, @@ -1415,11 +1574,20 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD // ํŒŒ์ผ ์ฒจ๋ถ€ ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ const gridColumns = 6; // ๊ธฐ๋ณธ๊ฐ’: 6์ปฌ๋Ÿผ - // ๐Ÿ”ฅ 10px ๊ณ ์ • ๊ฒฉ์ž - const calculatedSize = { - width: 400, - height: templateComp.size.height, - }; + const calculatedSize = + currentGridInfo && layout.gridSettings?.snapToGrid + ? (() => { + const newWidth = calculateWidthFromColumns( + gridColumns, + currentGridInfo, + layout.gridSettings as GridUtilSettings, + ); + return { + width: newWidth, + height: templateComp.size.height, + }; + })() + : templateComp.size; return { id: componentId, @@ -1457,11 +1625,20 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD // ์˜์—ญ ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ const gridColumns = 6; // ๊ธฐ๋ณธ๊ฐ’: 6์ปฌ๋Ÿผ (50% ๋„ˆ๋น„) - // ๐Ÿ”ฅ 10px ๊ณ ์ • ๊ฒฉ์ž - const calculatedSize = { - width: 600, // ์˜์—ญ ๊ธฐ๋ณธ ๋„ˆ๋น„ - height: templateComp.size.height, - }; + const calculatedSize = + currentGridInfo && layout.gridSettings?.snapToGrid + ? (() => { + const newWidth = calculateWidthFromColumns( + gridColumns, + currentGridInfo, + layout.gridSettings as GridUtilSettings, + ); + return { + width: newWidth, + height: templateComp.size.height, + }; + })() + : templateComp.size; return { id: componentId, @@ -1583,7 +1760,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD const widgetSize = currentGridInfo && layout.gridSettings?.snapToGrid ? { - width: 200, + width: calculateWidthFromColumns(1, currentGridInfo, layout.gridSettings as GridUtilSettings), height: templateComp.size.height, } : templateComp.size; @@ -1954,9 +2131,23 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD }); } - // ๐Ÿ—‘๏ธ 10px ๊ณ ์ • ๊ฒฉ์ž: gridColumns ๋กœ์ง ์ œ๊ฑฐ - // ๊ธฐ๋ณธ ํฌ๊ธฐ๋งŒ ์‚ฌ์šฉ - componentSize = component.defaultSize; + // ๊ทธ๋ฆฌ๋“œ ์‹œ์Šคํ…œ์ด ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ gridColumns์— ๋งž์ถฐ ๋„ˆ๋น„ ์žฌ๊ณ„์‚ฐ + if (layout.gridSettings?.snapToGrid && gridInfo) { + // gridColumns์— ๋งž๋Š” ์ •ํ™•ํ•œ ๋„ˆ๋น„ ๊ณ„์‚ฐ + const calculatedWidth = calculateWidthFromColumns( + gridColumns, + gridInfo, + layout.gridSettings as GridUtilSettings, + ); + + // ์ปดํฌ๋„ŒํŠธ๋ณ„ ์ตœ์†Œ ํฌ๊ธฐ ๋ณด์žฅ + const minWidth = isTableList ? 120 : isCardDisplay ? 400 : component.defaultSize.width; + + componentSize = { + ...component.defaultSize, + width: Math.max(calculatedWidth, minWidth), + }; + } console.log("๐ŸŽจ ์ตœ์ข… ์ปดํฌ๋„ŒํŠธ ํฌ๊ธฐ:", { componentId: component.id, @@ -2056,12 +2247,15 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD e.preventDefault(); const dragData = e.dataTransfer.getData("application/json"); + // console.log("๐ŸŽฏ ๋“œ๋กญ ์ด๋ฒคํŠธ:", { dragData }); if (!dragData) { + // console.log("โŒ ๋“œ๋ž˜๊ทธ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค"); return; } try { const parsedData = JSON.parse(dragData); + // console.log("๐Ÿ“‹ ํŒŒ์‹ฑ๋œ ๋ฐ์ดํ„ฐ:", parsedData); // ํ…œํ”Œ๋ฆฟ ๋“œ๋ž˜๊ทธ์ธ ๊ฒฝ์šฐ if (parsedData.type === "template") { @@ -2115,8 +2309,34 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD }; } else if (type === "column") { // console.log("๐Ÿ”„ ์ปฌ๋Ÿผ ๋“œ๋กญ ์ฒ˜๋ฆฌ:", { webType: column.widgetType, columnName: column.columnName }); - // ๐Ÿ”ฅ 10px ๊ณ ์ • ๊ฒฉ์ž ์‹œ์Šคํ…œ: ๊ฐ„๋‹จํ•œ ๊ธฐ๋ณธ ๋„ˆ๋น„ ์‚ฌ์šฉ - const defaultWidth = 200; // ๊ธฐ๋ณธ ๋„ˆ๋น„ 200px + // ํ˜„์žฌ ํ•ด์ƒ๋„์— ๋งž๋Š” ๊ฒฉ์ž ์ •๋ณด๋กœ ๊ธฐ๋ณธ ํฌ๊ธฐ ๊ณ„์‚ฐ + const currentGridInfo = layout.gridSettings + ? calculateGridInfo(screenResolution.width, screenResolution.height, { + columns: layout.gridSettings.columns, + gap: layout.gridSettings.gap, + padding: layout.gridSettings.padding, + snapToGrid: layout.gridSettings.snapToGrid || false, + }) + : null; + + // ๊ฒฉ์ž ์Šค๋ƒ…์ด ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ ์ •ํ™•ํ•œ ๊ฒฉ์ž ํฌ๊ธฐ๋กœ ์ƒ์„ฑ, ์•„๋‹ˆ๋ฉด ๊ธฐ๋ณธ๊ฐ’ + const defaultWidth = + currentGridInfo && layout.gridSettings?.snapToGrid + ? calculateWidthFromColumns(1, currentGridInfo, layout.gridSettings as GridUtilSettings) + : 200; + + console.log("๐ŸŽฏ ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ ์‹œ ํฌ๊ธฐ ๊ณ„์‚ฐ:", { + screenResolution: `${screenResolution.width}x${screenResolution.height}`, + gridSettings: layout.gridSettings, + currentGridInfo: currentGridInfo + ? { + columnWidth: currentGridInfo.columnWidth.toFixed(2), + totalWidth: currentGridInfo.totalWidth, + } + : null, + defaultWidth: defaultWidth.toFixed(2), + snapToGrid: layout.gridSettings?.snapToGrid, + }); // ์›นํƒ€์ž…๋ณ„ ๊ธฐ๋ณธ ๊ทธ๋ฆฌ๋“œ ์ปฌ๋Ÿผ ์ˆ˜ ๊ณ„์‚ฐ const getDefaultGridColumns = (widgetType: string): number => { @@ -2155,6 +2375,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD }; const defaultColumns = widthMap[widgetType] || 3; // ๊ธฐ๋ณธ๊ฐ’ 3 (1/4, 25%) + console.log("๐ŸŽฏ [ScreenDesigner] getDefaultGridColumns:", { widgetType, defaultColumns }); return defaultColumns; }; @@ -2167,7 +2388,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD file: 240, // ํŒŒ์ผ ์—…๋กœ๋“œ (40 * 6) }; - return heightMap[widgetType] || 30; // ๊ธฐ๋ณธ๊ฐ’ 30px๋กœ ๋ณ€๊ฒฝ + return heightMap[widgetType] || 40; // ๊ธฐ๋ณธ๊ฐ’ 40 }; // ์›นํƒ€์ž…๋ณ„ ๊ธฐ๋ณธ ์„ค์ • ์ƒ์„ฑ @@ -2326,9 +2547,22 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD // ์›นํƒ€์ž…๋ณ„ ์ ์ ˆํ•œ gridColumns ๊ณ„์‚ฐ const calculatedGridColumns = getDefaultGridColumns(column.widgetType); - // ๐Ÿ”ฅ 10px ๊ณ ์ • ๊ฒฉ์ž: ๊ฐ„๋‹จํ•œ ๋„ˆ๋น„ ๊ณ„์‚ฐ - const componentWidth = defaultWidth; + // gridColumns์— ๋งž๋Š” ์‹ค์ œ ๋„ˆ๋น„ ๊ณ„์‚ฐ + const componentWidth = + currentGridInfo && layout.gridSettings?.snapToGrid + ? calculateWidthFromColumns( + calculatedGridColumns, + currentGridInfo, + layout.gridSettings as GridUtilSettings, + ) + : defaultWidth; + console.log("๐ŸŽฏ ํผ ์ปจํ…Œ์ด๋„ˆ ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ:", { + widgetType: column.widgetType, + calculatedGridColumns, + componentWidth, + defaultWidth, + }); newComponent = { id: generateComponentId(), @@ -2349,7 +2583,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD codeCategory: column.codeCategory, }), style: { - labelDisplay: false, // ๋ผ๋ฒจ ์ˆจ๊น€ (placeholder ์‚ฌ์šฉ) + labelDisplay: false, // ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ์˜ ๊ธฐ๋ณธ ๋ผ๋ฒจ ํ‘œ์‹œ๋ฅผ false๋กœ ์„ค์ • labelFontSize: "12px", labelColor: "#212121", labelFontWeight: "500", @@ -2361,7 +2595,6 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD webType: column.widgetType, // ์›๋ณธ ์›นํƒ€์ž… ๋ณด์กด inputType: column.inputType, // โœ… input_type ์ถ”๊ฐ€ (category ๋“ฑ) ...getDefaultWebTypeConfig(column.widgetType), - placeholder: column.columnLabel || column.columnName, // placeholder์— ์ปฌ๋Ÿผ ๋ผ๋ฒจ๋ช… ํ‘œ์‹œ // ์ฝ”๋“œ ํƒ€์ž…์ธ ๊ฒฝ์šฐ ์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ ์ •๋ณด ์ถ”๊ฐ€ ...(column.widgetType === "code" && column.codeCategory && { @@ -2380,9 +2613,22 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD // ์›นํƒ€์ž…๋ณ„ ์ ์ ˆํ•œ gridColumns ๊ณ„์‚ฐ const calculatedGridColumns = getDefaultGridColumns(column.widgetType); - // ๐Ÿ”ฅ 10px ๊ณ ์ • ๊ฒฉ์ž: ๊ฐ„๋‹จํ•œ ๋„ˆ๋น„ ๊ณ„์‚ฐ - const componentWidth = defaultWidth; + // gridColumns์— ๋งž๋Š” ์‹ค์ œ ๋„ˆ๋น„ ๊ณ„์‚ฐ + const componentWidth = + currentGridInfo && layout.gridSettings?.snapToGrid + ? calculateWidthFromColumns( + calculatedGridColumns, + currentGridInfo, + layout.gridSettings as GridUtilSettings, + ) + : defaultWidth; + console.log("๐ŸŽฏ ์บ”๋ฒ„์Šค ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ:", { + widgetType: column.widgetType, + calculatedGridColumns, + componentWidth, + defaultWidth, + }); // ๐Ÿ” ์ด๋ฏธ์ง€ ํƒ€์ž… ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ ๋””๋ฒ„๊น… // if (column.widgetType === "image") { @@ -2412,7 +2658,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD codeCategory: column.codeCategory, }), style: { - labelDisplay: false, // ๋ผ๋ฒจ ์ˆจ๊น€ (placeholder ์‚ฌ์šฉ) + labelDisplay: true, // ํ…Œ์ด๋ธ” ํŒจ๋„์—์„œ ๋“œ๋ž˜๊ทธํ•œ ์ปดํฌ๋„ŒํŠธ๋Š” ๋ผ๋ฒจ์„ ๊ธฐ๋ณธ์ ์œผ๋กœ ํ‘œ์‹œ labelFontSize: "14px", labelColor: "#000000", // ์ˆœ์ˆ˜ํ•œ ๊ฒ€์ • labelFontWeight: "500", @@ -2424,7 +2670,6 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD webType: column.widgetType, // ์›๋ณธ ์›นํƒ€์ž… ๋ณด์กด inputType: column.inputType, // โœ… input_type ์ถ”๊ฐ€ (category ๋“ฑ) ...getDefaultWebTypeConfig(column.widgetType), - placeholder: column.columnLabel || column.columnName, // placeholder์— ์ปฌ๋Ÿผ ๋ผ๋ฒจ๋ช… ํ‘œ์‹œ // ์ฝ”๋“œ ํƒ€์ž…์ธ ๊ฒฝ์šฐ ์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ ์ •๋ณด ์ถ”๊ฐ€ ...(column.widgetType === "code" && column.codeCategory && { @@ -2456,6 +2701,21 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD newComponent.position = snapToGrid(newComponent.position, currentGridInfo, gridUtilSettings); newComponent.size = snapSizeToGrid(newComponent.size, currentGridInfo, gridUtilSettings); + console.log("๐Ÿงฒ ์ƒˆ ์ปดํฌ๋„ŒํŠธ ๊ฒฉ์ž ์Šค๋ƒ… ์ ์šฉ:", { + type: newComponent.type, + resolution: `${screenResolution.width}x${screenResolution.height}`, + snappedPosition: newComponent.position, + snappedSize: newComponent.size, + columnWidth: currentGridInfo.columnWidth, + }); + } + + if (newComponent.type === "group") { + console.log("๐Ÿ”“ ๊ทธ๋ฃน ์ปดํฌ๋„ŒํŠธ๋Š” ๊ฒฉ์ž ์Šค๋ƒ… ์ œ์™ธ:", { + type: newComponent.type, + position: newComponent.position, + size: newComponent.size, + }); } const newLayout = { @@ -2629,11 +2889,27 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD componentsToMove = [...componentsToMove, ...additionalComponents]; } - const finalGrabOffset = { - x: relativeMouseX - component.position.x, - y: relativeMouseY - component.position.y, - }; - + // console.log("๋“œ๋ž˜๊ทธ ์‹œ์ž‘:", component.id, "์ด๋™ํ•  ์ปดํฌ๋„ŒํŠธ ์ˆ˜:", componentsToMove.length); + console.log("๋งˆ์šฐ์Šค ์œ„์น˜ (์คŒ ๋ณด์ •):", { + zoomLevel, + clientX: event.clientX, + clientY: event.clientY, + rectLeft: rect.left, + rectTop: rect.top, + mouseRaw: { x: event.clientX - rect.left, y: event.clientY - rect.top }, + mouseZoomCorrected: { x: relativeMouseX, y: relativeMouseY }, + componentX: component.position.x, + componentY: component.position.y, + grabOffsetX: relativeMouseX - component.position.x, + grabOffsetY: relativeMouseY - component.position.y, + }); + + console.log("๐Ÿš€ ๋“œ๋ž˜๊ทธ ์‹œ์ž‘:", { + componentId: component.id, + componentType: component.type, + initialPosition: { x: component.position.x, y: component.position.y }, + }); + setDragState({ isDragging: true, draggedComponent: component, // ์ฃผ ๋“œ๋ž˜๊ทธ ์ปดํฌ๋„ŒํŠธ (๋งˆ์šฐ์Šค ์œ„์น˜ ๊ธฐ์ค€) @@ -2648,7 +2924,10 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD y: component.position.y, z: (component.position as Position).z || 1, }, - grabOffset: finalGrabOffset, + grabOffset: { + x: relativeMouseX - component.position.x, + y: relativeMouseY - component.position.y, + }, justFinishedDrag: false, }); }, @@ -2676,24 +2955,34 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD const rawX = relativeMouseX - dragState.grabOffset.x; const rawY = relativeMouseY - dragState.grabOffset.y; - // ๐Ÿ”ฅ ๊ฒฝ๊ณ„ ์ œํ•œ ๋กœ์ง ์ œ๊ฑฐ: ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ™”๋ฉด์„ ๋ฒ—์–ด๋‚˜๋„ ๋˜๊ฒŒ ํ•จ - // ์ด์œ : - // 1. ํฐ ์ปดํฌ๋„ŒํŠธ(884px)๋ฅผ ์ž‘์€ ์˜์—ญ(16px)์—๋งŒ ์ œํ•œํ•˜๋Š” ๊ฒƒ์€ ์‚ฌ์šฉ์„ฑ ๋ฌธ์ œ - // 2. ์‚ฌ์šฉ์ž๊ฐ€ ์ž์œ ๋กญ๊ฒŒ ๋ฐฐ์น˜ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•จ - // 3. ์ตœ์†Œ ์œ„์น˜๋งŒ 0 ์ด์ƒ์œผ๋กœ ์ œํ•œ (์Œ์ˆ˜ ์ขŒํ‘œ ๋ฐฉ์ง€) - const newPosition = { - x: Math.max(0, rawX), - y: Math.max(0, rawY), + x: Math.max(0, Math.min(rawX, screenResolution.width - componentWidth)), + y: Math.max(0, Math.min(rawY, screenResolution.height - componentHeight)), z: (dragState.draggedComponent.position as Position).z || 1, }; // ๋“œ๋ž˜๊ทธ ์ƒํƒœ ์—…๋ฐ์ดํŠธ + console.log("๐Ÿ”ฅ ScreenDesigner updateDragPosition (์คŒ ๋ณด์ •):", { + zoomLevel, + draggedComponentId: dragState.draggedComponent.id, + mouseRaw: { x: event.clientX - rect.left, y: event.clientY - rect.top }, + mouseZoomCorrected: { x: relativeMouseX, y: relativeMouseY }, + oldPosition: dragState.currentPosition, + newPosition: newPosition, + }); + setDragState((prev) => { const newState = { ...prev, currentPosition: { ...newPosition }, // ์ƒˆ๋กœ์šด ๊ฐ์ฒด ์ƒ์„ฑ }; + console.log("๐Ÿ”„ ScreenDesigner dragState ์—…๋ฐ์ดํŠธ:", { + prevPosition: prev.currentPosition, + newPosition: newState.currentPosition, + stateChanged: + prev.currentPosition.x !== newState.currentPosition.x || + prev.currentPosition.y !== newState.currentPosition.y, + }); return newState; }); @@ -2711,8 +3000,15 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD const draggedComponent = layout.components.find((c) => c.id === dragState.draggedComponent); let finalPosition = dragState.currentPosition; - // ๐Ÿ”ฅ 10px ๊ณ ์ • ๊ฒฉ์ž ์‹œ์Šคํ…œ: calculateGridInfo๋Š” columns, gap, padding์„ ๋ฌด์‹œํ•จ - const currentGridInfo = calculateGridInfo(screenResolution.width, screenResolution.height, layout.gridSettings); + // ํ˜„์žฌ ํ•ด์ƒ๋„์— ๋งž๋Š” ๊ฒฉ์ž ์ •๋ณด ๊ณ„์‚ฐ + const currentGridInfo = layout.gridSettings + ? calculateGridInfo(screenResolution.width, screenResolution.height, { + columns: layout.gridSettings.columns, + gap: layout.gridSettings.gap, + padding: layout.gridSettings.padding, + snapToGrid: layout.gridSettings.snapToGrid || false, + }) + : null; // ์ผ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ ๋ฐ ํ”Œ๋กœ์šฐ ๋ฒ„ํŠผ ๊ทธ๋ฃน์— ๊ฒฉ์ž ์Šค๋ƒ… ์ ์šฉ (์ผ๋ฐ˜ ๊ทธ๋ฃน ์ œ์™ธ) if (draggedComponent?.type !== "group" && layout.gridSettings?.snapToGrid && currentGridInfo) { @@ -2723,9 +3019,21 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD z: dragState.currentPosition.z ?? 1, }, currentGridInfo, - layout.gridSettings, + { + columns: layout.gridSettings.columns, + gap: layout.gridSettings.gap, + padding: layout.gridSettings.padding, + snapToGrid: layout.gridSettings.snapToGrid || false, + }, ); + console.log("๐ŸŽฏ ๊ฒฉ์ž ์Šค๋ƒ… ์ ์šฉ๋จ:", { + componentType: draggedComponent?.type, + resolution: `${screenResolution.width}x${screenResolution.height}`, + originalPosition: dragState.currentPosition, + snappedPosition: finalPosition, + columnWidth: currentGridInfo.columnWidth, + }); } // ์Šค๋ƒ…์œผ๋กœ ์ธํ•œ ์ถ”๊ฐ€ ์ด๋™ ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ @@ -2790,6 +3098,28 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD height: snappedHeight, }; + console.log("๐ŸŽฏ ๋“œ๋ž˜๊ทธ ์ข…๋ฃŒ ์‹œ ๊ทธ๋ฃน ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ๊ฒฉ์ž ์Šค๋ƒ… (ํŒจ๋”ฉ ๊ณ ๋ ค):", { + componentId: comp.id, + parentId: comp.parentId, + beforeSnap: { + x: originalComponent.position.x + totalDeltaX, + y: originalComponent.position.y + totalDeltaY, + }, + calculation: { + effectiveX, + effectiveY, + columnIndex, + rowIndex, + columnWidth, + fullColumnWidth, + widthInColumns, + gap: gap || 16, + padding, + }, + afterSnap: newPosition, + afterSizeSnap: newSize, + }); + return { ...comp, position: newPosition as Position, @@ -2812,6 +3142,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD if (selectedComponent && dragState.draggedComponents.some((c) => c.id === selectedComponent.id)) { const updatedSelectedComponent = updatedComponents.find((c) => c.id === selectedComponent.id); if (updatedSelectedComponent) { + console.log("๐Ÿ”„ ScreenDesigner: ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ ์œ„์น˜ ์—…๋ฐ์ดํŠธ", { + componentId: selectedComponent.id, + oldPosition: selectedComponent.position, + newPosition: updatedSelectedComponent.position, + }); setSelectedComponent(updatedSelectedComponent); } } diff --git a/frontend/components/screen/panels/GridPanel.tsx b/frontend/components/screen/panels/GridPanel.tsx index 34d324f8..f33cc601 100644 --- a/frontend/components/screen/panels/GridPanel.tsx +++ b/frontend/components/screen/panels/GridPanel.tsx @@ -1,79 +1,335 @@ +"use client"; + import React from "react"; import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; import { Checkbox } from "@/components/ui/checkbox"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Grid3X3 } from "lucide-react"; -import { GridSettings } from "@/types/screen-management"; +import { Button } from "@/components/ui/button"; +import { Separator } from "@/components/ui/separator"; +import { Slider } from "@/components/ui/slider"; +import { Grid3X3, RotateCcw, Eye, EyeOff, Zap, RefreshCw } from "lucide-react"; +import { GridSettings, ScreenResolution } from "@/types/screen"; +import { calculateGridInfo } from "@/lib/utils/gridUtils"; interface GridPanelProps { gridSettings: GridSettings; onGridSettingsChange: (settings: GridSettings) => void; + onResetGrid: () => void; + onForceGridUpdate?: () => void; // ๊ฐ•์ œ ๊ฒฉ์ž ์žฌ์กฐ์ • ์ถ”๊ฐ€ + screenResolution?: ScreenResolution; // ํ•ด์ƒ๋„ ์ •๋ณด ์ถ”๊ฐ€ } -/** - * ๊ฒฉ์ž ์„ค์ • ํŒจ๋„ (10px ๊ณ ์ • ๊ฒฉ์ž) - * - * ์‚ฌ์šฉ์ž ์„ค์ •: - * - ๊ฒฉ์ž ํ‘œ์‹œ ON/OFF - * - ๊ฒฉ์ž ์Šค๋ƒ… ON/OFF - * - * ์ž๋™ ์„ค์ • (๋ณ€๊ฒฝ ๋ถˆ๊ฐ€): - * - ๊ฒฉ์ž ํฌ๊ธฐ: 10px ๊ณ ์ • - * - ๊ฒฉ์ž ๊ฐ„๊ฒฉ: 10px ๊ณ ์ • - */ -export function GridPanel({ gridSettings, onGridSettingsChange }: GridPanelProps) { - const updateSetting = (key: K, value: GridSettings[K]) => { +export const GridPanel: React.FC = ({ + gridSettings, + onGridSettingsChange, + onResetGrid, + onForceGridUpdate, + screenResolution, +}) => { + const updateSetting = (key: keyof GridSettings, value: any) => { onGridSettingsChange({ ...gridSettings, [key]: value, }); }; + // ์ตœ๋Œ€ ์ปฌ๋Ÿผ ์ˆ˜ ๊ณ„์‚ฐ (์ตœ์†Œ ์ปฌ๋Ÿผ ๋„ˆ๋น„ 30px ๊ธฐ์ค€) + const MIN_COLUMN_WIDTH = 30; + const maxColumns = screenResolution + ? Math.floor((screenResolution.width - gridSettings.padding * 2 + gridSettings.gap) / (MIN_COLUMN_WIDTH + gridSettings.gap)) + : 24; + const safeMaxColumns = Math.max(1, Math.min(maxColumns, 100)); // ์ตœ๋Œ€ 100๊ฐœ๋กœ ์ œํ•œ + + // ์‹ค์ œ ๊ฒฉ์ž ์ •๋ณด ๊ณ„์‚ฐ + const actualGridInfo = screenResolution + ? calculateGridInfo(screenResolution.width, screenResolution.height, { + columns: gridSettings.columns, + gap: gridSettings.gap, + padding: gridSettings.padding, + snapToGrid: gridSettings.snapToGrid || false, + }) + : null; + + // ์‹ค์ œ ํ‘œ์‹œ๋˜๋Š” ์ปฌ๋Ÿผ ์ˆ˜ ๊ณ„์‚ฐ (ํ•ญ์ƒ ์„ค์ •๋œ ๊ฐœ์ˆ˜๋ฅผ ํ‘œ์‹œํ•˜๋˜, ๋„ˆ๋น„๊ฐ€ ๋„ˆ๋ฌด ์ž‘์œผ๋ฉด ๊ฒฝ๊ณ ) + const actualColumns = gridSettings.columns; + + // ์ปฌ๋Ÿผ์ด ๋„ˆ๋ฌด ์ž‘์€์ง€ ํ™•์ธ + const isColumnsTooSmall = + screenResolution && actualGridInfo + ? actualGridInfo.columnWidth < MIN_COLUMN_WIDTH + : false; + return ( - - -
- - ๊ฒฉ์ž ์„ค์ • -
-
- - {/* ๊ฒฉ์ž ํ‘œ์‹œ */} -
- - updateSetting("showGrid", checked as boolean)} - /> +
+ {/* ํ—ค๋” */} +
+
+
+ +

๊ฒฉ์ž ์„ค์ •

+
+ +
+ {onForceGridUpdate && ( + + )} + + +
- {/* ๊ฒฉ์ž ์Šค๋ƒ… */} -
- - updateSetting("snapToGrid", checked as boolean)} - /> + {/* ์ฃผ์š” ํ† ๊ธ€๋“ค */} +
+
+
+ {gridSettings.showGrid ? ( + + ) : ( + + )} + +
+ updateSetting("showGrid", checked)} + /> +
+ +
+
+ + +
+ updateSetting("snapToGrid", checked)} + /> +
+
+
+ + {/* ์„ค์ • ์˜์—ญ */} +
+ {/* ๊ฒฉ์ž ๊ตฌ์กฐ */} +
+

๊ฒฉ์ž ๊ตฌ์กฐ

+ +
+ +
+ { + const value = parseInt(e.target.value, 10); + if (!isNaN(value) && value >= 1 && value <= safeMaxColumns) { + updateSetting("columns", value); + } + }} + className="h-8 text-xs" + /> + / {safeMaxColumns} +
+ updateSetting("columns", value)} + className="w-full" + /> +
+ 1์—ด + {safeMaxColumns}์—ด +
+ {isColumnsTooSmall && ( +

+ โš ๏ธ ์ปฌ๋Ÿผ ๋„ˆ๋น„๊ฐ€ ๋„ˆ๋ฌด ์ž‘์Šต๋‹ˆ๋‹ค (์ตœ์†Œ {MIN_COLUMN_WIDTH}px ๊ถŒ์žฅ) +

+ )} +
+ +
+ + updateSetting("gap", value)} + className="w-full" + /> +
+ 0px + 40px +
+
+ +
+ + updateSetting("padding", value)} + className="w-full" + /> +
+ 0px + 60px +
+
- {/* ๊ฒฉ์ž ์ •๋ณด (์ฝ๊ธฐ ์ „์šฉ) */} -
-

๐Ÿ”ง ๊ฒฉ์ž ์‹œ์Šคํ…œ

-
    -
  • โ€ข ๊ฒฉ์ž ํฌ๊ธฐ: 10px ๊ณ ์ •
  • -
  • โ€ข ์ปดํฌ๋„ŒํŠธ๋Š” 10px ๋‹จ์œ„๋กœ ๋ฐฐ์น˜๋ฉ๋‹ˆ๋‹ค
  • -
  • โ€ข ๊ฒฉ์ž ์Šค๋ƒ…์„ ๋„๋ฉด ์ž์œ ๋กญ๊ฒŒ ๋ฐฐ์น˜ ๊ฐ€๋Šฅ
  • -
+ + + {/* ๊ฒฉ์ž ์Šคํƒ€์ผ */} +
+

๊ฒฉ์ž ์Šคํƒ€์ผ

+ +
+ +
+ updateSetting("gridColor", e.target.value)} + className="h-8 w-12 rounded border p-1" + /> + updateSetting("gridColor", e.target.value)} + placeholder="#d1d5db" + className="flex-1" + /> +
+
+ +
+ + updateSetting("gridOpacity", value)} + className="w-full" + /> +
+ 10% + 100% +
+
- - + + + + {/* ๋ฏธ๋ฆฌ๋ณด๊ธฐ */} +
+

๋ฏธ๋ฆฌ๋ณด๊ธฐ

+ +
+
+ ์ปดํฌ๋„ŒํŠธ ์˜ˆ์‹œ +
+
+
+
+ + {/* ํ‘ธํ„ฐ */} +
+
๐Ÿ’ก ๊ฒฉ์ž ์„ค์ •์€ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์บ”๋ฒ„์Šค์— ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค
+ + {/* ํ•ด์ƒ๋„ ๋ฐ ๊ฒฉ์ž ์ •๋ณด */} + {screenResolution && actualGridInfo && ( + <> + +
+

๊ฒฉ์ž ์ •๋ณด

+ +
+
+ ํ•ด์ƒ๋„: + + {screenResolution.width} ร— {screenResolution.height} + +
+ +
+ ์ปฌ๋Ÿผ ๋„ˆ๋น„: + + {actualGridInfo.columnWidth.toFixed(1)}px + {isColumnsTooSmall && " (๋„ˆ๋ฌด ์ž‘์Œ)"} + +
+ +
+ ์‚ฌ์šฉ ๊ฐ€๋Šฅ ๋„ˆ๋น„: + + {(screenResolution.width - gridSettings.padding * 2).toLocaleString()}px + +
+ + {isColumnsTooSmall && ( +
+ ๐Ÿ’ก ์ปฌ๋Ÿผ์ด ๋„ˆ๋ฌด ์ž‘์Šต๋‹ˆ๋‹ค. ์ปฌ๋Ÿผ ์ˆ˜๋ฅผ ์ค„์ด๊ฑฐ๋‚˜ ๊ฐ„๊ฒฉ์„ ์ค„์—ฌ๋ณด์„ธ์š”. +
+ )} +
+
+ + )} +
+
); -} +}; + +export default GridPanel; diff --git a/frontend/components/screen/panels/TablesPanel.tsx b/frontend/components/screen/panels/TablesPanel.tsx index 46bf55f8..abeff8d6 100644 --- a/frontend/components/screen/panels/TablesPanel.tsx +++ b/frontend/components/screen/panels/TablesPanel.tsx @@ -53,16 +53,12 @@ export const TablesPanel: React.FC = ({ onDragStart, placedColumns = new Set(), }) => { - // ์ˆจ๊ธธ ๊ธฐ๋ณธ ์ปฌ๋Ÿผ ๋ชฉ๋ก (id, created_date, updated_date, writer, company_code) - const hiddenColumns = new Set(['id', 'created_date', 'updated_date', 'writer', 'company_code']); - - // ์ด๋ฏธ ๋ฐฐ์น˜๋œ ์ปฌ๋Ÿผ + ๊ธฐ๋ณธ ์ปฌ๋Ÿผ์„ ์ œ์™ธํ•œ ํ…Œ์ด๋ธ” ์ •๋ณด ์ƒ์„ฑ + // ์ด๋ฏธ ๋ฐฐ์น˜๋œ ์ปฌ๋Ÿผ์„ ์ œ์™ธํ•œ ํ…Œ์ด๋ธ” ์ •๋ณด ์ƒ์„ฑ const tablesWithAvailableColumns = tables.map((table) => ({ ...table, columns: table.columns.filter((col) => { const columnKey = `${table.tableName}.${col.columnName}`; - // ๊ธฐ๋ณธ ์ปฌ๋Ÿผ ๋˜๋Š” ์ด๋ฏธ ๋ฐฐ์น˜๋œ ์ปฌ๋Ÿผ์€ ์ œ์™ธ - return !hiddenColumns.has(col.columnName) && !placedColumns.has(columnKey); + return !placedColumns.has(columnKey); }), })); diff --git a/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx b/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx index c37f0a85..6d063640 100644 --- a/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx +++ b/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx @@ -125,23 +125,6 @@ export const UnifiedPropertiesPanel: React.FC = ({ } }, [selectedComponent?.size?.height, selectedComponent?.id]); - // ๐Ÿ”ฅ ํ›…์€ ํ•ญ์ƒ ์ตœ์ƒ๋‹จ์— (early return ์ด์ „) - // ํฌ๊ธฐ ์ž…๋ ฅ ํ•„๋“œ์šฉ ๋กœ์ปฌ ์ƒํƒœ - const [localSize, setLocalSize] = useState({ - width: selectedComponent?.size?.width || 100, - height: selectedComponent?.size?.height || 40, - }); - - // ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด ๋กœ์ปฌ ์ƒํƒœ ๋™๊ธฐํ™” - useEffect(() => { - if (selectedComponent) { - setLocalSize({ - width: selectedComponent.size?.width || 100, - height: selectedComponent.size?.height || 40, - }); - } - }, [selectedComponent?.id, selectedComponent?.size?.width, selectedComponent?.size?.height]); - // ๊ฒฉ์ž ์„ค์ • ์—…๋ฐ์ดํŠธ ํ•จ์ˆ˜ (early return ์ด์ „์— ์ •์˜) const updateGridSetting = (key: string, value: any) => { if (onGridSettingsChange && gridSettings) { @@ -152,10 +135,17 @@ export const UnifiedPropertiesPanel: React.FC = ({ } }; - // ๊ฒฉ์ž ์„ค์ • ๋ Œ๋”๋ง (10px ๊ณ ์ • ๊ฒฉ์ž) + // ๊ฒฉ์ž ์„ค์ • ๋ Œ๋”๋ง (early return ์ด์ „์— ์ •์˜) const renderGridSettings = () => { if (!gridSettings || !onGridSettingsChange) return null; + // ์ตœ๋Œ€ ์ปฌ๋Ÿผ ์ˆ˜ ๊ณ„์‚ฐ + const MIN_COLUMN_WIDTH = 30; + const maxColumns = currentResolution + ? Math.floor((currentResolution.width - gridSettings.padding * 2 + gridSettings.gap) / (MIN_COLUMN_WIDTH + gridSettings.gap)) + : 24; + const safeMaxColumns = Math.max(1, Math.min(maxColumns, 100)); // ์ตœ๋Œ€ 100๊ฐœ๋กœ ์ œํ•œ + return (
@@ -164,7 +154,7 @@ export const UnifiedPropertiesPanel: React.FC = ({
- {/* ๊ฒฉ์ž ํ‘œ์‹œ */} + {/* ํ† ๊ธ€๋“ค */}
{gridSettings.showGrid ? ( @@ -178,12 +168,11 @@ export const UnifiedPropertiesPanel: React.FC = ({
updateGridSetting("showGrid", checked)} />
- {/* ๊ฒฉ์ž ์Šค๋ƒ… */}
@@ -198,14 +187,65 @@ export const UnifiedPropertiesPanel: React.FC = ({ />
- {/* ๊ฒฉ์ž ์ •๋ณด (์ฝ๊ธฐ ์ „์šฉ) */} -
-

๐Ÿ”ง ๊ฒฉ์ž ์‹œ์Šคํ…œ

-
    -
  • โ€ข ๊ฒฉ์ž ํฌ๊ธฐ: 10px ๊ณ ์ •
  • -
  • โ€ข ์ปดํฌ๋„ŒํŠธ๋Š” 10px ๋‹จ์œ„๋กœ ๋ฐฐ์น˜๋ฉ๋‹ˆ๋‹ค
  • -
  • โ€ข ๊ฒฉ์ž ์Šค๋ƒ…์„ ๋„๋ฉด ์ž์œ ๋กญ๊ฒŒ ๋ฐฐ์น˜ ๊ฐ€๋Šฅ
  • -
+ {/* ์ปฌ๋Ÿผ ์ˆ˜ */} +
+ +
+ { + const value = parseInt(e.target.value, 10); + if (!isNaN(value) && value >= 1 && value <= safeMaxColumns) { + updateGridSetting("columns", value); + } + }} + className="h-6 px-2 py-0 text-xs" + style={{ fontSize: "12px" }} + placeholder={`1~${safeMaxColumns}`} + /> +
+

+ ์ตœ๋Œ€ {safeMaxColumns}๊ฐœ๊นŒ์ง€ ์„ค์ • ๊ฐ€๋Šฅ (์ตœ์†Œ ์ปฌ๋Ÿผ ๋„ˆ๋น„ {MIN_COLUMN_WIDTH}px) +

+
+ + {/* ๊ฐ„๊ฒฉ */} +
+ + updateGridSetting("gap", value)} + className="w-full" + /> +
+ + {/* ์—ฌ๋ฐฑ */} +
+ + updateGridSetting("padding", value)} + className="w-full" + />
@@ -415,90 +455,47 @@ export const UnifiedPropertiesPanel: React.FC = ({
)} - {/* Z-Index */} -
- - handleUpdate("position.z", parseInt(e.target.value) || 1)} - className="h-6 w-full px-2 py-0 text-xs" - style={{ fontSize: "12px" }} - /> -
- - {/* ํฌ๊ธฐ (๋„ˆ๋น„/๋†’์ด) */} + {/* Grid Columns + Z-Index (๊ฐ™์€ ํ–‰) */}
+ {(selectedComponent as any).gridColumns !== undefined && ( +
+ +
+ { + const value = parseInt(e.target.value, 10); + const maxColumns = gridSettings?.columns || 12; + if (!isNaN(value) && value >= 1 && value <= maxColumns) { + handleUpdate("gridColumns", value); + + // width๋ฅผ ํผ์„ผํŠธ๋กœ ๊ณ„์‚ฐํ•˜์—ฌ ์—…๋ฐ์ดํŠธ + const widthPercent = (value / maxColumns) * 100; + handleUpdate("style.width", `${widthPercent}%`); + } + }} + className="h-6 w-full px-2 py-0 text-xs" + style={{ fontSize: "12px" }} + /> + + /{gridSettings?.columns || 12} + +
+
+ )}
- + { - // ์ž…๋ ฅ ์ค‘์—๋Š” ๋กœ์ปฌ ์ƒํƒœ๋งŒ ์—…๋ฐ์ดํŠธ - const value = e.target.value === "" ? "" : parseInt(e.target.value); - setLocalSize((prev) => ({ ...prev, width: value as number })); - }} - onBlur={(e) => { - // ํฌ์ปค์Šค ์•„์›ƒ ์‹œ ์‹ค์ œ ์ปดํฌ๋„ŒํŠธ ์—…๋ฐ์ดํŠธ - const rawValue = e.target.value; - const parsedValue = parseInt(rawValue); - const newWidth = Math.max(10, parsedValue || 10); - - // ๋กœ์ปฌ ์ƒํƒœ๋„ ์ตœ์ข…๊ฐ’์œผ๋กœ ์—…๋ฐ์ดํŠธ - setLocalSize((prev) => ({ ...prev, width: newWidth })); - - // size.width ๊ฒฝ๋กœ๋กœ ์—…๋ฐ์ดํŠธ (๊ฒฉ์ž ์Šค๋ƒ… ์ ์šฉ๋จ) - handleUpdate("size.width", newWidth); - }} - onKeyDown={(e) => { - // Enter ํ‚ค๋กœ๋„ ์ฆ‰์‹œ ์ ์šฉ - if (e.key === "Enter") { - const newWidth = Math.max(10, parseInt((e.target as HTMLInputElement).value) || 10); - setLocalSize((prev) => ({ ...prev, width: newWidth })); - handleUpdate("size.width", newWidth); - (e.target as HTMLInputElement).blur(); - } - }} + 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" style={{ fontSize: "12px" }} - /> -
-
- - { - // ์ž…๋ ฅ ์ค‘์—๋Š” ๋กœ์ปฌ ์ƒํƒœ๋งŒ ์—…๋ฐ์ดํŠธ - const value = e.target.value === "" ? "" : parseInt(e.target.value); - setLocalSize((prev) => ({ ...prev, height: value as number })); - }} - onBlur={(e) => { - // ํฌ์ปค์Šค ์•„์›ƒ ์‹œ ์‹ค์ œ ์ปดํฌ๋„ŒํŠธ ์—…๋ฐ์ดํŠธ - const rawValue = e.target.value; - const parsedValue = parseInt(rawValue); - const newHeight = Math.max(10, parsedValue || 10); - - // ๋กœ์ปฌ ์ƒํƒœ๋„ ์ตœ์ข…๊ฐ’์œผ๋กœ ์—…๋ฐ์ดํŠธ - setLocalSize((prev) => ({ ...prev, height: newHeight })); - - // size.height ๊ฒฝ๋กœ๋กœ ์—…๋ฐ์ดํŠธ (๊ฒฉ์ž ์Šค๋ƒ… ์ ์šฉ๋จ) - handleUpdate("size.height", newHeight); - }} - onKeyDown={(e) => { - // Enter ํ‚ค๋กœ๋„ ์ฆ‰์‹œ ์ ์šฉ - if (e.key === "Enter") { - const newHeight = Math.max(10, parseInt((e.target as HTMLInputElement).value) || 10); - setLocalSize((prev) => ({ ...prev, height: newHeight })); - handleUpdate("size.height", newHeight); - (e.target as HTMLInputElement).blur(); - } - }} - className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} />
diff --git a/frontend/lib/utils/gridUtils.ts b/frontend/lib/utils/gridUtils.ts index d9f8316f..7ea3f6b4 100644 --- a/frontend/lib/utils/gridUtils.ts +++ b/frontend/lib/utils/gridUtils.ts @@ -1,77 +1,205 @@ import { Position, Size } from "@/types/screen"; import { GridSettings } from "@/types/screen-management"; -// ๐ŸŽฏ 10px ๊ณ ์ • ๊ฒฉ์ž ์‹œ์Šคํ…œ -const GRID_SIZE = 10; // ๊ณ ์ •๊ฐ’ - export interface GridInfo { - gridSize: number; // ํ•ญ์ƒ 10px + columnWidth: number; totalWidth: number; totalHeight: number; } /** - * ๊ฒฉ์ž ์ •๋ณด ๊ณ„์‚ฐ (๋‹จ์ˆœํ™”) + * ๊ฒฉ์ž ์ •๋ณด ๊ณ„์‚ฐ */ export function calculateGridInfo( containerWidth: number, containerHeight: number, - _gridSettings?: GridSettings, // ํ˜ธํ™˜์„ฑ ์œ ์ง€์šฉ (์‚ฌ์šฉ ์•ˆ ํ•จ) + gridSettings: GridSettings, ): GridInfo { + const { gap, padding } = gridSettings; + let { columns } = gridSettings; + + // ๐Ÿ”ฅ ์ตœ์†Œ ์ปฌ๋Ÿผ ๋„ˆ๋น„๋ฅผ ๋ณด์žฅํ•˜๊ธฐ ์œ„ํ•œ ์ตœ๋Œ€ ์ปฌ๋Ÿผ ์ˆ˜ ๊ณ„์‚ฐ + const MIN_COLUMN_WIDTH = 30; // ์ตœ์†Œ ์ปฌ๋Ÿผ ๋„ˆ๋น„ 30px + const availableWidth = containerWidth - padding * 2; + const maxPossibleColumns = Math.floor((availableWidth + gap) / (MIN_COLUMN_WIDTH + gap)); + + // ์„ค์ •๋œ ์ปฌ๋Ÿผ ์ˆ˜๊ฐ€ ๋„ˆ๋ฌด ๋งŽ์œผ๋ฉด ์ž๋™์œผ๋กœ ์ œํ•œ + if (columns > maxPossibleColumns) { + console.warn( + `โš ๏ธ ๊ฒฉ์ž ์ปฌ๋Ÿผ ์ˆ˜๊ฐ€ ๋„ˆ๋ฌด ๋งŽ์Šต๋‹ˆ๋‹ค. ${columns}๊ฐœ โ†’ ${maxPossibleColumns}๊ฐœ๋กœ ์ž๋™ ์กฐ์ •๋จ (์ตœ์†Œ ์ปฌ๋Ÿผ ๋„ˆ๋น„: ${MIN_COLUMN_WIDTH}px)`, + ); + columns = Math.max(1, maxPossibleColumns); + } + + // ๊ฒฉ์ž ๊ฐ„๊ฒฉ์„ ๊ณ ๋ คํ•œ ์ปฌ๋Ÿผ ๋„ˆ๋น„ ๊ณ„์‚ฐ + const totalGaps = (columns - 1) * gap; + const columnWidth = (availableWidth - totalGaps) / columns; + return { - gridSize: GRID_SIZE, + columnWidth: Math.max(columnWidth, MIN_COLUMN_WIDTH), totalWidth: containerWidth, totalHeight: containerHeight, }; } /** - * ์œ„์น˜๋ฅผ 10px ๊ฒฉ์ž์— ๋งž์ถค + * ์œ„์น˜๋ฅผ ๊ฒฉ์ž์— ๋งž์ถค */ -export function snapToGrid(position: Position, _gridInfo: GridInfo, gridSettings: GridSettings): Position { +export function snapToGrid(position: Position, gridInfo: GridInfo, gridSettings: GridSettings): Position { if (!gridSettings.snapToGrid) { return position; } + const { columnWidth } = gridInfo; + const { gap, padding } = gridSettings; + + // ๊ฒฉ์ž ์…€ ํฌ๊ธฐ (์ปฌ๋Ÿผ ๋„ˆ๋น„ + ๊ฐ„๊ฒฉ์„ ํ•˜๋‚˜์˜ ๊ฒฉ์ž ๋‹จ์œ„๋กœ ๊ณ„์‚ฐ) + const cellWidth = columnWidth + gap; + const cellHeight = 10; // ํ–‰ ๋†’์ด 10px ๋‹จ์œ„๋กœ ๊ณ ์ • + + // ํŒจ๋”ฉ์„ ์ œ์™ธํ•œ ์ƒ๋Œ€ ์œ„์น˜ + const relativeX = position.x - padding; + const relativeY = position.y - padding; + + // ๊ฒฉ์ž ๊ธฐ์ค€์œผ๋กœ ์œ„์น˜ ๊ณ„์‚ฐ (๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ๊ฒฉ์ž์ ์œผ๋กœ ์Šค๋ƒ…) + const gridX = Math.round(relativeX / cellWidth); + const gridY = Math.round(relativeY / cellHeight); + + // ์‹ค์ œ ํ”ฝ์…€ ์œ„์น˜๋กœ ๋ณ€ํ™˜ + const snappedX = Math.max(padding, padding + gridX * cellWidth); + const snappedY = Math.max(padding, padding + gridY * cellHeight); + return { - x: Math.round(position.x / GRID_SIZE) * GRID_SIZE, - y: Math.round(position.y / GRID_SIZE) * GRID_SIZE, + x: snappedX, + y: snappedY, z: position.z, }; } /** - * ํฌ๊ธฐ๋ฅผ 10px ๊ฒฉ์ž์— ๋งž์ถค + * ํฌ๊ธฐ๋ฅผ ๊ฒฉ์ž์— ๋งž์ถค */ -export function snapSizeToGrid(size: Size, _gridInfo: GridInfo, gridSettings: GridSettings): Size { +export function snapSizeToGrid(size: Size, gridInfo: GridInfo, gridSettings: GridSettings): Size { if (!gridSettings.snapToGrid) { return size; } + const { columnWidth } = gridInfo; + const { gap } = gridSettings; + + // ๊ฒฉ์ž ๋‹จ์œ„๋กœ ๋„ˆ๋น„ ๊ณ„์‚ฐ + // ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ฐจ์ง€ํ•˜๋Š” ์ปฌ๋Ÿผ ์ˆ˜๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ณ„์‚ฐ + let gridColumns = 1; + + // ํ˜„์žฌ ๋„ˆ๋น„์—์„œ ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ๊ฒฉ์ž ์ปฌ๋Ÿผ ์ˆ˜ ์ฐพ๊ธฐ + for (let cols = 1; cols <= gridSettings.columns; cols++) { + const targetWidth = cols * columnWidth + (cols - 1) * gap; + if (size.width <= targetWidth + (columnWidth + gap) / 2) { + gridColumns = cols; + break; + } + gridColumns = cols; + } + + const snappedWidth = gridColumns * columnWidth + (gridColumns - 1) * gap; + + // ๋†’์ด๋Š” 10px ๋‹จ์œ„๋กœ ์Šค๋ƒ… + const rowHeight = 10; + const snappedHeight = Math.max(10, Math.round(size.height / rowHeight) * rowHeight); + + console.log( + `๐Ÿ“ ํฌ๊ธฐ ์Šค๋ƒ…: ${size.width}px โ†’ ${snappedWidth}px (${gridColumns}์ปฌ๋Ÿผ, ์ปฌ๋Ÿผ๋„ˆ๋น„:${columnWidth}px, ๊ฐ„๊ฒฉ:${gap}px)`, + ); + return { - width: Math.max(GRID_SIZE, Math.round(size.width / GRID_SIZE) * GRID_SIZE), - height: Math.max(GRID_SIZE, Math.round(size.height / GRID_SIZE) * GRID_SIZE), + width: Math.max(columnWidth, snappedWidth), + height: snappedHeight, }; } /** - * ๊ฒฉ์ž ๊ฐ€์ด๋“œ๋ผ์ธ ์ƒ์„ฑ (10px ๊ฐ„๊ฒฉ) + * ๊ฒฉ์ž ์ปฌ๋Ÿผ ์ˆ˜๋กœ ๋„ˆ๋น„ ๊ณ„์‚ฐ + */ +export function calculateWidthFromColumns(columns: number, gridInfo: GridInfo, gridSettings: GridSettings): number { + const { columnWidth } = gridInfo; + const { gap } = gridSettings; + + return columns * columnWidth + (columns - 1) * gap; +} + +/** + * gridColumns ์†์„ฑ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์ปดํฌ๋„ŒํŠธ ํฌ๊ธฐ ์—…๋ฐ์ดํŠธ + */ +export function updateSizeFromGridColumns( + component: { gridColumns?: number; size: Size }, + gridInfo: GridInfo, + gridSettings: GridSettings, +): Size { + if (!component.gridColumns || component.gridColumns < 1) { + return component.size; + } + + const newWidth = calculateWidthFromColumns(component.gridColumns, gridInfo, gridSettings); + + return { + width: newWidth, + height: component.size.height, // ๋†’์ด๋Š” ์œ ์ง€ + }; +} + +/** + * ์ปดํฌ๋„ŒํŠธ์˜ gridColumns๋ฅผ ์ž๋™์œผ๋กœ ํฌ๊ธฐ์— ๋งž๊ฒŒ ์กฐ์ • + */ +export function adjustGridColumnsFromSize( + component: { size: Size }, + gridInfo: GridInfo, + gridSettings: GridSettings, +): number { + const columns = calculateColumnsFromWidth(component.size.width, gridInfo, gridSettings); + return Math.min(Math.max(1, columns), gridSettings.columns); // 1-12 ๋ฒ”์œ„๋กœ ์ œํ•œ +} + +/** + * ๋„ˆ๋น„์—์„œ ๊ฒฉ์ž ์ปฌ๋Ÿผ ์ˆ˜ ๊ณ„์‚ฐ + */ +export function calculateColumnsFromWidth(width: number, gridInfo: GridInfo, gridSettings: GridSettings): number { + const { columnWidth } = gridInfo; + const { gap } = gridSettings; + + return Math.max(1, Math.round((width + gap) / (columnWidth + gap))); +} + +/** + * ๊ฒฉ์ž ๊ฐ€์ด๋“œ๋ผ์ธ ์ƒ์„ฑ */ export function generateGridLines( containerWidth: number, containerHeight: number, - _gridSettings?: GridSettings, + gridSettings: GridSettings, ): { verticalLines: number[]; horizontalLines: number[]; } { + const { columns, gap, padding } = gridSettings; + const gridInfo = calculateGridInfo(containerWidth, containerHeight, gridSettings); + const { columnWidth } = gridInfo; + + // ๊ฒฉ์ž ์…€ ํฌ๊ธฐ (์Šค๋ƒ… ๋กœ์ง๊ณผ ๋™์ผํ•˜๊ฒŒ) + const cellWidth = columnWidth + gap; + const cellHeight = 10; // ํ–‰ ๋†’์ด 10px ๋‹จ์œ„๋กœ ๊ณ ์ • + + // ์„ธ๋กœ ๊ฒฉ์ž์„  const verticalLines: number[] = []; - for (let x = 0; x <= containerWidth; x += GRID_SIZE) { - verticalLines.push(x); + for (let i = 0; i <= columns; i++) { + const x = padding + i * cellWidth; + if (x <= containerWidth) { + verticalLines.push(x); + } } + // ๊ฐ€๋กœ ๊ฒฉ์ž์„  const horizontalLines: number[] = []; - for (let y = 0; y <= containerHeight; y += GRID_SIZE) { + for (let y = padding; y < containerHeight; y += cellHeight) { horizontalLines.push(y); } @@ -114,21 +242,46 @@ export function alignGroupChildrenToGrid( ): any[] { if (!gridSettings.snapToGrid || children.length === 0) return children; - return children.map((child) => { - const padding = 16; - - // 10px ๋‹จ์œ„๋กœ ์Šค๋ƒ… - const snappedX = Math.max(padding, Math.round((child.position.x - padding) / GRID_SIZE) * GRID_SIZE + padding); - const snappedY = Math.max(padding, Math.round((child.position.y - padding) / GRID_SIZE) * GRID_SIZE + padding); - - const snappedWidth = Math.max(GRID_SIZE, Math.round(child.size.width / GRID_SIZE) * GRID_SIZE); - const snappedHeight = Math.max(GRID_SIZE, Math.round(child.size.height / GRID_SIZE) * GRID_SIZE); + console.log("๐Ÿ”ง alignGroupChildrenToGrid ์‹œ์ž‘:", { + childrenCount: children.length, + groupPosition, + gridInfo, + gridSettings, + }); - return { + return children.map((child, index) => { + console.log(`๐Ÿ“ ์ž์‹ ${index + 1} ์ฒ˜๋ฆฌ ์ค‘:`, { + childId: child.id, + originalPosition: child.position, + originalSize: child.size, + }); + + const { columnWidth } = gridInfo; + const { gap } = gridSettings; + + // ๊ทธ๋ฃน ๋‚ด๋ถ€ ํŒจ๋”ฉ ๊ณ ๋ คํ•œ ๊ฒฉ์ž ์ •๋ ฌ + const padding = 16; + const effectiveX = child.position.x - padding; + const columnIndex = Math.round(effectiveX / (columnWidth + gap)); + const snappedX = padding + columnIndex * (columnWidth + gap); + + // Y ์ขŒํ‘œ๋Š” 10px ๋‹จ์œ„๋กœ ์Šค๋ƒ… + const rowHeight = 10; + const effectiveY = child.position.y - padding; + const rowIndex = Math.round(effectiveY / rowHeight); + const snappedY = padding + rowIndex * rowHeight; + + // ํฌ๊ธฐ๋Š” ์™ธ๋ถ€ ๊ฒฉ์ž์™€ ๋™์ผํ•˜๊ฒŒ ์Šค๋ƒ… (columnWidth + gap ์‚ฌ์šฉ) + const fullColumnWidth = columnWidth + gap; // ์™ธ๋ถ€ ๊ฒฉ์ž์™€ ๋™์ผํ•œ ํฌ๊ธฐ + const widthInColumns = Math.max(1, Math.round(child.size.width / fullColumnWidth)); + const snappedWidth = widthInColumns * fullColumnWidth - gap; // gap ์ œ๊ฑฐํ•˜์—ฌ ์‹ค์ œ ์ปดํฌ๋„ŒํŠธ ํฌ๊ธฐ + const snappedHeight = Math.max(10, Math.round(child.size.height / rowHeight) * rowHeight); + + const snappedChild = { ...child, position: { - x: snappedX, - y: snappedY, + x: Math.max(padding, snappedX), // ํŒจ๋”ฉ๋งŒํผ ์ตœ์†Œ ์—ฌ๋ฐฑ ํ™•๋ณด + y: Math.max(padding, snappedY), z: child.position.z || 1, }, size: { @@ -136,6 +289,26 @@ export function alignGroupChildrenToGrid( height: snappedHeight, }, }; + + console.log(`โœ… ์ž์‹ ${index + 1} ๊ฒฉ์ž ์ •๋ ฌ ์™„๋ฃŒ:`, { + childId: child.id, + calculation: { + effectiveX, + effectiveY, + columnIndex, + rowIndex, + widthInColumns, + originalX: child.position.x, + snappedX: snappedChild.position.x, + padding, + }, + snappedPosition: snappedChild.position, + snappedSize: snappedChild.size, + deltaX: snappedChild.position.x - child.position.x, + deltaY: snappedChild.position.y - child.position.y, + }); + + return snappedChild; }); } @@ -144,13 +317,19 @@ export function alignGroupChildrenToGrid( */ export function calculateOptimalGroupSize( children: Array<{ position: Position; size: Size }>, - _gridInfo?: GridInfo, - _gridSettings?: GridSettings, + gridInfo: GridInfo, + gridSettings: GridSettings, ): Size { if (children.length === 0) { - return { width: GRID_SIZE * 20, height: GRID_SIZE * 10 }; + return { width: gridInfo.columnWidth * 2, height: 10 * 4 }; } + console.log("๐Ÿ“ calculateOptimalGroupSize ์‹œ์ž‘:", { + childrenCount: children.length, + children: children.map((c) => ({ pos: c.position, size: c.size })), + }); + + // ๋ชจ๋“  ์ž์‹ ์ปดํฌ๋„ŒํŠธ๋ฅผ ํฌํ•จํ•˜๋Š” ์ตœ์†Œ ๊ฒฝ๊ณ„ ๊ณ„์‚ฐ const bounds = children.reduce( (acc, child) => ({ minX: Math.min(acc.minX, child.position.x), @@ -161,38 +340,61 @@ export function calculateOptimalGroupSize( { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }, ); + console.log("๐Ÿ“ ๊ฒฝ๊ณ„ ๊ณ„์‚ฐ:", bounds); + const contentWidth = bounds.maxX - bounds.minX; const contentHeight = bounds.maxY - bounds.minY; - const padding = 16; - return { + // ๊ทธ๋ฃน์€ ๊ฒฉ์ž ์Šค๋ƒ… ์—†์ด ์ปจํ…์ธ ์— ๋งž๋Š” ์ž์—ฐ์Šค๋Ÿฌ์šด ํฌ๊ธฐ + const padding = 16; // ๊ทธ๋ฃน ๋‚ด๋ถ€ ์—ฌ๋ฐฑ + const groupSize = { width: contentWidth + padding * 2, height: contentHeight + padding * 2, }; + + console.log("โœ… ์ž์—ฐ์Šค๋Ÿฌ์šด ๊ทธ๋ฃน ํฌ๊ธฐ:", { + contentSize: { width: contentWidth, height: contentHeight }, + withPadding: groupSize, + strategy: "๊ทธ๋ฃน์€ ๊ฒฉ์ž ์Šค๋ƒ… ์—†์ด, ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ๋งŒ ๊ฒฉ์ž์— ๋งž์ถค", + }); + + return groupSize; } /** * ๊ทธ๋ฃน ๋‚ด ์ƒ๋Œ€ ์ขŒํ‘œ๋ฅผ ๊ฒฉ์ž ๊ธฐ์ค€์œผ๋กœ ์ •๊ทœํ™” */ -export function normalizeGroupChildPositions(children: any[], _gridSettings?: GridSettings): any[] { - if (children.length === 0) return children; +export function normalizeGroupChildPositions(children: any[], gridSettings: GridSettings): any[] { + if (!gridSettings.snapToGrid || children.length === 0) return children; + console.log("๐Ÿ”„ normalizeGroupChildPositions ์‹œ์ž‘:", { + childrenCount: children.length, + originalPositions: children.map((c) => ({ id: c.id, pos: c.position })), + }); + + // ๋ชจ๋“  ์ž์‹์˜ ์ตœ์†Œ ์œ„์น˜ ์ฐพ๊ธฐ const minX = Math.min(...children.map((child) => child.position.x)); const minY = Math.min(...children.map((child) => child.position.y)); - const padding = 16; - return children.map((child) => ({ + console.log("๐Ÿ“ ์ตœ์†Œ ์œ„์น˜:", { minX, minY }); + + // ๊ทธ๋ฃน ๋‚ด์—์„œ ์‹œ์ž‘์ ์„ ํŒจ๋”ฉ๋งŒํผ ๋–จ์–ด๋œจ๋ฆผ (์ž์—ฐ์Šค๋Ÿฌ์šด ์—ฌ๋ฐฑ) + const padding = 16; + const startX = padding; + const startY = padding; + + const normalizedChildren = children.map((child) => ({ ...child, position: { - x: child.position.x - minX + padding, - y: child.position.y - minY + padding, + x: child.position.x - minX + startX, + y: child.position.y - minY + startY, z: child.position.z || 1, }, })); -} -// ๐Ÿ—‘๏ธ ์ œ๊ฑฐ๋œ ํ•จ์ˆ˜๋“ค (๋” ์ด์ƒ ํ•„์š” ์—†์Œ) -// - calculateWidthFromColumns -// - updateSizeFromGridColumns -// - adjustGridColumnsFromSize -// - calculateColumnsFromWidth + console.log("โœ… ์ •๊ทœํ™” ์™„๋ฃŒ:", { + normalizedPositions: normalizedChildren.map((c) => ({ id: c.id, pos: c.position })), + }); + + return normalizedChildren; +} diff --git a/frontend/types/screen-management.ts b/frontend/types/screen-management.ts index 75c5d4d2..d83a6354 100644 --- a/frontend/types/screen-management.ts +++ b/frontend/types/screen-management.ts @@ -561,22 +561,21 @@ export interface LayoutData { } /** - * ๊ฒฉ์ž ์„ค์ • (10px ๊ณ ์ • ๊ฒฉ์ž) + * ๊ฒฉ์ž ์„ค์ • */ export interface GridSettings { - snapToGrid: boolean; // ๊ฒฉ์ž ์Šค๋ƒ… ON/OFF - showGrid?: boolean; // ๊ฒฉ์ž ํ‘œ์‹œ ์—ฌ๋ถ€ - gridColor?: string; // ๊ฒฉ์ž ์„  ์ƒ‰์ƒ - gridOpacity?: number; // ๊ฒฉ์ž ์„  ํˆฌ๋ช…๋„ - - // ๐Ÿ—‘๏ธ ์ œ๊ฑฐ๋œ ์†์„ฑ๋“ค (10px ๊ณ ์ •์œผ๋กœ ๋” ์ด์ƒ ํ•„์š” ์—†์Œ) - // - columns: ์ž๋™ ๊ณ„์‚ฐ (ํ•ด์ƒ๋„ รท 10px) - // - gap: 10px ๊ณ ์ • - // - padding: 0px ๊ณ ์ • - // - size: 10px ๊ณ ์ • - // - enabled: showGrid๋กœ ๋Œ€์ฒด - // - color: gridColor๋กœ ๋Œ€์ฒด - // - opacity: gridOpacity๋กœ ๋Œ€์ฒด + enabled: boolean; + size: number; + color: string; + opacity: number; + snapToGrid: boolean; + // gridUtils์—์„œ ํ•„์š”ํ•œ ์†์„ฑ๋“ค ์ถ”๊ฐ€ + columns: number; + gap: number; + padding: number; + showGrid?: boolean; + gridColor?: string; + gridOpacity?: number; } /**