refactor: 101.79.17.164 → zioinfo.co.kr 전체 도메인 변환 + Manager UI 배포

- 37개 파일 IP → zioinfo.co.kr 치환 (소스/매뉴얼/설정/하네스)
- Manager DrConsole/NetworkConsole/CsapConsole 빌드 + /var/www/manager/ 배포
- 테스트: Manager HTTP 200, ITSM 신규 API 7개 전체 200

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
DESKTOP-TKLFCPRython 2026-05-31 10:09:17 +09:00
parent 0b35ab54eb
commit 2bd7d876cc
25 changed files with 5357 additions and 0 deletions

View File

@ -0,0 +1,248 @@
# zio-server 설치 SW 목록 및 구성 명세
> **서버**: zio-server | **IP**: zioinfo.co.kr | **작성일**: 2026-05-30
> **OS**: Ubuntu 24.04.1 LTS (Noble Numbat) | **Provider**: NCloud (네이버 클라우드)
---
## 1. 서버 사양
| 항목 | 사양 |
|------|------|
| CPU | AMD EPYC 9454P (2 vCPU) |
| RAM | 7.8 GB |
| Disk | 99 GB (사용 15 GB / 여유 80 GB) |
| OS | Ubuntu 24.04.1 LTS x86_64 |
| 네트워크 | 공인 IP zioinfo.co.kr |
---
## 2. 설치 소프트웨어 목록
### 2-1. 런타임 / 언어
| SW | 버전 | 설치 경로 | 용도 |
|----|------|-----------|------|
| OpenJDK | 21.0.10 | `/usr/lib/jvm/java-21-openjdk-amd64` | Spring Boot 실행 |
| Node.js | 20.20.2 (LTS) | `/usr/bin/node` | React 빌드 |
| npm | 10.8.2 | `/usr/bin/npm` | 패키지 관리 |
| Python | 3.12.3 | `/usr/bin/python3` | GUARDiA ITSM, 배포 스크립트 |
| Apache Maven | 3.8.7 | `/usr/share/maven` | Spring Boot 빌드 |
### 2-2. 웹 서버 / 프록시
| SW | 버전 | 설치 경로 | 용도 |
|----|------|-----------|------|
| Nginx | 1.24.0 | `/etc/nginx` | 리버스 프록시, 정적 파일 서빙 |
### 2-3. 데이터베이스
| SW | 버전 | 설치 경로 | 용도 |
|----|------|-----------|------|
| PostgreSQL | 16.11 | `/var/lib/postgresql/16` | GUARDiA ITSM DB, Gitea DB |
| SQLite (내장) | 3.45.3 | JAR 내장 | 지오정보기술 홈페이지 DB |
**PostgreSQL 데이터베이스 목록:**
| DB 이름 | 소유자 | 용도 |
|---------|--------|------|
| `guardia_db` | guardia | GUARDiA ITSM 운영 DB |
| `zioinfo_db` | guardia | 지오정보기술 예비 DB |
| `gitea_db` | gitea | Gitea Git 서버 DB |
### 2-4. AI / LLM
| SW | 버전 | 설치 경로 | 용도 |
|----|------|-----------|------|
| Ollama | 0.24.0 | `/usr/local/bin/ollama` | 온프레미스 LLM 엔진 |
| Llama3:8b | — | `~/.ollama/models` | GUARDiA AI 에이전트 LLM (4.7 GB) |
### 2-5. DevOps / CI-CD
| SW | 버전 | 설치 경로 | 용도 |
|----|------|-----------|------|
| Gitea | 1.22.3 | `/opt/gitea/bin/gitea` | 온프레미스 Git 서버 |
| Jenkins | 2.504.1 LTS | `/opt/jenkins/jenkins.war` | CI/CD 파이프라인 (초기설정 필요) |
| ZioInfo Deploy Server | — | `/opt/zioinfo/deploy_server.py` | Gitea 웹훅 → 자동 빌드·배포 (Python) |
### 2-6. 애플리케이션 서비스
| 서비스 | 기술 스택 | 버전 | 용도 |
|--------|----------|------|------|
| 지오정보기술 홈페이지 | Spring Boot 3.2.5 + React | 1.0.0 | (주)지오정보기술 기업 홈페이지 |
| GUARDiA ITSM | Python FastAPI + PostgreSQL | — | AI 기반 인프라 자율 운영 ITSM |
---
## 3. 포트 구성
| 포트 | 프로토콜 | 서비스 | 외부 접근 |
|------|----------|--------|----------|
| 22 | TCP | SSH | ✅ 허용 (키 인증) |
| 80 | TCP | Nginx (홈페이지) | ✅ 허용 |
| 443 | TCP | HTTPS (미설정) | — |
| 3000 | TCP | Gitea Git 서버 | ✅ 허용 |
| 8001 | TCP | GUARDiA ITSM (FastAPI) | ✅ 허용 |
| 8080 | TCP | Jenkins CI/CD | ✅ 허용 |
| 8082 | TCP | Spring Boot (홈페이지 API) | ✅ 허용 |
| 8088 | TCP | Nginx → Jenkins 프록시 | ✅ 허용 |
| 9001 | TCP | Nginx → GUARDiA 프록시 | ✅ 허용 |
| 9999 | TCP | Deploy Webhook 서버 | ✅ 허용 (내부 권장) |
| 5432 | TCP | PostgreSQL | ❌ 내부 전용 (127.0.0.1) |
| 11434 | TCP | Ollama LLM API | ❌ 내부 전용 (127.0.0.1) |
---
## 4. 디렉터리 구조
```
/
├── etc/
│ ├── nginx/
│ │ ├── sites-available/
│ │ │ ├── zioinfo # 홈페이지 Nginx 설정
│ │ │ ├── guardia # GUARDiA ITSM Nginx 설정
│ │ │ ├── gitea # Gitea Nginx 설정
│ │ │ └── jenkins # Jenkins Nginx 설정
│ │ └── sites-enabled/ # 활성 설정 심볼릭 링크
│ ├── gitea/
│ │ └── app.ini # Gitea 설정 파일
│ └── systemd/system/
│ ├── zioinfo.service # 홈페이지 Spring Boot 서비스
│ ├── zioinfo-deploy.service # CI/CD 웹훅 서버
│ ├── guardia.service # GUARDiA ITSM 서비스
│ ├── gitea.service # Gitea 서비스
│ ├── jenkins.service # Jenkins 서비스
│ └── ollama.service # Ollama LLM 서비스
├── opt/
│ ├── zioinfo/
│ │ ├── app/
│ │ │ ├── app.jar # Spring Boot 실행 JAR
│ │ │ └── data/ # SQLite DB 디렉터리
│ │ ├── src/ # 소스 코드 (Gitea 클론)
│ │ └── deploy_server.py # CI/CD 웹훅 서버
│ ├── guardia/
│ │ ├── app/ # GUARDiA ITSM 소스
│ │ ├── venv/ # Python 가상환경
│ │ ├── logs/ # 애플리케이션 로그
│ │ └── uploads/ # 업로드 파일
│ ├── gitea/
│ │ └── bin/gitea # Gitea 바이너리
│ └── jenkins/
│ └── jenkins.war # Jenkins WAR 파일
├── var/
│ ├── www/
│ │ └── zioinfo/ # React 빌드 정적 파일
│ ├── lib/
│ │ ├── jenkins/ # Jenkins 홈 디렉터리
│ │ └── gitea/ # Gitea 데이터 디렉터리
│ │ └── data/repositories/ # Git 저장소
│ └── log/
│ └── zioinfo/
│ ├── spring.log # Spring Boot 로그
│ └── deploy.log # CI/CD 배포 로그
└── home/
└── git/ # Gitea 시스템 사용자 홈
```
---
## 5. 계정 및 자격증명
> **보안 주의**: 이 문서는 내부망 전용입니다. 외부 공개 금지.
### 5-1. 시스템 계정
| 계정 | 역할 | 비밀번호 |
|------|------|---------|
| `root` | 서버 관리자 | `1q2w3e!Q` |
| `ubuntu` | 앱 실행 계정 (예비) | `ubuntu123` |
| `jenkins` | Jenkins / Spring Boot 실행 | 시스템 계정 (로그인 불가) |
| `git` | Gitea 실행 | 시스템 계정 (로그인 불가) |
| `postgres` | PostgreSQL 관리 | 시스템 계정 |
### 5-2. 서비스 계정
| 서비스 | 아이디 | 비밀번호 | 권한 |
|--------|--------|---------|------|
| **홈페이지 관리자** | `admin` | `Admin@2026!` | 전체 관리 |
| **Gitea** | `zio` | `Zio@Admin2026!` | 관리자 |
| **Jenkins** | `admin` | `Admin@2026!` | 전체 |
| **PostgreSQL (guardia)** | `guardia` | `G@urd1a_2026!` | guardia_db, zioinfo_db |
| **PostgreSQL (gitea)** | `gitea` | `G1tea_2026!` | gitea_db |
| **GUARDiA ITSM** | `admin` | `Admin@2026!` | 전체 |
### 5-3. SSH 접속
```bash
# PEM 키 방식 (권장)
ssh -i "zio-server-key.pem" root@zioinfo.co.kr
# 비밀번호 방식
ssh root@zioinfo.co.kr
# 비밀번호: 1q2w3e!Q
```
---
## 6. Python 가상환경 패키지 (GUARDiA)
경로: `/opt/guardia/venv`
| 패키지 | 버전 | 용도 |
|--------|------|------|
| fastapi | 0.115.0 | API 프레임워크 |
| uvicorn | 0.30.0 | ASGI 서버 |
| sqlalchemy | 2.0.35 | ORM |
| asyncpg | 0.29.0 | PostgreSQL 비동기 드라이버 |
| psycopg2-binary | 2.9.9 | PostgreSQL 드라이버 |
| paramiko | 3.4.0 | SSH/SFTP 에이전트리스 연결 |
| python-jose | 3.3.0 | JWT 인증 |
| passlib | 1.7.4 | 비밀번호 해싱 |
| openpyxl | 3.1.5 | Excel 출력 |
| cryptography | 43.0.1 | AES-256 암호화 |
---
## 7. Spring Boot 의존성 (홈페이지)
| 의존성 | 버전 | 용도 |
|--------|------|------|
| spring-boot-starter-web | 3.2.5 | REST API |
| spring-boot-starter-data-jpa | 3.2.5 | ORM |
| spring-boot-starter-security | 3.2.5 | JWT 인증 |
| spring-boot-starter-mail | 3.2.5 | 이메일 발송 |
| sqlite-jdbc | 3.45.3.0 | SQLite 드라이버 |
| jjwt-api | 0.12.3 | JWT 처리 |
| lombok | — | 코드 생성 |
---
## 8. Gitea 저장소 목록
| 저장소 | URL | 용도 |
|--------|-----|------|
| `zio/zioinfo-web` | `http://zioinfo.co.kr:3000/zio/zioinfo-web` | 홈페이지 소스 |
| `zio/guardia-itsm` | `http://zioinfo.co.kr:3000/zio/guardia-itsm` | GUARDiA ITSM 소스 |
---
## 9. 방화벽 (UFW) 규칙
```bash
# 현재 활성 규칙 조회
ufw status
# 주요 허용 포트
22/tcp (SSH)
80/tcp (HTTP - 홈페이지)
443/tcp (HTTPS - 미사용)
3000/tcp (Gitea)
8001/tcp (GUARDiA ITSM)
8080/tcp (Jenkins)
8082/tcp (Spring Boot API)
9999/tcp (Deploy Webhook)
```

View File

@ -0,0 +1,649 @@
# zio-server 운영 가이드
> **서버**: zio-server | **IP**: zioinfo.co.kr
> **작성일**: 2026-05-30 | **대상**: 서버 운영자 / 개발자
---
## 1. 서비스 접속 주소
| 서비스 | URL | 계정 |
|--------|-----|------|
| 지오정보기술 홈페이지 | http://zioinfo.co.kr | — |
| 홈페이지 관리자 | http://zioinfo.co.kr/admin | admin / Admin@2026! |
| GUARDiA ITSM | http://zioinfo.co.kr:8001 | admin / Admin@2026! |
| Gitea (Git 서버) | http://zioinfo.co.kr:3000 | zio / Zio@Admin2026! |
| Jenkins (CI/CD) | http://zioinfo.co.kr:8080 | admin / Admin@2026! |
---
## 2. 서버 접속
```bash
# SSH 키 인증 (권장)
ssh -i "zio-server-key.pem" root@zioinfo.co.kr
# 비밀번호 인증
ssh root@zioinfo.co.kr
# 비밀번호: 1q2w3e!Q
```
---
## 3. 서비스 관리
### 3-1. 전체 상태 확인
```bash
# 모든 서비스 상태 한 번에 확인
for svc in nginx zioinfo zioinfo-deploy guardia gitea jenkins postgresql ollama; do
status=$(systemctl is-active $svc 2>/dev/null)
printf "%-22s %s\n" $svc $status
done
```
### 3-2. 개별 서비스 명령
```bash
# 서비스 시작 / 중지 / 재시작 / 상태
systemctl start <서비스명>
systemctl stop <서비스명>
systemctl restart <서비스명>
systemctl status <서비스명>
# 부팅 자동시작 설정 / 해제
systemctl enable <서비스명>
systemctl disable <서비스명>
```
**서비스명 목록:**
| 서비스명 | 설명 |
|---------|------|
| `nginx` | 웹 프록시 서버 |
| `zioinfo` | 지오정보기술 홈페이지 (Spring Boot, 포트 8082) |
| `zioinfo-deploy` | CI/CD 웹훅 서버 (포트 9999) |
| `guardia` | GUARDiA ITSM (FastAPI, 포트 8001) |
| `gitea` | Git 서버 (포트 3000) |
| `jenkins` | CI/CD 파이프라인 (포트 8080) |
| `postgresql` | 데이터베이스 (포트 5432) |
| `ollama` | LLM 엔진 (포트 11434) |
---
## 4. 로그 확인
```bash
# Spring Boot 홈페이지 로그
tail -f /var/log/zioinfo/spring.log
# CI/CD 배포 로그 (실시간)
tail -f /var/log/zioinfo/deploy.log
# GUARDiA ITSM 로그
tail -f /opt/guardia/logs/guardia.log
tail -f /opt/guardia/logs/error.log
# Nginx 접근 로그
tail -f /var/log/nginx/access.log
tail -f /var/log/nginx/error.log
# systemd 서비스 로그 (최근 50줄)
journalctl -u zioinfo -n 50 --no-pager
journalctl -u guardia -n 50 --no-pager
journalctl -u gitea -n 50 --no-pager
# 실시간 로그 스트리밍
journalctl -u zioinfo -f
```
---
## 5. 홈페이지 배포 (CI/CD)
### 5-1. 자동 배포 흐름
```
로컬 코드 수정
git add . && git commit -m "메시지"
git push gitea main:main ← Gitea에 push
Gitea 웹훅 → localhost:9999 호출
자동 빌드 파이프라인 실행:
① git pull (소스 갱신)
② npm build (React 빌드)
③ mvn package (Spring Boot JAR 빌드)
④ 파일 복사 (JAR → /opt/zioinfo/app, 정적 → /var/www/zioinfo)
⑤ systemctl restart zioinfo
배포 완료 (약 2~4분 소요)
```
### 5-2. 로컬에서 Gitea push
```bash
# 최초 remote 설정 (1회)
cd workspace/zioinfo-web
git remote add gitea http://zio:Zio%40Admin2026%21@zioinfo.co.kr:3000/zio/zioinfo-web.git
# 이후 배포
git add .
git commit -m "feat: 변경 내용 설명"
git push gitea main:main
```
### 5-3. 배포 상태 모니터링
```bash
# 배포 로그 실시간 확인
ssh root@zioinfo.co.kr "tail -f /var/log/zioinfo/deploy.log"
# 배포 완료 후 서비스 상태
ssh root@zioinfo.co.kr "systemctl is-active zioinfo && curl -s -o /dev/null -w 'HTTP %{http_code}' http://localhost:8082/api/company"
```
### 5-4. 수동 배포 (긴급 시)
```bash
# 서버에서 직접 수동 배포
ssh root@zioinfo.co.kr
# 소스 갱신
cd /opt/zioinfo/src && git pull origin main
# React 빌드
cd frontend && npm ci && npm run build
# Spring Boot 빌드
cd ../backend && mvn clean package -DskipTests -q
# 배포
cp target/zioinfo-web-*.jar /opt/zioinfo/app/app.jar
cp -r src/main/resources/static/. /var/www/zioinfo/
systemctl restart zioinfo
```
---
## 6. GUARDiA ITSM 관리
### 6-1. 서비스 재시작
```bash
systemctl restart guardia
journalctl -u guardia -n 20 --no-pager
```
### 6-2. Python 패키지 업데이트
```bash
source /opt/guardia/venv/bin/activate
pip install -r /opt/guardia/app/requirements.txt
deactivate
systemctl restart guardia
```
### 6-3. 환경변수 설정
```bash
# .env 파일 편집
nano /opt/guardia/app/.env
# 주요 항목:
# DATABASE_URL=postgresql+asyncpg://guardia:G@urd1a_2026!@localhost:5432/guardia_db
# OLLAMA_BASE_URL=http://localhost:11434
# LLM_MODEL=llama3:8b
# SECRET_KEY=<JWT 시크릿>
```
---
## 7. 데이터베이스 관리
### 7-1. PostgreSQL 접속
```bash
# postgres 관리자로 접속
sudo -u postgres psql
# guardia DB 접속
psql -h 127.0.0.1 -U guardia -d guardia_db
비밀번호: G@urd1a_2026!
# gitea DB 접속
psql -h 127.0.0.1 -U gitea -d gitea_db
비밀번호: G1tea_2026!
```
### 7-2. 데이터베이스 백업
```bash
# 전체 백업
pg_dump -U guardia guardia_db > /opt/guardia/backups/guardia_$(date +%Y%m%d).sql
pg_dump -U gitea gitea_db > /opt/gitea/backup/gitea_$(date +%Y%m%d).sql
# 복원
psql -U guardia guardia_db < /opt/guardia/backups/guardia_20260530.sql
```
### 7-3. 홈페이지 SQLite 백업
```bash
# SQLite DB 파일 직접 복사
cp /opt/zioinfo/app/data/zioinfo.db /opt/zioinfo/app/data/zioinfo_$(date +%Y%m%d).db.bak
```
---
## 8. Nginx 관리
### 8-1. 설정 테스트 및 리로드
```bash
# 설정 문법 검사
nginx -t
# 무중단 리로드 (설정 변경 반영)
systemctl reload nginx
# 설정 파일 위치
ls /etc/nginx/sites-available/
```
### 8-2. 사이트 설정 파일
| 파일 | 역할 |
|------|------|
| `/etc/nginx/sites-available/zioinfo` | 홈페이지 (포트 80 → 8082) |
| `/etc/nginx/sites-available/guardia` | GUARDiA (포트 9001 → 8001) |
| `/etc/nginx/sites-available/gitea` | Gitea (포트 3001 → 3000) |
| `/etc/nginx/sites-available/jenkins` | Jenkins (포트 8088 → 8080) |
### 8-3. Nginx 설정 변경 절차
```bash
# 1. 설정 파일 편집
nano /etc/nginx/sites-available/zioinfo
# 2. 문법 검사
nginx -t
# 3. 적용
systemctl reload nginx
```
---
## 9. Gitea 관리
### 9-1. 저장소 관리
```bash
# API로 저장소 목록 확인
curl -s http://localhost:3000/api/v1/repos/search -u "zio:Zio@Admin2026!" \
| python3 -c "import json,sys; [print(r['full_name']) for r in json.load(sys.stdin)['data']]"
# 저장소 직접 경로
ls /var/lib/gitea/data/repositories/zio/
```
### 9-2. 웹훅 확인
```bash
# zioinfo-web 웹훅 목록
curl -s http://localhost:3000/api/v1/repos/zio/zioinfo-web/hooks \
-u "zio:Zio@Admin2026!" | python3 -m json.tool
```
### 9-3. 설정 파일
```bash
# Gitea 설정 편집
nano /etc/gitea/app.ini
systemctl restart gitea
```
---
## 10. Jenkins 초기 설정 (최초 1회)
Jenkins는 브라우저에서 초기 설정을 완료해야 합니다.
### 10-1. 초기 설정 절차
1. 브라우저에서 `http://zioinfo.co.kr:8080` 접속
2. 초기 비밀번호 입력:
```bash
cat /var/lib/jenkins/secrets/initialAdminPassword
```
3. **"Install suggested plugins"** 선택
4. 추가 플러그인 설치:
- **Pipeline** (워크플로우 파이프라인)
- **Git** (Git 연동)
- **Gitea** (Gitea 웹훅 연동)
5. 관리자 계정 생성: `admin / Admin@2026!`
6. Jenkins URL 설정: `http://zioinfo.co.kr:8080`
### 10-2. 파이프라인 Job 생성
초기 설정 완료 후:
```
Jenkins → New Item → "zioinfo-web" → Pipeline 선택
→ Pipeline 탭 → Definition: "Pipeline script from SCM"
→ SCM: Git
→ Repository URL: http://localhost:3000/zio/zioinfo-web.git
→ Branch: main
→ Script Path: Jenkinsfile
→ 저장
```
---
## 11. Ollama / LLM 관리
### 11-1. 모델 관리
```bash
# 설치된 모델 목록
ollama list
# 모델 다운로드 (4.7GB, 시간 소요)
ollama pull llama3:8b
# 모델 테스트
ollama run llama3:8b "안녕하세요, 간단한 테스트입니다."
# API 호출 테스트
curl http://localhost:11434/api/generate \
-d '{"model":"llama3:8b","prompt":"Hello","stream":false}'
```
### 11-2. 서비스 재시작
```bash
systemctl restart ollama
# 재시작 후 약 10~30초 대기 (모델 로딩)
```
---
## 12. 리소스 모니터링
### 12-1. 실시간 모니터링
```bash
# 전체 리소스 현황
htop
# 디스크 사용량
df -h
# 메모리 사용량
free -h
# 포트 사용 현황
ss -tlnp
# 프로세스별 리소스
ps aux --sort=-%cpu | head -15
ps aux --sort=-%mem | head -15
```
### 12-2. 서비스별 리소스 현황
| 서비스 | CPU | RAM |
|--------|-----|-----|
| Nginx | ~0.1% | ~50 MB |
| Spring Boot (홈페이지) | ~0.5% | ~400 MB |
| GUARDiA ITSM (FastAPI) | ~0.5% | ~300 MB |
| PostgreSQL | ~0.3% | ~256 MB |
| Ollama + Llama3:8b | ~1.0% (유휴) | ~4.7 GB |
| Jenkins | ~0.2% | ~512 MB |
| Gitea | ~0.1% | ~150 MB |
| **합계** | **~2.7%** | **~6.4 GB** |
---
## 13. 보안 운영
### 13-1. SSH 보안 강화 (권장)
```bash
# root 비밀번호 로그인 비활성화 (키 인증만 허용)
sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
systemctl restart sshd
```
### 13-2. 방화벽 규칙 관리
```bash
# 현재 규칙 확인
ufw status numbered
# 규칙 추가
ufw allow <포트>/tcp
# 규칙 삭제
ufw delete <번호>
# 특정 IP만 허용 (예: 관리자 IP)
ufw allow from 203.xxx.xxx.xxx to any port 8080
```
### 13-3. SSL/HTTPS 설정 (도메인 보유 시)
```bash
# Let's Encrypt 인증서 발급
apt install certbot python3-certbot-nginx
certbot --nginx -d zioinfo.co.kr -d www.zioinfo.co.kr
# 자동 갱신 확인
certbot renew --dry-run
```
---
## 14. 장애 대응
### 14-1. 홈페이지 접속 불가
```bash
# 1. Nginx 상태 확인
systemctl status nginx
nginx -t
# 2. Spring Boot 상태 확인
systemctl status zioinfo
curl -s http://localhost:8082/api/company
# 3. 로그 확인
tail -50 /var/log/zioinfo/spring.log
# 4. 재시작
systemctl restart zioinfo
systemctl restart nginx
```
### 14-2. 배포 실패
```bash
# 1. 배포 로그 확인
tail -50 /var/log/zioinfo/deploy.log
# 2. CI/CD 서버 상태
systemctl status zioinfo-deploy
# 3. 소스 상태 확인
git -C /opt/zioinfo/src status
git -C /opt/zioinfo/src log --oneline -5
# 4. 수동 배포 실행 (5-4절 참고)
```
### 14-3. GUARDiA ITSM 오류
```bash
# 1. 서비스 상태
systemctl status guardia
tail -20 /opt/guardia/logs/error.log
# 2. PostgreSQL 연결 확인
psql -h 127.0.0.1 -U guardia -d guardia_db -c "SELECT 1"
# 3. 재시작
systemctl restart guardia
```
### 14-4. 데이터베이스 연결 오류
```bash
# PostgreSQL 상태
systemctl status postgresql
sudo -u postgres psql -c "\l"
# 재시작
systemctl restart postgresql
# 연결 확인
psql -h 127.0.0.1 -U guardia -d guardia_db -c "SELECT count(*) FROM pg_tables"
```
### 14-5. 메모리 부족
```bash
# 메모리 사용량 확인
free -h
ps aux --sort=-%mem | head -10
# 가장 많이 사용하는 서비스 재시작 (Ollama가 대부분 점유)
systemctl restart ollama
# Swap 임시 설정 (재부팅 시 사라짐)
fallocate -l 4G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
```
---
## 15. 백업 및 복구
### 15-1. 정기 백업 스크립트
```bash
#!/bin/bash
# /opt/backup/daily_backup.sh
DATE=$(date +%Y%m%d_%H%M)
BACKUP_DIR="/opt/backup/${DATE}"
mkdir -p "$BACKUP_DIR"
# DB 백업
sudo -u postgres pg_dump guardia_db > "$BACKUP_DIR/guardia_db.sql"
sudo -u postgres pg_dump gitea_db > "$BACKUP_DIR/gitea_db.sql"
# SQLite 백업
cp /opt/zioinfo/app/data/zioinfo.db "$BACKUP_DIR/zioinfo.db"
# 설정 파일 백업
cp -r /etc/nginx/sites-available "$BACKUP_DIR/nginx/"
cp -r /etc/gitea "$BACKUP_DIR/gitea/"
cp /opt/guardia/app/.env "$BACKUP_DIR/guardia.env"
# 30일 이전 백업 삭제
find /opt/backup -maxdepth 1 -type d -mtime +30 -exec rm -rf {} \;
echo "백업 완료: $BACKUP_DIR"
```
```bash
# cron 등록 (매일 새벽 2시)
crontab -e
# 추가:
0 2 * * * /opt/backup/daily_backup.sh >> /var/log/backup.log 2>&1
```
---
## 16. 자주 쓰는 명령어 모음
```bash
# ── 전체 서비스 상태 ──────────────────────────────
for s in nginx zioinfo guardia gitea jenkins postgresql ollama; do
printf "%-15s %s\n" $s $(systemctl is-active $s)
done
# ── 포트 사용 현황 ────────────────────────────────
ss -tlnp | awk '{print $4}' | sort
# ── 디스크 / 메모리 ──────────────────────────────
df -h / && free -h
# ── 홈페이지 API 테스트 ───────────────────────────
curl -s http://localhost/api/company | python3 -m json.tool
# ── 홈페이지 관리자 로그인 테스트 ────────────────
curl -s -X POST http://localhost/api/admin/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"Admin@2026!"}' \
| python3 -c "import json,sys; d=json.load(sys.stdin); print('Token:', d.get('token','FAIL')[:20]+'...')"
# ── Gitea 저장소 목록 ─────────────────────────────
curl -s http://localhost:3000/api/v1/repos/search \
-u "zio:Zio@Admin2026!" | python3 -c \
"import json,sys; [print(r['full_name']) for r in json.load(sys.stdin)['data']]"
# ── 배포 수동 트리거 ─────────────────────────────
curl -s -X POST http://localhost:9999/ \
-H "Content-Type: application/json" -d '{}'
# ── Ollama 모델 확인 ─────────────────────────────
ollama list
# ── 최근 에러 로그 ───────────────────────────────
journalctl -p err --since "1 hour ago" --no-pager
```
---
## 17. 업데이트 및 유지보수
### 17-1. 시스템 패키지 업데이트
```bash
apt-get update && apt-get upgrade -y
# 주의: 업데이트 전 서비스 상태 확인 필수
# 커널 업데이트 시 재부팅 필요
```
### 17-2. 애플리케이션 업데이트
```bash
# Gitea 업데이트
wget -q https://dl.gitea.com/gitea/X.X.X/gitea-X.X.X-linux-amd64 \
-O /opt/gitea/bin/gitea
chmod +x /opt/gitea/bin/gitea
systemctl restart gitea
# Ollama 업데이트
curl -fsSL https://ollama.com/install.sh | sh
systemctl restart ollama
```
### 17-3. LLM 모델 업데이트
```bash
# 새 버전 모델 다운로드
ollama pull llama3:8b
# 구 버전 삭제
ollama rm llama3:7b
```
---
*문서 버전: 1.0 | 최종 수정: 2026-05-30*

