오늘의 목표

더보기

✔️ 최종프로젝트 진행

 


⏱️ 오늘의 일정

최종프로젝트 진행


📜 최종프로젝트 진행

 

최종프로젝트를 진행했다.저번에 이어 게임 준비와 게임 시작 까지 완성해 게임의 기본적인 틀을 완성했다.

 

게임 준비 요청 처리

import net from 'net';
import { GlobalFailCode } from '../enumTyps.js';
import {
  CustomSocket,
  RedisUserData,
  Room,
  User,
} from '../../interface/interface.js';
import { config } from '../../config/config.js';
import { sendPacket } from '../../packet/createPacket.js';
import {
  getRoomByUserId,
  getRooms,
  getSocketByUser,
  getUserBySocket,
  setCharacterInfoInit,
  setRedisData,
} from '../handlerMethod.js';

export const gamePrepareHandler = async (
  socket: CustomSocket,
  payload: Object,
) => {
  try {
    // requset 보낸 유저
    const user: RedisUserData = await getUserBySocket(socket);

    // 유저가 있는 방 찾기
    if (user !== undefined) {
      const room: Room | null = await getRoomByUserId(user.id);
      if (room === null) {
        return;
      }
      if (room.users.length <= 1) {
        console.error('게임을 시작 할 수 없습니다(인원 부족).');
        const responseData = {
          success: false,
          failCode: GlobalFailCode.INVALID_REQUEST,
        };
        sendPacket(
          socket,
          config.packetType.GAME_PREPARE_RESPONSE,
          responseData,
        );
      } else {
        // 게임준비 시작 요건 충족
        const responseData = {
          success: true,
          failCode: GlobalFailCode.NONE,
        };
        sendPacket(
          socket,
          config.packetType.GAME_PREPARE_RESPONSE,
          responseData,
        );

        // 방에있는 유저들 캐릭터 랜덤 배정하기
        room.users = setCharacterInfoInit(room.users);
        const rooms: Room[] | null = await getRooms();
        if (!rooms) {
          return;
        }
        // 변경한 정보 덮어쓰기
        for (let i = 0; i < rooms.length; i++) {
          if (rooms[i].id === room.id) {
            rooms[i] = room;
            break;
          }
        }
        // 레디스에 있는 룸 배열에서 user가 속해 있는 방을 수정하고,
        // 위에서 수정한 방이 포함되어 있는 전체 배열을 넣음
        await setRedisData('roomData', rooms);

        // 방에있는 유저들에게 notifi 보내기
        for (let i = 0; i < room.users.length; i++) {
          const userSocket = await getSocketByUser(room.users[i]);
          if (!userSocket) {
            console.error('gamePrepareHandler: socket not found');
            return;
          }
          //console.dir(room, { depth: null });
          sendPacket(userSocket, config.packetType.GAME_PREPARE_NOTIFICATION, {
            room,
          });
        }
      }
    } else {
      console.error('위치: gamePrepareHandler, 유저를 찾을 수 없습니다.');
      const responseData = {
        success: false,
        failCode: GlobalFailCode.INVALID_REQUEST,
      };
      sendPacket(socket, config.packetType.GAME_PREPARE_RESPONSE, responseData);
    }
  } catch (err) {
    const responseData = {
      success: false,
      failCode: GlobalFailCode.INVALID_REQUEST,
    };
    sendPacket(socket, config.packetType.GAME_PREPARE_RESPONSE, responseData);
    console.error('gameStartHandler 오류', err);
  }
};

 

위 코드는 게임 준비 요청을 클라가 전송하면 서버에서 처리를 해주는 코드로,

우선 클라가 속한 방을 찾은 후, 방이 게임 준비 요건에 맞는지 검사한다.

 

방이 게임 준비 요건에 맞으면, 각 캐릭터들의 역할을 랜덤하게 서버가 정해주고 방 정보를 저장한 후,

방 안에 있는 유저들에게 noti를 날려 게임 준비가 완료되었다고 알려준다.

 

게임 준비가 완료되어 캐릭터와 역할이 정해진 모습


게임시작 요청 처리

import net from 'net';
import {
  CharacterPositionData,
  CustomSocket,
  RedisUserData,
} from '../../interface/interface.js';
import { GlobalFailCode, PhaseType } from '../enumTyps.js';
import { sendPacket } from '../../packet/createPacket.js';
import { config } from '../../config/config.js';
import {
  getRedisData,
  getRoomByUserId,
  getSocketByUser,
  getUserBySocket,
  setRedisData,
} from '../handlerMethod.js';

export const gameStartHandler = async (
  socket: CustomSocket,
  payload: Object,
) => {  
  try {
    // requset 보낸 유저
    const user: RedisUserData = await getUserBySocket(socket);
    // 유저가 있는 방 찾기
    if (user !== undefined) {
      const room = await getRoomByUserId(user.id);
      if (room === null) {
        const responseData = {
          success: false,
          failCode: GlobalFailCode.INVALID_REQUEST,
        };
        sendPacket(socket, config.packetType.GAME_START_RESPONSE, responseData);

        return;
      }

      if (room.users.length <= 1) {
        console.error('게임을 시작 할 수 없습니다.(인원 부족)');
      }

      const responseData = {
        success: true,
        failCode: GlobalFailCode.NONE,
      };
      sendPacket(socket, config.packetType.GAME_START_RESPONSE, responseData);

      let characterPositionDatas = await getRedisData('characterPositionDatas');
      if (!characterPositionDatas) {
        characterPositionDatas = { [room.id]: [] };
      } else {
        characterPositionDatas[room.id] = [];
      }

      for (let i = 0; i < room.users.length; i++) {
        // 위치 데이터 로직 필요
        const characterPositionData: CharacterPositionData = {
          id: room.users[i].id,
          x: 0 + 1 * i,
          y: 0 - 1 * i,
        };
        characterPositionDatas[room.id].push(characterPositionData);
      }

      await setRedisData('characterPositionDatas', characterPositionDatas);

      // 방에있는 유저들에게 notifi 보내기
      for (let i = 0; i < room.users.length; i++) {
        const userSocket = await getSocketByUser(room.users[i]);
        if (!userSocket) {
          console.error('gameStartHandler: socket not found');
          const responseData = {
            success: false,
            failCode: GlobalFailCode.INVALID_REQUEST,
          };
          sendPacket(
            socket,
            config.packetType.GAME_START_RESPONSE,
            responseData,
          );
          return;
        }
        // noti 데이터
        const now = Date.now() + 300000;
        const gameStateData = { phaseType: PhaseType.DAY, nextPhaseAt: now };
        const notifiData = {
          gameState: gameStateData,
          users: room.users,
          characterPositions: characterPositionDatas[room.id],
        };

        sendPacket(
          userSocket,
          config.packetType.GAME_START_NOTIFICATION,
          notifiData,
        );
      }
    } else {
      console.error('위치: gameStartHandler, 유저를 찾을 수 없습니다.');

      const responseData = {
        success: false,
        failCode: GlobalFailCode.INVALID_REQUEST,
      };
      sendPacket(socket, config.packetType.GAME_START_RESPONSE, responseData);
    }
  } catch (err) {
    const responseData = {
      success: false,
      failCode: GlobalFailCode.INVALID_REQUEST,
    };
    sendPacket(socket, config.packetType.GAME_START_RESPONSE, responseData);
    console.log('gameStartHandler 오류', err);
  }
};

 

위 코드는 게임 시작 요청을 클라가 전송하면 서버에서 처리를 해주는 코드로,

우선 클라가 속한 방을 찾은 후, 방이 게임 준비 요건에 맞는지 검사한다.

 

방이 게임 시작 요건에 맞으면, 각 캐릭터들의 좌표를 설정해주고, 레디스에 좌표를 저장 한 후

게임 시작에 필요한 정보를 설정하고, 방안에 있는 유저들에게 noti를 날려 게임시작이 완료되었다고 알려준다.

 

 

게임시작이 완료되어 캐릭터가 생성된 모습

 

 

게임 시작에서 캐릭터의 시작 위치를 랜덤하게 설정해줬는데,

추후에 미리 스폰 위치를 여러개 설정하고, 스폰 위치 중에서 랜덤하게 선택해 유저가 스폰될 수 있도록 고쳐야 겠다.

오늘의 목표

더보기

✔️ 최종프로젝트 진행


⏱️ 오늘의 일정

최종프로젝트 진행


📜 최종프로젝트 진행

 

