Phase 2: Sabre 예약 API 심층 분석

1. API 엔드포인트 개요

1.1 예약 관련 엔드포인트

엔드포인트메서드API 타입기능위치
/internals/SABRE/bookingsPOSTSOAP예약 생성SabreBookingController.kt:22-46
/internals/SABRE/bookings/{pnr}GETSOAP예약 조회SabreBookingController.kt:48-51
/internals/SABRE/bookings/{pnr}/checkGETSOAP예약 검증SabreBookingController.kt:53-58
/internals/SABRE/bookings/{pnr}/cancelPUTSOAP예약 취소SabreBookingController.kt:60-71
/internals/SABRE/bookings/{pnr}/confirmPOSTSOAP예약 확정SabreBookingController.kt:73-78
/internals/SABRE/bookings/{pnr}/repricingPOSTSOAP재운임 계산SabreBookingController.kt:80-85
/internals/SABRE/bookings/{pnr}/dividePOSTSOAP예약 분리SabreBookingController.kt:87-97

2. 예약 생성 (Create Booking)

2.1 전체 예약 플로우

flowchart TD
    A[예약 요청 수신] --> B[운임 정보 조회<br/>getFareItinerary]
    B --> C[승객 identity 추가<br/>identityCode 매핑]

    C --> D{Session Token<br/>시작}
    D --> E[Step 1: markSeat<br/>좌석 마킹 + PriceQuote 생성]

    E --> F[Step 2: savePassengerInfo<br/>승객 정보 저장 + PNR 생성]
    F --> G[Step 3: getBooking<br/>예약 조회]

    G --> H{유아 SSR<br/>상태 체크}
    H -->|KK/HK/NN 아님| I[INFANT_SOLD_OUT<br/>비동기 취소]
    H -->|정상| J{스케줄<br/>상태 체크}

    J -->|confirmed/confirming 아님| K[SOLD_OUT<br/>비동기 취소]
    J -->|정상| L{CarrierTimeLimit<br/>null?}

    L -->|Yes| M[3초 대기 후<br/>retrieveOrNull]
    L -->|No| N{Cabin<br/>null?}

    M --> N
    N -->|Yes| O[retrieveOrNull<br/>재조회]
    N -->|No| P{승객 수<br/>불일치?}

    O --> P
    P -->|Yes| Q[retrieveOrNull<br/>재조회 + 검증]
    P -->|No| R[Session Token<br/>종료]

    R --> S[예약 완료<br/>Booking 반환]

     다크/라이트 모드 호환 색상
    style D fill:#A8D5BA,stroke:#333,stroke-width:2px,color:#000
    style H fill:#F4E4B1,stroke:#333,stroke-width:2px,color:#000
    style I fill:#F39C9C,stroke:#333,stroke-width:2px,color:#000
    style K fill:#F39C9C,stroke:#333,stroke-width:2px,color:#000
    style L fill:#F4E4B1,stroke:#333,stroke-width:2px,color:#000
    style M fill:#9FB4CE,stroke:#333,stroke-width:2px,color:#000
    style S fill:#95D5A6,stroke:#333,stroke-width:2px,color:#000
    style ERR fill:#E8B4B8,stroke:#333,stroke-width:2px,color:#000

2.2 Session Token 관리 패턴

stateDiagram-v2
    [*] --> getSessionToken: 세션 시작
    getSessionToken --> markSeat: Token 획득
    markSeat --> savePassengerInfo: PriceQuote 생성
    savePassengerInfo --> getBooking: PNR 생성
    getBooking --> Validation: 예약 정보 조회
    Validation --> closeSessionToken: 모든 검증 완료
    closeSessionToken --> [*]: 세션 종료

    markSeat --> Error: 예외 발생
    savePassengerInfo --> Error: 예외 발생
    getBooking --> Error: 예외 발생
    Error --> closeSessionToken: 세션 정리
    closeSessionToken --> AsyncCancel: PNR 취소
    AsyncCancel --> [*]: 완료

