From 43a7ecfaa6f2f56863ee895249d72609f0e3257a Mon Sep 17 00:00:00 2001 From: fiore Date: Mon, 24 Mar 2025 18:28:14 +0900 Subject: [PATCH 1/7] =?UTF-8?q?DO-49=20socket=20io=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 방 생성 로직 개선 방 생성시 급수를 전달 받아서 비슷한 급수 유저와 매칭 시도함 - 항복 기능 추가 항복 메세지 요청시 상대 플레이어에게 메세지 전달 - 무승부 제안 기능 추가 무승부 제안 요청시 상대방에게 트리거 전달 - 무승부 제안 받을 경우 수락 혹은 거절 - 재대결 요청 기능 추가 --- app.js | 4 +- bin/www | 4 +- game.js | 56 -------------- routes/users.js | 1 + socket/handlers/gameEvents.js | 130 +++++++++++++++++++++++++++++++++ socket/handlers/matchmaking.js | 78 ++++++++++++++++++++ socket/handlers/roomEvents.js | 25 +++++++ socket/index.js | 70 ++++++++++++++++++ socket/models/Room.js | 32 ++++++++ utils/logger.js | 40 ++++++++++ 10 files changed, 380 insertions(+), 60 deletions(-) delete mode 100644 game.js create mode 100644 socket/handlers/gameEvents.js create mode 100644 socket/handlers/matchmaking.js create mode 100644 socket/handlers/roomEvents.js create mode 100644 socket/index.js create mode 100644 socket/models/Room.js create mode 100644 utils/logger.js diff --git a/app.js b/app.js index 43d1298..eabffc8 100644 --- a/app.js +++ b/app.js @@ -78,12 +78,12 @@ app.use('/users', usersRouter); app.use('/leaderboard', leaderboardRouter); app.use('/coins', coinsRouter); -// catch 404 and forward to error handler +// catch 404 and forward to error handlers app.use(function(req, res, next) { next(createError(404)); }); -// error handler +// error handlers app.use(function(err, req, res, next) { // set locals, only providing error in development res.locals.message = err.message; diff --git a/bin/www b/bin/www index d64f920..74cce72 100644 --- a/bin/www +++ b/bin/www @@ -7,7 +7,7 @@ var app = require('../app'); var debug = require('debug')('tictactoe-server:server'); var http = require('http'); -var game = require('../game'); +var socketModule = require('../socket'); /** * Get port from environment and store in Express. @@ -25,7 +25,7 @@ var server = http.createServer(app); /** * 게임 서버 실행 */ -game(server); +socketModule(server); /** * Listen on provided port, on all network interfaces. diff --git a/game.js b/game.js deleted file mode 100644 index 96fccf3..0000000 --- a/game.js +++ /dev/null @@ -1,56 +0,0 @@ -const {v4: uuidv4} = require('uuid'); - -module.exports = function(server) { - - const io = require('socket.io')(server, { - transports: ['websocket'] - }); - - // 방 정보 - var rooms = []; - var socketRooms = new Map(); - - io.on('connection', function(socket) { - console.log('Connected: ' + socket.id); - if (rooms.length > 0) { - var roomId = rooms.shift(); - socket.join(roomId) - socket.emit('joinRoom', { roomId: roomId }); - socket.to(roomId).emit('startGame', { roomId: socket.id }); - socketRooms.set(socket.id, roomId); - } else { - var roomId = uuidv4(); - socket.join(roomId); - socket.emit('createRoom', { room: roomId }); - rooms.push(roomId); - socketRooms.set(socket.id, roomId); - } - - socket.on('leaveRoom', function(roomData) { - socket.leave(roomData.roomId); - socket.emit('exitRoom'); - socket.to(roomData.roomId).emit('endGame'); - - // 방 만든 후 혼자 들어갔다가 나갈 때 rooms에서 방 삭제 - var roomId = socketRooms.get(socket.id); - const roomIdx = rooms.indexOf(roomId); - if (roomIdx !== -1) { - rooms.splice(roomIdx, 1); - console.log('Room removed:', roomId); - } - - socketRooms.delete(socket.id); - }); - - socket.on('doPlayer', function(moveData) { - const roomId = moveData.roomId; - const position = moveData.position; - - socket.to(roomId).emit('doOpponent', { position: position }); - }); - - socket.on('disconnect', function(reason) { - console.log('Disconnected: ' + socket.id + ', Reason: ' + reason); - }); - }); -}; \ No newline at end of file diff --git a/routes/users.js b/routes/users.js index 1b9c157..d9cfd64 100644 --- a/routes/users.js +++ b/routes/users.js @@ -210,6 +210,7 @@ router.post("/score-update", async function (req, res, next) { score: Number(userScore), win: Number(winCount), lose: Number(loseCount), + // TODO : 승급, 강등 여부 추가 -1 :강등, 0: 변화 없음 , 1: 승급 }); } catch(err) { diff --git a/socket/handlers/gameEvents.js b/socket/handlers/gameEvents.js new file mode 100644 index 0000000..712f9be --- /dev/null +++ b/socket/handlers/gameEvents.js @@ -0,0 +1,130 @@ +const { logger } = require('../../utils/logger'); + +module.exports = function(io, socket, gameState) { + // 플레이어 움직임 처리 + socket.on('doPlayer', function(moveData) { + try { + const roomId = moveData.roomId; + const position = moveData.position; + + socket.to(roomId).emit('doOpponent', { position: position }); + + logger.debug(`플레이어 ${socket.id}의 움직임, 위치: ${position}, 방: ${roomId}`); + } catch (err) { + logger.error(`플레이어 움직임 처리 중 오류: ${err}`); + socket.emit('error', { message: "움직임 처리 중 오류가 발생했습니다." }); + } + }); + + // 항복 요청 + socket.on('requestSurrender', function(data) { + try { + const roomId = data.roomId; + const message = "상대방이 항복했습니다."; + + socket.to(roomId).emit('doSurrender', { message }); + socket.emit('surrenderConfirmed', { message: "항복 요청이 전송되었습니다." }); + + logger.info(`항복 요청: 플레이어 ${socket.id}, 방 ${roomId}`); + } catch (err) { + logger.error(`항복 처리 중 오류: ${err}`); + socket.emit('error', { message: "항복 처리 중 오류가 발생했습니다." }); + } + }); + + // 무승부 신청 보내기 + socket.on('requestDraw', function(data) { + try { + const roomId = data.roomId; + const message = "상대방이 무승부 요청을 보냈습니다."; + + socket.to(roomId).emit('receiveDrawRequest', { message }); + socket.emit('drawRequestSent', { message: "무승부 요청이 전송되었습니다." }); + + logger.info(`무승부 요청: 플레이어 ${socket.id}, 방 ${roomId}`); + } catch (err) { + logger.error(`무승부 요청 처리 중 오류: ${err}`); + socket.emit('error', { message: "무승부 요청 처리 중 오류가 발생했습니다." }); + } + }); + + // 무승부 수락 + socket.on('acceptDraw', function(data) { + try { + const roomId = data.roomId; + const message = "상대방이 무승부를 수락했습니다."; + + socket.to(roomId).emit('drawAccepted', { message }); + socket.emit('drawConfirmed', { message: "무승부 수락이 완료되었습니다." }); + + logger.info(`무승부 수락: 플레이어 ${socket.id}, 방 ${roomId}`); + } catch (err) { + logger.error(`무승부 수락 처리 중 오류: ${err}`); + socket.emit('error', { message: "무승부 수락 처리 중 오류가 발생했습니다." }); + } + }); + + // 무승부 거절 + socket.on('rejectDraw', function(data) { + try { + const roomId = data.roomId; + const message = "상대방이 무승부를 거절했습니다."; + + socket.to(roomId).emit('drawRejected', { message }); + socket.emit('drawRejectionConfirmed', { message: "무승부 거절이 완료되었습니다." }); + + logger.info(`무승부 거절: 플레이어 ${socket.id}, 방 ${roomId}`); + } catch(err) { + logger.error(`무승부 거절 처리 중 오류: ${err}`); + socket.emit('error', { message: "무승부 거절 처리 중 오류가 발생했습니다." }); + } + }); + + // 재대결 신청 + socket.on('requestRevenge', function(data) { + try { + const roomId = data.roomId; + const message = "상대방이 재대결을 신청했습니다."; + + socket.to(roomId).emit('receiveRevengeRequest', { message }); + socket.emit('revengeRequestSent', { message: "재대결 신청이 전송되었습니다." }); + + logger.info(`재대결 요청: 플레이어 ${socket.id}, 방 ${roomId}`); + } catch (err) { + logger.error(`재대결 신청 처리 중 오류: ${err}`); + socket.emit('error', { message: "재대결 신청 처리 중 오류가 발생했습니다." }); + } + }); + + // 재대결 수락 + socket.on('acceptRevenge', function(data) { + try { + const roomId = data.roomId; + const message = "상대방이 재대결을 수락했습니다."; + + socket.to(roomId).emit('revengeAccepted', { message }); + socket.emit('revengeConfirmed', { message: "재대결 수락이 완료되었습니다."}); + + logger.info(`재대결 수락: 플레이어 ${socket.id}, 방 ${roomId}`); + } catch (err) { + logger.error(`재대결 수락 처리 중 오류: ${err}`); + socket.emit('error', { message: "재대결 수락 처리 중 오류가 발생했습니다." }); + } + }); + + // 재대결 거절 + socket.on('rejectRevenge', function(data) { + try { + const roomId = data.roomId; + const message = "상대방이 재대결을 거절했습니다."; + + socket.to(roomId).emit('revengeRejected', { message }); + socket.emit('revengeRejectionConfirmed', {message: "재대결 거절이 완료되었습니다."}); + + logger.info(`재대결 거절: 플레이어 ${socket.id}, 방 ${roomId}`); + } catch (err) { + logger.error(`재대결 거절 처리 중 오류: ${err}`); + socket.emit('error', { message: "재대결 거절 처리 중 오류가 발생했습니다." }); + } + }); +}; \ No newline at end of file diff --git a/socket/handlers/matchmaking.js b/socket/handlers/matchmaking.js new file mode 100644 index 0000000..8a8fee6 --- /dev/null +++ b/socket/handlers/matchmaking.js @@ -0,0 +1,78 @@ +const { v4: uuidv4 } = require('uuid'); +const { logger } = require('../../utils/logger'); + +module.exports = function(io, socket, gameState) { + socket.on('registerPlayer', function(data) { + try { + const rating = data.rating; + gameState.playerRating.set(socket.id, rating); + + logger.info(`플레이어 등록: ID ${socket.id}, 급수 ${rating}`); + + // 급수에 따른 매칭 진행 + findMatch(socket, rating); + + } catch (err) { + logger.error(`플레이어 등록 중 오류: ${err}`); + socket.emit('error', { message: "플레이어 등록 중 오류가 발생했습니다." }); + } + }); + + const findMatch = (socket, playerRating) => { + let matchedRoom = null; + + // 1. 같은 급수의 방 찾기 + matchedRoom = gameState.rooms.find(room => + Math.abs(gameState.playerRating.get(room.hostId) - playerRating) === 0); + + // 2. 같은 급수 방이 없으면 ±1급 범위로 확장 + if (!matchedRoom) { + matchedRoom = gameState.rooms.find(room => + Math.abs(gameState.playerRating.get(room.hostId) - playerRating) <= 1); + } + + if (matchedRoom) { + const roomId = matchedRoom.roomId; + + // 매칭된 방은 대기 목록에서 제거 + gameState.rooms = gameState.rooms.filter(room => room.roomId !== roomId); + + // 방 입장 + socket.join(roomId); + gameState.socketRooms.set(socket.id, roomId); + + // 클라이언트에게 방 정보 전송 (상대 급수 정보 포함) + socket.emit('joinRoom', { + roomId: roomId, + opponentRating: gameState.playerRating.get(matchedRoom.hostId) + }); + + // 상대방에게 게임 시작 알림 (내 급수 정보 포함) + socket.to(roomId).emit('startGame', { + opponentId: socket.id, + opponentRating: playerRating + }); + + logger.info(`매칭 성공: ${socket.id}(${playerRating}급) - ${matchedRoom.hostId}(${gameState.playerRating.get(matchedRoom.hostId)}급)`); + } else { // 4. 매칭된 방이 없으면 새 방 생성 + + logger.info("매칭된 방 없음!") + const roomId = uuidv4(); + socket.join(roomId); + + gameState.rooms.push({ + roomId: roomId, + hostId: socket.id, + rating: playerRating + }); + + gameState.socketRooms.set(socket.id, roomId); + socket.emit('createRoom', { + roomId: roomId, + message: "대기 중... 비슷한 상대를 찾고 있습니다." + }); + + logger.info(`대기방 생성: ID ${socket.id}, 급수 ${playerRating}, 방 ID ${roomId}`); + } + } +}; diff --git a/socket/handlers/roomEvents.js b/socket/handlers/roomEvents.js new file mode 100644 index 0000000..f822546 --- /dev/null +++ b/socket/handlers/roomEvents.js @@ -0,0 +1,25 @@ +const { logger } = require('../../utils/logger'); + +module.exports = function(io, socket, gameState) { + // 방 나가기 이벤트 + socket.on('leaveRoom', function(roomData) { + try { + const roomId = roomData.roomId; + + socket.leave(roomId); + socket.emit('exitRoom'); + socket.to(roomId).emit('endGame'); + + // 대기방 목록에서 제거 + gameState.rooms = gameState.rooms.filter(room => room.roomId !== roomId); + + // 매핑 정보 삭제 + gameState.socketRooms.delete(socket.id); + + logger.info(`방 나가기 처리: 플레이어 ${socket.id}, 방 ${roomId}`); + } catch (err) { + logger.error(`방 나가기 처리 중 오류: ${err}`); + socket.emit('error', { message: "방 나가기 처리 중 오류가 발생했습니다." }); + } + }); +}; \ No newline at end of file diff --git a/socket/index.js b/socket/index.js new file mode 100644 index 0000000..a11a46d --- /dev/null +++ b/socket/index.js @@ -0,0 +1,70 @@ +const {v4: uuidv4} = require('uuid'); +const matchmakingHandlers = require('./handlers/matchmaking'); +const roomEventHandlers = require('./handlers/roomEvents'); +const gameEventHandlers = require('./handlers/gameEvents'); +const { logger } = require('../utils/logger'); + +// 전역 상태 객체 (모든 핸들러에서 공유) +const gameState = { + rooms: [], // {roomId, hostId, rating} 형태로 저장 + socketRooms: new Map(), // 소켓ID와 방ID 매핑 + playerRating: new Map() // 소켓ID와 플레이어 급수 매핑 +}; + +module.exports = function(server) { + const io = require('socket.io')(server, { + transports: ['websocket'] + }); + + io.on('connection', function(socket) { + console.log('Connected: ' + socket.id); + + // 기존 단순 매칭 코드 + // if (rooms.length > 0) { + // var roomId = rooms.shift(); + // socket.join(roomId) + // socket.emit('joinRoom', { roomId: roomId }); + // socket.to(roomId).emit('startGame', { roomId: socket.id }); + // socketRooms.set(socket.id, roomId); + // } else { + // var roomId = uuidv4(); + // socket.join(roomId); + // socket.emit('createRoom', { room: roomId }); + // rooms.push(roomId); + // socketRooms.set(socket.id, roomId); + // } + + // 매칭 관련 이벤트 핸들러 등록 + matchmakingHandlers(io, socket, gameState) + + // 방 관련 이벤트 핸들러 등록 + roomEventHandlers(io, socket, gameState); + + // 게임 내 이벤트 핸들러 등록 + gameEventHandlers(io, socket, gameState); + + + // 연결 해제 시 정리 + socket.on('disconnect', function(reason) { + logger.info('Disconnected: ' + socket.id + ', Reason: ' + reason); + + // 해당 소켓이 속한 방 찾기 + const roomId = gameState.socketRooms.get(socket.id); + + if (roomId) { + // 상대방에게 연결 끊김 알림 + socket.to(roomId).emit('opponentDisconnected', { + message: "상대방의 연결이 끊어졌습니다." + }); + + // 대기방 목록에서 제거 + gameState.rooms = gameState.rooms.filter(room => room.roomId !== roomId); + + gameState.socketRooms.delete(socket.id); + } + + // 플레이어 급수 정보 제거 + gameState.playerRating.delete(socket.id); + }); + }); +}; \ No newline at end of file diff --git a/socket/models/Room.js b/socket/models/Room.js new file mode 100644 index 0000000..6fb34cf --- /dev/null +++ b/socket/models/Room.js @@ -0,0 +1,32 @@ +/** + * 게임 방 클래스 + */ +class Room { + /** + * 새로운 방 객체 생성 + * @param {string} roomId - 방 고유 ID + * @param {string} hostId - 방장 소켓 ID + * @param {number} rating - 방장의 급수 (1-18) + */ + constructor(roomId, hostId, rating) { + this.roomId = roomId; + this.hostId = hostId; + this.rating = rating; + this.createdAt = new Date(); + } + + /** + * 해당 방의 정보를 객체로 반환 + * @returns {Object} 방 정보 객체 + */ + toJSON() { + return { + roomId: this.roomId, + hostId: this.hostId, + rating: this.rating, + createdAt: this.createdAt + }; + } +} + +module.exports = Room; \ No newline at end of file diff --git a/utils/logger.js b/utils/logger.js new file mode 100644 index 0000000..3cf893b --- /dev/null +++ b/utils/logger.js @@ -0,0 +1,40 @@ +/** + * 간단한 로거 유틸리티 + */ +const logger = { + /** + * 디버그 레벨 로그 + * @param {string} message - 로그 메시지 + */ + debug: (message) => { + if (process.env.LOG_LEVEL === 'debug') { + console.log(`[DEBUG] ${new Date().toISOString()}: ${message}`); + } + }, + + /** + * 정보 레벨 로그 + * @param {string} message - 로그 메시지 + */ + info: (message) => { + console.log(`[INFO] ${new Date().toISOString()}: ${message}`); + }, + + /** + * 경고 레벨 로그 + * @param {string} message - 로그 메시지 + */ + warn: (message) => { + console.warn(`[WARN] ${new Date().toISOString()}: ${message}`); + }, + + /** + * 에러 레벨 로그 + * @param {string} message - 로그 메시지 + */ + error: (message) => { + console.error(`[ERROR] ${new Date().toISOString()}: ${message}`); + } +}; + +module.exports = { logger }; \ No newline at end of file From 072876bbf0becda9061e4276f5e909da8b467f6d Mon Sep 17 00:00:00 2001 From: fiore Date: Tue, 25 Mar 2025 09:19:30 +0900 Subject: [PATCH 2/7] =?UTF-8?q?DO-49=20=EB=A7=A4=EC=B9=98=20=EB=A9=94?= =?UTF-8?q?=EC=9D=B4=ED=82=B9=20=EC=8B=9C=20=EB=8B=89=EB=84=A4=EC=9E=84=20?= =?UTF-8?q?=EA=B5=90=ED=99=98=ED=86=A0=EB=A1=9D=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- socket/models/Room.js | 32 -------------------------------- 1 file changed, 32 deletions(-) delete mode 100644 socket/models/Room.js diff --git a/socket/models/Room.js b/socket/models/Room.js deleted file mode 100644 index 6fb34cf..0000000 --- a/socket/models/Room.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * 게임 방 클래스 - */ -class Room { - /** - * 새로운 방 객체 생성 - * @param {string} roomId - 방 고유 ID - * @param {string} hostId - 방장 소켓 ID - * @param {number} rating - 방장의 급수 (1-18) - */ - constructor(roomId, hostId, rating) { - this.roomId = roomId; - this.hostId = hostId; - this.rating = rating; - this.createdAt = new Date(); - } - - /** - * 해당 방의 정보를 객체로 반환 - * @returns {Object} 방 정보 객체 - */ - toJSON() { - return { - roomId: this.roomId, - hostId: this.hostId, - rating: this.rating, - createdAt: this.createdAt - }; - } -} - -module.exports = Room; \ No newline at end of file From 886cb4f08c8143c54a80779d0848eed9cae61912 Mon Sep 17 00:00:00 2001 From: fiore Date: Tue, 25 Mar 2025 09:22:54 +0900 Subject: [PATCH 3/7] =?UTF-8?q?DO-49=20=EC=9D=B4=EC=A0=84=20=EC=BB=A4?= =?UTF-8?q?=EB=B0=8B=EC=97=90=EC=84=9C=20=EB=88=84=EB=9D=BD=EB=90=9C=20?= =?UTF-8?q?=ED=95=AD=EB=AA=A9=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 4 ++-- socket/handlers/matchmaking.js | 21 +++++++++++++-------- socket/handlers/roomEvents.js | 4 ++-- socket/index.js | 18 ++---------------- 4 files changed, 19 insertions(+), 28 deletions(-) diff --git a/app.js b/app.js index eabffc8..75555bb 100644 --- a/app.js +++ b/app.js @@ -49,9 +49,9 @@ async function connectDB(){ // 연결 종료 처리 process.on("SIGINT", async ()=> { await client.close(); - console.log("Database Connected"); + console.log("Database Disconnected"); process.exit(0); - }) + }); } catch (err){ console.error("DB 연결 실패: " + err); process.exit(1); diff --git a/socket/handlers/matchmaking.js b/socket/handlers/matchmaking.js index 8a8fee6..a1be977 100644 --- a/socket/handlers/matchmaking.js +++ b/socket/handlers/matchmaking.js @@ -5,12 +5,14 @@ module.exports = function(io, socket, gameState) { socket.on('registerPlayer', function(data) { try { const rating = data.rating; + const nickname = data.nickname; gameState.playerRating.set(socket.id, rating); + gameState.nickname.set(socket.id, nickname); - logger.info(`플레이어 등록: ID ${socket.id}, 급수 ${rating}`); + logger.info(`플레이어 등록: ID ${socket.id}, 닉네임: ${nickname}, 급수 ${rating}`); // 급수에 따른 매칭 진행 - findMatch(socket, rating); + findMatch(socket, rating, nickname); } catch (err) { logger.error(`플레이어 등록 중 오류: ${err}`); @@ -18,7 +20,7 @@ module.exports = function(io, socket, gameState) { } }); - const findMatch = (socket, playerRating) => { + const findMatch = (socket, playerRating, nickname) => { let matchedRoom = null; // 1. 같은 급수의 방 찾기 @@ -44,19 +46,22 @@ module.exports = function(io, socket, gameState) { // 클라이언트에게 방 정보 전송 (상대 급수 정보 포함) socket.emit('joinRoom', { roomId: roomId, - opponentRating: gameState.playerRating.get(matchedRoom.hostId) + opponentRating: gameState.playerRating.get(matchedRoom.hostId), + opponentNickname: gameState.nickname.get(matchedRoom.hostId), }); - // 상대방에게 게임 시작 알림 (내 급수 정보 포함) + // 상대방에게 게임 시작 알림 (내 닉네임과 급수 정보 포함) socket.to(roomId).emit('startGame', { opponentId: socket.id, - opponentRating: playerRating + opponentRating: playerRating, + opponentNickname: nickname, }); logger.info(`매칭 성공: ${socket.id}(${playerRating}급) - ${matchedRoom.hostId}(${gameState.playerRating.get(matchedRoom.hostId)}급)`); - } else { // 4. 매칭된 방이 없으면 새 방 생성 + } + // 4. 매칭된 방이 없으면 새 방 생성 + else { - logger.info("매칭된 방 없음!") const roomId = uuidv4(); socket.join(roomId); diff --git a/socket/handlers/roomEvents.js b/socket/handlers/roomEvents.js index f822546..aeaf7ca 100644 --- a/socket/handlers/roomEvents.js +++ b/socket/handlers/roomEvents.js @@ -7,8 +7,8 @@ module.exports = function(io, socket, gameState) { const roomId = roomData.roomId; socket.leave(roomId); - socket.emit('exitRoom'); - socket.to(roomId).emit('endGame'); + socket.emit('exitRoom',{message: "방을 떠났습니다."}); + socket.to(roomId).emit('endGame', { message: "상대방이 방을 떠났습니다." }); // 대기방 목록에서 제거 gameState.rooms = gameState.rooms.filter(room => room.roomId !== roomId); diff --git a/socket/index.js b/socket/index.js index a11a46d..01c1acf 100644 --- a/socket/index.js +++ b/socket/index.js @@ -8,7 +8,8 @@ const { logger } = require('../utils/logger'); const gameState = { rooms: [], // {roomId, hostId, rating} 형태로 저장 socketRooms: new Map(), // 소켓ID와 방ID 매핑 - playerRating: new Map() // 소켓ID와 플레이어 급수 매핑 + playerRating: new Map(), // 소켓ID와 플레이어 급수 매핑 + nickname: new Map() // 플레이어 닉네임 }; module.exports = function(server) { @@ -19,21 +20,6 @@ module.exports = function(server) { io.on('connection', function(socket) { console.log('Connected: ' + socket.id); - // 기존 단순 매칭 코드 - // if (rooms.length > 0) { - // var roomId = rooms.shift(); - // socket.join(roomId) - // socket.emit('joinRoom', { roomId: roomId }); - // socket.to(roomId).emit('startGame', { roomId: socket.id }); - // socketRooms.set(socket.id, roomId); - // } else { - // var roomId = uuidv4(); - // socket.join(roomId); - // socket.emit('createRoom', { room: roomId }); - // rooms.push(roomId); - // socketRooms.set(socket.id, roomId); - // } - // 매칭 관련 이벤트 핸들러 등록 matchmakingHandlers(io, socket, gameState) From 576b4c96cee72ed771439d8e61a0fdf70adde836 Mon Sep 17 00:00:00 2001 From: fiore Date: Tue, 25 Mar 2025 09:35:20 +0900 Subject: [PATCH 4/7] =?UTF-8?q?DO-49=20=EB=A7=A4=EC=B9=AD=20=EC=84=B1?= =?UTF-8?q?=EC=82=AC=EC=8B=9C=20=EC=84=A0=EA=B3=B5=20=EC=97=AC=EB=B6=80=20?= =?UTF-8?q?=EA=B2=B0=EC=A0=95=ED=9B=84=20=EC=A0=84=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- socket/handlers/matchmaking.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/socket/handlers/matchmaking.js b/socket/handlers/matchmaking.js index a1be977..f12a53e 100644 --- a/socket/handlers/matchmaking.js +++ b/socket/handlers/matchmaking.js @@ -43,11 +43,15 @@ module.exports = function(io, socket, gameState) { socket.join(roomId); gameState.socketRooms.set(socket.id, roomId); + // 흑백 여부 결정 50% 확률 + let isHostFirst = Math.random() < 0.5; + // 클라이언트에게 방 정보 전송 (상대 급수 정보 포함) socket.emit('joinRoom', { roomId: roomId, opponentRating: gameState.playerRating.get(matchedRoom.hostId), opponentNickname: gameState.nickname.get(matchedRoom.hostId), + isBlack: !isHostFirst }); // 상대방에게 게임 시작 알림 (내 닉네임과 급수 정보 포함) @@ -55,6 +59,7 @@ module.exports = function(io, socket, gameState) { opponentId: socket.id, opponentRating: playerRating, opponentNickname: nickname, + isBlack: isHostFirst }); logger.info(`매칭 성공: ${socket.id}(${playerRating}급) - ${matchedRoom.hostId}(${gameState.playerRating.get(matchedRoom.hostId)}급)`); From 3b19abd84d12ae26740171369080ce0afd76fbe5 Mon Sep 17 00:00:00 2001 From: fiore Date: Tue, 25 Mar 2025 09:50:54 +0900 Subject: [PATCH 5/7] =?UTF-8?q?DO-49=20=EC=9E=AC=EB=8C=80=EA=B2=B0=20?= =?UTF-8?q?=EC=84=B1=EC=82=AC=EC=8B=9C=20=ED=9D=91=EB=B0=B1=20=EB=92=A4?= =?UTF-8?q?=EC=A7=91=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 수락자 본인의 흑 여부를 전달 받아서 뒤집어서 전달 --- socket/handlers/gameEvents.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/socket/handlers/gameEvents.js b/socket/handlers/gameEvents.js index 712f9be..30d2760 100644 --- a/socket/handlers/gameEvents.js +++ b/socket/handlers/gameEvents.js @@ -100,10 +100,18 @@ module.exports = function(io, socket, gameState) { socket.on('acceptRevenge', function(data) { try { const roomId = data.roomId; + let isBlack = data.isBlack; const message = "상대방이 재대결을 수락했습니다."; - socket.to(roomId).emit('revengeAccepted', { message }); - socket.emit('revengeConfirmed', { message: "재대결 수락이 완료되었습니다."}); + + socket.to(roomId).emit('revengeAccepted', { + message, + isBlack: isBlack, + }); + socket.emit('revengeConfirmed', { + message: "재대결 수락이 완료되었습니다.", + isBlack: !isBlack, + }); logger.info(`재대결 수락: 플레이어 ${socket.id}, 방 ${roomId}`); } catch (err) { From 08ca4cc436aef168f13288a1b7a1955a9bf12035 Mon Sep 17 00:00:00 2001 From: fiore Date: Tue, 25 Mar 2025 09:57:03 +0900 Subject: [PATCH 6/7] =?UTF-8?q?DO-49=20[feat]=20=EC=8A=B9=EA=B8=89?= =?UTF-8?q?=EC=97=AC=EB=B6=80=20=EB=B0=98=ED=99=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/users.js | 7 ++++++- socket/handlers/gameEvents.js | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/routes/users.js b/routes/users.js index d9cfd64..e1741f4 100644 --- a/routes/users.js +++ b/routes/users.js @@ -165,11 +165,14 @@ router.post("/score-update", async function (req, res, next) { requiredPoints = 10; // 1~4급은 10점 필요 } + let advancement = 0; + // 승급 확인 if (userScore >= requiredPoints) { if (userRating > 1) { // 1급보다 높은 급수인 경우만 승급 가능 userRating -= 1; // 급수 상승 (숫자는 작을수록 높은 급수) userScore = 0; // 승급 후 포인트 초기화 + advancement++; } else { // 1급인 경우 더 이상 승급 불가능, 최대 포인트로 유지 userScore = requiredPoints; @@ -181,6 +184,7 @@ router.post("/score-update", async function (req, res, next) { if (userRating < 18) { // 18급보다 낮은 급수인 경우만 강등 가능 userRating += 1; // 급수 하락 (숫자가 커짐) userScore = 0; // 강등 후 포인트 초기화 + advancement-- } else { // 18급인 경우 더 이상 강등 불가능, 최소 포인트로 유지 userScore = -requiredPoints + 1; @@ -210,7 +214,8 @@ router.post("/score-update", async function (req, res, next) { score: Number(userScore), win: Number(winCount), lose: Number(loseCount), - // TODO : 승급, 강등 여부 추가 -1 :강등, 0: 변화 없음 , 1: 승급 + // 승급, 강등 여부 추가 -1 :강등, 0: 변화 없음 , 1: 승급 + advancement: Number(advancement), }); } catch(err) { diff --git a/socket/handlers/gameEvents.js b/socket/handlers/gameEvents.js index 30d2760..a641ea8 100644 --- a/socket/handlers/gameEvents.js +++ b/socket/handlers/gameEvents.js @@ -103,7 +103,7 @@ module.exports = function(io, socket, gameState) { let isBlack = data.isBlack; const message = "상대방이 재대결을 수락했습니다."; - + socket.to(roomId).emit('revengeAccepted', { message, isBlack: isBlack, From e5169086f6be90e9cc611b796fd7b6177022dfd3 Mon Sep 17 00:00:00 2001 From: fiore Date: Tue, 25 Mar 2025 10:27:23 +0900 Subject: [PATCH 7/7] =?UTF-8?q?DO-49=20=EB=B3=80=EC=88=98=EB=AA=85/?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EC=9D=BC=EB=B6=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/users.js | 2 +- socket/handlers/roomEvents.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/routes/users.js b/routes/users.js index e1741f4..0679aea 100644 --- a/routes/users.js +++ b/routes/users.js @@ -215,7 +215,7 @@ router.post("/score-update", async function (req, res, next) { win: Number(winCount), lose: Number(loseCount), // 승급, 강등 여부 추가 -1 :강등, 0: 변화 없음 , 1: 승급 - advancement: Number(advancement), + isAdvancement: Number(advancement), }); } catch(err) { diff --git a/socket/handlers/roomEvents.js b/socket/handlers/roomEvents.js index aeaf7ca..b1a8d52 100644 --- a/socket/handlers/roomEvents.js +++ b/socket/handlers/roomEvents.js @@ -10,9 +10,8 @@ module.exports = function(io, socket, gameState) { socket.emit('exitRoom',{message: "방을 떠났습니다."}); socket.to(roomId).emit('endGame', { message: "상대방이 방을 떠났습니다." }); - // 대기방 목록에서 제거 + // 혼자 대기 중인 경우 대기방 목록에서 제거 gameState.rooms = gameState.rooms.filter(room => room.roomId !== roomId); - // 매핑 정보 삭제 gameState.socketRooms.delete(socket.id);