Compare commits

..

3 Commits

7 changed files with 188 additions and 55 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "lms", "name": "lms",
"version": "1.2.7", "version": "1.2.8",
"description": "Autonomy Studio LMS Electron app", "description": "Autonomy Studio LMS Electron app",
"main": "./out/main/index.js", "main": "./out/main/index.js",
"author": { "author": {
+13 -42
View File
@@ -4231,21 +4231,20 @@ async function installRemoteWorkspaceModule(
`STUDENT_REPO="$(printf %s ${quoteShellSingle(encodeBase64(studentRepositoryWithCredential))} | base64 -d)"`, `STUDENT_REPO="$(printf %s ${quoteShellSingle(encodeBase64(studentRepositoryWithCredential))} | base64 -d)"`,
`PREBUILT_JSON="$(printf %s ${quoteShellSingle(encodeBase64(prebuiltJson))} | base64 -d)"`, `PREBUILT_JSON="$(printf %s ${quoteShellSingle(encodeBase64(prebuiltJson))} | base64 -d)"`,
'mkdir -p "$ROOT"', 'mkdir -p "$ROOT"',
'if [ ! -d "$TARGET/.git" ]; then git clone "$TEMPLATE_REPO" "$TARGET"; fi', // An existing workspace owns its source, commits and build artifacts. Install
'git -C "$TARGET" remote set-url origin "$TEMPLATE_REPO"', // retries (including changed repository credentials) must not overwrite it.
'git -C "$TARGET" fetch origin --prune', 'if [ -e "$TARGET/.git" ]; then',
'CURRENT_STUDENT="$(git -C "$TARGET" remote get-url student 2>/dev/null || true)"', ' printf "OK\\t%s\\n" "$TARGET"',
'if [ "$CURRENT_STUDENT" != "$STUDENT_REPO" ]; then', ' exit 0',
' git -C "$TARGET" checkout -B main origin/main', 'fi',
' git -C "$TARGET" reset --hard origin/main', 'git clone "$TEMPLATE_REPO" "$TARGET"',
' git -C "$TARGET" clean -fdx', 'if [ -n "$STUDENT_REPO" ]; then',
' git -C "$TARGET" remote remove student >/dev/null 2>&1 || true',
' git -C "$TARGET" remote add student "$STUDENT_REPO"', ' git -C "$TARGET" remote add student "$STUDENT_REPO"',
' git -C "$TARGET" fetch student --prune || true', ' git -C "$TARGET" fetch student --prune',
' if git -C "$TARGET" show-ref --verify --quiet refs/remotes/student/main; then', ' if git -C "$TARGET" show-ref --verify --quiet refs/remotes/student/main; then',
' git -C "$TARGET" checkout -B student-work student/main', ' git -C "$TARGET" checkout -b student-work student/main',
' else', ' else',
' git -C "$TARGET" checkout -B student-work origin/main', ' git -C "$TARGET" checkout -b student-work origin/main',
' git -C "$TARGET" push student student-work:main', ' git -C "$TARGET" push student student-work:main',
' fi', ' fi',
'fi', 'fi',
@@ -5732,36 +5731,8 @@ async function resetRemoteWorkspaceSession(
targetInput: RemoteWorkspaceTarget, targetInput: RemoteWorkspaceTarget,
modules: Array<{ moduleName: string; folderName?: string | null; templateRepositoryUrl?: string | null }>, modules: Array<{ moduleName: string; folderName?: string | null; templateRepositoryUrl?: string | null }>,
): Promise<{ ok: boolean; message: string }> { ): Promise<{ ok: boolean; message: string }> {
try { // Keep the legacy IPC contract without resetting student files or Git history.
const target = normalizeRemoteTarget(targetInput) return stopRemoteWorkspaceModules(targetInput, modules)
await stopRemoteWorkspaceModules(target, modules).catch(() => ({ ok: false, message: '모듈 중지 실패' }))
const commands = [
'set -e',
`ROOT=${quoteShellSingle(target.workspaceRoot)}`,
...expandRemotePathVar('ROOT'),
]
for (const module of modules) {
const folderName = module.folderName?.trim()
const templateRepositoryUrl = module.templateRepositoryUrl?.trim()
if (!folderName || !templateRepositoryUrl) continue
const templateRepositoryWithCredential = withGitCredential(remoteRepositoryUrl(templateRepositoryUrl))
commands.push(
`MODULE="$ROOT/${folderName}"`,
'if [ -d "$MODULE/.git" ]; then',
' git -C "$MODULE" remote remove student >/dev/null 2>&1 || true',
` git -C "$MODULE" remote set-url origin ${quoteShellSingle(templateRepositoryWithCredential)}`,
' git -C "$MODULE" fetch origin --prune',
' git -C "$MODULE" checkout -B main origin/main',
' git -C "$MODULE" reset --hard origin/main',
' git -C "$MODULE" clean -fdx',
'fi',
)
}
await runSsh(target, commands.join('\n'), 180000)
return { ok: true, message: '템플릿 기준으로 초기화했습니다.' }
} catch (error) {
return { ok: false, message: error instanceof Error ? error.message : String(error) }
}
} }
async function restartWorkspaceModule( async function restartWorkspaceModule(
@@ -0,0 +1,116 @@
import { strict as assert } from 'node:assert'
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { spawnSync } from 'node:child_process'
import { test } from 'node:test'
import ts from 'typescript'
const source = ts.createSourceFile('index.ts', readFileSync(new URL('./index.ts', import.meta.url), 'utf8'), ts.ScriptTarget.Latest, true)
function loadFunction(name: string, dependencies: Record<string, unknown>) {
const declaration = source.statements.find((item) => ts.isFunctionDeclaration(item) && item.name?.text === name)
assert.ok(declaration)
const code = ts.transpile(declaration.getText(source), { target: ts.ScriptTarget.ES2022 })
return new Function(...Object.keys(dependencies), `${code}; return ${name};`)(...Object.values(dependencies))
}
function command(executable: string, args: string[], cwd: string) {
const result = spawnSync(executable, args, {
cwd, encoding: 'utf8', timeout: 15000,
env: { ...process.env, GIT_AUTHOR_NAME: 'Test', GIT_AUTHOR_EMAIL: 'test@example.com', GIT_COMMITTER_NAME: 'Test', GIT_COMMITTER_EMAIL: 'test@example.com' },
})
assert.equal(result.status, 0, result.stderr || String(result.error))
return result.stdout.trim()
}
function fixture() {
const root = mkdtempSync(join(tmpdir(), 'lms-preserve-'))
const template = join(root, 'template')
mkdirSync(template)
command('git', ['init', '-b', 'main'], template)
writeFileSync(join(template, 'node.py'), 'template source\n')
writeFileSync(join(template, '.gitignore'), 'build/\ninstall/\nlog/\n')
command('git', ['add', '.'], template)
command('git', ['commit', '-m', 'template'], template)
const install = loadFunction('installRemoteWorkspaceModule', {
normalizeRemoteTarget: (target: unknown) => target,
remoteRepositoryUrl: (url: string) => url,
withGitCredential: (url: string) => url,
remotePathJoin: join,
safeArtifactFileName: () => 'unused.tar',
quoteShellSingle: (value: string) => "'" + value.replaceAll("'", "'\\''") + "'",
encodeBase64: (value: string) => Buffer.from(value).toString('base64'),
expandRemotePathVar: () => [],
runSsh: async (_target: unknown, script: string) => command('bash', ['-c', script], root),
parseModuleFolderName: () => ({ name: 'Test', category: 'Control' }),
})
const target = { workspaceRoot: join(root, 'workspace') }
return { root, template, target, install, modulePath: join(target.workspaceRoot, 'module') }
}
test('reinstall preserves saved source, local commits, untracked files and build outputs despite changed repository URLs', async () => {
const f = fixture()
try {
await f.install(f.target, 'Test', 'module', f.template)
writeFileSync(join(f.modulePath, 'node.py'), 'student commit\n')
command('git', ['add', 'node.py'], f.modulePath)
command('git', ['commit', '-m', 'student work'], f.modulePath)
const head = command('git', ['rev-parse', 'HEAD'], f.modulePath)
writeFileSync(join(f.modulePath, 'node.py'), 'saved but not committed\n')
writeFileSync(join(f.modulePath, 'new.py'), 'new student file\n')
for (const dir of ['build', 'install', 'log']) {
mkdirSync(join(f.modulePath, dir))
writeFileSync(join(f.modulePath, dir, 'keep'), dir)
}
const status = command('git', ['status', '--porcelain'], f.modulePath)
await f.install(f.target, 'Test', 'module', '/missing/new-template', '/missing/new-student', {
prebuiltArtifacts: [{ url: 'https://invalid.example/overwrite.tar', required: true }],
})
assert.equal(command('git', ['rev-parse', 'HEAD'], f.modulePath), head)
assert.equal(command('git', ['status', '--porcelain'], f.modulePath), status)
assert.equal(readFileSync(join(f.modulePath, 'node.py'), 'utf8'), 'saved but not committed\n')
assert.equal(readFileSync(join(f.modulePath, 'new.py'), 'utf8'), 'new student file\n')
for (const dir of ['build', 'install', 'log']) assert.equal(readFileSync(join(f.modulePath, dir, 'keep'), 'utf8'), dir)
} finally { rmSync(f.root, { recursive: true, force: true }) }
})
test('first install restores the student repository instead of replacing it with template source', async () => {
const f = fixture()
try {
const student = join(f.root, 'student')
command('git', ['clone', f.template, student], f.root)
writeFileSync(join(student, 'node.py'), 'submitted student work\n')
command('git', ['add', '.'], student)
command('git', ['commit', '-m', 'submission'], student)
await f.install(f.target, 'Test', 'module', f.template, student)
assert.equal(readFileSync(join(f.modulePath, 'node.py'), 'utf8'), 'submitted student work\n')
assert.equal(command('git', ['branch', '--show-current'], f.modulePath), 'student-work')
} finally { rmSync(f.root, { recursive: true, force: true }) }
})
test('first install initializes an empty student repository', async () => {
const f = fixture()
try {
const student = join(f.root, 'student.git')
command('git', ['init', '--bare', student], f.root)
await f.install(f.target, 'Test', 'module', f.template, student)
assert.equal(command('git', ['--git-dir', student, 'show', 'main:node.py'], f.root), 'template source')
} finally { rmSync(f.root, { recursive: true, force: true }) }
})
test('legacy session reset only stops modules and returns stop failures', async () => {
const target = { workspaceRoot: '/student/workspace' }
const modules = [{ moduleName: 'Test', folderName: 'module', templateRepositoryUrl: '/template' }]
const result = { ok: false, message: 'stop failed' }
let calls = 0
const reset = loadFunction('resetRemoteWorkspaceSession', {
stopRemoteWorkspaceModules: async (actualTarget: unknown, actualModules: unknown) => {
calls++
assert.equal(actualTarget, target)
assert.equal(actualModules, modules)
return result
},
})
assert.equal(await reset(target, modules), result)
assert.equal(calls, 1)
})
+51 -9
View File
@@ -49,7 +49,7 @@
#cameraPreviewWrap { #cameraPreviewWrap {
position:absolute; position:absolute;
left:16px; left:16px;
top:16px; top:60px;
z-index:10; z-index:10;
display:none; display:none;
width:min(260px, calc(100vw - 32px)); width:min(260px, calc(100vw - 32px));
@@ -58,6 +58,7 @@
box-shadow:0 10px 26px rgba(0,0,0,.16); box-shadow:0 10px 26px rgba(0,0,0,.16);
} }
#cameraPreviewWrap.is-visible { display:block; } #cameraPreviewWrap.is-visible { display:block; }
#cameraToggle { display:grid; place-items:center; width:34px; flex:0 0 34px; padding:0; }
#cameraPreviewWrap.is-collapsed { background:transparent; border-color:transparent; box-shadow:none; } #cameraPreviewWrap.is-collapsed { background:transparent; border-color:transparent; box-shadow:none; }
#cameraPreview { #cameraPreview {
display:block; display:block;
@@ -96,6 +97,9 @@
display:flex; display:flex;
gap:7px; gap:7px;
align-items:flex-start; align-items:flex-start;
max-width:calc(100% - 32px);
flex-wrap:wrap;
justify-content:flex-end;
} }
#topLeftControls { #topLeftControls {
position:absolute; position:absolute;
@@ -241,6 +245,7 @@
#downloadMap, #downloadMap,
#pathToggle, #pathToggle,
#sensorToggle, #sensorToggle,
#cameraToggle,
#poseToggle, #poseToggle,
#goalToggle, #goalToggle,
#obstacleToggle, #obstacleToggle,
@@ -282,6 +287,7 @@
#downloadMap:hover, #downloadMap:hover,
#pathToggle:hover, #pathToggle:hover,
#sensorToggle:hover, #sensorToggle:hover,
#cameraToggle:hover,
#poseToggle:hover, #poseToggle:hover,
#goalToggle:hover, #goalToggle:hover,
#obstacleToggle:hover, #obstacleToggle:hover,
@@ -307,6 +313,7 @@
border-color:rgba(147,197,253,.55); border-color:rgba(147,197,253,.55);
color:#fff; color:#fff;
} }
#cameraToggle.active,
#sensorToggle.active { #sensorToggle.active {
background:rgba(14,116,144,.62); background:rgba(14,116,144,.62);
border-color:rgba(103,232,249,.55); border-color:rgba(103,232,249,.55);
@@ -385,6 +392,10 @@
</div> </div>
<button id="pathToggle" title="로봇 경로 표시" data-i18n-title="로봇 경로 표시" aria-label="로봇 경로 표시" data-i18n-aria-label="로봇 경로 표시">Path</button> <button id="pathToggle" title="로봇 경로 표시" data-i18n-title="로봇 경로 표시" aria-label="로봇 경로 표시" data-i18n-aria-label="로봇 경로 표시">Path</button>
<button id="sensorToggle" title="센서 표시" data-i18n-title="센서 표시" aria-label="센서 표시" data-i18n-aria-label="센서 표시" aria-pressed="false">Sensor</button> <button id="sensorToggle" title="센서 표시" data-i18n-title="센서 표시" aria-label="센서 표시" data-i18n-aria-label="센서 표시" aria-pressed="false">Sensor</button>
<button id="cameraToggle" type="button" title="카메라 영상 표시" data-i18n-title="카메라 영상 표시" aria-label="카메라 영상 표시" data-i18n-aria-label="카메라 영상 표시" aria-pressed="true" aria-controls="cameraPreviewWrap">
<!-- Lucide Camera icon. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13.997 4a2 2 0 0 1 1.76 1.05l.486.9A2 2 0 0 0 18.003 7H20a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h1.997a2 2 0 0 0 1.759-1.048l.489-.904A2 2 0 0 1 10.004 4z"/><circle cx="12" cy="13" r="3"/></svg>
</button>
<button id="poseToggle" title="초기 위치 지정" data-i18n-title="초기 위치 지정" aria-label="초기 위치 지정" data-i18n-aria-label="초기 위치 지정">Pose</button> <button id="poseToggle" title="초기 위치 지정" data-i18n-title="초기 위치 지정" aria-label="초기 위치 지정" data-i18n-aria-label="초기 위치 지정">Pose</button>
<button id="goalToggle" title="목표 위치 지정" data-i18n-title="목표 위치 지정" aria-label="목표 위치 지정" data-i18n-aria-label="목표 위치 지정">Goal</button> <button id="goalToggle" title="목표 위치 지정" data-i18n-title="목표 위치 지정" aria-label="목표 위치 지정" data-i18n-aria-label="목표 위치 지정">Goal</button>
<select id="cameraPresetSelect" title="카메라 시점" data-i18n-title="카메라 시점" aria-label="카메라 시점" data-i18n-aria-label="카메라 시점"> <select id="cameraPresetSelect" title="카메라 시점" data-i18n-title="카메라 시점" aria-label="카메라 시점" data-i18n-aria-label="카메라 시점">
@@ -475,6 +486,11 @@ const miniMapToggle = document.getElementById('miniMapToggle');
const cameraPreviewWrap = document.getElementById('cameraPreviewWrap'); const cameraPreviewWrap = document.getElementById('cameraPreviewWrap');
const cameraPreview = document.getElementById('cameraPreview'); const cameraPreview = document.getElementById('cameraPreview');
const cameraPreviewToggle = document.getElementById('cameraPreviewToggle'); const cameraPreviewToggle = document.getElementById('cameraPreviewToggle');
const cameraToggleButton = document.getElementById('cameraToggle');
const topRightControls = document.getElementById('topRightControls');
new ResizeObserver(() => {
cameraPreviewWrap.style.top = `${topRightControls.offsetTop + topRightControls.offsetHeight + 12}px`;
}).observe(topRightControls);
const responseWaitOverlay = document.getElementById('responseWaitOverlay'); const responseWaitOverlay = document.getElementById('responseWaitOverlay');
const mapSaveOverlay = document.getElementById('mapSaveOverlay'); const mapSaveOverlay = document.getElementById('mapSaveOverlay');
const downloadMapButton = document.getElementById('downloadMap'); const downloadMapButton = document.getElementById('downloadMap');
@@ -907,6 +923,8 @@ let cameraFrameRequestInFlight = false;
let cameraFrameRetryAt = 0; let cameraFrameRetryAt = 0;
let cameraFrameRefreshTimer = null; let cameraFrameRefreshTimer = null;
let cameraFrameObjectUrl = null; let cameraFrameObjectUrl = null;
let cameraPreviewEnabled = ENABLE_CAMERA_PREVIEW;
let cameraFrameAbortController = null;
let pathVisible = false; let pathVisible = false;
// SLAM always renders its live scan, including the forward beam. Navigation // SLAM always renders its live scan, including the forward beam. Navigation
// keeps the sensor opt-in so a saved-map view starts cleanly. // keeps the sensor opt-in so a saved-map view starts cleanly.
@@ -2052,6 +2070,7 @@ async function refreshSensorPointCloud() {
} }
function setCameraPreviewAvailable(available) { function setCameraPreviewAvailable(available) {
available = available && cameraPreviewEnabled;
cameraPreviewWrap?.classList.toggle('is-visible', available); cameraPreviewWrap?.classList.toggle('is-visible', available);
if (available) return; if (available) return;
if (cameraFrameObjectUrl) { if (cameraFrameObjectUrl) {
@@ -2067,7 +2086,7 @@ function hasLiveCameraState() {
} }
async function refreshCameraFrame() { async function refreshCameraFrame() {
if (!ENABLE_CAMERA_PREVIEW || cameraFrameRequestInFlight || document.hidden) return; if (!cameraPreviewEnabled || cameraFrameRequestInFlight || document.hidden) return;
// Reuse websocket topic status instead of probing an absent camera with HTTP. // Reuse websocket topic status instead of probing an absent camera with HTTP.
if (!hasLiveCameraState()) { if (!hasLiveCameraState()) {
setCameraPreviewAvailable(false); setCameraPreviewAvailable(false);
@@ -2075,15 +2094,18 @@ async function refreshCameraFrame() {
} }
if (Date.now() < cameraFrameRetryAt) return; if (Date.now() < cameraFrameRetryAt) return;
cameraFrameRequestInFlight = true; cameraFrameRequestInFlight = true;
const controller = new AbortController();
cameraFrameAbortController = controller;
const timeout = window.setTimeout(() => controller.abort(), 2000);
try { try {
const response = await fetch(`${BACKEND_URL}/camera-frame`, { cache: 'no-store', signal: AbortSignal.timeout(2000) }); const response = await fetch(`${BACKEND_URL}/camera-frame`, { cache: 'no-store', signal: controller.signal });
if (!response.ok) { if (!response.ok) {
cameraFrameRetryAt = Date.now() + 5000; cameraFrameRetryAt = Date.now() + 5000;
setCameraPreviewAvailable(false); setCameraPreviewAvailable(false);
return; return;
} }
const frame = await response.blob(); const frame = await response.blob();
if (!hasLiveCameraState() || document.hidden) { if (controller.signal.aborted || !cameraPreviewEnabled || !hasLiveCameraState() || document.hidden) {
setCameraPreviewAvailable(false); setCameraPreviewAvailable(false);
return; return;
} }
@@ -2094,13 +2116,35 @@ async function refreshCameraFrame() {
if (cameraPreview) cameraPreview.src = nextUrl; if (cameraPreview) cameraPreview.src = nextUrl;
if (previousUrl) URL.revokeObjectURL(previousUrl); if (previousUrl) URL.revokeObjectURL(previousUrl);
} catch (_) { } catch (_) {
cameraFrameRetryAt = Date.now() + 5000; if (!controller.signal.aborted) cameraFrameRetryAt = Date.now() + 5000;
setCameraPreviewAvailable(false); setCameraPreviewAvailable(false);
} finally { } finally {
window.clearTimeout(timeout);
cameraFrameAbortController = null;
cameraFrameRequestInFlight = false; cameraFrameRequestInFlight = false;
} }
} }
function setCameraPreviewEnabled(enabled) {
cameraPreviewEnabled = ENABLE_CAMERA_PREVIEW && enabled;
cameraToggleButton.classList.toggle('active', cameraPreviewEnabled);
cameraToggleButton.setAttribute('aria-pressed', String(cameraPreviewEnabled));
if (cameraFrameRefreshTimer) window.clearInterval(cameraFrameRefreshTimer);
cameraFrameRefreshTimer = null;
if (!cameraPreviewEnabled) {
cameraFrameAbortController?.abort();
setCameraPreviewAvailable(false);
return;
}
cameraFrameRetryAt = 0;
cameraPreviewWrap.classList.remove('is-collapsed');
cameraPreviewToggle.setAttribute('aria-expanded', 'true');
cameraPreviewToggle.setAttribute('aria-label', t('카메라 화면 접기'));
void refreshCameraFrame();
cameraFrameRefreshTimer = window.setInterval(() => void refreshCameraFrame(), 200);
}
cameraToggleButton.addEventListener('click', () => setCameraPreviewEnabled(!cameraPreviewEnabled));
cameraPreview?.addEventListener('load', () => setCameraPreviewAvailable(hasLiveCameraState())); cameraPreview?.addEventListener('load', () => setCameraPreviewAvailable(hasLiveCameraState()));
cameraPreview?.addEventListener('error', () => setCameraPreviewAvailable(false)); cameraPreview?.addEventListener('error', () => setCameraPreviewAvailable(false));
@@ -3231,16 +3275,14 @@ if (SHOW_SENSOR_POINT_CLOUD && navigationSensorVisible) {
void refreshSensorPointCloud(); void refreshSensorPointCloud();
sensorPointCloudRefreshTimer = window.setInterval(() => void refreshSensorPointCloud(), 750); sensorPointCloudRefreshTimer = window.setInterval(() => void refreshSensorPointCloud(), 750);
} }
if (ENABLE_CAMERA_PREVIEW) { setCameraPreviewEnabled(ENABLE_CAMERA_PREVIEW);
void refreshCameraFrame();
cameraFrameRefreshTimer = window.setInterval(() => void refreshCameraFrame(), 200);
}
window.addEventListener('beforeunload', () => { window.addEventListener('beforeunload', () => {
if (pointCloudRefreshTimer) window.clearInterval(pointCloudRefreshTimer); if (pointCloudRefreshTimer) window.clearInterval(pointCloudRefreshTimer);
if (sensorPointCloudRefreshTimer) window.clearInterval(sensorPointCloudRefreshTimer); if (sensorPointCloudRefreshTimer) window.clearInterval(sensorPointCloudRefreshTimer);
if (miniMapRenderTimer) window.clearTimeout(miniMapRenderTimer); if (miniMapRenderTimer) window.clearTimeout(miniMapRenderTimer);
if (cameraFrameRefreshTimer) window.clearInterval(cameraFrameRefreshTimer); if (cameraFrameRefreshTimer) window.clearInterval(cameraFrameRefreshTimer);
cameraFrameAbortController?.abort();
if (cameraFrameObjectUrl) URL.revokeObjectURL(cameraFrameObjectUrl); if (cameraFrameObjectUrl) URL.revokeObjectURL(cameraFrameObjectUrl);
}); });
@@ -7,6 +7,7 @@ const english = {
'맵 저장': 'Save map', '맵 저장': 'Save map',
'로봇 경로 표시': 'Show robot path', '로봇 경로 표시': 'Show robot path',
'센서 표시': 'Show sensors', '센서 표시': 'Show sensors',
'카메라 영상 표시': 'Show camera preview',
'초기 위치 지정': 'Set initial pose', '초기 위치 지정': 'Set initial pose',
'목표 위치 지정': 'Set goal', '목표 위치 지정': 'Set goal',
'카메라 시점': 'Camera view', '카메라 시점': 'Camera view',
+2 -2
View File
@@ -4493,9 +4493,9 @@ export default function App(): React.JSX.Element {
const modules = (workspaceWeek.modules ?? []) const modules = (workspaceWeek.modules ?? [])
.slice() .slice()
.sort((left, right) => (right.sortOrder ?? 0) - (left.sortOrder ?? 0)) .sort((left, right) => (right.sortOrder ?? 0) - (left.sortOrder ?? 0))
.map((module) => ({ moduleName: module.name, folderName: module.folderName, templateRepositoryUrl: module.templateRepositoryUrl })) .map((module) => ({ moduleName: module.name, folderName: module.folderName }))
if (!modules.length) return if (!modules.length) return
await window.remoteWorkspace.modules.resetSession(target, modules).catch(() => ({ ok: false, message: '플랫폼 초기화 실패' })) await window.remoteWorkspace.modules.stopMany(target, modules).catch(() => ({ ok: false, message: '모듈 중지 실패' }))
} }
const getFreshAccessToken = useCallback(async (): Promise<string> => { const getFreshAccessToken = useCallback(async (): Promise<string> => {
const currentSession = readStoredAuthSession() ?? authSessionRef.current const currentSession = readStoredAuthSession() ?? authSessionRef.current
@@ -115,7 +115,10 @@ export function useRemoteWorkspaceRuntime({
.sort((left, right) => (right.sortOrder ?? 0) - (left.sortOrder ?? 0)) .sort((left, right) => (right.sortOrder ?? 0) - (left.sortOrder ?? 0))
.map(({ moduleName, folderName, templateRepositoryUrl }) => ({ moduleName, folderName, templateRepositoryUrl })) .map(({ moduleName, folderName, templateRepositoryUrl }) => ({ moduleName, folderName, templateRepositoryUrl }))
if (!modules.length) return if (!modules.length) return
await window.remoteWorkspace.modules.resetSession(getRemoteWorkspaceTarget(), modules).catch(() => ({ ok: false, message: '플랫폼 초기화 실패' })) await window.remoteWorkspace.modules.stopMany(
getRemoteWorkspaceTarget(),
modules.map(({ moduleName, folderName }) => ({ moduleName, folderName })),
).catch(() => ({ ok: false, message: '모듈 중지 실패' }))
return return
} }
await window.localWorkspace.modules.stopRunning().catch(() => ({ ok: false, message: '모듈 중지 실패' })) await window.localWorkspace.modules.stopRunning().catch(() => ({ ok: false, message: '모듈 중지 실패' }))