회사에서 박람회를 다녀오면 참관보고서를 쓴다.

사내 양식이 정해져 있다. 표지, 목차, 섹션 구분면, 개요 표, 서술 페이지, 사진 그리드.

구조가 매번 같다. 다른 건 사진과 내용뿐이다.

두 번째 보고서를 쓰다가 생각했다.

형식이 매번 같으면, 그건 사람이 할 일이 아니다.

자동화의 경계를 먼저 그었다

시작하기 전에 하나를 정했다. 무엇을 자동화하고 무엇을 자동화하지 않을 것인가.

  • 자동화할 것: 형식. 로고 위치, 색상, 폰트 크기, 표 열 너비, 사진 배치, 페이지 번호, 워터마크
  • 자동화하지 않을 것: 내용. 무엇을 봤고 무엇이 중요한지

이 구분이 중요했다. 내용까지 자동으로 만들려고 하면 결국 검수하느라 직접 쓰는 것보다 오래 걸린다.

보고서에서 가치 있는 건 관찰과 판단이고, 그건 다녀온 사람만 쓸 수 있다.

반복되는 건 형식이지 내용이 아니다.

그래서 구조를 엔진과 드라이버로 나눴다.

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.namea: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에서도 유효하다

작은 도구지만, 앞으로 박람회를 다녀올 때마다 이득이 쌓인다.

한 번 만들면 계속 돌려받는 종류의 코드가 제일 기분 좋다.