From d44b235bd6ddbd848a5d48265f667b4d1120397b Mon Sep 17 00:00:00 2001 From: web-admin Date: Tue, 4 Aug 2026 02:12:50 +0000 Subject: [PATCH] platform/lms-adm-fe release promotion snapshot --- .vite/deps/_metadata.json | 8 + .vite/deps/package.json | 3 + src/pages/curriculum/CurriculumPage.tsx | 379 ++++++++++-------------- 3 files changed, 166 insertions(+), 224 deletions(-) create mode 100644 .vite/deps/_metadata.json create mode 100644 .vite/deps/package.json diff --git a/.vite/deps/_metadata.json b/.vite/deps/_metadata.json new file mode 100644 index 0000000..1f63db5 --- /dev/null +++ b/.vite/deps/_metadata.json @@ -0,0 +1,8 @@ +{ + "hash": "c60753b8", + "configHash": "84cc1fb0", + "lockfileHash": "707b26ba", + "browserHash": "992b5a32", + "optimized": {}, + "chunks": {} +} \ No newline at end of file diff --git a/.vite/deps/package.json b/.vite/deps/package.json new file mode 100644 index 0000000..3dbc1ca --- /dev/null +++ b/.vite/deps/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/src/pages/curriculum/CurriculumPage.tsx b/src/pages/curriculum/CurriculumPage.tsx index a8644b9..cf18f74 100644 --- a/src/pages/curriculum/CurriculumPage.tsx +++ b/src/pages/curriculum/CurriculumPage.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; 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'; @@ -14,6 +15,21 @@ type ModuleParameterDraft = { value: string; }; +type EditableEnvironmentProfile = CurriculumEnvironmentProfile & { + storageKey: 'local' | 'platform'; + platformKeys: string[]; +}; + +function isPlatformEnvironment(config: unknown): boolean { + if (!config || typeof config !== 'object' || Array.isArray(config)) return false; + const target = (config as Record).target; + return target === 'remote-jetson' || target === 'platform'; +} + +function environmentStorageKey(profile: Pick): 'local' | 'platform' { + return isPlatformEnvironment(profile.config) ? 'platform' : 'local'; +} + function parseModuleParameter(parameter: string): ModuleParameterDraft { const separatorIndex = parameter.indexOf(':='); if (separatorIndex < 0) return { key: parameter.trim(), value: '' }; @@ -38,37 +54,56 @@ function readEnvironmentModuleIds(profile: CurriculumEnvironmentProfile, modules )); } -function readEnvironmentModuleParameters(profile: CurriculumEnvironmentProfile): Record { +function readEnvironmentModuleParameters( + profile: CurriculumEnvironmentProfile, + modules: NonNullable, +): 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 {}; + const configuredModules = new globalThis.Map>( + !moduleSet || !Array.isArray(moduleSet.modules) + ? [] + : moduleSet.modules.flatMap((item) => ( + item && typeof item === 'object' && typeof (item as Record).id === 'string' + ? [[(item as Record).id as string, item as Record]] + : [] + )), + ); + const moduleDefaults = new globalThis.Map(modules.map((module) => [module.id, module.defaultParameters ?? []])); - 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]]; + return Object.fromEntries(readEnvironmentModuleIds(profile, modules).map((moduleId) => { + const configured = configuredModules.get(moduleId); + const parameters = Array.isArray(configured?.defaultParameters) + ? configured.defaultParameters.filter((value): value is string => typeof value === 'string') + : moduleDefaults.get(moduleId) ?? []; + return [moduleId, parameters.map(parseModuleParameter)]; })); } -function readEnvironmentModuleDockerImages(profile: CurriculumEnvironmentProfile): Record { +function readEnvironmentModuleDockerImages( + profile: CurriculumEnvironmentProfile, + modules: NonNullable, +): 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 {}; + const configuredModules = new globalThis.Map>( + !moduleSet || !Array.isArray(moduleSet.modules) + ? [] + : moduleSet.modules.flatMap((item) => ( + item && typeof item === 'object' && typeof (item as Record).id === 'string' + ? [[(item as Record).id as string, item as Record]] + : [] + )), + ); + const moduleDefaults = new globalThis.Map(modules.map((module) => [module.id, module.dockerImage ?? ''])); - 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]] - : []; + return Object.fromEntries(readEnvironmentModuleIds(profile, modules).map((moduleId) => { + const configuredImage = configuredModules.get(moduleId)?.dockerImage; + return [moduleId, typeof configuredImage === 'string' ? configuredImage : moduleDefaults.get(moduleId) ?? '']; })); } @@ -111,7 +146,7 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes const [referenceFile, setReferenceFile] = useState(null); const [exampleCodeTitle, setExampleCodeTitle] = useState(''); const [exampleCodeFile, setExampleCodeFile] = useState(null); - const [activeTab, setActiveTab] = useState<'apply' | 'edit' | 'modules' | 'examples'>('apply'); + const [activeTab, setActiveTab] = useState<'apply' | 'edit' | 'modules' | 'maps' | 'examples'>('apply'); const [error, setError] = useState(null); const [saving, setSaving] = useState(false); const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); @@ -135,6 +170,42 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes () => `${CURRICULUM_SELECTED_GROUP_STORAGE_PREFIX}:${session.user.id}:${selectedCourseId || 'default'}`, [selectedCourseId, session.user.id], ); + const editableEnvironmentProfiles = useMemo(() => { + if (!selectedWeek) return []; + + const savedProfiles = new globalThis.Map((selectedWeek.environmentProfiles ?? []).map((profile) => [profile.platformKey, profile])); + const profilesByEnvironment = new globalThis.Map<'local' | 'platform', EditableEnvironmentProfile>(); + + visualizationPlatforms + .filter((platform) => visualizationEnabledByKey[platform.key]) + .forEach((platform) => { + const profile = savedProfiles.get(platform.key) ?? { + id: `draft:${selectedWeek.week}:${platform.key}`, + platformKey: platform.key, + profileType: platform.type, + name: platform.displayName, + config: platform.config && typeof platform.config === 'object' && !Array.isArray(platform.config) + ? platform.config as Record + : undefined, + }; + const storageKey = environmentStorageKey(profile); + const existing = profilesByEnvironment.get(storageKey); + if (existing) { + existing.platformKeys.push(platform.key); + return; + } + profilesByEnvironment.set(storageKey, { + ...profile, + id: `draft:${selectedWeek.week}:${storageKey}`, + name: storageKey === 'local' ? '로컬' : '플랫폼', + profileType: storageKey === 'local' ? 'LOCAL' : 'PLATFORM', + storageKey, + platformKeys: [platform.key], + }); + }); + + return [...profilesByEnvironment.values()]; + }, [selectedWeek, visualizationEnabledByKey, visualizationPlatforms]); const resolveProgressSelection = (nextWeeks: CurriculumWeek[], setting: InstructorCurriculumResponse['setting']) => { const nextWeek = nextWeeks.find((week) => week.state === 'current')?.week @@ -239,17 +310,21 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes 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 ?? []), + const profilesByEnvironment = (selectedWeek.environmentProfiles ?? []).reduce>((groups, profile) => { + groups[environmentStorageKey(profile)].push(profile); + return groups; + }, { local: [], platform: [] }); + setEnvironmentModuleIdsByPlatformKey(Object.fromEntries(Object.entries(profilesByEnvironment).map(([key, profiles]) => [ + key, + [...new Set(profiles.flatMap((profile) => readEnvironmentModuleIds(profile, selectedWeek.modules ?? [])))], ]))); - setEnvironmentModuleParametersByPlatformKey(Object.fromEntries((selectedWeek.environmentProfiles ?? []).map((profile) => [ - profile.platformKey, - readEnvironmentModuleParameters(profile), + setEnvironmentModuleParametersByPlatformKey(Object.fromEntries(Object.entries(profilesByEnvironment).map(([key, profiles]) => [ + key, + Object.assign({}, ...profiles.map((profile) => readEnvironmentModuleParameters(profile, selectedWeek.modules ?? []))), ]))); - setEnvironmentModuleDockerImagesByPlatformKey(Object.fromEntries((selectedWeek.environmentProfiles ?? []).map((profile) => [ - profile.platformKey, - readEnvironmentModuleDockerImages(profile), + setEnvironmentModuleDockerImagesByPlatformKey(Object.fromEntries(Object.entries(profilesByEnvironment).map(([key, profiles]) => [ + key, + Object.assign({}, ...profiles.map((profile) => readEnvironmentModuleDockerImages(profile, selectedWeek.modules ?? []))), ]))); setVisualizationEnabledByKey(Object.fromEntries((selectedWeek.visualizationPlatforms ?? []).map((platform) => [platform.key, true]))); setMapEnabledByKey(Object.fromEntries((selectedWeek.mapKeys ?? []).map((key) => [key, true]))); @@ -424,17 +499,20 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes .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) => ({ + const environmentModuleSets = editableEnvironmentProfiles.flatMap((profile) => { + const moduleIds = environmentModuleIdsByPlatformKey[profile.storageKey] ?? (selectedWeek.modules ?? []).map((module) => module.id); + return profile.platformKeys.map((platformKey) => ({ + platformKey, + moduleIds, + modules: moduleIds.map((id) => ({ id, - defaultParameters: (environmentModuleParametersByPlatformKey[profile.platformKey]?.[id] ?? []) + defaultParameters: (environmentModuleParametersByPlatformKey[profile.storageKey]?.[id] ?? []) .filter((parameter) => parameter.key.trim()) .map((parameter) => `${parameter.key.trim()}:=${parameter.value.trim()}`), - dockerImage: environmentModuleDockerImagesByPlatformKey[profile.platformKey]?.[id]?.trim() || undefined, + dockerImage: environmentModuleDockerImagesByPlatformKey[profile.storageKey]?.[id]?.trim() || undefined, })), - })); + })); + }); const payload = await updateInstructorCurriculum(session.accessToken, { courseId: selectedCourseId || null, groupId: selectedGroupId || null, @@ -444,9 +522,6 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes 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); @@ -714,6 +789,13 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes > 모듈 관리 + ) : null} {activeTab === 'apply' ? ( @@ -880,127 +962,24 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes ) - ) : activeTab === 'modules' ? ( + ) : activeTab === 'modules' ? (
-
-
-
-

