오늘의 목표

더보기

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

✔️ 팀프로젝트 진행


⏱️ 오늘의 일정

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


 

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

 

최댓값과 최솟값

 

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에 접근 하는 코드에도 경로를 새로 지정해 사용하니 더이상은 에러가 나지 않았다.

 

 

Prisma를 이용해 게시판을 개발해보자

 

[게시판 프로젝트] 시작하기

[게시판 프로젝트] 설계

게시판 프로젝트 ERD

 

위 그림을 바탕으로 각 테이블간의 관계 및 요구사항을 정리해보자.

 

게시판 프로젝트 테이블 관계 및 요구사항 정리

사용자( Users )는 1개의 사용자 정보 ( UserInfo )를 가지고 있다.

  • Users 테이블과 UserInfo 테이블은 1:1 관계를 가지고 있다.

사용자( Users )는 여러개의 게시글 ( Posts )을 등록할 수 있다.

  • Users 테이블과 Posts 테이블을 1:N 관계를 가지고 있다.

사용자( Users )는 여러개의 댓글 ( Commnets )을 작성할 수 있다.

  • Users 테이블과 Comments 테이블은 1:N 관계를 가지고 있다.

하나의 게시글 ( Posts )은 여러개의 댓글 ( Comments )이 작성될 수 있다.

  • Posts 테이블과 Comments 테이블은 1:N 관계를 가지고 있다.

 


 

Community-Hub 프로젝트 생성

[게시판 프로젝트] 라이브러리 설치

# 프로젝트를 초기화합니다.
yarn init -y

# 라이브러리를 설치합니다.
yarn add express prisma @prisma/client cookie-parser jsonwebtoken

# nodemon 라이브러리를 DevDependency로 설치합니다.
yarn add -D nodemon

# 설치한 Prisma를 초기화 하여, Prisma를 사용할 수 있는 구조를 생성합니다.
npx prisma init
  • package.json에 "type":"module"도 추가 ( ES6 문법 사용하기 위함 )

 

프로젝트 구조 ( 게시판 프로젝트 )

내 프로젝트 폴더 이름
├── .env
├── .gitignore
├── package.json
├── yarn.lock
├── prisma
│    └── schema.prisma
└── src
  └── app.js

 

SQL로 미리 확인해보기

CREATE TABLE Users
(
    userId    INTEGER             NOT NULL AUTO_INCREMENT PRIMARY KEY,
    email     VARCHAR(191) UNIQUE NOT NULL,
    password  VARCHAR(191)        NOT NULL,
    createdAt DATETIME(3)         NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
    updatedAt DATETIME(3)         NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
);


CREATE TABLE UserInfos
(
    userInfoId   INTEGER        NOT NULL AUTO_INCREMENT PRIMARY KEY,
    userId       INTEGER UNIQUE NOT NULL, -- 1:1 관계 이므로 UNIQUE 조건을 삽입합니다.
    name         VARCHAR(191)   NOT NULL,
    age          INTEGER        NOT NULL,
    gender       VARCHAR(191)   NOT NULL,
    profileImage VARCHAR(191)   NULL,
    createdAt    DATETIME(3)
                                NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
    updatedAt    DATETIME(3)    NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
);

ALTER TABLE UserInfos
    ADD CONSTRAINT FK_UserInfos_Users
        FOREIGN KEY (userId) REFERENCES Users (userId) ON DELETE CASCADE;

CREATE TABLE Posts
(
    postId    INTEGER      NOT NULL AUTO_INCREMENT PRIMARY KEY,
    userId    INTEGER      NOT NULL,
    title     VARCHAR(191) NOT NULL,
    content   VARCHAR(191) NOT NULL,
    createdAt DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
    updatedAt DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
);

ALTER TABLE Posts
    ADD CONSTRAINT FK_Posts_Users
        FOREIGN KEY (userId) REFERENCES Users (userId) ON DELETE CASCADE;


CREATE TABLE Comments
(
    commentId INTEGER      NOT NULL AUTO_INCREMENT PRIMARY KEY,
    userId    INTEGER      NOT NULL,
    postId    INTEGER      NOT NULL,
    content   VARCHAR(191) NOT NULL,
    createdAt DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
    updatedAt DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
);

ALTER TABLE Comments
    ADD CONSTRAINT FK_Comments_Posts
        FOREIGN KEY (postId) REFERENCES Posts (postId) ON DELETE CASCADE;

ALTER TABLE Comments
    ADD CONSTRAINT FK_Comments_Users
        FOREIGN KEY (userId) REFERENCES Users (userId) ON DELETE CASCADE;

 

 

[게시판 프로젝트] Prisma 설계하기

Prisma model 구현하기

먼저, 구현될 요구사항을 바탕으로 Prisma의 모델을 작성해보자.

각 테이블의 요구사항을 바탕으로 schema.prisma 파일의 모델을 작성해보자.

 

사용자 ( Users ) 테이블

Name 타입 ( Type ) NULL default
userId ( PK ) INTEGER NOT NULL AUTO_INCREMENT
email STRING NOT NULL  
password STRING NOT NULL  
createdAt DATETIME NOT NULL 현재 시간
updatedAt DATETIME NOT NULL 현재 시간

 

게시글 ( Posts ) 테이블

Name 타입 ( Type ) NULL default
postId ( PK ) INTEGER NOT NULL AUTO_INCREMENT
title STRING NOT NULL  
content TEXT NOT NULL  
ceratedAt DATETIME NOT NULL 현재 시간
updatedAt DATETIME NOT NULL 현재 시간

 

사용자 정보 ( UsersInfos ) 테이블

Name 타입 ( Type ) NULL default
userInfoId ( PK ) INTEGER NOT NULL AUTO_INCREMENT
name STRING NOT NULL  
age INTEGER NULL  
gender STRING NOT NULL  
profileImage STRING NULL  
createdAt DATETIME NOT NULL 현재 시간
updatedAt DATETIME NOT NULL 현재 시간

 

댓글 ( Comments ) 테이블

