2024-09-12 CH-3 개인과제 (아이템 시뮬레이터 구현) 필수기능 구현 2

MOON·2024년 9월 12일
0

내일배움캠프 과제

목록 보기
4/36

와우 우여곡절끝에 거의 하루만에 필수기능들을 구현해보았습니다.

오늘만큼은 일찍 자자....

DB 테이블 모델링

일단 제 기준 가장 오래걸린건 테이블 모델링이였습니다.
서로다른 테이블들의 관계를 정리하는 부분과 어떤테이블에는 무슨 값이 들어가면 좋을지에 대해 고민해 보았습니다. 그래서 일단 눈에 보이면 좀 더 이해가 될것 같아 만든 ERD입니다.
확실히 눈에 보이게 만들어 두니 약간 감이 잡혔던거 같습니다.

처음 필수기능 내용만 보고 만든 ERD입니다.

현재 도전기능 내용을보고 수정한 ERD 입니다.

사실이게 맞게 한건지는 잘 모르지만 코드를 수정하면서 맞게끔 구현하게 만들면 될것 같습니다.

필수기능 API

제가 정리한 명세서입니다. 간단하게 작성해 보았습니다.
노션 API 명세서

회원가입 API

router.post('/sign-up', async (req, res, next) => {
  try {
    const { id, password, verifyPassword, name } = req.body;

    if (!id || !password || !verifyPassword || !name)
      return res.status(400).json({ errorMessage: '데이터 형식이 올바르지 않습니다.' });

    // 이미 해당 id로 회원가입했는데 여부확인
    const isExistUser = await prisma.users.findFirst({
      where: {
        id,
      },
    });
    if (isExistUser) {
      return res.status(409).json({ message: '이미 존재하는 아이디입니다.' });
    }

    if (password !== verifyPassword)
      return res.status(400).json({ message: '비밀번호와 비밀번호확인이 다릅니다.' });

    const passwordPattern = /^.{6,}$/;
    if (!passwordPattern.test(password))
      return res.status(400).json({ message: '비밀번호가 6글자 미만입니다.' });

    const idPattern = /^[a-z0-9]+$/;
    if (!idPattern.test(id))
      return res.status(400).json({ message: '아이디는 영문 소문자와 숫자 조합으로만 생성됩니다.' });

    // 비밀번호 해싱 암호화
    const hashedPassword = await bcrypt.hash(password, 10);

    // 트랜잭션
    const user = await prisma.$transaction(
      async (tx) => {
        const { userId } = await tx.users.create({
          data: {
            id: id,
            password: hashedPassword,
            verifyPassword: hashedPassword,
            name: name,
          },
        });

        const user = await tx.users.findFirst({
          where: { userId: userId },
          select: {
            userId: true,
            id: true,
            name: true,
          },
        });
        return user;
      },
      {
        isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted, // DB가 커밋된 후
      },
    );

    return res.status(201).json({ data: user });
  } catch (err) {
    return res.status(500).json({ errorMessage: err.message });
  }
});

계정 로그인 API

여기선 로그인시 헤더로 액세스 토큰을 반환해줍니다.

router.post('/sign-in', async (req, res, next) => {
  try {
    const { id, password } = req.body;

    if (!id || !password) return res.status(400).json({ errorMessage: '데이터 형식이 올바르지 않습니다.' });

    const user = await prisma.users.findFirst({ where: { id } });

    if (!user) return res.status(404).json({ message: '존재하지 않는 아이디입니다.' });
    const passwordCheck = await bcrypt.compare(password, user.password);
    if (!passwordCheck) return res.status(401).json({ message: '비밀번호가 일치하지 않습니다.' });

    const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET_KEY, { expiresIn: '2h' }); // expiresIn 옵션으로 토큰 만료기한 설정

    res.header('authorization', `Bearer ${token}`);
    return res.status(200).json({ message: '로그인에 성공하였습니다.' });
  } catch (err) {
    return res.status(500).json({ errorMessage: err.message });
  }
}); 

캐릭터 생성 API JWT 토큰 확인(인가)

해당 클라이언트가 JWT토큰이 제대로 있는지 체크합니다.

