Team Project - Nike Clone Coding (2)

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 역할 분담

⚡️ 나의 분담 API

List Page API
FILTER API
REVIEW API
KAKAO Login API
인가(장바구니 권한) 및 미들웨어 API

⚡️ 준혁님의 분담 API

Detail Page API
CART API
SNKRS API
검색 API

저번 프로젝트에서는 로그인 회원가입 및 장바구니와 좋아요 관련된 API를 구현했다. 이번에도 그러한 기능들을 구현한다면 작업은 순조롭겠지만 내 자신의 발전이 조금도 되지 않을거라고 판단하였다. 그렇기에 이번에는 나이키의 메인 기능 중 하나라고 생각하는 Filter API를 포함하여, REVIEW API, LIST PAGE API, KAKAO LOGIN 관련 API를 구축하기로 마음 먹었다. (작업을 하면서 FILTER API 는 리스트 페이지 내에서 자체적으로 수행하는게 좋을 것같아 두 API를 병합하여 작업하였습니다.)
가장 어려워 보이는 API는 FILTER APISNKRS API였다. 리스트 페이지 내의 좌측 필터는 내부에서는 같은 항목 내에서는 OR 기능, 다른 항목 끼리는 AND 기능을 통해 필터 기능을 구축하고 있었기 때문에 직관적으로 구현 방향을 찾기가 힘들었다. (예시를 들자면 색상에서 PINK, GRAY를 선택하면 해당 2컬러의 제품이 모두 나오고, 이후 브랜드에서 ACG를 선택한다면 이전 필터링에서 브랜드가 ACG인 제품만이 나오도록 필터가 구축되있다.)
SNKRS 기능은 유저가 정해진 시간 동안 제품의 응모가 가능하고, 응모가 끝날 경우 당첨 결과가 나오고 응모 페이지 신청이 마감되는 구조이기 때문에 처음 접해보는 기능을 구현하는 것이고, 새로운 기능인만큼 난이도가 있었다. 그렇기에 이 두 기능은 각자 한 가지씩 나눠서 구현하기로 진행하였다.


⭐️ LIST & FILTER API


<productController.js>

const productList = async (req, res) => {
  try {
    const {
      genderId,
      categoryId,
      colorId,
      sizeName,
      subBrandName,
      subIconName,
      subClothesName,
      subAccessoriesName,
      sortMethod,
      search,
    } = req.query; // request의 query에서 요청한 정보를 받아옴

    const list = await productServices.productList(
      genderId,
      categoryId,
      colorId,
      sizeName,
      subBrandName,
      subIconName,
      subClothesName,
      subAccessoriesName,
      +sortMethod, // 제품의 정렬 기준을 위한 sortMethod(숫자형)
      search
    );
    return res.status(200).json({ message: 'ProductFilterList', list });
  } catch (err) {
    console.log(err);
    return res.status(500).json({ message: 'Load Fail' });
  }
};

<productServices.js>

const productList = async (
  genderId,
  categoryId,
  colorId,
  sizeName,
  subBrandName,
  subIconName,
  subClothesName,
  subAccessoriesName,
  sortMethod,
  search
) => {
  const arrayChange = filterValue => {
    if (filterValue) {
      if (typeof filterValue === 'string') { // 받아온 정보가 단일이거나 복수일 경우에 따라 조건문을 통해 적합한 문자열로 변환)
        return `(${filterValue})`;
      } else return '(' + filterValue.join() + ')';
    } else return filterValue;
  };
  const arrayColorIdChange = filterValue => { 
    if (filterValue) {
      if (typeof filterValue === 'string') { // 받아온 정보가 단일이거나 복수일 경우에 따라 조건문을 통해 적합한 문자열로 변환)
        return `(${Number(filterValue)})`; // 변수를 문자형이 아닌 숫자로 변환후 반환
      } else return '(' + filterValue.join() + ')'; 
    } else return filterValue;
  };

  let isSearch = false;

  if (search) isSearch = true;

  let list = await productDao.getProductList(
    genderId,
    categoryId,
    arrayColorIdChange(colorId),
    arrayChange(subBrandName),
    arrayChange(subIconName),
    arrayChange(subClothesName),
    arrayChange(subAccessoriesName),
    sortMethod,
    search,
    isSearch
  );

  // 제품마다의 재고 사이즈를 할당 & 사이즈 필터링 기능 구현
  if (list) {
    for (let i = 0; i < list.length; i++) {
      let sizeObject = await productDao.getSizes(list[i].styleCode);
      let sizeArr = sizeObject.map(e => e.size);
      list[i].sizes = sizeArr;
    }

    if (sizeName) {
      let sizeSyntax = ``;
      if (typeof sizeName === 'string') {
        sizeSyntax = `x.sizes.indexOf(${sizeName})!==-1`;
      } else {
        for (let i = 0; i < sizeName.length; i++) {
          if (i === 0) {
            sizeSyntax += `x.sizes.indexOf(${sizeName[i]})!==-1`;
          } else {
            sizeSyntax += `||x.sizes.indexOf(${sizeName[i]})!==-1`;
          }
        }
      }
      list = list.filter(x => eval(sizeSyntax));
    }
  }
  return list;
};

<productDao.js>