Name 타입 ( Type ) NULL default
commentId ( PK ) INTEGER NOT NULL AUTO_INCREMENT
content STRING NOT NULL  
createdAt DATETIME NOT NULL 현재 시간
updatedAt DATETIME NOT NULL 현재 시간

 

위 작성한 테이블을 기준으로 Prisma model을 구현해보자.

schema.prisma에 Prisma mode을 입력한다.

 

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

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

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

	createdAt DateTime @default(now()) @map("createdAt")
	updatedAt DateTime @updatedAt @map("updatedAt")

	@@map("Users")
}

model Posts{
	postId Int @id @default(autoincrement()) @map("postId")
	title String @map("title")
	content String @map("content") @db.Text

	createdAt DateTime @default(now()) @map("createdAt")
	updatedAt DateTime @updatedAt @map("updatedAt")

	@@map("Posts")
}

model UserInfos{
	userInfoId Int @id @default(autoincrement()) @map("userInfoId")
	name String @map("name")
	age Int? @map("age")
	gender String @map("gender")
	profileImage String? @map("profileImage")

	createdAt DateTime @default(now()) @map("createdAt")
	updatedAt DateTime @updatedAt @map("updatedAt")

	@@map("UserInfos")
}

model Comments{
	commentId Int @id @default(autoincrement()) @map("commentId")
	content String @map("content")
	
	createdAt DateTime @default(now()) @map("createdAt")
	updatedAt DateTime @updatedAt @map("updatedAt")

	@@map("Comments")
}

 

Prisma 1:1 관계

요구사항 중 "사용자 ( Users )는 1개의 사용자 정보 ( UserInfo )를 가지고 있다." 에서 사용자와 사용자 정보 모델의 경우 1:1 관계를 가지고 있는 것을 확인할 수 있다. 해당 모델을 비교해 Prisma model은 어떤 방법으로 관계를 설정하는지 확인해보자.

 

// schema.prisma 

model Users {
  userId    Int      @id @default(autoincrement()) @map("userId")
  email     String   @unique @map("email")
  password  String   @map("password")
  createdAt DateTime @default(now()) @map("createdAt")
  updatedAt DateTime @updatedAt @map("updatedAt")

  userInfos UserInfos? // 사용자(Users) 테이블과 사용자 정보(UserInfos) 테이블이 1:1 관계를 맺습니다.

  @@map("Users")
}


model UserInfos {
  userInfoId   Int      @id @default(autoincrement()) @map("userInfoId")
  userId       Int      @unique @map("userId") // 사용자(Users) 테이블을 참조하는 외래키
  name         String   @map("name")
  age          Int?     @map("age")
  gender       String   @map("gender")
  profileImage String?  @map("profileImage")
  createdAt    DateTime @default(now()) @map("createdAt")
  updatedAt    DateTime @updatedAt @map("updatedAt")

  // Users 테이블과 관계를 설정합니다.
  user Users @relation(fields: [userId], references: [userId], onDelete: Cascade)

  @@map("UserInfos")
}

 

사용자 ( Users ) 모델은 사용자 정보 ( UserInfos ) 모델과 1:1관계를 가지고 있다.

여기서, 1:1 관계란 한 사용자가 하나의 사용자 정보만 가질 수 있고, 한 사용자 정보는 한 사용자에게만 속할 수 있다는 것을 의미한다.

 

이런 1:1 관계를 설정할 때는 다음과 같은 내용을 포함해야 한다.

  1. 관계를 설정하려는 모델 ( UserInfos )에서 어떤 모델과 관계를 맺을지 ( Users ) 설정해야 한다.
  2. 관계를 맺게되는 모델 ( Users )에서 어떤 모델이 관계를 맺는지 ( UserInfos ) 설정해야한다.
  3. 관계를 맺게되는 모델 ( Users )에서 타입을 지정할 때, Optional Parameter( ? )를 지정해 줘야한다. ( 사용자는 사용자 정보가 존재하지 않을 수 있기 때문 )

사용자 정보 ( UserInfos ) 모델에서 설정한 부분을 자세히 살펴보자.

 ● Users

  • 일반적인 Int, String과 같은 타입이 아닌, 참조할 다른 모델을 지정한다.
  • 사용자 ( Users ) 모델을 참조하므로 Users로 작성되어있다

 ● fields

    ● 사용자 정보 ( UserInfos ) 모델에서 사용할 외래키 ( Forien Key ) 컬럼을 지정한다.

        ● 여기서는 userId 컬럼으로 외래키를 지정했다.

 ● references

    ●  key : 참조하는 다른 모델의 Column를 지정한다.

        ●  여기서는 사용자 ( Users ) 모델의 userId 컬럼을 참조한다.

● onDelete | onUpdate

    ● 참조하는 모델이 삭제 or 수정될 경우 어떤 행위를 할 지 설정한다.

    ● Cascade 옵션을 선택해 사용자가 삭제될 경우 그에 연결된 사용자 정보도 함께 삭제되도록 설정했다.

 

Prisma 1:N 연관 관계

요구사항 중 "사용자 ( Users )는 여러개의 게시글 ( Posts )을 등록할 수 있다." 에서 사용자와 게시글 모델의 경우 1:N 관계를 가지고 있는 것을 확인할 수 있다. 이번에도 2가지의 모델의 Prisma model에서 어떻게 관계를 설정하는지 확인해보자.

 

먼저 게시글 ( Posts ) model에서 관계를 설정하는 부분을 살펴보자.

// schema.prisma

model Users {
  userId    Int      @id @default(autoincrement()) @map("userId")
  email     String   @unique @map("email")
  password  String   @map("password")
  createdAt DateTime @default(now()) @map("createdAt")
  updatedAt DateTime @updatedAt @map("updatedAt")

  userInfos UserInfos? // 사용자(Users) 테이블과 사용자 정보(UserInfos) 테이블이 1:1 관계를 맺습니다.
  posts     Posts[] // 사용자(Users) 테이블과 게시글(Posts) 테이블이 1:N 관계를 맺습니다.

  @@map("Users")
}