2.3 핵심 처리 단계

Step 0: 승객 Identity 추가

위치: SabreBookingService.kt:46-48

val addIdentityTypePassengers = passengers.map { passenger ->
    passenger.copy(identity = fareItinerary.passengerFares.first { it.type == passenger.type }.identityCode)
}
  • 목적: FareItinerary의 identityCode를 승객 정보에 매핑
  • 이유: Sabre는 승객 타입별 identity 코드 필요

Step 1: Session Token 획득

위치: SabreBookingService.kt:50

val token = sabreClient.getSessionToken()
  • 함수: SabreClient.kt:293-314
  • 반환: Session Token (String)
  • 특징: Amadeus와 달리 명시적 Token 관리 필요

Step 2: 좌석 마킹 (markSeat)

위치: SabreBookingService.kt:52-56

val priceQuoteMap = sabreClient.markSeat(
    token = token,
    fareItinerary = fareItinerary,
    passengers = addIdentityTypePassengers
)

SOAP API: SabreClient.kt:340-364

  • 엔드포인트: EnhancedAirBookRQ
  • 반환값: Map<PassengerType, String> (PriceQuote RPH)
  • 목적:
    1. 좌석 예약 및 가용성 확인
    2. PriceQuote 생성 (발권 시 사용)

처리 로직:

passengers.associate { passenger ->
    passenger.type to response.body.travelItineraryReadRS!!.travelItinerary
        .itineraryInfo.itineraryPricing!!.priceQuotes.first {
            it.pricedItinerary.airItineraryPricingInfo.passengerTypeQuantity.code == passenger.identity
        }.rph
}

Step 3: 승객 정보 저장 (savePassengerInfo)

위치: SabreBookingService.kt:57-66

val pnr = sabreClient.savePassengerInfo(
    token = token,
    validatingCarrier = fareItinerary.validatingCarrier,
    reservationUser = reservationUser,
    passengers = addIdentityTypePassengers,
    priceQuoteMap = priceQuoteMap,
    departureDate = fareItinerary.schedules.last().segments.last().departureAt.toLocalDate(),
    accountCode = fareItinerary.passengerFares.first { it.type == PassengerType.ADULT }
        .fareBases.firstOrNull { it.accountCode != null }?.accountCode
)

SOAP API: SabreClient.kt:366-401

  • 엔드포인트: PassengerDetailsRQ
  • 반환값: PNR (6자리 영숫자)
  • 포함 정보:
    • 승객 이름, 생년월일, 여권 정보
    • 연락처 (이메일, 전화번호)
    • PriceQuote RPH (markSeat에서 반환)
    • AccountCode (기업 할인 코드)
    • Agent 정보

Step 4: 예약 조회 및 검증 (getBooking)

위치: SabreBookingService.kt:67-89

sabreClient.getBooking(token = token, pnr = pnr).also { booking ->
    // 검증 1: 유아 SSR 상태 체크
    booking.passengers
        .flatMap { it.ssrInfos ?: emptyList() }
        .forEach {
            if (it.isInfantSoldOut) {
                cancelAsync(pnr = booking.pnr!!)
                throw StatusInvalidException(
                    ErrorMessage.INFANT_SOLD_OUT,
                    booking.pnr,
                    "INFT status is not KK or HK or NN : ${it.fullText}"
                )
            }
        }
 
    // 검증 2: 스케줄 상태 체크 (Amadeus와 동일)
    if (booking.schedules != null && booking.schedules.any { !(it.confirmed || it.confirming) }) {
        cancelAsync(pnr = booking.pnr!!)
        throw StatusInvalidException(ErrorMessage.SOLD_OUT, booking.pnr, "schedule is not confirmed")
    }
 
    removeFlightSearchKey(fareItinerary.requestKey)
    compareWithFareItinerary(booking = booking, fareItinerary = fareItinerary)
}

