FlightSearchController - 국제 항공 검색 API

1. API 개요

1.1 목적

국제 항공 노선에 대한 항공편 검색 기능을 제공하는 REST API 컨트롤러입니다. 편도, 왕복, 다구간(최대 4구간) 검색을 지원하며, 비동기 폴링 방식으로 검색 결과를 제공합니다.

1.2 기본 정보

항목
패키지com.triple.air.intl.search.interfaces.controller
클래스FlightSearchController
Base Path/flights
소스 위치nol-triple/air-intl-search/src/main/kotlin/.../interfaces/controller/FlightSearchController.kt

1.3 의존성

서비스역할소스 위치
FlightSearchService항공편 검색 핵심 로직application/FlightSearchService.kt
StandardFlightSearchUseCase표준 검색 유스케이스application/usecase/StandardFlightSearchUseCase.kt
AirlineService항공사 정보 조회application/AirlineService.kt
AirportService공항 정보 조회application/AirportService.kt
LocationService위치(도시/공항) 정보 조회application/LocationService.kt:9-32
BookableDateService예약 가능 일자 범위 조회application/BookableDateService.kt:7-13
BillingService결제 관련 서비스application/BillingService.kt

2. 엔드포인트 상세

2.1 편도 검색 (One-Way Trip)

기본 정보

항목
HTTP MethodGET
URL Pattern/flights/search/{originType}:{origin}-{destinationType}:{destination}/{outboundDate}
메서드searchOneWayTripFlightsListKey()
소스 위치FlightSearchController.kt:33-62
응답 코드200 OK

URL 예시

GET /flights/search/CITY:SEL-AIRPORT:NRT/2025-03-15?cabins=ECONOMY&adult=2

Path Variables

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

Query Parameters

파라미터타입필수기본값설명
cabinsSet<CabinType>Y-좌석 등급
adultIntY-성인 승객 수
childIntN0아동 승객 수
infantIntN0유아 승객 수
freeBaggageOnlyBooleanNfalse무료 수하물 포함 운임만 검색
useCacheBooleanNtrue캐시 사용 여부

2.2 왕복 검색 (Round Trip)

기본 정보

항목
HTTP MethodGET
URL Pattern/flights/search/{originType}:{origin}-{destinationType}:{destination}/{outboundDate}/{inboundDate}
메서드searchRoundTripFlightsListKey()
소스 위치FlightSearchController.kt:64-100
응답 코드200 OK

URL 예시

GET /flights/search/CITY:SEL-CITY:TYO/2025-03-15/2025-03-20?cabins=ECONOMY&adult=2

추가 Path Variables

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

왕복 검색 시 귀국편의 출발지/도착지가 자동으로 가는편의 역순으로 설정됩니다. (FlightSearchController.kt:94-98)


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

다구간 검색은 2구간, 3구간, 4구간을 지원합니다.

2구간 검색

항목
HTTP MethodGET
URL Pattern/flights/search/{originType1}:{origin1}-{destinationType1}:{destination1}/{date1}/{originType2}:{origin2}-{destinationType2}:{destination2}/{date2}
메서드searchMultiCityTripFlightsListKey() (2구간)
소스 위치FlightSearchController.kt:102-146

3구간 검색

항목
URL Pattern/flights/search/.../date1/.../date2/.../date3
소스 위치FlightSearchController.kt:148-204

4구간 검색

항목
URL Pattern/flights/search/.../date1/.../date2/.../date3/.../date4
소스 위치FlightSearchController.kt:206-274

2.4 검색 결과 조회 (Polling)

기본 정보

항목
HTTP MethodPOST
URL Pattern/flights/{listKey}
메서드getFlights()
소스 위치FlightSearchController.kt:306-333

Path Variables

파라미터타입필수설명
listKeyUUIDY검색 초기화 시 반환된 키

Request Body

// FlightSearchRequest.kt:7-25
data class FlightSearchRequest(
    val sort: SortType? = null,
    val filter: FilterCriteria? = null
) : PageRequest(page = 1, size = 20)
 
data class FilterCriteria(
    val byAirline: List<String>? = null,
    val byStop: List<LayoverFilterType>? = null,
    val byStopPoint: List<String>? = null,
    val byExcludeOutboundTimeSlot: TimeSlot? = null,
    val byExcludeInboundTimeSlot: TimeSlot? = null,
    val byFlightTimes: List<String?>? = null,
    val byExcludeCodeShare: Boolean = false,
    val byCardPromotion: List<String>? = null,
)

