feat: 라이브 강의 페이지 추가

This commit is contained in:
jhynsoo 2024-07-26 16:51:46 +09:00
parent f7f7554545
commit f841bf8b3f
14 changed files with 175 additions and 32 deletions

View File

@ -6,9 +6,9 @@ import NotFoundPage from './pages/NotFoundPage';
import { lazy } from 'react';
import MyPageLayout from './components/Layout/MyPageLayout';
import LearningLecturesPage from './pages/LearningLecturesPage/LearningLecturesPage';
import ChatRoom from './components/ChatRoom/ChatRoom';
import { MaxWidthLayout } from './components/Layout';
const LiveLayout = lazy(async () => await import('./components/Layout/LiveLayout'));
const LivePage = lazy(async () => await import('./pages/LivePage'));
const LectureLayout = lazy(async () => await import('./components/Layout/LectureLayout'));
const LearningLectureDetailPage = lazy(async () => await import('./pages/LearningLectureDetailPage'));
const NoticeListPage = lazy(async () => await import('./pages/NoticeListPage'));
@ -25,6 +25,16 @@ const MyInfoChangePage = lazy(async () => await import('./pages/MyInfoChangePage
const PasswordChangePage = lazy(async () => await import('./pages/PasswordChangePage'));
const router = createBrowserRouter([
{
path: 'live/:roomId',
element: <LiveLayout />,
children: [
{
index: true,
element: <LivePage />,
},
],
},
{
path: '',
element: <PageLayout />,
@ -38,17 +48,6 @@ const router = createBrowserRouter([
path: 'lecture/:lectureId/info',
element: <LectureInfoPage />,
},
{
// TODO:
path: 'chat/:lectureId',
element: (
<MaxWidthLayout>
<main>
<ChatRoom />
</main>
</MaxWidthLayout>
),
},
{
path: 'lecture/:lectureId',
element: <LectureLayout />,

View File

@ -4,13 +4,16 @@ import SendIcon from '/src/assets/icons/send.svg?react';
import useChatRoom from '../../hooks/chat/useChatRoom';
export default function ChatRoom() {
const { lectureId } = useParams();
const { messages, handleSubmit, inputRef } = useChatRoom(lectureId);
const { roomId } = useParams();
const { messages, handleSubmit, inputRef, chatListRef } = useChatRoom(roomId);
return (
<section className={styles.room}>
<h2 className={styles.title}>채팅</h2>
<ol className={styles.messageList}>
<ol
className={styles.messageList}
ref={chatListRef}
>
{messages.map((message) => (
<li
key={message.id}

View File

@ -1,37 +1,32 @@
.room {
display: flex;
flex-direction: column;
gap: 8px;
justify-content: space-between;
position: relative;
width: 100%;
height: 100%;
border: 1px solid var(--border-color);
border-radius: 16px;
overflow: hidden;
overflow-y: hidden;
}
.title {
position: sticky;
top: 0;
margin: 0;
/* padding: 16px 16px 32px; */
padding: 16px;
background: linear-gradient(0deg, var(--whiteOpacity50) 0%, var(--background) 50%);
/* background-color: var(--whiteOpacity500);
backdrop-filter: blur(16px); */
z-index: 9;
}
.messageList {
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: end;
align-items: start;
gap: 12px;
list-style: none;
height: 100%;
margin: 0;
padding: 0 16px;
overflow-y: auto;
white-space: nowrap;
overflow-x: hidden;
overflow-y: auto;
box-sizing: border-box;
}
@keyframes show {
@ -50,7 +45,11 @@
display: flex;
flex-direction: column;
animation: show 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
max-width: 100%;
padding: 0 16px 0 0;
word-break: break-word;
transform-origin: left bottom;
box-sizing: border-box;
& > .bubble {
padding: 8px 16px;
@ -65,8 +64,8 @@
}
.my {
padding: 0 0 0 16px;
align-self: end;
align-items: end;
transform-origin: right bottom;
& > .name {
@ -97,12 +96,12 @@
border-radius: 12px;
box-shadow: var(--shadow);
overflow: hidden;
z-index: 9;
& > input[type='text'] {
flex: 1;
padding: 16px;
padding: 12px 16px;
width: 100%;
height: 100%;
border: none;
border-radius: 12px;
color: var(--text-color);
@ -110,6 +109,7 @@
line-height: 1.4;
font-weight: 400;
outline: none;
box-sizing: border-box;
}
& > button {

View File

@ -0,0 +1,14 @@
import { Outlet } from 'react-router-dom';
import styles from './LiveLayout.module.css';
import { LiveHeader } from '../LiveHeader';
export default function LiveLayout() {
return (
<>
<LiveHeader />
<div className={styles.wrapper}>
<Outlet />
</div>
</>
);
}

View File

@ -0,0 +1,24 @@
.wrapper {
display: flex;
justify-content: stretch;
align-items: stretch;
width: 100vw;
height: 100vh;
padding-top: 64px;
box-sizing: border-box;
& > main {
flex: 1 1 auto;
width: 100%;
}
& > aside {
flex-shrink: 0;
display: flex;
flex-direction: column;
width: 320px;
height: 100%;
background-color: var(--background);
border-left: 1px solid var(--border-color);
}
}

View File

@ -1,2 +1,3 @@
export { default as PageLayout } from './PageLayout';
export { default as MaxWidthLayout } from './MaxWidthLayout';
export { default as LiveLayout } from './LiveLayout';

View File

@ -0,0 +1,26 @@
import styles from './LiveHeader.module.css';
export default function LiveHeader() {
const { data } = {
data: {
title: '정보처리기사 실기 완전정복',
subtitle: '2차시',
participants: 3,
},
};
return (
<header className={styles.header}>
<div className={styles.content}>
<div className={styles.area}>
<h1>{data.title}</h1>
<div>{data.subtitle}</div>
</div>
<div className={styles.area}>
<h2>참가자 </h2>
<div>{data.participants}</div>
</div>
</div>
</header>
);
}

View File

@ -0,0 +1,41 @@
.header {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 64px;
background-color: var(--background);
border-bottom: 1px solid var(--border-color);
z-index: 9999;
}
.content {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
height: 100%;
padding: 0 40px;
margin: 0 auto;
box-sizing: border-box;
}
.area {
display: flex;
align-items: center;
gap: 4px;
& > h1,
& > h2 {
font-size: 16px;
line-height: 1.4;
font-weight: 700;
color: var(--text-color);
margin: 0;
}
& > div {
font-weight: 400;
color: var(--text-color-secondary);
}
}

View File

@ -0,0 +1 @@
export { default as LiveHeader } from './LiveHeader';

View File

@ -9,6 +9,7 @@ export default function useChatRoom(roomId) {
const [messages, setMessages] = useState([]);
const userName = useBoundStore((state) => state.userName) ?? '익명';
const inputRef = useRef(null);
const chatListRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
@ -34,6 +35,7 @@ export default function useChatRoom(roomId) {
client.subscribe(`/sub/channel/${roomId}`, (response) => {
const data = JSON.parse(response.body);
const { content: message, name } = data;
setMessages((prev) => [...prev, { id: prev.length, text: message, isMine: USER_ID === data.userId, name }]);
});
};
@ -44,9 +46,16 @@ export default function useChatRoom(roomId) {
};
}, [client, roomId]);
useEffect(() => {
if (chatListRef.current.scrollHeight - chatListRef.current.scrollTop - chatListRef.current.clientHeight < 200) {
chatListRef.current.scrollTop = chatListRef.current.scrollHeight;
}
}, [messages]);
return {
messages,
inputRef,
handleSubmit,
chatListRef,
};
}

View File

@ -0,0 +1,14 @@
import useBoundStore from '../../store';
import ChatRoom from '../../components/ChatRoom/ChatRoom';
export default function LivePage() {
const liveTabStatus = useBoundStore((state) => state.liveTabStatus);
// const setLiveTabStatus = useBoundStore((state) => state.setLiveTabStatus);
return (
<>
<main></main>
<aside>{liveTabStatus === 'chat' ? <ChatRoom /> : <div>Quiz</div>}</aside>
</>
);
}

View File

@ -0,0 +1 @@
export { default } from './LivePage';

View File

@ -1,11 +1,15 @@
import { create } from 'zustand';
import { userTypeSlice } from './userTypeSlice';
import { tokenSlice } from './tokenSlice';
import { userNameSlice } from './userNameSlice';
import { liveSlice } from './liveSlice';
const useBoundStore = create((...a) => ({
// TODO: persist 옵션 추가
...userTypeSlice(...a),
...tokenSlice(...a),
...userNameSlice(...a),
...liveSlice(...a),
}));
export default useBoundStore;

View File

@ -0,0 +1,6 @@
// liveTabStatus: 'chat' | 'quiz'
export const liveSlice = (set) => ({
liveTabStatus: 'chat',
setLiveTabStatus: (liveTabStatus) => set({ liveTabStatus }),
});