본문 바로가기
BackEnd

Wecode 3st Project 회고록

by SoriKim 2023. 9. 24.
반응형

 

 

이번 project 로고

 

1.  팀소개 

▶︎ Team Name: Tickey Ticket

 

▶︎  팀 구성

 [ Back-End 6명 | Front-End 3명 ]

- Project-Manager : F 1명 

- Product-Manager : B 1명 

- Meta-Cognitive Follower(팀원): B 5명, F 2명

 

2.  프로젝트 소개 

2- 1. 설계 및 기획 & 기간

🔵 설계 및 기획 

Notion 설계 및 기획 정리 자료

 

🔵 프로젝트 기간

2023.09.04(월) ~ 2023.09.22(금) (약 3주) 

 

2- 2. 구현 기능 선정

🔧  사용 기술 스택

Back-End

Javascript, NodeJs, Express, Mysql

 

Front-End

React, Javascript, styled-components, Html, sass

 

Back-End & Front-End

AWS, Git, Github, Trello, Notion, Slack

 

📍 핵심 기능 목록

1. 지도 API 

- 현재 사용자의 위치 주변의 공연 List 전송

- 지도 페이지를 통해 모든 공연 정보 List 확인이 가능하며 마크를 통해 상세페이지로 접근 

- 지도 API는 카카오 지도 API와 사용자 현 위치 기반의 좌표 값을 얻을 수 있는 Geoloaction API를 활용

 

2. 장르별 카테고리 

- 메인, 서브 페이지에서 카테고리를 선택하여 카테고리 별로 사용자 위치 반경 5km 이내의 공연 List를 전송 

 

3. 예약 확인

- 예약 확인 페이지 상 티켓 QR 발급

- 예약 확인 페이지에서 리뷰 작성 기능 

 

 

2 - 3.  ERD 설계

 

 

2 - 4.  User Flow 및 Figma

 

 

2 - 5.  Product & End - User & Tech 분석 

 Product & End - User & Tech 분석 Notion Page

 

3.  담당 역할(담당 티켓) 

1️⃣ PM(Product Manager) 

이번 Project에서는 PM을 맡아 백엔드 티켓 분배 및 관리를 하며 최종 PR 작성 확인과 코드 확인을 했습니다. 

백엔드 관련하여 팀원들의 기술적 구현의 어려움을 공유하며 해소 하려 노력했으며 하나의 오류 및 문제를 가지고 너무 많은 시간을 낭비하지 않도록 함께 해당 코드를 확인하며 문제를 풀어나가고 제가 해결할 수 있는 문제(ex. git rebase, commit 기록 및 conflict 해결, middle ware의 흐름) 들은 함께 확인하며 알려주는 역할을 담당했습니다. 

또한 제가 알지 못하는 부분은 질문 내용을 정확하게 정리하여 멘토님께 공유하여 해결하도록 유도하는 역할을 했습니다. 

 

 

2️⃣ ERD 설계 및 수정 & DB 생성

모든 팀원과 함께 ERD를 설계하며 팀원들과 프론트에서 필요한 데이터 및 table들이 필요하거나 수정이 요구되면 팀원들과 의논하여 최종 결정을 내려 수정하는 스키마 파일을 작성했습니다. 

또한 Table간 참조 관계도를 파악하여 순차적으로 Table을 생성할 수 있게 순서도를 정하여 스키마 파일을 작성 및 생성했습니다. 

 

Table 생성 순서

  1. users
  2. business_registration_images
  3. user_location_agreement
  4. user_personal_information_agreement
  5. products
  6. product_options
  7. reviews
  8. thumbnail_images
  9. product_detail_images
  10. performers
  11. actor_images
  12. category_genres
  13. product_category
  14. category_hashtags
  15. product_hashtags
  16. map_coordinates
  17. payment_information
  18. wishlists

 

3️⃣ 메인 / 서브 / 지도 페이지 API

요구사항

- 해당 서비스 상 로그인과 회원가입 없이도 메인/ 서브/ 지도 페이지 확인이 가능

- 로그인 시 사용자가 좋아요 누른 항목 확인 가능

[ 로그인을 하지 않은 경우 모든 wishlist 값 false(0)로 전달 ]

