feat: /idea 명령 추가 — 키워드로 글감 자동 생성
텔레그램에서 /idea <키워드> [카테고리] 로 글감 등록. - Google 뉴스 RSS로 관련 기사 최대 5개 자동 검색 - 첫 번째 기사에서 설명/이미지 크롤링 - 검색된 기사들을 sources로 저장 → 글 작성 시 참고 자료로 활용 - 카테고리 미지정 시 키워드 기반 자동 추정 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -718,6 +718,149 @@ async def cmd_write(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
await update.message.reply_text(f"❌ 글 작성 오류: {e}")
|
||||
|
||||
|
||||
async def cmd_idea(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""키워드/아이디어로 글감 등록: /idea <키워드> [카테고리]"""
|
||||
args = context.args
|
||||
if not args:
|
||||
await update.message.reply_text(
|
||||
"사용법: /idea <키워드 또는 주제> [카테고리]\n"
|
||||
"예: /idea 테슬라 모델Y 가격 인하\n"
|
||||
"예: /idea 여름 휴가 추천지 여행맛집"
|
||||
)
|
||||
return
|
||||
|
||||
VALID_CORNERS = {"AI인사이트", "여행맛집", "스타트업", "TV로보는세상", "제품리뷰", "생활꿀팁", "건강정보", "재테크", "팩트체크"}
|
||||
|
||||
# 마지막 인자가 카테고리인지 확인
|
||||
corner = ''
|
||||
keyword_args = list(args)
|
||||
if keyword_args[-1] in VALID_CORNERS:
|
||||
corner = keyword_args.pop()
|
||||
|
||||
keyword = ' '.join(keyword_args)
|
||||
if not keyword:
|
||||
await update.message.reply_text("❌ 키워드를 입력하세요.")
|
||||
return
|
||||
|
||||
await update.message.reply_text(f"🔎 관련 자료 검색 중...\n키워드: {keyword}")
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
try:
|
||||
topic_data = await loop.run_in_executor(None, _search_and_build_topic, keyword, corner)
|
||||
except Exception as e:
|
||||
await update.message.reply_text(f"❌ 검색 실패: {e}")
|
||||
return
|
||||
|
||||
# topics 폴더에 저장
|
||||
topics_dir = DATA_DIR / 'topics'
|
||||
topics_dir.mkdir(parents=True, exist_ok=True)
|
||||
today = datetime.now().strftime('%Y%m%d')
|
||||
ts = datetime.now().strftime('%H%M%S')
|
||||
filename = f"{today}_{ts}_idea.json"
|
||||
topic_path = topics_dir / filename
|
||||
topic_path.write_text(json.dumps(topic_data, ensure_ascii=False, indent=2), encoding='utf-8')
|
||||
|
||||
# 오늘 글감 목록에서 몇 번인지 확인
|
||||
all_files = sorted(topics_dir.glob(f'{today}_*.json'))
|
||||
idx = next((i for i, f in enumerate(all_files, 1) if f.name == filename), len(all_files))
|
||||
|
||||
# 검색 결과 요약
|
||||
sources = topic_data.get('sources', [])
|
||||
source_lines = []
|
||||
for s in sources[:3]:
|
||||
source_lines.append(f" • {s.get('title', '')[:40]}")
|
||||
|
||||
sources_text = '\n'.join(source_lines) if source_lines else " (직접 검색 결과 없음 — AI가 자체 지식으로 작성)"
|
||||
|
||||
await update.message.reply_text(
|
||||
f"✅ 글감 등록 완료! (#{idx})\n\n"
|
||||
f"주제: {topic_data.get('topic', '')}\n"
|
||||
f"카테고리: {topic_data.get('corner', '')}\n\n"
|
||||
f"📰 참고 자료:\n{sources_text}\n\n"
|
||||
f"👉 /write {idx} 로 바로 글 작성\n"
|
||||
f"👉 /write {idx} AI인사이트 로 카테고리 변경 가능"
|
||||
)
|
||||
|
||||
|
||||
def _search_and_build_topic(keyword: str, corner: str = '') -> dict:
|
||||
"""키워드로 Google 뉴스 검색 → 관련 기사 수집 → topic_data 생성"""
|
||||
import requests
|
||||
import feedparser
|
||||
from urllib.parse import quote
|
||||
|
||||
# Google 뉴스 RSS로 검색
|
||||
search_url = f"https://news.google.com/rss/search?q={quote(keyword)}&hl=ko&gl=KR&ceid=KR:ko"
|
||||
sources = []
|
||||
best_description = ''
|
||||
best_image = ''
|
||||
|
||||
try:
|
||||
feed = feedparser.parse(search_url)
|
||||
for entry in feed.entries[:5]:
|
||||
title = entry.get('title', '')
|
||||
link = entry.get('link', '')
|
||||
pub_date = entry.get('published', '')
|
||||
|
||||
# Google 뉴스 URL → 실제 기사 URL 변환
|
||||
real_url = link
|
||||
if 'news.google.com' in link:
|
||||
try:
|
||||
resp = requests.head(link, timeout=8, allow_redirects=True,
|
||||
headers={'User-Agent': 'Mozilla/5.0'})
|
||||
if resp.url and 'news.google.com' not in resp.url:
|
||||
real_url = resp.url
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
sources.append({
|
||||
'url': real_url,
|
||||
'title': title,
|
||||
'date': pub_date,
|
||||
})
|
||||
|
||||
# 첫 번째 기사에서 설명/이미지 크롤링
|
||||
if not best_description and real_url != link:
|
||||
try:
|
||||
from bs4 import BeautifulSoup
|
||||
resp = requests.get(real_url, timeout=10,
|
||||
headers={'User-Agent': 'Mozilla/5.0'})
|
||||
if resp.status_code == 200:
|
||||
soup = BeautifulSoup(resp.text, 'lxml')
|
||||
og_desc = soup.find('meta', property='og:description')
|
||||
if og_desc and og_desc.get('content'):
|
||||
best_description = og_desc['content'].strip()[:300]
|
||||
og_img = soup.find('meta', property='og:image')
|
||||
if og_img and og_img.get('content', '').startswith('http'):
|
||||
best_image = og_img['content']
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 카테고리 자동 판별
|
||||
if not corner:
|
||||
desc_for_guess = best_description or keyword
|
||||
corner = _guess_corner(keyword, desc_for_guess)
|
||||
|
||||
description = best_description or f"{keyword}에 대한 최신 정보와 분석"
|
||||
|
||||
return {
|
||||
'topic': keyword,
|
||||
'description': description,
|
||||
'source': 'idea',
|
||||
'source_name': 'Google 뉴스 검색',
|
||||
'source_url': sources[0]['url'] if sources else '',
|
||||
'published_at': datetime.now().strftime('%Y-%m-%d'),
|
||||
'corner': corner,
|
||||
'quality_score': 85,
|
||||
'search_demand_score': 9,
|
||||
'topic_type': 'trending',
|
||||
'source_image': best_image,
|
||||
'is_english': not any('\uAC00' <= c <= '\uD7A3' for c in keyword),
|
||||
'sources': sources,
|
||||
}
|
||||
|
||||
|
||||
async def cmd_topic(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""URL을 글감으로 등록: /topic <URL> [카테고리]"""
|
||||
args = context.args
|
||||
@@ -1477,6 +1620,7 @@ async def main():
|
||||
app.add_handler(CommandHandler('pending', cmd_pending))
|
||||
app.add_handler(CallbackQueryHandler(callback_approve_reject, pattern=r'^(approve|reject):'))
|
||||
app.add_handler(CommandHandler('report', cmd_report))
|
||||
app.add_handler(CommandHandler('idea', cmd_idea))
|
||||
app.add_handler(CommandHandler('topic', cmd_topic))
|
||||
app.add_handler(CommandHandler('topics', cmd_show_topics))
|
||||
app.add_handler(CommandHandler('convert', cmd_convert))
|
||||
|
||||
Reference in New Issue
Block a user