diff --git a/itsm/main.py b/itsm/main.py
index d049e0e2..c77fadd5 100644
--- a/itsm/main.py
+++ b/itsm/main.py
@@ -46,6 +46,7 @@ from routers import (
jmeter,
public_checklist,
customer_portal,
+ onboarding,
groupware,
siem,
topology,
@@ -264,6 +265,7 @@ app.include_router(public_checklist.router)
# 추가 기능
app.include_router(customer_portal.router) # 고객 셀프서비스 포털
+app.include_router(onboarding.router) # 온보딩 가이드 챗봇
app.include_router(groupware.router) # 그룹웨어 전자결재 연동
app.include_router(siem.router) # SIEM 보안 이벤트 연동
app.include_router(topology.router) # 네트워크 토폴로지 시각화
diff --git a/itsm/routers/onboarding.py b/itsm/routers/onboarding.py
new file mode 100644
index 00000000..04832d1c
--- /dev/null
+++ b/itsm/routers/onboarding.py
@@ -0,0 +1,369 @@
+"""
+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),
+ }
diff --git a/itsm/static/index.html b/itsm/static/index.html
index 5769b467..7ce4b8b2 100644
--- a/itsm/static/index.html
+++ b/itsm/static/index.html
@@ -786,5 +786,7 @@
+
+