( 로그인 한 경우 사용자가 좋아요 누른 값은 true(1)로, 누르지 않은 값은 false(0)으로 전달 ] 

- 메인/ 서브 페이지는 사용자 위치 기반으로 반경 5km 이내의 공연들을 확인

- 사용자가 카테고리 클릭 시 카테고리 별로 List 확인

- SortBy (예매율순, 날짜순)으로 공연 List 확인

 

 

🔑 카테고리와 sortby 클릭 시 Query Builder를 Service에 생성하여 전달 

const getProductByCategory = async (
  userId,
  lng,
  lat,
  genreId,
  hashtagId,
  dateBy,
  sortBy
) => {
  const ordering = async (sortBy) => {
    switch (sortBy) {
      case 'bookingTicketDESC':
        return `ORDER BY totalBookingTicket DESC`;
      case 'bookingTicketASC':
        return `ORDER BY totalBookingTicket ASC`;
      default:
        return `ORDER BY p.id`;
    }
  };

  const dateRange = async (dateBy) => {
    switch (dateBy) {
      case 'dayOne':
        return `po.start_date >= CURDATE()  AND CASE WHEN po.start_date = CURDATE()
                THEN po.start_time >= ADDTIME(CURTIME() , "2:00:00")
                ELSE po.start_time
                END`;
      case 'dayWeek':
        return `po.start_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 1 WEEK) AND
                CASE WHEN po.start_date = CURDATE()
                THEN po.start_time >= ADDTIME(CURTIME() , "2:00:00")
                ELSE po.start_time
                END`;
      case 'dayMonth':
        return `po.start_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 1 MONTH) AND
                CASE WHEN po.start_date = CURDATE()
                THEN po.start_time >= ADDTIME(CURTIME() , "2:00:00")
                ELSE po.start_time
                END`;
      default:
        return `po.start_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 1 MONTH) AND
                CASE WHEN po.start_date = CURDATE()
                THEN po.start_time >= ADDTIME(CURTIME() , "2:00:00")
                ELSE po.start_time
                END`;
    }
  };

  const orderingQuery = await ordering(sortBy);
  const dateRangeQuery = await dateRange(dateBy);

  return await mainDao.getProductByCategory(
    userId,
    lng,
    lat,
    genreId,
    hashtagId,
    dateRangeQuery,
    orderingQuery
  );
};

🔑 Main Dao 

const getProductByCategory = async (
  userId,
  lng,
  lat,
  genreId,
  hashtagId,
  dateRangeQuery,
  orderingQuery
) => {
  try {
    const locationByProduct = await appDataSource.query(
      `
        SELECT 
            p.id AS productId,
            COALESCE(isLikedSubquery.isLiked, 0) AS isLiked,
            cg.genre_name AS genreName,
            pc.category_genres_id AS genreId,
            p.name,
            p.description,
            p.price,
            p.place AS performPlace,
            p.film_rating AS filmRating,
            CASE WHEN COUNT(po.start_date) > 1
            THEN DATE_FORMAT(MIN(po.start_date), '%Y-%m-%d') 
            ELSE DATE_FORMAT(MIN(po.start_date), '%Y-%m-%d') 
            END 
            AS startDate,
            CASE WHEN COUNT(po.start_date) > 1
            THEN DATE_FORMAT(MAX(po.start_date), '%Y-%m-%d') 
            ELSE DATE_FORMAT(MAX(po.start_date), '%Y-%m-%d') 
            END 
            AS endDate,
            CASE WHEN COUNT(po.start_time) > 1
            THEN MAX(po.start_time)
            ELSE MAX(po.start_time)
            END
            AS startTime,
            CASE WHEN COUNT(po.running_time)
            THEN MAX(po.running_time)
            ELSE MAX(po.running_time)
            END 
            AS runningTime,
            ti.thumbnail_image_url AS thumbnailImageUrl,
            (SELECT 
                SUM(pi.payment_total_quantity) 
            FROM payment_information pi 
            LEFT JOIN product_options po 
                ON po.id = pi.product_option_id 
            WHERE pi.deleted_at IS NULL 
                AND pi.product_option_id = po.id 
                AND po.product_id = p.id) AS totalBookingTicket,
            JSON_OBJECT( "lat", mc.coordinate_x, "lng", mc.coordinate_y) AS latlng,
            (6371 * acos(cos(radians(${lat})) * cos(radians(mc.coordinate_x)) * cos(radians(mc.coordinate_y)- radians(${lng})) + sin(radians(${lat})) * sin(radians(mc.coordinate_x)))) AS distance
        FROM products p
        LEFT JOIN 
            product_options po ON p.id = po.product_id
        LEFT JOIN 
            thumbnail_images ti ON p.id = ti.product_id
        LEFT JOIN 
            product_category pc ON p.id = pc.product_id
        LEFT JOIN 
            map_coordinates mc ON p.id = mc.product_id
        LEFT JOIN 
        (
            SELECT w.product_id, CASE WHEN w.user_id = ${userId} OR w.user_id IS NULL THEN 1 ELSE 0 END AS isLiked
            FROM wishlists w
            WHERE w.user_id = ${userId}
        ) AS isLikedSubquery ON p.id = isLikedSubquery.product_id
        LEFT JOIN 
            category_genres cg ON pc.category_genres_id  = cg.id 
        WHERE 
            pc.category_genres_id = IFNULL(?, pc.category_genres_id) AND
            ${dateRangeQuery}
            GROUP BY p.id, ti.id, mc.id, pc.id
            HAVING distance < 5
            ${orderingQuery};
            `,
      [genreId, hashtagId]
    );
    return locationByProduct;
  } catch {
    const error = new Error('dataSource Error');
    error.statusCode = 400;

    throw error;
  }
};

 

 

