오늘의 목표

더보기

✔️ 모의 면접 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 서버 강의 완강

 

개인 과제 프로젝트 시작


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

 

개인 정보 수효 유집기간

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/150370.%E2%80%85%EA%B0%9C%EC%9D%B8%EC%A0%95%EB%B3%B4%E2%80%85%EC%88%98%EC%A7%91%E2%80%85%EC%9C%A0%ED%9A%A8%EA%B8%B0%EA%B0%84

 

AlgorithmCodingTest/프로그래머스/1/150370. 개인정보 수집 유효기간 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(today, terms, privacies) {
    const answer = [];

    // 들어온 today를 Date형식으로 바꾼다
    const expireDay = new Date(today);

    const termArray = {};
    terms.forEach((item) => {
        // split() 메서드로 분리한 타입과 기간을
        // 구조 분해 할당으로 변수화 한다.
        const [type, term] = item.split(" ");

        // 타입에 기간이 얼만지 객체로 생성한다.    
        termArray[type] = Number(term);
    });

    // 개인정보 수집일자를 확인한다.
    privacies.forEach((item, index) => {
        // 위와 마찬가지로 구조 분해 할당을 한다.
        const [date, type] = item.split(" ");

        // 구한 date는 new Date() 메서드를 통해 Date 형식으로 변환한다.
        const newDate = new Date(date);

        // 그리고 Date의 매서드인 setMonth를 이용해
        // chDate의 현재 달 + 타입의 기간을 더한 값을 구한다.
        newDate.setMonth(newDate.getMonth() + termArray[type]);

        // 마지막으로 오늘 날짜와 구한 유효기간을 비교해
        // 유효기간이 오늘 보다 작거나 같으면 answer에 번호를 넣는다.
        if (newDate <= expireDay) {
            answer.push(index + 1);
        }
    });

    return answer;
}

 

 

📜 TCP 서버 강의 완강

 

캠프에서 제공해준 tcp 서버 강의를 완강했다.매우 많은 내용이 담겨 있었는데, 정리해보자면 다음과 같다.

 

웹소켓으로 서버를 열고 접속하는 것이 아닌 TCP로 서버를 열고 접속하는 방식,DB Connection Pool 생성, 서버와 클라가 주기적으로 주고 받는 핑퐁 패킷,프로토 버프, 레이턴시와 해결방법 등

 

DB Connection Pool은 이전 c++ 게임서버에서 만든 경험이 있어서 어떤 부분이 다른가를 기대하면서 봤는데,전체적인 방식은 똑같지만 아래 코드 처럼 mysql자체에서 pool을 호출해서 만든다는 점이 매우 흥미로웠다.

 const pool = mysql.createPool({
        host: dbConfig.host,
        port: dbConfig.port,
        user: dbConfig.user,
        password: dbConfig.password,
        database: dbConfig.name,
        // MAX 연결 값을 넘은 요청이 들어올 경우
        // 이미 요청 중인 대상의 작업이 끝날 때까지 대기한다는 의미
        waitForConnections: true,
        connectionLimit: 10, // 커넥션 풀에서 최대 연결 수
        // 대기할 때 몇개의 요청을 대기 시킬 것인지에 대한 값 ( 0일 경우 무제한 대기열 )                   
        // 예) 2일 경우 3부터 대기하는 대상에게는 에러를 반환
        queueLimit: 0, 
    });

 

 

핑퐁 패킷을 주고 받으면서 앞서 언급한 레이턴시를 측정하고, 해당 데이터를 통해 접속한 클라들의 레이턴시를 맞춰가면서 처리하는 부분이 매우 흥미로웠다.

 

아래 코드 처럼 핑 패킷을 보내고, 클라에서는 핑 패킷을 받으면 퐁 패킷을 보내고, 최종적으로 서버가 받아 퐁 패킷을 기록한다. 결국 핑을 보내고, 퐁을 받을 때의 시간차이를 기록해 해당 클라와의 레이턴시를 기록한다.

// 핑 패킷 보내기
ping() {
    const now = Date.now();
    
    this.socket.write(createPingPacket(now));
}

