FlightDetailController DFS (기능 명세서)

1. API 개요 및 목적

1.1 개요

FlightDetailController는 국제선 항공편 상세 정보를 제공하는 API 컨트롤러입니다. 항공편 검색 결과에서 선택한 특정 항공편의 상세 운임, 스케줄, 좌석 가용성, 운임 규정 및 카드 혜택 정보를 조회할 수 있습니다.

1.2 소스 위치

항목위치
Controllerair-intl-search/.../interfaces/controller/FlightDetailController.kt:19-104
Serviceair-intl-search/.../application/FlightDetailService.kt:32-353
Response Viewair-intl-search/.../interfaces/response/FlightDetailView.kt:8-59
Domain Modelair-intl-search/.../support/model/FlightDetail.kt:14-190

1.3 기본 경로

/flights/detail

1.4 의존성

// FlightDetailController.kt:19-23
class FlightDetailController(
    private val flightSearchService: FlightSearchService,
    private val flightDetailService: FlightDetailService,
    private val fareRuleService: FareRuleService,
)

2. 엔드포인트별 상세 분석

2.1 항공편 상세 조회 (by detailKey)

기본 정보

항목
HTTP MethodGET
URL/flights/detail/{detailKey}
메서드명getFlightDetail
소스 위치FlightDetailController.kt:24-44

Request 파라미터

파라미터타입필수기본값설명
detailKeyStringO-Path Variable. 항공편 상세 조회 키
adultIntO-성인 승객 수
childIntX0소아 승객 수
infantIntX0유아 승객 수
promotionPrincipleIdLong?Xnull프로모션 정책 ID

Response

// FlightDetailView.kt:8-59
data class FlightDetailView(
    val detailKey: String,           // 상세 조회 키
    val id: String,                  // 항공편 ID
    val passengerFares: List<PassengerFareView>,  // 승객별 운임
    val validatingCarrier: AirlineView,           // 발권 항공사
    val fares: List<FlightFareDetailView>,        // 운임 상세 목록
    val schedules: List<ScheduleView>,            // 스케줄 목록
    val avail: Int,                  // 잔여 좌석
    val passengerChangeable: Boolean,// 승객 변경 가능 여부
    val cardPromotionName: String?,  // 카드 프로모션명
    val promotionPrincipleId: Long?, // 프로모션 정책 ID
    val tags: List<String>,          // 태그 목록
    val tripType: TripType,          // 여정 타입 (ONE_WAY, ROUND_TRIP, MULTI_CITY)
)

파생 속성 (계산 필드)

// FlightDetailView.kt:22-32
val totalPrice: Long              // 총 운임 = passengerFares.sumOf { it.total * it.count }
val totalDiscount: Long?          // 총 할인액 (0보다 큰 경우만)
val adultPrice: Long              // 성인 1인 운임
val originalAdultPrice: Long      // 성인 1인 할인 전 운임

2.2 항공편 상세 조회 (by listKey + id)

기본 정보

항목
HTTP MethodGET
URL/flights/detail
메서드명getFlightDetailById
소스 위치FlightDetailController.kt:46-72

Request 파라미터

파라미터타입필수기본값설명
listKeyUUIDO-검색 결과 목록 키
idStringO-항공편 메타 ID
adultIntO-성인 승객 수
childIntX0소아 승객 수
infantIntX0유아 승객 수

처리 로직

// FlightDetailController.kt:55-67
// 1. MetaFareStrategy.LOWEST_REPRESENTATIVE_PROMOTION_FARE 전략으로 detailKey 조회
val flightInfo = flightDetailService.getDetailKeys(
    metaId = id,
    listKey = listKey,
    fareStrategy = MetaFareStrategy.LOWEST_REPRESENTATIVE_PROMOTION_FARE
)[0]
 
// 2. 조회된 detailKey로 상세 정보 조회
val flightDetail = flightDetailService.getFlightDetail(
    detailKey = flightInfo.key,
    adult = adult,
    child = child,
    infant = infant,
    promotionPrincipleId = flightInfo.promotionPrincipleId
)

Response

2.1과 동일한 FlightDetailView


2.3 운임 규정 조회 (Deprecated)

기본 정보

항목
HTTP MethodGET
URL/flights/detail/{detailKey}/fare-rules
메서드명getFareRules
소스 위치FlightDetailController.kt:74-92
상태Deprecated - 프론트 폴링 적용 후 삭제 예정

Request 파라미터

