93 lines
11 KiB
JavaScript
93 lines
11 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
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 lessons = {
|
|
1: [
|
|
['ROS2 기초', '노드와 패키지 구조를 이해하고 Topic, Service, Action으로 데이터를 주고받습니다. CLI로 통신 상태를 확인하고 TF를 통해 센서와 차량 좌표계의 관계를 살펴봅니다.', ['노드와 패키지', 'Topic · Service · Action', 'TF 좌표계']],
|
|
['Sensor Processing', 'LiDAR와 카메라 데이터의 형식, 갱신 주기와 좌표계를 확인합니다. 노이즈와 불필요한 데이터를 걸러내고 센서 보정 결과가 인식에 미치는 영향을 비교합니다.', ['센서 메시지와 타임스탬프', '필터링과 전처리', '센서 캘리브레이션']],
|
|
['Simulation 환경 구축 Part 1', 'Gazebo 월드와 차량 모듈을 실행하고 센서 데이터가 ROS2 토픽으로 전달되는 과정을 확인합니다. 시뮬레이션 시간과 TF 연결을 점검해 실습의 기본 환경을 구성합니다.', ['Gazebo 월드와 로봇 모델', '시뮬레이션 시간', '센서 토픽과 TF 연결']],
|
|
['Simulation 환경 구축 Part 2', '실습 차량에 맞는 모듈과 파라미터를 선택하고 실행 순서를 정리합니다. 차량 모델을 바꾸며 공통 NAV2 모듈과 차량별 인터페이스의 역할을 구분합니다.', ['환경별 모듈 구성', '차량 인터페이스', '실행 순서와 의존성']],
|
|
['LiDAR SLAM', '차량을 이동시키며 LiDAR 관측과 오도메트리를 이용해 2D 맵을 생성합니다. 반복 구간과 이동 속도에 따른 맵 품질을 비교하고, 완성한 맵을 저장해 다시 확인합니다.', ['점유격자 지도', '오도메트리와 스캔 정합', '루프 클로저']],
|
|
['LiDAR Localization', '저장한 맵을 불러오고 초기 위치를 지정해 AMCL 위치추정을 시작합니다. 이동 중 추정 위치와 센서 관측이 일치하는지 확인하고 초기화가 잘못된 경우의 증상을 살펴봅니다.', ['AMCL 입자 필터', '초기 위치 설정', 'map · odom · base_link']],
|
|
['Parameter Tuning', '센서 처리와 NAV2 모듈의 주요 파라미터를 한 번에 하나씩 변경합니다. 같은 주행 조건에서 경로, 오차와 처리 시간을 비교하고 설정값과 결과를 기록합니다.', ['기준 시나리오', '단일 변수 비교', '성능 지표와 재현성']],
|
|
['[심화] Camera based SLAM', '카메라 기반 SLAM의 영상 입력과 위치·맵 출력 구조를 살펴봅니다. 특징점 추적과 깊이 정보의 역할을 이해하고 조명 변화나 반복 무늬에서 발생하는 한계를 분석합니다.', ['영상 특징점 추적', '깊이 정보와 스케일', '시각 오도메트리']],
|
|
['Occupancy Map', '센서 관측이 점유격자와 costmap에 반영되는 흐름을 확인합니다. 장애물 주변의 비용과 차량 크기 설정을 바꾸며 통과 가능한 공간과 안전 여유가 달라지는 모습을 비교합니다.', ['점유 확률', 'Costmap 레이어', 'Footprint와 Inflation']],
|
|
['Planning', '시작 위치와 목표 지점을 설정해 전역 경로 생성 과정을 확인합니다. 장애물 배치와 주행 제약을 바꾸며 경로의 길이, 안전성 및 재계획 결과를 비교합니다.', ['전역 경로 탐색', '장애물 비용과 주행 제약', '경로 재계획']],
|
|
['Control', '계획된 경로를 따라가도록 속도 명령을 생성하는 제어 흐름을 확인합니다. 속도와 회전 관련 설정을 조절하고 곡선 및 좁은 구간에서 추종 오차와 안정성을 비교합니다.', ['경로 추종', '속도·회전 제약', '추종 오차와 안정성']],
|
|
['[심화] 딥러닝 기반 객체 인식', '카메라 영상에서 객체를 검출하고 클래스, 위치와 신뢰도 결과를 해석합니다. 인식 결과를 ROS2 데이터 흐름에 연결하고 오검출과 추론 지연이 주행 판단에 미치는 영향을 살펴봅니다.', ['객체 검출과 신뢰도', '오검출·미검출', '추론 지연과 ROS2 연동']],
|
|
],
|
|
2: [
|
|
['프로젝트 주제와 역할 분담', '팀이 해결할 자율주행 문제를 정의하고 실습 가능한 시나리오로 구체화합니다. 성공 기준과 역할, 일정을 정하고 공통으로 기록할 검증 항목을 합의합니다.', ['문제 정의와 요구사항', '역할과 인터페이스 분담', '성공 기준']],
|
|
['센서·모듈 구성 설계', '시나리오에 필요한 센서와 NAV2 모듈을 선택하고 입력·출력 토픽 및 TF 관계를 정리합니다. 로컬 시뮬레이션과 실차의 차이를 구분해 실행 순서와 파라미터를 설계합니다.', ['센서·모듈 데이터 흐름', '토픽과 TF 인터페이스', '환경별 실행 구성']],
|
|
['알고리즘 구현과 튜닝', 'SLAM, 위치추정, 경로 계획과 제어 중 개선할 부분을 정해 소스나 파라미터를 수정합니다. 변경 전후를 같은 조건에서 비교하고 팀원이 재현할 수 있도록 설정과 결과를 공유합니다.', ['소스 변경 관리', '파라미터 튜닝', '비교 실험']],
|
|
['시뮬레이션·플랫폼 검증', '시뮬레이션에서 기능을 확인한 뒤 안전한 실차 환경에서 동일한 시나리오를 검증합니다. 토픽, 로그와 주행 결과를 함께 분석해 환경 차이와 실패 원인을 정리합니다.', ['단계별 통합 검증', '시뮬레이션과 실차 차이', '로그 기반 원인 분석']],
|
|
['결과 제출과 발표', '문제 정의부터 설계, 구현과 검증까지의 과정을 소스 및 실행 결과와 함께 정리합니다. 성능 개선 근거와 남은 한계를 설명하고 다른 팀이 실행할 수 있는 절차를 제공합니다.', ['재현 가능한 실행 절차', '정량·정성 평가', '결과와 한계 전달']],
|
|
],
|
|
3: [
|
|
['SLAM', '원거리 MK-mini 플랫폼에 연결해 센서 토픽과 TF 상태를 점검하고 원격으로 모듈을 편집·빌드·실행합니다. 로컬 MK-mini 시뮬레이션에서 먼저 확인한 뒤 실차에서 SLAM 맵을 생성·저장하고 통신 지연과 맵 품질을 비교합니다.', ['원격 개발·실행 흐름', '센서 토픽과 TF 점검', 'SLAM 맵 생성·저장']],
|
|
['자율주행', '저장한 맵으로 MK-mini의 위치추정, 경로 계획과 제어 모듈을 구성하고 초기 위치와 목표 지점을 설정합니다. 원격으로 로그와 파라미터를 확인하며 주행 결과를 개선하고, 통신 단절에 대비한 정지·복구 절차를 점검합니다.', ['위치추정·계획·제어 연동', '원격 튜닝과 로그 분석', '통신 지연과 안전한 복구']],
|
|
],
|
|
};
|
|
const include = { weeks: { orderBy: { week: 'asc' }, include: { topics: { orderBy: { sortOrder: 'asc' } }, modules: { orderBy: { id: 'asc' } }, practices: true } }, instructorCurriculumWeekOverrides: { orderBy: { id: 'asc' } }, environmentProfiles: { orderBy: { id: 'asc' } } };
|
|
const snapshot = tx => tx.course.findMany({ orderBy: { id: 'asc' }, include });
|
|
async function main() {
|
|
const before = await snapshot(db);
|
|
const targets = before.filter(c => c.slug !== 'autonomy-studio-lms-guide');
|
|
assert.equal(targets.length, 3);
|
|
for (const c of targets) for (const week of [1, 2, 3]) {
|
|
const w = c.weeks.find(w => w.week === week); assert(w);
|
|
assert.deepEqual(w.topics.map(t => t.title), lessons[week].map(l => l[0]));
|
|
}
|
|
const backup = `backups/learning-steps-before-${Date.now()}.json`;
|
|
fs.mkdirSync('backups', { recursive: true });
|
|
fs.writeFileSync(backup, JSON.stringify(before, null, 2), { mode: 0o600, flag: 'wx' });
|
|
console.log('Backup:', backup);
|
|
await db.$transaction(async tx => {
|
|
assert.deepEqual(await snapshot(tx), before);
|
|
for (const c of targets) for (const week of [1, 2, 3]) {
|
|
const w = c.weeks.find(w => w.week === week);
|
|
for (const [i, topic] of w.topics.entries()) await tx.courseWeekTopic.update({ where: { id: topic.id }, data: { detail: lessons[week][i][1] } });
|
|
const existing = c.instructorCurriculumWeekOverrides.filter(o => o.week === week);
|
|
const overrides = existing.length ? existing : [{ organizationName: c.organizationName }];
|
|
for (const o of overrides) {
|
|
const steps = lessons[week].map(([title, description, concepts], i) => {
|
|
const old = o.learningSteps?.[i];
|
|
if (old) assert.equal(old.title, title);
|
|
const videoUrl = old?.videoUrl ?? o.topicVideoUrls?.[i] ?? '';
|
|
return { ...old, title, description, concepts, videoUrl, videoEnabled: old?.videoEnabled ?? Boolean(videoUrl) };
|
|
});
|
|
const data = { learningSteps: steps, topics: steps.map(s => s.title), topicDetails: steps.map(s => s.description) };
|
|
if (o.id) await tx.instructorCurriculumWeekOverride.update({ where: { id: o.id }, data });
|
|
else await tx.instructorCurriculumWeekOverride.create({ data: { courseId: c.id, week, organizationName: c.organizationName, ...data } });
|
|
}
|
|
}
|
|
const after = await snapshot(tx);
|
|
assert.deepEqual(after.find(c => c.slug === 'autonomy-studio-lms-guide'), before.find(c => c.slug === 'autonomy-studio-lms-guide'));
|
|
for (const old of targets) {
|
|
const current = after.find(c => c.id === old.id);
|
|
assert.deepEqual(current.environmentProfiles, old.environmentProfiles);
|
|
for (const w of current.weeks) {
|
|
const prev = old.weeks.find(p => p.id === w.id);
|
|
assert.deepEqual(w.modules, prev.modules);
|
|
assert.deepEqual(w.practices, prev.practices);
|
|
}
|
|
for (const o of current.instructorCurriculumWeekOverrides.filter(o => [1, 2, 3].includes(o.week))) {
|
|
assert.deepEqual(o.learningSteps.map(s => [s.title, s.description, s.concepts]), lessons[o.week]);
|
|
const prev = old.instructorCurriculumWeekOverrides.find(p => p.id === o.id);
|
|
if (prev) {
|
|
const omit = v => { const { learningSteps, topics, topicDetails, updatedAt, ...rest } = v; return rest; };
|
|
assert.deepEqual(omit(o), omit(prev));
|
|
if (prev.learningSteps) assert.deepEqual(o.learningSteps.map(s => [s.videoUrl, s.videoEnabled]), prev.learningSteps.map(s => [s.videoUrl, s.videoEnabled]));
|
|
}
|
|
}
|
|
}
|
|
}, { isolationLevel: 'Serializable', timeout: 30000 });
|
|
console.log('Updated 3 courses, 9 weeks, 57 learning steps; 3 concepts per step.');
|
|
}
|
|
main().catch(e => { console.error(e.message); process.exitCode = 1; }).finally(() => db.$disconnect());
|