From 5b79bfb19d9811e3d038b848a9da684833a932ae Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 7 Nov 2025 11:36:58 +0900 Subject: [PATCH 01/44] =?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, }; From 4294fbf60813e4beb3096ec87b2efab3088970e0 Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 7 Nov 2025 14:27:07 +0900 Subject: [PATCH 02/44] =?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` + +**κ΅¬ν˜„ μ™„λ£Œ!** 🎊 + +λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ μ‹€ν–‰ ν›„ λ°”λ‘œ μ‚¬μš© κ°€λŠ₯ν•©λ‹ˆλ‹€. + From e27845a82f894a2f837020b9af0b551581c478a5 Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 7 Nov 2025 17:12:01 +0900 Subject: [PATCH 03/44] =?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둜 λŒ€μ²΄ } /** From 1d6418ca63367a2baa2f3be49a3c8f3a9024982f Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 7 Nov 2025 17:39:51 +0900 Subject: [PATCH 04/44] =?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) => (
= ({
화면에 μ»΄ν¬λ„ŒνŠΈκ°€ μ—†μŠ΅λ‹ˆλ‹€.
)}
- -
+ + ); }; From e2f4b475888daa06111882e65adc42d5d67839a1 Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 10 Nov 2025 09:33:29 +0900 Subject: [PATCH 05/44] =?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}
From 68c3db5213e15e6ed67cb0206c72d38fff129b74 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Mon, 10 Nov 2025 11:56:39 +0900 Subject: [PATCH 06/44] =?UTF-8?q?feat:=20=EB=B6=84=ED=95=A0=20=ED=8C=A8?= =?UTF-8?q?=EB=84=90=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EB=A9=80?= =?UTF-8?q?=ED=8B=B0=ED=85=8C=EB=84=8C=EC=8B=9C=20=EB=B0=8F=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=20=ED=95=84=ED=84=B0=EB=A7=81=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 데이터 쑰회 API에 νšŒμ‚¬λ³„ μžλ™ 필터링 κΈ°λŠ₯ μΆ”κ°€ - GET /api/data/:tableName에 company_code ν•„ν„° μžλ™ 적용 - GET /api/data/join에 우츑 ν…Œμ΄λΈ” νšŒμ‚¬λ³„ 필터링 μΆ”κ°€ - 졜고 κ΄€λ¦¬μž(company_code = '*')λŠ” 전체 데이터 쑰회 κ°€λŠ₯ - λΆ„ν•  νŒ¨λ„ λ ˆμ΄μ•„μ›ƒ 우츑 μΆ”κ°€ μ‹œ 쑰인 컬럼 μžλ™ μž…λ ₯ - μ’ŒμΈ‘μ—μ„œ μ„ νƒν•œ ν•­λͺ©μ˜ 쑰인 ν‚€ 값을 우츑 μΆ”κ°€ λͺ¨λ‹¬μ— μžλ™ μ„€μ • - μžλ™ μ„€μ •λœ ν•„λ“œλŠ” 읽기 μ „μš©μœΌλ‘œ ν‘œμ‹œ (disabled + μ•ˆλ‚΄ 문ꡬ) - μ‚¬μš©μžλŠ” λ‚˜λ¨Έμ§€ ν•„λ“œλ§Œ μž…λ ₯ν•˜λ©΄ 됨 - 데이터 μ„œλΉ„μŠ€ κ°œμ„  - getJoinedData ν•¨μˆ˜μ— companyCode νŒŒλΌλ―Έν„° μΆ”κ°€ - checkColumnExists ν•¨μˆ˜λ₯Ό public으둜 λ³€κ²½ν•˜μ—¬ μž¬μ‚¬μš©μ„± ν–₯상 - 쑰인 쿼리에 DISTINCT μΆ”κ°€λ‘œ 쀑볡 데이터 λ°©μ§€ - 볡합킀 ν…Œμ΄λΈ”μ˜ λ ˆμ½”λ“œ μ‚­μ œ 지원 - λ ˆμ½”λ“œ 생성 μ‹œ λ©€ν‹°ν…Œλ„Œμ‹œ μžλ™ 처리 - company_code와 company_name μžλ™ μΆ”κ°€ - ν…Œμ΄λΈ” 컬럼 쑴재 μ—¬λΆ€ 체크 ν›„ μžλ™ μ„€μ • - λΆ„ν•  νŒ¨λ„ μ„€μ • UI κ°œμ„  - 쒌츑 νŒ¨λ„ ν‘œμ‹œ 컬럼 선택 UI μΆ”κ°€ - μΆ”κ°€ 폼에 ν‘œμ‹œν•  컬럼 선택 κΈ°λŠ₯ μΆ”κ°€ - Primary Key 정보 μžλ™ 쑰회 및 ν‘œμ‹œ --- .../SplitPanelLayoutComponent.tsx | 35 +++++++++++-- .../SplitPanelLayoutConfigPanel.tsx | 51 ++++++++++++++----- 2 files changed, 69 insertions(+), 17 deletions(-) diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index dff4ee3a..dbb99963 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -160,6 +160,16 @@ export const SplitPanelLayoutComponent: React.FC // searchTerm 제거 - ν΄λΌμ΄μ–ΈνŠΈ μ‚¬μ΄λ“œμ—μ„œ 필터링 }); + // κ°€λ‚˜λ‹€μˆœ μ •λ ¬ (쒌츑 νŒ¨λ„μ˜ ν‘œμ‹œ 컬럼 κΈ°μ€€) + const leftColumn = componentConfig.rightPanel?.relation?.leftColumn; + if (leftColumn && result.data.length > 0) { + result.data.sort((a, b) => { + const aValue = String(a[leftColumn] || ''); + const bValue = String(b[leftColumn] || ''); + return aValue.localeCompare(bValue, 'ko-KR'); + }); + } + // 계측 ꡬ쑰 λΉŒλ“œ const hierarchicalData = buildHierarchy(result.data); setLeftData(hierarchicalData); @@ -173,7 +183,7 @@ export const SplitPanelLayoutComponent: React.FC } finally { setIsLoadingLeft(false); } - }, [componentConfig.leftPanel?.tableName, isDesignMode, toast, buildHierarchy]); + }, [componentConfig.leftPanel?.tableName, componentConfig.rightPanel?.relation?.leftColumn, isDesignMode, toast, buildHierarchy]); // 우츑 데이터 λ‘œλ“œ const loadRightData = useCallback( @@ -293,9 +303,19 @@ export const SplitPanelLayoutComponent: React.FC // μΆ”κ°€ λ²„νŠΌ ν•Έλ“€λŸ¬ const handleAddClick = useCallback((panel: "left" | "right") => { setAddModalPanel(panel); - setAddModalFormData({}); + + // 우츑 νŒ¨λ„ μΆ”κ°€ μ‹œ, μ’ŒμΈ‘μ—μ„œ μ„ νƒλœ ν•­λͺ©μ˜ 쑰인 컬럼 값을 μžλ™μœΌλ‘œ 채움 + if (panel === "right" && selectedLeftItem && componentConfig.leftPanel?.leftColumn && componentConfig.rightPanel?.rightColumn) { + const leftColumnValue = selectedLeftItem[componentConfig.leftPanel.leftColumn]; + setAddModalFormData({ + [componentConfig.rightPanel.rightColumn]: leftColumnValue + }); + } else { + setAddModalFormData({}); + } + setShowAddModal(true); - }, []); + }, [selectedLeftItem, componentConfig]); // μˆ˜μ • λ²„νŠΌ ν•Έλ“€λŸ¬ const handleEditClick = useCallback((panel: "left" | "right", item: any) => { @@ -1316,10 +1336,17 @@ export const SplitPanelLayoutComponent: React.FC return modalColumns?.map((col, index) => { // ν•­λͺ©λ³„ μΆ”κ°€ λ²„νŠΌμœΌλ‘œ 열렸을 λ•Œ, parentColumn은 미리 μ±„μ›Œμ Έ 있고 μˆ˜μ • λΆˆκ°€ - const isPreFilled = addModalPanel === "left-item" + const isItemAddPreFilled = addModalPanel === "left-item" && componentConfig.leftPanel?.itemAddConfig?.parentColumn === col.name && addModalFormData[col.name]; + // 우츑 νŒ¨λ„ μΆ”κ°€ μ‹œ, 쑰인 컬럼(rightColumn)은 미리 μ±„μ›Œμ Έ 있고 μˆ˜μ • λΆˆκ°€ + const isRightJoinPreFilled = addModalPanel === "right" + && componentConfig.rightPanel?.rightColumn === col.name + && addModalFormData[col.name]; + + const isPreFilled = isItemAddPreFilled || isRightJoinPreFilled; + return (