작성 기준: develop 브랜치, 현재 코드 직접 검증. 디바이스/에뮬레이터(API 26+ 권장) 필요.
⚠️ 먼저 0. 사전 준비와 B. 현재 막혀 있는 경로를 읽으세요. 일부 신호(지오펜스·부팅·Health)는 코드/매니페스트 누락으로 앱 UI로는 E2E 테스트가 불가합니다.
-
BASE_URL에/api반영됐는지 확인(local.properties→https://staging-api.ruleup.co.kr/api) 후 재빌드 + 재설치. - 소셜 로그인 정상 동작(앞서 수정 완료) → 토큰 확보 상태.
- 자동인증 루틴이 있는 챌린지를 1개 생성/참여해 둔다(생성 플로우가 AUTO면 위치/활동 권한을 요청함).
challengeId를 메모해 둔다(딥링크·상세 테스트에 필요).
| 권한 | 용도 | 부여 방법 (현재) |
|---|---|---|
| ACCESS_FINE_LOCATION | 위치 샘플·지오펜스 등록 | 챌린지 생성 AUTO 플로우에서 자동 요청 |
| ACTIVITY_RECOGNITION / CAMERA | (AUTO) | 챌린지 생성에서 자동 요청 |
| GET_USAGE_STATS (사용기록 접근) | 스크린타임 | 인앱 UI 없음 → adb 우회: adb shell appops set com.ruleup.android_ruleup GET_USAGE_STATS allow |
| ACCESS_BACKGROUND_LOCATION | 앱 종료중 지오펜스 | 매니페스트 미선언 → 부여 불가 (B 참고) |
| Health Connect READ_* | HEALTH/SLEEP | 매니페스트 미선언 + 그랜트 UI 없음 → 부여 불가 (B 참고) |
# HTTP 로그 보기 (Timber Tree 심은 뒤부터 보임)
adb logcat -s HttpClient:D
# 딥링크로 화면 진입 (인앱 진입 동선이 없는 화면용)
adb shell am start -a android.intent.action.VIEW -d "ruleup://app/verification/progress" com.ruleup.android_ruleup
adb shell am start -a android.intent.action.VIEW -d "ruleup://app/verification/detail?challengeId=<ID>" com.ruleup.android_ruleup
adb shell am start -a android.intent.action.VIEW -d "ruleup://app/verification/manual?challengeId=<ID>" com.ruleup.android_ruleup
adb shell am start -a android.intent.action.VIEW -d "ruleup://app/verification/location?challengeMemberId=<ID>&defaultRadiusM=100&dwellMinutes=60" com.ruleup.android_ruleup
# 주기 sync 워커 강제 실행 (PeriodicWork는 즉시 안 돎)
adb shell dumpsys jobscheduler | findstr ruleup # JOB_ID 찾기 (Windows: findstr / *nix: grep)
adb shell cmd jobscheduler run -f com.ruleup.android_ruleup <JOB_ID>
# 위치 모킹: 에뮬레이터 Extended controls → Location, 또는 개발자옵션 '모의 위치 앱 선택'딥링크 URL에
&가 있으면 셸이 잘라먹으니 URL 전체를 따옴표로 감싸세요(위 예시처럼).
각 항목: 사전조건 / 절차 / 기대결과. HttpClient 로그로 요청·응답을 함께 확인.
- 사전조건: 위치 권한 허용, 네트워크 연결.
- 절차:
cmd jobscheduler run으로 sync 워커 강제 실행. - 기대결과:
POST .../api/v1/verifications/sync→ 200.- 요청 바디에
signals배열, 최소LOCATION1건(위치 샘플). 응답에nextSyncAfterSec. - 성공 후 다음 주기가 응답값으로 reschedule(
UPDATE).
- 사전조건: 위치 권한 허용.
- 절차: (a) 정상 위치에서 sync 1회. (b) 모의 위치 켜고 sync 1회.
- 기대결과: 페이로드
LOCATION이벤트의lat/lng/accuracy/isMock정상. 모의 위치일 때isMock=true전송(안티치트 핵심).
- 사전조건:
adb shell appops set ... GET_USAGE_STATS allow후 앱 재실행. - 절차: 앱 몇 개 전환(RESUMED/PAUSED), 화면 잠금/해제 후 sync.
- 기대결과: 페이로드에
SCREEN_TIME(AppUsageEvent·ScreenEvent). 커서가 증분 갱신돼 다음 sync에 중복 미전송. - 참고: 인앱 그랜트 UI는 없음(0-2). appop 미허용 시
SCREEN_TIME은 비어야 정상.
- 사전조건: 챌린지 존재, sync 1회 이상(progress 캐시 채움).
- 절차: 딥링크
verification/progress진입. - 기대결과:
- 카드별 제목 / "진행률 N%" / 오늘 상태 라벨("진행 중"/"인증 완료"/"인증 실패"/"오늘은 대상일 아님") / "남은 N일".
- 마지막 sync가 2시간 초과(또는 없음)면 "
⚠️ 신호 미수신 — 권한을 점검해주세요" 배지. - 카드 탭 → 상세로 이동.
- 사전조건: 백엔드가 각 상태를 반환하도록 데이터 구성(또는 가능한 케이스부터).
- 절차: 딥링크
verification/detail?challengeId=<ID>또는 Progress 카드 탭. - 기대결과:
- SUCCESS: "인증 완료" + verifiedAt + 증거(dwell/usage/firstUnlock).
- FAILED:
failureReason한글 카피 + 조건부 CTA 버튼. - PENDING: "아직 진행 중이에요. 창이 닫히면 확정돼요." — 빨강/실패로 보이면 버그.
- NOT_TARGET: "오늘은 이 챌린지의 대상일이 아니에요."
- 미지원 방식은 "(이 기기 미지원)" 표기, 하단 최근 dailyLogs 표시.
- 절차: FAILED 상태에서 reason별 CTA 탭.
- 기대결과:
NO_SIGNAL_RECEIVED/PERMISSION_MISSING→ "권한 설정으로" → 앱 설정 화면 오픈.GEOFENCE_NOT_CONFIGURED→ "지도 핀 설정으로" → 위치 핀 화면(verification/location) 이동.- 그 외 reason → CTA 버튼 없음.
- 사전조건: 챌린지 존재, 오늘 미인증.
- 절차: 딥링크
verification/manual?challengeId=<ID>→ "자가 체크로 인증". - 기대결과:
POST .../api/v1/challenges/<ID>/verifications200 → "오늘 인증을 제출했어요" → 뒤로. "사진으로 인증(준비 중)" 버튼은 비활성.
- 절차: "오늘은 수동으로 증명하기"(asFallback=true) 제출. 한도 초과까지 반복.
- 기대결과: 정상 시 "오늘은 수동으로 잠정 인증했어요. 이의가 없으면 확정돼요"(verifiedVia=MANUAL_FALLBACK, disputeClosesAt). 한도 초과 시 409 FALLBACK_LIMIT_EXCEEDED → "이번 주 수동 인증을 모두 사용했어요".
- 절차: 오늘 이미 인증된 챌린지에 수동 제출 재시도.
- 기대결과: 409 ALREADY_VERIFIED → "오늘은 이미 인증했어요"가 에러가 아닌 안내로 표시.
- 절차: 딥링크
verification/location?...→ 검색창에 "스포애니" 등 입력 → 검색 → 결과 탭 → 지도 핀/반경 조정 → 확인. - 기대결과:
GET .../api/v1/places/search?q=...&lat=...&lng=...&radiusM=...200, 좌표 없는 결과는 제외, 최대 10개. 결과 탭 시 지도 recenter. 반경 50~1000m로 클램프. 확인 시 바인딩 완료 메시지.
- 절차: (a) 연속으로 sync 강제 실행(서버 429 유도). (b) 비행기모드에서 워커 실행 후 네트워크를 복구하고 다시 실행.
- 기대결과:
- 429 SYNC_TOO_FREQUENT → markSynced 안 함(데이터 보존) + 워커 retry.
- 400 INVALID_SIGNAL_PAYLOAD → 해당 배치 discard(markSynced) 후 재전송 안 함(무한루프 방지).
- 오프라인 → 네트워크 constraint로 미실행/대기, 복구 후 재시도.
- 실패분 재전송(핵심): (b) 에서 실패한 신호가 다음 sync 요청 본문에 다시 실려 나가야 한다. 버퍼에 남아 있기만 하고 다음 배치에 안 실리면 그 신호는 영영 판정에 못 들어간다(#319). 확인법 — 실패 직후 요청의 신호 건수와 복구 후 요청의 신호 건수를 비교한다. 뒤쪽이 앞쪽을 포함해야 한다.
- 절차: 다양한 상태로 Progress/Detail 재확인.
- 기대결과: PENDING·NOT_TARGET은 어디서도 실패로 렌더되지 않음. stale 배지는 lastSyncedAt 기준 2시간 경계에서 토글.
직접 확인함:
verification/모듈에 AndroidManifest가 없고, 앱 매니페스트에도 아래 리시버/권한이 선언돼 있지 않음(app 매니페스트 권한 = INTERNET/ACCESS_NETWORK_STATE/WAKE_LOCK 뿐).
| 막힌 시나리오 | 원인 | 풀려면 |
|---|---|---|
| 지오펜스 ENTER/EXIT/DWELL 발화 | GeofenceBroadcastReceiver가 어떤 매니페스트에도 미등록 |
앱 매니페스트에 <receiver> + PendingIntent 대상 등록 |
| 재부팅 후 지오펜스 재등록 | BootReceiver 미등록 + RECEIVE_BOOT_COMPLETED 미선언 |
리시버 + 권한 선언 |
| 앱 종료 상태 지오펜스 | ACCESS_BACKGROUND_LOCATION 미선언/미요청 |
권한 선언 + 백그라운드 위치 그랜트 UI |
| HEALTH / SLEEP 수집 | Health Connect READ_* 권한 매니페스트 미선언 + 인앱 그랜트 UI 없음 |
HC 권한 선언 + requestPermissions 런처 |
| 인앱 권한 부여 UX | 사용기록·백그라운드위치·HC 모두 요청 화면 없음 | 각 권한 요청/딥링크 UI |
| 생성→스코프 연동 | 챌린지 생성이 HealthTargetStore/UsageTargetStore 미설정, BindLocation/RegisterGeofences 미호출 |
생성 완료 시 스코프 채우기 + 지오펜스 등록 |
→ 이 경로들을 테스트하려면 먼저 매니페스트/권한/스코프 배선 작업이 필요합니다. 원하면 이 부분 패치도 도와줄 수 있습니다.
JVM 단위 테스트로 검증된 로직 — 수동 QA에서 반복할 필요 없음:
RunSyncUseCaseTest— sync 흐름(빈 배치→미전송 / 200→markSynced+purge / 400→discard / 429→보존)VerificationCopyTest— §6.4 렌더 규칙(PENDING/NOT_TARGET≠실패, CTA 라우팅, stale 2h, ISO 파싱)VerificationRepositoryImplTest— 수동 제출 매핑(409 ALREADY/FALLBACK_LIMIT, fallback verifiedVia/disputeClosesAt)VerificationDtoSerializationTest— DTO 직렬화(geofence/HEALTH/SLEEP, epoch→ISO, nextSyncAfterSec 기본 1800, unknown→PENDING, 좌표 필터)SignalEntityMapperTest·UsageEntityMapperTest·UsageSignalMappingTest— 엔티티/이벤트 매핑, isMock 보존, unknown enum 폴백GeofenceReconcileTest— 지오펜스 diff-set 제거SyncOutcomeTest·BindLocationUseCaseTest— 예외→결과 매핑, 반경 클램프
단위 테스트 한 번 돌려두면 좋음:
./gradlew :verification:data:testDebugUnitTest :verification:domain:test :verification:presentation:testDebugUnitTest(※ 미커버: 모든 ViewModel/Compose 화면, 실제 신호 수집, Room DAO 통합, WorkManager 스케줄 — 수동 QA 대상.)