FlightSearchV2ProxyController DFS (기능명세서)

1. API 개요 및 목적

1.1 컨트롤러 정보

항목
클래스명FlightSearchV2ProxyController
패키지com.triple.air.intl.search.interfaces.controller.internal.proxy.v2
소스 위치air-intl-search/src/main/kotlin/.../proxy/v2/FlightSearchV2ProxyController.kt
Base Path/internals/proxy/flights/search/v2

1.2 목적

V2 프록시 검색 컨트롤러는 국제선 항공권 검색을 위한 내부 API를 제공합니다. V1과 달리 다음과 같은 특징을 가집니다:

  • MixFlightResponseType 파라미터를 통한 명시적인 SPLIT/COMBINED 모드 구분
  • COMBINED 모드 지원 (왕복 항공권과 MIX 항공권의 조합 결과 반환)
  • flightGroupCriteria 기본값이 SCHEDULE_WITH_FREE_BAGGAGE로 변경
  • V2 전용 응답 형식 (InternalProxyFlightItemV2View)

1.3 필수 헤더 요구사항

소스: FlightSearchV2ProxyController.kt:24-27

@RequestMapping(
    "/internals/proxy/flights/search/v2",
    headers = [Constants.TRIPLE_SALES_CHANNEL_HEADER, Constants.TRIPLE_SALES_FUNNEL_HEADER]
)
헤더상수명필수
x-triple-sales-channelTRIPLE_SALES_CHANNEL_HEADER-O
x-triple-sales-funnelTRIPLE_SALES_FUNNEL_HEADER-O

소스: Constants.kt:6-7


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

2.1 편도 검색 (One-Way Trip)

항목
HTTP MethodGET
URL Pattern/{originType}:{origin}-{destinationType}:{destination}/{outboundDate}
메서드명searchOneWayTrip
소스 위치FlightSearchV2ProxyController.kt:34-69
UseCaseStandardFlightSearchUseCase

Path Variables

파라미터타입설명
originTypeLocationType출발지 타입 (CITY/AIRPORT)
originString출발지 IATA 코드
destinationTypeLocationType도착지 타입 (CITY/AIRPORT)
destinationString도착지 IATA 코드
outboundDateLocalDate출발일 (ISO DATE 형식: yyyy-MM-dd)

Query Parameters

파라미터타입필수기본값설명
cabinsSet<CabinType>O-좌석 등급
adultIntO-성인 인원
childIntX0소아 인원
infantIntX0유아 인원
freeBaggageOnlyBooleanXfalse무료 수하물 포함 항공편만
useRecommendationBooleanXtrue추천 점수 사용 여부
useCacheBooleanXtrue캐시 사용 여부
flightGroupCriteriaFlightGroupCriteriaXSCHEDULE_WITH_FREE_BAGGAGE항공편 그룹핑 기준

2.2 왕복 검색 - 기본 모드 (Round Trip)

항목
HTTP MethodGET
URL Pattern/{originType}:{origin}-{destinationType}:{destination}/{outboundDate}/{inboundDate}
메서드명searchRoundTrip
소스 위치FlightSearchV2ProxyController.kt:71-113
UseCaseStandardFlightSearchUseCase

추가 Path Variables

파라미터타입설명
inboundDateLocalDate귀국일 (ISO DATE 형식: yyyy-MM-dd)

2.3 왕복 검색 - SPLIT 모드

항목
HTTP MethodGET
URL Pattern/{originType}:{origin}-{destinationType}:{destination}/{outboundDate}/{inboundDate}
필수 파라미터mixResponseType=SPLIT
메서드명searchSplitRoundTrip
소스 위치FlightSearchV2ProxyController.kt:115-167
UseCaseSplitFlightSearchUseCase

추가 Query Parameters

파라미터타입필수기본값설명
mixResponseTypeMixFlightResponseTypeX-SPLIT 지정
searchTripDirectionTypeSearchTripDirectionTypeX-OUTBOUND/INBOUND
detailKeyStringX-앵커 항공편 상세 키
promotionPrincipleIdLongX-프로모션 원칙 ID

