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-channel | TRIPLE_SALES_CHANNEL_HEADER | 필수 | 판매 채널 식별 |
x-triple-sales-funnel | TRIPLE_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 Method | GET |
| URL Path | /internals/proxy/flights/meta/search/{originType}:{origin}-{destinationType}:{destination}/{outboundDate} |
| 메서드 | searchOneWayTrip() |
| Response Status | 200 OK |
Path Variables
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
originType | LocationType | Y | 출발지 타입 (CITY / AIRPORT) |
origin | String | Y | 출발지 IATA 코드 |
destinationType | LocationType | Y | 도착지 타입 |
destination | String | Y | 도착지 IATA 코드 |
outboundDate | LocalDate | Y | 출발일 (ISO 형식: yyyy-MM-dd) |
Query Parameters
| 파라미터 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
cabins | Set<CabinType> | Y | - | 좌석 등급 |
adult | Int | Y | - | 성인 승객 수 |
child | Int | N | 0 | 소아 승객 수 |
infant | Int | N | 0 | 유아 승객 수 |
freeBaggageOnly | Boolean | N | false | 무료 수하물 포함 항공편만 조회 |
onlyDirect | Boolean | N | false | 직항편만 조회 |
useCache | Boolean | N | false | 상세 조회용 캐시 사용 여부 |
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 Method | GET |
| URL Path | /internals/proxy/flights/meta/search/{originType}:{origin}-{destinationType}:{destination}/{outboundDate}/{inboundDate} |
| 메서드 | searchRoundTrip() |
| Response Status | 200 OK |
추가 Path Variables
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
inboundDate | LocalDate | Y | 귀국일 (ISO 형식) |
추가 Query Parameters
| 파라미터 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
useMultiTicket | Boolean | N | false | 멀티 티켓(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
| 파라미터 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
useRecommendation | Boolean | N | true | 추천 점수 적용 여부 [확인 필요: 실제 사용되지 않음] |
[확인 필요]:
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
| Funnel | FareDecisionStrategy |
|---|---|
SKYSCANNER | LOWEST_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
| Funnel | FlightGroupCriteria |
|---|---|
NAVER, NAVER_SMART, NAVER_GOLD | SCHEDULE_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=false | StandardFlightSearchUseCase | 왕복(RT) 티켓만 |
useMultiTicket=true | CombineFlightSearchUseCase | RT + 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_COUNTRY | SY, 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 |
| 추천 사용 여부 | false | FlightMetaSearchProxyController.kt:317 |
| 대표 카드 프로모션만 | true | FlightMetaSearchProxyController.kt:320 |
| 캐시 TTL | 1일 | AirportService.kt:16, AirlineService.kt:14 |
12.2 Deprecated 필드
| 필드 | 위치 | 사유 |
|---|---|---|
scheduleKey | ProxyFlightMetaSearchView.kt:17-18 | external api에서 미사용 |
문서 생성일: 2024-12-15 소스 기준: air-intl-search 모듈