fix: 유럽 다국가 뉴스 요청이 빈손으로 돌아오던 문제 (+ bbc.co.uk 오류)
사용자 지적: "유럽은 여전한데?" 로그를 보니 모델은 유럽 요청을 이렇게 보낸다.
news_search({"category":"top","country":"gb,de,fr,it,es"})
직전 커밋(13e2f79)의 매체 제한이 두 이유로 빠져나갔다:
1. category가 있으면 건너뛰게 했는데 "top"은 분야가 아니라 전체 피드다.
correctNewsCategoryForBreadth는 이미 top을 통과시키고 있었으니, 두 곳의
판단이 어긋나 있었던 셈이다
2. country가 "gb,de,fr,it,es" 콤마 목록이라 나라별 조회 자체가 실패했다
- de/fr/it/es 목록 추가(전부 라이브 확인). 멜로니 개각설, 트럼프 비자 취소
17만5천 건 같은 실제 뉴스가 나온다
- 다국가는 라운드로빈으로 5개를 채운다. 앞에서부터 채우면 영국 매체만 다섯이
되어 대륙의 나머지가 통째로 빠진다
- category=top은 넓은 요청으로 취급한다
그리고 내가 만든 버그 하나를 같이 고쳤다: gb 목록의 bbc.co.uk는 NewsData DB에
없는 도메인이라(정답은 bbc.com) 요청 전체가 HTTP 422로 죽었다. **도메인 하나가
잘못되면 나머지 넷도 같이 실패한다.** us/kr 목록은 라이브로 확인하고 넣었는데
gb만 검증 없이 추가했다가 유럽 요청이 통째로 터졌다. 목록에 도메인을 추가할 땐
반드시 확인할 것 — 주석으로 남겼다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+37
-5
@@ -148,9 +148,21 @@ const CATEGORY_TOPIC: Record<string, RegExp> = {
|
||||
const MAJOR_OUTLETS: Record<string, string[]> = {
|
||||
us: ['reuters.com', 'apnews.com', 'nytimes.com', 'washingtonpost.com', 'npr.org'],
|
||||
kr: ['newsis.com', 'joongang.co.kr', 'chosun.com', 'hani.co.kr', 'donga.com'],
|
||||
gb: ['bbc.co.uk', 'theguardian.com', 'reuters.com', 'ft.com', 'independent.co.uk'],
|
||||
// bbc.com 이다. bbc.co.uk 는 NewsData DB에 없어서 요청 전체가 HTTP 422로 죽는다 —
|
||||
// 도메인 하나가 잘못되면 나머지 넷도 같이 실패한다. 2026-08-12에 이걸 검증 없이 넣었다가
|
||||
// 유럽 다국가 요청이 통째로 터졌다. 목록에 도메인을 추가할 땐 반드시 라이브로 확인할 것.
|
||||
gb: ['bbc.com', 'theguardian.com', 'reuters.com', 'ft.com', 'independent.co.uk'],
|
||||
// 2026-08-12 추가. 사용자가 "유럽은 여전한데?"라고 지적해 로그를 보니 모델이 유럽 요청을
|
||||
// country="gb,de,fr,it,es" 다국가 형태로 보내고 있었다. 아래 네 나라는 라이브로 확인했다.
|
||||
de: ['spiegel.de', 'zeit.de', 'faz.net', 'sueddeutsche.de', 'welt.de'],
|
||||
fr: ['lemonde.fr', 'lefigaro.fr', 'liberation.fr', 'france24.com', 'lesechos.fr'],
|
||||
it: ['corriere.it', 'repubblica.it', 'lastampa.it', 'ansa.it', 'ilsole24ore.com'],
|
||||
es: ['elpais.com', 'elmundo.es', 'abc.es', 'lavanguardia.com', 'rtve.es'],
|
||||
};
|
||||
|
||||
/** domainurl은 6개째부터 UnsupportedQueryLength로 거절된다. 편집 판단이 아니라 API 제약. */
|
||||
const MAX_DOMAINS = 5;
|
||||
|
||||
/**
|
||||
* Restricts a BROAD request to major outlets.
|
||||
*
|
||||
@@ -161,11 +173,31 @@ const MAJOR_OUTLETS: Record<string, string[]> = {
|
||||
*/
|
||||
export function applyMajorOutletsForBroadRequest(args: any, country: string): any {
|
||||
if (String(args?.query || '').trim()) return args;
|
||||
if (String(args?.category || '').trim()) return args;
|
||||
if (String(args?.domainurl || '').trim()) return args;
|
||||
const domains = MAJOR_OUTLETS[String(country || '').toLowerCase()];
|
||||
if (!domains) return args;
|
||||
return { ...(args || {}), domainurl: domains.join(',') };
|
||||
// "top"은 분야가 아니라 전체 피드다 — correctNewsCategoryForBreadth도 같은 이유로 top을
|
||||
// 통과시킨다. 두 곳이 어긋나면 모델이 top을 붙였다는 이유만으로 이 보정이 통째로 비활성화된다
|
||||
// (2026-08-12 유럽 요청이 정확히 그렇게 빠져나갔다).
|
||||
const category = String(args?.category || '').trim().toLowerCase();
|
||||
if (category && category !== 'top') return args;
|
||||
|
||||
// country는 "gb,de,fr,it,es" 처럼 여러 나라가 올 수 있다. 나라별 목록에서 라운드로빈으로
|
||||
// 뽑아 5개를 채운다 — 앞에서부터 채우면 첫 나라 매체만 다섯이 되어 나머지가 통째로 빠진다.
|
||||
const countries = String(country || '').toLowerCase().split(',').map(c => c.trim()).filter(Boolean);
|
||||
const lists = countries.map(c => MAJOR_OUTLETS[c]).filter(Boolean) as string[][];
|
||||
if (!lists.length) return args;
|
||||
|
||||
const picked: string[] = [];
|
||||
for (let i = 0; picked.length < MAX_DOMAINS; i++) {
|
||||
const before = picked.length;
|
||||
for (const list of lists) {
|
||||
if (picked.length >= MAX_DOMAINS) break;
|
||||
const d = list[i];
|
||||
if (d && !picked.includes(d)) picked.push(d);
|
||||
}
|
||||
if (picked.length === before) break; // 모든 목록이 소진됨
|
||||
}
|
||||
if (!picked.length) return args;
|
||||
return { ...(args || {}), domainurl: picked.join(',') };
|
||||
}
|
||||
|
||||
export function fillNewsCategoryWhenExplicit(args: any, currentMessage: string): any {
|
||||
|
||||
+35
-1
@@ -187,7 +187,7 @@ describe('applyMajorOutletsForBroadRequest — 넓은 요청만 주요 매체로
|
||||
test('주제 없는 요청은 주요 매체로 좁힌다', () => {
|
||||
assert.match(applyMajorOutletsForBroadRequest({}, 'us').domainurl, /reuters\.com/);
|
||||
assert.match(applyMajorOutletsForBroadRequest({}, 'kr').domainurl, /joongang\.co\.kr/);
|
||||
assert.match(applyMajorOutletsForBroadRequest({}, 'gb').domainurl, /bbc\.co\.uk/);
|
||||
assert.match(applyMajorOutletsForBroadRequest({}, 'gb').domainurl, /bbc\.com/);
|
||||
});
|
||||
|
||||
test('API 제한상 5개를 넘기지 않는다', () => {
|
||||
@@ -197,6 +197,40 @@ describe('applyMajorOutletsForBroadRequest — 넓은 요청만 주요 매체로
|
||||
}
|
||||
});
|
||||
|
||||
test('다국가 요청은 나라별로 고르게 뽑는다 — 2026-08-12 유럽 사고', () => {
|
||||
// 모델은 유럽 요청을 country="gb,de,fr,it,es" 로 보낸다. 앞에서부터 채우면 영국 매체만
|
||||
// 다섯이 되어 대륙의 나머지가 통째로 빠진다.
|
||||
const d = applyMajorOutletsForBroadRequest({}, 'gb,de,fr,it,es').domainurl.split(',');
|
||||
assert.equal(d.length, 5);
|
||||
for (const host of ['bbc.com', 'spiegel.de', 'lemonde.fr', 'corriere.it', 'elpais.com']) {
|
||||
assert.ok(d.includes(host), `${host} 누락: ${d.join(',')}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('나라가 적으면 각 나라에서 더 많이 뽑는다', () => {
|
||||
const d = applyMajorOutletsForBroadRequest({}, 'de,fr').domainurl.split(',');
|
||||
assert.equal(d.length, 5);
|
||||
const GERMAN = ['spiegel.de', 'zeit.de', 'faz.net', 'sueddeutsche.de', 'welt.de'];
|
||||
const FRENCH = ['lemonde.fr', 'lefigaro.fr', 'liberation.fr', 'france24.com', 'lesechos.fr'];
|
||||
assert.ok(d.filter(x => GERMAN.includes(x)).length >= 2, d.join(','));
|
||||
assert.ok(d.filter(x => FRENCH.includes(x)).length >= 2, d.join(','));
|
||||
});
|
||||
|
||||
test('category=top은 분야가 아니라 전체 피드이므로 보정을 막지 않는다', () => {
|
||||
// 유럽 요청이 정확히 이 구멍으로 빠져나갔다: 모델이 top을 붙였다는 이유만으로
|
||||
// 매체 제한이 통째로 비활성화됐다. correctNewsCategoryForBreadth도 top을 통과시키므로
|
||||
// 두 곳의 판단이 어긋나면 안 된다.
|
||||
assert.match(applyMajorOutletsForBroadRequest({ category: 'top' }, 'us').domainurl, /reuters/);
|
||||
});
|
||||
|
||||
test('목록에 없는 나라가 섞여도 있는 나라로 채운다', () => {
|
||||
// TLD로 검증하면 안 된다 — faz.net은 독일 매체지만 .de가 아니다.
|
||||
const GERMAN = ['spiegel.de', 'zeit.de', 'faz.net', 'sueddeutsche.de', 'welt.de'];
|
||||
const d = applyMajorOutletsForBroadRequest({}, 'jp,de').domainurl.split(',');
|
||||
assert.ok(d.length > 0);
|
||||
assert.ok(d.every(x => GERMAN.includes(x)), d.join(','));
|
||||
});
|
||||
|
||||
test('주제를 지정한 요청은 건드리지 않는다 — 좁히면 그 주제의 보도가 가려진다', () => {
|
||||
assert.equal(applyMajorOutletsForBroadRequest({ query: 'crime' }, 'us').domainurl, undefined);
|
||||
assert.equal(applyMajorOutletsForBroadRequest({ category: 'sports' }, 'us').domainurl, undefined);
|
||||
|
||||
Reference in New Issue
Block a user