feat: 텔레그램 인라인 버튼으로 승인/거부 (터치 한 번으로 발행)

- /write 완료 시 미리보기 + [승인 발행] [거부] 인라인 버튼 표시
- /pending 목록도 각 글마다 인라인 버튼 포함
- 버튼 클릭 → 즉시 발행/거부 처리, 메시지 업데이트
- 기존 /approve, /reject 명령어도 유지

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
JOUNGWOOK KWON
2026-03-30 12:16:14 +09:00
parent 01f95dbc6b
commit 7a03fb984a

View File

@@ -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('<', '&lt;').replace('>', '&gt;')
# 인라인 버튼으로 승인/거부
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"<b>{title}</b>\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('<', '&lt;').replace('>', '&gt;')
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"<b>{title}</b>\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))