[네이버 항공권 크롤링하기] - Nest.js를 활용하여 네이버의 항공권 API를 사용(탈취)하기

카카오테크 부트캠프에서 풀스택 개발자 포지션으로 여행 어시스턴트 플랫폼을 개발 중에 고민했던 문제 중 하나인 항공권 API의 부재 문제를 해결한 기록입니다. 저희 팀이 만들고 있는 플랫폼명은 GOAT(Go And Travel)이며, 키워드 기반으로 사용자의 여행지를 추천해주고, 항공권과 호텔을 추천해주는 서비스입니다.

 

키워드 기반으로 사용자의 여행지를 추천해주는 것은 AI 팀에서 작업을 해주고 있었고 저는 AI가 추천해준 국가, 도시, 공항을 기반으로 항공권을 보여주는 서비스를 구현해야 했습니다. 무료로 사용가능한 항공권 API가 존재하지 않았기에, 저는 다양한 방법들을 시도해보려고 했습니다.

먼저, 사용자의 요청을 기반으로 Puppeteer로 크롤링을 해주는 방법이었으나 매우 비효율적이라고 판단했습니다. 크롤링을 위해 브라우저를 키고, 특정 페이지가 뜨기까지 기다리는 시간이 5초이상 소요되었기에 이는 좋은 방법이 아니라고 생각했습니다.

 

그러던 와중 하나의 블로그 글을 발견하게 되었습니다. Python의 Requests 라이브러리 기반으로, GraphQL로 구현된 네이버 항공권 API를 크롤링하는 글이었습니다. 저는 이 글을 읽고 JavaScript의 Axios 처럼 fetch 기반 라이브러리 로도 항공권 API를 사용할 수 있을 것 같다는 확신이 들었습니다. 

 

먼저, 개발자도구의 네트워크 탭을 열어둔 채로 아래와 같은 네이버 항공권 URL로 접속하게 되면 다음과 같이 GraphQL 요청이 매우 많이 오게 됨을 확인할 수 있습니다. (URL은 인천과 다낭의 2024년 9월 7일 편도 비행기를 확인하는 URL입니다.)

https://m-flight.naver.com/flights/international/ICN-DAD-20240907?adult=1&isDirect=true&fareType=Y

 

 

이 GraphQL을 통해서 다양한 정보를 미리 받아오는 걸로 추정되며, 항공권 요청을 위한 특별한 키들과 항공권 정보, 프로모션 정보, 호텔 정보들을 요청하고 응답 받는 걸로 추정됩니다. 

 

그럼 각각의 GraphQL을 좀 더 디테일하게 파헤쳐보도록 하겠습니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

먼저 요청시에 보내지는 데이터들입니다. 

Request URL은 다음과 같으며, Header의 referer를 통해 어떤 URL에서 요청을 보냈는지 확인하고, 원하는 데이터를 넘겨줄 수 있습니다. 

 

또한, 여행에 대한 더 자세한 정보를 요청하기 위해 Payload 을 보내어줍니다.

응답의 형태는 다음과 같으며 여기서 저희는 galileoKey와 travelBizKey를 통해 다시 한 번 요청을 보내야 합니다. (이들이 하는 역할에 대해서는 정확히 모르겠습니다... 😢)

 

 

이러한 사전 지식을 바탕으로 인천 다낭 사이의 2024-09-07의 이코노미 항공권 데이터를 얻는 코드는 다음과 같습니다. 

참고로, fetch 라이브러리는 `got`을 사용하였습니다. 

import got from "got";

const url = "https://airline-api.naver.com/graphql";
const headers = {
  "Content-Type": "application/json",
  Referer:
    "https://m-flight.naver.com/flights/international/ICN-DAD-20240907?adult=1&isDirect=true&fareType=Y",
};

