FlightDetailInternalController DFS (기능명세서)

1. API 개요 및 목적

1.1 개요

항목
컨트롤러FlightDetailInternalController
기본 경로/internals/flights/detail
소스 위치air-intl-search/.../interfaces/controller/internal/FlightDetailInternalController.kt:8-33
용도내부 서비스 전용 API

1.2 목적

FlightDetailInternalController내부 서비스 간 통신을 위한 전용 컨트롤러로, PNR(Passenger Name Record) 기반의 항공편 상세 정보 조회 기능을 제공합니다.

주요 목적:

  • 재발행(Reissue) 시나리오 지원: 기존 예약(PNR)에 대한 항공편 상세 정보 조회
  • 내부 서비스 연동: 예약 서비스 등 내부 시스템에서 호출
  • 간소화된 응답: 프로모션, 가격 검증 등 복잡한 로직 없이 기본 항공편 정보만 제공

1.3 의존성

FlightDetailInternalController
    └── FlightDetailService
            ├── AdapterClient (외부 GDS 연동)
            ├── AirlineService (항공사 정보)
            └── AirportService (공항 정보)

소스 위치: FlightDetailInternalController.kt:10-12


2. 엔드포인트 상세 분석

2.1 PNR 기반 상세 조회

항목
HTTP MethodGET
URL Pattern/internals/flights/detail/{detailKey}
메서드명getFlightDetailByPnr
소스 위치FlightDetailInternalController.kt:13-32

2.1.1 Path Variable

파라미터타입필수설명
detailKeyStringO항공편 상세 키

detailKey 형식: {listKey}::{supplierKey} (예: ef4706bc-cbaa-4f73-86e3-9d578ca6a01d::AMADEUS_04633252-60d4-43fb-a372-5adec32034cf)

소스 위치: StringUtils.kt:3-9

2.1.2 Query Parameters

파라미터타입필수기본값설명
pnrStringO-예약 번호 (Passenger Name Record)
adultIntO-성인 승객 수
childIntX0소아 승객 수
infantIntX0유아 승객 수

소스 위치: FlightDetailInternalController.kt:16-19


3. Request/Response 구조

3.1 Request 예시

GET /internals/flights/detail/{detailKey}?pnr=ABC123&adult=2&child=1&infant=0

3.2 Response 구조: InternalFlightDetailView

소스 위치: FlightDetailView.kt:657-683

data class InternalFlightDetailView(
    val detailKey: String,              // 항공편 상세 키
    val passengerFares: List<InternalPassengerFareView>,  // 승객별 운임 정보
    val validatingCarrier: AirlineView, // 발권 항공사
    val totalPrice: Long,               // 총 가격
    val schedules: List<InternalScheduleView>,  // 여정 정보
    val avail: Int,                     // 가용 좌석 수
)

3.3 하위 구조

3.3.1 InternalPassengerFareView

소스 위치: FlightDetailView.kt:685-714

data class InternalPassengerFareView(
    val type: PassengerType,     // ADULT, CHILD, INFANT
    val count: Int,              // 승객 수
    val airPrice: Long,          // 항공 운임
    val tax: Long,               // 세금 총액
    val fuelCharge: Long,        // 유류할증료
    val otherTax: Long,          // 기타 세금
    val ticketingFee: Long,      // 발권 수수료
    val discount: Long,          // 할인액
    val carrierFee: Long,        // 항공사 수수료
) {
    val total: Long
        get() = airPrice + tax + ticketingFee - discount
}

3.3.2 InternalScheduleView

소스 위치: FlightDetailView.kt:716-756

data class InternalScheduleView(
    val title: String,                    // 여정 제목 (가는편/오는편)
    val totalFlightTime: String,          // 총 비행 시간
    val departure: LocationView,          // 출발지 정보
    val arrival: LocationView,            // 도착지 정보
    val segments: List<InternalSegmentView>,  // 구간 정보
    val stop: Int,                        // 경유 횟수
) {
    val mainCarrier: AirlineView          // 주 운항 항공사
    val carrierText: String               // 항공사 표시 텍스트 (공동운항 정보 포함)
}

3.3.3 InternalSegmentView

소스 위치: FlightDetailView.kt:758-791

data class InternalSegmentView(
    val departure: InternalLocationView,      // 출발지
    val arrival: InternalLocationView,        // 도착지
    val marketingCarrier: AirlineView,        // 마케팅 항공사
    val operatingCarrier: AirlineView?,       // 실제 운항 항공사 (공동운항 시)
    val flightNumber: String,                 // 편명 (3자리 패딩)
    val cabin: CabinType,                     // 좌석 등급
    val bookingClass: String,                 // 예약 클래스
    val freeBaggage: InternalFreeBaggageDetailView?,  // 무료 수하물
    val legs: List<InternalLegView>,          // 구간별 상세
)

