FlightMetaSearchProxyController DFS (기능명세서)

1. API 개요 및 목적

1.1 개요

항목
컨트롤러FlightMetaSearchProxyController
소스 위치air-intl-search/.../interfaces/controller/internal/proxy/FlightMetaSearchProxyController.kt:28
기본 경로/internals/proxy/flights/meta
목적메타 검색 플랫폼(네이버, 스카이스캐너 등)을 위한 국제선 항공편 검색 프록시 API

1.2 목적

이 컨트롤러는 외부 메타 검색 플랫폼과의 연동을 위한 프록시 역할을 수행합니다:

  • 메타 검색 최적화: Funnel별 맞춤 운임 결정 전략 및 항공편 그룹핑 기준 적용
  • 편도/왕복/다구간 지원: 최대 4개 여정까지 지원하는 다양한 검색 엔드포인트 제공
  • 멀티 티켓 옵션: 왕복 검색 시 편도+편도 조합(Mix) 검색 지원
  • 공항/항공사 정보 매핑: 검색 결과에 공항명, 항공사명 등 부가 정보 포함

2. 필수 헤더 요구사항

소스: FlightMetaSearchProxyController.kt:24-27, Constants.kt:6-7

컨트롤러 레벨에서 필수 헤더가 정의되어 있습니다:

헤더명상수명필수 여부설명
x-triple-sales-channelTRIPLE_SALES_CHANNEL_HEADER필수판매 채널 식별
x-triple-sales-funnelTRIPLE_SALES_FUNNEL_HEADER필수판매 퍼널(유입 경로) 식별

2.1 Channel 값

소스: Channel.kt:3-9

enum class Channel {
    YANOLJA,
    TRIPLE,
    INTERPARK,
    KAKAOMOBILITY,
    BTMS,
}

2.2 Funnel 값

소스: Funnel.kt:3-13

enum class Funnel {
    YANOLJA,
    SKYSCANNER,
    TRIPLE,
    NAVER,
    NAVER_SMART,
    NAVER_GOLD,
    INTERPARK,
    KAKAOMOBILITY,
    BTMS,
    NOL_UNIVERSE
}

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

3.1 편도 검색 (One-Way)

소스: FlightMetaSearchProxyController.kt:36-65

항목
HTTP MethodGET
URL Path/internals/proxy/flights/meta/search/{originType}:{origin}-{destinationType}:{destination}/{outboundDate}
메서드searchOneWayTrip()
Response Status200 OK

Path Variables

파라미터타입필수설명
originTypeLocationTypeY출발지 타입 (CITY / AIRPORT)
originStringY출발지 IATA 코드
destinationTypeLocationTypeY도착지 타입
destinationStringY도착지 IATA 코드
outboundDateLocalDateY출발일 (ISO 형식: yyyy-MM-dd)

Query Parameters

파라미터타입필수기본값설명
cabinsSet<CabinType>Y-좌석 등급
adultIntY-성인 승객 수
childIntN0소아 승객 수
infantIntN0유아 승객 수
freeBaggageOnlyBooleanNfalse무료 수하물 포함 항공편만 조회
onlyDirectBooleanNfalse직항편만 조회
useCacheBooleanNfalse상세 조회용 캐시 사용 여부

URL 예시

GET /internals/proxy/flights/meta/search/CITY:SEL-AIRPORT:NRT/2024-03-15?cabins=ECONOMY&adult=2

3.2 왕복 검색 (Round-Trip)

소스: FlightMetaSearchProxyController.kt:67-126

항목
HTTP MethodGET
URL Path/internals/proxy/flights/meta/search/{originType}:{origin}-{destinationType}:{destination}/{outboundDate}/{inboundDate}
메서드searchRoundTrip()
Response Status200 OK

추가 Path Variables

파라미터타입필수설명
inboundDateLocalDateY귀국일 (ISO 형식)

추가 Query Parameters

파라미터타입필수기본값설명
useMultiTicketBooleanNfalse멀티 티켓(Mix) 검색 활성화

useMultiTicket 분기 로직

소스: FlightMetaSearchProxyController.kt:85-125

