Team Project - Nike Clone Coding (3)

peaceminusone·2022년 2월 5일
0

📲 팀 프로젝트

저번 Wepleshop 1차 프로젝트를 수행 후 두번째 팀 프로젝트입니다. 1차 프로젝트에서의 부족했던 점과 더 배우고 싶었던 점을 깨달았고, 이를 보완하는 방향으로 프로젝트를 진행해보자 하는 생각으로 이번 프로젝트를 진행하였습니다.
이번 클론 코딩 프로젝트의 주제는 Nike 웹사이트 입니다.
나이키는 스포츠웨어 브랜드 중에서 가장 유명한 브랜드라고 해도 과언이 아닐 것입니다. 평소 나이키를 좋아하는 저와 희윤님에 의해 Nike 가 채택되었고, 1차 프로젝트인 마플샵 클론코딩과 같은 웹 쇼핑몰이기에 진행 방향이 비슷했습니다. 차이점은 리스트 페이지 내에서의 다양한 필터링 SNKRS 페이지에서의 응모 및 추첨 기능이었으며, 백엔드를 구현할 저와 준혁님은 각자 이러한 메인 기능(필터링, SNKRS)을 한 가지씩 분담하여 프로젝트를 진행하였습니다.


⭐️ Team : 역할 분담

백엔드 : 이준혁 김영욱
프론트 엔드 : 황희윤, 이진웅

적용 기술

  • Front-End : React.js, Sass
  • Back-End : Node.js, Express, Prisma, nodemon, JWT, Bcrypt, My SQL, CORS
  • Common : RESTful API
  • Community Tools : Slack, Zoom, Notion

저번 프로젝트는 프론트엔드백엔드를 개인이 모두 한가지 이상을 구현하며 Fullstack으로 프로젝트를 진행하였습니다.
1차 프로젝트를 통해 프론트 엔드백엔드 중 어떤 것이 각자 자신에게 적합한지를 파악 가능했으며, 이번 2차 프로젝트는 프론트 엔드백엔드의 역할을 명확히 나누어, 프로젝트를 진행하였습니다.

백엔드 외의 역할이 있었다면, 프론트 엔드단에서 사용할 이미지(제품 이미지)는 순조로운 작업을 위해 백엔드인 저와 준혁님이 자료 수집을 진행하였습니다.
그 외엔 프론트 엔드와 백엔드의 작업들이 모두 역할 분담을 통해 이루어졌습니다.


🐳 Nike Web Page

나이키 공식 홈페이지입니다. 해당 페이지의 큰 범주인 메인 페이지, 리스트 페이지 및 필터 기능, 디테일 페이지 입니다.

(나이키 디테일 페이지 / 출처 : 나이키 공식 홈페이지)


(나이키 리스트 페이지 / 출처 : 나이키 공식 홈페이지)


(나이키 리스트 페이지 - 필터링 (1) / 출처 : 나이키 공식 홈페이지)


(나이키 리스트 페이지 - 필터링 (2) / 출처 : 나이키 공식 홈페이지)


(나이키 디테일 페이지 / 출처 : 나이키 공식 홈페이지)


🐳 Backend - 모델링 이후 기능 구현

준혁님과 호흡을 맞췄던 백엔드 작업입니다. 초기 모델링, 프로젝트 초기 세팅, 데이터 생성 및 활용, 미들웨어 및 API 생성 등의 작업들을 진행하였습니다. 협업을 하면서 서로 부족한 점에 있어서 서로 도울 수 있었던 점이 매우 좋았으며, 처음 같이 작업을 진행하였지만, 합이 꽤나 잘 맞아서 재밌고 즐겁게 프로젝트를 진행할 수 있었습니다.
모델링 작업과 데이터 입력을 모두 마무리 후, 각자 구현할 API에 대한 역할 분담을 진행하였습니다.

⭐️ MVC Pattern & REST API


(MVC Pattern 적용 : Model, Services, Controller 계층화)


routes/index.js

routes/productRouter.js

routes/userRouter.js

routes/snkrsRouter.js

