디지털 3d 뷰어 10초단위 갱신 구현
This commit is contained in:
parent
2e7a215066
commit
69754a31cb
|
|
@ -51,6 +51,7 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
|
|||
const [isExternalMode, setIsExternalMode] = useState(false);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [layoutKey, setLayoutKey] = useState(0); // 레이아웃 강제 리렌더링용
|
||||
const [lastRefreshedAt, setLastRefreshedAt] = useState<Date | null>(null); // 마지막 갱신 시간
|
||||
const canvasContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 외부 업체 역할 체크
|
||||
|
|
@ -214,6 +215,8 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
|
|||
}),
|
||||
);
|
||||
}
|
||||
// 마지막 갱신 시간 기록
|
||||
setLastRefreshedAt(new Date());
|
||||
} else {
|
||||
throw new Error(response.error || "레이아웃 조회 실패");
|
||||
}
|
||||
|
|
@ -250,6 +253,155 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [layoutId]);
|
||||
|
||||
// 10초 주기 자동 갱신 (중앙 관제 화면 자동 새로고침)
|
||||
useEffect(() => {
|
||||
const AUTO_REFRESH_INTERVAL = 10000; // 10초
|
||||
|
||||
const silentRefresh = async () => {
|
||||
// 로딩 중이거나 새로고침 중이면 스킵
|
||||
if (isLoading || isRefreshing) return;
|
||||
|
||||
try {
|
||||
// 레이아웃 데이터 조용히 갱신
|
||||
const response = await getLayoutById(layoutId);
|
||||
|
||||
if (response.success && response.data) {
|
||||
const { layout, objects } = response.data;
|
||||
const dbConnectionId = layout.external_db_connection_id || layout.externalDbConnectionId;
|
||||
|
||||
// hierarchy_config 파싱
|
||||
let hierarchyConfigData: any = null;
|
||||
if (layout.hierarchy_config) {
|
||||
hierarchyConfigData =
|
||||
typeof layout.hierarchy_config === "string"
|
||||
? JSON.parse(layout.hierarchy_config)
|
||||
: layout.hierarchy_config;
|
||||
setHierarchyConfig(hierarchyConfigData);
|
||||
}
|
||||
|
||||
// 객체 데이터 변환
|
||||
const loadedObjects: PlacedObject[] = objects.map((obj: any) => {
|
||||
const objectType = obj.object_type;
|
||||
return {
|
||||
id: obj.id,
|
||||
type: objectType,
|
||||
name: obj.object_name,
|
||||
position: {
|
||||
x: parseFloat(obj.position_x),
|
||||
y: parseFloat(obj.position_y),
|
||||
z: parseFloat(obj.position_z),
|
||||
},
|
||||
size: {
|
||||
x: parseFloat(obj.size_x),
|
||||
y: parseFloat(obj.size_y),
|
||||
z: parseFloat(obj.size_z),
|
||||
},
|
||||
rotation: obj.rotation ? parseFloat(obj.rotation) : 0,
|
||||
color: getObjectColor(objectType, obj.color),
|
||||
areaKey: obj.area_key,
|
||||
locaKey: obj.loca_key,
|
||||
locType: obj.loc_type,
|
||||
materialCount: obj.loc_type === "STP" ? undefined : obj.material_count,
|
||||
materialPreview:
|
||||
obj.loc_type === "STP" || !obj.material_preview_height
|
||||
? undefined
|
||||
: { height: parseFloat(obj.material_preview_height) },
|
||||
parentId: obj.parent_id,
|
||||
displayOrder: obj.display_order,
|
||||
locked: obj.locked,
|
||||
visible: obj.visible !== false,
|
||||
hierarchyLevel: obj.hierarchy_level,
|
||||
parentKey: obj.parent_key,
|
||||
externalKey: obj.external_key,
|
||||
};
|
||||
});
|
||||
|
||||
// 외부 DB 연결이 있고 자재 설정이 있으면, 각 Location의 실제 자재 개수 조회
|
||||
if (dbConnectionId && hierarchyConfigData?.material) {
|
||||
const locationObjects = loadedObjects.filter(
|
||||
(obj) =>
|
||||
(obj.type === "location-bed" || obj.type === "location-temp" || obj.type === "location-dest") &&
|
||||
obj.locaKey,
|
||||
);
|
||||
|
||||
// 각 Location에 대해 자재 개수 조회 (병렬 처리)
|
||||
const materialCountPromises = locationObjects.map(async (obj) => {
|
||||
try {
|
||||
const matResponse = await getMaterials(dbConnectionId, {
|
||||
tableName: hierarchyConfigData.material.tableName,
|
||||
keyColumn: hierarchyConfigData.material.keyColumn,
|
||||
locationKeyColumn: hierarchyConfigData.material.locationKeyColumn,
|
||||
layerColumn: hierarchyConfigData.material.layerColumn,
|
||||
locaKey: obj.locaKey!,
|
||||
});
|
||||
if (matResponse.success && matResponse.data) {
|
||||
return { id: obj.id, count: matResponse.data.length };
|
||||
}
|
||||
} catch {
|
||||
// 자동 갱신 시에는 에러 로그 생략
|
||||
}
|
||||
return { id: obj.id, count: 0 };
|
||||
});
|
||||
|
||||
const materialCounts = await Promise.all(materialCountPromises);
|
||||
|
||||
// materialCount 업데이트
|
||||
const updatedObjects = loadedObjects.map((obj) => {
|
||||
const countData = materialCounts.find((m) => m.id === obj.id);
|
||||
if (countData && countData.count > 0) {
|
||||
return { ...obj, materialCount: countData.count };
|
||||
}
|
||||
return obj;
|
||||
});
|
||||
|
||||
setPlacedObjects(updatedObjects);
|
||||
} else {
|
||||
setPlacedObjects(loadedObjects);
|
||||
}
|
||||
|
||||
// 선택된 객체가 있으면 자재 목록도 갱신
|
||||
if (selectedObject && dbConnectionId && hierarchyConfigData?.material) {
|
||||
const currentObj = loadedObjects.find((o) => o.id === selectedObject.id);
|
||||
if (
|
||||
currentObj &&
|
||||
(currentObj.type === "location-bed" ||
|
||||
currentObj.type === "location-temp" ||
|
||||
currentObj.type === "location-dest") &&
|
||||
currentObj.locaKey
|
||||
) {
|
||||
const matResponse = await getMaterials(dbConnectionId, {
|
||||
tableName: hierarchyConfigData.material.tableName,
|
||||
keyColumn: hierarchyConfigData.material.keyColumn,
|
||||
locationKeyColumn: hierarchyConfigData.material.locationKeyColumn,
|
||||
layerColumn: hierarchyConfigData.material.layerColumn,
|
||||
locaKey: currentObj.locaKey,
|
||||
});
|
||||
if (matResponse.success && matResponse.data) {
|
||||
const layerColumn = hierarchyConfigData.material.layerColumn || "LOLAYER";
|
||||
const sortedMaterials = matResponse.data.sort(
|
||||
(a: any, b: any) => (b[layerColumn] || 0) - (a[layerColumn] || 0),
|
||||
);
|
||||
setMaterials(sortedMaterials);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 마지막 갱신 시간 기록
|
||||
setLastRefreshedAt(new Date());
|
||||
}
|
||||
} catch {
|
||||
// 자동 갱신 실패 시 조용히 무시 (사용자 경험 방해 안 함)
|
||||
}
|
||||
};
|
||||
|
||||
// 10초마다 자동 갱신
|
||||
const intervalId = setInterval(silentRefresh, AUTO_REFRESH_INTERVAL);
|
||||
|
||||
// 컴포넌트 언마운트 시 인터벌 정리
|
||||
return () => clearInterval(intervalId);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [layoutId, isLoading, isRefreshing, selectedObject]);
|
||||
|
||||
// Location의 자재 목록 로드
|
||||
const loadMaterialsForLocation = async (locaKey: string, externalDbConnectionId: number) => {
|
||||
if (!hierarchyConfig?.material) {
|
||||
|
|
@ -405,7 +557,14 @@ 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>
|
||||
<div className="flex items-center gap-3">
|
||||
<p className="text-muted-foreground text-sm">{isExternalMode ? "야드 관제 화면" : "읽기 전용 뷰"}</p>
|
||||
{lastRefreshedAt && (
|
||||
<span className="text-muted-foreground text-xs">
|
||||
마지막 갱신: {lastRefreshedAt.toLocaleTimeString("ko-KR")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 전체 화면 버튼 - 외부 업체 모드에서만 표시 */}
|
||||
|
|
|
|||
Loading…
Reference in New Issue