Skip to content

Commit

Permalink
feat(web): WebRTC을 사용한 라이브 페이지 초기 구현 (#165)
Browse files Browse the repository at this point in the history
* chore(web): 레이아웃 컴포넌트 초기 진입시에만 렌더링 되도록 수정

* feat(web): Live 타입 모듈로 추가

* chore(web): 오픈비두 환경변수 추가

* refactor(web): 기존 컴포넌트 내부에 선언된 api들을 tanstack-query로 이관

* refactor(web): 기존 Live 사용하던 타입들 모듈로 재사용

* refactor(web): 기존 컨벤션에 맞춰 네임스페이스 방식으로 Import 변경

* design(ui): FormLabel padding 수정

* design(ui): FormAttachment 모바일 스타일 수정

* feat(web): Live Form State 추가

* refactor(web): Ui Form 패키지 사용

* chore(web): Live 종료시 invalidQuery 실행 삭제

* chore(web): Live 리스트 Api 스펙 변경 적용

* pull feature_live

* private room password input modal add, chat stomp get userNumbers

* none

* disconnect fix

* openvidu disconnection 구현

* pc version live complite

* fix(web): lint error

---------

Co-authored-by: [email protected] <[email protected]>
Co-authored-by: doyupKim <[email protected]>
  • Loading branch information
3 people authored Apr 29, 2023
1 parent 9a0340f commit 000fc55
Show file tree
Hide file tree
Showing 29 changed files with 1,730 additions and 5 deletions.
2 changes: 1 addition & 1 deletion apps/web/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const withPWA = require('next-pwa')({
/** @type {import('next').NextConfig} */

const nextConfig = {
reactStrictMode: true,
reactStrictMode: false,
swcMinify: true,
compiler: {
styledComponents: {
Expand Down
5 changes: 4 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@
"node-fetch-commonjs": "^3.2.4",
"prettier": "^2.8.4",
"typescript": "^4.9.5",
"webpack": "^5.75.0"
"webpack": "^5.75.0",
"openvidu-browser": "^2.26.0",
"axios": "^1.3.4",
"@stomp/stompjs": "^7.0.0"
}
}
18 changes: 18 additions & 0 deletions apps/web/public/css/live.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.chatTextarea:focus {
outline: none;
}

.chatTextarea::placeholder {
color: #8E8E95;
font-weight: 500;
font-family: 'Pretendard';
}

.chatTextarea {
resize: none;
border: none;
height: 100%;
font-weight: 500;
font-family: 'Pretendard';
width: 100%;
}
3 changes: 3 additions & 0 deletions apps/web/public/images/live/icons/CameraCloseIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions apps/web/public/images/live/icons/LockerIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions apps/web/public/images/live/icons/SubscriberIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions apps/web/public/images/live/icons/TagCloseBtn.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions apps/web/public/images/live/icons/VolumeIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
265 changes: 265 additions & 0 deletions apps/web/src/components/live/ChatInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
import * as React from 'react';
import liveCss from 'public/css/live.module.css';
import { Client, StompSubscription } from '@stomp/stompjs';
import { Button, Wrapper } from '@supercarmarket/ui';
import { getSession, useSession } from 'next-auth/react';
import { css } from 'styled-components';

interface Props {
data: Live.LiveRoomDto | null | undefined;
isBroad: boolean;
setLiveViewCount: (broad: number) => void;
}

interface messageType {
type: string;
sender: string;
channelId: string;
data: string;
}

function ChatInfo(props: Props) {
const [chats, setChats] = React.useState<messageType[]>([]);
const [stomp, setStomp] = React.useState<Client>();
const [subscribes, setSubscribes] = React.useState<StompSubscription>();
const [userName, setUserName] = React.useState('');

const textAreaRef = React.useRef<HTMLTextAreaElement>(null);
const chatWrapRef = React.useRef<HTMLDivElement>(null);

const joinChat = async () => {
const session = await getSession();

if (!session?.accessToken) throw 'require logged in';
setUserName(session.nickname);

const client = new Client({
brokerURL: `wss://back.doyup.shop/ws`,
connectHeaders: {
ACCESS_TOKEN: `${session.accessToken}`,
},
reconnectDelay: 5000,
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000,
});

setStomp(client);

client.onConnect = function (frame) {
const subscribe = client.subscribe(
`/sub/${props.data?.sessionId}`,
(frame) => {
const getMessage = JSON.parse(frame.body);
if (getMessage.participantNumber) {
props.setLiveViewCount(parseInt(getMessage.participantNumber));
}
if (getMessage.sender !== 'server') {
setChats((prevState: messageType[]) => {
return prevState.concat([getMessage]);
});
setTimeout(() => {
if (chatWrapRef.current) {
chatWrapRef.current.scrollTop =
chatWrapRef.current.scrollHeight;
}
}, 100);
}
}
);

setSubscribes(subscribe);

client.publish({
destination: `/pub/chat/${props.data?.sessionId}`,
body: `{
"type": "ENTER",
"sender": "${session.nickname}",
"channelId": "${props.data?.sessionId}",
"data": "'${session.nickname}' 님이 접속하셨습니다."
}`,
});
};

client.onStompError = function (frame) {
console.log('Broker reported error: ' + frame.headers['message']);
console.log('Additional details: ' + frame.body);
};

client.activate();
};

const sendChat = () => {
if (stomp && textAreaRef.current && textAreaRef.current.value.length > 0) {
stomp.publish({
destination: `/pub/chat/${props.data?.sessionId}`,
body: `{
"type": "TALK",
"sender": "${userName}",
"channelId": "${props.data?.sessionId}",
"data": "${
textAreaRef.current?.value.replaceAll(/(\n|\r\n)/g, '<br>') ?? ''
}"
}`,
headers: {
'content-type': 'application/json',
},
});
}
if (textAreaRef.current) textAreaRef.current.value = '';
};

const exitChat = () => {
if (stomp) {
stomp.publish({
destination: `/pub/chat/${props.data?.sessionId}`,
body: `{
"type": "EXIT",
"sender": "${userName}",
"channelId": "${props.data?.sessionId}",
"data": "'${userName}' 님이 종료하셨습니다."
}`,
});
}
setTimeout(() => {
if (chatWrapRef.current) {
chatWrapRef.current.scrollTop = chatWrapRef.current.scrollHeight;
}
stomp?.deactivate();
}, 100);
};

React.useEffect(() => {
joinChat();
}, []);

React.useEffect(() => {
if (stomp) {
textAreaRef.current?.addEventListener('keypress', (event) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
sendChat();
}
});
}
return () => {
exitChat();
};
}, [stomp]);

return (
<Wrapper.Item
css={css`
margin-left: 16px;
width: 304px;
`}
>
<div
style={{
height: '500px',
overflowY: 'auto',
}}
ref={chatWrapRef}
>
{chats.map((data, idx) => {
if (data.type === 'ENTER' || data.type === 'EXIT') {
return (
<InitUserChat chat={data.data} key={`InitUserChat_${idx}`} />
);
}
if (data.sender === userName) {
return <MyChat chat={data.data} key={`MyChat_${idx}`} />;
}
return (
<UserChat
nickname={data.sender}
chat={data.data}
key={`UserChat_${idx}`}
/>
);
})}
</div>
<Wrapper.Item
css={css`
{
padding: 12px 16px;
border: 1px solid #c3c3c7;
display: flex;
border-radius: 4px;
height: 97px;
align-items: flex-end;
}
`}
>
<textarea
placeholder="채팅을 남겨보세요"
className={liveCss.chatTextarea}
maxLength={100}
ref={textAreaRef}
/>
<Button
style={{
width: '72px',
height: '38px',
}}
onClick={sendChat}
>
등록
</Button>
</Wrapper.Item>
</Wrapper.Item>
);
}

const UserChat = ({ nickname, chat }: { nickname: string; chat: string }) => {
return (
<div
style={{
backgroundColor: '#F7F7F8',
borderRadius: '4px 16px 16px 16px',
padding: '12px 16px',
gap: '8px',
marginBottom: '10px',
width: 'fit-content',
}}
>
<span style={{ fontSize: '14px', lineHeight: '150%' }}>{nickname}</span>{' '}
<span dangerouslySetInnerHTML={{ __html: chat }} />
</div>
);
};

const MyChat = ({ chat }: { chat: string }) => {
return (
<div
style={{
backgroundColor: '#EBE6DE',
borderRadius: '16px 4px 16px 16px',
padding: '12px 16px',
gap: '8px',
marginBottom: '10px',
marginLeft: 'auto',
width: 'fit-content',
}}
>
<span dangerouslySetInnerHTML={{ __html: chat }} />
</div>
);
};

const InitUserChat = ({ chat }: { chat: string }) => {
return (
<div
style={{
borderRadius: '16px 4px 16px 16px',
padding: '12px 16px',
gap: '8px',
marginBottom: '10px',
fontSize: '14px',
}}
>
{chat}
</div>
);
};

export default ChatInfo;
Loading

0 comments on commit 000fc55

Please sign in to comment.