zioinfo-mail/workspace/guardia-docs/gen_messenger_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

412 lines
22 KiB
Python

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