From 0892bbfb07d9c80f22f54b497c6e2d0b025c62d5 Mon Sep 17 00:00:00 2001 From: zio Date: Tue, 14 Jul 2026 07:09:27 +0900 Subject: [PATCH] =?UTF-8?q?feat(marketing):=20open=20EDM=20live=20send=20?= =?UTF-8?q?=E2=80=94=20double-confirm=20UI=20path=20(live=3Dtrue)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Server EDM_TEST_MODE flipped to false (env, owner request), so the UI gains an explicit live-send button: existing button stays as safe test send, live send requires an audience-count warning confirm plus typing LIVE. sendMut now carries {id, live}; i18n keys x4 locales. Co-Authored-By: Claude Fable 5 --- src/frontend/src/i18n/locales/en.json | 6 +++- src/frontend/src/i18n/locales/ja.json | 6 +++- src/frontend/src/i18n/locales/ko.json | 6 +++- src/frontend/src/i18n/locales/zh.json | 6 +++- .../src/screens/marketing/EdmCampaignPage.tsx | 34 +++++++++++++++---- 5 files changed, 48 insertions(+), 10 deletions(-) diff --git a/src/frontend/src/i18n/locales/en.json b/src/frontend/src/i18n/locales/en.json index 48e64bb..7ca96e5 100644 --- a/src/frontend/src/i18n/locales/en.json +++ b/src/frontend/src/i18n/locales/en.json @@ -2132,7 +2132,11 @@ "sendFail": "Failed to send.", "confirm": "Send the {{name}} campaign?", "sending": "Sending…", - "sendBtn": "Send" + "sendBtn": "Send", + "sendTestBtn": "Test send", + "sendLiveBtn": "Live send", + "confirmLive": "This will send the '{{name}}' campaign to {{audience}} real recipients. This cannot be undone. Continue?", + "confirmLiveType": "Type LIVE to confirm the live send." }, "program": { "title": "Program Management", diff --git a/src/frontend/src/i18n/locales/ja.json b/src/frontend/src/i18n/locales/ja.json index 5d726ad..61894ce 100644 --- a/src/frontend/src/i18n/locales/ja.json +++ b/src/frontend/src/i18n/locales/ja.json @@ -2132,7 +2132,11 @@ "sendFail": "送信に失敗しました。", "confirm": "{{name}} キャンペーンを送信しますか?", "sending": "送信中…", - "sendBtn": "送信" + "sendBtn": "送信", + "sendTestBtn": "テスト送信", + "sendLiveBtn": "本番送信", + "confirmLive": "'{{name}}' キャンペーンを実際の受信者 {{audience}} 名に本番送信します。取り消せません。続行しますか?", + "confirmLiveType": "本番送信を確定するには LIVE と入力してください。" }, "program": { "title": "プログラム管理", diff --git a/src/frontend/src/i18n/locales/ko.json b/src/frontend/src/i18n/locales/ko.json index f6a8cbd..51cb256 100644 --- a/src/frontend/src/i18n/locales/ko.json +++ b/src/frontend/src/i18n/locales/ko.json @@ -2132,7 +2132,11 @@ "sendFail": "발송에 실패했습니다.", "confirm": "{{name}} 캠페인을 발송하시겠습니까?", "sending": "발송 중…", - "sendBtn": "발송" + "sendBtn": "발송", + "sendTestBtn": "테스트 발송", + "sendLiveBtn": "운영 실발송", + "confirmLive": "'{{name}}' 캠페인을 실제 수신자 {{audience}}명에게 운영 발송합니다. 되돌릴 수 없습니다. 계속하시겠습니까?", + "confirmLiveType": "운영 발송을 확정하려면 LIVE 를 입력하세요." }, "program": { "title": "프로그램 관리", diff --git a/src/frontend/src/i18n/locales/zh.json b/src/frontend/src/i18n/locales/zh.json index 16dfa6a..771a682 100644 --- a/src/frontend/src/i18n/locales/zh.json +++ b/src/frontend/src/i18n/locales/zh.json @@ -2132,7 +2132,11 @@ "sendFail": "发送失败。", "confirm": "确定发送 {{name}} 活动吗?", "sending": "发送中…", - "sendBtn": "发送" + "sendBtn": "发送", + "sendTestBtn": "测试发送", + "sendLiveBtn": "正式发送", + "confirmLive": "将向 {{audience}} 名真实收件人正式发送'{{name}}'活动,此操作不可撤销。是否继续?", + "confirmLiveType": "输入 LIVE 以确认正式发送。" }, "program": { "title": "程序管理", diff --git a/src/frontend/src/screens/marketing/EdmCampaignPage.tsx b/src/frontend/src/screens/marketing/EdmCampaignPage.tsx index 1b59ee3..b8ed274 100644 --- a/src/frontend/src/screens/marketing/EdmCampaignPage.tsx +++ b/src/frontend/src/screens/marketing/EdmCampaignPage.tsx @@ -52,8 +52,9 @@ export function EdmCampaignPage() { const [sendMsg, setSendMsg] = useState(null); const sendMut = useMutation({ - // 안전 기본: 테스트 모드(live 미지정). 운영 실발송은 서버 설정(EDM_TEST_MODE=false)에서만 개방. - mutationFn: (id: string) => marketingApi.sendCampaign(eventId as string, id), + // 안전 기본: 테스트 모드(live=false). 운영 실발송은 ①UI 이중 확인(live=true) ②서버 EDM_TEST_MODE=false 둘 다 필요. + mutationFn: (vars: { id: string; live: boolean }) => + marketingApi.sendCampaign(eventId as string, vars.id, vars.live), onSuccess: (res) => { const n = res.sentCount.toLocaleString(); let base: string; @@ -88,10 +89,23 @@ export function EdmCampaignPage() { const ok = window.confirm(t('marketing.confirm', { name: c.name })); if (ok) { setSendMsg(null); - sendMut.mutate(c.id); + sendMut.mutate({ id: c.id, live: false }); } } + /** 운영 실발송 — 이중 확인(경고 confirm + 'LIVE' 타이핑). 서버 EDM_TEST_MODE=false 일 때만 실제 발송된다. */ + function confirmSendLive(c: CampaignDto) { + if (sendMut.isPending) return; + const ok = window.confirm( + t('marketing.confirmLive', { name: c.name, audience: c.audience.toLocaleString() }), + ); + if (!ok) return; + const typed = window.prompt(t('marketing.confirmLiveType')); + if (typed !== 'LIVE') return; + setSendMsg(null); + sendMut.mutate({ id: c.id, live: true }); + } + const audience = useMemo( () => SEGMENTS.filter((s) => segments[s.id]).reduce((sum, s) => sum + s.count, 0), [segments], @@ -204,7 +218,8 @@ export function EdmCampaignPage() { key={c.id} campaign={c} onSend={() => confirmSend(c)} - sending={sendMut.isPending && sendMut.variables === c.id} + onSendLive={() => confirmSendLive(c)} + sending={sendMut.isPending && sendMut.variables?.id === c.id} /> ))} {rows.length === 0 && ( @@ -333,10 +348,12 @@ export function EdmCampaignPage() { function CampaignRow({ campaign: c, onSend, + onSendLive, sending, }: { campaign: CampaignDto; onSend?: () => void; + onSendLive?: () => void; sending?: boolean; }) { const { t } = useTranslation(); @@ -371,8 +388,13 @@ function CampaignRow({ {c.clickRate ?? '-'} {canSend && onSend && ( - + )} + {canSend && onSendLive && ( + )}