파라미터타입필수기본값설명
detailKeyStringO-Path Variable
adultIntO-성인 승객 수
childIntX0소아 승객 수
infantIntX0유아 승객 수

Response

// FareRulesView.kt:14-25
data class FareRuleView(
    val groupTitle: String,              // 그룹 제목 (예: "규정 1")
    val rules: List<FareRuleItemView>    // 규정 항목 목록
)
 
data class FareRuleItemView(
    val type: FareRuleType?,    // 규정 타입
    val title: String,          // 규정 제목
    val content: String?,       // 규정 내용
    val order: Int,             // 정렬 순서
)

2.4 무이자 할부 카드 혜택 조회

기본 정보

항목
HTTP MethodGET
URL/flights/detail/{detailKey}/benefit/free-installment
메서드명findFreeInstallment
소스 위치FlightDetailController.kt:94-103

Request 파라미터

파라미터타입필수기본값설명
detailKeyStringO-Path Variable

Response

// CardBenefitView.kt:6-27
data class CardBenefitView(
    val title: String,                           // 혜택 제목
    val period: String,                          // 혜택 기간
    val interestFreeCards: List<InterestFreeView>,  // 무이자 카드 목록
    val cautions: List<String>,                  // 주의사항
)
 
data class InterestFreeView(
    val cardCompanies: List<String>,  // 카드사 목록
    val installments: String,         // 할부 개월 (예: "2-6" 또는 "3")
)

3. Request/Response 구조 상세

3.1 PassengerFareView (승객별 운임)

// FlightDetailView.kt:80-105
data class PassengerFareView(
    val type: PassengerType,     // ADULT, CHILD, INFANT
    val count: Int,              // 승객 수
    val airPrice: Long,          // 항공료 (카드/판매사 프로모션 할인 차감)
    val fuelCharge: Long,        // 유류할증료
    val otherTax: Long,          // 기타 세금
    val ticketingFee: Long,      // 발권 수수료
    val discount: Long,          // 판매사 할인
) {
    val total: Long              // 총액 = airPrice + fuelCharge + otherTax + ticketingFee - discount
}

3.2 ScheduleView (스케줄)

// FlightDetailView.kt:107-145
data class ScheduleView(
    val title: String,               // "가는편", "오는편", "여정 N"
    val totalFlightTime: String,     // 총 비행 시간
    val departure: LocationView,     // 출발지
    val arrival: LocationView,       // 도착지
    val segments: List<SegmentView>, // 구간 목록
    val stop: Int,                   // 경유 횟수
) {
    val mainCarrier: AirlineView     // 주 운항 항공사
    val carrierText: String          // 항공사 텍스트 (공동운항 표시 포함)
}

3.3 SegmentView (구간)

// FlightDetailView.kt:147-183
data class SegmentView(
    val departure: LocationView,          // 출발지
    val arrival: LocationView,            // 도착지
    val marketingCarrier: AirlineView,    // 마케팅 항공사
    val operatingCarrier: AirlineView?,   // 운항 항공사 (공동운항시)
    val flightNumber: String,             // 편명 (3자리 패딩)
    val cabin: CabinType,                 // 좌석 등급
    val bookingClass: String,             // 예약 클래스
    val freeBaggage: FreeBaggageView?,    // 무료 수하물 (volume > 0인 경우만)
    val legs: List<LegView>,              // 레그 목록
    val amenity: AmenityView?,            // 기내 편의시설
)

3.4 LocationView (위치)

// LocationView.kt:6-24
data class LocationView(
    val code: String,           // 공항 IATA 코드
    val name: String?,          // 공항명 (한글)
    val dateTime: LocalDateTime,// 날짜/시간
    val cityName: String?,      // 도시명 (한글)
    val terminal: String?,      // 터미널
)

3.5 AirlineView (항공사)

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

4. 비즈니스 로직 흐름

4.1 getFlightDetail 처리 흐름

