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

sooyoung choi·2024년 1월 3일
1

내일배움캠프

목록 보기
11/19
post-thumbnail

파트별로 코드 리뷰를 작성해보기

처음에 생각했던 예매 로직이다. 메모장에 끄적이면서 생각해보니 처음에 감도 못잡았던 로직들이 그나마 구체화됐고 원했던 방향으로 실현될 수 있었다.

예매 로직

예약 결제 - payment
user_id -> user guard


body값으로 받을 거: [ PerformanceId, ScheduleId, 등급, 좌석 번호(여러 개) ]
///////////////////////////////////////////////////////////
performance_id
schedule_id
seat_id
////////////////////////////////////////////////////////////


다른 테이블에서 참조 받아 사용 할 거:  [ 공연 가격 ] 
////////////////////////////////////////////////////////////
결제 로직
body값으로 받을 거 받아와서 save에 지정해주고,

seat_id가 여러개면 map 써서 하나씩 save              (예매내역 반환할때는 객체 배열로 묶어주기)

seat_id 따른 좌석 상태 true ? -> 
	결제 테이블 생성
		결제 상태 true, 결제 총합 totalprice = (performance.price * seat.grade따른 배율)
	좌석 테이블 생성
		body에서 받은 id값들, UserGuard에 인증된 user_id 같이 save
	포인트 테이블 변경
		deposit = 0,
		withdrawl = 결제 테이블 totalprice,
		(포인트의 total - withdrawl ) 값 update
false? -> retrun {success: false}
/////////////////////////////////////////////////////////////

trasactionStart
결제 시작전에 체크 ( 좌석 상태 check fasle면 rollback)
=> 
for문 돌리기
save 객체배열 형식 만들어 넣기 - ts, function

reservation 테이블 생성 {
jwt로 얻은 UserId 삽입

body로 받은 PerformanceId 삽입

body로 받은 ScheduleId 삽입

body로 받은 SeatId 삽입
    (
    body로 받은 등급 삽입 => 등급에 따라 가격 다르게 설정
    body로 받은 좌석 번호 삽입 => 좌석 번호의 개수에 따라 배수 다르게 설정 ex) 가격 * 등급 * 개수
    )
}


좌석 테이블 생성
~



등급, 좌석개수 그리고 공연 가격을 계산해서 총 가격을 계산하는 변수 = point

const userPoint = balance find({
where: {UserId: jwtid}
orderby: {createdAt: 'DESC'},
take: 1
select: [ balance ]
})

포인트 테이블 생성
save({ 
income: 0,
expense: point,
balance: userPoint - point
})

결제 완료전에 체크 ( 좌석 상태 check fasle면 rollback)
transactioncommit


동시성 처리

예매 취소 로직

예매 취소

✔ payment entity 수정 -> status 넣어주기 (defalut: true 0)
schedule entitiy 수정 -> start_at, end_at 시간날짜형식 변경 

CONTROLLER-------------------------------------------
@Delete(':paymentId')
body: @UserInfo(user), @Param paymentId


SERVICE----------------------------------------------
필요한거: user.id, performance.id, payment.status(true 0,false 1), schedule.id(좌석수), seat.id(안에 user.id) 


---------------------- 예매 취소 --------------------------------------------------------------
로그인한 유저아이디가 선택한 공연에서의 스케줄의 좌석을 취소
좌석 삭제 - 좌석(유저아이디 + 결제 아이디 같은거) -> 내역 자체 삭제
결제 status -> false (삭제아님)
포인트 - 최근 값에서 결제 아이디와 같은 withdraw 값을 deposit에 넣어주고 최근 balance 값에 deposit 값 더해주기
---------------------------------------------------------------------------------------------


공연 3시간 전임?
현재 날짜 시간, 공연 시작 날짜 시간, TIMEDIFF 비교 3시간 전이면 취소 가능

공연 스케줄별 date, start_at 가져오기
현재 시간 


-----------------------SEAT--------------------------------------
// 좌석 삭제
await queryRunner.manager.delete(Seat, {
	where: { 
		user: {id: user.id },
		payment: { id: paymentId }
	}
})