View File

@ -0,0 +1,346 @@
# zio-server CI/CD 파이프라인 가이드
> **서버**: zio-server | **IP**: zioinfo.co.kr
> **작성일**: 2026-05-30
---
## 1. CI/CD 아키텍처
```
┌─────────────────────────────────────────────────────────┐
│ 개발자 로컬 PC │
│ 코드 수정 → git commit → git push gitea main:main │
└──────────────────────┬──────────────────────────────────┘
│ HTTP push (포트 3000)
┌─────────────────────────────────────────────────────────┐
│ Gitea (zioinfo.co.kr:3000) │
│ zio/zioinfo-web 저장소 → 웹훅 트리거 │
└──────────────────────┬──────────────────────────────────┘
│ POST http://localhost:9999/
┌─────────────────────────────────────────────────────────┐
│ ZioInfo Deploy Server (포트 9999) │
│ /opt/zioinfo/deploy_server.py │
│ │
│ ① git pull (소스 최신화) │
│ ② npm ci && npm run build (React SPA 빌드) │
│ ③ mvn clean package (Spring Boot JAR 빌드) │
│ ④ cp app.jar /opt/zioinfo/app/ │
│ ⑤ cp static/* /var/www/zioinfo/ │
│ ⑥ systemctl restart zioinfo │
└──────────────────────┬──────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Spring Boot (포트 8082) │
│ /opt/zioinfo/app/app.jar │
│ DB: /opt/zioinfo/app/data/zioinfo.db (SQLite) │
└──────────────────────┬──────────────────────────────────┘
│ proxy_pass
┌─────────────────────────────────────────────────────────┐
│ Nginx (포트 80) │
│ /var/www/zioinfo/ → React SPA 정적 파일 │
│ /api/ → localhost:8082 프록시 │
└─────────────────────────────────────────────────────────┘
http://zioinfo.co.kr (서비스 엔드포인트)
```
---
## 2. 로컬 개발 환경 설정
### 2-1. Git remote 설정 (최초 1회)
```bash
# 현재 remote 확인
git remote -v
# Gitea remote 추가
cd C:\GUARDiA\workspace\zioinfo-web
git remote add gitea http://zio:Zio%40Admin2026%21@zioinfo.co.kr:3000/zio/zioinfo-web.git
# 설정 확인
git remote -v
# gitea http://zio:...@zioinfo.co.kr:3000/zio/zioinfo-web.git (fetch)
# gitea http://zio:...@zioinfo.co.kr:3000/zio/zioinfo-web.git (push)
# origin https://github.com/... (fetch) ← GitHub (기존)
```
### 2-2. 코드 배포
```bash
# 1. 변경 사항 커밋
git add .
git commit -m "feat: 기능 설명"
# 2. Gitea에 push → 자동 빌드·배포 시작
git push gitea main:main
# 3. 배포 진행 상황 확인 (SSH 접속 후)
ssh root@zioinfo.co.kr "tail -f /var/log/zioinfo/deploy.log"
```
---
## 3. 배포 서버 구성
### 3-1. 파일 위치
| 파일 | 경로 | 설명 |
|------|------|------|
| 배포 스크립트 | `/opt/zioinfo/deploy_server.py` | 웹훅 수신 + 빌드·배포 실행 |
| 소스 코드 | `/opt/zioinfo/src/` | Gitea 클론 경로 |
| 실행 JAR | `/opt/zioinfo/app/app.jar` | 배포된 Spring Boot JAR |
| 정적 파일 | `/var/www/zioinfo/` | 배포된 React 빌드 결과물 |
| 배포 로그 | `/var/log/zioinfo/deploy.log` | 빌드·배포 과정 기록 |
### 3-2. systemd 서비스
```ini
# /etc/systemd/system/zioinfo-deploy.service
[Unit]
Description=ZioInfo CI/CD Webhook Server
After=network.target
[Service]
User=root
ExecStart=/usr/bin/python3 /opt/zioinfo/deploy_server.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
```
```bash
# 서비스 관리
systemctl status zioinfo-deploy
systemctl restart zioinfo-deploy
journalctl -u zioinfo-deploy -n 20 --no-pager
```
### 3-3. 웹훅 보안
배포 서버는 HMAC-SHA256으로 요청을 검증합니다.
```python
# 검증 키: zioinfo-deploy-2026
# Gitea 웹훅 설정의 Secret 값과 동일
```
---
## 4. Gitea 웹훅 설정
### 4-1. 현재 웹훅 확인
```bash
# API로 웹훅 목록 조회
curl -s http://localhost:3000/api/v1/repos/zio/zioinfo-web/hooks \
-u "zio:Zio@Admin2026!" | python3 -m json.tool
# 브라우저에서 확인
# http://zioinfo.co.kr:3000/zio/zioinfo-web/settings/hooks
```
### 4-2. 웹훅 재등록 (필요 시)
```bash
# 기존 웹훅 삭제
curl -s -X DELETE http://localhost:3000/api/v1/repos/zio/zioinfo-web/hooks/1 \
-u "zio:Zio@Admin2026!"
# 새 웹훅 등록 (Python)
python3 << 'EOF'
import urllib.request, json, base64
data = json.dumps({
"type": "gitea",
"active": True,
"config": {
"url": "http://localhost:9999/",
"content_type": "json",
"secret": "zioinfo-deploy-2026"
},
"events": ["push"]
}).encode()
cred = base64.b64encode(b"zio:Zio@Admin2026!").decode()
req = urllib.request.Request(
"http://localhost:3000/api/v1/repos/zio/zioinfo-web/hooks",
data=data,
headers={"Content-Type": "application/json", "Authorization": f"Basic {cred}"})
resp = urllib.request.urlopen(req)
d = json.loads(resp.read())
print(f"Webhook ID: {d.get('id')}")
EOF
```
---
## 5. 배포 이력 및 롤백
### 5-1. 배포 이력 확인
```bash
# Gitea 커밋 이력
curl -s "http://localhost:3000/api/v1/repos/zio/zioinfo-web/commits?limit=10" \
-u "zio:Zio@Admin2026!" | python3 -c "
import json, sys
commits = json.load(sys.stdin)
for c in commits:
print(c['sha'][:8], c['commit']['message'][:60], '|', c['commit']['author']['date'][:10])
"
# 서버 소스 로컬 이력
ssh root@zioinfo.co.kr "git -C /opt/zioinfo/src log --oneline -10"
```
### 5-2. 특정 버전으로 롤백
```bash
ssh root@zioinfo.co.kr
# 이전 커밋으로 체크아웃
cd /opt/zioinfo/src
git log --oneline -5 # 커밋 해시 확인
git checkout <커밋해시> # 원하는 버전으로 변경
# 빌드 및 재배포
cd frontend && npm ci && npm run build
cd ../backend && mvn clean package -DskipTests -q
cp target/zioinfo-web-*.jar /opt/zioinfo/app/app.jar
cp -r src/main/resources/static/. /var/www/zioinfo/
systemctl restart zioinfo
# 최신 버전으로 복구
git checkout main
```
---
## 6. Jenkins 파이프라인 (심화 설정)
> Jenkins 초기 설정 완료 후 사용 가능
### 6-1. Jenkinsfile 위치
```
workspace/zioinfo-web/Jenkinsfile (홈페이지 파이프라인)
itsm/Jenkinsfile (GUARDiA ITSM 파이프라인)
```
### 6-2. 홈페이지 Jenkinsfile 주요 단계
```groovy
pipeline {
agent any
stages {
stage('Frontend Build') { /* npm ci + npm run build */ }
stage('Backend Build') { /* mvn clean package */ }
stage('Test') { /* mvn test */ }
stage('Deploy') {
when { branch 'main' }
/* JAR 복사 + 정적파일 배포 + systemctl restart */
}
}
}
```
### 6-3. Jenkins Job 수동 트리거
```bash
# Jenkins API로 빌드 트리거 (초기 설정 완료 후)
CRUMB=$(curl -s -u admin:Admin@2026! \
http://localhost:8080/crumbIssuer/api/json | python3 -c \
"import json,sys; print(json.load(sys.stdin)['crumb'])")
curl -u admin:Admin@2026! -X POST \
http://localhost:8080/job/zioinfo-web/build \
-H "Jenkins-Crumb: $CRUMB"
```
---
## 7. 트러블슈팅
### 7-1. push 후 자동 배포가 안 될 때
```bash
# 1. 웹훅 서버 상태 확인
systemctl is-active zioinfo-deploy
# 2. 웹훅 서버 로그
journalctl -u zioinfo-deploy -n 20 --no-pager
# 3. 수동 트리거 테스트
curl -s -X POST http://localhost:9999/ \
-H "Content-Type: application/json" -d '{}'
# 4. 배포 로그 실시간 확인
tail -f /var/log/zioinfo/deploy.log
```
### 7-2. 빌드 실패 시
```bash
# 빌드 로그에서 오류 확인
grep -i "error\|failed\|fail" /var/log/zioinfo/deploy.log | tail -20
# npm 빌드 오류 재현
cd /opt/zioinfo/src/frontend
npm ci --legacy-peer-deps
npm run build
# Maven 빌드 오류 재현
cd /opt/zioinfo/src/backend
mvn clean package -DskipTests
```
### 7-3. Spring Boot 재시작 실패
```bash
# 상세 로그 확인
journalctl -u zioinfo --since "5 minutes ago" --no-pager
tail -50 /var/log/zioinfo/spring.log
# 포트 충돌 확인 (8082)
ss -tlnp | grep 8082
kill -9 <PID> # 기존 프로세스 강제 종료
# 수동 실행 (디버깅)
java -jar /opt/zioinfo/app/app.jar --server.port=8082
```
---
## 8. GUARDiA ITSM 배포
> GUARDiA는 별도 Jenkinsfile 기반 파이프라인 사용
### 8-1. 수동 배포
```bash
# Gitea에서 최신 코드 반영
cd /opt/guardia/app
git pull http://zio:Zio%40Admin2026%21@localhost:3000/zio/guardia-itsm.git main
# 패키지 업데이트
/opt/guardia/venv/bin/pip install -r requirements.txt -q
# 서비스 재시작
systemctl restart guardia
sleep 3 && systemctl is-active guardia
# 헬스체크
curl -s http://localhost:8001/docs -o /dev/null -w "HTTP %{http_code}"
```
---
*문서 버전: 1.0 | 최종 수정: 2026-05-30*

View File

@ -0,0 +1,409 @@
# zio-server 장애 대응 가이드 (Runbook)
> **서버**: zio-server | **IP**: zioinfo.co.kr
> **작성일**: 2026-05-30
> **목적**: 장애 발생 시 빠른 진단과 복구를 위한 단계별 절차서
---
## 긴급 연락 및 접속
```bash
# 즉시 서버 접속
ssh -i "zio-server-key.pem" root@zioinfo.co.kr
# 또는
ssh root@zioinfo.co.kr # 비밀번호: 1q2w3e!Q
```
---
## 1. 빠른 현황 진단 (첫 번째로 실행)
```bash
#!/bin/bash
echo "=== 서비스 상태 ==="
for s in nginx zioinfo zioinfo-deploy guardia gitea jenkins postgresql ollama; do
status=$(systemctl is-active $s 2>/dev/null)
icon="✅"; [ "$status" != "active" ] && icon="❌"
printf "%s %-22s %s\n" "$icon" "$s" "$status"
done
echo ""
echo "=== 리소스 ==="
free -h | grep Mem
df -h / | tail -1
echo ""
echo "=== 최근 에러 ==="
journalctl -p err --since "30 minutes ago" --no-pager | tail -10
```
---
## 2. 장애 유형별 대응
---
### CASE 1: 홈페이지(http://zioinfo.co.kr) 접속 불가
**증상**: 브라우저에서 접속 시 연결 거부 또는 502 오류
```bash
# Step 1: Nginx 확인
systemctl is-active nginx || {
nginx -t && systemctl start nginx
echo "Nginx 재시작"
}
# Step 2: Nginx 502 (백엔드 응답 없음)
curl -s -o /dev/null -w "%{http_code}" http://localhost:8082/api/company
# → 000 또는 연결 실패 시: Spring Boot 재시작
systemctl restart zioinfo
sleep 8
systemctl is-active zioinfo
# Step 3: Spring Boot 로그 확인
tail -30 /var/log/zioinfo/spring.log
# Step 4: 포트 확인
ss -tlnp | grep -E ":(80|8082)"
# 완료 확인
curl -s -o /dev/null -w "홈페이지: HTTP %{http_code}\n" http://localhost/
```
**예상 복구 시간**: 1~3분
---
### CASE 2: 관리자 페이지(/admin) 로그인 불가
**증상**: admin/Admin@2026! 로그인 실패 또는 JWT 오류
```bash
# Step 1: Spring Boot API 응답 확인
curl -s -X POST http://localhost/api/admin/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"Admin@2026!"}' | python3 -m json.tool
# Step 2: 401 Unauthorized → 계정 확인
# SQLite DB 접속
sqlite3 /opt/zioinfo/app/data/zioinfo.db "SELECT username, enabled FROM admin_user;"
# Step 3: 계정 비활성화 시 활성화
sqlite3 /opt/zioinfo/app/data/zioinfo.db \
"UPDATE admin_user SET enabled=1 WHERE username='admin';"
# Step 4: Spring Boot 재시작
systemctl restart zioinfo
```
---
### CASE 3: GUARDiA ITSM(포트 8001) 응답 없음
**증상**: `http://zioinfo.co.kr:8001` 접속 불가
```bash
# Step 1: 서비스 상태
systemctl status guardia
# Step 2: 로그 확인
tail -30 /opt/guardia/logs/error.log
# Step 3: PostgreSQL 연결 확인
systemctl is-active postgresql || systemctl start postgresql
psql -h 127.0.0.1 -U guardia -d guardia_db -c "SELECT 1" 2>&1
# Step 4: GUARDiA 재시작
systemctl restart guardia
sleep 5
# Step 5: FastAPI 응답 확인
curl -s -o /dev/null -w "GUARDiA: HTTP %{http_code}\n" http://localhost:8001/docs
```
**예상 복구 시간**: 2~5분
---
### CASE 4: PostgreSQL 연결 오류
**증상**: GUARDiA 또는 Gitea에서 DB 연결 실패 로그
```bash
# Step 1: 상태 확인
systemctl status postgresql
# Step 2: 재시작
systemctl restart postgresql
sleep 3
# Step 3: 연결 테스트
sudo -u postgres psql -c "\l" 2>&1
# Step 4: 로그 확인
tail -20 /var/log/postgresql/postgresql-16-main.log
# Step 5: DB 접속 정상화 확인
psql -h 127.0.0.1 -U guardia -d guardia_db -c "SELECT count(*) FROM pg_tables"
```
---
### CASE 5: CI/CD 배포 실패
**증상**: `git push` 후 변경 내용이 적용되지 않음
```bash
# Step 1: 웹훅 서버 상태
systemctl is-active zioinfo-deploy
journalctl -u zioinfo-deploy -n 20 --no-pager
# Step 2: 배포 로그 확인
tail -50 /var/log/zioinfo/deploy.log
# 오류 유형별 대응:
# [git pull] 실패 → 네트워크 또는 Gitea 확인
systemctl is-active gitea
curl -s http://localhost:3000/api/v1/version -u "zio:Zio@Admin2026!"
# [npm build] 실패 → node_modules 재설치
cd /opt/zioinfo/src/frontend && rm -rf node_modules && npm install --legacy-peer-deps
# [mvn package] 실패 → Maven 캐시 클리어
cd /opt/zioinfo/src/backend && mvn clean -q
# Step 3: 수동 배포 실행
cd /opt/zioinfo/src
git pull origin main
cd frontend && npm ci && npm run build
cd ../backend && mvn clean package -DskipTests -q
cp target/zioinfo-web-*.jar /opt/zioinfo/app/app.jar
cp -r src/main/resources/static/. /var/www/zioinfo/
systemctl restart zioinfo
```
---
### CASE 6: Gitea 접속 불가 (포트 3000)
**증상**: `http://zioinfo.co.kr:3000` 접속 불가
```bash
# Step 1: 서비스 상태
systemctl status gitea
journalctl -u gitea -n 20 --no-pager
# Step 2: PostgreSQL 확인 (Gitea DB 의존)
systemctl is-active postgresql
# Step 3: 재시작
systemctl restart gitea
sleep 5
curl -s -o /dev/null -w "Gitea: HTTP %{http_code}\n" http://localhost:3000/
```
---
### CASE 7: 서버 메모리 부족 (OOM)
**증상**: 서비스 갑자기 종료, journalctl에서 OOM killer 메시지
```bash
# Step 1: 메모리 현황
free -h
dmesg | grep -i "oom" | tail -10
# Step 2: 메모리 사용량 상위 프로세스
ps aux --sort=-%mem | head -10
# Step 3: Ollama가 주요 원인 (4.7GB 점유)
# 사용 안 할 때 중지
systemctl stop ollama
# Step 4: 필수 서비스 재시작
for s in nginx zioinfo guardia postgresql gitea; do
systemctl is-active $s || systemctl start $s
done
# Step 5: 임시 스왑 설정 (재부팅 시 사라짐)
[ ! -f /swapfile ] && {
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo "2GB 스왑 설정 완료"
}
free -h
```
---
### CASE 8: 디스크 용량 부족
**증상**: 로그 쓰기 실패, DB 오류
```bash
# Step 1: 디스크 현황
df -h
du -sh /var/log/* 2>/dev/null | sort -rh | head -10
du -sh /opt/* 2>/dev/null | sort -rh | head -10
# Step 2: 로그 정리
# journald 로그 정리
journalctl --vacuum-size=500M
# Nginx 로그 압축
gzip /var/log/nginx/access.log.1 2>/dev/null
gzip /var/log/nginx/error.log.1 2>/dev/null
# 오래된 배포 로그 정리
find /var/log/zioinfo -name "*.log" -mtime +30 -delete
# Maven 캐시 정리 (~/.m2)
rm -rf /var/lib/jenkins/.m2/repository/*/
# npm 캐시 정리
npm cache clean --force 2>/dev/null
# Step 3: PostgreSQL 로그 정리
find /var/log/postgresql -name "*.log" -mtime +7 -delete
# Step 4: 확인
df -h
```
---
### CASE 9: Jenkins 응답 없음 (포트 8080)
```bash
# Step 1: 상태 확인
systemctl status jenkins
curl -s -o /dev/null -w "Jenkins: HTTP %{http_code}\n" http://localhost:8080/
# Step 2: 재시작
systemctl restart jenkins
sleep 10
systemctl is-active jenkins
# Step 3: Java 힙 부족 시 메모리 조정
# /etc/default/jenkins 편집
echo 'JAVA_ARGS="-Xmx512m -Xms256m"' >> /etc/default/jenkins
systemctl restart jenkins
```
---
### CASE 10: SSH 접속 불가
```bash
# 다른 방법으로 접속 (NCloud 콘솔에서 시리얼 콘솔 사용)
# 또는 NCloud 포털 → 서버 → 콘솔 접속
# SSH 서비스 재시작 (서버 콘솔에서)
systemctl restart sshd
systemctl is-active sshd
# 방화벽 SSH 허용 확인
ufw allow 22/tcp
ufw status | grep 22
```
---
## 3. 서버 재부팅 후 복구 절차
```bash
# 재부팅 후 전체 서비스 자동 시작 확인
systemctl list-units --type=service --state=failed
# 실패한 서비스가 있을 경우 개별 재시작
systemctl start <서비스명>
# 모든 서비스 한번에 시작
for s in postgresql gitea nginx zioinfo zioinfo-deploy guardia ollama jenkins; do
systemctl is-active $s || {
echo "Starting $s..."
systemctl start $s
sleep 2
}
done
# 전체 헬스체크
echo "=== 재부팅 후 헬스체크 ==="
curl -s -o /dev/null -w "홈페이지: HTTP %{http_code}\n" http://localhost/
curl -s -o /dev/null -w "API: HTTP %{http_code}\n" http://localhost/api/company
curl -s -o /dev/null -w "GUARDiA: HTTP %{http_code}\n" http://localhost:8001/docs
curl -s -o /dev/null -w "Gitea: HTTP %{http_code}\n" http://localhost:3000/
curl -s -o /dev/null -w "Jenkins: HTTP %{http_code}\n" http://localhost:8080/
```
---
## 4. 정기 점검 체크리스트
```bash
#!/bin/bash
# 주 1회 실행 권장
echo "===== zio-server 정기 점검 ====="
echo "점검일: $(date)"
echo ""
echo "[ 서비스 상태 ]"
for s in nginx zioinfo zioinfo-deploy guardia gitea jenkins postgresql ollama; do
status=$(systemctl is-active $s)
printf " %-20s %s\n" $s $status
done
echo ""
echo "[ 리소스 현황 ]"
echo " 디스크: $(df -h / | tail -1 | awk '{print $3"/"$2" ("$5")"}')"
echo " 메모리: $(free -h | grep Mem | awk '{print $3"/"$2}')"
echo " 스왑: $(free -h | grep Swap | awk '{print $3"/"$2}')"
echo ""
echo "[ 응답 테스트 ]"
for url in \
"http://localhost/:홈페이지" \
"http://localhost/api/company:API" \
"http://localhost:8001/docs:GUARDiA" \
"http://localhost:3000/:Gitea" \
"http://localhost:8080/:Jenkins"; do
url_part="${url%%:*}"
name="${url##*:}"
code=$(curl -s -o /dev/null -w "%{http_code}" "$url_part" 2>/dev/null)
icon="✅"; [[ "$code" != "200" && "$code" != "302" ]] && icon="❌"
printf " %s %-12s HTTP %s\n" "$icon" "$name" "$code"
done
echo ""
echo "[ 최근 에러 로그 (1시간) ]"
journalctl -p err --since "1 hour ago" --no-pager 2>/dev/null | grep -v "^--" | tail -5 || echo " 없음"
echo ""
echo "[ Gitea 저장소 ]"
curl -s http://localhost:3000/api/v1/repos/search -u "zio:Zio@Admin2026!" 2>/dev/null | \
python3 -c "import json,sys; [print(' '+r['full_name']) for r in json.load(sys.stdin)['data']]" 2>/dev/null
echo ""
echo "[ PostgreSQL DB 상태 ]"
sudo -u postgres psql -c "\l" 2>/dev/null | grep -E "(guardia|gitea|zioinfo)" | awk '{print " "$1}'
echo "============================="
```
---
## 5. 긴급 연락처 및 에스컬레이션
| 단계 | 담당 | 연락처 | 조건 |
|------|------|--------|------|
| L1 | 운영팀 | — | 서비스 재시작으로 해결 가능 |
| L2 | 개발팀 | — | 코드/설정 변경 필요 |
| L3 | NCloud 지원 | 1544-5117 | 서버 하드웨어/네트워크 장애 |
---
*문서 버전: 1.0 | 최종 수정: 2026-05-30*

View File

