From 000fc556bd372a87d78075187e67d80f5e218852 Mon Sep 17 00:00:00 2001 From: Junseo <66871265+blan19@users.noreply.github.com> Date: Sat, 29 Apr 2023 14:48:39 +0900 Subject: [PATCH] =?UTF-8?q?feat(web):=20WebRTC=EC=9D=84=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=9C=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=B4=88=EA=B8=B0=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?(#165)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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: 0qowlsdnd0@naver.com <0qowlsdnd0@naver.com> Co-authored-by: doyupKim --- apps/web/next.config.js | 2 +- apps/web/package.json | 5 +- apps/web/public/css/live.module.css | 18 + .../images/live/icons/CameraCloseIcon.svg | 3 + .../public/images/live/icons/LockerIcon.svg | 3 + .../images/live/icons/SubscriberIcon.svg | 3 + .../public/images/live/icons/TagCloseBtn.svg | 3 + .../public/images/live/icons/VolumeIcon.svg | 3 + apps/web/src/components/live/ChatInfo.tsx | 265 +++++++++++++ apps/web/src/components/live/Publisher.tsx | 211 +++++++++++ apps/web/src/components/live/Subscriber.tsx | 165 +++++++++ apps/web/src/components/live/modal/index.tsx | 58 +++ apps/web/src/constants/form/live.ts | 6 + apps/web/src/http/core/index.ts | 6 +- apps/web/src/http/server/live/apis.ts | 78 ++++ apps/web/src/http/server/live/index.ts | 1 + apps/web/src/http/server/live/keys.ts | 5 + apps/web/src/http/server/live/mutaitons.ts | 16 + apps/web/src/http/server/live/queries.ts | 22 ++ apps/web/src/pages/live/Create.tsx | 348 ++++++++++++++++++ apps/web/src/pages/live/[Channel].tsx | 104 ++++++ apps/web/src/pages/live/index.tsx | 306 +++++++++++++++ apps/web/src/types/env.d.ts | 2 + apps/web/src/types/live.d.ts | 21 ++ package.json | 2 +- packages/ui/form/formAttachment.tsx | 6 + packages/ui/form/formLabel.tsx | 5 +- pnpm-lock.yaml | 66 ++++ turbo.json | 2 + 29 files changed, 1730 insertions(+), 5 deletions(-) create mode 100644 apps/web/public/css/live.module.css create mode 100644 apps/web/public/images/live/icons/CameraCloseIcon.svg create mode 100644 apps/web/public/images/live/icons/LockerIcon.svg create mode 100644 apps/web/public/images/live/icons/SubscriberIcon.svg create mode 100644 apps/web/public/images/live/icons/TagCloseBtn.svg create mode 100644 apps/web/public/images/live/icons/VolumeIcon.svg create mode 100644 apps/web/src/components/live/ChatInfo.tsx create mode 100644 apps/web/src/components/live/Publisher.tsx create mode 100644 apps/web/src/components/live/Subscriber.tsx create mode 100644 apps/web/src/components/live/modal/index.tsx create mode 100644 apps/web/src/constants/form/live.ts create mode 100644 apps/web/src/http/server/live/apis.ts create mode 100644 apps/web/src/http/server/live/index.ts create mode 100644 apps/web/src/http/server/live/keys.ts create mode 100644 apps/web/src/http/server/live/mutaitons.ts create mode 100644 apps/web/src/http/server/live/queries.ts create mode 100644 apps/web/src/pages/live/Create.tsx create mode 100644 apps/web/src/pages/live/[Channel].tsx create mode 100644 apps/web/src/pages/live/index.tsx create mode 100644 apps/web/src/types/live.d.ts 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 ( + + ); + })} +
+ +