추가 Headers

헤더타입필수설명
cached-search-keyStringX캐시된 검색 키 (UUID)

응답 Headers

헤더설명
cached-search-key검색 결과 캐시 키 (후속 INBOUND 검색에 사용)

2.4 왕복 검색 - COMBINED 모드

항목
HTTP MethodGET
URL Pattern/{originType}:{origin}-{destinationType}:{destination}/{outboundDate}/{inboundDate}
필수 파라미터mixResponseType=COMBINED
메서드명searchCombinedRoundTrip
소스 위치FlightSearchV2ProxyController.kt:169-217
UseCaseCombineFlightSearchUseCase

추가 Query Parameters

파라미터타입필수기본값설명
mixResponseTypeMixFlightResponseTypeX-COMBINED 지정
useRecommendationBooleanXfalse추천 점수 사용 (기본 false)
debugBooleanXfalse디버그 모드

2.5 다구간 검색 (Multi-City Trip)

2구간 검색

항목
URL Pattern/{originType1}:{origin1}-{destinationType1}:{destination1}/{date1}/{originType2}:{origin2}-{destinationType2}:{destination2}/{date2}
메서드명searchMultiCityTrip (2구간)
소스 위치FlightSearchV2ProxyController.kt:219-268

3구간 검색

항목
URL Pattern2구간 패턴 + /{originType3}:{origin3}-{destinationType3}:{destination3}/{date3}
메서드명searchMultiCityTrip (3구간)
소스 위치FlightSearchV2ProxyController.kt:270-331

4구간 검색

항목
URL Pattern3구간 패턴 + /{originType4}:{origin4}-{destinationType4}:{destination4}/{date4}
메서드명searchMultiCityTrip (4구간)
소스 위치FlightSearchV2ProxyController.kt:333-406

3. V1(FlightSearchProxyController)과의 차이점

V1 소스 위치: air-intl-search/src/main/kotlin/.../proxy/FlightSearchProxyController.kt

3.1 URL 경로 차이

버전Base Path
V1/internals/proxy/flights (엔드포인트별 /search 포함)
V2/internals/proxy/flights/search/v2

3.2 MixFlightResponseType 지원

버전SPLITCOMBINED
V1useMultiTicket=true 플래그로 처리 (소스:85행)미지원
V2mixResponseType=SPLIT 명시적 파라미터mixResponseType=COMBINED 지원

3.3 flightGroupCriteria 기본값

버전기본값소스 위치
V1SCHEDULEFlightSearchProxyController.kt:49, 89, 163, 219, 287
V2SCHEDULE_WITH_FREE_BAGGAGEFlightSearchV2ProxyController.kt:51-52, 89-91, 138-140, 190-192, 244-246, 302-304, 372-374

3.4 응답 타입 차이

버전응답 타입특징
V1InternalProxyFlightItemViewvalidatingCarrier, tripDirectionType 포함
V2InternalProxyFlightItemV2Viewhidden 필드, items 배열(FlightItemFragment) 포함

3.5 UseCase 차이

버전사용 UseCase
V1StandardFlightSearchUseCase, SplitFlightSearchUseCase
V2StandardFlightSearchUseCase, SplitFlightSearchUseCase, CombineFlightSearchUseCase

4. Request/Response 구조

4.1 공통 Request 도메인 모델

LocationType

소스: support/enums/LocationType.kt:3-6

enum class LocationType(val shorter: String) {
    CITY(shorter = "c"),
    AIRPORT(shorter = "a");
}

CabinType

소스: support/enums/CabinType.kt:3-10

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

FlightGroupCriteria

소스: support/enums/FlightGroupCriteria.kt:6-11