최종 프로젝트 회의를 진행했다.

캠프에서 제공해주는 클라는 총 2가지 인데, 다음과 같다. 

 

 

 

3d 캐릭터로 구성되어 있는 턴제 기반의 클라

 


 

 

 

2D 캐릭터로 구성되어 있는 클라를 제공해줬다.

 


 

게임 플로우 정하기

 

팀원분들과 회의를 거쳐서 우리 팀이 만들고자 하는 게임을 정했다.

내용은 다음과 같다.

 

5명의 유저 중 1명이 보스가 되고 4명이 일반 유저가 되어 서로 싸우는 게임

  •  5명의 유저가 매칭이 되면 랜덤하게 1명이 보스가 되고 나머지 4명의 유저는 4개의 직업을 1개씩 나눠 가지고 게임이 시작된다.보스는 4명의 유저가 성장하기 전에 쫒아다니면서 그들을 죽인다.
  • 성장의 시간이 끝나면 탈출로가 열리고 해당 탈출로를 통해 탈출한 유저는 승리 그렇지 못한 유저는 패배가 된다.
  • 4명의 유저는 스폰되는 몬스터를 잡아서 보스에 대적할 수 있도록 실시간으로 성장하며 보스는 4명의 유저가 성장하기 전에 쫒아다니면서 그들을 죽인다.
  • 성장의 시간이 끝나면 탈출로가 열리고 해당 탈출로를 통해 탈출한 유저는 승리 그렇지 못한 유저는 패배가 된다.

 

위와 같은 게임 진행과정을 생각했을때, 개인적으로 2번 클라가 잘 어울린다고 생각을 해 2번을 추천했다.

높은 비율로 2번이 추천되어서 2번 클라를 선택했다.

 


ERD 그리기

 

 

User 테이블과 Match 테이블을 준비했다.

 

Match 테이블은 User가 게임 진행한 상황을 기록하는 테이블이다.

오늘의 목표

더보기

✔️ 최종 프로젝트 전 팀프로젝트 끝

✔️ 최종프로젝트 시작


⏱️ 오늘의 일정

최종 프로젝트 전 [팀프로젝트 끝]

최종 프로젝트 시작


 

📜 최종 프로젝트 전 [팀프로젝트 끝]

 

TCP로 타워 디펜스 온라인게임을 만드는 팀프로젝트가 끝났다.

 

앞서 포스팅한대로 원래는 9조 였으나, 팀원 2분의 건강 이슈로 인해 팀이 결국 폭파되어

늦게 2조에 합류해서 팀프로젝트를 진행했다.

 

아무래도 늦게 합류하다보니... 이미 코드 구현 담당자가 모두 배분되어 맡아야 하는 부분이 없던 상황이라.. ㅠㅠ

팀원들을 보조하는 느낌으로 진행했다. 그래도 내 이름으로 맡은 부분은 있어야 했기 때문에,

이전 개인 프로젝트에서 만들었던 더미 클라이언트( 스트레스 테스트 )를 이번 프로젝트에도 적용해보겠다고 팀에 말해서,

더미 클라이언트는 내가 담당해 만들었다. 

 

우리가 만든 게임은 위 그림처럼 2명의 유저가 방에 들어가서 오른쪽에 있는 성이 먼저 파괴되는 유저가 패배하는 게임이다. 개인 프로젝트와는 다르게 500명을 접속해 패킷을 전송해도 지연되는 상황은 딱히 발견되지 않았다.

아무래도 방안에 들어있는 2명에 대한 패킷을 받아 전달하기 때문에 패킷을 빠르게 처리할 수 있어서 지연 상황에 대한 문제는 발생하지 않은 것으로 추론했다.

 

트러블 슈팅은 총 3가지로 다음과 같다.

 

 

 

 

 

📜 최종 프로젝트 시작

 

팀프로젝트 발표가 끝난후, 내일배움캠프의 마지막 팀프로젝트인 최종프로젝트 주차에 진입했다.

이번 기수는 유니티클라 기수와 협업해서 진행할 수 있는 기수였는데, 아쉽게도 유니티클라 팀과의 협업은 하지 못하게 됐다.

아쉽지만.. 그래도 이번에 배정된 동료분들과 같이 우리가 원하는 재밌는 게임과 서버를 멋지게 만들어 봐야겠다.

 

캠프에서 제공해주는 클라 사용법을 내일 알려준다고 하는데, 어떤 클라를 제공해주는지 면밀하게 분석해봐야겠다.

오늘의 목표

더보기

✔️ 프로그래머스 코테 문제 풀이

✔️ 팀 프로젝트 진행


⏱️ 오늘의 일정

프로그래머스 코테 문제 풀이


📜 프로그래머스 코테 문제 풀이

 

H-Index

 

https://github.com/YamSaeng/AlgorithmCodingTest/tree/main/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4/2/42747.%E2%80%85H%EF%BC%8DIndex

 

AlgorithmCodingTest/프로그래머스/2/42747. H-Index at main · YamSaeng/AlgorithmCodingTest