응답 상태 코드

HTTP StatusPollingStatus설명
202 AcceptedPENDING검색 진행 중
200 OKCOMPLETE / NO_DATA검색 완료
500 Internal Server ErrorERROR검색 실패 (예외 발생)

2.5 검색 상태 확인

기본 정보

항목
HTTP MethodGET
URL Pattern/flights/{listKey}/status
메서드getCacheKeyStatus()
소스 위치FlightSearchController.kt:335-342
응답 코드200 OK

Response

{
    "status": "OK"
}

3. Request/Response 구조

3.1 검색 초기화 응답

{
    "key": "550e8400-e29b-41d4-a716-446655440000"
}

소스: FlightSearchController.kt:297-303

3.2 검색 결과 응답 (FlightSearchPageView)

// FlightSearchView.kt:20-99
data class FlightSearchPageView(
    val contents: List<FlightItemView>,
    val page: PageInfo,
    val status: PollingStatus,
    val filterData: FilterDataView? = null,
    val sortData: SortDataView? = null,
    val lowestPrice: Long? = null,
    val tripType: TripType? = null,
)

FlightItemView 구조

// FlightSearchView.kt:102-155
data class FlightItemView(
    val listKey: UUID,
    val detailKey: String,
    val avail: Int,
    val id: String,
    val tag: FlightTagView?,
    val adultPrice: Long,
    val totalPrice: Long,
    val schedules: List<ScheduleSummaryView>,
    val badges: List<BadgeView>,
    val fares: List<FlightFareView>,
)

PageInfo 구조

// PageView.kt:10-31
data class PageInfo(
    val currentPage: Int,
    val pageSize: Int,
    val totalCount: Int
)

3.3 에러 응답 (ErrorView)

// ErrorView.kt:3-27
data class ErrorView(
    val status: Int,
    val code: String,
    val title: String?,
    val message: String?,
    val messageArguments: List<*>?,
)

4. Enum 타입 정의

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 TripType

// TripType.kt:3-18
enum class TripType {
    ONE_WAY,
    ROUND_TRIP,
    MULTI_CITY;
}

4.4 PollingStatus

// PollingStatus.kt:3-8
enum class PollingStatus {
    PENDING,
    ERROR,
    COMPLETE,
    NO_DATA
}

4.5 SortType

// SortType.kt:8-32
enum class SortType {
    RECOMMENDATION,           // 추천순
    DIRECT,                   // 직항 우선
    PRICE,                    // 가격순
    FLIGHT_TIME,              // 비행시간순
    OUTBOUND_DEPARTURE_TIME,  // 출발편 출발시간순
    OUTBOUND_ARRIVAL_TIME,    // 출발편 도착시간순
    INBOUND_DEPARTURE_TIME,   // 귀국편 출발시간순
    INBOUND_ARRIVAL_TIME      // 귀국편 도착시간순
}

4.6 LayoverFilterType

// LayoverType.kt:7-28
enum class LayoverFilterType(val order: Int) {
    DIRECT(order = 1),           // 직항
    ONE(order = 2),              // 경유 1회
    MORE_THAN_TWO(order = 3);    // 경유 2회 이상
}

4.7 TimeSlot

// TimeSlot.kt:5-10
enum class TimeSlot(val start: LocalTime, val end: LocalTime) {
    MORNING(LocalTime.of(6, 0), LocalTime.NOON),        // 06:00-12:00
    AFTER_NOON(LocalTime.NOON, LocalTime.of(18, 0)),    // 12:00-18:00
    NIGHT(LocalTime.of(18, 0), LocalTime.MIDNIGHT),     // 18:00-00:00
    DAWN(LocalTime.MIDNIGHT, LocalTime.of(6, 0)),       // 00:00-06:00
}

5. 비즈니스 로직 흐름

5.1 검색 초기화 플로우

