화면 해상도 설정 기능 구현

This commit is contained in:
kjs 2025-09-04 17:01:07 +09:00
parent 9bf879e29d
commit 1c2249ee42
7 changed files with 498 additions and 129 deletions

View File

@ -45,12 +45,22 @@ export class ScreenManagementService {
screenData: CreateScreenRequest,
userCompanyCode: string
): Promise<ScreenDefinition> {
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,
};
}

View File

@ -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";
}
// 유효성 검증 규칙

View File

@ -58,7 +58,7 @@ export default function ScreenViewPage() {
if (loading) {
return (
<div className="flex h-screen w-screen items-center justify-center bg-white">
<div className="flex h-full min-h-[400px] w-full items-center justify-center bg-white">
<div className="text-center">
<Loader2 className="mx-auto h-8 w-8 animate-spin text-blue-600" />
<p className="mt-2 text-gray-600"> ...</p>
@ -69,7 +69,7 @@ export default function ScreenViewPage() {
if (error || !screen) {
return (
<div className="flex h-screen w-screen items-center justify-center bg-white">
<div className="flex h-full min-h-[400px] w-full items-center justify-center bg-white">
<div className="text-center">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-red-100">
<span className="text-2xl"></span>
@ -84,11 +84,23 @@ export default function ScreenViewPage() {
);
}
// 화면 해상도 정보가 있으면 해당 크기로, 없으면 기본 크기 사용
const screenWidth = layout?.screenResolution?.width || 1200;
const screenHeight = layout?.screenResolution?.height || 800;
return (
<div className="h-screen w-screen bg-white">
<div className="h-full w-full overflow-auto bg-white">
{layout && layout.components.length > 0 ? (
// 캔버스 컴포넌트들만 표시 - 전체 화면 사용
<div className="relative h-full w-full">
// 캔버스 컴포넌트들을 정확한 해상도로 표시
<div
className="relative mx-auto bg-white"
style={{
width: `${screenWidth}px`,
height: `${screenHeight}px`,
minWidth: `${screenWidth}px`,
minHeight: `${screenHeight}px`,
}}
>
{layout.components
.filter((comp) => !comp.parentId) // 최상위 컴포넌트만 렌더링 (그룹 포함)
.map((component) => {
@ -218,7 +230,15 @@ export default function ScreenViewPage() {
</div>
) : (
// 빈 화면일 때도 깔끔하게 표시
<div className="flex h-full items-center justify-center bg-gray-50">
<div
className="mx-auto flex items-center justify-center bg-gray-50"
style={{
width: `${screenWidth}px`,
height: `${screenHeight}px`,
minWidth: `${screenWidth}px`,
minHeight: `${screenHeight}px`,
}}
>
<div className="text-center">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-white shadow-sm">
<span className="text-2xl">📄</span>

View File

@ -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;
}

View File

@ -66,6 +66,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
// 팝업 화면 레이아웃 상태
const [popupLayout, setPopupLayout] = useState<ComponentData[]>([]);
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<InteractiveScreenViewerProps> = (
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<InteractiveScreenViewerProps> = (
<div className="text-gray-500"> ...</div>
</div>
) : popupLayout.length > 0 ? (
<div className="space-y-4">
<div className="relative bg-white border rounded" style={{
width: popupScreenResolution ? `${popupScreenResolution.width}px` : "100%",
height: popupScreenResolution ? `${popupScreenResolution.height}px` : "400px",
minHeight: "400px",
position: "relative",
overflow: "hidden"
}}>
{/* 팝업에서도 실제 위치와 크기로 렌더링 */}
{popupLayout.map((popupComponent) => (
<InteractiveScreenViewer
<div
key={popupComponent.id}
component={popupComponent}
allComponents={popupLayout}
hideLabel={false}
/>
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,
}}
>
<InteractiveScreenViewer
component={popupComponent}
allComponents={popupLayout}
hideLabel={false}
/>
</div>
))}
</div>
) : (

View File

@ -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}
/>
</FloatingPanel>

View File

@ -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<GridPanelProps> = ({
gridSettings,
onGridSettingsChange,
onResetGrid,
onForceGridUpdate,
screenResolution,
}) => {
const updateSetting = (key: keyof GridSettings, value: any) => {
@ -60,10 +62,25 @@ export const GridPanel: React.FC<GridPanelProps> = ({
<h3 className="font-medium text-gray-900"> </h3>
</div>
<Button size="sm" variant="outline" onClick={onResetGrid} className="flex items-center space-x-1">
<RotateCcw className="h-3 w-3" />
<span></span>
</Button>
<div className="flex items-center space-x-2">
{onForceGridUpdate && (
<Button
size="sm"
variant="outline"
onClick={onForceGridUpdate}
className="flex items-center space-x-1"
title="현재 해상도에 맞게 모든 컴포넌트를 격자에 재정렬합니다"
>
<RefreshCw className="h-3 w-3" />
<span></span>
</Button>
)}
<Button size="sm" variant="outline" onClick={onResetGrid} className="flex items-center space-x-1">
<RotateCcw className="h-3 w-3" />
<span></span>
</Button>
</div>
</div>
{/* 주요 토글들 */}
@ -100,19 +117,6 @@ export const GridPanel: React.FC<GridPanelProps> = ({
/>
</div>
</div>
{/* 해상도 정보 */}
{screenResolution && (
<div className="mt-3 rounded bg-blue-50 p-3">
<h4 className="text-xs font-medium text-blue-900"> </h4>
<p className="mt-1 text-xs text-blue-700">
{screenResolution.width} × {screenResolution.height}
</p>
{actualGridInfo && (
<p className="mt-1 text-xs text-blue-600"> : {Math.round(actualGridInfo.columnWidth)}px</p>
)}
</div>
)}
</div>
{/* 설정 영역 */}
@ -124,11 +128,6 @@ export const GridPanel: React.FC<GridPanelProps> = ({
<div>
<Label htmlFor="columns" className="mb-2 block text-sm font-medium">
: {gridSettings.columns}
{isColumnsTooSmall && (
<span className="ml-2 text-xs text-orange-600">
( : {Math.round(actualGridInfo!.columnWidth)}px)
</span>
)}
</Label>
<Slider
id="columns"
@ -258,7 +257,47 @@ export const GridPanel: React.FC<GridPanelProps> = ({
{/* 푸터 */}
<div className="border-t border-gray-200 bg-gray-50 p-3">
<div className="text-xs text-gray-600">💡 </div>
<div className="text-xs text-gray-600">💡 </div>
{/* 해상도 및 격자 정보 */}
{screenResolution && actualGridInfo && (
<>
<Separator />
<div className="space-y-3">
<h4 className="font-medium text-gray-900"> </h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">:</span>
<span className="font-mono">
{screenResolution.width} × {screenResolution.height}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600"> :</span>
<span className={`font-mono ${isColumnsTooSmall ? "text-red-600" : "text-gray-900"}`}>
{actualGridInfo.columnWidth.toFixed(1)}px
{isColumnsTooSmall && " (너무 작음)"}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600"> :</span>
<span className="font-mono">
{(screenResolution.width - gridSettings.padding * 2).toLocaleString()}px
</span>
</div>
{isColumnsTooSmall && (
<div className="rounded-md bg-yellow-50 p-2 text-xs text-yellow-800">
💡 . .
</div>
)}
</div>
</div>
</>
)}
</div>
</div>
);