diff --git a/src/lms-renderer/App.tsx b/src/lms-renderer/App.tsx index 5f6f958..b8b8f91 100644 --- a/src/lms-renderer/App.tsx +++ b/src/lms-renderer/App.tsx @@ -84,6 +84,11 @@ type IdeActivity = WorkspaceActivity const PUBLIC_GUIDE_COURSE_SLUG = 'autonomy-studio-lms-guide' +function isRemoteEnvironmentProfile(profile: EnvironmentProfile): boolean { + const target = profile.config?.target + return target === 'remote-jetson' || target === 'platform' +} + function resolveModuleRuntimeImages( profiles: EnvironmentProfile[], remoteWorkspace: boolean, @@ -92,7 +97,7 @@ function resolveModuleRuntimeImages( if (!remoteWorkspace) return activeRuntimeImages const images = profiles - .filter((profile) => ['real_slam', 'real_slam_3d', 'real_navigation'].includes(profile.profileType.toLowerCase())) + .filter(isRemoteEnvironmentProfile) .flatMap((profile) => Array.isArray(profile.config.runtimeImages) ? profile.config.runtimeImages : []) .filter((image): image is RuntimeImageArtifact => ( typeof image === 'object' @@ -740,7 +745,7 @@ function LocalPcWorkspaceMockup({ const runtimeModules = useMemo(() => resolveRuntimeModules( curriculumWeek.modules, remoteWorkspace - ? visualizationProfiles.filter((profile) => ['real_slam', 'real_slam_3d', 'real_navigation'].includes(profile.profileType.toLowerCase())) + ? visualizationProfiles.filter(isRemoteEnvironmentProfile) : activeEnvironmentProfile, remoteWorkspace ? 'platform' : 'local' ), [activeEnvironmentProfile, curriculumWeek.modules, remoteWorkspace, visualizationProfiles]) diff --git a/src/lms-renderer/features/workspace/useWorkspaceModuleList.ts b/src/lms-renderer/features/workspace/useWorkspaceModuleList.ts index 576d9c3..b1c6962 100644 --- a/src/lms-renderer/features/workspace/useWorkspaceModuleList.ts +++ b/src/lms-renderer/features/workspace/useWorkspaceModuleList.ts @@ -50,7 +50,9 @@ export function useWorkspaceModuleList({ const [modulesLoading, setModulesLoading] = useState(false) const [moduleListError, setModuleListError] = useState(null) const remoteTarget = useMemo(() => parseRemoteWorkspaceTarget(remoteWorkspaceTargetKey), [remoteWorkspaceTargetKey]) + const moduleInputsRef = useRef({ curriculumModules, applyModuleParameterOverrides }) const loadSequenceRef = useRef(0) + const moduleLoadInFlightRef = useRef(false) const pendingModuleStatesRef = useRef(new Map()) const moduleStateKey = (module: ModuleRow): string => module.folderName ?? module.name @@ -89,9 +91,13 @@ export function useWorkspaceModuleList({ })) }, []) + useEffect(() => { + moduleInputsRef.current = { curriculumModules, applyModuleParameterOverrides } + }, [applyModuleParameterOverrides, curriculumModuleKey, curriculumModules]) + const loadRemoteModules = useCallback(async (): Promise<{ modules: ModuleRow[] }> => { - if (!remoteTarget) return { modules: [] } - const result = await window.remoteWorkspace.modules.list(remoteTarget, curriculumModules ?? []) + if (!remoteTarget) throw new Error('원격 플랫폼 SSH 주소를 확인하지 못했습니다.') + const result = await window.remoteWorkspace.modules.list(remoteTarget, moduleInputsRef.current.curriculumModules ?? []) return { modules: result.modules.map((module) => { const rawModule = module as ModuleRow & { defaultParameters?: unknown } @@ -106,38 +112,42 @@ export function useWorkspaceModuleList({ }, [curriculumModuleKey, remoteTarget]) const loadAvailableModules = useCallback(async ({ silent = false }: LoadAvailableModulesOptions = {}): Promise => { + // Remote inventory probes open SSH sessions. Keep one request in flight so + // polling cannot continuously replace a slow response with a newer one. + if (moduleLoadInFlightRef.current) return + moduleLoadInFlightRef.current = true const requestSequence = ++loadSequenceRef.current if (!silent) setModulesLoading(true) try { + const { curriculumModules: currentCurriculumModules, applyModuleParameterOverrides: applyOverrides } = moduleInputsRef.current if (remoteWorkspace) { const result = await loadRemoteModules() if (requestSequence !== loadSequenceRef.current) return setModuleListError(null) - setAvailableModules(applyPendingModuleStates(applyModuleParameterOverrides(result.modules).sort(compareModulePriority))) + setAvailableModules(applyPendingModuleStates(applyOverrides(result.modules).sort(compareModulePriority))) return } - const result = await window.localWorkspace.modules.list(clonePathPreview, curriculumModules ?? []) + const result = await window.localWorkspace.modules.list(clonePathPreview, currentCurriculumModules ?? []) if (requestSequence !== loadSequenceRef.current) return setModuleListError(null) - setAvailableModules(applyPendingModuleStates(applyModuleParameterOverrides(result.modules).sort(compareModulePriority))) + setAvailableModules(applyPendingModuleStates(applyOverrides(result.modules).sort(compareModulePriority))) } catch (error) { if (requestSequence !== loadSequenceRef.current) return console.error('[modules] list failed', error) const message = error instanceof Error ? error.message : '모듈 목록을 불러오지 못했습니다.' if (!silent) { setModuleListError(message) - setAvailableModules(makeUninstalledModuleRows(curriculumModules)) + setAvailableModules(makeUninstalledModuleRows(moduleInputsRef.current.curriculumModules)) } } finally { + moduleLoadInFlightRef.current = false if (!silent && requestSequence === loadSequenceRef.current) setModulesLoading(false) } }, [ - applyModuleParameterOverrides, applyPendingModuleStates, clonePathPreview, curriculumModuleKey, - curriculumModules, loadRemoteModules, remoteWorkspace ]) @@ -151,10 +161,10 @@ export function useWorkspaceModuleList({ const intervalId = window.setInterval(() => { void loadAvailableModules({ silent: true }) - }, 5000) + }, remoteWorkspace ? 10_000 : 5_000) return () => window.clearInterval(intervalId) - }, [loadAvailableModules, pollingActive]) + }, [loadAvailableModules, pollingActive, remoteWorkspace]) return { availableModules, diff --git a/src/lms-renderer/features/workspace/useWorkspaceVisualization.ts b/src/lms-renderer/features/workspace/useWorkspaceVisualization.ts index 935cf50..93627e9 100644 --- a/src/lms-renderer/features/workspace/useWorkspaceVisualization.ts +++ b/src/lms-renderer/features/workspace/useWorkspaceVisualization.ts @@ -36,14 +36,14 @@ function resolveNav2Mode(profile: EnvironmentProfile | null): Nav2VisualizationM return profile?.profileType.toLowerCase() === 'real_navigation' ? 'navigation' : 'slam' } -function isRealPlatformProfile(profile: EnvironmentProfile | null): boolean { - const type = profile?.profileType.toLowerCase() - return type === 'real_slam' || type === 'real_navigation' +function isRemoteEnvironmentProfile(profile: EnvironmentProfile | null): boolean { + const target = profile?.config?.target + return target === 'remote-jetson' || target === 'platform' } function selectRuntimeProfiles(profiles: EnvironmentProfile[], remoteWorkspace: boolean): EnvironmentProfile[] { const matchingProfiles = profiles.filter((profile) => ( - remoteWorkspace ? isRealPlatformProfile(profile) : !isRealPlatformProfile(profile) + remoteWorkspace ? isRemoteEnvironmentProfile(profile) : !isRemoteEnvironmentProfile(profile) )) return matchingProfiles.length ? matchingProfiles : profiles } @@ -121,7 +121,7 @@ export function useWorkspaceVisualization({ const activeEnvironmentAgentUrl = typeof activeEnvironmentAgent?.url === 'string' ? activeEnvironmentAgent.url.replace(/\/+$/, '') : '' - const visualizationRemote = activeEnvironmentConfig.target === 'remote-jetson' && activeEnvironmentAgentUrl + const visualizationRemote = isRemoteEnvironmentProfile(activeEnvironmentProfile) && activeEnvironmentAgentUrl const visualizationRemoteBridgeHttpUrl = (() => { if (!visualizationRemote) return '' try { @@ -138,7 +138,7 @@ export function useWorkspaceVisualization({ : 'ws://localhost:8765' const visualizationBackendUrl = visualizationBridgeHttpUrl const visualizationViewerUrl = visualizationStack === 'slam' || visualizationStack === 'nav2' - ? `/slam-viewer/index.html?directBridge=1&mode=${nav2VisualizationMode}&runtime=${isRealPlatformProfile(activeEnvironmentProfile) ? 'real' : 'simulation'}&bridgeWs=${encodeURIComponent(`${visualizationBridgeUrl}/ws/state`)}&bridgeHttp=${encodeURIComponent(visualizationBridgeHttpUrl)}&mapKey=${encodeURIComponent(mapKey ?? 'nav2-turtlebot3-world')}&layoutVersion=${worldLayoutVersion ?? 0}` + ? `/slam-viewer/index.html?directBridge=1&mode=${nav2VisualizationMode}&runtime=${isRemoteEnvironmentProfile(activeEnvironmentProfile) ? 'real' : 'simulation'}&bridgeWs=${encodeURIComponent(`${visualizationBridgeUrl}/ws/state`)}&bridgeHttp=${encodeURIComponent(visualizationBridgeHttpUrl)}&mapKey=${encodeURIComponent(mapKey ?? 'nav2-turtlebot3-world')}&layoutVersion=${worldLayoutVersion ?? 0}` : `/autoware-viewer/index.html?source=ros&stack=${visualizationStack}&bridge=${encodeURIComponent(visualizationBridgeUrl)}&backend=${encodeURIComponent(visualizationBackendUrl)}` useEffect(() => {