4️⃣  지도 페이지 API

지도 페이지는 메인과 서브 페이지와는 다르게 사용자 위치 기반이 아닌 사용자가 어느 위치에 있든 모든 공연을 확인할 수 있게 했으며 해당 내용을 기준으로 API를 작성했습니다. 

 

🔑 main Dao

const getAllProdctList = async () => {
  try {
    const showAllProdcut = await appDataSource.query(
      `
            SELECT 
                p.id AS productId,
                p.name,
                p.description,
                p.price,
                p.place AS performPlace,
                p.film_rating AS filmRating,
                CASE WHEN COUNT(po.start_date) > 1
                THEN DATE_FORMAT(MIN(po.start_date), '%Y-%m-%d') 
                ELSE DATE_FORMAT(MIN(po.start_date), '%Y-%m-%d') 
                END 
                AS startDate,
                CASE WHEN COUNT(po.start_date) > 1
                THEN DATE_FORMAT(MAX(po.start_date), '%Y-%m-%d') 
                ELSE DATE_FORMAT(MAX(po.start_date), '%Y-%m-%d') 
                END 
                AS EndDate,
                CASE WHEN COUNT(po.start_time) > 1
                THEN MAX(po.start_time)
                ELSE MAX(po.start_time)
                END
                AS startTime,
                CASE WHEN COUNT(po.running_time)
                THEN MAX(po.running_time)
                ELSE MAX(po.running_time)
                END 
                AS runningTime,
                (SELECT 
                    SUM(pi.payment_total_quantity) 
                FROM payment_information pi 
                LEFT JOIN product_options po 
                    ON po.id = pi.product_option_id 
                WHERE pi.deleted_at IS NULL 
                    AND pi.product_option_id = po.id
                    AND po.product_id = p.id) AS totalBookingTicket,
                JSON_OBJECT( "lat", mc.coordinate_x, "lng", mc.coordinate_y) AS latlng,
                ti.thumbnail_image_url AS thumbnailImageUrl
            FROM products p
            LEFT JOIN 
                product_options po ON p.id = po.product_id
            LEFT JOIN 
                thumbnail_images ti ON p.id = ti.product_id
            LEFT JOIN 
                product_category pc ON p.id = pc.product_id
            LEFT JOIN 
                map_coordinates mc ON p.id = mc.product_id
            LEFT JOIN 
                wishlists w ON p.id = w.product_id 
            GROUP BY p.id, w.id, ti.id, mc.id;
            `
    );
    return showAllProdcut;
  } catch {
    const error = new Error('dataSource Error');
    error.statusCode = 400;

    throw error;
  }
};

 

 