model Posts {
  postId    Int      @id @default(autoincrement()) @map("postId")
  userId    Int      @map("userId") // 사용자(Users) 테이블을 참조하는 외래키
  title     String   @map("title")
  content   String   @map("content") @db.Text
  createdAt DateTime @default(now()) @map("createdAt")
  updatedAt DateTime @updatedAt @map("updatedAt")

  // Users 테이블과 관계를 설정합니다.
  user     Users     @relation(fields: [userId], references: [userId], onDelete: Cascade)

  @@map("Posts")
}

 

사용자 ( Users ) 모델과 게시글 ( Posts ) 모델은 1:N 관계를 가지고 있다.

여기서 1:N 관계랑 한 사용자는 여러개의 게시글을 작성할 수 있다는 것을 의미한다.

 

이런 1:N 관계를 설정할 때는 다음과 같은 내용을 포함해야 한다.

  1. 관계를 설정하려는 모델 ( Posts )에서 어떤 모델과 관계를 맺을지 ( Users ) 설정해야한다.
  2. 관계를 맺게되는 모델 ( Users )에서 어떤 모델이 관계를 맺는지 ( Posts ) 설정해야한다.
  3. 관계를 맺게되는 모델 ( Users )에서 타입을 지정할 때, 배열 연산자 ( [] )를 작성해줘야한다. ( 사용자는 여러개의 게시글을 가질 수 있기 때문 )

현재 게시글 모델의 경우 작성한 사용자가 회원 탈퇴 ( onDelete )하게 될 경우 작성한 모든 게시글이 삭제되도록 구현되어 있다. 이런 설정은 @relation 어노테이션을 사용해 지정한다.

// Users 테이블과 관계를 설정합니다.
user     Users     @relation(fields: [userId], references: [userId], onDelete: Cascade)

 

여기서 User는 게시글 ( Posts )이 참조하는 다른 모델을 지정하고, fields는 게시글 ( Posts ) 모델에서 사용할 외래키 컬럼을 지정한다. references는 참조하는 다른 모델의 컬럼을 지정하고, onDelete는 참조하는 모델이 삭제될 경우 어떤 행위를 할 지 설정한다.

 

onDelete의 경우, Cascade 옵션으로 사용자가 삭제될 경우 연관된 게시글 또한 삭제되도록 설정했다.

 

댓글(Comments) 또한, 게시글 ( Posts )과 마찬가지로 사용자 ( Users ) 모델과 1:N 관계를 가지고 있다.

댓글 ( Comments ) model에서 관계를 설정하는 부분을 살펴보자.

model Users {
  userId    Int      @id @default(autoincrement()) @map("userId")
  email     String   @unique @map("email")
  password  String   @map("password")
  createdAt DateTime @default(now()) @map("createdAt")
  updatedAt DateTime @updatedAt @map("updatedAt")

  userInfos UserInfos? // 사용자(Users) 테이블과 사용자 정보(UserInfos) 테이블이 1:1 관계를 맺습니다.
  posts     Posts[] // 사용자(Users) 테이블과 게시글(Posts) 테이블이 1:N 관계를 맺습니다.
  comments  Comments[] // 사용자(Users) 테이블과 댓글(Comments) 테이블이 1:N 관계를 맺습니다.

  @@map("Users")
}

model Posts {
  postId    Int      @id @default(autoincrement()) @map("postId")
  userId    Int      @map("userId") // 사용자(Users) 테이블을 참조하는 외래키
  title     String   @map("title")
  content   String   @map("content") @db.Text
  createdAt DateTime @default(now()) @map("createdAt")
  updatedAt DateTime @updatedAt @map("updatedAt")

  // Users 테이블과 관계를 설정합니다.
  user     Users      @relation(fields: [userId], references: [userId], onDelete: Cascade)
  comments Comments[] // 게시글(Posts) 테이블과 댓글(Comments) 테이블이 1:N 관계를 맺습니다.

  @@map("Posts")
}

model Comments {
  commentId Int      @id @default(autoincrement()) @map("commentId")
  postId    Int      @map("postId") // 게시글(Posts) 테이블을 참조하는 외래키
  userId    Int      @map("userId") // 사용자(Users) 테이블을 참조하는 외래키
  content   String   @map("content")
  createdAt DateTime @default(now()) @map("createdAt")
  updatedAt DateTime @updatedAt @map("updatedAt")

  // Posts 테이블과 관계를 설정합니다.
  post Posts @relation(fields: [postId], references: [postId], onDelete: Cascade)
  // Users 테이블과 관계를 설정합니다.
  user Users @relation(fields: [userId], references: [userId], onDelete: Cascade)

  @@map("Comments")
}

 

게시판 프로젝트 최종 Prisma model

// schema.prisma

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

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

model Users {
  userId    Int      @id @default(autoincrement()) @map("userId")
  email     String   @unique @map("email")
  password  String   @map("password")
  createdAt DateTime @default(now()) @map("createdAt")
  updatedAt DateTime @updatedAt @map("updatedAt")

  userInfos UserInfos? // 사용자(Users) 테이블과 사용자 정보(UserInfos) 테이블이 1:1 관계를 맺습니다.
  posts     Posts[] // 사용자(Users) 테이블과 게시글(Posts) 테이블이 1:N 관계를 맺습니다.
  comments  Comments[] // 사용자(Users) 테이블과 댓글(Comments) 테이블이 1:N 관계를 맺습니다.

  @@map("Users")
}

model Posts {
  postId    Int      @id @default(autoincrement()) @map("postId")
  userId    Int      @map("userId") // 사용자(Users) 테이블을 참조하는 외래키
  title     String   @map("title")
  content   String   @map("content") @db.Text
  createdAt DateTime @default(now()) @map("createdAt")
  updatedAt DateTime @updatedAt @map("updatedAt")

  // Users 테이블과 관계를 설정합니다.
  user     Users      @relation(fields: [userId], references: [userId], onDelete: Cascade)
  comments Comments[] // 게시글(Posts) 테이블과 댓글(Comments) 테이블이 1:N 관계를 맺습니다.

  @@map("Posts")
}