const payload1 = {
  operationName: "getInternationalList",
  variables: {
    adult: 1,
    child: 0,
    infant: 0,
    where: "pc",
    isDirect: true,
    galileoFlag: true,
    travelBizFlag: true,
    fareType: "Y",
    itinerary: [
      {
        departureAirport: "ICN",
        arrivalAirport: "DAD",
        departureDate: "20240907",
      },
      {
        departureAirport: "DAD",
        arrivalAirport: "ICN",
        departureDate: "20240912",
      },
    ],
    stayLength: "",
    trip: "OW",
    galileoKey: "",
    travelBizKey: "",
  },
  query: `query getInternationalList(
    $trip: InternationalList_TripType!,
    $itinerary: [InternationalList_itinerary]!,
    $adult: Int = 1,
    $child: Int = 0,
    $infant: Int = 0,
    $fareType: InternationalList_CabinClass!,
    $where: InternationalList_DeviceType = pc,
    $isDirect: Boolean = false,
    $stayLength: String,
    $galileoKey: String,
    $galileoFlag: Boolean = true,
    $travelBizKey: String,
    $travelBizFlag: Boolean = true
) {
    internationalList(
        input: {
            trip: $trip,
            itinerary: $itinerary,
            person: { adult: $adult, child: $child, infant: $infant },
            fareType: $fareType,
            where: $where,
            isDirect: $isDirect,
            stayLength: $stayLength,
            galileoKey: $galileoKey,
            galileoFlag: $galileoFlag,
            travelBizKey: $travelBizKey,
            travelBizFlag: $travelBizFlag
        }
    ) {
        galileoKey
        galileoFlag
        travelBizKey
        travelBizFlag
        totalResCnt
        resCnt
        results {
            airlines
            airports
            fareTypes
            schedules
            fares
            errors
        }
    }
}`,
};