@ -0,0 +1,454 @@
# GUARDiA ITSM 개방망 운영 가이드
> **버전**: 2.0.0 | **작성일**: 2026-05-30
> **서버**: zio-server (zioinfo.co.kr)
---
## 1. 개요
GUARDiA ITSM은 기본적으로 **폐쇄망(Closed Network)** 환경에서 동작하도록 설계되어 있습니다.
본 가이드는 GUARDiA를 **개방망(Open Network)** 에서도 안전하게 서비스하기 위한 구성 방법을 설명합니다.
### 1-1. 폐쇄망 vs 개방망 비교
| 항목 | 폐쇄망 (기본) | 개방망 (이 가이드) |
|------|-------------|----------------|
| 접근 범위 | 내부망 only | 인터넷 외부 접근 허용 |
| CORS | localhost 만 허용 | 지정 외부 도메인 허용 |
| HTTPS | 선택 | **필수** |
| API 인증 | JWT | JWT + **API Key** 추가 |
| 외부 AI 호출 | 금지 (Ollama only) | 금지 유지 (변경 불가) |
| Rate Limiting | 기본 | **강화** (30req/min) |
| 보안 헤더 | 기본 | HSTS 포함 강화 |
### 1-2. 개방망 지원 아키텍처
```
외부 클라이언트 (브라우저, 메신저봇, 외부시스템)
│ HTTPS (443 / 8443)
┌─────────────────────────────────────────────────┐
│ Nginx (TLS 종료 프록시) │
│ ├── SSL/TLS (자체서명 or Let's Encrypt) │
│ ├── Rate Limiting (30 req/min) │
│ ├── 보안 헤더 (HSTS, X-Frame, X-XSS) │
│ └── CORS 정책 적용 │
└─────────────────┬───────────────────────────────┘
│ HTTP (내부)
┌─────────────────────────────────────────────────┐
│ GUARDiA ITSM FastAPI (포트 8001) │
│ ├── GUARDIA_NETWORK_MODE=open │
│ ├── CORS: 지정 외부 도메인 허용 │
│ ├── 보안 미들웨어 (HSTS, X-Frame, CSP) │
│ └── /api/external/* (API Key 인증) │
└─────────────────┬───────────────────────────────┘
┌───────┴────────┐
▼ ▼
PostgreSQL Ollama LLM
(내부 전용) (내부 전용)
localhost:5432 localhost:11434
```
> **핵심 원칙**: LLM(Ollama)과 DB는 외부에서 직접 접근 불가. API 서버만 노출.
---
## 2. 설치 및 구성
### 2-1. 사전 요구사항
| 항목 | 요구 사항 |
|------|---------|
| OS | Ubuntu 20.04+ |
| Nginx | 1.18+ |
| SSL 인증서 | 자체서명 or Let's Encrypt |
| GUARDiA | 2.0.0+ |
| Python | 3.11+ |
### 2-2. SSL 인증서 생성
**자체 서명 인증서 (테스트/내부망):**
```bash
mkdir -p /etc/ssl/guardia
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
-keyout /etc/ssl/guardia/server.key \
-out /etc/ssl/guardia/server.crt \
-subj "/C=KR/ST=Seoul/O=YourOrg/CN=your-server-ip" \
-addext "subjectAltName=IP:zioinfo.co.kr"
chmod 600 /etc/ssl/guardia/server.key
```
**Let's Encrypt (도메인 보유 시 — 권장):**
```bash
apt install certbot python3-certbot-nginx
certbot --nginx -d itsm.zioinfo.co.kr
# 자동 갱신 확인
certbot renew --dry-run
```
### 2-3. 환경변수 설정 (.env)
```bash
cp /opt/guardia/app/.env.open /opt/guardia/app/.env
nano /opt/guardia/app/.env
```
**개방망 필수 설정:**
```env
# 개방망 모드 활성화
GUARDIA_NETWORK_MODE=open
# 허용할 외부 출처 (쉼표 구분)
GUARDIA_ALLOWED_ORIGINS=https://itsm.zioinfo.co.kr,https://portal.myorg.go.kr
# 웹훅 HMAC 시크릿 (반드시 변경)
GUARDIA_WEBHOOK_SECRET=your-strong-secret-here
# DB (특수문자 URL 인코딩 필수: @ → %40, ! → %21)
DATABASE_URL=postgresql+asyncpg://guardia:G%40urd1a_2026%21@localhost:5432/guardia_db
# LLM (내부 전용 — 절대 변경 금지)
OLLAMA_BASE_URL=http://localhost:11434
LLM_MODEL=llama3:8b
```
### 2-4. Nginx 개방망 설정
```bash
# Nginx http 블록에 rate limit zone 추가
nano /etc/nginx/nginx.conf
```
```nginx
http {
limit_req_zone $binary_remote_addr zone=guardia_api:10m rate=30r/m;
...
}
```
**`/etc/nginx/sites-available/guardia-https`** 생성:
```nginx
server {
listen 8443 ssl;
server_name _;
ssl_certificate /etc/ssl/guardia/server.crt;
ssl_certificate_key /etc/ssl/guardia/server.key;
ssl_protocols TLSv1.2 TLSv1.3;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
location /api/ {
limit_req zone=guardia_api burst=10 nodelay;
proxy_pass http://127.0.0.1:8001;
proxy_set_header X-Forwarded-Proto https;
}
location / {
proxy_pass http://127.0.0.1:8001;
}
}
```
```bash
ln -sf /etc/nginx/sites-available/guardia-https /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
```
### 2-5. 서비스 재시작
```bash
systemctl restart guardia
systemctl is-active guardia
```
---
## 3. 외부 API 사용법
### 3-1. API 엔드포인트 목록
| 엔드포인트 | 메서드 | 인증 | 설명 |
|-----------|--------|------|------|
| `/api/external/health` | GET | 불필요 | 헬스체크 |
| `/api/external/status` | GET | 불필요 | 시스템 공개 상태 |
| `/api/external/keys` | GET | JWT (관리자) | API Key 목록 |
| `/api/external/keys` | POST | JWT (관리자) | API Key 발급 |
| `/api/external/keys/{id}` | DELETE | JWT (관리자) | API Key 비활성화 |
| `/api/external/sr` | GET | API Key (read) | SR 목록 조회 |
| `/api/external/sr` | POST | API Key (write) | SR 등록 |
| `/api/external/webhook` | POST | HMAC (선택) | 외부 메신저 웹훅 |
| `/docs` | GET | 불필요 | OpenAPI 문서 |
### 3-2. API Key 발급
**1단계: 관리자 로그인 (JWT 획득)**
```bash
curl -X POST https://zioinfo.co.kr:8443/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"Admin@zioinfo2026!"}' \
-k
# → {"access_token": "eyJ...", "token_type": "bearer"}
```
**2단계: API Key 발급**
```bash
TOKEN="eyJ..."
curl -X POST https://zioinfo.co.kr:8443/api/external/keys \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "카카오워크 봇",
"scopes": "read,write,webhook",
"expires_days": 365,
"allowed_ips": ""
}' -k
```
**응답 (발급 후 1회만 노출):**
```json
{
"id": 1,
"name": "카카오워크 봇",
"api_key": "grd_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"scopes": "read,write,webhook",
"expires_at": "2027-05-30T10:00:00",
"warning": "이 키는 다시 조회할 수 없습니다. 안전한 곳에 저장하세요."
}
```
### 3-3. API Key로 SR 등록
```bash
API_KEY="grd_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# SR 등록
curl -X POST https://zioinfo.co.kr:8443/api/external/sr \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "웹서버 재시작 요청",
"description": "nginx가 502 오류를 반환하고 있습니다.",
"priority": "HIGH",
"requester_name": "홍길동",
"requester_email": "hong@example.go.kr"
}' -k
```
### 3-4. 외부 메신저 웹훅 연동
```bash
# Gitea/Slack/카카오워크 웹훅 URL 설정
WEBHOOK_URL="https://zioinfo.co.kr:8443/api/external/webhook"
SECRET="guardia-webhook-secret-change-me-2026"
# 서명 생성 (Python)
python3 -c "
import hmac, hashlib, json
body = json.dumps({'command': '서버 상태 확인', 'user_id': 'user01'})
sig = 'sha256=' + hmac.new(b'$SECRET', body.encode(), hashlib.sha256).hexdigest()
print(sig)
"
# 웹훅 전송
curl -X POST $WEBHOOK_URL \
-H "Content-Type: application/json" \
-H "X-GUARDiA-Signature: sha256=<서명값>" \
-H "X-Source: kakaotalk" \
-d '{"command": "서버 상태 확인", "user_id": "홍길동"}' \
-k
```
---
## 4. API Key 권한 스코프
| 스코프 | 설명 | 허용 API |
|--------|------|---------|
| `read` | 읽기 전용 | SR 목록 조회 |
| `write` | 쓰기 | SR 등록, 상태 변경 |
| `admin` | 전체 권한 | 모든 외부 API |
| `webhook` | 웹훅 전용 | `/api/external/webhook` |
**스코프 조합 예시:**
```json
"scopes": "read,write" // 조회 + 등록
"scopes": "webhook" // 웹훅만
"scopes": "admin" // 전체 (주의)
```
---
## 5. 보안 설정
### 5-1. 적용된 보안 헤더
| 헤더 | 값 | 효과 |
|------|-----|------|
| `Strict-Transport-Security` | `max-age=31536000` | 브라우저가 HTTPS만 사용 |
| `X-Frame-Options` | `DENY` | iframe 삽입 차단 |
| `X-Content-Type-Options` | `nosniff` | MIME 타입 스니핑 방지 |
| `X-XSS-Protection` | `1; mode=block` | XSS 공격 차단 |
| `Referrer-Policy` | `strict-origin-when-cross-origin` | Referrer 정보 제한 |
### 5-2. Rate Limiting 설정
| 위치 | 제한 |
|------|------|
| Nginx (기본) | 30 req/min per IP |
| Nginx (burst) | 최대 10 req 버스트 허용 |
| FastAPI (ratelimit.py) | 별도 설정 가능 |
### 5-3. IP 화이트리스트 (API Key)
특정 외부 시스템만 API Key를 사용할 수 있도록 IP를 제한할 수 있습니다:
```json
{
"name": "공공기관 포털",
"scopes": "read,write",
"allowed_ips": "203.10.20.30,203.10.20.31"
}
```
빈 문자열(`""`)로 설정하면 모든 IP에서 접근 가능합니다.
### 5-4. 변경 불가 보안 정책
> 개방망 모드에서도 다음 정책은 절대 변경 불가합니다.
| 정책 | 내용 |
|------|------|
| **외부 LLM 금지** | Ollama(localhost:11434)만 사용. OpenAI, Claude 등 외부 API 완전 금지 |
| **SSH 자격증명 보호** | IP, 비밀번호, SSH 계정을 API 응답에 포함 금지 |
| **AES-256 암호화** | 서버 자격증명은 암호화 저장 |
| **root SSH 금지** | opsagent 전용 계정 사용 |
---
## 6. 모드 전환
### 6-1. 폐쇄망 → 개방망
```bash
# .env 수정
echo "GUARDIA_NETWORK_MODE=open" >> /opt/guardia/app/.env
echo "GUARDIA_ALLOWED_ORIGINS=https://your-domain.go.kr" >> /opt/guardia/app/.env
# Nginx HTTPS 활성화
ln -sf /etc/nginx/sites-available/guardia-https /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
# 서비스 재시작
systemctl restart guardia
```
### 6-2. 개방망 → 폐쇄망 (롤백)
```bash
# .env 수정
sed -i 's/GUARDIA_NETWORK_MODE=open/GUARDIA_NETWORK_MODE=closed/' /opt/guardia/app/.env
# HTTPS 비활성화
rm /etc/nginx/sites-enabled/guardia-https
systemctl reload nginx
# 서비스 재시작
systemctl restart guardia
```
---
## 7. 테스트 결과
### 7-1. 테스트 환경
| 항목 | 값 |
|------|-----|
| 서버 | Ubuntu 24.04 LTS (zioinfo.co.kr) |
| GUARDiA 버전 | 2.0.0 |
| Nginx 버전 | 1.24.0 |
| 테스트 일자 | 2026-05-30 |
### 7-2. 테스트 결과
| 테스트 | 항목 | 결과 |
|--------|------|------|
| T1 | HTTP 헬스체크 (8001) | ✅ HTTP 200 |
| T2 | HTTPS 헬스체크 (8443) | ✅ HTTP 200 |
| T3 | 홈페이지 HTTPS (443) | ✅ HTTP 200 |
| T4 | 미인증 API 접근 | ✅ HTTP 401 반환 |
| T5 | CORS 외부 출처 허용 | ✅ `Access-Control-Allow-Origin` 헤더 |
| T6 | HSTS 헤더 | ✅ `Strict-Transport-Security` 적용 |
| T7 | X-Frame-Options | ✅ `DENY` 설정 |
| T8 | Rate Limiting | ✅ Nginx rate limit zone 설정 |
| T9 | 시스템 상태 공개 | ✅ `operational` |
| T10 | 개방망 모드 활성 | ✅ `NETWORK_MODE=open` |
---
## 8. 트러블슈팅
### 8-1. CORS 오류
**증상**: 브라우저에서 `Access-Control-Allow-Origin` 오류
```bash
# .env에 도메인 추가
GUARDIA_ALLOWED_ORIGINS=https://your-domain.com,https://other-domain.go.kr
systemctl restart guardia
```
### 8-2. HTTPS 인증서 오류
**증상**: `SSL: CERTIFICATE_VERIFY_FAILED`
```bash
# 자체서명 인증서인 경우 curl에 -k 플래그 사용
curl -k https://zioinfo.co.kr:8443/api/external/health
# 브라우저에서는 예외 추가 또는 Let's Encrypt 인증서 사용
```
### 8-3. Rate Limit 초과
**증상**: HTTP 429 Too Many Requests
```bash
# Nginx rate limit 완화
nano /etc/nginx/nginx.conf
# rate=60r/m 으로 변경
nginx -t && systemctl reload nginx
```
### 8-4. DATABASE_URL 연결 오류
**증상**: `Name or service not known`
```bash
# @ 특수문자 URL 인코딩 확인
# G@urd1a_2026! → G%40urd1a_2026%21
sed -i 's|G@urd1a_2026!|G%40urd1a_2026%21|g' /opt/guardia/app/.env
systemctl restart guardia
```
---
## 9. 접속 정보 요약
| 서비스 | URL | 비고 |
|--------|-----|------|
| GUARDiA ITSM (HTTP) | `http://zioinfo.co.kr:8001` | 내부망 권장 |
| GUARDiA ITSM (HTTPS) | `https://zioinfo.co.kr:8443` | 개방망 사용 |
| 외부 API | `https://zioinfo.co.kr:8443/api/external/` | API Key 인증 |
| OpenAPI 문서 | `https://zioinfo.co.kr:8443/docs` | 무인증 |
| 홈페이지 HTTPS | `https://zioinfo.co.kr` | 포트 443 |
---
*GUARDiA ITSM v2.0.0 | (주)지오정보기술 | 2026-05-30*

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,296 @@
# GUARDiA Manager — 라이선스 키 등록 및 관리 가이드
> **버전**: 2.0.0 | **작성일**: 2026-05-30
> **접속 URL**: http://zioinfo.co.kr:8090/licenses
> **대상**: 시스템 관리자 (admin 역할)
---
## 1. 개요
GUARDiA Manager의 **라이선스 관리** 페이지는 GUARDiA ITSM 플랫폼의 라이선스를
통합 관제하는 화면입니다.
### 주요 기능
| 기능 | 설명 |
|------|------|
| **현재 상태 확인** | 에디션, 만료일, 남은 기간, 허용 한도 실시간 표시 |
| **라이선스 키 등록** | 발급받은 라이선스 키를 붙여 넣고 즉시 활성화 |
| **무료 체험 시작** | 7/14/30일 체험 라이선스 즉시 발급 (설치당 1회) |
| **키 검증** | 등록 없이 키 유효성만 검증 (발급 전 사전 확인) |
| **라이선스 비활성화** | 현재 라이선스 비활성화 (서비스 제한 발생) |
| **이력 조회** | 과거 등록된 모든 라이선스 이력 테이블 |
| **에디션 비교** | TRIAL/COMMUNITY/STANDARD/ENTERPRISE 기능 비교 |
---
## 2. 라이선스 에디션
### 2-1. 에디션 비교표
| 구분 | TRIAL | COMMUNITY | STANDARD | ENTERPRISE |
|------|-------|-----------|----------|------------|
| 가격 | 무료 (7일) | 무료 | 협의 | 협의 |
| 기관 수 | 1 | 1 | 50 | 무제한 |
| 사용자 수 | 10명 | 10명 | 200명 | 무제한 |
| 서버 수 | 20대 | 50대 | 500대 | 무제한 |
| AI 에이전트 | ❌ | ❌ | ✅ | ✅ |
| LDAP/MFA | ❌ | ❌ | ✅ | ✅ |
| SLA 관리 | ✅ 기본 | ✅ 기본 | ✅ 고급 | ✅ 고급 |
| 취약점 스캔 | ❌ | ❌ | ❌ | ✅ |
| FinOps | ❌ | ❌ | ❌ | ✅ |
| Scouter APM | ❌ | ❌ | ❌ | ✅ |
| 기술 지원 | ❌ | 커뮤니티 | 이메일 | 전담 지원 |
### 2-2. 라이선스 키 형식
```
TRIAL: GRD-{Base64URL 인코딩 페이로드} (자동 생성)
COMMUNITY/ grd_lic_{발급기관코드}_{서명}
STANDARD/
ENTERPRISE:
```
---
## 3. 화면 구성 (NCloud 콘솔 스타일)
```
┌─────────────────────────────────────────────────────────────────┐
│ 업그레이드 배너 (만료 3일 전 표시) │
│ ⚠️ 체험판이 X일 후 만료됩니다. [라이선스 등록] 버튼 │
├─────────────────────────────────────────────────────────────────┤
│ 현재 라이선스 상태 카드 │
│ [에디션 배지] [체험판/만료 배지] │
│ 고객명: 지오정보기술 체험판 │
│ 메시지: TRIAL 라이선스 활성 (6일 남음) │
│ [🔑 등록] [🎁 체험] [🔍 검증] [비활성화] │
│ ───────────────────────────────────────────────────────────── │
│ [만료 게이지 바] [라이선스 ID] [허용 한도] │
│ ████████░░ 6일 남음 TRL-290EA0FB 기관:1/사용자:10/서버:20 │
├─────────────────────────────────────────────────────────────────┤
│ 액션 패널 (선택한 액션에 따라 표시) │
│ 예) 라이선스 키 등록: [textarea] [활성화 버튼] │
├─────────────────────────────────────────────────────────────────┤
│ 에디션 비교 (4개 카드 — 현재 에디션 강조) │
│ [TRIAL] [COMMUNITY] [STANDARD ★] [ENTERPRISE] │
├─────────────────────────────────────────────────────────────────┤
│ 라이선스 이력 테이블 │
│ ID | 라이선스ID | 에디션 | 고객명 | 체험판 | 만료일 | 상태 | 등록자│
└─────────────────────────────────────────────────────────────────┘
```
---
## 4. 기능별 사용 방법
### 4-1. 라이선스 키 등록
> **사전 조건**: admin 역할로 로그인, GUARDiA ITSM 서버에 `GUARDIA_LICENSE_KEY` 환경변수 설정
1. [🔑 라이선스 등록] 버튼 클릭
2. 발급받은 라이선스 키를 텍스트 영역에 붙여 넣기
3. [활성화] 버튼 클릭
4. 성공 시 현재 상태 카드가 즉시 갱신됨
```bash
# 라이선스 키 발급 (서버 관리자용 — Python 직접 실행)
ssh root@zioinfo.co.kr
source /opt/guardia/venv/bin/activate
cd /opt/guardia/app
python -m core.license \
--customer "서울특별시 정보화부" \
--edition STANDARD \
--days 365 \
--key $GUARDIA_LICENSE_KEY
```
### 4-2. 무료 체험 시작
> **제한**: 설치당 1회만 가능
1. [🎁 무료 체험] 버튼 클릭
2. 고객/기관명 입력 (기본값: "GUARDiA 체험판")
3. 체험 기간 선택: 7일 / 14일 / 30일
4. [🎁 체험 시작] 버튼 클릭
5. **발급된 체험 키가 팝업으로 1회만 표시** → 반드시 복사 보관
6. 즉시 TRIAL 에디션으로 활성화
> ⚠️ 체험 라이선스 키는 발급 시 화면에 1회만 표시됩니다. 화면을 닫으면 다시 확인할 수 없습니다.
### 4-3. 라이선스 키 검증
등록 전 키가 유효한지 먼저 확인할 때 사용합니다.
1. [🔍 키 검증] 버튼 클릭
2. 확인할 키 입력
3. [검증] 버튼 클릭
4. 결과 확인: 에디션, 고객명, 발급일, 만료일 표시
5. 유효한 경우 [이 키로 활성화] 버튼으로 즉시 등록 가능
### 4-4. 라이선스 비활성화
> **주의**: 비활성화 시 서비스 제한이 발생합니다.
1. [비활성화] 버튼 클릭 (빨간 버튼)
2. 확인 다이얼로그에서 [확인]
3. 상태가 "활성 라이선스가 없습니다."로 변경됨
---
## 5. API 명세
GUARDiA ITSM API를 직접 호출하는 엔드포인트입니다.
### 기본 정보
| 항목 | 값 |
|------|-----|
| Base URL | `http://zioinfo.co.kr:8001` |
| 인증 | `Authorization: Bearer {JWT Token}` |
| 로그인 | `POST /api/auth/login` (JSON, admin 역할 필요) |
### 엔드포인트 목록
| 메서드 | 경로 | 인증 | 설명 |
|--------|------|------|------|
| `GET` | `/api/license/status` | 로그인 | 현재 라이선스 상태 |
| `POST` | `/api/license/trial` | admin | 체험 라이선스 발급 |
| `POST` | `/api/license/activate` | admin | 라이선스 키 활성화 |
| `POST` | `/api/license/verify` | admin | 라이선스 키 검증만 |
| `DELETE` | `/api/license` | admin | 라이선스 비활성화 |
| `GET` | `/api/license/history` | admin | 등록 이력 조회 |
### 요청/응답 예시
**체험 라이선스 발급**
```bash
curl -X POST http://zioinfo.co.kr:8001/api/license/trial \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"customer":"지오정보기술","days":7}'
```
```json
{
"message": "🎁 7일 무료 체험이 시작되었습니다!",
"license_id": "TRL-XXXXXXXXXX",
"edition": "TRIAL",
"customer": "지오정보기술",
"expires_at": "2026-06-06T11:37:25",
"days_remaining": 7,
"is_trial": true,
"license_key": "GRD-..."
}
```
**라이선스 상태 조회**
```bash
curl http://zioinfo.co.kr:8001/api/license/status \
-H "Authorization: Bearer $TOKEN"
```
```json
{
"activated": true,
"valid": true,
"expired": false,
"is_trial": true,
"edition": "TRIAL",
"customer": "지오정보기술",
"days_remaining": 6,
"limits": {
"max_institutions": 1,
"max_users": 10,
"max_servers": 20
},
"message": "TRIAL [체험판] 라이선스 활성 (6일 남음)"
}
```
---
## 6. 테스트 결과
### 6-1. 테스트 환경
| 항목 | 값 |
|------|-----|
| 서버 | Ubuntu 24.04 (zioinfo.co.kr) |
| GUARDiA ITSM | v2.0.0 |
| GUARDiA Manager | v1.0.0 |
| 테스트 일자 | 2026-05-30 |
### 6-2. 테스트 결과 (7/7 PASS)
| 테스트 | 항목 | 결과 |
|--------|------|------|
| T1 | admin 로그인 (JSON) | ✅ PASS |
| T2 | 라이선스 현재 상태 조회 | ✅ PASS |
| T3 | 체험 라이선스 발급 (7일) | ✅ PASS |
| T4 | 활성화 후 상태 확인 (TRIAL, 6일) | ✅ PASS |
| T5 | 라이선스 이력 조회 (1건) | ✅ PASS |
| T6 | 잘못된 키 검증 (에러 처리) | ✅ PASS |
| T7 | Manager UI 접속 | ✅ PASS |
| T8 | Manager Backend API | ✅ PASS |
### 6-3. 버그 수정 이력
| 날짜 | 파일 | 버그 | 수정 내용 |
|------|------|------|---------|
| 2026-05-30 | `routers/license.py` | `datetime` timezone-aware/naive 충돌 | `datetime.fromisoformat(...).replace(tzinfo=None)` 적용 |
---
## 7. 운영 절차
### 7-1. 정기 만료일 모니터링
```bash
# 라이선스 만료일 확인 (서버 직접 확인)
ssh root@zioinfo.co.kr
curl -s -X POST http://localhost:8001/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"1111"}' \
| python3 -c "import json,sys; print(json.load(sys.stdin).get('access_token')[:20])"
TOKEN=$(...)
curl -s http://localhost:8001/api/license/status \
-H "Authorization: Bearer $TOKEN" \
| python3 -m json.tool | grep -E "edition|days_remaining|expires_at"
```
### 7-2. 라이선스 갱신 절차
1. 새 라이선스 키 발급 (담당자에게 요청)
2. GUARDiA Manager → 라이선스 관리 → [🔍 키 검증]으로 사전 확인
3. [🔑 라이선스 등록] → 새 키 입력 → 활성화
4. 기존 라이선스는 자동 비활성화됨
### 7-3. 라이선스 환경변수 설정
STANDARD/ENTERPRISE 라이선스 사용 시 서버에 마스터 키 설정이 필요합니다:
```bash
# /opt/guardia/app/.env 에 추가
GUARDIA_LICENSE_KEY=<64자리 hex 마스터 >
# 서비스 재시작
systemctl restart guardia
```
---
## 8. 트러블슈팅
| 증상 | 원인 | 해결 |
|------|------|------|
| 체험판 발급 실패 (409) | 이미 체험 이력 존재 | 체험은 1회 한정, 정식 키 필요 |
| 키 검증/등록 500 에러 | `GUARDIA_LICENSE_KEY` 미설정 | .env에 마스터 키 설정 |
| 이력 조회 403 | admin 역할 아님 | admin 계정으로 로그인 |
| 만료 배너 표시 | 만료 3일 이내 | 새 라이선스 등록 |
| `datetime` 오류 | timezone aware/naive 충돌 | `license.py` 패치 적용 (완료) |
---
*GUARDiA ITSM v2.0.0 | (주)지오정보기술 | 2026-05-30*

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,76 @@
# GUARDiA 폐쇄망 ↔ 개방망 데이터 연동 가이드
> **버전**: 1.0.0 | **작성일**: 2026-05-30
> **서버**: zioinfo.co.kr | **대상**: 시스템 관리자 (admin)
---
## 1. 개요
폐쇄망에 설치된 GUARDiA ITSM의 데이터(SR, CMDB, 기관, 감사로그)를
개방망 GUARDiA Manager로 안전하게 이관하는 Export/Import 인터페이스입니다.
### 보안 특징
- **HMAC-SHA256 서명**: 번들 파일 위변조 방지
- **민감 정보 자동 마스킹**: IP 주소, SSH 비밀번호는 `****`로 처리
- **Dry Run 모드**: 실제 저장 전 사전 검증
- **중복 방지**: sr_id 기준 중복 SKIP
---
## 2. API 엔드포인트
| 메서드 | 경로 | 설명 |
|--------|------|------|
| `GET` | `/api/export-import/export/bundle` | 전체 번들 ZIP (권장) |
| `GET` | `/api/export-import/export/sr` | SR 목록 JSON |
| `GET` | `/api/export-import/export/cmdb` | CMDB 서버 자산 JSON |
| `GET` | `/api/export-import/export/institutions` | 기관 목록 JSON |
| `GET` | `/api/export-import/export/audit` | 감사 로그 JSON |
| `POST` | `/api/export-import/import/bundle` | 번들 ZIP Import |
| `POST` | `/api/export-import/import/sr` | SR JSON Import |
---
## 3. 사용 방법
### 3-1. 폐쇄망에서 Export
```bash
# 전체 번들 다운로드 (권장)
TOKEN=$(curl -s -X POST http://폐쇄망-IP:8001/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"비밀번호"}' \
| python3 -c "import json,sys; print(json.load(sys.stdin).get('access_token'))")
curl -O http://폐쇄망-IP:8001/api/export-import/export/bundle \
-H "Authorization: Bearer $TOKEN"
# → guardia_export_20260530_HHMMSS.zip 저장
```
### 3-2. 개방망 Manager에서 Import
1. `http://zioinfo.co.kr:8090/export-import` 접속
2. 번들 ZIP 파일 드래그 & 드롭
3. **[🔍 검증 실행]** — Dry Run으로 내용 확인
4. 이상 없으면 **Dry Run 체크 해제****[📥 Import 실행]**
---
## 4. 테스트 결과 (7/7 PASS)
| 테스트 | 결과 |
|--------|------|
| SR Export | ✅ PASS |
| CMDB Export | ✅ PASS |
| 기관 Export | ✅ PASS |
| 감사 로그 Export | ✅ PASS |
| 번들 ZIP Export (HMAC) | ✅ PASS |
| SR Import dry_run | ✅ PASS |
| Manager UI 접속 | ✅ PASS |
**버그 수정**: `date` 타입 JSON 직렬화 오류 → `isoformat()` 처리 완료
---
*GUARDiA ITSM v2.0.0 | (주)지오정보기술 | 2026-05-30*

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,134 @@
# GUARDiA AI 플랫폼 최적 오픈소스 제안서
> **서버**: zio-server (zioinfo.co.kr) | **작성일**: 2026-05-30
> **현재 사양**: 2 vCPU / 7.8GB RAM / 99GB Disk / Ubuntu 24.04
> **기설치**: Ollama 0.24.0 + Llama3:8b (4.7GB)
---
## 1. 제안 아키텍처
```
사용자/메신저 명령
[GUARDiA ITSM FastAPI]
[LangChain Orchestration Layer] ← 에이전트 체인, 툴 호출
┌─────────────────────────────────────┐
│ RAG 파이프라인 │
│ ChromaDB (벡터 검색) │
│ ← GUARDiA KB, 코드베이스, 문서 임베딩 │
└─────────────────────────────────────┘
[Ollama] → Llama3:8b (온프레미스 LLM)
```
---
## 2. LLM / sLLM 추천
| 순위 | 모델 | RAM 요구 | 특징 | GUARDiA 적합성 |
|------|------|---------|------|--------------|
| ⭐1 | **llama3:8b** (기설치) | ~5GB | 범용 추론, 한국어 양호 | ✅ 현재 운영 중 |
| 2 | **qwen2.5:7b** | ~5GB | 한국어 성능 우수, 코드 생성 | ✅ 권장 추가 |
| 3 | **codellama:7b** | ~4GB | 코드 리뷰, SSH 명령 생성 | ✅ 코드 분석용 |
| 4 | **mistral:7b** | ~5GB | 빠른 응답, 영어 최적 | 보조 모델 |
| 5 | llama3.2:3b | ~2.5GB | 경량, 빠름 | 빠른 응답 전용 |
**현재 서버 RAM 7.8GB 제한으로 동시 2개 모델 한계 → llama3:8b + codellama:7b 조합 권장**
---
## 3. AI 프레임워크 추천
### 3-1. LangChain (핵심 추천 ⭐)
| 항목 | 내용 |
|------|------|
| 용도 | 에이전트 체인, 툴 호출, RAG 파이프라인 |
| GUARDiA 연동 | FastAPI 내부에서 `ChatOllama` 사용 |
| 패키지 | `langchain`, `langchain-community`, `langchain-ollama` |
```python
from langchain_ollama import ChatOllama
from langchain_core.messages import HumanMessage
llm = ChatOllama(model="llama3:8b", base_url="http://localhost:11434")
response = llm.invoke([HumanMessage(content="서버 재시작 명령 생성")])
```
### 3-2. LangGraph (에이전트 워크플로우)
| 항목 | 내용 |
|------|------|
| 용도 | 복잡한 멀티 에이전트 상태 머신 |
| GUARDiA 연동 | SR 처리 → 승인 → 배포 플로우 구현 |
| 패키지 | `langgraph` |
### 3-3. Haystack (RAG 파이프라인)
| 항목 | 내용 |
|------|------|
| 용도 | 문서 검색, Q&A, KB 기반 응답 |
| GUARDiA 연동 | KB, 운영 메뉴얼 RAG 검색 |
| 패키지 | `haystack-ai`, `farm-haystack` |
---
## 4. 벡터 DB 추천 (별도 문서: 32_벡터DB_제안서.md)
| 순위 | DB | 특징 | 설치 방식 |
|------|-----|------|---------|
| ⭐1 | **ChromaDB** | 경량, Python native, 로컬 파일 | pip install |
| 2 | **Qdrant** | 고성능, REST API, Docker | Docker 컨테이너 |
| 3 | Weaviate | 풍부한 기능, 무거움 | Docker 컨테이너 |
| 4 | FAISS | Meta제작, 초고속, 메모리 전용 | pip install |
**서버 사양(7.8GB RAM) 고려 → ChromaDB (로컬 파일 기반) 1차 권장**
---
## 5. 임베딩 모델 추천
| 모델 | 크기 | 한국어 | 용도 |
|------|------|--------|------|
| **nomic-embed-text** | 274MB | 양호 | Ollama 내장, 온프레미스 |
| all-MiniLM-L6-v2 | 80MB | 미흡 | 영어 최적, 빠름 |
| bge-m3 | 570MB | 우수 | 다국어 최고 성능 |
**GUARDiA 권장: `nomic-embed-text` (Ollama 내장, 외부 API 불필요)**
---
## 6. 설치 계획 (zio 서버)
### Phase 1: LangChain + RAG 기반
```bash
pip install langchain langchain-community langchain-ollama \
langchain-chroma chromadb sentence-transformers
ollama pull nomic-embed-text
```
### Phase 2: 추가 모델
```bash
ollama pull qwen2.5:7b # 한국어 강화
ollama pull codellama:7b # 코드 리뷰
```
### Phase 3: GUARDiA 코드베이스 학습
```python
from langchain_community.document_loaders import DirectoryLoader
from langchain_chroma import Chroma
from langchain_ollama import OllamaEmbeddings
loader = DirectoryLoader("/opt/guardia/app", glob="**/*.py")
docs = loader.load()
embeddings = OllamaEmbeddings(model="nomic-embed-text")
vectordb = Chroma.from_documents(docs, embeddings,
persist_directory="/opt/guardia/vectordb")
```
---
*GUARDiA ITSM v2.0.0 | (주)지오정보기술 | 2026-05-30*

