[내일배움캠프 - Nest.js 개인과제] Nest.js + TypeORM | 공연 예매 사이트 만들기 - 예매 파트, Nest.JS TypeORM 트랜잭션

sooyoung choi·2024년 1월 1일
0

내일배움캠프

목록 보기
10/19
post-thumbnail

공연 예매 사이트 과제 진행중에 고객이 좌석을 결제하다가 이미 예매된 좌석이 되었을땐 결제를 못하고 롤백 시키도록 트랜잭션을 해줬었다.
그런데 좌석 내역은 추가 되지 않고 롤백 되었으나 결제 내역은 추가 되는 상황이 발생하였다.
결국 해결하지 못하고 결제 상태 컬럼을 추가해서 true, false로 저장해둘까 했는데 그렇게 되면 너무 많은 쓸데없는 데이터만 늘어난다고 생각이 들며, typeORM 사이트, 구글 검색을 다 해봤었다.
결국 해답을 찾은건 Nest.js 공식문서에 나와있는 TypeORM Transactions 파트였다.

Nest.js TypeORM Transactions

사용해보기

Nest.js에서 TypeORM 트랜잭션을 쓰려면 데코레이터 트랜잭션이 아닌 DataSource를 가져와서 QueryRunner를 써야한다.

@Injectable()
export class UsersService {
  constructor(private dataSource: DataSource) {}
}

내가 트랜잭션을 쓸 부분에 공식 사이트에 나와있는 코드를 적용하면 된다.

// 공식사이트 코드
async createMany(users: User[]) {
  const queryRunner = this.dataSource.createQueryRunner();

  await queryRunner.connect();
  await queryRunner.startTransaction();
  try {
    await queryRunner.manager.save(users[0]);
    await queryRunner.manager.save(users[1]);

    await queryRunner.commitTransaction();
  } catch (err) {
    // since we have errors lets rollback the changes we made
    await queryRunner.rollbackTransaction();
  } finally {
    // you need to release a queryRunner which was manually instantiated
    await queryRunner.release();
  }
}

내 코드에 적용해보기


@Injectable()
export class PaymentService {
  constructor(
    @InjectRepository(Payment)
    private paymentRepository: Repository<Payment>,
    @InjectRepository(Seat)
    private seatRepository: Repository<Seat>,
    @InjectRepository(Performance)
    private performanceRepository: Repository<Performance>,
    @InjectRepository(Schedule)
    private scheduleRepository: Repository<Schedule>,
    @InjectRepository(Point)
    private pointRepository: Repository<Point>,
    private dataSource: DataSource,
  ) {}

async create(
    user: any,
    schedule_id: any,
    performance_id: any,
    createPaymentDto: CreatePaymentDto,
    createSeatDto: CreateSeatDto,
  ) {
    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();
    try {
      
      const { seats } = createSeatDto;

      // 유저가 선택한 공연
      const targetPerformance = await this.performanceRepository.findOne({
        where: { id: +performance_id },
      });
      
	 // 유저가 선택한 공연 스케줄
      const targetSchedule = await this.scheduleRepository.find({
        where: { performance: { id: +performance_id } },
      });

      const getSheduleWithId = await this.scheduleRepository.find({
        where: { id: schedule_id },
      });

      // 스케쥴 아이디에 따른 좌석들, 좌석 카운트용
      const getSeatCountWithScheduleId = await this.seatRepository.find({
        where: { schedule: { id: +schedule_id } },
      });

      let total_price = 0;

      if (!targetPerformance) {
        // targetPerformance가 null인 경우 예외 처리
        throw new Error('해당하는 공연이 없습니다.');
      }

      // 결제 생성
      const newPayment = await queryRunner.manager.save(Payment, {
        performance: { id: +performance_id },
        total_price,
        user_id: user.id,
      });

     // 등급별 좌석 금액
      let totalSeatPrice = 0;
      for (let i = 0; i < seats.length; i++) {
        const newGrade = seats[i].grade;
        const newSeatNum = seats[i].seat_num;

        let seatPriceWithGrade: number = 0;
        if (
          newGrade === 'V' &&
          getSeatCountWithScheduleId.length < getSheduleWithId[0].vip_seat_limit
        ) {
          seatPriceWithGrade = targetPerformance.price * 1.75;
        } else if (
          newGrade === 'R' &&
          getSeatCountWithScheduleId.length <
            getSheduleWithId[0].royal_seat_limit
        ) {
          seatPriceWithGrade = targetPerformance.price * 1.25;
        } else if (
          newGrade === 'S' &&
          getSeatCountWithScheduleId.length <
            getSheduleWithId[0].standard_seat_limit
        ) {
          seatPriceWithGrade = targetPerformance.price;
        }

        // 좌석이 예매됐는지 확인
        // 됐으면 payment도 x
        const reservedSeat = await queryRunner.manager.findOne(Seat, {
          where: { grade: newGrade, seat_num: newSeatNum },
        });

        console.log('reservedSeat: ', reservedSeat);

        if (reservedSeat !== null) {
          throw new Error();
          // return { success: false, message: '이미 예약된 좌석입니다.' };
        }
        const newSeat = await queryRunner.manager.save(Seat, {
          payment: { id: newPayment.id },
          schedule: schedule_id,
          grade: newGrade,
          seat_num: newSeatNum,
          performance: targetPerformance.id,
          seat_price: seatPriceWithGrade, // seat_price 값을 targetPerformance.price로 설정
          user: { id: user.id },
        });
       
        ...

      // 트랜잭션 커밋
      await queryRunner.commitTransaction();
      return { success: true, message: 'Reservation successful', total_price };
    } catch (error) 
      // 에러가 생기면 롤백
      await queryRunner.rollbackTransaction();
    } finally {
      // 사용이 끝난 후에는 항상 queryRunner를 해제
      await queryRunner.release();
    }
  }
  ...
}

0개의 댓글