This is an auto push repository for Baekjoon Online Judge created with [BaekjoonHub](https://github.com/BaekjoonHub/BaekjoonHub). - YamSaeng/AlgorithmCodingTest

github.com

 

 

📜 팀 프로젝트 진행

 

팀 프로젝트에서 맡은 파트인 게임 오버를 하려면 프로젝트가 어느정도 진행이 되야하는 상황이라서

팀원 분중 한분이 맡고 계셨던 걸 도와주는 식으로 우선 프로젝트를 진행했다.

추가로 맡은 부분은 위 그림처럼 회원가입 부분인데, 

export const registHandler = async ({ socket, payload }) => { 

  let registerResponsePayloadData = {};

  // 비밀번호 해시화
  let hashPassword = await bcrypt.hash(payload.password, 10);

  // 유저 검색
  let user = await DatabaseManager.GetInstance().findUser(payload.id, payload.email);

  if (user === undefined) {
    // 유저가 없다면 회원가입 진행
    DatabaseManager.GetInstance().createUser(payload.id, payload.email, hashPassword);

    registerResponsePayloadData.success = true;
    registerResponsePayloadData.message = '회원가입 성공';
    registerResponsePayloadData.failCode = 0;
  } else {
    // 이미 id값을 가진 유저가 DB에 저장되어 있음 ( 회원가입 실패 )
    registerResponsePayloadData.success = false;
    registerResponsePayloadData.message = '이미 존재하는 사용자입니다';
    registerResponsePayloadData.failCode = 1;
  }

  const registerResponsePacket = createResponse(
    PACKET_TYPE.REGISTER_RESPONSE,
    registerResponsePayloadData,
    socket.sequence,
  );
  
  socket.write(registerResponsePacket);
};


기본적으로 클라에서 보낸 아이디와 이메일이 DB에 있는지 확인하고, 회원 정보를 DB에 기록한다.

그 후 클라에게 적절한 응답 메세지 패킷을 보내주는걸로 구현을 완료했다.

 

 

오늘의 목표

더보기

✔️ 모의 면접 2차

✔️ 개인 프로젝트 진행


⏱️ 오늘의 일정

모의 면접 2차

개인 프로젝트 진행


📜 모의 면접 2차

 

오늘 모의 면접 2차가 있었다.

모의 면접에서 질문할 주제를 미리 제공받아 아래처럼 조사를 하고, 최대한 암기를 했다.

전송 계층 프로토콜에 대해 설명
 전송 계층 프로토콜에는 TCP와 UDP가 있습니다.
 TCP는 신뢰성과 순서를 보장하는 연결 프로토콜입니다.
 UDP는 빠른 전송을 중시하는 비연결 프로토콜입니다.

IP의 한계 
IP는 비연결형 통신(송수신 호스트 간에 사전 연결 수립 작업을 거치지 않는 특징)을 사용해 데이터를 전송합니다.
비연결형 통신은 몇 가지 한계를 나타내는데, 
신뢰성 부족 : 패킷 손실, 중복, 순서 변경 등이 발생할 수 있습니다.
패킷 순서 보장 없음 : 패킷이 서로 다른 경오를 통해 전송될 수 있어 수신 순서가 전송 순서와 다를 수 있습니다.

오류 제어, 흐름 제어 
오류제어와 흐름제어는 TCP의 주요 특징입니다.
오류제어는 데이터가 유실되거나 잘못된 데이터가 수신되었을 경우 대처하는 방법입니다.
오류 제어 기법으로는 체크섬, 재전송이 있습니다.
체크섬은 TCP 헤더와 데이터에 체크섬을 계산해 전송하고, 수신자는 체크섬을 사용해서
수신한 데이터의 무결성을 검증합니다.
재전송은 각 패킷에 대해 확인 응답을 기다립니다. 
일정 시간 내에 확인 응답을 받지 못하면 해당 패킷이 손실된 것으로 간주하고, 재전송을 요청합니다.

흐름제어는 송신자가 수신자의 처리 능력을 초과할 경우 데이터를 전송하지 않도록 조절하는 기능입니다.
TCP는 윈도우 크기를 사용해 흐름 제어를 수행합니다.
윈도우는 수신측에서 송신측에 수신 확인을 기다리지 않고 묶어서 송신할 수 있는 데이터의 양을 의미합니다.

대칭키, 비대칭키 암호화
대칭키 암호화는 동일한 키를 사용해 데이터를 암호화하고 복호화하는 방식입니다.
송신자와 수신자는 같은 키를 공유해야 하고, 이 키가 유출 되면 보안에 취약해지는 점이 있습니다.

비대칭키 암호화는 서로 다른 공개키와 개인키를 사용합니다. 공개키로 암호화된 데이터는 해당 개인키로만 복호화할 수 있습니다.

대칭키/비대칭키 혼합 사용 
대칭키와 비대칭키 암호화를 조합하여 사용하는 방식
주로 비대칭키로 대칭키를 안전하게 전송한 뒤, 이후의 데이터 전송은 대칭키로 암호화합니다.
TLS

HTTPS 
웹에서 데이터를 안전하게 전송하기 위한 프로토콜입니다.
앞서 언급한 대킹키/비대칭키 혼합 사용 방식인 TLS 방식을 사용합니다.

로드밸런싱
서버에 대한 요청을 여러 대의 서버에 분산시키는 기술을 말합니다.

로드밸런싱 알고리즘 
라운드 로빈 (Round Robin): 요청을 순서대로 각 서버에 분배합니다.
최소 연결 (Least Connections): 현재 연결 수가 가장 적은 서버에 요청을 보냅니다.
가중치 기반 (Weighted): 서버의 성능에 따라 가중치를 부여하고 요청을 분배합니다.

헬스체크
정의: 로드밸런서가 백엔드 서버의 상태를 모니터링하는 기능입니다. 
서버가 정상적으로 작동하고 있는지 확인하여, 문제가 있는 서버에 요청을 전송하지 않도록 합니다.
작동 방식: 정기적으로 서버에 요청을 보내 응답을 체크하며
, 응답이 없거나 비정상적인 경우 해당 서버를 로드밸런싱 풀에서 제외합니다.

 

저번 1차 모의 면접에서 피드백 받은 점을 최대한 지키려고 노력했다.

1차 모의 면접에서 질문을 받으면 해당 주제에 대해 장황하게 설명한다는 피드백을 받았다.

장황하게 설명하다보니 대화를 하는 느낌이 아니라 사전 그대로 읽는 것 같다는 이유였다.

 

이번 2차 모의 면접에서는 앞서 언급한 피드백 내용을 지키려고 최대한 단답식으로 각 주제를 끊으면서 대화를 하려고 했다.

확실히 단답으로 대답을 하니 면접관 분께서도 질문이 추가로 들어오고 말 그대로 대화하는 느낌을 받았다.

 

이번 2차 모의 면접 피드백은 다행히도 1차 모의 면접보다는 훨씬 나아져 전체적으로 매우 좋았다는 피드백을 받았다.

다만, 캠을 조정을 해서 최대한 얼굴이 보이게끔 하고, 아이컨택을 해달라는 조언을 들었다.

 

📜 개인 프로젝트 진행

 

개인 프로젝트를 진행했다.

 

아래 그림이 캠프에서 제공한 필수 기능 목록인데, 오늘 필수 기능을 다 구현할 수 있었다.

 

 

내일까지 도전 기능을 추가하고, 내가 원하던 기능인 DummyClient를 생성해서 다중 접속을 해 주고 받는패킷의 갯수를 세보고, 어느정도 까지 버텨볼 수 있는지 확인해봐야겠다.

 

async function DummyClientCreate(count: number) {
    for (let i = 0; i < count; i++) {
        const dummyClient = new Client();
        DummyClients.push(dummyClient);

        dummyClient.Connect();
        await delay(100);
    }
}

DummyClientCreate(100);

 

지금 Client 소스에는 위 코드가 들어가 있긴하다.

처음에는 그냥 바로 전부 접속 시도를 했는데, 서버에서 한번에 다 받지를 못하는 경우가 생겨서

0.1초마다 접속을 시도하게 변경하니 깔끔하게 다 접속을 했다.

오늘의 목표

더보기

✔️ 프로그래머스 코테 문제 풀기

✔️ TCP 서버 강의 듣기


⏱️ 오늘의 일정

프로그래머스 코테 문제 풀기

TCP 서버 강의 듣기

 

📜 프로그래머스 코테 문제 풀기

 

 

귤 종류 세기

function solution(k, tangerine) {
    let answer = 0;
    let kind = new Array(100001).fill(0);

    // 귤 종류 개수 세기
    for (let i = 0; i < tangerine.length; i++) {
        kind[tangerine[i]]++;
    }

    // 종류가 많은 귤을 제일 앞으로 옮겨주기 위해 내림차순으로 정렬
    kind.sort((a, b) => b - a);

    for (let j = 0; j < kind.length; j++) {
        if (kind[j] < k) {
            answer++; // 귤 종류의 개수
            k -= kind[j]; 
        }
        else if (kind[j] >= k) {
            answer++;
            break;
        }        
    }

    return answer;
}

 

📜 TCP 서버 강의 듣기

 

개인 과제 수행을 하기 위해 캠프에서 제공받은 Node.js를 이용해 만드는 TCP 서버 강의를 듣고 있다.

구조가 복잡하고, 폴더에 파일을 너무 나뉘어져 있어서 한눈에 알아보기가 힘들긴 하지만..

강의를 내일까지는 다 듣고, 개인 과제를 진행 할 수 있도록 해야겠다.

 

2024.10.24 - [IT] - [IT] 레이턴시 ( Latency )

 

[IT] 레이턴시 ( Latency )

레이턴시 ( Latency )한 지점에서 다른 지점으로 이동하는 데 걸리는 시간 라운드 트립 레이턴시 ( Round Trip Latency )데이터 패킷이 송신지에서 수신지로 이동하고, 다시 수신지에서 송신지로 돌아오

program-yam.tistory.com

 

흥미로운 주제인 레이턴시에 대해 강의에서 배웠다.

서버에서 보정을 해 클라들에게 동일한 환경을 제공해주는 기법이라고 이해를 했다.

구글링을 해서 좀 더 내용을 깊게 알아봐야겠다. 

오늘의 목표

더보기

✔️ 개인프로젝트 발제


⏱️ 오늘의 일정

개인 프로젝트 발제

 

 

📜 개인 프로젝트 발제

 

이번주 부터 다시 팀을 구성하고, 새로운 프로젝트를 시작한다.

앞서 여러번 진행했던 것처럼 개인프로젝트를 우선 진행하고, 개인프로젝트가 끝나면 팀 프로젝트가 진행된다.

이번 기한은 11월 1일까지로, 총 2주간의 시간이 주어졌다.

 

앞서 개인프로젝트들은 1주의 시간을 가졌는데, 아무래도 내일배움캠프가 막바지에 다다르다 보니,

정리할 시간도 같이 주는 것 같다. 시간을 잘 분배해서 지금까지 배운 내용들을 한번 더 정리해보고,

개인과제에도 집중할 수 있는 시간을 가져야 겠다.

 

 

제공받은 유니티 클라

 

 

이번 개인과제는 본격적인 TCP 과제로,

서버는 TCP로 구성하고 클라는 캠프에서 제공해주는 유니티 클라이언트를  사용해 멀티 플레이어 게임을 만드는것이 주요 발제 내용이다.

 

서버 디렉토리 구조

.
├── assets
│   ├── item.json
│   ├── item_unlock.json
│   └── stage.json
├── clients
├── package-lock.json
├── package.json
├── readme.md
└── src
    ├── classes            // 인스턴스 class 들을 정의
    │   ├── managers
    │   └── models
    ├── config             // 환경변수, DB 설정등을 선언
    ├── constants          // 상수 관리
    ├── db                 // db 로직 관리
    │   ├── game
    │   ├── migrations
    │   ├── seeders
    │   ├── sql
    │   └── user
    ├── events             // socket 이벤트
    ├── handlers           // 핸들러 관리
    │   ├── game
    │   └── user
    ├── init               // 서버 초기화
    ├── protobuf           // 패킷 구조
    │   ├── notification
    │   ├── request
    │   └── response
    ├── session             // 세션 관리
    └── utils               // 그 외 필요한 함수들 선언
        ├── db
        ├── error
        ├── notification
        ├── parser
        └── response

 

 

캠프에서 제공해준 서버 강의에서 흥미롭게 본 부분이 있는데, 위치 동기화와 추측 항법에 관한 파트다.

위치 동기화는 기존에 c++로 만든 서버에서 나름, 구현을 해봤는데

내가 구현한 위치 동기화와 튜터님이 만든 위치 동기화를 비교해보고, 어떤 부분이 다를지 기대가 매우 된다.

 

오늘의 목표

더보기

✔️ 프로그래머스 코테 문제 풀기

✔️ 팀 프로젝트 완성 및 발표


⏱️ 오늘의 일정

프로그래머스 코테 문제 풀기

팀 프로젝트 완성 및 발표


📜 프로그래머스 코테 문제 풀기

 

달리기 경주

 

https://github.com/YamSaeng/AlgorithmCodingTest/tree/main/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4/1/178871.%E2%80%85%EB%8B%AC%EB%A6%AC%EA%B8%B0%E2%80%85%EA%B2%BD%EC%A3%BC

 

AlgorithmCodingTest/프로그래머스/1/178871. 달리기 경주 at main · YamSaeng/AlgorithmCodingTest

This is an auto push repository for Baekjoon Online Judge created with [BaekjoonHub](https://github.com/BaekjoonHub/BaekjoonHub). - YamSaeng/AlgorithmCodingTest

github.com

 

문제에서 설명해준 대로 알고리즘을 구성하니 풀 수 있었다.

 

function solution(players, callings) {
    let answer = {};

    // player들의 이름을 key값으로, index를 value로 해서 담아둠
    for (let i = 0; i < players.length; i++) {
        answer[players[i]] = i;
    }

    for (let i = 0; i < callings.length; i++) {
        const index = answer[callings[i]];
        const temp = players[index - 1];

        // 해당 index와 이전 index의 value 값을 바꿈
        players[index - 1] = callings[i];
        players[index] = temp;

        // answer의 index도 갱신
        answer[callings[i]] = index - 1;
        answer[temp] = index;
    }

    return players;
}

 

Javascript 객체를 이용해 풀었다.

 

📜 팀 프로젝트 완성 및 발표

 

오늘 팀 프로젝트를 완성하고 제출했다.

 

오후 2시부터 발표회가 있어서 줌으로 참가했다.

하루 직전까지 버그가 나서 애태우며 원인을 찾고 문제를 해결했다.

 

이번 팀 프로젝트인 타워 디펜스 Git

https://github.com/YamSaeng/TowerDefence

 

GitHub - YamSaeng/TowerDefence

Contribute to YamSaeng/TowerDefence development by creating an account on GitHub.

github.com

 

import { handlerEvent } from "../clientHandler/handler.js";

class UserSocket {
  static gInstance = null;
  // 스테이지 정보들 저장
  constructor() {
    this.socket = null;
  }
  static getInstance() {
    if (!this.gInstance) {
      this.gInstance = new UserSocket();
    }
    return this.gInstance;
  }
  Connect() {
    let somewhere;
    this.socket = io(`http://localhost:3000`, {
      auth: {
        token: somewhere, // 토큰이 저장된 어딘가에서 가져와야 합니다!
      },
    });
    // 이벤트 핸들러
    this.socket.on("event", (data) => handlerEvent(data));
    // 응답 패킷 이벤트 할당
    this.socket.on("response", (data) => {
      console.log(data);
    });
  }
  // 서버에 패킷 전송
  SendEvent(handlerId, payload) {
    this.socket.emit("event", {
      handlerId,
      payload,
      accessToken: localStorage.getItem("authorization"),
    });
  }
}
export default UserSocket;

 

내가 소스를 소개한 부분은 싱글톤 부분이였는데,

자바스크립트에서 싱글톤을 구성하다보니 생성자를 막을 수 없었다..

이 점을 피드백을 받긴했는데.. c++에서는 생성자를 private: 으로 해주면 되는데,

자바스크립트에서는 어떻게 막아야하는지 구글링 해도 나오지를 않아 골치가 아프다 ㅠㅠ

 

회고

이번 팀 프로젝트는 저번 프로젝트와는 다른 점이 있었다.

저번 프로젝트는 express로 진행한 프로젝트였는데, 프로젝트를 진행함에 있어 각각 router를 맡았기 때문에

역할 분배가 매우 쉬웠다.

 

하지만 이번에는 정말 같이 작업하는 부분이 많았는데, 그런만큼 사람들의 생각을 조율하는 과정이 힘들었다.

특히, 팀원 모두가 같은 주제를 가지고 회의를 하고, 같은 의견으로 결론이나더라도 문서화 하지 않으면

결국에는 서로 다른 생각을 가지게 된다는 점을 아주 깊이 깨달았다.

 

정말 자세하게 a 부터 z 까지 문서화 시켜 서로가 이해한 점이 같은지 확인하고,

나중에 두번 세번 확인을 위해 문서화를 꼭 해야겠다.

사실 저번 프로젝트에서는 git 작업을 함에 있어서, 앞에서 언급한것과 같이 각각 맡은 파트가 분명하기 때문에

충돌하는 부분도 거의 없었다. 그래서 git pull을 배웠지만, 거의 pull을 날리면 그냥 받는 식이라 연습이 되지는 않았다.

 

이번에는 충돌도 나고, 충돌나는 거를 git 홈페이지에서 고치면서 서로 의견을 주고 받는 과정이 매우 좋았다.

 

 

오늘의 목표

더보기

✔️ 팀 프로젝트

 


⏱️ 오늘의 일정

팀프로젝트


📜 팀프로젝트

 

길고 길었던 팀프로젝트가 하루 남았다.

발표일은 이번주 수요일로, 화요일에 최종적으로 프로젝트를 리뷰하고, 발표 자료를 준비할 예정이다.

 

오늘은 로그아웃 기능을 추가했다.

로그아웃 기능을 추가하기 위해 유저 테이블에 isLogin이라는 변수를 추가해줬다.

isLogin은 bool 변수 값으로, 뜻처럼 로그인 여부를 나타낸다.

usersRouter.post("/SignOut", async (req, res, next) => {
  const { email } = req.body;  

  const LogOutReqUser = await prismaUser.user.findFirst({
    where: {
      email: email,
    },
  });

  if (!LogOutReqUser) {
    return res
      .status(404)
      .json({ message: `${email}은 존재하지 않는 이메일입니다.` });
  }

  await prismaUser.user.update({
    data: {
      isLogin: false
    },
    where: {
      id: LogOutReqUser.id
    }
  })
});

 

유저가 로그아웃 버튼을 클릭하면 SignOut로 들어와서 로그아웃 기능을 수행한다.

 

 

게임 플레이 버튼에도 로그인 확인 기능을 추가했다.

// 플레이 버튼 클릭
    document.getElementById("playButton").addEventListener("click", async () => {
      const accessToken = localStorage.getItem("authorization");
      if (accessToken !== null) {
        const response = await fetch("/TowerDefence/GamePlay", {
          method: "post",
          headers: {
            "Content-Type": "application/json",
            authorization: accessToken
          }
        });

        const s2cGamePlayResponse = await response.json();
        if (s2cGamePlayResponse.status === 201) {
          document.querySelector(".button-container").style.display = "none";
          document.getElementById("gameCanvas").style.display = "block";

          import("./src/game.js");
        }
        else if (s2cGamePlayResponse.status === 401) {
          window.location.href = "/index.html";

          alert(s2cGamePlayResponse.message);
        }

      }
      else {
        alert("로그인 하고 게임 플레이를 누르세요");
      }
    });

 

GamePlay 주소로 가서 게임플레이 요청한 대상이 로그인 중인지 확인하고, 로그인 중인 경우에만 게임 화면으로 이동하도록 수정했다.

오늘의 목표

더보기

✔️ 프로그래머스 코테 문제 풀이

✔️ 팀프로젝트 진행


⏱️ 오늘의 일정

프로그래머스 코테 문제 풀이팀프로젝트 진행


 

📜 프로그래머스 코테 문제 풀이

 

최댓값과 최솟값

 

https://github.com/YamSaeng/AlgorithmCodingTest/tree/main/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4/2/12939.%E2%80%85%EC%B5%9C%EB%8C%93%EA%B0%92%EA%B3%BC%E2%80%85%EC%B5%9C%EC%86%9F%EA%B0%92

 

AlgorithmCodingTest/프로그래머스/2/12939. 최댓값과 최솟값 at main · YamSaeng/AlgorithmCodingTest

This is an auto push repository for Baekjoon Online Judge created with [BaekjoonHub](https://github.com/BaekjoonHub/BaekjoonHub). - YamSaeng/AlgorithmCodingTest

github.com

 

문제 설명대로 쉽게 풀 수 있었다.

function solution(s) {
    var answer = [];

    let splitArray = s.split(" ").map(x => +x);
    let max = -999999;
    let min = 999999;

    for (let i = 0; i < splitArray.length; i++) {
        if (splitArray[i] >= max) {
            max = splitArray[i];
        }

        if (splitArray[i] <= min) {
            min = splitArray[i];
        }
    }

    answer.push(min);
    answer.push(max);

    return `${answer[0]} ${answer[1]}`;
}

 

 

📜 팀 프로젝트 진행

 

팀 프로젝트를 진행했다.

 

내가 맡은 회원가입과 로그인을 구현했다.

 

회원가입

usersRouter.post('/SignUp', async (req,res,next)=>{
    const { email, password, passwordCheck } = req.body;
    console.log(email, password);
    const isExistUser = await prismaUser.user.findFirst({
        where:{
            email : email
        }
    });    

    if(email.length === 0)
    {
        return res.status(404).json({ message: "이메일을 입력해주세요" });
    }

    if(password.length === 0)
    {
        return res.status(404).json({message:"비밀번호를 입력해주세요"});
    }

    if(isExistUser)
    {
        return res.status(404).json({message:"이미 존재하는 이메일입니다."});
    }

    const hashedPassword = await bcrypt.hash(password, 10);

    const newUser = await prismaUser.user.create({
        data:{            
            password: hashedPassword,       
            email : email,
            highScore : 0
        }                
    });

    return res
        .status(201)
        .json({ status: 201, message: `${email}로 회원가입이 완료되었습니다.` });
})

 

이메일 형식으로 아이디를 받고,

비밀번호를 입력받아 유저를 생성해 회원가입을 완료시켜준다.

 

이메일 검증하는 부분이 현재는 없긴한데, 정규표현식을 이용해 이메일 포멧 적용할 예정.

 

로그인

usersRouter.post('/SignIn', async (req, res, next) => {
    const { email, password } = req.body;

    if (email.length === 0) {
        return res.status(404).json({ message: "이메일을 입력해주세요" });
    }

    if(password.length ===0){
        return res.status(404).json({message:"비밀번호를 입력해주세요"});
    }

    const LoginReqUser = await prismaUser.user.findFirst({
        where: {
            email: email
        }
    });
    
    if(!LoginReqUser)
    {
        return res.status(404).json({ message:`${email}은 존재하지 않는 이메일입니다.`});
    }

    if(!(await bcrypt.compare(password, LoginReqUser.password))){
        return res.status(404).json({message:`비밀번호가 일치하지 않습니다.`});
    }

    const s2cAccessToken = CreateAccessToken(email);
    res.header("authorization", s2cAccessToken);

    return res
        .status(201)
        .json({ status: 201, message: `${email}로 로그인 성공` });
})

 

이메일과 비밀번호를 검증하고 성공하면 AccessToken을 발급해 header에 기록한다.

 

 

회원가입과 로그인을 구현하고 나서, game.js에 있는 코드를 클래스화 시켰다.

 

game.js

import { Base } from "./base.js";
import { Monster } from "./monster.js";
import { Tower } from "./tower.js";
/* 
  어딘가에 엑세스 토큰이 저장이 안되어 있다면 로그인을 유도하는 코드를 여기에 추가해주세요!
*/
let serverSocket; // 서버 웹소켓 객체
const canvas = document.getElementById("gameCanvas");
const ctx = canvas.getContext("2d");
const NUM_OF_MONSTERS = 5; // 몬스터 개수
let userGold = 0; // 유저 골드
let base; // 기지 객체
let baseHp = 0; // 기지 체력
let towerCost = 0; // 타워 구입 비용
let numOfInitialTowers = 0; // 초기 타워 개수
let monsterLevel = 0; // 몬스터 레벨
let monsterSpawnInterval = 0; // 몬스터 생성 주기
const monsters = [];
const towers = [];
let score = 0; // 게임 점수
let highScore = 0; // 기존 최고 점수
let isInitGame = false;
// 이미지 로딩 파트
const backgroundImage = new Image();
backgroundImage.src = "images/bg.webp";
const towerImage = new Image();
towerImage.src = "images/tower.png";
const baseImage = new Image();
baseImage.src = "images/base.png";
const pathImage = new Image();
pathImage.src = "images/path.png";
const monsterImages = [];
for (let i = 1; i <= NUM_OF_MONSTERS; i++) {
  const img = new Image();
  img.src = `images/monster${i}.png`;
  monsterImages.push(img);
}
let monsterPath;
function generateRandomMonsterPath() {
  const path = [];
  let currentX = 0;
  let currentY = Math.floor(Math.random() * 21) + 500; // 500 ~ 520 범위의 y 시작 (캔버스 y축 중간쯤에서 시작할 수 있도록 유도)
  path.push({ x: currentX, y: currentY });
  while (currentX < canvas.width) {
    currentX += Math.floor(Math.random() * 100) + 50; // 50 ~ 150 범위의 x 증가
    // x 좌표에 대한 clamp 처리
    if (currentX > canvas.width) {
      currentX = canvas.width;
    }
    currentY += Math.floor(Math.random() * 200) - 100; // -100 ~ 100 범위의 y 변경
    // y 좌표에 대한 clamp 처리
    if (currentY < 0) {
      currentY = 0;
    }
    if (currentY > canvas.height) {
      currentY = canvas.height;
    }
    path.push({ x: currentX, y: currentY });
  }
  return path;
}
function initMap() {
  ctx.drawImage(backgroundImage, 0, 0, canvas.width, canvas.height); // 배경 이미지 그리기
  drawPath();
}
function drawPath() {
  const segmentLength = 20; // 몬스터 경로 세그먼트 길이
  const imageWidth = 60; // 몬스터 경로 이미지 너비
  const imageHeight = 60; // 몬스터 경로 이미지 높이
  const gap = 5; // 몬스터 경로 이미지 겹침 방지를 위한 간격
  for (let i = 0; i < monsterPath.length - 1; i++) {
    const startX = monsterPath[i].x;
    const startY = monsterPath[i].y;
    const endX = monsterPath[i + 1].x;
    const endY = monsterPath[i + 1].y;
    const deltaX = endX - startX;
    const deltaY = endY - startY;
    const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); // 피타고라스 정리로 두 점 사이의 거리를 구함 (유클리드 거리)
    const angle = Math.atan2(deltaY, deltaX); // 두 점 사이의 각도는 tan-1(y/x)로 구해야 함 (자세한 것은 역삼각함수 참고): 삼각함수는 변의 비율! 역삼각함수는 각도를 구하는 것!
    for (let j = gap; j < distance - gap; j += segmentLength) {
      // 사실 이거는 삼각함수에 대한 기본적인 이해도가 있으면 충분히 이해하실 수 있습니다.
      // 자세한 것은 https://thirdspacelearning.com/gcse-maths/geometry-and-measure/sin-cos-tan-graphs/ 참고 부탁해요!
      const x = startX + Math.cos(angle) * j; // 다음 이미지 x좌표 계산(각도의 코사인 값은 x축 방향의 단위 벡터 * j를 곱하여 경로를 따라 이동한 x축 좌표를 구함)
      const y = startY + Math.sin(angle) * j; // 다음 이미지 y좌표 계산(각도의 사인 값은 y축 방향의 단위 벡터 * j를 곱하여 경로를 따라 이동한 y축 좌표를 구함)
      drawRotatedImage(pathImage, x, y, imageWidth, imageHeight, angle);
    }
  }
}
function drawRotatedImage(image, x, y, width, height, angle) {
  ctx.save();
  ctx.translate(x + width / 2, y + height / 2);
  ctx.rotate(angle);
  ctx.drawImage(image, -width / 2, -height / 2, width, height);
  ctx.restore();
}
function getRandomPositionNearPath(maxDistance) {
  // 타워 배치를 위한 몬스터가 지나가는 경로 상에서 maxDistance 범위 내에서 랜덤한 위치를 반환하는 함수!
  const segmentIndex = Math.floor(Math.random() * (monsterPath.length - 1));
  const startX = monsterPath[segmentIndex].x;
  const startY = monsterPath[segmentIndex].y;
  const endX = monsterPath[segmentIndex + 1].x;
  const endY = monsterPath[segmentIndex + 1].y;
  const t = Math.random();
  const posX = startX + t * (endX - startX);
  const posY = startY + t * (endY - startY);
  const offsetX = (Math.random() - 0.5) * 2 * maxDistance;
  const offsetY = (Math.random() - 0.5) * 2 * maxDistance;
  return {
    x: posX + offsetX,
    y: posY + offsetY,
  };
}
function placeInitialTowers() {
  /* 
    타워를 초기에 배치하는 함수입니다.
    무언가 빠진 코드가 있는 것 같지 않나요? 
  */
  for (let i = 0; i < numOfInitialTowers; i++) {
    const { x, y } = getRandomPositionNearPath(200);
    const tower = new Tower(x, y, towerCost);
    towers.push(tower);
    tower.draw(ctx, towerImage);
  }
}
function placeNewTower() {
  /* 
    타워를 구입할 수 있는 자원이 있을 때 타워 구입 후 랜덤 배치하면 됩니다.
    빠진 코드들을 채워넣어주세요! 
  */
  const { x, y } = getRandomPositionNearPath(200);
  const tower = new Tower(x, y);
  towers.push(tower);
  tower.draw(ctx, towerImage);
}
function placeBase() {
  const lastPoint = monsterPath[monsterPath.length - 1];
  base = new Base(lastPoint.x, lastPoint.y, baseHp);
  base.draw(ctx, baseImage);
}
function spawnMonster() {
  monsters.push(new Monster(monsterPath, monsterImages, monsterLevel));
}
function gameLoop() {
  // 렌더링 시에는 항상 배경 이미지부터 그려야 합니다! 그래야 다른 이미지들이 배경 이미지 위에 그려져요!
  ctx.drawImage(backgroundImage, 0, 0, canvas.width, canvas.height); // 배경 이미지 다시 그리기
  drawPath(monsterPath); // 경로 다시 그리기
  ctx.font = "25px Times New Roman";
  ctx.fillStyle = "skyblue";
  ctx.fillText(`최고 기록: ${highScore}`, 100, 50); // 최고 기록 표시
  ctx.fillStyle = "white";
  ctx.fillText(`점수: ${score}`, 100, 100); // 현재 스코어 표시
  ctx.fillStyle = "yellow";
  ctx.fillText(`골드: ${userGold}`, 100, 150); // 골드 표시
  ctx.fillStyle = "black";
  ctx.fillText(`현재 레벨: ${monsterLevel}`, 100, 200); // 최고 기록 표시
  // 타워 그리기 및 몬스터 공격 처리
  towers.forEach((tower) => {
    tower.draw(ctx, towerImage);
    tower.updateCooldown();
    monsters.forEach((monster) => {
      const distance = Math.sqrt(
        Math.pow(tower.x - monster.x, 2) + Math.pow(tower.y - monster.y, 2)
      );
      if (distance < tower.range) {
        tower.attack(monster);
      }
    });
  });
  // 몬스터가 공격을 했을 수 있으므로 기지 다시 그리기
  base.draw(ctx, baseImage);
  for (let i = monsters.length - 1; i >= 0; i--) {
    const monster = monsters[i];
    if (monster.hp > 0) {
      const isDestroyed = monster.move(base);
      if (isDestroyed) {
        /* 게임 오버 */
        alert("게임 오버. 스파르타 본부를 지키지 못했다...ㅠㅠ");
        location.reload();
      }
      monster.draw(ctx);
    } else {
      /* 몬스터가 죽었을 때 */
      monsters.splice(i, 1);
    }
  }
  requestAnimationFrame(gameLoop); // 지속적으로 다음 프레임에 gameLoop 함수 호출할 수 있도록 함
}
function initGame() {
  if (isInitGame) {
    return;
  }
  monsterPath = generateRandomMonsterPath(); // 몬스터 경로 생성
  initMap(); // 맵 초기화 (배경, 몬스터 경로 그리기)
  placeInitialTowers(); // 설정된 초기 타워 개수만큼 사전에 타워 배치
  placeBase(); // 기지 배치
  setInterval(spawnMonster, monsterSpawnInterval); // 설정된 몬스터 생성 주기마다 몬스터 생성
  gameLoop(); // 게임 루프 최초 실행
  isInitGame = true;
}
// 이미지 로딩 완료 후 서버와 연결하고 게임 초기화
Promise.all([
  new Promise((resolve) => (backgroundImage.onload = resolve)),
  new Promise((resolve) => (towerImage.onload = resolve)),
  new Promise((resolve) => (baseImage.onload = resolve)),
  new Promise((resolve) => (pathImage.onload = resolve)),
  ...monsterImages.map(
    (img) => new Promise((resolve) => (img.onload = resolve))
  ),
]).then(() => {
  /* 서버 접속 코드 (여기도 완성해주세요!) */
  let somewhere;
  serverSocket = io("http://localhost:3020", {
    auth: {
      token: somewhere, // 토큰이 저장된 어딘가에서 가져와야 합니다!
    },
  });
  /* 
    서버의 이벤트들을 받는 코드들은 여기다가 쭉 작성해주시면 됩니다! 
    e.g. serverSocket.on("...", () => {...});
    이 때, 상태 동기화 이벤트의 경우에 아래의 코드를 마지막에 넣어주세요! 최초의 상태 동기화 이후에 게임을 초기화해야 하기 때문입니다! 
    if (!isInitGame) {
      initGame();
    }
  */
});
const buyTowerButton = document.createElement("button");
buyTowerButton.textContent = "타워 구입";
buyTowerButton.style.position = "absolute";
buyTowerButton.style.top = "10px";
buyTowerButton.style.right = "10px";
buyTowerButton.style.padding = "10px 20px";
buyTowerButton.style.fontSize = "16px";
buyTowerButton.style.cursor = "pointer";
buyTowerButton.addEventListener("click", placeNewTower);
document.body.appendChild(buyTowerButton);

 

위 코드를 Game Class로 변환했다.

import UserSocket from "../Network/userSocket.js";
import { Tower } from "../tower.js";
import pathManager from "../path.js";
import { Monster } from "../monster.js";
import { Inhibitor } from "../base.js";
import Player from "../player.js";
import { getLocalStorage } from "../Local/localStorage.js";

class Game {
  constructor() {
    this.canvas = document.getElementById("gameCanvas");
    this.ctx = this.canvas.getContext("2d");

    this.player = new Player(this.ctx, 60, 60); // 플레이어
    this.userGold = 0; // 유저 돈
    this.inhibitor = null; // 억제기
    this.inhibitorHp = 0; // 억제기 체력

    this.towerCost = 0; // 타워 구입시 가격
    this.numOfInitialTowers = 0; // 게임 시작시 타워 자동 생성 -> 없어도 됨
    this.monsterLevel = 1; // 몬스터 레벨
    this.monsterSpawnInterval = 1000; // 몬스터 스폰시간

    this.monsters = []; // 몬스터 저장 배열
    this.towers = []; // 타워 저장 배열

    this.score = 0; // 현재 플레이어의 스코어
    this.highScore = 0; // 현재 서버 최고 스코어

    this.backgroundImage = null; // 배경 이미지
    this.towerImage = null; // 타워 이미지
    this.inhibitorImage = null; // 억제기 이미지
    this.pathImage = null; // 경로 이미지
    this.monsterImages = []; // 몬스터 이미지

    this.NUM_OF_MONSTERS = 5;

    this.monsterPath = null; // 몬스터가 지나가는 경로

    this.path = null; // 경로
    //this.monsterPath = null; => 중첩된 거 같아서 주석처리 했습니다.

    this.stageChange = true;
    this.startTime = 0; // 게임 시작 시간
    this.elpsedTime = 0; // 게임 종료 시간

    this.buyTowerButton = document.createElement("button");
    this.buyTowerButton.textContent = "타워 구입";
    this.buyTowerButton.style.position = "absolute";
    this.buyTowerButton.style.top = "10px";
    this.buyTowerButton.style.right = "10px";
    this.buyTowerButton.style.padding = "10px 20px";
    this.buyTowerButton.style.fontSize = "16px";
    this.buyTowerButton.style.cursor = "pointer";

    this.buyTowerButton.addEventListener("click", () => {
      const newTower = new Tower(this.player.x, this.player.y);
      this.towers.push(newTower);
    });

    document.body.appendChild(this.buyTowerButton);
  }

  InitMap() {
    this.ctx.drawImage(
      this.backgroundImage,
      0,
      0,
      this.canvas.width,
      this.canvas.height
    );
    this.path.drawPath(this.monsterPath);
  }

  InitGame() {
    this.monsterPath = this.path.generateRandomMonsterPath();
    this.InitMap();
    this.PlaceInitialTowers();
    this.placeinhibitor();
  }

  PlaceInitialTowers() {
    for (let i = 0; i < this.numOfInitialTowers; i++) {
      const { x, y } = this.path.getRandomPositionNearPath(
        200,
        this.monsterPath
      );
      const tower = new Tower(x, y, this.towerCost);
      this.towers.push(tower);
      tower.draw(this.ctx, this.towerImage);
    }
  }

  placeinhibitor() {
    const lastPoint = this.monsterPath[this.monsterPath.length - 1];
    this.inhibitor = new Inhibitor(lastPoint.x, lastPoint.y, this.inhibitorHp);
    this.inhibitor.draw(this.ctx, this.inhibitorImage);
  }

  SpawnMonster() {
    this.monsters.push(
      new Monster(this.monsterPath, this.monsterImages, this.monsterLevel)
    );
  }

  // 불러오는 정보가 추가되다보니 infos로 변경했습니다.
  GameStart(infos) {
    this.backgroundImage = infos.backgroundImage;
    this.towerImage = infos.towerImage;
    this.inhibitorImage = infos.inhibitorImage;
    this.pathImage = infos.pathImage;

    this.userGold = infos.userGold;
    this.inhibitorHp = infos.inhibitorHp;
    this.highScore = infos.highScore;

    this.startTime = infos.startTime;
    this.elpsedTime = infos.elpsedTime;

    this.monsterImages = infos.monsterImages;

    this.path = new pathManager(this.canvas, this.ctx, this.pathImage, 60, 60);

    UserSocket.GetInstance().SendEvent(1, {});
    this.InitGame();
  }

  async GameLoop() {
    this.ctx.drawImage(
      this.backgroundImage,
      0,
      0,
      this.canvas.width,
      this.canvas.height
    );
    this.path.drawPath(this.monsterPath);
    this.player.draw();

    this.elpsedTime++;

    // 현재 id가 마지막 스테이지일 때 스테이지 변경 금지
    if (
      getLocalStorage("currentStage").id ===
      getLocalStorage("stages")[getLocalStorage("stages").length - 1].id
    ) {
      this.stageChange = false;
    }

    // 일정 시간이 지날 경우 스테이집 ㅕㄴ경
    if ((this.elpsedTime - this.startTime) % 500 === 0 && this.stageChange) {
      UserSocket.GetInstance().SendEvent(2, {
        currentStage: getLocalStorage("currentStage"),
      });
    }

    this.ctx.font = "25px Times New Roman";
    this.ctx.fillStyle = "skyblue";
    this.ctx.fillText(`최고 기록: ${this.highScore}`, 100, 50); // 최고 기록 표시
    this.ctx.fillStyle = "white";
    this.ctx.fillText(`점수: ${this.score}`, 100, 100); // 현재 스코어 표시
    this.ctx.fillStyle = "yellow";
    this.ctx.fillText(`골드: ${this.userGold}`, 100, 150); // 골드 표시

    this.ctx.fillStyle = "red";
    this.ctx.fillText(
      `현재 스테이지: ${getLocalStorage("currentStage").id}`,
      100,
      200
    ); // 현재 스테이지 표시

    this.towers.forEach((tower) => {
      tower.draw(this.ctx, this.towerImage);
      tower.updateCooldown();
      tower.singleAttack(tower, this.monsters); // 단일 공격
      tower.multiAttack(tower, this.monsters); // 다중 공격
      tower.heal(tower, this.inhibitor); // 힐
    });

    this.inhibitor.draw(this.ctx, this.inhibitorImage);

    for (let i = this.monsters.length - 1; i >= 0; i--) {
      const monster = this.monsters[i];
      if (monster.hp > 0) {
        const isDestroyed = monster.move(this.inhibitor);
        if (isDestroyed) {
          /* 게임 오버 */
          alert("게임 오버. 스파르타 본부를 지키지 못했다...ㅠㅠ");
          location.reload();
        }
        monster.draw(this.ctx);
      } else {
        /* 몬스터가 죽었을 때 */
        this.monsters.splice(i, 1);
      }
    }

    requestAnimationFrame(() => {
      this.GameLoop();
    });
  }
}

export default Game;

 

 


 

prisma에 관한 이슈가 있었는데, 다행히도 해결했다.

이슈는 다음과 같다.

 

팀 프로젝트에서 prisma 스키마를 총 2가지 사용 한다.

 

user 테이블

generator client {
  provider = "prisma-client-js"  
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL_USER")
}

model user {
  id Int @id @default(autoincrement()) @map("id")
  highScore Int @map("highScore")
  password String @map("password")
  email String @map("email")

  @@map("User")
}

 

Asset 테이블

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL_ASSET")
}

enum attackType {
  singleAttack
  multiAttack
  heal
}


model initGame {
  id Int @id @default(autoincrement()) @map("Id")
  baseHp Int @map("baseHp")
  startGold Int @map("startGold")
  serverHighScore Int @map("serverHighScore")

  @@map("InitGame")
}

model monster {
  id Int @id @default(autoincrement()) @map("Id")
  hp Int @map("hp")
  atk Int @map("atk")
  speed Int @map("speed")
  score Int @map("score")
  gold Int @map("stargoldtGold")
  stage Int @map("stage")
  grade Int @map("servgradeerHighScore")

  @@map("Monster")
}

model tower {
  id Int @id @default(autoincrement()) @map("Id")
  towerName String @map("towerName")
  attackPower Int @map("attackPower")
  attackSpeed Int @map("attackSpeed")
  attackRange Int @map("attackRange")
  attackType attackType @map("attackType")
  towerPrice Int @map("towerPrice")
  upgradeAttackPower Int @map("upgradeAttackPower")

  @@map("Tower")
}

 

위처럼 구성을 하고 

npx prisma generate --schema=prisma/schemaAsset.prisma

npx prisma generate --schema=prisma/schemaUser.prisma 를 통해 각각 prismaClient를 생성해줬다.

 

import { PrismaClient } from "@prisma/client";
import dotenv from "dotenv";

dotenv.config();

export const prismaUser = new PrismaUserClient({
  log: ["query", "info", "warn", "error"],
  errorFormat: "pretty",
  datasources: {
    db: {
      url: process.env.DATABASE_URL_USER,
    },
  },
});

export const prismaAsset = new PrismaAssetClient({
  log: ["query", "info", "warn", "error"],
  errorFormat: "pretty",
  datasources: {
    db: {
      url: process.env.DATABASE_URL_ASSET,
    },
  },
});

 

위처럼, 따로 파일을 만들어 User 테이블 PrismaClient와 Asset 테이블 PrismaClient에 접근할 수 있도록 구성했다.

User  테이블에 접근할 때는 문제가 없었는데,

Asset 테이블에 접근하면 undefined이 뜨는 문제가 생겼다.

 

이유를 찾지 못해, 몇시간 동안 이유를 찾아보다가 결국 찾았는데,

node_moudles 안에 있는 index.js에서 원인을 찾았다.

 

@prisma/client ( node_modules/client ) 으로 가서 안에 있는 파일들을 살펴보니

index.js에서 PrismaClient를 가지고오는 것을 확인할 수 있었다.

문제는 index.js에 User 테이블에 관한 기록만 생성이 되어있고, Asset 테이블에 관한 기록이 없었다는 점이다.

 

Object.defineProperty(exports, "__esModule", { value: true });

const {
  PrismaClientKnownRequestError,
  PrismaClientUnknownRequestError,
  PrismaClientRustPanicError,
  PrismaClientInitializationError,
  PrismaClientValidationError,
  NotFoundError,
  getPrismaClient,
  sqltag,
  empty,
  join,
  raw,
  skip,
  Decimal,
  Debug,
  objectEnumValues,
  makeStrictEnum,
  Extensions,
  warnOnce,
  defineDmmfProperty,
  Public,
  getRuntime
} = require('@prisma/client/runtime/library.js')


const Prisma = {}

exports.Prisma = Prisma
exports.$Enums = {}

/**
 * Prisma Client JS version: 5.20.0
 * Query Engine version: 06fc58a368dc7be9fbbbe894adf8d445d208c284
 */
Prisma.prismaVersion = {
  client: "5.20.0",
  engine: "06fc58a368dc7be9fbbbe894adf8d445d208c284"
}

Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError;
Prisma.PrismaClientUnknownRequestError = PrismaClientUnknownRequestError
Prisma.PrismaClientRustPanicError = PrismaClientRustPanicError
Prisma.PrismaClientInitializationError = PrismaClientInitializationError
Prisma.PrismaClientValidationError = PrismaClientValidationError
Prisma.NotFoundError = NotFoundError
Prisma.Decimal = Decimal

/**
 * Re-export of sql-template-tag
 */
Prisma.sql = sqltag
Prisma.empty = empty
Prisma.join = join
Prisma.raw = raw
Prisma.validator = Public.validator

/**
* Extensions
*/
Prisma.getExtensionContext = Extensions.getExtensionContext
Prisma.defineExtension = Extensions.defineExtension

/**
 * Shorthand utilities for JSON filtering
 */
Prisma.DbNull = objectEnumValues.instances.DbNull
Prisma.JsonNull = objectEnumValues.instances.JsonNull
Prisma.AnyNull = objectEnumValues.instances.AnyNull

Prisma.NullTypes = {
  DbNull: objectEnumValues.classes.DbNull,
  JsonNull: objectEnumValues.classes.JsonNull,
  AnyNull: objectEnumValues.classes.AnyNull
}




  const path = require('path')

/**
 * Enums
 */
exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
  ReadUncommitted: 'ReadUncommitted',
  ReadCommitted: 'ReadCommitted',
  RepeatableRead: 'RepeatableRead',
  Serializable: 'Serializable'
});