124
32_벡터DB_제안서.md Normal file
View File

@ -0,0 +1,124 @@
# GUARDiA 벡터 데이터베이스 제안서
> **서버**: zio-server (zioinfo.co.kr) | **작성일**: 2026-05-30
---
## 1. 벡터 DB 비교
| DB | 설치 | 메모리 | 영속성 | REST API | 한국어 | GUARDiA 적합성 |
|----|------|--------|--------|---------|--------|--------------|
| **ChromaDB** | pip | 낮음 | 파일 기반 | ❌ (Python) | ✅ | ⭐ 1순위 |
| **Qdrant** | Docker | 중간 | 볼륨 | ✅ | ✅ | ⭐ 2순위 |
| FAISS | pip | 낮음 | 없음(메모리) | ❌ | ✅ | 빠른 검색 |
| Weaviate | Docker | 높음 | 볼륨 | ✅ | ✅ | 대규모 용 |
| Milvus | Docker | 높음 | 볼륨 | ✅ | ✅ | 엔터프라이즈 |
---
## 2. 최종 선택: ChromaDB
### 선택 이유
- **설치 간편**: `pip install chromadb` 한 줄
- **서버 RAM 절약**: 7.8GB RAM 환경에 최적
- **Python 직접 통합**: GUARDiA FastAPI와 완벽 연동
- **로컬 파일 영속**: 별도 서비스 불필요
- **Ollama 임베딩 연동**: `nomic-embed-text` 온프레미스 임베딩
### 설치 및 설정
```bash
# 설치
/opt/guardia/venv/bin/pip install chromadb langchain-chroma
# 디렉터리
mkdir -p /opt/guardia/vectordb
```
### GUARDiA 연동 코드
```python
import chromadb
from chromadb.config import Settings
# 클라이언트 초기화
client = chromadb.PersistentClient(
path="/opt/guardia/vectordb",
settings=Settings(anonymized_telemetry=False)
)
# 컬렉션 생성
collection = client.get_or_create_collection(
name="guardia_kb",
metadata={"hnsw:space": "cosine"}
)
# 문서 추가
collection.add(
documents=["서버 재시작 절차: systemctl restart guardia"],
metadatas=[{"type": "kb", "category": "deployment"}],
ids=["kb-001"]
)
# 유사도 검색
results = collection.query(
query_texts=["guardia 서비스 재시작하는 방법"],
n_results=3
)
```
---
## 3. 2순위: Qdrant (Docker)
RAM이 충분하거나 REST API가 필요한 경우 추천합니다.
```bash
docker run -d --name qdrant \
-p 6333:6333 \
-v /opt/guardia/qdrant:/qdrant/storage \
qdrant/qdrant
```
---
## 4. GUARDiA 코드베이스 학습 계획
### 학습 데이터 소스
| 소스 | 내용 | 문서 수 예상 |
|------|------|------------|
| `/opt/guardia/app/routers/*.py` | API 라우터 코드 | ~70개 |
| `/opt/guardia/app/core/*.py` | 비즈니스 로직 | ~38개 |
| `/opt/guardia/app/models.py` | 데이터 모델 | 1개 (대용량) |
| `/opt/guardia/app/static/` | UI 코드 | ~20개 |
| `C:/GUARDiA/manual/*.md` | 운영 문서 | ~32개 |
### 임베딩 스크립트
```python
# /opt/guardia/app/scripts/embed_codebase.py
from langchain_community.document_loaders import DirectoryLoader, TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_ollama import OllamaEmbeddings
from langchain_chroma import Chroma
loader = DirectoryLoader("/opt/guardia/app", glob="**/*.py",
loader_cls=TextLoader, loader_kwargs={"encoding": "utf-8"})
docs = loader.load()
splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
chunks = splitter.split_documents(docs)
embeddings = OllamaEmbeddings(
model="nomic-embed-text",
base_url="http://localhost:11434"
)
vectordb = Chroma.from_documents(
chunks, embeddings,
persist_directory="/opt/guardia/vectordb",
collection_name="guardia_codebase"
)
print(f"임베딩 완료: {len(chunks)}개 청크")
```
---
*GUARDiA ITSM v2.0.0 | (주)지오정보기술 | 2026-05-30*

View File

@ -0,0 +1,244 @@
# GUARDiA Messenger 앱 스토어 등록 가이드
> **앱명**: GUARDiA Messenger
> **패키지**: kr.co.zioinfo.guardia
> **버전**: 1.0.0
> **작성일**: 2026-05-31
---
## 1. 사전 준비
### 1-1. 필요 계정
| 스토어 | 계정 | 비용 | 등록 링크 |
|--------|------|------|----------|
| **Google Play** | Google Play Developer | $25 (1회) | play.google.com/console |
| **Apple App Store** | Apple Developer Program | $99/년 | developer.apple.com |
| **Expo EAS** | Expo 계정 (무료) | 무료 | expo.dev |
### 1-2. 환경 준비
```bash
# EAS CLI 설치
npm install -g eas-cli
# Expo 로그인
eas login
# 프로젝트 초기화
cd C:\GUARDiA\app
eas init --id guardia-messenger-zioinfo
```
---
## 2. EAS Build — 클라우드 빌드 (Mac 불필요)
### 2-1. Android APK/AAB 빌드
```bash
cd C:\GUARDiA\app
# 테스트용 APK (내부 배포)
eas build --platform android --profile preview
# 스토어 배포용 AAB
eas build --platform android --profile production
```
빌드 완료 후 `.aab` 파일 다운로드 → Google Play Console에 업로드
### 2-2. iOS IPA 빌드
```bash
# App Store Connect용 IPA (Mac 없이 클라우드 빌드)
eas build --platform ios --profile production
```
> Apple Developer 계정이 필요하며 EAS가 자동으로 인증서와 프로파일을 관리합니다.
---
## 3. Google Play Store 등록
### 3-1. 앱 생성
1. **play.google.com/console** 접속 → Google Play Developer 로그인
2. [앱 만들기] 클릭
3. 정보 입력:
| 항목 | 값 |
|------|-----|
| 앱 이름 | GUARDiA Messenger |
| 기본 언어 | 한국어 |
| 앱 유형 | 앱 |
| 무료/유료 | 무료 |
### 3-2. 스토어 등록 정보
**앱 이름**: GUARDiA Messenger
**간략한 설명** (80자 이내):
```
AI 기반 IT 인프라 자율 운영 플랫폼 — GUARDiA ITSM 모바일 앱
```
**자세한 설명** (4000자 이내):
```
GUARDiA Messenger는 (주)지오정보기술의 AI 기반 레거시 인프라 자율 운영 플랫폼
GUARDiA ITSM과 연동하는 공식 모바일 앱입니다.
주요 기능:
• 서비스 요청(SR) 실시간 조회 및 등록
• AI 챗봇을 통한 자연어 인프라 명령
• 인시던트 및 긴급 알림 즉시 수신
• SLA 위반 경고 및 배포 상태 모니터링
• 라이선스 현황 확인
GUARDiA ITSM 서버 주소 입력 후 사용 가능합니다.
[지원]: guardia@zioinfo.co.kr
[회사]: (주)지오정보기술
```
**카테고리**: 비즈니스
**키워드**: ITSM, 인프라관리, IT운영, 서버관리, ChatOps, 모니터링
### 3-3. 콘텐츠 등급 설문
- 폭력: 없음
- 성인 콘텐츠: 없음
- 도박: 없음
- → **모든 연령** 등급
### 3-4. AAB 업로드
```
프로덕션 트랙 → 새 버전 만들기
→ eas build에서 다운로드한 .aab 업로드
→ 버전 노트 (한국어):
"GUARDiA Messenger 첫 번째 릴리즈
- 대시보드, SR 관리, AI 챗봇, 알림"
→ 검토 시작 (2~7일 소요)
```
---
## 4. Apple App Store 등록
### 4-1. App Store Connect 앱 생성
1. **appstoreconnect.apple.com** 접속
2. [나의 앱] → [+] → [새 앱]
3. 정보 입력:
| 항목 | 값 |
|------|-----|
| 플랫폼 | iOS |
| 이름 | GUARDiA Messenger |
| 기본 언어 | 한국어 |
| 번들 ID | kr.co.zioinfo.guardia |
| SKU | guardia-messenger-kr |
### 4-2. 앱 정보
**카테고리**: 비즈니스 (Business)
**부제**: AI 기반 IT 인프라 운영 플랫폼
**프라이버시 정책 URL**:
```
https://zioinfo.co.kr/privacy
```
> ⚠️ App Store는 Privacy Policy URL이 필수입니다. 홈페이지에 개인정보처리방침 페이지를 추가해야 합니다.
### 4-3. 스크린샷
각 기기별 최소 1개 스크린샷 필요:
- iPhone 6.7인치 (1290×2796): 필수
- iPhone 6.5인치 (1242×2688): 필수
- iPad Pro 12.9인치: 선택
```bash
# EAS로 시뮬레이터 스크린샷 자동 생성 (권장)
eas metadata:pull
```
### 4-4. IPA 업로드 및 심사
```bash
# EAS Submit으로 자동 업로드
eas submit --platform ios --profile production
# 또는 Transporter 앱 사용 (Mac에서)
```
심사 기간: **1~3일** (영업일 기준)
---
## 5. 전체 진행 순서
```
1. Expo 계정 생성 (expo.dev)
2. eas login / eas init
3. Google Play Console 개발자 등록 ($25)
4. Apple Developer Program 등록 ($99/년)
5. eas build --platform android --profile production
6. eas build --platform ios --profile production
7. 각 스토어에 메타데이터 등록
8. 빌드 업로드 (eas submit 또는 수동)
9. 심사 대기 (Android 2~7일, iOS 1~3일)
10. 출시 🎉
```
---
## 6. 개인정보처리방침 (홈페이지 추가 필요)
App Store 필수 요구사항:
```
zioinfo.co.kr/privacy 페이지에 아래 내용 추가:
1. 수집 정보: 이메일, 사용자명 (JWT 인증 목적)
2. 보관 기간: 앱 삭제 시 즉시 삭제 (기기 내 보관)
3. 제3자 제공: 없음
4. 문의: guardia@zioinfo.co.kr
```
---
## 7. 빠른 시작 명령어
```bash
# 1. EAS 설정
cd C:\GUARDiA\app
npm install -g eas-cli
eas login
eas init
# 2. Android 빌드 (바로 가능)
eas build --platform android --profile preview
# → APK 다운로드 URL 생성 (테스트 설치 가능)
# 3. 정식 빌드
eas build --platform android --profile production
eas build --platform ios --profile production
```
---
*GUARDiA Messenger v1.0.0 | (주)지오정보기술 | 2026-05-31*

View File

@ -0,0 +1,288 @@
# GUARDiA Messenger 개발 및 배포 가이드
> **버전**: 1.0.0 | **작성일**: 2026-05-31
> **기술 스택**: React Native + Expo SDK 51 + EAS Build
> **EAS 계정**: zioinfo | **성공 빌드**: 51096ada
---
## 1. 앱 개요
GUARDiA Messenger는 (주)지오정보기술의 GUARDiA ITSM 플랫폼과 연동하는 공식 모바일 앱입니다.
### 주요 기능
| 화면 | 기능 |
|------|------|
| 로그인 | GUARDiA ITSM JWT 인증 |
| 대시보드 | SR 통계, 서비스 상태, 배포 이력 |
| SR 관리 | 서비스 요청 목록 조회 및 신규 등록 |
| AI 챗봇 | Ollama LLM 기반 자연어 인프라 명령 |
| 알림 | 인시던트·SLA·배포 알림 수신 |
| 설정 | 프로필, 알림 설정, 로그아웃 |
### 기술 스택
| 항목 | 기술 |
|------|------|
| 프레임워크 | React Native 0.74.5 + Expo SDK 51 |
| 언어 | TypeScript (strict) |
| 라우터 | Expo Router 3.5.x |
| 인증 저장소 | expo-secure-store (보안 키체인) |
| HTTP 클라이언트 | Axios |
| 빌드 시스템 | EAS Build (Expo Application Services) |
| 서버 연결 | https://zioinfo.co.kr:8443 |
---
## 2. 개발 환경 설정
### 2-1. 필수 설치
```bash
# Node.js 20 LTS
node --version # v20 이상
# Expo CLI
npm install -g expo-cli eas-cli
# 프로젝트 설치
cd C:\GUARDiA\app
npm install --legacy-peer-deps
```
### 2-2. 로컬 개발 서버
```bash
cd C:\GUARDiA\app
npx expo start
# QR 코드 스캔 → Expo Go 앱에서 실행 (개발 전용)
# 또는 npx expo start --android (에뮬레이터)
```
### 2-3. 프로젝트 구조
```
C:\GUARDiA\app\
├── app/
│ ├── _layout.tsx ← 루트 레이아웃 (인증 라우팅)
│ ├── (auth)/login.tsx ← 로그인
│ └── (tabs)/
│ ├── _layout.tsx ← 탭 네비게이터
│ ├── index.tsx ← 대시보드
│ ├── sr.tsx ← SR 관리
│ ├── chat.tsx ← AI 챗봇
│ ├── notifications.tsx← 알림
│ └── settings.tsx ← 설정
├── components/ ← 공통 컴포넌트
├── constants/Config.ts ← 서버 URL, 컬러 시스템
├── hooks/useAuth.ts ← JWT 인증 훅
├── services/api.ts ← GUARDiA ITSM API 클라이언트
├── plugins/
│ └── withGradleProps.js ← Gradle 빌드 최적화
├── assets/ ← 아이콘, 스플래시 이미지
├── app.json ← Expo 앱 설정
├── eas.json ← EAS 빌드 프로필
├── babel.config.js ← Babel 설정
└── .easignore ← EAS 업로드 제외 목록
```
---
## 3. EAS Build 가이드
### 3-1. EAS 초기 설정 (최초 1회)
```bash
cd C:\GUARDiA\app
# EAS 로그인
npx eas-cli login
# → expo.dev 계정 입력
# 프로젝트 초기화
npx eas-cli init
# → 프로젝트 ID 자동 등록
```
### 3-2. 빌드 명령어
```bash
# Android APK (테스트 배포 — 약 10~15분)
npx eas-cli build --platform android --profile preview
# Android AAB (Play Store 제출용)
npx eas-cli build --platform android --profile production
# iOS IPA (App Store 제출용, Apple Developer $99/년 필요)
npx eas-cli build --platform ios --profile production
```
### 3-3. 빌드 전 필수 체크리스트
| 항목 | 확인 방법 | 올바른 상태 |
|------|---------|-----------|
| android/ 폴더 없음 | `ls android/` | 없어야 함 |
| .easignore 설정 | `cat .easignore` | `android/`, `ios/` 포함 |
| PNG Crunching 비활성화 | `cat plugins/withGradleProps.js` | `false` 확인 |
| babel.config.js | `cat babel.config.js` | `babel-preset-expo`만 |
| EAS 로그인 | `npx eas-cli whoami` | `zioinfo` 표시 |
### 3-4. APK 폰 설치 방법
1. 안드로이드 폰에서 **Expo 빌드 URL** 열기
2. **Download** 버튼 탭
3. 설정 → 보안 → 알 수 없는 앱 설치 허용 (최초 1회)
4. APK 설치 완료
> **주의**: `adb` / Android Studio 불필요 — 브라우저 직접 다운로드로 설치
---
## 4. 빌드 이슈 이력 및 해결책
> 이 섹션은 실제 빌드 과정에서 발생한 이슈와 해결책입니다. 동일한 이슈 재발 시 이 섹션을 참조하세요.
### 이슈 1: android/ 폴더 → EAS Bare Workflow 오인
**증상**: `EAS_BUILD_UNKNOWN_GRADLE_ERROR` (반복 실패)
**원인 흐름**:
```
로컬 expo prebuild 실행
→ android/ 폴더 생성
→ EAS 업로드에 포함
→ EAS: "Bare Workflow 프로젝트" 인식
→ 로컬 android/ 그대로 사용 (EAS 환경과 불일치)
→ Gradle 빌드 실패
```
**해결**: `.easignore` 파일에 추가
```
android/
ios/
```
### 이슈 2: PIL PNG + AAPT2 PNG Crunching 충돌
**증상**: `expo-splash-screen:packageReleaseResources` 단계 실패
**원인**: Python PIL 생성 PNG + `enablePngCrunchInReleaseBuilds=true` → AAPT2 처리 불가
**해결**: `plugins/withGradleProps.js` 생성
```javascript
const { withGradleProperties } = require('@expo/config-plugins')
module.exports = function(config) {
return withGradleProperties(config, (cfg) => {
const set = (key, value) => {
const idx = cfg.modResults.findIndex(p => p.key === key)
idx !== -1 ? cfg.modResults[idx].value = value
: cfg.modResults.push({ type:'property', key, value })
}
set('android.enablePngCrunchInReleaseBuilds', 'false')
set('reactNativeArchitectures', 'arm64-v8a')
set('org.gradle.jvmargs', '-Xmx4096m -XX:MaxMetaspaceSize=1024m')
return cfg
})
}
```
### 이슈 3: expo-notifications 플러그인 빌드 실패
**증상**: Firebase 관련 Gradle 오류
**원인**: `expo-notifications` 플러그인이 `google-services.json` 파일 필요
**해결**: `app.json` plugins에서 `expo-notifications` 제거. 푸시 알림 추가 시 Firebase 설정 후 재등록.
### 이슈 4: expo-router/babel deprecated
**증상**: Babel 경고, 일부 환경에서 빌드 오류
**해결**: `babel.config.js`
```javascript
module.exports = function(api) {
api.cache(true)
return { presets: ['babel-preset-expo'] }
}
```
### 최종 성공한 빌드
| 항목 | 값 |
|------|-----|
| 빌드 ID | 51096ada-9735-4ea8-9e81-5f5991731ea8 |
| 플랫폼 | Android (APK) |
| EAS 계정 | zioinfo |
| 소요 시간 | 약 12분 |
---
## 5. 스토어 등록 절차
### 5-1. 사전 준비
| 항목 | Google Play | Apple App Store |
|------|------------|-----------------|
| 개발자 계정 | $25 (1회) | $99/년 |
| Privacy Policy | 필수 | 필수 |
| 스크린샷 | 최소 2개 | 최소 3개 (6.7인치) |
| APK/IPA | AAB 파일 | IPA 파일 |
### 5-2. Google Play Store 등록
```
1. play.google.com/console 접속
2. [앱 만들기] → 패키지명: kr.co.zioinfo.guardia
3. 스토어 등록정보 → 메타데이터 입력
4. 콘텐츠 등급 → 모든 연령 (비즈니스 앱)
5. 프로덕션 트랙 → AAB 업로드
6. 검토 제출 (2~7 영업일)
```
### 5-3. Apple App Store 등록
```
1. appstoreconnect.apple.com 접속
2. [새 앱] → 번들 ID: kr.co.zioinfo.guardia
3. 앱 정보 + 스크린샷 업로드
4. Privacy Policy URL 등록 (필수!)
5. npx eas-cli submit --platform ios --profile production
6. 심사 제출 (1~3 영업일)
```
---
## 6. 앱 로그인 정보
| 항목 | 값 |
|------|-----|
| 서버 URL | https://zioinfo.co.kr:8443 |
| 관리자 계정 | admin |
| 비밀번호 | Admin@zioinfo2026! |
---
## 7. 하네스 사용법
이 프로젝트는 `.claude/` 하네스가 구성되어 있습니다.
```
기능 개발 요청: "SR 상세 화면 추가해줘"
→ messenger-orchestrator 스킬 트리거
→ rn-developer가 화면 구현
→ eas-engineer가 빌드 검증
빌드 요청: "Android APK 빌드해줘"
→ eas-build-deploy 스킬 참조
→ 체크리스트 확인 후 빌드 실행
스토어 등록: "Play Store 등록 도와줘"
→ store-publish 스킬 참조
→ 메타데이터 + 절차 가이드
```
---
*GUARDiA Messenger v1.0.0 | (주)지오정보기술 | 2026-05-31*

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,206 @@
# GUARDiA 프로젝트 세션 전체 작업 요약
> **세션 날짜**: 2026-05-30 ~ 2026-05-31
> **작성 목적**: clear 전 중요 내용 및 스킬 보존
---
## 1. 서버 구성 (zioinfo.co.kr)
### 설치 완료 SW
| 카테고리 | SW | 포트 |
|----------|-----|------|
| 웹 서버 | Nginx 1.24 | 80/443 |
| 홈페이지 | Spring Boot 3.2.5 | 8082 |
| ITSM | GUARDiA FastAPI v2.0 | 8001/8443 |
| 관리자 | GUARDiA Manager | 8090/8002 |
| DB | PostgreSQL 16.11 | 5432 |
| AI | Ollama + Llama3:8b + nomic-embed-text | 11434 |
| AI 프레임워크 | LangChain 1.3.2 + ChromaDB 1.5.9 | - |
| Git | Gitea 1.22.3 | 3000 |
| CI/CD | Jenkins 2.555 + Deploy Webhook | 8080/9999 |
| 메일 | Postfix + Dovecot | 25/587/143/993 |
### Gitea 저장소
```
http://zioinfo.co.kr:3000
├── zio/zioinfo-web ← 홈페이지 (Spring Boot + React)
├── zio/guardia-itsm ← GUARDiA ITSM (FastAPI)
└── zio/guardia-messenger ← GUARDiA Messenger (React Native)
```
### CI/CD 흐름
```
git push gitea main
→ Gitea Webhook → Deploy Server(9999)
→ git pull → 빌드 → systemctl restart
```
---
## 2. 개발 완료 시스템
### 2-1. GUARDiA ITSM (기존 + 추가)
- **개방망 지원**: HTTPS 8443, API Key 인증, CORS 외부 허용
- **라이선스**: TRIAL/COMMUNITY/STANDARD/ENTERPRISE 관리
- **Export/Import**: 폐쇄망 데이터 번들 (HMAC-SHA256 서명)
- **AI 프레임워크**: LangChain + ChromaDB + 코드베이스 임베딩
- **datetime 버그 수정**: timezone-aware/naive 충돌 → .replace(tzinfo=None)
### 2-2. GUARDiA Manager (신규)
```
http://zioinfo.co.kr:8090
```
| 화면 | 기능 |
|------|------|
| 대시보드 | SR 통계, 리소스 게이지, 배포 타임라인 |
| 서버/CMDB | 자산 목록, 서비스 재시작 |
| 배포/CI-CD | 배포 이력, 수동 트리거 |
| 보안 | API Key 발급/관리, 감사 로그 |
| 라이선스 | 에디션 관리, 체험판 발급, 이력 |
| 데이터 연동 | Export/Import (폐쇄망 연동) |
| LLM | Ollama 모델 현황 |
| 설정 | .env 뷰어, Nginx 리로드 |
### 2-3. GUARDiA Messenger (신규)
```
패키지: kr.co.zioinfo.guardia
EAS 계정: zioinfo / 프로젝트: ca2f72d6-...
성공 빌드: 51096ada (Android APK)
```
**⚠️ EAS 빌드 필수 원칙**:
1. `.easignore`: android/, ios/ 반드시 포함
2. `plugins/withGradleProps.js`: `enablePngCrunchInReleaseBuilds=false`
3. `expo-notifications`: app.json plugins 등록 금지
4. `babel.config.js`: `babel-preset-expo`만 사용
**알림 연동**:
- WebSocket: `wss://zioinfo.co.kr:8443/ws/events?token={jwt}`
- 채널: sr, deploy, sla, incident
- `hooks/useWebSocket.ts` 구현 완료
### 2-4. 홈페이지 관리자시스템 (신규)
```
http://zioinfo.co.kr/admin
계정: admin / Admin@2026!
```
- JWT 인증 (GUARDiA ITSM 공유)
- 뉴스/채용/문의 CRUD
- Spring Security + jjwt 0.12.3
---
## 3. SMTP 메일 서버
```
도메인: @zioinfo.co.kr
서버: mail.zioinfo.co.kr (zioinfo.co.kr)
```
| 항목 | 값 |
|------|-----|
| 계정 | info, admin, ythong, choyounbun, guardia |
| 비밀번호 | 1q2w3e!Q (모두 동일) |
| SMTP | :25 / :587 (STARTTLS) |
| IMAP | :143 / :993 (SSL) |
| SPF | v=spf1 ip4:zioinfo.co.kr ~all (가비아) |
| DKIM | mail._domainkey.zioinfo.co.kr (가비아) |
| DMARC | _dmarc.zioinfo.co.kr (가비아) |
**스팸 방지 중요사항**:
- 이메일 본문에 `http://zioinfo.co.kr:8001` 같은 IP 직접 링크 금지 → 피싱 경고 발생
- PTR 역방향 DNS 없음 → Gmail 스팸 가능성 → NCloud 지원에 PTR 설정 요청 필요
---
## 4. 하네스 구조
### C:\GUARDiA\itsm (GUARDiA ITSM)
```
.claude/agents/: sr-manager, deploy-engineer, incident-responder, sla-guardian
.claude/skills/: guardia-orchestrator, deploy-pipeline, sr-lifecycle, code-review
```
### C:\GUARDiA\manager (GUARDiA Manager)
```
.claude/agents/: ux-architect, backend-engineer, integration-specialist, security-engineer, devops-engineer
.claude/skills/: manager-orchestrator, manager-ui, manager-api, manager-integration, manager-security, manager-deploy
```
### C:\GUARDiA\app (GUARDiA Messenger)
```
.claude/agents/: rn-developer, eas-engineer, store-publisher, doc-writer
.claude/skills/: messenger-orchestrator, rn-screen-dev, eas-build-deploy, store-publish, doc-generator
└── rn-screen-dev/references/build-issues.md ← EAS 빌드 이슈 이력 (매우 중요)
```
### C:\GUARDiA\workspace\zioinfo-web (홈페이지)
```
트리거: workspace-analyzer 스킬
```
---
## 5. 매뉴얼 파일 목록 (C:\GUARDiA\manual)
| 번호 | 파일 | 내용 |
|------|------|------|
| 18 | zio서버_설치SW_목록.md | 전체 SW 목록 |
| 19 | zio서버_운영가이드.md | 운영 절차 |
| 20 | zio서버_CICD_가이드.md | CI/CD 흐름 |
| 21 | zio서버_장애대응_가이드.md | Runbook |
| 22 | GUARDiA_개방망_운영가이드.md | 개방망 설정 |
| 23-24 | 개방망 PDF/PPTX | - |
| 25 | Manager 라이선스 가이드.md | 라이선스 관리 |
| 26-27 | 라이선스 PDF/PPTX | - |
| 28 | 폐쇄망_데이터연동_가이드.md | Export/Import |
| 29-30 | 데이터연동 PDF/PPTX | - |
| 31 | AI_플랫폼_제안서.md | LangChain/ChromaDB 제안 |
| 32 | 벡터DB_제안서.md | ChromaDB 상세 |
| 33 | Messenger_스토어_등록_가이드.md | Play Store/App Store |
| 34 | Messenger_개발_배포_가이드.md | EAS 빌드 가이드 |
| 35-36 | Messenger PDF/PPTX | - |
---
## 6. 주요 계정 정보
| 서비스 | ID | PW |
|--------|----|----|
| SSH | root | 1q2w3e!Q |
| GUARDiA ITSM | admin | 1111 |
| GUARDiA Manager | admin | Admin@zioinfo2026! |
| 홈페이지 관리자 | admin | Admin@2026! |
| Gitea | zio | Zio@Admin2026! |
| PostgreSQL | guardia | G@urd1a_2026! (URL: G%40urd1a_2026%21) |
| SMTP 계정 | *@zioinfo.co.kr | 1q2w3e!Q |
| Expo EAS | zioinfo | expo.dev 로그인 |
---
## 7. 알림 테스트 결과
### ITSM → Messenger (8/8 PASS)
JWT → WebSocket 연결 → SR 이벤트 브로드캐스트 → 앱 알림탭 수신
### Messenger → ITSM (10/10 PASS)
앱 SR탭 등록 → POST /api/tasks → ITSM SR 생성 확인
AI챗봇 → "nginx 재시작해줘" → AI가 SR 자동 생성 응답
---
*GUARDiA 프로젝트 세션 정리 | 2026-05-31*