(REST API 적용 : GET, POST, PUT , DELETE 활용)

프로젝트의 모든 진행은 MVC Pattern을 적용한 계층적 구조로 진행하였으며, REST API를 통한 API 구현을 진행하였습니다.


⭐️ 인가 API (장바구니 권한, 리뷰 포스팅, Member Access) 및 미들웨어 구축

<middleware/authorization.js>

import { verify } from 'jsonwebtoken';
import dotenv from 'dotenv';
import { userDao } from '../models';

dotenv.config();

const authentication = (req, res, next) => {
  const token = req.body.user_id; // token화된 user_id 반환
  const validToken = verifyToken(token); // token(user_id) verify
  if (validToken) {
    req.body.user_id = validToken.id[0].id; // verified user_id로 재할당
    next();
  } else {
    res.status(400).send('토큰이 유효하지 않습니다.');
    return;
  }
};

const memberProductBuying = async (req, res, next) => {
  const { user_id, is_member } = req.body;
  const isUserAuthorization = await userDao.isAuthorization(user_id); // 멤버 권한이 있는 유저인지 인가 확인
  if (is_member && !isUserAuthorization) {
    res.status(400).send('멤버 등록이 되지 않은 멤버입니다');
    return;
  } else {
    next();
  }
};

// 토큰 확인 절차
const verifyToken = token => {
  try {
    return verify(token, process.env.salt); //dotenv를 통한 환경변수 설정
  } catch (err) {
    return null;
  }
};

export default { authentication, memberProductBuying };

<userDao.js>

// 유저에게 멤버 권한 부여
const memberAuthorization = async id => { 
  const member = await prisma.$queryRaw`
    UPDATE
      users
    SET
      is_member=1
    WHERE
      id=${id};
  `;

  return member;
};

// 유저가 멤버인지 확인
const isAuthorization = async userId => { // 
  const [member] = await prisma.$queryRaw`
    SELECT
      id
    FROM
      users
    WHERE
      id=${userId} and is_member=1;
  `;

  return member;
};

장바구니와 리뷰 기능은 유저가 비회원인 경우 접근이 안되도록 설정해주기로 하였습니다. 그렇기에 Middleware를 구축하여, 사전에 비회원에게 해당 기능의 인가 권한을 주지 않도록 해주었습니다. 또한 나이키 홈페이지의 Member Access 제품군들은 유저가 회원이더라도 멤버가 아닐 경우 구매가 제한됩니다. 이에 대한 인가 기능을 구현하기 위한 Middleware 역시 authorization.jsmemberProductBuying 함수를 통해 구현해주었습니다.
구현된 MiddlewareRouter에 적용하여 인가 기능을 원할하게 구현 가능하도록 해주었습니다.


⭐️ REVIEW API


<userController.js>

const postReview = async (req, res) => { // 리뷰 포스트
  try {
    const { user_id, styleCode, color, size, comfort, width } = req.body;
    const review = await userServices.postReview(
      user_id,
      styleCode,
      color,
      size,
      comfort,
      width
    );
    
// 유효성 검사를 위한 변수
    const REQUIRED_KEYS = { user_id, styleCode, color, size, comfort, width }; 
    for (let key in REQUIRED_KEYS) { // 유효성 검사
      if (!REQUIRED_KEYS[key]) {
        return res
          .status(400)
          .json({ message: '모든 속성에 대해 리뷰를 입력해주세요.' });
      }
    }

    return res
      .status(200)
      .json({ message: 'REVIEW_POSTED', user_id, styleCode });
  } catch (err) {
    console.log(err);
    return res.status(err.statusCode || 500).json({ message: err.message });
  }
};

const getReview = async (req, res) => { // 유저의 리뷰 정보 받아오기
  try {
    const { user_id, styleCode } = req.body;
    const review = await userServices.getReview(user_id, styleCode);

    return res.status(200).json({ message: 'THIS_IS_REVIEW', review });
  } catch (err) {
    console.log(err);
    return res.status(err.statusCode || 500).json({ message: err.message });
  }
};