return if (useMultiTicket == true) {
    searchRoundFlights(...)  // CombineFlightSearchUseCase 사용
} else {
    searchFlights(...)       // StandardFlightSearchUseCase 사용
}
  • useMultiTicket=true: CombineFlightSearchUseCase를 통해 왕복 티켓과 편도+편도 조합(Mix) 결과를 함께 반환
  • useMultiTicket=false: StandardFlightSearchUseCase를 통해 일반 왕복 티켓만 반환

3.3 다구간 검색 (Multi-City)

다구간 검색은 2개, 3개, 4개 여정을 지원하는 개별 엔드포인트로 제공됩니다.

3.3.1 2구간 검색

소스: FlightMetaSearchProxyController.kt:128-171

항목
URL Path/internals/proxy/flights/meta/search/{originType1}:{origin1}-{destinationType1}:{destination1}/{date1}/{originType2}:{origin2}-{destinationType2}:{destination2}/{date2}

3.3.2 3구간 검색

소스: FlightMetaSearchProxyController.kt:173-226

항목
URL Path.../{date2}/{originType3}:{origin3}-{destinationType3}:{destination3}/{date3}

3.3.3 4구간 검색

소스: FlightMetaSearchProxyController.kt:228-291

항목
URL Path.../{date3}/{originType4}:{origin4}-{destinationType4}:{destination4}/{date4}

다구간 전용 Query Parameters

파라미터타입필수기본값설명
useRecommendationBooleanNtrue추천 점수 적용 여부 [확인 필요: 실제 사용되지 않음]

[확인 필요]: useRecommendation 파라미터가 수신되지만 searchFlights() 호출 시 useRecommendation = false로 하드코딩되어 있음 (FlightMetaSearchProxyController.kt:317)


4. Request/Response 구조

4.1 LocationType

소스: LocationType.kt:3-6

enum class LocationType(val shorter: String) {
    CITY(shorter = "c"),    // 도시 (해당 도시의 모든 공항 포함)
    AIRPORT(shorter = "a")  // 단일 공항
}

4.2 CabinType

소스: CabinType.kt:3-10

enum class CabinType(val value: String) {
    ECONOMY(value = "Y"),           // 이코노미
    PREMIUM_ECONOMY(value = "W"),   // 프리미엄 이코노미
    BUSINESS(value = "C"),          // 비지니스
    FIRST(value = "F")              // 일등석
}

4.3 Response: ProxyFlightMetaSearchView

소스: ProxyFlightMetaSearchView.kt:14-83

data class ProxyFlightMetaSearchView(
    val listKey: UUID,                           // 검색 결과 고유 키
    val supplier: String,                        // 공급사 코드
    @Deprecated("external api 에서 미사용하여 추후 제거")
    val scheduleKey: String,                     // 스케줄 키 (deprecated)
    val tags: List<TagType>?,                    // 추천 태그
    val validatingCarrier: String,               // 발권 항공사
    val schedules: List<ProxyMetaScheduleView>,  // 여정 정보
    val fares: List<ProxyMetaFareView>,          // 운임 정보
    val isMix: Boolean,                          // Mix 티켓 여부
)

4.4 ProxyMetaScheduleView

소스: ProxyFlightMetaSearchView.kt:85-129

data class ProxyMetaScheduleView(
    val sequence: Int,                           // 여정 순서
    val flightTime: String,                      // 총 비행시간
    val departure: String,                       // 출발 공항
    val arrival: String,                         // 도착 공항
    val departureAt: LocalDateTime,              // 출발 시각
    val arrivalAt: LocalDateTime,                // 도착 시각
    val stopPoints: List<String>,                // 경유지
    val addDay: Int,                             // 추가 일수
    val stop: Int,                               // 경유 횟수
    val avail: Int,                              // 잔여 좌석
    val freeBaggage: ProxyMetaFreeBaggageView?,  // 무료 수하물
    val segments: List<ProxyMetaSegmentView>,    // 구간 정보
)

4.5 ProxyMetaSegmentView

소스: ProxyFlightMetaSearchView.kt:131-178