enum class FlightGroupCriteria {
    NONE,                        // 그룹핑 없음
    SCHEDULE,                    // 스케줄 기준 (ITX운임과 일반운임 경합)
    SCHEDULE_WITH_CLASS,         // 스케줄 + 예약 클래스
    SCHEDULE_WITH_FREE_BAGGAGE,  // 스케줄 + 무료 수하물
}

SCHEDULE_WITH_FREE_BAGGAGE 그룹핑 로직 (소스: 34-46행):

  • 스케줄 키: {marketingCarrier}{flightNumber}{departureAt.dayOfWeek}
  • 수하물 2PC 이상인 경우 별도 그룹: {volume}PC
  • 수하물 있으면 Y, 없으면 빈 문자열

MixFlightResponseType

소스: support/enums/MixFlightResponseType.kt:3-8

enum class MixFlightResponseType {
    SPLIT,     // MIX 항공권 Outbound, Inbound 각각 반환
    COMBINED;  // MIX 항공권 outbound X inbound 조합으로 반환
}

SearchTripDirectionType

소스: support/enums/SearchTripDirectionType.kt:3-6

enum class SearchTripDirectionType {
    OUTBOUND,
    INBOUND,
}

4.2 내부 모델

SearchInfo

소스: support/model/SearchInfo.kt:15-106

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,
)

검증 로직 (validate 메서드, 소스: 46-52행):

  1. 예약 가능 날짜 범위 확인
  2. 검색 날짜 순서 확인 (출발일 귀국일)
  3. 탑승객 수 유효성 검사
  4. 여정 유효성 검사 (출도착 동일 불가, 국내선 불가, 제한 국가 불가)
  5. MIX 검색 유효성 검사

MultiTicket

소스: support/model/SearchInfo.kt:108-113

data class MultiTicket(
    val searchTripDirectionType: SearchTripDirectionType?,
    val detailKey: String?,
    val promotionPrincipleId: Long?,
    val cachedListKey: UUID?,
)

4.3 Response 구조

InternalProxyFlightItemV2View

소스: interfaces/response/ProxyCombinedFlightSearchView.kt:15-63

data class InternalProxyFlightItemV2View(
    val listKey: UUID,
    val score: BigDecimal?,
    val tags: List<TagType>?,
    val schedules: List<InternalProxyScheduleView>,
    val fares: List<InternalProxyFlightFareV2View>,
    val tripType: TripType,
    val hidden: Boolean = false,
)

InternalProxyFlightFareV2View

소스: interfaces/response/ProxyCombinedFlightSearchView.kt:66-108

data class InternalProxyFlightFareV2View(
    val avail: Int,
    val passengerFares: List<InternalProxyFlightPassengerFareView>,
    val adultAirPrice: Long,
    val adultTaxPrice: Long,
    val adultTotalPrice: Long,
    val tags: List<String>,
    val identityType: IdentityType,
    val items: List<InternalProxyFlightItemFragmentView>,
)

InternalProxyFlightItemFragmentView

소스: interfaces/response/ProxyCombinedFlightSearchView.kt:111-131

data class InternalProxyFlightItemFragmentView(
    val id: String,
    val key: String,
    val validatingCarrier: String,
    val promotionPrincipleId: Long?,
    val cardPromotionName: String? = null,
    val representative: Boolean? = null,
)

5. 비즈니스 로직 흐름 (UseCase 연동 분석)

5.1 기본 검색 흐름 (Standard)

sequenceDiagram
    participant C as Controller
    participant UC as StandardFlightSearchUseCase
    participant FS as FlightSearchService
    participant BDS as BookableDateService

    C->>C: searchFlights() 호출
    C->>C: SearchInfo 생성
    C->>UC: searchFlights()
    UC->>BDS: getBookableDateRange()
    UC->>UC: searchInfo.validate()
    UC->>FS: searchFlights()
    FS-->>UC: List<FlightItem>
    UC->>UC: groupBy(flightGroupCriteria.getKey)
    UC->>UC: pickOptimalFlight(fareDecisionStrategy)
    opt useRecommendation
        UC->>FS: applyRecommendationScores()
    end
    UC-->>C: List<FlightItem>
    C->>C: map to InternalProxyFlightItemV2View
    C-->>C: ResponseEntity