sequenceDiagram
    participant C as Controller
    participant V as Validator
    participant S as FlightDetailService
    participant A as AdapterClient
    participant P as PricingClient
    participant Air as AirlineService
    participant Apt as AirportService
    participant B as BookableDateService
    participant FS as FlightSearchService

    C->>V: checkSearchablePassengers(adult, child, infant)
    V-->>C: 유효성 통과

    C->>S: getFlightDetail(detailKey, adult, child, infant, promotionPrincipleId)

    S->>S: destructDetailKey() -> (listKey, key, supplier)
    S->>A: getFareItinerary(key, supplier, adult, child, infant)
    A-->>S: FareItineraryDetailResponse

    S->>B: getBookableDateRange().validateBookable(departureDate)

    Note over S: 가용 좌석 검증 (avail < adult + child)

    S->>Air: getAirlinesMap(airlineSet)
    S->>Apt: getAirportsMap(airportSet)

    S->>P: getMatchedFarePrinciple(fareItinerary, airportMap)
    Note over S: farePrinciple.display == false 검증

    S->>P: getMatchedTasfPrinciplesByPassengerType(...)
    S->>P: getMatchedSellerDiscountsAndPromotions(...)

    S->>S: FlightDetail.of(...) 생성

    S->>FS: getFlightItem(detailKey)
    Note over S: 가격 변동 검증 (|originTotalPrice - totalPrice| > 1000)

    S-->>C: FlightDetail
    C->>C: FlightDetailView.of(flightDetail, detailKey)
    C-->>Client: ResponseEntity<FlightDetailView>

4.2 주요 처리 단계 (FlightDetailService.getFlightDetail)

단계설명소스 위치
1detailKey 분해FlightDetailService.kt:52
2Adapter를 통한 운임 일정 조회FlightDetailService.kt:54-60
3예약 가능 일자 검증FlightDetailService.kt:61
4가용 좌석 검증FlightDetailService.kt:64-68
5항공사/공항 정보 조회FlightDetailService.kt:73-74
6Fare Principle 조회 및 노출 검증FlightDetailService.kt:80-87
7TASF(발권 수수료) Principle 조회FlightDetailService.kt:89-98
8할인/프로모션 Principle 조회FlightDetailService.kt:100-107
9FlightDetail 객체 생성FlightDetailService.kt:109-120
10가격 변동 검증FlightDetailService.kt:121-136

5. detailKey 구조 분석

5.1 detailKey 형식

{listKey}::{supplierKey}

예시:

ef4706bc-cbaa-4f73-86e3-9d578ca6a01d::AMADEUS_04633252-60d4-43fb-a372-5adec32034cf::1_1_1

5.2 destructDetailKey 함수

// StringUtils.kt:4-9
fun String.destructDetailKey(): Triple<String, String, String> {
    val listKey = this.substringBefore("::")   // ef4706bc-cbaa-4f73-86e3-9d578ca6a01d
    val key = this.substringAfter("::")        // AMADEUS_04633252-60d4-43fb-a372-5adec32034cf::1_1_1
    val supplier = key.substringBefore("_").uppercase()  // AMADEUS
    return Triple(listKey, key, supplier)
}

5.3 분해 결과

구성요소설명예시 값
listKey검색 결과 목록 키 (UUID)ef4706bc-cbaa-4f73-86e3-9d578ca6a01d
key공급자별 항공편 키AMADEUS_04633252-60d4-43fb-a372-5adec32034cf::1_1_1
supplier공급자 코드 (대문자)AMADEUS

5.4 지원 Supplier 목록

// Supplier.kt:3-15
enum class Supplier(val passengerChangeable: Boolean) {
    TWAY(passengerChangeable = true),
    JINAIR(passengerChangeable = true),
    SINGAPOREAIR(passengerChangeable = false),
    LUFTHANSA(passengerChangeable = false),
    SABRE(passengerChangeable = true),
    AMADEUS(passengerChangeable = true),
    AMADEUSNDC(passengerChangeable = false),
    GALILEO(passengerChangeable = true),
    GROUPAIR(passengerChangeable = true),
    KOREANAIR(passengerChangeable = false),
    JEJUAIR(passengerChangeable = true),
}

6. 유효성 검사

6.1 승객 수 검증 (checkSearchablePassengers)

// SearchValidator.kt:6-30
fun checkSearchablePassengers(adult: Int, child: Int, infant: Int) {
    // 1. 성인 최소 1명 필요
    if (adult < 1) {
        throw MethodArgumentInvalidException(MessageKey.INVALID_PASSENGERS)
    }
 
    // 2. 총 승객 9명 이하
    if (adult + child + infant > 9) {
        throw MethodArgumentInvalidException(MessageKey.INVALID_PASSENGERS_COUNT)
    }
 
    // 3. 소아는 성인의 3배 이하
    if (child > adult * 3) {
        throw MethodArgumentInvalidException(MessageKey.INVALID_PASSENGERS_CHILD)
    }
 
    // 4. 유아는 성인 수 이하
    if (adult < infant) {
        throw MethodArgumentInvalidException(MessageKey.INVALID_PASSENGERS_INFANT)
    }
}