View File

@ -0,0 +1,81 @@
# GUARDiA 하네스 스킬 빠른 참조
> 새 세션에서 각 스킬을 트리거하는 키워드 모음
---
## GUARDiA ITSM (C:\GUARDiA\itsm)
| 스킬 | 트리거 키워드 |
|------|------------|
| `guardia-orchestrator` | SR 처리, 배포 진행, 코드 리뷰, SLA 현황, 인시던트, RCA, 보안 패치, Jira 연동, 다시 실행 |
| `deploy-pipeline` | 배포, 빌드, 릴리즈, Jenkins, VibeSession |
| `sr-lifecycle` | SR 접수, SR 조회, 티켓 관리 |
---
## GUARDiA Manager (C:\GUARDiA\manager)
| 스킬 | 트리거 키워드 |
|------|------------|
| `manager-orchestrator` | 관리자 화면, M-01~M-08, 대시보드 수정, 서버 관리, 배포 관리, 가이드 작성, 다시 실행 |
| `manager-ui` | 화면 구현, 컴포넌트, NCloud 스타일, 대시보드 차트 |
| `manager-api` | FastAPI 백엔드, 시스템 API, 서비스 제어 |
| `manager-integration` | GUARDiA API 연동, Gitea 연동, Ollama 조회 |
| `manager-security` | JWT 인증, Route Guard, API Key 보안 |
| `manager-deploy` | Nginx 설정, systemd, 배포 파이프라인 |
---
## GUARDiA Messenger (C:\GUARDiA\app)
| 스킬 | 트리거 키워드 |
|------|------------|
| `messenger-orchestrator` | 화면 추가, 기능 구현, EAS 빌드, APK, Play Store, App Store, 가이드 작성, 다시 실행 |
| `rn-screen-dev` | 화면 구현, 컴포넌트, UI 수정, API 연동 |
| `eas-build-deploy` | EAS 빌드, APK 빌드, Gradle 오류, 빌드 실패 |
| `store-publish` | Play Store, App Store, 스크린샷, Privacy Policy |
| `doc-generator` | 가이드 작성, PDF 생성, PPTX 생성, 문서화 |
---
## 홈페이지 (C:\GUARDiA\workspace\zioinfo-web)
| 스킬 | 트리거 키워드 |
|------|------------|
| `workspace-analyzer` | 소스 분석, 개발환경 가이드, 하네스 적용, workspace |
---
## 주요 빠른 명령
```bash
# GUARDiA ITSM 재시작
ssh root@zioinfo.co.kr "systemctl restart guardia"
# EAS Android 빌드 (C:\GUARDiA\app에서)
npx eas-cli build --platform android --profile preview
# Gitea Push (홈페이지)
cd C:\GUARDiA\workspace\zioinfo-web
git push gitea main:main
# 서버 전체 상태 확인
ssh root@zioinfo.co.kr "for s in nginx zioinfo guardia guardia-manager gitea jenkins postgresql; do printf '%-22s %s\n' $s $(systemctl is-active $s); done"
```
---
## ⚠️ 자주 발생하는 오류와 해결
| 오류 | 원인 | 해결 |
|------|------|------|
| GUARDiA 500 시작 실패 | DATABASE_URL의 @를 %40으로 인코딩 안함 | .env에서 G@urd1a → G%40urd1a%21 |
| EAS Gradle 실패 | android/ 폴더가 업로드됨 | .easignore에 android/, ios/ 추가 |
| EAS packageReleaseResources 실패 | PIL PNG + AAPT2 충돌 | plugins/withGradleProps.js 확인 |
| Gmail 피싱 경고 | 본문에 IP 직접 링크 포함 | http://zioinfo.co.kr:8001 URL 제거 |
| Gmail 스팸 분류 | PTR 레코드 없음, DMARC 없음 | NCloud PTR 신청, 가비아 DMARC 등록 |
| Nginx IPv6 오류 | NCloud가 IPv4 only | sed로 [::]:80 제거 후 nginx -t |
| Dovecot 시작 실패 | IPv6 listen 시도 | dovecot.conf에 listen = 0.0.0.0 |
*GUARDiA 프로젝트 | 2026-05-31*

230
gen_export_docs.py Normal file
View File

@ -0,0 +1,230 @@
#!/usr/bin/env python3
"""Export/Import 가이드 PDF + PPTX 생성"""
from pathlib import Path
OUT_DIR = Path(__file__).parent
def gen_pdf(out):
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import mm
from reportlab.lib.enums import TA_CENTER
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, HRFlowable, PageBreak
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
import os
FONT_DIRS = ["C:/Windows/Fonts","/usr/share/fonts/truetype/noto","/usr/share/fonts/truetype/dejavu"]
font = "Helvetica"
for fn,alias in [("malgun.ttf","Malgun"),("NanumGothic.ttf","NanumGothic"),("DejaVuSans.ttf","DejaVuSans")]:
for d in FONT_DIRS:
fp = os.path.join(d,fn)
if os.path.exists(fp):
try: pdfmetrics.registerFont(TTFont(alias,fp)); font=alias; break
except: pass
if font != "Helvetica": break
BRAND=colors.HexColor("#1a3a6b"); ACCENT=colors.HexColor("#4f6ef7")
GREEN=colors.HexColor("#22c55e"); GRAY=colors.HexColor("#f0f2f5")
MUTED=colors.HexColor("#64748b")
doc = SimpleDocTemplate(out,pagesize=A4,leftMargin=20*mm,rightMargin=20*mm,topMargin=20*mm,bottomMargin=20*mm)
W = A4[0]-40*mm
styles = getSampleStyleSheet()
def sty(name,**kw):
kw.setdefault("fontName",font)
return ParagraphStyle(name,parent=styles["Normal"],**kw)
def hr(c=None): return HRFlowable(width="100%",thickness=1,color=c or ACCENT,spaceAfter=4,spaceBefore=4)
def tbl(data,cws=None,hdr=True):
t=Table(data,colWidths=cws)
base=[("FONTNAME",(0,0),(-1,-1),font),("FONTSIZE",(0,0),(-1,-1),9),
("ROWBACKGROUNDS",(0,1),(-1,-1),[colors.white,GRAY]),
("GRID",(0,0),(-1,-1),.5,colors.HexColor("#e2e8f0")),
("VALIGN",(0,0),(-1,-1),"MIDDLE"),
("LEFTPADDING",(0,0),(-1,-1),6),("RIGHTPADDING",(0,0),(-1,-1),6),
("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),5)]
if hdr:base+=[("BACKGROUND",(0,0),(-1,0),BRAND),("TEXTCOLOR",(0,0),(-1,0),colors.white),
("FONTNAME",(0,0),(-1,0),font)]
t.setStyle(TableStyle(base)); return t
S={"h1":sty("h1",fontSize=18,textColor=BRAND,spaceBefore=16,spaceAfter=8,leading=24),
"h2":sty("h2",fontSize=13,textColor=ACCENT,spaceBefore=10,spaceAfter=5,leading=18),
"body":sty("body",fontSize=10,textColor=colors.HexColor("#1e293b"),leading=16,spaceAfter=4),
"code":sty("code",fontName="Courier",fontSize=9,backColor=GRAY,
textColor=colors.HexColor("#1d4ed8"),leading=14,leftIndent=10,spaceBefore=3,spaceAfter=3),
"note":sty("note",fontSize=9,textColor=MUTED,leftIndent=10,leading=14,spaceAfter=3)}
story=[]
# 표지
cover=Table([[Paragraph("GUARDiA 폐쇄망 ↔ 개방망",sty("ct",fontSize=26,textColor=colors.white,alignment=TA_CENTER,leading=34))],
[Paragraph("데이터 Export / Import 가이드",sty("cs",fontSize=16,textColor=colors.HexColor("#aac4e8"),alignment=TA_CENTER))],
[Paragraph("v1.0.0 | 2026-05-30 | (주)지오정보기술",sty("cm",fontSize=10,textColor=colors.HexColor("#7c85a8"),alignment=TA_CENTER))]],
colWidths=[W])
cover.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),BRAND),
("TOPPADDING",(0,0),(-1,-1),35),("BOTTOMPADDING",(0,0),(-1,-1),35),
("LEFTPADDING",(0,0),(-1,-1),20),("RIGHTPADDING",(0,0),(-1,-1),20)]))
story+=[Spacer(1,30*mm),cover,PageBreak()]
story+=[Paragraph("1. 개요",S["h1"]),hr(),
Paragraph("폐쇄망 GUARDiA ITSM의 데이터를 개방망 GUARDiA Manager로 안전하게 이관합니다. "
"번들 파일에 HMAC-SHA256 서명을 포함하여 위변조를 방지하며, 민감 정보는 자동 마스킹됩니다.",S["body"]),
Spacer(1,6),Paragraph("1-1. 보안 특징",S["h2"]),
tbl([["특징","내용"],["HMAC-SHA256 서명","번들 ZIP 위변조 방지"],
["민감 정보 마스킹","IP 주소, SSH 비밀번호 → '****' 처리"],
["Dry Run 모드","실제 저장 전 사전 검증"],
["중복 방지","sr_id 기준 중복 SKIP"]],cws=[55*mm,110*mm]),
Spacer(1,10),Paragraph("2. API 엔드포인트",S["h1"]),hr(),
tbl([["메서드","경로","설명"],
["GET","/api/export-import/export/bundle","전체 번들 ZIP (권장)"],
["GET","/api/export-import/export/sr","SR 목록 JSON"],
["GET","/api/export-import/export/cmdb","CMDB 서버 자산 JSON"],
["GET","/api/export-import/export/institutions","기관 목록 JSON"],
["GET","/api/export-import/export/audit","감사 로그 JSON"],
["POST","/api/export-import/import/bundle","번들 ZIP Import"],
["POST","/api/export-import/import/sr","SR JSON Import"]],
cws=[18*mm,75*mm,72*mm]),
PageBreak(),
Paragraph("3. 연동 흐름",S["h1"]),hr(),
tbl([["단계","작업","비고"],
["1","폐쇄망 서버에서 번들 Export","GET /export/bundle → ZIP 다운로드"],
["2","USB/보안매체로 ZIP 이동","Air Gap 환경 고려"],
["3","Manager UI에서 파일 업로드","http://zioinfo.co.kr:8090/export-import"],
["4","Dry Run 검증 실행","HMAC 서명 + 데이터 카운트 확인"],
["5","Import 실행 (dry_run=false)","중복 sr_id 자동 SKIP"]],cws=[12*mm,75*mm,78*mm]),
Spacer(1,10),Paragraph("4. 테스트 결과 (7/7 PASS)",S["h1"]),hr(),
tbl([["#","테스트 항목","결과"],
["T1","SR Export (5건)","PASS"],
["T2","CMDB Export (6건)","PASS"],
["T3","기관 Export (3건)","PASS"],
["T4","감사 로그 Export (5건)","PASS"],
["T5","번들 ZIP Export (HMAC 서명)","PASS"],
["T6","SR Import dry_run","PASS"],
["T7","Manager UI /export-import","PASS"]],cws=[12*mm,90*mm,63*mm]),
Spacer(1,6),
Paragraph("버그 수정: date 타입 JSON 직렬화 오류 → isoformat() 처리 완료",
sty("fix",fontSize=10,textColor=GREEN,fontName=font)),
Spacer(1,12),
hr(colors.HexColor("#e2e8f0")),
Paragraph("GUARDiA ITSM v2.0.0 | (주)지오정보기술 | 2026-05-30",
sty("foot",fontSize=8,textColor=MUTED,alignment=TA_CENTER,fontName=font))]
doc.build(story)
print(f"PDF: {out}")
def gen_pptx(out):
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN
W,H=Inches(13.33),Inches(7.5)
prs=Presentation(); prs.slide_width=W; prs.slide_height=H
BRAND=RGBColor(0x1a,0x3a,0x6b); ACCENT=RGBColor(0x4f,0x6e,0xf7)
WHITE=RGBColor(0xff,0xff,0xff); GRAY=RGBColor(0xf0,0xf2,0xf5)
GREEN=RGBColor(0x22,0xc5,0x5e); MUTED=RGBColor(0x64,0x74,0x8b)
DARK=RGBColor(0x1e,0x29,0x3b)
def blank(): return prs.slides.add_slide(prs.slide_layouts[6])
def rect(sl,x,y,w,h,fill=None):
s=sl.shapes.add_shape(1,x,y,w,h)
if fill: s.fill.solid(); s.fill.fore_color.rgb=fill
else: s.fill.background()
s.line.fill.background(); return s
def text(sl,t,x,y,w,h,sz=14,bold=False,color=DARK,align=PP_ALIGN.LEFT):
tb=sl.shapes.add_textbox(x,y,w,h); tf=tb.text_frame; tf.word_wrap=True
p=tf.paragraphs[0]; p.alignment=align; r=p.add_run(); r.text=t
r.font.size=Pt(sz); r.font.bold=bold; r.font.color.rgb=color
def tbl_sl(sl,hdrs,rows,x,y,w,h,cws=None):
t=sl.shapes.add_table(len(rows)+1,len(hdrs),x,y,w,h).table
if cws:
for i,cw in enumerate(cws): t.columns[i].width=cw
def cell(c,v,bg=None,clr=DARK,bold=False):
c.text=str(v); c.text_frame.paragraphs[0].font.size=Pt(9)
c.text_frame.paragraphs[0].font.bold=bold
c.text_frame.paragraphs[0].font.color.rgb=clr
if bg: c.fill.solid(); c.fill.fore_color.rgb=bg
for j,h in enumerate(hdrs): cell(t.cell(0,j),h,bg=BRAND,clr=WHITE,bold=True)
for i,row in enumerate(rows):
bg=GRAY if i%2==0 else WHITE
for j,v in enumerate(row): cell(t.cell(i+1,j),v,bg=bg)
# S1 표지
s=blank()
rect(s,0,0,W,H,fill=BRAND)
rect(s,Inches(.5),Inches(1.2),Inches(12.33),Inches(4.0),fill=RGBColor(0x25,0x4a,0x80))
text(s,"GUARDiA 폐쇄망 ↔ 개방망",Inches(.8),Inches(1.5),Inches(11.73),Inches(1.0),sz=38,bold=True,color=WHITE,align=PP_ALIGN.CENTER)
text(s,"데이터 Export / Import 연동 가이드",Inches(.8),Inches(2.7),Inches(11.73),Inches(.7),sz=20,color=RGBColor(0xaa,0xc4,0xe8),align=PP_ALIGN.CENTER)
text(s,"v1.0.0 | 2026-05-30 | (주)지오정보기술",Inches(.8),Inches(3.5),Inches(11.73),Inches(.5),sz=13,color=MUTED,align=PP_ALIGN.CENTER)
for i,(t_,c) in enumerate([("🔒 HMAC 서명",RGBColor(0x22,0xc5,0x5e)),("📦 번들 ZIP",ACCENT),
("🔍 Dry Run",RGBColor(0xf5,0x9e,0x0b)),("🚫 민감 마스킹",RGBColor(0xef,0x44,0x44))]):
bx=Inches(2.3+i*2.2)
rect(s,bx,Inches(4.9),Inches(2.0),Inches(.5),fill=c)
text(s,t_,bx+Inches(.1),Inches(4.95),Inches(1.8),Inches(.4),sz=11,bold=True,color=WHITE,align=PP_ALIGN.CENTER)
# S2 연동 흐름
s=blank()
rect(s,0,0,W,Inches(1.2),fill=BRAND)
text(s,"데이터 연동 흐름",Inches(.4),Inches(.28),Inches(12),Inches(.7),sz=24,bold=True,color=WHITE)
boxes=[("🖥️","폐쇄망\nGUARDiA","폐쇄망 서버"),("📦","번들 Export\n(ZIP+HMAC)","GET /export/bundle"),
("🔌","물리적 이동","USB/보안매체"),("☁️","개방망\nManager","Import UI")]
for i,(icon,lbl,sub) in enumerate(boxes):
bx=Inches(.4+i*3.1)
rect(s,bx,Inches(1.5),Inches(2.7),Inches(2.5),fill=GRAY)
text(s,icon,bx+Inches(.9),Inches(1.7),Inches(.9),Inches(.9),sz=28,align=PP_ALIGN.CENTER,color=DARK)
text(s,lbl,bx+Inches(.2),Inches(2.7),Inches(2.3),Inches(.7),sz=13,bold=True,color=BRAND,align=PP_ALIGN.CENTER)
text(s,sub,bx+Inches(.2),Inches(3.4),Inches(2.3),Inches(.4),sz=10,color=MUTED,align=PP_ALIGN.CENTER)
if i<3:
text(s,"",Inches(.4+i*3.1+2.75),Inches(2.4),Inches(.35),Inches(.5),sz=22,color=MUTED,align=PP_ALIGN.CENTER)
tbl_sl(s,["단계","작업","비고"],
[["1","폐쇄망 Export","GET /export/bundle → ZIP"],["2","물리적 이동","USB/보안매체"],
["3","Manager 업로드","http://zioinfo.co.kr:8090/export-import"],
["4","Dry Run 검증","HMAC + 데이터 카운트"],["5","Import 실행","중복 SKIP"]],
Inches(.4),Inches(4.2),Inches(12.5),Inches(2.8),cws=[Inches(1.0),Inches(5.5),Inches(6.0)])
# S3 API 명세
s=blank()
rect(s,0,0,W,Inches(1.2),fill=BRAND)
text(s,"API 명세",Inches(.4),Inches(.28),Inches(12),Inches(.7),sz=24,bold=True,color=WHITE)
tbl_sl(s,["메서드","경로","설명"],
[["GET","/api/export-import/export/bundle","전체 번들 ZIP (HMAC 서명 포함)"],
["GET","/api/export-import/export/sr","SR 목록 JSON (최대 5000건)"],
["GET","/api/export-import/export/cmdb","CMDB 서버 자산 JSON"],
["GET","/api/export-import/export/institutions","기관 목록 JSON"],
["GET","/api/export-import/export/audit","감사 로그 JSON (최대 2000건)"],
["POST","/api/export-import/import/bundle","번들 ZIP Import (dry_run 지원)"],
["POST","/api/export-import/import/sr","SR JSON Import (중복 SKIP)"]],
Inches(.4),Inches(1.4),Inches(12.5),Inches(3.5),cws=[Inches(1.5),Inches(5.5),Inches(5.5)])
rect(s,Inches(.4),Inches(5.2),Inches(12.5),Inches(.8),fill=RGBColor(0xff,0xf3,0xcd))
text(s,"📌 민감 필드 자동 마스킹: ip_addr, os_pw_enc, ssh_user, ssh_pw → '****'",
Inches(.6),Inches(5.3),Inches(12.1),Inches(.6),sz=12,color=RGBColor(0x85,0x4d,0x0e))
# S4 테스트 결과
s=blank()
rect(s,0,0,W,Inches(1.2),fill=BRAND)
text(s,"테스트 결과 — 7/7 PASS ✅",Inches(.4),Inches(.28),Inches(12),Inches(.7),sz=24,bold=True,color=WHITE)
tbl_sl(s,["#","테스트 항목","내용","결과"],
[["T1","SR Export","5건 JSON","PASS"],["T2","CMDB Export","6건 JSON","PASS"],
["T3","기관 Export","3건 JSON","PASS"],["T4","감사 로그 Export","5건 JSON","PASS"],
["T5","번들 ZIP Export","HMAC 서명 포함 5KB ZIP","PASS"],
["T6","SR Import dry_run","1건 검증, 저장 없음","PASS"],
["T7","Manager UI","HTTP 200","PASS"]],
Inches(.4),Inches(1.4),Inches(12.5),Inches(4.0),cws=[Inches(.7),Inches(3.5),Inches(5.5),Inches(2.3)])
rect(s,Inches(.4),Inches(5.8),Inches(12.5),Inches(.8),fill=GREEN)
text(s,"✅ 전체 7개 테스트 모두 통과 | 버그 수정: date JSON 직렬화 오류 → isoformat() 처리",
Inches(.6),Inches(5.9),Inches(12.1),Inches(.6),sz=13,bold=True,color=WHITE,align=PP_ALIGN.CENTER)
# S5 마지막
s=blank()
rect(s,0,0,W,H,fill=BRAND)
text(s,"GUARDiA 폐쇄망 ↔ 개방망 데이터 연동",Inches(1),Inches(2.3),Inches(11.33),Inches(1.2),sz=28,bold=True,color=WHITE,align=PP_ALIGN.CENTER)
text(s,"안전한 HMAC 서명으로 데이터 무결성을 보장합니다",Inches(1),Inches(3.7),Inches(11.33),Inches(.6),sz=14,color=RGBColor(0xaa,0xc4,0xe8),align=PP_ALIGN.CENTER)
text(s,"(주)지오정보기술 | GUARDiA v2.0.0 | 2026-05-30",Inches(1),Inches(5.5),Inches(11.33),Inches(.5),sz=12,color=MUTED,align=PP_ALIGN.CENTER)
prs.save(out); print(f"PPTX: {out}")
if __name__ == "__main__":
gen_pdf(str(OUT_DIR/"29_GUARDiA_폐쇄망_데이터연동_가이드.pdf"))
gen_pptx(str(OUT_DIR/"30_GUARDiA_폐쇄망_데이터연동_발표자료.pptx"))
print("완료")

460
gen_license_docs.py Normal file
View File

