feat: add weekly curriculum cover image upload and display

This commit is contained in:
2026-09-23 16:03:33 +09:00
parent a7c9cdc16e
commit 656b1bf62b
23 changed files with 723 additions and 95 deletions
+43 -41
View File
@@ -1,5 +1,7 @@
import { app, BrowserWindow, dialog, Menu, globalShortcut, ipcMain, screen, shell } from 'electron'
import { checkDockerStatus } from './dockerStatus'
import { cameraRuntimeArtifacts } from './cameraRuntime'
import { remoteModuleFilesScript } from './remoteModuleFiles'
import { resolveContainerTerminal } from './containerTerminal'
import { yahboomDevicePreflight, yahboomModuleCases, yahboomRuntimeImage, yahboomRuntimeArtifacts, yahboomSimulation } from './yahboomRuntime'
import { visualizationProgress, reportVisualizationProgress } from './visualizationProgress'
@@ -142,6 +144,7 @@ function getBundledZstdPath(): string | null {
}
type CachedModuleFiles = {
filePaths?: string[]
root: string | null
absoluteRoot: string | null
files: Record<string, string>
@@ -4598,6 +4601,7 @@ function buildNav2DockerRunScript(): string {
'else',
'case "$MODULE_FOLDER" in',
...yahboomModuleCases(),
' *lms-nav2-camera-test*) PACKAGE=camera_test_pkg; LAUNCH=camera.launch.py ;;',
' *lms-nav2-mkmini*) PACKAGE=vehicle_pkg; LAUNCH=vehicle.launch.py ;;',
' *lms-nav2-gazebo-simulator*) PACKAGE=nav2_turtlebot_simulator; LAUNCH=simulator.launch.py ;;',
' *lms-nav2-gazebo-vehicle*) PACKAGE=vehicle_pkg; LAUNCH=vehicle.launch.py ;;',
@@ -4635,6 +4639,12 @@ function buildNav2DockerRunScript(): string {
'',
'test -f "$MODULE_ROOT/$MODULE_FOLDER/install/setup.bash" || { echo "Module is not built: $MODULE_FOLDER" >&2; exit 1; }',
...yahboomDevicePreflight(),
'CAMERA_DEVICE=""',
'if [[ "$MODULE_FOLDER" == *lms-nav2-camera-test* ]]; then',
' CAMERA_DEVICE=/dev/video0',
' for arg in "$@"; do case "$arg" in video_device:=*) CAMERA_DEVICE=${arg#video_device:=} ;; esac; done',
' [[ "$CAMERA_DEVICE" =~ ^/dev/video[0-9]+$ ]] && [ -c "$CAMERA_DEVICE" ] || { echo "Camera device not found: $CAMERA_DEVICE" >&2; exit 1; }',
'fi',
'docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true',
'',
'args=(',
@@ -4652,6 +4662,12 @@ function buildNav2DockerRunScript(): string {
' -v /etc/localtime:/etc/localtime:ro',
')',
'if [ -n "$YAHBOOM_DEVICE" ]; then args+=(--device "$YAHBOOM_DEVICE:$YAHBOOM_DEVICE"); fi',
'if [ -n "$CAMERA_DEVICE" ]; then',
' args+=(--device "$CAMERA_DEVICE:$CAMERA_DEVICE")',
' for capture_device in /dev/capture-vi-channel*; do',
' if [ -c "$capture_device" ]; then args+=(--device "$capture_device:$capture_device"); fi',
' done',
'fi',
'if [[ "$GPU_ENABLED" == "true" || "$GPU_ENABLED" == "1" ]]; then',
' args+=(--gpus all -e NVIDIA_VISIBLE_DEVICES=all -e NVIDIA_DRIVER_CAPABILITIES=compute,utility,graphics)',
'fi',
@@ -4695,6 +4711,7 @@ function buildNav2DockerBuildScript(): string {
'else',
'case "$MODULE_FOLDER" in',
...yahboomModuleCases(true),
' *lms-nav2-camera-test*) TARGET=camera_test_pkg ;;',
' *lms-nav2-mkmini*) TARGET=vehicle_pkg ;;',
' *lms-nav2-gazebo-simulator*) TARGET=nav2_turtlebot_simulator ;;',
' *lms-nav2-gazebo-vehicle*) TARGET=vehicle_pkg ;;',
@@ -5051,6 +5068,7 @@ async function resolveLocalModuleRuntime(
rosParams: string,
runtimeImages?: RuntimeImageArtifact[] | null,
): Promise<{ image: string; artifacts: RuntimeImageArtifact[] | null | undefined }> {
runtimeImages = cameraRuntimeArtifacts(folderName, runtimeImages)
if (yahboomSimulation(folderName, rosParams)) {
const artifact = runtimeImages?.find((item) =>
item.image?.trim() === defaultNav2GazeboRuntimeImage.image && item.url?.trim())
@@ -5062,7 +5080,9 @@ async function resolveLocalModuleRuntime(
)
return {
image,
artifacts: yahboomRuntimeImage(folderName)
artifacts: folderName.includes('lms-nav2-camera-test')
? (runtimeImages ?? []).filter((item) => item.image?.trim() === image)
: yahboomRuntimeImage(folderName)
? yahboomRuntimeArtifacts(folderName, image, runtimeImages)
: requiredRuntimeImages(runtimeImages, image),
}
@@ -5309,6 +5329,7 @@ async function startRemoteWorkspaceModule(
if (existsOutput.trim() !== 'yes') throw new Error('설치된 모듈을 찾을 수 없습니다.')
const nav2Module = isNav2ModuleFolder(targetFolderName)
runtimeImages = cameraRuntimeArtifacts(targetFolderName, runtimeImages)
const runtimeImage = nav2Module
? await resolveRemoteCustomModuleRuntimeImage(
target,
@@ -5320,7 +5341,9 @@ async function startRemoteWorkspaceModule(
? await ensureRemoteNav2RuntimeAssets(target)
: await ensureRemoteStudioRuntimeAssets(target)
const fastDdsConfigPath = await prepareRemoteRosNetworkConfig(target, runtimeRoot, rosNetwork, rosDomainId)
await prepareRemoteRuntimeImages(target, yahboomRuntimeArtifacts(targetFolderName, runtimeImage, runtimeImages))
await prepareRemoteRuntimeImages(target, targetFolderName.includes('lms-nav2-camera-test')
? (runtimeImages ?? []).filter((item) => item.image?.trim() === runtimeImage)
: yahboomRuntimeArtifacts(targetFolderName, runtimeImage, runtimeImages))
reportModuleProgress("모듈 실행 중...")
const currentStatus = await runSshOutput(target, `timeout 12s docker ps -a --filter ${quoteShellSingle(`name=^/${dockerInfo.containerKey}$`)} --format "{{.Status}}" 2>/dev/null || true`, 20000)
if (/^Up\b/i.test(currentStatus.trim())) return { ok: true, message: '이미 실행 중입니다.' }
@@ -5403,6 +5426,7 @@ async function buildRemoteWorkspaceModule(
): Promise<{ ok: boolean; message: string }> {
const target = normalizeRemoteTarget(targetInput)
const targetFolderName = folderName?.trim() || moduleName
runtimeImages = cameraRuntimeArtifacts(targetFolderName, runtimeImages)
const dockerInfo = parseModuleDockerInfo(targetFolderName)
if (!dockerInfo || !isAllowedContainerName(dockerInfo.containerKey)) throw new Error('모듈 폴더명 형식이 올바르지 않습니다.')
@@ -5413,7 +5437,9 @@ async function buildRemoteWorkspaceModule(
targetFolderName,
yahboomRuntimeImage(targetFolderName) ?? await resolveRemoteNav2RuntimeImage(runtimeImages),
)
await prepareRemoteRuntimeImages(target, yahboomRuntimeArtifacts(targetFolderName, runtimeImage, runtimeImages))
await prepareRemoteRuntimeImages(target, targetFolderName.includes('lms-nav2-camera-test')
? (runtimeImages ?? []).filter((item) => item.image?.trim() === runtimeImage)
: yahboomRuntimeArtifacts(targetFolderName, runtimeImage, runtimeImages))
reportModuleProgress("모듈 빌드 중...")
await runSsh(target, [
'set -e',
@@ -5723,44 +5749,11 @@ async function collectModuleFiles(moduleName: string, workspaceRoot?: string | n
async function collectRemoteModuleFiles(targetInput: RemoteWorkspaceTarget, moduleName: string, folderName?: string | null): Promise<CachedModuleFiles> {
const { target, moduleRoot } = await resolveRemoteGitModuleRoot(targetInput, moduleName, folderName)
const skippedNames = Array.from(skippedCacheDirs).map(quoteShellSingle).join(' ')
const script = [
'set +e',
`ROOT=${quoteShellSingle(moduleRoot)}`,
`SKIPPED="${skippedNames}"`,
'find "$ROOT" -mindepth 1 -maxdepth 10 \\( -name .git -o -name build -o -name install -o -name log -o -name node_modules -o -name docker \\) -prune -o -type d -printf "D\\t%P\\n" -o -type f -printf "F\\t%P\\t%s\\n" | while IFS="$(printf \'\\t\')" read -r KIND REL SIZE; do',
' [ -z "$REL" ] && continue',
' NAME="${REL##*/}"',
' case "$KIND" in',
' D) printf "D\\t%s\\n" "$(printf %s "$REL" | base64 -w0)" ;;',
' F)',
' CONTENT="QmluYXJ5IG9yIGxhcmdlIGZpbGUgcHJldmlldyBpcyBub3QgYXZhaWxhYmxlLg=="',
' if [ "${SIZE:-0}" -le 1048576 ]; then',
' case "$NAME" in',
' *.c|*.cc|*.cpp|*.cxx|*.h|*.hpp|*.hh|*.py|*.xml|*.urdf|*.xacro|*.yaml|*.yml|*.json|*.txt|*.md|*.cmake|*.launch|*.sh|*.bash|*.toml|*.ini|*.cfg|*.rviz) CONTENT="$(base64 -w0 "$ROOT/$REL" 2>/dev/null)" ;;',
' esac',
' fi',
' printf "F\\t%s\\t%s\\n" "$(printf %s "$REL" | base64 -w0)" "$CONTENT"',
' ;;',
' esac',
'done'
].join('\n')
// Source transfers must not hold up module start/stop on the control queue.
const output = await runUnqueuedSshOutput(target, script, 30000)
const files: Record<string, string> = {}
const folders: string[] = []
for (const line of output.split('\n')) {
if (!line.trim()) continue
const [kind, encodedPath = '', encodedContent = ''] = line.split('\t')
const relativePath = decodeRemoteField(encodedPath)
if (!relativePath) continue
const rendererPath = `modules/${moduleName}/${relativePath}`
if (kind === 'D') folders.push(rendererPath)
if (kind === 'F') files[rendererPath] = decodeRemoteField(encodedContent)
}
return { root: moduleRoot.split('/').pop() ?? moduleName, absoluteRoot: moduleRoot, files, folders }
const output = await runUnqueuedSshOutput(target, remoteModuleFilesScript(moduleRoot, { action: 'list' }), 30000)
const listing = JSON.parse(output) as { filePaths: string[]; folders: string[] }
const prefix = (path: string): string => `modules/${moduleName}/${path}`
return { root: moduleRoot.split('/').pop() ?? moduleName, absoluteRoot: moduleRoot,
files: {}, filePaths: listing.filePaths.map(prefix), folders: listing.folders.map(prefix) }
}
async function collectRootFiles(rootPath: string, rendererRoot: string): Promise<CachedModuleFiles> {
@@ -6447,6 +6440,15 @@ function registerLocalWorkspaceHandlers(): void {
ipcMain.handle('remote-workspace:git:submissions', (_event, target: RemoteWorkspaceTarget, modules: Array<{ moduleName: string; folderName?: string | null }>) => readRemoteWorkspaceGitSubmissions(target, modules))
ipcMain.handle('remote-workspace:git:commit-diff', (_event, target: RemoteWorkspaceTarget, moduleName: string, folderName?: string | null, commitHash?: string | null) => readRemoteWorkspaceGitCommitDiff(target, moduleName, folderName, commitHash))
ipcMain.handle('remote-workspace:module-files:read', (_event, target: RemoteWorkspaceTarget, moduleName: string, folderName?: string | null) => collectRemoteModuleFiles(target, moduleName, folderName))
ipcMain.handle('remote-workspace:module-files:content', async (_event, target: RemoteWorkspaceTarget, moduleName: string, path: string, folderName?: string | null) => {
const resolved = await resolveRemoteRendererModulePath(target, moduleName, path, folderName)
return JSON.parse(await runUnqueuedSshOutput(resolved.target, remoteModuleFilesScript(resolved.moduleRoot, { action: 'content', path: resolved.relativePath }), 30000))
})
ipcMain.handle('remote-workspace:module-files:search', async (_event, target: RemoteWorkspaceTarget, moduleName: string, query: string, folderName?: string | null) => {
const resolved = await resolveRemoteGitModuleRoot(target, moduleName, folderName)
const results = JSON.parse(await runUnqueuedSshOutput(resolved.target, remoteModuleFilesScript(resolved.moduleRoot, { action: 'search', query }), 30000)) as Array<{ file: string; lineNumber: number; lineText: string }>
return results.map((result) => ({ ...result, file: `modules/${moduleName}/${result.file}` }))
})
ipcMain.handle('remote-workspace:module-files:image', (_event, target: RemoteWorkspaceTarget, moduleName: string, rendererPath: string, folderName?: string | null) => readRemoteWorkspaceModuleImage(target, moduleName, rendererPath, folderName))
ipcMain.handle('remote-workspace:module-files:create', async (_event, target: RemoteWorkspaceTarget, moduleName: string, rendererPath: string, kind: 'file' | 'folder', folderName?: string | null) => {
const resolved = await resolveRemoteRendererModulePath(target, moduleName, rendererPath, folderName)