data class ProxyMetaSegmentView(
    val sequence: Int,
    val departure: String,
    val departureName: String?,                  // 출발 공항명
    val departureAt: LocalDateTime,
    val arrival: String,
    val arrivalName: String?,                    // 도착 공항명
    val arrivalAt: LocalDateTime,
    val marketingCarrier: String,                // 마케팅 항공사
    val marketingCarrierName: String?,           // 마케팅 항공사명
    val operatingCarrier: String?,               // 운항 항공사
    val operatingCarrierName: String?,           // 운항 항공사명
    val bookingClass: String,                    // 예약 클래스
    val flightNumber: String,                    // 편명
    val cabin: CabinType,                        // 좌석 등급
    val flightTime: String,                      // 비행시간
    val connectingTime: String?,                 // 연결 시간
    val freeBaggage: ProxyMetaFreeBaggageView?,
    val equipmentType: String?,                  // 기종
    val avail: Int,
    val legs: List<ProxyMetaLegView>
)

4.6 ProxyMetaFareView

소스: ProxyFlightMetaSearchView.kt:218-274

data class ProxyMetaFareView(
    val id: String,
    val funnel: Funnel,                          // 운임 적용 퍼널
    val lookUpKey: String,                       // 메타 매핑용 운임 키
    val key: String,                             // adapter detailKey
    val avail: Int,
    val passengerFares: List<ProxyMetaPassengerFareView>,
    val promotionPrincipleId: Long?,             // 프로모션 정책 ID
    val cardPromotionId: Long?,                  // 카드 프로모션 ID
    val cardPromotionName: String?,
    val representative: Boolean?,                // 대표 운임 여부
    val naverCardType: String?,                  // 네이버 카드 타입
    val tags: List<String>,                      // 운임 태그
    val lowestAdultTotalPrice: Long?,            // 성인 최저 총액
    val lowestTotalPrice: Long?,                 // 전체 최저 총액
    val identityType: IdentityType,              // 신분 유형
)

4.7 ProxyMetaPassengerFareView

소스: ProxyFlightMetaSearchView.kt:276-305

data class ProxyMetaPassengerFareView(
    val type: PassengerType,        // ADULT, CHILD, INFANT
    val count: Int,                 // 승객 수
    val airPrice: Long,             // 항공 운임
    val tax: Long,                  // 세금
    val fuelCharge: Long,           // 유류할증료
    val ticketingFee: Long,         // 발권 수수료
    val discounts: List<ProxyMetaDiscountView>,  // 할인 내역
    val total: Long,                // 총액
    val identityCode: String,
    val identityType: IdentityType?,
)

4.8 기타 Enum Types

TagType (추천 태그)

소스: TagType.kt:3-7

enum class TagType(val description: String) {
    LOWEST_PRICE("최저가 추천"),
    SHORTEST_DISTANCE("최단비행시간"),
    POPULAR_DEPARTURE_TIME("인기시간대")
}

PassengerType

소스: PassengerType.kt:3-9

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

IdentityType (신분 유형)

소스: IdentityType.kt:3-14

enum class IdentityType {
    ADULT, CHILD, INFANT,
    LABOR,              // 근로자
    VIETNAM_LABOR,      // 베트남 근로자
    STUDENT,            // 학생
    DISABLED,           // 장애인
    INCLUSIVE_TOUR      // 패키지 여행
}

DiscountType

소스: DiscountType.kt:3-8

enum class DiscountType {
    SELLER_DISCOUNT,    // 판매자 할인
    CARD_PROMOTION,     // 카드 프로모션
    SELLER_PROMOTION,   // 판매자 프로모션
    NAVER_DISCOUNT,     // 네이버 할인
}

BaggageUnit

소스: BaggageUnit.kt:3-12

enum class BaggageUnit(val shorter: String) {
    QUANTITY(shorter = "개"),      // 수량
    WEIGHT_KG(shorter = "kg"),     // 무게(KG)
    WEIGHT_LB(shorter = "lb")      // 무게(LB)
}

5. 비즈니스 로직 흐름

5.1 전체 처리 흐름

