From da24db8f37000bdded1b7eaef77020c6dca4bc80 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Thu, 18 Dec 2025 16:03:47 +0900 Subject: [PATCH] =?UTF-8?q?=EC=99=B8=EB=B6=80=20=EC=97=85=EC=B2=B4=20?= =?UTF-8?q?=EC=A0=84=EC=9A=A9=20=EB=AA=A8=EB=93=9C=20=EB=B0=8F=203D=20?= =?UTF-8?q?=EC=BA=94=EB=B2=84=EC=8A=A4=20=EC=A0=84=EC=B2=B4=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=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 --- backend-node/src/routes/roleRoutes.ts | 18 +-- .../widgets/yard-3d/DigitalTwinViewer.tsx | 131 +++++++++++++++--- 2 files changed, 124 insertions(+), 25 deletions(-) diff --git a/backend-node/src/routes/roleRoutes.ts b/backend-node/src/routes/roleRoutes.ts index 21c17ecb..0f8a64b0 100644 --- a/backend-node/src/routes/roleRoutes.ts +++ b/backend-node/src/routes/roleRoutes.ts @@ -22,6 +22,15 @@ const router = Router(); // 모든 role 라우트에 인증 미들웨어 적용 router.use(authenticateToken); +/** + * 사용자 권한 그룹 조회 (/:id 보다 먼저 정의해야 함) + */ +// 현재 사용자가 속한 권한 그룹 조회 +router.get("/user/my-groups", getUserRoleGroups); + +// 특정 사용자가 속한 권한 그룹 조회 +router.get("/user/:userId/groups", requireAdmin, getUserRoleGroups); + /** * 권한 그룹 CRUD */ @@ -67,13 +76,4 @@ router.get("/:id/menu-permissions", requireAdmin, getMenuPermissions); // 메뉴 권한 설정 router.put("/:id/menu-permissions", requireAdmin, setMenuPermissions); -/** - * 사용자 권한 그룹 조회 - */ -// 현재 사용자가 속한 권한 그룹 조회 -router.get("/user/my-groups", getUserRoleGroups); - -// 특정 사용자가 속한 권한 그룹 조회 -router.get("/user/:userId/groups", requireAdmin, getUserRoleGroups); - export default router; diff --git a/frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinViewer.tsx b/frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinViewer.tsx index 8df5c983..c5d3e463 100644 --- a/frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinViewer.tsx +++ b/frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinViewer.tsx @@ -1,7 +1,7 @@ "use client"; -import { useState, useEffect, useMemo } from "react"; -import { Loader2, Search, X, Grid3x3, Package, ParkingCircle, RefreshCw } from "lucide-react"; +import { useState, useEffect, useMemo, useRef } from "react"; +import { Loader2, Search, X, Grid3x3, Package, ParkingCircle, RefreshCw, Maximize, Minimize } from "lucide-react"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Button } from "@/components/ui/button"; @@ -12,6 +12,7 @@ import type { PlacedObject, MaterialData } from "@/types/digitalTwin"; import { getLayoutById, getMaterials } from "@/lib/api/digitalTwin"; import { OBJECT_COLORS, DEFAULT_COLOR } from "./constants"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"; +import { apiCall } from "@/lib/api/client"; const Yard3DCanvas = dynamic(() => import("./Yard3DCanvas"), { ssr: false, @@ -26,6 +27,9 @@ interface DigitalTwinViewerProps { layoutId: number; } +// 외부 업체 역할 코드 +const EXTERNAL_VENDOR_ROLE = "LSTHIRA_EXTERNAL_VENDOR"; + export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps) { const { toast } = useToast(); const [placedObjects, setPlacedObjects] = useState([]); @@ -43,6 +47,64 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps) const [filterType, setFilterType] = useState("all"); const [isRefreshing, setIsRefreshing] = useState(false); + // 외부 업체 모드 + const [isExternalMode, setIsExternalMode] = useState(false); + const [isFullscreen, setIsFullscreen] = useState(false); + const canvasContainerRef = useRef(null); + + // 외부 업체 역할 체크 + useEffect(() => { + const checkExternalRole = async () => { + try { + const response = await apiCall("GET", "/roles/user/my-groups"); + console.log("=== 사용자 권한 그룹 조회 ==="); + console.log("API 응답:", response); + console.log("찾는 역할:", EXTERNAL_VENDOR_ROLE); + + if (response.success && response.data) { + console.log("권한 그룹 목록:", response.data); + + // 사용자의 권한 그룹 중 LSTHIRA_EXTERNAL_VENDOR가 있는지 확인 + const hasExternalRole = response.data.some( + (group: any) => { + console.log("체크 중인 그룹:", group.authCode, group.authName); + return group.authCode === EXTERNAL_VENDOR_ROLE || group.authName === EXTERNAL_VENDOR_ROLE; + } + ); + + console.log("외부 업체 역할 보유:", hasExternalRole); + setIsExternalMode(hasExternalRole); + } + } catch (error) { + console.error("역할 조회 실패:", error); + } + }; + + checkExternalRole(); + }, []); + + // 전체 화면 토글 (3D 캔버스 영역만) + const toggleFullscreen = () => { + if (!document.fullscreenElement) { + // 3D 캔버스 컨테이너만 풀스크린 + canvasContainerRef.current?.requestFullscreen(); + setIsFullscreen(true); + } else { + document.exitFullscreen(); + setIsFullscreen(false); + } + }; + + // 전체 화면 변경 감지 + useEffect(() => { + const handleFullscreenChange = () => { + setIsFullscreen(!!document.fullscreenElement); + }; + + document.addEventListener("fullscreenchange", handleFullscreenChange); + return () => document.removeEventListener("fullscreenchange", handleFullscreenChange); + }, []); + // 레이아웃 데이터 로드 함수 const loadLayout = async () => { try { @@ -334,23 +396,42 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)

{layoutName || "디지털 트윈 야드"}

-

읽기 전용 뷰

+

+ {isExternalMode ? "야드 관제 화면" : "읽기 전용 뷰"} +

+
+
+ {/* 전체 화면 버튼 */} + +
-
{/* 메인 영역 */}
- {/* 좌측: 검색/필터 */} + {/* 좌측: 검색/필터 - 외부 모드에서는 숨김 */} + {!isExternalMode && (
{/* 검색 */} @@ -575,9 +656,13 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps) )}
+ )} {/* 중앙: 3D 캔버스 */} -
+
{!isLoading && ( {}} /> )} + {/* 풀스크린 모드일 때 종료 버튼 */} + {isFullscreen && ( + + )}
- {/* 우측: 정보 패널 */} + {/* 우측: 정보 패널 - 외부 모드에서는 숨김 */} + {!isExternalMode && (
{selectedObject ? (
@@ -693,6 +791,7 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
)}
+ )}
);