Notice
Recent Posts
Recent Comments
반응형
관리 메뉴

HM소프트

대량 메일 자동화 개발기 (2) — 수신자별 PDF 첨부를 단계별로 쌓기 본문

기술 개발일지

대량 메일 자동화 개발기 (2) — 수신자별 PDF 첨부를 단계별로 쌓기

HM소프트 2026. 6. 11. 17:11
반응형

수신자마다 다른 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 배포로 시리즈를 마친다.

반응형