본문 바로가기
내일배움 과제/CH4-1

크롬 공룡 만들기

by GREEN나무 2024. 12. 16.
728x90

1. 환경 설정

Vscode

http, css, js

DB : 

mySQL

prima

localhost:3306

# 프로젝트 시작
yarn init -y

# express socket.io 설치
yarn add express socket.io

# prettier nodemon 설치 (위에서 이미 설치됨으로 중복 제거)
yarn add -D nodemon prettier

# prisma
$ yarn prisma init

 

// package.json

// main 서버파일로 수정
"main": "src/app.js",

// 옵션 추가
"type": "module",
  "scripts": {
    "dev":"nodemon ./src/app.js"
    }

 

// .prettierrc

{
  "singleQuote": true,
  "semi": true,
  "useTabs": false,
  "tabWidth": 2,
  "trailingComma": "all",
  "printWidth": 100,
  "arrowParens": "always",
  "orderedImports": true,
  "bracketSpacing": true,
  "jsxBracketSameLine": false
}

 

express 서버 세팅하기

// ./src/app.js   서버 기본 틀
import express from "express";
import { createServer } from "http";

const app = express();
const server = createServer(app);

const PORT = 3000;

app.use(express.json());
app.use(express.urlencoded({ extended: false }));

server.listen(PORT, async () => {
  console.log(`Server is running on port ${PORT}`);
});

 

그 외 코드

더보기

 

# .gitignore

# .gitignore

# compiled output
/dist
/node_modules

# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# OS
.DS_Store

# Tests
/coverage
/.nyc_output

# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace

# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

 

 ./assets 폴더

 

// ./assets/item_unlock.json

{
  "name": "item_unlock",
  "version": "1.0.0",
  "data": [
    { "id":  101, "stage_id": 1001, "item_id": 1 },
    { "id":  201, "stage_id": 1002, "item_id": 2 }
  ]
}

// ./assets/item.json

{
  "name": "item",
  "version": "1.0.0",
  "data": [
    { "id":  1, "score": 10 },
    { "id":  2, "score": 20 },
    { "id":  3, "score": 30 },
    { "id":  4, "score": 40 },
    { "id":  5, "score": 50 },
    { "id":  6, "score": 60 }
  ]
}

// ./assets/stage.json

{
  "name": "stage",
  "version": "1.0.0",
  "data": [
    { "id":  1000, "score": 0 },
    { "id":  1001, "score": 100 },
    { "id":  1002, "score": 200 },
    { "id":  1003, "score": 300 },
    { "id":  1004, "score": 400 },
    { "id":  1005, "score": 500 },
    { "id":  1006, "score": 600 }
  ]
}

 

./src 폴더

// ./src/app.js

// ./src/app.js
import express from 'express';
import { createServer } from 'http';
import initSocket from './init/socket.js';
import { loadGameAssets } from './init/assets.js';

const app = express();
const server = createServer(app);

const PORT = 3000;

