회사에서 박람회를 다녀오면 참관보고서를 쓴다.
사내 양식이 정해져 있다. 표지, 목차, 섹션 구분면, 개요 표, 서술 페이지, 사진 그리드.
구조가 매번 같다. 다른 건 사진과 내용뿐이다.
두 번째 보고서를 쓰다가 생각했다.
형식이 매번 같으면, 그건 사람이 할 일이 아니다.
자동화의 경계를 먼저 그었다
시작하기 전에 하나를 정했다. 무엇을 자동화하고 무엇을 자동화하지 않을 것인가.
- 자동화할 것: 형식. 로고 위치, 색상, 폰트 크기, 표 열 너비, 사진 배치, 페이지 번호, 워터마크
- 자동화하지 않을 것: 내용. 무엇을 봤고 무엇이 중요한지
이 구분이 중요했다. 내용까지 자동으로 만들려고 하면 결국 검수하느라 직접 쓰는 것보다 오래 걸린다.
보고서에서 가치 있는 건 관찰과 판단이고, 그건 다녀온 사람만 쓸 수 있다.
반복되는 건 형식이지 내용이 아니다.
그래서 구조를 엔진과 드라이버로 나눴다.
pptxProductionSystem/
├─ rexgen_report.py ← 재사용 엔진 (슬라이드 빌더 모음)
├─ build_inlex2026.py ← 박람회별 드라이버 (내용만 채움)
├─ build_stk2026.py
├─ assets/ ← 로고, 워터마크
└─ expos/<박람회>/img/ ← 박람회별 사진
엔진은 한 번 만들고, 박람회가 생길 때마다 드라이버만 새로 쓴다.
드라이버는 이렇게 생겼다
실제 사용하는 코드가 이 정도다.
r = Report()
# 표지
r.cover("InLEX 2026 참관보고서", ["연구소 플랫폼팀"], "2026년 6월 9일 ~ 11일")
# 목차
r.contents(["전시회 개요", "주요 전시 기술", "기업 분석"])
# 섹션 구분면
r.section_divider(1, "전시회 개요", "정보 & 요약")
# 개요 표
r.info_table("1. 전시회 개요", "정보", [
("정식 명칭", "2026 대한민국 국방산업발전대전"),
("일자", "2026년 6월 9일(화) ~ 11일(목), 3일간"),
("장소", "대전컨벤션센터(DCC)"),
])
여기 어디에도 좌표나 색상이 없다.
"표지를 만들어라", "목차를 만들어라"만 있고, 그게 어떻게 생겼는지는 엔진이 안다.
보고서를 쓰는 사람은 무엇을 넣을지만 생각하면 된다.
한글 폰트가 제일 어려웠다
의외의 복병이 있었다.
python-pptx에서 run.font.name = "맑은 고딕"을 해도 한글이 적용되지 않는다.
파워포인트의 폰트 설정은 하나가 아니라 세 개로 나뉘어 있기 때문이다.
a:latin— 로마자a:ea— 동아시아 문자 (한글·한자·가나)a:cs— 복합 문자
라이브러리의 font.name은 a:latin만 건드린다. 그래서 영문은 바뀌고 한글은 안 바뀌는 기묘한 상태가 된다.
결국 XML을 직접 만졌다.
def _kfont(run, name):
run.font.name = name
rPr = run._r.get_or_add_rPr()
for tag in ('a:latin', 'a:ea', 'a:cs'):
el = rPr.find(qn(tag))
if el is None:
el = rPr.makeelement(qn(tag), {})
rPr.append(el)
el.set('typeface', name)
세 태그를 모두 찾아서, 없으면 만들고, 같은 폰트를 박아 넣는다.
이걸 모든 텍스트 런에 적용하는 헬퍼로 감싸두니 그 뒤로는 신경 쓸 일이 없어졌다.
라이브러리가 추상화해준 것 아래를 봐야 할 때가 있다는 걸 배웠다.
사진은 항상 말썽이다
두 번째 복병은 이미지 포맷이었다.
휴대폰으로 찍은 사진은 HEIC이고, 파워포인트는 HEIC을 못 넣는다.
그리고 세로로 찍은 사진이 옆으로 누워서 들어간다. EXIF 회전 정보를 파워포인트가 무시하기 때문이다.
이것도 결국 사전 처리 단계를 만들었다.
def _safe_image(path):
try:
im = Image.open(path)
if im.format in _PPTX_OK:
return path
out = os.path.splitext(path)[0] + ".conv.jpg"
im.convert("RGB").save(out, "JPEG", quality=90)
return out
except Exception:
return path
지원하는 포맷이면 그대로 쓰고, 아니면 JPG로 변환해서 그 경로를 돌려준다.
그리고 마지막 줄이 중요하다 — 실패하면 원본 경로를 그대로 돌려준다.
이미지 하나 변환에 실패했다고 보고서 생성 전체가 멈추면 안 된다.
일단 만들어지게 하고, 이상한 사진은 사람이 보고 고치는 게 낫다.
앞선 글에서 포맷 훅을 "항상 성공으로 끝내라"고 했던 것과 같은 생각이다.
도구는 막지 말고 통과시켜야 한다.
디자인 토큰은 여기도 있었다
색상을 코드 여기저기 쓰지 않고 상단에 모아뒀다.
TITLE_NAVY = RGBColor(0x1C, 0x47, 0x85)
FILL_NAVY = RGBColor(0x1C, 0x48, 0x85)
INK = RGBColor(0x1A, 0x1A, 0x1A)
BODY = RGBColor(0x26, 0x26, 0x26)
CAPGRAY = RGBColor(0x59, 0x59, 0x59)
FOOTGRAY = RGBColor(0xA6, 0xA6, 0xA6)
웹에서 CSS 변수로 하던 것과 똑같은 일이다.
사내 양식 색이 바뀌면 여기 한 줄만 고치면 된다.
포맷이 PPTX든 HTML이든, 하드코딩된 값은 어디서나 같은 문제를 일으킨다는 게 재밌었다.
얻은 것
보고서 하나 만드는 시간이 반나절에서 한 시간 남짓으로 줄었다.
그런데 시간보다 더 좋았던 게 두 개 있다.
① 형식이 흔들리지 않는다. 손으로 만들면 두 번째 보고서의 폰트 크기가 첫 번째와 미묘하게 다르다. 이제는 항상 같다.
② 쓰는 데 집중하게 된다. 좌표를 맞추고 표 너비를 조정하던 시간이 사라지니까, 무엇을 봤고 그게 우리 제품에 어떤 의미인지를 쓰는 데 시간을 쓴다.
자동화의 목적이 시간 절약인 줄 알았는데, 실제로 얻은 건 중요한 일에 쓸 시간의 확보였다.
정리하면 이렇다.
- 형식과 내용의 경계를 먼저 긋는다. 반복되는 건 형식이지 내용이 아니다
- 엔진과 드라이버를 나눈다. 새 케이스에서는 내용만 쓴다
- 라이브러리 아래를 봐야 할 때가 있다. 한글 폰트는 XML까지 내려가야 했다
- 전처리는 실패해도 통과시킨다. 도구가 작업을 막으면 안 된다
- 디자인 토큰은 PPTX에서도 유효하다
작은 도구지만, 앞으로 박람회를 다녀올 때마다 이득이 쌓인다.
한 번 만들면 계속 돌려받는 종류의 코드가 제일 기분 좋다.