전송 계층 프로토콜에 대해 설명
전송 계층 프로토콜에는 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 소스에는 위 코드가 들어가 있긴하다.
처음에는 그냥 바로 전부 접속 시도를 했는데, 서버에서 한번에 다 받지를 못하는 경우가 생겨서
functionsolution(k, tangerine) {
let answer = 0;
let kind = newArray(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];
}
elseif (kind[j] >= k) {
answer++;
break;
}
}
return answer;
}
📜 TCP 서버 강의 듣기
개인 과제 수행을 하기 위해 캠프에서 제공받은 Node.js를 이용해 만드는 TCP 서버 강의를 듣고 있다.
functionsolution(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]}`;
}
이메일과 비밀번호를 검증하고 성공하면 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;
functiongenerateRandomMonsterPath() {
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;
}
functioninitMap() {
ctx.drawImage(backgroundImage, 0, 0, canvas.width, canvas.height); // 배경 이미지 그리기
drawPath();
}
functiondrawPath() {
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);
}
}
}
functiondrawRotatedImage(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();
}
functiongetRandomPositionNearPath(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,
};
}
functionplaceInitialTowers() {
/*
타워를 초기에 배치하는 함수입니다.
무언가 빠진 코드가 있는 것 같지 않나요?
*/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);
}
}
functionplaceNewTower() {
/*
타워를 구입할 수 있는 자원이 있을 때 타워 구입 후 랜덤 배치하면 됩니다.
빠진 코드들을 채워넣어주세요!
*/const { x, y } = getRandomPositionNearPath(200);
const tower = new Tower(x, y);
towers.push(tower);
tower.draw(ctx, towerImage);
}
functionplaceBase() {
const lastPoint = monsterPath[monsterPath.length - 1];
base = new Base(lastPoint.x, lastPoint.y, baseHp);
base.draw(ctx, baseImage);
}
functionspawnMonster() {
monsters.push(new Monster(monsterPath, monsterImages, monsterLevel));
}
functiongameLoop() {
// 렌더링 시에는 항상 배경 이미지부터 그려야 합니다! 그래야 다른 이미지들이 배경 이미지 위에 그려져요!
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 함수 호출할 수 있도록 함
}
functioninitGame() {
if (isInitGame) {
return;
}
monsterPath = generateRandomMonsterPath(); // 몬스터 경로 생성
initMap(); // 맵 초기화 (배경, 몬스터 경로 그리기)
placeInitialTowers(); // 설정된 초기 타워 개수만큼 사전에 타워 배치
placeBase(); // 기지 배치setInterval(spawnMonster, monsterSpawnInterval); // 설정된 몬스터 생성 주기마다 몬스터 생성
gameLoop(); // 게임 루프 최초 실행
isInitGame = true;
}
// 이미지 로딩 완료 후 서버와 연결하고 게임 초기화Promise.all([
newPromise((resolve) => (backgroundImage.onload = resolve)),
newPromise((resolve) => (towerImage.onload = resolve)),
newPromise((resolve) => (baseImage.onload = resolve)),
newPromise((resolve) => (pathImage.onload = resolve)),
...monsterImages.map(
(img) =>newPromise((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";
classGame{
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();
}
asyncGameLoop() {
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();
});
}
}
exportdefault 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")
}