엘리스 1차 프로젝트 0523~0603 (36일차~45일차)

치즈말랑이·2022년 6월 6일
0

엘리스

목록 보기
37/47
post-thumbnail

엘리스 1차 프로젝트를 했다.

쇼핑몰 사이트를 만드는 것이였는데, 특이하게도 프론트엔드와 백엔드가 같은 프로젝트앱 안에서 작업한다.
폴더구조는 다음과 같다.

전체적으로 봐서는 백엔드 앱인데, views폴더 내에서 프론트엔드가 작업한다.

백엔드는 express.js, 프론트엔드는 프레임워크없이 기본 자바스크립트, css, html로만 작업한다.

mongoDB atlas에 연결해서 작업했으며, 장바구니 기능은 localStorage를 이용해 프론트에서 처리했다.
2주라는 짧은 기간이 주어져서 많은 기능을 넣지는 못했지만, 백엔드 코치님을 통해서 중요한것들을 몇가지 알게 되었다.

백엔드에서 정말 중요한것 -> 에러 처리

프로젝트를 하기 전까지는 error handler가 무슨 역할을 하는지 제대로 이해하지 못하고 있었다.
validation도 뭔지, 왜 하는지 모르고 있었는데 기능을 구현하고 프론트와 말을 맞춰보면서 왜 필요한지 알게 되었다.

또한 엘리스에서 간단하게 구현되어있는 스켈레톤 코드를 제공받아서 거기에 덧붙여서 작업을 했는데, 기본코드에는 간단하게 모든 에러를 400으로 받도록 되어있었다. 이건 아마 엘리스쪽에서 수강생들이 수정 하는지 보려고 일부로 해놓은거같은데, 난 코치님 말을 듣고 swtich-case문으로 어느정도 변경을 했다.

function errorHandler(error, req, res, next) {
  // 터미널에 노란색으로 출력됨.
  console.log("\x1b[33m%s\x1b[0m", error.stack);

  // error 메시지에 따른 에러 상태 코드
  switch (error.message) {
    // 401 에러, 로그인 오류
    case "Unauthorized":
      res.status(401).json({ result: "error", reason: error.message });
      break;
    // 403 에러, 사용자가 리소스에 대한 필요 권한 없음
    case "Forbidden":
      res.status(403).json({ result: "error", reason: error.message });
      break;
    // 406 에러
    case "headers의 Content-Type을 application/json으로 설정해주세요":
      res.status(406).json({ result: "error", reason: error.message });
      break;
    // default는 400 에러로 설정
    default:
      res.status(400).json({ result: "error", reason: error.message });
  }
}

export { errorHandler };

validation 부분은 코치님이 joi를 알려주셔서 사용할지 고민을 했는데, 프로젝트가 처음이기도 하고 내 맘대로 유연하게 설정해보고 싶어서 번거롭지만 직접 if문으로 처리를 했다.

// admin-router.js

// 전체 회원 목록 조회
adminRouter.get("/users", loginRequired, async (req, res, next) => {
  try {
    // 토큰에서 userId 추출
    const userId = req.currentUserId;
    // 관리자 계정 검증
    await adminService.adminVerify(userId);
    // db에서 전체 회원 목록 가져옴
    const users = await adminService.getUsers();

    res.status(200).json(users);
  } catch (error) {
    next(error);
  }
});
// admin-service.js

  // 회원 목록 조회
  async getUsers() {
    // db에서 회원 정보 조회
    const users = await this.userModel.findAll();
    if (!users) {
      throw new Error("회원 목록을 조회할 수 없습니다.");
    }
    return users;
  }

// 비밀번호 일치 여부 확인
  async userVerify(userId, currentPassword) {
    // 우선 해당 id의 유저가 db에 있는지 확인
    let user = await this.userModel.findById(userId);
    // db에서 찾지 못한 경우, 에러 메시지 반환
    if (!user) {
      throw new Error("가입 내역이 없습니다. 다시 한 번 확인해 주세요.");
    }

    // 이제, 정보 수정을 위해 사용자가 입력한 비밀번호가 올바른 값인지 확인해야 함

    // 비밀번호 일치 여부 확인
    const correctPasswordHash = user.password;
    const isPasswordCorrect = await bcrypt.compare(
      currentPassword,
      correctPasswordHash
    );

    if (!isPasswordCorrect) {
      throw new Error(
        "현재 비밀번호가 일치하지 않습니다. 다시 한 번 확인해 주세요."
      );
    }
  }

  // 관리자 계정 확인
  async adminVerify(userId) {
    if (!userId) {
      throw new Error("로그인이 필요합니다.");
    }
    const user = await this.userModel.findById(userId);
    if (!user) {
      throw new Error("가입 내역이 없습니다. 다시 한 번 확인해 주세요.");
    }

    const userRole = user.role;
    if (userRole !== "admin") {
      throw new Error("관리자가 아니면 접속할 수  없습니다.");
    }
  }

  async exceptPwd(userInfo) {
    return await (({ password, ...o }) => o)(userInfo);
  }