// First request to get the keys
const crawling = async () => {
  try {
    const response = await got.post(url, {
      json: payload1,
      headers: headers,
      responseType: "json",
    });

    const responseData = response.body;
    const travelBizKey = responseData.data.internationalList.travelBizKey;
    const galileoKey = responseData.data.internationalList.galileoKey;

    console.log("travel key:", travelBizKey);
    console.log("galileo key:", galileoKey);

    setTimeout(async () => {
      const payload2 = {
        operationName: "getInternationalList",
        variables: {
          adult: 1,
          child: 0,
          infant: 0,
          where: "pc",
          isDirect: true,
          galileoFlag: galileoKey !== "",
          travelBizFlag: travelBizKey !== "", // If empty, set to false
          fareType: "Y",
          itinerary: [
            {
              departureAirport: "ICN",
              arrivalAirport: "DAD",
              departureDate: "20240907",
            },
          ],
          stayLength: "",
          trip: "OW",
          galileoKey: galileoKey,
          travelBizKey: travelBizKey,
        },
        query: `query getInternationalList($trip: InternationalList_TripType!, $itinerary: [InternationalList_itinerary]!, $adult: Int = 1, $child: Int = 0, $infant: Int = 0, $fareType: InternationalList_CabinClass!, $where: InternationalList_DeviceType = pc, $isDirect: Boolean = false, $stayLength: String, $galileoKey: String, $galileoFlag: Boolean = true, $travelBizKey: String, $travelBizFlag: Boolean = true) {
                    internationalList(
                        input: {trip: $trip, itinerary: $itinerary, person: {adult: $adult, child: $child, infant: $infant}, fareType: $fareType, where: $where, isDirect: $isDirect, stayLength: $stayLength, galileoKey: $galileoKey, galileoFlag: $galileoFlag, travelBizKey: $travelBizKey, travelBizFlag: $travelBizFlag}
                    ) {
                        galileoKey
                        galileoFlag
                        travelBizKey
                        travelBizFlag
                        totalResCnt
                        resCnt
                        results {
                            airlines
                            airports
                            fareTypes
                            schedules
                            fares
                            errors
                        }
                    }
                }`,
      };

      // Second request to get the schedule details
      try {
        const response2 = await got.post(url, {
          json: payload2,
          headers: headers,
          responseType: "json",
        });

        const results = response2.body?.data?.internationalList?.results;
        const fares = results["fares"];
        const schedules = results["schedules"];
        const [departureSchedule, arrivalSchedule] = schedules;

        const crawled_data = {};

        for (const [key, value] of Object.entries(fares)) {
          // 가격
          const fare = value.fare["A01"][0];
  
          const pricePerAdult =
            parseInt(fare["Adult"]["NaverFare"], 10) ||
            parseInt(fare["Adult"]["Fare"], 10) +
              parseInt(fare["Adult"]["Tax"], 10) +
              parseInt(fare["Adult"]["QCharge"], 10);
          const pricePerChild =
            parseInt(fare["Child"]["NaverFare"], 10) ||
            parseInt(fare["Child"]["Fare"]) +
              parseInt(fare["Child"]["Tax"], 10) +
              parseInt(fare["Child"]["QCharge"], 10);
          const pricePerInfant =
            parseInt(fare["Infant"]["NaverFare"], 10) ||
            parseInt(fare["Infant"]["Fare"]) +
              parseInt(fare["Infant"]["Tax"], 10) +
              parseInt(fare["Infant"]["QCharge"], 10);

          const pricePerPerson = {
            adult: pricePerAdult,
            child: pricePerChild,
            infant: pricePerInfant,
          };

          const [departureSchKey, arrivalSchKey] = value.sch;
          const departureSch = departureSchedule[departureSchKey];
          const arrivalSch = arrivalSchedule[arrivalSchKey];

          crawled_data[key] = {
            id: key,
            departure: {
              departureAirport: departureSch.detail[0].sa, // 출발 공항
              departureDate: departureSch.detail[0].sdt, // 출발 날짜
              departureTime: departureSch.detail[0].sdt.slice(-4), // 출발 시각 (마지막 4자리)
              arrivalAirport: departureSch.detail[0].ea, // 도착 공항
              arrivalDate: departureSch.detail[0].edt, // 도착 날짜
              arrivalTime: departureSch.detail[0].edt.slice(-4), // 도착 시각 (마지막 4자리)
              airline: departureSch.detail[0].av, // 항공사
              journeyTime: {
                hours: parseInt(departureSch.journeyTime[0]),
                minutes: parseInt(departureSch.journeyTime[1]),
              },
              carbonEmission: departureSch.detail[0].carbonEmission,
            },

            arrival: {
              departureAirport: arrivalSch.detail[0].sa, // 출발 공항
              departureDate: arrivalSch.detail[0].sdt, // 출발 날짜
              departureTime: arrivalSch.detail[0].sdt.slice(-4), // 출발 시각 (마지막 4자리)
              arrivalAirport: arrivalSch.detail[0].ea, // 도착 공항
              arrivalDate: arrivalSch.detail[0].edt, // 도착 날짜
              arrivalTime: arrivalSch.detail[0].edt.slice(-4), // 도착 시각 (마지막 4자리)
              airline: arrivalSch.detail[0].av, // 항공사
              journeyTime: {
                hours: parseInt(arrivalSch.journeyTime[0]),
                minutes: parseInt(arrivalSch.journeyTime[1]),
              },
              carbonEmission: arrivalSch.detail[0].carbonEmission,
            },
            fare: pricePerPerson,
            link: fare?.ReserveParameter?.["#cdata-section"],
          };
        }

        // console.log(crawled_data);
      } catch (error) {
        console.error("Error fetching the second response:", error);
        if (error.response?.body?.errors) {
          error.response.body.errors.forEach((item) => {
            console.log(item.extensions);
            console.log(item.locations);
          });
        }
      }
    }, 5000);
  } catch (error) {
    console.error("Error fetching the first response:", error.response?.body);
    if (error.response?.body?.errors) {
      const err = error.response.body.errors[0];
      console.error("Error fetching the first response:", err.extensions);
      console.error(
        "This Got-based implementation is equivalent to the Axios code you provided. The code is structured to make two API calls to the Naver airline API, the first to retrieve necessary keys and the second to use those keys to fetch detailed flight schedules and fares."
      );
    }
  }
};
crawling();

 

 

 

