Skip to content

캠페인 알림 발송

작성: 0chil

빙글 관리의 알림 관리 → 캠페인 알림 발송에서 대상 회원 ID, 회원 ID 변수, 제목, 본문, 이미지 파일, 이동 경로(linkTo) 순으로 입력한다. 회원 ID는 최대 100,000개이며 회원 ID 변수·이미지·이동 경로는 선택 항목이다. ID는 쉼표·공백·줄바꿈으로 구분하며 중복을 제거하고 없는 회원은 제외한다. ${memberName}은 제목·본문·이동 경로에서 회원 이름으로 치환하며 이름이 없거나 공백이면 고객을 사용한다. 본문 줄바꿈은 보존한다.

추가 데이터(extras)는 지원하지 않는다. 이전에 등록한 캠페인의 raw_message에 남아 있는 extras는 PushNotificationCampaignMessage의 @JsonIgnoreProperties(ignoreUnknown = true)로 무시하고 읽는다. 이미지는 캠페인 전체에 동일하게 적용하고 기존 FCM 채널의 전송 방식을 사용한다.

이동 경로는 vingle://로 시작하는 앱 딥링크다. 예: vingle://home?sort=NEWEST, vingle://category?sort=NEWEST&styleIds=1,2. 다른 형식은 화면과 API 모두 400으로 거부한다. 제목·본문처럼 ${memberName}과 ${변수명[memberId]}를 치환하므로 vingle://product/${memberIdToProductId[memberId]}처럼 회원별로 다른 딥링크를 만들 수 있다.

회원마다 다른 값을 제목·본문·이동 경로에 넣을 때는 회원 ID 변수 입력란에 {"변수명": {"회원ID": 값}} 구조의 JSON 객체를 넣는다. 예를 들어 {"memberIdToProductId": {"123": 456, "789": "1011"}}를 입력하고 제목·본문·이동 경로에 ${memberIdToProductId[memberId]}를 쓰면 회원 123에게는 456, 회원 789에게는 1011로 치환한다. 대체어 목록 drawer는 입력한 JSON의 변수명마다 ${변수명[memberId]} 삽입 버튼을 추가한다.

  • 변수명은 영문·숫자·밑줄로 이루어진 식별자이며 memberName은 예약어라 사용할 수 없다. 회원 ID 키는 0보다 큰 정수 문자열이다.
  • 값은 비어 있지 않은 문자열이나 정수만 허용한다. 정수는 JSON에 쓰인 그대로 문자열로 보관하며 소수·불리언·null·배열·객체는 거부한다.
  • ${변수명[memberId]} 형태만 치환하며, memberId는 현재 수신자의 회원 ID를 뜻한다. ${변수명} 등 지원하지 않는 문법은 원문을 유지한다. 새 문법으로 참조한 변수가 JSON에 없으면 400(UNDEFINED_MEMBER_ID_VARIABLE)을 반환한다.
  • 제목·본문·이동 경로에서 참조한 변수는 모든 유효 발송 대상에 값이 있어야 한다. 값이 없는 대상이 있으면 미리보기·발송 등록 모두 400(MISSING_MEMBER_ID_VARIABLE_VALUE)을 반환하며, 변수별 인원수와 처음 20명의 회원 ID를 메시지에 담는다. 참조하지 않은 변수나 대상이 아닌 회원 ID의 값은 검사하지 않는다.

모든 API는 ADMIN 권한이 필요하다. 이미지 업로드는 POST /admin/push-notifications/images에 multipart image 파일을 전달한다. 비어 있지 않은 이미지 파일만 받으며, S3 업로드 후 201 {imageUrl}로 CDN URL을 반환한다. 화면은 미리보기 전에 업로드하고 파일을 변경하지 않으면 해당 URL을 재사용한다. 선택 해제 시 이미지 없이 발송한다.

미리보기·발송 등록 요청은 {title, content, memberIds, linkTo?, imageUrl?, memberIdVariables?}다. memberIdVariables는 변수 JSON이며 생략하거나 null이면 변수 없이 처리한다.

  • POST /admin/push-notifications/preview: 중복 제거 후 요청 수, 유효 대상 수, 제외 ID와 처음 20명의 문구·이동 경로, 이미지 URL을 반환한다. 유효한 발송 대상 인원은 recipientCount로 제공한다.
  • POST /admin/push-notifications/send-jobs: 대상을 다시 조회하고 캠페인과 1,000명 단위 작업을 함께 저장한다. 202 응답의 pushNotificationCampaignId는 발송 건을 구분하고 recipientCount는 등록한 인원수이며 실제 전송 완료를 의미하지 않는다. 유효한 대상이 없으면 아무 것도 생성하지 않고 400을 반환한다.

화면 경로는 /admin-page/push-notifications, 관리자 도메인에서는 /push-notifications다. 대상·문구 조회에 성공하면 미리보기 dialog에서 대상 요약·제외 ID·선택 항목·최종 문구를 확인한다. dialog 안의 발송 작업 등록 버튼에서 최종 확인을 거쳐 등록하며, 성공 시 창을 닫고 페이지에 등록 결과를 표시한다. 입력이 바뀌면 미리보기를 다시 확인해야 한다. 같은 내용으로 다시 등록하면 새 캠페인을 생성한다.

캠페인은 제목·본문·이동 경로 원본 템플릿과 imageUrl을 하나의 JSON으로 묶어 변경 불가능한 raw_message TEXT 컬럼에 저장한다. 변수는 요청 JSON 그대로 변경 불가능한 raw_member_id_variables LONGTEXT 컬럼에 저장하고, 변수가 없으면 null이다. 회원 ID 100,000명에 변수 여러 개를 넣어도 수 MB 수준이므로 TEXT가 아닌 LONGTEXT를 사용한다. 요청 관리자·요청 및 대상 인원수는 별도 컬럼에 저장한다. 청크 작업에는 캠페인 ID와 회원 ID만 저장한다. 청크 처리 시 두 JSON을 읽고 현재 회원 이름과 변수 값으로 제목·본문·이동 경로를 치환해 사용자별 PushMessageSendingJob을 등록하므로 미리보기 이후 변경된 이름이 적용될 수 있다. 없어진 회원은 제외한다. 등록 시 검증을 거치므로 청크 처리 시 변수 값이 없는 회원은 없어야 하지만, 있다면 해당 회원만 제외하고 error 로그를 남긴다. 캠페인 목록 조회는 raw_member_id_variables를 읽지 않는다. 청크의 사용자별 작업 등록도 하나의 트랜잭션이며 자동 재시도는 하지 않는다. 처리 실패는 기존 작업 큐 상태와 로그에서 확인한다.

발송 타입은 CAMPAIGN이며 pushNotificationCampaignId를 필수로 전달한다. 다른 타입에는 캠페인 ID를 지정하지 않는다. 사용자별 내역의 push_notification_campaign_id 컬럼과 조회 응답에 캠페인 ID를 기록한다. 기존 내역과 기존 작업 JSON의 ID는 null이고, 알림 내역 JSON 버전은 1을 유지한다. CAMPAIGN은 사용자 설정 항목에 추가하지 않는다. 기기 권한·미확인 개수·내역 및 이미지·이동 데이터 처리는 기존 FCM 채널이 담당한다.

사용자별 내역은 기존처럼 30일 후 정리한다. 캠페인 원본과 누적 통계는 정리 대상에 포함하지 않는다.

빙글 관리의 캠페인 알림 발송 내역(/admin-page/push-notifications/campaigns, 관리자 도메인에서는 /push-notifications/campaigns)에서 캠페인별 등록 시각·제목·원본 내용과 다음 지표를 확인한다. 새로고침하면 조회 시점까지 커밋된 통계를 표시한다.

응답 필드지표기준
recipientCount수신자 수발송 등록 시 확정한 유효 회원 수. 이후 작업 결과와 관계없이 유지
pushRecipientCount푸시 알림 수신자 수등록 당시 푸시 권한이 있는 기기를 하나 이상 가진 유효 회원 수. 회원당 한 번만 집계
pushClickCount푸시 알림 클릭 수최초 읽음이 푸시 클릭인 회원
readCount읽음 수알림 내역이나 푸시 중 하나라도 읽은 회원. 두 경로를 이용해도 한 명

푸시 알림 수신자는 수신자 중 등록 시점에 푸시 가능한 기기를 가진 회원이다. 이후 권한 변경·기기 삭제·FCM 발송 결과에 따라 이 값을 변경하지 않는다. 같은 회원이 두 수신자 지표에 포함되므로 두 값을 합산하지 않는다. 읽음 수와 푸시 클릭 수는 각각 고유 회원 수다. 최초 읽음이 푸시 클릭이면 읽음과 푸시 클릭에 모두 포함하고, 최초 읽음이 알림 내역이면 읽음에만 포함한다. 내역을 먼저 읽은 뒤 푸시를 클릭해도 두 지표를 추가 집계하지 않는다.

ADMIN 전용 GET /admin/push-notifications/campaigns는 최신 ID 순으로 조회한다. pageSize는 필수이며 허용 범위는 1~100이다. 관리 화면은 pageSize=20을 명시한다. 응답은 elements, hasNext, lastCursor이며 다음 페이지는 lastCursor.pushNotificationCampaignId 쿼리 파라미터로 요청한다. 항목은 캠페인 ID·등록 시각·관리자 ID·구조화된 원본 message와 4개 지표를 제공한다. 원본의 이름 템플릿은 치환하지 않는다.

수신자 수와 푸시 알림 수신자 수는 캠페인·청크 작업 등록 트랜잭션에서 확정하고 이후 증가시키지 않는다. 유효한 회원 ID를 1,000명씩 나눠 권한이 있는 기기의 회원 ID를 COUNT DISTINCT로 집계한다. 기기가 여러 개여도 회원당 한 번만 센다. 누적 카운터는 읽음 수와 푸시 클릭 수만 저장한다. FCM 채널은 캠페인을 조회하거나 갱신하지 않는다. 내역 생성·미확인 개수 증가는 한 트랜잭션으로 처리하고, FCM 결과 저장 시 성공 로그·내역의 성공 기기 수를 함께 커밋하거나 롤백한다. FCM 네트워크 호출은 트랜잭션 밖에서 수행한다.

두 클릭 경로 모두 내역 읽음의 조건부 UPDATE를 먼저 수행하고 내역 행 잠금을 유지한다. 알림 내역·푸시 중 어느 경로든 읽음 상태가 최초로 변경될 때만 캠페인의 readCount를 증가시킨다. 푸시 클릭 경로라면 사용자별 내역의 클릭 수를 증가시키고, 캠페인의 같은 UPDATE에서 pushClickCount도 함께 증가시킨다. 캠페인 통계 UPDATE는 내역에 연결된 캠페인 ID를 기준으로 적용하며, 해당 캠페인이 없으면 변경하지 않는다. 타입은 별도로 검사하지 않는다. 별도 조회나 SELECT FOR UPDATE 없이 반복 클릭·경로 간 동시 클릭도 각 지표에 한 번만 집계한다. 잠금 순서는 내역 → 회원 집계 → 캠페인이다. 사용자별 내역은 읽음 여부를 저장하고 readCount 응답은 읽지 않음 0·읽음 1로 제공한다. 사용자별 푸시 클릭 수도 최초 읽음이 푸시 클릭인 경우에만 증가하며 미확인 개수 API는 유지한다. 보관 기간은 사용자별 내역 삭제 작업에서만 관리한다. 이미 읽었거나 삭제된 내역은 추가 집계를 건너뛴다. 내역 삭제나 이후 클릭으로 캠페인 누적 통계를 차감하거나 복원하지 않는다.

작업 등록 이후 일부 작업이 실패하거나 회원이 삭제되어도 수신자 수는 변경하지 않는다. 수신자 수는 전송 완료나 알림 내역 생성을 보장하는 값이 아니다. 별도의 진행 상태·완료 판정·재시도·시간대별 분석은 제공하지 않는다.

회원 ID 변수 지원에는 다음 DDL을 먼저 적용한다. 기존 캠페인의 값은 null로 유지한다.

ALTER TABLE push_notification_campaign ADD COLUMN raw_member_id_variables LONGTEXT NULL AFTER raw_message;

최초 캠페인 기능 배포 절차는 다음과 같다.

  1. core/src/main/resources/db/add-push-notification-campaign.sql을 먼저 적용한다. 기존 내역의 push_notification_campaign_id는 null로 유지한다.
  2. 신규 CAMPAIGN 타입과 청크 작업 및 통계 갱신을 지원하는 API·워커를 모두 배포한다. 새 작업을 구버전 워커가 가져가지 않도록 배포 완료 전에는 캠페인을 등록하지 않는다. 아직 운영 캠페인 발송 전을 전제로 하며 통계 백필과 기존 클릭 카운터 정규화는 하지 않는다.
  3. 알림 내역을 소비하는 앱이 CAMPAIGN 타입과 nullable 캠페인 ID를 수용하는지 확인한 뒤 화면을 사용한다.