exceptPwd부분은 db에서 불러온 회원 정보에서 비밀번호를 제외하는 메소드이다.

이런 처리를 하지 않으면 암호화됐다고는 하지만 front에 비밀번호정보까지 넘어가기 때문이다.

또한,

PATCH -> PUT

엘리스 기본코드에는 PATCH로 되어 있었다. 그런데 코치님이 현업에서는 PUT을 주로 사용한다고 해서 전부 수정해주었다.
단어만 바꿨더니 정상적으로 작동해서 별다른 조치는 하지 않았다.

우리가 이번에 진행한 아키텍쳐 방식은 견고한 node.js 프로젝트 방식이다.

https://velog.io/@hopsprings2/%EA%B2%AC%EA%B3%A0%ED%95%9C-node.js-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90-%EC%84%A4%EA%B3%84%ED%95%98%EA%B8%B0

index.js에서 서버를 실행하고 routers, services, middlewares 폴더가 기본이 된다.

관심사 분리 원칙에 따라 router파일은 router 기능만 한다.

라우터는 클라이언트의 요청 경로(path)를 보고 이 요청을 처리할 수 있는 곳으로 기능을 전달해주는 역할을 한다. 이러한 역할을 라우팅이라고 하는데, 애플리케이션 엔드 포인트 (URI)의 정의, 그리고 URI가 클라이언트 요청에 응답하는 방식을 의미한다
https://cotak.tistory.com/85

MongoDB

가장 고민했던 부분이다.

import { Schema } from "mongoose";

const OrderSchema = new Schema(
  {
    userId: {
      type: String,
      required: true,
    },
    address: {
      type: new Schema(
        {
          addressName: String,
          receiverName: String,
          receiverPhoneNumber: String,
          postalCode: String,
          address1: String,
          address2: String,
        },
        {
          _id: false,
        }
      ),
      required: true,
    },
    request: {
      type: String,
      required: false,
      default: "요청사항 없음",
    },
    orderList: [
      new Schema({
        productId: String,
        title: String,
        quantity: Number,
        price: Number,
        status: {
          type: String,
          required: false,
          default: "상품 준비중",
        },
      }),
    ],
    totalPrice: {
      type: Number,
      required: true,
    },
    shippingFee: {
      type: Number,
      required: false,
      default: 5000,
    },
  },
  {
    collection: "orders",
    timestamps: true,
  }
);

export { OrderSchema };

여기서 orderList는 배열 안에 schema가 하나 더들어가있다.
이것을 어떻게 넣고 지우고 업데이트할것인가.. 고민을 하다가 코치님한테 여쭤보고 답을 찾았다.

  // 배송 상태 수정
  async setOrderStatus(orderInfoRequired, toUpdate) {
    const { orderId, productId } = orderInfoRequired;
    const { status } = toUpdate;
    if (!orderId || !productId) {
      throw new Error("주문 정보 혹은 상품 정보가 없습니다.");
    }
    if (!status) {
      throw new Error("배송 상태 정보가 없습니다.");
    }
    const updateOrderList = await this.orderModel.findById(orderId);
    if (!updateOrderList) {
      throw new Error("주문 정보가 없습니다.");
    }
    const newUpdate = await updateOrderList.orderList.map((e) => {
      if (e.productId === productId) {
        e.status = status;
      }
      return e;
    });

    const update = { $set: { orderList: newUpdate } };
    const updatedOrder = await this.orderModel.update({ orderId, update });
    if (!updatedOrder) {
      throw new Error("배송 상태가 수정되지 않았습니다.");
    }
    return updatedOrder;
  }

이런식으로 db에서 주문정보를 꺼내온다음에 orderList만 map으로 바꿀거 바꿔서 새로운 배열로 만들어준다.
그 후에 { $set: { orderList: newUpdate }이거를 mongoose.update에다가 두번째인자로 사용한다.
새로 값을 넣을때는 $set 대신에 $push를 사용하면 된다.

앞으로 적용해볼 것: 소셜로그인, joi, 댓글대댓글, 기회가 된다면 socket으로 실시간 접속자 보기

profile
공부일기

0개의 댓글