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[]
|
||||
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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string>();
|
||||
|
||||
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<number, string[]>();
|
||||
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;
|
||||
|
||||
@@ -106,11 +106,6 @@ export class UpdateInstructorCurriculumDto {
|
||||
}>;
|
||||
}>;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
visualizationPlatformKeys?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
|
||||
@@ -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<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({
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user