- 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>
702 lines
35 KiB
Python
702 lines
35 KiB
Python
#!/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}")
|