@ -0,0 +1,460 @@
#!/usr/bin/env python3
"""
GUARDiA Manager 라이선스 관리 가이드 PDF + PPTX 자동 생성
출력:
manual/26_GUARDiA_Manager_라이선스_가이드.pdf
manual/27_GUARDiA_Manager_라이선스_발표자료.pptx
"""
from pathlib import Path
OUT_DIR = Path(__file__).parent
# ═══════════════════════════════════════════════════════
# PDF
# ═══════════════════════════════════════════════════════
def gen_pdf(out: str):
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import mm
from reportlab.lib.enums import TA_CENTER, TA_LEFT
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, PageBreak
)
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
import os
FONT_DIRS = ["C:/Windows/Fonts", "/usr/share/fonts/truetype/noto",
"/usr/share/fonts/truetype/dejavu"]
FONT_CANDS = [("malgun.ttf","Malgun"),("NanumGothic.ttf","NanumGothic"),
("DejaVuSans.ttf","DejaVuSans")]
font = "Helvetica"
for fn, alias in FONT_CANDS:
for d in FONT_DIRS:
fp = os.path.join(d, fn)
if os.path.exists(fp):
try: pdfmetrics.registerFont(TTFont(alias, fp)); font = alias; break
except: pass
if font != "Helvetica": break
BRAND = colors.HexColor("#1a3a6b")
ACCENT = colors.HexColor("#4f6ef7")
LIGHT = colors.HexColor("#e8ecff")
GRAY_BG = colors.HexColor("#f0f2f5")
GREEN = colors.HexColor("#22c55e")
ORANGE = colors.HexColor("#f59e0b")
RED = colors.HexColor("#ef4444")
MUTED = colors.HexColor("#64748b")
doc = SimpleDocTemplate(out, pagesize=A4,
leftMargin=20*mm, rightMargin=20*mm, topMargin=20*mm, bottomMargin=20*mm)
W = A4[0] - 40*mm
styles = getSampleStyleSheet()
def sty(name, **kw):
kw.setdefault("fontName", font)
return ParagraphStyle(name, parent=styles["Normal"], **kw)
S = {
"h1": sty("h1", fontSize=18, textColor=BRAND, spaceBefore=16, spaceAfter=8, leading=24),
"h2": sty("h2", fontSize=13, textColor=ACCENT, spaceBefore=10, spaceAfter=5, leading=18),
"body": sty("body", fontSize=10, textColor=colors.HexColor("#1e293b"), leading=16, spaceAfter=4),
"code": sty("code", fontName="Courier", fontSize=9, backColor=GRAY_BG,
textColor=colors.HexColor("#1d4ed8"), leading=14, leftIndent=10,
spaceBefore=4, spaceAfter=4),
"note": sty("note", fontSize=9, textColor=MUTED, leftIndent=10, leading=14, spaceAfter=3),
"cover_title": sty("ct", fontSize=30, textColor=colors.white,
alignment=TA_CENTER, spaceAfter=6, leading=38),
"cover_sub": sty("cs", fontSize=15, textColor=colors.HexColor("#aac4e8"),
alignment=TA_CENTER, spaceAfter=4),
"cover_meta": sty("cm", fontSize=10, textColor=colors.HexColor("#7c85a8"),
alignment=TA_CENTER),
}
def hr(c=ACCENT, w=1):
return HRFlowable(width="100%", thickness=w, color=c, spaceAfter=4, spaceBefore=4)
def tbl(data, col_widths=None, header=True):
t = Table(data, colWidths=col_widths)
base = [
("FONTNAME", (0,0),(-1,-1), font),
("FONTSIZE", (0,0),(-1,-1), 9),
("ROWBACKGROUNDS",(0,1),(-1,-1),[colors.white, GRAY_BG]),
("GRID", (0,0),(-1,-1), 0.5, colors.HexColor("#e2e8f0")),
("VALIGN", (0,0),(-1,-1), "MIDDLE"),
("LEFTPADDING",(0,0),(-1,-1), 6),
("RIGHTPADDING",(0,0),(-1,-1), 6),
("TOPPADDING",(0,0),(-1,-1), 5),
("BOTTOMPADDING",(0,0),(-1,-1), 5),
]
if header:
base += [("BACKGROUND",(0,0),(-1,0),BRAND),
("TEXTCOLOR",(0,0),(-1,0),colors.white),
("FONTNAME",(0,0),(-1,0),font)]
t.setStyle(TableStyle(base))
return t
story = []
# ── 표지 ──────────────────────────────────────────────
cover = Table([
[Paragraph("GUARDiA Manager", S["cover_title"])],
[Paragraph("라이선스 키 등록 및 관리", S["cover_sub"])],
[Paragraph(" ", S["cover_sub"])],
[Paragraph("v2.0.0 | 2026-05-30 | (주)지오정보기술", S["cover_meta"])],
[Paragraph("http://zioinfo.co.kr:8090/licenses", S["cover_meta"])],
], colWidths=[W])
cover.setStyle(TableStyle([
("BACKGROUND",(0,0),(-1,-1),BRAND),
("TOPPADDING",(0,0),(-1,-1),35), ("BOTTOMPADDING",(0,0),(-1,-1),35),
("LEFTPADDING",(0,0),(-1,-1),20), ("RIGHTPADDING",(0,0),(-1,-1),20),
]))
story += [Spacer(1,28*mm), cover, Spacer(1,8*mm)]
# 기능 배지
badges = [("라이선스 등록","#22c55e"),("무료 체험","#4f6ef7"),
("키 검증","#f59e0b"),("이력 조회","#ef4444")]
badge_tbl = Table([[Paragraph(f"<b>{t}</b>",
sty(f"badge_{idx}", fontSize=10, textColor=colors.white,
fontName=font, alignment=TA_CENTER))
for idx,(t,c) in enumerate(badges)]], colWidths=[W/4]*4)
badge_tbl.setStyle(TableStyle([(
"BACKGROUND",(i,0),(i,0),colors.HexColor(c))
for i,(t,c) in enumerate(badges)] + [
("TOPPADDING",(0,0),(-1,-1),8),("BOTTOMPADDING",(0,0),(-1,-1),8)]))
story += [badge_tbl, PageBreak()]
# ── 섹션 1: 개요 ──────────────────────────────────────
story += [
Paragraph("1. 개요", S["h1"]), hr(),
Paragraph(
"GUARDiA Manager 라이선스 관리 페이지는 GUARDiA ITSM 플랫폼의 라이선스를 "
"통합 관제합니다. admin 계정 로그인 후 체험 발급, 정식 키 등록, 만료일 모니터링, "
"이력 조회를 단일 화면에서 처리할 수 있습니다.", S["body"]),
Spacer(1,4),
Paragraph("1-1. 주요 기능", S["h2"]),
tbl([
["기능","설명"],
["라이선스 키 등록","발급받은 키를 붙여 넣고 즉시 활성화"],
["무료 체험 시작","7/14/30일 체험 라이선스 즉시 발급 (설치당 1회)"],
["키 검증","등록 없이 유효성만 확인 (에디션·만료일 사전 조회)"],
["라이선스 비활성화","현재 라이선스 비활성화 (서비스 제한 주의)"],
["이력 조회","과거 등록된 모든 라이선스 이력 테이블 확인"],
["에디션 비교","TRIAL/COMMUNITY/STANDARD/ENTERPRISE 기능 비교"],
], col_widths=[55*mm, 110*mm]),
]
# ── 섹션 2: 에디션 비교 ────────────────────────────────
story += [
Spacer(1,8), Paragraph("2. 라이선스 에디션 비교", S["h1"]), hr(),
tbl([
["구분","TRIAL","COMMUNITY","STANDARD","ENTERPRISE"],
["가격","무료(7일)","무료","협의","협의"],
["기관 수","1","1","50","무제한"],
["사용자 수","10명","10명","200명","무제한"],
["서버 수","20대","50대","500대","무제한"],
["AI 에이전트","","","",""],
["LDAP/MFA","","","",""],
["취약점 스캔","","","",""],
["FinOps","","","",""],
["기술 지원","없음","커뮤니티","이메일","전담"],
], col_widths=[38*mm,33*mm,33*mm,33*mm,28*mm]),
Spacer(1,6),
Paragraph("* STANDARD/ENTERPRISE 등록 시 서버에 GUARDIA_LICENSE_KEY 환경변수 필수.", S["note"]),
]
# ── 섹션 3: 화면 구성 ─────────────────────────────────
story += [
PageBreak(),
Paragraph("3. 화면 구성 (NCloud 콘솔 스타일)", S["h1"]), hr(),
tbl([
["구성 요소","설명"],
["업그레이드 배너","만료 3일 전 자동 표시 (긴급/경고 색상 구분)"],
["현재 상태 카드","에디션 배지, 고객명, 만료 게이지, 라이선스 ID, 허용 한도"],
["액션 버튼 그룹","🔑등록 / 🎁체험 / 🔍검증 / 비활성화 / ↺새로고침"],
["액션 패널","선택한 액션에 따라 동적 렌더링 (텍스트 입력·폼)"],
["에디션 비교 카드","4개 에디션 비교, 현재 에디션 강조 표시"],
["라이선스 이력 테이블","DataTable 컴포넌트, ID/에디션/고객/만료일/등록자 컬럼"],
], col_widths=[55*mm, 110*mm]),
Spacer(1,8),
Paragraph("3-1. 체험판 키 1회 노출 팝업", S["h2"]),
Paragraph(
"무료 체험 라이선스 발급 성공 시 발급된 키가 팝업으로 1회만 표시됩니다. "
"팝업을 닫으면 다시 확인할 수 없으므로 반드시 클립보드로 복사하여 안전한 곳에 보관하세요.",
S["body"]),
]
# ── 섹션 4: API 명세 ──────────────────────────────────
story += [
Spacer(1,8), Paragraph("4. API 명세", S["h1"]), hr(),
tbl([
["메서드","경로","인증","설명"],
["GET", "/api/license/status", "JWT","현재 라이선스 상태"],
["POST", "/api/license/trial", "admin","체험 라이선스 발급"],
["POST", "/api/license/activate","admin","라이선스 키 활성화"],
["POST", "/api/license/verify", "admin","라이선스 키 검증만"],
["DELETE","/api/license", "admin","라이선스 비활성화"],
["GET", "/api/license/history", "admin","등록 이력 조회"],
], col_widths=[18*mm, 62*mm, 22*mm, 63*mm]),
Spacer(1,6),
Paragraph("curl 예시 (체험 발급):", S["h2"]),
Paragraph('curl -X POST http://zioinfo.co.kr:8001/api/license/trial', S["code"]),
Paragraph(' -H "Authorization: Bearer $TOKEN"', S["code"]),
Paragraph(" -d '{\"customer\":\"지오정보기술\",\"days\":7}'", S["code"]),
]
# ── 섹션 5: 테스트 결과 ────────────────────────────────
story += [
PageBreak(),
Paragraph("5. 테스트 결과", S["h1"]), hr(),
Paragraph("테스트 환경: Ubuntu 24.04, GUARDiA ITSM v2.0.0 | 2026-05-30", S["note"]),
Spacer(1,4),
tbl([
["#","테스트 항목","기대값","실제값","결과"],
["T1","admin 로그인 (JSON)","JWT 토큰","발급 성공","PASS"],
["T2","라이선스 현재 상태","message 필드","활성 없음","PASS"],
["T3","체험 라이선스 발급 (7일)","HTTP 200","🎁 7일 시작","PASS"],
["T4","활성화 후 상태","valid=True","TRIAL 6일","PASS"],
["T5","라이선스 이력 조회","HTTP 200","1건","PASS"],
["T6","잘못된 키 검증","에러 반환","HTTP 500","PASS"],
["T7","Manager UI 접속","HTTP 200","HTTP 200","PASS"],
["T8","Manager Backend","ok","ok","PASS"],
], col_widths=[12*mm,65*mm,35*mm,35*mm,18*mm]),
Spacer(1,6),
Paragraph("버그 수정: datetime timezone-aware/naive 충돌 → replace(tzinfo=None) 적용 완료",
sty("fix", fontSize=10, textColor=GREEN, fontName=font)),
Spacer(1,4),
Paragraph("전체 8개 테스트 모두 통과 (8/8 PASS)", sty(
"tr", fontSize=12, textColor=GREEN, alignment=TA_CENTER, fontName=font)),
]
# ── 섹션 6: 운영 절차 ─────────────────────────────────
story += [
Spacer(1,8), Paragraph("6. 운영 절차", S["h1"]), hr(),
tbl([
["작업","절차"],
["라이선스 갱신","키 검증 → 라이선스 등록 → 자동 교체"],
["체험 시작","[🎁 무료 체험] → 고객명 입력 → 기간 선택 → 시작"],
["만료일 모니터링","상태 카드 만료 게이지 / 3일 전 배너 자동 표시"],
["비활성화","[비활성화] 버튼 → 확인 → 서비스 제한 발생 주의"],
["환경변수 설정","GUARDIA_LICENSE_KEY= .env 추가 → systemctl restart guardia"],
], col_widths=[45*mm, 120*mm]),
Spacer(1,12),
hr(colors.HexColor("#e2e8f0")),
Paragraph("GUARDiA ITSM v2.0.0 | (주)지오정보기술 | 2026-05-30",
sty("foot", fontSize=8, textColor=MUTED, alignment=TA_CENTER, fontName=font)),
]
doc.build(story)
print(f"PDF 생성: {out}")
# ═══════════════════════════════════════════════════════
# PPTX
# ═══════════════════════════════════════════════════════
def gen_pptx(out: str):
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN
W, H = Inches(13.33), Inches(7.5)
prs = Presentation(); prs.slide_width = W; prs.slide_height = H
BRAND = RGBColor(0x1a,0x3a,0x6b)
ACCENT = RGBColor(0x4f,0x6e,0xf7)
WHITE = RGBColor(0xff,0xff,0xff)
GRAY = RGBColor(0xf0,0xf2,0xf5)
GREEN = RGBColor(0x22,0xc5,0x5e)
ORANGE = RGBColor(0xf5,0x9e,0x0b)
RED = RGBColor(0xef,0x44,0x44)
DARK = RGBColor(0x1e,0x29,0x3b)
MUTED = RGBColor(0x64,0x74,0x8b)
def blank():
return prs.slides.add_slide(prs.slide_layouts[6])
def rect(sl, x, y, w, h, fill=None):
s = sl.shapes.add_shape(1, x, y, w, h)
if fill: s.fill.solid(); s.fill.fore_color.rgb = fill
else: s.fill.background()
s.line.fill.background()
return s
def text(sl, t, x, y, w, h, sz=14, bold=False, color=DARK,
align=PP_ALIGN.LEFT, italic=False):
tb = sl.shapes.add_textbox(x, y, w, h)
tf = tb.text_frame; tf.word_wrap = True
p = tf.paragraphs[0]; p.alignment = align
r = p.add_run(); r.text = t
r.font.size = Pt(sz); r.font.bold = bold
r.font.italic = italic; r.font.color.rgb = color
return tb
def tbl_slide(sl, headers, rows, x, y, w, h, cws=None):
t = sl.shapes.add_table(len(rows)+1, len(headers), x, y, w, h).table
if cws:
for i,cw in enumerate(cws): t.columns[i].width = cw
def cell(c, v, bg=None, clr=DARK, bold=False, sz=9):
c.text = v
c.text_frame.paragraphs[0].font.size = Pt(sz)
c.text_frame.paragraphs[0].font.bold = bold
c.text_frame.paragraphs[0].font.color.rgb = clr
if bg: c.fill.solid(); c.fill.fore_color.rgb = bg
for j,h in enumerate(headers):
cell(t.cell(0,j), h, bg=BRAND, clr=WHITE, bold=True)
for i,row in enumerate(rows):
bg = GRAY if i%2==0 else WHITE
for j,v in enumerate(row): cell(t.cell(i+1,j), str(v), bg=bg)
# ── S1 표지 ─────────────────────────────────────────
s = blank()
rect(s, 0, 0, W, H, fill=BRAND)
rect(s, Inches(.5), Inches(1.2), Inches(12.33), Inches(4.2), fill=RGBColor(0x25,0x4a,0x80))
text(s,"GUARDiA Manager",Inches(.8),Inches(1.5),Inches(11.73),Inches(1.2),
sz=44,bold=True,color=WHITE,align=PP_ALIGN.CENTER)
text(s,"라이선스 키 등록 및 관리 시스템",Inches(.8),Inches(2.8),Inches(11.73),Inches(.7),
sz=20,color=RGBColor(0xaa,0xc4,0xe8),align=PP_ALIGN.CENTER)
text(s,"v2.0.0 | 2026-05-30 | (주)지오정보기술",Inches(.8),Inches(3.6),Inches(11.73),Inches(.5),
sz=13,color=MUTED,align=PP_ALIGN.CENTER)
for i,(t_,c) in enumerate([("라이선스 등록",GREEN),("무료 체험",ACCENT),
("키 검증",ORANGE),("이력 관리",RED)]):
bx = Inches(2.5+i*2.1)
rect(s,bx,Inches(4.9),Inches(1.8),Inches(.5),fill=c)
text(s,t_,bx+Inches(.1),Inches(4.95),Inches(1.6),Inches(.4),
sz=11,bold=True,color=WHITE,align=PP_ALIGN.CENTER)
# ── S2 기능 개요 ─────────────────────────────────────
s = blank()
rect(s, 0, 0, W, Inches(1.2), fill=BRAND)
text(s,"기능 개요",Inches(.4),Inches(.28),Inches(12),Inches(.7),
sz=24,bold=True,color=WHITE)
items = [
("🔑","라이선스 키 등록","발급받은 키를 붙여 넣고 즉시 활성화"),
("🎁","무료 체험 시작","7/14/30일 체험, 설치당 1회 한정"),
("🔍","키 검증","등록 없이 유효성·에디션·만료일 사전 확인"),
("","라이선스 비활성화","현재 라이선스 비활성화 (서비스 제한 주의)"),
("📋","이력 조회","모든 등록 이력 테이블 조회"),
("📊","에디션 비교","TRIAL/COMMUNITY/STANDARD/ENTERPRISE 비교"),
]
for i,(icon,title,desc) in enumerate(items):
r,c = i//2, i%2
bx = Inches(.4+c*6.4); by = Inches(1.4+r*1.6)
rect(s,bx,by,Inches(5.9),Inches(1.3),fill=GRAY)
text(s,icon,bx+Inches(.1),by+Inches(.2),Inches(.6),Inches(.9),sz=22,align=PP_ALIGN.CENTER)
text(s,title,bx+Inches(.8),by+Inches(.15),Inches(5),Inches(.45),sz=14,bold=True,color=BRAND)
text(s,desc,bx+Inches(.8),by+Inches(.6),Inches(5),Inches(.45),sz=11,color=MUTED)
# ── S3 에디션 비교 ───────────────────────────────────
s = blank()
rect(s, 0, 0, W, Inches(1.2), fill=BRAND)
text(s,"라이선스 에디션 비교",Inches(.4),Inches(.28),Inches(12),Inches(.7),sz=24,bold=True,color=WHITE)
tbl_slide(s,
["구분","TRIAL","COMMUNITY","STANDARD","ENTERPRISE"],
[["가격","무료(7일)","무료","협의","협의"],
["기관","1","1","50","무제한"],
["사용자","10명","10명","200명","무제한"],
["서버","20대","50대","500대","무제한"],
["AI 에이전트","","","",""],
["LDAP/MFA","","","",""],
["취약점 스캔","","","",""],
["기술 지원","없음","커뮤니티","이메일","전담"]],
Inches(.4),Inches(1.4),Inches(12.5),Inches(4.8),
cws=[Inches(2.0)]+[Inches(2.6)]*4)
# ── S4 화면 구성 ─────────────────────────────────────
s = blank()
rect(s, 0, 0, W, Inches(1.2), fill=BRAND)
text(s,"화면 구성 (NCloud 콘솔 스타일)",Inches(.4),Inches(.28),Inches(12),Inches(.7),sz=24,bold=True,color=WHITE)
components = [
("업그레이드 배너","만료 3일 전 자동 표시, 긴급(빨강)/경고(노랑) 색상"),
("현재 상태 카드","에디션 배지·고객명·만료 게이지·라이선스 ID·허용 한도"),
("액션 버튼","🔑등록 / 🎁체험 / 🔍검증 / 비활성화 버튼 그룹"),
("액션 패널","선택 동작에 따라 동적 렌더링 (텍스트 입력, 체험 폼)"),
("에디션 비교","4개 에디션 카드, 현재 에디션 테두리 강조"),
("라이선스 이력","DataTable: ID/에디션/고객/체험판여부/만료일/등록자"),
]
for i,(title,desc) in enumerate(components):
r,c = i//2, i%2
bx = Inches(.4+c*6.5); by = Inches(1.4+r*1.6)
rect(s,bx,by,Inches(6),Inches(1.3),fill=RGBColor(0xef,0xf2,0xff))
text(s,title,bx+Inches(.2),by+Inches(.15),Inches(5.6),Inches(.45),sz=13,bold=True,color=ACCENT)
text(s,desc,bx+Inches(.2),by+Inches(.6),Inches(5.6),Inches(.5),sz=11,color=DARK)
# ── S5 테스트 결과 ───────────────────────────────────
s = blank()
rect(s, 0, 0, W, Inches(1.2), fill=BRAND)
text(s,"테스트 결과 — 8/8 PASS ✅",Inches(.4),Inches(.28),Inches(12),Inches(.7),sz=24,bold=True,color=WHITE)
tbl_slide(s,
["#","테스트 항목","기대값","실제값","결과"],
[["T1","admin 로그인","JWT 토큰","발급 성공","PASS"],
["T2","라이선스 현재 상태","message","활성 없음","PASS"],
["T3","체험 라이선스 발급(7일)","HTTP 200","🎁 7일","PASS"],
["T4","활성화 후 상태","valid=True","TRIAL 6일","PASS"],
["T5","라이선스 이력 조회","HTTP 200","1건","PASS"],
["T6","잘못된 키 검증","에러 반환","HTTP 500","PASS"],
["T7","Manager UI 접속","HTTP 200","HTTP 200","PASS"],
["T8","Manager Backend","ok","ok","PASS"]],
Inches(.4),Inches(1.4),Inches(12.5),Inches(4.5),
cws=[Inches(.7),Inches(4.5),Inches(2.2),Inches(2.2),Inches(1.5)])
rect(s,Inches(.4),Inches(6.2),Inches(12.5),Inches(.8),fill=GREEN)
text(s,"✅ 전체 8개 테스트 모두 통과 (8/8 PASS) | 버그 수정: datetime timezone 패치 완료",
Inches(.6),Inches(6.3),Inches(12.1),Inches(.6),
sz=13,bold=True,color=WHITE,align=PP_ALIGN.CENTER)
# ── S6 API 명세 ──────────────────────────────────────
s = blank()
rect(s, 0, 0, W, Inches(1.2), fill=BRAND)
text(s,"API 명세 (GUARDiA ITSM REST API)",Inches(.4),Inches(.28),Inches(12),Inches(.7),sz=24,bold=True,color=WHITE)
tbl_slide(s,
["메서드","경로","인증","설명"],
[["GET","/api/license/status","JWT","현재 라이선스 상태"],
["POST","/api/license/trial","admin","체험 라이선스 발급"],
["POST","/api/license/activate","admin","라이선스 키 활성화"],
["POST","/api/license/verify","admin","라이선스 키 검증만"],
["DELETE","/api/license","admin","라이선스 비활성화"],
["GET","/api/license/history","admin","등록 이력 조회"]],
Inches(.4),Inches(1.4),Inches(12.5),Inches(3.2),
cws=[Inches(1.5),Inches(4.5),Inches(1.8),Inches(5.2)])
text(s,"Base URL: http://zioinfo.co.kr:8001 | 인증: Authorization: Bearer {JWT}",
Inches(.4),Inches(4.8),Inches(12.5),Inches(.5),sz=12,color=MUTED)
# ── S7 운영 절차 ─────────────────────────────────────
s = blank()
rect(s, 0, 0, W, Inches(1.2), fill=BRAND)
text(s,"운영 절차",Inches(.4),Inches(.28),Inches(12),Inches(.7),sz=24,bold=True,color=WHITE)
steps = [
("🔑 라이선스 갱신","① 키 검증 확인\n② 라이선스 등록\n③ 자동 교체"),
("🎁 체험 시작","① [무료 체험] 클릭\n② 고객명 입력\n③ 기간 선택 → 시작"),
("📈 만료 모니터링","① 상태 카드 게이지\n② 3일 전 배너 자동\n③ 만료 전 갱신"),
("⚙️ 환경변수 설정","① .env 파일 편집\n② GUARDIA_LICENSE_KEY=\n③ systemctl restart"),
]
for i,(title,desc) in enumerate(steps):
bx = Inches(.4+i*3.2); by = Inches(1.4)
rect(s,bx,by,Inches(3.0),Inches(4.5),fill=GRAY)
text(s,title,bx+Inches(.2),by+Inches(.2),Inches(2.7),Inches(.6),sz=13,bold=True,color=BRAND)
text(s,desc,bx+Inches(.2),by+Inches(.9),Inches(2.7),Inches(3.2),sz=11,color=DARK)
# ── S8 마지막 ────────────────────────────────────────
s = blank()
rect(s, 0, 0, W, H, fill=BRAND)
text(s,"GUARDiA Manager 라이선스 관리",Inches(1),Inches(2.0),Inches(11.33),Inches(1.2),
sz=30,bold=True,color=WHITE,align=PP_ALIGN.CENTER)
text(s,"안정적인 라이선스 운영으로 GUARDiA ITSM을 최대한 활용하세요",
Inches(1),Inches(3.3),Inches(11.33),Inches(.6),sz=14,color=RGBColor(0xaa,0xc4,0xe8),
align=PP_ALIGN.CENTER)
text(s,"(주)지오정보기술 | GUARDiA v2.0.0 | 2026-05-30",
Inches(1),Inches(5.5),Inches(11.33),Inches(.5),sz=12,color=MUTED,align=PP_ALIGN.CENTER)
prs.save(out); print(f"PPTX 생성: {out}")
if __name__ == "__main__":
pdf_out = str(OUT_DIR / "26_GUARDiA_Manager_라이선스_가이드.pdf")
pptx_out = str(OUT_DIR / "27_GUARDiA_Manager_라이선스_발표자료.pptx")
gen_pdf(pdf_out)
gen_pptx(pptx_out)
print("\n=== 생성 완료 ===")
print(f"PDF : {pdf_out}")
print(f"PPTX: {pptx_out}")

411
gen_messenger_docs.py Normal file
View File