SOAP API: SabreClient.kt:663-680

  • 엔드포인트: GetReservationRQ
  • 검증 항목:
    1. 유아 SSR 상태 (KK/HK/NN만 허용)
    2. 스케줄 상태 (confirmed 또는 confirming)

3. 특별 처리 로직

3.1 유아 SSR 상태 검증

위치: SabreBookingService.kt:68-78

flowchart TD
    A[SSR 정보 조회] --> B{INFT SSR 존재?}
    B -->|No| C[검증 통과]
    B -->|Yes| D{Status 확인}
    D -->|KK| C
    D -->|HK| C
    D -->|NN| C
    D -->|기타| E[INFANT_SOLD_OUT<br/>예외 발생]
    E --> F[비동기 PNR 취소]

     다크/라이트 모드 호환 색상
    style B fill:#F4E4B1,stroke:#333,stroke-width:2px,color:#000
    style C fill:#9FB4CE,stroke:#333,stroke-width:2px,color:#000
    style H fill:#E8B4B8,stroke:#333,stroke-width:2px,color:#000

4.2 예외 타입별 처리

위치: SabreBookingService.kt:90-98

} catch (e: Exception) {
    if (e is StatusInvalidException &&
        (e.errorMessage == ErrorMessage.SOLD_OUT || e.errorMessage == ErrorMessage.INFANT_SOLD_OUT)) {
        removeFlightSearchKey(fareItinerary.requestKey)
        saveUnexposedFareItinerary(fareItinerary)
    }
    throw e
} finally {
    sabreClient.closeSessionToken(token)
}
예외 타입조건처리 방법
SOLD_OUT스케줄 상태 불일치운임 캐시 제거, unexposed 저장
INFANT_SOLD_OUT유아 SSR 상태 불일치운임 캐시 제거, unexposed 저장
기타 예외-Session Token 종료만 수행

4.3 비동기 PNR 취소

위치: SabreBookingService.kt:136-140

private fun cancelAsync(pnr: String) {
    CoroutineScope(Dispatchers.IO).withLaunch {
        cancelService.onlyPnrCancel(pnr)
    }
}

특징:

  • 비동기 실행: Fire and Forget
  • 목적: 실패한 예약 정리
  • 호출 시점: SOLD_OUT, INFANT_SOLD_OUT 발생 시

5. 예약 조회 (Retrieve)

5.1 조회 플로우

flowchart TD
    A[retrieve 요청] --> B{Session Token<br/>시작}
    B --> C[GetReservationRQ<br/>SOAP 호출]

    C --> D{스케줄<br/>존재?}
    D -->|No| E[ALREADY_CANCELED_PNR<br/>예외]
    D -->|Yes| F{validateScheduleStatus}

    F -->|true| G[스케줄 상태 검증<br/>validateSchedulesCancellation]
    F -->|false| H[검증 생략]

    G --> I{상태 확인}
    I -->|XX/UC 등| J[CANCELED_SCHEDULE<br/>예외]
    I -->|정상| K[Booking 객체 변환<br/>toBooking]

    H --> K
    K --> L[Session Token<br/>종료]
    L --> M[Booking 반환]

     다크/라이트 모드 호환 색상
    style B fill:#A8D5BA,stroke:#333,stroke-width:2px,color:#000
    style D fill:#F4E4B1,stroke:#333,stroke-width:2px,color:#000
    style F fill:#F4E4B1,stroke:#333,stroke-width:2px,color:#000
    style I fill:#F4E4B1,stroke:#333,stroke-width:2px,color:#000
    style E fill:#F39C9C,stroke:#333,stroke-width:2px,color:#000
    style J fill:#F39C9C,stroke:#333,stroke-width:2px,color:#000
    style M fill:#95D5A6,stroke:#333,stroke-width:2px,color:#000

5.2 Retrieve 함수

위치: SabreBookingService.kt:162-173

