FlightSearchProxyController DFS (기능명세서)

1. API 개요 및 목적

1.1 개요

항목
컨트롤러FlightSearchProxyController
패키지com.triple.air.intl.search.interfaces.controller.internal.proxy
소스 위치air-intl-search/src/main/kotlin/.../interfaces/controller/internal/proxy/FlightSearchProxyController.kt
Base Path/internals/proxy/flights
용도내부 서비스 간 프록시용 국제선 항공편 검색 API

1.2 목적

  • 내부 서비스 전용: /internals/ 경로에서 알 수 있듯이 내부 마이크로서비스 간 통신용 API
  • 프록시 기능: 외부 GDS/공급사로부터 항공편을 검색하고 결과를 전달
  • 다양한 여정 지원: 편도(One-Way), 왕복(Round-Trip), 다구간(Multi-City) 검색 지원
  • MultiTicket(Split) 검색 지원: 왕복 여정을 가는편/오는편 별도로 검색하여 최적 조합 제공

1.3 의존성

의존 서비스역할소스 위치
StandardFlightSearchUseCase표준 항공편 검색 처리application/usecase/StandardFlightSearchUseCase.kt:21
SplitFlightSearchUseCaseMultiTicket(분할) 항공편 검색 처리application/usecase/SplitFlightSearchUseCase.kt:19
LocationServiceIATA 코드로 공항/도시 정보 조회application/LocationService.kt:10

2. 필수 헤더 요구사항

2.1 필수 헤더

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

// FlightSearchProxyController.kt:25-28
@RequestMapping(
    "/internals/proxy/flights",
    headers = [Constants.TRIPLE_SALES_CHANNEL_HEADER, Constants.TRIPLE_SALES_FUNNEL_HEADER]
)
헤더명상수값필수 여부설명
x-triple-sales-channelConstants.TRIPLE_SALES_CHANNEL_HEADER필수판매 채널 식별자
x-triple-sales-funnelConstants.TRIPLE_SALES_FUNNEL_HEADER필수판매 퍼널 식별자

소스: support/Constants.kt:6-7

2.2 선택적 헤더

헤더명상수값사용 엔드포인트설명
cached-search-keyConstants.CACHED_SEARCH_KEY왕복 검색 (MultiTicket)캐시된 검색 결과 키

소스: support/Constants.kt:10


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

3.1 편도 검색 (One-Way)

항목
HTTP MethodGET
URL Pattern/internals/proxy/flights/search/{originType}:{origin}-{destinationType}:{destination}/{outboundDate}
메서드명searchOneWayTrip()
소스 위치FlightSearchProxyController.kt:34-66

Path Variables

파라미터타입설명예시
originTypeLocationType출발지 유형 (CITY/AIRPORT)c 또는 a
originString출발지 IATA 코드SEL, ICN
destinationTypeLocationType도착지 유형c 또는 a
destinationString도착지 IATA 코드NRT, TYO
outboundDateLocalDate출발일 (ISO 형식)2024-03-15

Request Parameters

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

호출 예시

GET /internals/proxy/flights/search/c:SEL-a:NRT/2024-03-15?cabins=ECONOMY&adult=2&child=1

3.2 왕복 검색 (Round-Trip)

항목
HTTP MethodGET
URL Pattern/internals/proxy/flights/search/{originType}:{origin}-{destinationType}:{destination}/{outboundDate}/{inboundDate}
메서드명searchRoundTrip()
소스 위치FlightSearchProxyController.kt:68-136

Path Variables

편도와 동일 + 아래 추가:

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

Request Parameters

편도 파라미터 + 아래 추가:

파라미터타입필수기본값설명
useMultiTicketBooleanXfalseSplit 검색 모드 활성화
searchTripDirectionTypeSearchTripDirectionTypeXnull검색 방향 (OUTBOUND/INBOUND)
detailKeyStringXnull앵커 항공편 상세키 (INBOUND 검색 시 필수)
promotionPrincipleIdLongXnull프로모션 정책 ID

