#!/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("완료")