diff --git a/frontend/app/student/class-detail/[classId]/_components/ClassInfoSection/ClassInfoSection.module.scss b/frontend/app/student/class-detail/[classId]/_components/ClassInfoSection/ClassInfoSection.module.scss new file mode 100644 index 00000000..10d340dd --- /dev/null +++ b/frontend/app/student/class-detail/[classId]/_components/ClassInfoSection/ClassInfoSection.module.scss @@ -0,0 +1,37 @@ +.container { + display: flex; + flex-direction: column; + gap: 10px; + background-color: $color-blue; + padding: $spacing-3xl $spacing-xl; + color: $color-white; +} + +.classInfo { + display: flex; + flex-direction: column; + gap: $spacing-sm; +} + +.classInfoTitleContainer { + display: flex; + flex-direction: row; + gap: $spacing-sm; + align-items: center; + + .classInfoTitle { + font-size: $font-size-2xl; + } + + .classInfoProfessor { + font-size: $font-size-lg; + } +} + +.classInfoDate { + display: flex; + flex-direction: row; + gap: $spacing-sm; + align-items: center; + font-size: $font-size-lg; +} diff --git a/frontend/app/student/class-detail/[classId]/_components/ClassInfoSection/ClassInfoSection.tsx b/frontend/app/student/class-detail/[classId]/_components/ClassInfoSection/ClassInfoSection.tsx new file mode 100644 index 00000000..b116c311 --- /dev/null +++ b/frontend/app/student/class-detail/[classId]/_components/ClassInfoSection/ClassInfoSection.tsx @@ -0,0 +1,44 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import styles from "./ClassInfoSection.module.scss"; +import { useParams } from "next/navigation"; +import { fetchClassInfoByClassId } from "@/api/classes/fetchClassInfoByClassId"; +import { FetchClassInfoByClassIdResult } from "@/types/classes/fetchClassInfoByClassIdTypes"; +import { Calendar, Clock } from "lucide-react"; + +export default function ClassInfoSection() { + const { classId } = useParams(); + + const [classInfo, setClassInfo] = + useState(null); + + useEffect(() => { + fetchClassInfoByClassId(classId as string).then((res) => { + if (res.isSuccess) { + setClassInfo(res.result || null); + } + }); + }, [classId]); + + return ( +
+
+
+
{classInfo?.className}
+
+ {classInfo?.professorName} +
+
+
+ + {classInfo?.classDate} +
+
+ + {classInfo?.startDate} ~ {classInfo?.endDate} +
+
+
+ ); +} diff --git a/frontend/app/student/class-detail/[classId]/_components/LectureList/LectureList.module.scss b/frontend/app/student/class-detail/[classId]/_components/LectureList/LectureList.module.scss new file mode 100644 index 00000000..b6d037e2 --- /dev/null +++ b/frontend/app/student/class-detail/[classId]/_components/LectureList/LectureList.module.scss @@ -0,0 +1,88 @@ +.container { + width: 100%; + padding: $spacing-lg; +} + +.lectureList { + display: flex; + flex-direction: column; + gap: $spacing-md; +} + +.lectureCard { + display: flex; + justify-content: space-between; + align-items: center; + background: $color-white; + border-radius: $radius-md; + padding: $spacing-xl; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + transition: all 0.2s ease; + cursor: pointer; + + &:hover { + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); + transform: translateY(-2px); + } +} + +.lectureInfo { + flex: 1; + display: flex; + flex-direction: column; + gap: $spacing-sm; +} + +.lectureTitle { + font-size: $font-size-xl; + display: flex; + justify-content: space-between; + align-items: center; + color: $color-black; + line-height: 1.4; + border-bottom: 1px solid $color-neutral-7; + padding-bottom: $spacing-sm; +} + +.lectureDetails { + display: flex; + gap: $spacing-md; + color: $color-mutedblue; + font-size: $font-size-sm; +} + +.status { + margin-left: $spacing-sm; + color: $color-mutedblue; + font-size: $font-size-sm; +} + +.dateInfo, +.timeInfo { + display: flex; + align-items: center; + gap: $spacing-sm; + + svg { + color: $color-mutedblue; + } +} + +.viewButton { + width: 40px; + height: 40px; + border-radius: 50%; + background: $color-skyblue; + border: none; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + + svg { + color: $color-blue; + width: 16px; + height: 16px; + } +} diff --git a/frontend/app/student/class-detail/[classId]/_components/LectureList/LectureList.tsx b/frontend/app/student/class-detail/[classId]/_components/LectureList/LectureList.tsx new file mode 100644 index 00000000..6e891d4e --- /dev/null +++ b/frontend/app/student/class-detail/[classId]/_components/LectureList/LectureList.tsx @@ -0,0 +1,101 @@ +import React, { useEffect, useState } from "react"; +import styles from "./LectureList.module.scss"; +import { useParams } from "next/navigation"; +import { fetchLecturesByClass } from "@/api/classes/fetchLecturesByClass"; +import { FetchLecturesByClassResult } from "@/types/classes/fetchLecturesByClassTypes"; +import { Calendar, ChevronRight, Clock } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { ROUTES } from "@/constants/routes"; +import LoadingSpinner from "@/components/LoadingSpinner/LoadingSpinner"; + +export default function LectureList() { + const { classId } = useParams(); + const [lectures, setLectures] = useState([]); + const [loading, setLoading] = useState(true); + const router = useRouter(); + + useEffect(() => { + const fetchLectures = async () => { + try { + setLoading(true); + const response = await fetchLecturesByClass(classId as string); + if (response.isSuccess && response.result) { + setLectures(response.result); + } + } catch (error) { + console.error("강의 목록을 불러오는데 실패했습니다:", error); + } finally { + setLoading(false); + } + }; + fetchLectures(); + }, [classId]); + + const getStatusText = (status: string) => { + switch (status) { + case "beforeLecture": + return "강의 전"; + case "onLecture": + return "강의 중"; + case "afterLecture": + return "강의 종료"; + default: + return "알 수 없음"; + } + }; + + if (loading) { + return ( +
+ +
+ ); + } + + if (lectures.length === 0) { + return
등록된 강의가 없습니다.
; + } + + return ( +
+
+ {lectures.map((lecture) => ( +
{ + router.push(ROUTES.studentLectureDetail(lecture.lectureId)); + }} + > +
+
+
+ {String(lecture.session).padStart(2, "0")}. + {lecture.lectureName} + + {getStatusText(lecture.status)} + +
+ +
+
+
+ + {lecture.lectureDate} +
+
+ + + {lecture.startTime} ~ {lecture.endTime} + +
+
+
+
+ ))} +
+
+ ); +} diff --git a/frontend/app/student/class-detail/[classId]/_components/LectureListAndNote/LectureListAndNote.module.scss b/frontend/app/student/class-detail/[classId]/_components/LectureListAndNote/LectureListAndNote.module.scss new file mode 100644 index 00000000..3baf207c --- /dev/null +++ b/frontend/app/student/class-detail/[classId]/_components/LectureListAndNote/LectureListAndNote.module.scss @@ -0,0 +1,9 @@ +.container { + width: 100%; + display: flex; + flex-direction: column; +} + +.content { + width: 100%; +} diff --git a/frontend/app/student/class-detail/[classId]/_components/LectureListAndNote/LectureListAndNote.tsx b/frontend/app/student/class-detail/[classId]/_components/LectureListAndNote/LectureListAndNote.tsx new file mode 100644 index 00000000..0cf671cb --- /dev/null +++ b/frontend/app/student/class-detail/[classId]/_components/LectureListAndNote/LectureListAndNote.tsx @@ -0,0 +1,25 @@ +"use client"; +import React, { useState } from "react"; +import styles from "./LectureListAndNote.module.scss"; +import LectureList from "../LectureList/LectureList"; +import LectureNote from "../LectureNote/LectureNote"; +import Tab from "../../../../../../components/Tab/Tab"; + +export default function LectureListAndNote() { + const [selectedTab, setSelectedTab] = useState("강의 목록"); + + const tabs = ["강의 목록", "강의자료"]; + + const handleTabSelect = (tab: string) => { + setSelectedTab(tab); + }; + + return ( +
+ +
+ {selectedTab === "강의 목록" ? : } +
+
+ ); +} 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 new file mode 100644 index 00000000..53ba5fa5 --- /dev/null +++ b/frontend/app/student/class-detail/[classId]/_components/LectureNote/LectureNote.module.scss @@ -0,0 +1,60 @@ +.container { + width: 100%; + padding: $spacing-lg; +} + +.title { + font-size: $font-size-xl; + font-weight: $font-weight-bold; + margin-bottom: $spacing-lg; + color: $color-black; +} + +.fileList { + display: flex; + flex-direction: column; + gap: $spacing-md; +} + +.fileItem { + display: flex; + align-items: center; + justify-content: space-between; + padding: $spacing-md; + 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 { + flex: 1; + min-width: 0; + overflow: hidden; +} + +.fileItem > :first-child > span { + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; +} + +.fileInfo { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: $spacing-xs; + flex-shrink: 0; + margin-left: $spacing-md; +} + +.fileSize { + font-size: $font-size-sm; + color: $color-mutedblue; +} diff --git a/frontend/app/student/class-detail/[classId]/_components/LectureNote/LectureNote.tsx b/frontend/app/student/class-detail/[classId]/_components/LectureNote/LectureNote.tsx new file mode 100644 index 00000000..dce67792 --- /dev/null +++ b/frontend/app/student/class-detail/[classId]/_components/LectureNote/LectureNote.tsx @@ -0,0 +1,73 @@ +import React, { useEffect, useState } from "react"; +import styles from "./LectureNote.module.scss"; +import { useParams } from "next/navigation"; +import { fetchLectureNotesByClass } from "@/api/lectures/fetchLectureNotesByClass"; +import FileDisplay from "@/components/FileDisplay/FileDisplay"; +import LoadingSpinner from "@/components/LoadingSpinner/LoadingSpinner"; + +interface LectureNote { + lectureNoteId: string; + classId: string; + lectureNoteUrl: string; + lectureNoteName: string; + fileSize: string; + session: number[]; +} + +export default function LectureNote() { + const { classId } = useParams(); + const [lectureNotes, setLectureNotes] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchLectureNote = async () => { + try { + setLoading(true); + const response = await fetchLectureNotesByClass(classId as string); + if (response.isSuccess && response.result) { + setLectureNotes(response.result); + } + } catch (error) { + console.error("강의 자료를 불러오는데 실패했습니다:", error); + } finally { + setLoading(false); + } + }; + fetchLectureNote(); + }, [classId]); + + if (loading) { + return ( +
+ +
+ ); + } + + const handleNoteClick = (lectureNoteUrl: string) => { + window.open(lectureNoteUrl, "_blank"); + }; + + return ( +
+ {lectureNotes.length > 0 ? ( +
+ {lectureNotes.map((note) => ( +
handleNoteClick(note.lectureNoteUrl)} + > + +
+ {note.fileSize} +
+
+ ))} +
+ ) : ( +
등록된 강의 자료가 없습니다.
+ )} +
+ ); +} diff --git a/frontend/app/student/class-detail/[classId]/page.tsx b/frontend/app/student/class-detail/[classId]/page.tsx index 89c6f3ab..b128bec2 100644 --- a/frontend/app/student/class-detail/[classId]/page.tsx +++ b/frontend/app/student/class-detail/[classId]/page.tsx @@ -1,3 +1,11 @@ +import ClassInfoSection from "./_components/ClassInfoSection/ClassInfoSection"; +import LectureListAndNote from "./_components/LectureListAndNote/LectureListAndNote"; + export default function StudentClassDetailPage() { - return
클래스 상세
; + return ( +
+ + +
+ ); } diff --git a/frontend/app/student/class/_components/ClassListSection/ClassListSection.tsx b/frontend/app/student/class/_components/ClassListSection/ClassListSection.tsx index 7dd3c654..e7d198b2 100644 --- a/frontend/app/student/class/_components/ClassListSection/ClassListSection.tsx +++ b/frontend/app/student/class/_components/ClassListSection/ClassListSection.tsx @@ -1,18 +1,20 @@ "use client"; - import React, { useEffect, useState } from "react"; import styles from "./ClassListSection.module.scss"; import { fetchMyClassList } from "@/api/student-classes/fetchMyClassList"; import { FetchMyClassListResult } from "@/types/classes/fetchMyClassListTypes"; import { Calendar, Clock, ChevronRight } from "lucide-react"; import { ROUTES } from "@/constants/routes"; -import router from "next/router"; +import { useRouter } from "next/navigation"; +import LoadingSpinner from "@/components/LoadingSpinner/LoadingSpinner"; export default function ClassListSection() { const [classList, setClassList] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); + const router = useRouter(); + useEffect(() => { const loadClassList = async () => { try { @@ -46,9 +48,7 @@ export default function ClassListSection() { if (isLoading) { return (
-
-

클래스 목록을 불러오는 중...

-
+
); } diff --git a/frontend/components/Header/Student/BackWithProfileHeader/BackWithProfileHeader.module.scss b/frontend/components/Header/Student/BackWithProfileHeader/BackWithProfileHeader.module.scss index cf204cbe..2f232245 100644 --- a/frontend/components/Header/Student/BackWithProfileHeader/BackWithProfileHeader.module.scss +++ b/frontend/components/Header/Student/BackWithProfileHeader/BackWithProfileHeader.module.scss @@ -7,7 +7,7 @@ height: 6vh; margin: 0 auto; background-color: $color-blue; - border-bottom: 1px solid $color-neutral-7; + padding: 0 10px; position: fixed; diff --git a/frontend/components/Tab/Tab.module.scss b/frontend/components/Tab/Tab.module.scss index a3993ef5..7e69e09f 100644 --- a/frontend/components/Tab/Tab.module.scss +++ b/frontend/components/Tab/Tab.module.scss @@ -6,7 +6,7 @@ position: relative; .tabItem { - font-size: $font-size-md; + font-size: $font-size-lg; cursor: pointer; width: 100%; padding: $spacing-md 0;