model UserInfos {
  userInfoId   Int      @id @default(autoincrement()) @map("userInfoId")
  userId       Int      @unique @map("userId") // 사용자(Users) 테이블을 참조하는 외래키
  name         String   @map("name")
  age          Int?     @map("age")
  gender       String   @map("gender")
  profileImage String?  @map("profileImage")
  createdAt    DateTime @default(now()) @map("createdAt")
  updatedAt    DateTime @updatedAt @map("updatedAt")

  // Users 테이블과 관계를 설정합니다.
  user Users @relation(fields: [userId], references: [userId], onDelete: Cascade)

  @@map("UserInfos")
}

model Comments {
  commentId Int      @id @default(autoincrement()) @map("commentId")
  postId    Int      @map("postId") // 게시글(Posts) 테이블을 참조하는 외래키
  userId    Int      @map("userId") // 사용자(Users) 테이블을 참조하는 외래키
  content   String   @map("content")
  createdAt DateTime @default(now()) @map("createdAt")
  updatedAt DateTime @updatedAt @map("updatedAt")

  // Posts 테이블과 관계를 설정합니다.
  post Posts @relation(fields: [postId], references: [postId], onDelete: Cascade)
  // Users 테이블과 관계를 설정합니다.
  user Users @relation(fields: [userId], references: [userId], onDelete: Cascade)

  @@map("Comments")
}

 

Prisma DB, Table 생성

.env 설정

# .env

DATABASE_URL="mysql://root:aaaa4321@express-database.qeradf.ap-northeast-2.rds.amazonaws.com:3306/community_hub"

 

자신이 대여중인 아마존 RDS EndPoint를 참조해서 경로를 설정한다.

 

아래 명령어를 입력해 db와 앞서 설계한 테이블을 생성한다.

# 해당 프로젝트에 schema.prisma에 정의된 테이블을 MySQL에 생성합니다.
npx prisma db push

 

생성 모습

 

Prisma 라이브러리 설치

# yarn 프로젝트를 초기화합니다.
yarn init -y

# express, prisma, @prisma/client 라이브러리를 설치합니다.
yarn add express prisma @prisma/client

# nodemon 라이브러리를 DevDependency로 설치합니다.
yarn add -D nodemon

# 설치한 prisma를 초기화 하여, prisma를 사용할 수 있는 구조를 생성합니다.
npx prisma init

 

위 명령문을 통해 Prisma를 설치할 수 있다.

  • prisma는 Prisma를 터미널에서 사용할 수 있도록 도구를 설치하는 패키지다
  • @prisma/client는 Node.js에서 Prisma를 사용할 수 있게 해준다.
  • nodemon은 개발 코드가 변경되었을 때 자동으로 서버를 재시작 해주는 패키지다.

 

npx prisam init

내 프로젝트 폴더 이름
├── prisma
│   └── schema.prisma
├── .env
├── .gitignore
├── package.json
└── yarn.lock

 1. prisam 폴더 안에 prisma.schema 파일 생성

  • 이 파일은 Prisma가 사용할 데이터베이스를 설정하기 위해사용하는 파일이므로 지우면 안된다.

 2. root 폴더에 .env 파일 생성 

  • 이 파일은 외부에 공유되어선 안되는 비밀 정보들이 저장되어 있는 파일이다.

 3. root 폴더에 .gitignore 파일 생성

  • .env 파일이 git에 업로드 되지 않도록 설정 되어 있다.

여기서 생성된 폴더나 파일들을 임의로 옮기지 않아야 한다. Prisam는 정해진 경로에 있는 파일을 사용하고 저장하기 때문에 임의로 옮기면 오동작 할 가능성이 높다.

 

nodemon 라이브러리

파일을 저장할 때마다 변경 사항을 감지하고, 자동으로 서버를 재시작해 주는 라이브러리다. 개발 중 변경사항을 즉시 반영해 개발 효율성을 향상시킬 수 있다.

 

앞서 매번 코드를 수정하거나 서버에서 에러가 발생해 종료되었을 때, 매번 서버를 재시작해야했다.

이전에는 이를 위해 node app.js 명령어를 이용해 서버를 수동으로 재시작했는데, 이런 방법은 너무 번거로운 작업이다.

이런 상황을 nodemon 이라는 도구를 이용해 해결할 수 있다.

 

▶ nodemon 명령어

  • nodemon 으로 서버를 실행하기 위해서 아래와 같이 사용한다.
# 형식
nodemon <실행할 JavaScript 파일명>

# nodemon을 이용해 app.js 파일 실행하기
nodemon app.js

 

단순히 터미널에 명령어를 사용하는 것 뿐 아니라, package.json 에 nodemon 을 이용해 서버를 실행하는 스크립트 ( scripts )를 등록하면, 매번 명령어를 입력하지 않아도 간편하게 서버를 시작할 수 있다.

 

아래와 같이 package.json을 수정

// package.json

...

"scripts": {
	"dev": "nodemon app.js"
},

 

터미널에서 yarn run dev 명령어를 실행하면, nodemon을 이용해 서버를 시작할 수 있게 된다.


schema.prisma

Prisma가 사용할 데이터베이스의 설정 정보를 정의하기 위해 사용하는 파일이다.

Prisma를 가장 처음 초기화 하였을 때, prisma.schema 파일을 확인한다면, 아래의 2가지 구문이 작성되어 있는 것을 확인할 수 있다.

 ● datasource

  • 데이터베이스에 대한 정의를 하기 위해 사용된다.
  • Prisma가 어떤 데이터베이스 엔진을 사용할 것인지, 데이터베이스의 위치 ( URL )는 어디인지 등의 정보를 정의하는데 사용된다.

 ● generator

  • Prisma 클라이언트를 생성하는 방식을 설정하는 구문이다.

 

Prisma datasource

