patch curriculum..
This commit is contained in:
@@ -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;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
DROP TABLE IF EXISTS "course_week_visualization_platforms";
|
||||||
|
DROP TABLE IF EXISTS "visualization_platforms";
|
||||||
@@ -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;
|
||||||
@@ -162,7 +162,6 @@ model Course {
|
|||||||
instructorCurriculumSettings InstructorCurriculumSetting[]
|
instructorCurriculumSettings InstructorCurriculumSetting[]
|
||||||
instructorCurriculumWeekOverrides InstructorCurriculumWeekOverride[]
|
instructorCurriculumWeekOverrides InstructorCurriculumWeekOverride[]
|
||||||
environmentProfiles CourseEnvironmentProfile[]
|
environmentProfiles CourseEnvironmentProfile[]
|
||||||
visualizationSelections CourseWeekVisualizationPlatform[]
|
|
||||||
maps CourseMap[]
|
maps CourseMap[]
|
||||||
weekMaps CourseWeekMap[]
|
weekMaps CourseWeekMap[]
|
||||||
savedMaps StudentSavedMap[]
|
savedMaps StudentSavedMap[]
|
||||||
@@ -195,39 +194,6 @@ model CourseEnvironmentProfile {
|
|||||||
@@map("course_environment_profiles")
|
@@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 {
|
model CourseMap {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
courseId String @map("course_id") @db.Uuid
|
courseId String @map("course_id") @db.Uuid
|
||||||
|
|||||||
@@ -46,6 +46,16 @@ export class CurriculumController {
|
|||||||
return this.curriculumService.listCourses(this.getBearerToken(request));
|
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')
|
@Get('references/:referenceId/download')
|
||||||
async downloadReference(
|
async downloadReference(
|
||||||
@Req() request: Request,
|
@Req() request: Request,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { BadRequestException, ConflictException, Injectable, NotFoundException,
|
|||||||
import { Prisma } from '@prisma/client';
|
import { Prisma } from '@prisma/client';
|
||||||
import { join } from 'node:path';
|
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 { PlatformControlService } from '../platform-control/platform-control.service';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { R2PublicFileService } from '../storage/r2-public-file.service';
|
import { R2PublicFileService } from '../storage/r2-public-file.service';
|
||||||
@@ -30,20 +30,21 @@ export class CurriculumService {
|
|||||||
const lmsUser = authUser?.email
|
const lmsUser = authUser?.email
|
||||||
? await this.prisma.user.findUnique({ where: { email: authUser.email } })
|
? await this.prisma.user.findUnique({ where: { email: authUser.email } })
|
||||||
: null;
|
: null;
|
||||||
|
const guideCourse = await this.findPublicGuideCourse(lmsUser?.id);
|
||||||
|
const guideCourseItem = guideCourse ? this.serializeCourseListItem(guideCourse, true) : null;
|
||||||
|
|
||||||
if (!lmsUser) {
|
if (!lmsUser) {
|
||||||
const guideCourse = await this.findPublicGuideCourse();
|
return { courses: [], guideCourse: guideCourseItem };
|
||||||
|
|
||||||
return {
|
|
||||||
courses: guideCourse ? [this.serializeCourseListItem(guideCourse, true)] : [],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const enrollments = await this.prisma.studentEnrollment.findMany({
|
const enrollments = await this.prisma.studentEnrollment.findMany({
|
||||||
where: {
|
where: {
|
||||||
userId: lmsUser.id,
|
userId: lmsUser.id,
|
||||||
status: 'ACTIVE',
|
status: 'ACTIVE',
|
||||||
course: { isActive: true },
|
course: {
|
||||||
|
isActive: true,
|
||||||
|
slug: { not: PUBLIC_GUIDE_COURSE_SLUG },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
course: {
|
course: {
|
||||||
@@ -66,19 +67,128 @@ export class CurriculumService {
|
|||||||
orderBy: { updatedAt: 'desc' },
|
orderBy: { updatedAt: 'desc' },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (enrollments.length) {
|
|
||||||
return {
|
return {
|
||||||
courses: enrollments.map((enrollment) => this.serializeCourseListItem(enrollment.course)),
|
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<string>();
|
||||||
|
|
||||||
return {
|
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) {
|
async getActiveCurriculum(accessToken?: string, courseId?: string | null) {
|
||||||
const authUser = accessToken
|
const authUser = accessToken
|
||||||
? await this.authService.me(accessToken).then((response) => response.user).catch(() => null)
|
? await this.authService.me(accessToken).then((response) => response.user).catch(() => null)
|
||||||
@@ -197,15 +307,6 @@ export class CurriculumService {
|
|||||||
customModulesByWeek.set(module.week, items);
|
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<number, string[]>();
|
const mapKeysByWeek = new Map<number, string[]>();
|
||||||
course.weekMaps.forEach((selection) => {
|
course.weekMaps.forEach((selection) => {
|
||||||
const items = mapKeysByWeek.get(selection.week) ?? [];
|
const items = mapKeysByWeek.get(selection.week) ?? [];
|
||||||
@@ -232,19 +333,6 @@ export class CurriculumService {
|
|||||||
});
|
});
|
||||||
visualizationProfilesByWeek.set(profile.week, items);
|
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 progress = course.studentProgress[0];
|
||||||
const enrollment = lmsUser
|
const enrollment = lmsUser
|
||||||
? await this.prisma.studentEnrollment.findFirst({
|
? 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(
|
private serializeLearningStats(
|
||||||
progress: {
|
progress: {
|
||||||
completedWeeks: number;
|
completedWeeks: number;
|
||||||
|
|||||||
@@ -106,11 +106,6 @@ export class UpdateInstructorCurriculumDto {
|
|||||||
}>;
|
}>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsArray()
|
|
||||||
@IsString({ each: true })
|
|
||||||
visualizationPlatformKeys?: string[];
|
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@IsString({ each: true })
|
@IsString({ each: true })
|
||||||
|
|||||||
@@ -1406,26 +1406,7 @@ export class InstructorService {
|
|||||||
courseId: course.id,
|
courseId: course.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const visualizationPlatforms = await this.prisma.visualizationPlatform.findMany({
|
|
||||||
where: { enabled: true },
|
|
||||||
orderBy: { sortOrder: 'asc' },
|
|
||||||
});
|
|
||||||
const tagOptions = await this.findCurriculumTagOptions();
|
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<number, typeof visualizationSelections>();
|
|
||||||
visualizationSelections.forEach((selection) => {
|
|
||||||
const items = visualizationSelectionsByWeek.get(selection.week) ?? [];
|
|
||||||
items.push(selection);
|
|
||||||
visualizationSelectionsByWeek.set(selection.week, items);
|
|
||||||
});
|
|
||||||
const courseMaps = await this.prisma.courseMap.findMany({
|
const courseMaps = await this.prisma.courseMap.findMany({
|
||||||
where: { courseId: course.id },
|
where: { courseId: course.id },
|
||||||
orderBy: { createdAt: 'asc' },
|
orderBy: { createdAt: 'asc' },
|
||||||
@@ -1505,13 +1486,6 @@ export class InstructorService {
|
|||||||
name: profile.name,
|
name: profile.name,
|
||||||
config: profile.config,
|
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) ?? [],
|
mapKeys: mapKeysByWeek.get(week.week) ?? [],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -1546,13 +1520,6 @@ export class InstructorService {
|
|||||||
},
|
},
|
||||||
isTeamProject: override.isTeamProject ?? false,
|
isTeamProject: override.isTeamProject ?? false,
|
||||||
modules: [],
|
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) ?? [],
|
mapKeys: mapKeysByWeek.get(override.week) ?? [],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -1567,13 +1534,6 @@ export class InstructorService {
|
|||||||
},
|
},
|
||||||
weeks: [...baseWeeks, ...addedWeeks].sort((left, right) => left.week - right.week),
|
weeks: [...baseWeeks, ...addedWeeks].sort((left, right) => left.week - right.week),
|
||||||
tagOptions,
|
tagOptions,
|
||||||
visualizationPlatforms: visualizationPlatforms.map((platform) => ({
|
|
||||||
key: platform.key,
|
|
||||||
type: platform.type,
|
|
||||||
displayName: platform.displayName,
|
|
||||||
description: platform.description,
|
|
||||||
config: platform.defaultConfig,
|
|
||||||
})),
|
|
||||||
mapOptions: [
|
mapOptions: [
|
||||||
...NAV2_SYSTEM_MAPS,
|
...NAV2_SYSTEM_MAPS,
|
||||||
...courseMaps.map((map) => ({
|
...courseMaps.map((map) => ({
|
||||||
@@ -1710,15 +1670,6 @@ export class InstructorService {
|
|||||||
});
|
});
|
||||||
return items;
|
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({
|
const profiles = await this.prisma.courseEnvironmentProfile.findMany({
|
||||||
where: { courseId: course.id, week: input.activeWeek },
|
where: { courseId: course.id, week: input.activeWeek },
|
||||||
});
|
});
|
||||||
@@ -1732,33 +1683,22 @@ export class InstructorService {
|
|||||||
&& (profile.config as Prisma.JsonObject).target === 'remote-jetson')
|
&& (profile.config as Prisma.JsonObject).target === 'remote-jetson')
|
||||||
);
|
);
|
||||||
const wantsPlatform = logicalEnvironment === 'platform';
|
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
|
const existingProfile = isLogicalEnvironment
|
||||||
? profiles.find((profile) => wantsPlatform === isPlatformProfile(profile))
|
? profiles.find((profile) => wantsPlatform === isPlatformProfile(profile))
|
||||||
: profilesByPlatformKey.get(item.platformKey);
|
: profilesByPlatformKey.get(item.platformKey);
|
||||||
if (!isLogicalEnvironment && !selection && !existingProfile) {
|
if (!isLogicalEnvironment && !existingProfile) {
|
||||||
throw new BadRequestException('선택한 환경 프로필을 찾을 수 없습니다.');
|
throw new BadRequestException('선택한 환경 프로필을 찾을 수 없습니다.');
|
||||||
}
|
}
|
||||||
const resolvedPlatformKey = existingProfile?.platformKey
|
const resolvedPlatformKey = existingProfile?.platformKey
|
||||||
?? selection?.platformKey
|
|
||||||
?? (wantsPlatform ? 'futuredrive-platform' : 'futuredrive-local-pc');
|
?? (wantsPlatform ? 'futuredrive-platform' : 'futuredrive-local-pc');
|
||||||
const profileType = existingProfile?.profileType
|
const profileType = existingProfile?.profileType
|
||||||
?? selection?.platform.type
|
|
||||||
?? (wantsPlatform ? 'PLATFORM' : 'LOCAL');
|
?? (wantsPlatform ? 'PLATFORM' : 'LOCAL');
|
||||||
const profileName = existingProfile?.name
|
const profileName = existingProfile?.name
|
||||||
?? selection?.platform.displayName
|
|
||||||
?? (wantsPlatform ? '플랫폼' : '로컬');
|
?? (wantsPlatform ? '플랫폼' : '로컬');
|
||||||
const existingConfig = existingProfile?.config && typeof existingProfile.config === 'object' && !Array.isArray(existingProfile.config)
|
const existingConfig = existingProfile?.config && typeof existingProfile.config === 'object' && !Array.isArray(existingProfile.config)
|
||||||
? existingProfile.config as Prisma.JsonObject
|
? existingProfile.config as Prisma.JsonObject
|
||||||
: {};
|
: {};
|
||||||
const defaultConfig = selection?.platform.defaultConfig && typeof selection.platform.defaultConfig === 'object' && !Array.isArray(selection.platform.defaultConfig)
|
const defaultConfig = wantsPlatform ? { target: 'remote-jetson' } : { target: 'local' };
|
||||||
? selection.platform.defaultConfig as Prisma.JsonObject
|
|
||||||
: wantsPlatform ? { target: 'remote-jetson' } : { target: 'local' };
|
|
||||||
const config = {
|
const config = {
|
||||||
...defaultConfig,
|
...defaultConfig,
|
||||||
...existingConfig,
|
...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) {
|
if (input.mapKeys !== undefined) {
|
||||||
const selectedKeys = Array.from(new Set(input.mapKeys.map((key) => key.trim()).filter(Boolean)));
|
const selectedKeys = Array.from(new Set(input.mapKeys.map((key) => key.trim()).filter(Boolean)));
|
||||||
const customMaps = selectedKeys.length
|
const customMaps = selectedKeys.length
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import { AuthService } from '../auth/auth.service';
|
|||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { R2PublicFileService } from '../storage/r2-public-file.service';
|
import { R2PublicFileService } from '../storage/r2-public-file.service';
|
||||||
|
|
||||||
|
const PUBLIC_GUIDE_COURSE_SLUG = 'autonomy-studio-lms-guide';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class StudioService {
|
export class StudioService {
|
||||||
private readonly giteaBaseUrl: string;
|
private readonly giteaBaseUrl: string;
|
||||||
@@ -53,26 +55,7 @@ export class StudioService {
|
|||||||
const folderName = input.folderName?.trim();
|
const folderName = input.folderName?.trim();
|
||||||
if (!moduleName && !folderName) throw new BadRequestException('moduleName or folderName is required');
|
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();
|
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({
|
const courseWeek = await this.prisma.courseWeek.findFirst({
|
||||||
where: courseId
|
where: courseId
|
||||||
? {
|
? {
|
||||||
@@ -87,6 +70,11 @@ export class StudioService {
|
|||||||
isActive: true,
|
isActive: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
include: {
|
||||||
|
course: {
|
||||||
|
select: { slug: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
if (!courseWeek) throw new NotFoundException('Course week not found');
|
if (!courseWeek) throw new NotFoundException('Course week not found');
|
||||||
|
|
||||||
@@ -108,6 +96,42 @@ export class StudioService {
|
|||||||
throw new BadRequestException('templateRepositoryUrl is required');
|
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 organizationName = enrollmentOrganizationName ?? lmsUser.primaryOrganizationName ?? undefined;
|
||||||
const teamProject = await this.isTeamProjectWeek(courseWeek.courseId, courseWeek.week, organizationName);
|
const teamProject = await this.isTeamProjectWeek(courseWeek.courseId, courseWeek.week, organizationName);
|
||||||
const studentGroup = teamProject
|
const studentGroup = teamProject
|
||||||
|
|||||||
Reference in New Issue
Block a user