전송 계층 프로토콜에 대해 설명
전송 계층 프로토콜에는 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 소스에는 위 코드가 들어가 있긴하다.
처음에는 그냥 바로 전부 접속 시도를 했는데, 서버에서 한번에 다 받지를 못하는 경우가 생겨서
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 서버 강의를 듣고 있다.
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]}`;
}
이메일과 비밀번호를 검증하고 성공하면 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")
}