From 88c00b24993d6a4bb07fc603ce7119d8c15b73e7 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sat, 6 Sep 2025 09:20:34 +0900 Subject: [PATCH 01/47] =?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 6af5c66f56bea98cea9f4e36e9c7cc094a9d00df Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sat, 6 Sep 2025 09:24:50 +0900 Subject: [PATCH 02/47] =?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 96fc162b3c5025a084526cdffff2b2e26ed7ed4f Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sat, 6 Sep 2025 09:29:19 +0900 Subject: [PATCH 03/47] =?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 f638fabef47efdbd219a61d4226dcab45654ce97 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sat, 6 Sep 2025 09:35:12 +0900 Subject: [PATCH 04/47] =?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 db51702c2dcafa45dc08a4369604bd9d4e11589f Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sat, 6 Sep 2025 09:37:03 +0900 Subject: [PATCH 05/47] =?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 2dd9e50361629b288c683287587a1e0ee4e8adac Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 7 Sep 2025 09:32:16 +0900 Subject: [PATCH 06/47] =?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 f4d89828f71714cfc5e44b8c458b2e12e98f638c Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 7 Sep 2025 10:15:42 +0900 Subject: [PATCH 07/47] =?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 ade21238b5a706a80773b3039e2c5833a1d9c4dd Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 7 Sep 2025 11:38:58 +0900 Subject: [PATCH 08/47] =?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 023aaae2b07687458e80c765ac002262190d6b0a Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 7 Sep 2025 11:48:54 +0900 Subject: [PATCH 09/47] =?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 9a04e96c1654e49540f4dbde5745bf6121100c92 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 7 Sep 2025 11:49:32 +0900 Subject: [PATCH 10/47] =?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 52e36e3848b560d03bf67650d10889a3e976ae6c Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 7 Sep 2025 11:53:00 +0900 Subject: [PATCH 11/47] =?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 7a309c3c831fefa958ead399130ee28a9314c8a0 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 7 Sep 2025 11:53:27 +0900 Subject: [PATCH 12/47] =?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 b0d574f5b39509d1effb6f4da08601b26996b532 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 7 Sep 2025 11:57:06 +0900 Subject: [PATCH 13/47] =?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 41c3e0ba76a741325058d426d92bb0980f14033d Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 21 Sep 2025 00:28:44 +0900 Subject: [PATCH 14/47] =?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 223e37f6231e2e9a7bd962efe85316fe00c4c9c3 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 21 Sep 2025 00:32:57 +0900 Subject: [PATCH 15/47] =?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 060271401de09df18efba3e9d320c6bfed42bdad Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 28 Sep 2025 13:36:16 +0900 Subject: [PATCH 16/47] =?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 2da9672e8478dcdc6d690378d7a995c911b45512 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 28 Sep 2025 14:09:01 +0900 Subject: [PATCH 17/47] =?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 1ed5dd3d1432731519dbff415148b8ccbcfcff9f Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 28 Sep 2025 14:09:14 +0900 Subject: [PATCH 18/47] =?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 37a908dc6aff2c03c194b39ced7790a4f0f6ad28 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 28 Sep 2025 14:21:34 +0900 Subject: [PATCH 19/47] =?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 d0e6489f46a82e2ff4710b80e8c2844fd32169f2 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 28 Sep 2025 14:32:27 +0900 Subject: [PATCH 20/47] =?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 e1b10c214fb159266d1faa213b1b26876e922dd7 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 28 Sep 2025 14:49:02 +0900 Subject: [PATCH 21/47] =?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 f24fefdcc7a2338574bf727d83ad3eba1cb38ac5 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 28 Sep 2025 15:07:02 +0900 Subject: [PATCH 22/47] =?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 4a64009e580313af973bb906247734417aeae879 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 28 Sep 2025 15:30:45 +0900 Subject: [PATCH 23/47] =?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:27:30 +0900 Subject: [PATCH 24/47] =?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 7ab9a29dbad1f5c17b54f6ce3366300eddbbc673 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 28 Sep 2025 15:55:51 +0900 Subject: [PATCH 25/47] =?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 983ec000d9bfe1fc25f7363acce3dc7d91255830 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 28 Sep 2025 16:23:58 +0900 Subject: [PATCH 26/47] =?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 5d09aaf6494f2cbeaa9c00f51f2a40643c48c851 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 28 Sep 2025 16:44:42 +0900 Subject: [PATCH 27/47] =?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 b02995a5380ed87640e4c2a3601d5b157b21e047 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 28 Sep 2025 16:46:10 +0900 Subject: [PATCH 28/47] =?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 63bd5ad5778f86181851dad345e72065e541e062 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 28 Sep 2025 16:53:13 +0900 Subject: [PATCH 29/47] =?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 431173380ef9fce54ba1e62f9841c2277c56da18 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 5 Oct 2025 15:08:50 +0900 Subject: [PATCH 30/47] =?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 d3464121a5373ef60192fce2f9b7cce2d366fcd6 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 5 Oct 2025 15:37:11 +0900 Subject: [PATCH 31/47] =?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) => From 9ecf6641b21e4cf86a3a1f517a1c1c4f51de078d Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 5 Oct 2025 17:45:58 +0900 Subject: [PATCH 32/47] =?UTF-8?q?=E2=9C=A8=20(#332)=20=EA=B0=95=EC=9D=98?= =?UTF-8?q?=20=EC=A7=88=EB=AC=B8=20=EB=AA=A9=EB=A1=9D=EC=9D=98=20=EC=86=8C?= =?UTF-8?q?=EC=BC=93=20=EB=A1=9C=EC=A7=81=EC=9D=84=20useLectureChat=20?= =?UTF-8?q?=ED=9B=85=EC=9C=BC=EB=A1=9C=20=ED=86=B5=ED=95=A9=ED=95=98?= =?UTF-8?q?=EA=B3=A0=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=A0=84=EC=86=A1?= =?UTF-8?q?=C2=B7=ED=91=9C=EC=8B=9C=20=EB=A1=9C=EC=A7=81=20=EB=B0=8F=20UI?= =?UTF-8?q?=EB=A5=BC=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QuestionListSection.module.scss | 10 +- .../QuestionListSection.tsx | 112 ++++-------------- 2 files changed, 32 insertions(+), 90 deletions(-) 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 index c16fec61..38e61133 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/_components/QuestionListSection/QuestionListSection.module.scss +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/QuestionListSection/QuestionListSection.module.scss @@ -22,6 +22,9 @@ } .questionItem { + display: flex; + flex-direction: column; + gap: $spacing-xs; width: fit-content; padding: $spacing-sm; border: 1px solid $color-neutral-7; @@ -34,7 +37,6 @@ } .timestamp { - margin-top: $spacing-xs; font-size: $font-size-sm; color: $color-neutral-5; } @@ -46,6 +48,12 @@ font-weight: $font-weight-light; } +.teacherName { + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: $color-blue; +} + .questionInputContainer { display: flex; gap: $spacing-sm; 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 c8c5c684..d898e1b7 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,6 @@ -import React, { useEffect, useState, useRef, useCallback } from "react"; +import React, { useEffect, useState } from "react"; import { useLectureStatusStore } from "@/store/useLectureStatusStore"; +import { useLectureChat } from "@/hooks/useLectureChat"; import NoDataView from "@/components/NoDataView/NoDataView"; import { MessageCircle, Send } from "lucide-react"; import styles from "./QuestionListSection.module.scss"; @@ -13,104 +14,27 @@ export default function QuestionListSection({ lectureId: string; }) { const { lectureStatus } = useLectureStatusStore(); - const [questions, setQuestions] = useState([]); + const { messages, connected, sendMessage } = useLectureChat(lectureId); 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(), - }; + if (!questionInput.trim() || !connected) return; - socketRef.current.send(JSON.stringify(message)); + sendMessage(questionInput.trim()); setQuestionInput(""); // 입력창 초기화 }; useEffect(() => { - // TODO: API 호출로 변경 - setQuestions([ - "dd", - "AsdfasAsdfasAsdfasAsdfasAsdfasAsdfasAsdfasAsdfasAsdfasAsdfasAsdfasAsdfasAsdfasAsdfasAsdfasAsdfasAsdfasAsdfasAsdfas", - "Asdfadfg", - "Asdfadfg", - ]); + // TODO: API 호출로 변경 - 기존 질문 목록 불러오기 setLoading(false); + }, [lectureId]); - // 강의 중일 때만 소켓 연결 - if (lectureStatus === "onLecture") { - connectSocket(); - } - - // 컴포넌트 언마운트 시 소켓 연결 해제 - return () => { - if (socketRef.current) { - socketRef.current.close(); - } - }; - }, [lectureId, lectureStatus, connectSocket]); - - const now = () => { + // 시간 포맷팅 함수 + const formatTime = (timestamp: string) => { try { - const date = new Date(); + const date = new Date(timestamp); const hours = date.getHours().toString().padStart(2, "0"); const minutes = date.getMinutes().toString().padStart(2, "0"); return `${hours}:${minutes}`; @@ -126,10 +50,19 @@ export default function QuestionListSection({ {lectureStatus === "onLecture" ? (
    - {questions.map((q, index) => ( + {messages.map((message, index) => (
  • -
    {q}
    -
    {now()}
    +
    +
    {message.content}
    +
    +
    + {formatTime(message.timestamp)} +
    + {message.role === "TEACHER" && ( +
    + * 강사가 보낸 메시지입니다. +
    + )}
  • ))}
@@ -143,6 +76,7 @@ export default function QuestionListSection({ icon={} onClick={sendQuestion} ariaLabel={"전송"} + disabled={!connected} />
From 841cfd8c52d8ddae253a55c11d99a1408527e175 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 5 Oct 2025 17:58:22 +0900 Subject: [PATCH 33/47] =?UTF-8?q?=E2=9C=A8=20(#332)=20=EA=B0=95=EC=9D=98?= =?UTF-8?q?=20=EC=B1=84=ED=8C=85=20=EB=AA=A9=EB=A1=9D=EC=9D=84=20=EA=B0=80?= =?UTF-8?q?=EC=A0=B8=EC=98=A4=EB=8A=94=20API=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EA=B4=80=EB=A0=A8=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=A0=95=EC=9D=98,=20=EC=97=94=EB=93=9C=ED=8F=AC?= =?UTF-8?q?=EC=9D=B8=ED=8A=B8=20=EC=83=81=EC=88=98=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/api/lectures/fetchChattingList.ts | 19 +++++++++++++++++++ frontend/constants/endpoints.ts | 2 ++ .../types/lectures/fetchChattingListTypes.ts | 5 +++++ 3 files changed, 26 insertions(+) create mode 100644 frontend/api/lectures/fetchChattingList.ts create mode 100644 frontend/types/lectures/fetchChattingListTypes.ts diff --git a/frontend/api/lectures/fetchChattingList.ts b/frontend/api/lectures/fetchChattingList.ts new file mode 100644 index 00000000..8e1e1159 --- /dev/null +++ b/frontend/api/lectures/fetchChattingList.ts @@ -0,0 +1,19 @@ +import { axiosInstance } from "@/api/axiosInstance"; +import axios from "axios"; // 추가 +import { ENDPOINTS } from "@/constants/endpoints"; +import { ApiResponse } from "@/types/apiResponseTypes"; +import { FetchChattingListResult } from "@/types/lectures/fetchChattingListTypes"; + +export async function fetchChattingList(lectureId: string) { + try { + const response = await axiosInstance.get< + ApiResponse + >(ENDPOINTS.LECTURES.GET_CHATTING_LIST(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/constants/endpoints.ts b/frontend/constants/endpoints.ts index 18ae98e7..a21129e7 100644 --- a/frontend/constants/endpoints.ts +++ b/frontend/constants/endpoints.ts @@ -72,6 +72,8 @@ export const ENDPOINTS = { `${BASE_API}/lectures/student/${lectureId}`, GET_CLASS_NAME: (lectureId: string) => `${BASE_API}/lectures/classes/${lectureId}`, + GET_CHATTING_LIST: (lectureId: string) => + `${BASE_API}/lectures/chatting/before/${lectureId}`, // 노트 관련 UPLOAD_NOTE: (classId: string) => diff --git a/frontend/types/lectures/fetchChattingListTypes.ts b/frontend/types/lectures/fetchChattingListTypes.ts new file mode 100644 index 00000000..ca6703e3 --- /dev/null +++ b/frontend/types/lectures/fetchChattingListTypes.ts @@ -0,0 +1,5 @@ +export type FetchChattingListResult = { + content: string; + timestamp: string; + role: string; +}; From 89d761b52b4243d4e16c2fda3f83ec3ab3703962 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Sun, 5 Oct 2025 18:03:13 +0900 Subject: [PATCH 34/47] =?UTF-8?q?=E2=9C=A8=20(#332)=20=EA=B0=95=EC=9D=98?= =?UTF-8?q?=20=EC=A7=88=EB=AC=B8=20=EB=AA=A9=EB=A1=9D=20=EC=84=B9=EC=85=98?= =?UTF-8?q?=EC=97=90=20=EA=B3=BC=EA=B1=B0=20=EB=A9=94=EC=8B=9C=EC=A7=80?= =?UTF-8?q?=EB=A5=BC=20=EB=B6=88=EB=9F=AC=EC=98=A4=EB=8A=94=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=ED=91=9C=EC=8B=9C=20=EB=A1=9C=EC=A7=81=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 --- .../QuestionListSection.tsx | 43 ++++++++++++++++--- .../types/lectures/fetchChattingListTypes.ts | 2 +- 2 files changed, 39 insertions(+), 6 deletions(-) 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 d898e1b7..1697c664 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/_components/QuestionListSection/QuestionListSection.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/QuestionListSection/QuestionListSection.tsx @@ -1,12 +1,13 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { useLectureStatusStore } from "@/store/useLectureStatusStore"; -import { useLectureChat } from "@/hooks/useLectureChat"; +import { ChatMessage, useLectureChat } from "@/hooks/useLectureChat"; 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"; +import { fetchChattingList } from "@/api/lectures/fetchChattingList"; export default function QuestionListSection({ lectureId, @@ -17,6 +18,7 @@ export default function QuestionListSection({ const { messages, connected, sendMessage } = useLectureChat(lectureId); const [questionInput, setQuestionInput] = useState(""); const [loading, setLoading] = useState(true); + const [previousMessages, setPreviousMessages] = useState([]); // 질문 전송 함수 const sendQuestion = () => { @@ -27,10 +29,41 @@ export default function QuestionListSection({ }; useEffect(() => { - // TODO: API 호출로 변경 - 기존 질문 목록 불러오기 - setLoading(false); + let isMounted = true; + const loadPreviousMessages = async () => { + try { + const res = await fetchChattingList(lectureId); + if (!isMounted) return; + if (res.isSuccess && Array.isArray(res.result)) { + const mapped: ChatMessage[] = res.result.map((m) => ({ + senderId: null, + senderName: null, + content: m.content, + role: m.role, + timestamp: m.timestamp, + })); + setPreviousMessages(mapped); + } else { + setPreviousMessages([]); + } + } catch { + if (!isMounted) return; + setPreviousMessages([]); + } finally { + if (isMounted) setLoading(false); + } + }; + loadPreviousMessages(); + return () => { + isMounted = false; + }; }, [lectureId]); + const combinedMessages = useMemo(() => { + // 과거 메시지 이후에 실시간 메시지 순서로 노출 + return [...previousMessages, ...messages]; + }, [previousMessages, messages]); + // 시간 포맷팅 함수 const formatTime = (timestamp: string) => { try { @@ -50,7 +83,7 @@ export default function QuestionListSection({ {lectureStatus === "onLecture" ? (
    - {messages.map((message, index) => ( + {combinedMessages.map((message, index) => (
  • {message.content}
    diff --git a/frontend/types/lectures/fetchChattingListTypes.ts b/frontend/types/lectures/fetchChattingListTypes.ts index ca6703e3..f11a3ed4 100644 --- a/frontend/types/lectures/fetchChattingListTypes.ts +++ b/frontend/types/lectures/fetchChattingListTypes.ts @@ -2,4 +2,4 @@ export type FetchChattingListResult = { content: string; timestamp: string; role: string; -}; +}[]; From f8904ea528dcb4485733392c1f5b48a0e9c97fff Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Mon, 6 Oct 2025 16:24:17 +0900 Subject: [PATCH 35/47] =?UTF-8?q?=F0=9F=94=A7=20(#332)=20useAuthStore?= =?UTF-8?q?=EC=97=90=EC=84=9C=20refresh=5Ftoken=20=EC=BF=A0=ED=82=A4=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EB=A1=9C=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 --- frontend/api/axiosInstance.ts | 2 +- frontend/store/resetAllStores.ts | 33 -------------------------------- frontend/store/useAuthStore.ts | 23 ++++++++++++++++++++++ 3 files changed, 24 insertions(+), 34 deletions(-) delete mode 100644 frontend/store/resetAllStores.ts diff --git a/frontend/api/axiosInstance.ts b/frontend/api/axiosInstance.ts index 2514ad49..89b3c2db 100644 --- a/frontend/api/axiosInstance.ts +++ b/frontend/api/axiosInstance.ts @@ -7,7 +7,7 @@ export const axiosInstance = axios.create({ withCredentials: true, }); -// 토큰이 필요하지 않은 API들 (로그인, 로그아웃, 회원가입, 이메일 인증) +// 토큰이 필요하지 않은 API들 (로그인, 회원가입, 이메일 인증 페이지) const noTokenRequired = [ "/users/login", "/users/signup", diff --git a/frontend/store/resetAllStores.ts b/frontend/store/resetAllStores.ts deleted file mode 100644 index 8f052884..00000000 --- a/frontend/store/resetAllStores.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { useAuthStore } from "./useAuthStore"; -import { useQuizStore } from "./useQuizStore"; -import useLectureListStore from "./useLectureListStore"; -import useSelectedClassStore from "./useSelectedClassStore"; -import useClassListStore from "./useClassListStore"; -import { useSignupStore } from "./useSignupStore"; -import { useLectureStatusStore } from "./useLectureStatusStore"; - -/** - * 모든 스토어를 초기 상태로 리셋하는 함수 - * 로그아웃이나 토큰 만료 시 사용 - */ -export const resetAllStores = () => { - // 각 스토어의 reset 함수 호출 - // auth store는 직접 초기화 (무한 루프 방지) - useAuthStore.setState({ - accessToken: null, - userId: null, - role: null, - iat: null, - exp: null, - }); - localStorage.removeItem("accessToken"); - - useQuizStore.getState().reset(); - useLectureListStore.getState().reset(); - useSelectedClassStore.getState().reset(); - useClassListStore.getState().reset(); - useSignupStore.getState().reset(); - useLectureStatusStore.getState().clearLectureStatus(); - - console.log("모든 스토어가 초기화되었습니다."); -}; diff --git a/frontend/store/useAuthStore.ts b/frontend/store/useAuthStore.ts index cab5c9ad..7d6a09a8 100644 --- a/frontend/store/useAuthStore.ts +++ b/frontend/store/useAuthStore.ts @@ -6,6 +6,8 @@ import useLectureListStore from "./useLectureListStore"; import useSelectedClassStore from "./useSelectedClassStore"; import useClassListStore from "./useClassListStore"; import { useSignupStore } from "./useSignupStore"; +import { useLectureStatusStore } from "./useLectureStatusStore"; +import { useClassTitleStore } from "./useClassTitleStore"; interface AuthState { accessToken: string | null; @@ -37,6 +39,12 @@ function getInitialAuthState() { if (decodedToken.exp < currentTime) { // 토큰이 만료되었으면 localStorage에서 제거하고 초기 상태 반환 localStorage.removeItem("accessToken"); + + // refresh_token 쿠키 삭제 + if (typeof document !== "undefined") { + document.cookie = + "refresh_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; + } return { accessToken: null, userId: null, @@ -56,6 +64,12 @@ function getInitialAuthState() { } catch { // 토큰 파싱 실패 시 localStorage에서 제거하고 초기 상태 반환 localStorage.removeItem("accessToken"); + + // refresh_token 쿠키 삭제 + if (typeof document !== "undefined") { + document.cookie = + "refresh_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; + } return { accessToken: null, userId: null, @@ -131,6 +145,13 @@ export const useAuthStore = create((set, get) => ({ logout: () => { set({ accessToken: null, userId: null, role: null, iat: null, exp: null }); localStorage.removeItem("accessToken"); + + // refresh_token 쿠키 삭제 + if (typeof document !== "undefined") { + document.cookie = + "refresh_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; + } + if (refreshTimeout) clearTimeout(refreshTimeout); if (expirationCheckInterval) clearInterval(expirationCheckInterval); @@ -140,6 +161,8 @@ export const useAuthStore = create((set, get) => ({ useSelectedClassStore.getState().reset(); useClassListStore.getState().reset(); useSignupStore.getState().reset(); + useLectureStatusStore.getState().clearLectureStatus(); + useClassTitleStore.getState().clearClassTitle(); console.log("로그아웃: 모든 스토어가 초기화되었습니다."); }, From 8ab4b2884767b85d4e39a7912ea29f52a5dc4b56 Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Mon, 6 Oct 2025 17:02:27 +0900 Subject: [PATCH 36/47] =?UTF-8?q?=F0=9F=94=A7=20(#332)=20useAuthStore?= =?UTF-8?q?=EC=97=90=EC=84=9C=20refresh=5Ftoken=20=EC=BF=A0=ED=82=A4=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EB=A1=9C=EC=A7=81=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/store/useAuthStore.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/frontend/store/useAuthStore.ts b/frontend/store/useAuthStore.ts index 7d6a09a8..41e80639 100644 --- a/frontend/store/useAuthStore.ts +++ b/frontend/store/useAuthStore.ts @@ -40,11 +40,6 @@ function getInitialAuthState() { // 토큰이 만료되었으면 localStorage에서 제거하고 초기 상태 반환 localStorage.removeItem("accessToken"); - // refresh_token 쿠키 삭제 - if (typeof document !== "undefined") { - document.cookie = - "refresh_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; - } return { accessToken: null, userId: null, @@ -65,11 +60,6 @@ function getInitialAuthState() { // 토큰 파싱 실패 시 localStorage에서 제거하고 초기 상태 반환 localStorage.removeItem("accessToken"); - // refresh_token 쿠키 삭제 - if (typeof document !== "undefined") { - document.cookie = - "refresh_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; - } return { accessToken: null, userId: null, @@ -146,12 +136,6 @@ export const useAuthStore = create((set, get) => ({ set({ accessToken: null, userId: null, role: null, iat: null, exp: null }); localStorage.removeItem("accessToken"); - // refresh_token 쿠키 삭제 - if (typeof document !== "undefined") { - document.cookie = - "refresh_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; - } - if (refreshTimeout) clearTimeout(refreshTimeout); if (expirationCheckInterval) clearInterval(expirationCheckInterval); From 627783d748c0bf09e13be8bfba5dc12e8cd73d8b Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Mon, 6 Oct 2025 17:09:28 +0900 Subject: [PATCH 37/47] =?UTF-8?q?=E2=9C=A8=20(#332)=20useAuthStore?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20?= =?UTF-8?q?=EC=8B=9C=20=EC=84=A0=ED=83=9D=EB=90=9C=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EC=8A=A4=ED=86=A0=EC=96=B4=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=ED=99=94=20=EB=A1=9C=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 --- frontend/store/useAuthStore.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/store/useAuthStore.ts b/frontend/store/useAuthStore.ts index 41e80639..816ffe9c 100644 --- a/frontend/store/useAuthStore.ts +++ b/frontend/store/useAuthStore.ts @@ -147,6 +147,7 @@ export const useAuthStore = create((set, get) => ({ useSignupStore.getState().reset(); useLectureStatusStore.getState().clearLectureStatus(); useClassTitleStore.getState().clearClassTitle(); + useSelectedClassStore.getState().reset(); console.log("로그아웃: 모든 스토어가 초기화되었습니다."); }, From 916e1539a2571c8bd33dd5c2a02545ac4e47639e Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Mon, 6 Oct 2025 17:11:45 +0900 Subject: [PATCH 38/47] =?UTF-8?q?=F0=9F=94=A7=20=20(#332)=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20ID=20=EC=83=81=ED=83=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EB=A1=9C=EC=A7=81=EC=9D=84=20useSelectedClassStore?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=EC=9C=BC=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 --- .../LectureNoteButton/LectureNoteButton.tsx | 39 +++++++------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/frontend/app/teacher/lecture-live/[lectureId]/_components/LectureNote/LectureNoteButton/LectureNoteButton.tsx b/frontend/app/teacher/lecture-live/[lectureId]/_components/LectureNote/LectureNoteButton/LectureNoteButton.tsx index a920027f..ccdc2fc6 100644 --- a/frontend/app/teacher/lecture-live/[lectureId]/_components/LectureNote/LectureNoteButton/LectureNoteButton.tsx +++ b/frontend/app/teacher/lecture-live/[lectureId]/_components/LectureNote/LectureNoteButton/LectureNoteButton.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { useParams } from "next/navigation"; import IconButton from "@/components/Button/IconButton/IconButton"; import ToolPopover from "../../ToolPopover/ToolPopover"; @@ -10,6 +10,7 @@ import { FileText } from "lucide-react"; import { fetchLectureNoteByLectureId } from "@/api/lectures/fetchLectureNoteByLectureId"; import { FetchLectureNoteByLectureIdResult } from "@/types/lectures/fetchLectureNoteByLectureIdTypes"; import styles from "./LectureNoteButton.module.scss"; +import useSelectedClassStore from "@/store/useSelectedClassStore"; export default function LectureNoteButton() { const { lectureId } = useParams<{ lectureId: string }>(); @@ -17,22 +18,12 @@ export default function LectureNoteButton() { const [openDoc, setOpenDoc] = useState(false); const [uploadOpen, setUploadOpen] = useState(false); - const [classId, setClassId] = useState(null); - const [lectureNotes, setLectureNotes] = useState([]); + const [lectureNotes, setLectureNotes] = useState< + FetchLectureNoteByLectureIdResult[] + >([]); + const { selectedClassId } = useSelectedClassStore(); - useEffect(() => { - const raw = localStorage.getItem("class-storage"); - if (raw) { - try { - const parsed = JSON.parse(raw); - setClassId(parsed.state?.selectedClassId ?? null); - } catch (err) { - console.error("class-storage 파싱 실패:", err); - } - } - }, []); - - const fetchLectureNotes = async () => { + const fetchLectureNotes = useCallback(async () => { try { const response = await fetchLectureNoteByLectureId(lectureId); if (response.isSuccess && response.result) { @@ -45,11 +36,11 @@ export default function LectureNoteButton() { console.error("강의자료 조회 오류:", err); setLectureNotes([]); } - }; + }, [lectureId]); useEffect(() => { if (lectureId) fetchLectureNotes(); - }, [lectureId]); + }, [lectureId, fetchLectureNotes]); return ( <> @@ -69,15 +60,15 @@ export default function LectureNoteButton() { side="bottom" > setOpenDoc(false)} - onUploadRequest={() => setUploadOpen(true)} + notes={lectureNotes} + onPicked={() => setOpenDoc(false)} + onUploadRequest={() => setUploadOpen(true)} /> - {uploadOpen && classId && ( + {uploadOpen && selectedClassId && ( setUploadOpen(false)} registeredFiles={lectureNotes.map((n) => n.lectureNoteName)} @@ -89,4 +80,4 @@ export default function LectureNoteButton() { )} ); -} \ No newline at end of file +} From 53d1ef23206ea42c632487e0758c6baabb9a26be Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Mon, 6 Oct 2025 17:36:05 +0900 Subject: [PATCH 39/47] =?UTF-8?q?=E2=9C=A8=20(#332)=20=EA=B0=95=EC=9D=98?= =?UTF-8?q?=20=EC=A7=88=EB=AC=B8=20=EB=AA=A9=EB=A1=9D=20=EC=84=B9=EC=85=98?= =?UTF-8?q?=EC=9D=98=20=EB=A7=88=EC=A7=80=EB=A7=89=20=EC=A7=88=EB=AC=B8?= =?UTF-8?q?=EC=97=90=20=EC=97=AC=EB=B0=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QuestionListSection/QuestionListSection.module.scss | 1 + 1 file changed, 1 insertion(+) 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 index 38e61133..57bfe001 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/_components/QuestionListSection/QuestionListSection.module.scss +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/QuestionListSection/QuestionListSection.module.scss @@ -33,6 +33,7 @@ &:last-child { border-bottom: none; + margin-bottom: $spacing-sm; } } From f0c2ca4ef9e80333377d8809cd6254fe5831116b Mon Sep 17 00:00:00 2001 From: Son Ahyun Date: Mon, 6 Oct 2025 17:42:32 +0900 Subject: [PATCH 40/47] =?UTF-8?q?=E2=9C=A8=20(#332)=20=ED=95=AD=EC=83=81?= =?UTF-8?q?=20=EC=B5=9C=EC=8B=A0=20=EC=B1=84=ED=8C=85=EC=9D=B4=20=EB=B3=B4?= =?UTF-8?q?=EC=9D=B4=EB=8F=84=EB=A1=9D=20=EC=8A=A4=ED=81=AC=EB=A1=A4=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 --- .../QuestionListSection/QuestionListSection.module.scss | 4 ++++ .../QuestionListSection/QuestionListSection.tsx | 9 ++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) 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 index 57bfe001..f70ac5ca 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/_components/QuestionListSection/QuestionListSection.module.scss +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/QuestionListSection/QuestionListSection.module.scss @@ -37,6 +37,10 @@ } } +.bottomSpacer { + height: 1px; +} + .timestamp { font-size: $font-size-sm; color: $color-neutral-5; 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 1697c664..193d1329 100644 --- a/frontend/app/student/lecture-detail/[lectureId]/_components/QuestionListSection/QuestionListSection.tsx +++ b/frontend/app/student/lecture-detail/[lectureId]/_components/QuestionListSection/QuestionListSection.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from "react"; +import React, { useEffect, useMemo, useRef, useState } from "react"; import { useLectureStatusStore } from "@/store/useLectureStatusStore"; import { ChatMessage, useLectureChat } from "@/hooks/useLectureChat"; import NoDataView from "@/components/NoDataView/NoDataView"; @@ -19,6 +19,7 @@ export default function QuestionListSection({ const [questionInput, setQuestionInput] = useState(""); const [loading, setLoading] = useState(true); const [previousMessages, setPreviousMessages] = useState([]); + const listEndRef = useRef(null); // 질문 전송 함수 const sendQuestion = () => { @@ -64,6 +65,11 @@ export default function QuestionListSection({ return [...previousMessages, ...messages]; }, [previousMessages, messages]); + // 새로운 메시지가 추가될 때 항상 맨 아래로 스크롤 + useEffect(() => { + listEndRef.current?.scrollIntoView({ behavior: "smooth", block: "end" }); + }, [combinedMessages]); + // 시간 포맷팅 함수 const formatTime = (timestamp: string) => { try { @@ -98,6 +104,7 @@ export default function QuestionListSection({ )}
  • ))} +
Date: Tue, 7 Oct 2025 00:50:08 +0900 Subject: [PATCH 41/47] =?UTF-8?q?=E2=9C=A8=20(#336)=20=EA=B0=95=EC=9D=98?= =?UTF-8?q?=20=EC=A2=85=EB=A3=8C=EC=8B=9C=20=EC=B1=84=ED=8C=85=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20API=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/api/lectures/saveChatting.ts | 19 +++++++ .../LectureLiveHeader/LectureLiveHeader.tsx | 53 ++++++++++++------- frontend/constants/endpoints.ts | 6 +-- 3 files changed, 56 insertions(+), 22 deletions(-) create mode 100644 frontend/api/lectures/saveChatting.ts diff --git a/frontend/api/lectures/saveChatting.ts b/frontend/api/lectures/saveChatting.ts new file mode 100644 index 00000000..9e141c0f --- /dev/null +++ b/frontend/api/lectures/saveChatting.ts @@ -0,0 +1,19 @@ +import axios from "axios"; +import { axiosInstance } from "@/api/axiosInstance"; +import { ENDPOINTS } from "@/constants/endpoints"; +import { ApiResponse } from "@/types/apiResponseTypes"; + +export async function saveChatting( lectureId: string ) { + try { + const response = await axiosInstance.post>( + ENDPOINTS.LECTURES.SAVE_CHAT(lectureId) + ); + + return response.data; + } catch (error: unknown) { + if (axios.isAxiosError(error) && error.response) { + return error.response.data as ApiResponse; + } + throw error; + } +} \ No newline at end of file diff --git a/frontend/app/teacher/lecture-live/[lectureId]/_components/LectureLiveHeader/LectureLiveHeader.tsx b/frontend/app/teacher/lecture-live/[lectureId]/_components/LectureLiveHeader/LectureLiveHeader.tsx index 380f22d5..b961333e 100644 --- a/frontend/app/teacher/lecture-live/[lectureId]/_components/LectureLiveHeader/LectureLiveHeader.tsx +++ b/frontend/app/teacher/lecture-live/[lectureId]/_components/LectureLiveHeader/LectureLiveHeader.tsx @@ -7,7 +7,7 @@ import FitContentButton from "@/components/Button/FitContentButton/FitContentBut import { DocumentSideButtonConnected } from "../DocumentSideButton/DocumentSideButton"; import PenToolButtons from "../PenTool/PenToolButtons/PenToolButtons"; import { useLive } from "../LectureLiveProvider"; -import ChatingButton from "../Chating/ChatingButton/ChatingButton"; +import ChattingButton from "../Chatting/ChattingButton/ChattingButton"; import RecordingButton from "../Recording/RecordingButton/RecordingButton"; import ConfirmModal from "@/components/Modal/ConfirmModal/ConfirmModal"; import { getRecordingEngine, type RecState } from "../Recording/recordingEngine"; @@ -15,6 +15,7 @@ import { ROUTES } from "@/constants/routes"; import { Tool } from "../LectureLiveProvider"; import LectureNoteButton from "../LectureNote/LectureNoteButton/LectureNoteButton"; import { saveAudioFile } from "@/api/lectures/saveAudioFile"; +import { saveChatting } from "@/api/lectures/saveChatting"; export default function LectureLiveHeader({ onToggleChat, @@ -52,36 +53,50 @@ export default function LectureLiveHeader({ const { lectureId } = useParams<{ lectureId: string }>(); const handleConfirmEnd = async () => { - if (isRecording) { - try { - setSaving(true); + if (!lectureId) { + console.error("lectureId가 없습니다."); + return; + } + + setSaving(true); + + try { + // 녹음 중일 경우 녹음 저장 + if (isRecording) { + + await new Promise((resolve, reject) => { const off = engine.subscribe("done", async (blob) => { try { - if (lectureId) { - await saveAudioFile(lectureId, blob); - console.log("🎤 녹음 파일 저장 완료"); - } + await saveAudioFile(lectureId, blob); + console.log("🎤 녹음 파일 저장 완료"); off(); resolve(); } catch (e) { console.error("❌ 녹음 파일 저장 실패:", e); + off(); reject(e); } }); - engine.stop().catch(reject); }); - } catch (e) { - console.error("녹음 종료 중 오류:", e); - } finally { - setSaving(false); } - } + + // 채팅 저장 + const chatRes = await saveChatting(lectureId); - setEndOpen(false); - onEndLecture?.(); - router.push(ROUTES.teacherLectureDetail(lectureId)); + if (!chatRes?.isSuccess) { + console.warn("채팅 저장 실패:", chatRes?.code, chatRes?.message); + } + + setEndOpen(false); + onEndLecture?.(); + router.push(ROUTES.teacherLectureDetail(lectureId)); + } catch (e) { + console.error("강의 종료 처리 중 오류:", e); + } finally { + setSaving(false); + } }; const handleCancelEnd = () => setEndOpen(false); @@ -100,7 +115,7 @@ export default function LectureLiveHeader({ - { closePen(); onToggleChat?.(); @@ -120,7 +135,7 @@ export default function LectureLiveHeader({ disableActions={saving} > {saving ? ( - <>녹음 파일 저장 중입니다... ⏳ + <>저장 중입니다... ⏳ ) : isRecording ? ( <> 지금 녹음이 진행 중입니다. diff --git a/frontend/constants/endpoints.ts b/frontend/constants/endpoints.ts index a21129e7..2e4f0f91 100644 --- a/frontend/constants/endpoints.ts +++ b/frontend/constants/endpoints.ts @@ -94,12 +94,12 @@ export const ENDPOINTS = { `${BASE_API}/lectures/${lectureId}/recordings`, GET_RECORDING: (lectureId: string) => `${BASE_API}/lectures/${lectureId}/recordings`, - + // 채팅 관련 SAVE_CHAT: (lectureId: string) => - `${BASE_API}/lectures/${lectureId}/chating`, + `${BASE_API}/lectures/chatting/after/${lectureId}`, GET_CHAT: (lectureId: string) => - `${BASE_API}/lectures/${lectureId}/chating`, + `${BASE_API}/lectures/${lectureId}/chatting`, }, // 퀴즈 관련 From f11ff1ee12d9e0bcea1c13b1e5ee05a0080cb9b5 Mon Sep 17 00:00:00 2001 From: Haemin Kim Date: Tue, 7 Oct 2025 00:50:47 +0900 Subject: [PATCH 42/47] =?UTF-8?q?=F0=9F=93=A6=20(#336)=20=EC=B1=84?= =?UTF-8?q?=ED=8C=85=20=ED=8C=8C=EC=9D=BC=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ChattingButton/ChattingButton.module.scss} | 0 .../ChattingButton/ChattingButton.tsx} | 4 ++-- .../ChattingPanel/ChattingPanel.module.scss} | 0 .../ChattingPanel/ChattingPanel.tsx} | 4 ++-- .../_components/LectureMainGrid/LectureMainGrid.tsx | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) rename frontend/app/teacher/lecture-live/[lectureId]/_components/{Chating/ChatingButton/ChatingButton.module.scss => Chatting/ChattingButton/ChattingButton.module.scss} (100%) rename frontend/app/teacher/lecture-live/[lectureId]/_components/{Chating/ChatingButton/ChatingButton.tsx => Chatting/ChattingButton/ChattingButton.tsx} (92%) rename frontend/app/teacher/lecture-live/[lectureId]/_components/{Chating/ChatingPanel/ChatingPanel.module.scss => Chatting/ChattingPanel/ChattingPanel.module.scss} (100%) rename frontend/app/teacher/lecture-live/[lectureId]/_components/{Chating/ChatingPanel/ChatingPanel.tsx => Chatting/ChattingPanel/ChattingPanel.tsx} (96%) diff --git a/frontend/app/teacher/lecture-live/[lectureId]/_components/Chating/ChatingButton/ChatingButton.module.scss b/frontend/app/teacher/lecture-live/[lectureId]/_components/Chatting/ChattingButton/ChattingButton.module.scss similarity index 100% rename from frontend/app/teacher/lecture-live/[lectureId]/_components/Chating/ChatingButton/ChatingButton.module.scss rename to frontend/app/teacher/lecture-live/[lectureId]/_components/Chatting/ChattingButton/ChattingButton.module.scss diff --git a/frontend/app/teacher/lecture-live/[lectureId]/_components/Chating/ChatingButton/ChatingButton.tsx b/frontend/app/teacher/lecture-live/[lectureId]/_components/Chatting/ChattingButton/ChattingButton.tsx similarity index 92% rename from frontend/app/teacher/lecture-live/[lectureId]/_components/Chating/ChatingButton/ChatingButton.tsx rename to frontend/app/teacher/lecture-live/[lectureId]/_components/Chatting/ChattingButton/ChattingButton.tsx index 2bda7008..ced72d92 100644 --- a/frontend/app/teacher/lecture-live/[lectureId]/_components/Chating/ChatingButton/ChatingButton.tsx +++ b/frontend/app/teacher/lecture-live/[lectureId]/_components/Chatting/ChattingButton/ChattingButton.tsx @@ -4,9 +4,9 @@ import { useEffect, useState } from "react"; import IconButton from "@/components/Button/IconButton/IconButton"; import { MessageCircleMore } from "lucide-react"; import { useLive } from "../../LectureLiveProvider"; -import styles from "./ChatingButton.module.scss"; +import styles from "./chattingButton.module.scss"; -export default function ChatingButton({ +export default function chattingButton({ className, onPress, }: { diff --git a/frontend/app/teacher/lecture-live/[lectureId]/_components/Chating/ChatingPanel/ChatingPanel.module.scss b/frontend/app/teacher/lecture-live/[lectureId]/_components/Chatting/ChattingPanel/ChattingPanel.module.scss similarity index 100% rename from frontend/app/teacher/lecture-live/[lectureId]/_components/Chating/ChatingPanel/ChatingPanel.module.scss rename to frontend/app/teacher/lecture-live/[lectureId]/_components/Chatting/ChattingPanel/ChattingPanel.module.scss diff --git a/frontend/app/teacher/lecture-live/[lectureId]/_components/Chating/ChatingPanel/ChatingPanel.tsx b/frontend/app/teacher/lecture-live/[lectureId]/_components/Chatting/ChattingPanel/ChattingPanel.tsx similarity index 96% rename from frontend/app/teacher/lecture-live/[lectureId]/_components/Chating/ChatingPanel/ChatingPanel.tsx rename to frontend/app/teacher/lecture-live/[lectureId]/_components/Chatting/ChattingPanel/ChattingPanel.tsx index 9beefc2e..f51dfdc0 100644 --- a/frontend/app/teacher/lecture-live/[lectureId]/_components/Chating/ChatingPanel/ChatingPanel.tsx +++ b/frontend/app/teacher/lecture-live/[lectureId]/_components/Chatting/ChattingPanel/ChattingPanel.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect, useRef, useState } from "react"; -import styles from "./ChatingPanel.module.scss"; +import styles from "./chattingPanel.module.scss"; import IconButton from "@/components/Button/IconButton/IconButton"; import { X, SendHorizontal } from "lucide-react"; import { useLive } from "../../LectureLiveProvider"; @@ -10,7 +10,7 @@ import BasicInput from "@/components/Input/BasicInput/BasicInput"; import { useParams } from "next/navigation"; import { useLectureChat } from "@/hooks/useLectureChat"; -export default function ChatPanel() { +export default function ChattingPanel() { const { togglePanel } = useLive(); const { lectureId } = useParams<{ lectureId: string }>(); diff --git a/frontend/app/teacher/lecture-live/[lectureId]/_components/LectureMainGrid/LectureMainGrid.tsx b/frontend/app/teacher/lecture-live/[lectureId]/_components/LectureMainGrid/LectureMainGrid.tsx index 9f552e11..75b11d37 100644 --- a/frontend/app/teacher/lecture-live/[lectureId]/_components/LectureMainGrid/LectureMainGrid.tsx +++ b/frontend/app/teacher/lecture-live/[lectureId]/_components/LectureMainGrid/LectureMainGrid.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from "react"; import styles from "./LectureMainGrid.module.scss"; import { useLive } from "../LectureLiveProvider"; -import ChatPanel from "../Chating/ChatingPanel/ChatingPanel"; +import ChattingPanel from "../Chatting/ChattingPanel/ChattingPanel"; import dynamic from "next/dynamic"; import { FileText } from "lucide-react"; @@ -119,7 +119,7 @@ export default function LectureMainGrid() { ); From 9b0e2798ceca50e8af0dc9a786b460fc1b129f06 Mon Sep 17 00:00:00 2001 From: Haemin Kim Date: Tue, 7 Oct 2025 00:59:41 +0900 Subject: [PATCH 43/47] =?UTF-8?q?=E2=9C=A8=20(#336)=20=EC=9D=B4=EC=A0=84?= =?UTF-8?q?=20=EC=B1=84=ED=8C=85=20=EB=B6=88=EB=9F=AC=EC=98=A4=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Chatting/ChattingPanel/ChattingPanel.tsx | 54 +++++++++++++++++-- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/frontend/app/teacher/lecture-live/[lectureId]/_components/Chatting/ChattingPanel/ChattingPanel.tsx b/frontend/app/teacher/lecture-live/[lectureId]/_components/Chatting/ChattingPanel/ChattingPanel.tsx index f51dfdc0..b317ae4f 100644 --- a/frontend/app/teacher/lecture-live/[lectureId]/_components/Chatting/ChattingPanel/ChattingPanel.tsx +++ b/frontend/app/teacher/lecture-live/[lectureId]/_components/Chatting/ChattingPanel/ChattingPanel.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState, useMemo } from "react"; import styles from "./chattingPanel.module.scss"; import IconButton from "@/components/Button/IconButton/IconButton"; import { X, SendHorizontal } from "lucide-react"; @@ -8,7 +8,8 @@ import { useLive } from "../../LectureLiveProvider"; import ChatBox from "@/components/ChatBox/ChatBox"; import BasicInput from "@/components/Input/BasicInput/BasicInput"; import { useParams } from "next/navigation"; -import { useLectureChat } from "@/hooks/useLectureChat"; +import { useLectureChat, type ChatMessage } from "@/hooks/useLectureChat"; +import { fetchChattingList } from "@/api/lectures/fetchChattingList"; export default function ChattingPanel() { const { togglePanel } = useLive(); @@ -20,6 +21,9 @@ export default function ChattingPanel() { const [text, setText] = useState(""); const bodyRef = useRef(null); + const [previousMessages, setPreviousMessages] = useState([]); + const fetchedOnceRef = useRef(false); + const closeChat = () => togglePanel("chat"); // 메시지 전송 @@ -36,13 +40,53 @@ export default function ChattingPanel() { send(); }; + // 연결 직후 과거 대화 불러오기 + useEffect(() => { + if (!lectureId || !connected || fetchedOnceRef.current) return; + + let isMounted = true; + (async () => { + try { + const res = await fetchChattingList(lectureId); + if (!isMounted) return; + + if (res.isSuccess && Array.isArray(res.result)) { + const mapped: ChatMessage[] = res.result.map((m) => ({ + senderId: null, + senderName: null, + content: m.content, + role: m.role, + timestamp: m.timestamp, + })); + setPreviousMessages(mapped); + } else { + setPreviousMessages([]); + } + } catch (e) { + setPreviousMessages([]); + console.error("과거 채팅 불러오기 실패:", e); + } finally { + fetchedOnceRef.current = true; + } + })(); + + return () => { + isMounted = false; + }; + }, [lectureId, connected]); + + // 과거 + 실시간 합친 배열 + const combinedMessages = useMemo( + () => [...previousMessages, ...messages], + [previousMessages, messages] + ); + // 새로운 메시지 오면 스크롤 맨 아래로 이동 useEffect(() => { const el = bodyRef.current; if (!el) return; el.scrollTop = el.scrollHeight; - }, [messages]); - + }, [combinedMessages]); const fmt = (ts: string) => { const d = new Date(ts); @@ -66,7 +110,7 @@ export default function ChattingPanel() {
- {messages.map((m, i) => ( + {combinedMessages.map((m, i) => (
Date: Tue, 7 Oct 2025 01:06:52 +0900 Subject: [PATCH 44/47] =?UTF-8?q?=E2=9C=A8=20(#336)=20=ED=95=99=EC=83=9D?= =?UTF-8?q?=20=EC=B1=84=ED=8C=85=EC=9D=BC=20=EA=B2=BD=EC=9A=B0=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=EB=B0=9C=EC=83=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/hooks/useLectureChat.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/hooks/useLectureChat.ts b/frontend/hooks/useLectureChat.ts index a991ece1..b59c61f2 100644 --- a/frontend/hooks/useLectureChat.ts +++ b/frontend/hooks/useLectureChat.ts @@ -48,6 +48,12 @@ export function useLectureChat(lectureId: string | undefined) { try { const parsed: ChatMessage = JSON.parse(message.body); setMessages((prev) => [...prev, parsed]); + + if (typeof window !== "undefined") { + if (parsed.role !== "TEACHER") { + window.dispatchEvent(new CustomEvent("live:chat:new", { detail: parsed })); + } + } } catch (e) { console.error("❌ 메시지 파싱 실패:", e); } From e82e0c7f04eef34487ef8aeeb5b6effde3d36325 Mon Sep 17 00:00:00 2001 From: Haemin Kim Date: Thu, 9 Oct 2025 23:24:54 +0900 Subject: [PATCH 45/47] =?UTF-8?q?=F0=9F=90=9B=20(#336)=20=EB=B9=8C?= =?UTF-8?q?=EB=93=9C=20=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_components/Chatting/ChattingButton/ChattingButton.tsx | 4 ++-- .../_components/Chatting/ChattingPanel/ChattingPanel.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/app/teacher/lecture-live/[lectureId]/_components/Chatting/ChattingButton/ChattingButton.tsx b/frontend/app/teacher/lecture-live/[lectureId]/_components/Chatting/ChattingButton/ChattingButton.tsx index ced72d92..c43b7d2c 100644 --- a/frontend/app/teacher/lecture-live/[lectureId]/_components/Chatting/ChattingButton/ChattingButton.tsx +++ b/frontend/app/teacher/lecture-live/[lectureId]/_components/Chatting/ChattingButton/ChattingButton.tsx @@ -4,9 +4,9 @@ import { useEffect, useState } from "react"; import IconButton from "@/components/Button/IconButton/IconButton"; import { MessageCircleMore } from "lucide-react"; import { useLive } from "../../LectureLiveProvider"; -import styles from "./chattingButton.module.scss"; +import styles from "./ChattingButton.module.scss"; -export default function chattingButton({ +export default function ChattingButton({ className, onPress, }: { diff --git a/frontend/app/teacher/lecture-live/[lectureId]/_components/Chatting/ChattingPanel/ChattingPanel.tsx b/frontend/app/teacher/lecture-live/[lectureId]/_components/Chatting/ChattingPanel/ChattingPanel.tsx index b317ae4f..e2f8510a 100644 --- a/frontend/app/teacher/lecture-live/[lectureId]/_components/Chatting/ChattingPanel/ChattingPanel.tsx +++ b/frontend/app/teacher/lecture-live/[lectureId]/_components/Chatting/ChattingPanel/ChattingPanel.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect, useRef, useState, useMemo } from "react"; -import styles from "./chattingPanel.module.scss"; +import styles from "./ChattingPanel.module.scss"; import IconButton from "@/components/Button/IconButton/IconButton"; import { X, SendHorizontal } from "lucide-react"; import { useLive } from "../../LectureLiveProvider"; From 9eec310428b9af686dc754e4164d1d1b06a61bf0 Mon Sep 17 00:00:00 2001 From: sunninz Date: Thu, 18 Sep 2025 13:58:39 +0900 Subject: [PATCH 46/47] =?UTF-8?q?:sparkles:=20(#315)=20=EC=B1=84=ED=8C=85?= =?UTF-8?q?=20=EB=82=B4=EC=9A=A9=20=EC=A0=80=EC=9E=A5=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EC=84=A4=EA=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/domain/question/exception/QuestionErrorCode.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/main/java/org/example/backend/domain/question/exception/QuestionErrorCode.java b/backend/src/main/java/org/example/backend/domain/question/exception/QuestionErrorCode.java index f54fa5dc..648eac0e 100644 --- a/backend/src/main/java/org/example/backend/domain/question/exception/QuestionErrorCode.java +++ b/backend/src/main/java/org/example/backend/domain/question/exception/QuestionErrorCode.java @@ -10,6 +10,8 @@ @AllArgsConstructor public enum QuestionErrorCode implements BaseErrorCode { _FORBIDDEN_LECTURE_ACCESS(HttpStatus.FORBIDDEN,"QUESTION403_1","해당 강의를 수강중인 학생만 조회가능합니다."), + _INVALID_JSON_FORMAT(HttpStatus.BAD_REQUEST,"QUESTION400_1","저장된 채팅 데이터를 읽는 데 실패했습니다."), + _FORBIDDEN_CHATTING_ACCESS(HttpStatus.FORBIDDEN,"QUESTION403_2","채팅 저장 권한은 강사에게만 있습니다."); _CHAT_MESSAGE_SEND_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, "QUESTION500_1", "채팅 메시지 전송에 실패했습니다."), _INVALID_JSON_FORMAT(HttpStatus.BAD_REQUEST,"QUESTION400_1","저장된 채팅 데이터를 읽는 데 실패했습니다."), _FORBIDDEN_CHATTING_ACCESS(HttpStatus.FORBIDDEN,"QUESTION403_2","채팅 저장 권한은 강사에게만 있습니다."); From 45ab08292a68193f24628e5747998c93f930df18 Mon Sep 17 00:00:00 2001 From: Haemin Kim Date: Fri, 10 Oct 2025 01:47:53 +0900 Subject: [PATCH 47/47] =?UTF-8?q?:fire:=20(#336)=20=EC=9E=98=EB=AA=BB?= =?UTF-8?q?=EB=90=9C=20=EC=B6=A9=EB=8F=8C=20=ED=95=B4=EA=B2=B0=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=82=AD=EC=A0=9C=20=EB=B0=8F=20dev=20=EA=B8=B0?= =?UTF-8?q?=EC=A4=80=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/domain/question/exception/QuestionErrorCode.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/src/main/java/org/example/backend/domain/question/exception/QuestionErrorCode.java b/backend/src/main/java/org/example/backend/domain/question/exception/QuestionErrorCode.java index 648eac0e..f54fa5dc 100644 --- a/backend/src/main/java/org/example/backend/domain/question/exception/QuestionErrorCode.java +++ b/backend/src/main/java/org/example/backend/domain/question/exception/QuestionErrorCode.java @@ -10,8 +10,6 @@ @AllArgsConstructor public enum QuestionErrorCode implements BaseErrorCode { _FORBIDDEN_LECTURE_ACCESS(HttpStatus.FORBIDDEN,"QUESTION403_1","해당 강의를 수강중인 학생만 조회가능합니다."), - _INVALID_JSON_FORMAT(HttpStatus.BAD_REQUEST,"QUESTION400_1","저장된 채팅 데이터를 읽는 데 실패했습니다."), - _FORBIDDEN_CHATTING_ACCESS(HttpStatus.FORBIDDEN,"QUESTION403_2","채팅 저장 권한은 강사에게만 있습니다."); _CHAT_MESSAGE_SEND_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, "QUESTION500_1", "채팅 메시지 전송에 실패했습니다."), _INVALID_JSON_FORMAT(HttpStatus.BAD_REQUEST,"QUESTION400_1","저장된 채팅 데이터를 읽는 데 실패했습니다."), _FORBIDDEN_CHATTING_ACCESS(HttpStatus.FORBIDDEN,"QUESTION403_2","채팅 저장 권한은 강사에게만 있습니다.");