사용자 지적: "뭔가 이상한데" — 매체를 좁힌 뒤에도 함부르크 아파트 균열,
패러글라이딩 실종자 수색, "하비에르 바르뎀은 친근한 배우" 같은 기사가
"주요 뉴스"로 올라왔다.
원인은 매체가 아니라 정렬이었다. NewsData /latest는 시간 역순이고, 주요지도
하루에 수백 건을 내므로 **가장 최근이 가장 중요한 것은 아니다.** 매체 제한은
바닥(물병 광고)은 올렸지만 천장은 못 올렸다. API 자체 손잡이(category=top,
prioritydomain=top)는 앞서 전부 실측했고 소용없었다.
신문 1면이 빠진 신호다. 사람 편집자가 이미 하루를 정렬해 놓은 유일한 곳이고,
주요지는 전부 RSS로 공개한다. 같은 시각 같은 요청의 결과:
NewsData(매체제한) 함부르크 아파트 균열 / 카탈루냐 패러글라이딩 / 바르뎀 호평
1면 RSS 콜롬비아 지진 180명 사망 / 트럼프 앙카라 비밀 탈출 /
미네소타 경선 결과 / 스페인의 대이탈리아 국경 검문
- 넓은 요청(query 없고 category 없거나 top)에만 적용. 주제를 지정한 요청은
NewsData 전체 풀 그대로다 — 실측 확인: query="crime"은 여전히 지역지 기사까지
들어온다. 1면은 정의상 그날의 톱이라 특정 주제를 물으면 답이 안 나온다
- 다국가는 라운드로빈으로 병합. 발행량 많은 1면 하나가 나머지를 밀어내면 안 된다
- 피드 실패는 조용히 건너뛰고, 전부 실패하면 NewsData로 폴백한다
- 피드는 전부 라이브 확인 후 추가. 한국은 제외했다 — 연합/한겨레/경향/JTBC를
다 확인했으나 전부 시간순이고 편집 정렬된 1면 피드가 없어, kr은 기존
주요매체 경로를 그대로 쓴다
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
81 lines
3.1 KiB
TypeScript
81 lines
3.1 KiB
TypeScript
/**
|
|
* top-headlines — 1면 RSS 파싱
|
|
*
|
|
* 순수 함수만 테스트한다. 피드 수신은 네트워크에 의존하므로 다루지 않는다.
|
|
*
|
|
* 이 파서가 조용히 깨지면 "뉴스가 없는 날"처럼 보인다 — 남의 문서 구조에 기대는 코드라
|
|
* 언젠가 바뀐다. 저장된 조각으로 회귀를 잡는 게 이 테스트의 목적이다.
|
|
*/
|
|
|
|
import { test, describe } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { parseFeedItems, hasTopHeadlineFeeds, formatHeadlines } from '../src/tools/top-headlines';
|
|
|
|
const RSS = `<?xml version="1.0"?><rss><channel>
|
|
<title>NYT > Top Stories</title>
|
|
<item><title>Lisa Demuth Wins Republican Primary</title><link>https://ex.invalid/a</link></item>
|
|
<item><title><![CDATA[Rescuers scramble: 180 dead & counting]]></title><link>https://ex.invalid/b</link></item>
|
|
</channel></rss>`;
|
|
|
|
const ATOM = `<?xml version="1.0"?><feed>
|
|
<entry><title>Front page story</title><link rel="alternate" href="https://ex.invalid/c"/></entry>
|
|
</feed>`;
|
|
|
|
describe('parseFeedItems', () => {
|
|
test('RSS item에서 제목과 링크를 뽑는다', () => {
|
|
const items = parseFeedItems(RSS, 'nytimes.com');
|
|
assert.equal(items.length, 2);
|
|
assert.equal(items[0].title, 'Lisa Demuth Wins Republican Primary');
|
|
assert.equal(items[0].link, 'https://ex.invalid/a');
|
|
assert.equal(items[0].source, 'nytimes.com');
|
|
});
|
|
|
|
test('채널 제목은 기사로 세지 않는다', () => {
|
|
assert.ok(!parseFeedItems(RSS, 'x').some(i => i.title.includes('Top Stories')));
|
|
});
|
|
|
|
test('CDATA와 엔티티를 푼다', () => {
|
|
assert.equal(parseFeedItems(RSS, 'x')[1].title, 'Rescuers scramble: 180 dead & counting');
|
|
});
|
|
|
|
test('Atom entry도 읽는다 — link는 href 속성에 있다', () => {
|
|
const items = parseFeedItems(ATOM, 'x');
|
|
assert.equal(items[0].title, 'Front page story');
|
|
assert.equal(items[0].link, 'https://ex.invalid/c');
|
|
});
|
|
|
|
test('limit을 지킨다', () => {
|
|
assert.equal(parseFeedItems(RSS, 'x', 1).length, 1);
|
|
});
|
|
|
|
test('구조가 바뀌면 0건 — 호출부가 NewsData로 폴백할 신호', () => {
|
|
assert.deepEqual(parseFeedItems('<html>완전히 다른 문서</html>', 'x'), []);
|
|
assert.deepEqual(parseFeedItems('', 'x'), []);
|
|
});
|
|
});
|
|
|
|
describe('hasTopHeadlineFeeds', () => {
|
|
test('피드가 있는 나라', () => {
|
|
for (const c of ['us', 'gb', 'de', 'fr', 'it', 'es']) assert.equal(hasTopHeadlineFeeds(c), true, c);
|
|
});
|
|
|
|
test('다국가 목록은 하나라도 있으면 참', () => {
|
|
assert.equal(hasTopHeadlineFeeds('gb,de,fr,it,es'), true);
|
|
assert.equal(hasTopHeadlineFeeds('jp,de'), true);
|
|
});
|
|
|
|
test('피드가 없는 나라 — NewsData 경로로 간다', () => {
|
|
assert.equal(hasTopHeadlineFeeds('kr'), false);
|
|
assert.equal(hasTopHeadlineFeeds('jp'), false);
|
|
assert.equal(hasTopHeadlineFeeds(''), false);
|
|
});
|
|
});
|
|
|
|
describe('formatHeadlines', () => {
|
|
test('번호·링크·출처를 담는다', () => {
|
|
const out = formatHeadlines(parseFeedItems(RSS, 'nytimes.com'));
|
|
assert.match(out, /^\[1\] Lisa Demuth/);
|
|
assert.match(out, /Source: nytimes\.com/);
|
|
});
|
|
});
|