Illustrator를 그만두다: CSV와 Python으로 명함 PDF를 자동 생성하여 인쇄 업체에 바로 입고하는 방법
요약
Illustrator를 이용한 수동 명함 제작 방식에서 벗어나, CSV 데이터와 Python을 활용하여 인쇄 규격에 맞는 PDF를 자동 생성하는 파이프라인을 구축했습니다. GitHub Actions를 통해 데이터 업데이트 시 자동으로 PDF가 생성되도록 자동화하였으며, Gemini 1.5 Pro와 Claude 3.5 Sonnet 등 AI 모델을 활용해 디자인 리뷰 프로세스를 통합했습니다.
핵심 포인트
- ReportLab과 Ghostscript를 사용하여 인쇄용 규격(CMYK, 도련, 폰트 아웃라인)을 완벽히 준수하는 PDF 생성
- CSV 파일 업데이트만으로 전 직원의 명함을 일괄 생성하는 자동화 워크플로우 구축
- GitHub Actions를 활용한 CI/CD 기반의 인쇄물 생성 및 배포 자동화
- Gemini 1.5 Pro, Claude 3.5 Sonnet, Claude Code를 활용한 AI 기반 디자인 리뷰 체계 도입
3초 요약
Illustrator를 실행하여 한 명씩 명함 데이터를 만들던 운영 방식을 그만두었다. 사내 디자이너가 없기 때문에, CSV + Python으로 자동 생성하는 파이프라인으로 교체했다. 인쇄 업체의 템플릿(도련(Trim marks)·도련(Bleed) 3mm·CMYK·폰트 아웃라인화)과 완전히 일치하는 PDF를 ReportLab + Ghostscript로 출력한다.
employees.csv를 업데이트하면 GitHub Actions가 실행되어 전 직원의 PDF를 ZIP 파일로 결과물로 공개한다. 신입 사원 온보딩(Onboarding) 워크플로우에組み込めば(組み込めば), 입사일에 맞춰 자동 발주까지 일관되게 처리할 수 있다.
코드 생성은 Gemini 1.5 Pro / Claude 3.5 Sonnet / Claude Code의 3인 리뷰어 체제로 디자인 지적 사항을 반영한 frontend-design 스킬을 사용한다.
어떤 사람을 위한 기사인가?
- Illustrator로 명함, DM, 엽서를 한 명씩 만들며 소모되고 있는 총무·코퍼레이트(Corporate) 담당자
- 사내 디자이너가 없어서, 심플한 디자인의 인쇄물을 직원 수 규모로 운영하고 싶은 사람
- DTP 입고용 PDF(도련·도련·CMYK·폰트 아웃라인화)를 코드로 생성하고 싶은 사람
- 입사 온보딩의 '대여품·명함·비품' 발주를 자동화하고 싶은 SRE나 정보시스템(IT) 담당자
- AI 에이전트에게 리뷰를 맡기는 소프트웨어 개발 흐름을 실례로 보고 싶은 사람
왜 만들었는가
저희 회사(주식회사 마인디아)의 명함은 이런 느낌으로, 디자인은 매우 심플합니다. 앞면은 일본어, 뒷면은 영어, 기업 컬러인 오렌지 계열 그라데이션에 로고가 들어갈 뿐입니다.
그럼에도 지금까지는, 관리부 멤버가 Illustrator로 개인별 파일을 열어 이름과 직함, 이메일 주소를 수정하고, PDF로 내보낸 뒤, 인쇄 업체 사이트에 업로드하여 발주하는 운영 방식이었습니다. 저 자신이 하는 것이 아니라, 관리부(코퍼레이트 부문)의 수작업으로 고착화되어 있었습니다.
이것은 관리부의 시간을 은근하게 갉아먹는 타입의 태스크입니다.
- Illustrator의 실행이 무겁다. Apple Silicon에서도 수십 초를 기다려야 한다.
- 이름을 바꾸기만 하면 되는데, 매번 10분 이상이 소요된다.
- 입사가 몰리면 'A님 분량, B님 분량, C님 분량, D님 분량' 식으로 직렬 처리하게 되어, 반나절 업무가 된다.
- 사내 디자이너가 없어서 디자인 조정이 필요해지면 관리부 멤버가 툴과 사투를 벌일 수밖에 없다.
게다가 저희 회사의 디자인은 심플합니다. 텍스트와 로고와 그라데이션뿐입니다. "사람이 Illustrator로 파일을 하나씩 수정하는" 운영을 해야 할 이유는 객관적으로 단 하나도 없습니다.
관리부가 본래의 핵심 업무에 더 많은 시간을 쓸 수 있도록 하기 위해서라도, 여기는 코드로 해결하는 것이 정답이라고 판단했습니다. CTO(저)가 "자동화 기반을 만들어 전달하는" 역할, 관리부는 "CSV를 업데이트하는" 역할로 분업을 재설계한 것이 이번 시도입니다.
완성된 것
최종적으로 다음과 같은 경험이 가능해졌습니다.
# 전 직원분 생성 (미입력 행은 스킵)
$ make build
# 샘플 CSV로 1명분 확인
...
출력물은 인쇄 업체(MHT 디자인을 상정)의 입고 템플릿과 완전히 일치하는 PDF입니다. 도련(Trim marks)·도련(Bleed)·CMYK·폰트 아웃라인화가 완료되었습니다. 그대로 입고 사이트에 업로드하면 발주가 완료됩니다.
make build를 입력하면, employees.csv의 전 직원분이 output/ 디렉토리 아래에 PDF로 생성됩니다.
output/
├── taro_yamada.pdf
├── hanako_sato.pdf
...
그리고 후술하겠지만, employees.csv를 main 브랜치에 push하면 GitHub Actions가 전 직원의 명함을 생성하여 릴리스(Release)로 공개합니다. 관리부 측에서는 "CSV에 행을 하나 추가하고 PR을 머지(Merge)하기만 하면" 명함 발주용 파일을 얻을 수 있는 경험을 하게 됩니다.
전체 아키텍처
대략적인 흐름은 다음과 같습니다.
데이터, 템플릿, 설정, 코드를 확실히 분리하는 것이 핵심입니다. 성함이나 이메일 주소와 같은 가변 데이터 (Variable Data) 는 CSV, 회사 주소나 색상과 같은 반고정값 (Semi-fixed values) 은 TOML, 로직은 Python, 디자인의 정답은 업체 템플릿 (Vendor Template) 에 두었습니다.
그리고 CI/CD 측면은 다음과 같습니다.
기술 스택 (Tech Stack)
| 용도 | 채택한 기술 | 이유 |
|---|---|---|
| 언어 | Python 3.11+ | tomllib 표준 탑재, Pydantic v2, 데이터 가공과 PDF 생성 모두에 능숙 |
| PDF 생성 | ReportLab | DTP용 PDF를 세밀하게 제어할 수 있는 전통 있는 라이브러리. CMYK 및 그라데이션 대응 |
| SVG 읽기 | svglib | 아이콘 SVG 읽기용. 단, 로고의 linearGradient는 자체 파싱으로 전환 (후술) |
| 데이터 검증 | Pydantic v2 | CSV/TOML의 유효성 검사 (Validation) 를 스키마로 정의. EmailStr 타입을 지정하는 것만으로 이메일 주소 형식 체크를 직접 작성할 필요가 없음 |
| CLI | Typer | --csv --out --filter-id와 같은 인자 정의를 타입 어노테이션 (Type Annotation)만으로 해결 |
| 아웃라인화 (Outlining) | Ghostscript (gs -dNoOutputFonts) | 폰트 임베딩이 아닌 패스 (Path) 화. 업체의 폰트 환경에 의존하지 않게 함 |
| 템플릿 읽기 | PyMuPDF (fitz) | 업체의 입고 템플릿 PDF에서 도련(Trim marks) 선분을 실측 하여 재현 |
| 의존성 관리 | pyproject.toml + pip install -e . | 단순함 우선 |
| CI | GitHub Actions | CSV 업데이트 → PDF 전건 재생성 → Releases 공개 |
입고 사양의 함정과 이를 극복한 방법
여기서부터가 본론인 기술 파트입니다.
"명함 PDF를 만들어 인쇄 업체에 보내기만 하면 된다"라고 들으면 쉬워 보이지만, 상업 인쇄용 PDF에는 지키지 않으면 리젝트(Reject)되는 사양이 여러 가지 있습니다.
1. 페이지 크기 및 도련(Trim marks) 위치를 업체 템플릿과 완전히 일치시키기
인쇄 업체의 사이트에는 "입고 템플릿"이 배포되어 있으며, 이것과 한 치의 오차도 없는 위치에 도련, 도련선(Bleed), 재단선(Trim line)을 배치하는 것이 요구됩니다. 1mm라도 어긋나면 재단선이 틀어져 명함이 밀린 상태로 완성됩니다.
저희 회사가 사용하는 업체의 template_91x55.pdf를 PyMuPDF로 열어서, 도련(코너의 L자 선)의 좌표를 실측 하여 가져왔습니다.
@lru_cache(maxsize=1)
def _extract_tombo_lines() -> tuple[tuple[float, float, float, float], ...]:
"""template_91x55.pdf 에서 코너 인근의 검은 직선만을 추출한다."""
...
PyMuPDF의 Y축은 상단이 원점이지만, ReportLab은 하단이 원점입니다. page_h - p1.y를 통해 좌표계 (Coordinate System) 를 변환 하고 있습니다. 사소해 보이지만, 이 부분을 틀리면 도련이 상하 반대로 되어 입고가 불가능해집니다.
고안한 포인트는 두 가지입니다.
- 업체 템플릿에 포함된 주의사항 텍스트("일반 명함 91×55mm", "재단 위치는 여기"와 같은 선)를 전부 제외하고 싶습니다. 코너 인근(30pt 이내)이면서 축에 평행한(가로선 또는 세로선) 검은 선만을 필터링합니다.
@lru_cache를 사용하여 결과를 한 번만 계산합니다. 직원 수만큼의 PDF를 생성할 때 매번 PDF를 열어 다시 추출하는 것은 낭비이므로 캐싱을 적용했습니다.
2. CMYK + 폰트 아웃라인화 (Font Outlining)
상업 인쇄 업체는 기본적으로 RGB를 받지 않습니다. CMYK (Cyan, Magenta, Yellow, Black)로 데이터를 만들어야 합니다. 또한, 업체의 환경에 폰트가 설치되어 있지 않더라도 글자가 깨지지 않도록, 폰트를 "문자 정보"가 아닌 "패스의 집합"으로 변환 해야 합니다 (아웃라인화).
ReportLab은 일단 CMYK 출력이 가능하지만, 폰트의 아웃라인화는 할 수 없습니다. 그래서 Ghostscript로 후처리를 하고 있습니다.
def build_gs_command(input_pdf: Path, output_pdf: Path) -> list[str]:
gs = shutil.which("gs") or "gs"
return [
...
]
-dNoOutputFonts는 Ghostscript에서 "폰트 정보를 출력하지 않고, 모두 패스(Path)로 변환하는" 플래그입니다. 이를 통해 폰트가 임베드(Embed)되지 않았더라도 글자가 깨지는 현상을 방지할 수 있습니다. -dPDFSETTINGS=/prepress를 사용하면 상업용 인쇄에 적합한 해상도 및 압축 설정이 적용됩니다.
CLI 호출부에서는 이 후처리 과정을 옵션으로 스킵(Skip) 할 수 있도록 구현했습니다(디버깅 시에는 텍스트 선택이 가능한 PDF가 필요하기 때문입니다).
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp:
intermediate = Path(tmp.name)
try:
...
tempfile.NamedTemporaryFile(delete=False)를 사용하여 일단 중간 PDF를 작성한 뒤, Ghostscript에 입력하여 최종 PDF를 생성합니다. 예외가 발생하더라도 finally를 통해 반드시 중간 파일을 삭제하도록 했습니다.
3. 로고 SVG는 자체 파서(Parser)로 그리기 (svglib의 한계)
회사 로고는 Adobe Illustrator에서 SVG로 내보낸 파일을 assets/logo/text_logo.svg에 배치해 두었습니다. 아이콘(메일, 전화, URL)도 마찬가지입니다.
하지만 svglib에는 linearGradient(선형 그라데이션)가 제대로 그려지지 않는 지뢰가 있습니다. 저희 회사의 로고는 "왼쪽 하단 살몬색 #F54040 → 오른쪽 상단 피치색 #F5AA77"로 이어지는 CI 그라데이션이 핵심이므로, 이것이 단색으로 그려지는 것은 허용할 수 없습니다.
그래서 SVG의 <path d="..."/> 부분만 정규 표현식(Regular Expression)으로 추출하여, 자체 파서로 ReportLab의 Path API에 주입하는 방식으로 구현했습니다.
def _emit_path(p, d: str, sx: float, sy: float, tx: float, ty: float) -> None:
"""SVG d를 ReportLab Path로 주입함.
좌표 변환: ReportLab_x = svg_x * sx + tx, ReportLab_y = svg_y * sy + ty.
..."""
범용 SVG 파서를 만드는 것은 매우 어렵지만, Illustrator에서 내보낸 자사 로고 SVG는 절대 좌표 M / L / H / V / C / Z만을 사용한다는 것을 알고 있으므로, 대응하는 커맨드는 이것만으로 충분합니다. "라이브러리의 결점을 자체 구현으로 제한적으로 우회한다"는 결단입니다.
이를 통해 ReportLab 네이티브(native)의 clipPath와 linearGradient를 조합하여, 왼쪽 하단에서 오른쪽 상단으로 이어지는 CI 그라데이션을 완벽하게 재현할 수 있었습니다.
def draw_logo_gradient(c, x_right, y_bottom, target_w_pt):
sx, sy, tx, ty, target_h = _transform(x_right, y_bottom, target_w_pt)
bbox_left = tx
...
글자 패스로 클리핑(Clipping)을 한 뒤 그라데이션으로 채우는 방식입니다. SVG의 fill=url(#linearGradient...)와 동일한 처리를 ReportLab의 문법으로 번역한 셈입니다.
4. 폰트 크기 하한선 가드(Guard)
이것은 저가형 오프셋 인쇄(Offset printing)의 물리적인 제약에서 비롯된 가드입니다.
# MHT 디자인과 같은 저가형 오프셋 업체는 용지가 저렴하고
# 잉크 번짐(Dot gain)이 심하다.
# 일반적인 업계 하한선(본문 6pt)에서는
# 흐릿함이나 뭉침 리스크가 있으므로, 본 프로젝트에서는 **7pt를 절대 하한선**으로 정한다.
...
나중에 누군가가 "글자를 조금 더 작게 해도 되지 않을까요?"라고 말하며 6pt로 설정했을 때, 인쇄되어 나온 명함을 보고 "글자가 안 보이는데..."라고 당황하는 상황을 방지하기 위한 가드입니다. 도메인 지식(Domain knowledge)을 코드에 내장하는 타입의 유효성 검사(Validation)입니다.
「코드 레벨에서 디자이너를 대신한다」는 발상. 실제 인쇄물이 완성되기까지 피드백에 2주 이상 소요되는 영역이기에, 이러한 정적인 제약 사항(Static constraints)을 넉넉하게 설정해 두었습니다.
5. 데이터는 Pydantic으로 타입 안전(Type-safe)하게
CSV도 TOML도 Pydantic으로 스키마(Schema)화했습니다. email: EmailStr과 같이 타입을 한 줄 적는 것만으로도, hoge@@example과 같은 잘못된 이메일 주소를 파싱(Parsing) 단계에서 걸러낼 수 있습니다. 정규 표현식(Regular expression)을 직접 작성할 필요가 없습니다.
class Employee(BaseModel):
model_config = {"extra": "ignore"} # name_kana 등 정의되지 않은 컬럼은 무시
employee_id: Optional[str] = None
...
extra="ignore" 설정을 통해, CSV에 name_kana와 같이 "PDF에는 사용하지 않지만 사내 관리용으로 가지고 있고 싶은" 컬럼이 섞여 있어도 무시해 줍니다. 사내 스프레드시트를 그대로 내보내기(Export) 해도 파싱할 수 있는 유연성을 기본값으로 설정했습니다.
load_csv는 모든 행을 한 번에 파싱한 후 에러를 모아서 반환하는 방식으로 구현했습니다. "1행의 에러로 중단 → 수정 후 재실행 → 2행의 에러로 중단"되는 지옥을 피하기 위해서입니다.
for line_no, row in enumerate(reader, start=2):
row = {k: (v or "").strip() for k, v in row.items()}
if skip_incomplete and not _is_row_complete(row):
...
에러를 축적해 두었다가 마지막에 한꺼번에 던집니다. 이렇게 하면 CSV를 수정할 때의 인지 부하(Cognitive load)가 크게 줄어듭니다.
설정의 분리: config.toml이 회사의 "정의"
가변 데이터는 CSV, 반고정 값은 TOML, 로직은 코드로 작성했습니다. config.toml은 다음과 같은 형태입니다.
[colors]
# CMYK [C, M, Y, K] (0.0-1.0). MHT 입고는 CMYK만 가능.
black = [0.0, 0.0, 0.0, 1.0]
...
사무실을 이전했을 때, CI 색상을 바꾸고 싶을 때, 대표 전화번호를 바꾸고 싶을 때, 코드를 건드리지 않고 TOML만 수정하면 전 직원의 PDF가 새로운 정보로 재생성됩니다.
색상을 처음부터 CMYK 값으로 적어두는 것도 포인트입니다. RGB에서 CMYK로의 변환은 가역 변환(Reversible conversion)이 아니기 때문에, 디자이너(및 상담한 AI)가 의도한 CMYK를 그대로 입고(Submission) 할 수 있도록 했습니다.
employees.csv는 인사 시스템의 인터페이스
employee_id,name_ja,name_kana,name_en,title_ja,title_en,email,direct_phone,filename
,山田 太郎,やまだ たろう,Taro Yamada,代表取締役 CEO,Chief Executive Officer,taro.yamada@example.com,,
,佐藤 花子,さとう はなこ,Hanako Sato,取締役 CTO,Chief Technology Officer,hanako.sato@example.com,,
...
employee_id 열은 본 기사에서 가려두었습니다(실제 운용 시에는 여기에 사원 번호가 들어갑니다). employee_id를 넣으면 출력 파일명이 <id>_<name_en>.pdf 형식으로 지정되며, 사원 번호 순으로 정렬된 상태로 ZIP 파일에 담깁니다.
이 CSV를 인사 시스템 측에서 내보내기(Export) 하면 그대로 명함 생성의 입력값(Input)이 됩니다. 현재는 수동 커밋(Commit) 방식이지만, 향후에는 freee 인사노무나 SmartHR로부터 API를 통해 가져와 자동 커밋까지 구현하고 싶습니다.
AI를 통한 디자인 리뷰: 3인 리뷰어 체제
여기서부터는 "심플한 명함을 AI에게 디자인하게 하면, 디자이너 없이도 운영이 가능할까?"라는 실험 파트입니다.
결론부터 말씀드리면, 한 번에 깔끔하게 만들어주지는 않았습니다. 하지만 여러 LLM(Large Language Model)에게 리뷰를 시키면, 최종적으로 전문가 수준의 지적 사항이 나옵니다.
리뷰어로는 3명을 기용했습니다.
| 리뷰어 | 역할 |
|---|---|
| Google Gemini 3.1 Pro | 거시적인 관점. 레이아웃의 위화감을 빠르게 탐지 |
Claude Code frontend-design Skill | 전문 프론트엔드 엔지니어 관점. 색상 대비, 타이포그래피 (Typography) 기본 규칙 |
| Claude Opus 4.7 | 코드 리딩 (Code Reading)을 하며 「구현상의 근거」를 바탕으로 한 지적 |
AI로부터 나온 구체적인 지적
채택한 지적 사항 중 일부를 소개합니다.
「아이콘과 글자의 베이스라인 (Baseline)이 맞지 않음」
첫 번째 구현에서는 이메일 아이콘과 글자의 세로 위치가 미묘하게 어긋나 있었습니다. 아이콘은 이미지의 중앙을 기준으로 배치한 반면, 글자는 베이스라인 (Baseline) 기준이었기 때문에 시각적으로 일치하지 않았습니다.
# 연락처 (아이콘은 본문의 x-height에 맞춰 3.0mm)
line_h = mm(4.5)
icon_size = mm(3.5)
...
+ mm(0.8)
이라는 미세한 오프셋 (Offset)은 아이콘과 글자의 시각적 베이스라인 (Baseline) 을 맞추기 위한 조정입니다. x-height를 기준으로 하고 있어, 대문자 (Capital Letter)와 아이콘의 중심이 나란히 보이도록 설정했습니다.
「컬럼 (Column) 내에서 베이스라인 (Baseline)이 맞지 않음」
앞면의 연락처와 뒷면의 연락처에서 행 높이 (Line-height)가 들쑥날쑥하다는 지적입니다.
# 행 높이 4.0mm로 뒷면과 세로 리듬을 통일
line_h = mm(4.5)
line_h를 양면 모두 동일한 값으로 설정함으로써 앞뒤의 세로 리듬을 맞추었습니다. 양면 인쇄 후에 겹쳤을 때, 비쳐 보이는 연락처의 위치가 일치하도록 배려한 세심한 조치입니다.
「글자의 대비 (Contrast)가 부족하여 인식하기 어려움」
뒷면의 주소가 검은색 글자라면 「성명 (오렌지색) → 연락처 (오렌지색) → 회사명·주소 (검은색)」 순으로 정보 우선순위의 단계가 무너진다는 지적입니다.
# 5) 회사명·주소 (웜 차콜 (Warm Charcoal): 연락처보다 정보 우선순위를 낮춤)
bottom_y = oy + mm(constants.CARD_MARGIN_BOTTOM_MM)
c.setFillColor(colors.BACK_SUBTLE_DARK)
...
back_subtle_dark = [0.0, 0.10, 0.20, 0.78]의 CMYK 값, 즉 K 단색이 아니라 웜 차콜 (Warm Charcoal, CMY에 색을 조금씩 섞은 진한 회색) 을 채택했습니다. 시인성은 유지하면서 성명의 오렌지색을 돋보이게 하는 역할입니다.
색상 이름을 back_subtle_dark와 같이 의도(Intent) 기반으로 명명한 것도 포인트입니다. 「검정」이 아니라 「보충 정보용의 차분한 어두운 색」이라는 의미를 담았습니다. 코드를 다시 읽었을 때 이 색상이 무엇을 위해 존재하는지 한눈에 알 수 있습니다.
「가는 글꼴 웨이트 (Weight)는 저가형 오프셋 인쇄에서 사라짐」
Light/Thin 웨이트는 선이 가늘기 때문에, 저렴한 종이와 오프셋 인쇄 (Offset Printing)를 사용할 경우 번지거나 사라질 수 있다는 지적입니다. 이를 상수의 주석으로 영구히 남겨두었습니다.
# 가는 글씨 (Light/Thin 웨이트)는 동일한 크기라도 선이 사라지기 쉬우므로
# Regular/Bold만 사용할 것.
문서에 적어두면 잊히기 쉬우므로, 사용 직전의 코드에 경고로 배치하는 것이 포인트입니다. assert_min_font와 같은 런타임 검증 (Runtime Validation)과 병행하여 사용합니다.
한 번에 깔끔하게 되지 않았던 이야기
솔직히 말해서, 처음에는 처참했습니다.
- 로고가 왜곡됨 (svglib의 linearGradient 문제)
- 트림 마크 (Trim Mark)가 업체 템플릿과 1.5mm 어긋남 (좌표계 변환 실수)
- 폰트 크기가 제각각임
- 앞뒤 위치가 어긋남
이를 「Gemini 3.1 Pro로 지적을 받음 → Claude Code로 구현 → Opus 4.7으로 구조 리뷰 → frontend-design Skill으로 비주얼 리뷰」의 사이클을 5~6회 반복한 끝에 겨우 입고 가능한 수준에 도달했습니다.
「AI라면 한 번에 만들 수 있겠지?」라는 말은 큰 착각입니다. 도메인 지식 (Domain Knowledge)을 가진 인간이 리뷰 관점을 돌릴 수 있느냐에 따라 품질이 결정됩니다. 이번 사례의 경우 「인쇄물로서의 명함 제약 사항 (CMYK, 트림 마크, 망점 팽창 (Dot Gain))」을 제가 알고 있었고, 그것을 AI에게 던질 질문으로 번역할 수 있었던 것이 핵심이었습니다.
GitHub Actions로 빌드 자동화
CSV를 업데이트하면 모든 인원의 PDF가 자동으로 생성되도록 설정했습니다.
name: Build name cards
on:
push:
...
고안한 점은 두 가지입니다.
paths:를 사용하여 CSV, config, 코드 중 하나라도 변경되었을 때만 워크플로우를 실행합니다. README의 오타 수정 때문에 PDF를 다시 생성하지 않도록 했습니다.- **Workflow Artifacts (90일 보관)**와 GitHub Releases (영구 보관) 양쪽 모두에 출력합니다. 신입 사원이 입사하여 명함을 주문하고 싶을 때, 언제든 Release 목록에서 최신 ZIP 파일을 가져올 수 있습니다.
GitHub Releases에 영구 보관해 두는 것이 은근히 중요한데, 이는 과거의 명함 버전(주소 변경 전, CI 색상 변경 전 등)에 언제든 접근할 수 있는 디자인의 역사 기록 역할도 합니다.
입사 온보딩(Onboarding)으로의 응용
여기까지 만들고 나면, 다음으로는 입사 워크플로우 전체의 자동화로 연결하고 싶어집니다.
이상적인 형태는 이렇습니다. 인사 시스템이 '입사 1개월 전' 타이밍에 employees.csv로 PR(Pull Request)을 자동으로 생성하고, 머지(Merge)되면 인쇄 업체 API를 호출하여 발주한 뒤, Slack으로 알림이 흐르는 방식입니다. 이렇게 되면 사람의 손길은 완전히 사라집니다.
저희가 이용하는 업체에는 아직 API가 없어서 'ZIP을 다운로드하여 업로드'하는 마지막 한 단계는 사람이 필요하지만, 이 부분만 분리하여 에스컬레이션(Escalation)하는 것만으로도 운영은 가능합니다.
벤더(Vendor) 직속 발주 API가 정비된다면, 이는 완전한 무인화에 도달하게 됩니다. Vista Print 같은 글로벌 업체가 일본에 들어와 준다면 이야기가 빠르겠지만, 뭐 느긋하게 기다려 보겠습니다.
규모감: 코드 라인 수와 '사람이 작성한다면?'의 개산(Rough Estimate)
"이 프로젝트의 규모가 어느 정도인가요?"라고 궁금해하실 분들을 위해, 라인 수와 공수의 개산을 정리해 두겠습니다.
라인 수
| 구분 | 라인 수 | 내용 |
|---|---|---|
| 소스 코드 (Python) | 1,122행 | src/minedia_namecard/ 하위 16개 파일 (CLI / CSV/TOML 로더 / 프론트·백 렌더러 / 도련(Trim marks) 추출 / 로고 SVG 파서 / Ghostscript 래퍼 등) |
| 테스트 코드 | 494행 | tests/ 하위 9개 파일 (CSV 검증 / 레이아웃 상수 / 렌더링 smoke 테스트 / 통합 테스트 등) |
| 설정·CI·Makefile | 174행 | config.toml / pyproject.toml / Makefile / GitHub Actions workflow |
| 합계 | 약 1,790행 |
소스 코드의 내역을 보면 로고 SVG 파서(176행)와 표면 렌더러(141행)의 비중이 높습니다. 로고의 linearGradient 재현과 디자인 미세 조정이 쌓인 결과입니다.
'사람이 작성한다면?'의 개산
제 체감 기준으로 "AI 어시스트 없이, Python + ReportLab에 익숙한 중견 엔지니어가 혼자 작성한다"고 가정했을 때의 공수 개산은 다음과 같습니다.
| 페이즈 | 예상 공수 | 내용 |
|---|---|---|
| ReportLab / CMYK / DTP 입고 사양 조사 및 Spike | 0.5~1인일 | ReportLab의 CMYK 출력, 도련(Bleed) 3mm, 도련(Trim marks) 사양, Ghostscript의 -dNoOutputFonts 등을 가볍게 시행착오 |
| ... | 합계: 약 5인일 | 집중 시간 환산 시 약 40시간, 실제 달력 기준 1~2주 |
실제 인쇄 과정에서의 피드백은 별도로, 입고 → 납품의 물리적 리드 타임(Lead time)으로 1~2주가 추가되지만, 이는 사람이나 AI나 동일하므로 공수에는 포함하지 않았습니다.
실제로 걸린 시간
이와 대조적으로, 이번 시도는 AI 에이전트 (Claude Code / Gemini)와 병행하며 실제 시간 약 2시간 만에 1.0 버전에 도달했습니다. 리뷰 라운드를 5~6회 거친 시간을 포함한 수치입니다.
사람이 혼자 했을 경우의 **약 5인일 (집중 시간 환산 시 약 40시간)**이 단 2시간으로 압축된 계산이 됩니다. 대략 20배의 가속입니다.
솔직히 저 자신도 이 수치를 입력하면서 "아니, 그럴 리가 없는데..."라며 두 번 확인했지만, 다시 돌아봐도 이 수치가 맞습니다. 제가 한 일은 "Claude Code에게 작업 디렉토리에서 구현하게 하기", "중간에 다른 LLM (Gemini 1.5 Pro / frontend-design Skill)에게 리뷰시키기", "지적 사항을 해당 LLM에게 다시 전달하기"라는 루프를 돌린 것뿐이며, 저 자신이 에디터를 열어 직접 손으로 작성한 코드는 거의 제로에 가깝습니다.
무엇이 가속화되었는지 분석하면 다음과 같습니다.
- 기존 API에 대한 초기 대응 속도: ReportLab이나 PyMuPDF의 API를 "일단 이렇게 작성한다"는 초안이 거의 즉시 나옵니다.
- 시행착오의 총량: 하루에 10~20회 정도 렌더링 결과를 확인하고 피드백을 보내는 사이클을 돌릴 수 있습니다.
- 디자인 리뷰의 대체: 사내 디자이너가 없는 약점을 프론트엔드 디자인 계열의 Skill로 부분적으로 보완합니다.
- 문서 대용의 구현: "아이콘과 글자의 베이스라인(Baseline)을 맞춰야 한다"와 같은 **디자이너의 암묵지 (Implicit Knowledge)**가 주석이 달린 코드로 기록됩니다.
반대로, AI가 약했던 부분은 다음과 같습니다.
- 물리적 출력에 대한 피드백: 종이에 인쇄했을 때의 번짐이나 흐릿함은 AI가 볼 수 없습니다.
MIN_FONT_SIZE_PT = 7.0과 같은 **물리적 제약 기반의 도메인 지식 (Domain Knowledge)**은 인간이 코드에 심어야 합니다. - 입고 사양의 정확한 해석: 업체의 템플릿을 "실측하여 재현한다"는 발상은 인간이 "이것이 정답이다"라고 가르쳐주지 않으면 도달할 수 없었습니다.
설계 판단 복습
마지막으로, 이 구현을 하는 과정에서 반복적으로 의식했던 점을 정리해 두겠습니다.
1. 데이터·설정·코드를 완전히 분리하기
CSV 업데이트만으로 운영이 가능하도록 설계하려면, 코드 안에 성함이나 이메일 주소가 하드코딩 (Hard-coded) 되어 있어서는 안 됩니다. 반대로, 업체별 종이 크기나 도련(Trim marks) 위치는 좀처럼 바뀌지 않으므로 코드(또는 업체 템플릿)에 두었습니다. "변경 빈도"에 따라 배치 레이어를 결정합니다.
2. 도메인 지식을 코드에 심기
AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기