Skip to content

Commit 174ea9c

Browse files
authored
Merge pull request #325 from KW-ClassLog/Feat/#319/lecture-live-api
✨ Feat/#319 강의중 화면 API 연동
2 parents fa8c57b + 6bc8e25 commit 174ea9c

20 files changed

Lines changed: 612 additions & 244 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { axiosInstance } from "@/api/axiosInstance";
2+
import { ENDPOINTS } from "@/constants/endpoints";
3+
import { ApiResponse } from "@/types/apiResponseTypes";
4+
import { SaveAudioFileResult } from "@/types/lectures/saveAudioFileTypes";
5+
6+
export async function saveAudioFile( lectureId: string, blob: Blob) {
7+
8+
const file = new File([blob], `${lectureId}.mp3`, { type: "audio/mpeg" });
9+
10+
const formData = new FormData();
11+
formData.append("file", file);
12+
13+
const response = await axiosInstance.post<ApiResponse<SaveAudioFileResult | null>>(
14+
ENDPOINTS.LECTURES.SAVE_RECORDING(lectureId),
15+
formData,
16+
{
17+
headers: { "Content-Type": "multipart/form-data" },
18+
}
19+
);
20+
21+
return response.data;
22+
}

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

Lines changed: 41 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -7,57 +7,53 @@ 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";
10-
11-
type Msg = {
12-
id: string;
13-
text: string;
14-
role: "teacher" | "student";
15-
ts?: number;
16-
};
10+
import { useParams } from "next/navigation";
11+
import { useLectureChat } from "@/hooks/useLectureChat";
1712

1813
export default function ChatPanel() {
1914
const { togglePanel } = useLive();
15+
const { lectureId } = useParams<{ lectureId: string }>();
2016

21-
const [msgs, setMsgs] = useState<Msg[]>([
22-
{ id: "seed1", text: "질문이요~", role: "student", ts: Date.now()},
23-
]);
24-
const [text, setText] = useState("");
17+
// 소켓 연결
18+
const { messages, connected, sendMessage } = useLectureChat(lectureId);
2519

20+
const [text, setText] = useState("");
2621
const bodyRef = useRef<HTMLDivElement>(null);
2722

2823
const closeChat = () => togglePanel("chat");
2924

25+
// 메시지 전송
3026
const send = () => {
3127
const t = text.trim();
3228
if (!t) return;
33-
setMsgs((m) => [
34-
...m,
35-
{ id: String(Date.now()), text: t, role: "teacher", ts: Date.now() },
36-
]);
29+
sendMessage(t);
3730
setText("");
3831
};
3932

33+
// Enter로 전송
4034
const onSubmit: React.FormEventHandler = (e) => {
4135
e.preventDefault();
4236
send();
4337
};
4438

39+
// 새로운 메시지 오면 스크롤 맨 아래로 이동
4540
useEffect(() => {
4641
const el = bodyRef.current;
4742
if (!el) return;
4843
el.scrollTop = el.scrollHeight;
49-
}, [msgs]);
44+
}, [messages]);
5045

