외부 업체 전용 모드 및 3D 캔버스 전체 화면 기능 구현

This commit is contained in:
dohyeons 2025-12-18 16:03:47 +09:00
parent a617c26721
commit da24db8f37
2 changed files with 124 additions and 25 deletions

View File

@ -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;

View File

@ -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<PlacedObject[]>([]);
@ -43,6 +47,64 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
const [filterType, setFilterType] = useState<string>("all");
const [isRefreshing, setIsRefreshing] = useState(false);
// 외부 업체 모드
const [isExternalMode, setIsExternalMode] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
const canvasContainerRef = useRef<HTMLDivElement>(null);
// 외부 업체 역할 체크
useEffect(() => {
const checkExternalRole = async () => {
try {
const response = await apiCall<any[]>("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)
<div className="flex items-center justify-between border-b p-4">
<div>
<h2 className="text-lg font-semibold">{layoutName || "디지털 트윈 야드"}</h2>
<p className="text-muted-foreground text-sm"> </p>
<p className="text-muted-foreground text-sm">
{isExternalMode ? "야드 관제 화면" : "읽기 전용 뷰"}
</p>
</div>
<div className="flex items-center gap-2">
{/* 전체 화면 버튼 */}
<Button
variant="outline"
size="sm"
onClick={toggleFullscreen}
title={isFullscreen ? "전체 화면 종료" : "전체 화면"}
>
{isFullscreen ? (
<Minimize className="mr-2 h-4 w-4" />
) : (
<Maximize className="mr-2 h-4 w-4" />
)}
{isFullscreen ? "종료" : "전체 화면"}
</Button>
<Button
variant="outline"
size="sm"
onClick={handleRefresh}
disabled={isRefreshing || isLoading}
title="새로고침"
>
<RefreshCw className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} />
{isRefreshing ? "갱신 중..." : "새로고침"}
</Button>
</div>
<Button
variant="outline"
size="sm"
onClick={handleRefresh}
disabled={isRefreshing || isLoading}
title="새로고침"
>
<RefreshCw className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} />
{isRefreshing ? "갱신 중..." : "새로고침"}
</Button>
</div>
{/* 메인 영역 */}
<div className="flex flex-1 overflow-hidden">
{/* 좌측: 검색/필터 */}
{/* 좌측: 검색/필터 - 외부 모드에서는 숨김 */}
{!isExternalMode && (
<div className="flex h-full w-64 flex-shrink-0 flex-col border-r">
<div className="space-y-4 p-4">
{/* 검색 */}
@ -575,9 +656,13 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
)}
</div>
</div>
)}
{/* 중앙: 3D 캔버스 */}
<div className="relative flex-1">
<div
ref={canvasContainerRef}
className={`relative flex-1 ${isFullscreen ? "bg-background" : ""}`}
>
{!isLoading && (
<Yard3DCanvas
placements={canvasPlacements}
@ -587,9 +672,22 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
onCollisionDetected={() => {}}
/>
)}
{/* 풀스크린 모드일 때 종료 버튼 */}
{isFullscreen && (
<Button
variant="outline"
size="sm"
onClick={toggleFullscreen}
className="absolute top-4 right-4 z-50 bg-background/80 backdrop-blur-sm"
>
<Minimize className="mr-2 h-4 w-4" />
</Button>
)}
</div>
{/* 우측: 정보 패널 */}
{/* 우측: 정보 패널 - 외부 모드에서는 숨김 */}
{!isExternalMode && (
<div className="h-full w-[480px] flex-shrink-0 overflow-y-auto border-l">
{selectedObject ? (
<div className="p-4">
@ -693,6 +791,7 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
</div>
)}
</div>
)}
</div>
</div>
);