fun retrieve(pnr: String, validateScheduleStatus: Boolean = true): Booking {
    val token = sabreClient.getSessionToken()
    try {
        return sabreClient.getBooking(
            token = token,
            pnr = pnr,
            validateScheduleStatus = validateScheduleStatus,
        )
    } finally {
        sabreClient.closeSessionToken(token)
    }
}

특징:

  • Session Token 관리: try-finally로 보장
  • validateScheduleStatus: 스케줄 상태 검증 옵션
  • 단순 조회: markSeat 없이 PNR 정보만 조회

6. 검증 로직 (Validation)

6.1 승객 검증 (유아 나이 체크)

위치: SabreBookingService.kt:175-188

private fun validate(fareItinerary: FareItinerary, passengers: List<Passenger>) {
    // Sabre의 경우 유아는 소아로 좌석 점유 불가 (소아: 만 2세 이상 ~ 만 12세 미만)
    val lastItineraryDepartureDate = fareItinerary.schedules.last().segments.last().departureAt.toLocalDate()
    if (passengers.filter { it.type == PassengerType.CHILD }
            .any {
                !((it.birthDate!!.plusYears(2) <= lastItineraryDepartureDate) &&
                        (lastItineraryDepartureDate < it.birthDate.plusYears(12)))
            }
    ) {
        throw MethodArgumentInvalidException(
            ErrorMessage.INVALID_PASSENGERS_INFANT_TO_CHILD_BIRTH_DATE,
        )
    }
}

Sabre 특별 규칙:

  • 소아 (CHILD): 만 2세 이상 ~ 만 12세 미만
  • 유아 (INFANT): 만 2세 미만 (좌석 점유 불가)
  • 검증 기준: 마지막 여정의 출발일

6.2 FareItinerary 비교 검증

위치: SabreBookingService.kt:190-241

6.2.1 총 운임 비교

private fun compareTotalFare(booking: Booking, fareItinerary: FareItinerary) {
    booking.passengers.forEach {
        val bookingTotalFare = it.fare?.total
        val fareItineraryTotalFare =
            fareItinerary.passengerFares.firstOrNull { passengerFare -> passengerFare.type == it.type }?.total
        if (fareItineraryTotalFare != bookingTotalFare) {
            logger.error("[compareWithFareItinerary] different totalfare. (${booking.pnr}, $fareItineraryTotalFare, $bookingTotalFare)")
        }
    }
}

6.2.2 FareBasis 코드 비교

private fun compareFareBasisCodes(booking: Booking, fareItinerary: FareItinerary) {
    booking.passengers.forEach {
        val fareItineraryFareBasisCodes =
            fareItinerary.passengerFares.firstOrNull { passengerFare -> passengerFare.type == it.type }?.fareBasisCodes
        val bookingFarebasis = it.fareBasisCodes?.distinct()?.joinToString { "_" }
        val fareItineraryFarebasis = fareItineraryFareBasisCodes?.distinct()?.joinToString { "_" }
        if (bookingFarebasis != fareItineraryFarebasis) {
            logger.error("[compareWithFareItinerary] different ${it.type} farebasis. (${booking.pnr}, $fareItineraryFareBasisCodes, ${it.fareBasisCodes})")
        }
    }
}

6.2.3 BookingClass 비교

private fun compareBookingClass(booking: Booking, fareItinerary: FareItinerary) {
    booking.schedules?.forEachIndexed { index, schedule ->
        val bookingClass =
            fareItinerary.schedules.flatMap { it.segments.map { segment -> segment.bookingClass } }[index]
        if (schedule.bookingClass != bookingClass) {
            logger.error("[compareWithFareItinerary] different bookingclass. (${booking.pnr}, ${schedule.bookingClass}, $bookingClass)")
        }
    }
}

검증 전략:

  • 에러 로그만 기록, 예외 발생하지 않음
  • 목적: 데이터 일관성 모니터링

