diff --git a/package-lock.json b/package-lock.json
index 9a92e01..089b86d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "lms",
- "version": "1.2.26",
+ "version": "1.2.28",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "lms",
- "version": "1.2.26",
+ "version": "1.2.28",
"dependencies": {
"@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/utils": "^4.0.0",
diff --git a/package.json b/package.json
index 195dea5..cf12625 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "lms",
- "version": "1.2.26",
+ "version": "1.2.28",
"description": "Autonomy Studio LMS Electron app",
"main": "./out/main/index.js",
"author": {
diff --git a/scripts/test-lecture-viewer.cjs b/scripts/test-lecture-viewer.cjs
index 81d8ba9..efe9add 100644
--- a/scripts/test-lecture-viewer.cjs
+++ b/scripts/test-lecture-viewer.cjs
@@ -31,3 +31,13 @@ test('lecture controls have English translations', () => {
const en = require('../src/renderer/src/i18n/en.json');
for (const key of ['슬라이드·퀴즈 열기', '강의 닫기', '슬라이드', '퀴즈', '답안 제출', '다시 풀기']) assert.ok(en[key], key);
});
+
+test('presentation slides render key messages, examples and legacy body as safe text', () => {
+ const source = fs.readFileSync(require.resolve('../src/renderer/src/features/curriculum/LectureSlideCanvas.tsx'), 'utf8');
+ const exports = {};
+ const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, jsx: ts.JsxEmit.ReactJSX, esModuleInterop: true } }).outputText;
+ vm.runInNewContext(output, { exports, require: name => name === '../../i18n' ? { t: value => value } : require(name) });
+ const html = renderToStaticMarkup(React.createElement(exports.default, { slide: { id: 's1', type: 'content', title: '속도', subtitle: '핵심 메시지', body: '기존 본문', bullets: ['요점'], example: { label: '실습 예시', content: '
' } }, title: '제어 강의', index: 0, total: 4 }));
+ assert.match(html, /핵심 메시지/); assert.match(html, /기존 본문/); assert.match(html, /실습 예시/);
+ assert.doesNotMatch(html, /
+
+
+
+ {title}
+
+
+ {String(index + 1).padStart(2, "0")} /{" "}
+ {String(total).padStart(2, "0")}
+
+
+
+
+ {slide.title}
+
+ {slide.subtitle && (
+
+ {slide.subtitle}
+
+ )}
+ {slide.body && (
+
+ {slide.body}
+
+ )}
+ {slide.bullets.length > 0 && (
+
+ {slide.bullets.map((bullet, i) => (
+ -
+
+
+ {bullet}
+
+
+ ))}
+
+ )}
+ {slide.example?.content && (
+
+ )}
+ {slide.code && (
+
+
+
+ {slide.language || t("코드")}
+
+
+ {slide.code}
+
+
+ )}
+
+
+ );
+}
diff --git a/src/renderer/src/features/curriculum/LectureViewerModal.tsx b/src/renderer/src/features/curriculum/LectureViewerModal.tsx
index edffda7..c502254 100644
--- a/src/renderer/src/features/curriculum/LectureViewerModal.tsx
+++ b/src/renderer/src/features/curriculum/LectureViewerModal.tsx
@@ -9,26 +9,22 @@ import {
RotateCcw,
Send,
X,
+ Maximize2,
+ Minimize2,
} from "lucide-react";
+import LectureSlideCanvas, {
+ type LectureSlideData,
+} from "./LectureSlideCanvas";
import { t, useLocaleRender } from "../../i18n";
import { LMS_API_BASE_URL } from "../../config/appConfig";
-type Slide = {
- id: string;
- type: string;
- title: string;
- body: string;
- bullets: string[];
- code?: string;
- language?: string;
-};
type Question = { id: string; question: string; options: string[] };
type Lecture = {
id: string;
version: number;
title: string;
objectives: string[];
- slides: Slide[];
+ slides: LectureSlideData[];
quiz: Question[];
progress: { lastSlide: number; completedAt: string | null } | null;
};
@@ -68,6 +64,9 @@ export default function LectureViewerModal({
const [grade, setGrade] = useState(null);
const [error, setError] = useState(null);
const [submitting, setSubmitting] = useState(false);
+ const [expanded, setExpanded] = useState(false);
+ const navigation = useRef({ count: 0, tab: "slides" });
+ navigation.current = { count: lecture?.slides.length ?? 0, tab };
const requestId = useRef(null);
const controller = useRef(new AbortController());
const closeRef = useRef(onClose);
@@ -127,11 +126,48 @@ export default function LectureViewerModal({
);
});
const keydown = (event: KeyboardEvent) => {
- if (event.key === 'Tab') {
- const dialog = document.getElementById('lecture-viewer-title')?.closest('[role="dialog"]');
- const nodes = Array.from(dialog?.querySelectorAll('button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex="0"]') ?? []).filter(e => e.getClientRects().length);
- const first = nodes[0], last = nodes.at(-1);
- if (first && last && ((event.shiftKey && document.activeElement === first) || (!event.shiftKey && document.activeElement === last))) { event.preventDefault(); (event.shiftKey ? last : first).focus(); }
+ const editable =
+ event.target instanceof HTMLElement &&
+ Boolean(
+ event.target.closest(
+ 'input, textarea, select, [contenteditable="true"]',
+ ),
+ );
+ if (
+ !editable &&
+ navigation.current.tab === "slides" &&
+ ["ArrowRight", "ArrowLeft", "PageDown", "PageUp"].includes(event.key)
+ ) {
+ event.preventDefault();
+ const direction =
+ event.key === "ArrowRight" || event.key === "PageDown" ? 1 : -1;
+ setSlideIndex((current) =>
+ Math.min(
+ Math.max(0, current + direction),
+ Math.max(0, navigation.current.count - 1),
+ ),
+ );
+ }
+ if (event.key === "Tab") {
+ const dialog = document
+ .getElementById("lecture-viewer-title")
+ ?.closest('[role="dialog"]');
+ const nodes = Array.from(
+ dialog?.querySelectorAll(
+ 'button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex="0"]',
+ ) ?? [],
+ ).filter((e) => e.getClientRects().length);
+ const first = nodes[0],
+ last = nodes.at(-1);
+ if (
+ first &&
+ last &&
+ ((event.shiftKey && document.activeElement === first) ||
+ (!event.shiftKey && document.activeElement === last))
+ ) {
+ event.preventDefault();
+ (event.shiftKey ? last : first).focus();
+ }
}
if (event.key === "Escape") {
event.stopPropagation();
@@ -193,7 +229,7 @@ export default function LectureViewerModal({
aria-labelledby="lecture-viewer-title"
>
{lecture && (
)}
-
+
{error && (
{error}
@@ -266,38 +313,41 @@ export default function LectureViewerModal({
)}
{lecture && tab === "slides" && slide && (
-
-
- {slideIndex + 1} / {lecture.slides.length}
-
-
- {slide.title}
-
-
+
-
- {slide.bullets.map((b, i) => (
- -
- {b}
-
+ {lecture.slides.map((item, index) => (
+
))}
-
- {slide.code && (
-
- {slide.code}
-
- )}
-
+
+
+
)}
{lecture && tab === "quiz" && (
{grade && (
-
+
{grade.score} / {grade.total}
)}
@@ -318,7 +368,7 @@ export default function LectureViewerModal({
{q.options.map((option, i) => (