chore: 현재 작업 중간 커밋

This commit is contained in:
chungyeong
2026-03-05 11:00:45 +09:00
parent 02970df6af
commit be88b4fcec
43 changed files with 6837 additions and 466 deletions

117
src/crawlers/naver.js Normal file
View File

@@ -0,0 +1,117 @@
"use strict";
const { withBrowser, navigateWithHumanBehavior, extractPricesFromPage } = require("./baseCrawler");
/**
* Formats a date as YYYYMMDD for Naver Flights URLs.
*/
function getDStr(date) {
const d = new Date(date);
const yy = d.getFullYear();
const mm = String(d.getMonth() + 1).padStart(2, "0");
const dd = String(d.getDate()).padStart(2, "0");
return `${yy}${mm}${dd}`;
}
/**
* Maps internal cabin class to Naver fareType code.
* economy → Y, premium_economy → P, business → C, first → F
*/
function getFareType(byCabin) {
if (byCabin.first > 0) return "F";
if (byCabin.business > 0) return "C";
if (byCabin.premium_economy > 0) return "P";
return "Y";
}
/**
* Builds a Naver Flights URL from structured search params.
*
* Round-trip format:
* /flights/international/ICN-NRT-20260315/NRT-ICN-20260322?adult=1&fareType=Y
*
* Multi-city format:
* /flights/multi?itinerary=ICN:NRT:20260315,NRT:ICN:20260322&adult=1&fareType=Y
*
* One-way format:
* /flights/international/ICN-NRT-20260315?adult=1&fareType=Y
*/
function buildNaverUrl(searchParams) {
const segments = searchParams.segments || [];
if (segments.length === 0) return "https://flight.naver.com";
const tripType = searchParams.tripType;
const adults = searchParams.passengers?.total || 1;
const byCabin = searchParams.passengers?.byCabin || {};
const fareType = getFareType(byCabin);
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
if (tripType === "round_trip") {
const from = segments[0].from.toUpperCase();
const to = segments[0].to.toUpperCase();
const departDate = searchParams.departureDateWindow?.from || tomorrow;
const d1 = getDStr(departDate);
// Return date: use stayDurationDays if available, default to 7 days
const stayDays = searchParams.stayDurationDays?.minDays || 7;
const returnDate = new Date(departDate);
returnDate.setDate(returnDate.getDate() + stayDays);
const d2 = getDStr(returnDate);
// Naver round-trip: outbound segment / return segment (origin/dest reversed)
return `https://flight.naver.com/flights/international/${from}-${to}-${d1}/${to}-${from}-${d2}?adult=${adults}&fareType=${fareType}`;
}
if (tripType === "multi_city") {
// Naver multi-city: /flights/multi?itinerary=ICN:NRT:20260315,NRT:ICN:20260322
let currentBaseDate = new Date(searchParams.departureDateWindow?.from || tomorrow);
const itineraryParts = [];
for (const seg of segments) {
itineraryParts.push(`${seg.from.toUpperCase()}:${seg.to.toUpperCase()}:${getDStr(currentBaseDate)}`);
currentBaseDate.setDate(currentBaseDate.getDate() + 3);
}
return `https://flight.naver.com/flights/multi?itinerary=${itineraryParts.join(",")}&adult=${adults}&fareType=${fareType}`;
}
// one_way
const from = segments[0].from.toUpperCase();
const to = segments[0].to.toUpperCase();
const d1 = searchParams.departureDateWindow?.from ? getDStr(searchParams.departureDateWindow.from) : getDStr(tomorrow);
return `https://flight.naver.com/flights/international/${from}-${to}-${d1}?adult=${adults}&fareType=${fareType}`;
}
async function scrapeNaver(searchParams) {
const url = buildNaverUrl(searchParams);
console.log(`[Naver] Navigating to ${url}`);
return withBrowser(async (page) => {
await navigateWithHumanBehavior(page, url);
// Wait for price elements with generic selectors
try {
await page.waitForSelector('[class*="price"], [class*="Price"], [class*="fare"]', { timeout: 15000 });
} catch (e) {
console.log("[Naver] Timeout waiting for price elements.");
}
// Extract prices using generic Korean won patterns
const prices = await extractPricesFromPage(page);
if (prices.length === 0) {
console.log("[Naver] No prices found on page.");
return [];
}
return prices.map((price, i) => ({
provider: "naver",
price,
currency: "KRW",
metadata: { url, rank: i + 1 },
}));
});
}
module.exports = { scrapeNaver, buildNaverUrl };