3.3.4 AirlineView

소스 위치: AirlineView.kt:6-19

data class AirlineView(
    val code: String,    // IATA 코드
    val name: String?,   // 항공사명
    val logoUrl: String? // 로고 URL
)

3.3.5 LocationView

소스 위치: LocationView.kt:6-24

data class LocationView(
    val code: String,           // IATA 공항 코드
    val name: String?,          // 공항명 (한글)
    val dateTime: LocalDateTime, // 일시
    val cityName: String?,      // 도시명 (한글)
    val terminal: String?       // 터미널
)

4. 비즈니스 로직 흐름

4.1 처리 흐름도

sequenceDiagram
    participant Client as 내부 서비스
    participant Controller as FlightDetailInternalController
    participant Service as FlightDetailService
    participant Adapter as AdapterClient
    participant GDS as 외부 GDS
    participant AirlineService
    participant AirportService

    Client->>Controller: GET /internals/flights/detail/{detailKey}
    Controller->>Service: getFlightDetailByPnr(detailKey, pnr, adult, child, infant)

    Service->>Service: destructDetailKey(detailKey)
    Note right of Service: listKey, key, supplier 분리

    Service->>Adapter: getFareItineraryByPnr(key, supplier, pnr)
    Adapter->>GDS: GET /{supplier}/search/reissue
    GDS-->>Adapter: FareItineraryDetailResponse
    Adapter-->>Service: FareItineraryDetailResponse

    par 병렬 조회
        Service->>AirlineService: getAirlinesMap(airlineSet)
        Service->>AirportService: getAirportsMap(airportSet)
    end

    Service->>Service: FlightDetail.of(fareItinerary, airlineMap, airportMap, passengerCountMap)
    Service-->>Controller: FlightDetail

    Controller->>Controller: InternalFlightDetailView.of(flightDetail)
    Controller-->>Client: ResponseEntity<InternalFlightDetailView>

4.2 Service 로직 상세

소스 위치: FlightDetailService.kt:182-205

fun getFlightDetailByPnr(
    detailKey: String,
    pnr: String,
    adult: Int,
    child: Int,
    infant: Int,
): FlightDetail {
    // 1. detailKey 파싱
    val (_, key, supplier) = detailKey.destructDetailKey()
 
    // 2. PNR 기반 재발행 검색 API 호출
    val fareItinerary = adapterClient.getFareItineraryByPnr(
        key = key,
        supplier = supplier,
        pnr = pnr
    )
 
    // 3. 승객 수 맵 생성
    val passengerCountMap = mapOf(
        PassengerType.ADULT to adult,
        PassengerType.CHILD to child,
        PassengerType.INFANT to infant
    )
 
    // 4. FlightDetail 생성 (간소화된 버전)
    return FlightDetail.of(
        fareItinerary = fareItinerary,
        airlineMap = airlineService.getAirlinesMap(fareItinerary.airlineSet),
        airportMap = airportService.getAirportsMap(fareItinerary.airportSet),
        passengerCountMap = passengerCountMap
    )
}

4.3 FlightDetail 생성 (간소화 버전)

소스 위치: FlightDetail.kt:159-188

PNR 기반 조회에서는 다음 항목이 생략됩니다:

  • 할인 원칙 (discountPrinciple)
  • 프로모션 원칙 (promotionPrinciples)
  • TASF 원칙 (tasfPrinciples)
  • 운임 원칙 (farePrinciple)
  • 네이버 프로모션 (naverPromotion)

5. FlightDetailController와의 차이점 분석

5.1 기능 비교표

항목FlightDetailControllerFlightDetailInternalController
경로/flights/detail/internals/flights/detail
대상외부 클라이언트 (앱/웹)내부 서비스
PNR 필수XO
프로모션 적용OX
가격 검증O (원가 대비 1000원 이상 차이 시 에러)X
예약 가능일 검증OX
좌석 수 검증OX
승객 수 검증O (checkSearchablePassengers)X
응답 타입FlightDetailViewInternalFlightDetailView
카드 프로모션OX
태그 정보OX (빈 리스트)

5.2 엔드포인트 비교

FlightDetailController 엔드포인트들

소스 위치: FlightDetailController.kt:17-104

MethodPath설명
GET/{detailKey}detailKey로 상세 조회
GET/listKey + id로 상세 조회
GET/{detailKey}/fare-rules운임 규정 조회 (Deprecated)
GET/{detailKey}/benefit/free-installment무이자 혜택 조회

FlightDetailInternalController 엔드포인트

소스 위치: FlightDetailInternalController.kt:13-32

MethodPath설명
GET/{detailKey}PNR 기반 상세 조회

