✨ feat: Telegram 알림 기능 추가 및 Slack 의존성 제거
- notification-hook.sh에서 Slack 웹훅 관련 코드 제거하고 Telegram 알림으로 변경 - lotto_auto_buy.py에서 Slack 알림 기능을 Telegram으로 대체 - 환경 변수 설정에 Telegram 관련 변수 추가 - 설정 파일에서 Slack 웹훅 URL 제거
This commit is contained in:
@@ -1,67 +1,24 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
# Claude Code Notification 훅 - 권한 요청 및 사용자 입력 대기 알림
|
# Claude Code - Notification 훅
|
||||||
|
# Claude가 사용자 주의가 필요한 알림을 보낼 때 실행된다.
|
||||||
#
|
#
|
||||||
# 이 스크립트는 Claude Code가 Notification 이벤트를 발생시킬 때 실행됩니다.
|
# stdin JSON 형식: {"session_id": "...", "message": "..."}
|
||||||
# 주로 권한 요청이나 사용자 입력 대기 상황에서 Slack 알림을 보냅니다.
|
|
||||||
|
|
||||||
# .env 파일에서 Slack 웹훅 URL 로드 (CRLF 호환)
|
set -euo pipefail
|
||||||
ENV_FILE="${CLAUDE_PROJECT_DIR}/.env"
|
|
||||||
# Windows 경로 백슬래시를 슬래시로 변환
|
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
ENV_FILE="${ENV_FILE//\\//}"
|
|
||||||
if [ -f "$ENV_FILE" ]; then
|
# stdin에서 JSON 읽기
|
||||||
while IFS='=' read -r key value || [[ -n "$key" ]]; do
|
INPUT="$(cat)"
|
||||||
[[ "$key" =~ ^[[:space:]]*# ]] && continue
|
|
||||||
[[ -z "${key// }" ]] && continue
|
MESSAGE="$(echo "$INPUT" | jq -r '.message // ""')"
|
||||||
key="${key//$'\r'/}"
|
|
||||||
value="${value//$'\r'/}"
|
if [[ -z "$MESSAGE" ]]; then
|
||||||
export "$key=$value"
|
exit 0
|
||||||
done < "$ENV_FILE"
|
|
||||||
else
|
|
||||||
echo "오류: .env 파일을 찾을 수 없습니다: $ENV_FILE" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Slack 웹훅 URL 확인
|
TELEGRAM_MESSAGE="🔔 *Claude 알림*
|
||||||
if [ -z "$SLACK_WEBHOOK_URL" ]; then
|
|
||||||
echo "오류: SLACK_WEBHOOK_URL이 설정되지 않았습니다." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# stdin에서 JSON 입력 읽기
|
${MESSAGE}"
|
||||||
STDIN_DATA=$(cat)
|
|
||||||
|
|
||||||
# JSON 입력에서 메시지 추출
|
"$HOOK_DIR/notify_telegram.sh" "$TELEGRAM_MESSAGE"
|
||||||
MESSAGE=$(echo "$STDIN_DATA" | jq -r '.message // empty')
|
|
||||||
|
|
||||||
# 프로젝트명 추출 (Windows 경로 백슬래시 변환)
|
|
||||||
NORMALIZED_DIR="${CLAUDE_PROJECT_DIR//\\//}"
|
|
||||||
PROJECT_NAME=$(basename "$NORMALIZED_DIR")
|
|
||||||
|
|
||||||
# 현재 시간
|
|
||||||
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
|
|
||||||
|
|
||||||
# jq를 사용해 안전하게 JSON payload 생성 (특수문자 이스케이프 처리)
|
|
||||||
PAYLOAD=$(jq -n \
|
|
||||||
--arg project "$PROJECT_NAME" \
|
|
||||||
--arg message "$MESSAGE" \
|
|
||||||
--arg timestamp "$TIMESTAMP" \
|
|
||||||
'{
|
|
||||||
channel: "#claude-code",
|
|
||||||
username: "Claude Code",
|
|
||||||
icon_emoji: ":bell:",
|
|
||||||
text: ("🔔 권한 요청 알림\n\n프로젝트: " + $project + "\n상태: " + $message + "\n시간: " + $timestamp + "\n\nClaude Code에서 알림이 도착했습니다.")
|
|
||||||
}')
|
|
||||||
|
|
||||||
# Slack으로 알림 전송
|
|
||||||
curl -s -X POST \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$PAYLOAD" \
|
|
||||||
"$SLACK_WEBHOOK_URL" > /dev/null 2>&1
|
|
||||||
|
|
||||||
# 성공 여부 확인
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
echo "Slack 알림이 성공적으로 전송되었습니다." >&2
|
|
||||||
else
|
|
||||||
echo "Slack 알림 전송에 실패했습니다." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|||||||
41
.claude/hooks/notify_telegram.sh
Executable file
41
.claude/hooks/notify_telegram.sh
Executable file
@@ -0,0 +1,41 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Telegram 메시지 발송 공통 유틸리티
|
||||||
|
# 사용법: notify_telegram.sh "메시지 본문"
|
||||||
|
#
|
||||||
|
# 필요 환경변수:
|
||||||
|
# TELEGRAM_BOT_TOKEN - BotFather 발급 토큰
|
||||||
|
# TELEGRAM_CHAT_ID - 알림을 받을 채팅 ID
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# 환경변수 미설정 시 .env 파일에서 로드 (Claude Code 훅 실행 환경 대응)
|
||||||
|
ENV_FILE="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/.env"
|
||||||
|
if [[ -f "$ENV_FILE" ]]; then
|
||||||
|
# export 구문을 포함한 .env 파일 소싱
|
||||||
|
set -a
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "$ENV_FILE"
|
||||||
|
set +a
|
||||||
|
fi
|
||||||
|
|
||||||
|
TELEGRAM_BOT_TOKEN="${TELEGRAM_BOT_TOKEN:-}"
|
||||||
|
TELEGRAM_CHAT_ID="${TELEGRAM_CHAT_ID:-}"
|
||||||
|
MESSAGE="${1:-}"
|
||||||
|
|
||||||
|
# 환경변수 미설정 시 조용히 종료 (훅 실패로 Claude 작업 중단 방지)
|
||||||
|
if [[ -z "$TELEGRAM_BOT_TOKEN" || -z "$TELEGRAM_CHAT_ID" ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$MESSAGE" ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
curl -s -X POST \
|
||||||
|
"https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$(jq -n \
|
||||||
|
--argjson chat_id "$TELEGRAM_CHAT_ID" \
|
||||||
|
--arg text "$MESSAGE" \
|
||||||
|
'{chat_id: $chat_id, text: $text, parse_mode: "Markdown"}')" \
|
||||||
|
> /dev/null
|
||||||
26
.claude/hooks/pre-tool-hook.sh
Executable file
26
.claude/hooks/pre-tool-hook.sh
Executable file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Claude Code - PreToolUse 훅
|
||||||
|
# 툴 실행 직전에 실행되어 Telegram으로 알림을 보낸다.
|
||||||
|
#
|
||||||
|
# stdin JSON 형식:
|
||||||
|
# {"session_id": "...", "tool_name": "...", "tool_input": {...}}
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
INPUT="$(cat)"
|
||||||
|
|
||||||
|
TOOL_NAME="$(echo "$INPUT" | jq -r '.tool_name // ""')"
|
||||||
|
TOOL_INPUT="$(echo "$INPUT" | jq -r '.tool_input // {} | to_entries | map("\(.key): \(.value)") | join(", ")')"
|
||||||
|
|
||||||
|
if [[ -z "$TOOL_NAME" ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
TELEGRAM_MESSAGE="⚙️ *툴 실행*
|
||||||
|
|
||||||
|
🔧 \`${TOOL_NAME}\`
|
||||||
|
📝 ${TOOL_INPUT}"
|
||||||
|
|
||||||
|
"$HOOK_DIR/notify_telegram.sh" "$TELEGRAM_MESSAGE"
|
||||||
20
.claude/hooks/stop-hook copy.sh
Executable file
20
.claude/hooks/stop-hook copy.sh
Executable file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Claude Code - Stop 훅
|
||||||
|
# Claude가 응답을 완료하고 멈출 때 실행된다.
|
||||||
|
#
|
||||||
|
# stdin JSON 형식: {"session_id": "...", "stop_hook_active": true}
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
# stdin에서 JSON 읽기
|
||||||
|
INPUT="$(cat)"
|
||||||
|
|
||||||
|
SESSION_ID="$(echo "$INPUT" | jq -r '.session_id // ""')"
|
||||||
|
|
||||||
|
TELEGRAM_MESSAGE="✅ *Claude 작업 완료*
|
||||||
|
|
||||||
|
세션 ID: \`${SESSION_ID}\`"
|
||||||
|
|
||||||
|
"$HOOK_DIR/notify_telegram.sh" "$TELEGRAM_MESSAGE"
|
||||||
@@ -12,7 +12,18 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/notification-hook.sh\""
|
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/notification-hook.sh"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"PreToolUse": [
|
||||||
|
{
|
||||||
|
"matcher": "*",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/pre-tool-hook.sh"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -23,7 +34,7 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/stop-hook.sh\""
|
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/stop-hook.sh"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
2
.github/workflows/lotto-buy.yml
vendored
2
.github/workflows/lotto-buy.yml
vendored
@@ -76,4 +76,6 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
LOTTO_USER_ID: ${{ secrets.LOTTO_USER_ID }}
|
LOTTO_USER_ID: ${{ secrets.LOTTO_USER_ID }}
|
||||||
LOTTO_USER_PW: ${{ secrets.LOTTO_USER_PW }}
|
LOTTO_USER_PW: ${{ secrets.LOTTO_USER_PW }}
|
||||||
|
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||||
|
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||||
run: .venv/bin/python3 lotto-runner/lotto_auto_buy.py
|
run: .venv/bin/python3 lotto-runner/lotto_auto_buy.py
|
||||||
|
|||||||
@@ -35,12 +35,13 @@ from selenium.webdriver.common.keys import Keys
|
|||||||
# 설정
|
# 설정
|
||||||
# ─────────────────────────────────────────────
|
# ─────────────────────────────────────────────
|
||||||
CONFIG = {
|
CONFIG = {
|
||||||
"USER_ID": os.environ.get("LOTTO_USER_ID", "hyeonggil2"),
|
"USER_ID": os.environ.get("LOTTO_USER_ID", ""),
|
||||||
"USER_PW": os.environ.get("LOTTO_USER_PW", "Rlaehdbs062$"),
|
"USER_PW": os.environ.get("LOTTO_USER_PW", ""),
|
||||||
"BUY_COUNT": 1, # 구매 게임 수 (1~10)
|
"BUY_COUNT": 1, # 구매 게임 수 (1~10)
|
||||||
"HEADLESS": True, # True: 브라우저 창 숨김, False: 브라우저 창 표시
|
"HEADLESS": True, # True: 브라우저 창 숨김, False: 브라우저 창 표시
|
||||||
"LOG_FILE": "logs/lotto_log.json",
|
"LOG_FILE": "logs/lotto_log.json",
|
||||||
"SLACK_WEBHOOK_URL": os.environ.get("SLACK_WEBHOOK_URL", ""),
|
"TELEGRAM_BOT_TOKEN": os.environ.get("TELEGRAM_BOT_TOKEN", ""),
|
||||||
|
"TELEGRAM_CHAT_ID": os.environ.get("TELEGRAM_CHAT_ID", ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
# 로깅 설정
|
# 로깅 설정
|
||||||
@@ -263,10 +264,11 @@ def extract_purchase_result(driver):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def send_slack(result):
|
def send_telegram(result):
|
||||||
"""Slack Webhook으로 구매 결과 알림 전송"""
|
"""Telegram Bot으로 구매 결과 알림 전송"""
|
||||||
url = CONFIG["SLACK_WEBHOOK_URL"]
|
token = CONFIG["TELEGRAM_BOT_TOKEN"]
|
||||||
if not url:
|
chat_id = CONFIG["TELEGRAM_CHAT_ID"]
|
||||||
|
if not token or not chat_id:
|
||||||
return
|
return
|
||||||
|
|
||||||
status = result.get("status", "unknown")
|
status = result.get("status", "unknown")
|
||||||
@@ -280,26 +282,30 @@ def send_slack(result):
|
|||||||
for i, nums in enumerate(games, 1)
|
for i, nums in enumerate(games, 1)
|
||||||
)
|
)
|
||||||
text = (
|
text = (
|
||||||
f":ticket: *로또 구매 완료!*\n"
|
f"🎟 *로또 구매 완료\!*\n"
|
||||||
f"회차: {round_no} 추첨일: {draw_date}\n"
|
f"회차: {round_no} 추첨일: {draw_date}\n"
|
||||||
f"{games_text}"
|
f"{games_text}"
|
||||||
)
|
)
|
||||||
elif status == "failed":
|
elif status == "failed":
|
||||||
text = f":warning: *로또 구매 실패*\n{result.get('error', '')}"
|
text = f"⚠️ *로또 구매 실패*\n{result.get('error', '')}"
|
||||||
else:
|
else:
|
||||||
text = f":x: *로또 구매 오류*\n{result.get('error', status)}"
|
text = f"❌ *로또 구매 오류*\n{result.get('error', status)}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
payload = json.dumps({"text": text}).encode("utf-8")
|
payload = json.dumps({
|
||||||
|
"chat_id": int(chat_id),
|
||||||
|
"text": text,
|
||||||
|
"parse_mode": "MarkdownV2",
|
||||||
|
}).encode("utf-8")
|
||||||
req = urllib.request.Request(
|
req = urllib.request.Request(
|
||||||
url,
|
f"https://api.telegram.org/bot{token}/sendMessage",
|
||||||
data=payload,
|
data=payload,
|
||||||
headers={"Content-Type": "application/json"},
|
headers={"Content-Type": "application/json"},
|
||||||
)
|
)
|
||||||
urllib.request.urlopen(req, timeout=10)
|
urllib.request.urlopen(req, timeout=10)
|
||||||
logger.info("Slack 알림 전송 완료")
|
logger.info("Telegram 알림 전송 완료")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Slack 알림 전송 실패: {e}")
|
logger.warning(f"Telegram 알림 전송 실패: {e}")
|
||||||
|
|
||||||
|
|
||||||
def save_log(result):
|
def save_log(result):
|
||||||
@@ -350,6 +356,11 @@ def main():
|
|||||||
if balance >= 0 and balance < cost:
|
if balance >= 0 and balance < cost:
|
||||||
logger.error(f"예치금 부족! 현재: {balance:,}원, 필요: {cost:,}원")
|
logger.error(f"예치금 부족! 현재: {balance:,}원, 필요: {cost:,}원")
|
||||||
logger.error("동행복권 사이트에서 예치금을 충전해주세요.")
|
logger.error("동행복권 사이트에서 예치금을 충전해주세요.")
|
||||||
|
send_telegram({
|
||||||
|
"date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"status": "failed",
|
||||||
|
"error": f"예치금 부족! 현재: {balance:,}원, 필요: {cost:,}원",
|
||||||
|
})
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# 3. 로또 구매
|
# 3. 로또 구매
|
||||||
@@ -358,7 +369,7 @@ def main():
|
|||||||
# 4. 결과 저장 및 Slack 알림
|
# 4. 결과 저장 및 Slack 알림
|
||||||
if result:
|
if result:
|
||||||
save_log(result)
|
save_log(result)
|
||||||
send_slack(result)
|
send_telegram(result)
|
||||||
else:
|
else:
|
||||||
failed = {
|
failed = {
|
||||||
"date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
"date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
@@ -366,7 +377,7 @@ def main():
|
|||||||
"error": "구매 결과를 확인할 수 없습니다.",
|
"error": "구매 결과를 확인할 수 없습니다.",
|
||||||
}
|
}
|
||||||
save_log(failed)
|
save_log(failed)
|
||||||
send_slack(failed)
|
send_telegram(failed)
|
||||||
|
|
||||||
logger.info("스크립트 실행 완료!")
|
logger.info("스크립트 실행 완료!")
|
||||||
|
|
||||||
@@ -378,7 +389,7 @@ def main():
|
|||||||
"error": str(e),
|
"error": str(e),
|
||||||
}
|
}
|
||||||
save_log(error_result)
|
save_log(error_result)
|
||||||
send_slack(error_result)
|
send_telegram(error_result)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
if driver:
|
if driver:
|
||||||
|
|||||||
236
telegram_bot.py
Normal file
236
telegram_bot.py
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
"""
|
||||||
|
Telegram 봇 - FE AI 표준화 프로젝트 팀 업무 지원 봇
|
||||||
|
|
||||||
|
기능:
|
||||||
|
- 기본 에코
|
||||||
|
- 스탠드업 리마인더 자동 발송 (매일 09:30 KST)
|
||||||
|
- 로드맵 태스크 상태 조회
|
||||||
|
|
||||||
|
필요 패키지:
|
||||||
|
pip install python-telegram-bot[job-queue]
|
||||||
|
|
||||||
|
환경변수:
|
||||||
|
TELEGRAM_BOT_TOKEN - BotFather에서 발급받은 토큰 (필수)
|
||||||
|
STANDUP_CHAT_IDS - 스탠드업 리마인더를 받을 채팅 ID 목록 (쉼표 구분, 선택)
|
||||||
|
예) STANDUP_CHAT_IDS=123456789,987654321
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from datetime import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
from telegram import Update
|
||||||
|
from telegram.ext import (
|
||||||
|
Application,
|
||||||
|
CommandHandler,
|
||||||
|
ContextTypes,
|
||||||
|
MessageHandler,
|
||||||
|
filters,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── 로깅 설정 ─────────────────────────────────────────────
|
||||||
|
logging.basicConfig(
|
||||||
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||||
|
level=logging.INFO,
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# ── 설정 ─────────────────────────────────────────────────
|
||||||
|
BOT_TOKEN: Final[str] = os.environ.get("TELEGRAM_BOT_TOKEN", "")
|
||||||
|
TASKS_FILE: Final[Path] = Path(__file__).parent / "shrimp_data" / "tasks.json"
|
||||||
|
|
||||||
|
# 스탠드업 리마인더 발송 시간 (KST = UTC+9)
|
||||||
|
STANDUP_HOUR: Final[int] = 9
|
||||||
|
STANDUP_MINUTE: Final[int] = 30
|
||||||
|
|
||||||
|
# 환경변수로 사전 등록할 채팅 ID 목록
|
||||||
|
STANDUP_CHAT_IDS: Final[list[int]] = [
|
||||||
|
int(cid)
|
||||||
|
for cid in os.environ.get("STANDUP_CHAT_IDS", "").split(",")
|
||||||
|
if cid.strip()
|
||||||
|
]
|
||||||
|
|
||||||
|
STATUS_LABELS: Final[dict[str, str]] = {
|
||||||
|
"pending": "⏳ 대기",
|
||||||
|
"in-progress": "🔄 진행중",
|
||||||
|
"completed": "✅ 완료",
|
||||||
|
"deferred": "⏸ 보류",
|
||||||
|
"cancelled": "❌ 취소",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ── 유틸리티 ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
def load_tasks() -> list[dict]:
|
||||||
|
"""shrimp_data/tasks.json에서 태스크 목록을 로드한다."""
|
||||||
|
if not TASKS_FILE.exists():
|
||||||
|
return []
|
||||||
|
with TASKS_FILE.open(encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
return data.get("tasks", [])
|
||||||
|
|
||||||
|
|
||||||
|
def format_task_line(task: dict) -> str:
|
||||||
|
"""태스크 한 줄 요약을 반환한다."""
|
||||||
|
status = STATUS_LABELS.get(task.get("status", ""), task.get("status", ""))
|
||||||
|
name = task.get("name", "(이름 없음)")
|
||||||
|
return f"{status} {name}"
|
||||||
|
|
||||||
|
|
||||||
|
# ── 커맨드 핸들러 ─────────────────────────────────────────
|
||||||
|
|
||||||
|
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""/start — 봇 소개 및 명령어 안내"""
|
||||||
|
text = (
|
||||||
|
"안녕하세요! FE AI 표준화 프로젝트 봇입니다. 👋\n\n"
|
||||||
|
"사용 가능한 명령어\n"
|
||||||
|
"/tasks — 전체 태스크 목록\n"
|
||||||
|
"/tasks\\_pending — 대기 중인 태스크\n"
|
||||||
|
"/tasks\\_done — 완료된 태스크\n"
|
||||||
|
"/standup — 스탠드업 리마인더 구독\n"
|
||||||
|
"/help — 도움말"
|
||||||
|
)
|
||||||
|
await update.message.reply_text(text, parse_mode="Markdown")
|
||||||
|
|
||||||
|
|
||||||
|
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""/help — 도움말 (start와 동일)"""
|
||||||
|
await start(update, context)
|
||||||
|
|
||||||
|
|
||||||
|
async def tasks_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""/tasks — 전체 태스크를 상태별로 보여준다."""
|
||||||
|
all_tasks = load_tasks()
|
||||||
|
if not all_tasks:
|
||||||
|
await update.message.reply_text("태스크 데이터를 불러올 수 없습니다.")
|
||||||
|
return
|
||||||
|
|
||||||
|
total = len(all_tasks)
|
||||||
|
done = sum(1 for t in all_tasks if t.get("status") == "completed")
|
||||||
|
lines = [f"📋 *전체 태스크 ({done}/{total} 완료)*\n"]
|
||||||
|
for task in all_tasks:
|
||||||
|
lines.append(format_task_line(task))
|
||||||
|
|
||||||
|
await update.message.reply_text("\n".join(lines), parse_mode="Markdown")
|
||||||
|
|
||||||
|
|
||||||
|
async def tasks_pending_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""/tasks_pending — 대기 중인 태스크만 조회한다."""
|
||||||
|
pending = [t for t in load_tasks() if t.get("status") == "pending"]
|
||||||
|
|
||||||
|
if not pending:
|
||||||
|
await update.message.reply_text("✅ 대기 중인 태스크가 없습니다!")
|
||||||
|
return
|
||||||
|
|
||||||
|
lines = [f"⏳ *대기 중인 태스크 ({len(pending)}개)*\n"]
|
||||||
|
for task in pending:
|
||||||
|
lines.append(format_task_line(task))
|
||||||
|
|
||||||
|
await update.message.reply_text("\n".join(lines), parse_mode="Markdown")
|
||||||
|
|
||||||
|
|
||||||
|
async def tasks_done_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""/tasks_done — 완료된 태스크만 조회한다."""
|
||||||
|
done = [t for t in load_tasks() if t.get("status") == "completed"]
|
||||||
|
|
||||||
|
if not done:
|
||||||
|
await update.message.reply_text("아직 완료된 태스크가 없습니다.")
|
||||||
|
return
|
||||||
|
|
||||||
|
lines = [f"✅ *완료된 태스크 ({len(done)}개)*\n"]
|
||||||
|
for task in done:
|
||||||
|
lines.append(format_task_line(task))
|
||||||
|
|
||||||
|
await update.message.reply_text("\n".join(lines), parse_mode="Markdown")
|
||||||
|
|
||||||
|
|
||||||
|
async def standup_subscribe(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""/standup — 이 채팅을 스탠드업 리마인더 수신 대상으로 등록한다."""
|
||||||
|
chat_id = update.effective_chat.id
|
||||||
|
registered: set[int] = context.bot_data.setdefault("standup_chats", set())
|
||||||
|
|
||||||
|
if chat_id in registered:
|
||||||
|
await update.message.reply_text("이미 등록된 채팅입니다.")
|
||||||
|
return
|
||||||
|
|
||||||
|
registered.add(chat_id)
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"✅ 스탠드업 리마인더에 등록되었습니다.\n"
|
||||||
|
f"매일 *{STANDUP_HOUR:02d}:{STANDUP_MINUTE:02d} KST*에 알림을 보내드립니다.",
|
||||||
|
parse_mode="Markdown",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── 에코 핸들러 ───────────────────────────────────────────
|
||||||
|
|
||||||
|
async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""명령어가 아닌 텍스트 메시지를 그대로 반환한다."""
|
||||||
|
await update.message.reply_text(update.message.text)
|
||||||
|
|
||||||
|
|
||||||
|
# ── 스케줄 작업 ───────────────────────────────────────────
|
||||||
|
|
||||||
|
async def send_standup_reminder(context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""등록된 모든 채팅에 스탠드업 리마인더를 발송한다."""
|
||||||
|
message = (
|
||||||
|
"🌅 *스탠드업 리마인더*\n\n"
|
||||||
|
"오늘의 스탠드업을 시작할 시간입니다!\n\n"
|
||||||
|
"1️⃣ *어제 한 일*:\n"
|
||||||
|
"2️⃣ *오늘 할 일*:\n"
|
||||||
|
"3️⃣ *블로커*:"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 환경변수 등록 + 런타임 /standup 구독 채팅 합산
|
||||||
|
target_chats: set[int] = set(STANDUP_CHAT_IDS) | context.bot_data.get(
|
||||||
|
"standup_chats", set()
|
||||||
|
)
|
||||||
|
|
||||||
|
for chat_id in target_chats:
|
||||||
|
try:
|
||||||
|
await context.bot.send_message(
|
||||||
|
chat_id=chat_id,
|
||||||
|
text=message,
|
||||||
|
parse_mode="Markdown",
|
||||||
|
)
|
||||||
|
logger.info("스탠드업 리마인더 발송 완료 (chat_id=%s)", chat_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("스탠드업 리마인더 발송 실패 (chat_id=%s): %s", chat_id, e)
|
||||||
|
|
||||||
|
|
||||||
|
# ── 앱 빌드 & 실행 ────────────────────────────────────────
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
if not BOT_TOKEN:
|
||||||
|
raise ValueError(
|
||||||
|
"TELEGRAM_BOT_TOKEN 환경변수를 설정해주세요.\n"
|
||||||
|
"예) export TELEGRAM_BOT_TOKEN=your_token_here"
|
||||||
|
)
|
||||||
|
|
||||||
|
app = Application.builder().token(BOT_TOKEN).build()
|
||||||
|
|
||||||
|
# 커맨드 핸들러 등록
|
||||||
|
app.add_handler(CommandHandler("start", start))
|
||||||
|
app.add_handler(CommandHandler("help", help_command))
|
||||||
|
app.add_handler(CommandHandler("tasks", tasks_command))
|
||||||
|
app.add_handler(CommandHandler("tasks_pending", tasks_pending_command))
|
||||||
|
app.add_handler(CommandHandler("tasks_done", tasks_done_command))
|
||||||
|
app.add_handler(CommandHandler("standup", standup_subscribe))
|
||||||
|
|
||||||
|
# 에코 핸들러 (명령어가 아닌 텍스트)
|
||||||
|
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo))
|
||||||
|
|
||||||
|
# 매일 09:30 KST (UTC 00:30)에 스탠드업 리마인더 발송
|
||||||
|
app.job_queue.run_daily(
|
||||||
|
send_standup_reminder,
|
||||||
|
time=time(hour=0, minute=30), # UTC 기준 (KST -9h)
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("봇 시작 — polling 모드")
|
||||||
|
app.run_polling(allowed_updates=Update.ALL_TYPES)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user