zioinfo-mail/workspace/guardia-docs/gen_license_docs.py
DESKTOP-TKLFCPR\ython cfe2901a55 refactor(structure): consolidate all projects under workspace/
- itsm/    -> workspace/guardia-itsm/
- manager/ -> workspace/guardia-manager/
- app/     -> workspace/guardia-messenger/
- manual/  -> workspace/guardia-docs/

workspace/zioinfo-web/ unchanged.
git mv preserves full commit history.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 23:50:56 +09:00

461 lines
25 KiB
Python

#!/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}")