// 퐁 패킷 받기
handlePong(data) {
    const now = Date.now();
    // 왕복 값이므로 2로 나눠준다.
    this.latency = (now - data.timestamp) / 2;
}

 

 

📜 개인 과제 프로젝트 시작

 

TCP 강의를 완강하고, TS로 프로젝트를 만든 후 기본적인 서버를 열고, 클라로 접속하는 부분 까지 만들었다.

 

게임 서버 클래스 ( 게임 서버를 열고, 접속을 담당 한다 . )

import net from "net";
import { config } from "../config/Config.js";
import { OnData } from "../events/OnData.js";
import { OnEnd } from "../events/OnEnd.js";
import { OnError } from "../events/OnError.js";

class GameServer {
    public recvCount: number;
    public sendCount: number;
    private server: any;    

    constructor() {
        this.recvCount = 0;
        this.sendCount = 0;
        this.server = net.createServer(this.Accept);
    } 

    StartGameServer() {
        console.log("게임서버 시작");

        this.Listen();
    }

    Listen() {
        this.server.listen(config.gameserver.port, config.gameserver.host, () => {
            console.log(`서버가 ${config.gameserver.host}:${config.gameserver.port}에서 실행 중입니다.`);
            console.log(this.server.address());
        });
    }

    Accept(socket: any) {
        console.log("클라 접속", socket.remoteAddress, socket.remotePort);

        socket.on("data", OnData(socket));
        socket.on("end", OnEnd(socket));
        socket.on("error", OnError(socket));
    }
}

export default GameServer;

 

 

메인 ( 프로그램 진입점 )

import GameServer from "./server/Server.js";

const gameServer = new GameServer();

function Main() {
    gameServer.StartGameServer();    
}

Main();

 

클라이언트 ( 서버 접속 테스트 용 )

import { config } from "../config/Config.js";
import net from "net";

const client = new net.Socket();

client.connect(5555, config.gameserver.host, async () => {
   
});

client.on("close", () => {
    console.log("클라 소켓 종료");
});

 

클라이언트는 우선 간단하게 위처럼 만들었는데, 추후에 클래스로 다중 접속을 시도해볼 예정이다.

많은 더미를 생성해 내가 만든 서버를 테스트 해 볼 생각!

 

 

레이턴시 ( Latency )

  • 한 지점에서 다른 지점으로 이동하는 데 걸리는 시간

 

라운드 트립 레이턴시 ( Round Trip Latency )

  • 데이터 패킷이 송신지에서 수신지로 이동하고, 다시 수신지에서 송신지로 돌아오는 데 걸리는 전체 시간
  • 라운드 트립 타임( RTT, Round Trip Time )이라고도 불린다.
  • 핑( Ping ) 명령어를 통해 측정된다.

 

레이턴시 마스킹 ( Latency Masking )

  • 네트워크 지연을 사용자가 느끼지 못하도록 숨기는 기술 ( 예측, 보정, 보간 )

 

추측항법 ( Dead Reckoning )

  • 예측 및 보정
  • 이미 지난 약간의 시간 ( = 레이턴시 )만큼 예측해 데이터를 전달

 


 

적용 방법

서버에 유저들이 접속하면 각 유저들의 라운드트립 레이턴시를 측정해 서버에 기록한다.

간단한 방법으로 평균으로 레이턴시를 계산하는 방법과 가장 높은 값을 사용하는 방법이 있다.

 

평균으로 레이턴시 계산

평균으로 레이턴시를 구하면 최소, 최대 값의 차이가 큰 유저간의 차이가 커지게 되는 경우가 있다.

 

가장 높은 값을 사용하는 방법

가장 높은 값을 사용하면 모두에게 느리게 보이기 때문에 안정적인 멀티플레이 동기화 환경을 제공할 수 있다.

 

아래 그림과 같은 경우 상황이 아주 좋을 때를 보여준다.

 

반면 아래 그림과 같이 네트워크 환경이 좋지 않은 유저가 있을 경우 최악의 환경이 될 수 있다.

오늘의 목표

더보기

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

✔️ 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++로 만든 서버에서 나름, 구현을 해봤는데

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

 

바이트 저장 순서 ( byte order )

