From 3c86b22a9965897b0ab5bc6df2a22685cdb0586b Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 3 Sep 2025 18:23:47 +0900 Subject: [PATCH] =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EB=B3=B5=EC=82=AC?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/screenManagementController.ts | 35 + .../src/routes/screenManagementRoutes.ts | 2 + .../src/services/screenManagementService.ts | 124 + .../components/screen/CopyScreenModal.tsx | 192 ++ frontend/components/screen/ScreenList.tsx | 34 +- .../screen/panels/DataTableConfigPanel.tsx | 2242 +++++++++-------- .../screen/panels/PropertiesPanel.tsx | 4 + frontend/lib/api/screen.ts | 13 + 8 files changed, 1591 insertions(+), 1055 deletions(-) create mode 100644 frontend/components/screen/CopyScreenModal.tsx diff --git a/backend-node/src/controllers/screenManagementController.ts b/backend-node/src/controllers/screenManagementController.ts index acbfbfcc..73e22583 100644 --- a/backend-node/src/controllers/screenManagementController.ts +++ b/backend-node/src/controllers/screenManagementController.ts @@ -108,6 +108,41 @@ export const deleteScreen = async ( } }; +// 화면 복사 +export const copyScreen = async ( + req: AuthenticatedRequest, + res: Response +): Promise => { + try { + const { id } = req.params; + const { screenName, screenCode, description } = req.body; + const { companyCode, userId } = req.user as any; + + const copiedScreen = await screenManagementService.copyScreen( + parseInt(id), + { + screenName, + screenCode, + description, + companyCode, + createdBy: userId, + } + ); + + res.json({ + success: true, + data: copiedScreen, + message: "화면이 복사되었습니다.", + }); + } catch (error: any) { + console.error("화면 복사 실패:", error); + res.status(500).json({ + success: false, + message: error.message || "화면 복사에 실패했습니다.", + }); + } +}; + // 테이블 목록 조회 (모든 테이블) export const getTables = async (req: AuthenticatedRequest, res: Response) => { try { diff --git a/backend-node/src/routes/screenManagementRoutes.ts b/backend-node/src/routes/screenManagementRoutes.ts index 7e2dfd72..33fb8697 100644 --- a/backend-node/src/routes/screenManagementRoutes.ts +++ b/backend-node/src/routes/screenManagementRoutes.ts @@ -6,6 +6,7 @@ import { createScreen, updateScreen, deleteScreen, + copyScreen, getTables, getTableInfo, getTableColumns, @@ -28,6 +29,7 @@ router.get("/screens/:id", getScreen); router.post("/screens", createScreen); router.put("/screens/:id", updateScreen); router.delete("/screens/:id", deleteScreen); +router.post("/screens/:id/copy", copyScreen); // 화면 코드 자동 생성 router.get("/generate-screen-code/:companyCode", generateScreenCode); diff --git a/backend-node/src/services/screenManagementService.ts b/backend-node/src/services/screenManagementService.ts index 627bafff..c9049da5 100644 --- a/backend-node/src/services/screenManagementService.ts +++ b/backend-node/src/services/screenManagementService.ts @@ -14,8 +14,18 @@ import { WebType, WidgetData, } from "../types/screen"; + import { generateId } from "../utils/generateId"; +// 화면 복사 요청 인터페이스 +interface CopyScreenRequest { + screenName: string; + screenCode: string; + description?: string; + companyCode: string; + createdBy: string; +} + // 백엔드에서 사용할 테이블 정보 타입 interface TableInfo { tableName: string; @@ -968,6 +978,120 @@ export class ScreenManagementService { return `${companyCode}_${paddedNumber}`; } + + /** + * 화면 복사 (화면 정보 + 레이아웃 모두 복사) + */ + async copyScreen( + sourceScreenId: number, + copyData: CopyScreenRequest + ): Promise { + // 트랜잭션으로 처리 + return await prisma.$transaction(async (tx) => { + // 1. 원본 화면 정보 조회 + const sourceScreen = await tx.screen_definitions.findFirst({ + where: { + screen_id: sourceScreenId, + company_code: copyData.companyCode, + }, + }); + + if (!sourceScreen) { + throw new Error("복사할 화면을 찾을 수 없습니다."); + } + + // 2. 화면 코드 중복 체크 + const existingScreen = await tx.screen_definitions.findFirst({ + where: { + screen_code: copyData.screenCode, + company_code: copyData.companyCode, + }, + }); + + if (existingScreen) { + throw new Error("이미 존재하는 화면 코드입니다."); + } + + // 3. 새 화면 생성 + const newScreen = await tx.screen_definitions.create({ + data: { + screen_code: copyData.screenCode, + screen_name: copyData.screenName, + description: copyData.description || sourceScreen.description, + company_code: copyData.companyCode, + table_name: sourceScreen.table_name, + is_active: sourceScreen.is_active, + created_by: copyData.createdBy, + created_date: new Date(), + updated_by: copyData.createdBy, + updated_date: new Date(), + }, + }); + + // 4. 원본 화면의 레이아웃 정보 조회 + const sourceLayouts = await tx.screen_layouts.findMany({ + where: { + screen_id: sourceScreenId, + }, + orderBy: { display_order: "asc" }, + }); + + // 5. 레이아웃이 있다면 복사 + if (sourceLayouts.length > 0) { + try { + // ID 매핑 맵 생성 + const idMapping: { [oldId: string]: string } = {}; + + // 새로운 컴포넌트 ID 미리 생성 + sourceLayouts.forEach((layout) => { + idMapping[layout.component_id] = generateId(); + }); + + // 각 레이아웃 컴포넌트 복사 + for (const sourceLayout of sourceLayouts) { + const newComponentId = idMapping[sourceLayout.component_id]; + const newParentId = sourceLayout.parent_id + ? idMapping[sourceLayout.parent_id] + : null; + + await tx.screen_layouts.create({ + data: { + screen_id: newScreen.screen_id, + component_type: sourceLayout.component_type, + component_id: newComponentId, + parent_id: newParentId, + position_x: sourceLayout.position_x, + position_y: sourceLayout.position_y, + width: sourceLayout.width, + height: sourceLayout.height, + properties: sourceLayout.properties as any, + display_order: sourceLayout.display_order, + created_date: new Date(), + }, + }); + } + } catch (error) { + console.error("레이아웃 복사 중 오류:", error); + // 레이아웃 복사 실패해도 화면 생성은 유지 + } + } + + // 6. 생성된 화면 정보 반환 + return { + screenId: newScreen.screen_id, + screenCode: newScreen.screen_code, + screenName: newScreen.screen_name, + description: newScreen.description || "", + companyCode: newScreen.company_code, + tableName: newScreen.table_name, + isActive: newScreen.is_active, + createdBy: newScreen.created_by || undefined, + createdDate: newScreen.created_date, + updatedBy: newScreen.updated_by || undefined, + updatedDate: newScreen.updated_date, + }; + }); + } } // 서비스 인스턴스 export diff --git a/frontend/components/screen/CopyScreenModal.tsx b/frontend/components/screen/CopyScreenModal.tsx new file mode 100644 index 00000000..d540a783 --- /dev/null +++ b/frontend/components/screen/CopyScreenModal.tsx @@ -0,0 +1,192 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { Loader2, Copy } from "lucide-react"; +import { ScreenDefinition } from "@/types/screen"; +import { screenApi } from "@/lib/api/screen"; +import { toast } from "sonner"; + +interface CopyScreenModalProps { + isOpen: boolean; + onClose: () => void; + sourceScreen: ScreenDefinition | null; + onCopySuccess: () => void; +} + +export default function CopyScreenModal({ isOpen, onClose, sourceScreen, onCopySuccess }: CopyScreenModalProps) { + const [screenName, setScreenName] = useState(""); + const [screenCode, setScreenCode] = useState(""); + const [description, setDescription] = useState(""); + + const [isCopying, setIsCopying] = useState(false); + + // 모달이 열릴 때 초기값 설정 + useEffect(() => { + if (isOpen && sourceScreen) { + setScreenName(`${sourceScreen.screenName} (복사본)`); + setDescription(sourceScreen.description || ""); + // 화면 코드 자동 생성 + generateNewScreenCode(); + } + }, [isOpen, sourceScreen]); + + // 새로운 화면 코드 자동 생성 + const generateNewScreenCode = async () => { + if (!sourceScreen?.companyCode) return; + + try { + const newCode = await screenApi.generateScreenCode(sourceScreen.companyCode); + setScreenCode(newCode); + } catch (error) { + console.error("화면 코드 생성 실패:", error); + toast.error("화면 코드 생성에 실패했습니다."); + } + }; + + // 화면 복사 실행 + const handleCopy = async () => { + if (!sourceScreen) return; + + // 입력값 검증 + if (!screenName.trim()) { + toast.error("화면명을 입력해주세요."); + return; + } + + if (!screenCode.trim()) { + toast.error("화면 코드 생성에 실패했습니다. 잠시 후 다시 시도해주세요."); + return; + } + + try { + setIsCopying(true); + + // 화면 복사 API 호출 + await screenApi.copyScreen(sourceScreen.screenId, { + screenName: screenName.trim(), + screenCode: screenCode.trim(), + description: description.trim(), + }); + + toast.success("화면이 성공적으로 복사되었습니다."); + onCopySuccess(); + handleClose(); + } catch (error: any) { + console.error("화면 복사 실패:", error); + const errorMessage = error.response?.data?.message || "화면 복사에 실패했습니다."; + toast.error(errorMessage); + } finally { + setIsCopying(false); + } + }; + + // 모달 닫기 + const handleClose = () => { + setScreenName(""); + setScreenCode(""); + setDescription(""); + onClose(); + }; + + return ( + + + + + + 화면 복사 + + + {sourceScreen?.screenName} 화면을 복사합니다. 화면 구성도 함께 복사됩니다. + + + +
+ {/* 원본 화면 정보 */} +
+

원본 화면 정보

+
+
+ 화면명: {sourceScreen?.screenName} +
+
+ 화면코드: {sourceScreen?.screenCode} +
+
+ 회사코드: {sourceScreen?.companyCode} +
+
+
+ + {/* 새 화면 정보 입력 */} +
+
+ + setScreenName(e.target.value)} + placeholder="복사될 화면의 이름을 입력하세요" + className="mt-1" + /> +
+ +
+ + +
+ +
+ +