#!/usr/bin/env python3 """GUARDiA Messenger 개발·배포 가이드 PDF + PPTX 자동 생성""" from pathlib import Path OUT = Path(__file__).parent # ═══════════════════════════════════ # 공통 상수 # ═══════════════════════════════════ BRAND_HEX = "#1a3a6b" ACCENT_HEX = "#4f6ef7" GREEN_HEX = "#22c55e" ORANGE_HEX = "#f59e0b" RED_HEX = "#ef4444" GRAY_HEX = "#f0f2f5" # ═══════════════════════════════════ # 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 = "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(BRAND_HEX) ACCENT = colors.HexColor(ACCENT_HEX) GREEN = colors.HexColor(GREEN_HEX) GRAY = colors.HexColor(GRAY_HEX) MUTED = colors.HexColor("#64748b") W = A4[0] - 40*mm doc = SimpleDocTemplate(out, pagesize=A4, leftMargin=20*mm, rightMargin=20*mm, topMargin=20*mm, bottomMargin=20*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=4,spaceAfter=4), "note": sty("note",fontSize=9,textColor=MUTED,leftIndent=10,leading=14,spaceAfter=3), "ok": sty("ok",fontSize=10,textColor=GREEN,fontName=font), } story = [] # 표지 cover = Table([ [Paragraph("GUARDiA Messenger",sty("ct",fontSize=28,textColor=colors.white,alignment=TA_CENTER,leading=36))], [Paragraph("모바일 앱 개발 · EAS 빌드 · 스토어 배포 가이드",sty("cs",fontSize=14,textColor=colors.HexColor("#aac4e8"),alignment=TA_CENTER))], [Paragraph(" ",sty("sp",fontSize=8,textColor=colors.white,alignment=TA_CENTER))], [Paragraph("v1.0.0 | 2026-05-31 | (주)지오정보기술",sty("cm",fontSize=10,textColor=colors.HexColor("#7c85a8"),alignment=TA_CENTER))], [Paragraph("React Native + Expo SDK 51 + EAS Build",sty("cm2",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),32),("BOTTOMPADDING",(0,0),(-1,-1),32), ("LEFTPADDING",(0,0),(-1,-1),20),("RIGHTPADDING",(0,0),(-1,-1),20)])) story += [Spacer(1,26*mm), cover, PageBreak()] # 섹션 1: 앱 개요 story += [ Paragraph("1. 앱 개요", S["h1"]), hr(), Paragraph("GUARDiA Messenger는 GUARDiA ITSM과 연동하는 모바일 앱입니다.", S["body"]), Paragraph("1-1. 구현 화면", S["h2"]), tbl([["화면","경로","주요 기능"], ["로그인","(auth)/login.tsx","JWT 인증 · SecureStore 저장"], ["대시보드","(tabs)/index.tsx","SR 통계 · 서비스 상태 · 배포 이력"], ["SR 관리","(tabs)/sr.tsx","서비스 요청 목록 조회 및 신규 등록"], ["AI 챗봇","(tabs)/chat.tsx","Ollama LLM 자연어 인프라 명령"], ["알림","(tabs)/notifications.tsx","인시던트·SLA·배포 알림 수신"], ["설정","(tabs)/settings.tsx","프로필·알림설정·로그아웃"]], cws=[28*mm,50*mm,87*mm]), Spacer(1,6), Paragraph("1-2. 기술 스택", S["h2"]), tbl([["항목","기술"], ["프레임워크","React Native 0.74.5 + Expo SDK 51"], ["언어","TypeScript (strict)"], ["라우터","Expo Router 3.5.x (파일 기반)"], ["인증 저장소","expo-secure-store (보안 키체인)"], ["HTTP 클라이언트","Axios (서버: https://zioinfo.co.kr:8443)"], ["빌드 시스템","EAS Build (Expo Application Services)"]], cws=[40*mm,125*mm]), ] # 섹션 2: EAS 빌드 story += [ PageBreak(), Paragraph("2. EAS 빌드 가이드", S["h1"]), hr(), Paragraph("2-1. 빌드 명령어", S["h2"]), tbl([["명령어","용도","소요 시간"], ["eas build --platform android --profile preview","테스트 APK","~10분"], ["eas build --platform android --profile production","Play Store AAB","~15분"], ["eas build --platform ios --profile production","App Store IPA","~20분"]], cws=[105*mm,45*mm,15*mm]), Spacer(1,6), Paragraph("2-2. 빌드 전 필수 체크리스트", S["h2"]), tbl([["항목","올바른 상태","확인 명령"], ["android/ 폴더","없어야 함","ls android/ → 오류가 정상"], [".easignore","android/, ios/ 포함","cat .easignore"], ["PNG Crunching","false","cat plugins/withGradleProps.js"], ["babel.config","babel-preset-expo만","cat babel.config.js"], ["EAS 로그인","zioinfo 표시","npx eas-cli whoami"]], cws=[40*mm,50*mm,75*mm]), Spacer(1,6), Paragraph("2-3. APK 폰 설치 (Android Studio 불필요)", S["h2"]), Paragraph("안드로이드 폰 브라우저에서 Expo 빌드 URL 열기 → Download → 설치", S["body"]), Paragraph("설정 → 보안 → 알 수 없는 앱 설치 허용 (최초 1회 설정 필요)", S["note"]), ] # 섹션 3: 빌드 이슈 이력 story += [ PageBreak(), Paragraph("3. 빌드 이슈 이력 및 해결책", S["h1"]), hr(), Paragraph("실제 빌드 과정에서 발생한 4개 이슈와 검증된 해결책입니다.", S["note"]), Spacer(1,6), tbl([["이슈","원인","해결"], ["Gradle UNKNOWN ERROR","android/ 폴더 → EAS Bare Workflow 오인",".easignore에 android/, ios/ 추가"], ["packageReleaseResources 실패","PIL PNG + AAPT2 PNG Crunching 충돌","withGradleProps.js enablePngCrunchInReleaseBuilds=false"], ["Firebase Gradle 오류","expo-notifications 플러그인 (google-services.json 없음)","app.json plugins에서 expo-notifications 제거"], ["Babel 경고","expo-router/babel deprecated (SDK 51)","babel.config.js에서 제거, babel-preset-expo만 사용"]], cws=[55*mm,60*mm,50*mm]), Spacer(1,6), Paragraph("3-1. 핵심 수정 파일: plugins/withGradleProps.js", S["h2"]), Paragraph("android.enablePngCrunchInReleaseBuilds=false", S["code"]), Paragraph("reactNativeArchitectures=arm64-v8a", S["code"]), Paragraph("org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m", S["code"]), Spacer(1,4), Paragraph("최종 성공 빌드: 51096ada (Android APK, EAS zioinfo 계정)", S["ok"]), ] # 섹션 4: 스토어 등록 story += [ PageBreak(), Paragraph("4. 스토어 등록 절차", S["h1"]), hr(), tbl([["항목","Google Play","Apple App Store"], ["계정 비용","$25 1회","$99/년"], ["패키지/번들 ID","kr.co.zioinfo.guardia","kr.co.zioinfo.guardia"], ["Privacy Policy","필수","필수"], ["스크린샷","최소 2개","최소 3개 (6.7인치)"], ["제출 방법","AAB 업로드","eas submit --platform ios"], ["심사 기간","2~7 영업일","1~3 영업일"]], cws=[45*mm,70*mm,50*mm]), Spacer(1,6), Paragraph("4-1. Privacy Policy URL (App Store 필수)", S["h2"]), Paragraph("https://zioinfo.co.kr/privacy 페이지에 개인정보처리방침 등록 필요", S["body"]), Paragraph("수집 정보: 이메일, 사용자명 (JWT 인증 목적) / 제3자 제공 없음", S["note"]), ] # 섹션 5: 하네스 구조 story += [ Spacer(1,8), Paragraph("5. 하네스 (.claude/) 구조", S["h1"]), hr(), tbl([["에이전트","역할"], ["rn-developer","React Native 화면 구현, 컴포넌트 개발"], ["eas-engineer","EAS Build 실행, 빌드 실패 진단"], ["store-publisher","Play Store / App Store 등록 메타데이터"], ["doc-writer","개발 가이드 작성, PDF/PPTX 생성"]], cws=[45*mm,120*mm]), Spacer(1,6), tbl([["스킬","트리거 키워드"], ["messenger-orchestrator","화면 구현, EAS 빌드, 스토어 등록, 가이드 작성 등 모든 요청"], ["rn-screen-dev","화면 추가, 컴포넌트 작성, UI 수정, API 연동"], ["eas-build-deploy","APK 빌드, Gradle 오류, EAS 설정, 빌드 실패"], ["store-publish","Play Store, App Store, 스크린샷, Privacy Policy"], ["doc-generator","가이드 작성, PDF 생성, PPTX 생성, 문서화"]], cws=[50*mm,115*mm]), Spacer(1,10), hr(colors.HexColor("#e2e8f0")), Paragraph("GUARDiA Messenger v1.0.0 | (주)지오정보기술 | 2026-05-31", 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): 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, sz=9): c.text = str(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(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 RGBColor(0xff,0xff,0xff) 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 Messenger",Inches(.8),Inches(1.5),Inches(11.73),Inches(1.2), sz=42,bold=True,color=WHITE,align=PP_ALIGN.CENTER) text(s,"모바일 앱 개발 · EAS 빌드 · 스토어 배포 가이드",Inches(.8),Inches(2.8),Inches(11.73),Inches(.7), sz=18,color=RGBColor(0xaa,0xc4,0xe8),align=PP_ALIGN.CENTER) text(s,"v1.0.0 | 2026-05-31 | (주)지오정보기술 | React Native + Expo SDK 51", Inches(.8),Inches(3.6),Inches(11.73),Inches(.5),sz=13,color=MUTED,align=PP_ALIGN.CENTER) for i,(t_,c) in enumerate([("📱 6개 화면",GREEN),("🔨 EAS Build",ACCENT), ("🏪 스토어 등록",ORANGE),("🐛 4개 이슈 해결",RED)]): bx = Inches(2.0+i*2.3) rect(s,bx,Inches(4.9),Inches(2.1),Inches(.5),fill=c) text(s,t_,bx+Inches(.1),Inches(4.95),Inches(1.9),Inches(.4), sz=12,bold=True,color=WHITE,align=PP_ALIGN.CENTER) # S2: 앱 개요 s = blank() rect(s,0,0,W,Inches(1.2),fill=BRAND) text(s,"앱 개요 — 구현된 6개 화면",Inches(.4),Inches(.28),Inches(12),Inches(.7),sz=24,bold=True,color=WHITE) tbl_sl(s,["화면","경로","기능"], [["로그인","(auth)/login.tsx","JWT 인증 · SecureStore 저장"], ["대시보드","(tabs)/index.tsx","SR 통계 · 서비스 상태 · 배포 이력"], ["SR 관리","(tabs)/sr.tsx","서비스 요청 목록 조회 및 신규 등록"], ["AI 챗봇","(tabs)/chat.tsx","Ollama LLM 자연어 인프라 명령"], ["알림","(tabs)/notifications.tsx","인시던트·SLA·배포 알림 수신"], ["설정","(tabs)/settings.tsx","프로필·알림설정·로그아웃"]], Inches(.4),Inches(1.4),Inches(12.5),Inches(3.8), cws=[Inches(2.0),Inches(3.5),Inches(7.0)]) # S3: EAS 빌드 가이드 s = blank() rect(s,0,0,W,Inches(1.2),fill=BRAND) text(s,"EAS Build 가이드",Inches(.4),Inches(.28),Inches(12),Inches(.7),sz=24,bold=True,color=WHITE) tbl_sl(s,["명령어","용도","시간"], [["eas build --platform android --profile preview","테스트 APK","~10분"], ["eas build --platform android --profile production","Play Store AAB","~15분"], ["eas build --platform ios --profile production","App Store IPA","~20분"]], Inches(.4),Inches(1.4),Inches(12.5),Inches(2.0), cws=[Inches(6.5),Inches(4.0),Inches(2.0)]) text(s,"필수 체크리스트",Inches(.4),Inches(3.6),Inches(12),Inches(.4),sz=14,bold=True,color=BRAND) items = [("android/ 폴더 없음",GREEN),("PNG Crunching=false",GREEN), (".easignore 설정",GREEN),("babel-preset-expo만",GREEN),("EAS 로그인",GREEN)] for i,(t_,c) in enumerate(items): bx=Inches(.4+i*2.5); by=Inches(4.1) rect(s,bx,by,Inches(2.3),Inches(.6),fill=RGBColor(0xf0,0xfd,0xf4)) text(s,"✅ "+t_,bx+Inches(.1),by+Inches(.1),Inches(2.1),Inches(.4),sz=10,color=GREEN) # S4: 빌드 이슈 이력 s = blank() rect(s,0,0,W,Inches(1.2),fill=BRAND) text(s,"빌드 이슈 이력 — 4개 이슈 모두 해결",Inches(.4),Inches(.28),Inches(12),Inches(.7),sz=24,bold=True,color=WHITE) tbl_sl(s,["이슈","원인","해결"], [["Gradle UNKNOWN ERROR","android/ 폴더 → Bare Workflow 오인",".easignore android/, ios/ 추가"], ["packageReleaseResources 실패","PIL PNG + AAPT2 Crunching 충돌","withGradleProps.js PNG Crunching=false"], ["Firebase Gradle 오류","expo-notifications (google-services.json 없음)","app.json plugins에서 제거"], ["Babel 경고","expo-router/babel deprecated (SDK 51)","babel-preset-expo만 사용"]], Inches(.4),Inches(1.4),Inches(12.5),Inches(3.2), cws=[Inches(3.5),Inches(4.5),Inches(4.5)]) rect(s,Inches(.4),Inches(4.8),Inches(12.5),Inches(.8),fill=GREEN) text(s,"✅ 최종 성공 빌드: 51096ada (Android APK) — EAS 계정: zioinfo", Inches(.6),Inches(4.9),Inches(12.1),Inches(.6),sz=14,bold=True,color=WHITE,align=PP_ALIGN.CENTER) # S5: 스토어 등록 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_sl(s,["항목","Google Play","Apple App Store"], [["계정 비용","$25 (1회)","$99/년"], ["패키지/번들 ID","kr.co.zioinfo.guardia","kr.co.zioinfo.guardia"], ["Privacy Policy","필수","필수 (URL 필요)"], ["스크린샷","최소 2개","최소 3개 (6.7인치)"], ["제출 방법","AAB 업로드","eas submit --platform ios"], ["심사 기간","2~7 영업일","1~3 영업일"]], Inches(.4),Inches(1.4),Inches(12.5),Inches(3.5), cws=[Inches(3.0),Inches(4.75),Inches(4.75)]) rect(s,Inches(.4),Inches(5.2),Inches(12.5),Inches(.8),fill=RGBColor(0xff,0xf3,0xcd)) text(s,"📌 App Store는 Privacy Policy URL이 필수입니다: https://zioinfo.co.kr/privacy 페이지 등록 필요", Inches(.6),Inches(5.3),Inches(12.1),Inches(.6),sz=12,color=RGBColor(0x85,0x4d,0x0e)) # S6: 하네스 구조 s = blank() rect(s,0,0,W,Inches(1.2),fill=BRAND) text(s,"하네스 (.claude/) 구조 — 4 에이전트 + 5 스킬",Inches(.4),Inches(.28),Inches(12),Inches(.7),sz=24,bold=True,color=WHITE) agents = [("👨‍💻","rn-developer","RN 화면 구현"), ("🔨","eas-engineer","EAS 빌드 관리"), ("🏪","store-publisher","스토어 등록"), ("📝","doc-writer","가이드 + PDF/PPTX")] for i,(icon,name,desc) in enumerate(agents): bx=Inches(.4+i*3.2); by=Inches(1.4) rect(s,bx,by,Inches(3.0),Inches(2.5),fill=GRAY) text(s,icon,bx+Inches(.9),by+Inches(.2),Inches(1.2),Inches(.7),sz=28,align=PP_ALIGN.CENTER,color=DARK) text(s,name,bx+Inches(.2),by+Inches(1.0),Inches(2.6),Inches(.5),sz=13,bold=True,color=BRAND,align=PP_ALIGN.CENTER) text(s,desc,bx+Inches(.2),by+Inches(1.5),Inches(2.6),Inches(.5),sz=11,color=MUTED,align=PP_ALIGN.CENTER) tbl_sl(s,["스킬","트리거 키워드"], [["messenger-orchestrator","화면 구현, EAS 빌드, 스토어 등록, 가이드 작성 등 모든 요청"], ["rn-screen-dev","화면 추가, 컴포넌트, UI 수정, API 연동"], ["eas-build-deploy","APK 빌드, Gradle 오류, EAS 설정"], ["store-publish","Play Store, App Store, 스크린샷"], ["doc-generator","가이드 작성, PDF, PPTX 생성"]], Inches(.4),Inches(4.1),Inches(12.5),Inches(2.8), cws=[Inches(3.0),Inches(9.5)]) # 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) tbl_sl(s,["항목","값"], [["서버 URL","https://zioinfo.co.kr:8443"], ["관리자 계정","admin / Admin@zioinfo2026!"], ["EAS 계정","zioinfo (expo.dev)"], ["EAS 프로젝트 ID","ca2f72d6-7dda-4491-9590-7ace34b10a88"], ["성공 빌드 ID","51096ada-9735-4ea8-9e81-5f5991731ea8"], ["패키지명","kr.co.zioinfo.guardia"], ["개발 서버","npx expo start (로컬)"]], Inches(.4),Inches(1.4),Inches(12.5),Inches(4.0), cws=[Inches(4.0),Inches(8.5)]) # S8: 마지막 s = blank() rect(s,0,0,W,H,fill=BRAND) text(s,"GUARDiA Messenger",Inches(1),Inches(2.3),Inches(11.33),Inches(1.2), sz=34,bold=True,color=WHITE,align=PP_ALIGN.CENTER) text(s,"📱 APK 빌드 성공 · 🏪 스토어 등록 준비 완료",Inches(1),Inches(3.6),Inches(11.33),Inches(.7), sz=16,color=RGBColor(0xaa,0xc4,0xe8),align=PP_ALIGN.CENTER) text(s,"(주)지오정보기술 | GUARDiA Messenger v1.0.0 | 2026-05-31", Inches(1),Inches(5.5),Inches(11.33),Inches(.5),sz=13,color=MUTED,align=PP_ALIGN.CENTER) prs.save(out) print(f"PPTX: {out}") if __name__ == "__main__": pdf = str(OUT / "35_GUARDiA_Messenger_개발가이드.pdf") pptx = str(OUT / "36_GUARDiA_Messenger_발표자료.pptx") gen_pdf(pdf) gen_pptx(pptx) print("\n=== 완료 ===") print(f"MD : {OUT}/34_GUARDiA_Messenger_개발_배포_가이드.md") print(f"PDF : {pdf}") print(f"PPTX: {pptx}")