캔버스에 그리드 시스템 적용

This commit is contained in:
dohyeons 2025-10-01 15:32:35 +09:00
parent 1c00ee28e8
commit ae23a4408e
4 changed files with 111 additions and 16 deletions

View File

@ -9,7 +9,8 @@ interface CanvasComponentProps {
}
export function CanvasComponent({ component }: CanvasComponentProps) {
const { selectedComponentId, selectComponent, updateComponent, getQueryResult } = useReportDesigner();
const { selectedComponentId, selectComponent, updateComponent, getQueryResult, snapValueToGrid } =
useReportDesigner();
const [isDragging, setIsDragging] = useState(false);
const [isResizing, setIsResizing] = useState(false);
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
@ -53,13 +54,21 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
if (isDragging) {
const newX = Math.max(0, e.clientX - dragStart.x);
const newY = Math.max(0, e.clientY - dragStart.y);
updateComponent(component.id, { x: newX, y: newY });
// Grid Snap 적용
updateComponent(component.id, {
x: snapValueToGrid(newX),
y: snapValueToGrid(newY),
});
} else if (isResizing) {
const deltaX = e.clientX - resizeStart.x;
const deltaY = e.clientY - resizeStart.y;
const newWidth = Math.max(50, resizeStart.width + deltaX);
const newHeight = Math.max(30, resizeStart.height + deltaY);
updateComponent(component.id, { width: newWidth, height: newHeight });
// Grid Snap 적용
updateComponent(component.id, {
width: snapValueToGrid(newWidth),
height: snapValueToGrid(newHeight),
});
}
};
@ -86,6 +95,7 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
resizeStart.height,
component.id,
updateComponent,
snapValueToGrid,
]);
// 표시할 값 결정

View File