sequenceDiagram
    participant Client as 메타 검색 클라이언트
    participant Ctrl as FlightMetaSearchProxyController
    participant LocSvc as LocationService
    participant UseCase as StandardFlightSearchUseCase<br/>/ CombineFlightSearchUseCase
    participant FlightSvc as FlightSearchService
    participant AirportSvc as AirportService
    participant AirlineSvc as AirlineService

    Client->>Ctrl: GET /search/...
    Note over Ctrl: 헤더에서 Channel/Funnel 추출

    Ctrl->>LocSvc: getLocationInfo(origin, originType)
    LocSvc-->>Ctrl: LocationInfo

    Ctrl->>LocSvc: getLocationInfo(destination, destinationType)
    LocSvc-->>Ctrl: LocationInfo

    Ctrl->>UseCase: searchFlights(searchInfo, ...)
    UseCase->>FlightSvc: searchFlights(...)
    FlightSvc-->>UseCase: List<FlightItem>
    UseCase-->>Ctrl: List<FlightItem> / List<CombinedFlightItem>

    Ctrl->>Ctrl: extractCodesFromSchedules()
    Ctrl->>AirportSvc: getAirportsMap(airportCodes)
    AirportSvc-->>Ctrl: Map<String, Airport>

    Ctrl->>AirlineSvc: getAirlinesMap(airlineCodes)
    AirlineSvc-->>Ctrl: Map<String, Airline>

    Ctrl->>Ctrl: ProxyFlightMetaSearchView.of(...)
    Ctrl-->>Client: List<ProxyFlightMetaSearchView>

5.2 searchFlights 내부 처리

소스: FlightMetaSearchProxyController.kt:293-335

private fun searchFlights(...): List<ProxyFlightMetaSearchView> {
    // 1. SearchInfo 생성
    val searchInfo = SearchInfo(adult, child, infant, freeBaggageOnly, cabins, ...)
 
    // 2. MDC에서 Funnel 추출
    val funnel = MDCHolder.SalesFunnel.get()
 
    // 3. UseCase를 통한 항공편 검색
    val flightItems = standardFlightSearchUseCase.searchFlights(
        searchInfo = searchInfo,
        onlyDirect = onlyDirect,
        useRecommendation = false,                                    // 메타는 추천 미사용
        fareDecisionStrategy = FareDecisionStrategy.getByMetaFunnel(funnel),
        flightGroupCriteria = FlightGroupCriteria.getByMetaFunnel(funnel),
        onlyRepresentativeCardPromotion = true,                       // 대표 카드 프로모션만
        funnels = funnel.getMetaSearchFunnels(),
    )
 
    // 4. 공항/항공사 코드 추출 및 정보 조회
    val (airportCodes, airlineCodes) = extractCodesFromSchedules(flightItems)
    val airportMap = airportService.getAirportsMap(codes = airportCodes)
    val airlineMap = airlineService.getAirlinesMap(iatas = airlineCodes)
 
    // 5. View 변환 후 반환
    return flightItems.map {
        ProxyFlightMetaSearchView.of(flightItem = it, airportMap = airportMap, airlineMap = airlineMap)
    }
}

5.3 searchRoundFlights 내부 처리 (멀티 티켓)

소스: FlightMetaSearchProxyController.kt:337-379

useMultiTicket=true인 경우 CombineFlightSearchUseCase를 사용:

private fun searchRoundFlights(...): List<ProxyFlightMetaSearchView> {
    val searchInfo = SearchInfo(...)
    val funnel = MDCHolder.SalesFunnel.get()
 
    // CombineFlightSearchUseCase 사용 - RT와 Mix 모두 검색
    val flightItems = combineFlightSearchUseCase.searchFlights(
        searchInfo = searchInfo,
        onlyDirect = onlyDirect,
        useRecommendation = false,
        fareDecisionStrategy = FareDecisionStrategy.getByMetaFunnel(funnel),
        onlyRepresentativeCardPromotion = true,
        flightGroupCriteria = FlightGroupCriteria.getByMetaFunnel(funnel),
        funnels = funnel.getMetaSearchFunnels(),
    )
    // ... 공항/항공사 정보 조회 및 View 변환
}

6. MetaFunnel별 처리 전략

6.1 FareDecisionStrategy (운임 결정 전략)

소스: FareDecisionStrategy.kt:3-14

enum class FareDecisionStrategy {
    LOWEST_ADULT_FARE,                    // 성인 최저가 기준
    LOWEST_FARE_BY_IDENTITY_AND_CARD_TYPE // 신분유형 & 카드타입별 최저가
}

