Skip to content

Commit 2aae18b

Browse files
committed
Merge remote-tracking branch 'origin/Feat/#330/student-noti' into Feat/#330/student-noti
# Conflicts: # backend/src/main/java/org/example/backend/domain/lecture/entity/Lecture.java
2 parents 1cd7a64 + 98d6258 commit 2aae18b

File tree

28 files changed

+2492
-180
lines changed

28 files changed

+2492
-180
lines changed

backend/src/main/java/org/example/backend/domain/lecture/entity/Lecture.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@
1010

1111
import org.example.backend.domain.classroom.entity.Classroom;
1212
import org.example.backend.domain.lectureNoteMapping.entity.LectureNoteMapping;
13-
import org.example.backend.domain.notification.entity.Notification;
13+
import org.example.backend.domain.question.entity.Question;
1414
import org.example.backend.domain.quiz.entity.Quiz;
15+
import org.example.backend.domain.studentClass.entity.StudentClass;
1516
import org.example.backend.global.entitiy.BaseEntity;
17+
import org.hibernate.annotations.OnDelete;
18+
import org.hibernate.annotations.OnDeleteAction;
1619

1720
@Entity
1821
@Table(name = "lecture")
@@ -60,6 +63,6 @@ public class Lecture extends BaseEntity {
6063
private List<Quiz> quiz = new ArrayList<>();
6164

6265
@OneToMany(mappedBy = "lecture", cascade = CascadeType.ALL, orphanRemoval = true)
63-
private List<Notification> notification = new ArrayList<>();
66+
private List<Question> questions = new ArrayList<>();
6467

6568
}

