From 1bafe2f82b2d3b2fb879e98b0d4ab86810fe7590 Mon Sep 17 00:00:00 2001 From: Haemin Kim Date: Wed, 27 Aug 2025 17:46:01 +0900 Subject: [PATCH 01/43] =?UTF-8?q?=E2=9C=A8=20(#302)=20=EB=9D=BC=EC=9A=B0?= =?UTF-8?q?=ED=84=B0=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/teacher/lecture-live/[lectureId]/page.module.scss | 0 frontend/app/teacher/lecture-live/[lectureId]/page.tsx | 0 frontend/config/teacherRouteConfig.ts | 8 +++++++- frontend/constants/routes.ts | 2 ++ frontend/middleware.ts | 1 + 5 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 frontend/app/teacher/lecture-live/[lectureId]/page.module.scss create mode 100644 frontend/app/teacher/lecture-live/[lectureId]/page.tsx diff --git a/frontend/app/teacher/lecture-live/[lectureId]/page.module.scss b/frontend/app/teacher/lecture-live/[lectureId]/page.module.scss new file mode 100644 index 00000000..e69de29b diff --git a/frontend/app/teacher/lecture-live/[lectureId]/page.tsx b/frontend/app/teacher/lecture-live/[lectureId]/page.tsx new file mode 100644 index 00000000..e69de29b diff --git a/frontend/config/teacherRouteConfig.ts b/frontend/config/teacherRouteConfig.ts index 7892e907..153e4f41 100644 --- a/frontend/config/teacherRouteConfig.ts +++ b/frontend/config/teacherRouteConfig.ts @@ -22,7 +22,8 @@ export const TEACHER_ROUTE_CONFIG: Record< | "teacherLectureDetail" | "teacherSetting" | "teacherStudentManagement" - >, + | "teacherLectureLive" + >, RouteConfig > = { teacherHome: { @@ -55,6 +56,11 @@ export const TEACHER_ROUTE_CONFIG: Record< headerType: TeacherHeaderType.NONE, sidebarType: SiderbarType.NONE, }, + teacherLectureLive: { + path: ROUTES.teacherLectureLive, + headerType: TeacherHeaderType.NONE, + sidebarType: SiderbarType.NONE, + }, teacherSetting: { path: ROUTES.teacherSetting, headerType: TeacherHeaderType.NONE, diff --git a/frontend/constants/routes.ts b/frontend/constants/routes.ts index a4d47ca6..976746f2 100644 --- a/frontend/constants/routes.ts +++ b/frontend/constants/routes.ts @@ -24,6 +24,8 @@ export const ROUTES = { `/teacher/quiz-dashboard/${lectureId}`, // 강사 퀴즈 대시보드 teacherLectureDetail: (lectureId: string) => `/teacher/lecture-detail/${lectureId}`, // 강사 강의 상세 + teacherLectureLive: (lectureId: string) => + `/teacher/lecture-live/${lectureId}`, // 강사 실시간 강의 teacherSetting: "/teacher/setting", // 강사 설정 teacherStudentManagement: "/teacher/student-management", // 강사 학생 관리 }; diff --git a/frontend/middleware.ts b/frontend/middleware.ts index aa489896..472e22a1 100644 --- a/frontend/middleware.ts +++ b/frontend/middleware.ts @@ -26,6 +26,7 @@ const TEACHER_PATHS = [ ROUTES.teacherLectureNoteManagement, "/teacher/quiz-dashboard", "/teacher/lecture-detail", + "/teacher/lecture-live", ROUTES.teacherSetting, ROUTES.teacherStudentManagement, ]; From 14d8418a299fd0a7001c7b2c8eb4b0e2d940fb9e Mon Sep 17 00:00:00 2001 From: Haemin Kim Date: Thu, 28 Aug 2025 11:28:09 +0900 Subject: [PATCH 02/43] =?UTF-8?q?=E2=9C=A8=20(#302)=20FitContentButton=20P?= =?UTF-8?q?rops=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/Button/FitContentButton/FitContentButton.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/components/Button/FitContentButton/FitContentButton.tsx b/frontend/components/Button/FitContentButton/FitContentButton.tsx index 83a55444..90686871 100644 --- a/frontend/components/Button/FitContentButton/FitContentButton.tsx +++ b/frontend/components/Button/FitContentButton/FitContentButton.tsx @@ -8,6 +8,7 @@ interface FitContentButtonProps { onClick: () => void; disabled?: boolean; type?: "button" | "submit" | "reset"; + className?: string; } const FitContentButton: React.FC = ({ @@ -15,10 +16,11 @@ const FitContentButton: React.FC = ({ onClick, disabled = false, type = "button", + className, }) => { return ( + ))} + + + + ); + } + + if (type === "pptx") { + return ( +
+
+ {Array.from({ length: pptxCount }, (_, i) => ( + + ))} +
+
+ ); + } + + return null; +} \ No newline at end of file From 6715e0047f3111f318ee81fc58567f2c378b5546 Mon Sep 17 00:00:00 2001 From: Haemin Kim Date: Wed, 3 Sep 2025 10:41:05 +0900 Subject: [PATCH 06/43] =?UTF-8?q?=E2=9C=A8=20(#302)=20=EA=B0=95=EC=9D=98?= =?UTF-8?q?=EC=9E=90=EB=A3=8C=20viewer=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DocumentViewer/DocumentViewer.module.scss | 103 ++++++++++++++++++ .../DocumentViewer/DocumentViewer.tsx | 96 ++++++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 frontend/app/teacher/lecture-live/[lectureId]/_components/LectureNote/DocumentViewer/DocumentViewer.module.scss create mode 100644 frontend/app/teacher/lecture-live/[lectureId]/_components/LectureNote/DocumentViewer/DocumentViewer.tsx diff --git a/frontend/app/teacher/lecture-live/[lectureId]/_components/LectureNote/DocumentViewer/DocumentViewer.module.scss b/frontend/app/teacher/lecture-live/[lectureId]/_components/LectureNote/DocumentViewer/DocumentViewer.module.scss new file mode 100644 index 00000000..2164eb2f --- /dev/null +++ b/frontend/app/teacher/lecture-live/[lectureId]/_components/LectureNote/DocumentViewer/DocumentViewer.module.scss @@ -0,0 +1,103 @@ +@import "@/styles/variables"; + + +.viewer { + height: 100%; + display: grid; + grid-template-rows: 1fr auto; + gap: $spacing-sm; + min-height: 0; +} + + +.stage { + height: 100%; + min-height: 0; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + padding: 1px; +} + +.info { + font-size: $font-size-sm; + color: $color-neutral-6; +} + +.nav { + display: flex; + gap: $spacing-sm; + + button { + padding: 2px 8px; + border: 1px solid $color-neutral-7; + background: $color-white; + border-radius: 8px; + cursor: pointer; + font-size: $font-size-sm; + + &:disabled { opacity: .5; cursor: not-allowed; } + } +} + + +.pageMock { + width: 100%; + max-width: 100%; + max-height: 100%; + height: auto; + aspect-ratio: 16 / 9; + + background: #f3f6fa; + border: 1px solid $color-neutral-7; + + + display: grid; + place-items: center; +} + +.pageLabel { + color: $color-neutral-6; + font-weight: $font-weight-medium; +} + + +.nav { + display: flex; + gap: $spacing-xs; + + button { + width: 28px; + height: 28px; + display: inline-flex; + align-items: center; + justify-content: center; + + border: none; + background: transparent; + + + border-radius: 8px; + cursor: pointer; + padding: 0; + + &:hover:not(:disabled) { background: $color-neutral-7; } + &:disabled { opacity: .5; cursor: not-allowed; } + } +} + +.navIcon { + width: 18px; + height: 18px; +} + +.pptxFrame { + width: 100%; + height: 100%; + border: 0; +} + +.stage :global(canvas) { + display: block; +} \ No newline at end of file diff --git a/frontend/app/teacher/lecture-live/[lectureId]/_components/LectureNote/DocumentViewer/DocumentViewer.tsx b/frontend/app/teacher/lecture-live/[lectureId]/_components/LectureNote/DocumentViewer/DocumentViewer.tsx new file mode 100644 index 00000000..91de62ca --- /dev/null +++ b/frontend/app/teacher/lecture-live/[lectureId]/_components/LectureNote/DocumentViewer/DocumentViewer.tsx @@ -0,0 +1,96 @@ +"use client"; + +import styles from "./DocumentViewer.module.scss"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { Document, Page, pdfjs } from "react-pdf"; +import { getDocType, toAbsoluteUrl } from "@/types/lectures/documentUtilTypes"; + +export default function DocumentViewer({ + fileUrl, + currentPage, + onChangePage, + onLoad, +}: { + fileUrl: string; + currentPage: number; + onChangePage: (i: number) => void; + onLoad?: (numPages: number) => void; +}) { + const type = useMemo(() => getDocType(fileUrl), [fileUrl]); + + useEffect(() => { + if (type !== "pdf") return; + pdfjs.GlobalWorkerOptions.workerSrc = + `https://unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`; + }, [type]); + + const stageRef = useRef(null); + const [numPages, setNumPages] = useState(1); + const [stageW, setStageW] = useState(0); + const [stageH, setStageH] = useState(0); + const [naturalW, setNaturalW] = useState(1); + const [naturalH, setNaturalH] = useState(1); + + useEffect(() => { + if (type !== "pdf" || !stageRef.current) return; + const el = stageRef.current; + const measure = () => { + const cs = getComputedStyle(el); + const padX = parseFloat(cs.paddingLeft) + parseFloat(cs.paddingRight); + const padY = parseFloat(cs.paddingTop) + parseFloat(cs.paddingBottom); + setStageW(el.clientWidth - padX); + setStageH(el.clientHeight - padY); + }; + measure(); + const ro = new ResizeObserver(measure); + ro.observe(el); + return () => ro.disconnect(); + }, [type]); + + const SAFE_PAD = 2; + const scale = + type === "pdf" && naturalW && naturalH + ? Math.min((stageW - SAFE_PAD) / naturalW, (stageH - SAFE_PAD) / naturalH) + : 1; + + const handleDocLoad = ({ numPages }: { numPages: number }) => { + if (type !== "pdf") return; + setNumPages(numPages); + onLoad?.(numPages); + if (currentPage > numPages - 1) onChangePage(numPages - 1); + }; + const handlePageLoad = (page: any) => { + if (type !== "pdf") return; + const vp = page.getViewport({ scale: 1 }); + setNaturalW(vp.width); + setNaturalH(vp.height); + }; + + return ( +
+
+ {type === "pdf" && ( + + + + )} + + {type === "pptx" && ( +