소스: StandardFlightSearchUseCase.kt:26-82

5.2 SPLIT 검색 흐름

sequenceDiagram
    participant C as Controller
    participant UC as SplitFlightSearchUseCase
    participant FS as FlightSearchService
    participant Cache as CacheKeyGenerator

    C->>C: searchSplitFlights() 호출
    C->>C: SearchInfo 생성 (MultiTicket 포함)
    C->>Cache: getFlightSearchCacheKey() (if cachedListKey is null)
    C->>UC: searchFlights(listKey, searchInfo, flightGroupCriteria)
    alt cachedListKey 존재
        UC->>FS: getFlightItems(cachedListKey)
    else
        UC->>FS: searchFlights(useMultiTicket=true)
    end
    UC->>UC: splitMultiTicketSearchFlights()
    UC->>UC: groupBy & pickOptimalFlight
    UC-->>C: List<FlightItem>
    C->>C: map to InternalProxyFlightItemV2View
    C->>C: ResponseEntity with cached-search-key header

소스: SplitFlightSearchUseCase.kt:24-56, FlightSearchV2ProxyController.kt:446-492

SPLIT 검색 내부 로직

소스: SplitFlightSearchUseCase.kt:58-114

  1. MIX/왕복 분리: flightItems.partition { it.isMix }
  2. MIX Outbound/Inbound 분리: mixFlightItems.partition { it.tripDirectionType == OUTBOUND }
  3. searchTripDirectionType에 따른 분기:
    • INBOUND: anchorFlightItem 기준으로 Inbound 필터링
    • OUTBOUND: Outbound 항공편만 반환
    • null: 전체 검색 (Outbound + Inbound)

5.3 COMBINED 검색 흐름

sequenceDiagram
    participant C as Controller
    participant UC as CombineFlightSearchUseCase
    participant FS as FlightSearchService

    C->>C: searchCombinedFlights() 호출
    C->>UC: searchFlights()
    UC->>FS: searchFlights(useMultiTicket=true)
    UC->>UC: combineMultiTicketSearchFlights()
    UC->>UC: compact() - 동일 스케줄 그룹핑
    UC->>UC: groupBy(flightGroupCriteria.getKey)
    UC->>UC: pickOptimalFlight(fareDecisionStrategy)
    opt useRecommendation
        UC->>FS: applyRecommendationScores()
    end
    UC-->>C: List<CombinedFlightItem>
    C->>C: map to InternalProxyFlightItemV2View
    C-->>C: ResponseEntity

소스: CombineFlightSearchUseCase.kt:28-65

COMBINED 결합 로직

소스: CombineFlightSearchUseCase.kt:67-115

  1. MIX/왕복 분리: flightItems.partition { it.tripDirectionType != null }
  2. 왕복 항공권 래핑: CombinedFlightItem.ofRoundTrip()
  3. MIX 결합: Outbound X Inbound 모든 조합 생성
    • 연결 시간 검증: isValidConnectionTime() (3시간 이상)
    • 운임 정책 필터링: filterByOutboundAndPolicy()
  4. 결과 병합: 왕복 + MIX 결합 항공권

6. MixFlightResponseType별 처리

6.1 SPLIT 모드

특징설명
용도가는편/오는편 별도 선택 UI
결과 형태Outbound, Inbound 목록 각각 반환
캐시 키cached-search-key 헤더로 반환
후속 검색Outbound 선택 후 Inbound 검색 시 detailKey, cachedListKey 필요

6.2 COMBINED 모드

특징설명
용도왕복/MIX 통합 검색 결과
결과 형태왕복 + (Outbound X Inbound) 조합
FareDecisionStrategyLOWEST_FARE_BY_IDENTITY_AND_CARD_TYPE 고정 (소스: 522행)
debug 모드hidden 항목 포함, 로그 출력

7. flightGroupCriteria 기본값 차이

