feat(bot): PMS/보안/성능 봇 명령어 6개 추가
/pms /report /deliverables /issues /scan /checklist /perf 도움말 그룹 정리: SR/PMS/보안품질/배포/운영/SM Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1f8b926066
commit
62d3d14b5e
@ -273,6 +273,58 @@ async def handle_bot_command(
|
|||||||
bg.add_task(_cmd_bulk_action, cmd.room, cmd.user, action, sr_ids)
|
bg.add_task(_cmd_bulk_action, cmd.room, cmd.user, action, sr_ids)
|
||||||
return BotReply(room=cmd.room, text=f"[대량처리] {len(sr_ids)}건 {action} 처리 중...")
|
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 ─────────────────────────────────────────────────────────────────
|
# ── /help ─────────────────────────────────────────────────────────────────
|
||||||
elif keyword == "/help":
|
elif keyword == "/help":
|
||||||
return BotReply(room=cmd.room, text=_help_text())
|
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):
|
async def _cmd_create_sr(room: str, actor: str, title: str):
|
||||||
"""슬래시 /sr 명령 — SR 빠른 접수 (내부 API 호출)."""
|
"""슬래시 /sr 명령 — SR 빠른 접수 (내부 API 호출)."""
|
||||||
import httpx as _httpx
|
import httpx as _httpx
|
||||||
@ -583,6 +861,17 @@ def _help_text() -> str:
|
|||||||
!status <sr_id> → SR + 세션 상태 조회
|
!status <sr_id> → SR + 세션 상태 조회
|
||||||
/bulk <action> <SR-ID...> → SR 대량 처리 (close/assign/status)
|
/bulk <action> <SR-ID...> → SR 대량 처리 (close/assign/status)
|
||||||
|
|
||||||
|
[PMS 프로젝트 관리]
|
||||||
|
/pms <프로젝트코드> → 프로젝트 진척 현황
|
||||||
|
/report <코드> [daily|weekly|monthly] → 보고서 발송
|
||||||
|
/deliverables <프로젝트코드> → 산출물 제출 현황
|
||||||
|
/issues <프로젝트코드> → 미결 이슈 목록
|
||||||
|
|
||||||
|
[보안/품질]
|
||||||
|
/scan → 시큐어코딩/웹접근성/개인정보 점검
|
||||||
|
/checklist → 공공기관 필수 기능 현황
|
||||||
|
/perf [url] → 성능 테스트 실행
|
||||||
|
|
||||||
[배포]
|
[배포]
|
||||||
!vibe <sr_id> [project_id] → 바이브 코딩 세션 시작
|
!vibe <sr_id> [project_id] → 바이브 코딩 세션 시작
|
||||||
!build <session_id> → 빌드 실행
|
!build <session_id> → 빌드 실행
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user