CREATE TABLE "courses" ( "id" UUID NOT NULL DEFAULT gen_random_uuid(), "slug" TEXT NOT NULL, "title" TEXT NOT NULL, "subtitle" TEXT, "description" TEXT, "organization_name" TEXT NOT NULL, "teacher_id" UUID NOT NULL, "is_active" BOOLEAN NOT NULL DEFAULT true, "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updated_at" TIMESTAMP(3) NOT NULL, CONSTRAINT "courses_pkey" PRIMARY KEY ("id") ); CREATE TABLE "course_weeks" ( "id" UUID NOT NULL DEFAULT gen_random_uuid(), "course_id" UUID NOT NULL, "week" INTEGER NOT NULL, "title" TEXT NOT NULL, "tags" TEXT[] NOT NULL, "progress" INTEGER NOT NULL DEFAULT 0, "state" TEXT NOT NULL, "duration" TEXT NOT NULL, "github" TEXT NOT NULL, "overview" TEXT NOT NULL, "deliverable" TEXT NOT NULL, "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updated_at" TIMESTAMP(3) NOT NULL, CONSTRAINT "course_weeks_pkey" PRIMARY KEY ("id") ); CREATE TABLE "course_week_topics" ( "id" UUID NOT NULL DEFAULT gen_random_uuid(), "week_id" UUID NOT NULL, "title" TEXT NOT NULL, "detail" TEXT NOT NULL, "sort_order" INTEGER NOT NULL, CONSTRAINT "course_week_topics_pkey" PRIMARY KEY ("id") ); CREATE TABLE "course_week_practices" ( "id" UUID NOT NULL DEFAULT gen_random_uuid(), "week_id" UUID NOT NULL, "title" TEXT NOT NULL, "description" TEXT NOT NULL, "sort_order" INTEGER NOT NULL, CONSTRAINT "course_week_practices_pkey" PRIMARY KEY ("id") ); CREATE UNIQUE INDEX "courses_slug_key" ON "courses"("slug"); CREATE INDEX "courses_is_active_idx" ON "courses"("is_active"); CREATE INDEX "courses_teacher_id_idx" ON "courses"("teacher_id"); CREATE UNIQUE INDEX "course_weeks_course_id_week_key" ON "course_weeks"("course_id", "week"); CREATE INDEX "course_weeks_course_id_idx" ON "course_weeks"("course_id"); CREATE UNIQUE INDEX "course_week_topics_week_id_sort_order_key" ON "course_week_topics"("week_id", "sort_order"); CREATE INDEX "course_week_topics_week_id_idx" ON "course_week_topics"("week_id"); CREATE UNIQUE INDEX "course_week_practices_week_id_sort_order_key" ON "course_week_practices"("week_id", "sort_order"); CREATE INDEX "course_week_practices_week_id_idx" ON "course_week_practices"("week_id"); ALTER TABLE "courses" ADD CONSTRAINT "courses_teacher_id_fkey" FOREIGN KEY ("teacher_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "course_weeks" ADD CONSTRAINT "course_weeks_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "courses"("id") ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE "course_week_topics" ADD CONSTRAINT "course_week_topics_week_id_fkey" FOREIGN KEY ("week_id") REFERENCES "course_weeks"("id") ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE "course_week_practices" ADD CONSTRAINT "course_week_practices_week_id_fkey" FOREIGN KEY ("week_id") REFERENCES "course_weeks"("id") ON DELETE CASCADE ON UPDATE CASCADE; COMMENT ON TABLE "courses" IS 'LMS 강의 마스터. 소속과 강사 기준으로 전체 강의 정보를 관리한다.'; COMMENT ON TABLE "course_weeks" IS '강의별 주차 커리큘럼 정보.'; COMMENT ON TABLE "course_week_topics" IS '주차별 학습 주제와 상세 설명.'; COMMENT ON TABLE "course_week_practices" IS '주차별 실습 내용.'; COMMENT ON COLUMN "courses"."organization_name" IS '강의를 등록한 소속명. 현재 퓨처드라이브 기준.'; COMMENT ON COLUMN "courses"."teacher_id" IS '강의를 등록한 강사 사용자 UUID.'; INSERT INTO "users" ("id", "email", "name", "createdAt", "updatedAt") VALUES (gen_random_uuid(), 'teacher@futuredrive.net', '퓨처드라이브 강사', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) ON CONFLICT ("email") DO UPDATE SET "name" = EXCLUDED."name", "updatedAt" = CURRENT_TIMESTAMP; WITH teacher AS ( SELECT "id" FROM "users" WHERE "email" = 'teacher@futuredrive.net' ) INSERT INTO "courses" ("slug", "title", "subtitle", "description", "organization_name", "teacher_id", "updated_at") SELECT 'futuredrive-autonomy-basic', '단계별 자율주행 알고리즘 학습 과정', '주차별 커리큘럼', 'ROS2, 센서 처리, SLAM, 객체 인식, 경로 계획까지 자율주행 알고리즘 개발 흐름을 주차별로 학습합니다.', '퓨처드라이브', teacher."id", CURRENT_TIMESTAMP FROM teacher ON CONFLICT ("slug") DO UPDATE SET "title" = EXCLUDED."title", "subtitle" = EXCLUDED."subtitle", "description" = EXCLUDED."description", "organization_name" = EXCLUDED."organization_name", "teacher_id" = EXCLUDED."teacher_id", "is_active" = true, "updated_at" = CURRENT_TIMESTAMP; WITH course AS ( SELECT "id" FROM "courses" WHERE "slug" = 'futuredrive-autonomy-basic' ) INSERT INTO "course_weeks" ("course_id", "week", "title", "tags", "progress", "state", "duration", "github", "overview", "deliverable", "updated_at") SELECT course."id", data."week", data."title", data."tags", data."progress", data."state", data."duration", data."github", data."overview", data."deliverable", CURRENT_TIMESTAMP FROM course CROSS JOIN ( VALUES (1, 'ROS2 기초 & 개발 환경 설정', ARRAY['시뮬레이션','완료']::TEXT[], 100, 'done', '8-10시간', '완료', 'ROS2 패키지 구조와 노드 실행 흐름을 이해하고, 실습 환경에서 기본 통신 구조를 직접 구성합니다.', '실습 노드 코드, 실행 로그, README 정리본을 GitHub에 제출'), (2, '센서 데이터 수집 및 처리', ARRAY['MK-mini','완료']::TEXT[], 100, 'done', '8-10시간', '완료', '자율주행 시스템에서 사용하는 LiDAR와 카메라 데이터를 수집하고, ROS2 토픽 기반으로 전처리하는 흐름을 학습합니다.', '센서 토픽 분석표, 전처리 코드, 결과 캡처 이미지를 포함한 제출물'), (3, 'SLAM & 지도 생성', ARRAY['MK-mini','진행중']::TEXT[], 65, 'current', '8-10시간', '미제출', 'Cartographer 기반 SLAM을 실행해 주행 환경 지도를 생성하고, 저장된 맵을 이용해 Localization 흐름을 검증합니다.', '생성된 map 파일, SLAM 실행 로그, Localization 검증 결과를 GitHub에 제출'), (4, '객체 인식 & 딥러닝', ARRAY['시뮬레이션','진행예정']::TEXT[], 0, 'locked', '-', '진행예정', '카메라 기반 객체 인식 모델을 실행하고, 인식 결과를 주행 판단에 연결하는 과정을 학습합니다.', '모델 실행 로그와 인식 결과 리포트 제출'), (5, '경로 계획 알고리즘', ARRAY['f1tenth','진행예정']::TEXT[], 0, 'locked', '-', '진행예정', '생성된 지도와 차량 상태를 기반으로 경로를 계획하고, 제어 명령으로 연결하는 전체 흐름을 정리합니다.', '경로 계획 설정값, 주행 결과 영상, 개선 사항 정리본 제출') ) AS data("week", "title", "tags", "progress", "state", "duration", "github", "overview", "deliverable") ON CONFLICT ("course_id", "week") DO UPDATE SET "title" = EXCLUDED."title", "tags" = EXCLUDED."tags", "progress" = EXCLUDED."progress", "state" = EXCLUDED."state", "duration" = EXCLUDED."duration", "github" = EXCLUDED."github", "overview" = EXCLUDED."overview", "deliverable" = EXCLUDED."deliverable", "updated_at" = CURRENT_TIMESTAMP; WITH rows AS ( SELECT w."id" AS week_id, topic.* FROM "course_weeks" w JOIN "courses" c ON c."id" = w."course_id" JOIN LATERAL ( VALUES (1, 1, 'ROS2 노드 생성', '패키지 생성, 노드 실행, 로그 확인까지 ROS2 개발의 기본 흐름을 실습합니다.'), (1, 2, '토픽 통신', 'Publisher와 Subscriber를 구성하고 메시지 타입, 주기, 토픽 이름을 확인합니다.'), (1, 3, '파라미터 설정', '노드 파라미터를 외부에서 변경하고 launch 파일로 실행 구성을 정리합니다.'), (2, 1, 'LiDAR 포인트클라우드', 'LiDAR 토픽을 확인하고 포인트클라우드 메시지 구조와 좌표 범위를 분석합니다.'), (2, 2, '카메라 이미지 처리', '카메라 프레임을 수신해 기본 전처리와 시각화 결과를 확인합니다.'), (2, 3, '센서 퓨전 기초', 'LiDAR와 카메라 데이터를 같은 주행 상황 기준으로 비교하고 센서 결합 조건을 정리합니다.'), (3, 1, 'Cartographer SLAM', 'Cartographer 설정을 확인하고 센서 입력 조건에 맞춰 SLAM launch를 실행합니다.'), (3, 2, '맵 저장/로드', '생성된 지도를 저장한 뒤 재로딩 과정에서 맵 파일과 설정값을 검증합니다.'), (3, 3, 'Localization', '저장된 맵 위에서 차량 위치 추정이 안정적으로 동작하는지 확인합니다.'), (4, 1, '객체 인식 모델 실행', '객체 인식 모델의 입력 이미지와 출력 박스 구조를 확인합니다.'), (4, 2, 'ROS2 메시지 변환', '검출 결과를 ROS2 메시지로 변환하고 후속 노드에서 사용할 수 있게 정리합니다.'), (4, 3, '인식 결과 시각화', '시뮬레이션 장면에서 인식 결과를 시각화하고 실패 케이스를 기록합니다.'), (5, 1, '전역 경로 생성', '지도 기반 전역 경로를 생성하고 목적지까지의 기준 경로를 확인합니다.'), (5, 2, '지역 경로 갱신', '주변 장애물과 차량 상태를 반영해 지역 경로를 갱신합니다.'), (5, 3, '제어 명령 연결', '계획된 경로를 제어 명령으로 연결하고 주행 결과를 비교합니다.') ) AS topic(week_no, sort_order, title, detail) ON topic.week_no = w."week" WHERE c."slug" = 'futuredrive-autonomy-basic' ) INSERT INTO "course_week_topics" ("week_id", "sort_order", "title", "detail") SELECT week_id, sort_order, title, detail FROM rows ON CONFLICT ("week_id", "sort_order") DO UPDATE SET "title" = EXCLUDED."title", "detail" = EXCLUDED."detail"; WITH week3 AS ( SELECT w."id" AS week_id FROM "course_weeks" w JOIN "courses" c ON c."id" = w."course_id" WHERE c."slug" = 'futuredrive-autonomy-basic' AND w."week" = 3 ) INSERT INTO "course_week_practices" ("week_id", "sort_order", "title", "description") SELECT week3.week_id, data.sort_order, data.title, data.description FROM week3 CROSS JOIN ( VALUES (1, 'SLAM 개요', '동시적 위치추정 및 지도작성의 기본 개념'), (2, 'Cartographer 설정', 'ROS2 Cartographer 패키지 설정 방법'), (3, '실습: 맵 생성', 'LiDAR 데이터를 활용한 실시간 맵핑'), (4, '위치 추정', '생성된 맵에서 로봇 위치 인식') ) AS data(sort_order, title, description) ON CONFLICT ("week_id", "sort_order") DO UPDATE SET "title" = EXCLUDED."title", "description" = EXCLUDED."description";