Prisma가 데이터베이스를 연결할 수 있도록 설정하고, 관리하는 데 필요한 정보를 설정하는 구문이다.

우선 Prisma는 연결하려는 데이터베이스의 속성을 schema.prisma 파일에서 관리하고 있다.

여기서 datasource 프로퍼티에 정의한 속성들을 수정해 사용자 아이디, 비밀번호, 엔드 포인트 등 다양한 설정값을 입력해주어야 한다.

 

datasource 설정

// schema.prisma

datasource db {
  // MySQL 데이터베이스 엔진을 사용합니다.
  provider = "mysql"
  // 데이터베이스 연결 정보를 .env 파일의 DATABASE_URL 로부터 읽어옵니다.
  url      = env("DATABASE_URL")
}

 

처음 생성된 datasource 구문은 위와 같이 작성되어 있다. 각 프로퍼티들은 아래와 같은 속성을 가지고 있다.

 1. provider : Prisma 가 사용할 데이터베이스 엔진의 유형

 2. url : 데이터베이스를 연결하기 위한 URL

 

url 부분에서 env("DATABASE_URL") 방식으로, 데이터베이스의 주소가 노출되지 않게 작성하는 dotenv 의 문법을 사용하고 있다. env() 문법은 프로젝트의 root 폴더에 있는 .env 파일에 정의되어 있는 정보를 해당 schema.prisma 파일로 불러오는 것이다.

여기서, dotenv 는 어플리케이션의 환경 변수를 관리하는 모듈이다. 실제 코드에서 민감한 정보를 노출시키지 않도록 보호해주고, 개발 환경에 따라 다르게 설정해야 하는 값을 별도의 파일에서 관리할 수 있게 해준다.

 

env 파일 살펴보기

# .env
DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"

 

Prisma를 초기화 하고, .env 파일을 확인하면, 위와 같은 내용을 확인할 수 있다.

.env 파일은 key-value의 형태로 구성되어 있고, DATABASE_URL 이라는 하나의 변수가 선언되어 있다.

이곳의 URL을 변경하게 되면 해당 데이터베이스와 연결이 가능하다.

 

데이터베이스 URL

데이터베이스 URL은 Prisma가 어떤 데이터베이스와 어떻게 연결할지를 알려주는 중요한 정보다. 

URL 내부에는 데이터베이스 엔진 유형, 사용자 아이디, 패스워드와 같은 정보가 포함된다.

.env 파일의 DATABASE_URL에서 확인한 것처럼, 데이터베이스와 연결하기 위해선 URL을 생성해야 한다.

DATABASE_URL은 어떻게 구성되어 있는지, 그리고 어떻게 구성하는지를 확인해보도록 하자.

 

AWS RDS를 대여해 받아온 RDS의 엔드 포인트, 사용자 아이디, 비밀번호, Port 번호를 바탕으로 Prisma와 연결하기 위한 URL을 작성해보도록 하자.

Database URL

 

데이터베이스 URL은 크게 4가지로 나뉘어진다.

 ● Protocol

  • Prisma가 사용할 데이터베이스 엔진을 나타낸다.
  • postgresql, sqllite, mysql과 같은 데이터베이스 엔진을 정의한다.

 ● Base URL

  • 데이터베이스의 엔드 포인트와 아이디, 패스워드, 포트 번호를 나타낸다.
  • <Id>:<Password>@<RDS Endpoint>:<Port> 의 형태로 구성된다.

 ● Path

  • MySQL에서 사용할 데이터베이스 이름을 설정하는 구성 요소다.

 ● Arguments

  • Prisma에서 데이터베이스 연결을 설정하는데 필요한 추가 옵션을 나타낸다.
  • 데이터베이스와 연결할 수 있는 최대 커넥션 갯수, 타임아웃 시간 등이 있다.

 

Prisma의 데이터베이스 URL 구현해보기

 

AWS RDS에 접속하기 위한 URL의 속성값은 아래와 같다.

  • 데이터베이스 엔진 : mysql
  • 마스터 사용자 이름 : root
  • 마스터 암호 : aaaa4321
  • RDS 엔드포인트 : express-database.clxfpepff75.ap-northeast-2.rds.zmazonaws.com
  • Port 번호 : 3306
  • 사용할 DB 이름 : prisma_crud
mysql://root:aaaa4321@express-database.clxfpepff75.ap-northeast-2.rds.zmazonaws.com:3306/prisma_crud

 

.env 파일의 DATABASE_URL을 위 작성한 내용을 입력해 변경한다.

# .env

DATABASE_URL="mysql://root:aaaa4321@express-database.clxfpepff75.ap-northeast-2.rds.zmazonaws.com:3306/prisma_crud"

 


Prisma model

Prisma의 model 구문은 특정 Table과 Column의 속성값을 입력해, 데이터베이스와 Express 프로젝트를 연결 시켜준다.

  • model 구문은 Prisma를 사용할 때 가장 많이 작성하게 될 구문이며, Prisma가 사용할 데이터베이스의 테이블 구조를 정의하기 위해 사용된다.
  • schema.prisma 파일에서 model에 작성된 정보를 바탕으로 Prisma Client를 통해 Javascript에서 MySQL의 테이블을 조작할 수 있게 된다.
  • model 구문은 Javascript에서 MySQL의 테이블을 사용하기 위한 다리 역할을 수행하며, MySQL과 실제 연결되어 사용할 수 있게 도와준다.

 

그렇다면, 상품 ( Products )을 담당하는 테이블을 어떻게 구현하는지 확인해보자.

Products 테이블 예시

// schema.prisma

