[1차 프로젝트 회고]

이수현·2022년 7월 6일
2

TIL

목록 보기
23/23

🏡Stayfolio 클론 프로젝트

개요

  • 팀명: Staypler
  • 프로젝트 기간: 2022-06-20 ~ 2022-06-31
  • Back-End 사용 기술: NODE.JS | EXPRESS | MYSQL | PRISMA | POSTMAN | JWT | BCRYPT

📚ERD

📝 API 명세서

StayPler API 명세서

✏️소감

stayfolio를 클론 대상으로 선정했을 때, 이렇게 어려울지 생각을 하지못했다..
먼저 팀적인 부분부터 소감을 말하면, 프로젝트 코딩을 본격적으로 시작하기 전에 미리 API 명세를 만들어서 대략적으로 FRONT에서 요청을 보낼 때 양식과 BACK에서 응답을 보낼 때 양식을 작성했다.

그런데 혼자 조급해서 정해놨던 응답의 키값과 다르게 응답을 보내서 연결할 때 다시 양식에 맞춰서 수정하는 작업을 했다..
내가 수정하는데 시간이 들어가는 것은 괜찮았지만, 그 시간동안 해당 API와 연결을 하려고 하셨던 FRONT 담당 팀원에게 민폐를 끼쳤다...

이번 사건을 계기로 API 명세서를 만들었다면, 그 양식에 맞춰서 코드를 처음부터 작성해야하고, 만약 수정사항이 있다면 명세서를 갱신하고 팀원들에게 전달해야 된다는 것을 깨달았다..

그리고 우리 팀은 매일 오후 1시부터 전날 오후 7시부터 당일 1시 전까지 진행된 부분을 공유했다.
이 때 PR을 보며 리뷰를 했는데, 팀원들에게 작성한 코드를 설명하면서 이상한 점을 발견하거나, 작성한 코드에 대해 이해도가 높아져서 굉장히 의미있었다.
또한, 팀원들의 코드를 설명 들으면서 비슷한 기능을 나와는 다르게 구현한 부분을 보며 얻어가는 부분도 많았다.

하지만 아무래도 내가 Back부분만 맡아서 프로젝트를 진행하다보니, Front에 대해 정확한 리뷰를 하기가 어려웠고, 그러다보니 승인을 하면 안됐을 PR도 승인을 하는 실수가 나왔다.
그로인해 main 브랜치에 병합이돼서 막상 main 브랜치를 pull해서 실행했을 때 정상적으로 작동하지 않아서 골치 아팠던 적이 있었는데, 더 꼼꼼히 확인하고 리액트도 공부를 해야겠다는 생각이 들었다.

팀적으로는 사실 PR을 통한 코드 리뷰가 생소하다 보니 생긴 실수와, API 명세서에 맞추지 않아 발생한 문제뿐만 아니라 어려워 하는 팀원이 있다면 알아채고 도와줄 수 있어야 하는 역량도 있어야 겠다고 생각했다.
물론, 팀원뿐만 아니라 나도 문제에 직면하면 팀에게 문제 사항을 공유해서 어떻게 해결할지 같이 고민해 나가는게 중요한 것 같다.

그래야 진정한 '팀 프로젝트'가 아닐까... 생각해본다..

사실 방금 말한 것들에 대해 프로젝트 전에는 '아 그렇구나', '어려우면 팀원들에게 공유해야지' 이렇게 막연하게 생각했었는데, 이번 프로젝트가 종료되고 곱씹어보며 정말 중요하겠구나.. 생각을 했다.
과연 내가 문제에 직면하면 팀원들에게 문제 상황을 잘 정리해서 전달할 수 있을까? 라는 생각과 함께 문제 상황을 팀원들에게 잘 정리해서 전달하려면 막연하게 생각했던 생각을 구체화해서 마음속에 간직하고, 문제 상황을 잘 정리하는 능력도 필요하다고 생각했다.

이제 개인적인 부분에서 소감을 말하면, 생각보다 정말 너무 어려웠지만 재밌었다.
아무래도 숙소 예약에 대한 사이트를 클론 코딩하다보니, 날짜 필터링을 통해 해당 날짜에 숙소 내에 예약 가능한 방이 있다면, 해당 숙소가 리스트에 보여지는 부분을 처리하는 데 있어서 쿼리 작성을 할 때 굉장히 어려웠다...

그리고 개인적으로 인증을 체크하는 미들웨어를 만들어서, 인증이 필요한 메서드에 해당 미들웨어를 추가해줘서 작동되는 것을 보고 기분이 좋았다.
prisma 공식문서를 보며 queryRaw 메서드는 템플릿 리터럴 문법을 사용할 때 컬럼명, 테이블명이 포함되어 있으면 에러가 발생해서, queryRawUnsafe로 변경해줘야 한다는 사실도 알게되었다.

이번 프로젝트를 진행하면서 공식문서를 보며 조금이나마 일단 코드를 작성 및 수정할 수 있게 됐다는 것이 개인적으로 의미가 있었다.
그리고 쿼리를 작성할 때 Join이 여러개 묶였다면, GROUP BY도 신경써줘야 하고 어디서 필요한 데이터를 갖고와서 중복 row가 나오지 않도록 할 지, 서브 쿼리를 통해 Join하여 중복 row를 나오지 않도록 할 지 조금은 알게 되었다.
그래도 이번 프로젝트가 어려웠지만, 얻어가는 것도 그만큼 많았다고 생각한다...

2차 프로젝트는 앞서 말했던 팀적으로 신경써야할 부분을 더 생각하고, 개인적으로 느꼈던 공식문서와 서칭을 통해 문제를 해결해나가는 것을 더 성장 시켜야겠다고 생각한다...
남은 기간도 해이해지지 않고 더 열심히 마무리를 해보자!!!!...

