diff --git a/backend/src/services/festival/index.js b/backend/src/services/festival/index.js index db080a6..5901096 100644 --- a/backend/src/services/festival/index.js +++ b/backend/src/services/festival/index.js @@ -4,7 +4,7 @@ * 수집하여 행사 일정 자동 생성. Gemini 한 번의 호출로 두 타입을 함께 추출(type 필드로 구분). */ import fp from 'fastify-plugin'; -import { fetchSearchPostUrls } from './scraper.js'; +import { fetchSearchPostUrls, decodeUrl } from './scraper.js'; import { extractFestivalsFromUrls } from './gemini.js'; import { createEventSchedule, searchKakaoPlace, kakaoToVenue } from '../event.js'; import { CATEGORY_IDS } from '../../config/index.js'; @@ -206,10 +206,10 @@ async function festivalBotPlugin(fastify, opts) { throw err; // scheduler의 consecutiveErrors 처리로 전달 } - // 결과를 source_url별로 그룹화 + // 결과를 source_url별로 그룹화 (인코딩 차이 흡수를 위해 디코딩 기준으로 매칭) const bySource = new Map(); for (const f of festivals) { - const src = f.source_url || ''; + const src = decodeUrl(f.source_url || ''); if (!bySource.has(src)) bySource.set(src, []); bySource.get(src).push(f); } @@ -226,7 +226,7 @@ async function festivalBotPlugin(fastify, opts) { // 4. 배치의 모든 URL을 크롤 로그에 기록 for (const url of batch) { - const matched = bySource.get(url) || []; + const matched = bySource.get(decodeUrl(url)) || []; const status = matched.length > 0 ? 'processed' : 'no_event'; await logCrawled(url, status, matched.length); } diff --git a/backend/src/services/festival/scraper.js b/backend/src/services/festival/scraper.js index 74b7b3d..d7c08e9 100644 --- a/backend/src/services/festival/scraper.js +++ b/backend/src/services/festival/scraper.js @@ -30,11 +30,24 @@ async function fetchWithTimeout(url, timeout = FETCH_TIMEOUT) { } } +/** + * 퍼센트 인코딩된 한글 URL을 디코딩 (실패 시 원본 유지) + * Gemini가 긴 %인코딩 시퀀스를 도구 호출 시 잘못 복사해 fetch가 실패하는 문제 방지. + * 한글 그대로 전달하면 모델이 정확히 복사하고 url_context가 알아서 인코딩해 가져옴. + */ +export function decodeUrl(url) { + try { + return decodeURIComponent(url); + } catch { + return url; + } +} + /** * HTML에서 게시글(/entry/) URL 추출 * @param {string} html - 검색 페이지 HTML * @param {string} origin - 사이트 origin (예: https://memogipost.tistory.com) - * @returns {string[]} 절대 URL 배열 + * @returns {string[]} 절대 URL 배열 (한글 디코딩됨) */ export function extractEntryUrls(html, origin) { const urls = new Set(); @@ -48,7 +61,7 @@ export function extractEntryUrls(html, origin) { } // 쿼리스트링/해시 제거 url = url.split('#')[0].split('?')[0]; - urls.add(url); + urls.add(decodeUrl(url)); } return [...urls]; }