296 lines
9.2 KiB
TypeScript
296 lines
9.2 KiB
TypeScript
export type InstructorPlatform = {
|
|
id: string;
|
|
masterPlatformId: string;
|
|
name: string;
|
|
type: string;
|
|
status: string;
|
|
battery: number;
|
|
uptime: string;
|
|
speed: string;
|
|
errors: number;
|
|
group: string | null;
|
|
user: string | null;
|
|
emergencyStop: boolean;
|
|
vehicleType: string;
|
|
wheelbaseMeters: number;
|
|
maxSpeedMetersPerSecond: number;
|
|
maxSteeringAngleDegrees: number;
|
|
sensors: PlatformSettingItem[];
|
|
drivers: PlatformSettingItem[];
|
|
algorithms: PlatformAlgorithms;
|
|
controlDurationMinutes: number;
|
|
controlWarningMinutes: number;
|
|
agent: InstructorPlatformAgentBinding | null;
|
|
};
|
|
|
|
export type InstructorPlatformAgentBinding = {
|
|
nodeId: string | null;
|
|
hostname: string | null;
|
|
ownerEmail: string | null;
|
|
ip: string | null;
|
|
connected: boolean;
|
|
lastSeenAt: string | null;
|
|
};
|
|
|
|
export type PlatformInUseSession = {
|
|
id: string;
|
|
holderUserId: string;
|
|
holderName: string | null;
|
|
holderEmail: string | null;
|
|
startedAt: string;
|
|
endsAt: string;
|
|
};
|
|
|
|
export class PlatformInUseError extends Error {
|
|
activeSession: PlatformInUseSession;
|
|
|
|
constructor(message: string, activeSession: PlatformInUseSession) {
|
|
super(message);
|
|
this.name = 'PlatformInUseError';
|
|
this.activeSession = activeSession;
|
|
}
|
|
}
|
|
|
|
export type TeachingAgent = {
|
|
id: string;
|
|
hostname: string;
|
|
name: string;
|
|
user: string;
|
|
tags: string[];
|
|
ip: string;
|
|
sshUser?: string | null;
|
|
workspaceRoot?: string | null;
|
|
connected: boolean;
|
|
expired: boolean;
|
|
lastSeenAt: string | null;
|
|
};
|
|
|
|
export type PlatformSettingItem = {
|
|
title: string;
|
|
description: string;
|
|
enabled?: boolean;
|
|
installed?: boolean;
|
|
};
|
|
|
|
export type PlatformAlgorithms = {
|
|
slam: string;
|
|
planning: string;
|
|
control: string;
|
|
};
|
|
|
|
export type MasterPlatform = {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
iconKey: string;
|
|
vehicleType: string;
|
|
};
|
|
|
|
export type InstructorPlatformsSummary = {
|
|
totalPlatforms: number;
|
|
activePlatforms: number;
|
|
standbyPlatforms: number;
|
|
errorPlatforms: number;
|
|
};
|
|
|
|
export type InstructorPlatformsResponse = {
|
|
summary: InstructorPlatformsSummary;
|
|
platforms: InstructorPlatform[];
|
|
masterPlatforms: MasterPlatform[];
|
|
agents: TeachingAgent[];
|
|
};
|
|
|
|
export type InstructorPlatformRegistrationResponse = {
|
|
code: string;
|
|
expiresAt: string | null;
|
|
};
|
|
|
|
const LMS_API_BASE_URL = (import.meta.env.VITE_API_BASE_URL ?? 'https://workspace.autonomystudio.net/lms-api/api').replace(/\/+$/, '');
|
|
|
|
export async function listInstructorPlatforms(accessToken: string): Promise<InstructorPlatformsResponse> {
|
|
const response = await fetch(`${LMS_API_BASE_URL}/instructor/platforms`, {
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`플랫폼 현황을 불러오지 못했습니다. (${response.status})`);
|
|
}
|
|
|
|
return response.json() as Promise<InstructorPlatformsResponse>;
|
|
}
|
|
|
|
export async function listInstructorPlatformAgents(accessToken: string): Promise<{ agents: TeachingAgent[] }> {
|
|
const response = await fetch(`${LMS_API_BASE_URL}/instructor/platforms/agents`, {
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`에이전트 목록을 불러오지 못했습니다. (${response.status})`);
|
|
}
|
|
|
|
return response.json() as Promise<{ agents: TeachingAgent[] }>;
|
|
}
|
|
|
|
export async function createInstructorPlatform(
|
|
accessToken: string,
|
|
platformId: string,
|
|
displayName?: string,
|
|
agent?: TeachingAgent | null,
|
|
ssh?: { user?: string; port?: string },
|
|
): Promise<InstructorPlatformsResponse> {
|
|
const response = await fetch(`${LMS_API_BASE_URL}/instructor/platforms`, {
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
'content-type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
platformId,
|
|
displayName,
|
|
agentNodeId: agent?.id,
|
|
agentHostname: agent?.hostname || agent?.name,
|
|
agentOwnerEmail: agent?.user,
|
|
sshUser: ssh?.user ?? agent?.sshUser ?? undefined,
|
|
sshPort: ssh?.port,
|
|
workspaceRoot: agent?.workspaceRoot ?? undefined,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) throw new Error(await platformErrorMessage(response, '플랫폼을 등록하지 못했습니다.'));
|
|
return response.json() as Promise<InstructorPlatformsResponse>;
|
|
}
|
|
|
|
export async function issueInstructorPlatformRegistration(
|
|
accessToken: string,
|
|
): Promise<InstructorPlatformRegistrationResponse> {
|
|
const response = await fetch(`${LMS_API_BASE_URL}/instructor/platform-registration-code`, {
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
'content-type': 'application/json',
|
|
},
|
|
});
|
|
|
|
if (!response.ok) throw new Error(await platformErrorMessage(response, '등록 키를 발급하지 못했습니다.'));
|
|
return response.json() as Promise<InstructorPlatformRegistrationResponse>;
|
|
}
|
|
|
|
export async function updateInstructorPlatform(
|
|
accessToken: string,
|
|
platformId: string,
|
|
displayName: string,
|
|
vehicleType: string,
|
|
wheelbaseMeters: number,
|
|
maxSpeedMetersPerSecond: number,
|
|
maxSteeringAngleDegrees: number,
|
|
sensors: PlatformSettingItem[],
|
|
algorithms: PlatformAlgorithms,
|
|
controlDurationMinutes: number,
|
|
controlWarningMinutes: number,
|
|
): Promise<InstructorPlatformsResponse> {
|
|
const response = await fetch(`${LMS_API_BASE_URL}/instructor/platforms/${platformId}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
'content-type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
displayName,
|
|
vehicleType,
|
|
wheelbaseMeters,
|
|
maxSpeedMetersPerSecond,
|
|
maxSteeringAngleDegrees,
|
|
sensors,
|
|
algorithms,
|
|
controlDurationMinutes,
|
|
controlWarningMinutes,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) throw new Error(await platformErrorMessage(response, '플랫폼 설정을 저장하지 못했습니다.'));
|
|
return response.json() as Promise<InstructorPlatformsResponse>;
|
|
}
|
|
|
|
export async function deleteInstructorPlatform(
|
|
accessToken: string,
|
|
platformId: string,
|
|
options?: { force?: boolean },
|
|
): Promise<InstructorPlatformsResponse> {
|
|
const query = options?.force ? '?force=true' : '';
|
|
const response = await fetch(`${LMS_API_BASE_URL}/instructor/platforms/${platformId}${query}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const payload = await response.json().catch((): null => null);
|
|
if (response.status === 409 && payload && typeof payload === 'object' && (payload as { code?: unknown }).code === 'PLATFORM_IN_USE') {
|
|
const activeSession = (payload as { activeSession?: PlatformInUseSession }).activeSession;
|
|
if (activeSession) {
|
|
throw new PlatformInUseError(String((payload as { message?: unknown }).message ?? '현재 학생이 플랫폼을 사용 중입니다.'), activeSession);
|
|
}
|
|
}
|
|
throw new Error(platformErrorMessageFromPayload(response, payload, '플랫폼을 삭제하지 못했습니다.'));
|
|
}
|
|
return response.json() as Promise<InstructorPlatformsResponse>;
|
|
}
|
|
|
|
export async function assignInstructorPlatform(
|
|
accessToken: string,
|
|
platformId: string,
|
|
groupId: string,
|
|
options?: { force?: boolean },
|
|
): Promise<InstructorPlatformsResponse> {
|
|
const query = options?.force ? '?force=true' : '';
|
|
const response = await fetch(`${LMS_API_BASE_URL}/instructor/platforms/${platformId}/assignment${query}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
'content-type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ groupId }),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const payload = await response.json().catch((): null => null);
|
|
if (response.status === 409 && payload && typeof payload === 'object' && (payload as { code?: unknown }).code === 'PLATFORM_IN_USE') {
|
|
const activeSession = (payload as { activeSession?: PlatformInUseSession }).activeSession;
|
|
if (activeSession) {
|
|
throw new PlatformInUseError(String((payload as { message?: unknown }).message ?? '현재 학생이 플랫폼을 사용 중입니다.'), activeSession);
|
|
}
|
|
}
|
|
throw new Error(platformErrorMessageFromPayload(response, payload, '플랫폼을 배정하지 못했습니다.'));
|
|
}
|
|
return response.json() as Promise<InstructorPlatformsResponse>;
|
|
}
|
|
|
|
export async function syncInstructorPlatformAccess(accessToken: string): Promise<{ ok: boolean; assignmentCount: number; syncedAt: string }> {
|
|
const response = await fetch(`${LMS_API_BASE_URL}/instructor/platforms/access-sync`, {
|
|
method: 'POST',
|
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(await platformErrorMessage(response, '플랫폼 접근 설정을 동기화하지 못했습니다.'));
|
|
}
|
|
return response.json() as Promise<{ ok: boolean; assignmentCount: number; syncedAt: string }>;
|
|
}
|
|
|
|
async function platformErrorMessage(response: Response, fallback: string) {
|
|
const payload = await response.json().catch((): null => null);
|
|
return platformErrorMessageFromPayload(response, payload, fallback);
|
|
}
|
|
|
|
function platformErrorMessageFromPayload(response: Response, payload: unknown, fallback: string) {
|
|
if (payload && typeof payload === 'object' && 'message' in payload) {
|
|
return String((payload as { message?: unknown }).message);
|
|
}
|
|
return `${fallback} (${response.status})`;
|
|
}
|