-----------------------PAYMENT--------------------------------------

// 해당 payment status값 false 변경
await queryRunner.manager.update(Payment, {id: paymentId, status: false }) ???


-----------------------POINT--------------------------------------
// 포인트 - 최근 값(currentPoint)에서 결제 아이디와 같은 withdraw 값을 deposit에 넣어주고 최근 balance 값에 deposit 값 더해주기

const currentPoint = await queryRunner.manager.findOne(Point, {
        where: { user: { id: user.id } },
        order: { created_at: 'DESC' },
	take: 1,
});

const curretPointValue = currentPoint.balance;


-----------------------환불 전 결제상태 false 인지 재확인--------------------------------------
const targetPaymentStatus = await queryRunner.manager.findOne(Payment, { 
	where: { 
		id: paymentId
	},
	select: ['status'] 
})

if(targetPaymentStatus.status) {
	throw new Error();
}

const refundedPoint = await queryRunner.manager.findOne(Point, {
        where: { id: paymentId },
});

// 환불
await queryRunner.manager.save(Point, {
        user: { id: user.id },
        payment: { id: newPayment.id },
        deposit: refundedPointValue,
        withdraw: 0,
        balance: currentBalance + refundedPointValue,
});


등록했을때
start_date 2024-01-02
end_date 2024-01-03
start_at 2024-01-02 20:00:00
end_at 2024-01-02 22:00:00


적용된 로직

예매 파트

 async create(
    user: any,
    schedule_id: any,
    performance_id: any,
    createPaymentDto: CreatePaymentDto,
    createSeatDto: CreateSeatDto,
  ) {
    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    // 동시성 처리 - 격리 수준 READ COMMITTED
    await queryRunner.startTransaction('READ COMMITTED');
    try {
      const { seats } = createSeatDto;

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

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

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

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

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

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

      // 스케줄 시간
      const scheduleStartAt = getSheduleWithId[0].start_at;
      // 현재 시간
      const nowDate = new Date();

      const timeDifference = scheduleStartAt - nowDate;
      console.log('timeDifference: ', timeDifference);
      const hoursDifference = timeDifference / (1000 * 60 * 60);
     
      // 차이가 나지 않다면 해당 시간인 것
      if (hoursDifference <= 0) {
        throw new Error('공연 시작 시간 이후로는 예매 불가');
      }

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

      // 등급별 좌석 금액
      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 (
          newSeatNum > getSheduleWithId[0].vip_seat_limit ||
          newSeatNum > getSheduleWithId[0].royal_seat_limit ||
          newSeatNum > getSheduleWithId[0].standard_seat_limit
        )
          throw new Error('좌석 번호를 다시 입력하세요.');

        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 },
        });

        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: { id: targetPerformance.id },
          seat_price: seatPriceWithGrade, // seat_price 값을 targetPerformance.price로 설정
          user: { id: user.id },
        });
        totalSeatPrice += seatPriceWithGrade;
        console.log(newSeat);
      }

      // 포인트 차감
      // 가장 최신의 포인트 상태 가져오기
      const lastPoint = await queryRunner.manager.find(Point, {
        where: { user: { id: user.id } },
        order: { created_at: 'DESC' },
        take: 1,
      });

      // 잔액 없을때 차감 불가
      if (lastPoint[0].balance < totalSeatPrice)
        throw new Error('잔액이 부족합니다.');

      // 차감
      await queryRunner.manager.save(Point, {
        user: { id: user.id },
        payment: { id: newPayment.id },
        deposit: 0,
        withdraw: totalSeatPrice,
        balance: lastPoint[0].balance - totalSeatPrice,
      });

      // 트랜잭션 커밋
      await queryRunner.commitTransaction();
      return { success: true, message: '결제 성공' };
    } catch (error) {
      // 롤백 시에 실행할 코드 (예: 로깅)
      console.error('Error during reservation:', error);
      await queryRunner.rollbackTransaction();
      return { status: 404, message: error.message };
    } finally {
      // 사용이 끝난 후에는 항상 queryRunner를 해제
      await queryRunner.release();
    }
  }


예매 목록 확인 파트

