add commits..

This commit is contained in:
2026-08-12 14:46:57 +09:00
parent 324e644ef6
commit 38ab528d24
11 changed files with 450 additions and 31 deletions
+171
View File
@@ -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,