5️⃣  wishlist API 

- 사용자가 누른 상품 DB Table에 Insert( 해당 테이블에 DB user_id, product_id 값을 unique로 지정하여 중복된 user_id, product_id 가 들어올 경우 DataError)하는 API 

- 사용자가 좋아요 누른 상품 다시 클릭 시 Delete 하는 API 

 

wishlist Dao

const addWishList = async (userId, productId) => {
  try {
    const addWishProduct = await appDataSource.query(
      `
            INSERT INTO wishlists (
                user_id, 
                product_id
            ) VALUES (
                ?,
                ?
            )
            `,
      [userId, productId]
    );
    return addWishProduct;
  } catch {
    const error = new Error('dataSource Error');
    error.statusCode = 400;

    throw error;
  }
};

const deleteWishList = async (userId, productId) => {
  const deleteRows = await appDataSource.query(
    `
        DELETE FROM wishlists 
        WHERE 
            user_id = ? AND
            product_id = ? 
        `,
    [userId, productId]
  );

  return deleteRows;
};

 

6️⃣ 예약확인 페이지 예약 취소 API

- 사용자가 예약 티켓 취소 요청 시 해당 티켓이 있는 지 여부 확인 & now() 시간대와 티켓 예정 시작 날짜 및 시간을 확인하여 3시간 이내의 티켓은 취소 불가하도록 기능 구현한 API

- 해당 여부 확인은 Service 단에서 확인

 

🔑 service 

const deleteBookingTicket = async (userId, paymentCode, productOptionId) => {
  const ticket = await mypageDao.checkTicket(
    userId,
    paymentCode,
    productOptionId
  );
  const selectticket = await mypageDao.checkTicketList(
    userId,
    paymentCode,
    productOptionId
  );
  const checkticket = ticket.checkdate;
  const checkticketlist = selectticket.id;
  if (!checkticketlist || checkticket == 0) {
    const error = new Error('This Ticket do not cancel');
    error.statusCode = 401;

    throw error;
  }

  if (checkticket == 1) {
    return await mypageDao.deleteBookingTicket(
      userId,
      paymentCode,
      productOptionId
    );
  }
};

🔑 Dao 

const checkTicket = async( userId, paymentCode, productOptionId ) => {
    try { 
        const [checkdate] = await appDataSource.query(
        `
        SELECT 
            CASE 
                WHEN po.start_date >= CURDATE() AND 
                CASE WHEN po.start_date = CURDATE()
                THEN po.start_time >= ADDTIME(CURTIME() , "3:00:00")
                ELSE po.start_time 
                END
                THEN 1
                ELSE 0
                END  AS checkticketdate
        FROM product_options po 
        LEFT JOIN
            payment_information pi ON pi.product_option_id = po.id
        WHERE 
            pi.user_id = ? AND 
            pi.payment_code = ? AND
            pi.product_option_id = ? 
        `,
        [userId, paymentCode, productOptionId]
    );
    return checkdate;
} catch {
    const error = new Error('dataSource Error');
    error.statusCode = 400;

    throw error;
}
};

const checkTicketList = async( userId, paymentCode, productOptionId ) => {
    try{
        const [checklist] = await appDataSource.query(
            `
            SELECT 
                id
            FROM payment_information
            WHERE 
                user_id = ? AND 
                payment_code = ? AND
                product_option_id = ? 
            `
            ,
            [ userId, paymentCode, productOptionId ]
        );
        return checklist
    } catch {
        const error = new Error('dataSource Error');
        error.statusCode = 400;
    
        throw error;
    };
};

const deleteBookingTicket = async( userId, paymentCode, productOptionId) => {
    const deleteRows = (await appDataSource.query(
        `
        UPDATE payment_information pi
        SET pi.deleted_at = CURRENT_TIMESTAMP()
        WHERE 
            pi.user_id = ? AND
            pi.payment_code = ? AND
            pi.product_option_id = ? 
        `,
        [ userId, paymentCode, productOptionId ]
    ))

	return deleteRows
};

 

 

4.  About Good  & Problem 

🔵 Good 

- 매일 아침 Stand Meeting 

