""" GUARDiA ITSM 알림 모듈 — 이메일(SMTP) + 메신저(Webhook) 자동 발송. 설정은 환경 변수로 관리 (외부 API 호출 금지, 온프레미스 전용): 이메일 설정: SMTP_HOST = "" # 빈 문자열이면 이메일 비활성화 SMTP_PORT = "25" SMTP_USER = "" SMTP_PASSWORD = "" SMTP_FROM = "noreply@guardia.local" SMTP_TLS = "false" # SSL/TLS (포트 465 등) SMTP_STARTTLS = "false" # STARTTLS (포트 587 등) 메신저 설정: MESSENGER_WEBHOOK = "http://localhost:8000/api/messenger/webhook" MESSENGER_ENABLED = "true" MESSENGER_OPS_ROOM = "ops" # 운영팀 알림 채널 알림 트리거: NOTIFY_ON_CREATED = "true" # SR 접수 시 고객 이메일 발송 NOTIFY_STATUSES = "COMPLETED,REJECTED,FAILED_ROLLBACK" # 상태 변경 시 발송 """ import asyncio import os import smtplib import socket from datetime import datetime from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from typing import Optional try: import httpx _HTTPX = True except ImportError: _HTTPX = False # ── 환경변수 설정 ────────────────────────────────────────────────────────────── def _env(key: str, default: str = "") -> str: return os.environ.get(key, default).strip() def _env_bool(key: str, default: bool = True) -> bool: v = os.environ.get(key, "").strip().lower() if v in ("1", "true", "yes", "on"): return True if v in ("0", "false", "no", "off"): return False return default # ── 이메일 HTML 템플릿 ───────────────────────────────────────────────────────── _BRAND_COLOR = "#1E3A5F" def _html_email(title: str, body_html: str, sr_id: str = "", status_badge: str = "") -> str: badge_html = f'{status_badge}' if status_badge else "" return f"""
{title} {badge_html}
{body_html}안녕하세요.
서비스 요청이 정상적으로 접수되었습니다. 담당 엔지니어가 배정되면 작업을 시작합니다.
| SR 번호 | {sr_id} |
|---|---|
| 제목 | {title} |
| 유형 | {sr_type} |
| 우선순위 | {prio} |
| 요청자 | {req_by} |
| 접수 시각 | {datetime.now().strftime('%Y-%m-%d %H:%M')} |
진행 상황은 GUARDiA ITSM 포털에서 실시간으로 확인하실 수 있습니다.
""" html = _html_email("SR 접수 확인", body_html, sr_id, "RECEIVED") ok, err = await send_email(addrs, subject, html) asyncio.create_task(_log(sr_id, "EMAIL", ", ".join(addrs), subject, ok, err)) # ── 메신저 ───────────────────────────────────────────────────────────── if _env_bool("MESSENGER_ENABLED", True): room = _env("MESSENGER_OPS_ROOM", "ops") ok, err = await send_messenger(room, { "event": "sr_created", "sr_id": sr_id, "title": title, "sr_type": sr_type, "priority": prio, "requested_by": req_by, "message": f"[접수] {sr_id} — {title} ({sr_type} / {prio})", }) asyncio.create_task(_log(sr_id, "MESSENGER", room, f"SR 접수: {sr_id}", ok, err)) async def notify_sr_status_changed(sr, new_status: str, summary: str, db=None) -> None: """SR 상태 변경 시 이메일 + 메신저 알림 발송.""" notify_statuses = _env("NOTIFY_STATUSES", "COMPLETED,REJECTED,FAILED_ROLLBACK").split(",") if new_status not in notify_statuses: return # 필드 캡처 sr_id = sr.sr_id title = sr.title sr_type = sr.sr_type inst_id = sr.inst_id req_by = sr.requested_by status_ko = _status_ko(new_status) color = _status_color(new_status) # ── 이메일 ───────────────────────────────────────────────────────────── addrs = await _get_recipient_emails(req_by, inst_id) if addrs: subject = f"[GUARDiA] {sr_id} {status_ko} — {title}" if new_status == "COMPLETED": intro = "서비스 요청이 정상적으로 처리 완료되었습니다." elif new_status == "REJECTED": intro = "서비스 요청이 반려되었습니다. 내용을 확인 후 재요청 바랍니다." else: intro = "서비스 요청 처리 중 오류가 발생하여 롤백되었습니다. 담당자에게 문의해 주세요." body_html = f"""안녕하세요.
{intro}
| SR 번호 | {sr_id} |
|---|---|
| 제목 | {title} |
| 처리 상태 | {status_ko} |
| 처리 시각 | {datetime.now().strftime('%Y-%m-%d %H:%M')} |
처리 결과 요약
세부 처리 내역은 GUARDiA ITSM 포털에서 확인하실 수 있습니다.
""" html = _html_email(f"SR {status_ko}", body_html, sr_id, new_status) ok, err = await send_email(addrs, subject, html) asyncio.create_task(_log(sr_id, "EMAIL", ", ".join(addrs), subject, ok, err)) # ── 메신저 ───────────────────────────────────────────────────────────── if _env_bool("MESSENGER_ENABLED", True): room = _env("MESSENGER_OPS_ROOM", "ops") emoji = {"COMPLETED": "✅", "REJECTED": "❌", "FAILED_ROLLBACK": "⚠️"}.get(new_status, "ℹ️") ok, err = await send_messenger(room, { "event": "sr_status_changed", "sr_id": sr_id, "title": title, "sr_type": sr_type, "new_status": new_status, "result_summary": summary[:200] if summary else "", "message": f"{emoji} [{status_ko}] {sr_id} — {title}\n{summary[:100] if summary else ''}", }) asyncio.create_task(_log(sr_id, "MESSENGER", room, f"SR {status_ko}: {sr_id}", ok, err)) # ═══════════════════════════════════════════════════════════════════════════════ # ── A-3: 배포 승인 모바일 알림 ────────────────────────────────────────────────── # ═══════════════════════════════════════════════════════════════════════════════ async def notify_deploy_approval_required( session_id: int, sr_id: Optional[str], project_name: str, approvers: list[str], approve_url: str = "", ) -> None: """ 배포 승인 대기 알림. vibe 세션이 PENDING_APPROVAL 상태에 진입할 때 호출. 지정된 승인자(approvers)들에게 메신저 + 이메일 알림 발송. Args: session_id: VibeSession.id sr_id: 연관된 SR ID (없을 수 있음) project_name: 배포 대상 프로젝트명 approvers: 승인 담당자 username 목록 (User.email로 변환) approve_url: 승인 링크 (딥링크 또는 웹 URL) """ from database import SessionLocal from sqlalchemy import select from models import User subject = f"[GUARDiA] 배포 승인 요청 — {project_name} (세션 #{session_id})" ops_room = _env("MESSENGER_OPS_ROOM", "ops") approve_room = _env("MESSENGER_APPROVE_ROOM", ops_room) # 메신저 알림 msg = ( f"🚀 배포 승인 요청\n" f"프로젝트: {project_name}\n" f"세션 ID: #{session_id}\n" f"SR: {sr_id or '(없음)'}\n" f"승인 필요 담당자: {', '.join(approvers)}\n" f"승인 링크: {approve_url or '(관리 UI 확인)'}" ) ok, err = await send_messenger(approve_room, { "event": "deploy_approval_required", "session_id": session_id, "project": project_name, "approvers": approvers, "approve_url": approve_url, "type": "text", "text": msg, }) # 이메일 알림 (approvers → User.email 조회) if approvers: try: async with SessionLocal() as db: result = await db.execute( select(User).where(User.username.in_(approvers)) ) users = result.scalars().all() email_addrs = [u.email for u in users if u.email] if email_addrs: body_html = f"""안녕하세요.
배포 승인이 필요합니다.
| 프로젝트 | {project_name} |
|---|---|
| 세션 ID | #{session_id} |
| SR 번호 | {sr_id or '(없음)'} |
| 요청 시각 | {datetime.now().strftime('%Y-%m-%d %H:%M')} |
위 버튼이 작동하지 않으면 GUARDiA ITSM 관리 UI에서 직접 승인해 주세요.
""" html = _html_email(f"배포 승인 요청: {project_name}", body_html, str(session_id)) email_ok, email_err = await send_email(email_addrs, subject, html) asyncio.create_task(_log(sr_id, "EMAIL", ",".join(email_addrs), subject, email_ok, email_err)) except Exception: pass async def notify_deploy_completed( session_id: int, sr_id: Optional[str], project_name: str, success: bool, summary: str = "", ) -> None: """ 배포 완료/실패 알림. vibe 세션이 COMPLETED 또는 FAILED 상태에 진입할 때 호출. """ ops_room = _env("MESSENGER_OPS_ROOM", "ops") emoji = "✅" if success else "❌" status_text = "완료" if success else "실패" msg = ( f"{emoji} 배포 {status_text}\n" f"프로젝트: {project_name}\n" f"세션 ID: #{session_id}\n" f"SR: {sr_id or '(없음)'}\n" f"일시: {datetime.now().strftime('%Y-%m-%d %H:%M')}" + (f"\n결과: {summary[:200]}" if summary else "") ) ok, err = await send_messenger(ops_room, { "event": "deploy_completed", "session_id": session_id, "project": project_name, "success": success, "type": "text", "text": msg, }) asyncio.create_task(_log(sr_id, "MESSENGER", ops_room, f"배포 {status_text}: {project_name}", ok, err))