51-
const pad = (n: number) => n.toString().padStart(2, "0");
52-
const fmt = (ts: number) => {
46+
47+
const fmt = (ts: string) => {
5348
const d = new Date(ts);
54-
const yy = pad(d.getFullYear() % 100);
55-
const MM = pad(d.getMonth() + 1);
56-
const dd = pad(d.getDate());
57-
const hh = pad(d.getHours());
58-
const mm = pad(d.getMinutes());
59-
const ss = pad(d.getSeconds());
60-
return `${yy}.${MM}.${dd} ${hh}:${mm}:${ss}`;
49+
return d.toLocaleString("ko-KR", {
50+
year: "2-digit",
51+
month: "2-digit",
52+
day: "2-digit",
53+
hour: "2-digit",
54+
minute: "2-digit",
55+
second: "2-digit",
56+
});
6157
};
6258

6359
return (
@@ -70,38 +66,37 @@ export default function ChatPanel() {
7066
</div>
7167

7268
<div ref={bodyRef} className={styles.body}>
73-
{msgs.map((m) => {
74-
const tsText = m.ts ? fmt(m.ts) : "";
75-
return (
76-
<div
77-
key={m.id}
78-
className={`${styles.row} ${
79-
m.role === "teacher" ? styles.teacher : styles.student
80-
}`}
81-
>
82-
<ChatBox
83-
isAnonymous={true}
84-
nickname=""
85-
profilePicture=""
86-
message={m.text}
87-
timestamp={tsText}
88-
variant={m.role === "teacher" ? "teacher" : "student"}
89-
/>
90-
</div>
91-
);
92-
})}
69+
{messages.map((m, i) => (
70+
<div
71+
key={i}
72+
className={`${styles.row} ${
73+
m.role === "TEACHER" ? styles.teacher : styles.student
74+
}`}
75+
>
76+
<ChatBox
77+
isAnonymous={true}
78+
nickname={m.senderName ?? ""}
79+
profilePicture=""
80+
message={m.content}
81+
timestamp={fmt(m.timestamp)}
82+
variant={m.role === "TEACHER" ? "teacher" : "student"}
83+
/>
84+
</div>
85+
))}
9386
</div>
9487

9588
<form className={styles.inputRow} onSubmit={onSubmit}>
9689
<BasicInput
9790
value={text}
9891
onChange={(e) => setText(e.target.value)}
99-
placeholder="답변 입력하기"
92+
placeholder={connected ? "답변 입력하기" : "연결 중..."}
93+
disabled={!connected}
10094
iconRight={
10195
<IconButton
10296
ariaLabel="전송"
10397
onClick={send}
10498
icon={<SendHorizontal size={18} color="#9AA4B2" />}
99+
disabled={!connected}
105100
/>
106101
}
107102
/>

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

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,4 @@
4040
min-height: 0 !important;
4141
line-height: 2;
4242
padding-block: 3px !important;
43-
}
44-
45-
.docBtnZ {
46-
position: relative;
47-
z-index: 2000;
48-
}
43+
}

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

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,20 @@
11
"use client";
22

3-
import { useState, useRef, useMemo, useEffect } from "react";
3+
import { useState, useMemo, useEffect } from "react";
44
import { useRouter, useParams } from "next/navigation";
55
import styles from "./LectureLiveHeader.module.scss";
66
import FitContentButton from "@/components/Button/FitContentButton/FitContentButton";
7-
import IconButton from "@/components/Button/IconButton/IconButton";
87
import { DocumentSideButtonConnected } from "../DocumentSideButton/DocumentSideButton";
98
import PenToolButtons from "../PenTool/PenToolButtons/PenToolButtons";
109
import { useLive } from "../LectureLiveProvider";
11-
import { FileText } from "lucide-react";
12-
import ToolPopover from "../ToolPopover/ToolPopover";
13-
import LectureNotePopover from "../LectureNote/LectureNotePopover/LectureNotePopover";
1410
import ChatingButton from "../Chating/ChatingButton/ChatingButton";
1511
import RecordingButton from "../Recording/RecordingButton/RecordingButton";
1612
import ConfirmModal from "@/components/Modal/ConfirmModal/ConfirmModal";
1713
import { getRecordingEngine, type RecState } from "../Recording/recordingEngine";
1814
import { ROUTES } from "@/constants/routes";
1915
import { Tool } from "../LectureLiveProvider";
16+
import LectureNoteButton from "../LectureNote/LectureNoteButton/LectureNoteButton";
17+
import { saveAudioFile } from "@/api/lectures/saveAudioFile";
2018

2119
export default function LectureLiveHeader({
2220
onToggleChat,
@@ -28,10 +26,10 @@ export default function LectureLiveHeader({
2826
onEndLecture?: () => void;
2927
}) {
3028
const { tool, setTool } = useLive();
31-
const docBtnRef = useRef<HTMLSpanElement>(null);
32-
const [openDoc, setOpenDoc] = useState(false);
3329

3430
const [endOpen, setEndOpen] = useState(false);
31+
const [saving, setSaving] = useState(false);
32+
3533

3634
const engine = useMemo(() => getRecordingEngine(), []);
3735
const [recState, setRecState] = useState<RecState>(engine.getSnapshot().state);
@@ -56,14 +54,33 @@ export default function LectureLiveHeader({
5654
const handleConfirmEnd = async () => {
5755
if (isRecording) {
5856
try {
59-
await engine.stop();
57+
setSaving(true);
58+
await new Promise<void>((resolve, reject) => {
59+
const off = engine.subscribe("done", async (blob) => {
60+
try {
61+
if (lectureId) {
62+
await saveAudioFile(lectureId, blob);
63+
console.log("🎤 녹음 파일 저장 완료");
64+
}
65+
off();
66+
resolve();
67+
} catch (e) {
68+
console.error("❌ 녹음 파일 저장 실패:", e);
69+
reject(e);
70+
}
71+
});
72+
73+
engine.stop().catch(reject);
74+
});
6075
} catch (e) {
61-
console.error(e);
76+
console.error("녹음 종료 중 오류:", e);
77+
} finally {
78+
setSaving(false);
6279
}
6380
}
81+
6482
setEndOpen(false);
6583
onEndLecture?.();
66-
6784
router.push(ROUTES.teacherLectureDetail(lectureId));
6885
};
6986

@@ -75,23 +92,7 @@ export default function LectureLiveHeader({
7592
<div className={styles.left}>
7693
<DocumentSideButtonConnected />
7794

78-
<span ref={docBtnRef} className={styles.docBtnZ}>
79-
<IconButton
80-
ariaLabel="문서 불러오기"
81-
onClick={() => setOpenDoc((v) => !v)}
82-
icon={<FileText />}
83-
/>
84-
</span>
85-
86-
<ToolPopover
87-
open={openDoc}
88-
anchorRef={docBtnRef}
89-
onClose={() => setOpenDoc(false)}
90-
align="start"
91-
side="bottom"
92-
>
93-
<LectureNotePopover onPicked={() => setOpenDoc(false)} />
94-
</ToolPopover>
95+
<LectureNoteButton />
9596

9697
<span className={styles.divider} />
9798
<PenToolButtons value={tool} onChange={selectTool} />
@@ -113,8 +114,14 @@ export default function LectureLiveHeader({
113114
</div>
114115

115116
{endOpen && (
116-
<ConfirmModal onConfirm={handleConfirmEnd} onClose={handleCancelEnd}>
117-
{isRecording ? (
117+
<ConfirmModal
118+
onConfirm={handleConfirmEnd}
119+
onClose={handleCancelEnd}
120+
disableActions={saving}
121+
>
122+
{saving ? (
123+
<>녹음 파일 저장 중입니다... ⏳</>
124+
) : isRecording ? (
118125
<>
119126
지금 녹음이 진행 중입니다.
120127
<br />

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import React, { createContext, useContext, useMemo, useState, useRef, useCallback } from "react";
44

5-
export type DocType = "pdf" | "pptx" | "unknown";
5+
export type DocType = "pdf" | "unknown";
66
type DocState = { url: string; type: DocType; name: string };
77

88
export type Tool = "pencilOff" | "pen" | "eraser" | "highlighter";
@@ -61,9 +61,9 @@ export function LectureLiveProvider({ children }: { children: React.ReactNode })
6161
});
6262

6363
const [doc, setDocState] = useState<DocState>({
64-
url: "/file/기말보고서_졸업을하자.pdf",
65-
type: "pdf",
66-
name: "기말보고서_졸업을하자.pdf",
64+
url: "",
65+
type: "unknown",
66+
name: "",
6767
});
6868

6969
const drawStoreRef = useRef<Map<number, HTMLCanvasElement>>(new Map());

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,27 @@
5151
--right-col: 0px;
5252
grid-template-columns: 1fr;
5353
}
54+
}
55+
56+
.empty {
57+
height: 100%;
58+
display:flex; align-items:center; justify-content:center;
59+
}
60+
.emptyCard {
61+
padding: 20px 24px; border:1px dashed #d6d6d6; border-radius:12px;
62+
background:#fafafa; text-align:center;
63+
}
64+
.emptyTitle { font-weight:700; margin-bottom:6px; }
65+
.emptyDesc { color:#666; }
66+
.leftEmpty {
67+
height:100%; display:flex; align-items:center; justify-content:center;
68+
color:#777; font-size: 0.92rem; padding: 0 12px; text-align:center;
69+
}
70+
71+
.inlineIcon {
72+
width: 1.2em;
73+
height: 1.2em;
74+
vertical-align: middle;
75+
margin-bottom: 0.3em;
76+
margin-right: 0.1em;
5477
}

0 commit comments

Comments
 (0)