파트별로 코드 리뷰를 작성해보기
처음에 생각했던 예매 로직이다. 메모장에 끄적이면서 생각해보니 처음에 감도 못잡았던 로직들이 그나마 구체화됐고 원했던 방향으로 실현될 수 있었다.
예약 결제 - 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 등)