From 196ba51ec619705e7c6de42f88dc7aab70689c96 Mon Sep 17 00:00:00 2001 From: chyoo Date: Fri, 7 Aug 2026 10:27:19 +0900 Subject: [PATCH] patch curriculum.. --- .../migration.sql | 106 +++++++++++ .../migration.sql | 2 + .../migration.sql | 53 ++++++ prisma/schema.prisma | 34 ---- src/curriculum/curriculum.controller.ts | 10 + src/curriculum/curriculum.service.ts | 173 ++++++++++++++---- .../dto/update-instructor-curriculum.dto.ts | 5 - src/instructor/instructor.service.ts | 107 +---------- src/studio/studio.service.ts | 62 +++++-- 9 files changed, 353 insertions(+), 199 deletions(-) create mode 100644 prisma/migrations/20260806120000_seed_public_guide_nav2_slam_modules/migration.sql create mode 100644 prisma/migrations/20260806123000_remove_legacy_visualization_platforms/migration.sql create mode 100644 prisma/migrations/20260806124000_add_public_guide_nav2_perception/migration.sql diff --git a/prisma/migrations/20260806120000_seed_public_guide_nav2_slam_modules/migration.sql b/prisma/migrations/20260806120000_seed_public_guide_nav2_slam_modules/migration.sql new file mode 100644 index 0000000..5b3b403 --- /dev/null +++ b/prisma/migrations/20260806120000_seed_public_guide_nav2_slam_modules/migration.sql @@ -0,0 +1,106 @@ +WITH guide_week AS ( + SELECT w."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 +), modules AS ( + SELECT * + FROM ( + VALUES + ( + 1, + 'Simulator', + 'NAV2-Gazebo', + '[lms-nav2-gazebo-simulator]nav2-simulator_v1.0.0_ws', + 'http://192.168.0.242:3000/templates/lms-nav2-gazebo-simulator.git', + 'future-nav2-gazebo:humble', + '["gui:=false"]'::JSONB, + '[{"url":"https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-simulator/1.0.0/prebuilt/latest/lms-nav2-gazebo-simulator-build-install.tar.zst","type":"build-install","fileName":"lms-nav2-gazebo-simulator-build-install.tar.zst","required":false,"extractPath":"."}]'::JSONB + ), + ( + 2, + 'Vehicle', + 'NAV2-TurtleBot3Burger', + '[lms-nav2-gazebo-vehicle]nav2-vehicle_v1.0.0_ws', + 'http://192.168.0.242:3000/templates/lms-nav2-gazebo-vehicle.git', + 'future-nav2-gazebo:humble', + '["sim_mode:=true","platform:=turtlebot3","level:=2d"]'::JSONB, + '[{"url":"https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-vehicle/1.0.0/prebuilt/latest/lms-nav2-gazebo-vehicle-build-install.tar.zst","type":"build-install","fileName":"lms-nav2-gazebo-vehicle-build-install.tar.zst","required":false,"extractPath":"."}]'::JSONB + ), + ( + 3, + 'Localization', + 'NAV2-SLAM', + '[lms-nav2-gazebo-slam]nav2-slam_v1.0.0_ws', + 'http://192.168.0.242:3000/templates/lms-nav2-gazebo-slam.git', + 'future-nav2-gazebo:humble', + '[]'::JSONB, + '[{"url":"https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-slam/1.0.0/prebuilt/latest/lms-nav2-gazebo-slam-build-install.tar.zst","type":"build-install","fileName":"lms-nav2-gazebo-slam-build-install.tar.zst","required":false,"extractPath":"."}]'::JSONB + ) + ) AS value("sort_order", "category", "name", "folder_name", "template_repository_url", "docker_image", "default_parameters", "prebuilt_artifacts") +) +INSERT INTO "course_week_modules" ( + "week_id", + "sort_order", + "category", + "name", + "folder_name", + "template_repository_url", + "source_type", + "prebuilt_artifacts", + "default_parameters", + "docker_image", + "updated_at" +) +SELECT + guide_week."id", + modules."sort_order", + modules."category", + modules."name", + modules."folder_name", + modules."template_repository_url", + 'GIT', + modules."prebuilt_artifacts", + modules."default_parameters", + modules."docker_image", + CURRENT_TIMESTAMP +FROM guide_week +CROSS JOIN modules +ON CONFLICT ("week_id", "folder_name") DO UPDATE +SET "category" = EXCLUDED."category", + "name" = EXCLUDED."name", + "template_repository_url" = EXCLUDED."template_repository_url", + "source_type" = EXCLUDED."source_type", + "prebuilt_artifacts" = EXCLUDED."prebuilt_artifacts", + "default_parameters" = EXCLUDED."default_parameters", + "docker_image" = EXCLUDED."docker_image", + "updated_at" = CURRENT_TIMESTAMP; + +DELETE FROM "course_week_maps" maps +USING "courses" course +WHERE maps."course_id" = course."id" + AND course."slug" = 'autonomy-studio-lms-guide' + AND maps."week" = 1 + AND maps."map_key" <> 'nav2-turtlebot3-world'; + +INSERT INTO "course_week_maps" ( + "id", + "course_id", + "week", + "map_key", + "sort_order", + "updated_at" +) +SELECT + gen_random_uuid(), + course."id", + 1, + 'nav2-turtlebot3-world', + 0, + CURRENT_TIMESTAMP +FROM "courses" course +WHERE course."slug" = 'autonomy-studio-lms-guide' +ON CONFLICT ("course_id", "week", "map_key") DO UPDATE +SET "sort_order" = EXCLUDED."sort_order", + "updated_at" = CURRENT_TIMESTAMP; diff --git a/prisma/migrations/20260806123000_remove_legacy_visualization_platforms/migration.sql b/prisma/migrations/20260806123000_remove_legacy_visualization_platforms/migration.sql new file mode 100644 index 0000000..f26bdd1 --- /dev/null +++ b/prisma/migrations/20260806123000_remove_legacy_visualization_platforms/migration.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS "course_week_visualization_platforms"; +DROP TABLE IF EXISTS "visualization_platforms"; diff --git a/prisma/migrations/20260806124000_add_public_guide_nav2_perception/migration.sql b/prisma/migrations/20260806124000_add_public_guide_nav2_perception/migration.sql new file mode 100644 index 0000000..4eaaf8e --- /dev/null +++ b/prisma/migrations/20260806124000_add_public_guide_nav2_perception/migration.sql @@ -0,0 +1,53 @@ +UPDATE "course_week_modules" modules +SET "sort_order" = 4, + "updated_at" = CURRENT_TIMESTAMP +FROM "course_weeks" week, "courses" course +WHERE modules."week_id" = week."id" + AND week."course_id" = course."id" + AND course."slug" = 'autonomy-studio-lms-guide' + AND week."week" = 1 + AND modules."folder_name" = '[lms-nav2-gazebo-slam]nav2-slam_v1.0.0_ws'; + +WITH guide_week AS ( + SELECT w."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_modules" ( + "week_id", + "sort_order", + "category", + "name", + "folder_name", + "template_repository_url", + "source_type", + "prebuilt_artifacts", + "default_parameters", + "docker_image", + "updated_at" +) +SELECT + guide_week."id", + 3, + 'Perception', + 'NAV2-Perception', + '[lms-nav2-gazebo-perception]nav2-perception_v1.0.0_ws', + 'http://192.168.0.242:3000/templates/lms-nav2-gazebo-perception.git', + 'GIT', + '[{"url":"https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-perception/1.0.0/prebuilt/latest/lms-nav2-gazebo-perception-build-install.tar.zst","type":"build-install","fileName":"lms-nav2-gazebo-perception-build-install.tar.zst","required":false,"extractPath":"."}]'::JSONB, + '["sim_mode:=true"]'::JSONB, + 'future-nav2-gazebo:humble', + CURRENT_TIMESTAMP +FROM guide_week +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", + "source_type" = EXCLUDED."source_type", + "prebuilt_artifacts" = EXCLUDED."prebuilt_artifacts", + "default_parameters" = EXCLUDED."default_parameters", + "docker_image" = EXCLUDED."docker_image", + "updated_at" = CURRENT_TIMESTAMP; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f77e6e6..2345bc8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -162,7 +162,6 @@ model Course { instructorCurriculumSettings InstructorCurriculumSetting[] instructorCurriculumWeekOverrides InstructorCurriculumWeekOverride[] environmentProfiles CourseEnvironmentProfile[] - visualizationSelections CourseWeekVisualizationPlatform[] maps CourseMap[] weekMaps CourseWeekMap[] savedMaps StudentSavedMap[] @@ -195,39 +194,6 @@ model CourseEnvironmentProfile { @@map("course_environment_profiles") } -model VisualizationPlatform { - key String @id @map("platform_key") - type String @unique - displayName String @map("display_name") - description String? - defaultConfig Json @map("default_config") - sortOrder Int @default(0) @map("sort_order") - enabled Boolean @default(true) - createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) - updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) - courseWeekSelections CourseWeekVisualizationPlatform[] - - @@index([enabled, sortOrder]) - @@map("visualization_platforms") -} - -model CourseWeekVisualizationPlatform { - id String @id @default(uuid()) @db.Uuid - courseId String @map("course_id") @db.Uuid - week Int - platformKey String @map("platform_key") - enabled Boolean @default(true) - createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) - updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) - course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) - platform VisualizationPlatform @relation(fields: [platformKey], references: [key], onDelete: Cascade) - - @@unique([courseId, week, platformKey]) - @@index([courseId, week]) - @@index([platformKey]) - @@map("course_week_visualization_platforms") -} - model CourseMap { id String @id @default(uuid()) @db.Uuid courseId String @map("course_id") @db.Uuid diff --git a/src/curriculum/curriculum.controller.ts b/src/curriculum/curriculum.controller.ts index 53af87d..fe5436a 100644 --- a/src/curriculum/curriculum.controller.ts +++ b/src/curriculum/curriculum.controller.ts @@ -46,6 +46,16 @@ export class CurriculumController { return this.curriculumService.listCourses(this.getBearerToken(request)); } + @Get('course-catalog') + searchCourseCatalog(@Req() request: Request, @Query('query') query?: string) { + return this.curriculumService.searchCourseCatalog(this.getBearerToken(request), query); + } + + @Post('course-catalog/:courseId/enroll') + enrollCourse(@Req() request: Request, @Param('courseId') courseId: string) { + return this.curriculumService.enrollCourse(this.getBearerToken(request), courseId); + } + @Get('references/:referenceId/download') async downloadReference( @Req() request: Request, diff --git a/src/curriculum/curriculum.service.ts b/src/curriculum/curriculum.service.ts index 9cfab6b..0d2bdce 100644 --- a/src/curriculum/curriculum.service.ts +++ b/src/curriculum/curriculum.service.ts @@ -2,7 +2,7 @@ import { BadRequestException, ConflictException, Injectable, NotFoundException, import { Prisma } from '@prisma/client'; import { join } from 'node:path'; -import { AuthService } from '../auth/auth.service'; +import { AuthResponseUser, AuthService } from '../auth/auth.service'; import { PlatformControlService } from '../platform-control/platform-control.service'; import { PrismaService } from '../prisma/prisma.service'; import { R2PublicFileService } from '../storage/r2-public-file.service'; @@ -30,20 +30,21 @@ export class CurriculumService { const lmsUser = authUser?.email ? await this.prisma.user.findUnique({ where: { email: authUser.email } }) : null; + const guideCourse = await this.findPublicGuideCourse(lmsUser?.id); + const guideCourseItem = guideCourse ? this.serializeCourseListItem(guideCourse, true) : null; if (!lmsUser) { - const guideCourse = await this.findPublicGuideCourse(); - - return { - courses: guideCourse ? [this.serializeCourseListItem(guideCourse, true)] : [], - }; + return { courses: [], guideCourse: guideCourseItem }; } const enrollments = await this.prisma.studentEnrollment.findMany({ where: { userId: lmsUser.id, status: 'ACTIVE', - course: { isActive: true }, + course: { + isActive: true, + slug: { not: PUBLIC_GUIDE_COURSE_SLUG }, + }, }, include: { course: { @@ -66,19 +67,128 @@ export class CurriculumService { orderBy: { updatedAt: 'desc' }, }); - if (enrollments.length) { - return { - courses: enrollments.map((enrollment) => this.serializeCourseListItem(enrollment.course)), - }; - } + return { + courses: enrollments.map((enrollment) => this.serializeCourseListItem(enrollment.course)), + guideCourse: guideCourseItem, + }; + } - const guideCourse = await this.findPublicGuideCourse(lmsUser.id); + async searchCourseCatalog(accessToken: string | undefined, query?: string) { + const { user: authUser } = await this.authService.me(accessToken); + if (!authUser?.email) throw new UnauthorizedException('Invalid user'); + + const keyword = query?.trim() ?? ''; + const courses = await this.prisma.course.findMany({ + where: { + isActive: true, + slug: { not: PUBLIC_GUIDE_COURSE_SLUG }, + ...(keyword + ? { + OR: [ + { title: { contains: keyword, mode: 'insensitive' } }, + { teacher: { name: { contains: keyword, mode: 'insensitive' } } }, + { teacher: { email: { contains: keyword, mode: 'insensitive' } } }, + ], + } + : {}), + }, + select: { + id: true, + title: true, + subtitle: true, + organizationName: true, + teacher: { + select: { + name: true, + email: true, + primaryOrganizationName: true, + }, + }, + }, + orderBy: [{ title: 'asc' }], + take: 20, + }); + + const lmsUser = await this.prisma.user.findUnique({ + where: { email: authUser.email }, + select: { id: true }, + }); + const enrolledIds = lmsUser + ? new Set((await this.prisma.studentEnrollment.findMany({ + where: { + userId: lmsUser.id, + courseId: { in: courses.map((course) => course.id) }, + status: 'ACTIVE', + }, + select: { courseId: true }, + })).map((enrollment) => enrollment.courseId)) + : new Set(); return { - courses: guideCourse ? [this.serializeCourseListItem(guideCourse, true)] : [], + courses: courses.map((course) => ({ + ...course, + enrolled: enrolledIds.has(course.id), + })), }; } + async enrollCourse(accessToken: string | undefined, courseId: string) { + const { user: authUser } = await this.authService.me(accessToken); + if (!authUser?.email) throw new UnauthorizedException('Invalid user'); + + const normalizedCourseId = courseId.trim(); + if (!normalizedCourseId) throw new BadRequestException('courseId is required'); + + const course = await this.prisma.course.findFirst({ + where: { + id: normalizedCourseId, + isActive: true, + slug: { not: PUBLIC_GUIDE_COURSE_SLUG }, + }, + include: { + teacher: { + select: { + primaryOrganizationId: true, + }, + }, + }, + }); + if (!course) throw new NotFoundException('수강 신청할 강좌를 찾을 수 없습니다.'); + + const lmsUser = await this.prisma.user.upsert({ + where: { email: authUser.email }, + update: this.toUserSyncData(authUser), + create: { + email: authUser.email, + ...this.toUserSyncData(authUser), + }, + }); + + await this.prisma.studentEnrollment.upsert({ + where: { + userId_courseId: { + userId: lmsUser.id, + courseId: course.id, + }, + }, + update: { + organizationId: course.teacher.primaryOrganizationId, + organizationName: course.organizationName, + status: 'ACTIVE', + removedAt: null, + }, + create: { + userId: lmsUser.id, + courseId: course.id, + organizationId: course.teacher.primaryOrganizationId, + organizationName: course.organizationName, + status: 'ACTIVE', + }, + }); + + return { enrolled: true, courseId: course.id }; + } + async getActiveCurriculum(accessToken?: string, courseId?: string | null) { const authUser = accessToken ? await this.authService.me(accessToken).then((response) => response.user).catch(() => null) @@ -197,15 +307,6 @@ export class CurriculumService { customModulesByWeek.set(module.week, items); }); - const visualizationSelections = await this.prisma.courseWeekVisualizationPlatform.findMany({ - where: { - courseId: course.id, - enabled: true, - }, - include: { - platform: true, - }, - }); const mapKeysByWeek = new Map(); course.weekMaps.forEach((selection) => { const items = mapKeysByWeek.get(selection.week) ?? []; @@ -232,19 +333,6 @@ export class CurriculumService { }); visualizationProfilesByWeek.set(profile.week, items); }); - visualizationSelections.forEach((selection) => { - const items = visualizationProfilesByWeek.get(selection.week) ?? []; - if (items.some((profile) => profile.platformKey === selection.platformKey)) return; - items.push({ - id: selection.id, - week: selection.week, - platformKey: selection.platformKey, - profileType: selection.platform.type, - name: selection.platform.displayName, - config: selection.platform.defaultConfig, - }); - visualizationProfilesByWeek.set(selection.week, items); - }); const progress = course.studentProgress[0]; const enrollment = lmsUser ? await this.prisma.studentEnrollment.findFirst({ @@ -861,6 +949,19 @@ export class CurriculumService { }; } + private toUserSyncData(user: AuthResponseUser) { + return { + authUserId: user.id, + name: user.name, + authRole: user.role, + primaryOrganizationId: user.primaryAffiliation?.organizationId ?? null, + primaryOrganizationName: user.primaryAffiliation?.organizationName ?? null, + primaryOrganizationType: user.primaryAffiliation?.organizationType ?? null, + department: user.primaryAffiliation?.department ?? null, + position: user.primaryAffiliation?.position ?? null, + }; + } + private serializeLearningStats( progress: { completedWeeks: number; diff --git a/src/instructor/dto/update-instructor-curriculum.dto.ts b/src/instructor/dto/update-instructor-curriculum.dto.ts index a1cb018..6b21b73 100644 --- a/src/instructor/dto/update-instructor-curriculum.dto.ts +++ b/src/instructor/dto/update-instructor-curriculum.dto.ts @@ -106,11 +106,6 @@ export class UpdateInstructorCurriculumDto { }>; }>; - @IsOptional() - @IsArray() - @IsString({ each: true }) - visualizationPlatformKeys?: string[]; - @IsOptional() @IsArray() @IsString({ each: true }) diff --git a/src/instructor/instructor.service.ts b/src/instructor/instructor.service.ts index 0893fe2..7ec7a41 100644 --- a/src/instructor/instructor.service.ts +++ b/src/instructor/instructor.service.ts @@ -1406,26 +1406,7 @@ export class InstructorService { courseId: course.id, }, }); - const visualizationPlatforms = await this.prisma.visualizationPlatform.findMany({ - where: { enabled: true }, - orderBy: { sortOrder: 'asc' }, - }); const tagOptions = await this.findCurriculumTagOptions(); - const visualizationSelections = await this.prisma.courseWeekVisualizationPlatform.findMany({ - where: { - courseId: course.id, - enabled: true, - }, - include: { - platform: true, - }, - }); - const visualizationSelectionsByWeek = new Map(); - visualizationSelections.forEach((selection) => { - const items = visualizationSelectionsByWeek.get(selection.week) ?? []; - items.push(selection); - visualizationSelectionsByWeek.set(selection.week, items); - }); const courseMaps = await this.prisma.courseMap.findMany({ where: { courseId: course.id }, orderBy: { createdAt: 'asc' }, @@ -1505,13 +1486,6 @@ export class InstructorService { name: profile.name, config: profile.config, })), - visualizationPlatforms: (visualizationSelectionsByWeek.get(week.week) ?? []).map((selection) => ({ - key: selection.platform.key, - type: selection.platform.type, - displayName: selection.platform.displayName, - description: selection.platform.description, - config: selection.platform.defaultConfig, - })), mapKeys: mapKeysByWeek.get(week.week) ?? [], }; }); @@ -1546,13 +1520,6 @@ export class InstructorService { }, isTeamProject: override.isTeamProject ?? false, modules: [], - visualizationPlatforms: (visualizationSelectionsByWeek.get(override.week) ?? []).map((selection) => ({ - key: selection.platform.key, - type: selection.platform.type, - displayName: selection.platform.displayName, - description: selection.platform.description, - config: selection.platform.defaultConfig, - })), mapKeys: mapKeysByWeek.get(override.week) ?? [], }; }); @@ -1567,13 +1534,6 @@ export class InstructorService { }, weeks: [...baseWeeks, ...addedWeeks].sort((left, right) => left.week - right.week), tagOptions, - visualizationPlatforms: visualizationPlatforms.map((platform) => ({ - key: platform.key, - type: platform.type, - displayName: platform.displayName, - description: platform.description, - config: platform.defaultConfig, - })), mapOptions: [ ...NAV2_SYSTEM_MAPS, ...courseMaps.map((map) => ({ @@ -1710,15 +1670,6 @@ export class InstructorService { }); return items; }, []); - const selections = await this.prisma.courseWeekVisualizationPlatform.findMany({ - where: { - courseId: course.id, - week: input.activeWeek, - enabled: true, - }, - include: { platform: true }, - }); - const selectionsByPlatformKey = new Map(selections.map((selection) => [selection.platformKey, selection])); const profiles = await this.prisma.courseEnvironmentProfile.findMany({ where: { courseId: course.id, week: input.activeWeek }, }); @@ -1732,33 +1683,22 @@ export class InstructorService { && (profile.config as Prisma.JsonObject).target === 'remote-jetson') ); const wantsPlatform = logicalEnvironment === 'platform'; - const selection = isLogicalEnvironment - ? selections.find((candidate) => wantsPlatform === isPlatformProfile({ - profileType: candidate.platform.type, - config: candidate.platform.defaultConfig, - })) - : selectionsByPlatformKey.get(item.platformKey); const existingProfile = isLogicalEnvironment ? profiles.find((profile) => wantsPlatform === isPlatformProfile(profile)) : profilesByPlatformKey.get(item.platformKey); - if (!isLogicalEnvironment && !selection && !existingProfile) { + if (!isLogicalEnvironment && !existingProfile) { throw new BadRequestException('선택한 환경 프로필을 찾을 수 없습니다.'); } const resolvedPlatformKey = existingProfile?.platformKey - ?? selection?.platformKey ?? (wantsPlatform ? 'futuredrive-platform' : 'futuredrive-local-pc'); const profileType = existingProfile?.profileType - ?? selection?.platform.type ?? (wantsPlatform ? 'PLATFORM' : 'LOCAL'); const profileName = existingProfile?.name - ?? selection?.platform.displayName ?? (wantsPlatform ? '플랫폼' : '로컬'); const existingConfig = existingProfile?.config && typeof existingProfile.config === 'object' && !Array.isArray(existingProfile.config) ? existingProfile.config as Prisma.JsonObject : {}; - const defaultConfig = selection?.platform.defaultConfig && typeof selection.platform.defaultConfig === 'object' && !Array.isArray(selection.platform.defaultConfig) - ? selection.platform.defaultConfig as Prisma.JsonObject - : wantsPlatform ? { target: 'remote-jetson' } : { target: 'local' }; + const defaultConfig = wantsPlatform ? { target: 'remote-jetson' } : { target: 'local' }; const config = { ...defaultConfig, ...existingConfig, @@ -1855,49 +1795,6 @@ export class InstructorService { }); } - if (input.visualizationPlatformKeys !== undefined) { - const selectedKeys = Array.from(new Set(input.visualizationPlatformKeys.map((key) => key.trim().toUpperCase()).filter(Boolean))); - const validPlatforms = selectedKeys.length - ? await this.prisma.visualizationPlatform.findMany({ - where: { - key: { in: selectedKeys }, - enabled: true, - }, - select: { key: true }, - }) - : []; - const validKeySet = new Set(validPlatforms.map((platform) => platform.key)); - - await this.prisma.courseWeekVisualizationPlatform.deleteMany({ - where: { - courseId: course.id, - week: input.activeWeek, - platformKey: { notIn: selectedKeys }, - }, - }); - - await Promise.all(selectedKeys.filter((key) => validKeySet.has(key)).map((key) => ( - this.prisma.courseWeekVisualizationPlatform.upsert({ - where: { - courseId_week_platformKey: { - courseId: course.id, - week: input.activeWeek, - platformKey: key, - }, - }, - create: { - courseId: course.id, - week: input.activeWeek, - platformKey: key, - enabled: true, - }, - update: { - enabled: true, - }, - }) - ))); - } - if (input.mapKeys !== undefined) { const selectedKeys = Array.from(new Set(input.mapKeys.map((key) => key.trim()).filter(Boolean))); const customMaps = selectedKeys.length diff --git a/src/studio/studio.service.ts b/src/studio/studio.service.ts index 0b39d37..5d2e689 100644 --- a/src/studio/studio.service.ts +++ b/src/studio/studio.service.ts @@ -12,6 +12,8 @@ import { AuthService } from '../auth/auth.service'; import { PrismaService } from '../prisma/prisma.service'; import { R2PublicFileService } from '../storage/r2-public-file.service'; +const PUBLIC_GUIDE_COURSE_SLUG = 'autonomy-studio-lms-guide'; + @Injectable() export class StudioService { private readonly giteaBaseUrl: string; @@ -53,26 +55,7 @@ export class StudioService { const folderName = input.folderName?.trim(); if (!moduleName && !folderName) throw new BadRequestException('moduleName or folderName is required'); - const lmsUser = await this.prisma.user.findUnique({ - where: { email: authUser.email }, - }); - if (!lmsUser) throw new NotFoundException('LMS user not found'); - const courseId = input.courseId?.trim(); - let enrollmentOrganizationName: string | undefined; - if (courseId) { - const enrollment = await this.prisma.studentEnrollment.findFirst({ - where: { - userId: lmsUser.id, - courseId, - status: 'ACTIVE', - }, - select: { organizationName: true }, - }); - if (!enrollment) throw new NotFoundException('Student enrollment not found'); - enrollmentOrganizationName = enrollment.organizationName; - } - const courseWeek = await this.prisma.courseWeek.findFirst({ where: courseId ? { @@ -87,6 +70,11 @@ export class StudioService { isActive: true, }, }, + include: { + course: { + select: { slug: true }, + }, + }, }); if (!courseWeek) throw new NotFoundException('Course week not found'); @@ -108,6 +96,42 @@ export class StudioService { throw new BadRequestException('templateRepositoryUrl is required'); } + const isPublicGuide = courseWeek.course.slug === PUBLIC_GUIDE_COURSE_SLUG; + if (isPublicGuide) { + return { + id: `guide-${module.id}`, + owner: null, + repoName: null, + repositoryUrl: null, + repositoryUrlWithToken: null, + templateRepositoryUrl, + templateRepositoryUrlWithToken: this.withToken(templateRepositoryUrl), + sourceType: module.sourceType ?? 'GIT', + prebuiltArtifacts: module.prebuiltArtifacts ?? [], + workspaceScope: 'PERSONAL', + groupId: null, + }; + } + + const lmsUser = await this.prisma.user.findUnique({ + where: { email: authUser.email }, + }); + if (!lmsUser) throw new NotFoundException('LMS user not found'); + + let enrollmentOrganizationName: string | undefined; + if (courseId) { + const enrollment = await this.prisma.studentEnrollment.findFirst({ + where: { + userId: lmsUser.id, + courseId, + status: 'ACTIVE', + }, + select: { organizationName: true }, + }); + if (!enrollment) throw new NotFoundException('Student enrollment not found'); + enrollmentOrganizationName = enrollment.organizationName; + } + const organizationName = enrollmentOrganizationName ?? lmsUser.primaryOrganizationName ?? undefined; const teamProject = await this.isTeamProjectWeek(courseWeek.courseId, courseWeek.week, organizationName); const studentGroup = teamProject