exports.Prisma.UserScalarFieldEnum = {
  id: 'id',
  highScore: 'highScore',
  password: 'password',
  email: 'email'
};

exports.Prisma.SortOrder = {
  asc: 'asc',
  desc: 'desc'
};


exports.Prisma.ModelName = {
  user: 'user'
};
/**
 * Create the Client
 */
const config = {
  "generator": {
    "name": "client",
    "provider": {
      "fromEnvVar": null,
      "value": "prisma-client-js"
    },
    "output": {
      "value": "C:\\Node.js Project\\TowerDefence\\node_modules\\@prisma\\client",
      "fromEnvVar": null
    },
    "config": {
      "engineType": "library"
    },
    "binaryTargets": [
      {
        "fromEnvVar": null,
        "value": "windows",
        "native": true
      }
    ],
    "previewFeatures": [],
    "sourceFilePath": "C:\\Node.js Project\\TowerDefence\\prisma\\schemaUser.prisma"
  },
  "relativeEnvPaths": {
    "rootEnvPath": null,
    "schemaEnvPath": "../../../.env"
  },
  "relativePath": "../../../prisma",
  "clientVersion": "5.20.0",
  "engineVersion": "06fc58a368dc7be9fbbbe894adf8d445d208c284",
  "datasourceNames": [
    "db"
  ],
  "activeProvider": "mysql",
  "postinstall": false,
  "inlineDatasources": {
    "db": {
      "url": {
        "fromEnvVar": "DATABASE_URL_USER",
        "value": null
      }
    }
  },
  "inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\n// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?\n// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init\n\ngenerator client {\n  provider = \"prisma-client-js\"\n}\n\ndatasource db {\n  provider = \"mysql\"\n  url      = env(\"DATABASE_URL_USER\")\n}\n\nmodel user {\n  id        Int    @id @default(autoincrement()) @map(\"id\")\n  highScore Int    @map(\"highScore\")\n  password  String @map(\"password\")\n  email     String @map(\"email\")\n\n  @@map(\"User\")\n}\n",
  "inlineSchemaHash": "b41a0a6e2263d480ccedf76c8119c75ebcb8d58ab449c6161959952662e00187",
  "copyEngine": true
}