이 코드를 기반으로 저는 React에서 다이렉트로 요청을 시도했었습니다. 하지만 CORS 오류가 발생하였고 네이버 서버를 직접 수정할 수 없기에 저는 우회적인 방법으로 서버에서 API 요청을 시도한 후, 얻은 정보를 보내주는 REST API를 Nest.js로 구현하였습니다.  

 

일단 왕복 항공권만을 요청한다고 가정하에 진행하였습니다. 

 

가장 먼저 아키텍처를 그렸습니다. 이런식으로 port-adapter 패턴을 활용하여 코드를 구현하였습니다.

 

`flight/adapter/in/web/flight.controller.ts` 파일을 만들고 다음과 같이 코드를 작성하였습니다.

import { Body, Controller, Post, Res } from '@nestjs/common';
import { FlightService } from '../../../application/service/flight.service';
import { Response } from 'express';
import { TravelInformation } from '@/flight/dto/TravelInformation.dto';

@Controller('flights')
export class FlightController {
  constructor(private readonly flightService: FlightService) {}

  @Post('round-trip')
  async getRoundTripFlight(
    @Body() body: TravelInformation,
    @Res() res: Response,
  ) {
    try {
      // console.log('body', body);
      // TODO: 입력 유효성 검사
      const response = await this.flightService.recommendRoundTripFlight(body);
      // TODO: 만약 항공편이 없을 경우 처리
      return res.json({ data: response });
    } catch (error) {
      console.error('error');
      return res.status(400).json({
        error: 'Error fetching the first response',
      });
    }
  }
}

 

항공권 추천을 위한 유즈케이스 인터페이스를 `flight/application/port/in/RecommendFlightUseCase`에 작성합니다.

 

import {
  FlightCurationError,
  FlightCurationSuccess,
} from '@/flight/dto/FlightCuration.dto';
import { TravelInformation } from '@/flight/dto/TravelInformation.dto';

export interface RecommendFlightUseCase {
  recommendRoundTripFlight(
    travelInformation: TravelInformation,
  ): Promise<FlightCurationSuccess | FlightCurationError>;
}

 

그런 후에 `flight/application/service/flight.service.ts` 파일을 만들고 다음과 같이 UseCase 인터페이스를 구현하였습니다.

import { TravelInformation } from '@/flight/dto/TravelInformation.dto';
import { FetchNaverRecommendFlightListAdapter } from '@/flight/adapter/out/naverFlight.adapter';
import { Injectable } from '@nestjs/common';
import { RecommendFlightUseCase } from '@/flight/application/port/in/RecommendFlightUseCase';

@Injectable()
export class FlightService implements RecommendFlightUseCase {
  async recommendRoundTripFlight(travelInformation: TravelInformation) {
    try {
      const fetchNaverRecommendFlightListAdapter: FetchNaverRecommendFlightListAdapter =
        new FetchNaverRecommendFlightListAdapter();
      const roundTrip =
        await fetchNaverRecommendFlightListAdapter.fetchNaverRecommendFlight(
          travelInformation,
        );
      return roundTrip;
    } catch (error) {
      console.error('getRoundTripFlight Service Error');
    }
  }
}

 

그 다음 필요한 데이터를 전송하기 위한 DTO를 작성해봅시다.

 

`flight/dto/TravelInformation.dto.ts`