Funnel별 전략 매핑

소스: FareDecisionStrategy.kt:8-13

FunnelFareDecisionStrategy
SKYSCANNERLOWEST_ADULT_FARE
그 외 전체LOWEST_FARE_BY_IDENTITY_AND_CARD_TYPE

6.2 FlightGroupCriteria (항공편 그룹핑 기준)

소스: FlightGroupCriteria.kt:6-58

enum class FlightGroupCriteria {
    NONE,                      // 그룹핑 없음
    SCHEDULE,                  // 스케줄 기준 (기본)
    SCHEDULE_WITH_CLASS,       // 스케줄 + 예약클래스
    SCHEDULE_WITH_FREE_BAGGAGE // 스케줄 + 무료수하물
}

Funnel별 그룹핑 기준

소스: FlightGroupCriteria.kt:52-57

FunnelFlightGroupCriteria
NAVER, NAVER_SMART, NAVER_GOLDSCHEDULE_WITH_FREE_BAGGAGE
그 외 전체SCHEDULE

6.3 Funnel별 검색 퍼널 목록

소스: Funnel.kt:15-20

fun getMetaSearchFunnels(): List<Funnel> {
    return when (this) {
        NAVER -> listOf(NAVER, NAVER_SMART, NAVER_GOLD)  // 네이버는 3개 Funnel 모두 검색
        else -> listOf(this)                             // 나머지는 자기 자신만
    }
}

7. useMultiTicket 옵션 분석

7.1 개요

useMultiTicket은 왕복 검색 시 편도+편도 조합(Mix) 티켓 검색을 활성화하는 옵션입니다.

7.2 처리 흐름 비교

옵션UseCase결과
useMultiTicket=falseStandardFlightSearchUseCase왕복(RT) 티켓만
useMultiTicket=trueCombineFlightSearchUseCaseRT + Mix 티켓

7.3 CombineFlightSearchUseCase 로직

소스: CombineFlightSearchUseCase.kt:28-65

fun searchFlights(...): List<CombinedFlightItem> {
    // 1. 항공편 검색 (useMultiTicket=true로 호출)
    val flightItems = flightSearchService.searchFlights(
        searchInfo = searchInfo,
        useMultiTicket = true,  // Mix 검색 활성화
        ...
    )
 
    // 2. RT와 Mix 조합
    return combineMultiTicketSearchFlights(flightItems = flightItems)
        .compact(debug)                               // 중복 제거
        .groupBy { flightGroupCriteria.getKey(it) }   // 그룹핑
        .map { (_, items) -> items.pickOptimalFlight(fareDecisionStrategy) }
}

7.4 Mix 조합 로직

소스: CombineFlightSearchUseCase.kt:67-86

private fun combineMultiTicketSearchFlights(flightItems: List<FlightItem>): List<CombinedFlightItem> {
    // 1. Mix 가능 항공편과 왕복 항공편 분리
    val (mixedFlightItems, roundTripFlightItems) = flightItems.partition { it.tripDirectionType != null }
    val (outbounds, inbounds) = mixedFlightItems.partition { it.tripDirectionType == TripDirectionType.OUTBOUND }
 
    // 2. 왕복 항공편을 CombinedFlightItem으로 래핑
    val wrappedRoundTripFlightItem = roundTripFlightItems.map { CombinedFlightItem.ofRoundTrip(it) }
 
    // 3. 출발편 + 귀국편 조합
    val combinedMixFlightItems = outbounds.flatMap { outbound ->
        combineFlightItems(outbound, inbounds)
    }
 
    return wrappedRoundTripFlightItem + combinedMixFlightItems
}

7.5 연결 시간 검증

소스: FlightItem.kt:801-806

fun isValidConnectionTime(outbound: FlightItem, inbound: FlightItem): Boolean {
    return outbound.inboundArrivalAt differ inbound.outboundDepartureAt >= Duration.ofHours(Constants.FLIGHT_INTERVAL_HOURS)
}
// FLIGHT_INTERVAL_HOURS = 3L (Constants.kt:14)

Mix 조합 시 출발편 도착과 귀국편 출발 사이 최소 3시간 간격 필요


8. 공항/항공사 정보 조회 로직

8.1 코드 추출

