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))