From def692a28b1d6fcdd95391a24e675553113af932 Mon Sep 17 00:00:00 2001 From: chyoo Date: Mon, 3 Aug 2026 13:58:20 +0900 Subject: [PATCH] patch.. --- index.html | 1 + public/favicon.ico | Bin 0 -> 47107 bytes src/lib/curriculum.ts | 57 +++ src/lib/platforms.ts | 20 ++ src/pages/curriculum/CurriculumPage.tsx | 442 ++++++++++++++++++++++-- src/pages/platforms/PlatformsPage.tsx | 148 ++++++-- temp.png | Bin 46047 -> 0 bytes temp2.png | Bin 46814 -> 0 bytes 8 files changed, 609 insertions(+), 59 deletions(-) create mode 100644 public/favicon.ico delete mode 100644 temp.png delete mode 100644 temp2.png diff --git a/index.html b/index.html index 2c77dfe..e1e63fb 100644 --- a/index.html +++ b/index.html @@ -2,6 +2,7 @@ + 오토노미스튜디오 LMS | 강사로그인 diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..ba648d51da8c60166ecd3efa5c9d4e317f856187 GIT binary patch literal 47107 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 Kg<5&5u>S)MAei+4 literal 0 HcmV?d00001 diff --git a/src/lib/curriculum.ts b/src/lib/curriculum.ts index f87e817..c1b9dda 100644 --- a/src/lib/curriculum.ts +++ b/src/lib/curriculum.ts @@ -28,7 +28,19 @@ export type CurriculumWeek = { requiresSourceSubmission: boolean; }; modules?: CurriculumModule[]; + environmentProfiles?: CurriculumEnvironmentProfile[]; visualizationPlatforms?: CurriculumVisualizationPlatform[]; + mapKeys?: string[]; +}; + +export type CurriculumMap = { + key: string; + platformKey: 'NAV2'; + name: string; + description: string; + worldFile: string; + viewerModel: 'turtlebot3_world' | 'turtlebot3_house'; + custom?: boolean; }; export type CurriculumModule = { @@ -39,9 +51,18 @@ export type CurriculumModule = { folderName: string; templateRepositoryUrl?: string | null; dockerImage?: string | null; + defaultParameters?: string[]; enabled?: boolean; }; +export type CurriculumEnvironmentProfile = { + id: string; + platformKey: string; + profileType: string; + name: string; + config?: Record; +}; + export type CurriculumReference = { id: string; kind?: string; @@ -85,6 +106,7 @@ export type InstructorCurriculumResponse = { } | null; tagOptions?: CurriculumTagOption[]; visualizationPlatforms?: CurriculumVisualizationPlatform[]; + mapOptions?: CurriculumMap[]; }; const LMS_API_BASE_URL = (import.meta.env.VITE_API_BASE_URL ?? 'https://workspace.autonomystudio.net/lms-api/api').replace(/\/+$/, ''); @@ -139,7 +161,21 @@ export async function updateInstructorCurriculum( description?: string; }>; disabledModuleIds?: string[]; + moduleDefaultParameters?: Array<{ + moduleId: string; + parameters: string[]; + }>; + environmentModuleSets?: Array<{ + platformKey: string; + moduleIds: string[]; + modules?: Array<{ + id: string; + defaultParameters: string[]; + dockerImage?: string | null; + }>; + }>; visualizationPlatformKeys?: string[]; + mapKeys?: string[]; }, ): Promise { const response = await fetch(`${LMS_API_BASE_URL}/instructor/curriculum`, { @@ -163,6 +199,27 @@ export async function updateInstructorCurriculum( return response.json() as Promise; } +export async function createInstructorCurriculumMap( + accessToken: string, + input: { courseId?: string | null; name: string; description?: string }, +): Promise { + const response = await fetch(`${LMS_API_BASE_URL}/instructor/curriculum/maps`, { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + 'content-type': 'application/json', + }, + body: JSON.stringify(input), + }); + if (!response.ok) { + const payload = await response.json().catch((): null => null); + throw new Error(payload && typeof payload === 'object' && 'message' in payload + ? String(payload.message) + : `학습 맵을 등록하지 못했습니다. (${response.status})`); + } + return response.json() as Promise; +} + export async function deleteInstructorCurriculumWeek( accessToken: string, week: number, diff --git a/src/lib/platforms.ts b/src/lib/platforms.ts index 88762e6..b045ede 100644 --- a/src/lib/platforms.ts +++ b/src/lib/platforms.ts @@ -99,6 +99,11 @@ export type InstructorPlatformsResponse = { 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 { @@ -158,6 +163,21 @@ export async function createInstructorPlatform( 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, diff --git a/src/pages/curriculum/CurriculumPage.tsx b/src/pages/curriculum/CurriculumPage.tsx index 9082e28..a8644b9 100644 --- a/src/pages/curriculum/CurriculumPage.tsx +++ b/src/pages/curriculum/CurriculumPage.tsx @@ -1,14 +1,77 @@ import { useEffect, useMemo, useRef, useState } from 'react'; -import { CheckCircle2, CirclePlay, Clock3, FileCode2, FileText, Loader2, Plus, RadioTower, Save, Target, Trash2, Upload, type LucideIcon } from 'lucide-react'; +import { CheckCircle2, CirclePlay, Clock3, FileCode2, FileText, Loader2, Map, Plus, RadioTower, Save, Target, Trash2, Upload, type LucideIcon } from 'lucide-react'; import { ConfirmDialog } from '../../components/ConfirmDialog'; import { PageHeader } from '../../components/PageHeader'; import { useToast } from '../../components/ToastProvider'; -import { deleteCurriculumReference, deleteInstructorCurriculumWeek, getInstructorCurriculum, updateInstructorCurriculum, uploadCurriculumExampleCode, uploadCurriculumReference, type CurriculumWeek, type InstructorCurriculumResponse } from '../../lib/curriculum'; +import { createInstructorCurriculumMap, deleteCurriculumReference, deleteInstructorCurriculumWeek, getInstructorCurriculum, updateInstructorCurriculum, uploadCurriculumExampleCode, uploadCurriculumReference, type CurriculumEnvironmentProfile, type CurriculumWeek, type InstructorCurriculumResponse } from '../../lib/curriculum'; import { listInstructorGroups, type InstructorGroup } from '../../lib/groups'; import type { AuthSession } from '../../types/auth'; const CURRICULUM_SELECTED_GROUP_STORAGE_PREFIX = 'lms-admin-curriculum-selected-group-id'; +type ModuleParameterDraft = { + key: string; + value: string; +}; + +function parseModuleParameter(parameter: string): ModuleParameterDraft { + const separatorIndex = parameter.indexOf(':='); + if (separatorIndex < 0) return { key: parameter.trim(), value: '' }; + return { + key: parameter.slice(0, separatorIndex).trim(), + value: parameter.slice(separatorIndex + 2).trim(), + }; +} + +function readEnvironmentModuleIds(profile: CurriculumEnvironmentProfile, modules: NonNullable): string[] { + const config = profile.config; + const moduleSet = config && typeof config.moduleSet === 'object' && config.moduleSet !== null && !Array.isArray(config.moduleSet) + ? config.moduleSet as Record + : null; + if (!moduleSet || moduleSet.includeUnlisted !== false || !Array.isArray(moduleSet.modules)) { + return modules.map((module) => module.id); + } + return moduleSet.modules.flatMap((item) => ( + item && typeof item === 'object' && typeof (item as Record).id === 'string' + ? [(item as Record).id as string] + : [] + )); +} + +function readEnvironmentModuleParameters(profile: CurriculumEnvironmentProfile): Record { + const config = profile.config; + const moduleSet = config && typeof config.moduleSet === 'object' && config.moduleSet !== null && !Array.isArray(config.moduleSet) + ? config.moduleSet as Record + : null; + if (!moduleSet || !Array.isArray(moduleSet.modules)) return {}; + + return Object.fromEntries(moduleSet.modules.flatMap((item) => { + if (!item || typeof item !== 'object') return []; + const module = item as Record; + if (typeof module.id !== 'string') return []; + const parameters = Array.isArray(module.defaultParameters) + ? module.defaultParameters.filter((value): value is string => typeof value === 'string').map(parseModuleParameter) + : []; + return [[module.id, parameters]]; + })); +} + +function readEnvironmentModuleDockerImages(profile: CurriculumEnvironmentProfile): Record { + const config = profile.config; + const moduleSet = config && typeof config.moduleSet === 'object' && config.moduleSet !== null && !Array.isArray(config.moduleSet) + ? config.moduleSet as Record + : null; + if (!moduleSet || !Array.isArray(moduleSet.modules)) return {}; + + return Object.fromEntries(moduleSet.modules.flatMap((item) => { + if (!item || typeof item !== 'object') return []; + const module = item as Record; + return typeof module.id === 'string' && typeof module.dockerImage === 'string' + ? [[module.id, module.dockerImage]] + : []; + })); +} + export function CurriculumPage({ session, selectedCourseId }: { session: AuthSession; selectedCourseId: string }) { const { showToast } = useToast(); const [weeks, setWeeks] = useState([]); @@ -31,9 +94,19 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes const [draftTopicConcepts, setDraftTopicConcepts] = useState([]); const [draftPracticeContents, setDraftPracticeContents] = useState>([]); const [moduleEnabledById, setModuleEnabledById] = useState>({}); + const [moduleDefaultParametersById, setModuleDefaultParametersById] = useState>({}); + const [environmentModuleIdsByPlatformKey, setEnvironmentModuleIdsByPlatformKey] = useState>({}); + const [environmentModuleParametersByPlatformKey, setEnvironmentModuleParametersByPlatformKey] = useState>>({}); + const [environmentModuleDockerImagesByPlatformKey, setEnvironmentModuleDockerImagesByPlatformKey] = useState>>({}); const [tagOptions, setTagOptions] = useState>([]); const [visualizationPlatforms, setVisualizationPlatforms] = useState>([]); const [visualizationEnabledByKey, setVisualizationEnabledByKey] = useState>({}); + const [mapOptions, setMapOptions] = useState>([]); + const [mapEnabledByKey, setMapEnabledByKey] = useState>({}); + const [mapCreateOpen, setMapCreateOpen] = useState(false); + const [mapName, setMapName] = useState(''); + const [mapDescription, setMapDescription] = useState(''); + const [creatingMap, setCreatingMap] = useState(false); const [referenceTitle, setReferenceTitle] = useState(''); const [referenceFile, setReferenceFile] = useState(null); const [exampleCodeTitle, setExampleCodeTitle] = useState(''); @@ -90,6 +163,7 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes setWeeks(payload.weeks); setTagOptions(payload.tagOptions ?? []); setVisualizationPlatforms(payload.visualizationPlatforms ?? []); + setMapOptions(payload.mapOptions ?? []); const progress = resolveProgressSelection(payload.weeks, payload.setting); setCurrentProgressWeek(progress.week); setCurrentProgressLearningStep(progress.learningStep); @@ -164,7 +238,21 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes setDraftTopicConcepts(selectedWeek.topics.map((_, index) => selectedWeek.detail.learningSteps?.[index]?.concepts?.join('\n') ?? '')); setDraftPracticeContents(selectedWeek.detail.practiceContents?.length ? selectedWeek.detail.practiceContents : []); setModuleEnabledById(Object.fromEntries((selectedWeek.modules ?? []).map((module) => [module.id, module.enabled !== false]))); + setModuleDefaultParametersById(Object.fromEntries((selectedWeek.modules ?? []).map((module) => [module.id, (module.defaultParameters ?? []).map(parseModuleParameter)]))); + setEnvironmentModuleIdsByPlatformKey(Object.fromEntries((selectedWeek.environmentProfiles ?? []).map((profile) => [ + profile.platformKey, + readEnvironmentModuleIds(profile, selectedWeek.modules ?? []), + ]))); + setEnvironmentModuleParametersByPlatformKey(Object.fromEntries((selectedWeek.environmentProfiles ?? []).map((profile) => [ + profile.platformKey, + readEnvironmentModuleParameters(profile), + ]))); + setEnvironmentModuleDockerImagesByPlatformKey(Object.fromEntries((selectedWeek.environmentProfiles ?? []).map((profile) => [ + profile.platformKey, + readEnvironmentModuleDockerImages(profile), + ]))); setVisualizationEnabledByKey(Object.fromEntries((selectedWeek.visualizationPlatforms ?? []).map((platform) => [platform.key, true]))); + setMapEnabledByKey(Object.fromEntries((selectedWeek.mapKeys ?? []).map((key) => [key, true]))); setReferenceTitle(''); setReferenceFile(null); setExampleCodeTitle(''); @@ -330,6 +418,23 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes const disabledModuleIds = (selectedWeek.modules ?? []) .filter((module) => moduleEnabledById[module.id] === false) .map((module) => module.id); + const moduleDefaultParameters = (selectedWeek.modules ?? []).map((module) => ({ + moduleId: module.id, + parameters: (moduleDefaultParametersById[module.id] ?? []) + .filter((parameter) => parameter.key.trim()) + .map((parameter) => `${parameter.key.trim()}:=${parameter.value.trim()}`), + })); + const environmentModuleSets = (selectedWeek.environmentProfiles ?? []).map((profile) => ({ + platformKey: profile.platformKey, + moduleIds: environmentModuleIdsByPlatformKey[profile.platformKey] ?? [], + modules: (environmentModuleIdsByPlatformKey[profile.platformKey] ?? []).map((id) => ({ + id, + defaultParameters: (environmentModuleParametersByPlatformKey[profile.platformKey]?.[id] ?? []) + .filter((parameter) => parameter.key.trim()) + .map((parameter) => `${parameter.key.trim()}:=${parameter.value.trim()}`), + dockerImage: environmentModuleDockerImagesByPlatformKey[profile.platformKey]?.[id]?.trim() || undefined, + })), + })); const payload = await updateInstructorCurriculum(session.accessToken, { courseId: selectedCourseId || null, groupId: selectedGroupId || null, @@ -337,9 +442,12 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes activeLearningStep, applyToStudents: false, disabledModuleIds, + moduleDefaultParameters, + environmentModuleSets, visualizationPlatformKeys: visualizationPlatforms .filter((platform) => visualizationEnabledByKey[platform.key]) .map((platform) => platform.key), + mapKeys: mapOptions.filter((map) => mapEnabledByKey[map.key]).map((map) => map.key), }); applyPayload(payload); setActiveWeek(selectedWeek.week); @@ -354,6 +462,29 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes } }; + const handleCreateMap = async () => { + const name = mapName.trim(); + if (!name) return; + setCreatingMap(true); + try { + const map = await createInstructorCurriculumMap(session.accessToken, { + courseId: selectedCourseId || null, + name, + description: mapDescription.trim(), + }); + setMapOptions((current) => [...current, map]); + setMapEnabledByKey((current) => ({ ...current, [map.key]: true })); + setMapCreateOpen(false); + setMapName(''); + setMapDescription(''); + showToast({ title: '학습 맵이 추가되었습니다.', message: '현재 주차에 선택된 상태입니다.' }); + } catch (reason) { + setError(reason instanceof Error ? reason.message : '학습 맵을 등록하지 못했습니다.'); + } finally { + setCreatingMap(false); + } + }; + const handleAddWeek = async () => { const nextWeek = Math.max(0, ...weeks.map((week) => week.week)) + 1; setSaving(true); @@ -767,34 +898,236 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes {selectedWeek?.modules?.length ? selectedWeek.modules.map((module) => { const enabled = moduleEnabledById[module.id] !== false; return ( - +
+ { + const checked = event.target.checked; + setModuleEnabledById((current) => ({ ...current, [module.id]: checked })); + }} + /> + {module.sortOrder} + + {module.name} + {module.category} · {module.folderName} + + + {enabled ? '사용' : '미사용'} + +
+
+
+ 기본 파라미터 + +
+
+ {(moduleDefaultParametersById[module.id] ?? []).map((parameter, parameterIndex) => ( +
+ setModuleDefaultParametersById((current) => ({ + ...current, + [module.id]: (current[module.id] ?? []).map((item, index) => index === parameterIndex ? { ...item, key: event.target.value } : item), + }))} + /> + setModuleDefaultParametersById((current) => ({ + ...current, + [module.id]: (current[module.id] ?? []).map((item, index) => index === parameterIndex ? { ...item, value: event.target.value } : item), + }))} + /> + +
+ ))} +
+
+ ); }) : (

등록된 모듈이 없습니다.

)} + {(selectedWeek?.environmentProfiles?.length ?? 0) > 0 ? ( +
+
+

환경별 모듈 구성

+

소스 저장소는 공통으로 유지하고, 로컬과 플랫폼에서 실행할 모듈만 다르게 선택합니다.

+
+
+ {(selectedWeek.environmentProfiles ?? []).map((profile) => { + const selectedModuleIds = new Set(environmentModuleIdsByPlatformKey[profile.platformKey] ?? []); + const selectedCount = (selectedWeek.modules ?? []).filter((module) => selectedModuleIds.has(module.id)).length; + const targetLabel = profile.platformKey === 'futuredrive-local-pc' ? '로컬' : '플랫폼'; + return ( +
+
+
+ {profile.name} + {targetLabel} · {profile.profileType} +
+ {selectedCount}/{selectedWeek.modules?.length ?? 0} +
+
+ {(selectedWeek.modules ?? []).map((module) => { + const globallyEnabled = moduleEnabledById[module.id] !== false; + const checked = selectedModuleIds.has(module.id); + const parameterRows = environmentModuleParametersByPlatformKey[profile.platformKey]?.[module.id] ?? []; + return ( +
+ + {checked ? ( +
+ +
+ 실행 파라미터 + +
+ {parameterRows.map((parameter, parameterIndex) => ( +
+ setEnvironmentModuleParametersByPlatformKey((current) => { + const rows = [...(current[profile.platformKey]?.[module.id] ?? [])]; + rows[parameterIndex] = { ...rows[parameterIndex], key: event.target.value }; + return { ...current, [profile.platformKey]: { ...(current[profile.platformKey] ?? {}), [module.id]: rows } }; + })} + /> + setEnvironmentModuleParametersByPlatformKey((current) => { + const rows = [...(current[profile.platformKey]?.[module.id] ?? [])]; + rows[parameterIndex] = { ...rows[parameterIndex], value: event.target.value }; + return { ...current, [profile.platformKey]: { ...(current[profile.platformKey] ?? {}), [module.id]: rows } }; + })} + /> + +
+ ))} +
+ ) : null} +
+ ); + })} +
+
+ ); + })} +
+
+ ) : null}
@@ -838,6 +1171,43 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes )}
+
+
+
+