Request Headers

헤더설명
cached-search-keyMultiTicket 모드에서 캐시된 검색 키

비즈니스 로직 분기

// FlightSearchProxyController.kt:92-136
return if (useMultiTicket) {
    searchSplitFlights(...)  // Split 검색 UseCase 호출
} else {
    searchFlights(...)       // 표준 검색 UseCase 호출
}

3.3 다구간 검색 (Multi-City) - 2구간

항목
HTTP MethodGET
URL Pattern/internals/proxy/flights/search/{originType1}:{origin1}-{destinationType1}:{destination1}/{date1}/{originType2}:{origin2}-{destinationType2}:{destination2}/{date2}
메서드명searchMultiCityTrip() (2구간)
소스 위치FlightSearchProxyController.kt:138-185

3.4 다구간 검색 (Multi-City) - 3구간

항목
HTTP MethodGET
URL Pattern/internals/proxy/flights/search/.../date1/.../date2/.../date3
메서드명searchMultiCityTrip() (3구간)
소스 위치FlightSearchProxyController.kt:187-246

3.5 다구간 검색 (Multi-City) - 4구간

항목
HTTP MethodGET
URL Pattern/internals/proxy/flights/search/.../date1/.../date2/.../date3/.../date4
메서드명searchMultiCityTrip() (4구간)
소스 위치FlightSearchProxyController.kt:248-319

참고: 다구간 검색은 최대 4구간까지 지원


4. Request/Response 구조

4.1 공통 Request 모델

LocationType (Enum)

// support/enums/LocationType.kt:3-6
enum class LocationType(val shorter: String) {
    CITY(shorter = "c"),    // 도시 코드
    AIRPORT(shorter = "a"); // 공항 코드
}

CabinType (Enum)

// 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 (Enum)

// support/enums/FlightGroupCriteria.kt:6-11
enum class FlightGroupCriteria {
    NONE,                        // 그룹핑 없음
    SCHEDULE,                    // 스케줄 기준 (기본값)
    SCHEDULE_WITH_CLASS,         // 스케줄 + 예약 클래스
    SCHEDULE_WITH_FREE_BAGGAGE,  // 스케줄 + 무료 수하물
}

SearchTripDirectionType (Enum)

// support/enums/SearchTripDirectionType.kt:3-6
enum class SearchTripDirectionType {
    OUTBOUND,  // 가는편 검색
    INBOUND,   // 오는편 검색
}

4.2 Response 구조

InternalProxyFlightItemView

소스: interfaces/response/ProxyFlightSearchView.kt:10-48

data class InternalProxyFlightItemView(
    val listKey: UUID,                              // 검색 결과 캐시 키
    val score: BigDecimal?,                         // 추천 점수
    val tags: List<TagType>?,                       // 태그 목록
    val validatingCarrier: String,                  // 발권 항공사
    val schedules: List<InternalProxyScheduleView>, // 여정 정보
    val fares: List<InternalProxyFlightFareView>,   // 운임 정보
    val tripDirectionType: TripDirectionType?,      // 여정 방향 (MIX용)
    val tripType: TripType,                         // 여정 유형
)

InternalProxyScheduleView

소스: interfaces/response/ProxyFlightSearchView.kt:50-87

data class InternalProxyScheduleView(
    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: InternalProxyFreeBaggageView?, // 무료 수하물
    val segments: List<InternalProxySegmentView>, // 세그먼트
)

InternalProxyFlightFareView

소스: interfaces/response/ProxyFlightSearchView.kt:143-180