5.3 서비스 메서드 비교

FlightDetailService 메서드용도호출 컨트롤러
getFlightDetail()일반 상세 조회 (프로모션/검증 포함)FlightDetailController
getFlightDetailByPnr()PNR 기반 조회 (간소화)FlightDetailInternalController
getFlightDetails()다중 항공편 상세 조회기타

6. PNR 기반 조회의 특수성

6.1 재발행(Reissue) 시나리오

PNR 기반 조회는 기존 예약의 일정 변경 시나리오를 지원합니다:

  1. 고객이 기존 예약(PNR)의 일정 변경을 요청
  2. 예약 서비스가 변경 가능한 항공편 목록을 검색
  3. 선택된 항공편의 상세 정보 조회 시 기존 PNR 정보가 필요
  4. GDS에서 해당 PNR과 연계된 운임 정보 반환

6.2 AdapterClient 호출

소스 위치: AdapterClient.kt:277-318

fun getFareItineraryByPnr(
    key: String,
    pnr: String,
    supplier: String,
): FareItineraryDetailResponse {
    return "$endpoint/$supplier/search/reissue"
        .get(
            listOf(
                "key" to key,
                "pnr" to pnr
            )
        )
        // ...
}
  • API 경로: /{supplier}/search/reissue
  • 특징: 일반 조회와 달리 승객 수(adult/child/infant)를 전달하지 않음
  • PNR로 예약 정보 조회: GDS에서 PNR에 연결된 운임/여정 정보 반환

6.3 간소화된 FlightDetail 생성

소스 위치: FlightDetail.kt:159-188

PNR 기반 조회에서는 4개 파라미터만 사용하는 FlightDetail.of() 오버로드를 호출:

fun of(
    fareItinerary: FareItineraryDetailResponse,
    airlineMap: Map<String, Airline>,
    airportMap: Map<String, Airport>,
    passengerCountMap: Map<PassengerType, Int>,
): FlightDetail

결과:

  • cardPromotionName: null
  • tags: emptyList()
  • fares: 단일 요소 리스트 (프로모션 분기 없음)

7. 에러 처리 및 예외 상황

7.1 예외 유형

소스 위치: AdapterClient.kt:291-316

에러 코드예외 클래스MessageKey설명
INVALID_CACHE_KEYCacheKeyInvalidExceptionINVALID_CACHE_KEY캐시 키 만료/무효
NON_CHANGEABLE_SCHEDULESInternationalSearchExceptionNON_CHANGEABLE_SCHEDULES변경 불가 일정
NON_CHANGEABLE_SCHEDULES_BY_ANCILLARYInternationalSearchExceptionNON_CHANGEABLE_SCHEDULES_BY_ANCILLARY부가서비스로 인한 변경 불가
REISSUE_NON_CHANGEABLE_FARE_SCHEDULEInternationalSearchExceptionREISSUE_NON_CHANGEABLE_FARE_SCHEDULE운임 조건상 재발행 불가
기타InternationalSearchExceptionEXCEPTION일반 예외

7.2 예외 클래스 구조

소스 위치: Exceptions.kt:1-81

ApiException (abstract)
├── CacheKeyInvalidException    - 캐시 키 무효 (capturable: false)
├── InternationalSearchException - 일반 검색 예외
└── 기타 예외들

7.3 에러 응답 형식

{
    "code": "INVALID_CACHE_KEY",
    "message": "캐시 키가 만료되었습니다."
}

7.4 에러 시나리오

flowchart TD
    A[getFareItineraryByPnr 호출] --> B{응답 상태}
    B -->|성공| C[FareItineraryDetailResponse 반환]
    B -->|실패| D{에러 코드 확인}

    D -->|INVALID_CACHE_KEY| E[CacheKeyInvalidException]
    D -->|NON_CHANGEABLE_SCHEDULES| F[InternationalSearchException<br/>일정 변경 불가]
    D -->|NON_CHANGEABLE_SCHEDULES_BY_ANCILLARY| G[InternationalSearchException<br/>부가서비스로 인한 변경 불가]
    D -->|REISSUE_NON_CHANGEABLE_FARE_SCHEDULE| H[InternationalSearchException<br/>운임 조건상 재발행 불가]
    D -->|기타| I[InternationalSearchException<br/>일반 예외]

8. 관련 도메인 모델 분석

8.1 핵심 도메인 모델

FlightDetail

소스 위치: FlightDetail.kt:14-50

