Phase 2: Sabre 예약 API 심층 분석
1. API 엔드포인트 개요
1.1 예약 관련 엔드포인트
| 엔드포인트 | 메서드 | API 타입 | 기능 | 위치 |
|---|---|---|---|---|
/internals/SABRE/bookings | POST | SOAP | 예약 생성 | SabreBookingController.kt:22-46 |
/internals/SABRE/bookings/{pnr} | GET | SOAP | 예약 조회 | SabreBookingController.kt:48-51 |
/internals/SABRE/bookings/{pnr}/check | GET | SOAP | 예약 검증 | SabreBookingController.kt:53-58 |
/internals/SABRE/bookings/{pnr}/cancel | PUT | SOAP | 예약 취소 | SabreBookingController.kt:60-71 |
/internals/SABRE/bookings/{pnr}/confirm | POST | SOAP | 예약 확정 | SabreBookingController.kt:73-78 |
/internals/SABRE/bookings/{pnr}/repricing | POST | SOAP | 재운임 계산 | SabreBookingController.kt:80-85 |
/internals/SABRE/bookings/{pnr}/divide | POST | SOAP | 예약 분리 | 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) - 목적:
- 좌석 예약 및 가용성 확인
- 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
- 검증 항목:
- 유아 SSR 상태 (KK/HK/NN만 허용)
- 스케줄 상태 (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)
}
}처리 단계:
- PriceQuote 생성일 체크: 당일이 아니면 재생성
- deletePriceQuote: 기존 PriceQuote 삭제
- repricing: 새 PriceQuote 생성
- endTransaction: 변경사항 저장
- 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)
}처리 단계:
- retrieve: 원본 PNR의 승객 정보 조회
- nameId 추출: 요청 승객의 nameId (내부 ID) 추출
- openPnr: PNR 편집 모드로 열기
- splitPnr: 지정 승객을 새 PNR로 분리
- confirmWithEndTransaction: 분리 확정
- saveSplitPnr: 새 PNR 저장
- 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 비교
| 항목 | Amadeus | Sabre |
|---|---|---|
| API 타입 | SOAP | SOAP |
| Session 관리 | Stateful Builder (Start → InSeries → End) | getSessionToken → closeSessionToken |
| 좌석 마킹 | markSeat | markSeat (PriceQuote RPH 반환) |
| 승객 정보 저장 | saveReservationInfo | savePassengerInfo (PNR 반환) |
| PNR 생성 | savePnrWithShowWarnings | savePassengerInfo 단계에서 생성 |
| 경고 처리 | 최소 연결시간 체크 | 없음 |
| CarrierTimeLimit null | 3초 대기 후 재조회 | 3초 대기 후 재조회 (동일) |
| Cabin null | 없음 | 재조회 (Sabre 특유) |
| 승객 수 불일치 | 없음 | 재조회 + 검증 (Sabre 특유) |
| 유아 검증 | 없음 | SSR 상태 체크 (KK/HK/NN) |
| 비동기 PNR 취소 | pnrCancelAsync | cancelAsync + onlyPnrCancel |
| FareBasis 검증 | 없음 | Repricing 시 검증 |
| Divide | 없음 | splitPnr 지원 |
| CheckPnr | 없음 | checkPnr 지원 |
12. 주요 발견사항
12.1 Sabre 특유의 처리
- PriceQuote RPH 관리: markSeat에서 RPH 반환 → savePassengerInfo에 전달
- 3단계 재조회 로직:
- CarrierTimeLimit null → 3초 대기 후 재조회
- Cabin null → 재조회
- 승객 수 불일치 → 재조회 + 검증
- 유아 SSR 상태 검증: KK/HK/NN만 허용
- FareBasis 변경 검증: Repricing 시 운임 변경 체크
- 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 검증 전략
- 실시간 검증: 유아 SSR, 스케줄 상태
- 모니터링 검증: TotalFare, FareBasis, BookingClass (로그만 기록)
- 재조회 전략: 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 시 재조회
- 승객 수: 불일치 시 재조회