TIL

좌석 예약 하기

용찬 2023. 9. 13. 22:30

전 글에서 좌석 예약을 받기 위해서 시간표를 만들어주었다.
이제 본격적으로 좌석 예약 및 결제를 위한 로직을 작성 할 시간이다.

import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm';
import { TimeTable } from './timeTable.entity';
import { Seat } from './seat.entity';
import { Room } from './room.entity';

@Entity()
export class Reservation {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  timeTableId: number;

  @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
  reservationTime: Date;

  @Column({ type: 'boolean', default: false })
  stats: boolean;

  @Column()
  seatId: number;

  @Column()
  userId: number;

  @ManyToOne(() => TimeTable, (timeTable) => timeTable.reservations)
  timeTable: TimeTable;

  @ManyToOne(() => Seat, (seat) => seat.reservation)
  seat: Seat;

  @ManyToOne(() => Room, (room) => room.reservations) // Room 엔터티와 관계 설정
  room: Room; 
}

예약을 확인하기 위한 entity

import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  CreateDateColumn,
  OneToMany,
} from 'typeorm';
import { TimeTable } from './timeTable.entity';
import { User } from './user.entity';

@Entity({ schema: 'apple', name: 'payment' })
export class Payment {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ type: 'integer' })
  points: number;

  @CreateDateColumn()
  paymentTime: Date;

  @Column()
  userId: number;

  @OneToMany(() => TimeTable, (timeTable) => timeTable.payments)
  timeTable: TimeTable;

  @OneToMany(() => User, (user) => user.payments)
  user: User;
}

결제 내용을 저장하기 위한 entity


 async makeReservation(
    timeTableIds: number[],
    seatIds: number[],
    userId : number
  ): Promise<void> {
    const reservations: Reservation[] = [];
    for (const timeTableId of timeTableIds) {
      const timeTable = await this.timeTableRepository.findOne({
        where: { timeTableId },
        select: ['timeSlot'],
      });

      if (!timeTable) {
        throw new Error(
          `해당 시간표를 찾을 수 없습니다. (timeTableId: ${timeTableId})`,
        );
      }

      for (const seatId of seatIds) {
        const seat = await this.seatRepository.findOne({ where: { seatId } });
        if (!seat) {
          throw new Error(`해당 좌석을 찾을 수 없습니다. (seatId: ${seatId})`);
        }
        console.log(seat)
        const existingReservation = await this.reservationRepository.findOne({
          where: { timeTableId, seatId },
        });
        if (!existingReservation) {
          const reservation = new Reservation();
          reservation.timeTableId = timeTableId;
          reservation.seatId = seatId;

          await this.reservationRepository.save(reservation);
          const paymentSuccessful = await this.processPayment(seat.price,userId); // 결제 처리 함수
          if (paymentSuccessful) {
            await this.reservationRepository.save(reservation);
            // 예약 취소 타이머 설정 (예: 30분 후에 취소)
            setTimeout(async () => {
              // 예약 상태 확인
              const updatedReservation =
                await this.reservationRepository.findOne({
                  where: { timeTableId, seatId },
                  select: ['stats'], // state를 조회하도록 변경
                });

              if (updatedReservation && !updatedReservation.stats) {
                // 이미 결제가 완료되었으면 취소하지 않음
                return;
              }
              // 예약 취소 처리
              updatedReservation.stats = true;
              await this.deleteReservation(reservation.id); // 예시: 예약 정보 삭제 함수
            }, 10 * 60 * 1000); // 10분 후에 실행 (밀리초 단위)
          } else {
            // 결제가 실패한 경우, 상태를 true로 변경하여 예약 취소
            await this.reservationRepository.update(timeTableId, {
              stats: true,
            }); // state를 true로 변경
            throw new Error(
              '결제가 실패하여 예약이 취소되었습니다. 남은 포인트를 확인해주세요',
            );
          }
          reservations.push(reservation);
        } else {
          throw new Error('해당 시간대에 예약할 수 없습니다.');
        }
      }
    }
  }

위 로직은 reservation에서 seatId와 timeTableId를 조회 후 두 가지 모두 해당하는 예약이 없으면 예약이 없음으로
예약이 있는지 확인 후 예약을 받고 혹시나 예약이 취소되는 경우 10분의 타임아웃을 설정해주었다.


 private async processPayment(amount: number, userId:number): Promise<boolean> {
   // const userId = 2; // userId 주입
   console.log(userId)
    const point = await this.pointRepository.findOne({ where: { userId } });
    if (!point) {
      throw new Error('포인트를 조회 할 수 없습니다.');
    }
    if (point.point < amount) {
      return false;
    }
    const payment = new Payment();
    payment.userId = userId;
    payment.points = amount;
    point.point -= amount;

    await this.paymentRepository.save(payment);
    await this.pointRepository.save(point);
    return true;
  }

  // 스케줄러 메서드: 매일 자정에 실행
  @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
  async handleScheduledTask() {
    try {
      console.log('자정에 예약 정보가 모두 삭제됩니다.');
      // 예약 정보 삭제
      await this.deleteSchedule();

      console.log('예약 정보 삭제가 완료되었습니다/');
    } catch (error) {
      console.error('예약 정보 삭제 중 error', error);
    }
  }

  private async deleteSchedule(): Promise<void> {
    // 예약 정보를 데이터베이스에서 삭제
    const reservation = await this.reservationRepository.find();
    if (!reservation) {
      throw new Error('예약을 찾을 수 없습니다.');
    }
    await this.reservationRepository.remove(reservation);
  }
}

위 로직은 makeReservation ( 예약 및 결제 ) 를 위해 호출하는 함수들이다.
예약은 db 및 여러가지 문제로 다음 날 예약만 받기로 했고
다음 날 예약을 받기 위해 db내 예약 정보를 삭제해주는 handelScheduledTask + deleteSchdule 과
선택한 예약 정보의 가격을 결제하기 위해 포인트를 조회하고 연산을 해주는 processPayment로 구성되어있다.