컴퓨터는 데이터를 메모리에 저장할 때, 바이트 ( byte ) 단위로 나눠서 저장한다.

하지만 컴퓨터가 저장하는 데이터는 32 비트 ( 4바이트 ) 또는 64비트 ( 8바이트 )로 구성된다.

따라서 이렇게 연속되는 바이트를 순서대로 저장해야 하는데, 이를 바이트 저장 순서 ( byte order )라고 한다.

 

이때 바이트가 저장되는 순서에 따라 아래와 같이 두 가지 방식으로 나눌 수 있다.

1. 빅 엔디안 ( big endian )

2. 리틀 엔디안 ( little endian )


빅 엔디안 ( big endian )

빅 엔디안 방식은 낮은 주소에 데이터의 높은 바이트 ( MSB, Most Significant Bit )부터 저장하는 방식이다.

이 방식은 평소 우리가 숫자를 사용하는 선형 방식과 같은 방식이다.

따라서 메모리에 저장된 순서 그대로 읽을 수 있고, 이해하기가 쉽다는 장점을 가지고 있다.

SPARC를 포함한 대부분의 RISC CPU 계열에서는 이 방식으로 데이터를 저장한다.

 

예를 들어 아래와 같이 저장할 32비트 크기의 정수가 있다고 가정해보자.

0x12345678

 

이 정수는 각각 아래와 같이 1바이트값 4개로 구성된다.

0x12, 0x34, 0x56, 0x78

 

이 4개의 1바이트 값을 빅 엔디안 방식으로 저장하면 다음 그림과 같이 저장된다.

 

 


 

리틀 엔디안 ( little endian )

리틀 엔디안 방식은 낮은 주소에 데이터의 낮은 바이트 ( LSB, Least Significant Bit )부터 저장하는 방식이다.

이 방식은 평소 사용하는 숫자를 사용하는 선형 방식과는 반대로 수를 거꾸로 읽어야 한다.

대부분의 인텔 CPU 계열에서는 리틀 엔디안 방식으로 데이터를 저장한다.

 

위에서 예를 든 정수 "0x12345678"를 리틀 엔디안 방식으로 저장하면 아래 그림과 같이 저장된다.

 


 

빅 엔디안 vs 리틀 엔디안

빅 엔디안과 리틀 엔디안은 단지 저장해야 할 큰 데이터를 어떻게 나누어 저장하는가에 따른 차이일 뿐, 

어느 방식이 더 우수하다고 단정할 수 없다.

 

물리적으로 데이터를 조작하거나 산술 연산을 수행할 때는 리틀 엔디안 방식이 더 효율적이다.

하지만 데이터의 각 바이트를 배열처럼 취급할 때는 빅 엔디안 방식이 더 적합하다.

 

윈도우에서는 리틀 엔디안 방식을 사용하고 있다.

하지만 네트워크를 통해 데이터를 전송할 때는 빅 엔디안 방식이 사용된다.

따라서 인텔 기반의 시스템에서는 소켓 통신을 할 때는 바이트 순서에 신경을 써서 데이터를 전달해야 한다.

 

객체 지향 설계를 할 때 S.O.L.I.D 원칙을 따라 설계를 하는 것이 필수다.

 

S ( SRP. 단일 책임 원칙 )

 SRP 원칙

  • 클래스는 하나의 책임만 가져야 한다는 기본적인 원칙이다.
  • 5개의 설계 원칙 중 가장 기본적이고 중요한 원칙이다.

 

잘못된 예시

class UserService {
  constructor(private db: Database) {}

  getUser(id: number): User {
    // 사용자 조회 로직
    return this.db.findUser(id);
  }

  saveUser(user: User): void {
    // 사용자 저장 로직
    this.db.saveUser(user);
  }

  sendWelcomeEmail(user: User): void {
    // 이메일 전송 로직이 여기에 있으면 안된다.
    const emailService = new EmailService();
    emailService.sendWelcomeEmail(user);
  }
}

 

올바른 예시

class UserService {
  constructor(private db: Database) {}

  getUser(id: number): User {
    // 사용자 조회 로직
    return this.db.findUser(id);
  }

  saveUser(user: User): void {
    // 사용자 저장 로직
    this.db.saveUser(user);
  }
}

