Compare commits

11 Commits

Author SHA1 Message Date
chyoo 196ba51ec6 patch curriculum.. 2026-08-07 10:27:19 +09:00
chyoo f8ccd8b0a3 feat: enable hybrid local and remote module execution 2026-08-06 16:17:41 +09:00
chyoo e06b853bcb fix: confirm module shutdown before disconnect or release 2026-08-04 13:11:26 +09:00
chyoo b7e910dcf7 patch.. 2026-08-03 14:03:00 +09:00
chyoo 5e7cffa901 feat: add global AI guide to LMS launcher 2026-07-15 14:08:46 +09:00
chyoo 860334a86b modify avatar url.. 2026-07-09 15:24:15 +09:00
chyoo 944d731573 r2 modify 2026-07-08 15:06:11 +09:00
chyoo 65b5a3c29c change module delete logic... 2026-07-08 13:56:01 +09:00
chyoo a195c7334b sotre profile avatars in R2.. 2026-07-08 11:41:53 +09:00
chyoo d6f1272afc use enrollment organization for submissions 2026-07-08 11:00:27 +09:00
chyoo 5a2ecd0fea Sync LMS backend source 2026-07-07 16:09:12 +09:00
156 changed files with 26535 additions and 1 deletions
+9
View File
@@ -0,0 +1,9 @@
node_modules
dist
coverage
.git
.env
npm-debug.log*
Dockerfile
docker-compose.yml
README.md
+10
View File
@@ -0,0 +1,10 @@
NODE_ENV=development
PORT=4400
CORS_ORIGIN=http://localhost:5173,http://localhost:5174,http://localhost:5175,http://localhost:5176
DATABASE_URL=postgresql://lms:future1234!%40@localhost:5432/lms?schema=public
AUTH_SERVER_URL=http://localhost:4100
GITEA_BASE_URL=http://localhost:3000
GITEA_ADMIN_TOKEN=
GITEA_STUDENT_OWNER=student
PLATFORM_GATEWAY_URL=http://localhost:4500
PLATFORM_GATEWAY_TOKEN=change-me
+11
View File
@@ -0,0 +1,11 @@
node_modules
dist
coverage
*.tsbuildinfo
.env
.env.*
!.env.example
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
+4
View File
@@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}
+22
View File
@@ -0,0 +1,22 @@
FROM node:22-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
RUN npm prune --omit=dev
FROM node:22-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/prisma ./prisma
COPY package*.json ./
EXPOSE 4400
CMD ["node", "dist/main.js"]
+38 -1
View File
@@ -1,3 +1,40 @@
# lms-be
lms 백엔드
Nest.js backend API for the LMS Electron frontend.
## Local development
```bash
npm install
cp .env.example .env
npm run prisma:generate
npm run start:dev
```
The API listens on `http://localhost:4400`.
- `GET /health`
- `GET /health/db`
- `GET /api`
## Database
Local PostgreSQL is included in Docker Compose.
```bash
docker compose up -d postgres
npm run prisma:migrate
npm run start:dev
```
For a separated database server, set `DATABASE_URL` to the remote PostgreSQL host:
```bash
DATABASE_URL=postgresql://USER:PASSWORD@DB_HOST:5432/DB_NAME?schema=public
```
## Docker
```bash
docker compose up --build
```
+46
View File
@@ -0,0 +1,46 @@
services:
lms-be:
build:
context: .
container_name: lms-be
environment:
NODE_ENV: production
PORT: 4400
CORS_ORIGIN: ${CORS_ORIGIN:-*}
DATABASE_URL: ${DATABASE_URL:-postgresql://lms:future1234!%40@postgres:5432/lms?schema=public}
AUTH_SERVER_URL: ${AUTH_SERVER_URL:-http://host.docker.internal:4100}
ports:
- "${PORT:-4400}:4400"
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:4400/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
postgres:
image: postgres:17-alpine
container_name: lms-postgres
command: postgres -c timezone=Asia/Seoul
environment:
POSTGRES_DB: ${POSTGRES_DB:-lms}
POSTGRES_USER: ${POSTGRES_USER:-lms}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-future1234!@}
ports:
- "${POSTGRES_PORT:-5432}:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-lms} -d ${POSTGRES_DB:-lms}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
volumes:
postgres_data:
+33
View File
@@ -0,0 +1,33 @@
import eslint from '@eslint/js';
import prettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{
ignores: ['eslint.config.mjs', 'dist/**', 'node_modules/**', 'coverage/**'],
},
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
prettierRecommended,
{
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
{
files: ['**/*.spec.ts', 'test/**/*.ts'],
rules: {
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
},
},
);
+25
View File
@@ -0,0 +1,25 @@
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: lms-api-strip
spec:
stripPrefix:
prefixes:
- /lms-api
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: lms-api
spec:
entryPoints:
- web
routes:
- match: Host(`workspace.autonomystudio.net`) && PathPrefix(`/lms-api`)
kind: Rule
middlewares:
- name: lms-api-strip
services:
- name: lms-be
port: 4400
+8
View File
@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}
+11082
View File
File diff suppressed because it is too large Load Diff
+79
View File
@@ -0,0 +1,79 @@
{
"name": "lms-be",
"version": "0.1.0",
"private": true,
"description": "Backend API for LMS",
"license": "UNLICENSED",
"scripts": {
"build": "prisma generate && nest build",
"start": "nest start",
"start:dev": "nest start --watch",
"start:prod": "node dist/main.js",
"lint": "eslint \"{src,test}/**/*.ts\" --fix",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev",
"prisma:deploy": "prisma migrate deploy",
"prisma:studio": "prisma studio",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.1081.0",
"@nestjs/common": "^11.1.24",
"@nestjs/config": "^4.0.4",
"@nestjs/core": "^11.1.24",
"@nestjs/platform-express": "^11.1.24",
"@nestjs/platform-socket.io": "^11.1.24",
"@nestjs/websockets": "^11.1.24",
"@prisma/client": "^6.19.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.15.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2",
"socket.io": "^4.8.3"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@nestjs/cli": "^11.0.21",
"@nestjs/schematics": "^11.1.0",
"@nestjs/testing": "^11.1.24",
"@types/express": "^5.0.6",
"@types/jest": "^30.0.0",
"@types/node": "^25.9.1",
"@types/supertest": "^7.2.0",
"eslint": "^10.4.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.6",
"globals": "^17.6.0",
"jest": "^30.4.2",
"prettier": "^3.8.3",
"prisma": "^6.19.3",
"source-map-support": "^0.5.21",
"supertest": "^7.2.2",
"ts-jest": "^29.4.11",
"ts-loader": "^9.6.0",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^6.0.3",
"typescript-eslint": "^8.60.0"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": ".",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"src/**/*.(t|j)s"
],
"coverageDirectory": "coverage",
"testEnvironment": "node"
}
}
@@ -0,0 +1,13 @@
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"name" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
@@ -0,0 +1,22 @@
-- CreateTable
CREATE TABLE "lms_notifications" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"body" TEXT NOT NULL,
"category" TEXT NOT NULL DEFAULT 'notice',
"is_active" BOOLEAN NOT NULL DEFAULT true,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "lms_notifications_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "lms_notifications_is_active_created_at_idx" ON "lms_notifications"("is_active", "created_at");
-- Seed
INSERT INTO "lms_notifications" ("id", "title", "body", "category", "created_at", "updated_at")
VALUES
('lms-notification-course-review', '신규 과제 검토 요청', '자율주행 인지 실습 과정에 제출된 과제 3건의 검토가 필요합니다.', 'assignment', '2026-06-02T00:00:00.000Z', CURRENT_TIMESTAMP),
('lms-notification-live-session', '라이브 세션 일정 안내', '오늘 15:00에 SLAM 기초 라이브 세션이 예정되어 있습니다.', 'schedule', '2026-06-01T00:00:00.000Z', CURRENT_TIMESTAMP),
('lms-notification-system-maintenance', '시스템 점검 안내', '6월 5일 02:00부터 03:00까지 LMS 점검이 진행됩니다.', 'system', '2026-05-31T00:00:00.000Z', CURRENT_TIMESTAMP);
@@ -0,0 +1,21 @@
-- AlterTable
ALTER TABLE "lms_notifications" ADD COLUMN "audience" TEXT NOT NULL DEFAULT 'all';
-- Existing admin messages are instructor-facing.
UPDATE "lms_notifications"
SET "audience" = 'instructor'
WHERE "id" IN (
'lms-notification-course-review',
'lms-notification-live-session',
'lms-notification-system-maintenance'
);
-- Seed student messages.
INSERT INTO "lms_notifications" ("id", "title", "body", "category", "audience", "created_at", "updated_at")
VALUES
('lms-student-notification-assignment-due', '과제 제출 마감 안내', '인지 실습 리포트 제출 마감이 오늘 18:00까지입니다.', 'assignment', 'student', '2026-06-02T00:00:00.000Z', CURRENT_TIMESTAMP),
('lms-student-notification-new-lesson', '신규 강의 공개', '자율주행 센서 융합 기초 강의가 새로 공개되었습니다.', 'course', 'student', '2026-06-01T00:00:00.000Z', CURRENT_TIMESTAMP),
('lms-student-notification-session', '실습 세션 안내', '내일 10:00에 ROS2 실습 세션이 예정되어 있습니다.', 'schedule', 'student', '2026-05-31T00:00:00.000Z', CURRENT_TIMESTAMP);
-- CreateIndex
CREATE INDEX "lms_notifications_audience_is_active_created_at_idx" ON "lms_notifications"("audience", "is_active", "created_at");
@@ -0,0 +1,16 @@
COMMENT ON TABLE "users" IS 'LMS 내부 사용자 기본 정보';
COMMENT ON COLUMN "users"."id" IS 'LMS 사용자 고유 ID';
COMMENT ON COLUMN "users"."email" IS '사용자 이메일';
COMMENT ON COLUMN "users"."name" IS '사용자 이름';
COMMENT ON COLUMN "users"."createdAt" IS '사용자 생성 일시';
COMMENT ON COLUMN "users"."updatedAt" IS '사용자 수정 일시';
COMMENT ON TABLE "lms_notifications" IS 'LMS 학생/강사/전체 대상 알림 메시지';
COMMENT ON COLUMN "lms_notifications"."id" IS 'LMS 알림 고유 ID';
COMMENT ON COLUMN "lms_notifications"."title" IS '알림 제목';
COMMENT ON COLUMN "lms_notifications"."body" IS '알림 본문';
COMMENT ON COLUMN "lms_notifications"."category" IS '알림 분류';
COMMENT ON COLUMN "lms_notifications"."audience" IS '알림 대상. student, instructor, all 등을 사용';
COMMENT ON COLUMN "lms_notifications"."is_active" IS '알림 활성 여부';
COMMENT ON COLUMN "lms_notifications"."created_at" IS '알림 생성 일시';
COMMENT ON COLUMN "lms_notifications"."updated_at" IS '알림 수정 일시';
@@ -0,0 +1,15 @@
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
UPDATE "users"
SET "id" = gen_random_uuid()::text
WHERE "id" !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$';
UPDATE "lms_notifications"
SET "id" = gen_random_uuid()::text
WHERE "id" !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$';
ALTER TABLE "users"
ALTER COLUMN "id" TYPE uuid USING "id"::uuid;
ALTER TABLE "lms_notifications"
ALTER COLUMN "id" TYPE uuid USING "id"::uuid;
@@ -0,0 +1,6 @@
ALTER TABLE "users"
ADD COLUMN "auth_user_id" uuid;
CREATE UNIQUE INDEX "users_auth_user_id_key" ON "users"("auth_user_id");
COMMENT ON COLUMN "users"."auth_user_id" IS '중앙 auth DB users.id와 매핑되는 사용자 ID';
@@ -0,0 +1 @@
DROP TABLE IF EXISTS "lms_notifications";
@@ -0,0 +1,177 @@
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";
@@ -0,0 +1,119 @@
CREATE TABLE "platforms" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"platform_key" TEXT NOT NULL,
"display_name" TEXT NOT NULL,
"description" TEXT NOT NULL,
"organization_name" TEXT NOT NULL,
"icon_key" TEXT 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 "platforms_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "course_platforms" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"course_id" UUID NOT NULL,
"platform_id" UUID NOT NULL,
"status" TEXT NOT NULL,
"action" TEXT NOT NULL,
"state" TEXT NOT NULL,
"sort_order" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "course_platforms_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "course_student_progress" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"course_id" UUID NOT NULL,
"user_id" UUID NOT NULL,
"completed_weeks" INTEGER NOT NULL DEFAULT 0,
"total_weeks" INTEGER NOT NULL DEFAULT 0,
"learning_hours" INTEGER NOT NULL DEFAULT 0,
"commit_count" INTEGER NOT NULL DEFAULT 0,
"average_score" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "course_student_progress_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "platforms_platform_key_key" ON "platforms"("platform_key");
CREATE INDEX "platforms_organization_name_idx" ON "platforms"("organization_name");
CREATE INDEX "platforms_is_active_idx" ON "platforms"("is_active");
CREATE UNIQUE INDEX "course_platforms_course_id_platform_id_key" ON "course_platforms"("course_id", "platform_id");
CREATE INDEX "course_platforms_course_id_sort_order_idx" ON "course_platforms"("course_id", "sort_order");
CREATE INDEX "course_platforms_platform_id_idx" ON "course_platforms"("platform_id");
CREATE UNIQUE INDEX "course_student_progress_course_id_user_id_key" ON "course_student_progress"("course_id", "user_id");
CREATE INDEX "course_student_progress_user_id_idx" ON "course_student_progress"("user_id");
ALTER TABLE "course_platforms" ADD CONSTRAINT "course_platforms_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "courses"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "course_platforms" ADD CONSTRAINT "course_platforms_platform_id_fkey" FOREIGN KEY ("platform_id") REFERENCES "platforms"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "course_student_progress" ADD CONSTRAINT "course_student_progress_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "courses"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "course_student_progress" ADD CONSTRAINT "course_student_progress_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
COMMENT ON TABLE "platforms" IS '최고관리자가 등록하는 실습 플랫폼 마스터. 학교/기관별 개별 장비를 구분한다.';
COMMENT ON TABLE "course_platforms" IS '강사가 강의에 사용할 플랫폼을 선택해 배정한 목록.';
COMMENT ON TABLE "course_student_progress" IS '학생별 강의 학습 현황 통계.';
INSERT INTO "users" ("id", "email", "name", "createdAt", "updatedAt")
VALUES (gen_random_uuid(), 'student@futuredrive.net', '퓨처드라이브 학생', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
ON CONFLICT ("email") DO UPDATE
SET "name" = EXCLUDED."name",
"updatedAt" = CURRENT_TIMESTAMP;
INSERT INTO "platforms" ("platform_key", "display_name", "description", "organization_name", "icon_key", "updated_at")
VALUES
('futuredrive-local-pc', '로컬 PC', '내 PC에서 직접 개발 환경을 실행합니다.', '퓨처드라이브', 'laptop', CURRENT_TIMESTAMP),
('futuredrive-simulator-01', '시뮬레이터', '가상 환경에서 안전하게 테스트합니다.', '퓨처드라이브', 'monitor', CURRENT_TIMESTAMP),
('futuredrive-f1tenth-01', 'F1Tenth #01', '고속 주행 및 회피 알고리즘 실습 장비입니다.', '퓨처드라이브', 'car', CURRENT_TIMESTAMP),
('futuredrive-mkmini-02', 'MK-mini #02', '기초 SLAM 및 자율주행 실습 장비입니다.', '퓨처드라이브', 'car', CURRENT_TIMESTAMP)
ON CONFLICT ("platform_key") DO UPDATE
SET "display_name" = EXCLUDED."display_name",
"description" = EXCLUDED."description",
"organization_name" = EXCLUDED."organization_name",
"icon_key" = EXCLUDED."icon_key",
"is_active" = true,
"updated_at" = CURRENT_TIMESTAMP;
WITH course AS (
SELECT "id" FROM "courses" WHERE "slug" = 'futuredrive-autonomy-basic'
), data AS (
SELECT *
FROM (VALUES
('futuredrive-local-pc', '클라이언트 실행 시 제어권이 부여됩니다', '연결', 'ready', 1),
('futuredrive-simulator-01', '사용 가능', '제어권 요청', 'ready', 2),
('futuredrive-f1tenth-01', '타 학생 사용 중', '대기 예약', 'reserved', 3),
('futuredrive-mkmini-02', '접근 권한 없음', '강사에게 권한 요청', 'locked', 4)
) AS v(platform_key, status, action, state, sort_order)
)
INSERT INTO "course_platforms" ("course_id", "platform_id", "status", "action", "state", "sort_order", "updated_at")
SELECT course."id", p."id", data.status, data.action, data.state, data.sort_order, CURRENT_TIMESTAMP
FROM course
JOIN data ON true
JOIN "platforms" p ON p."platform_key" = data.platform_key
ON CONFLICT ("course_id", "platform_id") DO UPDATE
SET "status" = EXCLUDED."status",
"action" = EXCLUDED."action",
"state" = EXCLUDED."state",
"sort_order" = EXCLUDED."sort_order",
"updated_at" = CURRENT_TIMESTAMP;
WITH course AS (
SELECT "id" FROM "courses" WHERE "slug" = 'futuredrive-autonomy-basic'
), student AS (
SELECT "id" FROM "users" WHERE "email" = 'student@futuredrive.net'
)
INSERT INTO "course_student_progress" ("course_id", "user_id", "completed_weeks", "total_weeks", "learning_hours", "commit_count", "average_score", "updated_at")
SELECT course."id", student."id", 2, 5, 42, 18, 85, CURRENT_TIMESTAMP
FROM course, student
ON CONFLICT ("course_id", "user_id") DO UPDATE
SET "completed_weeks" = EXCLUDED."completed_weeks",
"total_weeks" = EXCLUDED."total_weeks",
"learning_hours" = EXCLUDED."learning_hours",
"commit_count" = EXCLUDED."commit_count",
"average_score" = EXCLUDED."average_score",
"updated_at" = CURRENT_TIMESTAMP;
@@ -0,0 +1,82 @@
COMMENT ON TABLE "users" IS 'LMS 내부 사용자 기본 정보. 중앙 auth 사용자와 동기화해 앱별 데이터를 연결한다.';
COMMENT ON COLUMN "users"."id" IS 'LMS 사용자 고유 UUID.';
COMMENT ON COLUMN "users"."auth_user_id" IS '중앙 auth DB users.id와 매핑되는 사용자 UUID.';
COMMENT ON COLUMN "users"."email" IS '사용자 로그인 이메일.';
COMMENT ON COLUMN "users"."name" IS '사용자 표시 이름.';
COMMENT ON COLUMN "users"."createdAt" IS '사용자 데이터 생성 일시.';
COMMENT ON COLUMN "users"."updatedAt" IS '사용자 데이터 수정 일시.';
COMMENT ON TABLE "courses" IS 'LMS 강의 마스터. 소속과 강사 기준으로 전체 강의 정보를 관리한다.';
COMMENT ON COLUMN "courses"."id" IS '강의 고유 UUID.';
COMMENT ON COLUMN "courses"."slug" IS '강의 식별용 문자열 키.';
COMMENT ON COLUMN "courses"."title" IS '강의 제목.';
COMMENT ON COLUMN "courses"."subtitle" IS '강의 부제목.';
COMMENT ON COLUMN "courses"."description" IS '강의 설명.';
COMMENT ON COLUMN "courses"."organization_name" IS '강의를 운영하는 소속명.';
COMMENT ON COLUMN "courses"."teacher_id" IS '강의를 등록하거나 운영하는 강사 사용자 UUID.';
COMMENT ON COLUMN "courses"."is_active" IS '강의 활성 여부.';
COMMENT ON COLUMN "courses"."created_at" IS '강의 생성 일시.';
COMMENT ON COLUMN "courses"."updated_at" IS '강의 수정 일시.';
COMMENT ON TABLE "course_weeks" IS '강의별 주차 커리큘럼 정보.';
COMMENT ON COLUMN "course_weeks"."id" IS '주차 커리큘럼 고유 UUID.';
COMMENT ON COLUMN "course_weeks"."course_id" IS '소속 강의 UUID.';
COMMENT ON COLUMN "course_weeks"."week" IS '강의 내 주차 번호.';
COMMENT ON COLUMN "course_weeks"."title" IS '주차 제목.';
COMMENT ON COLUMN "course_weeks"."tags" IS '주차 상태 및 분류 태그 목록.';
COMMENT ON COLUMN "course_weeks"."progress" IS '주차 기준 진행률.';
COMMENT ON COLUMN "course_weeks"."state" IS '주차 상태. done, current, locked 등을 사용한다.';
COMMENT ON COLUMN "course_weeks"."duration" IS '예상 학습 시간 표시값.';
COMMENT ON COLUMN "course_weeks"."github" IS 'GitHub 제출 상태 표시값.';
COMMENT ON COLUMN "course_weeks"."overview" IS '주차 개요 설명.';
COMMENT ON COLUMN "course_weeks"."deliverable" IS '주차 제출 기준.';
COMMENT ON COLUMN "course_weeks"."created_at" IS '주차 커리큘럼 생성 일시.';
COMMENT ON COLUMN "course_weeks"."updated_at" IS '주차 커리큘럼 수정 일시.';
COMMENT ON TABLE "course_week_topics" IS '주차별 학습 주제와 상세 설명.';
COMMENT ON COLUMN "course_week_topics"."id" IS '주차 학습 주제 고유 UUID.';
COMMENT ON COLUMN "course_week_topics"."week_id" IS '소속 주차 커리큘럼 UUID.';
COMMENT ON COLUMN "course_week_topics"."title" IS '학습 주제 제목.';
COMMENT ON COLUMN "course_week_topics"."detail" IS '학습 주제 상세 설명.';
COMMENT ON COLUMN "course_week_topics"."sort_order" IS '주차 내 학습 주제 표시 순서.';
COMMENT ON TABLE "course_week_practices" IS '주차별 실습 내용.';
COMMENT ON COLUMN "course_week_practices"."id" IS '주차 실습 내용 고유 UUID.';
COMMENT ON COLUMN "course_week_practices"."week_id" IS '소속 주차 커리큘럼 UUID.';
COMMENT ON COLUMN "course_week_practices"."title" IS '실습 항목 제목.';
COMMENT ON COLUMN "course_week_practices"."description" IS '실습 항목 설명.';
COMMENT ON COLUMN "course_week_practices"."sort_order" IS '주차 내 실습 항목 표시 순서.';
COMMENT ON TABLE "platforms" IS '최고관리자가 등록하는 실습 플랫폼 마스터. 학교/기관별 개별 장비를 구분한다.';
COMMENT ON COLUMN "platforms"."id" IS '실습 플랫폼 고유 UUID.';
COMMENT ON COLUMN "platforms"."platform_key" IS '플랫폼 식별용 문자열 키. 기관과 장비를 구분할 수 있어야 한다.';
COMMENT ON COLUMN "platforms"."display_name" IS '화면에 표시할 플랫폼 이름.';
COMMENT ON COLUMN "platforms"."description" IS '플랫폼 설명.';
COMMENT ON COLUMN "platforms"."organization_name" IS '플랫폼을 소유하거나 운영하는 소속명.';
COMMENT ON COLUMN "platforms"."icon_key" IS '프론트엔드에서 사용할 아이콘 키.';
COMMENT ON COLUMN "platforms"."is_active" IS '플랫폼 활성 여부.';
COMMENT ON COLUMN "platforms"."created_at" IS '플랫폼 생성 일시.';
COMMENT ON COLUMN "platforms"."updated_at" IS '플랫폼 수정 일시.';
COMMENT ON TABLE "course_platforms" IS '강사가 강의에 사용할 플랫폼을 선택해 배정한 목록.';
COMMENT ON COLUMN "course_platforms"."id" IS '강의-플랫폼 배정 고유 UUID.';
COMMENT ON COLUMN "course_platforms"."course_id" IS '플랫폼을 배정한 강의 UUID.';
COMMENT ON COLUMN "course_platforms"."platform_id" IS '강의에 배정된 플랫폼 UUID.';
COMMENT ON COLUMN "course_platforms"."status" IS '플랫폼 사용 가능 상태 설명.';
COMMENT ON COLUMN "course_platforms"."action" IS '플랫폼 버튼에 표시할 작업명.';
COMMENT ON COLUMN "course_platforms"."state" IS '플랫폼 상태. ready, reserved, locked 등을 사용한다.';
COMMENT ON COLUMN "course_platforms"."sort_order" IS '강의 내 플랫폼 표시 순서.';
COMMENT ON COLUMN "course_platforms"."created_at" IS '강의-플랫폼 배정 생성 일시.';
COMMENT ON COLUMN "course_platforms"."updated_at" IS '강의-플랫폼 배정 수정 일시.';
COMMENT ON TABLE "course_student_progress" IS '학생별 강의 학습 현황 통계.';
COMMENT ON COLUMN "course_student_progress"."id" IS '학생 학습 현황 고유 UUID.';
COMMENT ON COLUMN "course_student_progress"."course_id" IS '학습 현황이 속한 강의 UUID.';
COMMENT ON COLUMN "course_student_progress"."user_id" IS '학습 현황 대상 학생 사용자 UUID.';
COMMENT ON COLUMN "course_student_progress"."completed_weeks" IS '완료한 주차 수.';
COMMENT ON COLUMN "course_student_progress"."total_weeks" IS '전체 주차 수.';
COMMENT ON COLUMN "course_student_progress"."learning_hours" IS '누적 학습 시간.';
COMMENT ON COLUMN "course_student_progress"."commit_count" IS '누적 커밋 수.';
COMMENT ON COLUMN "course_student_progress"."average_score" IS '평균 점수.';
COMMENT ON COLUMN "course_student_progress"."created_at" IS '학생 학습 현황 생성 일시.';
COMMENT ON COLUMN "course_student_progress"."updated_at" IS '학생 학습 현황 수정 일시.';
@@ -0,0 +1,43 @@
UPDATE "users"
SET "auth_user_id" = '54803583-0a04-44f4-9398-78478239c566',
"name" = 'ATNMY Studio Admin',
"updatedAt" = CURRENT_TIMESTAMP
WHERE "email" = 'teacher@futuredrive.net';
INSERT INTO "users" ("id", "auth_user_id", "email", "name", "createdAt", "updatedAt")
VALUES (
gen_random_uuid(),
'a8e7f37e-ef63-4912-a412-b4874b91ee67',
'chyoo@futuredrive.net',
'유창훈',
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
)
ON CONFLICT ("email") DO UPDATE
SET "auth_user_id" = EXCLUDED."auth_user_id",
"name" = EXCLUDED."name",
"updatedAt" = CURRENT_TIMESTAMP;
WITH course AS (
SELECT "id" FROM "courses" WHERE "slug" = 'futuredrive-autonomy-basic'
), learner AS (
SELECT "id" FROM "users" WHERE "email" = 'chyoo@futuredrive.net'
)
INSERT INTO "course_student_progress" ("course_id", "user_id", "completed_weeks", "total_weeks", "learning_hours", "commit_count", "average_score", "updated_at")
SELECT course."id", learner."id", 2, 5, 42, 18, 85, CURRENT_TIMESTAMP
FROM course, learner
ON CONFLICT ("course_id", "user_id") DO UPDATE
SET "completed_weeks" = EXCLUDED."completed_weeks",
"total_weeks" = EXCLUDED."total_weeks",
"learning_hours" = EXCLUDED."learning_hours",
"commit_count" = EXCLUDED."commit_count",
"average_score" = EXCLUDED."average_score",
"updated_at" = CURRENT_TIMESTAMP;
DELETE FROM "course_student_progress"
WHERE "user_id" IN (
SELECT "id" FROM "users" WHERE "email" = 'student@futuredrive.net'
);
DELETE FROM "users"
WHERE "email" = 'student@futuredrive.net';
@@ -0,0 +1,4 @@
UPDATE "users"
SET "auth_user_id" = NULL,
"updatedAt" = CURRENT_TIMESTAMP
WHERE "email" IN ('teacher@futuredrive.net', 'chyoo@futuredrive.net');
@@ -0,0 +1,7 @@
DELETE FROM "course_student_progress"
WHERE "user_id" IN (
SELECT "id" FROM "users" WHERE "email" = 'chyoo@futuredrive.net'
);
DELETE FROM "users"
WHERE "email" = 'chyoo@futuredrive.net';
@@ -0,0 +1,72 @@
CREATE TYPE "ChatThreadType" AS ENUM ('AI', 'INSTRUCTOR', 'MIXED');
CREATE TYPE "ChatThreadStatus" AS ENUM ('OPEN', 'WAITING_INSTRUCTOR', 'RESOLVED', 'ARCHIVED');
CREATE TYPE "ChatSenderType" AS ENUM ('STUDENT', 'AI', 'INSTRUCTOR', 'SYSTEM');
CREATE TYPE "ChatContentFormat" AS ENUM ('TEXT', 'MARKDOWN');
CREATE TYPE "AiChatRunStatus" AS ENUM ('SUCCEEDED', 'FAILED');
CREATE TABLE "chat_threads" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"course_id" UUID NOT NULL,
"week_id" UUID,
"student_id" UUID NOT NULL,
"assigned_instructor_id" UUID,
"type" "ChatThreadType" NOT NULL,
"status" "ChatThreadStatus" NOT NULL DEFAULT 'OPEN',
"title" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"last_message_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "chat_threads_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "chat_messages" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"thread_id" UUID NOT NULL,
"sender_type" "ChatSenderType" NOT NULL,
"sender_user_id" UUID,
"content" TEXT NOT NULL,
"content_format" "ChatContentFormat" NOT NULL DEFAULT 'TEXT',
"metadata" JSONB,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMP(3),
CONSTRAINT "chat_messages_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "ai_chat_runs" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"message_id" UUID NOT NULL,
"model" TEXT NOT NULL,
"prompt_tokens" INTEGER,
"completion_tokens" INTEGER,
"total_tokens" INTEGER,
"latency_ms" INTEGER,
"status" "AiChatRunStatus" NOT NULL,
"error" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ai_chat_runs_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "chat_threads_course_id_type_last_message_at_idx" ON "chat_threads"("course_id", "type", "last_message_at");
CREATE INDEX "chat_threads_student_id_type_last_message_at_idx" ON "chat_threads"("student_id", "type", "last_message_at");
CREATE INDEX "chat_threads_week_id_idx" ON "chat_threads"("week_id");
CREATE INDEX "chat_threads_assigned_instructor_id_idx" ON "chat_threads"("assigned_instructor_id");
CREATE INDEX "chat_messages_thread_id_created_at_idx" ON "chat_messages"("thread_id", "created_at");
CREATE INDEX "chat_messages_sender_user_id_idx" ON "chat_messages"("sender_user_id");
CREATE UNIQUE INDEX "ai_chat_runs_message_id_key" ON "ai_chat_runs"("message_id");
CREATE INDEX "ai_chat_runs_status_created_at_idx" ON "ai_chat_runs"("status", "created_at");
ALTER TABLE "chat_threads" ADD CONSTRAINT "chat_threads_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "courses"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "chat_threads" ADD CONSTRAINT "chat_threads_week_id_fkey" FOREIGN KEY ("week_id") REFERENCES "course_weeks"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "chat_threads" ADD CONSTRAINT "chat_threads_student_id_fkey" FOREIGN KEY ("student_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "chat_threads" ADD CONSTRAINT "chat_threads_assigned_instructor_id_fkey" FOREIGN KEY ("assigned_instructor_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "chat_messages" ADD CONSTRAINT "chat_messages_thread_id_fkey" FOREIGN KEY ("thread_id") REFERENCES "chat_threads"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "chat_messages" ADD CONSTRAINT "chat_messages_sender_user_id_fkey" FOREIGN KEY ("sender_user_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "ai_chat_runs" ADD CONSTRAINT "ai_chat_runs_message_id_fkey" FOREIGN KEY ("message_id") REFERENCES "chat_messages"("id") ON DELETE CASCADE ON UPDATE CASCADE;
COMMENT ON TABLE "chat_threads" IS '학생 질의응답 대화방. AI, 강사, 혼합 대화의 상태와 연결 주차를 관리한다.';
COMMENT ON TABLE "chat_messages" IS '질의응답 대화방의 개별 메시지 기록.';
COMMENT ON TABLE "ai_chat_runs" IS 'AI 응답 생성 실행 기록. 모델, 토큰, 지연 시간, 오류를 추적한다.';
COMMENT ON COLUMN "chat_messages"."metadata" IS '코드 파일, 터미널 출력, 선택 모듈 등 메시지와 함께 보낸 추가 컨텍스트.';
@@ -0,0 +1,19 @@
DO $$
DECLARE
column_record record;
BEGIN
FOR column_record IN
SELECT table_schema, table_name, column_name
FROM information_schema.columns
WHERE table_schema = 'public'
AND data_type = 'timestamp without time zone'
LOOP
EXECUTE format(
'ALTER TABLE %I.%I ALTER COLUMN %I TYPE timestamptz USING %I AT TIME ZONE ''UTC''',
column_record.table_schema,
column_record.table_name,
column_record.column_name,
column_record.column_name
);
END LOOP;
END $$;
@@ -0,0 +1,10 @@
ALTER TABLE "users"
ADD COLUMN "auth_role" TEXT,
ADD COLUMN "primary_organization_id" UUID,
ADD COLUMN "primary_organization_name" TEXT,
ADD COLUMN "primary_organization_type" TEXT,
ADD COLUMN "department" TEXT,
ADD COLUMN "position" TEXT;
CREATE INDEX "users_auth_role_idx" ON "users"("auth_role");
CREATE INDEX "users_primary_organization_id_idx" ON "users"("primary_organization_id");
@@ -0,0 +1,73 @@
CREATE TABLE "student_groups" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"organization_id" UUID,
"organization_name" TEXT NOT NULL,
"name" TEXT NOT NULL,
"week" INTEGER NOT NULL DEFAULT 1,
"status" TEXT NOT NULL DEFAULT '진행중',
"platform_name" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "student_groups_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "student_group_members" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"group_id" UUID NOT NULL,
"user_id" UUID NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "student_group_members_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "student_groups_organization_name_name_key" ON "student_groups"("organization_name", "name");
CREATE INDEX "student_groups_organization_id_idx" ON "student_groups"("organization_id");
CREATE INDEX "student_groups_organization_name_idx" ON "student_groups"("organization_name");
CREATE UNIQUE INDEX "student_group_members_group_id_user_id_key" ON "student_group_members"("group_id", "user_id");
CREATE INDEX "student_group_members_user_id_idx" ON "student_group_members"("user_id");
ALTER TABLE "student_group_members"
ADD CONSTRAINT "student_group_members_group_id_fkey"
FOREIGN KEY ("group_id") REFERENCES "student_groups"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "student_group_members"
ADD CONSTRAINT "student_group_members_user_id_fkey"
FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
INSERT INTO "student_groups" ("organization_id", "organization_name", "name", "week", "status", "platform_name")
SELECT org."organization_id", org."organization_name", seed."name", seed."week", '진행중', seed."platform_name"
FROM (
VALUES
('그룹 A', 3, 'MK-mini #01'),
('그룹 B', 2, '시뮬레이션'),
('그룹 C', 4, 'f1tenth #01')
) AS seed("name", "week", "platform_name")
CROSS JOIN (
SELECT DISTINCT "primary_organization_id" AS "organization_id", "primary_organization_name" AS "organization_name"
FROM "users"
WHERE "primary_organization_name" IS NOT NULL
) org
ON CONFLICT ("organization_name", "name") DO NOTHING;
INSERT INTO "student_group_members" ("group_id", "user_id")
SELECT g."id", u."id"
FROM "student_groups" g
JOIN "users" u ON u."primary_organization_name" = g."organization_name"
WHERE g."name" = '그룹 A'
AND u."email" IN ('kms@yu.ac.kr', 'lsy@yu.ac.kr', 'cyj@yu.ac.kr')
ON CONFLICT ("group_id", "user_id") DO NOTHING;
INSERT INTO "student_group_members" ("group_id", "user_id")
SELECT g."id", u."id"
FROM "student_groups" g
JOIN "users" u ON u."primary_organization_name" = g."organization_name"
WHERE g."name" = '그룹 B'
AND u."email" IN ('jyr@yu.ac.kr', 'pjh@yu.ac.kr')
ON CONFLICT ("group_id", "user_id") DO NOTHING;
INSERT INTO "student_group_members" ("group_id", "user_id")
SELECT g."id", u."id"
FROM "student_groups" g
JOIN "users" u ON u."primary_organization_name" = g."organization_name"
WHERE g."name" = '그룹 C'
AND u."email" IN ('hdy@yu.ac.kr', 'ojh@yu.ac.kr', 'ysj@yu.ac.kr', 'kdh@yu.ac.kr')
ON CONFLICT ("group_id", "user_id") DO NOTHING;
@@ -0,0 +1,29 @@
CREATE TABLE "student_enrollments" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"user_id" UUID NOT NULL,
"organization_id" UUID,
"organization_name" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'ACTIVE',
"removed_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "student_enrollments_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "student_enrollments_user_id_organization_name_key" ON "student_enrollments"("user_id", "organization_name");
CREATE INDEX "student_enrollments_organization_id_status_idx" ON "student_enrollments"("organization_id", "status");
CREATE INDEX "student_enrollments_organization_name_status_idx" ON "student_enrollments"("organization_name", "status");
ALTER TABLE "student_enrollments" ADD CONSTRAINT "student_enrollments_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
INSERT INTO "student_enrollments" ("user_id", "organization_id", "organization_name", "status")
SELECT
u."id",
u."primary_organization_id",
u."primary_organization_name",
'ACTIVE'
FROM "users" u
WHERE u."primary_organization_name" IS NOT NULL
AND (u."auth_role" IS NULL OR u."auth_role" IN ('USER', 'STUDENT'))
ON CONFLICT ("user_id", "organization_name") DO NOTHING;
@@ -0,0 +1,37 @@
CREATE TABLE "instructor_platforms" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"organization_id" UUID,
"organization_name" TEXT NOT NULL,
"platform_id" UUID NOT NULL,
"display_name" TEXT NOT NULL,
"instance_no" INTEGER NOT NULL,
"status" TEXT NOT NULL DEFAULT '대기',
"battery" INTEGER NOT NULL DEFAULT 100,
"uptime_minutes" INTEGER NOT NULL DEFAULT 0,
"speed_meters" DECIMAL(6, 2) NOT NULL DEFAULT 0,
"error_count" INTEGER NOT NULL DEFAULT 0,
"emergency_stop" BOOLEAN NOT NULL DEFAULT false,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL,
CONSTRAINT "instructor_platforms_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "instructor_platforms_organization_name_display_name_key"
ON "instructor_platforms"("organization_name", "display_name");
CREATE UNIQUE INDEX "instructor_platforms_organization_name_platform_id_instance_no_key"
ON "instructor_platforms"("organization_name", "platform_id", "instance_no");
CREATE INDEX "instructor_platforms_organization_id_idx"
ON "instructor_platforms"("organization_id");
CREATE INDEX "instructor_platforms_organization_name_idx"
ON "instructor_platforms"("organization_name");
CREATE INDEX "instructor_platforms_platform_id_idx"
ON "instructor_platforms"("platform_id");
ALTER TABLE "instructor_platforms"
ADD CONSTRAINT "instructor_platforms_platform_id_fkey"
FOREIGN KEY ("platform_id") REFERENCES "platforms"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,25 @@
CREATE TABLE "instructor_curriculum_settings" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"organization_id" UUID,
"organization_name" TEXT NOT NULL,
"course_id" UUID NOT NULL,
"active_week" INTEGER NOT NULL DEFAULT 1,
"active_learning_step" TEXT,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL,
CONSTRAINT "instructor_curriculum_settings_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "instructor_curriculum_settings_organization_name_key"
ON "instructor_curriculum_settings"("organization_name");
CREATE INDEX "instructor_curriculum_settings_organization_id_idx"
ON "instructor_curriculum_settings"("organization_id");
CREATE INDEX "instructor_curriculum_settings_course_id_idx"
ON "instructor_curriculum_settings"("course_id");
ALTER TABLE "instructor_curriculum_settings"
ADD CONSTRAINT "instructor_curriculum_settings_course_id_fkey"
FOREIGN KEY ("course_id") REFERENCES "courses"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,37 @@
UPDATE "courses"
SET "organization_name" = '공통'
WHERE "slug" = 'futuredrive-autonomy-basic'
AND "organization_name" = '퓨처드라이브';
CREATE TABLE "instructor_curriculum_week_overrides" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"organization_id" UUID,
"organization_name" TEXT NOT NULL,
"course_id" UUID NOT NULL,
"week" INTEGER NOT NULL,
"title" TEXT,
"overview" TEXT,
"deliverable" TEXT,
"topics" JSONB,
"topic_details" JSONB,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL,
CONSTRAINT "instructor_curriculum_week_overrides_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "instructor_curriculum_week_overrides_organization_name_course_id_week_key"
ON "instructor_curriculum_week_overrides"("organization_name", "course_id", "week");
CREATE INDEX "instructor_curriculum_week_overrides_organization_id_idx"
ON "instructor_curriculum_week_overrides"("organization_id");
CREATE INDEX "instructor_curriculum_week_overrides_organization_name_idx"
ON "instructor_curriculum_week_overrides"("organization_name");
CREATE INDEX "instructor_curriculum_week_overrides_course_id_idx"
ON "instructor_curriculum_week_overrides"("course_id");
ALTER TABLE "instructor_curriculum_week_overrides"
ADD CONSTRAINT "instructor_curriculum_week_overrides_course_id_fkey"
FOREIGN KEY ("course_id") REFERENCES "courses"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,2 @@
ALTER TABLE "instructor_curriculum_week_overrides"
ADD COLUMN "duration" TEXT;
@@ -0,0 +1,32 @@
UPDATE "student_groups"
SET "platform_name" = NULL
WHERE "platform_name" IN ('로컬 PC', '시뮬레이션', '시뮬레이터', 'Simulator');
DELETE FROM "instructor_platforms"
WHERE "platform_id" IN (
SELECT "id"
FROM "platforms"
WHERE "platform_key" ILIKE '%local%'
OR "platform_key" ILIKE '%simulator%'
OR "display_name" ILIKE '%로컬%'
OR "display_name" ILIKE '%시뮬%'
OR "display_name" ILIKE '%simulator%'
);
DELETE FROM "course_platforms"
WHERE "platform_id" IN (
SELECT "id"
FROM "platforms"
WHERE "platform_key" ILIKE '%local%'
OR "platform_key" ILIKE '%simulator%'
OR "display_name" ILIKE '%로컬%'
OR "display_name" ILIKE '%시뮬%'
OR "display_name" ILIKE '%simulator%'
);
DELETE FROM "platforms"
WHERE "platform_key" ILIKE '%local%'
OR "platform_key" ILIKE '%simulator%'
OR "display_name" ILIKE '%로컬%'
OR "display_name" ILIKE '%시뮬%'
OR "display_name" ILIKE '%simulator%';
@@ -0,0 +1,32 @@
INSERT INTO "platforms" ("platform_key", "display_name", "description", "organization_name", "icon_key", "is_active", "updated_at")
VALUES
('futuredrive-local-pc', '로컬 PC', '내 PC에서 직접 개발 환경을 실행합니다.', '퓨처드라이브', 'laptop', true, CURRENT_TIMESTAMP),
('futuredrive-simulator-01', '시뮬레이터', '가상 환경에서 안전하게 테스트합니다.', '퓨처드라이브', 'monitor', true, CURRENT_TIMESTAMP)
ON CONFLICT ("platform_key") DO UPDATE
SET "display_name" = EXCLUDED."display_name",
"description" = EXCLUDED."description",
"organization_name" = EXCLUDED."organization_name",
"icon_key" = EXCLUDED."icon_key",
"is_active" = true,
"updated_at" = CURRENT_TIMESTAMP;
WITH course AS (
SELECT "id" FROM "courses" WHERE "slug" = 'futuredrive-autonomy-basic'
), data AS (
SELECT *
FROM (VALUES
('futuredrive-local-pc', '클라이언트 실행 시 제어권이 부여됩니다', '연결', 'ready', 1),
('futuredrive-simulator-01', '사용 가능', '제어권 요청', 'ready', 2)
) AS v(platform_key, status, action, state, sort_order)
)
INSERT INTO "course_platforms" ("course_id", "platform_id", "status", "action", "state", "sort_order", "updated_at")
SELECT course."id", p."id", data.status, data.action, data.state, data.sort_order, CURRENT_TIMESTAMP
FROM course
JOIN data ON true
JOIN "platforms" p ON p."platform_key" = data.platform_key
ON CONFLICT ("course_id", "platform_id") DO UPDATE
SET "status" = EXCLUDED."status",
"action" = EXCLUDED."action",
"state" = EXCLUDED."state",
"sort_order" = EXCLUDED."sort_order",
"updated_at" = CURRENT_TIMESTAMP;
@@ -0,0 +1,7 @@
UPDATE platforms
SET display_name = 'F1Tenth'
WHERE platform_key = 'futuredrive-f1tenth-01';
UPDATE platforms
SET display_name = 'MK-mini'
WHERE platform_key = 'futuredrive-mkmini-02';
@@ -0,0 +1,63 @@
ALTER TABLE "instructor_platforms"
ADD COLUMN "control_duration_minutes" INTEGER NOT NULL DEFAULT 30,
ADD COLUMN "control_warning_minutes" INTEGER NOT NULL DEFAULT 5;
CREATE TABLE "platform_control_sessions" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"organization_id" UUID,
"organization_name" TEXT NOT NULL,
"platform_id" UUID,
"instructor_platform_id" UUID,
"holder_user_id" UUID NOT NULL,
"group_id" UUID,
"status" TEXT NOT NULL DEFAULT 'ACTIVE',
"started_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"ends_at" TIMESTAMPTZ(3) NOT NULL,
"warning_at" TIMESTAMPTZ(3) NOT NULL,
"warning_sent_at" TIMESTAMPTZ(3),
"released_at" TIMESTAMPTZ(3),
"end_reason" TEXT,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "platform_control_sessions_pkey" PRIMARY KEY ("id"),
CONSTRAINT "platform_control_sessions_platform_id_fkey" FOREIGN KEY ("platform_id") REFERENCES "platforms"("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "platform_control_sessions_instructor_platform_id_fkey" FOREIGN KEY ("instructor_platform_id") REFERENCES "instructor_platforms"("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "platform_control_sessions_holder_user_id_fkey" FOREIGN KEY ("holder_user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "platform_control_sessions_group_id_fkey" FOREIGN KEY ("group_id") REFERENCES "student_groups"("id") ON DELETE SET NULL ON UPDATE CASCADE
);
CREATE TABLE "platform_control_queue_entries" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"organization_id" UUID,
"organization_name" TEXT NOT NULL,
"platform_id" UUID,
"instructor_platform_id" UUID,
"user_id" UUID NOT NULL,
"group_id" UUID,
"status" TEXT NOT NULL DEFAULT 'WAITING',
"requested_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"granted_at" TIMESTAMPTZ(3),
"canceled_at" TIMESTAMPTZ(3),
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "platform_control_queue_entries_pkey" PRIMARY KEY ("id"),
CONSTRAINT "platform_control_queue_entries_platform_id_fkey" FOREIGN KEY ("platform_id") REFERENCES "platforms"("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "platform_control_queue_entries_instructor_platform_id_fkey" FOREIGN KEY ("instructor_platform_id") REFERENCES "instructor_platforms"("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "platform_control_queue_entries_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "platform_control_queue_entries_group_id_fkey" FOREIGN KEY ("group_id") REFERENCES "student_groups"("id") ON DELETE SET NULL ON UPDATE CASCADE
);
CREATE INDEX "platform_control_sessions_organization_id_idx" ON "platform_control_sessions"("organization_id");
CREATE INDEX "platform_control_sessions_organization_name_idx" ON "platform_control_sessions"("organization_name");
CREATE INDEX "platform_control_sessions_platform_id_status_idx" ON "platform_control_sessions"("platform_id", "status");
CREATE INDEX "platform_control_sessions_instructor_platform_id_status_idx" ON "platform_control_sessions"("instructor_platform_id", "status");
CREATE INDEX "platform_control_sessions_holder_user_id_status_idx" ON "platform_control_sessions"("holder_user_id", "status");
CREATE INDEX "platform_control_sessions_ends_at_status_idx" ON "platform_control_sessions"("ends_at", "status");
CREATE INDEX "platform_control_queue_entries_organization_id_idx" ON "platform_control_queue_entries"("organization_id");
CREATE INDEX "platform_control_queue_entries_organization_name_idx" ON "platform_control_queue_entries"("organization_name");
CREATE INDEX "platform_control_queue_entries_platform_id_status_requested_at_idx" ON "platform_control_queue_entries"("platform_id", "status", "requested_at");
CREATE INDEX "platform_control_queue_entries_instructor_platform_id_status_requested_at_idx" ON "platform_control_queue_entries"("instructor_platform_id", "status", "requested_at");
CREATE INDEX "platform_control_queue_entries_user_id_status_idx" ON "platform_control_queue_entries"("user_id", "status");
@@ -0,0 +1,44 @@
ALTER TABLE "platforms"
ADD COLUMN "vehicle_type" TEXT NOT NULL DEFAULT 'MK-mini',
ADD COLUMN "wheelbase_meters" DECIMAL(6, 3) NOT NULL DEFAULT 0.325,
ADD COLUMN "max_speed_meters_per_second" DECIMAL(6, 2) NOT NULL DEFAULT 7.0,
ADD COLUMN "max_steering_angle_degrees" DECIMAL(6, 2) NOT NULL DEFAULT 30,
ADD COLUMN "sensors" JSONB NOT NULL DEFAULT '[]',
ADD COLUMN "drivers" JSONB NOT NULL DEFAULT '[]',
ADD COLUMN "algorithms" JSONB NOT NULL DEFAULT '{}',
ADD COLUMN "control_duration_minutes" INTEGER NOT NULL DEFAULT 30,
ADD COLUMN "control_warning_minutes" INTEGER NOT NULL DEFAULT 5;
ALTER TABLE "instructor_platforms"
ADD COLUMN "vehicle_type" TEXT NOT NULL DEFAULT 'MK-mini',
ADD COLUMN "wheelbase_meters" DECIMAL(6, 3) NOT NULL DEFAULT 0.325,
ADD COLUMN "max_speed_meters_per_second" DECIMAL(6, 2) NOT NULL DEFAULT 7.0,
ADD COLUMN "max_steering_angle_degrees" DECIMAL(6, 2) NOT NULL DEFAULT 30,
ADD COLUMN "sensors" JSONB NOT NULL DEFAULT '[]',
ADD COLUMN "drivers" JSONB NOT NULL DEFAULT '[]',
ADD COLUMN "algorithms" JSONB NOT NULL DEFAULT '{}';
UPDATE "platforms"
SET
"vehicle_type" = CASE
WHEN "display_name" ILIKE '%F1Tenth%' THEN 'F1tenth'
WHEN "display_name" ILIKE '%시뮬%' OR "display_name" ILIKE '%Simulator%' THEN 'Simulator'
ELSE 'MK-mini'
END,
"sensors" = '[{"title":"LiDAR","description":"Robosense Helios 16채널"},{"title":"RGB-D 카메라","description":"Intel Realsense D435"},{"title":"IMU 센서","description":"9축 관성측정장치"}]'::jsonb,
"drivers" = '[{"title":"LiDAR 드라이버","description":"robosense_driver v1.5.0"},{"title":"Camera 드라이버","description":"realsense2_camera v4.55.1"},{"title":"IMU 드라이버","description":"xsens_driver v2.2.0"}]'::jsonb,
"algorithms" = '{"slam":"Cartographer","planning":"A* + DWA","control":"PID Control"}'::jsonb;
UPDATE "instructor_platforms" AS ip
SET
"vehicle_type" = p."vehicle_type",
"wheelbase_meters" = p."wheelbase_meters",
"max_speed_meters_per_second" = p."max_speed_meters_per_second",
"max_steering_angle_degrees" = p."max_steering_angle_degrees",
"sensors" = p."sensors",
"drivers" = p."drivers",
"algorithms" = p."algorithms",
"control_duration_minutes" = p."control_duration_minutes",
"control_warning_minutes" = p."control_warning_minutes"
FROM "platforms" AS p
WHERE p."id" = ip."platform_id";
@@ -0,0 +1,11 @@
UPDATE "platforms"
SET "sensors" = (
SELECT COALESCE(jsonb_agg(sensor || jsonb_build_object('enabled', COALESCE((sensor->>'enabled')::boolean, true))), '[]'::jsonb)
FROM jsonb_array_elements("platforms"."sensors") AS sensor
);
UPDATE "instructor_platforms"
SET "sensors" = (
SELECT COALESCE(jsonb_agg(sensor || jsonb_build_object('enabled', COALESCE((sensor->>'enabled')::boolean, true))), '[]'::jsonb)
FROM jsonb_array_elements("instructor_platforms"."sensors") AS sensor
);
@@ -0,0 +1,62 @@
CREATE TABLE "course_week_modules" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"week_id" UUID NOT NULL,
"sort_order" INTEGER NOT NULL,
"category" TEXT NOT NULL,
"name" TEXT NOT NULL,
"folder_name" TEXT NOT NULL,
"template_repository_url" TEXT,
"docker_image" TEXT,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "course_week_modules_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "course_week_modules_week_id_folder_name_key" ON "course_week_modules"("week_id", "folder_name");
CREATE UNIQUE INDEX "course_week_modules_week_id_sort_order_key" ON "course_week_modules"("week_id", "sort_order");
CREATE INDEX "course_week_modules_week_id_idx" ON "course_week_modules"("week_id");
ALTER TABLE "course_week_modules"
ADD CONSTRAINT "course_week_modules_week_id_fkey"
FOREIGN KEY ("week_id") REFERENCES "course_weeks"("id") ON DELETE CASCADE ON UPDATE CASCADE;
COMMENT ON TABLE "course_week_modules" IS '주차별 개발 스튜디오 모듈 목록.';
COMMENT ON COLUMN "course_week_modules"."folder_name" IS '관리자 템플릿 또는 로컬 캐시의 원본 모듈 폴더명.';
COMMENT ON COLUMN "course_week_modules"."template_repository_url" IS '추후 Gitea 템플릿 저장소 연동 시 사용할 원본 repo URL.';
COMMENT ON COLUMN "course_week_modules"."docker_image" IS '모듈 실행에 사용할 Docker 이미지.';
WITH target_week 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
),
seed(sort_order, category, name, folder_name) AS (
VALUES
(1, 'Gui', 'AW-Rviz', '[furive-gui]AW-Rviz_v1.0.0_ws'),
(2, 'Map', 'AW-SampleMap', '[furive-map]AW-SampleMap_v1.0.0_ws'),
(3, 'Simulator', 'AW-PlanningSim', '[furive-simulator]AW-PlanningSim_v1.0.0_ws'),
(4, 'Map', 'DGIST-Campus', '[furive-map]DGIST-Campus_v1.0.0_ws'),
(5, 'Planning', 'AW-Planning', '[furive-planning]AW-Planning_v1.0.0_ws'),
(6, 'Bag', 'AW-SampleBag', '[furive-bag]AW-SampleBag_v1.0.0_ws'),
(7, 'Perception', 'AW-Perception', '[furive-perception]AW-Perception_v1.0.0_ws'),
(8, 'Sensing', 'test2', '[furive-sensing]test2_v1.0.0_ws'),
(9, 'Localization', 'AW-Localization', '[furive-localization]AW-Localization_v1.0.0_ws'),
(10, 'System', 'AW-System', '[furive-system]AW-System_v1.0.0_ws'),
(11, 'Vehicle', 'AW-TestSampleVehicle', '[furive-vehicle]AW-TestSampleVehicle_v1.0.0_ws'),
(12, 'Module', 'vehicle-f1', 'vehicle-f1_v1.0.0_ws'),
(13, 'Vehicle', 'AW-SampleVehicle', '[furive-vehicle]AW-SampleVehicle_v1.0.0_ws'),
(14, 'Control', 'AW-Control', '[furive-control]AW-Control_v1.0.0_ws'),
(15, 'Map', 'AW-SampleMapRosBag', '[furive-map]AW-SampleMapRosBag_v1.0.0_ws'),
(16, 'Sensing', 'AW-SampleSensor', '[furive-sensing]AW-SampleSensor_v1.0.0_ws')
)
INSERT INTO "course_week_modules" ("week_id", "sort_order", "category", "name", "folder_name")
SELECT target_week.week_id, seed.sort_order, seed.category, seed.name, seed.folder_name
FROM target_week
CROSS JOIN seed
ON CONFLICT ("week_id", "folder_name") DO UPDATE
SET "sort_order" = EXCLUDED."sort_order",
"category" = EXCLUDED."category",
"name" = EXCLUDED."name",
"updated_at" = CURRENT_TIMESTAMP;
@@ -0,0 +1,18 @@
DELETE FROM "course_week_modules";
WITH seed(sort_order, category, name, folder_name) AS (
VALUES
(1, 'Gui', 'AW-Rviz', '[furive-gui]AW-Rviz_v1.0.0_ws'),
(2, 'Map', 'AW-SampleMap', '[furive-map]AW-SampleMap_v1.0.0_ws'),
(3, 'Planning', 'AW-Planning', '[furive-planning]AW-Planning_v1.0.0_ws'),
(4, 'Perception', 'AW-Perception', '[furive-perception]AW-Perception_v1.0.0_ws'),
(5, 'Localization', 'AW-Localization', '[furive-localization]AW-Localization_v1.0.0_ws'),
(6, 'System', 'AW-System', '[furive-system]AW-System_v1.0.0_ws'),
(7, 'Vehicle', 'AW-SampleVehicle', '[furive-vehicle]AW-SampleVehicle_v1.0.0_ws'),
(8, 'Control', 'AW-Control', '[furive-control]AW-Control_v1.0.0_ws'),
(9, 'Sensing', 'AW-SampleSensor', '[furive-sensing]AW-SampleSensor_v1.0.0_ws')
)
INSERT INTO "course_week_modules" ("week_id", "sort_order", "category", "name", "folder_name")
SELECT w."id", seed.sort_order, seed.category, seed.name, seed.folder_name
FROM "course_weeks" w
CROSS JOIN seed;
@@ -0,0 +1,24 @@
UPDATE "course_week_modules"
SET "template_repository_url" = CASE "name"
WHEN 'AW-Rviz' THEN 'http://localhost:3000/templates/aw-rviz.git'
WHEN 'AW-SampleMap' THEN 'http://localhost:3000/templates/aw-samplemap.git'
WHEN 'AW-Planning' THEN 'http://localhost:3000/templates/aw-planning.git'
WHEN 'AW-Perception' THEN 'http://localhost:3000/templates/aw-perception.git'
WHEN 'AW-Localization' THEN 'http://localhost:3000/templates/aw-localization.git'
WHEN 'AW-System' THEN 'http://localhost:3000/templates/aw-system.git'
WHEN 'AW-SampleVehicle' THEN 'http://localhost:3000/templates/aw-samplevehicle.git'
WHEN 'AW-Control' THEN 'http://localhost:3000/templates/aw-control.git'
WHEN 'AW-SampleSensor' THEN 'http://localhost:3000/templates/aw-samplesensor.git'
ELSE "template_repository_url"
END
WHERE "name" IN (
'AW-Rviz',
'AW-SampleMap',
'AW-Planning',
'AW-Perception',
'AW-Localization',
'AW-System',
'AW-SampleVehicle',
'AW-Control',
'AW-SampleSensor'
);
@@ -0,0 +1,28 @@
CREATE TABLE "studio_repositories" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"user_id" UUID NOT NULL,
"week_id" UUID NOT NULL,
"module_id" UUID NOT NULL,
"repo_owner" TEXT NOT NULL,
"repo_name" TEXT NOT NULL,
"repository_url" TEXT NOT NULL,
"template_repository_url" TEXT NOT NULL,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "studio_repositories_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "studio_repositories_user_id_module_id_key" ON "studio_repositories"("user_id", "module_id");
CREATE INDEX "studio_repositories_week_id_idx" ON "studio_repositories"("week_id");
CREATE INDEX "studio_repositories_repo_owner_repo_name_idx" ON "studio_repositories"("repo_owner", "repo_name");
ALTER TABLE "studio_repositories"
ADD CONSTRAINT "studio_repositories_user_id_fkey"
FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "studio_repositories"
ADD CONSTRAINT "studio_repositories_module_id_fkey"
FOREIGN KEY ("module_id") REFERENCES "course_week_modules"("id") ON DELETE CASCADE ON UPDATE CASCADE;
COMMENT ON TABLE "studio_repositories" IS '개발 스튜디오 사용자별 모듈 Git 저장소 매핑.';
@@ -0,0 +1,31 @@
WITH target_weeks AS (
SELECT
w."id" AS week_id,
COALESCE(MAX(m."sort_order"), 0) + 1 AS next_sort_order
FROM "course_weeks" w
LEFT JOIN "course_week_modules" m ON m."week_id" = w."id"
GROUP BY w."id"
)
INSERT INTO "course_week_modules" (
"week_id",
"sort_order",
"category",
"name",
"folder_name",
"template_repository_url"
)
SELECT
target_weeks.week_id,
target_weeks.next_sort_order,
'Simulator',
'AW-PlanningSim',
'[furive-simulator]AW-PlanningSim_v1.0.0_ws',
'http://localhost:3000/templates/aw-planningsim'
FROM target_weeks
ON CONFLICT ("week_id", "folder_name") DO UPDATE
SET
"sort_order" = EXCLUDED."sort_order",
"category" = EXCLUDED."category",
"name" = EXCLUDED."name",
"template_repository_url" = EXCLUDED."template_repository_url",
"updated_at" = CURRENT_TIMESTAMP;
@@ -0,0 +1,9 @@
DELETE FROM "course_week_modules" m
USING "course_weeks" w
WHERE m."week_id" = w."id"
AND w."week" = 3
AND m."name" IN (
'AW-Perception',
'AW-Localization',
'AW-SampleSensor'
);
@@ -0,0 +1,11 @@
ALTER TABLE "platforms"
ADD COLUMN "default_studio_settings" JSONB NOT NULL DEFAULT '{"workspaceRoot":"~/autonomy-studio/workspaces","giteaBaseUrl":"http://localhost:3000","giteaOwner":"students"}';
ALTER TABLE "instructor_platforms"
ADD COLUMN "studio_settings_override" JSONB;
UPDATE "instructor_platforms" AS ip
SET "studio_settings_override" = p."default_studio_settings"
FROM "platforms" AS p
WHERE ip."platform_id" = p."id"
AND ip."studio_settings_override" IS NULL;
@@ -0,0 +1,37 @@
WITH priority(name, folder_name, sort_order) AS (
VALUES
('AW-SampleVehicle', '[furive-vehicle]AW-SampleVehicle_v1.0.0_ws', 1),
('AW-SampleSensor', '[furive-sensing]AW-SampleSensor_v1.0.0_ws', 2),
('AW-SampleMap', '[furive-map]AW-SampleMap_v1.0.0_ws', 3),
('AW-Localization', '[furive-localization]AW-Localization_v1.0.0_ws', 4),
('AW-Perception', '[furive-perception]AW-Perception_v1.0.0_ws', 5),
('AW-Planning', '[furive-planning]AW-Planning_v1.0.0_ws', 6),
('AW-Control', '[furive-control]AW-Control_v1.0.0_ws', 7),
('AW-System', '[furive-system]AW-System_v1.0.0_ws', 8),
('AW-Rviz', '[furive-gui]AW-Rviz_v1.0.0_ws', 9),
('AW-PlanningSim', '[furive-simulator]AW-PlanningSim_v1.0.0_ws', 10)
)
UPDATE "course_week_modules" module
SET "sort_order" = priority.sort_order + 1000,
"updated_at" = CURRENT_TIMESTAMP
FROM priority
WHERE module."folder_name" = priority.folder_name;
WITH priority(name, folder_name, sort_order) AS (
VALUES
('AW-SampleVehicle', '[furive-vehicle]AW-SampleVehicle_v1.0.0_ws', 1),
('AW-SampleSensor', '[furive-sensing]AW-SampleSensor_v1.0.0_ws', 2),
('AW-SampleMap', '[furive-map]AW-SampleMap_v1.0.0_ws', 3),
('AW-Localization', '[furive-localization]AW-Localization_v1.0.0_ws', 4),
('AW-Perception', '[furive-perception]AW-Perception_v1.0.0_ws', 5),
('AW-Planning', '[furive-planning]AW-Planning_v1.0.0_ws', 6),
('AW-Control', '[furive-control]AW-Control_v1.0.0_ws', 7),
('AW-System', '[furive-system]AW-System_v1.0.0_ws', 8),
('AW-Rviz', '[furive-gui]AW-Rviz_v1.0.0_ws', 9),
('AW-PlanningSim', '[furive-simulator]AW-PlanningSim_v1.0.0_ws', 10)
)
UPDATE "course_week_modules" module
SET "sort_order" = priority.sort_order,
"updated_at" = CURRENT_TIMESTAMP
FROM priority
WHERE module."folder_name" = priority.folder_name;
@@ -0,0 +1,122 @@
CREATE TABLE "course_environment_profiles" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"course_id" UUID NOT NULL,
"week" INTEGER NOT NULL,
"platform_key" TEXT NOT NULL DEFAULT 'futuredrive-local-pc',
"profile_type" TEXT NOT NULL,
"name" TEXT NOT NULL,
"config" JSONB NOT NULL,
"enabled" BOOLEAN NOT NULL DEFAULT true,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "course_environment_profiles_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "course_environment_profiles_course_id_week_platform_key_key"
ON "course_environment_profiles"("course_id", "week", "platform_key");
CREATE INDEX "course_environment_profiles_course_id_week_idx"
ON "course_environment_profiles"("course_id", "week");
CREATE INDEX "course_environment_profiles_profile_type_idx"
ON "course_environment_profiles"("profile_type");
ALTER TABLE "course_environment_profiles"
ADD CONSTRAINT "course_environment_profiles_course_id_fkey"
FOREIGN KEY ("course_id")
REFERENCES "courses"("id")
ON DELETE CASCADE
ON UPDATE CASCADE;
COMMENT ON TABLE "course_environment_profiles" IS '강좌/주차/플랫폼별 개발환경 실행 프로파일.';
COMMENT ON COLUMN "course_environment_profiles"."config" IS 'Docker runtime, ROS launch, bridge, viewer 실행 설정 JSON.';
INSERT INTO "course_environment_profiles" (
"course_id",
"week",
"platform_key",
"profile_type",
"name",
"config",
"updated_at"
)
SELECT
course."id",
3,
'futuredrive-local-pc',
'SLAM',
'TurtleBot3 SLAM 로컬 시뮬레이션',
'{
"type": "SLAM",
"viewer": {
"type": "slam",
"url": "/slam-viewer/index.html",
"healthUrl": "http://localhost:8765/health"
},
"runtime": {
"containers": [
{
"name": "nav2-slam",
"image": "nav2-slam:humble",
"network": "host",
"privileged": true,
"env": {
"DISPLAY": ":1",
"QT_X11_NO_MITSHM": "1",
"LIBGL_ALWAYS_SOFTWARE": "1",
"TURTLEBOT3_MODEL": "burger"
},
"volumes": [
"/tmp/.X11-unix:/tmp/.X11-unix",
"/dev/shm:/dev/shm",
"~/.gazebo:/root/.gazebo",
"~/.cache:/root/.cache"
],
"command": "tail -f /dev/null"
}
],
"steps": [
{
"container": "nav2-slam",
"mode": "execDetached",
"delayMs": 0,
"command": "ros2 launch gazebo_ros gzserver.launch.py world:=/opt/ros/humble/share/turtlebot3_gazebo/worlds/turtlebot3_world.world & ros2 launch turtlebot3_gazebo robot_state_publisher.launch.py use_sim_time:=true & sleep 6; ros2 launch turtlebot3_gazebo spawn_turtlebot3.launch.py x_pose:=-2.0 y_pose:=-0.5; wait",
"logFile": "/tmp/turtlebot3_gazebo.log"
},
{
"container": "nav2-slam",
"mode": "execDetached",
"delayMs": 6000,
"command": "ros2 launch slam_toolbox online_async_launch.py use_sim_time:=true",
"logFile": "/tmp/slam_toolbox.log"
},
{
"container": "nav2-slam",
"mode": "execDetached",
"delayMs": 3000,
"command": "ros2 launch nav2_bringup navigation_launch.py use_sim_time:=true autostart:=true",
"logFile": "/tmp/nav2.log"
}
]
},
"bridge": {
"name": "slam-ros-bridge",
"image": "slam-ros-bridge",
"buildContext": "resources/slam-ros-bridge",
"port": 8765,
"env": {
"ODOM_TOPIC": "/odom",
"SCAN_TOPIC": "/scan",
"MAP_TOPIC": "/map",
"CMD_VEL_TOPIC": "/cmd_vel",
"NAVIGATE_TO_POSE_ACTION": "/navigate_to_pose"
}
}
}'::jsonb,
CURRENT_TIMESTAMP
FROM "courses" AS course
ON CONFLICT ("course_id", "week", "platform_key") DO UPDATE
SET
"profile_type" = EXCLUDED."profile_type",
"name" = EXCLUDED."name",
"config" = EXCLUDED."config",
"enabled" = true,
"updated_at" = CURRENT_TIMESTAMP;
@@ -0,0 +1,175 @@
INSERT INTO "course_weeks" (
"course_id",
"week",
"title",
"tags",
"progress",
"state",
"duration",
"github",
"overview",
"deliverable",
"updated_at"
)
SELECT
course."id",
6,
'Autoware 기반 자율주행',
ARRAY['Autoware','진행예정']::TEXT[],
0,
'locked',
'10-12시간',
'미제출',
'Autoware 모듈을 실행하고 시각화 화면에서 초기 위치와 목표 포즈를 설정해 차량이 경로를 따라 주행하는 흐름을 검증합니다.',
'초기 위치/목표 포즈 설정값, 주행 로그, 경로 계획 및 제어 결과를 정리해 제출',
CURRENT_TIMESTAMP
FROM "courses" AS course
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 target_weeks AS (
SELECT w."id" AS week_id
FROM "course_weeks" w
WHERE w."week" = 6
)
INSERT INTO "course_week_topics" ("week_id", "sort_order", "title", "detail")
SELECT target_weeks.week_id, topic.sort_order, topic.title, topic.detail
FROM target_weeks
CROSS JOIN (
VALUES
(1, 'Autoware 실행 구조', '차량, 센서, 지도, 위치추정, 인지, 계획, 제어 모듈의 실행 순서와 ROS2 토픽 흐름을 확인합니다.'),
(2, '초기 위치 설정', '시각화 화면에서 초기 위치를 지정하고 Localization 상태와 차량 pose 갱신을 검증합니다.'),
(3, '목표 포즈와 경로 계획', '목표 포즈를 지정해 mission planning과 behavior planning 결과 경로를 확인합니다.'),
(4, '제어 명령과 주행 검증', '계획된 경로를 따라 차량이 움직이는지 제어 명령, 상태 토픽, 로그를 기준으로 점검합니다.')
) AS topic(sort_order, title, detail)
ON CONFLICT ("week_id", "sort_order") DO UPDATE
SET
"title" = EXCLUDED."title",
"detail" = EXCLUDED."detail";
WITH target_weeks AS (
SELECT w."id" AS week_id
FROM "course_weeks" w
WHERE w."week" = 6
)
INSERT INTO "course_week_practices" ("week_id", "sort_order", "title", "description")
SELECT target_weeks.week_id, practice.sort_order, practice.title, practice.description
FROM target_weeks
CROSS JOIN (
VALUES
(1, 'Autoware 모듈 실행', '주차에 등록된 Autoware 모듈을 순서대로 실행하고 컨테이너 상태를 확인합니다.'),
(2, '초기 위치 지정', '시각화에서 초기 위치를 지정하고 차량 위치가 지도 좌표계에 정상 반영되는지 확인합니다.'),
(3, '목표 포즈 지정', '목표 지점을 설정하고 생성된 route와 주행 상태를 비교합니다.'),
(4, '주행 결과 제출', '수정한 소스, 실행 로그, 주행 검증 결과를 제출합니다.')
) AS practice(sort_order, title, description)
ON CONFLICT ("week_id", "sort_order") DO UPDATE
SET
"title" = EXCLUDED."title",
"description" = EXCLUDED."description";
WITH week_pairs AS (
SELECT
source_week."id" AS source_week_id,
target_week."id" AS target_week_id
FROM "course_weeks" source_week
JOIN "course_weeks" target_week
ON target_week."course_id" = source_week."course_id"
AND target_week."week" = 6
WHERE source_week."week" = 3
)
INSERT INTO "course_week_modules" (
"week_id",
"sort_order",
"category",
"name",
"folder_name",
"template_repository_url",
"docker_image",
"updated_at"
)
SELECT
week_pairs.target_week_id,
source_module."sort_order",
source_module."category",
source_module."name",
source_module."folder_name",
source_module."template_repository_url",
source_module."docker_image",
CURRENT_TIMESTAMP
FROM week_pairs
JOIN "course_week_modules" source_module ON source_module."week_id" = week_pairs.source_week_id
ON CONFLICT ("week_id", "folder_name") DO UPDATE
SET
"sort_order" = EXCLUDED."sort_order",
"category" = EXCLUDED."category",
"name" = EXCLUDED."name",
"template_repository_url" = EXCLUDED."template_repository_url",
"docker_image" = EXCLUDED."docker_image",
"updated_at" = CURRENT_TIMESTAMP;
INSERT INTO "course_environment_profiles" (
"course_id",
"week",
"platform_key",
"profile_type",
"name",
"config",
"updated_at"
)
SELECT
course."id",
6,
'futuredrive-local-pc',
'AUTOWARE',
'Autoware 로컬 자율주행',
'{
"type": "autoware",
"viewer": {
"type": "autoware",
"url": "/autoware-viewer/index.html",
"healthUrl": "http://localhost:8765/health"
},
"runtime": {
"containers": [],
"steps": []
},
"bridge": {
"name": "autoware-ros-bridge",
"image": "autoware-ros-bridge",
"port": 8765,
"env": {
"ROBOT_STACK": "autoware",
"MAP_POINT_STRIDE": "8",
"MAP_POINT_LIMIT": "80000",
"AUTOWARE_ROUTE_INPUT": "rviz_topic",
"INITIAL_POSE_TOPIC": "/initialpose",
"GOAL_POSE_TOPIC": "/planning/mission_planning/goal",
"VEHICLE_POSE_TOPIC": "/localization/kinematic_state",
"TRAJECTORY_TOPIC": "/planning/scenario_planning/trajectory",
"ROUTE_TOPIC": "/planning/mission_planning/route"
}
}
}'::jsonb,
CURRENT_TIMESTAMP
FROM "courses" AS course
ON CONFLICT ("course_id", "week", "platform_key") DO UPDATE
SET
"profile_type" = EXCLUDED."profile_type",
"name" = EXCLUDED."name",
"config" = EXCLUDED."config",
"enabled" = true,
"updated_at" = CURRENT_TIMESTAMP;
UPDATE "course_student_progress"
SET
"total_weeks" = GREATEST("total_weeks", 6),
"updated_at" = CURRENT_TIMESTAMP;
@@ -0,0 +1,25 @@
ALTER TABLE "instructor_curriculum_settings"
ADD COLUMN "group_id" UUID;
ALTER TABLE "instructor_curriculum_settings"
ADD CONSTRAINT "instructor_curriculum_settings_group_id_fkey"
FOREIGN KEY ("group_id")
REFERENCES "student_groups"("id")
ON DELETE CASCADE
ON UPDATE CASCADE;
DROP INDEX IF EXISTS "instructor_curriculum_settings_organization_name_key";
CREATE UNIQUE INDEX "instructor_curriculum_settings_organization_default_key"
ON "instructor_curriculum_settings"("organization_name")
WHERE "group_id" IS NULL;
CREATE UNIQUE INDEX "instructor_curriculum_settings_organization_group_key"
ON "instructor_curriculum_settings"("organization_name", "group_id")
WHERE "group_id" IS NOT NULL;
CREATE INDEX "instructor_curriculum_settings_organization_name_idx"
ON "instructor_curriculum_settings"("organization_name");
CREATE INDEX "instructor_curriculum_settings_group_id_idx"
ON "instructor_curriculum_settings"("group_id");
@@ -0,0 +1,38 @@
ALTER TABLE "student_enrollments"
ADD COLUMN "course_id" UUID;
UPDATE "student_enrollments" enrollment
SET "course_id" = course."id"
FROM "courses" course
WHERE course."slug" = 'futuredrive-autonomy-basic'
AND course."is_active" = true
AND enrollment."course_id" IS NULL;
ALTER TABLE "student_enrollments"
ALTER COLUMN "course_id" SET NOT NULL;
ALTER TABLE "student_enrollments"
ADD CONSTRAINT "student_enrollments_course_id_fkey"
FOREIGN KEY ("course_id")
REFERENCES "courses"("id")
ON DELETE CASCADE
ON UPDATE CASCADE;
DROP INDEX IF EXISTS "student_enrollments_user_id_organization_name_key";
CREATE UNIQUE INDEX "student_enrollments_user_id_course_id_key"
ON "student_enrollments"("user_id", "course_id");
CREATE INDEX "student_enrollments_course_id_status_idx"
ON "student_enrollments"("course_id", "status");
DROP INDEX IF EXISTS "instructor_curriculum_settings_organization_default_key";
DROP INDEX IF EXISTS "instructor_curriculum_settings_organization_group_key";
CREATE UNIQUE INDEX "instructor_curriculum_settings_course_default_key"
ON "instructor_curriculum_settings"("organization_name", "course_id")
WHERE "group_id" IS NULL;
CREATE UNIQUE INDEX "instructor_curriculum_settings_course_group_key"
ON "instructor_curriculum_settings"("organization_name", "course_id", "group_id")
WHERE "group_id" IS NOT NULL;
@@ -0,0 +1,33 @@
WITH week3 AS (
SELECT "id" AS week_id
FROM "course_weeks"
WHERE "week" = 3
),
desired(sort_order, title, detail) AS (
VALUES
(1, 'SLAM 개요', '동시적 위치추정 및 지도작성의 기본 개념'),
(2, 'Cartographer 설정', 'ROS2 Cartographer 패키지 설정 방법'),
(3, '실습: 맵 생성', 'LiDAR 데이터를 활용한 실시간 맵핑'),
(4, '위치 추정', '생성된 맵에서 로봇 위치 인식')
)
INSERT INTO "course_week_topics" ("week_id", "sort_order", "title", "detail")
SELECT week3.week_id, desired.sort_order, desired.title, desired.detail
FROM week3
CROSS JOIN desired
ON CONFLICT ("week_id", "sort_order") DO UPDATE
SET
"title" = EXCLUDED."title",
"detail" = EXCLUDED."detail";
DELETE FROM "course_week_topics" topic
USING "course_weeks" week
WHERE topic."week_id" = week."id"
AND week."week" = 3
AND topic."sort_order" > 4;
UPDATE "instructor_curriculum_week_overrides"
SET
"topics" = '["SLAM 개요","Cartographer 설정","실습: 맵 생성","위치 추정"]'::jsonb,
"topic_details" = '["동시적 위치추정 및 지도작성의 기본 개념","ROS2 Cartographer 패키지 설정 방법","LiDAR 데이터를 활용한 실시간 맵핑","생성된 맵에서 로봇 위치 인식"]'::jsonb,
"updated_at" = CURRENT_TIMESTAMP
WHERE "week" = 3;
@@ -0,0 +1,4 @@
UPDATE "courses"
SET "description" = NULL,
"updated_at" = CURRENT_TIMESTAMP
WHERE "description" IS NOT NULL;
@@ -0,0 +1,216 @@
UPDATE "courses"
SET "organization_name" = '퓨처드라이브',
"updated_at" = CURRENT_TIMESTAMP
WHERE "slug" = 'futuredrive-autonomy-basic'
AND "organization_name" = '공통';
CREATE TEMP TABLE "_course_split_source" AS
SELECT "id" AS source_course_id
FROM "courses"
WHERE "slug" = 'futuredrive-autonomy-basic'
LIMIT 1;
INSERT INTO "courses" ("slug", "title", "subtitle", "description", "organization_name", "teacher_id", "is_active", "updated_at")
SELECT
'yeungnam-autonomy-basic',
source."title",
source."subtitle",
source."description",
'영남대학교',
teacher."id",
true,
CURRENT_TIMESTAMP
FROM "_course_split_source" split
JOIN "courses" source ON source."id" = split.source_course_id
JOIN "users" teacher ON teacher."email" = 'teacher@yu.ac.kr'
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;
CREATE TEMP TABLE "_course_split_target" AS
SELECT "id" AS target_course_id
FROM "courses"
WHERE "slug" = 'yeungnam-autonomy-basic'
LIMIT 1;
INSERT INTO "course_platforms" ("course_id", "platform_id", "status", "action", "state", "sort_order", "updated_at")
SELECT target.target_course_id, platform."platform_id", platform."status", platform."action", platform."state", platform."sort_order", CURRENT_TIMESTAMP
FROM "course_platforms" platform
JOIN "_course_split_source" source ON source.source_course_id = platform."course_id"
CROSS JOIN "_course_split_target" target
ON CONFLICT ("course_id", "platform_id") DO UPDATE
SET "status" = EXCLUDED."status",
"action" = EXCLUDED."action",
"state" = EXCLUDED."state",
"sort_order" = EXCLUDED."sort_order",
"updated_at" = CURRENT_TIMESTAMP;
INSERT INTO "course_environment_profiles" ("course_id", "week", "platform_key", "profile_type", "name", "config", "enabled", "updated_at")
SELECT target.target_course_id, profile."week", profile."platform_key", profile."profile_type", profile."name", profile."config", profile."enabled", CURRENT_TIMESTAMP
FROM "course_environment_profiles" profile
JOIN "_course_split_source" source ON source.source_course_id = profile."course_id"
CROSS JOIN "_course_split_target" target
ON CONFLICT ("course_id", "week", "platform_key") DO UPDATE
SET "profile_type" = EXCLUDED."profile_type",
"name" = EXCLUDED."name",
"config" = EXCLUDED."config",
"enabled" = EXCLUDED."enabled",
"updated_at" = CURRENT_TIMESTAMP;
INSERT INTO "course_weeks" ("course_id", "week", "title", "tags", "progress", "state", "duration", "github", "overview", "deliverable", "updated_at")
SELECT target.target_course_id, week."week", week."title", week."tags", week."progress", week."state", week."duration", week."github", week."overview", week."deliverable", CURRENT_TIMESTAMP
FROM "course_weeks" week
JOIN "_course_split_source" source ON source.source_course_id = week."course_id"
CROSS JOIN "_course_split_target" target
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;
CREATE TEMP TABLE "_course_split_week_map" AS
SELECT source_week."id" AS source_week_id, target_week."id" AS target_week_id, source_week."week"
FROM "course_weeks" source_week
JOIN "_course_split_source" source ON source.source_course_id = source_week."course_id"
JOIN "_course_split_target" target ON true
JOIN "course_weeks" target_week ON target_week."course_id" = target.target_course_id AND target_week."week" = source_week."week";
INSERT INTO "course_week_topics" ("week_id", "sort_order", "title", "detail")
SELECT week_map.target_week_id, topic."sort_order", topic."title", topic."detail"
FROM "course_week_topics" topic
JOIN "_course_split_week_map" week_map ON week_map.source_week_id = topic."week_id"
ON CONFLICT ("week_id", "sort_order") DO UPDATE
SET "title" = EXCLUDED."title",
"detail" = EXCLUDED."detail";
INSERT INTO "course_week_practices" ("week_id", "sort_order", "title", "description")
SELECT week_map.target_week_id, practice."sort_order", practice."title", practice."description"
FROM "course_week_practices" practice
JOIN "_course_split_week_map" week_map ON week_map.source_week_id = practice."week_id"
ON CONFLICT ("week_id", "sort_order") DO UPDATE
SET "title" = EXCLUDED."title",
"description" = EXCLUDED."description";
INSERT INTO "course_week_modules" ("week_id", "sort_order", "category", "name", "folder_name", "template_repository_url", "docker_image", "updated_at")
SELECT week_map.target_week_id, module."sort_order", module."category", module."name", module."folder_name", module."template_repository_url", module."docker_image", CURRENT_TIMESTAMP
FROM "course_week_modules" module
JOIN "_course_split_week_map" week_map ON week_map.source_week_id = module."week_id"
ON CONFLICT ("week_id", "folder_name") DO UPDATE
SET "sort_order" = EXCLUDED."sort_order",
"category" = EXCLUDED."category",
"name" = EXCLUDED."name",
"template_repository_url" = EXCLUDED."template_repository_url",
"docker_image" = EXCLUDED."docker_image",
"updated_at" = CURRENT_TIMESTAMP;
CREATE TEMP TABLE "_course_split_module_map" AS
SELECT source_module."id" AS source_module_id, target_module."id" AS target_module_id, week_map.source_week_id, week_map.target_week_id
FROM "course_week_modules" source_module
JOIN "_course_split_week_map" week_map ON week_map.source_week_id = source_module."week_id"
JOIN "course_week_modules" target_module ON target_module."week_id" = week_map.target_week_id AND target_module."folder_name" = source_module."folder_name";
INSERT INTO "student_enrollments" ("user_id", "course_id", "organization_id", "organization_name", "status", "removed_at", "updated_at")
SELECT enrollment."user_id", target.target_course_id, enrollment."organization_id", enrollment."organization_name", enrollment."status", enrollment."removed_at", CURRENT_TIMESTAMP
FROM "student_enrollments" enrollment
JOIN "_course_split_source" source ON source.source_course_id = enrollment."course_id"
CROSS JOIN "_course_split_target" target
WHERE enrollment."organization_name" = '영남대학교'
ON CONFLICT ("user_id", "course_id") DO UPDATE
SET "organization_id" = EXCLUDED."organization_id",
"organization_name" = EXCLUDED."organization_name",
"status" = EXCLUDED."status",
"removed_at" = EXCLUDED."removed_at",
"updated_at" = CURRENT_TIMESTAMP;
DELETE FROM "student_enrollments" enrollment
USING "_course_split_source" source
WHERE enrollment."course_id" = source.source_course_id
AND enrollment."organization_name" = '영남대학교';
INSERT INTO "course_student_progress" ("course_id", "user_id", "completed_weeks", "total_weeks", "learning_hours", "commit_count", "average_score", "updated_at")
SELECT target.target_course_id, progress."user_id", progress."completed_weeks", progress."total_weeks", progress."learning_hours", progress."commit_count", progress."average_score", CURRENT_TIMESTAMP
FROM "course_student_progress" progress
JOIN "_course_split_source" source ON source.source_course_id = progress."course_id"
JOIN "users" student ON student."id" = progress."user_id"
CROSS JOIN "_course_split_target" target
WHERE student."primary_organization_name" = '영남대학교'
ON CONFLICT ("course_id", "user_id") DO UPDATE
SET "completed_weeks" = EXCLUDED."completed_weeks",
"total_weeks" = EXCLUDED."total_weeks",
"learning_hours" = EXCLUDED."learning_hours",
"commit_count" = EXCLUDED."commit_count",
"average_score" = EXCLUDED."average_score",
"updated_at" = CURRENT_TIMESTAMP;
DELETE FROM "course_student_progress" progress
USING "_course_split_source" source, "users" student
WHERE progress."course_id" = source.source_course_id
AND student."id" = progress."user_id"
AND student."primary_organization_name" = '영남대학교';
UPDATE "instructor_curriculum_settings" setting
SET "course_id" = target.target_course_id,
"updated_at" = CURRENT_TIMESTAMP
FROM "_course_split_source" source, "_course_split_target" target
WHERE setting."course_id" = source.source_course_id
AND setting."organization_name" = '영남대학교';
INSERT INTO "instructor_curriculum_week_overrides" ("organization_id", "organization_name", "course_id", "week", "title", "duration", "overview", "deliverable", "topics", "topic_details", "updated_at")
SELECT override."organization_id", override."organization_name", target.target_course_id, override."week", override."title", override."duration", override."overview", override."deliverable", override."topics", override."topic_details", CURRENT_TIMESTAMP
FROM "instructor_curriculum_week_overrides" override
JOIN "_course_split_source" source ON source.source_course_id = override."course_id"
CROSS JOIN "_course_split_target" target
WHERE override."organization_name" = '영남대학교'
ON CONFLICT ("organization_name", "course_id", "week") DO UPDATE
SET "title" = EXCLUDED."title",
"duration" = EXCLUDED."duration",
"overview" = EXCLUDED."overview",
"deliverable" = EXCLUDED."deliverable",
"topics" = EXCLUDED."topics",
"topic_details" = EXCLUDED."topic_details",
"updated_at" = CURRENT_TIMESTAMP;
DELETE FROM "instructor_curriculum_week_overrides" override
USING "_course_split_source" source
WHERE override."course_id" = source.source_course_id
AND override."organization_name" = '영남대학교';
UPDATE "studio_repositories" repository
SET "week_id" = module_map.target_week_id,
"module_id" = module_map.target_module_id,
"updated_at" = CURRENT_TIMESTAMP
FROM "_course_split_module_map" module_map, "users" student
WHERE repository."module_id" = module_map.source_module_id
AND repository."user_id" = student."id"
AND student."primary_organization_name" = '영남대학교';
UPDATE "chat_threads" thread
SET "course_id" = target.target_course_id,
"updated_at" = CURRENT_TIMESTAMP
FROM "_course_split_source" source
JOIN "_course_split_target" target ON true
JOIN "users" student ON true
WHERE thread."course_id" = source.source_course_id
AND student."id" = thread."student_id"
AND student."primary_organization_name" = '영남대학교';
UPDATE "chat_threads" thread
SET "week_id" = week_map.target_week_id,
"updated_at" = CURRENT_TIMESTAMP
FROM "_course_split_week_map" week_map
JOIN "_course_split_target" target ON true
JOIN "users" student ON true
WHERE thread."course_id" = target.target_course_id
AND thread."week_id" = week_map.source_week_id
AND student."id" = thread."student_id"
AND student."primary_organization_name" = '영남대학교';
@@ -0,0 +1,2 @@
ALTER TABLE "instructor_curriculum_week_overrides"
ADD COLUMN "practice_contents" JSONB;
@@ -0,0 +1,8 @@
ALTER TABLE "course_weeks"
ADD COLUMN IF NOT EXISTS "requires_source_submission" BOOLEAN NOT NULL DEFAULT true;
ALTER TABLE "instructor_curriculum_week_overrides"
ADD COLUMN IF NOT EXISTS "requires_source_submission" BOOLEAN DEFAULT true;
COMMENT ON COLUMN "course_weeks"."requires_source_submission" IS '주차 제출 시 소스 커밋/푸시가 필요한지 여부.';
COMMENT ON COLUMN "instructor_curriculum_week_overrides"."requires_source_submission" IS '강사별 주차 소스 제출 필요 여부 오버라이드.';
@@ -0,0 +1,28 @@
CREATE TABLE IF NOT EXISTS "course_student_week_progress" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"course_id" UUID NOT NULL,
"user_id" UUID NOT NULL,
"week" INTEGER NOT NULL,
"submitted_at" TIMESTAMPTZ(3),
"commit_count" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "course_student_week_progress_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX IF NOT EXISTS "course_student_week_progress_course_user_week_key"
ON "course_student_week_progress"("course_id", "user_id", "week");
CREATE INDEX IF NOT EXISTS "course_student_week_progress_user_idx"
ON "course_student_week_progress"("user_id");
ALTER TABLE "course_student_week_progress"
ADD CONSTRAINT "course_student_week_progress_course_id_fkey"
FOREIGN KEY ("course_id") REFERENCES "courses"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "course_student_week_progress"
ADD CONSTRAINT "course_student_week_progress_user_id_fkey"
FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
COMMENT ON TABLE "course_student_week_progress" IS '학생별 주차 학습 및 제출 요약.';
COMMENT ON COLUMN "course_student_week_progress"."submitted_at" IS '해당 주차 소스 제출 완료 시각.';
@@ -0,0 +1,18 @@
WITH week6_topics AS (
SELECT
w."course_id",
jsonb_agg(t."title" ORDER BY t."sort_order") AS topics,
jsonb_agg(t."detail" ORDER BY t."sort_order") AS topic_details
FROM "course_weeks" w
JOIN "course_week_topics" t ON t."week_id" = w."id"
WHERE w."week" = 6
GROUP BY w."course_id"
)
UPDATE "instructor_curriculum_week_overrides" override
SET
"topics" = week6_topics.topics,
"topic_details" = week6_topics.topic_details,
"updated_at" = CURRENT_TIMESTAMP
FROM week6_topics
WHERE override."course_id" = week6_topics."course_id"
AND override."week" = 6;
@@ -0,0 +1,26 @@
WITH week6_content AS (
SELECT
w."course_id",
w."overview",
w."deliverable",
jsonb_agg(
jsonb_build_object(
'title', p."title",
'description', p."description"
)
ORDER BY p."sort_order"
) FILTER (WHERE p."id" IS NOT NULL) AS practice_contents
FROM "course_weeks" w
LEFT JOIN "course_week_practices" p ON p."week_id" = w."id"
WHERE w."week" = 6
GROUP BY w."course_id", w."overview", w."deliverable"
)
UPDATE "instructor_curriculum_week_overrides" override
SET
"overview" = week6_content."overview",
"deliverable" = week6_content."deliverable",
"practice_contents" = COALESCE(week6_content.practice_contents, '[]'::jsonb),
"updated_at" = CURRENT_TIMESTAMP
FROM week6_content
WHERE override."course_id" = week6_content."course_id"
AND override."week" = 6;
@@ -0,0 +1,21 @@
CREATE TABLE "course_week_references" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"week_id" UUID NOT NULL,
"title" TEXT NOT NULL,
"original_file_name" TEXT NOT NULL,
"stored_file_name" TEXT NOT NULL,
"content_type" TEXT,
"file_size_bytes" INTEGER NOT NULL DEFAULT 0,
"sort_order" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "course_week_references_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "course_week_references_week_id_idx" ON "course_week_references"("week_id");
CREATE UNIQUE INDEX "course_week_references_week_id_stored_file_name_key" ON "course_week_references"("week_id", "stored_file_name");
ALTER TABLE "course_week_references"
ADD CONSTRAINT "course_week_references_week_id_fkey"
FOREIGN KEY ("week_id") REFERENCES "course_weeks"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,2 @@
ALTER TABLE "instructor_curriculum_week_overrides"
ADD COLUMN "disabled_module_ids" JSONB;
@@ -0,0 +1,22 @@
CREATE TABLE "submission_scores" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"course_id" UUID NOT NULL,
"week_id" UUID NOT NULL,
"module_id" UUID NOT NULL,
"student_id" UUID NOT NULL,
"score" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL,
CONSTRAINT "submission_scores_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "submission_scores_student_id_module_id_key" ON "submission_scores"("student_id", "module_id");
CREATE INDEX "submission_scores_course_id_idx" ON "submission_scores"("course_id");
CREATE INDEX "submission_scores_week_id_idx" ON "submission_scores"("week_id");
CREATE INDEX "submission_scores_module_id_idx" ON "submission_scores"("module_id");
ALTER TABLE "submission_scores" ADD CONSTRAINT "submission_scores_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "courses"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "submission_scores" ADD CONSTRAINT "submission_scores_week_id_fkey" FOREIGN KEY ("week_id") REFERENCES "course_weeks"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "submission_scores" ADD CONSTRAINT "submission_scores_module_id_fkey" FOREIGN KEY ("module_id") REFERENCES "course_week_modules"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "submission_scores" ADD CONSTRAINT "submission_scores_student_id_fkey" FOREIGN KEY ("student_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,19 @@
CREATE TABLE "submission_total_scores" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"course_id" UUID NOT NULL,
"week_id" UUID NOT NULL,
"student_id" UUID NOT NULL,
"score" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL,
CONSTRAINT "submission_total_scores_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "submission_total_scores_student_id_week_id_key" ON "submission_total_scores"("student_id", "week_id");
CREATE INDEX "submission_total_scores_course_id_idx" ON "submission_total_scores"("course_id");
CREATE INDEX "submission_total_scores_week_id_idx" ON "submission_total_scores"("week_id");
ALTER TABLE "submission_total_scores" ADD CONSTRAINT "submission_total_scores_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "courses"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "submission_total_scores" ADD CONSTRAINT "submission_total_scores_week_id_fkey" FOREIGN KEY ("week_id") REFERENCES "course_weeks"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "submission_total_scores" ADD CONSTRAINT "submission_total_scores_student_id_fkey" FOREIGN KEY ("student_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,135 @@
CREATE TABLE "visualization_platforms" (
"platform_key" TEXT NOT NULL,
"type" TEXT NOT NULL,
"display_name" TEXT NOT NULL,
"description" TEXT,
"default_config" JSONB NOT NULL,
"sort_order" INTEGER NOT NULL DEFAULT 0,
"enabled" BOOLEAN NOT NULL DEFAULT true,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL,
CONSTRAINT "visualization_platforms_pkey" PRIMARY KEY ("platform_key")
);
CREATE UNIQUE INDEX "visualization_platforms_type_key" ON "visualization_platforms"("type");
CREATE INDEX "visualization_platforms_enabled_sort_order_idx" ON "visualization_platforms"("enabled", "sort_order");
CREATE TABLE "course_week_visualization_platforms" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"course_id" UUID NOT NULL,
"week" INTEGER NOT NULL,
"platform_key" TEXT NOT NULL,
"enabled" BOOLEAN NOT NULL DEFAULT true,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL,
CONSTRAINT "course_week_visualization_platforms_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "course_week_visualization_platforms_course_id_week_platform_key_key"
ON "course_week_visualization_platforms"("course_id", "week", "platform_key");
CREATE INDEX "course_week_visualization_platforms_course_id_week_idx"
ON "course_week_visualization_platforms"("course_id", "week");
CREATE INDEX "course_week_visualization_platforms_platform_key_idx"
ON "course_week_visualization_platforms"("platform_key");
ALTER TABLE "course_week_visualization_platforms"
ADD CONSTRAINT "course_week_visualization_platforms_course_id_fkey"
FOREIGN KEY ("course_id") REFERENCES "courses"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "course_week_visualization_platforms"
ADD CONSTRAINT "course_week_visualization_platforms_platform_key_fkey"
FOREIGN KEY ("platform_key") REFERENCES "visualization_platforms"("platform_key") ON DELETE CASCADE ON UPDATE CASCADE;
INSERT INTO "visualization_platforms" ("platform_key", "type", "display_name", "description", "default_config", "sort_order", "updated_at")
VALUES
(
'AUTOWARE',
'AUTOWARE',
'Autoware',
'Autoware 기반 자율주행 시각화와 제어 실습 환경',
'{
"type": "AUTOWARE",
"runtime": { "containers": [], "steps": [] },
"bridge": {
"name": "autoware-ros-bridge",
"image": "autoware-ros-bridge",
"port": 8765,
"env": {
"ROBOT_STACK": "autoware",
"MAP_POINT_STRIDE": "8",
"MAP_POINT_LIMIT": "80000",
"AUTOWARE_ROUTE_INPUT": "rviz_topic",
"INITIAL_POSE_TOPIC": "/initialpose",
"GOAL_POSE_TOPIC": "/planning/mission_planning/goal",
"VEHICLE_POSE_TOPIC": "/localization/kinematic_state",
"TRAJECTORY_TOPIC": "/planning/scenario_planning/trajectory",
"ROUTE_TOPIC": "/planning/mission_planning/route"
}
},
"viewer": { "stack": "autoware" }
}'::jsonb,
1,
CURRENT_TIMESTAMP
),
(
'NAV2',
'NAV2',
'Nav2',
'Nav2 기반 주행 알고리즘 시각화 실습 환경',
'{
"type": "NAV2",
"runtime": { "containers": [], "steps": [] },
"bridge": {
"name": "nav2-ros-bridge",
"image": "nav2-ros-bridge",
"port": 8765,
"env": {
"ROBOT_STACK": "nav2"
}
},
"viewer": { "stack": "nav2" }
}'::jsonb,
2,
CURRENT_TIMESTAMP
),
(
'SLAM',
'SLAM',
'SLAM',
'TurtleBot3 SLAM 지도 생성 시각화 실습 환경',
'{
"type": "SLAM",
"runtime": {
"containers": [
{ "name": "nav2-slam", "image": "nav2-slam:humble", "network": "host" },
{ "name": "slam-ros-bridge", "image": "slam-ros-bridge", "network": "host" }
]
},
"bridge": { "url": "http://localhost:8765" },
"viewer": { "stack": "slam" },
"ros": {
"turtlebotModel": "burger"
}
}'::jsonb,
3,
CURRENT_TIMESTAMP
)
ON CONFLICT ("platform_key") DO UPDATE
SET "type" = EXCLUDED."type",
"display_name" = EXCLUDED."display_name",
"description" = EXCLUDED."description",
"default_config" = EXCLUDED."default_config",
"sort_order" = EXCLUDED."sort_order",
"enabled" = true,
"updated_at" = CURRENT_TIMESTAMP;
INSERT INTO "course_week_visualization_platforms" ("course_id", "week", "platform_key", "enabled", "updated_at")
SELECT DISTINCT profile."course_id", profile."week", upper(profile."profile_type"), true, CURRENT_TIMESTAMP
FROM "course_environment_profiles" profile
JOIN "visualization_platforms" platform ON platform."platform_key" = upper(profile."profile_type")
WHERE profile."enabled" = true
ON CONFLICT ("course_id", "week", "platform_key") DO UPDATE
SET "enabled" = true,
"updated_at" = CURRENT_TIMESTAMP;
@@ -0,0 +1,41 @@
UPDATE "visualization_platforms"
SET "default_config" = '{
"type": "AUTOWARE",
"runtime": { "containers": [], "steps": [] },
"bridge": {
"name": "autoware-ros-bridge",
"image": "autoware-ros-bridge",
"port": 8765,
"env": {
"ROBOT_STACK": "autoware",
"MAP_POINT_STRIDE": "8",
"MAP_POINT_LIMIT": "80000",
"AUTOWARE_ROUTE_INPUT": "rviz_topic",
"INITIAL_POSE_TOPIC": "/initialpose",
"GOAL_POSE_TOPIC": "/planning/mission_planning/goal",
"VEHICLE_POSE_TOPIC": "/localization/kinematic_state",
"TRAJECTORY_TOPIC": "/planning/scenario_planning/trajectory",
"ROUTE_TOPIC": "/planning/mission_planning/route"
}
},
"viewer": { "stack": "autoware" }
}'::jsonb,
"updated_at" = CURRENT_TIMESTAMP
WHERE "platform_key" = 'AUTOWARE';
UPDATE "visualization_platforms"
SET "default_config" = '{
"type": "NAV2",
"runtime": { "containers": [], "steps": [] },
"bridge": {
"name": "nav2-ros-bridge",
"image": "nav2-ros-bridge",
"port": 8765,
"env": {
"ROBOT_STACK": "nav2"
}
},
"viewer": { "stack": "nav2" }
}'::jsonb,
"updated_at" = CURRENT_TIMESTAMP
WHERE "platform_key" = 'NAV2';
@@ -0,0 +1,85 @@
UPDATE "visualization_platforms"
SET "default_config" = '{
"type": "AUTOWARE",
"target": "local",
"runtime": { "containers": [], "steps": [] },
"bridge": {
"name": "autoware-ros-bridge",
"image": "autoware-ros-bridge",
"buildContext": "resources/autoware-ros-bridge",
"port": 8765,
"env": {
"ROBOT_STACK": "autoware",
"MAP_POINT_STRIDE": "8",
"MAP_POINT_LIMIT": "80000",
"AUTOWARE_ROUTE_INPUT": "rviz_topic",
"INITIAL_POSE_TOPIC": "/initialpose",
"GOAL_POSE_TOPIC": "/planning/mission_planning/goal",
"VEHICLE_POSE_TOPIC": "/localization/kinematic_state",
"TRAJECTORY_TOPIC": "/planning/scenario_planning/trajectory",
"ROUTE_TOPIC": "/planning/mission_planning/route"
}
},
"viewer": { "stack": "autoware" }
}'::jsonb,
"updated_at" = CURRENT_TIMESTAMP
WHERE "platform_key" = 'AUTOWARE';
UPDATE "visualization_platforms"
SET "default_config" = '{
"type": "NAV2",
"target": "local",
"runtime": { "containers": [], "steps": [] },
"bridge": {
"name": "nav2-ros-bridge",
"image": "nav2-ros-bridge",
"buildContext": "resources/nav2-ros-bridge",
"port": 8765,
"env": {
"ROBOT_STACK": "nav2"
}
},
"viewer": { "stack": "nav2" }
}'::jsonb,
"updated_at" = CURRENT_TIMESTAMP
WHERE "platform_key" = 'NAV2';
UPDATE "visualization_platforms"
SET "default_config" = jsonb_set("default_config", '{target}', '"local"'::jsonb, true),
"updated_at" = CURRENT_TIMESTAMP
WHERE "platform_key" = 'SLAM';
UPDATE "course_environment_profiles"
SET "config" = jsonb_set(
jsonb_set(
jsonb_set("config", '{target}', '"local"'::jsonb, true),
'{bridge,buildContext}',
'"resources/autoware-ros-bridge"'::jsonb,
true
),
'{bridge,image}',
'"autoware-ros-bridge"'::jsonb,
true
),
"updated_at" = CURRENT_TIMESTAMP
WHERE upper("profile_type") = 'AUTOWARE';
UPDATE "course_environment_profiles"
SET "config" = jsonb_set(
jsonb_set(
jsonb_set("config", '{target}', '"local"'::jsonb, true),
'{bridge,buildContext}',
'"resources/nav2-ros-bridge"'::jsonb,
true
),
'{bridge,image}',
'"nav2-ros-bridge"'::jsonb,
true
),
"updated_at" = CURRENT_TIMESTAMP
WHERE upper("profile_type") = 'NAV2';
UPDATE "course_environment_profiles"
SET "config" = jsonb_set("config", '{target}', '"local"'::jsonb, true),
"updated_at" = CURRENT_TIMESTAMP
WHERE upper("profile_type") = 'SLAM';
@@ -0,0 +1,29 @@
CREATE TABLE "course_week_platforms" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"course_id" UUID NOT NULL,
"week" INTEGER NOT NULL,
"platform_id" UUID NOT NULL,
"enabled" BOOLEAN NOT NULL DEFAULT true,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "course_week_platforms_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "course_week_platforms_course_id_week_platform_id_key" ON "course_week_platforms"("course_id", "week", "platform_id");
CREATE INDEX "course_week_platforms_course_id_week_idx" ON "course_week_platforms"("course_id", "week");
CREATE INDEX "course_week_platforms_platform_id_idx" ON "course_week_platforms"("platform_id");
ALTER TABLE "course_week_platforms"
ADD CONSTRAINT "course_week_platforms_course_id_fkey"
FOREIGN KEY ("course_id") REFERENCES "courses"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "course_week_platforms"
ADD CONSTRAINT "course_week_platforms_platform_id_fkey"
FOREIGN KEY ("platform_id") REFERENCES "platforms"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
INSERT INTO "course_week_platforms" ("course_id", "week", "platform_id", "enabled")
SELECT cw."course_id", cw."week", cp."platform_id", true
FROM "course_weeks" cw
JOIN "course_platforms" cp ON cp."course_id" = cw."course_id"
ON CONFLICT ("course_id", "week", "platform_id") DO NOTHING;
@@ -0,0 +1,4 @@
ALTER TABLE "instructor_curriculum_week_overrides"
ADD COLUMN IF NOT EXISTS "tag_label" TEXT;
DROP TABLE IF EXISTS "course_week_platforms";
@@ -0,0 +1,25 @@
CREATE TABLE "curriculum_tag_options" (
"option_key" TEXT NOT NULL,
"label" TEXT NOT NULL,
"sort_order" INTEGER NOT NULL DEFAULT 0,
"enabled" BOOLEAN NOT NULL DEFAULT true,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "curriculum_tag_options_pkey" PRIMARY KEY ("option_key")
);
CREATE UNIQUE INDEX "curriculum_tag_options_label_key" ON "curriculum_tag_options"("label");
CREATE INDEX "curriculum_tag_options_enabled_sort_order_idx" ON "curriculum_tag_options"("enabled", "sort_order");
INSERT INTO "curriculum_tag_options" ("option_key", "label", "sort_order", "enabled")
VALUES
('none', '없음', 0, true),
('simulation', '시뮬레이션', 10, true),
('mk-mini', 'MK-mini', 20, true),
('f1tenth', 'f1tenth', 30, true)
ON CONFLICT ("option_key") DO UPDATE SET
"label" = EXCLUDED."label",
"sort_order" = EXCLUDED."sort_order",
"enabled" = EXCLUDED."enabled",
"updated_at" = CURRENT_TIMESTAMP;
@@ -0,0 +1,11 @@
ALTER TABLE "platforms"
ADD COLUMN IF NOT EXISTS "curriculum_tag_only" BOOLEAN NOT NULL DEFAULT false;
UPDATE "platforms"
SET "curriculum_tag_only" = true,
"updated_at" = CURRENT_TIMESTAMP
WHERE lower("platform_key") LIKE '%simulator%'
OR "display_name" ILIKE '%시뮬%'
OR lower("display_name") LIKE '%simulator%';
DROP TABLE IF EXISTS "curriculum_tag_options";
@@ -0,0 +1,24 @@
ALTER TABLE "platforms"
ALTER COLUMN "default_studio_settings"
SET DEFAULT '{"workspaceRoot":"~/autonomy-studio/workspaces","giteaBaseUrl":"http://localhost:3000","giteaOwner":"student"}';
UPDATE "platforms"
SET "default_studio_settings" = jsonb_set(
COALESCE("default_studio_settings", '{}'::jsonb),
'{giteaOwner}',
'"student"'::jsonb,
true
),
"updated_at" = CURRENT_TIMESTAMP
WHERE COALESCE("default_studio_settings"->>'giteaOwner', '') IN ('', 'students');
UPDATE "instructor_platforms"
SET "studio_settings_override" = jsonb_set(
"studio_settings_override",
'{giteaOwner}',
'"student"'::jsonb,
true
),
"updated_at" = CURRENT_TIMESTAMP
WHERE "studio_settings_override" IS NOT NULL
AND "studio_settings_override"->>'giteaOwner' = 'students';
@@ -0,0 +1,4 @@
UPDATE "studio_repositories"
SET "repo_owner" = 'student',
"repository_url" = replace("repository_url", '/students/', '/student/')
WHERE "repo_owner" = 'students';
@@ -0,0 +1,350 @@
COMMENT ON TABLE "users" IS 'LMS 내부 사용자 프로필. 인증 서버 사용자와 LMS 수강/강의/채팅 데이터를 연결한다.';
COMMENT ON COLUMN "users"."id" IS 'LMS 사용자 ID';
COMMENT ON COLUMN "users"."auth_user_id" IS '인증 서버 사용자 ID';
COMMENT ON COLUMN "users"."email" IS '사용자 이메일';
COMMENT ON COLUMN "users"."name" IS '사용자 이름';
COMMENT ON COLUMN "users"."auth_role" IS '인증 서버 기준 역할';
COMMENT ON COLUMN "users"."primary_organization_id" IS '대표 소속 조직 ID';
COMMENT ON COLUMN "users"."primary_organization_name" IS '대표 소속 조직명';
COMMENT ON COLUMN "users"."primary_organization_type" IS '대표 소속 조직 유형';
COMMENT ON COLUMN "users"."department" IS '부서 또는 학과';
COMMENT ON COLUMN "users"."position" IS '직책 또는 직위';
COMMENT ON COLUMN "users"."createdAt" IS '레코드 생성 시각';
COMMENT ON COLUMN "users"."updatedAt" IS '레코드 수정 시각';
COMMENT ON TABLE "student_enrollments" IS '수강생과 강좌의 등록 관계. 기존 사용자는 새 계정 생성 없이 강좌에 연결된다.';
COMMENT ON COLUMN "student_enrollments"."id" IS '수강 등록 ID';
COMMENT ON COLUMN "student_enrollments"."user_id" IS '수강생 사용자 ID';
COMMENT ON COLUMN "student_enrollments"."course_id" IS '등록 강좌 ID';
COMMENT ON COLUMN "student_enrollments"."organization_id" IS '등록 기준 조직 ID';
COMMENT ON COLUMN "student_enrollments"."organization_name" IS '등록 기준 조직명';
COMMENT ON COLUMN "student_enrollments"."status" IS '수강 등록 상태';
COMMENT ON COLUMN "student_enrollments"."removed_at" IS '수강 등록 해제 시각';
COMMENT ON COLUMN "student_enrollments"."created_at" IS '레코드 생성 시각';
COMMENT ON COLUMN "student_enrollments"."updated_at" IS '레코드 수정 시각';
COMMENT ON TABLE "student_groups" IS '강사가 관리하는 수강생 그룹. 강의 진행 상태와 플랫폼 배정의 기준이다.';
COMMENT ON COLUMN "student_groups"."id" IS '수강생 그룹 ID';
COMMENT ON COLUMN "student_groups"."organization_id" IS '그룹 소속 조직 ID';
COMMENT ON COLUMN "student_groups"."organization_name" IS '그룹 소속 조직명';
COMMENT ON COLUMN "student_groups"."name" IS '그룹명';
COMMENT ON COLUMN "student_groups"."week" IS '그룹 기본 주차 값';
COMMENT ON COLUMN "student_groups"."status" IS '그룹 운영 상태';
COMMENT ON COLUMN "student_groups"."platform_name" IS '그룹에 배정된 플랫폼 표시명';
COMMENT ON COLUMN "student_groups"."created_at" IS '레코드 생성 시각';
COMMENT ON COLUMN "student_groups"."updated_at" IS '레코드 수정 시각';
COMMENT ON TABLE "student_group_members" IS '수강생 그룹과 사용자 간의 멤버십.';
COMMENT ON COLUMN "student_group_members"."id" IS '그룹 멤버 ID';
COMMENT ON COLUMN "student_group_members"."group_id" IS '수강생 그룹 ID';
COMMENT ON COLUMN "student_group_members"."user_id" IS '그룹에 포함된 사용자 ID';
COMMENT ON COLUMN "student_group_members"."created_at" IS '레코드 생성 시각';
COMMENT ON TABLE "courses" IS 'LMS 강좌 마스터. 주차별 커리큘럼과 수강생 등록의 상위 단위이다.';
COMMENT ON COLUMN "courses"."id" IS '강좌 ID';
COMMENT ON COLUMN "courses"."slug" IS '강좌 식별 슬러그';
COMMENT ON COLUMN "courses"."title" IS '강좌명';
COMMENT ON COLUMN "courses"."subtitle" IS '강좌 부제목';
COMMENT ON COLUMN "courses"."description" IS '강좌 설명';
COMMENT ON COLUMN "courses"."organization_name" IS '강좌 소속 조직명';
COMMENT ON COLUMN "courses"."teacher_id" IS '담당 강사 사용자 ID';
COMMENT ON COLUMN "courses"."is_active" IS '강좌 활성 여부';
COMMENT ON COLUMN "courses"."created_at" IS '레코드 생성 시각';
COMMENT ON COLUMN "courses"."updated_at" IS '레코드 수정 시각';
COMMENT ON TABLE "course_weeks" IS '강좌별 주차 커리큘럼 기본 데이터.';
COMMENT ON COLUMN "course_weeks"."id" IS '주차 커리큘럼 ID';
COMMENT ON COLUMN "course_weeks"."course_id" IS '소속 강좌 ID';
COMMENT ON COLUMN "course_weeks"."week" IS '주차 번호';
COMMENT ON COLUMN "course_weeks"."title" IS '주차 제목';
COMMENT ON COLUMN "course_weeks"."tags" IS '주차 태그 목록';
COMMENT ON COLUMN "course_weeks"."progress" IS '기본 진행률';
COMMENT ON COLUMN "course_weeks"."state" IS '주차 상태';
COMMENT ON COLUMN "course_weeks"."duration" IS '예상 학습 시간';
COMMENT ON COLUMN "course_weeks"."github" IS '관련 GitHub 또는 저장소 정보';
COMMENT ON COLUMN "course_weeks"."overview" IS '주차 개요';
COMMENT ON COLUMN "course_weeks"."deliverable" IS '제출 기준 또는 산출물 설명';
COMMENT ON COLUMN "course_weeks"."requires_source_submission" IS '소스 제출 필요 여부';
COMMENT ON COLUMN "course_weeks"."created_at" IS '레코드 생성 시각';
COMMENT ON COLUMN "course_weeks"."updated_at" IS '레코드 수정 시각';
COMMENT ON TABLE "course_week_topics" IS '주차별 학습단계 목록.';
COMMENT ON COLUMN "course_week_topics"."id" IS '학습단계 ID';
COMMENT ON COLUMN "course_week_topics"."week_id" IS '주차 커리큘럼 ID';
COMMENT ON COLUMN "course_week_topics"."title" IS '학습단계 제목';
COMMENT ON COLUMN "course_week_topics"."detail" IS '학습단계 상세 내용';
COMMENT ON COLUMN "course_week_topics"."sort_order" IS '학습단계 표시 순서';
COMMENT ON TABLE "course_week_practices" IS '주차별 실습내용 목록.';
COMMENT ON COLUMN "course_week_practices"."id" IS '실습내용 ID';
COMMENT ON COLUMN "course_week_practices"."week_id" IS '주차 커리큘럼 ID';
COMMENT ON COLUMN "course_week_practices"."title" IS '실습내용 제목';
COMMENT ON COLUMN "course_week_practices"."description" IS '실습내용 설명';
COMMENT ON COLUMN "course_week_practices"."sort_order" IS '실습내용 표시 순서';
COMMENT ON TABLE "course_week_references" IS '주차별 참고자료 파일 메타데이터.';
COMMENT ON COLUMN "course_week_references"."id" IS '참고자료 ID';
COMMENT ON COLUMN "course_week_references"."week_id" IS '주차 커리큘럼 ID';
COMMENT ON COLUMN "course_week_references"."title" IS '참고자료 제목';
COMMENT ON COLUMN "course_week_references"."original_file_name" IS '업로드 원본 파일명';
COMMENT ON COLUMN "course_week_references"."stored_file_name" IS '서버 저장 파일명';
COMMENT ON COLUMN "course_week_references"."content_type" IS '파일 MIME 타입';
COMMENT ON COLUMN "course_week_references"."file_size_bytes" IS '파일 크기 바이트';
COMMENT ON COLUMN "course_week_references"."sort_order" IS '참고자료 표시 순서';
COMMENT ON COLUMN "course_week_references"."created_at" IS '레코드 생성 시각';
COMMENT ON COLUMN "course_week_references"."updated_at" IS '레코드 수정 시각';
COMMENT ON TABLE "course_week_modules" IS '주차별 개발 스튜디오 모듈 목록.';
COMMENT ON COLUMN "course_week_modules"."id" IS '주차 모듈 ID';
COMMENT ON COLUMN "course_week_modules"."week_id" IS '주차 커리큘럼 ID';
COMMENT ON COLUMN "course_week_modules"."sort_order" IS '모듈 표시 및 실행 순서';
COMMENT ON COLUMN "course_week_modules"."category" IS '모듈 분류';
COMMENT ON COLUMN "course_week_modules"."name" IS '모듈명';
COMMENT ON COLUMN "course_week_modules"."folder_name" IS '워크스페이스 폴더명';
COMMENT ON COLUMN "course_week_modules"."template_repository_url" IS '모듈 템플릿 저장소 URL';
COMMENT ON COLUMN "course_week_modules"."docker_image" IS '모듈 실행 Docker 이미지';
COMMENT ON COLUMN "course_week_modules"."created_at" IS '레코드 생성 시각';
COMMENT ON COLUMN "course_week_modules"."updated_at" IS '레코드 수정 시각';
COMMENT ON TABLE "instructor_curriculum_settings" IS '강사/그룹별 현재 진행 중인 주차와 학습단계 설정.';
COMMENT ON COLUMN "instructor_curriculum_settings"."id" IS '강의 진행 설정 ID';
COMMENT ON COLUMN "instructor_curriculum_settings"."organization_id" IS '설정 소속 조직 ID';
COMMENT ON COLUMN "instructor_curriculum_settings"."organization_name" IS '설정 소속 조직명';
COMMENT ON COLUMN "instructor_curriculum_settings"."group_id" IS '진행 상태가 적용되는 수강생 그룹 ID';
COMMENT ON COLUMN "instructor_curriculum_settings"."course_id" IS '강좌 ID';
COMMENT ON COLUMN "instructor_curriculum_settings"."active_week" IS '현재 진행 주차';
COMMENT ON COLUMN "instructor_curriculum_settings"."active_learning_step" IS '현재 진행 학습단계 식별값';
COMMENT ON COLUMN "instructor_curriculum_settings"."created_at" IS '레코드 생성 시각';
COMMENT ON COLUMN "instructor_curriculum_settings"."updated_at" IS '레코드 수정 시각';
COMMENT ON TABLE "instructor_curriculum_week_overrides" IS '강사 소속/강좌별 주차 커리큘럼 수정값.';
COMMENT ON COLUMN "instructor_curriculum_week_overrides"."id" IS '주차 커리큘럼 수정값 ID';
COMMENT ON COLUMN "instructor_curriculum_week_overrides"."organization_id" IS '수정 소속 조직 ID';
COMMENT ON COLUMN "instructor_curriculum_week_overrides"."organization_name" IS '수정 소속 조직명';
COMMENT ON COLUMN "instructor_curriculum_week_overrides"."course_id" IS '강좌 ID';
COMMENT ON COLUMN "instructor_curriculum_week_overrides"."week" IS '주차 번호';
COMMENT ON COLUMN "instructor_curriculum_week_overrides"."tag_label" IS '주차 플랫폼/태그 표시 라벨';
COMMENT ON COLUMN "instructor_curriculum_week_overrides"."title" IS '수정된 주차 제목';
COMMENT ON COLUMN "instructor_curriculum_week_overrides"."duration" IS '수정된 예상 학습 시간';
COMMENT ON COLUMN "instructor_curriculum_week_overrides"."overview" IS '수정된 주차 개요';
COMMENT ON COLUMN "instructor_curriculum_week_overrides"."deliverable" IS '수정된 제출 기준';
COMMENT ON COLUMN "instructor_curriculum_week_overrides"."requires_source_submission" IS '소스 제출 필요 여부 수정값';
COMMENT ON COLUMN "instructor_curriculum_week_overrides"."topics" IS '수정된 학습단계 JSON';
COMMENT ON COLUMN "instructor_curriculum_week_overrides"."topic_details" IS '수정된 학습단계 상세 JSON';
COMMENT ON COLUMN "instructor_curriculum_week_overrides"."practice_contents" IS '수정된 실습내용 JSON';
COMMENT ON COLUMN "instructor_curriculum_week_overrides"."disabled_module_ids" IS '비활성화한 주차 모듈 ID 목록 JSON';
COMMENT ON COLUMN "instructor_curriculum_week_overrides"."created_at" IS '레코드 생성 시각';
COMMENT ON COLUMN "instructor_curriculum_week_overrides"."updated_at" IS '레코드 수정 시각';
COMMENT ON TABLE "platforms" IS '관리자가 정의하는 플랫폼 마스터. 강좌 태그와 강사 등록 플랫폼의 기준이다.';
COMMENT ON COLUMN "platforms"."id" IS '플랫폼 마스터 ID';
COMMENT ON COLUMN "platforms"."platform_key" IS '플랫폼 고유 키';
COMMENT ON COLUMN "platforms"."display_name" IS '플랫폼 표시명';
COMMENT ON COLUMN "platforms"."description" IS '플랫폼 설명';
COMMENT ON COLUMN "platforms"."organization_name" IS '플랫폼 기본 소속 조직명';
COMMENT ON COLUMN "platforms"."icon_key" IS '플랫폼 아이콘 키';
COMMENT ON COLUMN "platforms"."is_active" IS '플랫폼 활성 여부';
COMMENT ON COLUMN "platforms"."curriculum_tag_only" IS '커리큘럼 태그 전용 여부';
COMMENT ON COLUMN "platforms"."vehicle_type" IS '차량 또는 장비 유형';
COMMENT ON COLUMN "platforms"."wheelbase_meters" IS '휠베이스 미터 단위';
COMMENT ON COLUMN "platforms"."max_speed_meters_per_second" IS '최대 속도 m/s';
COMMENT ON COLUMN "platforms"."max_steering_angle_degrees" IS '최대 조향각 도 단위';
COMMENT ON COLUMN "platforms"."sensors" IS '기본 센서 구성 JSON';
COMMENT ON COLUMN "platforms"."drivers" IS '기본 드라이버 구성 JSON';
COMMENT ON COLUMN "platforms"."algorithms" IS '기본 알고리즘 구성 JSON';
COMMENT ON COLUMN "platforms"."default_studio_settings" IS '강사 플랫폼 등록 시 복사되는 기본 개발 스튜디오 설정 JSON';
COMMENT ON COLUMN "platforms"."control_duration_minutes" IS '기본 제어권 유지 시간 분';
COMMENT ON COLUMN "platforms"."control_warning_minutes" IS '기본 제어권 만료 경고 시간 분';
COMMENT ON COLUMN "platforms"."created_at" IS '레코드 생성 시각';
COMMENT ON COLUMN "platforms"."updated_at" IS '레코드 수정 시각';
COMMENT ON TABLE "instructor_platforms" IS '강사가 실제 수업에 등록한 플랫폼 인스턴스.';
COMMENT ON COLUMN "instructor_platforms"."id" IS '강사 등록 플랫폼 ID';
COMMENT ON COLUMN "instructor_platforms"."organization_id" IS '등록 소속 조직 ID';
COMMENT ON COLUMN "instructor_platforms"."organization_name" IS '등록 소속 조직명';
COMMENT ON COLUMN "instructor_platforms"."platform_id" IS '플랫폼 마스터 ID';
COMMENT ON COLUMN "instructor_platforms"."display_name" IS '수업에서 사용할 플랫폼 표시명';
COMMENT ON COLUMN "instructor_platforms"."instance_no" IS '동일 플랫폼 내 인스턴스 번호';
COMMENT ON COLUMN "instructor_platforms"."status" IS '플랫폼 운영 상태';
COMMENT ON COLUMN "instructor_platforms"."battery" IS '배터리 잔량 퍼센트';
COMMENT ON COLUMN "instructor_platforms"."uptime_minutes" IS '가동 시간 분';
COMMENT ON COLUMN "instructor_platforms"."speed_meters" IS '현재 속도 m/s';
COMMENT ON COLUMN "instructor_platforms"."error_count" IS '오류 발생 수';
COMMENT ON COLUMN "instructor_platforms"."emergency_stop" IS '긴급정지 여부';
COMMENT ON COLUMN "instructor_platforms"."vehicle_type" IS '차량 또는 장비 유형';
COMMENT ON COLUMN "instructor_platforms"."wheelbase_meters" IS '휠베이스 미터 단위';
COMMENT ON COLUMN "instructor_platforms"."max_speed_meters_per_second" IS '최대 속도 m/s';
COMMENT ON COLUMN "instructor_platforms"."max_steering_angle_degrees" IS '최대 조향각 도 단위';
COMMENT ON COLUMN "instructor_platforms"."sensors" IS '강사 플랫폼 센서 설정 JSON';
COMMENT ON COLUMN "instructor_platforms"."drivers" IS '강사 플랫폼 드라이버 정보 JSON';
COMMENT ON COLUMN "instructor_platforms"."algorithms" IS '강사 플랫폼 알고리즘 설정 JSON';
COMMENT ON COLUMN "instructor_platforms"."studio_settings_override" IS '플랫폼별 개발 스튜디오 설정 오버라이드 JSON';
COMMENT ON COLUMN "instructor_platforms"."control_duration_minutes" IS '제어권 유지 시간 분';
COMMENT ON COLUMN "instructor_platforms"."control_warning_minutes" IS '제어권 만료 경고 시간 분';
COMMENT ON COLUMN "instructor_platforms"."created_at" IS '레코드 생성 시각';
COMMENT ON COLUMN "instructor_platforms"."updated_at" IS '레코드 수정 시각';
COMMENT ON TABLE "course_platforms" IS '강좌에서 사용 가능한 플랫폼 목록.';
COMMENT ON COLUMN "course_platforms"."id" IS '강좌 플랫폼 ID';
COMMENT ON COLUMN "course_platforms"."course_id" IS '강좌 ID';
COMMENT ON COLUMN "course_platforms"."platform_id" IS '플랫폼 마스터 ID';
COMMENT ON COLUMN "course_platforms"."status" IS '강좌 내 플랫폼 표시 상태';
COMMENT ON COLUMN "course_platforms"."action" IS '강좌 내 플랫폼 기본 액션 문구';
COMMENT ON COLUMN "course_platforms"."state" IS '강좌 내 플랫폼 사용 상태 키';
COMMENT ON COLUMN "course_platforms"."sort_order" IS '플랫폼 표시 순서';
COMMENT ON COLUMN "course_platforms"."created_at" IS '레코드 생성 시각';
COMMENT ON COLUMN "course_platforms"."updated_at" IS '레코드 수정 시각';
COMMENT ON TABLE "course_environment_profiles" IS '강좌/주차/플랫폼별 개발환경 실행 프로파일.';
COMMENT ON COLUMN "course_environment_profiles"."id" IS '개발환경 프로파일 ID';
COMMENT ON COLUMN "course_environment_profiles"."course_id" IS '강좌 ID';
COMMENT ON COLUMN "course_environment_profiles"."week" IS '주차 번호';
COMMENT ON COLUMN "course_environment_profiles"."platform_key" IS '대상 플랫폼 키';
COMMENT ON COLUMN "course_environment_profiles"."profile_type" IS '환경 프로파일 유형';
COMMENT ON COLUMN "course_environment_profiles"."name" IS '프로파일 이름';
COMMENT ON COLUMN "course_environment_profiles"."config" IS '컨테이너, 브릿지, 뷰어 실행 설정 JSON';
COMMENT ON COLUMN "course_environment_profiles"."enabled" IS '프로파일 사용 여부';
COMMENT ON COLUMN "course_environment_profiles"."created_at" IS '레코드 생성 시각';
COMMENT ON COLUMN "course_environment_profiles"."updated_at" IS '레코드 수정 시각';
COMMENT ON TABLE "visualization_platforms" IS '시각화 화면에서 선택 가능한 주행/시뮬레이션 스택 마스터.';
COMMENT ON COLUMN "visualization_platforms"."platform_key" IS '시각화 플랫폼 키';
COMMENT ON COLUMN "visualization_platforms"."type" IS '시각화 스택 유형. AUTOWARE, NAV2, SLAM 등';
COMMENT ON COLUMN "visualization_platforms"."display_name" IS '시각화 플랫폼 표시명';
COMMENT ON COLUMN "visualization_platforms"."description" IS '시각화 플랫폼 설명';
COMMENT ON COLUMN "visualization_platforms"."default_config" IS '기본 시각화 실행 설정 JSON';
COMMENT ON COLUMN "visualization_platforms"."sort_order" IS '시각화 플랫폼 표시 순서';
COMMENT ON COLUMN "visualization_platforms"."enabled" IS '시각화 플랫폼 사용 여부';
COMMENT ON COLUMN "visualization_platforms"."created_at" IS '레코드 생성 시각';
COMMENT ON COLUMN "visualization_platforms"."updated_at" IS '레코드 수정 시각';
COMMENT ON TABLE "course_week_visualization_platforms" IS '강좌 주차별 허용 시각화 플랫폼 선택값.';
COMMENT ON COLUMN "course_week_visualization_platforms"."id" IS '주차 시각화 플랫폼 선택 ID';
COMMENT ON COLUMN "course_week_visualization_platforms"."course_id" IS '강좌 ID';
COMMENT ON COLUMN "course_week_visualization_platforms"."week" IS '주차 번호';
COMMENT ON COLUMN "course_week_visualization_platforms"."platform_key" IS '시각화 플랫폼 키';
COMMENT ON COLUMN "course_week_visualization_platforms"."enabled" IS '주차에서 해당 시각화 플랫폼 사용 여부';
COMMENT ON COLUMN "course_week_visualization_platforms"."created_at" IS '레코드 생성 시각';
COMMENT ON COLUMN "course_week_visualization_platforms"."updated_at" IS '레코드 수정 시각';
COMMENT ON TABLE "platform_control_sessions" IS '플랫폼 제어권을 실제로 점유 중이거나 종료된 세션.';
COMMENT ON COLUMN "platform_control_sessions"."id" IS '제어권 세션 ID';
COMMENT ON COLUMN "platform_control_sessions"."organization_id" IS '세션 소속 조직 ID';
COMMENT ON COLUMN "platform_control_sessions"."organization_name" IS '세션 소속 조직명';
COMMENT ON COLUMN "platform_control_sessions"."platform_id" IS '플랫폼 마스터 ID';
COMMENT ON COLUMN "platform_control_sessions"."instructor_platform_id" IS '강사 등록 플랫폼 ID';
COMMENT ON COLUMN "platform_control_sessions"."holder_user_id" IS '제어권 보유 사용자 ID';
COMMENT ON COLUMN "platform_control_sessions"."group_id" IS '제어권 대상 수강생 그룹 ID';
COMMENT ON COLUMN "platform_control_sessions"."status" IS '제어권 세션 상태';
COMMENT ON COLUMN "platform_control_sessions"."started_at" IS '제어권 시작 시각';
COMMENT ON COLUMN "platform_control_sessions"."ends_at" IS '제어권 만료 예정 시각';
COMMENT ON COLUMN "platform_control_sessions"."warning_at" IS '만료 경고 예정 시각';
COMMENT ON COLUMN "platform_control_sessions"."warning_sent_at" IS '만료 경고 발송 시각';
COMMENT ON COLUMN "platform_control_sessions"."released_at" IS '제어권 반납 또는 종료 시각';
COMMENT ON COLUMN "platform_control_sessions"."end_reason" IS '제어권 종료 사유';
COMMENT ON COLUMN "platform_control_sessions"."created_at" IS '레코드 생성 시각';
COMMENT ON COLUMN "platform_control_sessions"."updated_at" IS '레코드 수정 시각';
COMMENT ON TABLE "platform_control_queue_entries" IS '플랫폼 제어권 대기 예약 목록.';
COMMENT ON COLUMN "platform_control_queue_entries"."id" IS '제어권 대기 ID';
COMMENT ON COLUMN "platform_control_queue_entries"."organization_id" IS '대기 소속 조직 ID';
COMMENT ON COLUMN "platform_control_queue_entries"."organization_name" IS '대기 소속 조직명';
COMMENT ON COLUMN "platform_control_queue_entries"."platform_id" IS '플랫폼 마스터 ID';
COMMENT ON COLUMN "platform_control_queue_entries"."instructor_platform_id" IS '강사 등록 플랫폼 ID';
COMMENT ON COLUMN "platform_control_queue_entries"."user_id" IS '대기 신청 사용자 ID';
COMMENT ON COLUMN "platform_control_queue_entries"."group_id" IS '대기 대상 수강생 그룹 ID';
COMMENT ON COLUMN "platform_control_queue_entries"."status" IS '대기 예약 상태';
COMMENT ON COLUMN "platform_control_queue_entries"."requested_at" IS '대기 신청 시각';
COMMENT ON COLUMN "platform_control_queue_entries"."granted_at" IS '제어권 배정 시각';
COMMENT ON COLUMN "platform_control_queue_entries"."canceled_at" IS '대기 취소 시각';
COMMENT ON COLUMN "platform_control_queue_entries"."created_at" IS '레코드 생성 시각';
COMMENT ON COLUMN "platform_control_queue_entries"."updated_at" IS '레코드 수정 시각';
COMMENT ON TABLE "course_student_progress" IS '학생별 강좌 전체 학습 현황 집계.';
COMMENT ON COLUMN "course_student_progress"."id" IS '강좌 학습 현황 ID';
COMMENT ON COLUMN "course_student_progress"."course_id" IS '강좌 ID';
COMMENT ON COLUMN "course_student_progress"."user_id" IS '학생 사용자 ID';
COMMENT ON COLUMN "course_student_progress"."completed_weeks" IS '완료한 주차 수';
COMMENT ON COLUMN "course_student_progress"."total_weeks" IS '전체 주차 수';
COMMENT ON COLUMN "course_student_progress"."learning_hours" IS '누적 학습 시간 분';
COMMENT ON COLUMN "course_student_progress"."commit_count" IS '소스 제출 커밋 수';
COMMENT ON COLUMN "course_student_progress"."average_score" IS '평균 점수';
COMMENT ON COLUMN "course_student_progress"."created_at" IS '레코드 생성 시각';
COMMENT ON COLUMN "course_student_progress"."updated_at" IS '레코드 수정 시각';
COMMENT ON TABLE "course_student_week_progress" IS '학생별 주차 제출 및 진행 현황.';
COMMENT ON COLUMN "course_student_week_progress"."id" IS '주차 학습 현황 ID';
COMMENT ON COLUMN "course_student_week_progress"."course_id" IS '강좌 ID';
COMMENT ON COLUMN "course_student_week_progress"."user_id" IS '학생 사용자 ID';
COMMENT ON COLUMN "course_student_week_progress"."week" IS '주차 번호';
COMMENT ON COLUMN "course_student_week_progress"."submitted_at" IS '학생 제출 완료 시각';
COMMENT ON COLUMN "course_student_week_progress"."commit_count" IS '해당 주차 제출 커밋 수';
COMMENT ON COLUMN "course_student_week_progress"."created_at" IS '레코드 생성 시각';
COMMENT ON COLUMN "course_student_week_progress"."updated_at" IS '레코드 수정 시각';
COMMENT ON TABLE "studio_repositories" IS '학생별 주차 모듈 Gitea 저장소 매핑.';
COMMENT ON COLUMN "studio_repositories"."id" IS '스튜디오 저장소 매핑 ID';
COMMENT ON COLUMN "studio_repositories"."user_id" IS '학생 사용자 ID';
COMMENT ON COLUMN "studio_repositories"."week_id" IS '주차 커리큘럼 ID';
COMMENT ON COLUMN "studio_repositories"."module_id" IS '주차 모듈 ID';
COMMENT ON COLUMN "studio_repositories"."repo_owner" IS 'Gitea 저장소 owner 또는 조직';
COMMENT ON COLUMN "studio_repositories"."repo_name" IS 'Gitea 저장소명';
COMMENT ON COLUMN "studio_repositories"."repository_url" IS '학생 작업 저장소 URL';
COMMENT ON COLUMN "studio_repositories"."template_repository_url" IS '원본 템플릿 저장소 URL';
COMMENT ON COLUMN "studio_repositories"."created_at" IS '레코드 생성 시각';
COMMENT ON COLUMN "studio_repositories"."updated_at" IS '레코드 수정 시각';
COMMENT ON TABLE "submission_scores" IS '학생 제출물의 모듈별 점수.';
COMMENT ON COLUMN "submission_scores"."id" IS '모듈별 제출 점수 ID';
COMMENT ON COLUMN "submission_scores"."course_id" IS '강좌 ID';
COMMENT ON COLUMN "submission_scores"."week_id" IS '주차 커리큘럼 ID';
COMMENT ON COLUMN "submission_scores"."module_id" IS '주차 모듈 ID';
COMMENT ON COLUMN "submission_scores"."student_id" IS '학생 사용자 ID';
COMMENT ON COLUMN "submission_scores"."score" IS '모듈별 점수';
COMMENT ON COLUMN "submission_scores"."created_at" IS '레코드 생성 시각';
COMMENT ON COLUMN "submission_scores"."updated_at" IS '레코드 수정 시각';
COMMENT ON TABLE "submission_total_scores" IS '학생 제출물의 주차별 총점.';
COMMENT ON COLUMN "submission_total_scores"."id" IS '주차 제출 총점 ID';
COMMENT ON COLUMN "submission_total_scores"."course_id" IS '강좌 ID';
COMMENT ON COLUMN "submission_total_scores"."week_id" IS '주차 커리큘럼 ID';
COMMENT ON COLUMN "submission_total_scores"."student_id" IS '학생 사용자 ID';
COMMENT ON COLUMN "submission_total_scores"."score" IS '주차 제출 총점';
COMMENT ON COLUMN "submission_total_scores"."created_at" IS '레코드 생성 시각';
COMMENT ON COLUMN "submission_total_scores"."updated_at" IS '레코드 수정 시각';
COMMENT ON TABLE "chat_threads" IS '개발 스튜디오 질의응답 채팅방.';
COMMENT ON COLUMN "chat_threads"."id" IS '채팅방 ID';
COMMENT ON COLUMN "chat_threads"."course_id" IS '채팅 대상 강좌 ID';
COMMENT ON COLUMN "chat_threads"."week_id" IS '채팅 대상 주차 ID';
COMMENT ON COLUMN "chat_threads"."student_id" IS '질문 학생 사용자 ID';
COMMENT ON COLUMN "chat_threads"."assigned_instructor_id" IS '배정된 강사 사용자 ID';
COMMENT ON COLUMN "chat_threads"."type" IS '채팅방 유형. AI, 강사, 혼합';
COMMENT ON COLUMN "chat_threads"."status" IS '채팅방 처리 상태';
COMMENT ON COLUMN "chat_threads"."title" IS '채팅방 제목';
COMMENT ON COLUMN "chat_threads"."created_at" IS '레코드 생성 시각';
COMMENT ON COLUMN "chat_threads"."updated_at" IS '레코드 수정 시각';
COMMENT ON COLUMN "chat_threads"."last_message_at" IS '마지막 메시지 작성 시각';
COMMENT ON TABLE "chat_messages" IS '개발 스튜디오 질의응답 메시지.';
COMMENT ON COLUMN "chat_messages"."id" IS '채팅 메시지 ID';
COMMENT ON COLUMN "chat_messages"."thread_id" IS '소속 채팅방 ID';
COMMENT ON COLUMN "chat_messages"."sender_type" IS '메시지 발신자 유형';
COMMENT ON COLUMN "chat_messages"."sender_user_id" IS '발신 사용자 ID';
COMMENT ON COLUMN "chat_messages"."content" IS '메시지 내용';
COMMENT ON COLUMN "chat_messages"."content_format" IS '메시지 내용 형식';
COMMENT ON COLUMN "chat_messages"."metadata" IS '메시지 부가 메타데이터 JSON';
COMMENT ON COLUMN "chat_messages"."created_at" IS '레코드 생성 시각';
COMMENT ON COLUMN "chat_messages"."deleted_at" IS '메시지 삭제 시각';
COMMENT ON TABLE "ai_chat_runs" IS 'AI 채팅 응답 생성 실행 로그.';
COMMENT ON COLUMN "ai_chat_runs"."id" IS 'AI 실행 로그 ID';
COMMENT ON COLUMN "ai_chat_runs"."message_id" IS 'AI 응답 메시지 ID';
COMMENT ON COLUMN "ai_chat_runs"."model" IS '사용한 AI 모델명';
COMMENT ON COLUMN "ai_chat_runs"."prompt_tokens" IS '프롬프트 토큰 수';
COMMENT ON COLUMN "ai_chat_runs"."completion_tokens" IS '응답 토큰 수';
COMMENT ON COLUMN "ai_chat_runs"."total_tokens" IS '전체 토큰 수';
COMMENT ON COLUMN "ai_chat_runs"."latency_ms" IS 'AI 응답 지연 시간 밀리초';
COMMENT ON COLUMN "ai_chat_runs"."status" IS 'AI 실행 상태';
COMMENT ON COLUMN "ai_chat_runs"."error" IS 'AI 실행 오류 메시지';
COMMENT ON COLUMN "ai_chat_runs"."created_at" IS '레코드 생성 시각';
@@ -0,0 +1,25 @@
ALTER TABLE "student_groups"
ADD COLUMN "instructor_platform_id" UUID;
UPDATE "student_groups" AS g
SET "instructor_platform_id" = p."id"
FROM "instructor_platforms" AS p
WHERE g."platform_name" IS NOT NULL
AND g."platform_name" = p."display_name"
AND (
g."organization_id" = p."organization_id"
OR g."organization_name" = p."organization_name"
);
CREATE INDEX "student_groups_instructor_platform_id_idx"
ON "student_groups"("instructor_platform_id");
ALTER TABLE "student_groups"
ADD CONSTRAINT "student_groups_instructor_platform_id_fkey"
FOREIGN KEY ("instructor_platform_id")
REFERENCES "instructor_platforms"("id")
ON DELETE SET NULL
ON UPDATE CASCADE;
COMMENT ON COLUMN "student_groups"."platform_name" IS '기존 호환용 플랫폼 표시명. 실제 배정 참조는 instructor_platform_id를 사용한다.';
COMMENT ON COLUMN "student_groups"."instructor_platform_id" IS '그룹에 배정된 강사 등록 플랫폼 ID';
@@ -0,0 +1,4 @@
ALTER TABLE "instructor_curriculum_week_overrides"
ADD COLUMN "topic_video_urls" JSONB;
COMMENT ON COLUMN "instructor_curriculum_week_overrides"."topic_video_urls" IS '학습단계별 동영상 강의 URL 목록';
@@ -0,0 +1,7 @@
ALTER TABLE "course_week_references"
ADD COLUMN IF NOT EXISTS "kind" TEXT NOT NULL DEFAULT 'REFERENCE';
CREATE INDEX IF NOT EXISTS "course_week_references_week_id_kind_idx"
ON "course_week_references"("week_id", "kind");
COMMENT ON COLUMN "course_week_references"."kind" IS '주차 파일 자료 유형. REFERENCE는 참고자료, EXAMPLE_CODE는 예제 코드 파일입니다.';
@@ -0,0 +1,11 @@
CREATE TYPE "ChatMessageStatus" AS ENUM ('ACTIVE', 'CANCELED');
ALTER TABLE "chat_messages"
ADD COLUMN "status" "ChatMessageStatus" NOT NULL DEFAULT 'ACTIVE',
ADD COLUMN "canceled_at" TIMESTAMPTZ(3);
CREATE INDEX "chat_messages_thread_id_status_created_at_idx"
ON "chat_messages"("thread_id", "status", "created_at");
COMMENT ON COLUMN "chat_messages"."status" IS '채팅 메시지 처리 상태. AI 응답 중단 시 CANCELED로 표시해 다음 AI 컨텍스트에서 제외한다.';
COMMENT ON COLUMN "chat_messages"."canceled_at" IS 'AI 응답이 사용자에 의해 중단된 시각.';
@@ -0,0 +1,6 @@
ALTER TABLE "course_week_modules"
ADD COLUMN IF NOT EXISTS "source_type" TEXT NOT NULL DEFAULT 'GIT',
ADD COLUMN IF NOT EXISTS "prebuilt_artifacts" JSONB NOT NULL DEFAULT '[]';
COMMENT ON COLUMN "course_week_modules"."source_type" IS '모듈 소스 제공 방식입니다. 기본값 GIT은 template_repository_url을 사용합니다.';
COMMENT ON COLUMN "course_week_modules"."prebuilt_artifacts" IS 'R2 등 외부 저장소에 둔 모듈별 사전 빌드 산출물 목록입니다. build/install tar 아카이브를 기록합니다.';
@@ -0,0 +1,49 @@
UPDATE "course_environment_profiles"
SET "config" = jsonb_set(
jsonb_set("config", '{bridge,image}', '"nav2-slam:humble"'::jsonb, true),
'{bridge,buildContext}',
'"resources/slam-ros-bridge"'::jsonb,
true
),
"updated_at" = CURRENT_TIMESTAMP
WHERE upper("profile_type") = 'SLAM';
UPDATE "course_environment_profiles"
SET "config" = jsonb_set(
"config",
'{runtime,containers}',
COALESCE((
SELECT jsonb_agg(container)
FROM jsonb_array_elements("config" #> '{runtime,containers}') AS container
WHERE container->>'name' <> 'slam-ros-bridge'
), '[]'::jsonb),
true
),
"updated_at" = CURRENT_TIMESTAMP
WHERE upper("profile_type") = 'SLAM'
AND jsonb_typeof("config" #> '{runtime,containers}') = 'array';
UPDATE "visualization_platforms"
SET "default_config" = jsonb_set(
jsonb_set("default_config", '{bridge,image}', '"nav2-slam:humble"'::jsonb, true),
'{bridge,buildContext}',
'"resources/slam-ros-bridge"'::jsonb,
true
),
"updated_at" = CURRENT_TIMESTAMP
WHERE "platform_key" = 'SLAM';
UPDATE "visualization_platforms"
SET "default_config" = jsonb_set(
"default_config",
'{runtime,containers}',
COALESCE((
SELECT jsonb_agg(container)
FROM jsonb_array_elements("default_config" #> '{runtime,containers}') AS container
WHERE container->>'name' <> 'slam-ros-bridge'
), '[]'::jsonb),
true
),
"updated_at" = CURRENT_TIMESTAMP
WHERE "platform_key" = 'SLAM'
AND jsonb_typeof("default_config" #> '{runtime,containers}') = 'array';
@@ -0,0 +1,139 @@
INSERT INTO "users" ("id", "email", "name", "auth_role", "primary_organization_name", "createdAt", "updatedAt")
VALUES (gen_random_uuid(), 'system@autonomystudio.local', 'Autonomy Studio', 'SYSTEM', 'Autonomy Studio', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
ON CONFLICT ("email") DO UPDATE
SET "name" = EXCLUDED."name",
"auth_role" = EXCLUDED."auth_role",
"primary_organization_name" = EXCLUDED."primary_organization_name",
"updatedAt" = CURRENT_TIMESTAMP;
WITH teacher AS (
SELECT "id" FROM "users" WHERE "email" = 'system@autonomystudio.local'
)
INSERT INTO "courses" ("slug", "title", "subtitle", "description", "organization_name", "teacher_id", "is_active", "updated_at")
SELECT
'autonomy-studio-lms-guide',
'NAV2 알아보기',
'처음 사용하는 수강생을 위한 짧은 체험 강좌',
'강좌 배정 전 신규 사용자가 LMS 화면 흐름을 빠르게 익힐 수 있는 공통 체험 강좌입니다. NAV2 개념을 예시로 커리큘럼 확인, 학습단계 열람, 질의응답 사용법만 간단히 안내합니다.',
'Autonomy Studio',
teacher."id",
true,
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" = 'autonomy-studio-lms-guide'
)
INSERT INTO "course_weeks" (
"course_id",
"week",
"title",
"tags",
"progress",
"state",
"duration",
"github",
"overview",
"deliverable",
"requires_source_submission",
"updated_at"
)
SELECT
course."id",
1,
'NAV2 알아보기',
ARRAY['NAV2','진행중']::TEXT[],
0,
'current',
'15분',
'제출 없음',
'NAV2가 어떤 역할을 하는지 간단히 살펴보고, LMS에서 강좌 내용과 학습단계, 질문 기능을 어떻게 사용하는지 확인합니다.',
'이 강좌는 체험용이므로 별도 제출은 없습니다. 실제 수업 강좌가 배정되면 해당 강좌가 우선 표시됩니다.',
false,
CURRENT_TIMESTAMP
FROM course
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",
"requires_source_submission" = false,
"updated_at" = CURRENT_TIMESTAMP;
DELETE FROM "course_weeks"
USING "courses"
WHERE "course_weeks"."course_id" = "courses"."id"
AND "courses"."slug" = 'autonomy-studio-lms-guide'
AND "course_weeks"."week" <> 1;
DELETE FROM "course_week_practices"
USING "course_weeks", "courses"
WHERE "course_week_practices"."week_id" = "course_weeks"."id"
AND "course_weeks"."course_id" = "courses"."id"
AND "courses"."slug" = 'autonomy-studio-lms-guide';
DELETE FROM "course_week_topics"
USING "course_weeks", "courses"
WHERE "course_week_topics"."week_id" = "course_weeks"."id"
AND "course_weeks"."course_id" = "courses"."id"
AND "courses"."slug" = 'autonomy-studio-lms-guide';
WITH week1 AS (
SELECT w."id" AS week_id
FROM "course_weeks" w
JOIN "courses" c ON c."id" = w."course_id"
WHERE c."slug" = 'autonomy-studio-lms-guide' AND w."week" = 1
)
INSERT INTO "course_week_topics" ("week_id", "sort_order", "title", "detail")
SELECT week1.week_id, data.sort_order, data.title, data.detail
FROM week1
CROSS JOIN (
VALUES
(1, 'NAV2란 무엇인가', 'NAV2는 ROS 2 기반 로봇 내비게이션 스택입니다. 지도, 위치추정, 경로계획, 제어 흐름이 어떻게 이어지는지 큰 그림을 확인합니다.'),
(2, 'LMS에서 강좌 읽기', '주차 카드와 학습단계를 열어 강의 내용, 참고 자료, 진행 상태를 확인하는 방법을 익힙니다.'),
(3, '질문 기능 사용하기', '학습 중 궁금한 내용을 AI 또는 강사에게 질문하고, 답변을 확인하는 기본 흐름을 살펴봅니다.')
) AS data(sort_order, title, detail);
WITH week1 AS (
SELECT w."id" AS week_id
FROM "course_weeks" w
JOIN "courses" c ON c."id" = w."course_id"
WHERE c."slug" = 'autonomy-studio-lms-guide' AND w."week" = 1
)
INSERT INTO "course_week_practices" ("week_id", "sort_order", "title", "description")
SELECT week1.week_id, data.sort_order, data.title, data.description
FROM week1
CROSS JOIN (
VALUES
(1, '화면 둘러보기', '커리큘럼 화면에서 주차 카드, 학습단계, 강의 내용을 차례로 확인합니다.'),
(2, '질문 남겨보기', '질의응답 화면에서 NAV2 관련 질문을 입력하는 위치를 확인합니다.')
) AS data(sort_order, title, description);
WITH guide AS (
SELECT "id" FROM "courses" WHERE "slug" = 'autonomy-studio-lms-guide'
),
local_platform AS (
SELECT "id" FROM "platforms" WHERE "platform_key" = 'futuredrive-local-pc'
)
INSERT INTO "course_platforms" ("course_id", "platform_id", "status", "action", "state", "sort_order", "updated_at")
SELECT guide."id", local_platform."id", '사용 가능', '연결', 'ready', 1, CURRENT_TIMESTAMP
FROM guide
CROSS JOIN local_platform
ON CONFLICT ("course_id", "platform_id") DO UPDATE
SET "status" = EXCLUDED."status",
"action" = EXCLUDED."action",
"state" = EXCLUDED."state",
"sort_order" = EXCLUDED."sort_order",
"updated_at" = CURRENT_TIMESTAMP;
@@ -0,0 +1,4 @@
ALTER TABLE "instructor_curriculum_week_overrides"
ADD COLUMN IF NOT EXISTS "learning_steps" JSONB;
COMMENT ON COLUMN "instructor_curriculum_week_overrides"."learning_steps" IS '학습단계별 제목, 설명, 동영상, 핵심개념을 묶은 JSON 목록.';
@@ -0,0 +1,4 @@
ALTER TABLE "course_week_modules"
ADD COLUMN IF NOT EXISTS "default_parameters" JSONB NOT NULL DEFAULT '[]';
COMMENT ON COLUMN "course_week_modules"."default_parameters" IS '주차 모듈 실행 시 적용하는 기본 ROS 파라미터 배열.';
@@ -0,0 +1,45 @@
CREATE TABLE "course_maps" (
"id" UUID NOT NULL,
"course_id" UUID NOT NULL,
"key" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"platform_key" TEXT NOT NULL DEFAULT 'NAV2',
"world_file" TEXT NOT NULL DEFAULT 'empty_world.world',
"viewer_model" TEXT NOT NULL DEFAULT 'turtlebot3_world',
"layout" JSONB NOT NULL DEFAULT '{"version":1,"objects":[]}',
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL,
CONSTRAINT "course_maps_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "course_week_maps" (
"id" UUID NOT NULL,
"course_id" UUID NOT NULL,
"week" INTEGER NOT NULL,
"map_key" TEXT NOT NULL,
"sort_order" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL,
CONSTRAINT "course_week_maps_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "course_maps_course_id_key_key" ON "course_maps"("course_id", "key");
CREATE INDEX "course_maps_course_id_platform_key_idx" ON "course_maps"("course_id", "platform_key");
CREATE UNIQUE INDEX "course_week_maps_course_id_week_map_key_key" ON "course_week_maps"("course_id", "week", "map_key");
CREATE INDEX "course_week_maps_course_id_week_idx" ON "course_week_maps"("course_id", "week");
ALTER TABLE "course_maps" ADD CONSTRAINT "course_maps_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "courses"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "course_week_maps" ADD CONSTRAINT "course_week_maps_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "courses"("id") ON DELETE CASCADE ON UPDATE CASCADE;
INSERT INTO "course_week_maps" ("id", "course_id", "week", "map_key", "sort_order", "updated_at")
SELECT gen_random_uuid(), "course_id", "week", 'nav2-turtlebot3-world', 0, CURRENT_TIMESTAMP
FROM "course_week_visualization_platforms"
WHERE "platform_key" = 'NAV2' AND "enabled" = true
ON CONFLICT ("course_id", "week", "map_key") DO NOTHING;
INSERT INTO "course_week_maps" ("id", "course_id", "week", "map_key", "sort_order", "updated_at")
SELECT gen_random_uuid(), "course_id", "week", 'nav2-turtlebot3-house', 1, CURRENT_TIMESTAMP
FROM "course_week_visualization_platforms"
WHERE "platform_key" = 'NAV2' AND "enabled" = true
ON CONFLICT ("course_id", "week", "map_key") DO NOTHING;
@@ -0,0 +1,27 @@
CREATE TABLE "student_saved_maps" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"user_id" UUID NOT NULL,
"course_id" UUID NOT NULL,
"week" INTEGER NOT NULL,
"name" TEXT NOT NULL,
"source_map_key" TEXT NOT NULL,
"world_file" TEXT NOT NULL,
"viewer_model" TEXT NOT NULL,
"yaml_path" TEXT NOT NULL,
"pgm_path" TEXT NOT NULL,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL,
CONSTRAINT "student_saved_maps_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "student_saved_maps_user_id_course_id_week_yaml_path_key"
ON "student_saved_maps"("user_id", "course_id", "week", "yaml_path");
CREATE INDEX "student_saved_maps_user_id_course_id_week_created_at_idx"
ON "student_saved_maps"("user_id", "course_id", "week", "created_at");
ALTER TABLE "student_saved_maps"
ADD CONSTRAINT "student_saved_maps_user_id_fkey"
FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "student_saved_maps"
ADD CONSTRAINT "student_saved_maps_course_id_fkey"
FOREIGN KEY ("course_id") REFERENCES "courses"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,27 @@
INSERT INTO "visualization_platforms" (
"platform_key", "type", "display_name", "description", "default_config", "sort_order", "enabled", "created_at", "updated_at"
) VALUES
(
'REAL_SLAM',
'REAL_SLAM',
'실차 SLAM',
'원격 플랫폼의 실제 센서로 지도를 생성합니다.',
'{"type":"REAL_SLAM","target":"remote-jetson","runtimeKind":"real","viewer":{"stack":"nav2","mode":"slam"},"bridge":{"name":"nav2-ros-bridge","port":8765,"image":"slam-ros-bridge:latest","env":{"ROBOT_STACK":"nav2"}},"runtimeImages":[{"url":"https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/runtime-images/slam-ros-bridge/latest/slam-ros-bridge-latest.tar.zst","image":"slam-ros-bridge:latest","fileName":"slam-ros-bridge-latest.tar.zst","required":true}]}'::jsonb,
30, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
),
(
'REAL_NAVIGATION',
'REAL_NAVIGATION',
'실차 자율주행',
'실차 SLAM 결과 맵에서 초기 위치와 목표점을 지정해 주행합니다.',
'{"type":"REAL_NAVIGATION","target":"remote-jetson","runtimeKind":"real","viewer":{"stack":"nav2","mode":"navigation"},"bridge":{"name":"nav2-ros-bridge","port":8765,"image":"slam-ros-bridge:latest","env":{"ROBOT_STACK":"nav2"}},"runtimeImages":[{"url":"https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/runtime-images/slam-ros-bridge/latest/slam-ros-bridge-latest.tar.zst","image":"slam-ros-bridge:latest","fileName":"slam-ros-bridge-latest.tar.zst","required":true}]}'::jsonb,
31, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
)
ON CONFLICT ("platform_key") DO UPDATE SET
"type" = EXCLUDED."type",
"display_name" = EXCLUDED."display_name",
"description" = EXCLUDED."description",
"default_config" = EXCLUDED."default_config",
"sort_order" = EXCLUDED."sort_order",
"enabled" = EXCLUDED."enabled",
"updated_at" = CURRENT_TIMESTAMP;
@@ -0,0 +1,12 @@
UPDATE "visualization_platforms"
SET
"display_name" = CASE "platform_key"
WHEN 'REAL_SLAM' THEN '플랫폼 SLAM'
WHEN 'REAL_NAVIGATION' THEN '플랫폼 자율주행'
END,
"description" = CASE "platform_key"
WHEN 'REAL_SLAM' THEN '원격 플랫폼의 센서로 지도를 생성합니다.'
WHEN 'REAL_NAVIGATION' THEN '플랫폼 SLAM 결과 맵에서 초기 위치와 목표점을 지정해 주행합니다.'
END,
"updated_at" = CURRENT_TIMESTAMP
WHERE "platform_key" IN ('REAL_SLAM', 'REAL_NAVIGATION');
@@ -0,0 +1,2 @@
ALTER TABLE "platform_control_sessions"
ADD COLUMN "extension_count" INTEGER NOT NULL DEFAULT 0;
@@ -0,0 +1,18 @@
INSERT INTO "visualization_platforms" (
"platform_key", "type", "display_name", "description", "default_config", "sort_order", "enabled", "created_at", "updated_at"
) VALUES (
'REAL_SLAM_3D',
'REAL_SLAM_3D',
'플랫폼 3D SLAM',
'플랫폼의 3D LiDAR 포인트클라우드와 ICP 위치를 읽기 전용으로 표시합니다.',
'{"type":"REAL_SLAM_3D","target":"remote-jetson","runtimeKind":"real","viewer":{"stack":"nav2","mode":"slam3d"},"bridge":{"name":"nav2-ros-bridge","port":8765,"image":"slam-ros-bridge:jetson_orin-3d","env":{"ROBOT_STACK":"nav2","READ_ONLY":"true","ODOM_TOPIC":"/odom","ICP_ODOM_TOPIC":"/icp_odom","SCAN_TOPIC":"/scan","POINT_CLOUD_TOPIC":"/cloud_map","FALLBACK_POINT_CLOUD_TOPIC":"/lidar_filtered"}},"runtimeImages":[{"url":"https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/runtime-images/slam-ros-bridge/jetson-orin-3d/slam-ros-bridge-jetson-orin-3d.tar.zst","image":"slam-ros-bridge:jetson_orin-3d","fileName":"slam-ros-bridge-jetson-orin-3d.tar.zst","required":true}]}'::jsonb,
32, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
)
ON CONFLICT ("platform_key") DO UPDATE SET
"type" = EXCLUDED."type",
"display_name" = EXCLUDED."display_name",
"description" = EXCLUDED."description",
"default_config" = EXCLUDED."default_config",
"sort_order" = EXCLUDED."sort_order",
"enabled" = EXCLUDED."enabled",
"updated_at" = CURRENT_TIMESTAMP;
@@ -0,0 +1,2 @@
ALTER TABLE "student_saved_maps"
ADD COLUMN "database_path" TEXT;
@@ -0,0 +1,2 @@
ALTER TABLE "student_saved_maps"
ADD COLUMN "preview_pgm" TEXT;
@@ -0,0 +1,31 @@
DO $$
DECLARE
target record;
BEGIN
FOR target IN
SELECT * FROM (VALUES
('student_enrollments', 'created_at'),
('student_enrollments', 'updated_at'),
('student_enrollments', 'removed_at'),
('student_groups', 'created_at'),
('student_groups', 'updated_at'),
('student_group_members', 'created_at')
) AS targets(table_name, column_name)
LOOP
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = target.table_name
AND column_name = target.column_name
AND data_type = 'timestamp without time zone'
) THEN
EXECUTE format(
'ALTER TABLE %I ALTER COLUMN %I TYPE TIMESTAMPTZ(3) USING %I AT TIME ZONE ''UTC''',
target.table_name,
target.column_name,
target.column_name
);
END IF;
END LOOP;
END $$;
@@ -0,0 +1,31 @@
CREATE TABLE "student_custom_modules" (
"id" UUID NOT NULL,
"user_id" UUID NOT NULL,
"course_id" UUID NOT NULL,
"week" INTEGER NOT NULL,
"sort_order" INTEGER NOT NULL,
"category" TEXT NOT NULL,
"name" TEXT NOT NULL,
"folder_name" TEXT NOT NULL,
"docker_image" TEXT,
"package_name" TEXT NOT NULL,
"launch_file" TEXT NOT NULL,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL,
CONSTRAINT "student_custom_modules_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "student_custom_modules_user_id_course_id_week_folder_name_key"
ON "student_custom_modules"("user_id", "course_id", "week", "folder_name");
CREATE INDEX "student_custom_modules_user_id_course_id_week_sort_order_idx"
ON "student_custom_modules"("user_id", "course_id", "week", "sort_order");
ALTER TABLE "student_custom_modules"
ADD CONSTRAINT "student_custom_modules_user_id_fkey"
FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "student_custom_modules"
ADD CONSTRAINT "student_custom_modules_course_id_fkey"
FOREIGN KEY ("course_id") REFERENCES "courses"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,3 @@
UPDATE "student_custom_modules"
SET "folder_name" = '[lms-nav2-custom]' || "package_name" || '_v1.0.0_ws'
WHERE "folder_name" <> '[lms-nav2-custom]' || "package_name" || '_v1.0.0_ws';
@@ -0,0 +1,12 @@
ALTER TABLE "student_saved_maps"
ADD COLUMN "runtime_kind" TEXT NOT NULL DEFAULT 'SIMULATION';
UPDATE "student_saved_maps"
SET "runtime_kind" = CASE
WHEN "database_path" IS NOT NULL THEN 'PLATFORM_3D'
WHEN "source_map_key" = 'nav2-platform-slam' THEN 'PLATFORM_2D'
ELSE 'SIMULATION'
END;
CREATE INDEX "student_saved_maps_user_id_course_id_week_runtime_kind_idx"
ON "student_saved_maps"("user_id", "course_id", "week", "runtime_kind");
@@ -0,0 +1,4 @@
ALTER TABLE "student_saved_maps"
ADD COLUMN "yaml_url" TEXT,
ADD COLUMN "pgm_url" TEXT,
ADD COLUMN "database_url" TEXT;
@@ -0,0 +1,10 @@
ALTER TABLE "student_custom_modules"
ADD COLUMN "runtime_target" TEXT NOT NULL DEFAULT 'LOCAL';
DROP INDEX "student_custom_modules_user_id_course_id_week_folder_name_key";
CREATE UNIQUE INDEX "student_custom_modules_user_id_course_id_week_folder_name_runtime_target_key"
ON "student_custom_modules"("user_id", "course_id", "week", "folder_name", "runtime_target");
CREATE INDEX "student_custom_modules_user_id_course_id_week_runtime_target_sort_order_idx"
ON "student_custom_modules"("user_id", "course_id", "week", "runtime_target", "sort_order");
@@ -0,0 +1,64 @@
ALTER TABLE "instructor_curriculum_week_overrides"
ADD COLUMN "is_team_project" BOOLEAN DEFAULT false;
CREATE TABLE "team_studio_repositories" (
"id" UUID NOT NULL,
"group_id" UUID NOT NULL,
"week_id" UUID NOT NULL,
"module_id" UUID NOT NULL,
"repo_owner" TEXT NOT NULL,
"repo_name" TEXT NOT NULL,
"repository_url" TEXT NOT NULL,
"template_repository_url" TEXT NOT NULL,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL,
CONSTRAINT "team_studio_repositories_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "team_studio_repositories_group_id_module_id_key"
ON "team_studio_repositories"("group_id", "module_id");
CREATE INDEX "team_studio_repositories_week_id_idx"
ON "team_studio_repositories"("week_id");
CREATE INDEX "team_studio_repositories_repo_owner_repo_name_idx"
ON "team_studio_repositories"("repo_owner", "repo_name");
ALTER TABLE "team_studio_repositories"
ADD CONSTRAINT "team_studio_repositories_group_id_fkey"
FOREIGN KEY ("group_id") REFERENCES "student_groups"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "team_studio_repositories"
ADD CONSTRAINT "team_studio_repositories_module_id_fkey"
FOREIGN KEY ("module_id") REFERENCES "course_week_modules"("id") ON DELETE CASCADE ON UPDATE CASCADE;
CREATE TABLE "team_custom_modules" (
"id" UUID NOT NULL,
"group_id" UUID NOT NULL,
"creator_id" UUID NOT NULL,
"course_id" UUID NOT NULL,
"week" INTEGER NOT NULL,
"sort_order" INTEGER NOT NULL,
"category" TEXT NOT NULL,
"name" TEXT NOT NULL,
"folder_name" TEXT NOT NULL,
"docker_image" TEXT,
"package_name" TEXT NOT NULL,
"launch_file" TEXT NOT NULL,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL,
CONSTRAINT "team_custom_modules_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "team_custom_modules_group_id_course_id_week_folder_name_key"
ON "team_custom_modules"("group_id", "course_id", "week", "folder_name");
CREATE INDEX "team_custom_modules_group_id_course_id_week_sort_order_idx"
ON "team_custom_modules"("group_id", "course_id", "week", "sort_order");
ALTER TABLE "team_custom_modules"
ADD CONSTRAINT "team_custom_modules_group_id_fkey"
FOREIGN KEY ("group_id") REFERENCES "student_groups"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "team_custom_modules"
ADD CONSTRAINT "team_custom_modules_creator_id_fkey"
FOREIGN KEY ("creator_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "team_custom_modules"
ADD CONSTRAINT "team_custom_modules_course_id_fkey"
FOREIGN KEY ("course_id") REFERENCES "courses"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,45 @@
CREATE TABLE "student_custom_module_repositories" (
"id" UUID NOT NULL,
"user_id" UUID NOT NULL,
"custom_module_id" UUID NOT NULL,
"repo_owner" TEXT NOT NULL,
"repo_name" TEXT NOT NULL,
"repository_url" TEXT NOT NULL,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL,
CONSTRAINT "student_custom_module_repositories_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "team_custom_module_repositories" (
"id" UUID NOT NULL,
"group_id" UUID NOT NULL,
"custom_module_id" UUID NOT NULL,
"repo_owner" TEXT NOT NULL,
"repo_name" TEXT NOT NULL,
"repository_url" TEXT NOT NULL,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL,
CONSTRAINT "team_custom_module_repositories_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "student_custom_module_repositories_custom_module_id_key" ON "student_custom_module_repositories"("custom_module_id");
CREATE INDEX "student_custom_module_repositories_user_id_idx" ON "student_custom_module_repositories"("user_id");
CREATE INDEX "student_custom_module_repositories_repo_owner_repo_name_idx" ON "student_custom_module_repositories"("repo_owner", "repo_name");
CREATE UNIQUE INDEX "team_custom_module_repositories_custom_module_id_key" ON "team_custom_module_repositories"("custom_module_id");
CREATE INDEX "team_custom_module_repositories_group_id_idx" ON "team_custom_module_repositories"("group_id");
CREATE INDEX "team_custom_module_repositories_repo_owner_repo_name_idx" ON "team_custom_module_repositories"("repo_owner", "repo_name");
ALTER TABLE "student_custom_module_repositories"
ADD CONSTRAINT "student_custom_module_repositories_user_id_fkey"
FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "student_custom_module_repositories"
ADD CONSTRAINT "student_custom_module_repositories_custom_module_id_fkey"
FOREIGN KEY ("custom_module_id") REFERENCES "student_custom_modules"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "team_custom_module_repositories"
ADD CONSTRAINT "team_custom_module_repositories_group_id_fkey"
FOREIGN KEY ("group_id") REFERENCES "student_groups"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "team_custom_module_repositories"
ADD CONSTRAINT "team_custom_module_repositories_custom_module_id_fkey"
FOREIGN KEY ("custom_module_id") REFERENCES "team_custom_modules"("id") ON DELETE CASCADE ON UPDATE CASCADE;

Some files were not shown because too many files have changed in this diff Show More