From e27845a82f894a2f837020b9af0b551581c478a5 Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 7 Nov 2025 17:12:01 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=ED=83=AD?= =?UTF-8?q?=20=EB=93=9C=EB=9E=98=EA=B7=B8=EC=95=A4=EB=93=9C=EB=A1=AD=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20AI-=EA=B0=9C=EB=B0=9C=EC=9E=90?= =?UTF-8?q?=20=ED=98=91=EC=97=85=20=EA=B7=9C=EC=B9=99=20=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로 대체 } /**