[2024.07.05 TIL] 내일배움캠프 57일차 (공연 예매 개인과제, 티켓 예매 취소)

My_Code·2024년 7월 5일
0

TIL

목록 보기
73/112
post-thumbnail

본 내용은 내일배움캠프에서 활동한 내용을 기록한 글입니다.


💻 TIL(Today I Learned)

📌 Today I Done

✏️ 좌석 예매 취소하기 구현

  • 좌석 예매 취소하기 기능은 로그인한 사용자가 자신이 예매한 공연의 티켓 ID를 기반으로 예매를 취소하는 기능임

  • 우선 DTO를 통해서 사용자에게 취소할 티켓의 ID를 받아옴

import { IsNotEmpty, IsNumber } from 'class-validator';

export class CancelTicketDto {
  // 티켓 ID
  @IsNumber()
  @IsNotEmpty({ message: '티켓 ID를 입력해 주세요.' })
  ticketId: number;
}

✏️ Controller의 예매 취소 메서드

  • Controller에서는 DTO를 통해 전달받은 사용자의 입력값을 Service의 예매 취소 메서드로 전달함

  • 예매 취소 기능은 로그인한 사용만 사용할 수 있기 때문에 AuthGuard('jwt')를 통해 해당 Access Token이 유효한지 확인함

  • 이 때, Path Parameter로 ShowId를 받아오는 공연 좌석 예매 메서드의 주소와 겹칠 수 있음

  • 그렇기에 예매 취소 메서드는 좌석 예매 메서드보다 위에 선언되어야 함

// seat.controller.ts

...

@Controller('seats')
export class SeatController {
  constructor(private readonly seatService: SeatService) {}

  // 공연 예매 취소
  @UseGuards(AuthGuard('jwt'))
  @Post('/cancel')
  async cancelTicket(@UserInfo() user: User, @Body() cancelTicketDto: CancelTicketDto) {
    return await this.seatService.cancelTicket(user, cancelTicketDto);
  }

  // 공연 좌석 예매
  @UseGuards(AuthGuard('jwt'))
  @Post('/:showId')
  async reserveSeat(
    @UserInfo() user: User,
    @Param('showId') showId: string,
    @Body() reserveSeatDto: ReserveSeatDto,
  ) {
    return await this.seatService.reserveSeat(user, +showId, reserveSeatDto);
  }

...

✏️ Service의 예매 취소 메서드

  • Servie에서의 예매 취소 비즈니스 로직은 아주 조금 복잡함

  • 예매를 취소하는 과정은 좌석을 예매하는 과정과 거의 유사함

  • 우선 DTO를 통해 전달 받은 티켓 ID이 유효한지 체크함

  • 티켓의 소유자와 로그인한 사용자가 같은지 확인하고 티켓에 있는 좌석 ID를 기반으로 해당 좌석 데이터를 가져옴

  • 그리고 예매 취소 과정에서 여러 개의 테이블을 수정하기 때문에 트랜젝션을 이용해서 하나로 묶어줌

  1. 사용자 포인트 환불
  2. 좌석의 예매 상태를 예매 가능 상태로 변경
  3. 잔여 좌석 수를 변경
  4. 티켓의 취소 여부 상태를 True로 변경
  5. 공연 예매 취소 완료
  • 위와 같은 과정을 하나의 트랜젝션에서 처리함
  // 공연 예매 취소
  async cancelTicket(user: User, cancelTicketDto: CancelTicketDto) {
    const { ticketId } = cancelTicketDto;

    // 해당 티켓이 존재하는지 확인
    const ticket = await this.ticketRepository.findOne({
      where: { id: ticketId },
    });
    if (_.isNil(ticket)) {
      throw new NotFoundException('예매된 티켓이 없습니다.');
    }

    // 티켓의 소유자와 로그인한 사용자가 같은지 확인
    if (ticket.userId !== user.id) {
      throw new UnauthorizedException('접근 권한이 없습니다.');
    }

    // 해당 좌석 조회
    const seat = await this.seatRepository.findOne({
      where: { id: ticket.seatId },
    });

    // 해당 공연의 좌석 수 조회
    const place = await this.showPlaceRepository.findOne({
      where: { showTimeId: seat.showTimeId },
    });

    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();

    try {
      // 사용자 포인트 환불
      const updatedUser = this.userRepository.merge(user, { point: user.point + ticket.price });

      // 해당 좌석 예매 상태 변경
      const updatedSeat = this.seatRepository.merge(seat, { isReserved: false });

      // 어떤 좌석의 수를 변경할지에 대한 객체 생성
      const condition = {};
      if (seat.grade === Grade.A) {
        condition['seatA'] = place.seatA + 1;
      } else if (seat.grade === Grade.S) {
        condition['seatS'] = place.seatS + 1;
      } else if (seat.grade === Grade.R) {
        condition['seatR'] = place.seatR + 1;
      } else {
        condition['seatVip'] = place.seatVip + 1;
      }

      // 잔여 좌석 수 변경
      const updatedPlace = this.showPlaceRepository.merge(place, condition);

      // 공연 예매 취소 (티켓 isCanceled: True)
      const canceledTicket = this.ticketRepository.merge(ticket, { isCanceled: true });

      await Promise.all([
        queryRunner.manager.save(updatedUser),
        queryRunner.manager.save(updatedSeat),
        queryRunner.manager.save(updatedPlace),
        queryRunner.manager.save(canceledTicket),
      ]);

      await queryRunner.commitTransaction();

      return { message: '공연 예매 취소에 성공했습니다.' };
    } catch (err) {
      await queryRunner.rollbackTransaction();
      throw new InternalServerErrorException('공연 예매 취소에 실패했습니다.');
    } finally {
      await queryRunner.release();
    }
  }


📌 Tomorrow's Goal

✏️ API 명세서 작성 및 동시성 테스트 진행

