From 2e7a21506609c5ebe3cc9e6f112037d0a901f301 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Fri, 19 Dec 2025 14:00:38 +0900 Subject: [PATCH] =?UTF-8?q?=EC=98=A4=EB=A5=B8=EC=AA=BD=20=EA=B7=B8?= =?UTF-8?q?=EB=A6=AC=EB=93=9C=20=ED=81=AC=EA=B8=B0=20=EC=A1=B0=EC=A0=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../widgets/yard-3d/DigitalTwinViewer.tsx | 641 +++++++++--------- 1 file changed, 314 insertions(+), 327 deletions(-) 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) => ( - - ))} - - - - {materials.map((material, index) => { - const layerColumn = hierarchyConfig?.material?.layerColumn || "LOLAYER"; - const displayColumns = hierarchyConfig?.material?.displayColumns || []; - return ( - - - {displayColumns.map((colConfig: any) => ( - - ))} - - ); - })} - -
- {colConfig.label} -
- {material[layerColumn]}단 - - {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) => ( + + ))} + + + + {materials.map((material, index) => { + const layerColumn = hierarchyConfig?.material?.layerColumn || "LOLAYER"; + const displayColumns = hierarchyConfig?.material?.displayColumns || []; + return ( + + + {displayColumns.map((colConfig: any) => ( + + ))} + + ); + })} + +
+ {colConfig.label} +
+ {material[layerColumn]}단 + + {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" > 종료