feat: 텔레그램 인라인 버튼으로 승인/거부 (터치 한 번으로 발행)
- /write 완료 시 미리보기 + [승인 발행] [거부] 인라인 버튼 표시 - /pending 목록도 각 글마다 인라인 버튼 포함 - 버튼 클릭 → 즉시 발행/거부 처리, 메시지 업데이트 - 기존 /approve, /reject 명령어도 유지 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+77
-9
@@ -21,8 +21,8 @@ ensure_project_runtime(
|
|||||||
|
|
||||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from telegram import Update
|
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
|
||||||
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes
|
from telegram.ext import Application, CommandHandler, MessageHandler, CallbackQueryHandler, filters, ContextTypes
|
||||||
|
|
||||||
import anthropic
|
import anthropic
|
||||||
import re
|
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)
|
await loop.run_in_executor(None, _call_openclaw, topic_data, draft_path, direction)
|
||||||
# 자동으로 pending_review로 이동
|
# 자동으로 pending_review로 이동
|
||||||
await loop.run_in_executor(None, _publish_next)
|
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"<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:
|
except Exception as e:
|
||||||
await update.message.reply_text(f"❌ 글 작성 오류: {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:
|
if not pending:
|
||||||
await update.message.reply_text("수동 검토 대기 글이 없습니다.")
|
await update.message.reply_text("수동 검토 대기 글이 없습니다.")
|
||||||
return
|
return
|
||||||
lines = [f"🔍 수동 검토 대기 ({len(pending)}개):"]
|
for i, item in enumerate(pending[:10], 1):
|
||||||
for i, item in enumerate(pending[:5], 1):
|
filepath = item.get('_filepath', '')
|
||||||
lines.append(f" {i}. [{item.get('corner','')}] {item.get('title','')[:50]}")
|
filename = Path(filepath).name if filepath else ''
|
||||||
lines.append(f" 사유: {item.get('pending_reason','')}")
|
title = item.get('title', '')[:50]
|
||||||
lines.append("\n/approve [번호] /reject [번호]")
|
corner = item.get('corner', '')
|
||||||
await update.message.reply_text('\n'.join(lines))
|
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"<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):
|
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','')}")
|
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):
|
async def cmd_report(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
await update.message.reply_text("주간 리포트 생성 중...")
|
await update.message.reply_text("주간 리포트 생성 중...")
|
||||||
sys.path.insert(0, str(BASE_DIR / 'bots'))
|
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('approve', cmd_approve))
|
||||||
app.add_handler(CommandHandler('reject', cmd_reject))
|
app.add_handler(CommandHandler('reject', cmd_reject))
|
||||||
app.add_handler(CommandHandler('pending', cmd_pending))
|
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('report', cmd_report))
|
||||||
app.add_handler(CommandHandler('topics', cmd_show_topics))
|
app.add_handler(CommandHandler('topics', cmd_show_topics))
|
||||||
app.add_handler(CommandHandler('convert', cmd_convert))
|
app.add_handler(CommandHandler('convert', cmd_convert))
|
||||||
|
|||||||
Reference in New Issue
Block a user