feat: add course AI agent backend

This commit is contained in:
2026-10-14 14:25:42 +09:00
parent 5ccdc1e843
commit 56d7f2060c
20 changed files with 788 additions and 3 deletions
+61
View File
@@ -30,6 +30,8 @@ import { UpdateInstructorCurriculumDto } from './dto/update-instructor-curriculu
import { UpdateInstructorPlatformDto } from './dto/update-instructor-platform.dto';
import { CreateInstructorCourseMapDto } from './dto/create-instructor-course-map.dto';
import { NAV2_SYSTEM_MAPS } from '../curriculum/course-maps';
import { AiCredentialService } from '../ai/ai-credential.service';
import { UpdateCourseAiSettingDto } from '../ai/dto/update-course-ai-setting.dto';
const INSTRUCTOR_ROLES = new Set(['INSTRUCTOR', 'LMS_ADMIN']);
const DEFAULT_SUBMISSION_BOT_EMAIL = 'lms-bot@futuredrive.net';
@@ -95,6 +97,7 @@ export class InstructorService {
private readonly logger = new Logger(InstructorService.name);
constructor(
private readonly aiCredentialService: AiCredentialService,
private readonly authService: AuthService,
private readonly chatGateway: ChatGateway,
private readonly prisma: PrismaService,
@@ -103,6 +106,64 @@ export class InstructorService {
private readonly r2PublicFileService: R2PublicFileService,
) {}
async getCourseAiSetting(accessToken: string | undefined, courseId: string) {
const { authUser, instructor } = await this.getInstructor(accessToken);
const organizationName = authUser.primaryAffiliation?.organizationName?.trim();
if (!organizationName) throw new BadRequestException('Instructor organization is required');
const course = await this.findInstructorCourse(organizationName, instructor.id, courseId);
if (!course) throw new NotFoundException('Course not found');
const setting = await this.prisma.courseAiSetting.findUnique({ where: { courseId: course.id } });
return {
courseId: course.id,
enabled: setting?.enabled ?? false,
provider: setting?.provider ?? 'OPENAI',
model: setting?.model ?? 'gpt-4.1-mini',
hasApiKey: Boolean(setting?.encryptedApiKey),
updatedAt: setting?.updatedAt ?? null,
};
}
async updateCourseAiSetting(
accessToken: string | undefined,
courseId: string,
input: UpdateCourseAiSettingDto,
) {
const { authUser, instructor } = await this.getInstructor(accessToken);
const organizationName = authUser.primaryAffiliation?.organizationName?.trim();
if (!organizationName) throw new BadRequestException('Instructor organization is required');
const course = await this.findInstructorCourse(organizationName, instructor.id, courseId);
if (!course) throw new NotFoundException('Course not found');
const existing = await this.prisma.courseAiSetting.findUnique({ where: { courseId: course.id } });
const apiKey = input.apiKey?.trim();
if (input.enabled && !apiKey && (!existing?.encryptedApiKey || existing.provider !== input.provider)) {
throw new BadRequestException('AI 에이전트를 사용하거나 공급자를 변경하려면 해당 공급자의 API 키를 입력하세요.');
}
const setting = await this.prisma.courseAiSetting.upsert({
where: { courseId: course.id },
create: {
courseId: course.id,
enabled: input.enabled,
provider: input.provider,
model: input.model.trim(),
encryptedApiKey: apiKey ? this.aiCredentialService.encrypt(apiKey) : null,
},
update: {
enabled: input.enabled,
provider: input.provider,
model: input.model.trim(),
...(apiKey ? { encryptedApiKey: this.aiCredentialService.encrypt(apiKey) } : {}),
},
});
return {
courseId: course.id,
enabled: setting.enabled,
provider: setting.provider,
model: setting.model,
hasApiKey: Boolean(setting.encryptedApiKey),
updatedAt: setting.updatedAt,
};
}
async listCourses(accessToken: string | undefined) {
const { authUser, instructor } = await this.getInstructor(accessToken);
const organizationName = authUser.primaryAffiliation?.organizationName?.trim();