From 7a03fb984ababef1cc94dea0ac15fb8021d0f7c7 Mon Sep 17 00:00:00 2001 From: JOUNGWOOK KWON Date: Mon, 30 Mar 2026 12:16:14 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=85=94=EB=A0=88=EA=B7=B8=EB=9E=A8=20?= =?UTF-8?q?=EC=9D=B8=EB=9D=BC=EC=9D=B8=20=EB=B2=84=ED=8A=BC=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=8A=B9=EC=9D=B8/=EA=B1=B0=EB=B6=80=20(=ED=84=B0?= =?UTF-8?q?=EC=B9=98=20=ED=95=9C=20=EB=B2=88=EC=9C=BC=EB=A1=9C=20=EB=B0=9C?= =?UTF-8?q?=ED=96=89)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /write 완료 시 미리보기 + [승인 발행] [거부] 인라인 버튼 표시 - /pending 목록도 각 글마다 인라인 버튼 포함 - 버튼 클릭 → 즉시 발행/거부 처리, 메시지 업데이트 - 기존 /approve, /reject 명령어도 유지 Co-Authored-By: Claude Opus 4.6 --- bots/scheduler.py | 86 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 77 insertions(+), 9 deletions(-) diff --git a/bots/scheduler.py b/bots/scheduler.py index 24c6154..8f0cba7 100644 --- a/bots/scheduler.py +++ b/bots/scheduler.py @@ -21,8 +21,8 @@ ensure_project_runtime( from apscheduler.schedulers.asyncio import AsyncIOScheduler from dotenv import load_dotenv -from telegram import Update -from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import Application, CommandHandler, MessageHandler, CallbackQueryHandler, filters, ContextTypes import anthropic import re @@ -683,7 +683,31 @@ async def cmd_write(update: Update, context: ContextTypes.DEFAULT_TYPE): 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 으로 검토하세요.") + # pending_review에서 방금 작성된 글 찾기 + pending_dir = DATA_DIR / 'pending_review' + pending_file = pending_dir / topic_file.name + if pending_file.exists(): + article = json.loads(pending_file.read_text(encoding='utf-8')) + title = article.get('title', '')[:50] + corner = article.get('corner', '') + body_preview = article.get('body', '')[:200].replace('<', '<').replace('>', '>') + # 인라인 버튼으로 승인/거부 + keyboard = InlineKeyboardMarkup([ + [ + InlineKeyboardButton("✅ 승인 발행", callback_data=f"approve:{topic_file.name}"), + InlineKeyboardButton("🗑 거부", callback_data=f"reject:{topic_file.name}"), + ] + ]) + await update.message.reply_text( + f"📝 [수동 검토 필요]\n\n" + f"{title}\n" + f"코너: {corner}\n\n" + f"미리보기:\n{body_preview}...\n", + parse_mode='HTML', + reply_markup=keyboard, + ) + else: + await update.message.reply_text("✅ 완료! /pending 으로 검토하세요.") except Exception as e: await update.message.reply_text(f"❌ 글 작성 오류: {e}") @@ -721,12 +745,28 @@ async def cmd_pending(update: Update, context: ContextTypes.DEFAULT_TYPE): if not pending: await update.message.reply_text("수동 검토 대기 글이 없습니다.") return - lines = [f"🔍 수동 검토 대기 ({len(pending)}개):"] - for i, item in enumerate(pending[:5], 1): - lines.append(f" {i}. [{item.get('corner','')}] {item.get('title','')[:50]}") - lines.append(f" 사유: {item.get('pending_reason','')}") - lines.append("\n/approve [번호] /reject [번호]") - await update.message.reply_text('\n'.join(lines)) + for i, item in enumerate(pending[:10], 1): + filepath = item.get('_filepath', '') + filename = Path(filepath).name if filepath else '' + title = item.get('title', '')[:50] + corner = item.get('corner', '') + reason = item.get('pending_reason', '') + body_preview = item.get('body', '')[:150].replace('<', '<').replace('>', '>') + keyboard = InlineKeyboardMarkup([ + [ + InlineKeyboardButton("✅ 승인 발행", callback_data=f"approve:{filename}"), + InlineKeyboardButton("🗑 거부", callback_data=f"reject:{filename}"), + ] + ]) + await update.message.reply_text( + f"🔍 [{i}/{len(pending)}] 수동 검토 대기\n\n" + f"{title}\n" + f"코너: {corner}\n" + f"사유: {reason}\n\n" + f"{body_preview}...", + parse_mode='HTML', + reply_markup=keyboard, + ) async def cmd_approve(update: Update, context: ContextTypes.DEFAULT_TYPE): @@ -763,6 +803,33 @@ async def cmd_reject(update: Update, context: ContextTypes.DEFAULT_TYPE): await update.message.reply_text(f"🗑 거부 완료: {pending[idx].get('title','')}") +async def callback_approve_reject(update: Update, context: ContextTypes.DEFAULT_TYPE): + """인라인 버튼 콜백: 승인/거부""" + query = update.callback_query + await query.answer() + data = query.data # "approve:filename.json" or "reject:filename.json" + action, filename = data.split(':', 1) + pending_dir = DATA_DIR / 'pending_review' + filepath = pending_dir / filename + + if not filepath.exists(): + await query.edit_message_text("⚠️ 해당 글을 찾을 수 없습니다. (이미 처리됨)") + return + + sys.path.insert(0, str(BASE_DIR / 'bots')) + import publisher_bot + + if action == 'approve': + success = publisher_bot.approve_pending(str(filepath)) + if success: + await query.edit_message_text(f"✅ 발행 완료!\n\n{query.message.text_html or query.message.text}", parse_mode='HTML') + else: + await query.edit_message_text("❌ 발행 실패. 로그를 확인하세요.") + elif action == 'reject': + publisher_bot.reject_pending(str(filepath)) + await query.edit_message_text(f"🗑 거부 완료\n\n{query.message.text_html or query.message.text}", parse_mode='HTML') + + async def cmd_report(update: Update, context: ContextTypes.DEFAULT_TYPE): await update.message.reply_text("주간 리포트 생성 중...") sys.path.insert(0, str(BASE_DIR / 'bots')) @@ -1249,6 +1316,7 @@ async def main(): app.add_handler(CommandHandler('approve', cmd_approve)) app.add_handler(CommandHandler('reject', cmd_reject)) 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('topics', cmd_show_topics)) app.add_handler(CommandHandler('convert', cmd_convert))