LibCore · 광고 페이지 카운팅

페이지 카운팅 동작 명세

전면광고 노출 조건의 기준이 되는 페이지 카운트가 어떻게 판정·누적·소비되는지에 대한 현재 동작 명세. 원칙은 하나다 — 사용자 조작(user action) 1회는 페이지 최대 1회로 계상된다. 이 문서는 LibCore 공통 규칙을 다루며, 앱 쪽 대응(명시 카운팅 지점·진입/제외 배선·상속 요건·디버그 도구)은 워드빗 적용 명세 → 에 따로 있다.

유저 입력의 정의 — 아래 두 가지가 전부이며, 이때 FLAG 가 켜진다(ON).

터치 다운 — 유저가 화면 아무 곳이나 누르는 순간 (ACTION_DOWN. 어디를 눌렀는지·스크롤/클릭 여부 무관)
키 다운내비게이션 바의 백(뒤로가기) 키만 해당 (다른 키는 유저 입력으로 치지 않음)

01동작 원리

유저 입력이 있고 난 뒤에 화면(Activity 또는 Fragment)이 변경되었을 때만 카운트한다. 상태는 ViewExtensions.userActionPending(FLAG) boolean 1개.

기본 동작

 유저 입력 (터치/키 다운)
   → FLAG ON
   → Activity.onResume     → 카운팅 & FLAG 전환 (OFF)
   → Fragment.onResume     → FLAG == OFF → SKIP

 // 조작 1회 = 카운트 정확히 1회. 같은 입력에서 이어지는 연쇄 resume 은 전부 SKIP

예외 — 앱 아이콘 실행 진입

아이콘 탭은 런처(다른 앱)의 터치라 FLAG 가 서지 않지만, 앱 실행도 카운트되어야 하는 진입이다. 그래서 이 경우에만 메인 진입을 "유저 입력이 있는 상태"로 만들어 준다:

 Splash → AppOpen 광고 → Count 0 (리셋)
   → MainActivity.onCreate   → 유저 입력 처리 (FLAG ON)   // goMain 이 실어 보낸 extra 소비
   → MainActivity.onResume   → 카운팅 (count = 1) & FLAG 전환 (OFF)
   → LearnFragment.onResume  → FLAG == OFF → SKIP
   → WordFragment.onResume   → FLAG == OFF → SKIP   // 한 진입 = 정확히 1회
부가 정보 — 플래그(FLAG) 상세 규칙

02카운트가 올라가는 경로

모든 경로가 increasePageCount() 한 곳으로 모이고, 판정은 그 안의 플래그가 한다.

경로호출 지점비고
액티비티 진입 BaseActivity2.onResume 단, 다음 두 경우는 제외 — ① countsAsPageView = false: Splash 등 비콘텐츠 화면 ② skipPageCountByIntent: 노티로 진입한 화면
※ ②는 카운트만 제외될 뿐, 노티 진입은 setAdEnable 이 광고 게이트를 별도로 개방하는 동작이 따로 있다(05장)
프래그먼트 진입 BaseFragment.onResume 호스트가 resume 사이클 중이면(isResumingForPageCountCycle) 생략 — 액티비티 쪽 호출이 대표
단어 넘김 BottomNavigator prev/next 클릭 명시 호출 (buttonPrev/buttonNext 소스)
페이저 스와이프 registerPagerPageCounter 의 페이지 콜백 사용자가 실제 드래그한 전환만(userDragged), 소형 페이저(200×300dp 미만) 제외, FragmentStateAdapter 페이저는 페이지 fragment 의 onResume 이 담당
기타 앱 코드 오버스크롤·AI챗 복귀 등 수동 호출 전부 같은 함수로 모이므로 판정 규칙 동일

03카운트되지 않는 경우

차단 장치거르는 것
조작 플래그 없음 (01-3) DB 로드 후 콘텐츠 교체로 인한 fragment 재-resume, 자식 fragment 의 늦은 attach, 자동 진입 체인, 광고 닫고 호스트 복귀, 백그라운드 복귀, 화면 회전, 자동 롤링 배너
countsAsPageView = false 비콘텐츠 화면 (Splash·온보딩 등이 오버라이드)
EXTRA_SKIP_PAGE_COUNT intent extra 노티 자동 실행으로 열린 화면과 그 안의 fragment 전부 — MainActivity.nextStep 의 "type" 자동 분기가 ReviewActivity / LearnStatisticsActivity / DeliveryOthersActivity 인텐트에 부여. 액티비티 수명 내내 유지
백그라운드 무효화 앱이 백그라운드로 갈 때 FLAG 를 강제로 OFF 로 내린다 (onActivityStoppedclearPendingUserInteraction). 화면 전환 없이 끝난 터치(스크롤·홈 제스처)가 FLAG ON 인 채 세션을 넘어가면, 다음 진입의 자동 체인 첫 resume 이 그걸 잘못 소비해 오카운트되는 것을 막는다.
※ 카운팅이 0회라는 것이지 광고 억제를 뜻하지 않는다 — 자동 배달·노티 진입은 setAdEnable 이 게이트를 별도로 연다(05장).

04시나리오별 동작

시나리오동작이유
탭/버튼으로 화면 이동 1회 터치가 플래그 셋 → 새 화면 resume 이 소비
Activity + Fragment 동시 resume 1회 먼저 온 호출이 플래그 소비, 나머지는 skip
자식 fragment 가 데이터 로드 후 늦게 attach 추가 0회 부모 resume 이 이미 소비 — 타이밍과 무관
앱 실행 → Splash → Main (앱오픈 광고 유무 무관) 1회 goMain 의 extra → Main onCreate 부여 → Main resume 소비
노티 자동 실행 (리뷰/통계/배달) 카운트 0회 · 게이트 별도 개방 조작 없음 + EXTRA_SKIP_PAGE_COUNT 이중 방어. 단 게이트는 진입 시 setAdEnable(ReviewPopupActivity[noti])로 별도 개방
자동 배달 체인 (설치 1일 후 Main → Review) 카운트 0회 · 게이트 별도 개방 조작 없음 + 백그라운드 무효화로 잔존 터치도 차단. 단 게이트는 수신자의 setAdEnable 로 별도 개방(진입 광고 허용)
전면광고 닫고 복귀 복귀 화면 1회 광고 닫는 탭도 user action — setAdPageAndTimeReset 이 부여 (리셋 후 첫 페이지)
백그라운드 복귀 / 화면 회전 0회 조작이 아님 — 복귀 후 첫 조작부터 다시 계상
빠른 연속 이동 (연타) 각 1회 조작마다 플래그가 새로 서고 각각 소비됨 — 시간 제한 없음

05광고 게이트와의 관계

카운트는 전면광고 노출 조건(isAdShowEnable)의 입력이다.

판정

isAdShowEnable

PAGE_COUNT ≥ AD_PAGE_LIMIT_COUNTS(RC) 이면 허용.

리셋

setAdPageAndTimeReset

전체화면 광고가 실제로 노출된 시점의 단일 기록점 — 전면 dismiss, 앱오픈 onShow, Splash 진입(getOpenAD) 에서 호출. PAGE_COUNT=0 + 광고 닫는 조작 부여. 여기서부터 다음 억제 창이 시작된다.

setAdEnable(게이트 강제 개방)은 "진짜 진입" 지점에서만 호출된다. 이 함수는 PAGE_COUNT 를 limit 값으로 채워 게이트를 즉시 여는 함수라 호출 위치가 곧 정책 준수다. 현재 호출 지점은 ① BaseLockScreenReceiver 의 mainClass 실행부(잠금화면·자동 배달 진입 — Main 이 살아 있으면 이 진입이 onNewIntent 로 도달하며, 게이트는 수신자가 연다) ② 노티 진입(ReviewPopupActivity[noti]) 두 갈래다. Main 의 onNewIntent 자체는 부르지 않는다 — 내부 재호출까지 도달하는 지점에서 무조건 열면 앱오픈 광고 직후의 리셋이 무효화되어 전면광고가 연속 노출되는 것이 재현으로 확인됐기 때문이다.

06재진입 경로와 Splash 중복 (태스크 동작)

홈으로 나갔다 돌아올 때 Splash·앱오픈 광고가 다시 나올지는 태스크의 상태와 출생 기록(baseIntent)이 결정한다.

재진입 방법태스크 상태동작
런처 아이콘 탭 없음 (콜드) Splash 부터 정상 시작 → 앱오픈 광고 → goMain → Main
살아 있음 (홈 복귀) 태스크 전면 전환만 — 인텐트는 버려지고 Splash 는 타지 않는다. Main 이 마지막 상태 그대로 복귀 (앱오픈 광고 없음 — 광고는 Splash.getOpenAD 에서만 호출되므로)
명시 인텐트 (am start · 노티 · 딥링크) 살아 있음 특수 처리 없이 살아있는 Main 위에 Splash 를 새로 쌓는다 → 아래 "중복 진입 체인" 참조

baseIntent 오염 — 아이콘 탭에서도 중복이 재현되는 조건

baseIntent(태스크를 만든 인텐트)는 태스크 생성 시 딱 한 번 기록되고, 위에 액티비티를 얹는 것으로는 바뀌지 않는다. 아이콘 탭의 "전면 전환만" 특수 처리는 런처 인텐트가 baseIntent 와 filterEquals 로 일치할 때만 적용된다.

 ── 오염 시나리오: 태스크가 명시 인텐트로 '생성'된 경우 ──────────────

 앱 죽은 상태에서 노티 클릭 / 딥링크 / am start
   → 태스크 생성, baseIntent = 명시 인텐트 (MAIN/LAUNCHER 카테고리 없음)

 이후 아이콘 탭 (태스크 살아 있음)
   → 런처 인텐트와 filterEquals 불일치 → "다른 실행" 으로 판단
   → 살아있는 Main 위에 Splash 를 쌓음

 ── 중복 진입 체인 (문제) ─────────────────────────────────────────

 [Main(살아있음), Splash(새로 얹힘)]
   → getOpenAD → 앱오픈 광고 SHOW (리셋: count 0)
   → DISMISS → goMain (NEW_TASK | CLEAR_TOP)
   → 아래의 구 Main 재사용 (singleTop) → ★ onNewIntent
   → onNewIntent 는 게이트를 열지 않음 (setAdEnable 은 잠금화면 수신자 전용)
   → count 0 그대로 → 전면광고 안 뜸 // 연속 광고 차단 — 남는 현상은 앱오픈 광고 중복 노출뿐

재현 레시피와 확인 명령

# 1. 태스크 제거 후 명시 인텐트로 '태스크 생성' (baseIntent 오염)
adb shell am force-stop net.wordbit.enkr
adb shell am start -n net.wordbit.enkr/.SplashActivity
# 2. Main 진입 → 홈키 → 아이콘 탭 → Splash 가 Main 위에 쌓이면 재현

# 재현 가능 상태인지 사전 확인 — cat=[...LAUNCHER] 가 없으면 재현됨
adb shell dumpsys activity activities | grep -A3 "baseIntent.*wordbit"

현재 방어 상태 — 이 체인이 발생해도 광고 게이트는 열리지 않는다. setAdEnable(게이트 개방)을 BaseLockScreenReceiver 의 mainClass 실행부에서만 호출하도록 한정했기 때문에, Splash 중복 → onNewIntent 가 일어나더라도 카운트는 리셋(0) 상태를 유지해 연속 전면광고로 이어지지 않는다. 남는 현상은 앱오픈 광고가 한 번 더 보이는 것뿐이다.

07기록과 Firestore 업로드

카운팅 정확도를 실데이터로 검증하기 위한 장치. RC 게이트 기본 off.

1

로컬 기록 (항상)

increasePageCount 확정마다 {t, source, count} 를, setAdPageAndTimeReset 마다 {t, "setAdPageAndTimeReset[소스]", 0} 을 SharedPreference(page_count_records_json) JSON 배열에 누적. 상한 500건(초과 시 오래된 것부터 삭제), 파싱 손상 시 빈 배열로 자가 복구.

2

Firestore 배출 (전면 노출 확정 시, RC on 일 때만)

AdFullDialog.show 의 노출 확정 분기에서 uploadPageCountRecords(actionName) 호출. 게이트는 RC ad_configure.page_count_upload(boolean, 기본 off). 문서 경로 PageCountWorker/{package}/records/{pseudoId}_{uploadedAt}, 상단 필드로 pseudo_id·adid(캐시)·package·uploaded_at·action. 성공 시 업로드한 개수만큼만 삭제, 실패 시 다음 노출 때 재시도, in-flight 가드로 중복 방지.

운영 전제 2가지: RC 콘솔의 ad_configure"page_count_upload": true 등록, Firestore 보안규칙에 PageCountWorker/** 클라이언트 write 허용.

08디버깅 방법

수단보는 법
로그캣 updatePageCount : N (from 소스) / skip increasePageCount (사용자 조작 없음) / clear pending user interaction (background) / setAdPageAndTimeReset (from 소스)
디버그 오버레이 [PC] 배너 오버레이 롱클릭 → 시트에서 이벤트 로그 토글 on → 화면 위 리스트에 카운트 실시간 표시 (DEBUG 빌드)
Firestore 기록 노출별 문서의 records[] 타임라인으로 어느 화면에서 언제 쌓였고 언제 0 이 됐는지 확인