학습 맵

+

학생이 맵 선택 화면에서 사용할 NAV2 학습 환경을 선택하세요.

+
+ +
+
+ {mapOptions.map((map) => { + const enabled = Boolean(mapEnabledByKey[map.key]); + return ( + + ); + })} +
+
) : activeTab === 'examples' ? (
@@ -1218,6 +1588,34 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes
+ {mapCreateOpen ? ( +
+
+
+

학습 맵 추가

+

등록 후 현재 주차에 선택된 상태로 추가됩니다.

+
+
+ + +
+
+ + +
+
+
+ ) : null} + (null); const [selectedMasterId, setSelectedMasterId] = useState(''); const [selectedAgentId, setSelectedAgentId] = useState(''); const [platformName, setPlatformName] = useState(''); const [agentsLoading, setAgentsLoading] = useState(false); const [agentsError, setAgentsError] = useState(null); + const [registrationCode, setRegistrationCode] = useState(null); + const [registrationExpiresAt, setRegistrationExpiresAt] = useState(null); const [settingsName, setSettingsName] = useState(''); const [settingsVehicleType, setSettingsVehicleType] = useState('MK-mini'); const [settingsWheelbaseMeters, setSettingsWheelbaseMeters] = useState(0.325); @@ -90,7 +94,7 @@ export function PlatformsPage({ session }: { session: AuthSession }) { const [assignmentGroupsError, setAssignmentGroupsError] = useState(null); const [selectedAssignmentGroupId, setSelectedAssignmentGroupId] = useState(''); const [assigningPlatformId, setAssigningPlatformId] = useState(null); - const [forceAssignmentTarget, setForceAssignmentTarget] = useState<{ platform: InstructorPlatform; group: InstructorGroup; session: PlatformInUseError['activeSession'] } | null>(null); + const [forceAssignmentTarget, setForceAssignmentTarget] = useState<{ platform: InstructorPlatform; group: InstructorGroup | null; session: PlatformInUseError['activeSession'] } | null>(null); const filteredPlatforms = useMemo(() => { const keyword = query.trim().toLowerCase(); @@ -106,7 +110,7 @@ export function PlatformsPage({ session }: { session: AuthSession }) { const keys = new Set(); platforms.forEach((platform) => { if (!platform.agent) return; - [platform.agent.nodeId, platform.agent.hostname, platform.agent.ip, platform.agent.ownerEmail] + [platform.agent.nodeId, platform.agent.hostname, platform.agent.ip] .map(normalizeAgentKey) .filter(Boolean) .forEach((key) => keys.add(key)); @@ -121,6 +125,7 @@ export function PlatformsPage({ session }: { session: AuthSession }) { { label: '오류 발생', value: `${summary.errorPlatforms}대`, icon: CircleAlert, tone: 'bg-[#fff1f2] text-[#e11d48]' }, ]; const selectedAssignmentGroup = assignmentGroups.find((group) => group.id === selectedAssignmentGroupId) ?? null; + const assignmentUnassignedSelected = selectedAssignmentGroupId === 'unassigned'; const applyPayload = (payload: InstructorPlatformsResponse) => { setError(null); @@ -159,13 +164,13 @@ export function PlatformsPage({ session }: { session: AuthSession }) { }, [filteredPlatforms.length, page, pageSize]); useEffect(() => { - if (!modalOpen && !settingsTarget && !deleteTarget && !assignmentTarget) return; + if (!modalOpen && !registrationModalOpen && !settingsTarget && !deleteTarget && !assignmentTarget) return; const originalOverflow = document.body.style.overflow; document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = originalOverflow; }; - }, [assignmentTarget, modalOpen, settingsTarget, deleteTarget]); + }, [assignmentTarget, deleteTarget, modalOpen, registrationModalOpen, settingsTarget]); useEffect(() => { if (!modalOpen) return; @@ -238,6 +243,20 @@ export function PlatformsPage({ session }: { session: AuthSession }) { } }; + const handleIssueRegistrationCode = async () => { + setSaving(true); + setError(null); + try { + const payload = await issueInstructorPlatformRegistration(session.accessToken); + setRegistrationCode(payload.code); + setRegistrationExpiresAt(payload.expiresAt); + } catch (reason) { + setError(reason instanceof Error ? reason.message : '등록 키를 발급하지 못했습니다.'); + } finally { + setSaving(false); + } + }; + const handleDeletePlatform = async (platform: InstructorPlatform, force = false) => { setDeletingPlatformId(platform.id); setError(null); @@ -283,20 +302,20 @@ export function PlatformsPage({ session }: { session: AuthSession }) { const handleAssignPlatform = async ( platform: InstructorPlatform, - group: InstructorGroup, + group: InstructorGroup | null, force = false, ) => { setAssigningPlatformId(platform.id); setError(null); try { - const payload = await assignInstructorPlatform(session.accessToken, platform.id, group.id, { force }); + const payload = await assignInstructorPlatform(session.accessToken, platform.id, group?.id ?? 'unassigned', { force }); applyPayload(payload); setAssignmentTarget(null); setForceAssignmentTarget(null); setSelectedAssignmentGroupId(''); showToast({ - title: '플랫폼이 배정되었습니다.', - message: `${platform.name} · ${group.name}`, + title: group ? '플랫폼이 배정되었습니다.' : '플랫폼 배정이 해제되었습니다.', + message: group ? `${platform.name} · ${group.name}` : platform.name, }); } catch (reason) { if (reason instanceof PlatformInUseError) { @@ -353,10 +372,24 @@ export function PlatformsPage({ session }: { session: AuthSession }) { -