From f0df24ecaa92be392be72e171b09782c09f501c0 Mon Sep 17 00:00:00 2001 From: chyoo Date: Tue, 7 Jul 2026 16:12:30 +0900 Subject: [PATCH] modify publick curriculum --- src/main/index.ts | 25 ++++++- src/renderer/src/App.tsx | 72 +++++++++++++------ src/renderer/src/components/ChatWidgets.tsx | 8 ++- .../curriculum/CurriculumDetailPanel.tsx | 20 +++++- .../curriculum/CurriculumHomeScreen.tsx | 8 ++- .../features/curriculum/LearningStatsCard.tsx | 15 ++-- src/renderer/src/features/curriculum/types.ts | 9 +++ .../workspace/WorkspaceGuidePanel.tsx | 15 ++-- 8 files changed, 134 insertions(+), 38 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index 0ef52bc..cf0dbb6 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -12,6 +12,9 @@ import https from 'https' let mainWindow: BrowserWindow | null = null let splashWindow: BrowserWindow | null = null +const MIN_SPLASH_VISIBLE_MS = 2000 +let splashVisibleStartedAt = 0 +let splashCloseTimer: NodeJS.Timeout | null = null const terminalProcesses = new Map() const terminalStopTimers = new Map() const intentionalTerminalStops = new Set() @@ -3176,8 +3179,16 @@ function createSplashWindow(): void { } }) - splashWindow.once('ready-to-show', () => splashWindow?.show()) + splashVisibleStartedAt = Date.now() + splashWindow.once('ready-to-show', () => { + splashVisibleStartedAt = Date.now() + splashWindow?.show() + }) splashWindow.on('closed', () => { + if (splashCloseTimer) { + clearTimeout(splashCloseTimer) + splashCloseTimer = null + } splashWindow = null }) splashWindow.loadFile(join(__dirname, '../renderer/splash.html')).catch((error) => { @@ -3194,6 +3205,18 @@ function updateSplashStatus(message: string): void { } function closeSplashAndShowMain(): void { + if (splashCloseTimer) return + const elapsed = splashVisibleStartedAt ? Date.now() - splashVisibleStartedAt : MIN_SPLASH_VISIBLE_MS + const remaining = Math.max(0, MIN_SPLASH_VISIBLE_MS - elapsed) + + if (remaining > 0) { + splashCloseTimer = setTimeout(() => { + splashCloseTimer = null + closeSplashAndShowMain() + }, remaining) + return + } + if (mainWindow && !mainWindow.isDestroyed() && !mainWindow.isVisible()) { splashWindow?.close() splashWindow = null diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 956edb0..c4cd1a1 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -78,6 +78,8 @@ import { GitFileContextMenu, PowerActionModal, SourceSettingsSaveConfirmModal, S type IdeActivity = WorkspaceActivity +const PUBLIC_GUIDE_COURSE_SLUG = 'autonomy-studio-lms-guide' + function makeStudentSlug(email?: string | null): string { return ((email ?? '').split('@')[0] || 'student').replace(/[^a-zA-Z0-9_-]+/g, '-').toLowerCase() } @@ -107,6 +109,7 @@ const EMPTY_WEEK: CurriculumWeek = { github: '-', detail: { overview: '', + learningSteps: [], topicDetails: [], topicVideoUrls: [], references: [], @@ -376,6 +379,8 @@ function LocalPcWorkspaceMockup({ courseKey, curriculumWeek, activeLearningStep, + allowFutureLearningSteps, + onLearningStepSelect, onSubmissionRecorded }: { onClose: () => void @@ -400,6 +405,8 @@ function LocalPcWorkspaceMockup({ courseKey: string curriculumWeek: CurriculumWeek activeLearningStep: string | null + allowFutureLearningSteps: boolean + onLearningStepSelect?: (step: string) => void onSubmissionRecorded: () => void }): React.JSX.Element { const [powerActionModalOpen, setPowerActionModalOpen] = useState(false) @@ -571,10 +578,19 @@ function LocalPcWorkspaceMockup({ const timer = window.setInterval(() => setControlCountdownNow(Date.now()), 1000) return () => window.clearInterval(timer) }, [controlEndsAt]) - const materialItems = curriculumWeek.topics.map((topic, index) => ({ - title: topic, - description: curriculumWeek.detail.topicDetails[index] ?? '', - videoUrl: curriculumWeek.detail.topicVideoUrls?.[index] ?? '' + const materialItems = (curriculumWeek.detail.learningSteps?.length + ? curriculumWeek.detail.learningSteps + : curriculumWeek.topics.map((topic, index) => ({ + title: topic, + description: curriculumWeek.detail.topicDetails[index] ?? '', + videoUrl: curriculumWeek.detail.topicVideoUrls?.[index] ?? '', + concepts: [] as string[], + })) + ).map((step, index) => ({ + title: step.title, + description: step.description, + videoUrl: step.videoEnabled === false ? '' : (step.videoUrl ?? ''), + concepts: step.concepts?.length ? step.concepts : [curriculumWeek.topics[index]].filter(Boolean), })) const activeTopicIndex = curriculumWeek.topics.findIndex((topic) => topic === activeLearningStep) const activeMaterialIndex = Math.min( @@ -585,7 +601,11 @@ function LocalPcWorkspaceMockup({ setExpandedMaterialIndexes([activeMaterialIndex]) }, [activeLearningStep, activeMaterialIndex, curriculumWeek.week]) const toggleMaterialItem = (index: number): void => { - if (index > activeMaterialIndex) return + if (!allowFutureLearningSteps && index > activeMaterialIndex) return + if (allowFutureLearningSteps) { + const nextStep = materialItems[index]?.title + if (nextStep) onLearningStepSelect?.(nextStep) + } setExpandedMaterialIndexes((indexes) => ( indexes.includes(index) ? indexes.filter((item) => item !== index) @@ -849,6 +869,7 @@ function LocalPcWorkspaceMockup({ } } const recordLearningCompletion = async (commitCount: number, completedWeeks: number): Promise => { + if (allowFutureLearningSteps) return if (!accessToken) return const requestAccessToken = await getFreshAccessToken() const recordResponse = await fetch(`${LMS_API_BASE_URL}/studio/submissions/record`, { @@ -869,6 +890,7 @@ function LocalPcWorkspaceMockup({ } useEffect(() => { + if (allowFutureLearningSteps) return if (!accessToken || !courseKey || weekNumber < 1) return const sessionKey = `autonomy-studio.lms.learning-session:${courseKey}:week-${weekNumber}:${platformId}` @@ -920,7 +942,7 @@ function LocalPcWorkspaceMockup({ cancelled = true window.clearInterval(intervalId) } - }, [accessToken, courseKey, getFreshAccessToken, platformId, weekNumber]) + }, [accessToken, allowFutureLearningSteps, courseKey, getFreshAccessToken, platformId, weekNumber]) const submitAllModuleChanges = async (): Promise => { if (gitSubmitting) return @@ -2219,6 +2241,7 @@ function LocalPcWorkspaceMockup({ materialItems={materialItems} activeMaterialIndex={activeMaterialIndex} expandedMaterialIndexes={expandedMaterialIndexes} + allowFutureMaterialSteps={allowFutureLearningSteps} topics={curriculumWeek.topics} overview={curriculumWeek.detail.overview} topicDetails={curriculumWeek.detail.topicDetails} @@ -2662,18 +2685,6 @@ function LocalPcWorkspaceMockup({ ) } -function BootLoadingOverlay({ message }: { message: string }): React.JSX.Element { - return ( -
-
-
-

Autonomy Studio LMS

-

{message}

-
-
- ) -} - export default function App(): React.JSX.Element { const restoredStudioSessionRef = useRef(readStudioRuntimeSessionState()) const previousWeekStateRef = useRef>({}) @@ -2762,6 +2773,7 @@ export default function App(): React.JSX.Element { const selectedWeek = selectedWeekState ?? EMPTY_WEEK const workspaceWeek = workspaceWeekState ?? selectedWeekState ?? EMPTY_WEEK const selectedCourse = studentCourses.find((course) => course.id === selectedCourseId) ?? null + const publicGuideCourseActive = selectedCourse?.slug === PUBLIC_GUIDE_COURSE_SLUG const studioDisabled = !selectedWeekState const connectedEnvironmentProfile = connectedPlatform ? ( @@ -2777,6 +2789,13 @@ export default function App(): React.JSX.Element { && Boolean(authSession?.user.id) && session?.holderUserId === authSession?.user.id } + useEffect(() => { + if (!publicGuideCourseActive) return + const firstStep = selectedWeekState?.topics[0] + if (!firstStep) return + if (activeLearningStep && selectedWeekState.topics.includes(activeLearningStep)) return + setActiveLearningStep(firstStep) + }, [activeLearningStep, publicGuideCourseActive, selectedWeekState]) const curriculumSummary = curriculumWeeks.length ? `${curriculumWeeks.length}개 섹션` : '등록된 섹션 없음' const showTitlebarNotice = (title: string, tone: 'info' | 'warning' | 'error' = 'info', addToList = false): void => { setTitlebarNotices((notices) => [ @@ -3326,6 +3345,7 @@ export default function App(): React.JSX.Element { ? 'absolute inset-0' : ['fixed bottom-0 right-0 top-[38px]', sidebarCollapsed ? 'left-[68px]' : 'left-[240px]'].join(' ') ].join(' ') + const fullAppModalOverlayClass = 'fixed bottom-0 left-0 right-0 top-[38px] z-[80] grid place-items-center bg-black/36' const canReleaseConnectedPlatform = connectedPlatform?.control?.status === 'ACTIVE' const connectedPlatformControlEndsAt = canReleaseConnectedPlatform ? connectedPlatform?.control?.session?.endsAt ?? null @@ -4042,8 +4062,10 @@ export default function App(): React.JSX.Element { studentEmail={authSession?.user.email ?? 'student'} weekNumber={workspaceWeek.week} courseKey={courseKey} - curriculumWeek={workspaceWeek} + curriculumWeek={workspaceWeek} activeLearningStep={activeLearningStep} + allowFutureLearningSteps={publicGuideCourseActive} + onLearningStepSelect={publicGuideCourseActive ? setActiveLearningStep : undefined} onSubmissionRecorded={() => { setCurriculumRefreshSignal((current) => current + 1) void loadStudentCourses({ autoSelectSingle: false }) @@ -4069,8 +4091,10 @@ export default function App(): React.JSX.Element { activeLearningStep={activeLearningStep} studioDisabled={studioDisabled} hasMaterials={hasSelectedWeekMaterials} + publicGuideCourse={publicGuideCourseActive} learningStats={learningStats} onSelectWeek={setSelectedWeekState} + onSelectLearningStep={publicGuideCourseActive ? setActiveLearningStep : undefined} onStartStudio={requestStudioStart} onOpenMaterials={() => { if (hasSelectedWeekMaterials) setMaterialModalOpen(true) @@ -4228,7 +4252,7 @@ export default function App(): React.JSX.Element { )} {sessionConfirmAction && ( - {bootLoadingVisible && } + {bootLoadingVisible && ( + + )} ) } diff --git a/src/renderer/src/components/ChatWidgets.tsx b/src/renderer/src/components/ChatWidgets.tsx index 4ceb8b4..4398d0e 100644 --- a/src/renderer/src/components/ChatWidgets.tsx +++ b/src/renderer/src/components/ChatWidgets.tsx @@ -144,7 +144,9 @@ export function ExpandableMarkdownMessage({ }, [content, defaultCollapsed]) useEffect(() => { - if (collapseOnSignal) setExpanded(false) + if (collapseOnSignal) { + setExpanded(false) + } }, [collapseOnSignal, collapseSignal]) return ( @@ -195,7 +197,9 @@ export function ExpandablePlainMessage({ }, [content]) useEffect(() => { - if (collapseOnSignal) setExpanded(false) + if (collapseOnSignal) { + setExpanded(false) + } }, [collapseOnSignal, collapseSignal]) return ( diff --git a/src/renderer/src/features/curriculum/CurriculumDetailPanel.tsx b/src/renderer/src/features/curriculum/CurriculumDetailPanel.tsx index 47367c6..d36d176 100644 --- a/src/renderer/src/features/curriculum/CurriculumDetailPanel.tsx +++ b/src/renderer/src/features/curriculum/CurriculumDetailPanel.tsx @@ -20,12 +20,16 @@ type CurriculumDetailPanelProps = { week: CurriculumDetailWeek selected: boolean activeLearningStep: string | null + allowLearningStepSelection?: boolean + onSelectLearningStep?: (step: string) => void } export default function CurriculumDetailPanel({ week, selected, - activeLearningStep + activeLearningStep, + allowLearningStepSelection = false, + onSelectLearningStep }: CurriculumDetailPanelProps): React.JSX.Element { return (
@@ -42,18 +46,28 @@ export default function CurriculumDetailPanel({
{week.topics.map((topic, index) => { const active = topic === activeLearningStep + const selectable = allowLearningStepSelection && Boolean(onSelectLearningStep) return (
onSelectLearningStep?.(topic) : undefined} + onKeyDown={selectable ? (event) => { + if (event.key !== 'Enter' && event.key !== ' ') return + event.preventDefault() + onSelectLearningStep?.(topic) + } : undefined} className={[ - 'rounded-lg border p-4 shadow-[0_10px_22px_rgba(20,27,44,0.04)]', + 'rounded-lg border p-4 shadow-[0_10px_22px_rgba(20,27,44,0.04)] transition', + selectable ? 'cursor-pointer hover:border-[#b7a8ff] hover:bg-[#fbf9ff]' : '', active ? 'border-[#714ef6] bg-[#f7f3ff]' : 'border-[#dde2ec] bg-white' ].join(' ')} >
{index + 1}

{topic}

- {active ? 현재 : null} + {active ? 진행중 : null}

{week.detail.topicDetails[index]} diff --git a/src/renderer/src/features/curriculum/CurriculumHomeScreen.tsx b/src/renderer/src/features/curriculum/CurriculumHomeScreen.tsx index 3c1788f..5466d7e 100644 --- a/src/renderer/src/features/curriculum/CurriculumHomeScreen.tsx +++ b/src/renderer/src/features/curriculum/CurriculumHomeScreen.tsx @@ -33,8 +33,10 @@ type CurriculumHomeScreenProps = { activeLearningStep: string | null studioDisabled: boolean hasMaterials: boolean + publicGuideCourse: boolean learningStats: LearningStatItem[] onSelectWeek: (week: Week) => void + onSelectLearningStep?: (step: string) => void onStartStudio: () => void onOpenMaterials: () => void } @@ -47,8 +49,10 @@ export default function CurriculumHomeScreen({ activeLearningStep, studioDisabled, hasMaterials, + publicGuideCourse, learningStats, onSelectWeek, + onSelectLearningStep, onStartStudio, onOpenMaterials }: CurriculumHomeScreenProps): React.JSX.Element { @@ -91,6 +95,8 @@ export default function CurriculumHomeScreen({ week={selectedWeek} selected={selected} activeLearningStep={activeLearningStep} + allowLearningStepSelection={publicGuideCourse} + onSelectLearningStep={onSelectLearningStep} />

@@ -103,7 +109,7 @@ export default function CurriculumHomeScreen({ onStartStudio={onStartStudio} onOpenMaterials={onOpenMaterials} /> - +
) diff --git a/src/renderer/src/features/curriculum/LearningStatsCard.tsx b/src/renderer/src/features/curriculum/LearningStatsCard.tsx index add664f..168fa3a 100644 --- a/src/renderer/src/features/curriculum/LearningStatsCard.tsx +++ b/src/renderer/src/features/curriculum/LearningStatsCard.tsx @@ -17,21 +17,28 @@ const STAT_ICONS: Record = { } type LearningStatsCardProps = { + guideMode?: boolean stats: LearningStatItem[] } -export default function LearningStatsCard({ stats }: LearningStatsCardProps): React.JSX.Element { +export default function LearningStatsCard({ guideMode = false, stats }: LearningStatsCardProps): React.JSX.Element { return (
- 내 학습 현황 - 전체 진행 상황 및 통계 + {guideMode ? '체험 강좌' : '내 학습 현황'} + + {guideMode ? '배정 전 사용법 안내 과정' : '전체 진행 상황 및 통계'} +
- {stats.length ? ( + {guideMode ? ( +
+ 이 강좌는 Autonomy Studio LMS 사용법을 확인하기 위한 공통 안내입니다. 실제 학습 시간, 커밋 수, 제출 현황은 강사가 배정한 강좌에서부터 집계됩니다. +
+ ) : stats.length ? ( stats.map(({ value, label, iconKey }, index) => { const Icon = STAT_ICONS[iconKey] ?? Gauge return ( diff --git a/src/renderer/src/features/curriculum/types.ts b/src/renderer/src/features/curriculum/types.ts index 24012c3..f8ceff7 100644 --- a/src/renderer/src/features/curriculum/types.ts +++ b/src/renderer/src/features/curriculum/types.ts @@ -23,6 +23,14 @@ export type DownloadResource = { export type LearningStat = LearningStatItem +export type LearningStep = { + title: string + description: string + videoEnabled?: boolean + videoUrl?: string + concepts?: string[] +} + export type CurriculumWeek = { week: number title: string @@ -34,6 +42,7 @@ export type CurriculumWeek = { github: string detail: { overview: string + learningSteps?: LearningStep[] topicDetails: string[] topicVideoUrls?: string[] practiceContents?: Array<{ diff --git a/src/renderer/src/features/workspace/WorkspaceGuidePanel.tsx b/src/renderer/src/features/workspace/WorkspaceGuidePanel.tsx index e5a3a1f..3133dcc 100644 --- a/src/renderer/src/features/workspace/WorkspaceGuidePanel.tsx +++ b/src/renderer/src/features/workspace/WorkspaceGuidePanel.tsx @@ -7,6 +7,7 @@ export type WorkspaceGuideMaterialItem = { title: string description: string videoUrl: string + concepts?: string[] } type WorkspaceGuideResource = { @@ -29,6 +30,7 @@ type WorkspaceGuidePanelProps = { materialItems: WorkspaceGuideMaterialItem[] activeMaterialIndex: number expandedMaterialIndexes: number[] + allowFutureMaterialSteps?: boolean topics: string[] overview: string topicDetails: string[] @@ -81,6 +83,7 @@ export default function WorkspaceGuidePanel {index + 1}
- {item.title} + {item.title} {active && ( @@ -232,10 +235,10 @@ export default function WorkspaceGuidePanel 핵심 개념
- {topics.map((topic) => ( -
+ {(item.concepts?.length ? item.concepts : [topics[index]].filter(Boolean)).map((concept) => ( +
- {topic} + {concept}
))}