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 Method | GET |
| URL Pattern | /internals/flights/detail/{detailKey} |
| 메서드명 | getFlightDetailByPnr |
| 소스 위치 | FlightDetailInternalController.kt:13-32 |
2.1.1 Path Variable
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
detailKey | String | O | 항공편 상세 키 |
detailKey 형식: {listKey}::{supplierKey} (예: ef4706bc-cbaa-4f73-86e3-9d578ca6a01d::AMADEUS_04633252-60d4-43fb-a372-5adec32034cf)
소스 위치: StringUtils.kt:3-9
2.1.2 Query Parameters
| 파라미터 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
pnr | String | O | - | 예약 번호 (Passenger Name Record) |
adult | Int | O | - | 성인 승객 수 |
child | Int | X | 0 | 소아 승객 수 |
infant | Int | X | 0 | 유아 승객 수 |
소스 위치: FlightDetailInternalController.kt:16-19
3. Request/Response 구조
3.1 Request 예시
GET /internals/flights/detail/{detailKey}?pnr=ABC123&adult=2&child=1&infant=03.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 기능 비교표
| 항목 | FlightDetailController | FlightDetailInternalController |
|---|---|---|
| 경로 | /flights/detail | /internals/flights/detail |
| 대상 | 외부 클라이언트 (앱/웹) | 내부 서비스 |
| PNR 필수 | X | O |
| 프로모션 적용 | O | X |
| 가격 검증 | O (원가 대비 1000원 이상 차이 시 에러) | X |
| 예약 가능일 검증 | O | X |
| 좌석 수 검증 | O | X |
| 승객 수 검증 | O (checkSearchablePassengers) | X |
| 응답 타입 | FlightDetailView | InternalFlightDetailView |
| 카드 프로모션 | O | X |
| 태그 정보 | O | X (빈 리스트) |
5.2 엔드포인트 비교
FlightDetailController 엔드포인트들
소스 위치: FlightDetailController.kt:17-104
| Method | Path | 설명 |
|---|---|---|
| GET | /{detailKey} | detailKey로 상세 조회 |
| GET | / | listKey + id로 상세 조회 |
| GET | /{detailKey}/fare-rules | 운임 규정 조회 (Deprecated) |
| GET | /{detailKey}/benefit/free-installment | 무이자 혜택 조회 |
FlightDetailInternalController 엔드포인트
소스 위치: FlightDetailInternalController.kt:13-32
| Method | Path | 설명 |
|---|---|---|
| GET | /{detailKey} | PNR 기반 상세 조회 |
5.3 서비스 메서드 비교
| FlightDetailService 메서드 | 용도 | 호출 컨트롤러 |
|---|---|---|
getFlightDetail() | 일반 상세 조회 (프로모션/검증 포함) | FlightDetailController |
getFlightDetailByPnr() | PNR 기반 조회 (간소화) | FlightDetailInternalController |
getFlightDetails() | 다중 항공편 상세 조회 | 기타 |
6. PNR 기반 조회의 특수성
6.1 재발행(Reissue) 시나리오
PNR 기반 조회는 기존 예약의 일정 변경 시나리오를 지원합니다:
- 고객이 기존 예약(PNR)의 일정 변경을 요청
- 예약 서비스가 변경 가능한 항공편 목록을 검색
- 선택된 항공편의 상세 정보 조회 시 기존 PNR 정보가 필요
- 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:nulltags:emptyList()fares: 단일 요소 리스트 (프로모션 분기 없음)
7. 에러 처리 및 예외 상황
7.1 예외 유형
소스 위치: AdapterClient.kt:291-316
| 에러 코드 | 예외 클래스 | MessageKey | 설명 |
|---|---|---|---|
INVALID_CACHE_KEY | CacheKeyInvalidException | INVALID_CACHE_KEY | 캐시 키 만료/무효 |
NON_CHANGEABLE_SCHEDULES | InternationalSearchException | NON_CHANGEABLE_SCHEDULES | 변경 불가 일정 |
NON_CHANGEABLE_SCHEDULES_BY_ANCILLARY | InternationalSearchException | NON_CHANGEABLE_SCHEDULES_BY_ANCILLARY | 부가서비스로 인한 변경 불가 |
REISSUE_NON_CHANGEABLE_FARE_SCHEDULE | InternationalSearchException | REISSUE_NON_CHANGEABLE_FARE_SCHEDULE | 운임 조건상 재발행 불가 |
| 기타 | InternationalSearchException | EXCEPTION | 일반 예외 |
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 | 값 | 소스 |
|---|---|---|
PassengerType | ADULT, CHILD, INFANT | [확인 필요: enum 파일 위치] |
CabinType | ECONOMY, BUSINESS, FIRST 등 | [확인 필요: enum 파일 위치] |
TripType | ONE_WAY, ROUND_TRIP, MULTI_CITY | [확인 필요: enum 파일 위치] |
9. 관련 문서
- FlightDetailController - 외부 클라이언트용 상세 조회 API
- FlightDetailService - 항공편 상세 조회 서비스
- AdapterClient - GDS 어댑터 클라이언트
- InternalFlightDetailView - 내부 API 응답 DTO
10. 변경 이력
| 날짜 | 작성자 | 내용 |
|---|---|---|
| 2025-12-15 | Claude | 최초 작성 |
부록: 소스 파일 참조
| 파일 | 경로 |
|---|---|
| FlightDetailInternalController | air-intl-search/src/main/kotlin/com/triple/air/intl/search/interfaces/controller/internal/FlightDetailInternalController.kt |
| FlightDetailController | air-intl-search/src/main/kotlin/com/triple/air/intl/search/interfaces/controller/FlightDetailController.kt |
| FlightDetailService | air-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 |
| FlightDetail | air-intl-search/src/main/kotlin/com/triple/air/intl/search/support/model/FlightDetail.kt |
| AdapterClient | air-intl-search/src/main/kotlin/com/triple/air/intl/search/infrastructure/adapter/AdapterClient.kt |
| FlightSearchResponse | air-intl-search/src/main/kotlin/com/triple/air/intl/search/infrastructure/adapter/FlightSearchResponse.kt |
| StringUtils | air-intl-search/src/main/kotlin/com/triple/air/intl/search/support/util/StringUtils.kt |
| MessageKey | air-intl-search/src/main/kotlin/com/triple/air/intl/search/support/MessageKey.kt |
| Exceptions | air-intl-search/src/main/kotlin/com/triple/air/intl/search/support/exception/Exceptions.kt |
| LocationView | air-intl-search/src/main/kotlin/com/triple/air/intl/search/interfaces/response/LocationView.kt |
| AirlineView | air-intl-search/src/main/kotlin/com/triple/air/intl/search/interfaces/response/AirlineView.kt |