리포트 관리 되돌리기 #94

Merged
hyeonsu merged 2 commits from feature/report into main 2025-10-13 19:16:31 +09:00
6 changed files with 37 additions and 222 deletions

View File

@ -32,7 +32,6 @@ import mailTemplateFileRoutes from "./routes/mailTemplateFileRoutes";
import mailAccountFileRoutes from "./routes/mailAccountFileRoutes";
import mailSendSimpleRoutes from "./routes/mailSendSimpleRoutes";
import mailReceiveBasicRoutes from "./routes/mailReceiveBasicRoutes";
import mailSentHistoryRoutes from "./routes/mailSentHistoryRoutes";
import dataRoutes from "./routes/dataRoutes";
import testButtonDataflowRoutes from "./routes/testButtonDataflowRoutes";
import externalDbConnectionRoutes from "./routes/externalDbConnectionRoutes";
@ -75,8 +74,8 @@ app.use(
})
);
app.use(compression());
app.use(express.json({ limit: "50mb" }));
app.use(express.urlencoded({ extended: true, limit: "50mb" }));
app.use(express.json({ limit: "10mb" }));
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
// 정적 파일 서빙 전에 CORS 미들웨어 추가 (OPTIONS 요청 처리)
app.options("/uploads/*", (req, res) => {
@ -176,19 +175,7 @@ app.use("/api/layouts", layoutRoutes);
app.use("/api/mail/accounts", mailAccountFileRoutes); // 파일 기반 계정
app.use("/api/mail/templates-file", mailTemplateFileRoutes); // 파일 기반 템플릿
app.use("/api/mail/send", mailSendSimpleRoutes); // 메일 발송
// 메일 수신 라우트 디버깅 - 모든 요청 로깅
app.use("/api/mail/receive", (req, res, next) => {
console.log(`\n🔍 [MAIL RECEIVE REQUEST]`);
console.log(` Method: ${req.method}`);
console.log(` URL: ${req.originalUrl}`);
console.log(` Path: ${req.path}`);
console.log(` Base URL: ${req.baseUrl}`);
console.log(` Params: ${JSON.stringify(req.params)}`);
console.log(` Query: ${JSON.stringify(req.query)}`);
next();
});
app.use("/api/mail/receive", mailReceiveBasicRoutes); // 메일 수신
app.use("/api/mail/sent", mailSentHistoryRoutes); // 발송 이력
app.use("/api/screen", screenStandardRoutes);
app.use("/api/data", dataRoutes);
app.use("/api/test-button-dataflow", testButtonDataflowRoutes);

View File

@ -6,7 +6,6 @@ import { useReportDesigner } from "@/contexts/ReportDesignerContext";
import { ComponentConfig } from "@/types/report";
import { CanvasComponent } from "./CanvasComponent";
import { Ruler } from "./Ruler";
import { GridLayer } from "./GridLayer";
import { v4 as uuidv4 } from "uuid";
export function ReportDesignerCanvas() {
@ -33,7 +32,6 @@ export function ReportDesignerCanvas() {
undo,
redo,
showRuler,
gridConfig,
} = useReportDesigner();
const [{ isOver }, drop] = useDrop(() => ({
@ -333,16 +331,16 @@ 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}
>
{/* 그리드 레이어 */}
<GridLayer
gridConfig={gridConfig}
pageWidth={canvasWidth * 3.7795} // mm to px
pageHeight={canvasHeight * 3.7795}
/>
{/* 페이지 여백 가이드 */}
{currentPage && (
<div

View File

@ -13,7 +13,6 @@ import { useReportDesigner } from "@/contexts/ReportDesignerContext";
import { QueryManager } from "./QueryManager";
import { SignaturePad } from "./SignaturePad";
import { SignatureGenerator } from "./SignatureGenerator";
import { GridSettingsPanel } from "./GridSettingsPanel";
import { reportApi } from "@/lib/api/reportApi";
import { useToast } from "@/hooks/use-toast";
@ -103,7 +102,7 @@ export function ReportDesignerRightPanel() {
<div className="w-[450px] border-l bg-white">
<Tabs value={activeTab} onValueChange={setActiveTab} className="h-full">
<div className="border-b p-2">
<TabsList className="grid w-full grid-cols-4">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="page" className="gap-1 text-xs">
<Settings className="h-3 w-3" />
@ -112,10 +111,6 @@ export function ReportDesignerRightPanel() {
<Settings className="h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="grid" className="gap-1 text-xs">
<Settings className="h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="queries" className="gap-1 text-xs">
<Database className="h-3 w-3" />
@ -1401,15 +1396,6 @@ export function ReportDesignerRightPanel() {
</TabsContent>
{/* 쿼리 탭 */}
{/* 그리드 탭 */}
<TabsContent value="grid" className="mt-0 h-[calc(100vh-120px)]">
<ScrollArea className="h-full">
<div className="space-y-4 p-4">
<GridSettingsPanel />
</div>
</ScrollArea>
</TabsContent>
<TabsContent value="queries" className="mt-0 h-[calc(100vh-120px)]">
<QueryManager />
</TabsContent>

View File

@ -1,23 +1,10 @@
"use client";
import { createContext, useContext, useState, useCallback, ReactNode, useEffect, useRef } from "react";
import {
ComponentConfig,
ReportDetail,
ReportLayout,
ReportPage,
ReportLayoutConfig,
GridConfig,
} from "@/types/report";
import { ComponentConfig, ReportDetail, ReportLayout, ReportPage, ReportLayoutConfig } from "@/types/report";
import { reportApi } from "@/lib/api/reportApi";
import { useToast } from "@/hooks/use-toast";
import { v4 as uuidv4 } from "uuid";
import {
snapComponentToGrid,
createDefaultGridConfig,
calculateGridDimensions,
detectGridCollision,
} from "@/lib/utils/gridUtils";
export interface ReportQuery {
id: string;
@ -84,10 +71,6 @@ interface ReportDesignerContextType {
// 템플릿 적용
applyTemplate: (templateId: string) => void;
// 그리드 관리
gridConfig: GridConfig;
updateGridConfig: (updates: Partial<GridConfig>) => void;
// 캔버스 설정
canvasWidth: number;
canvasHeight: number;
@ -226,50 +209,10 @@ export function ReportDesignerProvider({ reportId, children }: { reportId: strin
[], // ref를 사용하므로 의존성 배열 비움
);
// 그리드 설정
const [gridConfig, setGridConfig] = useState<GridConfig>(() => {
// 기본 페이지 크기 (A4: 794 x 1123 px at 96 DPI)
const defaultPageWidth = 794;
const defaultPageHeight = 1123;
return createDefaultGridConfig(defaultPageWidth, defaultPageHeight);
});
// gridConfig 업데이트 함수
const updateGridConfig = useCallback(
(updates: Partial<GridConfig>) => {
setGridConfig((prev) => {
const newConfig = { ...prev, ...updates };
// cellWidth나 cellHeight가 변경되면 rows/columns 재계산
if (updates.cellWidth || updates.cellHeight) {
const pageWidth = currentPage?.width ? currentPage.width * 3.7795275591 : 794; // mm to px
const pageHeight = currentPage?.height ? currentPage.height * 3.7795275591 : 1123;
const { rows, columns } = calculateGridDimensions(
pageWidth,
pageHeight,
newConfig.cellWidth,
newConfig.cellHeight,
);
newConfig.rows = rows;
newConfig.columns = columns;
}
return newConfig;
});
},
[currentPage],
);
// 레거시 호환성을 위한 별칭
const gridSize = gridConfig.cellWidth;
const showGrid = gridConfig.visible;
const snapToGrid = gridConfig.snapToGrid;
const setGridSize = useCallback(
(size: number) => updateGridConfig({ cellWidth: size, cellHeight: size }),
[updateGridConfig],
);
const setShowGrid = useCallback((visible: boolean) => updateGridConfig({ visible }), [updateGridConfig]);
const setSnapToGrid = useCallback((snap: boolean) => updateGridConfig({ snapToGrid: snap }), [updateGridConfig]);
// 레이아웃 도구 설정
const [gridSize, setGridSize] = useState(10); // Grid Snap 크기 (px)
const [showGrid, setShowGrid] = useState(true); // Grid 표시 여부
const [snapToGrid, setSnapToGrid] = useState(true); // Grid Snap 활성화
// 눈금자 표시
const [showRuler, setShowRuler] = useState(true);
@ -1235,23 +1178,9 @@ export function ReportDesignerProvider({ reportId, children }: { reportId: strin
// 컴포넌트 추가 (현재 페이지에)
const addComponent = useCallback(
(component: ComponentConfig) => {
// 그리드 스냅 적용
const snappedComponent = snapComponentToGrid(component, gridConfig);
// 충돌 감지
const currentComponents = currentPage?.components || [];
if (detectGridCollision(snappedComponent, currentComponents, gridConfig)) {
toast({
title: "경고",
description: "다른 컴포넌트와 겹칩니다. 다른 위치에 배치해주세요.",
variant: "destructive",
});
return;
}
setComponents((prev) => [...prev, snappedComponent]);
setComponents((prev) => [...prev, component]);
},
[setComponents, gridConfig, currentPage, toast],
[setComponents],
);
// 컴포넌트 업데이트 (현재 페이지에서)
@ -1259,60 +1188,18 @@ export function ReportDesignerProvider({ reportId, children }: { reportId: strin
(id: string, updates: Partial<ComponentConfig>) => {
if (!currentPageId) return;
setLayoutConfig((prev) => {
let hasCollision = false;
const newPages = prev.pages.map((page) => {
if (page.page_id !== currentPageId) return page;
const newComponents = page.components.map((comp) => {
if (comp.id !== id) return comp;
// 업데이트된 컴포넌트에 그리드 스냅 적용
const updated = { ...comp, ...updates };
// 위치나 크기가 변경된 경우에만 스냅 적용 및 충돌 감지
if (
updates.x !== undefined ||
updates.y !== undefined ||
updates.width !== undefined ||
updates.height !== undefined
) {
const snapped = snapComponentToGrid(updated, gridConfig);
// 충돌 감지 (자신을 제외한 다른 컴포넌트와)
const otherComponents = page.components.filter((c) => c.id !== id);
if (detectGridCollision(snapped, otherComponents, gridConfig)) {
hasCollision = true;
return comp; // 충돌 시 원래 상태 유지
setLayoutConfig((prev) => ({
pages: prev.pages.map((page) =>
page.page_id === currentPageId
? {
...page,
components: page.components.map((comp) => (comp.id === id ? { ...comp, ...updates } : comp)),
}
return snapped;
}
return updated;
});
return {
...page,
components: newComponents,
};
});
// 충돌이 감지된 경우 토스트 메시지 표시 및 업데이트 취소
if (hasCollision) {
toast({
title: "경고",
description: "다른 컴포넌트와 겹칩니다.",
variant: "destructive",
});
return prev;
}
return { pages: newPages };
});
: page,
),
}));
},
[currentPageId, gridConfig, toast],
[currentPageId],
);
// 컴포넌트 삭제 (현재 페이지에서)
@ -1426,36 +1313,14 @@ export function ReportDesignerProvider({ reportId, children }: { reportId: strin
window.history.replaceState({}, "", `/admin/report/designer/${actualReportId}`);
}
// 백엔드 호환성을 위해 첫 번째 페이지 정보를 레거시 필드로 변환
const firstPage = layoutConfig.pages[0];
const legacyFormat = firstPage
? {
canvasWidth: firstPage.width,
canvasHeight: firstPage.height,
pageOrientation: firstPage.orientation,
components: firstPage.components,
margins: firstPage.margins,
// 새로운 페이지 기반 구조도 함께 전송
layoutConfig,
queries: queries.map((q) => ({
...q,
externalConnectionId: q.externalConnectionId || undefined,
})),
}
: {
canvasWidth: 210,
canvasHeight: 297,
pageOrientation: "portrait" as const,
components: [],
layoutConfig,
queries: queries.map((q) => ({
...q,
externalConnectionId: q.externalConnectionId || undefined,
})),
};
// 레이아웃 저장
await reportApi.saveLayout(actualReportId, legacyFormat);
// 레이아웃 저장 (페이지 구조로)
await reportApi.saveLayout(actualReportId, {
layoutConfig, // 페이지 기반 구조
queries: queries.map((q) => ({
...q,
externalConnectionId: q.externalConnectionId || undefined,
})),
});
toast({
title: "성공",
@ -1676,9 +1541,6 @@ export function ReportDesignerProvider({ reportId, children }: { reportId: strin
// 그룹화
groupComponents,
ungroupComponents,
// 그리드 관리
gridConfig,
updateGridConfig,
};
return <ReportDesignerContext.Provider value={value}>{children}</ReportDesignerContext.Provider>;

View File

@ -12,12 +12,12 @@ const getApiBaseUrl = (): string => {
const currentHost = window.location.hostname;
const currentPort = window.location.port;
// 🎯 로컬 개발환경: Next.js 프록시 사용 (대용량 요청 안정성)
// 로컬 개발환경: localhost:9771 또는 localhost:3000 → localhost:8080
if (
(currentHost === "localhost" || currentHost === "127.0.0.1") &&
(currentPort === "9771" || currentPort === "3000")
) {
return "/api"; // 프록시 사용
return "http://localhost:8080/api";
}
}

View File

@ -81,18 +81,6 @@ export interface ExternalConnection {
is_active: string;
}
// 그리드 설정
export interface GridConfig {
cellWidth: number; // 그리드 셀 너비 (px)
cellHeight: number; // 그리드 셀 높이 (px)
rows: number; // 세로 그리드 수 (계산값: pageHeight / cellHeight)
columns: number; // 가로 그리드 수 (계산값: pageWidth / cellHeight)
visible: boolean; // 그리드 표시 여부
snapToGrid: boolean; // 그리드 스냅 활성화 여부
gridColor: string; // 그리드 선 색상
gridOpacity: number; // 그리드 투명도 (0-1)
}
// 페이지 설정
export interface ReportPage {
page_id: string;
@ -108,7 +96,6 @@ export interface ReportPage {
right: number;
};
background_color: string;
gridConfig?: GridConfig; // 그리드 설정 (옵셔널)
components: ComponentConfig[];
}
@ -126,11 +113,6 @@ export interface ComponentConfig {
width: number;
height: number;
zIndex: number;
// 그리드 좌표 (옵셔널)
gridX?: number; // 시작 열 (0부터 시작)
gridY?: number; // 시작 행 (0부터 시작)
gridWidth?: number; // 차지하는 열 수
gridHeight?: number; // 차지하는 행 수
fontSize?: number;
fontFamily?: string;
fontWeight?: string;