feat: add LMS web client

This commit is contained in:
2026-07-15 13:41:00 +09:00
commit 05ff2644e8
308 changed files with 487923 additions and 0 deletions
@@ -0,0 +1,137 @@
import { useCallback, useEffect } from 'react'
import { LMS_API_BASE_URL } from '../../config/appConfig'
import type { CurriculumWeek } from '../curriculum/types'
import type { ModulePrebuiltArtifact, ModuleRow, RuntimeImageArtifact } from './types'
type UseRemoteWorkspaceRuntimeOptions = {
availableModules: ModuleRow[]
curriculumModules: CurriculumWeek['modules']
courseKey: string
weekNumber: number
clonePathPreview: string
remoteWorkspace: boolean
remoteWorkspaceTargetKey: string
activeRuntimeImages: RuntimeImageArtifact[]
getFreshAccessToken: () => Promise<string>
getRemoteWorkspaceTarget: () => RemoteWorkspaceTarget
saveDirtyFilesForModule: (module: ModuleRow) => Promise<void>
onRegisterStopRunningModules?: (handler: (() => Promise<void>) | null) => void
}
type UseRemoteWorkspaceRuntimeResult = {
prepareRemoteRuntimeModule: (module: ModuleRow) => Promise<void>
}
export function useRemoteWorkspaceRuntime({
availableModules,
curriculumModules,
courseKey,
weekNumber,
clonePathPreview,
remoteWorkspace,
remoteWorkspaceTargetKey,
activeRuntimeImages,
getFreshAccessToken,
getRemoteWorkspaceTarget,
saveDirtyFilesForModule,
onRegisterStopRunningModules
}: UseRemoteWorkspaceRuntimeOptions): UseRemoteWorkspaceRuntimeResult {
const prepareRemoteRuntimeModule = useCallback(async (module: ModuleRow): Promise<void> => {
const requestAccessToken = await getFreshAccessToken()
await saveDirtyFilesForModule(module)
const response = await fetch(`${LMS_API_BASE_URL}/studio/repositories/prepare`, {
method: 'POST',
headers: {
Authorization: `Bearer ${requestAccessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
courseId: courseKey,
week: weekNumber,
moduleName: module.name,
folderName: module.folderName,
templateRepositoryUrl: module.templateRepositoryUrl
})
})
if (!response.ok) throw new Error(await response.text())
const prepared = await response.json() as {
repositoryUrlWithToken?: string
templateRepositoryUrlWithToken?: string
templateRepositoryUrl?: string
prebuiltArtifacts?: ModulePrebuiltArtifact[] | null
}
const submitResult = await window.localWorkspace.git.submit(
module.name,
clonePathPreview,
module.folderName,
`Run: ${weekNumber}주차 ${module.name}`,
undefined,
undefined,
undefined
)
if (!submitResult.ok && !submitResult.message.includes('제출할 변경사항이 없습니다')) {
throw new Error(`[${module.name}] ${submitResult.message}`)
}
const templateRepositoryUrl = prepared.templateRepositoryUrlWithToken ?? prepared.templateRepositoryUrl ?? module.templateRepositoryUrl
await window.remoteWorkspace.modules.install(
getRemoteWorkspaceTarget(),
module.name,
module.folderName,
templateRepositoryUrl,
prepared.repositoryUrlWithToken,
{
prebuiltArtifacts: prepared.prebuiltArtifacts ?? module.prebuiltArtifacts ?? null,
runtimeImages: activeRuntimeImages
}
)
}, [
activeRuntimeImages,
clonePathPreview,
courseKey,
getFreshAccessToken,
getRemoteWorkspaceTarget,
saveDirtyFilesForModule,
weekNumber
])
const stopRunningWorkspaceModules = useCallback(async (): Promise<void> => {
if (remoteWorkspace) {
const moduleMap = new Map<string, { moduleName: string; folderName?: string | null; sortOrder?: number | null }>()
for (const module of availableModules) {
moduleMap.set(module.name, {
moduleName: module.name,
folderName: module.folderName,
sortOrder: module.sortOrder
})
}
for (const module of curriculumModules ?? []) {
if (!moduleMap.has(module.name)) {
moduleMap.set(module.name, {
moduleName: module.name,
folderName: module.folderName,
sortOrder: module.sortOrder
})
}
}
const modules = Array.from(moduleMap.values())
.sort((left, right) => (right.sortOrder ?? 0) - (left.sortOrder ?? 0))
.map(({ moduleName, folderName }) => ({ moduleName, folderName }))
if (!modules.length) return
await window.remoteWorkspace.modules.resetSession(getRemoteWorkspaceTarget(), modules).catch(() => ({ ok: false, message: '플랫폼 초기화 실패' }))
return
}
await window.localWorkspace.modules.stopRunning().catch(() => ({ ok: false, message: '모듈 중지 실패' }))
}, [availableModules, curriculumModules, getRemoteWorkspaceTarget, remoteWorkspace, remoteWorkspaceTargetKey])
useEffect(() => {
onRegisterStopRunningModules?.(stopRunningWorkspaceModules)
return () => onRegisterStopRunningModules?.(null)
}, [onRegisterStopRunningModules, stopRunningWorkspaceModules])
return {
prepareRemoteRuntimeModule
}
}