model Products {
  productId   Int     @id @default(autoincrement()) @map("productId")
  productName String  @unique @map("productName")
  price       Int     @default(1000) @map("price")
  info        String? @map("info") @db.Text

  createdAt DateTime @default(now()) @map("createdAt")
  updatedAt DateTime @updatedAt @map("updatedAt")

  @@map("Products")
}

 

  • 데이터 유형은 각 필드의 데이터를 어떤 형식으로 저장할 것인지 결정하게 된다.
  • Prisma에서 다양한 데이터 유형을 지원하는데, 위 예시에서 Int, String, DateTime 등의 데이터 유형이 사용된다.
  • 데이터 유형 뒤에 ? 가 붙으면, NULL을 허용하는 컬럼이 된다.
  • SQL에서 사용하는 것과 동일하게, UNIQUE 제약 조건과 AUTO_INCREMENT 제약조건을 사용할 수 있다.
  • 왼쪽에 있는 이름은 Node.js에서 해당 Prisma를 사용할 때 쓰는 이름이고, 오른쪽에 있는 @map("")는 데이터베이스 상에 기록될 이름을 말한다.
  • String이면 기본적으로는 VARCHAR을 의미하는데 @db.Text를 붙이면 Text 타입을 가진다.
  • updatedAt은 자동으로 업데이트 된 시간을 저장한다. 
  • @@map("Products")는 Products 테이블을 MySQL에서 Products 란 이름으로 사용한다는 의미다. ( @@map() 을 작성하지 않으면, 테이블명의 대문자는 전부 소문자로 치환된다 )

Products 테이블의 요구사항

Name 타입 ( Type ) NULL 제약 조건 default
productId ( PK ) INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT
productName STRING NOT NULL UNIQUE  
price INTEGER NOT NULL   1000
info TEXT NULL    
createdAt DATETIME NOT NULL   현재 시간
updatedAt DATETIME NOT NULL   현재 시간

 

Products 테이블의 생성 .sql 파일

