리포트 관리 되돌리기
This commit is contained in:
parent
a53940cff9
commit
28d460fecd
|
|
@ -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";
|
||||
|
|
@ -74,8 +73,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) => {
|
||||
|
|
@ -175,19 +174,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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue