| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
- javascript
- JWT
- typescript
- Kotlin
- github actions
- FastAPI
- 메일자동화
- php
- 프론트엔드
- 보안
- 도메인설계
- 백엔드
- 온디바이스AI
- REST API
- Android
- python
- 인증
- 상태관리
- contextBridge
- 배포
- 크롤링
- Vite
- 아키텍처
- React
- 소셜로그인
- sqlite
- next.js
- 마이그레이션
- playwright
- electron
- Today
- Total
HM소프트
대량 메일 자동화 개발기 (2) — 수신자별 PDF 첨부를 단계별로 쌓기 본문

수신자마다 다른 PDF를 붙이는 첨부 기능을 스키마·업로드·값 스캔·상태 관리·발송 다섯 단계로 쪼개 구현한다. 엑셀 한 컬럼을 매칭 키로 삼는 컬럼→값→파일 3단 구조, UUID 저장과 원본 파일명 복원, 업로드 디렉토리 누락 버그, 프론트 camelCase와 백엔드 snake_case 직렬화 불일치로 첨부가 조용히 빠지던 사고와 해결을 다룬다.
2편: 수신자별 PDF 첨부
1편에서 템플릿 치환을 다뤘다. 이번 편은 이 도구에서 가장 공들인 기능이다. 단순히 같은 PDF를 모두에게 붙이는 게 아니라, 수신자마다 다른 PDF를 매칭해서 첨부한다. 예를 들어 각 거래처에 그 거래처용 견적서 PDF를 자동으로 붙이는 식이다.
기능 하나지만, 프론트와 백엔드에 걸쳐 여러 조각이 맞물려야 했다. 한 번에 만들지 않고 단계별로 쌓았다.
1) 스키마와 업로드 디렉토리
먼저 PDF 첨부의 데이터 모양(스키마)을 정의하고, 서버에 PDF 전용 업로드 디렉토리를 마련했다.
여기서 한 번 막혔던 디테일: 서버 시작 시 업로드 디렉토리를 생성하는 목록에 PDF 하위 디렉토리를 빼먹어서 업로드가 실패했다. "디렉토리가 없어서 저장 실패"는 로그를 안 보면 원인을 못 찾는 류라, 시작 시 생성 목록에 명시적으로 추가해 해결했다.
2) 업로드 API + 드롭존 UI
백엔드에 PDF 업로드 엔드포인트를 만들고, 프론트엔드엔 드래그앤드롭 드롭존을 구현했다. 파일을 끌어다 놓으면 업로드되고, 목록에 쌓인다.
// 드롭존 — 파일을 받아 업로드 API로
function PdfDropzone({ onUploaded }: Props) {
const onDrop = async (files: FileList) => {
for (const file of files) {
const form = new FormData();
form.append("pdf", file);
const res = await api.pdf.upload(form); // POST /pdf/upload
onUploaded(res);
}
};
// dragover/drop 이벤트 바인딩 ...
}
3) 누구에게 어떤 PDF를? — 컬럼 값 스캔
핵심 난제는 "어떤 PDF를 누구에게 붙이느냐"였다. 이걸 표의 컬럼 값으로 매칭한다. 예를 들어 파일명이나 특정 컬럼 값과 PDF를 연결한다. 이를 위해 PDF 컬럼 값을 스캔하는 API를 두어, 매칭 기준이 될 값들을 뽑아냈다.
4) 상태 관리와 API 클라이언트
프론트엔드 store에 PDF 첨부 상태를 추가하고, API 클라이언트 함수를 붙였다. 어떤 PDF가 업로드됐는지, 어떤 컬럼으로 매칭하는지가 전역 상태로 관리된다.
여기서 프론트-백엔드 간 필드명 규칙이 문제를 일으켰다. 프론트는 camelCase, 백엔드는 snake_case를 쓰는데, PDF 필드명이 어긋나 "붙는 줄 알았는데 안 붙는" 버그가 났다. 필드명을 snake_case로 통일하고, API 타입과 URL을 정리해 맞췄다. 이런 경계의 불일치가 첨부 기능에서 가장 흔한 버그의 원천이었다.
5) 발송 연동
마지막으로 메일 서비스에 PDF 첨부 파라미터를 추가해, 실제 발송 시 매칭된 PDF가 붙도록 했다. 그리고 작성기(MailComposer)에 PDF 첨부 섹션을 통합해, 작성 화면에서 한 번에 설정하게 했다.
def send_mail(to, subject, body, pdf_path: str | None = None):
msg = build_message(to, subject, body)
if pdf_path:
attach_pdf(msg, pdf_path) # 수신자별로 매칭된 PDF
smtp.send(msg)
단계별로 쌓은 이유
이 기능을 한 덩어리로 만들었다면 "어디서 끊겼는지" 알기 어려웠을 것이다. 스키마 → 업로드 → 매칭 → 상태 → 발송으로 각 단계를 독립적으로 만들고 검증하니, 문제가 생겨도 어느 단계인지 좁히기 쉬웠다. 실제로 디렉토리 누락(1단계)과 필드명 불일치(4단계) 버그를 각 단계 안에서 빠르게 잡을 수 있었다.
기능은 통합 테스트로 발송 흐름 전체를 검증하고 문서화했다.
정리하며
- 단계별로 쌓기(스키마→업로드→매칭→상태→발송)로 복잡한 기능을 검증 가능한 조각으로 나눴다.
- 프론트-백 필드명 통일(snake_case)이 경계 버그를 잡는 핵심이었다.
- "디렉토리 생성 목록에 추가" 같은 작은 디테일이 조용한 실패를 만든다 — 명시적으로.
다음 마지막 3편에서는 프론트/백 경계 정리와 Docker 배포로 시리즈를 마친다.
'기술 개발일지' 카테고리의 다른 글
| 에디토리얼 스타일 UI와 브랜드 아이덴티티 설계 — 장부 관리 앱 (1) (0) | 2026.06.12 |
|---|---|
| 프론트/백 경계 정리와 Docker 배포 — 대량 메일 자동화 (3) (0) | 2026.06.11 |
| 대량 메일 템플릿 변수 치환 + 값 미리보기 엔진 (1) (0) | 2026.06.11 |
| 크롤러 데이터 검증·검색·스케줄러·데스크톱앱화 — 공개 데이터 크롤러 (4) (0) | 2026.06.11 |
| cross-PC 강건성 잡기 — '내 PC에서는 되는데' 크롤러 (3) (0) | 2026.06.11 |