sequenceDiagram
    participant Client
    participant Controller as FlightSearchController
    participant LocationService
    participant UseCase as StandardFlightSearchUseCase
    participant FlightSearchService
    participant Repository as FlightSearchRepository

    Client->>Controller: GET /flights/search/...
    Controller->>LocationService: getLocationInfo(iata, type)
    LocationService-->>Controller: LocationInfo

    Controller->>Controller: SearchInfo 생성
    Controller->>Controller: searchInfo.validate()

    Controller->>UseCase: init(searchInfo, criteria, useRecommendation)
    UseCase->>UseCase: CacheKeyGenerator.getFlightSearchCacheKey()
    UseCase->>Repository: savePolling(listKey, PENDING)

    UseCase->>UseCase: CoroutineScope(Dispatchers.IO)
    Note over UseCase: 비동기 검색 시작

    UseCase-->>Controller: listKey (UUID)
    Controller-->>Client: {"key": "uuid"}

    Note over UseCase,FlightSearchService: 백그라운드에서 검색 진행
    UseCase->>FlightSearchService: searchFlights(...)
    FlightSearchService-->>UseCase: List<FlightItem>
    UseCase->>Repository: savePolling(listKey, COMPLETE)

소스: StandardFlightSearchUseCase.kt:84-101

5.2 검색 결과 조회 플로우

sequenceDiagram
    participant Client
    participant Controller as FlightSearchController
    participant FlightSearchService
    participant AirlineService
    participant AirportService

    Client->>Controller: POST /flights/{listKey}
    Controller->>FlightSearchService: getFlightSearch(listKey)
    FlightSearchService-->>Controller: FlightSearch

    alt status == PENDING
        Controller-->>Client: 202 Accepted (FlightSearchPageView.ofPending)
    else status == ERROR
        Controller->>Controller: throw InternationalSearchException
    else status == COMPLETE/NO_DATA
        Controller->>AirlineService: getAirlinesMap(airlineSet)
        Controller->>AirportService: getAirportsMap(airportSet)
        Controller-->>Client: 200 OK (FlightSearchPageView.ofComplete)
    end

소스: FlightSearchController.kt:306-333

5.3 검색 흐름 상세

flowchart TD
    A[검색 요청] --> B[LocationService.getLocationInfo]
    B --> C[SearchInfo 생성]
    C --> D{validate}

    D --> |실패| E[MethodArgumentInvalidException]
    D --> |성공| F[StandardFlightSearchUseCase.init]

    F --> G[UUID 생성 및 PENDING 저장]
    G --> H[비동기 검색 시작]
    H --> I[FlightSearchService.searchFlights]

    I --> J[AirConsoleService.findSearchCondition]
    J --> K[AdapterClient.getFareItineraries]
    K --> L[스케줄 필터링]
    L --> M[PricingClient.getActivePricingPrinciples]
    M --> N[FlightItem 생성]
    N --> O{useRecommendation?}

    O --> |Yes| P[RecommendationService.findScoreMap]
    O --> |No| Q[결과 저장]
    P --> Q

    Q --> R[FlightSearch.complete 저장]

6. 유효성 검사

6.1 SearchInfo.validate()

SearchInfo.kt:46-52 에서 정의된 검증 로직:

fun validate(bookableDateRange: LongRange) {
    checkBookableDate(bookableDateRange = bookableDateRange)
    checkSearchableDates()
    checkSearchablePassengers(adult = this.adult, child = this.child, infant = this.infant)
    checkSearchableItinerary()
    checkMultiTicketSearch()
}

6.2 예약 가능 날짜 검증

BookableDateValidator.kt:9-15:

검증 항목조건예외
예약 가능 범위오늘 기준 설정된 일수 범위 내INVALID_BOOKABLE_DATE

6.3 날짜 순서 검증

SearchInfo.kt:59-67:

검증 항목조건예외
여정 날짜 순서이전 여정 날짜 다음 여정 날짜INVALID_DATES

6.4 승객 수 검증

SearchValidator.kt:6-30:

검증 항목조건예외
성인 최소 인원adult >= 1INVALID_PASSENGERS
총 승객 수adult + child + infant 9INVALID_PASSENGERS_COUNT
아동 비율child adult * 3INVALID_PASSENGERS_CHILD
유아 인원infant adultINVALID_PASSENGERS_INFANT

6.5 여정 검증

SearchInfo.kt:69-84:

검증 항목조건예외
출도착 동일출발지와 도착지가 다름INVALID_ITINERARY
국내선 제외출발/도착 모두 국내가 아님INVALID_ITINERARY
제한 국가SY, IR, UA, CU 미포함INVALID_ITINERARY_RESTRICTED_COUNTRY