@ -9,8 +9,18 @@ import { v4 as uuidv4 } from "uuid";
export function ReportDesignerCanvas() {
const canvasRef = useRef<HTMLDivElement>(null);
const { components, addComponent, canvasWidth, canvasHeight, selectComponent, selectedComponentId, removeComponent } =
useReportDesigner();
const {
components,
addComponent,
canvasWidth,
canvasHeight,
selectComponent,
selectedComponentId,
removeComponent,
showGrid,
gridSize,
snapValueToGrid,
} = useReportDesigner();
const [{ isOver }, drop] = useDrop(() => ({
accept: "component",
@ -25,14 +35,14 @@ export function ReportDesignerCanvas() {
const x = offset.x - canvasRect.left;
const y = offset.y - canvasRect.top;
// 새 컴포넌트 생성
// 새 컴포넌트 생성 (Grid Snap 적용)
const newComponent: ComponentConfig = {
id: `comp_${uuidv4()}`,
type: item.componentType,
x: Math.max(0, x - 100),
y: Math.max(0, y - 25),
width: 200,
height: item.componentType === "table" ? 200 : 100,
x: snapValueToGrid(Math.max(0, x - 100)),
y: snapValueToGrid(Math.max(0, y - 25)),
width: snapValueToGrid(200),
height: snapValueToGrid(item.componentType === "table" ? 200 : 100),
zIndex: components.length,
fontSize: 13,
fontFamily: "Malgun Gothic",
@ -89,6 +99,13 @@ export function ReportDesignerCanvas() {
style={{
width: `${canvasWidth}mm`,
minHeight: `${canvasHeight}mm`,
backgroundImage: showGrid
? `
linear-gradient(to right, #e5e7eb 1px, transparent 1px),
linear-gradient(to bottom, #e5e7eb 1px, transparent 1px)
`
: undefined,
backgroundSize: showGrid ? `${gridSize}px ${gridSize}px` : undefined,
}}
onClick={handleCanvasClick}
>

View File

@ -1,7 +1,7 @@
"use client";
import { Button } from "@/components/ui/button";
import { Save, Eye, RotateCcw, ArrowLeft, Loader2, BookTemplate } from "lucide-react";
import { Save, Eye, RotateCcw, ArrowLeft, Loader2, BookTemplate, Grid3x3 } from "lucide-react";
import { useRouter } from "next/navigation";
import { useReportDesigner } from "@/contexts/ReportDesignerContext";
import { useState } from "react";
@ -13,8 +13,20 @@ import { ReportPreviewModal } from "./ReportPreviewModal";
export function ReportDesignerToolbar() {
const router = useRouter();
const { reportDetail, saveLayout, isSaving, loadLayout, components, canvasWidth, canvasHeight, queries } =
useReportDesigner();
const {
reportDetail,
saveLayout,
isSaving,
loadLayout,
components,
canvasWidth,
canvasHeight,
queries,
snapToGrid,
setSnapToGrid,
showGrid,
setShowGrid,
} = useReportDesigner();
const [showPreview, setShowPreview] = useState(false);
const [showSaveAsTemplate, setShowSaveAsTemplate] = useState(false);
const { toast } = useToast();
@ -22,6 +34,13 @@ export function ReportDesignerToolbar() {
// 템플릿 저장 가능 여부: 컴포넌트가 있어야 함
const canSaveAsTemplate = components.length > 0;
// Grid 토글 (Snap과 Grid 표시 함께 제어)
const handleToggleGrid = () => {
const newValue = !snapToGrid;
setSnapToGrid(newValue);
setShowGrid(newValue);
};
const handleSave = async () => {
await saveLayout();
};
@ -120,6 +139,16 @@ export function ReportDesignerToolbar() {
</div>
<div className="flex items-center gap-2">
<Button
variant={snapToGrid && showGrid ? "default" : "outline"}
size="sm"
onClick={handleToggleGrid}
className="gap-2"
title="Grid Snap 및 표시 켜기/끄기"
>
<Grid3x3 className="h-4 w-4" />
{snapToGrid && showGrid ? "Grid ON" : "Grid OFF"}
</Button>
<Button variant="outline" size="sm" onClick={handleReset} className="gap-2">
<RotateCcw className="h-4 w-4" />

View File

@ -307,6 +307,15 @@ interface ReportDesignerContextType {
left: number;
right: number;
};
// 레이아웃 도구
gridSize: number;
setGridSize: (size: number) => void;
showGrid: boolean;
setShowGrid: (show: boolean) => void;
snapToGrid: boolean;
setSnapToGrid: (snap: boolean) => void;
snapValueToGrid: (value: number) => number;
}
const ReportDesignerContext = createContext<ReportDesignerContextType | undefined>(undefined);
@ -322,6 +331,20 @@ export function ReportDesignerProvider({ reportId, children }: { reportId: strin
const [isSaving, setIsSaving] = useState(false);
const { toast } = useToast();
// 레이아웃 도구 설정
const [gridSize, setGridSize] = useState(10); // Grid Snap 크기 (px)
const [showGrid, setShowGrid] = useState(true); // Grid 표시 여부
const [snapToGrid, setSnapToGrid] = useState(true); // Grid Snap 활성화
// Grid Snap 함수
const snapValueToGrid = useCallback(
(value: number): number => {
if (!snapToGrid) return value;
return Math.round(value / gridSize) * gridSize;
},
[snapToGrid, gridSize],
);
// 캔버스 설정 (기본값)
const [canvasWidth, setCanvasWidth] = useState(210);
const [canvasHeight, setCanvasHeight] = useState(297);
@ -562,7 +585,7 @@ export function ReportDesignerProvider({ reportId, children }: { reportId: strin
// 커스텀 템플릿 찾기
const customTemplates = response.data.custom || [];
const template = customTemplates.find((t: any) => t.template_id === templateId);
const template = customTemplates.find((t: { template_id: string }) => t.template_id === templateId);
if (!template) {
throw new Error("템플릿을 찾을 수 없습니다.");
@ -578,13 +601,21 @@ export function ReportDesignerProvider({ reportId, children }: { reportId: strin
: template.default_queries || [];
// 컴포넌트 적용 (ID 재생성)
const newComponents = layoutConfig.components.map((comp: any) => ({
const newComponents = (layoutConfig.components as ComponentConfig[]).map((comp) => ({
...comp,
id: `comp-${Date.now()}-${Math.random()}`,
}));
// 쿼리 적용 (ID 재생성)
const newQueries = defaultQueries.map((q: any) => ({
const newQueries = (
defaultQueries as Array<{
name: string;
type: "MASTER" | "DETAIL";
sqlQuery: string;
parameters: string[];
externalConnectionId?: number | null;
}>
).map((q) => ({
id: `query-${Date.now()}-${Math.random()}`,
name: q.name,
type: q.type,
@ -637,6 +668,14 @@ export function ReportDesignerProvider({ reportId, children }: { reportId: strin
canvasHeight,
pageOrientation,
margins,
// 레이아웃 도구
gridSize,
setGridSize,
showGrid,
setShowGrid,
snapToGrid,
setSnapToGrid,
snapValueToGrid,
};
return <ReportDesignerContext.Provider value={value}>{children}</ReportDesignerContext.Provider>;