소스: FlightMetaSearchProxyController.kt:381-400

private fun extractCodesFromSchedules(flightItems: List<CommonFlightItem>): Pair<Set<String>, Set<String>> {
    val airportCodes = mutableSetOf<String>()
    val airlineCodes = mutableSetOf<String>()
 
    flightItems.forEach { flightItem ->
        flightItem.schedules.forEach { schedule ->
            schedule.segments.forEach { segment ->
                segment.legs.forEach { leg ->
                    airportCodes.add(leg.departure)
                    airportCodes.add(leg.arrival)
                }
                airlineCodes.add(segment.marketingCarrier)
                if (segment.operatingCarrier != null) {
                    airlineCodes.add(segment.operatingCarrier)
                }
            }
        }
    }
    return airportCodes to airlineCodes
}

8.2 AirportService

소스: AirportService.kt:9-37

@Service
class AirportService(
    private val cityClient: CityClient,
    airportRedisTemplate: RedisTemplate<String, Airport>,
) {
    private val airportCacheKeyPrefix = "airport"
    private val airportTTL = Duration.ofDays(1)  // 1일 TTL
 
    fun getAirportsMap(codes: Set<String>): Map<String, Airport> {
        return codes.map { getAirportByIata(it) }.associateBy { it.iataCode }
    }
 
    fun getAirportByIata(iata: String): Airport {
        val key = "$airportCacheKeyPrefix::$iata"
        return airportValueOperation.get(key)
            ?: cityClient.getAirportByIata(iata)
                ?.also { airportValueOperation.set(key, it, airportTTL) }
            ?: Airport.ofIata(iata)  // 미등록 공항은 IATA 코드로 기본 객체 생성
    }
}

8.3 AirlineService

소스: AirlineService.kt:9-38

@Service
class AirlineService(
    airlineRedisTemplate: RedisTemplate<String, Airline>,
    private val flightClient: FlightClient,
) {
    private val airlineCacheKeyPrefix = "airline"
    private val airlineTTL = Duration.ofDays(1)  // 1일 TTL
 
    fun getAirlinesMap(iatas: Set<String>): Map<String, Airline> {
        return iatas.map { getAirline(it) }.associateBy { it.iata }
    }
 
    private fun getAirline(iata: String): Airline {
        val key = "$airlineCacheKeyPrefix::$iata"
        return valueOperation.get(key)
            ?: flightClient.findAirlineByIata(iata)
                ?.also { valueOperation.set(key, it, airlineTTL) }
            ?: Airline.ofIata(iata)  // 미등록 항공사는 IATA 코드로 기본 객체 생성
    }
}

8.4 캐싱 전략

항목캐시 키 패턴TTL
공항airport::{IATA}1일
항공사airline::{IATA}1일

9. 에러 처리 및 예외 상황

9.1 유효성 검사 (SearchInfo.validate)

소스: SearchInfo.kt:46-52

fun validate(bookableDateRange: LongRange) {
    checkBookableDate(bookableDateRange)   // 예약 가능 날짜 범위 검증
    checkSearchableDates()                  // 여정 날짜 순서 검증
    checkSearchablePassengers(adult, child, infant)  // 승객 수 검증
    checkSearchableItinerary()              // 여정 유효성 검증
    checkMultiTicketSearch()                // 멀티티켓 검색 조건 검증
}

9.2 여정 유효성 검사

소스: SearchInfo.kt:69-84

검사 항목예외 메시지 키설명
출발지 = 도착지INVALID_ITINERARY동일 공항/도시
국내선INVALID_ITINERARY국제선 전용 서비스
제한 국가INVALID_ITINERARY_RESTRICTED_COUNTRYSY, IR, UA, CU

9.3 제한 국가 목록

소스: OriginDestinationLocationInfo.kt:45-47

fun isRestrictedCountry(): Boolean {
    return setOf("SY", "IR", "UA", "CU").contains(countryCode)
}
국가 코드국가명
SY시리아
IR이란
UA우크라이나
CU쿠바

9.4 Location 조회 실패

소스: LocationService.kt:15-30