6.2 유효성 검사 규칙 요약

규칙조건에러 코드
성인 필수adult >= 1INVALID_PASSENGERS
총 인원 제한adult + child + infant <= 9INVALID_PASSENGERS_COUNT
소아 비율child <= adult * 3INVALID_PASSENGERS_CHILD
유아 비율infant <= adultINVALID_PASSENGERS_INFANT

6.3 유지보수 시간 체크 (checkMaintenanceTime)

// Maintenance.kt:5-9
fun checkMaintenanceTime(supplier: String) {
    when (supplier) {
        "AMADEUS" -> Unit  // 현재 별도 처리 없음
    }
}

7. 에러 처리 및 예외 상황

7.1 예외 클래스 구조

// Exceptions.kt:73-76
class InternationalSearchException : ApiException {
    constructor(messageKey: MessageKey, vararg objs: Any) : super(messageKey, *objs)
    constructor(messageKey: MessageKey, cause: Throwable, vararg objs: Any) : super(messageKey, cause, *objs)
}

7.2 발생 가능 에러 목록

상황MessageKey발생 위치
승객 수 유효성 실패INVALID_PASSENGERS, INVALID_PASSENGERS_COUNT, INVALID_PASSENGERS_CHILD, INVALID_PASSENGERS_INFANTSearchValidator.kt:6-30
가용 좌석 부족INVALID_PASSENGERS_AVAILFlightDetailService.kt:66-68
운임 노출 불가CHANGED_PRICE (“Fares Not Exposed”)FlightDetailService.kt:85-87
프로모션 없음CHANGED_PRICE (“Not Found Promotion”)FlightDetailService.kt:334-336
가격 변동CHANGED_PRICEFlightDetailService.kt:134-136
품절 (ID 없음)SOLD_OUTFlightDetailService.kt:250
품절 (운임 없음)SOLD_OUTFlightDetailService.kt:233-248
캐시 키 무효INVALID_CACHE_KEYFareRuleService.kt:78

7.3 MessageKey 정의

// MessageKey.kt:3-36
enum class MessageKey(private val messageSourceKey: String) {
    INVALID_CACHE_KEY(messageSourceKey = "invalid.cache.key"),
    INVALID_PARAMETER(messageSourceKey = "invalid.parameter"),
    INVALID_PASSENGERS(messageSourceKey = "invalid.passengers"),
    INVALID_PASSENGERS_COUNT(messageSourceKey = "invalid.passengers.count"),
    INVALID_PASSENGERS_CHILD(messageSourceKey = "invalid.passengers.child"),
    INVALID_PASSENGERS_INFANT(messageSourceKey = "invalid.passengers.infant"),
    INVALID_PASSENGERS_AVAIL(messageSourceKey = "invalid.passengers.avail"),
    CHANGED_PRICE(messageSourceKey = "changed.price"),
    SOLD_OUT(messageSourceKey = "sold-out"),
    // ...
}

7.4 가격 변동 검증 로직

// FlightDetailService.kt:121-136
val originTotalPrice = flightItem.fares.firstOrNull {
    (it.promotionPrincipleId == promotionPrincipleId ||
     (promotionPrincipleId == null && it.isBasicFare)) &&
    it.adultFare.identityType == flightDetail.adultIdentityType
}?.let { fare ->
    fare.passengerFares.sumOf { passengerFare ->
        passengerFare.total * passengerCountMap.getValue(passengerFare.type)
    }
} ?: 0
 
val totalPrice = flightDetail.totalPrice +
    flightDetail.passengerFares.sumOf { it.naverDiscount * it.count }
 
// 1000원 초과 차이시 에러
if (abs(originTotalPrice - totalPrice) > 1000) {
    throw InternationalSearchException(MessageKey.CHANGED_PRICE, originTotalPrice, totalPrice)
}

8. 관련 도메인 모델 분석

8.1 FlightDetail 도메인 모델

// FlightDetail.kt:14-25
data class FlightDetail(
    val key: String,
    val id: String,
    val validatingCarrier: Airline,
    val schedules: List<ScheduleDetail>,
    val avail: Int,
    val fares: List<FlightFareDetail>,
    val cardPromotionName: String?,
    val promotionPrincipleId: Long? = null,
    val tags: List<String>,
    val tripType: TripType,
)