interface PassengerInformation {
  count: {
    adult: number;
    child: number;
    infant: number;
  };
  fareType: 'Y' | 'C' | 'F'; // Economy, Business, First
}
export interface TravelInformation {
  passenger: PassengerInformation;
  trip: 'OW' | 'RT'; // One Way, Round Trip
  originCityCode: string;
  destinationCityCode: string;
  departureDate: string;
  arrivalDate: string;
}

`flight/dto/FlightCuration.dto.ts`

interface JourneyTime {
  hours: number;
  minutes: number;
}

interface FlightDetail {
  departureAirport: string;
  departureDate: string;
  departureTime: string;
  arrivalAirport: string;
  arrivalDate: string;
  arrivalTime: string;
  airline: string;
  journeyTime: JourneyTime;
  carbonEmission: number;
}

interface PricePerPerson {
  adult: number;
  child: number;
  infant: number;
}

interface FlightResult {
  id: string;
  departure: FlightDetail;
  arrival: FlightDetail;
  fare: PricePerPerson;
  link?: string;
}

type Airlines = {
  [key: string]: string;
};

type Airports = {
  [key: string]: string;
};

export type FlightCurationSuccess = {
  flights: {
    [key: string]: FlightResult;
  };
} & { airlines: Airlines; airports: Airports };

export interface FlightCurationError {
  error: string;
}

 

먼저 아웃고잉 어댑터를 위한 포트 인터페이스를 `flight/application/port/out/FetchNaverRecommendFlightUseCase.ts`에 작성해줍시다.

import {
  FlightCurationError,
  FlightCurationSuccess,
} from '@/flight/dto/FlightCuration.dto';
import { TravelInformation } from '@/flight/dto/TravelInformation.dto';

export interface FetchNaverRecommendFlightUseCase {
  fetchNaverRecommendFlight(
    travelInformation: TravelInformation,
  ): Promise<FlightCurationSuccess | FlightCurationError>;
}

 

구현하기 전에 GraphQL Payload를 생성해주는 코드를 우선적으로 작성해봅시다.

export const makeInternationalRoundTripFlightListKeyPayload = ({
  passenger: {
    count: { adult, child, infant },
    fareType,
  },
  trip,
  originCityCode,
  destinationCityCode,
  departureDate,
  arrivalDate,
}: TravelInformation) => {
  const payload = {
    operationName: 'getInternationalList',
    variables: {
      adult,
      child,
      infant,
      where: 'pc',
      isDirect: true, // 직항
      galileoFlag: true,
      travelBizFlag: true,
      fareType: fareType,
      itinerary: [
        {
          departureAirport: originCityCode,
          arrivalAirport: destinationCityCode,
          departureDate: departureDate,
        },
        {
          departureAirport: destinationCityCode,
          arrivalAirport: originCityCode,
          departureDate: arrivalDate,
        },
      ],
      stayLength: '',
      // trip: 'OW',
      trip: trip,
      galileoKey: '',
      travelBizKey: '',
    },
    query: `query getInternationalList(
        $trip: InternationalList_TripType!,
        $itinerary: [InternationalList_itinerary]!,
        $adult: Int = 1,
        $child: Int = 0,
        $infant: Int = 0,
        $fareType: InternationalList_CabinClass!,
        $where: InternationalList_DeviceType = pc,
        $isDirect: Boolean = false,
        $stayLength: String,
        $galileoKey: String,
        $galileoFlag: Boolean = true,
        $travelBizKey: String,
        $travelBizFlag: Boolean = true
    ) {
        internationalList(
            input: {
                trip: $trip,
                itinerary: $itinerary,
                person: { adult: $adult, child: $child, infant: $infant },
                fareType: $fareType,
                where: $where,
                isDirect: $isDirect,
                stayLength: $stayLength,
                galileoKey: $galileoKey,
                galileoFlag: $galileoFlag,
                travelBizKey: $travelBizKey,
                travelBizFlag: $travelBizFlag
            }
        ) {
            galileoKey
            galileoFlag
            travelBizKey
            travelBizFlag
            totalResCnt
            resCnt
            results {
                airlines
                airports
                fareTypes
                schedules
                fares
                errors
            }
        }
    }`,
  };
  return payload;
};

