From 1c2249ee425d8a4acb9f5d811c6ab3d4da5b64ad Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 4 Sep 2025 17:01:07 +0900 Subject: [PATCH] =?UTF-8?q?=ED=99=94=EB=A9=B4=20=ED=95=B4=EC=83=81?= =?UTF-8?q?=EB=8F=84=20=EC=84=A4=EC=A0=95=20=EA=B8=B0=EB=8A=A5=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 --- .../src/services/screenManagementService.ts | 83 ++++- backend-node/src/types/screen.ts | 13 + .../app/(main)/screens/[screenId]/page.tsx | 32 +- frontend/components/layout/AppLayout.tsx | 22 +- .../screen/InteractiveScreenViewer.tsx | 51 ++- frontend/components/screen/ScreenDesigner.tsx | 339 +++++++++++++----- .../components/screen/panels/GridPanel.tsx | 87 +++-- 7 files changed, 498 insertions(+), 129 deletions(-) diff --git a/backend-node/src/services/screenManagementService.ts b/backend-node/src/services/screenManagementService.ts index c9049da5..76b5da12 100644 --- a/backend-node/src/services/screenManagementService.ts +++ b/backend-node/src/services/screenManagementService.ts @@ -45,12 +45,22 @@ export class ScreenManagementService { screenData: CreateScreenRequest, userCompanyCode: string ): Promise { + console.log(`=== 화면 생성 요청 ===`); + console.log(`요청 데이터:`, screenData); + console.log(`사용자 회사 코드:`, userCompanyCode); + // 화면 코드 중복 확인 const existingScreen = await prisma.screen_definitions.findUnique({ where: { screen_code: screenData.screenCode }, }); + console.log( + `화면 코드 '${screenData.screenCode}' 중복 검사 결과:`, + existingScreen ? "중복됨" : "사용 가능" + ); + if (existingScreen) { + console.log(`기존 화면 정보:`, existingScreen); throw new Error("이미 존재하는 화면 코드입니다."); } @@ -437,6 +447,8 @@ export class ScreenManagementService { console.log(`=== 레이아웃 저장 시작 ===`); console.log(`화면 ID: ${screenId}`); console.log(`컴포넌트 수: ${layoutData.components.length}`); + console.log(`격자 설정:`, layoutData.gridSettings); + console.log(`해상도 설정:`, layoutData.screenResolution); // 권한 확인 const existingScreen = await prisma.screen_definitions.findUnique({ @@ -451,12 +463,37 @@ export class ScreenManagementService { throw new Error("이 화면의 레이아웃을 저장할 권한이 없습니다."); } - // 기존 레이아웃 삭제 + // 기존 레이아웃 삭제 (컴포넌트와 메타데이터 모두) await prisma.screen_layouts.deleteMany({ where: { screen_id: screenId }, }); - // 새 레이아웃 저장 + // 1. 메타데이터 저장 (격자 설정과 해상도 정보) + if (layoutData.gridSettings || layoutData.screenResolution) { + const metadata: any = { + gridSettings: layoutData.gridSettings, + screenResolution: layoutData.screenResolution, + }; + + await prisma.screen_layouts.create({ + data: { + screen_id: screenId, + component_type: "_metadata", // 특별한 타입으로 메타데이터 식별 + component_id: `_metadata_${screenId}`, + parent_id: null, + position_x: 0, + position_y: 0, + width: 0, + height: 0, + properties: metadata, + display_order: -1, // 메타데이터는 맨 앞에 배치 + }, + }); + + console.log(`메타데이터 저장 완료:`, metadata); + } + + // 2. 컴포넌트 저장 for (const component of layoutData.components) { const { id, ...componentData } = component; @@ -531,14 +568,45 @@ export class ScreenManagementService { console.log(`DB에서 조회된 레이아웃 수: ${layouts.length}`); - if (layouts.length === 0) { + // 메타데이터와 컴포넌트 분리 + const metadataLayout = layouts.find( + (layout) => layout.component_type === "_metadata" + ); + const componentLayouts = layouts.filter( + (layout) => layout.component_type !== "_metadata" + ); + + // 기본 메타데이터 설정 + let gridSettings = { + columns: 12, + gap: 16, + padding: 16, + snapToGrid: true, + showGrid: true, + }; + let screenResolution = null; + + // 저장된 메타데이터가 있으면 적용 + if (metadataLayout && metadataLayout.properties) { + const metadata = metadataLayout.properties as any; + if (metadata.gridSettings) { + gridSettings = { ...gridSettings, ...metadata.gridSettings }; + } + if (metadata.screenResolution) { + screenResolution = metadata.screenResolution; + } + console.log(`메타데이터 로드:`, { gridSettings, screenResolution }); + } + + if (componentLayouts.length === 0) { return { components: [], - gridSettings: { columns: 12, gap: 16, padding: 16 }, + gridSettings, + screenResolution, }; } - const components: ComponentData[] = layouts.map((layout) => { + const components: ComponentData[] = componentLayouts.map((layout) => { const properties = layout.properties as any; const component = { id: layout.component_id, @@ -567,10 +635,13 @@ export class ScreenManagementService { console.log(`=== 레이아웃 로드 완료 ===`); console.log(`반환할 컴포넌트 수: ${components.length}`); + console.log(`최종 격자 설정:`, gridSettings); + console.log(`최종 해상도 설정:`, screenResolution); return { components, - gridSettings: { columns: 12, gap: 16, padding: 16 }, + gridSettings, + screenResolution, }; } diff --git a/backend-node/src/types/screen.ts b/backend-node/src/types/screen.ts index 9cb63cea..9b9a55bf 100644 --- a/backend-node/src/types/screen.ts +++ b/backend-node/src/types/screen.ts @@ -108,6 +108,7 @@ export type ComponentData = export interface LayoutData { components: ComponentData[]; gridSettings?: GridSettings; + screenResolution?: ScreenResolution; } // 그리드 설정 @@ -115,6 +116,18 @@ export interface GridSettings { columns: number; // 기본값: 12 gap: number; // 기본값: 16px padding: number; // 기본값: 16px + snapToGrid?: boolean; // 격자에 맞춤 여부 (기본값: true) + showGrid?: boolean; // 격자 표시 여부 (기본값: true) + gridColor?: string; // 격자 색상 (기본값: #d1d5db) + gridOpacity?: number; // 격자 투명도 (기본값: 0.5) +} + +// 화면 해상도 설정 +export interface ScreenResolution { + width: number; + height: number; + name: string; + category: "desktop" | "tablet" | "mobile" | "custom"; } // 유효성 검증 규칙 diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index d5d2f8ba..f391b63b 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -58,7 +58,7 @@ export default function ScreenViewPage() { if (loading) { return ( -
+

화면을 불러오는 중...

@@ -69,7 +69,7 @@ export default function ScreenViewPage() { if (error || !screen) { return ( -
+
⚠️ @@ -84,11 +84,23 @@ export default function ScreenViewPage() { ); } + // 화면 해상도 정보가 있으면 해당 크기로, 없으면 기본 크기 사용 + const screenWidth = layout?.screenResolution?.width || 1200; + const screenHeight = layout?.screenResolution?.height || 800; + return ( -
+
{layout && layout.components.length > 0 ? ( - // 캔버스 컴포넌트들만 표시 - 전체 화면 사용 -
+ // 캔버스 컴포넌트들을 정확한 해상도로 표시 +
{layout.components .filter((comp) => !comp.parentId) // 최상위 컴포넌트만 렌더링 (그룹 포함) .map((component) => { @@ -218,7 +230,15 @@ export default function ScreenViewPage() {
) : ( // 빈 화면일 때도 깔끔하게 표시 -
+
📄 diff --git a/frontend/components/layout/AppLayout.tsx b/frontend/components/layout/AppLayout.tsx index 2d65082f..fdb56ed5 100644 --- a/frontend/components/layout/AppLayout.tsx +++ b/frontend/components/layout/AppLayout.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import { useRouter, usePathname } from "next/navigation"; +import { useRouter, usePathname, useSearchParams } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Shield, @@ -194,6 +194,7 @@ const convertSingleMenu = (menu: MenuItem, allMenus: MenuItem[], userInfo: Exten export function AppLayout({ children }: AppLayoutProps) { const router = useRouter(); const pathname = usePathname(); + const searchParams = useSearchParams(); const { user, logout, refreshUserData } = useAuth(); const { userMenus, adminMenus, loading, refreshMenus } = useMenu(); const [sidebarOpen, setSidebarOpen] = useState(false); @@ -216,8 +217,8 @@ export function AppLayout({ children }: AppLayoutProps) { saveProfile, } = useProfile(user, refreshUserData, refreshMenus); - // 현재 경로에 따라 어드민 모드인지 판단 - const isAdminMode = pathname.startsWith("/admin"); + // 현재 경로에 따라 어드민 모드인지 판단 (쿼리 파라미터도 고려) + const isAdminMode = pathname.startsWith("/admin") || searchParams.get("mode") === "admin"; // 현재 모드에 따라 표시할 메뉴 결정 const currentMenus = isAdminMode ? adminMenus : userMenus; @@ -246,7 +247,20 @@ export function AppLayout({ children }: AppLayoutProps) { if (assignedScreens.length > 0) { // 할당된 화면이 있으면 첫 번째 화면으로 이동 const firstScreen = assignedScreens[0]; - router.push(`/screens/${firstScreen.screenId}`); + + // 관리자 모드 상태를 쿼리 파라미터로 전달 + const screenPath = isAdminMode + ? `/screens/${firstScreen.screenId}?mode=admin` + : `/screens/${firstScreen.screenId}`; + + console.log("🎯 메뉴에서 화면으로 이동:", { + menuName: menu.name, + screenId: firstScreen.screenId, + isAdminMode, + targetPath: screenPath, + }); + + router.push(screenPath); setSidebarOpen(false); return; } diff --git a/frontend/components/screen/InteractiveScreenViewer.tsx b/frontend/components/screen/InteractiveScreenViewer.tsx index 9c955385..cd5d58cb 100644 --- a/frontend/components/screen/InteractiveScreenViewer.tsx +++ b/frontend/components/screen/InteractiveScreenViewer.tsx @@ -66,6 +66,7 @@ export const InteractiveScreenViewer: React.FC = ( // 팝업 화면 레이아웃 상태 const [popupLayout, setPopupLayout] = useState([]); const [popupLoading, setPopupLoading] = useState(false); + const [popupScreenResolution, setPopupScreenResolution] = useState<{ width: number; height: number } | null>(null); // 팝업 화면 레이아웃 로드 React.useEffect(() => { @@ -73,10 +74,28 @@ export const InteractiveScreenViewer: React.FC = ( const loadPopupLayout = async () => { try { setPopupLoading(true); + console.log("🔍 팝업 화면 로드 시작:", { + screenId: popupScreen.screenId, + title: popupScreen.title, + size: popupScreen.size + }); + const layout = await screenApi.getLayout(popupScreen.screenId); + console.log("📊 팝업 화면 레이아웃 로드 완료:", { + componentsCount: layout.components?.length || 0, + gridSettings: layout.gridSettings, + screenResolution: layout.screenResolution, + components: layout.components?.map(c => ({ + id: c.id, + type: c.type, + title: (c as any).title + })) + }); + setPopupLayout(layout.components || []); + setPopupScreenResolution(layout.screenResolution || null); } catch (error) { - console.error("팝업 화면 레이아웃 로드 실패:", error); + console.error("❌ 팝업 화면 레이아웃 로드 실패:", error); setPopupLayout([]); } finally { setPopupLoading(false); @@ -1162,14 +1181,32 @@ export const InteractiveScreenViewer: React.FC = (
화면을 불러오는 중...
) : popupLayout.length > 0 ? ( -
+
+ {/* 팝업에서도 실제 위치와 크기로 렌더링 */} {popupLayout.map((popupComponent) => ( - + className="absolute" + style={{ + left: `${popupComponent.position.x}px`, + top: `${popupComponent.position.y}px`, + width: `${popupComponent.size.width}px`, + height: `${popupComponent.size.height}px`, + zIndex: popupComponent.position.z || 1, + }} + > + +
))}
) : ( diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index a99d94ae..b4ed5c44 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -197,19 +197,24 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD height = canvasSize.height || window.innerHeight - 200; } - if (canvasRef.current) { - const rect = canvasRef.current.getBoundingClientRect(); - width = rect.width || width; - height = rect.height || height; - } - - return calculateGridInfo(width, height, { + const newGridInfo = calculateGridInfo(width, height, { columns: layout.gridSettings.columns, gap: layout.gridSettings.gap, padding: layout.gridSettings.padding, snapToGrid: layout.gridSettings.snapToGrid || false, }); - }, [layout.gridSettings, canvasSize, screenResolution]); + + console.log("🧮 격자 정보 재계산:", { + resolution: `${width}x${height}`, + columns: layout.gridSettings.columns, + gap: layout.gridSettings.gap, + padding: layout.gridSettings.padding, + columnWidth: newGridInfo.columnWidth.toFixed(2), + snapToGrid: layout.gridSettings.snapToGrid, + }); + + return newGridInfo; + }, [layout.gridSettings, screenResolution]); // 격자 라인 생성 const gridLines = useMemo(() => { @@ -357,11 +362,22 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD gridInfo && newComp.type !== "group" ) { - const snappedSize = snapSizeToGrid(newComp.size, gridInfo, layout.gridSettings as GridUtilSettings); + // 현재 해상도에 맞는 격자 정보로 스냅 적용 + const currentGridInfo = calculateGridInfo(screenResolution.width, screenResolution.height, { + columns: layout.gridSettings.columns, + gap: layout.gridSettings.gap, + padding: layout.gridSettings.padding, + snapToGrid: layout.gridSettings.snapToGrid || false, + }); + const snappedSize = snapSizeToGrid(newComp.size, currentGridInfo, layout.gridSettings as GridUtilSettings); newComp.size = snappedSize; // 크기 변경 시 gridColumns도 자동 조정 - const adjustedColumns = adjustGridColumnsFromSize(newComp, gridInfo, layout.gridSettings as GridUtilSettings); + const adjustedColumns = adjustGridColumnsFromSize( + newComp, + currentGridInfo, + layout.gridSettings as GridUtilSettings, + ); if (newComp.gridColumns !== adjustedColumns) { newComp.gridColumns = adjustedColumns; console.log("📏 크기 변경으로 gridColumns 자동 조정:", { @@ -372,15 +388,52 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD } } + // gridColumns 변경 시 크기를 격자에 맞게 자동 조정 + if (path === "gridColumns" && layout.gridSettings?.snapToGrid && newComp.type !== "group") { + const currentGridInfo = calculateGridInfo(screenResolution.width, screenResolution.height, { + columns: layout.gridSettings.columns, + gap: layout.gridSettings.gap, + padding: layout.gridSettings.padding, + snapToGrid: layout.gridSettings.snapToGrid || false, + }); + + // gridColumns에 맞는 정확한 너비 계산 + const newWidth = calculateWidthFromColumns( + newComp.gridColumns, + currentGridInfo, + layout.gridSettings as GridUtilSettings, + ); + newComp.size = { + ...newComp.size, + width: newWidth, + }; + + console.log("📐 gridColumns 변경으로 크기 자동 조정:", { + componentId, + gridColumns: newComp.gridColumns, + oldWidth: comp.size.width, + newWidth: newWidth, + columnWidth: currentGridInfo.columnWidth, + gap: layout.gridSettings.gap, + }); + } + // 위치 변경 시 격자 스냅 적용 (그룹 내부 컴포넌트 포함) if ( (path === "position.x" || path === "position.y" || path === "position") && - layout.gridSettings?.snapToGrid && - gridInfo + 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 && gridInfo) { - const { columnWidth } = gridInfo; + if (newComp.parentId && currentGridInfo) { + const { columnWidth } = currentGridInfo; const { gap } = layout.gridSettings; // 그룹 내부 패딩 고려한 격자 정렬 @@ -432,7 +485,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD }); } else if (newComp.type !== "group") { // 그룹이 아닌 일반 컴포넌트만 격자 스냅 적용 - const snappedPosition = snapToGrid(newComp.position, gridInfo, layout.gridSettings as GridUtilSettings); + const snappedPosition = snapToGrid( + newComp.position, + currentGridInfo, + layout.gridSettings as GridUtilSettings, + ); newComp.position = snappedPosition; console.log("🧲 일반 컴포넌트 격자 스냅:", { @@ -637,61 +694,96 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD // 해상도 변경 핸들러 const handleResolutionChange = useCallback( (newResolution: ScreenResolution) => { + console.log("📱 해상도 변경 시작:", { + from: `${screenResolution.width}x${screenResolution.height}`, + to: `${newResolution.width}x${newResolution.height}`, + hasComponents: layout.components.length > 0, + snapToGrid: layout.gridSettings?.snapToGrid || false, + }); + setScreenResolution(newResolution); - console.log("📱 해상도 변경:", newResolution); - // 레이아웃에 해상도 정보 즉시 반영 - const updatedLayout = { ...layout, screenResolution: newResolution }; + // 해상도 변경 시에는 격자 스냅을 적용하지 않고 해상도 정보만 업데이트 + // 이는 기존 컴포넌트들의 위치를 보존하기 위함 + const updatedLayout = { + ...layout, + screenResolution: newResolution, + }; - // 격자 스냅이 활성화된 경우, 기존 컴포넌트들을 새로운 해상도의 격자에 맞게 조정 - if (layout.gridSettings?.snapToGrid && layout.components.length > 0) { - // 새로운 해상도로 격자 정보 재계산 - const newGridInfo = calculateGridInfo(newResolution.width, newResolution.height, { - columns: layout.gridSettings.columns, - gap: layout.gridSettings.gap, - padding: layout.gridSettings.padding, - snapToGrid: layout.gridSettings.snapToGrid || false, - }); + setLayout(updatedLayout); + saveToHistory(updatedLayout); - const gridUtilSettings = { - columns: layout.gridSettings.columns, - gap: layout.gridSettings.gap, - padding: layout.gridSettings.padding, - snapToGrid: layout.gridSettings.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 > layout.gridSettings!.columns) { - adjustedGridColumns = adjustGridColumnsFromSize({ size: snappedSize }, newGridInfo, gridUtilSettings); - } - - return { - ...comp, - position: snappedPosition, - size: snappedSize, - gridColumns: adjustedGridColumns, - }; - }); - - const newLayout = { ...updatedLayout, components: adjustedComponents }; - setLayout(newLayout); - saveToHistory(newLayout); - console.log("해상도 변경으로 컴포넌트 위치 및 크기 자동 조정:", adjustedComponents.length, "개"); - console.log("새로운 격자 정보:", newGridInfo); - } else { - // 격자 조정이 없는 경우에도 해상도 정보가 포함된 레이아웃 저장 - setLayout(updatedLayout); - saveToHistory(updatedLayout); - } + console.log("✅ 해상도 변경 완료:", { + newResolution: `${newResolution.width}x${newResolution.height}`, + preservedComponents: layout.components.length, + note: "컴포넌트 위치는 보존됨 (격자 스냅 생략)", + }); }, - [layout, saveToHistory], + [layout, saveToHistory, screenResolution], ); + // 강제 격자 재조정 핸들러 (해상도 변경 후 수동 격자 맞춤용) + const handleForceGridUpdate = useCallback(() => { + if (!layout.gridSettings?.snapToGrid || layout.components.length === 0) { + console.log("격자 재조정 생략: 스냅 비활성화 또는 컴포넌트 없음"); + return; + } + + console.log("🔄 격자 강제 재조정 시작:", { + componentsCount: layout.components.length, + resolution: `${screenResolution.width}x${screenResolution.height}`, + gridSettings: layout.gridSettings, + }); + + // 현재 해상도로 격자 정보 계산 + const currentGridInfo = calculateGridInfo(screenResolution.width, screenResolution.height, { + columns: layout.gridSettings.columns, + gap: layout.gridSettings.gap, + padding: layout.gridSettings.padding, + snapToGrid: layout.gridSettings.snapToGrid || false, + }); + + const gridUtilSettings = { + columns: layout.gridSettings.columns, + gap: layout.gridSettings.gap, + padding: layout.gridSettings.padding, + snapToGrid: layout.gridSettings.snapToGrid, + }; + + const adjustedComponents = layout.components.map((comp) => { + 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, + }; + }); + + const newLayout = { ...layout, components: adjustedComponents }; + setLayout(newLayout); + saveToHistory(newLayout); + + console.log("✅ 격자 강제 재조정 완료:", { + adjustedComponents: adjustedComponents.length, + gridInfo: { + columnWidth: currentGridInfo.columnWidth.toFixed(2), + totalWidth: currentGridInfo.totalWidth, + columns: layout.gridSettings.columns, + }, + }); + + toast.success(`${adjustedComponents.length}개 컴포넌트가 격자에 맞게 재정렬되었습니다.`); + }, [layout, screenResolution, saveToHistory]); + // 저장 const handleSave = useCallback(async () => { if (!selectedScreen?.screenId) return; @@ -703,6 +795,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD ...layout, screenResolution: screenResolution, }; + console.log("💾 저장할 레이아웃 데이터:", { + componentsCount: layoutWithResolution.components.length, + gridSettings: layoutWithResolution.gridSettings, + screenResolution: layoutWithResolution.screenResolution, + }); await screenApi.saveLayout(selectedScreen.screenId, layoutWithResolution); toast.success("화면이 저장되었습니다."); } catch (error) { @@ -722,10 +819,20 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD const dropX = e.clientX - rect.left; const dropY = e.clientY - rect.top; + // 현재 해상도에 맞는 격자 정보 계산 + 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 snappedPosition = - layout.gridSettings?.snapToGrid && gridInfo - ? snapToGrid({ x: dropX, y: dropY, z: 1 }, gridInfo, layout.gridSettings as GridUtilSettings) + layout.gridSettings?.snapToGrid && currentGridInfo + ? snapToGrid({ x: dropX, y: dropY, z: 1 }, currentGridInfo, layout.gridSettings as GridUtilSettings) : { x: dropX, y: dropY, z: 1 }; console.log("🎨 템플릿 드롭:", { @@ -756,8 +863,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD // 격자 스냅 적용 const finalPosition = - layout.gridSettings?.snapToGrid && gridInfo - ? snapToGrid({ x: absoluteX, y: absoluteY, z: 1 }, gridInfo, layout.gridSettings as GridUtilSettings) + layout.gridSettings?.snapToGrid && currentGridInfo + ? snapToGrid({ x: absoluteX, y: absoluteY, z: 1 }, currentGridInfo, layout.gridSettings as GridUtilSettings) : { x: absoluteX, y: absoluteY, z: 1 }; if (templateComp.type === "container") { @@ -766,11 +873,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD typeof templateComp.size.width === "number" && templateComp.size.width <= 12 ? templateComp.size.width : 4; // 기본 4컬럼 const calculatedSize = - gridInfo && layout.gridSettings?.snapToGrid + currentGridInfo && layout.gridSettings?.snapToGrid ? (() => { const newWidth = calculateWidthFromColumns( gridColumns, - gridInfo, + currentGridInfo, layout.gridSettings as GridUtilSettings, ); return { @@ -804,11 +911,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD // gridColumns에 맞는 크기 계산 const calculatedSize = - gridInfo && layout.gridSettings?.snapToGrid + currentGridInfo && layout.gridSettings?.snapToGrid ? (() => { const newWidth = calculateWidthFromColumns( gridColumns, - gridInfo, + currentGridInfo, layout.gridSettings as GridUtilSettings, ); return { @@ -822,7 +929,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD gridColumns, templateSize: templateComp.size, calculatedSize, - hasGridInfo: !!gridInfo, + hasGridInfo: !!currentGridInfo, hasGridSettings: !!layout.gridSettings?.snapToGrid, }); @@ -934,6 +1041,15 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD } }; + // 위젯 크기도 격자에 맞게 조정 + const widgetSize = + currentGridInfo && layout.gridSettings?.snapToGrid + ? { + width: calculateWidthFromColumns(1, currentGridInfo, layout.gridSettings as GridUtilSettings), + height: templateComp.size.height, + } + : templateComp.size; + return { id: componentId, type: "widget", @@ -943,7 +1059,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD columnName: `field_${index + 1}`, parentId: templateComp.parentId ? idMapping[templateComp.parentId] : undefined, position: finalPosition, - size: templateComp.size, + size: widgetSize, required: templateComp.required || false, readonly: templateComp.readonly || false, gridColumns: 1, @@ -1034,8 +1150,34 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD }, }; } else if (type === "column") { - // 격자 기반 컬럼 너비 계산 - const columnWidth = gridInfo ? gridInfo.columnWidth : 200; + // 현재 해상도에 맞는 격자 정보로 기본 크기 계산 + const currentGridInfo = layout.gridSettings + ? calculateGridInfo(screenResolution.width, screenResolution.height, { + columns: layout.gridSettings.columns, + gap: layout.gridSettings.gap, + padding: layout.gridSettings.padding, + snapToGrid: layout.gridSettings.snapToGrid || false, + }) + : null; + + // 격자 스냅이 활성화된 경우 정확한 격자 크기로 생성, 아니면 기본값 + const defaultWidth = + currentGridInfo && layout.gridSettings?.snapToGrid + ? calculateWidthFromColumns(1, currentGridInfo, layout.gridSettings as GridUtilSettings) + : 200; + + console.log("🎯 컴포넌트 생성 시 크기 계산:", { + screenResolution: `${screenResolution.width}x${screenResolution.height}`, + gridSettings: layout.gridSettings, + currentGridInfo: currentGridInfo + ? { + columnWidth: currentGridInfo.columnWidth.toFixed(2), + totalWidth: currentGridInfo.totalWidth, + } + : null, + defaultWidth: defaultWidth.toFixed(2), + snapToGrid: layout.gridSettings?.snapToGrid, + }); // 웹타입별 기본 설정 생성 const getDefaultWebTypeConfig = (widgetType: string) => { @@ -1183,7 +1325,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD readonly: false, parentId: formContainerId, // 폼 컨테이너의 자식으로 설정 position: { x: relativeX, y: relativeY, z: 1 } as Position, - size: { width: 200, height: 40 }, + size: { width: defaultWidth, height: 40 }, style: { labelDisplay: true, labelFontSize: "12px", @@ -1208,7 +1350,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD required: column.required, readonly: false, position: { x, y, z: 1 } as Position, - size: { width: columnWidth, height: 40 }, + size: { width: defaultWidth, height: 40 }, gridColumns: 1, style: { labelDisplay: true, @@ -1225,20 +1367,30 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD } // 격자 스냅 적용 (그룹 컴포넌트 제외) - if (layout.gridSettings?.snapToGrid && gridInfo && newComponent.type !== "group") { + if (layout.gridSettings?.snapToGrid && newComponent.type !== "group") { + // 현재 해상도에 맞는 격자 정보 계산 + const currentGridInfo = calculateGridInfo(screenResolution.width, screenResolution.height, { + columns: layout.gridSettings.columns, + gap: layout.gridSettings.gap, + padding: layout.gridSettings.padding, + snapToGrid: layout.gridSettings.snapToGrid || false, + }); + const gridUtilSettings = { columns: layout.gridSettings.columns, gap: layout.gridSettings.gap, padding: layout.gridSettings.padding, snapToGrid: layout.gridSettings.snapToGrid || false, }; - newComponent.position = snapToGrid(newComponent.position, gridInfo, gridUtilSettings); - newComponent.size = snapSizeToGrid(newComponent.size, gridInfo, gridUtilSettings); + 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, }); } @@ -1412,15 +1564,25 @@ 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; + // 일반 컴포넌트만 격자 스냅 적용 (그룹 제외) - if (draggedComponent?.type !== "group" && layout.gridSettings?.snapToGrid && gridInfo) { + if (draggedComponent?.type !== "group" && layout.gridSettings?.snapToGrid && currentGridInfo) { finalPosition = snapToGrid( { x: dragState.currentPosition.x, y: dragState.currentPosition.y, z: dragState.currentPosition.z ?? 1, }, - gridInfo, + currentGridInfo, { columns: layout.gridSettings.columns, gap: layout.gridSettings.gap, @@ -1428,6 +1590,13 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD snapToGrid: layout.gridSettings.snapToGrid || false, }, ); + + console.log("🎯 격자 스냅 적용됨:", { + resolution: `${screenResolution.width}x${screenResolution.height}`, + originalPosition: dragState.currentPosition, + snappedPosition: finalPosition, + columnWidth: currentGridInfo.columnWidth, + }); } // 스냅으로 인한 추가 이동 거리 계산 @@ -2291,6 +2460,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD ...layout, screenResolution: screenResolution, }; + console.log("⚡ 자동 저장할 레이아웃 데이터:", { + componentsCount: layoutWithResolution.components.length, + gridSettings: layoutWithResolution.gridSettings, + screenResolution: layoutWithResolution.screenResolution, + }); await screenApi.saveLayout(selectedScreen.screenId, layoutWithResolution); toast.success("레이아웃이 저장되었습니다."); } catch (error) { @@ -2758,6 +2932,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD const defaultSettings = { columns: 12, gap: 16, padding: 16, snapToGrid: true, showGrid: true }; updateGridSettings(defaultSettings); }} + onForceGridUpdate={handleForceGridUpdate} screenResolution={screenResolution} /> diff --git a/frontend/components/screen/panels/GridPanel.tsx b/frontend/components/screen/panels/GridPanel.tsx index 0cf121f5..f3073918 100644 --- a/frontend/components/screen/panels/GridPanel.tsx +++ b/frontend/components/screen/panels/GridPanel.tsx @@ -7,7 +7,7 @@ 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 } from "lucide-react"; +import { Grid3X3, RotateCcw, Eye, EyeOff, Zap, RefreshCw } from "lucide-react"; import { GridSettings, ScreenResolution } from "@/types/screen"; import { calculateGridInfo } from "@/lib/utils/gridUtils"; @@ -15,6 +15,7 @@ interface GridPanelProps { gridSettings: GridSettings; onGridSettingsChange: (settings: GridSettings) => void; onResetGrid: () => void; + onForceGridUpdate?: () => void; // 강제 격자 재조정 추가 screenResolution?: ScreenResolution; // 해상도 정보 추가 } @@ -22,6 +23,7 @@ export const GridPanel: React.FC = ({ gridSettings, onGridSettingsChange, onResetGrid, + onForceGridUpdate, screenResolution, }) => { const updateSetting = (key: keyof GridSettings, value: any) => { @@ -60,10 +62,25 @@ export const GridPanel: React.FC = ({

격자 설정

- +
+ {onForceGridUpdate && ( + + )} + + +
{/* 주요 토글들 */} @@ -100,19 +117,6 @@ export const GridPanel: React.FC = ({ />
- - {/* 해상도 정보 */} - {screenResolution && ( -
-

현재 해상도

-

- {screenResolution.width} × {screenResolution.height} -

- {actualGridInfo && ( -

컬럼 너비: {Math.round(actualGridInfo.columnWidth)}px

- )} -
- )}
{/* 설정 영역 */} @@ -124,11 +128,6 @@ export const GridPanel: React.FC = ({
= ({ {/* 푸터 */}
-
💡 격자 설정은 실시간으로 캔버스에 반영됩니다
+
💡 격자 설정은 실시간으로 캔버스에 반영됩니다
+ + {/* 해상도 및 격자 정보 */} + {screenResolution && actualGridInfo && ( + <> + +
+

격자 정보

+ +
+
+ 해상도: + + {screenResolution.width} × {screenResolution.height} + +
+ +
+ 컬럼 너비: + + {actualGridInfo.columnWidth.toFixed(1)}px + {isColumnsTooSmall && " (너무 작음)"} + +
+ +
+ 사용 가능 너비: + + {(screenResolution.width - gridSettings.padding * 2).toLocaleString()}px + +
+ + {isColumnsTooSmall && ( +
+ 💡 컬럼이 너무 작습니다. 컬럼 수를 줄이거나 간격을 줄여보세요. +
+ )} +
+
+ + )}
);