디지털 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 [isExternalMode, setIsExternalMode] = useState(false);
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
const [layoutKey, setLayoutKey] = useState(0); // 레이아웃 강제 리렌더링용
|
const [layoutKey, setLayoutKey] = useState(0); // 레이아웃 강제 리렌더링용
|
||||||
|
const [lastRefreshedAt, setLastRefreshedAt] = useState<Date | null>(null); // 마지막 갱신 시간
|
||||||
const canvasContainerRef = useRef<HTMLDivElement>(null);
|
const canvasContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// 외부 업체 역할 체크
|
// 외부 업체 역할 체크
|
||||||
|
|
@ -214,6 +215,8 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// 마지막 갱신 시간 기록
|
||||||
|
setLastRefreshedAt(new Date());
|
||||||
} else {
|
} else {
|
||||||
throw new Error(response.error || "레이아웃 조회 실패");
|
throw new Error(response.error || "레이아웃 조회 실패");
|
||||||
}
|
}
|
||||||
|
|
@ -250,6 +253,155 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [layoutId]);
|
}, [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의 자재 목록 로드
|
// Location의 자재 목록 로드
|
||||||
const loadMaterialsForLocation = async (locaKey: string, externalDbConnectionId: number) => {
|
const loadMaterialsForLocation = async (locaKey: string, externalDbConnectionId: number) => {
|
||||||
if (!hierarchyConfig?.material) {
|
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 className="flex items-center justify-between border-b p-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold">{layoutName || "디지털 트윈 야드"}</h2>
|
<h2 className="text-lg font-semibold">{layoutName || "디지털 트윈 야드"}</h2>
|
||||||
<p className="text-muted-foreground text-sm">{isExternalMode ? "야드 관제 화면" : "읽기 전용 뷰"}</p>
|
<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>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{/* 전체 화면 버튼 - 외부 업체 모드에서만 표시 */}
|
{/* 전체 화면 버튼 - 외부 업체 모드에서만 표시 */}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue