System Design · Live BM

조건부 팝업 시스템 설계

특정 조건을 충족한 유저에게만, 정해진 시간 동안 노출되는 CRM 팝업 시스템

작성 한승호 프로젝트 SUPERSTAR SMTOWN / SUPERSTAR ATEEZ (라이브 서비스) 역할 신규 BM 시스템 단독 설계 — 기획 · 데이터 구조 · 운영 반영 성과 도입 후 매출 · MPU 상승 확인 (MPU +31.1%)

상시 상점에 상품을 늘리는 방식은 노출이 분산되고, 정작 그 상품이 필요한 유저에게 닿지 못합니다. 조건부 팝업은 "유저의 상태"를 조건으로 삼아, 아이템이 필요한 시점에 맞는 상품을 그 유저에게만 제안하는 시스템입니다. 조건 · 상품 · 팝업을 3개 데이터 시트로 분리해, 기획자가 코드 수정 없이 시트 값만으로 새로운 조건부 팝업을 운영할 수 있도록 설계했습니다.

본 문서는 재직 중 직접 작성한 사내 기획서를 포트폴리오용으로 재구성한 것입니다. 회사 로고 · 대외비 표기는 제거했고, 실제 인앱 상품 코드(SKU) · 로케일 코드는 예시 값으로 대체했으며, 테이블 · 컬럼명은 실제 스키마와 다르게 표기했습니다(구조 · 의미는 동일). 데이터 테이블은 핵심 연산 로직 설명을 위해 축약되었으며, 실제 스키마에는 노출 조건 · 예외 처리 등 추가 속성이 존재합니다.

1. 배경과 목표

"상품을 더 만드는 것"이 아니라 "필요한 유저에게 닿게 하는 것"이 목표였습니다.

상시 상점은 모든 유저에게 같은 상품을 노출합니다. 신규 상품을 계속 추가하면 상점이 길어지고, 정작 그 상품이 필요한 유저(재화가 부족하거나, 특정 카드가 없는 유저)의 눈에는 잘 띄지 않습니다.

조건부 팝업은 유저의 현재 상태를 조건으로 삼습니다. 조건을 충족한 유저에게만, 정해진 시간 동안 팝업을 띄워 "지금 당신에게 필요한 상품"을 제안합니다.

설계 의도

구매 전환이 1차 목표지만, 구조는 상품에 한정하지 않았습니다. "조건을 만족한 유저에게 시간제한 콘텐츠를 노출한다"는 뼈대로 설계해서, 미션 유도 · 이벤트 참여 유도 같은 비-BM 용도로도 재사용할 수 있습니다.

그래서 팝업 타입을 상품 구매형 하나로 못 박지 않고, URL 이동형을 함께 두어 "팝업 = 특정 페이지로 보내는 트리거"라는 더 넓은 정의를 유지했습니다.

로비 화면 — 할당된 조건부 팝업이 있으면 우측 상단에 아이콘 + 남은 시간을 노출해 접근성을 높인다.
로비 화면 — 할당된 조건부 팝업이 있으면 우측 상단에 아이콘 + 남은 시간을 노출해 접근성을 높인다.
조건부 팝업 화면 — 좌측 리스트(동시 할당된 팝업들) + 우측 상세(상품 이미지 · 가격 · 남은 시간/수량 · 행동 버튼).
조건부 팝업 화면 — 좌측 리스트(동시 할당된 팝업들) + 우측 상세(상품 이미지 · 가격 · 남은 시간/수량 · 행동 버튼).

2. 시스템 구성 요소

조건부 팝업은 세 가지 축으로 정의됩니다 — ① 팝업 타입 · ② 활성화 조건(트리거) · ③ 운영 기간.

① 조건부 팝업 타입 — 동작에 따라 2종

상품 구매형
  • 팝업 안에서 바로 상품 구매 · 결제 진행
  • 즉각적인 매출 전환이 목표
  • 최초 버전은 이 타입만 개발
URL 이동형
  • 팝업 터치 시 사전에 정의된 URL로 이동
  • 직접 결제 없음 → 상점 · 미션 · 이벤트 페이지로 안내 유도가 목적
  • 설계만 하고 최초 버전은 스펙아웃 (재사용 여지를 남긴 것)

② 활성화 조건 (트리거)

아이템 보유 상태
  • 재화가 부족한 경우 / 특정 카드 · 로비배경 · 프로필을 보유(또는 미보유)한 경우
  • 최초 개발 범위 — 이 타입(ItemAmount)만 사용
행동 조건 (추후)
  • 특정/모든 카드 강화 N회 이상 성공 또는 실패
기타 조건 (추후)
  • 결제 N회 이하인 경우 등

③ 운영 기간 — 3개 시간 개념

운영 기간

이 팝업이 노출될 수 있는 전체 기간. 종료되면 어떤 조건에서도 다시 노출 안 됨

→

활성화 시간

운영 기간 안에서, 유저에게 팝업이 실제로 보이는 시간 (활성화된 시점부터 N시간, hour 단위)

→

쿨타임

한 번 노출된 뒤 다음 노출까지 대기하는 시간. 이 동안은 조건을 다시 충족해도 재할당되지 않음

Design Note — 왜 활성화 시간과 쿨타임을 나눴나

조건을 만족하는 순간 계속 팝업이 뜨면 유저 피로도가 급격히 올라갑니다. "할당되면 N시간만 보이고(활성화 시간), 사라진 뒤 M시간은 다시 안 뜬다(쿨타임)"로 노출 빈도를 데이터 값만으로 조절할 수 있게 했습니다. 유저가 활성화 조건을 벗어나도 활성화 시간 동안은 팝업이 유지되도록 해, 결제 도중 재화가 바뀌어 팝업이 사라지는 문제를 막았습니다.

3. 트리거 조건 상세 설계

최초 개발 범위의 조건 타입은 ItemAmount 하나입니다 — 현재 유저의 특정 아이템 보유량을 목표 값(Value)과 비교합니다. 조건의 상세는 matchScript 한 컬럼에 JSON으로 담아, 조건 종류가 늘어도 컬럼 구조를 바꾸지 않도록 했습니다.

matchScript = { category, targetCode, operator }

① category
  • 조건 판별 기준이 되는 아이템 카테고리
  • Enum = 0:NULL / 1:currency / 2:card / 3:lobbyBG / 4:profile
  • 디폴트: 없음(누락 시 디버그 출력)
② targetCode
  • 기준이 되는 아이템 코드. category에 따라 해당 타입 테이블에서 데이터 참조 (예: card → CardData 시트의 카드 code)
  • 디폴트: 카테고리 내 모든 아이템
③ operator
  • ②의 보유량과 Value를 어떻게 비교할지 정의 — 같음 / 다름 / 이상 / 이하 / 초과 / 미만 6종
  • 디폴트: 이하

ConditionData

조건부 팝업에서 사용할 "조건"을 정의하는 시트

컬럼자료형설명
ruleIdInteger
  • 활성화 조건(트리거) 고유 값
  • 여러 조건부 팝업에서 동일 값을 재사용 가능하도록 개발 요청
ruleTypeInteger
  • 조건 타입. 최초 제안 기준 1:ItemAmount 만 사용
matchScriptString
  • 조건을 결정하는 스크립트
  • { "category": ①, "targetCode": ②, "operator": ③ } 형태로 입력
설명ruleIdruleTypematchScript
보유 다이아 수량 = N10021{"category":0,"targetCode":2,"operator":1}
보유 다이아 수량 <= N10031{"category":0,"targetCode":2,"operator":101}
보유 다이아 수량 > N10061{"category":0,"targetCode":2,"operator":202}

비교할 목표 값(N)은 조건 시트가 아니라 팝업 시트의 conditionTarget에 둔다 → 같은 조건(예: "다이아 이하")을 여러 팝업이 서로 다른 임계값으로 재사용.

4. 활성화 메커니즘

조건부 팝업 생명주기

① 할당

운영 기간 + 활성화 조건을 충족하면 해당 팝업이 유저에게 할당됨 (동일 팝업 중복 할당 방지)

→

② 활성화

할당 이후 로비 진입 시점부터 활성화 시간 동안 노출

→

③ 종료

활성화 시간 종료 / 운영 기간 경과 / 상품 구매 횟수 소진 / 테이블에서 데이터 삭제 시

→

④ 재노출

쿨타임 경과 후 조건 · 운영 기간을 재검증하여 충족 시 재노출 (구매 가능 횟수는 초기화 안 됨)

팝업 강제 호출 & 우선순위

호출 시점
게임 접속 시 / 곡 플레이를 마치고 로비 진입 시, 할당된 팝업을 즉시 호출
우선순위
  • 1순위 — 남은 활성화 시간이 적은 팝업 (곧 사라질 것 먼저)
  • 2순위 — 남은 시간이 같으면 campaignId 값이 작은 순
로비 아이콘
할당된 팝업이 있을 때만 아이콘 노출. 여러 개면 우선순위 1위 아이콘만. 터치 시 해당 팝업 페이지로 진입
오늘 하루 안보기
  • 체크 시 당일 23:59:59까지 강제 호출 안 됨 (KST 00:00 초기화)
  • 개별 팝업이 아니라 조건부 팝업 통합으로 작동 → 여러 팝업이 번갈아 강제 호출되는 피로 방지
  • 설정은 클라이언트 저장 (재설치로 초기화돼도 무방)

Design Note — "오늘 하루 안보기"를 통합으로 묶은 이유

팝업마다 개별로 "안보기"를 처리하면, 유저가 A 팝업을 껐는데 접속할 때마다 B, C 팝업이 순서대로 강제 호출됩니다. 유저 입장에서는 "껐는데도 계속 뜬다"로 느껴집니다. 조건부 팝업 전체를 하나의 스위치로 묶어, "오늘은 그만"이 실제로 오늘 하루 유효하도록 했습니다.

5. 화면 구성

조건부 팝업 화면 전체 레이아웃 — A(정보) · B(리스트) · C(상세) 3개 영역으로 구성.
조건부 팝업 화면 전체 레이아웃 — A(정보) · B(리스트) · C(상세) 3개 영역으로 구성.
A. 정보 영역 상세 — 상점 UI 규격을 재사용해 신규 리소스 제작 부담을 줄였다.
A. 정보 영역 상세 — 상점 UI 규격을 재사용해 신규 리소스 제작 부담을 줄였다.

화면 영역 정의 — 정보 / 리스트 / 상세 3개 영역

A. 정보 영역
  • ① 조건부 상점 타이틀 텍스트 (로케일)
  • ② 상세 확률 표시 버튼 (상점의 확률 표시와 동일 액션)
  • ③ 유저 재화 보유 정보 (상점 재화 영역과 동일)
  • ④ 닫기 버튼
  • ⑤ 특별 상거래법 버튼 (일본 언어 환경에서만 노출)
B. 리스트 영역
  • 현재 유저에게 할당된 조건부 팝업 목록 (우선순위 순 정렬)
  • 타이틀 버튼 — 미터치 시 N 뱃지 노출
  • 오늘 하루 안보기 버튼
C. 상세 영역
  • 조건부 팝업 이미지 (홈 광고 이미지 규격 재사용 가능)
  • 상호작용 버튼 — 상품 구매형: [구매 ₩{가격}] / URL 이동형: [확인하러 가기]
  • 남은 시간 · 남은 수량 텍스트
  • 청약 철회 안내 텍스트 (canRefund 값에 따라 분기)
  • 돋보기(상세 정보) 버튼 — 추천 상점과 동일 팝업 재사용

결제 도중 활성화 시간이 끝나도, 이미 결제가 진행 중이면 이어서 완료 가능. 결제 취소 시 상점과 동일한 "구매를 취소하였습니다" 안내 팝업을 호출합니다.

6. 데이터 테이블 설계

조건 · 상품 · 팝업을 3개 시트로 분리 — 기획자가 코드 수정 없이 시트 값만 채워 새 조건부 팝업을 운영합니다.

시트 관계

ConditionData

조건 정의 (category/targetCode/operator)

→

PopupData

팝업 = 조건 + 상품 + 기간을 묶는 중심 시트

→

ProductData

상품 정의 (구성품/가격/SKU/청약철회)

PopupData

조건부 팝업 및 구성 요소를 정의하는 중심 시트

컬럼자료형설명
campaignIdInteger
  • 조건부 팝업 고유 값. 코드 재사용 가능 형태 (다음 업데이트에 리소스 · 기간만 바꿔 재사용)
popupTypeInteger
  • 팝업 타입. 1:Product(상품 구매형) — 최초 개발은 이 타입만
productRefString
  • type==Product일 때 상품 코드 입력 → ProductData [itemCode] 참조
conditionRefInteger
  • 이 팝업이 사용할 조건 → ConditionData [ruleId] 참조
conditionTargetInteger
  • 조건에서 비교할 목표 값 (N)
beginAt / expireAtTimestamp
  • 운영 시작 / 종료 일시 (YYYY-MM-DD HH:MM:SS)
activeTimeInteger
  • 활성화 시간 (hour). 활성화 시점부터 이 시간 동안만 노출
cooldownHoursInteger
  • 쿨타임 (hour). 경과 후 재활성화
titleTextId / descTextIdInteger
  • 타이틀 · 돋보기 문구 로케일 코드 (로케일 시트 참조)
badgeImage / bannerImageString
  • 로비 아이콘 / 팝업 화면 이미지 리소스 경로
campaignIdpopupTypeproductRefconditionRefconditionTargetactiveTimecooldownHours
113000021002502424
2130000210021007272
313000011006150004848

ConditionData

조건 정의 시트 (3번 항목에서 상술)

컬럼자료형설명
ruleIdInteger조건 고유 값 (여러 팝업이 공유)
ruleTypeInteger조건 타입 (1:ItemAmount)
matchScriptString{ category, targetCode, operator } 스크립트

ProductData

조건부 팝업에서 판매할 상품을 정의하는 시트

컬럼자료형설명
itemCodeInteger상품 코드 값
rewardItemsString
  • 상품 구매 시 지급할 아이템 구성 (다른 상점 시스템의 지급 아이템 컬럼과 같은 포맷 재사용)
purchaseLimitInteger
  • 운영 기간 내 구매 가능 횟수. 소진 시 팝업 미노출. 코드별 구매 횟수는 재노출 시 초기화되지 않음
currencyTypeInteger
  • 결제 재화 타입 (1:인앱/다이아, 3:리듬 포인트). 최초 개발은 인앱 상품만
priceValueDouble
  • 상품 가격. 실제 결제/노출 가격은 SKU 값을 따름. 추후 재화 결제 추가 시 사용
aosSku / iosSkuString
  • AOS / iOS SKU 값. 기존에는 OS별 코드를 3개 만들었으나 이번엔 컬럼을 나눠 1행으로 관리
etcSkuString
  • 기타(Windows 등) SKU 값. 불필요 시 삭제 요청
canRefundBoolean
  • 청약 철회 가능 여부. TRUE=철회 가능 / FALSE=철회 불가. 값에 따라 페이지 내 청약 철회 안내 로케일을 다르게 출력
itemCoderewardItemspurchaseLimitcurrencyTypepriceValueaosSkucanRefund
300001[{"p":40105,"q":1}]114.99[SKU_예시_01]TRUE
300002[{"p":40106,"q":1}]514.99[SKU_예시_02]FALSE
300003[{"p":40107,"q":1}]014.99[SKU_예시_03]TRUE

실제 인앱 SKU · 로케일 코드는 예시 값으로 대체했습니다.

7. 도입 결과

실제 도입 화면 — 로비에 노출된 조건부 팝업 아이콘(빨간 박스, 남은 시간 표시).
실제 도입 화면 — 로비에 노출된 조건부 팝업 아이콘(빨간 박스, 남은 시간 표시).
실제 도입 화면 — 조건을 충족한 유저에게 노출되는 FINALE 패키지 조건부 팝업.
실제 도입 화면 — 조건을 충족한 유저에게 노출되는 FINALE 패키지 조건부 팝업.

회고

"상품을 늘리는" 요청을 "조건을 만족한 유저에게 시간제한 콘텐츠를 보내는 트리거"라는 더 일반적인 문제로 바꿔 정의한 것이 핵심이었습니다. 덕분에 첫 버전은 BM으로 썼지만, 같은 시스템을 미션 · 이벤트 유도에도 쓸 수 있는 여지를 남겼습니다.