모듈 관리

-

체크된 모듈만 학생 앱의 모듈 리스트에 표시됩니다.

-
- {selectedWeek?.modules?.length ? ( - - {(selectedWeek.modules ?? []).filter((module) => moduleEnabledById[module.id] !== false).length}/{selectedWeek.modules.length} - - ) : null} -
-
- {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 ? ( -
+ {editableEnvironmentProfiles.length > 0 ? ( +

환경별 모듈 구성

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

- {(selectedWeek.environmentProfiles ?? []).map((profile) => { - const selectedModuleIds = new Set(environmentModuleIdsByPlatformKey[profile.platformKey] ?? []); + {editableEnvironmentProfiles.map((profile) => { + const selectedModuleIds = new Set(environmentModuleIdsByPlatformKey[profile.storageKey] ?? (selectedWeek.modules ?? []).map((module) => module.id)); const selectedCount = (selectedWeek.modules ?? []).filter((module) => selectedModuleIds.has(module.id)).length; - const targetLabel = profile.platformKey === 'futuredrive-local-pc' ? '로컬' : '플랫폼'; return (
{profile.name} - {targetLabel} · {profile.profileType} + {profile.storageKey === 'local' ? '로컬 실행 환경' : '플랫폼 실행 환경'}
{selectedCount}/{selectedWeek.modules?.length ?? 0}
@@ -1008,7 +987,7 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes {(selectedWeek.modules ?? []).map((module) => { const globallyEnabled = moduleEnabledById[module.id] !== false; const checked = selectedModuleIds.has(module.id); - const parameterRows = environmentModuleParametersByPlatformKey[profile.platformKey]?.[module.id] ?? []; + const parameterRows = environmentModuleParametersByPlatformKey[profile.storageKey]?.[module.id] ?? []; return (
{ const enabled = event.target.checked; setEnvironmentModuleIdsByPlatformKey((current) => { - const currentIds = new Set(current[profile.platformKey] ?? []); + const currentIds = new Set(current[profile.storageKey] ?? (selectedWeek.modules ?? []).map((item) => item.id)); if (enabled) currentIds.add(module.id); else currentIds.delete(module.id); - return { ...current, [profile.platformKey]: [...currentIds] }; + return { ...current, [profile.storageKey]: [...currentIds] }; }); if (!enabled) { setEnvironmentModuleParametersByPlatformKey((current) => { - const profileParameters = { ...(current[profile.platformKey] ?? {}) }; + const profileParameters = { ...(current[profile.storageKey] ?? {}) }; delete profileParameters[module.id]; - return { ...current, [profile.platformKey]: profileParameters }; + return { ...current, [profile.storageKey]: profileParameters }; }); setEnvironmentModuleDockerImagesByPlatformKey((current) => { - const profileImages = { ...(current[profile.platformKey] ?? {}) }; + const profileImages = { ...(current[profile.storageKey] ?? {}) }; delete profileImages[module.id]; - return { ...current, [profile.platformKey]: profileImages }; + return { ...current, [profile.storageKey]: profileImages }; }); } }} @@ -1049,13 +1028,13 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes
) : null} -
-
-
-

시각화 플랫폼

-

시각화 화면을 사용할 플랫폼을 선택하세요.

-
- {visualizationPlatforms.length ? ( - - {visualizationPlatforms.filter((platform) => visualizationEnabledByKey[platform.key]).length}/{visualizationPlatforms.length} - - ) : null} -
-
- {visualizationPlatforms.length ? visualizationPlatforms.map((platform) => { - const enabled = Boolean(visualizationEnabledByKey[platform.key]); - return ( - - ); - }) : ( -

등록된 시각화 플랫폼이 없습니다.

- )} -
-
-
-
+
+ ) : activeTab === 'maps' ? ( +
+
+

학습 맵

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

-
{mapOptions.map((map) => { @@ -1588,7 +1519,7 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes
- {mapCreateOpen ? ( + {mapCreateOpen ? createPortal(
@@ -1614,7 +1545,7 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes
- ) : null} + , document.body) : null}