data class InternalProxyFlightFareView(
    val key: String,                                        // 상세 키
    val id: String,                                         // 운임 ID
    val funnel: Funnel,                                     // 판매 퍼널
    val avail: Int,                                         // 잔여 좌석
    val passengerFares: List<InternalProxyFlightPassengerFareView>, // 승객별 운임
    val adultAirPrice: Long,                                // 성인 항공료
    val adultTaxPrice: Long,                                // 성인 세금
    val adultTotalPrice: Long,                              // 성인 총액
    val promotionPrincipleId: Long?,                        // 프로모션 정책 ID
    val cardPromotionName: String?,                         // 카드 프로모션명
    val representative: Boolean?,                           // 대표 카드 여부
    val tags: List<String>,                                 // 태그
    val lowestAdultTotalPrice: Long?,                       // 최저 성인 총액 (MultiTicket용)
    val lowestTotalPrice: Long?,                            // 최저 총액 (MultiTicket용)
    val identityType: IdentityType,                         // 신분 유형
)

5. 비즈니스 로직 흐름

5.1 표준 검색 흐름 (searchFlights)

flowchart TD
    A[Controller 진입] --> B[SearchInfo 생성]
    B --> C[StandardFlightSearchUseCase.searchFlights]
    C --> D[SearchInfo.validate 검증]
    D --> E[FlightSearchService.searchFlights]
    E --> F[AdapterClient 호출<br/>GDS/공급사 검색]
    F --> G[FareItinerary 필터링]
    G --> H[FlightItem 생성]
    H --> I[FlightGroupCriteria별 그룹핑]
    I --> J[최적 운임 선택<br/>pickOptimalFlight]
    J --> K{useRecommendation?}
    K -->|Yes| L[추천 점수 적용]
    K -->|No| M[InternalProxyFlightItemView 변환]
    L --> M
    M --> N[ResponseEntity 반환]

상세 코드 흐름

// FlightSearchProxyController.kt:321-357
private fun searchFlights(...): ResponseEntity<List<InternalProxyFlightItemView>> {
    // 1. SearchInfo 생성
    val searchInfo = SearchInfo(
        adult = adult,
        child = child,
        infant = infant,
        freeBaggageOnly = freeBaggageOnly,
        cabins = cabins,
        originDestinationLocationInfos = originDestinationLocationInfo.toList(),
        useCache = useCache,
    )
 
    // 2. UseCase 호출
    val flightItems = standardFlightSearchUseCase.searchFlights(
        searchInfo = searchInfo,
        onlyDirect = false,
        useRecommendation = useRecommendation,
        flightGroupCriteria = flightGroupCriteria,
    )
 
    // 3. View 변환 및 반환
    return ResponseEntity.ok(
        flightItems.map {
            InternalProxyFlightItemView.of(
                flightItem = it,
                tripType = searchInfo.getTripType(),
            )
        }
    )
}

5.2 Split 검색 흐름 (searchSplitFlights)

flowchart TD
    A[Controller 진입<br/>useMultiTicket=true] --> B[SearchInfo 생성<br/>MultiTicket 포함]
    B --> C[listKey 생성/사용]
    C --> D[SplitFlightSearchUseCase.searchFlights]
    D --> E{cachedListKey 존재?}
    E -->|Yes| F[캐시에서 FlightItems 조회]
    E -->|No| G[FlightSearchService 검색]
    F --> H[splitMultiTicketSearchFlights]
    G --> H
    H --> I{searchTripDirectionType?}
    I -->|INBOUND| J[앵커 기반 오는편 필터링]
    I -->|OUTBOUND| K[가는편만 반환]
    I -->|null| L[전체 반환<br/>가는편+오는편]
    J --> M[그룹핑 및 최적 운임 선택]
    K --> M
    L --> M
    M --> N[cached-search-key 헤더 설정]
    N --> O[ResponseEntity 반환]

상세 코드 흐름

// FlightSearchProxyController.kt:359-408
private fun searchSplitFlights(...): ResponseEntity<List<InternalProxyFlightItemView>> {
    val searchInfo = SearchInfo(
        ...
        multiTicket = MultiTicket(
            searchTripDirectionType = searchTripDirectionType,
            detailKey = detailKey,
            promotionPrincipleId = promotionPrincipleId,
            cachedListKey = cachedListKey,
        ),
        ...
    )
 
    val listKey = cachedListKey ?: CacheKeyGenerator.getFlightSearchCacheKey()
    val flightItems = splitFlightSearchUseCase.searchFlights(
        listKey = listKey,
        searchInfo = searchInfo,
        flightGroupCriteria = flightGroupCriteria,
    )
 
    return ResponseEntity(
        flightItems.map { ... },
        HttpHeaders().apply { set(Constants.CACHED_SEARCH_KEY, listKey.toString()) },
        HttpStatus.OK,
    )
}