const getReviewAverage = async (req, res) => { // 해당 제품의 리뷰 평균 정보(전체 별점을 위한) 받아오기
  try {
    const { styleCode } = req.body;
    const review = await userServices.getReviewAverage(styleCode);

    return res.status(200).json({ message: 'THIS_IS_REVIEW_AVERAGE', review });
  } catch (err) {
    console.log(err);
    return res.status(err.statusCode || 500).json({ message: err.message });
  }
};

<userServices.js>

const postReview = async (userId, styleCode, color, size, comfort, width) => {
  const review = await userDao.getReview(userId, styleCode);

  if (review) {
    const error = new Error('이미 작성한 리뷰가 존재합니다.');
    error.statusCode = 400;

    throw error;
  }
  await userDao.countPlus(styleCode); // 리뷰 작성시 제품의 리뷰 카운트 증가 
  return await userDao.postReview( // 리뷰 작성
    userId,
    styleCode,
    color,
    size,
    comfort,
    width
  );
};

const getReview = async (userId, styleCode) => {
  const review = await userDao.getReview(userId, styleCode);

  if (!review) {
    const error = new Error('리뷰가 존재하지 않습니다.');
    error.statusCode = 400;

    throw error;
  }

  return review;
};

const getReviewAverage = async styleCode => {
  const review = await userDao.getReviewAverage(styleCode);

  if (!review) {
    const error = new Error('평균을 계산할 리뷰가 충분히 존재하지 않습니다.');
    error.statusCode = 400;

    throw error;
  }

  return review;
};

<userDao.js>


//리뷰 작성
const postReview = async (userId, styleCode, color, size, comfort, width) => {
  const [review] = await prisma.$queryRaw`
    INSERT INTO 
      product_reviews (user_id, style_code, color, size, comfort, width) 
    VALUES 
      (${userId}, ${styleCode}, ${color}, ${size}, ${comfort}, ${width})
  `;
  return review;
};

// 리뷰 등록시 해당 제품의 review_counts 증가
const countPlus = async styleCode => { 
  const [count] = await prisma.$queryRaw`
    UPDATE
      products
    SET
      review_counts=review_counts+1
    WHERE
      style_code = ${styleCode}
  `;
  return count;
};

// 해당 유저가 작성한 해당 제품 리뷰 정보 반환
const getReview = async (userId, styleCode) => {
  const [review] = await prisma.$queryRaw`
    SELECT 
      user_id, style_code, color, size, comfort, width
    FROM 
      product_reviews
    WHERE
      user_id=${userId} and style_code=${styleCode};
  `;

  return review;
};

//해당 제품군의 전체 리뷰의 평균 정보 반환
const getReviewAverage = async styleCode => {
  const [review] = await prisma.$queryRaw`
    SELECT 
      style_code,
      avg(color) as colorAverage, //avg 메소드를 통해 평균값 추출
      avg(size) as sizeAverage,
      avg(comfort) as comfortAverage,
      avg(width) as widthAverage,
      (avg(color)+avg(size)+avg(comfort)+avg(width))/4 as totalAverage
    FROM 
      product_reviews
    WHERE
      style_code=${styleCode}
    GROUP BY
      style_code;
  `;

  return review;
};

MVC PATTERN을 적용한 REVIEW API입니다. 구축한 미들웨어를 통해 인가 정보가 없을 경우에는 postReview의 권한이 부여되지 않습니다.
Controller단에서 요청할 정보의 유효성 검사를 진행하고 Layered Pattern을 통해 기능이 구현되도록 설정해주었습니다. 이외에도 GET METHOD를 통해서 리뷰의 정보와 해당 제품군 리뷰의 평균값을 반환해주는 기능을 수행하도록 API를 구성하였습니다.


[Team Project - Nike Clone Coding (4)]( https://velog.io/@peaceminusone/4) 에서 이 포스팅에서 다루지 않은 API에 대한 포스팅이 이어집니다.
긴 글 읽어주셔서 감사합니다 🥰

profile
넘어져도 일어나서 나아가는 개발자의 삶을 염원합니다.

0개의 댓글