8.2 FlightDetail 계산 속성

// FlightDetail.kt:26-51
val totalPrice: Long                  // passengerFares 합계
val passengerFares: List<PassengerFareDetail>  // 프로모션 적용된 운임 또는 일반 운임
val adult: Int                        // 성인 수
val child: Int                        // 소아 수
val infant: Int                       // 유아 수
val adultIdentityType: IdentityType?  // 성인 신분 타입

8.3 PassengerFareDetail 도메인 모델

// FlightDetail.kt:361-391
data class PassengerFareDetail(
    val passengerType: PassengerType,
    val count: Int,
    val airPrice: Long,
    val tax: Long,
    val fuelCharge: Long,
    val qCharge: Long,
    val discounts: List<DiscountDetail>,
    val ticketingFee: Long,
    val carrierFee: Long,
    val identityCode: String,
    val identityType: IdentityType?,
) {
    val otherTax: Long                  // tax - fuelCharge
    val total: Long                     // airPrice + tax - discounts + ticketingFee
    val cardPromotionDiscount: Long     // 카드 프로모션 할인
    val sellerPromotionDiscount: Long   // 판매사 프로모션 할인
    val sellerDiscount: Long            // 판매사 할인
    val naverDiscount: Long             // 네이버 할인
}

8.4 TripType Enum

// TripType.kt:3-18
enum class TripType {
    ONE_WAY,       // 편도
    ROUND_TRIP,    // 왕복
    MULTI_CITY;    // 다구간
 
    fun getScheduleTitle(scheduleIndex: Int): String {
        return when (this) {
            ONE_WAY -> "가는편"
            ROUND_TRIP -> if (scheduleIndex == 0) "가는편" else "오는편"
            MULTI_CITY -> "여정 ${scheduleIndex + 1}"
        }
    }
}

8.5 PassengerType Enum

// PassengerType.kt:3-9
enum class PassengerType(val value: String) {
    ADULT(value = "ADT"),   // 성인
    CHILD(value = "CHD"),   // 소아
    INFANT(value = "INF")   // 유아
}

8.6 CardBenefit 모델

// BillingResponse.kt:3-14
data class CardBenefit(
    val prepayment: Boolean,
    val title: String,
    val period: String,
    val interestFreeCards: List<InterestFreeCard>,
    val cautions: List<String>,
)
 
data class InterestFreeCard(
    val cardCompany: String,
    val installments: List<Int>,
)

9. MetaFareStrategy (운임 조회 전략)

9.1 전략 종류

// MetaFareStrategy.kt:3-8
enum class MetaFareStrategy {
    LOWEST_FARE,                            // 최저가
    LOWEST_REPRESENTATIVE_PROMOTION_FARE,   // 대표카드 프로모션 중 최저가
    LOWEST_FARE_WITH_CARD_PROMOTION,        // 특정 카드 프로모션 중 최저가
    MATCHED_FARE_WITH_PROMOTION_PRINCIPLE   // 특정 프로모션 정책 운임
}

9.2 getDetailKeys 전략별 처리

// FlightDetailService.kt:229-249
when (fareStrategy) {
    MetaFareStrategy.LOWEST_FARE ->
        fares.minByOrNull { it.totalPrice }
 
    MetaFareStrategy.LOWEST_REPRESENTATIVE_PROMOTION_FARE ->
        fares.filter { it.representative ?: true }.minByOrNull { it.totalPrice }
 
    MetaFareStrategy.LOWEST_FARE_WITH_CARD_PROMOTION ->
        fares.filter { it.cardPromotionId == flightTraceInfo.cardPromotionId }
            .minByOrNull { it.totalPrice }
 
    else ->
        fares.find { it.promotionPrincipleId == flightTraceInfo.promotionPrincipleId }
}

10. 참고 정보

10.1 관련 API

API설명
FlightSearchController항공편 검색 API
FareRuleController운임 규정 조회 API (폴링 방식)
FlightDetailInternalController내부용 상세 조회 API

10.2 외부 의존성

서비스용도
AdapterClientGDS/공급사 연동 (운임 조회)
PricingClient가격 정책 조회 (할인, 프로모션, TASF)
BillingService카드 혜택 조회
AirlineService항공사 정보 조회
AirportService공항 정보 조회
BookableDateService예약 가능 일자 검증

변경 이력

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

관련 문서