export type NaverFlightKey = {
  galileoKey: string;
  travelBizKey: string;
};

export const makeInternationalRoundTripFlightListPayload = (
  { galileoKey, travelBizKey }: NaverFlightKey,
  {
    passenger: {
      count: { adult, child, infant },
      fareType,
    },
    trip,
    originCityCode,
    destinationCityCode,
    departureDate,
    arrivalDate,
  }: TravelInformation,
) => {
  const payload = {
    operationName: 'getInternationalList',
    variables: {
      adult,
      child,
      infant,
      where: 'pc',
      isDirect: true,
      galileoFlag: galileoKey !== '',
      travelBizFlag: travelBizKey !== '', // If empty, set to false
      fareType,
      itinerary: [
        {
          departureAirport: originCityCode,
          arrivalAirport: destinationCityCode,
          departureDate: departureDate,
        },
        {
          departureAirport: destinationCityCode,
          arrivalAirport: originCityCode,
          departureDate: arrivalDate,
        },
      ],
      stayLength: '',
      trip,
      galileoKey: galileoKey,
      travelBizKey: travelBizKey,
    },
    query: `query getInternationalList($trip: InternationalList_TripType!, $itinerary: [InternationalList_itinerary]!, $adult: Int = 1, $child: Int = 0, $infant: Int = 0, $fareType: InternationalList_CabinClass!, $where: InternationalList_DeviceType = pc, $isDirect: Boolean = false, $stayLength: String, $galileoKey: String, $galileoFlag: Boolean = true, $travelBizKey: String, $travelBizFlag: Boolean = true) {
                internationalList(
                    input: {trip: $trip, itinerary: $itinerary, person: {adult: $adult, child: $child, infant: $infant}, fareType: $fareType, where: $where, isDirect: $isDirect, stayLength: $stayLength, galileoKey: $galileoKey, galileoFlag: $galileoFlag, travelBizKey: $travelBizKey, travelBizFlag: $travelBizFlag}
                ) {
                    galileoKey
                    galileoFlag
                    travelBizKey
                    travelBizFlag
                    totalResCnt
                    resCnt
                    results {
                        airlines
                        airports
                        fareTypes
                        schedules
                        fares
                        errors
                    }
                }
            }`,
  };

  return payload;
};

 

`flight/adapter/out/naverFlight.adapter.ts`에 `FetchNaverRecommendFlightUseCase`를 구현하는 코드를 작성하여 외부 네이버 항공권 API를 사용하여 데이터를 가져오면 됩니다.

import {
  makeInternationalRoundTripFlightListKeyPayload,
  makeInternationalRoundTripFlightListPayload,
  NaverFlightKey,
} from '@/flight/adapter/payload/makeInternationalFlightListPayload';
import { TravelInformation } from '@/flight/dto/TravelInformation.dto';
import {
  FlightCurationError,
  FlightCurationSuccess,
} from '@/flight/dto/FlightCuration.dto';
import { FetchNaverRecommendFlightUseCase } from '@/flight/application/port/out/FetchNaverRecommendFlightUseCase';

const got = async () => {
  const module = await import('got');
  return module.default;
};

const makeNaverFlightRecommendationApiOptions = ({
  arrivalDate,
  departureDate,
  destinationCityCode,
  originCityCode,
  passenger: { count, fareType },
}: TravelInformation) => {
  const url = 'https://airline-api.naver.com/graphql';

  let passengerInfo = '';
  if (count.adult > 0) {
    passengerInfo += `adult=${count.adult}`;
  }
  if (count.child > 0) {
    passengerInfo += `&child=${count.child}`;
  }
  if (count.infant > 0) {
    passengerInfo += `&infant=${count.infant}`;
  }

  const headers = {
    'Content-Type': 'application/json',
    Referer: `https://m-flight.naver.com/flights/international/${originCityCode}-${destinationCityCode}-${departureDate}/${destinationCityCode}-${originCityCode}-${arrivalDate}?${passengerInfo}&isDirect=true&fareType=${fareType}`,
  };
  return { url, headers };
};

