디지털 3d 뷰어 10초단위 갱신 구현

This commit is contained in:
dohyeons 2025-12-19 14:12:05 +09:00
parent 2e7a215066
commit 69754a31cb
1 changed files with 160 additions and 1 deletions

View File

@ -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">
{/* 전체 화면 버튼 - 외부 업체 모드에서만 표시 */} {/* 전체 화면 버튼 - 외부 업체 모드에서만 표시 */}