외부 업체 전용 모드 및 3D 캔버스 전체 화면 기능 구현
This commit is contained in:
parent
a617c26721
commit
da24db8f37
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue