From 3d6736503f841019f174e2e45929d2472443d879 Mon Sep 17 00:00:00 2001 From: JOUNGWOOK KWON Date: Mon, 30 Mar 2026 15:14:37 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20/idea=20=EB=AA=85=EB=A0=B9=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=E2=80=94=20=ED=82=A4=EC=9B=8C=EB=93=9C=EB=A1=9C=20?= =?UTF-8?q?=EA=B8=80=EA=B0=90=20=EC=9E=90=EB=8F=99=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 텔레그램에서 /idea <키워드> [카테고리] 로 글감 등록. - Google 뉴스 RSS로 관련 기사 최대 5개 자동 검색 - 첫 번째 기사에서 설명/이미지 크롤링 - 검색된 기사들을 sources로 저장 → 글 작성 시 참고 자료로 활용 - 카테고리 미지정 시 키워드 기반 자동 추정 Co-Authored-By: Claude Opus 4.6 --- bots/scheduler.py | 144 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/bots/scheduler.py b/bots/scheduler.py index 7275817..604aae5 100644 --- a/bots/scheduler.py +++ b/bots/scheduler.py @@ -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 [카테고리]""" 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))