diff --git a/frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinViewer.tsx b/frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinViewer.tsx
index 7e0986a3..3e7b5471 100644
--- a/frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinViewer.tsx
+++ b/frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinViewer.tsx
@@ -61,18 +61,16 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
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;
- }
- );
-
+ 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);
}
@@ -101,12 +99,12 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
const handleFullscreenChange = () => {
const isNowFullscreen = !!document.fullscreenElement;
setIsFullscreen(isNowFullscreen);
-
+
// 전체화면 종료 시 레이아웃 강제 리렌더링
if (!isNowFullscreen) {
setTimeout(() => {
- setLayoutKey(prev => prev + 1);
- window.dispatchEvent(new Event('resize'));
+ setLayoutKey((prev) => prev + 1);
+ window.dispatchEvent(new Event("resize"));
}, 50);
}
};
@@ -407,9 +405,7 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
{layoutName || "디지털 트윈 야드"}
-
- {isExternalMode ? "야드 관제 화면" : "읽기 전용 뷰"}
-
+
{isExternalMode ? "야드 관제 화면" : "읽기 전용 뷰"}
{/* 전체 화면 버튼 - 외부 업체 모드에서만 표시 */}
@@ -420,11 +416,7 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
onClick={toggleFullscreen}
title={isFullscreen ? "전체 화면 종료" : "전체 화면"}
>
- {isFullscreen ? (
-
- ) : (
-
- )}
+ {isFullscreen ?
:
}
{isFullscreen ? "종료" : "전체 화면"}
)}
@@ -445,234 +437,234 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
{/* 좌측: 검색/필터 - 외부 모드에서는 숨김 */}
{!isExternalMode && (
-
-
- {/* 검색 */}
-
-
-
-
-
setSearchQuery(e.target.value)}
- placeholder="이름, Area, Location 검색..."
- className="h-10 pl-9 text-sm"
- />
- {searchQuery && (
-
- )}
+
+
+ {/* 검색 */}
+
+
+
+
+ setSearchQuery(e.target.value)}
+ placeholder="이름, Area, Location 검색..."
+ className="h-10 pl-9 text-sm"
+ />
+ {searchQuery && (
+
+ )}
+
+
+ {/* 타입 필터 */}
+
+
+
+
+
+ {/* 필터 초기화 */}
+ {(searchQuery || filterType !== "all") && (
+
+ )}
- {/* 타입 필터 */}
-
-
-
-
+ {/* 객체 목록 */}
+
+
+ {filteredObjects.length === 0 ? (
+
+ {searchQuery ? "검색 결과가 없습니다" : "객체가 없습니다"}
+
+ ) : (
+ (() => {
+ // Area 객체가 있는 경우 계층 트리 아코디언 적용
+ const areaObjects = filteredObjects.filter((obj) => obj.type === "area");
- {/* 필터 초기화 */}
- {(searchQuery || filterType !== "all") && (
-
- )}
-
+ // Area가 없으면 기존 평면 리스트 유지
+ if (areaObjects.length === 0) {
+ return (
+
+ {filteredObjects.map((obj) => {
+ let typeLabel = obj.type;
+ if (obj.type === "location-bed") typeLabel = "베드(BED)";
+ else if (obj.type === "location-stp") typeLabel = "정차포인트(STP)";
+ else if (obj.type === "location-temp") typeLabel = "임시베드(TMP)";
+ else if (obj.type === "location-dest") typeLabel = "지정착지(DES)";
+ else if (obj.type === "crane-mobile") typeLabel = "크레인";
+ else if (obj.type === "area") typeLabel = "Area";
+ else if (obj.type === "rack") typeLabel = "랙";
- {/* 객체 목록 */}
-
-
- {filteredObjects.length === 0 ? (
-
- {searchQuery ? "검색 결과가 없습니다" : "객체가 없습니다"}
-
- ) : (
- (() => {
- // Area 객체가 있는 경우 계층 트리 아코디언 적용
- const areaObjects = filteredObjects.filter((obj) => obj.type === "area");
-
- // Area가 없으면 기존 평면 리스트 유지
- if (areaObjects.length === 0) {
- return (
-
- {filteredObjects.map((obj) => {
- let typeLabel = obj.type;
- if (obj.type === "location-bed") typeLabel = "베드(BED)";
- else if (obj.type === "location-stp") typeLabel = "정차포인트(STP)";
- else if (obj.type === "location-temp") typeLabel = "임시베드(TMP)";
- else if (obj.type === "location-dest") typeLabel = "지정착지(DES)";
- else if (obj.type === "crane-mobile") typeLabel = "크레인";
- else if (obj.type === "area") typeLabel = "Area";
- else if (obj.type === "rack") typeLabel = "랙";
-
- return (
-
handleObjectClick(obj.id)}
- className={`bg-background hover:bg-accent cursor-pointer rounded-lg border p-3 transition-all ${
- selectedObject?.id === obj.id ? "ring-primary bg-primary/5 ring-2" : "hover:shadow-sm"
- }`}
- >
-
-
-
{obj.name}
-
-
-
{typeLabel}
+ return (
+
handleObjectClick(obj.id)}
+ className={`bg-background hover:bg-accent cursor-pointer rounded-lg border p-3 transition-all ${
+ selectedObject?.id === obj.id ? "ring-primary bg-primary/5 ring-2" : "hover:shadow-sm"
+ }`}
+ >
+
+
+
{obj.name}
+
+
+ {typeLabel}
+
+
+ {obj.areaKey && (
+
+ Area: {obj.areaKey}
+
+ )}
+ {obj.locaKey && (
+
+ Location: {obj.locaKey}
+
+ )}
+ {obj.materialCount !== undefined && obj.materialCount > 0 && (
+
+ 자재: {obj.materialCount}개
+
+ )}
+
-
- {obj.areaKey && (
-
- Area: {obj.areaKey}
-
+ );
+ })}
+
+ );
+ }
+
+ // Area가 있는 경우: Area → Location 계층 아코디언
+ return (
+
+ {areaObjects.map((areaObj) => {
+ const childLocations = filteredObjects.filter(
+ (obj) =>
+ obj.type !== "area" &&
+ obj.areaKey === areaObj.areaKey &&
+ (obj.parentId === areaObj.id || obj.externalKey === areaObj.externalKey),
+ );
+
+ return (
+
+
+ {
+ e.stopPropagation();
+ handleObjectClick(areaObj.id);
+ }}
+ >
+
+
+ {areaObj.name}
+
+
+ ({childLocations.length})
+
+
+
+
+
+ {childLocations.length === 0 ? (
+ Location이 없습니다
+ ) : (
+
+ {childLocations.map((locationObj) => (
+
handleObjectClick(locationObj.id)}
+ className={`cursor-pointer rounded-lg border p-2 transition-all ${
+ selectedObject?.id === locationObj.id
+ ? "border-primary bg-primary/10"
+ : "hover:border-primary/50"
+ }`}
+ >
+
+
+ {locationObj.type === "location-stp" ? (
+
+ ) : (
+
+ )}
+
{locationObj.name}
+
+
+
+
+ 위치: ({locationObj.position.x.toFixed(1)}, {locationObj.position.z.toFixed(1)})
+
+ {locationObj.locaKey && (
+
+ Location: {locationObj.locaKey}
+
+ )}
+ {locationObj.materialCount !== undefined && locationObj.materialCount > 0 && (
+
+ 자재: {locationObj.materialCount}개
+
+ )}
+
+ ))}
+
)}
- {obj.locaKey && (
-
- Location: {obj.locaKey}
-
- )}
- {obj.materialCount !== undefined && obj.materialCount > 0 && (
-
- 자재: {obj.materialCount}개
-
- )}
-
-
+
+
);
})}
-
+
);
- }
-
- // Area가 있는 경우: Area → Location 계층 아코디언
- return (
-
- {areaObjects.map((areaObj) => {
- const childLocations = filteredObjects.filter(
- (obj) =>
- obj.type !== "area" &&
- obj.areaKey === areaObj.areaKey &&
- (obj.parentId === areaObj.id || obj.externalKey === areaObj.externalKey),
- );
-
- return (
-
-
- {
- e.stopPropagation();
- handleObjectClick(areaObj.id);
- }}
- >
-
-
- {areaObj.name}
-
-
- ({childLocations.length})
-
-
-
-
-
- {childLocations.length === 0 ? (
- Location이 없습니다
- ) : (
-
- {childLocations.map((locationObj) => (
-
handleObjectClick(locationObj.id)}
- className={`cursor-pointer rounded-lg border p-2 transition-all ${
- selectedObject?.id === locationObj.id
- ? "border-primary bg-primary/10"
- : "hover:border-primary/50"
- }`}
- >
-
-
- {locationObj.type === "location-stp" ? (
-
- ) : (
-
- )}
-
{locationObj.name}
-
-
-
-
- 위치: ({locationObj.position.x.toFixed(1)}, {locationObj.position.z.toFixed(1)})
-
- {locationObj.locaKey && (
-
- Location: {locationObj.locaKey}
-
- )}
- {locationObj.materialCount !== undefined && locationObj.materialCount > 0 && (
-
- 자재: {locationObj.materialCount}개
-
- )}
-
- ))}
-
- )}
-
-
- );
- })}
-
- );
- })()
- )}
+ })()
+ )}
+
-
)}
{/* 중앙 + 우측 컨테이너 (전체화면 시 함께 표시) */}
-
@@ -691,107 +683,102 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
{/* 우측: 정보 패널 */}
- {selectedObject ? (
-
-
-
상세 정보
-
{selectedObject.name}
-
-
- {/* 기본 정보 */}
-
-
-
-
{selectedObject.type}
+ {selectedObject ? (
+
+
+
상세 정보
+
{selectedObject.name}
- {selectedObject.areaKey && (
-
-
-
{selectedObject.areaKey}
-
- )}
- {selectedObject.locaKey && (
-
-
-
{selectedObject.locaKey}
-
- )}
- {selectedObject.materialCount !== undefined && selectedObject.materialCount > 0 && (
-
-
-
{selectedObject.materialCount}개
-
- )}
-
- {/* 자재 목록 (Location인 경우) - 테이블 형태 */}
- {(selectedObject.type === "location-bed" ||
- selectedObject.type === "location-stp" ||
- selectedObject.type === "location-temp" ||
- selectedObject.type === "location-dest") && (
-
- {loadingMaterials ? (
-
-
+ {/* 기본 정보 */}
+
+
+
+
{selectedObject.type}
+
+ {selectedObject.areaKey && (
+
+
+
{selectedObject.areaKey}
- ) : materials.length === 0 ? (
-
- {externalDbConnectionId ? "자재가 없습니다" : "외부 DB 연결이 설정되지 않았습니다"}
+ )}
+ {selectedObject.locaKey && (
+
+
+
{selectedObject.locaKey}
- ) : (
-
-
- {/* 테이블 형태로 전체 조회 */}
-
-
-
-
- | 층 |
- {(hierarchyConfig?.material?.displayColumns || []).map((colConfig: any) => (
-
- {colConfig.label}
- |
- ))}
-
-
-
- {materials.map((material, index) => {
- const layerColumn = hierarchyConfig?.material?.layerColumn || "LOLAYER";
- const displayColumns = hierarchyConfig?.material?.displayColumns || [];
- return (
-
- |
- {material[layerColumn]}단
- |
- {displayColumns.map((colConfig: any) => (
-
- {material[colConfig.column] || "-"}
- |
- ))}
-
- );
- })}
-
-
-
+ )}
+ {selectedObject.materialCount !== undefined && selectedObject.materialCount > 0 && (
+
+
+
{selectedObject.materialCount}개
)}
- )}
-
- ) : (
-
- )}
+
+ {/* 자재 목록 (Location인 경우) - 테이블 형태 */}
+ {(selectedObject.type === "location-bed" ||
+ selectedObject.type === "location-stp" ||
+ selectedObject.type === "location-temp" ||
+ selectedObject.type === "location-dest") && (
+
+ {loadingMaterials ? (
+
+
+
+ ) : materials.length === 0 ? (
+
+ {externalDbConnectionId ? "자재가 없습니다" : "외부 DB 연결이 설정되지 않았습니다"}
+
+ ) : (
+
+
+ {/* 테이블 형태로 전체 조회 */}
+
+
+
+
+ | 층 |
+ {(hierarchyConfig?.material?.displayColumns || []).map((colConfig: any) => (
+
+ {colConfig.label}
+ |
+ ))}
+
+
+
+ {materials.map((material, index) => {
+ const layerColumn = hierarchyConfig?.material?.layerColumn || "LOLAYER";
+ const displayColumns = hierarchyConfig?.material?.displayColumns || [];
+ return (
+
+ |
+ {material[layerColumn]}단
+ |
+ {displayColumns.map((colConfig: any) => (
+
+ {material[colConfig.column] || "-"}
+ |
+ ))}
+
+ );
+ })}
+
+
+
+
+ )}
+
+ )}
+
+ ) : (
+
+ )}
{/* 풀스크린 모드일 때 종료 버튼 */}
@@ -800,7 +787,7 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
variant="outline"
size="sm"
onClick={toggleFullscreen}
- className="absolute top-4 right-4 z-50 bg-background/80 backdrop-blur-sm"
+ className="bg-background/80 absolute top-4 right-4 z-50 backdrop-blur-sm"
>
종료