├── .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 |
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등수 | 18 |닉네임 | 19 |플레이 타임 | 20 |공격 횟수 | 21 |받은 횟수 | 22 |
---|---|---|---|---|
[{rankIdx++}] | 29 |{r.nickname} | 30 |{r.playTime}s | 31 |{r.attackCnt} | 32 |{r.attackedCnt} | 33 |
SELECT Login Button
57 | {OAUTH_LIST.map(({ type }, idx) => ( 58 |(C) Attendance starts from the first number
78 |BOOSTRIS
78 |