LibCore · 광고 페이지 카운팅
전면광고 노출 조건의 기준이 되는 페이지 카운트가 어떻게 판정·누적·소비되는지에 대한 현재 동작 명세. 원칙은 하나다 — 사용자 조작(user action) 1회는 페이지 최대 1회로 계상된다. 이 문서는 LibCore 공통 규칙을 다루며, 앱 쪽 대응(명시 카운팅 지점·진입/제외 배선·상속 요건·디버그 도구)은 워드빗 적용 명세 → 에 따로 있다.
유저 입력의 정의 — 아래 두 가지가 전부이며, 이때 FLAG 가 켜진다(ON).
① 터치 다운 — 유저가 화면 아무 곳이나 누르는 순간 (ACTION_DOWN. 어디를 눌렀는지·스크롤/클릭 여부 무관)
② 키 다운 — 내비게이션 바의 백(뒤로가기) 키만 해당 (다른 키는 유저 입력으로 치지 않음)
유저 입력이 있고 난 뒤에 화면(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회
BaseActivity2.dispatchTouchEvent(터치 다운)·dispatchKeyEvent(백 키)에서
noteUserInteraction() 호출. 같은 지점에서 increaseTouchCount()(터치 집계)도 함께 돈다.
백 키는 경로가 하나 더 있다 — API 33+ 에서 화면이 자기 OnBackPressedCallback 을 가지면 back 이
dispatchKeyEvent 에 오지 않으므로, backUserActionSpy(소비하지 않는 감시 콜백)가 FLAG 만 기록하고
원래 처리자에게 재-dispatch 한다. 세는 단위는 AdMob 정책의 user action 정의(clicks, swipes, back button presses)에 맞춘 것.goMain 이 EXTRA_NOTE_USER_ACTION extra 를 실어 보내고 Main 의 onCreate 가 1회 소비하며 ON
(launch 시점에 직접 켜면 Splash finish 의 무효화와 경합하므로 extra 로 전달)
② 광고 닫기 — setAdPageAndTimeReset 안에서 ON (광고 닫는 탭은 광고 SDK 윈도우에서 발생).increasePageCount 진입 시 FLAG 가 ON 이면 카운트 후 OFF,
OFF 면 카운트 없이 skip (사용자 조작 없음) 로그만 남긴다.BaseApplication2.onActivityStopped →
clearPendingUserInteraction) 소비되지 않은 잔존 FLAG 를 끈다 — 다음 진입의 자동 체인(배달 등)이
이전 세션의 터치를 잘못 소비해 오카운트되는 것을 방지. 정상 네비게이션은 새 화면 resume(소비)이
이전 화면 stop 보다 먼저라 영향 없다.모든 경로가 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챗 복귀 등 수동 호출 | 전부 같은 함수로 모이므로 판정 규칙 동일 |
| 차단 장치 | 거르는 것 |
|---|---|
| 조작 플래그 없음 (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 로 내린다
(onActivityStopped → clearPendingUserInteraction).
화면 전환 없이 끝난 터치(스크롤·홈 제스처)가 FLAG ON 인 채 세션을 넘어가면, 다음 진입의
자동 체인 첫 resume 이 그걸 잘못 소비해 오카운트되는 것을 막는다.
※ 카운팅이 0회라는 것이지 광고 억제를 뜻하지 않는다 — 자동 배달·노티 진입은 setAdEnable 이 게이트를 별도로 연다(05장). |
| 시나리오 | 동작 | 이유 |
|---|---|---|
| 탭/버튼으로 화면 이동 | 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회 | 조작마다 플래그가 새로 서고 각각 소비됨 — 시간 제한 없음 |
카운트는 전면광고 노출 조건(isAdShowEnable)의 입력이다.
PAGE_COUNT ≥ AD_PAGE_LIMIT_COUNTS(RC) 이면 허용.
전체화면 광고가 실제로 노출된 시점의 단일 기록점 — 전면 dismiss, 앱오픈 onShow,
Splash 진입(getOpenAD) 에서 호출. PAGE_COUNT=0 + 광고 닫는 조작 부여.
여기서부터 다음 억제 창이 시작된다.
setAdEnable(게이트 강제 개방)은 "진짜 진입" 지점에서만 호출된다.
이 함수는 PAGE_COUNT 를 limit 값으로 채워 게이트를 즉시 여는 함수라 호출 위치가 곧 정책 준수다.
현재 호출 지점은 ① BaseLockScreenReceiver 의 mainClass 실행부(잠금화면·자동 배달 진입 —
Main 이 살아 있으면 이 진입이 onNewIntent 로 도달하며, 게이트는 수신자가 연다)
② 노티 진입(ReviewPopupActivity[noti]) 두 갈래다.
Main 의 onNewIntent 자체는 부르지 않는다 — 내부 재호출까지 도달하는 지점에서 무조건 열면
앱오픈 광고 직후의 리셋이 무효화되어 전면광고가 연속 노출되는 것이 재현으로 확인됐기 때문이다.
홈으로 나갔다 돌아올 때 Splash·앱오픈 광고가 다시 나올지는 태스크의 상태와 출생 기록(baseIntent)이 결정한다.
| 재진입 방법 | 태스크 상태 | 동작 |
|---|---|---|
| 런처 아이콘 탭 | 없음 (콜드) | Splash 부터 정상 시작 → 앱오픈 광고 → goMain → Main |
| 살아 있음 (홈 복귀) | 태스크 전면 전환만 — 인텐트는 버려지고 Splash 는 타지 않는다. Main 이 마지막 상태 그대로 복귀 (앱오픈 광고 없음 — 광고는 Splash.getOpenAD 에서만 호출되므로) | |
| 명시 인텐트 (am start · 노티 · 딥링크) | 살아 있음 | 특수 처리 없이 살아있는 Main 위에 Splash 를 새로 쌓는다 → 아래 "중복 진입 체인" 참조 |
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) 상태를
유지해 연속 전면광고로 이어지지 않는다. 남는 현상은 앱오픈 광고가 한 번 더 보이는 것뿐이다.
카운팅 정확도를 실데이터로 검증하기 위한 장치. RC 게이트 기본 off.
로컬 기록 (항상)
increasePageCount 확정마다 {t, source, count} 를,
setAdPageAndTimeReset 마다 {t, "setAdPageAndTimeReset[소스]", 0} 을
SharedPreference(page_count_records_json) JSON 배열에 누적. 상한 500건(초과 시 오래된 것부터 삭제),
파싱 손상 시 빈 배열로 자가 복구.
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 허용.
| 수단 | 보는 법 |
|---|---|
| 로그캣 | updatePageCount : N (from 소스) / skip increasePageCount (사용자 조작 없음) /
clear pending user interaction (background) / setAdPageAndTimeReset (from 소스) |
| 디버그 오버레이 [PC] | 배너 오버레이 롱클릭 → 시트에서 이벤트 로그 토글 on → 화면 위 리스트에 카운트 실시간 표시 (DEBUG 빌드) |
| Firestore 기록 | 노출별 문서의 records[] 타임라인으로 어느 화면에서 언제 쌓였고 언제 0 이 됐는지 확인 |