data class FlightDetail(
    val key: String,                          // 항공편 키
    val id: String,                           // 항공편 ID
    val validatingCarrier: Airline,           // 발권 항공사
    val schedules: List<ScheduleDetail>,      // 여정 목록
    val avail: Int,                           // 가용 좌석
    val fares: List<FlightFareDetail>,        // 운임 옵션 목록
    val cardPromotionName: String?,           // 카드 프로모션명
    val promotionPrincipleId: Long?,          // 프로모션 ID
    val tags: List<String>,                   // 태그 목록
    val tripType: TripType,                   // 여정 유형
) {
    val totalPrice: Long                      // 총 가격 (계산)
    val passengerFares: List<PassengerFareDetail>  // 승객별 운임
}

ScheduleDetail

소스 위치: FlightDetail.kt:192-229

data class ScheduleDetail(
    val flightTime: String,           // 총 비행시간
    val segments: List<SegmentDetail>, // 구간 목록
    val addDay: Int,                  // 추가 일수
    val stop: Int,                    // 경유 횟수
    val avail: Int,                   // 가용 좌석
) {
    val departure: Airport            // 출발 공항
    val departureAt: LocalDateTime    // 출발 시각
    val arrival: Airport              // 도착 공항
    val arrivalAt: LocalDateTime      // 도착 시각
}

SegmentDetail

소스 위치: FlightDetail.kt:231-298

data class SegmentDetail(
    val departureAt: LocalDateTime,
    val arrivalAt: LocalDateTime,
    val departure: Airport,
    val departureTerminal: String?,
    val arrival: Airport,
    val arrivalTerminal: String?,
    val marketingCarrier: Airline,
    val operatingCarrier: Airline?,
    val flightNumber: String,
    val equipmentType: String?,
    val cabin: CabinType,
    val bookingClass: String,
    val avail: Int,
    val flightTime: String,
    val connectingTime: String?,
    val overNightStay: Long,
    val addDay: Int,
    val legs: List<LegDetail>,
    val freeBaggage: FreeBaggageDetail?,
    val amenity: AmenityDetail?,
)

8.2 외부 응답 모델

FareItineraryDetailResponse

소스 위치: FlightSearchResponse.kt:119-182

data class FareItineraryDetailResponse(
    val itemKey: String,
    val key: String,
    val id: String,
    val supplier: String,
    val scheduleKey: String,
    val passengerFares: List<PassengerFareResponse>,
    val validatingCarrier: String,
    val schedules: List<ScheduleResponse>,
    val avail: Int,
    val tripDirectionType: TripDirectionType?,
) {
    val departureAt: LocalDateTime
    val departureDate: LocalDate
    val departure: String
    val arrival: String
    val adultFare: PassengerFareResponse
    val airlineSet: Set<String>
    val airportSet: Set<String>
    val stopPoints: Set<String>
 
    fun getTripType(airportMap: Map<String, Airport>): TripType
}

8.3 Enum 타입

Enum소스
PassengerTypeADULT, CHILD, INFANT[확인 필요: enum 파일 위치]
CabinTypeECONOMY, BUSINESS, FIRST 등[확인 필요: enum 파일 위치]
TripTypeONE_WAY, ROUND_TRIP, MULTI_CITY[확인 필요: enum 파일 위치]

9. 관련 문서


10. 변경 이력

날짜작성자내용
2025-12-15Claude최초 작성

부록: 소스 파일 참조

파일경로
FlightDetailInternalControllerair-intl-search/src/main/kotlin/com/triple/air/intl/search/interfaces/controller/internal/FlightDetailInternalController.kt
FlightDetailControllerair-intl-search/src/main/kotlin/com/triple/air/intl/search/interfaces/controller/FlightDetailController.kt
FlightDetailServiceair-intl-search/src/main/kotlin/com/triple/air/intl/search/application/FlightDetailService.kt
FlightDetailView (InternalFlightDetailView 포함)air-intl-search/src/main/kotlin/com/triple/air/intl/search/interfaces/response/FlightDetailView.kt
FlightDetailair-intl-search/src/main/kotlin/com/triple/air/intl/search/support/model/FlightDetail.kt
AdapterClientair-intl-search/src/main/kotlin/com/triple/air/intl/search/infrastructure/adapter/AdapterClient.kt
FlightSearchResponseair-intl-search/src/main/kotlin/com/triple/air/intl/search/infrastructure/adapter/FlightSearchResponse.kt
StringUtilsair-intl-search/src/main/kotlin/com/triple/air/intl/search/support/util/StringUtils.kt
MessageKeyair-intl-search/src/main/kotlin/com/triple/air/intl/search/support/MessageKey.kt
Exceptionsair-intl-search/src/main/kotlin/com/triple/air/intl/search/support/exception/Exceptions.kt
LocationViewair-intl-search/src/main/kotlin/com/triple/air/intl/search/interfaces/response/LocationView.kt
AirlineViewair-intl-search/src/main/kotlin/com/triple/air/intl/search/interfaces/response/AirlineView.kt

관련 문서