6. useMultiTicket 옵션 분석 (Split 검색)

6.1 개요

MultiTicket(Split) 검색은 왕복 항공권을 가는편과 오는편을 별도로 검색하여 사용자에게 더 많은 조합 선택지를 제공하는 기능입니다.

6.2 검색 단계

sequenceDiagram
    participant Client
    participant Controller
    participant SplitUseCase
    participant FlightService
    participant Cache

    Note over Client,Cache: 1단계: 전체/가는편 검색
    Client->>Controller: searchRoundTrip(useMultiTicket=true)
    Controller->>SplitUseCase: searchFlights(searchTripDirectionType=null/OUTBOUND)
    SplitUseCase->>FlightService: searchFlights(useMultiTicket=true)
    FlightService-->>SplitUseCase: MIX + RoundTrip FlightItems
    SplitUseCase->>Cache: save(listKey, flightItems)
    SplitUseCase-->>Controller: 가는편 목록 (lowestTotalPrice 포함)
    Controller-->>Client: Response + cached-search-key 헤더

    Note over Client,Cache: 2단계: 오는편 검색 (가는편 선택 후)
    Client->>Controller: searchRoundTrip(useMultiTicket=true, searchTripDirectionType=INBOUND, detailKey=선택한가는편키)
    Controller->>SplitUseCase: searchFlights(searchTripDirectionType=INBOUND)
    SplitUseCase->>Cache: getFlightItems(cachedListKey)
    Cache-->>SplitUseCase: 캐시된 FlightItems
    SplitUseCase->>SplitUseCase: filterAnchorMatchedFlightItems
    SplitUseCase-->>Controller: 오는편 목록
    Controller-->>Client: Response

6.3 MIX vs RoundTrip 항공편

// SplitFlightSearchUseCase.kt:58-114
fun splitMultiTicketSearchFlights(...): List<FlightItem> {
    // MIX 항공편과 왕복 항공편 분리
    val (mixFlightItems, roundTripFlightItems) = flightItems.partition { it.isMix }
 
    // MIX 항공편을 가는편/오는편으로 분리
    val (mixOutboundFlightItems, mixInboundFlightItems) = mixFlightItems
        .map { flightItem ->
            // MIX 항공권은 성인 기본 PTC만 허용
            flightItem.copy(fares = flightItem.fares.filter { it.identityType.isDefault })
        }
        .partition { it.tripDirectionType == TripDirectionType.OUTBOUND }
 
    return when (searchTripDirectionType) {
        SearchTripDirectionType.INBOUND -> {
            // 앵커(선택된 가는편) 기반 오는편 필터링
            ...
        }
        else -> {
            // 전체/가는편 검색
            ...
        }
    }
}

6.4 최저가 계산 (가는편용)

// SplitFlightSearchUseCase.kt:123-147
private fun calculateLowestFare(
    outboundFlightItems: List<FlightItem>,
    inboundFlightItems: List<FlightItem>,
) {
    // 오는편의 카드프로모션별 최저가 계산
    val inboundLowestFareMap = inboundFlightItems
        .flatMap { it.fares }
        .groupBy { it.cardPromotionId }
        .mapValues { (_, fares) -> fares.minBy { it.adultFare.total } }
 
    // 가는편 운임에 예상 최저 총액 설정
    outboundFlightItems.forEach { outboundFlightItem ->
        outboundFlightItem.fares.forEach { outboundFare ->
            val inboundLowestFares = inboundLowestFareMap.getOrDefault(
                outboundFare.cardPromotionId,
                inboundLowestFareMap.getValue(null)
            )
            outboundFare.lowestAdultTotalPrice =
                outboundFare.adultFare.total + inboundLowestFares.adultFare.total
            outboundFare.lowestTotalPrice =
                outboundFare.totalPrice + inboundLowestFares.totalPrice
        }
    }
}

