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; }; 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 { 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; } 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 { 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; } export async function issueInstructorPlatformRegistration( accessToken: string, ): Promise { 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; } 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 { 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; } export async function deleteInstructorPlatform( accessToken: string, platformId: string, options?: { force?: boolean }, ): Promise { 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; } export async function assignInstructorPlatform( accessToken: string, platformId: string, groupId: string, options?: { force?: boolean }, ): Promise { 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; } 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})`; }