From 247f5347af62095401a098c89a59d8f17e2fc120 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sat, 6 Sep 2025 09:20:34 +0900 Subject: [PATCH 01/32] =?UTF-8?q?=E2=9C=A8=20(#305)=20=EA=B0=95=EC=9D=98?= =?UTF-8?q?=20=EC=83=81=EC=84=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=B0=8F?= =?UTF-8?q?=20=EC=A0=95=EB=B3=B4=20=EC=84=B9=EC=85=98=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80,=20=EC=8A=A4=ED=83=80?= =?UTF-8?q?=EC=9D=BC=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LectureInfoSection.module.scss | 50 ++++++++ .../LectureInfoSection/LectureInfoSection.tsx | 118 ++++++++++++++++++ .../[lectureId]/page.module.scss | 3 + .../lecture-detail/[lectureId]/page.tsx | 13 +- 4 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 frontend/app/student/lecture-detail/[lectureId]/_components/LectureInfoSection/LectureInfoSection.module.scss create mode 100644 frontend/app/student/lecture-detail/[lectureId]/_components/LectureInfoSection/LectureInfoSection.tsx diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/LectureInfoSection/LectureInfoSection.module.scss b/frontend/app/student/lecture-detail/[lectureId]/_components/LectureInfoSection/LectureInfoSection.module.scss new file mode 100644 index 00000000..b0005059 --- /dev/null +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/LectureInfoSection/LectureInfoSection.module.scss @@ -0,0 +1,50 @@ +.card { + background: white; + border-radius: $radius-md; + padding: $spacing-lg; + box-shadow: 0px 0px 16px rgba($color-mutedblue, 0.3); + margin-bottom: 16px; +} + +.header { + display: flex; + align-items: center; + gap: $spacing-xs; + margin-bottom: $spacing-sm; +} + +.lectureTitle { + font-size: $font-size-lg; + margin: 0; + font-weight: $font-weight-medium; +} + +.status { + font-size: $font-size-md; + color: $color-mutedblue; + font-weight: 400; +} + +.divider { + height: 1px; + background-color: $color-neutral-7; + margin: 12px 0; +} + +.infoRow { + display: flex; + justify-content: space-between; + align-items: center; +} + +.infoItem { + display: flex; + align-items: center; + gap: 6px; + color: $color-mutedblue; + font-size: $font-size-md; +} + +.icon { + color: $color-mutedblue; +} diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/LectureInfoSection/LectureInfoSection.tsx b/frontend/app/student/lecture-detail/[lectureId]/_components/LectureInfoSection/LectureInfoSection.tsx new file mode 100644 index 00000000..44282103 --- /dev/null +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/LectureInfoSection/LectureInfoSection.tsx @@ -0,0 +1,118 @@ +"use client"; +import React, { useEffect, useState } from "react"; +import styles from "./LectureInfoSection.module.scss"; +import { BookOpenText, Calendar, Clock } from "lucide-react"; +import { fetchLectureDetail } from "@/api/lectures/fetchLectureDetail"; +import { FetchLectureDetailResult } from "@/types/lectures/fetchLectureDetailTypes"; +import LoadingSpinner from "@/components/LoadingSpinner/LoadingSpinner"; +import NoDataView from "@/components/NoDataView/NoDataView"; + +interface LectureInfoSectionProps { + lectureId: string; +} + +export default function LectureInfoSection({ + lectureId, +}: LectureInfoSectionProps) { + const [lectureData, setLectureData] = + useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const loadLectureData = async () => { + try { + setLoading(true); + const response = await fetchLectureDetail(lectureId); + if (response.isSuccess && response.result) { + setLectureData(response.result); + } else { + setError("강의 정보를 불러올 수 없습니다."); + } + } catch { + setError("강의 정보를 불러오는 중 오류가 발생했습니다."); + } finally { + setLoading(false); + } + }; + + loadLectureData(); + }, [lectureId]); + const getStatusText = (status: string) => { + switch (status) { + case "beforeLecture": + return "강의 전"; + case "onLecture": + return "강의 중"; + case "makeQuiz": + case "checkDashboard": + return "강의 종료"; + default: + return "강의 중"; + } + }; + + const formatDate = (dateString: string, weekDay: string) => { + const date = new Date(dateString); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}.${month}.${day} (${weekDay})`; + }; + + const formatTime = (startTime: string, endTime: string) => { + const formatTimeString = (time: string) => { + const [hours, minutes] = time.split(":"); + const hour = parseInt(hours); + const ampm = hour >= 12 ? "PM" : "AM"; + const displayHour = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour; + return `${displayHour}:${minutes} ${ampm}`; + }; + + return `${formatTimeString(startTime)} - ${formatTimeString(endTime)}`; + }; + + if (loading) { + return ; + } + + if (error || !lectureData) { + return ( + + ); + } + + return ( +
+
+

+ {String(lectureData.session).padStart(2, "0")}.{" "} + {lectureData.lectureName} +

+ + {getStatusText(lectureData.status)} + +
+ +
+ +
+
+ + + {formatDate(lectureData.lectureDate, lectureData.weekDay)} + +
+ +
+ + {formatTime(lectureData.startTime, lectureData.endTime)} +
+
+
+ ); +} diff --git a/frontend/app/student/lecture-detail/[lectureId]/page.module.scss b/frontend/app/student/lecture-detail/[lectureId]/page.module.scss index e69de29b..19aecb08 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/page.module.scss +++ b/frontend/app/student/lecture-detail/[lectureId]/page.module.scss @@ -0,0 +1,3 @@ +.lectureDetailPage { + padding: $spacing-lg; +} diff --git a/frontend/app/student/lecture-detail/[lectureId]/page.tsx b/frontend/app/student/lecture-detail/[lectureId]/page.tsx index 1e417598..337d3f15 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/page.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/page.tsx @@ -1,3 +1,14 @@ +"use client"; +import { useParams } from "next/navigation"; +import LectureInfoSection from "./_components/LectureInfoSection/LectureInfoSection"; +import style from "./page.module.scss"; + export default function StudentLectureDetailPage() { - return
강의 상세
; + const params = useParams(); + const lectureId = params.lectureId as string; + return ( +
+ +
+ ); } From 051cf0c4d16b672d510b4b9a29f041722f869053 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sat, 6 Sep 2025 09:24:50 +0900 Subject: [PATCH 02/32] =?UTF-8?q?=E2=9C=A8=20(#305)=20=EA=B0=95=EC=9D=98?= =?UTF-8?q?=20=EC=A0=9C=EB=AA=A9=20=EC=83=81=ED=83=9C=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=20=EC=8A=A4=ED=86=A0=EC=96=B4=20=EC=B6=94=EA=B0=80=20=EB=B0=8F?= =?UTF-8?q?=20=EA=B0=95=EC=9D=98=20=EC=83=81=EC=84=B8=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EC=99=80=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83?= =?UTF-8?q?=EC=97=90=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/app/student/layout.tsx | 8 +++++++- .../LectureInfoSection/LectureInfoSection.tsx | 3 +++ frontend/store/useLectureTitleStore.ts | 13 +++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 frontend/store/useLectureTitleStore.ts diff --git a/frontend/app/student/layout.tsx b/frontend/app/student/layout.tsx index 79678209..7454b4c6 100644 --- a/frontend/app/student/layout.tsx +++ b/frontend/app/student/layout.tsx @@ -9,6 +9,7 @@ import BackWithTitleHeader from "@/components/Header/Student/BackWithTitleHeader import TitleHeader from "@/components/Header/Student/TitleHeader/TitleHeader"; import Navigation from "@/components/Navigation/Navigation"; +import { useLectureTitleStore } from "@/store/useLectureTitleStore"; export default function StudentLayout({ children, @@ -16,6 +17,7 @@ export default function StudentLayout({ children: React.ReactNode; }) { const pathname = usePathname(); + const { lectureTitle } = useLectureTitleStore(); // 현재 경로에 해당하는 라우트 설정 찾기 const currentRoute = Object.values(STUDENT_ROUTE_CONFIG).find((config) => { @@ -38,7 +40,11 @@ export default function StudentLayout({ case StudentHeaderType.TITLE: return ; case StudentHeaderType.BACK_WITH_TITLE: - return ; + return ( + + ); case StudentHeaderType.BACK_WITH_PROFILE: return ; default: diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/LectureInfoSection/LectureInfoSection.tsx b/frontend/app/student/lecture-detail/[lectureId]/_components/LectureInfoSection/LectureInfoSection.tsx index 44282103..39a6e90d 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/_components/LectureInfoSection/LectureInfoSection.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/LectureInfoSection/LectureInfoSection.tsx @@ -6,6 +6,7 @@ import { fetchLectureDetail } from "@/api/lectures/fetchLectureDetail"; import { FetchLectureDetailResult } from "@/types/lectures/fetchLectureDetailTypes"; import LoadingSpinner from "@/components/LoadingSpinner/LoadingSpinner"; import NoDataView from "@/components/NoDataView/NoDataView"; +import { useLectureTitleStore } from "@/store/useLectureTitleStore"; interface LectureInfoSectionProps { lectureId: string; @@ -18,6 +19,7 @@ export default function LectureInfoSection({ useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const { setLectureTitle } = useLectureTitleStore(); useEffect(() => { const loadLectureData = async () => { @@ -26,6 +28,7 @@ export default function LectureInfoSection({ const response = await fetchLectureDetail(lectureId); if (response.isSuccess && response.result) { setLectureData(response.result); + setLectureTitle(response.result.lectureName); } else { setError("강의 정보를 불러올 수 없습니다."); } diff --git a/frontend/store/useLectureTitleStore.ts b/frontend/store/useLectureTitleStore.ts new file mode 100644 index 00000000..439f3b0c --- /dev/null +++ b/frontend/store/useLectureTitleStore.ts @@ -0,0 +1,13 @@ +import { create } from "zustand"; + +interface LectureTitleStore { + lectureTitle: string; + setLectureTitle: (title: string) => void; + clearLectureTitle: () => void; +} + +export const useLectureTitleStore = create((set) => ({ + lectureTitle: "", + setLectureTitle: (title: string) => set({ lectureTitle: title }), + clearLectureTitle: () => set({ lectureTitle: "" }), +})); From 172575e58ea452ee87ba859899821e8f2aa7d6f6 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sat, 6 Sep 2025 09:29:19 +0900 Subject: [PATCH 03/32] =?UTF-8?q?=E2=9C=A8=20(#305)=20=EA=B0=95=EC=9D=98?= =?UTF-8?q?=20=EC=83=81=EC=84=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=20?= =?UTF-8?q?=ED=83=AD=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20=EB=B0=8F?= =?UTF-8?q?=20=ED=83=AD=EC=97=90=20=EB=94=B0=EB=9D=BC=20=EC=84=B9=EC=85=98?= =?UTF-8?q?=20=EB=A0=8C=EB=8D=94=EB=A7=81=20=EB=B6=84=EA=B8=B0=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LecrureRecordSection.tsx | 5 +++ .../LectureNoteListSection.tsx | 5 +++ .../QuestionListSection.tsx | 5 +++ .../_components/QuizSection/QuizSection.tsx | 5 +++ .../lecture-detail/[lectureId]/page.tsx | 32 +++++++++++++++++++ 5 files changed, 52 insertions(+) create mode 100644 frontend/app/student/lecture-detail/[lectureId]/_components/LecrureRecordSection/LecrureRecordSection.tsx create mode 100644 frontend/app/student/lecture-detail/[lectureId]/_components/LectureNoteListSection/LectureNoteListSection.tsx create mode 100644 frontend/app/student/lecture-detail/[lectureId]/_components/QuestionListSection/QuestionListSection.tsx create mode 100644 frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/LecrureRecordSection/LecrureRecordSection.tsx b/frontend/app/student/lecture-detail/[lectureId]/_components/LecrureRecordSection/LecrureRecordSection.tsx new file mode 100644 index 00000000..030a9642 --- /dev/null +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/LecrureRecordSection/LecrureRecordSection.tsx @@ -0,0 +1,5 @@ +import React from "react"; + +export default function LecrureRecordSection() { + return
LecrureRecordSection
; +} diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/LectureNoteListSection/LectureNoteListSection.tsx b/frontend/app/student/lecture-detail/[lectureId]/_components/LectureNoteListSection/LectureNoteListSection.tsx new file mode 100644 index 00000000..c9532749 --- /dev/null +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/LectureNoteListSection/LectureNoteListSection.tsx @@ -0,0 +1,5 @@ +import React from "react"; + +export default function LectureNoteListSection() { + return
LectureNoteListSection
; +} diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/QuestionListSection/QuestionListSection.tsx b/frontend/app/student/lecture-detail/[lectureId]/_components/QuestionListSection/QuestionListSection.tsx new file mode 100644 index 00000000..9c852db2 --- /dev/null +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/QuestionListSection/QuestionListSection.tsx @@ -0,0 +1,5 @@ +import React from "react"; + +export default function QuestionListSection() { + return
QuestionListSection
; +} diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx b/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx new file mode 100644 index 00000000..97d1a716 --- /dev/null +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx @@ -0,0 +1,5 @@ +import React from "react"; + +export default function QuizSection() { + return
QuizSection
; +} diff --git a/frontend/app/student/lecture-detail/[lectureId]/page.tsx b/frontend/app/student/lecture-detail/[lectureId]/page.tsx index 337d3f15..324e4d51 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/page.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/page.tsx @@ -1,14 +1,46 @@ "use client"; import { useParams } from "next/navigation"; +import { useState } from "react"; import LectureInfoSection from "./_components/LectureInfoSection/LectureInfoSection"; +import LectureNoteListSection from "./_components/LectureNoteListSection/LectureNoteListSection"; +import QuestionListSection from "./_components/QuestionListSection/QuestionListSection"; +import LecrureRecordSection from "./_components/LecrureRecordSection/LecrureRecordSection"; +import QuizSection from "./_components/QuizSection/QuizSection"; import style from "./page.module.scss"; +import Tab from "@/components/Tab/Tab"; export default function StudentLectureDetailPage() { const params = useParams(); const lectureId = params.lectureId as string; + const [selectedTab, setSelectedTab] = useState(0); + + const tabs = ["수업 자료", "질문하기", "강의 녹음", "복습 퀴즈"]; + + const handleTabSelect = (tabName: string) => { + const tabIndex = tabs.indexOf(tabName); + setSelectedTab(tabIndex); + }; + + const renderSection = () => { + switch (selectedTab) { + case 0: + return ; + case 1: + return ; + case 2: + return ; + case 3: + return ; + default: + return ; + } + }; + return (
+ + {renderSection()}
); } From 095265684e4be4385217419b135011f96e46f88d Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sat, 6 Sep 2025 09:35:12 +0900 Subject: [PATCH 04/32] =?UTF-8?q?=F0=9F=94=A8=20(#305)=20=ED=97=A4?= =?UTF-8?q?=EB=8D=94=EC=97=90=EC=84=9C=20lectureTitle=EC=9D=B4=20=EC=95=84?= =?UTF-8?q?=EB=8B=8C=20classTitle=20=EC=A0=80=EC=9E=A5=20=ED=9B=84=20?= =?UTF-8?q?=EB=B6=88=EB=9F=AC=EC=98=A4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/app/student/layout.tsx | 8 +++----- .../LectureInfoSection/LectureInfoSection.tsx | 3 --- .../app/student/lecture-detail/[lectureId]/page.tsx | 4 ++++ frontend/store/useClassTitleStore.ts | 13 +++++++++++++ frontend/store/useLectureTitleStore.ts | 13 ------------- 5 files changed, 20 insertions(+), 21 deletions(-) create mode 100644 frontend/store/useClassTitleStore.ts delete mode 100644 frontend/store/useLectureTitleStore.ts diff --git a/frontend/app/student/layout.tsx b/frontend/app/student/layout.tsx index 7454b4c6..603d518b 100644 --- a/frontend/app/student/layout.tsx +++ b/frontend/app/student/layout.tsx @@ -9,7 +9,7 @@ import BackWithTitleHeader from "@/components/Header/Student/BackWithTitleHeader import TitleHeader from "@/components/Header/Student/TitleHeader/TitleHeader"; import Navigation from "@/components/Navigation/Navigation"; -import { useLectureTitleStore } from "@/store/useLectureTitleStore"; +import { useClassTitleStore } from "@/store/useClassTitleStore"; export default function StudentLayout({ children, @@ -17,7 +17,7 @@ export default function StudentLayout({ children: React.ReactNode; }) { const pathname = usePathname(); - const { lectureTitle } = useLectureTitleStore(); + const { classTitle } = useClassTitleStore(); // 현재 경로에 해당하는 라우트 설정 찾기 const currentRoute = Object.values(STUDENT_ROUTE_CONFIG).find((config) => { @@ -41,9 +41,7 @@ export default function StudentLayout({ return ; case StudentHeaderType.BACK_WITH_TITLE: return ( - + ); case StudentHeaderType.BACK_WITH_PROFILE: return ; diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/LectureInfoSection/LectureInfoSection.tsx b/frontend/app/student/lecture-detail/[lectureId]/_components/LectureInfoSection/LectureInfoSection.tsx index 39a6e90d..44282103 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/_components/LectureInfoSection/LectureInfoSection.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/LectureInfoSection/LectureInfoSection.tsx @@ -6,7 +6,6 @@ import { fetchLectureDetail } from "@/api/lectures/fetchLectureDetail"; import { FetchLectureDetailResult } from "@/types/lectures/fetchLectureDetailTypes"; import LoadingSpinner from "@/components/LoadingSpinner/LoadingSpinner"; import NoDataView from "@/components/NoDataView/NoDataView"; -import { useLectureTitleStore } from "@/store/useLectureTitleStore"; interface LectureInfoSectionProps { lectureId: string; @@ -19,7 +18,6 @@ export default function LectureInfoSection({ useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const { setLectureTitle } = useLectureTitleStore(); useEffect(() => { const loadLectureData = async () => { @@ -28,7 +26,6 @@ export default function LectureInfoSection({ const response = await fetchLectureDetail(lectureId); if (response.isSuccess && response.result) { setLectureData(response.result); - setLectureTitle(response.result.lectureName); } else { setError("강의 정보를 불러올 수 없습니다."); } diff --git a/frontend/app/student/lecture-detail/[lectureId]/page.tsx b/frontend/app/student/lecture-detail/[lectureId]/page.tsx index 324e4d51..dc10f828 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/page.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/page.tsx @@ -8,12 +8,16 @@ import LecrureRecordSection from "./_components/LecrureRecordSection/LecrureReco import QuizSection from "./_components/QuizSection/QuizSection"; import style from "./page.module.scss"; import Tab from "@/components/Tab/Tab"; +import { useClassTitleStore } from "@/store/useClassTitleStore"; export default function StudentLectureDetailPage() { const params = useParams(); + const { setClassTitle } = useClassTitleStore(); const lectureId = params.lectureId as string; const [selectedTab, setSelectedTab] = useState(0); + // TODO: lectureId로 클래스타이틀 불러오고, 이를 setClassTitle 이용해서 스토어에 저장해야 함 + const tabs = ["수업 자료", "질문하기", "강의 녹음", "복습 퀴즈"]; const handleTabSelect = (tabName: string) => { diff --git a/frontend/store/useClassTitleStore.ts b/frontend/store/useClassTitleStore.ts new file mode 100644 index 00000000..57fbc801 --- /dev/null +++ b/frontend/store/useClassTitleStore.ts @@ -0,0 +1,13 @@ +import { create } from "zustand"; + +interface ClassTitleStore { + classTitle: string; + setClassTitle: (title: string) => void; + clearClassTitle: () => void; +} + +export const useClassTitleStore = create((set) => ({ + classTitle: "", + setClassTitle: (title: string) => set({ classTitle: title }), + clearClassTitle: () => set({ classTitle: "" }), +})); diff --git a/frontend/store/useLectureTitleStore.ts b/frontend/store/useLectureTitleStore.ts deleted file mode 100644 index 439f3b0c..00000000 --- a/frontend/store/useLectureTitleStore.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { create } from "zustand"; - -interface LectureTitleStore { - lectureTitle: string; - setLectureTitle: (title: string) => void; - clearLectureTitle: () => void; -} - -export const useLectureTitleStore = create((set) => ({ - lectureTitle: "", - setLectureTitle: (title: string) => set({ lectureTitle: title }), - clearLectureTitle: () => set({ lectureTitle: "" }), -})); From 7e869d29a5ebafa6a42eee9d513607ed0b40d004 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sat, 6 Sep 2025 09:37:03 +0900 Subject: [PATCH 05/32] =?UTF-8?q?=E2=9C=A8=20(#305)=20=EA=B0=95=EC=9D=98?= =?UTF-8?q?=20=EC=83=81=EC=84=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=20?= =?UTF-8?q?=EC=BD=98=ED=85=90=EC=B8=A0=20=EC=98=81=EC=97=AD=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../student/lecture-detail/[lectureId]/page.module.scss | 7 +++++++ frontend/app/student/lecture-detail/[lectureId]/page.tsx | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/app/student/lecture-detail/[lectureId]/page.module.scss b/frontend/app/student/lecture-detail/[lectureId]/page.module.scss index 19aecb08..50f25bfc 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/page.module.scss +++ b/frontend/app/student/lecture-detail/[lectureId]/page.module.scss @@ -1,3 +1,10 @@ .lectureDetailPage { padding: $spacing-lg; } + +.content { + margin: $spacing-lg 0; + height: 100%; + background-color: white; + overflow-y: scroll; +} diff --git a/frontend/app/student/lecture-detail/[lectureId]/page.tsx b/frontend/app/student/lecture-detail/[lectureId]/page.tsx index dc10f828..16e08431 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/page.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/page.tsx @@ -44,7 +44,7 @@ export default function StudentLectureDetailPage() {
- {renderSection()} +
{renderSection()}
); } From b48221a6825dcbd57690fca495a11a2c2eebb3ba Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 7 Sep 2025 09:32:16 +0900 Subject: [PATCH 06/32] =?UTF-8?q?=E2=9C=A8=20(#305)=20=EA=B0=95=EC=9D=98?= =?UTF-8?q?=20=EC=83=81=EC=84=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=20?= =?UTF-8?q?=EA=B0=95=EC=9D=98=20=EB=85=B9=EC=9D=8C=20=EC=84=B9=EC=85=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=83=81=ED=83=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EC=8A=A4=ED=86=A0=EC=96=B4=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LecrureRecordSection.module.scss | 32 +++++ .../LecrureRecordSection.tsx | 130 +++++++++++++++++- .../LectureInfoSection/LectureInfoSection.tsx | 8 +- .../QuestionListSection.tsx | 11 +- .../_components/QuizSection/QuizSection.tsx | 11 +- .../lecture-detail/[lectureId]/page.tsx | 2 +- frontend/store/resetAllStores.ts | 2 + frontend/store/useLectureStatusStore.ts | 19 +++ 8 files changed, 208 insertions(+), 7 deletions(-) create mode 100644 frontend/app/student/lecture-detail/[lectureId]/_components/LecrureRecordSection/LecrureRecordSection.module.scss create mode 100644 frontend/store/useLectureStatusStore.ts diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/LecrureRecordSection/LecrureRecordSection.module.scss b/frontend/app/student/lecture-detail/[lectureId]/_components/LecrureRecordSection/LecrureRecordSection.module.scss new file mode 100644 index 00000000..ce645ce1 --- /dev/null +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/LecrureRecordSection/LecrureRecordSection.module.scss @@ -0,0 +1,32 @@ +.card { + background: white; + border-radius: 8px; + padding: 20px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + margin-bottom: 16px; +} + +.title { + font-size: 18px; + font-weight: 600; + margin-bottom: 16px; + color: #333; +} + +.audioItem { + display: flex; + flex-direction: column; + gap: 12px; +} + +.audioName { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.audioPlayer { + width: 100%; + height: 40px; +} diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/LecrureRecordSection/LecrureRecordSection.tsx b/frontend/app/student/lecture-detail/[lectureId]/_components/LecrureRecordSection/LecrureRecordSection.tsx index 030a9642..2b04cea7 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/_components/LecrureRecordSection/LecrureRecordSection.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/LecrureRecordSection/LecrureRecordSection.tsx @@ -1,5 +1,129 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; +import { useLectureStatusStore } from "@/store/useLectureStatusStore"; +import { FetchAudioFileResult } from "@/types/lectures/fetchAudioFileTypes"; +import { fetchAudioFile } from "@/api/lectures/fetchAudioFile"; +import { Download, Mic } from "lucide-react"; +import FileDisplay from "@/components/FileDisplay/FileDisplay"; +import IconButton from "@/components/Button/IconButton/IconButton"; +import styles from "./LecrureRecordSection.module.scss"; +import NoDataView from "@/components/NoDataView/NoDataView"; +import LoadingSpinner from "@/components/LoadingSpinner/LoadingSpinner"; -export default function LecrureRecordSection() { - return
LecrureRecordSection
; +interface LecrureRecordSectionProps { + lectureId: string; +} + +export default function LecrureRecordSection({ + lectureId, +}: LecrureRecordSectionProps) { + const { lectureStatus } = useLectureStatusStore(); + const [audio, setAudio] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const getStatusText = (status: string) => { + switch (status) { + case "beforeLecture": + case "onLecture": + return "강의가 종료된 후 강의 녹음을 확인할 수 있습니다. "; + case "makeQuiz": + case "checkDashboard": + return null; + default: + return "강의 중"; + } + }; + + const handleDownload = async () => { + if (!audio?.audioUrl) return; + + try { + const response = await fetch(audio.audioUrl); + if (!response.ok) { + throw new Error("다운로드에 실패했습니다."); + } + + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = audio.audioName || "강의녹음본.mp3"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + } catch (err) { + console.error("다운로드 실패:", err); + alert("다운로드 중 오류가 발생했습니다."); + } + }; + + useEffect(() => { + const fetchAudio = async () => { + try { + setLoading(true); + setError(null); + const response = await fetchAudioFile(lectureId); + + if (response.isSuccess && response.result) { + setAudio(response.result); + } else { + setAudio(null); + } + } catch (err) { + console.error("오디오 파일 조회 실패:", err); + setError("오디오 파일을 불러오는 중 오류가 발생했습니다."); + setAudio(null); + } finally { + setLoading(false); + } + }; + + fetchAudio(); + }, [lectureId]); + + if (!lectureStatus) return null; + + if (loading) return ; + + if (error) { + return ( +
+ +
+ ); + } + + return ( +
+ {getStatusText(lectureStatus) !== null ? ( +
{getStatusText(lectureStatus)}
+ ) : audio ? ( +
+ + + } + onClick={handleDownload} + ariaLabel={"강의 녹음본 다운로드"} + /> + + +
+ ) : ( + + )} +
+ ); } diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/LectureInfoSection/LectureInfoSection.tsx b/frontend/app/student/lecture-detail/[lectureId]/_components/LectureInfoSection/LectureInfoSection.tsx index 44282103..84d1d601 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/_components/LectureInfoSection/LectureInfoSection.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/LectureInfoSection/LectureInfoSection.tsx @@ -6,6 +6,10 @@ import { fetchLectureDetail } from "@/api/lectures/fetchLectureDetail"; import { FetchLectureDetailResult } from "@/types/lectures/fetchLectureDetailTypes"; import LoadingSpinner from "@/components/LoadingSpinner/LoadingSpinner"; import NoDataView from "@/components/NoDataView/NoDataView"; +import { + useLectureStatusStore, + LectureStatus, +} from "@/store/useLectureStatusStore"; interface LectureInfoSectionProps { lectureId: string; @@ -14,6 +18,7 @@ interface LectureInfoSectionProps { export default function LectureInfoSection({ lectureId, }: LectureInfoSectionProps) { + const { setLectureStatus } = useLectureStatusStore(); const [lectureData, setLectureData] = useState(null); const [loading, setLoading] = useState(true); @@ -26,6 +31,7 @@ export default function LectureInfoSection({ const response = await fetchLectureDetail(lectureId); if (response.isSuccess && response.result) { setLectureData(response.result); + setLectureStatus(response.result.status as LectureStatus); } else { setError("강의 정보를 불러올 수 없습니다."); } @@ -37,7 +43,7 @@ export default function LectureInfoSection({ }; loadLectureData(); - }, [lectureId]); + }, [lectureId, setLectureStatus]); const getStatusText = (status: string) => { switch (status) { case "beforeLecture": diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/QuestionListSection/QuestionListSection.tsx b/frontend/app/student/lecture-detail/[lectureId]/_components/QuestionListSection/QuestionListSection.tsx index 9c852db2..41060d84 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/_components/QuestionListSection/QuestionListSection.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/QuestionListSection/QuestionListSection.tsx @@ -1,5 +1,14 @@ import React from "react"; +import { useLectureStatusStore } from "@/store/useLectureStatusStore"; export default function QuestionListSection() { - return
QuestionListSection
; + const { lectureStatus } = useLectureStatusStore(); + + return ( +
+

질문하기

+

현재 강의 상태: {lectureStatus}

+ {/* 여기에 질문하기 관련 로직 추가 */} +
+ ); } diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx b/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx index 97d1a716..b91bd66b 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx @@ -1,5 +1,14 @@ import React from "react"; +import { useLectureStatusStore } from "@/store/useLectureStatusStore"; export default function QuizSection() { - return
QuizSection
; + const { lectureStatus } = useLectureStatusStore(); + + return ( +
+

복습 퀴즈

+

현재 강의 상태: {lectureStatus}

+ {/* 여기에 복습 퀴즈 관련 로직 추가 */} +
+ ); } diff --git a/frontend/app/student/lecture-detail/[lectureId]/page.tsx b/frontend/app/student/lecture-detail/[lectureId]/page.tsx index 16e08431..b6e41348 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/page.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/page.tsx @@ -32,7 +32,7 @@ export default function StudentLectureDetailPage() { case 1: return ; case 2: - return ; + return ; case 3: return ; default: diff --git a/frontend/store/resetAllStores.ts b/frontend/store/resetAllStores.ts index 7c5fef37..8f052884 100644 --- a/frontend/store/resetAllStores.ts +++ b/frontend/store/resetAllStores.ts @@ -4,6 +4,7 @@ import useLectureListStore from "./useLectureListStore"; import useSelectedClassStore from "./useSelectedClassStore"; import useClassListStore from "./useClassListStore"; import { useSignupStore } from "./useSignupStore"; +import { useLectureStatusStore } from "./useLectureStatusStore"; /** * 모든 스토어를 초기 상태로 리셋하는 함수 @@ -26,6 +27,7 @@ export const resetAllStores = () => { useSelectedClassStore.getState().reset(); useClassListStore.getState().reset(); useSignupStore.getState().reset(); + useLectureStatusStore.getState().clearLectureStatus(); console.log("모든 스토어가 초기화되었습니다."); }; diff --git a/frontend/store/useLectureStatusStore.ts b/frontend/store/useLectureStatusStore.ts new file mode 100644 index 00000000..86a1a32f --- /dev/null +++ b/frontend/store/useLectureStatusStore.ts @@ -0,0 +1,19 @@ +import { create } from "zustand"; + +export type LectureStatus = + | "beforeLecture" + | "onLecture" + | "makeQuiz" + | "checkDashboard"; + +interface LectureStatusStore { + lectureStatus: LectureStatus | null; + setLectureStatus: (status: LectureStatus) => void; + clearLectureStatus: () => void; +} + +export const useLectureStatusStore = create((set) => ({ + lectureStatus: null, + setLectureStatus: (status: LectureStatus) => set({ lectureStatus: status }), + clearLectureStatus: () => set({ lectureStatus: null }), +})); From c8cdfaf04d35e4ec1b9df73550c01127f93d506e Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 7 Sep 2025 09:52:04 +0900 Subject: [PATCH 07/32] =?UTF-8?q?=E2=9C=A8=20(#305)=20next.config.ts?= =?UTF-8?q?=EC=97=90=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/next.config.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/next.config.ts b/frontend/next.config.ts index 44f15820..0f6633e5 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -17,6 +17,9 @@ const nextConfig: NextConfig = { @use "@/styles/_variables.scss" as *; @use "@/styles/_mixins.scss" as *;`, }, + images: { + domains: ["kwclasslog.s3.ap-southeast-2.amazonaws.com"], + }, }; export default nextPWA(nextConfig); From a6b8eb4185b104711839edf00f636f9043453311 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 7 Sep 2025 10:15:42 +0900 Subject: [PATCH 08/32] =?UTF-8?q?=E2=9C=A8=20(#305)=20=EA=B0=95=EC=9D=98?= =?UTF-8?q?=20=EC=83=81=EC=84=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=20?= =?UTF-8?q?=EC=A7=88=EB=AC=B8=20=EB=AA=A9=EB=A1=9D=20=EC=84=B9=EC=85=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=86=8C=EC=BC=93=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=20=EC=A4=80=EB=B9=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QuestionListSection.module.scss | 53 ++++++ .../QuestionListSection.tsx | 156 +++++++++++++++++- .../[lectureId]/page.module.scss | 6 +- .../lecture-detail/[lectureId]/page.tsx | 2 +- .../_components/QuestionList/QuestionList.tsx | 2 +- 5 files changed, 209 insertions(+), 10 deletions(-) create mode 100644 frontend/app/student/lecture-detail/[lectureId]/_components/QuestionListSection/QuestionListSection.module.scss diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/QuestionListSection/QuestionListSection.module.scss b/frontend/app/student/lecture-detail/[lectureId]/_components/QuestionListSection/QuestionListSection.module.scss new file mode 100644 index 00000000..c16fec61 --- /dev/null +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/QuestionListSection/QuestionListSection.module.scss @@ -0,0 +1,53 @@ +.questionListSection { + height: 100%; +} + +.questionListContainer { + display: flex; + flex-direction: column; + gap: $spacing-sm; + justify-content: space-between; + height: 100%; +} + +.questionList { + height: 100%; + overflow-y: scroll; + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: $spacing-sm; +} + +.questionItem { + width: fit-content; + padding: $spacing-sm; + border: 1px solid $color-neutral-7; + border-radius: $radius-md; + box-shadow: $shadow-sm; + + &:last-child { + border-bottom: none; + } +} + +.timestamp { + margin-top: $spacing-xs; + font-size: $font-size-sm; + color: $color-neutral-5; +} + +.message { + font-size: $font-size-md; + line-height: 1.4; + word-break: break-word; + font-weight: $font-weight-light; +} + +.questionInputContainer { + display: flex; + gap: $spacing-sm; + align-items: center; +} diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/QuestionListSection/QuestionListSection.tsx b/frontend/app/student/lecture-detail/[lectureId]/_components/QuestionListSection/QuestionListSection.tsx index 41060d84..c8c5c684 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/_components/QuestionListSection/QuestionListSection.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/QuestionListSection/QuestionListSection.tsx @@ -1,14 +1,158 @@ -import React from "react"; +import React, { useEffect, useState, useRef, useCallback } from "react"; import { useLectureStatusStore } from "@/store/useLectureStatusStore"; +import NoDataView from "@/components/NoDataView/NoDataView"; +import { MessageCircle, Send } from "lucide-react"; +import styles from "./QuestionListSection.module.scss"; +import LoadingSpinner from "@/components/LoadingSpinner/LoadingSpinner"; +import BasicInput from "@/components/Input/BasicInput/BasicInput"; +import IconButton from "@/components/Button/IconButton/IconButton"; -export default function QuestionListSection() { +export default function QuestionListSection({ + lectureId, +}: { + lectureId: string; +}) { const { lectureStatus } = useLectureStatusStore(); + const [questions, setQuestions] = useState([]); + const [questionInput, setQuestionInput] = useState(""); + const [loading, setLoading] = useState(true); + const socketRef = useRef(null); + + // 소켓 연결 함수 + const connectSocket = useCallback(() => { + if (socketRef.current?.readyState === WebSocket.OPEN) return; + + try { + // TODO: 실제 소켓 서버 URL로 변경 + const socketUrl = `ws://localhost:8080/ws/lecture/${lectureId}`; + socketRef.current = new WebSocket(socketUrl); + + socketRef.current.onopen = () => { + console.log("소켓 연결 성공"); + }; + + socketRef.current.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + handleSocketMessage(data); + } catch (error) { + console.error("소켓 메시지 파싱 오류:", error); + } + }; + + socketRef.current.onclose = () => { + console.log("소켓 연결 종료"); + }; + + socketRef.current.onerror = (error) => { + console.error("소켓 오류:", error); + }; + } catch (error) { + console.error("소켓 연결 실패:", error); + } + }, [lectureId]); + + // 소켓 메시지 처리 함수 + const handleSocketMessage = (data: { + type: string; + question?: string; + questions?: string[]; + }) => { + switch (data.type) { + case "newQuestion": + setQuestions((prev) => [...prev, data.question || ""]); + break; + case "questionList": + setQuestions(data.questions || []); + break; + default: + console.log("알 수 없는 메시지 타입:", data.type); + } + }; + + // 질문 전송 함수 + const sendQuestion = () => { + if (!questionInput.trim() || !socketRef.current) return; + + const message = { + type: "sendQuestion", + lectureId: lectureId, + question: questionInput.trim(), + timestamp: new Date().toISOString(), + }; + + socketRef.current.send(JSON.stringify(message)); + setQuestionInput(""); // 입력창 초기화 + }; + + useEffect(() => { + // TODO: API 호출로 변경 + setQuestions([ + "dd", + "AsdfasAsdfasAsdfasAsdfasAsdfasAsdfasAsdfasAsdfasAsdfasAsdfasAsdfasAsdfasAsdfasAsdfasAsdfasAsdfasAsdfasAsdfasAsdfas", + "Asdfadfg", + "Asdfadfg", + ]); + setLoading(false); + + // 강의 중일 때만 소켓 연결 + if (lectureStatus === "onLecture") { + connectSocket(); + } + + // 컴포넌트 언마운트 시 소켓 연결 해제 + return () => { + if (socketRef.current) { + socketRef.current.close(); + } + }; + }, [lectureId, lectureStatus, connectSocket]); + + const now = () => { + try { + const date = new Date(); + const hours = date.getHours().toString().padStart(2, "0"); + const minutes = date.getMinutes().toString().padStart(2, "0"); + return `${hours}:${minutes}`; + } catch { + return "00:00"; + } + }; + + if (loading) return ; return ( -
-

질문하기

-

현재 강의 상태: {lectureStatus}

- {/* 여기에 질문하기 관련 로직 추가 */} +
+ {lectureStatus === "onLecture" ? ( +
+
    + {questions.map((q, index) => ( +
  • +
    {q}
    +
    {now()}
    +
  • + ))} +
+
+ setQuestionInput(e.target.value)} + placeholder="질문을 입력해주세요." + /> + } + onClick={sendQuestion} + ariaLabel={"전송"} + /> +
+
+ ) : ( + + )}
); } diff --git a/frontend/app/student/lecture-detail/[lectureId]/page.module.scss b/frontend/app/student/lecture-detail/[lectureId]/page.module.scss index 50f25bfc..61b01287 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/page.module.scss +++ b/frontend/app/student/lecture-detail/[lectureId]/page.module.scss @@ -1,10 +1,12 @@ .lectureDetailPage { + height: 94vh; + display: flex; + flex-direction: column; padding: $spacing-lg; } .content { - margin: $spacing-lg 0; height: 100%; - background-color: white; + margin-top: $spacing-lg; overflow-y: scroll; } diff --git a/frontend/app/student/lecture-detail/[lectureId]/page.tsx b/frontend/app/student/lecture-detail/[lectureId]/page.tsx index b6e41348..73c370f1 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/page.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/page.tsx @@ -30,7 +30,7 @@ export default function StudentLectureDetailPage() { case 0: return ; case 1: - return ; + return ; case 2: return ; case 3: diff --git a/frontend/app/teacher/lecture-detail/[lectureId]/_components/QuestionList/QuestionList.tsx b/frontend/app/teacher/lecture-detail/[lectureId]/_components/QuestionList/QuestionList.tsx index e3f2fa55..8dc06ff5 100644 --- a/frontend/app/teacher/lecture-detail/[lectureId]/_components/QuestionList/QuestionList.tsx +++ b/frontend/app/teacher/lecture-detail/[lectureId]/_components/QuestionList/QuestionList.tsx @@ -6,7 +6,7 @@ import Image from "next/image"; import { IMAGES } from "@/constants/images"; import { useLectureDetail } from "../LectureDetailContext"; -interface Question { +export interface Question { sender: string; message: string; timestamp: string; From 481290c8099e2d3d4c501ce30b68dadbec339bb0 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 7 Sep 2025 11:38:58 +0900 Subject: [PATCH 09/32] =?UTF-8?q?=E2=9C=A8=20(#305)=20recordSection?= =?UTF-8?q?=EC=97=90=20height=20100%=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LecrureRecordSection/LecrureRecordSection.module.scss | 4 ++++ .../_components/LecrureRecordSection/LecrureRecordSection.tsx | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/LecrureRecordSection/LecrureRecordSection.module.scss b/frontend/app/student/lecture-detail/[lectureId]/_components/LecrureRecordSection/LecrureRecordSection.module.scss index ce645ce1..58e7affc 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/_components/LecrureRecordSection/LecrureRecordSection.module.scss +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/LecrureRecordSection/LecrureRecordSection.module.scss @@ -1,3 +1,7 @@ +.recordSection { + height: 100%; +} + .card { background: white; border-radius: 8px; diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/LecrureRecordSection/LecrureRecordSection.tsx b/frontend/app/student/lecture-detail/[lectureId]/_components/LecrureRecordSection/LecrureRecordSection.tsx index 2b04cea7..cac32029 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/_components/LecrureRecordSection/LecrureRecordSection.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/LecrureRecordSection/LecrureRecordSection.tsx @@ -99,7 +99,7 @@ export default function LecrureRecordSection({ } return ( -
+
{getStatusText(lectureStatus) !== null ? (
{getStatusText(lectureStatus)}
) : audio ? ( From 9326b7132c218127c940c971bd5ad1a96612a050 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 7 Sep 2025 11:48:54 +0900 Subject: [PATCH 10/32] =?UTF-8?q?=E2=9C=A8=20(#305)=20FileDisplay=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=EC=97=90=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=ED=81=AC=EA=B8=B0=20=ED=91=9C=EC=8B=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=8A=A4=ED=83=80?= =?UTF-8?q?=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/FileDisplay/FileDisplay.module.scss | 11 +++++++++++ frontend/components/FileDisplay/FileDisplay.tsx | 9 ++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/frontend/components/FileDisplay/FileDisplay.module.scss b/frontend/components/FileDisplay/FileDisplay.module.scss index 5809b686..3e16b45e 100644 --- a/frontend/components/FileDisplay/FileDisplay.module.scss +++ b/frontend/components/FileDisplay/FileDisplay.module.scss @@ -27,3 +27,14 @@ text-overflow: ellipsis; } } + +.fileInfoContainer { + display: flex; + flex-direction: column; + gap: $spacing-xs; +} + +.size { + font-size: $font-size-sm; + color: $color-neutral-6; +} diff --git a/frontend/components/FileDisplay/FileDisplay.tsx b/frontend/components/FileDisplay/FileDisplay.tsx index f047fd2e..13dab913 100644 --- a/frontend/components/FileDisplay/FileDisplay.tsx +++ b/frontend/components/FileDisplay/FileDisplay.tsx @@ -23,9 +23,10 @@ const fileIcons: { [key: string]: StaticImageData } = { type FileDisplayProps = { fileName: string; + size?: string; }; -const FileDisplay: React.FC = ({ fileName }) => { +const FileDisplay: React.FC = ({ fileName, size }) => { // 파일 확장자 추출 const fileExtension = fileName.split(".").pop()?.toLowerCase(); @@ -81,8 +82,10 @@ const FileDisplay: React.FC = ({ fileName }) => { height={24} />
- - {fileName} +
+ {fileName} + {size} +
); }; From c4b257019adf1bb5a23c8f25fa566778bce45b78 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 7 Sep 2025 11:49:32 +0900 Subject: [PATCH 11/32] =?UTF-8?q?=E2=9C=A8=20(#305)=20=EA=B0=95=EC=9D=98?= =?UTF-8?q?=20=EC=83=81=EC=84=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=9D=98=20?= =?UTF-8?q?LectureNoteListSection=20=EB=B0=8F=20QuizSection=EC=97=90=20lec?= =?UTF-8?q?tureId=20prop=20=EC=B6=94=EA=B0=80,=20=EA=B0=95=EC=9D=98=20?= =?UTF-8?q?=EC=9E=90=EB=A3=8C=20=EB=AA=A9=EB=A1=9D=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=EC=8A=A4=ED=83=80=EC=9D=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LectureNoteListSection.module.scss | 22 ++++ .../LectureNoteListSection.tsx | 100 +++++++++++++++++- .../_components/QuizSection/QuizSection.tsx | 7 +- .../lecture-detail/[lectureId]/page.tsx | 6 +- 4 files changed, 128 insertions(+), 7 deletions(-) create mode 100644 frontend/app/student/lecture-detail/[lectureId]/_components/LectureNoteListSection/LectureNoteListSection.module.scss diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/LectureNoteListSection/LectureNoteListSection.module.scss b/frontend/app/student/lecture-detail/[lectureId]/_components/LectureNoteListSection/LectureNoteListSection.module.scss new file mode 100644 index 00000000..56064a37 --- /dev/null +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/LectureNoteListSection/LectureNoteListSection.module.scss @@ -0,0 +1,22 @@ +.materialList { + display: flex; + flex-direction: column; + gap: $spacing-sm; +} + +.materialItem { + display: flex; + align-items: center; + justify-content: space-between; + padding-bottom: $spacing-xs; + border-bottom: 1px solid $color-neutral-7; + + &:last-child { + border-bottom: none; + } +} + +.size { + color: $color-neutral-6; + font-size: $font-size-sm; +} diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/LectureNoteListSection/LectureNoteListSection.tsx b/frontend/app/student/lecture-detail/[lectureId]/_components/LectureNoteListSection/LectureNoteListSection.tsx index c9532749..49bc6f1f 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/_components/LectureNoteListSection/LectureNoteListSection.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/LectureNoteListSection/LectureNoteListSection.tsx @@ -1,5 +1,99 @@ -import React from "react"; +import { fetchLectureNoteByLectureId } from "@/api/lectures/fetchLectureNoteByLectureId"; +import FileDisplay from "@/components/FileDisplay/FileDisplay"; +import LoadingSpinner from "@/components/LoadingSpinner/LoadingSpinner"; +import { FetchLectureNoteByLectureIdResult } from "@/types/lectures/fetchLectureNoteByLectureIdTypes"; +import React, { useCallback, useEffect, useState } from "react"; +import styles from "./LectureNoteListSection.module.scss"; +import NoDataView from "@/components/NoDataView/NoDataView"; +import { Download, FileText } from "lucide-react"; +import IconButton from "@/components/Button/IconButton/IconButton"; -export default function LectureNoteListSection() { - return
LectureNoteListSection
; +interface LectureNoteListSectionProps { + lectureId: string; +} + +export default function LectureNoteListSection({ + lectureId, +}: LectureNoteListSectionProps) { + const [lectureNotes, setLectureNotes] = useState< + FetchLectureNoteByLectureIdResult[] + >([]); + const [loading, setLoading] = useState(true); + + const fetchLectureNotes = useCallback(async () => { + try { + setLoading(true); + const response = await fetchLectureNoteByLectureId(lectureId); + + if (response.isSuccess && response.result) { + setLectureNotes(response.result); + } else { + console.error("강의자료 조회 실패:", response.message); + setLectureNotes([]); + } + } catch (error) { + console.error("강의자료 조회 중 오류 발생:", error); + setLectureNotes([]); + } finally { + setLoading(false); + } + }, [lectureId]); + + const handleDownload = async (note: FetchLectureNoteByLectureIdResult) => { + if (!note.lectureNoteUrl) return; + + try { + const response = await fetch(note.lectureNoteUrl); + if (!response.ok) { + throw new Error("다운로드에 실패했습니다."); + } + + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = note.lectureNoteName || "강의자료"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + } catch (err) { + console.error("다운로드 실패:", err); + alert("다운로드 중 오류가 발생했습니다."); + } + }; + + useEffect(() => { + fetchLectureNotes(); + }, [lectureId, fetchLectureNotes]); + + if (loading) return ; + + return ( +
+ {lectureNotes.length === 0 ? ( + + ) : ( + lectureNotes.map((note) => ( +
+
+ +
+ } + onClick={() => handleDownload(note)} + ariaLabel={"강의자료 다운로드"} + /> +
+ )) + )} +
+ ); } diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx b/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx index b91bd66b..6cb24ba9 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx @@ -1,13 +1,18 @@ import React from "react"; import { useLectureStatusStore } from "@/store/useLectureStatusStore"; -export default function QuizSection() { +interface QuizSectionProps { + lectureId: string; +} + +export default function QuizSection({ lectureId }: QuizSectionProps) { const { lectureStatus } = useLectureStatusStore(); return (

복습 퀴즈

현재 강의 상태: {lectureStatus}

+

강의 ID: {lectureId}

{/* 여기에 복습 퀴즈 관련 로직 추가 */}
); diff --git a/frontend/app/student/lecture-detail/[lectureId]/page.tsx b/frontend/app/student/lecture-detail/[lectureId]/page.tsx index 73c370f1..7349a040 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/page.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/page.tsx @@ -28,15 +28,15 @@ export default function StudentLectureDetailPage() { const renderSection = () => { switch (selectedTab) { case 0: - return ; + return ; case 1: return ; case 2: return ; case 3: - return ; + return ; default: - return ; + return ; } }; From 22feaf343e32c8861122bd96db5dcfce9c42f2d2 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 7 Sep 2025 11:53:00 +0900 Subject: [PATCH 12/32] =?UTF-8?q?=E2=9C=A8=20(#305)=20=EA=B0=95=EC=9D=98?= =?UTF-8?q?=20=EC=9E=90=EB=A3=8C=20=EB=8B=A4=EC=9A=B4=EB=A1=9C=EB=93=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=8A=A4?= =?UTF-8?q?=ED=83=80=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LectureNote/LectureNote.module.scss | 4 -- .../_components/LectureNote/LectureNote.tsx | 46 ++++++++++++++----- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/frontend/app/student/class-detail/[classId]/_components/LectureNote/LectureNote.module.scss b/frontend/app/student/class-detail/[classId]/_components/LectureNote/LectureNote.module.scss index 53ba5fa5..1f65501e 100644 --- a/frontend/app/student/class-detail/[classId]/_components/LectureNote/LectureNote.module.scss +++ b/frontend/app/student/class-detail/[classId]/_components/LectureNote/LectureNote.module.scss @@ -24,11 +24,7 @@ border: 1px solid $color-neutral-7; border-radius: $radius-md; background-color: $color-white; - cursor: pointer; transition: background-color 0.3s ease; - &:hover { - background-color: $color-skyblue; - } } .fileItem > :first-child { diff --git a/frontend/app/student/class-detail/[classId]/_components/LectureNote/LectureNote.tsx b/frontend/app/student/class-detail/[classId]/_components/LectureNote/LectureNote.tsx index dce67792..346ecba3 100644 --- a/frontend/app/student/class-detail/[classId]/_components/LectureNote/LectureNote.tsx +++ b/frontend/app/student/class-detail/[classId]/_components/LectureNote/LectureNote.tsx @@ -4,6 +4,9 @@ import { useParams } from "next/navigation"; import { fetchLectureNotesByClass } from "@/api/lectures/fetchLectureNotesByClass"; import FileDisplay from "@/components/FileDisplay/FileDisplay"; import LoadingSpinner from "@/components/LoadingSpinner/LoadingSpinner"; +import IconButton from "@/components/Button/IconButton/IconButton"; +import { Download } from "lucide-react"; +import { FetchLectureNoteByLectureIdResult } from "@/types/lectures/fetchLectureNoteByLectureIdTypes"; interface LectureNote { lectureNoteId: string; @@ -44,8 +47,28 @@ export default function LectureNote() { ); } - const handleNoteClick = (lectureNoteUrl: string) => { - window.open(lectureNoteUrl, "_blank"); + const handleDownload = async (note: FetchLectureNoteByLectureIdResult) => { + if (!note.lectureNoteUrl) return; + + try { + const response = await fetch(note.lectureNoteUrl); + if (!response.ok) { + throw new Error("다운로드에 실패했습니다."); + } + + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = note.lectureNoteName || "강의자료"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + } catch (err) { + console.error("다운로드 실패:", err); + alert("다운로드 중 오류가 발생했습니다."); + } }; return ( @@ -53,15 +76,16 @@ export default function LectureNote() { {lectureNotes.length > 0 ? (
{lectureNotes.map((note) => ( -
handleNoteClick(note.lectureNoteUrl)} - > - -
- {note.fileSize} -
+
+ + } + onClick={() => handleDownload(note)} + ariaLabel={"강의자료 다운로드"} + />
))}
From e0eeefb37aa5a2ab4f24f678a4e9efc9b0780ab3 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 7 Sep 2025 11:53:27 +0900 Subject: [PATCH 13/32] =?UTF-8?q?=E2=9C=A8=20(#305)=20=EA=B0=95=EC=9D=98?= =?UTF-8?q?=20=EB=85=B8=ED=8A=B8=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=84=B9?= =?UTF-8?q?=EC=85=98=EC=9D=98=20=EB=86=92=EC=9D=B4=EB=A5=BC=20100%?= =?UTF-8?q?=EB=A1=9C=20=EC=84=A4=EC=A0=95=ED=95=98=EC=97=AC=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=95=84=EC=9B=83=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LectureNoteListSection/LectureNoteListSection.module.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/LectureNoteListSection/LectureNoteListSection.module.scss b/frontend/app/student/lecture-detail/[lectureId]/_components/LectureNoteListSection/LectureNoteListSection.module.scss index 56064a37..9ba2fdb3 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/_components/LectureNoteListSection/LectureNoteListSection.module.scss +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/LectureNoteListSection/LectureNoteListSection.module.scss @@ -2,6 +2,7 @@ display: flex; flex-direction: column; gap: $spacing-sm; + height: 100%; } .materialItem { From 705b18d358fc4a1f0dc3359d962753a9b65cba46 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 7 Sep 2025 11:57:06 +0900 Subject: [PATCH 14/32] =?UTF-8?q?=E2=9C=A8=20(#305)=20=EA=B0=95=EC=9D=98?= =?UTF-8?q?=20=EC=9E=90=EB=A3=8C=20=EB=B0=8F=20=EB=85=B9=EC=9D=8C=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EB=8B=A4=EC=9A=B4=EB=A1=9C=EB=93=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EC=9D=84=20downloadUtils=20=EC=9C=A0?= =?UTF-8?q?=ED=8B=B8=EB=A6=AC=ED=8B=B0=EB=A1=9C=20=ED=86=B5=ED=95=A9?= =?UTF-8?q?=ED=95=98=EC=97=AC=20=EC=BD=94=EB=93=9C=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20=EC=97=90=EB=9F=AC=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_components/LectureNote/LectureNote.tsx | 26 ++------ .../LecrureRecordSection.tsx | 24 ++----- .../LectureNoteListSection.tsx | 26 ++------ .../LectureRecording/LectureRecording.tsx | 24 ++----- frontend/utils/downloadUtils.ts | 63 +++++++++++++++++++ 5 files changed, 83 insertions(+), 80 deletions(-) create mode 100644 frontend/utils/downloadUtils.ts diff --git a/frontend/app/student/class-detail/[classId]/_components/LectureNote/LectureNote.tsx b/frontend/app/student/class-detail/[classId]/_components/LectureNote/LectureNote.tsx index 346ecba3..d0e61a0f 100644 --- a/frontend/app/student/class-detail/[classId]/_components/LectureNote/LectureNote.tsx +++ b/frontend/app/student/class-detail/[classId]/_components/LectureNote/LectureNote.tsx @@ -7,6 +7,7 @@ import LoadingSpinner from "@/components/LoadingSpinner/LoadingSpinner"; import IconButton from "@/components/Button/IconButton/IconButton"; import { Download } from "lucide-react"; import { FetchLectureNoteByLectureIdResult } from "@/types/lectures/fetchLectureNoteByLectureIdTypes"; +import { downloadFileWithErrorHandling } from "@/utils/downloadUtils"; interface LectureNote { lectureNoteId: string; @@ -48,27 +49,10 @@ export default function LectureNote() { } const handleDownload = async (note: FetchLectureNoteByLectureIdResult) => { - if (!note.lectureNoteUrl) return; - - try { - const response = await fetch(note.lectureNoteUrl); - if (!response.ok) { - throw new Error("다운로드에 실패했습니다."); - } - - const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.download = note.lectureNoteName || "강의자료"; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - window.URL.revokeObjectURL(url); - } catch (err) { - console.error("다운로드 실패:", err); - alert("다운로드 중 오류가 발생했습니다."); - } + await downloadFileWithErrorHandling( + note.lectureNoteUrl, + note.lectureNoteName || "강의자료" + ); }; return ( diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/LecrureRecordSection/LecrureRecordSection.tsx b/frontend/app/student/lecture-detail/[lectureId]/_components/LecrureRecordSection/LecrureRecordSection.tsx index cac32029..9ad658db 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/_components/LecrureRecordSection/LecrureRecordSection.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/LecrureRecordSection/LecrureRecordSection.tsx @@ -8,6 +8,7 @@ import IconButton from "@/components/Button/IconButton/IconButton"; import styles from "./LecrureRecordSection.module.scss"; import NoDataView from "@/components/NoDataView/NoDataView"; import LoadingSpinner from "@/components/LoadingSpinner/LoadingSpinner"; +import { downloadFileWithErrorHandling } from "@/utils/downloadUtils"; interface LecrureRecordSectionProps { lectureId: string; @@ -37,25 +38,10 @@ export default function LecrureRecordSection({ const handleDownload = async () => { if (!audio?.audioUrl) return; - try { - const response = await fetch(audio.audioUrl); - if (!response.ok) { - throw new Error("다운로드에 실패했습니다."); - } - - const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.download = audio.audioName || "강의녹음본.mp3"; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - window.URL.revokeObjectURL(url); - } catch (err) { - console.error("다운로드 실패:", err); - alert("다운로드 중 오류가 발생했습니다."); - } + await downloadFileWithErrorHandling( + audio.audioUrl, + audio.audioName || "강의녹음본.mp3" + ); }; useEffect(() => { diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/LectureNoteListSection/LectureNoteListSection.tsx b/frontend/app/student/lecture-detail/[lectureId]/_components/LectureNoteListSection/LectureNoteListSection.tsx index 49bc6f1f..a1f58cd3 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/_components/LectureNoteListSection/LectureNoteListSection.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/LectureNoteListSection/LectureNoteListSection.tsx @@ -7,6 +7,7 @@ import styles from "./LectureNoteListSection.module.scss"; import NoDataView from "@/components/NoDataView/NoDataView"; import { Download, FileText } from "lucide-react"; import IconButton from "@/components/Button/IconButton/IconButton"; +import { downloadFileWithErrorHandling } from "@/utils/downloadUtils"; interface LectureNoteListSectionProps { lectureId: string; @@ -40,27 +41,10 @@ export default function LectureNoteListSection({ }, [lectureId]); const handleDownload = async (note: FetchLectureNoteByLectureIdResult) => { - if (!note.lectureNoteUrl) return; - - try { - const response = await fetch(note.lectureNoteUrl); - if (!response.ok) { - throw new Error("다운로드에 실패했습니다."); - } - - const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.download = note.lectureNoteName || "강의자료"; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - window.URL.revokeObjectURL(url); - } catch (err) { - console.error("다운로드 실패:", err); - alert("다운로드 중 오류가 발생했습니다."); - } + await downloadFileWithErrorHandling( + note.lectureNoteUrl, + note.lectureNoteName || "강의자료" + ); }; useEffect(() => { diff --git a/frontend/app/teacher/lecture-detail/[lectureId]/_components/LectureRecording/LectureRecording.tsx b/frontend/app/teacher/lecture-detail/[lectureId]/_components/LectureRecording/LectureRecording.tsx index 3ba4a462..9b1243da 100644 --- a/frontend/app/teacher/lecture-detail/[lectureId]/_components/LectureRecording/LectureRecording.tsx +++ b/frontend/app/teacher/lecture-detail/[lectureId]/_components/LectureRecording/LectureRecording.tsx @@ -8,6 +8,7 @@ import { fetchAudioFile } from "@/api/lectures/fetchAudioFile"; import { useLectureDetail } from "../LectureDetailContext"; import { FetchAudioFileResult } from "@/types/lectures/fetchAudioFileTypes"; import IconButton from "@/components/Button/IconButton/IconButton"; +import { downloadFileWithErrorHandling } from "@/utils/downloadUtils"; export default function LectureRecording() { const { lectureId } = useLectureDetail(); @@ -18,25 +19,10 @@ export default function LectureRecording() { const handleDownload = async () => { if (!audio?.audioUrl) return; - try { - const response = await fetch(audio.audioUrl); - if (!response.ok) { - throw new Error("다운로드에 실패했습니다."); - } - - const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.download = audio.audioName || "강의녹음본.mp3"; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - window.URL.revokeObjectURL(url); - } catch (err) { - console.error("다운로드 실패:", err); - alert("다운로드 중 오류가 발생했습니다."); - } + await downloadFileWithErrorHandling( + audio.audioUrl, + audio.audioName || "강의녹음본.mp3" + ); }; useEffect(() => { diff --git a/frontend/utils/downloadUtils.ts b/frontend/utils/downloadUtils.ts new file mode 100644 index 00000000..bcafa31c --- /dev/null +++ b/frontend/utils/downloadUtils.ts @@ -0,0 +1,63 @@ +/** + * 파일 다운로드 유틸리티 함수 + * @param url - 다운로드할 파일의 URL + * @param fileName - 다운로드될 파일명 (기본값: "파일") + * @returns Promise + */ +export const downloadFile = async ( + url: string, + fileName: string = "파일" +): Promise => { + if (!url) { + throw new Error("다운로드 URL이 제공되지 않았습니다."); + } + + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`다운로드에 실패했습니다. (${response.status})`); + } + + const blob = await response.blob(); + const downloadUrl = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + + link.href = downloadUrl; + link.download = fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(downloadUrl); + } catch (error) { + console.error("파일 다운로드 실패:", error); + throw error; + } +}; + +/** + * 파일 다운로드 함수 (에러 처리 포함) + * @param url - 다운로드할 파일의 URL + * @param fileName - 다운로드될 파일명 (기본값: "파일") + * @param onError - 에러 발생 시 실행할 콜백 함수 (선택사항) + */ +export const downloadFileWithErrorHandling = async ( + url: string, + fileName: string = "파일", + onError?: (error: Error) => void +): Promise => { + try { + await downloadFile(url, fileName); + } catch (error) { + const errorMessage = + error instanceof Error + ? error.message + : "다운로드 중 오류가 발생했습니다."; + console.error("다운로드 실패:", error); + + if (onError) { + onError(error instanceof Error ? error : new Error(errorMessage)); + } else { + alert(errorMessage); + } + } +}; From 6565ac7b3320ef2a4ad1ca128847b7f28b658c63 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 21 Sep 2025 00:28:44 +0900 Subject: [PATCH 15/32] =?UTF-8?q?=E2=9C=A8=20(#305)=20=ED=95=99=EC=83=9D?= =?UTF-8?q?=EC=9A=A9=20=EA=B0=95=EC=9D=98=20=EC=83=81=EC=84=B8=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20api=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/lectures/fetchStudentLectureDetail.ts | 20 +++++++++++++++++++ .../LectureInfoSection/LectureInfoSection.tsx | 13 ++++++------ frontend/constants/endpoints.ts | 2 ++ .../fetchStudentLectureDetailTypes.ts | 16 +++++++++++++++ 4 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 frontend/api/lectures/fetchStudentLectureDetail.ts create mode 100644 frontend/types/lectures/fetchStudentLectureDetailTypes.ts diff --git a/frontend/api/lectures/fetchStudentLectureDetail.ts b/frontend/api/lectures/fetchStudentLectureDetail.ts new file mode 100644 index 00000000..50c2a759 --- /dev/null +++ b/frontend/api/lectures/fetchStudentLectureDetail.ts @@ -0,0 +1,20 @@ +import { axiosInstance } from "@/api/axiosInstance"; +import axios from "axios"; +import { ENDPOINTS } from "@/constants/endpoints"; +import { ApiResponse } from "@/types/apiResponseTypes"; +import { FetchStudentLectureDetailResult } from "@/types/lectures/fetchStudentLectureDetailTypes"; + +export async function fetchStudentLectureDetail(lectureId: string) { + try { + const response = await axiosInstance.get< + ApiResponse + >(ENDPOINTS.LECTURES.GET_STUDENT_LECTURE_DETAIL(lectureId)); + return response.data; + } catch (error: unknown) { + if (axios.isAxiosError(error) && error.response) { + return error.response + .data as ApiResponse; + } + throw error; + } +} diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/LectureInfoSection/LectureInfoSection.tsx b/frontend/app/student/lecture-detail/[lectureId]/_components/LectureInfoSection/LectureInfoSection.tsx index 84d1d601..7c8435ee 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/_components/LectureInfoSection/LectureInfoSection.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/LectureInfoSection/LectureInfoSection.tsx @@ -2,8 +2,8 @@ import React, { useEffect, useState } from "react"; import styles from "./LectureInfoSection.module.scss"; import { BookOpenText, Calendar, Clock } from "lucide-react"; -import { fetchLectureDetail } from "@/api/lectures/fetchLectureDetail"; -import { FetchLectureDetailResult } from "@/types/lectures/fetchLectureDetailTypes"; +import { fetchStudentLectureDetail } from "@/api/lectures/fetchStudentLectureDetail"; +import { FetchStudentLectureDetailResult } from "@/types/lectures/fetchStudentLectureDetailTypes"; import LoadingSpinner from "@/components/LoadingSpinner/LoadingSpinner"; import NoDataView from "@/components/NoDataView/NoDataView"; import { @@ -20,7 +20,7 @@ export default function LectureInfoSection({ }: LectureInfoSectionProps) { const { setLectureStatus } = useLectureStatusStore(); const [lectureData, setLectureData] = - useState(null); + useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -28,7 +28,7 @@ export default function LectureInfoSection({ const loadLectureData = async () => { try { setLoading(true); - const response = await fetchLectureDetail(lectureId); + const response = await fetchStudentLectureDetail(lectureId); if (response.isSuccess && response.result) { setLectureData(response.result); setLectureStatus(response.result.status as LectureStatus); @@ -50,8 +50,9 @@ export default function LectureInfoSection({ return "강의 전"; case "onLecture": return "강의 중"; - case "makeQuiz": - case "checkDashboard": + case "afterLectureBeforeQuiz": + case "quizReadyForSubmission": + case "viewMyQuizResult": return "강의 종료"; default: return "강의 중"; diff --git a/frontend/constants/endpoints.ts b/frontend/constants/endpoints.ts index bebd6480..64e74781 100644 --- a/frontend/constants/endpoints.ts +++ b/frontend/constants/endpoints.ts @@ -68,6 +68,8 @@ export const ENDPOINTS = { `${BASE_API}/lectures/teacher/today?date=${date}`, GET_STUDENT_LECTURES_BY_DATE: (date: string) => `${BASE_API}/lectures/student/today?date=${date}`, + GET_STUDENT_LECTURE_DETAIL: (lectureId: string) => + `${BASE_API}/lectures/student/${lectureId}`, // 노트 관련 UPLOAD_NOTE: (classId: string) => diff --git a/frontend/types/lectures/fetchStudentLectureDetailTypes.ts b/frontend/types/lectures/fetchStudentLectureDetailTypes.ts new file mode 100644 index 00000000..7eb94658 --- /dev/null +++ b/frontend/types/lectures/fetchStudentLectureDetailTypes.ts @@ -0,0 +1,16 @@ +export interface FetchStudentLectureDetailResult { + lectureId: string; + classId: string; + lectureName: string; + lectureDate: string; + weekDay: string; + session: number; + startTime: string; + endTime: string; + status: + | "beforeLecture" + | "onLecture" + | "afterLectureBeforeQuiz" + | "quizReadyForSubmission" + | "viewMyQuizResult"; +} From 0b4ab3db7824ad0cbc90a96b16c5f28dedfd789b Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 21 Sep 2025 00:32:57 +0900 Subject: [PATCH 16/32] =?UTF-8?q?=F0=9F=94=A8=20(#305)=20LecrureRecordSect?= =?UTF-8?q?ion=EC=97=90=EC=84=9C=20=EA=B0=95=EC=9D=98=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=83=81=ED=83=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LecrureRecordSection/LecrureRecordSection.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/LecrureRecordSection/LecrureRecordSection.tsx b/frontend/app/student/lecture-detail/[lectureId]/_components/LecrureRecordSection/LecrureRecordSection.tsx index 9ad658db..db711ddd 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/_components/LecrureRecordSection/LecrureRecordSection.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/LecrureRecordSection/LecrureRecordSection.tsx @@ -27,11 +27,12 @@ export default function LecrureRecordSection({ case "beforeLecture": case "onLecture": return "강의가 종료된 후 강의 녹음을 확인할 수 있습니다. "; - case "makeQuiz": - case "checkDashboard": + case "afterLectureBeforeQuiz": + case "quizReadyForSubmission": + case "viewMyQuizResult": return null; default: - return "강의 중"; + return "강의가 종료된 후 강의 녹음을 확인할 수 있습니다. "; } }; From 9f6f44f528277720ae50b649f0a3e3859920f105 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 28 Sep 2025 13:36:16 +0900 Subject: [PATCH 17/32] =?UTF-8?q?=F0=9F=92=84=20(#305)=20overflow-y=20scro?= =?UTF-8?q?ll=EC=97=90=EC=84=9C=20auto=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/student/lecture-detail/[lectureId]/page.module.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/student/lecture-detail/[lectureId]/page.module.scss b/frontend/app/student/lecture-detail/[lectureId]/page.module.scss index 61b01287..866c5d6e 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/page.module.scss +++ b/frontend/app/student/lecture-detail/[lectureId]/page.module.scss @@ -8,5 +8,5 @@ .content { height: 100%; margin-top: $spacing-lg; - overflow-y: scroll; + overflow-y: auto; } From 2d84ae5828e98ad9ad02e3baa9e8654e6d22150e Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 28 Sep 2025 14:09:01 +0900 Subject: [PATCH 18/32] =?UTF-8?q?=F0=9F=94=A7=20(#305)=20=EA=B0=95?= =?UTF-8?q?=EC=9D=98=20=EC=83=81=ED=83=9C=20=EA=B4=80=EB=A0=A8=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=EC=9D=84=20=ED=86=B5=ED=95=A9=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A6=AC=20=EB=B0=8F=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/hooks/useLectureStatusAction.ts | 7 +------ frontend/store/useLectureStatusStore.ts | 14 +++++--------- frontend/types/lectures/fetchLectureDetailTypes.ts | 8 +++++++- .../lectures/fetchStudentLectureDetailTypes.ts | 14 ++++++++------ 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/frontend/hooks/useLectureStatusAction.ts b/frontend/hooks/useLectureStatusAction.ts index 93822813..0151ed6d 100644 --- a/frontend/hooks/useLectureStatusAction.ts +++ b/frontend/hooks/useLectureStatusAction.ts @@ -2,12 +2,7 @@ import { useRouter } from "next/navigation"; import { ROUTES } from "@/constants/routes"; import dayjs from "dayjs"; import { ChevronRight } from "lucide-react"; - -export type LectureStatus = - | "beforeLecture" - | "onLecture" - | "makeQuiz" - | "checkDashboard"; +import { LectureStatus } from "@/types/lectures/fetchLectureDetailTypes"; interface UseLectureStatusActionProps { status: LectureStatus; diff --git a/frontend/store/useLectureStatusStore.ts b/frontend/store/useLectureStatusStore.ts index 86a1a32f..4dc32afb 100644 --- a/frontend/store/useLectureStatusStore.ts +++ b/frontend/store/useLectureStatusStore.ts @@ -1,19 +1,15 @@ +import { StudentLectureStatus } from "@/types/lectures/fetchStudentLectureDetailTypes"; import { create } from "zustand"; -export type LectureStatus = - | "beforeLecture" - | "onLecture" - | "makeQuiz" - | "checkDashboard"; - interface LectureStatusStore { - lectureStatus: LectureStatus | null; - setLectureStatus: (status: LectureStatus) => void; + lectureStatus: StudentLectureStatus | null; + setLectureStatus: (status: StudentLectureStatus) => void; clearLectureStatus: () => void; } export const useLectureStatusStore = create((set) => ({ lectureStatus: null, - setLectureStatus: (status: LectureStatus) => set({ lectureStatus: status }), + setLectureStatus: (status: StudentLectureStatus) => + set({ lectureStatus: status }), clearLectureStatus: () => set({ lectureStatus: null }), })); diff --git a/frontend/types/lectures/fetchLectureDetailTypes.ts b/frontend/types/lectures/fetchLectureDetailTypes.ts index 04d29aa3..9c78884f 100644 --- a/frontend/types/lectures/fetchLectureDetailTypes.ts +++ b/frontend/types/lectures/fetchLectureDetailTypes.ts @@ -7,5 +7,11 @@ export interface FetchLectureDetailResult { session: number; startTime: string; endTime: string; - status: "beforeLecture" | "onLecture" | "makeQuiz" | "checkDashboard"; + status: LectureStatus; } + +export type LectureStatus = + | "beforeLecture" + | "onLecture" + | "makeQuiz" + | "checkDashboard"; diff --git a/frontend/types/lectures/fetchStudentLectureDetailTypes.ts b/frontend/types/lectures/fetchStudentLectureDetailTypes.ts index 7eb94658..20d1eda2 100644 --- a/frontend/types/lectures/fetchStudentLectureDetailTypes.ts +++ b/frontend/types/lectures/fetchStudentLectureDetailTypes.ts @@ -7,10 +7,12 @@ export interface FetchStudentLectureDetailResult { session: number; startTime: string; endTime: string; - status: - | "beforeLecture" - | "onLecture" - | "afterLectureBeforeQuiz" - | "quizReadyForSubmission" - | "viewMyQuizResult"; + status: StudentLectureStatus; } + +export type StudentLectureStatus = + | "beforeLecture" + | "onLecture" + | "afterLectureBeforeQuiz" + | "quizReadyForSubmission" + | "viewMyQuizResult"; From 63e19c40c55ed2350c8845e10ded07a832b53f98 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 28 Sep 2025 14:09:14 +0900 Subject: [PATCH 19/32] =?UTF-8?q?=E2=9C=A8=20(#305)=EA=B0=95=EC=9D=98=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=EC=97=90=20=EB=94=B0=EB=A5=B8=20=ED=80=B4?= =?UTF-8?q?=EC=A6=88=20=EC=83=81=ED=83=9C=20=EA=B4=80=EB=A6=AC=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_components/QuizSection/QuizSection.tsx | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx b/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx index 6cb24ba9..6edae8cf 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx @@ -1,12 +1,43 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import { useLectureStatusStore } from "@/store/useLectureStatusStore"; +import dayjs from "dayjs"; interface QuizSectionProps { lectureId: string; } +export type QuizStatus = "notYet" | "solve" | "waitingResult" | "viewResult"; + export default function QuizSection({ lectureId }: QuizSectionProps) { const { lectureStatus } = useLectureStatusStore(); + const [quizStatus, setQuizStatus] = useState("notYet"); + + useEffect(() => { + const canViewResult = () => { + const now = dayjs( + new Date(new Date().toLocaleString("en-US", { timeZone: "Asia/Seoul" })) + ); + const midnight = now.startOf("day").add(1, "day"); + return now.isAfter(midnight); + }; + switch (lectureStatus) { + case "beforeLecture": + case "onLecture": + case "afterLectureBeforeQuiz": + setQuizStatus("notYet"); + break; + case "quizReadyForSubmission": + setQuizStatus("solve"); + break; + case "viewMyQuizResult": + if (canViewResult()) { + setQuizStatus("viewResult"); + } else { + setQuizStatus("waitingResult"); + } + break; + } + }, [lectureStatus]); return (
From dd2d35c63405bb61c4ac0c6661afcd4486a13334 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 28 Sep 2025 14:21:34 +0900 Subject: [PATCH 20/32] =?UTF-8?q?=F0=9F=94=A7=20(#305)=20=EA=B0=95?= =?UTF-8?q?=EC=9D=98=20=EC=A0=95=EB=B3=B4=20=EC=84=B9=EC=85=98=EC=97=90?= =?UTF-8?q?=EC=84=9C=20lectureDate=20=EC=83=81=ED=83=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EA=B4=80=EB=A0=A8=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LectureInfoSection/LectureInfoSection.tsx | 12 +++++------- .../LectureNoteListSection.tsx | 2 ++ .../_components/QuizSection/QuizSection.tsx | 11 ++++++++--- frontend/store/useLectureStatusStore.ts | 4 ++++ 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/LectureInfoSection/LectureInfoSection.tsx b/frontend/app/student/lecture-detail/[lectureId]/_components/LectureInfoSection/LectureInfoSection.tsx index 7c8435ee..3dc828a4 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/_components/LectureInfoSection/LectureInfoSection.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/LectureInfoSection/LectureInfoSection.tsx @@ -6,10 +6,7 @@ import { fetchStudentLectureDetail } from "@/api/lectures/fetchStudentLectureDet import { FetchStudentLectureDetailResult } from "@/types/lectures/fetchStudentLectureDetailTypes"; import LoadingSpinner from "@/components/LoadingSpinner/LoadingSpinner"; import NoDataView from "@/components/NoDataView/NoDataView"; -import { - useLectureStatusStore, - LectureStatus, -} from "@/store/useLectureStatusStore"; +import { useLectureStatusStore } from "@/store/useLectureStatusStore"; interface LectureInfoSectionProps { lectureId: string; @@ -18,7 +15,7 @@ interface LectureInfoSectionProps { export default function LectureInfoSection({ lectureId, }: LectureInfoSectionProps) { - const { setLectureStatus } = useLectureStatusStore(); + const { setLectureStatus, setLectureDate } = useLectureStatusStore(); const [lectureData, setLectureData] = useState(null); const [loading, setLoading] = useState(true); @@ -31,7 +28,8 @@ export default function LectureInfoSection({ const response = await fetchStudentLectureDetail(lectureId); if (response.isSuccess && response.result) { setLectureData(response.result); - setLectureStatus(response.result.status as LectureStatus); + setLectureStatus(response.result.status); + setLectureDate(response.result.lectureDate); } else { setError("강의 정보를 불러올 수 없습니다."); } @@ -43,7 +41,7 @@ export default function LectureInfoSection({ }; loadLectureData(); - }, [lectureId, setLectureStatus]); + }, [lectureId, setLectureStatus, setLectureDate]); const getStatusText = (status: string) => { switch (status) { case "beforeLecture": diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/LectureNoteListSection/LectureNoteListSection.tsx b/frontend/app/student/lecture-detail/[lectureId]/_components/LectureNoteListSection/LectureNoteListSection.tsx index a1f58cd3..ffb37a36 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/_components/LectureNoteListSection/LectureNoteListSection.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/LectureNoteListSection/LectureNoteListSection.tsx @@ -1,3 +1,5 @@ +"use client"; + import { fetchLectureNoteByLectureId } from "@/api/lectures/fetchLectureNoteByLectureId"; import FileDisplay from "@/components/FileDisplay/FileDisplay"; import LoadingSpinner from "@/components/LoadingSpinner/LoadingSpinner"; diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx b/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx index 6edae8cf..6591c7ac 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx @@ -1,3 +1,4 @@ +"use client"; import React, { useEffect, useState } from "react"; import { useLectureStatusStore } from "@/store/useLectureStatusStore"; import dayjs from "dayjs"; @@ -9,15 +10,17 @@ interface QuizSectionProps { export type QuizStatus = "notYet" | "solve" | "waitingResult" | "viewResult"; export default function QuizSection({ lectureId }: QuizSectionProps) { - const { lectureStatus } = useLectureStatusStore(); + const { lectureStatus, lectureDate } = useLectureStatusStore(); const [quizStatus, setQuizStatus] = useState("notYet"); useEffect(() => { + // lectureDate 당일의 밤 12시가 지나면 true, 아니면 false 반환 const canViewResult = () => { + if (!lectureDate) return false; + const midnight = dayjs(lectureDate + " 00:00").add(1, "day"); // 강의일의 다음날 0시(=강의일 밤 12시) const now = dayjs( new Date(new Date().toLocaleString("en-US", { timeZone: "Asia/Seoul" })) ); - const midnight = now.startOf("day").add(1, "day"); return now.isAfter(midnight); }; switch (lectureStatus) { @@ -32,12 +35,14 @@ export default function QuizSection({ lectureId }: QuizSectionProps) { case "viewMyQuizResult": if (canViewResult()) { setQuizStatus("viewResult"); + console.log("viewResult"); } else { setQuizStatus("waitingResult"); + console.log("waitingResult"); } break; } - }, [lectureStatus]); + }, [lectureStatus, lectureDate]); return (
diff --git a/frontend/store/useLectureStatusStore.ts b/frontend/store/useLectureStatusStore.ts index 4dc32afb..9e404af8 100644 --- a/frontend/store/useLectureStatusStore.ts +++ b/frontend/store/useLectureStatusStore.ts @@ -3,12 +3,16 @@ import { create } from "zustand"; interface LectureStatusStore { lectureStatus: StudentLectureStatus | null; + lectureDate: string | null; + setLectureDate: (date: string) => void; setLectureStatus: (status: StudentLectureStatus) => void; clearLectureStatus: () => void; } export const useLectureStatusStore = create((set) => ({ lectureStatus: null, + lectureDate: null, + setLectureDate: (date: string) => set({ lectureDate: date }), setLectureStatus: (status: StudentLectureStatus) => set({ lectureStatus: status }), clearLectureStatus: () => set({ lectureStatus: null }), From 6b315ab17b903363cd980cf433f5f80f9f4bd7e2 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 28 Sep 2025 14:32:27 +0900 Subject: [PATCH 21/32] =?UTF-8?q?=E2=9C=A8=20(#305)=20=ED=80=B4=EC=A6=88?= =?UTF-8?q?=20=EB=AA=A9=EB=A1=9D=20=EB=B0=9B=EC=95=84=EC=98=A4=EA=B8=B0=20?= =?UTF-8?q?api=20=ED=95=A8=EC=88=98=20+=20dto=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/api/quizzes/fetchQuizList.ts | 19 +++++++++++++++++++ frontend/types/quizzes/fetchQuizListTypes.ts | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 frontend/api/quizzes/fetchQuizList.ts create mode 100644 frontend/types/quizzes/fetchQuizListTypes.ts diff --git a/frontend/api/quizzes/fetchQuizList.ts b/frontend/api/quizzes/fetchQuizList.ts new file mode 100644 index 00000000..427daa6c --- /dev/null +++ b/frontend/api/quizzes/fetchQuizList.ts @@ -0,0 +1,19 @@ +import axios from "axios"; +import { axiosInstance } from "@/api/axiosInstance"; +import { ENDPOINTS } from "@/constants/endpoints"; +import { ApiResponse } from "@/types/apiResponseTypes"; +import { fetchQuizListResult } from "@/types/quizzes/fetchQuizListTypes"; + +export async function fetchQuizList(lectureId: string) { + try { + const response = await axiosInstance.get>( + ENDPOINTS.QUIZZES.GET(lectureId) + ); + return response.data; + } catch (error: unknown) { + if (axios.isAxiosError(error) && error.response) { + return error.response.data as ApiResponse; + } + throw error; + } +} diff --git a/frontend/types/quizzes/fetchQuizListTypes.ts b/frontend/types/quizzes/fetchQuizListTypes.ts new file mode 100644 index 00000000..35bed592 --- /dev/null +++ b/frontend/types/quizzes/fetchQuizListTypes.ts @@ -0,0 +1,19 @@ +export interface fetchQuizListResult { + lectureId: string; + quizList: Quiz[]; +} + +type Quiz = { + quizId: string; + quizOrder: number; + quizBody: string; + solution: string; + type: "multipleChoice" | "shortAnswer" | "trueFalse"; + options: QuizOption[] | []; +}; + +type QuizOption = { + id: string; + optionOrder: number; + text: string; +}; From 18671fe217b1954318344767eed6395573dd2d58 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 28 Sep 2025 14:49:02 +0900 Subject: [PATCH 22/32] =?UTF-8?q?=E2=9C=A8=20(#305)=20=ED=80=B4=EC=A6=88?= =?UTF-8?q?=20=EC=A0=9C=EC=B6=9C=20API=20=ED=95=A8=EC=88=98=20=EB=B0=8F=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=ED=83=80=EC=9E=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/api/quizzes/submitQuiz.ts | 23 +++++++++++++++++++++++ frontend/constants/endpoints.ts | 2 +- frontend/types/quizzes/submitQuizTypes.ts | 13 +++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 frontend/api/quizzes/submitQuiz.ts create mode 100644 frontend/types/quizzes/submitQuizTypes.ts diff --git a/frontend/api/quizzes/submitQuiz.ts b/frontend/api/quizzes/submitQuiz.ts new file mode 100644 index 00000000..2d8122a6 --- /dev/null +++ b/frontend/api/quizzes/submitQuiz.ts @@ -0,0 +1,23 @@ +import axios from "axios"; +import { axiosInstance } from "@/api/axiosInstance"; +import { ENDPOINTS } from "@/constants/endpoints"; +import { ApiResponse } from "@/types/apiResponseTypes"; +import { + SubmitQuizRequest, + SubmitQuizResult, +} from "@/types/quizzes/submitQuizTypes"; + +export async function submitQuiz(data: SubmitQuizRequest) { + try { + const response = await axiosInstance.post< + ApiResponse + >(ENDPOINTS.QUIZZES.SUBMIT, data); + + return response.data; + } catch (error: unknown) { + if (axios.isAxiosError(error) && error.response) { + return error.response.data as ApiResponse; + } + throw error; + } +} diff --git a/frontend/constants/endpoints.ts b/frontend/constants/endpoints.ts index 64e74781..9e796354 100644 --- a/frontend/constants/endpoints.ts +++ b/frontend/constants/endpoints.ts @@ -106,7 +106,7 @@ export const ENDPOINTS = { SAVE: (lectureId: string) => `${BASE_API}/quizzes/${lectureId}/save`, UPDATE: (lectureId: string) => `${BASE_API}/quizzes/${lectureId}`, GET: (lectureId: string) => `${BASE_API}/quizzes/${lectureId}`, - SUBMIT: (lectureId: string) => `${BASE_API}/quizzes/${lectureId}/submit`, + SUBMIT: `${BASE_API}/quizzes/submit`, GET_RESULT: (lectureId: string) => `${BASE_API}/quizzes/${lectureId}/result`, }, diff --git a/frontend/types/quizzes/submitQuizTypes.ts b/frontend/types/quizzes/submitQuizTypes.ts new file mode 100644 index 00000000..9fc12cb1 --- /dev/null +++ b/frontend/types/quizzes/submitQuizTypes.ts @@ -0,0 +1,13 @@ +export interface SubmitQuizRequest { + answers: Answer[]; +} + +type Answer = { + quizId: string; + answer: string; +}; + +export interface SubmitQuizResult { + userId: string; + savedCount: number; +} From 425033e572138698da1a15a3ab68bad77a31ff92 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 28 Sep 2025 15:07:02 +0900 Subject: [PATCH 23/32] =?UTF-8?q?=E2=9C=A8=20(#305)=20=ED=80=B4=EC=A6=88?= =?UTF-8?q?=20=EA=B2=B0=EA=B3=BC=20=EC=A1=B0=ED=9A=8C=20API=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=EB=B0=8F=20=EA=B4=80=EB=A0=A8=20=ED=83=80=EC=9E=85?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/api/quizzes/getMyQuizResult.ts | 19 ++++++++ .../types/quizzes/getMyQuizResultTypes.ts | 45 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 frontend/api/quizzes/getMyQuizResult.ts create mode 100644 frontend/types/quizzes/getMyQuizResultTypes.ts diff --git a/frontend/api/quizzes/getMyQuizResult.ts b/frontend/api/quizzes/getMyQuizResult.ts new file mode 100644 index 00000000..6a35ed46 --- /dev/null +++ b/frontend/api/quizzes/getMyQuizResult.ts @@ -0,0 +1,19 @@ +import axios from "axios"; +import { axiosInstance } from "@/api/axiosInstance"; +import { ENDPOINTS } from "@/constants/endpoints"; +import { ApiResponse } from "@/types/apiResponseTypes"; +import { getMyQuizResultResult } from "@/types/quizzes/getMyQuizResultTypes"; + +export async function getMyQuizResult(lectureId: string) { + try { + const response = await axiosInstance.get< + ApiResponse + >(ENDPOINTS.QUIZZES.GET_RESULT(lectureId)); + return response.data; + } catch (error: unknown) { + if (axios.isAxiosError(error) && error.response) { + return error.response.data as ApiResponse; + } + throw error; + } +} diff --git a/frontend/types/quizzes/getMyQuizResultTypes.ts b/frontend/types/quizzes/getMyQuizResultTypes.ts new file mode 100644 index 00000000..1cb395fb --- /dev/null +++ b/frontend/types/quizzes/getMyQuizResultTypes.ts @@ -0,0 +1,45 @@ +export interface getMyQuizResultResult { + lectureId: string; + quizzes: Quiz[]; +} + +type Quiz = MultipleChoiceQuiz | ShortAnswerQuiz | TrueFalseQuiz; + +type MultipleChoiceQuiz = { + quizId: string; + quizOrder: number; + quizBody: string; + solution: string; + type: "multipleChoice"; + studentAnswer: string; + options: QuizOption[]; + isCollect: boolean; +}; + +type ShortAnswerQuiz = { + quizId: string; + quizOrder: number; + quizBody: string; + solution: string; + type: "shortAnswer"; + studentAnswer: string; + options: []; + isCollect: boolean; +}; + +type TrueFalseQuiz = { + quizId: string; + quizOrder: number; + quizBody: string; + solution: string; + type: "trueFalse"; + studentAnswer: string; + options: []; + isCollect: boolean; +}; + +type QuizOption = { + id: string; + optionOrder: number; + text: string; +}; From c119f79410bcbb6105d9ec001689f9d260175a98 Mon Sep 17 00:00:00 2001 From: Haemin Kim Date: Sun, 28 Sep 2025 15:27:30 +0900 Subject: [PATCH 24/32] =?UTF-8?q?:recycle:=20(#328)=20=ED=80=B4=EC=A6=88?= =?UTF-8?q?=20=EB=AC=B8=EC=A0=9C=20=EC=A0=95=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/quiz/converter/QuizConverter.java | 46 ++++++++++--------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/backend/src/main/java/org/example/backend/domain/quiz/converter/QuizConverter.java b/backend/src/main/java/org/example/backend/domain/quiz/converter/QuizConverter.java index 7d89b7ca..c574a0af 100644 --- a/backend/src/main/java/org/example/backend/domain/quiz/converter/QuizConverter.java +++ b/backend/src/main/java/org/example/backend/domain/quiz/converter/QuizConverter.java @@ -20,27 +20,31 @@ public class QuizConverter { private final OptionRepository optionRepository; public QuizListResponseDTO toQuizListResponseDTO(UUID lectureId, List quizList) { - List quizDTOs = quizList.stream().map(quiz -> { - List options = new ArrayList<>(); - if (quiz.getType() == QuizType.MULTIPLE_CHOICE) { - options = optionRepository.findByQuizId(quiz.getId()) - .stream() - .map(option -> new OptionResponseDTO( - option.getId(), - option.getOptionOrder(), - option.getText() - )) - .toList(); - } - return new QuizListResponseDTO.QuizDTO( - quiz.getId(), - quiz.getQuizOrder(), - quiz.getQuiz(), - quiz.getSolution(), - QuizResultStudentConverter.toCamelCase(quiz.getType()), - options - ); - }).toList(); + List quizDTOs = quizList.stream() + .sorted((q1, q2) -> Integer.compare(q1.getQuizOrder(), q2.getQuizOrder())) + .map(quiz -> { + List options = new ArrayList<>(); + if (quiz.getType() == QuizType.MULTIPLE_CHOICE) { + options = optionRepository.findByQuizId(quiz.getId()) + .stream() + .map(option -> new OptionResponseDTO( + option.getId(), + option.getOptionOrder(), + option.getText() + )) + .sorted((o1, o2) -> Integer.compare(o1.getOptionOrder(), o2.getOptionOrder())) + .toList(); + } + return new QuizListResponseDTO.QuizDTO( + quiz.getId(), + quiz.getQuizOrder(), + quiz.getQuiz(), + quiz.getSolution(), + QuizResultStudentConverter.toCamelCase(quiz.getType()), + options + ); + }) + .toList(); return new QuizListResponseDTO(lectureId, quizDTOs); } From 952a9a3611d7673f09fb602728b2f2a4a4a5b5f3 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 28 Sep 2025 15:30:45 +0900 Subject: [PATCH 25/32] =?UTF-8?q?=E2=9C=A8=20(#305)=20=ED=80=B4=EC=A6=88?= =?UTF-8?q?=20=EC=84=B9=EC=85=98=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=8A=A4=ED=83=80=EC=9D=BC?= =?UTF-8?q?=EB=A7=81,=20=ED=80=B4=EC=A6=88=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=20=EB=A1=9C=EB=93=9C=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QuizSection/QuizSection.module.scss | 5 + .../_components/QuizSection/QuizSection.tsx | 109 ++++++++++++++++-- .../QuizToggleCard/QuizToggleCard.tsx | 2 +- frontend/types/quizzes/fetchQuizListTypes.ts | 2 +- 4 files changed, 108 insertions(+), 10 deletions(-) create mode 100644 frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.module.scss diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.module.scss b/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.module.scss new file mode 100644 index 00000000..f1144bf4 --- /dev/null +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.module.scss @@ -0,0 +1,5 @@ +.QuizSection { + display: flex; + flex-direction: column; + gap: $spacing-md; +} diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx b/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx index 6591c7ac..07acd3f7 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx @@ -2,6 +2,13 @@ import React, { useEffect, useState } from "react"; import { useLectureStatusStore } from "@/store/useLectureStatusStore"; import dayjs from "dayjs"; +import { fetchQuizList } from "@/api/quizzes/fetchQuizList"; +import { fetchQuizListResult } from "@/types/quizzes/fetchQuizListTypes"; +import QuizToggleCard from "@/components/QuizToggleCard/QuizToggleCard"; +import styles from "./QuizSection.module.scss"; +import LoadingSpinner from "@/components/LoadingSpinner/LoadingSpinner"; +import NoDataView from "@/components/NoDataView/NoDataView"; +import { CheckCircle, Clock } from "lucide-react"; interface QuizSectionProps { lectureId: string; @@ -12,12 +19,16 @@ export type QuizStatus = "notYet" | "solve" | "waitingResult" | "viewResult"; export default function QuizSection({ lectureId }: QuizSectionProps) { const { lectureStatus, lectureDate } = useLectureStatusStore(); const [quizStatus, setQuizStatus] = useState("notYet"); + const [quizData, setQuizData] = useState(null); + const [userAnswers, setUserAnswers] = useState<{ [quizId: string]: string }>( + {} + ); + const [loading, setLoading] = useState(false); useEffect(() => { - // lectureDate 당일의 밤 12시가 지나면 true, 아니면 false 반환 const canViewResult = () => { if (!lectureDate) return false; - const midnight = dayjs(lectureDate + " 00:00").add(1, "day"); // 강의일의 다음날 0시(=강의일 밤 12시) + const midnight = dayjs(lectureDate + " 00:00").add(1, "day"); const now = dayjs( new Date(new Date().toLocaleString("en-US", { timeZone: "Asia/Seoul" })) ); @@ -35,21 +46,103 @@ export default function QuizSection({ lectureId }: QuizSectionProps) { case "viewMyQuizResult": if (canViewResult()) { setQuizStatus("viewResult"); - console.log("viewResult"); } else { setQuizStatus("waitingResult"); - console.log("waitingResult"); } break; } }, [lectureStatus, lectureDate]); + // quizStatus가 solve로 변경될 때 퀴즈 데이터 로드 + useEffect(() => { + const loadQuizData = async () => { + if (quizStatus !== "solve") return; + + setLoading(true); + try { + const response = await fetchQuizList(lectureId); + if (response.isSuccess && response.result) { + setQuizData(response.result); + } + } catch (error) { + console.error("퀴즈 데이터 로드 실패:", error); + } finally { + setLoading(false); + } + }; + + loadQuizData(); + }, [quizStatus, lectureId]); + + // 퀴즈 답변 선택 핸들러 + const handleQuizSelect = (quizId: string, answer: string) => { + setUserAnswers((prev) => ({ + ...prev, + [quizId]: answer, + })); + }; + + // 단답형 퀴즈 입력 변경 핸들러 + const handleQuizInputChange = (quizId: string, inputAnswer: string) => { + setUserAnswers((prev) => ({ + ...prev, + [quizId]: inputAnswer, + })); + }; + + if (loading) { + return ; + } + return (
-

복습 퀴즈

-

현재 강의 상태: {lectureStatus}

-

강의 ID: {lectureId}

- {/* 여기에 복습 퀴즈 관련 로직 추가 */} + {quizStatus === "solve" && quizData && ( +
+ {quizData.quizzes.map((quiz) => ( + option.text) + } + onSelect={(label) => handleQuizSelect(quiz.quizId, label)} + onInputChange={(inputAnswer) => + handleQuizInputChange(quiz.quizId, inputAnswer) + } + /> + ))} +
+ )} + + {quizStatus === "notYet" && ( + + )} + + {quizStatus === "waitingResult" && ( + + )} + + {quizStatus === "viewResult" && ( + + )}
); } diff --git a/frontend/components/QuizToggleCard/QuizToggleCard.tsx b/frontend/components/QuizToggleCard/QuizToggleCard.tsx index afb5625d..76122121 100644 --- a/frontend/components/QuizToggleCard/QuizToggleCard.tsx +++ b/frontend/components/QuizToggleCard/QuizToggleCard.tsx @@ -89,7 +89,7 @@ const QuizToggleCard: React.FC = (props) => { return (
{props.type === "multipleChoice" || props.type === "trueFalse" ? ( - getLabels()?.map((label) => ( + props.labels?.map((label) => ( Date: Sun, 28 Sep 2025 15:55:51 +0900 Subject: [PATCH 26/32] =?UTF-8?q?=E2=9C=A8=20(#305)=20=ED=80=B4=EC=A6=88?= =?UTF-8?q?=20=EC=84=B9=EC=85=98=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=20=EB=B0=8F=20=EC=83=81=ED=83=9C=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8=20UI=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8,=20?= =?UTF-8?q?=ED=80=B4=EC=A6=88=20=EC=A0=9C=EC=B6=9C=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QuizSection/QuizSection.module.scss | 16 +++- .../_components/QuizSection/QuizSection.tsx | 75 ++++++++++++------- .../FullWidthButton.module.scss | 1 - 3 files changed, 64 insertions(+), 28 deletions(-) diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.module.scss b/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.module.scss index f1144bf4..0e51539c 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.module.scss +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.module.scss @@ -1,5 +1,19 @@ -.QuizSection { +.quizSection { + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.quizContainer { display: flex; flex-direction: column; gap: $spacing-md; + height: 100%; + overflow-y: auto; +} + +.buttonContainer { + margin-top: $spacing-md; + flex: 1; } diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx b/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx index 07acd3f7..d746feea 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx @@ -9,6 +9,7 @@ import styles from "./QuizSection.module.scss"; import LoadingSpinner from "@/components/LoadingSpinner/LoadingSpinner"; import NoDataView from "@/components/NoDataView/NoDataView"; import { CheckCircle, Clock } from "lucide-react"; +import FullWidthButton from "@/components/Button/FullWidthButton/FullWidthButton"; interface QuizSectionProps { lectureId: string; @@ -94,10 +95,46 @@ export default function QuizSection({ lectureId }: QuizSectionProps) { return ; } + if (quizStatus === "notYet") { + return ( +
+ +
+ ); + } + + if (quizStatus === "waitingResult") { + return ( +
+ +
+ ); + } + + if (quizStatus === "viewResult") { + return ( +
+ +
+ ); + } + return ( -
+
{quizStatus === "solve" && quizData && ( -
+
{quizData.quizzes.map((quiz) => ( )} - - {quizStatus === "notYet" && ( - - )} - - {quizStatus === "waitingResult" && ( - - )} - - {quizStatus === "viewResult" && ( - - )} +
+ {}} + disabled={ + Object.keys(userAnswers).length !== quizData?.quizzes.length + } + > + 퀴즈 제출 + +
); } diff --git a/frontend/components/Button/FullWidthButton/FullWidthButton.module.scss b/frontend/components/Button/FullWidthButton/FullWidthButton.module.scss index 3382c02b..4d123882 100644 --- a/frontend/components/Button/FullWidthButton/FullWidthButton.module.scss +++ b/frontend/components/Button/FullWidthButton/FullWidthButton.module.scss @@ -13,7 +13,6 @@ &:hover { background-color: $color-lightblue; - transform: scale(1.02); cursor: pointer; } From 58ee79b743a3f4fc02723b54c8b92628115c670a Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 28 Sep 2025 16:23:58 +0900 Subject: [PATCH 27/32] =?UTF-8?q?=E2=9C=A8=20(#305)=20=ED=80=B4=EC=A6=88?= =?UTF-8?q?=20=EC=A0=9C=EC=B6=9C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20=EB=B0=8F=20=EA=B2=B0=EA=B3=BC=20=EB=AA=A8=EB=8B=AC=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84,=20=ED=80=B4=EC=A6=88=20=EC=84=B9=EC=85=98?= =?UTF-8?q?=20=EB=A6=AC=ED=94=84=EB=A0=88=EC=8B=9C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_components/QuizSection/QuizSection.tsx | 65 +++++++++++++++++-- .../lecture-detail/[lectureId]/page.tsx | 9 ++- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx b/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx index d746feea..13003297 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx @@ -4,20 +4,27 @@ import { useLectureStatusStore } from "@/store/useLectureStatusStore"; import dayjs from "dayjs"; import { fetchQuizList } from "@/api/quizzes/fetchQuizList"; import { fetchQuizListResult } from "@/types/quizzes/fetchQuizListTypes"; +import { submitQuiz } from "@/api/quizzes/submitQuiz"; +import { SubmitQuizRequest } from "@/types/quizzes/submitQuizTypes"; import QuizToggleCard from "@/components/QuizToggleCard/QuizToggleCard"; import styles from "./QuizSection.module.scss"; import LoadingSpinner from "@/components/LoadingSpinner/LoadingSpinner"; import NoDataView from "@/components/NoDataView/NoDataView"; +import AlertModal from "@/components/Modal/AlertModal/AlertModal"; import { CheckCircle, Clock } from "lucide-react"; import FullWidthButton from "@/components/Button/FullWidthButton/FullWidthButton"; interface QuizSectionProps { lectureId: string; + onRefresh?: () => void; } export type QuizStatus = "notYet" | "solve" | "waitingResult" | "viewResult"; -export default function QuizSection({ lectureId }: QuizSectionProps) { +export default function QuizSection({ + lectureId, + onRefresh, +}: QuizSectionProps) { const { lectureStatus, lectureDate } = useLectureStatusStore(); const [quizStatus, setQuizStatus] = useState("notYet"); const [quizData, setQuizData] = useState(null); @@ -25,6 +32,10 @@ export default function QuizSection({ lectureId }: QuizSectionProps) { {} ); const [loading, setLoading] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [showResultModal, setShowResultModal] = useState(false); + const [resultMessage, setResultMessage] = useState(""); + const [refreshKey, setRefreshKey] = useState(0); useEffect(() => { const canViewResult = () => { @@ -91,6 +102,37 @@ export default function QuizSection({ lectureId }: QuizSectionProps) { })); }; + // 퀴즈 제출 핸들러 + const handleSubmitQuiz = async () => { + if (!quizData) return; + + setSubmitting(true); + try { + const submitData: SubmitQuizRequest = { + answers: Object.entries(userAnswers).map(([quizId, answer]) => ({ + quizId, + answer, + })), + }; + + const response = await submitQuiz(submitData); + + if (response.isSuccess && response.result) { + setResultMessage( + `성공적으로 제출되었습니다. 12시 이후 퀴즈 결과를 확인할 수 있습니다.` + ); + } else { + setResultMessage(response.message || "퀴즈 제출에 실패했습니다."); + } + } catch (error) { + console.error("퀴즈 제출 오류:", error); + setResultMessage("퀴즈 제출 중 오류가 발생했습니다."); + } finally { + setSubmitting(false); + setShowResultModal(true); + } + }; + if (loading) { return ; } @@ -132,7 +174,7 @@ export default function QuizSection({ lectureId }: QuizSectionProps) { } return ( -
+
{quizStatus === "solve" && quizData && (
{quizData.quizzes.map((quiz) => ( @@ -158,14 +200,27 @@ export default function QuizSection({ lectureId }: QuizSectionProps) { )}
{}} + onClick={handleSubmitQuiz} disabled={ - Object.keys(userAnswers).length !== quizData?.quizzes.length + Object.keys(userAnswers).length !== quizData?.quizzes.length || + submitting } > - 퀴즈 제출 + {submitting ? "제출 중..." : "퀴즈 제출"}
+ + {showResultModal && ( + { + setShowResultModal(false); + setRefreshKey((prev) => prev + 1); + onRefresh?.(); + }} + > + {resultMessage} + + )}
); } diff --git a/frontend/app/student/lecture-detail/[lectureId]/page.tsx b/frontend/app/student/lecture-detail/[lectureId]/page.tsx index 7349a040..e76de44d 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/page.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/page.tsx @@ -15,6 +15,7 @@ export default function StudentLectureDetailPage() { const { setClassTitle } = useClassTitleStore(); const lectureId = params.lectureId as string; const [selectedTab, setSelectedTab] = useState(0); + const [quizRefreshKey, setQuizRefreshKey] = useState(0); // TODO: lectureId로 클래스타이틀 불러오고, 이를 setClassTitle 이용해서 스토어에 저장해야 함 @@ -34,7 +35,13 @@ export default function StudentLectureDetailPage() { case 2: return ; case 3: - return ; + return ( + setQuizRefreshKey((prev) => prev + 1)} + /> + ); default: return ; } From dc2c788a2c2544713047b1d9f55e91442646b085 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 28 Sep 2025 16:44:42 +0900 Subject: [PATCH 28/32] =?UTF-8?q?=E2=9C=A8=20(#305)=20=EA=B0=95=EC=9D=98?= =?UTF-8?q?=20=EB=AA=A9=EB=A1=9D=20=EC=84=B9=EC=85=98=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EA=B5=90=EC=88=98=20=EC=9D=B4=EB=A6=84=20=EB=B0=8F=20=EA=B0=95?= =?UTF-8?q?=EC=9D=98=20=EB=82=A0=EC=A7=9C=20=EC=A0=95=EB=B3=B4=EB=A5=BC=20?= =?UTF-8?q?=EB=8F=99=EC=A0=81=EC=9C=BC=EB=A1=9C=20=ED=91=9C=EC=8B=9C?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_components/ClassListSection/ClassListSection.tsx | 10 +++++++--- frontend/types/classes/fetchMyClassListTypes.ts | 1 + 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/frontend/app/student/class/_components/ClassListSection/ClassListSection.tsx b/frontend/app/student/class/_components/ClassListSection/ClassListSection.tsx index e7d198b2..c82e4e2a 100644 --- a/frontend/app/student/class/_components/ClassListSection/ClassListSection.tsx +++ b/frontend/app/student/class/_components/ClassListSection/ClassListSection.tsx @@ -86,7 +86,9 @@ export default function ClassListSection() {
{classItem.className} - 박재성 + + {classItem.professorName} +
@@ -94,12 +96,14 @@ export default function ClassListSection() {
- 월 (10:15~11:45)/수 (12:00~13:15) + {classItem.classDate}
- 2024.03.04 ~ 2025.06.13 + + {classItem.startDate} ~ {classItem.endDate} +
diff --git a/frontend/types/classes/fetchMyClassListTypes.ts b/frontend/types/classes/fetchMyClassListTypes.ts index 13b91b95..65ad7922 100644 --- a/frontend/types/classes/fetchMyClassListTypes.ts +++ b/frontend/types/classes/fetchMyClassListTypes.ts @@ -4,4 +4,5 @@ export interface FetchMyClassListResult { startDate: string; endDate: string; classDate: string; + professorName: string; } From 639214cd4153aa35bc07ff1ac152dd262f45b20c Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 28 Sep 2025 16:46:10 +0900 Subject: [PATCH 29/32] =?UTF-8?q?=E2=9C=A8=20(#305)=20=EA=B0=95=EC=9D=98?= =?UTF-8?q?=20=EC=83=81=EC=84=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90?= =?UTF-8?q?=EC=84=9C=20lectureId=EB=A5=BC=20=EA=B8=B0=EB=B0=98=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=EC=9D=84=20=EB=B6=88=EB=9F=AC=EC=98=A4=EB=8A=94=20API=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/classes/fetchClassNameByLectureId.ts | 20 +++++++++ .../_components/QuizSection/QuizSection.tsx | 41 ++++++++++++------- .../lecture-detail/[lectureId]/page.tsx | 16 +++++++- frontend/constants/endpoints.ts | 1 + .../classes/fetchClassNameByLectureIdTypes.ts | 3 ++ 5 files changed, 65 insertions(+), 16 deletions(-) create mode 100644 frontend/api/classes/fetchClassNameByLectureId.ts create mode 100644 frontend/types/classes/fetchClassNameByLectureIdTypes.ts diff --git a/frontend/api/classes/fetchClassNameByLectureId.ts b/frontend/api/classes/fetchClassNameByLectureId.ts new file mode 100644 index 00000000..30b95e76 --- /dev/null +++ b/frontend/api/classes/fetchClassNameByLectureId.ts @@ -0,0 +1,20 @@ +import { axiosInstance } from "@/api/axiosInstance"; +import axios from "axios"; +import { ENDPOINTS } from "@/constants/endpoints"; +import { ApiResponse } from "@/types/apiResponseTypes"; +import { FetchClassNameByLectureIdResult } from "@/types/classes/fetchClassNameByLectureIdTypes"; + +export async function fetchClassNameByLectureId(lectureId: string) { + try { + const response = await axiosInstance.get< + ApiResponse + >(ENDPOINTS.CLASSES.GET_CLASS_NAME(lectureId)); + return response.data; + } catch (error: unknown) { + if (axios.isAxiosError(error) && error.response) { + return error.response + .data as ApiResponse; + } + throw error; + } +} diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx b/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx index 13003297..d8ec5583 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx @@ -1,19 +1,29 @@ "use client"; import React, { useEffect, useState } from "react"; -import { useLectureStatusStore } from "@/store/useLectureStatusStore"; import dayjs from "dayjs"; +import { CheckCircle, Clock } from "lucide-react"; + +// API import { fetchQuizList } from "@/api/quizzes/fetchQuizList"; -import { fetchQuizListResult } from "@/types/quizzes/fetchQuizListTypes"; import { submitQuiz } from "@/api/quizzes/submitQuiz"; + +// Types +import { fetchQuizListResult } from "@/types/quizzes/fetchQuizListTypes"; import { SubmitQuizRequest } from "@/types/quizzes/submitQuizTypes"; + +// Components import QuizToggleCard from "@/components/QuizToggleCard/QuizToggleCard"; -import styles from "./QuizSection.module.scss"; import LoadingSpinner from "@/components/LoadingSpinner/LoadingSpinner"; import NoDataView from "@/components/NoDataView/NoDataView"; import AlertModal from "@/components/Modal/AlertModal/AlertModal"; -import { CheckCircle, Clock } from "lucide-react"; import FullWidthButton from "@/components/Button/FullWidthButton/FullWidthButton"; +// Store +import { useLectureStatusStore } from "@/store/useLectureStatusStore"; + +// Styles +import styles from "./QuizSection.module.scss"; + interface QuizSectionProps { lectureId: string; onRefresh?: () => void; @@ -37,6 +47,7 @@ export default function QuizSection({ const [resultMessage, setResultMessage] = useState(""); const [refreshKey, setRefreshKey] = useState(0); + // 강의 상태에 따른 퀴즈 상태 설정 useEffect(() => { const canViewResult = () => { if (!lectureDate) return false; @@ -46,6 +57,7 @@ export default function QuizSection({ ); return now.isAfter(midnight); }; + switch (lectureStatus) { case "beforeLecture": case "onLecture": @@ -56,16 +68,12 @@ export default function QuizSection({ setQuizStatus("solve"); break; case "viewMyQuizResult": - if (canViewResult()) { - setQuizStatus("viewResult"); - } else { - setQuizStatus("waitingResult"); - } + setQuizStatus(canViewResult() ? "viewResult" : "waitingResult"); break; } }, [lectureStatus, lectureDate]); - // quizStatus가 solve로 변경될 때 퀴즈 데이터 로드 + // 퀴즈 데이터 로드 useEffect(() => { const loadQuizData = async () => { if (quizStatus !== "solve") return; @@ -86,7 +94,7 @@ export default function QuizSection({ loadQuizData(); }, [quizStatus, lectureId]); - // 퀴즈 답변 선택 핸들러 + // 퀴즈 답변 핸들러 const handleQuizSelect = (quizId: string, answer: string) => { setUserAnswers((prev) => ({ ...prev, @@ -94,7 +102,6 @@ export default function QuizSection({ })); }; - // 단답형 퀴즈 입력 변경 핸들러 const handleQuizInputChange = (quizId: string, inputAnswer: string) => { setUserAnswers((prev) => ({ ...prev, @@ -102,7 +109,7 @@ export default function QuizSection({ })); }; - // 퀴즈 제출 핸들러 + // 퀴즈 제출 const handleSubmitQuiz = async () => { if (!quizData) return; @@ -119,7 +126,7 @@ export default function QuizSection({ if (response.isSuccess && response.result) { setResultMessage( - `성공적으로 제출되었습니다. 12시 이후 퀴즈 결과를 확인할 수 있습니다.` + "성공적으로 제출되었습니다. 12시 이후 퀴즈 결과를 확인할 수 있습니다." ); } else { setResultMessage(response.message || "퀴즈 제출에 실패했습니다."); @@ -133,10 +140,12 @@ export default function QuizSection({ } }; + // 로딩 상태 if (loading) { return ; } + // 퀴즈 시작 전 if (quizStatus === "notYet") { return (
@@ -149,6 +158,7 @@ export default function QuizSection({ ); } + // 결과 대기 중 if (quizStatus === "waitingResult") { return (
@@ -161,6 +171,7 @@ export default function QuizSection({ ); } + // 결과 확인 가능 if (quizStatus === "viewResult") { return (
@@ -173,6 +184,7 @@ export default function QuizSection({ ); } + // 퀴즈 풀이 화면 return (
{quizStatus === "solve" && quizData && ( @@ -198,6 +210,7 @@ export default function QuizSection({ ))}
)} +
{ + (async () => { + try { + const res = await fetchClassNameByLectureId(lectureId); + if (res.isSuccess) { + setClassTitle(res.result?.className || ""); + } + } catch (error) { + console.error("클래스 이름을 불러오는 중 오류 발생:", error); + } + })(); + }, [lectureId, setClassTitle]); const tabs = ["수업 자료", "질문하기", "강의 녹음", "복습 퀴즈"]; diff --git a/frontend/constants/endpoints.ts b/frontend/constants/endpoints.ts index 9e796354..b89885dc 100644 --- a/frontend/constants/endpoints.ts +++ b/frontend/constants/endpoints.ts @@ -48,6 +48,7 @@ export const ENDPOINTS = { GET_ALL_NOTES: (classId: string) => `${BASE_API}/classes/${classId}/notes`, GET_MY_CLASSES: `${BASE_API}/classes/teacher/myclass`, GET_QUIZZES: (classId: string) => `${BASE_API}/classes/${classId}/quiz`, + GET_CLASS_NAME: (lectureId: string) => `${BASE_API}/classes/${lectureId}`, }, // 학생 클래스 관련 diff --git a/frontend/types/classes/fetchClassNameByLectureIdTypes.ts b/frontend/types/classes/fetchClassNameByLectureIdTypes.ts new file mode 100644 index 00000000..f268863e --- /dev/null +++ b/frontend/types/classes/fetchClassNameByLectureIdTypes.ts @@ -0,0 +1,3 @@ +export interface FetchClassNameByLectureIdResult { + className: string; +} From a4b50144ea93eaad54fcd7ec309ed946fce38972 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 28 Sep 2025 16:53:13 +0900 Subject: [PATCH 30/32] =?UTF-8?q?=F0=9F=94=A8=20(#305)=20=ED=80=B4?= =?UTF-8?q?=EC=A6=88=20=EC=84=B9=EC=85=98=EC=9D=98=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=EC=97=90=20'before'=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=EC=83=81=ED=83=9C=EC=97=90=20=EB=94=B0=EB=A5=B8=20?= =?UTF-8?q?UI=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_components/QuizSection/QuizSection.tsx | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx b/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx index d8ec5583..771702a6 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx @@ -29,7 +29,12 @@ interface QuizSectionProps { onRefresh?: () => void; } -export type QuizStatus = "notYet" | "solve" | "waitingResult" | "viewResult"; +export type QuizStatus = + | "before" + | "notYet" + | "solve" + | "waitingResult" + | "viewResult"; export default function QuizSection({ lectureId, @@ -61,6 +66,8 @@ export default function QuizSection({ switch (lectureStatus) { case "beforeLecture": case "onLecture": + setQuizStatus("before"); + break; case "afterLectureBeforeQuiz": setQuizStatus("notYet"); break; @@ -146,7 +153,7 @@ export default function QuizSection({ } // 퀴즈 시작 전 - if (quizStatus === "notYet") { + if (quizStatus === "before") { return (
+ +
+ ); + } + // 결과 대기 중 if (quizStatus === "waitingResult") { return ( From 2fa0d64e3680e2e71ff95d239973f32d4fd06878 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 5 Oct 2025 15:08:50 +0900 Subject: [PATCH 31/32] =?UTF-8?q?=E2=9C=A8=20(#305)=20=ED=80=B4=EC=A6=88?= =?UTF-8?q?=20=EC=84=B9=EC=85=98=EC=97=90=20=ED=80=B4=EC=A6=88=20=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=A1=9C=EB=93=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20UI=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0,=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20?= =?UTF-8?q?=EC=B9=B4=EC=9A=B4=ED=8A=B8=20=EC=A0=95=EB=B3=B4=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_components/QuizSection/QuizSection.tsx | 67 +++++++++++++++++-- .../QuizChoiceButton/QuizChoiceButton.tsx | 8 +-- .../components/Input/QuizInput/QuizInput.tsx | 1 - .../QuizToggleCard/QuizToggleCard.tsx | 5 +- frontend/constants/endpoints.ts | 2 +- 5 files changed, 67 insertions(+), 16 deletions(-) diff --git a/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx b/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx index 771702a6..bc51172a 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/QuizSection/QuizSection.tsx @@ -6,10 +6,12 @@ import { CheckCircle, Clock } from "lucide-react"; // API import { fetchQuizList } from "@/api/quizzes/fetchQuizList"; import { submitQuiz } from "@/api/quizzes/submitQuiz"; +import { getMyQuizResult } from "@/api/quizzes/getMyQuizResult"; // Types import { fetchQuizListResult } from "@/types/quizzes/fetchQuizListTypes"; import { SubmitQuizRequest } from "@/types/quizzes/submitQuizTypes"; +import { getMyQuizResultResult } from "@/types/quizzes/getMyQuizResultTypes"; // Components import QuizToggleCard from "@/components/QuizToggleCard/QuizToggleCard"; @@ -43,6 +45,8 @@ export default function QuizSection({ const { lectureStatus, lectureDate } = useLectureStatusStore(); const [quizStatus, setQuizStatus] = useState("notYet"); const [quizData, setQuizData] = useState(null); + const [quizResultData, setQuizResultData] = + useState(null); const [userAnswers, setUserAnswers] = useState<{ [quizId: string]: string }>( {} ); @@ -101,6 +105,27 @@ export default function QuizSection({ loadQuizData(); }, [quizStatus, lectureId]); + // 퀴즈 결과 데이터 로드 + useEffect(() => { + const loadQuizResultData = async () => { + if (quizStatus !== "viewResult") return; + + setLoading(true); + try { + const response = await getMyQuizResult(lectureId); + if (response.isSuccess && response.result) { + setQuizResultData(response.result); + } + } catch (error) { + console.error("퀴즈 결과 데이터 로드 실패:", error); + } finally { + setLoading(false); + } + }; + + loadQuizResultData(); + }, [quizStatus, lectureId]); + // 퀴즈 답변 핸들러 const handleQuizSelect = (quizId: string, answer: string) => { setUserAnswers((prev) => ({ @@ -192,13 +217,45 @@ export default function QuizSection({ // 결과 확인 가능 if (quizStatus === "viewResult") { + if (loading) { + return ; + } + + if (!quizResultData) { + return ( +
+ +
+ ); + } + return (
- +
+ {quizResultData.quizzes.map((quiz) => ( + option.text) + : undefined + } + userAnswer={quiz.studentAnswer} + correctAnswer={quiz.solution} + /> + ))} +
); } diff --git a/frontend/components/Button/QuizChoiceButton/QuizChoiceButton.tsx b/frontend/components/Button/QuizChoiceButton/QuizChoiceButton.tsx index f8f011d1..cf5ea69c 100644 --- a/frontend/components/Button/QuizChoiceButton/QuizChoiceButton.tsx +++ b/frontend/components/Button/QuizChoiceButton/QuizChoiceButton.tsx @@ -15,7 +15,6 @@ type ResultModeProps = { label: string; userAnswer: string; // 사용자 답변 correctAnswer: string; // 정답 - count: number; // 선택한 사람 수 }; type TeacherModeProps = { @@ -99,10 +98,9 @@ const QuizChoiceButton: React.FC = (props) => { disabled={isDisabled} > {props.label} - {(isResultMode(props) || isTeacherMode(props)) && - props.count !== undefined && ( - ({props.count}명) - )} + {isTeacherMode(props) && props.count !== undefined && ( + ({props.count}명) + )} ); }; diff --git a/frontend/components/Input/QuizInput/QuizInput.tsx b/frontend/components/Input/QuizInput/QuizInput.tsx index 6f4be664..ba78fdd8 100644 --- a/frontend/components/Input/QuizInput/QuizInput.tsx +++ b/frontend/components/Input/QuizInput/QuizInput.tsx @@ -83,7 +83,6 @@ const QuizInput: React.FC = (props) => { {isResultMode(props) && props.userAnswer !== props.correctAnswer && (

정답: {props.correctAnswer}

-

({props.count}명)

)}
diff --git a/frontend/components/QuizToggleCard/QuizToggleCard.tsx b/frontend/components/QuizToggleCard/QuizToggleCard.tsx index 76122121..b95ada6f 100644 --- a/frontend/components/QuizToggleCard/QuizToggleCard.tsx +++ b/frontend/components/QuizToggleCard/QuizToggleCard.tsx @@ -25,8 +25,6 @@ type ResultModeProps = { labels?: string[]; // multipleChoice 타입일때만 필요 userAnswer: string; // 사용자 답변 correctAnswer: string; // 정답 - counts: { [key: string]: number }; // 각 라벨에 대한 선택자 수 - correctRate: number; // 정답률 }; type TeacherModeProps = { @@ -119,7 +117,6 @@ const QuizToggleCard: React.FC = (props) => { label={label} userAnswer={props.userAnswer} correctAnswer={props.correctAnswer} - count={props.counts[label]} // 해당 label에 대한 선택자 수 mode="result" /> )) @@ -177,7 +174,7 @@ const QuizToggleCard: React.FC = (props) => { /> )}

퀴즈 {props.quizIndex}

- {(props.mode === "result" || props.mode === "teacher") && ( + {props.mode === "teacher" && (

정답률: {props.correctRate}%

)}
diff --git a/frontend/constants/endpoints.ts b/frontend/constants/endpoints.ts index b89885dc..0e46ad68 100644 --- a/frontend/constants/endpoints.ts +++ b/frontend/constants/endpoints.ts @@ -109,6 +109,6 @@ export const ENDPOINTS = { GET: (lectureId: string) => `${BASE_API}/quizzes/${lectureId}`, SUBMIT: `${BASE_API}/quizzes/submit`, GET_RESULT: (lectureId: string) => - `${BASE_API}/quizzes/${lectureId}/result`, + `${BASE_API}/quizzes/${lectureId}/result/student`, }, }; From a49e307609a57d4d02c217c36a58df557442b31c Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 5 Oct 2025 15:37:11 +0900 Subject: [PATCH 32/32] =?UTF-8?q?=F0=9F=94=A7=20(#305)=20=EA=B0=95?= =?UTF-8?q?=EC=9D=98=20ID=EC=97=90=20=EB=94=B0=EB=9D=BC=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EC=9D=B4=EB=A6=84=EC=9D=84=20=EA=B0=80?= =?UTF-8?q?=EC=A0=B8=EC=98=A4=EB=8A=94=20API=20=EC=97=94=EB=93=9C=ED=8F=AC?= =?UTF-8?q?=EC=9D=B8=ED=8A=B8=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=83=81=EC=88=98=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/api/classes/fetchClassNameByLectureId.ts | 2 +- frontend/constants/endpoints.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/api/classes/fetchClassNameByLectureId.ts b/frontend/api/classes/fetchClassNameByLectureId.ts index 30b95e76..d40dfc5e 100644 --- a/frontend/api/classes/fetchClassNameByLectureId.ts +++ b/frontend/api/classes/fetchClassNameByLectureId.ts @@ -8,7 +8,7 @@ export async function fetchClassNameByLectureId(lectureId: string) { try { const response = await axiosInstance.get< ApiResponse - >(ENDPOINTS.CLASSES.GET_CLASS_NAME(lectureId)); + >(ENDPOINTS.LECTURES.GET_CLASS_NAME(lectureId)); return response.data; } catch (error: unknown) { if (axios.isAxiosError(error) && error.response) { diff --git a/frontend/constants/endpoints.ts b/frontend/constants/endpoints.ts index 0e46ad68..18ae98e7 100644 --- a/frontend/constants/endpoints.ts +++ b/frontend/constants/endpoints.ts @@ -48,7 +48,6 @@ export const ENDPOINTS = { GET_ALL_NOTES: (classId: string) => `${BASE_API}/classes/${classId}/notes`, GET_MY_CLASSES: `${BASE_API}/classes/teacher/myclass`, GET_QUIZZES: (classId: string) => `${BASE_API}/classes/${classId}/quiz`, - GET_CLASS_NAME: (lectureId: string) => `${BASE_API}/classes/${lectureId}`, }, // 학생 클래스 관련 @@ -71,6 +70,8 @@ export const ENDPOINTS = { `${BASE_API}/lectures/student/today?date=${date}`, GET_STUDENT_LECTURE_DETAIL: (lectureId: string) => `${BASE_API}/lectures/student/${lectureId}`, + GET_CLASS_NAME: (lectureId: string) => + `${BASE_API}/lectures/classes/${lectureId}`, // 노트 관련 UPLOAD_NOTE: (classId: string) =>