Skip to content

Adding a News Source

โ€œMinjung edited this page Sep 26, 2026 · 1 revision

Adding a News Source

์ƒˆ๋กœ์šด ๋‰ด์Šค ์†Œ์Šค๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•ˆ๋‚ดํ•ฉ๋‹ˆ๋‹ค.

๊ฐœ์š”

HackDigest์˜ ๋‰ด์Šค ์†Œ์Šค๋Š” ํ† ํ”ฝ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค. ์ƒˆ ํ† ํ”ฝ์„ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜, ๊ธฐ์กด ํ† ํ”ฝ์— ์†Œ์Šค๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

1. ์ƒˆ ํ† ํ”ฝ ์ถ”๊ฐ€

Step 1: Topic Enum ์ถ”๊ฐ€

src/models.py์— ์ƒˆ ํ† ํ”ฝ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค:

class Topic(str, Enum):
    TECH = "tech"
    STOCKS = "stocks"
    REALESTATE = "realestate"
    CRYPTO = "crypto"  # ์ƒˆ ํ† ํ”ฝ

Step 2: Fetcher ๊ตฌํ˜„

src/fetchers/ ๋””๋ ‰ํ† ๋ฆฌ์— ์ƒˆ fetcher ํŒŒ์ผ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค:

# src/fetchers/crypto.py

"""Fetch cryptocurrency news from RSS sources."""

import logging
import re
import feedparser
from src.models import Article, Language, Topic

logger = logging.getLogger(__name__)

# ์˜์–ด ์†Œ์Šค
EN_FEEDS = [
    ("CoinDesk", "https://www.coindesk.com/arc/outboundfeeds/rss/"),
]

# ํ•œ๊ตญ์–ด ์†Œ์Šค
KR_FEEDS = [
    ("Google ์ฝ”์ธ", "https://news.google.com/rss/search?q=๋น„ํŠธ์ฝ”์ธ+์ฝ”์ธ+๊ฐ€์ƒํ™”ํ&hl=ko&gl=KR&ceid=KR:ko"),
]

# ํ‚ค์›Œ๋“œ ํ•„ํ„ฐ
KEYWORDS = re.compile(
    r"\b(bitcoin|crypto|ethereum|blockchain|defi|nft|token|altcoin)\b",
    re.IGNORECASE,
)


def fetch(language: Language = Language.EN) -> list[Article]:
    """Fetch crypto news articles."""
    feeds = KR_FEEDS if language == Language.KO else EN_FEEDS
    articles = []
    
    for source_name, url in feeds:
        feed = feedparser.parse(url)
        for entry in feed.entries[:20]:
            title = entry.get("title", "")
            link = entry.get("link", "")
            
            if not title or not link:
                continue
            
            # ํ‚ค์›Œ๋“œ ํ•„ํ„ฐ๋ง (์˜์–ด ์†Œ์Šค๋งŒ)
            if language == Language.EN and not KEYWORDS.search(title):
                continue
            
            articles.append(Article(
                title=title,
                url=link,
                hn_url="",
                score=0,
                comment_count=0,
                topic=Topic.CRYPTO,
            ))
    
    # ์ค‘๋ณต ์ œ๊ฑฐ (URL ๊ธฐ์ค€)
    seen = set()
    unique = []
    for a in articles:
        if a.url not in seen:
            seen.add(a.url)
            unique.append(a)
    
    return unique[:5]  # ์ƒ์œ„ 5๊ฐœ

Step 3: Fetcher ๋“ฑ๋ก

src/fetchers/__init__.py์— ์ƒˆ fetcher๋ฅผ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค:

from src.fetchers.crypto import fetch as fetch_crypto

FETCHER_MAP = {
    Topic.TECH: fetch_tech,
    Topic.STOCKS: fetch_stocks,
    Topic.REALESTATE: fetch_realestate,
    Topic.CRYPTO: fetch_crypto,  # ์ƒˆ ์ถ”๊ฐ€
}

# ์–ธ์–ด ์˜์กด์  ํ† ํ”ฝ ๋ชฉ๋ก์— ์ถ”๊ฐ€ (์–ธ์–ด๋ณ„ ๋‹ค๋ฅธ ์†Œ์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ)
LANGUAGE_DEPENDENT_TOPICS = {Topic.STOCKS, Topic.REALESTATE, Topic.CRYPTO}

Step 4: ๊ตฌ๋… ํŽ˜์ด์ง€ ์—…๋ฐ์ดํŠธ

docs/index.html๊ณผ Cloudflare Worker์˜ ALLOWED_TOPICS์— ์ƒˆ ํ† ํ”ฝ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค:

// worker/src/index.js
const ALLOWED_TOPICS = ['tech', 'stocks', 'realestate', 'crypto'];