7. 예약 확정 (Confirm)

7.1 Confirm 프로세스

위치: SabreBookingService.kt:252-271

flowchart TD
    A[Confirm 요청] --> B[Session Token 획득]
    B --> C[getBooking 조회]
    C --> D{스케줄 상태<br/>confirmed?}
    D -->|No| E[onlyPnrCancel<br/>PNR 취소]
    D -->|Yes| F[Session Token 종료]
    E --> G[NOT_OK_SCHEDULE<br/>예외 발생]
    F --> H[Booking 반환]

     다크/라이트 모드 호환 색상
    style D fill:#F4E4B1,stroke:#333,stroke-width:2px,color:#000
    style F fill:#9FB4CE,stroke:#333,stroke-width:2px,color:#000
    style G fill:#9FB4CE,stroke:#333,stroke-width:2px,color:#000
    style K fill:#F4E4B1,stroke:#333,stroke-width:2px,color:#000
    style L fill:#F39C9C,stroke:#333,stroke-width:2px,color:#000
    style M fill:#F39C9C,stroke:#333,stroke-width:2px,color:#000
    style O fill:#95D5A6,stroke:#333,stroke-width:2px,color:#000

8.2 Repricing 로직

위치: SabreBookingService.kt:273-290

fun repricing(pnr: String): Booking {
    val token = sabreClient.getSessionToken()
    return try {
        val booking = sabreClient.getBooking(token = token, pnr = pnr)
        if (booking.priceQuoteCreatedAt.toLocalDate() != today()) {
            sabreClient.deletePriceQuote(token)
            sabreClient.repricing(token, booking.passengers)
            sabreClient.endTransaction(token)
            sabreClient.getBooking(token = token, pnr = pnr).also {
                validateFareBasisChange(originBooking = booking, newBooking = it)
            }
        } else {
            booking
        }
    } finally {
        sabreClient.closeSessionToken(token)
    }
}

처리 단계:

  1. PriceQuote 생성일 체크: 당일이 아니면 재생성
  2. deletePriceQuote: 기존 PriceQuote 삭제
  3. repricing: 새 PriceQuote 생성
  4. endTransaction: 변경사항 저장
  5. FareBasis 검증: 운임 기준 코드 변경 여부 확인

8.3 FareBasis 변경 검증

위치: SabreBookingService.kt:311-328 (Booking), SabreTicketingService.kt:199-216 (Ticketing)

private fun validateFareBasisChange(originBooking: Booking, newBooking: Booking) {
    newBooking.passengers.forEach { newPassenger ->
        val originFareBasis =
            originBooking.passengers.first {
                it.type == newPassenger.type && it.identificationKey == newPassenger.identificationKey
            }.fare?.fareComponents?.joinToString(",")
        val newFareBasis = newPassenger.fare?.fareComponents?.joinToString(",")
 
        if (originFareBasis != newFareBasis) {
            cancelService.onlyPnrCancel(pnr = originBooking.pnr!!)
 
            throw StatusInvalidException(
                ErrorMessage.SOLD_OUT,
                "${newPassenger.type.name} fareBasis is changed ($originFareBasis->$newFareBasis)",
            ).capture()
        }
    }
}

변경 시 처리:

  • PNR 취소 (운임 변경으로 예약 무효)
  • SOLD_OUT 예외 발생

9. 예약 분리 (Divide)

9.1 Divide 플로우

flowchart TD
    A[Divide 요청] --> B[retrieve 조회<br/>전체 승객 확인]
    B --> C[요청 승객의<br/>nameId 추출]
    C --> D[Session Token 획득]
    D --> E[openPnr<br/>PNR 열기]
    E --> F[splitPnr<br/>승객 분리]
    F --> G[confirmWithEndTransaction<br/>분리 확정]
    G --> H[saveSplitPnr<br/>새 PNR 저장]
    H --> I[endTransaction<br/>트랜잭션 종료]
    I --> J[Session Token 종료]
    J --> K[새 PNR로<br/>retrieve 조회]
    K --> L[Booking 반환]

    %% 다크/라이트 모드 호환 색상
    style D fill:#A8D5BA,stroke:#333,stroke-width:2px,color:#000
    style F fill:#9FB4CE,stroke:#333,stroke-width:2px,color:#000
    style H fill:#9FB4CE,stroke:#333,stroke-width:2px,color:#000
    style L fill:#95D5A6,stroke:#333,stroke-width:2px,color:#000

