Singapore Airlines 발권 API 심층 분석
1. API 엔드포인트 개요
1.1 발권 관련 엔드포인트
| 엔드포인트 | 메서드 | API 타입 | 기능 | 위치 |
|---|---|---|---|---|
/internals/SINGAPOREAIR/ticketing/ready | POST | SOAP (NDC) | 발권 준비 (예약 조회) | SingaporeairTicketingService.kt:27-30 |
/internals/SINGAPOREAIR/ticketing | POST | SOAP (NDC) | 발권 실행 | SingaporeairTicketingService.kt:32-62 |
/internals/SINGAPOREAIR/ticketing/reissue | POST | SOAP (NDC) | 재발행 | SingaporeairTicketingService.kt:64-95 |
2. 발권 준비 (Ticketing Ready)
2.1 전체 플로우
flowchart TD A[발권 준비 요청] --> B[예약 조회<br/>retrieve] B --> C[스케줄 정보<br/>반환] style B fill:#9FB4CE,stroke:#333,stroke-width:2px,color:#000 style C fill:#95D5A6,stroke:#333,stroke-width:2px,color:#000
2.2 상세 분석
위치: SingaporeairTicketingService.kt:27-30
fun ready(pnr: String): Pair<List<Passenger>?, List<Schedule>> {
val booking = singaporeairClient.retrieve(pnr)
return null to booking.schedules!!
}처리 내용:
- PNR로 예약 조회
- 스케줄 정보 반환
- 승객 정보는 null 반환 (NDC 특성)
Galileo와의 차이점:
| 항목 | Singapore Air (NDC) | Galileo |
|---|---|---|
| 발권 전 검증 | 없음 (예약 조회만) | validateBookingConditionForTicketing |
| 승객 정보 | null 반환 | 승객 정보 반환 |
| 스케줄 정보 | OrderViewRS에 포함 | UniversalRecordRetrieveRS에 포함 |
3. 발권 실행 (Issue)
3.1 전체 발권 플로우
flowchart TD A[발권 요청 수신] --> B[총 금액 계산<br/>cardPrice + cashPrice] B --> C[Payment 저장<br/>singaporeairClient.savePayment] C --> D{에러 발생?} D -->|Yes| E{NO_QUOTA<br/>에러?} E -->|Yes| F[Slack 알림<br/>sendNoQuota] E -->|No| G[에러 처리] F --> H[5초 대기 후<br/>비동기 취소] G --> H H --> I[예외 재발생] D -->|No| J[발권 완료<br/>승객 정보 반환] %% 타임아웃 처리 C --> K{타임아웃?} K -->|Yes| L[Slack 알림<br/>sendTicketingTimeout] L --> I style B fill:#A8D5BA,stroke:#333,stroke-width:2px,color:#000 style C fill:#9FB4CE,stroke:#333,stroke-width:2px,color:#000 style E fill:#F4E4B1,stroke:#333,stroke-width:2px,color:#000 style F fill:#E8B4B8,stroke:#333,stroke-width:2px,color:#000 style H fill:#9FB4CE,stroke:#333,stroke-width:2px,color:#000 style J fill:#95D5A6,stroke:#333,stroke-width:2px,color:#000
3.2 단계별 상세 분석
Step 1: 총 금액 계산
위치: SingaporeairTicketingService.kt:36
val amount = passengerPrices.sumOf { it.cardPrice + it.cashPrice }PassengerPrice 구조:
data class PassengerPrice(
val type: PassengerType, // ADT, CHD, INF
val cardPrice: Long, // 카드 결제 금액
val cashPrice: Long, // 현금 결제 금액
)총 금액 계산:
- 모든 승객의
cardPrice + cashPrice합산 - NDC는 승객별 분리 결제 지원
Step 2: Payment 저장 (OrderChangeRQ)
위치: SingaporeairTicketingService.kt:38-59
val booking = try {
singaporeairClient.savePayment(
pnr = pnr,
amount = amount,
timeoutCallback = {
slackService.sendTicketingTimeout(
supplier = Supplier.SINGAPOREAIR,
pnr = pnr,
)
}
)
} catch (e: Exception) {
// NO_QUOTA 특별 처리
if (e is ApiException && e.errorMessage == ErrorMessage.NO_QUOTA) {
slackService.sendNoQuota(
supplier = Supplier.SINGAPOREAIR,
airline = "SQ",
reason = e.message
)
}
// 발권 실패 시 5초 대기 후 비동기 취소
cancelAsync(pnr)
throw e
}타임아웃 콜백:
- 발권 API 타임아웃 시 Slack 알림
- 60초 타임아웃 (기본값)
에러 처리:
-
NO_QUOTA 에러: 항공사 할당량 초과
- Slack 알림 전송 (항공사별)
- 비동기 취소 실행
-
기타 에러:
- 비동기 취소 실행
- 예외 재발생
Step 3: 비동기 취소 처리
위치: SingaporeairTicketingService.kt:97-111
private fun cancelAsync(pnr: String) {
CoroutineScope(Dispatchers.IO).withLaunch {
delay(5000) // 5초 대기
try {
singaporeairClient.cancel(pnr)
} catch (e: Exception) {
slackService.sendCancelFail(
supplier = Supplier.SINGAPOREAIR,
pnr = pnr,
reason = e.message
)
throw e
}
}
}비동기 취소 특징:
- 5초 대기 후 취소 (시스템 안정화 대기)
- 비동기 처리 (API 응답 지연 방지)
- 취소 실패 시 Slack 알림
Galileo와의 차이점:
- Galileo: 즉시 비동기 취소 (5초 대기 있음)
- Singapore Air: 5초 대기 후 비동기 취소
- 공통점: 취소 실패 시 Slack 알림
4. savePayment API 상세 분석
4.1 전체 플로우
위치: SingaporeairClient.kt:366-409
fun savePayment(
pnr: String,
amount: Long,
timeoutCallback: () -> Unit,
): Booking {
val singaporeairApiProperties = singaporeairProperties.getApiProperties()
val request = OrderChangeRQ.ofPayment(
pnr = pnr,
amount = amount,
iataNumber = singaporeairApiProperties.iataCode,
agencyName = singaporeairApiProperties.agencyName
)
return singaporeairApiProperties.endpoint
.post(request)
.header(getHeaderMap(request))
.requestBodyConvert(soapRequestBodyConverter(singaporeairApiProperties))
.execute<OrderViewRS>(soapBodyDeserializerOf(logger, objectMapper))
.fold(
success = { orderViewRS ->
orderViewRS.also {
it.checkError { code, message ->
if (message.contains("MAXIMUM TICKET LIMIT REACHED")) {
throw InternationalAdapterException(ErrorMessage.NO_QUOTA, code, message)
} else {
// TODO payment error code 정리 필요(ref: Code set Dictionary 18.1.pdf - 9321)
throw InternationalAdapterException(
ErrorMessage.TICKETING_FAILED,
pnr,
code,
message
).capture()
}
}
}.response!!.toBooking()
},
failure = { failure ->
if (failure.isTimeout) {
timeoutCallback() // Slack 알림
}
throw failure.handleSoapFaultException(ErrorMessage.TICKETING_FAILED, pnr)
}
)
}4.2 에러 처리
NO_QUOTA 에러 처리
if (message.contains("MAXIMUM TICKET LIMIT REACHED")) {
throw InternationalAdapterException(ErrorMessage.NO_QUOTA, code, message)
}NO_QUOTA 특징:
- 항공사 할당량 초과 시 발생
- 메시지: “MAXIMUM TICKET LIMIT REACHED”
- Slack 알림: 항공사별로 전송
타임아웃 처리
failure = { failure ->
if (failure.isTimeout) {
timeoutCallback() // Slack 알림
}
throw failure.handleSoapFaultException(ErrorMessage.TICKETING_FAILED, pnr)
}타임아웃 특징:
- 60초 타임아웃 (기본값)
- 타임아웃 시 Slack 알림 전송
- 예외 재발생
5. Request/Response 구조 상세
5.1 OrderChangeRQ.ofPayment 구조
위치: infrastructure/request/OrderChangeRQ.kt
주요 필드:
data class OrderChangeRQ(
val query: Query,
) : SingaporeairRequest {
override val action = "http://www.iata.org/IATA/2015/00/2018.2/OrderChangeRQ"
companion object {
fun ofPayment(
pnr: String,
amount: Long,
iataNumber: String,
agencyName: String,
): OrderChangeRQ {
return OrderChangeRQ(
query = Query(
order = ChangeOrder.ofPayment(
pnr = pnr,
amount = amount,
iataNumber = iataNumber,
agencyName = agencyName
)
)
)
}
}
}ChangeOrder.ofPayment 구조:
data class ChangeOrder(
val actionType: ActionType, // UPDATE
val orderId: OrderId, // PNR
val payments: Payments?, // 결제 정보
) {
companion object {
fun ofPayment(
pnr: String,
amount: Long,
iataNumber: String,
agencyName: String,
): ChangeOrder {
return ChangeOrder(
actionType = ActionType(value = "UPDATE"),
orderId = OrderId(value = pnr),
payments = Payments(
payment = listOf(
Payment(
paymentMethod = PaymentMethod(
otherPaymentMethod = OtherPaymentMethod(
otherPaymentMethodID = OtherPaymentMethodID(value = "CASH"),
otherPaymentMethodText = OtherPaymentMethodText(value = "CASH")
),
cash = Cash(
cashInd = CashInd(value = true),
amount = Amount(
currencyAmountValue = CurrencyAmountValue(
currencyCode = CurrencyCode(value = "KRW"),
value = Value(value = amount.toString())
)
)
)
),
pointOfSale = PointOfSale(
country = Country(countryCode = "KR"),
agencyID = AgencyID(value = iataNumber),
agencyName = AgencyName(value = agencyName),
)
)
)
)
)
}
}
}Payment 구조:
data class Payment(
val paymentMethod: PaymentMethod, // 결제 수단
val pointOfSale: PointOfSale, // 판매 지점
)PaymentMethod 구조:
data class PaymentMethod(
val otherPaymentMethod: OtherPaymentMethod?, // CASH
val cash: Cash?, // 현금 결제 정보
val paymentCard: PaymentCard?, // 카드 결제 정보 (미사용)
)Cash 구조:
data class Cash(
val cashInd: CashInd, // true
val amount: Amount, // 금액
)
data class Amount(
val currencyAmountValue: CurrencyAmountValue,
)
data class CurrencyAmountValue(
val currencyCode: CurrencyCode, // KRW
val value: Value, // 금액
)5.2 OrderViewRS 구조 (발권 응답)
위치: infrastructure/response/OrderViewRS.kt
주요 필드:
data class OrderViewRS(
val response: Response?,
val errors: List<Error>?,
) {
data class Response(
val order: Order,
val dataList: DataList,
val warnings: List<Warning>?,
)
}Order 구조 (발권 후):
data class Order(
val orderId: OrderId, // PNR
val orderItems: OrderItems, // 주문 항목
val ownerCode: OwnerCode, // SQ
val statusCode: StatusCode, // TICKETED
val totalPrice: TotalPrice, // 총 가격
val bookingRefs: BookingRefs?, // 항공사 예약 번호
val paymentInfo: List<PaymentInfo>?, // 결제 정보
val ticketDocInfos: List<TicketDocInfo>?, // 티켓 정보
)StatusCode 값:
| 값 | 의미 |
|---|---|
| CONFIRMED | 예약 확정 |
| TICKETED | 발권 완료 |
| CANCELLED | 취소됨 |
TicketDocInfos 구조:
data class TicketDocInfos(
val ticketDocInfo: List<TicketDocInfo>,
)
data class TicketDocInfo(
val ticketDocNbr: TicketDocNbr, // 티켓 번호 (13자리)
val ticketDocTypeCode: TicketDocTypeCode, // 701 (E-Ticket), 702 (EMD)
val paxRefId: PaxRefId, // 승객 참조 ID
val reportingTypeCode: ReportingTypeCode?, // RPT
)TicketDocTypeCode 값:
| 코드 | 의미 |
|---|---|
| 701 | E-Ticket (항공권) |
| 702 | EMD (Electronic Miscellaneous Document, 부가서비스) |
PaymentInfo 구조:
data class PaymentInfo(
val paymentInfoId: PaymentInfoId, // 결제 ID
val amount: Amount, // 결제 금액
val paymentMethod: PaymentMethod, // 결제 수단
val paymentStatusCode: PaymentStatusCode, // 결제 상태
val paxRefIds: List<PaxRefId>?, // 승객 참조 ID
)6. 재발행 (Reissue)
6.1 전체 재발행 플로우
flowchart TD A[재발행 요청] --> B[원본 예약 조회<br/>retrieve] B --> C[FareItinerary<br/>조회] C --> D[Repricing<br/>repricingWithReissue] D --> E{가격 일치<br/>확인} E -->|불일치| F[RETICKETING_FAILED_BY_MISMATCH_PRICE] E -->|일치| G[재발행 실행<br/>reissue] G --> H[재발행 완료<br/>ReissueResult 반환] style B fill:#A8D5BA,stroke:#333,stroke-width:2px,color:#000 style D fill:#F4E4B1,stroke:#333,stroke-width:2px,color:#000 style E fill:#9FB4CE,stroke:#333,stroke-width:2px,color:#000 style F fill:#F39C9C,stroke:#333,stroke-width:2px,color:#000 style H fill:#95D5A6,stroke:#333,stroke-width:2px,color:#000
6.2 단계별 상세 분석
Step 1: 원본 예약 조회
위치: SingaporeairTicketingService.kt:69
val originBooking = singaporeairClient.retrieve(pnr)Step 2: FareItinerary 조회
위치: SingaporeairTicketingService.kt:70
val fareItinerary = singaporeairFlightSearchService.getFareItinerary(detailKey)detailKey:
- 재발행 검색 결과의 키
- Redis에 캐싱된 FareItinerary 조회
Step 3: Repricing (재가격 계산)
위치: SingaporeairTicketingService.kt:73-76
val passengerFares = singaporeairClient.repricingWithReissue(
booking = originBooking,
fareItinerary = fareItinerary
).passengerFaresrepricingWithReissue API: SingaporeairClient.kt:700-729
- 엔드포인트: NDC API (OrderReshopRQ with Pricing)
- 목적: 재발행 운임 재계산
- 반환값: FareItinerary (passengerFares 포함)
Step 4: 가격 일치 확인
위치: SingaporeairTicketingService.kt:78-86
if (prepaidPrice != passengerFares.sumOf {
(it.total + (it.carrierFee ?: 0)) * it.count
}) { //total = airPrice + tax
throw InternationalAdapterException(
ErrorMessage.RETICKETING_FAILED_BY_MISMATCH_PRICE,
pnr,
"price : $prepaidPrice , pricedTotal : ${passengerFares.sumOf { (it.total + (it.carrierFee ?: 0)) * it.count }}}"
)
}가격 계산:
총 가격 = Σ[(airPrice + tax + carrierFee) * count]가격 불일치 시:
RETICKETING_FAILED_BY_MISMATCH_PRICE예외 발생- 로그: 요청 가격 vs 재계산 가격
Step 5: 재발행 실행
위치: SingaporeairTicketingService.kt:88-92
val newBooking = singaporeairClient.reissue(
booking = originBooking,
price = prepaidPrice,
fareItinerary = fareItinerary
)reissue API: SingaporeairClient.kt:731-771
- 엔드포인트: NDC API (OrderChangeRQ with Reissue)
- 파라미터: 원본 예약, 가격, 새 FareItinerary
- 반환값: 새 Booking
Step 6: 결과 반환
위치: SingaporeairTicketingService.kt:94
return ReissueResult(booking = newBooking, passengers = passengerFares)ReissueResult 구조:
data class ReissueResult<B, P>(
val booking: B, // 새 Booking
val passengers: List<P>, // 승객별 운임
)7. reissue API 상세 분석
7.1 전체 플로우
위치: SingaporeairClient.kt:731-771
fun reissue(
booking: Booking,
price: Long,
fareItinerary: FareItinerary,
): Booking {
val singaporeairApiProperties = singaporeairProperties.getApiProperties()
val request = OrderChangeRQ.ofReissue(
booking = booking,
price = price,
fareItinerary = fareItinerary,
iataNumber = singaporeairApiProperties.iataCode,
agencyName = singaporeairApiProperties.agencyName
)
return singaporeairApiProperties.endpoint
.post(request)
.header(getHeaderMap(request))
.requestBodyConvert(soapRequestBodyConverter(singaporeairApiProperties))
.execute<OrderViewRS>(soapBodyDeserializerOf(logger, objectMapper))
.fold(
success = { orderViewRS ->
orderViewRS.checkError { code, message ->
throw if (message.contains("TICKET IS NOT ELIGIBLE FOR EXCHANGE")) {
InternationalAdapterException(
ErrorMessage.TICKET_IS_NOT_ELIGIBLE_FOR_EXCHANGE,
booking.pnr,
code,
message
)
} else {
InternationalAdapterException(ErrorMessage.RETICKETING_FAILED, booking.pnr, code, message)
}.capture()
}
orderViewRS.response!!.toReissueBooking(originBooking = booking)
},
failure = {
throw it.exception
}
)
}7.2 에러 처리
TICKET_IS_NOT_ELIGIBLE_FOR_EXCHANGE 에러
if (message.contains("TICKET IS NOT ELIGIBLE FOR EXCHANGE")) {
throw InternationalAdapterException(
ErrorMessage.TICKET_IS_NOT_ELIGIBLE_FOR_EXCHANGE,
booking.pnr,
code,
message
)
}발생 원인:
- 티켓이 재발행 불가능한 상태
- 운임 규정상 교환 불가
- 이미 사용된 티켓
7.3 OrderChangeRQ.ofReissue 구조
위치: infrastructure/request/OrderChangeRQ.kt
주요 필드:
companion object {
fun ofReissue(
booking: Booking,
price: Long,
fareItinerary: FareItinerary,
iataNumber: String,
agencyName: String,
): OrderChangeRQ {
return OrderChangeRQ(
query = Query(
order = ChangeOrder.ofReissue(
booking = booking,
price = price,
fareItinerary = fareItinerary,
iataNumber = iataNumber,
agencyName = agencyName
)
)
)
}
}ChangeOrder.ofReissue 구조:
data class ChangeOrder(
val actionType: ActionType, // REPLACE
val orderId: OrderId, // 원본 PNR
val deleteOrderItems: List<DeleteOrderItem>?, // 삭제할 항목
val addOrderItems: List<AddOrderItem>?, // 추가할 항목
val payments: Payments?, // 결제 정보
) {
companion object {
fun ofReissue(
booking: Booking,
price: Long,
fareItinerary: FareItinerary,
iataNumber: String,
agencyName: String,
): ChangeOrder {
return ChangeOrder(
actionType = ActionType(value = "REPLACE"),
orderId = OrderId(value = booking.pnr),
deleteOrderItems = booking.schedules!!.map { schedule ->
DeleteOrderItem(
orderItemRefId = OrderItemRefId(value = schedule.orderItemId)
)
},
addOrderItems = listOf(
AddOrderItem.of(fareItinerary)
),
payments = Payments.of(price, iataNumber, agencyName)
)
}
}
}재발행 로직:
- DeleteOrderItems: 기존 항공편 삭제
- AddOrderItems: 새 항공편 추가
- Payments: 차액 결제 정보
8. 에러 처리 및 재시도 메커니즘
8.1 에러 타입별 처리 매트릭스
| 에러 타입 | 발생 시점 | 처리 방법 | Slack 알림 | 비동기 취소 |
|---|---|---|---|---|
NO_QUOTA | savePayment | InternationalAdapterException | Yes (항공사별) | Yes (5초 대기) |
TICKETING_FAILED | savePayment | InternationalAdapterException | No | Yes (5초 대기) |
TICKETING_TIMEOUT | savePayment | SoapFaultException | Yes | No |
RETICKETING_FAILED_BY_MISMATCH_PRICE | Reissue | InternationalAdapterException | No | No |
TICKET_IS_NOT_ELIGIBLE_FOR_EXCHANGE | Reissue | InternationalAdapterException | No | No |
RETICKETING_FAILED | Reissue | InternationalAdapterException | No | No |
8.2 Slack 알림 케이스
1. 발권 타임아웃
timeoutCallback = {
slackService.sendTicketingTimeout(
supplier = Supplier.SINGAPOREAIR,
pnr = pnr,
)
}2. NO_QUOTA (할당량 초과)
if (e is ApiException && e.errorMessage == ErrorMessage.NO_QUOTA) {
slackService.sendNoQuota(
supplier = Supplier.SINGAPOREAIR,
airline = "SQ",
reason = e.message
)
}3. 취소 실패
slackService.sendCancelFail(
supplier = Supplier.SINGAPOREAIR,
pnr = pnr,
reason = e.message
)8.3 재시도 메커니즘
현재 상태: 재시도 로직 없음
비동기 취소만 지원:
- 5초 대기 후 취소
- 취소 실패 시 Slack 알림
9. GDS와의 차이점 비교
9.1 발권 플로우 비교
| 항목 | Singapore Air (NDC) | Galileo | Amadeus | Sabre |
|---|---|---|---|---|
| 발권 전 검증 | 없음 | validateBookingConditionForTicketing | 복잡한 상태 검증 | 상태 검증 |
| 발권 API | OrderChangeRQ (Payment) | AirTicketing | DocIssuance | TicketingClient |
| Payment 타입 | CASH (고정) | CASH | CASH | CASH |
| 티켓 조회 | OrderViewRS에 포함 | 별도 API | 별도 API | 별도 API |
| 재발행 | OrderChangeRQ.ofReissue | N/A | N/A | N/A |
| 비동기 취소 | 5초 대기 후 | 5초 대기 후 | 즉시 | 즉시 |
9.2 에러 처리 비교
| 에러 시나리오 | Singapore Air | Galileo | Amadeus | Sabre |
|---|---|---|---|---|
| 할당량 초과 | ”MAXIMUM TICKET LIMIT REACHED” → NO_QUOTA | N/A | ”QUOTA EXCEEDED" | "NO QUOTA” |
| 타임아웃 | Slack 알림 | Slack 알림 | Slack 알림 | Slack 알림 |
| 재발행 불가 | ”TICKET IS NOT ELIGIBLE FOR EXCHANGE” | N/A | N/A | N/A |
| 가격 불일치 | RETICKETING_FAILED_BY_MISMATCH_PRICE | N/A | N/A | N/A |
9.3 독특한 특징
Singapore Airlines NDC 고유 특징
-
OrderChangeRQ 다중 용도:
- Payment (발권)
- Seat (좌석 선택)
- Ancillary (부가서비스)
- Divide (예약 분할)
- Reissue (재발행)
-
통합 응답 구조:
- OrderViewRS에 예약/티켓 정보 모두 포함
- GDS처럼 별도 티켓 조회 API 불필요
-
재발행 지원:
- NDC 표준의 고유 기능
- GDS는 재발행 API 없음
-
NO_QUOTA 에러 처리:
- 메시지 기반 판별 (“MAXIMUM TICKET LIMIT REACHED”)
- 항공사별 Slack 알림
-
발권 전 검증 없음:
- GDS는 복잡한 상태 검증 수행
- NDC는 예약 조회만
10. 성능 최적화
10.1 비동기 처리
위치: SingaporeairTicketingService.kt:97-111
private fun cancelAsync(pnr: String) {
CoroutineScope(Dispatchers.IO).withLaunch {
delay(5000)
try {
singaporeairClient.cancel(pnr)
} catch (e: Exception) {
slackService.sendCancelFail(...)
throw e
}
}
}비동기 처리 대상:
- 발권 실패 시 예약 취소
성능 향상:
- API 응답 지연 방지
- 5초 대기 (시스템 안정화)
10.2 타임아웃 설정
// ClientSupport.kt
defaultTimeout = 60000 // 60초타임아웃 특징:
- 발권 API: 60초 (기본값)
- 타임아웃 시 Slack 알림
11. 주요 발견사항
11.1 Singapore Airlines NDC 발권 시스템의 특징
- OrderChangeRQ 기반: NDC 표준의 OrderChange 사용
- 통합 응답: OrderViewRS에 티켓 정보 포함
- 재발행 지원: NDC 고유 기능
- NO_QUOTA 처리: 메시지 기반 판별
- 발권 전 검증 없음: 예약 조회만 수행
11.2 개선 가능 영역
1. Payment Error Code 정리
현황: TODO 주석만 존재
// TODO payment error code 정리 필요(ref: Code set Dictionary 18.1.pdf - 9321)제안: Code set Dictionary 참고하여 에러 코드 문서화
2. 재발행 가격 불일치 로깅
현황: 예외 메시지에만 포함
"price : $prepaidPrice , pricedTotal : ${...}"제안: 로깅 추가
logger.warn(
"[SINGAPOREAIR] Reissue price mismatch, PNR: $pnr, " +
"requested: $prepaidPrice, calculated: ${passengerFares.sumOf { ... }}"
)3. 비동기 취소 로깅
현황: 로그 없음
제안: 로깅 추가
private fun cancelAsync(pnr: String) {
CoroutineScope(Dispatchers.IO).withLaunch {
logger.info("[SINGAPOREAIR] Cancelling PNR after ticketing failure: $pnr (5s delay)")
delay(5000)
try {
singaporeairClient.cancel(pnr)
logger.info("[SINGAPOREAIR] PNR cancelled successfully: $pnr")
} catch (e: Exception) {
logger.error("[SINGAPOREAIR] Failed to cancel PNR: $pnr", e)
slackService.sendCancelFail(...)
throw e
}
}
}12. 참고 자료
12.1 주요 클래스
SingaporeairTicketingService.kt: 발권 서비스 (27-111)SingaporeairClient.kt: SOAP 클라이언트 (366-409, 700-771)OrderChangeRQ.kt: 변경 요청 모델 (Payment, Reissue)OrderViewRS.kt: 응답 모델
12.2 주요 메소드
SingaporeairTicketingService.ready(): 발권 준비 (27-30)SingaporeairTicketingService.issue(): 발권 실행 (32-62)SingaporeairTicketingService.reissue(): 재발행 (64-95)SingaporeairClient.savePayment(): SOAP 발권 API (366-409)SingaporeairClient.reissue(): SOAP 재발행 API (731-771)
12.3 관련 API
- OrderChangeRQ: 주문 변경 (Payment, Reissue)
- OrderViewRS: 주문 조회 응답 (티켓 정보 포함)
- OrderReshopRQ: 재발행 검색/Pricing
이 문서는 Triple Air International Adapter 프로젝트의 Singapore Airlines 발권 API 심층 분석 문서입니다.