Step 5: Workflow ์—…๋ฐ์ดํŠธ

subscribers.yml์˜ ํ† ํ”ฝ ์ฒดํฌ๋ฐ•์Šค์— ์ƒˆ ํ•ญ๋ชฉ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

2. ๊ธฐ์กด ํ† ํ”ฝ์— ์†Œ์Šค ์ถ”๊ฐ€

๊ธฐ์กด fetcher์— RSS ํ”ผ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์€ ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด, ํ•œ๊ตญ์–ด ์ฃผ์‹ ๋‰ด์Šค์— ๋„ค์ด๋ฒ„ ๊ธˆ์œต RSS๋ฅผ ์ถ”๊ฐ€ํ•˜๋ ค๋ฉด:

# src/fetchers/stocks.py

KR_STOCK_FEEDS = [
    ("Google ์ฃผ์‹+์ฝ”์Šคํ”ผ", "https://news.google.com/rss/search?q=..."),
    ("Google ์ฆ์‹œ+์ฃผ๊ฐ€", "https://news.google.com/rss/search?q=..."),
    ("๋„ค์ด๋ฒ„ ๊ธˆ์œต", "https://rss.news.naver.com/..."),  # ์ƒˆ ์ถ”๊ฐ€
]

3. Fetcher ๊ตฌํ˜„ ๊ฐ€์ด๋“œ๋ผ์ธ

ํ•„์ˆ˜ ์ธํ„ฐํŽ˜์ด์Šค

def fetch(language: Language = Language.EN) -> list[Article]:
    """
    Args:
        language: ๊ตฌ๋…์ž ์–ธ์–ด (Language.KO ๋˜๋Š” Language.EN)
    
    Returns:
        list[Article]: ์ตœ๋Œ€ 5๊ฐœ์˜ ๊ธฐ์‚ฌ ๋ชฉ๋ก
    """

Article ๋ฐ์ดํ„ฐ ๋ชจ๋ธ

@dataclass
class Article:
    title: str           # ๊ธฐ์‚ฌ ์ œ๋ชฉ
    url: str             # ์›๋ณธ ๊ธฐ์‚ฌ URL
    hn_url: str          # Hacker News URL (HN์ด ์•„๋‹Œ ๊ฒฝ์šฐ ๋นˆ ๋ฌธ์ž์—ด)
    score: int           # ์ ์ˆ˜/์ธ๊ธฐ๋„ (์—†์œผ๋ฉด 0)
    comment_count: int   # ๋Œ“๊ธ€ ์ˆ˜ (์—†์œผ๋ฉด 0)
    topic: Topic         # ํ† ํ”ฝ
    summary: str = ""    # ์š”์•ฝ (summarizer๊ฐ€ ์ฑ„์›€)

๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค

  1. Rate Limiting โ€” API ํ˜ธ์ถœ์— ์ ์ ˆํ•œ ๋”œ๋ ˆ์ด/์žฌ์‹œ๋„ ๋กœ์ง ์ถ”๊ฐ€
  2. ํ‚ค์›Œ๋“œ ํ•„ํ„ฐ๋ง โ€” ๊ด€๋ จ ์—†๋Š” ๊ธฐ์‚ฌ ํ•„ํ„ฐ๋ง์œผ๋กœ ํ’ˆ์งˆ ์œ ์ง€
  3. ์—๋Ÿฌ ํ•ธ๋“ค๋ง โ€” ์†Œ์Šค ๋ถˆ๊ฐ€ ์‹œ ๋นˆ ๋ฆฌ์ŠคํŠธ ๋ฐ˜ํ™˜ (์ „์ฒด ํŒŒ์ดํ”„๋ผ์ธ ์ค‘๋‹จ ๋ฐฉ์ง€)
  4. ์ค‘๋ณต ์ œ๊ฑฐ โ€” URL ๊ธฐ์ค€์œผ๋กœ ์ค‘๋ณต ๊ธฐ์‚ฌ ์ œ๊ฑฐ
  5. ๊ฒฐ๊ณผ ์ œํ•œ โ€” ์ตœ๋Œ€ 5~10๊ฐœ๋กœ ์ œํ•œ (์ด๋ฉ”์ผ ๊ธธ์ด ๊ด€๋ฆฌ)
  6. ๋กœ๊น… โ€” logger.info/warning/error๋กœ ์ ์ ˆํ•œ ๋กœ๊น…

ํ…Œ์ŠคํŠธ ๋ฐฉ๋ฒ•

# ํŠน์ • fetcher๋งŒ ํ…Œ์ŠคํŠธ
python -c "
from src.fetchers.crypto import fetch
from src.models import Language
articles = fetch(Language.KO)
for a in articles:
    print(f'{a.title} โ€” {a.url}')
"