const fs = require('fs')

config.dirname = __dirname
if (!fs.existsSync(path.join(__dirname, 'schema.prisma'))) {
  const alternativePaths = [
    "node_modules/.prisma/client",
    ".prisma/client",
  ]
  
  const alternativePath = alternativePaths.find((altPath) => {
    return fs.existsSync(path.join(process.cwd(), altPath, 'schema.prisma'))
  }) ?? alternativePaths[0]

  config.dirname = path.join(process.cwd(), alternativePath)
  config.isBundled = true
}

config.runtimeDataModel = JSON.parse("{\"models\":{\"user\":{\"dbName\":\"User\",\"fields\":[{\"name\":\"id\",\"dbName\":\"id\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":true,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"Int\",\"default\":{\"name\":\"autoincrement\",\"args\":[]},\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"highScore\",\"dbName\":\"highScore\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"Int\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"password\",\"dbName\":\"password\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"email\",\"dbName\":\"email\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false}],\"primaryKey\":null,\"uniqueFields\":[],\"uniqueIndexes\":[],\"isGenerated\":false}},\"enums\":{},\"types\":{}}")
defineDmmfProperty(exports.Prisma, config.runtimeDataModel)
config.engineWasm = undefined


const { warnEnvConflicts } = require('@prisma/client/runtime/library.js')