6.5 연결 시간 검증

// domain/FlightItem.kt:801-806
fun isValidConnectionTime(
    outbound: FlightItem,
    inbound: FlightItem,
): Boolean {
    // 가는편 도착시간과 오는편 출발시간 차이가 3시간 이상이어야 함
    return outbound.inboundArrivalAt differ inbound.outboundDepartureAt >= Duration.ofHours(Constants.FLIGHT_INTERVAL_HOURS)
}

소스: support/Constants.kt:14 - FLIGHT_INTERVAL_HOURS = 3L


7. 에러 처리 및 예외 상황

7.1 SearchInfo 유효성 검사

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

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

7.2 주요 예외 케이스

예외 상황MessageKey설명소스 위치
잘못된 캐시 키INVALID_CACHE_KEY존재하지 않는 캐시 키MessageKey.kt:5
잘못된 파라미터INVALID_PARAMETER유효하지 않은 요청 파라미터MessageKey.kt:6
승객 수 오류INVALID_PASSENGERS승객 수 규칙 위반MessageKey.kt:7
날짜 오류INVALID_DATES출발일 > 귀국일MessageKey.kt:13
예약 불가 날짜INVALID_BOOKABLE_DATE예약 가능 범위 벗어남MessageKey.kt:14
잘못된 여정INVALID_ITINERARY출발지=도착지, 국내선 등MessageKey.kt:15
제한 국가INVALID_ITINERARY_RESTRICTED_COUNTRY검색 제한 국가 (SY, IR, UA, CU)MessageKey.kt:16
검색 실패SEARCH_FAILED공급사 검색 실패MessageKey.kt:22

7.3 MultiTicket 검색 검증

// support/model/SearchInfo.kt:86-98
private fun checkMultiTicketSearch() {
    multiTicket?.let {
        // INBOUND 검색 시 detailKey와 cachedListKey 필수
        if (it.searchTripDirectionType == SearchTripDirectionType.INBOUND
            && (it.detailKey == null || it.cachedListKey == null)) {
            throw MethodArgumentInvalidException(MessageKey.INVALID_PARAMETER)
        }
 
        // OUTBOUND/전체 검색 시 detailKey 불허
        if (it.detailKey != null && it.searchTripDirectionType != SearchTripDirectionType.INBOUND) {
            throw MethodArgumentInvalidException(MessageKey.INVALID_PARAMETER)
        }
    }
}

7.4 제한 국가

// support/model/OriginDestinationLocationInfo.kt:45-47
fun isRestrictedCountry(): Boolean {
    return setOf("SY", "IR", "UA", "CU").contains(countryCode)
}
국가 코드국가명제한 사유
SY시리아제재 국가
IR이란제재 국가
UA우크라이나분쟁 지역
CU쿠바제재 국가

8. 관련 도메인 모델 분석

8.1 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,              // MultiTicket 설정
    val useCache: Boolean,                             // 캐시 사용
) {
    val key: String  // 검색 조건 식별 키
 
    fun validate(bookableDateRange: LongRange)  // 유효성 검사
    fun getTripType(): TripType                 // 여정 유형 반환
}

8.2 MultiTicket

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

data class MultiTicket(
    val searchTripDirectionType: SearchTripDirectionType?,  // 검색 방향
    val detailKey: String?,                                  // 앵커 항공편 키
    val promotionPrincipleId: Long?,                        // 프로모션 정책
    val cachedListKey: UUID?,                               // 캐시 키
)

8.3 OriginDestinationLocationInfo

소스: support/model/OriginDestinationLocationInfo.kt:11-30

