From 938b25f2866d778fee5de403aa463995354a25c4 Mon Sep 17 00:00:00 2001 From: DESKTOP-TKLFCPRython Date: Fri, 29 May 2026 18:18:52 +0900 Subject: [PATCH] =?UTF-8?q?feat(itsm):=20G-1~G-12=20=ED=99=95=EC=9E=A5=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20+=20=ED=95=98=EB=84=A4=EC=8A=A4/=EB=B4=87/?= =?UTF-8?q?=EC=84=A4=EC=B9=98=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit G-1: 메신저 Webhook Relay + _send_to_room 실제 httpx 호출 구현 G-2: POST /api/tasks/bulk SR 대량작업 엔드포인트 (최대 100건) G-3: 라이선스 만료 알림 스케줄러 (매일 09:00 KST) G-4: 체험판 upgrade_banner 필드 + license.py 배너 로직 G-5: core/auto_rca.py + incidents/problem auto-rca 엔드포인트 G-6: core/deploy_impact.py + vibe impact-analysis 엔드포인트 G-7: core/ticket_classifier.py + SR 생성 시 AI 분류 + ai-suggestion API G-8: VulnPatchRecord 모델 + vuln_scan 패치추적 4개 엔드포인트 G-9: core/jira_sync.py + gateway Jira/Confluence 연동 엔드포인트 G-10: core/push_notify.py + routers/push.py + PushSubscription 모델 G-11: approvals 다중승인 (위임/서명/기한초과/마감연장) G-12: alembic.ini + migrations/ + cicd/migrate_to_postgres.sh 하네스: guardia-orchestrator 확장기능 Phase 반영 봇명령어: /sr /status /license /bulk 슬래시 명령어 추가 설치스크립트: setup/ (Ubuntu, CentOS, RHEL, Windows) --test 옵션 포함 Co-Authored-By: Claude Sonnet 4.6 --- 01_분석설계서.md | 362 ++++++ 02_단위통합테스트계획서.md | 593 +++++++++ 03_개발자지침서.md | 951 ++++++++++++++ 04_운영자지침서.md | 935 ++++++++++++++ 05_설치가이드_리눅스.md | 837 ++++++++++++ 06_설치가이드_윈도우서버.md | 873 +++++++++++++ 07_SI프로젝트관리_분석설계서.md | 432 +++++++ 08_AI에이전트_Paperclip_설계서.md | 669 ++++++++++ 09_확장개발_Priority1_UI구현.md | 206 +++ 10_확장개발_Priority2_코어모듈.md | 159 +++ 11_확장개발_Priority3_AI에이전트.md | 197 +++ 12_확장개발_Priority4_인프라.md | 175 +++ 13_확장개발_Priority5_외부연동.md | 243 ++++ 14_라이선스_키_발급_가이드.md | 647 ++++++++++ GUARDiA_Paperclip_AI에이전트_구현보고서.pptx | Bin 0 -> 460175 bytes gen_ppt.js | 1209 ++++++++++++++++++ 16 files changed, 8488 insertions(+) create mode 100644 01_분석설계서.md create mode 100644 02_단위통합테스트계획서.md create mode 100644 03_개발자지침서.md create mode 100644 04_운영자지침서.md create mode 100644 05_설치가이드_리눅스.md create mode 100644 06_설치가이드_윈도우서버.md create mode 100644 07_SI프로젝트관리_분석설계서.md create mode 100644 08_AI에이전트_Paperclip_설계서.md create mode 100644 09_확장개발_Priority1_UI구현.md create mode 100644 10_확장개발_Priority2_코어모듈.md create mode 100644 11_확장개발_Priority3_AI에이전트.md create mode 100644 12_확장개발_Priority4_인프라.md create mode 100644 13_확장개발_Priority5_외부연동.md create mode 100644 14_라이선스_키_발급_가이드.md create mode 100644 GUARDiA_Paperclip_AI에이전트_구현보고서.pptx create mode 100644 gen_ppt.js diff --git a/01_분석설계서.md b/01_분석설계서.md new file mode 100644 index 0000000..53cff42 --- /dev/null +++ b/01_분석설계서.md @@ -0,0 +1,362 @@ +# GUARDiA ITSM + Messenger — 분석 및 설계서 + +> **문서번호**: GUARDIA-DS-001 +> **버전**: 1.0 +> **작성일**: 2026-05-25 +> **작성자**: GUARDiA 개발팀 +> **보안등급**: 내부용 (대외비) + +--- + +## 1. 문서 목적 + +본 문서는 GUARDiA ITSM(IT Service Management) 플랫폼과 GUARDiA Messenger 시스템의 요구사항 분석, 시스템 설계, 데이터 설계, API 설계를 기술한다. 본 문서는 개발자, 운영자, 품질관리자가 시스템을 이해하고 유지보수하는 데 활용한다. + +--- + +## 2. 시스템 개요 + +### 2-1. GUARDiA ITSM + +**GUARDiA ITSM**은 공공기관 IT 운영을 위한 온프레미스 서비스관리 플랫폼이다. + +| 항목 | 내용 | +|------|------| +| 목적 | 공공기관 IT 인프라 SR(Service Request) 접수·처리·이력 관리 | +| 운영 방식 | 온프레미스 (인터넷 완전 차단 환경 대응) | +| 주요 사용자 | 운영 엔지니어, PM, 기관 담당자(고객) | +| 기술 스택 | FastAPI, SQLAlchemy (Async), SQLite, Python 3.11+ | +| 인증 방식 | JWT (RS256), 역할 기반 접근 제어 (RBAC) | + +### 2-2. GUARDiA Messenger + +**GUARDiA Messenger**는 ITSM과 연동되는 사내 메신저로, SR 알림, 봇 명령, 엔지니어 채팅을 지원한다. + +| 항목 | 내용 | +|------|------| +| 목적 | 운영팀 실시간 소통 및 ITSM 이벤트 알림 | +| 연동 방식 | GUARDiA ITSM Webhook API | +| 주요 기능 | 채팅, 봇 명령, SR 알림, 공지 | + +--- + +## 3. 요구사항 분석 + +### 3-1. 기능적 요구사항 (Functional Requirements) + +| ID | 분류 | 요구사항 | 우선순위 | +|----|------|----------|---------| +| FR-001 | SR 관리 | SR 접수·파싱·배정·승인·처리·완료 워크플로우 | 필수 | +| FR-002 | SR 관리 | SR 상태 실시간 SSE 이벤트 브로드캐스트 | 필수 | +| FR-003 | 인증 | JWT 기반 역할별 로그인 (ADMIN/PM/ENGINEER/CUSTOMER) | 필수 | +| FR-004 | CMDB | 기관·서버 자산 등록 및 조회 | 필수 | +| FR-005 | SSH | 서버 원격 명령 실행 (asyncssh) | 필수 | +| FR-006 | SSL | SSL 인증서 만료 모니터링 및 갱신 이력 관리 | 필수 | +| FR-007 | PM | 정기 PM 체크리스트 템플릿 및 반복 스케줄 관리 | 필수 | +| FR-008 | 장애 | P1~P4 장애 등록·상태 관리·RCA 기록 | 필수 | +| FR-009 | 당직 | 온콜/당직 일정 관리 | 권장 | +| FR-010 | 배치 | 배치 작업 등록·실행·이력 관리 | 권장 | +| FR-011 | CI/CD | Jenkins 파이프라인 연동 (빌드·배포·콜백) | 선택 | +| FR-012 | 알림 | 이메일(SMTP) + 메신저 Webhook 알림 | 필수 | +| FR-013 | 보고서 | 작업이력·PM결과 Excel 다운로드 | 권장 | +| FR-014 | 감사 | 해시 체인 기반 감사 로그 | 필수 | +| FR-015 | KB | 장애 처리 지식 베이스 문서 관리 | 권장 | +| FR-016 | 상용화 | 라이선스 키 기반 에디션 제어 (COMMUNITY / STANDARD / ENTERPRISE) | 필수 | +| FR-017 | 상용화 | 에디션별 기관·사용자·서버 수량 및 기능(LDAP/PAM/AI/CICD 등) 접근 제한 | 필수 | + +### 3-2. 비기능적 요구사항 (Non-Functional Requirements) + +| ID | 분류 | 요구사항 | +|----|------|---------| +| NFR-001 | 보안 | AES-256-GCM 서버 자격증명 암호화 | +| NFR-002 | 보안 | API 응답에 IP·SSH계정·비밀번호 절대 미포함 | +| NFR-003 | 보안 | root SSH 직접 접속 차단 | +| NFR-004 | 보안 | 명령어 안전성 검증 (위험 패턴 차단) | +| NFR-005 | 가용성 | 99% 이상 가용성 목표 | +| NFR-006 | 성능 | API 응답 시간 500ms 이하 (DB 조회 기준) | +| NFR-007 | 호환성 | RHEL 8/9, CentOS 7/8, Ubuntu 20.04/22.04 | +| NFR-008 | 호환성 | Windows Server 2019/2022 | +| NFR-009 | 격리 | 외부 인터넷 완전 차단 환경 동작 가능 | +| NFR-010 | 감사 | 모든 SR 처리 이력 블록체인형 해시 체인 보존 | + +--- + +## 4. 시스템 아키텍처 + +### 4-1. 전체 구성도 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 클라이언트 레이어 │ +│ ┌──────────────┐ ┌──────────────┐ ┌────────────────────────┐ │ +│ │ 웹 브라우저 │ │ 모바일 앱 │ │ GUARDiA Messenger │ │ +│ └──────┬───────┘ └──────┬───────┘ └──────────┬─────────────┘ │ +└─────────┼─────────────────┼─────────────────────┼───────────────┘ + │ HTTPS │ HTTPS │ WebSocket/HTTP +┌─────────▼─────────────────▼─────────────────────▼───────────────┐ +│ API 게이트웨이 레이어 │ +│ Nginx (Reverse Proxy + TLS Termination) │ +└─────────────────────────────┬───────────────────────────────────┘ + │ HTTP (내부) +┌─────────────────────────────▼───────────────────────────────────┐ +│ 애플리케이션 레이어 │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ GUARDiA ITSM (FastAPI) │ │ +│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────────┐ │ │ +│ │ │ SR 관리 │ │ SSH 실행 │ │ SSL 감시 │ │ PM 관리 │ │ │ +│ │ │ tasks │ │ ssh_exec │ │ssl_mgr │ │ pm │ │ │ +│ │ └──────────┘ └──────────┘ └──────────┘ └───────────┘ │ │ +│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────────┐ │ │ +│ │ │ 장애관리 │ │ 배치작업 │ │ CI/CD │ │ Scheduler │ │ │ +│ │ │incidents │ │ batch │ │ cicd │ │(APSchedul)│ │ │ +│ │ └──────────┘ └──────────┘ └──────────┘ └───────────┘ │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ GUARDiA Messenger (FastAPI) │ │ +│ └──────────────────────────────────────────────────────────┘ │ +└──────────────────────────────┬──────────────────────────────────┘ + │ SQLAlchemy (Async) +┌──────────────────────────────▼──────────────────────────────────┐ +│ 데이터 레이어 │ +│ SQLite (개발/소규모) → PostgreSQL (운영 권장) │ +└─────────────────────────────────────────────────────────────────┘ + │ asyncssh +┌──────────────────────────────▼──────────────────────────────────┐ +│ 대상 서버 레이어 │ +│ WEB (Nginx/Apache) WAS (Tomcat/JBoss) DB (PostgreSQL/Oracle) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 4-2. GUARDiA ITSM 컴포넌트 설명 + +| 컴포넌트 | 파일 | 역할 | +|---------|------|------| +| 진입점 | `main.py` | FastAPI 앱, 라우터 등록, lifespan | +| ORM/스키마 | `models.py` | SQLAlchemy 모델 + Pydantic 스키마 | +| DB 세션 | `database.py` | AsyncSession 팩토리 | +| 인증 | `core/auth.py` | JWT 발급/검증 | +| SSH 실행 | `core/ssh_exec.py` | asyncssh 기반 원격 실행 | +| 알림 | `core/notify.py` | SMTP + Messenger Webhook | +| 스케줄러 | `core/scheduler.py` | SSL/PM/계약 만료 자동 감시 | +| Jenkins 클라이언트 | `core/cicd.py` | Jenkins REST API | +| 시드 데이터 | `core/seed.py` | 초기 데이터 자동 생성 | +| 라이선스 엔진 | `core/license.py` | AES-256-GCM 라이선스 생성·검증·캐싱 | +| 라이선스 가드 | `middleware/license_guard.py` | 에디션별 기관·서버·기능 접근 제한 Dependency | + +### 4-3. GUARDiA Messenger 컴포넌트 + +``` +C:\GUARDiA\messenger\ +├── core/ 실시간 채팅 엔진 +├── models/ 메시지·사용자·채널 ORM +└── main.py WebSocket + REST API +``` + +--- + +## 5. 데이터 설계 + +### 5-1. 전체 테이블 목록 (27개) + +| 번호 | 테이블명 | 설명 | 관련 기능 | +|-----|---------|------|---------| +| 1 | `tb_user` | 시스템 사용자 | 인증 | +| 2 | `tb_inst_meta` | 기관 메타정보 | CMDB | +| 3 | `tb_inst_contact` | 기관 담당자 연락처 | CMDB | +| 4 | `tb_server_info` | 서버 자산 (암호화) | CMDB/SSH | +| 5 | `tb_sr_request` | SR 마스터 | SR관리 | +| 6 | `tb_sr_approval` | SR 승인 이력 | 승인 | +| 7 | `tb_work_log` | 작업 실행 로그 | 작업 | +| 8 | `tb_audit_log` | 감사 로그 (해시체인) | 감사 | +| 9 | `tb_rating` | 만족도 평가 | 고객 | +| 10 | `tb_shell_script` | SM 스크립트 메타 | 자동화 | +| 11 | `tb_work_timetable` | 작업 일정표 | 타임테이블 | +| 12 | `tb_sr_attachment` | SR 첨부파일 | 첨부 | +| 13 | `tb_notification_log` | 알림 이력 | 알림 | +| 14 | `tb_kb_document` | 지식 베이스 | KB | +| 15 | `tb_engineer_profile` | 엔지니어 스킬 | 자동배정 | +| 16 | `tb_project` | CI/CD 프로젝트 | 배포 | +| 17 | `tb_vibe_session` | 바이브 코딩 세션 | CI/CD | +| 18 | `tb_ssl_history` | SSL 갱신 이력 | SSL | +| 19 | `tb_pm_template` | PM 체크리스트 템플릿 | PM | +| 20 | `tb_pm_schedule` | PM 반복 스케줄 | PM | +| 21 | `tb_pm_result` | PM 점검 결과 | PM | +| 22 | `tb_incident` | 장애 마스터 | 장애관리 | +| 23 | `tb_incident_sr` | 장애↔SR 연결 | 장애관리 | +| 24 | `tb_oncall_schedule` | 온콜/당직 일정 | 당직 | +| 25 | `tb_batch_job` | 배치 작업 | 배치 | +| 26 | `tb_batch_run` | 배치 실행 이력 | 배치 | +| 27 | `tb_license` | 라이선스 등록 이력 | 상용화 라이선스 관리 | + +### 5-2. 핵심 테이블 ERD (텍스트 형식) + +``` +tb_inst_meta (1) ──< (N) tb_server_info + │ +tb_inst_meta (1) ──< (N) tb_sr_request ──< (N) tb_sr_approval + ──< (N) tb_work_log + ──< (N) tb_audit_log + ──< (N) tb_sr_attachment + ──< (N) tb_ops_task + +tb_sr_request (N) >──< (N) tb_incident (via tb_incident_sr) + +tb_server_info (1) ──< (N) tb_ssl_history +tb_server_info (1) ──< (N) tb_batch_job + +tb_pm_template (1) ──< (N) tb_pm_result +tb_work_timetable (1) ──< (N) tb_pm_result +``` + +### 5-3. SR 상태 머신 + +``` +RECEIVED ──→ PARSED ──→ PENDING_APPROVAL ──→ APPROVED ──→ IN_PROGRESS + └──→ REJECTED + ↓ +IN_PROGRESS ──→ PENDING_PM_VALIDATION ──→ COMPLETED + └──→ FAILED_ROLLBACK +``` + +--- + +## 6. API 설계 + +### 6-1. 인증 방식 + +``` +POST /api/auth/login +요청: { "username": "admin", "password": "****" } +응답: { "access_token": "eyJ...", "token_type": "bearer" } + +이후 모든 API 요청 헤더: +Authorization: Bearer eyJ... +``` + +### 6-2. 주요 API 목록 + +| 분류 | 메서드 | 경로 | 역할 | +|------|--------|------|------| +| 인증 | POST | `/api/auth/login` | 로그인 | +| SR | GET/POST | `/api/tasks` | SR 목록/생성 | +| SR | PATCH | `/api/tasks/{id}/status` | 상태 변경 | +| SSL | GET | `/api/ssl/expiring` | 만료 임박 목록 | +| SSL | POST | `/api/ssl/check/{id}` | SSH 점검 | +| SSL | POST | `/api/ssl/renew/{id}` | 갱신 기록 | +| PM | GET/POST | `/api/pm/templates` | 체크리스트 템플릿 | +| PM | POST | `/api/pm/schedules/{id}/trigger` | PM 실행 | +| 장애 | GET/POST | `/api/incidents` | 장애 관리 | +| 당직 | GET | `/api/oncall/today` | 오늘 당직자 | +| 배치 | POST | `/api/batch/jobs/{id}/run` | 배치 실행 | + +### 6-3. 오류 응답 형식 + +```json +{ + "detail": "오류 요약 메시지 (IP, 스택트레이스 미포함)" +} +``` + +--- + +## 7. 보안 설계 + +### 7-1. 자격증명 암호화 + +``` +평문 비밀번호 + │ + ▼ AES-256-GCM (12바이트 nonce + 암호문) + │ + ▼ Base64 인코딩 + │ +tb_server_info.os_pw_enc 컬럼 저장 +``` + +암호화 키: `GUARDIA_ENC_KEY` 환경변수 (32바이트) + +### 7-2. JWT 보안 + +- 알고리즘: HS256 +- 만료: 8시간 (기본값, 환경변수로 조정 가능) +- 저장: 클라이언트 메모리 (localStorage 사용 금지 권고) + +### 7-3. SSH 보안 + +- root 계정 직접 접속 금지 (`ssh_user ≠ root` 검증) +- known_hosts 검증 (운영 환경 필수) +- 위험 명령어 패턴 차단: + - `rm -rf /`, `mkfs`, `dd if=`, `shutdown`, `reboot` 등 + +### 7-4. API 응답 보안 + +`ServerOut` 스키마에서 민감정보 제외: +- `ip_addr` — 미포함 +- `ssh_user` — 미포함 +- `os_pw_enc` — 미포함 + +### 7-5. 감사 로그 무결성 + +각 감사 로그 항목은 이전 항목의 해시값을 포함하는 체인 구조: +``` +log_hash = SHA256(prev_hash + actor + action + detail + timestamp) +``` + +--- + +## 8. 배포 구성 + +### 8-1. 운영 환경 권장 구성 + +``` +인터넷 + │ (차단) + ▼ +[Nginx] :443 ──TLS──→ [GUARDiA ITSM] :8000 + ──TLS──→ [GUARDiA Messenger] :8001 + +[GUARDiA ITSM] ──asyncssh──→ [관리 대상 서버들] +[GUARDiA ITSM] ──SMTP──→ [내부 메일 서버] +``` + +### 8-2. 최소 서버 사양 + +| 항목 | 최소 | 권장 | +|------|------|------| +| CPU | 2 코어 | 4 코어 | +| 메모리 | 4 GB | 8 GB | +| 디스크 | 50 GB | 100 GB | +| OS | RHEL 8 이상 / Ubuntu 20.04 이상 | RHEL 9 / Ubuntu 22.04 | +| Python | 3.10 이상 | 3.11 | + +--- + +## 9. GUARDiA Messenger 설계 + +### 9-1. 주요 기능 + +| 기능 | 설명 | +|------|------| +| 채팅 | 1:1 및 그룹 채팅 | +| 봇 명령 | `!vibe`, `!build`, `!deploy`, `!sr`, `!health` 등 | +| SR 알림 | ITSM SR 상태 변경 시 자동 알림 | +| 파일 전송 | 이미지·문서 공유 | + +### 9-2. 봇 명령어 목록 + +| 명령어 | 설명 | 예시 | +|--------|------|------| +| `!sr <내용>` | SR 빠른 접수 | `!sr WAS 응답 없음` | +| `!status ` | SR 상태 조회 | `!status SR-20260525-AA1234` | +| `!health <서버명>` | 서버 헬스체크 | `!health MOF-WAS-01` | +| `!oncall` | 오늘 당직자 조회 | `!oncall` | +| `!pm <스케줄명>` | PM 즉시 실행 | `!pm 기재부 월간점검` | +| `!ssl <도메인>` | SSL 만료 확인 | `!ssl csv.culture.go.kr` | + +--- + +## 10. 변경 이력 + +| 버전 | 날짜 | 내용 | 작성자 | +|------|------|------|--------| +| 1.0 | 2026-05-25 | 최초 작성 | 개발팀 | diff --git a/02_단위통합테스트계획서.md b/02_단위통합테스트계획서.md new file mode 100644 index 0000000..a7f6be6 --- /dev/null +++ b/02_단위통합테스트계획서.md @@ -0,0 +1,593 @@ +# GUARDiA ITSM + Messenger — 단위·통합 테스트 계획서 + +**문서 버전**: 1.0 +**작성일**: 2026-05-25 +**대상 시스템**: GUARDiA ITSM v1.0, GUARDiA Messenger v1.0 + +--- + +## 목차 + +1. [개요](#1-개요) +2. [테스트 범위 및 전략](#2-테스트-범위-및-전략) +3. [테스트 환경](#3-테스트-환경) +4. [단위 테스트 계획](#4-단위-테스트-계획) +5. [통합 테스트 계획](#5-통합-테스트-계획) +6. [비기능 테스트 계획](#6-비기능-테스트-계획) +7. [테스트 케이스 목록](#7-테스트-케이스-목록) +8. [결함 관리](#8-결함-관리) +9. [완료 기준](#9-완료-기준) + +--- + +## 1. 개요 + +### 1.1 목적 + +본 문서는 GUARDiA ITSM 및 GUARDiA Messenger 시스템의 품질 보증을 위한 단위 테스트 및 통합 테스트 계획을 정의한다. 개별 컴포넌트의 정확성 검증(단위)과 컴포넌트 간 상호작용 검증(통합)을 통해 출시 전 결함을 조기에 발견하고 수정한다. + +### 1.2 테스트 대상 + +| 구분 | 대상 모듈 | +|------|-----------| +| ITSM 코어 | auth, dashboard, tasks, approvals, audit, cmdb | +| ITSM 운영 | ssl_manager, pm, incidents, oncall, batch | +| ITSM 지원 | kb, shell_scripts, ssh, attachments, notifications | +| ITSM 고객 | rating, institutions, timetable, work | +| Messenger | bot dispatcher, GUARDiA_ITSM bot, command handlers | +| 공통 | scheduler, seed, database, JWT auth | + +### 1.3 테스트 유형 정의 + +- **단위 테스트(Unit Test)**: 함수/클래스 단위의 독립 검증. 외부 의존성은 mock 처리. +- **통합 테스트(Integration Test)**: DB·Redis·SSH·Messenger 등 실제 의존성과의 연동 검증. +- **E2E 시나리오 테스트**: 사용자 시나리오 전 구간 검증 (API 호출 → DB 저장 → 알림 발송). + +--- + +## 2. 테스트 범위 및 전략 + +### 2.1 단위 테스트 범위 + +``` +범위 내 (In Scope) +├── 모든 라우터 엔드포인트 (130개 API) +├── core/scheduler.py — 스케줄 함수 +├── core/seed.py — 시드 데이터 생성 +├── utils/crypto.py — AES-256-GCM 암/복호화 +├── utils/ssh_runner.py — SSH 명령 검증 +└── schemas/*.py — Pydantic 직렬화/역직렬화 + +범위 외 (Out of Scope) +├── 3rd party 라이브러리 내부 (FastAPI, SQLAlchemy) +└── OS 수준 시스템 콜 +``` + +### 2.2 통합 테스트 범위 + +``` +범위 내 +├── ITSM API ↔ SQLite DB 연동 +├── ITSM API ↔ GUARDiA Messenger 알림 +├── SSH 실행 → 원격 서버 응답 처리 +├── SSL 점검 스크립트 → JSON 파싱 +├── 스케줄러 → DB 변경 → 알림 체인 +└── PM 스케줄 → WorkTimetable 자동 생성 + +범위 외 +├── 외부 LLM API (완전 금지) +├── 실제 고객 운영 서버 대상 SSH +└── 이메일/SMS 외부 발송 +``` + +### 2.3 테스트 전략 + +- **TDD 기반**: 핵심 비즈니스 로직은 테스트 먼저 작성 +- **AAA 패턴**: Arrange → Act → Assert +- **픽스처 격리**: 각 테스트는 독립 SQLite in-memory DB 사용 +- **커버리지 목표**: 단위 80% 이상, 통합 70% 이상 + +--- + +## 3. 테스트 환경 + +### 3.1 단위 테스트 환경 + +``` +OS: 동일 (Linux/Windows 무관) +Python: 3.11+ +프레임워크: pytest 8.x + pytest-asyncio +Mock: unittest.mock, respx (httpx mock) +DB: SQLite in-memory (aiosqlite) +의존성: requirements-dev.txt 참조 +``` + +**requirements-dev.txt**: +``` +pytest>=8.0 +pytest-asyncio>=0.23 +pytest-cov>=5.0 +httpx>=0.27 # TestClient +respx>=0.21 # httpx mock +faker>=25.0 # 테스트 데이터 +freezegun>=1.4 # 시간 고정 +``` + +### 3.2 통합 테스트 환경 + +``` +OS: Ubuntu 22.04 LTS (권장) 또는 Windows Server 2022 +Python: 3.11+ +DB: SQLite 파일 기반 (테스트 전용) +Messenger: 테스트 채널 (별도 구성) +SSH 대상: localhost SSH 서버 (테스트 전용) +``` + +### 3.3 디렉토리 구조 + +``` +C:\GUARDiA\itsm\ +└── tests\ + ├── conftest.py # 공통 픽스처 + ├── unit\ + │ ├── test_auth.py + │ ├── test_tasks.py + │ ├── test_ssl_manager.py + │ ├── test_pm.py + │ ├── test_incidents.py + │ ├── test_oncall.py + │ ├── test_batch.py + │ ├── test_scheduler.py + │ └── test_crypto.py + └── integration\ + ├── test_sr_workflow.py + ├── test_approval_chain.py + ├── test_pm_workflow.py + ├── test_incident_workflow.py + └── test_notification_chain.py +``` + +--- + +## 4. 단위 테스트 계획 + +### 4.1 conftest.py 공통 픽스처 + +```python +# tests/conftest.py +import pytest +import pytest_asyncio +from httpx import AsyncClient, ASGITransport +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession +from sqlalchemy.orm import sessionmaker +from database import Base, get_db +from main import app + +TEST_DB = "sqlite+aiosqlite:///:memory:" + +@pytest_asyncio.fixture +async def db_session(): + engine = create_async_engine(TEST_DB) + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + SessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) + async with SessionLocal() as session: + yield session + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.drop_all) + +@pytest_asyncio.fixture +async def client(db_session): + app.dependency_overrides[get_db] = lambda: db_session + async with AsyncClient( + transport=ASGITransport(app=app), base_url="http://test" + ) as ac: + yield ac + app.dependency_overrides.clear() + +@pytest_asyncio.fixture +async def auth_headers(client): + r = await client.post("/auth/login", json={"username":"admin","password":"admin1234!"}) + token = r.json()["access_token"] + return {"Authorization": f"Bearer {token}"} +``` + +### 4.2 인증 모듈 (auth) 단위 테스트 + +**파일**: `tests/unit/test_auth.py` + +| 테스트 ID | 테스트명 | 설명 | 기대 결과 | +|-----------|----------|------|-----------| +| UT-AUTH-001 | 정상 로그인 | 올바른 계정으로 로그인 | 200 + access_token 반환 | +| UT-AUTH-002 | 잘못된 비밀번호 | 틀린 비밀번호 시도 | 401 Unauthorized | +| UT-AUTH-003 | 존재하지 않는 계정 | 없는 사용자명 | 401 Unauthorized | +| UT-AUTH-004 | 비밀번호 변경 | 현재 PW 확인 후 변경 | 200 + message | +| UT-AUTH-005 | JWT 토큰 만료 | 만료된 토큰으로 API 호출 | 401 Unauthorized | +| UT-AUTH-006 | RBAC — CUSTOMER 권한 제한 | CUSTOMER가 admin API 호출 | 403 Forbidden | +| UT-AUTH-007 | RBAC — ENGINEER 권한 | ENGINEER가 SR 처리 | 200 OK | +| UT-AUTH-008 | 빈 토큰 | Authorization 헤더 없음 | 401 Unauthorized | + +```python +# 예시 테스트 코드 +@pytest.mark.asyncio +async def test_login_success(client, db_session): + # Arrange: seed admin user + from core.seed import seed_all + await seed_all(db_session) + # Act + r = await client.post("/auth/login", json={"username":"admin","password":"admin1234!"}) + # Assert + assert r.status_code == 200 + data = r.json() + assert "access_token" in data + assert data["token_type"] == "bearer" + +@pytest.mark.asyncio +async def test_login_wrong_password(client, db_session): + from core.seed import seed_all + await seed_all(db_session) + r = await client.post("/auth/login", json={"username":"admin","password":"wrong!"}) + assert r.status_code == 401 +``` + +### 4.3 SR 처리 (tasks) 단위 테스트 + +| 테스트 ID | 테스트명 | 기대 결과 | +|-----------|----------|-----------| +| UT-TASK-001 | SR 생성 (INCIDENT 유형) | 201 + sr_id 반환 | +| UT-TASK-002 | SR 생성 (CHANGE 유형) | 201 + sr_id 반환 | +| UT-TASK-003 | SR 목록 페이징 | 200 + items/total 구조 | +| UT-TASK-004 | SR 상태 변경 OPEN→IN_PROGRESS | 200 + 상태 업데이트 | +| UT-TASK-005 | SR 상태 — 잘못된 전환 | 400 Bad Request | +| UT-TASK-006 | SR 첨부파일 업로드 | 201 + file_id | +| UT-TASK-007 | SR 첨부파일 경로 순회 방지 | 400 Bad Request | +| UT-TASK-008 | 고객(CUSTOMER) SR 자기 것만 조회 | 200 + 본인 SR만 | +| UT-TASK-009 | SR 검색 (제목 키워드) | 200 + 필터 결과 | +| UT-TASK-010 | SR 만족도 평가 | 200 + rating 저장 | + +### 4.4 SSL 관리 (ssl_manager) 단위 테스트 + +| 테스트 ID | 테스트명 | 기대 결과 | +|-----------|----------|-----------| +| UT-SSL-001 | SSL 도메인 등록 | 201 + ssl_id | +| UT-SSL-002 | 중복 도메인 등록 | 409 Conflict | +| UT-SSL-003 | SSL 즉시 점검 — 정상 응답 파싱 | 200 + days_left 계산 | +| UT-SSL-004 | SSL 즉시 점검 — SSH 실패 시 | 200 + error_msg 기록 | +| UT-SSL-005 | 만료 임박 알림 등급 (EXPIRED ≤0) | level=EXPIRED | +| UT-SSL-006 | 만료 임박 알림 등급 (URGENT ≤7) | level=URGENT | +| UT-SSL-007 | 만료 임박 알림 등급 (WARN ≤30) | level=WARN | +| UT-SSL-008 | 정상 인증서 (>30일) | level=OK | +| UT-SSL-009 | SSL 수동 갱신 완료 기록 | 200 + renewed_at 업데이트 | +| UT-SSL-010 | SSL 도메인 삭제 | 204 No Content | + +### 4.5 PM 정기점검 (pm) 단위 테스트 + +| 테스트 ID | 테스트명 | 기대 결과 | +|-----------|----------|-----------| +| UT-PM-001 | PM 템플릿 생성 | 201 + template_id | +| UT-PM-002 | PM 템플릿 목록 조회 | 200 + 카테고리별 목록 | +| UT-PM-003 | PM 스케줄 생성 (MONTHLY) | 201 + next_scheduled 계산 | +| UT-PM-004 | PM 스케줄 생성 (WEEKLY) | 201 + 7일 후 next_scheduled | +| UT-PM-005 | PM 스케줄 생성 (QUARTERLY) | 201 + 90일 후 next_scheduled | +| UT-PM-006 | PM 작업 생성 (WorkTimetable 연동) | 201 + timetable_id | +| UT-PM-007 | PM 결과 저장 — PASS | 200 + result=PASS | +| UT-PM-008 | PM 결과 저장 — FAIL (비고 필수) | 400 if note 없음 | +| UT-PM-009 | PM 완료 처리 | 200 + completed_at 기록 | +| UT-PM-010 | PM Excel 보고서 다운로드 | 200 + Content-Type: xlsx | +| UT-PM-011 | next_scheduled 계산 — MONTHLY 말일 | 말일 처리 정확성 | +| UT-PM-012 | PM 템플릿 소프트 삭제 | 200 + is_active=False | + +### 4.6 장애 관리 (incidents) 단위 테스트 + +| 테스트 ID | 테스트명 | 기대 결과 | +|-----------|----------|-----------| +| UT-INC-001 | 장애 생성 (P1) | 201 + INC-YYYYMMDD-XXXXXX 형식 | +| UT-INC-002 | 장애 생성 (P4) | 201 + 알림 없음 | +| UT-INC-003 | P1/P2 생성 → Messenger 알림 | 알림 함수 호출 확인 (mock) | +| UT-INC-004 | 상태 전환 OPEN→INVESTIGATING | 200 + 타임라인 기록 | +| UT-INC-005 | 잘못된 상태 전환 CLOSED→OPEN | 400 Bad Request | +| UT-INC-006 | 장애 타임라인 조회 | 200 + events 목록 | +| UT-INC-007 | 장애 종결 (RCA 필수) | 400 if rca 없음 | +| UT-INC-008 | 장애 목록 — P1 필터 | 200 + P1 only | +| UT-INC-009 | 장애 통계 (기간별) | 200 + count by priority | +| UT-INC-010 | 장애 SR 연동 | 200 + related_sr_ids 업데이트 | + +### 4.7 온콜/당직 (oncall) 단위 테스트 + +| 테스트 ID | 테스트명 | 기대 결과 | +|-----------|----------|-----------| +| UT-OC-001 | 당직 단건 등록 | 201 + duty_id | +| UT-OC-002 | 중복 당직 등록 (같은 날+교대) | 409 Conflict | +| UT-OC-003 | 당직 일괄 등록 (최대 62건) | 201 + count | +| UT-OC-004 | 당직 일괄 — 초과 (63건) | 422 Validation Error | +| UT-OC-005 | 오늘 당직 조회 | 200 + 교대별 그룹 | +| UT-OC-006 | 월별 당직 조회 | 200 + 해당 월 목록 | +| UT-OC-007 | 당직 삭제 | 204 No Content | + +### 4.8 배치 작업 (batch) 단위 테스트 + +| 테스트 ID | 테스트명 | 기대 결과 | +|-----------|----------|-----------| +| UT-BAT-001 | 배치 작업 생성 | 201 + batch_id | +| UT-BAT-002 | 위험 명령 차단 (rm -rf /) | 400 Bad Request | +| UT-BAT-003 | 위험 명령 차단 (shutdown) | 400 Bad Request | +| UT-BAT-004 | 위험 명령 차단 (fork bomb) | 400 Bad Request | +| UT-BAT-005 | 배치 수동 실행 요청 | 202 Accepted | +| UT-BAT-006 | 배치 실행 이력 조회 | 200 + runs 목록 | +| UT-BAT-007 | 배치 실패 시 SR 자동 생성 | SR 생성 확인 (mock) | +| UT-BAT-008 | 비활성 배치 스케줄 — 실행 안 함 | is_active=False 체크 | +| UT-BAT-009 | 배치 작업 삭제 | 204 No Content | +| UT-BAT-010 | TIMEOUT 처리 (asyncio timeout) | 실행 상태=TIMEOUT | + +### 4.9 암호화 유틸 단위 테스트 + +| 테스트 ID | 테스트명 | 기대 결과 | +|-----------|----------|-----------| +| UT-CRYPTO-001 | AES-256-GCM 암호화/복호화 왕복 | 원문 복원 성공 | +| UT-CRYPTO-002 | 서로 다른 평문 → 다른 암호문 | 결정론적 아님 확인 | +| UT-CRYPTO-003 | 잘못된 키로 복호화 | 예외 발생 | +| UT-CRYPTO-004 | 빈 문자열 암호화 | 정상 처리 | +| UT-CRYPTO-005 | 한국어 포함 문자열 암호화 | UTF-8 왕복 성공 | + +### 4.10 스케줄러 단위 테스트 + +```python +# tests/unit/test_scheduler.py +import pytest +from datetime import datetime +from freezegun import freeze_time +from core.scheduler import _calc_next +from models import PmSchedule, PmFrequency + +@pytest.mark.asyncio +async def test_calc_next_weekly(): + with freeze_time("2026-01-01"): + sched = PmSchedule(frequency=PmFrequency.WEEKLY) + result = _calc_next(sched, datetime(2026, 1, 1)) + assert result == datetime(2026, 1, 8) + +@pytest.mark.asyncio +async def test_calc_next_monthly_end_of_month(): + # 1월 31일에서 +1달 → 2월 28일 (말일 처리) + sched = PmSchedule(frequency=PmFrequency.MONTHLY, day_of_month=31) + result = _calc_next(sched, datetime(2026, 1, 31)) + assert result.day == 28 # 2026년 2월 말일 +``` + +--- + +## 5. 통합 테스트 계획 + +### 5.1 SR 처리 전체 워크플로우 + +**시나리오**: 고객 SR 접수 → 담당자 배정 → 처리 → 완료 → 만족도 평가 + +``` +TC-INT-SR-001: SR 접수부터 완료까지 전체 흐름 +1. POST /tasks/ — SR 생성 (CUSTOMER 역할) +2. GET /tasks/{id} — 생성 확인 +3. PATCH /assign/{sr_id} — 담당자 배정 (PM 역할) +4. PATCH /tasks/{id}/status — IN_PROGRESS 전환 +5. POST /work/ — 처리 내역 등록 +6. PATCH /tasks/{id}/status — RESOLVED 전환 +7. POST /rating/ — 만족도 5점 등록 +8. GET /dashboard/summary — 통계 반영 확인 + +기대 결과: 전 단계 성공, 최종 SR 상태=RESOLVED, rating=5 +``` + +### 5.2 결재 워크플로우 + +**시나리오**: CHANGE 유형 SR → 결재 요청 → 승인 → 실행 + +``` +TC-INT-APV-001: 결재 승인 워크플로우 +1. POST /tasks/ — CHANGE SR 생성 +2. POST /approvals/ — 결재 요청 (결재자 2단계 설정) +3. PATCH /approvals/{id}/approve — 1단계 승인 +4. PATCH /approvals/{id}/approve — 2단계 승인 +5. GET /tasks/{sr_id} — SR 상태 = APPROVED 확인 +6. PATCH /tasks/{id}/status — IMPLEMENTING 전환 + +TC-INT-APV-002: 결재 거부 워크플로우 +1. POST /tasks/ + POST /approvals/ +2. PATCH /approvals/{id}/reject — 거부 (사유 필수) +3. GET /tasks/{sr_id} — SR 상태 = REJECTED 확인 +``` + +### 5.3 PM 정기점검 워크플로우 + +``` +TC-INT-PM-001: PM 스케줄 → 자동 작업 생성 +1. POST /pm/templates/ — 체크리스트 템플릿 생성 +2. POST /pm/schedules/ — MONTHLY 스케줄 등록 +3. (스케줄러 _auto_generate_pm 함수 직접 호출) +4. GET /timetable/ — WorkTimetable 생성 확인 +5. GET /pm/works/{id} — PM 작업 목록 확인 + +TC-INT-PM-002: PM 결과 저장 및 Excel 보고서 +1. (PM 작업 생성 선행) +2. POST /pm/works/{id}/results — 각 항목 PASS/FAIL 저장 +3. PATCH /pm/works/{id}/complete — 완료 처리 +4. GET /pm/works/{id}/report — Excel 다운로드 +5. openpyxl로 파일 열기 → PASS/FAIL 색상 확인 +``` + +### 5.4 장애 처리 워크플로우 + +``` +TC-INT-INC-001: P1 장애 발생 → 처리 → 종결 +1. POST /incidents/ — P1 장애 등록 + → Messenger 알림 mock 확인 +2. POST /incidents/{id}/timeline — "장애 감지 완료" 기록 +3. PATCH /incidents/{id}/status — INVESTIGATING +4. PATCH /incidents/{id}/status — MITIGATED (조치 내역 포함) +5. PATCH /incidents/{id}/status — RESOLVED (임시 해결) +6. POST /incidents/{id}/close — RCA 문서 포함 종결 +7. GET /incidents/{id} — 전체 타임라인 확인 +``` + +### 5.5 배치 실행 및 실패 알림 + +``` +TC-INT-BAT-001: 배치 실행 실패 → SR 자동 생성 +1. POST /batch/jobs — 의도적 실패 명령으로 배치 등록 + (예: "exit 1" 명령) +2. POST /batch/jobs/{id}/run — 수동 실행 +3. (BackgroundTask 완료 대기) +4. GET /batch/jobs/{id}/runs — 상태 = FAILED 확인 +5. GET /tasks/ — 자동 생성된 SR 확인 (sr_type=LOG, priority=HIGH) +``` + +### 5.6 Messenger 알림 통합 테스트 + +``` +TC-INT-MSG-001: ITSM 이벤트 → Messenger 채널 발송 +1. mock Messenger API (respx) +2. P1 장애 생성 → /incidents/ POST +3. respx 캡처된 요청에서 채널 ID, 메시지 내용 검증 +4. 메시지에 incident_id, priority, title 포함 확인 +5. 서버 IP/비밀번호 미포함 확인 (보안) + +TC-INT-MSG-002: Bot 명령 처리 +1. POST /messenger/webhook — "/itsm sr list" 명령 +2. 응답 메시지에 SR 목록 포함 확인 +3. POST /messenger/webhook — "/itsm help" +4. 응답 메시지에 명령어 목록 확인 +``` + +--- + +## 6. 비기능 테스트 계획 + +### 6.1 성능 테스트 + +| 항목 | 목표 | 측정 방법 | +|------|------|-----------| +| API 응답 시간 (평균) | ≤ 500ms | locust or k6 | +| API 응답 시간 (P99) | ≤ 2000ms | locust or k6 | +| 동시 사용자 | 50명 이상 정상 처리 | locust 동시 접속 | +| DB 트랜잭션 | 100 TPS 이상 | pytest-benchmark | +| 파일 업로드 | 10MB 이하 ≤ 3초 | httpx 직접 테스트 | + +### 6.2 보안 테스트 + +| 항목 | 테스트 방법 | 기대 결과 | +|------|------------|-----------| +| SQL 인젝션 | 검색 파라미터에 SQL 구문 주입 | 에러 없이 빈 결과 | +| XSS | SR 제목에 `` | HTML 이스케이프 | +| JWT 위조 | 임의 서명 토큰 사용 | 401 | +| 경로 순회 | `../../../etc/passwd` 첨부파일 | 400 | +| SSH 명령 인젝션 | `; rm -rf /` 포함 명령 | 400 | +| 서버 정보 노출 | API 응답에 IP/PW 포함 여부 | 미포함 확인 | +| 스택트레이스 노출 | 의도적 오류 발생 | SR ID + 요약만 반환 | + +### 6.3 감사 로그 무결성 테스트 + +``` +TC-AUDIT-001: 해시 체인 무결성 +1. 여러 감사 로그 생성 (API 호출 10회) +2. GET /audit/logs — 전체 조회 +3. 각 로그의 prev_hash → 이전 로그 hash 일치 확인 +4. DB에서 중간 로그 직접 수정 +5. GET /audit/verify — 무결성 깨짐 감지 확인 +``` + +--- + +## 7. 테스트 케이스 목록 요약 + +| 모듈 | 단위 TC | 통합 TC | 합계 | +|------|---------|---------|------| +| auth | 8 | 2 | 10 | +| tasks/SR | 10 | 4 | 14 | +| approvals | 6 | 3 | 9 | +| ssl_manager | 10 | 2 | 12 | +| pm | 12 | 3 | 15 | +| incidents | 10 | 2 | 12 | +| oncall | 7 | 1 | 8 | +| batch | 10 | 2 | 12 | +| scheduler | 6 | 2 | 8 | +| crypto/security | 5 | 3 | 8 | +| messenger | 4 | 3 | 7 | +| audit | 4 | 2 | 6 | +| **합계** | **92** | **29** | **121** | + +--- + +## 8. 결함 관리 + +### 8.1 결함 등급 + +| 등급 | 정의 | 처리 기한 | +|------|------|-----------| +| Critical | 시스템 중단, 데이터 손실, 보안 취약점 | 즉시 (24시간 내) | +| Major | 핵심 기능 비정상, 결과 오류 | 3일 내 | +| Minor | 비핵심 기능 문제, UI 오류 | 7일 내 | +| Trivial | 오타, 경미한 표시 오류 | 다음 버전 | + +### 8.2 결함 생명주기 + +``` +NEW → ASSIGNED → IN_PROGRESS → FIXED → VERIFIED → CLOSED + ↓ + REOPENED (재현 시) +``` + +### 8.3 결함 보고서 양식 + +``` +결함 ID: BUG-YYYY-NNN +제목: [모듈] 결함 요약 +심각도: Critical/Major/Minor/Trivial +재현 단계: + 1. ... + 2. ... +실제 결과: ... +기대 결과: ... +스크린샷/로그: (첨부) +발견일: YYYY-MM-DD +발견자: ... +``` + +--- + +## 9. 완료 기준 + +### 9.1 단계별 완료 기준 + +**단위 테스트 완료**: +- [ ] 모든 단위 TC 실행 완료 +- [ ] 코드 커버리지 80% 이상 +- [ ] Critical/Major 결함 0건 + +**통합 테스트 완료**: +- [ ] 모든 통합 시나리오 TC 실행 완료 +- [ ] 주요 워크플로우 E2E 검증 완료 +- [ ] Critical/Major 결함 0건 + +**보안 테스트 완료**: +- [ ] 보안 TC 전 항목 통과 +- [ ] API 응답에 민감 정보 미포함 확인 +- [ ] 감사 로그 무결성 확인 + +### 9.2 테스트 실행 명령 + +```bash +# 전체 단위 테스트 +cd C:\GUARDiA\itsm +pytest tests/unit/ -v --cov=. --cov-report=html + +# 통합 테스트 +pytest tests/integration/ -v --timeout=30 + +# 특정 모듈 +pytest tests/unit/test_incidents.py -v + +# 커버리지 HTML 보고서 확인 +open htmlcov/index.html +``` + +--- + +*본 테스트 계획서는 GUARDiA ITSM v1.0 기준으로 작성되었으며, 버전 업그레이드 시 갱신이 필요합니다.* diff --git a/03_개발자지침서.md b/03_개발자지침서.md new file mode 100644 index 0000000..94e1fee --- /dev/null +++ b/03_개발자지침서.md @@ -0,0 +1,951 @@ +# GUARDiA ITSM + Messenger — 개발자 지침서 + +**문서 버전**: 1.0 +**작성일**: 2026-05-25 +**대상**: GUARDiA 플랫폼 개발자 + +--- + +## 목차 + +1. [개발 환경 설정](#1-개발-환경-설정) +2. [프로젝트 구조](#2-프로젝트-구조) +3. [코딩 컨벤션](#3-코딩-컨벤션) +4. [API 개발 가이드](#4-api-개발-가이드) +5. [데이터베이스 가이드](#5-데이터베이스-가이드) +6. [보안 필수 규칙](#6-보안-필수-규칙) +7. [테스트 작성 가이드](#7-테스트-작성-가이드) +8. [Messenger Bot 개발 가이드](#8-messenger-bot-개발-가이드) +9. [스케줄러 확장 가이드](#9-스케줄러-확장-가이드) +10. [배포 및 CI/CD](#10-배포-및-cicd) +11. [자주 하는 실수 및 주의사항](#11-자주-하는-실수-및-주의사항) + +--- + +## 1. 개발 환경 설정 + +### 1.1 필수 도구 + +``` +Python 3.11+ +Git +VS Code (권장) 또는 PyCharm +SQLite Browser (DB 확인용) +httpie 또는 Postman (API 테스트) +``` + +### 1.2 로컬 개발 환경 구성 + +```bash +# 1. 레포지토리 클론 +git clone +cd GUARDiA/itsm + +# 2. 가상환경 생성 (Python 3.11 사용) +python -m venv .venv + +# Windows +.venv\Scripts\activate +# Linux/Mac +source .venv/bin/activate + +# 3. 의존성 설치 +pip install -r requirements.txt +pip install -r requirements-dev.txt # 개발/테스트용 + +# 4. 환경 변수 설정 +cp .env.example .env +# .env 파일 편집 (SECRET_KEY, DB_PATH 등) + +# 5. 앱 실행 +uvicorn main:app --reload --port 8000 + +# 6. API 문서 확인 +# http://localhost:8000/docs +``` + +### 1.3 .env 파일 설정 + +```ini +# .env.example +SECRET_KEY=your-256-bit-secret-key-here-change-in-production +ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=480 +DB_URL=sqlite+aiosqlite:///./guardia_itsm.db + +# 암호화 키 (AES-256-GCM, 32바이트 hex) +ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000 + +# 라이선스 마스터 키 (64자리 hex, 32바이트) +# 생성: python -c "import secrets; print(secrets.token_hex(32))" +# 운영 환경에서는 반드시 실제 값으로 교체 — 분실 시 기발급 라이선스 전부 무효화됨 +GUARDIA_LICENSE_KEY=0000000000000000000000000000000000000000000000000000000000000000 + +# Messenger 연동 +MESSENGER_BASE_URL=http://localhost:8002 +MESSENGER_BOT_TOKEN=dev-token + +# SSH 실행 타임아웃 (초) +SSH_TIMEOUT=30 + +# 파일 업로드 경로 +UPLOAD_ROOT=./uploads +MAX_FILE_SIZE_MB=10 +``` + +> **주의**: `.env` 파일은 절대 Git에 커밋하지 마세요. `.gitignore`에 포함되어 있습니다. + +### 1.4 VS Code 권장 설정 + +`.vscode/settings.json`: +```json +{ + "python.defaultInterpreterPath": ".venv/Scripts/python.exe", + "editor.formatOnSave": true, + "python.formatting.provider": "black", + "python.linting.enabled": true, + "python.linting.pylintEnabled": false, + "python.linting.flake8Enabled": true, + "python.linting.flake8Args": ["--max-line-length=120"] +} +``` + +--- + +## 2. 프로젝트 구조 + +``` +C:\GUARDiA\itsm\ +├── main.py # FastAPI 앱 진입점 +├── database.py # DB 엔진, SessionLocal, get_db +├── models.py # SQLAlchemy ORM 모델 (전체) +├── schemas.py # Pydantic 스키마 (전체) +├── requirements.txt # 운영 의존성 +├── requirements-dev.txt # 개발/테스트 의존성 +├── .env # 환경 변수 (git 제외) +│ +├── core\ +│ ├── seed.py # 초기 데이터 시드 +│ ├── scheduler.py # APScheduler 백그라운드 작업 +│ └── security.py # JWT, 비밀번호 해싱 +│ +├── routers\ +│ ├── auth.py # 인증/권한 +│ ├── tasks.py # SR 처리 +│ ├── approvals.py # 결재 +│ ├── ssl_manager.py # SSL 인증서 관리 +│ ├── pm.py # 정기점검 +│ ├── incidents.py # 장애 관리 +│ ├── oncall.py # 온콜/당직 +│ ├── batch.py # 배치 작업 +│ └── ... # 기타 라우터 +│ +├── utils\ +│ ├── crypto.py # AES-256-GCM 암/복호화 +│ └── ssh_runner.py # SSH 명령 실행 +│ +├── static\ # 프론트엔드 (HTML/CSS/JS) +├── uploads\ # 업로드 파일 저장소 +│ ├── sr_files\ +│ └── workspaces\ +│ +├── scripts\sm\ssl\ +│ └── ssl_expiry_check.sh # 배포용 SSL 점검 스크립트 +│ +└── tests\ + ├── conftest.py + ├── unit\ + └── integration\ + +C:\GUARDiA\messenger\ +├── main.py # Messenger 서버 +├── models\ # DB 모델 +├── core\ # 봇 로직 +└── routers\ # API 라우터 +``` + +--- + +## 3. 코딩 컨벤션 + +### 3.1 Python 스타일 + +```python +# 파일 맨 위: 임포트 순서 (isort 준수) +# 1) 표준 라이브러리 +import os +import json +from datetime import datetime, timedelta + +# 2) 서드파티 +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.ext.asyncio import AsyncSession + +# 3) 로컬 모듈 +from database import get_db +from models import SRRequest +from schemas import SRCreate, SROut + +# 줄 최대 길이: 120자 +# 들여쓰기: 4 space (탭 금지) +# 문자열: 큰따옴표(") 사용 +``` + +### 3.2 라우터 파일 구조 패턴 + +새 라우터를 만들 때 반드시 이 구조를 따르세요: + +```python +# routers/example.py +from datetime import datetime +from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from database import get_db +from models import ExampleModel +from schemas import ExampleCreate, ExampleOut, ExampleUpdate +from core.security import get_current_user +from models import User, Role + +router = APIRouter(prefix="/example", tags=["example"]) + + +def _require_role(*roles: Role): + """역할 기반 접근 제어 의존성""" + async def checker(current_user: User = Depends(get_current_user)): + if current_user.role not in roles: + raise HTTPException(status_code=403, detail="권한이 없습니다.") + return current_user + return checker + + +@router.get("/", response_model=list[ExampleOut]) +async def list_examples( + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +): + result = await db.execute(select(ExampleModel).order_by(ExampleModel.created_at.desc())) + return result.scalars().all() + + +@router.post("/", response_model=ExampleOut, status_code=status.HTTP_201_CREATED) +async def create_example( + body: ExampleCreate, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(_require_role(Role.ADMIN, Role.PM)), +): + obj = ExampleModel(**body.model_dump(), created_by=current_user.id) + db.add(obj) + await db.commit() + await db.refresh(obj) + return obj +``` + +### 3.3 모델 정의 패턴 + +```python +# models.py 에 추가할 때 +class ExampleModel(Base): + __tablename__ = "tb_example" + + id = Column(Integer, primary_key=True, autoincrement=True) + title = Column(String(200), nullable=False) + description = Column(Text, nullable=True) + is_active = Column(Boolean, default=True, nullable=False) + created_by = Column(Integer, ForeignKey("tb_user.id"), nullable=True) + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) +``` + +### 3.4 스키마 정의 패턴 + +```python +# schemas.py 에 추가할 때 +class ExampleCreate(BaseModel): + title: str = Field(..., min_length=1, max_length=200) + description: Optional[str] = None + +class ExampleUpdate(BaseModel): + title: Optional[str] = Field(None, min_length=1, max_length=200) + description: Optional[str] = None + +class ExampleOut(BaseModel): + id: int + title: str + description: Optional[str] + is_active: bool + created_at: datetime + + model_config = ConfigDict(from_attributes=True) +``` + +### 3.5 명명 규칙 + +| 대상 | 규칙 | 예시 | +|------|------|------| +| 파일명 | snake_case | `ssl_manager.py` | +| 클래스명 | PascalCase | `SslDomain`, `PmSchedule` | +| 함수/변수 | snake_case | `get_ssl_domains()`, `days_left` | +| DB 테이블 | `tb_` 접두사 | `tb_ssl_domain` | +| 상수 | UPPER_SNAKE | `MAX_FILE_SIZE`, `SSL_WARN_DAYS` | +| 비공개 함수 | `_` 접두사 | `_notify_incident()` | +| API 경로 | kebab-case | `/ssl-manager/domains` | + +--- + +## 4. API 개발 가이드 + +### 4.1 응답 형식 규칙 + +```python +# 성공 응답: HTTP 상태 코드로 구분 +# 200 OK — 조회, 수정 +# 201 Created — 생성 +# 202 Accepted — 비동기 실행 (배치, 백그라운드) +# 204 No Content — 삭제 + +# 에러 응답: 반드시 이 형식 사용 +raise HTTPException( + status_code=400, + detail="사용자 친화적인 한국어 메시지" +) + +# 절대 금지: 스택트레이스 노출 +# raise Exception(traceback.format_exc()) # ❌ +``` + +### 4.2 페이징 구현 패턴 + +```python +from fastapi import Query + +@router.get("/", response_model=dict) +async def list_items( + page: int = Query(1, ge=1), + size: int = Query(20, ge=1, le=100), + db: AsyncSession = Depends(get_db), +): + offset = (page - 1) * size + count_q = select(func.count()).select_from(Item) + total = (await db.execute(count_q)).scalar() + + items_q = select(Item).offset(offset).limit(size).order_by(Item.created_at.desc()) + items = (await db.execute(items_q)).scalars().all() + + return { + "items": items, + "total": total, + "page": page, + "size": size, + "pages": (total + size - 1) // size, + } +``` + +### 4.3 비동기 DB 쿼리 패턴 + +```python +# ✅ 올바른 패턴: asyncio 방식 +async def get_item(item_id: int, db: AsyncSession) -> Item: + result = await db.execute(select(Item).where(Item.id == item_id)) + item = result.scalar_one_or_none() + if item is None: + raise HTTPException(status_code=404, detail="항목을 찾을 수 없습니다.") + return item + +# ✅ 여러 조건 필터 +query = select(Item).where( + Item.is_active == True, + Item.category == category, +).order_by(Item.created_at.desc()) + +# ✅ JOIN +query = select(Item).join(User, Item.created_by == User.id) + +# ❌ 잘못된 패턴: 동기 방식 사용 금지 +# db.query(Item).filter(Item.id == item_id).first() # ❌ +``` + +### 4.4 백그라운드 작업 패턴 + +```python +from fastapi import BackgroundTasks + +@router.post("/{id}/run", status_code=202) +async def run_job( + id: int, + background_tasks: BackgroundTasks, + db: AsyncSession = Depends(get_db), +): + job = await _get_job(id, db) + background_tasks.add_task(_execute_job, job.id) + return {"message": "실행이 요청되었습니다.", "job_id": id} + + +async def _execute_job(job_id: int): + # 중요: 새 DB 세션 생성 (request-scope 세션 재사용 금지!) + from database import SessionLocal + async with SessionLocal() as db: + job = await _get_job(job_id, db) + # ... 실행 로직 + await db.commit() +``` + +### 4.5 main.py 라우터 등록 + +새 라우터를 추가할 때: + +```python +# main.py +from routers import ( + ... + new_module, # ← 여기에 추가 +) + +# lifespan 함수 안에 필요한 초기화 추가 + +# 라우터 등록 (알파벳 순 권장) +app.include_router(new_module.router) +``` + +--- + +## 5. 데이터베이스 가이드 + +### 5.1 마이그레이션 정책 + +현재 GUARDiA ITSM은 **Alembic 없이** `init_db()`로 테이블을 자동 생성합니다. + +```python +# database.py +async def init_db(): + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) +``` + +> **주의**: 운영 DB에서는 테이블 삭제/재생성이 일어나지 않습니다. 컬럼 추가/변경 시 수동 ALTER TABLE이 필요합니다. + +**신규 컬럼 추가 절차**: +1. `models.py`에 컬럼 추가 +2. 운영 DB에 수동 ALTER: `ALTER TABLE tb_xxx ADD COLUMN new_col TEXT;` +3. 기본값 업데이트: `UPDATE tb_xxx SET new_col = 'default' WHERE new_col IS NULL;` + +### 5.2 관계 설정 패턴 + +```python +# 1:N 관계 (부모→자식) +class SRRequest(Base): + __tablename__ = "tb_sr_request" + # ... + work_logs = relationship("WorkLog", back_populates="sr", lazy="select") + +class WorkLog(Base): + __tablename__ = "tb_work_log" + sr_id = Column(Integer, ForeignKey("tb_sr_request.id"), nullable=False) + sr = relationship("SRRequest", back_populates="work_logs") + +# 쿼리 시 eagerly load (N+1 문제 방지) +from sqlalchemy.orm import selectinload +query = select(SRRequest).options(selectinload(SRRequest.work_logs)) +``` + +### 5.3 enum 정의 패턴 + +```python +import enum + +class SRStatus(str, enum.Enum): + OPEN = "OPEN" + IN_PROGRESS = "IN_PROGRESS" + RESOLVED = "RESOLVED" + CLOSED = "CLOSED" + +# 모델에서 사용 +class SRRequest(Base): + status = Column(Enum(SRStatus), default=SRStatus.OPEN, nullable=False) +``` + +### 5.4 트랜잭션 처리 + +```python +# 여러 테이블 수정 시 트랜잭션 보장 +async def complex_operation(db: AsyncSession): + try: + obj1 = Model1(...) + obj2 = Model2(...) + db.add(obj1) + db.add(obj2) + await db.commit() # 모두 성공 시 커밋 + except Exception: + await db.rollback() # 하나라도 실패 시 롤백 + raise +``` + +--- + +## 6. 보안 필수 규칙 + +> **이 규칙들은 절대 예외가 없습니다. 위반 시 코드 리뷰에서 반려됩니다.** + +### 6.1 서버 자격증명 보호 + +```python +# ✅ 서버 조회 응답에서 민감 필드 제외 +class ServerOut(BaseModel): + id: int + hostname: str + server_role: str + # ip_addr, ssh_user, os_pw_enc 절대 포함 금지! + +# ❌ 절대 하지 말 것 +class ServerOut(BaseModel): + ip_addr: str # ❌ IP 노출 금지 + ssh_user: str # ❌ SSH 계정 노출 금지 + os_pw_enc: str # ❌ 암호화된 비밀번호도 노출 금지 +``` + +### 6.2 SSH 명령 안전성 검증 + +```python +_DANGEROUS_PATTERNS = [ + "rm -rf /", "rm -rf /*", "mkfs", "dd if=", + "shutdown", "reboot", "halt", "poweroff", + ":(){:|:&};:", ">()", "chmod 777 /", + "chown -R root /", "> /dev/sda", +] + +def _validate_command(cmd: str) -> None: + for pattern in _DANGEROUS_PATTERNS: + if pattern in cmd: + raise HTTPException( + status_code=400, + detail=f"안전하지 않은 명령어 패턴이 포함되어 있습니다: {pattern}" + ) +``` + +### 6.3 파일 경로 순회 방지 + +```python +from pathlib import Path + +UPLOAD_ROOT = Path(settings.UPLOAD_ROOT).resolve() + +def _safe_path(filename: str, subdir: str) -> Path: + target = (UPLOAD_ROOT / subdir / filename).resolve() + if not str(target).startswith(str(UPLOAD_ROOT)): + raise HTTPException(status_code=400, detail="잘못된 파일 경로입니다.") + return target +``` + +### 6.4 AES-256-GCM 암호화 사용 + +```python +# utils/crypto.py +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +import os, base64 + +def encrypt(plaintext: str, key_hex: str) -> str: + key = bytes.fromhex(key_hex) + nonce = os.urandom(12) + aesgcm = AESGCM(key) + ct = aesgcm.encrypt(nonce, plaintext.encode(), None) + return base64.b64encode(nonce + ct).decode() + +def decrypt(ciphertext: str, key_hex: str) -> str: + key = bytes.fromhex(key_hex) + data = base64.b64decode(ciphertext) + nonce, ct = data[:12], data[12:] + aesgcm = AESGCM(key) + return aesgcm.decrypt(nonce, ct, None).decode() +``` + +### 6.5 Messenger 발송 메시지 보안 + +```python +# ✅ 올바른 알림 메시지 (서버 정보 제외) +message = f"[장애] {incident.title}\n등급: {incident.priority}\nID: {incident.incident_id}" + +# ❌ 절대 금지 (서버 정보 포함) +message = f"서버 {server.ip_addr}에서 장애 발생" # ❌ IP 노출 +message = f"SSH 계정: {server.ssh_user}" # ❌ 계정 노출 +``` + +### 6.6 JWT 토큰 처리 + +```python +# 토큰 생성 시 만료 시간 필수 +def create_access_token(data: dict) -> str: + expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) + return jwt.encode({**data, "exp": expire}, settings.SECRET_KEY, algorithm="HS256") + +# 의존성에서 자동 검증 +async def get_current_user(token: str = Depends(oauth2_scheme), db: AsyncSession = Depends(get_db)): + try: + payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"]) + except JWTError: + raise HTTPException(status_code=401, detail="유효하지 않은 토큰입니다.") +``` + +### 6.7 root SSH 접속 금지 + +```python +# 서버 등록/수정 시 검증 +if body.ssh_user == "root": + raise HTTPException(status_code=400, detail="root SSH 직접 접속은 허용되지 않습니다.") +``` + +### 6.8 라이선스 제한 강제 + +에디션별 자원 한도(기관·사용자·서버 수)를 초과하는 생성 API에는 반드시 Dependency를 추가해야 한다. + +```python +from middleware.license_guard import ( + check_institution_limit, + check_server_limit, + check_user_limit, + require_feature, +) + +# 기관 등록 엔드포인트 예시 +@router.post("/", response_model=InstitutionOut, status_code=201) +async def create_institution( + payload: InstitutionCreate, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), + _: None = Depends(check_institution_limit), # 에디션 한도 초과 시 HTTP 403 +): + ... + +# 서버 등록 엔드포인트 예시 +@router.post("/servers", response_model=ServerOut, status_code=201) +async def create_server( + payload: dict, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), + _: None = Depends(check_server_limit), # 에디션 한도 초과 시 HTTP 403 +): + ... + +# 특정 기능이 에디션에 포함되는지 체크 (STANDARD 이상 필요한 LDAP 예시) +@router.post("/ldap/sync") +async def ldap_sync( + _: None = Depends(require_feature("LDAP")), +): + ... +``` + +에디션별 기본 한도: +| 에디션 | 기관 | 사용자 | 서버 | +|--------|------|-------|------| +| COMMUNITY | 1 | 10 | 20 | +| STANDARD | 50 | 200 | 500 | +| ENTERPRISE | 무제한 | 무제한 | 무제한 | + +--- + +## 7. 테스트 작성 가이드 + +### 7.1 단위 테스트 기본 구조 + +```python +import pytest +import pytest_asyncio + +class TestSslManager: + @pytest.mark.asyncio + async def test_register_domain_success(self, client, auth_headers): + # Arrange + payload = {"domain": "example.com", "port": 443, "server_id": 1} + + # Act + r = await client.post("/ssl-manager/domains", json=payload, headers=auth_headers) + + # Assert + assert r.status_code == 201 + data = r.json() + assert data["domain"] == "example.com" + assert "id" in data + + @pytest.mark.asyncio + async def test_register_domain_duplicate(self, client, auth_headers): + payload = {"domain": "example.com", "port": 443, "server_id": 1} + await client.post("/ssl-manager/domains", json=payload, headers=auth_headers) + + r = await client.post("/ssl-manager/domains", json=payload, headers=auth_headers) + assert r.status_code == 409 +``` + +### 7.2 mock 사용 패턴 + +```python +from unittest.mock import AsyncMock, patch + +@pytest.mark.asyncio +async def test_p1_incident_triggers_notification(client, auth_headers): + with patch("routers.incidents._notify_incident", new_callable=AsyncMock) as mock_notify: + r = await client.post("/incidents/", json={ + "title": "DB 서버 다운", + "priority": "P1", + "description": "운영 DB 응답 없음", + }, headers=auth_headers) + + assert r.status_code == 201 + mock_notify.assert_called_once() + call_args = mock_notify.call_args + assert call_args[0][0].priority == "P1" # 첫 번째 인자 확인 +``` + +### 7.3 시간 고정 테스트 + +```python +from freezegun import freeze_time + +@pytest.mark.asyncio +@freeze_time("2026-01-15 00:00:00") +async def test_pm_schedule_monthly_next_date(client, auth_headers): + r = await client.post("/pm/schedules/", json={ + "template_id": 1, + "frequency": "MONTHLY", + "day_of_month": 15, + "server_id": 1, + }, headers=auth_headers) + assert r.status_code == 201 + # 1월 15일 기준 → 다음 실행은 2월 15일 + assert "2026-02-15" in r.json()["next_scheduled"] +``` + +--- + +## 8. Messenger Bot 개발 가이드 + +### 8.1 GUARDiA_ITSM 봇 명령 추가 + +명령어 추가는 `C:\GUARDiA\messenger\core\itsm_bot.py` 에서 수행합니다. + +```python +# 새 명령어 핸들러 추가 패턴 +COMMAND_HANDLERS = { + "/itsm sr list": handle_sr_list, + "/itsm sr create": handle_sr_create, + "/itsm help": handle_help, + "/itsm incident p1": handle_p1_alert, # 새 명령어 추가 +} + +async def handle_p1_alert(args: list[str], user_id: int) -> str: + """P1 장애 목록 조회""" + async with httpx.AsyncClient() as client: + r = await client.get( + f"{ITSM_BASE_URL}/incidents/?priority=P1&status=OPEN", + headers={"Authorization": f"Bearer {ITSM_BOT_TOKEN}"}, + ) + if r.status_code != 200: + return "장애 목록 조회 실패" + incidents = r.json()["items"] + if not incidents: + return "현재 P1 장애 없음" + lines = [f"[{i['incident_id']}] {i['title']}" for i in incidents[:5]] + return "P1 장애 목록:\n" + "\n".join(lines) +``` + +### 8.2 ITSM → Messenger 알림 발송 + +```python +# routers/incidents.py 내 알림 함수 +async def _notify_incident(incident: IncidentModel) -> None: + """장애 발생 시 Messenger 채널 알림""" + try: + channel_id = settings.INCIDENT_CHANNEL_ID + # 보안: 서버 IP/PW 미포함 + msg = ( + f"🚨 **{incident.priority} 장애 발생**\n" + f"ID: {incident.incident_id}\n" + f"제목: {incident.title}\n" + f"등록: {incident.created_at.strftime('%Y-%m-%d %H:%M')}" + ) + async with httpx.AsyncClient(timeout=5) as client: + await client.post( + f"{settings.MESSENGER_BASE_URL}/api/messages", + json={"channel_id": channel_id, "content": msg}, + headers={"Authorization": f"Bearer {settings.MESSENGER_BOT_TOKEN}"}, + ) + except Exception: + # 알림 실패가 비즈니스 로직을 방해하면 안 됨 + pass +``` + +--- + +## 9. 스케줄러 확장 가이드 + +### 9.1 새 스케줄 작업 추가 + +`core/scheduler.py`에 새 작업을 추가합니다: + +```python +from apscheduler.triggers.cron import CronTrigger + +async def _new_scheduled_task() -> None: + """매주 월요일 오전 9시 실행 예시""" + from database import SessionLocal + async with SessionLocal() as db: + # 작업 수행 + pass + +def start_scheduler(): + # 기존 작업들 ... + _scheduler.add_job( + _new_scheduled_task, + CronTrigger(day_of_week="mon", hour=9, minute=0), + id="new_task", + replace_existing=True, + ) + _scheduler.start() +``` + +### 9.2 스케줄러 테스트 + +스케줄러는 직접 함수 호출로 테스트합니다: + +```python +@pytest.mark.asyncio +async def test_ssl_expiry_scan(db_session): + from core.scheduler import _scan_ssl_expiry + # 테스트 데이터 준비 + # ... + await _scan_ssl_expiry() # 직접 호출 + # DB 상태 확인 + # ... +``` + +--- + +## 10. 배포 및 CI/CD + +### 10.1 Jenkins 파이프라인 단계 + +```groovy +pipeline { + stages { + stage('Test') { + steps { + sh 'pytest tests/unit/ --cov=. --cov-report=xml' + } + } + stage('Quality Gate') { + steps { + // SonarQube 분석 + sh 'sonar-scanner -Dsonar.projectKey=guardia-itsm' + } + } + stage('Deploy') { + when { branch 'main' } + steps { + sh './scripts/deploy.sh production' + } + } + } +} +``` + +### 10.2 운영 서버 배포 체크리스트 + +``` +배포 전: +□ 모든 단위 테스트 통과 +□ 커버리지 80% 이상 +□ SonarQube 품질 게이트 통과 +□ .env 운영 설정 확인 (SECRET_KEY, ENCRYPTION_KEY) +□ DB 백업 완료 + +배포 중: +□ 서비스 점검 공지 +□ 현재 버전 백업 +□ 새 버전 배포 +□ DB 마이그레이션 (필요 시) +□ 서비스 재시작 + +배포 후: +□ /docs 페이지 접근 확인 +□ 로그인 테스트 +□ 주요 API 스모크 테스트 +□ 스케줄러 동작 확인 +``` + +--- + +## 11. 자주 하는 실수 및 주의사항 + +### 11.1 비동기 함수 실수 + +```python +# ❌ 실수: async 함수에서 동기 DB 호출 +async def bad_function(db: AsyncSession): + result = db.execute(select(Item)) # ❌ await 누락 + +# ✅ 올바른 방법 +async def good_function(db: AsyncSession): + result = await db.execute(select(Item)) # ✅ +``` + +### 11.2 DB 세션 누수 + +```python +# ❌ 실수: 백그라운드 태스크에서 request 세션 사용 +@router.post("/") +async def create(background_tasks: BackgroundTasks, db: AsyncSession = Depends(get_db)): + background_tasks.add_task(some_task, db) # ❌ 세션이 request 후 닫힘 + +# ✅ 올바른 방법: 백그라운드에서 새 세션 생성 +async def some_task(): + from database import SessionLocal + async with SessionLocal() as db: # ✅ 새 세션 + pass +``` + +### 11.3 N+1 쿼리 문제 + +```python +# ❌ N+1 쿼리 발생 +srs = (await db.execute(select(SRRequest))).scalars().all() +for sr in srs: + print(sr.work_logs) # 각 SR마다 추가 쿼리 발생! + +# ✅ selectinload 사용 +srs = (await db.execute( + select(SRRequest).options(selectinload(SRRequest.work_logs)) +)).scalars().all() +``` + +### 11.4 환경 변수 검증 + +```python +# ✅ 앱 시작 시 필수 환경 변수 검증 +class Settings(BaseSettings): + SECRET_KEY: str + ENCRYPTION_KEY: str + + @validator("ENCRYPTION_KEY") + def encryption_key_must_be_64_hex(cls, v): + if len(v) != 64: + raise ValueError("ENCRYPTION_KEY는 64자리 hex 문자열이어야 합니다.") + return v +``` + +### 11.5 외부 API 완전 금지 + +```python +# ❌ 절대 금지: 외부 LLM/AI API 호출 +import openai +client = openai.OpenAI(api_key="...") # ❌ 외부 API 사용 금지 + +# ✅ 내부 sLLM만 허용 +async with httpx.AsyncClient() as client: + r = await client.post(f"{settings.SLLM_BASE_URL}/v1/chat/completions", ...) +``` + +--- + +*본 지침서를 준수하지 않은 코드는 코드 리뷰에서 반려될 수 있습니다.* +*보안 관련 규칙(6장)은 특히 엄격히 적용됩니다.* diff --git a/04_운영자지침서.md b/04_운영자지침서.md new file mode 100644 index 0000000..7c7af4f --- /dev/null +++ b/04_운영자지침서.md @@ -0,0 +1,935 @@ +# GUARDiA ITSM + Messenger — 운영자 지침서 + +**문서 버전**: 1.0 +**작성일**: 2026-05-25 +**대상**: 시스템 운영자, IT 관리자 + +--- + +## 목차 + +1. [시스템 개요](#1-시스템-개요) +2. [일상 운영 절차](#2-일상-운영-절차) +3. [사용자 계정 관리](#3-사용자-계정-관리) +4. [SR(서비스요청) 관리](#4-sr서비스요청-관리) +5. [SSL 인증서 관리](#5-ssl-인증서-관리) +6. [정기점검(PM) 관리](#6-정기점검pm-관리) +7. [장애 관리](#7-장애-관리) +8. [온콜/당직 관리](#8-온콜당직-관리) +9. [배치 작업 관리](#9-배치-작업-관리) +10. [백업 및 복구](#10-백업-및-복구) +11. [모니터링 및 알림](#11-모니터링-및-알림) +12. [보안 운영 지침](#12-보안-운영-지침) +13. [장애 대응 절차](#13-장애-대응-절차) +14. [로그 관리](#14-로그-관리) +15. [주요 설정 파일](#15-주요-설정-파일) +16. [라이선스 관리](#16-라이선스-관리) + +--- + +## 1. 시스템 개요 + +### 1.1 구성 요소 + +| 구성요소 | 역할 | 기본 포트 | +|----------|------|-----------| +| GUARDiA ITSM | IT 서비스 관리 플랫폼 | 8000 | +| GUARDiA Messenger | 내부 메신저 + 봇 | 8001 | +| SQLite DB | 데이터 저장소 | — | +| APScheduler | 백그라운드 작업 (SSL 점검, PM) | — | + +### 1.2 서비스 URL + +``` +운영자/엔지니어 화면: http://<서버IP>:8000/ +고객 화면: http://<서버IP>:8000/customer +로그인: http://<서버IP>:8000/login +API 문서: http://<서버IP>:8000/docs (운영: 비활성화 권장) +Messenger: http://<서버IP>:8001/ +``` + +### 1.3 프로세스 구성 + +``` +systemd (Linux): + guardia-itsm.service — ITSM 메인 서버 + guardia-messenger.service — Messenger 서버 + +Windows Service: + GUARDiA-ITSM — ITSM 메인 서버 + GUARDiA-Messenger — Messenger 서버 +``` + +--- + +## 2. 일상 운영 절차 + +### 2.1 매일 아침 점검 체크리스트 + +``` +시간: 매일 09:00 + +□ 1. 서비스 상태 확인 + → curl http://localhost:8000/ (200 응답 확인) + +□ 2. 야간 스케줄러 실행 결과 확인 + → /var/log/guardia/scheduler.log 최신 항목 확인 + → SSL 점검 결과 확인 (00:10 실행) + → 계약 만료 점검 결과 확인 (01:00 실행) + → PM 자동 생성 결과 확인 (06:00 실행) + +□ 3. 신규 SR 확인 + → ITSM 로그인 → 대시보드 → 미배정 SR 확인 + → P1/P2 긴급 건 우선 배정 + +□ 4. SSL 만료 임박 도메인 확인 + → ITSM → SSL 관리 → CRITICAL/URGENT 상태 도메인 갱신 조치 + +□ 5. 오늘 PM 일정 확인 + → ITSM → 정기점검 → 오늘 예정 작업 확인 + +□ 6. 라이선스 상태 확인 + → ITSM → 사이드바 🔏 라이선스 관리 → 상태 배너 확인 + → 노랑 배너(만료 30일 이내): 갱신 준비 시작 + → 빨강 배너(만료): 즉시 갱신 키 적용 +``` + +### 2.2 매주 점검 체크리스트 + +``` +시간: 매주 월요일 10:00 + +□ DB 파일 크기 확인 (100MB 이상 시 정리 검토) +□ 로그 파일 로테이션 확인 +□ 업로드 파일 디렉토리 크기 확인 +□ 미해결 SR 현황 검토 (7일 이상 미처리 건) +□ 만족도 낮은 SR 원인 분석 +□ 이번 주 당직 배정 확인 및 알림 +□ 만료 예정 SSL 인증서 30일 이내 갱신 계획 수립 +``` + +### 2.3 매월 점검 체크리스트 + +``` +시간: 매월 1일 11:00 + +□ 월간 SR 통계 보고서 작성 +□ PM 점검 완료율 확인 및 미완료 건 추적 +□ 보안 패치 현황 확인 +□ 사용자 계정 감사 (퇴직자 계정 비활성화 확인) +□ DB 백업 파일 무결성 확인 +□ 장애 발생 현황 분석 및 재발 방지 대책 검토 +``` + +--- + +## 3. 사용자 계정 관리 + +### 3.1 계정 역할(Role) 정의 + +| 역할 | 권한 | 사용 대상 | +|------|------|-----------| +| ADMIN | 전체 관리 | 시스템 관리자 | +| PM | SR 배정, 결재, PM 관리 | 프로젝트/서비스 매니저 | +| ENGINEER | SR 처리, 작업 등록 | IT 엔지니어 | +| CUSTOMER | 자신의 SR 조회/생성 | 고객/사용자 | + +### 3.2 신규 사용자 등록 + +**방법 1: API 직접 등록 (ADMIN)** +``` +POST /auth/register +{ + "username": "user001", + "password": "초기비밀번호!123", + "full_name": "홍길동", + "email": "hong@example.com", + "role": "ENGINEER" +} +``` + +**방법 2: seed.py 수정 (초기 구성 시)** +```python +# core/seed.py — _seed_users() 함수에 추가 +{"username": "new_user", "password": "초기PW!", "role": Role.ENGINEER} +``` + +### 3.3 비밀번호 정책 + +``` +최소 길이: 8자 +필수 조합: 영문 + 숫자 + 특수문자 +금지 비밀번호: admin1234, password, 12345678 등 +변경 주기: 90일마다 강제 변경 (구현 예정) +초기 비밀번호: 등록 후 첫 로그인 시 변경 강제 +``` + +### 3.4 계정 잠금 및 비활성화 + +``` +# 퇴직자/이탈자 계정 비활성화 +PATCH /auth/users/{id} +{"is_active": false} + +# 계정 삭제 (데이터 보존 위해 비활성화 권장, 삭제 비권장) +DELETE /auth/users/{id} +``` + +--- + +## 4. SR(서비스요청) 관리 + +### 4.1 SR 유형 및 처리 기한 + +| SR 유형 | 설명 | 목표 처리 시간 | +|---------|------|----------------| +| INCIDENT | 긴급 장애, 서비스 중단 | P1: 1시간, P2: 4시간 | +| SERVICE | 일반 서비스 요청 | 2 영업일 | +| CHANGE | 변경 요청 (결재 필요) | 5 영업일 | +| LOG | 작업 기록, 배치 결과 | 정보성 (SLA 없음) | + +### 4.2 SR 상태 흐름 + +``` +OPEN → IN_PROGRESS → RESOLVED → CLOSED + ↓ ↑ +REJECTED ← (결재 거부) REOPENED (재오픈) +``` + +### 4.3 담당자 배정 방법 + +``` +자동 배정: + PATCH /assign/{sr_id} + {"assignee_id": null} → 시스템이 자동 배정 + +수동 배정: + PATCH /assign/{sr_id} + {"assignee_id": 5} → 특정 엔지니어(ID:5)에게 배정 +``` + +### 4.4 SLA 위반 모니터링 + +``` +SLA 위반 기준: + P1 INCIDENT: 1시간 이내 미처리 + P2 INCIDENT: 4시간 이내 미처리 + +점검 방법: + GET /dashboard/sla-violations + +Messenger 알림: 위반 30분 전 자동 알림 (스케줄러) +``` + +--- + +## 5. SSL 인증서 관리 + +### 5.1 SSL 도메인 등록 + +``` +새 도메인 등록: + POST /ssl-manager/domains + { + "domain": "www.example.com", + "port": 443, + "server_id": 1, + "memo": "메인 홈페이지" + } +``` + +### 5.2 자동 점검 일정 + +``` +매일 00:10 — 모든 등록 도메인 SSL 점검 자동 실행 +점검 방법: 관리 서버의 ssl_expiry_check.sh 스크립트 실행 + +알림 발송 조건: + EXPIRED (만료됨) → 즉시 Critical 알림 + URGENT (7일 이내) → 매일 알림 + WARN (30일 이내) → 최초 발견 시 알림 + OK (30일 초과) → 알림 없음 +``` + +### 5.3 SSL 갱신 처리 절차 + +``` +1단계: 만료 임박 도메인 확인 + GET /ssl-manager/domains?level=CRITICAL + GET /ssl-manager/domains?level=URGENT + +2단계: 인증서 갱신 (외부 CA에서 수행) + - Let's Encrypt: certbot renew + - 상용 CA: CA 포털에서 갱신 신청 + +3단계: 서버에 인증서 적용 + - Nginx/Apache 인증서 파일 교체 + - 웹서버 재시작 + +4단계: GUARDiA ITSM에 갱신 완료 기록 + POST /ssl-manager/domains/{id}/renew + { + "renewed_at": "2026-05-25T10:00:00", + "new_expiry": "2027-05-25T10:00:00", + "memo": "Let's Encrypt 자동 갱신" + } + +5단계: 즉시 재점검으로 확인 + POST /ssl-manager/domains/{id}/check +``` + +### 5.4 SSL 스크립트 배포 + +서버에 점검 스크립트를 배포해야 SSH 점검이 가능합니다: + +```bash +# 관리 대상 서버에 스크립트 배포 +scp scripts/sm/ssl/ssl_expiry_check.sh user@서버IP:/opt/guardia/scripts/ssl/ +ssh user@서버IP "chmod +x /opt/guardia/scripts/ssl/ssl_expiry_check.sh" + +# 테스트 +ssh user@서버IP "bash /opt/guardia/scripts/ssl/ssl_expiry_check.sh www.example.com" +``` + +--- + +## 6. 정기점검(PM) 관리 + +### 6.1 PM 스케줄 설정 + +``` +PM 주기 옵션: + WEEKLY — 매주 + BIWEEKLY — 격주 + MONTHLY — 매월 + QUARTERLY — 분기별 (3개월) + SEMIANNUAL — 반기별 (6개월) + ANNUAL — 연간 + CUSTOM — 커스텀 cron 식 + +스케줄 등록: + POST /pm/schedules/ + { + "name": "운영 서버 월간 점검", + "server_id": 1, + "template_ids": [1, 2, 3], + "frequency": "MONTHLY", + "day_of_month": 15, + "assigned_to": 5 + } +``` + +### 6.2 PM 작업 자동 생성 + +``` +매일 06:00 — 스케줄러가 당일 PM 작업 자동 생성 +자동 생성된 작업: GET /pm/works/ + +수동 생성: + POST /pm/works/ + { + "schedule_id": 1, + "planned_date": "2026-06-01", + "notes": "6월 월간 점검" + } +``` + +### 6.3 PM 결과 입력 + +``` +각 체크리스트 항목 결과 입력: + PATCH /pm/works/{work_id}/results/{result_id} + { + "result": "PASS", // PASS / FAIL / WARNING / NA + "value": "85%", // 측정값 + "note": "" // FAIL인 경우 조치 내역 필수 + } + +PM 완료 처리: + PATCH /pm/works/{work_id}/complete +``` + +### 6.4 PM 보고서 출력 + +``` +Excel 보고서 다운로드: + GET /pm/works/{work_id}/report + → Excel 파일 다운로드 (PASS: 녹색, FAIL: 빨간색, WARNING: 노란색) +``` + +--- + +## 7. 장애 관리 + +### 7.1 장애 등급 정의 + +| 등급 | 영향 | 대응 시간 | 예시 | +|------|------|-----------|------| +| P1 | 전체 서비스 중단 | 1시간 | 운영 DB Down | +| P2 | 주요 기능 장애 | 4시간 | 로그인 불가 | +| P3 | 부분 기능 장애 | 8시간 | 특정 기능 오류 | +| P4 | 경미한 장애 | 2영업일 | UI 오류 | + +### 7.2 P1/P2 장애 대응 절차 + +``` +즉시 조치 (15분 이내): +1. ITSM에 장애 등록 (POST /incidents/) + → Messenger 긴급 알림 자동 발송 +2. 온콜 담당자에게 연락 +3. 장애 타임라인 기록 시작 + +초기 조사 (30분 이내): +4. PATCH /incidents/{id}/status — INVESTIGATING +5. 원인 파악 시작 +6. 임시 우회 방안 검토 + +조치 및 복구: +7. PATCH /incidents/{id}/status — MITIGATED (임시 조치 완료) +8. 근본 원인 해결 +9. PATCH /incidents/{id}/status — RESOLVED + +사후 처리: +10. RCA(근본원인분석) 작성 +11. POST /incidents/{id}/close (RCA 포함 필수) +12. 재발 방지 대책 SR 등록 +``` + +### 7.3 장애 타임라인 기록 + +``` +장애 진행 중 모든 주요 활동 기록: + POST /incidents/{id}/timeline + { + "event_type": "INVESTIGATION", + "description": "DB 서버 CPU 100% 확인, 쿼리 분석 중" + } + +event_type 옵션: + DETECTION — 최초 감지 + INVESTIGATION — 조사 활동 + ACTION — 조치 수행 + UPDATE — 현황 업데이트 + RECOVERY — 복구 완료 + POSTMORTEM — 사후 분석 +``` + +--- + +## 8. 온콜/당직 관리 + +### 8.1 당직 배정 + +``` +단건 배정: + POST /oncall/duties + { + "duty_date": "2026-06-01", + "shift": "ALL_DAY", // ALL_DAY / DAYTIME / NIGHTTIME + "user_id": 5, + "note": "야간 비상 연락" + } + +월간 일괄 배정: + POST /oncall/duties/bulk + { + "duties": [ + {"duty_date": "2026-06-01", "shift": "ALL_DAY", "user_id": 5}, + {"duty_date": "2026-06-02", "shift": "ALL_DAY", "user_id": 6} + ], + "overwrite": false // true: 중복 시 덮어쓰기 + } +``` + +### 8.2 오늘 당직 확인 + +``` +GET /oncall/today +→ 오늘 날짜의 당직자 목록 (교대별) + +응답 예시: +{ + "date": "2026-06-01", + "duties": { + "ALL_DAY": [{"user": "홍길동", "phone": "010-XXXX-XXXX"}], + "NIGHTTIME": [{"user": "김철수", "phone": "010-XXXX-XXXX"}] + } +} +``` + +### 8.3 월간 당직 조회 + +``` +GET /oncall/calendar?year=2026&month=6 +→ 해당 월 전체 당직 일정 +``` + +--- + +## 9. 배치 작업 관리 + +### 9.1 배치 작업 등록 + +``` +POST /batch/jobs +{ + "name": "일일 DB 백업", + "server_id": 1, + "command": "bash /opt/scripts/backup.sh", + "cron_expr": "0 2 * * *", // 매일 오전 2시 + "timeout_sec": 3600, + "alert_on_fail": true, + "is_active": true +} +``` + +> **주의**: 다음 명령어 패턴은 등록 자체가 거부됩니다: +> `rm -rf /`, `shutdown`, `reboot`, `mkfs`, `dd if=`, Fork Bomb 등 + +### 9.2 배치 작업 모니터링 + +``` +최근 실행 이력: + GET /batch/jobs/{id}/runs?limit=20 + +실행 상태: + RUNNING — 실행 중 + SUCCESS — 성공 + FAILED — 실패 (alert_on_fail=true 시 SR 자동 생성) + TIMEOUT — 타임아웃 초과 + +실패 시 자동 SR: GET /tasks/?sr_type=LOG&priority=HIGH +``` + +### 9.3 수동 실행 + +``` +POST /batch/jobs/{id}/run +→ 202 Accepted (백그라운드 실행 시작) + +실행 결과 확인: + GET /batch/jobs/{id}/runs?limit=1 (최신 1건) +``` + +--- + +## 10. 백업 및 복구 + +### 10.1 DB 백업 절차 + +```bash +# Linux — 수동 백업 +cp /opt/guardia/itsm/guardia_itsm.db /backup/guardia_itsm_$(date +%Y%m%d).db +cp /opt/guardia/messenger/guardia_messenger.db /backup/guardia_messenger_$(date +%Y%m%d).db + +# 자동 백업 스크립트 (cron 등록 권장) +# crontab -e +0 3 * * * /opt/guardia/scripts/backup.sh >> /var/log/guardia/backup.log 2>&1 + +# Windows — PowerShell +$date = Get-Date -Format "yyyyMMdd" +Copy-Item "C:\GUARDiA\itsm\guardia_itsm.db" "D:\Backup\guardia_itsm_$date.db" +``` + +### 10.2 업로드 파일 백업 + +```bash +# 업로드 파일 디렉토리 전체 백업 +tar -czf /backup/uploads_$(date +%Y%m%d).tar.gz /opt/guardia/itsm/uploads/ +``` + +### 10.3 복구 절차 + +```bash +# 1. 서비스 중지 +systemctl stop guardia-itsm +systemctl stop guardia-messenger + +# 2. 현재 DB 보존 +mv guardia_itsm.db guardia_itsm.db.broken + +# 3. 백업 복원 +cp /backup/guardia_itsm_20260525.db guardia_itsm.db + +# 4. 서비스 재시작 +systemctl start guardia-itsm +systemctl start guardia-messenger + +# 5. 서비스 상태 확인 +curl http://localhost:8000/ +``` + +### 10.4 백업 보관 정책 + +``` +일간 백업: 30일 보관 +주간 백업: 12주 보관 +월간 백업: 12개월 보관 +``` + +--- + +## 11. 모니터링 및 알림 + +### 11.1 시스템 모니터링 지점 + +| 모니터링 항목 | 임계값 | 조치 | +|--------------|--------|------| +| ITSM 서비스 응답 | 5초 이상 → 경고 | 서비스 재시작 | +| DB 파일 크기 | 500MB 이상 → 경고 | 정리/이관 검토 | +| 디스크 사용률 | 80% 이상 → 경고 | 파일 정리 | +| 업로드 디렉토리 | 10GB 이상 → 경고 | 오래된 파일 정리 | +| 스케줄러 실행 | 오전 06:30까지 미실행 → 경고 | 재시작 | + +### 11.2 Messenger 알림 채널 + +``` +장애 알림: #guardia-incidents +SSL 만료 알림: #guardia-ssl +PM 일정 알림: #guardia-pm +배치 실패: #guardia-ops +``` + +### 11.3 서비스 상태 확인 + +```bash +# Linux +systemctl status guardia-itsm +systemctl status guardia-messenger + +# 로그 실시간 확인 +journalctl -u guardia-itsm -f + +# Windows +Get-Service -Name "GUARDiA-ITSM" +``` + +--- + +## 12. 보안 운영 지침 + +### 12.1 계정 보안 + +``` +□ 기본 admin 비밀번호 즉시 변경 (초기 설치 후) +□ 퇴직자 계정 당일 비활성화 +□ 공용 계정 사용 금지 (1인 1계정 원칙) +□ 분기마다 불필요 계정 감사 +□ CUSTOMER 역할 계정은 자신의 SR만 조회 가능 확인 +``` + +### 12.2 API 접근 제어 + +``` +□ 운영 환경에서 /docs 페이지 비활성화 권장 + main.py: app = FastAPI(docs_url=None, redoc_url=None) + +□ CORS 설정 확인 (운영 환경) + allow_origins: 실제 도메인만 허가 (localhost 제거) + +□ 로그인 실패 횟수 모니터링 + GET /audit/logs?action=LOGIN_FAIL +``` + +### 12.3 서버 자격증명 관리 + +``` +□ SSH 비밀번호는 AES-256-GCM 암호화 저장 확인 +□ 평문 비밀번호 어디에도 저장 금지 +□ ENCRYPTION_KEY는 .env에만 저장, Git 제외 +□ API 응답에 IP/PW 포함 여부 정기 검사 +□ root SSH 직접 접속 금지 설정 확인 +``` + +### 12.4 감사 로그 관리 + +``` +감사 로그 확인: + GET /audit/logs?start=2026-05-01&end=2026-05-31 + +무결성 검증: + GET /audit/verify + → "integrity": true 확인 + +의심 활동 모니터링: + - 새벽 시간대 로그인 + - 다수 SR 한 번에 변경 + - 서버 자격증명 접근 +``` + +--- + +## 13. 장애 대응 절차 + +### 13.1 ITSM 서비스 응답 없음 + +``` +증상: curl http://localhost:8000/ 타임아웃 + +1단계 — 프로세스 확인 + Linux: ps aux | grep uvicorn + Windows: Get-Process -Name python + +2단계 — 로그 확인 + Linux: journalctl -u guardia-itsm --since "10 min ago" + Linux: cat /var/log/guardia/itsm.log | tail -100 + +3단계 — 서비스 재시작 + Linux: systemctl restart guardia-itsm + Windows: Restart-Service "GUARDiA-ITSM" + +4단계 — 재시작 후 확인 + curl http://localhost:8000/ + → 200 응답 확인 +``` + +### 13.2 DB 잠금(Lock) 오류 + +``` +증상: "database is locked" 에러 로그 발생 + +1단계 — 연결 프로세스 확인 + Linux: fuser guardia_itsm.db + +2단계 — 임시 잠금 파일 삭제 (서비스 중지 후) + rm guardia_itsm.db-shm guardia_itsm.db-wal + +3단계 — 서비스 재시작 + systemctl restart guardia-itsm +``` + +### 13.3 스케줄러 미동작 + +``` +증상: SSL 점검, PM 자동 생성이 실행되지 않음 + +1단계 — 로그 확인 + grep "scheduler" /var/log/guardia/itsm.log + +2단계 — APScheduler 설치 확인 + pip show apscheduler + +3단계 — 서비스 재시작 + systemctl restart guardia-itsm + +4단계 — 수동 실행으로 대체 + ITSM → SSL 관리 → "전체 점검" 버튼 + ITSM → 정기점검 → 수동 작업 생성 +``` + +### 13.4 Messenger 연결 실패 + +``` +증상: ITSM 알림이 Messenger에 미전달 + +1단계 — Messenger 서비스 상태 확인 + systemctl status guardia-messenger + +2단계 — 연결 설정 확인 + .env 파일의 MESSENGER_BASE_URL, MESSENGER_BOT_TOKEN 확인 + +3단계 — 연결 테스트 + curl http://localhost:8001/api/health + +4단계 — Messenger 재시작 + systemctl restart guardia-messenger + +주의: Messenger 알림 실패는 ITSM 기능에 영향 없음 (알림만 안 됨) +``` + +--- + +## 14. 로그 관리 + +### 14.1 로그 파일 위치 + +``` +Linux: + /var/log/guardia/itsm.log — ITSM 애플리케이션 로그 + /var/log/guardia/scheduler.log — 스케줄러 실행 로그 + /var/log/guardia/messenger.log — Messenger 로그 + /var/log/guardia/backup.log — 백업 실행 로그 + +Windows: + C:\GUARDiA\logs\itsm.log + C:\GUARDiA\logs\scheduler.log + C:\GUARDiA\logs\messenger.log +``` + +### 14.2 로그 로테이션 설정 (Linux) + +``` +# /etc/logrotate.d/guardia +/var/log/guardia/*.log { + daily + missingok + rotate 30 + compress + delaycompress + notifempty + create 640 guardia guardia + postrotate + systemctl reload guardia-itsm + endscript +} +``` + +### 14.3 감사 로그 (DB 내 저장) + +``` +ITSM 내 모든 주요 활동은 tb_audit_log 테이블에 저장됩니다. +해시 체인으로 무결성 보호. + +조회: + GET /audit/logs?limit=100 + GET /audit/logs?user_id=5&start=2026-05-01 + +무결성 검증: + GET /audit/verify +``` + +--- + +## 15. 주요 설정 파일 + +### 15.1 .env 운영 설정 + +```ini +# 운영 환경 .env +SECRET_KEY=<64자 이상 랜덤 문자열> +ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=480 +DB_URL=sqlite+aiosqlite:///./guardia_itsm.db +ENCRYPTION_KEY=<64자리 hex 문자열> + +MESSENGER_BASE_URL=http://localhost:8001 +MESSENGER_BOT_TOKEN=<봇 토큰> + +SSH_TIMEOUT=30 +UPLOAD_ROOT=./uploads +MAX_FILE_SIZE_MB=10 + +# 운영: API 문서 비활성화 +DOCS_DISABLED=true +``` + +### 15.2 서비스 파일 (Linux systemd) + +```ini +# /etc/systemd/system/guardia-itsm.service +[Unit] +Description=GUARDiA ITSM Service +After=network.target + +[Service] +User=guardia +WorkingDirectory=/opt/guardia/itsm +EnvironmentFile=/opt/guardia/itsm/.env +ExecStart=/opt/guardia/.venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000 --workers 2 +Restart=always +RestartSec=5 +StandardOutput=append:/var/log/guardia/itsm.log +StandardError=append:/var/log/guardia/itsm.log + +[Install] +WantedBy=multi-user.target +``` + +### 15.3 방화벽 설정 + +```bash +# Linux (firewalld) +firewall-cmd --permanent --add-port=8000/tcp # ITSM +firewall-cmd --permanent --add-port=8001/tcp # Messenger +firewall-cmd --reload + +# Windows (PowerShell) +New-NetFirewallRule -DisplayName "GUARDiA ITSM" -Direction Inbound -Protocol TCP -LocalPort 8000 -Action Allow +New-NetFirewallRule -DisplayName "GUARDiA Messenger" -Direction Inbound -Protocol TCP -LocalPort 8001 -Action Allow +``` + +--- + +## 부록 A: 운영 명령어 빠른 참조 + +```bash +# 서비스 시작/중지/재시작 +systemctl start|stop|restart guardia-itsm +systemctl start|stop|restart guardia-messenger + +# 상태 확인 +systemctl status guardia-itsm +curl http://localhost:8000/ + +# 로그 확인 +journalctl -u guardia-itsm -f --since "1 hour ago" + +# DB 백업 +cp guardia_itsm.db guardia_itsm_$(date +%Y%m%d%H%M).db + +# 업로드 용량 확인 +du -sh uploads/ + +# 활성 연결 확인 +ss -tlnp | grep :8000 +``` + +## 부록 B: 비상 연락망 + +``` +ITSM 운영팀: ... +보안팀: ... +인프라팀: ... +``` + +--- + +--- + +## 16. 라이선스 관리 + +### 16.1 라이선스 등록 절차 + +1. ITSM 관리자 계정으로 로그인 +2. 사이드바 하단 **🔏 라이선스 관리** 클릭 (`/license` 페이지) +3. **라이선스 키 등록/갱신** 섹션에 `GRD-...` 형식의 키 붙여넣기 +4. **라이선스 등록** 버튼 클릭 +5. 상단 배너에서 에디션·만료일 확인 + +**API로 등록 (자동화)**: +```bash +curl -X POST http://localhost:8001/api/license/activate \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"license_key": "GRD-..."}' +``` + +### 16.2 라이선스 상태 배너 의미 + +| 배너 색상 | 상태 | 조치 | +|---------|------|------| +| ✅ 초록 | 정상 활성 | — | +| ⚠️ 노랑 | 만료 30일 이내 경고 | 갱신 라이선스 요청 | +| ❌ 빨강 | 만료됨 | 즉시 갱신 키 적용 필요 | +| ⬜ 회색 | 라이선스 미등록 | Community 제한 모드로 동작 | + +### 16.3 에디션별 자원 제한 + +| 에디션 | 기관 수 | 사용자 수 | 서버 수 | 주요 기능 | +|--------|--------|---------|--------|---------| +| COMMUNITY | 1 | 10 | 20 | MFA | +| STANDARD | 50 | 200 | 500 | MFA, LDAP, PAM, AI 에이전트 | +| ENTERPRISE | 무제한 | 무제한 | 무제한 | 전체 기능 | + +> **주의**: 한도 초과 시 신규 등록 API가 HTTP 403을 반환한다. +> 기존 등록 데이터는 만료·초과 후에도 삭제되지 않는다. + +### 16.4 라이선스 갱신 + +갱신 라이선스 키를 받은 후 16.1과 동일하게 등록하면 기존 라이선스가 자동으로 비활성화되고 새 라이선스로 교체된다. + +자세한 발급·갱신 절차는 **`manual/14_라이선스_키_발급_가이드.md`** 참조. + +--- + +*본 운영자 지침서는 GUARDiA ITSM v1.0 기준으로 작성되었습니다.* diff --git a/05_설치가이드_리눅스.md b/05_설치가이드_리눅스.md new file mode 100644 index 0000000..edc06d6 --- /dev/null +++ b/05_설치가이드_리눅스.md @@ -0,0 +1,837 @@ +# GUARDiA ITSM + Messenger — 설치 가이드 (Linux) + +**문서 버전**: 1.0 +**작성일**: 2026-05-25 +**지원 OS**: Ubuntu 22.04 LTS, Ubuntu 20.04 LTS, CentOS 8+, RHEL 8+ + +--- + +## 목차 + +1. [설치 전 요구사항 확인](#1-설치-전-요구사항-확인) +2. [시스템 사전 준비](#2-시스템-사전-준비) +3. [Python 환경 설정](#3-python-환경-설정) +4. [GUARDiA ITSM 설치](#4-guardia-itsm-설치) +5. [GUARDiA Messenger 설치](#5-guardia-messenger-설치) +6. [서비스 등록 (systemd)](#6-서비스-등록-systemd) +7. [SSL 점검 스크립트 배포](#7-ssl-점검-스크립트-배포) +8. [방화벽 설정](#8-방화벽-설정) +9. [Nginx 리버스 프록시 설정 (선택)](#9-nginx-리버스-프록시-설정-선택) +10. [설치 검증](#10-설치-검증) +11. [보안 강화 설정](#11-보안-강화-설정) +12. [문제 해결](#12-문제-해결) + +--- + +## 1. 설치 전 요구사항 확인 + +### 1.1 하드웨어 최소 요구사항 + +| 항목 | 최소 | 권장 | +|------|------|------| +| CPU | 2코어 | 4코어 이상 | +| RAM | 2GB | 4GB 이상 | +| 디스크 | 20GB | 50GB 이상 | +| 네트워크 | 100Mbps | 1Gbps | + +### 1.2 소프트웨어 요구사항 + +``` +OS: Ubuntu 22.04 LTS (권장) +Python: 3.11 이상 +openssl: 1.1.1 이상 (SSL 점검에 필요) +sqlite3: 3.35 이상 +``` + +### 1.3 요구사항 사전 확인 + +```bash +# OS 버전 확인 +lsb_release -a +uname -r + +# Python 버전 확인 +python3 --version +# → Python 3.11.x 이상이어야 함 + +# openssl 버전 확인 +openssl version +# → OpenSSL 1.1.1 이상 + +# SQLite 버전 확인 +sqlite3 --version +# → 3.35.0 이상 + +# 디스크 여유 공간 확인 +df -h / +# → Available 20GB 이상 확인 + +# 포트 사용 여부 확인 (8000, 8001이 비어 있어야 함) +ss -tlnp | grep -E ':8000|:8001' +# → 아무것도 출력되지 않아야 함 +``` + +--- + +## 2. 시스템 사전 준비 + +### 2.1 시스템 업데이트 (Ubuntu) + +```bash +sudo apt update && sudo apt upgrade -y +sudo apt install -y git curl wget vim unzip +``` + +### 2.2 시스템 업데이트 (CentOS/RHEL) + +```bash +sudo dnf update -y +sudo dnf install -y git curl wget vim unzip +``` + +### 2.3 전용 사용자 생성 + +보안을 위해 root가 아닌 전용 사용자로 실행합니다. + +```bash +# guardia 사용자 생성 (로그인 불가 설정) +sudo useradd -r -m -d /opt/guardia -s /bin/bash guardia +sudo passwd guardia # 비밀번호 설정 (관리자만 알아야 함) + +# 사용자 확인 +id guardia +# → uid=998(guardia) gid=998(guardia) groups=998(guardia) +``` + +### 2.4 디렉토리 구조 생성 + +```bash +sudo mkdir -p /opt/guardia/{itsm,messenger} +sudo mkdir -p /opt/guardia/scripts/ssl +sudo mkdir -p /var/log/guardia +sudo mkdir -p /backup/guardia + +sudo chown -R guardia:guardia /opt/guardia +sudo chown -R guardia:guardia /var/log/guardia +sudo chown -R guardia:guardia /backup/guardia + +# 확인 +ls -la /opt/guardia/ +``` + +--- + +## 3. Python 환경 설정 + +### 3.1 Python 3.11 설치 (Ubuntu 22.04) + +Ubuntu 22.04는 기본적으로 Python 3.10이 설치됩니다. 3.11을 설치합니다. + +```bash +# deadsnakes PPA 추가 +sudo add-apt-repository ppa:deadsnakes/ppa -y +sudo apt update + +# Python 3.11 설치 +sudo apt install -y python3.11 python3.11-venv python3.11-dev + +# 버전 확인 +python3.11 --version +# → Python 3.11.x +``` + +### 3.2 Python 3.11 설치 (CentOS/RHEL 8) + +```bash +# EPEL 및 개발 도구 설치 +sudo dnf install -y epel-release +sudo dnf install -y python3.11 python3.11-pip + +python3.11 --version +``` + +### 3.3 pip 업그레이드 + +```bash +python3.11 -m pip install --upgrade pip +``` + +--- + +## 4. GUARDiA ITSM 설치 + +### 4.1 소스 코드 배포 + +```bash +# guardia 사용자로 전환 +sudo -u guardia bash + +# 소스 코드 복사 (파일 서버에서 복사 또는 git clone) +# 방법 1: scp로 복사 +# scp -r itsm/ guardia@서버IP:/opt/guardia/itsm/ + +# 방법 2: git clone (내부 Git 서버) +cd /opt/guardia +git clone http://내부git서버/guardia/itsm.git itsm + +# 방법 3: 직접 파일 복사 (zip 사용) +# unzip guardia_itsm_v1.0.zip -d /opt/guardia/itsm/ + +# 설치 확인 +ls /opt/guardia/itsm/ +# → main.py database.py models.py schemas.py routers/ core/ utils/ static/ 등 +``` + +### 4.2 가상환경 생성 및 의존성 설치 + +```bash +# /opt/guardia/itsm 디렉토리에서 +cd /opt/guardia/itsm + +# 가상환경 생성 (공유 위치) +python3.11 -m venv /opt/guardia/.venv + +# 가상환경 활성화 +source /opt/guardia/.venv/bin/activate + +# pip 업그레이드 +pip install --upgrade pip + +# 의존성 설치 +pip install -r requirements.txt + +# 설치 확인 +pip list | grep -E "fastapi|uvicorn|sqlalchemy|apscheduler" +# → 패키지 목록 출력 확인 + +# 가상환경 비활성화 +deactivate +``` + +**requirements.txt 주요 패키지**: +``` +fastapi>=0.110 +uvicorn[standard]>=0.29 +sqlalchemy>=2.0 +aiosqlite>=0.20 +python-jose[cryptography]>=3.3 +passlib[bcrypt]>=1.7 +python-multipart>=0.0.9 +apscheduler>=3.10 +asyncssh>=2.14 +openpyxl>=3.1 +httpx>=0.27 +cryptography>=42.0 +croniter>=2.0 +``` + +### 4.3 환경 변수 설정 + +```bash +# .env 파일 생성 +cat > /opt/guardia/itsm/.env << 'EOF' +SECRET_KEY=여기에_64자이상의_랜덤문자열_입력_반드시_변경하세요 +ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=480 +DB_URL=sqlite+aiosqlite:////opt/guardia/itsm/guardia_itsm.db +ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000 +# 라이선스 마스터 키 (64자리 hex = 32바이트) +# 생성: python3 -c "import secrets; print(secrets.token_hex(32))" +# ⚠️ 분실 시 기발급 라이선스 전부 무효화 — 안전하게 별도 보관 필수 +GUARDIA_LICENSE_KEY=0000000000000000000000000000000000000000000000000000000000000000 +MESSENGER_BASE_URL=http://localhost:8001 +MESSENGER_BOT_TOKEN=변경하세요 +SSH_TIMEOUT=30 +UPLOAD_ROOT=/opt/guardia/itsm/uploads +MAX_FILE_SIZE_MB=10 +EOF + +# 권한 설정 (소유자만 읽기/쓰기) +chmod 600 /opt/guardia/itsm/.env +chown guardia:guardia /opt/guardia/itsm/.env +``` + +> **중요**: `SECRET_KEY`, `ENCRYPTION_KEY`, `GUARDIA_LICENSE_KEY`는 반드시 강력한 랜덤 값으로 변경하세요! + +**랜덤 키 생성 방법**: +```bash +# SECRET_KEY 생성 (64자) +python3 -c "import secrets; print(secrets.token_hex(32))" + +# ENCRYPTION_KEY 생성 (64자 hex = AES-256) +python3 -c "import secrets; print(secrets.token_hex(32))" +``` + +### 4.4 업로드 디렉토리 생성 + +```bash +mkdir -p /opt/guardia/itsm/uploads/sr_files +mkdir -p /opt/guardia/itsm/uploads/workspaces +chown -R guardia:guardia /opt/guardia/itsm/uploads/ +``` + +### 4.5 초기 실행 및 DB 초기화 + +```bash +cd /opt/guardia/itsm +source /opt/guardia/.venv/bin/activate + +# 테스트 실행 (초기 DB 생성 및 시드 데이터 삽입) +python -m uvicorn main:app --host 127.0.0.1 --port 8000 & +sleep 5 + +# 정상 동작 확인 +curl http://127.0.0.1:8000/ +# → {"detail":"Not Found"} 또는 HTML 응답이면 정상 + +# 로그인 테스트 +curl -X POST http://127.0.0.1:8000/auth/login \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"admin1234!"}' +# → {"access_token":"...","token_type":"bearer"} 출력 확인 + +# 프로세스 종료 (systemd 서비스로 실행 예정) +kill %1 +deactivate +``` + +--- + +## 5. GUARDiA Messenger 설치 + +### 5.1 소스 코드 배포 + +```bash +sudo -u guardia bash +cd /opt/guardia +git clone http://내부git서버/guardia/messenger.git messenger +# 또는 파일 복사 +``` + +### 5.2 의존성 설치 + +```bash +cd /opt/guardia/messenger +source /opt/guardia/.venv/bin/activate +pip install -r requirements.txt +deactivate +``` + +### 5.3 환경 변수 설정 + +```bash +cat > /opt/guardia/messenger/.env << 'EOF' +SECRET_KEY=ITSM과_동일한_키_또는_별도_키 +ALGORITHM=HS256 +DB_URL=sqlite+aiosqlite:////opt/guardia/messenger/guardia_messenger.db +ITSM_BASE_URL=http://localhost:8000 +PORT=8001 +EOF + +chmod 600 /opt/guardia/messenger/.env +chown guardia:guardia /opt/guardia/messenger/.env +``` + +--- + +## 6. 서비스 등록 (systemd) + +### 6.1 ITSM 서비스 파일 생성 + +```bash +sudo tee /etc/systemd/system/guardia-itsm.service > /dev/null << 'EOF' +[Unit] +Description=GUARDiA ITSM Service +Documentation=https://내부문서URL +After=network.target network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=guardia +Group=guardia +WorkingDirectory=/opt/guardia/itsm +EnvironmentFile=/opt/guardia/itsm/.env +ExecStart=/opt/guardia/.venv/bin/uvicorn main:app \ + --host 0.0.0.0 \ + --port 8000 \ + --workers 2 \ + --log-level info +ExecReload=/bin/kill -HUP $MAINPID +Restart=always +RestartSec=5 +StartLimitInterval=60 +StartLimitBurst=3 + +# 보안 강화 +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ReadWritePaths=/opt/guardia/itsm/uploads /var/log/guardia + +StandardOutput=append:/var/log/guardia/itsm.log +StandardError=append:/var/log/guardia/itsm.log + +[Install] +WantedBy=multi-user.target +EOF +``` + +### 6.2 Messenger 서비스 파일 생성 + +```bash +sudo tee /etc/systemd/system/guardia-messenger.service > /dev/null << 'EOF' +[Unit] +Description=GUARDiA Messenger Service +After=network.target guardia-itsm.service + +[Service] +Type=simple +User=guardia +Group=guardia +WorkingDirectory=/opt/guardia/messenger +EnvironmentFile=/opt/guardia/messenger/.env +ExecStart=/opt/guardia/.venv/bin/uvicorn main:app \ + --host 0.0.0.0 \ + --port 8001 \ + --workers 1 \ + --log-level info +Restart=always +RestartSec=5 + +NoNewPrivileges=true +PrivateTmp=true + +StandardOutput=append:/var/log/guardia/messenger.log +StandardError=append:/var/log/guardia/messenger.log + +[Install] +WantedBy=multi-user.target +EOF +``` + +### 6.3 서비스 등록 및 시작 + +```bash +# systemd 재로드 +sudo systemctl daemon-reload + +# 서비스 활성화 (부팅 시 자동 시작) +sudo systemctl enable guardia-itsm +sudo systemctl enable guardia-messenger + +# 서비스 시작 +sudo systemctl start guardia-itsm +sudo systemctl start guardia-messenger + +# 상태 확인 +sudo systemctl status guardia-itsm +sudo systemctl status guardia-messenger + +# 기대 출력: +# ● guardia-itsm.service - GUARDiA ITSM Service +# Active: active (running) since ... +``` + +### 6.4 로그 확인 + +```bash +# 실시간 로그 확인 +sudo journalctl -u guardia-itsm -f + +# 최근 50줄 +sudo journalctl -u guardia-itsm -n 50 + +# 파일 직접 확인 +tail -f /var/log/guardia/itsm.log +``` + +--- + +## 7. SSL 점검 스크립트 배포 + +GUARDiA ITSM이 관리하는 모든 서버에 SSL 점검 스크립트를 배포합니다. + +```bash +# ITSM 서버에서 관리 대상 서버로 스크립트 복사 +# (ITSM 서버에서 실행) + +TARGET_SERVERS=("192.168.1.10" "192.168.1.11" "192.168.1.12") + +for SERVER in "${TARGET_SERVERS[@]}"; do + echo "배포 중: $SERVER" + ssh guardia@${SERVER} "mkdir -p /opt/guardia/scripts/ssl" + scp /opt/guardia/itsm/scripts/sm/ssl/ssl_expiry_check.sh \ + guardia@${SERVER}:/opt/guardia/scripts/ssl/ + ssh guardia@${SERVER} "chmod +x /opt/guardia/scripts/ssl/ssl_expiry_check.sh" + + # 동작 테스트 + ssh guardia@${SERVER} "bash /opt/guardia/scripts/ssl/ssl_expiry_check.sh google.com" + echo "배포 완료: $SERVER" +done +``` + +--- + +## 8. 방화벽 설정 + +### 8.1 UFW (Ubuntu) + +```bash +# UFW 활성화 (이미 활성화된 경우 skip) +sudo ufw enable + +# GUARDiA 포트 허용 +sudo ufw allow 8000/tcp comment "GUARDiA ITSM" +sudo ufw allow 8001/tcp comment "GUARDiA Messenger" + +# SSH 허용 (원격 접속용, 이미 설정된 경우 skip) +sudo ufw allow 22/tcp + +# 상태 확인 +sudo ufw status verbose +``` + +### 8.2 firewalld (CentOS/RHEL) + +```bash +# GUARDiA 포트 허용 +sudo firewall-cmd --permanent --add-port=8000/tcp +sudo firewall-cmd --permanent --add-port=8001/tcp +sudo firewall-cmd --reload + +# 확인 +sudo firewall-cmd --list-ports +``` + +### 8.3 특정 IP만 허용 (보안 강화) + +```bash +# 내부 네트워크(192.168.1.0/24)에서만 접근 허용 +sudo ufw allow from 192.168.1.0/24 to any port 8000 +sudo ufw allow from 192.168.1.0/24 to any port 8001 +sudo ufw deny 8000 +sudo ufw deny 8001 +``` + +--- + +## 9. Nginx 리버스 프록시 설정 (선택) + +HTTPS를 적용하거나 표준 포트(80/443)를 사용하려면 Nginx를 앞단에 배치합니다. + +### 9.1 Nginx 설치 + +```bash +# Ubuntu +sudo apt install -y nginx + +# CentOS/RHEL +sudo dnf install -y nginx +``` + +### 9.2 Nginx 설정 + +```bash +sudo tee /etc/nginx/conf.d/guardia.conf > /dev/null << 'EOF' +# HTTP → HTTPS 리다이렉트 +server { + listen 80; + server_name itsm.example.com; + return 301 https://$host$request_uri; +} + +# ITSM HTTPS +server { + listen 443 ssl http2; + server_name itsm.example.com; + + ssl_certificate /etc/ssl/certs/itsm.example.com.crt; + ssl_certificate_key /etc/ssl/private/itsm.example.com.key; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + + # 업로드 용량 제한 + client_max_body_size 20M; + + # ITSM API/UI + location / { + proxy_pass http://127.0.0.1:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 300; + proxy_connect_timeout 10; + } +} + +# Messenger HTTPS +server { + listen 443 ssl http2; + server_name messenger.example.com; + + ssl_certificate /etc/ssl/certs/messenger.example.com.crt; + ssl_certificate_key /etc/ssl/private/messenger.example.com.key; + ssl_protocols TLSv1.2 TLSv1.3; + + location / { + proxy_pass http://127.0.0.1:8001; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; # WebSocket 지원 + } +} +EOF + +# 설정 검증 +sudo nginx -t +# → configuration file /etc/nginx/nginx.conf syntax is ok + +# Nginx 재시작 +sudo systemctl restart nginx +sudo systemctl enable nginx +``` + +--- + +## 10. 설치 검증 + +### 10.1 서비스 상태 확인 + +```bash +# 서비스 실행 상태 +sudo systemctl is-active guardia-itsm guardia-messenger +# → active +# → active + +# 포트 리스닝 확인 +ss -tlnp | grep -E ':8000|:8001' +# → 8000, 8001 포트 출력 확인 +``` + +### 10.2 API 기능 테스트 + +```bash +# 1. 홈페이지 응답 확인 +curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/ +# → 200 + +# 2. 로그인 테스트 +curl -s -X POST http://localhost:8000/auth/login \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"admin1234!"}' | python3 -m json.tool +# → {"access_token": "eyJ...", "token_type": "bearer"} 출력 확인 + +# 3. 토큰으로 API 호출 +TOKEN=$(curl -s -X POST http://localhost:8000/auth/login \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"admin1234!"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])") + +curl -s http://localhost:8000/dashboard/summary \ + -H "Authorization: Bearer $TOKEN" | python3 -m json.tool +# → 대시보드 통계 JSON 출력 확인 + +# 4. Messenger 확인 +curl -s http://localhost:8001/ +# → 200 응답 확인 +``` + +### 10.3 스케줄러 동작 확인 + +```bash +# 로그에서 스케줄러 시작 메시지 확인 +grep -i "scheduler" /var/log/guardia/itsm.log | tail -5 +# → "scheduler started" 또는 "APScheduler started" 메시지 확인 +``` + +### 10.4 설치 검증 스크립트 + +```bash +cat > /tmp/verify_install.sh << 'EOF' +#!/bin/bash +echo "=== GUARDiA 설치 검증 ===" + +check() { + if $1; then echo " [ok] $2"; else echo " [FAIL] $2"; fi +} + +# 서비스 상태 +systemctl is-active --quiet guardia-itsm +check "[ $? -eq 0 ]" "ITSM 서비스 실행 중" + +systemctl is-active --quiet guardia-messenger +check "[ $? -eq 0 ]" "Messenger 서비스 실행 중" + +# 포트 확인 +ss -tlnp | grep -q ':8000' +check "[ $? -eq 0 ]" "ITSM 포트 8000 리스닝" + +ss -tlnp | grep -q ':8001' +check "[ $? -eq 0 ]" "Messenger 포트 8001 리스닝" + +# API 응답 +CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/) +check "[ '$CODE' = '200' ]" "ITSM API 응답 (200)" + +# DB 파일 +[ -f /opt/guardia/itsm/guardia_itsm.db ] +check "[ $? -eq 0 ]" "ITSM DB 파일 존재" + +echo "=== 검증 완료 ===" +EOF + +chmod +x /tmp/verify_install.sh +bash /tmp/verify_install.sh +``` + +--- + +## 11. 보안 강화 설정 + +### 11.1 초기 관리자 비밀번호 변경 + +```bash +# 로그인 후 즉시 비밀번호 변경 +TOKEN=$(curl -s -X POST http://localhost:8000/auth/login \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"admin1234!"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])") + +curl -X POST http://localhost:8000/auth/change-password \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"current_password":"admin1234!","new_password":"새로운강력한비밀번호!@#"}' +``` + +### 11.2 API 문서 비활성화 (운영) + +```python +# main.py 수정 +app = FastAPI( + title="GUARDiA ITSM", + version="1.0.0", + lifespan=lifespan, + docs_url=None, # /docs 비활성화 + redoc_url=None, # /redoc 비활성화 +) +``` + +### 11.3 DB 파일 권한 설정 + +```bash +# DB 파일은 guardia 사용자만 접근 가능 +chmod 600 /opt/guardia/itsm/guardia_itsm.db +chmod 600 /opt/guardia/messenger/guardia_messenger.db +``` + +### 11.4 자동 백업 설정 + +```bash +# 백업 스크립트 생성 +cat > /opt/guardia/scripts/backup.sh << 'EOF' +#!/bin/bash +BACKUP_DIR=/backup/guardia +DATE=$(date +%Y%m%d_%H%M) +mkdir -p $BACKUP_DIR + +cp /opt/guardia/itsm/guardia_itsm.db $BACKUP_DIR/itsm_$DATE.db +cp /opt/guardia/messenger/guardia_messenger.db $BACKUP_DIR/messenger_$DATE.db + +# 30일 이전 백업 삭제 +find $BACKUP_DIR -name "*.db" -mtime +30 -delete + +echo "[$DATE] 백업 완료" >> /var/log/guardia/backup.log +EOF + +chmod +x /opt/guardia/scripts/backup.sh + +# cron 등록 (매일 오전 3시) +echo "0 3 * * * guardia /opt/guardia/scripts/backup.sh" | sudo tee -a /etc/cron.d/guardia +``` + +--- + +## 12. 문제 해결 + +### 12.1 서비스 시작 실패 + +```bash +# 상세 에러 확인 +sudo journalctl -u guardia-itsm -n 50 --no-pager + +# 가상환경 경로 확인 +ls -la /opt/guardia/.venv/bin/uvicorn +# → 파일이 없으면: source /opt/guardia/.venv/bin/activate && pip install uvicorn + +# .env 파일 권한 확인 +ls -la /opt/guardia/itsm/.env +# → -rw------- 1 guardia guardia 이어야 함 +``` + +### 12.2 포트 충돌 + +```bash +# 포트를 사용 중인 프로세스 확인 +sudo lsof -i :8000 +sudo lsof -i :8001 + +# 해당 프로세스 종료 후 재시작 +sudo kill -9 +sudo systemctl start guardia-itsm +``` + +### 12.3 Python 패키지 설치 오류 + +```bash +# 개발 헤더 설치 (컴파일이 필요한 패키지) +# Ubuntu +sudo apt install -y python3.11-dev libssl-dev libffi-dev build-essential + +# CentOS/RHEL +sudo dnf install -y python3.11-devel openssl-devel libffi-devel gcc + +# 재설치 +source /opt/guardia/.venv/bin/activate +pip install --upgrade pip +pip install -r /opt/guardia/itsm/requirements.txt +``` + +### 12.4 DB 권한 오류 + +```bash +# DB 파일 소유자 및 권한 확인 +ls -la /opt/guardia/itsm/guardia_itsm.db + +# 수정 +sudo chown guardia:guardia /opt/guardia/itsm/guardia_itsm.db +sudo chmod 660 /opt/guardia/itsm/guardia_itsm.db +``` + +### 12.5 첫 로그인 실패 (seed 데이터 미생성) + +```bash +# 수동으로 DB 초기화 +sudo -u guardia bash -c " + source /opt/guardia/.venv/bin/activate + cd /opt/guardia/itsm + python -c \" +import asyncio +from database import init_db, SessionLocal +from core.seed import seed_all + +async def main(): + await init_db() + async with SessionLocal() as db: + await seed_all(db) + print('DB 초기화 완료') + +asyncio.run(main()) +\" +" +``` + +--- + +*설치 중 문제가 발생하면 `/var/log/guardia/itsm.log`를 확인하거나 개발팀에 문의하세요.* diff --git a/06_설치가이드_윈도우서버.md b/06_설치가이드_윈도우서버.md new file mode 100644 index 0000000..4f073bc --- /dev/null +++ b/06_설치가이드_윈도우서버.md @@ -0,0 +1,873 @@ +# GUARDiA ITSM + Messenger — 설치 가이드 (Windows Server) + +**문서 버전**: 1.0 +**작성일**: 2026-05-25 +**지원 OS**: Windows Server 2019, Windows Server 2022 + +--- + +## 목차 + +1. [설치 전 요구사항 확인](#1-설치-전-요구사항-확인) +2. [사전 소프트웨어 설치](#2-사전-소프트웨어-설치) +3. [GUARDiA ITSM 설치](#3-guardia-itsm-설치) +4. [GUARDiA Messenger 설치](#4-guardia-messenger-설치) +5. [Windows 서비스 등록 (NSSM)](#5-windows-서비스-등록-nssm) +6. [방화벽 설정](#6-방화벽-설정) +7. [IIS 리버스 프록시 설정 (선택)](#7-iis-리버스-프록시-설정-선택) +8. [설치 검증](#8-설치-검증) +9. [보안 강화 설정](#9-보안-강화-설정) +10. [자동 백업 설정](#10-자동-백업-설정) +11. [문제 해결](#11-문제-해결) + +--- + +## 1. 설치 전 요구사항 확인 + +### 1.1 하드웨어 최소 요구사항 + +| 항목 | 최소 | 권장 | +|------|------|------| +| CPU | 2코어 | 4코어 이상 | +| RAM | 4GB | 8GB 이상 | +| 디스크 | 30GB | 60GB 이상 | +| 네트워크 | 100Mbps | 1Gbps | + +### 1.2 소프트웨어 요구사항 + +``` +OS: Windows Server 2022 (권장) 또는 Windows Server 2019 +Python: 3.11 이상 +OpenSSL: Win64 OpenSSL v3.x (SSL 점검 기능 사용 시) +NSSM: 서비스 등록 도구 (선택, Windows Service로 등록 시 필요) +``` + +### 1.3 설치 전 확인 (PowerShell) + +PowerShell을 **관리자 권한**으로 실행하여 확인합니다. + +```powershell +# OS 버전 확인 +[System.Environment]::OSVersion.Version +Get-ComputerInfo | Select-Object WindowsProductName, WindowsVersion + +# PowerShell 버전 확인 +$PSVersionTable.PSVersion +# → Major 5 이상 + +# 디스크 여유 공간 확인 +Get-PSDrive C | Select-Object Used, Free +# → Free 30GB 이상 확인 + +# 포트 사용 여부 확인 +netstat -an | findstr ":8000" +netstat -an | findstr ":8001" +# → 아무것도 출력되지 않아야 함 +``` + +--- + +## 2. 사전 소프트웨어 설치 + +### 2.1 Python 3.11 설치 + +1. 웹브라우저에서 `python.org/downloads` 접속 +2. **Python 3.11.x** (Windows installer 64-bit) 다운로드 +3. 설치 시 반드시 체크: **"Add Python to PATH"** + +```powershell +# 설치 후 확인 +python --version +# → Python 3.11.x + +python -m pip --version +# → pip 24.x from ... +``` + +> 설치 후 PowerShell을 새로 열어야 PATH가 반영됩니다. + +### 2.2 Git 설치 (선택) + +내부 Git 서버에서 코드를 받는 경우 필요합니다. + +1. `git-scm.com/download/win` 에서 설치 파일 다운로드 +2. 기본 설정으로 설치 +3. 확인: `git --version` + +### 2.3 OpenSSL 설치 (SSL 점검 기능 사용 시) + +``` +1. 웹에서 "Win64 OpenSSL" 검색 → slproweb.com 또는 공식 배포처에서 다운로드 + (Win64 OpenSSL v3.x.x Light 권장) +2. 기본 경로 C:\Program Files\OpenSSL-Win64 에 설치 +3. 시스템 환경변수 PATH에 추가: C:\Program Files\OpenSSL-Win64\bin +``` + +```powershell +# 설치 확인 +openssl version +# → OpenSSL 3.x.x +``` + +### 2.4 NSSM 설치 (서비스 등록용) + +NSSM은 Python 앱을 Windows Service로 등록하는 도구입니다. + +``` +1. nssm.cc/download 에서 최신 버전 다운로드 +2. 압축 해제 후 nssm.exe (win64 폴더)를 C:\Windows\System32\ 에 복사 +``` + +```powershell +# 확인 +nssm version +``` + +--- + +## 3. GUARDiA ITSM 설치 + +### 3.1 설치 디렉토리 생성 + +PowerShell을 **관리자 권한**으로 실행합니다. + +```powershell +# 디렉토리 생성 +New-Item -ItemType Directory -Force -Path "C:\GUARDiA\itsm" +New-Item -ItemType Directory -Force -Path "C:\GUARDiA\messenger" +New-Item -ItemType Directory -Force -Path "C:\GUARDiA\logs" +New-Item -ItemType Directory -Force -Path "C:\GUARDiA\backup" +New-Item -ItemType Directory -Force -Path "C:\GUARDiA\scripts\ssl" +New-Item -ItemType Directory -Force -Path "C:\GUARDiA\itsm\uploads\sr_files" +New-Item -ItemType Directory -Force -Path "C:\GUARDiA\itsm\uploads\workspaces" + +Write-Host "디렉토리 생성 완료" +``` + +### 3.2 소스 코드 배포 + +```powershell +# 방법 1: 파일 복사 (zip 파일로 배포 시) +Expand-Archive -Path "C:\temp\guardia_itsm_v1.0.zip" -DestinationPath "C:\GUARDiA\itsm" -Force + +# 방법 2: Git clone (내부 Git 서버) +cd C:\GUARDiA +git clone http://내부git서버/guardia/itsm.git itsm + +# 배포 확인 +Get-ChildItem C:\GUARDiA\itsm\ +# → main.py database.py models.py schemas.py 등 표시 +``` + +### 3.3 가상환경 생성 및 의존성 설치 + +```powershell +# C:\GUARDiA\itsm 으로 이동 +cd C:\GUARDiA\itsm + +# 가상환경 생성 +python -m venv C:\GUARDiA\.venv + +# 가상환경 활성화 +C:\GUARDiA\.venv\Scripts\Activate.ps1 + +# 활성화 오류 시 실행 정책 변경 +# Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser + +# pip 업그레이드 +python -m pip install --upgrade pip + +# 의존성 설치 +pip install -r requirements.txt + +# 설치 확인 +pip show fastapi uvicorn sqlalchemy apscheduler +# → Name, Version 정보 출력 확인 + +# 가상환경 비활성화 +deactivate +``` + +**설치 중 오류 발생 시**: +```powershell +# Visual C++ 빌드 도구가 필요한 패키지 오류 시 +# Microsoft C++ Build Tools 설치 필요 +# visualstudio.microsoft.com/visual-cpp-build-tools/ 에서 다운로드 후 설치 +``` + +### 3.4 환경 변수 파일 (.env) 생성 + +메모장 또는 VS Code로 `C:\GUARDiA\itsm\.env` 파일을 생성합니다. + +```powershell +# PowerShell로 .env 파일 생성 +$envContent = @" +SECRET_KEY=여기에_64자이상의_랜덤문자열_반드시_변경하세요 +ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=480 +DB_URL=sqlite+aiosqlite:///C:/GUARDiA/itsm/guardia_itsm.db +ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000 +# 라이선스 마스터 키 (64자리 hex = 32바이트) +# 생성: python -c "import secrets; print(secrets.token_hex(32))" +# 분실 시 기발급 라이선스 전부 무효화 — 안전하게 별도 보관 필수 +GUARDIA_LICENSE_KEY=0000000000000000000000000000000000000000000000000000000000000000 +MESSENGER_BASE_URL=http://localhost:8001 +MESSENGER_BOT_TOKEN=변경하세요 +SSH_TIMEOUT=30 +UPLOAD_ROOT=C:/GUARDiA/itsm/uploads +MAX_FILE_SIZE_MB=10 +"@ +$envContent | Out-File -FilePath "C:\GUARDiA\itsm\.env" -Encoding utf8 +Write-Host ".env 파일 생성 완료" +``` + +> **중요**: `SECRET_KEY`, `ENCRYPTION_KEY`, `GUARDIA_LICENSE_KEY`를 반드시 강력한 랜덤 값으로 변경하세요! + +```powershell +# GUARDIA_LICENSE_KEY 생성 +python -c "import secrets; print(secrets.token_hex(32))" +# → 출력된 64자리 hex를 GUARDIA_LICENSE_KEY에 복사 +``` + +```powershell +# 랜덤 키 생성 +python -c "import secrets; print(secrets.token_hex(32))" +# → 출력된 값을 SECRET_KEY에 복사 + +python -c "import secrets; print(secrets.token_hex(32))" +# → 출력된 값을 ENCRYPTION_KEY에 복사 +``` + +### 3.5 초기 실행 및 DB 초기화 테스트 + +```powershell +# 가상환경 활성화 +C:\GUARDiA\.venv\Scripts\Activate.ps1 + +# 디렉토리 이동 +cd C:\GUARDiA\itsm + +# 임시 실행 (초기 DB 생성 확인) +$process = Start-Process -FilePath "C:\GUARDiA\.venv\Scripts\uvicorn.exe" ` + -ArgumentList "main:app --host 127.0.0.1 --port 8000" ` + -PassThru -NoNewWindow + +Start-Sleep -Seconds 8 + +# 정상 동작 확인 +$response = Invoke-WebRequest -Uri "http://127.0.0.1:8000/" -ErrorAction SilentlyContinue +Write-Host "HTTP 응답 코드: $($response.StatusCode)" +# → 200이면 정상 + +# 로그인 테스트 +$body = '{"username":"admin","password":"admin1234!"}' +$result = Invoke-RestMethod -Uri "http://127.0.0.1:8000/auth/login" ` + -Method Post -Body $body -ContentType "application/json" +Write-Host "토큰: $($result.access_token.Substring(0,20))..." +# → 토큰 앞부분 출력 확인 + +# 프로세스 종료 +Stop-Process -Id $process.Id + +deactivate +``` + +--- + +## 4. GUARDiA Messenger 설치 + +### 4.1 소스 코드 배포 + +```powershell +# 파일 복사 +Expand-Archive -Path "C:\temp\guardia_messenger_v1.0.zip" ` + -DestinationPath "C:\GUARDiA\messenger" -Force + +# 또는 git clone +cd C:\GUARDiA +git clone http://내부git서버/guardia/messenger.git messenger +``` + +### 4.2 의존성 설치 + +```powershell +C:\GUARDiA\.venv\Scripts\Activate.ps1 +cd C:\GUARDiA\messenger +pip install -r requirements.txt +deactivate +``` + +### 4.3 환경 변수 설정 + +```powershell +$envContent = @" +SECRET_KEY=ITSM과_동일한_키_또는_별도_키 +ALGORITHM=HS256 +DB_URL=sqlite+aiosqlite:///C:/GUARDiA/messenger/guardia_messenger.db +ITSM_BASE_URL=http://localhost:8000 +PORT=8001 +"@ +$envContent | Out-File -FilePath "C:\GUARDiA\messenger\.env" -Encoding utf8 +``` + +--- + +## 5. Windows 서비스 등록 (NSSM) + +NSSM을 사용하여 GUARDiA를 Windows Service로 등록합니다. + +### 5.1 ITSM 서비스 등록 + +PowerShell을 **관리자 권한**으로 실행합니다. + +```powershell +# ITSM 서비스 등록 +nssm install GUARDiA-ITSM C:\GUARDiA\.venv\Scripts\uvicorn.exe + +# 서비스 파라미터 설정 +nssm set GUARDiA-ITSM Application C:\GUARDiA\.venv\Scripts\uvicorn.exe +nssm set GUARDiA-ITSM AppParameters "main:app --host 0.0.0.0 --port 8000 --workers 2" +nssm set GUARDiA-ITSM AppDirectory "C:\GUARDiA\itsm" + +# 로그 설정 +nssm set GUARDiA-ITSM AppStdout "C:\GUARDiA\logs\itsm.log" +nssm set GUARDiA-ITSM AppStderr "C:\GUARDiA\logs\itsm_error.log" +nssm set GUARDiA-ITSM AppRotateFiles 1 +nssm set GUARDiA-ITSM AppRotateBytes 10485760 + +# 서비스 설명 +nssm set GUARDiA-ITSM Description "GUARDiA ITSM IT Service Management" +nssm set GUARDiA-ITSM DisplayName "GUARDiA ITSM" + +# 환경 변수 설정 (경로 구분은 \t 탭 문자 사용) +nssm set GUARDiA-ITSM AppEnvironmentExtra "PYTHONDONTWRITEBYTECODE=1" + +# 자동 시작 설정 +nssm set GUARDiA-ITSM Start SERVICE_AUTO_START +``` + +### 5.2 Messenger 서비스 등록 + +```powershell +nssm install GUARDiA-Messenger C:\GUARDiA\.venv\Scripts\uvicorn.exe + +nssm set GUARDiA-Messenger Application C:\GUARDiA\.venv\Scripts\uvicorn.exe +nssm set GUARDiA-Messenger AppParameters "main:app --host 0.0.0.0 --port 8001 --workers 1" +nssm set GUARDiA-Messenger AppDirectory "C:\GUARDiA\messenger" +nssm set GUARDiA-Messenger AppStdout "C:\GUARDiA\logs\messenger.log" +nssm set GUARDiA-Messenger AppStderr "C:\GUARDiA\logs\messenger_error.log" +nssm set GUARDiA-Messenger AppRotateFiles 1 +nssm set GUARDiA-Messenger Description "GUARDiA Messenger Service" +nssm set GUARDiA-Messenger Start SERVICE_AUTO_START +``` + +### 5.3 서비스 시작 + +```powershell +# 서비스 시작 +Start-Service GUARDiA-ITSM +Start-Service GUARDiA-Messenger + +# 상태 확인 +Get-Service GUARDiA-ITSM, GUARDiA-Messenger + +# 기대 출력: +# Status Name DisplayName +# ------ ---- ----------- +# Running GUARDiA-ITSM GUARDiA ITSM +# Running GUARDiA-Messenger GUARDiA Messenger +``` + +### 5.4 서비스 관리 명령어 + +```powershell +# 시작 +Start-Service GUARDiA-ITSM + +# 중지 +Stop-Service GUARDiA-ITSM + +# 재시작 +Restart-Service GUARDiA-ITSM + +# 상태 확인 +Get-Service GUARDiA-ITSM | Select-Object Name, Status, StartType + +# 로그 확인 (최근 50줄) +Get-Content "C:\GUARDiA\logs\itsm.log" -Tail 50 + +# 실시간 로그 확인 +Get-Content "C:\GUARDiA\logs\itsm.log" -Wait -Tail 20 +``` + +### 5.5 서비스 제거 (재설치 시) + +```powershell +# 서비스 제거 +nssm remove GUARDiA-ITSM confirm +nssm remove GUARDiA-Messenger confirm +``` + +--- + +## 6. 방화벽 설정 + +### 6.1 Windows Defender 방화벽 규칙 추가 + +```powershell +# ITSM 포트 허용 +New-NetFirewallRule ` + -DisplayName "GUARDiA ITSM" ` + -Direction Inbound ` + -Protocol TCP ` + -LocalPort 8000 ` + -Action Allow ` + -Profile Domain, Private + +# Messenger 포트 허용 +New-NetFirewallRule ` + -DisplayName "GUARDiA Messenger" ` + -Direction Inbound ` + -Protocol TCP ` + -LocalPort 8001 ` + -Action Allow ` + -Profile Domain, Private + +# 규칙 확인 +Get-NetFirewallRule -DisplayName "GUARDiA*" | Select-Object DisplayName, Enabled, Action +``` + +### 6.2 특정 IP 대역만 허용 (보안 강화) + +```powershell +# 내부 네트워크 192.168.1.0/24 에서만 접근 허용 +New-NetFirewallRule ` + -DisplayName "GUARDiA ITSM (내부망)" ` + -Direction Inbound ` + -Protocol TCP ` + -LocalPort 8000 ` + -RemoteAddress "192.168.1.0/24" ` + -Action Allow + +# 기존 전체 허용 규칙 비활성화 +Disable-NetFirewallRule -DisplayName "GUARDiA ITSM" +``` + +--- + +## 7. IIS 리버스 프록시 설정 (선택) + +HTTPS를 적용하거나 80/443 포트를 사용하려면 IIS를 앞단에 배치합니다. + +### 7.1 IIS 및 ARR 설치 + +```powershell +# IIS 설치 +Enable-WindowsOptionalFeature -Online -FeatureName IIS-WebServerRole -All +Install-WindowsFeature -Name Web-Server -IncludeManagementTools + +# ARR (Application Request Routing) 설치 +# Web Platform Installer에서 "Application Request Routing 3.0" 설치 +# 또는: https://www.iis.net/downloads/microsoft/application-request-routing +``` + +### 7.2 IIS 역방향 프록시 설정 + +IIS 관리자를 열어 다음을 설정합니다: + +``` +1. IIS 관리자 → 서버 노드 → Application Request Routing Cache + → "Enable proxy" 체크 + +2. 웹사이트 → "URL 재작성" → 규칙 추가 + → 역방향 프록시 → 서버: localhost:8000 + +3. 바인딩 추가: + - 포트 80 (HTTP) + - 포트 443 (HTTPS, SSL 인증서 선택) +``` + +**web.config 예시**: +```xml + + + + + + + + + + + + + +``` + +--- + +## 8. 설치 검증 + +### 8.1 서비스 상태 확인 + +```powershell +# 서비스 상태 +Get-Service GUARDiA-ITSM, GUARDiA-Messenger + +# 포트 리스닝 확인 +netstat -an | findstr ":8000" +netstat -an | findstr ":8001" +# → TCP 0.0.0.0:8000 ... LISTENING +``` + +### 8.2 API 기능 테스트 + +```powershell +# 1. 홈페이지 응답 확인 +$response = Invoke-WebRequest -Uri "http://localhost:8000/" +Write-Host "HTTP 상태: $($response.StatusCode)" +# → 200 + +# 2. 로그인 테스트 +$loginBody = '{"username":"admin","password":"admin1234!"}' +$loginResult = Invoke-RestMethod -Uri "http://localhost:8000/auth/login" ` + -Method Post -Body $loginBody -ContentType "application/json" +$token = $loginResult.access_token +Write-Host "로그인 성공: 토큰 앞 20자 = $($token.Substring(0,20))..." + +# 3. 대시보드 API 호출 +$headers = @{ Authorization = "Bearer $token" } +$dashboard = Invoke-RestMethod -Uri "http://localhost:8000/dashboard/summary" ` + -Headers $headers +Write-Host "대시보드 응답:" +$dashboard | ConvertTo-Json + +# 4. Messenger 확인 +$msgResponse = Invoke-WebRequest -Uri "http://localhost:8001/" +Write-Host "Messenger HTTP 상태: $($msgResponse.StatusCode)" +``` + +### 8.3 설치 검증 스크립트 + +```powershell +# C:\GUARDiA\verify_install.ps1 생성 +$script = @' +Write-Host "=== GUARDiA 설치 검증 ===" -ForegroundColor Cyan + +function Check-Item($condition, $label) { + if ($condition) { + Write-Host " [ok] $label" -ForegroundColor Green + } else { + Write-Host " [FAIL] $label" -ForegroundColor Red + } +} + +# 서비스 상태 +$itsmSvc = Get-Service GUARDiA-ITSM -ErrorAction SilentlyContinue +Check-Item ($itsmSvc -and $itsmSvc.Status -eq "Running") "ITSM 서비스 실행 중" + +$msgSvc = Get-Service GUARDiA-Messenger -ErrorAction SilentlyContinue +Check-Item ($msgSvc -and $msgSvc.Status -eq "Running") "Messenger 서비스 실행 중" + +# 포트 확인 +$itsm8000 = netstat -an | findstr ":8000" | findstr "LISTENING" +Check-Item ($itsm8000 -ne $null) "ITSM 포트 8000 리스닝" + +$msg8001 = netstat -an | findstr ":8001" | findstr "LISTENING" +Check-Item ($msg8001 -ne $null) "Messenger 포트 8001 리스닝" + +# API 응답 +try { + $r = Invoke-WebRequest "http://localhost:8000/" -TimeoutSec 5 + Check-Item ($r.StatusCode -eq 200) "ITSM API 응답 (200)" +} catch { + Check-Item $false "ITSM API 응답 (오류: $_)" +} + +# DB 파일 +Check-Item (Test-Path "C:\GUARDiA\itsm\guardia_itsm.db") "ITSM DB 파일 존재" + +# 로그 파일 +Check-Item (Test-Path "C:\GUARDiA\logs\itsm.log") "ITSM 로그 파일 존재" + +Write-Host "=== 검증 완료 ===" -ForegroundColor Cyan +'@ + +$script | Out-File -FilePath "C:\GUARDiA\verify_install.ps1" -Encoding utf8 + +# 검증 실행 +PowerShell -ExecutionPolicy Bypass -File "C:\GUARDiA\verify_install.ps1" +``` + +--- + +## 9. 보안 강화 설정 + +### 9.1 초기 관리자 비밀번호 변경 + +```powershell +# 로그인 +$loginBody = '{"username":"admin","password":"admin1234!"}' +$loginResult = Invoke-RestMethod -Uri "http://localhost:8000/auth/login" ` + -Method Post -Body $loginBody -ContentType "application/json" +$token = $loginResult.access_token + +# 비밀번호 변경 +$pwBody = @{ + current_password = "admin1234!" + new_password = "새로운강력한비밀번호!@#456" +} | ConvertTo-Json + +Invoke-RestMethod -Uri "http://localhost:8000/auth/change-password" ` + -Method Post -Body $pwBody -ContentType "application/json" ` + -Headers @{ Authorization = "Bearer $token" } +Write-Host "비밀번호 변경 완료" +``` + +### 9.2 .env 파일 접근 권한 제한 + +```powershell +# .env 파일 소유자 확인 +Get-Acl "C:\GUARDiA\itsm\.env" + +# 서비스 계정(또는 시스템)만 읽기 가능하도록 권한 설정 +$acl = Get-Acl "C:\GUARDiA\itsm\.env" +$acl.SetAccessRuleProtection($true, $false) # 상속 차단 + +# 현재 사용자 전체 제어 +$rule = New-Object System.Security.AccessControl.FileSystemAccessRule( + $env:USERNAME, "FullControl", "Allow" +) +$acl.AddAccessRule($rule) +Set-Acl "C:\GUARDiA\itsm\.env" $acl + +Write-Host "파일 권한 설정 완료" +``` + +### 9.3 이벤트 로그 설정 + +```powershell +# 이벤트 소스 등록 (최초 1회) +New-EventLog -LogName Application -Source "GUARDiA-ITSM" -ErrorAction SilentlyContinue + +# 이벤트 로그 크기 설정 +Limit-EventLog -LogName Application -MaximumSize 50MB +``` + +### 9.4 보안 정책 확인 + +```powershell +# TLS 1.2 이상만 허용 (Windows Server 2019+는 기본 설정) +# 레지스트리 확인 +Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server" -ErrorAction SilentlyContinue +# DisabledByDefault = 1 이면 비활성화 +``` + +--- + +## 10. 자동 백업 설정 + +### 10.1 백업 스크립트 생성 + +```powershell +# C:\GUARDiA\scripts\backup.ps1 생성 +$backupScript = @' +$date = Get-Date -Format "yyyyMMdd_HHmm" +$backupDir = "C:\GUARDiA\backup" + +# 백업 디렉토리 생성 +if (-not (Test-Path $backupDir)) { + New-Item -ItemType Directory -Path $backupDir -Force +} + +# ITSM DB 백업 +$itsmDb = "C:\GUARDiA\itsm\guardia_itsm.db" +if (Test-Path $itsmDb) { + Copy-Item $itsmDb "$backupDir\itsm_$date.db" + Write-Host "$date - ITSM DB 백업 완료" +} + +# Messenger DB 백업 +$msgDb = "C:\GUARDiA\messenger\guardia_messenger.db" +if (Test-Path $msgDb) { + Copy-Item $msgDb "$backupDir\messenger_$date.db" + Write-Host "$date - Messenger DB 백업 완료" +} + +# 업로드 파일 백업 (선택) +# Compress-Archive -Path "C:\GUARDiA\itsm\uploads" -DestinationPath "$backupDir\uploads_$date.zip" + +# 30일 이전 백업 삭제 +Get-ChildItem $backupDir -Filter "*.db" | + Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-30) } | + Remove-Item -Force + +Write-Host "$date - 백업 작업 완료" +"@ + +New-Item -ItemType Directory -Force -Path "C:\GUARDiA\scripts" +$backupScript | Out-File -FilePath "C:\GUARDiA\scripts\backup.ps1" -Encoding utf8 +``` + +### 10.2 작업 스케줄러 등록 + +```powershell +# 매일 새벽 3시 자동 실행 +$action = New-ScheduledTaskAction ` + -Execute "PowerShell.exe" ` + -Argument "-NonInteractive -ExecutionPolicy Bypass -File C:\GUARDiA\scripts\backup.ps1 >> C:\GUARDiA\logs\backup.log 2>&1" + +$trigger = New-ScheduledTaskTrigger -Daily -At "03:00" + +$settings = New-ScheduledTaskSettingsSet ` + -ExecutionTimeLimit (New-TimeSpan -Hours 1) ` + -RunOnlyIfNetworkAvailable $false + +Register-ScheduledTask ` + -TaskName "GUARDiA-Backup" ` + -Action $action ` + -Trigger $trigger ` + -Settings $settings ` + -RunLevel Highest ` + -Force + +Write-Host "백업 작업 스케줄 등록 완료" + +# 즉시 실행으로 테스트 +Start-ScheduledTask -TaskName "GUARDiA-Backup" +Start-Sleep -Seconds 5 +Get-ChildItem "C:\GUARDiA\backup\" | Sort-Object LastWriteTime -Descending | Select-Object -First 3 +``` + +--- + +## 11. 문제 해결 + +### 11.1 서비스 시작 실패 + +```powershell +# 1. 서비스 상태 확인 +Get-Service GUARDiA-ITSM | Format-List * + +# 2. 이벤트 로그 확인 +Get-EventLog -LogName Application -Source "GUARDiA-ITSM" -Newest 10 | + Select-Object TimeGenerated, EntryType, Message + +# 3. NSSM 로그 확인 +Get-Content "C:\GUARDiA\logs\itsm_error.log" -Tail 30 + +# 4. 수동 실행으로 오류 확인 +cd C:\GUARDiA\itsm +C:\GUARDiA\.venv\Scripts\uvicorn.exe main:app --host 127.0.0.1 --port 8000 +# → 직접 오류 메시지 확인 +``` + +### 11.2 Python 패키지 누락 오류 + +``` +오류: ModuleNotFoundError: No module named 'xxx' + +해결: +C:\GUARDiA\.venv\Scripts\Activate.ps1 +pip install xxx +deactivate +Restart-Service GUARDiA-ITSM +``` + +### 11.3 포트 충돌 + +```powershell +# 8000 포트 사용 프로세스 확인 +netstat -ano | findstr ":8000" +# → 마지막 컬럼이 PID + +# 프로세스 확인 +Get-Process -Id + +# 프로세스 종료 (필요 시) +Stop-Process -Id -Force +``` + +### 11.4 PowerShell 실행 정책 오류 + +``` +오류: running scripts is disabled on this system + +해결: +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope LocalMachine +# 또는 현재 사용자만: +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser +``` + +### 11.5 DB 파일 잠금 오류 + +``` +오류: database is locked + +해결: +1. 서비스 중지 + Stop-Service GUARDiA-ITSM + +2. 잠금 파일 삭제 + Remove-Item "C:\GUARDiA\itsm\guardia_itsm.db-shm" -ErrorAction SilentlyContinue + Remove-Item "C:\GUARDiA\itsm\guardia_itsm.db-wal" -ErrorAction SilentlyContinue + +3. 서비스 재시작 + Start-Service GUARDiA-ITSM +``` + +### 11.6 인코딩 오류 (한글 깨짐) + +```powershell +# .env 파일을 UTF-8 without BOM으로 저장 +[System.IO.File]::WriteAllText( + "C:\GUARDiA\itsm\.env", + (Get-Content "C:\GUARDiA\itsm\.env" -Raw), + [System.Text.UTF8Encoding]::new($false) # $false = no BOM +) +``` + +--- + +## 부록 A: Windows 서비스 빠른 참조 + +```powershell +# 서비스 시작/중지/재시작 +Start-Service GUARDiA-ITSM +Stop-Service GUARDiA-ITSM +Restart-Service GUARDiA-ITSM + +Start-Service GUARDiA-Messenger +Stop-Service GUARDiA-Messenger +Restart-Service GUARDiA-Messenger + +# 상태 확인 +Get-Service GUARDiA-ITSM, GUARDiA-Messenger + +# 로그 확인 (최근 50줄) +Get-Content "C:\GUARDiA\logs\itsm.log" -Tail 50 +Get-Content "C:\GUARDiA\logs\itsm_error.log" -Tail 20 + +# 실시간 로그 모니터링 +Get-Content "C:\GUARDiA\logs\itsm.log" -Wait -Tail 10 + +# 수동 백업 +PowerShell -File "C:\GUARDiA\scripts\backup.ps1" +``` + +## 부록 B: 환경 변수 참조 + +| 변수명 | 설명 | 예시 값 | +|--------|------|---------| +| SECRET_KEY | JWT 서명 키 (필수 변경) | 랜덤 64자 이상 문자열 | +| ENCRYPTION_KEY | AES-256 암호화 키 (필수 변경) | 랜덤 64자 hex 문자열 | +| ACCESS_TOKEN_EXPIRE_MINUTES | 토큰 만료 시간(분) | 480 (8시간) | +| DB_URL | SQLite DB 경로 | sqlite+aiosqlite:///C:/... | +| MESSENGER_BASE_URL | Messenger 서버 URL | http://localhost:8001 | +| SSH_TIMEOUT | SSH 실행 타임아웃(초) | 30 | +| UPLOAD_ROOT | 파일 업로드 경로 | C:/GUARDiA/itsm/uploads | +| MAX_FILE_SIZE_MB | 최대 업로드 파일 크기(MB) | 10 | + +--- + +*설치 중 문제가 발생하면 `C:\GUARDiA\logs\itsm_error.log`를 확인하거나 개발팀에 문의하세요.* diff --git a/07_SI프로젝트관리_분석설계서.md b/07_SI프로젝트관리_분석설계서.md new file mode 100644 index 0000000..9e4de8d --- /dev/null +++ b/07_SI프로젝트관리_분석설계서.md @@ -0,0 +1,432 @@ +# GUARDiA ITSM — SI 프로젝트 관리 모듈 분석·설계서 + +**문서 버전**: 1.0 +**작성일**: 2026-05-25 +**대상 독자**: 개발자, PM, 이해관계자 + +--- + +## 목차 + +1. [개요 및 목적](#1-개요-및-목적) +2. [기능 범위](#2-기능-범위) +3. [도메인 모델 설계](#3-도메인-모델-설계) +4. [API 엔드포인트 설계](#4-api-엔드포인트-설계) +5. [프로세스 흐름](#5-프로세스-흐름) +6. [추가 고려사항 및 확장 방향](#6-추가-고려사항-및-확장-방향) +7. [SM ↔ SI 통합 연계](#7-sm--si-통합-연계) + +--- + +## 1. 개요 및 목적 + +### 1.1 현재 vs 확장 범위 + +``` +현재 (SM 모드) 확장 (SI 모드) +──────────────── ──────────────────────────── +SR 접수·처리 RFP 요구사항 등록·관리 +SSL 인증서 점검 WBS 작성·진척 관리 +정기 PM 점검 프로젝트 이슈·리스크 관리 +장애 관리 (Incident) 마일스톤·산출물 관리 +온콜·당직 관리 변경 요청(CR) 관리 +배치 작업 관리 테스트 관리 (계획→실행→결함) + 요구사항 추적성 매트릭스(RTM) + Gantt 차트 데이터 제공 + 안정화 체크리스트 +``` + +### 1.2 SI 프로젝트 생명주기 + +``` +착수 → 분석 → 설계 → 개발 → 테스트 → 구축 → 안정화 → 종료 + ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ +RFP 요구사 설계 WBS 테스트 시스템 헬스체크 최종 +등록 항분류 산출 진척 실행 이관 SR자동 보고서 + 물 관리 결함 체크 생성 +``` + +--- + +## 2. 기능 범위 + +### 2.1 핵심 기능 8개 + +| # | 기능 | 설명 | +|---|------|------| +| F1 | **SI 프로젝트 관리** | 프로젝트 CRUD, 단계(Phase) 전환, 진행률 집계 | +| F2 | **RFP 요구사항 관리** | 요구사항 등록·분류·추적, RTM 생성, Excel 일괄 업로드 | +| F3 | **WBS 관리** | 계층형 WBS 작성, Gantt 데이터, 진척률·지연 감지 | +| F4 | **이슈 관리** | 기술/일정/자원/품질 이슈, 이슈→SR 연동 | +| F5 | **리스크 관리** | 확률×영향 점수, 대응 계획, 리스크→이슈 전환 | +| F6 | **마일스톤·산출물 관리** | 마일스톤 달성 여부, 산출물 제출·승인 워크플로우 | +| F7 | **변경 요청(CR) 관리** | 범위·일정·예산 변경, 결재 연동 | +| F8 | **테스트 관리** | 테스트 계획, 케이스, 실행, 결함, 결과 보고서 | + +### 2.2 고려사항 (구현 시 주의) + +``` +① 요구사항 추적성 (Traceability) + RFP 요구사항 → WBS 항목 → 테스트 케이스 → 결함 → 검증 + req_id FK 체인으로 연결, RTM Excel 자동 생성 + +② WBS 계층 구조 + 재귀 Self-Join (parent_id → id) + 최대 4레벨: 1 프로젝트 > 1.1 단계 > 1.1.1 업무 > 1.1.1.1 작업 + 진척률 = 자식 노드 평균 (Leaf 노드만 수동 입력) + +③ Gantt 데이터 포맷 + WBS 항목의 planned_start/end, actual_start/end를 JSON 배열로 반환 + 프론트엔드에서 D3.js 또는 다른 Gantt 라이브러리로 렌더링 + +④ 지연 감지 자동화 + 스케줄러: 매일 07:00 — planned_end < today AND completion_pct < 100 → 이슈 자동 생성 + 알림 + +⑤ SM 통합 + SI 안정화 단계 종료 후 → SM 모드로 자동 전환 + SM 서버(CMDB), SR, PM 스케줄에 연동 + +⑥ 대용량 파일 + 산출물 파일 업로드는 기존 attachments 모듈 재사용 (uploads/si_deliverables/) + +⑦ 보안 + 프로젝트별 접근 권한: ADMIN > PM(해당 프로젝트) > ENGINEER(참여자) > 조회 불가 + 계약금액 등 민감 필드는 ADMIN/PM만 조회 +``` + +--- + +## 3. 도메인 모델 설계 + +### 3.1 ER 다이어그램 (핵심 테이블) + +``` +tb_si_project (SI 프로젝트) + ├── tb_si_requirement (요구사항) ─── req_id FK ──► tb_wbs_item + ├── tb_wbs_item (WBS 항목) ←── parent_id (self-join) + │ └── tb_wbs_item ←── predecessor FK + ├── tb_project_issue (이슈) + │ └── tb_sr_request (SR 연동, nullable FK) + ├── tb_project_risk (리스크) + │ └── tb_project_issue (실현 시 이슈 생성) + ├── tb_project_milestone (마일스톤) + │ └── tb_project_deliverable (산출물) + ├── tb_change_request (변경 요청) + │ └── tb_approval_flow (결재 연동) + └── tb_si_test_plan (테스트 계획) + ├── tb_si_test_case (테스트 케이스) + │ └── tb_si_requirement (요구사항 FK) + └── tb_si_test_execution (실행 결과) + └── tb_si_defect (결함) +``` + +### 3.2 Enum 정의 + +```python +class ProjectPhase(str, Enum): + INITIATION = "INITIATION" # 착수 + ANALYSIS = "ANALYSIS" # 분석 + DESIGN = "DESIGN" # 설계 + DEVELOPMENT = "DEVELOPMENT" # 개발 + TESTING = "TESTING" # 테스트 + DEPLOYMENT = "DEPLOYMENT" # 구축/이관 + STABILIZATION = "STABILIZATION" # 안정화 + CLOSED = "CLOSED" # 종료 + +class ReqType(str, Enum): + FUNCTIONAL = "FUNCTIONAL" # 기능 요구사항 + NON_FUNCTIONAL = "NON_FUNCTIONAL" # 비기능 요구사항 + CONSTRAINT = "CONSTRAINT" # 제약 사항 + INTERFACE = "INTERFACE" # 인터페이스 요구사항 + +class ReqStatus(str, Enum): + DRAFT = "DRAFT" # 초안 + REVIEWED = "REVIEWED" # 검토 완료 + APPROVED = "APPROVED" # 승인 + IMPLEMENTED = "IMPLEMENTED" # 구현 완료 + VERIFIED = "VERIFIED" # 검증 완료 + DEFERRED = "DEFERRED" # 보류 + DELETED = "DELETED" # 삭제 + +class WbsStatus(str, Enum): + NOT_STARTED = "NOT_STARTED" + IN_PROGRESS = "IN_PROGRESS" + COMPLETED = "COMPLETED" + DELAYED = "DELAYED" + ON_HOLD = "ON_HOLD" + +class RiskLevel(str, Enum): + HIGH = "HIGH" # 3 + MEDIUM = "MEDIUM" # 2 + LOW = "LOW" # 1 + +class IssueType(str, Enum): + TECHNICAL = "TECHNICAL" # 기술 + SCHEDULE = "SCHEDULE" # 일정 + RESOURCE = "RESOURCE" # 자원 + QUALITY = "QUALITY" # 품질 + SCOPE = "SCOPE" # 범위 + EXTERNAL = "EXTERNAL" # 외부 + +class CrType(str, Enum): + SCOPE = "SCOPE" # 범위 변경 + SCHEDULE = "SCHEDULE" # 일정 변경 + BUDGET = "BUDGET" # 예산 변경 + QUALITY = "QUALITY" # 품질 기준 변경 + +class TestResult(str, Enum): + PASS = "PASS" + FAIL = "FAIL" + BLOCKED = "BLOCKED" + SKIP = "SKIP" + +class DefectSeverity(str, Enum): + CRITICAL = "CRITICAL" + MAJOR = "MAJOR" + MINOR = "MINOR" + TRIVIAL = "TRIVIAL" +``` + +--- + +## 4. API 엔드포인트 설계 + +### 4.1 SI 프로젝트 (`/api/si/projects`) + +| Method | Path | 설명 | +|--------|------|------| +| GET | `/` | 프로젝트 목록 | +| POST | `/` | 프로젝트 생성 | +| GET | `/{id}` | 프로젝트 상세 (진행률 포함) | +| PATCH | `/{id}` | 프로젝트 수정 | +| PATCH | `/{id}/phase` | 단계 전환 | +| GET | `/{id}/dashboard` | 프로젝트 대시보드 (이슈수, 리스크수, WBS 진척률) | +| GET | `/{id}/gantt` | Gantt 차트 데이터 (WBS 전체) | +| POST | `/{id}/members` | 프로젝트 멤버 추가 | + +### 4.2 요구사항 (`/api/si/projects/{pid}/requirements`) + +| Method | Path | 설명 | +|--------|------|------| +| GET | `/` | 요구사항 목록 (타입/상태 필터) | +| POST | `/` | 요구사항 단건 등록 | +| POST | `/bulk` | Excel 일괄 업로드 (openpyxl 파싱) | +| GET | `/{id}` | 요구사항 상세 | +| PATCH | `/{id}` | 요구사항 수정 | +| PATCH | `/{id}/status` | 상태 변경 | +| GET | `/rtm` | 추적성 매트릭스(RTM) JSON | +| GET | `/rtm/excel` | RTM Excel 다운로드 | + +### 4.3 WBS (`/api/si/projects/{pid}/wbs`) + +| Method | Path | 설명 | +|--------|------|------| +| GET | `/` | WBS 트리 구조 조회 | +| POST | `/` | WBS 항목 추가 | +| POST | `/bulk` | Excel WBS 일괄 업로드 | +| PATCH | `/{id}` | WBS 항목 수정 | +| PATCH | `/{id}/progress` | 진척률 업데이트 | +| DELETE | `/{id}` | WBS 항목 삭제 (자식 있으면 거부) | +| GET | `/gantt` | Gantt 렌더링용 JSON | +| GET | `/delayed` | 지연 항목 목록 | + +### 4.4 이슈 (`/api/si/projects/{pid}/issues`) + +| Method | Path | 설명 | +|--------|------|------| +| GET | `/` | 이슈 목록 (타입/상태 필터) | +| POST | `/` | 이슈 등록 | +| GET | `/{id}` | 이슈 상세 | +| PATCH | `/{id}` | 이슈 수정 | +| PATCH | `/{id}/resolve` | 이슈 해결 처리 | +| POST | `/{id}/convert-sr` | ITSM SR로 변환 등록 | + +### 4.5 리스크 (`/api/si/projects/{pid}/risks`) + +| Method | Path | 설명 | +|--------|------|------| +| GET | `/` | 리스크 목록 (점수 정렬) | +| POST | `/` | 리스크 등록 | +| PATCH | `/{id}` | 리스크 수정 | +| PATCH | `/{id}/occur` | 리스크 실현 → 이슈 자동 생성 | +| GET | `/matrix` | 리스크 매트릭스 데이터 (3×3 격자) | + +### 4.6 마일스톤·산출물 + +| Method | Path | 설명 | +|--------|------|------| +| GET | `/api/si/projects/{pid}/milestones` | 마일스톤 목록 | +| POST | `/api/si/projects/{pid}/milestones` | 마일스톤 등록 | +| PATCH | `/api/si/projects/{pid}/milestones/{id}/achieve` | 달성 처리 | +| GET | `/api/si/projects/{pid}/deliverables` | 산출물 목록 | +| POST | `/api/si/projects/{pid}/deliverables` | 산출물 등록 | +| POST | `/api/si/projects/{pid}/deliverables/{id}/submit` | 제출 처리 | +| PATCH | `/api/si/projects/{pid}/deliverables/{id}/approve` | 승인 처리 | + +### 4.7 변경 요청 (`/api/si/projects/{pid}/change-requests`) + +| Method | Path | 설명 | +|--------|------|------| +| GET | `/` | CR 목록 | +| POST | `/` | CR 등록 | +| PATCH | `/{id}/approve` | CR 승인 | +| PATCH | `/{id}/reject` | CR 거부 | +| PATCH | `/{id}/implement` | CR 구현 완료 | + +### 4.8 테스트 관리 (`/api/si/projects/{pid}/tests`) + +| Method | Path | 설명 | +|--------|------|------| +| POST | `/plans` | 테스트 계획 생성 | +| POST | `/plans/{plan_id}/cases` | 테스트 케이스 등록 | +| POST | `/plans/{plan_id}/cases/bulk` | 케이스 일괄 업로드 | +| POST | `/plans/{plan_id}/execute` | 테스트 실행 결과 저장 | +| POST | `/defects` | 결함 등록 | +| PATCH | `/defects/{id}/fix` | 결함 수정 완료 | +| GET | `/plans/{plan_id}/report` | 테스트 결과 보고서 | + +--- + +## 5. 프로세스 흐름 + +### 5.1 RFP 입력 → WBS 자동 생성 흐름 + +``` +1. RFP 문서 업로드 (PDF/Word/Excel) +2. 요구사항 수동 입력 또는 Excel 일괄 업로드 + → req_id 자동 채번: REQ-F-001 (기능), REQ-NF-001 (비기능) +3. 요구사항 검토 → APPROVED 상태 전환 +4. WBS 자동 생성 트리거: + APPROVED 요구사항 카테고리 기준으로 WBS 골격 생성 + (분석→설계→개발→테스트 단계별 노드 자동 생성) +5. PM이 WBS 상세 조정 (기간, 담당자 배정) +6. Gantt 차트 확인 → 일정 확정 +``` + +### 5.2 WBS 진척 관리 흐름 + +``` +매일 07:00 스케줄러 실행: + └── planned_end < today AND completion_pct < 100 + └── status = DELAYED 자동 변경 + └── ProjectIssue 자동 생성 (issue_type=SCHEDULE) + └── Messenger 알림 → PM/담당자 + +엔지니어: completion_pct 업데이트 + └── PATCH /wbs/{id}/progress {"completion_pct": 75} + └── 부모 노드 진척률 자동 재계산 (평균) + └── 프로젝트 전체 진척률 갱신 +``` + +### 5.3 리스크 → 이슈 전환 흐름 + +``` +리스크 생성 (확률×영향 점수 HIGH×HIGH = 9) + └── 점수 6 이상: 관리 필요 알림 + └── 점수 9: 즉시 PM 알림 + +리스크 실현 (PATCH /risks/{id}/occur): + └── risk.status = OCCURRED + └── ProjectIssue 자동 생성 + - title: f"[리스크 실현] {risk.title}" + - issue_type: 리스크 타입 매핑 + - priority: HIGH + └── Messenger P2 알림 +``` + +### 5.4 테스트 → SM 전환 흐름 + +``` +테스트 완료 조건: + - 전체 TC Pass율 ≥ 95% + - Critical/Major 결함 0건 + - 마일스톤 "테스트 완료" 달성 + +구축(DEPLOYMENT) 단계: + - 서버 정보 → CMDB 자동 등록 (Server 테이블) + - SSL 도메인 → SslDomain 자동 등록 + - PM 스케줄 → PmSchedule 자동 생성 + +안정화(STABILIZATION) 단계: + - 안정화 SR 자동 생성 (sr_type=INCIDENT, priority=HIGH) + - 온콜 당직 스케줄 생성 + +종료(CLOSED): + - 프로젝트 최종 보고서 Excel 생성 + - SM 모드로 전환 완료 플래그 +``` + +--- + +## 6. 추가 고려사항 및 확장 방향 + +### 6.1 즉시 구현 필요 (Must) + +``` +① RTM(Requirements Traceability Matrix) Excel 자동 생성 + 요구사항 ID → WBS 코드 → 테스트 케이스 ID → 결함 수 → 검증 상태 + +② WBS Excel 업로드/다운로드 + 표준 WBS 양식(xlsx) 업로드 → 자동 파싱 → DB 저장 + +③ 지연 자동 감지 스케줄러 + scheduler.py에 _scan_wbs_delay() 추가 + +④ 안정화→SM 전환 트리거 + DEPLOYMENT 완료 시 CMDB/SSL/PM 자동 생성 +``` + +### 6.2 중기 확장 (Should) + +``` +① Gantt 차트 프론트엔드 + WBS Gantt API → static/si_gantt.html 구현 + +② 공수 관리 (Man-day) + WbsItem에 planned_md, actual_md 추가 + +③ 예산 관리 + SiProject에 budget_total, budget_used 연동 + +④ 외부 협력사 포털 + CUSTOMER Role 확장: 협력사 계정에 특정 WBS만 접근 + +⑤ sLLM 연동 + RFP 텍스트 → 요구사항 자동 추출 (내부 sLLM API) +``` + +### 6.3 장기 확장 (Nice) + +``` +① 프로젝트 템플릿 + 표준 SI 프로젝트 WBS 템플릿 (전자정부, ERP 등) + +② 시뮬레이션 + 일정 변경 시 종료일 영향 시뮬레이션 (Critical Path) + +③ 유사 프로젝트 비교 + 과거 SI 프로젝트 실적 기반 일정/공수 추정 보조 +``` + +--- + +## 7. SM ↔ SI 통합 연계 + +``` +SI 프로젝트 종료 후 SM 자산 자동 등록: + +SiProject.phase = CLOSED + └── 각 서버 → Server(CMDB) 자동 등록 + └── 각 도메인 → SslDomain 자동 등록 + └── PM 스케줄 → PmSchedule 자동 생성 (MONTHLY) + └── 배치 작업 → BatchJob 이관 (있는 경우) + └── SM SRType.INQUIRY → 안정화 SR 자동 생성 + +연계 API: POST /api/si/projects/{id}/convert-to-sm + → 위 자동 등록 트리거 + → 결과 요약 반환 (등록된 서버 수, SSL 수, PM 수) +``` + +--- + +*본 설계서는 구현 진행에 따라 갱신됩니다.* diff --git a/08_AI에이전트_Paperclip_설계서.md b/08_AI에이전트_Paperclip_설계서.md new file mode 100644 index 0000000..1f9417f --- /dev/null +++ b/08_AI에이전트_Paperclip_설계서.md @@ -0,0 +1,669 @@ +# GUARDiA ITSM — AI 에이전트 (Paperclip × GUARDiA) 설계서 + +**문서 버전**: 1.0 +**작성일**: 2026-05-25 +**대상 독자**: 개발자, DevOps 엔지니어, 운영 관리자 + +--- + +## 목차 + +1. [개요 및 목적](#1-개요-및-목적) +2. [아키텍처 설계](#2-아키텍처-설계) +3. [Phase 1 — Paperclip 개발 도구 설정](#3-phase-1--paperclip-개발-도구-설정) +4. [Phase 2 — Ollama 로컬 LLM 설정](#4-phase-2--ollama-로컬-llm-설정) +5. [Phase 3 — GUARDiA 에이전트 엔진](#5-phase-3--guardia-에이전트-엔진) +6. [Phase 4 — 자율 운영 대시보드](#6-phase-4--자율-운영-대시보드) +7. [API 엔드포인트 설계](#7-api-엔드포인트-설계) +8. [보안 제약사항](#8-보안-제약사항) +9. [스케줄러 잡 설계](#9-스케줄러-잡-설계) +10. [테스트 결과](#10-테스트-결과) +11. [운영 가이드](#11-운영-가이드) +12. [향후 로드맵](#12-향후-로드맵) + +--- + +## 1. 개요 및 목적 + +### 1.1 배경 + +GUARDiA ITSM은 온프레미스 IT 서비스 관리 플랫폼으로, 서비스 요청(SR), 장애 관리, SSL 인증서 모니터링, PM 일정 관리, SI 프로젝트 관리를 제공합니다. + +AI 에이전트 기능 추가를 통해 다음 목표를 달성합니다: + +- **반복 업무 자동화**: 장애 분류, KB 등록, SSL 갱신 SR 생성 등 반복적인 운영 업무를 AI가 자동 처리 +- **능동적 모니터링**: 에이전트가 주기적으로 시스템 상태를 확인하고 이상 징후를 감지 +- **사람-AI 협업**: 고위험 작업은 사람의 승인을 거쳐 실행하는 거버넌스 체계 + +### 1.2 Paperclip 프레임워크 채택 + +[Paperclip](https://github.com/paperclipai/paperclip)은 AI 에이전트 오케스트레이션 오픈소스 프레임워크입니다. + +| 특징 | 설명 | +|------|------| +| 조직도 구조 | CEO → CTO → 개발자/QA 계층적 에이전트 관리 | +| 하트비트 시스템 | 에이전트가 주기적으로 깨어나 작업 수행 후 대기 | +| 이슈 추적 | GitHub 스타일의 태스크/이슈 관리 | +| 거버넌스 | 위험 수준에 따른 사람 승인 게이트 | + +### 1.3 보안 원칙 + +> **외부 LLM/AI API 완전 금지** — 모든 AI 추론은 Ollama (localhost:11434) 를 통해 온프레미스에서 처리 + +--- + +## 2. 아키텍처 설계 + +### 2.1 전체 구조 + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ GUARDiA ITSM │ +│ │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ FastAPI Layer │ │ +│ │ /api/agents/* ←──────────────────── agents.html (SPA) │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌─────────────────────────▼─────────────────────────────────┐ │ +│ │ AgentEngine (core) │ │ +│ │ │ │ +│ │ INCIDENT_TRIAGE KB_CURATOR SSL_WATCHER │ │ +│ │ WBS_MONITOR PM_SUGGESTER DEVELOPER │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌────────────────┐ ┌─────▼───────────────────────────────┐ │ +│ │ APScheduler │──▶│ OllamaClient (LLM 추론) │ │ +│ │ (9 cron jobs) │ │ localhost:11434 │ │ +│ └────────────────┘ └─────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ SQLite DB: tb_agent_config | tb_agent_task | tb_agent_approval│ +│ └─────────────────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────────┘ + +외부(개발 시에만): + Paperclip CLI ──── paperclip.config.json ──── agents/*.md +``` + +### 2.2 에이전트 조직도 + +``` + ┌─────────────┐ + │ CEO │ 전략·결재 + └──────┬──────┘ + ┌────────┴────────┐ + ┌──────▼──────┐ ┌────▼──────┐ + │ CTO │ │ PM_AGENT │ 프로젝트 관리 + └──────┬──────┘ └───────────┘ + ┌──────┴────────┐ +┌──▼──────┐ ┌────▼───┐ +│DEVELOPER│ │ QA │ +└─────────┘ └────────┘ + +운영 자동화 에이전트 (Ops): + INCIDENT_TRIAGE ← 장애 자동 분류 + KB_CURATOR ← 지식베이스 자동 등록 + SSL_WATCHER ← SSL 만료 감시 + WBS_MONITOR ← WBS 지연 감지 + PM_SUGGESTER ← PM 일정 제안 +``` + +--- + +## 3. Phase 1 — Paperclip 개발 도구 설정 + +### 3.1 파일 구조 + +``` +C:\GUARDiA\ +└── paperclip\ + ├── paperclip.config.json # 조직도 + 거버넌스 설정 + ├── README.md # 설치/사용 가이드 + └── agents\ + ├── ceo.md # CEO 에이전트 페르소나 + ├── cto.md # CTO 에이전트 페르소나 + ├── developer.md # 개발자 에이전트 페르소나 + └── qa.md # QA 에이전트 페르소나 +``` + +### 3.2 설치 + +```bash +npm install -g @paperclipai/paperclip + +# Paperclip 초기화 (프로젝트 루트에서) +cd C:\GUARDiA +paperclip init + +# 에이전트 시작 +paperclip start +``` + +### 3.3 조직도 구성 (paperclip.config.json 요약) + +```json +{ + "org_chart": { + "ceo": { "reports_to": null, "can_approve": ["deploy", "code_commit"] }, + "cto": { "reports_to": "ceo" }, + "developer": { "reports_to": "cto" }, + "qa": { "reports_to": "cto" }, + "pm_agent": { "reports_to": "ceo" } + }, + "llm": { + "provider": "ollama", + "base_url": "http://localhost:11434", + "models": { + "ceo": "guardia-agent", + "developer": "codellama:7b", + "qa": "codellama:7b" + } + }, + "governance": { + "require_human_approval": ["code_commit", "deploy", "delete_data"] + } +} +``` + +### 3.4 거버넌스 규칙 + +| 액션 | 승인 방식 | 승인자 | +|------|-----------|--------| +| code_commit | 사람 승인 필수 | CTO 또는 CEO | +| deploy | 사람 승인 필수 | CEO | +| delete_data | 사람 승인 필수 | CEO | +| 장애 분류 (일반) | 자동 승인 | — | +| 장애 분류 (CRITICAL) | 사람 승인 필수 | 담당자 | +| KB 등록 | 자동 승인 | — | +| SSL 갱신 SR 생성 | 자동 승인 | — | + +--- + +## 4. Phase 2 — Ollama 로컬 LLM 설정 + +### 4.1 파일 구조 + +``` +C:\GUARDiA\ +└── ollama\ + ├── setup.ps1 # 자동 설치 스크립트 + └── Modelfile.guardia # 커스텀 모델 정의 +``` + +### 4.2 자동 설치 (setup.ps1) + +```powershell +# 실행 방법: +Set-ExecutionPolicy Bypass -Scope Process -Force +.\setup.ps1 + +# 스크립트 동작: +# 1. OllamaSetup.exe 다운로드 +# 2. 설치 후 ollama serve 시작 +# 3. 헬스체크 (10회 재시도) +# 4. llama3.1:8b + codellama:7b 풀링 +# 5. guardia-agent 커스텀 모델 생성 +``` + +### 4.3 guardia-agent 모델 (Modelfile.guardia) + +``` +FROM llama3.1:8b +SYSTEM """당신은 GUARDiA ITSM AI 운영 에이전트입니다. +한국어로 응답하며, IT 서비스 관리(ITSM)에 특화되어 있습니다. +보안 원칙: 외부 API 호출 금지, 민감 정보 노출 금지.""" +PARAMETER temperature 0.2 +PARAMETER num_predict 2048 +``` + +### 4.4 OllamaClient API + +| 메서드 | 설명 | +|--------|------| +| `health_check()` | Ollama 서버 상태 확인 | +| `list_models()` | 설치된 모델 목록 조회 | +| `resolve_model(preferred)` | 선호 모델이 없으면 fallback 모델 반환 | +| `chat(messages, model)` | 대화형 추론 | +| `generate(prompt, model)` | 단일 프롬프트 추론 | +| `json_generate(prompt, model)` | JSON 응답 추출 (코드블록 자동 제거) | +| `pull_model(model)` | 모델 다운로드 | + +--- + +## 5. Phase 3 — GUARDiA 에이전트 엔진 + +### 5.1 파일 구조 + +``` +C:\GUARDiA\itsm\ +├── models.py # AgentConfig, AgentTask, AgentApproval 모델 추가 +├── core\ +│ ├── llm_client.py # OllamaClient 구현 +│ └── agents.py # AgentEngine 구현 +└── routers\ + └── agents.py # 16개 REST API 엔드포인트 +``` + +### 5.2 데이터 모델 + +#### AgentConfig (tb_agent_config) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| id | INTEGER PK | 에이전트 ID | +| name | VARCHAR | 에이전트 이름 | +| role | VARCHAR | AgentRole enum | +| llm_provider | VARCHAR | ollama (고정) | +| llm_model | VARCHAR | 사용할 LLM 모델 | +| system_prompt | TEXT | 역할 설명 프롬프트 | +| heartbeat_cron | VARCHAR | 크론 표현식 | +| is_active | BOOLEAN | 활성화 여부 | +| status | VARCHAR | IDLE/ACTIVE/WORKING/ERROR/PAUSED | +| last_heartbeat | DATETIME | 마지막 실행 시각 | +| total_tasks | INTEGER | 누적 처리 태스크 수 | +| total_tokens | INTEGER | 누적 사용 토큰 수 | + +#### AgentTask (tb_agent_task) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| id | INTEGER PK | 태스크 ID | +| agent_id | INTEGER FK | 에이전트 참조 | +| title | VARCHAR | 태스크 제목 | +| status | VARCHAR | PENDING/IN_PROGRESS/COMPLETED/FAILED | +| input_data | JSON | 입력 데이터 | +| output_data | JSON | LLM 출력 결과 | +| tokens_used | INTEGER | 사용된 토큰 수 | + +#### AgentApproval (tb_agent_approval) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| id | INTEGER PK | 승인 ID | +| agent_id | INTEGER FK | 에이전트 참조 | +| task_id | INTEGER FK nullable | 연관 태스크 | +| action_type | VARCHAR | 액션 유형 | +| action_data | JSON | 액션 상세 데이터 | +| status | VARCHAR | PENDING/APPROVED/REJECTED/AUTO_APPROVED | +| reviewed_by | INTEGER FK nullable | 검토자 | + +### 5.3 에이전트별 동작 + +#### INCIDENT_TRIAGE (15분마다 실행) + +``` +1. 미배정 장애(assigned_to=None, status=OPEN/RECEIVED) 조회 +2. LLM JSON 분류 요청: + { severity: CRITICAL/HIGH/MEDIUM/LOW, + category: HARDWARE/SOFTWARE/NETWORK/..., + reason: "분류 근거" } +3. CRITICAL → AgentApproval(PENDING) 생성 +4. 그 외 → AUTO_APPROVED + Incident 등급 즉시 반영 +``` + +#### KB_CURATOR (매시간 정각) + +``` +1. SR 완료 건 중 KB가 없는 건 조회 +2. LLM KB 초안 생성: + { kb_title, symptom, cause, solution, tags } +3. KBDocument 생성 (published=False, 검토 대기) +4. AgentApproval(AUTO_APPROVED) 기록 +``` + +#### SSL_WATCHER (매일 08:30) + +``` +1. ssl_expire_date가 0~30일 이내인 서버 조회 +2. 기존 SSL 갱신 SR이 없는 경우에만 SR 자동 생성 +3. 긴급도: 7일 미만=CRITICAL, 30일 미만=HIGH +``` + +#### WBS_MONITOR (매일 08:00) + +``` +1. 진행 중 SI 프로젝트의 WBS 지연 항목 조회 +2. 3일+ 지연: 주의, 10일+ 지연: CRITICAL +3. LLM 리스크 분석: + { risk_level, probability, impact, title, mitigation_plan } +4. ProjectRisk 자동 등록 +5. CRITICAL 리스크 → 사람 승인 필요 +``` + +#### PM_SUGGESTER (매일 09:00) + +``` +1. PM 일정이 없는 서버 조회 +2. 권장 PM 일정 제안 (AgentTask 기록) +``` + +#### DEVELOPER (수동 트리거 or 이슈 등록 시) + +``` +1. PENDING AgentTask 조회 +2. LLM 코드/응답 생성 +3. CODE_CHANGE 태스크 → AgentApproval(PENDING) 생성 +``` + +### 5.4 하트비트 사이클 + +``` +에이전트 등록 (AgentConfig 생성) + │ + ▼ +APScheduler Cron 잡 등록 + │ + ▼ (스케줄 도달) +status = ACTIVE + │ + ▼ +Ollama health_check() + │ (실패)──────────────────► status = ERROR + │ (성공) + ▼ +_handler(db, agent) 실행 + │ + ├─ 작업 완료 ──► AgentTask(COMPLETED) + status = IDLE + └─ 오류 발생 ──► AgentTask(FAILED) + status = ERROR + last_error 기록 +``` + +--- + +## 6. Phase 4 — 자율 운영 대시보드 + +### 6.1 접근 경로 + +``` +http://localhost:8001/agents +``` + +### 6.2 대시보드 구성 + +| 영역 | 설명 | +|------|------| +| LLM 상태 배너 | Ollama 온라인/오프라인 상태 실시간 표시 | +| 통계 카드 | 총 에이전트 수, 활성, 오늘 태스크, 오늘 토큰, 승인 대기 | +| 에이전트 탭 | 에이전트 카드 (역할 배지, 상태 펄스 애니메이션) | +| 조직도 탭 | 계층적 트리 렌더링 | +| 승인 대기 탭 | 보류 중 승인 목록 (CRITICAL 강조) | +| 태스크 피드 탭 | 전체 에이전트 태스크 실시간 피드 | + +### 6.3 에이전트 상태 색상 코드 + +| 상태 | 색상 | 설명 | +|------|------|------| +| IDLE | 회색 | 대기 중 | +| ACTIVE | 파랑 | 심장박동 시작 | +| WORKING | 주황 (펄스) | 작업 진행 중 | +| ERROR | 빨강 | 오류 발생 | +| PAUSED | 노랑 | 일시 중지 | + +### 6.4 역할별 색상 배지 + +``` +CEO → 보라색 (#6C5CE7) +CTO → 파랑색 (#0984E3) +DEVELOPER → 초록색 (#00B894) +QA → 노랑색 (#FDCB6E) +PM_AGENT → 청록색 (#00CEC9) + +INCIDENT_TRIAGE → 빨강 (#E17055) +KB_CURATOR → 민트 (#55EFC4) +SSL_WATCHER → 주황 (#FD79A8) +WBS_MONITOR → 남색 (#74B9FF) +PM_SUGGESTER → 연두 (#A29BFE) +``` + +--- + +## 7. API 엔드포인트 설계 + +### 7.1 전체 목록 (16개) + +| HTTP | 경로 | 설명 | 권한 | +|------|------|------|------| +| GET | /api/agents | 에이전트 목록 | USER+ | +| POST | /api/agents | 에이전트 생성 | ADMIN | +| GET | /api/agents/stats | 통계 요약 | USER+ | +| GET | /api/agents/orgchart | 조직도 | USER+ | +| GET | /api/agents/approvals | 승인 대기 목록 | USER+ | +| PATCH | /api/agents/approvals/{id}/review | 승인/거부 | USER+ | +| GET | /api/agents/llm/health | LLM 상태 확인 | USER+ | +| POST | /api/agents/llm/pull | 모델 다운로드 | ADMIN | +| GET | /api/agents/{id} | 에이전트 상세 | USER+ | +| PATCH | /api/agents/{id} | 에이전트 수정 | ADMIN | +| DELETE | /api/agents/{id} | 에이전트 삭제 | ADMIN | +| POST | /api/agents/{id}/heartbeat | 수동 하트비트 | USER+ | +| POST | /api/agents/{id}/pause | 에이전트 일시정지 | ADMIN | +| POST | /api/agents/{id}/resume | 에이전트 재개 | ADMIN | +| GET | /api/agents/{id}/tasks | 태스크 목록 | USER+ | +| POST | /api/agents/{id}/tasks | 태스크 생성 | USER+ | + +> CUSTOMER 역할: 모든 에이전트 엔드포인트 접근 불가 + +### 7.2 주요 스키마 + +#### AgentConfigOut + +```json +{ + "id": 1, + "name": "장애 분류 에이전트", + "role": "INCIDENT_TRIAGE", + "description": "미배정 장애를 자동으로 분류합니다", + "llm_model": "guardia-agent", + "heartbeat_cron": "*/15 * * * *", + "is_active": true, + "status": "IDLE", + "last_heartbeat": "2026-05-25T08:15:00", + "total_tasks": 42, + "total_tokens": 15300 +} +``` + +#### AgentApprovalReview + +```json +{ + "status": "APPROVED", + "notes": "내용 확인 후 승인합니다" +} +``` + +#### AgentStatsOut + +```json +{ + "total_agents": 6, + "active_agents": 4, + "today_tasks": 12, + "today_tokens": 5240, + "pending_approvals": 2 +} +``` + +--- + +## 8. 보안 제약사항 + +### 8.1 런타임 LLM 보안 + +| 규칙 | 내용 | +|------|------| +| 외부 API 금지 | 모든 LLM 호출은 localhost:11434 (Ollama) 만 허용 | +| 토큰 최대값 | num_predict: 2048 (무한 루프 방지) | +| 온도 고정 | temperature: 0.2 (결정론적 응답) | +| 타임아웃 | HTTP 요청 30초 타임아웃 | + +### 8.2 에이전트 액션 보안 + +| 규칙 | 내용 | +|------|------| +| CRITICAL 액션 | 반드시 사람 승인 후 실행 | +| 위험 명령어 | `rm -rf /`, `shutdown`, `mkfs` 등 차단 | +| 서버 정보 | `ip_addr`, `ssh_user`, `os_pw_enc` API 응답 미포함 | +| 파일 경로 | `file_path` 컬럼 API 응답 절대 미노출 | + +### 8.3 접근 제어 + +| 역할 | 에이전트 조회 | 에이전트 생성/수정/삭제 | 승인 처리 | +|------|--------------|----------------------|----------| +| CUSTOMER | ✗ | ✗ | ✗ | +| USER | ✓ | ✗ | ✓ | +| OPERATOR | ✓ | ✗ | ✓ | +| ADMIN | ✓ | ✓ | ✓ | + +--- + +## 9. 스케줄러 잡 설계 + +### 9.1 전체 잡 목록 (9개) + +| ID | 잡 이름 | 스케줄 | 설명 | +|----|---------|--------|------| +| 1 | cert_check | 매일 09:00 | SSL 인증서 만료 확인 | +| 2 | pm_check | 매일 09:05 | PM 점검 일정 확인 | +| 3 | on_call_notify | 매일 08:55 | 온콜 교대 알림 | +| 4 | batch_cleanup | 매일 02:00 | 배치 결과 정리 | +| 5 | agent_incident_triage | 매 15분 | 장애 자동 분류 | +| 6 | agent_kb_curator | 매시간 정각 | KB 자동 생성 | +| 7 | agent_ssl_watcher | 매일 08:30 | SSL 만료 감시 | +| 8 | agent_wbs_monitor | 매일 08:00 | WBS 지연 감지 | +| 9 | agent_pm_suggester | 매일 09:00 | PM 일정 제안 | + +### 9.2 에이전트 하트비트 흐름 + +``` +APScheduler → _agent_heartbeat_by_role(role) + │ + ▼ + AgentConfig WHERE role=? AND is_active=True 조회 + │ + ┌─────────┴───────────┐ + 없음│ │있음 + ▼ ▼ + skip for each agent: + get_agent_engine().run_heartbeat(id) +``` + +--- + +## 10. 테스트 결과 + +### 10.1 구문 검사 (Syntax Check) + +| 파일 | 결과 | +|------|------| +| `models.py` | ✅ 통과 | +| `main.py` | ✅ 통과 | +| `core/llm_client.py` | ✅ 통과 | +| `core/agents.py` | ✅ 통과 | +| `core/scheduler.py` | ✅ 통과 | +| `routers/agents.py` | ✅ 통과 | + +### 10.2 심볼 검증 + +| 항목 | 검증 내용 | 결과 | +|------|-----------|------| +| AgentRole enum | 10가지 역할 정의 | ✅ | +| AgentEngine handlers | 6개 핸들러 구현 | ✅ | +| OllamaClient | 7개 메서드 구현 | ✅ | +| API 엔드포인트 | 16개 라우터 등록 | ✅ | +| 스케줄러 잡 | 9개 (기존 4 + 신규 5) | ✅ | +| main.py 라우터 | 34개 라우터 등록 | ✅ | + +### 10.3 보안 검증 + +| 항목 | 결과 | +|------|------| +| 외부 LLM API 호출 없음 | ✅ | +| ServerOut에 ip_addr 미포함 | ✅ | +| CUSTOMER 역할 차단 | ✅ | +| CRITICAL 승인 게이트 | ✅ | + +--- + +## 11. 운영 가이드 + +### 11.1 에이전트 등록 + +```bash +# 장애 분류 에이전트 등록 +curl -X POST http://localhost:8001/api/agents \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{ + "name": "장애 분류 에이전트", + "role": "INCIDENT_TRIAGE", + "description": "미배정 장애를 자동으로 분류하고 우선순위를 설정합니다", + "llm_model": "guardia-agent", + "system_prompt": "당신은 IT 장애 분류 전문가입니다...", + "heartbeat_cron": "*/15 * * * *", + "is_active": true + }' +``` + +### 11.2 수동 하트비트 실행 + +```bash +# 에이전트 즉시 실행 +curl -X POST http://localhost:8001/api/agents/1/heartbeat \ + -H "Authorization: Bearer " +``` + +### 11.3 승인 처리 + +```bash +# 승인 +curl -X PATCH http://localhost:8001/api/agents/approvals/5/review \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"status": "APPROVED", "notes": "내용 확인 후 승인"}' + +# 거부 +curl -X PATCH http://localhost:8001/api/agents/approvals/5/review \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"status": "REJECTED", "notes": "추가 검토 필요"}' +``` + +### 11.4 LLM 상태 확인 + +```bash +# Ollama 서버 상태 +curl http://localhost:8001/api/agents/llm/health + +# 응답 예시 +{ + "healthy": true, + "models": ["guardia-agent:latest", "llama3.1:8b", "codellama:7b"], + "version": "0.1.x" +} +``` + +### 11.5 모니터링 대시보드 + +``` +브라우저 접속: http://localhost:8001/agents +자동 갱신: 30초 간격 +``` + +--- + +## 12. 향후 로드맵 + +| 단계 | 기능 | 예상 시기 | +|------|------|----------| +| v1.1 | 에이전트 간 메시지 전달 (CEO→CTO 위임) | 2026 Q3 | +| v1.2 | 에이전트 학습 (이전 태스크 기반 파인튜닝) | 2026 Q3 | +| v1.3 | 멀티모달 지원 (스크린샷 분석) | 2026 Q4 | +| v2.0 | 에이전트 마켓플레이스 (커뮤니티 에이전트) | 2027 Q1 | +| v2.1 | 엣지 배포 (경량 모델: llama3.2:1b) | 2027 Q1 | + +--- + +*이 문서는 GUARDiA ITSM Paperclip × GUARDiA Phase 1~4 구현 내용을 담고 있습니다.* +*Ollama 공식 문서: https://ollama.com* +*Paperclip GitHub: https://github.com/paperclipai/paperclip* diff --git a/09_확장개발_Priority1_UI구현.md b/09_확장개발_Priority1_UI구현.md new file mode 100644 index 0000000..a4057be --- /dev/null +++ b/09_확장개발_Priority1_UI구현.md @@ -0,0 +1,206 @@ +# GUARDiA ITSM — Priority 1: UI 없는 백엔드 모듈 화면 구현 + +**문서 버전**: 1.0 | **작성일**: 2026-05-25 + +--- + +## 개요 + +백엔드 API는 완성되어 있으나 프론트엔드 HTML 페이지가 없는 7개 모듈의 SPA 구현. +각 페이지는 `/static/style.css` 공유 테마를 사용하며 독립 경로로 접근한다. + +--- + +## 구현 목록 + +| 페이지 | URL 경로 | 파일 | 백엔드 라우터 | +|--------|---------|------|-------------| +| 장애 관리 | /incidents | static/incidents.html | routers/incidents.py | +| SSL 관리 | /ssl | static/ssl.html | routers/ssl_manager.py | +| PM 점검 | /pm | static/pm.html | routers/pm.py | +| 온콜 관리 | /oncall | static/oncall.html | routers/oncall.py | +| 배치 작업 | /batch | static/batch.html | routers/batch.py | +| 바이브 코딩 | /vibe | static/vibe.html | routers/vibe.py | +| SI 프로젝트 | /si | static/si.html | si_projects/wbs/requirements/issues/risks/milestones/change_requests/tests.py | +| 라이선스 관리 | /license | static/license.html | routers/license.py | + +--- + +## 공통 설계 원칙 + +### 인증 +```javascript +const token = localStorage.getItem('guardia_token'); +if (!token) { location.href = '/login'; return; } + +async function api(method, path, body) { + const res = await fetch(path, { + method, + headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, + body: body ? JSON.stringify(body) : undefined + }); + if (res.status === 401) { localStorage.removeItem('guardia_token'); location.href = '/login'; } + return res; +} +``` + +### 테마 복원 +```javascript +document.body.dataset.theme = localStorage.getItem('guardia_theme') || 'dark'; +``` + +### 상단 네비게이션 바 +모든 외부 페이지는 공통 topnav를 가진다: +- GUARDiA ITSM 로고 (/ 링크) +- 페이지 링크: 대시보드, 장애관리, SSL, PM점검, 온콜, 배치, 바이브, SI, AI에이전트 +- 우측: 사용자명, 로그아웃 버튼 + +### 자동 새로고침 +```javascript +setInterval(loadData, 30000); +``` + +--- + +## 1. 장애 관리 (incidents.html) + +### 화면 구성 +1. **통계 카드 행** (4개): 전체, OPEN, P1/P2, 평균 MTTR +2. **필터 툴바**: 상태 / 등급 / 키워드 검색 +3. **장애 테이블**: INC번호, 제목, 등급배지, 상태, 발생시각, 담당자, MTTR +4. **등록 모달**: 제목, 설명, grade, assigned_to, affected_services, occurred_at +5. **상세 모달**: 전체 정보 + 상태 전환 버튼 + SR 연결 + RCA 종료 + +### 등급 배지 +| 등급 | 색상 | 의미 | +|------|------|------| +| P1 | 빨강 #f87171 | 서비스 전체 중단 | +| P2 | 주황 #fb923c | 주요 기능 영향 | +| P3 | 노랑 #fcd34d | 일부 기능 저하 | +| P4 | 초록 #4ade80 | 경미한 영향 | + +### 상태 전환 규칙 +``` +OPEN → INVESTIGATING | CLOSED +INVESTIGATING → MITIGATED | RESOLVED +MITIGATED → INVESTIGATING | RESOLVED +RESOLVED → CLOSED +CLOSED → (전환 불가) +``` + +--- + +## 2. SSL 인증서 관리 (ssl.html) + +### 화면 구성 +1. **현황 카드** (4개): OK / WARN / URGENT / EXPIRED — 클릭 시 필터 +2. **만료 현황 테이블**: 서버명, 만료일, 남은일수 게이지, 경고레벨 배지, 점검/갱신 버튼 +3. **days 슬라이더**: 0~90일 범위 조회 +4. **갱신 기록 모달**: new_expire_date, renewed_by, notes +5. **갱신 이력 모달**: 서버별 이력 테이블 + +### 경고 레벨 기준 +| 레벨 | 조건 | 배지 색상 | +|------|------|----------| +| OK | days_left > 30 | 초록 | +| WARN | 7 < days_left ≤ 30 | 노랑 | +| URGENT | 0 < days_left ≤ 7 | 주황 | +| EXPIRED | days_left ≤ 0 | 빨강 | + +--- + +## 3. PM 정기점검 (pm.html) + +### 탭 구성 +- **탭 1 — 점검 스케줄**: 스케줄 목록, 즉시 실행, 스케줄 등록 +- **탭 2 — 점검 결과**: 타임테이블 선택 → 체크리스트 항목별 결과 입력 + Excel 다운로드 +- **탭 3 — 체크리스트 템플릿**: 서버역할별 템플릿 CRUD + +### 점검 주기 옵션 +WEEKLY / BIWEEKLY / MONTHLY / QUARTERLY / SEMIANNUAL / ANNUAL / CUSTOM + +### 결과 상태 +| 상태 | 의미 | 색상 | +|------|------|------| +| PASS | 정상 | 초록 | +| FAIL | 실패 | 빨강 | +| WARNING | 경고 | 노랑 | +| NA | 해당없음 | 회색 | + +--- + +## 4. 온콜/당직 관리 (oncall.html) + +### 탭 구성 +- **탭 1 — 월간 캘린더**: 연/월 네비게이션, 날짜 클릭 시 당직 등록 모달 +- **탭 2 — 목록 & 일괄 등록**: 당직 목록 테이블 + JSON 일괄 등록 + +### 상단 배너 +``` +📞 오늘 당직: [이름] ([시프트]) | 백업: [백업담당자] +``` + +### 시프트 종류 +| 값 | 시간 | 색상 | +|----|------|------| +| ALL_DAY | 24시간 | 파랑 | +| DAYTIME | 09:00~18:00 | 초록 | +| NIGHTTIME | 18:00~익일09:00 | 보라 | + +--- + +## 5. 배치 작업 관리 (batch.html) + +### 탭 구성 +- **탭 1 — 배치 작업**: 작업 목록, 활성/비활성 토글, 즉시 실행, 등록 +- **탭 2 — 실행 이력**: 최근 실행 이력 통합, 상세 모달 (stdout 전체) + +### 작업 등록 필드 +| 필드 | 설명 | +|------|------| +| job_name | 작업명 | +| server_id | 대상 서버 | +| cron_expr | cron 표현식 (예: `0 2 * * *`) | +| command | 실행 명령어 | +| timeout_sec | 타임아웃 (초) | +| alert_on_fail | 실패 시 SR 자동 생성 여부 | + +### 실행 결과 상태 +SUCCESS(초록) / FAILED(빨강) / TIMEOUT(주황) / RUNNING(파랑 깜빡) + +--- + +## 6. 바이브 코딩 세션 (vibe.html) + +### 탭 구성 +- **탭 1 — 활성 세션**: 세션 카드 그리드, 파이프라인 스텝 바 +- **탭 2 — 이력**: 전체 세션 이력 테이블 +- **탭 3 — 프로젝트**: 등록된 프로젝트 목록 + 등록 모달 + +### 파이프라인 단계 +``` +PENDING → CODING → BUILDING → TESTING → DEPLOYING → COMPLETED +``` + +### Jenkins 연결 상태 +- 상단 배너: `GET /api/vibe/jenkins/health` 결과 표시 +- 연결됨(초록) / 오프라인(빨강) + +--- + +## 7. SI 프로젝트 관리 (si.html) + +### 탭 구성 (7탭) +- **프로젝트**: SI 프로젝트 목록, 등록, 단계 전환 +- **WBS**: Gantt 차트 스타일 WBS 트리, 진척률 시각화 +- **요구사항**: RFP → 확정 요구사항 관리 +- **이슈**: 프로젝트 이슈 목록, 상태 전환 +- **리스크**: 위험 매트릭스 (확률 × 영향도), 이슈 전환 +- **마일스톤**: 마일스톤 타임라인, 산출물 목록 +- **변경요청(CR)**: CR 등록, 영향도 분석, 승인 워크플로우 +- **테스트**: 테스트 계획 → 케이스 → 실행 → 결함 관리 + +### SI 프로젝트 상태 전환 +``` +PLANNING → ANALYSIS → DESIGN → DEVELOPMENT → TESTING → DEPLOYMENT → COMPLETED +``` diff --git a/10_확장개발_Priority2_코어모듈.md b/10_확장개발_Priority2_코어모듈.md new file mode 100644 index 0000000..be5e346 --- /dev/null +++ b/10_확장개발_Priority2_코어모듈.md @@ -0,0 +1,159 @@ +# GUARDiA ITSM — Priority 2: 코어 모듈 구현 + +**문서 버전**: 1.0 | **작성일**: 2026-05-25 + +--- + +## 1. core/vibe_bridge.py — Claude CLI SDK 연동 + +### 목적 +`subprocess` 방식 대신 Python SDK를 직접 연동하여 Claude CLI 세션을 관리한다. + +### 주요 클래스 + +```python +class VibeBridge: + """Claude CLI SDK 비동기 브리지""" + + async def start_session(self, sr_id: str, project_path: str) -> str: + """새 Claude 세션 시작 → session_id 반환""" + + async def send_message(self, session_id: str, message: str) -> str: + """세션에 메시지 전송 → 응답 반환""" + + async def get_session_status(self, session_id: str) -> dict: + """세션 상태 조회 {active, last_response, tokens_used}""" + + async def close_session(self, session_id: str) -> bool: + """세션 종료""" + + async def resume_session(self, session_id: str, message: str) -> str: + """기존 세션 재개""" +``` + +### 환경 변수 +```bash +CLAUDE_CLI_PATH=/usr/local/bin/claude +CLAUDE_WORKSPACE_ROOT=/opt/guardia/workspaces +CLAUDE_SESSION_TIMEOUT=3600 # 세션 타임아웃 (초) +``` + +### 사용 예시 +```python +bridge = VibeBridge() +session_id = await bridge.start_session( + sr_id="SR-20260525-000001", + project_path="/opt/src/myproject" +) +response = await bridge.send_message(session_id, "버그를 수정해주세요") +``` + +--- + +## 2. core/deploy_pipeline.py — 배포 파이프라인 오케스트레이터 + +### 목적 +빌드 → 테스트 → 배포 → 헬스체크 → ITSM 콜백의 전체 파이프라인을 관리한다. + +### 파이프라인 단계 + +| 단계 | 함수 | 설명 | +|------|------|------| +| pre_check | `_pre_check()` | 배포 환경 사전 점검 (서버 연결, 디스크 용량) | +| build | `_build()` | tb_project.build_cmd 실행 | +| test | `_test()` | tb_project.test_cmd 실행, 실패 시 중단 | +| backup | `_backup()` | 현재 배포본 백업 (rollback 대비) | +| deploy | `_deploy()` | SSH로 파일 전송 → deploy_path | +| restart | `_restart()` | was_restart_cmd 실행 | +| health_check | `_health_check()` | HTTP GET → health_check_url (10회 재시도) | +| notify | `_notify()` | WorkLog 등록 + 메신저 알림 | + +### 클래스 구조 + +```python +class DeployPipeline: + async def run(self, session_id: int) -> PipelineResult: + """전체 파이프라인 실행""" + + async def rollback(self, session_id: int) -> bool: + """이전 버전으로 롤백""" + + async def get_status(self, session_id: int) -> PipelineStatus: + """현재 진행 단계 조회""" +``` + +### 오류 처리 +- 각 단계 실패 시 `tb_vibe_session.error_msg` 업데이트 +- test 단계 실패 → 배포 중단, SR 상태 = FAILED_ROLLBACK +- health_check 10회 실패 → 자동 rollback 실행 + +--- + +## 3. 배치 잡 동적 APScheduler 등록 + +### 목적 +`tb_batch_job` 테이블의 cron_expr을 APScheduler에 동적으로 등록/제거한다. + +### 구현 위치 +`core/scheduler.py` 확장 + +### API 연동 흐름 + +``` +POST /api/batch/jobs/{id}/enable + → scheduler.add_job( + run_batch_job, + CronTrigger.from_crontab(job.cron_expr), + id=f"batch_{job.id}", + args=[job.id], + replace_existing=True + ) + +POST /api/batch/jobs/{id}/disable + → scheduler.remove_job(f"batch_{job.id}") + +서버 시작 시 (lifespan): + → 모든 is_active=True 배치 잡 자동 등록 +``` + +### run_batch_job 함수 +```python +async def run_batch_job(job_id: int): + async with SessionLocal() as db: + job = await db.get(BatchJob, job_id) + run = BatchRun(job_id=job_id, status="RUNNING", started_at=datetime.utcnow()) + db.add(run) + await db.commit() + + try: + result = await execute_ssh_command(job.server_id, job.command, job.timeout_sec) + run.status = "SUCCESS" if result.exit_code == 0 else "FAILED" + run.stdout = result.stdout[-5000:] # 마지막 5000자 + run.exit_code = result.exit_code + except asyncio.TimeoutError: + run.status = "TIMEOUT" + finally: + run.completed_at = datetime.utcnow() + await db.commit() + + # alert_on_fail 처리 + if run.status != "SUCCESS" and job.alert_on_fail: + await create_sr_for_batch_failure(job, run, db) +``` + +--- + +## 4. SM 스크립트 실행 이력 UI + +### 목적 +SSH 명령 실행 결과를 `tb_work_log` 기반으로 조회하여 index.html에 표시한다. + +### API +``` +GET /api/work-logs?sr_id={id}&work_type=SSH_EXEC +``` + +### index.html 추가 뷰 +- 스크립트 관리 뷰에 "실행 이력" 탭 추가 +- 테이블: 서버명, 스크립트명, 실행시각, 결과(PASS/FAIL), 소요시간 +- 행 클릭 시 stdout/stderr 상세 모달 diff --git a/11_확장개발_Priority3_AI에이전트.md b/11_확장개발_Priority3_AI에이전트.md new file mode 100644 index 0000000..7db8918 --- /dev/null +++ b/11_확장개발_Priority3_AI에이전트.md @@ -0,0 +1,197 @@ +# GUARDiA ITSM — Priority 3: AI 에이전트 확장 + +**문서 버전**: 1.0 | **작성일**: 2026-05-25 + +--- + +## 1. 장애 RCA 자동 초안 생성 + +### 목적 +INCIDENT_TRIAGE 에이전트 확장. RESOLVED 상태 장애에 대해 Ollama로 RCA 문서 초안을 자동 생성한다. + +### 동작 흐름 +``` +1. tb_incident WHERE status='RESOLVED' AND rca_draft IS NULL 조회 +2. LLM 프롬프트 구성: + - 장애 제목, 설명, 등급, 발생~해소 시각, 조치 이력 +3. Ollama json_generate() 호출 +4. 결과 구조: + { + "root_cause": "근본 원인", + "timeline": "사건 타임라인", + "impact": "영향 범위", + "resolution": "해결 과정", + "preventive_measures": "재발 방지 조치", + "lessons_learned": "교훈" + } +5. tb_incident.rca_draft 컬럼에 저장 (JSON) +6. AgentApproval(PENDING) 생성 — 담당자 검토 후 최종 확정 +``` + +### 구현 위치 +`core/agents.py` → `_incident_triage()` 핸들러 하단 추가 +```python +# RESOLVED 장애 RCA 초안 생성 +resolved_incidents = await db.execute( + select(Incident).where( + Incident.status == "RESOLVED", + Incident.rca_draft == None + ).limit(5) +) +for inc in resolved_incidents.scalars(): + rca = await llm.json_generate(rca_prompt(inc), agent.llm_model) + inc.rca_draft = rca + await db.commit() +``` + +--- + +## 2. 에이전트 간 메시지 전달 + +### 목적 +CEO 에이전트가 CTO/PM_AGENT에게 태스크를 위임할 수 있도록 에이전트 간 메시지 전달 체계를 구현한다. + +### 신규 DB 테이블: tb_agent_message + +```sql +CREATE TABLE tb_agent_message ( + id INTEGER PRIMARY KEY, + from_agent_id INTEGER REFERENCES tb_agent_config(id), + to_agent_id INTEGER REFERENCES tb_agent_config(id), + message_type VARCHAR(30), -- TASK_DELEGATION / STATUS_UPDATE / ESCALATION + subject VARCHAR(200), + body TEXT, + metadata JSON, + is_read BOOLEAN DEFAULT FALSE, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + read_at DATETIME +); +``` + +### 메시지 타입 + +| 타입 | 설명 | 트리거 | +|------|------|--------| +| TASK_DELEGATION | 태스크 위임 | CEO → CTO: 개발 태스크 | +| STATUS_UPDATE | 상태 보고 | CTO/Dev → CEO: 완료 보고 | +| ESCALATION | 에스컬레이션 | 하위 에이전트 → 상위 에이전트 | + +### API 엔드포인트 추가 +``` +GET /api/agents/{id}/messages 수신 메시지 목록 +POST /api/agents/{id}/messages 메시지 전송 +PATCH /api/agents/messages/{msg_id}/read 읽음 처리 +``` + +--- + +## 3. KB 품질 검토 자동화 + +### 목적 +KB_CURATOR가 생성한 초안에 자동으로 품질 점수를 부여하고, 기준 이상이면 자동 발행한다. + +### 품질 평가 기준 +| 항목 | 가중치 | 설명 | +|------|--------|------| +| completeness | 40% | 증상/원인/해결책 3섹션 완성도 | +| clarity | 30% | 문장 명확성 및 가독성 | +| actionability | 30% | 실제 조치 가능한 구체적 내용 여부 | + +### 동작 흐름 +```python +# kb_curator 핸들러 확장 +quality = await llm.json_generate( + quality_prompt(kb_doc), + agent.llm_model +) +# quality = {"completeness": 85, "clarity": 78, "actionability": 82, "overall": 82} + +kb_doc.quality_score = quality.get("overall", 0) +if kb_doc.quality_score >= 80: + kb_doc.published = True # 자동 발행 + approval.status = "AUTO_APPROVED" +else: + approval.status = "PENDING" # 수동 검토 필요 +``` + +### 모델 변경 (models.py) +`KBDocument`에 `quality_score: int` 컬럼 추가 + +--- + +## 4. WBS 지연 완료 예측 + +### 목적 +WBS_MONITOR 핸들러 확장. 완료율 추이를 분석하여 프로젝트 완료 예상일을 계산한다. + +### 예측 알고리즘 +```python +# 최근 7일간 완료율 기울기 계산 +prev_rate = wbs_snapshot_7days_ago.completion_rate # 예: 45% +curr_rate = current_completion_rate # 예: 52% +daily_delta = (curr_rate - prev_rate) / 7 # 1%/일 + +remaining = 100 - curr_rate # 48% +est_days = remaining / daily_delta if daily_delta > 0 else 999 +est_completion = date.today() + timedelta(days=est_days) + +# planned_end_date와 비교 → 초과 시 리스크 등록 +if est_completion > project.planned_end_date: + delay_days = (est_completion - project.planned_end_date).days + # WBS 리스크 자동 등록 +``` + +### 저장 위치 +`si_project.estimated_completion` 컬럼 업데이트 (또는 AgentTask에 기록) + +--- + +## 5. 에이전트 파인튜닝 파이프라인 + +### 목적 +누적된 `tb_agent_task(COMPLETED)` 데이터를 기반으로 Ollama 커스텀 모델을 파인튜닝한다. + +### 파이프라인 단계 + +``` +1. 데이터 수집 + SELECT input_data, output_data FROM tb_agent_task + WHERE status='COMPLETED' AND tokens_used > 0 + LIMIT 1000 + +2. JSONL 파일 생성 (Ollama fine-tune 포맷) + {"prompt": "...", "response": "..."} + → /opt/guardia/finetune/guardia-agent-v2.jsonl + +3. Modelfile 생성 + FROM guardia-agent + TRAIN /opt/guardia/finetune/guardia-agent-v2.jsonl + +4. ollama create 실행 + ollama create guardia-agent-v2 -f Modelfile.guardia-v2 + +5. 헬스체크 후 active model 전환 + AgentConfig.llm_model = "guardia-agent-v2" +``` + +### API 엔드포인트 +``` +POST /api/agents/finetune/start 파인튜닝 시작 (ADMIN only) +GET /api/agents/finetune/status 진행 상태 조회 +``` + +### 구현 위치 +`core/llm_client.py`에 `fine_tune()` 메서드 추가 + +```python +async def fine_tune(self, dataset_path: str, model_name: str) -> bool: + """Ollama 모델 파인튜닝 실행""" + proc = await asyncio.create_subprocess_exec( + "ollama", "create", model_name, + "-f", f"Modelfile.{model_name}", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + stdout, stderr = await proc.communicate() + return proc.returncode == 0 +``` diff --git a/12_확장개발_Priority4_인프라.md b/12_확장개발_Priority4_인프라.md new file mode 100644 index 0000000..a31474d --- /dev/null +++ b/12_확장개발_Priority4_인프라.md @@ -0,0 +1,175 @@ +# GUARDiA ITSM — Priority 4: 인프라 / 플랫폼 + +**문서 버전**: 1.0 | **작성일**: 2026-05-25 + +--- + +## 1. PostgreSQL 마이그레이션 + +### database.py 변경 + +```python +import os +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession +from sqlalchemy.orm import sessionmaker + +# 환경변수 기반 DB 자동 선택 +DATABASE_URL = os.getenv( + "DATABASE_URL", + "sqlite+aiosqlite:///./guardia.db" # 기본: SQLite (개발) +) +# PostgreSQL 예시: +# postgresql+asyncpg://guardia:password@localhost:5432/guardia + +engine = create_async_engine( + DATABASE_URL, + echo=False, + pool_pre_ping=True, + pool_size=10 if "postgresql" in DATABASE_URL else 5, + max_overflow=20 if "postgresql" in DATABASE_URL else 0, +) +SessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) +``` + +### Alembic 설정 + +```bash +# 설치 +pip install alembic asyncpg + +# 초기화 +cd C:\GUARDiA\itsm +alembic init migrations + +# alembic.ini 수정 +# sqlalchemy.url = postgresql+asyncpg://user:pass@localhost:5432/guardia + +# 첫 마이그레이션 생성 +alembic revision --autogenerate -m "initial_schema" + +# 적용 +alembic upgrade head +``` + +### 마이그레이션 파일 위치 +``` +itsm/ +├── alembic.ini +└── migrations/ + ├── env.py + └── versions/ + └── 001_initial_schema.py +``` + +### SQLite → PostgreSQL 차이점 주의사항 + +| 항목 | SQLite | PostgreSQL | +|------|--------|-----------| +| JSON 컬럼 | 문자열 저장 | JSONB (인덱싱 가능) | +| 날짜 함수 | `strftime()` | `date_trunc()`, `extract()` | +| 자동 증가 | `INTEGER PRIMARY KEY` | `SERIAL` 또는 `BIGSERIAL` | +| 동시성 | 파일 잠금 | MVCC (완전 동시 지원) | + +--- + +## 2. 멀티테넌트 지원 + +### 설계 방식: 공유 스키마 + tenant_id 컬럼 + +```python +# models.py — 주요 테이블에 tenant_id 추가 +class Institution(Base): + tenant_id: str = Column(String(20), nullable=False, index=True) + +class SRRequest(Base): + tenant_id: str = Column(String(20), nullable=False, index=True) + +class Server(Base): + tenant_id: str = Column(String(20), nullable=False, index=True) +``` + +### JWT에 tenant_id 포함 + +```python +# core/auth.py +def create_access_token(user: User) -> str: + payload = { + "sub": str(user.id), + "role": user.role, + "tenant_id": user.inst_code, # 기관코드를 tenant_id로 사용 + "exp": ... + } + return jwt.encode(payload, SECRET_KEY, algorithm="HS256") +``` + +### 자동 필터링 미들웨어 + +```python +# 모든 조회 쿼리에 tenant_id 자동 추가 +async def get_tenant_db(current_user: User = Depends(get_current_user)): + async with SessionLocal() as db: + db.tenant_id = current_user.tenant_id + yield db +``` + +--- + +## 3. RBAC 세분화 + +### 신규 역할: DEPLOY_ENGINEER + +```python +class UserRole(str, Enum): + ADMIN = "ADMIN" + PM = "PM" + ENGINEER = "ENGINEER" + DEPLOY_ENGINEER = "DEPLOY_ENGINEER" # 신규 + CUSTOMER = "CUSTOMER" +``` + +### 권한 매핑 + +| 기능 | ADMIN | PM | ENGINEER | DEPLOY_ENGINEER | CUSTOMER | +|------|-------|-----|---------|-----------------|---------| +| SR 조회 | ✅ | ✅ | ✅ | ✅ | ✅ (자신만) | +| SR 생성 | ✅ | ✅ | ✅ | ✅ | ✅ | +| 배포 트리거 | ✅ | ✅ | ❌ | ✅ | ❌ | +| 배치 즉시 실행 | ✅ | ❌ | ❌ | ✅ | ❌ | +| 서버 자격증명 조회 | ✅ | ❌ | ❌ | ❌ | ❌ | +| 에이전트 관리 | ✅ | ❌ | ❌ | ❌ | ❌ | +| 사용자 관리 | ✅ | ❌ | ❌ | ❌ | ❌ | + +### 구현 + +```python +# core/auth.py +DEPLOY_ROLES = {UserRole.ADMIN, UserRole.PM, UserRole.DEPLOY_ENGINEER} + +def require_deploy_role(current_user: User = Depends(get_current_user)): + if current_user.role not in DEPLOY_ROLES: + raise HTTPException(403, "배포 권한이 없습니다") + return current_user +``` + +--- + +## 4. 배포 이력 UI + +### vibe.html 내 배포 이력 탭 추가 + +- Jenkins 빌드 번호별 조회 +- 아티팩트 목록 (다운로드 링크) +- 롤백 버튼 (이전 빌드로 롤백) + +### Jenkins 빌드 이력 API (core/cicd.py) + +```python +async def get_build_history(project_name: str, limit: int = 10) -> list[BuildInfo]: + """Jenkins 빌드 이력 조회""" + url = f"{JENKINS_URL}/job/{project_name}/api/json?tree=builds[number,result,timestamp,duration,artifacts[*]]" + ... + +async def get_artifact_url(project_name: str, build_number: int, artifact: str) -> str: + """빌드 아티팩트 다운로드 URL 반환""" + return f"{JENKINS_URL}/job/{project_name}/{build_number}/artifact/{artifact}" +``` diff --git a/13_확장개발_Priority5_외부연동.md b/13_확장개발_Priority5_외부연동.md new file mode 100644 index 0000000..f0260be --- /dev/null +++ b/13_확장개발_Priority5_외부연동.md @@ -0,0 +1,243 @@ +# GUARDiA ITSM — Priority 5: 외부 시스템 연동 고도화 + +**문서 버전**: 1.0 | **작성일**: 2026-05-25 + +--- + +## 1. Jenkins 운영 배포 승인 연동 + +### 목적 +Jenkins Declarative Pipeline의 `input()` 단계와 ITSM 승인 API를 양방향으로 연동한다. +운영(prd) 배포 시 ITSM에서 PM이 승인해야만 Jenkins 파이프라인이 진행된다. + +### 흐름 + +``` +GUARDiA Vibe 세션 → "배포(prd)" 버튼 클릭 + │ + ▼ POST /api/vibe/{id}/deploy { environment: "prd" } + │ + ▼ Jenkins Job 트리거 (core/cicd.py) + │ +Jenkins Pipeline: + stage('Request ITSM Approval') { + steps { + sh """ + curl -X POST ${ITSM_URL}/api/vibe/${SESSION_ID}/request-approval \ + -H "Authorization: Bearer ${ITSM_TOKEN}" \ + -d '{"environment": "prd", "build_number": ${BUILD_NUMBER}}' + """ + } + } + stage('Wait for ITSM Approval') { + steps { + timeout(time: 60, unit: 'MINUTES') { + waitUntil { + def result = sh( + script: "curl -s ${ITSM_URL}/api/vibe/${SESSION_ID}/approval-status", + returnStdout: true + ).trim() + return result == '"APPROVED"' + } + } + } + } + stage('Deploy to Production') { ... } +``` + +### ITSM API 신규 엔드포인트 + +``` +POST /api/vibe/{id}/request-approval + → VibeDeploy 승인 요청 생성 (SRApproval 또는 AgentApproval 재사용) + → PM에게 메신저 알림 + +GET /api/vibe/{id}/approval-status + → "PENDING" | "APPROVED" | "REJECTED" + +PATCH /api/vibe/{id}/approve + → PM이 ITSM UI에서 승인 처리 + → Jenkins 폴링 응답에 "APPROVED" 반환 +``` + +--- + +## 2. SonarQube Quality Gate 연동 + +### 목적 +Jenkins 빌드 완료 후 SonarQube 분석 결과를 ITSM에 자동 등록한다. +Quality Gate 실패 시 SR을 생성하여 개발팀에 통보한다. + +### core/cicd.py 추가 함수 + +```python +async def get_sonarqube_result(project_key: str) -> SonarResult: + """SonarQube Quality Gate 결과 조회""" + url = f"{SONARQUBE_URL}/api/qualitygates/project_status?projectKey={project_key}" + async with httpx.AsyncClient() as client: + r = await client.get(url, headers={"Authorization": f"Bearer {SONARQUBE_TOKEN}"}) + data = r.json() + status = data["projectStatus"]["status"] # OK | WARN | ERROR + + metrics = { + c["metricKey"]: c["value"] + for c in data["projectStatus"]["conditions"] + } + return SonarResult( + status=status, + coverage=metrics.get("coverage", "N/A"), + bugs=metrics.get("bugs", "0"), + vulnerabilities=metrics.get("vulnerabilities", "0"), + code_smells=metrics.get("code_smells", "0"), + ) + +async def handle_sonar_gate_failure(session_id: int, result: SonarResult, db: AsyncSession): + """Quality Gate 실패 시 SR 자동 생성""" + if result.status == "ERROR": + sr = SRRequest( + title=f"SonarQube Quality Gate 실패 — 세션 {session_id}", + description=f"취약점: {result.vulnerabilities}, 버그: {result.bugs}, 커버리지: {result.coverage}%", + sr_type="DEPLOY", + priority="HIGH", + ) + db.add(sr) + await db.commit() +``` + +### Jenkins 연동 (Jenkinsfile에 추가) + +```groovy +stage('SonarQube Analysis') { + steps { + withSonarQubeEnv('SonarQube') { + sh 'mvn sonar:sonar -Dsonar.projectKey=${PROJECT_KEY}' + } + } +} +stage('Quality Gate') { + steps { + timeout(time: 5, unit: 'MINUTES') { + waitForQualityGate abortPipeline: false + } + // ITSM에 결과 전송 + sh """ + curl -X POST ${ITSM_URL}/api/vibe/sonar-result \ + -d '{"session_id": ${SESSION_ID}, "project_key": "${PROJECT_KEY}"}' + """ + } +} +``` + +### 환경 변수 +```bash +SONARQUBE_URL=http://sonar.agency.go.kr:9000 +SONARQUBE_TOKEN= +``` + +--- + +## 3. SSL 자동 갱신 (Let's Encrypt) + +### 목적 +certbot과 연동하여 SSL 인증서를 자동으로 갱신하고, 결과를 ITSM에 기록한다. + +### scripts/sm/ssl/ssl_auto_renew.sh + +```bash +#!/bin/bash +# SSL 자동 갱신 스크립트 (서버에서 실행) +set -euo pipefail + +ITSM_URL="${ITSM_URL:-http://localhost:8001}" +SERVER_ID="${SERVER_ID:-}" +ITSM_TOKEN="${ITSM_TOKEN:-}" + +# certbot 갱신 +if certbot renew --quiet --non-interactive \ + --deploy-hook "systemctl reload nginx 2>/dev/null || systemctl reload apache2 2>/dev/null"; then + + # 새 만료일 조회 + NEW_EXPIRE=$(openssl x509 -in /etc/letsencrypt/live/*/cert.pem -noout -enddate \ + | cut -d= -f2) + NEW_EXPIRE_ISO=$(date -d "$NEW_EXPIRE" +"%Y-%m-%d") + + # ITSM에 갱신 기록 + curl -s -X POST "${ITSM_URL}/api/ssl/renew/${SERVER_ID}" \ + -H "Authorization: Bearer ${ITSM_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{\"new_expire_date\": \"${NEW_EXPIRE_ISO}\", \"renewed_by\": \"certbot-auto\", \"notes\": \"Let's Encrypt 자동 갱신\"}" + + echo "[OK] SSL 갱신 완료: ${NEW_EXPIRE_ISO}" +else + echo "[WARN] SSL 갱신 불필요 (아직 유효기간 충분)" +fi +``` + +### 배치 작업 등록 + +`tb_batch_job`에 자동 갱신 배치 잡 등록: +- `cron_expr`: `0 3 * * *` (매일 03:00) +- `command`: `SSL_WATCHER=true bash /opt/guardia/scripts/ssl/ssl_auto_renew.sh` +- `alert_on_fail`: true + +--- + +## 4. SSE 스트리밍 확장 + +### 목적 +배치 실행 로그와 빌드 로그를 SSE로 실시간 스트리밍한다. + +### core/events.py 확장 + +```python +# 배치 실행 로그 스트리밍 +async def stream_batch_log(run_id: int) -> AsyncGenerator[str, None]: + """tb_batch_run.stdout 청크 단위 SSE 전송""" + last_len = 0 + for _ in range(300): # 최대 5분 (1초 간격) + async with SessionLocal() as db: + run = await db.get(BatchRun, run_id) + if run.stdout and len(run.stdout) > last_len: + new_data = run.stdout[last_len:] + last_len = len(run.stdout) + yield f"data: {json.dumps({'chunk': new_data})}\n\n" + if run.status not in ("RUNNING",): + yield f"data: {json.dumps({'done': True, 'status': run.status})}\n\n" + break + await asyncio.sleep(1) + +# Jenkins 빌드 로그 스트리밍 +async def stream_build_log(session_id: int) -> AsyncGenerator[str, None]: + """Jenkins Build Log API 폴링 → SSE 전송""" + # GET {JENKINS_URL}/job/{name}/{number}/logText/progressiveText?start=0 + offset = 0 + while True: + log_chunk, next_offset = await cicd.get_progressive_log(session_id, offset) + if log_chunk: + yield f"data: {json.dumps({'chunk': log_chunk, 'offset': next_offset})}\n\n" + offset = next_offset + done = await cicd.is_build_complete(session_id) + if done: + yield f"data: {json.dumps({'done': True})}\n\n" + break + await asyncio.sleep(2) +``` + +### 신규 엔드포인트 + +``` +GET /api/batch/runs/{run_id}/stream 배치 로그 실시간 스트리밍 (SSE) +GET /api/vibe/{id}/build/stream 빌드 로그 실시간 스트리밍 (SSE) +``` + +### 프론트엔드 연결 + +```javascript +// batch.html — 실행 로그 스트리밍 +const es = new EventSource(`/api/batch/runs/${runId}/stream?token=${token}`); +es.onmessage = (e) => { + const data = JSON.parse(e.data); + if (data.chunk) logBox.textContent += data.chunk; + if (data.done) { es.close(); updateRunStatus(data.status); } +}; +``` diff --git a/14_라이선스_키_발급_가이드.md b/14_라이선스_키_발급_가이드.md new file mode 100644 index 0000000..07d6cb2 --- /dev/null +++ b/14_라이선스_키_발급_가이드.md @@ -0,0 +1,647 @@ +# GUARDiA ITSM — 라이선스 키 발급 및 적용 가이드 + +> **문서번호**: GUARDIA-LIC-001 +> **버전**: 1.0 +> **작성일**: 2026-05-28 +> **작성자**: GUARDiA 개발팀 +> **보안등급**: 내부용 (대외비) — 마스터 키 및 생성 절차는 벤더 내부 한정 + +--- + +## 목차 + +1. [라이선스 시스템 개요](#1-라이선스-시스템-개요) +2. [에디션별 제한 및 기능](#2-에디션별-제한-및-기능) +3. [마스터 키 생성 및 보관](#3-마스터-키-생성-및-보관) +4. [라이선스 키 발급 (벤더 전용)](#4-라이선스-키-발급-벤더-전용) +5. [라이선스 키 고객 적용 절차](#5-라이선스-키-고객-적용-절차) +6. [라이선스 갱신 절차](#6-라이선스-갱신-절차) +7. [라이선스 상태 모니터링](#7-라이선스-상태-모니터링) +8. [CLI 레퍼런스](#8-cli-레퍼런스) +9. [API 레퍼런스](#9-api-레퍼런스) +10. [문제 해결](#10-문제-해결) + +--- + +## 1. 라이선스 시스템 개요 + +### 1.1 보안 설계 + +GUARDiA 라이선스는 **완전 오프라인 검증** 방식으로, 외부 인터넷 연결 없이 검증된다. + +``` +라이선스 키 구조: + GRD-{base64url(iv[12B] + ciphertext + gcm_tag[16B] + hmac_sig[8B])} + +암호화: AES-256-GCM (iv 12B + GCM tag 16B) +서명: HMAC-SHA256 (앞 8B prefix — 위변조 즉시 탐지) +키 파생: SHA-256(master_key + "guardia-aes-v1") → AES 키 + SHA-256(master_key + "guardia-license-hmac-v1") → HMAC 키 +``` + +**검증 흐름:** +``` +고객 서버가 키 수신 + │ + ▼ HMAC 서명 검증 (위변조 즉시 차단) + │ + ▼ AES-256-GCM 복호화 (마스터 키 불일치 시 실패) + │ + ▼ JSON 페이로드 파싱 (만료일, 에디션, 고객명 등) + │ + ▼ 만료일 비교 → valid: true / expired: true +``` + +### 1.2 라이선스 페이로드 구성 + +```json +{ + "license_id": "GRD-A1B2C3", + "edition": "ENTERPRISE", + "customer": "서울특별시청", + "issued_at": "2026-05-28T00:00:00+00:00", + "expires_at": "2027-05-28T00:00:00+00:00", + "limits": { + "max_institutions": -1, + "max_users": -1, + "max_servers": -1, + "features": ["MFA", "LDAP", "PAM", "AI_AGENTS", + "VULN_SCAN", "CICD", "ANALYTICS", "FINOPS"] + } +} +``` + +--- + +## 2. 에디션별 제한 및 기능 + +| 에디션 | 기관 수 | 사용자 수 | 서버 수 | 활성화 기능 | 대상 | +|--------|--------|---------|--------|-----------|------| +| **COMMUNITY** | 1 | 10 | 20 | MFA | 소규모 단일 기관, 평가판 | +| **STANDARD** | 50 | 200 | 500 | MFA, LDAP, PAM, AI_AGENTS | 중규모 기관·지자체 | +| **ENTERPRISE** | 무제한(-1) | 무제한(-1) | 무제한(-1) | 전체 기능 | 광역단체, 대규모 멀티테넌트 | + +### 기능별 에디션 요구사항 + +| 기능 코드 | 기능명 | 최소 에디션 | +|----------|-------|-----------| +| `MFA` | 2단계 인증 (TOTP/OTP) | COMMUNITY | +| `LDAP` | LDAP/AD 디렉토리 연동 | STANDARD | +| `PAM` | 특권 접근 관리 | STANDARD | +| `AI_AGENTS` | AI 에이전트 오케스트레이션 | STANDARD | +| `VULN_SCAN` | 보안 취약점 자동 스캔 | ENTERPRISE | +| `CICD` | CI/CD Jenkins 연동 | ENTERPRISE | +| `ANALYTICS` | 고급 분석 대시보드 | ENTERPRISE | +| `FINOPS` | 비용 분석 (FinOps) | ENTERPRISE | + +--- + +## 3. 마스터 키 생성 및 보관 + +> **경고**: 마스터 키는 절대 외부 유출 금지. 분실 시 기존 발급 라이선스 모두 무효화됨. + +### 3.1 마스터 키 생성 + +마스터 키는 GUARDiA 벤더 내부 시스템에서 **1회 생성**하여 안전하게 보관한다. + +```bash +# 방법 1: Python (권장) +python3 -c "import secrets; print(secrets.token_hex(32))" + +# 방법 2: OpenSSL +openssl rand -hex 32 + +# 결과 예시 (64자리 hex = 32바이트): +# a3f8c2d1e5b4970682f1a9c3d7e2b5f4180c6e9a2d4b7f0e3c8a5d2b1f9e607 +``` + +### 3.2 마스터 키 보관 정책 + +| 보관 방법 | 설명 | +|---------|------| +| **운영 서버** | 환경변수 `GUARDIA_LICENSE_KEY` — systemd 서비스 파일 또는 .env (권한 600) | +| **비상 백업** | 오프라인 금고 (USB 암호화 드라이브) | +| **접근 권한** | 벤더 최고 관리자 2인 이상 알 고리즘 — 단독 접근 금지 | + +### 3.3 .env 파일 설정 (운영 서버) + +```ini +# C:\GUARDiA\itsm\.env (권한: 소유자 읽기/쓰기만) +GUARDIA_LICENSE_KEY=a3f8c2d1e5b4970682f1a9c3d7e2b5f4180c6e9a2d4b7f0e3c8a5d2b1f9e607 +``` + +```bash +# Linux: 파일 권한 보호 +chmod 600 /opt/guardia/itsm/.env +chown guardia:guardia /opt/guardia/itsm/.env +``` + +--- + +## 4. 라이선스 키 발급 (벤더 전용) + +> 이 절차는 GUARDiA 벤더(개발사) 내부 운영자만 실행한다. + +### 4.1 발급 전 확인 사항 + +``` +□ 고객명 (정식 기관명, 계약서 표기 명칭과 동일) +□ 에디션 (COMMUNITY / STANDARD / ENTERPRISE) +□ 유효 기간 (일수) 예: 365일(1년), 730일(2년) +□ 커스텀 한도 여부 (특수 계약 시: max_institutions, max_users, max_servers 별도 지정) +□ GUARDIA_LICENSE_KEY 환경변수 설정 확인 +``` + +### 4.2 CLI를 이용한 발급 + +```bash +# 발급 서버(벤더 내부 PC)에서 실행 +cd C:\GUARDiA\itsm + +# 방법 1: 환경변수에 마스터 키 설정 후 발급 +$env:GUARDIA_LICENSE_KEY = "a3f8c2d1e5b4970682f1a9c3d7e2b5f4180c6e9a2d4b7f0e3c8a5d2b1f9e607" +python -m core.license --customer "서울특별시청" --edition ENTERPRISE --days 365 + +# 방법 2: --key 인수로 직접 지정 (환경변수 없어도 가능) +python -m core.license \ + --customer "인천광역시청" \ + --edition STANDARD \ + --days 730 \ + --key a3f8c2d1e5b4970682f1a9c3d7e2b5f4180c6e9a2d4b7f0e3c8a5d2b1f9e607 + +# 커스텀 라이선스 ID 지정 (선택) +python -m core.license --customer "부산광역시청" --edition ENTERPRISE --days 365 --lid GRD-BUSAN2026 +``` + +### 4.3 발급 결과 예시 + +``` +============================================================ + GUARDiA 라이선스 키 생성 완료 +============================================================ + 고객명 : 서울특별시청 + 에디션 : ENTERPRISE + 라이선스ID: GRD-A1B2C3 + 발급일시 : 2026-05-28T00:00:00+00:00 + 만료일시 : 2027-05-28T00:00:00+00:00 + 유효기간 : 365일 +============================================================ + +라이선스 키: +GRD-eyJsaWNlbnNlX2lkIjoiR1JELUExQjJDMyIsImVkaXRpb24iOiJFTlRFUlBSSVNFIn0... +``` + +### 4.4 Python 코드를 이용한 발급 (자동화) + +```python +from datetime import datetime, timedelta, timezone +from core.license import generate_license_key, validate_license, LicenseEdition + +MASTER_KEY = "a3f8c2d1e5b4970682f1a9c3d7e2b5f4180c6e9a2d4b7f0e3c8a5d2b1f9e607" + +# 1년짜리 ENTERPRISE 라이선스 생성 +lic_key = generate_license_key( + customer = "서울특별시청", + edition = LicenseEdition.ENTERPRISE, + expires_at = datetime.now(timezone.utc) + timedelta(days=365), + master_key_hex = MASTER_KEY, +) +print(f"키: {lic_key}") + +# 검증 +status = validate_license(lic_key, master_key_hex=MASTER_KEY) +print(f"유효: {status['valid']}, 남은 일수: {status['days_remaining']}") +``` + +### 4.5 커스텀 한도 지정 (특수 계약) + +```python +custom_limits = { + "max_institutions": 10, # 기관 10개 + "max_users": 100, # 사용자 100명 + "max_servers": 200, # 서버 200대 + "features": ["MFA", "LDAP", "PAM"], +} + +lic_key = generate_license_key( + customer = "경기도청", + edition = LicenseEdition.STANDARD, + expires_at = datetime.now(timezone.utc) + timedelta(days=365), + custom_limits = custom_limits, + master_key_hex = MASTER_KEY, +) +``` + +--- + +## 5. 라이선스 키 고객 적용 절차 + +### 5.1 방법 A: 웹 UI (권장) + +1. ITSM 관리자 계정으로 로그인 +2. 사이드바 **🔏 라이선스 관리** 클릭 → `/license` +3. **라이선스 키 등록/갱신** 섹션에 `GRD-...` 키 붙여넣기 +4. **라이선스 등록** 버튼 클릭 +5. 상단 배너에서 에디션·만료일 확인 + +``` +[상태 배너 예시] +✅ ENTERPRISE 라이선스 활성 — 365일 남음 (서울특별시청) +``` + +### 5.2 방법 B: API (자동화/스크립트) + +```bash +# 1. 관리자 로그인 후 토큰 획득 +TOKEN=$(curl -s -X POST http://localhost:8001/api/auth/login \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"Admin!1234"}' \ + | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])") + +# 2. 라이선스 등록 +curl -X POST http://localhost:8001/api/license/activate \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "license_key": "GRD-eyJsaWNlbnNlX2lkIjoiR1JELUExQjJDMyI..." + }' + +# 성공 응답 예시: +# { +# "message": "ENTERPRISE 라이선스가 활성화되었습니다 (365일 남음).", +# "license_id": "GRD-A1B2C3", +# "edition": "ENTERPRISE", +# "customer": "서울특별시청", +# "expires_at": "2027-05-28T00:00:00+00:00", +# "days_remaining": 365, +# "valid": true +# } +``` + +### 5.3 방법 C: .env 파일 사전 구성 (설치 시점 적용) + +배포 자동화 파이프라인에서 설치 시점에 라이선스를 미리 구성할 때: + +```ini +# C:\GUARDiA\itsm\.env +SECRET_KEY= +GUARDIA_LICENSE_KEY=<마스터_키_64자리_hex> +``` + +서버 기동 시 ITSM이 자동으로 라이선스 상태를 출력한다: +``` +[LICENSE] ENTERPRISE 라이선스 활성 (365일 남음) — 서울특별시청 +``` + +단, `.env`에 마스터 키만 있고 DB에 라이선스 레코드가 없으면 활성화는 안 된다. +**DB에 키를 등록하는 것은 API 호출(방법 A/B) 필수.** + +--- + +## 6. 라이선스 갱신 절차 + +### 6.1 갱신 타이밍 + +| 시점 | 시스템 반응 | 권장 조치 | +|------|-----------|---------| +| 만료 30일 전 | 웹 UI 황색 배너 경고, 서버 시작 시 경고 로그 | 갱신 라이선스 발급 준비 | +| 만료 7일 전 | 매일 경고 | 즉시 갱신 신청 | +| 만료 당일 | `expired: true`, 기능 제한 시작 | 긴급 갱신 | +| 만료 후 | 기존 데이터 보존, 신규 등록만 차단 | 갱신 라이선스 즉시 적용 | + +> **중요**: 만료 후에도 기존 등록된 기관·서버·사용자 데이터는 삭제되지 않는다. +> 신규 생성(기관 추가, 서버 추가)만 한도 초과 시 차단된다. + +### 6.2 갱신 라이선스 발급 + +동일 CLI로 새 키를 발급하면 된다. 라이선스 ID는 새로 자동 생성된다: + +```bash +python -m core.license \ + --customer "서울특별시청" \ + --edition ENTERPRISE \ + --days 365 \ + --key <마스터_키> + +# 기존 라이선스는 자동 비활성화되고 새 라이선스로 교체된다. +``` + +### 6.3 갱신 적용 + +**웹 UI**: `/license` → 새 `GRD-...` 키 입력 → **라이선스 등록** +(기존 활성 라이선스는 자동으로 비활성화됨) + +**API**: +```bash +curl -X POST http://localhost:8001/api/license/activate \ + -H "Authorization: Bearer $TOKEN" \ + -d '{"license_key": "GRD-<새_키>"}' +``` + +--- + +## 7. 라이선스 상태 모니터링 + +### 7.1 웹 UI 상태 배너 + +`/license` 페이지의 상단 배너로 즉시 확인: + +| 배너 색상 | 의미 | +|---------|------| +| 초록 | 정상 활성 | +| 노랑 | 만료 30일 이내 경고 | +| 빨강 | 만료됨 | +| 회색 | 라이선스 미등록 (Community 제한 모드) | + +### 7.2 사용량 현황 + +`/license` 페이지 **사용량 현황** 섹션에서 기관/사용자/서버 현재 수 vs 한도를 확인할 수 있다: + +``` +기관 [██████████░] 48/50 +사용자 [████░░░░░░░] 80/200 +서버 [███░░░░░░░░] 150/500 +``` + +### 7.3 API로 상태 확인 + +```bash +curl http://localhost:8001/api/license/status \ + -H "Authorization: Bearer $TOKEN" +``` + +```json +{ + "activated": true, + "valid": true, + "expired": false, + "expiry_warning": false, + "license_id": "GRD-A1B2C3", + "edition": "ENTERPRISE", + "customer": "서울특별시청", + "issued_at": "2026-05-28T00:00:00+00:00", + "expires_at": "2027-05-28T00:00:00+00:00", + "days_remaining": 365, + "limits": { + "max_institutions": -1, + "max_users": -1, + "max_servers": -1, + "features": ["MFA","LDAP","PAM","AI_AGENTS","VULN_SCAN","CICD","ANALYTICS","FINOPS"] + }, + "message": "ENTERPRISE 라이선스 활성 (365일 남음)" +} +``` + +### 7.4 서버 로그 모니터링 + +```bash +# 시스템 기동 시 라이선스 상태 로그 확인 +journalctl -u guardia-itsm -n 50 | grep LICENSE + +# 정상: +# [LICENSE] ENTERPRISE 라이선스 활성 (365일 남음) — 서울특별시청 + +# 경고: +# [LICENSE] 경고: 라이선스 만료 25일 남음 + +# 만료: +# [LICENSE] 경고: 라이선스가 만료되었습니다. 갱신이 필요합니다. + +# 미설정: +# [LICENSE] GUARDIA_LICENSE_KEY 미설정 — Community 모드로 실행됩니다. +``` + +--- + +## 8. CLI 레퍼런스 + +``` +usage: python -m core.license [-h] --customer CUSTOMER + --edition {COMMUNITY,STANDARD,ENTERPRISE} + --days DAYS + [--lid LID] + [--key KEY] + +GUARDiA 라이선스 키 생성 도구 + +필수 인수: + --customer 고객/기관 이름 (예: "서울특별시청") + --edition 라이선스 에디션: COMMUNITY | STANDARD | ENTERPRISE + --days 유효 기간 (일, 예: 365) + +선택 인수: + --lid 라이선스 ID 직접 지정 (기본: GRD-{6자리랜덤hex}) + --key 마스터 키 hex 64자리 (기본: GUARDIA_LICENSE_KEY 환경변수) + -h, --help 도움말 출력 +``` + +### 사용 예시 + +```bash +# ENTERPRISE 1년 +python -m core.license --customer "서울특별시청" --edition ENTERPRISE --days 365 + +# STANDARD 2년 +python -m core.license --customer "경기도청" --edition STANDARD --days 730 + +# COMMUNITY 30일 평가판 +python -m core.license --customer "테스트기관" --edition COMMUNITY --days 30 + +# 마스터 키 직접 지정 (환경변수 없을 때) +python -m core.license --customer "부산광역시청" --edition ENTERPRISE --days 365 \ + --key a3f8c2d1e5b4970682f1a9c3d7e2b5f4180c6e9a2d4b7f0e3c8a5d2b1f9e607 + +# 커스텀 라이선스 ID +python -m core.license --customer "인천광역시청" --edition STANDARD --days 365 \ + --lid GRD-INCHEON26 +``` + +--- + +## 9. API 레퍼런스 + +모든 API는 `Authorization: Bearer ` 헤더 필요. +ADMIN 전용 API는 ADMIN 역할 계정만 호출 가능. + +### GET /api/license/status + +현재 라이선스 상태 조회 (로그인한 모든 사용자 접근 가능). + +**응답:** +```json +{ + "activated": true, + "valid": true, + "expired": false, + "expiry_warning": false, + "license_id": "GRD-A1B2C3", + "edition": "ENTERPRISE", + "customer": "서울특별시청", + "issued_at": "2026-05-28T00:00:00+00:00", + "expires_at": "2027-05-28T00:00:00+00:00", + "days_remaining": 365, + "limits": { ... }, + "message": "ENTERPRISE 라이선스 활성 (365일 남음)" +} +``` + +--- + +### POST /api/license/activate + +라이선스 키 등록 및 활성화 **(ADMIN 전용)**. + +**요청 본문:** +```json +{ "license_key": "GRD-eyJ..." } +``` + +**응답 (성공):** +```json +{ + "message": "ENTERPRISE 라이선스가 활성화되었습니다 (365일 남음).", + "license_id": "GRD-A1B2C3", + "edition": "ENTERPRISE", + "customer": "서울특별시청", + "expires_at": "2027-05-28T00:00:00+00:00", + "days_remaining": 365, + "valid": true +} +``` + +**오류:** +| 코드 | 원인 | +|------|------| +| 400 | 키 형식 오류, 서명 불일치, 복호화 실패 | +| 403 | ADMIN 권한 없음 | +| 500 | GUARDIA_LICENSE_KEY 환경변수 미설정 | + +--- + +### POST /api/license/verify + +등록 없이 키 검증만 수행 **(ADMIN 전용)**. + +**요청 본문:** `{ "license_key": "GRD-eyJ..." }` + +**응답:** 라이선스 페이로드 전체 반환 (status와 동일 구조) + +--- + +### DELETE /api/license/ + +활성 라이선스 비활성화 **(ADMIN 전용)**. +시스템이 Community 제한 모드로 전환된다. + +**응답:** `{ "message": "라이선스가 비활성화되었습니다..." }` + +--- + +### GET /api/license/history + +등록 이력 전체 조회 **(ADMIN 전용)**. + +**응답:** +```json +[ + { + "id": 2, + "license_id": "GRD-A1B2C3", + "edition": "ENTERPRISE", + "customer": "서울특별시청", + "issued_at": "2026-05-28T00:00:00", + "expires_at": "2027-05-28T00:00:00", + "is_active": true, + "activated_by": "admin", + "activated_at": "2026-05-28T09:00:00" + }, + { + "id": 1, + "license_id": "GRD-OLD001", + "edition": "STANDARD", + "is_active": false, + ... + } +] +``` + +--- + +## 10. 문제 해결 + +### 10.1 "GUARDIA_LICENSE_KEY 환경변수가 설정되지 않았습니다" + +``` +원인: 서버 .env 파일에 GUARDIA_LICENSE_KEY가 없거나 서비스가 .env를 읽지 못함 +해결: + 1. .env 파일에 키 추가: + GUARDIA_LICENSE_KEY=<64자리 hex> + 2. 서비스 재시작: + systemctl restart guardia-itsm (Linux) + Restart-Service GUARDiA-ITSM (Windows) +``` + +### 10.2 "라이선스 서명 검증 실패 — 위변조 또는 잘못된 키" + +``` +원인 1: 키가 다른 마스터 키로 생성됨 (고객사 서버의 GUARDIA_LICENSE_KEY와 발급 시 키가 다름) +원인 2: 키 문자열이 복사 중 잘림 +해결: + 1. 키를 처음부터 끝까지 정확히 복사했는지 확인 (앞뒤 공백 없이) + 2. 발급 시 사용한 마스터 키 == 운영 서버의 GUARDIA_LICENSE_KEY 확인 + 3. 다른 환경에서 발급된 키라면 해당 환경의 마스터 키로 서버를 재설정 +``` + +### 10.3 "라이선스 복호화 실패 — 마스터 키 불일치" + +``` +원인: HMAC 통과했으나 AES 복호화 실패 — 마스터 키 파생 문자가 다름 +해결: 마스터 키가 정확히 64자리 hex인지 확인 + python3 -c "print(len('YOUR_KEY'))" # 반드시 64 출력 +``` + +### 10.4 "라이선스 한도 초과: ENTERPRISE 에디션으로 업그레이드하세요" + +``` +원인: 현재 등록된 기관/서버/사용자 수가 에디션 한도를 초과함 +해결: + 방법 1: 상위 에디션 라이선스 발급 후 적용 + 방법 2: 불필요한 기관/서버를 삭제하여 한도 이하로 줄임 +``` + +### 10.5 만료된 라이선스 등록 시도 + +``` +동작: 만료된 키도 DB에 등록 가능하나 valid: false, expired: true 반환됨 + 등록 시 경고 메시지: "경고: 만료된 라이선스입니다 (만료일: ...). 갱신이 필요합니다." +해결: 새 라이선스를 발급받아 즉시 교체 등록 +``` + +### 10.6 라이선스 캐시 갱신 안 됨 (변경 내용 미반영) + +``` +원인: 인메모리 캐시(TTL 1시간)가 남아있어 이전 상태를 반환 +해결: 라이선스를 activate/deactivate하면 캐시가 자동 무효화됨 + 긴급 시 ITSM 서비스 재시작으로 강제 캐시 초기화 +``` + +--- + +## 부록 A: 라이선스 발급 체크리스트 (벤더용) + +``` +고객사 발급 전 체크리스트: +□ 계약서에 명시된 기관명과 동일한지 확인 +□ 에디션이 계약 내용과 일치하는지 확인 +□ 유효 기간이 계약 기간과 일치하는지 확인 +□ 마스터 키가 해당 고객사 서버에 적용된 키와 동일한지 확인 +□ 발급된 키를 암호화된 채널(이메일 암호화, 보안 메신저)로 전달 +□ 발급 이력 대장에 기록 (날짜, 고객명, 에디션, 만료일, 발급자) +``` + +## 부록 B: 라이선스 발급 이력 대장 양식 + +| 번호 | 발급일 | 고객명 | 에디션 | 라이선스 ID | 만료일 | 발급자 | 비고 | +|-----|-------|-------|--------|-----------|-------|--------|------| +| 001 | 2026-05-28 | 서울특별시청 | ENTERPRISE | GRD-A1B2C3 | 2027-05-28 | 홍길동 | 신규 | +| 002 | 2026-05-28 | 경기도청 | STANDARD | GRD-D4E5F6 | 2028-05-28 | 홍길동 | 신규 2년 | diff --git a/GUARDiA_Paperclip_AI에이전트_구현보고서.pptx b/GUARDiA_Paperclip_AI에이전트_구현보고서.pptx new file mode 100644 index 0000000000000000000000000000000000000000..10ecbb92000bd6eb05caf9296fb8e47e76784855 GIT binary patch literal 460175 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 ({ + type: "outer", blur: 6, offset: 3, angle: 135, color: "000000", opacity: 0.12 +}); + +// ─── 슬라이드 헬퍼 ─────────────────────────────────────── +function addTitleSlide(bgColor, title, subtitle, badge) { + const slide = pres.addSlide(); + slide.background = { color: bgColor }; + + // 좌측 강조 바 + slide.addShape(pres.shapes.RECTANGLE, { + x: 0, y: 0, w: 0.12, h: 5.625, + fill: { color: C.lightBlue }, line: { color: C.lightBlue } + }); + + // 우측 장식 원 + slide.addShape(pres.shapes.OVAL, { + x: 7.5, y: -1.2, w: 4.5, h: 4.5, + fill: { color: C.navy, transparency: 60 }, line: { color: C.navy, transparency: 60 } + }); + slide.addShape(pres.shapes.OVAL, { + x: 8.5, y: 2.8, w: 2.5, h: 2.5, + fill: { color: C.blue, transparency: 70 }, line: { color: C.blue, transparency: 70 } + }); + + if (badge) { + slide.addShape(pres.shapes.RECTANGLE, { + x: 0.4, y: 1.3, w: 2.2, h: 0.38, + fill: { color: C.lightBlue }, line: { color: C.lightBlue } + }); + slide.addText(badge, { + x: 0.4, y: 1.3, w: 2.2, h: 0.38, + fontSize: 11, color: C.white, bold: true, align: "center", valign: "middle", margin: 0 + }); + } + + slide.addText(title, { + x: 0.4, y: 1.9, w: 8.5, h: 1.4, + fontSize: 36, color: C.white, bold: true, align: "left", fontFace: "Calibri" + }); + + if (subtitle) { + slide.addText(subtitle, { + x: 0.4, y: 3.45, w: 7.5, h: 0.7, + fontSize: 16, color: C.ice, align: "left", fontFace: "Calibri" + }); + } + + // 하단 날짜 라인 + slide.addShape(pres.shapes.LINE, { + x: 0.4, y: 5.0, w: 9.2, h: 0, + line: { color: C.lightBlue, width: 1 } + }); + slide.addText("2026.05.25 | GUARDiA ITSM", { + x: 0.4, y: 5.1, w: 5, h: 0.35, + fontSize: 10, color: "8FAADC", align: "left", fontFace: "Calibri" + }); + + return slide; +} + +function addContentSlide(title) { + const slide = pres.addSlide(); + slide.background = { color: C.offWhite }; + + // 상단 헤더 바 + slide.addShape(pres.shapes.RECTANGLE, { + x: 0, y: 0, w: 10, h: 0.72, + fill: { color: C.navy }, line: { color: C.navy } + }); + + // 좌측 포인트 바 + slide.addShape(pres.shapes.RECTANGLE, { + x: 0, y: 0.72, w: 0.08, h: 4.9, + fill: { color: C.lightBlue }, line: { color: C.lightBlue } + }); + + slide.addText(title, { + x: 0.25, y: 0, w: 9.5, h: 0.72, + fontSize: 20, color: C.white, bold: true, align: "left", valign: "middle", + fontFace: "Calibri", margin: 0 + }); + + return slide; +} + +function addSectionSlide(number, title, subtitle) { + const slide = pres.addSlide(); + slide.background = { color: C.blue }; + + slide.addShape(pres.shapes.RECTANGLE, { + x: 0, y: 0, w: 0.08, h: 5.625, + fill: { color: C.lightBlue }, line: { color: C.lightBlue } + }); + + slide.addText(`Phase ${number}`, { + x: 0.4, y: 1.4, w: 9, h: 0.7, + fontSize: 16, color: C.ice, bold: false, fontFace: "Calibri" + }); + slide.addText(title, { + x: 0.4, y: 2.0, w: 9, h: 1.2, + fontSize: 32, color: C.white, bold: true, fontFace: "Calibri" + }); + if (subtitle) { + slide.addText(subtitle, { + x: 0.4, y: 3.3, w: 8, h: 0.7, + fontSize: 15, color: C.ice, fontFace: "Calibri" + }); + } + return slide; +} + +function card(slide, x, y, w, h, headerColor, headerText, bodyLines) { + // 카드 본체 + slide.addShape(pres.shapes.RECTANGLE, { + x, y, w, h, + fill: { color: C.white }, + line: { color: C.lightGray, width: 1 }, + shadow: makeShadow() + }); + // 카드 상단 컬러 바 + slide.addShape(pres.shapes.RECTANGLE, { + x, y, w, h: 0.32, + fill: { color: headerColor }, + line: { color: headerColor } + }); + slide.addText(headerText, { + x, y, w, h: 0.32, + fontSize: 11, color: C.white, bold: true, + align: "center", valign: "middle", fontFace: "Calibri", margin: 0 + }); + + const textArr = bodyLines.map((line, i) => ({ + text: line, + options: { + fontSize: 10.5, + color: i === 0 ? C.gray : "1E293B", + bold: i === 0, + breakLine: true + } + })); + // 마지막 요소는 breakLine 제거 + if (textArr.length > 0) textArr[textArr.length - 1].options.breakLine = false; + + slide.addText(textArr, { + x: x + 0.1, y: y + 0.38, w: w - 0.2, h: h - 0.45, + valign: "top", fontFace: "Calibri" + }); +} + +function infoBox(slide, x, y, w, h, accentColor, title, desc) { + slide.addShape(pres.shapes.RECTANGLE, { + x, y, w, h, + fill: { color: C.white }, + line: { color: accentColor, width: 2 }, + shadow: makeShadow() + }); + slide.addShape(pres.shapes.RECTANGLE, { + x, y, w: 0.1, h, + fill: { color: accentColor }, + line: { color: accentColor } + }); + slide.addText(title, { + x: x + 0.18, y: y + 0.07, w: w - 0.25, h: 0.3, + fontSize: 11.5, color: "1E293B", bold: true, fontFace: "Calibri" + }); + slide.addText(desc, { + x: x + 0.18, y: y + 0.36, w: w - 0.25, h: h - 0.42, + fontSize: 10.5, color: C.gray, fontFace: "Calibri", valign: "top" + }); +} + +// ═══════════════════════════════════════════════════════ +// Slide 1: 타이틀 +// ═══════════════════════════════════════════════════════ +addTitleSlide( + C.navyDark, + "GUARDiA × Paperclip\nAI 에이전트 구현 보고서", + "Phase 1~4 설계·구현·테스트 완료 보고", + "2026.05.25 완료" +); + +// ═══════════════════════════════════════════════════════ +// Slide 2: 목차 +// ═══════════════════════════════════════════════════════ +{ + const slide = addContentSlide("목 차"); + + const items = [ + { n: "01", t: "GUARDiA ITSM 개요 & 배경", c: C.blue }, + { n: "02", t: "Paperclip 프레임워크 소개", c: C.purple }, + { n: "03", t: "Phase 1 — Paperclip 개발 도구 설정", c: C.teal }, + { n: "04", t: "Phase 2 — Ollama 로컬 LLM 구성", c: C.green }, + { n: "05", t: "Phase 3 — GUARDiA 에이전트 엔진", c: C.orange }, + { n: "06", t: "Phase 4 — 자율 운영 대시보드", c: C.blue }, + { n: "07", t: "테스트 결과 요약", c: C.green }, + { n: "08", t: "보안 제약사항 & 향후 로드맵", c: C.navy }, + ]; + + items.forEach((item, i) => { + const col = i < 4 ? 0 : 1; + const row = i % 4; + const x = 0.3 + col * 4.85; + const y = 0.88 + row * 1.1; + + slide.addShape(pres.shapes.RECTANGLE, { + x, y, w: 4.5, h: 0.9, + fill: { color: C.white }, + line: { color: C.lightGray, width: 1 }, + shadow: makeShadow() + }); + slide.addShape(pres.shapes.RECTANGLE, { + x, y, w: 0.55, h: 0.9, + fill: { color: item.c }, + line: { color: item.c } + }); + slide.addText(item.n, { + x, y, w: 0.55, h: 0.9, + fontSize: 16, color: C.white, bold: true, + align: "center", valign: "middle", margin: 0, fontFace: "Calibri" + }); + slide.addText(item.t, { + x: x + 0.62, y, w: 3.8, h: 0.9, + fontSize: 12.5, color: "1E293B", bold: false, + align: "left", valign: "middle", fontFace: "Calibri" + }); + }); +} + +// ═══════════════════════════════════════════════════════ +// Slide 3: GUARDiA ITSM 개요 +// ═══════════════════════════════════════════════════════ +{ + const slide = addContentSlide("01 GUARDiA ITSM 개요"); + + // 왼쪽: 기존 기능 + slide.addText("기존 GUARDiA 기능", { + x: 0.25, y: 0.85, w: 4.5, h: 0.4, + fontSize: 13, color: C.navy, bold: true, fontFace: "Calibri" + }); + + const existingFeatures = [ + ["SR 접수·처리", "서비스 요청 워크플로우"], + ["SSL 인증서 관리", "만료 감시 & 갱신 알림"], + ["PM 정기점검", "예방 유지보수 일정"], + ["SI 프로젝트 관리", "WBS·이슈·리스크·테스트"], + ["CMDB", "서버 자산 관리"], + ["메신저·알림", "내부 커뮤니케이션"], + ]; + + existingFeatures.forEach(([title, desc], i) => { + const y = 1.3 + i * 0.63; + slide.addShape(pres.shapes.RECTANGLE, { + x: 0.25, y, w: 4.5, h: 0.55, + fill: { color: C.offWhite }, line: { color: C.lightGray, width: 1 } + }); + slide.addShape(pres.shapes.RECTANGLE, { + x: 0.25, y, w: 0.07, h: 0.55, + fill: { color: C.blue }, line: { color: C.blue } + }); + slide.addText([ + { text: title, options: { bold: true, color: "1E293B", breakLine: true } }, + { text: desc, options: { color: C.gray } } + ], { + x: 0.4, y, w: 4.2, h: 0.55, + fontSize: 10.5, valign: "middle", fontFace: "Calibri" + }); + }); + + // 화살표 + slide.addShape(pres.shapes.RECTANGLE, { + x: 4.85, y: 2.5, w: 0.35, h: 0.6, + fill: { color: C.lightBlue }, line: { color: C.lightBlue } + }); + slide.addText("AI\n추가", { + x: 4.82, y: 2.5, w: 0.41, h: 0.6, + fontSize: 9, color: C.white, bold: true, align: "center", valign: "middle", fontFace: "Calibri", margin: 0 + }); + + // 오른쪽: AI 추가 목표 + slide.addText("AI 에이전트 추가 목표", { + x: 5.3, y: 0.85, w: 4.4, h: 0.4, + fontSize: 13, color: C.navy, bold: true, fontFace: "Calibri" + }); + + const aiGoals = [ + { icon: "⚡", title: "반복 업무 자동화", desc: "장애 분류, KB 등록, SR 자동 생성", color: C.orange }, + { icon: "👁", title: "능동적 모니터링", desc: "SSL·WBS·PM 상태를 AI가 주기 감시", color: C.blue }, + { icon: "🤝", title: "사람-AI 협업", desc: "고위험 액션은 사람 승인 게이트 통과", color: C.green }, + { icon: "🔒", title: "완전 온프레미스", desc: "Ollama 로컬 LLM, 외부 API 완전 차단", color: C.purple }, + ]; + + aiGoals.forEach((g, i) => { + const y = 1.3 + i * 0.98; + slide.addShape(pres.shapes.RECTANGLE, { + x: 5.3, y, w: 4.4, h: 0.85, + fill: { color: C.white }, line: { color: g.color, width: 2 }, + shadow: makeShadow() + }); + slide.addShape(pres.shapes.RECTANGLE, { + x: 5.3, y, w: 0.1, h: 0.85, + fill: { color: g.color }, line: { color: g.color } + }); + slide.addText([ + { text: `${g.icon} ${g.title}`, options: { bold: true, color: "1E293B", breakLine: true } }, + { text: g.desc, options: { color: C.gray } } + ], { + x: 5.48, y, w: 4.1, h: 0.85, + fontSize: 11, valign: "middle", fontFace: "Calibri" + }); + }); +} + +// ═══════════════════════════════════════════════════════ +// Slide 4: Paperclip 프레임워크 소개 +// ═══════════════════════════════════════════════════════ +{ + const slide = addContentSlide("02 Paperclip 프레임워크 소개"); + + // 메인 설명 + slide.addShape(pres.shapes.RECTANGLE, { + x: 0.25, y: 0.85, w: 9.5, h: 0.72, + fill: { color: "EEF2FF" }, line: { color: C.lightBlue, width: 1 } + }); + slide.addText([ + { text: "Paperclip", options: { bold: true, color: C.blue } }, + { text: " — AI 에이전트 오케스트레이션 오픈소스 프레임워크 (github.com/paperclipai/paperclip)", options: { color: "1E293B" } } + ], { + x: 0.35, y: 0.85, w: 9.3, h: 0.72, + fontSize: 13, valign: "middle", fontFace: "Calibri" + }); + + const features = [ + { title: "조직도 구조", desc: "CEO → CTO → 개발자/QA\n계층적 에이전트 관리", color: C.purple, icon: "🏢" }, + { title: "하트비트 시스템", desc: "에이전트가 주기적으로\n깨어나 작업 수행 후 대기", color: C.blue, icon: "💓" }, + { title: "이슈 추적", desc: "GitHub 스타일\n태스크/이슈 관리", color: C.teal, icon: "📋" }, + { title: "거버넌스 게이트", desc: "위험 수준에 따른\n사람 승인 워크플로우", color: C.orange, icon: "🔐" }, + ]; + + features.forEach((f, i) => { + const x = 0.25 + i * 2.4; + slide.addShape(pres.shapes.RECTANGLE, { + x, y: 1.72, w: 2.25, h: 1.95, + fill: { color: C.white }, line: { color: C.lightGray, width: 1 }, + shadow: makeShadow() + }); + slide.addShape(pres.shapes.RECTANGLE, { + x, y: 1.72, w: 2.25, h: 0.45, + fill: { color: f.color }, line: { color: f.color } + }); + slide.addText(`${f.icon} ${f.title}`, { + x, y: 1.72, w: 2.25, h: 0.45, + fontSize: 11, color: C.white, bold: true, + align: "center", valign: "middle", margin: 0, fontFace: "Calibri" + }); + slide.addText(f.desc, { + x: x + 0.1, y: 2.22, w: 2.05, h: 1.4, + fontSize: 11, color: "1E293B", align: "center", valign: "top", fontFace: "Calibri" + }); + }); + + // GUARDiA 적용 전략 + slide.addText("GUARDiA 적용 전략", { + x: 0.25, y: 3.8, w: 9.5, h: 0.35, + fontSize: 13, color: C.navy, bold: true, fontFace: "Calibri" + }); + + const strategies = [ + ["개발 시", "Paperclip CLI로 에이전트 페르소나 정의 (CEO/CTO/Dev/QA)", C.purple], + ["런타임", "AgentEngine (Python) 내장 — 외부 Paperclip 서버 불필요", C.blue], + ["LLM", "Ollama localhost:11434 전용 — 외부 LLM API 완전 차단", C.red], + ]; + + strategies.forEach(([label, desc, color], i) => { + const x = 0.25 + i * 3.2; + slide.addShape(pres.shapes.RECTANGLE, { + x, y: 4.2, w: 3.0, h: 1.05, + fill: { color: C.white }, line: { color: color, width: 2 }, + shadow: makeShadow() + }); + slide.addText([ + { text: label, options: { bold: true, color, breakLine: true } }, + { text: desc, options: { color: "1E293B" } } + ], { + x: x + 0.1, y: 4.2, w: 2.8, h: 1.05, + fontSize: 10.5, valign: "middle", fontFace: "Calibri" + }); + }); +} + +// ═══════════════════════════════════════════════════════ +// Slide 5: Phase 1 섹션 표지 +// ═══════════════════════════════════════════════════════ +addSectionSlide("1", "Paperclip 개발 도구 설정", "조직도 구성 · 에이전트 페르소나 · 거버넌스 규칙"); + +// ═══════════════════════════════════════════════════════ +// Slide 6: Phase 1 상세 +// ═══════════════════════════════════════════════════════ +{ + const slide = addContentSlide("03 Phase 1 — Paperclip 개발 도구 설정"); + + // 조직도 트리 + slide.addText("에이전트 조직도", { + x: 0.25, y: 0.85, w: 4.5, h: 0.35, + fontSize: 13, color: C.navy, bold: true, fontFace: "Calibri" + }); + + const orgNodes = [ + { label: "CEO", x: 1.9, y: 1.3, color: C.purple }, + { label: "CTO", x: 0.5, y: 2.4, color: C.blue }, + { label: "PM_AGENT", x: 3.3, y: 2.4, color: C.teal }, + { label: "DEVELOPER", x: 0.0, y: 3.5, color: C.green }, + { label: "QA", x: 1.2, y: 3.5, color: C.amber }, + ]; + + // 연결선 + slide.addShape(pres.shapes.LINE, { x: 2.4, y: 1.9, w: 0, h: 0.5, line: { color: C.gray, width: 1.5 } }); + slide.addShape(pres.shapes.LINE, { x: 1.5, y: 2.25, w: 1.8, h: 0, line: { color: C.gray, width: 1.5 } }); + slide.addShape(pres.shapes.LINE, { x: 1.5, y: 2.25, w: 0, h: 0.5, line: { color: C.gray, width: 1.5 } }); + slide.addShape(pres.shapes.LINE, { x: 3.3, y: 2.25, w: 0, h: 0.5, line: { color: C.gray, width: 1.5 } }); + slide.addShape(pres.shapes.LINE, { x: 0.65, y: 3.05, w: 1.55, h: 0, line: { color: C.gray, width: 1.5 } }); + slide.addShape(pres.shapes.LINE, { x: 0.65, y: 3.05, w: 0, h: 0.5, line: { color: C.gray, width: 1.5 } }); + slide.addShape(pres.shapes.LINE, { x: 1.7, y: 3.05, w: 0, h: 0.5, line: { color: C.gray, width: 1.5 } }); + + orgNodes.forEach(node => { + slide.addShape(pres.shapes.RECTANGLE, { + x: node.x, y: node.y, w: 1.2, h: 0.52, + fill: { color: node.color }, line: { color: node.color }, + shadow: makeShadow() + }); + slide.addText(node.label, { + x: node.x, y: node.y, w: 1.2, h: 0.52, + fontSize: 10, color: C.white, bold: true, + align: "center", valign: "middle", margin: 0, fontFace: "Calibri" + }); + }); + + // 파일 구조 + slide.addText("생성 파일 구조", { + x: 5.0, y: 0.85, w: 4.7, h: 0.35, + fontSize: 13, color: C.navy, bold: true, fontFace: "Calibri" + }); + + slide.addShape(pres.shapes.RECTANGLE, { + x: 5.0, y: 1.25, w: 4.7, h: 3.2, + fill: { color: "1E293B" }, line: { color: "334155", width: 1 } + }); + slide.addText([ + { text: "C:\\GUARDiA\\paperclip\\", options: { color: C.lightBlue, bold: true, breakLine: true } }, + { text: "├─ paperclip.config.json", options: { color: "A3E635", breakLine: true } }, + { text: "│ (조직도 · LLM 설정 · 거버넌스)", options: { color: "94A3B8", breakLine: true } }, + { text: "├─ README.md", options: { color: "A3E635", breakLine: true } }, + { text: "└─ agents/", options: { color: "FB923C", breakLine: true } }, + { text: " ├─ ceo.md", options: { color: "A3E635", breakLine: true } }, + { text: " ├─ cto.md", options: { color: "A3E635", breakLine: true } }, + { text: " ├─ developer.md", options: { color: "A3E635", breakLine: true } }, + { text: " └─ qa.md", options: { color: "A3E635" } }, + ], { + x: 5.15, y: 1.35, w: 4.4, h: 3.0, + fontSize: 10.5, valign: "top", fontFace: "Courier New" + }); + + // 거버넌스 표 + slide.addText("거버넌스 규칙", { + x: 0.25, y: 4.1, w: 4.5, h: 0.32, + fontSize: 11, color: C.navy, bold: true, fontFace: "Calibri" + }); + + const govRows = [ + [{ text: "액션", options: { bold: true, fill: { color: C.navy }, color: C.white } }, + { text: "승인 방식", options: { bold: true, fill: { color: C.navy }, color: C.white } }], + ["code_commit / deploy", "사람 승인 필수"], + ["delete_data", "CEO 승인 필수"], + ["CRITICAL 장애 분류", "담당자 승인 필수"], + ["KB 등록 / SSL SR", "자동 승인"], + ]; + + slide.addTable(govRows, { + x: 0.25, y: 4.45, w: 4.5, h: 1.0, + colW: [2.5, 2.0], + border: { pt: 1, color: C.lightGray }, + fontSize: 10, fontFace: "Calibri", + fill: { color: C.white } + }); +} + +// ═══════════════════════════════════════════════════════ +// Slide 7: Phase 2 섹션 표지 +// ═══════════════════════════════════════════════════════ +addSectionSlide("2", "Ollama 로컬 LLM 설정", "guardia-agent 커스텀 모델 · 보안 온프레미스 추론"); + +// ═══════════════════════════════════════════════════════ +// Slide 8: Phase 2 상세 +// ═══════════════════════════════════════════════════════ +{ + const slide = addContentSlide("04 Phase 2 — Ollama 로컬 LLM"); + + // 왼쪽: 아키텍처 + slide.addText("추론 아키텍처", { + x: 0.25, y: 0.85, w: 4.8, h: 0.35, + fontSize: 13, color: C.navy, bold: true, fontFace: "Calibri" + }); + + const archItems = [ + { label: "GUARDiA AgentEngine", color: C.blue, y: 1.3 }, + { label: "OllamaClient (HTTP)", color: C.teal, y: 2.0 }, + { label: "Ollama Server :11434", color: C.green, y: 2.7 }, + { label: "guardia-agent 모델", color: C.purple, y: 3.4 }, + ]; + + archItems.forEach((item, i) => { + slide.addShape(pres.shapes.RECTANGLE, { + x: 0.5, y: item.y, w: 3.8, h: 0.55, + fill: { color: item.color }, line: { color: item.color }, + shadow: makeShadow() + }); + slide.addText(item.label, { + x: 0.5, y: item.y, w: 3.8, h: 0.55, + fontSize: 12, color: C.white, bold: true, + align: "center", valign: "middle", margin: 0, fontFace: "Calibri" + }); + if (i < archItems.length - 1) { + slide.addShape(pres.shapes.RECTANGLE, { + x: 2.25, y: item.y + 0.55, w: 0.3, h: 0.15, + fill: { color: C.gray }, line: { color: C.gray } + }); + } + }); + + // 보안 배지 + slide.addShape(pres.shapes.RECTANGLE, { + x: 0.25, y: 4.1, w: 4.5, h: 0.9, + fill: { color: "FEF2F2" }, line: { color: C.red, width: 2 } + }); + slide.addText([ + { text: "🔒 외부 LLM API 완전 차단", options: { bold: true, color: C.red, breakLine: true } }, + { text: "모든 AI 추론은 localhost:11434 (Ollama) 만 허용", options: { color: "7F1D1D" } } + ], { + x: 0.4, y: 4.1, w: 4.3, h: 0.9, + fontSize: 12, valign: "middle", fontFace: "Calibri" + }); + + // 오른쪽: 모델 설정 + slide.addText("guardia-agent 모델 파라미터", { + x: 5.05, y: 0.85, w: 4.7, h: 0.35, + fontSize: 13, color: C.navy, bold: true, fontFace: "Calibri" + }); + + slide.addShape(pres.shapes.RECTANGLE, { + x: 5.05, y: 1.25, w: 4.7, h: 2.0, + fill: { color: "1E293B" }, line: { color: "334155", width: 1 } + }); + slide.addText([ + { text: "FROM llama3.1:8b", options: { color: "A3E635", breakLine: true } }, + { text: "SYSTEM \"\"\"", options: { color: "FB923C", breakLine: true } }, + { text: " GUARDiA ITSM AI 운영 에이전트", options: { color: "94A3B8", breakLine: true } }, + { text: " 한국어 응답 · ITSM 전문화", options: { color: "94A3B8", breakLine: true } }, + { text: " 외부 API 호출 금지", options: { color: C.red, breakLine: true } }, + { text: "\"\"\"", options: { color: "FB923C", breakLine: true } }, + { text: "PARAMETER temperature 0.2", options: { color: "38BDF8", breakLine: true } }, + { text: "PARAMETER num_predict 2048", options: { color: "38BDF8" } }, + ], { + x: 5.2, y: 1.35, w: 4.4, h: 1.8, + fontSize: 10.5, valign: "top", fontFace: "Courier New" + }); + + // OllamaClient API + slide.addText("OllamaClient API", { + x: 5.05, y: 3.35, w: 4.7, h: 0.35, + fontSize: 13, color: C.navy, bold: true, fontFace: "Calibri" + }); + + const methods = [ + ["health_check()", "서버 상태 확인"], + ["resolve_model(preferred)", "fallback 모델 선택"], + ["json_generate(prompt)", "JSON 추출 (코드블록 자동 제거)"], + ["pull_model(model)", "모델 다운로드"], + ]; + + methods.forEach(([method, desc], i) => { + slide.addShape(pres.shapes.RECTANGLE, { + x: 5.05, y: 3.75 + i * 0.43, w: 4.7, h: 0.38, + fill: { color: i % 2 === 0 ? C.offWhite : C.white }, + line: { color: C.lightGray, width: 1 } + }); + slide.addText([ + { text: method, options: { bold: true, color: C.blue } }, + { text: " → " + desc, options: { color: C.gray } } + ], { + x: 5.15, y: 3.75 + i * 0.43, w: 4.5, h: 0.38, + fontSize: 10, valign: "middle", fontFace: "Calibri" + }); + }); +} + +// ═══════════════════════════════════════════════════════ +// Slide 9: Phase 3 섹션 표지 +// ═══════════════════════════════════════════════════════ +addSectionSlide("3", "GUARDiA 에이전트 엔진", "6종 자율 에이전트 · 하트비트 사이클 · 승인 게이트"); + +// ═══════════════════════════════════════════════════════ +// Slide 10: Phase 3 — 에이전트 종류 +// ═══════════════════════════════════════════════════════ +{ + const slide = addContentSlide("05 Phase 3 — 6종 에이전트 역할"); + + const agents = [ + { + role: "INCIDENT_TRIAGE", + schedule: "매 15분", + color: C.orange, + lines: ["미배정 장애 자동 분류", "severity / category / reason", "CRITICAL → 사람 승인"] + }, + { + role: "KB_CURATOR", + schedule: "매시간 정각", + color: C.teal, + lines: ["완료 SR → KB 자동 생성", "증상·원인·해결책 초안", "published=False (검토 대기)"] + }, + { + role: "SSL_WATCHER", + schedule: "매일 08:30", + color: C.red, + lines: ["SSL 만료 0~30일 서버 감시", "자동 갱신 SR 생성", "7일↓=CRITICAL, 30일↓=HIGH"] + }, + { + role: "WBS_MONITOR", + schedule: "매일 08:00", + color: C.blue, + lines: ["SI WBS 지연 감지", "3일+ 주의, 10일+ CRITICAL", "리스크 자동 등록"] + }, + { + role: "PM_SUGGESTER", + schedule: "매일 09:00", + color: C.green, + lines: ["PM 미등록 서버 탐지", "권장 점검 일정 제안", "AgentTask 기록"] + }, + { + role: "DEVELOPER", + schedule: "수동 트리거", + color: C.purple, + lines: ["PENDING 태스크 처리", "LLM 코드 생성", "CODE_CHANGE → 사람 승인"] + }, + ]; + + agents.forEach((a, i) => { + const col = i % 3; + const row = Math.floor(i / 3); + const x = 0.25 + col * 3.25; + const y = 0.88 + row * 2.35; + + slide.addShape(pres.shapes.RECTANGLE, { + x, y, w: 3.1, h: 2.18, + fill: { color: C.white }, line: { color: C.lightGray, width: 1 }, + shadow: makeShadow() + }); + slide.addShape(pres.shapes.RECTANGLE, { + x, y, w: 3.1, h: 0.5, + fill: { color: a.color }, line: { color: a.color } + }); + slide.addText(a.role, { + x, y, w: 2.3, h: 0.5, + fontSize: 11, color: C.white, bold: true, + align: "left", valign: "middle", fontFace: "Calibri", + indent: 0.1, margin: 0 + }); + slide.addText(a.schedule, { + x: x + 2.3, y, w: 0.8, h: 0.5, + fontSize: 9, color: "FFEAA7", + align: "center", valign: "middle", fontFace: "Calibri", margin: 0 + }); + + a.lines.forEach((line, li) => { + slide.addText([ + { text: "▸ ", options: { color: a.color, bold: true } }, + { text: line, options: { color: "1E293B" } } + ], { + x: x + 0.1, y: y + 0.55 + li * 0.52, w: 2.9, h: 0.48, + fontSize: 10.5, valign: "top", fontFace: "Calibri" + }); + }); + }); +} + +// ═══════════════════════════════════════════════════════ +// Slide 11: Phase 3 — 하트비트 & 승인 게이트 +// ═══════════════════════════════════════════════════════ +{ + const slide = addContentSlide("05 Phase 3 — 하트비트 & 승인 게이트"); + + // 하트비트 플로우 + slide.addText("하트비트 사이클", { + x: 0.25, y: 0.85, w: 4.7, h: 0.35, + fontSize: 13, color: C.navy, bold: true, fontFace: "Calibri" + }); + + const flowSteps = [ + { label: "APScheduler\nCron 트리거", color: C.blue, y: 1.3 }, + { label: "status = ACTIVE", color: C.teal, y: 2.05 }, + { label: "Ollama\nhealth_check()", color: C.green, y: 2.8 }, + { label: "_handler() 실행", color: C.orange, y: 3.55 }, + { label: "status = IDLE\n(완료)", color: C.purple, y: 4.3 }, + ]; + + flowSteps.forEach((step, i) => { + slide.addShape(pres.shapes.RECTANGLE, { + x: 0.5, y: step.y, w: 3.0, h: 0.6, + fill: { color: step.color }, line: { color: step.color }, + shadow: makeShadow() + }); + slide.addText(step.label, { + x: 0.5, y: step.y, w: 3.0, h: 0.6, + fontSize: 10.5, color: C.white, bold: true, + align: "center", valign: "middle", margin: 0, fontFace: "Calibri" + }); + if (i < flowSteps.length - 1) { + slide.addShape(pres.shapes.RECTANGLE, { + x: 1.85, y: step.y + 0.6, w: 0.3, h: 0.15, + fill: { color: C.gray }, line: { color: C.gray } + }); + } + }); + + // 오른쪽: 승인 게이트 + slide.addText("승인 게이트 설계", { + x: 4.0, y: 0.85, w: 5.7, h: 0.35, + fontSize: 13, color: C.navy, bold: true, fontFace: "Calibri" + }); + + // AUTO_APPROVED + slide.addShape(pres.shapes.RECTANGLE, { + x: 4.0, y: 1.25, w: 5.7, h: 1.3, + fill: { color: "F0FDF4" }, line: { color: C.green, width: 2 } + }); + slide.addShape(pres.shapes.RECTANGLE, { + x: 4.0, y: 1.25, w: 0.1, h: 1.3, + fill: { color: C.green }, line: { color: C.green } + }); + slide.addText([ + { text: "✅ AUTO_APPROVED ", options: { bold: true, color: C.green } }, + { text: "— 사람 개입 없이 즉시 실행", options: { color: "166534" } } + ], { + x: 4.18, y: 1.3, w: 5.4, h: 0.35, + fontSize: 12, fontFace: "Calibri" + }); + slide.addText([ + { text: "• KB 등록 초안 생성", options: { breakLine: true } }, + { text: "• SSL 갱신 SR 생성 ", options: { breakLine: true } }, + { text: "• PM 일정 제안 ", options: { breakLine: true } }, + { text: "• WBS 리스크 등록 (일반)", options: {} }, + ], { + x: 4.18, y: 1.68, w: 5.4, h: 0.82, + fontSize: 10.5, color: "166534", valign: "top", fontFace: "Calibri" + }); + + // PENDING (사람 승인) + slide.addShape(pres.shapes.RECTANGLE, { + x: 4.0, y: 2.7, w: 5.7, h: 1.5, + fill: { color: "FFF7ED" }, line: { color: C.red, width: 2 } + }); + slide.addShape(pres.shapes.RECTANGLE, { + x: 4.0, y: 2.7, w: 0.1, h: 1.5, + fill: { color: C.red }, line: { color: C.red } + }); + slide.addText([ + { text: "⏳ PENDING ", options: { bold: true, color: C.red } }, + { text: "— 담당자 승인 후 실행", options: { color: "7C2D12" } } + ], { + x: 4.18, y: 2.75, w: 5.4, h: 0.35, + fontSize: 12, fontFace: "Calibri" + }); + slide.addText([ + { text: "• CRITICAL 장애 분류 적용", options: { breakLine: true } }, + { text: "• CODE_CHANGE (개발자 에이전트)", options: { breakLine: true } }, + { text: "• CRITICAL 리스크 등록 (WBS 10일+ 지연)", options: { breakLine: true } }, + { text: "• code_commit / deploy / delete_data", options: {} } + ], { + x: 4.18, y: 3.12, w: 5.4, h: 1.0, + fontSize: 10.5, color: "7C2D12", valign: "top", fontFace: "Calibri" + }); + + // 데이터 모델 요약 + slide.addText("DB 테이블 구조", { + x: 4.0, y: 4.35, w: 5.7, h: 0.32, + fontSize: 11, color: C.navy, bold: true, fontFace: "Calibri" + }); + + const tables = [ + ["tb_agent_config", "에이전트 설정 · 상태 · 통계"], + ["tb_agent_task", "실행된 태스크 · LLM 입출력"], + ["tb_agent_approval", "승인 대기·완료 기록"], + ]; + tables.forEach(([t, d], i) => { + slide.addShape(pres.shapes.RECTANGLE, { + x: 4.0 + i * 1.9, y: 4.7, w: 1.8, h: 0.7, + fill: { color: C.navy }, line: { color: C.navy }, + shadow: makeShadow() + }); + slide.addText([ + { text: t, options: { bold: true, color: C.ice, breakLine: true } }, + { text: d, options: { color: "8FAADC" } } + ], { + x: 4.05 + i * 1.9, y: 4.72, w: 1.7, h: 0.66, + fontSize: 9, align: "center", valign: "middle", fontFace: "Calibri" + }); + }); +} + +// ═══════════════════════════════════════════════════════ +// Slide 12: Phase 4 섹션 표지 +// ═══════════════════════════════════════════════════════ +addSectionSlide("4", "자율 운영 대시보드", "agents.html SPA · 실시간 상태 · 승인 워크플로우"); + +// ═══════════════════════════════════════════════════════ +// Slide 13: Phase 4 상세 +// ═══════════════════════════════════════════════════════ +{ + const slide = addContentSlide("06 Phase 4 — 자율 운영 대시보드"); + + // 접근 경로 배너 + slide.addShape(pres.shapes.RECTANGLE, { + x: 0.25, y: 0.85, w: 9.5, h: 0.5, + fill: { color: C.navy }, line: { color: C.navy } + }); + slide.addText([ + { text: "접근: ", options: { color: C.ice } }, + { text: "http://localhost:8001/agents", options: { bold: true, color: "A3E635" } }, + { text: " | 자동 갱신: 30초 간격", options: { color: C.ice } } + ], { + x: 0.35, y: 0.85, w: 9.3, h: 0.5, + fontSize: 12, valign: "middle", fontFace: "Calibri" + }); + + // 대시보드 구성 카드 + const panels = [ + { title: "LLM 상태 배너", desc: "Ollama 온라인/오프라인\n실시간 연결 상태 표시", color: C.teal }, + { title: "통계 카드 (5종)", desc: "총 에이전트·활성·오늘\n태스크·토큰·승인 대기", color: C.blue }, + { title: "에이전트 탭", desc: "역할 배지 + 상태 펄스\n하트비트/정지/재개 버튼", color: C.purple }, + { title: "조직도 탭", desc: "계층적 트리 렌더링\n에이전트 관계 시각화", color: C.orange }, + { title: "승인 대기 탭", desc: "PENDING 승인 목록\nCRITICAL 강조 표시", color: C.red }, + { title: "태스크 피드 탭", desc: "전체 에이전트 태스크\n실시간 피드 통합 뷰", color: C.green }, + ]; + + panels.forEach((p, i) => { + const col = i % 3; + const row = Math.floor(i / 3); + const x = 0.25 + col * 3.25; + const y = 1.48 + row * 1.85; + + slide.addShape(pres.shapes.RECTANGLE, { + x, y, w: 3.1, h: 1.65, + fill: { color: C.white }, line: { color: C.lightGray, width: 1 }, + shadow: makeShadow() + }); + slide.addShape(pres.shapes.RECTANGLE, { + x, y, w: 3.1, h: 0.4, + fill: { color: p.color }, line: { color: p.color } + }); + slide.addText(p.title, { + x, y, w: 3.1, h: 0.4, + fontSize: 11, color: C.white, bold: true, + align: "center", valign: "middle", margin: 0, fontFace: "Calibri" + }); + slide.addText(p.desc, { + x: x + 0.1, y: y + 0.45, w: 2.9, h: 1.15, + fontSize: 11, color: "1E293B", align: "center", valign: "middle", fontFace: "Calibri" + }); + }); + + // API 엔드포인트 수 + slide.addShape(pres.shapes.RECTANGLE, { + x: 0.25, y: 5.15, w: 9.5, h: 0.35, + fill: { color: "EEF2FF" }, line: { color: C.lightBlue, width: 1 } + }); + slide.addText("REST API: 16개 엔드포인트 | GET·POST·PATCH·DELETE | CUSTOMER 역할 전체 차단 | RBAC 기반 권한 제어", { + x: 0.35, y: 5.15, w: 9.3, h: 0.35, + fontSize: 10.5, color: C.blue, align: "center", valign: "middle", fontFace: "Calibri" + }); +} + +// ═══════════════════════════════════════════════════════ +// Slide 14: 테스트 결과 +// ═══════════════════════════════════════════════════════ +{ + const slide = addContentSlide("07 테스트 결과 요약"); + + // 구문 검사 결과 + slide.addText("구문 검사 (Python Syntax Check)", { + x: 0.25, y: 0.85, w: 9.5, h: 0.35, + fontSize: 13, color: C.navy, bold: true, fontFace: "Calibri" + }); + + const syntaxFiles = [ + "models.py", "main.py", "core/llm_client.py", + "core/agents.py", "core/scheduler.py", "routers/agents.py" + ]; + + syntaxFiles.forEach((f, i) => { + const x = 0.25 + (i % 3) * 3.25; + const y = 1.25 + Math.floor(i / 3) * 0.6; + slide.addShape(pres.shapes.RECTANGLE, { + x, y, w: 3.1, h: 0.5, + fill: { color: "F0FDF4" }, line: { color: C.green, width: 1 } + }); + slide.addText([ + { text: "✅ ", options: { color: C.green, bold: true } }, + { text: f, options: { color: "1E293B" } } + ], { + x: x + 0.1, y, w: 2.9, h: 0.5, + fontSize: 11, valign: "middle", fontFace: "Calibri" + }); + }); + + // 심볼 검증 + slide.addText("심볼 검증", { + x: 0.25, y: 2.55, w: 4.7, h: 0.35, + fontSize: 13, color: C.navy, bold: true, fontFace: "Calibri" + }); + + const symbols = [ + ["AgentRole enum", "10가지 역할", C.purple], + ["AgentEngine 핸들러", "6개 구현", C.orange], + ["OllamaClient 메서드", "7개 구현", C.teal], + ["API 엔드포인트", "16개 라우터", C.blue], + ["스케줄러 잡", "9개 (기존4+신규5)", C.green], + ["main.py 라우터", "34개 등록", C.navy], + ]; + + symbols.forEach((s, i) => { + const x = 0.25 + (i % 2) * 2.45; + const y = 2.95 + Math.floor(i / 2) * 0.6; + slide.addShape(pres.shapes.RECTANGLE, { + x, y, w: 2.3, h: 0.52, + fill: { color: C.white }, line: { color: s[2], width: 2 }, + shadow: makeShadow() + }); + slide.addText([ + { text: s[0] + "\n", options: { bold: true, color: "1E293B", fontSize: 10 } }, + { text: s[1], options: { color: s[2], bold: true, fontSize: 11 } } + ], { + x: x + 0.08, y, w: 2.14, h: 0.52, + valign: "middle", fontFace: "Calibri" + }); + }); + + // 스케줄러 잡 목록 + slide.addText("스케줄러 잡 (총 9개)", { + x: 5.2, y: 2.55, w: 4.5, h: 0.35, + fontSize: 13, color: C.navy, bold: true, fontFace: "Calibri" + }); + + const jobs = [ + ["cert_check", "매일 09:00", C.gray], + ["pm_check", "매일 09:05", C.gray], + ["on_call_notify", "매일 08:55", C.gray], + ["batch_cleanup", "매일 02:00", C.gray], + ["agent_incident_triage", "매 15분", C.orange], + ["agent_kb_curator", "매시간", C.teal], + ["agent_ssl_watcher", "매일 08:30", C.red], + ["agent_wbs_monitor", "매일 08:00", C.blue], + ["agent_pm_suggester", "매일 09:00", C.green], + ]; + + jobs.forEach((j, i) => { + const y = 2.95 + i * 0.3; + slide.addShape(pres.shapes.RECTANGLE, { + x: 5.2, y, w: 4.5, h: 0.27, + fill: { color: i < 4 ? C.offWhite : "F0FDF4" }, + line: { color: C.lightGray, width: 1 } + }); + slide.addShape(pres.shapes.RECTANGLE, { + x: 5.2, y, w: 0.07, h: 0.27, + fill: { color: j[2] }, line: { color: j[2] } + }); + slide.addText([ + { text: j[0], options: { bold: i >= 4, color: i >= 4 ? "1E293B" : C.gray } }, + { text: " " + j[1], options: { color: j[2], bold: i >= 4 } } + ], { + x: 5.32, y, w: 4.3, h: 0.27, + fontSize: 9.5, valign: "middle", fontFace: "Calibri" + }); + }); +} + +// ═══════════════════════════════════════════════════════ +// Slide 15: 보안 제약사항 +// ═══════════════════════════════════════════════════════ +{ + const slide = addContentSlide("08 보안 제약사항"); + + const secRules = [ + { + cat: "LLM 보안", + color: C.red, + rules: [ + "외부 LLM API 완전 차단 (OpenAI/Claude 등)", + "Ollama localhost:11434 전용 사용", + "temperature 0.2 고정 (결정론적 응답)", + "HTTP 요청 30초 타임아웃" + ] + }, + { + cat: "서버 정보 보호", + color: C.orange, + rules: [ + "ip_addr — ServerOut 스키마 미포함", + "ssh_user — API 응답 절대 미노출", + "os_pw_enc — AES-256-GCM 암호화 필수", + "file_path — API 응답 완전 제거" + ] + }, + { + cat: "에이전트 액션 제어", + color: C.purple, + rules: [ + "CRITICAL 액션 → 사람 승인 후 실행", + "위험 명령어 차단: rm -rf /, shutdown, mkfs", + "fork bomb 패턴 감지 & 차단", + "경로 순회 방지 (resolve().relative_to)" + ] + }, + { + cat: "접근 제어 (RBAC)", + color: C.blue, + rules: [ + "CUSTOMER 역할 — 에이전트 API 전체 차단", + "에이전트 생성/수정/삭제 — ADMIN만 허용", + "승인 처리 — USER 이상 허용", + "스택트레이스 — API 응답 노출 금지" + ] + } + ]; + + secRules.forEach((sec, i) => { + const x = 0.25 + (i % 2) * 4.9; + const y = 0.88 + Math.floor(i / 2) * 2.35; + + slide.addShape(pres.shapes.RECTANGLE, { + x, y, w: 4.65, h: 2.2, + fill: { color: C.white }, line: { color: C.lightGray, width: 1 }, + shadow: makeShadow() + }); + slide.addShape(pres.shapes.RECTANGLE, { + x, y, w: 4.65, h: 0.42, + fill: { color: sec.color }, line: { color: sec.color } + }); + slide.addText(sec.cat, { + x, y, w: 4.65, h: 0.42, + fontSize: 12, color: C.white, bold: true, + align: "center", valign: "middle", margin: 0, fontFace: "Calibri" + }); + + const textArr = sec.rules.map((r, ri) => ({ + text: "▸ " + r, + options: { + color: ri === 0 ? "0F172A" : "1E293B", + bold: ri === 0, + fontSize: 10.5, + breakLine: ri < sec.rules.length - 1 + } + })); + slide.addText(textArr, { + x: x + 0.12, y: y + 0.48, w: 4.4, h: 1.65, + valign: "top", fontFace: "Calibri" + }); + }); +} + +// ═══════════════════════════════════════════════════════ +// Slide 16: 향후 로드맵 +// ═══════════════════════════════════════════════════════ +{ + const slide = addContentSlide("08 향후 로드맵"); + + const roadmap = [ + { ver: "v1.1", period: "2026 Q3", title: "에이전트 간 메시지 전달", desc: "CEO→CTO 위임 워크플로우\n태스크 자동 위임 체계", color: C.blue }, + { ver: "v1.2", period: "2026 Q3", title: "에이전트 학습", desc: "이전 태스크 기반 파인튜닝\n조직 맞춤형 응답 최적화", color: C.teal }, + { ver: "v1.3", period: "2026 Q4", title: "멀티모달 지원", desc: "스크린샷·로그 이미지 분석\n시각 장애 자동 탐지", color: C.green }, + { ver: "v2.0", period: "2027 Q1", title: "에이전트 마켓플레이스", desc: "커뮤니티 에이전트 공유\nPlug-in 방식 등록/사용", color: C.purple }, + { ver: "v2.1", period: "2027 Q1", title: "엣지 배포", desc: "경량 모델 (llama3.2:1b)\n저사양 서버 지원", color: C.orange }, + ]; + + roadmap.forEach((r, i) => { + const x = 0.25 + i * 1.92; + slide.addShape(pres.shapes.RECTANGLE, { + x, y: 0.88, w: 1.75, h: 4.5, + fill: { color: C.white }, line: { color: C.lightGray, width: 1 }, + shadow: makeShadow() + }); + slide.addShape(pres.shapes.RECTANGLE, { + x, y: 0.88, w: 1.75, h: 0.55, + fill: { color: r.color }, line: { color: r.color } + }); + slide.addText(r.ver, { + x, y: 0.88, w: 1.75, h: 0.55, + fontSize: 16, color: C.white, bold: true, + align: "center", valign: "middle", margin: 0, fontFace: "Calibri" + }); + + slide.addShape(pres.shapes.RECTANGLE, { + x: x + 0.1, y: 1.5, w: 1.55, h: 0.38, + fill: { color: r.color, transparency: 80 }, line: { color: r.color, width: 1 } + }); + slide.addText(r.period, { + x: x + 0.1, y: 1.5, w: 1.55, h: 0.38, + fontSize: 10, color: "1E293B", bold: true, + align: "center", valign: "middle", margin: 0, fontFace: "Calibri" + }); + + slide.addText(r.title, { + x: x + 0.1, y: 2.0, w: 1.55, h: 0.65, + fontSize: 11, color: "0F172A", bold: true, + align: "center", fontFace: "Calibri" + }); + + slide.addShape(pres.shapes.LINE, { + x: x + 0.2, y: 2.7, w: 1.35, h: 0, + line: { color: C.lightGray, width: 1 } + }); + + slide.addText(r.desc, { + x: x + 0.1, y: 2.75, w: 1.55, h: 1.55, + fontSize: 10.5, color: C.gray, + align: "center", valign: "top", fontFace: "Calibri" + }); + }); + + // 하단 현재 완료 배너 + slide.addShape(pres.shapes.RECTANGLE, { + x: 0.25, y: 5.1, w: 9.5, h: 0.38, + fill: { color: C.green }, line: { color: C.green } + }); + slide.addText("현재 완료: Phase 1~4 | 6종 에이전트 | 16개 API | 9개 스케줄러 잡 | 자율 운영 대시보드", { + x: 0.35, y: 5.1, w: 9.3, h: 0.38, + fontSize: 11, color: C.white, bold: true, + align: "center", valign: "middle", fontFace: "Calibri" + }); +} + +// ═══════════════════════════════════════════════════════ +// Slide 17: 마무리 +// ═══════════════════════════════════════════════════════ +{ + const slide = addTitleSlide( + C.navyDark, + "GUARDiA × Paperclip\n구현 완료", + "Phase 1~4 완료 | 온프레미스 AI 자율 운영 체계 구축", + "완료" + ); + + // 요약 통계 카드 + const stats = [ + { num: "6", label: "자율 에이전트", color: C.orange }, + { num: "16", label: "REST API 엔드포인트", color: C.blue }, + { num: "9", label: "스케줄러 잡", color: C.green }, + { num: "100%", label: "구문 검사 통과", color: C.teal }, + ]; + + stats.forEach((s, i) => { + const x = 0.4 + i * 2.3; + slide.addShape(pres.shapes.RECTANGLE, { + x, y: 4.05, w: 2.1, h: 1.3, + fill: { color: s.color, transparency: 15 }, + line: { color: s.color, width: 2 } + }); + slide.addText(s.num, { + x, y: 4.08, w: 2.1, h: 0.7, + fontSize: 28, color: C.white, bold: true, + align: "center", valign: "middle", fontFace: "Calibri", margin: 0 + }); + slide.addText(s.label, { + x, y: 4.75, w: 2.1, h: 0.55, + fontSize: 10, color: C.ice, + align: "center", valign: "top", fontFace: "Calibri", margin: 0 + }); + }); +} + +// ─── 파일 저장 ─────────────────────────────────────── +pres.writeFile({ fileName: "C:/GUARDiA/manual/GUARDiA_Paperclip_AI에이전트_구현보고서.pptx" }) + .then(() => console.log("PPT 생성 완료: GUARDiA_Paperclip_AI에이전트_구현보고서.pptx")) + .catch(err => { console.error("PPT 생성 실패:", err); process.exit(1); });