#!/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은 기본적으로 폐쇄망(Closed Network) 환경에서 운영됩니다. "
"그러나 외부 메신저(카카오워크, 네이버웍스, Slack)와의 연동, 공공기관 포털 연계, "
"재택/원격 관리 등의 요구사항이 증가함에 따라 개방망 지원 기능을 추가하였습니다.",
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(
"⚠ 핵심 원칙 유지: 개방망 모드에서도 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(
"환경변수 GUARDIA_NETWORK_MODE에 따라 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(
"도메인이 있는 경우 Let's Encrypt 인증서 사용을 권장합니다. "
"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}")