[필수-1] 언인스톨 스크립트 (이식성 > 설치성)
- setup/uninstall.sh: Linux 완전 제거 (표준/purge 모드)
- 백업 → 서비스중지 → Ollama/Gitea → 파일/DB 제거 → 보고
- setup/uninstall.ps1: Windows 완전 제거 (NSSM 서비스 제거)
- -Purge -NoBackup -KeepJava -KeepDb 파라미터
[필수-2] 화면별 도움말 시스템 (사용성)
- static/help.js: 7개 화면 도움말 DB + F1/? 버튼 자동 삽입
- 팝업: 아이콘+제목+내용+주제별 네비게이션
- 키보드: F1(열기), ESC(닫기)
- 검색: 도움말 전체 텍스트 검색
[필수-3] 에러 코드 목록 (기능 적합성)
- GET /api/admin/errors/codes: 17개 에러코드 + 해결방법
AUTH_001~004, SR_001~004, LIC_001~003, CMDB_001~002, AI_001~002, SYS_001~002, VAL_001
[필수-4] 웹 접근성 개선 (사용성)
- --text-muted: #64748b(3.1:1) → #94a3b8(4.7:1) 색상 대비 개선
- :focus-visible 규칙 추가 (키보드 포커스 표시)
- 마우스 클릭 시 포커스 링 숨김 (UX 개선)
[필수-5] 성능 시험 실시
- 20명 동시 접속: avg 527ms, P95 864ms (GS기준 3초 통과)
- certification/05_시험성적서/성능_시험_결과.md 작성
[필수-6] 백업/복구 API (신뢰성 > 복구성)
- POST /api/admin/backup: DB+.env+업로드 ZIP 백업
- GET /api/admin/backups: 백업 목록
- GET /api/admin/backups/{file}/download: 백업 다운로드
- POST /api/admin/restore/{file}: 백업 복원
[필수-7] About/버전 화면 (유지보수성)
- GET /api/admin/about: 제품명/버전/빌드일/오픈소스목록
- GET /api/admin/health: DB+Ollama+디스크+라이선스 종합 상태
예상 GS 1등급 점수: 93점 / 100점
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
348 lines
13 KiB
Python
348 lines
13 KiB
Python
from contextlib import asynccontextmanager
|
||
|
||
from fastapi import FastAPI
|
||
from fastapi.responses import FileResponse
|
||
from fastapi.staticfiles import StaticFiles
|
||
|
||
from database import init_db
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
|
||
from routers import (
|
||
approvals, assign, audit, auth, cmdb, dashboard, kb, nlcmd, rating, tasks, work,
|
||
institutions, shell_scripts, timetable, attachments, notifications,
|
||
messenger, ssh, projects, vibe,
|
||
ssl_manager, pm, incidents, oncall, batch,
|
||
si_projects, si_wbs, si_requirements, si_issues,
|
||
si_risks, si_milestones, si_change_requests, si_tests,
|
||
agents,
|
||
analytics,
|
||
ws as ws_router,
|
||
timeline,
|
||
code_review,
|
||
anomaly,
|
||
chatbot,
|
||
kb_agent,
|
||
orchestrator,
|
||
predictive,
|
||
change,
|
||
problem,
|
||
capacity,
|
||
catalog,
|
||
ldap,
|
||
pam,
|
||
vuln_scan,
|
||
report,
|
||
metrics,
|
||
finops,
|
||
tenant_mgmt,
|
||
gateway,
|
||
license as license_router,
|
||
learning,
|
||
push as push_router,
|
||
scouter as scouter_router,
|
||
deliverables,
|
||
si_report,
|
||
compliance,
|
||
jmeter,
|
||
public_checklist,
|
||
customer_portal,
|
||
onboarding,
|
||
groupware,
|
||
siem,
|
||
topology,
|
||
portfolio,
|
||
infra_ext,
|
||
admin as admin_router,
|
||
)
|
||
|
||
|
||
@asynccontextmanager
|
||
async def lifespan(app: FastAPI):
|
||
# 디렉토리 생성
|
||
from pathlib import Path
|
||
(Path(__file__).parent / "uploads" / "sr_files").mkdir(parents=True, exist_ok=True)
|
||
(Path(__file__).parent / "uploads" / "workspaces").mkdir(parents=True, exist_ok=True)
|
||
|
||
await init_db()
|
||
from database import SessionLocal
|
||
from core.seed import seed_all
|
||
async with SessionLocal() as db:
|
||
await seed_all(db)
|
||
|
||
# 라이선스 상태 확인 (시작 시) — TRIAL 키는 GUARDIA_LICENSE_KEY 없이도 동작
|
||
from routers.license import get_license_status
|
||
async with SessionLocal() as db:
|
||
lic_status = await get_license_status(db)
|
||
if lic_status.get("valid"):
|
||
edition = lic_status["edition"]
|
||
days = lic_status["days_remaining"]
|
||
cust = lic_status["customer"]
|
||
if lic_status.get("is_trial"):
|
||
print(f"[LICENSE] TRIAL 체험판 활성 (D-{days}) - {cust}")
|
||
else:
|
||
print(f"[LICENSE] {edition} 라이선스 활성 ({days}일 남음) - {cust}")
|
||
if lic_status.get("expiry_warning"):
|
||
print(f"[LICENSE] 만료 {days}일 남음 - 갱신을 준비하세요.")
|
||
elif lic_status.get("expired"):
|
||
print("[LICENSE] 라이선스가 만료되었습니다. 갱신이 필요합니다.")
|
||
else:
|
||
print("[LICENSE] 라이선스 미등록 - /license 에서 무료 체험을 시작하거나 키를 등록하세요.")
|
||
|
||
# A-1: WebSocket ↔ SSE 통합 패치
|
||
from routers.ws import _integrate_with_sse_bus
|
||
_integrate_with_sse_bus()
|
||
|
||
# 백그라운드 스케줄러 시작
|
||
from core.scheduler import start_scheduler, init_batch_jobs_from_db
|
||
start_scheduler()
|
||
await init_batch_jobs_from_db() # DB에서 활성 배치 잡 자동 등록
|
||
|
||
yield
|
||
|
||
# 스케줄러 종료
|
||
from core.scheduler import stop_scheduler
|
||
stop_scheduler()
|
||
|
||
# F-2: Redis 연결 종료
|
||
try:
|
||
from core.cache import close_redis
|
||
await close_redis()
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
app = FastAPI(title="GUARDiA ITSM", version="2.0.0", lifespan=lifespan)
|
||
|
||
|
||
@app.middleware("http")
|
||
async def add_copyright_header(request, call_next):
|
||
response = await call_next(request)
|
||
response.headers["X-Powered-By"] = "GUARDiA ITSM 2.0"
|
||
response.headers["X-Copyright"] = "Copyright 2026 GUARDiA All Rights Reserved"
|
||
return response
|
||
|
||
# ── F-2: Redis 캐시 종료 훅 ──────────────────────────────────────────────────
|
||
# (lifespan의 yield 이후에 실행 — close_redis는 shutdown시 호출)
|
||
|
||
# ── F-3: Rate Limiting 미들웨어 등록 ─────────────────────────────────────────
|
||
from core.ratelimit import setup_rate_limiting
|
||
setup_rate_limiting(app)
|
||
|
||
app.add_middleware(
|
||
CORSMiddleware,
|
||
allow_origins=["http://localhost:8001", "http://127.0.0.1:8001"],
|
||
allow_methods=["*"],
|
||
allow_headers=["*"],
|
||
)
|
||
|
||
app.include_router(auth.router)
|
||
app.include_router(dashboard.router)
|
||
app.include_router(assign.router)
|
||
app.include_router(kb.router)
|
||
app.include_router(nlcmd.router)
|
||
app.include_router(tasks.router)
|
||
app.include_router(approvals.router)
|
||
app.include_router(cmdb.router)
|
||
app.include_router(audit.router)
|
||
app.include_router(work.router)
|
||
app.include_router(rating.router)
|
||
app.include_router(institutions.router)
|
||
app.include_router(shell_scripts.router)
|
||
app.include_router(timetable.router)
|
||
app.include_router(attachments.router)
|
||
app.include_router(notifications.router)
|
||
app.include_router(messenger.router)
|
||
app.include_router(ssh.router)
|
||
app.include_router(projects.router)
|
||
app.include_router(vibe.router)
|
||
app.include_router(ssl_manager.router)
|
||
app.include_router(pm.router)
|
||
app.include_router(incidents.router)
|
||
app.include_router(oncall.router)
|
||
app.include_router(batch.router)
|
||
|
||
# ── SI 프로젝트 관리 (분석→설계→구현→인도) ─────────────────────────────────
|
||
app.include_router(si_projects.router)
|
||
app.include_router(si_wbs.router)
|
||
app.include_router(si_requirements.router)
|
||
app.include_router(si_issues.router)
|
||
app.include_router(si_risks.router)
|
||
app.include_router(si_milestones.router)
|
||
app.include_router(si_change_requests.router)
|
||
app.include_router(si_tests.router)
|
||
|
||
# ── AI 에이전트 (Paperclip × GUARDiA, Ollama 로컬 LLM) ──────────────────────
|
||
app.include_router(agents.router)
|
||
|
||
# ── Analytics (E-2 배포 성공률 트렌드, E-3 엔지니어 워크로드) ─────────────────
|
||
app.include_router(analytics.router)
|
||
|
||
# ── A-1: WebSocket 실시간 대시보드 ───────────────────────────────────────────
|
||
app.include_router(ws_router.router)
|
||
|
||
# ── A-4: 운영 이벤트 타임라인 ────────────────────────────────────────────────
|
||
app.include_router(timeline.router)
|
||
|
||
# ── B-3: 코드 리뷰 에이전트 ──────────────────────────────────────────────────
|
||
app.include_router(code_review.router)
|
||
|
||
# ── B-1: AI 이상 탐지 ────────────────────────────────────────────────────────
|
||
app.include_router(anomaly.router)
|
||
|
||
# ── B-2: 자연어 SR 접수 챗봇 ─────────────────────────────────────────────────
|
||
app.include_router(chatbot.router)
|
||
|
||
# ── B-4: KB 자동 업데이트 에이전트 ───────────────────────────────────────────
|
||
app.include_router(kb_agent.router)
|
||
|
||
# ── B-5: 멀티 에이전트 협업 오케스트레이션 ────────────────────────────────────
|
||
app.include_router(orchestrator.router)
|
||
|
||
# ── B-6: 예측 유지보수 ────────────────────────────────────────────────────────
|
||
app.include_router(predictive.router)
|
||
|
||
# ── C-2: 변경 관리 CAB ───────────────────────────────────────────────────────
|
||
app.include_router(change.router)
|
||
|
||
# ── C-3: Problem Management ─────────────────────────────────────────────────
|
||
app.include_router(problem.router)
|
||
|
||
# ── C-4: 용량 관리 대시보드 ──────────────────────────────────────────────────
|
||
app.include_router(capacity.router)
|
||
|
||
# ── C-5: 서비스 카탈로그 ──────────────────────────────────────────────────────
|
||
app.include_router(catalog.router)
|
||
|
||
# ── D-1: LDAP/AD 연동 ────────────────────────────────────────────────────────
|
||
app.include_router(ldap.router)
|
||
|
||
# ── D-3: 특권 접근 관리 (PAM) ─────────────────────────────────────────────────
|
||
app.include_router(pam.router)
|
||
|
||
# ── D-4: 보안 취약점 자동 스캔 ───────────────────────────────────────────────
|
||
app.include_router(vuln_scan.router)
|
||
|
||
# ── E-1: 월별 리포트 자동 생성 ───────────────────────────────────────────────
|
||
app.include_router(report.router)
|
||
|
||
# ── E-4: Grafana 연동 (Prometheus 메트릭) ─────────────────────────────────
|
||
app.include_router(metrics.router)
|
||
|
||
# ── E-5: FinOps 비용 분석 ────────────────────────────────────────────────
|
||
app.include_router(finops.router)
|
||
|
||
# ── F-1: 멀티테넌트 데이터 격리 ──────────────────────────────────────────
|
||
from middleware.tenant import TenantMiddleware
|
||
app.add_middleware(TenantMiddleware)
|
||
app.include_router(tenant_mgmt.router)
|
||
|
||
# ── F-5: OpenAPI 외부 연동 게이트웨이 ────────────────────────────────────
|
||
app.include_router(gateway.router)
|
||
|
||
# ── Self-Improving Learning Loop ──────────────────────────────────────────
|
||
app.include_router(learning.router)
|
||
|
||
# ── 라이선스 관리 ──────────────────────────────────────────────────────────
|
||
app.include_router(license_router.router)
|
||
|
||
# ── G-10: PWA Push 알림 ──────────────────────────────────────────────────
|
||
app.include_router(push_router.router)
|
||
|
||
# Scouter APM
|
||
app.include_router(scouter_router.router)
|
||
|
||
# PMS — 산출물 + 보고서
|
||
app.include_router(deliverables.router)
|
||
app.include_router(si_report.router)
|
||
|
||
# 준수성 점검 (시큐어코딩/웹접근성/개인정보보호)
|
||
app.include_router(compliance.router)
|
||
|
||
# 성능 테스트 (JMeter JTL 분석 + 내장 부하 테스트)
|
||
app.include_router(jmeter.router)
|
||
|
||
# 공공기관 필수 기능 체크리스트
|
||
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) # 네트워크 토폴로지 시각화
|
||
app.include_router(portfolio.router) # 포트폴리오 + 리소스 관리
|
||
app.include_router(infra_ext.router) # Zero Trust + K8s + ERP
|
||
app.include_router(admin_router.router) # GS인증: About + 백업/복구 + 에러코드
|
||
|
||
|
||
@app.get("/topology")
|
||
async def topology_page():
|
||
return FileResponse("static/index.html")
|
||
|
||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||
|
||
|
||
@app.get("/")
|
||
async def index():
|
||
return FileResponse("static/index.html")
|
||
|
||
|
||
@app.get("/customer")
|
||
async def customer():
|
||
return FileResponse("static/customer.html")
|
||
|
||
|
||
@app.get("/login")
|
||
async def login_page():
|
||
return FileResponse("static/login.html")
|
||
|
||
|
||
@app.get("/change-password")
|
||
async def change_password_page():
|
||
return FileResponse("static/change-password.html")
|
||
|
||
|
||
@app.get("/agents")
|
||
async def agents_page():
|
||
return FileResponse("static/agents.html")
|
||
|
||
|
||
@app.get("/incidents")
|
||
async def incidents_page():
|
||
return FileResponse("static/incidents.html")
|
||
|
||
|
||
@app.get("/ssl")
|
||
async def ssl_page():
|
||
return FileResponse("static/ssl.html")
|
||
|
||
|
||
@app.get("/pm")
|
||
async def pm_page():
|
||
return FileResponse("static/pm.html")
|
||
|
||
|
||
@app.get("/oncall")
|
||
async def oncall_page():
|
||
return FileResponse("static/oncall.html")
|
||
|
||
|
||
@app.get("/batch")
|
||
async def batch_page():
|
||
return FileResponse("static/batch.html")
|
||
|
||
|
||
@app.get("/vibe")
|
||
async def vibe_page():
|
||
return FileResponse("static/vibe.html")
|
||
|
||
|
||
@app.get("/si")
|
||
async def si_page():
|
||
return FileResponse("static/si.html")
|
||
|
||
|
||
@app.get("/license")
|
||
async def license_page():
|
||
return FileResponse("static/license.html")
|