diff --git a/itsm/routers/messenger.py b/itsm/routers/messenger.py index 32a220d7..18c38aa9 100644 --- a/itsm/routers/messenger.py +++ b/itsm/routers/messenger.py @@ -273,6 +273,58 @@ async def handle_bot_command( bg.add_task(_cmd_bulk_action, cmd.room, cmd.user, action, sr_ids) return BotReply(room=cmd.room, text=f"[대량처리] {len(sr_ids)}건 {action} 처리 중...") + # ── /report <프로젝트코드> [daily|weekly|monthly] ───────────────────────── + elif keyword in ("/report", "!report"): + if len(parts) < 2: + return BotReply(room=cmd.room, text="사용법: /report <프로젝트코드> [daily|weekly|monthly]") + proj_code = parts[1] + rtype = parts[2].lower() if len(parts) >= 3 else "weekly" + if rtype not in ("daily", "weekly", "monthly"): + return BotReply(room=cmd.room, text="보고서 유형: daily | weekly | monthly") + bg.add_task(_cmd_project_report, cmd.room, cmd.user, proj_code, rtype, db) + rtype_ko = {"daily": "일일", "weekly": "주간", "monthly": "월간"}.get(rtype, rtype) + return BotReply(room=cmd.room, text=f"[{rtype_ko}보고서] {proj_code} 보고서 생성 중...") + + # ── /pms <프로젝트코드> ─── 프로젝트 현황 요약 ─────────────────────────── + elif keyword in ("/pms", "!pms"): + if len(parts) < 2: + return BotReply(room=cmd.room, text="사용법: /pms <프로젝트코드>") + proj_code = parts[1] + reply = await _cmd_pms_status(proj_code, db) + return BotReply(room=cmd.room, text=reply) + + # ── /deliverables <프로젝트코드> ─── 산출물 현황 ──────────────────────── + elif keyword in ("/deliverables", "/산출물", "!deliverables"): + if len(parts) < 2: + return BotReply(room=cmd.room, text="사용법: /deliverables <프로젝트코드>") + proj_code = parts[1] + reply = await _cmd_deliverables(proj_code, db) + return BotReply(room=cmd.room, text=reply) + + # ── /scan ─── 시큐어코딩 / 보안 스캔 ──────────────────────────────────── + elif keyword in ("/scan", "!scan"): + bg.add_task(_cmd_compliance_scan, cmd.room, cmd.user) + return BotReply(room=cmd.room, text="[보안 스캔] 시큐어코딩/웹접근성/개인정보 점검 시작...") + + # ── /checklist ─── 공공기관 체크리스트 현황 ────────────────────────────── + elif keyword in ("/checklist", "!checklist"): + reply = await _cmd_checklist_status() + return BotReply(room=cmd.room, text=reply) + + # ── /perf [url] ─── 성능 테스트 ───────────────────────────────────────── + elif keyword in ("/perf", "!perf"): + target = parts[1] if len(parts) >= 2 else "http://localhost:8001" + bg.add_task(_cmd_perf_test, cmd.room, cmd.user, target) + return BotReply(room=cmd.room, text=f"[성능 테스트] {target} 대상 10명/30초 부하 테스트 시작...") + + # ── /issues <프로젝트코드> ─── 미결 이슈 목록 ──────────────────────────── + elif keyword in ("/issues", "!issues"): + if len(parts) < 2: + return BotReply(room=cmd.room, text="사용법: /issues <프로젝트코드>") + proj_code = parts[1] + reply = await _cmd_open_issues(proj_code, db) + return BotReply(room=cmd.room, text=reply) + # ── /help ───────────────────────────────────────────────────────────────── elif keyword == "/help": return BotReply(room=cmd.room, text=_help_text()) @@ -463,6 +515,232 @@ async def _cmd_sm(room: str, actor: str, server: str, ) +async def _cmd_project_report(room: str, actor: str, proj_code: str, rtype: str, db): + """/report — 프로젝트 보고서 생성 후 요약 발송.""" + import httpx as _httpx + rtype_ko = {"daily": "일일", "weekly": "주간", "monthly": "월간"}.get(rtype, rtype) + try: + # 프로젝트 조회 + from models import SiProject + from sqlalchemy import select as sel + async with SessionLocal() as _db: + proj = (await _db.execute( + sel(SiProject).where(SiProject.project_code == proj_code) + )).scalars().first() + + if not proj: + await _send_to_room(room, f"프로젝트를 찾을 수 없습니다: {proj_code}") + return + + # 메신저 발송 + async with _httpx.AsyncClient(timeout=30.0) as client: + r = await client.post( + f"http://localhost:8001/api/si/projects/{proj.id}/report/send", + json={"room": room, "report_type": rtype, "fmt": "excel"}, + headers={"Authorization": f"Bearer {_get_internal_token()}"}, + ) + if r.status_code == 200: + data = r.json() + await _send_to_room(room, data.get("summary", f"[{rtype_ko}보고서] {proj_code} 완료")) + else: + await _send_to_room(room, f"[{rtype_ko}보고서] 생성 실패 ({r.status_code})") + except Exception as e: + await _send_to_room(room, f"[보고서 오류] {str(e)[:100]}") + + +async def _cmd_pms_status(proj_code: str, db) -> str: + """/pms — 프로젝트 현황 요약.""" + import httpx as _httpx + try: + async with SessionLocal() as _db: + from models import SiProject + from sqlalchemy import select as sel + proj = (await _db.execute( + sel(SiProject).where(SiProject.project_code == proj_code) + )).scalars().first() + + if not proj: + return f"프로젝트를 찾을 수 없습니다: {proj_code}" + + async with _httpx.AsyncClient(timeout=10.0) as client: + r = await client.get( + f"http://localhost:8001/api/si/projects/{proj.id}/report/status", + headers={"Authorization": f"Bearer {_get_internal_token()}"}, + ) + if r.status_code == 200: + d = r.json() + lines = [ + f"[PMS 현황] {d.get('project_name','')} ({proj_code})", + f"단계: {d.get('phase','?')} | 건강: {d.get('health_status','?')}", + f"진척률: {d.get('overall_progress',0)}% | 예산: {d.get('budget_pct',0)}%", + f"WBS: {d.get('wbs_done',0)}/{d.get('wbs_total',0)} 완료 | 지연: {d.get('wbs_delayed',0)}건", + f"미결 이슈: {d.get('issue_open',0)}건 | 고위험: {d.get('high_risks',0)}건", + ] + if d.get("upcoming_milestones"): + m = d["upcoming_milestones"][0] + lines.append(f"다음 마일스톤: {m['name']} ({m['target_date']})") + return "\n".join(lines) + return f"프로젝트 현황 조회 실패 ({r.status_code})" + except Exception as e: + return f"PMS 조회 오류: {str(e)[:100]}" + + +async def _cmd_deliverables(proj_code: str, db) -> str: + """/deliverables — 산출물 제출 현황.""" + import httpx as _httpx + try: + async with SessionLocal() as _db: + from models import SiProject + from sqlalchemy import select as sel + proj = (await _db.execute( + sel(SiProject).where(SiProject.project_code == proj_code) + )).scalars().first() + if not proj: + return f"프로젝트를 찾을 수 없습니다: {proj_code}" + + async with _httpx.AsyncClient(timeout=10.0) as client: + r = await client.get( + f"http://localhost:8001/api/si/projects/{proj.id}/deliverables/summary", + headers={"Authorization": f"Bearer {_get_internal_token()}"}, + ) + if r.status_code == 200: + d = r.json() + lines = [ + f"[산출물 현황] {proj_code}", + f"전체: {d['total']}건 | 제출률: {d['submit_rate']}% | 승인률: {d['approval_rate']}%", + ] + if d.get("overdue"): + lines.append(f"⚠️ 기한 초과: {len(d['overdue'])}건") + for o in d["overdue"][:3]: + lines.append(f" - {o['name']} ({o['days_overdue']}일 초과)") + return "\n".join(lines) + return f"산출물 현황 조회 실패 ({r.status_code})" + except Exception as e: + return f"산출물 조회 오류: {str(e)[:100]}" + + +async def _cmd_compliance_scan(room: str, actor: str): + """/scan — 시큐어코딩/웹접근성/개인정보 자동 점검.""" + import httpx as _httpx + try: + async with _httpx.AsyncClient(timeout=60.0) as client: + r = await client.post( + "http://localhost:8001/api/compliance/scan", + headers={"Authorization": f"Bearer {_get_internal_token()}"}, + ) + if r.status_code == 200: + d = r.json() + msg = ( + f"[보안 스캔 완료]\n" + f"종합 위험도: {d.get('risk_level','?')}\n" + f"총 발견: {d.get('total_findings',0)}건\n" + f"시큐어코딩: {d.get('summary',{}).get('secure_coding',0)}건\n" + f"웹접근성: {d.get('summary',{}).get('accessibility',0)}건\n" + f"개인정보: {d.get('summary',{}).get('privacy',0)}건\n" + f"상세: http://localhost:8001/compliance" + ) + else: + msg = f"[보안 스캔 실패] {r.status_code}" + await _send_to_room(room, msg) + except Exception as e: + await _send_to_room(room, f"[스캔 오류] {str(e)[:100]}") + + +async def _cmd_checklist_status() -> str: + """/checklist — 공공기관 체크리스트 현황.""" + import httpx as _httpx + try: + async with _httpx.AsyncClient(timeout=10.0) as client: + r = await client.get( + "http://localhost:8001/api/public/status", + headers={"Authorization": f"Bearer {_get_internal_token()}"}, + ) + if r.status_code == 200: + d = r.json() + by_cat = d.get("by_category", {}) + lines = [ + f"[공공기관 체크리스트]", + f"GUARDiA 구현율: {d.get('impl_rate',0)}% ({d.get('guardia_impl',0)}/{d.get('total_items',0)})", + f"검증 완료: {d.get('verify_rate',0)}% ({d.get('verified',0)}/{d.get('total_items',0)})", + ] + for cat, v in by_cat.items(): + lines.append(f" {cat}: {v['impl']}/{v['total']} 구현") + actions = d.get("action_items", []) + if actions: + lines.append(f"미완료 조치: {len(actions)}건") + for a in actions[:2]: + lines.append(f" - {a[:60]}") + return "\n".join(lines) + return f"체크리스트 조회 실패 ({r.status_code})" + except Exception as e: + return f"체크리스트 오류: {str(e)[:100]}" + + +async def _cmd_perf_test(room: str, actor: str, target_url: str): + """/perf — 성능 테스트 실행.""" + import httpx as _httpx + try: + async with _httpx.AsyncClient(timeout=90.0) as client: + r = await client.post( + "http://localhost:8001/api/perf/run", + json={ + "target_url": target_url, + "endpoints": ["/", "/api/tasks", "/api/dashboard/me"], + "users": 10, + "duration": 30, + "ramp_up": 5, + "think_time": 0.1, + }, + headers={"Authorization": f"Bearer {_get_internal_token()}"}, + ) + if r.status_code == 200: + d = r.json() + s = d.get("summary", {}) + msg = ( + f"[성능 테스트 완료] {target_url}\n" + f"TPS: {s.get('tps',0)} | 평균: {s.get('avg_ms',0)}ms\n" + f"P95: {s.get('p95_ms',0)}ms | 에러율: {s.get('error_rate_pct',0)}%\n" + f"결과 ID: {d.get('result_id','?')}\n" + f"보고서: http://localhost:8001/api/perf/results/{d.get('result_id','')}/html" + ) + else: + msg = f"[성능 테스트 실패] {r.status_code}" + await _send_to_room(room, msg) + except Exception as e: + await _send_to_room(room, f"[성능 테스트 오류] {str(e)[:100]}") + + +async def _cmd_open_issues(proj_code: str, db) -> str: + """/issues — 프로젝트 미결 이슈 목록.""" + import httpx as _httpx + try: + async with SessionLocal() as _db: + from models import SiProject + from sqlalchemy import select as sel + proj = (await _db.execute( + sel(SiProject).where(SiProject.project_code == proj_code) + )).scalars().first() + if not proj: + return f"프로젝트를 찾을 수 없습니다: {proj_code}" + + async with _httpx.AsyncClient(timeout=10.0) as client: + r = await client.get( + f"http://localhost:8001/api/si/projects/{proj.id}/issues?status=OPEN&limit=5", + headers={"Authorization": f"Bearer {_get_internal_token()}"}, + ) + if r.status_code == 200: + issues = r.json() + if not issues: + return f"[이슈] {proj_code} — 미결 이슈 없음 ✅" + lines = [f"[미결 이슈] {proj_code} ({len(issues)}건)"] + for iss in issues[:5]: + lines.append(f" [{iss.get('issue_id','?')}] {iss.get('title','')[:40]} — {iss.get('assigned_to','미배정')}") + return "\n".join(lines) + return f"이슈 조회 실패 ({r.status_code})" + except Exception as e: + return f"이슈 조회 오류: {str(e)[:100]}" + + async def _cmd_create_sr(room: str, actor: str, title: str): """슬래시 /sr 명령 — SR 빠른 접수 (내부 API 호출).""" import httpx as _httpx @@ -579,24 +857,35 @@ def _help_text() -> str: return """GUARDiA ITSM 봇 명령어 ━━━━━━━━━━━━━━━━━━━━ [SR 관리] -/sr <제목> → SR 빠른 접수 -!status → SR + 세션 상태 조회 -/bulk → SR 대량 처리 (close/assign/status) +/sr <제목> → SR 빠른 접수 +!status → SR + 세션 상태 조회 +/bulk → SR 대량 처리 (close/assign/status) + +[PMS 프로젝트 관리] +/pms <프로젝트코드> → 프로젝트 진척 현황 +/report <코드> [daily|weekly|monthly] → 보고서 발송 +/deliverables <프로젝트코드> → 산출물 제출 현황 +/issues <프로젝트코드> → 미결 이슈 목록 + +[보안/품질] +/scan → 시큐어코딩/웹접근성/개인정보 점검 +/checklist → 공공기관 필수 기능 현황 +/perf [url] → 성능 테스트 실행 [배포] -!vibe [project_id] → 바이브 코딩 세션 시작 -!build → 빌드 실행 -!deploy → 배포 (SSH + WAS 재기동) -!cancel → 세션 취소 +!vibe [project_id] → 바이브 코딩 세션 시작 +!build → 빌드 실행 +!deploy → 배포 (SSH + WAS 재기동) +!cancel → 세션 취소 [운영 정보] -/status → 시스템 현황 요약 -/license → 라이선스 상태 조회 +/status → 시스템 현황 요약 +/license → 라이선스 상태 조회 [SM 원격 제어] -!sm