์ด์ ์ค์ ํญ๊ณต๊ถ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ค๊ธฐ ์ํ ์์ ์ ํด๋ณผ ๊ฒ์ด๋ค.
๋จผ์ ํญ๊ณต๊ถ์ด ํ๋งค๋๋ ์์คํ ๋ถํฐ ์์๋ณด๋ฉด, ํญ๊ณต์ฌ ์์ฒด ํํ์ด์ง์์ ํ๋งคํ๋ ํญ๊ณต๊ถ์ด ์๊ณ ์ค์นด์ด์ค์บ๋๊ฐ์ ์ฌ์ดํธ์ ์ ๊ณตํ๋ ํญ๊ณต๊ถ๋ฑ์ด ๋ฐ๋ก ์์ด์ ์ด๋์๋ ์ฌ๋ผ์์๋๋ฐ ์ด๋์๋ ์๊ฑฐ๋, ๊ฐ์ ํญ๊ณตํธ์ด๋ผ๋ ๊ฐ๊ฒฉ์ด ๋ค๋ฅธ ์ํฉ์ด ๋ฐ์ํ๋ค.
๊ทธ๋์ ์ต๋ํ ๋ง์ ๊ณณ์์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์จ ๋ค, ํ๋งค์ฒ๋ณ๋ก ๊ฐ๊ฒฉ์ ๋ณด์ฌ์ฃผ๋ ์์ผ๋ก ๊ตฌ์กฐ๋ฅผ ๋ณ๊ฒฝํ๋ ๊ฒ ์ข์ ๊ฒ ๊ฐ๋ค.
๋จผ์ ์๋ฒ์ธก์ ์์ฒญ, ๋ฐํ ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ฅผ ์์ ํด์ค๋ค.
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List
from search_naver import search_naver_flights
app = FastAPI()
# ๋ฐํํ ๋ฐ์ดํฐ ๊ตฌ์กฐ
class PriceItem(BaseModel):
provider : str
price : int
class Ticket(BaseModel):
airline : str
departTime : str
arrivalTime : str
prices : List[PriceItem]
# Request ๊ตฌ์กฐ
class SearchRequest(BaseModel):
from_: str
to: str
date: str # YYYY-MM-DD
# Response ๊ตฌ์กฐ
class SearchResponse(BaseModel):
results: List[Ticket]
@app.post("/search", response_model=SearchResponse)
def search(req: SearchRequest):
print("[REQUEST]", req.from_, "→", req.to, req.date) # Log
results_json = search_naver_flights(req.from_, req.to, req.date)
results = [Ticket(**r) for r in results_json] # Pydantic ๋ชจ๋ธ๋ก ๋ณํ
return {"results": results}
ํ๋ฌํฐ์ธก๋ ๊ตฌ์กฐ์ ๋ง๊ฒ ์์ ํด์ค๋ค.
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'dart:io';
import 'package:flutter/foundation.dart';
class TicketApi {
static String get baseUrl {
if (kIsWeb) return "http://127.0.0.1:8000";
if (Platform.isAndroid) return "http://10.0.2.2:8000";
return "http://127.0.0.1:8000";
}
static Future<List<dynamic>> searchFlights({required String from, required String to, required String date,}) async {
final url = Uri.parse("$baseUrl/search");
final response = await http.post(
url,
headers: {"Content-Type": "application/json"},
body: jsonEncode({
"from_": from,
"to": to,
"date": date,
}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data["results"];
}
else{ throw Exception("์๋ฒ ์ค๋ฅ: ${response.statusCode}"); }
}
}
๊ตฌ์กฐ๋ฅผ ์์ ํ์ผ๋ฉด ์ด์ ํฌ๋กค๋ง์ ๊ตฌํํด๋ณด์.
ํฌ๋กค๋ง์ Playwright ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ฌ ๊ตฌํํ ๊ฒ์ด๋ค.
Playwright๋ ๋ธ๋ผ์ฐ์ ๋ฅผ ์ฝ๋๋ก ์กฐ์ํ ์ ์๋ ์๋ํ ์์ง์ด๋ค.
pip install playwright
python -m playwright install
๋ก ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ค์นํด์ฃผ๊ณ
์ด๋ฒ์๋ ๋ค์ด๋ฒ ํญ๊ณต๊ถ์ ์๋ก ๋ค๋ฉด, ํญ๊ณต๊ถ ๊ฒ์ ์
https://flight.naver.com/flights/international/ICN:airport-TYO:city-20260113?adult=1&fareType=Y
์ด๋ฐ ์์ผ๋ก ๊ฒ์ ์กฐ๊ฑด(์ถ๋ฐ์ง, ๋์ฐฉ์ง, ๋ ์ง, ์ธ์ ์ ๋ฑ)์ด url์ ํฌํจ๋์ด์๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
์ด๋ฅผ ์ด์ฉํด์ ๊ฒ์ ์์ฒด๋ ์ฌ์ฉ์์ ์ ๋ ฅ์ url์ ํฌํจ์ํค๋ ์์ผ๋ก ๊ฐ๋จํ๊ฒ ๊ตฌํํ ์ ์๋ค!
(์ ๋ ฅ๋ฐ์ ๋ณ์๋ฅผ ๊ฐ ์ฌ์ดํธ์ ๋ง๋ ํ์์ผ๋ก ๋ณํ ํ์)
๊ทธ๋ ๋ค๋ฉด ๋จ์ ๊ฑด ๊ฒ์ํ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ์ดํฐ๋ก ์ ๋ฆฌํ์ฌ ๋ณด์ฌ์ฃผ๋ ๊ฒ์ด๋ค.
๊ฒ์ ๊ฒฐ๊ณผ์ฐฝ์์ ๋ธ๋ผ์ฐ์ (ํฌ๋กฌ)์ ๊ฐ๋ฐ์ ๋๊ตฌ(F12)๋ก ํ์ธํด๋ณด๋ฉด

์ฌ์ง์ฒ๋ผ ์ํ๋ ๋ฐ์ดํฐ์ ํด๋์ค๋ช ์ ํ์ธํ ์ ์๋ค.
ํ์ํ ํด๋์ค๋ช ๋ค์ ์ฐพ์์ผ๋ฉด ํฌ๋กค๋ง์ฉ ํ์ด์ฌ ํ์ผ์ ํ๋ ์์ฑํ ๋ค ์ฝ๋๋ฅผ ์์ฑํด์ค๋ค.
from playwright.sync_api import sync_playwright
import time
NAVER_CODE_MAP = {
"INCHEON": "ICN",
"TOKYO": "TYO",
}
def search_naver_flights(from_: str, to: str, date: str):
date_str = date.replace("-", "")
naver_from = NAVER_CODE_MAP.get(from_, from_)
naver_to = NAVER_CODE_MAP.get(to, to)
url = f"https://flight.naver.com/flights/international/{naver_from}:airport-{naver_to}:city-{date_str}?adult=1&fareType=Y"
results = []
try:
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
page.goto(url, wait_until="networkidle", timeout=10000)
# ํญ๊ณต์ฌ๋ช
์์๋ฅผ ์ฐพ์๋๊น์ง ๋๊ธฐ (์ต๋ 10์ด)
airline_found = False
for i in range(10):
time.sleep(1)
airline_count = page.locator('[class*="airline_name__"]').count()
if airline_count > 0:
airline_found = True
break
if not airline_found:
print("[WARNING] ํญ๊ณตํธ์ ์ฐพ์ ์ ์์ต๋๋ค")
browser.close()
return results
flight_items = page.locator('[class*="combination_ConcurrentItemContainer__"]')
flight_count = flight_items.count()
print(f"{flight_count}๊ฐ ํญ๊ณต๊ถ ๋ฐ๊ฒฌ")
if flight_count == 0:
print("[WARNING] flight_items์ ์ฐพ์ ์ ์์ต๋๋ค.")
browser.close()
return results
# ๊ฐ ํญ๊ณตํธ ์ปจํ
์ด๋ ๋ด์์ ๋ฐ์ดํฐ ์ถ์ถ
for i in range(flight_count):
try:
flight_item = flight_items.nth(i)
# ํญ๊ณต์ฌ๋ช
airline = flight_item.locator('[class*="airline_name__"]').inner_text()
# ์ถ๋ฐ์๊ฐ, ๋์ฐฉ์๊ฐ
route_times = flight_item.locator('[class*="route_time__"]')
depart_time = route_times.nth(0).inner_text()
arrival_time = route_times.nth(1).inner_text()
# ๊ฐ๊ฒฉ
price_text = flight_item.locator('[class*="item_num__"]').first.inner_text()
price = int(price_text.replace(",", "").replace("์", "").strip())
results.append({
"airline": airline.strip(),
"departTime": depart_time.strip(),
"arrivalTime": arrival_time.strip(),
"prices": [{"provider": "๋ค์ด๋ฒํญ๊ณต๊ถ", "price": price}]
})
# print(f"[{i+1:2d}] {airline.strip():15s} {depart_time.strip()}→{arrival_time.strip():8s} - {price:,}์")
except Exception as e:
print(f"[ERROR-{i}] ํญ๊ณต๊ถ {i} ํ์ฑ ์คํจ: {e}")
continue
# ์ค๋ณต ์ ๊ฑฐ
seen = set()
unique_results = []
for flight in results:
key = (flight['airline'], flight['departTime'], flight['arrivalTime'], flight['prices'][0]['price'])
if key not in seen:
seen.add(key)
unique_results.append(flight)
results = unique_results
browser.close()
print("ํฌ๋กค๋ง ์๋ฃ")
except Exception as e:
print(f"[FATAL ERROR] {type(e).__name__}: {e}")
import traceback
traceback.print_exc()
raise
return results
์์์๋ถํฐ ์ดํด๋ณด๋ฉด ๋จผ์ ๋ ์ง, ์ถ๋ฐ์ง, ๋์ฐฉ์ง ๋ฑ์ ๋ค์ด๋ฒํญ๊ณต๊ถ ์ฌ์ดํธ์ ๋ง๊ฒ ๋ณํํด์ฃผ๊ณ ,
ํด๋น url๋ก ๋ธ๋ผ์ฐ์ ๋ฅผ ์ด๊ณ ์คํฌ๋ฆฝํธ๊ฐ ๋ ๋๋ง์ด ๋ ๊ฒ์ ํ์ธํ์ผ๋ฉด ๊ฐ ํญ๊ณต๊ถ ์ปจํ ์ด๋(๋ฌถ์)์์ ํญ๊ณต์ฌ๋ช , ์ถ๋ฐ์๊ฐ, ๋์ฐฉ์๊ฐ, ๊ฐ๊ฒฉ๋ฑ์ ์ฝ์ด์ results๋ก ์ ๋ฆฌํ ๋ค ๋ฐํํด์ค๋ค.
ํ๋ฌํฐ์ ๊ธฐ์กด UI๋ ๋ฐ๋ ํ์์ ๋ง๊ฒ ์์ ํด์ฃผ๋ฉด

์ค์ ํญ๊ณต๊ถ ๋ฐ์ดํฐ๊ฐ ์ ๋ถ๋ฌ์์ง ๋ชจ์ต์ด๋ค!
์ด์ ๋ค๋ฅธ ํญ๊ณต์ฌ๋ ํญ๊ณต๊ถ ์ฌ์ดํธ๋ค์ ์ถ๊ฐํด์ฃผ๋ฉด ๋๋ค.