const getProductList = async (
  genderId,
  categoryId,
  colorId,
  subBrandName,
  subIconName,
  subClothesName,
  subAccessoriesName,
  sortMethod,
  search,
  isSearch
) => {
  // 필터링할 기능에 따라 filterQuery를 구현 (prisma.raw)
  let filterQuery = ``;
  if (colorId) {
    filterQuery += ` AND products.color_id in ${colorId}`;
  }
  if (subBrandName) {
    filterQuery += ` AND sub_brand.name in ${subBrandName}`;
  }
  if (subIconName) {
    filterQuery += ` AND sub_icon.name in ${subIconName}`;
  }
  if (subClothesName) {
    filterQuery += ` AND sub_clothes.name in ${subClothesName}`;
  }
  if (subAccessoriesName) {
    filterQuery += ` AND sub_accessories.name in ${subAccessoriesName}`;
  }

  const list = await prisma.$queryRaw`
      SELECT
        product_genders.name as genderName,
        categories.name as categoryName,
        products.style_code as styleCode,
        products.name as productName,
        products.normal_price,
        products.sale_rate,
        products.sale_price,
        products.is_member,
        product_img_urls.name as imgUrl,
        product_colors.name as colorName,
        products.color_id,
        sub_brand.name as subBrandName,
        sub_icon.name as subIconName,
        sub_clothes.name as subClothesName,
        sub_accessories.name as subAccessoriesName
      FROM
        products
      JOIN
        product_genders ON products.gender_id=product_genders.id
      JOIN
        categories ON products.category_id=categories.id
      JOIN
        product_img_urls ON products.style_code=product_img_urls.style_code
      LEFT JOIN
        product_colors ON products.color_id=product_colors.id
      LEFT JOIN
        sub_icon ON products.sub_icon_id=sub_icon.id
      LEFT JOIN
        sub_brand ON products.sub_brand_id=sub_brand.id
      LEFT JOIN
        sub_clothes ON products.sub_clothes_id=sub_clothes.id
      LEFT JOIN
        sub_accessories ON products.sub_accessories_id=sub_accessories.id
      WHERE
        If(${isSearch},products.name like '%${raw(search)}%',TRUE)
      AND
        product_img_urls.is_main=1
      AND
        CASE
        WHEN ${genderId} and ${categoryId} THEN products.gender_id = ${genderId} and products.category_id = ${categoryId}
        WHEN ${genderId} THEN products.gender_id = ${genderId}
        WHEN ${categoryId} THEN products.category_id = ${categoryId}
        ELSE TRUE
        END
      ${raw(filterQuery)}
      ORDER BY
        case WHEN ${sortMethod} = null then products.create_at end ASC,
        case WHEN ${sortMethod} = 1 then products.create_at end ASC,
        case WHEN ${sortMethod} = 2 then products.review_counts end DESC,
        case WHEN ${sortMethod} = 3 then products.name end DESC,
        case WHEN ${sortMethod} = 4 then products.sale_rate end DESC,
        case WHEN ${sortMethod} = 5 then products.normal_price end ASC,
        case WHEN ${sortMethod} = 6 then products.normal_price end DESC;
  `;
  return list;
};

// 각 제품에 따른 재고 사이즈 파악을 위한 Query
const getSizes = async styleCode => {
  const sizes = await prisma.$queryRaw`
    SELECT 
      product_sizes.name as size
    FROM
      product_with_sizes
    JOIN
      product_sizes ON product_with_sizes.product_size_id=product_sizes.id
    WHERE
      product_with_sizes.style_code=${styleCode};
  `;
  return sizes;
};

가장 많은 시간과 공을 들인 LIST & FILTER API 입니다. 우선 리스트 페이지에서 필요한 정보들을 받아오는 API인 LIST API를 먼저 구현하였고, 이후 불러온 리스트 페이지의 데이터를 필터링할 방법에 대한 긴 고민이 이어졌습니다. 계속 모니터 앞에 앉아 있어도 좋은 생각이 떠오르지 않아, 산책을 하던 중, 나이키 홈페이지에서 자체적으로 필터 기능을 어떻게 처리할지에 대해 생각하던 중 '리스트 API로 받아온 정보들을 filter 메소드를 통해 원하는 정보만 추출하면 되지 않을까?' 라는 생각이 들었고, 시간을 들여 코드를 적용해본 결과 필터링이 적용된 원하는 제품들만 뽑아낼 수 있도록 구현을 성공했습니다.!

허나, 기능이 잘 작동되어 기뻤던 것도 잠시, 멘토님의 코드 리뷰를 통해 Services 단의 많은 함수들로 구현이 아닌 Dao 단에서 SQL Query로 통해 구현하는 것이 더 바람직하다는 피드백을 받았고, SQL Query 문법중 하나인 WHERE IN 기능을 통해 OR 조건을 구현 가능하다는 것을 깨달았습니다. (진작 알았더라면.. ㅠㅠ)
이후 Services 단에서의 사이즈 필터링 과정을 제외하고 모든 필터 기능을 Dao단의 Query를 통해 구축하기로 하였습니다. 단, WHERE IN 조건문은 예를 들면 WHERE product_colors.id in (1,2,3) 처럼 배열이 아닌 tuple을 통해 조건을 받아낼 수 있기 때문에 조건을 받아올 대상을 튜플로 변환하는 과정을 Services 단에서 진행하였습니다.

이렇게 모든 필터링 기능 구현에 성공할 수 있었습니다.

Team Project - Nike Clone Coding (3) 에서 이 포스팅에서 다루지 않은 API에 대한 포스팅이 이어집니다.
긴 글 읽어주셔서 감사합니다 🥰

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

0개의 댓글