From ad41509aa41563236180d6f1eedef99b480443e2 Mon Sep 17 00:00:00 2001 From: web-admin Date: Thu, 6 Aug 2026 07:33:51 +0000 Subject: [PATCH] platform/lms-adm-fe release promotion snapshot --- package-lock.json | 69 ++++ package.json | 2 + src/lib/curriculum.ts | 4 +- src/lib/submissions.ts | 56 +++ src/pages/curriculum/CurriculumPage.tsx | 177 ++++----- src/pages/dashboard/DashboardPage.tsx | 2 +- src/pages/students/StudentsPage.tsx | 22 +- src/pages/submissions/SubmissionsPage.tsx | 461 ++++++++++++---------- 8 files changed, 480 insertions(+), 313 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3834567..783e813 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,11 @@ "name": "lms-adm-fe", "version": "0.1.0", "dependencies": { + "@monaco-editor/react": "^4.7.0", "@tailwindcss/vite": "^4.3.0", "@vitejs/plugin-react": "^4.4.1", "lucide-react": "^1.14.0", + "monaco-editor": "^0.56.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-router-dom": "^7.18.0", @@ -748,6 +750,29 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@monaco-editor/loader": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", + "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==", + "license": "MIT", + "dependencies": { + "state-local": "^1.0.6" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", + "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", + "license": "MIT", + "dependencies": { + "@monaco-editor/loader": "^1.5.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -1460,6 +1485,13 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -1597,6 +1629,15 @@ "node": ">=8" } }, + "node_modules/dompurify": { + "version": "3.4.8", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.8.tgz", + "integrity": "sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.364", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.364.tgz", @@ -2061,6 +2102,28 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/monaco-editor": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.56.0.tgz", + "integrity": "sha512-sXboRm3BeBeLm938eaiyLMe0OxzfXIlZvbv4ir/jVgQy1zDhWjgmny0WoN45fuDKhCCQsYMbBJrv/A6jd8aCUg==", + "license": "MIT", + "dependencies": { + "dompurify": "3.4.8", + "marked": "14.0.0" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2310,6 +2373,12 @@ "node": ">=0.10.0" } }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", diff --git a/package.json b/package.json index b231ed8..76a0d64 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,11 @@ "preview": "vite preview" }, "dependencies": { + "@monaco-editor/react": "^4.7.0", "@tailwindcss/vite": "^4.3.0", "@vitejs/plugin-react": "^4.4.1", "lucide-react": "^1.14.0", + "monaco-editor": "^0.56.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-router-dom": "^7.18.0", diff --git a/src/lib/curriculum.ts b/src/lib/curriculum.ts index c1b9dda..7c4b790 100644 --- a/src/lib/curriculum.ts +++ b/src/lib/curriculum.ts @@ -25,12 +25,12 @@ export type CurriculumWeek = { references?: CurriculumReference[]; exampleCodes?: CurriculumReference[]; deliverable: string; - requiresSourceSubmission: boolean; }; modules?: CurriculumModule[]; environmentProfiles?: CurriculumEnvironmentProfile[]; visualizationPlatforms?: CurriculumVisualizationPlatform[]; mapKeys?: string[]; + isTeamProject?: boolean; }; export type CurriculumMap = { @@ -145,7 +145,7 @@ export async function updateInstructorCurriculum( duration?: string; overview?: string; deliverable?: string; - requiresSourceSubmission?: boolean; + isTeamProject?: boolean; topics?: string[]; learningSteps?: Array<{ title?: string; diff --git a/src/lib/submissions.ts b/src/lib/submissions.ts index b474796..c66e4ed 100644 --- a/src/lib/submissions.ts +++ b/src/lib/submissions.ts @@ -2,6 +2,8 @@ export type InstructorSubmissionCommitFile = { path: string; status: string; patch: string; + originalRef?: string | null; + modifiedRef?: string | null; }; export type InstructorSubmissionCommit = { @@ -30,12 +32,14 @@ export type InstructorSubmissionItem = { id: string; week: number; title: string; + isTeamProject: boolean; }; module: { id: string; name: string; folderName: string; sortOrder: number; + sourceType?: 'CUSTOM' | 'GIT'; }; student: { id: string; @@ -43,6 +47,17 @@ export type InstructorSubmissionItem = { email: string; group: string; }; + team?: { + id: string; + name: string; + members: Array<{ + id: string; + name: string; + email: string; + commitCount: number; + lastCommittedAt: string | null; + }>; + } | null; repository: { id: string; owner: string; @@ -73,6 +88,7 @@ export type InstructorSubmissionsResponse = { id: string; week: number; title: string; + isTeamProject: boolean; modules: Array<{ id: string; name: string; @@ -88,6 +104,7 @@ export type InstructorSubmissionsResponse = { group: string; }>; items: InstructorSubmissionItem[]; + teamItems: InstructorSubmissionItem[]; }; const LMS_API_BASE_URL = (import.meta.env.VITE_API_BASE_URL ?? 'https://workspace.autonomystudio.net/lms-api/api').replace(/\/+$/, ''); @@ -106,6 +123,45 @@ export async function listInstructorSubmissions(accessToken: string): Promise; } +export async function getInstructorSubmissionFile( + accessToken: string, + input: { + owner: string; + repoName: string; + path: string; + baseRef?: string | null; + headRef?: string | null; + }, +): Promise<{ + original: string; + modified: string; + originalAvailable: boolean; + modifiedAvailable: boolean; +}> { + const query = new URLSearchParams({ + owner: input.owner, + repo: input.repoName, + path: input.path, + }); + if (input.baseRef) query.set('baseRef', input.baseRef); + if (input.headRef) query.set('headRef', input.headRef); + + const response = await fetch(`${LMS_API_BASE_URL}/instructor/submissions/file?${query.toString()}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + if (!response.ok) { + throw new Error(`파일 내용을 불러오지 못했습니다. (${response.status})`); + } + return response.json() as Promise<{ + original: string; + modified: string; + originalAvailable: boolean; + modifiedAvailable: boolean; + }>; +} + export async function saveInstructorSubmissionScore( accessToken: string, input: { studentId: string; weekId: string; score: number }, diff --git a/src/pages/curriculum/CurriculumPage.tsx b/src/pages/curriculum/CurriculumPage.tsx index cf18f74..5b9e290 100644 --- a/src/pages/curriculum/CurriculumPage.tsx +++ b/src/pages/curriculum/CurriculumPage.tsx @@ -1,6 +1,6 @@ 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 { CheckCircle2, CirclePlay, Clock3, FileCode2, FileText, Loader2, Map, Plus, RadioTower, Save, Target, Trash2, Upload, UsersRound, type LucideIcon } from 'lucide-react'; import { ConfirmDialog } from '../../components/ConfirmDialog'; import { PageHeader } from '../../components/PageHeader'; import { useToast } from '../../components/ToastProvider'; @@ -26,8 +26,8 @@ function isPlatformEnvironment(config: unknown): boolean { return target === 'remote-jetson' || target === 'platform'; } -function environmentStorageKey(profile: Pick): 'local' | 'platform' { - return isPlatformEnvironment(profile.config) ? 'platform' : 'local'; +function environmentStorageKey(profile: Pick): 'local' | 'platform' { + return profile.profileType.toUpperCase() === 'PLATFORM' || isPlatformEnvironment(profile.config) ? 'platform' : 'local'; } function parseModuleParameter(parameter: string): ModuleParameterDraft { @@ -121,7 +121,7 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes const [draftDuration, setDraftDuration] = useState(''); const [draftOverview, setDraftOverview] = useState(''); const [draftDeliverable, setDraftDeliverable] = useState(''); - const [draftRequiresSourceSubmission, setDraftRequiresSourceSubmission] = useState(true); + const [draftIsTeamProject, setDraftIsTeamProject] = useState(false); const [draftTopics, setDraftTopics] = useState([]); const [draftTopicDetails, setDraftTopicDetails] = useState([]); const [draftTopicVideoUrls, setDraftTopicVideoUrls] = useState([]); @@ -173,39 +173,19 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes 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]); + return (['local', 'platform'] as const).map((storageKey) => { + const savedProfile = (selectedWeek.environmentProfiles ?? []).find((profile) => environmentStorageKey(profile) === storageKey); + return { + id: `draft:${selectedWeek.week}:${storageKey}`, + platformKey: storageKey, + profileType: storageKey === 'local' ? 'LOCAL' : 'PLATFORM', + name: storageKey === 'local' ? '로컬' : '플랫폼', + config: savedProfile?.config ?? (storageKey === 'platform' ? { target: 'remote-jetson' } : { target: 'local' }), + storageKey, + platformKeys: [storageKey], + }; + }); + }, [selectedWeek]); const resolveProgressSelection = (nextWeeks: CurriculumWeek[], setting: InstructorCurriculumResponse['setting']) => { const nextWeek = nextWeeks.find((week) => week.state === 'current')?.week @@ -300,7 +280,7 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes setDraftDuration(selectedWeek.duration); setDraftOverview(selectedWeek.detail.overview); setDraftDeliverable(selectedWeek.detail.deliverable); - setDraftRequiresSourceSubmission(selectedWeek.detail.requiresSourceSubmission ?? true); + setDraftIsTeamProject(Boolean(selectedWeek.isTeamProject)); setDraftTopics(selectedWeek.topics.length ? selectedWeek.topics : ['']); setDraftTopicDetails(selectedWeek.detail.topicDetails.length ? selectedWeek.detail.topicDetails : ['']); const topicVideoUrls = selectedWeek.topics.map((_, index) => selectedWeek.detail.topicVideoUrls?.[index] ?? ''); @@ -376,7 +356,7 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes duration: draftDuration, overview: draftOverview, deliverable: draftDeliverable, - requiresSourceSubmission: draftRequiresSourceSubmission, + isTeamProject: draftIsTeamProject, topics: draftTopics.map((topic) => topic.trim()).filter(Boolean), learningSteps: buildDraftLearningSteps(), topicDetails: draftTopicDetails, @@ -424,7 +404,7 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes duration: draftDuration, overview: draftOverview, deliverable: draftDeliverable, - requiresSourceSubmission: draftRequiresSourceSubmission, + isTeamProject: draftIsTeamProject, topics, learningSteps: buildDraftLearningSteps(), topicDetails: draftTopicDetails, @@ -465,7 +445,7 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes duration: draftDuration, overview: draftOverview, deliverable: draftDeliverable, - requiresSourceSubmission: draftRequiresSourceSubmission, + isTeamProject: draftIsTeamProject, topics: draftTopics.map((topic) => topic.trim()).filter(Boolean), learningSteps: buildDraftLearningSteps(), topicDetails: draftTopicDetails, @@ -577,7 +557,6 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes duration: '-', overview: '', deliverable: '', - requiresSourceSubmission: true, topics: ['학습단계 1'], learningSteps: [{ title: '학습단계 1', description: '', videoEnabled: false, videoUrl: '', concepts: [] }], topicDetails: [''], @@ -751,7 +730,7 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes
- +
@@ -988,6 +967,9 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes const globallyEnabled = moduleEnabledById[module.id] !== false; const checked = selectedModuleIds.has(module.id); const parameterRows = environmentModuleParametersByPlatformKey[profile.storageKey]?.[module.id] ?? []; + const dockerImage = environmentModuleDockerImagesByPlatformKey[profile.storageKey]?.[module.id] + || module.dockerImage + || '-'; return (
{checked ? (
- +
+ 실행 이미지 + {dockerImage} +
실행 파라미터