├── client ├── src │ ├── hooks │ │ ├── index.js │ │ ├── useIsMobile.js │ │ ├── useInnerWidth.js │ │ ├── useToast.js │ │ └── useShiftingToWhichView.js │ ├── assets │ │ ├── cat.gif │ │ ├── error.png │ │ ├── exit.png │ │ ├── share.png │ │ ├── timer.png │ │ ├── success.png │ │ ├── warning.png │ │ ├── background.png │ │ ├── exit-black.png │ │ ├── information.png │ │ ├── main-logo-large.png │ │ └── main-logo-large-old.png │ ├── actions │ │ ├── index.js │ │ ├── game.js │ │ └── global.js │ ├── contexts │ │ ├── DispatchContext.js │ │ ├── GlobalContext.js │ │ └── index.js │ ├── store │ │ ├── index.js │ │ ├── globalState.js │ │ └── globalReducer.js │ ├── presentation │ │ ├── pages │ │ │ ├── Game │ │ │ │ ├── store │ │ │ │ │ ├── index.js │ │ │ │ │ ├── gameState.js │ │ │ │ │ └── gameReducer.js │ │ │ │ ├── style.js │ │ │ │ └── presenter.jsx │ │ │ ├── index.js │ │ │ ├── Ranking │ │ │ │ ├── style.js │ │ │ │ └── index.js │ │ │ └── MainPage.jsx │ │ ├── components │ │ │ ├── Logo.jsx │ │ │ ├── Buttons │ │ │ │ ├── style.js │ │ │ │ ├── index.js │ │ │ │ ├── ReadyButton.jsx │ │ │ │ ├── ExitButton.jsx │ │ │ │ ├── SendButton.jsx │ │ │ │ ├── CandidateButton.jsx │ │ │ │ ├── MenuButton.jsx │ │ │ │ ├── MoreButton.jsx │ │ │ │ └── ShareUrlButton.jsx │ │ │ ├── UnderlinedSpace.jsx │ │ │ ├── Message.jsx │ │ │ ├── UnderlinedLetter.jsx │ │ │ ├── Slogan.jsx │ │ │ ├── CenterTimer.jsx │ │ │ ├── Nickname.jsx │ │ │ ├── Title.jsx │ │ │ ├── Description.jsx │ │ │ ├── StreamerVideo.jsx │ │ │ ├── Timer.jsx │ │ │ ├── RankingRow.jsx │ │ │ ├── index.js │ │ │ ├── ChattingRow.jsx │ │ │ ├── ToastContent.jsx │ │ │ ├── QuizDisplay.jsx │ │ │ ├── MessageInput.jsx │ │ │ ├── TextInput.jsx │ │ │ ├── GameMessageBox.jsx │ │ │ ├── ChattingWindow.jsx │ │ │ ├── ScoreBoardScoreRow.jsx │ │ │ ├── Toast.jsx │ │ │ ├── InputWindow.jsx │ │ │ ├── RankPodium.jsx │ │ │ └── PlayerProfile.jsx │ │ └── containers │ │ │ ├── MainTitle.jsx │ │ │ ├── StreamingPanel │ │ │ ├── style.js │ │ │ ├── presenter.js │ │ │ └── index.js │ │ │ ├── index.js │ │ │ ├── MobileChattingPanel.jsx │ │ │ ├── WordCandidates.jsx │ │ │ ├── Introduction.jsx │ │ │ ├── ScoreBoard.jsx │ │ │ ├── ChattingPanel.jsx │ │ │ ├── TopRankPanel.jsx │ │ │ ├── BottomRankPanel.jsx │ │ │ ├── Menu.jsx │ │ │ └── PlayerPanel.jsx │ ├── constants │ │ ├── socket.js │ │ ├── path.js │ │ ├── button.js │ │ ├── inputConstraints.js │ │ ├── responsiveView.js │ │ ├── ranking.js │ │ ├── browser.js │ │ ├── chatting.js │ │ ├── timer.js │ │ ├── webRTC.js │ │ ├── game.js │ │ ├── message.js │ │ ├── toast.js │ │ ├── styleColors.js │ │ ├── actionTypes.js │ │ └── events.js │ ├── App.test.jsx │ ├── utils │ │ ├── index.js │ │ ├── copyUrlToClipboard.js │ │ ├── makeViewPlayerList.js │ │ ├── createShareUrlButtonClickHandler.js │ │ └── browserLocalStorage.js │ ├── index.js │ ├── api │ │ └── index.js │ ├── App.jsx │ ├── __test__ │ │ └── Game.test.js │ ├── service │ │ ├── Timer.js │ │ └── ChattingManager.js │ └── damodata.js ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── style.css │ ├── manifest.json │ ├── reset.css │ └── index.html ├── config │ ├── jest │ │ ├── cssTransform.js │ │ └── fileTransform.js │ ├── pnpTs.js │ ├── paths.js │ ├── env.js │ └── modules.js ├── .eslintrc.json ├── scripts │ └── test.js └── package.json ├── server ├── app.test.js ├── constants │ ├── timer.js │ ├── player.js │ ├── socket.js │ ├── gameStatus.js │ ├── database.js │ ├── gameRule.js │ ├── api.js │ └── event.js ├── service │ ├── io.js │ ├── game │ │ ├── eventHandlers │ │ │ ├── askSocketIdHandler.js │ │ │ ├── index.js │ │ │ ├── connectPeerHandler.js │ │ │ ├── sendReadyHandler.js │ │ │ ├── selectQuizHandler.js │ │ │ ├── matchHandler.js │ │ │ ├── disconnectingHandler.js │ │ │ └── sendChattingMessageHandler.js │ │ ├── registerGameEvents.js │ │ ├── models │ │ │ ├── Player.js │ │ │ └── Timer.js │ │ └── controllers │ │ │ └── roomController.js │ ├── signaling │ │ ├── eventHandlers │ │ │ ├── index.js │ │ │ ├── sendDescriptionHandler.js │ │ │ └── sendIceCandidateHandler.js │ │ └── registerSignalingEvents.js │ └── index.js ├── databaseFiles │ ├── repositories │ │ ├── index.js │ │ ├── utils.js │ │ ├── QuizRepository.js │ │ └── RankingRepository.js │ ├── databaseModels │ │ ├── index.js │ │ ├── seeder │ │ │ ├── scripts │ │ │ │ └── seedQuizzes.js │ │ │ ├── quizzes.csv │ │ │ ├── index.js │ │ │ └── ranking.csv │ │ ├── Quiz.js │ │ └── Ranking.js │ └── connection.js ├── utils │ ├── colorGenerator.js │ ├── chatUtils.js │ └── getCurrentTime.js ├── .eslintrc.json ├── app.js ├── package.json ├── routes │ └── api.js ├── bin │ └── www └── __test__ │ └── GameManager.test.js ├── mysql.cnf ├── .github └── PULL_REQUEST_TEMPLATE.md ├── .prettierrc ├── sample.env ├── docker-compose-dev.yml ├── .vscode └── settings.json ├── package.json ├── docker-compose.yml ├── nginx └── default.conf.template └── .gitignore /client/src/hooks/index.js: -------------------------------------------------------------------------------- 1 | export { default as useToast } from './useToast'; 2 | -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /server/app.test.js: -------------------------------------------------------------------------------- 1 | it('For initial testing setup', () => { 2 | expect(true).toBeTruthy(); 3 | }); 4 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connect-foundation/2019-09/HEAD/client/public/favicon.ico -------------------------------------------------------------------------------- /client/src/assets/cat.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connect-foundation/2019-09/HEAD/client/src/assets/cat.gif -------------------------------------------------------------------------------- /client/src/assets/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connect-foundation/2019-09/HEAD/client/src/assets/error.png -------------------------------------------------------------------------------- /client/src/assets/exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connect-foundation/2019-09/HEAD/client/src/assets/exit.png -------------------------------------------------------------------------------- /client/src/assets/share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connect-foundation/2019-09/HEAD/client/src/assets/share.png -------------------------------------------------------------------------------- /client/src/assets/timer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connect-foundation/2019-09/HEAD/client/src/assets/timer.png -------------------------------------------------------------------------------- /client/src/assets/success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connect-foundation/2019-09/HEAD/client/src/assets/success.png -------------------------------------------------------------------------------- /client/src/assets/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connect-foundation/2019-09/HEAD/client/src/assets/warning.png -------------------------------------------------------------------------------- /client/src/assets/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connect-foundation/2019-09/HEAD/client/src/assets/background.png -------------------------------------------------------------------------------- /client/src/assets/exit-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connect-foundation/2019-09/HEAD/client/src/assets/exit-black.png -------------------------------------------------------------------------------- /client/src/assets/information.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connect-foundation/2019-09/HEAD/client/src/assets/information.png -------------------------------------------------------------------------------- /server/constants/timer.js: -------------------------------------------------------------------------------- 1 | const TIMER = { 2 | ONE_SECOND_IN_MILLISECONDS: 1000, 3 | }; 4 | 5 | module.exports = TIMER; 6 | -------------------------------------------------------------------------------- /client/src/assets/main-logo-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connect-foundation/2019-09/HEAD/client/src/assets/main-logo-large.png -------------------------------------------------------------------------------- /mysql.cnf: -------------------------------------------------------------------------------- 1 | [mysqld] 2 | character-set-server = utf8mb4 3 | collation-server = utf8mb4_unicode_ci 4 | skip-character-set-client-handshake -------------------------------------------------------------------------------- /client/src/actions/index.js: -------------------------------------------------------------------------------- 1 | import global from './global'; 2 | import game from './game'; 3 | 4 | export default { ...global, ...game }; 5 | -------------------------------------------------------------------------------- /server/constants/player.js: -------------------------------------------------------------------------------- 1 | const PLAYER = { 2 | VIEWER: 'viewer', 3 | STREAMER: 'streamer', 4 | }; 5 | 6 | module.exports = PLAYER; 7 | -------------------------------------------------------------------------------- /server/constants/socket.js: -------------------------------------------------------------------------------- 1 | const SOCKET = { 2 | PING_INTERVAL: 1000, 3 | PING_TIMEOUT: 5000, 4 | }; 5 | 6 | module.exports = SOCKET; 7 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Pull Request 2 | 3 | ## What 4 | 5 | - 6 | 7 | ## Why 8 | 9 | - 10 | 11 | ## Etc. 12 | 13 | - 14 | -------------------------------------------------------------------------------- /client/src/assets/main-logo-large-old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connect-foundation/2019-09/HEAD/client/src/assets/main-logo-large-old.png -------------------------------------------------------------------------------- /server/service/io.js: -------------------------------------------------------------------------------- 1 | const io = require('socket.io')(); 2 | 3 | const { rooms } = io.sockets.adapter; 4 | 5 | module.exports = { io, rooms }; 6 | -------------------------------------------------------------------------------- /client/src/contexts/DispatchContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Context = React.createContext(); 4 | 5 | export default Context; 6 | -------------------------------------------------------------------------------- /client/src/store/index.js: -------------------------------------------------------------------------------- 1 | export { default as globalReducer } from './globalReducer'; 2 | export { default as globalState } from './globalState'; 3 | -------------------------------------------------------------------------------- /client/src/contexts/GlobalContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const GlobalContext = React.createContext(); 4 | 5 | export default GlobalContext; 6 | -------------------------------------------------------------------------------- /client/src/contexts/index.js: -------------------------------------------------------------------------------- 1 | export { default as DispatchContext } from './DispatchContext'; 2 | export { default as GlobalContext } from './GlobalContext'; 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true, 4 | "useTabs": false, 5 | "tabWidth": 2, 6 | "trailingComma": "all", 7 | "printWidth": 80 8 | } 9 | -------------------------------------------------------------------------------- /client/src/presentation/pages/Game/store/index.js: -------------------------------------------------------------------------------- 1 | export { default as gameReducer } from './gameReducer'; 2 | export { default as gameState } from './gameState'; 3 | -------------------------------------------------------------------------------- /client/src/constants/socket.js: -------------------------------------------------------------------------------- 1 | const SOCKETIO_SERVER_URL = 2 | process.env.NODE_ENV === 'development' ? 'localhost:3001' : ''; 3 | 4 | export { SOCKETIO_SERVER_URL }; 5 | -------------------------------------------------------------------------------- /client/src/constants/path.js: -------------------------------------------------------------------------------- 1 | const LINK_PATH = { 2 | GAME_PAGE: '/game', 3 | RANKING_PAGE: '/ranking', 4 | MAIN_PAGE: '/', 5 | }; 6 | 7 | export default LINK_PATH; 8 | -------------------------------------------------------------------------------- /client/src/presentation/pages/index.js: -------------------------------------------------------------------------------- 1 | export { default as MainPage } from './MainPage'; 2 | export { default as Game } from './Game'; 3 | export { default as Ranking } from './Ranking'; 4 | -------------------------------------------------------------------------------- /client/src/constants/button.js: -------------------------------------------------------------------------------- 1 | const PLAY_WITH_FRIENDS_BUTTON_TEXT = 'Play With Friends'; 2 | 3 | const SHARE_URL_BUTTON_TEXT = 'Copy URL'; 4 | 5 | export { PLAY_WITH_FRIENDS_BUTTON_TEXT, SHARE_URL_BUTTON_TEXT }; 6 | -------------------------------------------------------------------------------- /client/src/constants/inputConstraints.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_TEXT_INPUT_MAX_LENGTH = 100; 2 | const NICKNAME_LENGTH = 8; 3 | const MAX_CHAT_LENGTH = 40; 4 | 5 | export { DEFAULT_TEXT_INPUT_MAX_LENGTH, NICKNAME_LENGTH, MAX_CHAT_LENGTH }; 6 | -------------------------------------------------------------------------------- /server/databaseFiles/repositories/index.js: -------------------------------------------------------------------------------- 1 | const QuizRepository = require('./QuizRepository'); 2 | const RankingRepository = require('./RankingRepository'); 3 | 4 | module.exports = { 5 | QuizRepository, 6 | RankingRepository, 7 | }; 8 | -------------------------------------------------------------------------------- /client/src/App.test.jsx: -------------------------------------------------------------------------------- 1 | import { cleanup } from '@testing-library/react'; 2 | import '@testing-library/jest-dom/extend-expect'; 3 | 4 | afterEach(cleanup); 5 | 6 | it('For initial testing setup', () => { 7 | expect(true).toBeTruthy(); 8 | }); 9 | -------------------------------------------------------------------------------- /server/databaseFiles/databaseModels/index.js: -------------------------------------------------------------------------------- 1 | const connection = require('../connection'); 2 | const Ranking = require('./Ranking'); 3 | const Quiz = require('./Quiz'); 4 | 5 | module.exports = { 6 | connection, 7 | Ranking, 8 | Quiz, 9 | }; 10 | -------------------------------------------------------------------------------- /server/databaseFiles/databaseModels/seeder/scripts/seedQuizzes.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | require('dotenv').config({ 3 | path: `${path.join(__dirname)}/../../../../../.env`, 4 | }); 5 | 6 | const { initializeQuizzes } = require('../index'); 7 | 8 | initializeQuizzes(); 9 | -------------------------------------------------------------------------------- /server/service/game/eventHandlers/askSocketIdHandler.js: -------------------------------------------------------------------------------- 1 | const { SEND_SOCKET_ID } = require('../../../constants/event'); 2 | 3 | const askSocketIdHandler = socket => { 4 | socket.emit(SEND_SOCKET_ID, { socketId: socket.id }); 5 | }; 6 | 7 | module.exports = askSocketIdHandler; 8 | -------------------------------------------------------------------------------- /server/service/signaling/eventHandlers/index.js: -------------------------------------------------------------------------------- 1 | const sendIceCandidateHandler = require('./sendIceCandidateHandler'); 2 | const sendDescriptionHandler = require('./sendDescriptionHandler'); 3 | 4 | module.exports = { 5 | sendIceCandidateHandler, 6 | sendDescriptionHandler, 7 | }; 8 | -------------------------------------------------------------------------------- /server/constants/gameStatus.js: -------------------------------------------------------------------------------- 1 | const GAME_STATUS = { 2 | WAITING: 'waiting', 3 | PLAYING: 'playing', 4 | ENDING: 'ending', 5 | CONNECTING: 'connecting', 6 | INITIALIZING: 'initializing', 7 | SCORE_SHARING: 'scoreSharing', 8 | }; 9 | 10 | module.exports = GAME_STATUS; 11 | -------------------------------------------------------------------------------- /server/databaseFiles/repositories/utils.js: -------------------------------------------------------------------------------- 1 | const convertSequelizeArrayData = sequelizeArrayData => { 2 | const convertedData = sequelizeArrayData.map(data => { 3 | return data.dataValues; 4 | }); 5 | return convertedData; 6 | }; 7 | 8 | module.exports = { convertSequelizeArrayData }; 9 | -------------------------------------------------------------------------------- /sample.env: -------------------------------------------------------------------------------- 1 | NGINX_SERVER_NAME= 2 | NGINX_HOST_REQUEST_URI=$host$request_uri 3 | HOST=$host 4 | HTTP_UPGRADE=$http_upgrade 5 | MYSQL_ROOT_PASSWORD= 6 | MYSQL_DATABASE= 7 | MYSQL_USER= 8 | MYSQL_PASSWORD= 9 | DATABASE_HOST= 10 | DATABASE_DIALECT=mysql 11 | DATABASE_CHARSET=utf8mb4 12 | DATABASE_COLLATE=utf8mb4_unicode_ci -------------------------------------------------------------------------------- /client/public/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'CookieRunOTF-Bold'; 3 | src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_twelve@1.0/CookieRunOTF-Bold00.woff') 4 | format('woff'); 5 | font-weight: normal; 6 | font-style: normal; 7 | } 8 | * { 9 | font-family: 'CookieRunOTF-Bold' !important; 10 | } 11 | -------------------------------------------------------------------------------- /client/src/utils/index.js: -------------------------------------------------------------------------------- 1 | export { default as browserLocalStorage } from './browserLocalStorage'; 2 | export { default as makeViewPlayerList } from './makeViewPlayerList'; 3 | export { default as copyUrlToClipoard } from './copyUrlToClipboard'; 4 | export { default as createShareUrlButtonClickHandler } from './createShareUrlButtonClickHandler'; 5 | -------------------------------------------------------------------------------- /docker-compose-dev.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | mysql: 5 | image: mysql:5.7 6 | container_name: mysql 7 | volumes: 8 | - ./mysql.cnf:/etc/mysql/conf.d/custom.cnf 9 | - ./mysql/data:/var/lib/mysql 10 | restart: always 11 | env_file: 12 | - ./.env 13 | ports: 14 | - '3306:3306' 15 | -------------------------------------------------------------------------------- /server/constants/database.js: -------------------------------------------------------------------------------- 1 | const DATABASE = { 2 | MODEL_QUIZ: 'quiz', 3 | MODEL_RANKING: 'ranking', 4 | RANKING_COUNTS: 20, 5 | MAXIMUM_CONNECTION_COUNT: 150, 6 | MINIMUM_CONNECTION_COUNT: 0, 7 | MAXIMUM_IDLE_TIME: 10000, 8 | MAXIMUM_WAITING_TIME: 30000, 9 | INVALID_DATE: 'Invalid Date', 10 | }; 11 | 12 | module.exports = DATABASE; 13 | -------------------------------------------------------------------------------- /server/service/signaling/registerSignalingEvents.js: -------------------------------------------------------------------------------- 1 | const { 2 | sendDescriptionHandler, 3 | sendIceCandidateHandler, 4 | } = require('./eventHandlers'); 5 | 6 | module.exports = socket => { 7 | socket.on('sendDescription', sendDescriptionHandler.bind(null, socket)); 8 | socket.on('sendIceCandidate', sendIceCandidateHandler.bind(null, socket)); 9 | }; 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.workingDirectories": ["server"], 3 | "eslint.autoFixOnSave": true, 4 | "javascript.format.enable": false, 5 | "editor.formatOnSave": true, 6 | "editor.tabSize": 2, 7 | "editor.insertSpaces": true, 8 | "editor.detectIndentation": false, 9 | "editor.codeActionsOnSave": { 10 | "source.fixAll.eslint": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /server/service/signaling/eventHandlers/sendDescriptionHandler.js: -------------------------------------------------------------------------------- 1 | const { io } = require('../../io'); 2 | const { SEND_DESCRIPTION } = require('../../../constants/event'); 3 | 4 | const sendDescriptionHandler = (socket, { target, description }) => { 5 | io.to(target).emit(SEND_DESCRIPTION, { target: socket.id, description }); 6 | }; 7 | 8 | module.exports = sendDescriptionHandler; 9 | -------------------------------------------------------------------------------- /client/src/constants/responsiveView.js: -------------------------------------------------------------------------------- 1 | const MOBILE_PANEL_HEIGHT = '90%'; 2 | const MOBILE_VIEW = 'mobile'; 3 | const MOBILE_VIEW_BREAKPOINT = 600; 4 | const DESKTOP_VIEW = 'desktop'; 5 | const MOBILE_ONE_REM_IN_PIXELS = 6; 6 | 7 | export { 8 | MOBILE_PANEL_HEIGHT, 9 | MOBILE_VIEW, 10 | MOBILE_VIEW_BREAKPOINT, 11 | DESKTOP_VIEW, 12 | MOBILE_ONE_REM_IN_PIXELS, 13 | }; 14 | -------------------------------------------------------------------------------- /server/service/signaling/eventHandlers/sendIceCandidateHandler.js: -------------------------------------------------------------------------------- 1 | const { io } = require('../../io'); 2 | const { SEND_ICE_CANDIDATE } = require('../../../constants/event'); 3 | 4 | const sendIceCandidateHandler = (socket, { target, iceCandidate }) => { 5 | io.to(target).emit(SEND_ICE_CANDIDATE, { target: socket.id, iceCandidate }); 6 | }; 7 | 8 | module.exports = sendIceCandidateHandler; 9 | -------------------------------------------------------------------------------- /client/src/constants/ranking.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_SCORE = '0'; 2 | const DEFAULT_NICKNAMES = { 3 | FIRST_PLACE: '1등', 4 | SECOND_PLACEC: '2등', 5 | THIRD_PLACE: '3등', 6 | }; 7 | const FIRST_PLACE = '1'; 8 | const SECOND_PLACE = '2'; 9 | const THIRD_PLACE = '3'; 10 | 11 | export { 12 | DEFAULT_SCORE, 13 | DEFAULT_NICKNAMES, 14 | FIRST_PLACE, 15 | SECOND_PLACE, 16 | THIRD_PLACE, 17 | }; 18 | -------------------------------------------------------------------------------- /client/src/constants/browser.js: -------------------------------------------------------------------------------- 1 | const INPUT_TAG = 'input'; 2 | const BROWSER_COPY_COMMAND = 'copy'; 3 | const ENTER_KEYCODE = 13; 4 | const LOCALSTORAGE_NICKNAME_KEY = 'trycatch-nickname'; 5 | const LOCALSTORAGE_DEFAULT_NICKNAME = 'Anonymous'; 6 | export { 7 | INPUT_TAG, 8 | BROWSER_COPY_COMMAND, 9 | ENTER_KEYCODE, 10 | LOCALSTORAGE_NICKNAME_KEY, 11 | LOCALSTORAGE_DEFAULT_NICKNAME, 12 | }; 13 | -------------------------------------------------------------------------------- /server/utils/colorGenerator.js: -------------------------------------------------------------------------------- 1 | const getRandomColor = () => { 2 | const colorList = [ 3 | '#32ff7e', 4 | '#67e6dc', 5 | '#3d3d3d', 6 | '#c56cf0', 7 | '#ff3838', 8 | '#17c0eb', 9 | '#ffb8b8', 10 | '#ff9f1a', 11 | '#7158e2', 12 | ]; 13 | return colorList[Math.round(Math.random() * (colorList.length - 1))]; 14 | }; 15 | 16 | module.exports = { getRandomColor }; 17 | -------------------------------------------------------------------------------- /client/config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /server/service/index.js: -------------------------------------------------------------------------------- 1 | const registerSignalingEvents = require('./signaling/registerSignalingEvents'); 2 | const registerGameEvents = require('./game/registerGameEvents'); 3 | const { io } = require('./io'); 4 | const { CONNECTION } = require('../constants/event'); 5 | 6 | io.on(CONNECTION, socket => { 7 | registerSignalingEvents(socket); 8 | registerGameEvents(socket); 9 | }); 10 | 11 | module.exports = io; 12 | -------------------------------------------------------------------------------- /server/utils/chatUtils.js: -------------------------------------------------------------------------------- 1 | const { MAX_CHAT_LENGTH } = require('../constants/gameRule'); 2 | 3 | const sliceChatToMaxLength = chat => { 4 | return chat.slice(0, MAX_CHAT_LENGTH); 5 | }; 6 | 7 | const processChatWithSystemRule = chat => { 8 | const trimmedChat = chat.trim(); 9 | if (!trimmedChat) return trimmedChat; 10 | return sliceChatToMaxLength(trimmedChat); 11 | }; 12 | 13 | module.exports = { processChatWithSystemRule }; 14 | -------------------------------------------------------------------------------- /client/src/constants/chatting.js: -------------------------------------------------------------------------------- 1 | const WELCOME_MESSAGE = (isRoomPrivate, minutes, seconds) => ({ 2 | nickname: '안내', 3 | message: `채팅방에 입장하였습니다. ${ 4 | isRoomPrivate ? `${minutes}분` : `${seconds}초` 5 | } 안에 READY버튼을 눌러주세요.🙌`, 6 | }); 7 | 8 | const DEFAULT_NICKNAME = 'Guest'; 9 | 10 | const CHATTING_INPUT_PLACEHOLER = 'Please enter a message.'; 11 | 12 | export { WELCOME_MESSAGE, DEFAULT_NICKNAME, CHATTING_INPUT_PLACEHOLER }; 13 | -------------------------------------------------------------------------------- /client/src/presentation/pages/Game/store/gameState.js: -------------------------------------------------------------------------------- 1 | import { MOBILE_VIEW_BREAKPOINT } from '../../../../constants/responsiveView'; 2 | 3 | const initialIsMobile = window.innerWidth < MOBILE_VIEW_BREAKPOINT; 4 | 5 | const gameInitialState = { 6 | mobileChattingPanelVisibility: initialIsMobile, 7 | isPlayerListVisible: !initialIsMobile, 8 | gamePageRootHeight: window.innerHeight, 9 | }; 10 | 11 | export default gameInitialState; 12 | -------------------------------------------------------------------------------- /client/src/presentation/components/Logo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import PropTypes from 'prop-types'; 4 | 5 | const LogoImage = styled.img` 6 | width: 100%; 7 | height: auto; 8 | `; 9 | 10 | const Logo = ({ logoSrc }) => { 11 | return ; 12 | }; 13 | 14 | Logo.propTypes = { 15 | logoSrc: PropTypes.string.isRequired, 16 | }; 17 | 18 | export default Logo; 19 | -------------------------------------------------------------------------------- /client/src/presentation/components/Buttons/style.js: -------------------------------------------------------------------------------- 1 | import styleColors from '../../../constants/styleColors'; 2 | 3 | const buttonStyle = { 4 | backgroundColor: styleColors.THEME_COLOR, 5 | border: 0, 6 | borderRadius: 3, 7 | color: styleColors.BASE_WHITE_COLOR, 8 | padding: '0 30px', 9 | fontSize: '1.5rem', 10 | '&:hover': { 11 | backgroundColor: styleColors.THEME_HOVER_COLOR, 12 | }, 13 | }; 14 | 15 | export default buttonStyle; 16 | -------------------------------------------------------------------------------- /client/src/presentation/components/Buttons/index.js: -------------------------------------------------------------------------------- 1 | export { default as MenuButton } from './MenuButton'; 2 | export { default as SendButton } from './SendButton'; 3 | export { default as ReadyButton } from './ReadyButton'; 4 | export { default as ExitButton } from './ExitButton'; 5 | export { default as CandidateButton } from './CandidateButton'; 6 | export { default as MoreButton } from './MoreButton'; 7 | export { default as ShareUrlButton } from './ShareUrlButton'; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trycatch", 3 | "version": "0.0.1", 4 | "main": "index.js", 5 | "repository": "https://github.com/connect-foundation/2019-09.git", 6 | "author": "OriginJang ", 7 | "license": "MIT", 8 | "private": true, 9 | "workspaces": [ 10 | "server", 11 | "client" 12 | ], 13 | "scripts": { 14 | "test": "yarn workspace client test && yarn workspace server test", 15 | "dev-start": "nodemon server/bin/www" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /server/constants/gameRule.js: -------------------------------------------------------------------------------- 1 | const GAME_RULE = { 2 | INITIAL_ROUND: 1, 3 | MIN_PLAYER_COUNT: 2, 4 | MAX_PLAYER_COUNT: 4, 5 | ONE_SET_SECONDS: 30, 6 | MAX_ROUND_NUMBER: 1, 7 | MAX_QUIZ_SELECTION_WAITING_SECONDS: 10, 8 | MAX_PEER_CONNECTION_WAITING_SECONDS: 10, 9 | QUIZ_CANDIDATES_COUNT: 3, 10 | SECONDS_BETWEEN_SETS: 5, 11 | SECONDS_AFTER_GAME_END: 10, 12 | MAX_CHAT_LENGTH: 40, 13 | NICKNAME_LENGTH: 8, 14 | DEFAULT_QUIZ: '', 15 | }; 16 | 17 | module.exports = GAME_RULE; 18 | -------------------------------------------------------------------------------- /client/src/hooks/useIsMobile.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import useInnerWidth from './useInnerWidth'; 3 | 4 | const useIsMobile = mobileViewBreakpoint => { 5 | const innerWidth = useInnerWidth(); 6 | const [isMobile, setIsMobile] = useState( 7 | window.innerWidth < mobileViewBreakpoint, 8 | ); 9 | useEffect(() => { 10 | setIsMobile(innerWidth < mobileViewBreakpoint); 11 | }, [innerWidth]); 12 | return isMobile; 13 | }; 14 | 15 | export default useIsMobile; 16 | -------------------------------------------------------------------------------- /server/constants/api.js: -------------------------------------------------------------------------------- 1 | const API = { 2 | ERROR_500_DATABASE: { 3 | error: { 4 | errors: [ 5 | { 6 | domain: 'Database', 7 | reason: 'Internal error', 8 | message: 'Database failed to send data.', 9 | }, 10 | ], 11 | code: 500, 12 | message: 'Database failed to send data.', 13 | }, 14 | }, 15 | PATH: { 16 | RANKING: '/ranking', 17 | RANKING_INFORMATION: '/ranking/information', 18 | }, 19 | }; 20 | 21 | module.exports = API; 22 | -------------------------------------------------------------------------------- /client/src/constants/timer.js: -------------------------------------------------------------------------------- 1 | const ONE_SECOND = 1000; 2 | const DEFAULT_INACTIVE_PLAYER_BAN_TIME = 20; 3 | const PRIVATE_ROOM_INACTIVE_PLAYER_BAN_TIME = 300; 4 | const PRIVATE_ROOM_INACTIVE_PLAYER_BAN_TIME_IN_MINUTE = 5 | PRIVATE_ROOM_INACTIVE_PLAYER_BAN_TIME / 60; 6 | const CLIPBOARD_COPY_MESSAGE_DURATION = 1; 7 | export { 8 | ONE_SECOND, 9 | DEFAULT_INACTIVE_PLAYER_BAN_TIME, 10 | PRIVATE_ROOM_INACTIVE_PLAYER_BAN_TIME, 11 | PRIVATE_ROOM_INACTIVE_PLAYER_BAN_TIME_IN_MINUTE, 12 | CLIPBOARD_COPY_MESSAGE_DURATION, 13 | }; 14 | -------------------------------------------------------------------------------- /client/src/utils/copyUrlToClipboard.js: -------------------------------------------------------------------------------- 1 | import { INPUT_TAG, BROWSER_COPY_COMMAND } from '../constants/browser'; 2 | 3 | const copyUrlToClipboard = () => { 4 | const temporaryInput = document.createElement(INPUT_TAG); 5 | const urlToCopy = window.location.href; 6 | document.body.appendChild(temporaryInput); 7 | temporaryInput.value = urlToCopy; 8 | temporaryInput.select(); 9 | document.execCommand(BROWSER_COPY_COMMAND); 10 | document.body.removeChild(temporaryInput); 11 | }; 12 | 13 | export default copyUrlToClipboard; 14 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | // eslint-disable-next-line import/no-cycle 4 | import styled from 'styled-components'; 5 | import App from './App'; 6 | import backgroundImageSource from './assets/background.png'; 7 | 8 | const AppBackgroundWrapper = styled.div` 9 | height: 100%; 10 | background-image: url(${backgroundImageSource}); 11 | `; 12 | 13 | ReactDOM.render( 14 | 15 | 16 | , 17 | document.getElementById('root'), 18 | ); 19 | -------------------------------------------------------------------------------- /client/src/constants/webRTC.js: -------------------------------------------------------------------------------- 1 | const PEER_CONNECTION_CONFIG = { 2 | iceServers: [ 3 | { urls: 'stun:stun.services.mozilla.com' }, 4 | { urls: 'stun:stun.l.google.com:19302' }, 5 | { 6 | url: 'turn:numb.viagenie.ca', 7 | credential: 'muazkh', 8 | username: 'webrtc@live.com', 9 | }, 10 | ], 11 | }; 12 | 13 | const MEDIA_CONSTRAINTS = { 14 | video: true, 15 | audio: false, 16 | }; 17 | 18 | const DESCRIPTION_TYPE = { 19 | ANSWER: 'answer', 20 | }; 21 | 22 | export { PEER_CONNECTION_CONFIG, MEDIA_CONSTRAINTS, DESCRIPTION_TYPE }; 23 | -------------------------------------------------------------------------------- /client/src/presentation/components/UnderlinedSpace.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles } from '@material-ui/core/styles'; 3 | 4 | const UnderlinedSpace = () => { 5 | const useStyles = makeStyles(() => ({ 6 | underlinedEmpty: { 7 | textDecoration: 'underline', 8 | fontSize: '2rem', 9 | fontWeight: 'bold', 10 | minWidth: '2rem', 11 | }, 12 | })); 13 | 14 | const classes = useStyles(); 15 | 16 | return   ; 17 | }; 18 | 19 | export default UnderlinedSpace; 20 | -------------------------------------------------------------------------------- /server/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true 7 | }, 8 | "extends": ["airbnb-base", "prettier"], 9 | "globals": { 10 | "Atomics": "readonly", 11 | "SharedArrayBuffer": "readonly" 12 | }, 13 | "parserOptions": { 14 | "ecmaVersion": 2018 15 | }, 16 | "rules": { 17 | "linebreak-style": 0, 18 | "no-restricted-globals": 0, 19 | "class-methods-use-this": 0, 20 | "no-plusplus": 0, 21 | "no-undef": 0, 22 | "no-param-reassign": 0 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /client/src/presentation/containers/MainTitle.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Logo } from '../components'; 4 | import logoSrc from '../../assets/main-logo-large.png'; 5 | 6 | const TitleWrapper = styled.div` 7 | margin-top: 2rem; 8 | heigth: auto; 9 | width: 100%; 10 | padding: 0 30px; 11 | box-sizing: border-box; 12 | `; 13 | 14 | const MainTitle = () => { 15 | return ( 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default MainTitle; 23 | -------------------------------------------------------------------------------- /server/databaseFiles/databaseModels/seeder/quizzes.csv: -------------------------------------------------------------------------------- 1 | word 2 | 이 3 | 아이 4 | 오이 5 | 우유 6 | 코 7 | 모자 8 | 구두 9 | 나무 10 | 바지 11 | 포도 12 | 사자 13 | 새 14 | 배 15 | 베개 16 | 수박 17 | 눈 18 | 옷 19 | 달 20 | 밤 21 | 공 22 | 빵 23 | 자음 24 | 모음 25 | 감 26 | 강 27 | 밥 28 | 산 29 | 사전 30 | 어머니 31 | 아버지 32 | 한국 33 | 일본 34 | 몽골 35 | 중국 36 | 베트남 37 | 태국 38 | 사람 39 | 국적 40 | 문법 41 | 연습 42 | 직업 43 | 학습 44 | 선생님 45 | 회사원 46 | 의사 47 | 경찰 48 | 공무원 49 | 이름 50 | 동작 51 | 공부 52 | 운동 53 | 수면 54 | 전화 55 | 이야기 56 | 캐러비안의해적 57 | 학교 58 | 오렌지 59 | 명함 60 | 커피 61 | 주스 62 | 스타크래프트 63 | 니모를찾아서 64 | 군대 65 | 병장 66 | 상병 67 | 일병 68 | 이병 69 | 예비군 70 | -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /client/src/presentation/components/Message.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import styleColors from '../../constants/styleColors'; 5 | 6 | const MessageContent = styled.span` 7 | font-size: 1.4rem; 8 | word-wrap: break-word; 9 | color: ${styleColors.BASE_BLACK_COLOR}; 10 | line-height: 1.4; 11 | `; 12 | 13 | const Message = ({ children }) => { 14 | return {children}; 15 | }; 16 | 17 | Message.propTypes = { 18 | children: PropTypes.string.isRequired, 19 | }; 20 | 21 | export default Message; 22 | -------------------------------------------------------------------------------- /client/src/constants/game.js: -------------------------------------------------------------------------------- 1 | const READY_BUTTON_TEXT = { 2 | READY: 'Ready', 3 | CANCEL: 'Cancel', 4 | }; 5 | 6 | const STREAMING_PANEL_MESSAGE_TYPE = { 7 | STREAMER_LOADING: 'streamerLoading', 8 | QUIZ_SELECTION: 'quizSelection', 9 | SCORE_BOARD: 'scoreBoard', 10 | }; 11 | 12 | const GAME_STATUS = { 13 | WAITING: 'waiting', 14 | PLAYING: 'playing', 15 | SCORE_SHARING: 'scoreSharing', 16 | }; 17 | 18 | const PLAYER_TYPES = { 19 | VIEWER: 'viewer', 20 | STREAMER: 'streamer', 21 | }; 22 | 23 | export { 24 | READY_BUTTON_TEXT, 25 | STREAMING_PANEL_MESSAGE_TYPE, 26 | GAME_STATUS, 27 | PLAYER_TYPES, 28 | }; 29 | -------------------------------------------------------------------------------- /client/src/hooks/useInnerWidth.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import EVENTS from '../constants/events'; 3 | 4 | const useInnerWidth = () => { 5 | const [innerWidth, setInnerWidth] = useState(window.innerWidth); 6 | 7 | const resizeEventHandler = event => { 8 | setInnerWidth(event.target.innerWidth); 9 | }; 10 | 11 | useEffect(() => { 12 | window.addEventListener(EVENTS.RESIZE, resizeEventHandler); 13 | return () => { 14 | window.removeEventListener(EVENTS.RESIZE, resizeEventHandler); 15 | }; 16 | }, []); 17 | 18 | return innerWidth; 19 | }; 20 | 21 | export default useInnerWidth; 22 | -------------------------------------------------------------------------------- /client/src/presentation/components/UnderlinedLetter.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles } from '@material-ui/core/styles'; 3 | import PropTypes from 'prop-types'; 4 | 5 | const UnderlinedLetter = ({ letter }) => { 6 | const useStyles = makeStyles(() => ({ 7 | underlinedLetter: { 8 | fontSize: '2rem', 9 | fontWeight: 'bold', 10 | }, 11 | })); 12 | 13 | const classes = useStyles(); 14 | 15 | return {letter}; 16 | }; 17 | 18 | UnderlinedLetter.propTypes = { 19 | letter: PropTypes.string.isRequired, 20 | }; 21 | 22 | export default UnderlinedLetter; 23 | -------------------------------------------------------------------------------- /client/src/presentation/pages/Ranking/style.js: -------------------------------------------------------------------------------- 1 | import { makeStyles } from '@material-ui/core/styles'; 2 | 3 | const useStyle = makeStyles({ 4 | mainPage: { 5 | width: '40rem', 6 | display: 'flex', 7 | flexDirection: 'column', 8 | '& > *': { 9 | marginBottom: '2rem', 10 | }, 11 | }, 12 | mainPageWrapper: { 13 | margin: 0, 14 | width: '100%', 15 | height: '100%', 16 | overflow: 'auto', 17 | }, 18 | exitButtonWrapper: { 19 | position: 'fixed', 20 | top: '1.5rem', 21 | right: '2.5rem', 22 | }, 23 | MoreButton: { 24 | textAlign: 'center', 25 | marginBottom: '2rem', 26 | }, 27 | }); 28 | 29 | export default useStyle; 30 | -------------------------------------------------------------------------------- /client/src/presentation/containers/StreamingPanel/style.js: -------------------------------------------------------------------------------- 1 | import { makeStyles } from '@material-ui/core/styles'; 2 | import styleColors from '../../../constants/styleColors'; 3 | 4 | const useStyles = makeStyles(theme => ({ 5 | container: { 6 | position: 'relative', 7 | height: '100%', 8 | backgroundColor: styleColors.BASE_BLACK_COLOR, 9 | boxShadow: '0 0.2rem 0.7rem 0 rgba(0, 0, 0, 0.5)', 10 | borderRadius: '0.3rem', 11 | [theme.breakpoints.down('xs')]: { 12 | borderRadius: '0', 13 | }, 14 | padding: 'none', 15 | display: 'flex', 16 | alignItems: 'center', 17 | justifyContent: 'center', 18 | }, 19 | })); 20 | 21 | export default useStyles; 22 | -------------------------------------------------------------------------------- /client/src/presentation/containers/index.js: -------------------------------------------------------------------------------- 1 | export { default as MainTitle } from './MainTitle'; 2 | export { default as Menu } from './Menu'; 3 | export { default as Introduction } from './Introduction'; 4 | export { default as StreamingPanel } from './StreamingPanel'; 5 | export { default as ChattingPanel } from './ChattingPanel'; 6 | export { default as PlayerPanel } from './PlayerPanel'; 7 | export { default as MobileChattingPanel } from './MobileChattingPanel'; 8 | export { default as TopRankPanel } from './TopRankPanel'; 9 | export { default as BottomRankPanel } from './BottomRankPanel'; 10 | // eslint-disable-next-line import/no-unresolved 11 | export { default as ScoreBoard } from './ScoreBoard'; 12 | -------------------------------------------------------------------------------- /server/service/game/eventHandlers/index.js: -------------------------------------------------------------------------------- 1 | const matchHandler = require('./matchHandler'); 2 | const sendReadyHandler = require('./sendReadyHandler'); 3 | const sendChattingMessageHandler = require('./sendChattingMessageHandler'); 4 | const askSocketIdHandler = require('./askSocketIdHandler'); 5 | const connectPeerHandler = require('./connectPeerHandler'); 6 | const selectQuizHandler = require('./selectQuizHandler'); 7 | const disconnectingHandler = require('./disconnectingHandler'); 8 | 9 | module.exports = { 10 | matchHandler, 11 | sendReadyHandler, 12 | sendChattingMessageHandler, 13 | askSocketIdHandler, 14 | connectPeerHandler, 15 | selectQuizHandler, 16 | disconnectingHandler, 17 | }; 18 | -------------------------------------------------------------------------------- /server/databaseFiles/databaseModels/Quiz.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require('sequelize'); 2 | const connection = require('../connection'); 3 | const { MODEL_QUIZ } = require('../../constants/database'); 4 | 5 | class Quiz extends Sequelize.Model {} 6 | 7 | Quiz.init( 8 | { 9 | word: { 10 | type: Sequelize.STRING, 11 | allowNull: false, 12 | defaultValue: '', 13 | }, 14 | selected_count: { 15 | type: Sequelize.INTEGER, 16 | defaultValue: 0, 17 | }, 18 | hits: { 19 | type: Sequelize.INTEGER, 20 | defaultValue: 0, 21 | }, 22 | }, 23 | { 24 | sequelize: connection, 25 | modelName: MODEL_QUIZ, 26 | }, 27 | ); 28 | 29 | module.exports = Quiz; 30 | -------------------------------------------------------------------------------- /client/src/presentation/components/Slogan.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { makeStyles } from '@material-ui/core/styles'; 4 | import Typography from '@material-ui/core/Typography'; 5 | 6 | const useStyle = makeStyles({ 7 | slogan: { 8 | margin: '0 0.2rem', 9 | whiteSpace: 'pre-line', 10 | fontWeight: '600', 11 | }, 12 | }); 13 | 14 | const Slogan = ({ content }) => { 15 | const classes = useStyle(); 16 | return ( 17 | 18 | {content} 19 | 20 | ); 21 | }; 22 | 23 | Slogan.propTypes = { 24 | content: PropTypes.string.isRequired, 25 | }; 26 | 27 | export default Slogan; 28 | -------------------------------------------------------------------------------- /client/src/presentation/components/CenterTimer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles } from '@material-ui/core/styles'; 3 | import PropTypes from 'prop-types'; 4 | import styleColors from '../../constants/styleColors'; 5 | 6 | const useStyles = makeStyles(() => ({ 7 | timer: { 8 | fontSize: '6rem', 9 | fontWeight: 'bold', 10 | color: styleColors.PURE_WHITE_COLOR, 11 | textAlign: 'center', 12 | }, 13 | })); 14 | 15 | const CenterTimer = ({ currentSeconds }) => { 16 | const classes = useStyles(); 17 | return
{currentSeconds}
; 18 | }; 19 | 20 | CenterTimer.propTypes = { 21 | currentSeconds: PropTypes.string.isRequired, 22 | }; 23 | 24 | export default CenterTimer; 25 | -------------------------------------------------------------------------------- /client/src/presentation/components/Nickname.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import styleColors from '../../constants/styleColors'; 5 | 6 | const NicknameContent = styled.span` 7 | font-size: 1.6rem; 8 | color: ${props => { 9 | return props.nicknameColor || styleColors.INFORMATION_COLOR; 10 | }}; 11 | `; 12 | 13 | const Nickname = ({ children, nicknameColor }) => { 14 | return ( 15 | {children} 16 | ); 17 | }; 18 | 19 | Nickname.propTypes = { 20 | children: PropTypes.string.isRequired, 21 | nicknameColor: PropTypes.string.isRequired, 22 | }; 23 | 24 | export default Nickname; 25 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const logger = require('morgan'); 4 | const service = require('./service'); 5 | const api = require('./routes/api'); 6 | 7 | const app = express(); 8 | 9 | app.use(logger('dev')); 10 | app.use(express.json()); 11 | app.use( 12 | express.urlencoded({ 13 | extended: false, 14 | }), 15 | ); 16 | 17 | app.use(express.static(path.join(__dirname, '../client/build'))); 18 | 19 | app.use('/api', api); 20 | 21 | app.get('*', (req, res) => { 22 | res.sendFile(path.join(__dirname, '../client/build/index.html')); 23 | }); 24 | 25 | app.use((req, res) => { 26 | res.redirect('/'); 27 | }); 28 | 29 | app.service = service; 30 | 31 | module.exports = app; 32 | -------------------------------------------------------------------------------- /client/src/utils/makeViewPlayerList.js: -------------------------------------------------------------------------------- 1 | const makeViewPlayerList = (localPlayer, remotePlayers) => { 2 | const viewLocalPlayer = { ...localPlayer, isLocalPlayer: true }; 3 | const viewRemotePlayerSocketIds = Object.keys(remotePlayers); 4 | const viewRemotePlayers = viewRemotePlayerSocketIds.reduce( 5 | (accum, remotePlayerSocketId) => { 6 | const viewRemotePlayer = { 7 | ...remotePlayers[remotePlayerSocketId], 8 | isLocalPlayer: false, 9 | socketId: remotePlayerSocketId, 10 | }; 11 | return [viewRemotePlayer, ...accum]; 12 | }, 13 | [], 14 | ); 15 | const viewPlayerList = [viewLocalPlayer, ...viewRemotePlayers]; 16 | return viewPlayerList; 17 | }; 18 | 19 | export default makeViewPlayerList; 20 | -------------------------------------------------------------------------------- /server/databaseFiles/databaseModels/Ranking.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require('sequelize'); 2 | const connection = require('../connection'); 3 | const { MODEL_RANKING } = require('../../constants/database'); 4 | 5 | class Ranking extends Sequelize.Model {} 6 | 7 | Ranking.init( 8 | { 9 | nickname: { 10 | type: Sequelize.STRING, 11 | allowNull: false, 12 | }, 13 | score: { 14 | type: Sequelize.INTEGER, 15 | defaultValue: 0, 16 | allowNull: false, 17 | }, 18 | season: { 19 | type: Sequelize.INTEGER, 20 | defaultValue: 1, 21 | allowNull: false, 22 | }, 23 | }, 24 | { 25 | sequelize: connection, 26 | modelName: MODEL_RANKING, 27 | }, 28 | ); 29 | 30 | module.exports = Ranking; 31 | -------------------------------------------------------------------------------- /client/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": ["airbnb", "prettier"], 7 | "globals": { 8 | "Atomics": "readonly", 9 | "SharedArrayBuffer": "readonly" 10 | }, 11 | "parserOptions": { 12 | "ecmaFeatures": { 13 | "jsx": true 14 | }, 15 | "ecmaVersion": 2018, 16 | "sourceType": "module" 17 | }, 18 | "plugins": ["react", "react-hooks"], 19 | "rules": { 20 | "react/jsx-filename-extension": [ 21 | 1, 22 | { 23 | "extensions": [".js", ".jsx"] 24 | } 25 | ], 26 | "import/prefer-default-export": 0, 27 | "no-undef": 0, 28 | "react-hooks/rules-of-hooks": "error", 29 | "react-hooks/exhaustive-deps": "warn" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /client/src/api/index.js: -------------------------------------------------------------------------------- 1 | const getRankings = async (offset, dateTime) => { 2 | try { 3 | const responseData = await fetch( 4 | `/api/ranking?offset=${offset}&datetime=${dateTime}`, 5 | { 6 | method: 'GET', 7 | }, 8 | ); 9 | const jsonData = await responseData.json(); 10 | return jsonData; 11 | } catch (error) { 12 | return []; 13 | } 14 | }; 15 | 16 | const getRankingInformation = async () => { 17 | try { 18 | const responseData = await fetch(`/api/ranking/information`, { 19 | method: 'GET', 20 | }); 21 | const jsonData = await responseData.json(); 22 | return jsonData; 23 | } catch (error) { 24 | return []; 25 | } 26 | }; 27 | 28 | export { getRankings, getRankingInformation }; 29 | -------------------------------------------------------------------------------- /client/src/hooks/useToast.js: -------------------------------------------------------------------------------- 1 | /** 2 | * toasType: contants에 있는 toast 타입, 3 | * message: toast에 보여줄 메시지, 4 | * open: global state의 toast open, 5 | * dispatch: global dispatch 6 | * @param {*} param0 7 | */ 8 | const useToast = ({ open, dispatch, actions }) => { 9 | const openToastTimeoutHandler = (toastType, message) => { 10 | dispatch(actions.openToast(toastType, message)); 11 | }; 12 | const openToast = (toastType, message) => { 13 | if (open) { 14 | dispatch(actions.closeToast()); 15 | } 16 | setTimeout(openToastTimeoutHandler.bind(null, toastType, message), 0); 17 | }; 18 | const closeToast = () => { 19 | dispatch(actions.closeToast()); 20 | }; 21 | return { openToast, closeToast }; 22 | }; 23 | 24 | export default useToast; 25 | -------------------------------------------------------------------------------- /client/config/pnpTs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { resolveModuleName } = require('ts-pnp'); 4 | 5 | exports.resolveModuleName = ( 6 | typescript, 7 | moduleName, 8 | containingFile, 9 | compilerOptions, 10 | resolutionHost 11 | ) => { 12 | return resolveModuleName( 13 | moduleName, 14 | containingFile, 15 | compilerOptions, 16 | resolutionHost, 17 | typescript.resolveModuleName 18 | ); 19 | }; 20 | 21 | exports.resolveTypeReferenceDirective = ( 22 | typescript, 23 | moduleName, 24 | containingFile, 25 | compilerOptions, 26 | resolutionHost 27 | ) => { 28 | return resolveModuleName( 29 | moduleName, 30 | containingFile, 31 | compilerOptions, 32 | resolutionHost, 33 | typescript.resolveTypeReferenceDirective 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /client/src/presentation/components/Title.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { makeStyles } from '@material-ui/core/styles'; 4 | import Typography from '@material-ui/core/Typography'; 5 | 6 | const useStyle = makeStyles({ 7 | title: props => ({ 8 | margin: '0.5rem 0', 9 | fontSize: props.fontSize || '2rem', 10 | }), 11 | }); 12 | 13 | const Title = ({ content, fontSize }) => { 14 | const classes = useStyle({ fontSize }); 15 | return ( 16 | 17 | {content} 18 | 19 | ); 20 | }; 21 | 22 | Title.propTypes = { 23 | content: PropTypes.string.isRequired, 24 | fontSize: PropTypes.string.isRequired, 25 | }; 26 | 27 | export default Title; 28 | -------------------------------------------------------------------------------- /client/src/utils/createShareUrlButtonClickHandler.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-alert */ 2 | import copyUrlToClipoard from './copyUrlToClipboard'; 3 | import { COPY_TO_CLIPBOARD_MESSAGE } from '../constants/message'; 4 | import { TOAST_TYPES } from '../constants/toast'; 5 | import { CLIPBOARD_COPY_MESSAGE_DURATION } from '../constants/timer'; 6 | import Timer from '../service/Timer'; 7 | 8 | const createShareUrlButtonClickHandler = (openToast, closeToast) => { 9 | return () => { 10 | copyUrlToClipoard(); 11 | openToast( 12 | TOAST_TYPES.INFORMATION, 13 | `${window.location.href} ${COPY_TO_CLIPBOARD_MESSAGE}`, 14 | ); 15 | new Timer().startTimeoutTimer(CLIPBOARD_COPY_MESSAGE_DURATION, closeToast); 16 | }; 17 | }; 18 | 19 | export default createShareUrlButtonClickHandler; 20 | -------------------------------------------------------------------------------- /server/utils/getCurrentTime.js: -------------------------------------------------------------------------------- 1 | const leadingZeros = (number, digits) => { 2 | let zero = ''; 3 | const numberToString = number.toString(); 4 | 5 | if (numberToString.length < digits) { 6 | for (i = 0; i < digits - numberToString.length; i++) zero += '0'; 7 | } 8 | return zero + numberToString; 9 | }; 10 | 11 | const getCurrentTime = () => { 12 | const date = new Date(); 13 | const dateString = `${leadingZeros(date.getFullYear(), 4)}-${leadingZeros( 14 | date.getMonth() + 1, 15 | 2, 16 | )}-${leadingZeros(date.getDate(), 2)} ${leadingZeros( 17 | date.getHours(), 18 | 2, 19 | )}:${leadingZeros(date.getMinutes(), 2)}:${leadingZeros( 20 | date.getSeconds(), 21 | 2, 22 | )}`; 23 | 24 | return dateString; 25 | }; 26 | 27 | module.exports = { getCurrentTime }; 28 | -------------------------------------------------------------------------------- /server/databaseFiles/connection.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require('sequelize'); 2 | 3 | const { 4 | MAXIMUM_CONNECTION_COUNT, 5 | MINIMUM_CONNECTION_COUNT, 6 | MAXIMUM_WAITING_TIME, 7 | MAXIMUM_IDLE_TIME, 8 | } = require('../constants/database'); 9 | 10 | const { env } = process; 11 | 12 | const connection = new Sequelize.Sequelize( 13 | env.MYSQL_DATABASE, 14 | env.MYSQL_USER, 15 | env.MYSQL_PASSWORD, 16 | { 17 | host: env.DATABASE_HOST, 18 | dialect: env.DATABASE_DIALECT, 19 | charset: env.DATABASE_CHARSET, 20 | collate: env.DATABASE_COLLATE, 21 | pool: { 22 | max: MAXIMUM_CONNECTION_COUNT, 23 | min: MINIMUM_CONNECTION_COUNT, 24 | acquire: MAXIMUM_WAITING_TIME, 25 | idle: MAXIMUM_IDLE_TIME, 26 | }, 27 | }, 28 | ); 29 | 30 | module.exports = connection; 31 | -------------------------------------------------------------------------------- /client/src/presentation/components/Buttons/ReadyButton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { makeStyles } from '@material-ui/core/styles'; 4 | import Button from '@material-ui/core/Button'; 5 | import buttonStyle from './style'; 6 | 7 | const useStyles = makeStyles({ 8 | button: { 9 | ...buttonStyle, 10 | width: '100%', 11 | height: '3.2rem', 12 | }, 13 | }); 14 | 15 | const ReadyButton = ({ onClick, children }) => { 16 | const classes = useStyles(); 17 | return ( 18 | 21 | ); 22 | }; 23 | 24 | ReadyButton.propTypes = { 25 | children: PropTypes.string.isRequired, 26 | onClick: PropTypes.func.isRequired, 27 | }; 28 | 29 | export default ReadyButton; 30 | -------------------------------------------------------------------------------- /client/src/presentation/pages/Game/store/gameReducer.js: -------------------------------------------------------------------------------- 1 | import TYPES from '../../../../constants/actionTypes'; 2 | 3 | const gameReducer = (state, action) => { 4 | switch (action.type) { 5 | case TYPES.SET_MOBILE_CHATTING_PANEL_VISIBILITY: 6 | return { 7 | ...state, 8 | mobileChattingPanelVisibility: 9 | action.payload.mobileChattingPanelVisibility, 10 | }; 11 | case TYPES.SET_IS_PLAYER_LIST_VISIBLE: 12 | return { 13 | ...state, 14 | isPlayerListVisible: action.payload.isPlayerListVisible, 15 | }; 16 | case TYPES.SET_GAME_PAGE_ROOT_HEIGHT: 17 | return { 18 | ...state, 19 | gamePageRootHeight: action.payload.gamePageRootHeight, 20 | }; 21 | default: 22 | throw new Error(); 23 | } 24 | }; 25 | 26 | export default gameReducer; 27 | -------------------------------------------------------------------------------- /client/src/hooks/useShiftingToWhichView.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import useIsMobile from './useIsMobile'; 3 | import { MOBILE_VIEW, DESKTOP_VIEW } from '../constants/responsiveView'; 4 | 5 | const useShiftingToWhichView = mobileViewBreakpoint => { 6 | const isMobile = useIsMobile(mobileViewBreakpoint); 7 | const [shiftingTo, setShiftingTo] = useState(''); 8 | 9 | const isViewShiftingToMobile = currentIsMobile => { 10 | return currentIsMobile && currentIsMobile !== shiftingTo; 11 | }; 12 | 13 | useEffect(() => { 14 | if (!isViewShiftingToMobile(isMobile)) { 15 | setShiftingTo(DESKTOP_VIEW); 16 | } else if (isViewShiftingToMobile(isMobile)) { 17 | setShiftingTo(MOBILE_VIEW); 18 | } 19 | }, [isMobile]); 20 | return shiftingTo; 21 | }; 22 | 23 | export default useShiftingToWhichView; 24 | -------------------------------------------------------------------------------- /client/src/presentation/components/Description.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { makeStyles } from '@material-ui/core/styles'; 4 | import Typography from '@material-ui/core/Typography'; 5 | 6 | const useStyle = makeStyles({ 7 | description: props => ({ 8 | margin: '0 0.2rem', 9 | fontSize: props.fontSize || '1rem', 10 | whiteSpace: 'pre-line', 11 | }), 12 | }); 13 | 14 | const Description = ({ content, fontSize }) => { 15 | const classes = useStyle({ fontSize }); 16 | return ( 17 | 18 | {content} 19 | 20 | ); 21 | }; 22 | 23 | Description.propTypes = { 24 | content: PropTypes.string.isRequired, 25 | fontSize: PropTypes.string.isRequired, 26 | }; 27 | 28 | export default Description; 29 | -------------------------------------------------------------------------------- /client/src/presentation/components/Buttons/ExitButton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | 5 | const StyledExitButton = styled.button` 6 | width: 3rem; 7 | padding: 0; 8 | cursor: pointer; 9 | background-color: transparent; 10 | border: none; 11 | outline: none; 12 | img { 13 | width: 100%; 14 | } 15 | `; 16 | 17 | const ExitButton = ({ onClick, imageSource }) => { 18 | return ( 19 | 20 | exit 21 | 22 | ); 23 | }; 24 | 25 | ExitButton.defaultProps = { 26 | onClick: () => {}, 27 | }; 28 | 29 | ExitButton.propTypes = { 30 | onClick: PropTypes.func, 31 | imageSource: PropTypes.string.isRequired, 32 | }; 33 | 34 | export default ExitButton; 35 | -------------------------------------------------------------------------------- /client/src/actions/game.js: -------------------------------------------------------------------------------- 1 | import TYPES from '../constants/actionTypes'; 2 | 3 | const setMobileChattingPanelVisibility = mobileChattingPanelVisibility => { 4 | return { 5 | type: TYPES.SET_MOBILE_CHATTING_PANEL_VISIBILITY, 6 | payload: { 7 | mobileChattingPanelVisibility, 8 | }, 9 | }; 10 | }; 11 | 12 | const setIsPlayerListVisible = isPlayerListVisible => { 13 | return { 14 | type: TYPES.SET_IS_PLAYER_LIST_VISIBLE, 15 | payload: { 16 | isPlayerListVisible, 17 | }, 18 | }; 19 | }; 20 | 21 | const setGamePageRootHeight = gamePageRootHeight => { 22 | return { 23 | type: TYPES.SET_GAME_PAGE_ROOT_HEIGHT, 24 | payload: { 25 | gamePageRootHeight, 26 | }, 27 | }; 28 | }; 29 | 30 | export default { 31 | setMobileChattingPanelVisibility, 32 | setIsPlayerListVisible, 33 | setGamePageRootHeight, 34 | }; 35 | -------------------------------------------------------------------------------- /server/service/game/eventHandlers/connectPeerHandler.js: -------------------------------------------------------------------------------- 1 | const roomController = require('../controllers/roomController'); 2 | const gameController = require('../controllers/gameController'); 3 | const GAME_STATUS = require('../../../constants/gameStatus'); 4 | 5 | const connectPeerHandler = socket => { 6 | const { gameManager, timer } = roomController.getRoomByRoomId(socket.roomId); 7 | const connectedPlayer = gameManager.getPlayerBySocketId(socket.id); 8 | connectedPlayer.setIsConnectedToStreamer(true); 9 | 10 | if ( 11 | gameManager.getStreamer() && 12 | gameManager.checkAllConnectionsToStreamer() && 13 | gameManager.getStatus() !== GAME_STATUS.INITIALIZING 14 | ) { 15 | /** 16 | * 연결 준비 후 정상 시작 17 | */ 18 | timer.clear(); 19 | gameController.prepareSet(gameManager, timer); 20 | } 21 | }; 22 | 23 | module.exports = connectPeerHandler; 24 | -------------------------------------------------------------------------------- /client/src/presentation/components/StreamerVideo.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/media-has-caption */ 2 | import React, { useEffect } from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import { makeStyles } from '@material-ui/core/styles'; 5 | 6 | const useStyles = makeStyles(() => ({ 7 | video: { 8 | maxWidth: '100%', 9 | height: '100%', 10 | transform: 'rotateY(180deg)', 11 | }, 12 | })); 13 | 14 | const StreamerVideo = ({ stream }) => { 15 | const classes = useStyles(); 16 | const ref = React.createRef(); 17 | 18 | useEffect(() => { 19 | const videoElement = ref.current; 20 | videoElement.srcObject = stream; 21 | }, [stream]); 22 | 23 | return