본문 바로가기
Development/C#

멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 68일차

by Mobics 2025. 3. 6.

 

목차


    Tic Tac Toe 서버 만들기

    25.03.06

    지난 시간에 놓친 활동하기

    : Tic Tac Toe 자동 로그인 만들기

    • 점수 불러오기 기능과 로그인 기능을 이용해 자동 로그인 기능을 만들어 보세요.

    --> 한번 로그인을 하면 자동으로 로그인이 되지만, 아직 안한 상태라면 로그인 창이 뜨도록

     


    25.03.12

    >> GameManager.cs 의 Start()에서 NetworkManager의 GetScore()를 호출하여 구현

    : 자동 로그인에 실패했을 때 OpenSigninPanel()을 호출하여 다시 로그인 할 수 있도록 구현

    --> 아래의 최종 코드도 수정해놓음

     

    >> 'NavigationPanel'의 Button들의 Source Image 수정

    : 기존의 Source Image가 깨져있어서 'Dark Theme UI' Asset에서 다운 받은 Sprite로 수정

    --> 크기는 그대로

     

    >> 'MainPanel'의 'Buttons'의 Height 수정

    : 버튼이 4개인데 Height가 맞지 않아서 크기에 맞게 수정

    --> 버튼의 Height 100 x 4개 + Spacing 20 x 3 = 460


    활동

    : TicTacToe 스코어 등록 및 리더보드 만들기

    • 싱글 플레이에서 게임에 승리하면 유저에게 10점 부여하세요 --> 현재 싱글 플레이에는 Minimax 알고리즘이 적용되어 절대 승리할 수 없기 때문에 2인 플레이에서 승리하면 유저에게 10점 부여하는 식으로 확인 가능
    • 전체 유저를 대상으로 랭킹을 보여주는 리더보드를 만드세요
    • 리더보드는 Scroll View를 이용해 만드세요
    • Scroll View에는 유저 닉네임 + 점수를 표시하세요
    • 서버에서 유저 랭킹을 보여주는 기능을 만드세요
    • 리더보드를 실행했을 때 자신의 점수를 바로 볼 수 있게 만들어주세요

    ※ Hint : 처음 로그인 했을 때 Cookie가 전달되고 Session 파일이 존재하면 Cookie가 전달되지 않을 것이기 때문에 로그인 했을 때 받은 Cookie 값을 저장해놓고 점수 저장과 불러오기를 구현하면 된다.

    ※ UnityWebRequest에서도 Cookie가 사용된다. 정확히는 게임 세션 동안 Cookie를 캐싱한다.

    --> UnityWebRequest.ClearCookieCache(); 를 사용하면 캐싱된 쿠키를 제거할 수 있다.

     

    └ 리더보드 만들기

    >> 코드 작성

    • NetworkManager.cs
    • SigninPanelController.cs
    • app.js
    • leaderboard.js

    25.03.13

    >> LeaderboardPanel 만들기

    1. Prefab에 있는 'ConfirmPanel'을 복붙하여 LeaderboardPanel로 이름 변경

     

    2. 'MessageText'와 'OKButton'을 삭제

     

    3. 'Panel'의 자식으로 Scroll View 추가

     

    4. Scroll View의 Anchor 조정 및 Scroll Rect의 Horizontal 체크 해제

    : CloseButton과 안 겹치도록 Top 조정

    --> Anchor는 Alt + Shift

     

    5. 'Content'의 자식으로 'ScoreCell' 생성

    : UI-Panel로 만들어서 Source Image 넣고 Color도 Alpha 값을 255로 조정

    --> Anchor, Left, Right, PosY, Height도 조정했지만 이후에 Content의 Vertical Layout Group에서 전부 조정해줄 예정

    --> PosY를 0으로 조정

     

    6. 'ScoreCell'의 자식으로 'NicknameText'와 'ScoreText' 생성

    : UI-Text로 만듦

    • NicknameText : Anchor, PosX, Width, Font, Color, Alignment 조정 (Text Input은 상관없음)
    • ScoreText : Anchor, PosX, Font, Font Style, Color, Alignment 조정 (Text Input은 상관없음)

    --> 둘다 Anchor는 Alt + Shift

     

    7. 'ScoreCellController.cs' 생성

    : 코드 작성

     

    8. 'ScoreCell'에 'ScoreCellController.cs' 추가 및 바인딩

     

    9. ScoreCell을 Prefab화

    : 이후 Hierarchy에서 삭제

     

    10. 'Content'에 'Content Size Fitter', 'Vertical Layout Group' 추가

    : Test를 위해 ScoreCell Prefab을 'Content'의 자식으로 2개 추가

    --> Content Size Fitter의 Vertical Fit을 Preferred Size로 설정

    --> Vertical Layout Group 설정

    • Control Child Size의 Width를 체크
    • Padding의 Left를 30, Right를 30, Bottom을 50으로 설정
    • Child Alignment를 Middle Center로 설정
    • Spacing을 20으로 설정

    --> 테스트 후 ScoreCell 삭제

    --> Scroll View의 Color도 테스트용이었기 때문에 (255, 255, 255, 100)으로 원상복귀

     

    11. 'Scrollbar Vertical'의 PosX, Source Image, Color 수정

    : Scrollbar_bg를 Slice, Color는 (216, 216, 216, 255)

     

    12. 'Handle'의 Source Image, Color 수정

    : Color는 (140, 140, 140, 255)

     

    13. 'LeaderboardPanelController.cs' 생성

    : 코드 작성

     

    14. 'LeaderboardPanel'에 'LeaderboardPanelController.cs' 추가 및 바인딩

     

    15. 'CloseButton'의 OnClick()에 함수 바인딩

     

    16. 'LeaderboardPanel'을 Prefab화 후, Hierarchy에서 삭제

     

    17. MainPanel의 Buttons의 자식으로 'SettingsButton'을 복붙하여 'LeaderboardButton' 생성

    : Buttons의 Height를 580으로 조정하고 Text를 '리더보드'로 수정

     

    18. 'GameManager.cs', 'MainPanelController.cs'에 코드 작성

    : 'LeaderboardButton'의 OnClick()에 함수 바인딩, GameManager.cs에 'LeaderboardPanel' Prefab 바인딩

     

    ※ NullReferenceException 에러가 발생

    : GameManager.cs의 OpenLeaderboardPanel()에서 GetLeaderboard()할 때, ranks.scores가 null이 나오는 게 문제

    --> Postman으로 테스트했을 때는 문제 없이 Score가 출력됨


    25.03.14

    해결 방법

    : ChatGPT를 통해 해결했는데, 결과적으로 GetLeaderboard()의 'JsonUtility.FromJson<Scores>(result);'가 문제였다. 즉, JsonUtility의 문제였고 이를 대신하여 Newtonsoft.Json을 사용하니 해결되었다. (NetworkManager.cs에서 코드 수정)

    --> Newtonsoft.Json 라이브러리는 JSON 키와 C# 필드명이 다르거나, 구조가 복잡할 때 더 유연하게 동작한다.

     

    >> 리더보드 창의 부자연스러운 부분 수정

    : 옆에 튀어나온 부분이나 Scrollbar가 아래로 더 튀어나온 것 수정하고 Panel의 크기를 늘림

    --> Scroll View의 Bottom, Source Image와 Color의 Alpha값 수정 (Alpha 값은 0)

    --> (왼)수정 전, (오)수정 후

     

    └ 게임 승리 시, 점수 획득 구현

    : 싱글 플레이로 AI와 대결하면 절대 승리할 수 없고 무승부가 최대기 때문에, 2인 플레이에서 승리하면 점수를 10점 얻도록 구현

     

    >> NetworkManager.cs에 AddScore() 코드 작성

    : JsonUtility.ToJson()은 int 값을 JSON으로 변환할 수 없으며, 객체만 변환 가능하다. --> 따라서 따로 구조체를 만들어야 한다.

     

    >> GameLogic.cs의 ProcessMove()에서 GameResult가 Win일 때 AddScore()를 호출

    : ProcessMove()를 가지고 있는 BasePlayerState가 MonoBehaviour를 상속받지 않기 때문에 StartCoroutine을 호출하면 에러가 발생

    --> CoroutineHelper.cs를 Singleton 패턴으로 생성하여 StartCoroutine을 대신 실행해주는 함수를 구현

     

    >> users.js 에서 addscore 관련 코드 수정

    : ChatGPT를 통해 분석해보니, 서버의 /addscore API가 score를 덮어쓰기(set) 방식으로 저장하고 있었다. 따라서 기존 점수에서 10점을 추가하는 방식으로 구현하려면 $inc 연산자를 사용하여야 한다.

     

    ※ 70일차 글을 참고하여 멀티플레이를 위해 클라우드 서버에 연결해야한다!


    채팅 서버 만들기

    >> www 파일에 코드 추가

    var game = require('../game');  // 추가
    
    
    /**
     * 게임 서버 실행
     */
    game(server); // 추가

     

    >> game.js 생성 및 코드 작성

     

    >> 'Main Panel'의 자식으로 'Chatting Panel' 생성

    : Color는 임의로 정한 색

    --> 이름에 t 하나 빼먹음..

     

    >> Chating Panel의 자식으로 Scroll View 생성

    --> Anchor는 Alt + Shift

     

    >> Content

     

    >> Content의 자식으로 Text 추가

     

    >> Chating Panel의 자식으로 Input Field 생성

    --> Anchor는 Alt + Shift

     

    >> Placeholder와 Text의 Alignment를 Middle로 설정

     

    최종 코드

    >> NetworkManager.cs

    using System;
    using System.Collections;
    using UnityEngine;
    using UnityEngine.SceneManagement;
    using UnityEngine.Networking;
    
    public class NetworkManager : Singleton<NetworkManager>
    {
        protected override void OnSceneLoaded(Scene scene, LoadSceneMode mode)
        {
            
        }
        
        public IEnumerator Signup(SignupData signupData, Action success, Action failure)
        {
            string jsonString = JsonUtility.ToJson(signupData);
            byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(jsonString);
    
            // () 안의 객체가 {}를 벗어나면 Disposing되는 문법
            using (UnityWebRequest www =
                   new UnityWebRequest(Constants.ServerURL + "/users/signup", UnityWebRequest.kHttpVerbPOST))
            {
                www.uploadHandler = new UploadHandlerRaw(bodyRaw);
                www.downloadHandler = new DownloadHandlerBuffer();
                www.SetRequestHeader("Content-Type", "application/json");
                
                yield return www.SendWebRequest();
    
                if (www.result == UnityWebRequest.Result.ConnectionError || 
                    www.result == UnityWebRequest.Result.ProtocolError)
                {
                    Debug.Log("Error : " + www.error);
    
                    if (www.responseCode == 409) // 중복 사용자
                    {
                        // TODO: 중복 사용자 생성 팝업 표시
                        Debug.Log("중복 사용자");
                        GameManager.Instance.OpenConfirmPanel("이미 존재하는 사용자입니다.", () =>
                        {
                            failure?.Invoke();
                        });
                    }
                }
                else
                {
                    var result = www.downloadHandler.text;
                    Debug.Log("Result : " + result);
                    
                    // 회원가입 성공 팝업 표시
                    GameManager.Instance.OpenConfirmPanel("회원가입이 완료 되었습니다.", () =>
                    {
                        success?.Invoke();
                    });
                }
            }
        }
        
        public IEnumerator Signin(SigninData signinData, Action success, Action<int> failure)
        {
            string jsonString = JsonUtility.ToJson(signinData);
            byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(jsonString);
    
            using (UnityWebRequest www =
                   new UnityWebRequest(Constants.ServerURL + "/users/signin", UnityWebRequest.kHttpVerbPOST))
            {
                www.uploadHandler = new UploadHandlerRaw(bodyRaw);
                www.downloadHandler = new DownloadHandlerBuffer();
                www.SetRequestHeader("Content-Type", "application/json");
                
                yield return www.SendWebRequest();
    
                if (www.result == UnityWebRequest.Result.ConnectionError ||
                    www.result == UnityWebRequest.Result.ProtocolError)
                {
                    
                }
                else
                {
                    // 점수 불러오기 기능을 구현하기 위해 cookie 값 저장 (수강생님 코드 참고)
                    var cookie = www.GetResponseHeader("set-cookie");
                    if (!string.IsNullOrEmpty(cookie))
                    {
                        //int lastIndex = cookie.LastIndexOf(";");
                        //string sid = cookie.Substring(0, lastIndex);
                        //PlayerPrefs.SetString("sid", sid);
                        
                        int lastIndex = cookie.IndexOf('='); // 첫 번째 '='의 위치 찾기
                        if (lastIndex != -1 && lastIndex + 1 < cookie.Length) // '='이 존재하고, 뒤에 값이 있을 경우
                        {
                            string sid = cookie.Substring(lastIndex + 1).Split(';')[0]; // '=' 다음부터 ';' 이전까지 추출
                            Debug.Log("Session ID : " + sid);
                            PlayerPrefs.SetString("sid", sid);
                        }
                        else
                        {
                            // 올바른 쿠키 형식이 아님
                        }
                    }
                    else
                    {
                        Debug.Log("쿠키가 비어있습니다.");
                    }
                    
                    var resultString = www.downloadHandler.text;
                    var result = JsonUtility.FromJson<SigninResult>(resultString);
    
                    if (result.result == 0)
                    {
                        // username이 유효하지 않음
                        GameManager.Instance.OpenConfirmPanel("Username이 유효하지 않습니다.", () =>
                        {
                            failure?.Invoke(0);
                        });
                    }
                    else if (result.result == 1)
                    {
                        // password가 유효하지 않음
                        GameManager.Instance.OpenConfirmPanel("Password가 유효하지 않습니다.", () =>
                        {
                            failure?.Invoke(1);
                        });
                    }
                    else if (result.result == 2)
                    {
                        // 성공
                        GameManager.Instance.OpenConfirmPanel("로그인에 성공하였습니다.", () =>
                        {
                            success?.Invoke();
                        });
                    }
                }
            }
        }
    
        public IEnumerator GetScore(Action<ScoreResult> success, Action failure)
        {
            using (UnityWebRequest www = 
                   new UnityWebRequest(Constants.ServerURL + "/users/score", UnityWebRequest.kHttpVerbGET))
            {
                www.downloadHandler = new DownloadHandlerBuffer();
                
                string sid = PlayerPrefs.GetString("sid", "");
                if (!string.IsNullOrEmpty(sid))
                {
                    www.SetRequestHeader("Cookie", sid);
                }
                
                yield return www.SendWebRequest();
    
                if (www.result == UnityWebRequest.Result.ConnectionError || 
                    www.result == UnityWebRequest.Result.ProtocolError)
                {
                    if (www.responseCode == 403)
                    {
                        Debug.Log("로그인이 필요합니다.");
                    }
                    
                    failure?.Invoke();
                }
                else
                {
                    var result = www.downloadHandler.text;
                    var userScore = JsonUtility.FromJson<ScoreResult>(result);
                    
                    Debug.Log(userScore.score);
                    
                    success?.Invoke(userScore);
                }
            }
        }
    
        public IEnumerator GetLeaderboard(Action<Scores> success, Action failure)
        {
            using (UnityWebRequest www = 
                   new UnityWebRequest(Constants.ServerURL + "/leaderboard", UnityWebRequest.kHttpVerbGET))
            {
                www.downloadHandler = new DownloadHandlerBuffer();
                
                string sid = PlayerPrefs.GetString("sid", "");
                if (!string.IsNullOrEmpty(sid))
                {
                    www.SetRequestHeader("Cookie", sid);
                }
                
                yield return www.SendWebRequest();
    
                if (www.result == UnityWebRequest.Result.ConnectionError || 
                    www.result == UnityWebRequest.Result.ProtocolError)
                {
                    if (www.responseCode == 403)
                    {
                        Debug.Log("로그인이 필요합니다.");
                    }
                    
                    failure?.Invoke();
                }
                else
                {
                    var result = www.downloadHandler.text;
                    var scores = JsonUtility.FromJson<Scores>(result);
                    
                    success?.Invoke(scores);
                }
            }
        }
    }

     

    >> SigninPanelController.cs

    using TMPro;
    using UnityEngine;
    
    public struct SigninData
    {
        public string username;
        public string password;
    }
    
    public struct SigninResult
    {
        public int result;
    }
    
    public struct ScoreResult
    {
        public string id;
        public string username;
        public string nickname;
        public int score;
    
    }
    
    public struct ScoreInfo
    {
        public string username;
        public string nickname;
        public int score;
    }
    
    public struct Scores
    {
        public ScoreInfo[] scores;
    }
    
    public class SigninPanelController : MonoBehaviour
    {
        [SerializeField] private TMP_InputField usernameInputField;
        [SerializeField] private TMP_InputField passwordInputField;
    
        public void OnClickSigninButton()
        {
            string username = usernameInputField.text;
            string password = passwordInputField.text;
    
            if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
            {
                // TODO: 누락된 값 입력 요청 팝업 표시
                return;
            }
    
            var signinData = new SigninData();
            signinData.username = username;
            signinData.password = password;
    
            StartCoroutine(NetworkManager.Instance.Signin(signinData, () =>
            {
                Destroy(gameObject);
            }, result =>
            {
                if (result == 0)
                {
                    usernameInputField.text = "";
                }
                else if (result == 1)
                {
                    passwordInputField.text = "";
                }
            }));
        }
    
        public void OnClickSignupButton()
        {
            GameManager.Instance.OpenSignupPanel();
        }
    }

     

    >> GameManager.cs

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.SceneManagement;
    
    public class GameManager : Singleton<GameManager>
    {
        [SerializeField] private GameObject settingsPanel;
        [SerializeField] private GameObject confirmPanel;
        [SerializeField] private GameObject signinPanel;
        [SerializeField] private GameObject signupPanel;
        
        private GameUIController _gameUIController;
        private Canvas _canvas;
        
        private Constants.GameType _gameType;
        private GameLogic _gameLogic;
    
        private void Start()
        {
            // 로그인
            StartCoroutine(NetworkManager.Instance.GetScore((userInfo) =>
            {
                Debug.Log("자동 로그인 성공" + "\n아이디 : " + userInfo.username + "\n로그인 닉네임 : " + userInfo.nickname);
                OpenConfirmPanel(userInfo.nickname + "님 로그인에 성공하였습니다.", () => { });
            }, () =>
            {
                Debug.Log("자동 로그인 실패");
                OpenSigninPanel();
            }));
        }
    
        public void ChangeToGameScene(Constants.GameType gameType)
        {
            _gameType = gameType;
            SceneManager.LoadScene("Game");
        }
    
        public void ChangeToMainScene()
        {
            _gameLogic?.Dispose();
            _gameLogic = null;
            SceneManager.LoadScene("Main");
        }
    
        public void OpenSettingsPanel()
        {
            if (_canvas != null)
            {
                var settingsPanelObject = Instantiate(settingsPanel, _canvas.transform);
                settingsPanelObject.GetComponent<PanelController>().Show();
            }
        }
    
        public void OpenConfirmPanel(string message, ConfirmPanelController.OnConfirmButtonClick onConfirmButtonClick)
        {
            if (_canvas != null)
            {
                var confirmPanelObject = Instantiate(confirmPanel, _canvas.transform);
                confirmPanelObject.GetComponent<ConfirmPanelController>()
                    .Show(message, onConfirmButtonClick);
            }
        }
    
        public void OpenSigninPanel()
        {
            if (_canvas != null)
            {
                var signinPanelObject = Instantiate(signinPanel, _canvas.transform);
            }
        }
    
        public void OpenSignupPanel()
        {
            if (_canvas != null)
            {
                var signupPanelObject = Instantiate(signupPanel, _canvas.transform);
            }
        }
    
        public void OpenGameOverPanel()
        {
            _gameUIController.SetGameUIMode(GameUIController.GameUIMode.GameOver);
        }
        
        protected override void OnSceneLoaded(Scene scene, LoadSceneMode mode)
        {
            if (scene.name == "Game")
            {
                // Scene에 배치된 Object 찾기 (BlockController, GameUIController)
                var blockController = GameObject.FindObjectOfType<BlockController>();
                _gameUIController = GameObject.FindObjectOfType<GameUIController>();
                
                // BlockController 초기화
                blockController.InitBlocks();
                
                // Game UI 초기화
                _gameUIController.SetGameUIMode(GameUIController.GameUIMode.Init);
                
                // Game Logic 객체 생성 --> 생성자 호출
                if (_gameLogic != null) _gameLogic.Dispose();
                _gameLogic = new GameLogic(blockController, _gameType);
            }
    
            // Canvas는 Main과 Game 모두 필요하기 때문에 if 밖에서 찾음
            _canvas = GameObject.FindObjectOfType<Canvas>();
        }
    
        private void OnApplicationQuit()
        {
            _gameLogic?.Dispose();
            _gameLogic = null;
        }
    }

     

    >> app.js

    var createError = require('http-errors');
    var express = require('express');
    var path = require('path');
    var cookieParser = require('cookie-parser');
    var logger = require('morgan');
    var mongodb = require('mongodb');       // 추가
    var MongoClient = mongodb.MongoClient;  // 추가

    var indexRouter = require('./routes/index');
    var usersRouter = require('./routes/users');
    var leaderboardRouter = require('./routes/leaderboard'); // 리더보드 구현
    const req = require('express/lib/request');
    const session = require('express-session');             // 추가
    var fileStore = require('session-file-store')(session); // 추가

    var app = express();

    // 로그인 만들기
    app.use(session({
      secret: process.env.SESSION_SECRET || 'session-login',
      resave: false,
      saveUninitialized: false,
      store: new fileStore({
        path: './sessions',
        ttl: 24 * 60 * 60, // 24시간
        reapInterval: 60 * 60 // 1시간
      }),
      cookie: {
        httpOnly: true,
        secure: process.env.NODE_ENV === 'production',
        maxAge: 24 * 60 * 60 * 1000 // 24시간
      }
    }));

    // MongoDB 추가
    async function connectDB() {
      var databaseURL = "mongodb://localhost:27017/tictactoe";

      try {
        const database = await MongoClient.connect(databaseURL, {
          useNewUrlParser: true,
          useUnifiedTopology: true
        });
        console.log("DB 연결 완료 : " + databaseURL);
        app.set('database', database.db('tictactoe'));

        // 연결 종료 처리
        process.on("SIGINT", async () => {
          await database.close();
          console.log("DB 연결 종료");
          process.exit(0);
        });
      } catch (err) {
        console.error("DB 연결 실패 : " + err);
        process.exit(1);
      }
    }

    connectDB().catch(err => {
      console.error("초기 DB 연결 실패 : " + err);
      process.exit(1);
    });

    // view engine setup
    app.set('views', path.join(__dirname, 'views'));
    app.set('view engine', 'pug');

    app.use(logger('dev'));
    app.use(express.json());
    app.use(express.urlencoded({ extended: false }));
    app.use(cookieParser());
    app.use(express.static(path.join(__dirname, 'public')));

    app.use('/', indexRouter);
    app.use('/users', usersRouter);
    app.use('/leaderboard', leaderboardRouter); // 리더보드 구현

    // catch 404 and forward to error handler
    app.use(function(req, res, next) {
      next(createError(404));
    });

    // error handler
    app.use(function(err, req, res, next) {
      // set locals, only providing error in development
      res.locals.message = err.message;
      res.locals.error = req.app.get('env') === 'development' ? err : {};

      // render the error page
      res.status(err.status || 500);
      res.render('error');
    });

    module.exports = app;

     

    >> leaderboard.js

    var express = require('express');
    var router = express.Router();

    router.get('/', async function(req, res, next) {
        try {
            if (!req.session.isAuthenticated) {
                return res.status(400).send("로그인이 필요합니다.");
            }

            var database = req.app.get('database');
            var users = database.collection('users');

            const allUsers = await users.find({}, {
                projection: {
                    username: 1,
                    nickname: 1,
                    score: 1
                }
            }).sort({score: -1}).toArray();

            const scoreList = allUsers.map(user => ({
                username: user.username,
                nickname: user.nickname,
                score: user.score || 0
            }));

            const result = {
                scores: scoreList
            };

            res.status(200).json(result);

        } catch (err) {
            console.error("리더보드 조회 중 오류 발생 : ", err);
            res.status(500).send("서버 오류가 발생했습니다.");
        }
    });

    module.exports = router;

     

    >> www

    #!/usr/bin/env node

    /**
     * Module dependencies.
     */

    var app = require('../app');
    var debug = require('debug')('tictactoe-server:server');
    var http = require('http');
    var game = require('../game');  // 추가

    /**
     * Get port from environment and store in Express.
     */

    var port = normalizePort(process.env.PORT || '3000');
    app.set('port', port);

    /**
     * Create HTTP server.
     */

    var server = http.createServer(app);

    /**
     * 게임 서버 실행
     */
    game(server); // 추가

    /**
     * Listen on provided port, on all network interfaces.
     */

    server.listen(port);
    server.on('error', onError);
    server.on('listening', onListening);

    /**
     * Normalize a port into a number, string, or false.
     */

    function normalizePort(val) {
      var port = parseInt(val, 10);

      if (isNaN(port)) {
        // named pipe
        return val;
      }

      if (port >= 0) {
        // port number
        return port;
      }

      return false;
    }

    /**
     * Event listener for HTTP server "error" event.
     */

    function onError(error) {
      if (error.syscall !== 'listen') {
        throw error;
      }

      var bind = typeof port === 'string'
        ? 'Pipe ' + port
        : 'Port ' + port;

      // handle specific listen errors with friendly messages
      switch (error.code) {
        case 'EACCES':
          console.error(bind + ' requires elevated privileges');
          process.exit(1);
          break;
        case 'EADDRINUSE':
          console.error(bind + ' is already in use');
          process.exit(1);
          break;
        default:
          throw error;
      }
    }

    /**
     * Event listener for HTTP server "listening" event.
     */

    function onListening() {
      var addr = server.address();
      var bind = typeof addr === 'string'
        ? 'pipe ' + addr
        : 'port ' + addr.port;
      debug('Listening on ' + bind);
    }

     

    >> game.js

    const { v4: uuidv4 } = require('uuid');

    module.exports = function(server) {
        const io = require('socket.io')(server);

        var rooms = [];

        io.on('connection', function(socket) {
            // 클라이언트가 연결되면 실행되는 이벤트 핸들러
            console.log('사용자가 연결 되었습니다.');

            if (rooms.length > 0) {
                var roomId = rooms.shift();
                socket.join(roomId);
                socket.emit('joinRoom', { roomId: roomId});
                socket.to(roomId).emit('startGame', { userId: socket.id });
            } else {
                var roomId = uuidv4();
                socket.join(roomId);
                socket.emit('createRoom', { roomId: roomId});
                rooms.push(roomId);
            }

            socket.on('leaveRoom', function() {
                socket.leave(roomId);
                socket.emit('leaveRoom', { roomId: roomId });
                socket.to(roomId).emit('gameEnded', { userId: socket.id });
            });

            socket.on('sendMessage', function(message) {
                console.log('메세지를 받았습니다 : ' + message.nickName + ' : ' + message.message + ' : ' + message.roomId);
                socket.to(message.roomId).emit('receiveMessage', { nickName: message.nickName, message: message.message });
            });

            socket.on('disconnect', function() {
                console.log('사용자가 연결을 해제했습니다.');
            });
        });
    }

    └ 추가 최종 코드

    >> GameManager.cs

    : 자동 로그인, 리더보드

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.SceneManagement;
    
    public class GameManager : Singleton<GameManager>
    {
        [SerializeField] private GameObject leaderboardPanel;
        [SerializeField] private GameObject settingsPanel;
        [SerializeField] private GameObject confirmPanel;
        [SerializeField] private GameObject signinPanel;
        [SerializeField] private GameObject signupPanel;
        
        private GameUIController _gameUIController;
        private Canvas _canvas;
        
        private Constants.GameType _gameType;
        private GameLogic _gameLogic;
    
        private void Start()
        {
            // 로그인
            StartCoroutine(NetworkManager.Instance.GetScore((userInfo) =>
            {
                Debug.Log("자동 로그인 성공" + "\n아이디 : " + userInfo.username + "\n로그인 닉네임 : " + userInfo.nickname);
                OpenConfirmPanel(userInfo.nickname + "님 로그인에 성공하였습니다.", () => { });
            }, () =>
            {
                Debug.Log("자동 로그인 실패");
                OpenSigninPanel();
            }));
        }
    
        public void ChangeToGameScene(Constants.GameType gameType)
        {
            _gameType = gameType;
            SceneManager.LoadScene("Game");
        }
    
        public void ChangeToMainScene()
        {
            _gameLogic?.Dispose();
            _gameLogic = null;
            SceneManager.LoadScene("Main");
        }
    
        public void OpenLeaderboardPanel()
        {
            if (_canvas != null)
            {
                var leaderboardPanelObject = Instantiate(leaderboardPanel, _canvas.transform);
    
                StartCoroutine(NetworkManager.Instance.GetLeaderboard(ranks =>
                {
                    foreach (var rank in ranks.scores)
                    {
                        var leaderboardController = leaderboardPanelObject.GetComponent<LeaderboardPanelController>();
                        leaderboardController.CreateScoreCell(rank);
                    }
                }, () =>
                {
                    Debug.Log("랭킹 가져오기 실패");
                }));
                
                leaderboardPanelObject.GetComponent<PanelController>().Show();
            }
        }
    
        public void OpenSettingsPanel()
        {
            if (_canvas != null)
            {
                var settingsPanelObject = Instantiate(settingsPanel, _canvas.transform);
                settingsPanelObject.GetComponent<PanelController>().Show();
            }
        }
    
        public void OpenConfirmPanel(string message, ConfirmPanelController.OnConfirmButtonClick onConfirmButtonClick)
        {
            if (_canvas != null)
            {
                var confirmPanelObject = Instantiate(confirmPanel, _canvas.transform);
                confirmPanelObject.GetComponent<ConfirmPanelController>()
                    .Show(message, onConfirmButtonClick);
            }
        }
    
        public void OpenSigninPanel()
        {
            if (_canvas != null)
            {
                var signinPanelObject = Instantiate(signinPanel, _canvas.transform);
            }
        }
    
        public void OpenSignupPanel()
        {
            if (_canvas != null)
            {
                var signupPanelObject = Instantiate(signupPanel, _canvas.transform);
            }
        }
    
        public void OpenGameOverPanel()
        {
            _gameUIController.SetGameUIMode(GameUIController.GameUIMode.GameOver);
        }
        
        protected override void OnSceneLoaded(Scene scene, LoadSceneMode mode)
        {
            if (scene.name == "Game")
            {
                // Scene에 배치된 Object 찾기 (BlockController, GameUIController)
                var blockController = GameObject.FindObjectOfType<BlockController>();
                _gameUIController = GameObject.FindObjectOfType<GameUIController>();
                
                // BlockController 초기화
                blockController.InitBlocks();
                
                // Game UI 초기화
                _gameUIController.SetGameUIMode(GameUIController.GameUIMode.Init);
                
                // Game Logic 객체 생성 --> 생성자 호출
                if (_gameLogic != null) _gameLogic.Dispose();
                _gameLogic = new GameLogic(blockController, _gameType);
            }
    
            // Canvas는 Main과 Game 모두 필요하기 때문에 if 밖에서 찾음
            _canvas = GameObject.FindObjectOfType<Canvas>();
        }
    
        private void OnApplicationQuit()
        {
            _gameLogic?.Dispose();
            _gameLogic = null;
        }
    }

     

    >> NetworkManager.cs

    : 리더보드, 승리 시 점수 획득

    using System;
    using System.Collections;
    using Newtonsoft.Json;
    using UnityEngine;
    using UnityEngine.SceneManagement;
    using UnityEngine.Networking;
    
    public class NetworkManager : Singleton<NetworkManager>
    {
        protected override void OnSceneLoaded(Scene scene, LoadSceneMode mode)
        {
            
        }
        
        public IEnumerator Signup(SignupData signupData, Action success, Action failure)
        {
            string jsonString = JsonUtility.ToJson(signupData);
            byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(jsonString);
    
            // () 안의 객체가 {}를 벗어나면 Disposing되는 문법
            using (UnityWebRequest www =
                   new UnityWebRequest(Constants.ServerURL + "/users/signup", UnityWebRequest.kHttpVerbPOST))
            {
                www.uploadHandler = new UploadHandlerRaw(bodyRaw);
                www.downloadHandler = new DownloadHandlerBuffer();
                www.SetRequestHeader("Content-Type", "application/json");
    
                yield return www.SendWebRequest();
    
                if (www.result == UnityWebRequest.Result.ConnectionError || 
                    www.result == UnityWebRequest.Result.ProtocolError)
                {
                    Debug.Log("Error : " + www.error);
    
                    if (www.responseCode == 409) // 중복 사용자
                    {
                        // TODO: 중복 사용자 생성 팝업 표시
                        Debug.Log("중복 사용자");
                        GameManager.Instance.OpenConfirmPanel("이미 존재하는 사용자입니다.", () =>
                        {
                            failure?.Invoke();
                        });
                    }
                }
                else
                {
                    var result = www.downloadHandler.text;
                    Debug.Log("Result : " + result);
                    
                    // 회원가입 성공 팝업 표시
                    GameManager.Instance.OpenConfirmPanel("회원가입이 완료 되었습니다.", () =>
                    {
                        success?.Invoke();
                    });
                }
            }
        }
        
        public IEnumerator Signin(SigninData signinData, Action success, Action<int> failure)
        {
            string jsonString = JsonUtility.ToJson(signinData);
            byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(jsonString);
    
            using (UnityWebRequest www =
                   new UnityWebRequest(Constants.ServerURL + "/users/signin", UnityWebRequest.kHttpVerbPOST))
            {
                www.uploadHandler = new UploadHandlerRaw(bodyRaw);
                www.downloadHandler = new DownloadHandlerBuffer();
                www.SetRequestHeader("Content-Type", "application/json");
    
                yield return www.SendWebRequest();
    
                if (www.result == UnityWebRequest.Result.ConnectionError ||
                    www.result == UnityWebRequest.Result.ProtocolError)
                {
                    
                }
                else
                {
                    // 점수 불러오기 기능을 구현하기 위해 cookie 값 저장 (수강생님 코드 참고)
                    var cookie = www.GetResponseHeader("set-cookie");
                    if (!string.IsNullOrEmpty(cookie))
                    {
                        //int lastIndex = cookie.LastIndexOf(";");
                        //string sid = cookie.Substring(0, lastIndex);
                        //PlayerPrefs.SetString("sid", sid);
                        
                        int lastIndex = cookie.IndexOf('='); // 첫 번째 '='의 위치 찾기
                        if (lastIndex != -1 && lastIndex + 1 < cookie.Length) // '='이 존재하고, 뒤에 값이 있을 경우
                        {
                            string sid = cookie.Substring(lastIndex + 1).Split(';')[0]; // '=' 다음부터 ';' 이전까지 추출
                            Debug.Log("Session ID : " + sid);
                            PlayerPrefs.SetString("sid", sid);
                        }
                        else
                        {
                            // 올바른 쿠키 형식이 아님
                        }
                    }
                    else
                    {
                        Debug.Log("쿠키가 비어있습니다.");
                    }
                    
                    var resultString = www.downloadHandler.text;
                    var result = JsonUtility.FromJson<SigninResult>(resultString);
    
                    if (result.result == 0)
                    {
                        // username이 유효하지 않음
                        GameManager.Instance.OpenConfirmPanel("Username이 유효하지 않습니다.", () =>
                        {
                            failure?.Invoke(0);
                        });
                    }
                    else if (result.result == 1)
                    {
                        // password가 유효하지 않음
                        GameManager.Instance.OpenConfirmPanel("Password가 유효하지 않습니다.", () =>
                        {
                            failure?.Invoke(1);
                        });
                    }
                    else if (result.result == 2)
                    {
                        // 성공
                        GameManager.Instance.OpenConfirmPanel("로그인에 성공하였습니다.", () =>
                        {
                            success?.Invoke();
                        });
                    }
                }
            }
        }
    
        public IEnumerator GetScore(Action<ScoreResult> success, Action failure)
        {
            using (UnityWebRequest www = 
                   new UnityWebRequest(Constants.ServerURL + "/users/score", UnityWebRequest.kHttpVerbGET))
            {
                www.downloadHandler = new DownloadHandlerBuffer();
                
                string sid = PlayerPrefs.GetString("sid", "");
                if (!string.IsNullOrEmpty(sid))
                {
                    www.SetRequestHeader("Cookie", sid);
                }
    
                yield return www.SendWebRequest();
    
                if (www.result == UnityWebRequest.Result.ConnectionError || 
                    www.result == UnityWebRequest.Result.ProtocolError)
                {
                    if (www.responseCode == 403)
                    {
                        Debug.Log("로그인이 필요합니다.");
                    }
                    
                    failure?.Invoke();
                }
                else
                {
                    var result = www.downloadHandler.text;
                    var userScore = JsonUtility.FromJson<ScoreResult>(result);
                    
                    Debug.Log(userScore.score);
                    
                    success?.Invoke(userScore);
                }
            }
        }
    
        public IEnumerator AddScore(ScoreData score, Action success, Action failure)
        {
            string jsonString = JsonUtility.ToJson(score);
            byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(jsonString);
            
            using (UnityWebRequest www =
                   new UnityWebRequest(Constants.ServerURL + "/users/addscore", UnityWebRequest.kHttpVerbPOST))
            {
                www.uploadHandler = new UploadHandlerRaw(bodyRaw);
                www.downloadHandler = new DownloadHandlerBuffer();
                www.SetRequestHeader("Content-Type", "application/json");
                
                string sid = PlayerPrefs.GetString("sid", "");
                if (!string.IsNullOrEmpty(sid))
                {
                    www.SetRequestHeader("Cookie", sid);
                }
    
                yield return www.SendWebRequest();
    
                if (www.result == UnityWebRequest.Result.ConnectionError ||
                    www.result == UnityWebRequest.Result.ProtocolError)
                {
                    if (www.responseCode == 403)
                    {
                        Debug.Log("로그인이 필요합니다.");
                    }
    
                    if (www.responseCode == 404)
                    {
                        Debug.Log("사용자를 찾을 수 없습니다.");
                    }
                    
                    failure?.Invoke();
                }
                else
                {
                    Debug.Log("Success Add Score");
                    success?.Invoke();
                }
            }
        }
    
        public IEnumerator GetLeaderboard(Action<Scores> success, Action failure)
        {
            using (UnityWebRequest www = 
                   new UnityWebRequest(Constants.ServerURL + "/leaderboard", UnityWebRequest.kHttpVerbGET))
            {
                www.downloadHandler = new DownloadHandlerBuffer();
                
                string sid = PlayerPrefs.GetString("sid", "");
                if (!string.IsNullOrEmpty(sid))
                {
                    www.SetRequestHeader("Cookie", sid);
                }
    
                yield return www.SendWebRequest();
    
                if (www.result == UnityWebRequest.Result.ConnectionError || 
                    www.result == UnityWebRequest.Result.ProtocolError)
                {
                    if (www.responseCode == 403)
                    {
                        Debug.Log("로그인이 필요합니다.");
                    }
                    
                    failure?.Invoke();
                }
                else
                {
                    var result = www.downloadHandler.text;
                    //var scores = JsonUtility.FromJson<Scores>(result);    // scores가 null로 나오는 문제 발생
                    var scores = JsonConvert.DeserializeObject<Scores>(result); // 해결 코드
                    
                    success?.Invoke(scores);
                }
            }
        }
    }

     

    >> MainPanelController.cs

    : 리더보드

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.SceneManagement;
    
    public class MainPanelController : MonoBehaviour
    {
        public void OnClickSinglePlayButton()
        {
            GameManager.Instance.ChangeToGameScene(Constants.GameType.SinglePlayer);
        }
        
        public void OnClickDualPlayButton()
        {
            GameManager.Instance.ChangeToGameScene(Constants.GameType.DualPlayer);
        }
    
        public void OnClickMultiplayButton()
        {
            GameManager.Instance.ChangeToGameScene(Constants.GameType.MultiPlayer);
        }
    
        public void OnClickLeaderboardButton()
        {
            GameManager.Instance.OpenLeaderboardPanel();
        }
        
        public void OnClickSettingsButton()
        {
            GameManager.Instance.OpenSettingsPanel();
        }
    }

     

    >> LeaderboardPanelController.cs

    : 리더보드

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class LeaderboardPanelController : PanelController
    {
        [SerializeField] private GameObject scoreCell;
        [SerializeField] private Transform content;
    
        public void CreateScoreCell(ScoreInfo scoreInfo)
        {
            var scoreCellObject = Instantiate(scoreCell, content);
            var scoreCellController = scoreCellObject.GetComponent<ScoreCellController>();
            scoreCellController.SetCellInfo(scoreInfo);
        }
        
        /// <summary>
        /// X 버튼 클릭 시 호출되는 함수
        /// </summary>
        public void OnClickCloseButton()
        {
            Hide();
        }
    }

     

    >> ScoreCellController.cs

    : 리더보드

    using System.Collections;
    using System.Collections.Generic;
    using TMPro;
    using UnityEngine;
    
    public class ScoreCellController : MonoBehaviour
    {
        [SerializeField] private TMP_Text nicknameText;
        [SerializeField] private TMP_Text scoreText;
    
        public void SetCellInfo(ScoreInfo scoreInfo)
        {
            nicknameText.text = scoreInfo.nickname;
            scoreText.text = scoreInfo.score.ToString();
        }
    }

     

    >> GameLogic.cs

    : 승리 시 점수 획득

    using System;
    using UnityEngine;
    
    public abstract class BasePlayerState
    {
        public abstract void OnEnter(GameLogic gameLogic);  // 해당 State가 할당될 때 처리
        public abstract void OnExit(GameLogic gameLogic);   // 해당 State가 교체될 때 처리
        public abstract void HandleMove(GameLogic gameLogic, int row, int col); // 해당 State의 행위에 따라 마커 표시
        protected abstract void HandleNextTurn(GameLogic gameLogic);    // State 전환
    
        // 마커 표시 및 결과 처리
        protected void ProcessMove(GameLogic gameLogic, Constants.PlayerType playerType, int row, int col)
        {
            if (gameLogic.SetNewBoardValue(playerType, row, col))
            {
                var gameResult = gameLogic.CheckGameResult();
    
                if (gameResult == GameLogic.GameResult.None)
                {
                    HandleNextTurn(gameLogic);
                }
                else
                {
                    switch (gameResult)
                    {
                        case GameLogic.GameResult.Win:
                            Debug.Log("GameResult : Win");
                            CoroutineHelper.Instance.AddScore();
                            break;
                    }
                    gameLogic.EndGame(gameResult);
                }
            }
        }
    }
    
    // 직접 플레이 (싱글, 네트워크)
    public class PlayerState : BasePlayerState
    {
        private Constants.PlayerType _playerType;
        private bool _isFirstPlayer;
        
        private MultiplayManager _multiplayManager;
        private string _roomId;
        private bool _isMultiplay;
        
        public PlayerState(bool isFirstPlayer)
        {
            _isFirstPlayer = isFirstPlayer;
            _playerType = _isFirstPlayer ? Constants.PlayerType.PlayerA : Constants.PlayerType.PlayerB;
            _isMultiplay = false;
        }
    
        public PlayerState(bool isFirstPlayer, MultiplayManager multiplayManager, string roomId) : this(isFirstPlayer)
        {
            _multiplayManager = multiplayManager;
            _roomId = roomId;
            _isMultiplay = true;
        }
        
        public override void OnEnter(GameLogic gameLogic)
        {
            gameLogic.blockController.OnBlockClickedDelegate = (row, col) =>
            {
                HandleMove(gameLogic, row, col);
            };
        }
    
        public override void OnExit(GameLogic gameLogic)
        {
            gameLogic.blockController.OnBlockClickedDelegate = null; // 게임이 끝났을 때 board를 Click해도 바뀌지 않도록
        }
    
        public override void HandleMove(GameLogic gameLogic, int row, int col)
        {
            ProcessMove(gameLogic, _playerType, row, col);
            if (_isMultiplay)
            {
                _multiplayManager.SendPlayerMove(_roomId, row * 3 + col);
            }
        }
    
        protected override void HandleNextTurn(GameLogic gameLogic)
        {
            if (_isFirstPlayer)
            {
                gameLogic.SetState(gameLogic.secondPlayerState);
            }
            else
            {
                gameLogic.SetState(gameLogic.firstPlayerState);
            }
        }
    }
    
    // AI 플레이
    public class AIState : BasePlayerState
    {
        public override void OnEnter(GameLogic gameLogic)
        {
            // AI 연산
            var result = MinimaxAIController.GetBestMove(gameLogic.GetBoard());
            if (result.HasValue)
            {
                HandleMove(gameLogic, result.Value.row, result.Value.col);
            }
            else
            {
                gameLogic.EndGame(GameLogic.GameResult.Draw);
            }
        }
    
        public override void OnExit(GameLogic gameLogic)
        {
        }
    
        public override void HandleMove(GameLogic gameLogic, int row, int col)
        {
            ProcessMove(gameLogic, Constants.PlayerType.PlayerB, row, col);
        }
    
        protected override void HandleNextTurn(GameLogic gameLogic)
        {
            gameLogic.SetState(gameLogic.firstPlayerState);
        }
    }
    
    // 네트워크 플레이 --> 서버로부터 상대 Player를 기다림
    public class MultiplayState : BasePlayerState
    {
        private Constants.PlayerType _playerType;
        private bool _isFirstPlayer;
        
        private MultiplayManager _multiplayManager;
    
        public MultiplayState(bool isFirstPlayer, MultiplayManager multiplayManager)
        {
            _isFirstPlayer = isFirstPlayer;
            _playerType = _isFirstPlayer ? Constants.PlayerType.PlayerA : Constants.PlayerType.PlayerB;
            _multiplayManager = multiplayManager;
        }
        
        public override void OnEnter(GameLogic gameLogic)
        {
            _multiplayManager.OnOpponentMove = moveData =>
            {
                var row = moveData.position / 3;
                var col = moveData.position % 3;
                UnityThread.executeInUpdate(() =>
                {
                    HandleMove(gameLogic, row, col);
                });
            };
        }
    
        public override void OnExit(GameLogic gameLogic)
        {
            _multiplayManager.OnOpponentMove = null;
        }
    
        public override void HandleMove(GameLogic gameLogic, int row, int col)
        {
            ProcessMove(gameLogic, _playerType, row, col);
        }
    
        protected override void HandleNextTurn(GameLogic gameLogic)
        {
            if (_isFirstPlayer)
            {
                gameLogic.SetState(gameLogic.secondPlayerState);
            }
            else
            {
                gameLogic.SetState(gameLogic.firstPlayerState);
            }
        }
    }
    
    public class GameLogic : IDisposable
    {
        public BlockController blockController;
        private Constants.PlayerType[,] _board;
        
        public BasePlayerState firstPlayerState;      // 첫 번째 턴 상태 객체
        public BasePlayerState secondPlayerState;     // 두 번째 턴 상태 객체
        private BasePlayerState _currentPlayerState;  // 현재 턴 상태 객체
        
        private MultiplayManager _multiplayManager;
        private string _roomId;
        
        public enum GameResult { None, Win, Lose, Draw }
        
        public GameLogic(BlockController blockController, Constants.GameType gameType,
            MultiplayManager multiplayManager = null, string roomId = null, bool isFirstPlayer = true)
        {
            this.blockController = blockController;
            
            // _board 초기화
            _board = new Constants.PlayerType[3, 3];
    
            switch (gameType)
            {
                case Constants.GameType.SinglePlayer:
                    firstPlayerState = new PlayerState(true);
                    secondPlayerState = new AIState();
                    // 게임 시작
                    SetState(firstPlayerState);
                    break;
                case Constants.GameType.DualPlayer:
                    firstPlayerState = new PlayerState(true);
                    secondPlayerState = new PlayerState(false);
                    // 게임 시작
                    SetState(firstPlayerState);
                    break;
                case Constants.GameType.MultiPlayer:
                    // Multiplay Manager 생성
                    _multiplayManager = new MultiplayManager((state, roomId) =>
                    {
                        _roomId = roomId;
                        switch (state)
                        {
                            case Constants.MultiplayManagerState.CreateRoom:
                                Debug.Log("## Create Room ##");
                                // TODO: 대기 화면 표시
                                // GameManager에게 대기 화면 표시 요청
                                break;
                            case Constants.MultiplayManagerState.JoinRoom: // 게임 실행 --> Room에 입장한 사람
                                Debug.Log("## Join Room ##");
                                firstPlayerState = new MultiplayState(true, _multiplayManager);
                                secondPlayerState = new PlayerState(false, _multiplayManager, roomId);
                                // 게임 시작
                                SetState(firstPlayerState);
                                break;
                            case Constants.MultiplayManagerState.ExitRoom:
                                Debug.Log("## Exit Room ##");
                                // TODO: Exit Room 처리
                                break;
                            case Constants.MultiplayManagerState.StartGame: // 대기 화면을 닫고 게임 실행 --> Create Room을 한 사람
                                Debug.Log("## Start Game ##");
                                firstPlayerState = new PlayerState(true, _multiplayManager, roomId);
                                secondPlayerState = new MultiplayState(false, _multiplayManager);
                                // 게임 시작
                                SetState(firstPlayerState);
                                break;
                            case Constants.MultiplayManagerState.EndGame:
                                Debug.Log("## End Game ##");
                                // TODO: End Game 처리
                                break;
                        }
                    });
                    break;
            }
        }
    
        public void SetState(BasePlayerState state)
        {
            _currentPlayerState?.OnExit(this);
            _currentPlayerState = state;
            _currentPlayerState?.OnEnter(this);
        }
        
        /// <summary>
        /// _board에 새로운 값을 할당하는 함수
        /// </summary>
        /// <param name="playerType">할당하고자 하는 플레이어 타입</param>
        /// <param name="row">Row</param>
        /// <param name="col">Col</param>
        /// <returns>False : 할당할 수 없음, True : 할당이 완료됨</returns>
        public bool SetNewBoardValue(Constants.PlayerType playerType, int row, int col)
        {
            if (_board[row, col] != Constants.PlayerType.None) return false; // 중복 체크 방지
            
            if (playerType == Constants.PlayerType.PlayerA)
            {
                _board[row, col] = playerType;
                blockController.PlaceMarker(Block.MarkerType.O, row, col);
                return true;
            }
            else if (playerType == Constants.PlayerType.PlayerB)
            {
                _board[row, col] = playerType;
                blockController.PlaceMarker(Block.MarkerType.X, row, col);
                return true;
            }
            return false;
        }
        
        /// <summary>
        /// 게임 결과 확인 함수
        /// </summary>
        /// <returns>플레이어 기준 게임 결과</returns>
        public GameResult CheckGameResult()
        {
            if (CheckGameWin(Constants.PlayerType.PlayerA)) return GameResult.Win;
            if (CheckGameWin(Constants.PlayerType.PlayerB)) return GameResult.Lose;
            if (MinimaxAIController.IsAllBlocksPlaced(_board)) return GameResult.Draw;
            
            return GameResult.None;
        }
        
        // 게임의 승패를 판단하는 함수
        private bool CheckGameWin(Constants.PlayerType playerType)
        {
            // 가로로 마커가 일치하는지 확인 
            for (var row = 0; row < _board.GetLength(0); row++)
            {
                if (_board[row, 0] == playerType && _board[row, 1] == playerType && _board[row, 2] == playerType)
                {
                    (int, int)[] blocks = { (row, 0), (row, 1), (row, 2) };
                    blockController.SetBlockColor(playerType, blocks);
                    return true;
                }
            }
            
            // 세로로 마커가 일치하는지 확인
            for (var col = 0; col < _board.GetLength(1); col++)
            {
                if (_board[0, col] == playerType && _board[1, col] == playerType && _board[2, col] == playerType)
                {
                    (int, int)[] blocks = { (0, col), (1, col), (2, col) };
                    blockController.SetBlockColor(playerType, blocks);
                    return true;
                }
            }
            
            // 대각선으로 마커가 일치하는지 확인
            if (_board[0, 0] == playerType && _board[1, 1] == playerType && _board[2, 2] == playerType)
            {
                (int, int)[] blocks = { (0, 0), (1, 1), (2, 2) };
                blockController.SetBlockColor(playerType, blocks);
                return true;
            }
    
            if (_board[0, 2] == playerType && _board[1, 1] == playerType && _board[2, 0] == playerType)
            {
                (int, int)[] blocks = { (0, 2), (1, 1), (2, 0) };
                blockController.SetBlockColor(playerType, blocks);
                return true;
            }
            
            return false;
        }
        
        /// <summary>
        /// 게임 오버 시, 호출되는 함수
        /// gameResult에 따라 결과 출력
        /// </summary>
        /// <param name="gameResult">win, lose, draw</param>
        public void EndGame(GameResult gameResult)
        {
            SetState(null);
    
            firstPlayerState = null;
            secondPlayerState = null;
            
            // 게임 오버 표시
            GameManager.Instance.OpenGameOverPanel();
        }
        
        public Constants.PlayerType[,] GetBoard()
        {
            return _board;
        }
    
        public void Dispose()
        {
            _multiplayManager?.LeaveRoom(_roomId);
            _multiplayManager?.Dispose();
        }
    }

     

    >> CoroutineHelper.cs

    : 승리 시 점수 획득

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.SceneManagement;
    
    public struct ScoreData
    {
        public int score;
    }
    
    public class CoroutineHelper : Singleton<CoroutineHelper>
    {
        ScoreData scoreData = new ScoreData { score = 10 };
        
        protected override void OnSceneLoaded(Scene scene, LoadSceneMode mode)
        {
            
        }
        
        public void MyStartCoroutine(IEnumerator coroutine)
        {
            StartCoroutine(coroutine);
        }
    
        public void AddScore()
        {
            StartCoroutine(NetworkManager.Instance.AddScore((scoreData), () =>
            {
                Debug.Log("AddScore Complete");
            }, () => { }));
        }
    }

     

    >> users.js

    : 승리 시 점수 획득

    var express = require('express');
    var router = express.Router();
    var bcrypt = require('bcrypt');
    const { ObjectId } = require('mongodb');
    var saltrounds = 10;

    var ResponseType = {
      INVALID_USERNAME: 0,
      INVALID_PASSWORD: 1,
      SUCCESS: 2
    }

    /* GET users listing. */
    router.get('/', function(req, res, next) {
      res.send('respond with a resource');
    });

    // 회원가입
    router.post('/signup', async function(req, res, next) {
      try {
        var username = req.body.username;
        var password = req.body.password;
        var nickname = req.body.nickname;

        // 입력값 검증
        if (!username || !password || !nickname) {
          return res.status(400).send("모든 필드를 입력해주세요");
        }

        // 사용자 중복 체크
        var database = req.app.get('database');
        var users = database.collection('users');
       
        const existingUser = await users.findOne({ username: username });
        if (existingUser) {
          return res.status(409).send("이미 존재하는 사용자입니다");
        }

        // 비밀번호 암호화
        var salt = bcrypt.genSaltSync(saltrounds);
        var hash = bcrypt.hashSync(password, salt);
       
        // DB에 저장
        await users.insertOne({
          username: username,
          password: hash, // 해시된 비밀번호 저장
          nickname: nickname
        });
        res.status(201).send("사용자가 성공적으로 생성되었습니다");
      } catch (err) {
        console.error("사용자 추가 중 오류 발생 :", err);
        res.status(500).send("서버 오류가 발생했습니다");
      }
    });

    // 로그인
    router.post("/signin", async function(req, res, next) {
      try {
        var username = req.body.username;
        var password = req.body.password;
       
        var database = req.app.get('database');
        var users = database.collection('users');

        // 입력값 검증
        if (!username || !password) {
          return res.status(400).send("모든 필드를 입력해주세요.");
        }

        const existingUser = await users.findOne({ username: username });
        if (existingUser) {
          var compareResult = bcrypt.compareSync(password, existingUser.password);
          if (compareResult) {
            // 세션에 사용자 정보(로그인 정보) 저장
            req.session.isAuthenticated = true;
            req.session.userId = existingUser._id.toString();
            req.session.username = existingUser.username;
            req.session.nickname = existingUser.nickname;

            res.json({ result: ResponseType.SUCCESS });
          } else {
            res.json({ result: ResponseType.INVALID_PASSWORD });
          }
        } else {
          res.json({ result: ResponseType.INVALID_USERNAME });
        }
      } catch (err) {
        console.error("로그인 중 오류 발생.", err);
        res.status(500).send("서버 오류가 발생했습니다");
      }
    });

    // 로그아웃
    router.post('/signout', function(req, res, next) {
      req.session.destroy((err) => {
        if (err) {
          console.log("로그아웃 중 오류 발생");
          return res.status(500).send("서버 오류가 발생했습니다.");
        }
        res.status(200).send("로그아웃 되었습니다.");
      });
    });

    // 점수 추가
    router.post('/addscore', async function(req, res, next) {
      try {
        // 로그인 여부 체크
        if (!req.session.isAuthenticated) {
          return res.status(403).send("로그인이 필요합니다.");
        }

        var userId = req.session.userId;
        var score = req.body.score;

        // 점수 유효성 검사
        if (!score || isNaN(score)) {
          return res.status(400).send("유효한 점수를 입력해주세요.");
        }

        var database = req.app.get('database');
        var users = database.collection('users');

        const result = await users.updateOne(
          { _id: new ObjectId(userId) },
          {
              $inc: { score: Number(score) }, // 기존 점수에 추가
              $set: { updatedAt: new Date() }
          }
        );

        if (result.matchedCount === 0) {
          return res.status(404).send("사용자를 찾을 수 없습니다.");
        }

        res.status(200).json({ message: "점수가 성공적으로 업데이트 되었습니다."});
      } catch (err) {
        console.error("점수 추가 중 오류 발생 : ", err);
        res.status(500).send("서버 오류가 발생했습니다.");
      }
    });

    // 점수 조회
    router.get('/score', async function(req, res, next) {
      try {
        if (!req.session.isAuthenticated) {
          return res.status(403).send("로그인이 필요합니다.");
        }

        var userId = req.session.userId;
        var database = req.app.get('database');
        var users = database.collection('users');

        const user = await users.findOne({ _id: new ObjectId(userId) });

        if (!user) {
          return res.status(404).send("사용자를 찾을 수 없습니다.");
        }

        res.json({
          id: user._id.toString(),
          username: user.username,
          nickname: user.nickname,
          score: user.score || 0
        });
      } catch (err) {
        console.error("점수 조회 중 오류 발생 : ", err);
        res.status(500).send("서버 오류가 발생했습니다.");
      }
    });

    module.exports = router;