fix(festival-bot): 한글 URL 디코딩으로 Gemini url_context 실패 해결

퍼센트 인코딩된 긴 한글 URL(%EC%BA%90...)을 Gemini가 도구 호출 시
잘못 복사해 fetch가 실패('URL 불일치')하고 no_event로 기록되던 문제
(캐리비안 베이 글 누락 원인). 스크래퍼가 URL을 한글로 디코딩해 전달하고
source_url 매칭도 디코딩 기준으로 정규화. 기존 크롤로그 24건 디코딩
마이그레이션 + 캐리비안 베이 재처리 → 7/4 일정 생성 확인.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-06-12 14:12:53 +09:00
parent f0eae805e6
commit 5c31977411
2 changed files with 19 additions and 6 deletions

View file

@ -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);
}

View file

@ -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];
}