data class OriginDestinationLocationInfo(
    val origin: LocationInfo,       // 출발지 정보
    val destination: LocationInfo,  // 도착지 정보
    val date: LocalDate,            // 출발일
) {
    fun isDomesticRoute(): Boolean       // 국내선 여부
    fun isRestrictedRoute(): Boolean     // 제한 국가 여부
    fun isSameOriginDestination(): Boolean // 출발지=도착지 여부
}

8.4 LocationInfo (Sealed Class)

소스: support/model/OriginDestinationLocationInfo.kt:32-93

sealed class LocationInfo(val type: LocationType) {
    abstract val iata: String           // IATA 코드
    abstract val airportIatas: Set<String>  // 공항 IATA 목록
    abstract val cityIata: String       // 도시 IATA 코드
    abstract val countryCode: String    // 국가 코드
    abstract val zoneId: ZoneId         // 타임존
 
    data class City(...) : LocationInfo(LocationType.CITY)
    data class Airport(...) : LocationInfo(LocationType.AIRPORT)
}

8.5 FlightItem

소스: domain/FlightItem.kt:21-329

data class FlightItem(
    val listKey: UUID,                      // 검색 리스트 키
    val supplier: String,                   // 공급사
    val scheduleKey: String,                // 스케줄 키
    val validatingCarrier: String,          // 발권 항공사
    val schedules: List<Schedule>,          // 여정 목록
    val fares: List<FlightFare>,            // 운임 목록
    var score: BigDecimal?,                 // 추천 점수
    var tags: List<RecommendationTag>?,     // 추천 태그
    val tripDirectionType: TripDirectionType?, // 여정 방향 (MIX용)
    val prepayment: Boolean?,               // 선결제 여부
    val groupKey: String = "",              // 그룹 키
) {
    val isMix: Boolean                      // MIX 항공편 여부
    val lowestFare: FlightFare              // 최저가 운임
    val isDirect: Boolean                   // 직항 여부
    val maxStop: Int                        // 최대 경유 횟수
}

8.6 TripType

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

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

8.7 TripDirectionType

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

enum class TripDirectionType {
    OUTBOUND,  // 가는편 (MIX 항공편용)
    INBOUND,   // 오는편 (MIX 항공편용)
}

9. 처리 흐름 다이어그램

9.1 전체 시스템 흐름

flowchart TB
    subgraph Controller["FlightSearchProxyController"]
        A1[searchOneWayTrip]
        A2[searchRoundTrip]
        A3[searchMultiCityTrip]
    end

    subgraph UseCase["UseCase Layer"]
        B1[StandardFlightSearchUseCase]
        B2[SplitFlightSearchUseCase]
    end

    subgraph Service["Service Layer"]
        C1[FlightSearchService]
        C2[LocationService]
        C3[AirConsoleService]
    end

    subgraph Infrastructure["Infrastructure"]
        D1[AdapterClient<br/>GDS/공급사]
        D2[PricingClient<br/>가격정책]
        D3[FlightItemRepository<br/>캐시]
    end

    A1 --> B1
    A2 -->|useMultiTicket=false| B1
    A2 -->|useMultiTicket=true| B2
    A3 --> B1

    B1 --> C1
    B2 --> C1

    Controller --> C2

    C1 --> C3
    C1 --> D1
    C1 --> D2
    C1 --> D3
    B2 --> D3

9.2 Response 헤더 처리

검색 유형cached-search-key 헤더
표준 검색 (편도/왕복/다구간)없음
Split 검색 (useMultiTicket=true)항상 포함
// FlightSearchProxyController.kt:396-406
return ResponseEntity(
    flightItems.map { ... },
    HttpHeaders().apply {
        set(Constants.CACHED_SEARCH_KEY, listKey.toString())
    },
    HttpStatus.OK,
)

10. 참고 사항

10.1 캐시 키 생성

// support/cache/CacheKeyGenerator.kt:5-8
object CacheKeyGenerator {
    fun getFlightSearchCacheKey(): UUID {
        return UUID.randomUUID()
    }
}

10.2 관련 문서


변경 이력

버전날짜작성자변경 내용
1.02025-12-15Claude최초 작성

관련 문서