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