7.1 V1 기본값: SCHEDULE

소스: FlightSearchProxyController.kt:49

@RequestParam(required = false, defaultValue = "SCHEDULE") flightGroupCriteria: FlightGroupCriteria,

그룹핑 키 생성 (소스: FlightGroupCriteria.kt:17-23):

flightItem.schedules.forEach { schedule ->
    schedule.segments.forEach {
        append("${it.marketingCarrier}${it.flightNumber}${it.departureAt.dayOfWeek.value}")
    }
}

7.2 V2 기본값: SCHEDULE_WITH_FREE_BAGGAGE

소스: FlightSearchV2ProxyController.kt:51-52

@RequestParam(
    required = false,
    defaultValue = "SCHEDULE_WITH_FREE_BAGGAGE"
) flightGroupCriteria: FlightGroupCriteria,

그룹핑 키 생성 (소스: FlightGroupCriteria.kt:34-46):

flightItem.schedules.forEach { schedule ->
    schedule.segments.forEach {
        append("${it.marketingCarrier}${it.flightNumber}${it.departureAt.dayOfWeek.value}")
    }
    // PC 단위 수하물은 2개 이상부터 별도 그룹핑
    if (schedule.freeBaggage?.unit == BaggageUnit.QUANTITY && schedule.freeBaggage.volume > 1) {
        append("${schedule.freeBaggage.volume}PC")
    } else {
        append(if ((schedule.freeBaggage?.volume ?: 0) > 0) "Y" else "")
    }
}

차이점 요약:

  • V2는 동일 스케줄이라도 수하물 유무/수량에 따라 별도 그룹으로 분리
  • 무료 수하물 2PC 이상은 {volume}PC로 구분
  • 무료 수하물 1PC 이하는 Y 또는 빈 문자열

8. 에러 처리 및 예외 상황

8.1 SearchInfo 검증 예외

소스: support/model/SearchInfo.kt:46-98

예외 조건MessageKey설명
날짜 순서 오류INVALID_DATES출발일 > 귀국일
출도착 동일INVALID_ITINERARY출발지가 도착지에 포함
국내선INVALID_ITINERARY출도착 모두 국내 공항
제한 국가INVALID_ITINERARY_RESTRICTED_COUNTRYSY, IR, UA, CU
INBOUND 검색 파라미터 누락INVALID_PARAMETERdetailKey 또는 cachedListKey 없음
OUTBOUND에 detailKey 포함INVALID_PARAMETERINBOUND가 아닌데 detailKey 존재

8.2 예약 가능 날짜 범위

소스: SearchInfo.kt:54-56

private fun checkBookableDate(bookableDateRange: LongRange) {
    originDestinationLocationInfos
        .forEach { bookableDateRange.validateBookable(date = it.date) }
}

8.3 탑승객 수 검증

소스: SearchInfo.kt:49

checkSearchablePassengers(adult = this.adult, child = this.child, infant = this.infant)

9. 관련 도메인 모델 분석

9.1 TripType

소스: support/enums/TripType.kt:3-7

enum class TripType {
    ONE_WAY,      // 편도
    ROUND_TRIP,   // 왕복
    MULTI_CITY;   // 다구간
}

판정 로직 (소스: 20-34행):

  • 여정 1개: ONE_WAY
  • 여정 2개 + 출발지귀국도착지 + 목적지귀국출발지: ROUND_TRIP
  • 그 외: MULTI_CITY

9.2 FareDecisionStrategy

소스: support/enums/FareDecisionStrategy.kt:3-6

enum class FareDecisionStrategy {
    LOWEST_ADULT_FARE,                    // 성인 최저가 기준
    LOWEST_FARE_BY_IDENTITY_AND_CARD_TYPE; // 신분유형 & 카드 프로모션별 최저가
}

COMBINED 모드에서는 LOWEST_FARE_BY_IDENTITY_AND_CARD_TYPE 고정 사용 (소스: FlightSearchV2ProxyController.kt:522)

