├── .github ├── ISSUE_TEMPLATE │ ├── -----.md │ └── bug-report.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── deploy.yml ├── .gitignore ├── .gitmessage.txt ├── .prettierrc ├── README.md ├── back-end ├── api-server │ ├── bin │ │ └── www │ ├── build │ │ └── swagger.yaml │ ├── database │ │ ├── connect.ts │ │ └── query.ts │ ├── middlewares │ │ └── jwt.ts │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── index.html │ │ └── stylesheets │ │ │ └── style.css │ ├── routes │ │ └── api-routes │ │ │ ├── auth.ts │ │ │ ├── friend.ts │ │ │ ├── gameRecord.ts │ │ │ ├── index.ts │ │ │ ├── profile.ts │ │ │ ├── rankingSearch.ts │ │ │ └── registerDBInsert.ts │ ├── services │ │ ├── auth.ts │ │ ├── friend.ts │ │ └── gameRecord.ts │ ├── src │ │ └── index.ts │ └── tsconfig.json └── socket-server │ ├── constant │ └── room.ts │ ├── package-lock.json │ ├── package.json │ ├── services │ ├── lobbyUserSocket.ts │ ├── socket.ts │ └── tetrisSocket.ts │ ├── src │ └── index.ts │ ├── tsconfig.json │ ├── type │ └── socketType.ts │ └── utils │ ├── dateUtil.ts │ ├── tetrisUtil.ts │ └── userUtil.ts ├── deploy.sh └── front-end ├── README.md ├── craco.config.js ├── package-lock.json ├── package.json ├── public ├── assets │ ├── block.png │ ├── error.png │ ├── logo.png │ ├── logo_appbar.png │ ├── other_block.png │ └── profile.png ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.scss ├── App.test.tsx ├── App.tsx ├── app │ ├── hooks.ts │ └── store.ts ├── common │ ├── assets │ │ ├── checkbox.png │ │ └── lock.png │ ├── fonts │ │ ├── DungGeunMo.eot │ │ ├── DungGeunMo.ttf │ │ ├── DungGeunMo.woff │ │ └── DungGeunMo.woff2 │ └── styles │ │ └── _base.scss ├── components │ ├── BasicButton │ │ ├── index.tsx │ │ └── style.scss │ ├── BasicInput │ │ ├── index.tsx │ │ └── style.scss │ ├── BubbleButton │ │ ├── index.tsx │ │ └── style.scss │ ├── InfiniteScroll │ │ ├── index.tsx │ │ └── style.scss │ ├── LobbyChat │ │ ├── index.tsx │ │ └── style.scss │ ├── Modal │ │ ├── index.tsx │ │ └── style.scss │ ├── OauthLoginButton │ │ ├── index.tsx │ │ └── style.scss │ ├── Popper │ │ ├── index.tsx │ │ └── style.scss │ ├── SEO │ │ └── index.tsx │ ├── SectionTitle │ │ ├── index.tsx │ │ └── style.scss │ ├── Tetris │ │ ├── Board │ │ │ └── index.tsx │ │ ├── HoldBlock │ │ │ └── index.tsx │ │ ├── OtherBoard │ │ │ └── index.tsx │ │ ├── PreviewBlocks │ │ │ └── index.tsx │ │ ├── RankTable │ │ │ └── index.tsx │ │ ├── types.ts │ │ └── utils │ │ │ ├── block.ts │ │ │ └── tetrisDrawUtil.ts │ └── UserPopper │ │ ├── index.tsx │ │ └── style.scss ├── constants │ ├── index.ts │ └── tetris.ts ├── context │ └── SocketContext.tsx ├── features │ ├── counter │ │ ├── Counter.module.css │ │ ├── Counter.tsx │ │ ├── counterAPI.ts │ │ ├── counterSlice.spec.ts │ │ └── counterSlice.ts │ ├── friend │ │ ├── friendAPI.ts │ │ └── friendSlice.ts │ ├── socket │ │ └── socketSlice.ts │ └── user │ │ ├── userAPI.ts │ │ └── userSlice.ts ├── hooks │ ├── use-auth │ │ └── index.ts │ └── use-query-params │ │ └── index.ts ├── index.scss ├── index.tsx ├── layout │ └── AppbarLayout │ │ ├── index.tsx │ │ └── style.scss ├── pages │ ├── ErrorPage │ │ ├── constants.ts │ │ ├── index.tsx │ │ └── style.scss │ ├── GamePage │ │ ├── index.tsx │ │ └── style.scss │ ├── LobbyPage │ │ ├── index.tsx │ │ └── style.scss │ ├── LoginPage │ │ ├── index.tsx │ │ └── style.scss │ ├── ProfilePage │ │ ├── index.tsx │ │ ├── profileFetch.tsx │ │ └── style.scss │ ├── RankingPage │ │ ├── index.tsx │ │ ├── rankFetch.tsx │ │ └── style.scss │ ├── RegisterPage │ │ ├── index.tsx │ │ └── style.scss │ └── WithSocketPage │ │ └── index.tsx ├── react-app-env.d.ts ├── routes │ ├── OauthCallbackRouter │ │ ├── GithubCallback.tsx │ │ ├── GoogleCallback.tsx │ │ ├── NaverCallback.tsx │ │ └── index.tsx │ └── RequireAuth.tsx ├── serviceWorker.ts ├── setupProxy.js └── setupTests.ts └── tsconfig.json /.github/ISSUE_TEMPLATE/-----.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 기능 추가 3 | about: 새로운 기능을 추가합니다. 4 | title: '' 5 | labels: feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | **제목** 11 | 12 | **선행 이슈** 13 | 14 | **해결해야 했던 문제** 15 | 16 | - [ ] 띠용 17 | 18 | **첨부 사항(optional)** 19 | 20 | *화면 캡처, 피그마 주소 등등* 21 | 22 | **이것만은 명심해라** 23 | - 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: 고치세요 ^^ 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **버그 설명** 11 | A clear and concise description of what the bug is. 12 | 13 | **재현 방법** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **원래는 이렇게 동작해야했다..** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **증거** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### 작업 개요 2 | 5 | ### Github Issue Number 6 | 9 | ### 작업 분류 10 | - [ ] 버그 수정 11 | - [ ] 신규 기능 12 | - [ ] 프로젝트 구조 변경 13 | 18 | ### 작업 상세 내용 19 | 24 | ### 생각해볼 문제 25 | 29 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | # Controls when the workflow will run 4 | on: 5 | push: 6 | branches: [ develop ] 7 | pull_request: 8 | types: [closed] 9 | branches: [ develop ] 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 15 | jobs: 16 | # This workflow contains a single job called "build" 17 | build: 18 | # The type of runner that the job will run on 19 | runs-on: ubuntu-latest 20 | #if: github.event.pull_request.merged 21 | 22 | # Steps represent a sequence of tasks that will be executed as part of the job 23 | steps: 24 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 25 | - uses: actions/checkout@v2 26 | 27 | # Runs a single command using the runners shell 28 | - name: Run a one-line script 29 | run: echo Hello, world! 30 | 31 | - name: ncloud CD 32 | uses: appleboy/ssh-action@master 33 | with: 34 | key: ${{secrets.SSH_PRIVATE_KEY}} 35 | host: ${{secrets.REMOTE_HOST}} 36 | username: ${{secrets.REMOTE_USERNAME}} 37 | port: ${{secrets.REMOTE_PORT_NUMBER}} 38 | script: sh /var/www/app/web24-boostris/deploy.sh 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | */build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # session-file 26 | back-end/sessions 27 | 28 | # env file 29 | .env -------------------------------------------------------------------------------- /.gitmessage.txt: -------------------------------------------------------------------------------- 1 | ################ 2 | # <타입> : <제목> 의 형식으로 제목을 아래 공백줄에 작성 3 | # 제목은 50자 이내 / 변경사항이 "무엇"인지 명확히 작성 / 끝에 마침표 금지 4 | # 예) feat : 로그인 기능 추가 5 | ################ 6 | # 필요한 것을 앞에 주석을 지우고 사용하세요 !! 7 | # ✨ : 새 기능 8 | # 🔨 : 리펙토링 9 | # 🐛 : 버그 수정 10 | # 🛠 : config 파일 수정 11 | # 💄 : UI/스타일 파일 추가/수정 12 | # 📝 : 문서 추가/수정 13 | # 🔥 : 코드/파일 삭제 14 | # 👌 : 코드 리뷰 적용 15 | # 🔖 : 릴리즈/버전 태그 16 | # 🚀 : 배포 17 | ################ 18 | 19 | # 바로 아래 공백은 지우지 마세요 (제목과 본문의 분리를 위함) 20 | 21 | ################ 22 | # 본문(구체적인 내용)을 아랫줄에 작성 23 | # 여러 줄의 메시지를 작성할 땐 "-"로 구분 (한 줄은 72자 이내) 24 | 25 | ################ 26 | # 꼬릿말(footer)을 아랫줄에 작성 (현재 커밋과 관련된 이슈 번호 추가 등) 27 | # 예) Close #7 28 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 100 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Boostris 4 | 5 | 6 |
7 | 8 |
9 | 10 | ## 팀원 소개 11 | 12 | | [J182\_전용후](https://github.com/jyh0521) | [J203\_채호경](https://github.com/24to26) | [J215\_한찬호](https://github.com/ChanHoHan) | [J223\_황정빈](https://github.com/jeongbbn) | 13 | | ----------------------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------ | 14 | | | | | | 15 | | @jyh0521 | @24to26 | @ChanHoHan | @jeongbbn | 16 | 17 |
18 | 19 | ## 프로젝트 소개 20 | 21 | ... "덕후의 끝판왕은 직접 만드는 것이다" 22 |

23 | ... 테트리스에 진심인 사람들이 모여 만든 "소셜 테트리스" 24 |