class EmailService {
  // 이메일 관련된 기능은 이메일 서비스에서 총괄하는게 맞다.
  // 다른 서비스에서 이메일 관련된 기능을 쓴다는 것은 영역을 침범하기 때문..
  sendWelcomeEmail(user: User): void {
    // 이메일 전송 로직
    console.log(`Sending welcome email to ${user.email}`);
  }
}

 

O ( OCP. 개발 폐쇄 원칙 ) ▶ 인터페이스 혹은 상속을 잘 쓰자!

  • 클래스는 확장에 대해서는 열려 있어야 하며, 수정에 대해서는 닫혀 있어야 한다는 원칙
  • 클래스의 기존 코드를 변경하지 않고도 기능을 확장할 수 있어야 한다.
  • 즉, 인터페이스상속을 통해 이를 해결할 수가 있다. ( 부모 클래스의 기존 코드 변경을 하지 않고 기능을 확장하는데 아무런 문제가 없기 때문 )

 

L ( LSP. 리스코프 치환 원칙 )

 LSP 원칙

  • 서브타입은 기반이 되는 슈퍼타입을 대체할 수 있어야 한다는 원칙
  • 다시 말해, 자식 클래스는 부모 클래스의 기능을 수정하지 않고도 부모 클래스와 호환되어야 한다.
  • 논리적으로 엄격하게 관계가 정립이 되어야 한다는 의미

잘못된 예시

class Bird {
  fly(): void {
    console.log("펄럭펄럭~");
  }
}

class Penguin extends Bird {
  // 으잉? 펭귄이 날 수 있나요? 펭귄이 펄럭펄럭~ 한다는 것은 명백한 위반이죠.
}

 

올바른 예시

abstract class Bird {
  abstract move(): void;
}

class FlyingBird extends Bird {
  move() {
    console.log("펄럭펄럭~");
  }
}

class NonFlyingBird extends Bird {
   move() {
    console.log("뚜벅뚜벅!");
  }
}

class Penguin extends NonFlyingBird {} // 이제 위배되는 것은 아무것도 없네요!

 

 

I ( ISP. 인터페이스 분리 원칙 )

  • 클래스는 자신이 사용하지 않는 인터페이스의 영향을 받지 않아야 한다.
  • 즉, 해당 클래스에게 무의미한 메소드의 구현을 막자는 의미
  • 인터페이스를 너무 크게 정의하기보다는 필요한 만큼 정의하고 클래스는 입맛에 맞게 필요한 인터페이스들을 구현하도록 유도한다.

 

D ( DIP. 의존성 역전 원칙 )

 DIP 원칙

  •  DIP는 Java의 Spring 프레임워크나 Node.js의 Nest.js 프레임워크와 같이 웹 서버 프레임워크내에서 많이 나오는 원칙이다.
  • 이 원칙은 하위 수준 모듈 ( 구현 클래스 )보다 상위 수준 모듈 ( 인터페이스 )에 의존을 해야한다는 의미다.

사용 예시

interface MyStorage {
  save(data: string): void;
}

class MyLocalStorage implements MyStorage {
  save(data: string): void {
    console.log(`로컬에 저장: ${data}`);
  }
}

class MyCloudStorage implements MyStorage {
  save(data: string): void {
    console.log(`클라우드에 저장: ${data}`);
  }
}

class Database {
  // 상위 수준 모듈인 MyStorage 타입을 의존! 
  // 여기서 MyLocalStorage, MyCloudStorage 같은 하위 수준 모듈에 의존하지 않는게 핵심!
  constructor(private storage: MyStorage) {}

  saveData(data: string): void {
    this.storage.save(data);
  }
}

const myLocalStorage = new MyLocalStorage();
const myCloudStorage = new MyCloudStorage();

const myLocalDatabase = new Database(myLocalStorage);
const myCloudDatabase = new Database(myCloudStorage);

myLocalDatabase.saveData("로컬 데이터");
myCloudDatabase.saveData("클라우드 데이터");

오늘의 목표

더보기

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

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


⏱️ 오늘의 일정

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

팀 프로젝트 완성 및 발표


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

 

달리기 경주

 

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