├── .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 | You need to enable JavaScript to run this app.
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 |
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 |
29 |
30 |
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 ? : }
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 ? : }
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 |
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 | {target} 을(를) 처분할까요?
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
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 | setIsOpen(true)}>
16 |
17 |
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 | {nickname || 'Judangs'}
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 |
12 | Sooltreaming
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | 재ㅁㅣ있는 술자.ㄹㅣ;를 위한
24 | 화상ㅊㅐ팅
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('(?.*)(?\\s\\([\\w]{4}\\:[\\w]{4}\\))');
2 |
3 | export const filterLabel = (string): string => {
4 | const filtered = labelExpr.exec(string);
5 | const filteredLabel = filtered?.groups?.label ?? '';
6 |
7 | return !filteredLabel ? string : filteredLabel;
8 | };
9 |
--------------------------------------------------------------------------------
/client/src/utils/request.ts:
--------------------------------------------------------------------------------
1 | import { BACK_BASE_URL, BACK_VERSION } from '@constant/envs';
2 | import { fetchParams } from '@ts-types/utils';
3 |
4 | const BASE_URL = `${BACK_BASE_URL}/api/${BACK_VERSION}`;
5 |
6 | const customFetch =
7 | (method: string) =>
8 | async ({ url, query, body = {}, headerOptions, options }: fetchParams) => {
9 | const query_string = query
10 | ? `?${Object.entries(query)
11 | .reduce((prev, [key, value]) => [...prev, `${key}=${value}`], [] as Array)
12 | .join('&')}`
13 | : '';
14 |
15 | const init = {
16 | ...(options ?? {}),
17 | method,
18 | credentials: 'include' as RequestCredentials,
19 | headers: {
20 | ...(headerOptions ?? { 'Content-Type': 'application/json' }),
21 | },
22 | };
23 |
24 | if (method !== 'GET') {
25 | if (body.toString() === '[object FormData]') {
26 | init.body = body as FormData;
27 | } else {
28 | init.body = JSON.stringify(body);
29 | }
30 | }
31 |
32 | const resolve = await fetch(`${BASE_URL}${url}${query_string}`, init);
33 | const { status } = resolve;
34 | if (!status) return resolve;
35 | const json = await resolve.json();
36 | return { json, status };
37 | };
38 |
39 | const request = {
40 | get: customFetch('GET'),
41 | post: customFetch('POST'),
42 | patch: customFetch('PATCH'),
43 | delete: customFetch('DELETE'),
44 | };
45 |
46 | export default request;
47 |
--------------------------------------------------------------------------------
/client/tsconfig.alias.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./src",
4 | "paths": {
5 | "@src/*": ["./*"],
6 | "@api/*": ["./api/*"],
7 | "@components/*": ["./components/*"],
8 | "@constant/*": ["./constant/*"],
9 | "@hooks/*": ["./hooks/*"],
10 | "@pages/*": ["./pages/*"],
11 | "@socket/*": ["./socket/*"],
12 | "@store/*": ["./store/*"],
13 | "@sagas/*": ["./sagas/*"],
14 | "@ts-types/*": ["./ts-types/*"],
15 | "@utils/*": ["./utils/*"]
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.alias.json",
3 | "compilerOptions": {
4 | "target": "es5",
5 | "lib": [
6 | "dom",
7 | "dom.iterable",
8 | "esnext"
9 | ],
10 | "allowJs": true,
11 | "skipLibCheck": true,
12 | "esModuleInterop": true,
13 | "allowSyntheticDefaultImports": true,
14 | "strict": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "noFallthroughCasesInSwitch": false,
17 | "module": "esnext",
18 | "moduleResolution": "node",
19 | "resolveJsonModule": true,
20 | "isolatedModules": true,
21 | "noEmit": true,
22 | "noImplicitAny": false,
23 | "jsx": "react-jsx"
24 | },
25 | "include": [
26 | "src",
27 | "craco.config.js"
28 | ],
29 | "exclude": [
30 | "node_modules"
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/domain/constant/addition.ts:
--------------------------------------------------------------------------------
1 | export const CHEERS_GIF_NUM = 2;
2 | export const CHEERS_TIME = 5000;
3 | export const LISTED_GIF = ['/images/beer-cheers1.gif', '/images/beer-cheers2.gif'];
4 |
5 | export const SECOND_TO_MS = 1000;
6 | export const VOTE_TIME = 60;
7 |
8 | export const TOAST_TIME = 3500;
9 |
10 | export const FILE_PUBLIC_URL = '/uploads';
11 | export const DEFAULT_PROFILE_IMAGE = 'HumanIcon.svg';
12 |
13 | export const NCP_ENDPOINT = 'https://kr.object.ncloudstorage.com';
14 | export const NCP_BUCKET = 'sooltreaming';
15 |
16 | export const DEFAULT_PROFILE_IMAGE_URL = `${NCP_ENDPOINT}/${NCP_BUCKET}${FILE_PUBLIC_URL}/${DEFAULT_PROFILE_IMAGE}`;
17 |
--------------------------------------------------------------------------------
/domain/constant/gameName.ts:
--------------------------------------------------------------------------------
1 | export const LIAR = '라이어';
2 | export const UP_DOWN = '업다운';
3 | export const RANDOM_PICK = '랜덤픽';
4 |
--------------------------------------------------------------------------------
/domain/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sooltreaming-domain",
3 | "version": "1.0.0",
4 | "description": "Sooltreaming Default Domain",
5 | "author": "Judangs",
6 | "license": "ISC",
7 | "main": "index.js",
8 | "scripts": {
9 | "build": "tsc"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/domain/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "esModuleInterop": true,
5 | "target": "es5",
6 | "noImplicitAny": false,
7 | "removeComments": true,
8 | "allowSyntheticDefaultImports": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sooltreaming-server",
3 | "version": "0.2.0",
4 | "private": true,
5 | "scripts": {
6 | "start": "npm link ../domain && nodemon --exec ts-node -r tsconfig-paths/register ./src/app.ts",
7 | "deploy": "npx tsc && pm2 start ./build/app.js --node-args='-r ./tsconfig-paths-bootstrap.js'"
8 | },
9 | "dependencies": {
10 | "@types/multer": "^1.4.7",
11 | "cookie-parser": "~1.4.4",
12 | "cors": "^2.8.5",
13 | "debug": "~2.6.9",
14 | "dotenv": "^10.0.0",
15 | "express": "~4.16.1",
16 | "express-session": "^1.17.2",
17 | "mongoose": "^6.0.12",
18 | "morgan": "~1.9.1",
19 | "multer": "^1.4.3",
20 | "passport": "^0.5.0",
21 | "passport-github": "^1.1.0",
22 | "passport-naver": "^1.0.6",
23 | "socket.io": "^4.3.1",
24 | "sooltreaming-domain": "file:../domain"
25 | },
26 | "devDependencies": {
27 | "@types/cookie-parser": "^1.4.2",
28 | "@types/cors": "^2.8.12",
29 | "@types/express": "^4.17.13",
30 | "@types/express-session": "^1.17.4",
31 | "@types/morgan": "^1.9.3",
32 | "@types/node": "^16.11.6",
33 | "@types/passport": "^1.0.7",
34 | "@types/passport-github": "^1.1.6",
35 | "@types/passport-naver": "^0.2.1",
36 | "@types/socket.io": "^3.0.2",
37 | "aws-sdk": "^2.348.0",
38 | "multer-s3": "^2.10.0",
39 | "node-cron": "^3.0.0",
40 | "nodemon": "^2.0.14",
41 | "ts-node": "^10.4.0",
42 | "tsconfig-paths": "^3.11.0",
43 | "typescript": "^4.4.4"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/server/src/api/index.ts:
--------------------------------------------------------------------------------
1 | import v1Router from '@api/v1';
2 | import express from 'express';
3 | const router = express.Router();
4 |
5 | router.use('/v1', v1Router);
6 |
7 | router.use(function (err: any, req: any, res: any, next: any): void {
8 | const { status, message } = err;
9 | if (status < 400) next();
10 | res.status(status).send({ error: message });
11 | });
12 |
13 | export default router;
14 |
--------------------------------------------------------------------------------
/server/src/api/v1/auth.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import passport from 'passport';
3 | import { AUTH_REDIRECT_URL } from '@src/constant';
4 | import { CustomError } from '@utils/error';
5 |
6 | const router = express.Router();
7 |
8 | const redirectRouter = (req, res) => res.redirect(AUTH_REDIRECT_URL);
9 | router.get('/github', passport.authenticate('github'), redirectRouter);
10 | router.get('/naver', passport.authenticate('naver'), redirectRouter);
11 |
12 | router.get('/login', (req, res, next): any => {
13 | try {
14 | const isAuth = req.isAuthenticated();
15 | if (!isAuth) throw new CustomError(401, 'fail to login');
16 |
17 | const user = req.user;
18 |
19 | return res.status(202).json(user);
20 | } catch (error) {
21 | next(error);
22 | }
23 | });
24 |
25 | router.get('/logout', (req, res, next) => {
26 | req.logout();
27 | req.session.destroy(function () {
28 | res.status(302).send();
29 | });
30 | });
31 |
32 | export default router;
33 |
--------------------------------------------------------------------------------
/server/src/api/v1/friend.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import {
3 | postFriend,
4 | getSendFriend,
5 | getReceiveFriend,
6 | getFriend,
7 | patchSendFriend,
8 | patchReceiveFriend,
9 | patchUnfriend,
10 | patchFriend,
11 | } from '@controller/friend';
12 |
13 | const router = express.Router();
14 |
15 | router.post('/', postFriend);
16 | router.get('/send', getSendFriend);
17 | router.get('/receive', getReceiveFriend);
18 | router.get('/', getFriend);
19 | router.patch('/send', patchSendFriend);
20 | router.patch('/receive', patchReceiveFriend);
21 | router.patch('/remove', patchUnfriend);
22 | router.patch('/accept', patchFriend);
23 |
24 | export default router;
25 |
--------------------------------------------------------------------------------
/server/src/api/v1/index.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | const router = express.Router();
3 |
4 | import testRouter from './test';
5 | import authRouter from './auth';
6 | import userRouter from './user';
7 | import friendRouter from './friend';
8 | import rankRouter from './rank';
9 |
10 | router.use('/user', userRouter);
11 | router.use('/test', testRouter);
12 | router.use('/auth', authRouter);
13 | router.use('/friend', friendRouter);
14 | router.use('/rank', rankRouter);
15 | export default router;
16 |
--------------------------------------------------------------------------------
/server/src/api/v1/rank.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import { getRank } from '@controller/rank';
3 | const router = express.Router();
4 |
5 | router.get('/:type', getRank);
6 |
7 | export default router;
8 |
--------------------------------------------------------------------------------
/server/src/api/v1/test.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import User from '@models/User';
3 | import NicknameLog from '@models/NicknameLog';
4 | const router = express.Router();
5 |
6 | router.get('/', async function (req, res, next) {
7 | const user = new User({
8 | githubId: 'github_id',
9 | naverId: 'naver_id',
10 | nickname: 'nickname',
11 | imgUrl: 'http://dfjskdfdjf.com',
12 | });
13 | const nickname = new NicknameLog({
14 | userId: user._id,
15 | nickname: user.nickname,
16 | });
17 |
18 | try {
19 | const result = await user.save();
20 | const nickResult = await nickname.save();
21 | res.status(201).json({ result, nickResult });
22 | } catch (e) {
23 | console.log(e);
24 | }
25 | });
26 |
27 | router.get('/error', function (req, res, next) {
28 | next({ message: 'hi', status: 404 });
29 | });
30 |
31 | export default router;
32 |
--------------------------------------------------------------------------------
/server/src/api/v1/user.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import { getUserInformation, postUserImage, patchUserNickname } from '@controller/user';
3 | import { upload } from '@service/user';
4 |
5 | const router = express.Router();
6 |
7 | router.get('/', getUserInformation);
8 | router.post('/image', upload.single('image'), postUserImage);
9 | router.patch('/nickname', patchUserNickname);
10 |
11 | export default router;
12 |
--------------------------------------------------------------------------------
/server/src/app.ts:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 | import express from 'express';
3 | import Loader from '@src/loader';
4 | import http from 'http';
5 | import apiRouter from '@src/api';
6 | import { PORT } from '@src/constant';
7 | import { updateRankCron } from '@utils/cron';
8 | import { updateRank } from '@service/rank';
9 |
10 | const app = express();
11 | const server = http.createServer(app);
12 | app.set('port', PORT);
13 |
14 | Loader({ server, app });
15 |
16 | app.use('/api', apiRouter);
17 |
18 | updateRank();
19 | updateRankCron.start();
20 |
21 | function onListening(): void {
22 | const addr = server.address();
23 | const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port;
24 | console.log(bind);
25 | }
26 |
27 | server.listen(PORT);
28 | server.on('listening', onListening);
29 |
--------------------------------------------------------------------------------
/server/src/controller/passport/github.ts:
--------------------------------------------------------------------------------
1 | import GitHubStrategy from 'passport-github';
2 | import User from '@models/User';
3 | import { GITHUB_ID, GITHUB_SECRET, OAUTH_CALLBACK_URL } from '@src/constant';
4 | import type { GithubProfileType } from '@src/types';
5 |
6 | const gitHubStrategy = new GitHubStrategy.Strategy(
7 | {
8 | clientID: GITHUB_ID,
9 | clientSecret: GITHUB_SECRET,
10 | callbackURL: OAUTH_CALLBACK_URL,
11 | },
12 | async (_accessToken, _refreshToken, profile, cb): Promise => {
13 | try {
14 | const { login: githubId, avatar_url: imgUrl } = profile._json as GithubProfileType;
15 | const existUser = await User.findOne({ githubId });
16 | if (existUser) return cb(null, existUser);
17 |
18 | const newUser = await new User({
19 | githubId,
20 | nickname: githubId,
21 | imgUrl,
22 | }).save();
23 | return cb(null, newUser);
24 | } catch (err) {
25 | return cb(err);
26 | }
27 | },
28 | );
29 |
30 | export default gitHubStrategy;
31 |
--------------------------------------------------------------------------------
/server/src/controller/passport/naver.ts:
--------------------------------------------------------------------------------
1 | import NaverStrategy from 'passport-naver';
2 | import User from '@models/User';
3 | import { NAVER_ID, NAVER_SECRET, OAUTH_CALLBACK_URL } from '@src/constant';
4 | import type { NaverProfileType } from '@src/types';
5 |
6 | const naverStrategy = new NaverStrategy.Strategy(
7 | {
8 | clientID: NAVER_ID,
9 | clientSecret: NAVER_SECRET,
10 | callbackURL: OAUTH_CALLBACK_URL,
11 | },
12 | async (_accessToken, _refreshToken, profile, cb): Promise => {
13 | try {
14 | const { email: naverId, nickname, profile_image: imgUrl } = profile._json as NaverProfileType;
15 |
16 | const existUser = await User.findOne({ naverId });
17 | if (existUser) return cb(null, existUser);
18 |
19 | const newUser = await new User({ naverId, nickname, imgUrl }).save();
20 | return cb(null, newUser);
21 | } catch (err) {
22 | return cb(err);
23 | }
24 | },
25 | );
26 |
27 | export default naverStrategy;
28 |
--------------------------------------------------------------------------------
/server/src/controller/rank.ts:
--------------------------------------------------------------------------------
1 | import { allRank } from '@service/rank';
2 | import { CustomError, errorWrapper } from '@utils/error';
3 | import { ERROR } from '@src/constant';
4 |
5 | export const getRank = errorWrapper(async (req, res, next): Promise => {
6 | const rankType = req.params.type;
7 | if (!rankType) throw new CustomError(400, ERROR.INVALID_TYPE);
8 | const result = allRank[rankType];
9 | res.status(200).json(result);
10 | });
11 |
--------------------------------------------------------------------------------
/server/src/controller/socket/animation.ts:
--------------------------------------------------------------------------------
1 | import { createLog } from '@service/user';
2 | import {
3 | CHEERS_BROADCAST,
4 | CLOSEUP_ON,
5 | CLOSEUP_OFF,
6 | } from 'sooltreaming-domain/constant/socketEvent';
7 | import { STATUS_VOTE_NORMAL, STATUS_VOTE_EXECUTING } from '@src/constant';
8 | import type { SocketPropType } from '@src/types';
9 |
10 | const animation = ({ io, socket, rooms, targetInfo }: SocketPropType): SocketPropType => {
11 | socket.on(CHEERS_BROADCAST, () => {
12 | const { code } = targetInfo;
13 | if (!(code in rooms)) return;
14 | io.to(code).emit(CHEERS_BROADCAST);
15 | createLog(rooms[code].users[socket.id].id, CHEERS_BROADCAST);
16 | });
17 |
18 | socket.on(CLOSEUP_ON, () => {
19 | const { code } = targetInfo;
20 | if (!(code in rooms)) return;
21 | rooms[code].closeupUser = socket.id;
22 | io.to(code).emit(CLOSEUP_ON, socket.id);
23 |
24 | const id = rooms[code].users[socket.id].id;
25 | createLog(id, CLOSEUP_ON);
26 | });
27 |
28 | socket.on(CLOSEUP_OFF, () => {
29 | const { code } = targetInfo;
30 | if (!(code in rooms)) return;
31 | if (rooms[code].status === STATUS_VOTE_EXECUTING && rooms[code].hostSID !== socket.id) return;
32 | rooms[code].status = STATUS_VOTE_NORMAL;
33 | rooms[code].closeupUser = '';
34 | io.to(code).emit(CLOSEUP_OFF);
35 | });
36 |
37 | return { io, socket, rooms, targetInfo };
38 | };
39 |
40 | export default animation;
41 |
--------------------------------------------------------------------------------
/server/src/controller/socket/chat.ts:
--------------------------------------------------------------------------------
1 | import { createLog } from '@service/user';
2 | import { getTimeString } from '@utils/time';
3 | import { CHAT_RECEIVE, CHAT_SENDING } from 'sooltreaming-domain/constant/socketEvent';
4 | import type { SocketPropType } from '@src/types';
5 |
6 | const chat = ({ io, socket, rooms, targetInfo }: SocketPropType): SocketPropType => {
7 | socket.on(CHAT_SENDING, ({ msg }) => {
8 | const { code } = targetInfo;
9 |
10 | const messageData = {
11 | msg,
12 | sid: socket.id,
13 | date: getTimeString(),
14 | };
15 | io.to(code).emit(CHAT_RECEIVE, messageData);
16 |
17 | const id = rooms[code].users[socket.id].id;
18 | createLog(id, CHAT_SENDING);
19 | });
20 | return { io, socket, rooms, targetInfo };
21 | };
22 |
23 | export default chat;
24 |
--------------------------------------------------------------------------------
/server/src/controller/socket/create.ts:
--------------------------------------------------------------------------------
1 | import type { RoomType } from '@src/types';
2 | import { CREATE_REQUEST, CREATE_SUCCESS } from 'sooltreaming-domain/constant/socketEvent';
3 | import { STATUS_VOTE_NORMAL } from '@src/constant';
4 | import type { SocketPropType } from '@src/types';
5 |
6 | const createRoomCode = (rooms: RoomType) => {
7 | while (true) {
8 | const code = Math.random().toString(16).substr(2, 5);
9 | if (!(code in rooms)) return code;
10 | }
11 | };
12 |
13 | const create = ({ io, socket, rooms, targetInfo }: SocketPropType): SocketPropType => {
14 | socket.on(CREATE_REQUEST, (user) => {
15 | const roomCode = createRoomCode(rooms);
16 | rooms[roomCode] = {
17 | hostSID: null,
18 | isOpen: true,
19 | waiters: [],
20 | closeupUser: '',
21 | users: {},
22 | usersDevices: {},
23 | status: STATUS_VOTE_NORMAL,
24 | vote: {
25 | trial: null,
26 | defendant: '',
27 | cool: {},
28 | voteBox: {},
29 | },
30 | game: {
31 | title: '',
32 | host: '',
33 | },
34 | };
35 | socket.emit(CREATE_SUCCESS, { roomCode });
36 | });
37 |
38 | return { io, socket, rooms, targetInfo };
39 | };
40 |
41 | export default create;
42 |
--------------------------------------------------------------------------------
/server/src/controller/socket/friend.ts:
--------------------------------------------------------------------------------
1 | import { FRIEND_REQUEST } from 'sooltreaming-domain/constant/socketEvent';
2 | import type { SocketPropType } from '@src/types';
3 |
4 | const friend = ({ io, socket, rooms, targetInfo }: SocketPropType): SocketPropType => {
5 | socket.on(FRIEND_REQUEST, ({ sid, id, imgUrl, nickname }) => {
6 | io.to(sid).emit(FRIEND_REQUEST, { id, imgUrl, nickname });
7 | });
8 | return { io, socket, rooms, targetInfo };
9 | };
10 |
11 | export default friend;
12 |
--------------------------------------------------------------------------------
/server/src/controller/socket/mark.ts:
--------------------------------------------------------------------------------
1 | import { createLog } from '@service/user';
2 | import { MARK_BROADCAST } from 'sooltreaming-domain/constant/socketEvent';
3 | import type { SocketPropType } from '@src/types';
4 |
5 | const mark = ({ io, socket, rooms, targetInfo }: SocketPropType): SocketPropType => {
6 | socket.on(MARK_BROADCAST, ({ x, y }) => {
7 | const { code } = targetInfo;
8 | if (!(code in rooms)) return;
9 |
10 | io.to(code).emit(MARK_BROADCAST, { x, y });
11 | const id = rooms[code].users[socket.id].id;
12 | createLog(id, MARK_BROADCAST);
13 | });
14 | return { io, socket, rooms, targetInfo };
15 | };
16 |
17 | export default mark;
18 |
--------------------------------------------------------------------------------
/server/src/controller/socket/signal.ts:
--------------------------------------------------------------------------------
1 | import { SIGNAL_OFFER, SIGNAL_ANSWER, SIGNAL_ICE } from 'sooltreaming-domain/constant/socketEvent';
2 | import type { SocketPropType } from '@src/types';
3 |
4 | const signal = ({ io, socket, rooms, targetInfo }: SocketPropType): SocketPropType => {
5 | socket.on(SIGNAL_OFFER, ({ offer, receiverSID, senderSID }) => {
6 | io.to(receiverSID).emit(SIGNAL_OFFER, { offer, targetSID: senderSID });
7 | });
8 |
9 | socket.on(SIGNAL_ANSWER, ({ answer, receiverSID, senderSID }) => {
10 | io.to(receiverSID).emit(SIGNAL_ANSWER, { answer, targetSID: senderSID });
11 | });
12 |
13 | socket.on(SIGNAL_ICE, ({ candidate, receiverSID, senderSID }) => {
14 | io.to(receiverSID).emit(SIGNAL_ICE, { candidate, targetSID: senderSID });
15 | });
16 | return { io, socket, rooms, targetInfo };
17 | };
18 |
19 | export default signal;
20 |
--------------------------------------------------------------------------------
/server/src/controller/socket/stream.ts:
--------------------------------------------------------------------------------
1 | import { STREAM_CHANGE_VIDEO, STREAM_CHANGE_AUDIO } from 'sooltreaming-domain/constant/socketEvent';
2 | import type { SocketPropType } from '@src/types';
3 |
4 | const stream = ({ io, socket, rooms, targetInfo }: SocketPropType): SocketPropType => {
5 | socket.on(STREAM_CHANGE_VIDEO, ({ isVideoOn }) => {
6 | const { code } = targetInfo;
7 | if (!(code in rooms)) return;
8 | const targetRoom = rooms[code];
9 | const sid = socket.id;
10 | targetRoom.usersDevices[sid] = { ...targetRoom.usersDevices[sid], isVideoOn };
11 | io.to(code).emit(STREAM_CHANGE_VIDEO, { sid: socket.id, isVideoOn });
12 | });
13 |
14 | socket.on(STREAM_CHANGE_AUDIO, ({ isAudioOn }) => {
15 | const { code } = targetInfo;
16 | if (!(code in rooms)) return;
17 | const targetRoom = rooms[code];
18 | const sid = socket.id;
19 | targetRoom.usersDevices[sid] = { ...targetRoom.usersDevices[sid], isAudioOn };
20 | io.to(code).emit(STREAM_CHANGE_AUDIO, { sid: socket.id, isAudioOn });
21 | });
22 |
23 | return { io, socket, rooms, targetInfo };
24 | };
25 |
26 | export default stream;
27 |
--------------------------------------------------------------------------------
/server/src/controller/socket/ticket.ts:
--------------------------------------------------------------------------------
1 | import {
2 | TICKET_REQUEST,
3 | TICKET_SUCCESS,
4 | TICKET_FAILURE,
5 | } from 'sooltreaming-domain/constant/socketEvent';
6 | import type { SocketPropType } from '@src/types';
7 | import { ERROR } from '@src/constant';
8 |
9 | const ticket = ({ io, socket, rooms, targetInfo }: SocketPropType): SocketPropType => {
10 | let roomCode = '';
11 | socket.on(TICKET_REQUEST, ({ code }) => {
12 | roomCode = code;
13 | if (!(code in rooms)) return socket.emit(TICKET_FAILURE, { message: ERROR.NOT_EXIST_ROOM });
14 | if (!rooms[code].isOpen)
15 | return socket.emit(TICKET_FAILURE, { message: ERROR.UNAUTHORIZED_ROOM });
16 |
17 | rooms[code].waiters.push(socket.id);
18 | });
19 |
20 | socket.on(TICKET_SUCCESS, () => {
21 | if (!(roomCode in rooms)) return;
22 | rooms[roomCode].waiters = rooms[roomCode].waiters.filter((id) => id !== socket.id);
23 | });
24 |
25 | return { io, socket, rooms, targetInfo };
26 | };
27 |
28 | export default ticket;
29 |
--------------------------------------------------------------------------------
/server/src/controller/user.ts:
--------------------------------------------------------------------------------
1 | import { CustomError, errorWrapper } from '@utils/error';
2 | import { getUserInfoService, updateNickname, updateUserImage } from '@service/user';
3 | import { ERROR, SUCCESS } from '@src/constant';
4 |
5 | export const getUserInformation = errorWrapper(async (req, res, next): Promise => {
6 | const { id: _id } = req.query;
7 | const { information, nicknameLog } = await getUserInfoService(_id);
8 |
9 | res.status(200).json({
10 | information,
11 | nicknameLog,
12 | });
13 | });
14 |
15 | export const postUserImage = errorWrapper(async (req, res, next): Promise => {
16 | const _id = req.user._id;
17 | let image = req.file;
18 | const imgUrl = await updateUserImage(_id, image);
19 |
20 | res.status(200).json({
21 | imgUrl,
22 | message: SUCCESS.message,
23 | });
24 | });
25 |
26 | export const patchUserNickname = errorWrapper(async (req, res, next): Promise => {
27 | const _id = req.user._id;
28 | const { nickname } = req.body;
29 | if (!nickname) throw new CustomError(400, ERROR.INVALID_DATA);
30 |
31 | await updateNickname(_id, nickname);
32 |
33 | res.status(200).json({
34 | message: SUCCESS.message,
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/server/src/loader/basic.ts:
--------------------------------------------------------------------------------
1 | import logger from 'morgan';
2 | import express from 'express';
3 | import cookieParser from 'cookie-parser';
4 | import cors from 'cors';
5 | import session from 'express-session';
6 | import { FRONT_BASE_URL, SESSION_SECRET } from '@src/constant';
7 |
8 | const basicLoader = (app): any => {
9 | app.use(
10 | cors({
11 | origin: [FRONT_BASE_URL],
12 | credentials: true,
13 | }),
14 | );
15 | app.use(logger('dev'));
16 | app.use(express.json());
17 | app.use(express.urlencoded({ extended: false }));
18 | app.use(
19 | session({
20 | secret: SESSION_SECRET,
21 | resave: true,
22 | saveUninitialized: false,
23 | }),
24 | );
25 | app.use(cookieParser());
26 |
27 | return app;
28 | };
29 |
30 | export default basicLoader;
31 |
--------------------------------------------------------------------------------
/server/src/loader/index.ts:
--------------------------------------------------------------------------------
1 | import mongoLoader from '@loader/mongo';
2 | import passportLoader from '@loader/passport';
3 | import basicLoader from '@loader/basic';
4 | import socketLoader from '@loader/socket';
5 |
6 | const Loader = ({ server, app }): void => {
7 | mongoLoader();
8 | basicLoader(app);
9 | passportLoader(app);
10 | socketLoader(server, app);
11 | };
12 |
13 | export default Loader;
14 |
--------------------------------------------------------------------------------
/server/src/loader/mongo.ts:
--------------------------------------------------------------------------------
1 | import initMongo from '@src/models';
2 | import { DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD } from '@src/constant';
3 |
4 | const dbConfig: Object = {
5 | host: DB_HOST,
6 | port: DB_PORT,
7 | dbName: DB_NAME,
8 | user: DB_USER,
9 | pwd: DB_PASSWORD,
10 | };
11 |
12 | const mongoLoader = (): void => {
13 | initMongo(dbConfig);
14 | };
15 |
16 | export default mongoLoader;
17 |
--------------------------------------------------------------------------------
/server/src/loader/passport.ts:
--------------------------------------------------------------------------------
1 | import passport from 'passport';
2 | import gitHubStrategy from '@controller/passport/github';
3 | import naverStrategy from '@controller/passport/naver';
4 | import User from '@models/User';
5 | import type { ObjectId } from 'mongoose';
6 |
7 | const passportLoader = (app): void => {
8 | app.use(passport.initialize());
9 | app.use(passport.session());
10 |
11 | // serializeUser : Session에 유저 정보를 저장
12 | passport.serializeUser((user: { _id: ObjectId }, done): void => {
13 | done(null, user._id);
14 | });
15 | // deserializeUser : 페이지 접근 때마다 Session에서 Request 객체에 넘겨줄 값 설정
16 | passport.deserializeUser(async (_id, done): Promise => {
17 | const user = await User.findById(_id).select('_id nickname imgUrl');
18 | done(null, user);
19 | });
20 |
21 | // 전략 설정
22 | passport.use(gitHubStrategy);
23 | passport.use(naverStrategy);
24 | };
25 |
26 | export default passportLoader;
27 |
--------------------------------------------------------------------------------
/server/src/loader/socket.ts:
--------------------------------------------------------------------------------
1 | import { Socket, Server } from 'socket.io';
2 | import animation from '@controller/socket/animation';
3 | import chat from '@controller/socket/chat';
4 | import control from '@controller/socket/control';
5 | import create from '@controller/socket/create';
6 | import enter from '@controller/socket/enter';
7 | import friend from '@controller/socket/friend';
8 | import game from '@controller/socket/game';
9 | import mark from '@controller/socket/mark';
10 | import signal from '@controller/socket/signal';
11 | import stream from '@controller/socket/stream';
12 | import ticket from '@controller/socket/ticket';
13 | import vote from '@controller/socket/vote';
14 |
15 | import pipe from '@utils/pipe';
16 | import { FRONT_BASE_URL } from '@src/constant';
17 | import type { RoomType } from '@src/types';
18 |
19 | const socketLoader = (server, app): any => {
20 | const io = new Server(server, {
21 | cors: {
22 | origin: FRONT_BASE_URL,
23 | credentials: true,
24 | methods: ['GET', 'POST'],
25 | },
26 | });
27 | const rooms: RoomType = {};
28 |
29 | io.on('connection', (socket: Socket): void => {
30 | console.log('socket connection!!', socket.id);
31 |
32 | pipe(
33 | enter,
34 | animation,
35 | chat,
36 | control,
37 | create,
38 | friend,
39 | game,
40 | mark,
41 | signal,
42 | stream,
43 | ticket,
44 | vote,
45 | )({ io, socket, rooms });
46 |
47 | socket.on('disconnect', () => {
48 | console.log('disconnect socket!!' + socket.id);
49 | });
50 | });
51 |
52 | return app;
53 | };
54 |
55 | export default socketLoader;
56 |
--------------------------------------------------------------------------------
/server/src/models/NicknameLog.ts:
--------------------------------------------------------------------------------
1 | import { Schema, model } from 'mongoose';
2 | import type { NicknameLog } from '@src/types';
3 |
4 | const nicknameLogSchema = new Schema(
5 | {
6 | userId: {
7 | type: Schema.Types.ObjectId,
8 | ref: 'User',
9 | required: true,
10 | index: true,
11 | },
12 | nickname: {
13 | type: String,
14 | required: true,
15 | },
16 | },
17 | { timestamps: true },
18 | );
19 |
20 | export default model('NicknameLog', nicknameLogSchema);
21 |
--------------------------------------------------------------------------------
/server/src/models/index.ts:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 |
3 | const db = mongoose.connection;
4 | db.on('error', () => {
5 | console.log('MongoDB Connection Failed!');
6 | });
7 | db.once('open', () => {
8 | console.log('MongoDB Connected');
9 | });
10 |
11 | const initMongo = (dbConfig): void => {
12 | const { host, port, dbName, user, pwd } = dbConfig;
13 | mongoose.connect(`mongodb://${user}:${pwd}@${host}:${port}/${dbName}`);
14 | };
15 |
16 | export default initMongo;
17 |
--------------------------------------------------------------------------------
/server/src/service/rank.ts:
--------------------------------------------------------------------------------
1 | import { userCount } from '@utils/userCount';
2 | import User from '@models/User';
3 |
4 | export const allRank = {
5 | chatCount: [],
6 | hookCount: [],
7 | pollCount: [],
8 | closeupCount: [],
9 | dieCount: [],
10 | speakCount: [],
11 | starterCount: [],
12 | totalSeconds: [],
13 | };
14 |
15 | export const updateRank = async () => {
16 | try {
17 | const newRank = await Promise.all(
18 | userCount.map((count) =>
19 | User.find()
20 | .select(`imgUrl nickname ${count}`)
21 | .sort({ [count]: -1 }),
22 | ),
23 | );
24 | Object.keys(allRank).forEach((key, index) => (allRank[key] = newRank[index]));
25 | } catch (error) {
26 | console.error(error);
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/server/src/utils/cron.ts:
--------------------------------------------------------------------------------
1 | import cron from 'node-cron';
2 | import { updateRank } from '@service/rank';
3 |
4 | export const updateRankCron = cron.schedule(
5 | '*/5 * * * *',
6 | () => {
7 | updateRank();
8 | },
9 | {
10 | scheduled: false,
11 | },
12 | );
13 |
--------------------------------------------------------------------------------
/server/src/utils/error.ts:
--------------------------------------------------------------------------------
1 | import { ERROR } from '@src/constant';
2 |
3 | export class CustomError extends Error {
4 | status: number;
5 | constructor(status: number, message: string) {
6 | super(message);
7 | this.status = status;
8 | }
9 | }
10 |
11 | const errorHandler = (error): Object => {
12 | const { status, message } = error;
13 | return { status: status || 500, message };
14 | };
15 |
16 | export const errorWrapper = (fn): any => {
17 | return async (req, res, next): Promise => {
18 | try {
19 | if (!req.user) throw new CustomError(401, ERROR.SESSION_EXPIRE);
20 | await fn(req, res, next);
21 | } catch (error) {
22 | next(errorHandler(error));
23 | }
24 | };
25 | };
26 |
--------------------------------------------------------------------------------
/server/src/utils/pipe.ts:
--------------------------------------------------------------------------------
1 | const pipe = (...functions): Function => {
2 | return (first_value) => functions.reduce((prev_value, func) => func(prev_value), first_value);
3 | };
4 |
5 | export default pipe;
6 |
--------------------------------------------------------------------------------
/server/src/utils/time.ts:
--------------------------------------------------------------------------------
1 | export const getTimeString = (): string => {
2 | const now = new Date();
3 | let part = 'AM';
4 | let nowHour = now.getHours();
5 | let nowMinute = now.getMinutes().toString().padStart(2, '0');
6 | if (nowHour > 12) {
7 | nowHour -= 12;
8 | part = 'PM';
9 | }
10 | return `${nowHour}:${nowMinute} ${part}`;
11 | };
12 |
--------------------------------------------------------------------------------
/server/src/utils/transaction.ts:
--------------------------------------------------------------------------------
1 | import { startSession } from 'mongoose';
2 |
3 | export const transaction = async (fn: Function): Promise => {
4 | const session = await startSession();
5 | await session.withTransaction(async () => {
6 | await fn(session);
7 | });
8 | session.endSession();
9 | };
10 |
--------------------------------------------------------------------------------
/server/src/utils/userCount.ts:
--------------------------------------------------------------------------------
1 | export const userCount: string[] = [
2 | 'chatCount',
3 | 'hookCount',
4 | 'pollCount',
5 | 'closeupCount',
6 | 'dieCount',
7 | 'cheersCount',
8 | 'starterCount',
9 | 'totalSeconds',
10 | ];
11 |
--------------------------------------------------------------------------------
/server/tsconfig-paths-bootstrap.js:
--------------------------------------------------------------------------------
1 | const tsConfig = require('./tsconfig.json');
2 | const tsConfigPaths = require('tsconfig-paths');
3 |
4 | tsConfigPaths.register({
5 | baseUrl: tsConfig.compilerOptions.outDir,
6 | paths: tsConfig.compilerOptions.paths,
7 | });
8 |
--------------------------------------------------------------------------------
/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "esModuleInterop": true,
5 | "target": "es6",
6 | "noImplicitAny": false,
7 | "moduleResolution": "node",
8 | "sourceMap": true,
9 | "removeComments": true,
10 | "outDir": "./build",
11 | "baseUrl": "./src",
12 | "paths": {
13 | "@src/*": ["./*"],
14 | "@api/*": ["./api/*"],
15 | "@controller/*": ["./controller/*"],
16 | "@service/*": ["./service/*"],
17 | "@loader/*": ["./loader/*"],
18 | "@models/*": ["./models/*"],
19 | "@utils/*": ["./utils/*"],
20 | "@ts-types": ["./ts-types/*"]
21 | }
22 | },
23 | "include": ["src"],
24 | "exclude": ["node_modules"]
25 | }
26 |
--------------------------------------------------------------------------------