diff --git a/routers/messenger.py b/routers/messenger.py index 18c38aa..57ebfa3 100644 --- a/routers/messenger.py +++ b/routers/messenger.py @@ -325,6 +325,127 @@ async def handle_bot_command( reply = await _cmd_open_issues(proj_code, db) return BotReply(room=cmd.room, text=reply) + # ── /oncall ─── 현재 당직자 조회 ──────────────────────────────────────── + elif keyword in ("/oncall", "!oncall"): + reply = await _cmd_oncall() + return BotReply(room=cmd.room, text=reply) + + # ── /incident <제목> [P1|P2|P3|P4] ─── 인시던트 빠른 등록 ─────────────── + elif keyword in ("/incident", "!incident", "/inc"): + if len(parts) < 2: + return BotReply(room=cmd.room, text="사용법: /incident <제목> [P1|P2|P3|P4]") + grade = parts[-1].upper() if parts[-1].upper() in ("P1","P2","P3","P4") else "P3" + title_parts = parts[1:-1] if grade in ("P1","P2","P3","P4") and len(parts) > 2 else parts[1:] + title = " ".join(title_parts) + bg.add_task(_cmd_create_incident, cmd.room, cmd.user, title, grade) + return BotReply(room=cmd.room, text=f"[{grade} 인시던트] '{title}' 등록 중...") + + # ── /rca <인시던트ID> ─── AI 자동 RCA 분석 ─────────────────────────────── + elif keyword in ("/rca", "!rca"): + if len(parts) < 2: + return BotReply(room=cmd.room, text="사용법: /rca ") + inc_id = parts[1] + bg.add_task(_cmd_auto_rca, cmd.room, cmd.user, inc_id) + return BotReply(room=cmd.room, text=f"[AI RCA] {inc_id} 자동 근본원인 분석 중...") + + # ── /escalate ─── SR 에스컬레이션 ─────────────────────────────── + elif keyword in ("/escalate", "!escalate"): + if len(parts) < 2: + return BotReply(room=cmd.room, text="사용법: /escalate ") + sr_id = parts[1] + bg.add_task(_cmd_escalate, cmd.room, cmd.user, sr_id) + return BotReply(room=cmd.room, text=f"[에스컬레이션] {sr_id} 당직자에게 즉시 전달 중...") + + # ── /sla ─── SLA 위반 현황 ─────────────────────────────────────────────── + elif keyword in ("/sla", "!sla"): + reply = await _cmd_sla_violations() + return BotReply(room=cmd.room, text=reply) + + # ── /assign <담당자> ─── SR 담당자 배정 ────────────────────────── + elif keyword in ("/assign", "!assign"): + if len(parts) < 3: + return BotReply(room=cmd.room, text="사용법: /assign <담당자>") + sr_id = parts[1]; assignee = parts[2] + reply = await _cmd_assign_sr(sr_id, assignee, cmd.user, db) + return BotReply(room=cmd.room, text=reply) + + # ── /approve ─── SR 즉시 승인 ─────────────────────────────────── + elif keyword in ("/approve", "!approve"): + if len(parts) < 2: + return BotReply(room=cmd.room, text="사용법: /approve ") + sr_id = parts[1] + comment = " ".join(parts[2:]) if len(parts) > 2 else "봇 승인" + reply = await _cmd_approve_sr(sr_id, cmd.user, comment, db) + return BotReply(room=cmd.room, text=reply) + + # ── /reject [사유] ─── SR 반려 ─────────────────────────────────── + elif keyword in ("/reject", "!reject"): + if len(parts) < 2: + return BotReply(room=cmd.room, text="사용법: /reject [사유]") + sr_id = parts[1] + reason = " ".join(parts[2:]) if len(parts) > 2 else "봇 반려" + reply = await _cmd_reject_sr(sr_id, cmd.user, reason, db) + return BotReply(room=cmd.room, text=reply) + + # ── /kb <검색어> ─── KB 문서 검색 ─────────────────────────────────────── + elif keyword in ("/kb", "!kb"): + if len(parts) < 2: + return BotReply(room=cmd.room, text="사용법: /kb <검색어>") + query = " ".join(parts[1:]) + reply = await _cmd_kb_search(query, db) + return BotReply(room=cmd.room, text=reply) + + # ── /wbs <프로젝트코드> ─── WBS 지연 현황 ─────────────────────────────── + elif keyword in ("/wbs", "!wbs"): + if len(parts) < 2: + return BotReply(room=cmd.room, text="사용법: /wbs <프로젝트코드>") + proj_code = parts[1] + reply = await _cmd_wbs_status(proj_code, db) + return BotReply(room=cmd.room, text=reply) + + # ── /scouter <서버명> ─── Scouter APM 실시간 메트릭 ───────────────────── + elif keyword in ("/scouter", "!scouter"): + if len(parts) < 2: + return BotReply(room=cmd.room, text="사용법: /scouter <서버명>") + server_name = parts[1] + reply = await _cmd_scouter_metrics(server_name) + return BotReply(room=cmd.room, text=reply) + + # ── /rollback <세션ID> ─── 배포 긴급 롤백 ─────────────────────────────── + elif keyword in ("/rollback", "!rollback"): + if len(parts) < 2: + return BotReply(room=cmd.room, text="사용법: /rollback <세션ID>") + try: + sid = int(parts[1]) + except ValueError: + return BotReply(room=cmd.room, text="세션 ID는 숫자여야 합니다.") + bg.add_task(_cmd_rollback, cmd.room, cmd.user, sid) + return BotReply(room=cmd.room, text=f"[긴급 롤백] Session #{sid} 롤백 시작...") + + # ── /notify <메시지> ─── 운영팀 전체 공지 ─────────────────────────────── + elif keyword in ("/notify", "!notify"): + if len(parts) < 2: + return BotReply(room=cmd.room, text="사용법: /notify <메시지>") + message = " ".join(parts[1:]) + bg.add_task(_cmd_broadcast_notify, cmd.room, cmd.user, message) + return BotReply(room=cmd.room, text=f"[공지] 운영팀 전체에 메시지 발송 중...") + + # ── /topology <서버명> ─── CI 의존관계 조회 ───────────────────────────── + elif keyword in ("/topology", "!topology"): + if len(parts) < 2: + return BotReply(room=cmd.room, text="사용법: /topology <서버명>") + server_name = parts[1] + reply = await _cmd_topology(server_name, db) + return BotReply(room=cmd.room, text=reply) + + # ── /vuln <서버명> ─── 서버 취약점 스캔 ──────────────────────────────── + elif keyword in ("/vuln", "!vuln"): + if len(parts) < 2: + return BotReply(room=cmd.room, text="사용법: /vuln <서버명|IP>") + target = parts[1] + bg.add_task(_cmd_vuln_scan, cmd.room, cmd.user, target) + return BotReply(room=cmd.room, text=f"[취약점 스캔] {target} 스캔 시작 (약 30초 소요)...") + # ── /help ───────────────────────────────────────────────────────────────── elif keyword == "/help": return BotReply(room=cmd.room, text=_help_text()) @@ -515,6 +636,403 @@ async def _cmd_sm(room: str, actor: str, server: str, ) +# ══════════════════════════════════════════════════════════════════════════════ +# 신규 봇 명령어 헬퍼 함수 (14개) +# ══════════════════════════════════════════════════════════════════════════════ + +async def _cmd_oncall() -> str: + """/oncall — 현재 당직자 조회.""" + import httpx as _h + try: + async with _h.AsyncClient(timeout=10.0) as c: + r = await c.get("http://localhost:8001/api/oncall/on-duty", + headers={"Authorization": f"Bearer {_get_internal_token()}"}) + if r.status_code == 200: + d = r.json() + if not d: + return "[당직자] 오늘 등록된 당직자가 없습니다." + lines = ["[현재 당직자]"] + for oc in (d if isinstance(d, list) else [d])[:3]: + eng = oc.get("engineer") or oc.get("username") or "—" + phone = oc.get("phone") or oc.get("contact") or "" + lines.append(f" {eng}" + (f" ({phone})" if phone else "")) + return "\n".join(lines) + return f"당직자 조회 실패 ({r.status_code})" + except Exception as e: + return f"당직자 조회 오류: {str(e)[:80]}" + + +async def _cmd_create_incident(room: str, actor: str, title: str, grade: str): + """/incident — 인시던트 빠른 등록.""" + import httpx as _h + try: + async with _h.AsyncClient(timeout=10.0) as c: + r = await c.post("http://localhost:8001/api/incidents", + headers={"Authorization": f"Bearer {_get_internal_token()}", + "Content-Type": "application/json"}, + json={"title": title, "grade": grade, + "description": f"봇 등록 — 담당: {actor}", + "reported_by": actor}) + if r.status_code == 201: + d = r.json() + inc_id = d.get("incident_id", "?") + grade_emoji = {"P1": "🚨", "P2": "🔴", "P3": "🟠", "P4": "🟡"}.get(grade, "⚠️") + msg = (f"{grade_emoji} [{grade}] 인시던트 등록 완료\n" + f"번호: {inc_id}\n제목: {title}\n담당: {actor}") + await _send_to_room(room, msg) + else: + await _send_to_room(room, f"[인시던트 등록 실패] {r.status_code}: {r.text[:100]}") + except Exception as e: + await _send_to_room(room, f"[인시던트 오류] {str(e)[:80]}") + + +async def _cmd_auto_rca(room: str, actor: str, inc_id: str): + """/rca — AI 자동 RCA 분석.""" + import httpx as _h + try: + async with _h.AsyncClient(timeout=120.0) as c: + r = await c.post(f"http://localhost:8001/api/incidents/{inc_id}/auto-rca", + headers={"Authorization": f"Bearer {_get_internal_token()}"}) + if r.status_code == 200: + d = r.json() + rca = d.get("rca", {}) + msg = (f"[AI RCA 완료] {inc_id}\n" + f"근본원인: {rca.get('root_cause','')[:100]}\n" + f"신뢰도: {int(rca.get('confidence',0)*100)}%\n" + f"재발방지: {', '.join(rca.get('prevention',[])[:2])}") + await _send_to_room(room, msg) + else: + # problem RCA 시도 + async with _h.AsyncClient(timeout=120.0) as c: + r2 = await c.post(f"http://localhost:8001/api/problem/{inc_id}/auto-rca", + headers={"Authorization": f"Bearer {_get_internal_token()}"}) + if r2.status_code == 200: + d = r2.json() + rca = d.get("rca", {}) + msg = (f"[AI RCA 완료] {inc_id}\n" + f"근본원인: {rca.get('root_cause','')[:100]}\n" + f"신뢰도: {int(rca.get('confidence',0)*100)}%") + await _send_to_room(room, msg) + else: + await _send_to_room(room, f"[RCA 실패] {inc_id} — 인시던트/Problem ID를 확인하세요") + except Exception as e: + await _send_to_room(room, f"[RCA 오류] {str(e)[:80]}") + + +async def _cmd_escalate(room: str, actor: str, sr_id: str): + """/escalate — SR 에스컬레이션.""" + import httpx as _h + try: + # 당직자 조회 + async with _h.AsyncClient(timeout=10.0) as c: + oc_r = await c.get("http://localhost:8001/api/oncall/on-duty", + headers={"Authorization": f"Bearer {_get_internal_token()}"}) + oncall_info = oc_r.json() if oc_r.status_code == 200 else {} + oncall_eng = "" + if isinstance(oncall_info, list) and oncall_info: + oncall_eng = oncall_info[0].get("engineer", "") + elif isinstance(oncall_info, dict): + oncall_eng = oncall_info.get("engineer", "") + + # SLA 에스컬레이션 API 호출 + async with _h.AsyncClient(timeout=10.0) as c: + r = await c.post("http://localhost:8001/api/oncall/escalate", + headers={"Authorization": f"Bearer {_get_internal_token()}", + "Content-Type": "application/json"}, + json={"sr_id": sr_id, "escalate_to": oncall_eng, + "reason": f"봇 에스컬레이션 — 요청자: {actor}"}) + if r.status_code in (200, 201): + msg = (f"[에스컬레이션 완료] {sr_id}\n" + f"담당자: {oncall_eng or '당직자'}\n" + f"요청자: {actor}") + else: + msg = f"[에스컬레이션] {sr_id} → 당직자({oncall_eng or '미정'})에게 전달됨 (SR 상태 확인 필요)" + await _send_to_room(room, msg) + except Exception as e: + await _send_to_room(room, f"[에스컬레이션 오류] {str(e)[:80]}") + + +async def _cmd_sla_violations() -> str: + """/sla — SLA 위반 현황.""" + import httpx as _h + try: + async with _h.AsyncClient(timeout=10.0) as c: + r = await c.get("http://localhost:8001/api/tasks/sla/violations", + headers={"Authorization": f"Bearer {_get_internal_token()}"}) + if r.status_code == 200: + rows = r.json() + if not rows: + return "[SLA] 현재 위반 중인 SR이 없습니다. ✅" + lines = [f"[SLA 위반] {len(rows)}건"] + for sr in rows[:5]: + overdue = sr.get("overdue_minutes", 0) + h, m = divmod(overdue, 60) + lines.append(f" {sr.get('sr_id','?')} [{sr.get('priority','?')}] " + f"{sr.get('title','')[:25]} — {h}시간{m}분 초과") + if len(rows) > 5: + lines.append(f" ... 외 {len(rows)-5}건") + return "\n".join(lines) + return f"SLA 조회 실패 ({r.status_code})" + except Exception as e: + return f"SLA 조회 오류: {str(e)[:80]}" + + +async def _cmd_assign_sr(sr_id: str, assignee: str, actor: str, db) -> str: + """/assign — SR 담당자 즉시 배정.""" + import httpx as _h + try: + async with _h.AsyncClient(timeout=10.0) as c: + r = await c.post(f"http://localhost:8001/api/assign/{sr_id}", + headers={"Authorization": f"Bearer {_get_internal_token()}", + "Content-Type": "application/json"}, + json={"engineer": assignee, "reason": f"봇 배정 by {actor}"}) + if r.status_code in (200, 201): + return f"[배정 완료] {sr_id} → {assignee}" + # 직접 상태 업데이트로 폴백 + async with _h.AsyncClient(timeout=10.0) as c: + r2 = await c.patch(f"http://localhost:8001/api/tasks/{sr_id}/status", + headers={"Authorization": f"Bearer {_get_internal_token()}", + "Content-Type": "application/json"}, + json={"status": "IN_PROGRESS", "actor": actor, + "comment": f"담당자 배정: {assignee}"}) + return (f"[배정] {sr_id} → {assignee} (수동 확인 필요)" + if r2.status_code == 200 else f"배정 실패 ({r.status_code})") + except Exception as e: + return f"배정 오류: {str(e)[:80]}" + + +async def _cmd_approve_sr(sr_id: str, actor: str, comment: str, db) -> str: + """/approve — SR 즉시 승인.""" + import httpx as _h + try: + async with _h.AsyncClient(timeout=10.0) as c: + r = await c.post(f"http://localhost:8001/api/approvals/{sr_id}", + headers={"Authorization": f"Bearer {_get_internal_token()}", + "Content-Type": "application/json"}, + json={"approver": actor, "result": "APPROVED", + "comment": f"[봇승인] {comment}"}) + if r.status_code == 201: + return f"[승인 완료] {sr_id} — 승인자: {actor}" + return f"승인 실패 ({r.status_code}) — SR이 승인 대기 상태인지 확인" + except Exception as e: + return f"승인 오류: {str(e)[:80]}" + + +async def _cmd_reject_sr(sr_id: str, actor: str, reason: str, db) -> str: + """/reject — SR 반려.""" + import httpx as _h + try: + async with _h.AsyncClient(timeout=10.0) as c: + r = await c.post(f"http://localhost:8001/api/approvals/{sr_id}", + headers={"Authorization": f"Bearer {_get_internal_token()}", + "Content-Type": "application/json"}, + json={"approver": actor, "result": "REJECTED", + "comment": f"[봇반려] {reason}"}) + if r.status_code == 201: + return f"[반려 완료] {sr_id} — 사유: {reason}" + return f"반려 실패 ({r.status_code})" + except Exception as e: + return f"반려 오류: {str(e)[:80]}" + + +async def _cmd_kb_search(query: str, db) -> str: + """/kb — KB 문서 검색.""" + import httpx as _h + try: + async with _h.AsyncClient(timeout=10.0) as c: + r = await c.get("http://localhost:8001/api/kb", + params={"keyword": query, "limit": 5}, + headers={"Authorization": f"Bearer {_get_internal_token()}"}) + if r.status_code == 200: + docs = r.json() + if isinstance(docs, dict): + docs = docs.get("items", docs.get("results", [])) + if not docs: + return f"[KB 검색] '{query}' 결과 없음 — 다른 키워드로 시도해 보세요." + lines = [f"[KB 검색] '{query}' — {len(docs)}건"] + for d in docs[:4]: + title = d.get("title", "") + doc_id = d.get("doc_id", d.get("id", "")) + lines.append(f" [{doc_id}] {title[:45]}") + return "\n".join(lines) + return f"KB 검색 실패 ({r.status_code})" + except Exception as e: + return f"KB 검색 오류: {str(e)[:80]}" + + +async def _cmd_wbs_status(proj_code: str, db) -> str: + """/wbs — WBS 지연 현황.""" + import httpx as _h + from datetime import date as _date + 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: + return f"프로젝트를 찾을 수 없습니다: {proj_code}" + + async with _h.AsyncClient(timeout=10.0) as c: + r = await c.get(f"http://localhost:8001/api/si/projects/{proj.id}/wbs", + headers={"Authorization": f"Bearer {_get_internal_token()}"}) + if r.status_code == 200: + wbs_items = r.json() + today = str(_date.today()) + delayed = [ + w for w in wbs_items + if w.get("completion_pct", 0) < 100 + and w.get("planned_end", "") and w.get("planned_end", "") < today + and w.get("is_leaf", True) + ] + lines = [f"[WBS 현황] {proj_code}", + f"전체: {len(wbs_items)}건 | 지연: {len(delayed)}건"] + for w in delayed[:5]: + lines.append(f" {w.get('wbs_code','?')} {w.get('title','')[:30]} " + f"({w.get('completion_pct',0)}%)") + return "\n".join(lines) + return f"WBS 조회 실패 ({r.status_code})" + except Exception as e: + return f"WBS 조회 오류: {str(e)[:80]}" + + +async def _cmd_scouter_metrics(server_name: str) -> str: + """/scouter — Scouter APM 실시간 메트릭.""" + import httpx as _h + try: + async with _h.AsyncClient(timeout=10.0) as c: + r = await c.get("http://localhost:8001/api/scouter/servers", + headers={"Authorization": f"Bearer {_get_internal_token()}"}) + if r.status_code != 200: + return "Scouter 서버 목록 조회 실패" + + servers = r.json().get("servers", []) + target = next((s for s in servers + if server_name.lower() in s.get("objName","").lower()), None) + if not target: + return f"[Scouter] '{server_name}' 서버를 찾을 수 없습니다.\n모니터링 서버 목록을 확인하세요." + + obj_hash = target.get("objHash") + async with _h.AsyncClient(timeout=10.0) as c: + r2 = await c.get(f"http://localhost:8001/api/scouter/servers/{obj_hash}/metrics", + headers={"Authorization": f"Bearer {_get_internal_token()}"}) + if r2.status_code == 200: + m = r2.json().get("metrics", {}) + return (f"[Scouter] {target.get('objName')}\n" + f"CPU: {m.get('cpu',0):.1f}% | " + f"Heap: {m.get('heap_used',0)}MB/{m.get('heap_max',0)}MB\n" + f"TPS: {m.get('tps',0):.1f} | " + f"응답: {m.get('response_time',0):.0f}ms | " + f"에러: {m.get('error_rate',0):.1f}%") + return f"메트릭 조회 실패 ({r2.status_code})" + except Exception as e: + return f"Scouter 조회 오류: {str(e)[:80]}" + + +async def _cmd_rollback(room: str, actor: str, session_id: int): + """/rollback — 배포 긴급 롤백.""" + import httpx as _h + try: + async with _h.AsyncClient(timeout=120.0) as c: + r = await c.post(f"http://localhost:8001/api/vibe/{session_id}/status", + headers={"Authorization": f"Bearer {_get_internal_token()}", + "Content-Type": "application/json"}, + json={"status": "FAILED", "actor": actor, + "note": "봇 긴급 롤백 요청"}) + msg = (f"[긴급 롤백] Session #{session_id}\n" + + ("롤백 처리 중 — Jenkins 파이프라인을 확인하세요." + if r.status_code == 200 + else f"롤백 요청 전송 ({r.status_code}) — 수동 확인 필요")) + await _send_to_room(room, msg) + except Exception as e: + await _send_to_room(room, f"[롤백 오류] {str(e)[:80]}") + + +async def _cmd_broadcast_notify(room: str, actor: str, message: str): + """/notify — 운영팀 전체 공지 발송.""" + full_msg = f"[공지 — {actor}]\n{message}" + # 운영팀 채널로 발송 + await _send_to_room(room, full_msg) + # ops 채널 추가 발송 (다른 채널이면) + if room != "ops": + await _send_to_room("ops", full_msg) + + +async def _cmd_topology(server_name: str, db) -> str: + """/topology — 서버 CI 의존관계.""" + import httpx as _h + from models import Server, ConfigItem + from sqlalchemy import select as _sel + try: + async with SessionLocal() as _db: + srv = (await _db.execute( + _sel(Server).where(Server.server_name.contains(server_name)) + )).scalars().first() + if not srv: + return f"[토폴로지] 서버 '{server_name}'를 찾을 수 없습니다." + ci = (await _db.execute( + _sel(ConfigItem).where(ConfigItem.linked_server_id == srv.id) + )).scalars().first() + + if not ci: + return f"[토폴로지] '{server_name}' — CMDB CI가 연결되지 않았습니다." + + async with _h.AsyncClient(timeout=10.0) as c: + r = await c.get(f"http://localhost:8001/api/topology/graph/{ci.id}", + params={"depth": 2}, + headers={"Authorization": f"Bearer {_get_internal_token()}"}) + if r.status_code == 200: + d = r.json() + nodes = d.get("nodes", []) + links = d.get("links", []) + lines = [f"[토폴로지] {server_name} ({ci.ci_type})", + f"연결 노드: {len(nodes)}개 | 관계: {len(links)}개"] + for n in nodes[:6]: + if not n.get("is_root"): + lines.append(f" └─ {n.get('name','')} [{n.get('type','')}]") + if len(nodes) > 7: + lines.append(f" ... 외 {len(nodes)-7}개") + lines.append(f"상세: http://localhost:8001/api/topology/page?ci={ci.id}") + return "\n".join(lines) + return f"토폴로지 조회 실패 ({r.status_code})" + except Exception as e: + return f"토폴로지 오류: {str(e)[:80]}" + + +async def _cmd_vuln_scan(room: str, actor: str, target: str): + """/vuln — 서버 취약점 스캔.""" + import httpx as _h + try: + async with _h.AsyncClient(timeout=60.0) as c: + r = await c.post("http://localhost:8001/api/vuln/scan", + headers={"Authorization": f"Bearer {_get_internal_token()}", + "Content-Type": "application/json"}, + json={"host": target, "include_llm": False, "timeout": 1.0}) + if r.status_code == 202: + d = r.json() + scan_id = d.get("scan_id", "?") + # 결과 폴링 (최대 30초) + import asyncio as _aio + await _aio.sleep(30) + async with _h.AsyncClient(timeout=10.0) as c2: + r2 = await c2.get(f"http://localhost:8001/api/vuln/scans/{scan_id}", + headers={"Authorization": f"Bearer {_get_internal_token()}"}) + if r2.status_code == 200: + res = r2.json() + msg = (f"[취약점 스캔 완료] {target}\n" + f"위험 수준: {res.get('risk_level','?')} | 점수: {res.get('risk_score',0)}\n" + f"취약점: {len(res.get('vulnerabilities',[]))}개\n" + f"열린 포트: {', '.join(str(p) for p in res.get('open_ports',[])[:5])}") + else: + msg = f"[취약점 스캔] {target} 완료 — scan_id: {scan_id} (결과 조회: /api/vuln/scans/{scan_id})" + else: + msg = f"[취약점 스캔 실패] {target} ({r.status_code})" + await _send_to_room(room, msg) + except Exception as e: + await _send_to_room(room, f"[스캔 오류] {str(e)[:80]}") + + async def _cmd_project_report(room: str, actor: str, proj_code: str, rtype: str, db): """/report — 프로젝트 보고서 생성 후 요약 발송.""" import httpx as _httpx @@ -855,38 +1373,55 @@ def _get_internal_token() -> str: def _help_text() -> str: return """GUARDiA ITSM 봇 명령어 -━━━━━━━━━━━━━━━━━━━━ +━━━━━━━━━━━━━━━━━━━━━━━━ [SR 관리] -/sr <제목> → SR 빠른 접수 -!status → SR + 세션 상태 조회 -/bulk → SR 대량 처리 (close/assign/status) +/sr <제목> → SR 빠른 접수 +!status → SR + 세션 상태 조회 +/bulk → SR 대량 처리 +/assign <담당자> → SR 담당자 즉시 배정 +/approve [의견] → SR 즉시 승인 +/reject [사유] → SR 반려 +/escalate → 당직자에게 에스컬레이션 +/sla → SLA 위반 현황 -[PMS 프로젝트 관리] -/pms <프로젝트코드> → 프로젝트 진척 현황 +[인시던트 / 장애] +/incident <제목> [P1|P2|P3|P4] → 인시던트 빠른 등록 +/rca → AI 자동 RCA 분석 +/oncall → 현재 당직자 조회 + +[PMS 프로젝트] +/pms <코드> → 프로젝트 진척 현황 +/wbs <코드> → WBS 지연 현황 /report <코드> [daily|weekly|monthly] → 보고서 발송 -/deliverables <프로젝트코드> → 산출물 제출 현황 -/issues <프로젝트코드> → 미결 이슈 목록 +/deliverables <코드> → 산출물 제출 현황 +/issues <코드> → 미결 이슈 목록 -[보안/품질] -/scan → 시큐어코딩/웹접근성/개인정보 점검 -/checklist → 공공기관 필수 기능 현황 -/perf [url] → 성능 테스트 실행 +[인프라 / 보안] +/topology <서버명> → CI 의존관계 조회 +/scouter <서버명> → Scouter APM 실시간 메트릭 +/vuln <서버명|IP> → 취약점 스캔 +/scan → 소스 보안 점검 +/checklist → 공공기관 이행 현황 +/perf [url] → 성능 테스트 -[배포] -!vibe [project_id] → 바이브 코딩 세션 시작 -!build → 빌드 실행 -!deploy → 배포 (SSH + WAS 재기동) -!cancel → 세션 취소 +[배포 제어] +!vibe [project_id] → 바이브 코딩 세션 +!build → 빌드 실행 +!deploy → 배포 +/rollback → 긴급 롤백 +!cancel → 세션 취소 -[운영 정보] -/status → 시스템 현황 요약 -/license → 라이선스 상태 조회 +[정보 / 운영] +/status → 시스템 현황 +/license → 라이선스 상태 +/kb <검색어> → KB 문서 검색 +/notify <메시지> → 운영팀 전체 공지 [SM 원격 제어] -!sm