app.use(express.static('public'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
initSocket(server);

app.get('/', (req, res) => {
  res.send('<h1>Hello World</h1>');
});

server.listen(PORT, async () => {
  console.log(`Server is running on port ${PORT}`);

  try {
    const assets = await loadGameAssets();
    console.log(assets);
    console.log('Assets loaded successfully');
  } catch (error) {
    console.error('Failed to load game assets:', error);
  }
});

// ./src/constants.js

// ./src/constants.js
export const CLIENT_VERSION = ['1.0.0', '1.0.1', '1.1.0'];

 

  ./src/handlers 폴더

 

// ./src/handlers/game.handler.js

// // ./src/handlers/game.handler.js
import { getGameAssets } from '../init/assets.js';
import { clearStage, getStage, setStage } from '../models/stage.model.js';

export const gameStart = (uuid, payload) => {
  const { stages } = getGameAssets();
  clearStage(uuid);
  setStage(uuid, stages.data[0].id, payload.timestamp);
  console.log('Stage:', getStage(uuid));

  return { status: 'success' };
};

export const gameEnd = (uuid, payload) => {
  // 클라이언트에서 받은 게임 종료 시 타임스탬프와 총 점수
  const { timestamp: gameEndTime, score } = payload;
  const stages = getStage(uuid);

  if (!stages.length) {
    return { status: 'fail', message: 'No stages found for user' };
  }

  // 각 스테이지의 지속 시간을 계산하여 총 점수 계산
  let totalScore = 0;
  stages.forEach((stage, index) => {
    let stageEndTime;
    if (index === stages.length - 1) {
      // 마지막 스테이지의 경우 종료 시간이 게임의 종료 시간
      stageEndTime = gameEndTime;
    } else {
      // 다음 스테이지의 시작 시간을 현재 스테이지의 종료 시간으로 사용
      stageEndTime = stages[index + 1].timestamp;
    }
    const stageDuration = (stageEndTime - stage.timestamp) / 1000; // 스테이지 지속 시간 (초 단위)
    totalScore += stageDuration; // 1초당 1점
  });

  // 점수와 타임스탬프 검증 (예: 클라이언트가 보낸 총점과 계산된 총점 비교)
  // 오차범위 5
  if (Math.abs(score - totalScore) > 5) {
    return { status: 'fail', message: 'Score verification failed' };
  }

  // 모든 검증이 통과된 후, 클라이언트에서 제공한 점수 저장하는 로직
  // saveGameResult(userId, clientScore, gameEndTime);
  // 검증이 통과되면 게임 종료 처리
  return { status: 'success', message: 'Game ended successfully', score };
};

// ./src/handlers/handlerMapping.js

// ./src/handlers/handlerMapping.js
import { moveStageHandler } from './stage.handler.js';
import { gameEnd, gameStart } from './game.handler.js';

const handlerMappings = {
  2: gameStart,
  3: gameEnd,
  11: moveStageHandler,
};

export default handlerMappings;

// ./src/handlers/helper.js

// ./src/handlers/helper.js
import { getUsers, removeUser } from '../models/user.model.js';
import { CLIENT_VERSION } from '../constants.js';
import handlerMappings from './handlerMapping.js';
import { createStage } from '../models/stage.model.js';

export const handleConnection = (socket, userUUID) => {
  console.log(`New user connected: ${userUUID} with socket ID ${socket.id}`);
  console.log('Current users:', getUsers());

  // 스테이지 빈 배열 생성
  createStage(userUUID);

  socket.emit('connection', { uuid: userUUID });
};

export const handleDisconnect = (socket, uuid) => {
  removeUser(socket.id); // 사용자 삭제
  console.log(`User disconnected: ${socket.id}`);
  console.log('Current users:', getUsers());
};

export const handleEvent = (io, socket, data) => {
  if (!CLIENT_VERSION.includes(data.clientVersion)) {
    socket.emit('response', { status: 'fail', message: 'Client version mismatch' });
    return;
  }

  const handler = handlerMappings[data.handlerId];
  if (!handler) {
    socket.emit('response', { status: 'fail', message: 'Handler not found' });
    return;
  }

  const response = handler(data.userId, data.payload);
  if (response.broadcast) {
    io.emit('response', 'broadcast');
    return;
  }
  socket.emit('response', response);
};

// ./src/handlers/register.handler.js

// ./src/handlers/register.handler.js
import { v4 as uuidv4 } from 'uuid';
import { addUser } from '../models/user.model.js';
import { handleConnection, handleDisconnect, handleEvent } from './helper.js';

const registerHandler = (io) => {
  io.on('connection', (socket) => {
    // 최초 커넥션을 맺은 이후 발생하는 각종 이벤트를 처리하는 곳

    const userUUID = uuidv4(); // UUID 생성
    addUser({ uuid: userUUID, socketId: socket.id }); // 사용자 추가

    handleConnection(socket, userUUID);

    // 모든 서비스 이벤트 처리
    socket.on('event', (data) => handleEvent(io, socket, data));
    // 접속 해제시 이벤트 처리
    socket.on('disconnect', () => handleDisconnect(socket, userUUID));
  });
};

export default registerHandler;

// ./src/handlers/stage.handler.js

// ./src/handlers/stage.handler.js
import { getStage, setStage } from '../models/stage.model.js';
import { getGameAssets } from '../init/assets.js';

export const moveStageHandler = (userId, payload) => {
  // 유저의 현재 스테이지 배열을 가져오고, 최대 스테이지 ID를 찾는다.
  let currentStages = getStage(userId);
  if (!currentStages.length) {
    return { status: 'fail', message: 'No stages found for user' };
  }

  // 오름차순 정렬 후 가장 큰 스테이지 ID 확인 = 가장 상위의 스테이지 = 현재 스테이지
  currentStages.sort((a, b) => a.id - b.id);
  const currentStage = currentStages[currentStages.length - 1];

  // payload 의 currentStage 와 비교
  if (currentStage.id !== payload.currentStage) {
    return { status: 'fail', message: 'Current stage mismatch' };
  }

  // 점수 검증
  const serverTime = Date.now();
  const elapsedTime = (serverTime - currentStage.timestamp) / 1000; // 초 단위로 계산

  // 1초당 1점, 100점이상 다음스테이지 이동, 오차범위 5
  // 클라이언트와 서버 간의 통신 지연시간을 고려해서 오차범위 설정
  // elapsedTime 은 100 이상 105 이하 일 경우만 통과
  if (elapsedTime < 100 || elapsedTime > 105) {
    return { status: 'fail', message: 'Invalid elapsed time' };
  }

  // 게임 에셋에서 다음 스테이지의 존재 여부 확인
  const { stages } = getGameAssets();
  if (!stages.data.some((stage) => stage.id === payload.targetStage)) {
    return { status: 'fail', message: 'Target stage does not exist' };
  }

  // 유저의 다음 스테이지 정보 업데이트 + 현재 시간
  setStage(userId, payload.targetStage, serverTime);
  return { status: 'success' };
};

 

  ./src/init 폴더

// ./src/init/assets.js

// ./src/init/assets.js
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

// import.meta.url은 현재 모듈의 URL을 나타내는 문자열
// fileURLToPath는 URL 문자열을 파일 시스템의 경로로 변환

// 현재 파일의 절대 경로. 이 경로는 파일의 이름을 포함한 전체 경로
const __filename = fileURLToPath(import.meta.url);

// path.dirname() 함수는 파일 경로에서 디렉토리 경로만 추출 (파일 이름을 제외한 디렉토리의 전체 경로)
const __dirname = path.dirname(__filename);
const basePath = path.join(__dirname, '../../assets');
let gameAssets = {}; // 전역함수로 선언

const readFileAsync = (filename) => {
  return new Promise((resolve, reject) => {
    fs.readFile(path.join(basePath, filename), 'utf8', (err, data) => {
      if (err) {
        reject(err);
        return;
      }
      resolve(JSON.parse(data));
    });
  });
};

export const loadGameAssets = async () => {
  try {
    const [stages, items, itemUnlocks] = await Promise.all([
      readFileAsync('stage.json'),
      readFileAsync('item.json'),
      readFileAsync('item_unlock.json'),
    ]);
    gameAssets = { stages, items, itemUnlocks };
    return gameAssets;
  } catch (error) {
    throw new Error('Failed to load game assets: ' + error.message);
  }
};

export const getGameAssets = () => {
  return gameAssets;
};

// ./src/init/socket.js

// ./src/init/socket.js
import { Server as SocketIO } from 'socket.io';
import registerHandler from '../handlers/register.handler.js';

const initSocket = (server) => {
  const io = new SocketIO();
  io.attach(server);
  registerHandler(io);
};

export default initSocket;

 

./src/model 폴더

// ./src/models/stage.model.js

// ./src/models/stage.model.js
const stages = {};

export const createStage = (uuid) => {
  stages[uuid] = []; // 초기 스테이지 배열 생성
};

export const getStage = (uuid) => {
  return stages[uuid];
};

export const setStage = (uuid, id, timestamp) => {
  return stages[uuid].push({ id, timestamp });
};

export const clearStage = (uuid) => {
  return (stages[uuid] = []);
};

// ./src/model/user.model.js

// ./src/model/user.model.js
const users = [];

export const addUser = (user) => {
  users.push(user);
};

export const removeUser = (socketId) => {
  const index = users.findIndex((user) => user.socketId === socketId);
  if (index !== -1) {
    return users.splice(index, 1)[0];
  }
};

export const getUsers = () => {
  return users;
};

 

 

 

 

 

// ./src/init/socket.js

// ./src/init/socket.js
import { Server as SocketIO } from "socket.io";

const initSocket = (server) => {
  const io = new SocketIO();
  io.attach(server);
};

export default initSocket;

 

 

실행 명령어

// 서버 실행
yarn dev

 


2. 문서화

 

DB

https://drawsql.app/teams/yewon-yoon/diagrams/ch4-1-2

 

와이어 프레임

https://app.eraser.io/workspace/e3wJkFL4vCMaAdlGDsnz?origin=share&elements=wJwOhwamJ_fiaqYzeLpi2A

 

 


3. 오류 정리

 

 

 

 

참조무결성 제약 On Delete / On Update

더보기

1) On Delete

 Cascade : 부모 데이터 삭제 시 자식 데이터도 삭제 

 Set null : 부모 데이터 삭제 시 자식 테이블의 참조 컬럼을 Null로 업데이트

 Set default : 부모 데이터 삭제 시 자식 테이블의 참조 컬럼을 Default 값으로 업데이트

 Restrict : 자식 테이블이 참조하고 있을 경우, 데이터 삭제 불가

 No Action : Restrict와 동일, 옵션을 지정하지 않았을 경우 자동으로 선택된다.

2) On Update

 Cascade : 부모 데이터 업데이트 시 자식 데이터도 업데이트 

 Set null : 부모 데이터 업데이트 시 자식 테이블의 참조 컬럼을 Null로 업데이트

 Set default : 부모 데이터 업데이트 시 자식 테이블의 참조 컬럼을 Default 값으로 업데이트

 Restrict : 자식 테이블이 참조하고 있을 경우, 업데이트 불가

 No Action : Restrict와 동일, 옵션을 지정하지 않았을 경우 자동으로 선택된다.

 

model Users {
  userId Int    @id @default(autoincrement()) @map("userId")
  name   String @unique @map("name")

  currentStage Stages? @relation("StagesToUsersCurrent", fields: [stageId], references: [currentStage], onDelete: SetNull)
  targetStage  Stages? @relation("StagesToUsersTarget", fields: [stageId], references: [targetStage], onDelete: SetNull)

  payload Payloads[] @relation("UsersToPayloads")

  @@map("Users")
}

model Stages {
  stageId Int @id @default(autoincrement())

  targetStage  Users[] @relation("StagesToUsersTarget")
  currentStage Users[] @relation("StagesToUsersCurrent")

  @@map("Stages")
}

 

 

 

 


4. 시현

 

 

참고

 

참조 무결성 제약 : https://swingswing.tistory.com/5

 

프리즈마 문서 : https://www.prisma.io/docs/orm/prisma-client/queries/relation-queries

 

 

 

'내일배움 과제 > CH4-1' 카테고리의 다른 글

아이템  (0) 2024.12.18
CH4 발제  (0) 2024.12.10