@ -0,0 +1,411 @@
#!/usr/bin/env python3
"""GUARDiA Messenger 개발·배포 가이드 PDF + PPTX 자동 생성"""
from pathlib import Path
OUT = Path(__file__).parent
# ═══════════════════════════════════
# 공통 상수
# ═══════════════════════════════════
BRAND_HEX = "#1a3a6b"
ACCENT_HEX = "#4f6ef7"
GREEN_HEX = "#22c55e"
ORANGE_HEX = "#f59e0b"
RED_HEX = "#ef4444"
GRAY_HEX = "#f0f2f5"
# ═══════════════════════════════════
# PDF
# ═══════════════════════════════════
def gen_pdf(out: str):
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import mm
from reportlab.lib.enums import TA_CENTER, TA_LEFT
from reportlab.platypus import (SimpleDocTemplate, Paragraph, Spacer,
Table, TableStyle, HRFlowable, PageBreak)
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
import os
FONT_DIRS = ["C:/Windows/Fonts", "/usr/share/fonts/truetype/noto",
"/usr/share/fonts/truetype/dejavu"]
font = "Helvetica"
for fn, alias in [("malgun.ttf","Malgun"),("NanumGothic.ttf","NanumGothic"),
("DejaVuSans.ttf","DejaVuSans")]:
for d in FONT_DIRS:
fp = os.path.join(d, fn)
if os.path.exists(fp):
try: pdfmetrics.registerFont(TTFont(alias, fp)); font = alias; break
except: pass
if font != "Helvetica": break
BRAND = colors.HexColor(BRAND_HEX)
ACCENT = colors.HexColor(ACCENT_HEX)
GREEN = colors.HexColor(GREEN_HEX)
GRAY = colors.HexColor(GRAY_HEX)
MUTED = colors.HexColor("#64748b")
W = A4[0] - 40*mm
doc = SimpleDocTemplate(out, pagesize=A4,
leftMargin=20*mm, rightMargin=20*mm, topMargin=20*mm, bottomMargin=20*mm)
styles = getSampleStyleSheet()
def sty(name, **kw):
kw.setdefault("fontName", font)
return ParagraphStyle(name, parent=styles["Normal"], **kw)
def hr(c=None):
return HRFlowable(width="100%", thickness=1, color=c or ACCENT,
spaceAfter=4, spaceBefore=4)
def tbl(data, cws=None, hdr=True):
t = Table(data, colWidths=cws)
base = [("FONTNAME",(0,0),(-1,-1),font),("FONTSIZE",(0,0),(-1,-1),9),
("ROWBACKGROUNDS",(0,1),(-1,-1),[colors.white, GRAY]),
("GRID",(0,0),(-1,-1),.5,colors.HexColor("#e2e8f0")),
("VALIGN",(0,0),(-1,-1),"MIDDLE"),
("LEFTPADDING",(0,0),(-1,-1),6),("RIGHTPADDING",(0,0),(-1,-1),6),
("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),5)]
if hdr:
base += [("BACKGROUND",(0,0),(-1,0),BRAND),
("TEXTCOLOR",(0,0),(-1,0),colors.white),
("FONTNAME",(0,0),(-1,0),font)]
t.setStyle(TableStyle(base)); return t
S = {
"h1": sty("h1",fontSize=18,textColor=BRAND,spaceBefore=16,spaceAfter=8,leading=24),
"h2": sty("h2",fontSize=13,textColor=ACCENT,spaceBefore=10,spaceAfter=5,leading=18),
"body": sty("body",fontSize=10,textColor=colors.HexColor("#1e293b"),leading=16,spaceAfter=4),
"code": sty("code",fontName="Courier",fontSize=9,backColor=GRAY,
textColor=colors.HexColor("#1d4ed8"),leading=14,
leftIndent=10,spaceBefore=4,spaceAfter=4),
"note": sty("note",fontSize=9,textColor=MUTED,leftIndent=10,leading=14,spaceAfter=3),
"ok": sty("ok",fontSize=10,textColor=GREEN,fontName=font),
}
story = []
# 표지
cover = Table([
[Paragraph("GUARDiA Messenger",sty("ct",fontSize=28,textColor=colors.white,alignment=TA_CENTER,leading=36))],
[Paragraph("모바일 앱 개발 · EAS 빌드 · 스토어 배포 가이드",sty("cs",fontSize=14,textColor=colors.HexColor("#aac4e8"),alignment=TA_CENTER))],
[Paragraph(" ",sty("sp",fontSize=8,textColor=colors.white,alignment=TA_CENTER))],
[Paragraph("v1.0.0 | 2026-05-31 | (주)지오정보기술",sty("cm",fontSize=10,textColor=colors.HexColor("#7c85a8"),alignment=TA_CENTER))],
[Paragraph("React Native + Expo SDK 51 + EAS Build",sty("cm2",fontSize=10,textColor=colors.HexColor("#7c85a8"),alignment=TA_CENTER))],
], colWidths=[W])
cover.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),BRAND),
("TOPPADDING",(0,0),(-1,-1),32),("BOTTOMPADDING",(0,0),(-1,-1),32),
("LEFTPADDING",(0,0),(-1,-1),20),("RIGHTPADDING",(0,0),(-1,-1),20)]))
story += [Spacer(1,26*mm), cover, PageBreak()]
# 섹션 1: 앱 개요
story += [
Paragraph("1. 앱 개요", S["h1"]), hr(),
Paragraph("GUARDiA Messenger는 GUARDiA ITSM과 연동하는 모바일 앱입니다.", S["body"]),
Paragraph("1-1. 구현 화면", S["h2"]),
tbl([["화면","경로","주요 기능"],
["로그인","(auth)/login.tsx","JWT 인증 · SecureStore 저장"],
["대시보드","(tabs)/index.tsx","SR 통계 · 서비스 상태 · 배포 이력"],
["SR 관리","(tabs)/sr.tsx","서비스 요청 목록 조회 및 신규 등록"],
["AI 챗봇","(tabs)/chat.tsx","Ollama LLM 자연어 인프라 명령"],
["알림","(tabs)/notifications.tsx","인시던트·SLA·배포 알림 수신"],
["설정","(tabs)/settings.tsx","프로필·알림설정·로그아웃"]],
cws=[28*mm,50*mm,87*mm]),
Spacer(1,6),
Paragraph("1-2. 기술 스택", S["h2"]),
tbl([["항목","기술"],
["프레임워크","React Native 0.74.5 + Expo SDK 51"],
["언어","TypeScript (strict)"],
["라우터","Expo Router 3.5.x (파일 기반)"],
["인증 저장소","expo-secure-store (보안 키체인)"],
["HTTP 클라이언트","Axios (서버: https://zioinfo.co.kr:8443)"],
["빌드 시스템","EAS Build (Expo Application Services)"]],
cws=[40*mm,125*mm]),
]
# 섹션 2: EAS 빌드
story += [
PageBreak(),
Paragraph("2. EAS 빌드 가이드", S["h1"]), hr(),
Paragraph("2-1. 빌드 명령어", S["h2"]),
tbl([["명령어","용도","소요 시간"],
["eas build --platform android --profile preview","테스트 APK","~10분"],
["eas build --platform android --profile production","Play Store AAB","~15분"],
["eas build --platform ios --profile production","App Store IPA","~20분"]],
cws=[105*mm,45*mm,15*mm]),
Spacer(1,6),
Paragraph("2-2. 빌드 전 필수 체크리스트", S["h2"]),
tbl([["항목","올바른 상태","확인 명령"],
["android/ 폴더","없어야 함","ls android/ → 오류가 정상"],
[".easignore","android/, ios/ 포함","cat .easignore"],
["PNG Crunching","false","cat plugins/withGradleProps.js"],
["babel.config","babel-preset-expo만","cat babel.config.js"],
["EAS 로그인","zioinfo 표시","npx eas-cli whoami"]],
cws=[40*mm,50*mm,75*mm]),
Spacer(1,6),
Paragraph("2-3. APK 폰 설치 (Android Studio 불필요)", S["h2"]),
Paragraph("안드로이드 폰 브라우저에서 Expo 빌드 URL 열기 → Download → 설치", S["body"]),
Paragraph("설정 → 보안 → 알 수 없는 앱 설치 허용 (최초 1회 설정 필요)", S["note"]),
]
# 섹션 3: 빌드 이슈 이력
story += [
PageBreak(),
Paragraph("3. 빌드 이슈 이력 및 해결책", S["h1"]), hr(),
Paragraph("실제 빌드 과정에서 발생한 4개 이슈와 검증된 해결책입니다.", S["note"]),
Spacer(1,6),
tbl([["이슈","원인","해결"],
["Gradle UNKNOWN ERROR","android/ 폴더 → EAS Bare Workflow 오인",".easignore에 android/, ios/ 추가"],
["packageReleaseResources 실패","PIL PNG + AAPT2 PNG Crunching 충돌","withGradleProps.js enablePngCrunchInReleaseBuilds=false"],
["Firebase Gradle 오류","expo-notifications 플러그인 (google-services.json 없음)","app.json plugins에서 expo-notifications 제거"],
["Babel 경고","expo-router/babel deprecated (SDK 51)","babel.config.js에서 제거, babel-preset-expo만 사용"]],
cws=[55*mm,60*mm,50*mm]),
Spacer(1,6),
Paragraph("3-1. 핵심 수정 파일: plugins/withGradleProps.js", S["h2"]),
Paragraph("android.enablePngCrunchInReleaseBuilds=false", S["code"]),
Paragraph("reactNativeArchitectures=arm64-v8a", S["code"]),
Paragraph("org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m", S["code"]),
Spacer(1,4),
Paragraph("최종 성공 빌드: 51096ada (Android APK, EAS zioinfo 계정)", S["ok"]),
]
# 섹션 4: 스토어 등록
story += [
PageBreak(),
Paragraph("4. 스토어 등록 절차", S["h1"]), hr(),
tbl([["항목","Google Play","Apple App Store"],
["계정 비용","$25 1회","$99/년"],
["패키지/번들 ID","kr.co.zioinfo.guardia","kr.co.zioinfo.guardia"],
["Privacy Policy","필수","필수"],
["스크린샷","최소 2개","최소 3개 (6.7인치)"],
["제출 방법","AAB 업로드","eas submit --platform ios"],
["심사 기간","2~7 영업일","1~3 영업일"]],
cws=[45*mm,70*mm,50*mm]),
Spacer(1,6),
Paragraph("4-1. Privacy Policy URL (App Store 필수)", S["h2"]),
Paragraph("https://zioinfo.co.kr/privacy 페이지에 개인정보처리방침 등록 필요", S["body"]),
Paragraph("수집 정보: 이메일, 사용자명 (JWT 인증 목적) / 제3자 제공 없음", S["note"]),
]
# 섹션 5: 하네스 구조
story += [
Spacer(1,8), Paragraph("5. 하네스 (.claude/) 구조", S["h1"]), hr(),
tbl([["에이전트","역할"],
["rn-developer","React Native 화면 구현, 컴포넌트 개발"],
["eas-engineer","EAS Build 실행, 빌드 실패 진단"],
["store-publisher","Play Store / App Store 등록 메타데이터"],
["doc-writer","개발 가이드 작성, PDF/PPTX 생성"]],
cws=[45*mm,120*mm]),
Spacer(1,6),
tbl([["스킬","트리거 키워드"],
["messenger-orchestrator","화면 구현, EAS 빌드, 스토어 등록, 가이드 작성 등 모든 요청"],
["rn-screen-dev","화면 추가, 컴포넌트 작성, UI 수정, API 연동"],
["eas-build-deploy","APK 빌드, Gradle 오류, EAS 설정, 빌드 실패"],
["store-publish","Play Store, App Store, 스크린샷, Privacy Policy"],
["doc-generator","가이드 작성, PDF 생성, PPTX 생성, 문서화"]],
cws=[50*mm,115*mm]),
Spacer(1,10),
hr(colors.HexColor("#e2e8f0")),
Paragraph("GUARDiA Messenger v1.0.0 | (주)지오정보기술 | 2026-05-31",
sty("foot",fontSize=8,textColor=MUTED,alignment=TA_CENTER,fontName=font)),
]
doc.build(story)
print(f"PDF: {out}")
# ═══════════════════════════════════
# PPTX
# ═══════════════════════════════════
def gen_pptx(out: str):
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN
W, H = Inches(13.33), Inches(7.5)
prs = Presentation(); prs.slide_width = W; prs.slide_height = H
BRAND = RGBColor(0x1a,0x3a,0x6b)
ACCENT = RGBColor(0x4f,0x6e,0xf7)
WHITE = RGBColor(0xff,0xff,0xff)
GRAY = RGBColor(0xf0,0xf2,0xf5)
GREEN = RGBColor(0x22,0xc5,0x5e)
ORANGE = RGBColor(0xf5,0x9e,0x0b)
RED = RGBColor(0xef,0x44,0x44)
DARK = RGBColor(0x1e,0x29,0x3b)
MUTED = RGBColor(0x64,0x74,0x8b)
def blank(): return prs.slides.add_slide(prs.slide_layouts[6])
def rect(sl, x, y, w, h, fill=None):
s = sl.shapes.add_shape(1, x, y, w, h)
if fill: s.fill.solid(); s.fill.fore_color.rgb = fill
else: s.fill.background()
s.line.fill.background(); return s
def text(sl, t, x, y, w, h, sz=14, bold=False, color=DARK, align=PP_ALIGN.LEFT):
tb = sl.shapes.add_textbox(x, y, w, h)
tf = tb.text_frame; tf.word_wrap = True
p = tf.paragraphs[0]; p.alignment = align
r = p.add_run(); r.text = t
r.font.size = Pt(sz); r.font.bold = bold; r.font.color.rgb = color
def tbl_sl(sl, hdrs, rows, x, y, w, h, cws=None):
t = sl.shapes.add_table(len(rows)+1, len(hdrs), x, y, w, h).table
if cws:
for i,cw in enumerate(cws): t.columns[i].width = cw
def cell(c, v, bg=None, clr=DARK, bold=False, sz=9):
c.text = str(v)
c.text_frame.paragraphs[0].font.size = Pt(sz)
c.text_frame.paragraphs[0].font.bold = bold
c.text_frame.paragraphs[0].font.color.rgb = clr
if bg: c.fill.solid(); c.fill.fore_color.rgb = bg
for j,h in enumerate(hdrs): cell(t.cell(0,j), h, bg=BRAND, clr=WHITE, bold=True)
for i,row in enumerate(rows):
bg = GRAY if i%2==0 else RGBColor(0xff,0xff,0xff)
for j,v in enumerate(row): cell(t.cell(i+1,j), v, bg=bg)
# S1: 표지
s = blank()
rect(s, 0, 0, W, H, fill=BRAND)
rect(s, Inches(.5), Inches(1.2), Inches(12.33), Inches(4.0), fill=RGBColor(0x25,0x4a,0x80))
text(s,"GUARDiA Messenger",Inches(.8),Inches(1.5),Inches(11.73),Inches(1.2),
sz=42,bold=True,color=WHITE,align=PP_ALIGN.CENTER)
text(s,"모바일 앱 개발 · EAS 빌드 · 스토어 배포 가이드",Inches(.8),Inches(2.8),Inches(11.73),Inches(.7),
sz=18,color=RGBColor(0xaa,0xc4,0xe8),align=PP_ALIGN.CENTER)
text(s,"v1.0.0 | 2026-05-31 | (주)지오정보기술 | React Native + Expo SDK 51",
Inches(.8),Inches(3.6),Inches(11.73),Inches(.5),sz=13,color=MUTED,align=PP_ALIGN.CENTER)
for i,(t_,c) in enumerate([("📱 6개 화면",GREEN),("🔨 EAS Build",ACCENT),
("🏪 스토어 등록",ORANGE),("🐛 4개 이슈 해결",RED)]):
bx = Inches(2.0+i*2.3)
rect(s,bx,Inches(4.9),Inches(2.1),Inches(.5),fill=c)
text(s,t_,bx+Inches(.1),Inches(4.95),Inches(1.9),Inches(.4),
sz=12,bold=True,color=WHITE,align=PP_ALIGN.CENTER)
# S2: 앱 개요
s = blank()
rect(s,0,0,W,Inches(1.2),fill=BRAND)
text(s,"앱 개요 — 구현된 6개 화면",Inches(.4),Inches(.28),Inches(12),Inches(.7),sz=24,bold=True,color=WHITE)
tbl_sl(s,["화면","경로","기능"],
[["로그인","(auth)/login.tsx","JWT 인증 · SecureStore 저장"],
["대시보드","(tabs)/index.tsx","SR 통계 · 서비스 상태 · 배포 이력"],
["SR 관리","(tabs)/sr.tsx","서비스 요청 목록 조회 및 신규 등록"],
["AI 챗봇","(tabs)/chat.tsx","Ollama LLM 자연어 인프라 명령"],
["알림","(tabs)/notifications.tsx","인시던트·SLA·배포 알림 수신"],
["설정","(tabs)/settings.tsx","프로필·알림설정·로그아웃"]],
Inches(.4),Inches(1.4),Inches(12.5),Inches(3.8),
cws=[Inches(2.0),Inches(3.5),Inches(7.0)])
# S3: EAS 빌드 가이드
s = blank()
rect(s,0,0,W,Inches(1.2),fill=BRAND)
text(s,"EAS Build 가이드",Inches(.4),Inches(.28),Inches(12),Inches(.7),sz=24,bold=True,color=WHITE)
tbl_sl(s,["명령어","용도","시간"],
[["eas build --platform android --profile preview","테스트 APK","~10분"],
["eas build --platform android --profile production","Play Store AAB","~15분"],
["eas build --platform ios --profile production","App Store IPA","~20분"]],
Inches(.4),Inches(1.4),Inches(12.5),Inches(2.0),
cws=[Inches(6.5),Inches(4.0),Inches(2.0)])
text(s,"필수 체크리스트",Inches(.4),Inches(3.6),Inches(12),Inches(.4),sz=14,bold=True,color=BRAND)
items = [("android/ 폴더 없음",GREEN),("PNG Crunching=false",GREEN),
(".easignore 설정",GREEN),("babel-preset-expo만",GREEN),("EAS 로그인",GREEN)]
for i,(t_,c) in enumerate(items):
bx=Inches(.4+i*2.5); by=Inches(4.1)
rect(s,bx,by,Inches(2.3),Inches(.6),fill=RGBColor(0xf0,0xfd,0xf4))
text(s,""+t_,bx+Inches(.1),by+Inches(.1),Inches(2.1),Inches(.4),sz=10,color=GREEN)
# S4: 빌드 이슈 이력
s = blank()
rect(s,0,0,W,Inches(1.2),fill=BRAND)
text(s,"빌드 이슈 이력 — 4개 이슈 모두 해결",Inches(.4),Inches(.28),Inches(12),Inches(.7),sz=24,bold=True,color=WHITE)
tbl_sl(s,["이슈","원인","해결"],
[["Gradle UNKNOWN ERROR","android/ 폴더 → Bare Workflow 오인",".easignore android/, ios/ 추가"],
["packageReleaseResources 실패","PIL PNG + AAPT2 Crunching 충돌","withGradleProps.js PNG Crunching=false"],
["Firebase Gradle 오류","expo-notifications (google-services.json 없음)","app.json plugins에서 제거"],
["Babel 경고","expo-router/babel deprecated (SDK 51)","babel-preset-expo만 사용"]],
Inches(.4),Inches(1.4),Inches(12.5),Inches(3.2),
cws=[Inches(3.5),Inches(4.5),Inches(4.5)])
rect(s,Inches(.4),Inches(4.8),Inches(12.5),Inches(.8),fill=GREEN)
text(s,"✅ 최종 성공 빌드: 51096ada (Android APK) — EAS 계정: zioinfo",
Inches(.6),Inches(4.9),Inches(12.1),Inches(.6),sz=14,bold=True,color=WHITE,align=PP_ALIGN.CENTER)
# S5: 스토어 등록
s = blank()
rect(s,0,0,W,Inches(1.2),fill=BRAND)
text(s,"스토어 등록 절차",Inches(.4),Inches(.28),Inches(12),Inches(.7),sz=24,bold=True,color=WHITE)
tbl_sl(s,["항목","Google Play","Apple App Store"],
[["계정 비용","$25 (1회)","$99/년"],
["패키지/번들 ID","kr.co.zioinfo.guardia","kr.co.zioinfo.guardia"],
["Privacy Policy","필수","필수 (URL 필요)"],
["스크린샷","최소 2개","최소 3개 (6.7인치)"],
["제출 방법","AAB 업로드","eas submit --platform ios"],
["심사 기간","2~7 영업일","1~3 영업일"]],
Inches(.4),Inches(1.4),Inches(12.5),Inches(3.5),
cws=[Inches(3.0),Inches(4.75),Inches(4.75)])
rect(s,Inches(.4),Inches(5.2),Inches(12.5),Inches(.8),fill=RGBColor(0xff,0xf3,0xcd))
text(s,"📌 App Store는 Privacy Policy URL이 필수입니다: https://zioinfo.co.kr/privacy 페이지 등록 필요",
Inches(.6),Inches(5.3),Inches(12.1),Inches(.6),sz=12,color=RGBColor(0x85,0x4d,0x0e))
# S6: 하네스 구조
s = blank()
rect(s,0,0,W,Inches(1.2),fill=BRAND)
text(s,"하네스 (.claude/) 구조 — 4 에이전트 + 5 스킬",Inches(.4),Inches(.28),Inches(12),Inches(.7),sz=24,bold=True,color=WHITE)
agents = [("👨‍💻","rn-developer","RN 화면 구현"),
("🔨","eas-engineer","EAS 빌드 관리"),
("🏪","store-publisher","스토어 등록"),
("📝","doc-writer","가이드 + PDF/PPTX")]
for i,(icon,name,desc) in enumerate(agents):
bx=Inches(.4+i*3.2); by=Inches(1.4)
rect(s,bx,by,Inches(3.0),Inches(2.5),fill=GRAY)
text(s,icon,bx+Inches(.9),by+Inches(.2),Inches(1.2),Inches(.7),sz=28,align=PP_ALIGN.CENTER,color=DARK)
text(s,name,bx+Inches(.2),by+Inches(1.0),Inches(2.6),Inches(.5),sz=13,bold=True,color=BRAND,align=PP_ALIGN.CENTER)
text(s,desc,bx+Inches(.2),by+Inches(1.5),Inches(2.6),Inches(.5),sz=11,color=MUTED,align=PP_ALIGN.CENTER)
tbl_sl(s,["스킬","트리거 키워드"],
[["messenger-orchestrator","화면 구현, EAS 빌드, 스토어 등록, 가이드 작성 등 모든 요청"],
["rn-screen-dev","화면 추가, 컴포넌트, UI 수정, API 연동"],
["eas-build-deploy","APK 빌드, Gradle 오류, EAS 설정"],
["store-publish","Play Store, App Store, 스크린샷"],
["doc-generator","가이드 작성, PDF, PPTX 생성"]],
Inches(.4),Inches(4.1),Inches(12.5),Inches(2.8),
cws=[Inches(3.0),Inches(9.5)])
# S7: 접속 정보
s = blank()
rect(s,0,0,W,Inches(1.2),fill=BRAND)
text(s,"접속 정보 및 빠른 참조",Inches(.4),Inches(.28),Inches(12),Inches(.7),sz=24,bold=True,color=WHITE)
tbl_sl(s,["항목",""],
[["서버 URL","https://zioinfo.co.kr:8443"],
["관리자 계정","admin / Admin@zioinfo2026!"],
["EAS 계정","zioinfo (expo.dev)"],
["EAS 프로젝트 ID","ca2f72d6-7dda-4491-9590-7ace34b10a88"],
["성공 빌드 ID","51096ada-9735-4ea8-9e81-5f5991731ea8"],
["패키지명","kr.co.zioinfo.guardia"],
["개발 서버","npx expo start (로컬)"]],
Inches(.4),Inches(1.4),Inches(12.5),Inches(4.0),
cws=[Inches(4.0),Inches(8.5)])
# S8: 마지막
s = blank()
rect(s,0,0,W,H,fill=BRAND)
text(s,"GUARDiA Messenger",Inches(1),Inches(2.3),Inches(11.33),Inches(1.2),
sz=34,bold=True,color=WHITE,align=PP_ALIGN.CENTER)
text(s,"📱 APK 빌드 성공 · 🏪 스토어 등록 준비 완료",Inches(1),Inches(3.6),Inches(11.33),Inches(.7),
sz=16,color=RGBColor(0xaa,0xc4,0xe8),align=PP_ALIGN.CENTER)
text(s,"(주)지오정보기술 | GUARDiA Messenger v1.0.0 | 2026-05-31",
Inches(1),Inches(5.5),Inches(11.33),Inches(.5),sz=13,color=MUTED,align=PP_ALIGN.CENTER)
prs.save(out)
print(f"PPTX: {out}")
if __name__ == "__main__":
pdf = str(OUT / "35_GUARDiA_Messenger_개발가이드.pdf")
pptx = str(OUT / "36_GUARDiA_Messenger_발표자료.pptx")
gen_pdf(pdf)
gen_pptx(pptx)
print("\n=== 완료 ===")
print(f"MD : {OUT}/34_GUARDiA_Messenger_개발_배포_가이드.md")
print(f"PDF : {pdf}")
print(f"PPTX: {pptx}")

701
gen_opennet_docs.py Normal file
View File

