TypeScript 로 진행하는 간단한 영화 예매 시스템

devswansong·2025년 9월 7일

간단한 영화 예매 시스템

요구사항

  • 영화: 1개 고정
  • 사용자: 정수 ID로 구분
  • 좌석: 10x10 = 100석
  • 예매/취소 기능

구현할 기능

1. 기본 모델

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
  ) {}
}

2. 결제 시스템 (Strategy Pattern)

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

3. 예매 시스템 클래스

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

적용할 디자인 패턴

1. Strategy Pattern (결제 시스템)

// 이미 위에서 구현됨
// PaymentProcessor 인터페이스와 각 결제수단별 구현체

2. Factory Pattern (결제 프로세서 생성)

// PaymentProcessorFactory 클래스로 구현됨

3. Singleton Pattern

class BookingSystem {
  private static instance: BookingSystem;
  
  public static getInstance(): BookingSystem {
    if (!BookingSystem.instance) {
      BookingSystem.instance = new BookingSystem();
    }
    return BookingSystem.instance;
  }
}

4. Command Pattern (예매/결제 트랜잭션)

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

활용할 내장 함수

Array 메소드

// 빈 좌석 찾기
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))

Map/Set 활용

// 예매 저장소
private bookings = new Map<string, Booking>();

// 중복 예매 방지
private activeBookings = new Set<string>();

테스트 시나리오

1. 기본 기능 테스트

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

2. Edge Case 테스트

  • 잘못된 좌석 번호
  • 동시 예매 시도
  • 결제 실패 시 예매 롤백
  • 존재하지 않는 예매 취소
  • 지원하지 않는 결제수단

실행 계획

Phase 1: 기본 구조

  • 인터페이스 및 클래스 정의 (Payment, Booking)
  • 좌석 초기화 로직
  • 결제 수단 enum 정의

Phase 2: 결제 시스템 구현

  • PaymentProcessor 인터페이스 구현
  • 각 결제수단별 프로세서 클래스 (신용카드, 카카오페이, 네이버페이, 토스페이)
  • PaymentProcessorFactory 구현

Phase 3: 예매 시스템 통합

  • 예매 로직 구현 (결제 포함)
  • 취소 로직 구현 (환불 포함)
  • 상태 조회 및 결제 내역 기능

Phase 4: 디자인 패턴 적용

  • Strategy Pattern (결제 시스템)
  • Factory Pattern (결제 프로세서 생성)
  • Singleton Pattern (예매 시스템)
  • Command Pattern (트랜잭션 관리)

Phase 5: 테스트

  • Jest 설정
  • 결제 프로세서 단위 테스트
  • 예매 시스템 통합 테스트
  • 비동기 결제 처리 테스트

프로젝트 구조

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

핵심 학습 포인트:

  • 5가지 결제수단 (Strategy Pattern)
  • 비동기 결제 처리 (Promise/async-await)
  • 트랜잭션 관리 (Command Pattern)
  • 에러 처리 및 롤백
  • 포괄적인 테스트 커버리지

실무에서 자주 사용되는 결제 시스템 패턴을 TypeScript로 완벽하게 학습할 수 있습니다!

profile
unagi.zoso == ziggy stardust == devswansong

0개의 댓글