Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
1bafe2f
✨ (#302) 라우터 설정
mumminn Aug 27, 2025
14d8418
✨ (#302) FitContentButton Props 추가
mumminn Aug 28, 2025
45aefa9
📁 (#302) 테스트용 pdf 파일 추가
mumminn Sep 3, 2025
59bd2e1
➕ (#302) PDF 뷰어 관련 의존성 추가
mumminn Sep 3, 2025
42ae776
✨ (#302) 페이지 썸네일 사이드 바 추가
mumminn Sep 3, 2025
6715e00
✨ (#302) 강의자료 viewer 추가
mumminn Sep 3, 2025
4baa9e3
✨ (#302) 헤더 아이콘 팝업 컴포넌트 추가
mumminn Sep 3, 2025
c0c4972
✨ (#302) 펜 Popover 추가
mumminn Sep 4, 2025
2a26236
✨ (#302) 강의자료 위에만 필기 가능하도록 CanvasOverlay 추가
mumminn Sep 4, 2025
8ed1152
✨ (#302) 강의자료 viewer에 CanvasOverlay 추가
mumminn Sep 4, 2025
3635ff5
💄 (#302) 강의자료 viewer css 변경
mumminn Sep 4, 2025
0fbbf41
📁 (#302) 테스트용 다른 pdf 파일 추가
mumminn Sep 4, 2025
d91586b
✨ (#302) 강의자료 type 추가
mumminn Sep 4, 2025
2677a14
✨ (#302) 지우개 Popover 추가
mumminn Sep 5, 2025
37c310d
✨ (#302) LectureLiveProvider 추가
mumminn Sep 5, 2025
e8fb227
✨ (#302) canvas overlay 지우개 설정 추가
mumminn Sep 5, 2025
74199f5
💄 (#302) 지우개 커서 css 추가
mumminn Sep 5, 2025
eab8187
✨ (#302) 형관펜 Popover 추가
mumminn Sep 6, 2025
46c6918
🐛 (#302) 펜 색 클릭시 클릭효과 바로 적용 안 되는 에러 수정
mumminn Sep 6, 2025
296d630
🐛 (#302) 지우개 전체 삭제 눌렀을 때 바로 삭제 안되는 에러 수정
mumminn Sep 8, 2025
6218334
✨ (#302) Header 펜 관련 버튼 추가
mumminn Sep 8, 2025
0fe7f73
✨ (#302) 강의자료 썸네일 사이드바 버튼 추가
mumminn Sep 8, 2025
f2cd7d0
✨ (#302) 강의자료 선택 Popover 추가
mumminn Sep 8, 2025
319c1bf
✨ (#302) 채팅 헤더 버튼 추가
mumminn Sep 8, 2025
35e60b2
♻️ (#302) 익명 모드일 때 시간, 메세지만 나오도록 변경
mumminn Sep 9, 2025
5b86ca4
✨ (#302) ChatBox Role에 따라 채팅 배경색 변경 추가
mumminn Sep 9, 2025
e26ef4f
💄 (#302) 새로운 질문 왔을 때 생기는 빨간점 위치 변경
mumminn Sep 9, 2025
7dbf31a
✨ (#302) 채팅 panel 추가
mumminn Sep 9, 2025
1414c24
💄 (#302) ToolPopover 위치, 색 수정
mumminn Sep 10, 2025
3bf0f49
✨ (#302) 녹음 버튼, 녹음 종료 시 발생하는 모달 추가
mumminn Sep 10, 2025
13fb9c7
➕ (#302) mic-recorder-to-mp3 라이브러리 설치
mumminn Sep 10, 2025
8bf746a
✨ (#302) 녹음 기능, 녹음 Popover 추가
mumminn Sep 10, 2025
85ab9af
♻️ (#302) 클라이언트에서 채팅 시간 넣는 방식으로 변경
mumminn Sep 10, 2025
37541bc
💄 (#302) 녹음 종료 모달 줄 바꿈 추가
mumminn Sep 11, 2025
be4183d
✨ (#302) 강의시작 라우팅 설정
mumminn Sep 11, 2025
668c166
✨ (#302) 강의 페이지 들어왔을 때 녹음 바로 시작하는지 물어보는 모달 추가
mumminn Sep 11, 2025
4bec3eb
♻️ (#302) 강의자료 로드 방식 변경
mumminn Sep 11, 2025
6c586f0
✨ (#302) 모든 컴포넌트 추가한 강의 main grid
mumminn Sep 11, 2025
c4e0d86
✨ (#302) 필요한 모든 컴포넌트 포함한 header, page추가
mumminn Sep 11, 2025
fc1b47e
♻️ (#302) 펜 디폴트 크기, 색 변경
mumminn Sep 11, 2025
0644ac9
💄 (#302) 형광펜, 펜 popover 선택 테두리 색상 통일, 배치 변경
mumminn Sep 11, 2025
2c22f30
✨ (#302) S3 관련 설정 추가
mumminn Sep 13, 2025
4c61eeb
🐛 (#302) 빌드 에러 수정
mumminn Sep 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import MakeQuizModal from "@/components/Modal/MakeQuizModal/MakeQuizModal";
import { FetchLectureDetailResult } from "@/types/lectures/fetchLectureDetailTypes";
import { fetchLectureDetail } from "@/api/lectures/fetchLectureDetail";
import { useLectureDetail } from "../LectureDetailContext";
import { useRouter } from "next/navigation";
import { ROUTES } from "@/constants/routes";

export default function LectureHeader() {
const { lectureId, setClassId, refreshKey } = useLectureDetail();
Expand All @@ -17,6 +19,8 @@ export default function LectureHeader() {
const [showQuizModal, setShowQuizModal] = useState(false);
const [loading, setLoading] = useState(true);

const router = useRouter();

const fetchData = useCallback(async () => {
try {
setLoading(true);
Expand All @@ -43,8 +47,7 @@ export default function LectureHeader() {
}, [lectureId, setClassId, fetchData, refreshKey]);

const handleStartLecture = () => {
// TODO: 강의 시작 로직 구현
console.log("강의 시작");
router.push(ROUTES.teacherLectureLive(lectureId));
};

const handleQuizModalClose = () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@import "@/styles/variables";

.wrap {
position: relative;
display: inline-flex;
}

.badge {
position: absolute;
top: 6px;
right: 6px;
width: 6px;
height: 6px;
border-radius: 50%;
background: #ef4444;
box-shadow: 0 0 0 2px #fff;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"use client";

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";

export default function ChatingButton({
className,
onPress,
}: {
className?: string;
onPress?: () => void;
}) {
const { panels, togglePanel } = useLive();
const isOpen = panels.chat;
const [unread, setUnread] = useState(false);

useEffect(() => {
if (isOpen) setUnread(false);
}, [isOpen]);

useEffect(() => {
const onNew = () => {
if (!isOpen) setUnread(true);
};
window.addEventListener("live:chat:new", onNew as EventListener);
return () => window.removeEventListener("live:chat:new", onNew as EventListener);
}, [isOpen]);

const onClick = () => {
onPress?.();
togglePanel("chat");
};

return (
<span className={`${styles.wrap} ${className ?? ""}`}>
<IconButton
ariaLabel="채팅"
onClick={onClick}
icon={<MessageCircleMore data-active={isOpen} />}
/>
{unread && !isOpen && <i className={styles.badge} aria-hidden />}
</span>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
@import "@/styles/variables";

.wrap {
height: 100%;
display: grid;
grid-template-rows: auto 1fr auto;
}

.header {
padding: $spacing-sm $spacing-sm 0 $spacing-sm;
display: flex;
align-items: center;
justify-content: space-between;
}

.headerTitle {
font-size: $font-size-lg;
font-weight: $font-weight-medium;
color: $color-neutral-1;
}

.closeBtn :global(button) {
width: 28px;
height: 28px;
padding: 0;
background: transparent;
border: none;
border-radius: 8px;

&:hover { background: $color-neutral-7; }
}

.body {
display: flex;
flex-direction: column;
overflow: auto;
padding: $spacing-sm;
gap: $spacing-xs;
}

.row {
display: flex;
}

.teacher { justify-content: flex-end; }
.student { justify-content: flex-start; }

.inputRow {
padding: $spacing-sm;
border-top: 1px solid $color-neutral-7;

:global(.inputBox) {
padding-right: 6px;
}
}

.inputRow :global(.iconRight) {
display: inline-flex;
align-items: center;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"use client";

import { useEffect, useRef, useState } from "react";
import styles from "./ChatingPanel.module.scss";
import IconButton from "@/components/Button/IconButton/IconButton";
import { X, SendHorizontal } from "lucide-react";
import { useLive } from "../../LectureLiveProvider";
import ChatBox from "@/components/ChatBox/ChatBox";
import BasicInput from "@/components/Input/BasicInput/BasicInput";

type Msg = {
id: string;
text: string;
role: "teacher" | "student";
ts?: number;
};

export default function ChatPanel() {
const { togglePanel } = useLive();

const [msgs, setMsgs] = useState<Msg[]>([
{ id: "seed1", text: "질문이요~", role: "student", ts: Date.now()},
]);
const [text, setText] = useState("");

const bodyRef = useRef<HTMLDivElement>(null);

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

const send = () => {
const t = text.trim();
if (!t) return;
setMsgs((m) => [
...m,
{ id: String(Date.now()), text: t, role: "teacher", ts: Date.now() },
]);
setText("");
};

const onSubmit: React.FormEventHandler = (e) => {
e.preventDefault();
send();
};

useEffect(() => {
const el = bodyRef.current;
if (!el) return;
el.scrollTop = el.scrollHeight;
}, [msgs]);

const pad = (n: number) => n.toString().padStart(2, "0");
const fmt = (ts: number) => {
const d = new Date(ts);
const yy = pad(d.getFullYear() % 100);
const MM = pad(d.getMonth() + 1);
const dd = pad(d.getDate());
const hh = pad(d.getHours());
const mm = pad(d.getMinutes());
const ss = pad(d.getSeconds());
return `${yy}.${MM}.${dd} ${hh}:${mm}:${ss}`;
};

return (
<div className={styles.wrap}>
<div className={styles.header}>
<span className={styles.headerTitle}>Question</span>
<div className={styles.closeBtn}>
<IconButton ariaLabel="채팅 닫기" onClick={closeChat} icon={<X />} />
</div>
</div>

<div ref={bodyRef} className={styles.body}>
{msgs.map((m) => {
const tsText = m.ts ? fmt(m.ts) : "";
return (
<div
key={m.id}
className={`${styles.row} ${
m.role === "teacher" ? styles.teacher : styles.student
}`}
>
<ChatBox
isAnonymous={true}
nickname=""
profilePicture=""
message={m.text}
timestamp={tsText}
variant={m.role === "teacher" ? "teacher" : "student"}
/>
</div>
);
})}
</div>

<form className={styles.inputRow} onSubmit={onSubmit}>
<BasicInput
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="답변 입력하기"
iconRight={
<IconButton
ariaLabel="전송"
onClick={send}
icon={<SendHorizontal size={18} color="#9AA4B2" />}
/>
}
/>
</form>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"use client";

import IconButton from "@/components/Button/IconButton/IconButton";
import { PanelLeft, PanelLeftDashed } from "lucide-react";
import { useLive } from "../LectureLiveProvider";

export type DocumentSideButtonProps = {
open: boolean;
onToggle: () => void;
ariaLabelOpen?: string;
ariaLabelClose?: string;
disabled?: boolean;
};

export default function DocumentSideButton({
open,
onToggle,
ariaLabelOpen = "슬라이드 패널 열기",
ariaLabelClose = "슬라이드 패널 닫기",
disabled = false,
}: DocumentSideButtonProps) {
const label = open ? ariaLabelClose : ariaLabelOpen;

return (
<IconButton
ariaLabel={label}
onClick={onToggle}
disabled={disabled}
icon={open ? <PanelLeft /> : <PanelLeftDashed />}
/>
);
}

export function DocumentSideButtonConnected() {
const { panels, togglePanel } = useLive();
return (
<DocumentSideButton
open={panels.files}
onToggle={() => togglePanel("files")}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
@import "@/styles/variables";

.container {
width: 100%;
background: $color-white;
border-bottom: 1px solid $color-neutral-7;
}

.inner {
height: auto;
display: flex;
align-items: center;
justify-content: space-between;
padding: 3px $spacing-md;
}

.left {
display: flex;
align-items: center;
gap: $spacing-xs;
}

.divider {
width: 1px;
height: 20px;
background: $color-neutral-7;
margin: 0 $spacing-xs;
}


.left svg[data-active="true"] {
color: $color-blue;
stroke: $color-blue;
}


.endBtn {
align-self: center;
height: auto !important;
min-height: 0 !important;
line-height: 2;
padding-block: 3px !important;
}

.docBtnZ {
position: relative;
z-index: 2000;
}
Loading