Files
lms-be/scripts/add-base-remote-development-week.cjs
T

135 lines
7.1 KiB
JavaScript

const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
require('dotenv').config({ quiet: true });
const { PrismaClient } = require('@prisma/client');
const url = new URL(process.env.DATABASE_URL);
url.hostname = '192.168.0.20';
url.port = '15437';
url.pathname = '/lms';
url.searchParams.set('connect_timeout', '5');
const db = new PrismaClient({ datasources: { db: { url: url.toString() } } });
const apply = process.argv.includes('--apply');
const slug = 'futuredrive-autonomy-basic';
const localImage = 'future-nav2-gazebo:humble';
const remoteImage = 'future-nav2:humble-jetson_orin';
const names = ['NAV2-Control', 'NAV2-Localization', 'NAV2-Map', 'NAV2-Perception',
'NAV2-Perception3D', 'NAV2-Planning', 'NAV2-Gazebo', 'NAV2-SLAM', 'NAV2-SLAM3D',
'NAV2-MKMini', 'NAV2-Rviz2'];
const steps = [
{ title: 'SLAM', detail: '원거리 MK-mini 플랫폼에 연결하여 모듈 소스와 파라미터를 수정하고, 센서 토픽을 확인하며 SLAM 맵을 생성·저장합니다. 로컬 MK-mini 시뮬레이션에서 먼저 검증합니다.' },
{ title: '자율주행', detail: '저장한 맵으로 위치추정·경로 계획·제어 모듈을 구성하고 목표 지점 주행을 검증합니다. 원격 코드 편집과 빌드, 실행 로그 분석 및 파라미터 튜닝을 실습합니다.' },
];
function parameters(module, remote) {
const sim = `sim_mode:=${!remote}`;
switch (module.name) {
case 'NAV2-Gazebo': return ['gui:=false', 'platform:=mkmini'];
case 'NAV2-MKMini': return [sim, 'platform:=mkmini', 'robot:=mkmini', 'level:=2d', ...(!remote ? ['simulator:=gazebo'] : [])];
case 'NAV2-Control': return [sim, 'robot:=mkmini', 'level:=2d'];
case 'NAV2-Perception': return [sim];
case 'NAV2-Perception3D':
case 'NAV2-SLAM3D': return [sim, 'robot:=mkmini'];
case 'NAV2-Localization': return [sim, 'robot:=mkmini', 'level:=2d', 'localization_mode:=amcl'];
case 'NAV2-SLAM':
case 'NAV2-Planning':
case 'NAV2-Map': return [sim, 'robot:=mkmini', 'level:=2d'];
default: return module.defaultParameters;
}
}
async function snapshot(client) {
return client.course.findUniqueOrThrow({ where: { slug }, include: {
weeks: { orderBy: { week: 'asc' }, include: {
modules: { orderBy: { sortOrder: 'asc' } }, topics: { orderBy: { sortOrder: 'asc' } },
practices: { orderBy: { sortOrder: 'asc' } }, references: true,
} }, environmentProfiles: { orderBy: [{ week: 'asc' }, { platformKey: 'asc' }] },
weekMaps: true,
} });
}
async function main() {
const before = await snapshot(db);
assert.equal(before.weeks.length, 2, 'Expected only weeks 1 and 2; refusing to overwrite');
const source = before.weeks.find(w => w.week === 1);
assert(source);
const selected = names.map(name => {
const matches = source.modules.filter(m => m.name === name);
assert.equal(matches.length, 1, `Missing or ambiguous module: ${name}`);
return matches[0];
});
const profiles = before.environmentProfiles.filter(p => p.week === 1);
assert.equal(profiles.length, 2);
assert(profiles.some(p => p.config.target === 'local'));
assert(profiles.some(p => p.config.target === 'remote-jetson'));
if (apply) {
const backup = path.resolve('backups', `base-before-week3-${Date.now()}.json`);
fs.mkdirSync(path.dirname(backup), { recursive: true });
fs.writeFileSync(backup, JSON.stringify(before, null, 2), { mode: 0o600, flag: 'wx' });
console.log('Backup:', backup);
}
let summary;
try {
await db.$transaction(async tx => {
assert.deepEqual(await snapshot(tx), before, 'Source changed during preparation');
const week = await tx.courseWeek.create({ data: {
courseId: before.id, week: 3, title: '원격 개발', tags: ['MK-mini', '진행예정'],
progress: 0, state: 'locked', duration: '1주', github: source.github,
overview: '원거리 MK-mini 플랫폼을 연결하고 원격으로 코드를 편집·빌드·실행합니다. 로컬 MK-mini 시뮬레이션과 실차에서 SLAM 및 자율주행을 검증합니다.',
deliverable: 'SLAM 저장 맵, 자율주행 검증 결과, 원격 개발 소스 변경 내역 및 실행 로그를 제출합니다.',
requiresSourceSubmission: true,
topics: { create: steps.map((s, i) => ({ ...s, sortOrder: i + 1 })) },
practices: { create: steps.map((s, i) => ({ title: s.title, description: s.detail, sortOrder: i + 1 })) },
} });
const mapping = new Map();
const created = [];
for (const [i, m] of selected.entries()) {
const { id, weekId, createdAt, updatedAt, ...data } = m;
const next = await tx.courseWeekModule.create({ data: {
...data, weekId: week.id, sortOrder: i + 1,
dockerImage: localImage, defaultParameters: parameters(m, false),
} });
mapping.set(id, next.id);
created.push(next);
}
const newProfiles = [];
for (const p of profiles) {
const remote = p.config.target === 'remote-jetson';
const config = structuredClone(p.config);
config.moduleSet.modules = p.config.moduleSet.modules.flatMap(entry => {
if (!mapping.has(entry.id)) return [];
const m = selected.find(m => m.id === entry.id);
return [{ ...entry, id: mapping.get(entry.id), dockerImage: remote ? remoteImage : localImage,
defaultParameters: parameters(m, remote) }];
});
config.moduleSet.includeUnlisted = false;
assert.equal(config.moduleSet.modules.length, 9);
assert(config.moduleSet.modules.every(m => created.some(c => c.id === m.id)));
newProfiles.push(await tx.courseEnvironmentProfile.create({ data: {
courseId: before.id, week: 3, platformKey: p.platformKey, profileType: p.profileType,
name: p.name, enabled: p.enabled, config,
} }));
}
for (const m of before.weekMaps.filter(m => m.week === 1)) {
await tx.courseWeekMap.create({ data: { courseId: before.id, week: 3, mapKey: m.mapKey, sortOrder: m.sortOrder } });
}
const after = await snapshot(tx);
assert.deepEqual(after.weeks.filter(w => w.week !== 3), before.weeks);
assert.deepEqual(after.environmentProfiles.filter(p => p.week !== 3), before.environmentProfiles);
assert.equal(after.weeks.find(w => w.week === 3).topics.length, 2);
assert(!created.some(m => /yahboom|turtlebot/i.test(JSON.stringify(m))));
summary = { applied: apply, course: before.title, weekId: week.id, title: week.title,
steps: steps.map(s => s.title), modules: created.map(m => m.name),
environments: newProfiles.map(p => ({ target: p.config.target, modules: p.config.moduleSet.modules.map(m => ({
name: created.find(c => c.id === m.id).name, image: m.dockerImage, parameters: m.defaultParameters,
})) })) };
if (!apply) throw new Error('DRY_RUN_ROLLBACK');
}, { isolationLevel: 'Serializable', timeout: 30000 });
} catch (e) {
if (e.message !== 'DRY_RUN_ROLLBACK') throw e;
}
console.log(JSON.stringify(summary, null, 2));
}
main().catch(e => { console.error(e.message); process.exitCode = 1; }).finally(() => db.$disconnect());