Group Air — 프로토콜·전문
module-groupair api-rest pattern-protocol config-endpoints
이 노트의 위치
Group Air 모듈이 외부 콘솔리데이터와 실제로 어떤 전선(wire)으로 무슨 메시지를 주고받는지를 다룹니다. 오퍼레이션 시퀀스는 groupair-operations, 함정은 groupair-pitfalls, 모듈 전체 그림은 groupair-overview를 참고하세요. 어댑터 내부 컨트롤러 DTO(우리 쪽 API 스펙)는 interfaces-dtos와 구분됩니다. 이 노트는 어댑터 ↔ 콘솔리데이터 구간만 봅니다.
0. 한 줄 요약
결론부터
Group Air는 순수 JSON REST입니다. SOAP envelope도, NDC XML도, XSD 스키마도 없습니다. 인증은 토큰/시그니처/세션이 아니라 쿼리 파라미터
agentId(대리점 ID) 하나뿐인 무상태(stateless) B2B 연동입니다. 모든 응답은GroupairResponse<T>{ code, data, message }제네릭 봉투로 감싸여 오고,code != "OK"면 에러입니다.분석 대상:
infrastructure/GroupairClient.kt,infrastructure/request/*,infrastructure/response/*, 전송 공통support/web/ClientSupport.kt. (src/test/schema/groupair,src/test/resources/mockData/groupair는 존재하지 않음 — 실측 전문 샘플 없음.)
1. 프로토콜과 전송 방식
1.1 무엇으로 전송하는가 — REST JSON (SOAP/XML/NDC 아님)
Group Air는 11개 공급사 중 유일하게 그룹/단체 운임 콘솔리데이터(중개사)의 사설 JSON REST API에 직결합니다. GDS(amadeus/sabre/galileo)의 SOAP envelope, NDC(koreanair/lufthansa/singaporeair)의 EDIST/IATA XML과 달리 전송 단위가 평범한 JSON입니다.
코드로 확인되는 근거:
| 근거 | 위치 | 의미 |
|---|---|---|
기본 헤더 Content-Type: application/json | support/web/ClientSupport.kt:31 | JSON 본문 전송 |
요청 본문 변환 = Jackson objectMapper.writeValueAsString | ClientSupport.kt:32 | DTO → JSON 직렬화 (SOAP 마샬링 아님) |
응답 역직렬화 = Jackson readValue(TypeReference<RES>) | ClientSupport.kt:176 | JSON → DTO |
경로가 /goods/search, /reservations, /fare-rules 같은 RESTful 리소스 | GroupairClient.kt:55,107,152 | 리소스 지향 URL |
| HTTP 동사 GET/POST/PUT/DELETE를 의미대로 사용 | ClientSupport.kt:50-60 | REST 시맨틱 |
SOAP 경로는 정의돼 있지만 groupair는 안 씀
공통
ClientSupport에는 SOAP fault를 파싱하는handleSoapFaultException(...)(ClientSupport.kt:62-74)이 있지만 이는 GDS 모듈용입니다.GroupairClient는 이 함수를 전혀 호출하지 않습니다. groupair의 에러는 SOAP fault가 아니라 JSON 봉투의code필드로 옵니다(아래 4절).
1.2 HTTP 동사 ↔ 동작 매핑
GroupairClient의 8개 호출이 사용하는 동사와 경로는 다음과 같습니다. base는 검색계(searchEndpoint)와 예약계(bookingEndpoint) 두 종류로 나뉩니다.
| # | 함수 | 동사 | 경로 (base 제외) | 본문 | base |
|---|---|---|---|---|---|
| 1 | search | GET | /goods/search/{출발타입:코드}-{도착타입:코드}/{출발일}/{귀국일} | 없음(쿼리) | search |
| 2 | findFareRules | GET | /fare-rules | 없음(쿼리) | search |
| 3 | createReservation | POST | /reservations | ReservationCreateRequest | booking |
| 4 | getReservation | GET | /reservations/{supplierIdentificationKey} | 없음(쿼리) | booking |
| 5 | updateReservation | PUT | /reservations/{supplierIdentificationKey} | ReservationUpdateRequest | booking |
| 6 | ticketing | PUT | /reservations/{supplierIdentificationKey}/channel-confirm | 없음 | booking |
| 7 | expectedCancel | GET | /reservations/{supplierIdentificationKey}/expected-cancel | 없음(쿼리) | booking |
| 8 | cancel | DELETE | /reservations/{supplierIdentificationKey} | 없음(쿼리 passengerSequences) | booking |
| 9 | changeApis | PUT | /passengers/apis | List<PassengerApisUpdateRequest> | booking |
PUT/POST의 빈 본문 처리에 주의
ClientSupport.buildRequest는 POST/PUT을 만들 때 일단"".toRequestBody()(빈 본문)로 빌드하고(ClientSupport.kt:82-83), 실제 본문은execute()단계에서requestBody를 JSON으로 직렬화해 교체합니다(ClientSupport.kt:149-160).ticketing처럼 본문이 없는 PUT은.put()(인자 없음)이라 빈 문자열이 전송됩니다. 즉 본문 없는 PUT도 빈 바디로 정상 전송됩니다.
1.3 경로 조립 디테일 — search 엔드포인트
검색 URL이 가장 특이합니다. RESTful path에 출발/도착 location과 날짜를 통째로 끼워 넣고, 나머지는 쿼리로 보냅니다.
// GroupairClient.kt:55
"${searchEndpoint}/goods/search/${dep.originType}:${dep.origin}-${arr.originType}:${arr.origin}/${dep.departureDate}/${arr.departureDate}"originType은AIRPORT/CITY같은 location 종류(상위support.model.OriginDestination).- 예:
.../goods/search/AIRPORT:ICN-AIRPORT:NRT/2026-06-10/2026-06-15 - 왕복 가정:
originDestinations[0]=출발편,originDestinations[1]=귀국편으로 고정 인덱스 접근(GroupairClient.kt:51-52). 편도/다구간 처리 함정은 groupair-pitfalls 참고.
검색 쿼리 파라미터(GroupairClient.kt:57-69):
| 파라미터 | 값 | 비고 |
|---|---|---|
agentId | groupairApiProperties.agencyId | 인증 (2절) |
cabins | CabinType 다중 | 좌석등급 반복 추가 |
airlines | 항공사 코드 다중 | 선택(nullable) |
adult / child / infant | 인원 수 | preference.passenger.*.count |
freeBaggageOnly | Boolean | 무료수하물 포함 상품만 |
2. 인증·세션
인증 =
agentId쿼리 파라미터 하나. 세션/토큰/시그니처 없음GDS의 stateful PNR 세션, NDC의 OAuth 토큰, tway의 SEED 암호화 같은 게 전혀 없습니다. 모든 요청에 대리점 식별자(
agencyId)를 쿼리 또는 본문 필드로 실어 보내는 것이 인증의 전부입니다. 따라서 groupair는 완전 무상태(stateless) 입니다.
agentId가 실리는 자리:
| 위치 | 형태 | 코드 |
|---|---|---|
| 검색 | 쿼리 agentId | GroupairClient.kt:58 |
| 운임규정 | 쿼리 agentId | GroupairClient.kt:110 |
| 예약 생성 | 본문 필드 agentId | ReservationCreateRequest.kt:16 ← GroupairClient.kt:149 |
값의 출처는 설정의 GroupairApiProperties.agencyId이며, 채널/퍼널별로 다른 대리점 ID·엔드포인트가 선택됩니다(3절).
Authorization헤더는 안 쓴다
OkHttpRequestBuilder에는authenticate(user, pwd)(Basic),bearer(token)헬퍼가 있지만(ClientSupport.kt:131-139),GroupairClient는 둘 다 호출하지 않습니다. 표준 HTTP 인증 헤더 없이agentId만으로 식별합니다. 운영 보안은 콘솔리데이터 쪽 IP allowlist/사설망에 의존한다고 보는 것이 안전합니다.
3. 엔드포인트 설정 — 채널/퍼널별 라우팅
엔드포인트는 코드 상수가 아니라 설정(GroupairProperties)에서 채널·퍼널별로 선택됩니다. 단일 콘솔리데이터가 아니라 판매 채널/퍼널에 따라 다른 백엔드/대리점이 붙을 수 있는 구조입니다.
// configuration/Properties.kt:470-505 (요약)
data class GroupairApiProperties(
override val funnel: String,
val searchEndpoint: String, // 검색계 base
val bookingEndpoint: String, // 예약계 base
val agencyId: String, // 인증용 대리점 ID
) : FunnelProperties
@ConfigurationProperties(prefix = "supplier.groupair")
data class GroupairProperties(val channels: List<GroupairChannelProperties>) {
fun getApiProperties(
salesChannel: String = MDCHolder.SalesChannel.get(),
salesFunnel: String = MDCHolder.SalesFunnel.get(),
): GroupairApiProperties { ... } // 채널→퍼널 2단 조회
}흐름:
flowchart TD H["요청 헤더"] --> M["MDC<br/>SalesChannel, SalesFunnel"] M --> P["GroupairClient.groupairApiProperties<br/>get 프로퍼티, 매 호출 평가"] P -->|"groupairProperties.getApiProperties()"| FC["channels.find 채널 == salesChannel"] FC -->|"실패"| EC["NOT_SUPPORTED_SALES_CHANNEL"] FC -->|"성공"| FF["get funnel == salesFunnel"] FF -->|"실패"| EF["NOT_SUPPORTED_SALES_FUNNEL"] FF -->|"성공"| R["GroupairApiProperties<br/>searchEndpoint, bookingEndpoint, agencyId"]
GroupairClient.groupairApiProperties는get()프로퍼티라 매 호출 시점의 MDC 값으로 재계산됨.- 채널 조회 실패 시
NOT_SUPPORTED_SALES_CHANNEL, 퍼널 조회 실패 시NOT_SUPPORTED_SALES_FUNNEL예외. - 최종 결과
GroupairApiProperties는searchEndpoint,bookingEndpoint,agencyId를 담음.
엔드포인트는 매 호출 MDC에서 동적 결정
GroupairClient.groupairApiProperties는val이 아니라get()프로퍼티라 호출 시점의 MDC 값으로 매번 다시 계산됩니다(GroupairClient.kt:37-38). MDC에SalesChannel/SalesFunnel이 안 채워져 있으면 위 두 예외(NOT_SUPPORTED_SALES_*)가 터집니다. MDC 전파는 configuration-and-infra 참고.
설정 import는 supplier/groupair.yml에서 환경별로 AWS Secrets Manager를 optional import 합니다(dev|qa|staging|prod/air-intl-adapter/groupair). 즉 실제 엔드포인트/대리점 ID는 시크릿에 들어 있습니다(resources/supplier/groupair.yml:7,15,23,31). 빌드/배포 설정 전반은 build-deploy-config 참고.
4. 응답 봉투(envelope)와 에러 모델
모든 응답은 동일한 제네릭 봉투로 감싸여 옵니다.
// infrastructure/response/GroupairResponse.kt
data class GroupairResponse<T>(
val code: String, // "OK"면 성공, 그 외는 에러코드
val data: T?, // 페이로드 (제네릭)
val message: String?,// 에러 메시지
) {
fun checkError(callback: ((code: String, message: String) -> Unit)) {
if (code != HttpStatus.OK.name) { // "OK" 문자열 비교
callback(code, message ?: "Unknown Error")
}
}
}2겹 에러 판정 — HTTP 상태 + 봉투 code
에러는 두 단계로 걸러집니다.
- HTTP 레벨:
response.isSuccessful(2xx)이 아니면ClientSupport가Result.failure(OkHttpError(...))로 만듭니다(ClientSupport.kt:180-187). 각 함수의failure = { ... }분기로 빠짐.- 봉투 레벨: HTTP 2xx여도 봉투
code != "OK"면checkError가 콜백을 호출해InternationalAdapterException을 던집니다.즉 HTTP 200 +
code:"BOOKING_FAILED"같은 “성공 상태 코드인데 실패한 응답”을 반드시 봉투로 다시 판정합니다. 이걸 놓치면 실패를 성공으로 오인합니다.
봉투 code에 따른 분기는 거의 일괄 변환이지만, 취소(cancel)만 특정 코드를 의미있게 분기합니다:
// GroupairClient.kt:288-296 (cancel)
when (code) {
"NON_CANCELABLE_RESERVATION" -> throw InternationalAdapterException(ErrorMessage.CANCEL_UNABLE)
"ALREADY_CANCELED_RESERVATION" -> throw InternationalAdapterException(ErrorMessage.ALREADY_CANCELED_PNR)
else -> throw InternationalAdapterException(ErrorMessage.CANCEL_FAILED, code, message).capture()
}| 오퍼레이션 | 매핑 에러(ErrorMessage) | 특이코드 분기 |
|---|---|---|
| search | SEARCH_FAILED | — |
| findFareRules | FETCH_FARE_RULES_FAILED | — |
| createReservation | BOOKING_FAILED | — |
| getReservation | RETRIEVE_FAILED | //todo already canceled 주석만(미구현) |
| updateReservation | SAVE_FAILED | — |
| ticketing | TICKETING_FAILED | — |
| expectedCancel | CALCULATE_CANCEL_FEE_FAILED | — |
| cancel | CANCEL_FAILED | NON_CANCELABLE_RESERVATION→CANCEL_UNABLE, ALREADY_CANCELED_RESERVATION→ALREADY_CANCELED_PNR |
| changeApis | PASSENGER_CHANGE_FAILED | — |
.capture()의 의미에러 생성 시 끝에 붙는
.capture()(예:GroupairClient.kt:81)는 Slack 경보·관측(observability)으로 흘려보내는 캡처입니다. 단,cancel의CANCEL_UNABLE/ALREADY_CANCELED_PNR은.capture()가 없습니다 — “정상 범주의 비즈니스 거절”은 경보를 울리지 않습니다. 에러 전파/경보 큰 그림은 error-handling, resilience-and-events 참고.
response.data!!강제 언랩
checkError를 통과(=code OK)한 뒤 곧바로response.data!!(non-null 단언)를 씁니다(GroupairClient.kt:84,127,160,183,263,298,327). 콘솔리데이터가code:"OK"인데data:null을 주면NullPointerException이 터지고failure분기가 아니라 일반 예외로 빠집니다. 이는 groupair-pitfalls에서 자세히 다룹니다.
5. 전송 계층 부가 사양 (공통 ClientSupport)
groupair만의 특수 전송은 없고, 공통 OkHttp 래퍼를 그대로 씁니다. 그래도 protocol로서 알아야 할 점:
| 항목 | 값/동작 | 위치 |
|---|---|---|
| 타임아웃(검색) | connect/read/write/call 각 30,000ms | GroupairClient.kt:34(searchTimeout=30000) |
| 타임아웃(그 외) | 각 60,000ms | GroupairClient.kt:35(defaultTimeout=60000) |
검색만 searchClient 사용 | .client(searchClient) 명시 | GroupairClient.kt:71 (나머지는 defaultClient) |
| 응답 압축 해제 | Content-Encoding별 gzip/deflate/br 자동 해제 | OkhttpClientConfiguration.kt:18-26 |
| 요청/응답 로깅 | LoggingAndCompressionInterceptor가 본문을 구조화 로그로 | OkhttpClientConfiguration.kt:28-69 |
| Unit/String 응답 | RES==Unit 또는 String이면 역직렬화 건너뜀 | ClientSupport.kt:173-174 |
GroupairResponse<Unit>의 함정성 동작
updateReservation/ticketing은execute<GroupairResponse<Unit>>()을 씁니다(GroupairClient.kt:200,223).RES는GroupairResponse<Unit>(Unit이 아님)이라 봉투 자체는 정상적으로 JSON 파싱되고, 내부data:Unit?만 비게 됩니다. 즉 봉투의code검사는 그대로 동작합니다. (위 표의 “Unit이면 역직렬화 스킵”은RES==Unit::class일 때만 — 여기 해당 없음.)
6. 요청 전문 모델 — 핵심 필드 매핑
6.1 ReservationCreateRequest (예약 생성 본문)
// infrastructure/request/ReservationCreateRequest.kt
data class ReservationCreateRequest(
val departureGoodSequence: Long, // 어떤 "상품(good)"을 잡을지
val goodCategory: GoodCategory = GoodCategory.REAL_TIME,
val agentId: String, // 인증 = 대리점 ID
val agentReservationNumber: String? = null,
val passengers: List<PassengerCreateRequest>,
)| 필드 | 출처(도메인) | 의미 |
|---|---|---|
departureGoodSequence | FareItinerary.departureGoodSequence | 상품 식별자 — 검색 결과에서 고른 그룹운임 상품 |
goodCategory | 기본 REAL_TIME | 상품종류(REAL_TIME/PACKAGE/FREE, support/enums/GoodCategory.kt) |
agentId | groupairApiProperties.agencyId | 채널/퍼널별 대리점 ID |
passengers[] | PassengerCreateModel | 아래 6.2 |
PassengerCreateRequest (승객 1명):
| 필드 | 비고 |
|---|---|
type | PassengerType(ADULT/CHILD/INFANT) |
lastName / firstName | 영문 성/이름 |
birthDate | LocalDate |
title | Title(MR/MS/MSTR/MISS) — 성별 추론에도 쓰임 |
passportNumber/ExpireDate/IssueNation/Nation | 여권(APIS), nullable |
stayInfo | 체류정보(목적지 입국용), nullable |
remark | 특이사항 |
예약의 핵심은 단 하나,
departureGoodSequenceGDS/NDC가 운임을 다시 가격책정(repricing)하는 것과 달리, groupair 예약은 검색 때 받은 상품 일련번호 하나로 “그 상품을 잡아라” 라고 지시할 뿐입니다. 운임/스케줄을 본문에 다시 보내지 않습니다. 단체 운임이 미리 묶여 있기 때문입니다(groupair-overview 1.1).
6.2 그 외 요청 전문
| DTO | 쓰임 | 핵심 필드 |
|---|---|---|
ReservationUpdateRequest | updateReservation(발권 1단계) | agentReservationNumber, eventId — 둘 다 MDC 주문번호로 채움(GroupairClient.kt:192-196) |
PassengerApisUpdateRequest | changeApis(여권/체류정보 변경) | sequence(= 승객 identificationKey), passport, stayInfo |
PassengerApisUpdateRequest.of의!!
sequence = passenger.identificationKey!!.toLong()(ReservationCreateRequest.kt:85) — APIS 변경 대상 승객에identificationKey가 없으면 NPE. 이 키는 예약 조회 응답의PassengerResponse.sequence에서 채워지므로, 예약을 먼저 조회한 승객 객체여야 안전합니다.
7. 응답 전문 모델 — 핵심 필드 매핑
응답 DTO는 7개 파일에 흩어져 있습니다. 봉투(GroupairResponse<T>)의 T 자리에 들어가는 페이로드 타입을 정리합니다.
| 오퍼레이션 | 봉투 T 타입 | → 변환되는 도메인 |
|---|---|---|
| search | List<GoodSearchItemResponse> | List<FareItinerary> |
| findFareRules | List<FareRuleResponse> | List<FareRule> |
| createReservation / getReservation | ReservationDetailResponse | Booking |
| expectedCancel | List<PassengerCancelResponse> | List<Refund> |
| cancel | CancelResponse | List<Refund> |
| changeApis | List<PassengerResponse> | List<Passenger> |
| update / ticketing | Unit | (없음) |
7.1 검색 응답 — GoodSearchItemResponse
GoodSearchItemResponse (상품 1건)
├── goodSequence / departureGoodSequence / salesMarkupSequence
├── validatingCarrier ← 발권 항공사 (상품마다 다름!)
├── schedules : List<GoodScheduleResponse>
│ ├── departure/arrival, departureAt/arrivalAt, addDay, stop, avail
│ ├── freeBaggage : FreeBaggageResponse?
│ └── segments : List<GoodSegmentResponse>
│ └── marketing/operatingCarrier, bookingClass, flightNumber, cabin, flightTime(ISO Duration)
└── passengerFares : List<GoodPassengerFareResponse>
└── type, airPrice, tax, fuelCharge, total핵심 매핑 디테일(response/GoodSearchItemResponse.kt):
| 동작 | 위치 | 주의점 |
|---|---|---|
flightTime이 ISO-8601 Duration 문자열 | :58-60 | Duration.parse(it.flightTime) — "PT2H30M" 형식. 숫자/분 아님 |
| 세그먼트 합산이 schedule 총 비행시간 | :58-60 | 멀티세그먼트면 각 flightTime을 ms로 합산 |
tax = tax + fuelCharge로 합산 저장 | :144 | 유류할증료를 tax에 합쳐 보관 (유의: 이중집계 X, 도메인 규약) |
| 무료수하물 단위 환산 | :111 | BaggageUnit.getBaggageUnitBySupplier(unit, Supplier.GROUPAIR) — 공급사별 단위 매핑 |
7.2 예약상세 응답 — ReservationDetailResponse → Booking
가장 큰 응답 DTO이며 조회/예약 양쪽이 공유합니다. 변환 시 굳은 규칙들:
| 도메인 필드 | 소스 | 규칙 (ReservationDetailResponse.kt) |
|---|---|---|
Booking.supplierIdentificationKey | sequence.toString() | 후속 모든 호출의 키 (PNR 아님!) :55 |
Booking.pnr | pnr?.pnrNumber | 없으면 UNDEFINED_PNR("UNKNOWN") :49 |
carrierPnr | pnr?.carrierPnrNumber ?: pnrNumber | 없으면 pnr로 폴백 :50 |
carrierTimeLimit | now().plusHours(1) | 콘솔리데이터가 안 줘서 임의 +1h 하드코딩 :59 |
paymentTimeLimit | null | 항상 null :60 |
voidable | true | 항상 void 가능 고정 :62 |
| segment status | "HK" | 모든 세그먼트 상태 하드코딩 :120 |
bookingClass | bookingClass ?: "Y" | 없으면 Y로 폴백 :118 |
PassengerResponse → Passenger 매핑 디테일:
| 도메인 | 소스/규칙 |
|---|---|
identificationKey | sequence.toString() — APIS 변경/취소 시 승객 키 |
gender | title로 추론: MR/MSTR→MALE, MS/MISS→FEMALE (:168-171) |
commission | fares.first().commissionAmount, 타입 NET (:183-187) |
tickets | fares.mapNotNull { it.ticket?.toTicket() } (발권 후에만 채워짐) |
fares.first()빈 리스트 위험
PassengerResponse.toPassenger()는fares.first().commissionAmount를 호출합니다(:183).fares가 빈 리스트면NoSuchElementException. 발권 전/특수 상태 응답에서 위험. groupair-pitfalls 참고.
TicketResponse.toTicket()의 타입 분기: type == "TICKET" → TicketType.TICKET, 그 외 전부 → TicketType.EMD(:243).
7.3 취소/환불 응답
// CancelResponse.kt
data class CancelResponse(val sequence: Long, val passengers: List<PassengerCancelResponse>)
data class PassengerCancelResponse(
val sequence: Long, val cancelStatus: String, val type: String, val penalty: Long,
) { fun toRefund() = Refund(sequence.toString(), PassengerType.getPassengerTypeCodeBySupplier(type, Supplier.GROUPAIR), penalty) }expectedCancel은 봉투T가List<PassengerCancelResponse>(승객별 예상 환불수수료)이고,cancel은CancelResponse(래퍼) — 같은PassengerCancelResponse를 다른 봉투 형태로 받습니다(GroupairClient.kt:253,284,298).penalty(Long)가Refund.refundFee로 그대로 들어갑니다. 승객type문자열은 공급사별 매핑 함수로 표준PassengerType으로 환산.
7.4 운임규정 응답 — FareRuleResponse
// FareRuleResponse.kt
data class FareRuleResponse(val name: String, val content: String, val order: Int)카테고리를 "이름 문자열 한글 키워드"로 추론
toFareRule()은name에"취소"/"환불"/"변경"이 있으면REFUND_AND_CHANGE,"수하물"/"수화물"이면BAGGAGE,"마일리지"면MILEAGE, 아니면COMMON으로 분류합니다(FareRuleResponse.kt:14-21). 즉 운임규정 분류가 한국어 키워드 매칭에 의존합니다 — 콘솔리데이터가 규정 이름 표기를 바꾸면 분류가 깨질 수 있습니다(groupair-pitfalls).
8. XSD 스키마 / 실측 전문 샘플
groupair에는 XSD도, mockData 샘플도 없음
src/test/schema/groupair: 존재하지 않음. XSD 스키마 자체가 없습니다. JSON REST라 스키마는 Kotlin DTO 클래스가 사실상의 “스키마”입니다. (SOAP/NDC 모듈인 amadeus/galileo/singaporeair는 XSD가 있지만 groupair는 무관.)src/test/resources/mockData/groupair: 존재하지 않음. 캡처된 실전문 샘플이 없어, 본 노트의 전문 구조는 전부 DTO 정의에서 역설계한 것입니다.따라서 신입은 “전송 포맷을 알고 싶다 = 위 DTO 클래스를 읽어라”가 정답입니다. SOAP envelope처럼 외부 스키마 파일을 뒤질 필요가 없습니다.
전문 구조를 빠르게 알고 싶으면 보는 파일:
| 알고 싶은 것 | 보는 파일 |
|---|---|
| 검색 요청 쿼리 | GroupairClient.kt:40-95 |
| 검색 응답 JSON 구조 | infrastructure/response/GoodSearchItemResponse.kt |
| 예약 요청 JSON 구조 | infrastructure/request/ReservationCreateRequest.kt |
| 예약 응답 JSON 구조 | infrastructure/response/ReservationDetailResponse.kt |
| 공통 봉투/에러 | infrastructure/response/GroupairResponse.kt |
9. 기존 분석문서 교차참조
groupair 전용 supplier-api-analysis 문서는 없음
content/nol/supplier-api-analysis/아래에는 GDS/대형 NDC 위주로 amadeus-gds, sabre-gds, galileo-gds, singaporeair-ndc만 존재하고 groupair 분석문서는 없습니다. groupair의 1차 자료는 이 노트 시리즈(groupair-overview/groupair-operations/groupair-protocol/groupair-pitfalls)가 사실상 유일합니다.
다음으로 읽을 노트
- Group Air — 오퍼레이션 : 이 전문들이 검색→예약→발권(2-스텝)→취소 시퀀스에서 어떻게 엮이는지
- Group Air — 함정 :
data!!NPE,fares.first(), 키워드 기반 FareRule 분류,supplierIdentificationKeyvs PNR - 인터페이스·DTO : 어댑터 내부(컨트롤러) DTO — 콘솔리데이터 전문과의 경계 구분
- 에러 처리 · 복원성·이벤트 :
checkError/.capture()/서킷브레이커가 전체에서 동작하는 방식