9.2 Divide 로직

위치: SabreBookingService.kt:292-309

fun divide(pnr: String, requestPassengers: List<PassengerIdentification>): Booking {
    val passengers = retrieve(pnr).passengers
    val nameNumbers = requestPassengers.mapNotNull { requestPassenger ->
        passengers.find { it.identificationKey == requestPassenger.identificationKey }?.nameId
    }
    val token = sabreClient.getSessionToken()
    val newPnr = try {
        sabreClient.openPnr(token, pnr)
        sabreClient.splitPnr(token, nameNumbers).also {
            sabreClient.confirmWithEndTransaction(token)
            sabreClient.saveSplitPnr(token)
            sabreClient.endTransaction(token)
        }
    } finally {
        sabreClient.closeSessionToken(token)
    }
    return retrieve(newPnr)
}

처리 단계:

  1. retrieve: 원본 PNR의 승객 정보 조회
  2. nameId 추출: 요청 승객의 nameId (내부 ID) 추출
  3. openPnr: PNR 편집 모드로 열기
  4. splitPnr: 지정 승객을 새 PNR로 분리
  5. confirmWithEndTransaction: 분리 확정
  6. saveSplitPnr: 새 PNR 저장
  7. retrieve: 새 PNR 정보 조회 및 반환

10. PNR 검증 (CheckPnr)

10.1 CheckPnr 로직

위치: SabreBookingService.kt:243-250

fun checkPnr(pnr: String) {
    val token = sabreClient.getSessionToken()
    try {
        sabreClient.checkPnr(token = token, pnr = pnr)
    } finally {
        sabreClient.closeSessionToken(token)
    }
}

SOAP API: SabreClient.kt:689-696

fun checkPnr(token: String, pnr: String) {
    val reservationRS = retrieve(token = token, pnr = pnr)
        .also { validateSchedulesCancellation(reservationRS = it, pnr = pnr) }
 
    reservationRS.checkPnr {
        throw InternationalAdapterException(ErrorMessage.PNR_CHECK_FAILED, ResponseMessage(it))
    }
}

검증 항목:

  • 스케줄 취소 여부 (validateSchedulesCancellation)
  • PNR 상태 (checkPnr)

11. Amadeus vs Sabre 비교

항목AmadeusSabre
API 타입SOAPSOAP
Session 관리Stateful Builder (Start → InSeries → End)getSessionToken → closeSessionToken
좌석 마킹markSeatmarkSeat (PriceQuote RPH 반환)
승객 정보 저장saveReservationInfosavePassengerInfo (PNR 반환)
PNR 생성savePnrWithShowWarningssavePassengerInfo 단계에서 생성
경고 처리최소 연결시간 체크없음
CarrierTimeLimit null3초 대기 후 재조회3초 대기 후 재조회 (동일)
Cabin null없음재조회 (Sabre 특유)
승객 수 불일치없음재조회 + 검증 (Sabre 특유)
유아 검증없음SSR 상태 체크 (KK/HK/NN)
비동기 PNR 취소pnrCancelAsynccancelAsync + onlyPnrCancel
FareBasis 검증없음Repricing 시 검증
Divide없음splitPnr 지원
CheckPnr없음checkPnr 지원

12. 주요 발견사항

