edmTestRecipients) {
+ this.edmTestRecipients = edmTestRecipients == null ? new ArrayList<>() : edmTestRecipients;
+ }
+
+ public int getEdmCampaignCap() {
+ return edmCampaignCap;
+ }
+
+ public void setEdmCampaignCap(int edmCampaignCap) {
+ this.edmCampaignCap = edmCampaignCap;
+ }
+
+ public int getEdmRatePerMinute() {
+ return edmRatePerMinute;
+ }
+
+ public void setEdmRatePerMinute(int edmRatePerMinute) {
+ this.edmRatePerMinute = edmRatePerMinute;
+ }
}
diff --git a/src/backend/src/main/java/com/zioinfo/kintex/mail/MailTemplates.java b/src/backend/src/main/java/com/zioinfo/kintex/mail/MailTemplates.java
index 427f3cb..7474bf2 100644
--- a/src/backend/src/main/java/com/zioinfo/kintex/mail/MailTemplates.java
+++ b/src/backend/src/main/java/com/zioinfo/kintex/mail/MailTemplates.java
@@ -43,8 +43,19 @@ public final class MailTemplates {
+ WRAP_FOOT;
}
- /** EDM 광고 본문 — 캠페인 명 + 안내 + 정보통신망법 수신거부 문구. */
+ /** EDM 광고 본문(수신거부 링크 없음 — 하위호환). 원클릭 수신거부는 {@link #edm(String, String, String)} 사용. */
public static String edm(String campaignName, String eventName) {
+ return edm(campaignName, eventName, null);
+ }
+
+ /**
+ * EDM 광고 본문 — 캠페인 명 + 안내 + 정보통신망법 수신거부 문구 + 원클릭 수신거부 링크.
+ * {@code unsubUrl} 이 있으면 하단에 수신거부 링크를 삽입한다(불투명 토큰 링크 — PII 미포함).
+ */
+ public static String edm(String campaignName, String eventName, String unsubUrl) {
+ String unsubLine = (unsubUrl == null || unsubUrl.isBlank())
+ ? "수신을 원치 않으시면 마이페이지 > 수신설정에서 수신거부하실 수 있습니다."
+ : "수신을 원치 않으시면 수신거부 하실 수 있습니다.";
return WRAP_HEAD
+ "(광고) " + esc(eventName) + "
"
+ "" + esc(campaignName) + "
"
@@ -52,8 +63,7 @@ public final class MailTemplates {
+ "자세한 내용과 사전등록은 아래에서 확인하실 수 있습니다."
+ "
"
+ "본 메일은 수신동의(마케팅 정보 수신)를 하신 분께 발송되었습니다. "
- + "수신을 원치 않으시면 마이페이지 > 수신설정에서 수신거부하실 수 있습니다. "
- + "(정보통신망 이용촉진 및 정보보호 등에 관한 법률 준수)
"
+ + unsubLine + " (정보통신망 이용촉진 및 정보보호 등에 관한 법률 준수)"
+ "KINTEX 전시운영 시스템 · 발신 전용
";
}
diff --git a/src/backend/src/main/java/com/zioinfo/kintex/marketing/MarketingController.java b/src/backend/src/main/java/com/zioinfo/kintex/marketing/MarketingController.java
index 89b65d7..9efa469 100644
--- a/src/backend/src/main/java/com/zioinfo/kintex/marketing/MarketingController.java
+++ b/src/backend/src/main/java/com/zioinfo/kintex/marketing/MarketingController.java
@@ -46,14 +46,19 @@ public class MarketingController {
return ApiResponse.ok(service.createCampaign(eventId, req));
}
- /** 캠페인 발송(수신동의자 한정) — 매니저 이상. 정보통신망법 준수·회당 상한·배치 분할은 서비스가 강제. */
+ /**
+ * 캠페인 발송(수신동의자 한정) — 매니저 이상. 정보통신망법 준수·회당 상한·rate cap·수신거부 링크는 서비스가 강제.
+ * 기본은 테스트 모드(실 수신자 미발송) — 운영 발송은 요청 본문 {@code live=true} 명시 + 서버 설정(EDM_TEST_MODE=false) 필요.
+ */
@Audited(action = "CAMPAIGN_SEND", targetType = "edm_campaign")
@PostMapping("/campaigns/{id}/send")
public ApiResponse sendCampaign(@AuthenticationPrincipal KintexPrincipal principal,
@PathVariable String eventId,
- @PathVariable String id) {
+ @PathVariable String id,
+ @RequestBody(required = false) CampaignSendRequest req) {
guard.requireRole(principal, eventId, EventRole.ORGANIZER, EventRole.HALL_MANAGER);
- return ApiResponse.ok(service.sendCampaign(eventId, id));
+ Boolean live = req == null ? null : req.live();
+ return ApiResponse.ok(service.sendCampaign(eventId, id, live));
}
/** 스폰서십 패키지·판매 현황. */
diff --git a/src/backend/src/main/java/com/zioinfo/kintex/marketing/MarketingMapper.java b/src/backend/src/main/java/com/zioinfo/kintex/marketing/MarketingMapper.java
index e166d57..1e19f40 100644
--- a/src/backend/src/main/java/com/zioinfo/kintex/marketing/MarketingMapper.java
+++ b/src/backend/src/main/java/com/zioinfo/kintex/marketing/MarketingMapper.java
@@ -53,29 +53,64 @@ public interface MarketingMapper {
Map findCampaign(@Param("eventId") String eventId, @Param("id") String id);
/**
- * 발송 대상 이메일(수신동의 = agree_marketing=true, 이메일 보유). 원문 이메일은 발송 경로 한정 —
- * 응답/로그에 노출하지 않는다. 회당 상한(limit) 가드는 서비스가 적용.
+ * 발송 대상(수신동의 = agree_marketing=true, 미수신거부 = marketing_unsubscribed=false, 이메일 보유).
+ * 원문 이메일은 발송 경로 한정 — 응답/로그에 노출하지 않는다(§0-3). 각 행에 불투명 수신거부 토큰(unsubToken) 동봉.
+ * 회당 상한(limit) 가드는 서비스가 적용. 행사 단위 이메일 유니크(uq_visitor_reg_event_email)로 중복 없음.
*/
@Select("""
- SELECT DISTINCT lower(email) AS "email"
+ SELECT lower(email) AS "email", unsub_token AS "unsubToken"
FROM visitor_registration
WHERE event_id = #{eventId}
AND agree_marketing = true
+ AND marketing_unsubscribed = false
AND email IS NOT NULL AND email <> ''
ORDER BY lower(email)
LIMIT #{limit}
""")
List