router.post('/characters', authMiddleware, async (req, res, next) => {
  try {
    const { characterName } = req.body;
    const { userId } = req.user;

    if (!characterName || !userId)
      return res.status(400).json({ errorMessage: '데이터 형식이 올바르지 않습니다.' });

    const isExistCharacter = await prisma.characters.findFirst({
      where: {
        characterName,
      },
    });

    if (isExistCharacter) return res.status(409).json({ message: '이미 존재하는 캐릭터 이름입니다.' });

    const character = await prisma.characters.create({
      data: {
        userId: +userId,
        characterName: characterName,
      },
    });

    return res.status(201).json({ characterId: character.characterId });
  } catch (err) {
    return res.status(500).json({ errorMessage: err.message });
  }
});

캐릭터 삭제 API JWT 토큰 확인(인가)

해당 클라이언트가 JWT토큰이 제대로 있는지 체크합니다.

router.delete('/characters/:characterId', authMiddleware, async (req, res, next) => {
  try {
    const { characterId } = req.params;
    const { userId } = req.user;

    if (!characterId || !userId)
      return res.status(400).json({ errorMessage: '데이터 형식이 올바르지 않습니다.' });

    const character = await prisma.characters.findFirst({
      where: {
        characterId: +characterId,
      },
    });
    if (!character || userId !== character.userId)
      return res.status(404).json({ message: '존재하지 않거나 다른 아이디의 캐릭터입니다.' });

    await prisma.characters.delete({
      where: {
        characterId: +characterId,
      },
    });

    return res.status(200).json({ message: `${characterId}번 캐릭터가 삭제 되었습니다.` });
  } catch (err) {
    return res.status(500).json({ errorMessage: err.message });
  }
});

authMiddleware - 인가확인 미들웨어

중간에 계정이 알맞게 로그인이 되었는지 즉, 액세스 토큰을 제대로 발급 받았는지에 대해 인가과정을 거쳐 이 계정이 권한이 있는지를 알아야할때 마다 authMiddleware이 미들웨어를 만들어 요청할때 서버보다 먼저 실행되게 코드를 작성하였습니다.

import jwt from 'jsonwebtoken';
import { prisma } from '../utils/prisma/index.js';
import dotenv from 'dotenv';

dotenv.config();

export default async function (req, res, next) {
  try {
    // console.log(req.headers);
    const { authorization } = req.headers;
    if (!authorization) throw new Error('요청한 사용자의 토큰이 존재하지 않습니다.');

    const [tokenType, token] = authorization.split(' ');
    if (tokenType !== 'Bearer') throw new Error('토큰 타입이 Bearer 형식이 아닙니다.');

    const decodedToken = jwt.verify(token, process.env.JWT_SECRET_KEY);
    // console.log(decodedToken);
    const id = decodedToken.id;

    const user = await prisma.users.findFirst({
      where: { id: id },
    });
    if (!user) throw new Error('토큰 사용자가 존재하지 않습니다.');

    req.user = user;

    next();
  } catch (error) {
    if (error.name === 'TokenExpiredError')
      return res.status(401).json({ message: '토큰이 만료되었습니다.' });
    if (error.name === 'JsonWebTokenError')
      return res.status(401).json({ message: '토큰이 조작되었습니다.' });
    return res.status(400).json({ message: error.message });
  }
}

캐릭터 조회 API

여기선 본인의 캐릭터를 조회하면 money까지 조회합니다.
다른 계정이나 로그인 하지 않았다면 money를 제외하고 조회합니다.