9.3 CombinedFlightItem

소스: support/model/CombinedFlightItem.kt:14-71

data class CombinedFlightItem(
    val listKey: UUID,
    override val schedules: List<Schedule>,
    val fares: List<CombinedFlightFare>,
    var score: BigDecimal?,
    var tags: List<RecommendationTag>?,
    val isMix: Boolean,
    val hidden: Boolean = false,  // V2 전용
    val groupKey: String = ""
)

9.4 CombinedFlightFare

소스: support/model/CombinedFlightItem.kt:73-144

data class CombinedFlightFare(
    val funnel: Funnel,
    val avail: Int,
    val passengerFares: List<PassengerFare>,
    val tags: List<String> = emptyList(),
    val items: List<FlightItemFragment>,  // MIX인 경우 2개, 왕복인 경우 1개
)

9.5 FlightItemFragment

소스: support/model/CombinedFlightItem.kt:146-181

data class FlightItemFragment(
    val id: String,
    val lookUpKey: String = "",
    val detailKey: String,
    val scheduleKey: String,
    val supplier: String,
    val prepayment: Boolean?,
    val validatingCarrier: String,
    val cardPromotionName: String? = null,
    val cardPromotionId: Long? = null,
    val promotionPrincipleId: Long? = null,
    val representative: Boolean? = null,
    val naverCardType: String? = null,
    val freeBaggageInfo: List<FreeBaggage>,
)

10. 의존성 주입

소스: FlightSearchV2ProxyController.kt:28-33

class FlightSearchV2ProxyController(
    private val standardFlightSearchUseCase: StandardFlightSearchUseCase,
    private val splitFlightSearchUseCase: SplitFlightSearchUseCase,
    private val combineFlightSearchUseCase: CombineFlightSearchUseCase,
    private val locationService: LocationService,
)
의존성역할
StandardFlightSearchUseCase기본 검색 (편도, 왕복, 다구간)
SplitFlightSearchUseCaseSPLIT 모드 검색
CombineFlightSearchUseCaseCOMBINED 모드 검색
LocationServiceIATA 코드 LocationInfo 변환

11. 요청 예시

11.1 편도 검색

GET /internals/proxy/flights/search/v2/CITY:SEL-CITY:TYO/2024-03-15?cabins=ECONOMY&adult=2
x-triple-sales-channel: TRIPLE
x-triple-sales-funnel: TRIPLE

11.2 왕복 검색 (기본)

GET /internals/proxy/flights/search/v2/CITY:SEL-CITY:TYO/2024-03-15/2024-03-20?cabins=ECONOMY&adult=2
x-triple-sales-channel: TRIPLE
x-triple-sales-funnel: TRIPLE

11.3 왕복 검색 (SPLIT - Outbound)

GET /internals/proxy/flights/search/v2/CITY:SEL-CITY:TYO/2024-03-15/2024-03-20?cabins=ECONOMY&adult=2&mixResponseType=SPLIT&searchTripDirectionType=OUTBOUND
x-triple-sales-channel: TRIPLE
x-triple-sales-funnel: TRIPLE

11.4 왕복 검색 (SPLIT - Inbound)

GET /internals/proxy/flights/search/v2/CITY:SEL-CITY:TYO/2024-03-15/2024-03-20?cabins=ECONOMY&adult=2&mixResponseType=SPLIT&searchTripDirectionType=INBOUND&detailKey=xxx&promotionPrincipleId=123
x-triple-sales-channel: TRIPLE
x-triple-sales-funnel: TRIPLE
cached-search-key: uuid-from-outbound-search

11.5 왕복 검색 (COMBINED)

GET /internals/proxy/flights/search/v2/CITY:SEL-CITY:TYO/2024-03-15/2024-03-20?cabins=ECONOMY&adult=2&mixResponseType=COMBINED
x-triple-sales-channel: TRIPLE
x-triple-sales-funnel: TRIPLE

12. 참조 문서

관련 문서