warnEnvConflicts({
    rootEnvPath: config.relativeEnvPaths.rootEnvPath && path.resolve(config.dirname, config.relativeEnvPaths.rootEnvPath),
    schemaEnvPath: config.relativeEnvPaths.schemaEnvPath && path.resolve(config.dirname, config.relativeEnvPaths.schemaEnvPath)
})

const PrismaClient = getPrismaClient(config)
exports.PrismaClient = PrismaClient
Object.assign(exports, Prisma)

// file annotations for bundling tools to include these files
path.join(__dirname, "query_engine-windows.dll.node");
path.join(process.cwd(), "node_modules/.prisma/client/query_engine-windows.dll.node")
// file annotations for bundling tools to include these files
path.join(__dirname, "schema.prisma");
path.join(process.cwd(), "node_modules/.prisma/client/schema.prisma")

 

위 코드를 살펴보면 User 테이블에 관한 기록은 되어 있지만, Asset 테이블에 관한 기록은 없는 것을 확인할 수 있다.

 

 

따라서 PrismaClient를 각각 따로 생성해줘야 해결이 될 것 같다라는 생각을 했다.

generator client {
  provider = "prisma-client-js"  
}

 

prisma 에서 table을 구성하는 파일인 .prima 파일의 generator client 에서

output 기능을 사용하면 각각의 PrismaClient를 생성해주는 것을 찾았다.

 

