6 Commits

Author SHA1 Message Date
JOUNGWOOK KWON e3c963a014 feat: 영문 RSS 글감 자동 번역+재작성 지원
- 수집 시 영문 소스 자동 감지 (한국어 비율 5% 미만)
- 영문 글감 글쓰기 프롬프트에 번역+한국맥락 재작성 지시 추가
- 한국 시장 비교, 국내 대안 서비스 언급 유도
- 제목도 한국어로 새로 작성하도록 지시

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 11:53:01 +09:00
JOUNGWOOK KWON af57c3500c fix: 수집 필터 완화 — 영문 RSS 살리기 + 코너 자동배정 + 클릭베이트 완화
- 영문 RSS(카테고리 지정됨)에 한국관련성 기본 10점 부여 (즉시폐기 방지)
- korean_relevance 키워드에 AI/GPT/Apple/Netflix 등 글로벌 키워드 추가
- 키워드 매칭을 case-insensitive로 변경
- RSS 카테고리를 corner로 직접 배정 (쉬운세상 대신 실제 라벨)
- 클릭베이트 필터에서 충격/대박/레전드/역대급 제거 (TV뉴스 과다 필터링)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 11:50:47 +09:00
JOUNGWOOK KWON 0783775cdd feat: RSS 소스 17개 추가 — TV로보는세상 6개 + 기존 카테고리 보강
- TV로보는세상: 연합뉴스연예, 한경연예, MBC, TV리포트, OSEN, 스포츠조선
- AI인사이트: Google News AI, Ars Technica
- 여행맛집: Google News 여행맛집, 마이리얼트립
- 제품리뷰: 뽐뿌, Wired
- 앱추천: 9to5Mac, Android Authority
- 재테크: 조선비즈, 이데일리
- x_keywords: TV/드라마/넷플릭스 키워드 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 11:48:11 +09:00
JOUNGWOOK KWON 2c80ed1a52 chore: 라벨 변경 — 재테크절약→재테크, TV로보는세상 추가
blogs.json, sources.json 라벨을 블로그 메뉴와 일치시킴

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 11:46:23 +09:00
JOUNGWOOK KWON 9cf1f44a8b fix: /collect, /topics 결과에 글 번호 표시 추가
수집 완료 후 바로 번호 포함 목록을 보여줘서
/write [번호]로 바로 글 작성할 수 있도록 개선

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 11:42:28 +09:00
JOUNGWOOK KWON 9f68133217 feat: /collect, /write 텔레그램 명령어 추가 + eli 페르소나 적용
- cmd_collect: 즉시 글감 수집
- cmd_write [번호] [방향]: 특정 글감 글 작성 + auto pending
- _publish_next(): originals → pending_review 자동 이동
- _call_openclaw: direction 파라미터 지원
- 글쓰기 시스템 프롬프트 eli 블로그 페르소나로 변경
- 기본 코너: 쉬운세상 → AI인사이트

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 11:27:02 +09:00
5 changed files with 166 additions and 26 deletions
+21 -6
View File
@@ -95,7 +95,7 @@ def calc_freshness_score(published_at: datetime | None, max_score: int = 20) ->
return int(max_score * ratio) return int(max_score * ratio)
def calc_korean_relevance(text: str, rules: dict) -> int: def calc_korean_relevance(text: str, rules: dict, rss_category: str = '') -> int:
"""한국 독자 관련성 점수""" """한국 독자 관련성 점수"""
max_score = rules['scoring']['korean_relevance']['max'] max_score = rules['scoring']['korean_relevance']['max']
keywords = rules['scoring']['korean_relevance']['keywords'] keywords = rules['scoring']['korean_relevance']['keywords']
@@ -107,11 +107,14 @@ def calc_korean_relevance(text: str, rules: dict) -> int:
base = 15 # 한국어 텍스트면 기본 15점 base = 15 # 한국어 텍스트면 기본 15점
elif korean_ratio >= 0.05: elif korean_ratio >= 0.05:
base = 8 base = 8
elif rss_category:
# RSS 카테고리가 지정된 영문 소스는 큐레이션된 것이므로 기본점수 부여
base = 10
else: else:
base = 0 base = 0
# 브랜드/지역 키워드 보너스 # 브랜드/지역 키워드 보너스
matched = sum(1 for kw in keywords if kw in text) matched = sum(1 for kw in keywords if kw.lower() in text.lower())
bonus = min(matched * 5, max_score - base) bonus = min(matched * 5, max_score - base)
return min(base + bonus, max_score) return min(base + bonus, max_score)
@@ -199,7 +202,11 @@ def apply_discard_rules(item: dict, rules: dict, published_titles: list[str]) ->
def assign_corner(item: dict, topic_type: str) -> str: def assign_corner(item: dict, topic_type: str) -> str:
"""글감에 코너 배정""" """글감에 코너 배정 — RSS 카테고리가 있으면 우선 사용"""
rss_cat = item.get('_rss_category', '')
if rss_cat:
return rss_cat
title = item.get('topic', '').lower() title = item.get('topic', '').lower()
source = item.get('source', 'rss').lower() source = item.get('source', 'rss').lower()
@@ -227,7 +234,7 @@ def calculate_quality_score(item: dict, rules: dict) -> int:
except Exception: except Exception:
pass pass
kr_score = calc_korean_relevance(text, rules) kr_score = calc_korean_relevance(text, rules, rss_category=item.get('_rss_category', ''))
fresh_score = calc_freshness_score(pub_at) fresh_score = calc_freshness_score(pub_at)
# search_demand: pytrends 연동 후 실제값 사용 (RSS 기본값 12) # search_demand: pytrends 연동 후 실제값 사용 (RSS 기본값 12)
search_score = item.get('search_demand_score', 12) search_score = item.get('search_demand_score', 12)
@@ -379,9 +386,15 @@ def collect_rss_feeds(sources_cfg: dict) -> list[dict]:
pub_at = None pub_at = None
if hasattr(entry, 'published_parsed') and entry.published_parsed: if hasattr(entry, 'published_parsed') and entry.published_parsed:
pub_at = datetime(*entry.published_parsed[:6], tzinfo=timezone.utc).isoformat() pub_at = datetime(*entry.published_parsed[:6], tzinfo=timezone.utc).isoformat()
title_text = entry.get('title', '')
desc_text = entry.get('summary', '') or entry.get('description', '')
# 한국어 문자가 거의 없으면 영문 소스로 판단
combined = title_text + desc_text
kr_chars = sum(1 for c in combined if '\uac00' <= c <= '\ud7a3')
is_english = kr_chars / max(len(combined), 1) < 0.05
items.append({ items.append({
'topic': entry.get('title', ''), 'topic': title_text,
'description': entry.get('summary', '') or entry.get('description', ''), 'description': desc_text,
'source': 'rss', 'source': 'rss',
'source_name': feed_cfg.get('name', ''), 'source_name': feed_cfg.get('name', ''),
'source_url': entry.get('link', ''), 'source_url': entry.get('link', ''),
@@ -389,6 +402,8 @@ def collect_rss_feeds(sources_cfg: dict) -> list[dict]:
'search_demand_score': 8, 'search_demand_score': 8,
'topic_type': 'trending', 'topic_type': 'trending',
'_trust_override': trust, '_trust_override': trust,
'_rss_category': feed_cfg.get('category', ''),
'is_english': is_english,
}) })
except Exception as e: except Exception as e:
logger.warning(f"RSS 수집 실패 ({url}): {e}") logger.warning(f"RSS 수집 실패 ({url}): {e}")
+116 -11
View File
@@ -151,16 +151,25 @@ def _safe_slug(text: str) -> str:
def _build_openclaw_prompt(topic_data: dict) -> tuple[str, str]: def _build_openclaw_prompt(topic_data: dict) -> tuple[str, str]:
topic = topic_data.get('topic', '').strip() topic = topic_data.get('topic', '').strip()
corner = topic_data.get('corner', '쉬운세상').strip() or '쉬운세상' corner = topic_data.get('corner', 'AI인사이트').strip() or 'AI인사이트'
description = topic_data.get('description', '').strip() description = topic_data.get('description', '').strip()
source = topic_data.get('source_url') or topic_data.get('source') or '' source = topic_data.get('source_url') or topic_data.get('source') or ''
published_at = topic_data.get('published_at', '') published_at = topic_data.get('published_at', '')
is_english = topic_data.get('is_english', False)
system = ( system = (
"당신은 The 4th Path 블로그 엔진의 전문 에디터다. " "당신은 'AI? 그게 뭔데?' 블로그의 편집자 eli다. "
"비전문가도 쉽게 읽을 수 있는 친근한 톤으로 글을 쓴다. "
"반드시 아래 섹션 헤더 형식만 사용해 완성된 Blogger-ready HTML 원고를 출력하라. " "반드시 아래 섹션 헤더 형식만 사용해 완성된 Blogger-ready HTML 원고를 출력하라. "
"본문(BODY)은 HTML로 작성하고, KEY_POINTS는 3줄 이내로 작성한다." "본문(BODY)은 HTML로 작성하고, KEY_POINTS는 3줄 이내로 작성한다."
) )
prompt = f"""다음 글감을 바탕으로 한국어 블로그 원고를 작성해줘. if is_english:
system += (
" 영문 원문을 단순 번역하지 말고, 한국 독자 관점에서 재해석하여 작성하라. "
"한국 시장/사용자에게 어떤 의미인지, 국내 대안이나 비교 서비스가 있다면 함께 언급하라. "
"제목도 한국어로 매력적으로 새로 작성하라."
)
lang_note = "\n⚠️ 영문 원문입니다. 단순 번역이 아닌, 한국 독자 맥락으로 재작성해주세요." if is_english else ""
prompt = f"""다음 글감을 바탕으로 한국어 블로그 원고를 작성해줘.{lang_note}
주제: {topic} 주제: {topic}
코너: {corner} 코너: {corner}
@@ -205,8 +214,8 @@ def _build_openclaw_prompt(topic_data: dict) -> tuple[str, str]:
return system, prompt return system, prompt
def _call_openclaw(topic_data: dict, output_path: Path): def _call_openclaw(topic_data: dict, output_path: Path, direction: str = ''):
logger.info(f"OpenClaw 작성 요청: {topic_data.get('topic', '')}") logger.info(f" 작성 요청: {topic_data.get('topic', '')}")
sys.path.insert(0, str(BASE_DIR)) sys.path.insert(0, str(BASE_DIR))
sys.path.insert(0, str(BASE_DIR / 'bots')) sys.path.insert(0, str(BASE_DIR / 'bots'))
@@ -214,18 +223,20 @@ def _call_openclaw(topic_data: dict, output_path: Path):
from article_parser import parse_output from article_parser import parse_output
system, prompt = _build_openclaw_prompt(topic_data) system, prompt = _build_openclaw_prompt(topic_data)
if direction:
prompt += f"\n\n운영자 지시사항: {direction}"
writer = EngineLoader().get_writer() writer = EngineLoader().get_writer()
raw_output = writer.write(prompt, system=system).strip() raw_output = writer.write(prompt, system=system).strip()
if not raw_output: if not raw_output:
raise RuntimeError('OpenClaw writer 응답이 비어 있습니다.') raise RuntimeError('Writer 응답이 비어 있습니다.')
article = parse_output(raw_output) article = parse_output(raw_output)
if not article: if not article:
raise RuntimeError('OpenClaw writer 출력 파싱 실패') raise RuntimeError('Writer 출력 파싱 실패')
article.setdefault('title', topic_data.get('topic', '').strip()) article.setdefault('title', topic_data.get('topic', '').strip())
article['slug'] = article.get('slug') or _safe_slug(article['title']) article['slug'] = article.get('slug') or _safe_slug(article['title'])
article['corner'] = article.get('corner') or topic_data.get('corner', '쉬운세상') article['corner'] = article.get('corner') or topic_data.get('corner', 'AI인사이트')
article['topic'] = topic_data.get('topic', '') article['topic'] = topic_data.get('topic', '')
article['description'] = topic_data.get('description', '') article['description'] = topic_data.get('description', '')
article['quality_score'] = topic_data.get('quality_score', 0) article['quality_score'] = topic_data.get('quality_score', 0)
@@ -239,7 +250,28 @@ def _call_openclaw(topic_data: dict, output_path: Path):
json.dumps(article, ensure_ascii=False, indent=2), json.dumps(article, ensure_ascii=False, indent=2),
encoding='utf-8', encoding='utf-8',
) )
logger.info(f"OpenClaw 원고 저장 완료: {output_path.name}") logger.info(f"원고 저장 완료: {output_path.name}")
def _publish_next():
"""originals/ → pending_review/ 이동 (안전장치 체크)"""
sys.path.insert(0, str(BASE_DIR / 'bots'))
import publisher_bot
originals_dir = DATA_DIR / 'originals'
pending_dir = DATA_DIR / 'pending_review'
pending_dir.mkdir(exist_ok=True)
safety_cfg = publisher_bot.load_config('safety_keywords.json')
for f in sorted(originals_dir.glob('*.json')):
try:
article = json.loads(f.read_text(encoding='utf-8'))
needs_review, reason = publisher_bot.check_safety(article, safety_cfg)
article['pending_reason'] = reason or '수동 승인 필요'
dest = pending_dir / f.name
dest.write_text(json.dumps(article, ensure_ascii=False, indent=2), encoding='utf-8')
f.unlink()
logger.info(f"검토 대기로 이동: {f.name} ({reason})")
except Exception as e:
logger.error(f"publish_next 오류 ({f.name}): {e}")
def job_convert(): def job_convert():
@@ -577,6 +609,76 @@ async def cmd_resume_publish(update: Update, context: ContextTypes.DEFAULT_TYPE)
await update.message.reply_text("🟢 발행이 재개되었습니다.") await update.message.reply_text("🟢 발행이 재개되었습니다.")
async def cmd_collect(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text("🔄 글감 수집을 시작합니다...")
loop = asyncio.get_event_loop()
try:
await loop.run_in_executor(None, job_collector)
topics_dir = DATA_DIR / 'topics'
today = datetime.now().strftime('%Y%m%d')
files = sorted(topics_dir.glob(f'{today}_*.json'))
if not files:
await update.message.reply_text("✅ 수집 완료! 오늘 수집된 글감이 없습니다.")
return
lines = [f"✅ 수집 완료! 오늘 글감 {len(files)}개:"]
for i, f in enumerate(files[:15], 1):
try:
data = json.loads(f.read_text(encoding='utf-8'))
lines.append(f" {i}. [{data.get('corner','')}] {data.get('topic','')[:50]}")
except Exception:
pass
lines.append("\n✍️ /write [번호] 로 글 작성")
await update.message.reply_text('\n'.join(lines))
except Exception as e:
await update.message.reply_text(f"❌ 수집 오류: {e}")
async def cmd_write(update: Update, context: ContextTypes.DEFAULT_TYPE):
topics_dir = DATA_DIR / 'topics'
today = datetime.now().strftime('%Y%m%d')
files = sorted(topics_dir.glob(f'{today}_*.json'))
if not files:
await update.message.reply_text("오늘 수집된 글감이 없습니다. /collect 먼저 실행하세요.")
return
args = context.args
if not args:
lines = ["📋 글감 목록 (번호를 선택하세요):"]
for i, f in enumerate(files[:10], 1):
try:
data = json.loads(f.read_text(encoding='utf-8'))
lines.append(f" {i}. [{data.get('corner','')}] {data.get('topic','')[:50]}")
except Exception:
pass
lines.append("\n사용법: /write [번호] [방향(선택)]")
await update.message.reply_text('\n'.join(lines))
return
try:
idx = int(args[0]) - 1
if idx < 0 or idx >= len(files):
await update.message.reply_text(f"❌ 1~{len(files)} 사이 번호를 입력하세요.")
return
except ValueError:
await update.message.reply_text("❌ 숫자를 입력하세요. 예: /write 1")
return
topic_file = files[idx]
topic_data = json.loads(topic_file.read_text(encoding='utf-8'))
direction = ' '.join(args[1:]) if len(args) > 1 else ''
draft_path = DATA_DIR / 'originals' / topic_file.name
(DATA_DIR / 'originals').mkdir(exist_ok=True)
await update.message.reply_text(
f"✍️ 글 작성 중...\n주제: {topic_data.get('topic','')[:50]}"
+ (f"\n방향: {direction}" if direction else "")
)
loop = asyncio.get_event_loop()
try:
await loop.run_in_executor(None, _call_openclaw, topic_data, draft_path, direction)
# 자동으로 pending_review로 이동
await loop.run_in_executor(None, _publish_next)
await update.message.reply_text("✅ 완료! /pending 으로 검토하세요.")
except Exception as e:
await update.message.reply_text(f"❌ 글 작성 오류: {e}")
async def cmd_show_topics(update: Update, context: ContextTypes.DEFAULT_TYPE): async def cmd_show_topics(update: Update, context: ContextTypes.DEFAULT_TYPE):
topics_dir = DATA_DIR / 'topics' topics_dir = DATA_DIR / 'topics'
today = datetime.now().strftime('%Y%m%d') today = datetime.now().strftime('%Y%m%d')
@@ -585,12 +687,13 @@ async def cmd_show_topics(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text("오늘 수집된 글감이 없습니다.") await update.message.reply_text("오늘 수집된 글감이 없습니다.")
return return
lines = [f"📋 오늘 수집된 글감 ({len(files)}개):"] lines = [f"📋 오늘 수집된 글감 ({len(files)}개):"]
for f in files[:10]: for i, f in enumerate(files[:15], 1):
try: try:
data = json.loads(f.read_text(encoding='utf-8')) data = json.loads(f.read_text(encoding='utf-8'))
lines.append(f" [{data.get('quality_score',0)}점][{data.get('corner','')}] {data.get('topic','')[:50]}") lines.append(f" {i}. [{data.get('quality_score',0)}점][{data.get('corner','')}] {data.get('topic','')[:50]}")
except Exception: except Exception:
pass pass
lines.append("\n✍️ /write [번호] 로 글 작성")
await update.message.reply_text('\n'.join(lines)) await update.message.reply_text('\n'.join(lines))
@@ -1124,6 +1227,8 @@ async def main():
# 발행 관련 # 발행 관련
app.add_handler(CommandHandler('status', cmd_status)) app.add_handler(CommandHandler('status', cmd_status))
app.add_handler(CommandHandler('collect', cmd_collect))
app.add_handler(CommandHandler('write', cmd_write))
app.add_handler(CommandHandler('approve', cmd_approve)) app.add_handler(CommandHandler('approve', cmd_approve))
app.add_handler(CommandHandler('reject', cmd_reject)) app.add_handler(CommandHandler('reject', cmd_reject))
app.add_handler(CommandHandler('pending', cmd_pending)) app.add_handler(CommandHandler('pending', cmd_pending))
+1 -1
View File
@@ -9,7 +9,7 @@
"tagline": "어렵지 않아요, 그냥 읽어봐요", "tagline": "어렵지 않아요, 그냥 읽어봐요",
"active": true, "active": true,
"phase": 1, "phase": 1,
"labels": ["AI인사이트", "여행맛집", "스타트업", "제품리뷰", "생활꿀팁", "앱추천", "재테크절약", "팩트체크"] "labels": ["AI인사이트", "여행맛집", "스타트업", "TV로보는세상", "제품리뷰", "생활꿀팁", "앱추천", "재테크", "팩트체크"]
} }
] ]
} }
+2 -2
View File
@@ -4,7 +4,7 @@
"korean_relevance": { "korean_relevance": {
"max": 30, "max": 30,
"description": "한국 독자 관련성", "description": "한국 독자 관련성",
"keywords": ["한국", "국내", "한글", "카카오", "네이버", "쿠팡", "삼성", "LG", "현대", "기아", "배달", "토스", "당근", "야놀자"] "keywords": ["한국", "국내", "한글", "카카오", "네이버", "쿠팡", "삼성", "LG", "현대", "기아", "배달", "토스", "당근", "야놀자", "AI", "GPT", "ChatGPT", "Claude", "Gemini", "Apple", "Google", "iPhone", "갤럭시", "Netflix", "넷플릭스", "YouTube"]
}, },
"freshness": { "freshness": {
"max": 20, "max": 20,
@@ -63,7 +63,7 @@
{ {
"id": "clickbait", "id": "clickbait",
"description": "클릭베이트성 주제", "description": "클릭베이트성 주제",
"patterns": ["충격", "경악", "난리", "ㅋㅋ", "ㅠㅠ", "대박", "레전드", "역대급"] "patterns": ["경악", "난리", "ㅋㅋ", "ㅠㅠ"]
} }
], ],
"evergreen_keywords": [ "evergreen_keywords": [
+26 -6
View File
@@ -7,6 +7,8 @@
{ "name": "테크크런치 AI", "url": "https://techcrunch.com/category/artificial-intelligence/feed/", "category": "AI인사이트", "trust_level": "high" }, { "name": "테크크런치 AI", "url": "https://techcrunch.com/category/artificial-intelligence/feed/", "category": "AI인사이트", "trust_level": "high" },
{ "name": "MIT 테크리뷰", "url": "https://www.technologyreview.com/feed/", "category": "AI인사이트", "trust_level": "high" }, { "name": "MIT 테크리뷰", "url": "https://www.technologyreview.com/feed/", "category": "AI인사이트", "trust_level": "high" },
{ "name": "전자신문", "url": "https://www.etnews.com/rss/rss.xml", "category": "AI인사이트", "trust_level": "high" }, { "name": "전자신문", "url": "https://www.etnews.com/rss/rss.xml", "category": "AI인사이트", "trust_level": "high" },
{ "name": "딥러닝 뉴스", "url": "https://news.google.com/rss/search?q=AI+인공지능&hl=ko&gl=KR&ceid=KR:ko", "category": "AI인사이트", "trust_level": "medium" },
{ "name": "Ars Technica AI", "url": "https://feeds.arstechnica.com/arstechnica/technology-lab", "category": "AI인사이트", "trust_level": "high" },
{ "name": "Bloter", "url": "https://www.bloter.net/feed", "category": "스타트업", "trust_level": "high" }, { "name": "Bloter", "url": "https://www.bloter.net/feed", "category": "스타트업", "trust_level": "high" },
{ "name": "플래텀", "url": "https://platum.kr/feed", "category": "스타트업", "trust_level": "high" }, { "name": "플래텀", "url": "https://platum.kr/feed", "category": "스타트업", "trust_level": "high" },
@@ -20,11 +22,15 @@
{ "name": "한국관광공사", "url": "https://kto.visitkorea.or.kr/rss/rss.kto", "category": "여행맛집", "trust_level": "medium" }, { "name": "한국관광공사", "url": "https://kto.visitkorea.or.kr/rss/rss.kto", "category": "여행맛집", "trust_level": "medium" },
{ "name": "대한항공 뉴스", "url": "https://www.koreanair.com/content/koreanair/global/en/footer/about-korean-air/news-and-pr/press-releases.rss.xml", "category": "여행맛집", "trust_level": "medium" }, { "name": "대한항공 뉴스", "url": "https://www.koreanair.com/content/koreanair/global/en/footer/about-korean-air/news-and-pr/press-releases.rss.xml", "category": "여행맛집", "trust_level": "medium" },
{ "name": "론리플래닛", "url": "https://www.lonelyplanet.com/news/feed", "category": "여행맛집", "trust_level": "medium" }, { "name": "론리플래닛", "url": "https://www.lonelyplanet.com/news/feed", "category": "여행맛집", "trust_level": "medium" },
{ "name": "다나와 여행", "url": "https://news.google.com/rss/search?q=국내여행+맛집&hl=ko&gl=KR&ceid=KR:ko", "category": "여행맛집", "trust_level": "medium" },
{ "name": "마이리얼트립 블로그","url": "https://blog.myrealtrip.com/feed", "category": "여행맛집", "trust_level": "medium" },
{ "name": "ITWorld Korea", "url": "https://www.itworld.co.kr/rss/feed", "category": "제품리뷰", "trust_level": "medium" }, { "name": "ITWorld Korea", "url": "https://www.itworld.co.kr/rss/feed", "category": "제품리뷰", "trust_level": "medium" },
{ "name": "디지털데일리", "url": "https://www.ddaily.co.kr/rss/rss.xml", "category": "제품리뷰", "trust_level": "medium" }, { "name": "디지털데일리", "url": "https://www.ddaily.co.kr/rss/rss.xml", "category": "제품리뷰", "trust_level": "medium" },
{ "name": "The Verge", "url": "https://www.theverge.com/rss/index.xml", "category": "제품리뷰", "trust_level": "high" }, { "name": "The Verge", "url": "https://www.theverge.com/rss/index.xml", "category": "제품리뷰", "trust_level": "high" },
{ "name": "Engadget", "url": "https://www.engadget.com/rss.xml", "category": "제품리뷰", "trust_level": "high" }, { "name": "Engadget", "url": "https://www.engadget.com/rss.xml", "category": "제품리뷰", "trust_level": "high" },
{ "name": "뽐뿌 뉴스", "url": "https://www.ppomppu.co.kr/rss.php?id=news", "category": "제품리뷰", "trust_level": "medium" },
{ "name": "Wired", "url": "https://www.wired.com/feed/rss", "category": "제품리뷰", "trust_level": "high" },
{ "name": "위키트리", "url": "https://www.wikitree.co.kr/rss/", "category": "생활꿀팁", "trust_level": "medium" }, { "name": "위키트리", "url": "https://www.wikitree.co.kr/rss/", "category": "생활꿀팁", "trust_level": "medium" },
{ "name": "오마이뉴스 라이프", "url": "https://rss2.ohmynews.com/rss/ohmyrss.xml", "category": "생활꿀팁", "trust_level": "medium" }, { "name": "오마이뉴스 라이프", "url": "https://rss2.ohmynews.com/rss/ohmyrss.xml", "category": "생활꿀팁", "trust_level": "medium" },
@@ -34,12 +40,23 @@
{ "name": "Product Hunt", "url": "https://www.producthunt.com/feed", "category": "앱추천", "trust_level": "medium" }, { "name": "Product Hunt", "url": "https://www.producthunt.com/feed", "category": "앱추천", "trust_level": "medium" },
{ "name": "테크크런치 앱", "url": "https://techcrunch.com/category/apps/feed/", "category": "앱추천", "trust_level": "high" }, { "name": "테크크런치 앱", "url": "https://techcrunch.com/category/apps/feed/", "category": "앱추천", "trust_level": "high" },
{ "name": "앱스토리", "url": "https://www.appstory.co.kr/rss/rss.xml", "category": "앱추천", "trust_level": "medium" }, { "name": "앱스토리", "url": "https://www.appstory.co.kr/rss/rss.xml", "category": "앱추천", "trust_level": "medium" },
{ "name": "9to5Mac", "url": "https://9to5mac.com/feed/", "category": "앱추천", "trust_level": "high" },
{ "name": "Android Authority", "url": "https://www.androidauthority.com/feed/", "category": "앱추천", "trust_level": "high" },
{ "name": "매일경제 IT", "url": "https://rss.mk.co.kr/rss/30000001/", "category": "재테크절약", "trust_level": "high" }, { "name": "매일경제 IT", "url": "https://rss.mk.co.kr/rss/30000001/", "category": "재테크", "trust_level": "high" },
{ "name": "머니투데이", "url": "https://rss.mt.co.kr/news/mt_news.xml", "category": "재테크절약", "trust_level": "high" }, { "name": "머니투데이", "url": "https://rss.mt.co.kr/news/mt_news.xml", "category": "재테크", "trust_level": "high" },
{ "name": "한국경제 재테크", "url": "https://www.hankyung.com/feed/finance", "category": "재테크절약", "trust_level": "high" }, { "name": "한국경제 재테크", "url": "https://www.hankyung.com/feed/finance", "category": "재테크", "trust_level": "high" },
{ "name": "뱅크샐러드 블로그", "url": "https://blog.banksalad.com/rss.xml", "category": "재테크절약", "trust_level": "medium" }, { "name": "뱅크샐러드 블로그", "url": "https://blog.banksalad.com/rss.xml", "category": "재테크", "trust_level": "medium" },
{ "name": "서울경제", "url": "https://www.sedaily.com/rss/rss.xml", "category": "재테크절약", "trust_level": "high" }, { "name": "서울경제", "url": "https://www.sedaily.com/rss/rss.xml", "category": "재테크", "trust_level": "high" },
{ "name": "조선비즈 경제", "url": "https://biz.chosun.com/site/data/rss/rss.xml", "category": "재테크", "trust_level": "high" },
{ "name": "이데일리 금융", "url": "https://www.edaily.co.kr/rss/rss.asp?media_key=20", "category": "재테크", "trust_level": "high" },
{ "name": "스포츠조선 연예", "url": "https://sports.chosun.com/site/data/rss/rss.xml", "category": "TV로보는세상","trust_level": "medium" },
{ "name": "연합뉴스 연예", "url": "https://www.yna.co.kr/rss/entertainment.xml", "category": "TV로보는세상","trust_level": "high" },
{ "name": "한국경제 연예", "url": "https://www.hankyung.com/feed/entertainment", "category": "TV로보는세상","trust_level": "high" },
{ "name": "MBC 연예", "url": "https://imnews.imbc.com/rss/entertainment.xml", "category": "TV로보는세상","trust_level": "high" },
{ "name": "TV리포트", "url": "https://www.tvreport.co.kr/rss/allArticle.xml", "category": "TV로보는세상","trust_level": "medium" },
{ "name": "OSEN 연예", "url": "https://www.osen.co.kr/rss/osen.xml", "category": "TV로보는세상","trust_level": "medium" },
{ "name": "연합뉴스 팩트체크", "url": "https://www.yna.co.kr/rss/factcheck.xml", "category": "팩트체크", "trust_level": "high" }, { "name": "연합뉴스 팩트체크", "url": "https://www.yna.co.kr/rss/factcheck.xml", "category": "팩트체크", "trust_level": "high" },
{ "name": "SBS 뉴스", "url": "https://news.sbs.co.kr/news/SectionRssFeed.do?sectionId=01&plink=RSSREADER", "category": "팩트체크", "trust_level": "high" }, { "name": "SBS 뉴스", "url": "https://news.sbs.co.kr/news/SectionRssFeed.do?sectionId=01&plink=RSSREADER", "category": "팩트체크", "trust_level": "high" },
@@ -56,7 +73,10 @@
"맛집 추천", "맛집 추천",
"스타트업 소식", "스타트업 소식",
"재테크 방법", "재테크 방법",
"쇼핑 추천" "쇼핑 추천",
"TV 예능 화제",
"드라마 추천",
"넷플릭스 신작"
], ],
"github_trending": { "github_trending": {
"url": "https://github.com/trending", "url": "https://github.com/trending",