Skip to content

Commit 986cdb9

Browse files
authored
Merge pull request #343 from KW-ClassLog/Feat/#336/save-chatting-api
✨ Feat/#336 강의중 화면 채팅 저장, 이전 채팅 불러오기 API 연동
2 parents ee59ec1 + 45ab082 commit 986cdb9

File tree

9 files changed

+117
-33
lines changed

9 files changed

+117
-33
lines changed
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+
}

frontend/app/teacher/lecture-live/[lectureId]/_components/Chating/ChatingButton/ChatingButton.module.scss renamed to frontend/app/teacher/lecture-live/[lectureId]/_components/Chatting/ChattingButton/ChattingButton.module.scss

File renamed without changes.

frontend/app/teacher/lecture-live/[lectureId]/_components/Chating/ChatingButton/ChatingButton.tsx renamed to frontend/app/teacher/lecture-live/[lectureId]/_components/Chatting/ChattingButton/ChattingButton.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import { useEffect, useState } from "react";
44
import IconButton from "@/components/Button/IconButton/IconButton";
55
import { MessageCircleMore } from "lucide-react";
66
import { useLive } from "../../LectureLiveProvider";
7-
import styles from "./ChatingButton.module.scss";
7+
import styles from "./ChattingButton.module.scss";
88

9-
export default function ChatingButton({
9+
export default function ChattingButton({
1010
className,
1111
onPress,
1212
}: {

frontend/app/teacher/lecture-live/[lectureId]/_components/Chating/ChatingPanel/ChatingPanel.module.scss renamed to frontend/app/teacher/lecture-live/[lectureId]/_components/Chatting/ChattingPanel/ChattingPanel.module.scss

File renamed without changes.

frontend/app/teacher/lecture-live/[lectureId]/_components/Chating/ChatingPanel/ChatingPanel.tsx renamed to frontend/app/teacher/lecture-live/[lectureId]/_components/Chatting/ChattingPanel/ChattingPanel.tsx

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
"use client";
22

3-
import { useEffect, useRef, useState } from "react";
4-
import styles from "./ChatingPanel.module.scss";
3+
import { useEffect, useRef, useState, useMemo } from "react";
4+
import styles from "./ChattingPanel.module.scss";
55
import IconButton from "@/components/Button/IconButton/IconButton";
66
import { X, SendHorizontal } from "lucide-react";
77
import { useLive } from "../../LectureLiveProvider";
88
import ChatBox from "@/components/ChatBox/ChatBox";
99
import BasicInput from "@/components/Input/BasicInput/BasicInput";
1010
import { useParams } from "next/navigation";
11-
import { useLectureChat } from "@/hooks/useLectureChat";
11+
import { useLectureChat, type ChatMessage } from "@/hooks/useLectureChat";
12+
import { fetchChattingList } from "@/api/lectures/fetchChattingList";
1213

13-
export default function ChatPanel() {
14+
export default function ChattingPanel() {
1415
const { togglePanel } = useLive();
1516
const { lectureId } = useParams<{ lectureId: string }>();
1617

@@ -20,6 +21,9 @@ export default function ChatPanel() {
2021
const [text, setText] = useState("");
2122
const bodyRef = useRef<HTMLDivElement>(null);
2223

24+
const [previousMessages, setPreviousMessages] = useState<ChatMessage[]>([]);
25+
const fetchedOnceRef = useRef(false);
26+
2327
const closeChat = () => togglePanel("chat");
2428

2529
// 메시지 전송
@@ -36,13 +40,53 @@ export default function ChatPanel() {
3640
send();
3741
};
3842

43+
// 연결 직후 과거 대화 불러오기
44+
useEffect(() => {
45+
if (!lectureId || !connected || fetchedOnceRef.current) return;
46+
47+
let isMounted = true;
48+
(async () => {
49+
try {
50+
const res = await fetchChattingList(lectureId);
51+
if (!isMounted) return;
52+
53+
if (res.isSuccess && Array.isArray(res.result)) {
54+
const mapped: ChatMessage[] = res.result.map((m) => ({
55+
senderId: null,
56+
senderName: null,
57+
content: m.content,
58+
role: m.role,
59+
timestamp: m.timestamp,
60+
}));
61+
setPreviousMessages(mapped);
62+
} else {
63+
setPreviousMessages([]);
64+
}
65+
} catch (e) {
66+
setPreviousMessages([]);
67+
console.error("과거 채팅 불러오기 실패:", e);
68+
} finally {
69+
fetchedOnceRef.current = true;
70+
}
71+
})();
72+
73+
return () => {
74+
isMounted = false;
75+
};
76+
}, [lectureId, connected]);
77+
78+
// 과거 + 실시간 합친 배열
79+
const combinedMessages = useMemo(
80+
() => [...previousMessages, ...messages],
81+
[previousMessages, messages]
82+
);
83+
3984
// 새로운 메시지 오면 스크롤 맨 아래로 이동
4085
useEffect(() => {
4186
const el = bodyRef.current;
4287
if (!el) return;
4388
el.scrollTop = el.scrollHeight;
44-
}, [messages]);
45-
89+
}, [combinedMessages]);
4690

4791
const fmt = (ts: string) => {
4892
const d = new Date(ts);
@@ -66,7 +110,7 @@ export default function ChatPanel() {
66110
</div>
67111

68112
<div ref={bodyRef} className={styles.body}>
69-
{messages.map((m, i) => (
113+
{combinedMessages.map((m, i) => (
70114
<div
71115
key={i}
72116
className={`${styles.row} ${

frontend/app/teacher/lecture-live/[lectureId]/_components/LectureLiveHeader/LectureLiveHeader.tsx

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ import FitContentButton from "@/components/Button/FitContentButton/FitContentBut
77
import { DocumentSideButtonConnected } from "../DocumentSideButton/DocumentSideButton";
88
import PenToolButtons from "../PenTool/PenToolButtons/PenToolButtons";
99
import { useLive } from "../LectureLiveProvider";
10-
import ChatingButton from "../Chating/ChatingButton/ChatingButton";
10+
import ChattingButton from "../Chatting/ChattingButton/ChattingButton";
1111
import RecordingButton from "../Recording/RecordingButton/RecordingButton";
1212
import ConfirmModal from "@/components/Modal/ConfirmModal/ConfirmModal";
1313
import { getRecordingEngine, type RecState } from "../Recording/recordingEngine";
1414
import { ROUTES } from "@/constants/routes";
1515
import { Tool } from "../LectureLiveProvider";
1616
import LectureNoteButton from "../LectureNote/LectureNoteButton/LectureNoteButton";
1717
import { saveAudioFile } from "@/api/lectures/saveAudioFile";
18+
import { saveChatting } from "@/api/lectures/saveChatting";
1819

1920
export default function LectureLiveHeader({
2021
onToggleChat,
@@ -52,36 +53,50 @@ export default function LectureLiveHeader({
5253
const { lectureId } = useParams<{ lectureId: string }>();
5354

5455
const handleConfirmEnd = async () => {
55-
if (isRecording) {
56-
try {
57-
setSaving(true);
56+
if (!lectureId) {
57+
console.error("lectureId가 없습니다.");
58+
return;
59+
}
60+
61+
setSaving(true);
62+
63+
try {
64+
// 녹음 중일 경우 녹음 저장
65+
if (isRecording) {
66+
67+
5868
await new Promise<void>((resolve, reject) => {
5969
const off = engine.subscribe("done", async (blob) => {
6070
try {
61-
if (lectureId) {
62-
await saveAudioFile(lectureId, blob);
63-
console.log("🎤 녹음 파일 저장 완료");
64-
}
71+
await saveAudioFile(lectureId, blob);
72+
console.log("🎤 녹음 파일 저장 완료");
6573
off();
6674
resolve();
6775
} catch (e) {
6876
console.error("❌ 녹음 파일 저장 실패:", e);
77+
off();
6978
reject(e);
7079
}
7180
});
72-
7381
engine.stop().catch(reject);
7482
});
75-
} catch (e) {
76-
console.error("녹음 종료 중 오류:", e);
77-
} finally {
78-
setSaving(false);
7983
}
80-
}
84+
85+
// 채팅 저장
86+
const chatRes = await saveChatting(lectureId);
8187

82-
setEndOpen(false);
83-
onEndLecture?.();
84-
router.push(ROUTES.teacherLectureDetail(lectureId));
88+
if (!chatRes?.isSuccess) {
89+
console.warn("채팅 저장 실패:", chatRes?.code, chatRes?.message);
90+
}
91+
92+
setEndOpen(false);
93+
onEndLecture?.();
94+
router.push(ROUTES.teacherLectureDetail(lectureId));
95+
} catch (e) {
96+
console.error("강의 종료 처리 중 오류:", e);
97+
} finally {
98+
setSaving(false);
99+
}
85100
};
86101

87102
const handleCancelEnd = () => setEndOpen(false);
@@ -100,7 +115,7 @@ export default function LectureLiveHeader({
100115

101116
<RecordingButton />
102117

103-
<ChatingButton
118+
<ChattingButton
104119
onPress={() => {
105120
closePen();
106121
onToggleChat?.();
@@ -120,7 +135,7 @@ export default function LectureLiveHeader({
120135
disableActions={saving}
121136
>
122137
{saving ? (
123-
<>녹음 파일 저장 중입니다... ⏳</>
138+
<>저장 중입니다... ⏳</>
124139
) : isRecording ? (
125140
<>
126141
지금 녹음이 진행 중입니다.

frontend/app/teacher/lecture-live/[lectureId]/_components/LectureMainGrid/LectureMainGrid.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { useEffect, useState } from "react";
44
import styles from "./LectureMainGrid.module.scss";
55
import { useLive } from "../LectureLiveProvider";
6-
import ChatPanel from "../Chating/ChatingPanel/ChatingPanel";
6+
import ChattingPanel from "../Chatting/ChattingPanel/ChattingPanel";
77
import dynamic from "next/dynamic";
88
import { FileText } from "lucide-react";
99

@@ -119,7 +119,7 @@ export default function LectureMainGrid() {
119119
</section>
120120

121121
<aside className={styles.right}>
122-
<ChatPanel />
122+
<ChattingPanel />
123123
</aside>
124124
</main>
125125
);

frontend/constants/endpoints.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,12 @@ export const ENDPOINTS = {
9494
`${BASE_API}/lectures/${lectureId}/recordings`,
9595
GET_RECORDING: (lectureId: string) =>
9696
`${BASE_API}/lectures/${lectureId}/recordings`,
97-
97+
9898
// 채팅 관련
9999
SAVE_CHAT: (lectureId: string) =>
100-
`${BASE_API}/lectures/${lectureId}/chating`,
100+
`${BASE_API}/lectures/chatting/after/${lectureId}`,
101101
GET_CHAT: (lectureId: string) =>
102-
`${BASE_API}/lectures/${lectureId}/chating`,
102+
`${BASE_API}/lectures/${lectureId}/chatting`,
103103
},
104104

105105
// 퀴즈 관련

frontend/hooks/useLectureChat.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ export function useLectureChat(lectureId: string | undefined) {
4848
try {
4949
const parsed: ChatMessage = JSON.parse(message.body);
5050
setMessages((prev) => [...prev, parsed]);
51+
52+
if (typeof window !== "undefined") {
53+
if (parsed.role !== "TEACHER") {
54+
window.dispatchEvent(new CustomEvent("live:chat:new", { detail: parsed }));
55+
}
56+
}
5157
} catch (e) {
5258
console.error("❌ 메시지 파싱 실패:", e);
5359
}

0 commit comments

Comments
 (0)