7. 에러 처리

7.1 예외 타입별 HTTP 응답

예외 클래스HTTP Status설명
MethodArgumentInvalidException400 Bad Request잘못된 파라미터
CacheKeyInvalidException410 Gone유효하지 않은 캐시 키
InternationalSearchException500 Internal Server Error검색 실패
EntityNotFoundException404 Not Found엔티티 미발견
AuthorizationFailedException401 Unauthorized인증 실패
SystemMaintenanceException550 (Custom)시스템 점검

소스: RestExceptionHandler.kt:22-226

7.2 MessageKey 정의

MessageKey.kt:3-36:

MessageKey설명
INVALID_CACHE_KEY유효하지 않은 캐시 키
INVALID_PARAMETER잘못된 파라미터
INVALID_PASSENGERS잘못된 승객 정보
INVALID_PASSENGERS_COUNT총 승객 수 초과 (9명)
INVALID_PASSENGERS_CHILD아동 비율 초과
INVALID_PASSENGERS_INFANT유아 수 초과
INVALID_DATES잘못된 날짜 순서
INVALID_BOOKABLE_DATE예약 불가 날짜
INVALID_ITINERARY잘못된 여정
INVALID_ITINERARY_RESTRICTED_COUNTRY제한 국가 포함
SEARCH_FAILED검색 실패

8. 도메인 모델

8.1 SearchInfo

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

8.2 OriginDestinationLocationInfo

OriginDestinationLocationInfo.kt:11-30:

data class OriginDestinationLocationInfo(
    val origin: LocationInfo,
    val destination: LocationInfo,
    val date: LocalDate,
)

8.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)
}

8.4 FlightSearch

FlightSearch.kt:10-80:

data class FlightSearch(
    val status: PollingStatus,
    val messageKey: MessageKey? = null,
    val message: String? = null,
    val flightItems: List<FlightItem> = emptyList(),
    val defaultSortType: SortType = SortType.PRICE,
    val tripType: TripType? = null,
    val airlineSet: Set<String> = emptySet(),
    val airportSet: Set<String> = emptySet(),
    val searchInfoKey: String? = null,
)

8.5 FlightItem

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?,
    val prepayment: Boolean?,
    val groupKey: String = "",
)

8.6 Schedule

FlightItem.kt:598-676:

data class Schedule(
    val key: String = "",
    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: FreeBaggage?,
    val segments: List<Segment>,
)

8.7 FlightFare

FlightItem.kt:331-480:

data class FlightFare(
    val id: String,
    val lookUpKey: String = "",
    val funnel: Funnel,
    val avail: Int,
    val detailKey: String,
    val passengerFares: List<PassengerFare>,
    val representative: Boolean? = null,
    val naverCardType: String? = null,
    val cardPromotionName: String? = null,
    val cardPromotionId: Long? = null,
    val promotionPrincipleId: Long? = null,
    val tags: List<String> = emptyList(),
    var lowestAdultTotalPrice: Long? = null,
    var lowestTotalPrice: Long? = null,
)

9. 관련 문서


10. 변경 이력

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

11. 소스 참조 인덱스

파일주요 내용
FlightSearchController.kt컨트롤러 엔드포인트 정의 (:33-342)
StandardFlightSearchUseCase.kt검색 유스케이스 (:21-102)
FlightSearchService.kt검색 서비스 로직 (:34-419)
FlightSearchRequest.kt요청 DTO (:7-25)
FlightSearchView.kt응답 View 모델 (:20-738)
SearchInfo.kt검색 정보 모델 (:15-113)
OriginDestinationLocationInfo.kt출도착 정보 모델 (:11-93)
FlightItem.kt항공편 도메인 모델 (:21-837)
FlightSearch.kt검색 결과 도메인 (:10-80)
Exceptions.kt예외 클래스 정의 (:1-81)
RestExceptionHandler.kt예외 핸들러 (:22-226)
MessageKey.kt메시지 키 열거형 (:3-36)
SearchValidator.kt승객 검증 (:6-30)
BookableDateValidator.kt날짜 검증 (:9-15)
LocationService.kt위치 서비스 (:9-32)
BookableDateService.kt예약일 서비스 (:7-13)

관련 문서