add commits..
This commit is contained in:
@@ -31,6 +31,31 @@ import { NAV2_SYSTEM_MAPS } from '../curriculum/course-maps';
|
||||
const INSTRUCTOR_ROLES = new Set(['INSTRUCTOR', 'LMS_ADMIN']);
|
||||
const DEFAULT_SUBMISSION_BOT_EMAIL = 'lms-bot@futuredrive.net';
|
||||
const COURSE_REFERENCE_UPLOAD_ROOT = join(process.cwd(), 'uploads', 'course-references');
|
||||
const BASE_COURSE_SLUGS = ['futuredrive-autonomy-basic', 'yeungnam-autonomy-basic'];
|
||||
|
||||
function copyJson(value: Prisma.JsonValue): Prisma.InputJsonValue | Prisma.JsonNullValueInput {
|
||||
return value === null ? Prisma.JsonNull : JSON.parse(JSON.stringify(value)) as Prisma.InputJsonValue;
|
||||
}
|
||||
|
||||
function remapProfileModuleIds(value: Prisma.JsonValue, moduleIds: Map<string, string>) {
|
||||
const config = copyJson(value);
|
||||
if (!config || typeof config !== 'object' || Array.isArray(config)) return config;
|
||||
|
||||
const moduleSet = (config as Record<string, unknown>).moduleSet;
|
||||
if (!moduleSet || typeof moduleSet !== 'object' || Array.isArray(moduleSet)) return config;
|
||||
|
||||
const modules = (moduleSet as Record<string, unknown>).modules;
|
||||
if (!Array.isArray(modules)) return config;
|
||||
|
||||
for (const module of modules) {
|
||||
if (!module || typeof module !== 'object' || Array.isArray(module)) continue;
|
||||
const id = (module as Record<string, unknown>).id;
|
||||
if (typeof id === 'string' && moduleIds.has(id)) {
|
||||
(module as Record<string, unknown>).id = moduleIds.get(id)!;
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
type SubmissionCommitFile = {
|
||||
path: string;
|
||||
@@ -3612,6 +3637,152 @@ export class InstructorService {
|
||||
return { authUser: user, instructor };
|
||||
}
|
||||
|
||||
async createInitialCourse(accessToken: string | undefined, title: string) {
|
||||
const { authUser, instructor } = await this.getInstructor(accessToken);
|
||||
const organizationName = authUser.primaryAffiliation?.organizationName?.trim();
|
||||
if (!organizationName) throw new BadRequestException('강사 소속 조직을 확인할 수 없습니다.');
|
||||
|
||||
const course = await this.createBaseCourseForInstructor(instructor.id, organizationName, title);
|
||||
return {
|
||||
course: {
|
||||
id: course.id,
|
||||
slug: course.slug,
|
||||
title: course.title,
|
||||
subtitle: course.subtitle,
|
||||
description: course.description,
|
||||
organizationName: course.organizationName,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async createBaseCourseForInstructor(instructorId: string, organizationName: string, title: string) {
|
||||
const courseTitle = title.trim();
|
||||
if (!courseTitle) throw new BadRequestException('강좌명을 입력해 주세요.');
|
||||
|
||||
const existingCourse = await this.prisma.course.findFirst({
|
||||
where: { teacherId: instructorId, isActive: true },
|
||||
select: { id: true },
|
||||
});
|
||||
if (existingCourse) throw new ConflictException('이미 생성된 강좌가 있습니다.');
|
||||
|
||||
const template = await this.prisma.course.findFirst({
|
||||
where: { slug: { in: BASE_COURSE_SLUGS }, isActive: true },
|
||||
include: {
|
||||
weeks: {
|
||||
orderBy: { week: 'asc' },
|
||||
include: {
|
||||
topics: { orderBy: { sortOrder: 'asc' } },
|
||||
practices: { orderBy: { sortOrder: 'asc' } },
|
||||
references: { orderBy: [{ sortOrder: 'asc' }, { createdAt: 'asc' }] },
|
||||
modules: { orderBy: { sortOrder: 'asc' } },
|
||||
},
|
||||
},
|
||||
environmentProfiles: { orderBy: [{ week: 'asc' }, { platformKey: 'asc' }] },
|
||||
maps: { orderBy: { key: 'asc' } },
|
||||
weekMaps: { orderBy: [{ week: 'asc' }, { sortOrder: 'asc' }] },
|
||||
},
|
||||
});
|
||||
if (!template) {
|
||||
this.logger.error('Base NAV2 course was not found; skipping instructor course initialization.');
|
||||
throw new NotFoundException('베이스 강좌를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
const courseSlug = `nav2-autonomy-${this.slug(instructorId).slice(0, 12)}`;
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const courseAlreadyCreated = await tx.course.findFirst({
|
||||
where: { teacherId: instructorId, isActive: true },
|
||||
select: { id: true },
|
||||
});
|
||||
if (courseAlreadyCreated) throw new ConflictException('이미 생성된 강좌가 있습니다.');
|
||||
|
||||
const course = await tx.course.create({
|
||||
data: {
|
||||
slug: courseSlug,
|
||||
title: courseTitle,
|
||||
subtitle: template.subtitle,
|
||||
description: template.description,
|
||||
organizationName,
|
||||
teacherId: instructorId,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
const moduleIds = new Map<string, string>();
|
||||
for (const sourceWeek of template.weeks) {
|
||||
const week = await tx.courseWeek.create({
|
||||
data: {
|
||||
courseId: course.id,
|
||||
week: sourceWeek.week,
|
||||
title: sourceWeek.title,
|
||||
tags: sourceWeek.tags,
|
||||
progress: sourceWeek.progress,
|
||||
state: sourceWeek.state,
|
||||
duration: sourceWeek.duration,
|
||||
github: sourceWeek.github,
|
||||
overview: sourceWeek.overview,
|
||||
deliverable: sourceWeek.deliverable,
|
||||
requiresSourceSubmission: sourceWeek.requiresSourceSubmission,
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
tx.courseWeekTopic.createMany({ data: sourceWeek.topics.map((topic) => ({ weekId: week.id, title: topic.title, detail: topic.detail, sortOrder: topic.sortOrder })) }),
|
||||
tx.courseWeekPractice.createMany({ data: sourceWeek.practices.map((practice) => ({ weekId: week.id, title: practice.title, description: practice.description, sortOrder: practice.sortOrder })) }),
|
||||
tx.courseWeekReference.createMany({ data: sourceWeek.references.map((reference) => ({ weekId: week.id, kind: reference.kind, title: reference.title, originalFileName: reference.originalFileName, storedFileName: reference.storedFileName, contentType: reference.contentType, fileSizeBytes: reference.fileSizeBytes, sortOrder: reference.sortOrder })) }),
|
||||
]);
|
||||
|
||||
for (const sourceModule of sourceWeek.modules) {
|
||||
const module = await tx.courseWeekModule.create({
|
||||
data: {
|
||||
weekId: week.id,
|
||||
sortOrder: sourceModule.sortOrder,
|
||||
category: sourceModule.category,
|
||||
name: sourceModule.name,
|
||||
folderName: sourceModule.folderName,
|
||||
templateRepositoryUrl: sourceModule.templateRepositoryUrl,
|
||||
sourceType: sourceModule.sourceType,
|
||||
prebuiltArtifacts: copyJson(sourceModule.prebuiltArtifacts),
|
||||
defaultParameters: copyJson(sourceModule.defaultParameters),
|
||||
dockerImage: sourceModule.dockerImage,
|
||||
},
|
||||
});
|
||||
moduleIds.set(sourceModule.id, module.id);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
...template.environmentProfiles.map((profile) => tx.courseEnvironmentProfile.create({
|
||||
data: {
|
||||
courseId: course.id,
|
||||
week: profile.week,
|
||||
platformKey: profile.platformKey,
|
||||
profileType: profile.profileType,
|
||||
name: profile.name,
|
||||
config: remapProfileModuleIds(profile.config, moduleIds),
|
||||
enabled: profile.enabled,
|
||||
},
|
||||
})),
|
||||
...template.maps.map((map) => tx.courseMap.create({
|
||||
data: {
|
||||
courseId: course.id,
|
||||
key: map.key,
|
||||
name: map.name,
|
||||
description: map.description,
|
||||
platformKey: map.platformKey,
|
||||
worldFile: map.worldFile,
|
||||
viewerModel: map.viewerModel,
|
||||
layout: copyJson(map.layout),
|
||||
},
|
||||
})),
|
||||
...template.weekMaps.map((weekMap) => tx.courseWeekMap.create({
|
||||
data: { courseId: course.id, week: weekMap.week, mapKey: weekMap.mapKey, sortOrder: weekMap.sortOrder },
|
||||
})),
|
||||
]);
|
||||
|
||||
return course;
|
||||
});
|
||||
}
|
||||
|
||||
private toUserSyncData(user: AuthResponseUser) {
|
||||
return {
|
||||
authUserId: user.id,
|
||||
|
||||
Reference in New Issue
Block a user