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());