async findAll(user: any, user_id: number) {
    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    // 동시성 처리 - 격리 수준 READ COMMITTED
    await queryRunner.startTransaction('READ COMMITTED');
    try {
      
      // 사용자의 모든 결제 내역 
      // 결제 내역에는 공연 정보도 필요하다
      const allPayment = await queryRunner.manager.find(Payment, {
        where: {
          user: { id: user_id },
        },
        order: { created_at: 'DESC' },
        relations: ['performance'],
      });

     
      await queryRunner.commitTransaction();
      return { allPayment };
    } catch (error) {
      // 롤백 시에 실행할 코드 (예: 로깅)
      console.error('Error during reservation:', error);
      await queryRunner.rollbackTransaction();
      return { status: 404, message: error.message };
    } finally {
      // 사용이 끝난 후에는 항상 queryRunner를 해제
      await queryRunner.release();
    }
  }


예매 취소 파트

async remove(user: any, paymentId: number) {
    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    // 동시성 처리 - 격리 수준 READ COMMITTED
    await queryRunner.startTransaction('READ COMMITTED');

    const userId = user.id;

    try {
      // 공연 3시간 전?
      // 스케줄 가져오기
      const targetPayment = await queryRunner.manager.find(Seat, {
        where: { payment: { id: paymentId } },
        relations: ['schedule', 'user'],
      });

      const paymentUser = targetPayment[0].user.id;

      // 유저 확인
      if (paymentUser !== user.id) {
        throw new Error('권한이 없습니다.');
      }

      
      // 스케줄 시간
      const scheduleStartAt = targetPayment[0].schedule.start_at;
      // 현재 시간
      const nowDate = new Date();

      const timeDifference = scheduleStartAt - nowDate;
      const hoursDifference = timeDifference / (1000 * 60 * 60);

       // 공연 3시간 전?
      if (hoursDifference <= 3) {
        throw new Error('공연 시간 3시간 전 예매 취소 불가');
      }

      //  좌석 삭제
      await queryRunner.manager.delete(Seat, {
        user: { id: userId },
        payment: { id: paymentId },
      });

      // 결제 내역 상태 변경
      await queryRunner.manager.update(
        Payment,
        { id: paymentId },
        { status: paymentStatus.CANCLE },
      );

      const targetPaymentStatus = await queryRunner.manager.findOne(Payment, {
        where: {
          id: paymentId,
        },
        select: ['status'],
      });
      console.log(targetPaymentStatus);

      if (targetPaymentStatus.status !== 'CANCLE') {
        throw new Error('결제 상태 CANCLE 아님');
      } else {
        const currentPoint = await queryRunner.manager.find(Point, {
          where: { user: { id: user.id } },
          order: { created_at: 'DESC' },
          take: 1,
        });

        // 현재 잔액
        const currentBalance = currentPoint[0].balance;

        const refundedPoint = await queryRunner.manager.findOne(Point, {
          where: { payment: { id: paymentId } },
        });

        // 환불 받을 금액
        const refundedPointValue = refundedPoint.withdraw;

        // 환불
        await queryRunner.manager.save(Point, {
          user: { id: user.id },
          payment: { id: paymentId },
          deposit: refundedPointValue,
          withdraw: 0,
          balance: currentBalance + refundedPointValue,
        });
      }

      await queryRunner.commitTransaction();
      return { success: true, message: '결제 취소 완료' };
    } catch (error) {
      // 롤백 시에 실행할 코드 (예: 로깅)
      console.error('Error during reservation:', error);
      await queryRunner.rollbackTransaction();
      return { status: 404, message: error.message };
    } finally {
      // 사용이 끝난 후에는 항상 queryRunner를 해제
      await queryRunner.release();
    }
  }

데이터베이스의 이해도가 부족해서 많이 헤맸던 예매 파트였다. erd 수정도 꽤 많이 진행했으며 수정할때마다 데이터베이스에 저장되어있던 컬럼들을 지워가며 답답한 일도 많았지만, 구현해놓으니 뿌듯하다.
많은 걸 배울 수 있었던 예매파트였다. (트랜잭션, mysql, typeorm 등)

0개의 댓글