router.get('/characters/:characterId', async (req, res, next) => {
  try {
    const { characterId } = req.params;

    if (!characterId) return res.status(400).json({ errorMessage: '데이터 형식이 올바르지 않습니다.' });

    const isExistCharacter = await prisma.characters.findFirst({
      where: { characterId: +characterId },
    });
    if (!isExistCharacter) return res.status(404).json({ message: '존재하지 않는 캐릭터입니다.' });

    // 헤더로 토큰을 받고 받은 토큰이 해당 로그인한 아이디가 맞으면
    const { authorization } = req.headers;
    if (authorization) {
      const [tokenType, token] = authorization.split(' ');
      if (tokenType === 'Bearer') {
        const decodedToken = jwt.verify(token, process.env.JWT_SECRET_KEY);
        const id = decodedToken.id;

        // id를 가지고 user 찾아오기
        const user = await prisma.users.findFirst({
          where: { id: id },
        });

        // userId가 맞는지 확인 즉, 로그인한 본인의 캐릭터를 조회하는게 맞는지
        if (user.userId === isExistCharacter.userId) {
          const character = await prisma.characters.findFirst({
            where: { characterId: +characterId },
            select: {
              characterName: true,
              health: true,
              power: true,
              money: true,
            },
          });

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

    const character = await prisma.characters.findFirst({
      where: { characterId: +characterId },
      select: {
        characterName: true,
        health: true,
        power: true,
      },
    });

    return res.status(200).json({ character: character });
  } catch (err) {
    return res.status(500).json({ errorMessage: err.message });
  }
});

아이템 생성 API

router.post('/items', async (req, res, next) => {
  try {
    const { itemCode, itemName, itemStat, itemPrice } = req.body;

    if (!itemCode || !itemName || !itemStat || !itemPrice)
      return res.status(400).json({ errorMessage: '데이터 형식이 올바르지 않습니다.' });

    const isExistItem = await prisma.items.findFirst({
      where: {
        itemCode: itemCode,
      },
    });

    if (isExistItem)
      return res.status(409).json({
        message: `해당 코드 아이템은 이미 생성되어 있습니다. itemCode: ${isExistItem.itemCode}, 아이템 이름 : ${isExistItem.itemName}`,
      });

    const item = await prisma.items.create({
      data: {
        itemCode,
        itemName,
        itemStat,
        itemPrice,
      },
    });

    return res.status(201).json({ item: item });
  } catch (err) {
    return res.status(500).json({ errorMessage: err.message });
  }
});

아이템 수정 API

router.patch('/items/:itemCode', async (req, res, next) => {
  try {
    const { itemCode } = req.params;
    const { itemName, itemStat } = req.body;

    if (!itemCode || !itemName || !itemStat)
      return res.status(400).json({ errorMessage: '데이터 형식이 올바르지 않습니다.' });

    const item = await prisma.items.findFirst({
      where: { itemCode: +itemCode },
    });
    if (!item) return res.status(404).json({ message: '해당 아이템이 존재하지 않습니다.' });

    const renewalItem = await prisma.items.update({
      data: {
        itemName,
        itemStat,
      },
      where: {
        itemCode: +itemCode,
      },
    });

    return res.status(200).json({ message: '아이템 정보에 성공허였습니다.', renewalItem: renewalItem });
  } catch (err) {
    return res.status(500).json({ errorMessage: err.message });
  }
});

아이템 목록 조회 API

router.get('/items', async (req, res, next) => {
  try {
    const items = await prisma.items.findMany({
      select: {
        itemCode: true,
        itemName: true,
        itemPrice: true,
      },
    });

    if (!items) return res.status(404).json({ message: '현재 목록에 아이템이 존재하지 않습니다.' });

    return res.status(200).json({ items: items });
  } catch (err) {
    return res.status(500).json({ errorMessage: err.message });
  }
});

// 아이템 상세 조회 API
router.get('/items/:itemCode', async (req, res, next) => {
  try {
    const { itemCode } = req.params;
    const item = await prisma.items.findFirst({
      where: { itemCode: +itemCode },
    });
    if (!item) return res.status(404).json({ message: '해당 아이템이 존재하지 않습니다.' });

    return res.status(200).json({ item: item });
  } catch (err) {
    return res.status(500).json({ errorMessage: err.message });
  }
});

오늘의 회고
익숙하지 않은 코드를 칠려고하니 어색한감이 있었지만
그래도 과제 내용이 강의 내용에 거의 맞게 내주셨더라구요! 그래서 다행히 금방 강의자료들과 구글링을 보면서 코드를 수정하고 직접 오류경험하여 처리하는 등을 하면서 일단 제출시간이 얼마남지않아 필수기능 구현까지하고 제출하였습니다.

제출은 이미 했지만 그래도 도전기능도 구현해볼려고합니다. 이게 재밌을거 같거든요
다음편에 계속...

오늘도 화이팅!

profile
안녕하세요

0개의 댓글