Repository navigation
[FEAT] 단체 메일 발송 및 발송 이력 기록 #629
Copy link
Copy link
Open
2 / 22 of 2 issues completedLabels
👨🏻💻backend백엔드 작업백엔드 작업
Description
목표
운영자가 한 모집 공고에서 선택한 지원자들에게 하나의 메일 템플릿으로 단체 메일을 발송하고, 발송 작업과 수신자별 결과를 남길 수 있도록 합니다.
예약 발송이 아니라 요청 시점에 실행되는 단체 메일입니다. 템플릿 원본과 이번 발송에서 사용할 제목·입력값을 구분해야 합니다.
요청 및 실행 계약
- 관리자용 API를
/admin/mails/**아래에 추가합니다. 발송 요청에는recruitId,scenarioId,applyIds, 선택적인 제목 덮어쓰기 값, 입력 변수 맵이 포함되어야 합니다. - 선택 대상은 모두 요청한 모집 공고에 속한 제출 완료 지원이어야 합니다. 다른 모집 공고의 ID, 중복 ID, 미제출·반려·삭제 지원이 섞이면 외부 발송 전에 전체 요청을 거부합니다.
- 대상자는 최소 1명, 최대 500명입니다. 최대치를 초과하면 일부만 발송하지 말고 요청 전체를 거부합니다.
- 비활성 템플릿은 발송할 수 없습니다.
- 제목 덮어쓰기는 이번 발송에만 적용하며
MailScenario원본을 수정하지 않습니다. 덮어쓰기가 없으면 템플릿 제목을 사용합니다. - 최종 제목은 공백을 제거한 길이가 2~40자여야 합니다.
- 필수 입력 변수, 선언되지 않은 입력 변수,
DATE_TIME등 타입 오류, 시스템 변수 미해결 여부는 발송 전에 대상자 전원에 대해 검증합니다. - 검증 단계에서 대상자별 제목·본문을 모두 렌더링하고 하나라도 실패하면 외부 메일 서비스 호출을 시작하지 않습니다.
- 발송 작업 생성과 외부 메일 호출의 트랜잭션 경계를 분리합니다. 작업 상태와 대상 상태를 저장하고, 같은 작업이 이미 실행 중이거나 완료된 경우 중복 실행하지 않습니다.
- 외부 메일 서비스 호출 중 한 수신자가 실패해도 나머지 대상 처리를 계속합니다. 수신자별 성공·실패와 실패 사유를 기록하고 성공 건수·실패 건수를 응답 또는 작업 결과에서 확인할 수 있어야 합니다.
이력 데이터 계약
발송 작업 단위와 수신자 단위를 분리해 저장합니다.
발송 작업에는 다음을 기록합니다.
- 작업 ID와 상태
- 사용한 템플릿 ID(필요하면 시나리오 코드·이름 snapshot)
- 모집 공고 ID
- 대상 수
- 요청한 관리자 ID(인증 주체에서 취득)
- 작업 요청·시작·종료 시각
- 성공·실패·처리 중 건수
수신자별 기록에는 다음을 기록합니다.
- 작업 ID
applyId와 발송 시점의 이메일 주소 snapshot- 대상 처리 상태
- 성공 시각 또는 실패 사유
외부 발송이 완료된 뒤 템플릿을 수정해도 과거 작업의 대상·주소·성공 여부를 확인할 수 있어야 합니다. 이미 전송된 메일을 DB 트랜잭션 rollback으로 되돌릴 수 있다고 가정하지 않습니다.
API 및 구현 경계
- 동기·비동기 실행 방식은 현재 외부 메일 어댑터와 관리자 화면의 흐름에 맞추되, 요청이 반환된 뒤에도 작업 상태와 수신자별 결과를 조회할 수 있어야 합니다.
- 기존 단건 메일 발송 서비스와 rate limit을 재사용합니다. 외부 서비스의 제한을 우회하기 위해 병렬 요청을 무제한으로 만들지 않습니다.
- 메일 본문은 지원자별 시스템 변수(
name, 모집 공고 기수, 예비 번호)와 공통 입력 변수를 병합해 렌더링합니다. 시스템 변수는 요청의 입력 변수로 덮어쓸 수 없습니다. - API 경로, DTO, 상태 enum은 기존 관리자 메일 코드와 호환되는 이름을 우선하고, 최종 계약을 Swagger와 PR 설명에 남깁니다.
완료 조건
- 선택한 1~500명의 제출 완료 지원자에게 개인화된 제목·본문이 발송된다.
- 선택 대상이 비어 있거나 모집 공고·지원 상태가 맞지 않으면 발송되지 않는다.
- 발송 전 전체 렌더링과 변수 검증이 수행되고, 실패 시 외부 호출이 0건이다.
- 제목 덮어쓰기가 템플릿 원본을 변경하지 않는다.
- 2자 미만·40자 초과 제목과 필수 입력 누락이 거부된다.
- 일부 수신자 실패가 나머지 발송을 중단하지 않는다.
- 발송 작업과 수신자별 성공·실패·실패 사유·이메일 snapshot이 저장된다.
- 요청 관리자와 발송 시각을 확인할 수 있다.
- 같은 작업의 중복 실행이 방지된다.
- rate limit을 준수한다.
- 단위, 서비스, API, 저장소/통합 테스트로 전체 성공·전체 실패·부분 실패·사전 검증 실패를 검증한다.
이번 이슈에서 하지 않는 것
- 미래 시각을 저장하는 예약 발송 및 스케줄러
- 발송 이력 목록·상세·실패 대상 조회 API
- 발송 취소 또는 이미 전송된 메일 회수
- 자동 재시도 정책과 별도 워커/인프라 구축
- 첨부파일, 템플릿 버전 관리, 레거시 고정형 메일 플로우 제거
선행 작업 및 참고
- [FEAT] 메일 발송용 모집 공고 목록 조회 #624 (모집 공고 선택)
- [FEAT] 메일 발송 대상자 조회 #627 (발송 대상 조회와 제출 지원 기준)
- [FEAT] 시스템 변수 해석 및 메일 미리보기 #628 (수신자별 렌더링·변수 검증)
- #626의 입력 변수 타입 검증을 재사용합니다.
- 발송 이력 용어와 단체 메일 정의는
CONTEXT.md를 따릅니다.
범위 확정
- 이 이슈의 완료 범위는 요청 시점 즉시 발송, 발송 작업·수신자별 결과 저장, 부분 실패 집계입니다.
- 발송 작업·수신자별 결과 조회 API는 후속 이슈 #669에서 다룹니다.
- Outbox·워커·자동 재시도·재기동 복구·idempotency key payload 충돌 검증은 후속 이슈 #670에서 다룹니다.
- 실패 사유는 외부 메일 공급자의 원문이 아닌 안전한 오류 코드로 저장합니다.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
👨🏻💻backend백엔드 작업백엔드 작업
Activity
feat/#629 메일 발송 이력 저장 구조 추가 (#655)
feat/#629 단체 메일 발송 사전 검증 구현 (#659)
feat/#629 단체 메일 발송 실행과 관리자 API 구현 (#660)
feat/#629 메일 발송 이력 저장 구조 (#662)
feat/#629 메일 템플릿 렌더링 공통화 (#666)
feat/#629 단체 메일 발송 사전 검증 구현 (#667)
feat/#629 단체 메일 발송 실행과 관리자 API 구현 (#668)
023-dev commentedon Sep 9, 2026
메일 발송 진행 상황
dev통합 PR 진행 중dev반영 확인 후 종료#629는 #678과 후속 #669·#670 작업이 모두 정리될 때까지 열어둡니다.
feat/#629 단체 메일 발송 MVP dev 통합 (#678)
test: #629 메일 대상 누락과 후반 렌더링 실패 검증 추가
Merge pull request #691 from JECT-Study/feat/629-mail-dispatch-failur…
feat: #670 메일 발송 중복 요청의 본문 일치 검증 구현 (#687)
feat: #670 메일 worker·복구 및 안전한 재시도 dev 통합 (#700)
feat: #670 메일 발송 장애 복구 검증 및 운영 관측 보강 (#702)
test: #670 메일 조회 검증 통합 및 요청 식별 테스트 보강 (#706)
feat: #502 메일 예약 생성·조회 및 취소 기반 구현 (#707)