25 | ![게임 진행중 + 끝2 사이즈 작음](https://user-images.githubusercontent.com/46598292/144737411-0c1059f0-2662-49d6-8270-216b10a85368.gif) 26 | 27 |
28 | 29 | ## 링크 30 | 31 | 32 | [Boostris 즐기러 가기](https://www.boostris.com) 33 | 34 | 35 | |Notion|Wiki|Figma|Backlog|Youtube| 36 | |------|---|---|---|---| 37 | |[Notion 바로가기](https://www.notion.so/Boostris-7123af3df6d34873b81f28fa6fa1e9a0)|[Wiki 바로가기](https://github.com/boostcampwm-2021/web24-boostris/wiki)|[Figma 바로가기](https://www.figma.com/file/L74jqi18RdQtEody3LXcZT/Boostris?node-id=165%3A3)|[Backlog 바로가기](https://www.notion.so/e44a722a32d949c496c977007efc5357?v=89ee1ebfb84e45bbb1130fa883f7d8d7)|[Youtube 바로가기](https://www.youtube.com/watch?v=KVUbau2fy8M&t=186s)| 38 | 39 |
40 | 41 | ## 기술 스택 42 | 43 |
44 | 45 |
46 | React 47 | Sass 48 | TypeScript 49 | Python 50 | Socket.io
51 | Express 52 | MySQL 53 | Redis 54 | Swagger
55 | PM2 56 | NGINX 57 | Docker 58 |
59 |
60 | 61 | ## 기능 화면 62 | 63 | ### 🌈 로비 화면 64 | 65 | 66 | ### 🌈 친구 기능 67 | 68 | 69 | ### 🌈 프로필 70 | 71 | 72 | ### 🌈 랭킹 73 | 74 |
75 | 76 | ## 프로젝트 구조 77 | 78 | 79 | 80 |
81 | 82 | ## ERD 83 | 84 | 85 | -------------------------------------------------------------------------------- /back-end/api-server/bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('api-server:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3001'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | -------------------------------------------------------------------------------- /back-end/api-server/database/connect.ts: -------------------------------------------------------------------------------- 1 | const mysql = require('mysql2/promise'); 2 | 3 | const pool = mysql.createPool({ 4 | host: process.env.MYSQL_HOST, 5 | port: process.env.MYSQL_PORT, 6 | user: process.env.MYSQL_USER, 7 | password: process.env.MYSQL_PASSWORD, 8 | database: process.env.MYSQL_DATABASE, 9 | }); 10 | 11 | export default pool; 12 | -------------------------------------------------------------------------------- /back-end/api-server/database/query.ts: -------------------------------------------------------------------------------- 1 | import pool from './connect'; 2 | 3 | const connectionQuery = async (queryLine) => { 4 | const connection = await pool.getConnection(async (conn) => conn); 5 | const [rows] = await connection.query(queryLine); 6 | connection.release(); // 커넥션 반환 7 | return rows; 8 | }; 9 | 10 | export const selectTable = (column, table, condition = null, ...rest) => { 11 | let queryLine = `SELECT ${column} FROM ${table} `; 12 | queryLine += condition ? `WHERE ${condition}` : ``; 13 | rest.map((value) => (queryLine += value)); 14 | return connectionQuery(queryLine); 15 | }; 16 | 17 | export const insertIntoTable = (table, into, values) => { 18 | let queryLine = `INSERT INTO ${table} ${into} VALUES (${values})`; 19 | return connectionQuery(queryLine); 20 | }; 21 | 22 | export const innerJoinTable = async ( 23 | column, 24 | tableA, 25 | tableB, 26 | on = null, 27 | condition = null, 28 | orderBy = null, 29 | limit = null 30 | ) => { 31 | let queryLine = `SELECT ${column} FROM ${tableA} INNER JOIN ${tableB} ON ${on}`; 32 | queryLine += condition ? ` WHERE ${condition}` : ``; 33 | queryLine += orderBy ? ` ORDER BY ${orderBy}` : ``; 34 | queryLine += limit ? ` LIMIT ${limit}` : ``; 35 | return connectionQuery(queryLine); 36 | }; 37 | 38 | export const updateTable = async (table, set, condition = null) => { 39 | let queryLine = `UPDATE ${table} SET ${set} `; 40 | queryLine += condition ? `WHERE ${condition}` : ``; 41 | return connectionQuery(queryLine); 42 | }; 43 | 44 | export const deleteTable = async (table, condition) => { 45 | let queryLine = `DELETE FROM ${table} WHERE ${condition}`; 46 | return connectionQuery(queryLine); 47 | }; 48 | -------------------------------------------------------------------------------- /back-end/api-server/middlewares/jwt.ts: -------------------------------------------------------------------------------- 1 | import { setJWT } from './../services/auth'; 2 | import { insertIntoTable, selectTable } from './../database/query'; 3 | import * as jwt from 'jsonwebtoken'; 4 | 5 | export function authenticateToken(req, res, next) { 6 | try { 7 | const token = req.cookies.user; 8 | if (token == null) return res.sendStatus(401); 9 | else if (token) { 10 | jwt.verify(token, process.env.JWT_SECRET_KEY as string, (err: any, user: any) => { 11 | if (err) return res.sendStatus(403); 12 | 13 | req.user = user; 14 | 15 | next(); 16 | }); 17 | } 18 | } catch (error) { 19 | return res.sendStatus(403); 20 | } 21 | } 22 | 23 | export async function registerDupCheck(req, res, next) { 24 | try { 25 | const nickName = req.body['registerData']['nickname']; 26 | const userDupCheckResult = await selectTable('*', 'USER_INFO', `nickname='${nickName}'`); 27 | if (userDupCheckResult?.length) { 28 | throw Error('already exists'); 29 | } else { 30 | next(); 31 | } 32 | } catch (error) { 33 | res.json({ dupCheck: false }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /back-end/api-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-server", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "npx nodemon --exec ts-node ./src/index.ts" 7 | }, 8 | "dependencies": { 9 | "@types/express": "^4.17.13", 10 | "axios": "^0.24.0", 11 | "cookie-parser": "~1.4.4", 12 | "debug": "~2.6.9", 13 | "dotenv": "^10.0.0", 14 | "express": "~4.16.1", 15 | "jsonwebtoken": "^8.5.1", 16 | "morgan": "~1.9.1", 17 | "mysql2": "^2.3.2", 18 | "typescript": "^4.4.4" 19 | }, 20 | "devDependencies": { 21 | "@types/cors": "^2.8.12", 22 | "@types/jsonwebtoken": "^8.5.5", 23 | "@types/swagger-jsdoc": "^6.0.1", 24 | "@types/swagger-ui-express": "^4.1.3", 25 | "@types/yamljs": "^0.2.31", 26 | "cors": "^2.8.5", 27 | "nodemon": "^2.0.14", 28 | "swagger-jsdoc": "^6.1.0", 29 | "swagger-ui-express": "^4.1.6", 30 | "ts-node": "^10.4.0", 31 | "yamljs": "^0.3.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /back-end/api-server/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Express 5 | 6 | 7 | 8 | 9 |

Express

10 |

Welcome to Express

11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /back-end/api-server/public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | -------------------------------------------------------------------------------- /back-end/api-server/routes/api-routes/auth.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import axios from 'axios'; 3 | import * as jwt from 'jsonwebtoken'; 4 | import { selectTable } from '../../database/query'; 5 | import 'dotenv/config'; 6 | 7 | import { getGithubUser, getUserInfoFromNaver, setJWT } from '../../services/auth'; 8 | 9 | const AuthRouter = express.Router(); 10 | 11 | /* 12 | 이미 존재하는 회원인지 확인 13 | */ 14 | const isOauthIdInDB = (oauthID) => { 15 | return selectTable('*', 'USER_INFO', `oauth_id='${oauthID}'`); 16 | }; 17 | 18 | const oauthDupCheck = async (id, req, res) => { 19 | try { 20 | const userList = await isOauthIdInDB(id); 21 | /* 만약 oauth 로그인에 성공하면 jwt 토큰 발급 */ 22 | if (userList && userList.length) { 23 | console.log(userList); 24 | const [user] = userList; 25 | setJWT(req, res, user); 26 | return [true, user]; 27 | } else { 28 | /* 회원 가입 페이지로 redirect */ 29 | console.log('fail'); 30 | return [false]; 31 | } 32 | } catch (e) { 33 | console.log(e); 34 | return [false]; 35 | } 36 | }; 37 | 38 | AuthRouter.post('/github/code', async (req, res) => { 39 | const { code } = req.body; 40 | 41 | try { 42 | if (code) { 43 | const { data } = await axios({ 44 | method: 'POST', 45 | url: 'https://github.com/login/oauth/access_token', 46 | headers: { 47 | accept: 'application/json', 48 | 'Content-Type': 'application/json', 49 | }, 50 | data: { 51 | client_id: process.env.GITHUB_CLIENT_ID, 52 | client_secret: process.env.GITHUB_CLIENT_SECRET, 53 | code, 54 | }, 55 | timeout: 3000, 56 | timeoutErrorMessage: 'time out', 57 | }); 58 | const { access_token } = data; 59 | if (access_token) { 60 | const user = await getGithubUser(access_token); 61 | if (!user) { 62 | throw Error('github error'); 63 | } 64 | const [isOurUser, target] = await oauthDupCheck(user['id'], req, res); // 일단 중복 안되는 login 으로 해놓음 65 | const id = user.id; 66 | res.status(200).json({ id, isOurUser, nickname: target?.nickname }); 67 | } else { 68 | throw Error('github error'); 69 | } 70 | } else { 71 | res.status(400).json({ 72 | success: false, 73 | }); 74 | } 75 | } catch (error) { 76 | console.error(error); 77 | res.sendStatus(400); 78 | } 79 | }); 80 | 81 | AuthRouter.post('/naver/token', async (req, res) => { 82 | const { accessToken } = req.body; 83 | try { 84 | const userInfoFromNaver = await getUserInfoFromNaver(accessToken); 85 | const id = userInfoFromNaver['response']['id']; 86 | 87 | if (id) { 88 | const [isOurUser, target] = await oauthDupCheck(id, req, res); 89 | res.json({ id, isOurUser, nickname: target?.nickname }); 90 | } else { 91 | throw Error('naver error'); 92 | } 93 | } catch (error) { 94 | console.error(error); 95 | res.sendStatus(400); 96 | } 97 | }); 98 | 99 | AuthRouter.post('/google/user', async (req, res) => { 100 | const { email, name } = req.body; 101 | try { 102 | const [isOurUser, target] = await oauthDupCheck(email, req, res); 103 | res.json({ id: email, isOurUser, nickname: target?.nickname }); 104 | } catch (error) { 105 | console.error(error); 106 | res.sendStatus(400); 107 | } 108 | }); 109 | 110 | AuthRouter.get('/jwt', async (req, res) => { 111 | try { 112 | if (req.cookies.user) { 113 | const { nickname, oauth_id } = jwt.verify( 114 | req.cookies.user, 115 | process.env.JWT_SECRET_KEY 116 | ) as any; 117 | res.json({ authenticated: true, nickname, oauth_id }); 118 | } else { 119 | throw new Error('no-cookie'); 120 | } 121 | } catch (error) { 122 | res.json({ authenticated: false }); 123 | } 124 | }); 125 | 126 | AuthRouter.get('/logout', async (req, res) => { 127 | res.clearCookie('user'); 128 | res.json({ authenticated: false }); 129 | }); 130 | 131 | export default AuthRouter; 132 | -------------------------------------------------------------------------------- /back-end/api-server/routes/api-routes/friend.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { 3 | checkAlreadyFriend, 4 | getFriendList, 5 | requestFriend, 6 | requestFriendList, 7 | requestFriendUpdate, 8 | } from '../../services/friend'; 9 | 10 | const FriendRouter = express.Router(); 11 | interface friendReturnFormInterface { 12 | data: Object; 13 | message: String; 14 | } 15 | const friendReturnForm: friendReturnFormInterface = { 16 | data: {}, 17 | message: '', 18 | }; 19 | 20 | const setMessage = (data, message) => { 21 | friendReturnForm.data = data; 22 | friendReturnForm.message = message; 23 | return friendReturnForm; 24 | }; 25 | 26 | // 친구 요청 받을 시, 친구 요청 테이블에 넣기 27 | FriendRouter.post('/request', async (req, res, next) => { 28 | const { requestee, requester } = req.body; // userId : 친구 요청을 보낸 사람, friendId : 친구 요청을 받은 사람 29 | try { 30 | const result = await requestFriend({ requestee, requester }); 31 | const checkFriend = await checkAlreadyFriend({ requestee, requester }); // 이미 친구인지 확인 32 | if (result && !checkFriend) { 33 | res.status(200).json(setMessage({}, 'success')); 34 | } else { 35 | throw Error('request make error'); 36 | } 37 | } catch (error) { 38 | console.error(error); 39 | res.status(400).json(setMessage({}, 'fail')); 40 | } 41 | }); 42 | 43 | // 친구 요청 수락, 거절 + 수락햇을때 실제 친구 데이터베이스에 넣기 44 | FriendRouter.post('/request-update', async (req, res, next) => { 45 | const { isAccept, requestee, requester } = req.body; 46 | try { 47 | const result = await requestFriendUpdate({ isAccept, requestee, requester }); 48 | if (result) { 49 | res.status(200).json(setMessage({}, 'success')); 50 | } else { 51 | throw Error('request update error'); 52 | } 53 | } catch (error) { 54 | console.error(error); 55 | res.status(400).json(setMessage({}, 'fail')); 56 | } 57 | }); 58 | 59 | // 나한테 들어온 친구 요청 목록 가져오기 60 | FriendRouter.get('/request-list', async (req, res, next) => { 61 | const requestee = req.query.requestee; 62 | try { 63 | const friendRequestList = await requestFriendList(requestee); 64 | if (friendRequestList ?? 0) { 65 | res.status(200).json(setMessage(friendRequestList, 'success')); 66 | } else { 67 | throw Error('request list error'); 68 | } 69 | } catch (error) { 70 | console.error(error); 71 | res.status(400).json(setMessage([], 'fail')); 72 | } 73 | }); 74 | 75 | // 내 친구 목록 가져오기 76 | FriendRouter.get('/list', async (req, res, next) => { 77 | const oauthId = req.query.oauthId; 78 | try { 79 | const friendList = await getFriendList(oauthId); 80 | if (friendList ?? 0) { 81 | res.status(200).json(setMessage(friendList, 'success')); 82 | } else { 83 | throw Error('friendlist error'); 84 | } 85 | } catch (error) { 86 | console.error(error); 87 | res.status(400).json(setMessage([], 'fail')); 88 | } 89 | }); 90 | 91 | export default FriendRouter; 92 | -------------------------------------------------------------------------------- /back-end/api-server/routes/api-routes/gameRecord.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { insertGameInfo, insertPlayerInfo } from '../../services/gameRecord'; 3 | 4 | const GameRecordRouter = express.Router(); 5 | 6 | GameRecordRouter.post('/', async (req, res, next) => { 7 | const { game, players } = req.body; 8 | const insertGameInfoResult = await insertGameInfo(game); 9 | const insertPlayerInfoResult = await insertPlayerInfo(game.game_id, players); 10 | if (insertGameInfoResult && insertPlayerInfoResult) { 11 | res.status(200).json({ message: 'success' }); 12 | } else { 13 | res.status(400).json({ message: 'fail' }); 14 | } 15 | }); 16 | 17 | export default GameRecordRouter; 18 | -------------------------------------------------------------------------------- /back-end/api-server/routes/api-routes/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import AuthRouter from './auth'; 3 | import InsertDbRegister from './registerDBInsert'; 4 | import ProfileRouter from './profile'; 5 | import RankingRouter from './rankingSearch'; 6 | import FriendRouter from './friend'; 7 | import GameRecordRouter from './gameRecord'; 8 | import { registerDupCheck } from '../../middlewares/jwt'; 9 | 10 | const ApiRouter = express.Router(); 11 | 12 | ApiRouter.use('/auth', AuthRouter); 13 | ApiRouter.use('/rank', RankingRouter); 14 | ApiRouter.use('/register', registerDupCheck, InsertDbRegister); 15 | ApiRouter.use('/profile', ProfileRouter); 16 | ApiRouter.use('/friend', FriendRouter); 17 | ApiRouter.use('/game/record', GameRecordRouter); 18 | 19 | export default ApiRouter; 20 | -------------------------------------------------------------------------------- /back-end/api-server/routes/api-routes/profile.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { selectTable, innerJoinTable, updateTable } from '../../database/query'; 3 | import { setJWT } from '../../services/auth'; 4 | 5 | const ProfileRouter = express.Router(); 6 | 7 | ProfileRouter.post('/stateMessage', async (req, res, next) => { 8 | try { 9 | const stateMessageList = await getStateMessageInDB(req.body.nickname); 10 | if (stateMessageList.length === 0) { 11 | //잘못된 경우 에러처리 12 | res.status(401).json({ error: '잘못된 인증입니다.' }); 13 | } else { 14 | const { state_message } = stateMessageList[0]; 15 | res.status(200).json({ state_message }); 16 | } 17 | } catch (error) { 18 | res.status(401).json({ error: '잘못된 인증입니다.' }); 19 | } 20 | }); 21 | 22 | ProfileRouter.post('/total', async (req, res, next) => { 23 | try { 24 | const [{ oauth_id }] = await getOauthId(req.body.nickname); 25 | const totalList = await getTotalInDB(oauth_id); 26 | const [total, win] = totalList; 27 | const data = { ...total[0], ...win[0] }; 28 | res.status(200).json(data); 29 | } catch (error) { 30 | res.status(401).json({ error: '잘못된 인증입니다.' }); 31 | } 32 | }); 33 | 34 | ProfileRouter.post('/recent', async (req, res, next) => { 35 | try { 36 | const { nickname, offset, limit } = req.body; 37 | const [{ oauth_id }] = await getOauthId(nickname); 38 | const data = await getRecentInDB(oauth_id, offset, limit); 39 | res.status(200).json(data); 40 | } catch (error) { 41 | res.status(401).json({ error: '잘못된 인증입니다.' }); 42 | } 43 | }); 44 | 45 | ProfileRouter.patch('/', async (req, res, next) => { 46 | try { 47 | const { nickname, id } = req.body; 48 | const result = await updateProfileInDB(req.body); 49 | if (result.warningStatus !== 0) { 50 | res.status(401).json({ error: '잘못된 인증입니다.' }); 51 | } else { 52 | res.clearCookie('user'); 53 | setJWT(req, res, { nickname, oauth_id: id }); 54 | res.status(200).json({ message: 'done' }); 55 | } 56 | } catch (error) { 57 | res.status(401).json({ error: '잘못된 인증입니다.' }); 58 | } 59 | }); 60 | 61 | const getOauthId = (nickname) => { 62 | return selectTable('oauth_id', 'USER_INFO', `nickname='${nickname}'`); 63 | }; 64 | 65 | const updateProfileInDB = ({ nickname, stateMessage, id }) => { 66 | return updateTable( 67 | 'USER_INFO', 68 | `state_message='${stateMessage}', nickname='${nickname}'`, 69 | `oauth_id='${id}'` 70 | ); 71 | }; 72 | 73 | const getStateMessageInDB = (nickname) => { 74 | return selectTable('state_message', 'USER_INFO', `nickname='${nickname}'`); 75 | }; 76 | 77 | const getTotalInDB = async (id) => { 78 | return await Promise.all([ 79 | selectTable( 80 | 'SUM(attack_cnt) as total_attack_cnt, COUNT(oauth_id) as total_game_cnt, SEC_TO_TIME(SUM(play_time)) as total_play_time ', 81 | 'PLAY', 82 | `oauth_id='${id}'` 83 | ), 84 | innerJoinTable( 85 | `SUM(case when game_mode='normal' then player_win else 0 end) as multi_player_win`, 86 | 'PLAY', 87 | 'GAME_INFO', 88 | 'PLAY.game_id = GAME_INFO.game_id', 89 | `oauth_id='${id}'` 90 | ), 91 | ]); 92 | }; 93 | 94 | const getRecentInDB = (id, offset, limit) => { 95 | return innerJoinTable( 96 | 'game_date, game_mode, ranking, SEC_TO_TIME(play_time) as play_time, attack_cnt, attacked_cnt', 97 | 'PLAY', 98 | 'GAME_INFO', 99 | 'PLAY.game_id = GAME_INFO.game_id', 100 | `oauth_id='${id}'`, 101 | `game_date DESC`, 102 | `${offset}, ${limit}` 103 | ); 104 | }; 105 | 106 | export default ProfileRouter; 107 | -------------------------------------------------------------------------------- /back-end/api-server/routes/api-routes/rankingSearch.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { selectTable } from '../../database/query'; 3 | 4 | const RankingRouter = express.Router(); 5 | 6 | const categoryBox: any = { 7 | totalWin: 'player_win', 8 | attackCnt: 'attack_cnt', 9 | }; 10 | 11 | // 무한 스크롤 구현을 염두해서, offset을 추가적으로 받습니다. 12 | // offsetRank : 랭킹 몇 번째 까지 스크롤을 내렸는지 파악하기 위함. 13 | interface Query { 14 | category?: any; 15 | mode?: String; 16 | nickName?: String; 17 | offsetRank?: any; 18 | lastNickName?: String; // 클라이언트에서 마지막으로 뜬 닉네임 19 | } 20 | 21 | const rankResponse = { 22 | data: [], 23 | categoryKey: '', 24 | message: '', 25 | }; 26 | 27 | const profileResponse = { 28 | data: [], 29 | message: '', 30 | }; 31 | 32 | RankingRouter.post('/myInfo', async (req, res) => { 33 | try { 34 | const { oauthId } = req.body.myInfoTemplate; 35 | let queryResult = await selectTable( 36 | `sum(player_win) as player_win, sum(attack_cnt) as attack_cnt`, 37 | `PLAY group by oauth_id having oauth_id = '${oauthId}'` 38 | ); // 지금은 nickname이 아니라 oauth id이므로 추후 스토어에 추가되면 바꿀 예정. 39 | profileResponse.data = queryResult?.[0]; 40 | profileResponse.message = 'success'; 41 | res.status(200).json(profileResponse); 42 | } catch (error) { 43 | profileResponse.message = 'fail'; 44 | res.status(400).json(profileResponse); 45 | } 46 | }); 47 | 48 | RankingRouter.post('/', async (req, res) => { 49 | try { 50 | const { category, mode, nickName, offsetRank, lastNickName }: Query = req.body.rankApiTemplate; 51 | let queryResult = await selectTable( 52 | '*', 53 | `(SELECT 54 | u.nickname as nickname, 55 | sum(p.${categoryBox[category]}) as category, 56 | ANY_VALUE(u.state_message) as state_message, 57 | rank() over (order by sum(p.${categoryBox[category]}) desc) as ranking 58 | FROM 59 | PLAY as p 60 | inner join USER_INFO as u on p.oauth_id = u.oauth_id 61 | inner join GAME_INFO as g on p.game_id = g.game_id and g.\`game_mode\` = '${mode}' 62 | group by p.oauth_id) a` 63 | //`ranking >= ${Number(offsetRank)} and ranking < ${Number(offsetRank) + 20}` 64 | //바로 위 코드는 무한 스크롤 구현 시 고려해 볼 것 65 | ); 66 | // 클라이언트로 부터 받은 닉네임이 있으면, 그 닉네임 부터의 배열을 보내면 됨 67 | if (nickName) { 68 | let nickNameIndex = queryResult.findIndex(function (item) { 69 | return item.nickname === nickName; 70 | }); 71 | queryResult = nickNameIndex < 0 ? queryResult : queryResult.slice(nickNameIndex); // 정해지는 정책에 따라 다를 것으로 보임 72 | } 73 | rankResponse.data = queryResult; 74 | rankResponse.message = 'success'; 75 | res.status(200).json(rankResponse); 76 | } catch (error) { 77 | console.log(error); 78 | rankResponse.message = 'error'; 79 | res.status(400).json(rankResponse); 80 | } 81 | }); 82 | 83 | export default RankingRouter; 84 | -------------------------------------------------------------------------------- /back-end/api-server/routes/api-routes/registerDBInsert.ts: -------------------------------------------------------------------------------- 1 | import { setJWT } from '../../services/auth'; 2 | import * as express from 'express'; 3 | import { insertIntoTable } from '../../database/query'; 4 | 5 | const RegisterRouter = express.Router(); 6 | 7 | RegisterRouter.post('/insert', (req, res, next) => { 8 | //db insert 로직 필요 9 | const data = req.body.registerData; 10 | const nickName = data.nickname; 11 | const message = data.message; 12 | const authId = data.oauthInfo; 13 | if ( 14 | insertIntoTable( 15 | 'USER_INFO', 16 | '(nickname, state_message, oauth_id)', 17 | `'${nickName}', '${message}', '${authId}'` 18 | ) 19 | ) { 20 | setJWT(req, res, { nickname: nickName, oauth_id: authId }); 21 | res.json({ dupCheck: true, dbInsertError: false }); 22 | } else { 23 | // DB에 넣는 것이 실패한 경우 24 | res.json({ dupCheck: true, dbInsertError: true }); 25 | } 26 | }); 27 | 28 | export default RegisterRouter; 29 | //`INSERT INTO ${table} VALUES (${values})`; 30 | -------------------------------------------------------------------------------- /back-end/api-server/services/auth.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import * as jwt from 'jsonwebtoken'; 3 | 4 | export const getGithubUser = async (token) => { 5 | try { 6 | const { data } = await axios.get('https://api.github.com/user', { 7 | headers: { 8 | Authorization: `token ${token}`, 9 | 'Content-Type': 'application/json', 10 | }, 11 | }); 12 | return data; 13 | } catch (error) { 14 | return false; 15 | } 16 | }; 17 | 18 | export const getUserInfoFromNaver = async (accessToken) => { 19 | const header = 'Bearer ' + accessToken; 20 | const apiUrl = 'https://openapi.naver.com/v1/nid/me'; 21 | const headers = { 22 | Authorization: header, 23 | }; 24 | const { data } = await axios.get(apiUrl, { 25 | headers: headers, 26 | }); 27 | return data; 28 | }; 29 | 30 | export const setJWT = (req, res, { nickname, oauth_id }) => { 31 | const jwtSignature = jwt.sign( 32 | { expiresIn: '10h', nickname, oauth_id }, // 임의 값 넣어놓음 33 | process.env.JWT_SECRET_KEY 34 | ); 35 | res.cookie('user', jwtSignature, { 36 | expires: new Date(Date.now() + 10 * 60 * 60 * 1000), 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /back-end/api-server/services/friend.ts: -------------------------------------------------------------------------------- 1 | import { request } from 'express'; 2 | import { deleteTable, insertIntoTable, selectTable } from '../database/query'; 3 | 4 | export const requestFriend = async ({ requestee, requester }) => { 5 | try { 6 | await insertIntoTable( 7 | 'FRIEND_REQUEST', 8 | `(friend_requestee, friend_requester)`, 9 | `'${requestee}', '${requester}'` 10 | ); 11 | return true; 12 | } catch (error) { 13 | return false; 14 | } 15 | }; 16 | 17 | export const requestFriendUpdate = async ({ isAccept, requestee, requester }) => { 18 | try { 19 | // delete 한 후에 insert 부분에서 에러가 난 경우를 생각해서 추후에 개선 필요 20 | await deleteTable( 21 | 'FRIEND_REQUEST', 22 | `friend_requestee='${requestee}' and friend_requester='${requester}'` 23 | ); 24 | if (isAccept) { 25 | await insertIntoTable(`FRIENDSHIP`, `(friend1, friend2)`, `'${requestee}', '${requester}'`); 26 | await insertIntoTable(`FRIENDSHIP`, `(friend1, friend2)`, `'${requester}', '${requestee}'`); 27 | } 28 | return true; 29 | } catch (error) { 30 | return false; 31 | } 32 | }; 33 | 34 | export const requestFriendList = async (requestee) => { 35 | try { 36 | const result = await selectTable( 37 | `u.nickname, r.created_at, u.oauth_id`, 38 | `FRIEND_REQUEST r left outer join USER_INFO u ON r.friend_requester = u.oauth_id`, 39 | `r.friend_requestee = '${requestee}'` 40 | ); 41 | const returnData = []; 42 | result.map((value) => 43 | returnData.push({ 44 | oauth_id: value.oauth_id, 45 | nickname: value.nickname, 46 | created_at: value.created_at, 47 | }) 48 | ); 49 | return returnData; 50 | } catch (error) { 51 | return undefined; 52 | } 53 | }; 54 | 55 | export const getFriendList = async (oauthId) => { 56 | try { 57 | const result = await selectTable( 58 | `nickname`, 59 | `USER_INFO`, 60 | `oauth_id in (select friend2 from FRIENDSHIP where friend1='${oauthId}')` 61 | ); 62 | const returnData = []; 63 | result.map((value) => returnData.push(value.nickname)); 64 | return returnData; 65 | } catch (error) { 66 | return undefined; 67 | } 68 | }; 69 | 70 | export const checkAlreadyFriend = async ({ requestee, requester }) => { 71 | try { 72 | if (requestee === requester) return false; // 나 자신과 친구를 맺을 수 없으므로 73 | const result = await selectTable( 74 | `friend1`, 75 | `FRIENDSHIP`, 76 | `friend1='${requestee}' and friend2='${requester}'` 77 | ); 78 | if (result && result.length > 0) { 79 | return true; 80 | } else { 81 | return false; 82 | } 83 | } catch (error) { 84 | return false; 85 | } 86 | }; 87 | -------------------------------------------------------------------------------- /back-end/api-server/services/gameRecord.ts: -------------------------------------------------------------------------------- 1 | import { insertIntoTable } from '../database/query'; 2 | 3 | export const insertGameInfo = async (game) => { 4 | try { 5 | await insertIntoTable( 6 | `GAME_INFO`, 7 | `(game_id, game_date, game_mode)`, 8 | `'${game.game_id}', '${game.game_date}', '${game.game_mode}'` 9 | ); 10 | return true; 11 | } catch (error) { 12 | return false; 13 | } 14 | }; 15 | 16 | export const insertPlayerInfo = (game_id, players) => { 17 | try { 18 | players.map(async (obj) => { 19 | obj.player_win = obj.player_win === false ? 0 : 1; 20 | await insertIntoTable( 21 | `PLAY`, 22 | `(oauth_id, game_id, play_time, ranking, attack_cnt, attacked_cnt, player_win)`, 23 | `'${obj.oauth_id}', '${game_id}', ${obj.play_time}, ${obj.ranking}, ${obj.attack_cnt}, ${obj.attacked_cnt}, ${obj.player_win}` 24 | ); 25 | }); 26 | return true; 27 | } catch (error) { 28 | return false; 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /back-end/api-server/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | const cors = require('cors'); 3 | const cookieParser = require('cookie-parser'); 4 | const path = require('path'); 5 | const logger = require('morgan'); 6 | 7 | import 'dotenv/config'; 8 | import * as swaggerUi from 'swagger-ui-express'; 9 | import * as YAML from 'yamljs'; 10 | import ApiRouter from '../routes/api-routes/index'; 11 | 12 | class App { 13 | public application: express.Application; 14 | constructor() { 15 | this.application = express(); 16 | } 17 | } 18 | const app = new App().application; 19 | const swaggerSpec = YAML.load(path.join(__dirname, '../build/swagger.yaml')); 20 | 21 | app.use(cookieParser()); 22 | app.use(express.json()); 23 | app.use( 24 | cors({ 25 | origin: true, 26 | credentials: true, 27 | }) 28 | ); 29 | app.use(logger('dev')); 30 | app.use(express.urlencoded({ extended: false })); 31 | app.use(express.static(path.join(__dirname, 'public'))); 32 | app.use('/api', ApiRouter); 33 | app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); 34 | 35 | app.listen(4000, () => console.log('start')); 36 | -------------------------------------------------------------------------------- /back-end/api-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es5", "es6"], 4 | "target": "es5", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "outDir": "./build", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "sourceMap": true 11 | }, 12 | "exclude": [], 13 | "include": ["./src/"] 14 | } 15 | -------------------------------------------------------------------------------- /back-end/socket-server/constant/room.ts: -------------------------------------------------------------------------------- 1 | export let roomList = []; 2 | 3 | export const setRoomList = (rooms) => { 4 | roomList = rooms; 5 | } -------------------------------------------------------------------------------- /back-end/socket-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "socket-server", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "npx nodemon --exec ts-node ./src/index.ts" 7 | }, 8 | "dependencies": { 9 | "@socket.io/redis-adapter": "^7.0.1", 10 | "@socket.io/redis-emitter": "^4.1.0", 11 | "@types/express": "^4.17.13", 12 | "axios": "^0.24.0", 13 | "cookie-parser": "~1.4.4", 14 | "debug": "~2.6.9", 15 | "dotenv": "^10.0.0", 16 | "express": "~4.16.1", 17 | "jsonwebtoken": "^8.5.1", 18 | "morgan": "~1.9.1", 19 | "redis": "^3.1.2", 20 | "redis-lock": "^0.1.4", 21 | "socket.io": "^4.3.2", 22 | "typescript": "^4.4.4" 23 | }, 24 | "devDependencies": { 25 | "@types/redis": "^2.8.32", 26 | "nodemon": "^2.0.14", 27 | "ts-node": "^10.4.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /back-end/socket-server/services/lobbyUserSocket.ts: -------------------------------------------------------------------------------- 1 | import { getSockets } from './../utils/userUtil'; 2 | import { Namespace } from 'socket.io'; 3 | import { randomUUID } from 'crypto'; 4 | 5 | import { userRemote, userSocket } from '../type/socketType'; 6 | import { 7 | broadcastUserList, 8 | broadcastRoomList, 9 | updateRoomCurrent, 10 | getRooms, 11 | } from '../utils/userUtil'; 12 | import { pubClient } from './socket'; 13 | import { RedisAdapter } from '@socket.io/redis-adapter'; 14 | 15 | export const initLobbyUserSocket = (mainSpace: Namespace, socket: userSocket) => { 16 | socket.on('duplicate check', async (oauthID) => { 17 | const sockets = await getSockets(mainSpace); 18 | if (sockets.filter((s) => s.oauthID === oauthID.toString()).length >= 1) { 19 | mainSpace.to(socket.id).emit('duplicate check:fail'); 20 | socket.disconnect(); 21 | } else { 22 | mainSpace.emit('duplicate check:success'); 23 | } 24 | }); 25 | 26 | socket.on('set userName', async (userName, oauthID) => { 27 | await pubClient.SADD('sid', socket.id); 28 | await pubClient.HSET(`user:${socket.id}`, 'userName', userName, 'oauthID', oauthID); 29 | 30 | socket.userName = userName; 31 | socket.oauthID = oauthID; 32 | broadcastUserList(mainSpace); 33 | broadcastRoomList(mainSpace); 34 | }); 35 | 36 | socket.on('create room', ({ owner, name, limit, nickname }) => { 37 | try { 38 | let newRoomID = randomUUID(); 39 | socket.roomID = newRoomID; 40 | socket.join(newRoomID); 41 | pubClient.set( 42 | `room:${newRoomID}`, 43 | JSON.stringify({ 44 | id: newRoomID, 45 | owner, 46 | name, 47 | limit, 48 | current: mainSpace.adapter.rooms.get(newRoomID).size, 49 | gameOverPlayer: 0, 50 | garbageBlockCnt: [], 51 | gameStart: false, 52 | player: [{ id: socket.id, nickname: nickname }], 53 | gamingPlayer: [], 54 | rank: [], 55 | semaphore: 0, 56 | }) 57 | ); 58 | 59 | mainSpace.to(socket.id).emit('create room:success', newRoomID); 60 | 61 | broadcastRoomList(mainSpace); 62 | } catch (error) { 63 | mainSpace.to(socket.id).emit('create room:fail'); 64 | } 65 | }); 66 | 67 | socket.on('check valid room', async ({ roomID, id }) => { 68 | const roomList = await getRooms(mainSpace); 69 | const target = roomList.find((r) => r.id === roomID); 70 | const redisAdapter = mainSpace.adapter as RedisAdapter; 71 | 72 | if ( 73 | target && 74 | target.current < target.limit && 75 | (await redisAdapter.allRooms()).has(roomID) && 76 | !(await redisAdapter.sockets(new Set([roomID]))).has(id) 77 | ) { 78 | try { 79 | const isPlayer = target.player.find((p) => p.id === socket.id); 80 | 81 | if (!isPlayer) { 82 | target.player.push({ id: socket.id, nickname: socket.userName }); 83 | pubClient.set(`room:${target.id}`, JSON.stringify(target)); 84 | socket.roomID = roomID; 85 | socket.join(roomID); 86 | 87 | updateRoomCurrent(mainSpace, roomID); 88 | 89 | mainSpace.to(socket.id).emit('join room:success', roomID, target.gameStart); 90 | socket.broadcast.to(roomID).emit('enter new player', socket.id, socket.userName); 91 | } 92 | } catch (error) { 93 | mainSpace.to(socket.id).emit('join room:fail', roomID); 94 | } 95 | } else { 96 | mainSpace.to(socket.id).emit('redirect to lobby'); 97 | } 98 | }); 99 | 100 | socket.on('leave room', async (roomID: string) => { 101 | const roomList = await getRooms(mainSpace); 102 | const target = roomList.find((r) => r.id === roomID); 103 | 104 | if (target) { 105 | target.player = target.player.filter((p) => p.id !== socket.id); 106 | target.gamingPlayer = target.gamingPlayer.filter((p) => p.id !== socket.id); 107 | target.garbageBlockCnt = target.garbageBlockCnt.filter((p) => p.id !== socket.id); 108 | target.rank = target.rank.filter((r) => r.nickname !== socket.userName); 109 | pubClient.set(`room:${target.id}`, JSON.stringify(target)); 110 | 111 | if (target.gamingPlayer.length === 1) { 112 | mainSpace.to(roomID).emit('every player game over'); 113 | target.gameStart = false; 114 | pubClient.set(`room:${target.id}`, JSON.stringify(target)); 115 | } 116 | 117 | socket.broadcast.to(socket.roomID).emit('leave player', socket.id); 118 | socket.leave(roomID); 119 | broadcastRoomList(mainSpace); 120 | } 121 | }); 122 | 123 | socket.on('join room', async (roomID: string, nickname) => { 124 | const roomList = await getRooms(mainSpace); 125 | const target = roomList.find((r) => r.id === roomID); 126 | 127 | try { 128 | const isPlayer = target.player.find((p) => p.id === socket.id); 129 | 130 | if (!isPlayer) { 131 | target.player.push({ id: socket.id, nickname: nickname }); 132 | 133 | socket.roomID = roomID; 134 | socket.join(roomID); 135 | pubClient.set(`room:${target.id}`, JSON.stringify(target)); 136 | updateRoomCurrent(mainSpace, roomID); 137 | 138 | mainSpace.to(socket.id).emit('join room:success', roomID, target.gameStart); 139 | socket.broadcast.to(roomID).emit('enter new player', socket.id, nickname); 140 | } 141 | } catch (error) { 142 | mainSpace.to(socket.id).emit('join room:fail', roomID); 143 | } 144 | }); 145 | 146 | socket.on('send message', ({ roomID, from, message, id }) => { 147 | mainSpace.to(roomID).emit('receive message', { id, from, message }); 148 | }); 149 | 150 | socket.on('send lobby message', ({ from, message, id }) => { 151 | mainSpace.emit('receive lobby message', { id, from, message }); 152 | }); 153 | 154 | socket.on('refresh friend list', async (oauthID) => { 155 | const sockets = await getSockets(mainSpace); 156 | const target = sockets.find((s) => s.oauthID === oauthID); 157 | if (target) { 158 | mainSpace.to(target.id).emit('refresh friend list'); 159 | } 160 | }); 161 | 162 | socket.on('refresh request list', (socketId) => { 163 | mainSpace.to(socketId).emit('refresh request list'); 164 | }); 165 | 166 | socket.on('disconnecting', async () => { 167 | await pubClient.HDEL(`user:${socket.id}`, 'userName', 'oauthID'); 168 | const roomList = await getRooms(mainSpace); 169 | const target = roomList.find((r) => r.id === socket.roomID); 170 | 171 | if (target) { 172 | target.player = target.player.filter((p) => p.id !== socket.id); 173 | socket.broadcast.to(socket.roomID).emit('leave player', socket.id); 174 | pubClient.set(`room:${target.id}`, JSON.stringify(target)); 175 | 176 | if (target.player.length === 1) { 177 | mainSpace.to(socket.roomID).emit('every player game over'); 178 | target.gameStart = false; 179 | pubClient.set(`room:${target.id}`, JSON.stringify(target)); 180 | } 181 | } 182 | 183 | const roomsWillDelete = []; 184 | const redisAdapter = mainSpace.adapter as RedisAdapter; 185 | [...(await redisAdapter.allRooms())].forEach(async (rId) => { 186 | if (socket.rooms.has(rId) && (await redisAdapter.sockets(new Set([rId]))).size === 1) 187 | roomsWillDelete.push(rId); 188 | }); 189 | roomsWillDelete.forEach((rID) => { 190 | pubClient.del(`room:${rID}`); 191 | }); 192 | broadcastRoomList(mainSpace); 193 | }); 194 | 195 | socket.on('disconnect', async () => { 196 | broadcastUserList(mainSpace); 197 | }); 198 | }; 199 | -------------------------------------------------------------------------------- /back-end/socket-server/services/socket.ts: -------------------------------------------------------------------------------- 1 | import { 2 | broadcastRoomMemberUpdate, 3 | broadcastRoomList, 4 | updateRoomCurrent, 5 | getRooms, 6 | } from './../utils/userUtil'; 7 | import { Server } from 'socket.io'; 8 | 9 | import { initLobbyUserSocket } from './lobbyUserSocket'; 10 | import { initTetrisSocket } from './tetrisSocket'; 11 | import { userSocket } from '../type/socketType'; 12 | import { createAdapter } from '@socket.io/redis-adapter'; 13 | 14 | import { createClient } from 'redis'; 15 | import { promisify } from 'util'; 16 | const { Emitter } = require('@socket.io/redis-emitter'); 17 | const LOCK = require('redis-lock'); 18 | 19 | const wrap = (middleware) => (socket, next) => middleware(socket.request, {}, next); 20 | 21 | const io = new Server(); 22 | 23 | export const pubClient = createClient({ host: 'localhost', port: 6379 }); 24 | 25 | const subClient = pubClient.duplicate(); 26 | 27 | export const redisEmitter = new Emitter(pubClient); 28 | 29 | export const getallAsync = promisify(pubClient.HGETALL).bind(pubClient); 30 | export const hgetAsync = promisify(pubClient.hget).bind(pubClient); 31 | export const getAsync = promisify(pubClient.get).bind(pubClient); 32 | export const asmembers = promisify(pubClient.smembers).bind(pubClient); 33 | export const ahkeys = promisify(pubClient.hkeys).bind(pubClient); 34 | export const lock = promisify(LOCK(pubClient)); 35 | 36 | export const initSocket = (httpServer, port) => { 37 | const io = new Server(httpServer, { 38 | /* options */ 39 | cors: { 40 | origin: '*', 41 | }, 42 | }); 43 | io.sockets.setMaxListeners(0); 44 | 45 | io.adapter(createAdapter(pubClient, subClient)); 46 | 47 | const mainSpace = io.of('/'); 48 | 49 | mainSpace.setMaxListeners(0); 50 | mainSpace.adapter.setMaxListeners(0); 51 | 52 | mainSpace.on('connection', async (socket: userSocket) => { 53 | socket.setMaxListeners(0); 54 | socket.emit('port notify', port); 55 | initLobbyUserSocket(mainSpace, socket); 56 | initTetrisSocket(mainSpace, socket); 57 | }); 58 | mainSpace.adapter.on('create-room', (room) => {}); 59 | mainSpace.adapter.on('join-room', async (room, id) => { 60 | await updateRoomCurrent(mainSpace, room); 61 | await broadcastRoomMemberUpdate(mainSpace, room, id); 62 | await broadcastRoomList(mainSpace); 63 | }); 64 | 65 | mainSpace.adapter.on('leave-room', async (roomID, id) => { 66 | const roomList = await getRooms(mainSpace); 67 | const target = roomList.find((r) => r.id === roomID); 68 | 69 | if (target) { 70 | target.player = target.player.filter((p) => p.id !== id); 71 | target.gamingPlayer = target.gamingPlayer.filter((p) => p.id !== id); 72 | target.garbageBlockCnt = target.garbageBlockCnt.filter((p) => p.id !== id); 73 | target.rank = target.rank.filter((r) => r.id !== id); 74 | target.current = (await mainSpace.adapter.sockets(new Set([roomID]))).size; 75 | await pubClient.set(`room:${target.id}`, JSON.stringify(target)); 76 | if (target.gamingPlayer.length === 1) { 77 | mainSpace.to(roomID).emit('every player game over'); 78 | target.gameStart = false; 79 | await pubClient.set(`room:${target.id}`, JSON.stringify(target)); 80 | } 81 | } 82 | 83 | await updateRoomCurrent(mainSpace, roomID); 84 | await broadcastRoomMemberUpdate(mainSpace, roomID, id); 85 | await broadcastRoomList(mainSpace); 86 | }); 87 | }; 88 | -------------------------------------------------------------------------------- /back-end/socket-server/services/tetrisSocket.ts: -------------------------------------------------------------------------------- 1 | import { getRooms } from './../utils/userUtil'; 2 | import { Namespace } from 'socket.io'; 3 | 4 | import { 5 | gameOverProcess, 6 | initGameInfo, 7 | playerAttackProcess, 8 | calcPlayerRank, 9 | } from './../utils/tetrisUtil'; 10 | import { userSocket } from '../type/socketType'; 11 | 12 | export const initTetrisSocket = (mainSpace: Namespace, socket: userSocket) => { 13 | socket.on('get other players info', async (res) => { 14 | const roomList = await getRooms(mainSpace); 15 | const target = roomList.find((r) => r.id === socket.roomID); 16 | 17 | if (target) { 18 | const otherPlayer = target.player.filter((p) => p.id !== socket.id); 19 | res(otherPlayer); 20 | } 21 | }); 22 | 23 | socket.on('game start', async () => { 24 | const roomList = await getRooms(mainSpace); 25 | const target = roomList.find((r) => r.id === socket.roomID); 26 | 27 | if (target) { 28 | initGameInfo(mainSpace, socket, target); 29 | } 30 | }); 31 | 32 | socket.on('drop block', (board, block) => { 33 | socket.broadcast.to(socket.roomID).emit(`other player's drop block`, socket.id, board, block); 34 | }); 35 | 36 | socket.on('attack other player', async (garbage) => { 37 | const roomList = await getRooms(mainSpace); 38 | const target = roomList.find((r) => r.id === socket.roomID); 39 | 40 | if (target && target.garbageBlockCnt.length !== 1) { 41 | playerAttackProcess(mainSpace, socket, target, garbage); 42 | } 43 | }); 44 | 45 | socket.on('attacked finish', () => { 46 | socket.broadcast.to(socket.roomID).emit('someone attacked finish', socket.id); 47 | }); 48 | 49 | socket.on('game over', async () => { 50 | const roomList = await getRooms(mainSpace); 51 | const target = roomList.find((r) => r.id === socket.roomID); 52 | 53 | if (target) { 54 | gameOverProcess(mainSpace, socket, target); 55 | } 56 | }); 57 | 58 | socket.on('get game over info', async (data) => { 59 | const roomList = await getRooms(mainSpace); 60 | const target = roomList.find((r) => r.id === socket.roomID); 61 | 62 | if (target) { 63 | calcPlayerRank(mainSpace, socket, target, data); 64 | } 65 | }); 66 | }; 67 | -------------------------------------------------------------------------------- /back-end/socket-server/src/index.ts: -------------------------------------------------------------------------------- 1 | import { initSocket } from './../services/socket'; 2 | import * as express from 'express'; 3 | import 'dotenv/config'; 4 | 5 | const cors = require('cors'); 6 | const cookieParser = require('cookie-parser'); 7 | const path = require('path'); 8 | const logger = require('morgan'); 9 | const http = require('http'); 10 | 11 | const port = process.env.PORT || 5001; 12 | class App { 13 | public application: express.Application; 14 | constructor() { 15 | this.application = express(); 16 | } 17 | } 18 | 19 | const app = new App().application; 20 | app.use(cookieParser()); 21 | app.use(express.json()); 22 | app.use( 23 | cors({ 24 | origin: true, 25 | credentials: true, 26 | }) 27 | ); 28 | app.use(logger('dev')); 29 | app.use(express.urlencoded({ extended: false })); 30 | app.use(express.static(path.join(__dirname, 'public'))); 31 | 32 | const server = http.createServer(app); 33 | 34 | initSocket(server, port); 35 | 36 | server.listen(port); 37 | -------------------------------------------------------------------------------- /back-end/socket-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es5", "es6"], 4 | "target": "es5", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "outDir": "./build", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "sourceMap": true, 11 | "downlevelIteration": true 12 | }, 13 | "exclude": [], 14 | "include": ["./src/"] 15 | } 16 | -------------------------------------------------------------------------------- /back-end/socket-server/type/socketType.ts: -------------------------------------------------------------------------------- 1 | import { RemoteSocket, Socket } from 'socket.io'; 2 | import { DefaultEventsMap } from 'socket.io/dist/typed-events'; 3 | 4 | export interface userRemote extends RemoteSocket { 5 | userName: string; 6 | oauthID: string; 7 | } 8 | 9 | export interface userSocket extends Socket { 10 | userName: string; 11 | roomID: string; 12 | oauthID: string; 13 | } 14 | -------------------------------------------------------------------------------- /back-end/socket-server/utils/dateUtil.ts: -------------------------------------------------------------------------------- 1 | // 현재 시간을 구하는 함수 yyyy-mm-dd hh:mm:ss 형태로 (DB의 datetime형태랑 동일) 2 | export const getTimeStamp = (date) => { 3 | let d = new Date(date); 4 | let s = 5 | leadingZeros(d.getFullYear(), 4) + 6 | '-' + 7 | leadingZeros(d.getMonth() + 1, 2) + 8 | '-' + 9 | leadingZeros(d.getDate(), 2) + 10 | ' ' + 11 | leadingZeros(d.getHours(), 2) + 12 | ':' + 13 | leadingZeros(d.getMinutes(), 2) + 14 | ':' + 15 | leadingZeros(d.getSeconds(), 2); 16 | 17 | return s; 18 | }; 19 | 20 | // 0 붙이기 21 | const leadingZeros = (n, digits) => { 22 | let zero = ''; 23 | n = n.toString(); 24 | 25 | if (n.length < digits) { 26 | for (let i = 0; i < digits - n.length; i++) zero += '0'; 27 | } 28 | 29 | return zero + n; 30 | }; 31 | -------------------------------------------------------------------------------- /back-end/socket-server/utils/tetrisUtil.ts: -------------------------------------------------------------------------------- 1 | import { pubClient, getAsync, lock } from './../services/socket'; 2 | import axios from 'axios'; 3 | import { getTimeStamp } from '../utils/dateUtil'; 4 | 5 | export const initGameInfo = (mainSpace, socket, target) => { 6 | [ 7 | target.gameStart, 8 | target.semaphore, 9 | target.gameOverPlayer, 10 | target.rank, 11 | target.gamingPlayer, 12 | target.garbageBlockCnt, 13 | ] = [true, 0, 0, [], [], []]; 14 | 15 | target.player.forEach((p) => { 16 | target.gamingPlayer.push({ id: p.id }); 17 | target.garbageBlockCnt.push({ id: p.id, garbageCnt: 0 }); 18 | }); 19 | 20 | pubClient.set(`room:${target.id}`, JSON.stringify(target)); 21 | 22 | mainSpace.to(socket.roomID).emit('game started'); 23 | }; 24 | 25 | export const playerAttackProcess = (mainSpace, socket, target, garbage) => { 26 | let idx = setAttackPlayer(target, socket.id); 27 | mainSpace.to(target.garbageBlockCnt[idx].id).emit('attacked', garbage); 28 | 29 | target.gamingPlayer.forEach((p) => { 30 | if (p.id === target.garbageBlockCnt[idx].id) return; 31 | mainSpace.to(p.id).emit('someone attacked', garbage, target.garbageBlockCnt[idx].id); 32 | }); 33 | 34 | target.garbageBlockCnt[idx].garbageCnt += garbage; 35 | pubClient.set(`room:${target.id}`, JSON.stringify(target)); 36 | }; 37 | 38 | const setAttackPlayer = (target, id) => { 39 | target.garbageBlockCnt.sort((a, b) => a.garbageCnt - b.garbageCnt); 40 | 41 | let idx = 0; 42 | 43 | if (target.garbageBlockCnt[idx].id === id) { 44 | idx++; 45 | } 46 | 47 | pubClient.set(`room:${target.id}`, JSON.stringify(target)); 48 | return idx; 49 | }; 50 | 51 | export const gameOverProcess = (mainSpace, socket, target) => { 52 | target.garbageBlockCnt = target.garbageBlockCnt.filter((p) => p.id !== socket.id); 53 | target.rank.push({ 54 | id: socket.id, 55 | oauthID: '', 56 | nickname: socket.userName, 57 | playTime: 0, 58 | ranking: target.gamingPlayer.length - target.gameOverPlayer, 59 | attackCnt: 0, 60 | attackedCnt: 0, 61 | }); 62 | target.gameOverPlayer++; 63 | 64 | pubClient.set(`room:${target.id}`, JSON.stringify(target)); 65 | everyPlayerGameOverCheck(mainSpace, socket, target); 66 | }; 67 | 68 | const everyPlayerGameOverCheck = (mainSpace, socket, target) => { 69 | if ( 70 | target.gamingPlayer.length === 1 || 71 | target.gameOverPlayer === target.gamingPlayer.length - 1 72 | ) { 73 | pubClient.set(`room:${target.id}`, JSON.stringify(target)); 74 | mainSpace.to(socket.roomID).emit('every player game over'); 75 | 76 | if (target.gamingPlayer.length !== 1) { 77 | pubClient.set(`room:${target.id}`, JSON.stringify(target)); 78 | target.gamingPlayer.forEach((p) => { 79 | mainSpace.to(p.id).emit('game over info'); 80 | }); 81 | } 82 | } 83 | 84 | pubClient.set(`room:${target.id}`, JSON.stringify(target)); 85 | }; 86 | 87 | export const calcPlayerRank = async (mainSpace, socket, target, data) => { 88 | const unlock = await lock(`${target.id}`); 89 | pubClient.get(`room:${target.id}`, (err, result) => { 90 | let target = JSON.parse(result); 91 | const rankTarget = target.rank.find((r) => r.nickname === socket.userName); 92 | 93 | target.semaphore++; 94 | pubClient.set(`room:${target.id}`, JSON.stringify(target)); 95 | if (!rankTarget) { 96 | target.rank.push({ 97 | id: socket.id, 98 | oauthID: data.oauthID, 99 | nickname: socket.userName, 100 | playTime: data.playTime, 101 | ranking: 1, 102 | attackCnt: data.attackCnt, 103 | attackedCnt: data.attackedCnt, 104 | }); 105 | pubClient.set(`room:${target.id}`, JSON.stringify(target)); 106 | } else { 107 | [rankTarget.oauthID, rankTarget.playTime, rankTarget.attackCnt, rankTarget.attackedCnt] = [ 108 | data.oauthID, 109 | data.playTime, 110 | data.attackCnt, 111 | data.attackedCnt, 112 | ]; 113 | pubClient.set(`room:${target.id}`, JSON.stringify(target)); 114 | } 115 | 116 | pubClient.set(`room:${target.id}`, JSON.stringify(target)); 117 | 118 | if (target.gameStart && target.semaphore === target.gamingPlayer.length) { 119 | [target.gameStart, target.semaphore, target.gameOverPlayer] = [false, 0, 0]; 120 | 121 | target.rank.sort((a, b) => a.ranking - b.ranking); 122 | mainSpace.to(socket.roomID).emit('send rank table', target.rank); 123 | 124 | pubClient.set(`room:${target.id}`, JSON.stringify(target)); 125 | sendData(makeData(socket, target)); 126 | } 127 | }); 128 | unlock(); 129 | }; 130 | 131 | const makeData = (socket, target) => { 132 | const date = new Date(); 133 | const game_id = `${socket.roomID}${date.getTime()}`; 134 | const data = { 135 | game: { 136 | game_id: game_id, 137 | game_date: getTimeStamp(date), 138 | game_mode: 'normal', 139 | }, 140 | players: [], 141 | }; 142 | 143 | target.rank.forEach((r, idx) => { 144 | data.players.push({ 145 | oauth_id: r.oauthID, 146 | play_time: r.playTime, 147 | ranking: r.ranking, 148 | attack_cnt: r.attackCnt, 149 | attacked_cnt: r.attackedCnt, 150 | player_win: idx === 0 ? true : false, 151 | }); 152 | }); 153 | 154 | pubClient.set(`room:${target.id}`, JSON.stringify(target)); 155 | return data; 156 | }; 157 | 158 | const sendData = (data) => { 159 | axios({ 160 | method: 'post', 161 | url: `${process.env.API_HOST}/api/game/record`, 162 | data: data, 163 | }); 164 | }; 165 | -------------------------------------------------------------------------------- /back-end/socket-server/utils/userUtil.ts: -------------------------------------------------------------------------------- 1 | import { RedisAdapter } from '@socket.io/redis-adapter'; 2 | import { getallAsync, getAsync, asmembers, pubClient, redisEmitter } from './../services/socket'; 3 | import { Namespace } from 'socket.io'; 4 | 5 | import { userRemote } from '../type/socketType'; 6 | 7 | export const getRooms = async (mainSpace: Namespace) => { 8 | const sockeList = await mainSpace.adapter.sockets(new Set()); 9 | const redisAdapter = mainSpace.adapter as RedisAdapter; 10 | const roomIDs = [...(await redisAdapter.allRooms())].filter((r) => !sockeList.has(r)); 11 | const roomList = []; 12 | await Promise.all( 13 | roomIDs.map(async (roomId) => { 14 | const data = await getAsync(`room:${roomId}`); 15 | if (data) { 16 | const roomInfo = JSON.parse(data); 17 | if (roomInfo.current !== 0) { 18 | roomList.push(roomInfo); 19 | } 20 | } 21 | return data; 22 | }) 23 | ); 24 | 25 | return roomList; 26 | }; 27 | 28 | export const getSockets = async (mainSpace: Namespace) => { 29 | const sids = [...(await mainSpace.adapter.sockets(new Set()))]; 30 | 31 | const sockets = []; 32 | await Promise.all( 33 | sids.map(async (sid) => { 34 | const data = await getallAsync(`user:${sid}`); 35 | if (data) { 36 | sockets.push({ ...data, id: sid, nickname: data.userName }); 37 | } 38 | return data; 39 | }) 40 | ); 41 | 42 | return sockets; 43 | }; 44 | 45 | export const broadcastRoomList = async (mainSpace: Namespace) => { 46 | const roomList = await getRooms(mainSpace); 47 | redisEmitter.emit('room list update', roomList); 48 | }; 49 | 50 | export const broadcastUserList = async (mainSpace: Namespace) => { 51 | const sockets = await getSockets(mainSpace); 52 | mainSpace.emit( 53 | 'user list update', 54 | sockets.filter((s) => s.userName) 55 | ); 56 | }; 57 | 58 | export const updateRoomCurrent = async (mainSpace: Namespace, room) => { 59 | const roomList = await getRooms(mainSpace); 60 | let target = roomList.find((r) => r.id === room); 61 | if (target) { 62 | target.current = (await mainSpace.adapter.sockets(new Set([room]))).size; 63 | await pubClient.set(`room:${target.id}`, JSON.stringify(target)); 64 | } 65 | }; 66 | 67 | export const broadcastRoomMemberUpdate = async (mainSpace: Namespace, room, id) => { 68 | const sockets = await getSockets(mainSpace); 69 | const redisAdapter = mainSpace.adapter as RedisAdapter; 70 | const targetSockets = await mainSpace.adapter.sockets(new Set([room])); 71 | if (room !== id && targetSockets.size) { 72 | const socketsInRoom = [...targetSockets].map((sid) => sockets.find((s) => s.id === sid)); 73 | 74 | mainSpace.to(room).emit( 75 | 'room member list', 76 | socketsInRoom.map((s) => ({ nickname: s.userName, id: s.id, oauthID: s.oauthID })) 77 | ); 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | cd /var/www/app/web24-boostris 2 | git fetch --all 3 | git reset --hard origin/main 4 | cd /var/www/app/web24-boostris/front-end 5 | npm ci 6 | npm run build 7 | rm -rf /var/www/html/* 8 | cp -r /var/www/app/web24-boostris/front-end/build/* /var/www/html 9 | cd /var/www/app/web24-boostris/back-end/api-server 10 | pm2 reload ts-node 11 | -------------------------------------------------------------------------------- /front-end/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app), using the [Redux](https://redux.js.org/) and [Redux Toolkit](https://redux-toolkit.js.org/) template. 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | -------------------------------------------------------------------------------- /front-end/craco.config.js: -------------------------------------------------------------------------------- 1 | // craco.config.js 2 | const webpack = require("webpack") 3 | const BundleAnalyzerPlugin = require("webpack-bundle-analyzer") 4 | .BundleAnalyzerPlugin 5 | 6 | module.exports = function ({ env }) { 7 | const isProductionBuild = process.env.NODE_ENV === "production" 8 | const analyzerMode = process.env.REACT_APP_INTERACTIVE_ANALYZE 9 | ? "server" 10 | : "json" 11 | 12 | const plugins = [] 13 | 14 | if (isProductionBuild) { 15 | plugins.push(new BundleAnalyzerPlugin({ analyzerMode })) 16 | } 17 | 18 | return { 19 | webpack: { 20 | plugins, 21 | }, 22 | } 23 | } -------------------------------------------------------------------------------- /front-end/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "front-end", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@reduxjs/toolkit": "^1.6.2", 7 | "@testing-library/jest-dom": "^4.2.4", 8 | "@testing-library/react": "^9.5.0", 9 | "@testing-library/user-event": "^7.2.1", 10 | "node-sass": "^6.0.1", 11 | "react": "^17.0.2", 12 | "react-dom": "^17.0.2", 13 | "react-google-login": "^5.2.2", 14 | "react-helmet-async": "^1.1.2", 15 | "react-redux": "^7.2.6", 16 | "react-router-dom": "^6.0.0", 17 | "react-scripts": "4.0.3", 18 | "react-virtual": "^2.8.2", 19 | "socket.io-client": "^4.3.2" 20 | }, 21 | "devDependencies": { 22 | "@craco/craco": "^6.4.2", 23 | "@types/jest": "^27.0.2", 24 | "@types/node": "^16.11.6", 25 | "@types/react": "^17.0.34", 26 | "@types/react-dom": "^17.0.11", 27 | "@types/react-helmet": "^6.1.4", 28 | "@types/react-redux": "^7.1.20", 29 | "@types/react-router-dom": "^5.3.2", 30 | "cross-env": "^7.0.3", 31 | "http-proxy-middleware": "^2.0.1", 32 | "typescript": "^4.4.4", 33 | "webpack-bundle-analyzer": "^4.5.0" 34 | }, 35 | "scripts": { 36 | "start": "craco start", 37 | "build": "craco build", 38 | "test": "craco test", 39 | "analyze": "cross-env REACT_APP_INTERACTIVE_ANALYZE=1 npm run build", 40 | "eject": "react-scripts eject" 41 | }, 42 | "eslintConfig": { 43 | "extends": "react-app" 44 | }, 45 | "browserslist": { 46 | "production": [ 47 | ">0.2%", 48 | "not dead", 49 | "not op_mini all" 50 | ], 51 | "development": [ 52 | "last 1 chrome version", 53 | "last 1 firefox version", 54 | "last 1 safari version" 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /front-end/public/assets/block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web24-boostris/12074beb6b335111019a4bed22228756de318a84/front-end/public/assets/block.png -------------------------------------------------------------------------------- /front-end/public/assets/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web24-boostris/12074beb6b335111019a4bed22228756de318a84/front-end/public/assets/error.png -------------------------------------------------------------------------------- /front-end/public/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web24-boostris/12074beb6b335111019a4bed22228756de318a84/front-end/public/assets/logo.png -------------------------------------------------------------------------------- /front-end/public/assets/logo_appbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web24-boostris/12074beb6b335111019a4bed22228756de318a84/front-end/public/assets/logo_appbar.png -------------------------------------------------------------------------------- /front-end/public/assets/other_block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web24-boostris/12074beb6b335111019a4bed22228756de318a84/front-end/public/assets/other_block.png -------------------------------------------------------------------------------- /front-end/public/assets/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web24-boostris/12074beb6b335111019a4bed22228756de318a84/front-end/public/assets/profile.png -------------------------------------------------------------------------------- /front-end/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web24-boostris/12074beb6b335111019a4bed22228756de318a84/front-end/public/favicon.ico -------------------------------------------------------------------------------- /front-end/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 26 | Boostris 27 | 28 | 29 | 30 |
31 |
32 |
33 | 43 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /front-end/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web24-boostris/12074beb6b335111019a4bed22228756de318a84/front-end/public/logo192.png -------------------------------------------------------------------------------- /front-end/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web24-boostris/12074beb6b335111019a4bed22228756de318a84/front-end/public/logo512.png -------------------------------------------------------------------------------- /front-end/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 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /front-end/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /front-end/src/App.scss: -------------------------------------------------------------------------------- 1 | @import 'common/styles/base'; 2 | 3 | .App { 4 | color: $white; 5 | background-color: $dark-navy; 6 | display: flex; 7 | flex: 1; 8 | overflow: auto; 9 | 10 | & > .full__page--root { 11 | flex: 1; 12 | display: flex; 13 | flex-direction: column; 14 | align-items: center; 15 | justify-content: center; 16 | } 17 | } 18 | 19 | .mb--40 { 20 | padding-bottom: 40px; 21 | } 22 | 23 | .absolute_border_bottom { 24 | position: relative; 25 | &::after { 26 | content: ''; 27 | position: absolute; 28 | background-color: $white; 29 | width: 100%; 30 | height: 1px; 31 | top: 100%; 32 | left: 0; 33 | } 34 | } 35 | .fancy__scroll { 36 | /* width */ 37 | &::-webkit-scrollbar { 38 | width: 6px; 39 | border-radius: 10px; 40 | } 41 | 42 | /* Track */ 43 | &::-webkit-scrollbar-track { 44 | background: $new-color; 45 | border-radius: 10px; 46 | } 47 | 48 | /* Handle */ 49 | &::-webkit-scrollbar-thumb { 50 | background: $white; 51 | border-radius: 10px; 52 | } 53 | 54 | /* Handle on hover */ 55 | &::-webkit-scrollbar-thumb:hover { 56 | background: $azure-blue; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /front-end/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import { Provider } from 'react-redux'; 4 | import { store } from './app/store'; 5 | import App from './App'; 6 | 7 | test('renders learn react link', () => { 8 | const { getByText } = render( 9 | 10 | 11 | 12 | ); 13 | 14 | expect(getByText(/learn/i)).toBeInTheDocument(); 15 | }); 16 | -------------------------------------------------------------------------------- /front-end/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Route, Routes } from 'react-router-dom'; 2 | 3 | import { useEffect, lazy, Suspense } from 'react'; 4 | import { useAppDispatch } from './app/hooks'; 5 | import { checkAuth } from './features/user/userSlice'; 6 | import useAuth from './hooks/use-auth'; 7 | import OauthCallbackRouter from './routes/OauthCallbackRouter'; 8 | import RequireAuth from './routes/RequireAuth'; 9 | import './App.scss'; 10 | 11 | const Login = lazy(() => import('./pages/LoginPage')); 12 | const Register = lazy(() => import('./pages/RegisterPage')); 13 | const Profile = lazy(() => import('./pages/ProfilePage')); 14 | const WithSocket = lazy(() => import('./pages/WithSocketPage')); 15 | const Lobby = lazy(() => import('./pages/LobbyPage')); 16 | const Game = lazy(() => import('./pages/GamePage')); 17 | const Ranking = lazy(() => import('./pages/RankingPage')); 18 | const Error = lazy(() => import('./pages/ErrorPage')); 19 | 20 | function App() { 21 | let { auth } = useAuth(); 22 | const dispatch = useAppDispatch(); 23 | 24 | useEffect(() => { 25 | dispatch(checkAuth()); 26 | }, [dispatch]); 27 | 28 | if (auth.status === 'loading') { 29 | return
loading...
; 30 | } 31 | 32 | return ( 33 | loading 36 | } 37 | > 38 |
39 | 40 | } /> 41 | } /> 42 | } /> 43 | }> 44 | 48 | 49 | 50 | } 51 | /> 52 | 56 | 57 | 58 | } 59 | /> 60 | } /> 61 | } /> 62 | } /> 63 | 64 | 65 |
66 |
67 | ); 68 | } 69 | 70 | export default App; 71 | -------------------------------------------------------------------------------- /front-end/src/app/hooks.ts: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; 2 | import type { RootState, AppDispatch } from './store'; 3 | 4 | // Use throughout your app instead of plain `useDispatch` and `useSelector` 5 | export const useAppDispatch = () => useDispatch(); 6 | export const useAppSelector: TypedUseSelectorHook = useSelector; 7 | -------------------------------------------------------------------------------- /front-end/src/app/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit'; 2 | import counterReducer from '../features/counter/counterSlice'; 3 | import userReducer from '../features/user/userSlice'; 4 | import socketReducer from '../features/socket/socketSlice'; 5 | import friendReducer from '../features/friend/friendSlice'; 6 | 7 | export const store = configureStore({ 8 | reducer: { 9 | counter: counterReducer, 10 | user: userReducer, 11 | socket: socketReducer, 12 | friend: friendReducer, 13 | }, 14 | }); 15 | 16 | export type AppDispatch = typeof store.dispatch; 17 | export type RootState = ReturnType; 18 | export type AppThunk = ThunkAction< 19 | ReturnType, 20 | RootState, 21 | unknown, 22 | Action 23 | >; 24 | -------------------------------------------------------------------------------- /front-end/src/common/assets/checkbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web24-boostris/12074beb6b335111019a4bed22228756de318a84/front-end/src/common/assets/checkbox.png -------------------------------------------------------------------------------- /front-end/src/common/assets/lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web24-boostris/12074beb6b335111019a4bed22228756de318a84/front-end/src/common/assets/lock.png -------------------------------------------------------------------------------- /front-end/src/common/fonts/DungGeunMo.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web24-boostris/12074beb6b335111019a4bed22228756de318a84/front-end/src/common/fonts/DungGeunMo.eot -------------------------------------------------------------------------------- /front-end/src/common/fonts/DungGeunMo.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web24-boostris/12074beb6b335111019a4bed22228756de318a84/front-end/src/common/fonts/DungGeunMo.ttf -------------------------------------------------------------------------------- /front-end/src/common/fonts/DungGeunMo.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web24-boostris/12074beb6b335111019a4bed22228756de318a84/front-end/src/common/fonts/DungGeunMo.woff -------------------------------------------------------------------------------- /front-end/src/common/fonts/DungGeunMo.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web24-boostris/12074beb6b335111019a4bed22228756de318a84/front-end/src/common/fonts/DungGeunMo.woff2 -------------------------------------------------------------------------------- /front-end/src/common/styles/_base.scss: -------------------------------------------------------------------------------- 1 | $dark-navy: #1c2137; 2 | $fluo-blue: #0055fb; 3 | $white: #ffffff; 4 | $azure-blue: #3863b8; 5 | $light-navy: #43508b; 6 | $navy: #2b3150; 7 | $offline: #c4c4c4; 8 | $new-color: #364171; 9 | $transparent-light-navy: #43508bee; 10 | $transparent-azure-blue: #3863b8ee; 11 | $transparent-notification: #3b4883cf; 12 | -------------------------------------------------------------------------------- /front-end/src/components/BasicButton/index.tsx: -------------------------------------------------------------------------------- 1 | import './style.scss'; 2 | import { MouseEventHandler } from 'react'; 3 | 4 | function BasicButton({ 5 | variant = 'contained', 6 | label = '', 7 | handleClick, 8 | }: { 9 | variant: string; 10 | label: string; 11 | handleClick: MouseEventHandler; 12 | }) { 13 | return ( 14 | 17 | ); 18 | } 19 | 20 | export default BasicButton; 21 | -------------------------------------------------------------------------------- /front-end/src/components/BasicButton/style.scss: -------------------------------------------------------------------------------- 1 | @import 'common/styles/base'; 2 | 3 | .btn__root { 4 | padding: 8px 14px; 5 | border-radius: 10px; 6 | 7 | &.btn__root--contained { 8 | background-color: $dark-navy; 9 | color: $white; 10 | } 11 | &.btn__root--outlined { 12 | background-color: $white; 13 | color: $dark-navy; 14 | border: 1px solid $dark-navy; 15 | } 16 | cursor: pointer; 17 | } 18 | -------------------------------------------------------------------------------- /front-end/src/components/BasicInput/index.tsx: -------------------------------------------------------------------------------- 1 | import './style.scss'; 2 | 3 | function BasicInput({ 4 | label = '', 5 | placeholder = '', 6 | type = 'input', 7 | handleChange = () => {}, 8 | inputRef, 9 | }: { 10 | label: string; 11 | placeholder: string; 12 | type: string; 13 | handleChange: Function; 14 | inputRef: any; 15 | }) { 16 | return ( 17 |