interface Seat {
row: number; // 1-10
col: number; // 1-10
isBooked: boolean;
bookedBy?: number; // 사용자 ID
}
enum PaymentMethod {
CREDIT_CARD = 'CREDIT_CARD',
BANK_TRANSFER = 'BANK_TRANSFER',
KAKAO_PAY = 'KAKAO_PAY',
NAVER_PAY = 'NAVER_PAY',
TOSS_PAY = 'TOSS_PAY'
}
enum PaymentStatus {
PENDING = 'PENDING',
COMPLETED = 'COMPLETED',
FAILED = 'FAILED',
REFUNDED = 'REFUNDED'
}
interface Payment {
id: string;
amount: number;
method: PaymentMethod;
status: PaymentStatus;
transactionId?: string;
processedAt?: Date;
}
interface Booking {
id: string;
userId: number;
seatRow: number;
seatCol: number;
bookingTime: Date;
payment: Payment;
status: 'RESERVED' | 'CONFIRMED' | 'CANCELLED';
}
class Movie {
constructor(
public title: string = "어벤져스: 엔드게임",
public duration: number = 180,
public price: number = 12000
) {}
}
interface PaymentProcessor {
processPayment(amount: number, paymentData: any): Promise<Payment>;
refundPayment(transactionId: string): Promise<boolean>;
}
class CreditCardProcessor implements PaymentProcessor {
async processPayment(amount: number, cardData: any): Promise<Payment> {
// 신용카드 결제 로직
return {
id: crypto.randomUUID(),
amount,
method: PaymentMethod.CREDIT_CARD,
status: PaymentStatus.COMPLETED,
transactionId: `CC_${Date.now()}`,
processedAt: new Date()
};
}
async refundPayment(transactionId: string): Promise<boolean> {
// 환불 로직
return true;
}
}
class KakaoPayProcessor implements PaymentProcessor {
async processPayment(amount: number, kakaoData: any): Promise<Payment> {
// 카카오페이 결제 로직
return {
id: crypto.randomUUID(),
amount,
method: PaymentMethod.KAKAO_PAY,
status: PaymentStatus.COMPLETED,
transactionId: `KAKAO_${Date.now()}`,
processedAt: new Date()
};
}
async refundPayment(transactionId: string): Promise<boolean> {
return true;
}
}
class PaymentProcessorFactory {
static createProcessor(method: PaymentMethod): PaymentProcessor {
switch (method) {
case PaymentMethod.CREDIT_CARD:
return new CreditCardProcessor();
case PaymentMethod.KAKAO_PAY:
return new KakaoPayProcessor();
case PaymentMethod.NAVER_PAY:
return new NaverPayProcessor();
case PaymentMethod.TOSS_PAY:
return new TossPayProcessor();
default:
throw new Error(`Unsupported payment method: ${method}`);
}
}
}
class BookingSystem {
private seats: Seat[][];
private bookings: Map<string, Booking>;
private movie: Movie;
private paymentProcessors: Map<PaymentMethod, PaymentProcessor>;
constructor() {
this.initializeSeats();
this.bookings = new Map();
this.movie = new Movie();
this.initializePaymentProcessors();
}
// 좌석 초기화
private initializeSeats(): void {}
// 결제 프로세서 초기화
private initializePaymentProcessors(): void {
this.paymentProcessors = new Map();
Object.values(PaymentMethod).forEach(method => {
this.paymentProcessors.set(method, PaymentProcessorFactory.createProcessor(method));
});
}
// 예매하기 (결제 포함)
public async bookSeat(
userId: number,
row: number,
col: number,
paymentMethod: PaymentMethod,
paymentData: any
): Promise<string | null> {
// 좌석 확인
if (this.isSeatBooked(row, col)) {
return null;
}
// 결제 처리
const processor = this.paymentProcessors.get(paymentMethod);
if (!processor) {
throw new Error('Payment processor not found');
}
try {
const payment = await processor.processPayment(this.movie.price, paymentData);
// 예매 생성
const booking: Booking = {
id: crypto.randomUUID(),
userId,
seatRow: row,
seatCol: col,
bookingTime: new Date(),
payment,
status: 'CONFIRMED'
};
this.bookings.set(booking.id, booking);
this.seats[row-1][col-1].isBooked = true;
this.seats[row-1][col-1].bookedBy = userId;
return booking.id;
} catch (error) {
return null;
}
}
// 예매 취소 (환불 포함)
public async cancelBooking(bookingId: string): Promise<boolean> {
const booking = this.bookings.get(bookingId);
if (!booking || booking.status === 'CANCELLED') {
return false;
}
// 환불 처리
const processor = this.paymentProcessors.get(booking.payment.method);
if (processor && booking.payment.transactionId) {
await processor.refundPayment(booking.payment.transactionId);
booking.payment.status = PaymentStatus.REFUNDED;
}
// 좌석 해제
this.seats[booking.seatRow-1][booking.seatCol-1].isBooked = false;
this.seats[booking.seatRow-1][booking.seatCol-1].bookedBy = undefined;
booking.status = 'CANCELLED';
return true;
}
// 좌석 현황 보기
public getSeatStatus(): Seat[][] {}
// 특정 사용자 예매 내역
public getUserBookings(userId: number): Booking[] {}
// 결제 내역 조회
public getPaymentHistory(userId: number): Payment[] {
return Array.from(this.bookings.values())
.filter(booking => booking.userId === userId)
.map(booking => booking.payment);
}
}
// 이미 위에서 구현됨
// PaymentProcessor 인터페이스와 각 결제수단별 구현체
// PaymentProcessorFactory 클래스로 구현됨
class BookingSystem {
private static instance: BookingSystem;
public static getInstance(): BookingSystem {
if (!BookingSystem.instance) {
BookingSystem.instance = new BookingSystem();
}
return BookingSystem.instance;
}
}
interface BookingCommand {
execute(): Promise<string | null>;
undo(): Promise<boolean>;
}
class BookSeatCommand implements BookingCommand {
constructor(
private system: BookingSystem,
private userId: number,
private row: number,
private col: number,
private paymentMethod: PaymentMethod,
private paymentData: any
) {}
async execute(): Promise<string | null> {
return await this.system.bookSeat(
this.userId,
this.row,
this.col,
this.paymentMethod,
this.paymentData
);
}
async undo(): Promise<boolean> {
// 예매 취소 로직
return true;
}
}
// 빈 좌석 찾기
seats.flat().filter(seat => !seat.isBooked)
// 사용자별 예매 찾기
bookings.filter(booking => booking.userId === userId)
// 좌석 존재 여부
seats.some(row => row.some(seat => seat.row === targetRow))
// 모든 좌석 예매됨 확인
seats.every(row => row.every(seat => seat.isBooked))
// 예매 저장소
private bookings = new Map<string, Booking>();
// 중복 예매 방지
private activeBookings = new Set<string>();
describe('BookingSystem', () => {
let system: BookingSystem;
beforeEach(() => {
system = BookingSystem.getInstance();
});
test('should book available seat with payment', async () => {
const bookingId = await system.bookSeat(
1, 5, 5,
PaymentMethod.CREDIT_CARD,
{ cardNumber: '1234-5678-9012-3456' }
);
expect(bookingId).toBeTruthy();
});
test('should not book occupied seat', async () => {
await system.bookSeat(1, 5, 5, PaymentMethod.CREDIT_CARD, {});
const result = await system.bookSeat(2, 5, 5, PaymentMethod.KAKAO_PAY, {});
expect(result).toBeNull();
});
test('should process different payment methods', async () => {
const cardBooking = await system.bookSeat(1, 1, 1, PaymentMethod.CREDIT_CARD, {});
const kakaoBooking = await system.bookSeat(2, 1, 2, PaymentMethod.KAKAO_PAY, {});
const naverBooking = await system.bookSeat(3, 1, 3, PaymentMethod.NAVER_PAY, {});
expect(cardBooking).toBeTruthy();
expect(kakaoBooking).toBeTruthy();
expect(naverBooking).toBeTruthy();
});
test('should cancel booking and refund payment', async () => {
const bookingId = await system.bookSeat(1, 5, 5, PaymentMethod.TOSS_PAY, {});
const cancelled = await system.cancelBooking(bookingId!);
expect(cancelled).toBe(true);
});
});
describe('PaymentProcessors', () => {
test('should process credit card payment', async () => {
const processor = new CreditCardProcessor();
const payment = await processor.processPayment(12000, {});
expect(payment.method).toBe(PaymentMethod.CREDIT_CARD);
expect(payment.amount).toBe(12000);
expect(payment.status).toBe(PaymentStatus.COMPLETED);
});
test('should create correct processor from factory', () => {
const processor = PaymentProcessorFactory.createProcessor(PaymentMethod.KAKAO_PAY);
expect(processor).toBeInstanceOf(KakaoPayProcessor);
});
});
src/
├── models/
│ ├── Seat.ts
│ ├── Booking.ts
│ ├── Payment.ts
│ └── Movie.ts
├── services/
│ ├── BookingSystem.ts
│ └── PaymentService.ts
├── payments/
│ ├── PaymentProcessor.ts
│ ├── CreditCardProcessor.ts
│ ├── KakaoPayProcessor.ts
│ ├── NaverPayProcessor.ts
│ ├── TossPayProcessor.ts
│ └── PaymentProcessorFactory.ts
├── patterns/
│ ├── Singleton.ts
│ ├── Command.ts
│ ├── Strategy.ts
│ └── Factory.ts
├── utils/
│ └── validators.ts
└── tests/
├── BookingSystem.test.ts
├── PaymentProcessor.test.ts
└── integration.test.ts
핵심 학습 포인트:
실무에서 자주 사용되는 결제 시스템 패턴을 TypeScript로 완벽하게 학습할 수 있습니다!