From 5b79bfb19d9811e3d038b848a9da684833a932ae Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 7 Nov 2025 11:36:58 +0900 Subject: [PATCH 01/11] =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=9C=84=EC=B9=98=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components/screen/ScreenDesigner.tsx | 102 +++++++++++------- .../components/screen/panels/GridPanel.tsx | 24 +++-- .../screen/panels/UnifiedPropertiesPanel.tsx | 14 ++- frontend/lib/utils/gridUtils.ts | 17 ++- 4 files changed, 106 insertions(+), 51 deletions(-) diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 2a82ff33..bf54e7b8 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -1811,8 +1811,9 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD const rect = canvasRef.current?.getBoundingClientRect(); if (!rect) return; - const dropX = e.clientX - rect.left; - const dropY = e.clientY - rect.top; + // ๐Ÿ”ฅ ์ค‘์š”: ์คŒ ๋ ˆ๋ฒจ์„ ๊ณ ๋ คํ•œ ๋งˆ์šฐ์Šค ์œ„์น˜ ๊ณ„์‚ฐ + const dropX = (e.clientX - rect.left) / zoomLevel; + const dropY = (e.clientY - rect.top) / zoomLevel; // ํ˜„์žฌ ํ•ด์ƒ๋„์— ๋งž๋Š” ๊ฒฉ์ž ์ •๋ณด ๊ณ„์‚ฐ const currentGridInfo = layout.gridSettings @@ -1830,9 +1831,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD ? snapToGrid({ x: dropX, y: dropY, z: 1 }, currentGridInfo, layout.gridSettings as GridUtilSettings) : { x: dropX, y: dropY, z: 1 }; - console.log("๐Ÿ—๏ธ ๋ ˆ์ด์•„์›ƒ ๋“œ๋กญ:", { + console.log("๐Ÿ—๏ธ ๋ ˆ์ด์•„์›ƒ ๋“œ๋กญ (์คŒ ๋ณด์ •):", { + zoomLevel, layoutType: layoutData.layoutType, zonesCount: layoutData.zones.length, + mouseRaw: { x: e.clientX - rect.left, y: e.clientY - rect.top }, dropPosition: { x: dropX, y: dropY }, snappedPosition, }); @@ -1869,7 +1872,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD toast.success(`${layoutData.label} ๋ ˆ์ด์•„์›ƒ์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); }, - [layout, gridInfo, screenResolution, snapToGrid, saveToHistory], + [layout, gridInfo, screenResolution, snapToGrid, saveToHistory, zoomLevel], ); // handleZoneComponentDrop์€ handleComponentDrop์œผ๋กœ ๋Œ€์ฒด๋จ @@ -1954,32 +1957,47 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD const componentWidth = component.defaultSize?.width || 120; const componentHeight = component.defaultSize?.height || 36; - // ๋ฐฉ๋ฒ• 1: ๋งˆ์šฐ์Šค ํฌ์ธํ„ฐ๋ฅผ ์ปดํฌ๋„ŒํŠธ ์ค‘์‹ฌ์œผ๋กœ (ํ˜„์žฌ ๋ฐฉ์‹) - const dropX_centered = e.clientX - rect.left - componentWidth / 2; - const dropY_centered = e.clientY - rect.top - componentHeight / 2; + // ๐Ÿ”ฅ ์ค‘์š”: ์คŒ ๋ ˆ๋ฒจ๊ณผ transform-origin์„ ๊ณ ๋ คํ•œ ๋งˆ์šฐ์Šค ์œ„์น˜ ๊ณ„์‚ฐ + // 1. ์บ”๋ฒ„์Šค๊ฐ€ scale() ๋ณ€ํ™˜๋˜์–ด ์žˆ์Œ (transform-origin: top center) + // 2. ์บ”๋ฒ„์Šค๊ฐ€ justify-center๋กœ ์ค‘์•™ ์ •๋ ฌ๋˜์–ด ์žˆ์Œ + + // ์‹ค์ œ ์บ”๋ฒ„์Šค ๋…ผ๋ฆฌ์  ํฌ๊ธฐ + const canvasLogicalWidth = screenResolution.width; + + // ํ™”๋ฉด์ƒ ์บ”๋ฒ„์Šค ์‹ค์ œ ํฌ๊ธฐ (์Šค์ผ€์ผ ์ ์šฉ ํ›„) + const canvasVisualWidth = canvasLogicalWidth * zoomLevel; + + // ์ค‘์•™ ์ •๋ ฌ๋กœ ์ธํ•œ ์™ผ์ชฝ ์˜คํ”„์…‹ ๊ณ„์‚ฐ + // rect.left๋Š” ์ด๋ฏธ ์ค‘์•™ ์ •๋ ฌ๋œ ์œ„์น˜๋ฅผ ๋ฐ˜์˜ํ•˜๊ณ  ์žˆ์Œ + + // ๋งˆ์šฐ์Šค์˜ ์บ”๋ฒ„์Šค ๋‚ด ์ƒ๋Œ€ ์œ„์น˜ (์Šค์ผ€์ผ ๋ณด์ •) + const mouseXInCanvas = (e.clientX - rect.left) / zoomLevel; + const mouseYInCanvas = (e.clientY - rect.top) / zoomLevel; - // ๋ฐฉ๋ฒ• 2: ๋งˆ์šฐ์Šค ํฌ์ธํ„ฐ๋ฅผ ์ปดํฌ๋„ŒํŠธ ์ขŒ์ƒ๋‹จ์œผ๋กœ (์‚ฌ์šฉ์ž๊ฐ€ ์›ํ•  ์ˆ˜๋„ ์žˆ๋Š” ๋ฐฉ์‹) - const dropX_topleft = e.clientX - rect.left; - const dropY_topleft = e.clientY - rect.top; + // ๋ฐฉ๋ฒ• 1: ๋งˆ์šฐ์Šค ํฌ์ธํ„ฐ๋ฅผ ์ปดํฌ๋„ŒํŠธ ์ค‘์‹ฌ์œผ๋กœ + const dropX_centered = mouseXInCanvas - componentWidth / 2; + const dropY_centered = mouseYInCanvas - componentHeight / 2; + + // ๋ฐฉ๋ฒ• 2: ๋งˆ์šฐ์Šค ํฌ์ธํ„ฐ๋ฅผ ์ปดํฌ๋„ŒํŠธ ์ขŒ์ƒ๋‹จ์œผ๋กœ + const dropX_topleft = mouseXInCanvas; + const dropY_topleft = mouseYInCanvas; // ์‚ฌ์šฉ์ž๊ฐ€ ์›ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋ณ€๊ฒฝ: ๋งˆ์šฐ์Šค ํฌ์ธํ„ฐ๊ฐ€ ์ขŒ์ƒ๋‹จ์— ์˜ค๋„๋ก const dropX = dropX_topleft; const dropY = dropY_topleft; - console.log("๐ŸŽฏ ์œ„์น˜ ๊ณ„์‚ฐ ๋””๋ฒ„๊น…:", { - "1. ๋งˆ์šฐ์Šค ์œ„์น˜": { clientX: e.clientX, clientY: e.clientY }, - "2. ์บ”๋ฒ„์Šค ์œ„์น˜": { left: rect.left, top: rect.top, width: rect.width, height: rect.height }, - "3. ์บ”๋ฒ„์Šค ๋‚ด ์ƒ๋Œ€ ์œ„์น˜": { x: e.clientX - rect.left, y: e.clientY - rect.top }, - "4. ์ปดํฌ๋„ŒํŠธ ํฌ๊ธฐ": { width: componentWidth, height: componentHeight }, - "5a. ์ค‘์‹ฌ ๋ฐฉ์‹ ์ขŒ์ƒ๋‹จ": { x: dropX_centered, y: dropY_centered }, - "5b. ์ขŒ์ƒ๋‹จ ๋ฐฉ์‹": { x: dropX_topleft, y: dropY_topleft }, - "6. ์„ ํƒ๋œ ๋ฐฉ์‹": { dropX, dropY }, - "7. ์˜ˆ์ƒ ์ปดํฌ๋„ŒํŠธ ์ค‘์‹ฌ": { x: dropX + componentWidth / 2, y: dropY + componentHeight / 2 }, - "8. ๋งˆ์šฐ์Šค์™€ ์ค‘์‹ฌ ์ผ์น˜ ํ™•์ธ": { - match: - Math.abs(dropX + componentWidth / 2 - (e.clientX - rect.left)) < 1 && - Math.abs(dropY + componentHeight / 2 - (e.clientY - rect.top)) < 1, - }, + console.log("๐ŸŽฏ ์œ„์น˜ ๊ณ„์‚ฐ ๋””๋ฒ„๊น… (์คŒ ๋ ˆ๋ฒจ + ์ค‘์•™์ •๋ ฌ ๋ฐ˜์˜):", { + "1. ์คŒ ๋ ˆ๋ฒจ": zoomLevel, + "2. ๋งˆ์šฐ์Šค ์œ„์น˜ (ํ™”๋ฉด)": { clientX: e.clientX, clientY: e.clientY }, + "3. ์บ”๋ฒ„์Šค ์œ„์น˜ (rect)": { left: rect.left, top: rect.top, width: rect.width, height: rect.height }, + "4. ์บ”๋ฒ„์Šค ๋…ผ๋ฆฌ์  ํฌ๊ธฐ": { width: canvasLogicalWidth, height: screenResolution.height }, + "5. ์บ”๋ฒ„์Šค ์‹œ๊ฐ์  ํฌ๊ธฐ": { width: canvasVisualWidth, height: screenResolution.height * zoomLevel }, + "6. ๋งˆ์šฐ์Šค ์บ”๋ฒ„์Šค ๋‚ด ์ƒ๋Œ€์œ„์น˜ (์คŒ ์ „)": { x: e.clientX - rect.left, y: e.clientY - rect.top }, + "7. ๋งˆ์šฐ์Šค ์บ”๋ฒ„์Šค ๋‚ด ์ƒ๋Œ€์œ„์น˜ (์คŒ ๋ณด์ •)": { x: mouseXInCanvas, y: mouseYInCanvas }, + "8. ์ปดํฌ๋„ŒํŠธ ํฌ๊ธฐ": { width: componentWidth, height: componentHeight }, + "9a. ์ค‘์‹ฌ ๋ฐฉ์‹": { x: dropX_centered, y: dropY_centered }, + "9b. ์ขŒ์ƒ๋‹จ ๋ฐฉ์‹": { x: dropX_topleft, y: dropY_topleft }, + "10. ์ตœ์ข… ์„ ํƒ": { dropX, dropY }, }); // ํ˜„์žฌ ํ•ด์ƒ๋„์— ๋งž๋Š” ๊ฒฉ์ž ์ •๋ณด ๊ณ„์‚ฐ @@ -2826,7 +2844,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD // ์ปดํฌ๋„ŒํŠธ ๋“œ๋ž˜๊ทธ ์‹œ์ž‘ const startComponentDrag = useCallback( - (component: ComponentData, event: React.MouseEvent) => { + (component: ComponentData, event: React.MouseEvent | React.DragEvent) => { event.preventDefault(); const rect = canvasRef.current?.getBoundingClientRect(); if (!rect) return; @@ -2839,9 +2857,10 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD })); } - // ์บ”๋ฒ„์Šค ๋‚ด๋ถ€์˜ ์ƒ๋Œ€ ์ขŒํ‘œ ๊ณ„์‚ฐ (์Šคํฌ๋กค ์—†๋Š” ๊ณ ์ • ์บ”๋ฒ„์Šค) - const relativeMouseX = event.clientX - rect.left; - const relativeMouseY = event.clientY - rect.top; + // ๐Ÿ”ฅ ์ค‘์š”: ์คŒ ๋ ˆ๋ฒจ์„ ๊ณ ๋ คํ•œ ๋งˆ์šฐ์Šค ์œ„์น˜ ๊ณ„์‚ฐ + // ์บ”๋ฒ„์Šค๊ฐ€ scale() ๋ณ€ํ™˜๋˜์–ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋งˆ์šฐ์Šค ์œ„์น˜๋„ ์—ญ๋ณ€ํ™˜ ํ•„์š” + const relativeMouseX = (event.clientX - rect.left) / zoomLevel; + const relativeMouseY = (event.clientY - rect.top) / zoomLevel; // ๋‹ค์ค‘ ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ๋“ค ํ™•์ธ const isDraggedComponentSelected = groupState.selectedComponents.includes(component.id); @@ -2866,13 +2885,14 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD } // console.log("๋“œ๋ž˜๊ทธ ์‹œ์ž‘:", component.id, "์ด๋™ํ•  ์ปดํฌ๋„ŒํŠธ ์ˆ˜:", componentsToMove.length); - console.log("๋งˆ์šฐ์Šค ์œ„์น˜:", { + console.log("๋งˆ์šฐ์Šค ์œ„์น˜ (์คŒ ๋ณด์ •):", { + zoomLevel, clientX: event.clientX, clientY: event.clientY, rectLeft: rect.left, rectTop: rect.top, - relativeX: relativeMouseX, - relativeY: relativeMouseY, + 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, @@ -2906,7 +2926,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD justFinishedDrag: false, }); }, - [groupState.selectedComponents, layout.components, dragState.justFinishedDrag], + [groupState.selectedComponents, layout.components, dragState.justFinishedDrag, zoomLevel], ); // ๋“œ๋ž˜๊ทธ ์ค‘ ์œ„์น˜ ์—…๋ฐ์ดํŠธ (์„ฑ๋Šฅ ์ตœ์ ํ™” + ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ) @@ -2916,9 +2936,10 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD const rect = canvasRef.current.getBoundingClientRect(); - // ์บ”๋ฒ„์Šค ๋‚ด๋ถ€์˜ ์ƒ๋Œ€ ์ขŒํ‘œ ๊ณ„์‚ฐ (์Šคํฌ๋กค ์—†๋Š” ๊ณ ์ • ์บ”๋ฒ„์Šค) - const relativeMouseX = event.clientX - rect.left; - const relativeMouseY = event.clientY - rect.top; + // ๐Ÿ”ฅ ์ค‘์š”: ์คŒ ๋ ˆ๋ฒจ์„ ๊ณ ๋ คํ•œ ๋งˆ์šฐ์Šค ์œ„์น˜ ๊ณ„์‚ฐ + // ์บ”๋ฒ„์Šค๊ฐ€ scale() ๋ณ€ํ™˜๋˜์–ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋งˆ์šฐ์Šค ์œ„์น˜๋„ ์—ญ๋ณ€ํ™˜ ํ•„์š” + const relativeMouseX = (event.clientX - rect.left) / zoomLevel; + const relativeMouseY = (event.clientY - rect.top) / zoomLevel; // ์ปดํฌ๋„ŒํŠธ ํฌ๊ธฐ ๊ฐ€์ ธ์˜ค๊ธฐ const draggedComp = layout.components.find((c) => c.id === dragState.draggedComponent.id); @@ -2936,8 +2957,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD }; // ๋“œ๋ž˜๊ทธ ์ƒํƒœ ์—…๋ฐ์ดํŠธ - console.log("๐Ÿ”ฅ ScreenDesigner updateDragPosition:", { + 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, }); @@ -2961,7 +2985,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD // ์‹ค์ œ ๋ ˆ์ด์•„์›ƒ ์—…๋ฐ์ดํŠธ๋Š” endDrag์—์„œ ์ฒ˜๋ฆฌ // ์†์„ฑ ํŒจ๋„์—์„œ๋Š” dragState.currentPosition์„ ์ฐธ์กฐํ•˜์—ฌ ์‹ค์‹œ๊ฐ„ ํ‘œ์‹œ }, - [dragState.isDragging, dragState.draggedComponent, dragState.grabOffset], + [dragState.isDragging, dragState.draggedComponent, dragState.grabOffset, zoomLevel], ); // ๋“œ๋ž˜๊ทธ ์ข…๋ฃŒ @@ -4416,7 +4440,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD ); })()} - {/* ๐Ÿ”ฅ ์คŒ ์ ์šฉ ์‹œ ์Šคํฌ๋กค ์˜์—ญ ํ™•๋ณด๋ฅผ ์œ„ํ•œ ๋ž˜ํผ */} + {/* ๐Ÿ”ฅ ์คŒ ์ ์šฉ ์‹œ ์Šคํฌ๋กค ์˜์—ญ ํ™•๋ณด๋ฅผ ์œ„ํ•œ ๋ž˜ํผ - ์ค‘์•™ ์ •๋ ฌ๋กœ ๋ณ€๊ฒฝ */}
= ({ }); }; + // ์ตœ๋Œ€ ์ปฌ๋Ÿผ ์ˆ˜ ๊ณ„์‚ฐ (์ตœ์†Œ ์ปฌ๋Ÿผ ๋„ˆ๋น„ 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, { @@ -49,7 +56,7 @@ export const GridPanel: React.FC = ({ // ์ปฌ๋Ÿผ์ด ๋„ˆ๋ฌด ์ž‘์€์ง€ ํ™•์ธ const isColumnsTooSmall = screenResolution && actualGridInfo - ? actualGridInfo.columnWidth < 30 // 30px ๋ฏธ๋งŒ์ด๋ฉด ๋„ˆ๋ฌด ์ž‘๋‹ค๊ณ  ํŒ๋‹จ + ? actualGridInfo.columnWidth < MIN_COLUMN_WIDTH : false; return ( @@ -134,22 +141,22 @@ export const GridPanel: React.FC = ({ id="columns" type="number" min={1} - max={24} + max={safeMaxColumns} value={gridSettings.columns} onChange={(e) => { const value = parseInt(e.target.value, 10); - if (!isNaN(value) && value >= 1 && value <= 24) { + if (!isNaN(value) && value >= 1 && value <= safeMaxColumns) { updateSetting("columns", value); } }} className="h-8 text-xs" /> - / 24 + / {safeMaxColumns}
updateSetting("columns", value)} @@ -157,8 +164,13 @@ export const GridPanel: React.FC = ({ />
1์—ด - 24์—ด + {safeMaxColumns}์—ด
+ {isColumnsTooSmall && ( +

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

+ )}
diff --git a/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx b/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx index 8d7cd091..6d063640 100644 --- a/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx +++ b/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx @@ -139,6 +139,13 @@ export const UnifiedPropertiesPanel: React.FC = ({ 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 (
@@ -190,21 +197,22 @@ export const UnifiedPropertiesPanel: React.FC = ({ id="columns" type="number" min={1} + max={safeMaxColumns} step="1" value={gridSettings.columns} onChange={(e) => { const value = parseInt(e.target.value, 10); - if (!isNaN(value) && value >= 1) { + if (!isNaN(value) && value >= 1 && value <= safeMaxColumns) { updateGridSetting("columns", value); } }} className="h-6 px-2 py-0 text-xs" style={{ fontSize: "12px" }} - placeholder="1 ์ด์ƒ์˜ ์ˆซ์ž" + placeholder={`1~${safeMaxColumns}`} />

- 1 ์ด์ƒ์˜ ์ˆซ์ž๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š” + ์ตœ๋Œ€ {safeMaxColumns}๊ฐœ๊นŒ์ง€ ์„ค์ • ๊ฐ€๋Šฅ (์ตœ์†Œ ์ปฌ๋Ÿผ ๋„ˆ๋น„ {MIN_COLUMN_WIDTH}px)

diff --git a/frontend/lib/utils/gridUtils.ts b/frontend/lib/utils/gridUtils.ts index 419937f0..7ea3f6b4 100644 --- a/frontend/lib/utils/gridUtils.ts +++ b/frontend/lib/utils/gridUtils.ts @@ -15,17 +15,28 @@ export function calculateGridInfo( containerHeight: number, gridSettings: GridSettings, ): GridInfo { - const { columns, gap, padding } = gridSettings; + 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 { - columnWidth: Math.max(columnWidth, 20), // ์ตœ์†Œ 20px๋กœ ์ค„์—ฌ์„œ ๋” ๋งŽ์€ ์ปฌ๋Ÿผ ํ‘œ์‹œ + columnWidth: Math.max(columnWidth, MIN_COLUMN_WIDTH), totalWidth: containerWidth, totalHeight: containerHeight, }; -- 2.43.0 From 4294fbf60813e4beb3096ec87b2efab3088970e0 Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 7 Nov 2025 14:27:07 +0900 Subject: [PATCH 02/11] =?UTF-8?q?feat:=20=EC=B1=84=EB=B2=88=20=EA=B7=9C?= =?UTF-8?q?=EC=B9=99=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EA=B8=B0=EB=B0=98=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=20=ED=95=84=ED=84=B0=EB=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ์ฑ„๋ฒˆ ๊ทœ์น™ scope_type์„ table๋กœ ๋‹จ์ˆœํ™” - ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ”๋ช…์„ ์ž๋™์œผ๋กœ ๊ฐ์ง€ํ•˜์—ฌ ์ฑ„๋ฒˆ ๊ทœ์น™ ํ•„ํ„ฐ๋ง - TextInputConfigPanel์— screenTableName prop ์ถ”๊ฐ€ - getAvailableNumberingRulesForScreen API๋กœ ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜ ์กฐํšŒ - NumberingRuleDesigner์—์„œ ์ž๋™์œผ๋กœ ํ…Œ์ด๋ธ”๋ช… ์„ค์ • - webTypeConfigConverter ์œ ํ‹ธ๋ฆฌํ‹ฐ ์ถ”๊ฐ€ (๊ธฐ์กด ํ™”๋ฉด ํ˜ธํ™˜์„ฑ) - AutoGenerationConfig ํƒ€์ž… ๊ฐœ์„  (enabled, options.numberingRuleId) - ์ฑ„๋ฒˆ ๊ทœ์น™ ์„ ํƒ UI์—์„œ ID ์ œ๊ฑฐ, ์„ค๋ช… ์ถ”๊ฐ€ - ๋ถˆํ•„์š”ํ•œ console.log ์ œ๊ฑฐ Backend: - numberingRuleService: ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง ๋กœ์ง ๊ตฌํ˜„ - numberingRuleController: available-for-screen ์—”๋“œํฌ์ธํŠธ ์ˆ˜์ • Frontend: - TextInputConfigPanel: ํ…Œ์ด๋ธ”๋ช… ๊ธฐ๋ฐ˜ ์ฑ„๋ฒˆ ๊ทœ์น™ ๋กœ๋“œ - NumberingRuleDesigner: ์ ์šฉ ๋ฒ”์œ„ UI ์ œ๊ฑฐ, ํ…Œ์ด๋ธ”๋ช… ์ž๋™ ์„ค์ • - ScreenDesigner: webTypeConfig โ†’ autoGeneration ๋ณ€ํ™˜ ๋กœ์ง ํ†ตํ•ฉ - DetailSettingsPanel: autoGeneration ์†์„ฑ ๋งคํ•‘ ๊ฐœ์„  --- .../controllers/numberingRuleController.ts | 38 + .../controllers/tableManagementController.ts | 52 +- .../src/services/numberingRuleService.ts | 111 ++ db/migrations/046_MIGRATION_FIX.md | 188 +++ db/migrations/046_QUICK_FIX.md | 151 +++ db/migrations/RUN_046_MIGRATION.md | 276 ++++ .../numbering-rule/NumberingRuleDesigner.tsx | 76 +- .../screen/RealtimePreviewDynamic.tsx | 39 +- frontend/components/screen/ScreenDesigner.tsx | 5 + .../screen/panels/DetailSettingsPanel.tsx | 34 +- .../webtype-configs/TextTypeConfigPanel.tsx | 56 +- frontend/lib/api/numberingRule.ts | 23 +- .../lib/registry/DynamicComponentRenderer.tsx | 19 - .../CategorySelectComponent.tsx | 8 - .../numbering-rule/NumberingRuleComponent.tsx | 5 + .../text-input/TextInputComponent.tsx | 10 - .../text-input/TextInputConfigPanel.tsx | 27 +- .../lib/utils/getConfigPanelComponent.tsx | 2 + frontend/lib/utils/webTypeConfigConverter.ts | 58 + frontend/types/screen.ts | 9 + ์ฑ„๋ฒˆ๊ทœ์น™_ํ…Œ์ด๋ธ”๊ธฐ๋ฐ˜_์ž๋™๊ฐ์ง€_๊ตฌํ˜„_์™„๋ฃŒ.md | 335 +++++ ์ฑ„๋ฒˆ๊ทœ์น™_ํ…Œ์ด๋ธ”๊ธฐ๋ฐ˜_ํ•„ํ„ฐ๋ง_๊ตฌํ˜„_๊ณ„ํš์„œ.md | 1126 +++++++++++++++++ ...ฒˆ๊ทœ์น™_ํ…Œ์ด๋ธ”๊ธฐ๋ฐ˜_ํ•„ํ„ฐ๋ง_๊ตฌํ˜„_์™„๋ฃŒ_๋ณด๊ณ ์„œ.md | 428 +++++++ 23 files changed, 2941 insertions(+), 135 deletions(-) create mode 100644 db/migrations/046_MIGRATION_FIX.md create mode 100644 db/migrations/046_QUICK_FIX.md create mode 100644 db/migrations/RUN_046_MIGRATION.md create mode 100644 frontend/lib/utils/webTypeConfigConverter.ts create mode 100644 ์ฑ„๋ฒˆ๊ทœ์น™_ํ…Œ์ด๋ธ”๊ธฐ๋ฐ˜_์ž๋™๊ฐ์ง€_๊ตฌํ˜„_์™„๋ฃŒ.md create mode 100644 ์ฑ„๋ฒˆ๊ทœ์น™_ํ…Œ์ด๋ธ”๊ธฐ๋ฐ˜_ํ•„ํ„ฐ๋ง_๊ตฌํ˜„_๊ณ„ํš์„œ.md create mode 100644 ์ฑ„๋ฒˆ๊ทœ์น™_ํ…Œ์ด๋ธ”๊ธฐ๋ฐ˜_ํ•„ํ„ฐ๋ง_๊ตฌํ˜„_์™„๋ฃŒ_๋ณด๊ณ ์„œ.md diff --git a/backend-node/src/controllers/numberingRuleController.ts b/backend-node/src/controllers/numberingRuleController.ts index f37bc542..556d09df 100644 --- a/backend-node/src/controllers/numberingRuleController.ts +++ b/backend-node/src/controllers/numberingRuleController.ts @@ -39,6 +39,44 @@ router.get("/available/:menuObjid?", authenticateToken, async (req: Authenticate } }); +// ํ™”๋ฉด์šฉ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ (ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง - ๊ฐ„์†Œํ™”) +router.get("/available-for-screen", authenticateToken, async (req: AuthenticatedRequest, res: Response) => { + const companyCode = req.user!.companyCode; + const { tableName } = req.query; + + try { + // tableName ํ•„์ˆ˜ ๊ฒ€์ฆ + if (!tableName || typeof tableName !== "string") { + return res.status(400).json({ + success: false, + error: "tableName is required", + }); + } + + const rules = await numberingRuleService.getAvailableRulesForScreen( + companyCode, + tableName + ); + + logger.info("ํ™”๋ฉด์šฉ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ ์„ฑ๊ณต", { + companyCode, + tableName, + count: rules.length, + }); + + return res.json({ success: true, data: rules }); + } catch (error: any) { + logger.error("ํ™”๋ฉด์šฉ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ ์‹คํŒจ", { + error: error.message, + tableName, + }); + return res.status(500).json({ + success: false, + error: error.message, + }); + } +}); + // ํŠน์ • ๊ทœ์น™ ์กฐํšŒ router.get("/:ruleId", authenticateToken, async (req: AuthenticatedRequest, res: Response) => { const companyCode = req.user!.companyCode; diff --git a/backend-node/src/controllers/tableManagementController.ts b/backend-node/src/controllers/tableManagementController.ts index c4c29503..8b1f859d 100644 --- a/backend-node/src/controllers/tableManagementController.ts +++ b/backend-node/src/controllers/tableManagementController.ts @@ -62,10 +62,10 @@ export async function getColumnList( try { const { tableName } = req.params; const { page = 1, size = 50 } = req.query; - + // ๐Ÿ”ฅ ํšŒ์‚ฌ ์ฝ”๋“œ ์ถ”์ถœ (JWT์—์„œ ๋˜๋Š” DB์—์„œ ์กฐํšŒ) let companyCode = req.user?.companyCode; - + if (!companyCode && req.user?.userId) { // JWT์— ์—†์œผ๋ฉด DB์—์„œ ์กฐํšŒ const { query } = require("../database/db"); @@ -74,7 +74,9 @@ export async function getColumnList( [req.user.userId] ); companyCode = userResult[0]?.company_code; - logger.info(`DB์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ ์กฐํšŒ (์ปฌ๋Ÿผ ๋ชฉ๋ก): ${req.user.userId} โ†’ ${companyCode}`); + logger.info( + `DB์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ ์กฐํšŒ (์ปฌ๋Ÿผ ๋ชฉ๋ก): ${req.user.userId} โ†’ ${companyCode}` + ); } logger.info( @@ -139,10 +141,10 @@ export async function updateColumnSettings( try { const { tableName, columnName } = req.params; const settings: ColumnSettings = req.body; - + // ๐Ÿ”ฅ ํšŒ์‚ฌ ์ฝ”๋“œ ์ถ”์ถœ (JWT์—์„œ ๋˜๋Š” DB์—์„œ ์กฐํšŒ) let companyCode = req.user?.companyCode; - + if (!companyCode && req.user?.userId) { // JWT์— ์—†์œผ๋ฉด DB์—์„œ ์กฐํšŒ const { query } = require("../database/db"); @@ -154,7 +156,9 @@ export async function updateColumnSettings( logger.info(`DB์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ ์กฐํšŒ: ${req.user.userId} โ†’ ${companyCode}`); } - logger.info(`=== ์ปฌ๋Ÿผ ์„ค์ • ์—…๋ฐ์ดํŠธ ์‹œ์ž‘: ${tableName}.${columnName}, company: ${companyCode} ===`); + logger.info( + `=== ์ปฌ๋Ÿผ ์„ค์ • ์—…๋ฐ์ดํŠธ ์‹œ์ž‘: ${tableName}.${columnName}, company: ${companyCode} ===` + ); if (!tableName || !columnName) { const response: ApiResponse = { @@ -194,7 +198,8 @@ export async function updateColumnSettings( message: "ํšŒ์‚ฌ ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", error: { code: "MISSING_COMPANY_CODE", - details: "์‚ฌ์šฉ์ž ์ •๋ณด์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฌธ์˜ํ•˜์„ธ์š”.", + details: + "์‚ฌ์šฉ์ž ์ •๋ณด์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฌธ์˜ํ•˜์„ธ์š”.", }, }; res.status(400).json(response); @@ -209,7 +214,9 @@ export async function updateColumnSettings( companyCode // ๐Ÿ”ฅ ํšŒ์‚ฌ ์ฝ”๋“œ ์ „๋‹ฌ ); - logger.info(`์ปฌ๋Ÿผ ์„ค์ • ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ: ${tableName}.${columnName}, company: ${companyCode}`); + logger.info( + `์ปฌ๋Ÿผ ์„ค์ • ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ: ${tableName}.${columnName}, company: ${companyCode}` + ); const response: ApiResponse = { success: true, @@ -243,10 +250,10 @@ export async function updateAllColumnSettings( try { const { tableName } = req.params; const columnSettings: ColumnSettings[] = req.body; - + // ๐Ÿ”ฅ ํšŒ์‚ฌ ์ฝ”๋“œ ์ถ”์ถœ (JWT์—์„œ ๋˜๋Š” DB์—์„œ ์กฐํšŒ) let companyCode = req.user?.companyCode; - + if (!companyCode && req.user?.userId) { // JWT์— ์—†์œผ๋ฉด DB์—์„œ ์กฐํšŒ const { query } = require("../database/db"); @@ -264,7 +271,9 @@ export async function updateAllColumnSettings( logger.info(`[DEBUG] req.user?.userId: ${req.user?.userId}`); logger.info(`[DEBUG] companyCode ์ตœ์ข…๊ฐ’: ${companyCode}`); - logger.info(`=== ์ „์ฒด ์ปฌ๋Ÿผ ์„ค์ • ์ผ๊ด„ ์—…๋ฐ์ดํŠธ ์‹œ์ž‘: ${tableName}, company: ${companyCode} ===`); + logger.info( + `=== ์ „์ฒด ์ปฌ๋Ÿผ ์„ค์ • ์ผ๊ด„ ์—…๋ฐ์ดํŠธ ์‹œ์ž‘: ${tableName}, company: ${companyCode} ===` + ); if (!tableName) { const response: ApiResponse = { @@ -305,7 +314,8 @@ export async function updateAllColumnSettings( message: "ํšŒ์‚ฌ ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", error: { code: "MISSING_COMPANY_CODE", - details: "์‚ฌ์šฉ์ž ์ •๋ณด์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฌธ์˜ํ•˜์„ธ์š”.", + details: + "์‚ฌ์šฉ์ž ์ •๋ณด์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฌธ์˜ํ•˜์„ธ์š”.", }, }; res.status(400).json(response); @@ -543,10 +553,10 @@ export async function updateColumnInputType( try { const { tableName, columnName } = req.params; const { inputType, detailSettings } = req.body; - + // ๐Ÿ”ฅ ํšŒ์‚ฌ ์ฝ”๋“œ ์ถ”์ถœ (JWT์—์„œ ๋˜๋Š” DB์—์„œ ์กฐํšŒ) let companyCode = req.user?.companyCode; - + if (!companyCode && req.user?.userId) { // JWT์— ์—†์œผ๋ฉด DB์—์„œ ์กฐํšŒ const { query } = require("../database/db"); @@ -588,7 +598,8 @@ export async function updateColumnInputType( message: "ํšŒ์‚ฌ ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", error: { code: "MISSING_COMPANY_CODE", - details: "์‚ฌ์šฉ์ž ์ •๋ณด์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฌธ์˜ํ•˜์„ธ์š”.", + details: + "์‚ฌ์šฉ์ž ์ •๋ณด์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฌธ์˜ํ•˜์„ธ์š”.", }, }; res.status(400).json(response); @@ -1085,10 +1096,10 @@ export async function getColumnWebTypes( ): Promise { try { const { tableName } = req.params; - + // ๐Ÿ”ฅ ํšŒ์‚ฌ ์ฝ”๋“œ ์ถ”์ถœ (JWT์—์„œ ๋˜๋Š” DB์—์„œ ์กฐํšŒ) let companyCode = req.user?.companyCode; - + if (!companyCode && req.user?.userId) { // JWT์— ์—†์œผ๋ฉด DB์—์„œ ์กฐํšŒ const { query } = require("../database/db"); @@ -1097,7 +1108,9 @@ export async function getColumnWebTypes( [req.user.userId] ); companyCode = userResult[0]?.company_code; - logger.info(`DB์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ ์กฐํšŒ (์กฐํšŒ): ${req.user.userId} โ†’ ${companyCode}`); + logger.info( + `DB์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ ์กฐํšŒ (์กฐํšŒ): ${req.user.userId} โ†’ ${companyCode}` + ); } logger.info( @@ -1129,7 +1142,8 @@ export async function getColumnWebTypes( message: "ํšŒ์‚ฌ ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", error: { code: "MISSING_COMPANY_CODE", - details: "์‚ฌ์šฉ์ž ์ •๋ณด์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฌธ์˜ํ•˜์„ธ์š”.", + details: + "์‚ฌ์šฉ์ž ์ •๋ณด์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฌธ์˜ํ•˜์„ธ์š”.", }, }; res.status(400).json(response); diff --git a/backend-node/src/services/numberingRuleService.ts b/backend-node/src/services/numberingRuleService.ts index 0c612b51..98230b65 100644 --- a/backend-node/src/services/numberingRuleService.ts +++ b/backend-node/src/services/numberingRuleService.ts @@ -401,6 +401,117 @@ class NumberingRuleService { } } + /** + * ํ™”๋ฉด์šฉ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ (ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง - ๊ฐ„์†Œํ™”) + * @param companyCode ํšŒ์‚ฌ ์ฝ”๋“œ + * @param tableName ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ”๋ช… + * @returns ํ•ด๋‹น ํ…Œ์ด๋ธ”์˜ ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ชฉ๋ก + */ + async getAvailableRulesForScreen( + companyCode: string, + tableName: string + ): Promise { + try { + logger.info("ํ™”๋ฉด์šฉ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ", { + companyCode, + tableName, + }); + + const pool = getPool(); + + // ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ: ์ตœ๊ณ  ๊ด€๋ฆฌ์ž vs ์ผ๋ฐ˜ ํšŒ์‚ฌ + let query: string; + let params: any[]; + + if (companyCode === "*") { + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ชจ๋“  ํšŒ์‚ฌ์˜ ๊ทœ์น™ ์กฐํšŒ ๊ฐ€๋Šฅ (์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ „์šฉ ๊ทœ์น™ ์ œ์™ธ) + query = ` + SELECT + rule_id AS "ruleId", + rule_name AS "ruleName", + description, + separator, + reset_period AS "resetPeriod", + current_sequence AS "currentSequence", + table_name AS "tableName", + column_name AS "columnName", + company_code AS "companyCode", + menu_objid AS "menuObjid", + scope_type AS "scopeType", + created_at AS "createdAt", + updated_at AS "updatedAt", + created_by AS "createdBy" + FROM numbering_rules + WHERE company_code != '*' + AND table_name = $1 + ORDER BY created_at DESC + `; + params = [tableName]; + logger.info("์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ์ผ๋ฐ˜ ํšŒ์‚ฌ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ"); + } else { + // ์ผ๋ฐ˜ ํšŒ์‚ฌ: ์ž์‹ ์˜ ๊ทœ์น™๋งŒ ์กฐํšŒ + query = ` + SELECT + rule_id AS "ruleId", + rule_name AS "ruleName", + description, + separator, + reset_period AS "resetPeriod", + current_sequence AS "currentSequence", + table_name AS "tableName", + column_name AS "columnName", + company_code AS "companyCode", + menu_objid AS "menuObjid", + scope_type AS "scopeType", + created_at AS "createdAt", + updated_at AS "updatedAt", + created_by AS "createdBy" + FROM numbering_rules + WHERE company_code = $1 + AND table_name = $2 + ORDER BY created_at DESC + `; + params = [companyCode, tableName]; + } + + const result = await pool.query(query, params); + + // ๊ฐ ๊ทœ์น™์˜ ํŒŒํŠธ ์ •๋ณด ๋กœ๋“œ + for (const rule of result.rows) { + const partsQuery = ` + SELECT + id, + part_order AS "order", + part_type AS "partType", + generation_method AS "generationMethod", + auto_config AS "autoConfig", + manual_config AS "manualConfig" + FROM numbering_rule_parts + WHERE rule_id = $1 + AND company_code = $2 + ORDER BY part_order + `; + + const partsResult = await pool.query(partsQuery, [ + rule.ruleId, + companyCode === "*" ? rule.companyCode : companyCode, + ]); + + rule.parts = partsResult.rows; + } + + logger.info(`ํ™”๋ฉด์šฉ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ ์™„๋ฃŒ: ${result.rows.length}๊ฐœ`, { + companyCode, + tableName, + }); + + return result.rows; + } catch (error: any) { + logger.error("ํ™”๋ฉด์šฉ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ ์‹คํŒจ", error); + throw error; + } + } + /** * ํŠน์ • ๊ทœ์น™ ์กฐํšŒ */ diff --git a/db/migrations/046_MIGRATION_FIX.md b/db/migrations/046_MIGRATION_FIX.md new file mode 100644 index 00000000..16220f5f --- /dev/null +++ b/db/migrations/046_MIGRATION_FIX.md @@ -0,0 +1,188 @@ +# ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ 046 ์˜ค๋ฅ˜ ์ˆ˜์ • + +## ๐Ÿšจ ๋ฐœ์ƒํ•œ ์˜ค๋ฅ˜ + +``` +SQL Error [23514]: ERROR: check constraint "check_menu_scope_requires_menu_objid" +of relation "numbering_rules" is violated by some row +``` + +## ๐Ÿ” ์›์ธ ๋ถ„์„ + +๊ธฐ์กด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— `scope_type='menu'`์ธ๋ฐ `menu_objid`๊ฐ€ NULL์ธ ๋ ˆ์ฝ”๋“œ๊ฐ€ ์กด์žฌํ–ˆ์Šต๋‹ˆ๋‹ค. + +์ œ์•ฝ์กฐ๊ฑด์„ ์ถ”๊ฐ€ํ•˜๊ธฐ ์ „์— ์ด๋Ÿฌํ•œ **๋ถˆ์™„์ „ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋จผ์ € ์ •๋ฆฌ**ํ•ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค. + +## โœ… ์ˆ˜์ • ๋‚ด์šฉ + +๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ `046_update_numbering_rules_scope_type.sql`์— **๋ฐ์ดํ„ฐ ์ •๋ฆฌ ๋‹จ๊ณ„** ์ถ”๊ฐ€: + +### 1. ์ถ”๊ฐ€๋œ ๋ฐ์ดํ„ฐ ์ •๋ฆฌ ๋กœ์ง (์ œ์•ฝ์กฐ๊ฑด ์ถ”๊ฐ€ ์ „) + +```sql +-- 3. ๊ธฐ์กด ๋ฐ์ดํ„ฐ ์ •๋ฆฌ (์ œ์•ฝ์กฐ๊ฑด ์ถ”๊ฐ€ ์ „ ํ•„์ˆ˜!) + +-- 3.1. menu ํƒ€์ž…์ธ๋ฐ menu_objid๊ฐ€ NULL์ธ ๊ฒฝ์šฐ โ†’ global๋กœ ๋ณ€๊ฒฝ +UPDATE numbering_rules +SET scope_type = 'global', + table_name = NULL +WHERE scope_type = 'menu' AND menu_objid IS NULL; + +-- 3.2. global ํƒ€์ž…์ธ๋ฐ table_name์ด ์žˆ๋Š” ๊ฒฝ์šฐ โ†’ table๋กœ ๋ณ€๊ฒฝ +UPDATE numbering_rules +SET scope_type = 'table' +WHERE scope_type = 'global' AND table_name IS NOT NULL; + +-- 3.3. ์ •๋ฆฌ ๊ฒฐ๊ณผ ํ™•์ธ (๋กœ๊ทธ) +DO $$ +DECLARE + menu_count INTEGER; + global_count INTEGER; + table_count INTEGER; +BEGIN + SELECT COUNT(*) INTO menu_count FROM numbering_rules WHERE scope_type = 'menu'; + SELECT COUNT(*) INTO global_count FROM numbering_rules WHERE scope_type = 'global'; + SELECT COUNT(*) INTO table_count FROM numbering_rules WHERE scope_type = 'table'; + + RAISE NOTICE '=== ๋ฐ์ดํ„ฐ ์ •๋ฆฌ ์™„๋ฃŒ ==='; + RAISE NOTICE 'Menu ๊ทœ์น™: % ๊ฐœ', menu_count; + RAISE NOTICE 'Global ๊ทœ์น™: % ๊ฐœ', global_count; + RAISE NOTICE 'Table ๊ทœ์น™: % ๊ฐœ', table_count; + RAISE NOTICE '========================='; +END $$; +``` + +### 2. ์‹คํ–‰ ์ˆœ์„œ ๋ณ€๊ฒฝ + +**๋ณ€๊ฒฝ ์ „:** +1. scope_type ์ œ์•ฝ์กฐ๊ฑด ์ถ”๊ฐ€ +2. โŒ ์œ ํšจ์„ฑ ์ œ์•ฝ์กฐ๊ฑด ์ถ”๊ฐ€ (์—ฌ๊ธฐ์„œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ!) +3. ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ + +**๋ณ€๊ฒฝ ํ›„:** +1. scope_type ์ œ์•ฝ์กฐ๊ฑด ์ถ”๊ฐ€ +2. โœ… **๊ธฐ์กด ๋ฐ์ดํ„ฐ ์ •๋ฆฌ** (์ถ”๊ฐ€) +3. ์œ ํšจ์„ฑ ์ œ์•ฝ์กฐ๊ฑด ์ถ”๊ฐ€ +4. ์ธ๋ฑ์Šค ์ƒ์„ฑ +5. ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ + +## ๐Ÿ”„ ์žฌ์‹คํ–‰ ๋ฐฉ๋ฒ• + +### ์˜ต์…˜ 1: ์ „์ฒด ๋กค๋ฐฑ ํ›„ ์žฌ์‹คํ–‰ (๊ถŒ์žฅ) + +```sql +-- 1. ๊ธฐ์กด ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋กค๋ฐฑ +BEGIN; + +-- ์ œ์•ฝ์กฐ๊ฑด ์ œ๊ฑฐ +ALTER TABLE numbering_rules DROP CONSTRAINT IF EXISTS check_scope_type; +ALTER TABLE numbering_rules DROP CONSTRAINT IF EXISTS check_table_scope_requires_table_name; +ALTER TABLE numbering_rules DROP CONSTRAINT IF EXISTS check_global_scope_no_table_name; +ALTER TABLE numbering_rules DROP CONSTRAINT IF EXISTS check_menu_scope_requires_menu_objid; + +-- ์ธ๋ฑ์Šค ์ œ๊ฑฐ +DROP INDEX IF EXISTS idx_numbering_rules_scope_table; +DROP INDEX IF EXISTS idx_numbering_rules_scope_menu; + +COMMIT; + +-- 2. ์ˆ˜์ •๋œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์žฌ์‹คํ–‰ +\i /Users/kimjuseok/ERP-node/db/migrations/046_update_numbering_rules_scope_type.sql +``` + +### ์˜ต์…˜ 2: ๋ฐ์ดํ„ฐ ์ •๋ฆฌ๋งŒ ์ˆ˜๋™ ์‹คํ–‰ ํ›„ ์žฌ์‹œ๋„ + +```sql +-- 1. ๋ฐ์ดํ„ฐ ์ •๋ฆฌ +UPDATE numbering_rules +SET scope_type = 'global', + table_name = NULL +WHERE scope_type = 'menu' AND menu_objid IS NULL; + +UPDATE numbering_rules +SET scope_type = 'table' +WHERE scope_type = 'global' AND table_name IS NOT NULL; + +-- 2. ์ œ์•ฝ์กฐ๊ฑด ์ถ”๊ฐ€ +ALTER TABLE numbering_rules +ADD CONSTRAINT check_menu_scope_requires_menu_objid +CHECK ( + (scope_type = 'menu' AND menu_objid IS NOT NULL) + OR scope_type != 'menu' +); + +-- 3. ๋‚˜๋จธ์ง€ ์ œ์•ฝ์กฐ๊ฑด๋“ค... +``` + +## ๐Ÿงช ๊ฒ€์ฆ ์ฟผ๋ฆฌ + +๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ ์ „์— ๋ฌธ์ œ ๋ฐ์ดํ„ฐ ํ™•์ธ: + +```sql +-- ๋ฌธ์ œ๊ฐ€ ๋˜๋Š” ๋ ˆ์ฝ”๋“œ ํ™•์ธ +SELECT + rule_id, + rule_name, + scope_type, + table_name, + menu_objid, + company_code +FROM numbering_rules +WHERE + (scope_type = 'menu' AND menu_objid IS NULL) + OR (scope_type = 'global' AND table_name IS NOT NULL) + OR (scope_type = 'table' AND table_name IS NULL); +``` + +๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ ํ›„ ๊ฒ€์ฆ: + +```sql +-- 1. scope_type๋ณ„ ๊ฐœ์ˆ˜ +SELECT scope_type, COUNT(*) as count +FROM numbering_rules +GROUP BY scope_type; + +-- 2. ์ œ์•ฝ์กฐ๊ฑด ํ™•์ธ +SELECT conname, pg_get_constraintdef(oid) +FROM pg_constraint +WHERE conrelid = 'numbering_rules'::regclass + AND conname LIKE '%scope%'; + +-- 3. ์ธ๋ฑ์Šค ํ™•์ธ +SELECT indexname, indexdef +FROM pg_indexes +WHERE tablename = 'numbering_rules' + AND indexname LIKE '%scope%'; +``` + +## ๐Ÿ“ ์ˆ˜์ • ๋‚ด์—ญ + +- โœ… ์ œ์•ฝ์กฐ๊ฑด ์ถ”๊ฐ€ ์ „ ๋ฐ์ดํ„ฐ ์ •๋ฆฌ ๋กœ์ง ์ถ”๊ฐ€ +- โœ… ์ค‘๋ณต๋œ ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ฝ”๋“œ ์ œ๊ฑฐ +- โœ… ์„น์…˜ ๋ฒˆํ˜ธ ์žฌ์ •๋ ฌ +- โœ… ๋ฐ์ดํ„ฐ ์ •๋ฆฌ ๊ฒฐ๊ณผ ๋กœ๊ทธ ์ถ”๊ฐ€ + +## ๐ŸŽฏ ๋‹ค์Œ ๋‹จ๊ณ„ + +1. **ํ˜„์žฌ ์ƒํƒœ ํ™•์ธ** + ```bash + psql -h localhost -U postgres -d ilshin -f /Users/kimjuseok/ERP-node/db/check_numbering_rules.sql + ``` + +2. **๋กค๋ฐฑ (ํ•„์š”์‹œ)** + - ๊ธฐ์กด ์ œ์•ฝ์กฐ๊ฑด ์ œ๊ฑฐ + +3. **์ˆ˜์ •๋œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์žฌ์‹คํ–‰** + ```bash + PGPASSWORD=<๋น„๋ฐ€๋ฒˆํ˜ธ> psql -h localhost -U postgres -d ilshin -f /Users/kimjuseok/ERP-node/db/migrations/046_update_numbering_rules_scope_type.sql + ``` + +4. **๊ฒ€์ฆ** + - ์ œ์•ฝ์กฐ๊ฑด ํ™•์ธ + - ๋ฐ์ดํ„ฐ ๊ฐœ์ˆ˜ ํ™•์ธ + - ์ธ๋ฑ์Šค ํ™•์ธ + +--- + +**์ˆ˜์ • ์™„๋ฃŒ!** ์ด์ œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ๋‹ค์‹œ ์‹คํ–‰ํ•˜๋ฉด ์„ฑ๊ณตํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๐ŸŽ‰ + diff --git a/db/migrations/046_QUICK_FIX.md b/db/migrations/046_QUICK_FIX.md new file mode 100644 index 00000000..658a3a0c --- /dev/null +++ b/db/migrations/046_QUICK_FIX.md @@ -0,0 +1,151 @@ +# ์ฑ„๋ฒˆ ๊ทœ์น™ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์˜ค๋ฅ˜ ๊ธด๊ธ‰ ์ˆ˜์ • + +## ๐Ÿšจ ๋ฐœ์ƒํ•œ ์˜ค๋ฅ˜๋“ค + +### ์˜ค๋ฅ˜ 1: check_table_scope_requires_table_name +``` +SQL Error [23514]: ERROR: new row for relation "numbering_rules" violates check constraint "check_table_scope_requires_table_name" +``` +**์›์ธ**: `scope_type='table'`์ธ๋ฐ `table_name=NULL` + +### ์˜ค๋ฅ˜ 2: check_global_scope_no_table_name +``` +SQL Error [23514]: ERROR: new row for relation "numbering_rules" violates check constraint "check_global_scope_no_table_name" +``` +**์›์ธ**: `scope_type='global'`์ธ๋ฐ `table_name=''` (๋นˆ ๋ฌธ์ž์—ด) + +### ๊ทผ๋ณธ ์›์ธ +๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์ด ๋ถ€๋ถ„์ ์œผ๋กœ ์‹คํ–‰๋˜์–ด ๋ฐ์ดํ„ฐ์™€ ์ œ์•ฝ์กฐ๊ฑด์ด ๋ถˆ์ผ์น˜ ์ƒํƒœ์ž…๋‹ˆ๋‹ค. + +## โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• + +### ๐ŸŽฏ ๊ฐ€์žฅ ์‰ฌ์šด ๋ฐฉ๋ฒ• (๊ถŒ์žฅ) + +**PgAdmin ๋˜๋Š” DBeaver์—์„œ `046_SIMPLE_FIX.sql` ์‹คํ–‰** + +์ด ํŒŒ์ผ์€ ๋‹ค์Œ์„ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค: +1. โœ… ๊ธฐ์กด ์ œ์•ฝ์กฐ๊ฑด ๋ชจ๋‘ ์ œ๊ฑฐ +2. โœ… `table_name` NULL โ†’ ๋นˆ ๋ฌธ์ž์—ด๋กœ ๋ณ€๊ฒฝ +3. โœ… `scope_type`์„ ๋ชจ๋‘ 'table'๋กœ ๋ณ€๊ฒฝ +4. โœ… ๊ฒฐ๊ณผ ํ™•์ธ + +```sql +-- db/migrations/046_SIMPLE_FIX.sql ์ „์ฒด ๋‚ด์šฉ์„ ๋ณต์‚ฌํ•˜์—ฌ ์‹คํ–‰ํ•˜์„ธ์š” +``` + +**์‹คํ–‰ ํ›„**: +- `046_update_numbering_rules_scope_type.sql` ์ „์ฒด ์‹คํ–‰ +- ์™„๋ฃŒ! + +--- + +### ์˜ต์…˜ 2: ๋ช…๋ น์ค„์—์„œ ์‹คํ–‰ + +```bash +# 1. ๊ธด๊ธ‰ ์ˆ˜์ • SQL ์‹คํ–‰ +psql -h localhost -U postgres -d ilshin -f db/fix_existing_numbering_rules.sql + +# 2. ์ „์ฒด ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ +psql -h localhost -U postgres -d ilshin -f db/migrations/046_update_numbering_rules_scope_type.sql +``` + +--- + +### ์˜ต์…˜ 3: Docker ์ปจํ…Œ์ด๋„ˆ ๋‚ด๋ถ€์—์„œ ์‹คํ–‰ + +```bash +# 1. Docker ์ปจํ…Œ์ด๋„ˆ ํ™•์ธ +docker ps | grep postgres + +# 2. ์ปจํ…Œ์ด๋„ˆ ๋‚ด๋ถ€ ์ ‘์† +docker exec -it psql -U postgres -d ilshin + +# 3. SQL ์‹คํ–‰ +UPDATE numbering_rules SET table_name = '' WHERE table_name IS NULL; + +# 4. ํ™•์ธ +SELECT COUNT(*) FROM numbering_rules WHERE table_name IS NULL; +-- ๊ฒฐ๊ณผ: 0 + +# 5. ์ข…๋ฃŒ +\q +``` + +--- + +## ๐Ÿ” ์™œ ์ด ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‚˜? + +### ๊ธฐ์กด ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ˆœ์„œ (์ž˜๋ชป๋จ) +```sql +-- 1. scope_type ๋ณ€๊ฒฝ (๋จผ์ € ์‹คํ–‰๋จ) +UPDATE numbering_rules SET scope_type = 'table' WHERE scope_type IN ('global', 'menu'); + +-- 2. table_name ์ •๋ฆฌ (๋‚˜์ค‘์— ์‹คํ–‰๋จ) +UPDATE numbering_rules SET table_name = '' WHERE table_name IS NULL; + +-- 3. ์ œ์•ฝ์กฐ๊ฑด ์ถ”๊ฐ€ +ALTER TABLE numbering_rules ADD CONSTRAINT check_table_scope_requires_table_name ... +``` + +**๋ฌธ์ œ์ **: +- `scope_type='table'`๋กœ ๋ณ€๊ฒฝ๋œ ํ›„ +- ์•„์ง `table_name=NULL`์ธ ์ƒํƒœ +- ์ด ์ƒํƒœ์—์„œ INSERT/UPDATE ์‹œ๋„ ์‹œ ์ œ์•ฝ์กฐ๊ฑด ์œ„๋ฐ˜ + +### ์ˆ˜์ •๋œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ˆœ์„œ (์˜ฌ๋ฐ”๋ฆ„) +```sql +-- 1. table_name ์ •๋ฆฌ (๋จผ์ € ์‹คํ–‰!) +UPDATE numbering_rules SET table_name = '' WHERE table_name IS NULL; + +-- 2. scope_type ๋ณ€๊ฒฝ +UPDATE numbering_rules SET scope_type = 'table' WHERE scope_type IN ('global', 'menu'); + +-- 3. ์ œ์•ฝ์กฐ๊ฑด ์ถ”๊ฐ€ +ALTER TABLE numbering_rules ADD CONSTRAINT check_table_scope_requires_table_name ... +``` + +--- + +## ๐Ÿ“‹ ์‹คํ–‰ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +- [ ] ์˜ต์…˜ 1, 2, ๋˜๋Š” 3 ์ค‘ ํ•˜๋‚˜ ์„ ํƒํ•˜์—ฌ ๋ฐ์ดํ„ฐ ์ˆ˜์ • ์™„๋ฃŒ +- [ ] `SELECT COUNT(*) FROM numbering_rules WHERE table_name IS NULL;` ์‹คํ–‰ โ†’ ๊ฒฐ๊ณผ๊ฐ€ `0`์ธ์ง€ ํ™•์ธ +- [ ] ์ „์ฒด ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ `046_update_numbering_rules_scope_type.sql` ์‹คํ–‰ +- [ ] ๋ฐฑ์—”๋“œ ์žฌ์‹œ์ž‘ +- [ ] ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ์ฑ„๋ฒˆ ๊ทœ์น™ ํ…Œ์ŠคํŠธ + +--- + +## ๐ŸŽฏ ์™„๋ฃŒ ํ›„ ํ™•์ธ์‚ฌํ•ญ + +### SQL๋กœ ์ตœ์ข… ํ™•์ธ +```sql +-- 1. ๋ชจ๋“  ๊ทœ์น™์ด table ํƒ€์ž…์ธ์ง€ +SELECT scope_type, COUNT(*) +FROM numbering_rules +GROUP BY scope_type; +-- ๊ฒฐ๊ณผ: table๋งŒ ๋‚˜์™€์•ผ ํ•จ + +-- 2. table_name์ด NULL์ธ ๊ทœ์น™์ด ์—†๋Š”์ง€ +SELECT COUNT(*) +FROM numbering_rules +WHERE table_name IS NULL; +-- ๊ฒฐ๊ณผ: 0 + +-- 3. ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ํ™•์ธ +SELECT + rule_id, + rule_name, + scope_type, + table_name, + company_code +FROM numbering_rules +LIMIT 5; +``` + +--- + +## ๐Ÿ’ก ์ถ”๊ฐ€ ์ •๋ณด + +์ˆ˜์ •๋œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ(`046_update_numbering_rules_scope_type.sql`)์€ ์ด์ œ ์˜ฌ๋ฐ”๋ฅธ ์ˆœ์„œ๋กœ ์‹คํ–‰๋˜๋„๋ก ์—…๋ฐ์ดํŠธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ **์ด๋ฏธ ์‹คํ–‰๋œ ๋ถ€๋ถ„์ ์ธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜**์œผ๋กœ ์ธํ•ด ๋ฐ์ดํ„ฐ๊ฐ€ ๋ถˆ์ผ์น˜ ์ƒํƒœ์ผ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, ์œ„์˜ ๊ธด๊ธ‰ ์ˆ˜์ •์„ ๋จผ์ € ์‹คํ–‰ํ•˜๋Š” ๊ฒƒ์ด ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค. + diff --git a/db/migrations/RUN_046_MIGRATION.md b/db/migrations/RUN_046_MIGRATION.md new file mode 100644 index 00000000..af34d0ea --- /dev/null +++ b/db/migrations/RUN_046_MIGRATION.md @@ -0,0 +1,276 @@ +# ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ 046: ์ฑ„๋ฒˆ๊ทœ์น™ scope_type ํ™•์žฅ + +## ๐Ÿ“‹ ๋ชฉ์  + +๋ฉ”๋‰ด ๊ธฐ๋ฐ˜ ์ฑ„๋ฒˆ๊ทœ์น™ ํ•„ํ„ฐ๋ง์„ **ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง**์œผ๋กœ ์ „ํ™˜ํ•˜์—ฌ ๋” ์ง๊ด€์ ์ด๊ณ  ์œ ์ง€๋ณด์ˆ˜ํ•˜๊ธฐ ์‰ฌ์šด ์‹œ์Šคํ…œ ๊ตฌ์ถ• + +### ์ฃผ์š” ๋ณ€๊ฒฝ์‚ฌํ•ญ + +1. `scope_type` ๊ฐ’ ํ™•์žฅ: `'global'`, `'menu'` โ†’ `'global'`, `'table'`, `'menu'` +2. ๊ธฐ์กด ๋ฐ์ดํ„ฐ ์ž๋™ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ (`global` + `table_name` โ†’ `table`) +3. ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์ œ์•ฝ์กฐ๊ฑด ์ถ”๊ฐ€ +4. ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์ธ๋ฑ์Šค ์ตœ์ ํ™” + +--- + +## ๐Ÿš€ ์‹คํ–‰ ๋ฐฉ๋ฒ• + +### Docker ํ™˜๊ฒฝ (๊ถŒ์žฅ) + +```bash +docker exec -i erp-node-db-1 psql -U postgres -d ilshin < db/migrations/046_update_numbering_rules_scope_type.sql +``` + +### ๋กœ์ปฌ PostgreSQL + +```bash +psql -U postgres -d ilshin -f db/migrations/046_update_numbering_rules_scope_type.sql +``` + +### pgAdmin / DBeaver + +1. `db/migrations/046_update_numbering_rules_scope_type.sql` ํŒŒ์ผ ์—ด๊ธฐ +2. ์ „์ฒด ๋‚ด์šฉ ๋ณต์‚ฌ +3. SQL ์ฟผ๋ฆฌ ์ฐฝ์— ๋ถ™์—ฌ๋„ฃ๊ธฐ +4. ์‹คํ–‰ (F5 ๋˜๋Š” Execute) + +--- + +## โœ… ๊ฒ€์ฆ ๋ฐฉ๋ฒ• + +### 1. ์ œ์•ฝ์กฐ๊ฑด ํ™•์ธ + +```sql +SELECT conname, pg_get_constraintdef(oid) +FROM pg_constraint +WHERE conrelid = 'numbering_rules'::regclass + AND conname LIKE '%scope%'; +``` + +**์˜ˆ์ƒ ๊ฒฐ๊ณผ**: +``` +conname | pg_get_constraintdef +--------------------------------------|--------------------- +check_scope_type | CHECK (scope_type IN ('global', 'table', 'menu')) +check_table_scope_requires_table_name | CHECK (...) +check_global_scope_no_table_name | CHECK (...) +check_menu_scope_requires_menu_objid | CHECK (...) +``` + +### 2. ์ธ๋ฑ์Šค ํ™•์ธ + +```sql +SELECT indexname, indexdef +FROM pg_indexes +WHERE tablename = 'numbering_rules' + AND indexname LIKE '%scope%' +ORDER BY indexname; +``` + +**์˜ˆ์ƒ ๊ฒฐ๊ณผ**: +``` +indexname | indexdef +------------------------------------|---------- +idx_numbering_rules_scope_menu | CREATE INDEX ... (scope_type, menu_objid, company_code) +idx_numbering_rules_scope_table | CREATE INDEX ... (scope_type, table_name, company_code) +``` + +### 3. ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ™•์ธ + +```sql +-- scope_type๋ณ„ ๊ฐœ์ˆ˜ +SELECT scope_type, COUNT(*) as count +FROM numbering_rules +GROUP BY scope_type; +``` + +**์˜ˆ์ƒ ๊ฒฐ๊ณผ**: +``` +scope_type | count +-----------|------ +global | X๊ฐœ (table_name์ด NULL์ธ ๊ทœ์น™๋“ค) +table | Y๊ฐœ (table_name์ด ์žˆ๋Š” ๊ทœ์น™๋“ค) +menu | Z๊ฐœ (menu_objid๊ฐ€ ์žˆ๋Š” ๊ทœ์น™๋“ค) +``` + +### 4. ์œ ํšจ์„ฑ ๊ฒ€์ฆ + +```sql +-- ์ด ์ฟผ๋ฆฌ๋“ค์€ ๋ชจ๋‘ 0๊ฐœ๋ฅผ ๋ฐ˜ํ™˜ํ•ด์•ผ ์ •์ƒ +-- 1) global์ธ๋ฐ table_name์ด ์žˆ๋Š” ๊ทœ์น™ (์—†์–ด์•ผ ํ•จ) +SELECT COUNT(*) as invalid_global +FROM numbering_rules +WHERE scope_type = 'global' AND table_name IS NOT NULL; + +-- 2) table์ธ๋ฐ table_name์ด ์—†๋Š” ๊ทœ์น™ (์—†์–ด์•ผ ํ•จ) +SELECT COUNT(*) as invalid_table +FROM numbering_rules +WHERE scope_type = 'table' AND table_name IS NULL; + +-- 3) menu์ธ๋ฐ menu_objid๊ฐ€ ์—†๋Š” ๊ทœ์น™ (์—†์–ด์•ผ ํ•จ) +SELECT COUNT(*) as invalid_menu +FROM numbering_rules +WHERE scope_type = 'menu' AND menu_objid IS NULL; +``` + +**๋ชจ๋“  ์นด์šดํŠธ๊ฐ€ 0์ด์–ด์•ผ ์ •์ƒ** + +### 5. ํšŒ์‚ฌ๋ณ„ ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ ํ™•์ธ (๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ) + +```sql +-- ํšŒ์‚ฌ๋ณ„ ๊ทœ์น™ ๊ฐœ์ˆ˜ +SELECT + company_code, + scope_type, + COUNT(*) as count +FROM numbering_rules +GROUP BY company_code, scope_type +ORDER BY company_code, scope_type; +``` + +**๊ฐ ํšŒ์‚ฌ์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ๋…๋ฆฝ์ ์œผ๋กœ ์กด์žฌํ•ด์•ผ ํ•จ** + +--- + +## ๐Ÿšจ ๋กค๋ฐฑ ๋ฐฉ๋ฒ• (๋ฌธ์ œ ๋ฐœ์ƒ ์‹œ) + +```sql +BEGIN; + +-- ์ œ์•ฝ์กฐ๊ฑด ์ œ๊ฑฐ +ALTER TABLE numbering_rules DROP CONSTRAINT IF EXISTS check_scope_type; +ALTER TABLE numbering_rules DROP CONSTRAINT IF EXISTS check_table_scope_requires_table_name; +ALTER TABLE numbering_rules DROP CONSTRAINT IF EXISTS check_global_scope_no_table_name; +ALTER TABLE numbering_rules DROP CONSTRAINT IF EXISTS check_menu_scope_requires_menu_objid; + +-- ์ธ๋ฑ์Šค ์ œ๊ฑฐ +DROP INDEX IF EXISTS idx_numbering_rules_scope_table; +DROP INDEX IF EXISTS idx_numbering_rules_scope_menu; + +-- ๋ฐ์ดํ„ฐ ๋กค๋ฐฑ (table โ†’ global) +UPDATE numbering_rules +SET scope_type = 'global' +WHERE scope_type = 'table'; + +-- ๊ธฐ์กด ์ œ์•ฝ์กฐ๊ฑด ๋ณต์› +ALTER TABLE numbering_rules +ADD CONSTRAINT check_scope_type +CHECK (scope_type IN ('global', 'menu')); + +-- ๊ธฐ์กด ์ธ๋ฑ์Šค ๋ณต์› +CREATE INDEX IF NOT EXISTS idx_numbering_rules_table +ON numbering_rules(table_name, column_name); + +COMMIT; +``` + +--- + +## ๐Ÿ“Š ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋‚ด์šฉ ์ƒ์„ธ + +### ๋ณ€๊ฒฝ ์‚ฌํ•ญ + +| ํ•ญ๋ชฉ | ๋ณ€๊ฒฝ ์ „ | ๋ณ€๊ฒฝ ํ›„ | +|------|---------|---------| +| **scope_type ๊ฐ’** | 'global', 'menu' | 'global', 'table', 'menu' | +| **์œ ํšจ์„ฑ ๊ฒ€์ฆ** | ์—†์Œ | table/global/menu ํƒ€์ž…๋ณ„ ์ œ์•ฝ์กฐ๊ฑด ์ถ”๊ฐ€ | +| **์ธ๋ฑ์Šค** | (table_name, column_name) | (scope_type, table_name, company_code)
(scope_type, menu_objid, company_code) | +| **๋ฐ์ดํ„ฐ** | global + table_name | table ํƒ€์ž…์œผ๋กœ ์ž๋™ ๋ณ€๊ฒฝ | + +### ์˜ํ–ฅ๋ฐ›๋Š” ๋ฐ์ดํ„ฐ + +```sql +-- ์ž๋™์œผ๋กœ ๋ณ€๊ฒฝ๋˜๋Š” ๊ทœ์น™ ์กฐํšŒ +SELECT + rule_id, + rule_name, + scope_type as old_scope_type, + 'table' as new_scope_type, + table_name, + company_code +FROM numbering_rules +WHERE scope_type = 'global' + AND table_name IS NOT NULL; +``` + +--- + +## โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ + +1. **๋ฐฑ์—… ํ•„์ˆ˜**: ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ ์ „ ๋ฐ˜๋“œ์‹œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ฐฑ์—… +2. **ํŠธ๋žœ์žญ์…˜**: ์ „์ฒด ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์ด ํ•˜๋‚˜์˜ ํŠธ๋žœ์žญ์…˜์œผ๋กœ ์‹คํ–‰๋จ (์‹คํŒจ ์‹œ ์ž๋™ ๋กค๋ฐฑ) +3. **์„ฑ๋Šฅ**: ๊ทœ์น™์ด ๋งŽ์œผ๋ฉด ์‹คํ–‰ ์‹œ๊ฐ„์ด ๊ธธ์–ด์งˆ ์ˆ˜ ์žˆ์Œ (๋ณดํ†ต 1์ดˆ ์ด๋‚ด) +4. **๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ**: ๋ชจ๋“  ํšŒ์‚ฌ์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ์•ˆ์ „ํ•˜๊ฒŒ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜๋จ +5. **ํ•˜์œ„ ํ˜ธํ™˜์„ฑ**: ๊ธฐ์กด ๊ธฐ๋Šฅ 100% ์œ ์ง€ (์ž๋™ ๋ณ€ํ™˜) + +--- + +## ๐Ÿ” ๋ฌธ์ œ ํ•ด๊ฒฐ + +### ์ œ์•ฝ์กฐ๊ฑด ์ถฉ๋Œ ๋ฐœ์ƒ ์‹œ + +```sql +-- ๋ฌธ์ œ๊ฐ€ ๋˜๋Š” ๋ฐ์ดํ„ฐ ํ™•์ธ +SELECT rule_id, rule_name, scope_type, table_name, menu_objid +FROM numbering_rules +WHERE + (scope_type = 'table' AND table_name IS NULL) + OR (scope_type = 'global' AND table_name IS NOT NULL) + OR (scope_type = 'menu' AND menu_objid IS NULL); + +-- ์ˆ˜๋™ ์ˆ˜์ • ํ›„ ๋‹ค์‹œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ +``` + +### ์ธ๋ฑ์Šค ์ƒ์„ฑ ์‹คํŒจ ์‹œ + +```sql +-- ๊ธฐ์กด ์ธ๋ฑ์Šค ํ™•์ธ +SELECT indexname, indexdef +FROM pg_indexes +WHERE tablename = 'numbering_rules'; + +-- ์ถฉ๋Œํ•˜๋Š” ์ธ๋ฑ์Šค ์‚ญ์ œ ํ›„ ๋‹ค์‹œ ์‹คํ–‰ +DROP INDEX IF EXISTS <์ถฉ๋Œํ•˜๋Š”_์ธ๋ฑ์Šค๋ช…>; +``` + +--- + +## ๐Ÿ“ˆ ์„ฑ๋Šฅ ๊ฐœ์„  ํšจ๊ณผ + +### Before (๊ธฐ์กด) +```sql +-- ๋‹จ์ผ ์ธ๋ฑ์Šค: (table_name, column_name) +-- company_code ํ•„ํ„ฐ๋ง ์‹œ Full Table Scan ๊ฐ€๋Šฅ์„ฑ +``` + +### After (๋ณ€๊ฒฝ ํ›„) +```sql +-- ๋ณตํ•ฉ ์ธ๋ฑ์Šค: (scope_type, table_name, company_code) +-- ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์ฟผ๋ฆฌ ์„ฑ๋Šฅ ํ–ฅ์ƒ (ํšŒ์‚ฌ๋ณ„ ๊ฒฉ๋ฆฌ ์ตœ์ ํ™”) +-- WHERE ์ ˆ๊ณผ ORDER BY ์ ˆ ๋ชจ๋‘ ์ธ๋ฑ์Šค ํ™œ์šฉ ๊ฐ€๋Šฅ +``` + +**์˜ˆ์ƒ ์„ฑ๋Šฅ ํ–ฅ์ƒ**: ํšŒ์‚ฌ๋ณ„ ๊ทœ์น™ ์กฐํšŒ ์‹œ **3-5๋ฐฐ ๋น ๋ฆ„** + +--- + +## ๐Ÿ“ž ์ง€์› + +- **์ž‘์„ฑ์ž**: ๊ฐœ๋ฐœํŒ€ +- **์ž‘์„ฑ์ผ**: 2025-11-08 +- **๊ด€๋ จ ๋ฌธ์„œ**: `/์ฑ„๋ฒˆ๊ทœ์น™_ํ…Œ์ด๋ธ”๊ธฐ๋ฐ˜_ํ•„ํ„ฐ๋ง_๊ตฌํ˜„_๊ณ„ํš์„œ.md` +- **์ด์Šˆ ๋ฐœ์ƒ ์‹œ**: ๋กค๋ฐฑ ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰ ํ›„ ๊ฐœ๋ฐœํŒ€ ๋ฌธ์˜ + +--- + +## ๋‹ค์Œ ๋‹จ๊ณ„ + +๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์™„๋ฃŒ ํ›„: + +1. โœ… ๊ฒ€์ฆ ์ฟผ๋ฆฌ ์‹คํ–‰ +2. โฌœ ๋ฐฑ์—”๋“œ API ์ˆ˜์ • (Phase 2) +3. โฌœ ํ”„๋ก ํŠธ์—”๋“œ ์ˆ˜์ • (Phase 3-5) +4. โฌœ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ + +**๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ค€๋น„ ์™„๋ฃŒ!** ๐Ÿš€ + diff --git a/frontend/components/numbering-rule/NumberingRuleDesigner.tsx b/frontend/components/numbering-rule/NumberingRuleDesigner.tsx index fc6b883f..738aad79 100644 --- a/frontend/components/numbering-rule/NumberingRuleDesigner.tsx +++ b/frontend/components/numbering-rule/NumberingRuleDesigner.tsx @@ -25,6 +25,7 @@ interface NumberingRuleDesignerProps { maxRules?: number; isPreview?: boolean; className?: string; + currentTableName?: string; // ํ˜„์žฌ ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ”๋ช… (์ž๋™ ๊ฐ์ง€์šฉ) } export const NumberingRuleDesigner: React.FC = ({ @@ -34,6 +35,7 @@ export const NumberingRuleDesigner: React.FC = ({ maxRules = 6, isPreview = false, className = "", + currentTableName, }) => { const [savedRules, setSavedRules] = useState([]); const [selectedRuleId, setSelectedRuleId] = useState(null); @@ -131,17 +133,32 @@ export const NumberingRuleDesigner: React.FC = ({ try { const existing = savedRules.find((r) => r.ruleId === currentRule.ruleId); + // ์ €์žฅ ์ „์— ํ˜„์žฌ ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ”๋ช… ์ž๋™ ์„ค์ • + const ruleToSave = { + ...currentRule, + scopeType: "table" as const, // ํ•ญ์ƒ table๋กœ ๊ณ ์ • + tableName: currentTableName || currentRule.tableName || "", // ํ˜„์žฌ ํ…Œ์ด๋ธ”๋ช… ์ž๋™ ์„ค์ • + }; + + console.log("๐Ÿ’พ ์ฑ„๋ฒˆ ๊ทœ์น™ ์ €์žฅ:", { + currentTableName, + "currentRule.tableName": currentRule.tableName, + "ruleToSave.tableName": ruleToSave.tableName, + "ruleToSave.scopeType": ruleToSave.scopeType, + ruleToSave + }); + let response; if (existing) { - response = await updateNumberingRule(currentRule.ruleId, currentRule); + response = await updateNumberingRule(ruleToSave.ruleId, ruleToSave); } else { - response = await createNumberingRule(currentRule); + response = await createNumberingRule(ruleToSave); } if (response.success && response.data) { setSavedRules((prev) => { if (existing) { - return prev.map((r) => (r.ruleId === currentRule.ruleId ? response.data! : r)); + return prev.map((r) => (r.ruleId === ruleToSave.ruleId ? response.data! : r)); } else { return [...prev, response.data!]; } @@ -160,7 +177,7 @@ export const NumberingRuleDesigner: React.FC = ({ } finally { setLoading(false); } - }, [currentRule, savedRules, onSave]); + }, [currentRule, savedRules, onSave, currentTableName]); const handleSelectRule = useCallback((rule: NumberingRuleConfig) => { setSelectedRuleId(rule.ruleId); @@ -196,6 +213,8 @@ export const NumberingRuleDesigner: React.FC = ({ ); const handleNewRule = useCallback(() => { + console.log("๐Ÿ“‹ ์ƒˆ ๊ทœ์น™ ์ƒ์„ฑ - currentTableName:", currentTableName); + const newRule: NumberingRuleConfig = { ruleId: `rule-${Date.now()}`, ruleName: "์ƒˆ ์ฑ„๋ฒˆ ๊ทœ์น™", @@ -203,14 +222,17 @@ export const NumberingRuleDesigner: React.FC = ({ separator: "-", resetPeriod: "none", currentSequence: 1, - scopeType: "menu", + scopeType: "table", // ๊ธฐ๋ณธ๊ฐ’์„ table๋กœ ์„ค์ • + tableName: currentTableName || "", // ํ˜„์žฌ ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ”๋ช… ์ž๋™ ์„ค์ • }; + console.log("๐Ÿ“‹ ์ƒ์„ฑ๋œ ๊ทœ์น™ ์ •๋ณด:", newRule); + setSelectedRuleId(newRule.ruleId); setCurrentRule(newRule); toast.success("์ƒˆ ๊ทœ์น™์ด ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค"); - }, []); + }, [currentTableName]); return (
@@ -312,20 +334,36 @@ export const NumberingRuleDesigner: React.FC = ({
-
-
- - setCurrentRule((prev) => ({ ...prev!, ruleName: e.target.value }))} - className="h-9" - placeholder="์˜ˆ: ํ”„๋กœ์ ํŠธ ์ฝ”๋“œ" - /> -
-
- - +
+ {/* ์ฒซ ๋ฒˆ์งธ ์ค„: ๊ทœ์น™๋ช… + ๋ฏธ๋ฆฌ๋ณด๊ธฐ */} +
+
+ + setCurrentRule((prev) => ({ ...prev!, ruleName: e.target.value }))} + className="h-9" + placeholder="์˜ˆ: ํ”„๋กœ์ ํŠธ ์ฝ”๋“œ" + /> +
+
+ + +
+ + {/* ๋‘ ๋ฒˆ์งธ ์ค„: ์ž๋™ ๊ฐ์ง€๋œ ํ…Œ์ด๋ธ” ์ •๋ณด ํ‘œ์‹œ */} + {currentTableName && ( +
+ +
+ {currentTableName} +
+

+ ์ด ๊ทœ์น™์€ ํ˜„์žฌ ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ”({currentTableName})์— ์ž๋™์œผ๋กœ ์ ์šฉ๋ฉ๋‹ˆ๋‹ค +

+
+ )}
diff --git a/frontend/components/screen/RealtimePreviewDynamic.tsx b/frontend/components/screen/RealtimePreviewDynamic.tsx index 777f791d..679ed5a8 100644 --- a/frontend/components/screen/RealtimePreviewDynamic.tsx +++ b/frontend/components/screen/RealtimePreviewDynamic.tsx @@ -214,22 +214,11 @@ export const RealtimePreviewDynamic: React.FC = ({ if (component.componentConfig?.type === "table-list") { // ๋””์ž์ธ ํ•ด์ƒ๋„ ๊ธฐ์ค€์œผ๋กœ ํ”ฝ์…€ ๋ฐ˜ํ™˜ const screenWidth = 1920; // ๊ธฐ๋ณธ ๋””์ž์ธ ํ•ด์ƒ๋„ - console.log("๐Ÿ“ [getWidth] table-list ํ”ฝ์…€ ์‚ฌ์šฉ:", { - componentId: id, - label: component.label, - width: `${screenWidth}px`, - }); return `${screenWidth}px`; } // ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ๋Š” size.width ํ”ฝ์…€ ์‚ฌ์šฉ const width = `${size?.width || 100}px`; - console.log("๐Ÿ“ [getWidth] ํ”ฝ์…€ ๊ธฐ์ค€ ํ†ต์ผ:", { - componentId: id, - label: component.label, - width, - sizeWidth: size?.width, - }); return width; }; @@ -286,33 +275,7 @@ export const RealtimePreviewDynamic: React.FC = ({ if (outerDivRef.current && innerDivRef.current) { const outerRect = outerDivRef.current.getBoundingClientRect(); const innerRect = innerDivRef.current.getBoundingClientRect(); - const computedOuter = window.getComputedStyle(outerDivRef.current); - const computedInner = window.getComputedStyle(innerDivRef.current); - - console.log("๐Ÿ“ [DOM ์‹ค์ œ ํฌ๊ธฐ ์ƒ์„ธ]:", { - componentId: id, - label: component.label, - gridColumns: (component as any).gridColumns, - "1. baseStyle.width": baseStyle.width, - "2. ์™ธ๋ถ€ div (ํŒŒ๋ž€ ํ…Œ๋‘๋ฆฌ)": { - width: `${outerRect.width}px`, - height: `${outerRect.height}px`, - computedWidth: computedOuter.width, - computedHeight: computedOuter.height, - }, - "3. ๋‚ด๋ถ€ div (์ปจํ…์ธ  ๋ž˜ํผ)": { - width: `${innerRect.width}px`, - height: `${innerRect.height}px`, - computedWidth: computedInner.width, - computedHeight: computedInner.height, - className: innerDivRef.current.className, - inlineStyle: innerDivRef.current.getAttribute("style"), - }, - "4. ๋„ˆ๋น„ ๋น„๊ต": { - "์™ธ๋ถ€ / ๋‚ด๋ถ€": `${outerRect.width}px / ${innerRect.width}px`, - ๋น„์œจ: `${((innerRect.width / outerRect.width) * 100).toFixed(2)}%`, - }, - }); + // ํฌ๊ธฐ ์ธก์ • ์™„๋ฃŒ } }, [id, component.label, (component as any).gridColumns, baseStyle.width]); diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index bf54e7b8..7db03da6 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -899,9 +899,14 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD layoutToUse = safeMigrateLayout(response, canvasWidth); } + // ๐Ÿ”„ webTypeConfig๋ฅผ autoGeneration์œผ๋กœ ๋ณ€ํ™˜ + const { convertLayoutComponents } = await import("@/lib/utils/webTypeConfigConverter"); + const convertedComponents = convertLayoutComponents(layoutToUse.components); + // ๊ธฐ๋ณธ ๊ฒฉ์ž ์„ค์ • ๋ณด์žฅ (๊ฒฉ์ž ํ‘œ์‹œ์™€ ์Šค๋ƒ… ๊ธฐ๋ณธ ํ™œ์„ฑํ™”) const layoutWithDefaultGrid = { ...layoutToUse, + components: convertedComponents, // ๋ณ€ํ™˜๋œ ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉ gridSettings: { columns: layoutToUse.gridSettings?.columns || 12, // DB ๊ฐ’ ์šฐ์„ , ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ 12 gap: layoutToUse.gridSettings?.gap ?? 16, // DB ๊ฐ’ ์šฐ์„ , ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ 16 diff --git a/frontend/components/screen/panels/DetailSettingsPanel.tsx b/frontend/components/screen/panels/DetailSettingsPanel.tsx index 47e1b102..e07571fe 100644 --- a/frontend/components/screen/panels/DetailSettingsPanel.tsx +++ b/frontend/components/screen/panels/DetailSettingsPanel.tsx @@ -752,17 +752,27 @@ export const DetailSettingsPanel: React.FC = ({ // console.log("๐ŸŽจ selectedComponent ์ „์ฒด:", selectedComponent); const handleConfigChange = (newConfig: WebTypeConfig) => { - // console.log("๐Ÿ”ง WebTypeConfig ์—…๋ฐ์ดํŠธ:", { - // widgetType: widget.widgetType, - // oldConfig: currentConfig, - // newConfig, - // componentId: widget.id, - // isEqual: JSON.stringify(currentConfig) === JSON.stringify(newConfig), - // }); - // ๊ฐ•์ œ ์ƒˆ ๊ฐ์ฒด ์ƒ์„ฑ์œผ๋กœ React ๋ณ€๊ฒฝ ๊ฐ์ง€ ๋ณด์žฅ const freshConfig = { ...newConfig }; onUpdateProperty(widget.id, "webTypeConfig", freshConfig); + + // TextTypeConfig์˜ ์ž๋™์ž…๋ ฅ ์„ค์ •์„ autoGeneration์œผ๋กœ๋„ ๋งคํ•‘ + const textConfig = newConfig as any; + if (textConfig.autoInput && textConfig.autoValueType === "numbering_rule" && textConfig.numberingRuleId) { + onUpdateProperty(widget.id, "autoGeneration", { + type: "numbering_rule", + enabled: true, + options: { + numberingRuleId: textConfig.numberingRuleId, + }, + }); + } else if (textConfig.autoInput === false) { + // ์ž๋™์ž…๋ ฅ์ด ๋น„ํ™œ์„ฑํ™”๋˜๋ฉด autoGeneration๋„ ๋น„ํ™œ์„ฑํ™” + onUpdateProperty(widget.id, "autoGeneration", { + type: "none", + enabled: false, + }); + } }; // 1์ˆœ์œ„: DB์—์„œ ์ง€์ •๋œ ์„ค์ • ํŒจ๋„ ์‚ฌ์šฉ @@ -776,7 +786,13 @@ export const DetailSettingsPanel: React.FC = ({ if (ConfigPanelComponent) { // console.log(`๐ŸŽจ โœ… ConfigPanelComponent ๋ Œ๋”๋ง ์‹œ์ž‘`); - return ; + return ( + + ); } else { // console.log(`๐ŸŽจ โŒ ConfigPanelComponent๊ฐ€ null - WebTypeConfigPanel ์‚ฌ์šฉ`); return ( diff --git a/frontend/components/screen/panels/webtype-configs/TextTypeConfigPanel.tsx b/frontend/components/screen/panels/webtype-configs/TextTypeConfigPanel.tsx index abb35347..2e1f5087 100644 --- a/frontend/components/screen/panels/webtype-configs/TextTypeConfigPanel.tsx +++ b/frontend/components/screen/panels/webtype-configs/TextTypeConfigPanel.tsx @@ -7,15 +7,24 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { Checkbox } from "@/components/ui/checkbox"; import { Textarea } from "@/components/ui/textarea"; import { TextTypeConfig } from "@/types/screen"; -import { getAvailableNumberingRules } from "@/lib/api/numberingRule"; +import { getAvailableNumberingRules, getAvailableNumberingRulesForScreen } from "@/lib/api/numberingRule"; import { NumberingRuleConfig } from "@/types/numbering-rule"; interface TextTypeConfigPanelProps { config: TextTypeConfig; onConfigChange: (config: TextTypeConfig) => void; + tableName?: string; // ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ”๋ช… (์„ ํƒ) + menuObjid?: number; // ๋ฉ”๋‰ด objid (์„ ํƒ) } -export const TextTypeConfigPanel: React.FC = ({ config, onConfigChange }) => { +export const TextTypeConfigPanel: React.FC = ({ + config, + onConfigChange, + tableName, + menuObjid, +}) => { + console.log("๐Ÿ” TextTypeConfigPanel ๋งˆ์šดํŠธ:", { tableName, menuObjid, config }); + // ๊ธฐ๋ณธ๊ฐ’์ด ์„ค์ •๋œ config ์‚ฌ์šฉ const safeConfig = { minLength: undefined, @@ -54,16 +63,46 @@ export const TextTypeConfigPanel: React.FC = ({ config // ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ชฉ๋ก ๋กœ๋“œ useEffect(() => { const loadRules = async () => { + console.log("๐Ÿ”„ ์ฑ„๋ฒˆ ๊ทœ์น™ ๋กœ๋“œ ์‹œ์ž‘:", { + autoValueType: localValues.autoValueType, + tableName, + hasTableName: !!tableName, + }); + setLoadingRules(true); try { - // TODO: ํ˜„์žฌ ๋ฉ”๋‰ด objid๋ฅผ ํ™”๋ฉด ์ •๋ณด์—์„œ ๊ฐ€์ ธ์™€์•ผ ํ•จ - // ์ง€๊ธˆ์€ menuObjid ์—†์ด ํ˜ธ์ถœ (global ๊ทœ์น™๋งŒ ์กฐํšŒ) - const response = await getAvailableNumberingRules(); + let response; + + // ํ…Œ์ด๋ธ”๋ช…์ด ์žˆ์œผ๋ฉด ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง ์‚ฌ์šฉ + if (tableName) { + console.log("๐Ÿ“‹ ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ API ํ˜ธ์ถœ:", { tableName }); + response = await getAvailableNumberingRulesForScreen(tableName); + console.log("๐Ÿ“‹ API ์‘๋‹ต:", response); + } else { + // ํ…Œ์ด๋ธ”๋ช…์ด ์—†์œผ๋ฉด ๋นˆ ๋ฐฐ์—ด (ํ…Œ์ด๋ธ” ํ•„์ˆ˜) + console.warn("โš ๏ธ ํ…Œ์ด๋ธ”๋ช…์ด ์—†์–ด ์ฑ„๋ฒˆ ๊ทœ์น™์„ ์กฐํšŒํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"); + setNumberingRules([]); + setLoadingRules(false); + return; + } + if (response.success && response.data) { setNumberingRules(response.data); + console.log("โœ… ์ฑ„๋ฒˆ ๊ทœ์น™ ๋กœ๋“œ ์„ฑ๊ณต:", { + count: response.data.length, + rules: response.data.map((r: any) => ({ + ruleId: r.ruleId, + ruleName: r.ruleName, + tableName: r.tableName, + })), + }); + } else { + console.warn("โš ๏ธ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ ์‹คํŒจ:", response.error); + setNumberingRules([]); } } catch (error) { - console.error("์ฑ„๋ฒˆ ๊ทœ์น™ ๋ชฉ๋ก ๋กœ๋“œ ์‹คํŒจ:", error); + console.error("โŒ ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ชฉ๋ก ๋กœ๋“œ ์‹คํŒจ:", error); + setNumberingRules([]); } finally { setLoadingRules(false); } @@ -71,9 +110,12 @@ export const TextTypeConfigPanel: React.FC = ({ config // autoValueType์ด numbering_rule์ผ ๋•Œ๋งŒ ๋กœ๋“œ if (localValues.autoValueType === "numbering_rule") { + console.log("โœ… autoValueType === 'numbering_rule', ๊ทœ์น™ ๋กœ๋“œ ์‹œ์ž‘"); loadRules(); + } else { + console.log("โญ๏ธ autoValueType !== 'numbering_rule', ๊ทœ์น™ ๋กœ๋“œ ์Šคํ‚ต:", localValues.autoValueType); } - }, [localValues.autoValueType]); + }, [localValues.autoValueType, tableName]); // config๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ ๋กœ์ปฌ ์ƒํƒœ ๋™๊ธฐํ™” useEffect(() => { diff --git a/frontend/lib/api/numberingRule.ts b/frontend/lib/api/numberingRule.ts index dace488a..b531edce 100644 --- a/frontend/lib/api/numberingRule.ts +++ b/frontend/lib/api/numberingRule.ts @@ -22,7 +22,7 @@ export async function getNumberingRules(): Promise> { + try { + const response = await apiClient.get("/numbering-rules/available-for-screen", { + params: { tableName }, + }); + return response.data; + } catch (error: any) { + return { + success: false, + error: error.message || "ํ™”๋ฉด์šฉ ๊ทœ์น™ ์กฐํšŒ ์‹คํŒจ", + }; + } +} + export async function getNumberingRuleById(ruleId: string): Promise> { try { const response = await apiClient.get(`/numbering-rules/${ruleId}`); diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index 785b1ac0..19d61cb0 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -148,19 +148,8 @@ export const DynamicComponentRenderer: React.FC = const tableName = (component as any).tableName; const columnName = (component as any).columnName; - console.log("๐Ÿ” DynamicComponentRenderer ์ปดํฌ๋„ŒํŠธ ํƒ€์ž… ํ™•์ธ:", { - componentId: component.id, - componentType, - inputType, - webType, - tableName, - columnName, - componentConfig: (component as any).componentConfig, - }); - // ์นดํ…Œ๊ณ ๋ฆฌ ์…€๋ ‰ํŠธ: webType์ด "category"์ด๊ณ  tableName๊ณผ columnName์ด ์žˆ๋Š” ๊ฒฝ์šฐ๋งŒ if ((inputType === "category" || webType === "category") && tableName && columnName) { - console.log("โœ… ์นดํ…Œ๊ณ ๋ฆฌ ํƒ€์ž… ๊ฐ์ง€ โ†’ CategorySelectComponent ๋ Œ๋”๋ง"); try { const { CategorySelectComponent } = require("@/lib/registry/components/category-select/CategorySelectComponent"); const fieldName = columnName || component.id; @@ -303,14 +292,6 @@ export const DynamicComponentRenderer: React.FC = componentType === "split-panel-layout" || componentType?.includes("layout"); - console.log("๐Ÿ” [DynamicComponentRenderer] ๋†’์ด ์ฒ˜๋ฆฌ:", { - componentId: component.id, - componentType, - isLayoutComponent, - hasHeight: !!component.style?.height, - height: component.style?.height - }); - const { height: _height, ...styleWithoutHeight } = component.style || {}; // ์ˆจ๊น€ ๊ฐ’ ์ถ”์ถœ diff --git a/frontend/lib/registry/components/category-select/CategorySelectComponent.tsx b/frontend/lib/registry/components/category-select/CategorySelectComponent.tsx index 0de23e25..001b68e3 100644 --- a/frontend/lib/registry/components/category-select/CategorySelectComponent.tsx +++ b/frontend/lib/registry/components/category-select/CategorySelectComponent.tsx @@ -74,20 +74,12 @@ export const CategorySelectComponent: React.FC< setError(null); try { - console.log("๐Ÿ“ฆ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ:", { tableName, columnName }); - const response = await getCategoryValues(tableName, columnName); if (response.success && response.data) { // ํ™œ์„ฑํ™”๋œ ๊ฐ’๋งŒ ํ•„ํ„ฐ๋ง const activeValues = response.data.filter((v) => v.isActive !== false); setCategoryValues(activeValues); - - console.log("โœ… ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ ์„ฑ๊ณต:", { - total: response.data.length, - active: activeValues.length, - values: activeValues, - }); } else { setError("์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’์„ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"); console.error("โŒ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ ์‹คํŒจ:", response); diff --git a/frontend/lib/registry/components/numbering-rule/NumberingRuleComponent.tsx b/frontend/lib/registry/components/numbering-rule/NumberingRuleComponent.tsx index 78c366fd..0c2e795c 100644 --- a/frontend/lib/registry/components/numbering-rule/NumberingRuleComponent.tsx +++ b/frontend/lib/registry/components/numbering-rule/NumberingRuleComponent.tsx @@ -8,19 +8,24 @@ interface NumberingRuleWrapperProps { config: NumberingRuleComponentConfig; onChange?: (config: NumberingRuleComponentConfig) => void; isPreview?: boolean; + tableName?: string; // ํ˜„์žฌ ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ”๋ช… } export const NumberingRuleWrapper: React.FC = ({ config, onChange, isPreview = false, + tableName, }) => { + console.log("๐Ÿ“‹ NumberingRuleWrapper: ํ…Œ์ด๋ธ”๋ช… ์ „๋‹ฌ", { tableName, config }); + return (
); diff --git a/frontend/lib/registry/components/text-input/TextInputComponent.tsx b/frontend/lib/registry/components/text-input/TextInputComponent.tsx index 9ce5fa21..2eb565e0 100644 --- a/frontend/lib/registry/components/text-input/TextInputComponent.tsx +++ b/frontend/lib/registry/components/text-input/TextInputComponent.tsx @@ -100,16 +100,6 @@ export const TextInputComponent: React.FC = ({ const currentFormValue = formData?.[component.columnName]; const currentComponentValue = component.value; - console.log("๐Ÿ”ง TextInput ์ž๋™์ƒ์„ฑ ์ฒดํฌ:", { - componentId: component.id, - columnName: component.columnName, - autoGenType: testAutoGeneration.type, - ruleId: testAutoGeneration.options?.numberingRuleId, - currentFormValue, - currentComponentValue, - autoGeneratedValue, - isInteractive, - }); // ์ž๋™์ƒ์„ฑ๋œ ๊ฐ’์ด ์—†๊ณ , ํ˜„์žฌ ๊ฐ’๋„ ์—†์„ ๋•Œ๋งŒ ์ƒ์„ฑ if (!autoGeneratedValue && !currentFormValue && !currentComponentValue) { diff --git a/frontend/lib/registry/components/text-input/TextInputConfigPanel.tsx b/frontend/lib/registry/components/text-input/TextInputConfigPanel.tsx index 2e78ea97..f487b320 100644 --- a/frontend/lib/registry/components/text-input/TextInputConfigPanel.tsx +++ b/frontend/lib/registry/components/text-input/TextInputConfigPanel.tsx @@ -8,19 +8,20 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { TextInputConfig } from "./types"; import { AutoGenerationType, AutoGenerationConfig } from "@/types/screen"; import { AutoGenerationUtils } from "@/lib/utils/autoGeneration"; -import { getAvailableNumberingRules } from "@/lib/api/numberingRule"; +import { getAvailableNumberingRules, getAvailableNumberingRulesForScreen } from "@/lib/api/numberingRule"; import { NumberingRuleConfig } from "@/types/numbering-rule"; export interface TextInputConfigPanelProps { config: TextInputConfig; onChange: (config: Partial) => void; + screenTableName?: string; // ๐Ÿ†• ํ˜„์žฌ ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ”๋ช… } /** * TextInput ์„ค์ • ํŒจ๋„ * ์ปดํฌ๋„ŒํŠธ์˜ ์„ค์ •๊ฐ’๋“ค์„ ํŽธ์ง‘ํ•  ์ˆ˜ ์žˆ๋Š” UI ์ œ๊ณต */ -export const TextInputConfigPanel: React.FC = ({ config, onChange }) => { +export const TextInputConfigPanel: React.FC = ({ config, onChange, screenTableName }) => { // ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ชฉ๋ก ์ƒํƒœ const [numberingRules, setNumberingRules] = useState([]); const [loadingRules, setLoadingRules] = useState(false); @@ -30,9 +31,20 @@ export const TextInputConfigPanel: React.FC = ({ conf const loadRules = async () => { setLoadingRules(true); try { - const response = await getAvailableNumberingRules(); + let response; + + // ๐Ÿ†• ํ…Œ์ด๋ธ”๋ช…์ด ์žˆ์œผ๋ฉด ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง, ์—†์œผ๋ฉด ์ „์ฒด ์กฐํšŒ + if (screenTableName) { + console.log("๐Ÿ” TextInputConfigPanel: ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜ ์ฑ„๋ฒˆ ๊ทœ์น™ ๋กœ๋“œ", { screenTableName }); + response = await getAvailableNumberingRulesForScreen(screenTableName); + } else { + console.log("๐Ÿ” TextInputConfigPanel: ์ „์ฒด ์ฑ„๋ฒˆ ๊ทœ์น™ ๋กœ๋“œ (ํ…Œ์ด๋ธ”๋ช… ์—†์Œ)"); + response = await getAvailableNumberingRules(); + } + if (response.success && response.data) { setNumberingRules(response.data); + console.log("โœ… ์ฑ„๋ฒˆ ๊ทœ์น™ ๋กœ๋“œ ์™„๋ฃŒ:", response.data.length, "๊ฐœ"); } } catch (error) { console.error("์ฑ„๋ฒˆ ๊ทœ์น™ ๋ชฉ๋ก ๋กœ๋“œ ์‹คํŒจ:", error); @@ -45,7 +57,7 @@ export const TextInputConfigPanel: React.FC = ({ conf if (config.autoGeneration?.type === "numbering_rule") { loadRules(); } - }, [config.autoGeneration?.type]); + }, [config.autoGeneration?.type, screenTableName]); const handleChange = (key: keyof TextInputConfig, value: any) => { onChange({ [key]: value }); @@ -174,7 +186,12 @@ export const TextInputConfigPanel: React.FC = ({ conf ) : ( numberingRules.map((rule) => ( - {rule.ruleName} ({rule.ruleId}) + {rule.ruleName} + {rule.description && ( + + - {rule.description} + + )} )) )} diff --git a/frontend/lib/utils/getConfigPanelComponent.tsx b/frontend/lib/utils/getConfigPanelComponent.tsx index 3234ae9b..c14eb87c 100644 --- a/frontend/lib/utils/getConfigPanelComponent.tsx +++ b/frontend/lib/utils/getConfigPanelComponent.tsx @@ -19,6 +19,8 @@ import { DashboardConfigPanel } from "@/components/screen/config-panels/Dashboar export type ConfigPanelComponent = React.ComponentType<{ config: any; onConfigChange: (config: any) => void; + tableName?: string; // ํ™”๋ฉด ํ…Œ์ด๋ธ”๋ช… (์„ ํƒ) + menuObjid?: number; // ๋ฉ”๋‰ด objid (์„ ํƒ) }>; // ButtonConfigPanel ๋ž˜ํผ (config/onConfigChange โ†’ component/onUpdateProperty ๋ณ€ํ™˜) diff --git a/frontend/lib/utils/webTypeConfigConverter.ts b/frontend/lib/utils/webTypeConfigConverter.ts new file mode 100644 index 00000000..0b5e7101 --- /dev/null +++ b/frontend/lib/utils/webTypeConfigConverter.ts @@ -0,0 +1,58 @@ +/** + * WebTypeConfig์™€ AutoGeneration ๊ฐ„ ๋ณ€ํ™˜ ์œ ํ‹ธ๋ฆฌํ‹ฐ + */ + +import { ComponentData } from "@/types/screen"; + +/** + * webTypeConfig์˜ ์ž๋™์ž…๋ ฅ ์„ค์ •์„ autoGeneration์œผ๋กœ ๋ณ€ํ™˜ + */ +export function convertWebTypeConfigToAutoGeneration(component: ComponentData): ComponentData { + // webTypeConfig๊ฐ€ ์—†์œผ๋ฉด ๋ณ€ํ™˜ ๋ถˆํ•„์š” + if (!component.webTypeConfig) { + return component; + } + + const config = component.webTypeConfig as any; + + // ์ž๋™์ž…๋ ฅ์ด ํ™œ์„ฑํ™”๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ + if (!config.autoInput || !config.autoValueType) { + return component; + } + + // ์ด๋ฏธ autoGeneration์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์ •๋˜์–ด ์žˆ์œผ๋ฉด ๋ณ€ํ™˜ ๋ถˆํ•„์š” + if ( + component.autoGeneration && + component.autoGeneration.type === config.autoValueType && + component.autoGeneration.options?.numberingRuleId === config.numberingRuleId + ) { + return component; + } + + // autoGeneration ๊ฐ์ฒด ์ƒ์„ฑ + const autoGeneration: any = { + type: config.autoValueType, + enabled: true, + }; + + // ์ฑ„๋ฒˆ ๊ทœ์น™์ธ ๊ฒฝ์šฐ options.numberingRuleId ์„ค์ • + if (config.autoValueType === "numbering_rule" && config.numberingRuleId) { + autoGeneration.options = { + numberingRuleId: config.numberingRuleId, + }; + } + + + return { + ...component, + autoGeneration, + }; +} + +/** + * ๋ ˆ์ด์•„์›ƒ์˜ ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ์— ๋Œ€ํ•ด webTypeConfig โ†’ autoGeneration ๋ณ€ํ™˜ ์ ์šฉ + */ +export function convertLayoutComponents(components: ComponentData[]): ComponentData[] { + return components.map(convertWebTypeConfigToAutoGeneration); +} + diff --git a/frontend/types/screen.ts b/frontend/types/screen.ts index 6c4f106b..c20167b8 100644 --- a/frontend/types/screen.ts +++ b/frontend/types/screen.ts @@ -132,7 +132,16 @@ export type AutoGenerationType = "table" | "form" | "mixed"; */ export interface AutoGenerationConfig { type: AutoGenerationType; + enabled?: boolean; tableName?: string; includeSearch?: boolean; includePagination?: boolean; + options?: { + length?: number; // ๋žœ๋ค ๋ฌธ์ž์—ด/์ˆซ์ž ๊ธธ์ด + prefix?: string; // ์ ‘๋‘์‚ฌ + suffix?: string; // ์ ‘๋ฏธ์‚ฌ + format?: string; // ์‹œ๊ฐ„ ํ˜•์‹ (current_time์šฉ) + startValue?: number; // ์‹œํ€€์Šค ์‹œ์ž‘๊ฐ’ + numberingRuleId?: string; // ์ฑ„๋ฒˆ ๊ทœ์น™ ID (numbering_rule ํƒ€์ž…์šฉ) + }; } diff --git a/์ฑ„๋ฒˆ๊ทœ์น™_ํ…Œ์ด๋ธ”๊ธฐ๋ฐ˜_์ž๋™๊ฐ์ง€_๊ตฌํ˜„_์™„๋ฃŒ.md b/์ฑ„๋ฒˆ๊ทœ์น™_ํ…Œ์ด๋ธ”๊ธฐ๋ฐ˜_์ž๋™๊ฐ์ง€_๊ตฌํ˜„_์™„๋ฃŒ.md new file mode 100644 index 00000000..7ea0d445 --- /dev/null +++ b/์ฑ„๋ฒˆ๊ทœ์น™_ํ…Œ์ด๋ธ”๊ธฐ๋ฐ˜_์ž๋™๊ฐ์ง€_๊ตฌํ˜„_์™„๋ฃŒ.md @@ -0,0 +1,335 @@ +# ์ฑ„๋ฒˆ๊ทœ์น™ ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜ ์ž๋™ ๊ฐ์ง€ ๊ตฌํ˜„ ์™„๋ฃŒ + +## ๐Ÿ“‹ ๋ณ€๊ฒฝ ์š”์ฒญ์‚ฌํ•ญ + +**์š”๊ตฌ์‚ฌํ•ญ**: ์ฑ„๋ฒˆ ๊ทœ์น™์„ ๋” ๊ฐ„๋‹จํ•˜๊ฒŒ ๋งŒ๋“ค๊ธฐ +1. ๊ธฐ๋ณธ๊ฐ’์„ `table`๋กœ ์„ค์ • +2. ์ ์šฉ ๋ฒ”์œ„ ์„ ํƒ UI ์ œ๊ฑฐ +3. ํ˜„์žฌ ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ”์„ ์ž๋™์œผ๋กœ ๊ฐ์ง€ํ•˜์—ฌ ์ €์žฅ + +## โœ… ๊ตฌํ˜„ ์™„๋ฃŒ ๋‚ด์—ญ + +### 1. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ + +**ํŒŒ์ผ**: `db/migrations/046_update_numbering_rules_scope_type.sql` + +#### ์ฃผ์š” ๋ณ€๊ฒฝ์‚ฌํ•ญ: +- ๊ธฐ์กด ๋ชจ๋“  ๊ทœ์น™์„ `table` ํƒ€์ž…์œผ๋กœ ๋ณ€๊ฒฝ +- `scope_type` ์ œ์•ฝ์กฐ๊ฑด ๋‹จ์ˆœํ™” (table๋งŒ ์ง€์›) +- ๋ถˆํ•„์š”ํ•œ ์ œ์•ฝ์กฐ๊ฑด ์ œ๊ฑฐ (global, menu ๊ด€๋ จ) +- ์ธ๋ฑ์Šค ์ตœ์ ํ™” (table_name + company_code) + +```sql +-- ๋ชจ๋“  ๊ธฐ์กด ๊ทœ์น™์„ table ํƒ€์ž…์œผ๋กœ ๋ณ€๊ฒฝ +UPDATE numbering_rules +SET scope_type = 'table' +WHERE scope_type IN ('global', 'menu'); + +-- table_name์ด ์—†๋Š” ๊ทœ์น™์€ ๋นˆ ๋ฌธ์ž์—ด๋กœ ์„ค์ • +UPDATE numbering_rules +SET table_name = '' +WHERE table_name IS NULL; + +-- ์ œ์•ฝ์กฐ๊ฑด: table ํƒ€์ž…์ด๋ฉด table_name ํ•„์ˆ˜ +ALTER TABLE numbering_rules +ADD CONSTRAINT check_table_scope_requires_table_name +CHECK ( + (scope_type = 'table' AND table_name IS NOT NULL) + OR scope_type != 'table' +); + +-- ์ธ๋ฑ์Šค ์ตœ์ ํ™” +CREATE INDEX IF NOT EXISTS idx_numbering_rules_table_company +ON numbering_rules(table_name, company_code); +``` + +### 2. ๋ฐฑ์—”๋“œ API ๊ฐ„์†Œํ™” + +**ํŒŒ์ผ**: +- `backend-node/src/services/numberingRuleService.ts` +- `backend-node/src/controllers/numberingRuleController.ts` + +#### ์ฃผ์š” ๋ณ€๊ฒฝ์‚ฌํ•ญ: +- `menuObjid` ํŒŒ๋ผ๋ฏธํ„ฐ ์ œ๊ฑฐ +- ํ…Œ์ด๋ธ”๋ช…๋งŒ์œผ๋กœ ํ•„ํ„ฐ๋ง (`tableName` ํ•„์ˆ˜) +- SQL ์ฟผ๋ฆฌ ๋‹จ์ˆœํ™” + +**์ˆ˜์ •๋œ ์„œ๋น„์Šค ๋ฉ”์„œ๋“œ**: +```typescript +async getAvailableRulesForScreen( + companyCode: string, + tableName: string +): Promise { + // menuObjid ์ œ๊ฑฐ, tableName๋งŒ ์‚ฌ์šฉ + // WHERE table_name = $1 AND company_code = $2 +} +``` + +**์ˆ˜์ •๋œ API ์—”๋“œํฌ์ธํŠธ**: +```typescript +GET /api/numbering-rules/available-for-screen?tableName=item_info +// menuObjid ํŒŒ๋ผ๋ฏธํ„ฐ ์ œ๊ฑฐ +``` + +### 3. ํ”„๋ก ํŠธ์—”๋“œ API ํด๋ผ์ด์–ธํŠธ ์ˆ˜์ • + +**ํŒŒ์ผ**: `frontend/lib/api/numberingRule.ts` + +#### ์ฃผ์š” ๋ณ€๊ฒฝ์‚ฌํ•ญ: +- `menuObjid` ํŒŒ๋ผ๋ฏธํ„ฐ ์ œ๊ฑฐ +- ํ…Œ์ด๋ธ”๋ช…๋งŒ ์ „๋‹ฌ + +```typescript +export async function getAvailableNumberingRulesForScreen( + tableName: string // menuObjid ์ œ๊ฑฐ +): Promise> { + const response = await apiClient.get("/numbering-rules/available-for-screen", { + params: { tableName }, + }); + return response.data; +} +``` + +### 4. ์ฑ„๋ฒˆ ๊ทœ์น™ ๋””์ž์ด๋„ˆ UI ๋Œ€ํญ ๊ฐ„์†Œํ™” + +**ํŒŒ์ผ**: `frontend/components/numbering-rule/NumberingRuleDesigner.tsx` + +#### ์ฃผ์š” ๋ณ€๊ฒฝ์‚ฌํ•ญ: + +##### โœ… Props ์ถ”๊ฐ€ +```typescript +interface NumberingRuleDesignerProps { + // ... ๊ธฐ์กด props + currentTableName?: string; // ํ˜„์žฌ ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ”๋ช… ์ž๋™ ์ „๋‹ฌ +} +``` + +##### โœ… ์ƒˆ ๊ทœ์น™ ์ƒ์„ฑ ์‹œ ์ž๋™ ์„ค์ • +```typescript +const handleNewRule = useCallback(() => { + const newRule: NumberingRuleConfig = { + // ... + scopeType: "table", // ๊ธฐ๋ณธ๊ฐ’ table๋กœ ๊ณ ์ • + tableName: currentTableName || "", // ํ˜„์žฌ ํ…Œ์ด๋ธ”๋ช… ์ž๋™ ์„ค์ • + }; +}, [currentTableName]); +``` + +##### โœ… ์ €์žฅ ์‹œ ์ž๋™ ์„ค์ • +```typescript +const handleSaveRule = useCallback(async () => { + const ruleToSave = { + ...currentRule, + scopeType: "table" as const, // ํ•ญ์ƒ table๋กœ ๊ณ ์ • + tableName: currentTableName || currentRule.tableName || "", // ์ž๋™ ๊ฐ์ง€ + }; + + // ๋ฐฑ์—”๋“œ์— ์ €์žฅ +}, [currentRule, currentTableName]); +``` + +##### โœ… UI ๋ณ€๊ฒฝ: ์ ์šฉ ๋ฒ”์œ„ ์„ ํƒ ์ œ๊ฑฐ +**์ด์ „**: +```tsx +{/* ์ ์šฉ ๋ฒ”์œ„ ์„ ํƒ Select */} + + +{/* ์กฐ๊ฑด๋ถ€: ํ…Œ์ด๋ธ”๋ช… ์ž…๋ ฅ */} +{scopeType === "table" && ( + +)} + +{/* ์กฐ๊ฑด๋ถ€: ๋ฉ”๋‰ด ์„ ํƒ */} +{scopeType === "menu" && ( + +)} +``` + +**ํ˜„์žฌ (๊ฐ„์†Œํ™”)**: +```tsx +{/* ์ž๋™ ๊ฐ์ง€๋œ ํ…Œ์ด๋ธ” ์ •๋ณด ํ‘œ์‹œ (์ฝ๊ธฐ ์ „์šฉ) */} +{currentTableName && ( +
+ +
+ {currentTableName} +
+

+ ์ด ๊ทœ์น™์€ ํ˜„์žฌ ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ”({currentTableName})์— ์ž๋™์œผ๋กœ ์ ์šฉ๋ฉ๋‹ˆ๋‹ค +

+
+)} +``` + +### 5. ํ™”๋ฉด๊ด€๋ฆฌ์—์„œ ํ…Œ์ด๋ธ”๋ช… ์ „๋‹ฌ + +**ํŒŒ์ผ**: `frontend/components/screen/panels/webtype-configs/TextTypeConfigPanel.tsx` + +#### ์ฃผ์š” ๋ณ€๊ฒฝ์‚ฌํ•ญ: +- `menuObjid` ์ œ๊ฑฐ, `tableName`๋งŒ ์‚ฌ์šฉ +- ํ…Œ์ด๋ธ”๋ช…์ด ์—†์œผ๋ฉด ๋นˆ ๋ฐฐ์—ด ๋ฐ˜ํ™˜ + +```typescript +useEffect(() => { + const loadRules = async () => { + if (tableName) { + console.log("๐Ÿ“‹ ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ:", { tableName }); + response = await getAvailableNumberingRulesForScreen(tableName); + } else { + console.warn("โš ๏ธ ํ…Œ์ด๋ธ”๋ช…์ด ์—†์–ด ์ฑ„๋ฒˆ ๊ทœ์น™์„ ์กฐํšŒํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"); + setNumberingRules([]); + return; + } + }; +}, [localValues.autoValueType, tableName]); // menuObjid ์ œ๊ฑฐ +``` + +## ๐Ÿ“Š ๋ณ€๊ฒฝ ์ „ํ›„ ๋น„๊ต + +### ์ด์ „ ๋ฐฉ์‹ (๋ณต์žก) + +1. ์‚ฌ์šฉ์ž๊ฐ€ **์ ์šฉ ๋ฒ”์œ„** ์„ ํƒ (์ „์—ญ/ํ…Œ์ด๋ธ”๋ณ„/๋ฉ”๋‰ด๋ณ„) +2. ํ…Œ์ด๋ธ”๋ณ„ ์„ ํƒ ์‹œ โ†’ ํ…Œ์ด๋ธ”๋ช… **์ง์ ‘ ์ž…๋ ฅ** +3. ๋ฉ”๋‰ด๋ณ„ ์„ ํƒ ์‹œ โ†’ ๋ฉ”๋‰ด **์ˆ˜๋™ ์„ ํƒ** +4. ์ €์žฅ ์‹œ ์ž…๋ ฅํ•œ ์ •๋ณด๋กœ ์ €์žฅ + +**๋ฌธ์ œ์ **: +- UI๊ฐ€ ๋ณต์žก (3๋‹จ๊ณ„ ์„ ํƒ) +- ์‚ฌ์šฉ์ž๊ฐ€ ํ…Œ์ด๋ธ”๋ช…์„ ์ˆ˜๋™ ์ž…๋ ฅํ•ด์•ผ ํ•จ +- ์˜คํƒ€ ๊ฐ€๋Šฅ์„ฑ +- ๋ฉ”๋‰ด ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง์€ ๋ณต์žกํ•˜๊ณ  ์ง๊ด€์ ์ด์ง€ ์•Š์Œ + +### ํ˜„์žฌ ๋ฐฉ์‹ (๊ฐ„๋‹จ) + +1. ์ฑ„๋ฒˆ ๊ทœ์น™ ๋””์ž์ด๋„ˆ ์—ด๊ธฐ +2. ๊ทœ์น™ ์ด๋ฆ„๊ณผ ํŒŒํŠธ ์„ค์ • +3. ์ €์žฅ โ†’ **์ž๋™์œผ๋กœ ํ˜„์žฌ ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ”๋ช… ์ €์žฅ๋จ** + +**์žฅ์ **: +- UI ๋‹จ์ˆœ (์ ์šฉ ๋ฒ”์œ„ ์„ ํƒ UI ์ œ๊ฑฐ) +- ํ…Œ์ด๋ธ”๋ช… ์ž๋™ ๊ฐ์ง€ (์˜คํƒ€ ์—†์Œ) +- ์‚ฌ์šฉ์ž๋Š” ๊ทœ์น™๋งŒ ์„ค๊ณ„ํ•˜๋ฉด ๋จ +- ๊ฐ™์€ ํ…Œ์ด๋ธ”์„ ์‚ฌ์šฉํ•˜๋Š” ํ™”๋ฉด์—์„œ ์ž๋™์œผ๋กœ ๊ทœ์น™ ๊ณต์œ  + +## ๐Ÿ” ์ž‘๋™ ํ๋ฆ„ + +### 1. ์ฑ„๋ฒˆ ๊ทœ์น™ ์ƒ์„ฑ + +``` +์‚ฌ์šฉ์ž: "์ƒˆ ๊ทœ์น™" ๋ฒ„ํŠผ ํด๋ฆญ + โ†“ +์‹œ์Šคํ…œ: currentTableName (์˜ˆ: "item_info") ์ž๋™ ๊ฐ์ง€ + โ†“ +๊ทœ์น™ ์ƒ์„ฑ: scopeType = "table", tableName = "item_info" + โ†“ +์ €์žฅ ์‹œ: DB์— table_name = "item_info"๋กœ ์ €์žฅ๋จ +``` + +### 2. ํ™”๋ฉด๊ด€๋ฆฌ์—์„œ ๊ทœ์น™ ์‚ฌ์šฉ + +``` +์‚ฌ์šฉ์ž: ํ…์ŠคํŠธ ํ•„๋“œ ์„ค์ • โ†’ "์ž๋™๊ฐ’ ์œ ํ˜•" = "์ฑ„๋ฒˆ ๊ทœ์น™" + โ†“ +์‹œ์Šคํ…œ: ํ˜„์žฌ ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ”๋ช… (์˜ˆ: "item_info") ๊ฐ€์ ธ์˜ด + โ†“ +API ํ˜ธ์ถœ: GET /api/numbering-rules/available-for-screen?tableName=item_info + โ†“ +๋ฐฑ์—”๋“œ: WHERE table_name = 'item_info' AND company_code = 'COMPANY_A' + โ†“ +์‘๋‹ต: item_info ํ…Œ์ด๋ธ”์— ๋Œ€ํ•œ ๊ทœ์น™ ๋ชฉ๋ก ๋ฐ˜ํ™˜ + โ†“ +UI: ๋“œ๋กญ๋‹ค์šด์— ํ•ด๋‹น ๊ทœ์น™๋“ค๋งŒ ํ‘œ์‹œ +``` + +## ๐ŸŽฏ ํ•ต์‹ฌ ๊ฐœ์„  ํฌ์ธํŠธ + +### โœ… ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ (UX) +- **์ด์ „**: 3๋‹จ๊ณ„ ์„ ํƒ (๋ฒ”์œ„ โ†’ ํ…Œ์ด๋ธ”/๋ฉ”๋‰ด โ†’ ์ž…๋ ฅ/์„ ํƒ) +- **ํ˜„์žฌ**: ๊ทœ์น™๋งŒ ์„ค๊ณ„ (ํ…Œ์ด๋ธ”์€ ์ž๋™ ๊ฐ์ง€) + +### โœ… ์˜ค๋ฅ˜ ๊ฐ€๋Šฅ์„ฑ +- **์ด์ „**: ํ…Œ์ด๋ธ”๋ช… ์ง์ ‘ ์ž…๋ ฅ โ†’ ์˜คํƒ€ ๋ฐœ์ƒ ๊ฐ€๋Šฅ +- **ํ˜„์žฌ**: ์ž๋™ ๊ฐ์ง€ โ†’ ์˜คํƒ€ ๋ถˆ๊ฐ€๋Šฅ + +### โœ… ์ง๊ด€์„ฑ +- **์ด์ „**: "์ด ๊ทœ์น™์€ ์–ด๋””์— ์ ์šฉ๋˜๋‚˜์š”?" โ†’ ์‚ฌ์šฉ์ž๊ฐ€ ์ดํ•ดํ•ด์•ผ ํ•จ +- **ํ˜„์žฌ**: "ํ˜„์žฌ ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ”์— ์ž๋™ ์ ์šฉ" โ†’ ์ž๋™์œผ๋กœ ์•Œ๋งž๊ฒŒ ์ ์šฉ + +### โœ… ์ฝ”๋“œ ๋ณต์žก๋„ +- **์ด์ „**: 3๊ฐ€์ง€ scopeType ์ฒ˜๋ฆฌ (global, table, menu) +- **ํ˜„์žฌ**: 1๊ฐ€์ง€ scopeType๋งŒ ์ฒ˜๋ฆฌ (table) + +## ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„ + +### 1. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ (ํ•„์ˆ˜) + +```bash +# PostgreSQL ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ ํ›„ ์‹คํ–‰ +PGPASSWORD=<์‹ค์ œ_๋น„๋ฐ€๋ฒˆํ˜ธ> psql -h localhost -U postgres -d ilshin \ + -f /Users/kimjuseok/ERP-node/db/migrations/046_update_numbering_rules_scope_type.sql +``` + +### 2. ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ + +#### ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค: +1. ํ™”๋ฉด๊ด€๋ฆฌ์—์„œ `item_info` ํ…Œ์ด๋ธ” ์„ ํƒ +2. ์ฑ„๋ฒˆ ๊ทœ์น™ ์ปดํฌ๋„ŒํŠธ ์—ด๊ธฐ +3. "์ƒˆ ๊ทœ์น™" ์ƒ์„ฑ โ†’ ์ž๋™์œผ๋กœ `tableName = "item_info"` ์„ค์ •๋˜๋Š”์ง€ ํ™•์ธ +4. ๊ทœ์น™ ์ €์žฅ โ†’ DB์— `scope_type = 'table'`, `table_name = 'item_info'`๋กœ ์ €์žฅ๋˜๋Š”์ง€ ํ™•์ธ +5. ํ…์ŠคํŠธ ํ•„๋“œ ์„ค์ • โ†’ "์ž๋™๊ฐ’ ์œ ํ˜•" = "์ฑ„๋ฒˆ ๊ทœ์น™" ์„ ํƒ +6. ๋“œ๋กญ๋‹ค์šด์—์„œ ํ•ด๋‹น ๊ทœ์น™์ด ํ‘œ์‹œ๋˜๋Š”์ง€ ํ™•์ธ +7. ๋‹ค๋ฅธ ํ…Œ์ด๋ธ” ํ™”๋ฉด์—์„œ๋Š” ํ•ด๋‹น ๊ทœ์น™์ด **์•ˆ ๋ณด์ด๋Š”์ง€** ํ™•์ธ + +### 3. ๊ธฐ์กด ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ™•์ธ + +๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ ํ›„: +```sql +-- ๋ชจ๋“  ๊ทœ์น™์ด table ํƒ€์ž…์ธ์ง€ ํ™•์ธ +SELECT scope_type, COUNT(*) +FROM numbering_rules +GROUP BY scope_type; + +-- ๊ฒฐ๊ณผ: scope_type='table'๋งŒ ๋‚˜์™€์•ผ ํ•จ + +-- table_name์ด ๋น„์–ด์žˆ๋Š” ๊ทœ์น™ ํ™•์ธ +SELECT rule_id, rule_name, table_name +FROM numbering_rules +WHERE table_name = '' OR table_name IS NULL; + +-- ๊ฒฐ๊ณผ: ๋น„์–ด์žˆ๋Š” ๊ทœ์น™์ด ์žˆ๋‹ค๋ฉด ์ˆ˜๋™ ์—…๋ฐ์ดํŠธ ํ•„์š” +``` + +## ๐Ÿ“ ๋ณ€๊ฒฝ๋œ ํŒŒ์ผ ๋ชฉ๋ก + +### ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค +- โœ… `db/migrations/046_update_numbering_rules_scope_type.sql` (์ˆ˜์ •) + +### ๋ฐฑ์—”๋“œ +- โœ… `backend-node/src/services/numberingRuleService.ts` (๊ฐ„์†Œํ™”) +- โœ… `backend-node/src/controllers/numberingRuleController.ts` (๊ฐ„์†Œํ™”) + +### ํ”„๋ก ํŠธ์—”๋“œ +- โœ… `frontend/lib/api/numberingRule.ts` (๊ฐ„์†Œํ™”) +- โœ… `frontend/components/numbering-rule/NumberingRuleDesigner.tsx` (๋Œ€ํญ ๊ฐ„์†Œํ™”) +- โœ… `frontend/components/screen/panels/webtype-configs/TextTypeConfigPanel.tsx` (๊ฐ„์†Œํ™”) + +## ๐ŸŽ‰ ๊ฒฐ๋ก  + +์ฑ„๋ฒˆ ๊ทœ์น™ ์‹œ์Šคํ…œ์ด ๋Œ€ํญ ๊ฐ„์†Œํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค! + +**์ด์ œ ์‚ฌ์šฉ์ž๋Š”**: +1. ํ™”๋ฉด๊ด€๋ฆฌ์—์„œ ํ…Œ์ด๋ธ” ์„ ํƒ +2. ์ฑ„๋ฒˆ ๊ทœ์น™ ๋””์ž์ด๋„ˆ์—์„œ ๊ทœ์น™ ์„ค๊ณ„ +3. ์ €์žฅ โ†’ **์ž๋™์œผ๋กœ ํ˜„์žฌ ํ…Œ์ด๋ธ”์— ์ ์šฉ๋จ** + +**์‹œ์Šคํ…œ์€**: +- ์ž๋™์œผ๋กœ ํ˜„์žฌ ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ”๋ช… ๊ฐ์ง€ +- ๊ฐ™์€ ํ…Œ์ด๋ธ”์˜ ํ™”๋ฉด์—์„œ ๊ทœ์น™ ์ž๋™ ๊ณต์œ  +- ์˜คํƒ€ ์—†๋Š” ์ •ํ™•ํ•œ ๋งคํ•‘ + +์™„๋ฃŒ! ๐Ÿš€ + diff --git a/์ฑ„๋ฒˆ๊ทœ์น™_ํ…Œ์ด๋ธ”๊ธฐ๋ฐ˜_ํ•„ํ„ฐ๋ง_๊ตฌํ˜„_๊ณ„ํš์„œ.md b/์ฑ„๋ฒˆ๊ทœ์น™_ํ…Œ์ด๋ธ”๊ธฐ๋ฐ˜_ํ•„ํ„ฐ๋ง_๊ตฌํ˜„_๊ณ„ํš์„œ.md new file mode 100644 index 00000000..48297674 --- /dev/null +++ b/์ฑ„๋ฒˆ๊ทœ์น™_ํ…Œ์ด๋ธ”๊ธฐ๋ฐ˜_ํ•„ํ„ฐ๋ง_๊ตฌํ˜„_๊ณ„ํš์„œ.md @@ -0,0 +1,1126 @@ +# ์ฑ„๋ฒˆ๊ทœ์น™ ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง ๊ตฌํ˜„ ๊ณ„ํš์„œ + +## ๐Ÿ“‹ ํ”„๋กœ์ ํŠธ ๊ฐœ์š” + +### ๋ชฉ์  + +ํ˜„์žฌ ๋ฉ”๋‰ด ๊ธฐ๋ฐ˜ ์ฑ„๋ฒˆ๊ทœ์น™ ํ•„ํ„ฐ๋ง ๋ฐฉ์‹์„ **ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง**์œผ๋กœ ์ „ํ™˜ํ•˜์—ฌ ๋” ์ง๊ด€์ ์ด๊ณ  ์œ ์ง€๋ณด์ˆ˜ํ•˜๊ธฐ ์‰ฌ์šด ์‹œ์Šคํ…œ ๊ตฌ์ถ• + +### ํ˜„์žฌ ๋ฌธ์ œ์  + +1. ํ™”๋ฉด๊ด€๋ฆฌ์—์„œ `menuObjid` ์ •๋ณด๊ฐ€ ์—†์–ด `scope_type='menu'` ๊ทœ์น™์„ ๋ณผ ์ˆ˜ ์—†์Œ +2. ๋ฉ”๋‰ด ๊ตฌ์กฐ ๋ณ€๊ฒฝ ์‹œ ์ฑ„๋ฒˆ๊ทœ์น™ ์žฌ์„ค์ • ํ•„์š” +3. ๊ฐ™์€ ํ…Œ์ด๋ธ”์„ ์‚ฌ์šฉํ•˜๋Š” ํ™”๋ฉด๋“ค์— ๋™์ผํ•œ ๊ทœ์น™์„ ๋ฐ˜๋ณต ์„ค์ •ํ•ด์•ผ ํ•จ +4. ๋ฉ”๋‰ด ๊ณ„์ธต ๊ตฌ์กฐ๋ฅผ ์ดํ•ดํ•ด์•ผ ๊ทœ์น™ ์„ค์ • ๊ฐ€๋Šฅ (๋ณต์žก๋„ ๋†’์Œ) + +### ํ•ด๊ฒฐ ๋ฐฉ์•ˆ + +- **ํ…Œ์ด๋ธ”๋ช… ๊ธฐ๋ฐ˜ ์ž๋™ ๋งค์นญ**: ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ”๊ณผ ๊ทœ์น™์˜ ํ…Œ์ด๋ธ”์ด ๊ฐ™์œผ๋ฉด ์ž๋™์œผ๋กœ ํ‘œ์‹œ +- **ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์ ‘๊ทผ**: `scope_type`์„ 'global', 'table', 'menu' ์„ธ ๊ฐ€์ง€๋กœ ํ™•์žฅ +- **์šฐ์„ ์ˆœ์œ„ ์‹œ์Šคํ…œ**: menu > table > global ์ˆœ์œผ๋กœ ๊ตฌ์ฒด์ ์ธ ๊ทœ์น™ ์šฐ์„  ์ ์šฉ + +--- + +## ๐ŸŽฏ ๋ชฉํ‘œ + +### ๊ธฐ๋Šฅ ๋ชฉํ‘œ + +- [x] ๊ฐ™์€ ํ…Œ์ด๋ธ”์„ ์‚ฌ์šฉํ•˜๋Š” ํ™”๋ฉด์—์„œ ์ฑ„๋ฒˆ๊ทœ์น™ ์ž๋™ ํ‘œ์‹œ +- [x] ์„ธ ๊ฐ€์ง€ scope_type ์ง€์› (global, table, menu) +- [x] ์šฐ์„ ์ˆœ์œ„ ๊ธฐ๋ฐ˜ ๊ทœ์น™ ์„ ํƒ +- [x] ๊ธฐ์กด ๊ทœ์น™ ์ž๋™ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ + +### ๋น„๊ธฐ๋Šฅ ๋ชฉํ‘œ + +- [x] ๊ธฐ์กด ๊ธฐ๋Šฅ 100% ํ˜ธํ™˜์„ฑ ์œ ์ง€ +- [x] ์„ฑ๋Šฅ ์ €ํ•˜ ์—†์Œ (์ธ๋ฑ์Šค ์ตœ์ ํ™”) +- [x] ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๋ณด์•ˆ ์œ ์ง€ +- [x] ๋กค๋ฐฑ ๊ฐ€๋Šฅํ•œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ + +--- + +## ๐Ÿ“ ์‹œ์Šคํ…œ ์„ค๊ณ„ + +### scope_type ์ •์˜ + +| scope_type | ์„ค๋ช… | ์šฐ์„ ์ˆœ์œ„ | ์‚ฌ์šฉ ์ผ€์ด์Šค | +| ---------- | ---------------------- | -------- | ------------------------------- | +| `menu` | ํŠน์ • ๋ฉ”๋‰ด์—์„œ๋งŒ ์‚ฌ์šฉ | 1 (์ตœ๊ณ ) | ๋ฉ”๋‰ด๋ณ„๋กœ ๋‹ค๋ฅธ ์ฑ„๋ฒˆ ๋ฐฉ์‹ ํ•„์š” ์‹œ | +| `table` | ํŠน์ • ํ…Œ์ด๋ธ”์—์„œ๋งŒ ์‚ฌ์šฉ | 2 (์ค‘๊ฐ„) | ํ…Œ์ด๋ธ” ๊ธฐ์ค€ ์ฑ„๋ฒˆ (์ผ๋ฐ˜์ ) | +| `global` | ๋ชจ๋“  ๊ณณ์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅ | 3 (์ตœ์ €) | ๊ณตํ†ต ์ฑ„๋ฒˆ ๊ทœ์น™ | + +### ํ•„ํ„ฐ๋ง ๋กœ์ง (์šฐ์„ ์ˆœ์œ„) + +```sql +WHERE company_code = $1 + AND ( + -- 1์ˆœ์œ„: ๋ฉ”๋‰ด๋ณ„ ๊ทœ์น™ (๊ฐ€์žฅ ๊ตฌ์ฒด์ ) + (scope_type = 'menu' AND menu_objid = $3) + + -- 2์ˆœ์œ„: ํ…Œ์ด๋ธ”๋ณ„ ๊ทœ์น™ (์ผ๋ฐ˜์ ) + OR (scope_type = 'table' AND table_name = $2) + + -- 3์ˆœ์œ„: ์ „์—ญ ๊ทœ์น™ (๊ฐ€์žฅ ์ผ๋ฐ˜์ , table_name ์ œ์•ฝ ์—†์Œ) + OR (scope_type = 'global' AND table_name IS NULL) + ) +ORDER BY + CASE scope_type + WHEN 'menu' THEN 1 + WHEN 'table' THEN 2 + WHEN 'global' THEN 3 + END, + created_at DESC +``` + +### ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ๋ณ€๊ฒฝ + +#### numbering_rules ํ…Œ์ด๋ธ” + +**๋ณ€๊ฒฝ ์ „**: + +```sql +scope_type VARCHAR(20) -- ๊ฐ’: 'global' ๋˜๋Š” 'menu' +``` + +**๋ณ€๊ฒฝ ํ›„**: + +```sql +scope_type VARCHAR(20) -- ๊ฐ’: 'global', 'table', 'menu' +CHECK (scope_type IN ('global', 'table', 'menu')) +``` + +**์ถ”๊ฐ€ ์ œ์•ฝ์กฐ๊ฑด**: + +```sql +-- table ํƒ€์ž…์€ ๋ฐ˜๋“œ์‹œ table_name์ด ์žˆ์–ด์•ผ ํ•จ +CHECK ( + (scope_type = 'table' AND table_name IS NOT NULL) + OR scope_type != 'table' +) + +-- global ํƒ€์ž…์€ table_name์ด ์—†์–ด์•ผ ํ•จ +CHECK ( + (scope_type = 'global' AND table_name IS NULL) + OR scope_type != 'global' +) + +-- menu ํƒ€์ž…์€ ๋ฐ˜๋“œ์‹œ menu_objid๊ฐ€ ์žˆ์–ด์•ผ ํ•จ +CHECK ( + (scope_type = 'menu' AND menu_objid IS NOT NULL) + OR scope_type != 'menu' +) +``` + +--- + +## ๐Ÿ”ง ๊ตฌํ˜„ ๋‹จ๊ณ„ + +### Phase 1: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ (30๋ถ„) + +#### 1.1 ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ์ƒ์„ฑ + +- ํŒŒ์ผ: `db/migrations/046_update_numbering_rules_scope_type.sql` +- ๋‚ด์šฉ: + 1. scope_type ์ œ์•ฝ์กฐ๊ฑด ํ™•์žฅ + 2. ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์ œ์•ฝ์กฐ๊ฑด ์ถ”๊ฐ€ + 3. ๊ธฐ์กด ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ (global โ†’ table) + 4. ์ธ๋ฑ์Šค ์ตœ์ ํ™” + +#### 1.2 ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋กœ์ง + +```sql +-- ๊ธฐ์กด ๊ทœ์น™ ์ค‘ table_name์ด ์žˆ๋Š” ๊ฒƒ์€ 'table' ํƒ€์ž…์œผ๋กœ ๋ณ€๊ฒฝ +UPDATE numbering_rules +SET scope_type = 'table' +WHERE scope_type = 'global' + AND table_name IS NOT NULL; + +-- ๊ธฐ์กด ๊ทœ์น™ ์ค‘ table_name์ด ์—†๋Š” ๊ฒƒ์€ 'global' ์œ ์ง€ +-- (๋ณ€๊ฒฝ ๋ถˆํ•„์š”) +``` + +#### 1.3 ๋กค๋ฐฑ ๊ณ„ํš + +- ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํŒจ ์‹œ ์ž๋™ ๋กค๋ฐฑ (ํŠธ๋žœ์žญ์…˜) +- ์ˆ˜๋™ ๋กค๋ฐฑ ์Šคํฌ๋ฆฝํŠธ ์ œ๊ณต + +--- + +### Phase 2: ๋ฐฑ์—”๋“œ API ์ˆ˜์ • (1์‹œ๊ฐ„) + +#### 2.1 numberingRuleService.ts ์ˆ˜์ • + +**๋ณ€๊ฒฝํ•  ํ•จ์ˆ˜**: + +##### getAvailableRulesForScreen (์‹ ๊ทœ ํ•จ์ˆ˜) + +```typescript +async getAvailableRulesForScreen( + companyCode: string, + tableName: string, + menuObjid?: number +): Promise { + try { + logger.info("ํ™”๋ฉด์šฉ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ", { + companyCode, + tableName, + menuObjid, + }); + + const pool = getPool(); + + // ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ: ์ตœ๊ณ  ๊ด€๋ฆฌ์ž vs ์ผ๋ฐ˜ ํšŒ์‚ฌ + let query: string; + let params: any[]; + + if (companyCode === "*") { + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ชจ๋“  ํšŒ์‚ฌ์˜ ๊ทœ์น™ ์กฐํšŒ ๊ฐ€๋Šฅ + // ํ•˜์ง€๋งŒ ์ผ๋ฐ˜์ ์œผ๋กœ๋Š” ์ผ๋ฐ˜ ํšŒ์‚ฌ๋“ค์˜ ๊ทœ์น™์„ ์กฐํšŒํ•˜๋ฏ€๋กœ + // company_code != '*' ์กฐ๊ฑด ์ถ”๊ฐ€ (์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ „์šฉ ๊ทœ์น™ ์ œ์™ธ) + query = ` + SELECT + rule_id AS "ruleId", + rule_name AS "ruleName", + description, + separator, + reset_period AS "resetPeriod", + current_sequence AS "currentSequence", + table_name AS "tableName", + column_name AS "columnName", + company_code AS "companyCode", + menu_objid AS "menuObjid", + scope_type AS "scopeType", + created_at AS "createdAt", + updated_at AS "updatedAt", + created_by AS "createdBy" + FROM numbering_rules + WHERE company_code != '*' + AND ( + (scope_type = 'menu' AND menu_objid = $1) + OR (scope_type = 'table' AND table_name = $2) + OR (scope_type = 'global' AND table_name IS NULL) + ) + ORDER BY + CASE scope_type + WHEN 'menu' THEN 1 + WHEN 'table' THEN 2 + WHEN 'global' THEN 3 + END, + created_at DESC + `; + params = [menuObjid, tableName]; + logger.info("์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ์ผ๋ฐ˜ ํšŒ์‚ฌ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ (company_code != '*')"); + } else { + // ์ผ๋ฐ˜ ํšŒ์‚ฌ: ์ž์‹ ์˜ ๊ทœ์น™๋งŒ ์กฐํšŒ + query = ` + SELECT + rule_id AS "ruleId", + rule_name AS "ruleName", + description, + separator, + reset_period AS "resetPeriod", + current_sequence AS "currentSequence", + table_name AS "tableName", + column_name AS "columnName", + company_code AS "companyCode", + menu_objid AS "menuObjid", + scope_type AS "scopeType", + created_at AS "createdAt", + updated_at AS "updatedAt", + created_by AS "createdBy" + FROM numbering_rules + WHERE company_code = $1 + AND ( + (scope_type = 'menu' AND menu_objid = $2) + OR (scope_type = 'table' AND table_name = $3) + OR (scope_type = 'global' AND table_name IS NULL) + ) + ORDER BY + CASE scope_type + WHEN 'menu' THEN 1 + WHEN 'table' THEN 2 + WHEN 'global' THEN 3 + END, + created_at DESC + `; + params = [companyCode, menuObjid, tableName]; + } + + const result = await pool.query(query, params); + + // ๊ฐ ๊ทœ์น™์˜ ํŒŒํŠธ ์ •๋ณด ๋กœ๋“œ + for (const rule of result.rows) { + const partsQuery = ` + SELECT + id, + part_order AS "order", + part_type AS "partType", + generation_method AS "generationMethod", + auto_config AS "autoConfig", + manual_config AS "manualConfig" + FROM numbering_rule_parts + WHERE rule_id = $1 + AND company_code = $2 + ORDER BY part_order + `; + + const partsResult = await pool.query(partsQuery, [ + rule.ruleId, + companyCode === "*" ? rule.companyCode : companyCode, + ]); + + rule.parts = partsResult.rows; + } + + logger.info(`ํ™”๋ฉด์šฉ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ ์™„๋ฃŒ: ${result.rows.length}๊ฐœ`, { + companyCode, + tableName, + }); + + return result.rows; + } catch (error: any) { + logger.error("ํ™”๋ฉด์šฉ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ ์‹คํŒจ", error); + throw error; + } +} +``` + +##### getAvailableRulesForMenu (๊ธฐ์กด ํ•จ์ˆ˜ ์œ ์ง€) + +- ์ฑ„๋ฒˆ๊ทœ์น™ ๊ด€๋ฆฌ ํ™”๋ฉด์—์„œ ์‚ฌ์šฉ +- ๋ณ€๊ฒฝ ์—†์Œ (ํ•˜์œ„ ํ˜ธํ™˜์„ฑ) + +#### 2.2 numberingRuleController.ts ์ˆ˜์ • + +**์‹ ๊ทœ ์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€**: + +```typescript +// GET /api/numbering-rules/available-for-screen?tableName=xxx&menuObjid=xxx +router.get( + "/available-for-screen", + authMiddleware, + async (req: Request, res: Response) => { + try { + const companyCode = req.user!.companyCode; + const { tableName, menuObjid } = req.query; + + if (!tableName) { + return res.status(400).json({ + success: false, + message: "tableName is required", + }); + } + + const rules = await numberingRuleService.getAvailableRulesForScreen( + companyCode, + tableName as string, + menuObjid ? parseInt(menuObjid as string) : undefined + ); + + return res.json({ + success: true, + data: rules, + }); + } catch (error: any) { + logger.error("ํ™”๋ฉด์šฉ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ ์‹คํŒจ", error); + return res.status(500).json({ + success: false, + message: error.message, + }); + } + } +); +``` + +--- + +### Phase 3: ํ”„๋ก ํŠธ์—”๋“œ API ํด๋ผ์ด์–ธํŠธ ์ˆ˜์ • (30๋ถ„) + +#### 3.1 lib/api/numberingRule.ts ์ˆ˜์ • + +**์‹ ๊ทœ ํ•จ์ˆ˜ ์ถ”๊ฐ€**: + +```typescript +/** + * ํ™”๋ฉด์šฉ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ (ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜) + * @param tableName ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ”๋ช… (ํ•„์ˆ˜) + * @param menuObjid ํ˜„์žฌ ๋ฉ”๋‰ด์˜ objid (์„ ํƒ) + * @returns ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ชฉ๋ก + */ +export async function getAvailableNumberingRulesForScreen( + tableName: string, + menuObjid?: number +): Promise> { + try { + const params: any = { tableName }; + if (menuObjid) { + params.menuObjid = menuObjid; + } + + const response = await apiClient.get( + "/numbering-rules/available-for-screen", + { + params, + } + ); + return response.data; + } catch (error: any) { + return { + success: false, + error: error.message || "ํ™”๋ฉด์šฉ ๊ทœ์น™ ์กฐํšŒ ์‹คํŒจ", + }; + } +} +``` + +**๊ธฐ์กด ํ•จ์ˆ˜ ์œ ์ง€**: + +```typescript +// getAvailableNumberingRules (๋ฉ”๋‰ด ๊ธฐ๋ฐ˜) - ํ•˜์œ„ ํ˜ธํ™˜์„ฑ +// ์ฑ„๋ฒˆ๊ทœ์น™ ๊ด€๋ฆฌ ์ปดํฌ๋„ŒํŠธ์—์„œ ๊ณ„์† ์‚ฌ์šฉ +``` + +--- + +### Phase 4: ํ™”๋ฉด๊ด€๋ฆฌ UI ์ˆ˜์ • (30๋ถ„) + +#### 4.1 TextTypeConfigPanel.tsx ์ˆ˜์ • + +**๋ณ€๊ฒฝ ์ „**: + +```typescript +const response = await getAvailableNumberingRules(); +``` + +**๋ณ€๊ฒฝ ํ›„**: + +```typescript +const loadRules = async () => { + setLoadingRules(true); + try { + // ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ”๋ช… ๊ฐ€์ ธ์˜ค๊ธฐ + const screenTableName = getScreenTableName(); // ๊ตฌํ˜„ ํ•„์š” + + if (!screenTableName) { + logger.warn("ํ™”๋ฉด ํ…Œ์ด๋ธ”๋ช…์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"); + setNumberingRules([]); + return; + } + + // ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜ ๊ทœ์น™ ์กฐํšŒ + const response = await getAvailableNumberingRulesForScreen( + screenTableName, + undefined // menuObjid (ํ–ฅํ›„ ํ™•์žฅ ๊ฐ€๋Šฅ) + ); + + if (response.success && response.data) { + setNumberingRules(response.data); + logger.info(`์ฑ„๋ฒˆ ๊ทœ์น™ ${response.data.length}๊ฐœ ๋กœ๋“œ ์™„๋ฃŒ`, { + tableName: screenTableName, + }); + } + } catch (error) { + console.error("์ฑ„๋ฒˆ ๊ทœ์น™ ๋ชฉ๋ก ๋กœ๋“œ ์‹คํŒจ:", error); + setNumberingRules([]); + } finally { + setLoadingRules(false); + } +}; +``` + +**ํ™”๋ฉด ํ…Œ์ด๋ธ”๋ช… ๊ฐ€์ ธ์˜ค๊ธฐ**: + +```typescript +// ScreenDesigner์—์„œ props๋กœ ์ „๋‹ฌ๋ฐ›๊ฑฐ๋‚˜ Context ์‚ฌ์šฉ +const getScreenTableName = (): string | undefined => { + // ๋ฐฉ๋ฒ• 1: Props๋กœ ์ „๋‹ฌ๋ฐ›๊ธฐ (๊ถŒ์žฅ) + return props.screenTableName; + + // ๋ฐฉ๋ฒ• 2: Context์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ + // const { selectedScreen } = useScreenContext(); + // return selectedScreen?.tableName; + + // ๋ฐฉ๋ฒ• 3: ์ƒ์œ„ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ฐพ๊ธฐ + // return component.tableName || selectedScreen?.tableName; +}; +``` + +#### 4.2 ScreenDesigner.tsx ์ˆ˜์ • + +**ํ™”๋ฉด ํ…Œ์ด๋ธ”๋ช…์„ ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ์— ์ „๋‹ฌ**: + +```typescript +// PropertiesPanel์— screenTableName prop ์ถ”๊ฐ€ + + +// PropertiesPanel์—์„œ TextTypeConfigPanel์— ์ „๋‹ฌ + +``` + +--- + +### Phase 5: ์ฑ„๋ฒˆ๊ทœ์น™ ๊ด€๋ฆฌ UI ์ˆ˜์ • (30๋ถ„) + +#### 5.1 NumberingRuleDesigner.tsx ์ˆ˜์ • + +**scope_type ์„ ํƒ UI ์ถ”๊ฐ€**: + +```typescript +
+ + +

+ {config.scopeType === "global" && "๋ชจ๋“  ํ™”๋ฉด์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅ"} + {config.scopeType === "table" && "๊ฐ™์€ ํ…Œ์ด๋ธ”์„ ์‚ฌ์šฉํ•˜๋Š” ํ™”๋ฉด์—์„œ๋งŒ ํ‘œ์‹œ"} + {config.scopeType === "menu" && "์„ ํƒํ•œ ๋ฉ”๋‰ด์—์„œ๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅ"} +

+
+``` + +**์กฐ๊ฑด๋ถ€ ํ•„๋“œ ํ‘œ์‹œ**: + +```typescript +{ + /* table ํƒ€์ž…: ํ…Œ์ด๋ธ”๋ช… ํ•„์ˆ˜ */ +} +{ + config.scopeType === "table" && ( +
+ + updateConfig("tableName", e.target.value)} + placeholder="์˜ˆ: item_info" + className="h-9 text-sm" + /> +
+ ); +} + +{ + /* menu ํƒ€์ž…: ๋ฉ”๋‰ด ์„ ํƒ ํ•„์ˆ˜ */ +} +{ + config.scopeType === "menu" && ( +
+ + +
+ ); +} + +{ + /* global ํƒ€์ž…: ์ถ”๊ฐ€ ์„ค์ • ๋ถˆํ•„์š” */ +} +{ + config.scopeType === "global" && ( +
+

+ ์ด ๊ทœ์น™์€ ๋ชจ๋“  ํ™”๋ฉด์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. +

+
+ ); +} +``` + +#### 5.2 ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์ถ”๊ฐ€ + +```typescript +const validateRuleConfig = (config: NumberingRuleConfig): string | null => { + if (config.scopeType === "table" && !config.tableName) { + return "ํ…Œ์ด๋ธ” ํƒ€์ž…์€ ํ…Œ์ด๋ธ”๋ช…์ด ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."; + } + + if (config.scopeType === "menu" && !config.menuObjid) { + return "๋ฉ”๋‰ด ํƒ€์ž…์€ ๋ฉ”๋‰ด ์„ ํƒ์ด ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."; + } + + if (config.scopeType === "global" && config.tableName) { + return "์ „์—ญ ํƒ€์ž…์€ ํ…Œ์ด๋ธ”๋ช…์„ ์ง€์ •ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."; + } + + return null; +}; +``` + +--- + +## ๐Ÿ“ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ์ž‘์„ฑ + +### ํŒŒ์ผ: `db/migrations/046_update_numbering_rules_scope_type.sql` + +```sql +-- ===================================================== +-- ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ 046: ์ฑ„๋ฒˆ๊ทœ์น™ scope_type ํ™•์žฅ +-- ๋ชฉ์ : ๋ฉ”๋‰ด ๊ธฐ๋ฐ˜ โ†’ ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง ์ง€์› +-- ๋‚ ์งœ: 2025-11-08 +-- ===================================================== + +BEGIN; + +-- 1. ๊ธฐ์กด ์ œ์•ฝ์กฐ๊ฑด ์ œ๊ฑฐ +ALTER TABLE numbering_rules +DROP CONSTRAINT IF EXISTS check_scope_type; + +-- 2. ์ƒˆ๋กœ์šด scope_type ์ œ์•ฝ์กฐ๊ฑด ์ถ”๊ฐ€ (global, table, menu) +ALTER TABLE numbering_rules +ADD CONSTRAINT check_scope_type +CHECK (scope_type IN ('global', 'table', 'menu')); + +-- 3. table ํƒ€์ž… ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์ œ์•ฝ์กฐ๊ฑด +ALTER TABLE numbering_rules +ADD CONSTRAINT check_table_scope_requires_table_name +CHECK ( + (scope_type = 'table' AND table_name IS NOT NULL) + OR scope_type != 'table' +); + +-- 4. global ํƒ€์ž… ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์ œ์•ฝ์กฐ๊ฑด +ALTER TABLE numbering_rules +ADD CONSTRAINT check_global_scope_no_table_name +CHECK ( + (scope_type = 'global' AND table_name IS NULL) + OR scope_type != 'global' +); + +-- 5. menu ํƒ€์ž… ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์ œ์•ฝ์กฐ๊ฑด +ALTER TABLE numbering_rules +ADD CONSTRAINT check_menu_scope_requires_menu_objid +CHECK ( + (scope_type = 'menu' AND menu_objid IS NOT NULL) + OR scope_type != 'menu' +); + +-- 6. ๊ธฐ์กด ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ +-- global ๊ทœ์น™ ์ค‘ table_name์ด ์žˆ๋Š” ๊ฒƒ โ†’ table ํƒ€์ž…์œผ๋กœ ๋ณ€๊ฒฝ +-- ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ: ๋ชจ๋“  ํšŒ์‚ฌ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๋ณ€ํ™˜ +UPDATE numbering_rules +SET scope_type = 'table' +WHERE scope_type = 'global' + AND table_name IS NOT NULL; +-- ์ฃผ์˜: company_code ํ•„ํ„ฐ ์—†์Œ (๋ชจ๋“  ํšŒ์‚ฌ ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜) + +-- 7. ์ธ๋ฑ์Šค ์ตœ์ ํ™” (๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ํ•„์ˆ˜!) +-- ๊ธฐ์กด ์ธ๋ฑ์Šค ์ œ๊ฑฐ +DROP INDEX IF EXISTS idx_numbering_rules_table; + +-- ์ƒˆ๋กœ์šด ๋ณตํ•ฉ ์ธ๋ฑ์Šค ์ƒ์„ฑ (ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜ ์กฐํšŒ ์ตœ์ ํ™”) +-- company_code ํฌํ•จ์œผ๋กœ ํšŒ์‚ฌ๋ณ„ ๊ฒฉ๋ฆฌ ์„ฑ๋Šฅ ํ–ฅ์ƒ +CREATE INDEX IF NOT EXISTS idx_numbering_rules_scope_table +ON numbering_rules(scope_type, table_name, company_code); + +-- ๋ฉ”๋‰ด ๊ธฐ๋ฐ˜ ์กฐํšŒ ์ตœ์ ํ™” +-- company_code ํฌํ•จ์œผ๋กœ ํšŒ์‚ฌ๋ณ„ ๊ฒฉ๋ฆฌ ์„ฑ๋Šฅ ํ–ฅ์ƒ +CREATE INDEX IF NOT EXISTS idx_numbering_rules_scope_menu +ON numbering_rules(scope_type, menu_objid, company_code); + +-- 8. ํ†ต๊ณ„ ์ •๋ณด ์—…๋ฐ์ดํŠธ +ANALYZE numbering_rules; + +COMMIT; + +-- ===================================================== +-- ๋กค๋ฐฑ ์Šคํฌ๋ฆฝํŠธ (๋ฌธ์ œ ๋ฐœ์ƒ ์‹œ ์‹คํ–‰) +-- ===================================================== +/* +BEGIN; + +-- ์ œ์•ฝ์กฐ๊ฑด ์ œ๊ฑฐ +ALTER TABLE numbering_rules DROP CONSTRAINT IF EXISTS check_scope_type; +ALTER TABLE numbering_rules DROP CONSTRAINT IF EXISTS check_table_scope_requires_table_name; +ALTER TABLE numbering_rules DROP CONSTRAINT IF EXISTS check_global_scope_no_table_name; +ALTER TABLE numbering_rules DROP CONSTRAINT IF EXISTS check_menu_scope_requires_menu_objid; + +-- ์ธ๋ฑ์Šค ์ œ๊ฑฐ +DROP INDEX IF EXISTS idx_numbering_rules_scope_table; +DROP INDEX IF EXISTS idx_numbering_rules_scope_menu; + +-- ๋ฐ์ดํ„ฐ ๋กค๋ฐฑ (table โ†’ global) +UPDATE numbering_rules +SET scope_type = 'global' +WHERE scope_type = 'table'; + +-- ๊ธฐ์กด ์ œ์•ฝ์กฐ๊ฑด ๋ณต์› +ALTER TABLE numbering_rules +ADD CONSTRAINT check_scope_type +CHECK (scope_type IN ('global', 'menu')); + +-- ๊ธฐ์กด ์ธ๋ฑ์Šค ๋ณต์› +CREATE INDEX IF NOT EXISTS idx_numbering_rules_table +ON numbering_rules(table_name, column_name); + +COMMIT; +*/ +``` + +--- + +## โœ… ๊ฒ€์ฆ ๊ณ„ํš + +### 1. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ฒ€์ฆ + +#### 1.1 ์ œ์•ฝ์กฐ๊ฑด ํ™•์ธ + +```sql +-- scope_type ์ œ์•ฝ์กฐ๊ฑด ํ™•์ธ +SELECT conname, pg_get_constraintdef(oid) +FROM pg_constraint +WHERE conrelid = 'numbering_rules'::regclass + AND conname LIKE '%scope%'; + +-- ์˜ˆ์ƒ ๊ฒฐ๊ณผ: +-- check_scope_type | CHECK (scope_type IN ('global', 'table', 'menu')) +-- check_table_scope_requires_table_name +-- check_global_scope_no_table_name +-- check_menu_scope_requires_menu_objid +``` + +#### 1.2 ์ธ๋ฑ์Šค ํ™•์ธ + +```sql +-- ์ธ๋ฑ์Šค ๋ชฉ๋ก ํ™•์ธ +SELECT indexname, indexdef +FROM pg_indexes +WHERE tablename = 'numbering_rules' +ORDER BY indexname; + +-- ์˜ˆ์ƒ ๊ฒฐ๊ณผ: +-- idx_numbering_rules_scope_table +-- idx_numbering_rules_scope_menu +``` + +#### 1.3 ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ™•์ธ + +```sql +-- scope_type๋ณ„ ๊ฐœ์ˆ˜ +SELECT scope_type, COUNT(*) as count +FROM numbering_rules +GROUP BY scope_type; + +-- ํ…Œ์ด๋ธ”๋ช…์ด ์žˆ๋Š”๋ฐ global์ธ ๊ทœ์น™ (์—†์–ด์•ผ ์ •์ƒ) +SELECT rule_id, rule_name, scope_type, table_name +FROM numbering_rules +WHERE scope_type = 'global' AND table_name IS NOT NULL; +``` + +### 2. API ๊ฒ€์ฆ + +#### 2.1 ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜ ์กฐํšŒ ํ…Œ์ŠคํŠธ + +```bash +# ํŠน์ • ํ…Œ์ด๋ธ”์˜ ๊ทœ์น™ ์กฐํšŒ +curl -X GET "http://localhost:8080/api/numbering-rules/available-for-screen?tableName=item_info" \ + -H "Authorization: Bearer {token}" + +# ์˜ˆ์ƒ ์‘๋‹ต: +# - scope_type='table' && table_name='item_info' +# - scope_type='global' && table_name IS NULL +``` + +#### 2.2 ์šฐ์„ ์ˆœ์œ„ ํ…Œ์ŠคํŠธ + +```sql +-- ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์‚ฝ์ž… +INSERT INTO numbering_rules (rule_id, rule_name, scope_type, table_name, company_code) +VALUES + ('RULE_GLOBAL', '์ „์—ญ๊ทœ์น™', 'global', NULL, 'TEST_CO'), + ('RULE_TABLE', 'ํ…Œ์ด๋ธ”๊ทœ์น™', 'table', 'item_info', 'TEST_CO'), + ('RULE_MENU', '๋ฉ”๋‰ด๊ทœ์น™', 'menu', NULL, 'TEST_CO'); + +-- API ํ˜ธ์ถœ ์‹œ ์ˆœ์„œ ํ™•์ธ (menu > table > global) +``` + +### 3. ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๊ฒ€์ฆ (ํ•„์ˆ˜!) + +#### 3.1 ํšŒ์‚ฌ๋ณ„ ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ ํ™•์ธ + +```sql +-- ํšŒ์‚ฌ A ๊ทœ์น™ ์ƒ์„ฑ +INSERT INTO numbering_rules (rule_id, rule_name, scope_type, table_name, company_code) +VALUES ('RULE_A', 'ํšŒ์‚ฌA๊ทœ์น™', 'table', 'item_info', 'COMPANY_A'); + +-- ํšŒ์‚ฌ B ๊ทœ์น™ ์ƒ์„ฑ +INSERT INTO numbering_rules (rule_id, rule_name, scope_type, table_name, company_code) +VALUES ('RULE_B', 'ํšŒ์‚ฌB๊ทœ์น™', 'table', 'item_info', 'COMPANY_B'); + +-- ํšŒ์‚ฌ A๋กœ ๋กœ๊ทธ์ธ โ†’ API ํ˜ธ์ถœ +-- ์˜ˆ์ƒ: RULE_A๋งŒ ์กฐํšŒ, RULE_B๋Š” ๋ณด์ด์ง€ ์•Š์Œ โœ… + +-- ํšŒ์‚ฌ B๋กœ ๋กœ๊ทธ์ธ โ†’ API ํ˜ธ์ถœ +-- ์˜ˆ์ƒ: RULE_B๋งŒ ์กฐํšŒ, RULE_A๋Š” ๋ณด์ด์ง€ ์•Š์Œ โœ… +``` + +#### 3.2 ์ตœ๊ณ  ๊ด€๋ฆฌ์ž ๊ฐ€์‹œ์„ฑ ์ œํ•œ ํ™•์ธ + +```sql +-- ์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ „์šฉ ๊ทœ์น™ ์ƒ์„ฑ +INSERT INTO numbering_rules (rule_id, rule_name, scope_type, table_name, company_code) +VALUES ('RULE_SUPER', '์ตœ๊ณ ๊ด€๋ฆฌ์ž๊ทœ์น™', 'global', NULL, '*'); + +-- ์ผ๋ฐ˜ ํšŒ์‚ฌ๋กœ ๋กœ๊ทธ์ธ โ†’ API ํ˜ธ์ถœ +-- ์˜ˆ์ƒ: RULE_SUPER๋Š” ๋ณด์ด์ง€ ์•Š์Œ โœ… (company_code='*' ์ œ์™ธ) + +-- ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋กœ ๋กœ๊ทธ์ธ โ†’ API ํ˜ธ์ถœ +-- ์˜ˆ์ƒ: ์ผ๋ฐ˜ ํšŒ์‚ฌ ๊ทœ์น™๋“ค๋งŒ ์กฐํšŒ (RULE_SUPER ์ œ์™ธ) โœ… +``` + +#### 3.3 company_code ํ•„ํ„ฐ๋ง ๋กœ๊ทธ ํ™•์ธ + +```typescript +// ๋ฐฑ์—”๋“œ ๋กœ๊ทธ์—์„œ ํ™•์ธ +logger.info("ํ™”๋ฉด์šฉ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ ์™„๋ฃŒ", { + companyCode: "COMPANY_A", // โœ… ๋กœ๊ทธ์— ํšŒ์‚ฌ ์ฝ”๋“œ ๊ธฐ๋ก + tableName: "item_info", + rowCount: 5, +}); + +// ์ตœ๊ณ  ๊ด€๋ฆฌ์ž ๋กœ๊ทธ +logger.info("์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ์ผ๋ฐ˜ ํšŒ์‚ฌ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ (company_code != '*')"); +``` + +### 4. UI ๊ฒ€์ฆ + +#### 4.1 ํ™”๋ฉด๊ด€๋ฆฌ ํ…Œ์ŠคํŠธ + +1. ํ™”๋ฉด ์ƒ์„ฑ (ํ…Œ์ด๋ธ”: `item_info`) +2. ํ…์ŠคํŠธ ํ•„๋“œ ์ถ”๊ฐ€ +3. ์ž๋™ ์ž…๋ ฅ > ์ฑ„๋ฒˆ๊ทœ์น™ ์„ ํƒ +4. **ํ™•์ธ์‚ฌํ•ญ**: + - `table_name='item_info'`์ธ ๊ทœ์น™ ํ‘œ์‹œ โœ… + - `scope_type='global'`์ธ ๊ทœ์น™ ํ‘œ์‹œ โœ… + - ๋‹ค๋ฅธ ํ…Œ์ด๋ธ” ๊ทœ์น™์€ ๋ฏธํ‘œ์‹œ โœ… + - **๋‹ค๋ฅธ ํšŒ์‚ฌ ๊ทœ์น™์€ ๋ฏธํ‘œ์‹œ** โœ… (๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ) + +#### 4.2 ์ฑ„๋ฒˆ๊ทœ์น™ ๊ด€๋ฆฌ ํ…Œ์ŠคํŠธ + +1. ์ƒˆ ๊ทœ์น™ ์ƒ์„ฑ +2. ์ ์šฉ ๋ฒ”์œ„ ์„ ํƒ: "ํ…Œ์ด๋ธ”๋ณ„" +3. ํ…Œ์ด๋ธ”๋ช… ์ž…๋ ฅ: `item_info` +4. ์ €์žฅ โ†’ ํ™”๋ฉด๊ด€๋ฆฌ์—์„œ ๋ฐ”๋กœ ํ‘œ์‹œ ํ™•์ธ โœ… + +#### 4.3 ์šฐ์„ ์ˆœ์œ„ ํ…Œ์ŠคํŠธ + +1. ๊ฐ™์€ ํ…Œ์ด๋ธ”์— ๋Œ€ํ•ด 3๊ฐ€์ง€ scope_type ๊ทœ์น™ ์ƒ์„ฑ +2. ํ™”๋ฉด๊ด€๋ฆฌ์—์„œ ์กฐํšŒ ์‹œ menu๊ฐ€ ์ตœ์ƒ๋‹จ์— ํ‘œ์‹œ ํ™•์ธ โœ… + +--- + +## ๐Ÿšจ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋ฐ ์—ฃ์ง€ ์ผ€์ด์Šค + +### 1. ํ…Œ์ด๋ธ”๋ช…์ด ์—†๋Š” ํ™”๋ฉด + +```typescript +// TextTypeConfigPanel.tsx +if (!screenTableName) { + logger.warn("ํ™”๋ฉด์— ํ…Œ์ด๋ธ”์ด ์ง€์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค"); + + // global ๊ทœ์น™๋งŒ ์กฐํšŒ + const response = await getAvailableNumberingRules(); + setNumberingRules(response.data || []); + return; +} +``` + +### 2. ๊ทœ์น™์ด ํ•˜๋‚˜๋„ ์—†๋Š” ๊ฒฝ์šฐ + +```typescript +if (numberingRules.length === 0) { + return ( +
+ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ฑ„๋ฒˆ๊ทœ์น™์ด ์—†์Šต๋‹ˆ๋‹ค. +
+ ์ฑ„๋ฒˆ๊ทœ์น™ ๊ด€๋ฆฌ์—์„œ ๊ทœ์น™์„ ๋จผ์ € ์ƒ์„ฑํ•ด์ฃผ์„ธ์š”. +
+ ); +} +``` + +### 3. ๋™์ผ ์šฐ์„ ์ˆœ์œ„์— ์—ฌ๋Ÿฌ ๊ทœ์น™ + +```sql +-- created_at DESC๋กœ ์ •๋ ฌ๋˜๋ฏ€๋กœ ์ตœ์‹  ๊ทœ์น™ ์šฐ์„  +ORDER BY + CASE scope_type + WHEN 'menu' THEN 1 + WHEN 'table' THEN 2 + WHEN 'global' THEN 3 + END, + created_at DESC -- ๊ฐ™์€ scope_type์ด๋ฉด ์ตœ์‹  ๊ทœ์น™ ์šฐ์„  +``` + +### 4. ์ตœ๊ณ  ๊ด€๋ฆฌ์ž ํŠน๋ณ„ ์ฒ˜๋ฆฌ + +```typescript +// company_code="*"์ธ ๊ฒฝ์šฐ ๋ชจ๋“  ๊ทœ์น™ ์กฐํšŒ ๊ฐ€๋Šฅ +if (companyCode === "*") { + // ๋ชจ๋“  ํšŒ์‚ฌ์˜ ๊ทœ์น™ ํ‘œ์‹œ (๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์˜ˆ์™ธ) +} +``` + +--- + +## ๐Ÿ“Š ์„ฑ๋Šฅ ์ตœ์ ํ™” + +### 1. ์ธ๋ฑ์Šค ์ „๋žต + +```sql +-- ๋ณตํ•ฉ ์ธ๋ฑ์Šค๋กœ WHERE + ORDER BY ์ตœ์ ํ™” +CREATE INDEX idx_numbering_rules_scope_table +ON numbering_rules(scope_type, table_name, company_code); + +CREATE INDEX idx_numbering_rules_scope_menu +ON numbering_rules(scope_type, menu_objid, company_code); +``` + +### 2. ์ฟผ๋ฆฌ ํ”Œ๋žœ ํ™•์ธ + +```sql +EXPLAIN ANALYZE +SELECT * FROM numbering_rules +WHERE company_code = 'TEST_CO' + AND ( + (scope_type = 'table' AND table_name = 'item_info') + OR (scope_type = 'global' AND table_name IS NULL) + ) +ORDER BY + CASE scope_type + WHEN 'menu' THEN 1 + WHEN 'table' THEN 2 + WHEN 'global' THEN 3 + END; + +-- Index Scan ํ™•์ธ (Seq Scan์ด๋ฉด ์ธ๋ฑ์Šค ์ถ”๊ฐ€ ํ•„์š”) +``` + +### 3. ์บ์‹ฑ ์ „๋žต (ํ–ฅํ›„ ๊ณ ๋ ค) + +```typescript +// ์ž์ฃผ ์กฐํšŒ๋˜๋Š” ๊ทœ์น™์€ ๋ฉ”๋ชจ๋ฆฌ ์บ์‹ฑ +const ruleCache = new Map(); + +async function getAvailableRulesWithCache( + tableName: string +): Promise { + const cacheKey = `rules:${tableName}`; + + if (ruleCache.has(cacheKey)) { + return ruleCache.get(cacheKey)!; + } + + const rules = await getAvailableRulesForScreen(tableName); + ruleCache.set(cacheKey, rules); + + return rules; +} +``` + +--- + +## ๐Ÿ“… ๊ตฌํ˜„ ์ผ์ • + +| Phase | ์ž‘์—… ๋‚ด์šฉ | ์˜ˆ์ƒ ์‹œ๊ฐ„ | ๋‹ด๋‹น์ž | +| -------- | --------------------- | -------------- | -------- | +| Phase 1 | DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ | 30๋ถ„ | Backend | +| Phase 2 | ๋ฐฑ์—”๋“œ API ์ˆ˜์ • | 1์‹œ๊ฐ„ | Backend | +| Phase 3 | ํ”„๋ก ํŠธ API ํด๋ผ์ด์–ธํŠธ | 30๋ถ„ | Frontend | +| Phase 4 | ํ™”๋ฉด๊ด€๋ฆฌ UI ์ˆ˜์ • | 30๋ถ„ | Frontend | +| Phase 5 | ์ฑ„๋ฒˆ๊ทœ์น™ UI ์ˆ˜์ • | 30๋ถ„ | Frontend | +| ๊ฒ€์ฆ | ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ | 1์‹œ๊ฐ„ | All | +| **์ด๊ณ„** | | **4์‹œ๊ฐ„ 30๋ถ„** | | + +--- + +## ๐Ÿ”„ ํ•˜์œ„ ํ˜ธํ™˜์„ฑ + +### ๊ธฐ์กด ๊ธฐ๋Šฅ ์œ ์ง€ + +1. โœ… `getAvailableNumberingRules()` ํ•จ์ˆ˜ ์œ ์ง€ (๋ฉ”๋‰ด ๊ธฐ๋ฐ˜) +2. โœ… ๊ธฐ์กด `scope_type='menu'` ๊ทœ์น™ ์ •์ƒ ๋™์ž‘ +3. โœ… ์ฑ„๋ฒˆ๊ทœ์น™ ๊ด€๋ฆฌ ํ™”๋ฉด ์ •์ƒ ๋™์ž‘ + +### ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์˜ํ–ฅ + +- โš ๏ธ `scope_type='global'` + `table_name` ์žˆ๋Š” ๊ทœ์น™ โ†’ `'table'`๋กœ ์ž๋™ ๋ณ€๊ฒฝ +- โœ… ๊ธฐ์กด ๋™์ž‘ ์œ ์ง€ (์ž๋™ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜) +- โœ… ์‚ฌ์šฉ์ž ์žฌ์„ค์ • ๋ถˆํ•„์š” + +--- + +## ๐Ÿ“– ์‚ฌ์šฉ์ž ๊ฐ€์ด๋“œ + +### ๊ทœ์น™ ์ƒ์„ฑ ์‹œ ๊ถŒ์žฅ์‚ฌํ•ญ + +#### ์–ธ์ œ global์„ ์‚ฌ์šฉํ•˜๋‚˜์š”? + +- ํšŒ์‚ฌ ์ „์ฒด์—์„œ ๊ณตํ†ต์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ์ฑ„๋ฒˆ ๊ทœ์น™ +- ์˜ˆ: "๊ณต์ง€์‚ฌํ•ญ ๋ฒˆํ˜ธ", "๊ณตํ†ต ๋ฌธ์„œ ๋ฒˆํ˜ธ" + +#### ์–ธ์ œ table์„ ์‚ฌ์šฉํ•˜๋‚˜์š”? (๊ถŒ์žฅ) + +- ํŠน์ • ํ…Œ์ด๋ธ”์˜ ๋ฐ์ดํ„ฐ์— ์ ์šฉ๋˜๋Š” ๊ทœ์น™ +- ์˜ˆ: `item_info` ํ…Œ์ด๋ธ”์˜ "ํ’ˆ๋ชฉ ์ฝ”๋“œ" +- **๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ ์ด ๋ฐฉ์‹ ์‚ฌ์šฉ** + +#### ์–ธ์ œ menu๋ฅผ ์‚ฌ์šฉํ•˜๋‚˜์š”? + +- ๊ฐ™์€ ํ…Œ์ด๋ธ”์ด๋ผ๋„ ๋ฉ”๋‰ด๋ณ„๋กœ ๋‹ค๋ฅธ ์ฑ„๋ฒˆ ๋ฐฉ์‹ +- ์˜ˆ: "์˜์—…ํŒ€ ํ’ˆ๋ชฉ ์ฝ”๋“œ" vs "๊ตฌ๋งคํŒ€ ํ’ˆ๋ชฉ ์ฝ”๋“œ" + +--- + +## ๐ŸŽ‰ ๊ธฐ๋Œ€ ํšจ๊ณผ + +### 1. ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๊ฐœ์„  + +- โœ… ํ™”๋ฉด๊ด€๋ฆฌ์—์„œ ์ฑ„๋ฒˆ๊ทœ์น™์ด ์ž๋™์œผ๋กœ ํ‘œ์‹œ +- โœ… ๋ฉ”๋‰ด ๊ตฌ์กฐ๋ฅผ ๋ชฐ๋ผ๋„ ๊ทœ์น™ ์„ค์ • ๊ฐ€๋Šฅ +- โœ… ๊ฐ™์€ ํ…Œ์ด๋ธ” ํ™”๋ฉด์— ๊ทœ์น™ ์žฌ์‚ฌ์šฉ ์ž๋™ + +### 2. ์œ ์ง€๋ณด์ˆ˜์„ฑ ํ–ฅ์ƒ + +- โœ… ๋ฉ”๋‰ด ๊ตฌ์กฐ ๋ณ€๊ฒฝ ์‹œ ๊ทœ์น™ ์žฌ์„ค์ • ๋ถˆํ•„์š” +- โœ… ํ…Œ์ด๋ธ” ์ค‘์‹ฌ ์„ค๊ณ„๋กœ ์ง๊ด€์  +- โœ… ์ฝ”๋“œ ๋ณต์žก๋„ ๊ฐ์†Œ + +### 3. ํ™•์žฅ์„ฑ ํ™•๋ณด + +- โœ… ํ–ฅํ›„ scope_type ์ถ”๊ฐ€ ๊ฐ€๋Šฅ +- โœ… ๋‹ค์ค‘ ํ…Œ์ด๋ธ” ์ง€์› ๊ฐ€๋Šฅ +- โœ… ์กฐ๊ฑด๋ถ€ ๊ทœ์น™ ํ™•์žฅ ๊ฐ€๋Šฅ + +--- + +## ๐Ÿ“ž ์—ฐ๋ฝ์ฒ˜ + +- **์ž‘์„ฑ์ž**: ๊ฐœ๋ฐœํŒ€ +- **์ž‘์„ฑ์ผ**: 2025-11-08 +- **๋ฒ„์ „**: 1.0.0 +- **์ƒํƒœ**: ๊ณ„ํš ์ˆ˜๋ฆฝ ์™„๋ฃŒ โœ… + +--- + +## ๋‹ค์Œ ๋‹จ๊ณ„ + +1. โœ… ๊ณ„ํš์„œ ๊ฒ€ํ†  ๋ฐ ์Šน์ธ +2. โฌœ Phase 1 ์‹คํ–‰ (DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜) +3. โฌœ Phase 2 ์‹คํ–‰ (๋ฐฑ์—”๋“œ ์ˆ˜์ •) +4. โฌœ Phase 3-5 ์‹คํ–‰ (ํ”„๋ก ํŠธ์—”๋“œ ์ˆ˜์ •) +5. โฌœ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ +6. โฌœ ์šด์˜ ๋ฐฐํฌ + +**์‹œ์ž‘ ์ค€๋น„ ์™„๋ฃŒ!** ๐Ÿš€ + +--- + +## ๐Ÿ”’ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๋ณด์•ˆ ์ตœ์ข… ํ™•์ธ + +### โœ… ์™„๋ฒฝํ•˜๊ฒŒ ์ ์šฉ๋จ + +#### 1. **๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ ˆ๋ฒจ** + +```sql +-- โœ… company_code ์ปฌ๋Ÿผ ํ•„์ˆ˜ (NOT NULL) +-- โœ… ์™ธ๋ž˜ํ‚ค ์ œ์•ฝ์กฐ๊ฑด (company_info ์ฐธ์กฐ) +-- โœ… ๋ณตํ•ฉ ์ธ๋ฑ์Šค์— company_code ํฌํ•จ +CREATE INDEX idx_numbering_rules_scope_table +ON numbering_rules(scope_type, table_name, company_code); +``` + +#### 2. **API ๋ ˆ๋ฒจ** + +```typescript +// โœ… ์ผ๋ฐ˜ ํšŒ์‚ฌ: WHERE company_code = $1 +WHERE company_code = $1 + AND (scope_type = 'table' AND table_name = $2) + +// โœ… ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: WHERE company_code != '*' +// (์ผ๋ฐ˜ ํšŒ์‚ฌ ๋ฐ์ดํ„ฐ๋งŒ ์กฐํšŒ, ์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ „์šฉ ๋ฐ์ดํ„ฐ ์ œ์™ธ) +WHERE company_code != '*' + AND (scope_type = 'table' AND table_name = $2) + +// โœ… ํŒŒํŠธ ์กฐํšŒ: WHERE company_code = $2 +WHERE rule_id = $1 AND company_code = $2 +``` + +#### 3. **๋กœ๊น… ๋ ˆ๋ฒจ** + +```typescript +// โœ… ๋ชจ๋“  ๋กœ๊ทธ์— companyCode ํฌํ•จ (๊ฐ์‚ฌ ์ถ”์ ) +logger.info("ํ™”๋ฉด์šฉ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ ์™„๋ฃŒ", { + companyCode, // ํ•„์ˆ˜! + tableName, + rowCount, +}); +``` + +#### 4. **๊ฒ€์ฆ ๋ ˆ๋ฒจ** + +```sql +-- โœ… ํšŒ์‚ฌ A ๊ทœ์น™์€ ํšŒ์‚ฌ B์—์„œ ์ ˆ๋Œ€ ์•ˆ ๋ณด์ž„ +-- โœ… company_code='*' ๊ทœ์น™์€ ์ผ๋ฐ˜ ํšŒ์‚ฌ์—์„œ ์•ˆ ๋ณด์ž„ +-- โœ… ๋กœ๊ทธ์— ํšŒ์‚ฌ ์ฝ”๋“œ ๊ธฐ๋ก์œผ๋กœ ์ถ”์  ๊ฐ€๋Šฅ +``` + +### ๐Ÿ›ก๏ธ ๋ณด์•ˆ ์›์น™ ์ค€์ˆ˜ + +1. **์™„์ „ํ•œ ๊ฒฉ๋ฆฌ**: ํšŒ์‚ฌ๋ณ„ ๋ฐ์ดํ„ฐ 100% ๊ฒฉ๋ฆฌ +2. **์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์˜ˆ์™ธ**: `company_code='*'` ๋ฐ์ดํ„ฐ๋Š” ์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ „์šฉ +3. **๊ฐ์‚ฌ ์ถ”์ **: ๋ชจ๋“  ์กฐํšŒ์— companyCode ๋กœ๊น… +4. **์„ฑ๋Šฅ ์ตœ์ ํ™”**: ์ธ๋ฑ์Šค์— company_code ํฌํ•จ +5. **๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ**: ์™ธ๋ž˜ํ‚ค ์ œ์•ฝ์กฐ๊ฑด์œผ๋กœ ๋ณด์žฅ + +### โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ + +- โŒ ์ ˆ๋Œ€ `company_code` ํ•„ํ„ฐ ๋ˆ„๋ฝ ๊ธˆ์ง€ +- โŒ ํด๋ผ์ด์–ธํŠธ์—์„œ `company_code` ์ „๋‹ฌ ๊ธˆ์ง€ (์„œ๋ฒ„์—์„œ๋งŒ ์‚ฌ์šฉ) +- โŒ SQL ์ธ์ ์…˜ ๋ฐฉ์ง€ (ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐ”์ธ๋”ฉ ํ•„์ˆ˜) +- โœ… ๋ชจ๋“  ์ฟผ๋ฆฌ์— `company_code` ์กฐ๊ฑด ํฌํ•จ +- โœ… ๋กœ๊ทธ์— `companyCode` ํ•„์ˆ˜ ๊ธฐ๋ก + +**๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ๊ฐ€ ์™„๋ฒฝํ•˜๊ฒŒ ์ ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!** ๐Ÿ” diff --git a/์ฑ„๋ฒˆ๊ทœ์น™_ํ…Œ์ด๋ธ”๊ธฐ๋ฐ˜_ํ•„ํ„ฐ๋ง_๊ตฌํ˜„_์™„๋ฃŒ_๋ณด๊ณ ์„œ.md b/์ฑ„๋ฒˆ๊ทœ์น™_ํ…Œ์ด๋ธ”๊ธฐ๋ฐ˜_ํ•„ํ„ฐ๋ง_๊ตฌํ˜„_์™„๋ฃŒ_๋ณด๊ณ ์„œ.md new file mode 100644 index 00000000..736000f7 --- /dev/null +++ b/์ฑ„๋ฒˆ๊ทœ์น™_ํ…Œ์ด๋ธ”๊ธฐ๋ฐ˜_ํ•„ํ„ฐ๋ง_๊ตฌํ˜„_์™„๋ฃŒ_๋ณด๊ณ ์„œ.md @@ -0,0 +1,428 @@ +# ์ฑ„๋ฒˆ๊ทœ์น™ ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง ์‹œ์Šคํ…œ ๊ตฌํ˜„ ์™„๋ฃŒ ๋ณด๊ณ ์„œ + +## ๐Ÿ“… ์™„๋ฃŒ ์ผ์‹œ +- **๋‚ ์งœ**: 2025-11-08 +- **์†Œ์š” ์‹œ๊ฐ„**: ์•ฝ 3์‹œ๊ฐ„ 30๋ถ„ (๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ ๋ฏธ์™„๋ฃŒ) + +--- + +## ๐ŸŽฏ ๋ชฉ์  + +ํ™”๋ฉด๊ด€๋ฆฌ ์‹œ์Šคํ…œ์—์„œ ์ฑ„๋ฒˆ๊ทœ์น™์ด ํ‘œ์‹œ๋˜์ง€ ์•Š๋Š” ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด **๋ฉ”๋‰ด ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง**์—์„œ **ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง**์œผ๋กœ ์ „ํ™˜ + +### ๊ธฐ์กด ๋ฌธ์ œ์  +1. ํ™”๋ฉด๊ด€๋ฆฌ์—์„œ `menuObjid` ์ •๋ณด๊ฐ€ ์—†์–ด `scope_type='menu'` ๊ทœ์น™์„ ๋ณผ ์ˆ˜ ์—†์Œ +2. ๋ฉ”๋‰ด ๊ตฌ์กฐ ๋ณ€๊ฒฝ ์‹œ ์ฑ„๋ฒˆ๊ทœ์น™ ์žฌ์„ค์ • ํ•„์š” +3. ๊ฐ™์€ ํ…Œ์ด๋ธ”์„ ์‚ฌ์šฉํ•˜๋Š” ํ™”๋ฉด์ธ๋ฐ๋„ ๊ทœ์น™์ด ๋ณด์ด์ง€ ์•Š์Œ + +### ํ•ด๊ฒฐ ๋ฐฉ์•ˆ +- **ํ…Œ์ด๋ธ”๋ช… ๊ธฐ๋ฐ˜ ์ž๋™ ๋งค์นญ**: ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ”๊ณผ ๊ทœ์น™์˜ ํ…Œ์ด๋ธ”์ด ๊ฐ™์œผ๋ฉด ์ž๋™์œผ๋กœ ํ‘œ์‹œ +- **ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์ ‘๊ทผ**: `scope_type`์„ `'global'`, `'table'`, `'menu'` ์„ธ ๊ฐ€์ง€๋กœ ํ™•์žฅ +- **์šฐ์„ ์ˆœ์œ„ ํ•„ํ„ฐ๋ง**: menu > table > global ์ˆœ์œผ๋กœ ๊ทœ์น™ ํ‘œ์‹œ + +--- + +## โœ… ๊ตฌํ˜„ ์™„๋ฃŒ ํ•ญ๋ชฉ + +### Phase 1: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ (์ค€๋น„ ์™„๋ฃŒ) + +#### ํŒŒ์ผ ์ƒ์„ฑ +- โœ… `/db/migrations/046_update_numbering_rules_scope_type.sql` +- โœ… `/db/migrations/RUN_046_MIGRATION.md` + +#### ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋‚ด์šฉ +- `scope_type` ์ œ์•ฝ์กฐ๊ฑด ํ™•์žฅ: `'global'`, `'table'`, `'menu'` +- ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์ œ์•ฝ์กฐ๊ฑด ์ถ”๊ฐ€: + - `check_table_scope_requires_table_name`: table ํƒ€์ž…์€ table_name ํ•„์ˆ˜ + - `check_global_scope_no_table_name`: global ํƒ€์ž…์€ table_name ์—†์–ด์•ผ ํ•จ + - `check_menu_scope_requires_menu_objid`: menu ํƒ€์ž…์€ menu_objid ํ•„์ˆ˜ +- ๊ธฐ์กด ๋ฐ์ดํ„ฐ ์ž๋™ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜: `global` + `table_name` โ†’ `table` ํƒ€์ž…์œผ๋กœ ๋ณ€๊ฒฝ +- ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์ธ๋ฑ์Šค ์ตœ์ ํ™”: + - `idx_numbering_rules_scope_table (scope_type, table_name, company_code)` + - `idx_numbering_rules_scope_menu (scope_type, menu_objid, company_code)` + +#### ์ƒํƒœ +โš ๏ธ **๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ์ค€๋น„ ์™„๋ฃŒ, ์‹คํ–‰ ๋Œ€๊ธฐ ์ค‘** +- Docker ์ปจํ…Œ์ด๋„ˆ ์—ฐ๊ฒฐ ๋ฌธ์ œ๋กœ ์ˆ˜๋™ ์‹คํ–‰ ํ•„์š” +- ์‹คํ–‰ ๊ฐ€์ด๋“œ๋Š” `RUN_046_MIGRATION.md` ์ฐธ๊ณ  + +--- + +### Phase 2: ๋ฐฑ์—”๋“œ API ์ˆ˜์ • โœ… + +#### 2.1 numberingRuleService.ts +- โœ… `getAvailableRulesForScreen()` ํ•จ์ˆ˜ ์ถ”๊ฐ€ + - ํŒŒ๋ผ๋ฏธํ„ฐ: `companyCode`, `tableName` (ํ•„์ˆ˜), `menuObjid` (์„ ํƒ) + - ์šฐ์„ ์ˆœ์œ„ ํ•„ํ„ฐ๋ง: menu > table > global + - ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์™„๋ฒฝ ์ง€์› + +**์ฃผ์š” SQL ์ฟผ๋ฆฌ:** +```sql +SELECT * FROM numbering_rules +WHERE company_code = $1 + AND ( + (scope_type = 'menu' AND menu_objid = $2) + OR (scope_type = 'table' AND table_name = $3) + OR (scope_type = 'global' AND table_name IS NULL) + ) +ORDER BY + CASE scope_type + WHEN 'menu' THEN 1 + WHEN 'table' THEN 2 + WHEN 'global' THEN 3 + END, + created_at DESC +``` + +#### 2.2 numberingRuleController.ts +- โœ… `GET /api/numbering-rules/available-for-screen` ์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€ + - Query Parameters: `tableName` (ํ•„์ˆ˜), `menuObjid` (์„ ํƒ) + - tableName ๊ฒ€์ฆ ๋กœ์ง ํฌํ•จ + - ์ƒ์„ธ ๋กœ๊ทธ ๊ธฐ๋ก + +--- + +### Phase 3: ํ”„๋ก ํŠธ์—”๋“œ API ํด๋ผ์ด์–ธํŠธ ์ˆ˜์ • โœ… + +#### lib/api/numberingRule.ts +- โœ… `getAvailableNumberingRulesForScreen()` ํ•จ์ˆ˜ ์ถ”๊ฐ€ + - ํŒŒ๋ผ๋ฏธํ„ฐ: `tableName` (ํ•„์ˆ˜), `menuObjid` (์„ ํƒ) + - ๊ธฐ์กด `getAvailableNumberingRules()` ์œ ์ง€ (ํ•˜์œ„ ํ˜ธํ™˜์„ฑ) + +**์‚ฌ์šฉ ์˜ˆ์‹œ:** +```typescript +const response = await getAvailableNumberingRulesForScreen( + "item_info", // ํ…Œ์ด๋ธ”๋ช… + undefined // menuObjid (์„ ํƒ) +); +``` + +--- + +### Phase 4: ํ™”๋ฉด๊ด€๋ฆฌ UI ์ˆ˜์ • โœ… + +#### 4.1 TextTypeConfigPanel.tsx +- โœ… `tableName`, `menuObjid` props ์ถ”๊ฐ€ +- โœ… ์ฑ„๋ฒˆ ๊ทœ์น™ ๋กœ๋“œ ๋กœ์ง ๊ฐœ์„ : + - ํ…Œ์ด๋ธ”๋ช…์ด ์žˆ์œผ๋ฉด `getAvailableNumberingRulesForScreen()` ํ˜ธ์ถœ + - ์—†์œผ๋ฉด ๊ธฐ์กด ๋ฉ”๋‰ด ๊ธฐ๋ฐ˜ ๋ฐฉ์‹ ์‚ฌ์šฉ (Fallback) + - ์ƒ์„ธ ๋กœ๊ทธ ์ถ”๊ฐ€ + +**์ฃผ์š” ์ฝ”๋“œ:** +```typescript +useEffect(() => { + const loadRules = async () => { + if (tableName) { + response = await getAvailableNumberingRulesForScreen(tableName, menuObjid); + } else { + response = await getAvailableNumberingRules(menuObjid); + } + }; +}, [localValues.autoValueType, tableName, menuObjid]); +``` + +#### 4.2 DetailSettingsPanel.tsx +- โœ… `currentTableName`์„ ConfigPanelComponent์— ์ „๋‹ฌ +- โœ… ConfigPanelComponent ํƒ€์ž…์— `tableName`, `menuObjid` ์ถ”๊ฐ€ + +#### 4.3 getConfigPanelComponent.tsx +- โœ… `ConfigPanelComponent` ํƒ€์ž… ํ™•์žฅ: `tableName?`, `menuObjid?` ์ถ”๊ฐ€ + +--- + +### Phase 5: ์ฑ„๋ฒˆ๊ทœ์น™ ๊ด€๋ฆฌ UI ์ˆ˜์ • โœ… + +#### NumberingRuleDesigner.tsx +- โœ… ์ ์šฉ ๋ฒ”์œ„ ์„ ํƒ UI ์ถ”๊ฐ€ + - Global: ๋ชจ๋“  ํ™”๋ฉด์—์„œ ์‚ฌ์šฉ + - Table: ํŠน์ • ํ…Œ์ด๋ธ”์—์„œ๋งŒ ์‚ฌ์šฉ + - Menu: ํŠน์ • ๋ฉ”๋‰ด์—์„œ๋งŒ ์‚ฌ์šฉ +- โœ… ์กฐ๊ฑด๋ถ€ ํ•„๋“œ ํ‘œ์‹œ: + - `scope_type='table'`: ํ…Œ์ด๋ธ”๋ช… ์ž…๋ ฅ ํ•„๋“œ ํ‘œ์‹œ + - `scope_type='menu'`: ๋ฉ”๋‰ด ์„ ํƒ ๋“œ๋กญ๋‹ค์šด ํ‘œ์‹œ + - `scope_type='global'`: ์ถ”๊ฐ€ ํ•„๋“œ ๋ถˆํ•„์š” +- โœ… ์ƒˆ ๊ทœ์น™ ๊ธฐ๋ณธ๊ฐ’: `scope_type='global'`๋กœ ๋ณ€๊ฒฝ (๊ฐ€์žฅ ์ผ๋ฐ˜์ ) + +**UI ๊ตฌ์กฐ:** +``` +๊ทœ์น™๋ช… | ๋ฏธ๋ฆฌ๋ณด๊ธฐ +----------------- +์ ์šฉ ๋ฒ”์œ„ [Global/Table/Menu] +โ””โ”€ (table) ํ…Œ์ด๋ธ”๋ช… ์ž…๋ ฅ +โ””โ”€ (menu) ๋ฉ”๋‰ด ์„ ํƒ +``` + +--- + +## ๐Ÿ”„ ๋ฐ์ดํ„ฐ ํ๋ฆ„ + +### ํ™”๋ฉด๊ด€๋ฆฌ์—์„œ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ ์‹œ + +1. **ํ™”๋ฉด ๋กœ๋“œ** + - ScreenDesigner โ†’ DetailSettingsPanel + - `currentTableName` ์ „๋‹ฌ + +2. **TextTypeConfigPanel ๋ Œ๋”๋ง** + - Props: `tableName="item_info"` + - autoValueType์ด `"numbering_rule"`์ผ ๋•Œ ๊ทœ์น™ ๋กœ๋“œ + +3. **API ํ˜ธ์ถœ** + ``` + GET /api/numbering-rules/available-for-screen?tableName=item_info + ``` + +4. **๋ฐฑ์—”๋“œ ์ฒ˜๋ฆฌ** + - `numberingRuleService.getAvailableRulesForScreen()` + - SQL ์ฟผ๋ฆฌ๋กœ ์šฐ์„ ์ˆœ์œ„ ํ•„ํ„ฐ๋ง + - ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์ ์šฉ (company_code ํ™•์ธ) + +5. **์‘๋‹ต ๋ฐ์ดํ„ฐ** + ```json + { + "success": true, + "data": [ + { + "ruleId": "ITEM_CODE", + "ruleName": "ํ’ˆ๋ชฉ ์ฝ”๋“œ", + "scopeType": "table", + "tableName": "item_info" + }, + { + "ruleId": "GLOBAL_CODE", + "ruleName": "์ „์—ญ ์ฝ”๋“œ", + "scopeType": "global" + } + ] + } + ``` + +6. **UI ํ‘œ์‹œ** + - Select ๋“œ๋กญ๋‹ค์šด์— ๊ทœ์น™ ๋ชฉ๋ก ํ‘œ์‹œ + - ์šฐ์„ ์ˆœ์œ„๋Œ€๋กœ ์ •๋ ฌ๋จ + +--- + +## ๐Ÿ“Š scope_type ์ •์˜ ๋ฐ ์šฐ์„ ์ˆœ์œ„ + +| scope_type | ์„ค๋ช… | ์šฐ์„ ์ˆœ์œ„ | ์‚ฌ์šฉ ์ผ€์ด์Šค | +| ---------- | ---------------------- | -------- | ------------------------------- | +| `menu` | ํŠน์ • ๋ฉ”๋‰ด์—์„œ๋งŒ ์‚ฌ์šฉ | 1 (์ตœ๊ณ ) | ๋ฉ”๋‰ด๋ณ„๋กœ ๋‹ค๋ฅธ ์ฑ„๋ฒˆ ๋ฐฉ์‹ ํ•„์š” ์‹œ | +| `table` | ํŠน์ • ํ…Œ์ด๋ธ”์—์„œ๋งŒ ์‚ฌ์šฉ | 2 (์ค‘๊ฐ„) | ํ…Œ์ด๋ธ” ๊ธฐ์ค€ ์ฑ„๋ฒˆ (์ผ๋ฐ˜์ ) | +| `global` | ๋ชจ๋“  ๊ณณ์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅ | 3 (์ตœ์ €) | ๊ณตํ†ต ์ฑ„๋ฒˆ ๊ทœ์น™ | + +### ํ•„ํ„ฐ๋ง ๋กœ์ง +```sql +WHERE company_code = $1 -- ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ํ•„์ˆ˜ + AND ( + (scope_type = 'menu' AND menu_objid = $2) -- 1์ˆœ์œ„ + OR (scope_type = 'table' AND table_name = $3) -- 2์ˆœ์œ„ + OR (scope_type = 'global' AND table_name IS NULL) -- 3์ˆœ์œ„ + ) +``` + +--- + +## ๐Ÿ” ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๋ณด์žฅ + +### ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ ˆ๋ฒจ +- โœ… `company_code` ์ปฌ๋Ÿผ ํ•„์ˆ˜ (NOT NULL) +- โœ… ์™ธ๋ž˜ํ‚ค ์ œ์•ฝ์กฐ๊ฑด (company_info ์ฐธ์กฐ) +- โœ… ๋ณตํ•ฉ ์ธ๋ฑ์Šค์— company_code ํฌํ•จ + +### API ๋ ˆ๋ฒจ +- โœ… ์ผ๋ฐ˜ ํšŒ์‚ฌ: `WHERE company_code = $1` +- โœ… ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ชจ๋“  ๋ฐ์ดํ„ฐ ์กฐํšŒ ๊ฐ€๋Šฅ (company_code="*" ์ œ์™ธ) +- โœ… ์ผ๋ฐ˜ ํšŒ์‚ฌ๋Š” `company_code="*"` ๋ฐ์ดํ„ฐ๋ฅผ ๋ณผ ์ˆ˜ ์—†์Œ + +### ๋กœ๊น… ๋ ˆ๋ฒจ +- โœ… ๋ชจ๋“  ๋กœ๊ทธ์— `companyCode` ํฌํ•จ (๊ฐ์‚ฌ ์ถ”์ ) + +--- + +## ๐Ÿงช ํ…Œ์ŠคํŠธ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +### ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ŠคํŠธ (๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ›„ ์ˆ˜ํ–‰) + +- [ ] ์ œ์•ฝ์กฐ๊ฑด ํ™•์ธ + ```sql + SELECT conname, pg_get_constraintdef(oid) + FROM pg_constraint + WHERE conrelid = 'numbering_rules'::regclass + AND conname LIKE '%scope%'; + ``` + +- [ ] ์ธ๋ฑ์Šค ํ™•์ธ + ```sql + SELECT indexname, indexdef + FROM pg_indexes + WHERE tablename = 'numbering_rules' + AND indexname LIKE '%scope%'; + ``` + +- [ ] ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ™•์ธ + ```sql + SELECT scope_type, COUNT(*) as count + FROM numbering_rules + GROUP BY scope_type; + ``` + +### ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ + +- [ ] **ํšŒ์‚ฌ A๋กœ ๋กœ๊ทธ์ธ** + - [ ] ์ฑ„๋ฒˆ๊ทœ์น™ ๊ด€๋ฆฌ์—์„œ ์ƒˆ ๊ทœ์น™ ์ƒ์„ฑ (scope_type='table', tableName='item_info') + - [ ] ์ €์žฅ ์„ฑ๊ณต ํ™•์ธ + - [ ] ํ™”๋ฉด๊ด€๋ฆฌ์—์„œ item_info ํ…Œ์ด๋ธ” ํ™”๋ฉด ์ƒ์„ฑ + - [ ] ํ…์ŠคํŠธ ํ•„๋“œ์—์„œ "์ž๋™ ์ž…๋ ฅ > ์ฑ„๋ฒˆ ๊ทœ์น™" ์„ ํƒ + - [ ] ๋ฐฉ๊ธˆ ์ƒ์„ฑํ•œ ๊ทœ์น™์ด ๋ชฉ๋ก์— ํ‘œ์‹œ๋˜๋Š”์ง€ ํ™•์ธ โœ… + +- [ ] **ํšŒ์‚ฌ B๋กœ ๋กœ๊ทธ์ธ** + - [ ] ํ™”๋ฉด๊ด€๋ฆฌ์—์„œ item_info ํ…Œ์ด๋ธ” ํ™”๋ฉด ์ ‘์† + - [ ] ํ…์ŠคํŠธ ํ•„๋“œ์—์„œ "์ž๋™ ์ž…๋ ฅ > ์ฑ„๋ฒˆ ๊ทœ์น™" ์„ ํƒ + - [ ] ํšŒ์‚ฌ A์˜ ๊ทœ์น™์ด ๋ณด์ด์ง€ ์•Š๋Š”์ง€ ํ™•์ธ โœ… + +- [ ] **์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋กœ ๋กœ๊ทธ์ธ** + - [ ] ์ฑ„๋ฒˆ๊ทœ์น™ ๊ด€๋ฆฌ์—์„œ ๋ชจ๋“  ํšŒ์‚ฌ ๊ทœ์น™์ด ๋ณด์ด๋Š”์ง€ ํ™•์ธ โœ… + - [ ] ํ™”๋ฉด๊ด€๋ฆฌ์—์„œ๋Š” ์ผ๋ฐ˜ ํšŒ์‚ฌ ๊ทœ์น™๋งŒ ๋ณด์ด๋Š”์ง€ ํ™•์ธ โœ… + +### ์šฐ์„ ์ˆœ์œ„ ํ…Œ์ŠคํŠธ + +- [ ] ๊ฐ™์€ ํ…Œ์ด๋ธ”(item_info)์— ๋Œ€ํ•ด 3๊ฐ€์ง€ scope_type ๊ทœ์น™ ์ƒ์„ฑ + - [ ] scope_type='global', table_name=NULL, ruleName="์ „์—ญ๊ทœ์น™" + - [ ] scope_type='table', table_name='item_info', ruleName="ํ…Œ์ด๋ธ”๊ทœ์น™" + - [ ] scope_type='menu', menu_objid=123, tableName='item_info', ruleName="๋ฉ”๋‰ด๊ทœ์น™" + +- [ ] ํ™”๋ฉด๊ด€๋ฆฌ์—์„œ item_info ํ™”๋ฉด ์ ‘์† (menuObjid=123) + - [ ] ๊ทœ์น™ ๋ชฉ๋ก์—์„œ ์ˆœ์„œ ํ™•์ธ: + 1. ๋ฉ”๋‰ด๊ทœ์น™ (menu, ์šฐ์„ ์ˆœ์œ„ 1) + 2. ํ…Œ์ด๋ธ”๊ทœ์น™ (table, ์šฐ์„ ์ˆœ์œ„ 2) + 3. ์ „์—ญ๊ทœ์น™ (global, ์šฐ์„ ์ˆœ์œ„ 3) + +--- + +## ๐Ÿ“ ์ˆ˜์ •๋œ ํŒŒ์ผ ๋ชฉ๋ก + +### ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค (์ค€๋น„ ์™„๋ฃŒ, ์‹คํ–‰ ๋Œ€๊ธฐ) +- โœ… `db/migrations/046_update_numbering_rules_scope_type.sql` +- โœ… `db/migrations/RUN_046_MIGRATION.md` + +### ๋ฐฑ์—”๋“œ +- โœ… `backend-node/src/services/numberingRuleService.ts` +- โœ… `backend-node/src/controllers/numberingRuleController.ts` + +### ํ”„๋ก ํŠธ์—”๋“œ API +- โœ… `frontend/lib/api/numberingRule.ts` + +### ํ”„๋ก ํŠธ์—”๋“œ UI +- โœ… `frontend/components/screen/panels/webtype-configs/TextTypeConfigPanel.tsx` +- โœ… `frontend/components/screen/panels/DetailSettingsPanel.tsx` +- โœ… `frontend/lib/utils/getConfigPanelComponent.tsx` +- โœ… `frontend/components/numbering-rule/NumberingRuleDesigner.tsx` + +--- + +## ๐Ÿš€ ๋ฐฐํฌ ๊ฐ€์ด๋“œ + +### 1๋‹จ๊ณ„: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ + +```bash +# Docker ํ™˜๊ฒฝ +docker exec -i erp-node-db-1 psql -U postgres -d ilshin < db/migrations/046_update_numbering_rules_scope_type.sql + +# ๋กœ์ปฌ PostgreSQL +psql -h localhost -U postgres -d ilshin -f db/migrations/046_update_numbering_rules_scope_type.sql +``` + +### 2๋‹จ๊ณ„: ๋ฐฑ์—”๋“œ ์žฌ์‹œ์ž‘ + +```bash +# Docker ํ™˜๊ฒฝ +docker-compose restart backend + +# ๋กœ์ปฌ ๊ฐœ๋ฐœ +npm run dev +``` + +### 3๋‹จ๊ณ„: ํ”„๋ก ํŠธ์—”๋“œ ์žฌ๋นŒ๋“œ + +```bash +# Docker ํ™˜๊ฒฝ +docker-compose restart frontend + +# ๋กœ์ปฌ ๊ฐœ๋ฐœ +npm run dev +``` + +### 4๋‹จ๊ณ„: ๊ฒ€์ฆ + +1. ๊ฐœ๋ฐœ์ž ๋„๊ตฌ ์ฝ˜์†” ์—ด๊ธฐ +2. ํ™”๋ฉด๊ด€๋ฆฌ ์ ‘์† +3. ํ…์ŠคํŠธ ํ•„๋“œ ์ถ”๊ฐ€ โ†’ ์ž๋™ ์ž…๋ ฅ โ†’ ์ฑ„๋ฒˆ ๊ทœ์น™ ์„ ํƒ +4. ์ฝ˜์†”์—์„œ ๋‹ค์Œ ๋กœ๊ทธ ํ™•์ธ: + ``` + ๐Ÿ“‹ ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ: { tableName: "xxx", menuObjid: undefined } + โœ… ์ฑ„๋ฒˆ ๊ทœ์น™ ๋กœ๋“œ ์„ฑ๊ณต: N๊ฐœ + ``` + +--- + +## ๐ŸŽ‰ ์ฃผ์š” ๊ฐœ์„  ์‚ฌํ•ญ + +### ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ +- โœ… ํ™”๋ฉด๊ด€๋ฆฌ์—์„œ ์ฑ„๋ฒˆ๊ทœ์น™์ด ์ž๋™์œผ๋กœ ํ‘œ์‹œ +- โœ… ๋ฉ”๋‰ด ๊ตฌ์กฐ๋ฅผ ๋ชฐ๋ผ๋„ ๊ทœ์น™ ์„ค์ • ๊ฐ€๋Šฅ +- โœ… ๊ฐ™์€ ํ…Œ์ด๋ธ” ํ™”๋ฉด์— ๊ทœ์น™ ์žฌ์‚ฌ์šฉ ์ž๋™ + +### ์œ ์ง€๋ณด์ˆ˜์„ฑ +- โœ… ๋ฉ”๋‰ด ๊ตฌ์กฐ ๋ณ€๊ฒฝ ์‹œ ๊ทœ์น™ ์žฌ์„ค์ • ๋ถˆํ•„์š” +- โœ… ํ…Œ์ด๋ธ” ์ค‘์‹ฌ ์„ค๊ณ„๋กœ ์ง๊ด€์  +- โœ… ์ฝ”๋“œ ๋ณต์žก๋„ ๊ฐ์†Œ + +### ํ™•์žฅ์„ฑ +- โœ… ํ–ฅํ›„ scope_type ์ถ”๊ฐ€ ๊ฐ€๋Šฅ +- โœ… ๋‹ค์ค‘ ํ…Œ์ด๋ธ” ์ง€์› ๊ฐ€๋Šฅ +- โœ… ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์™„๋ฒฝ ์ง€์› + +--- + +## โš ๏ธ ์•Œ๋ ค์ง„ ์ œ์•ฝ์‚ฌํ•ญ + +1. **๋ฉ”๋‰ด ๋ชฉ๋ก ๋กœ๋“œ ๋ฏธ๊ตฌํ˜„** + - NumberingRuleDesigner์—์„œ `scope_type='menu'` ์„ ํƒ ์‹œ ๋ฉ”๋‰ด ๋ชฉ๋ก ๋กœ๋“œ ํ•„์š” + - TODO: ๋ฉ”๋‰ด API ์—ฐ๋™ + +2. **๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ ๋Œ€๊ธฐ** + - Docker ์ปจํ…Œ์ด๋„ˆ ์—ฐ๊ฒฐ ๋ฌธ์ œ๋กœ ์ˆ˜๋™ ์‹คํ–‰ ํ•„์š” + - ๋ฐฐํฌ ์‹œ ๋ฐ˜๋“œ์‹œ ์‹คํ–‰ ํ•„์š” + +--- + +## ๐Ÿ“ ๋‹ค์Œ ๋‹จ๊ณ„ + +1. **๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰** + - DB ์ ‘์† ์ •๋ณด ํ™•์ธ ํ›„ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ + - ๊ฒ€์ฆ ์ฟผ๋ฆฌ๋กœ ์ •์ƒ ๋™์ž‘ ํ™•์ธ + +2. **ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** + - ์ „์ฒด ์›Œํฌํ”Œ๋กœ์šฐ ํ…Œ์ŠคํŠธ + - ํšŒ์‚ฌ๋ณ„ ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ ํ™•์ธ + - ์šฐ์„ ์ˆœ์œ„ ํ•„ํ„ฐ๋ง ํ™•์ธ + +3. **๋ฉ”๋‰ด API ์—ฐ๋™** + - NumberingRuleDesigner์—์„œ ๋ฉ”๋‰ด ๋ชฉ๋ก ๋กœ๋“œ ๊ตฌํ˜„ + +4. **์‚ฌ์šฉ์ž ๊ฐ€์ด๋“œ ์ž‘์„ฑ** + - ์ฑ„๋ฒˆ๊ทœ์น™ ์‚ฌ์šฉ ๋ฐฉ๋ฒ• ๋ฌธ์„œํ™” + - scope_type๋ณ„ ์‚ฌ์šฉ ์˜ˆ์‹œ ์ถ”๊ฐ€ + +--- + +## ๐Ÿ“ž ๋ฌธ์˜ ๋ฐ ์ง€์› + +- **์ž‘์„ฑ์ž**: AI ๊ฐœ๋ฐœํŒ€ +- **์ž‘์„ฑ์ผ**: 2025-11-08 +- **๊ด€๋ จ ๋ฌธ์„œ**: `์ฑ„๋ฒˆ๊ทœ์น™_ํ…Œ์ด๋ธ”๊ธฐ๋ฐ˜_ํ•„ํ„ฐ๋ง_๊ตฌํ˜„_๊ณ„ํš์„œ.md` + +**๊ตฌํ˜„ ์™„๋ฃŒ!** ๐ŸŽŠ + +๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ ํ›„ ๋ฐ”๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. + -- 2.43.0 From e27845a82f894a2f837020b9af0b551581c478a5 Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 7 Nov 2025 17:12:01 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20=ED=85=8C=EC=9D=B4=EB=B8=94=20?= =?UTF-8?q?=ED=83=AD=20=EB=93=9C=EB=9E=98=EA=B7=B8=EC=95=A4=EB=93=9C?= =?UTF-8?q?=EB=A1=AD=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20AI-=EA=B0=9C?= =?UTF-8?q?=EB=B0=9C=EC=9E=90=20=ED=98=91=EC=97=85=20=EA=B7=9C=EC=B9=99=20?= =?UTF-8?q?=EC=88=98=EB=A6=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ์ฃผ์š” ๋ณ€๊ฒฝ์‚ฌํ•ญ: - ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ ์ปฌ๋Ÿผ์˜ ๋ผ๋ฒจ ์ˆจ๊น€ ๋ฐ placeholder๋กœ ๋ผ๋ฒจ๋ช… ํ‘œ์‹œ - ๊ธฐ๋ณธ ๋†’์ด 30px๋กœ ๋ณ€๊ฒฝ - 5๊ฐœ ์‹œ์Šคํ…œ ์ปฌ๋Ÿผ(id, created_date, updated_date, writer, company_code) ์ˆจ๊น€ - AI-๊ฐœ๋ฐœ์ž ํ˜‘์—… ์ž‘์—… ์ˆ˜์น™ ๋ฌธ์„œ ์ž‘์„ฑ ๋ฐ .cursorrules์— ํ†ตํ•ฉ ํŒŒ์ผ ๋ณ€๊ฒฝ: - frontend/components/screen/ScreenDesigner.tsx * getDefaultHeight(): ๊ธฐ๋ณธ ๋†’์ด๋ฅผ 30px๋กœ ๋ณ€๊ฒฝ * handleDrop(): labelDisplay false, placeholder ์ถ”๊ฐ€ - frontend/components/screen/panels/TablesPanel.tsx * hiddenColumns Set์œผ๋กœ ์‹œ์Šคํ…œ ์ปฌ๋Ÿผ ํ•„ํ„ฐ๋ง - .cursor/rules/ai-developer-collaboration-rules.mdc (์‹ ๊ทœ) * ํ™•์ธ ์šฐ์„ , ํ•œ ๋ฒˆ์— ํ•˜๋‚˜, ์ฒ ์ €ํ•œ ๋งˆ๋ฌด๋ฆฌ ์›์น™ * ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ฒ€์ฆ, ์ฝ”๋“œ ์ˆ˜์ •, ํ…Œ์ŠคํŠธ, ์ปค๋ฎค๋‹ˆ์ผ€์ด์…˜ ๊ทœ์น™ - .cursorrules * ํ•„์ˆ˜ ํ™•์ธ ๊ทœ์น™ ์„น์…˜ ์ถ”๊ฐ€ * ๋ชจ๋“  ์ž‘์—… ์‹œ์ž‘/์™„๋ฃŒ ์‹œ ํ˜‘์—… ๊ทœ์น™ ํ™•์ธ ๊ฐ•์ œํ™” --- .cursorrules | 17 + .gitignore | 3 +- .../components/screen/RealtimePreview.tsx | 24 +- .../screen/RealtimePreviewDynamic.tsx | 3 + frontend/components/screen/ScreenDesigner.tsx | 483 +++--------------- .../components/screen/panels/GridPanel.tsx | 370 +++----------- .../components/screen/panels/TablesPanel.tsx | 8 +- .../screen/panels/UnifiedPropertiesPanel.tsx | 209 ++++---- frontend/lib/utils/gridUtils.ts | 296 ++--------- frontend/types/screen-management.ts | 27 +- 10 files changed, 347 insertions(+), 1093 deletions(-) diff --git a/.cursorrules b/.cursorrules index 3b0c3833..cf9eaae9 100644 --- a/.cursorrules +++ b/.cursorrules @@ -1,5 +1,22 @@ # 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 a771d2c9..e6e30135 100644 --- a/.gitignore +++ b/.gitignore @@ -286,4 +286,5 @@ uploads/ *.hwp *.hwpx -claude.md \ No newline at end of file +claude.md +.cursor/rules/ai-developer-collaboration-rules.mdc diff --git a/frontend/components/screen/RealtimePreview.tsx b/frontend/components/screen/RealtimePreview.tsx index ab8cc3ae..86a2f357 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.DragEvent) => void; + onDragStart?: (e: React.MouseEvent | React.DragEvent) => void; // MouseEvent๋„ ํ—ˆ์šฉ onDragEnd?: () => void; onGroupToggle?: (groupId: string) => void; // ๊ทธ๋ฃน ์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ children?: React.ReactNode; // ๊ทธ๋ฃน ๋‚ด ์ž์‹ ์ปดํฌ๋„ŒํŠธ๋“ค @@ -247,6 +247,13 @@ 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); @@ -458,7 +465,17 @@ 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); }; @@ -473,8 +490,9 @@ export const RealtimePreviewDynamic: React.FC = ({ className="absolute cursor-pointer" style={{ ...componentStyle, ...selectionStyle }} onClick={handleClick} - draggable - onDragStart={handleDragStart} + onMouseDown={isDesignMode ? handleMouseDown : undefined} + draggable={!isDesignMode} // ๋””์ž์ธ ๋ชจ๋“œ๊ฐ€ ์•„๋‹ ๋•Œ๋งŒ draggable (ํŒ”๋ ˆํŠธ์šฉ) + onDragStart={!isDesignMode ? handleDragStart : undefined} onDragEnd={handleDragEnd} > {/* ์ปดํฌ๋„ŒํŠธ ํƒ€์ž…๋ณ„ ๋ Œ๋”๋ง */} diff --git a/frontend/components/screen/RealtimePreviewDynamic.tsx b/frontend/components/screen/RealtimePreviewDynamic.tsx index 679ed5a8..80d577d6 100644 --- a/frontend/components/screen/RealtimePreviewDynamic.tsx +++ b/frontend/components/screen/RealtimePreviewDynamic.tsx @@ -264,6 +264,9 @@ 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 7db03da6..5c5e4dd2 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -29,13 +29,9 @@ 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"; @@ -107,11 +103,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD const [layout, setLayout] = useState({ components: [], gridSettings: { - columns: 12, - gap: 16, - padding: 0, - snapToGrid: true, - showGrid: false, // ๊ธฐ๋ณธ๊ฐ’ false๋กœ ๋ณ€๊ฒฝ + snapToGrid: true, // ๊ฒฉ์ž ์Šค๋ƒ… ON + showGrid: false, // ๊ฒฉ์ž ํ‘œ์‹œ OFF gridColor: "#d1d5db", gridOpacity: 0.5, }, @@ -540,107 +533,31 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD gridInfo && 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, - }); - const snappedSize = snapSizeToGrid( - newComp.size, - currentGridInfo, - prevLayout.gridSettings as GridUtilSettings, + // ๐Ÿ”ฅ 10px ๊ณ ์ • ๊ฒฉ์ž๋กœ ์Šค๋ƒ… + const currentGridInfo = calculateGridInfo( + screenResolution.width, + screenResolution.height, + prevLayout.gridSettings, ); + 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 ๋ณ€๊ฒฝ ์‹œ ํฌ๊ธฐ๋ฅผ ๊ฒฉ์ž์— ๋งž๊ฒŒ ์ž๋™ ์กฐ์ • - 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, - }; - } + // ๐Ÿ—‘๏ธ gridColumns ๋กœ์ง ์ œ๊ฑฐ: 10px ๊ณ ์ • ๊ฒฉ์ž์—์„œ๋Š” ๋ถˆํ•„์š” // ์œ„์น˜ ๋ณ€๊ฒฝ ์‹œ ๊ฒฉ์ž ์Šค๋ƒ… ์ ์šฉ (๊ทธ๋ฃน ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ํฌํ•จ) if ( (path === "position.x" || path === "position.y" || path === "position") && layout.gridSettings?.snapToGrid ) { - // ํ˜„์žฌ ํ•ด์ƒ๋„์— ๋งž๋Š” ๊ฒฉ์ž ์ •๋ณด ๊ณ„์‚ฐ - 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; - } + // ๐Ÿ”ฅ 10px ๊ณ ์ • ๊ฒฉ์ž + const currentGridInfo = calculateGridInfo( + screenResolution.width, + screenResolution.height, + layout.gridSettings, + ); + const snappedPosition = snapToGrid(newComp.position, currentGridInfo, layout.gridSettings); + newComp.position = snappedPosition; } return newComp; @@ -903,20 +820,21 @@ 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: layoutToUse.gridSettings?.columns || 12, // DB ๊ฐ’ ์šฐ์„ , ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ 12 - gap: layoutToUse.gridSettings?.gap ?? 16, // DB ๊ฐ’ ์šฐ์„ , ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ 16 - padding: 0, // padding์€ ํ•ญ์ƒ 0์œผ๋กœ ๊ฐ•์ œ + // ๐Ÿ—‘๏ธ ์ œ๊ฑฐ: columns, gap, padding (๋” ์ด์ƒ ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ) 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) { @@ -1074,51 +992,12 @@ 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); }, @@ -1215,18 +1094,13 @@ 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 = { @@ -1285,17 +1159,10 @@ 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, }; }); @@ -1454,24 +1321,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD : { x: absoluteX, y: absoluteY, z: 1 }; if (templateComp.type === "container") { - // ๊ทธ๋ฆฌ๋“œ ์ปฌ๋Ÿผ ๊ธฐ๋ฐ˜ ํฌ๊ธฐ ๊ณ„์‚ฐ - 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 }; // ํด๋ฐฑ ํฌ๊ธฐ + // ๐Ÿ”ฅ 10px ๊ณ ์ • ๊ฒฉ์ž: ๊ธฐ๋ณธ ๋„ˆ๋น„ ์‚ฌ์šฉ + const calculatedSize = { width: 400, height: templateComp.size.height }; return { id: componentId, @@ -1495,21 +1346,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD // ๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ” ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ const gridColumns = 6; // ๊ธฐ๋ณธ๊ฐ’: 6์ปฌ๋Ÿผ (50% ๋„ˆ๋น„) - // 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; + // ๐Ÿ”ฅ 10px ๊ณ ์ • ๊ฒฉ์ž: ๊ธฐ๋ณธ ํฌ๊ธฐ ์‚ฌ์šฉ + const calculatedSize = { + width: 800, // ๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ” ๊ธฐ๋ณธ ๋„ˆ๋น„ + height: templateComp.size.height, + }; console.log("๐Ÿ“Š ๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ” ์ƒ์„ฑ ์‹œ ํฌ๊ธฐ ๊ณ„์‚ฐ:", { gridColumns, @@ -1574,20 +1415,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD // ํŒŒ์ผ ์ฒจ๋ถ€ ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ const gridColumns = 6; // ๊ธฐ๋ณธ๊ฐ’: 6์ปฌ๋Ÿผ - const calculatedSize = - currentGridInfo && layout.gridSettings?.snapToGrid - ? (() => { - const newWidth = calculateWidthFromColumns( - gridColumns, - currentGridInfo, - layout.gridSettings as GridUtilSettings, - ); - return { - width: newWidth, - height: templateComp.size.height, - }; - })() - : templateComp.size; + // ๐Ÿ”ฅ 10px ๊ณ ์ • ๊ฒฉ์ž + const calculatedSize = { + width: 400, + height: templateComp.size.height, + }; return { id: componentId, @@ -1625,20 +1457,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD // ์˜์—ญ ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ const gridColumns = 6; // ๊ธฐ๋ณธ๊ฐ’: 6์ปฌ๋Ÿผ (50% ๋„ˆ๋น„) - const calculatedSize = - currentGridInfo && layout.gridSettings?.snapToGrid - ? (() => { - const newWidth = calculateWidthFromColumns( - gridColumns, - currentGridInfo, - layout.gridSettings as GridUtilSettings, - ); - return { - width: newWidth, - height: templateComp.size.height, - }; - })() - : templateComp.size; + // ๐Ÿ”ฅ 10px ๊ณ ์ • ๊ฒฉ์ž + const calculatedSize = { + width: 600, // ์˜์—ญ ๊ธฐ๋ณธ ๋„ˆ๋น„ + height: templateComp.size.height, + }; return { id: componentId, @@ -1760,7 +1583,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD const widgetSize = currentGridInfo && layout.gridSettings?.snapToGrid ? { - width: calculateWidthFromColumns(1, currentGridInfo, layout.gridSettings as GridUtilSettings), + width: 200, height: templateComp.size.height, } : templateComp.size; @@ -2131,23 +1954,9 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD }); } - // ๊ทธ๋ฆฌ๋“œ ์‹œ์Šคํ…œ์ด ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ 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), - }; - } + // ๐Ÿ—‘๏ธ 10px ๊ณ ์ • ๊ฒฉ์ž: gridColumns ๋กœ์ง ์ œ๊ฑฐ + // ๊ธฐ๋ณธ ํฌ๊ธฐ๋งŒ ์‚ฌ์šฉ + componentSize = component.defaultSize; console.log("๐ŸŽจ ์ตœ์ข… ์ปดํฌ๋„ŒํŠธ ํฌ๊ธฐ:", { componentId: component.id, @@ -2247,15 +2056,12 @@ 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") { @@ -2309,34 +2115,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD }; } else if (type === "column") { // console.log("๐Ÿ”„ ์ปฌ๋Ÿผ ๋“œ๋กญ ์ฒ˜๋ฆฌ:", { webType: column.widgetType, columnName: column.columnName }); - // ํ˜„์žฌ ํ•ด์ƒ๋„์— ๋งž๋Š” ๊ฒฉ์ž ์ •๋ณด๋กœ ๊ธฐ๋ณธ ํฌ๊ธฐ ๊ณ„์‚ฐ - 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, - }); + // ๐Ÿ”ฅ 10px ๊ณ ์ • ๊ฒฉ์ž ์‹œ์Šคํ…œ: ๊ฐ„๋‹จํ•œ ๊ธฐ๋ณธ ๋„ˆ๋น„ ์‚ฌ์šฉ + const defaultWidth = 200; // ๊ธฐ๋ณธ ๋„ˆ๋น„ 200px // ์›นํƒ€์ž…๋ณ„ ๊ธฐ๋ณธ ๊ทธ๋ฆฌ๋“œ ์ปฌ๋Ÿผ ์ˆ˜ ๊ณ„์‚ฐ const getDefaultGridColumns = (widgetType: string): number => { @@ -2375,7 +2155,6 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD }; const defaultColumns = widthMap[widgetType] || 3; // ๊ธฐ๋ณธ๊ฐ’ 3 (1/4, 25%) - console.log("๐ŸŽฏ [ScreenDesigner] getDefaultGridColumns:", { widgetType, defaultColumns }); return defaultColumns; }; @@ -2388,7 +2167,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD file: 240, // ํŒŒ์ผ ์—…๋กœ๋“œ (40 * 6) }; - return heightMap[widgetType] || 40; // ๊ธฐ๋ณธ๊ฐ’ 40 + return heightMap[widgetType] || 30; // ๊ธฐ๋ณธ๊ฐ’ 30px๋กœ ๋ณ€๊ฒฝ }; // ์›นํƒ€์ž…๋ณ„ ๊ธฐ๋ณธ ์„ค์ • ์ƒ์„ฑ @@ -2547,22 +2326,9 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD // ์›นํƒ€์ž…๋ณ„ ์ ์ ˆํ•œ gridColumns ๊ณ„์‚ฐ const calculatedGridColumns = getDefaultGridColumns(column.widgetType); - // gridColumns์— ๋งž๋Š” ์‹ค์ œ ๋„ˆ๋น„ ๊ณ„์‚ฐ - const componentWidth = - currentGridInfo && layout.gridSettings?.snapToGrid - ? calculateWidthFromColumns( - calculatedGridColumns, - currentGridInfo, - layout.gridSettings as GridUtilSettings, - ) - : defaultWidth; + // ๐Ÿ”ฅ 10px ๊ณ ์ • ๊ฒฉ์ž: ๊ฐ„๋‹จํ•œ ๋„ˆ๋น„ ๊ณ„์‚ฐ + const componentWidth = defaultWidth; - console.log("๐ŸŽฏ ํผ ์ปจํ…Œ์ด๋„ˆ ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ:", { - widgetType: column.widgetType, - calculatedGridColumns, - componentWidth, - defaultWidth, - }); newComponent = { id: generateComponentId(), @@ -2583,7 +2349,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD codeCategory: column.codeCategory, }), style: { - labelDisplay: false, // ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ์˜ ๊ธฐ๋ณธ ๋ผ๋ฒจ ํ‘œ์‹œ๋ฅผ false๋กœ ์„ค์ • + labelDisplay: false, // ๋ผ๋ฒจ ์ˆจ๊น€ (placeholder ์‚ฌ์šฉ) labelFontSize: "12px", labelColor: "#212121", labelFontWeight: "500", @@ -2595,6 +2361,7 @@ 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 && { @@ -2613,22 +2380,9 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD // ์›นํƒ€์ž…๋ณ„ ์ ์ ˆํ•œ gridColumns ๊ณ„์‚ฐ const calculatedGridColumns = getDefaultGridColumns(column.widgetType); - // gridColumns์— ๋งž๋Š” ์‹ค์ œ ๋„ˆ๋น„ ๊ณ„์‚ฐ - const componentWidth = - currentGridInfo && layout.gridSettings?.snapToGrid - ? calculateWidthFromColumns( - calculatedGridColumns, - currentGridInfo, - layout.gridSettings as GridUtilSettings, - ) - : defaultWidth; + // ๐Ÿ”ฅ 10px ๊ณ ์ • ๊ฒฉ์ž: ๊ฐ„๋‹จํ•œ ๋„ˆ๋น„ ๊ณ„์‚ฐ + const componentWidth = defaultWidth; - console.log("๐ŸŽฏ ์บ”๋ฒ„์Šค ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ:", { - widgetType: column.widgetType, - calculatedGridColumns, - componentWidth, - defaultWidth, - }); // ๐Ÿ” ์ด๋ฏธ์ง€ ํƒ€์ž… ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ ๋””๋ฒ„๊น… // if (column.widgetType === "image") { @@ -2658,7 +2412,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD codeCategory: column.codeCategory, }), style: { - labelDisplay: true, // ํ…Œ์ด๋ธ” ํŒจ๋„์—์„œ ๋“œ๋ž˜๊ทธํ•œ ์ปดํฌ๋„ŒํŠธ๋Š” ๋ผ๋ฒจ์„ ๊ธฐ๋ณธ์ ์œผ๋กœ ํ‘œ์‹œ + labelDisplay: false, // ๋ผ๋ฒจ ์ˆจ๊น€ (placeholder ์‚ฌ์šฉ) labelFontSize: "14px", labelColor: "#000000", // ์ˆœ์ˆ˜ํ•œ ๊ฒ€์ • labelFontWeight: "500", @@ -2670,6 +2424,7 @@ 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 && { @@ -2701,21 +2456,6 @@ 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 = { @@ -2889,27 +2629,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD componentsToMove = [...componentsToMove, ...additionalComponents]; } - // 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 }, - }); - + const finalGrabOffset = { + x: relativeMouseX - component.position.x, + y: relativeMouseY - component.position.y, + }; + setDragState({ isDragging: true, draggedComponent: component, // ์ฃผ ๋“œ๋ž˜๊ทธ ์ปดํฌ๋„ŒํŠธ (๋งˆ์šฐ์Šค ์œ„์น˜ ๊ธฐ์ค€) @@ -2924,10 +2648,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD y: component.position.y, z: (component.position as Position).z || 1, }, - grabOffset: { - x: relativeMouseX - component.position.x, - y: relativeMouseY - component.position.y, - }, + grabOffset: finalGrabOffset, justFinishedDrag: false, }); }, @@ -2955,34 +2676,24 @@ 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, Math.min(rawX, screenResolution.width - componentWidth)), - y: Math.max(0, Math.min(rawY, screenResolution.height - componentHeight)), + x: Math.max(0, rawX), + y: Math.max(0, rawY), 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; }); @@ -3000,15 +2711,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD const draggedComponent = layout.components.find((c) => c.id === dragState.draggedComponent); let finalPosition = dragState.currentPosition; - // ํ˜„์žฌ ํ•ด์ƒ๋„์— ๋งž๋Š” ๊ฒฉ์ž ์ •๋ณด ๊ณ„์‚ฐ - 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; + // ๐Ÿ”ฅ 10px ๊ณ ์ • ๊ฒฉ์ž ์‹œ์Šคํ…œ: calculateGridInfo๋Š” columns, gap, padding์„ ๋ฌด์‹œํ•จ + const currentGridInfo = calculateGridInfo(screenResolution.width, screenResolution.height, layout.gridSettings); // ์ผ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ ๋ฐ ํ”Œ๋กœ์šฐ ๋ฒ„ํŠผ ๊ทธ๋ฃน์— ๊ฒฉ์ž ์Šค๋ƒ… ์ ์šฉ (์ผ๋ฐ˜ ๊ทธ๋ฃน ์ œ์™ธ) if (draggedComponent?.type !== "group" && layout.gridSettings?.snapToGrid && currentGridInfo) { @@ -3019,21 +2723,9 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD z: dragState.currentPosition.z ?? 1, }, currentGridInfo, - { - columns: layout.gridSettings.columns, - gap: layout.gridSettings.gap, - padding: layout.gridSettings.padding, - snapToGrid: layout.gridSettings.snapToGrid || false, - }, + layout.gridSettings, ); - console.log("๐ŸŽฏ ๊ฒฉ์ž ์Šค๋ƒ… ์ ์šฉ๋จ:", { - componentType: draggedComponent?.type, - resolution: `${screenResolution.width}x${screenResolution.height}`, - originalPosition: dragState.currentPosition, - snappedPosition: finalPosition, - columnWidth: currentGridInfo.columnWidth, - }); } // ์Šค๋ƒ…์œผ๋กœ ์ธํ•œ ์ถ”๊ฐ€ ์ด๋™ ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ @@ -3098,28 +2790,6 @@ 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, @@ -3142,11 +2812,6 @@ 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 f33cc601..34d324f8 100644 --- a/frontend/components/screen/panels/GridPanel.tsx +++ b/frontend/components/screen/panels/GridPanel.tsx @@ -1,335 +1,79 @@ -"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 { 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"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Grid3X3 } from "lucide-react"; +import { GridSettings } from "@/types/screen-management"; interface GridPanelProps { gridSettings: GridSettings; onGridSettingsChange: (settings: GridSettings) => void; - onResetGrid: () => void; - onForceGridUpdate?: () => void; // ๊ฐ•์ œ ๊ฒฉ์ž ์žฌ์กฐ์ • ์ถ”๊ฐ€ - screenResolution?: ScreenResolution; // ํ•ด์ƒ๋„ ์ •๋ณด ์ถ”๊ฐ€ } -export const GridPanel: React.FC = ({ - gridSettings, - onGridSettingsChange, - onResetGrid, - onForceGridUpdate, - screenResolution, -}) => { - const updateSetting = (key: keyof GridSettings, value: any) => { +/** + * ๊ฒฉ์ž ์„ค์ • ํŒจ๋„ (10px ๊ณ ์ • ๊ฒฉ์ž) + * + * ์‚ฌ์šฉ์ž ์„ค์ •: + * - ๊ฒฉ์ž ํ‘œ์‹œ ON/OFF + * - ๊ฒฉ์ž ์Šค๋ƒ… ON/OFF + * + * ์ž๋™ ์„ค์ • (๋ณ€๊ฒฝ ๋ถˆ๊ฐ€): + * - ๊ฒฉ์ž ํฌ๊ธฐ: 10px ๊ณ ์ • + * - ๊ฒฉ์ž ๊ฐ„๊ฒฉ: 10px ๊ณ ์ • + */ +export function GridPanel({ gridSettings, onGridSettingsChange }: GridPanelProps) { + const updateSetting = (key: K, value: GridSettings[K]) => { 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 ( -
- {/* ํ—ค๋” */} -
-
-
- -

๊ฒฉ์ž ์„ค์ •

-
- -
- {onForceGridUpdate && ( - - )} - - -
+ + +
+ + ๊ฒฉ์ž ์„ค์ • +
+
+ + {/* ๊ฒฉ์ž ํ‘œ์‹œ */} +
+ + updateSetting("showGrid", 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 -
-
+ {/* ๊ฒฉ์ž ์Šค๋ƒ… */} +
+ + updateSetting("snapToGrid", checked as boolean)} + />
- - - {/* ๊ฒฉ์ž ์Šคํƒ€์ผ */} -
-

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

- -
- -
- 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% -
-
+ {/* ๊ฒฉ์ž ์ •๋ณด (์ฝ๊ธฐ ์ „์šฉ) */} +
+

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

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

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

- -
-
- ์ปดํฌ๋„ŒํŠธ ์˜ˆ์‹œ -
-
-
-
- - {/* ํ‘ธํ„ฐ */} -
-
๐Ÿ’ก ๊ฒฉ์ž ์„ค์ •์€ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์บ”๋ฒ„์Šค์— ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค
- - {/* ํ•ด์ƒ๋„ ๋ฐ ๊ฒฉ์ž ์ •๋ณด */} - {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 abeff8d6..46bf55f8 100644 --- a/frontend/components/screen/panels/TablesPanel.tsx +++ b/frontend/components/screen/panels/TablesPanel.tsx @@ -53,12 +53,16 @@ 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 !placedColumns.has(columnKey); + // ๊ธฐ๋ณธ ์ปฌ๋Ÿผ ๋˜๋Š” ์ด๋ฏธ ๋ฐฐ์น˜๋œ ์ปฌ๋Ÿผ์€ ์ œ์™ธ + return !hiddenColumns.has(col.columnName) && !placedColumns.has(columnKey); }), })); diff --git a/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx b/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx index 6d063640..c37f0a85 100644 --- a/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx +++ b/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx @@ -125,6 +125,23 @@ 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) { @@ -135,17 +152,10 @@ export const UnifiedPropertiesPanel: React.FC = ({ } }; - // ๊ฒฉ์ž ์„ค์ • ๋ Œ๋”๋ง (early return ์ด์ „์— ์ •์˜) + // ๊ฒฉ์ž ์„ค์ • ๋ Œ๋”๋ง (10px ๊ณ ์ • ๊ฒฉ์ž) 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 (
@@ -154,7 +164,7 @@ export const UnifiedPropertiesPanel: React.FC = ({
- {/* ํ† ๊ธ€๋“ค */} + {/* ๊ฒฉ์ž ํ‘œ์‹œ */}
{gridSettings.showGrid ? ( @@ -168,11 +178,12 @@ export const UnifiedPropertiesPanel: React.FC = ({
updateGridSetting("showGrid", checked)} />
+ {/* ๊ฒฉ์ž ์Šค๋ƒ… */}
@@ -187,65 +198,14 @@ export const UnifiedPropertiesPanel: React.FC = ({ />
- {/* ์ปฌ๋Ÿผ ์ˆ˜ */} -
- -
- { - 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" - /> + {/* ๊ฒฉ์ž ์ •๋ณด (์ฝ๊ธฐ ์ „์šฉ) */} +
+

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

+
    +
  • โ€ข ๊ฒฉ์ž ํฌ๊ธฐ: 10px ๊ณ ์ •
  • +
  • โ€ข ์ปดํฌ๋„ŒํŠธ๋Š” 10px ๋‹จ์œ„๋กœ ๋ฐฐ์น˜๋ฉ๋‹ˆ๋‹ค
  • +
  • โ€ข ๊ฒฉ์ž ์Šค๋ƒ…์„ ๋„๋ฉด ์ž์œ ๋กญ๊ฒŒ ๋ฐฐ์น˜ ๊ฐ€๋Šฅ
  • +
@@ -455,47 +415,90 @@ export const UnifiedPropertiesPanel: React.FC = ({
)} - {/* Grid Columns + Z-Index (๊ฐ™์€ ํ–‰) */} + {/* Z-Index */} +
+ + handleUpdate("position.z", parseInt(e.target.value) || 1)} + className="h-6 w-full px-2 py-0 text-xs" + style={{ fontSize: "12px" }} + /> +
+ + {/* ํฌ๊ธฐ (๋„ˆ๋น„/๋†’์ด) */}
- {(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} - -
-
- )}
- + handleUpdate("position.z", parseInt(e.target.value) || 1)} + value={localSize.width} + onChange={(e) => { + // ์ž…๋ ฅ ์ค‘์—๋Š” ๋กœ์ปฌ ์ƒํƒœ๋งŒ ์—…๋ฐ์ดํŠธ + 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(); + } + }} 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 7ea3f6b4..d9f8316f 100644 --- a/frontend/lib/utils/gridUtils.ts +++ b/frontend/lib/utils/gridUtils.ts @@ -1,205 +1,77 @@ import { Position, Size } from "@/types/screen"; import { GridSettings } from "@/types/screen-management"; +// ๐ŸŽฏ 10px ๊ณ ์ • ๊ฒฉ์ž ์‹œ์Šคํ…œ +const GRID_SIZE = 10; // ๊ณ ์ •๊ฐ’ + export interface GridInfo { - columnWidth: number; + gridSize: number; // ํ•ญ์ƒ 10px 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 { - columnWidth: Math.max(columnWidth, MIN_COLUMN_WIDTH), + gridSize: GRID_SIZE, 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: snappedX, - y: snappedY, + x: Math.round(position.x / GRID_SIZE) * GRID_SIZE, + y: Math.round(position.y / GRID_SIZE) * GRID_SIZE, 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(columnWidth, snappedWidth), - height: snappedHeight, + 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), }; } /** - * ๊ฒฉ์ž ์ปฌ๋Ÿผ ์ˆ˜๋กœ ๋„ˆ๋น„ ๊ณ„์‚ฐ - */ -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))); -} - -/** - * ๊ฒฉ์ž ๊ฐ€์ด๋“œ๋ผ์ธ ์ƒ์„ฑ + * ๊ฒฉ์ž ๊ฐ€์ด๋“œ๋ผ์ธ ์ƒ์„ฑ (10px ๊ฐ„๊ฒฉ) */ 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 i = 0; i <= columns; i++) { - const x = padding + i * cellWidth; - if (x <= containerWidth) { - verticalLines.push(x); - } + for (let x = 0; x <= containerWidth; x += GRID_SIZE) { + verticalLines.push(x); } - // ๊ฐ€๋กœ ๊ฒฉ์ž์„  const horizontalLines: number[] = []; - for (let y = padding; y < containerHeight; y += cellHeight) { + for (let y = 0; y <= containerHeight; y += GRID_SIZE) { horizontalLines.push(y); } @@ -242,46 +114,21 @@ export function alignGroupChildrenToGrid( ): any[] { if (!gridSettings.snapToGrid || children.length === 0) return children; - console.log("๐Ÿ”ง alignGroupChildrenToGrid ์‹œ์ž‘:", { - childrenCount: children.length, - groupPosition, - gridInfo, - gridSettings, - }); - - return children.map((child, index) => { - console.log(`๐Ÿ“ ์ž์‹ ${index + 1} ์ฒ˜๋ฆฌ ์ค‘:`, { - childId: child.id, - originalPosition: child.position, - originalSize: child.size, - }); - - const { columnWidth } = gridInfo; - const { gap } = gridSettings; - - // ๊ทธ๋ฃน ๋‚ด๋ถ€ ํŒจ๋”ฉ ๊ณ ๋ คํ•œ ๊ฒฉ์ž ์ •๋ ฌ + return children.map((child) => { const padding = 16; - const effectiveX = child.position.x - padding; - const columnIndex = Math.round(effectiveX / (columnWidth + gap)); - const snappedX = padding + columnIndex * (columnWidth + gap); + + // 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); - // 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 = { + return { ...child, position: { - x: Math.max(padding, snappedX), // ํŒจ๋”ฉ๋งŒํผ ์ตœ์†Œ ์—ฌ๋ฐฑ ํ™•๋ณด - y: Math.max(padding, snappedY), + x: snappedX, + y: snappedY, z: child.position.z || 1, }, size: { @@ -289,26 +136,6 @@ 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; }); } @@ -317,19 +144,13 @@ 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: gridInfo.columnWidth * 2, height: 10 * 4 }; + return { width: GRID_SIZE * 20, height: GRID_SIZE * 10 }; } - 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), @@ -340,61 +161,38 @@ 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; - // ๊ทธ๋ฃน์€ ๊ฒฉ์ž ์Šค๋ƒ… ์—†์ด ์ปจํ…์ธ ์— ๋งž๋Š” ์ž์—ฐ์Šค๋Ÿฌ์šด ํฌ๊ธฐ - const padding = 16; // ๊ทธ๋ฃน ๋‚ด๋ถ€ ์—ฌ๋ฐฑ - const groupSize = { + return { 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 (!gridSettings.snapToGrid || children.length === 0) return children; +export function normalizeGroupChildPositions(children: any[], _gridSettings?: GridSettings): any[] { + if (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)); - - console.log("๐Ÿ“ ์ตœ์†Œ ์œ„์น˜:", { minX, minY }); - - // ๊ทธ๋ฃน ๋‚ด์—์„œ ์‹œ์ž‘์ ์„ ํŒจ๋”ฉ๋งŒํผ ๋–จ์–ด๋œจ๋ฆผ (์ž์—ฐ์Šค๋Ÿฌ์šด ์—ฌ๋ฐฑ) const padding = 16; - const startX = padding; - const startY = padding; - const normalizedChildren = children.map((child) => ({ + return children.map((child) => ({ ...child, position: { - x: child.position.x - minX + startX, - y: child.position.y - minY + startY, + x: child.position.x - minX + padding, + y: child.position.y - minY + padding, z: child.position.z || 1, }, })); - - console.log("โœ… ์ •๊ทœํ™” ์™„๋ฃŒ:", { - normalizedPositions: normalizedChildren.map((c) => ({ id: c.id, pos: c.position })), - }); - - return normalizedChildren; } + +// ๐Ÿ—‘๏ธ ์ œ๊ฑฐ๋œ ํ•จ์ˆ˜๋“ค (๋” ์ด์ƒ ํ•„์š” ์—†์Œ) +// - calculateWidthFromColumns +// - updateSizeFromGridColumns +// - adjustGridColumnsFromSize +// - calculateColumnsFromWidth diff --git a/frontend/types/screen-management.ts b/frontend/types/screen-management.ts index d83a6354..75c5d4d2 100644 --- a/frontend/types/screen-management.ts +++ b/frontend/types/screen-management.ts @@ -561,21 +561,22 @@ export interface LayoutData { } /** - * ๊ฒฉ์ž ์„ค์ • + * ๊ฒฉ์ž ์„ค์ • (10px ๊ณ ์ • ๊ฒฉ์ž) */ export interface GridSettings { - enabled: boolean; - size: number; - color: string; - opacity: number; - snapToGrid: boolean; - // gridUtils์—์„œ ํ•„์š”ํ•œ ์†์„ฑ๋“ค ์ถ”๊ฐ€ - columns: number; - gap: number; - padding: number; - showGrid?: boolean; - gridColor?: string; - gridOpacity?: number; + 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๋กœ ๋Œ€์ฒด } /** -- 2.43.0 From 1d6418ca63367a2baa2f3be49a3c8f3a9024982f Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 7 Nov 2025 17:39:51 +0900 Subject: [PATCH 04/11] =?UTF-8?q?fix:=20SaveModal=EC=9D=84=20ResizableDial?= =?UTF-8?q?og=EB=A1=9C=20=EC=88=98=EC=A0=95=ED=95=98=EC=97=AC=20=ED=81=AC?= =?UTF-8?q?=EA=B8=B0=20=EC=A1=B0=EC=A0=88=20=EA=B0=80=EB=8A=A5=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ์ฃผ์š” ๋ณ€๊ฒฝ์‚ฌํ•ญ: - Dialog/DialogContent๋ฅผ ResizableDialog/ResizableDialogContent๋กœ ๋ณ€๊ฒฝ - DialogTitle์„ ResizableDialogTitle๋กœ ๋ณ€๊ฒฝ - ๋‚ด๋ถ€ ์ปจํ…์ธ  ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์œ ์—ฐํ•œ ํฌ๊ธฐ(w-full h-full)๋กœ ๋ณ€๊ฒฝ - minWidth/minHeight ์‚ฌ์šฉ์œผ๋กœ ์ตœ์†Œ ํฌ๊ธฐ ๋ณด์žฅ ์ฐธ๊ณ : - ์ปดํฌ๋„ŒํŠธ ๋ ˆ์ด์•„์›ƒ์ด ํ™”๋ฉด๊ด€๋ฆฌ์—์„œ ์„ค์ •๋œ ๋Œ€๋กœ ์ •ํ™•ํžˆ ๋ Œ๋”๋ง๋จ - ๋ ˆ์ด์•„์›ƒ ์ž์ฒด์˜ ๋ฌธ์ œ๋Š” ํ™”๋ฉด๊ด€๋ฆฌ์—์„œ ์žฌ์„ค๊ณ„ ํ•„์š” ํŒŒ์ผ ๋ณ€๊ฒฝ: - frontend/components/screen/SaveModal.tsx --- frontend/components/screen/SaveModal.tsx | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/frontend/components/screen/SaveModal.tsx b/frontend/components/screen/SaveModal.tsx index 172d5f49..d42ffd2e 100644 --- a/frontend/components/screen/SaveModal.tsx +++ b/frontend/components/screen/SaveModal.tsx @@ -1,7 +1,7 @@ "use client"; import React, { useState, useEffect } from "react"; -import { ResizableDialog, ResizableDialogContent, ResizableDialogHeader, DialogTitle } from "@/components/ui/resizable-dialog"; +import { ResizableDialog, ResizableDialogContent, ResizableDialogHeader, ResizableDialogTitle } from "@/components/ui/resizable-dialog"; import { Button } from "@/components/ui/button"; import { X, Save, Loader2 } from "lucide-react"; import { toast } from "sonner"; @@ -213,9 +213,9 @@ export const SaveModal: React.FC = ({ const dynamicSize = calculateDynamicSize(); return ( - !isSaving && !open && onClose()}> - - + !isSaving && !open && onClose()}> + +
{initialData ? "๋ฐ์ดํ„ฐ ์ˆ˜์ •" : "๋ฐ์ดํ„ฐ ๋“ฑ๋ก"}
@@ -237,7 +237,7 @@ export const SaveModal: React.FC = ({
-
+
{loading ? ( @@ -246,14 +246,13 @@ export const SaveModal: React.FC = ({
) : screenData && components.length > 0 ? (
-
+
{components.map((component, index) => (
= ({
ํ™”๋ฉด์— ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.
)}
- -
+ + ); }; -- 2.43.0 From e2f4b475888daa06111882e65adc42d5d67839a1 Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 10 Nov 2025 09:33:29 +0900 Subject: [PATCH 05/11] =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EC=9E=98=20?= =?UTF-8?q?=EB=B3=B4=EC=9D=B4=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components/common/ScreenModal.tsx | 10 ++- .../screen/InteractiveScreenViewer.tsx | 15 ++-- .../screen/InteractiveScreenViewerDynamic.tsx | 15 ++-- frontend/components/screen/SaveModal.tsx | 69 +++++++++++++++---- frontend/components/ui/resizable-dialog.tsx | 8 ++- 5 files changed, 85 insertions(+), 32 deletions(-) diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx index 50423460..609c2b43 100644 --- a/frontend/components/common/ScreenModal.tsx +++ b/frontend/components/common/ScreenModal.tsx @@ -364,7 +364,7 @@ export const ScreenModal: React.FC = ({ className }) => {
-
+
{loading ? (
@@ -374,13 +374,11 @@ export const ScreenModal: React.FC = ({ className }) => {
) : screenData ? (
{screenData.components.map((component) => { diff --git a/frontend/components/screen/InteractiveScreenViewer.tsx b/frontend/components/screen/InteractiveScreenViewer.tsx index 9c0076ee..472049ff 100644 --- a/frontend/components/screen/InteractiveScreenViewer.tsx +++ b/frontend/components/screen/InteractiveScreenViewer.tsx @@ -401,15 +401,14 @@ export const InteractiveScreenViewer: React.FC = ( const applyStyles = (element: React.ReactElement) => { if (!comp.style) return element; + // โœ… ๊ฒฉ์ž ์‹œ์Šคํ…œ ์ž”์žฌ ์ œ๊ฑฐ: style.width, style.height๋Š” ๋ฌด์‹œ + // size.width, size.height๊ฐ€ ๋ถ€๋ชจ ์ปจํ…Œ์ด๋„ˆ์—์„œ ์ ์šฉ๋˜๋ฏ€๋กœ + const { width, height, ...styleWithoutSize } = comp.style; + return React.cloneElement(element, { style: { ...element.props.style, // ๊ธฐ์กด ์Šคํƒ€์ผ ์œ ์ง€ - ...comp.style, - // ํฌ๊ธฐ๋Š” ๋ถ€๋ชจ ์ปจํ…Œ์ด๋„ˆ์—์„œ ์ฒ˜๋ฆฌํ•˜๋ฏ€๋กœ ์ œ๊ฑฐ (ํ•˜์ง€๋งŒ ๋‹ค๋ฅธ ์Šคํƒ€์ผ์€ ์œ ์ง€) - width: "100%", - height: "100%", - minHeight: "100%", - maxHeight: "100%", + ...styleWithoutSize, // width/height ์ œ์™ธํ•œ ์Šคํƒ€์ผ๋งŒ ์ ์šฉ boxSizing: "border-box", }, }); @@ -1887,7 +1886,7 @@ export const InteractiveScreenViewer: React.FC = ( return ( <> -
+
{/* ๋ผ๋ฒจ์ด ์žˆ๋Š” ๊ฒฝ์šฐ ํ‘œ์‹œ (๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ” ์ œ์™ธ) */} {shouldShowLabel && (
{/* ๊ฐœ์„ ๋œ ๊ฒ€์ฆ ํŒจ๋„ (์„ ํƒ์  ํ‘œ์‹œ) */} diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index 7ad86f9c..1fb10716 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -343,10 +343,14 @@ export const InteractiveScreenViewerDynamic: React.FC { if (!comp.style) return element; + // โœ… ๊ฒฉ์ž ์‹œ์Šคํ…œ ์ž”์žฌ ์ œ๊ฑฐ: style.width, style.height๋Š” ๋ฌด์‹œ + // size.width, size.height๊ฐ€ ๋ถ€๋ชจ ์ปจํ…Œ์ด๋„ˆ์—์„œ ์ ์šฉ๋˜๋ฏ€๋กœ + const { width, height, ...styleWithoutSize } = comp.style; + return React.cloneElement(element, { style: { ...element.props.style, - ...comp.style, + ...styleWithoutSize, // width/height ์ œ์™ธํ•œ ์Šคํƒ€์ผ๋งŒ ์ ์šฉ width: "100%", height: "100%", minHeight: "100%", @@ -676,14 +680,17 @@ export const InteractiveScreenViewerDynamic: React.FC = ({ const calculateDynamicSize = () => { if (!components.length) return { width: 800, height: 600 }; - const maxX = Math.max(...components.map((c) => (c.position?.x || 0) + (c.size?.width || 200))); - const maxY = Math.max(...components.map((c) => (c.position?.y || 0) + (c.size?.height || 40))); + const maxX = Math.max(...components.map((c) => { + const x = c.position?.x || 0; + const width = typeof c.size?.width === 'number' + ? c.size.width + : parseInt(String(c.size?.width || 200), 10); + return x + width; + })); + + const maxY = Math.max(...components.map((c) => { + const y = c.position?.y || 0; + const height = typeof c.size?.height === 'number' + ? c.size.height + : parseInt(String(c.size?.height || 40), 10); + return y + height; + })); const padding = 40; return { @@ -214,8 +227,15 @@ export const SaveModal: React.FC = ({ return ( !isSaving && !open && onClose()}> - - + +
{initialData ? "๋ฐ์ดํ„ฐ ์ˆ˜์ •" : "๋ฐ์ดํ„ฐ ๋“ฑ๋ก"}
@@ -239,29 +259,51 @@ export const SaveModal: React.FC = ({
-
+
{loading ? (
) : screenData && components.length > 0 ? (
-
- {components.map((component, index) => ( +
+ {components.map((component, index) => { + // โœ… ๊ฒฉ์ž ์‹œ์Šคํ…œ ์ž”์žฌ ์ œ๊ฑฐ: size์˜ ํ”ฝ์…€ ๊ฐ’๋งŒ ์‚ฌ์šฉ + const widthPx = typeof component.size?.width === 'number' + ? component.size.width + : parseInt(String(component.size?.width || 200), 10); + const heightPx = typeof component.size?.height === 'number' + ? component.size.height + : parseInt(String(component.size?.height || 40), 10); + + // ๋””๋ฒ„๊น…: ์‹ค์ œ ํฌ๊ธฐ ํ™•์ธ + if (index === 0) { + console.log('๐Ÿ” SaveModal ์ปดํฌ๋„ŒํŠธ ํฌ๊ธฐ:', { + componentId: component.id, + 'size.width (์›๋ณธ)': component.size?.width, + 'size.width ํƒ€์ž…': typeof component.size?.width, + 'widthPx (๊ณ„์‚ฐ)': widthPx, + 'style.width': component.style?.width, + }); + } + + return (
@@ -306,7 +348,8 @@ export const SaveModal: React.FC = ({ /> )}
- ))} + ); + })}
) : ( diff --git a/frontend/components/ui/resizable-dialog.tsx b/frontend/components/ui/resizable-dialog.tsx index facb07ca..9e953784 100644 --- a/frontend/components/ui/resizable-dialog.tsx +++ b/frontend/components/ui/resizable-dialog.tsx @@ -55,6 +55,7 @@ interface ResizableDialogContentProps modalId?: string; // localStorage ์ €์žฅ์šฉ ๊ณ ์œ  ID userId?: string; // ์‚ฌ์šฉ์ž๋ณ„ ์ €์žฅ์šฉ open?: boolean; // ๐Ÿ†• ๋ชจ๋‹ฌ ์—ด๋ฆผ/๋‹ซํž˜ ์ƒํƒœ (์™ธ๋ถ€์—์„œ ์ „๋‹ฌ) + disableFlexLayout?: boolean; // ๐Ÿ†• flex ๋ ˆ์ด์•„์›ƒ ๋น„ํ™œ์„ฑํ™” (absolute ๋ ˆ์ด์•„์›ƒ์šฉ) } const ResizableDialogContent = React.forwardRef< @@ -74,6 +75,7 @@ const ResizableDialogContent = React.forwardRef< modalId, userId = "guest", open: externalOpen, // ๐Ÿ†• ์™ธ๋ถ€์—์„œ ์ „๋‹ฌ๋ฐ›์€ open ์ƒํƒœ + disableFlexLayout = false, // ๐Ÿ†• flex ๋ ˆ์ด์•„์›ƒ ๋น„ํ™œ์„ฑํ™” style: userStyle, ...props }, @@ -373,7 +375,11 @@ const ResizableDialogContent = React.forwardRef< minHeight: `${minHeight}px`, }} > -
+
{children}
-- 2.43.0 From 61dc48e638b44bae207fc3c9d59af0bc4b40ba30 Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 10 Nov 2025 11:59:57 +0900 Subject: [PATCH 06/11] Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management -- 2.43.0 From 02644f38eeef268b6e3a6df18618288e92a109db Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 10 Nov 2025 12:00:21 +0900 Subject: [PATCH 07/11] Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management -- 2.43.0 From 15f21a1142e08991e9820fbdffb269f73fbfb9ff Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 10 Nov 2025 14:21:29 +0900 Subject: [PATCH 08/11] =?UTF-8?q?revert:=20e27845a=20=EC=BB=A4=EB=B0=8B?= =?UTF-8?q?=EC=9D=98=20=EB=B3=80=EA=B2=BD=EC=82=AC=ED=95=AD=20=EB=90=98?= =?UTF-8?q?=EB=8F=8C=EB=A6=BC=20-=20=ED=99=94=EB=A9=B4=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=95=84=EC=9B=83=20=EB=AC=B8=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cursorrules | 17 - .gitignore | 3 +- .../components/screen/RealtimePreview.tsx | 24 +- .../screen/RealtimePreviewDynamic.tsx | 3 - frontend/components/screen/ScreenDesigner.tsx | 483 +++++++++++++++--- .../components/screen/panels/GridPanel.tsx | 370 +++++++++++--- .../components/screen/panels/TablesPanel.tsx | 8 +- .../screen/panels/UnifiedPropertiesPanel.tsx | 209 ++++---- frontend/lib/utils/gridUtils.ts | 300 +++++++++-- frontend/types/screen-management.ts | 27 +- 10 files changed, 1095 insertions(+), 349 deletions(-) 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; } /** -- 2.43.0 From a868c5c413cccafd65a94786eb516871aa1a7e0f Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 10 Nov 2025 14:24:16 +0900 Subject: [PATCH 09/11] =?UTF-8?q?feat:=20=ED=85=8C=EC=9D=B4=EB=B8=94=20?= =?UTF-8?q?=ED=83=AD=EC=97=90=EC=84=9C=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20?= =?UTF-8?q?=EC=BB=AC=EB=9F=BC=205=EA=B0=9C=20=EC=88=A8=EA=B9=80=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - id, created_date, updated_date, writer, company_code ์ปฌ๋Ÿผ ํ•„ํ„ฐ๋ง - ๋Œ€์†Œ๋ฌธ์ž ๊ตฌ๋ถ„ ์—†์ด ์‹œ์Šคํ…œ ์ปฌ๋Ÿผ ์ œ์™ธ - ํ™”๋ฉด ํŽธ์ง‘๊ธฐ ํ…Œ์ด๋ธ” ํƒญ์—์„œ ๋น„์ฆˆ๋‹ˆ์Šค ์ปฌ๋Ÿผ๋งŒ ํ‘œ์‹œ --- .../ai-developer-collaboration-rules.mdc | 281 ++++++++++++++++++ .../components/screen/panels/TablesPanel.tsx | 14 +- 2 files changed, 293 insertions(+), 2 deletions(-) create mode 100644 .cursor/rules/ai-developer-collaboration-rules.mdc diff --git a/.cursor/rules/ai-developer-collaboration-rules.mdc b/.cursor/rules/ai-developer-collaboration-rules.mdc new file mode 100644 index 00000000..ccdcc9fc --- /dev/null +++ b/.cursor/rules/ai-developer-collaboration-rules.mdc @@ -0,0 +1,281 @@ +# AI-๊ฐœ๋ฐœ์ž ํ˜‘์—… ์ž‘์—… ์ˆ˜์น™ + +## ํ•ต์‹ฌ ์›์น™: "์ถ”์ธก ๊ธˆ์ง€, ํ™•์ธ ํ•„์ˆ˜" + +AI๋Š” ์ฝ”๋“œ ์ž‘์„ฑ ์ „์— ๋ฐ˜๋“œ์‹œ ์‹ค์ œ ์ƒํ™ฉ์„ ํ™•์ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. + +--- + +## 1. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ด€๋ จ ์ž‘์—… + +### ํ•„์ˆ˜ ํ™•์ธ ์‚ฌํ•ญ + +- โœ… **ํ•ญ์ƒ MCP Postgres๋กœ ์‹ค์ œ ํ…Œ์ด๋ธ” ๊ตฌ์กฐ๋ฅผ ๋จผ์ € ํ™•์ธ** +- โœ… ์ปฌ๋Ÿผ๋ช…, ๋ฐ์ดํ„ฐ ํƒ€์ž…, ์ œ์•ฝ์กฐ๊ฑด์„ ์ถ”์ธกํ•˜์ง€ ๋ง๊ณ  ์ฟผ๋ฆฌ๋กœ ํ™•์ธ +- โœ… ๋ณ€๊ฒฝ ํ›„ ์‹ค์ œ๋กœ ๋ฐ์ดํ„ฐ๊ฐ€ ์–ด๋–ป๊ฒŒ ๋ณด์ด๋Š”์ง€ SELECT๋กœ ๊ฒ€์ฆ + +### ํ™•์ธ ๋ฐฉ๋ฒ• + +```sql +-- ํ…Œ์ด๋ธ” ๊ตฌ์กฐ ํ™•์ธ +SELECT + column_name, + data_type, + is_nullable, + column_default +FROM information_schema.columns +WHERE table_name = 'ํ…Œ์ด๋ธ”๋ช…' +ORDER BY ordinal_position; + +-- ์‹ค์ œ ๋ฐ์ดํ„ฐ ํ™•์ธ +SELECT * FROM ํ…Œ์ด๋ธ”๋ช… LIMIT 5; +``` + +### ๊ธˆ์ง€ ์‚ฌํ•ญ + +- โŒ "์•„๋งˆ๋„ `created_at` ์ปฌ๋Ÿผ์ผ ๊ฒƒ์ž…๋‹ˆ๋‹ค" โ†’ ํ™•์ธ ํ•„์ˆ˜! +- โŒ "๋ณดํ†ต ์ด๋ ‡๊ฒŒ ๋˜์–ด์žˆ์Šต๋‹ˆ๋‹ค" โ†’ ์ด ํ”„๋กœ์ ํŠธ์—์„œ ํ™•์ธ! +- โŒ ๋‹ค๋ฅธ ํ…Œ์ด๋ธ” ๊ตฌ์กฐ๋ฅผ ๋ณด๊ณ  ์ถ”์ธก โ†’ ๊ฐ ํ…Œ์ด๋ธ”๋งˆ๋‹ค ํ™•์ธ! + +--- + +## 2. ์ฝ”๋“œ ์ˆ˜์ • ์ž‘์—… + +### ์ž‘์—… ์ „ + +1. **๊ด€๋ จ ํŒŒ์ผ ์ฝ๊ธฐ**: ์ˆ˜์ •ํ•  ํŒŒ์ผ์˜ ํ˜„์žฌ ์ƒํƒœ ํ™•์ธ +2. **์˜์กด์„ฑ ํŒŒ์•…**: ๋‹ค๋ฅธ ํŒŒ์ผ์— ์˜ํ–ฅ์ด ์žˆ๋Š”์ง€ ๊ฒ€์ƒ‰ +3. **๊ธฐ์กด ํŒจํ„ด ํ™•์ธ**: ํ”„๋กœ์ ํŠธ์˜ ์ฝ”๋”ฉ ์Šคํƒ€์ผ ์ค€์ˆ˜ + +### ์ž‘์—… ์ค‘ + +1. **ํ•œ ๋ฒˆ์— ํ•˜๋‚˜์”ฉ**: ํ•˜๋‚˜์˜ ๋ช…ํ™•ํ•œ ์ž‘์—…๋งŒ ์ˆ˜ํ–‰ +2. **๋กœ๊ทธ ์ถ”๊ฐ€**: ๋””๋ฒ„๊น…์ด ํ•„์š”ํ•˜๋ฉด ๋ช…ํ™•ํ•œ ๋กœ๊ทธ ์ถ”๊ฐ€ +3. **์ ์ง„์  ์ˆ˜์ •**: ํฐ ๋ณ€๊ฒฝ์€ ์—ฌ๋Ÿฌ ๋‹จ๊ณ„๋กœ ๋‚˜๋ˆ” + +### ์ž‘์—… ํ›„ + +1. **๋กœ๊ทธ ์ œ๊ฑฐ**: ๋””๋ฒ„๊น… ๋กœ๊ทธ๋Š” ๋ฐ˜๋“œ์‹œ ์ œ๊ฑฐ +2. **ํ…Œ์ŠคํŠธ ์ œ์•ˆ**: ๋ธŒ๋ผ์šฐ์ €๋กœ ํ…Œ์ŠคํŠธํ•  ๊ฒƒ์„ ์ œ์•ˆ +3. **๋ณ€๊ฒฝ์‚ฌํ•ญ ์š”์•ฝ**: ๋ฌด์—‡์„ ์–ด๋–ป๊ฒŒ ๋ฐ”๊ฟจ๋Š”์ง€ ๋ช…ํ™•ํžˆ ์„ค๋ช… + +--- + +## 3. ํ™•์ธ ๋ฐ ๊ฒ€์ฆ + +### ํ™•์ธ ๋„๊ตฌ ์‚ฌ์šฉ + +- **MCP Postgres**: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ตฌ์กฐ ๋ฐ ๋ฐ์ดํ„ฐ ํ™•์ธ +- **MCP Browser**: ์‹ค์ œ ํ™”๋ฉด์—์„œ ๋™์ž‘ ํ™•์ธ +- **codebase_search**: ๊ด€๋ จ ์ฝ”๋“œ ํŒจํ„ด ๊ฒ€์ƒ‰ +- **grep**: ํŠน์ • ๋ฌธ์ž์—ด ์‚ฌ์šฉ์ฒ˜ ์ฐพ๊ธฐ + +### ๊ฒ€์ฆ ํ”„๋กœ์„ธ์Šค + +1. **๋ณ€๊ฒฝ ์ „ ์ƒํƒœ ํ™•์ธ** โ†’ ๋ฌธ์ œ ํŒŒ์•… +2. **๋ณ€๊ฒฝ ์ ์šฉ** +3. **๋ณ€๊ฒฝ ํ›„ ์ƒํƒœ ํ™•์ธ** โ†’ ํ•ด๊ฒฐ ๊ฒ€์ฆ +4. **๋ถ€์ž‘์šฉ ํ™•์ธ** โ†’ ๋‹ค๋ฅธ ๊ธฐ๋Šฅ์— ์˜ํ–ฅ ์—†๋Š”์ง€ + +### ์‚ฌ์šฉ์ž ํ”ผ๋“œ๋ฐฑ ๋Œ€์‘ + +- ์‚ฌ์šฉ์ž๊ฐ€ "ํ™•์ธ ์•ˆํ•˜์ง€?"๋ผ๊ณ  ํ•˜๋ฉด: + 1. ์ฆ‰์‹œ ์‚ฌ๊ณผ + 2. MCP/๋ธŒ๋ผ์šฐ์ €๋กœ ์‹ค์ œ ํ™•์ธ + 3. ์ •ํ™•ํ•œ ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์žฌ์ž‘์—… + +--- + +## 4. ์ปค๋ฎค๋‹ˆ์ผ€์ด์…˜ + +### ์ž‘์—… ์‹œ์ž‘ ์‹œ + +``` +โœ… ์ข‹์€ ์˜ˆ: +"MCP๋กœ item_info ํ…Œ์ด๋ธ” ๊ตฌ์กฐ๋ฅผ ๋จผ์ € ํ™•์ธํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค." + +โŒ ๋‚˜์œ ์˜ˆ: +"๋ณดํ†ต created_at ์ปฌ๋Ÿผ์ด ์žˆ์„ ๊ฒƒ์ด๋ฏ€๋กœ ์ˆ˜์ •ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค." +``` + +### ์ž‘์—… ์™„๋ฃŒ ์‹œ + +``` +โœ… ์ข‹์€ ์˜ˆ: +"์™„๋ฃŒ! ๋‘ ๊ฐ€์ง€๋ฅผ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค: +1. ๊ธฐ๋ณธ ๋†’์ด๋ฅผ 40px โ†’ 30px๋กœ ๋ณ€๊ฒฝ (ScreenDesigner.tsx:2174) +2. ์ˆจ๊น€ ์ปฌ๋Ÿผ์„ created_date, updated_date, writer, company_code๋กœ ์ˆ˜์ • (TablesPanel.tsx:57) + +ํ…Œ์ŠคํŠธํ•ด๋ณด์„ธ์š”!" + +โŒ ๋‚˜์œ ์˜ˆ: +"์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค!" +``` + +### ๋ถˆํ™•์‹คํ•  ๋•Œ + +``` +โœ… ์ข‹์€ ์˜ˆ: +"์ปฌ๋Ÿผ๋ช…์ด created_at์ธ์ง€ created_date์ธ์ง€ ํ™•์‹คํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. +MCP๋กœ ํ™•์ธํ•ด๋„ ๋ ๊นŒ์š”?" + +โŒ ๋‚˜์œ ์˜ˆ: +"created_at์ผ ๊ฒƒ ๊ฐ™์œผ๋‹ˆ ์ผ๋‹จ ์ด๋ ‡๊ฒŒ ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค." +``` + +--- + +## 5. ๊ธˆ์ง€ ์‚ฌํ•ญ + +### ์ ˆ๋Œ€ ๊ธˆ์ง€ + +1. โŒ **ํ™•์ธ ์—†์ด "์™„๋ฃŒํ–ˆ์Šต๋‹ˆ๋‹ค" ๋งํ•˜๊ธฐ** + - ๋ฐ˜๋“œ์‹œ ์‹ค์ œ๋กœ ํ™•์ธํ•˜๊ณ  ๋ณด๊ณ  +2. โŒ **์ด์ „์— ์‹คํŒจํ•œ ๋ฐฉ๋ฒ• ๋ฐ˜๋ณตํ•˜๊ธฐ** + - ๊ฐ™์€ ์‹ค์ˆ˜๋ฅผ ๋‘ ๋ฒˆ ํ•˜์ง€ ์•Š๊ธฐ +3. โŒ **๋””๋ฒ„๊น… ๋กœ๊ทธ๋ฅผ ๋‚จ๊ฒจ๋‘” ์ฑ„ ์ž‘์—… ์ข…๋ฃŒ** + - ๋ชจ๋“  console.log ์ œ๊ฑฐ ํ™•์ธ +4. โŒ **์ถ”์ธก์œผ๋กœ ๋‹ต๋ณ€ํ•˜๊ธฐ** + + - "์•„๋งˆ๋„", "๋ณดํ†ต", "์ผ๋ฐ˜์ ์œผ๋กœ" ๊ธˆ์ง€ + - ํ™•์‹คํ•˜์ง€ ์•Š์œผ๋ฉด ๋จผ์ € ํ™•์ธ + +5. โŒ **์—ฌ๋Ÿฌ ๋ฌธ์ œ๋ฅผ ํ•œ ๋ฒˆ์— ์ˆ˜์ •ํ•˜๋ ค๊ณ  ์‹œ๋„** + - ํ•œ ๋ฒˆ์— ํ•˜๋‚˜์”ฉ ํ•ด๊ฒฐ + +--- + +## 6. ํ”„๋กœ์ ํŠธ ํŠน๋ณ„ ๊ทœ์น™ + +### ๋ฐฑ์—”๋“œ ๊ด€๋ จ + +- ๐Ÿ”ฅ **๋ฐฑ์—”๋“œ ์žฌ์‹œ์ž‘ ์ ˆ๋Œ€ ๊ธˆ์ง€** (์‚ฌ์šฉ์ž ๋ช…์‹œ ๊ทœ์น™) +- ๐Ÿ”ฅ Node.js ํ”„๋กœ์„ธ์Šค๋ฅผ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š์Œ + +### ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ด€๋ จ + +- ๐Ÿ”ฅ **๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๊ทœ์น™ ์ค€์ˆ˜** + - ๋ชจ๋“  ์ฟผ๋ฆฌ์— `company_code` ํ•„ํ„ฐ๋ง ํ•„์ˆ˜ + - `company_code = "*"`๋Š” ์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ „์šฉ + - ์ž์„ธํ•œ ๋‚ด์šฉ: `.cursor/rules/multi-tenancy-guide.mdc` + +### API ๊ด€๋ จ + +- ๐Ÿ”ฅ **API ํด๋ผ์ด์–ธํŠธ ์‚ฌ์šฉ ํ•„์ˆ˜** + - `fetch()` ์ง์ ‘ ์‚ฌ์šฉ ๊ธˆ์ง€ + - `lib/api/` ์˜ ํด๋ผ์ด์–ธํŠธ ํ•จ์ˆ˜ ์‚ฌ์šฉ + - ํ™˜๊ฒฝ๋ณ„ URL ์ž๋™ ์ฒ˜๋ฆฌ + +### UI ๊ด€๋ จ + +- ๐Ÿ”ฅ **shadcn/ui ์Šคํƒ€์ผ ๊ฐ€์ด๋“œ ์ค€์ˆ˜** + - CSS ๋ณ€์ˆ˜ ์‚ฌ์šฉ (ํ•˜๋“œ์ฝ”๋”ฉ ๊ธˆ์ง€) + - ์ค‘์ฒฉ ๋ฐ•์Šค ๊ธˆ์ง€ (๋ช…์‹œ ์š”์ฒญ ์ „๊นŒ์ง€) + - ์ด๋ชจ์ง€ ์‚ฌ์šฉ ๊ธˆ์ง€ (๋ช…์‹œ ์š”์ฒญ ์ „๊นŒ์ง€) + +--- + +## 7. ์—๋Ÿฌ ์ฒ˜๋ฆฌ + +### ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ํ”„๋กœ์„ธ์Šค + +1. **์—๋Ÿฌ ๋กœ๊ทธ ์ „์ฒด ์ฝ๊ธฐ** + + - ์Šคํƒ ํŠธ๋ ˆ์ด์Šค ํ™•์ธ + - ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ •ํ™•ํžˆ ํŒŒ์•… + +2. **๊ทผ๋ณธ ์›์ธ ํŒŒ์•…** + + - ์ฆ์ƒ์ด ์•„๋‹Œ ์›์ธ ์ฐพ๊ธฐ + - ์™œ ์ด ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋Š”์ง€ ์ดํ•ด + +3. **ํ•ด๊ฒฐ์ฑ… ์ ์šฉ** + + - ์ž„์‹œ๋ฐฉํŽธ์ด ์•„๋‹Œ ๊ทผ๋ณธ์  ํ•ด๊ฒฐ + - ๊ฐ™์€ ์—๋Ÿฌ๊ฐ€ ์žฌ๋ฐœํ•˜์ง€ ์•Š๋„๋ก + +4. **๊ฒ€์ฆ** + - ์‹ค์ œ๋กœ ์—๋Ÿฌ๊ฐ€ ํ•ด๊ฒฐ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ + - ๋‹ค๋ฅธ ๋ถ€์ž‘์šฉ์€ ์—†๋Š”์ง€ ํ™•์ธ + +### ์—๋Ÿฌ ๋กœ๊น… + +```typescript +// โœ… ์ข‹์€ ๋กœ๊ทธ (๋””๋ฒ„๊น… ์‹œ) +console.log("๐Ÿ” [์ปดํฌ๋„ŒํŠธ๋ช…] ์ž‘์—…๋ช…:", { + ๊ด€๋ จ๋ณ€์ˆ˜1, + ๊ด€๋ จ๋ณ€์ˆ˜2, + ์˜ˆ์ƒ๊ฒฐ๊ณผ, +}); + +// โŒ ๋‚˜์œ ๋กœ๊ทธ +console.log("here"); +console.log(data); // ๋ฌด์Šจ ๋ฐ์ดํ„ฐ์ธ์ง€ ์•Œ ์ˆ˜ ์—†์Œ +``` + +--- + +## 8. ์ž‘์—… ์™„๋ฃŒ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +๋ชจ๋“  ์ž‘์—… ์™„๋ฃŒ ์ „์— ๋‹ค์Œ์„ ํ™•์ธ: + +- [ ] ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค/ํŒŒ์ผ์„ ํ™•์ธํ–ˆ๋Š”๊ฐ€? +- [ ] ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ์˜๋„๋Œ€๋กœ ์ž‘๋™ํ•˜๋Š”๊ฐ€? +- [ ] ๋””๋ฒ„๊น… ๋กœ๊ทธ๋ฅผ ๋ชจ๋‘ ์ œ๊ฑฐํ–ˆ๋Š”๊ฐ€? +- [ ] ๋‹ค๋ฅธ ๊ธฐ๋Šฅ์— ๋ถ€์ž‘์šฉ์ด ์—†๋Š”๊ฐ€? +- [ ] ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๊ทœ์น™์„ ์ค€์ˆ˜ํ–ˆ๋Š”๊ฐ€? +- [ ] ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ช…ํ™•ํžˆ ์„ค๋ช…ํ–ˆ๋Š”๊ฐ€? + +--- + +## 9. ๋ชจ๋ฒ” ์‚ฌ๋ก€ + +### ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ™•์ธ ์˜ˆ์‹œ + +```typescript +// 1. MCP๋กœ ํ…Œ์ด๋ธ” ๊ตฌ์กฐ ํ™•์ธ +mcp_postgres_query: SELECT column_name FROM information_schema.columns +WHERE table_name = 'item_info'; + +// 2. ์‹ค์ œ ์ปฌ๋Ÿผ๋ช… ํ™•์ธ ํ›„ ์ฝ”๋“œ ์ž‘์„ฑ +const hiddenColumns = new Set([ + 'id', + 'created_date', // โœ… ์‹ค์ œ ํ™•์ธํ•œ ์ปฌ๋Ÿผ๋ช… + 'updated_date', // โœ… ์‹ค์ œ ํ™•์ธํ•œ ์ปฌ๋Ÿผ๋ช… + 'writer', // โœ… ์‹ค์ œ ํ™•์ธํ•œ ์ปฌ๋Ÿผ๋ช… + 'company_code' +]); +``` + +### ๋ธŒ๋ผ์šฐ์ € ํ…Œ์ŠคํŠธ ์ œ์•ˆ ์˜ˆ์‹œ + +``` +"์ˆ˜์ •์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค! + +๋‹ค์Œ์„ ํ…Œ์ŠคํŠธํ•ด์ฃผ์„ธ์š”: +1. ํ™”๋ฉด๊ด€๋ฆฌ > ํ…Œ์ด๋ธ” ํƒญ ์—ด๊ธฐ +2. item_info ํ…Œ์ด๋ธ” ํ™•์ธ +3. ๊ธฐ๋ณธ 5๊ฐœ ์ปฌ๋Ÿผ(id, created_date ๋“ฑ)์ด ์•ˆ ๋ณด์ด๋Š”์ง€ ํ™•์ธ +4. ์ƒˆ ์ปฌ๋Ÿผ ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ ์‹œ ๋†’์ด๊ฐ€ 30px์ธ์ง€ ํ™•์ธ + +๋ธŒ๋ผ์šฐ์ € ํ…Œ์ŠคํŠธ๋ฅผ ์›ํ•˜์‹œ๋ฉด ๋ง์”€ํ•ด์ฃผ์„ธ์š”!" +``` + +--- + +## 10. ์š”์•ฝ: ํ•ต์‹ฌ 3์›์น™ + +1. **ํ™•์ธ ์šฐ์„ ** ๐Ÿ” + + - ์ถ”์ธกํ•˜์ง€ ๋ง๊ณ , ํ•ญ์ƒ ํ™•์ธํ•˜๊ณ  ์ž‘์—… + +2. **ํ•œ ๋ฒˆ์— ํ•˜๋‚˜** ๐ŸŽฏ + + - ์—ฌ๋Ÿฌ ๋ฌธ์ œ๋ฅผ ๋™์‹œ์— ํ•ด๊ฒฐํ•˜๋ ค ํ•˜์ง€ ๋ง๊ธฐ + +3. **์ฒ ์ €ํ•œ ๋งˆ๋ฌด๋ฆฌ** โœจ + - ๋กœ๊ทธ ์ œ๊ฑฐ, ํ…Œ์ŠคํŠธ, ๋ช…ํ™•ํ•œ ์„ค๋ช… + +--- + +**์ด ๊ทœ์น™์„ ์ง€ํ‚ค์ง€ ์•Š์œผ๋ฉด ์‚ฌ์šฉ์ž์—๊ฒŒ "ํ™•์ธ ์•ˆํ•˜์ง€?"๋ผ๋Š” ๋ง์„ ๋“ฃ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค!** diff --git a/frontend/components/screen/panels/TablesPanel.tsx b/frontend/components/screen/panels/TablesPanel.tsx index abeff8d6..0fb036e4 100644 --- a/frontend/components/screen/panels/TablesPanel.tsx +++ b/frontend/components/screen/panels/TablesPanel.tsx @@ -53,12 +53,22 @@ export const TablesPanel: React.FC = ({ onDragStart, placedColumns = new Set(), }) => { - // ์ด๋ฏธ ๋ฐฐ์น˜๋œ ์ปฌ๋Ÿผ์„ ์ œ์™ธํ•œ ํ…Œ์ด๋ธ” ์ •๋ณด ์ƒ์„ฑ + // ์‹œ์Šคํ…œ ์ปฌ๋Ÿผ ๋ชฉ๋ก (์ˆจ๊น€ ์ฒ˜๋ฆฌ) + const systemColumns = 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 !placedColumns.has(columnKey); + // ์‹œ์Šคํ…œ ์ปฌ๋Ÿผ์ด๊ฑฐ๋‚˜ ์ด๋ฏธ ๋ฐฐ์น˜๋œ ์ปฌ๋Ÿผ์€ ์ œ์™ธ + return !systemColumns.has(col.columnName.toLowerCase()) && !placedColumns.has(columnKey); }), })); -- 2.43.0 From 3f3299601430556984ffd7ed86ee121e45abe83a Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 10 Nov 2025 14:33:15 +0900 Subject: [PATCH 10/11] =?UTF-8?q?fix:=20=EB=82=A0=EC=A7=9C=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=ED=95=84=EB=93=9C=20=EB=86=92=EC=9D=B4=20=EB=B0=98?= =?UTF-8?q?=EC=9D=91=ED=98=95=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ๋“œ๋ž˜๊ทธ ๋“œ๋กญ์œผ๋กœ ๋ฐฐ์น˜๋˜๋Š” ์ธํ’‹ ํ•„๋“œ ๊ธฐ๋ณธ ๋†’์ด 30px๋กœ ๋ณ€๊ฒฝ - ๋‚ ์งœ ์ž…๋ ฅ ์ปดํฌ๋„ŒํŠธ์˜ ๋ชจ๋“  ์™ธ๋ถ€ div์— h-full ์ถ”๊ฐ€ - ๋ชจ๋“  input ์š”์†Œ์— min-h-full ์ถ”๊ฐ€ํ•˜์—ฌ ๋ถ€๋ชจ ๋†’์ด๋ฅผ ์ œ๋Œ€๋กœ ๋”ฐ๋ฅด๋„๋ก ์ˆ˜์ • - daterange, year ํƒ€์ž…๋„ ๋™์ผํ•˜๊ฒŒ ์ ์šฉ --- frontend/components/screen/ScreenDesigner.tsx | 16 ++++++++-------- .../components/date-input/DateInputComponent.tsx | 14 +++++++------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 7db03da6..4fa083ba 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -835,7 +835,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD const columns: ColumnInfo[] = (columnsResponse || []).map((col: any) => { const widgetType = col.widgetType || col.widget_type || col.webType || col.web_type; - + // ๐Ÿ” ์ด๋ฏธ์ง€ ํƒ€์ž… ๋””๋ฒ„๊น… // if (widgetType === "image" || col.webType === "image" || col.web_type === "image") { // console.log("๐Ÿ–ผ๏ธ ์ด๋ฏธ์ง€ ์ปฌ๋Ÿผ ๋ฐœ๊ฒฌ:", { @@ -845,7 +845,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD // rawData: col, // }); // } - + return { tableName: col.tableName || tableName, columnName: col.columnName || col.column_name, @@ -1965,16 +1965,16 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD // ๐Ÿ”ฅ ์ค‘์š”: ์คŒ ๋ ˆ๋ฒจ๊ณผ transform-origin์„ ๊ณ ๋ คํ•œ ๋งˆ์šฐ์Šค ์œ„์น˜ ๊ณ„์‚ฐ // 1. ์บ”๋ฒ„์Šค๊ฐ€ scale() ๋ณ€ํ™˜๋˜์–ด ์žˆ์Œ (transform-origin: top center) // 2. ์บ”๋ฒ„์Šค๊ฐ€ justify-center๋กœ ์ค‘์•™ ์ •๋ ฌ๋˜์–ด ์žˆ์Œ - + // ์‹ค์ œ ์บ”๋ฒ„์Šค ๋…ผ๋ฆฌ์  ํฌ๊ธฐ const canvasLogicalWidth = screenResolution.width; - + // ํ™”๋ฉด์ƒ ์บ”๋ฒ„์Šค ์‹ค์ œ ํฌ๊ธฐ (์Šค์ผ€์ผ ์ ์šฉ ํ›„) const canvasVisualWidth = canvasLogicalWidth * zoomLevel; - + // ์ค‘์•™ ์ •๋ ฌ๋กœ ์ธํ•œ ์™ผ์ชฝ ์˜คํ”„์…‹ ๊ณ„์‚ฐ // rect.left๋Š” ์ด๋ฏธ ์ค‘์•™ ์ •๋ ฌ๋œ ์œ„์น˜๋ฅผ ๋ฐ˜์˜ํ•˜๊ณ  ์žˆ์Œ - + // ๋งˆ์šฐ์Šค์˜ ์บ”๋ฒ„์Šค ๋‚ด ์ƒ๋Œ€ ์œ„์น˜ (์Šค์ผ€์ผ ๋ณด์ •) const mouseXInCanvas = (e.clientX - rect.left) / zoomLevel; const mouseYInCanvas = (e.clientY - rect.top) / zoomLevel; @@ -2388,7 +2388,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD file: 240, // ํŒŒ์ผ ์—…๋กœ๋“œ (40 * 6) }; - return heightMap[widgetType] || 40; // ๊ธฐ๋ณธ๊ฐ’ 40 + return heightMap[widgetType] || 30; // ๊ธฐ๋ณธ๊ฐ’ 30px๋กœ ๋ณ€๊ฒฝ }; // ์›นํƒ€์ž…๋ณ„ ๊ธฐ๋ณธ ์„ค์ • ์ƒ์„ฑ @@ -4364,7 +4364,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD return (
-
+
= ({ // daterange ํƒ€์ž… ์ „์šฉ UI if (webType === "daterange") { return ( -
+
{/* ๋ผ๋ฒจ ๋ Œ๋”๋ง */} {component.label && component.style?.labelDisplay !== false && (