diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 74efd7aa..c2888624 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -9,7 +9,7 @@ const withPWA = require('next-pwa')({ /** @type {import('next').NextConfig} */ const nextConfig = { - reactStrictMode: true, + reactStrictMode: false, swcMinify: true, compiler: { styledComponents: { diff --git a/apps/web/package.json b/apps/web/package.json index 885ba061..5d4c051b 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -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" } } diff --git a/apps/web/public/css/live.module.css b/apps/web/public/css/live.module.css new file mode 100644 index 00000000..884a0cfc --- /dev/null +++ b/apps/web/public/css/live.module.css @@ -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%; +} \ No newline at end of file diff --git a/apps/web/public/images/live/icons/CameraCloseIcon.svg b/apps/web/public/images/live/icons/CameraCloseIcon.svg new file mode 100644 index 00000000..d9838d82 --- /dev/null +++ b/apps/web/public/images/live/icons/CameraCloseIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/public/images/live/icons/LockerIcon.svg b/apps/web/public/images/live/icons/LockerIcon.svg new file mode 100644 index 00000000..d43111b8 --- /dev/null +++ b/apps/web/public/images/live/icons/LockerIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/public/images/live/icons/SubscriberIcon.svg b/apps/web/public/images/live/icons/SubscriberIcon.svg new file mode 100644 index 00000000..137aa9c4 --- /dev/null +++ b/apps/web/public/images/live/icons/SubscriberIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/public/images/live/icons/TagCloseBtn.svg b/apps/web/public/images/live/icons/TagCloseBtn.svg new file mode 100644 index 00000000..4c695bc0 --- /dev/null +++ b/apps/web/public/images/live/icons/TagCloseBtn.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/public/images/live/icons/VolumeIcon.svg b/apps/web/public/images/live/icons/VolumeIcon.svg new file mode 100644 index 00000000..07495dff --- /dev/null +++ b/apps/web/public/images/live/icons/VolumeIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/components/live/ChatInfo.tsx b/apps/web/src/components/live/ChatInfo.tsx new file mode 100644 index 00000000..b0a4e1e5 --- /dev/null +++ b/apps/web/src/components/live/ChatInfo.tsx @@ -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([]); + const [stomp, setStomp] = React.useState(); + const [subscribes, setSubscribes] = React.useState(); + const [userName, setUserName] = React.useState(''); + + const textAreaRef = React.useRef(null); + const chatWrapRef = React.useRef(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, '
') ?? '' + }" + }`, + 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 ( + +
+ {chats.map((data, idx) => { + if (data.type === 'ENTER' || data.type === 'EXIT') { + return ( + + ); + } + if (data.sender === userName) { + return ; + } + return ( + + ); + })} +
+ +