backend/src/main/java/org/example/backend/domain/question/service/ChatServiceImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public void sendMessage(UUID lectureId, MessageRequestDTO.MessageDTO messageDTO,
6161
// 2. 메시지 전파
6262
// 메시지 구성
6363
MessageRequestDTO.MessageDTO maskMessage = MessageRequestDTO.MessageDTO.builder()
64-
.senderId(null)
64+
.senderId(userId)
6565
.senderName(null)
6666
.content(messageDTO.getContent())
6767
.role(sender.getRole())

backend/src/main/java/org/example/backend/domain/quizAnswer/converter/QuizResultStudentConverter.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,20 @@
1313
import java.util.UUID;
1414
import java.util.stream.Collectors;
1515

16-
1716
@NoArgsConstructor(access = AccessLevel.PRIVATE)
1817
@Component
1918
public class QuizResultStudentConverter {
2019
public static QuizResultStudentResponseDTO toResultStudentResponse(
2120
UUID lectureId,
2221
List<QuizResultStudentResponseDTO.QuizDTO> quizzes
2322
) {
23+
List<QuizResultStudentResponseDTO.QuizDTO> sortedQuizzes = quizzes.stream()
24+
.sorted((q1, q2) -> Integer.compare(q1.getQuizOrder(), q2.getQuizOrder()))
25+
.collect(Collectors.toList());
26+
2427
return QuizResultStudentResponseDTO.builder()
2528
.lectureId(lectureId)
26-
.quizzes(quizzes)
29+
.quizzes(sortedQuizzes)
2730
.build();
2831
}
2932

@@ -36,6 +39,7 @@ public static QuizResultStudentResponseDTO.QuizDTO toQuizDTO(
3639
boolean isCollect = (myAnswer != null) && Boolean.TRUE.equals(myAnswer.getIsCollect());
3740

3841
List<QuizResultStudentResponseDTO.OptionDTO> optionDTOs = options.stream()
42+
.sorted((o1, o2) -> Integer.compare(o1.getOptionOrder(), o2.getOptionOrder()))
3943
.map(o -> QuizResultStudentResponseDTO.OptionDTO.builder()
4044
.id(o.getId())
4145
.optionOrder(o.getOptionOrder())

docker-compose.local.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ services:
3131

3232

3333
ai:
34-
image: classlog/ai:v3.4
34+
image: classlog/ai:v4.1
3535
container_name: classlog-ai
3636
ports:
3737
- 8000:8000

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ services:
6565

6666

6767
ai:
68-
image: classlog/ai:v3.4
68+
image: classlog/ai:v4.1
6969
container_name: classlog-ai
7070
ports:
7171
- 8000:8000
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import axios from "axios";
2+
import { axiosInstance } from "@/api/axiosInstance";
3+
import { ENDPOINTS } from "@/constants/endpoints";
4+
import { ApiResponse } from "@/types/apiResponseTypes";
5+
6+
export async function saveChatting( lectureId: string ) {
7+
try {
8+
const response = await axiosInstance.post<ApiResponse<null>>(
9+
ENDPOINTS.LECTURES.SAVE_CHAT(lectureId)
10+
);
11+
12+
return response.data;
13+
} catch (error: unknown) {
14+
if (axios.isAxiosError(error) && error.response) {
15+
return error.response.data as ApiResponse<null>;
16+
}
17+
throw error;
18+
}
19+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"use client";
2+
3+
import React from "react";
4+
import Image from "next/image";
5+
import Link from "next/link";
6+
import styles from "../page.module.scss";
7+
import { IMAGES } from "@/constants/images";
8+
import { ROUTES } from "@/constants/routes";
9+
import { MoveDown } from "lucide-react";
10+
11+
interface HeroSectionProps {
12+
introSectionRef?: React.RefObject<HTMLElement | null>;
13+
}
14+
15+
export default function HeroSection({ introSectionRef }: HeroSectionProps) {
16+
const handleScrollToIntro = () => {
17+
if (introSectionRef?.current) {
18+
introSectionRef.current.scrollIntoView({
19+
behavior: "smooth",
20+
block: "start",
21+
});
22+
}
23+
};
24+
return (
25+
<section className={styles.heroSection}>
26+
<div className={styles.heroInner}>
27+
<div className={styles.heroLeft}>
28+
<div className={styles.textWrapper}>
29+
<Image
30+
src={IMAGES.logo4}
31+
alt="ClassLog Logo"
32+
width={500}
33+
height={120}
34+
className={styles.logo}
35+
/>
36+
<h2>당신의 강의를 더 스마트하게</h2>
37+
<p>수업 녹음, 실시간 소통, AI 기반 퀴즈 생성을 통해</p>
38+
<p>강의 준비부터 피드백까지 한 번에 해결하세요.</p>
39+
</div>
40+
<div className={styles.heroCtas}>
41+
<Link href={ROUTES.login} className={styles.primaryCta}>
42+
시작하기
43+
<span className={styles.arrow} aria-hidden>
44+
45+
</span>
46+
</Link>
47+
</div>
48+
</div>
49+
50+
<div className={styles.introImageWrapper}>
51+
<Image
52+
src={IMAGES.introImage}
53+
alt="소개 이미지"
54+
width={650}
55+
height={300}
56+
className={styles.heroImage}
57+
priority
58+
/>
59+
</div>
60+
</div>
61+
62+
<div className={styles.scrollIndicator} onClick={handleScrollToIntro}>
63+
<MoveDown className={styles.downArrow} size={30} strokeWidth={2.5} />
64+
</div>
65+
</section>
66+
);
67+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import React from "react";
2+
import styles from "../page.module.scss";
3+
import {
4+
MessageCircle,
5+
MessageCircleQuestion,
6+
PencilLine,
7+
Videotape,
8+
} from "lucide-react";
9+
10+
const FEATURES = [
11+
{
12+
icon: <MessageCircle />,
13+
title: "즉시 질문, 즉시 피드백",
14+
description:
15+
"수업 중 궁금한 점은 바로 질문하고 실시간으로 소통할 수 있어요.",
16+
},
17+
{
18+
icon: <MessageCircleQuestion />,
19+
title: "AI 기반 맞춤형 퀴즈",
20+
description:
21+
"강의자료와 녹음을 바탕으로 AI가 자동으로 퀴즈를 만들어요. 복습과 이해도 확인이 더 쉬워집니다.",
22+
},
23+
{
24+
icon: <PencilLine />,
25+
title: "강의자료 업로드 & 수업용 실시간 필기",
26+
description:
27+
"강의 중 자료에 바로 필기하며 핵심 내용을 학생들과 함께 나눌 수 있어요.",
28+
},
29+
{
30+
icon: <Videotape />,
31+
title: "언제든 다시 듣는 수업",
32+
description:
33+
"수업은 자동 녹음되며, 언제든 다시 듣거나 다운로드할 수 있어요.",
34+
},
35+
];
36+
37+
export default function IntroSection({
38+
ref,
39+
}: {
40+
ref: React.RefObject<HTMLElement | null>;
41+
}) {
42+
return (
43+
<section ref={ref} className={styles.introSection}>
44+
<div className={styles.introHeader}>
45+
<h2 className={styles.introTitle}>
46+
What You Can Do
47+
<br />
48+
with ClassLog
49+
</h2>
50+
<div className={styles.introDescription}>
51+
<p className={styles.introSubtitle}>
52+
ClassLog의 핵심 기능 4가지를 소개합니다!
53+
</p>
54+
<p className={styles.introText}>
55+
실시간 질문, AI 기반 퀴즈 생성, 강의자료 업로드 및 필기, 자동 수업
56+
녹음 등 <br />
57+
수업의 전 과정을 하나의 플랫폼에서 관리하고, 학습의 흐름을 놓치지
58+
않도록 도와줍니다.
59+
</p>
60+
</div>
61+
</div>
62+
<div className={styles.featuresContainer}>
63+
{FEATURES.map((feature) => (
64+
<div key={feature.title} className={styles.featureCard}>
65+
<div className={styles.featureIcon}>{feature.icon}</div>
66+
<div className={styles.featureContent}>
67+
<h3 className={styles.featureTitle}>{feature.title}</h3>
68+
<p className={styles.featureDescription}>{feature.description}</p>
69+
</div>
70+
</div>
71+
))}
72+
</div>
73+
</section>
74+
);
75+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"use client";
2+
3+
import Link from "next/link";
4+
import Image from "next/image";
5+
import { useEffect, useState } from "react";
6+
import styles from "../page.module.scss";
7+
import { ROUTES } from "@/constants/routes";
8+
import { IMAGES } from "@/constants/images";
9+
10+
export default function Navbar() {
11+
const [isScrolled, setIsScrolled] = useState(false);
12+
13+
useEffect(() => {
14+
const handleScroll = () => {
15+
const scrollTop = window.scrollY;
16+
setIsScrolled(scrollTop > 50);
17+
};
18+
19+
handleScroll();
20+
21+
window.addEventListener("scroll", handleScroll, { passive: true });
22+
23+
return () => {
24+
window.removeEventListener("scroll", handleScroll);
25+
};
26+
}, []);
27+
28+
return (
29+
<nav className={`${styles.navbar} ${isScrolled ? styles.scrolled : ""}`}>
30+
<Image
31+
src={isScrolled ? IMAGES.logo3 : IMAGES.logo5}
32+
alt="logo"
33+
width={200}
34+
height={100}
35+
className={styles.logo}
36+
/>
37+
<Link href={ROUTES.login} className={styles.login}>
38+
Login
39+
</Link>
40+
</nav>
41+
);
42+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import styles from "../page.module.scss";
2+
3+
const ROAD_MAP = [
4+
{
5+
title: "강사",
6+
steps: [
7+
"클래스 생성",
8+
"클래스별 강의 생성",
9+
"강의자료 업로드",
10+
"강의 시작 (필기 + 질문 뷰어 + 자동 녹음)",
11+
"강의 종료 후 AI 퀴즈 자동 생성 및 배포",
12+
"퀴즈 결과 기반 대시보드 제공",
13+
],
14+
},
15+
{
16+
title: "학생",
17+
steps: [
18+
"클래스 입장",
19+
"강의자료 다운로드",
20+
"실시간 질문 참여",
21+
"녹음 파일 다운로드 및 다시 듣기",
22+
"AI 퀴즈 풀기 + 결과 확인",
23+
],
24+
},
25+
];
26+
27+
export default function RoadMapSection() {
28+
return (
29+
<section className={styles.roadMapSection}>
30+
<div className={styles.roadMapHeader}>
31+
<h2 className={styles.roadMapTitle}>HOW IT WORKS?</h2>
32+
<div className={styles.roadMapDescription}>
33+
<div className={styles.roadMapText}>
34+
ClassLog, 어떻게 활용할 수 있을까요?
35+
<br />
36+
강사와 학생 각각의 흐름에 맞춰 스마트한 학습 경험을 제공합니다.
37+
<br />
38+
수업 전부터 수업 중, 수업 후까지 — 모든 과정을 ClassLog 하나로
39+
완성하세요.
40+
</div>
41+
<div className={styles.roadMapCta}>
42+
<p>
43+
Get to know More <br />
44+
about ClassLog
45+
</p>
46+
</div>
47+
</div>
48+
</div>
49+
<div className={styles.roadMapCards}>
50+
{ROAD_MAP.map((item) => (
51+
<div key={item.title} className={styles.roadMapCard}>
52+
<h3 className={styles.cardTitle}>{item.title}</h3>
53+
<ul className={styles.stepsList}>
54+
{item.steps.map((step, index) => (
55+
<li key={step} className={styles.stepItem}>
56+
<span className={styles.stepNumber}>{index + 1}</span>
57+
<span className={styles.stepText}>{step}</span>
58+
</li>
59+
))}
60+
</ul>
61+
</div>
62+
))}
63+
</div>
64+
</section>
65+
);
66+
}

0 commit comments

Comments
 (0)