즉 위 코드를

generator client {
  provider = "prisma-client-js"
  output = "../prisma/assetPrisma/client"
}

 

generator client {
  provider = "prisma-client-js"  
  output = "../prisma/userPrisma/client"
}

 

이렇게 생성하면

 

이렇게 node_modules 폴더에 생성하지 않고, 내가 직접 지정해준 경로에 생성해준다는 것을 찾아서,

asset과 user를 각각 분리했다.

 

import { PrismaClient as PrismaUserClient } from "../../../../prisma/userPrisma/client/index.js";
import { PrismaClient as PrismaAssetClient } from "../../../../prisma/assetPrisma/client/index.js";
import dotenv from "dotenv";

dotenv.config();

export const prismaUser = new PrismaUserClient({
  log: ["query", "info", "warn", "error"],
  errorFormat: "pretty",
  datasources: {
    db: {
      url: process.env.DATABASE_URL_USER,
    },
  },
});

export const prismaAsset = new PrismaAssetClient({
  log: ["query", "info", "warn", "error"],
  errorFormat: "pretty",
  datasources: {
    db: {
      url: process.env.DATABASE_URL_ASSET,
    },
  },
});

 

그리고 나서 PrismaClient에 접근 하는 코드에도 경로를 새로 지정해 사용하니 더이상은 에러가 나지 않았다.

 

 

+ Recent posts