""" GUARDiA ITSM GS인증 신청서 PDF 생성기 실행: python generate_pdf.py """ import os import sys from pathlib import Path from datetime import datetime # reportlab 임포트 from reportlab.lib import colors from reportlab.lib.pagesizes import A4 from reportlab.lib.units import mm from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT, TA_JUSTIFY from reportlab.platypus import ( SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, HRFlowable, PageBreak, KeepTogether ) from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont # ── 한글 폰트 등록 ──────────────────────────────────────────── def register_korean_fonts(): """Windows/Linux에서 한글 폰트 자동 탐색 후 등록.""" font_candidates = [ # Windows "C:/Windows/Fonts/malgun.ttf", "C:/Windows/Fonts/malgunbd.ttf", "C:/Windows/Fonts/gulim.ttc", "C:/Windows/Fonts/batang.ttc", # Linux "/usr/share/fonts/truetype/nanum/NanumGothic.ttf", "/usr/share/fonts/truetype/unfonts-core/UnDotum.ttf", "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", "/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc", ] font_bold_candidates = [ "C:/Windows/Fonts/malgunbd.ttf", "/usr/share/fonts/truetype/nanum/NanumGothicBold.ttf", ] # 일반 폰트 for path in font_candidates: if os.path.exists(path): try: pdfmetrics.registerFont(TTFont("KorFont", path)) print(f" 폰트 등록: {path}") break except Exception as e: continue # 볼드 폰트 for path in font_bold_candidates: if os.path.exists(path): try: pdfmetrics.registerFont(TTFont("KorFontBold", path)) break except Exception: continue # 폰트 패밀리 등록 try: from reportlab.pdfbase.pdfmetrics import registerFontFamily registerFontFamily("KorFont", normal="KorFont", bold="KorFontBold") except Exception: pass # ── 색상 정의 ────────────────────────────────────────────────── NAVY = colors.HexColor("#1e3a5f") BLUE = colors.HexColor("#0051A2") ACCENT = colors.HexColor("#00A3E0") LIGHT_BG = colors.HexColor("#EBF3FB") GRAY = colors.HexColor("#64748B") LIGHT_GRAY = colors.HexColor("#F3F4F6") WHITE = colors.white BLACK = colors.black GREEN = colors.HexColor("#065F46") GREEN_BG = colors.HexColor("#D1FAE5") # ── 페이지 설정 ──────────────────────────────────────────────── W, H = A4 MARGIN = 20 * mm def make_styles(): """스타일 정의.""" register_korean_fonts() base_font = "KorFont" if "KorFont" in pdfmetrics.getRegisteredFontNames() else "Helvetica" bold_font = "KorFontBold" if "KorFontBold" in pdfmetrics.getRegisteredFontNames() else "Helvetica-Bold" styles = { "cover_title": ParagraphStyle("cover_title", fontName=bold_font, fontSize=26, textColor=WHITE, alignment=TA_CENTER, leading=32, spaceAfter=6), "cover_sub": ParagraphStyle("cover_sub", fontName=base_font, fontSize=13, textColor=colors.HexColor("#BDE3FF"), alignment=TA_CENTER, leading=18), "cover_info": ParagraphStyle("cover_info", fontName=base_font, fontSize=11, textColor=WHITE, alignment=TA_CENTER, leading=16), "section": ParagraphStyle("section", fontName=bold_font, fontSize=13, textColor=WHITE, leading=18, leftIndent=6), "h2": ParagraphStyle("h2", fontName=bold_font, fontSize=11, textColor=NAVY, leading=16, spaceBefore=8, spaceAfter=4), "body": ParagraphStyle("body", fontName=base_font, fontSize=9.5, textColor=BLACK, leading=15, spaceAfter=3), "body_center": ParagraphStyle("body_center", fontName=base_font, fontSize=9.5, textColor=BLACK, leading=15, alignment=TA_CENTER), "small": ParagraphStyle("small", fontName=base_font, fontSize=8.5, textColor=GRAY, leading=13), "bold": ParagraphStyle("bold", fontName=bold_font, fontSize=9.5, textColor=BLACK, leading=15), "footer": ParagraphStyle("footer", fontName=base_font, fontSize=8, textColor=GRAY, alignment=TA_CENTER, leading=12), "note": ParagraphStyle("note", fontName=base_font, fontSize=8.5, textColor=GRAY, leading=13, leftIndent=10), } return styles, base_font, bold_font def build_pdf(output_path: str): """PDF 빌드 메인 함수.""" styles, base_font, bold_font = make_styles() doc = SimpleDocTemplate( output_path, pagesize=A4, leftMargin=MARGIN, rightMargin=MARGIN, topMargin=MARGIN, bottomMargin=MARGIN, title="GUARDiA ITSM GS인증 신청서", author="(주)지오정보기술", subject="GS인증 (Good Software) 1등급 신청", ) story = [] W_CONTENT = W - 2 * MARGIN # ── 표지 (Page 1) ───────────────────────────────────────── # 파란 헤더 배너 header_table = Table( [[Paragraph("GS인증 신청서", styles["cover_title"]), Paragraph("Good Software Certification Application", styles["cover_sub"])]], colWidths=[W_CONTENT], ) header_table.setStyle(TableStyle([ ("BACKGROUND", (0, 0), (-1, -1), NAVY), ("ROUNDEDCORNERS", (0, 0), (-1, -1), [8]), ("TOPPADDING", (0, 0), (-1, -1), 20), ("BOTTOMPADDING",(0, 0), (-1, -1), 20), ("LEFTPADDING", (0, 0), (-1, -1), 20), ("SPAN", (0,0), (0,-1)), ])) story.append(header_table) story.append(Spacer(1, 10*mm)) # 제품 로고 영역 logo_data = [ [Paragraph("GUARDiA ITSM", ParagraphStyle("logo", fontName=bold_font, fontSize=36, textColor=BLUE, alignment=TA_CENTER))], [Paragraph("AI 기반 레거시 인프라 자율 운영 플랫폼", ParagraphStyle("logo_sub", fontName=base_font, fontSize=14, textColor=GRAY, alignment=TA_CENTER))], [Spacer(1, 4*mm)], [Paragraph("Version 2.0.0 | 2026년 9월 신청", ParagraphStyle("ver", fontName=base_font, fontSize=11, textColor=GRAY, alignment=TA_CENTER))], ] logo_table = Table(logo_data, colWidths=[W_CONTENT]) logo_table.setStyle(TableStyle([ ("BACKGROUND", (0,0), (-1,-1), LIGHT_BG), ("ROUNDEDCORNERS", (0,0), (-1,-1), [8]), ("TOPPADDING", (0,0), (-1,-1), 16), ("BOTTOMPADDING", (0,0), (-1,-1), 16), ])) story.append(logo_table) story.append(Spacer(1, 8*mm)) # 제출 정보 박스 submit_data = [ ["제 출 처", "한국정보통신기술협회 (TTA) SW시험인증연구소"], ["신 청 등 급", "GS 1등급 (Good Software 1st Grade)"], ["신 청 일 자", "2026년 9월"], ["문 의 처", "031-724-0114 | sw@tta.or.kr"], ] submit_table = Table( [[Paragraph(k, styles["bold"]), Paragraph(v, styles["body"])] for k, v in submit_data], colWidths=[40*mm, W_CONTENT - 40*mm], ) submit_table.setStyle(TableStyle([ ("BACKGROUND", (0, 0), (0, -1), NAVY), ("BACKGROUND", (1, 0), (1, -1), WHITE), ("TEXTCOLOR", (0, 0), (0, -1), WHITE), ("FONTNAME", (0, 0), (0, -1), bold_font), ("FONTNAME", (1, 0), (1, -1), base_font), ("FONTSIZE", (0, 0), (-1, -1), 9.5), ("ALIGN", (0, 0), (0, -1), "CENTER"), ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), ("TOPPADDING", (0, 0), (-1, -1), 6), ("BOTTOMPADDING",(0, 0), (-1, -1), 6), ("LEFTPADDING", (0, 0), (-1, -1), 8), ("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor("#CBD5E1")), ])) story.append(submit_table) # 표지 하단 story.append(Spacer(1, 12*mm)) story.append(HRFlowable(width=W_CONTENT, color=BLUE, thickness=2)) story.append(Spacer(1, 4*mm)) story.append(Paragraph( "(주)지오정보기술 | Copyright © 2026 All Rights Reserved.", styles["footer"] )) story.append(PageBreak()) # ── 1. 신청자 정보 ───────────────────────────────────────── def section_header(title, subtitle=""): data = [[Paragraph(f" {title}", styles["section"])]] t = Table(data, colWidths=[W_CONTENT]) t.setStyle(TableStyle([ ("BACKGROUND", (0,0), (-1,-1), NAVY), ("ROUNDEDCORNERS", (0,0), (-1,-1), [6]), ("TOPPADDING", (0,0), (-1,-1), 8), ("BOTTOMPADDING", (0,0), (-1,-1), 8), ])) return t def info_table(data_rows, col_widths=None): """키-값 테이블 생성.""" if col_widths is None: col_widths = [45*mm, W_CONTENT - 45*mm] rows = [] for k, v in data_rows: rows.append([ Paragraph(k, styles["bold"]), Paragraph(v, styles["body"]), ]) t = Table(rows, colWidths=col_widths) t.setStyle(TableStyle([ ("BACKGROUND", (0, 0), (0, -1), LIGHT_BG), ("FONTNAME", (0, 0), (0, -1), bold_font), ("FONTSIZE", (0, 0), (-1, -1), 9.5), ("ALIGN", (0, 0), (0, -1), "CENTER"), ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), ("TOPPADDING", (0, 0), (-1, -1), 6), ("BOTTOMPADDING",(0, 0), (-1, -1), 6), ("LEFTPADDING", (0, 0), (-1, -1), 8), ("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor("#CBD5E1")), ])) return t story.append(section_header("1. 신청자 정보")) story.append(Spacer(1, 4*mm)) story.append(info_table([ ("회 사 명", "(주)지오정보기술"), ("대 표 자", "(대표이사명)"), ("사업자등록번호", "000-00-00000"), ("주 소", "서울특별시 (상세주소)"), ("전 화", "02-000-0000"), ("팩 스", "02-000-0001"), ("이 메 일", "gs@zioinfo.co.kr"), ("담 당 자", "(담당자명) / 개발팀장"), ("담당자 연락처", "010-0000-0000"), ])) story.append(Spacer(1, 8*mm)) # ── 2. 소프트웨어 정보 ──────────────────────────────────── story.append(section_header("2. 소프트웨어 정보")) story.append(Spacer(1, 4*mm)) story.append(info_table([ ("제 품 명", "GUARDiA ITSM"), ("버 전", "2.0.0"), ("빌 드 일", "2026-08-31"), ("신청 등급", "GS 1등급"), ("제품 분류", "시스템 관리 소프트웨어"), ("세부 분류", "IT서비스관리(ITSM) / 인프라 자동화 플랫폼"), ("운영 환경", "서버 소프트웨어"), ("저작권 등록번호", "C-2026-XXXXXX (등록 후 기입)"), ("소프트웨어사업자", "제XXX호 (신고 후 기입)"), ])) story.append(Spacer(1, 8*mm)) # ── 3. 제품 개요 ────────────────────────────────────────── story.append(section_header("3. 제품 개요")) story.append(Spacer(1, 4*mm)) # 제품 설명 박스 desc_text = ( "GUARDiA ITSM은 1,000개 이상 공공기관의 레거시 IT 인프라를 AI로 자율 운영하는 " "온프레미스 통합 관리 플랫폼입니다. 메신저 한 줄 명령으로 에이전트 설치 없이 " "SSH/SFTP를 통해 WAS 배포·운영을 완전 자동화하며, SR 관리, 인시던트 대응, " "변경관리, CMDB, PMS 등 ITSM 전 기능을 단일 플랫폼에서 제공합니다." ) desc_table = Table([[Paragraph(desc_text, styles["body"])]], colWidths=[W_CONTENT]) desc_table.setStyle(TableStyle([ ("BACKGROUND", (0,0), (-1,-1), LIGHT_BG), ("LEFTPADDING", (0,0), (-1,-1), 12), ("RIGHTPADDING", (0,0), (-1,-1), 12), ("TOPPADDING", (0,0), (-1,-1), 10), ("BOTTOMPADDING", (0,0), (-1,-1), 10), ("ROUNDEDCORNERS",(0,0), (-1,-1), [6]), ])) story.append(desc_table) story.append(Spacer(1, 5*mm)) # 핵심 기능 story.append(Paragraph("핵심 기능", styles["h2"])) features = [ ("1", "AI ChatOps", "메신저 25개 명령어로 인프라 실시간 제어"), ("2", "에이전트리스 배포", "SSH/SFTP 기반 — 대상 서버 소프트웨어 설치 불필요"), ("3", "통합 ITSM", "SR·인시던트·변경관리·SLA·CMDB 완비"), ("4", "PMS 프로젝트 관리", "WBS·산출물·일간/주간/월간 자동 보고서"), ("5", "보안 완전성", "JWT+MFA+AES-256-GCM+PAM+취약점스캔"), ] feat_data = [[Paragraph(no, styles["body_center"]), Paragraph(name, styles["bold"]), Paragraph(desc, styles["body"])] for no, name, desc in features] feat_table = Table(feat_data, colWidths=[10*mm, 50*mm, W_CONTENT-60*mm]) feat_table.setStyle(TableStyle([ ("BACKGROUND", (0, 0), (0, -1), NAVY), ("TEXTCOLOR", (0, 0), (0, -1), WHITE), ("FONTNAME", (0, 0), (0, -1), bold_font), ("BACKGROUND", (1, 0), (1, -1), LIGHT_BG), ("FONTSIZE", (0, 0), (-1, -1), 9.5), ("ALIGN", (0, 0), (0, -1), "CENTER"), ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), ("TOPPADDING", (0, 0), (-1, -1), 5), ("BOTTOMPADDING",(0, 0), (-1, -1), 5), ("LEFTPADDING", (0, 0), (-1, -1), 6), ("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor("#CBD5E1")), ])) story.append(feat_table) story.append(Spacer(1, 5*mm)) # 지원 환경 story.append(Paragraph("지원 환경", styles["h2"])) env_data = [ ["서버 OS", "Ubuntu 20.04+, CentOS 7+, RHEL 8+, Windows Server 2019+"], ["브라우저", "Chrome 90+, Firefox 88+, Edge 90+"], ["최소 사양", "CPU 2코어, RAM 8GB, 디스크 20GB"], ] env_table = Table( [[Paragraph(k, styles["bold"]), Paragraph(v, styles["body"])] for k, v in env_data], colWidths=[30*mm, W_CONTENT-30*mm], ) env_table.setStyle(TableStyle([ ("BACKGROUND", (0,0), (0,-1), LIGHT_BG), ("FONTSIZE", (0,0), (-1,-1), 9.5), ("ALIGN", (0,0), (0,-1), "CENTER"), ("VALIGN", (0,0), (-1,-1), "MIDDLE"), ("TOPPADDING", (0,0), (-1,-1), 5), ("BOTTOMPADDING",(0,0), (-1,-1), 5), ("LEFTPADDING", (0,0), (-1,-1), 8), ("GRID", (0,0), (-1,-1), 0.5, colors.HexColor("#CBD5E1")), ])) story.append(env_table) story.append(PageBreak()) # ── 4. 시험 요청 항목 ────────────────────────────────────── story.append(section_header("4. 시험 요청 항목 (ISO/IEC 25010)")) story.append(Spacer(1, 4*mm)) qualities = [ ("기능 적합성", "Functional Suitability", "✓", "기능 완전성, 정확성, 적절성"), ("성능 효율성", "Performance Efficiency", "✓", "동시 100명 기준 응답시간 3초 이내"), ("호 환 성", "Compatibility", "✓", "4개 OS, 주요 브라우저 지원"), ("사 용 성", "Usability", "✓", "웹접근성(KWCAG 2.1 AA) 포함"), ("신 뢰 성", "Reliability", "✓", "백업/복구, 에러 처리 포함"), ("보 안 성", "Security", "✓", "시큐어코딩 점검 내장"), ("유지보수성", "Maintainability", "✓", "버전 정보, 오류코드 체계 포함"), ("이 식 성", "Portability", "✓", "설치/제거 스크립트 완비"), ] qual_header = [[ Paragraph("품질 특성", styles["section"]), Paragraph("ISO 명칭", styles["section"]), Paragraph("요청", styles["section"]), Paragraph("비고", styles["section"]), ]] qual_data = [ [Paragraph(k, styles["bold"]), Paragraph(en, styles["small"]), Paragraph(req, ParagraphStyle("check", fontName=bold_font, fontSize=12, textColor=GREEN, alignment=TA_CENTER)), Paragraph(note, styles["body"])] for k, en, req, note in qualities ] qual_table = Table( qual_header + qual_data, colWidths=[30*mm, 45*mm, 15*mm, W_CONTENT-90*mm], ) qual_table.setStyle(TableStyle([ ("BACKGROUND", (0, 0), (-1, 0), NAVY), ("TEXTCOLOR", (0, 0), (-1, 0), WHITE), ("BACKGROUND", (0, 1), (-1, -1), WHITE), ("ROWBACKGROUNDS",(0, 1), (-1, -1), [WHITE, LIGHT_BG]), ("FONTSIZE", (0, 0), (-1, -1), 9.5), ("ALIGN", (0, 0), (-1, 0), "CENTER"), ("ALIGN", (2, 1), (2, -1), "CENTER"), ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), ("TOPPADDING", (0, 0), (-1, -1), 6), ("BOTTOMPADDING",(0, 0), (-1, -1), 6), ("LEFTPADDING", (0, 0), (-1, -1), 8), ("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor("#CBD5E1")), ])) story.append(qual_table) story.append(Spacer(1, 8*mm)) # ── 5. 제출 서류 목록 ───────────────────────────────────── story.append(section_header("5. 제출 서류 목록")) story.append(Spacer(1, 4*mm)) docs = [ ("필수", "GS인증 신청서", "본 문서"), ("필수", "사업자등록증 사본", "법인"), ("필수", "소프트웨어사업자 신고확인서", "과학기술정보통신부"), ("필수", "제품 기능 명세서", "certification/04_기술문서/ 참조"), ("필수", "사용자 매뉴얼 (PDF)", "관리자용 + 일반사용자용"), ("필수", "설치·제거 매뉴얼", "setup/uninstall.sh 포함"), ("필수", "소프트웨어 패키지", "설치 USB 또는 다운로드 링크"), ("필수", "저작권 등록증", "한국저작권위원회"), ("필수", "심사 수수료 납부 영수증", "약 650만원"), ("선택", "기술특허 출원서", "에이전트리스 자동화 방법"), ("선택", "GS이전버전 인증서", "없음 (신규 신청)"), ] doc_header = [[ Paragraph("구분", styles["section"]), Paragraph("서류명", styles["section"]), Paragraph("비고", styles["section"]), ]] doc_data = [] for kind, name, note in docs: bg = GREEN_BG if kind == "필수" else LIGHT_BG doc_data.append([ Paragraph(kind, ParagraphStyle("dk", fontName=bold_font, fontSize=9, textColor=GREEN if kind=="필수" else GRAY, alignment=TA_CENTER)), Paragraph(name, styles["bold"]), Paragraph(note, styles["body"]), ]) doc_table = Table( doc_header + doc_data, colWidths=[18*mm, 65*mm, W_CONTENT-83*mm], ) doc_table.setStyle(TableStyle([ ("BACKGROUND", (0, 0), (-1, 0), NAVY), ("TEXTCOLOR", (0, 0), (-1, 0), WHITE), ("ROWBACKGROUNDS",(0, 1), (-1, -1), [WHITE, LIGHT_BG]), ("FONTSIZE", (0, 0), (-1, -1), 9.5), ("ALIGN", (0, 0), (-1, 0), "CENTER"), ("ALIGN", (0, 1), (0, -1), "CENTER"), ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), ("TOPPADDING", (0, 0), (-1, -1), 5), ("BOTTOMPADDING",(0, 0), (-1, -1), 5), ("LEFTPADDING", (0, 0), (-1, -1), 8), ("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor("#CBD5E1")), ])) story.append(doc_table) story.append(Spacer(1, 8*mm)) # ── 6. 수수료 ───────────────────────────────────────────── story.append(section_header("6. 예상 수수료")) story.append(Spacer(1, 4*mm)) fee_data = [ ["심사 수수료", "5,000,000원"], ["시험 수수료", "1,500,000원"], ["합 계", "6,500,000원 (VAT 별도)"], ] fee_table = Table( [[Paragraph(k, styles["bold"]), Paragraph(v, styles["body"])] for k, v in fee_data], colWidths=[40*mm, W_CONTENT-40*mm], ) fee_table.setStyle(TableStyle([ ("BACKGROUND", (0, 0), (0, -1), LIGHT_BG), ("BACKGROUND", (0, 2), (-1, 2), LIGHT_BG), ("FONTNAME", (0, 2), (-1, 2), bold_font), ("FONTSIZE", (0, 0), (-1, -1), 9.5), ("ALIGN", (0, 0), (0, -1), "CENTER"), ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), ("TOPPADDING", (0, 0), (-1, -1), 6), ("BOTTOMPADDING",(0, 0), (-1, -1), 6), ("LEFTPADDING", (0, 0), (-1, -1), 8), ("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor("#CBD5E1")), ])) story.append(fee_table) story.append(Spacer(1, 5*mm)) story.append(Paragraph( "※ 실제 금액은 TTA 접수 시 확정됩니다. 위 금액은 2026년 기준 추정치입니다.", styles["note"] )) story.append(Spacer(1, 10*mm)) # ── 7. 서약 및 서명 ─────────────────────────────────────── story.append(section_header("7. 서약 및 서명")) story.append(Spacer(1, 6*mm)) pledge_text = ( "본 신청서에 기재한 내용이 사실과 다름이 없음을 확인하며, " "한국정보통신기술협회(TTA)의 GS인증 심사 규정을 준수할 것을 서약합니다. " "심사 중 제출 자료의 허위 기재 사실이 밝혀질 경우 인증이 취소될 수 있음을 인지합니다." ) pledge_table = Table([[Paragraph(pledge_text, styles["body"])]], colWidths=[W_CONTENT]) pledge_table.setStyle(TableStyle([ ("BACKGROUND", (0,0), (-1,-1), LIGHT_BG), ("LEFTPADDING", (0,0), (-1,-1), 12), ("RIGHTPADDING", (0,0), (-1,-1), 12), ("TOPPADDING", (0,0), (-1,-1), 10), ("BOTTOMPADDING", (0,0), (-1,-1), 10), ])) story.append(pledge_table) story.append(Spacer(1, 10*mm)) # 날짜 및 서명 sign_data = [ ["신 청 일", "2026년 월 일", ""], ["", "", ""], ["회 사 명", "(주)지오정보기술", ""], ["대 표 자", "", " (인)"], ] sign_table = Table( [[Paragraph(k, styles["bold"]), Paragraph(v, styles["body"]), Paragraph(s, styles["body"])] for k, v, s in sign_data], colWidths=[30*mm, 90*mm, 30*mm], ) sign_table.setStyle(TableStyle([ ("BACKGROUND", (0, 0), (0, -1), LIGHT_BG), ("FONTSIZE", (0, 0), (-1, -1), 10), ("ALIGN", (0, 0), (0, -1), "CENTER"), ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), ("TOPPADDING", (0, 0), (-1, -1), 8), ("BOTTOMPADDING",(0, 0), (-1, -1), 8), ("LEFTPADDING", (0, 0), (-1, -1), 8), ("LINEBELOW", (1, 3), (1, 3), 1, BLACK), ("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor("#CBD5E1")), ])) story.append(sign_table) # ── 최종 푸터 ───────────────────────────────────────────── story.append(Spacer(1, 12*mm)) story.append(HRFlowable(width=W_CONTENT, color=BLUE, thickness=1.5)) story.append(Spacer(1, 4*mm)) footer_data = [ Paragraph("한국정보통신기술협회 (TTA) | 경기도 성남시 분당구 양현로 322", styles["footer"]), Paragraph("Tel: 031-724-0114 | E-mail: sw@tta.or.kr | www.tta.or.kr", styles["footer"]), Spacer(1, 3*mm), Paragraph( f"본 문서는 GUARDiA ITSM GS인증 신청을 위해 (주)지오정보기술이 작성한 공식 신청서입니다. " f"생성일: {datetime.now().strftime('%Y-%m-%d')}", styles["footer"] ), ] for item in footer_data: story.append(item) # ── PDF 빌드 ────────────────────────────────────────────── def on_first_page(canvas, doc): canvas.saveState() canvas.restoreState() def on_later_pages(canvas, doc): canvas.saveState() # 페이지 번호 canvas.setFont("Helvetica", 8) canvas.setFillColor(GRAY) canvas.drawRightString(W - MARGIN, 10*mm, f"- {doc.page} -") canvas.drawString(MARGIN, 10*mm, "GUARDiA ITSM GS인증 신청서 | (주)지오정보기술") canvas.restoreState() doc.build(story, onFirstPage=on_first_page, onLaterPages=on_later_pages) size_kb = Path(output_path).stat().st_size / 1024 print(f"\n[OK] PDF 생성 완료: {output_path}") print(f" 파일 크기: {size_kb:.1f} KB") if __name__ == "__main__": cert_dir = Path(__file__).parent output = cert_dir / "GS인증_신청서_GUARDiA_ITSM_v2.0.pdf" import sys sys.stdout.reconfigure(encoding='utf-8', errors='replace') print("GUARDiA ITSM GS인증 신청서 PDF 생성 중...") build_pdf(str(output))