feat: add configurable VDO.Ninja overview and floating window focus

This commit is contained in:
2026-09-22 17:32:51 +09:00
parent d70f99e9c9
commit a5cd3528b7
19 changed files with 29510 additions and 3 deletions
+44 -2
View File
@@ -23,6 +23,7 @@ import { CreateInstructorPlatformDto } from './dto/create-instructor-platform.dt
import { CreateInstructorStudentDto } from './dto/create-instructor-student.dto';
import { SaveInstructorGroupDto } from './dto/save-instructor-group.dto';
import { groupCourseScope } from './group-course-scope';
import { normalizeCameraUrl, readPlatformCamera } from './platform-camera';
import { SaveSubmissionScoreDto } from './dto/save-submission-score.dto';
import { UpdateInstructorCurriculumDto } from './dto/update-instructor-curriculum.dto';
import { UpdateInstructorPlatformDto } from './dto/update-instructor-platform.dto';
@@ -1337,6 +1338,7 @@ export class InstructorService {
type: this.toPlatformType(platform.platform.displayName),
status: activeControl ? '활성' : '대기',
battery: platform.battery,
camera: readPlatformCamera(platform.studioSettingsOverride),
uptime: typeof agentConnection?.agentUptimeSeconds === 'number'
? this.formatUptimeSeconds(agentConnection.agentUptimeSeconds)
: this.formatUptime(platform.uptimeMinutes),
@@ -1462,6 +1464,7 @@ export class InstructorService {
return {
week: week.week,
title: override?.title ?? week.title,
canDelete: false,
tags: this.resolveWeekTags(this.resolveBaseWeekTags(week.tags, override?.tagLabel), progressState.state),
progress: progressState.progress,
state: progressState.state,
@@ -1516,6 +1519,7 @@ export class InstructorService {
return {
week: override.week,
title: override.title ?? `${override.week}주차 커리큘럼`,
canDelete: true,
tags: this.resolveWeekTags([override.tagLabel ?? '추가', '대기'], progressState.state),
progress: progressState.progress,
state: progressState.state,
@@ -1907,13 +1911,13 @@ export class InstructorService {
});
}
async deleteCurriculumWeek(accessToken: string | undefined, week: number) {
async deleteCurriculumWeek(accessToken: string | undefined, week: number, courseId?: string | null) {
const { authUser, instructor } = await this.getInstructor(accessToken);
const organizationId = authUser.primaryAffiliation?.organizationId;
const organizationName = authUser.primaryAffiliation?.organizationName?.trim();
if (!organizationName) throw new BadRequestException('Instructor organization is required');
const course = await this.findInstructorCourse(organizationName, instructor.id);
const course = await this.findInstructorCourse(organizationName, instructor.id, courseId);
if (!course) throw new NotFoundException('Course not found');
const baseWeek = course.weeks.find((courseWeek) => courseWeek.week === week);
@@ -2223,6 +2227,12 @@ export class InstructorService {
const maxSteeringAngleDegrees = this.toBoundedNumber(input.maxSteeringAngleDegrees, 30, 0, 90);
const sensors = this.normalizeNameDescriptionList(input.sensors, { withEnabled: true });
const algorithms = this.normalizeAlgorithms(input.algorithms);
let cameraViewUrl: string | undefined;
try {
cameraViewUrl = input.cameraViewUrl === undefined ? undefined : normalizeCameraUrl(input.cameraViewUrl);
} catch {
throw new BadRequestException('VDO.Ninja 시청 URL에 올바른 room과 view를 지정해주세요.');
}
const platform = await this.prisma.instructorPlatform.findFirst({
where: {
@@ -2235,6 +2245,7 @@ export class InstructorService {
select: {
id: true,
displayName: true,
studioSettingsOverride: true,
},
});
if (!platform) throw new NotFoundException('Platform not found');
@@ -2253,6 +2264,14 @@ export class InstructorService {
algorithms,
controlDurationMinutes,
controlWarningMinutes,
...((cameraViewUrl !== undefined || input.cameraPassword !== undefined) ? {
studioSettingsOverride: {
...(platform.studioSettingsOverride && typeof platform.studioSettingsOverride === 'object' && !Array.isArray(platform.studioSettingsOverride)
? platform.studioSettingsOverride : {}),
...(cameraViewUrl !== undefined ? { cameraViewUrl } : {}),
...(cameraViewUrl === '' ? { cameraPassword: '' } : input.cameraPassword !== undefined ? { cameraPassword: input.cameraPassword } : {}),
},
} : {}),
},
}),
this.prisma.studentGroup.updateMany({
@@ -3692,6 +3711,7 @@ export class InstructorService {
environmentProfiles: { orderBy: [{ week: 'asc' }, { platformKey: 'asc' }] },
maps: { orderBy: { key: 'asc' } },
weekMaps: { orderBy: [{ week: 'asc' }, { sortOrder: 'asc' }] },
instructorCurriculumWeekOverrides: true,
},
});
if (!template) {
@@ -3743,6 +3763,28 @@ export class InstructorService {
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 })) }),
]);
const learningOverride = template.instructorCurriculumWeekOverrides.find(
(override) => override.week === sourceWeek.week && override.organizationName === template.organizationName,
);
if (learningOverride?.learningSteps) {
const learningSteps = this.resolveLearningSteps(
learningOverride.learningSteps,
sourceWeek.topics.map((topic) => topic.title),
sourceWeek.topics.map((topic) => topic.detail),
);
await tx.instructorCurriculumWeekOverride.create({
data: {
courseId: course.id,
organizationName,
week: week.week,
learningSteps,
topics: learningSteps.map((step) => step.title),
topicDetails: learningSteps.map((step) => step.description),
topicVideoUrls: learningSteps.map((step) => step.videoUrl),
},
});
}
for (const sourceModule of sourceWeek.modules) {
const module = await tx.courseWeekModule.create({
data: {