const fetchNaverFlightKey = async (
  url: string,
  headers: Record<string, string>,
  travelInformation: TravelInformation,
): Promise<NaverFlightKey> => {
  const gotInstance = await got();
  try {
    const payload =
      makeInternationalRoundTripFlightListKeyPayload(travelInformation);
    const response = await gotInstance.post(url, {
      json: payload,
      headers,
      responseType: 'json',
    });

    const responseData = response.body as any;
    const travelBizKey = responseData.data?.internationalList?.travelBizKey;
    const galileoKey = responseData.data?.internationalList?.galileoKey;

    return {
      travelBizKey: travelBizKey || '',
      galileoKey: galileoKey || '',
    };
  } catch (error) {
    console.log(error.response?.body?.errors);
  }
};

const fetchInternationalFlightList = async (
  url: string,
  headers: Record<string, string>,
  {
    galileoKey,
    travelBizKey,
    travelInformation,
  }: {
    galileoKey: string;
    travelBizKey: string;
    travelInformation: TravelInformation;
  },
): Promise<FlightCurationSuccess | FlightCurationError> => {
  const gotInstance = await got();

  return new Promise((resolve) => {
    setTimeout(async () => {
      const payload = makeInternationalRoundTripFlightListPayload(
        {
          galileoKey,
          travelBizKey,
        },
        travelInformation,
      );
      const response = (await gotInstance.post(url, {
        json: payload,
        headers,
        responseType: 'json',
      })) as any;

      const results = response.body?.data?.internationalList?.results;

      const fares = results['fares'];
      const schedules = results['schedules'];
      const airlines = results['airlines'];
      const [departureSchedule, arrivalSchedule] = schedules as [any, any];

      const result: FlightCurationSuccess = {
        flights: {},
        airlines: {},
        airports: {},
      } as FlightCurationSuccess;

      for (const item of Object.entries(fares)) {
        // 가격
        const [key, value] = item as [string, any];
        const fare = value.fare['A01'][0];
        const pricePerAdult =
          parseInt(fare['Adult']['NaverFare'], 10) ||
          parseInt(fare['Adult']['Fare'], 10) +
            parseInt(fare['Adult']['Tax'], 10) +
            parseInt(fare['Adult']['QCharge'], 10);
        const pricePerChild =
          parseInt(fare['Child']['NaverFare'], 10) ||
          parseInt(fare['Child']['Fare']) +
            parseInt(fare['Child']['Tax'], 10) +
            parseInt(fare['Child']['QCharge'], 10);
        const pricePerInfant =
          parseInt(fare['Infant']['NaverFare'], 10) ||
          parseInt(fare['Infant']['Fare']) +
            parseInt(fare['Infant']['Tax'], 10) +
            parseInt(fare['Infant']['QCharge'], 10);

        const pricePerPerson = {
          adult: pricePerAdult,
          child: pricePerChild,
          infant: pricePerInfant,
        };

        const [departureSchKey, arrivalSchKey] = value.sch;
        const departureSch = departureSchedule[departureSchKey];
        const arrivalSch = arrivalSchedule[arrivalSchKey];
        // console.log(results['airlines'], airlines);

        result.flights[key] = {
          id: key,
          departure: {
            departureAirport: departureSch.detail[0].sa, // 출발 공항
            departureDate: departureSch.detail[0].sdt, // 출발 날짜
            departureTime: departureSch.detail[0].sdt.slice(-4), // 출발 시각 (마지막 4자리)
            arrivalAirport: departureSch.detail[0].ea, // 도착 공항
            arrivalDate: departureSch.detail[0].edt, // 도착 날짜
            arrivalTime: departureSch.detail[0].edt.slice(-4), // 도착 시각 (마지막 4자리)
            airline: departureSch.detail[0].av, // 항공사
            journeyTime: {
              hours: parseInt(departureSch.journeyTime[0]),
              minutes: parseInt(departureSch.journeyTime[1]),
            },
            carbonEmission: departureSch.detail[0].carbonEmission,
          },

          arrival: {
            departureAirport: arrivalSch.detail[0].sa, // 출발 공항
            departureDate: arrivalSch.detail[0].sdt, // 출발 날짜
            departureTime: arrivalSch.detail[0].sdt.slice(-4), // 출발 시각 (마지막 4자리)
            arrivalAirport: arrivalSch.detail[0].ea, // 도착 공항
            arrivalDate: arrivalSch.detail[0].edt, // 도착 날짜
            arrivalTime: arrivalSch.detail[0].edt.slice(-4), // 도착 시각 (마지막 4자리)
            airline: arrivalSch.detail[0].av, // 항공사
            journeyTime: {
              hours: parseInt(arrivalSch.journeyTime[0]),
              minutes: parseInt(arrivalSch.journeyTime[1]),
            },
            carbonEmission: arrivalSch.detail[0].carbonEmission,
          },
          fare: pricePerPerson,
          link: fare?.ReserveParameter?.['#cdata-section'],
        };
      }

      result.airlines = airlines;
      result.airports = results['airports'];

      // airlines, airports 키를 제외한 나머지 키가 2개 이하일 경우 에러로 처리
      if (Object.keys(result.flights).length === 0) {
        resolve({ error: 'No results found' });
      } else {
        resolve(result);
      }
    }, 2000);
  });
};