개인적으로 마음에 들었던 코드..

import jwt from 'jsonwebtoken';
import * as userRepository from '../models/user.js';
import dotenv from 'dotenv';

dotenv.config();
const AUTH_ERROR = { message: 'Authentication Error' };
export const validateToken = async (req, res, next) => {
  const authHeader = req.get('Authorization');
  if (!(authHeader && authHeader.startsWith('Bearer '))) {
    return res.status(401).json(AUTH_ERROR);
  }

  const token = authHeader.split(' ')[1];
  jwt.verify(token, process.env.SECRET_KEY, async (error, decoded) => {
    if (error) {
      return res.status(401).json(AUTH_ERROR);
    }
    const user = await userRepository.getUserIdbyId(decoded.id);
    if (!user) {
      return res.status(401).json(AUTH_ERROR);
    }
    req.userId = user[0].id;
    req.token = token;
    next();
  });
};

위 코드는 인증 미들웨어이다. 작동 방식은 만약 마이페이지에 접근할 때 토큰이 없다면 인증 에러가 발생하고, 토큰이 있다면 next() 메서드를 통해 해당 마이페이지 메서드가 작동된다.
만약 routes폴더 안의 모든 router가 인증 미들웨어를 거쳐야 한다면 router.use(middleware)를 상단에 추가하여 편리하게 사용할 수 있다.

export async function readAll(
  userId,
  date,
  keyword,
  filter,
  sortKeyword,
  page
) {
  const sql = `SELECT
  rooms.id,
  rooms.title,
  rooms.type,
  rooms.province,
  rooms.city,
  ri.images,
  IFNULL(rooms_like_sum.likes,0) likes,
  ${userId ? `IFNULL(rooms_like.isLike,0) islike,` : ``}
  MAX(room_type.max_limit) max_limit,
  MIN(room_type.min_limit) min_limit,
  MAX(room_type.price) max_price,
  MIN(room_type.price) min_price,
  theme.name theme
FROM rooms
LEFT JOIN (SELECT rooms_image.rooms_id, JSON_ARRAYAGG(CASE WHEN rooms_image.id IS NOT NULL THEN rooms_image.image END) images FROM rooms_image GROUP BY rooms_image.rooms_id) ri
ON rooms.id = ri.rooms_id
${
  userId
    ? `LEFT JOIN (SELECT id,rooms_id, isLike FROM likes WHERE user_id=${userId} group by id) as rooms_like ON rooms_like.rooms_id = rooms.id`
    : ``
} 
${generateJoinDateStatement(date.start_date, date.end_date)}
LEFT JOIN (SELECT rooms_id, SUM(isLike) likes FROM likes group by rooms_id) rooms_like_sum
ON rooms_like_sum.rooms_id = rooms.id
LEFT JOIN (SELECT rooms_id, theme.name FROM theme JOIN rooms_theme on theme.id = rooms_theme.theme_id) theme
ON theme.rooms_id = rooms.id
LEFT JOIN reservation on room_type.id = reservation.room_type_id
${keyword ? `WHERE rooms.title LIKE '%${keyword}%'` : ``}
GROUP BY rooms.id, theme ${userId ? `, islike` : ``}
${generateHavingStatement(filter)}
${generateOrderByStatemnet(sortKeyword)}
`;
  const roomsCnt = await prismaClient.$queryRawUnsafe(
    `SELECT COUNT(*) total_rows FROM (${sql}) t`
  );
  const rooms = await prismaClient.$queryRawUnsafe(
    sql + generateLimitOffsetStatement(page)
  );
  return [rooms, roomsCnt];
}

function generateOrderByStatemnet(orderkeyword) {
  return orderkeyword
    ? ` ORDER BY rooms.id,${orderkeyword}`
    : ' ORDER BY rooms.id';
}

function generateHavingStatement({
  min_price,
  max_price,
  type,
  max_limit,
  theme,
}) {
  const havingArray = [];
  if (min_price) {
    havingArray.push(`min_price > ${min_price}`);
  }
  if (max_price) {
    havingArray.push(`max_price < ${max_price}`);
  }
  if (type) {
    havingArray.push(
      `type IN (${type
        .split(',')
        .map(name => `'${name}'`)
        .join(',')})`
    );
  }
  if (max_limit) {
    havingArray.push(`max_limit > ${max_limit}`);
  }
  if (theme) {
    havingArray.push(
      `theme.name IN (${theme
        .split(',')
        .map(name => `'${name}'`)
        .join(',')})`
    );
  }
  return havingArray.length ? `HAVING ${havingArray.join(' and ')}` : '';
}

function generateJoinDateStatement(start_date, end_date) {
  return start_date && end_date
    ? `JOIN (SELECT * 
      FROM (SELECT * 
        FROM room_type 
        WHERE room_type.id NOT IN (SELECT reservation.room_type_id 
          FROM reservation 
          WHERE ( '${start_date}' < reservation.end_date ) AND ( reservation.start_date < '${end_date}' ))) rtype) room_type
    ON rooms.id = room_type.rooms_id`
    : `JOIN room_type ON rooms.id = room_type.rooms_id`;
}

function generateLimitOffsetStatement(page) {
  const limit = 6;
  return ` LIMIT ${limit} OFFSET ${limit * page}`;
}

Reference

이 프로젝트는 테라로사 사이트를 참조하여 학습목적으로 만들었습니다.
실무수준의 프로젝트이지만 학습용으로 만들었기 때문에 이 코드를 활용하여 이득을 취하거나 무단 배포할 경우 법적으로 문제될 수 있습니다.
이 프로젝트에서 사용하고 있는 사진 대부분은 위코드에서 구매한 것이므로 해당 프로젝트 외부인이 사용할 수 없습니다.

0개의 댓글