리포트 관리 되돌리기

This commit is contained in:
dohyeons 2025-10-13 19:15:52 +09:00
parent a53940cff9
commit 28d460fecd
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 mailAccountFileRoutes from "./routes/mailAccountFileRoutes";
import mailSendSimpleRoutes from "./routes/mailSendSimpleRoutes"; import mailSendSimpleRoutes from "./routes/mailSendSimpleRoutes";
import mailReceiveBasicRoutes from "./routes/mailReceiveBasicRoutes"; import mailReceiveBasicRoutes from "./routes/mailReceiveBasicRoutes";
import mailSentHistoryRoutes from "./routes/mailSentHistoryRoutes";
import dataRoutes from "./routes/dataRoutes"; import dataRoutes from "./routes/dataRoutes";
import testButtonDataflowRoutes from "./routes/testButtonDataflowRoutes"; import testButtonDataflowRoutes from "./routes/testButtonDataflowRoutes";
import externalDbConnectionRoutes from "./routes/externalDbConnectionRoutes"; import externalDbConnectionRoutes from "./routes/externalDbConnectionRoutes";
@ -74,8 +73,8 @@ app.use(
}) })
); );
app.use(compression()); app.use(compression());
app.use(express.json({ limit: "50mb" })); app.use(express.json({ limit: "10mb" }));
app.use(express.urlencoded({ extended: true, limit: "50mb" })); app.use(express.urlencoded({ extended: true, limit: "10mb" }));
// 정적 파일 서빙 전에 CORS 미들웨어 추가 (OPTIONS 요청 처리) // 정적 파일 서빙 전에 CORS 미들웨어 추가 (OPTIONS 요청 처리)
app.options("/uploads/*", (req, res) => { app.options("/uploads/*", (req, res) => {
@ -175,19 +174,7 @@ app.use("/api/layouts", layoutRoutes);
app.use("/api/mail/accounts", mailAccountFileRoutes); // 파일 기반 계정 app.use("/api/mail/accounts", mailAccountFileRoutes); // 파일 기반 계정
app.use("/api/mail/templates-file", mailTemplateFileRoutes); // 파일 기반 템플릿 app.use("/api/mail/templates-file", mailTemplateFileRoutes); // 파일 기반 템플릿
app.use("/api/mail/send", mailSendSimpleRoutes); // 메일 발송 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/receive", mailReceiveBasicRoutes); // 메일 수신
app.use("/api/mail/sent", mailSentHistoryRoutes); // 발송 이력
app.use("/api/screen", screenStandardRoutes); app.use("/api/screen", screenStandardRoutes);
app.use("/api/data", dataRoutes); app.use("/api/data", dataRoutes);
app.use("/api/test-button-dataflow", testButtonDataflowRoutes); app.use("/api/test-button-dataflow", testButtonDataflowRoutes);

View File

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

View File

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

View File

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

View File

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

View File

@ -81,18 +81,6 @@ export interface ExternalConnection {
is_active: string; 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 { export interface ReportPage {
page_id: string; page_id: string;
@ -108,7 +96,6 @@ export interface ReportPage {
right: number; right: number;
}; };
background_color: string; background_color: string;
gridConfig?: GridConfig; // 그리드 설정 (옵셔널)
components: ComponentConfig[]; components: ComponentConfig[];
} }
@ -126,11 +113,6 @@ export interface ComponentConfig {
width: number; width: number;
height: number; height: number;
zIndex: number; zIndex: number;
// 그리드 좌표 (옵셔널)
gridX?: number; // 시작 열 (0부터 시작)
gridY?: number; // 시작 행 (0부터 시작)
gridWidth?: number; // 차지하는 열 수
gridHeight?: number; // 차지하는 행 수
fontSize?: number; fontSize?: number;
fontFamily?: string; fontFamily?: string;
fontWeight?: string; fontWeight?: string;