  • API 명세서가 아직 구체적인 내용이 작성되지 않았기 때문에 작성할 예정

  • 보너스 과제로 있는 동시성 테스트를 진행할 예정

  • 일단 JMeter를 사용해서 동시성 테스트를 진행할 예정

  • 사실 처음 써보기에 여러가지 시행착오가 있을 것 같음



📌 Today's Goal I Done

✔️ 좌석 예매 정보 조회 및 예매 취소 구현

  • 좌석 예매 정보 조회는 해당 공연 좌석들의 정보와 예매 가능 여부를 보기 위한 API

  • 클라이언트에게 해당 공연 ID의 좌석 정보를 좌석 테이블에서 find()해서 반환하는 것이기에 굉장히 간단하게 끝남

  • 그리고 예매 취소 기능도 구현함

  • 예매 취소는 좌석 예매 기능과 유사한 점이 많았음

  • 트랜젝션을 이용해서 사용자 포인트, 좌석 수, 좌석 상태, 티켓 상태을 묶어서 진행하면되기에 좌석 예매 기능과 구조는 크게 다르지 않았음

  • 이후에는 테스트를 통해서 짜잘한 버그들을 고치고 Insomnia도 고치면서 하루를 보냄



📌 ⚠️ 구현 시 발생한 문제

✔️ Insonmia의 말썽

  • 오늘 아침에 Insomnia의 자동 업데이트가 진행됨

  • 공연 등록 API에서 좌석당 5만 포인트라는 상한선이 존재하는 것을 확인하고 바로 수정에 들어감

  • 수정자체는 조건문 하나면 되기 때문에 굉장히 빠르게 끝남

  • 수정이 끝나고 Multipart 형태로 공연 등록 API를 테스트하니 무언가 이상한게 보였음

  • 내가 Send 버튼을 눌러서 보낸 값이 바로바로 적용되지 않는 것임

  • 처음에는 내가 코드를 잘못 짰나해서 코드를 확인하니 문제될 것은 하나도 없었음

  • 결국 튜터님께 도움을 요청하니 포스트맨, Terminal에서는 멀쩡히 잘 동작했기에 Insomnia 자체의 문제라는 결론이 나옴

  • 오류 발생 시나리오는 다음과 같았음

  1. 공연 등록 API에서 Multipart 형태로 데이터를 전송함
  2. 좌석의 가격을 3만 포인트로 설정해서 전송함
  3. 정상적으로 동작함
  4. 좌석의 가격을 30만 포인트로 바꿔서 전송함
  5. 성공함...?
  6. 실행 결과창을 보니 전송된 데이터가 3만 포인트임
  7. 데이터베이스도 확인하니 3만 포인트로 저장됨
  8. 다시 Send를 누르니 이제야 5만 포인트 초과의 에러 문구가 출력됨
  • 그래서 Insomnia 버전 자체의 문제라고 판단하고 다운그레이드를 진행함

  • 우선 Insomnia의 다운로드 페이지로 이동함 (https://insomnia.rest/download)

  • 위 사진의 버튼을 눌러서 Insomnia 깃허브 페이지로 이동함

  • 그 다음 현재 최신버전이 아닌 다른 릴리즈 버전을 선택하기 위해서 릴리스 메뉴로 이동

  • 검색 창에 9.2.0 버전을 검색

  • 9.2.0 페이지로 이동해서 제일 밑에 있는 다운로드 파일을 선택해서 다운로드 진행

  • 본인은 Window라서 .exe로 간단하게 설치 가능

  • 설치가 끝나면 기존에 띄워진 Insomnia 창은 닫고 윈도우 검색창에 Insomnia 검색해서 파일 위치 열기 버튼 클릭

  • 보통은 바로가기가 보일텐데 일단은 우클릭해서 속성 메뉴를 클릭

  • 대상의 위치를 복사해서 원본 Insomnia로 이동

  • 아까 다운받은 9.2.0 버전의 .exe 파일을 원본의 이름을 그대로 사용하면서 교체함

  • 위 과정을 모두 진행하면 Insomnia 다운그레이드가 완료됨

  • 사실 Postman으로 옮길까도 싶었지만 Insomnia에서 작업한 데이터들이 가까워서 일단 다운그레이드 과정을 진행함

profile
조금씩 정리하자!!!

0개의 댓글