- itsm/ -> workspace/guardia-itsm/ - manager/ -> workspace/guardia-manager/ - app/ -> workspace/guardia-messenger/ - manual/ -> workspace/guardia-docs/ workspace/zioinfo-web/ unchanged. git mv preserves full commit history. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
370 lines
15 KiB
Python
370 lines
15 KiB
Python
"""
|
|
GUARDiA ITSM 온보딩 가이드 API
|
|
|
|
설치 완료 후 자동 시작되어 사용자를 단계별로 안내.
|
|
|
|
엔드포인트:
|
|
GET /api/onboarding/status — 현재 온보딩 상태
|
|
POST /api/onboarding/step — 단계 업데이트
|
|
POST /api/onboarding/message — 현재 화면 기반 봇 응답
|
|
POST /api/onboarding/complete — 온보딩 완료 처리
|
|
POST /api/onboarding/reset — 온보딩 초기화 (재시작)
|
|
GET /api/onboarding/steps — 전체 단계 목록
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from core.auth import get_current_user
|
|
from database import get_db
|
|
from models import User
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/api/onboarding", tags=["onboarding"])
|
|
|
|
# ── 온보딩 단계 정의 ─────────────────────────────────────────────────────────
|
|
|
|
ONBOARDING_STEPS = [
|
|
{
|
|
"id": "welcome",
|
|
"order": 0,
|
|
"title": "환영합니다!",
|
|
"icon": "👋",
|
|
"view": None, # 어느 화면에서나
|
|
"target": None, # 하이라이트할 요소
|
|
"message": (
|
|
"안녕하세요! GUARDiA ITSM에 오신 것을 환영합니다.\n\n"
|
|
"설치가 완료되었습니다. 지금부터 **5단계** 가이드로\n"
|
|
"첫 번째 프로젝트를 등록하고 소스코드 관리까지 완료해 드리겠습니다.\n\n"
|
|
"준비가 되셨으면 **시작하기**를 눌러주세요."
|
|
),
|
|
"actions": [{"label": "시작하기 →", "action": "next"}],
|
|
},
|
|
{
|
|
"id": "change_password",
|
|
"order": 1,
|
|
"title": "비밀번호 변경",
|
|
"icon": "🔑",
|
|
"view": "/change-password",
|
|
"target": "#current-password",
|
|
"message": (
|
|
"보안을 위해 **초기 비밀번호를 변경**해 주세요.\n\n"
|
|
"초기 비밀번호: `1111`\n\n"
|
|
"현재 비밀번호에 `1111`을 입력하고,\n"
|
|
"새로운 안전한 비밀번호로 변경해 주세요.\n\n"
|
|
"💡 **강력한 비밀번호 조건**\n"
|
|
"- 8자 이상\n- 대문자 + 숫자 + 특수문자 포함"
|
|
),
|
|
"actions": [
|
|
{"label": "비밀번호 변경 페이지로", "action": "navigate", "path": "/change-password"},
|
|
{"label": "이미 변경했어요", "action": "next"},
|
|
],
|
|
},
|
|
{
|
|
"id": "explore_dashboard",
|
|
"order": 2,
|
|
"title": "대시보드 둘러보기",
|
|
"icon": "📊",
|
|
"view": "/",
|
|
"target": ".dash-tab-nav",
|
|
"message": (
|
|
"이곳이 GUARDiA **통합 대시보드**입니다.\n\n"
|
|
"4가지 탭으로 구성되어 있어요:\n"
|
|
"- 📊 **운영 현황** — SR·SLA·이슈 현황\n"
|
|
"- 🖥️ **인프라** — 서버·CMDB 현황\n"
|
|
"- 🔒 **보안** — 취약점·패치 현황\n"
|
|
"- 🤖 **AI 인사이트** — 예측·이상탐지\n\n"
|
|
"각 탭을 클릭해서 살펴보세요!"
|
|
),
|
|
"actions": [
|
|
{"label": "대시보드로 이동", "action": "navigate", "path": "/"},
|
|
{"label": "다음 단계 →", "action": "next"},
|
|
],
|
|
},
|
|
{
|
|
"id": "create_project",
|
|
"order": 3,
|
|
"title": "첫 프로젝트 등록",
|
|
"icon": "🏗️",
|
|
"view": "/si",
|
|
"target": ".btn-primary",
|
|
"message": (
|
|
"**PMS(프로젝트 관리)**에서 첫 번째 프로젝트를 등록해보세요.\n\n"
|
|
"**1.** 좌측 메뉴 → **PMS 프로젝트** 클릭\n"
|
|
"**2.** 우측 상단 **+ 프로젝트 생성** 버튼 클릭\n"
|
|
"**3.** 아래 정보를 입력하세요:\n\n"
|
|
"```\n프로젝트 코드: PRJ-2026-001\n프로젝트명: 우리 회사 정보화사업\n담당 PM: admin\n```\n\n"
|
|
"등록하면 WBS, 산출물, 보고서 기능을 바로 사용할 수 있어요."
|
|
),
|
|
"actions": [
|
|
{"label": "프로젝트 관리로 이동", "action": "navigate", "path": "/si"},
|
|
{"label": "프로젝트 등록 완료", "action": "next"},
|
|
],
|
|
},
|
|
{
|
|
"id": "register_server",
|
|
"order": 4,
|
|
"title": "관리 서버 등록",
|
|
"icon": "🖥️",
|
|
"view": "/",
|
|
"target": "[data-view='cmdb']",
|
|
"message": (
|
|
"**CMDB**에 관리할 서버를 등록해 주세요.\n\n"
|
|
"**1.** 좌측 메뉴 → **인프라 → CMDB** 클릭\n"
|
|
"**2.** **서버 등록** 버튼 클릭\n"
|
|
"**3.** 아래 정보를 입력하세요:\n\n"
|
|
"```\n서버명: web-server-01\nIP: 10.0.0.1\nOS: CentOS 7\nSSH 계정: opsagent\n```\n\n"
|
|
"⚠️ 보안: SSH 비밀번호는 AES-256-GCM으로 암호화 저장됩니다.\n\n"
|
|
"서버 등록 후 메신저 봇으로 즉시 제어할 수 있어요."
|
|
),
|
|
"actions": [
|
|
{"label": "CMDB로 이동", "action": "navigate", "path": "/?view=cmdb"},
|
|
{"label": "나중에 등록", "action": "next"},
|
|
{"label": "서버 등록 완료", "action": "next"},
|
|
],
|
|
},
|
|
{
|
|
"id": "register_source",
|
|
"order": 5,
|
|
"title": "소스 코드 등록",
|
|
"icon": "📦",
|
|
"view": None,
|
|
"target": None,
|
|
"message": (
|
|
"**Gitea**에 소스 코드를 등록하세요.\n\n"
|
|
"Gitea는 설치 시 자동으로 구성되었습니다.\n\n"
|
|
"**1.** Gitea 접속: `http://[서버IP]:3000`\n"
|
|
"**2.** 계정: `gitadmin / Gitea@guardia!`\n"
|
|
"**3.** guardia 조직 → GUARDiA 저장소에 소스 Push:\n\n"
|
|
"```bash\ngit remote add guardia \\\n http://gitadmin@[서버IP]:3000/guardia/GUARDiA.git\ngit push guardia main\n```\n\n"
|
|
"등록 후 Jenkins CI/CD와 자동 연동됩니다."
|
|
),
|
|
"actions": [
|
|
{"label": "Gitea 열기", "action": "external", "url": "http://localhost:3000"},
|
|
{"label": "소스 등록 완료", "action": "next"},
|
|
],
|
|
},
|
|
{
|
|
"id": "setup_messenger",
|
|
"order": 6,
|
|
"title": "메신저 봇 연결",
|
|
"icon": "💬",
|
|
"view": None,
|
|
"target": None,
|
|
"message": (
|
|
"**메신저 봇**을 연결하면 채팅으로 인프라를 제어할 수 있어요.\n\n"
|
|
"지원 플랫폼:\n"
|
|
"- 카카오워크\n- 네이버웍스\n- 슬랙\n\n"
|
|
"**설정 방법:**\n"
|
|
"`.env` 파일에 아래를 추가:\n\n"
|
|
"```\nMESSENGER_BASE_URL=http://메신저서버:8002\nBOT_SEND_API=http://봇API주소\n```\n\n"
|
|
"설정 후 `/sr 테스트 메시지`로 동작을 확인하세요.\n\n"
|
|
"📌 봇 명령어 25개: `/sr`, `/deploy`, `/rca`, `/scan` 등"
|
|
),
|
|
"actions": [
|
|
{"label": "나중에 설정", "action": "next"},
|
|
{"label": "설정 완료", "action": "next"},
|
|
],
|
|
},
|
|
{
|
|
"id": "complete",
|
|
"order": 7,
|
|
"title": "설정 완료!",
|
|
"icon": "🎉",
|
|
"view": None,
|
|
"target": None,
|
|
"message": (
|
|
"**축하합니다!** GUARDiA ITSM 초기 설정이 완료되었습니다.\n\n"
|
|
"✅ 비밀번호 변경\n"
|
|
"✅ 프로젝트 등록\n"
|
|
"✅ 서버 등록\n"
|
|
"✅ 소스 코드 등록\n"
|
|
"✅ 메신저 봇 연결\n\n"
|
|
"이제 GUARDiA의 모든 기능을 사용할 수 있습니다!\n\n"
|
|
"도움이 필요하면 우측 하단 **?** 버튼을 클릭하거나\n"
|
|
"봇 명령어 `/help`를 입력하세요."
|
|
),
|
|
"actions": [
|
|
{"label": "완료 — 시작하기!", "action": "complete"},
|
|
],
|
|
},
|
|
]
|
|
|
|
# ── 온보딩 상태 (운영 시 DB 테이블로 이전) ─────────────────────────────────
|
|
# 사용자별 상태 저장
|
|
_onboarding_state: dict[str, dict] = {}
|
|
|
|
|
|
def _get_user_state(username: str) -> dict:
|
|
if username not in _onboarding_state:
|
|
_onboarding_state[username] = {
|
|
"username": username,
|
|
"current_step": 0,
|
|
"completed": False,
|
|
"dismissed": False,
|
|
"started_at": datetime.utcnow().isoformat(),
|
|
"completed_at": None,
|
|
"step_history": [],
|
|
}
|
|
return _onboarding_state[username]
|
|
|
|
|
|
# ── 스키마 ───────────────────────────────────────────────────────────────────
|
|
|
|
class StepUpdateRequest(BaseModel):
|
|
step_id: str
|
|
action: str = "complete" # complete | skip | back
|
|
|
|
|
|
class MessageRequest(BaseModel):
|
|
current_view: str = "/"
|
|
current_step: Optional[str] = None
|
|
user_message: Optional[str] = None
|
|
|
|
|
|
# ── 엔드포인트 ───────────────────────────────────────────────────────────────
|
|
|
|
@router.get("/status")
|
|
async def get_onboarding_status(
|
|
cu: User = Depends(get_current_user),
|
|
):
|
|
"""현재 온보딩 상태 반환."""
|
|
state = _get_user_state(cu.username)
|
|
step_idx = state["current_step"]
|
|
current = ONBOARDING_STEPS[step_idx] if step_idx < len(ONBOARDING_STEPS) else None
|
|
|
|
return {
|
|
"username": cu.username,
|
|
"current_step": step_idx,
|
|
"total_steps": len(ONBOARDING_STEPS),
|
|
"completed": state["completed"],
|
|
"dismissed": state["dismissed"],
|
|
"show_bot": not state["completed"] and not state["dismissed"],
|
|
"current": current,
|
|
"progress_pct": round(step_idx / len(ONBOARDING_STEPS) * 100, 0),
|
|
}
|
|
|
|
|
|
@router.get("/steps")
|
|
async def get_steps(_u: User = Depends(get_current_user)):
|
|
"""전체 온보딩 단계 목록."""
|
|
return {"steps": ONBOARDING_STEPS, "total": len(ONBOARDING_STEPS)}
|
|
|
|
|
|
@router.post("/step")
|
|
async def update_step(
|
|
body: StepUpdateRequest,
|
|
cu: User = Depends(get_current_user),
|
|
):
|
|
"""단계 업데이트 (완료/건너뜀/뒤로)."""
|
|
state = _get_user_state(cu.username)
|
|
|
|
current_idx = state["current_step"]
|
|
|
|
if body.action in ("complete", "next", "skip"):
|
|
if current_idx < len(ONBOARDING_STEPS) - 1:
|
|
state["current_step"] = current_idx + 1
|
|
state["step_history"].append({
|
|
"step_id": body.step_id,
|
|
"action": body.action,
|
|
"at": datetime.utcnow().isoformat(),
|
|
})
|
|
elif body.action == "back" and current_idx > 0:
|
|
state["current_step"] = current_idx - 1
|
|
|
|
new_idx = state["current_step"]
|
|
new_step = ONBOARDING_STEPS[new_idx] if new_idx < len(ONBOARDING_STEPS) else None
|
|
|
|
return {
|
|
"current_step": new_idx,
|
|
"step": new_step,
|
|
"completed": state["completed"],
|
|
}
|
|
|
|
|
|
@router.post("/complete")
|
|
async def complete_onboarding(cu: User = Depends(get_current_user)):
|
|
"""온보딩 완료 처리."""
|
|
state = _get_user_state(cu.username)
|
|
state["completed"] = True
|
|
state["current_step"] = len(ONBOARDING_STEPS) - 1
|
|
state["completed_at"] = datetime.utcnow().isoformat()
|
|
return {"message": "온보딩 완료!", "completed": True}
|
|
|
|
|
|
@router.post("/dismiss")
|
|
async def dismiss_onboarding(cu: User = Depends(get_current_user)):
|
|
"""온보딩 임시 숨기기."""
|
|
state = _get_user_state(cu.username)
|
|
state["dismissed"] = True
|
|
return {"dismissed": True}
|
|
|
|
|
|
@router.post("/reset")
|
|
async def reset_onboarding(cu: User = Depends(get_current_user)):
|
|
"""온보딩 초기화 (재시작)."""
|
|
_onboarding_state.pop(cu.username, None)
|
|
return {"message": "온보딩 초기화 완료 — 새로고침하면 다시 시작됩니다."}
|
|
|
|
|
|
@router.post("/message")
|
|
async def get_bot_message(
|
|
body: MessageRequest,
|
|
cu: User = Depends(get_current_user),
|
|
):
|
|
"""현재 화면 + 사용자 메시지 기반 봇 응답 (Ollama 연동)."""
|
|
state = _get_user_state(cu.username)
|
|
|
|
# 현재 단계 메시지 반환 (기본)
|
|
step_idx = state["current_step"]
|
|
current = ONBOARDING_STEPS[step_idx] if step_idx < len(ONBOARDING_STEPS) else None
|
|
|
|
# 사용자 질문이 있으면 Ollama로 답변
|
|
if body.user_message:
|
|
try:
|
|
from core.llm_client import get_llm_client
|
|
context = (
|
|
f"당신은 GUARDiA ITSM 설치 가이드 AI입니다. "
|
|
f"현재 사용자는 '{current['title'] if current else '설치 완료'}' 단계에 있습니다. "
|
|
f"현재 화면: {body.current_view}\n"
|
|
f"사용자 질문: {body.user_message}\n\n"
|
|
f"간결하고 친절하게 한국어로 답변하세요. "
|
|
f"GUARDiA API나 기능에 대해 모르면 솔직히 말하세요."
|
|
)
|
|
client = get_llm_client()
|
|
resp = await client.chat(context)
|
|
return {
|
|
"type": "llm_answer",
|
|
"message": resp.content.strip()[:500],
|
|
"current": current,
|
|
}
|
|
except Exception as e:
|
|
logger.debug("Ollama 응답 실패: %s", e)
|
|
|
|
# 화면별 컨텍스트 힌트
|
|
view_hints = {
|
|
"/": "대시보드 화면입니다. 좌측 메뉴에서 다양한 기능에 접근할 수 있어요.",
|
|
"/change-password":"비밀번호 변경 화면입니다. 현재 비밀번호 1111을 새 비밀번호로 변경하세요.",
|
|
"/si": "PMS 프로젝트 관리 화면입니다. + 버튼으로 새 프로젝트를 만드세요.",
|
|
"/incidents": "인시던트 관리 화면입니다. 장애 발생 시 여기서 처리하세요.",
|
|
"/agents": "AI 에이전트 현황입니다. Ollama 연결 상태를 확인할 수 있어요.",
|
|
"/license": "라이선스 관리 화면입니다. 현재 체험판 라이선스가 적용되어 있어요.",
|
|
}
|
|
hint = view_hints.get(body.current_view, "")
|
|
|
|
return {
|
|
"type": "step_guide",
|
|
"message": current["message"] if current else "온보딩이 완료되었습니다!",
|
|
"hint": hint,
|
|
"current": current,
|
|
"step_idx":step_idx,
|
|
"progress":round(step_idx / len(ONBOARDING_STEPS) * 100, 0),
|
|
}
|