-- CreateTable
CREATE TABLE `Products` (
    `productId` INTEGER NOT NULL AUTO_INCREMENT,
    `productName` VARCHAR(191) NOT NULL,
    `price` INTEGER NOT NULL DEFAULT 1000,
    `info` TEXT NULL,
    `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
    `updatedAt` DATETIME(3) NOT NULL,

    UNIQUE INDEX `Products_productName_key`(`productName`),
    PRIMARY KEY (`productId`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
  • string 타입은 VARCHAR(191) 의 형식을 가진다.
  • DateTime 타입은 DATETIME(3) 의 형식을 가진다.
  • Text? 타입은 Text 타입과 함께, NULL 제약 조건을 가진다.

 

Posts 테이블 요구사항

Name 타입 ( Type ) NULL 제약 조건 default
postId ( PK ) INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT
title STRING NOT NULL    
content TEXT NOT NULL    
password STRING NOT NULL    
createdAt DATETIME NOT NULL   현재 시간
updateAt DATETIME NOT NULL   현재 시간

 

// schema.prisma

model Posts {
  postId    Int      @id @default(autoincrement()) @map("postId")
  title     String   @map("title")
  content   String   @map("content") @db.Text
  password  String   @map("password")
  createdAt DateTime @default(now()) @map("createdAt")
  updatedAt DateTime @updatedAt @map("updatedAt")

  @@map("Posts")
}

 

 

Prisma DB, Table 생성하기

# schema.prisma 파일에 설정된 모델을 바탕으로 MySQL에 정보를 업로드합니다.
npx prisma db push

 

위 Prisma CLI 명령어를 이용해 schema.prisma 파일에 정의한 내용을 바탕으로 새로운 DB와 테이블을 생성한다.

생성된 모습


Prisma CLI 더 알아보기

 ● prisma db push

  • schema.prisma 파일에 정의한 설정값을 실제 데이터베이스에 반영한다.
  • 내부적으로 prisma generate가 실행된다.
  • 데이터베이스 구조를 변경하거나 새로운 테이블을 생성할 수 있다.

 ● prisma_init

  • Prisma를 사용하기 위한 초기 설정을 생성한다.
  • 이 명령어를 실행하면 schema.prisma 파일과 같은 필요한 설정 파일들이 생성된다.

 ● prisma generate

  • Prisma Client를 생성하거나 업데이트 한다.
  • 대표적으로, schema.prisma 파일에 변경 사항이 생겼거나, 데이터베이스 구조가 변경되었을 때, 이 명령어를 사용해 Prisma Client를 최신 상태로 유지할 수 있다.

 ● prisma db pull

  • 현재 연결된 데이터베이스의 구조를 prisma.schema 파일로 가져온다. ( pull )
  • 데이터베이스에서 구조 변경이 발생했을 때, 이 명령어를 사용하면 Prisma Schema를 최신 상태로 유지할 수 있다.
  • 이후 prisma generate 명령어를 사용해 변경 사항을 Prisma Client에 반영할 수 있다.

더욱 다양한 Prisma CLI 명령어 https://www.prisma.io/docs/reference/api-reference/command-reference#synopsis

 

Prisma CLI reference | Prisma Documentation

This page gives an overview of all available Prisma CLI commands, explains their options and shows numerous usage examples.

www.prisma.io


Prisma Client

 

Prisma는 model을 generate 하면, 해당 모델에 대한 정보가 node_modules 폴더 내에 있는 Prisma Client에 전달된다.

( prisma db push도 내부적으로 generate 가 실행된다. )

Prisma Client는 Prisma Schema에 정의한 데이터베이스 모델 ( model )을 TypeScript 코드로 변환해, 개발자가 데이터베이스와 상호작용할 수 있게 해준다. 이러한 과정을 통해, 데이터베이스를 JavaScript에서 손쉽게 다룰 수 있게 되고, Prisma Schema와 동기화된 Prisma Client를 이용해 데이터베이스를 사용할 수 있게 된다.

 

Prisma Client 확인해보기

// node_modules/.prisma/client/index.d.ts

export type ProductsPayload<ExtArgs extends $Extensions.Args = $Extensions.DefaultArgs> = {
  name: "Products"
  objects: {}
  scalars: $Extensions.GetResult<{
    productId: number
    productName: string
    price: number
    info: string | null
    createdAt: Date
    updatedAt: Date
  }, ExtArgs["result"]["products"]>
  composites: {}
}

/**
 * Model Products
 * 
 */
export type Products = runtime.Types.DefaultSelection<ProductsPayload>

 

  • schema.prisma 파일에 정의한 내용처럼, Products 테이블에 대한 내용이 위와 같이 작성되어 있다.
  • schema.prisma 정의한 내용을 prisma generate를 이용해 index.d.ts에 추가하고 최종적으로는 index.d.ts에 있는 내용을 바탕으로 prisma를 구성한다.

 

Prisma Method

Prisma는 mongoose와 동일하게, findMany(), findFirst(), findUnique() 등 다양한 메서드를 지원한다.

mongoose를 사용했을 때는 Schema를 이용해 DB를 사용했다면, Prisma에서는 Prisma Client를 이용해 MySQL의 데이터를 조작한다.

 

Posts 테이블의 구조를 살펴보자.

Posts 테이블

 

Posts 테이블은 게시글 제목 ( title ), 내용 ( content ), 비밀번호 ( password ) 총 3개의 컬럼을 가지고 있고 postId, createdAt, updatedAt 컬럼은 아무런 데이터를 입력하지 않더라도 기본값을 가질 수 있도록 구성되어 있다.

 

그러면 게시글을 생성 및 수정할 때 필수 인자값 3개를 이용해 권한 검증 및 데이터 생성을 구현해보자.

 

API를 구현하기 앞서 routes/posts.router.js 파일을 생성하고 express 프로젝트를 초기화 하자.

// routes/posts.router.js

import express from 'express';
import { PrismaClient } from '@prisma/client';

const router = express.Router(); // express.Router()를 이용해 라우터를 생성한다.
const prisma = new PrismaClient({
  // Prisma를 이용해 데이터베이스를 접근할 때, SQL을 출력해준다.
  log: ['query', 'info', 'warn', 'error'],

  // 에러 메시지를 평문이 아닌, 개발자가 읽기 쉬운 형태로 출력해준다.
  errorFormat: 'pretty',
}); // PrismaClient 인스턴스를 생성한다.

export default router;

 

// app.js

import express from 'express';
import PostsRouter from './routes/posts.router.js';

const app = express();
const PORT = 3017;

app.use(express.json());
app.use('/api', [PostsRouter]);

app.listen(PORT, () => {
  console.log(PORT, '포트로 서버가 열렸어요!');
});


Prisma 게시글 생성 ( Create ) API

게시글 생성 API의 비즈니스 로직

  1. title, content, password 를 body로 전달받는다.
  2. title, content, password 를 이용해 Posts 테이블에 데이터를 삽입 한다.
  3. 생성된 게시글을 반환한다.

Prisma 게시글 생성 API

// routes/posts.router.js

// 게시글 생성
router.post('/posts', async (req, res, next) => {
  const { title, content, password } = req.body;
  const post = await prisma.posts.create({
    data: {
      title,
      content,
      password,
    },
  });

  return res.status(201).json({ data: post });
});

 

create 메서드를 이용해 데이터를 생성한다.

 

Prisma 게시글 조회 ( Read ) API

게시글 조회 API는 게시글 목록 조회, 게시글 상세 조회 2개의 API로 구현할 수 있다.

게시글 목록 조회 API의 경우 게시글의 내용 ( content )을 제외하고,

게시글 상세 조회 API의 경우에만 게시글의 전체 내용이 출력되도록 만들어 볼 예정

 

Prisma 게시글 목록 조회 API

// routes/posts.router.js

/** 게시글 전체 조회 API **/
router.get('/posts', async (req, res, next) => {
  const posts = await prisma.posts.findMany({
    select: {
      postId: true,
      title: true,
      createdAt: true,
      updatedAt: true,
    },
  });

  return res.status(200).json({ data: posts });
});

 

 

Prisma 게시글 상세 조회 API

// routes/posts.router.js

/** 게시글 상세 조회 API **/
router.get('/posts/:postId', async (req, res, next) => {
  const { postId } = req.params;
  const post = await prisma.posts.findFirst({
    where: { postId: +postId },
    select: {
      postId: true,
      title: true,
      content: true,
      createdAt: true,
      updatedAt: true,
    },
  });

  return res.status(200).json({ data: post });
});

 

게시글 목록 조회 API는 findMany() 메서드를 이용해 Posts 테이블이 가지고 있는 모든 데이터들을 배열의 형태로 조회한다.

게시글 상세 조회 API는 특정한 게시글 1개만 출력해야 하니까 findFirst() 메서드를 이용해 Posts 테이블에 특정한 데이터 1개를 조회한다.

 

select의 역할

Prisma는 schema.prisma model에 설정한 정보를 바탕으로 해당하는 SQL을 생성한다.

게시글 목록 조회 API의 응답 ( response )은 postId, title, createdAt, updatedAt 4가지의 컬럼만 출력되어야 한다.

만약 select 속성을 사용하지 않을 경우 전체 목록에서 출력되지 않아야하는 password, content 컬럼이 출력되게 된다.

 

postId

postId는 변수 명 앞에, + 연산자가 붙은 경우, 문자열 타입을 숫자형 타입으로 변환해준다.

postId를 타입 변환하지 않은 상태로 데이터를 조회하려 하면 아래와 같은 에러가 발생할 수 있다.

Argument Error

 

위 에러는 숫자형 컬럼을 문자열 데이터로 조회했기 때문에 발생하는 에러다.

Posts 테이블을 schema.prisma model에서 정의할 때, postId 컬럼을 INTEGER로 선언했다. 

따라서 해당하는 정ㅈ보를 조회할 때에도 숫자형 타입으로 조회를 해야하는 것.

 

postId와 같은 방법 이 외에도 아래와 같은 방법으로 타입 변환을 사용할 수 있다.

where: { postId: parseInt(postId) },

다만, 이 방법은 변환 함수가 명시적인 점이 장점이긴 하지만, 표현하기에 불편하다는 문제점이 있다.

+ 연산자를 사용하면 이를 좀 더 간결하게 표현할 수 있다.

 

Prisma의 조회 메서드 좀더 자세한 정보 : https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#model-queries

 

Prisma Client API | Prisma Documentation

API reference documentation for Prisma Client.

www.prisma.io

 

Prisma 게시글 수정 ( Update ) API

게시글 수정 API의 비즈니스 로직

  1. Path Parameters로 어떤 게시글을 수정할 지 postId를 전달받는다.
  2. 변경할 title, content와 권한 검증을 위한 password를 body로 전달받는다.
  3. postId를 기준으로 게시글을 검색하고, 게시글이 존재하는지 확인한다.
  4. 게시글이 조회되었다면 해당하는 게시글의 password가 일치하는지 확인한다.
  5. 모든 조건을 통과했으면 게시글을 수정한다.
// routes/posts.router.js

/** 게시글 수정 API **/
router.put('/posts/:postId', async (req, res, next) => {
  const { postId } = req.params;
  const { title, content, password } = req.body;

  const post = await prisma.posts.findUnique({
    where: { postId: +postId },
  });

  if (!post)
    return res.status(404).json({ message: '게시글이 존재하지 않습니다.' });
  else if (post.password !== password)
    return res.status(401).json({ message: '비밀번호가 일치하지 않습니다.' });

  await prisma.posts.update({
    data: { title, content },
    where: {
      postId: +postId,
      password,
    },
  });

  return res.status(200).json({ data: '게시글이 수정되었습니다.' });
});

 

게시글 수정 API의 경우 모든 비즈니스 로직을 수행한 이후 데이터를 수정하도록 구현했다.

게시글을 수정하는 update 메서드에서 특정한 게시글을 바로 수정하는 것이 아니라, 한번 더 postId, password를 검증해 안전하게 게시글을 수정하는 것을 확인할 수 있다.

 

where 속성의 조건

SQL에서는 특정한 데이터를 검출하기 위해 where절에서 OR, AND, LIKE, 정규표현식 등 다양한 연산자를 사용할 수 있다.

Prisma의 where절은 여러개의 조건이 들어올 경우 AND 연산자를 사용한 것과 동일한 결과를 출력해준다.

이외의 연산자를 사용하고 싶으면, 아래와 같은 문법으로도 사용할 수 있다.

await prisma.users.findMany({
  where: {
    OR: [
      {
        email: {
          endsWith: 'prisma.io',
        },
      },
      { email: { endsWith: 'gmail.com' } },
    ],
    NOT: {
      email: {
        endsWith: 'hotmail.com',
      },
    },
  },
})

 

Prisma의 논리 연산자에 대한 자세한 정보 : https://www.prisma.io/docs/concepts/components/prisma-client/filtering-and-sorting#combining-operators

 

Filtering and Sorting (Concepts) | Prisma Documentation

Use Prisma Client API to filter records by any combination of fields or related record fields, and/or sort query results.

www.prisma.io

 

Prisma 게시글 삭제 ( Delete ) API

게시글 삭제 API는 게시글 수정 API와 동일한 로직을 수행하지만 Body에서 password만 전달받는 것이 유일한 차이다.

 

게시글 삭제 API의 비즈니스 로직

  1. Path Parameters로 어떤 게시글을 수정할 지 postId를 전달받는다.
  2. 권한 검증을 위한 password를 body로 전달받는다.
  3. postId를 기준으로 게시글을 검색하고, 게시글이 존재하는지 확인한다.
  4. 게시글이 조회되었다면 해당하는 게시글의 password가 일치하는지 확인한다.
  5. 모든 조건을 통과하면 게시글을 삭제한다.
// routes/posts.router.js

/** 게시글 삭제 API **/
router.delete('/posts/:postId', async (req, res, next) => {
  const { postId } = req.params;
  const { password } = req.body;

  const post = await prisma.posts.findFirst({ where: { postId: +postId } });

  if (!post)
    return res.status(404).json({ message: '게시글이 존재하지 않습니다.' });
  else if (post.password !== password)
    return res.status(401).json({ message: '비밀번호가 일치하지 않습니다.' });

  await prisma.posts.delete({ where: { postId: +postId } });

  return res.status(200).json({ data: '게시글이 삭제되었습니다.' });
});

 


 

Prisma 리팩토링

PrismaClient 는 Prisma를 사용해 실제 데이터베이스와의 연결을 관리하는 객체다.

new PrismaClient() 를 이용해 Javascript에서 Prisma를 사용할 수 있도록 인스턴스를 생성하게 된다.

const prisma = new PrismaClient();

 

앞서 작성한 게시글 ( Posts ) 라우터만 구현했지만, 이후에 사용자 ( Users ), 사용자 정보 ( UserInfos ), 해시 태그 ( HashTags )와 같은 여러 라우터들이 추가된다면, 각각의 라우터 갯수마다 데이터베이스와 연결하게 되는 문제가 발생한다.

여러번 데이터베이스의 연결을 생성한다면, 리소스가 과도하게 사용되고, 그로인해 어플리케이션의 성능이 저하될 수 있다. 따라서, 최대한 데이터베이스의 연결을 줄이는 것이 효율적인 방법이다.

이런 문제를 해결하기 위해 /utils/prisma/index.js 파일을 구현해, 하나의 파일에서 데이터베이스 커넥션을 관리해 최초로 1번만 MySQL과 커넥션을 생성하도록 코드를 구현하면 된다.

 

utils/prisma/index.js Prisma 리팩토링

// utils/prisma/index.js

import { PrismaClient } from '@prisma/client';

export const prisma = new PrismaClient({
  // Prisma를 이용해 데이터베이스를 접근할 때, SQL을 출력해줍니다.
  log: ['query', 'info', 'warn', 'error'],

  // 에러 메시지를 평문이 아닌, 개발자가 읽기 쉬운 형태로 출력해줍니다.
  errorFormat: 'pretty',
}); // PrismaClient 인스턴스를 생성합니다.

 

'데이터베이스 > 실습' 카테고리의 다른 글

[DATABASE][실습] Raw Query 실습  (0) 2024.09.06

+ Recent posts