팀원 모두가 매일 아침 10~11시에 meeting을 통해 진행사항 및 오류들을 공유하며 파악한 점이 좋은 경험이었다고 생각합니다. 서로 계속해서 소통을 하며 힘들어도 서로 이끌어 주는 팀 간의 좋은 상호작용이 있었으며 팀 간의 협업이 힘을 준 소중한 경험을 하게 되었습니다. 

 

- 백엔드 팀원 간의 오류 공유 및 해결 

제가 팀원들의 코드를 계속해서 확인하며 오류 발생 시 팀원과 공유가 되어 같이 해결하면서 백엔드 API 기능 상 모든 흐름들을 파악할 수 있는 좋은 시간을 겪었습니다. 

저의 코드만 이해하는 것이 아닌 모든 사람들의 코드에 대해 이해할 수 있었으며 오류를 해결하고 알려주며 저에게도 성장할 수 있는 시간을 가졌고 더 다양한 오류들을 빠르게 해결할 수 있는 능력또한 이번 프로젝트를 통해 발전했다고 느꼈습니다. 

 

 

🔴 Problem

- ERD 설계의 미흡 

ERD 설계 시 너무 많은 정규화를 하여 쿼리문 작성 시 많은 JOIN 문이 필요하여 쿼리 성능 과 비용 효율성의 이점을 극대화하지 못한 점에 대해 제일 아쉬웠고 Front-End의 정확한 요구사항이 DB 생성 이후에 있어 계속해서 수정해야 하는 문제점이 있었습니다. 

해당 아쉬움들을 통해 추후에는 이런 요구사항을 Back-End 뿐 아니라 Front-End와도 정확하게 이야기하며 정해 ERD 설계하여 이후에 수정해야 하는 일을 줄여야겠다고 생각했습니다. 

해당 ERD는 앞으로 팀원들과 진행할 Refactoring을 통해 너무 정규화된 ERD Table을 합치려 생각하고 있습니다. 

 

- TypeORM의 쿼리빌더 미사용 

이번 프로젝트에서 직접 쿼리빌더를 만들어 사용하여 이미 TypeORM 에서 제공하는 쿼리빌더를 미사용 한 점에 대해 아쉬운 점이라고 생각합니다. TypeORM 쿼리 빌더를 사용시 타입 안정성, ORM 기능, DB 독립성, 쿼리 작성의 용이성, 보안성, 코드유지 보수성과 같은 다양한 이점을 제공하는 것을 사용하지 못했으며 이후 해당 내용 또한  Refactoring 과정에서 이미 만들어둔 쿼리 빌더를 기준으로 TypeORM의 쿼리빌더를 적용하여 TypeORM 쿼리빌더가 가진 이점을 사용해볼 예정입니다. 

 

- 빠른 Front - Back의 통신 부재 및 부족한 MVP 설정

이번 프로젝트에서는 F와 B의 통신이 늦어져 통신을 하며 코드의 수정을 통해 조금 더 많은 시간의 소요가 들어갔으며 

프로젝트를 진행할 때 우선적으로 MVP를 설정하여 중요하고 최소한의 기능을 목표로 설정하지 못한 점에서 아쉬움을 느꼈습니다. 

MVP를 설정하여 해당 기능을 우선으로 완성해 프 백 간의 통신이 완료된 후 추가 기능을 고려하여 프로젝트를 진행해야 한다는 것을 이번 프로젝트의 프,백의 통신을 통한 시간 소요를 통해 깨달았습니다.

추후 진행하는 프로젝트에서는 해당 느꼈던 바를 참고하여 MVP를 설정하여 해당 기능을 우선으로 완성해 프 백 간의 통신이 완료 된 후 추가 기능을 고려하여 프로젝트를 진행해야 한다는 것을 명확하게 알고 진행해야겠다 생각했습니다. 

 

반응형

'BackEnd' 카테고리의 다른 글

[AWS] EC2란?  (0) 2023.10.04
Git & GitHub  (0) 2023.10.02
Wecode 2st Project 회고록  (0) 2023.09.03
Wecode 1차 프로젝트 회고록  (0) 2023.08.20
Database  (0) 2023.08.15

댓글