export class FetchNaverRecommendFlightListAdapter
  implements FetchNaverRecommendFlightUseCase
{
  async fetchNaverRecommendFlight(
    travelInformation: TravelInformation,
  ): Promise<FlightCurationSuccess | FlightCurationError> {
    try {
      const { url, headers } =
        makeNaverFlightRecommendationApiOptions(travelInformation);
      const { galileoKey, travelBizKey } = await fetchNaverFlightKey(
        url,
        headers,
        travelInformation,
      );
      let data: FlightCurationSuccess | FlightCurationError;
      let attempts = 0; // 시도 횟수 제한을 위해 설정 (옵션)
      const maxAttempts = 5; // 최대 시도 횟수

      while (attempts < maxAttempts) {
        data = await fetchInternationalFlightList(url, headers, {
          galileoKey,
          travelBizKey,
          travelInformation,
        });
        console.log(data);
        if (!('error' in data)) {
          return data; // 에러가 없으면 데이터를 반환
        }

        console.log('No results found, retrying...');

        attempts++; // 시도 횟수 증가
      }

      // 최대 시도 횟수를 초과했을 경우 에러 처리
      console.log('Max attempts reached, no results found.');
      return { error: 'Max attempts reached, no results found.' };
    } catch (error) {
      console.error('Error fetching the second response:', error);
    }
  }
}

 

 

이를 통해 구현한 저희의 서비스 메인 페이지는 다음과 같습니다. 

 

 


출처 

https://velog.io/@j2noo/When-You-Buy-%EB%84%A4%EC%9D%B4%EB%B2%84-%ED%95%AD%EA%B3%B5%EA%B6%8C%EC%97%90%EC%84%9C-POST%EB%B0%A9%EC%8B%9D%EC%9D%98-api%EB%A5%BC-%ED%9B%94%EC%B3%90%EC%84%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0with-Python

 

[When-You-Buy] 네이버 항공권에서 POST방식의 api를 훔쳐서 데이터 가져오기(with Python)

Selenium을 사용하지 않고, 파이썬의 Requests 라이브러리를 사용하여 네이버 항공권 api 훔쳐쓰기

velog.io