12.1 Sabre 특유의 처리

  1. PriceQuote RPH 관리: markSeat에서 RPH 반환 → savePassengerInfo에 전달
  2. 3단계 재조회 로직:
    • CarrierTimeLimit null → 3초 대기 후 재조회
    • Cabin null → 재조회
    • 승객 수 불일치 → 재조회 + 검증
  3. 유아 SSR 상태 검증: KK/HK/NN만 허용
  4. FareBasis 변경 검증: Repricing 시 운임 변경 체크
  5. Divide 기능: 예약 분리 지원

12.2 Session Token 관리 전략

val token = sabreClient.getSessionToken()
try {
    // 모든 SOAP API 호출
} finally {
    sabreClient.closeSessionToken(token)
}
  • try-finally 패턴: 예외 발생 시에도 반드시 Token 종료
  • 리소스 관리: Session 누수 방지

12.3 비동기 처리 패턴

private fun cancelAsync(pnr: String) {
    CoroutineScope(Dispatchers.IO).withLaunch {
        cancelService.onlyPnrCancel(pnr)
    }
}
  • Fire and Forget: 응답 대기 없이 즉시 반환
  • 목적: 실패한 예약 정리 (사용자 응답 지연 방지)

12.4 검증 전략

  1. 실시간 검증: 유아 SSR, 스케줄 상태
  2. 모니터링 검증: TotalFare, FareBasis, BookingClass (로그만 기록)
  3. 재조회 전략: null 값 발견 시 재조회 (최대 3회)

13. 개선 가능 영역

13.1 TODO 주석 처리

위치: SabreBookingService.kt:256, 266

// TODO: EWR이 완료되면 schedule.confirming이 추가될 수 있게 되야 한다.
  • 현황: confirming 상태 미지원
  • 제안: EWR 완료 후 confirming 상태 추가

13.2 재조회 로직 통합

현황: CarrierTimeLimit null, Cabin null, 승객 수 불일치 시 각각 재조회

제안:

private fun retrieveWithRetry(pnr: String, maxRetries: Int = 3): Booking {
    repeat(maxRetries) {
        val booking = retrieveOrNull(pnr)
        if (booking != null &&
            booking.carrierTimeLimit != null &&
            booking.schedules?.all { it.cabin != null } == true) {
            return booking
        }
        delay(1000 * (it + 1))
    }
    throw InternationalAdapterException(ErrorMessage.RETRIEVE_FAILED, pnr)
}

13.3 검증 로직 일원화

현황: compareTotalFare, compareFareBasisCodes, compareBookingClass 분리

제안: 검증 결과를 구조화하여 반환

data class ValidationResult(
    val isValid: Boolean,
    val warnings: List<String>,
    val errors: List<String>
)

14. 참고 자료

14.1 주요 클래스

  • SabreBookingService.kt: 예약 서비스 (23-329)
  • SabreClient.kt: SOAP 클라이언트 (340-845)
  • SabreBookingController.kt: REST 컨트롤러 (22-97)

14.2 주요 메소드

  • SabreClient.markSeat(): 좌석 마킹 + PriceQuote 생성 (340-364)
  • SabreClient.savePassengerInfo(): 승객 정보 저장 + PNR 생성 (366-401)
  • SabreClient.getBooking(): 예약 조회 (663-680)
  • SabreBookingService.book(): 예약 생성 통합 (34-134)
  • SabreBookingService.repricing(): 재운임 계산 (273-290)
  • SabreBookingService.divide(): 예약 분리 (292-309)

14.3 SOAP API 목록

  • EnhancedAirBookRQ: 좌석 마킹
  • PassengerDetailsRQ: 승객 정보 저장
  • GetReservationRQ: 예약 조회
  • SplitDivideRQ: 예약 분리

14.4 주요 검증 항목

  • 유아 SSR 상태: KK, HK, NN만 허용
  • 스케줄 상태: confirmed 또는 confirming
  • CarrierTimeLimit: null 시 재조회
  • Cabin: null 시 재조회
  • 승객 수: 불일치 시 재조회