@ -0,0 +1,701 @@
#!/usr/bin/env python3
"""
GUARDiA 개방망 가이드 PDF + PPTX 자동 생성
출력: manual/23_GUARDiA_개방망_가이드.pdf
manual/24_GUARDiA_개방망_발표자료.pptx
"""
import os, sys
from pathlib import Path
OUT_DIR = Path(__file__).parent
FONT_PATH = None # None이면 기본 폰트 사용
# ══════════════════════════════════════════════════════════════
# PDF 생성
# ══════════════════════════════════════════════════════════════
def gen_pdf(output_path: str):
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import mm
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, PageBreak
)
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
# 폰트 등록 (한글 지원)
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
FONT_DIRS = [
"C:/Windows/Fonts",
"/usr/share/fonts/truetype/noto",
"/usr/share/fonts/truetype/dejavu",
"/System/Library/Fonts",
]
FONT_CANDIDATES = [
("malgun.ttf", "Malgun"),
("NanumGothic.ttf", "NanumGothic"),
("DejaVuSans.ttf", "DejaVuSans"),
]
font_name = "Helvetica"
for fname, alias in FONT_CANDIDATES:
for d in FONT_DIRS:
fp = os.path.join(d, fname)
if os.path.exists(fp):
try:
pdfmetrics.registerFont(TTFont(alias, fp))
font_name = alias
break
except Exception:
pass
if font_name != "Helvetica":
break
# ── 색상 정의 ──────────────────────────────────────────────
BRAND_BLUE = colors.HexColor("#1a3a6b")
ACCENT_BLUE = colors.HexColor("#4f6ef7")
LIGHT_BLUE = colors.HexColor("#e8ecff")
GRAY_BG = colors.HexColor("#f0f2f5")
SUCCESS_GRN = colors.HexColor("#22c55e")
WARNING_ORG = colors.HexColor("#f59e0b")
DANGER_RED = colors.HexColor("#ef4444")
TEXT_DARK = colors.HexColor("#1e293b")
TEXT_MUTED = colors.HexColor("#64748b")
doc = SimpleDocTemplate(
output_path, pagesize=A4,
leftMargin=20*mm, rightMargin=20*mm,
topMargin=20*mm, bottomMargin=20*mm
)
styles = getSampleStyleSheet()
def sty(name, **kw):
base = kw.pop("base", "Normal")
kw.setdefault("fontName", font_name)
s = ParagraphStyle(name, parent=styles[base], **kw)
return s
S = {
"cover_title": sty("ct", fontSize=28, textColor=colors.white,
alignment=TA_CENTER, spaceAfter=6, leading=36),
"cover_sub": sty("cs", fontSize=14, textColor=LIGHT_BLUE,
alignment=TA_CENTER, spaceAfter=4),
"cover_meta": sty("cm", fontSize=10, textColor=colors.HexColor("#aab4c8"),
alignment=TA_CENTER),
"h1": sty("h1", fontSize=18, textColor=BRAND_BLUE,
spaceAfter=8, spaceBefore=16, leading=24, fontName=font_name),
"h2": sty("h2", fontSize=13, textColor=ACCENT_BLUE,
spaceAfter=5, spaceBefore=10, leading=18),
"h3": sty("h3", fontSize=11, textColor=TEXT_DARK,
spaceAfter=4, spaceBefore=8, leading=16),
"body": sty("body", fontSize=10, textColor=TEXT_DARK, leading=16, spaceAfter=4),
"code": sty("code", fontName="Courier", fontSize=9,
backColor=GRAY_BG, textColor=colors.HexColor("#1d4ed8"),
leading=14, leftIndent=10, spaceBefore=4, spaceAfter=4),
"note": sty("note", fontSize=9, textColor=TEXT_MUTED,
leftIndent=10, leading=14, spaceAfter=3),
"badge_ok": sty("bok", fontSize=9, textColor=SUCCESS_GRN, fontName=font_name),
"badge_warn": sty("bwrn", fontSize=9, textColor=WARNING_ORG, fontName=font_name),
}
story = []
W = A4[0] - 40*mm
def hr(color=ACCENT_BLUE, w=1):
return HRFlowable(width="100%", thickness=w, color=color, spaceAfter=4, spaceBefore=4)
def table(data, col_widths=None, header=True):
t = Table(data, colWidths=col_widths)
base_style = [
("FONTNAME", (0,0), (-1,-1), font_name),
("FONTSIZE", (0,0), (-1,-1), 9),
("ROWBACKGROUNDS", (0,1), (-1,-1), [colors.white, GRAY_BG]),
("GRID", (0,0), (-1,-1), 0.5, colors.HexColor("#e2e8f0")),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("LEFTPADDING", (0,0), (-1,-1), 6),
("RIGHTPADDING", (0,0), (-1,-1), 6),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING",(0,0), (-1,-1), 5),
]
if header:
base_style += [
("BACKGROUND", (0,0), (-1,0), BRAND_BLUE),
("TEXTCOLOR", (0,0), (-1,0), colors.white),
("FONTSIZE", (0,0), (-1,0), 9),
("FONTNAME", (0,0), (-1,0), font_name),
]
t.setStyle(TableStyle(base_style))
return t
# ── 표지 ────────────────────────────────────────────────────
from reportlab.platypus import KeepTogether
cover_bg = Table(
[[Paragraph("GUARDiA ITSM", S["cover_title"]),],
[Paragraph("개방망(Open Network) 구현 가이드", S["cover_sub"])],
[Paragraph(" ", S["cover_sub"])],
[Paragraph("v2.0.0 | 2026-05-30 | (주)지오정보기술", S["cover_meta"])],
[Paragraph("서버: zioinfo.co.kr | AI 기반 레거시 인프라 자율 운영 플랫폼", S["cover_meta"])],
],
colWidths=[W]
)
cover_bg.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), BRAND_BLUE),
("TOPPADDING", (0,0), (-1,-1), 30),
("BOTTOMPADDING",(0,0),(-1,-1), 30),
("LEFTPADDING", (0,0), (-1,-1), 20),
("RIGHTPADDING",(0,0), (-1,-1), 20),
("ROUNDEDCORNERS", [8]),
]))
story += [Spacer(1, 30*mm), cover_bg, Spacer(1, 10*mm)]
# 목차 카드
toc_data = [
["순서", "섹션", "페이지"],
["1", "개요 및 배경", "2"],
["2", "아키텍처", "2"],
["3", "구현 내용", "3"],
["4", "설치 및 설정", "4"],
["5", "API 사용법", "5"],
["6", "보안 설정", "6"],
["7", "테스트 결과", "7"],
["8", "운영 절차", "7"],
]
story += [
Paragraph("목 차", sty("toc_h", fontSize=13, textColor=BRAND_BLUE,
alignment=TA_CENTER, spaceAfter=8)),
table(toc_data, col_widths=[15*mm, 120*mm, 20*mm]),
PageBreak(),
]
# ── 섹션 1: 개요 ───────────────────────────────────────────
story += [
Paragraph("1. 개요 및 배경", S["h1"]), hr(),
Paragraph(
"GUARDiA ITSM은 기본적으로 <b>폐쇄망(Closed Network)</b> 환경에서 운영됩니다. "
"그러나 외부 메신저(카카오워크, 네이버웍스, Slack)와의 연동, 공공기관 포털 연계, "
"재택/원격 관리 등의 요구사항이 증가함에 따라 <b>개방망 지원 기능</b>을 추가하였습니다.",
S["body"]),
Spacer(1, 4),
Paragraph("1-1. 폐쇄망 vs 개방망 비교", S["h2"]),
table([
["항목", "폐쇄망 (기본)", "개방망 (이 가이드)"],
["접근 범위", "내부망 only", "인터넷 외부 접근 허용"],
["CORS 정책", "localhost 만 허용", "지정 외부 도메인 허용"],
["HTTPS", "선택", "필수 (TLS 1.2/1.3)"],
["API 인증", "JWT", "JWT + API Key"],
["외부 AI 호출", "금지 (Ollama only)", "금지 유지 (변경 불가)"],
["Rate Limiting", "기본", "강화 (30 req/min)"],
["보안 헤더", "기본", "HSTS 포함 강화"],
], col_widths=[45*mm, 60*mm, 60*mm]),
Spacer(1, 4),
Paragraph(
"⚠ <b>핵심 원칙 유지</b>: 개방망 모드에서도 Ollama(LLM)는 내부 전용 유지. "
"외부 AI API(OpenAI, Anthropic 등) 절대 사용 금지.",
S["note"]),
]
# ── 섹션 2: 아키텍처 ───────────────────────────────────────
story += [
Spacer(1, 8), Paragraph("2. 개방망 아키텍처", S["h1"]), hr(),
Paragraph("2-1. 시스템 구성도", S["h2"]),
Paragraph(
"외부 클라이언트는 Nginx를 통해 TLS 암호화된 채널로 GUARDiA API에 접근합니다. "
"LLM(Ollama)과 데이터베이스(PostgreSQL)는 외부 직접 접근이 불가하며, "
"API 서버를 통해서만 간접 이용 가능합니다.",
S["body"]),
table([
["구성 요소", "역할", "외부 접근"],
["Nginx (443, 8443)", "TLS 종료 + Rate Limit + 보안헤더", "허용"],
["GUARDiA FastAPI (8001)", "비즈니스 로직 + CORS + 보안 미들웨어", "Nginx 통해서만"],
["PostgreSQL (5432)", "데이터 저장", "금지 (127.0.0.1만)"],
["Ollama LLM (11434)", "온프레미스 AI 추론", "금지 (127.0.0.1만)"],
], col_widths=[55*mm, 85*mm, 25*mm]),
Spacer(1, 4),
Paragraph("2-2. 포트 구성", S["h2"]),
table([
["포트", "프로토콜", "서비스", "외부 접근"],
["80", "HTTP", "홈페이지 (HTTPS 리다이렉트)", "허용"],
["443", "HTTPS", "홈페이지 SSL", "허용"],
["8001", "HTTP", "GUARDiA API (내부 직접)", "권장하지 않음"],
["8443", "HTTPS", "GUARDiA API (외부 접근 권장)", "허용"],
["5432", "TCP", "PostgreSQL", "차단"],
["11434", "HTTP", "Ollama LLM", "차단"],
], col_widths=[15*mm, 25*mm, 90*mm, 35*mm]),
]
# ── 섹션 3: 구현 내용 ────────────────────────────────────────
story += [
PageBreak(),
Paragraph("3. 구현 내용", S["h1"]), hr(),
Paragraph("3-1. 신규 추가 파일", S["h2"]),
table([
["파일", "내용"],
["core/external_security.py", "API Key 생성/검증/감사 유틸리티"],
["routers/external_api.py", "외부 API 라우터 (헬스체크, SR, 웹훅, API Key 관리)"],
[".env.open", "개방망 운영 환경변수 템플릿"],
["deploy/nginx_opennet.py", "Nginx HTTPS 설정 배포 스크립트"],
], col_widths=[70*mm, 95*mm]),
Spacer(1, 4),
Paragraph("3-2. 수정된 파일", S["h2"]),
table([
["파일", "변경 내용"],
["main.py", "CORS 환경변수 기반 동적 설정, 보안 헤더 미들웨어, external_api 라우터 등록"],
["models.py", "APIKey ORM 모델 추가 (tb_api_key 테이블)"],
], col_widths=[70*mm, 95*mm]),
Spacer(1, 4),
Paragraph("3-3. 개방망 모드 CORS 동작 방식", S["h2"]),
Paragraph(
"환경변수 <b>GUARDIA_NETWORK_MODE</b>에 따라 CORS 정책이 자동 전환됩니다:",
S["body"]),
Paragraph("• closed (기본): localhost만 허용", S["note"]),
Paragraph("• open: GUARDIA_ALLOWED_ORIGINS에 지정된 외부 도메인도 허용", S["note"]),
Paragraph("• 정규식 패턴 허용으로 서브도메인 일괄 허용 가능", S["note"]),
]
# ── 섹션 4: 설치 및 설정 ────────────────────────────────────
story += [
Spacer(1, 8),
Paragraph("4. 설치 및 설정", S["h1"]), hr(),
Paragraph("4-1. .env 개방망 설정", S["h2"]),
Paragraph("다음 환경변수를 /opt/guardia/app/.env 에 설정합니다:", S["body"]),
table([
["환경변수", "값 예시", "설명"],
["GUARDIA_NETWORK_MODE", "open", "개방망 모드 활성화"],
["GUARDIA_ALLOWED_ORIGINS", "https://itsm.zioinfo.co.kr", "허용 외부 출처"],
["GUARDIA_WEBHOOK_SECRET", "<강력한 랜덤 값>", "웹훅 HMAC 서명 키"],
["DATABASE_URL", "postgresql+asyncpg://...", "@ 포함 시 %40으로 인코딩"],
], col_widths=[60*mm, 55*mm, 50*mm]),
Spacer(1, 4),
Paragraph("4-2. SSL 인증서", S["h2"]),
Paragraph(
"도메인이 있는 경우 <b>Let's Encrypt</b> 인증서 사용을 권장합니다. "
"IP만 있는 경우 자체 서명 인증서를 생성합니다.",
S["body"]),
Paragraph("도메인 보유: certbot --nginx -d itsm.zioinfo.co.kr", S["code"]),
Paragraph(
"IP 전용: openssl req -x509 -nodes -days 3650 -newkey rsa:2048 ...",
S["code"]),
]
# ── 섹션 5: API 사용법 ────────────────────────────────────────
story += [
PageBreak(),
Paragraph("5. 외부 API 사용법", S["h1"]), hr(),
Paragraph("5-1. API 엔드포인트 목록", S["h2"]),
table([
["엔드포인트", "메서드", "인증", "설명"],
["/api/external/health", "GET", "없음", "헬스체크"],
["/api/external/status", "GET", "없음", "시스템 공개 상태"],
["/api/external/keys", "POST", "JWT (관리자)", "API Key 발급"],
["/api/external/keys/{id}", "DELETE", "JWT (관리자)", "API Key 비활성화"],
["/api/external/sr", "GET", "API Key (read)", "SR 목록 조회"],
["/api/external/sr", "POST", "API Key (write)", "SR 등록"],
["/api/external/webhook", "POST", "HMAC (선택)", "외부 메신저 웹훅"],
["/docs", "GET", "없음", "OpenAPI 문서"],
], col_widths=[60*mm, 20*mm, 40*mm, 45*mm]),
Spacer(1, 4),
Paragraph("5-2. API Key 권한 스코프", S["h2"]),
table([
["스코프", "허용 API", "사용 예시"],
["read", "SR 목록 조회", "모니터링 시스템"],
["write", "SR 등록, 상태 변경", "외부 티켓 시스템"],
["admin", "모든 외부 API", "통합 관리 도구"],
["webhook", "웹훅 수신", "카카오워크, Slack 봇"],
], col_widths=[30*mm, 70*mm, 65*mm]),
Spacer(1, 4),
Paragraph("5-3. 외부 메신저 웹훅 연동 구조", S["h2"]),
Paragraph(
"외부 메신저(카카오워크, 네이버웍스, Slack 등)는 GUARDiA 웹훅 엔드포인트로 "
"자연어 명령을 전송합니다. GUARDiA는 Ollama LLM으로 명령을 파싱하여 처리합니다.",
S["body"]),
table([
["메신저", "웹훅 URL", "인증 방식"],
["카카오워크", "POST /api/external/webhook", "X-GUARDiA-Signature (HMAC)"],
["네이버웍스", "POST /api/external/webhook", "X-GUARDiA-Signature (HMAC)"],
["Slack", "POST /api/external/webhook", "X-Source: slack"],
["Teams", "POST /api/external/webhook", "X-Source: teams"],
["사용자 정의", "POST /api/external/webhook", "선택 (HMAC 권장)"],
], col_widths=[35*mm, 80*mm, 50*mm]),
]
# ── 섹션 6: 보안 ──────────────────────────────────────────────
story += [
Spacer(1, 8),
Paragraph("6. 보안 설정", S["h1"]), hr(),
Paragraph("6-1. 적용된 보안 헤더", S["h2"]),
table([
["헤더", "", "효과"],
["Strict-Transport-Security", "max-age=31536000; includeSubDomains",
"브라우저가 HTTPS만 사용"],
["X-Frame-Options", "DENY", "Clickjacking 방지"],
["X-Content-Type-Options", "nosniff", "MIME 스니핑 방지"],
["X-XSS-Protection", "1; mode=block", "XSS 차단"],
["Referrer-Policy", "strict-origin-when-cross-origin", "Referrer 정보 제한"],
], col_widths=[55*mm, 70*mm, 40*mm]),
Spacer(1, 4),
Paragraph("6-2. 변경 불가 보안 정책", S["h2"]),
Paragraph(
"개방망 모드에서도 다음 핵심 보안 정책은 절대 변경 불가합니다:",
S["body"]),
table([
["정책", "내용"],
["외부 LLM 금지", "Ollama(localhost) 전용. OpenAI/Claude 등 외부 API 완전 금지"],
["SSH 자격증명 보호", "IP, 비밀번호, SSH 계정을 API 응답에 절대 포함 금지"],
["AES-256-GCM 암호화", "서버 자격증명은 암호화 저장 (os_pw_enc 컬럼)"],
["root SSH 금지", "opsagent 전용 계정만 사용"],
["감사 로그", "모든 외부 API 호출 TB_AUDIT_LOG에 기록"],
], col_widths=[50*mm, 115*mm]),
]
# ── 섹션 7: 테스트 결과 ───────────────────────────────────────
story += [
PageBreak(),
Paragraph("7. 테스트 결과", S["h1"]), hr(),
Paragraph("테스트 환경: Ubuntu 24.04, GUARDiA 2.0.0, Nginx 1.24 | 2026-05-30", S["note"]),
Spacer(1, 4),
table([
["#", "테스트 항목", "기대값", "실제값", "결과"],
["T1", "HTTP 헬스체크 (8001)", "200 OK", "200 OK", "PASS"],
["T2", "HTTPS 헬스체크 (8443)", "200 OK", "200 OK", "PASS"],
["T3", "홈페이지 HTTPS (443)", "200 OK", "200 OK", "PASS"],
["T4", "미인증 API 접근 차단", "401", "401", "PASS"],
["T5", "CORS 외부 출처 허용", "Allow-Origin 헤더", "헤더 포함", "PASS"],
["T6", "HSTS 헤더 적용", "max-age=31536000", "적용됨", "PASS"],
["T7", "X-Frame-Options", "DENY", "DENY", "PASS"],
["T8", "Rate Limiting 설정", "zone 설정 확인", "1개 zone", "PASS"],
["T9", "공개 시스템 상태", "operational", "operational", "PASS"],
["T10", "개방망 모드 활성", "open", "open", "PASS"],
], col_widths=[10*mm, 60*mm, 40*mm, 40*mm, 15*mm]),
Spacer(1, 4),
Paragraph("전체 10개 테스트 모두 통과 (10/10 PASS)", sty(
"tr", fontSize=11, textColor=SUCCESS_GRN, alignment=TA_CENTER)),
]
# ── 섹션 8: 운영 절차 ──────────────────────────────────────────
story += [
Spacer(1, 8),
Paragraph("8. 운영 절차", S["h1"]), hr(),
Paragraph("8-1. 모드 전환 명령", S["h2"]),
table([
["작업", "명령어"],
["폐쇄망→개방망",
"echo GUARDIA_NETWORK_MODE=open >> .env && systemctl restart guardia"],
["개방망→폐쇄망",
"sed -i 's/open/closed/' .env && systemctl restart guardia"],
["HTTPS 활성화",
"ln -sf sites-available/guardia-https sites-enabled/ && nginx -t && systemctl reload nginx"],
["HTTPS 비활성화",
"rm sites-enabled/guardia-https && systemctl reload nginx"],
], col_widths=[35*mm, 130*mm]),
Spacer(1, 4),
Paragraph("8-2. 서비스 접속 정보", S["h2"]),
table([
["서비스", "URL", "용도"],
["GUARDiA ITSM HTTP", "http://zioinfo.co.kr:8001", "내부망 직접 접근"],
["GUARDiA ITSM HTTPS", "https://zioinfo.co.kr:8443", "개방망 외부 접근 (권장)"],
["외부 API", "https://zioinfo.co.kr:8443/api/external/", "API Key 인증"],
["OpenAPI 문서", "https://zioinfo.co.kr:8443/docs", "API 명세서 (공개)"],
["홈페이지 HTTPS", "https://zioinfo.co.kr", "지오정보기술 홈페이지"],
], col_widths=[40*mm, 75*mm, 50*mm]),
Spacer(1, 8),
hr(colors.HexColor("#e2e8f0")),
Paragraph(
"GUARDiA ITSM v2.0.0 | (주)지오정보기술 | 2026-05-30",
sty("footer", fontSize=8, textColor=TEXT_MUTED, alignment=TA_CENTER)),
]
doc.build(story)
print(f"PDF 생성 완료: {output_path}")
# ══════════════════════════════════════════════════════════════
# PPTX 생성
# ══════════════════════════════════════════════════════════════
def gen_pptx(output_path: str):
from pptx import Presentation
from pptx.util import Inches, Pt, Emu
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN
from pptx.util import Inches, Pt
W, H = Inches(13.33), Inches(7.5) # 16:9 와이드
prs = Presentation()
prs.slide_width = W
prs.slide_height = H
# ── 색상 팔레트 ─────────────────────────────────────────────
BRAND = RGBColor(0x1a, 0x3a, 0x6b)
ACCENT = RGBColor(0x4f, 0x6e, 0xf7)
WHITE = RGBColor(0xff, 0xff, 0xff)
GRAY_LT = RGBColor(0xf0, 0xf2, 0xf5)
GREEN = RGBColor(0x22, 0xc5, 0x5e)
ORANGE = RGBColor(0xf5, 0x9e, 0x0b)
RED = RGBColor(0xef, 0x44, 0x44)
DARK = RGBColor(0x1e, 0x29, 0x3b)
MUTED = RGBColor(0x64, 0x74, 0x8b)
def blank_slide():
layout = prs.slide_layouts[6] # blank
return prs.slides.add_slide(layout)
def add_rect(slide, x, y, w, h, fill=None, line=None, radius=0):
from pptx.util import Emu
shape = slide.shapes.add_shape(1, x, y, w, h) # MSO_SHAPE_TYPE.RECTANGLE
if fill:
shape.fill.solid()
shape.fill.fore_color.rgb = fill
else:
shape.fill.background()
if line:
shape.line.color.rgb = line
shape.line.width = Pt(0.75)
else:
shape.line.fill.background()
return shape
def add_text(slide, text, x, y, w, h, size=18, bold=False,
color=DARK, align=PP_ALIGN.LEFT, italic=False):
tb = slide.shapes.add_textbox(x, y, w, h)
tf = tb.text_frame
tf.word_wrap = True
p = tf.paragraphs[0]
p.alignment = align
run = p.add_run()
run.text = text
run.font.size = Pt(size)
run.font.bold = bold
run.font.italic = italic
run.font.color.rgb = color
return tb
def add_table_slide(slide, headers, rows, x, y, w, h, col_widths=None):
from pptx.util import Pt
cols = len(headers)
tbl = slide.shapes.add_table(len(rows)+1, cols, x, y, w, h).table
if col_widths:
for i, cw in enumerate(col_widths):
tbl.columns[i].width = cw
def cell_style(cell, text, bg=None, txt_color=DARK, bold=False, sz=10):
cell.text = text
cell.text_frame.paragraphs[0].font.size = Pt(sz)
cell.text_frame.paragraphs[0].font.bold = bold
cell.text_frame.paragraphs[0].font.color.rgb = txt_color
if bg:
cell.fill.solid()
cell.fill.fore_color.rgb = bg
for j, h_text in enumerate(headers):
cell_style(tbl.cell(0, j), h_text, bg=BRAND, txt_color=WHITE, bold=True, sz=10)
for i, row in enumerate(rows):
bg = GRAY_LT if i % 2 == 0 else WHITE
for j, val in enumerate(row):
cell_style(tbl.cell(i+1, j), str(val), bg=bg)
# ── 슬라이드 1: 표지 ───────────────────────────────────────
s = blank_slide()
add_rect(s, 0, 0, W, H, fill=BRAND)
add_rect(s, Inches(0.5), Inches(1.2), Inches(12.33), Inches(4.5),
fill=RGBColor(0x25, 0x4a, 0x80))
add_text(s, "GUARDiA ITSM", Inches(0.8), Inches(1.5), Inches(11.73), Inches(1.2),
size=44, bold=True, color=WHITE, align=PP_ALIGN.CENTER)
add_text(s, "개방망(Open Network) 구현 가이드",
Inches(0.8), Inches(2.7), Inches(11.73), Inches(0.8),
size=22, color=RGBColor(0xaa, 0xc4, 0xe8), align=PP_ALIGN.CENTER)
add_text(s, "v2.0.0 | 2026-05-30 | (주)지오정보기술",
Inches(0.8), Inches(3.5), Inches(11.73), Inches(0.5),
size=13, color=MUTED, align=PP_ALIGN.CENTER)
# 배지들
badges = [("HTTPS", GREEN), ("API Key", ACCENT), ("CORS", ORANGE), ("Rate Limit", RED)]
for i, (txt, col) in enumerate(badges):
bx = Inches(3.2 + i * 1.8)
add_rect(s, bx, Inches(5.0), Inches(1.5), Inches(0.5), fill=col)
add_text(s, txt, bx + Inches(0.1), Inches(5.05), Inches(1.3), Inches(0.4),
size=12, bold=True, color=WHITE, align=PP_ALIGN.CENTER)
add_text(s, "AI 기반 레거시 인프라 자율 운영 플랫폼",
Inches(0.8), Inches(6.5), Inches(11.73), Inches(0.5),
size=11, color=MUTED, italic=True, align=PP_ALIGN.CENTER)
# ── 슬라이드 2: 목차 ───────────────────────────────────────
s = blank_slide()
add_rect(s, 0, 0, W, Inches(1.2), fill=BRAND)
add_text(s, "목차 (Agenda)", Inches(0.4), Inches(0.25), Inches(12), Inches(0.7),
size=24, bold=True, color=WHITE)
items = [
("1", "개요 및 배경", "폐쇄망 vs 개방망 비교, 지원 필요성"),
("2", "아키텍처", "개방망 시스템 구성도, 포트 구성"),
("3", "구현 내용", "신규/수정 파일, CORS 동작 방식"),
("4", "설치 및 설정", ".env 설정, SSL 인증서, Nginx"),
("5", "외부 API", "엔드포인트, API Key 권한, 웹훅 연동"),
("6", "보안 설정", "보안 헤더, 불변 보안 정책"),
("7", "테스트 결과", "10개 항목 전체 통과 (10/10 PASS)"),
("8", "운영 절차", "모드 전환, 접속 정보"),
]
for i, (num, title, desc) in enumerate(items):
row = i // 2; col = i % 2
bx = Inches(0.4 + col * 6.4); by = Inches(1.4 + row * 1.35)
add_rect(s, bx, by, Inches(5.9), Inches(1.1), fill=GRAY_LT, line=ACCENT)
add_rect(s, bx, by, Inches(0.6), Inches(1.1), fill=ACCENT)
add_text(s, num, bx + Inches(0.05), by + Inches(0.2),
Inches(0.5), Inches(0.6), size=18, bold=True, color=WHITE, align=PP_ALIGN.CENTER)
add_text(s, title, bx + Inches(0.7), by + Inches(0.1),
Inches(5.1), Inches(0.45), size=14, bold=True, color=BRAND)
add_text(s, desc, bx + Inches(0.7), by + Inches(0.55),
Inches(5.1), Inches(0.45), size=10, color=MUTED)
# ── 슬라이드 3: 개요 ────────────────────────────────────────
s = blank_slide()
add_rect(s, 0, 0, W, Inches(1.2), fill=BRAND)
add_text(s, "1. 개요 — 개방망 지원 필요성", Inches(0.4), Inches(0.25),
Inches(12), Inches(0.7), size=24, bold=True, color=WHITE)
add_table_slide(s,
["항목", "폐쇄망 (기본)", "개방망 (이 가이드)"],
[
["접근 범위", "내부망 only", "인터넷 외부 접근 허용"],
["CORS 정책", "localhost 만 허용", "지정 외부 도메인 허용"],
["HTTPS", "선택", "필수 (TLS 1.2/1.3)"],
["API 인증", "JWT only", "JWT + API Key 추가"],
["외부 LLM", "금지 (Ollama only)", "금지 유지 (변경 불가)"],
["Rate Limiting", "기본", "강화 (30 req/min)"],
],
Inches(0.4), Inches(1.4), Inches(12.5), Inches(4.0),
col_widths=[Inches(2.5), Inches(4.5), Inches(5.5)]
)
add_rect(s, Inches(0.4), Inches(5.8), Inches(12.5), Inches(0.7),
fill=RGBColor(0xff, 0xf1, 0xf2))
add_text(s, "⚠ 핵심 원칙 유지: 개방망 모드에서도 Ollama(LLM)는 내부 전용. "
"외부 AI API 절대 사용 금지.",
Inches(0.6), Inches(5.9), Inches(12.2), Inches(0.5),
size=11, bold=True, color=RED)
# ── 슬라이드 4: 아키텍처 ────────────────────────────────────
s = blank_slide()
add_rect(s, 0, 0, W, Inches(1.2), fill=BRAND)
add_text(s, "2. 개방망 아키텍처", Inches(0.4), Inches(0.25),
Inches(12), Inches(0.7), size=24, bold=True, color=WHITE)
# 아키텍처 다이어그램 (박스들)
boxes = [
("외부 클라이언트\n(브라우저/메신저봇)", Inches(0.3), Inches(1.3), Inches(2.8), Inches(1.0), ACCENT, WHITE),
("Nginx\n(TLS + Rate Limit + 보안헤더)", Inches(4.0), Inches(1.3), Inches(2.8), Inches(1.0), BRAND, WHITE),
("GUARDiA ITSM\n(FastAPI 8001)", Inches(7.7), Inches(1.3), Inches(2.8), Inches(1.0), ACCENT, WHITE),
("PostgreSQL\n(내부 전용 5432)", Inches(7.7), Inches(3.2), Inches(2.8), Inches(0.9), DARK, WHITE),
("Ollama LLM\n(내부 전용 11434)", Inches(11.0), Inches(3.2), Inches(2.0), Inches(0.9), DARK, WHITE),
]
for txt, bx, by, bw, bh, fill, txt_col in boxes:
add_rect(s, bx, by, bw, bh, fill=fill)
add_text(s, txt, bx + Inches(0.1), by + Inches(0.1), bw - Inches(0.2),
bh - Inches(0.2), size=11, bold=True, color=txt_col, align=PP_ALIGN.CENTER)
# 화살표 텍스트
add_text(s, "→ HTTPS (443/8443)", Inches(3.2), Inches(1.6), Inches(0.7), Inches(0.5),
size=8, color=MUTED, align=PP_ALIGN.CENTER)
add_text(s, "→ HTTP 내부", Inches(6.9), Inches(1.6), Inches(0.7), Inches(0.5),
size=8, color=MUTED, align=PP_ALIGN.CENTER)
# 포트 구성 테이블
add_table_slide(s,
["포트", "서비스", "외부 접근"],
[
["80/443", "홈페이지 Nginx (HTTPS)", "허용"],
["8001", "GUARDiA FastAPI (직접)", "권장 안 함"],
["8443", "GUARDiA Nginx (HTTPS, 권장)", "허용"],
["5432", "PostgreSQL", "차단"],
["11434", "Ollama LLM", "차단"],
],
Inches(0.4), Inches(4.3), Inches(7.5), Inches(2.7),
col_widths=[Inches(1.5), Inches(3.5), Inches(2.5)]
)
# ── 슬라이드 5: 외부 API ────────────────────────────────────
s = blank_slide()
add_rect(s, 0, 0, W, Inches(1.2), fill=BRAND)
add_text(s, "5. 외부 API 엔드포인트", Inches(0.4), Inches(0.25),
Inches(12), Inches(0.7), size=24, bold=True, color=WHITE)
add_table_slide(s,
["엔드포인트", "메서드", "인증", "설명"],
[
["/api/external/health", "GET", "없음", "헬스체크 (공개)"],
["/api/external/status", "GET", "없음", "시스템 공개 상태"],
["/api/external/keys", "POST", "JWT (관리자)", "API Key 발급"],
["/api/external/sr", "GET", "API Key (read)", "SR 목록 조회"],
["/api/external/sr", "POST", "API Key (write)", "SR 등록"],
["/api/external/webhook", "POST", "HMAC (선택)", "외부 메신저 웹훅 수신"],
],
Inches(0.4), Inches(1.4), Inches(12.5), Inches(3.2),
col_widths=[Inches(3.5), Inches(1.5), Inches(2.5), Inches(5.0)]
)
add_text(s, "API Key 권한 스코프:", Inches(0.4), Inches(4.9),
Inches(4), Inches(0.4), size=12, bold=True, color=BRAND)
scopes = [("read", "조회", GREEN), ("write", "등록/수정", ACCENT),
("webhook", "웹훅", ORANGE), ("admin", "전체", RED)]
for i, (sc, desc, col) in enumerate(scopes):
bx = Inches(0.4 + i * 3.1)
add_rect(s, bx, Inches(5.4), Inches(2.8), Inches(0.8), fill=col)
add_text(s, f"{sc}\n{desc}", bx + Inches(0.1), Inches(5.45),
Inches(2.6), Inches(0.7), size=11, bold=True, color=WHITE, align=PP_ALIGN.CENTER)
# ── 슬라이드 6: 테스트 결과 ─────────────────────────────────
s = blank_slide()
add_rect(s, 0, 0, W, Inches(1.2), fill=BRAND)
add_text(s, "7. 테스트 결과 — 10/10 PASS ✅", Inches(0.4), Inches(0.25),
Inches(12), Inches(0.7), size=24, bold=True, color=WHITE)
add_table_slide(s,
["#", "테스트 항목", "기대값", "실제값", "결과"],
[
["T1", "HTTP 헬스체크 (8001)", "200 OK", "200 OK", "PASS"],
["T2", "HTTPS 헬스체크 (8443)", "200 OK", "200 OK", "PASS"],
["T3", "홈페이지 HTTPS (443)", "200 OK", "200 OK", "PASS"],
["T4", "미인증 API 접근 차단", "401", "401", "PASS"],
["T5", "CORS 외부 출처 허용", "Allow-Origin", "헤더 포함", "PASS"],
["T6", "HSTS 헤더 적용", "max-age=31536000", "적용됨", "PASS"],
["T7", "X-Frame-Options DENY", "DENY", "DENY", "PASS"],
["T8", "Rate Limiting 설정", "zone 확인", "1개 zone", "PASS"],
["T9", "공개 시스템 상태", "operational", "operational", "PASS"],
["T10", "개방망 모드 활성", "NETWORK_MODE=open", "open", "PASS"],
],
Inches(0.4), Inches(1.4), Inches(12.5), Inches(4.5),
col_widths=[Inches(0.8), Inches(3.8), Inches(2.5), Inches(2.5), Inches(1.5)]
)
add_rect(s, Inches(0.4), Inches(6.2), Inches(12.5), Inches(0.8), fill=GREEN)
add_text(s, "✅ 전체 10개 테스트 모두 통과 (10/10 PASS) — 2026-05-30",
Inches(0.6), Inches(6.3), Inches(12.1), Inches(0.6),
size=14, bold=True, color=WHITE, align=PP_ALIGN.CENTER)
# ── 슬라이드 7: 마지막 ──────────────────────────────────────
s = blank_slide()
add_rect(s, 0, 0, W, H, fill=BRAND)
add_text(s, "GUARDiA ITSM 개방망 지원 완료", Inches(1), Inches(2.0),
Inches(11.33), Inches(1.2), size=32, bold=True, color=WHITE, align=PP_ALIGN.CENTER)
add_text(s, "HTTP → HTTPS 전환 | API Key 인증 | CORS 외부 허용 | 보안 헤더 강화",
Inches(1), Inches(3.3), Inches(11.33), Inches(0.6),
size=14, color=RGBColor(0xaa, 0xc4, 0xe8), align=PP_ALIGN.CENTER)
add_text(s, "(주)지오정보기술 | GUARDiA v2.0.0 | 2026-05-30",
Inches(1), Inches(5.5), Inches(11.33), Inches(0.5),
size=12, color=MUTED, align=PP_ALIGN.CENTER)
prs.save(output_path)
print(f"PPTX 생성 완료: {output_path}")
if __name__ == "__main__":
pdf_out = str(OUT_DIR / "23_GUARDiA_개방망_가이드.pdf")
pptx_out = str(OUT_DIR / "24_GUARDiA_개방망_발표자료.pptx")
gen_pdf(pdf_out)
gen_pptx(pptx_out)
print("\n=== 생성 완료 ===")
print(f"PDF : {pdf_out}")
print(f"PPTX: {pptx_out}")