├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ └── basic-issue-template.md ├── PULL_REQUEST_TEMPLATE.md ├── auto_assign.yml └── workflows │ └── release.yml ├── .gitignore ├── .prettierrc ├── README.md ├── client ├── craco.config.js ├── package.json ├── public │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── audios │ │ └── mia-ping.mp3 │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── images │ │ ├── ImgBox.png │ │ ├── LeftIcon.png │ │ ├── agree.png │ │ ├── beer-cheers1.gif │ │ ├── beer-cheers2.gif │ │ ├── bomb.png │ │ ├── check.png │ │ ├── deny.png │ │ ├── disagree.png │ │ ├── github-login.png │ │ ├── github-logo.png │ │ ├── humanIcon.svg │ │ ├── logo.png │ │ ├── mia-ping.gif │ │ ├── naver-login.png │ │ ├── naver-logo.png │ │ ├── profileBox.svg │ │ ├── question-sprite.png │ │ ├── requestFriend.png │ │ ├── soju-cap.png │ │ └── xbutton.png │ ├── index.html │ ├── mstile-150x150.png │ ├── robots.txt │ ├── safari-pinned-tab.svg │ └── site.webmanifest ├── src │ ├── App.tsx │ ├── GlobalStyle.js │ ├── api │ │ ├── friend.ts │ │ ├── index.ts │ │ ├── rank.ts │ │ └── user.ts │ ├── components │ │ ├── Header.style.js │ │ ├── Header.tsx │ │ ├── custom │ │ │ ├── Dropdown.style.js │ │ │ ├── Dropdown.tsx │ │ │ ├── ErrorToast.style.ts │ │ │ ├── ErrorToast.tsx │ │ │ ├── Loading.style.js │ │ │ ├── Loading.tsx │ │ │ ├── Modal.style.ts │ │ │ ├── Modal.tsx │ │ │ ├── ServerError.style.js │ │ │ └── ServerError.tsx │ │ ├── icons │ │ │ ├── CancelIcon.tsx │ │ │ ├── ChangeNicknameIcon.tsx │ │ │ ├── ChatIcon.tsx │ │ │ ├── CheersIcon.tsx │ │ │ ├── CloseUpIcon.tsx │ │ │ ├── CopyIcon.tsx │ │ │ ├── DeleteFriendIcon.tsx │ │ │ ├── DeleteIcon.tsx │ │ │ ├── DownIcon.tsx │ │ │ ├── ExitIcon.tsx │ │ │ ├── FriendHomeIcon.tsx │ │ │ ├── GameIcon.tsx │ │ │ ├── GameRuleIcon.tsx │ │ │ ├── GreenXButtonIcon.tsx │ │ │ ├── HistoryIcon.tsx │ │ │ ├── HomeIcon.tsx │ │ │ ├── HostIcon.tsx │ │ │ ├── HumanIcon.tsx │ │ │ ├── LeftIcon.tsx │ │ │ ├── LiarGameIcon.tsx │ │ │ ├── LogoutIcon.tsx │ │ │ ├── MicIcon.tsx │ │ │ ├── NicknameChangeIcon.tsx │ │ │ ├── PaperPlaneIcon.tsx │ │ │ ├── PeopleIcon.tsx │ │ │ ├── ProfileSquareIcon.tsx │ │ │ ├── RandomPickGameIcon.tsx │ │ │ ├── RejectIcon.tsx │ │ │ ├── SettingIcon.tsx │ │ │ ├── SmallAcceptIcon.tsx │ │ │ ├── SmallCancelIcon.tsx │ │ │ ├── SmallRejectIcon.tsx │ │ │ ├── SpeakerIcon.tsx │ │ │ ├── UpdownGameIcon.tsx │ │ │ ├── VideoIcon.tsx │ │ │ ├── VoterIcon.tsx │ │ │ ├── XIcon.tsx │ │ │ └── index.ts │ │ ├── room │ │ │ ├── ControlBar.style.js │ │ │ ├── ControlBar.tsx │ │ │ ├── RoomMenu.style.js │ │ │ ├── RoomMenu.tsx │ │ │ ├── RouteMenu.style.js │ │ │ ├── RouteMenu.tsx │ │ │ ├── animation-screen │ │ │ │ ├── QuestionMark.style.ts │ │ │ │ ├── QuestionMark.tsx │ │ │ │ ├── index.style.ts │ │ │ │ └── index.tsx │ │ │ ├── chat │ │ │ │ ├── ChatForm.style.js │ │ │ │ ├── ChatForm.tsx │ │ │ │ ├── ChatItem.style.ts │ │ │ │ ├── ChatItem.tsx │ │ │ │ ├── ChatMenuIcon.style.js │ │ │ │ ├── ChatMenuIcon.tsx │ │ │ │ ├── index.style.js │ │ │ │ └── index.tsx │ │ │ ├── games │ │ │ │ ├── GameBox.style.js │ │ │ │ ├── GameBox.tsx │ │ │ │ ├── GameMenu.style.js │ │ │ │ ├── GameMenu.tsx │ │ │ │ ├── LiarGame.style.ts │ │ │ │ ├── LiarGame.tsx │ │ │ │ ├── RandomPickGame.style.js │ │ │ │ ├── RandomPickGame.tsx │ │ │ │ ├── UpdownGame.style.js │ │ │ │ ├── UpdownGame.tsx │ │ │ │ ├── gameList.tsx │ │ │ │ └── index.tsx │ │ │ ├── host │ │ │ │ ├── Participant.style.js │ │ │ │ ├── Participant.tsx │ │ │ │ ├── ParticipantController.style.js │ │ │ │ ├── ParticipantController.tsx │ │ │ │ ├── RoomController.style.ts │ │ │ │ ├── RoomController.tsx │ │ │ │ ├── index.style.js │ │ │ │ └── index.tsx │ │ │ ├── index.style.js │ │ │ ├── index.tsx │ │ │ ├── monitor │ │ │ │ ├── MyVideo.tsx │ │ │ │ ├── OtherVideo.tsx │ │ │ │ ├── Video.style.ts │ │ │ │ ├── index.style.ts │ │ │ │ └── index.tsx │ │ │ └── scaffold │ │ │ │ ├── TimerBomb.style.js │ │ │ │ ├── TimerBomb.tsx │ │ │ │ ├── VotePresser.style.js │ │ │ │ ├── VotePresser.tsx │ │ │ │ ├── Voters.style.js │ │ │ │ ├── Voters.tsx │ │ │ │ ├── index.style.ts │ │ │ │ └── index.tsx │ │ ├── setting │ │ │ ├── DeviceSelections.tsx │ │ │ ├── DeviceToggles.tsx │ │ │ ├── SettingDropdown.style.js │ │ │ ├── SettingDropdown.tsx │ │ │ ├── SettingToggle.style.js │ │ │ ├── SettingToggle.tsx │ │ │ ├── index.style.js │ │ │ └── index.tsx │ │ ├── user-information │ │ │ ├── Information.style.ts │ │ │ ├── UserHeader.style.ts │ │ │ ├── UserHeader.tsx │ │ │ ├── friend-list │ │ │ │ ├── FriendItem.style.js │ │ │ │ ├── FriendItem.tsx │ │ │ │ ├── index.style.js │ │ │ │ └── index.tsx │ │ │ ├── index.tsx │ │ │ ├── information │ │ │ │ ├── UserData.style.js │ │ │ │ ├── UserData.tsx │ │ │ │ ├── UserProfile.style.js │ │ │ │ ├── UserProfile.tsx │ │ │ │ ├── index.style.ts │ │ │ │ └── index.tsx │ │ │ ├── modals │ │ │ │ ├── FriendDeleteModal.style.js │ │ │ │ ├── FriendDeleteModal.tsx │ │ │ │ ├── FriendInfoModal.style.js │ │ │ │ ├── FriendInfoModal.tsx │ │ │ │ ├── FriendRequestModal.style.js │ │ │ │ ├── FriendRequestModal.tsx │ │ │ │ ├── NickChangeModal.style.js │ │ │ │ ├── NickChangeModal.tsx │ │ │ │ ├── NickLogModal.style.js │ │ │ │ ├── NickLogModal.tsx │ │ │ │ └── index.style.js │ │ │ └── ranks │ │ │ │ ├── RankingBox.style.js │ │ │ │ ├── RankingBox.tsx │ │ │ │ ├── index.style.js │ │ │ │ └── index.tsx │ │ └── user │ │ │ ├── Users.style.js │ │ │ └── Users.tsx │ ├── constant │ │ ├── envs.js │ │ └── style.js │ ├── hooks │ │ ├── redux.ts │ │ ├── socket │ │ │ ├── useAnimationSocket.ts │ │ │ ├── useChatSocket.ts │ │ │ ├── useControlSocket.ts │ │ │ ├── useEnterSocket.ts │ │ │ ├── useFriendSocket.ts │ │ │ ├── useGameSocket.ts │ │ │ ├── useMarkSocket.ts │ │ │ ├── useSignalSocket.ts │ │ │ ├── useStreamSocket.ts │ │ │ ├── useTicketSocket.ts │ │ │ └── useVoteSocket.ts │ │ ├── useToggleSpeaker.ts │ │ ├── useUpdateSpeaker.ts │ │ └── useUpdateStream.ts │ ├── index.tsx │ ├── pages │ │ ├── CreateRoom.tsx │ │ ├── JoinRoom.tsx │ │ ├── Lobby.style.js │ │ ├── Lobby.tsx │ │ ├── Login.style.js │ │ ├── Login.tsx │ │ ├── Splash.tsx │ │ ├── UserPage.style.js │ │ └── UserPage.tsx │ ├── react-app-env.d.ts │ ├── sagas │ │ ├── device.ts │ │ ├── friend.ts │ │ ├── index.ts │ │ └── user.ts │ ├── socket │ │ ├── animation.ts │ │ ├── chat.ts │ │ ├── control.ts │ │ ├── create.ts │ │ ├── enter.ts │ │ ├── friend.ts │ │ ├── game.ts │ │ ├── mark.ts │ │ ├── signal.ts │ │ ├── socket.ts │ │ ├── stream.ts │ │ ├── ticket.ts │ │ └── vote.ts │ ├── store │ │ ├── device.ts │ │ ├── friend.ts │ │ ├── index.ts │ │ ├── notice.ts │ │ ├── room.ts │ │ ├── store.ts │ │ └── user.ts │ ├── ts-types │ │ ├── components │ │ │ ├── custom.ts │ │ │ ├── icons.ts │ │ │ ├── room.ts │ │ │ ├── setting.ts │ │ │ ├── user-information.ts │ │ │ └── user.ts │ │ ├── store.ts │ │ └── utils.ts │ └── utils │ │ ├── customRTC.ts │ │ ├── date.ts │ │ ├── regExpr.ts │ │ └── request.ts ├── tsconfig.alias.json └── tsconfig.json ├── domain ├── constant │ ├── addition.ts │ ├── gameName.ts │ └── socketEvent.ts ├── package.json └── tsconfig.json └── server ├── package.json ├── src ├── api │ ├── index.ts │ └── v1 │ │ ├── auth.ts │ │ ├── friend.ts │ │ ├── index.ts │ │ ├── rank.ts │ │ ├── test.ts │ │ └── user.ts ├── app.ts ├── constant.ts ├── controller │ ├── friend.ts │ ├── passport │ │ ├── github.ts │ │ └── naver.ts │ ├── rank.ts │ ├── socket │ │ ├── animation.ts │ │ ├── chat.ts │ │ ├── control.ts │ │ ├── create.ts │ │ ├── enter.ts │ │ ├── friend.ts │ │ ├── game.ts │ │ ├── mark.ts │ │ ├── signal.ts │ │ ├── stream.ts │ │ ├── ticket.ts │ │ └── vote.ts │ └── user.ts ├── loader │ ├── basic.ts │ ├── index.ts │ ├── mongo.ts │ ├── passport.ts │ └── socket.ts ├── models │ ├── NicknameLog.ts │ ├── User.ts │ └── index.ts ├── service │ ├── friend.ts │ ├── rank.ts │ └── user.ts ├── types.ts └── utils │ ├── cron.ts │ ├── error.ts │ ├── pipe.ts │ ├── time.ts │ ├── transaction.ts │ └── userCount.ts ├── tsconfig-paths-bootstrap.js └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": ["plugin:react/recommended", "airbnb", "prettier"], 7 | "globals": { 8 | "Atomics": "readonly", 9 | "SharedArrayBuffer": "readonly" 10 | }, 11 | "parserOptions": { 12 | "ecmaFeatures": { 13 | "tsx": true 14 | }, 15 | "ecmaVersion": 12, 16 | "sourceType": "module" 17 | }, 18 | "plugins": ["react", "prettier"], 19 | "rules": { 20 | "camelcase": "off", 21 | "prettier/prettier": [ 22 | "error", 23 | { 24 | "endOfLine": "auto" 25 | } 26 | ], 27 | "react/jsx-filename-extension": [1, { "extensions": [".ts", ".tsx"] }], 28 | "react/state-in-constructor": "off", 29 | "import/no-extraneous-dependencies": [ 30 | "error", 31 | { 32 | "devDependencies": true, 33 | "optionalDependencies": false, 34 | "peerDependencies": false 35 | } 36 | ] 37 | }, 38 | "settings": { 39 | "import/resolver": { 40 | "alias": { 41 | "map": [], 42 | "extensions": [".ts", ".tsx"] 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/basic-issue-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: basic issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Description 11 | 기능에 대한 설명 12 | 13 | ### To do 14 | - [ ] 내용1 15 | - [ ] 내용2 16 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### WorkList 2 | 3 | - [x] work 4 | 5 | ### Issue 6 | 7 | - text 8 | 9 | ### Description 10 | 11 | text 12 | -------------------------------------------------------------------------------- /.github/auto_assign.yml: -------------------------------------------------------------------------------- 1 | # Set to true to add reviewers to pull requests 2 | addReviewers: true 3 | 4 | # Set to true to add assignees to pull requests 5 | addAssignees: true 6 | 7 | # A list of reviewers to be added to pull requests (GitHub user name) 8 | reviewers: 9 | - yeon52 10 | - alittlekitten 11 | - jyo-jyo 12 | - pyo-sh 13 | 14 | # A list of reviewers to be added to pull requests (GitHub user name) 15 | assignees: 16 | - yeon52 17 | - alittlekitten 18 | - jyo-jyo 19 | - pyo-sh 20 | 21 | 22 | # A list of keywords to be skipped the process that add reviewers if pull requests include it 23 | skipKeywords: 24 | - wip 25 | 26 | # A number of reviewers added to the pull request 27 | # Set 0 to add all the reviewers (default: 0) 28 | numberOfReviewers: 4 -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: autoDeploy 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: executing remote ssh commands using password 15 | uses: appleboy/ssh-action@master 16 | with: 17 | host: ${{ secrets.SSH_HOST }} 18 | username: ${{ secrets.SSH_USERNAME }} 19 | password: ${{ secrets.SSH_PWD }} 20 | port: ${{ secrets.SSH_PORT }} 21 | script: | 22 | cd ${{ secrets.SSH_REPOSITORY }} 23 | git fetch --all 24 | git reset --hard origin/main 25 | cd ./domain 26 | npm run build 27 | cd ../client 28 | npm install 29 | rm -rf build 30 | npm run deploy 31 | cd ../server 32 | npm install 33 | pm2 kill 34 | rm -rf build 35 | npm run deploy 36 | 37 | - name: send result to slack 38 | uses: 8398a7/action-slack@v3 39 | with: 40 | status: ${{job.status}} 41 | fields: repo,message,commit,author,action,took 42 | author_name: testRepo 43 | 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # node_modules 2 | /client/node_modules 3 | /server/node_modules 4 | 5 | # environments 6 | /client/.env 7 | /server/.env 8 | 9 | # package-lock.json 10 | /client/package-lock.json 11 | /server/package-lock.json 12 | /domain/package-lock.json 13 | 14 | # building files 15 | /client/build/ 16 | /server/build/ 17 | 18 | # public Images 19 | /server/uploads/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "crlf", 3 | "jsxBracketSameLine": false, 4 | "bracketSpacing": true, 5 | "printWidth": 100, 6 | "tabWidth": 2, 7 | "singleQuote": true, 8 | "jsxSingleQuote": false, 9 | "semi": true, 10 | "trailingComma": "all", 11 | "useTabs": false 12 | } 13 | -------------------------------------------------------------------------------- /client/craco.config.js: -------------------------------------------------------------------------------- 1 | const CracoAlias = require('craco-alias'); 2 | 3 | module.exports = { 4 | plugins: [ 5 | { 6 | plugin: CracoAlias, 7 | options: { 8 | source: 'tsconfig', 9 | baseUrl: './src', 10 | tsConfigPath: './tsconfig.alias.json', 11 | }, 12 | }, 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /client/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web12-sooltreaming/e9184fdd32b1fad5fdd582c3d8d05cb43ae9bd97/client/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /client/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web12-sooltreaming/e9184fdd32b1fad5fdd582c3d8d05cb43ae9bd97/client/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /client/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web12-sooltreaming/e9184fdd32b1fad5fdd582c3d8d05cb43ae9bd97/client/public/apple-touch-icon.png -------------------------------------------------------------------------------- /client/public/audios/mia-ping.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web12-sooltreaming/e9184fdd32b1fad5fdd582c3d8d05cb43ae9bd97/client/public/audios/mia-ping.mp3 -------------------------------------------------------------------------------- /client/public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #00aba9 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /client/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web12-sooltreaming/e9184fdd32b1fad5fdd582c3d8d05cb43ae9bd97/client/public/favicon-16x16.png -------------------------------------------------------------------------------- /client/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web12-sooltreaming/e9184fdd32b1fad5fdd582c3d8d05cb43ae9bd97/client/public/favicon-32x32.png -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web12-sooltreaming/e9184fdd32b1fad5fdd582c3d8d05cb43ae9bd97/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/images/ImgBox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web12-sooltreaming/e9184fdd32b1fad5fdd582c3d8d05cb43ae9bd97/client/public/images/ImgBox.png -------------------------------------------------------------------------------- /client/public/images/LeftIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web12-sooltreaming/e9184fdd32b1fad5fdd582c3d8d05cb43ae9bd97/client/public/images/LeftIcon.png -------------------------------------------------------------------------------- /client/public/images/agree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web12-sooltreaming/e9184fdd32b1fad5fdd582c3d8d05cb43ae9bd97/client/public/images/agree.png -------------------------------------------------------------------------------- /client/public/images/beer-cheers1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web12-sooltreaming/e9184fdd32b1fad5fdd582c3d8d05cb43ae9bd97/client/public/images/beer-cheers1.gif -------------------------------------------------------------------------------- /client/public/images/beer-cheers2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web12-sooltreaming/e9184fdd32b1fad5fdd582c3d8d05cb43ae9bd97/client/public/images/beer-cheers2.gif -------------------------------------------------------------------------------- /client/public/images/bomb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web12-sooltreaming/e9184fdd32b1fad5fdd582c3d8d05cb43ae9bd97/client/public/images/bomb.png -------------------------------------------------------------------------------- /client/public/images/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web12-sooltreaming/e9184fdd32b1fad5fdd582c3d8d05cb43ae9bd97/client/public/images/check.png -------------------------------------------------------------------------------- /client/public/images/deny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web12-sooltreaming/e9184fdd32b1fad5fdd582c3d8d05cb43ae9bd97/client/public/images/deny.png -------------------------------------------------------------------------------- /client/public/images/disagree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web12-sooltreaming/e9184fdd32b1fad5fdd582c3d8d05cb43ae9bd97/client/public/images/disagree.png -------------------------------------------------------------------------------- /client/public/images/github-login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web12-sooltreaming/e9184fdd32b1fad5fdd582c3d8d05cb43ae9bd97/client/public/images/github-login.png -------------------------------------------------------------------------------- /client/public/images/github-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web12-sooltreaming/e9184fdd32b1fad5fdd582c3d8d05cb43ae9bd97/client/public/images/github-logo.png -------------------------------------------------------------------------------- /client/public/images/humanIcon.svg: -------------------------------------------------------------------------------- 1 | 9 | 14 | 18 | 23 | -------------------------------------------------------------------------------- /client/public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web12-sooltreaming/e9184fdd32b1fad5fdd582c3d8d05cb43ae9bd97/client/public/images/logo.png -------------------------------------------------------------------------------- /client/public/images/mia-ping.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web12-sooltreaming/e9184fdd32b1fad5fdd582c3d8d05cb43ae9bd97/client/public/images/mia-ping.gif -------------------------------------------------------------------------------- /client/public/images/naver-login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web12-sooltreaming/e9184fdd32b1fad5fdd582c3d8d05cb43ae9bd97/client/public/images/naver-login.png -------------------------------------------------------------------------------- /client/public/images/naver-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web12-sooltreaming/e9184fdd32b1fad5fdd582c3d8d05cb43ae9bd97/client/public/images/naver-logo.png -------------------------------------------------------------------------------- /client/public/images/profileBox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /client/public/images/question-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web12-sooltreaming/e9184fdd32b1fad5fdd582c3d8d05cb43ae9bd97/client/public/images/question-sprite.png -------------------------------------------------------------------------------- /client/public/images/requestFriend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web12-sooltreaming/e9184fdd32b1fad5fdd582c3d8d05cb43ae9bd97/client/public/images/requestFriend.png -------------------------------------------------------------------------------- /client/public/images/soju-cap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web12-sooltreaming/e9184fdd32b1fad5fdd582c3d8d05cb43ae9bd97/client/public/images/soju-cap.png -------------------------------------------------------------------------------- /client/public/images/xbutton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web12-sooltreaming/e9184fdd32b1fad5fdd582c3d8d05cb43ae9bd97/client/public/images/xbutton.png -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Sooltreaming 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /client/public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web12-sooltreaming/e9184fdd32b1fad5fdd582c3d8d05cb43ae9bd97/client/public/mstile-150x150.png -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / -------------------------------------------------------------------------------- /client/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Sooltreaming", 3 | "short_name": "STRM", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { lazy, Suspense } from 'react'; 2 | import GlobalStyle from '@src/GlobalStyle'; 3 | import ErrorToast from '@components/custom/ErrorToast'; 4 | import { BrowserRouter, Route, Switch, Redirect } from 'react-router-dom'; 5 | import Loading from '@components/custom/Loading'; 6 | const Lobby = lazy(() => import('@pages/Lobby')); 7 | const JoinRoom = lazy(() => import('@pages/JoinRoom')); 8 | const Login = lazy(() => import('@pages/Login')); 9 | const AuthRoute = lazy(() => import('@pages/Splash')); 10 | const CreateRoom = lazy(() => import('@pages/CreateRoom')); 11 | const UserPage = lazy(() => import('@pages/UserPage')); 12 | 13 | const App: React.FC = (): React.ReactElement => { 14 | return ( 15 | <> 16 | 17 | 18 | }> 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | } /> 27 | 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | }; 35 | 36 | export default App; 37 | -------------------------------------------------------------------------------- /client/src/GlobalStyle.js: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | import { normalize } from 'styled-normalize'; 3 | 4 | const GlobalStyle = createGlobalStyle` 5 | ${normalize} 6 | // Noto Sans Korean (한국어만) 7 | @font-face { 8 | src: 'Noto Sans KR'; 9 | font-family: 'Noto Sans KR'; 10 | unicode-range: U+AC00-U+D7A3; 11 | } 12 | body { 13 | width: 100vw; 14 | height: 100vh; 15 | padding: 0; 16 | margin: 0; 17 | font-family: 'Merriweather', 'Noto Sans KR', sans-serif; 18 | } 19 | #root { 20 | width: 100%; 21 | height: 100%; 22 | overflow: hidden; 23 | } 24 | * { 25 | box-sizing: border-box; 26 | } 27 | `; 28 | 29 | export default GlobalStyle; 30 | -------------------------------------------------------------------------------- /client/src/api/friend.ts: -------------------------------------------------------------------------------- 1 | import request from '@utils/request'; 2 | 3 | export const postFriend = async (targetId: string) => { 4 | const result = await request.post({ url: '/friend', body: { targetId } }); 5 | return result; 6 | }; 7 | 8 | export const getSendFriend = async () => { 9 | const result = await request.get({ url: '/friend/send' }); 10 | return result; 11 | }; 12 | 13 | export const getReceiveFriend = async () => { 14 | const result = await request.get({ url: '/friend/receive' }); 15 | return result; 16 | }; 17 | 18 | export const getFriend = async () => { 19 | const result = await request.get({ url: '/friend' }); 20 | return result; 21 | }; 22 | 23 | export const patchSendFriend = async (targetId: string) => { 24 | const result = await request.patch({ url: '/friend/send', body: { targetId } }); 25 | return result; 26 | }; 27 | 28 | export const patchReceiveFriend = async (targetId: string) => { 29 | const result = await request.patch({ url: '/friend/receive', body: { targetId } }); 30 | return result; 31 | }; 32 | 33 | export const patchUnfriend = async (targetId: string) => { 34 | const result = await request.patch({ url: '/friend/remove', body: { targetId } }); 35 | return result; 36 | }; 37 | 38 | export const patchFriend = async (targetId: string) => { 39 | const result = await request.patch({ url: '/friend/accept', body: { targetId } }); 40 | return result; 41 | }; 42 | -------------------------------------------------------------------------------- /client/src/api/index.ts: -------------------------------------------------------------------------------- 1 | import { getUserInformation, postUserImage, patchUserNickname, patchTotalSeconds } from '@api/user'; 2 | import { 3 | postFriend, 4 | getSendFriend, 5 | getReceiveFriend, 6 | getFriend, 7 | patchSendFriend, 8 | patchReceiveFriend, 9 | patchUnfriend, 10 | patchFriend, 11 | } from '@api/friend'; 12 | import { getRank } from '@api/rank'; 13 | import { setNoticeMessage } from '@store/notice'; 14 | import { store } from '@store/store'; 15 | import { resetUser } from '@store/user'; 16 | 17 | export const API = { 18 | TYPE: { 19 | PATCH_TOTAL_SECONDS: patchTotalSeconds, 20 | PATCH_USER_NICKNAME: patchUserNickname, 21 | POST_USER_IMAGE: postUserImage, 22 | GET_USER_INFORMATION: getUserInformation, 23 | 24 | POST_FRIEND: postFriend, 25 | GET_SEND_FRIEND: getSendFriend, 26 | GET_RECEIVE_FRIEND: getReceiveFriend, 27 | GET_FRIEND: getFriend, 28 | PATCH_SEND_FRIEND: patchSendFriend, 29 | PATCH_RECEIVE_FRIEND: patchReceiveFriend, 30 | PATCH_UNFRIEND: patchUnfriend, 31 | PATCH_FRIEND: patchFriend, 32 | 33 | GET_RANK: getRank, 34 | }, 35 | 36 | call: async function (api, data = {}) { 37 | try { 38 | const { json, status } = await api(data); 39 | if (status < 400) return json; 40 | if (status === 401) { 41 | store.dispatch(resetUser({})); 42 | } 43 | throw new Error(json.error); 44 | } catch (error: any) { 45 | store.dispatch(setNoticeMessage({ errorMessage: error.message })); 46 | } 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /client/src/api/rank.ts: -------------------------------------------------------------------------------- 1 | import request from '@utils/request'; 2 | 3 | export const getRank = async (type) => { 4 | const result = await request.get({ url: `/rank/${type}` }); 5 | return result; 6 | }; 7 | -------------------------------------------------------------------------------- /client/src/api/user.ts: -------------------------------------------------------------------------------- 1 | import request from '@utils/request'; 2 | 3 | export const loginWithSession = async () => { 4 | const result = await request.get({ url: '/auth/login' }); 5 | const { status, json } = result; 6 | if (status === 202) { 7 | const { _id: id, imgUrl, nickname } = json; 8 | return { id, imgUrl, nickname }; 9 | } else throw new Error(status.toString()); 10 | }; 11 | 12 | export const logoutAPI = async (callback) => { 13 | await request.get({ 14 | url: '/auth/logout', 15 | options: { redirect: 'manual' as RequestRedirect }, 16 | }); 17 | callback(); 18 | }; 19 | 20 | export const getUserInformation = async (id) => { 21 | const result = await request.get({ url: '/user', query: { id } }); 22 | return result; 23 | }; 24 | 25 | export const patchUserNickname = async (newNickname) => { 26 | const result = await request.patch({ 27 | url: '/user/nickname', 28 | body: { nickname: newNickname }, 29 | }); 30 | return result; 31 | }; 32 | 33 | export const postUserImage = async (newFile) => { 34 | const result = await request.post({ 35 | url: '/user/image', 36 | headerOptions: {}, 37 | body: newFile, 38 | }); 39 | return result; 40 | }; 41 | 42 | export const patchTotalSeconds = async (exitTime) => { 43 | await request.patch({ 44 | url: '/user/exit', 45 | body: { exitTime }, 46 | }); 47 | return; 48 | }; 49 | -------------------------------------------------------------------------------- /client/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { LineContainer, LogoLink, RightBox, UserLink, LogoutContainer } from './Header.style.js'; 3 | import { useSelector, useDispatch } from 'react-redux'; 4 | import { RootState } from '@src/store'; 5 | import { HumanIcon, LogoutIcon } from '@components/icons'; 6 | import { useHistory } from 'react-router-dom'; 7 | import { logoutAPI } from '@api/user'; 8 | import { resetUser } from '@store/user'; 9 | 10 | const Header: React.FC = (): React.ReactElement => { 11 | const history = useHistory(); 12 | const dispatch = useDispatch(); 13 | const { id, nickname, imgUrl } = useSelector((state: RootState) => state.user); 14 | 15 | const goToMyPage = () => { 16 | history.push(`/myPage/${id}`); 17 | }; 18 | 19 | const logout = async (e) => { 20 | e.stopPropagation(); 21 | logoutAPI(() => { 22 | dispatch(resetUser({})); 23 | history.push('/login'); 24 | }); 25 | }; 26 | 27 | return ( 28 | 29 | 30 | 로고 31 | Sooltreaming 32 | 33 | 34 | 35 |
36 | {!imgUrl ? : 프로필사진} 37 |
38 | {nickname || 'judangs'} 39 |
40 | 41 | 42 | 43 |
44 |
45 | ); 46 | }; 47 | 48 | export default Header; 49 | -------------------------------------------------------------------------------- /client/src/components/custom/Dropdown.style.js: -------------------------------------------------------------------------------- 1 | import styled, { keyframes } from 'styled-components'; 2 | import { COLOR, Z_INDEX, BOX_SHADOW } from '@constant/style'; 3 | 4 | export const Container = styled.div` 5 | max-width: 340px; 6 | width: 100%; 7 | height: 100%; 8 | 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | `; 14 | 15 | export const ItemListBox = styled.div` 16 | width: 100%; 17 | position: relative; 18 | top: -10px; 19 | z-index: ${Z_INDEX.modal}; 20 | `; 21 | 22 | export const soft = keyframes` 23 | from { opacity: 0; } 24 | to { opacity: 1; } 25 | `; 26 | 27 | export const ItemList = styled.ul` 28 | width: 80%; 29 | ${BOX_SHADOW} 30 | 31 | position: absolute; 32 | /* right: 0; */ 33 | list-style: none; 34 | padding-left: 0px; 35 | overflow: hidden; 36 | 37 | border-radius: 10px; 38 | 39 | -webkit-animation: ${soft} 0.2s linear; 40 | animation: ${soft} 0.2s linear; 41 | background: ${COLOR.background}; 42 | 43 | & > li { 44 | border-bottom: 1px solid ${COLOR.line}; 45 | &:last-child { 46 | border-bottom: none; 47 | } 48 | } 49 | `; 50 | 51 | export const ToggleButton = styled.button` 52 | width: 100%; 53 | height: 100%; 54 | padding: 0; 55 | margin: 0; 56 | border: none; 57 | background: transparent; 58 | outline: none; 59 | 60 | svg { 61 | flex: 0 0 auto; 62 | margin-left: 10px; 63 | transition: transform 0.3s; 64 | transform: rotate(-180deg); 65 | } 66 | &:focus { 67 | svg { 68 | transform: rotate(0deg); 69 | } 70 | } 71 | `; 72 | -------------------------------------------------------------------------------- /client/src/components/custom/Dropdown.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from 'react'; 2 | import { 3 | Container, 4 | ItemList, 5 | ItemListBox, 6 | ToggleButton, 7 | } from '@components/custom/Dropdown.style.js'; 8 | import type { DropdownPropType } from '@ts-types/components/custom'; 9 | 10 | const Dropdown: React.FC = ({ 11 | renderButton, 12 | renderItem, 13 | itemList, 14 | }): React.ReactElement => { 15 | const [isActive, setActive] = useState(false); 16 | const isMouseOn = useRef(false); 17 | 18 | const toggleDropdown = () => { 19 | setActive((prev) => !prev); 20 | }; 21 | const closeDropdown = () => { 22 | if (isMouseOn.current) return; 23 | setActive(false); 24 | }; 25 | 26 | return ( 27 | (isMouseOn.current = true)} 29 | onMouseLeave={() => (isMouseOn.current = false)} 30 | > 31 | 32 | {renderButton()} 33 | 34 | {isActive && ( 35 | 36 | 37 | {itemList.map((item) => renderItem({ closeDropdown: () => setActive(false), item }))} 38 | 39 | 40 | )} 41 | 42 | ); 43 | }; 44 | 45 | export default Dropdown; 46 | -------------------------------------------------------------------------------- /client/src/components/custom/ErrorToast.style.ts: -------------------------------------------------------------------------------- 1 | import styled, { keyframes } from 'styled-components'; 2 | import { COLOR, Z_INDEX, BOX_SHADOW } from '@constant/style'; 3 | import { TOAST_TIME } from 'sooltreaming-domain/constant/addition'; 4 | 5 | const fadeOut = keyframes` 6 | 0% { 7 | opacity: 0; 8 | margin-left: -25%; 9 | } 10 | 87%{ 11 | opacity: 1; 12 | margin-left: -50%; 13 | } 14 | 95%{ 15 | opacity: 1; 16 | margin-left: -50%; 17 | } 18 | 100% { 19 | opacity: 0; 20 | margin-left: -75%; 21 | } 22 | `; 23 | 24 | export const ErrorToastBox = styled.div` 25 | ${BOX_SHADOW} 26 | position: fixed; 27 | left: 100%; 28 | bottom: 100px; 29 | border: 2px solid ${COLOR.error}; 30 | padding: 15px 25px; 31 | 32 | transform: translateX(-50%); 33 | 34 | background-color: ${COLOR.white}; 35 | 36 | z-index: ${Z_INDEX.toast}; 37 | 38 | font-size: 20px; 39 | font-weight: bold; 40 | 41 | animation-duration: ${TOAST_TIME}ms; 42 | animation-timing-function: ease-out; 43 | animation-name: ${fadeOut}; 44 | animation-fill-mode: forwards; 45 | 46 | color: ${COLOR.error}; 47 | `; 48 | -------------------------------------------------------------------------------- /client/src/components/custom/ErrorToast.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { RootState } from '@src/store'; 4 | import { resetNoticeMessage } from '@store/notice'; 5 | import { ErrorToastBox } from '@components/custom/ErrorToast.style'; 6 | import { TOAST_TIME } from 'sooltreaming-domain/constant/addition'; 7 | 8 | const ErrorToast: React.FC = (): React.ReactElement => { 9 | const dispatch = useDispatch(); 10 | const errorMessage = useSelector((state: RootState) => state.notice.errorMessage); 11 | const [displayMessage, setDisplayMessage] = useState(''); 12 | const [previousAct, setPreviousAct] = useState | null>(null); 13 | 14 | const activeMessage = useCallback(() => { 15 | setPreviousAct(null); 16 | setDisplayMessage(''); 17 | }, []); 18 | 19 | useEffect(() => { 20 | if (!errorMessage) return; 21 | if (previousAct) clearTimeout(previousAct); 22 | const closeMethod = setTimeout(activeMessage, TOAST_TIME); 23 | setPreviousAct(closeMethod); 24 | setDisplayMessage(errorMessage); 25 | dispatch(resetNoticeMessage({})); 26 | }, [errorMessage]); 27 | 28 | if (!displayMessage) return <>; 29 | return ( 30 | 31 | {displayMessage} 32 | 33 | ); 34 | }; 35 | 36 | export default ErrorToast; 37 | -------------------------------------------------------------------------------- /client/src/components/custom/Loading.style.js: -------------------------------------------------------------------------------- 1 | import styled, { keyframes } from 'styled-components'; 2 | import { COLOR } from '@constant/style'; 3 | 4 | export const Container = styled.div` 5 | width: 100%; 6 | height: 100%; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | background-color: ${COLOR.background}; 11 | `; 12 | 13 | const spin = keyframes` 14 | 0% { transform: rotate(0deg); } 15 | 100% { transform: rotate(360deg); } 16 | `; 17 | const loadingSize = 300; 18 | 19 | export const Spinner = styled.div` 20 | z-index: 1; 21 | width: ${loadingSize}px; 22 | height: ${loadingSize}px; 23 | border: 30px solid ${COLOR.line}; 24 | border-radius: 50%; 25 | border-top: 30px solid ${COLOR.titleActive}; 26 | -webkit-animation: ${spin} 1s linear infinite; 27 | animation: ${spin} 1s linear infinite; 28 | 29 | display: flex; 30 | justify-content: center; 31 | align-items: center; 32 | 33 | & > img { 34 | width: 200px; 35 | height: 200px; 36 | } 37 | `; 38 | -------------------------------------------------------------------------------- /client/src/components/custom/Loading.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Spinner } from './Loading.style'; 2 | 3 | const Loading = (): React.ReactElement => { 4 | return ( 5 | 6 | 7 | logo 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default Loading; 14 | -------------------------------------------------------------------------------- /client/src/components/custom/Modal.style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR, Z_INDEX } from '@constant/style'; 3 | import type { ModalPosType } from '@ts-types/components/custom'; 4 | 5 | const getPositionCSS = (pos: ModalPosType): string => { 6 | return Object.entries(pos) 7 | .map(([key, value]) => 8 | ['top', 'left', 'right', 'bottom'].includes(key) ? `${key}: ${value};` : '', 9 | ) 10 | .join(''); 11 | }; 12 | 13 | export const RelativeBox = styled.div<{ pos: ModalPosType }>` 14 | position: relative; 15 | ${(props) => getPositionCSS(props.pos)} 16 | `; 17 | 18 | export const AbsoluteBox = styled.div<{ renderCenter: boolean; pos: ModalPosType }>` 19 | position: absolute; 20 | z-index: ${Z_INDEX.modal}; 21 | ${(props) => getPositionCSS(props.pos)} 22 | padding: 10px; 23 | background-color: ${COLOR.background}; 24 | border-radius: 10px; 25 | box-shadow: 0px 0px 4px rgba(204, 204, 204, 0.5), 0px 2px 4px rgba(0, 0, 0, 0.25); 26 | ${(props) => (props.renderCenter ? 'transform:translate(-50%, -50%);' : '')} 27 | `; 28 | -------------------------------------------------------------------------------- /client/src/components/custom/Modal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { RelativeBox, AbsoluteBox } from '@components/custom/Modal.style'; 3 | import type { ModalPropType } from '@ts-types/components/custom'; 4 | 5 | const Modal: React.FunctionComponent = ({ 6 | children, 7 | isOpen, 8 | renderCenter = false, 9 | isRelative = true, 10 | relativePos = {}, 11 | absolutePos = {}, 12 | }): React.ReactElement => { 13 | if (!isOpen) return <>; 14 | if (!isRelative) 15 | return ( 16 | 17 | {children} 18 | 19 | ); 20 | return ( 21 | 22 | 23 | {children} 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default Modal; 30 | -------------------------------------------------------------------------------- /client/src/components/custom/ServerError.style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR } from '@constant/style'; 3 | 4 | export const Container = styled.div` 5 | width: 100%; 6 | height: 100%; 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: center; 10 | align-items: center; 11 | background-color: ${COLOR.background}; 12 | `; 13 | 14 | export const StatusText = styled.div` 15 | margin-bottom: 40px; 16 | font-weight: bold; 17 | font-size: 60px; 18 | color: ${COLOR.error}; 19 | user-select: none; 20 | 21 | & > span { 22 | color: ${COLOR.titleActive}; 23 | } 24 | `; 25 | 26 | export const GuideText = styled.div` 27 | font-weight: bold; 28 | font-size: 30px; 29 | color: ${COLOR.primary3}; 30 | user-select: none; 31 | `; 32 | -------------------------------------------------------------------------------- /client/src/components/custom/ServerError.tsx: -------------------------------------------------------------------------------- 1 | import { Container, StatusText, GuideText } from './ServerError.style'; 2 | 3 | const ServerError = ({ status }): React.ReactElement => { 4 | return ( 5 | 6 | 7 | {status} 8 | Error! 9 | 10 | please try again 11 | 12 | ); 13 | }; 14 | 15 | export default ServerError; 16 | -------------------------------------------------------------------------------- /client/src/components/icons/CancelIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { IconPropType } from '@ts-types/components/icons'; 3 | 4 | const CancelIcon = ({ 5 | className, 6 | width, 7 | height, 8 | fill, 9 | stroke, 10 | }: IconPropType): React.ReactElement => { 11 | return ( 12 | 19 | 23 | 27 | 28 | ); 29 | }; 30 | 31 | export default CancelIcon; 32 | -------------------------------------------------------------------------------- /client/src/components/icons/ChangeNicknameIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { IconPropType } from '@ts-types/components/icons'; 3 | 4 | const ChangeNicknameIcon = ({ 5 | className, 6 | width, 7 | height, 8 | fill, 9 | stroke, 10 | }: IconPropType): React.ReactElement => { 11 | return ( 12 | 19 | 23 | 27 | 28 | ); 29 | }; 30 | 31 | export default ChangeNicknameIcon; 32 | -------------------------------------------------------------------------------- /client/src/components/icons/ChatIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { IconPropType } from '@ts-types/components/icons'; 3 | 4 | const ChatIcon = ({ className, width, height, fill, stroke }: IconPropType): React.ReactElement => { 5 | return ( 6 | 14 | 20 | 21 | ); 22 | }; 23 | 24 | export default ChatIcon; 25 | -------------------------------------------------------------------------------- /client/src/components/icons/CopyIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { IconPropType } from '@ts-types/components/icons'; 3 | 4 | const CopyIcon = ({ width, height, fill, stroke }: IconPropType): React.ReactElement => { 5 | return ( 6 | 13 | 20 | 27 | 28 | ); 29 | }; 30 | 31 | export default CopyIcon; 32 | -------------------------------------------------------------------------------- /client/src/components/icons/DeleteFriendIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { IconPropType } from '@ts-types/components/icons'; 3 | 4 | const DeleteFriendIcon = ({ 5 | className, 6 | width, 7 | height, 8 | fill, 9 | stroke, 10 | }: IconPropType): React.ReactElement => { 11 | return ( 12 | 19 | 23 | 24 | 25 | ); 26 | }; 27 | 28 | export default DeleteFriendIcon; 29 | -------------------------------------------------------------------------------- /client/src/components/icons/DeleteIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { IconPropType } from '@ts-types/components/icons'; 3 | 4 | const DeleteIcon = ({ 5 | className, 6 | width, 7 | height, 8 | fill, 9 | stroke, 10 | }: IconPropType): React.ReactElement => { 11 | return ( 12 | 19 | 23 | 27 | 28 | ); 29 | }; 30 | 31 | export default DeleteIcon; 32 | -------------------------------------------------------------------------------- /client/src/components/icons/DownIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { IconPropType } from '@ts-types/components/icons'; 3 | 4 | const DownIcon = ({ className, width, height, fill, stroke }: IconPropType): React.ReactElement => { 5 | return ( 6 | 14 | 21 | 22 | ); 23 | }; 24 | 25 | export default DownIcon; 26 | -------------------------------------------------------------------------------- /client/src/components/icons/GameRuleIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { IconPropType } from '@ts-types/components/icons'; 3 | 4 | const GameRuleIcon = ({ 5 | className, 6 | width, 7 | height, 8 | fill, 9 | stroke, 10 | }: IconPropType): React.ReactElement => { 11 | return ( 12 | 20 | 21 | 25 | 26 | ); 27 | }; 28 | 29 | export default GameRuleIcon; 30 | -------------------------------------------------------------------------------- /client/src/components/icons/GreenXButtonIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { IconPropType } from '@ts-types/components/icons'; 3 | 4 | const GreenXButtonIcon = ({ 5 | className, 6 | width, 7 | height, 8 | fill, 9 | stroke, 10 | }: IconPropType): React.ReactElement => { 11 | return ( 12 | 19 | 20 | 26 | 32 | 33 | ); 34 | }; 35 | 36 | export default GreenXButtonIcon; 37 | -------------------------------------------------------------------------------- /client/src/components/icons/HistoryIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { IconPropType } from '@ts-types/components/icons'; 3 | 4 | const HistoryIcon = ({ 5 | className, 6 | width, 7 | height, 8 | fill, 9 | stroke, 10 | }: IconPropType): React.ReactElement => { 11 | return ( 12 | 19 | 23 | 28 | 32 | 33 | ); 34 | }; 35 | 36 | export default HistoryIcon; 37 | -------------------------------------------------------------------------------- /client/src/components/icons/HumanIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { IconPropType } from '@ts-types/components/icons'; 3 | 4 | const HumanIcon = ({ 5 | className, 6 | width, 7 | height, 8 | fill, 9 | stroke, 10 | }: IconPropType): React.ReactElement => { 11 | return ( 12 | 20 | 25 | 29 | 34 | 35 | ); 36 | }; 37 | 38 | export default HumanIcon; 39 | -------------------------------------------------------------------------------- /client/src/components/icons/LeftIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { IconPropType } from '@ts-types/components/icons'; 3 | 4 | const LeftIcon = ({ className, width, height, fill, stroke }: IconPropType): React.ReactElement => { 5 | return ( 6 | 13 | 20 | 21 | ); 22 | }; 23 | 24 | export default LeftIcon; 25 | -------------------------------------------------------------------------------- /client/src/components/icons/PaperPlaneIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { IconPropType } from '@ts-types/components/icons'; 3 | 4 | const PaperPlaneIcon = ({ 5 | className, 6 | width, 7 | height, 8 | fill, 9 | stroke, 10 | }: IconPropType): React.ReactElement => { 11 | return ( 12 | 20 | 27 | 34 | 35 | ); 36 | }; 37 | 38 | export default PaperPlaneIcon; 39 | -------------------------------------------------------------------------------- /client/src/components/icons/ProfileSquareIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { IconPropType } from '@ts-types/components/icons'; 3 | 4 | const ProfileSquareIcon = ({ 5 | className, 6 | width, 7 | height, 8 | fill, 9 | stroke, 10 | }: IconPropType): React.ReactElement => { 11 | return ( 12 | 19 | 28 | 34 | 40 | 46 | 47 | ); 48 | }; 49 | 50 | export default ProfileSquareIcon; 51 | -------------------------------------------------------------------------------- /client/src/components/icons/RejectIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { IconPropType } from '@ts-types/components/icons'; 3 | 4 | const RejectIcon = ({ 5 | className, 6 | width, 7 | height, 8 | fill, 9 | stroke, 10 | }: IconPropType): React.ReactElement => { 11 | return ( 12 | 19 | 23 | 27 | 28 | ); 29 | }; 30 | 31 | export default RejectIcon; 32 | -------------------------------------------------------------------------------- /client/src/components/icons/SmallAcceptIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { IconPropType } from '@ts-types/components/icons'; 3 | 4 | const SmallAcceptIcon = ({ 5 | className, 6 | width, 7 | height, 8 | fill, 9 | stroke, 10 | }: IconPropType): React.ReactElement => { 11 | return ( 12 | 19 | 23 | 27 | 28 | ); 29 | }; 30 | 31 | export default SmallAcceptIcon; 32 | -------------------------------------------------------------------------------- /client/src/components/icons/SmallRejectIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { IconPropType } from '@ts-types/components/icons'; 3 | 4 | const SmallRejectIcon = ({ 5 | className, 6 | width, 7 | height, 8 | fill, 9 | stroke, 10 | }: IconPropType): React.ReactElement => { 11 | return ( 12 | 19 | 23 | 27 | 28 | ); 29 | }; 30 | 31 | export default SmallRejectIcon; 32 | -------------------------------------------------------------------------------- /client/src/components/icons/VideoIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { IconPropType } from '@ts-types/components/icons'; 3 | 4 | const VideoIcon = ({ 5 | className, 6 | width, 7 | height, 8 | fill, 9 | stroke, 10 | }: IconPropType): React.ReactElement => { 11 | return ( 12 | 20 | 26 | 27 | ); 28 | }; 29 | 30 | export default VideoIcon; 31 | -------------------------------------------------------------------------------- /client/src/components/icons/XIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { IconPropType } from '@ts-types/components/icons'; 3 | 4 | const XIcon = ({ className, width, height, fill, stroke }: IconPropType): React.ReactElement => { 5 | return ( 6 | 14 | 20 | 26 | 27 | ); 28 | }; 29 | 30 | export default XIcon; 31 | -------------------------------------------------------------------------------- /client/src/components/room/ControlBar.style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR } from '@constant/style'; 3 | 4 | export const BarContainer = styled.div` 5 | width: 100%; 6 | height: 50px; 7 | display: flex; 8 | 9 | background-color: ${COLOR.primary2}; 10 | align-items: center; 11 | justify-content: space-between; 12 | `; 13 | 14 | export const LineBox = styled.div` 15 | display: flex; 16 | justify-content: center; 17 | `; 18 | 19 | export const ControlButton = styled.button` 20 | background-color: none; 21 | background: none; 22 | border: none; 23 | cursor: pointer; 24 | margin: 0 5px; 25 | &:hover { 26 | & > svg { 27 | padding: 2px; 28 | } 29 | } 30 | & > svg { 31 | pointer-events: none; 32 | } 33 | `; 34 | -------------------------------------------------------------------------------- /client/src/components/room/RoomMenu.style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR } from '@constant/style'; 3 | 4 | export const MenuBox = styled.div` 5 | flex: 0 0 auto; 6 | width: 320px; 7 | height: 100%; 8 | border-left: 1px solid ${COLOR.line}; 9 | display: flex; 10 | flex-direction: column; 11 | `; 12 | 13 | export const TopBar = styled.div` 14 | flex: 0 0 auto; 15 | width: 100%; 16 | height: 58px; 17 | padding: 0 15px; 18 | display: flex; 19 | justify-content: space-between; 20 | align-items: center; 21 | border-bottom: 1px solid ${COLOR.line}; 22 | 23 | &::before { 24 | width: 23px; 25 | content: ''; 26 | visibility: hidden; 27 | } 28 | & > span { 29 | font-size: 18px; 30 | user-select: none; 31 | } 32 | `; 33 | 34 | export const CloseButton = styled.button` 35 | width: 23px; 36 | height: 23px; 37 | padding: 0; 38 | 39 | background-color: ${COLOR.body}; 40 | outline: none; 41 | border: none; 42 | border-radius: 50%; 43 | cursor: pointer; 44 | 45 | display: flex; 46 | justify-content: center; 47 | align-items: center; 48 | 49 | &::before, 50 | &::after { 51 | display: inline-block; 52 | position: absolute; 53 | content: ' '; 54 | height: 15px; 55 | width: 2px; 56 | background-color: ${COLOR.white}; 57 | border-radius: 10px; 58 | } 59 | &::before { 60 | transform: rotate(45deg); 61 | } 62 | &::after { 63 | transform: rotate(-45deg); 64 | } 65 | `; 66 | 67 | export const SelectBox = styled.div` 68 | padding: 10px; 69 | height: 250px; 70 | display: flex; 71 | flex-direction: column; 72 | justify-content: center; 73 | align-items: center; 74 | `; 75 | -------------------------------------------------------------------------------- /client/src/components/room/RoomMenu.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { MenuBox, TopBar, CloseButton } from '@components/room/RoomMenu.style'; 3 | import { useSelector, useDispatch } from 'react-redux'; 4 | import { RootState } from '@src/store'; 5 | import { setMenuType } from '@store/room'; 6 | import RouteMenu from '@components/room/RouteMenu'; 7 | import useChatSocket from '@hooks/socket/useChatSocket'; 8 | import type { RoomMenuPropType } from '@ts-types/components/room'; 9 | 10 | const RoomMenu: React.FC = ({ 11 | startVoteRef, 12 | startGamesRef, 13 | onclickRequestFriend, 14 | }): React.ReactElement => { 15 | const menuType = useSelector((state: RootState) => state.room.menuType); 16 | const dispatch = useDispatch(); 17 | 18 | const { sendMessage } = useChatSocket(); 19 | 20 | if (!menuType) return <>; 21 | return ( 22 | 23 | 24 | {menuType} 25 | dispatch(setMenuType(''))}> 26 | 27 | 33 | 34 | ); 35 | }; 36 | 37 | export default RoomMenu; 38 | -------------------------------------------------------------------------------- /client/src/components/room/RouteMenu.style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const SelectBox = styled.div` 4 | padding: 10px; 5 | height: 250px; 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: center; 9 | align-items: center; 10 | `; 11 | -------------------------------------------------------------------------------- /client/src/components/room/RouteMenu.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { RootState } from '@src/store'; 4 | import { SelectBox } from '@components/room/RouteMenu.style'; 5 | import DeviceSelections from '@components/setting/DeviceSelections'; 6 | import Chat from '@components/room/chat/'; 7 | import Users from '@components/user/Users'; 8 | import Host from '@components/room/host/'; 9 | import GameMenu from '@components/room/games/GameMenu'; 10 | 11 | const RouteMenu = ({ 12 | startVoteRef, 13 | sendMessage, 14 | startGamesRef, 15 | onclickRequestFriend, 16 | }): React.ReactElement => { 17 | const menuType = useSelector((state: RootState) => state.room.menuType); 18 | 19 | switch (menuType) { 20 | case '설정': 21 | return ( 22 | 23 | 24 | 25 | ); 26 | case '채팅': 27 | return ; 28 | case '참가자': 29 | return ; 30 | case '방장': 31 | return ; 32 | case '게임': 33 | return ; 34 | default: 35 | return <>; 36 | } 37 | }; 38 | 39 | export default RouteMenu; 40 | -------------------------------------------------------------------------------- /client/src/components/room/animation-screen/QuestionMark.style.ts: -------------------------------------------------------------------------------- 1 | import styled, { keyframes } from 'styled-components'; 2 | import { Z_INDEX } from '@constant/style'; 3 | 4 | const activeMark = keyframes` 5 | 100% { 6 | background-position: -6900px 0; 7 | } 8 | `; 9 | 10 | export const QuestionScreen = styled.div<{ x: number; y: number }>` 11 | width: 150px; 12 | height: 213px; 13 | position: absolute; 14 | top: ${(props) => props.y - 170}px; 15 | left: ${(props) => props.x - 75}px; 16 | 17 | background-image: url('/images/question-sprite.png'); 18 | background-repeat: no-repeat; 19 | animation: ${activeMark} 2s steps(46) 1; 20 | 21 | z-index: ${Z_INDEX.question}; 22 | `; 23 | -------------------------------------------------------------------------------- /client/src/components/room/animation-screen/QuestionMark.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import { QuestionScreen } from '@components/room/animation-screen/QuestionMark.style'; 3 | import useUpdateSpeaker from '@hooks/useUpdateSpeaker'; 4 | import useToggleSpeaker from '@hooks/useToggleSpeaker'; 5 | import type { QuestionMarkPropType } from '@ts-types/components/room'; 6 | 7 | const QuestionMark: React.FC = ({ x, y }): React.ReactElement => { 8 | const audioRef = useRef(null); 9 | 10 | useUpdateSpeaker(audioRef); 11 | useToggleSpeaker(audioRef); 12 | 13 | return ( 14 | <> 15 | 16 | 17 | 18 | ); 19 | }; 20 | 21 | export default React.memo(QuestionMark); 22 | -------------------------------------------------------------------------------- /client/src/components/room/animation-screen/index.style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { Z_INDEX } from '@constant/style'; 3 | 4 | export const Screen = styled.div` 5 | position: relative; 6 | width: 100%; 7 | height: 100%; 8 | top: -100%; 9 | 10 | z-index: ${Z_INDEX.cheers}; 11 | `; 12 | 13 | export const CheersScreen = styled.img` 14 | width: 100%; 15 | height: 100%; 16 | display: none; 17 | `; 18 | -------------------------------------------------------------------------------- /client/src/components/room/chat/ChatForm.style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR, INPUT_STYLE } from '@constant/style'; 3 | 4 | export const SendingForm = styled.form` 5 | flex: 0 0 auto; 6 | width: 100%; 7 | height: 52px; 8 | 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | 13 | border-top: 1px solid ${COLOR.line}; 14 | background-color: ${COLOR.background}; 15 | 16 | & > input { 17 | ${INPUT_STYLE} 18 | flex: 1 1 auto; 19 | height: 36px; 20 | padding: 0 8px; 21 | margin: 0 8px; 22 | border-radius: 8px; 23 | } 24 | & > button { 25 | flex: 0 0 auto; 26 | width: 28px; 27 | height: 28px; 28 | padding: 0; 29 | margin: 0 16px 0 8px; 30 | 31 | display: flex; 32 | justify-content: center; 33 | align-items: center; 34 | 35 | border: none; 36 | border-radius: 8px; 37 | background-color: transparent; 38 | cursor: pointer; 39 | &:active { 40 | background-color: ${COLOR.primary1}; 41 | } 42 | } 43 | `; 44 | -------------------------------------------------------------------------------- /client/src/components/room/chat/ChatForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useParams } from 'react-router-dom'; 3 | import { SendingForm } from '@components/room/chat/ChatForm.style'; 4 | import { PaperPlaneIcon } from '@components/icons'; 5 | import { RootState } from '@src/store'; 6 | import { useSelector } from 'react-redux'; 7 | import type { ChatPropType } from '@ts-types/components/room'; 8 | 9 | const ChatForm: React.FC = ({ sendMessage }): React.ReactElement => { 10 | const { code } = useParams(); 11 | const [message, setMessage] = useState(''); 12 | const user = useSelector((state: RootState) => state.user); 13 | 14 | const onSubmitMessage = (e) => { 15 | if (message) { 16 | sendMessage({ 17 | msg: message, 18 | chatRoomCode: code, 19 | user, 20 | }); 21 | setMessage(''); 22 | } 23 | }; 24 | 25 | return ( 26 | e.preventDefault()}> 27 | setMessage(target?.value ?? '')} /> 28 | 31 | 32 | ); 33 | }; 34 | 35 | export default ChatForm; 36 | -------------------------------------------------------------------------------- /client/src/components/room/chat/ChatItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { 4 | ColumnBox, 5 | UserContainer, 6 | ProfileContainer, 7 | Name, 8 | MsgContent, 9 | } from '@components/room/chat/ChatItem.style'; 10 | import { HumanIcon } from '@components/icons'; 11 | import { RootState } from '@src/store'; 12 | import type { ChatItemPropType } from '@ts-types/components/room'; 13 | 14 | const ChatItem: React.FC = ({ 15 | isSelf, 16 | message, 17 | date, 18 | sid, 19 | }): React.ReactElement => { 20 | const users = useSelector((state: RootState) => state.room.users); 21 | const { imgUrl, nickname } = useSelector((state: RootState) => state.user); 22 | const targetNick = isSelf ? nickname : users[sid]?.nickname; 23 | const targetImg = isSelf ? imgUrl : users[sid]?.imgUrl; 24 | 25 | return ( 26 | 27 | 28 | 29 | {targetImg ? UserProfile : } 30 | 31 | {targetNick || 'judangs'} 32 | {date} 33 | 34 | {message} 35 | 36 | ); 37 | }; 38 | 39 | export default React.memo(ChatItem); 40 | -------------------------------------------------------------------------------- /client/src/components/room/chat/ChatMenuIcon.style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR } from '@constant/style'; 3 | 4 | export const IconContainer = styled.button` 5 | background-color: none; 6 | background: none; 7 | border: none; 8 | cursor: pointer; 9 | margin: 0 5px; 10 | &:hover { 11 | & > svg { 12 | padding: 2px; 13 | } 14 | } 15 | & > svg { 16 | pointer-events: none; 17 | } 18 | `; 19 | 20 | export const CountBox = styled.div` 21 | max-width: 50px; 22 | position: absolute; 23 | padding: 2px 5px; 24 | margin-top: -8px; 25 | margin-left: -8px; 26 | 27 | overflow: hidden; 28 | text-overflow: ellipsis; 29 | white-space: nowrap; 30 | font-size: 12px; 31 | font-weight: bold; 32 | 33 | color: ${COLOR.white}; 34 | background-color: ${COLOR.error}; 35 | border-radius: 7px; 36 | `; 37 | -------------------------------------------------------------------------------- /client/src/components/room/chat/ChatMenuIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { RootState } from '@src/store'; 4 | import { IconContainer, CountBox } from '@components/room/chat/ChatMenuIcon.style'; 5 | import { ChatIcon } from '@components/icons'; 6 | 7 | const ChatMenuIcon = (): React.ReactElement => { 8 | const unreadChat = useSelector((state: RootState) => state.room.unreadChat); 9 | 10 | return ( 11 | 12 | {!!unreadChat && {unreadChat}} 13 | 14 | 15 | ); 16 | }; 17 | 18 | export default ChatMenuIcon; 19 | -------------------------------------------------------------------------------- /client/src/components/room/chat/index.style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR } from '@constant/style'; 3 | 4 | export const ScrollBox = styled.div` 5 | flex: 1 1 auto; 6 | padding: 0 20; 7 | display: flex; 8 | flex-direction: column; 9 | background-color: ${COLOR.primary2}; 10 | overflow: hidden; 11 | `; 12 | 13 | export const MessageList = styled.ul` 14 | flex: 1 1 auto; 15 | padding: 0 8px 0 0; 16 | margin: 10px 5px 10px; 17 | 18 | overflow-x: hidden; 19 | overflow-y: scroll; 20 | word-wrap: break-word; 21 | 22 | &::-webkit-scrollbar { 23 | width: 4px; 24 | height: 16px; 25 | border-radius: 10px; 26 | background: ${COLOR.line}; 27 | } 28 | &::-webkit-scrollbar-thumb { 29 | background-color: ${COLOR.primary3}; 30 | border-radius: 10px; 31 | } 32 | `; 33 | -------------------------------------------------------------------------------- /client/src/components/room/chat/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { RootState } from '@src/store'; 4 | import Socket from '@socket/socket'; 5 | import { ScrollBox, MessageList } from '@components/room/chat/index.style'; 6 | import ChatItem from '@components/room/chat/ChatItem'; 7 | import ChatForm from '@components/room/chat/ChatForm'; 8 | import type { ChatPropType } from '@ts-types/components/room'; 9 | 10 | const Chat: React.FC = ({ sendMessage }): React.ReactElement => { 11 | const chatLog = useSelector((state: RootState) => state.room.chatLog); 12 | const chatWindow = useRef(null); 13 | const myID = Socket.getSID(); 14 | 15 | const downScroll = (): void => { 16 | const refDom = chatWindow.current; 17 | if (!refDom) return; 18 | refDom.scrollTop = refDom.scrollHeight; 19 | }; 20 | 21 | useEffect(() => { 22 | downScroll(); 23 | }, [chatLog]); 24 | 25 | return ( 26 | 27 | 28 | {chatLog.map(({ sid, msg, date }) => ( 29 | 36 | ))} 37 | 38 | 39 | 40 | ); 41 | }; 42 | 43 | export default Chat; 44 | -------------------------------------------------------------------------------- /client/src/components/room/games/GameBox.style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR, BOX_SHADOW } from '@constant/style'; 3 | 4 | export const InfoContainer = styled.li` 5 | ${BOX_SHADOW} 6 | display: flex; 7 | background-color: ${COLOR.white}; 8 | padding: 15px; 9 | margin-bottom: 15px; 10 | cursor: pointer; 11 | justify-content: space-between; 12 | align-items: center; 13 | 14 | div { 15 | display: flex; 16 | align-items: center; 17 | } 18 | `; 19 | 20 | export const Title = styled.div` 21 | display: flex; 22 | margin: auto 0; 23 | align-items: center; 24 | svg { 25 | margin-right: 10px; 26 | } 27 | `; 28 | -------------------------------------------------------------------------------- /client/src/components/room/games/GameBox.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { InfoContainer, Title } from '@components/room/games/GameBox.style'; 3 | import { GameRuleIcon } from '@components/icons'; 4 | import Modal from '@components/custom/Modal'; 5 | import type { GameBoxPropType } from '@ts-types/components/room'; 6 | 7 | const GameBox: React.FC = ({ 8 | children, 9 | icon, 10 | title, 11 | start, 12 | }): React.ReactElement => { 13 | const [isOpen, setIsOpen] = useState(false); 14 | 15 | const toggleRule = (e) => { 16 | e.stopPropagation(); 17 | setIsOpen((prev) => !prev); 18 | }; 19 | const closeRule = () => { 20 | setIsOpen(false); 21 | }; 22 | 23 | return ( 24 | <> 25 | 26 | 27 | {icon} 28 | {title}게임 29 | 30 |
31 | 32 |
33 |
34 | 35 | {children} 36 | 37 | 38 | ); 39 | }; 40 | 41 | export default GameBox; 42 | -------------------------------------------------------------------------------- /client/src/components/room/games/GameMenu.style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR } from '@constant/style'; 3 | 4 | export const GameListBox = styled.ul` 5 | flex: 1 1 auto; 6 | padding: 15px; 7 | margin: 0; 8 | display: flex; 9 | flex-direction: column; 10 | background-color: ${COLOR.primary2}; 11 | overflow: hidden; 12 | `; 13 | 14 | export const GameRuleBox = styled.div` 15 | padding: 10px; 16 | font-size: 15px; 17 | `; 18 | 19 | export const GameTitle = styled.div` 20 | font-size: 18px; 21 | font-weight: bold; 22 | color: ${COLOR.titleActive}; 23 | margin-bottom: 10px; 24 | align-items: center; 25 | `; 26 | -------------------------------------------------------------------------------- /client/src/components/room/games/GameMenu.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { GameListBox, GameRuleBox, GameTitle } from '@components/room/games/GameMenu.style'; 3 | import GameBox from '@components/room/games/GameBox'; 4 | import { gameList } from '@src/components/room/games/gameList'; 5 | import type { GamesPropType } from '@ts-types/components/room'; 6 | 7 | const GameMenu: React.FC = ({ startGamesRef }): React.ReactElement => { 8 | return ( 9 | 10 | {gameList.map(({ icon, title, content }) => ( 11 | 12 | 13 | {title} 게임 설명서 14 | {content} 15 | 16 | 17 | ))} 18 | 19 | ); 20 | }; 21 | 22 | export default GameMenu; 23 | -------------------------------------------------------------------------------- /client/src/components/room/games/LiarGame.style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR, BTN_STYLE } from '@constant/style'; 3 | 4 | export const Contents = styled.div<{ 5 | keyword: React.MutableRefObject<{ subject: string; keyword: string }>; 6 | }>` 7 | width: 400px; 8 | height: 200px; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: space-between; 12 | align-items: center; 13 | padding: 20px; 14 | position: relative; 15 | 16 | .host { 17 | span { 18 | font-weight: 600; 19 | color: ${COLOR.black}; 20 | } 21 | } 22 | 23 | .subject { 24 | span { 25 | font-weight: 600; 26 | color: ${COLOR.black}; 27 | } 28 | } 29 | 30 | .keyword { 31 | span { 32 | font-weight: 600; 33 | ${(props) => 34 | props.keyword.current.keyword === '라이어' 35 | ? `color: ${COLOR.error3}` 36 | : `color: ${COLOR.titleActive}`}; 37 | } 38 | } 39 | `; 40 | 41 | export const GameTitle = styled.div` 42 | width: 100%; 43 | font-weight: bold; 44 | color: ${COLOR.titleActive}; 45 | `; 46 | 47 | export const GameStopButton = styled.button` 48 | ${BTN_STYLE}; 49 | padding: 5px 10px; 50 | font-size: 15px; 51 | `; 52 | -------------------------------------------------------------------------------- /client/src/components/room/games/RandomPickGame.style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR, BTN_STYLE } from '@constant/style'; 3 | 4 | export const Contents = styled.div` 5 | width: 400px; 6 | height: 200px; 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: space-between; 10 | align-items: center; 11 | padding: 20px; 12 | position: relative; 13 | 14 | .host { 15 | span { 16 | font-weight: 600; 17 | color: ${COLOR.black}; 18 | } 19 | } 20 | 21 | .random-pick { 22 | span { 23 | font-weight: 600; 24 | color: ${COLOR.error3}; 25 | } 26 | } 27 | `; 28 | 29 | export const GameTitle = styled.div` 30 | width: 100%; 31 | font-weight: bold; 32 | color: ${COLOR.titleActive}; 33 | `; 34 | 35 | export const GameStopButton = styled.button` 36 | ${BTN_STYLE}; 37 | padding: 5px 10px; 38 | font-size: 15px; 39 | `; 40 | -------------------------------------------------------------------------------- /client/src/components/room/games/RandomPickGame.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Modal from '@src/components/custom/Modal'; 3 | import Socket from '@socket/socket'; 4 | import { useSelector, useDispatch } from 'react-redux'; 5 | import { RootState } from '@src/store'; 6 | import { setCurrentGame } from '@store/room'; 7 | import { Contents, GameTitle, GameStopButton } from '@components/room/games/RandomPickGame.style'; 8 | import type { RandomPickGamePropType } from '@ts-types/components/room'; 9 | 10 | const RandomPickGame: React.FC = ({ onePickRef }): React.ReactElement => { 11 | const dispatch = useDispatch(); 12 | const users = useSelector((state: RootState) => state.room.users); 13 | const gameHost = useSelector((state: RootState) => state.room.currentGame.host); 14 | const stopGame = () => { 15 | dispatch(setCurrentGame({ title: '', host: gameHost })); 16 | }; 17 | return ( 18 | 24 | 25 | 랜덤픽 게임 26 |
27 | {users[gameHost].nickname} 님이 게임을 시작하셨습니다. 28 |
29 |
30 | {users[onePickRef.current].nickname} 님이 당첨되셨습니다! 31 |
32 | {gameHost === Socket.getSID() ? ( 33 | 전체 닫기 34 | ) : ( 35 | 닫기 36 | )} 37 |
38 |
39 | ); 40 | }; 41 | 42 | export default RandomPickGame; 43 | -------------------------------------------------------------------------------- /client/src/components/room/games/UpdownGame.style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR, BTN_STYLE, Z_INDEX } from '@constant/style'; 3 | 4 | export const Contents = styled.div` 5 | width: 400px; 6 | height: 200px; 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: space-between; 10 | align-items: center; 11 | padding: 20px; 12 | position: relative; 13 | 14 | img { 15 | position: absolute; 16 | top: 40px; 17 | } 18 | 19 | span { 20 | font-weight: 600; 21 | color: ${COLOR.black}; 22 | } 23 | 24 | .random-num { 25 | position: relative; 26 | z-index: ${Z_INDEX.updownNum}; 27 | left: 5px; 28 | } 29 | `; 30 | 31 | export const GameTitle = styled.div` 32 | width: 100%; 33 | font-weight: bold; 34 | color: ${COLOR.titleActive}; 35 | `; 36 | 37 | export const GameStopButton = styled.button` 38 | ${BTN_STYLE}; 39 | padding: 5px 10px; 40 | font-size: 15px; 41 | `; 42 | -------------------------------------------------------------------------------- /client/src/components/room/games/gameList.tsx: -------------------------------------------------------------------------------- 1 | import { UpdownGameIcon, LiarGameIcon, RandomPickGameIcon } from '@components/icons'; 2 | import { LIAR, UP_DOWN, RANDOM_PICK } from 'sooltreaming-domain/constant/gameName'; 3 | 4 | export const gameList = [ 5 | { 6 | icon: , 7 | title: UP_DOWN, 8 | content: ( 9 |
10 | 업다운 게임은 주최자에게 주어진 숫자를 맞히는 게임입니다. 11 |
12 |
1-50 사이의 숫자가 게임의 주최자에게 주어집니다.
13 | 나머지 플레이어들은 돌아가면서 숫자를 말합니다.
14 | 주최자는 해당 숫자가 정답보다 크면 'up', 작으면 'down'을 말합니다. 15 |
N 바퀴를 돌 때까지 숫자를 못 맞히면 주최자 승리!
16 | 숫자를 맞히면 참가자 승리!
17 |
※ 추가적인 벌칙, 승리는 참가자들이 자유롭게 정할 수 있습니다. 18 |
19 | ), 20 | }, 21 | { 22 | icon: , 23 | title: LIAR, 24 | content: ( 25 |
26 | 라이어 게임은 라이어를 찾는 게임입니다.
27 |
28 | 게임을 시작하면 참가자 전원에게 키워드가 적힌 카드가 주어집니다.
29 | 그중 단 한 명은 키워드가 없는 '라이어' 카드가 주어집니다.
30 | 참가자들은 돌아가면서 자신이 받은 키워드에 대하여 설명합니다.
31 | 모두가 설명이 끝나면 라이어를 찾습니다.
32 |
33 | 라이어를 맞히면 참가자 승리!
못 맞히면 라이어 승리! 34 |
35 |
36 |
※ 추가적인 벌칙, 승리는 참가자들이 자유롭게 정할 수 있습니다. 37 |
38 | ), 39 | }, 40 | { 41 | icon: , 42 | title: RANDOM_PICK, 43 | content: ( 44 |
45 | 랜덤픽 게임은 랜덤한 사람 1명을 뽑아주는 게임입니다.
46 |
47 | 게임을 시작하면 방에 접속해있는 사람들 중 1명이 선정됩니다. 48 |
49 |
※ 추가적인 벌칙, 승리는 참가자들이 자유롭게 정할 수 있습니다. 50 |
51 | ), 52 | }, 53 | ]; 54 | -------------------------------------------------------------------------------- /client/src/components/room/games/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { RootState } from '@src/store'; 4 | import UpdownGame from '@components/room/games/UpdownGame'; 5 | import useGameSocket from '@hooks/socket/useGameSocket'; 6 | import type { GamesPropType } from '@ts-types/components/room'; 7 | import LiarGame from '@components/room/games/LiarGame'; 8 | import RandomPickGame from '@components/room/games/RandomPickGame'; 9 | 10 | const Games: React.FC = ({ startGamesRef }): React.ReactElement => { 11 | const currentGame = useSelector((state: RootState) => state.room.currentGame.title); 12 | const { GameStartHandlerList, randomNumRef, keywordRef, onePickRef } = useGameSocket(); 13 | 14 | useEffect(() => { 15 | startGamesRef.current = GameStartHandlerList; 16 | }, []); 17 | 18 | switch (currentGame) { 19 | case '업다운': 20 | return ; 21 | case '라이어': 22 | return ; 23 | case '랜덤픽': 24 | return ; 25 | default: 26 | return <>; 27 | } 28 | }; 29 | 30 | export default Games; 31 | -------------------------------------------------------------------------------- /client/src/components/room/host/Participant.style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR, BOX_SHADOW } from '@constant/style'; 3 | 4 | export const RowBox = styled.div` 5 | display: flex; 6 | width: 100%; 7 | height: 50px; 8 | align-items: center; 9 | justify-content: space-between; 10 | background-color: ${COLOR.white}; 11 | list-style: none; 12 | 13 | ${BOX_SHADOW} 14 | padding: 15px; 15 | margin-bottom: 15px; 16 | & > div { 17 | display: flex; 18 | } 19 | `; 20 | 21 | export const Profile = styled.div` 22 | width: 100%; 23 | height: 32px; 24 | display: flex; 25 | align-items: center; 26 | overflow-x: hidden; 27 | overflow-y: hidden; 28 | margin-right: 8px; 29 | 30 | & > img { 31 | width: 30px; 32 | height: 30px; 33 | border-radius: 5px; 34 | -webkit-user-drag: none; 35 | user-select: none; 36 | margin-right: 10px; 37 | } 38 | 39 | & > div { 40 | margin: auto 0; 41 | } 42 | 43 | span { 44 | overflow: hidden; 45 | text-overflow: ellipsis; 46 | white-space: nowrap; 47 | user-select: none; 48 | -webkit-user-drag: none; 49 | } 50 | `; 51 | 52 | export const FlexBox = styled.div` 53 | display: flex; 54 | justify-content: center; 55 | align-items: center; 56 | `; 57 | -------------------------------------------------------------------------------- /client/src/components/room/host/Participant.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { RowBox, Profile, FlexBox } from '@components/room/host/Participant.style'; 3 | import { HumanIcon, VideoIcon, MicIcon } from '@components/icons'; 4 | import SettingToggle from '@components/setting/SettingToggle'; 5 | 6 | const Participant = ({ 7 | sid, 8 | user, 9 | userDevices, 10 | turnOffOtherVideo, 11 | turnOffOtherAudio, 12 | }): React.ReactElement => { 13 | const targetNick = user?.nickname; 14 | const targetImg = user?.imgUrl; 15 | const isVideoOn = userDevices?.isVideoOn; 16 | const isAudioOn = userDevices?.isAudioOn; 17 | 18 | const turnOffVideo = () => { 19 | if (!isVideoOn) return; 20 | turnOffOtherVideo({ sid, isVideoOn: false }); 21 | }; 22 | 23 | const turnOffAudio = () => { 24 | if (!isAudioOn) return; 25 | turnOffOtherAudio({ sid, isAudioOn: false }); 26 | }; 27 | 28 | return ( 29 | 30 | 31 | {targetImg ? other_user_image : } 32 | {targetNick || 'judangs'} 33 | 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default Participant; 43 | -------------------------------------------------------------------------------- /client/src/components/room/host/ParticipantController.style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR } from '@constant/style'; 3 | 4 | export const ColumnBox = styled.div` 5 | display: flex; 6 | 7 | flex-direction: column; 8 | overflow-y: scroll; 9 | 10 | margin-bottom: 10px; 11 | padding: 15px; 12 | position: relative; 13 | 14 | &::-webkit-scrollbar { 15 | width: 4px; 16 | height: 16px; 17 | border-radius: 10px; 18 | background: ${COLOR.line}; 19 | } 20 | &::-webkit-scrollbar-thumb { 21 | background-color: ${COLOR.primary3}; 22 | border-radius: 10px; 23 | } 24 | `; 25 | -------------------------------------------------------------------------------- /client/src/components/room/host/ParticipantController.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux'; 2 | import { RootState } from '@src/store'; 3 | import { ColumnBox } from '@components/room/host/ParticipantController.style'; 4 | import Participant from '@components/room/host/Participant'; 5 | 6 | const ParticipantController = ({ turnOffOtherVideo, turnOffOtherAudio }): React.ReactElement => { 7 | const users = useSelector((state: RootState) => state.room.users); 8 | const hostSID = useSelector((state: RootState) => state.room.hostSID); 9 | const usersDevices = useSelector((state: RootState) => state.room.usersDevices); 10 | 11 | return ( 12 | 13 | {Object.entries(users).map(([sid, user]) => { 14 | if (hostSID === sid) return null; 15 | return ( 16 | 24 | ); 25 | })} 26 | 27 | ); 28 | }; 29 | 30 | export default ParticipantController; 31 | -------------------------------------------------------------------------------- /client/src/components/room/host/RoomController.style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR, BOX_SHADOW } from '@constant/style'; 3 | 4 | export const ColumnBox = styled.div` 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | 9 | flex-direction: column; 10 | `; 11 | 12 | export const RowBox = styled.div` 13 | display: flex; 14 | justify-content: center; 15 | align-items: center; 16 | 17 | margin: 8px; 18 | 19 | & > span { 20 | font-size: 20px; 21 | font-weight: bold; 22 | } 23 | `; 24 | 25 | export const IconButton = styled.button` 26 | background-color: none; 27 | background: none; 28 | border: none; 29 | cursor: pointer; 30 | margin: 0 5px; 31 | 32 | & > svg { 33 | pointer-events: none; 34 | } 35 | `; 36 | 37 | export const ToggleButton = styled.div` 38 | width: 140px; 39 | height: 40px; 40 | cursor: pointer; 41 | user-select: none; 42 | 43 | position: relative; 44 | 45 | background-color: ${COLOR.line}; 46 | border-radius: 10px; 47 | 48 | margin-left: 8px; 49 | `; 50 | 51 | export const DialogButton = styled.div<{ isSelected: boolean }>` 52 | display: flex; 53 | justify-content: center; 54 | align-items: center; 55 | 56 | min-width: 70px; 57 | width: 50%; 58 | height: 100%; 59 | 60 | cursor: pointer; 61 | background-color: ${(props) => (props.isSelected ? COLOR.titleActive : COLOR.error)}; 62 | color: ${COLOR.white}; 63 | 64 | box-sizing: border-box; 65 | ${BOX_SHADOW} 66 | 67 | padding: 8px 12px; 68 | 69 | position: absolute; 70 | left: ${(props) => (props.isSelected ? 0 : 70)}px; 71 | transition: all 0.3s ease; 72 | 73 | font-size: 14px; 74 | font-weight: bold; 75 | `; 76 | -------------------------------------------------------------------------------- /client/src/components/room/host/RoomController.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CopyIcon } from '@components/icons'; 3 | import { useSelector } from 'react-redux'; 4 | import { RootState } from '@src/store'; 5 | 6 | import { 7 | ColumnBox, 8 | RowBox, 9 | IconButton, 10 | ToggleButton, 11 | DialogButton, 12 | } from '@components/room/host/RoomController.style'; 13 | 14 | const copyURL = (): void => { 15 | navigator.clipboard.writeText(window.location.href); 16 | }; 17 | 18 | const RoomController = ({ toggleRoomEntry }): React.ReactElement => { 19 | const code = useSelector((state: RootState) => state.room.roomCode); 20 | const isOpen = useSelector((state: RootState) => state.room.isOpen); 21 | 22 | return ( 23 | 24 | 25 | 방 코드 번호 : {code} 26 | 27 | 28 | 29 | 30 | 31 | 방 접속 제한 : 32 | 33 | {isOpen ? 'Open' : 'Close'} 34 | 35 | 36 | 37 | ); 38 | }; 39 | 40 | export default RoomController; 41 | -------------------------------------------------------------------------------- /client/src/components/room/host/index.style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR } from '@constant/style'; 3 | 4 | export const ControlBox = styled.div` 5 | flex: 1 1 auto; 6 | display: flex; 7 | justify-content: space-between; 8 | flex-direction: column; 9 | background-color: ${COLOR.primary2}; 10 | overflow: hidden; 11 | `; 12 | -------------------------------------------------------------------------------- /client/src/components/room/host/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ControlBox } from '@components/room/host/index.style'; 3 | 4 | import ParticipantController from '@components/room/host/ParticipantController'; 5 | import RoomController from '@components/room/host/RoomController'; 6 | import useControlSocket from '@hooks/socket/useControlSocket'; 7 | 8 | const Host: React.FC = (): React.ReactElement => { 9 | const { toggleRoomEntry, turnOffOtherVideo, turnOffOtherAudio } = useControlSocket(); 10 | return ( 11 | 12 | 16 | 17 | 18 | ); 19 | }; 20 | 21 | export default Host; 22 | -------------------------------------------------------------------------------- /client/src/components/room/index.style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR } from '@constant/style'; 3 | 4 | export const FullScreen = styled.div` 5 | width: 100%; 6 | height: 100%; 7 | display: flex; 8 | background-color: ${COLOR.background}; 9 | `; 10 | 11 | export const FlexBox = styled.section` 12 | position: relative; 13 | flex: 1 1 auto; 14 | overflow: hidden; 15 | position: relative; 16 | `; 17 | 18 | export const ColumnBox = styled(FullScreen)` 19 | flex-direction: column; 20 | `; 21 | -------------------------------------------------------------------------------- /client/src/components/room/monitor/Video.style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR, Z_INDEX } from '@constant/style'; 3 | 4 | export const CameraContainer = styled.div` 5 | position: relative; 6 | width: 100%; 7 | height: 100%; 8 | z-index: ${Z_INDEX.camOn}; 9 | padding: 10px; 10 | border-radius: 10px; 11 | overflow: hidden; 12 | `; 13 | 14 | export const Camera = styled.video` 15 | position: absolute; 16 | top: 0; 17 | left: 0; 18 | width: 100%; 19 | height: 100%; 20 | background-color: #c4c4c4; 21 | `; 22 | 23 | export const ImageBox = styled.div<{ isVideoOn: any }>` 24 | background-color: ${COLOR.body}; 25 | object-fit: contain; 26 | position: absolute; 27 | top: 0; 28 | left: 0; 29 | width: 100%; 30 | height: 100%; 31 | visibility: ${(props) => (props.isVideoOn ? 'hidden' : 'block')}; 32 | z-index: ${Z_INDEX.camOff}; 33 | 34 | display: flex; 35 | justify-content: center; 36 | align-items: center; 37 | `; 38 | 39 | export const ProfileImage = styled.img` 40 | height: 80%; 41 | object-fit: contain; 42 | border-radius: 50%; 43 | `; 44 | 45 | export const Name = styled.span` 46 | display: flex; 47 | position: absolute; 48 | bottom: 10px; 49 | background-color: ${COLOR.black}; 50 | color: ${COLOR.white}; 51 | padding: 5px 7px; 52 | opacity: 0.5; 53 | z-index: ${Z_INDEX.nickname}; 54 | 55 | & > svg { 56 | margin-left: 7px; 57 | &:last-child { 58 | position: absolute; 59 | right: 6px; 60 | } 61 | } 62 | `; 63 | -------------------------------------------------------------------------------- /client/src/components/room/monitor/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Monitor, CloseUpContainer } from '@components/room/monitor/index.style'; 3 | import { useSelector } from 'react-redux'; 4 | import { RootState } from '@src/store'; 5 | import OtherVideo from '@components/room/monitor/OtherVideo'; 6 | import MyVideo from '@components/room/monitor/MyVideo'; 7 | 8 | const ChatMonitor: React.FC = (): React.ReactElement => { 9 | const streams = useSelector((state: RootState) => state.room.streams); 10 | const closeUpUser = useSelector((state: RootState) => state.room.closeUpUser); 11 | const count = Object.values(streams).length + 1; 12 | 13 | return ( 14 | 15 | 16 | 17 | {Object.entries(streams).map(([sid, otherStream]) => { 18 | const peerClassName = closeUpUser ? (sid === closeUpUser ? 'closeup' : 'mini') : ''; 19 | return ( 20 | 21 | ); 22 | })} 23 | 24 | 25 | ); 26 | }; 27 | 28 | export default ChatMonitor; 29 | -------------------------------------------------------------------------------- /client/src/components/room/scaffold/TimerBomb.style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR } from '@constant/style'; 3 | 4 | export const TimerContainer = styled.div` 5 | align-self: flex-end; 6 | width: 83px; 7 | height: 83px; 8 | flex: 0 0 auto; 9 | 10 | & > img { 11 | width: 100%; 12 | -webkit-user-drag: none; 13 | } 14 | & > div { 15 | width: 60px; 16 | height: 60px; 17 | margin: 16px 23px 7px 0; 18 | position: relative; 19 | top: -100%; 20 | 21 | display: flex; 22 | justify-content: center; 23 | align-items: center; 24 | 25 | font-size: 25px; 26 | font-weight: bold; 27 | color: ${COLOR.white}; 28 | user-select: none; 29 | } 30 | `; 31 | -------------------------------------------------------------------------------- /client/src/components/room/scaffold/TimerBomb.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import { TimerContainer } from '@components/room/scaffold/TimerBomb.style'; 3 | import { SECOND_TO_MS, VOTE_TIME } from 'sooltreaming-domain/constant/addition'; 4 | 5 | const TimerBomb: React.FC = (): React.ReactElement => { 6 | const [time, setTime] = useState(VOTE_TIME); 7 | const timeout = useRef(); 8 | 9 | useEffect(() => { 10 | return () => { 11 | if (timeout.current) clearTimeout(timeout.current); 12 | }; 13 | }, []); 14 | useEffect(() => { 15 | if (time <= 0) return; 16 | timeout.current = setTimeout(() => { 17 | setTime((prev) => prev - 1); 18 | }, SECOND_TO_MS); 19 | }, [time]); 20 | 21 | return ( 22 | 23 | timer 24 |
{time.toString()}
25 |
26 | ); 27 | }; 28 | 29 | export default TimerBomb; 30 | -------------------------------------------------------------------------------- /client/src/components/room/scaffold/VotePresser.style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR } from '@constant/style'; 3 | 4 | export const Title = styled.h2` 5 | position: absolute; 6 | left: 0; 7 | top: 0; 8 | width: 100%; 9 | padding: 30px 100px 15px 30px; 10 | color: ${COLOR.titleActive}; 11 | user-select: none; 12 | overflow: hidden; 13 | text-overflow: ellipsis; 14 | white-space: nowrap; 15 | 16 | & span { 17 | color: ${COLOR.point}; 18 | } 19 | `; 20 | 21 | export const PressSection = styled.div` 22 | width: 640px; 23 | padding: 50px 70px; 24 | 25 | display: flex; 26 | justify-content: space-around; 27 | align-items: center; 28 | 29 | & > button { 30 | width: 180px; 31 | padding: 15px; 32 | 33 | outline: none; 34 | background-color: ${COLOR.white}; 35 | border-radius: 10px; 36 | border: 1px solid ${COLOR.line}; 37 | cursor: pointer; 38 | &:hover { 39 | background-color: ${COLOR.offWhite}; 40 | } 41 | &:active { 42 | background-color: ${COLOR.line}; 43 | border: 1px solid ${COLOR.primary2}; 44 | } 45 | } 46 | & img { 47 | width: 100%; 48 | height: 100%; 49 | 50 | -webkit-user-drag: none; 51 | } 52 | `; 53 | -------------------------------------------------------------------------------- /client/src/components/room/scaffold/VotePresser.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Title, PressSection } from '@src/components/room/scaffold/VotePresser.style'; 3 | 4 | const VotePresser = ({ isVote, target, sendDecision }): React.ReactElement => { 5 | if (isVote) return <>; 6 | return ( 7 | <> 8 | 9 | <span>{target}</span>을(를) 처분할까요? 10 | 11 | 12 | 15 | 18 | 19 | 20 | ); 21 | }; 22 | 23 | export default VotePresser; 24 | -------------------------------------------------------------------------------- /client/src/components/room/scaffold/Voters.style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const FlexBox = styled.div` 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | 8 | & > svg { 9 | margin: 0 3px; 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /client/src/components/room/scaffold/Voters.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FlexBox } from '@components/room/scaffold/Voters.style'; 3 | import { VoterIcon } from '@components/icons'; 4 | import { COLOR } from '@constant/style'; 5 | import type { VotersPropType } from '@ts-types/components/room'; 6 | 7 | const Voters: React.FC = ({ total, approves, rejects }): React.ReactElement => { 8 | return ( 9 | 10 | {Array.from({ length: total }, (_, index) => { 11 | let color = COLOR.body; 12 | if (index < approves) color = COLOR.titleActive; 13 | if (index > total - rejects - 1) color = COLOR.error; 14 | return ; 15 | })} 16 | 17 | ); 18 | }; 19 | 20 | export default Voters; 21 | -------------------------------------------------------------------------------- /client/src/components/room/scaffold/index.style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Content = styled.div<{ isVote: boolean }>` 4 | padding-bottom: ${(props) => (props.isVote ? 0 : '30px')}; 5 | 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | flex-direction: ${(props) => (props.isVote ? 'row' : 'column')}; 10 | `; 11 | -------------------------------------------------------------------------------- /client/src/components/room/scaffold/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Content } from '@components/room/scaffold/index.style'; 3 | import useVoteSocket from '@hooks/socket/useVoteSocket'; 4 | import Modal from '@components/custom/Modal'; 5 | import TimerBomb from '@src/components/room/scaffold/TimerBomb'; 6 | import VotePresser from '@src/components/room/scaffold/VotePresser'; 7 | import Voters from '@src/components/room/scaffold/Voters'; 8 | import type { ScaffoldPropType } from '@ts-types/components/room'; 9 | 10 | const Scaffold: React.FC = ({ startVoteRef }): React.ReactElement => { 11 | const [isVote, setIsVote] = useState(false); 12 | const { isOpen, target, total, approves, rejects, startVoting, makeDecision } = useVoteSocket(); 13 | 14 | useEffect(() => { 15 | if (!isOpen) setIsVote(false); 16 | }, [isOpen]); 17 | 18 | useEffect(() => { 19 | startVoteRef.current = startVoting; 20 | }, []); 21 | 22 | const sendDecision = (isApprove) => () => { 23 | makeDecision({ isApprove }); 24 | setIsVote(true); 25 | }; 26 | 27 | const position = isVote ? '30px' : '50%'; 28 | return ( 29 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | }; 43 | 44 | export default Scaffold; 45 | -------------------------------------------------------------------------------- /client/src/components/setting/DeviceToggles.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { RootState } from '@src/store'; 4 | import { setVideoPower, setAudioPower, setSpeakerPower } from '@store/device'; 5 | import SettingToggle from '@components/setting/SettingToggle'; 6 | import { VideoIcon, MicIcon, SpeakerIcon } from '@components/icons'; 7 | 8 | const DeviceToggles: React.FC = (): React.ReactElement => { 9 | const dispatch = useDispatch(); 10 | const isVideoOn = useSelector((state: RootState) => state.device.isVideoOn); 11 | const isAudioOn = useSelector((state: RootState) => state.device.isAudioOn); 12 | const isSpeakerOn = useSelector((state: RootState) => state.device.isSpeakerOn); 13 | 14 | const toggleVideoPower = () => { 15 | dispatch(setVideoPower({ isVideoOn: !isVideoOn })); 16 | }; 17 | const toggleAudioPower = () => { 18 | dispatch(setAudioPower({ isAudioOn: !isAudioOn })); 19 | }; 20 | const toggleSpeakerPower = () => { 21 | dispatch(setSpeakerPower({ isSpeakerOn: !isSpeakerOn })); 22 | }; 23 | 24 | return ( 25 | <> 26 | 27 | 28 | 33 | 34 | ); 35 | }; 36 | 37 | export default DeviceToggles; 38 | -------------------------------------------------------------------------------- /client/src/components/setting/SettingDropdown.style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR, BTN_STYLE } from '@constant/style'; 3 | 4 | export const MenuButton = styled.div` 5 | ${BTN_STYLE} 6 | max-width: 340px; 7 | width: 100%; 8 | height: 50px; 9 | 10 | display: flex; 11 | justify-content: space-between; 12 | align-items: center; 13 | 14 | font-size: 19px; 15 | font-weight: 500px; 16 | border-radius: 100px; 17 | 18 | padding: 30px; 19 | color: ${COLOR.white}; 20 | 21 | & > span { 22 | overflow: hidden; 23 | text-overflow: ellipsis; 24 | white-space: nowrap; 25 | } 26 | `; 27 | 28 | export const MenuItem = styled.li` 29 | width: 100%; 30 | min-height: 50px; 31 | display: flex; 32 | align-items: center; 33 | 34 | list-style: none; 35 | padding: 12px 23px; 36 | color: ${COLOR.titleActive}; 37 | word-break: break-all; 38 | 39 | &:hover { 40 | background: ${COLOR.titleActive}; 41 | color: ${COLOR.background}; 42 | } 43 | `; 44 | -------------------------------------------------------------------------------- /client/src/components/setting/SettingDropdown.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Dropdown from '@components/custom/Dropdown'; 3 | import { DownIcon } from '@components/icons'; 4 | import { MenuButton, MenuItem } from '@components/setting/SettingDropdown.style'; 5 | import { filterLabel } from '@utils/regExpr'; 6 | import type { SettingDropdownPropType } from '@ts-types/components/setting'; 7 | 8 | const SettingDropdown: React.FC = ({ 9 | menuList, 10 | selected, 11 | setSelected, 12 | }): React.ReactElement => { 13 | const choiceMenu = (toggleDropdown, item) => () => { 14 | setSelected(item); 15 | toggleDropdown(); 16 | }; 17 | 18 | return ( 19 | ( 21 | 22 | {filterLabel(selected?.label)} 23 | 24 | 25 | )} 26 | renderItem={({ closeDropdown, item }) => ( 27 | 28 | {filterLabel(item?.label)} 29 | 30 | )} 31 | itemList={menuList} 32 | /> 33 | ); 34 | }; 35 | 36 | export default SettingDropdown; 37 | -------------------------------------------------------------------------------- /client/src/components/setting/SettingToggle.style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const ToggleButton = styled.div` 4 | width: 45px; 5 | height: 45px; 6 | 7 | background-color: none; 8 | background: none; 9 | border: none; 10 | 11 | display: flex; 12 | justify-content: center; 13 | align-items: center; 14 | 15 | & > svg { 16 | position: absolute; 17 | cursor: pointer; 18 | &:hover { 19 | padding: 2px; 20 | } 21 | } 22 | `; 23 | -------------------------------------------------------------------------------- /client/src/components/setting/SettingToggle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { XIcon } from '@components/icons'; 3 | import { ToggleButton } from '@components/setting/SettingToggle.style'; 4 | import type { SettingTogglePropType } from '@ts-types/components/setting'; 5 | 6 | const SettingToggle: React.FC = ({ 7 | Icon, 8 | isDeviceOn, 9 | setIsDeviceOn, 10 | }): React.ReactElement => { 11 | return ( 12 | setIsDeviceOn((prev) => !prev)}> 13 | 14 | {!isDeviceOn && } 15 | 16 | ); 17 | }; 18 | 19 | export default SettingToggle; 20 | -------------------------------------------------------------------------------- /client/src/components/user-information/UserHeader.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | HeaderBox, 3 | Title, 4 | SecondHeaderBox, 5 | MenuList, 6 | MenuItem, 7 | } from '@components/user-information/UserHeader.style'; 8 | import { useHistory } from 'react-router-dom'; 9 | 10 | const LISTED_MENU = ['information', 'friendList', 'ranking']; 11 | const MENU_NAME = { 12 | information: '내 정보', 13 | friendList: '친구 목록', 14 | ranking: '랭킹', 15 | }; 16 | 17 | const UserHeader: React.FC = ({ menu, defineMenu }): React.ReactElement => { 18 | const history = useHistory(); 19 | 20 | const goBack = () => { 21 | history.push('/'); 22 | }; 23 | 24 | return ( 25 | <> 26 | 27 | 뒤로가기 28 | 마이 페이지 29 | 30 | 31 | 32 | {LISTED_MENU.map((menuType) => ( 33 | 38 | {MENU_NAME[menuType]} 39 | 40 | ))} 41 | 42 | 43 | 44 | ); 45 | }; 46 | 47 | export default UserHeader; 48 | -------------------------------------------------------------------------------- /client/src/components/user-information/friend-list/FriendItem.style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR, BOX_SHADOW } from '@constant/style'; 3 | 4 | export const FriendItemBox = styled.li` 5 | ${BOX_SHADOW}; 6 | height: 60px; 7 | padding: 0 15px; 8 | margin: 15px; 9 | 10 | border: 1px solid ${COLOR.primary1}; 11 | background-color: ${COLOR.white}; 12 | 13 | display: flex; 14 | justify-content: space-between; 15 | align-items: center; 16 | `; 17 | 18 | export const LeftBox = styled.div` 19 | flex: 1; 20 | overflow: hidden; 21 | display: flex; 22 | align-items: center; 23 | 24 | & > img { 25 | flex: 0 0 auto; 26 | width: 32px; 27 | height: 32px; 28 | border-radius: 3px; 29 | } 30 | & > p { 31 | flex: 1; 32 | margin: 16px 5px 16px 16px; 33 | overflow: hidden; 34 | text-overflow: ellipsis; 35 | white-space: nowrap; 36 | line-height: 20px; 37 | } 38 | `; 39 | 40 | export const RightBox = styled.div` 41 | flex: 0 0 auto; 42 | `; 43 | -------------------------------------------------------------------------------- /client/src/components/user-information/friend-list/FriendItem.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | FriendItemBox, 3 | LeftBox, 4 | RightBox, 5 | } from '@components/user-information/friend-list/FriendItem.style'; 6 | import type { FriendType } from '@ts-types/components/user-information'; 7 | 8 | export const FriendItem: React.FC = ({ 9 | imgUrl, 10 | nickname, 11 | children, 12 | }): React.ReactElement => { 13 | return ( 14 | 15 | 16 | 프로필사진 17 |

{nickname}

18 |
19 | {children} 20 |
21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /client/src/components/user-information/friend-list/index.style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const FriendsContainer = styled.ul` 4 | margin: 0; 5 | padding: 0; 6 | display: grid; 7 | grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); 8 | grid-auto-rows: auto; 9 | gap: 10px; 10 | `; 11 | -------------------------------------------------------------------------------- /client/src/components/user-information/friend-list/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FriendsContainer } from '@components/user-information/friend-list/index.style'; 3 | 4 | import FriendRequestModal from '@components/user-information/modals/FriendRequestModal'; 5 | import FriendInfoModal from '@components/user-information/modals/FriendInfoModal'; 6 | import { FriendItem } from '@components/user-information/friend-list/FriendItem'; 7 | 8 | import { useSelector } from 'react-redux'; 9 | import { RootState } from '@store/index'; 10 | 11 | const FriendList: React.FC = (): React.ReactElement => { 12 | const friendList = useSelector((state: RootState) => state.friend.friendList); 13 | 14 | return ( 15 | <> 16 | 17 | {friendList.map(({ _id: id, imgUrl, nickname }) => ( 18 | 19 | 20 | 21 | ))} 22 | 23 | 24 | 25 | ); 26 | }; 27 | 28 | export default FriendList; 29 | -------------------------------------------------------------------------------- /client/src/components/user-information/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Information from '@components/user-information/information'; 3 | import FriendList from '@components/user-information/friend-list'; 4 | import Ranks from '@components/user-information/ranks'; 5 | import { useSelector } from 'react-redux'; 6 | import { RootState } from '@src/store'; 7 | import type { MenuPropType } from '@ts-types/components/user-information'; 8 | 9 | const UserInformation: React.FunctionComponent = ({ menu }): React.ReactElement => { 10 | const { id, imgUrl, nickname } = useSelector((state: RootState) => state.user); 11 | 12 | switch (menu) { 13 | case 'information': 14 | return ; 15 | case 'friendList': 16 | return ; 17 | case 'ranking': 18 | return ; 19 | } 20 | return <>; 21 | }; 22 | 23 | export default UserInformation; 24 | -------------------------------------------------------------------------------- /client/src/components/user-information/information/UserData.style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR, BOX_SHADOW } from '@constant/style'; 3 | 4 | export const DataTable = styled.table` 5 | ${BOX_SHADOW} 6 | width: 410px; 7 | margin: 50px auto 0 auto; 8 | 9 | display: flex; 10 | flex-direction: column; 11 | align-items: center; 12 | 13 | background-color: ${COLOR.white}; 14 | border: 1px solid ${COLOR.line}; 15 | border-radius: 10px; 16 | border-spacing: 0; 17 | border-collapse: collapse; 18 | `; 19 | 20 | export const DataRow = styled.tr` 21 | border-bottom: 1px solid ${COLOR.line}; 22 | &:last-child { 23 | border-bottom: none; 24 | } 25 | `; 26 | 27 | export const Title = styled.td` 28 | width: 150px; 29 | padding: 15px; 30 | border-right: 1px solid ${COLOR.line}; 31 | text-align: center; 32 | user-select: none; 33 | `; 34 | export const Data = styled.p` 35 | width: 260px; 36 | padding: 10px; 37 | margin: 0; 38 | text-align: center; 39 | word-break: break-all; 40 | white-space: wrap; 41 | font-weight: bold; 42 | color: ${COLOR.titleActive}; 43 | -webkit-user-drag: none; 44 | `; 45 | -------------------------------------------------------------------------------- /client/src/components/user-information/information/UserData.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | DataTable, 4 | DataRow, 5 | Title, 6 | Data, 7 | } from '@components/user-information/information/UserData.style'; 8 | import { filterDate } from '@utils/date'; 9 | 10 | const UNITS = { 11 | createdAt: (value) => ['가입 일자', `${filterDate(value)}`], 12 | chatCount: (value) => ['총 채팅 횟수', `${value}번`], 13 | hookCount: (value) => ['갈고리 사용 횟수', `${value}번`], 14 | pollCount: (value) => ['투표 선정 횟수', `${value}번`], 15 | closeupCount: (value) => ['클로즈업 횟수', `${value}번`], 16 | dieCount: (value) => ['단두대 횟수', `${value}회`], 17 | cheersCount: (value) => ['건배 횟수', `${value}회`], 18 | starterCount: (value) => ['게임 주최 횟수', `${value}번`], 19 | totalSeconds: (value) => ['총 접속 시간', `${value}초`], 20 | }; 21 | 22 | const UserData = ({ userInformation }): React.ReactElement => { 23 | return ( 24 | 25 | 26 | {Object.entries(userInformation).map(([key, value], index) => { 27 | const [title, amount] = UNITS[key](value); 28 | return ( 29 | 30 | {title} 31 | 32 | {amount} 33 | 34 | 35 | ); 36 | })} 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default UserData; 43 | -------------------------------------------------------------------------------- /client/src/components/user-information/information/UserProfile.style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR } from '@constant/style'; 3 | 4 | export const ProfileBox = styled.div` 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | `; 9 | 10 | export const ImageBox = styled.div` 11 | width: 175px; 12 | height: 175px; 13 | padding: 15px; 14 | 15 | background-color: ${COLOR.white}; 16 | 17 | border: 1px solid #d7d7d7; 18 | box-sizing: border-box; 19 | border-radius: 10px; 20 | 21 | img { 22 | width: 144px; 23 | height: 144px; 24 | -webkit-user-drag: none; 25 | border-radius: 5px; 26 | } 27 | `; 28 | 29 | export const Contents = styled.div` 30 | display: flex; 31 | flex-direction: column; 32 | margin-left: 40px; 33 | `; 34 | 35 | export const Nickname = styled.div` 36 | width: 200px; 37 | margin: 10px 0 20px 0; 38 | padding-bottom: 10px; 39 | color: ${COLOR.body}; 40 | font-size: 25px; 41 | 42 | overflow: hidden; 43 | text-overflow: ellipsis; 44 | white-space: nowrap; 45 | user-select: none; 46 | -webkit-user-drag: none; 47 | 48 | border-bottom: 1px solid ${COLOR.line}; 49 | `; 50 | 51 | export const ButtonsContainer = styled.div` 52 | width: 110px; 53 | display: flex; 54 | justify-content: space-between; 55 | align-items: center; 56 | `; 57 | -------------------------------------------------------------------------------- /client/src/components/user-information/information/UserProfile.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { RootState } from '@src/store'; 4 | import { 5 | ProfileBox, 6 | ImageBox, 7 | Contents, 8 | Nickname, 9 | ButtonsContainer, 10 | } from '@components/user-information/information/UserProfile.style'; 11 | import NickLogModal from '@components/user-information/modals/NickLogModal'; 12 | import NickChangeModal from '@components/user-information/modals/NickChangeModal'; 13 | import FriendDeleteModal from '@components/user-information/modals/FriendDeleteModal'; 14 | import type { UserProfilePropType } from '@ts-types/components/user-information'; 15 | 16 | const UserProfile: React.FC = ({ 17 | id, 18 | imgUrl, 19 | nickname, 20 | nicknameLog, 21 | }): React.ReactElement => { 22 | const myID = useSelector((state: RootState) => state.user.id); 23 | 24 | return ( 25 | 26 | 27 | 프로필 28 | 29 | 30 | {nickname} 31 | 32 | 33 | {id === myID && } 34 | {id !== myID && } 35 | 36 | 37 | 38 | ); 39 | }; 40 | 41 | export default UserProfile; 42 | -------------------------------------------------------------------------------- /client/src/components/user-information/information/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { API } from '@src/api'; 3 | import UserProfile from '@components/user-information/information/UserProfile'; 4 | import UserData from '@components/user-information/information/UserData'; 5 | import Loading from '@components/custom/Loading'; 6 | import type { NicknameLogType, InformationPropType } from '@ts-types/components/user-information'; 7 | 8 | const Information: React.FC = ({ 9 | id, 10 | imgUrl, 11 | nickname, 12 | }): React.ReactElement => { 13 | const [isLoading, setIsLoading] = useState(true); 14 | const [userInformation, setUserInformation] = useState({}); 15 | const [nicknameLog, setNicknameLog] = useState([]); 16 | 17 | useEffect(() => { 18 | const requestGetUserInformation = async () => { 19 | const result = await API.call(API.TYPE.GET_USER_INFORMATION, id); 20 | if (!result) return; 21 | const { information, nicknameLog } = result; 22 | setUserInformation(information); 23 | setNicknameLog(nicknameLog); 24 | setIsLoading(false); 25 | }; 26 | requestGetUserInformation(); 27 | }, []); 28 | 29 | if (isLoading) return ; 30 | return ( 31 | <> 32 | 33 | 34 | 35 | ); 36 | }; 37 | 38 | export default Information; 39 | -------------------------------------------------------------------------------- /client/src/components/user-information/modals/FriendDeleteModal.style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR } from '@constant/style'; 3 | 4 | export const DeleteFriendPressSection = styled.div` 5 | padding: 10px; 6 | 7 | display: flex; 8 | justify-content: center; 9 | margin: 0; 10 | align-items: center; 11 | & > div { 12 | margin: 0 10px; 13 | } 14 | `; 15 | 16 | export const DeleteIconWrapper = styled.div` 17 | cursor: pointer; 18 | path:first-child:hover { 19 | fill: ${COLOR.error2}; 20 | } 21 | 22 | path:first-child:active { 23 | fill: ${COLOR.error3}; 24 | } 25 | `; 26 | 27 | export const CancelIconWrapper = styled.div` 28 | cursor: pointer; 29 | path:first-child:hover { 30 | fill: ${COLOR.primary3}; 31 | } 32 | 33 | path:first-child:active { 34 | fill: 10px solid ${COLOR.titleActive}; 35 | } 36 | `; 37 | 38 | export const ModalContents = styled.div` 39 | display: flex; 40 | flex-direction: column; 41 | justify-content: center; 42 | align-items: center; 43 | padding: 40px; 44 | `; 45 | -------------------------------------------------------------------------------- /client/src/components/user-information/modals/FriendInfoModal.style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR, BTN_STYLE, BOX_SHADOW } from '@constant/style'; 3 | 4 | export const HomeButton = styled.div` 5 | ${BTN_STYLE} 6 | ${BOX_SHADOW} 7 | width: 28px; 8 | height: 28px; 9 | 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | `; 14 | 15 | export const ModalContents = styled.div` 16 | width: 600px; 17 | max-height: 95vh; 18 | padding: 50px 30px; 19 | 20 | overflow-x: hidden; 21 | overflow-y: scroll; 22 | 23 | &::-webkit-scrollbar { 24 | width: 4px; 25 | height: 16px; 26 | border-radius: 10px; 27 | background: transparent; 28 | } 29 | &::-webkit-scrollbar-thumb { 30 | background-color: ${COLOR.primary3}; 31 | border-radius: 10px; 32 | } 33 | `; 34 | -------------------------------------------------------------------------------- /client/src/components/user-information/modals/FriendInfoModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { 3 | HomeButton, 4 | ModalContents, 5 | } from '@components/user-information/modals/FriendInfoModal.style'; 6 | import { CloseBox } from '@components/user-information/modals/index.style'; 7 | import Modal from '@components/custom/Modal'; 8 | import Information from '@components/user-information/information/'; 9 | import { HomeIcon, GreenXButtonIcon } from '@components/icons'; 10 | import type { FriendInfoModalPropType } from '@ts-types/components/user-information'; 11 | 12 | const FriendInfoModal: React.FC = ({ 13 | id, 14 | nickname, 15 | imgUrl, 16 | }): React.ReactElement => { 17 | const [isOpen, setIsOpen] = useState(false); 18 | const openModal = () => setIsOpen(true); 19 | const closeModal = () => setIsOpen(false); 20 | 21 | return ( 22 | <> 23 | 24 | 25 | 26 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | ); 41 | }; 42 | 43 | export default FriendInfoModal; 44 | -------------------------------------------------------------------------------- /client/src/components/user-information/modals/FriendRequestModal.style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR, BTN_STYLE, CANCEL_BTN_STYLE, BOX_SHADOW } from '@constant/style'; 3 | 4 | export const OpenListButton = styled.button` 5 | position: absolute; 6 | right: 2rem; 7 | bottom: 2rem; 8 | width: 129px; 9 | height: 129px; 10 | 11 | border: none; 12 | outline: none; 13 | background-color: transparent; 14 | background-image: url('/images/requestFriend.png'); 15 | 16 | &:hover { 17 | cursor: pointer; 18 | padding: 2px; 19 | right: 2.1rem; 20 | bottom: 2.1rem; 21 | } 22 | `; 23 | 24 | export const FriendList = styled.ul` 25 | width: 640px; 26 | margin: 0 0 40px 0; 27 | padding: 0; 28 | 29 | & > li { 30 | min-width: 250px; 31 | width: 250px; 32 | } 33 | 34 | display: flex; 35 | align-items: center; 36 | 37 | overflow-x: scroll; 38 | overflow-y: hidden; 39 | 40 | &::-webkit-scrollbar { 41 | height: 5px; 42 | border-radius: 10px; 43 | background: transparent; 44 | } 45 | &::-webkit-scrollbar-thumb { 46 | background-color: ${COLOR.primary3}; 47 | border-radius: 10px; 48 | } 49 | `; 50 | 51 | const buttonMaker = (buttonStyle) => styled.button` 52 | ${buttonStyle} 53 | ${BOX_SHADOW} 54 | min-width: 30px; 55 | padding: 0 5px; 56 | margin: 0 3px; 57 | border-radius: 100px; 58 | `; 59 | export const AcceptButton = buttonMaker(BTN_STYLE); 60 | export const DenyButton = buttonMaker(CANCEL_BTN_STYLE); 61 | -------------------------------------------------------------------------------- /client/src/components/user-information/modals/NickLogModal.style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR } from '@constant/style'; 3 | 4 | export const ModalContents = styled.div` 5 | width: 500px; 6 | height: 300px; 7 | padding: 20px; 8 | margin-left: 80px; 9 | margin-right: 80px; 10 | margin-bottom: 40px; 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: flex-start; 14 | align-items: center; 15 | 16 | overflow-x: hidden; 17 | overflow-y: scroll; 18 | 19 | &::-webkit-scrollbar { 20 | width: 4px; 21 | height: 16px; 22 | border-radius: 10px; 23 | background: ${COLOR.line}; 24 | } 25 | &::-webkit-scrollbar-thumb { 26 | background-color: ${COLOR.primary3}; 27 | border-radius: 10px; 28 | } 29 | `; 30 | 31 | export const LogData = styled.p` 32 | font-size: 20px; 33 | margin: 0 0 20px 0; 34 | &:last-child { 35 | margin: 0; 36 | } 37 | `; 38 | -------------------------------------------------------------------------------- /client/src/components/user-information/modals/NickLogModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Modal from '@components/custom/Modal'; 3 | import { ModalContents, LogData } from '@components/user-information/modals/NickLogModal.style'; 4 | import { Header, Button, CloseBox } from '@components/user-information/modals/index.style'; 5 | import { HistoryIcon, GreenXButtonIcon } from '@components/icons'; 6 | import type { NickLogModalPropType } from '@ts-types/components/user-information'; 7 | 8 | const NickLogModal: React.FC = ({ 9 | nickname, 10 | nicknameLog, 11 | }): React.ReactElement => { 12 | const [isOpen, setIsOpen] = useState(false); 13 | return ( 14 | <> 15 | 18 | 24 |
25 | {nickname} 님의 닉네임 변경 내역 26 |
27 | 28 | {nicknameLog.map(({ nickname: prevNickname }, index) => ( 29 | {prevNickname} 30 | ))} 31 | 32 | setIsOpen(false)}> 33 | 34 | 35 |
36 | 37 | ); 38 | }; 39 | 40 | export default NickLogModal; 41 | -------------------------------------------------------------------------------- /client/src/components/user-information/modals/index.style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR } from '@constant/style'; 3 | 4 | export const Header = styled.h2` 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | 9 | padding: 0; 10 | margin: 30px 0 40px 0; 11 | color: ${COLOR.titleActive}; 12 | user-select: none; 13 | overflow: hidden; 14 | text-overflow: ellipsis; 15 | white-space: nowrap; 16 | font-size: 28px; 17 | overflow-x: hidden; 18 | overflow-y: hidden; 19 | 20 | span { 21 | color: ${COLOR.point}; 22 | overflow: hidden; 23 | text-overflow: ellipsis; 24 | white-space: nowrap; 25 | user-select: none; 26 | -webkit-user-drag: none; 27 | } 28 | `; 29 | 30 | export const Button = styled.button` 31 | padding: 0; 32 | margin: 0; 33 | 34 | border: none; 35 | cursor: pointer; 36 | background-color: transparent; 37 | 38 | &:hover { 39 | & > svg { 40 | padding: 2px; 41 | } 42 | } 43 | & > svg { 44 | pointer-events: none; 45 | } 46 | `; 47 | 48 | export const CloseBox = styled.div` 49 | position: absolute; 50 | right: 20px; 51 | top: 20px; 52 | cursor: pointer; 53 | `; 54 | -------------------------------------------------------------------------------- /client/src/components/user-information/ranks/RankingBox.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | RankData, 4 | PersonalRankBox, 5 | RankNum, 6 | RankTitle, 7 | Container, 8 | } from '@components/user-information/ranks/RankingBox.style'; 9 | import { useSelector } from 'react-redux'; 10 | import { RootState } from '@src/store'; 11 | import type { RankingBoxPropType } from '@ts-types/components/user-information'; 12 | 13 | const RankingBox: React.FC = ({ 14 | title, 15 | rank, 16 | nowSelect, 17 | filterList, 18 | }): React.ReactElement => { 19 | const myId = useSelector((state: RootState) => state.user.id); 20 | const rankList = 21 | filterList.length === rank.length 22 | ? rank 23 | : rank.filter(({ _id }) => [...filterList, myId].includes(_id)); 24 | 25 | return ( 26 | 27 | {title} 28 | 29 | {Object.values(rankList).map((userInfo: any, index) => ( 30 | 31 |
32 | {index + 1} 33 | 프로필 34 |
35 | {userInfo.nickname} 36 |
37 |
38 |
{userInfo[nowSelect]}
39 |
40 | ))} 41 |
42 |
43 | ); 44 | }; 45 | 46 | export default RankingBox; 47 | -------------------------------------------------------------------------------- /client/src/components/user-information/ranks/index.style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR } from '@constant/style'; 3 | 4 | export const HeaderContainer = styled.div` 5 | width: 100%; 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | justify-content: center; 10 | img { 11 | flex: 0 0 auto; 12 | width: 90px; 13 | margin-bottom: 2.5rem; 14 | } 15 | 16 | .announce p { 17 | font-weight: 600; 18 | 19 | span { 20 | font-weight: 600; 21 | color: ${COLOR.error}; 22 | } 23 | } 24 | `; 25 | 26 | export const RankContainer = styled.div` 27 | display: flex; 28 | width: 100%; 29 | height: 50%; 30 | justify-content: center; 31 | margin: 2rem 0; 32 | `; 33 | -------------------------------------------------------------------------------- /client/src/constant/envs.js: -------------------------------------------------------------------------------- 1 | export const DEPLOYMENT = process.env.REACT_APP_DEPLOYMENT; 2 | 3 | const getBackBaseUrl = () => { 4 | const _PORT = process.env.REACT_APP_BACK_PORT; 5 | const BACK_PORT = !_PORT ? '' : `:${_PORT}`; 6 | const BACK_HOST = process.env.REACT_APP_BACK_HOST || ''; 7 | const PROTOCOL = DEPLOYMENT === 'production' ? 'https' : 'http'; 8 | return `${PROTOCOL}://${BACK_HOST}${BACK_PORT}`; 9 | }; 10 | 11 | export const BACK_BASE_URL = getBackBaseUrl(); 12 | export const BACK_VERSION = process.env.REACT_APP_BACK_VERSION; 13 | export const GITHUB_ID = process.env.REACT_APP_GITHUB_ID; 14 | export const NAVER_ID = process.env.REACT_APP_NAVER_ID; 15 | export const NAVER_REDIRECT_URL = `${BACK_BASE_URL}/api/${BACK_VERSION}/auth/naver`; 16 | -------------------------------------------------------------------------------- /client/src/hooks/redux.ts: -------------------------------------------------------------------------------- 1 | type actionType = (payload: T) => { type: string; payload: T }; 2 | 3 | export function createAction(type: string): [string, actionType] { 4 | const action: actionType = (payload) => ({ type, payload }); 5 | return [type, action]; 6 | } 7 | -------------------------------------------------------------------------------- /client/src/hooks/socket/useAnimationSocket.ts: -------------------------------------------------------------------------------- 1 | import { useMemo, useCallback, useEffect } from 'react'; 2 | import Socket from '@socket/socket'; 3 | import { useDispatch } from 'react-redux'; 4 | import { setIsCheers, setCloseUpUser, resetRoomInfo } from '@store/room'; 5 | 6 | const useAnimationSocket = () => { 7 | const dispatch = useDispatch(); 8 | 9 | const updateCheers = useCallback((data) => { 10 | dispatch(setIsCheers(data)); 11 | }, []); 12 | 13 | const updateCloseUpUser = useCallback((data) => { 14 | dispatch(setCloseUpUser(data)); 15 | }, []); 16 | 17 | const socket = useMemo(() => Socket.animation({ updateCheers, updateCloseUpUser }), []); 18 | useEffect(() => { 19 | return () => { 20 | socket.disconnecting(); 21 | dispatch(resetRoomInfo({})); 22 | }; 23 | }, []); 24 | 25 | return socket; 26 | }; 27 | 28 | export default useAnimationSocket; 29 | -------------------------------------------------------------------------------- /client/src/hooks/socket/useChatSocket.ts: -------------------------------------------------------------------------------- 1 | import { useMemo, useCallback, useEffect } from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | import { addChatLog } from '@store/room'; 4 | import type { ChatLogType } from '@ts-types/store'; 5 | import Socket from '@socket/socket'; 6 | 7 | const useChatSocket = () => { 8 | const dispatch = useDispatch(); 9 | const addChat = useCallback((data: ChatLogType) => { 10 | dispatch(addChatLog(data)); 11 | }, []); 12 | 13 | const socket = useMemo(() => { 14 | return Socket.chat({ addChat }); 15 | }, []); 16 | useEffect(() => { 17 | return () => { 18 | socket.disconnecting(); 19 | }; 20 | }, []); 21 | return socket; 22 | }; 23 | 24 | export default useChatSocket; 25 | -------------------------------------------------------------------------------- /client/src/hooks/socket/useControlSocket.ts: -------------------------------------------------------------------------------- 1 | import { useMemo, useCallback, useEffect } from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | 4 | import { toggleIsOpen } from '@store/room'; 5 | import { setNoticeMessage } from '@store/notice'; 6 | 7 | import Socket from '@socket/socket'; 8 | 9 | const useControlSocket = () => { 10 | const dispatch = useDispatch(); 11 | 12 | const errorControl = useCallback((message) => { 13 | dispatch(setNoticeMessage({ errorMessage: message })); 14 | }, []); 15 | 16 | const changeIsOpen = useCallback(() => { 17 | dispatch(toggleIsOpen({})); 18 | }, []); 19 | 20 | const socket = useMemo( 21 | () => 22 | Socket.control({ 23 | errorControl, 24 | changeIsOpen, 25 | }), 26 | [], 27 | ); 28 | 29 | useEffect(() => { 30 | return () => { 31 | socket.disconnecting(); 32 | }; 33 | }, []); 34 | 35 | return socket; 36 | }; 37 | 38 | export default useControlSocket; 39 | -------------------------------------------------------------------------------- /client/src/hooks/socket/useFriendSocket.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo } from 'react'; 2 | import Socket from '@socket/socket'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import { RootState } from '@src/store'; 5 | import { sendFriendRequest, receiveFriendRequest } from '@store/friend'; 6 | import { API } from '@api/index'; 7 | 8 | const useFriendSocket = () => { 9 | const dispatch = useDispatch(); 10 | const { 11 | id: myId, 12 | imgUrl: myImgUrl, 13 | nickname: myNickname, 14 | } = useSelector((state: RootState) => state.user); 15 | 16 | const updateReceiveFriends = ({ id, imgUrl, nickname }) => { 17 | dispatch(receiveFriendRequest({ _id: id, imgUrl, nickname })); 18 | }; 19 | 20 | const onclickRequestFriend = async ({ sid, id, imgUrl, nickname }) => { 21 | const result = await API.call(API.TYPE.POST_FRIEND, id); 22 | if (!result) return; 23 | socket.sendFriendRequest({ sid, id: myId, imgUrl: myImgUrl, nickname: myNickname }); 24 | 25 | dispatch(sendFriendRequest({ _id: id, imgUrl, nickname })); 26 | }; 27 | 28 | const socket = useMemo(() => Socket.friend({ updateReceiveFriends }), []); 29 | useEffect(() => { 30 | return () => { 31 | socket.disconnecting(); 32 | }; 33 | }, []); 34 | 35 | return { onclickRequestFriend }; 36 | }; 37 | 38 | export default useFriendSocket; 39 | -------------------------------------------------------------------------------- /client/src/hooks/socket/useMarkSocket.ts: -------------------------------------------------------------------------------- 1 | import { useState, useMemo, useCallback, useEffect } from 'react'; 2 | import Socket from '@socket/socket'; 3 | 4 | export type MarkType = { 5 | [key: number]: { x: number; y: number }; 6 | }; 7 | 8 | const QUESTION_MARK_TIME = 1900; 9 | 10 | const useMarkSocket = () => { 11 | const [marks, setMarks] = useState({}); 12 | 13 | const removeQuestionMark = useCallback((id) => { 14 | setTimeout(() => { 15 | setMarks((prev) => { 16 | const newMarks = { ...prev }; 17 | delete newMarks[id]; 18 | return newMarks; 19 | }); 20 | }, QUESTION_MARK_TIME); 21 | }, []); 22 | 23 | const socket = useMemo(() => { 24 | return Socket.mark({ setMarks, removeQuestionMark }); 25 | }, []); 26 | useEffect(() => { 27 | return () => { 28 | socket.disconnecting(); 29 | }; 30 | }, []); 31 | return { marks, addQuestionMark: socket.addQuestionMark }; 32 | }; 33 | 34 | export default useMarkSocket; 35 | -------------------------------------------------------------------------------- /client/src/hooks/socket/useSignalSocket.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo } from 'react'; 2 | import Socket from '@socket/socket'; 3 | import { useSelector, useDispatch } from 'react-redux'; 4 | import { RootState } from '@src/store'; 5 | import { addStreams } from '@store/room'; 6 | 7 | const useSignalSocket = () => { 8 | const dispatch = useDispatch(); 9 | const stream = useSelector((state: RootState) => state.device.stream); 10 | 11 | const addStream = (sid) => (stream) => { 12 | dispatch(addStreams({ sid, stream })); 13 | }; 14 | 15 | const socket = useMemo(() => Socket.signal({ addStream, stream }), []); 16 | useEffect(() => { 17 | return () => { 18 | socket.disconnecting(); 19 | }; 20 | }, []); 21 | return socket; 22 | }; 23 | 24 | export default useSignalSocket; 25 | -------------------------------------------------------------------------------- /client/src/hooks/socket/useStreamSocket.ts: -------------------------------------------------------------------------------- 1 | import { useMemo, useCallback, useEffect } from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | import { setAudioPower, setVideoPower } from '@store/device'; 4 | import { updateDeviceVideo, updateDeviceAudio } from '@store/room'; 5 | import { setNoticeMessage } from '@store/notice'; 6 | import Socket from '@socket/socket'; 7 | 8 | const useStreamSocket = () => { 9 | const dispatch = useDispatch(); 10 | 11 | const errorControl = useCallback((message) => { 12 | dispatch(setNoticeMessage({ errorMessage: message })); 13 | }, []); 14 | 15 | const updateOtherVideo = useCallback((updateData) => { 16 | dispatch(updateDeviceVideo(updateData)); 17 | }, []); 18 | 19 | const updateMyVideo = useCallback((updateData) => { 20 | dispatch(setVideoPower(updateData)); 21 | }, []); 22 | 23 | const updateOtherAudio = useCallback((updateData) => { 24 | dispatch(updateDeviceAudio(updateData)); 25 | }, []); 26 | 27 | const updateMyAudio = useCallback((updateData) => { 28 | dispatch(setAudioPower(updateData)); 29 | }, []); 30 | 31 | const socket = useMemo( 32 | () => 33 | Socket.stream({ 34 | errorControl, 35 | updateOtherVideo, 36 | updateMyVideo, 37 | updateOtherAudio, 38 | updateMyAudio, 39 | }), 40 | [], 41 | ); 42 | useEffect(() => { 43 | return () => { 44 | socket.disconnecting(); 45 | }; 46 | }, []); 47 | 48 | return socket; 49 | }; 50 | 51 | export default useStreamSocket; 52 | -------------------------------------------------------------------------------- /client/src/hooks/socket/useTicketSocket.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo } from 'react'; 2 | import { useHistory, useParams } from 'react-router-dom'; 3 | import { useDispatch } from 'react-redux'; 4 | import { setNoticeMessage } from '@store/notice'; 5 | import Socket from '@socket/socket'; 6 | 7 | const useTicketSocket = () => { 8 | const history = useHistory(); 9 | const dispatch = useDispatch(); 10 | const { code } = useParams(); 11 | 12 | const abortEnter = ({ message }) => { 13 | dispatch(setNoticeMessage({ errorMessage: message || '방에 입장하지 못 했습니다.' })); 14 | history.replace('/'); 15 | }; 16 | 17 | const socket = useMemo(() => Socket.ticket({ abortEnter }), []); 18 | useEffect(() => { 19 | Socket.connect(); 20 | socket.requestValidation({ code }); 21 | 22 | return () => { 23 | socket.disconnecting(); 24 | Socket.disconnect(); 25 | }; 26 | }, []); 27 | 28 | return { successValidtaion: socket.successValidtaion }; 29 | }; 30 | 31 | export default useTicketSocket; 32 | -------------------------------------------------------------------------------- /client/src/hooks/useToggleSpeaker.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { RootState } from '@src/store'; 4 | 5 | const useToggleSpeaker = (elementRef) => { 6 | const isSpeakerOn = useSelector((state: RootState) => state.device.isSpeakerOn); 7 | 8 | useEffect(() => { 9 | const element = elementRef.current; 10 | const isMute = !(isSpeakerOn || false); 11 | if (!element) return; 12 | element.muted = isMute; 13 | }, [isSpeakerOn]); 14 | }; 15 | 16 | export default useToggleSpeaker; 17 | -------------------------------------------------------------------------------- /client/src/hooks/useUpdateSpeaker.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { RootState } from '@src/store'; 4 | 5 | const attachSinkId = (element, sinkId) => { 6 | if (!element) return console.error('No Element Exists'); 7 | if (typeof element.sinkId === 'undefined') 8 | return console.error('Browser does not support output device selection.'); 9 | 10 | element.setSinkId(sinkId).catch((error) => { 11 | let errorMessage = error; 12 | if (error.name === 'SecurityError') { 13 | errorMessage = `You need to use HTTPS for selecting audio output device: ${error}`; 14 | } 15 | console.error(errorMessage); 16 | }); 17 | }; 18 | 19 | const useUpdateSpeaker = (elementRef) => { 20 | const speakerInfo = useSelector((state: RootState) => state.device.speakerInfo); 21 | 22 | useEffect(() => { 23 | const element = elementRef.current; 24 | const sinkId = speakerInfo?.deviceId || ''; 25 | if (!element) return; 26 | attachSinkId(element, sinkId); 27 | }, [speakerInfo]); 28 | }; 29 | 30 | export default useUpdateSpeaker; 31 | -------------------------------------------------------------------------------- /client/src/hooks/useUpdateStream.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | const useUpdateStream = (elementRef, srcObject, callback = () => {}) => { 4 | useEffect(() => { 5 | const element = elementRef.current; 6 | if (!element) return; 7 | element.srcObject = srcObject ?? null; 8 | callback(); 9 | }, [srcObject]); 10 | }; 11 | 12 | export default useUpdateStream; 13 | -------------------------------------------------------------------------------- /client/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from '@src/App'; 4 | import 'dotenv/config'; 5 | // Redux 기본 Setting 6 | import { Provider } from 'react-redux'; 7 | import { store } from '@src/store/store'; 8 | 9 | ReactDOM.render( 10 | 11 | 12 | , 13 | document.getElementById('root'), 14 | ); 15 | -------------------------------------------------------------------------------- /client/src/pages/CreateRoom.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useHistory } from 'react-router-dom'; 3 | import { useDispatch } from 'react-redux'; 4 | import { setNoticeMessage } from '@store/notice'; 5 | import Socket from '@socket/socket'; 6 | import Loading from '@components/custom/Loading'; 7 | 8 | const CreateRoom: React.FC = (): React.ReactElement => { 9 | const dispatch = useDispatch(); 10 | const history = useHistory(); 11 | 12 | useEffect(() => { 13 | Socket.connect(); 14 | 15 | const waiting = setTimeout(() => { 16 | dispatch(setNoticeMessage({ errorMessage: '방을 생성하지 못 했습니다.' })); 17 | history.replace('/'); 18 | }, 5000); 19 | const joining = ({ roomCode }) => { 20 | clearTimeout(waiting); 21 | history.replace(`/room/${roomCode}`); 22 | }; 23 | 24 | const functions = Socket.create({ joining }); 25 | functions.createRoom(); 26 | 27 | return () => { 28 | functions?.disconnecting(); 29 | Socket.disconnect(); 30 | }; 31 | }, []); 32 | 33 | return ; 34 | }; 35 | 36 | export default CreateRoom; 37 | -------------------------------------------------------------------------------- /client/src/pages/JoinRoom.tsx: -------------------------------------------------------------------------------- 1 | import React, { lazy, useState, useEffect } from 'react'; 2 | import { useParams } from 'react-router-dom'; 3 | import { useSelector, useDispatch } from 'react-redux'; 4 | import { RootState } from '@src/store'; 5 | import { requestInitInfo } from '@store/device'; 6 | import { setRoomCode } from '@store/room'; 7 | import Setting from '@src/components/setting'; 8 | import Loading from '@components/custom/Loading'; 9 | const Room = lazy(() => import('@components/room/')); 10 | 11 | const JoinRoom: React.FC = (): React.ReactElement => { 12 | const dispatch = useDispatch(); 13 | const stream = useSelector((state: RootState) => state.device.stream); 14 | const isLoading = useSelector((state: RootState) => state.device.isLoading); 15 | const [isFirst, setIsFirst] = useState(true); 16 | const { code } = useParams(); 17 | 18 | useEffect(() => { 19 | dispatch(requestInitInfo({})); 20 | dispatch(setRoomCode(code)); 21 | }, []); 22 | 23 | useEffect(() => { 24 | return () => { 25 | stream?.getTracks().forEach((track) => { 26 | track.stop(); 27 | }); 28 | }; 29 | }, [stream]); 30 | 31 | const renderRoom = () => { 32 | setIsFirst(false); 33 | }; 34 | 35 | if (isLoading) return ; 36 | if (isFirst) return ; 37 | return ; 38 | }; 39 | 40 | export default JoinRoom; 41 | -------------------------------------------------------------------------------- /client/src/pages/Lobby.style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR, INPUT_STYLE, BTN_STYLE, BOX_SHADOW } from '@constant/style'; 3 | 4 | export const FullScreen = styled.div` 5 | width: 100%; 6 | height: 100%; 7 | 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: center; 11 | align-items: center; 12 | 13 | background-color: ${COLOR.background}; 14 | `; 15 | 16 | export const Title = styled.div` 17 | margin-bottom: 100px; 18 | font-size: 48px; 19 | font-weight: 700; 20 | color: ${COLOR.titleActive}; 21 | user-select: none; 22 | white-space: nowrap; 23 | 24 | & > span { 25 | color: ${COLOR.point}; 26 | } 27 | `; 28 | 29 | export const CodeInput = styled.input` 30 | &:focus { 31 | ${BOX_SHADOW} 32 | } 33 | ${INPUT_STYLE} 34 | width: 100%; 35 | max-width: 616px; 36 | height: 76px; 37 | padding: 0 30px; 38 | margin-bottom: 20px; 39 | display: flex; 40 | align-items: center; 41 | border-radius: 10px; 42 | font-weight: 500; 43 | font-size: 32px; 44 | text-align: center; 45 | `; 46 | 47 | export const BigButton = styled.button` 48 | ${BTN_STYLE} 49 | ${BOX_SHADOW} 50 | width: 100%; 51 | max-width: 469px; 52 | height: 76px; 53 | margin-top: 50px; 54 | 55 | display: flex; 56 | justify-content: center; 57 | align-items: center; 58 | 59 | font-size: 32px; 60 | font-weight: 500px; 61 | border-radius: 100px; 62 | `; 63 | -------------------------------------------------------------------------------- /client/src/pages/Lobby.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import { FullScreen, Title, CodeInput, BigButton } from '@pages/Lobby.style.js'; 3 | import { useHistory } from 'react-router-dom'; 4 | import { useSelector } from 'react-redux'; 5 | import { RootState } from '@src/store'; 6 | import Header from '@components/Header'; 7 | 8 | const Lobby: React.FC = (): React.ReactElement => { 9 | const history = useHistory(); 10 | const nickname = useSelector((state: RootState) => state.user.nickname); 11 | const chatRoomCodeInput = useRef(null); 12 | 13 | const joinChatRoom = () => { 14 | const roomCode = chatRoomCodeInput.current?.value; 15 | if (roomCode !== '') history.push(`room/${roomCode}`); 16 | }; 17 | 18 | const createChatRoom = () => { 19 | history.push('/create'); 20 | }; 21 | 22 | return ( 23 | 24 |
25 | 26 | 오늘도 적당히 음주하세요! 27 | <span> {nickname || 'Judangs'} </span> 28 | 님! 29 | 30 | 31 | 방 참가하기 32 | 방 생성하기 33 | 34 | ); 35 | }; 36 | 37 | export default Lobby; 38 | -------------------------------------------------------------------------------- /client/src/pages/Login.style.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | import { COLOR, BOX_SHADOW } from '@constant/style'; 3 | 4 | const flexColumnCenter = css` 5 | display: flex; 6 | flex-direction: column; 7 | justify-content: center; 8 | align-items: center; 9 | `; 10 | 11 | export const FullScreen = styled.div` 12 | ${flexColumnCenter} 13 | 14 | width: 100%; 15 | min-width: 400px; 16 | height: 100%; 17 | overflow: hidden; 18 | 19 | background-color: ${COLOR.background}; 20 | `; 21 | 22 | export const LogoBox = styled.div` 23 | ${flexColumnCenter} 24 | 25 | & > img { 26 | width: 250px; 27 | margin-bottom: 10px; 28 | -webkit-user-drag: none; 29 | } 30 | & > span { 31 | font-weight: 700; 32 | font-size: 48px; 33 | user-select: none; 34 | color: ${COLOR.titleActive}; 35 | } 36 | `; 37 | 38 | export const ButtonsDiv = styled.div` 39 | ${flexColumnCenter} 40 | margin: 50px 0; 41 | `; 42 | 43 | export const LoginLink = styled.a` 44 | width: 312px; 45 | height: 65px; 46 | padding: 0; 47 | margin-bottom: 30px; 48 | overflow: hidden; 49 | 50 | display: flex; 51 | align-items: center; 52 | 53 | outline: none; 54 | border: none; 55 | cursor: pointer; 56 | ${BOX_SHADOW} 57 | 58 | & > img { 59 | width: 100%; 60 | -webkit-user-drag: none; 61 | } 62 | `; 63 | 64 | export const Title = styled.div` 65 | text-align: center; 66 | 67 | font-size: 48px; 68 | font-weight: 700; 69 | color: ${COLOR.titleActive}; 70 | user-select: none; 71 | 72 | & > span { 73 | color: ${COLOR.point}; 74 | } 75 | `; 76 | -------------------------------------------------------------------------------- /client/src/pages/Login.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FullScreen, LogoBox, LoginLink, ButtonsDiv, Title } from '@pages/Login.style'; 3 | import { GITHUB_ID, NAVER_ID, NAVER_REDIRECT_URL } from '@constant/envs'; 4 | const githubUrl = `https://github.com/login/oauth/authorize?client_id=${GITHUB_ID}`; 5 | const naverUrl = `https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id=${NAVER_ID}&redirect_url=${NAVER_REDIRECT_URL}`; 6 | 7 | const Login: React.FC = (): React.ReactElement => { 8 | return ( 9 | 10 | 11 | logo 12 | Sooltreaming 13 | 14 | 15 | 16 | github login 17 | 18 | 19 | naver login 20 | 21 | 22 | 23 | 재ㅁㅣ있는 술자.ㄹㅣ;를 위한 24 | <span> 화상ㅊㅐ팅 </span> 25 | 애플ㄹ케.2션 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default Login; 32 | -------------------------------------------------------------------------------- /client/src/pages/UserPage.style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR } from '@constant/style'; 3 | 4 | export const FullScreen = styled.div` 5 | width: 100%; 6 | height: 100%; 7 | 8 | display: flex; 9 | flex-direction: column; 10 | 11 | background-color: ${COLOR.background}; 12 | `; 13 | 14 | export const Contents = styled.div` 15 | flex: 1; 16 | padding: 40px 20px; 17 | 18 | overflow-x: hidden; 19 | overflow-y: scroll; 20 | 21 | &::-webkit-scrollbar { 22 | width: 4px; 23 | height: 16px; 24 | border-radius: 10px; 25 | background: transparent; 26 | } 27 | &::-webkit-scrollbar-thumb { 28 | background-color: ${COLOR.primary3}; 29 | border-radius: 10px; 30 | } 31 | `; 32 | -------------------------------------------------------------------------------- /client/src/pages/UserPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback } from 'react'; 2 | import { FullScreen, Contents } from '@pages/UserPage.style'; 3 | import { useDispatch } from 'react-redux'; 4 | import { friendListRequest, sendFriendListRequest, receiveFriendListRequest } from '@store/friend'; 5 | import UserHeader from '@components/user-information/UserHeader'; 6 | import UserInformation from '@components/user-information'; 7 | 8 | const UserPage: React.FC = (): React.ReactElement => { 9 | const dispatch = useDispatch(); 10 | const [menu, setMenu] = useState('information'); 11 | 12 | const defineMenu = useCallback( 13 | (menuType) => () => { 14 | setMenu(menuType); 15 | }, 16 | [], 17 | ); 18 | 19 | useEffect(() => { 20 | dispatch(friendListRequest([])); 21 | dispatch(sendFriendListRequest([])); 22 | dispatch(receiveFriendListRequest([])); 23 | }, []); 24 | 25 | return ( 26 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default UserPage; 36 | -------------------------------------------------------------------------------- /client/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /client/src/sagas/index.ts: -------------------------------------------------------------------------------- 1 | import { all, call } from 'redux-saga/effects'; 2 | import user from '@sagas/user'; 3 | import device from '@sagas/device'; 4 | import friend from '@sagas/friend'; 5 | export default function* rootSaga() { 6 | yield all([call(user), call(device), call(friend)]); 7 | } 8 | -------------------------------------------------------------------------------- /client/src/sagas/user.ts: -------------------------------------------------------------------------------- 1 | import { call, put, all, fork, takeLatest } from 'redux-saga/effects'; 2 | import { 3 | USER_LOGIN_REQUEST, 4 | userLoginRequest, 5 | userLoginSuccess, 6 | userLoginFailure, 7 | } from '@store/user'; 8 | import { loginWithSession } from '@api/user'; 9 | import { UserStateType } from '@ts-types/store'; 10 | 11 | // 로그인 12 | async function UserLoginAPI({}: any) { 13 | const data = await loginWithSession(); 14 | return data; 15 | } 16 | function* LogInUser(action: ReturnType) { 17 | try { 18 | const result: UserStateType = yield call(UserLoginAPI, action.payload); 19 | yield put(userLoginSuccess(result)); 20 | } catch ({ message }) { 21 | yield put(userLoginFailure({ message: message as string })); 22 | } 23 | } 24 | function* watchLogInUser() { 25 | yield takeLatest(USER_LOGIN_REQUEST, LogInUser); 26 | } 27 | 28 | export default function* userSaga() { 29 | yield all([fork(watchLogInUser)]); 30 | } 31 | -------------------------------------------------------------------------------- /client/src/socket/animation.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from 'socket.io-client'; 2 | import { 3 | CHEERS_BROADCAST, 4 | CLOSEUP_ON, 5 | CLOSEUP_OFF, 6 | CLOSEUP_BREAK, 7 | } from 'sooltreaming-domain/constant/socketEvent'; 8 | 9 | const animation = (socket: Socket) => (closure: any) => { 10 | const { updateCheers, updateCloseUpUser } = closure; 11 | 12 | socket.on(CHEERS_BROADCAST, () => { 13 | updateCheers(true); 14 | setTimeout(() => { 15 | updateCheers(false); 16 | }, 5000); 17 | }); 18 | 19 | socket.on(CLOSEUP_ON, (sid) => { 20 | updateCloseUpUser(sid); 21 | }); 22 | 23 | socket.on(CLOSEUP_OFF, () => { 24 | updateCloseUpUser(''); 25 | }); 26 | 27 | socket.on(CLOSEUP_BREAK, (closeupUser) => { 28 | updateCloseUpUser(closeupUser); 29 | }); 30 | 31 | const activateCheers = (mydata) => { 32 | socket.emit(CHEERS_BROADCAST, mydata); 33 | }; 34 | 35 | const deactivateCloseup = () => { 36 | socket.emit(CLOSEUP_OFF); 37 | }; 38 | 39 | const activateCloseup = () => { 40 | socket.emit(CLOSEUP_ON); 41 | }; 42 | 43 | const disconnecting = () => { 44 | socket.off(CHEERS_BROADCAST); 45 | socket.off(CLOSEUP_ON); 46 | socket.off(CLOSEUP_OFF); 47 | }; 48 | 49 | return { activateCheers, activateCloseup, deactivateCloseup, disconnecting }; 50 | }; 51 | 52 | export default animation; 53 | -------------------------------------------------------------------------------- /client/src/socket/chat.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from 'socket.io-client'; 2 | import { CHAT_RECEIVE, CHAT_SENDING } from 'sooltreaming-domain/constant/socketEvent'; 3 | 4 | const chat = (socket: Socket) => (closure: any) => { 5 | const { addChat } = closure; 6 | 7 | socket.on(CHAT_RECEIVE, (chat) => { 8 | addChat(chat); 9 | }); 10 | 11 | const sendMessage = (myChat) => { 12 | socket.emit(CHAT_SENDING, myChat); 13 | }; 14 | const disconnecting = () => { 15 | socket.off(CHAT_RECEIVE); 16 | }; 17 | 18 | return { sendMessage, disconnecting }; 19 | }; 20 | 21 | export default chat; 22 | -------------------------------------------------------------------------------- /client/src/socket/control.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from 'socket.io-client'; 2 | import { 3 | CONTROL_AUTHORITY_ERROR, 4 | CONTROL_TOGGLE_ENTRY, 5 | CONTROL_OTHER_VIDEO_OFF, 6 | CONTROL_OTHER_AUDIO_OFF, 7 | } from 'sooltreaming-domain/constant/socketEvent'; 8 | 9 | const control = (socket: Socket) => (closure: any) => { 10 | const { errorControl, changeIsOpen } = closure; 11 | 12 | socket.on(CONTROL_AUTHORITY_ERROR, (message) => { 13 | errorControl(message); 14 | }); 15 | 16 | socket.on(CONTROL_TOGGLE_ENTRY, (result) => { 17 | if (!result) return; 18 | changeIsOpen(); 19 | }); 20 | 21 | const toggleRoomEntry = () => { 22 | socket.emit(CONTROL_TOGGLE_ENTRY); 23 | }; 24 | 25 | const turnOffOtherVideo = ({ sid, isVideoOn }) => { 26 | socket.emit(CONTROL_OTHER_VIDEO_OFF, { sid, isVideoOn }); 27 | }; 28 | 29 | const turnOffOtherAudio = ({ sid, isAudioOn }) => { 30 | socket.emit(CONTROL_OTHER_AUDIO_OFF, { sid, isAudioOn }); 31 | }; 32 | 33 | const disconnecting = () => { 34 | socket.off(CONTROL_TOGGLE_ENTRY); 35 | }; 36 | 37 | return { toggleRoomEntry, turnOffOtherVideo, turnOffOtherAudio, disconnecting }; 38 | }; 39 | 40 | export default control; 41 | -------------------------------------------------------------------------------- /client/src/socket/create.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from 'socket.io-client'; 2 | import { CREATE_REQUEST, CREATE_SUCCESS } from 'sooltreaming-domain/constant/socketEvent'; 3 | 4 | const create = (socket: Socket) => (closure: any) => { 5 | const { joining } = closure; 6 | 7 | socket.on(CREATE_SUCCESS, joining); 8 | 9 | const createRoom = () => socket.emit(CREATE_REQUEST); 10 | 11 | const disconnecting = () => { 12 | socket.off(CREATE_SUCCESS); 13 | }; 14 | 15 | return { createRoom, disconnecting }; 16 | }; 17 | 18 | export default create; 19 | -------------------------------------------------------------------------------- /client/src/socket/enter.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from 'socket.io-client'; 2 | import { 3 | ENTER_ROOM, 4 | ENTER_ROOM_ERROR, 5 | ENTER_ALL_USER, 6 | ENTER_ONE_USER, 7 | DISCONNECT_USER, 8 | ENTER_CHANGE_HOST, 9 | } from 'sooltreaming-domain/constant/socketEvent'; 10 | 11 | const enter = (socket: Socket) => (closure: any) => { 12 | const { errorControl, addUser, deleteUser, initUsers, changeRoomHost, updateFriendList } = 13 | closure; 14 | 15 | socket.on(ENTER_ALL_USER, (allUsers, allUsersDevices) => { 16 | initUsers({ users: { ...allUsers }, usersDevices: { ...allUsersDevices } }); 17 | }); 18 | socket.on(ENTER_ONE_USER, (user, userDevices, sid) => { 19 | addUser({ user, userDevices, sid }); 20 | updateFriendList(); 21 | }); 22 | socket.on(DISCONNECT_USER, (id) => { 23 | if (socket.id === id) return; 24 | deleteUser(id); 25 | }); 26 | socket.on(ENTER_CHANGE_HOST, (isOpen) => { 27 | changeRoomHost(isOpen); 28 | }); 29 | 30 | socket.on(ENTER_ROOM_ERROR, (errorMessage) => { 31 | errorControl(errorMessage); 32 | }); 33 | 34 | const joinRoom = (myData) => socket.emit(ENTER_ROOM, myData); 35 | 36 | const disconnecting = () => { 37 | socket.off(ENTER_ROOM_ERROR); 38 | socket.off(ENTER_ALL_USER); 39 | socket.off(ENTER_ONE_USER); 40 | socket.off(DISCONNECT_USER); 41 | socket.off(ENTER_CHANGE_HOST); 42 | }; 43 | 44 | return { joinRoom, disconnecting }; 45 | }; 46 | 47 | export default enter; 48 | -------------------------------------------------------------------------------- /client/src/socket/friend.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from 'socket.io-client'; 2 | import { FRIEND_REQUEST } from 'sooltreaming-domain/constant/socketEvent'; 3 | 4 | const friend = (socket: Socket) => (closure: any) => { 5 | const { updateReceiveFriends } = closure; 6 | 7 | socket.on(FRIEND_REQUEST, (data) => { 8 | updateReceiveFriends(data); 9 | }); 10 | 11 | const sendFriendRequest = (data) => { 12 | socket.emit(FRIEND_REQUEST, data); 13 | }; 14 | 15 | const disconnecting = () => { 16 | socket.off(FRIEND_REQUEST); 17 | }; 18 | 19 | return { sendFriendRequest, disconnecting }; 20 | }; 21 | 22 | export default friend; 23 | -------------------------------------------------------------------------------- /client/src/socket/mark.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from 'socket.io-client'; 2 | import { MARK_BROADCAST } from 'sooltreaming-domain/constant/socketEvent'; 3 | 4 | const mark = (socket: Socket) => (closure: any) => { 5 | const { setMarks, removeQuestionMark } = closure; 6 | let count = 0; 7 | let marks = {}; 8 | let timeout; 9 | 10 | socket.on(MARK_BROADCAST, ({ x, y }) => { 11 | marks[++count] = { x, y }; 12 | if (!timeout) { 13 | timeout = setTimeout(() => { 14 | timeout = null; 15 | Object.keys(marks).forEach((cnt) => { 16 | removeQuestionMark(cnt); 17 | }); 18 | setMarks((prev) => { 19 | return { ...prev, ...marks }; 20 | }); 21 | marks = {}; 22 | }, 100); 23 | } 24 | }); 25 | 26 | const addQuestionMark = (position) => { 27 | socket.emit(MARK_BROADCAST, position); 28 | }; 29 | 30 | const disconnecting = () => { 31 | socket.off(MARK_BROADCAST); 32 | }; 33 | 34 | return { addQuestionMark, disconnecting }; 35 | }; 36 | 37 | export default mark; 38 | -------------------------------------------------------------------------------- /client/src/socket/socket.ts: -------------------------------------------------------------------------------- 1 | import io from 'socket.io-client'; 2 | import { BACK_BASE_URL } from '@constant/envs'; 3 | import animation from '@socket/animation'; 4 | import chat from '@socket/chat'; 5 | import control from '@socket/control'; 6 | import create from '@socket/create'; 7 | import enter from '@socket/enter'; 8 | import friend from '@socket/friend'; 9 | import game from '@socket/game'; 10 | import mark from '@socket/mark'; 11 | import signal from '@socket/signal'; 12 | import stream from '@socket/stream'; 13 | import ticket from '@socket/ticket'; 14 | import vote from '@socket/vote'; 15 | 16 | const Socket = () => { 17 | const socket = io(BACK_BASE_URL, { 18 | transports: ['websocket'], 19 | upgrade: false, 20 | forceNew: true, 21 | }); 22 | socket.disconnect(); 23 | 24 | return { 25 | getSID: () => socket.id, 26 | connect: () => socket.connect(), 27 | disconnect: () => socket.disconnect(), 28 | animation: animation(socket), 29 | chat: chat(socket), 30 | control: control(socket), 31 | create: create(socket), 32 | enter: enter(socket), 33 | friend: friend(socket), 34 | game: game(socket), 35 | mark: mark(socket), 36 | signal: signal(socket), 37 | stream: stream(socket), 38 | ticket: ticket(socket), 39 | vote: vote(socket), 40 | }; 41 | }; 42 | export default Socket(); 43 | -------------------------------------------------------------------------------- /client/src/socket/stream.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from 'socket.io-client'; 2 | import { 3 | STREAM_CHANGE_VIDEO, 4 | STREAM_CHANGE_AUDIO, 5 | STREAM_FORCE_CHANGE_VIDEO, 6 | STREAM_FORCE_CHANGE_AUDIO, 7 | } from 'sooltreaming-domain/constant/socketEvent'; 8 | 9 | const stream = (socket: Socket) => (closure: any) => { 10 | const { updateOtherVideo, updateMyVideo, updateOtherAudio, updateMyAudio } = closure; 11 | 12 | socket.on(STREAM_CHANGE_VIDEO, ({ sid, isVideoOn }) => { 13 | if (sid === socket.id) return; 14 | updateOtherVideo({ sid, isVideoOn }); 15 | }); 16 | 17 | socket.on(STREAM_CHANGE_AUDIO, ({ sid, isAudioOn }) => { 18 | if (sid === socket.id) return; 19 | updateOtherAudio({ sid, isAudioOn }); 20 | }); 21 | 22 | socket.on(STREAM_FORCE_CHANGE_VIDEO, ({ sid, isVideoOn }) => { 23 | if (sid === socket.id) updateMyVideo({ isVideoOn }); 24 | else updateOtherVideo({ sid, isVideoOn }); 25 | }); 26 | 27 | socket.on(STREAM_FORCE_CHANGE_AUDIO, ({ sid, isAudioOn }) => { 28 | if (sid === socket.id) updateMyAudio({ isAudioOn }); 29 | else updateOtherAudio({ sid, isAudioOn }); 30 | }); 31 | 32 | const videoChange = (isVideoOn) => { 33 | socket.emit(STREAM_CHANGE_VIDEO, { isVideoOn }); 34 | }; 35 | 36 | const audioChange = (isAudioOn) => { 37 | socket.emit(STREAM_CHANGE_AUDIO, { isAudioOn }); 38 | }; 39 | 40 | const disconnecting = () => { 41 | socket.off(STREAM_CHANGE_VIDEO); 42 | socket.off(STREAM_CHANGE_AUDIO); 43 | }; 44 | 45 | return { videoChange, audioChange, disconnecting }; 46 | }; 47 | 48 | export default stream; 49 | -------------------------------------------------------------------------------- /client/src/socket/ticket.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from 'socket.io-client'; 2 | import { 3 | TICKET_REQUEST, 4 | TICKET_SUCCESS, 5 | TICKET_FAILURE, 6 | } from 'sooltreaming-domain/constant/socketEvent'; 7 | 8 | const ticket = (socket: Socket) => (closure: any) => { 9 | const { abortEnter } = closure; 10 | 11 | socket.on(TICKET_FAILURE, abortEnter); 12 | 13 | const requestValidation = (param) => socket.emit(TICKET_REQUEST, param); 14 | const successValidtaion = () => socket.emit(TICKET_SUCCESS); 15 | const disconnecting = () => { 16 | socket.off(TICKET_FAILURE); 17 | }; 18 | 19 | return { requestValidation, successValidtaion, disconnecting }; 20 | }; 21 | 22 | export default ticket; 23 | -------------------------------------------------------------------------------- /client/src/socket/vote.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from 'socket.io-client'; 2 | import { 3 | VOTE_START, 4 | VOTE_DECISION, 5 | VOTE_GET_DECISION, 6 | VOTE_JUDGE_ON, 7 | VOTE_JUDGE_OFF, 8 | VOTE_PRISON_BREAK, 9 | } from 'sooltreaming-domain/constant/socketEvent'; 10 | 11 | const vote = 12 | (socket: Socket) => 13 | ({ openJudgment, closeJudgement, addApprove, addReject, resetJudgement }) => { 14 | socket.on(VOTE_JUDGE_ON, ({ targetName, participants }) => { 15 | openJudgment({ targetName, participants }); 16 | }); 17 | socket.on(VOTE_GET_DECISION, ({ isApprove }) => { 18 | if (isApprove) addApprove(); 19 | else addReject(); 20 | }); 21 | socket.on(VOTE_JUDGE_OFF, ({ targetSID, targetName, percentage, resetTime }) => { 22 | closeJudgement({ targetSID, targetName, percentage, resetTime }); 23 | }); 24 | socket.on(VOTE_PRISON_BREAK, resetJudgement); 25 | 26 | const startVoting = (targetSID) => { 27 | if (!targetSID) return; 28 | socket.emit(VOTE_START, { targetSID }); 29 | }; 30 | const makeDecision = ({ isApprove }) => { 31 | socket.emit(VOTE_DECISION, { isApprove }); 32 | }; 33 | 34 | const disconnecting = () => { 35 | socket.off(VOTE_GET_DECISION); 36 | socket.off(VOTE_JUDGE_ON); 37 | socket.off(VOTE_JUDGE_OFF); 38 | socket.off(VOTE_PRISON_BREAK); 39 | }; 40 | return { 41 | startVoting, 42 | makeDecision, 43 | disconnecting, 44 | }; 45 | }; 46 | 47 | export default vote; 48 | -------------------------------------------------------------------------------- /client/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import user from '@store/user'; 3 | import notice from '@store/notice'; 4 | import device from '@store/device'; 5 | import room from '@store/room'; 6 | import friend from '@store/friend'; 7 | 8 | // rootReducer Type 9 | export type RootState = ReturnType; 10 | 11 | const rootReducer = combineReducers({ 12 | user, 13 | notice, 14 | device, 15 | room, 16 | friend, 17 | }); 18 | 19 | export default rootReducer; 20 | -------------------------------------------------------------------------------- /client/src/store/notice.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from '@hooks/redux'; 2 | import type { NoticeStateType, ErrorMessageType } from '@ts-types/store'; 3 | 4 | const initialState: NoticeStateType = { 5 | errorMessage: '', 6 | }; 7 | 8 | export const [SET_NOTICE_MESSAGE, setNoticeMessage] = 9 | createAction('SET_NOTICE_MESSAGE'); 10 | export const [RESET_NOTICE_MESSAGE, resetNoticeMessage] = createAction<{}>('RESET_NOTICE_MESSAGE'); 11 | 12 | type NoticeAction = ReturnType; 13 | 14 | function noticeReducer( 15 | state: NoticeStateType = initialState, 16 | action: NoticeAction, 17 | ): NoticeStateType { 18 | switch (action.type) { 19 | case SET_NOTICE_MESSAGE: { 20 | const { errorMessage } = action.payload as ErrorMessageType; 21 | return { 22 | ...state, 23 | errorMessage, 24 | }; 25 | } 26 | case RESET_NOTICE_MESSAGE: { 27 | return { 28 | ...state, 29 | errorMessage: '', 30 | }; 31 | } 32 | default: 33 | return state; 34 | } 35 | } 36 | 37 | export default noticeReducer; 38 | -------------------------------------------------------------------------------- /client/src/store/store.ts: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import createSagaMiddleware from 'redux-saga'; 3 | // Redux / Saga 나의 Setting 4 | import rootReducer from '@src/store'; 5 | import rootSaga from '@src/sagas'; 6 | import { DEPLOYMENT } from '@constant/envs'; 7 | 8 | const sagaMiddleware = createSagaMiddleware(); 9 | const composeEnhancers = 10 | DEPLOYMENT === 'development' 11 | ? (window as any)?.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose 12 | : compose; 13 | const enhancer = composeEnhancers(applyMiddleware(sagaMiddleware)); 14 | 15 | export const store = createStore(rootReducer, enhancer); 16 | 17 | sagaMiddleware.run(rootSaga); 18 | -------------------------------------------------------------------------------- /client/src/ts-types/components/custom.ts: -------------------------------------------------------------------------------- 1 | export type DropdownPropType = { 2 | renderButton: (prop?: any) => React.ReactNode; 3 | renderItem: (prop?: any) => React.ReactNode; 4 | itemList: any[]; 5 | }; 6 | 7 | export type ModalPosType = { 8 | top?: string; 9 | right?: string; 10 | bottom?: string; 11 | left?: string; 12 | }; 13 | 14 | export type ModalPropType = { 15 | children: any; 16 | isOpen: boolean; 17 | renderCenter?: boolean; 18 | isRelative?: boolean; 19 | relativePos?: ModalPosType; 20 | absolutePos?: ModalPosType; 21 | }; 22 | -------------------------------------------------------------------------------- /client/src/ts-types/components/icons.ts: -------------------------------------------------------------------------------- 1 | export type IconPropType = { 2 | className?: string; 3 | width?: number; 4 | height?: number; 5 | fill?: string; 6 | stroke?: string; 7 | }; 8 | -------------------------------------------------------------------------------- /client/src/ts-types/components/setting.ts: -------------------------------------------------------------------------------- 1 | export type SettingPropType = { 2 | renderRoom: Function; 3 | }; 4 | 5 | export type SettingDropdownPropType = { 6 | menuList: MediaDeviceInfo[]; 7 | selected: MediaDeviceInfo | null; 8 | setSelected: Function; 9 | }; 10 | 11 | export type SettingTogglePropType = { 12 | isDeviceOn: boolean; 13 | setIsDeviceOn: Function; 14 | Icon: Function; 15 | }; 16 | -------------------------------------------------------------------------------- /client/src/ts-types/components/user-information.ts: -------------------------------------------------------------------------------- 1 | export type FriendType = { 2 | imgUrl: string; 3 | nickname: string; 4 | children: JSX.Element[] | JSX.Element; 5 | }; 6 | 7 | export type NicknameLogType = Array<{ nickname: string }>; 8 | 9 | export type InformationPropType = { 10 | id: string; 11 | imgUrl: string; 12 | nickname: string; 13 | }; 14 | 15 | export type UserProfilePropType = { 16 | id: string; 17 | imgUrl: string; 18 | nickname: string; 19 | nicknameLog: NicknameLogType; 20 | }; 21 | 22 | export type FriendDeleteModalPropType = { 23 | id: string; 24 | nickname: string; 25 | }; 26 | 27 | export type FriendInfoModalPropType = { 28 | id: string; 29 | nickname: string; 30 | imgUrl: string; 31 | }; 32 | 33 | export type NickLogModalPropType = { 34 | nickname: string; 35 | nicknameLog: NicknameLogType; 36 | }; 37 | 38 | export type RankingBoxPropType = { 39 | title: string; 40 | rank: { _id: string }[]; 41 | nowSelect: string; 42 | filterList: string[]; 43 | }; 44 | 45 | export type MenuPropType = { 46 | menu: string; 47 | }; 48 | -------------------------------------------------------------------------------- /client/src/ts-types/components/user.ts: -------------------------------------------------------------------------------- 1 | import { MouseEventHandler } from 'react'; 2 | 3 | export type UsersPropType = { 4 | startVoteRef: React.MutableRefObject; 5 | onclickRequestFriend: (prop?: any) => MouseEventHandler; 6 | }; 7 | -------------------------------------------------------------------------------- /client/src/ts-types/utils.ts: -------------------------------------------------------------------------------- 1 | export type fetchParams = { 2 | url: string; 3 | query?: { [key: string]: string }; 4 | body?: FormData | Object; 5 | headerOptions?: HeadersInit; 6 | options?: RequestInit; 7 | }; 8 | -------------------------------------------------------------------------------- /client/src/utils/date.ts: -------------------------------------------------------------------------------- 1 | export const filterDate = (dateString: string): string => { 2 | const targetDate = new Date(dateString); 3 | const targetString = targetDate.toString(); 4 | const year = targetString.slice(11, 15); 5 | const month = (targetDate.getMonth() + 1).toString().padStart(2, '0'); 6 | const day = targetString.slice(8, 10); 7 | const hour = targetString.slice(16, 18); 8 | const minute = targetString.slice(19, 21); 9 | const second = targetString.slice(22, 24); 10 | 11 | return ` 12 | ${year}년 ${month}월 ${day}일 13 | ${hour}:${minute}:${second} 14 | `; 15 | }; 16 | -------------------------------------------------------------------------------- /client/src/utils/regExpr.ts: -------------------------------------------------------------------------------- 1 | const labelExpr = new RegExp('(?