fun getLocationInfo(type: LocationType, iata: String): LocationInfo {
    return try {
        when (type) {
            LocationType.CITY -> { ... }
            LocationType.AIRPORT -> { ... }
        }
    } catch (e: Exception) {
        throw MethodArgumentInvalidException(
            messageKey = MessageKey.INVALID_PARAMETER,
            cause = e,
            iata
        )
    }
}

9.5 검색 실패

소스: FlightSearchService.kt:221-223

} catch (e: Exception) {
    throw InternationalSearchException(MessageKey.SEARCH_FAILED, e)
}

10. 관련 도메인 모델 분석

10.1 SearchInfo

소스: SearchInfo.kt:15-44

검색 조건을 담는 핵심 도메인 모델입니다.

data class SearchInfo(
    val adult: Int,                                              // 성인 수
    val child: Int,                                              // 소아 수
    val infant: Int,                                             // 유아 수
    val freeBaggageOnly: Boolean,                                // 무료수하물 필터
    val cabins: Set<CabinType>,                                  // 좌석 등급
    val originDestinationLocationInfos: List<OriginDestinationLocationInfo>,  // 여정 정보
    val multiTicket: MultiTicket? = null,                        // 멀티티켓 옵션
    val useCache: Boolean,                                       // 캐시 사용 여부
)

10.2 OriginDestinationLocationInfo

소스: OriginDestinationLocationInfo.kt:11-30

개별 여정 정보를 담는 모델입니다.

data class OriginDestinationLocationInfo(
    val origin: LocationInfo,       // 출발지 정보
    val destination: LocationInfo,  // 도착지 정보
    val date: LocalDate,            // 출발일
)

10.3 LocationInfo (Sealed Class)

소스: OriginDestinationLocationInfo.kt:32-93

sealed class LocationInfo(val type: LocationType) {
    abstract val iata: String
    abstract val airportIatas: Set<String>
    abstract val cityIata: String
    abstract val countryCode: String
    abstract val zoneId: ZoneId
 
    data class City(...) : LocationInfo(LocationType.CITY)
    data class Airport(...) : LocationInfo(LocationType.AIRPORT)
}

10.4 FlightItem vs CombinedFlightItem

모델용도소스
FlightItem단일 티켓 (편도/왕복)FlightItem.kt:21-329
CombinedFlightItem조합 티켓 (RT + Mix)CombinedFlightItem.kt:14-71

10.5 Airport 데이터 모델

소스: CityResponse.kt:19-52

data class Airport(
    val id: UUID?,
    val iataCode: String,
    val names: NameResponse,       // 한글/영문/현지어 이름
    val shortNames: NameResponse?,
    val country: SimpleCountryResponse,
    val city: SimpleCityResponse,
    val timezone: String?,
    val poiId: String?
)

10.6 Airline 데이터 모델

소스: FlightResponse.kt:8-26

data class Airline(
    val id: Long?,
    val nameEn: String?,
    val nameKo: String?,
    val iata: String,
    val logoUrl: String?,          // 기본값: Constants.DEFAULT_AIRLINE_LOGO
    val carrierType: CarrierType
)

11. 의존성 관계

graph TD
    A[FlightMetaSearchProxyController] --> B[StandardFlightSearchUseCase]
    A --> C[CombineFlightSearchUseCase]
    A --> D[LocationService]
    A --> E[AirportService]
    A --> F[AirlineService]

    B --> G[FlightSearchService]
    B --> H[BookableDateService]

    C --> G
    C --> H

    D --> I[CityService]
    D --> E

    E --> J[CityClient]
    E --> K[(Redis)]

    F --> L[FlightClient]
    F --> K

    G --> M[AdapterClient]
    G --> N[PricingClient]
    G --> O[AirConsoleService]

12. 참고 사항

12.1 하드코딩된 값

항목위치
연결 시간 최소값3시간Constants.kt:14
추천 사용 여부falseFlightMetaSearchProxyController.kt:317
대표 카드 프로모션만trueFlightMetaSearchProxyController.kt:320
캐시 TTL1일AirportService.kt:16, AirlineService.kt:14

12.2 Deprecated 필드

필드위치사유
scheduleKeyProxyFlightMetaSearchView.kt:17-18external api에서 미사용

문서 생성일: 2024-12-15 소스 기준: air-intl-search 모듈

관련 문서