feat(release): prepare 1.1.0 candidate

This commit is contained in:
Yun Chan 2026-08-29 18:33:45 +09:00
parent 5a34f66981
commit 5205dcdfa9
736 changed files with 115667 additions and 12203 deletions

View file

@ -0,0 +1,400 @@
# D3RO Voice 모바일 앱 완성 정본 (SSOT)
> 상태: ACTIVE
> 최초 감사 기준: 2026-08-21 / `main` @ `a9c9a1c`
> 마지막 release-readiness 스냅샷: 2026-08-29 (Play Console live 재확인, release identity `1.1.0`/`1010001`, production AAB 대기)
> 제품 모바일 런타임: `apps/mobile-rn`
> 데이터·권한 정본: `server/supabase/migrations` + Supabase Auth/Storage/Edge Functions
> 제품화 재개 핸드오프: [`MOBILE_PRODUCTIZATION_HANDOFF_2026-08-21.md`](./MOBILE_PRODUCTIZATION_HANDOFF_2026-08-21.md)
> 이 문서는 이전 `docs/v3/00-mobile-master-plan.md`의 미래 로드맵을 대체하는 구현·검증 정본이다.
## 0. 2026-08-29 출시 게이트 스냅샷
| 게이트 | 현재 증거 | 판정 |
|---|---|---|
| release identity | `release/product-version.json`: version `1.1.0`, Android versionCode / iOS build `1010001` | source SSOT GREEN; 실제 store artifact 미검증 |
| Play 앱 | app ID `4976102237698469110`, package `com.d3ro.voice`, 상태 `임시` | 앱 생성 GREEN, 제출 RED |
| Play 설정 | 0/11, 첫 AAB 0건 | RED |
| App Signing | Play SHA-256 `01:00:19:21:DB:4F:33:40:85:CE:21:E4:B8:DE:CC:BD:71:DA:87:67:C5:6E:3B:59:83:2A:A1:C8:29:EA:0D:AB` | Play identity 확인; upload certificate 대조는 첫 AAB 후 |
| upload key | external PKCS12·user-only ACL·Credential Manager·private-key readback GREEN; SHA-256 `4F:AC:69:24:...:15:2B:54` | local key GREEN; CI secret·복구 백업·AAB signer/Console 대조는 RED |
| release evidence key | Ed25519 external private/public pair roundtrip GREEN; public SSOT·keyId `2797d3e6...4a890b7f` | local pair GREEN; CI private-key secret·복구 백업·실제 signed evidence는 RED |
| App Links | source `assetlinks.json` 3개는 Play SHA-256으로 갱신 | live는 구 certificate라 배포·public 재검증 전까지 RED |
| Firebase | Console `u/0`, `u/1` 모두 D3RO project 없음 | 사용자 승인 후 project·Android app 생성 필요; RED |
| AdMob | App/Banner/Rewarded ID·SSV SSOT 확정 | `검토 필요`·`광고 게재 제한`·store 미연결·결제 프로필 미완료; RED |
| production inputs | production Firebase config·AdMob/upload/evidence CI secret 주입 증거·AAB 없음 | RED |
| closed test | 0/12명, 연속 14일 미시작 | RED |
| production access | disabled | RED |
이 스냅샷의 RED는 내부 구현을 미완성으로 되돌리는 뜻이 아니다. 다만 source/test GREEN을 Play production GREEN으로 확대 해석하지 않는다. Data safety, App content, production access 답변은 실제 Console 문구를 사용자가 검토·승인하기 전에 저장·제출하지 않는다.
## 0.1 완료 계약
이 작업에서 "완료"는 화면이 존재하거나 타입 검사가 통과했다는 뜻이 아니다. 아래 조건을 모두 만족해야 한다.
1. `apps/mobile-rn`의 모든 사용자 노출 버튼·메뉴·딥링크·공유 진입점이 실제 동작한다.
2. 데스크톱과 Web의 서버 공유 기능은 동일한 Supabase 사용자·구독·데이터에서 읽고 쓴다.
3. 모바일에서 불가능한 데스크톱 OS 전용 동작은 모바일 네이티브 동등 기능으로 제공한다.
4. 인증·결제·광고 보상·동기화·삭제는 클라이언트 성공 알림만으로 성공 처리하지 않는다.
5. Free tier 광고는 실제 광고 SDK 완료 콜백과 서버의 중복 방지 보상 기록을 통과해야 한다.
6. 정상·실패·취소·재시도·오프라인·중복 요청·권한 거부·세션 만료 경로를 자동화한다.
7. Android 에뮬레이터에서 설치된 APK를 대상으로 전체 E2E를 수행하고 증거를 남긴다.
8. 실행 코드에 미구현 placeholder, 가짜 성공 타이머, 개발 우회 로그인, 무반응 Pressable을 남기지 않는다.
9. 각 작업은 코드, 자동 테스트, 에뮬레이터 증거를 모두 확보한 뒤에만 `[x]`로 바꾼다.
## 1. 정본 경계와 보존 규칙
- `apps/mobile-rn`: Android/iOS 제품 모바일 앱. 모든 신규 구현은 여기에 한다.
- `apps/mobile`: Expo 51 기반 과거 스켈레톤. 참고만 하며 제품 기능을 이쪽에 이중 구현하지 않는다.
- `apps/web`: 서버 공유 기능과 사용자 데이터 UX의 비교 기준.
- `apps/desktop`: 데스크톱 기능 목록과 로컬 전용 기능의 의미 기준.
- `packages/api-client`: Supabase 타입과 공용 API 호출의 정본. 모바일에서 임의 테이블 타입을 중복 작성하지 않는다.
- `packages/core`: 기능·구독·광고·오류 도메인 타입의 정본.
- `packages/ui-native`: 모바일 디자인 토큰과 공용 컴포넌트 정본.
- 기존 dirty worktree와 `main`의 미푸시 커밋 2개를 보존한다. 관련 없는 바이너리·Web·스크립트 변경을 되돌리거나 함께 정리하지 않는다.
- 비밀값은 저장소에 하드코딩하지 않는다. 빌드 시 주입하고, 누락 시 기능을 숨겨 성공처럼 보이게 하지 말고 진단 가능한 오류로 닫는다.
## 2. 2026-08-21 초기 감사 결과
> 아래 표는 구현 전 기준선이다. 현재 판정은 바로 다음 `2.1`과 4절 체크리스트,
> 7절 실행 증거를 따른다. 기준선의 실패 문구를 현재 상태로 읽으면 안 된다.
| 영역 | 현재 사실 | 판정 |
|---|---|---|
| 앱 기반 | RN CLI 0.85 / React 19, Android 네이티브 프로젝트와 서명 APK가 존재 | 기반만 존재 |
| 화면 | Login, Dash, History, Record, Talk, Settings, ProPaywall 7개 화면 | 기능 동등성 미달 |
| Supabase | `SUPABASE_URL`과 anon key가 빈 문자열이며 dummy fallback으로 크래시만 회피 | 실제 인증·데이터 불가 |
| 세션 라우팅 | 앱은 항상 Login을 첫 화면으로 열며 인증 상태에 따른 root 전환이 없음 | 실패 |
| 회원가입 | 회원가입 문구는 Pressable이지만 handler와 화면이 없음 | 실패 |
| OAuth | 브라우저 호출 코드는 있으나 설정 주입·콜백 수명주기·취소/오류 검증이 불완전 | 미검증 |
| 계정 관리 | 이메일 표시와 로그아웃 확인만 존재. 프로필 수정·비밀번호·연결 계정·삭제 없음 | 실패 |
| 녹음 | recorder hook이 placeholder이며 stop 결과가 항상 `null` | 실패 |
| 파일 전사 | document picker, content URI 처리, 업로드 진행/취소 화면 없음 | 없음 |
| 히스토리 | 최대 50개 조회만 수행. 필터는 데이터에 적용되지 않고 상세/편집/삭제/공유 없음 | 실패 |
| AI 대화 | 사용자 말풍선만 로컬 state에 추가하고 서버 LLM 응답을 호출하지 않음 | 실패 |
| 설정 | 언어·자동 교정·햅틱·STT 스위치가 고정값이며 저장되지 않음 | 실패 |
| 결제 | 1.8초 대기 후 "결제 완료" 알림을 띄움 | 가짜 성공, 출시 차단 |
| 구매 복원 | 1.2초 대기 후 성공 알림을 띄움 | 가짜 성공, 출시 차단 |
| 광고 보상 | 2.5초 대기 후 로컬 토큰을 50 증가시킴 | 가짜 보상, 출시 차단 |
| 광고 인프라 | 데스크톱 mediation 타입·adapter·테스트가 있으나 다수 adapter가 합성 bid/creative를 반환. 모바일 광고 SDK 없음 | 기반 참고만 가능 |
| 백엔드 | Auth/RLS/Storage/STT/LLM/팀/Push/Stripe/Payple/Admin 함수와 스키마가 존재 | 재사용 가능, 모바일 IAP·광고 원장 없음 |
| 신원 체계 | Supabase Auth/Postgres, 별도 .NET JWT/SQLite, 데스크톱 로컬 license가 서로 다른 사용자·권한을 가짐 | 단일 SSOT 필요 |
| STT 실패 | Supabase와 .NET STT가 provider 전체 실패 때 합성 transcript를 성공으로 반환 가능 | 가짜 성공, 출시 차단 |
| 배포 APK | 공개 APK가 `application-debuggable`이며 arm64 단일 ABI. 보유 AVD는 x86_64 | 에뮬레이터 설치 불가 |
| 테스트 | 모바일 Jest smoke 1개 외에 기능 단위·통합·기기 E2E가 없음 | 실패 |
| Android 권한 | 제품 manifest에는 INTERNET 외 녹음·알림·미디어/foreground service 권한이 없음 | 실패 |
### 2.1 현재 실행 스냅샷
| 묶음 | 현재 판정 | 확인된 증거 / 남은 게이트 |
|---|---|---|
| Auth·계정 | 구현·자동화 GREEN, 외부 OAuth·privacy purge 기기 E2E 대기 | auth/signup/invite 경계는 최신 exact APK `.11`에서 PASS. Keychain 세션, 중앙 `purgeAllAccountLocalData`, cold no-session·SIGNED_OUT·refresh session loss·A→B 전환·실패 재시도 경계는 전체 모바일 57 suites/353 tests에 포함돼 GREEN. 실제 token-expiry 강제 로그아웃·logout/delete/provider 연결/해제 기기 E2E와 Google/GitHub/Apple 동의→callback은 남음 |
| 온보딩·테마 | 구현·기기 journey GREEN | 신규/기존 분기, skip/replay, 계정별 preference merge, system/light/dark, reduced motion, 알림·마이크 권한 상태 구현. production preference `0→skip→1`, Android PID 유지, 5.8초 내 Main, navigation warning 0을 실측 |
| 녹음·전사·Talk | 핵심 경로 GREEN | API 34 microphone FGS, 지속 알림, pause/resume/discard, 실제 통화 audio-focus, 프로세스 kill 후 WAV 복구, 파일 import, cloud STT fail-closed, on-device Whisper, Claude SSE, Android TTS를 통합 APK에서 확인. STT provider 호출 전 원자 quota reservation·실패 환불은 20-way 동시성으로 검증. Android `ACTION_SEND` 수신은 허용 MIME·크기·provider 예외·MP4 AAC 추출을 fail-closed로 구현했고 API 34 instrumentation 4/4(CSPRNG 1 + share 3)와 별도 UID 외부 ContentProvider APK→`ACTION_SEND`→Record→durable queue→cancel cleanup E2E가 GREEN |
| 데이터 portability | 구현·공유 E2E GREEN | 00021 atomic export/restore, 손상/타 사용자/충돌 rollback, legacy import를 local integration으로 확인. `portable_exports`→FileProvider `shared_exports` 경계와 PDF Print/DOCX chooser·30초 cleanup을 실제 기기에서 확인 |
| History·회의 | 실제 데이터·회의 생성 계약 GREEN | Supabase history pagination/search/favorite/CAS, Web↔mobile history E2E, meetings/memos/documents RLS/CAS, meeting recording lifecycle 구현. 00025의 제목·참석자·4개 언어·소유 템플릿 원자 생성은 2계정 43 assertions와 competing-payload PT409로 검증. speaker/audio timestamp와 최종 교차 앱 E2E는 남음 |
| Teams·Push | 팀 GREEN, 외부 Push 대기 | production 17 assertions와 local 36 assertions로 원자 팀/초대/RLS/role/device token 계약 확인. durable push outbox 00015~00020과 logout device `1→0` 확인. 실제 FCM 수신은 Firebase/FCM 자격 증명·cron 배포가 필요 |
| Billing·Ads | test 광고와 production AdMob identity/SSV 준비, store·실제 지급 E2E RED | Google Play restore는 실제 store list→모든 token 서버 검증→entitlement snapshot 일치 후에만 성공. Google 공식 test adaptive banner와 rewarded `AdActivity` 완주를 실측했다. production app `ca-app-pub-1039714767792854~6427959892`, banner `/9840591290`, rewarded `/2255790918`, `50 cloud_ai_tokens`, SSV URL, Edge v13, durable receipt 원장은 SSOT에 반영했다. 그러나 live는 `검토 필요`, `광고 게재 제한`, store 미연결, 결제 프로필 미완료다 |
| Admin | 서버·Web·모바일 E2E와 production DB·Edge 배포 GREEN | .NET 21/21, 00023 관리 integration 25 assertions, Edge/JWT 47 assertions와 TLS Chromium E2E GREEN. 모바일 API 34에서 ordinary/manager/admin/super_admin 및 stale 강등을 55 assertions로 실측: Free test 광고, Pro 광고 미요청, 관리자 메뉴/API 차단, 구독 수정, 역할 경계·변경, 삭제 확인·취소, stale JWT 403와 민감 state purge가 모두 PASS. fixture 잔존 0. production 00023 및 admin Edge 4개 배포 후 익명 GET은 모두 `401 authentication_required` + `Cache-Control: no-store`로 재검증 |
| 품질·배포 | exact-APK 에뮬레이터 gate GREEN, production 배포 RED | mobile ESLint 0, typecheck GREEN, 전체 Jest 57 suites/353 tests GREEN은 2026-08-24 실행 증거다. universal non-debuggable TEST-DEMO `0.0.0-e2e.20260821.11`/`202608221`을 API 34에 fresh install해 cold launch·auth/signup/invite를 검증했지만 production artifact가 아니다. 2026-08-29 release identity `1.1.0`/`1010001`, local upload key, Ed25519 evidence keypair, AdMob identity는 확정했지만 production Firebase·CI secret 주입·AAB·Play track·Fold6 Play 설치 증거는 없다 |
## 3. 플랫폼 동등성 원칙
| 데스크톱 기능 | 모바일 동등 기능 |
|---|---|
| 전역 단축키 녹음 | 앱 바로가기, 알림 action, 홈 화면 quick action, 앱 내 큰 녹음 CTA |
| OS 현재 앱에 텍스트 삽입 | 복사, Android 공유 시트, 선택한 앱으로 공유 |
| 로컬 Whisper/Ollama | 모바일 기기 지원 여부를 감지하고, 기본은 인증된 Cloud STT/LLM. 결과·쿼터·오류 계약은 동일 |
| 파일 드롭 | Android Storage Access Framework 문서 선택과 다른 앱의 `ACTION_SEND`/`ACTION_VIEW` 수신 |
| 데스크톱 SQLite | Supabase SSOT + 암호화된 모바일 캐시·업로드 큐 |
| 시스템 오디오 회의 녹음 | 마이크 foreground service 녹음. Android 정책상 캡처 가능한 미디어 projection은 별도 명시 동의 후만 사용 |
| 데스크톱 설정 모달 | 모바일 설정 navigation stack과 계정·기기·데이터 관리 화면 |
## 4. 전체 기능 매트릭스와 작업 등록
상태 표기: `[ ]` 미완료, `[~]` 구현 중, `[x]` 코드+자동화+에뮬레이터 증거 완료, `[!]` 외부 콘솔/계정 증거 대기.
### G — 거버넌스·기반
- [x] G-001 제품 모바일 런타임을 `apps/mobile-rn`으로 확정하고 본 문서에 기록
- [ ] G-002 `apps/mobile` 참조 코드에서 재사용할 기능을 선별하고 이중 런타임 의존 제거
- [x] G-003 모바일 환경 설정 스키마 작성: Supabase URL/anon key, API origin, OAuth redirect, AdMob app/unit IDs, Play product IDs
- [x] G-004 debug/test/prod 설정 분리와 시작 시 구성 진단 화면 구현
- [x] G-005 하드코딩된 release signing 자격 증명을 로컬/CI secret 주입으로 이동
- [ ] G-006 공용 API·DB 타입을 `packages/api-client`에서 소비하고 화면별 임시 타입 제거
- [ ] G-007 오류 코드·사용자 메시지·관찰 가능 로그를 공용 규약으로 통일
- [ ] G-008 legacy bundle/APK 생성물을 소스 변경과 분리하고 재현 가능한 build script 확정
- [ ] G-009 Supabase Auth/profile/subscription을 단일 사용자·entitlement SSOT로 확정하고 .NET 계정은 제거 또는 명시적 매핑
- [ ] G-010 Android application ID, iOS bundle ID, OAuth client, 딥링크 scheme을 하나의 제품 식별자로 통일
- [ ] G-011 노출된 signing/Forgejo/OAuth/JWT 자격 증명을 폐기·회전하고 CI secret으로 이동
- [x] G-012 .NET 고정 관리자 seed·정적 salt SHA-256 비밀번호·fallback JWT secret 제거
### A — 인증·회원가입·본인 계정 관리
- [ ] A-001 auth bootstrap splash와 session restore 구현
- [ ] A-002 인증 상태에 따라 Login/Main/Onboarding을 원자적으로 전환
- [ ] A-003 이메일 로그인 입력 검증·rate limit 메시지·세션 저장
- [ ] A-004 이메일 회원가입 화면과 약관 동의, 중복 계정 오류, 이메일 확인 대기 화면
- [ ] A-005 이메일 확인 딥링크와 확인 후 session 전환
- [ ] A-006 비밀번호 찾기 메일 요청·recovery 딥링크·새 비밀번호 설정
- [ ] A-007 Google OAuth PKCE 시작/취소/콜백/코드 교환/재실행 검증
- [ ] A-008 GitHub OAuth PKCE 시작/취소/콜백/코드 교환 검증
- [ ] A-009 Apple 로그인은 iOS에서 네이티브 정책 준수, Android에서는 지원 여부를 명확히 표시
- [ ] A-010 OAuth identity 충돌·동일 이메일 연결·provider 오류 처리
- [ ] A-011 프로필 이름·아바타·locale 조회/편집과 optimistic rollback
- [ ] A-012 본인 이메일·provider·가입일·마지막 로그인·구독 상태 확인 화면
- [ ] A-013 비밀번호 변경과 재인증 요구 처리
- [~] A-014 연결된 로그인 provider 목록과 연결/해제 안전장치 — 코드·Jest GREEN, 실제 provider 동의/연결/해제 E2E 대기
- [~] A-015 모든 로컬 민감 캐시를 지우는 로그아웃 및 navigation reset — Keychain·recorder·durable queue purge 코드·Jest GREEN, 최신 APK 재검증 대기
- [~] A-016 계정 삭제: 재인증, 명시 확인, 서버 cascade/보존 정책, 완료 후 로컬 purge — 서버 성공 후 로컬 purge 코드·Jest GREEN, 최신 APK 재검증 대기
- [~] A-017 access token 만료·refresh 실패·강제 로그아웃 E2E — 중앙 privacy purge와 session-loss/race/failure-retry Jest GREEN, 실제 token-expiry 기기 E2E 대기
- [ ] A-018 release 빌드에서 dev bypass와 dummy auth 성공 경로 완전 제거
### O — 온보딩·튜토리얼·테마·접근성
- [ ] O-001 첫 실행 welcome과 기존 사용자/신규 사용자 분기
- [ ] O-002 로그인 전 개인정보·클라우드 처리·녹음 권한 설명
- [ ] O-003 마이크 권한 요청/거부/다시 묻지 않음/설정 이동
- [ ] O-004 알림 권한은 가치 설명 뒤 요청하고 거부 시 기능 저하 안내
- [ ] O-005 첫 녹음→전사→공유를 안내하는 단계별 튜토리얼
- [ ] O-006 주요 탭과 파일 가져오기·동기화·결제 관리 튜토리얼
- [ ] O-007 튜토리얼 다시 보기·건너뛰기·완료 상태 동기화
- [ ] O-008 system/light/dark 테마와 D3RO 토큰 기반 렌더링
- [ ] O-009 테마·언어·햅틱·자동 교정·STT 옵션 저장 및 기기간 동기화
- [ ] O-010 글자 확대, TalkBack label/role, focus order, 48dp 터치 영역, 색 대비 검증
- [ ] O-011 reduced motion과 애니메이션 비활성 경로
- [ ] O-012 한국어/영어 필수 전체 번역과 하드코딩 문자열 제거
### D — 데이터 가져오기·동기화·오프라인
- [ ] D-001 `user_settings`, `devices`, `audio_files`, `processing_jobs` 스키마·RLS·Realtime 확정
- [ ] D-002 모바일 기기 등록, last seen, push token, 앱 버전 갱신
- [ ] D-003 연결 기기 목록·현재 기기 표시·원격 연결 해제
- [ ] D-004 데스크톱/Web에서 생성한 history/meeting/document/dictionary/knowledge/team 데이터 조회
- [ ] D-005 모바일 변경을 데스크톱/Web에 Realtime 또는 재조회로 반영
- [ ] D-006 최근 데이터 암호화 캐시와 로그인 사용자별 격리
- [ ] D-007 오프라인 생성·편집 queue, 재시도 backoff, 앱 재시작 복구
- [ ] D-008 충돌 정책: 서버 revision/updated_at 기반 감지와 사용자 선택 또는 안전 병합
- [ ] D-009 pull-to-refresh, background refresh, stale/offline 표시
- [ ] D-010 기존 데스크톱 내보내기 파일 가져오기와 형식 검증
- [ ] D-011 Web/모바일 JSON·CSV·TXT 호환 가져오기와 중복 idempotency
- [ ] D-012 데이터 전체 내보내기, 계정 데이터 다운로드, 공유
- [ ] D-013 부분 실패·중복 import·다른 사용자 데이터·손상 파일 fail-closed 테스트
- [ ] D-014 앱 삭제/재설치·다른 기기 로그인 후 클라우드 복원 E2E
### R — 녹음·모바일 파일·전사·공유
- [ ] R-001 Android `RECORD_AUDIO` runtime permission과 manifest 선언
- [ ] R-002 실제 RN recorder 모듈로 시작/일시정지/재개/중지/취소 구현
- [ ] R-003 오디오 레벨 meter와 timer가 실제 recorder state에서 파생
- [ ] R-004 앱 background 전환·전화/오디오 focus loss·프로세스 종료 복구
- [ ] R-005 Android foreground service와 지속 알림으로 장시간 녹음
- [ ] R-006 저장 공간 부족·마이크 사용 중·권한 철회·장치 오류 처리
- [ ] R-007 녹음 파일 메타데이터, content type, duration, checksum 생성
- [ ] R-008 Supabase Storage 사용자 경로 업로드와 RLS 검증
- [ ] R-009 업로드 진행률·취소·재시도·resume 가능한 queue
- [x] R-010 STT 함수 호출, 인증/쿼터/413/415/429/5xx 오류 계약 처리 — provider 호출 전 원자 reservation, 20-way 동시성·overage·환불·ACL integration GREEN
- [ ] R-011 전사 job 상태 Realtime/poll fallback과 앱 재시작 복원
- [ ] R-012 완료 transcript/history/audio_files를 한 transaction 계약으로 연결
- [ ] R-013 Android 파일 선택기로 m4a/mp3/wav/aac/flac/ogg/mp4/mov/webm 선택
- [ ] R-014 `content://`, cloud provider, 한글/공백 파일명, MIME 불일치 안전 처리
- [ ] R-015 대용량 파일 크기·시간 제한, 청크 업로드, 진행률, 취소, 재개
- [x] R-016 다른 앱 공유 시트 `ACTION_SEND`/`ACTION_VIEW` 오디오·비디오 수신 — native instrumentation 3/3와 별도 UID Android ContentProvider APK의 실제 `ACTION_SEND`→Record→durable queue→cancel cleanup E2E GREEN
- [ ] R-017 transcript/요약/회의록을 텍스트·Markdown·파일로 공유
- [ ] R-018 원본 오디오 공유는 사용자가 명시 선택한 경우에만 임시 URI 권한으로 제공
- [ ] R-019 전사 결과 복사·편집·저장·재처리·삭제
- [ ] R-020 녹음/파일 전사 정상·실패·취소·오프라인·재시작 E2E
- [x] R-021 Supabase/.NET STT provider 전체 실패 시 합성 transcript 성공을 제거하고 fail-closed — 502/503만 반환, 합성 성공 경로 0, Deno/.NET 계약 테스트 GREEN
- [x] R-022 .NET STT endpoint 인증·사용자 quota를 강제하고 익명 user fallback 제거 — 사용자 경로는 Supabase JWT+원자 quota SSOT, .NET provider endpoint는 32-byte service token 전용. anonymous 401, direct authenticated legacy 410, exact internal token만 provider orchestration
### H — 히스토리·회의·문서
- [ ] H-001 History 사용자 범위 pagination과 날짜 그룹
- [ ] H-002 전체/즐겨찾기/처리중 filter를 실제 query/state에 적용
- [ ] H-003 검색, 날짜/모드/상태 정렬, empty/error/retry UI
- [ ] H-004 히스토리 상세: 원문·교정문·메타데이터·오디오 재생
- [ ] H-005 즐겨찾기, 제목 변경, 편집, 재처리, 삭제, 복구 가능 확인
- [x] H-006 회의 생성: 제목·참석자·언어·템플릿
- [ ] H-007 회의 중 녹음·메모·부분 전사 timeline
- [ ] H-008 회의 종료와 전체 transcript 병합·처리 상태
- [ ] H-009 요약·결정사항·액션 아이템·회의록 생성
- [ ] H-010 transcript/summary/documents 탭 조회·편집·동시 수정 충돌
- [ ] H-011 오디오 seek와 transcript timestamp 연동
- [ ] H-012 PDF/DOCX/TXT/Markdown 내보내기와 Android 공유
- [ ] H-013 회의 목록 search/filter/sort/delete/share
- [ ] H-014 화자 구분 상태·화자명 편집·미지원 fallback
- [ ] H-015 Web↔모바일 동일 회의 교차 편집 E2E
### F — 데스크톱/Web 전체 기능의 모바일 화면
- [ ] F-001 Dashboard 통계·사용량·처리 job·구독·동기화 상태를 실제 데이터로 표시
- [ ] F-002 Dictionary 목록/검색/추가/편집/삭제/import/export/sync
- [ ] F-003 Custom instructions 목록/활성화/CRUD/reorder/sync
- [ ] F-004 Voice commands 목록/활성화/CRUD와 모바일 실행 가능한 action 구분
- [ ] F-005 Actions 실행, 권한 확인, 실행 결과·실패 기록
- [ ] F-006 AI text chat server 호출, streaming, 취소, retry, session history
- [ ] F-007 Push-to-talk→STT→LLM→TTS 음성 대화와 재생/중단
- [ ] F-008 custom instruction과 대화 backend/model/voice 선택
- [ ] F-009 Knowledge 문서 목록/검색/추가/삭제와 RAG query
- [ ] F-010 모바일 파일에서 지식 문서 가져오기와 처리 상태
- [x] F-011 Templates 목록/선택/CRUD와 회의·전사 문서 생성 연동
- [x] F-012 Memos 목록/CRUD/search/share/sync
- [ ] F-013 연결 상태·백엔드 건강·quota를 실제 probe 결과로 표시
- [ ] F-014 데스크톱 전용 기능은 이유와 모바일 대안을 명시하고 무반응 UI를 두지 않음
### T — 팀·관리·알림
- [ ] T-001 팀 목록/생성/이름 변경/삭제와 role 권한
- [ ] T-002 이메일/링크 초대, 딥링크 수락, 만료·중복·다른 계정 처리
- [ ] T-003 팀원 목록, admin/member/viewer 권한, 제거·나가기
- [ ] T-004 팀 회의·문서 공유와 RLS 교차 사용자 음성/문서 접근 차단
- [ ] T-005 팀 코멘트·활동 feed·알림
- [x] T-006 일반 사용자의 본인 데이터 관리와 관리자 전용 기능을 분리 — ordinary UI에서 Admin 숨김·관리 API request 0, manager/admin/super 분기 API 34 E2E PASS
- [x] T-007 admin/manager role 사용자는 사용자·구독·결제 감사 정보를 모바일에서도 안전 조회 — manager read/subscription update, stale JWT 403와 target/modal purge API 34 E2E PASS
- [x] T-008 role 변경·구독 수정 등 파괴적 관리 동작은 재확인·감사 로그·서버 권한 검증 — admin role 범위·delete confirm/cancel, super manager→admin, RPC readback과 cleanup 0 E2E PASS
- [ ] T-009 push token 등록/교체/로그아웃 삭제와 중복 방지
- [ ] T-010 전사 완료·팀 초대·댓글·결제 상태 push 수신과 해당 화면 딥링크
- [ ] T-011 알림 카테고리 설정·OS 권한 미허용 fallback
- [ ] T-012 foreground/background/terminated 상태 알림 E2E
- [x] T-013 Admin API 장애 때 realistic mock 운영 데이터를 반환하지 않고 명시 오류·stale 상태 표시 — stale-role 403 실측에서 `admin-error` 노출 및 기존 민감 state 제거
### M — 구독·구매·결제·Free tier 광고
- [x] M-001 서버 구독 상태와 quota를 읽는 entitlement provider 구현
- [x] M-002 Web Payple/Stripe 구독을 모바일에서 자동 인식하고 중복 구매 경고
- [x] M-003 Android Google Play Billing 라이브러리와 product ID 구성
- [x] M-004 Play product/offer 조회와 스토어가 반환한 현지 가격·기간 표시
- [x] M-005 Pro/Pro+ 구매 시작, 사용자 취소, pending, already-owned, 오류 처리
- [x] M-006 purchase acknowledgement와 앱 재시작 시 미완료 purchase 복구
- [x] M-007 구매 token을 클라이언트에서 신뢰하지 않는 서버 검증 함수
- [x] M-008 검증 결과를 idempotent purchase 원장과 subscriptions entitlement에 반영
- [x] M-009 Real-time Developer Notifications 또는 정기 검증으로 갱신·취소·환불·보류 반영
- [x] M-010 구매 복원은 실제 스토어 조회+서버 검증 후에만 성공 표시
- [x] M-011 구독 관리 버튼을 Google Play 구독 관리 화면으로 연결
- [x] M-012 현재 플랜·만료/갱신일·결제 provider·사용량·결제 내역 표시
- [~] M-013 가격·기능표를 서버/스토어 SSOT로 통일하고 hardcoded 가짜 가격 제거 — Web billing catalog·desktop checkout server readback·모바일 Play product 가격 사용, production Play/Payple/Stripe 교차 검증 대기
- [x] M-014 Google Mobile Ads SDK와 test/prod app ID·unit ID 분리
- [x] M-015 Free entitlement에만 adaptive banner를 안정적인 앱 레이아웃 영역에 표시
- [x] M-016 Pro/Pro+ entitlement에서는 광고 request 자체를 하지 않음
- [x] M-017 rewarded ad load/show/earned/closed/failure state machine
- [x] M-018 광고를 끝까지 보지 않거나 SDK 오류 시 보상 금지
- [x] M-019 서버 side verification 또는 signed reward nonce와 광고 보상 원장
- [x] M-020 동일 광고·nonce·사용자 중복 보상 차단과 cooldown/rate limit
- [x] M-021 서버 확정 잔액을 재조회한 뒤 UI에 반영하고 로컬 임의 증가 제거
- [x] M-022 광고 동의/연령/지역별 privacy options와 consent 상태 저장
- [x] M-023 광고 차단·네트워크 없음·no fill에서 핵심 Free 기능은 안전 fallback
- [x] M-024 debug 에뮬레이터에서 Google test banner/rewarded ad E2E
- [ ] M-025 Play Billing license tester에서 구매·복원·취소/pending E2E
- [x] M-026 결제/광고 receipt·token·개인정보를 로그와 analytics에 노출하지 않음
- [ ] M-027 Payple webhook 요청 인증·서명 검증과 replay 방지
- [x] M-028 Stripe `pro_plus` 상품 계약과 DB tier를 통일하고 subscriptions/profiles drift 방지
- [x] M-029 데스크톱 임의 checkout URL·무조건 성공 payment verify·legacy license 우회 제거 — 인증된 Stripe/Payple Edge-only 계약, trusted URL·server readback·deadline·idempotency fail-closed와 전체 desktop 590 tests GREEN
### Q — 자동화·Android 에뮬레이터·출시 게이트
- [x] Q-001 모바일 typecheck, lint, Jest 기본 gate 정비
- [x] Q-002 auth/account service 단위·통합 테스트
- [x] Q-003 recorder/upload/job/file import service 단위·통합 테스트
- [x] Q-004 sync/cache/conflict/retry 테스트
- [x] Q-005 subscription/IAP verification/idempotency 테스트
- [x] Q-006 ad entitlement/reward/cooldown/fraud 테스트
- [x] Q-007 Supabase migration을 새 로컬 DB에 전부 적용
- [x] Q-008 RLS cross-user/anonymous/admin 권한 SQL 테스트
- [x] Q-009 Edge Function auth/quota/content type/size/idempotency 테스트
- [~] Q-010 Android debug·release APK clean build — universal debug와 non-debuggable E2E 양 ABI clean build GREEN, production signed release APK/AAB는 외부 secret gate
- [~] Q-011 JS bundle이 포함된 release APK의 offline cold start — embedded bundle non-debuggable E2E APK cold start GREEN, production signed release artifact는 대기
- [ ] Q-012 에뮬레이터 API 35+ 생성·부팅·초기화 script
- [ ] Q-013 fresh install→onboarding→signup/login→logout E2E
- [ ] Q-014 OAuth provider redirect→callback→session→Main E2E
- [x] Q-015 녹음 권한→실제 오디오 fixture 주입/녹음→전사 결과 E2E
- [x] Q-016 파일 picker→fixture 선택→upload→전사→상세→공유 E2E
- [x] Q-017 Web seeded 데이터→모바일 조회/편집→Web 확인 sync E2E
- [x] Q-018 Free banner/rewarded test ad와 Pro 광고 미요청 E2E — official Google test ad 실측, ordinary Free 광고 노출과 manager/admin Pro 광고 비노출·미요청 API 34 PASS
- [ ] Q-019 Play Billing test purchase/restore/manage E2E
- [ ] Q-020 모든 탭·설정·팀·관리·실패 복구 journey E2E
- [ ] Q-021 회전·폰트 확대·TalkBack·dark/light·작은/큰 화면 visual gate
- [ ] Q-022 airplane mode·process kill·token expiry·server 5xx·중복 tap 회복 E2E
- [~] Q-023 앱 로그 crash/ANR/React error/민감정보 0건 확인 — exact `.9` auth/signup/invite/small-screen 및 admin role journeys에서 fatal/ANR/React fatal 0, production provider journey 민감정보 로그 감사 대기
- [x] Q-024 APK package/version/signature/SHA-256와 설치된 앱 버전 증거
- [x] Q-025 최종 증거 표에 명령·결과·artifact·스크린샷/영상 경로 기록
- [ ] Q-026 Node 24 LTS/JDK 17/Gradle 환경을 로컬·CI에서 통일
- [~] Q-027 debug x86_64+arm64, release arm64 APK/AAB ABI 정책과 에뮬레이터 호환성 검증 — universal E2E APK 정책 GREEN, 실제 production arm64 APK/AAB CI 검증 대기
- [~] Q-028 공식 배포가 `assembleRelease` 비-debuggable artifact만 허용하도록 fail-closed — signed evidence·hash/version/package/cert/prod AdMob·create-only 경계와 negative self-test GREEN, 실제 production AAB/CI/secrets 대기
## 5. 구현 순서와 게이트
1. Gate 0 — G 기반, 실제 Supabase 구성, auth root, 테스트 harness
2. Gate 1 — A 계정 전체 + O 온보딩/권한/테마
3. Gate 2 — R 녹음·파일 전사 + H 히스토리 상세·공유
4. Gate 3 — D 동기화/가져오기/오프라인 + F 전체 기능 화면
5. Gate 4 — H 회의/문서 + T 팀/관리/알림
6. Gate 5 — M 실제 Play Billing·광고·서버 검증
7. Gate 6 — Q 전체 자동화와 Android 에뮬레이터 clean-room E2E
각 Gate는 다음 Gate로 가기 전 typecheck/unit/integration와 해당 에뮬레이터 journey를 통과한다. 뒤 단계에서 회귀가 발견되면 해당 작업을 다시 `[ ]`로 되돌리고 수정·재검증한다.
## 6. 외부 시스템 준비 항목
아래 항목은 구현을 멈추는 이유가 아니다. Google test ID, 로컬 Supabase, fixture account로 코드와 자동화를 먼저 완성한다. 다만 production 완료 표시는 실제 콘솔 증거가 있어야 한다.
- [~] EXT-001 Supabase production URL/anon key, redirect allow-list, OAuth provider secrets — production Auth와 Google provider entry는 GREEN, GitHub/Apple provider secret 및 실제 mobile consent callback은 남음
- [!] EXT-002 Play app-signing SHA-256은 확인했지만 Android OAuth/Firebase client의 package·SHA-1/SHA-256 등록과 live App Links 배포가 남음
- [!] EXT-003 Google Play Console 앱 `4976102237698469110` / package `com.d3ro.voice`는 생성. Pro/Pro+ base plan/offer/product IDs와 track 설정은 남음
- [!] EXT-004 Play Billing license tester와 테스트 결제 수단
- [!] EXT-005 Google service account, Android Publisher API, RTDN Pub/Sub
- [~] EXT-006 AdMob app `ca-app-pub-1039714767792854~6427959892`, banner `/9840591290`, rewarded `/2255790918`, SSV 설정 — identity/URL SSOT는 확정. `검토 필요`·`광고 게재 제한`·결제 프로필·store 미연결·test-device 실제 지급 E2E 대기
- [x] EXT-007 개인정보 처리방침·이용약관·계정 삭제 공개 URL — NAS host/runtime에 동일 해시로 좁게 배포, `/privacy/`, `/terms/`, `/delete-account/` 외부 HTTPS 200·정확한 title·삭제 요청 링크 확인. 모바일 Settings와 Paywall에서 canonical URL 진입
- [!] EXT-008 local upload key·Ed25519 evidence keypair는 ACL/readback/roundtrip GREEN. production Firebase/AdMob/signing/evidence secret의 CI 보관·보호 환경 승인·복구 백업 및 `1.1.0`/`1010001` production AAB workflow는 대기
- [!] EXT-011 Firebase Console `u/0`, `u/1` 모두 D3RO project 0개 — 사용자 action-time 승인 후 production project·Android app·Play fingerprint·FCM 생성/연결
- [!] EXT-009 Fold6 physical-device install/share/OAuth/purchase 증거 — exact `.11` APK Taildrop 전송 완료, 사용자 설치·실행·공유/OAuth/구매 확인 필요
- [!] EXT-010 Play closed test 12명·연속 14일 완료와 production access 승인 — 2026-08-29 현재 0명, access disabled
## 7. 검증 증거 표
| Gate | 명령/시나리오 | 결과 | 증거 경로 | 확인 시각 |
|---|---|---|---|---|
| Historical baseline | `git status --short --branch` | 2026-08-21 당시 `main`이 remote보다 2 commit 앞서고 dirty 변경이 다수였다. 현재 release SHA로 재사용하지 않는 역사 증거다 | 본 문서 1절 | 2026-08-21 |
| Mobile lint | `npm --prefix apps/mobile-rn run lint` | GREEN, warning/error 0 (`--max-warnings=0`) | `apps/mobile-rn/.eslintrc.js`, `package.json` | 2026-08-21 |
| Mobile typecheck | `npm --prefix apps/mobile-rn run typecheck` | GREEN | `apps/mobile-rn/tsconfig.json` | 2026-08-21 |
| Mobile unit | `npm --prefix apps/mobile-rn test` | 전체 통합 tree 57 suites / 353 tests GREEN; 중앙 account-local privacy purge, cold/session-loss/A→B auth 경계, secure logout, identity safety, mobile admin, rewarded/banner retry·template/memo·meeting creation·recording language·generation idempotency 포함 | `apps/mobile-rn/__tests__` | 2026-08-24 |
| Supabase local | migration-up + shadow replay 00001~00028 및 domain integration | 기존 domain integration에 더해 AdMob receipt 원장의 unknown/ineligible/cooldown replay, account-delete unlink, ACL, 교차 사용자·동시 요청을 검증했다. mobile-platform 85/85, reward race 2회 연속, SSV Deno 6/6, db lint error 0, shadow diff empty, fixture cleanup 0 | `server/supabase/migrations/20260821000028_ad_reward_receipt_replay_protection.sql`, `server/supabase/tests`, `server/supabase/functions/_shared/admob-ssv.test.ts` | 2026-08-24 |
| Android build | `:app:assembleDebug -PreactNativeArchitectures=arm64-v8a,x86_64` | universal BUILD SUCCESSFUL; 두 ABI의 screens/gesture/Nitro/Whisper SO 완전성 확인 후 x86_64 clean cold launch GREEN, SHA-256 `E4647A4C041174DF2B2F094EF7BB61B4B01DD10A379DB32CCBE4105231E04A7C` | `apps/mobile-rn/android/app/build/outputs/apk/debug/app-debug.apk`, `scratch/demo/d3ro-mobile-dual-abi-installed.png` | 2026-08-21 |
| Latest Android E2E test artifact | JDK 17 `:app:assembleE2e -PreactNativeArchitectures=arm64-v8a,x86_64` + artifact/build-config verifiers | BUILD/verifier GREEN; `com.d3ro.voice`, version `0.0.0-e2e.20260821.11`/`202608221`, non-debuggable, embedded bundle 3,808,068 B·Whisper model 77,691,713 B/SHA 검증, Google test AdMob ID, arm64+x86_64. APK 146,739,935 B, SHA-256 `74035B69DB0A564846DF5ED4B85CA5D0E2289D5BB7D73DEE02C35DFD57380C2B`. API 34 exact-APK fresh install와 cold launch GREEN. production artifact가 아니다 | `apps/mobile-rn/android/app/build/outputs/apk/e2e/app-e2e.apk`, `scripts/ci/verify-android-artifact.mjs`, `scripts/ci/verify-mobile-build-config.mjs` | 2026-08-21 |
| Public Forgejo distribution | 최신 E2E TEST-DEMO 고유 asset upload + anonymous redownload hash 대조 | PENDING; `.11` SHA-256 `74035B69…380C2B` 공개 업로드·anonymous redownload 검증은 수행하지 않음 | Forgejo release asset | 2026-08-21 |
| Mobile release boundary | `npm run release:mobile:boundary:test` + source-contract/security/syntax/YAML checks | GREEN; release mode·expected version/package/cert/prod AdMob·Ed25519 signed evidence와 artifact hash 일치, immutable snapshot, hardlink/reparse/path-swap/static APK 우회·overwrite·test AdMob/debug signer/nonrelease 거부를 검증. 실제 production APK/AAB에는 아직 실행하지 않음 | `scripts/ci/mobile-release-evidence-lib.mjs`, `scripts/ci/verify-mobile-release-boundary.mjs`, `.github/workflows/release.yml` | 2026-08-21 |
| Production Android release gate | release/config/artifact/provenance self-tests + Play/Firebase/AdMob live 재확인 | release identity `1.1.0`/`1010001`, Play app ID `4976102237698469110`, package `com.d3ro.voice`, app-signing SHA-256, local upload/evidence key, AdMob identity를 확인했다. Play는 `임시`, 설정 0/11, AAB 0건, closed tester 0/12명, production access disabled다. Firebase project·CI secret 주입·AAB가 없으므로 현재 판정은 RED다 | `release/product-version.json`, `release/android-release-identity.json`, `scripts/ci/verify-mobile-release-config.mjs`, `scripts/ci/verify-android-artifact.mjs`, live consoles | 2026-08-29 |
| App Links release identity | source/live exact certificate 대조 | `site/public`, `apps/web/public`, `apps/api-server/wwwroot`의 source 3개는 Play app-signing SHA-256으로 갱신. live는 아직 폐기된 과거 certificate를 반환하므로 deploy·public 재검증 전까지 RED | `*/.well-known/assetlinks.json`, `scripts/ci/verify-android-app-links.mjs`, live `d3ro.chanpaca.net` | 2026-08-29 |
| Public legal and deletion resources | scoped NAS host/container deploy + anonymous HTTPS render | 개인정보처리방침, 이용약관, 앱 외부 계정 삭제 요청 경로를 기존 사이트 전체와 분리해 배포했다. host와 running container 4개 파일 SHA 일치; `/privacy/`, `/terms/`, `/delete-account/` 모두 외부 HTTPS 200·정확한 title, privacy→deletion 링크 visible. 실서버 본문 운영자는 `YUN CHAN`, Cloudflare email-protection 복호화 연락처는 `yunchan8804@gmail.com`, `TWENTYOZ` 잔존은 0건이다. Google Play 정책상 필요한 앱 식별·개발자 문의·수집/공유·보관/삭제·외부 삭제 요청 경로를 포함하고 모바일 Settings/Paywall에도 canonical link를 노출 | `site/public/legal.css`, `site/public/privacy/index.html`, `site/public/terms/index.html`, `site/public/delete-account/index.html`, `apps/mobile-rn/src/screens/SettingsScreen.tsx`, live `d3ro.chanpaca.net` | 2026-08-21 |
| Android upload signing identity | create-only 3072-bit RSA PKCS12 + Windows credential vault + certificate readback | 2026-08-21에 생성한 local upload-key 후보 SHA-256은 `4F:AC:69:24:82:1C:50:DA:AB:ED:76:49:32:A5:3C:48:6F:8C:6C:5F:34:B9:F1:8D:B9:20:AA:40:99:15:2B:54`다. 다만 production CI secret·오프라인 복구 백업·실제 AAB signer 증거는 없다. Play Console upload certificate는 첫 AAB 업로드 후 표시되며, 현재 확인한 Play app-signing SHA-256과 분리해 대조해야 한다 | `release/android-release-identity.json`, `scripts/gen-keystore.js`, Windows Credential Manager | 2026-08-29 |
| Release evidence signing identity | Ed25519 keypair creation + public-key fingerprint + sign/verify roundtrip | private key는 `C:\Users\encep\.d3ro\release\d3ro-mobile-evidence-ed25519-private.pem` user-only ACL, public key는 `release/mobile-release-evidence-public.pem`이다. keyId `2797d3e63affd2348941bc2871b57e520c958f7f5da1a4bbe8aa46904a890b7f`, pair/roundtrip GREEN. CI private-key secret·오프라인 복구 백업·실제 signed evidence는 대기 | `release/mobile-release-evidence-public.pem`, `release/android-release-identity.json`, `scripts/ci/verify-release-evidence-key-pair.mjs` | 2026-08-29 |
| Firebase project readiness | Firebase Console `u/0`, `u/1` live 확인 | 두 계정 모두 D3RO project가 없다. production project·Android app·`google-services.json`·FCM은 사용자 승인 후 새로 준비해야 한다 | Firebase Console, `apps/mobile-rn/android/app` | 2026-08-29 |
| AdMob production identity | live console + release identity exact match | app `ca-app-pub-1039714767792854~6427959892`, banner `ca-app-pub-1039714767792854/9840591290`, rewarded `ca-app-pub-1039714767792854/2255790918`는 일치한다. 그러나 `검토 필요`, `광고 게재 제한`, store 미연결, 결제 프로필 미완료라 production 게재 GREEN이 아니다 | `release/android-release-identity.json`, AdMob live console | 2026-08-29 |
| Production Supabase deployment | linked migration/function deploy + live AdMob probes | remote migration `00001`~`00028`이 local과 일치하고 `admob-ssv` v13이 ACTIVE다. rewarded unit secret 존재, unsigned missing-query는 200, unknown key는 503, AdMob URL verification은 GREEN이다. 00028 전후 schema dump는 각각 SHA-256 `92CAA70E…A1DF`, `7545ADD2…EB4E`로 보존했다. Stripe/Play/Resend/STT/FCM provider gates와 실제 Google-signed reward 지급 E2E는 남음 | `scratch/production-backups/admob-ssv-before-20260821000028-20260824.sql`, `scratch/production-backups/admob-ssv-after-20260821000028-20260824.sql`, `server/supabase/migrations/20260821000028_ad_reward_receipt_replay_protection.sql`, `server/supabase/functions/admob-ssv` | 2026-08-24 |
| Production DB credential | linked schema dump + Supabase CLI source audit | CLI `db dump --dry-run`이 credential을 stdout에 출력하지만, CLI 공식 소스 대조 결과 primary DB password가 아니라 API가 매 작업마다 생성하고 만료시키는 `cli_login_postgres` 임시 역할 password다. primary rotation은 불필요하며 저장소·Supabase trace 잔존 0. 해당 dry-run 명령은 credential 출력 때문에 재사용 금지 | Supabase CLI `internal/utils/flags/db_url.go`, `scratch/prod-public-schema-before-mobile-20260821.sql` | 2026-08-21 |
| Physical Fold6 | exact `.11` APK Taildrop→`z-fold6` | TRANSFERRED over active direct Tailnet; `D3RO-Voice-0.0.0-e2e.20260821.11-TEST-DEMO-74035b69db0a.apk`, 146,739,935 B, SHA-256 `74035B69DB0A564846DF5ED4B85CA5D0E2289D5BB7D73DEE02C35DFD57380C2B`. 실제 기기 설치·실행·OAuth·구매는 사용자 확인 PENDING | Fold6 Taildrop/runtime | 2026-08-21 |
| Mobile secure auth/account | secure storage·logout·recording cleanup·identity linking Jest | Keychain strict response 검증, legacy migration, local purge, remote sign-out 실패 fallback, recorder/queue cleanup, provider link/unlink 안전장치 GREEN. 최신 APK logout/delete/provider E2E는 PENDING | `apps/mobile-rn/src/lib/secure-auth-storage.ts`, `apps/mobile-rn/src/lib/logout.ts`, `apps/mobile-rn/__tests__` | 2026-08-21 |
| Mobile admin E2E | ordinary/manager/admin/super_admin/stale-manager API 34 journeys | 55 assertions PASS. Free test 광고 표시·관리 request 0, Pro 광고 미요청, manager 구독 credits/memo 수정, admin bounded role+delete confirm/cancel, super manager→admin, stale JWT 즉시 403와 target/modal purge. auth/profile/operation/audit fixture 0 | `apps/mobile-rn/.maestro-output/admin-local/evidence-role-e2e-pass.json`, `admin-local-ordinary-free-ad-visible.png`, `admin-local-admin-bounded-role-options.png`, `admin-local-stale-manager-forbidden-purged.png` | 2026-08-21 |
| Android incoming share | main/androidTest Kotlin compile + API 34 instrumentation + external provider E2E | 허용 MIME만 수신, UI thread 밖 provider I/O, size/name/type cap, provider 예외·unsupported/empty/oversize·비 AAC MP4 fail-closed 구현. slow provider 비차단, provider `SecurityException`, unsupported video one-shot failure 3/3 GREEN. 별도 UID ContentProvider APK의 WAV를 `ACTION_SEND`해 Record 라우팅, app-owned cache copy, retry queue, cancel cleanup까지 GREEN; canonical cache path와 RN cache alias 불일치도 실제 E2E에서 발견·교정 | `apps/mobile-rn/android/app/src/main`, `apps/mobile-rn/android/app/src/androidTest`, `apps/mobile-rn/.maestro/incoming-share-local-check.yaml`, `scripts/ci/run-mobile-incoming-share-local-e2e.ps1` | 2026-08-21 |
| Emulator recorder | 권한→FGS 녹음→HOME→통화 focus loss→resume→SIGKILL→cold recovery→discard | GREEN, 실제 WAV 성장/header/journal/notification cleanup 확인 | API 34 `emulator-5554` logcat + 앱 DB/cache inspection | 2026-08-21 |
| Emulator STT/Talk | 실제 Korean WAV/import→STT→history/share, PTT WAV→STT→Claude SSE→TTS/stop | GREEN; cloud provider 미구성은 503 fail-closed, local Whisper fallback은 인증 우회 없이 동작 | API 34 `emulator-5554`, public Supabase rows | 2026-08-21 |
| Auth live | production `/auth/v1/settings`, Google authorize canonical redirect, disposable email login/logout/delete | Email·Google provider entry GREEN; settings는 Google=true/GitHub=false/Apple=false, Google authorize 302→`accounts.google.com`, Supabase canonical callback·state/client ID present. full Google consent→mobile callback와 GitHub/Apple은 PENDING | production Supabase + API 34 emulator | 2026-08-21 |
| Billing test | restore/provider/entitlement unit+hook integration | 3 suites / 28 tests GREEN; sideload는 `storeInstallRequired` fail-closed. Play license test는 EXTERNAL | `apps/mobile-rn/__tests__/billing*.test*`, `entitlement.test.ts` | 2026-08-21 |
| Ads test | Free Google test banner→rewarded 완주 + production 콘솔/SSV 연결 | 공식 `Test Ad` banner/`AdActivity`/`Reward granted` 실측과 미구성 시 fail-closed를 유지한다. production ID, `50 cloud_ai_tokens`, secret, verified SSV URL, Edge v13, receipt exactly-once는 운영 반영했다. production 광고를 클릭하지 않았으며 test-device 실제 Google-signed reward E2E는 남음 | `scratch/demo/d3ro-mobile-test-ad.png`, `d3ro-mobile-rewarded-test-ad.png`, `server/supabase/functions/admob-ssv`, AdMob live console | 2026-08-24 |
| Admin live-local | login→JWT proxy→role/subscription/audit + stale-role | .NET 21/21, migration 25, Edge/JWT 47 assertions, TLS Chromium console/5xx 0 | `scratch/admin-browser-e2e.png` | 2026-08-21 |
| STT fail-closed quota | Deno/.NET contracts + 00026 SQL/REST concurrency | provider 전체 실패 502/503, direct legacy user 410, internal exact service token only. 20 concurrent remaining-base 요청에서 exactly 1 reserve, replay single charge, 3 overage exact, provider failure refund, authenticated ledger/RPC denial, fixture 0 | `server/supabase/functions/stt-proxy`, `apps/api-server/Controllers/SttController.cs`, `server/supabase/migrations/20260821000026_stt_quota_reservations.sql`, `apps/mobile-rn/__tests__/stt-quota.local.integration.mjs` | 2026-08-21 |
| Desktop payment·ads | authenticated Edge-only checkout/subscription readback + no-fill·위조 event·reward·settlement 경계 | 임의 provider/tier/URL, 무조건 성공 verify, legacy local license 우회를 제거하고 timeout·retry idempotency를 fail-closed로 검증. 광고 20/20, 전체 desktop 590 tests, typecheck/build/scoped lint GREEN; 실제 production provider 결제 E2E는 외부 게이트 | `apps/desktop/src/main/ipc/payment-handlers.ts`, `apps/desktop/tests/unit/payment-handlers.spec.ts`, `scratch/demo/d3ro-desktop-ads-fail-closed.png` | 2026-08-21 |
| Credential gate | repository runtime secret scan + release-keystore audit + API fail-closed bootstrap | 평문 keystore password를 포함하던 untracked 생성 스크립트를 발견해 환경변수·PKCS12·create-only로 교체하고 scanner에 keytool literal negative case 추가. 노출된 키는 live direct APK cert `06:EE:…:E4:81`과 동일해 `d3ro-release-key.COMPROMISED-20260821.keystore`로 보존 격리했으며 active path에서 제거. scanner self-test/전체 scan, .NET 26/26, script syntax GREEN | `scripts/gen-keystore.js`, `scripts/ci/check-no-hardcoded-secrets.mjs`, `.github/workflows/ci.yml` | 2026-08-21 |
| Exact-APK cold launch | fresh uninstall/install→`am start -W`→top activity/logcat | GREEN; exact `.11` SHA-256 `74035B69…380C2B` fresh install, `LaunchState:COLD`, total 0.872초, `MainActivity` top-resumed. fresh auth·signup·invite PASS, fatal/ANR/React fatal 0 | API 34 `emulator-5556`, installed package inspection + Maestro + logcat | 2026-08-21 |
| Exact-APK Maestro E2E | exact `.11` TEST-DEMO fresh install + auth/signup/invite, 직전 `.9` small-screen journey | `.11` GREEN: fresh-install-auth 30s, fresh-install-signup 40s, invite warm listener+cold `getInitialURL` 14s. 동일 UI `.9`의 320x568dp signup 2m39s PASS. `.11` 전체 구간 fatal/ANR/React fatal 0 | `apps/mobile-rn/.maestro/*.v11.junit.xml`, `apps/mobile-rn/.maestro-output`, `apps/mobile-rn/.maestro-output/final-20260821-9` | 2026-08-21 |
| Android instrumentation | API 34 `connectedDebugAndroidTest` | 4 tests, failures 0, errors 0: CSPRNG 1 + incoming share 3 GREEN | `apps/mobile-rn/android/app/build/outputs/androidTest-results/connected/debug/TEST-Pixel_7_API_34(AVD) - 14-_app-.xml`, `apps/mobile-rn/android/app/build/reports/androidTests/connected/debug` | 2026-08-21 |
## 8. 종료 감사
최종 종료 전 아래 검색과 대조를 수행한다.
- `apps/mobile-rn/src`에서 `TODO`, `placeholder`, mock 성공, 무반응 handler, 고정 사용자 데이터가 0건인지 확인한다.
- 데스크톱 navigation·service 목록, Web route 목록, 본 문서 F/H/T 항목을 다시 대조한다.
- 모든 `Pressable`, `PhysicalButton`, switch, 링크가 정상·disabled·오류 상태를 갖는지 검사한다.
- 모든 Supabase query에 사용자/RLS 경계와 pagination/error 처리가 있는지 검사한다.
- 결제·광고 보상은 서버 검증 전에 entitlement/토큰을 바꾸는 경로가 0건인지 검사한다.
- 에뮬레이터를 초기화한 clean install과 기존 사용자 upgrade install을 모두 검증한다.
- 실제 설치 APK의 version/signature/hash와 화면에 표시된 버전을 대조한다.

View file

@ -0,0 +1,401 @@
# D3RO Voice 모바일 제품화 핸드오프
> 작성 시각: 2026-08-21 KST
> 마지막 사실 동기화: 2026-08-29 KST
> 제품 런타임: `apps/mobile-rn`
> 구현·검증 정본: [`MOBILE_APP_COMPLETION_SSOT.md`](./MOBILE_APP_COMPLETION_SSOT.md)
> 2026-08-21 Git 기준선: `main` @ `a9c9a1ca6efdfc83ae7a12a0c1f2a05f15ef9138` (역사 증거, 현재 release SHA 아님)
> release identity: version `1.1.0`, Android versionCode / iOS build `1010001`
> 판정: **내부 기능·local upload/evidence key·AdMob identity는 준비됐지만 production AAB·Firebase·CI secret 주입·closed test가 없어 Play 출시는 RED**
## 0. 2026-08-29 release readiness 스냅샷
| 영역 | 현재 사실 | 다음 게이트 |
|---|---|---|
| Play 앱 | `D3RO Voice`, `com.d3ro.voice`, app ID `4976102237698469110`; Console 상태 `임시` | 첫 AAB 업로드 후 설정 0/11 채우기 |
| 버전 | product `1.1.0`, Android versionCode / iOS build `1010001` | AAB·desktop artifact·release record가 같은 identity를 가리키는지 CI에서 증명 |
| Play App Signing | SHA-256 `01:00:19:21:DB:4F:33:40:85:CE:21:E4:B8:DE:CC:BD:71:DA:87:67:C5:6E:3B:59:83:2A:A1:C8:29:EA:0D:AB` 실확인 | local upload key는 있다. 첫 AAB 후 Console 표시가 local cert와 일치하는지 증명 |
| App Links | source `assetlinks.json` 3개는 Play SHA-256으로 갱신 | live는 아직 구 certificate라 배포·public 재검증 필요 |
| Play 트랙 | 첫 AAB 0건, closed tester 0명 | 12명을 연속 14일 유지하고 production access 신청·승인 |
| production access | disabled | 승인 전에 production/open testing 가능으로 간주 금지 |
| local release keys | upload PKCS12 private-key readback GREEN; Ed25519 evidence keypair roundtrip GREEN | CI secret 업로드·오프라인 복구 백업·실제 AAB 서명 대기 |
| production 입력 | AAB·Firebase config·AdMob/upload/evidence CI secret 주입 증거 없음 | protected secret store와 승인 경계에 주입한 후 fail-closed build |
일반적인 “배포해” 지시는 Data safety, App content, production access 답변과 같은 외부 법적·정책 선언의 대리 승인이 아니다. 실제 Console 문구를 사용자가 읽고 action-time 승인하기 전에는 저장·제출하지 않는다.
## 0.1 2026-08-24 AdMob 재개 결과
- 정본 계정 `yunchan8804@gmail.com`에서 AdMob 계정과 D3RO Voice Android 앱이 생성된 상태를 재확인했다.
- App ID: `ca-app-pub-1039714767792854~6427959892`
- banner: `ca-app-pub-1039714767792854/9840591290`
- rewarded: `ca-app-pub-1039714767792854/2255790918`
- rewarded contract: `50 cloud_ai_tokens`
- SSV URL: `https://llnocwyqvhgwpdjcqqyw.supabase.co/functions/v1/admob-ssv`; AdMob의 URL 확인 성공 후 저장하고 새로고침 뒤 영속 상태를 재확인했다.
- Supabase production migration `20260821000028``admob-ssv` Edge v13을 배포했다. unknown key는 HTTP 503, 영구 거부는 HTTP 200으로 응답한다.
- 보상 transaction은 `ad_reward_receipts.transaction_id UNIQUE`에서 먼저 소비한다. cooldown/cap/ineligible/unknown-user도 영구 terminal 처리하며 계정 삭제 시 사용자 UUID는 NULL로 분리한다.
- live 상태는 `검토 필요`, `광고 게재 제한`, Play store 미연결, 결제 프로필 미완료다. production signed AAB와 test-device/Play 설치본의 실제 Google-signed SSV 지급 E2E도 남았다.
## 1. 가장 먼저 읽을 것
1. Google/Firebase/Play/AdMob의 정본 계정은 **`yunchan8804@gmail.com`**이다.
2. Play 개발자 계정은 **`YUN CHAN`**, 개발자 ID는 **`7374613349497835192`**다.
3. **TWENTYOZ 계정으로 프로젝트·앱·광고 계정을 만들거나 제출하면 안 된다.** 잘못된 계정에서는 실제 제출한 항목이 0건이다.
4. 과거 Git 커밋의 author email `yunchan@twentyoz.kr`은 저장소 메타데이터일 뿐이며 Google Console 계정 선택 근거가 아니다.
5. 현재 worktree에는 여러 작업자의 대규모 수정·신규 파일이 함께 있다. `git reset --hard`, `git checkout --`, `git clean`, 광범위한 포맷·삭제를 하면 안 된다.
6. 보존된 APK 증거는 **Google TEST AdMob + debug signer의 TEST-DEMO**다. Play production 또는 공식 signed APK로 게시하면 안 된다.
7. 이 문서와 모바일 SSOT는 현재 untracked다. 정식 릴리스 전 관련 소스·migration·Edge Function·문서를 검토 가능한 커밋으로 보존해야 한다.
## 2. 현재 한눈에 보기
| 영역 | 현재 상태 | 출시까지 남은 것 |
|---|---|---|
| 모바일 기능 | 인증, 온보딩, 테마, 녹음/전사, Talk, History, 회의, 팀, Push 계약, portability, template/memo, billing, ads, admin 구현 | 외부 provider의 production journey |
| 모바일 정적 검증 | typecheck·ESLint GREEN | 없음 |
| 모바일 Jest | **57 suites / 353 tests GREEN** | 없음 |
| Android E2E | API 34 exact `.11` TEST-DEMO auth/signup/invite 및 cold launch GREEN | production-signed artifact로 재실행 |
| Supabase DB/Edge | production migration `00001`~`00028`, AdMob SSV v13 ACTIVE·rewarded secret 설정 | FCM drain cron, 실제 provider-signed E2E |
| 법적 페이지 | production HTTPS 200 | Play Console 등록정보에 URL 입력 |
| Firebase | Console `u/0`, `u/1` 모두 D3RO project 없음; production Android config·CI secret 없음 | 사용자 승인 후 정본 프로젝트/Android 앱·Play fingerprint·FCM 연결 |
| Play Console | 앱 `4976102237698469110` 생성, App Signing SHA-256 확인; 상태 `임시`, 설정 0/11, AAB 0건 | AAB·스토어 설정·상품·internal/closed track·12명 14일·production access |
| AdMob | App/Banner/Rewarded ID·SSV URL 설정 완료 | `검토 필요`·`광고 게재 제한`·지급 프로필·store 미연결·UMP/test-device E2E |
| production APK/AAB | 없음 | secret 주입 후 signed APK+AAB build/verifier/CI |
| Fold6 | `.11` TEST-DEMO Taildrop 전송 완료 | 설치·실행·공유·OAuth·구매 실증 |
## 3. 계정과 브라우저 상태
### Firebase
- Firebase Console `u/0``u/1`을 2026-08-29에 모두 재확인했고 D3RO 프로젝트는 0개였다.
- production `google-services.json`, Android 앱 등록, Play app-signing fingerprint, FCM/CI secret은 없다.
- 새 Cloud/Firebase 프로젝트와 Android 앱 등록은 영구 외부 상태를 만든다. 사용자에게 계정·프로젝트 ID·Analytics 선택·생성 범위를 보여준 뒤 action-time 승인을 받는다.
### Google Play Console
- 계정: `yunchan8804@gmail.com`
- 개발자 계정: `YUN CHAN`
- 개발자 ID: `7374613349497835192`
- 앱: `D3RO Voice`, package `com.d3ro.voice`, app ID `4976102237698469110`
- 상태: `임시`; 설정 0/11, 첫 AAB 0건
- Play app-signing SHA-256: `01:00:19:21:DB:4F:33:40:85:CE:21:E4:B8:DE:CC:BD:71:DA:87:67:C5:6E:3B:59:83:2A:A1:C8:29:EA:0D:AB`
- Console upload certificate 표시: 첫 AAB 업로드 후. local upload key 존재 여부와는 다른 상태다.
- closed test: 0명 / 요건 12명, 연속 14일
- production access: disabled
앱 생성 당시 정책·수출법 선언은 사용자 승인 후 완료됐다. 이후 Data safety, App content, production access 답변도 동일하게 사용자의 법적·정책 진술이므로 action-time 승인 전에 제출하지 않는다.
### AdMob
- 계정 표시: `yunchan8804@gmail.com` (`authuser=1`)
- D3RO Voice Android 앱과 production App/Banner/Rewarded ID: 생성 완료
- rewarded `50 cloud_ai_tokens`: 저장 완료
- production SSV URL: Google 확인 및 저장 완료
- 지급 프로필: 미완료. 이 상태에서는 앱 검토가 보류된다.
- live 상태: `검토 필요`, `광고 게재 제한`
- store 등록정보: 미연결. 앱 검토와 Privacy & messaging/UMP production 검증: 대기
`authuser=0`은 정본 계정 경로가 아니다. 이후에도 `authuser=1`과 화면의 `yunchan8804@gmail.com` 표시를 같이 확인한다.
### 사용자에게 필요한 다음 확인
Firebase 프로젝트/Android 앱 생성, 지급·세금 프로필, Data safety/App content 답변, production access 신청은 각각 외부 상태를 영구적으로 바꾼다. 실제 Console 문구와 입력할 사실을 보여준 뒤 해당 액션별로 명시 승인을 받는다.
## 4. 완료된 핵심 구현과 증거
상세 요구사항별 상태는 모바일 SSOT 4절과 7절이 정본이다. 핵심 실행 증거만 요약한다.
- 모바일 lint: warning/error 0
- 모바일 typecheck: GREEN
- 모바일 Jest: 57 suites / 353 tests GREEN
- API 34 TEST-DEMO exact APK:
- package `com.d3ro.voice`
- version `0.0.0-e2e.20260821.11`
- versionCode `202608221`
- non-debuggable
- arm64-v8a + x86_64
- embedded Hermes bundle + Whisper model verified
- cold launch 0.872초, top-resumed
- fresh auth 30초, signup 40초, invite warm+cold 14초 PASS
- fatal/ANR/React fatal 0
- Android instrumentation: CSPRNG 1 + incoming-share 3, 총 4/4 GREEN
- 모바일 Admin 역할 E2E: ordinary/manager/admin/super_admin/stale-manager 55 assertions PASS, fixture 0
- Free tier Google TEST banner/rewarded 실제 렌더 및 SSV 미구성 시 credit 0 fail-closed 확인
- 녹음 FGS, HOME, 통화 audio focus, process kill 복구, 알림 discard, 실제 WAV 복구 확인
- Talk: WAV→STT→Claude SSE→Android TTS/stop 확인
- 외부 ContentProvider `ACTION_SEND`→Record→durable queue→cancel cleanup 확인
- production Supabase:
- linked ref `llnocwyqvhgwpdjcqqyw`
- migration `00001`~`00028` local/remote 일치
- Edge Functions 25개 ACTIVE
- `admob-ssv` v13, rewarded unit secret 존재, AdMob URL verification GREEN
- 익명 user/admin 함수 fail-closed 401 및 admin `no-store`
- 배포 전후 production aggregate 불변
## 5. 현재 산출물
### TEST-DEMO APK
- 빌드 경로: `apps/mobile-rn/android/app/build/outputs/apk/e2e/app-e2e.apk`
- 전달 복사본: `scratch/D3RO-Voice-0.0.0-e2e.20260821.11-TEST-DEMO-74035b69db0a.apk`
- 크기: `146,739,935` bytes
- SHA-256: `74035B69DB0A564846DF5ED4B85CA5D0E2289D5BB7D73DEE02C35DFD57380C2B`
- Fold6 Taildrop 파일명: `D3RO-Voice-0.0.0-e2e.20260821.11-TEST-DEMO-74035b69db0a.apk`
이 APK는 debug certificate와 Google TEST AdMob ID를 사용한다. 공식 배포, Play internal testing, production 광고 검증에 사용하지 않는다.
### 준비된 Android upload key
- PKCS12: `C:\Users\encep\.d3ro\release\d3ro-upload-key-20260821.p12`
- alias: `d3ro-upload-20260821`
- password vault target: `D3ROVoice-Android-Upload-20260821`
- certificate SHA-256: `4F:AC:69:24:82:1C:50:DA:AB:ED:76:49:32:A5:3C:48:6F:8C:6C:5F:34:B9:F1:8D:B9:20:AA:40:99:15:2B:54`
- file/parent directory user-only ACL과 private-key readback: GREEN
- 별도 오프라인 복구 백업: 아직 필요
- protected CI secret 등록: 아직 필요
- Play Console upload certificate 표시·local cert 대조: 첫 AAB 업로드 후 가능. Console에 아직 표시되지 않는다고 key가 없는 것은 아니다.
암호·PKCS12를 출력, 채팅, Git, 일반 로그, browser upload 임시 경로에 남기지 않는다.
### release-evidence Ed25519 keypair
- private: `C:\Users\encep\.d3ro\release\d3ro-mobile-evidence-ed25519-private.pem`, 저장소 밖 user-only ACL
- public: `release/mobile-release-evidence-public.pem`
- keyId: `2797d3e63affd2348941bc2871b57e520c958f7f5da1a4bbe8aa46904a890b7f`
- private/public 일치·sign/verify roundtrip: GREEN
- protected CI private-key secret 등록·오프라인 복구 백업: 아직 필요
### 폐기된 key
- `apps/mobile-rn/android/app/d3ro-release-key.COMPROMISED-20260821.keystore`
- 재사용 금지
- 현재 live `https://d3ro.chanpaca.net/.well-known/assetlinks.json`에는 이 과거 계열 certificate fingerprint `06:EE:C7:57:...:E4:81`가 남아 있다.
- source 3개는 Play app-signing SHA-256 `01:00:19:21:...:EA:0D:AB`로 교체했다. 배포 후 public HTTPS 응답이 정확히 이 한 개 fingerprint만 반환하는지 검증한다. upload certificate와 혼동하지 않는다.
### production schema 백업
- `scratch/prod-public-schema-before-mobile-20260821.sql`
- SHA-256: `35CDD059669D097C58B98D0ADF157B49A1AAA2FF03D9AC6497E84F550511A5AC`
- `supabase db dump --dry-run`은 임시 DB credential을 stdout에 출력하므로 재사용하지 않는다.
## 6. 공개 법적 리소스
- 개인정보처리방침: `https://d3ro.chanpaca.net/privacy/`
- 이용약관: `https://d3ro.chanpaca.net/terms/`
- 앱 외부 계정 삭제: `https://d3ro.chanpaca.net/delete-account/`
세 URL 모두 production HTTPS 200이며 운영자는 `YUN CHAN`, Cloudflare email-protection 복호화 연락처는 `yunchan8804@gmail.com`, TWENTYOZ 문자열은 0건이다.
주의: 법적 파일은 NAS host와 현재 컨테이너 양쪽에 좁게 배포했으나, 현재 compose의 `/app/wwwroot` 영속 mount가 확실하지 않다. 컨테이너 재생성 전에 mount 계약을 고치거나 재검증한다. dirty한 `site/dist` 전체를 NAS에 덮어쓰지 않는다.
## 7. 재개 순서
### 7.1 Firebase 생성과 Android 등록
1. `u/0`, `u/1` 모두 D3RO 프로젝트가 없다는 2026-08-29 기준선에서 시작한다.
2. 사용자에게 Google 계정·프로젝트 ID·Analytics 선택을 보여주고 명시 승인을 받은 후에만 새 프로젝트를 생성한다.
3. Android 앱 `com.d3ro.voice`를 등록한다.
4. `google-services.json`을 내려받아 로컬에서는 `apps/mobile-rn/android/app/google-services.json`에 둔다.
5. 파일을 Git에 커밋하지 않는다. CI에는 base64로 `ANDROID_GOOGLE_SERVICES_JSON_B64` secret을 주입한다.
6. FCM service account를 최소 권한으로 준비하고 Supabase secret에 `FCM_SERVICE_ACCOUNT_JSON`, `FCM_PROJECT_ID`를 설정한다.
7. Play Console의 app-signing SHA-1/SHA-256을 Firebase Android 앱에 추가한다. upload key fingerprint만으로 production OAuth/App Links를 완료 처리하지 않는다.
### 7.2 AdMob
완료:
1. `authuser=1` / `yunchan8804@gmail.com`의 Android app `ca-app-pub-1039714767792854~6427959892`, banner `ca-app-pub-1039714767792854/9840591290`, rewarded `ca-app-pub-1039714767792854/2255790918`을 만들었고 `release/android-release-identity.json`과 대조했다.
2. rewarded 값을 `50 cloud_ai_tokens`로 저장했다.
3. Supabase secret `ADMOB_REWARDED_AD_UNIT_ANDROID`를 production rewarded unit과 맞췄다.
4. production `admob-ssv` Edge v13과 durable receipt migration을 배포했다.
5. AdMob에서 SSV URL을 확인·저장하고 새로고침 뒤 영속 상태를 확인했다.
남음:
1. 사용자가 결제·세금 프로필을 직접 작성한다.
2. Play store 등록정보와 AdMob 앱을 연결하고 `검토 필요`·`광고 게재 제한` 상태를 해소한다.
3. Privacy & messaging의 production UMP 메시지를 게시하고 EEA/regulated-US/non-EEA를 검증한다.
4. build/CI secret:
- `ADMOB_APP_ID`
- `ADMOB_BANNER_UNIT_ID`
- `ADMOB_REWARDED_UNIT_ID`
5. Play internal 설치본과 등록된 test device로 실제 reward 완료→Google-signed SSV→receipt/claim exactly-once→credit +50을 확인한다.
6. 본인·팀의 production 광고 클릭으로 테스트하지 않는다. Google test ads 또는 공식 test device만 사용한다.
### 7.3 Play 앱, App Signing, 상품
1. `YUN CHAN` / developer ID `7374613349497835192` / app ID `4976102237698469110`를 확인한다.
2. version `1.1.0`, versionCode `1010001`의 production AAB를 보호된 CI에서 생성·검증한다.
3. 첫 AAB를 internal track에 제한 업로드한 뒤 upload certificate를 확인하고 Play app-signing certificate와 다름을 증명한다.
4. 확인된 app-signing SHA-256으로 갱신한 다음 source 3개를 배포한다.
- `site/public/.well-known/assetlinks.json`
- Firebase Android certificate fingerprints
- Google OAuth Android client가 필요하면 package/certificate 조합
5. subscription product를 정확한 ID로 만든다.
- `d3ro_voice_pro_monthly`
- `d3ro_voice_pro_plus_monthly`
6. 두 상품 모두 활성 월간 base plan과 구매 가능한 offer를 가져야 한다. offer tag `default`를 권장하며, 없으면 앱은 첫 offer를 사용한다.
7. license tester, internal testing tester, 실제 SKU/offer를 설정한다.
8. Google Play Developer API service account를 앱에 연결하고 Supabase secret을 설정한다.
- `GOOGLE_PLAY_SERVICE_ACCOUNT_JSON`
- `GOOGLE_PLAY_PACKAGE_NAME=com.d3ro.voice`
- RTDN 사용 시 `GOOGLE_PLAY_PUBSUB_AUDIENCE`
- RTDN 사용 시 `GOOGLE_PLAY_PUBSUB_SERVICE_ACCOUNT`
9. RTDN Pub/Sub topic/subscription과 `google-play-rtdn` 인증 경계를 실제 메시지로 검증한다.
### 7.4 Push drain과 초대 이메일
- production invite canonical origin은 `https://d3ro.chanpaca.net`이다.
- 필요한 Supabase secrets:
- `SITE_URL`
- `RESEND_FROM`
- `RESEND_API_KEY`
- `FCM_SERVICE_ACCOUNT_JSON`
- `FCM_PROJECT_ID`
- durable push outbox drain:
- `POST /functions/v1/send-push?mode=drain&limit=100`
- header `apikey: <service secret>`
- body `{}`
- pg_cron + pg_net + Vault를 사용하고 service secret을 SQL, migration, 로그에 평문으로 넣지 않는다.
- 실제 2-device 부분 실패, retry, stale token, logout token 제거, notification open allowlist를 production FCM으로 재검증한다.
### 7.5 production provider secrets
현재 production DB/Edge 코드는 배포됐지만 다음 provider 경로는 실제 자격 증명과 콘솔 설정이 필요하다.
- Google Play/RTDN
- AdMob 실제 Google-signed reward 지급 E2E(콘솔/secret/SSV URL은 완료)
- Firebase Cloud Messaging
- Resend
- STT: `GROQ_API_KEY`, `OPENAI_API_KEY`, `DEEPGRAM_API_KEY` 또는 내부 `D3RO_API_URL` + `D3RO_API_TOKEN`
- LLM: `ANTHROPIC_API_KEY`
- Stripe: secret, Pro/Pro+ price, webhook secret
- Payple: production merchant/return/webhook configuration
secret 누락 상태는 의도적으로 fail-closed다. 가짜 값이나 성공 mock으로 통과시키지 않는다.
## 8. production build와 CI
### 필수 protected CI secrets
- `ANDROID_RELEASE_KEYSTORE_B64`
- `ANDROID_GOOGLE_SERVICES_JSON_B64`
- `ANDROID_UPLOAD_CERT_SHA256`
- `ANDROID_PLAY_APP_SIGNING_CERT_SHA256`
- `ANDROID_RELEASE_STORE_PASSWORD`
- `ANDROID_RELEASE_KEY_ALIAS`
- `ANDROID_RELEASE_KEY_PASSWORD`
- `ADMOB_APP_ID`
- `ADMOB_BANNER_UNIT_ID`
- `ADMOB_REWARDED_UNIT_ID`
- `ANDROID_RELEASE_EVIDENCE_PRIVATE_KEY_B64`
- `ANDROID_RELEASE_EVIDENCE_PUBLIC_KEY_B64`
2026-08-29 기준 upload/evidence private key는 local external path에, AdMob ID는 release identity에 준비됐지만 production Firebase/AdMob/signing/evidence secret을 실행 가능한 CI에 주입하지 않았다. 복구 백업도 대기다. `mobile-production-release` protected environment 승인과 trusted main/tag 경계를 유지한다.
### 릴리스 빌드 계약
- JDK 17
- package `com.d3ro.voice`
- release version: `1.1.0`; Android versionCode / iOS build: `1010001`. CI run number로 다시 계산하지 않고 release SSOT의 고정된 단조 증가 값을 사용
- arm64-v8a production APK/AAB
- `google-services.json` 필수
- production AdMob App/Banner/Rewarded ID 필수
- 준비한 `d3ro-upload-20260821` key를 CI에 보호 주입하고 실제 AAB signer를 검증한다. upload 인증서와 Play app-signing 인증서는 서로 달라야 하며,
App Links/Firebase 공급자 등록에는 Play app-signing fingerprint를 사용한다.
- TEST AdMob ID, debug signer, x86-only, missing Firebase config면 gate가 실패해야 정상
정본 게이트는 실제 remote에 연결된 `.gitlab-ci.yml`과 동등 계약의 `.github/workflows/release.yml`이다. 현재 remote/runner/secrets가 실제로 연결된 파이프라인을 사용하고, 로컬 ad-hoc publish로 대체하지 않는다.
주요 검증:
```powershell
npm --prefix apps/mobile-rn run lint
npm --prefix apps/mobile-rn run typecheck
npm --prefix apps/mobile-rn run test:ci
npm run security:secrets:test
npm run security:secrets
npm run release:mobile:boundary:test
node scripts/ci/verify-mobile-build-config.mjs
```
CI는 signed APK+AAB를 만든 뒤 다음을 검증한다.
- BuildConfig version/build type/Firebase/production ad IDs
- package/version/versionCode
- upload signer certificate
- arm64 native library 완전성
- embedded Hermes/Whisper model hash
- APK/AAB metadata·signer·AdMob 동등성
- Ed25519 signed release evidence
- App Links certificate
- publication artifact hash/create-only 경계
## 9. Play internal testing 완료 조건
아래가 모두 실제 Play 설치본에서 GREEN이어야 production 완료로 볼 수 있다.
1. Internal track AAB upload 성공
2. Play 설치본의 `installedFromPlayStore=true`
3. 신규 설치/기존 로그인/온보딩/theme/permission
4. Google OAuth consent→Supabase callback→앱 session
5. Google Play Pro 구매
6. Pro→Pro+ upgrade 및 replacement mode
7. restore, pending, cancel, billing issue, subscription management deep link
8. server purchase verification, acknowledge, entitlement snapshot 일치
9. Free production banner 및 rewarded 표시
10. SSV 완료 전 credit 0, SSV 이후 exactly-once credit
11. Pro/Pro+에서 광고 요청·표시 0
12. FCM 실제 수신, open allowlist, logout 후 token/device cleanup
13. `ACTION_SEND` audio/video, 파일 import, 전사, History, 공유
14. 계정 로그아웃·삭제·session loss 후 모든 로컬 민감 데이터 purge
15. Fold6 cold launch, background recorder, share, OAuth, purchase
16. fatal/ANR/React fatal/민감 로그 0
## 10. 절대 하지 말 것
- TWENTYOZ Google/AdMob/Firebase/Play 계정 사용
- 사용자 확인 없이 Play 수출법/정책 또는 AdSense 약관 제출
- TEST-DEMO APK를 official/release/signed 이름으로 공개
- 폐기된 `d3ro-release-key.COMPROMISED-20260821.keystore` 재사용
- upload certificate를 Play app-signing certificate처럼 사용
- `google-services.json`, service-account JSON, keystore password, OAuth secret을 Git/로그/채팅에 노출
- `supabase db dump --dry-run` 출력 공유
- dirty worktree를 reset/clean/checkout하거나 광범위하게 덮어쓰기
- NAS 전체 `wwwroot` 또는 dirty `site/dist`를 통째로 배포
- provider 장애·secret 누락·store sideload를 성공으로 간주
- production 광고를 직접 클릭해 테스트
## 11. 재개자가 먼저 실행할 15분 체크
```powershell
git status --short --branch
Get-FileHash apps/mobile-rn/android/app/build/outputs/apk/e2e/app-e2e.apk -Algorithm SHA256
Test-Path apps/mobile-rn/android/app/google-services.json
npm --prefix apps/mobile-rn run typecheck
npm --prefix apps/mobile-rn run lint
npm --prefix apps/mobile-rn run test:ci
npm run security:secrets
```
기대값:
- branch, HEAD, remote tracking 상태를 현재 출력대로 새 release record에 기록. 2026-08-21의 `2 commit ahead`는 역사 기준선일 뿐 현재 기대값이 아님
- E2E APK SHA-256 `74035B69...380C2B`
- `google-services.json`은 Firebase 등록 전까지 없음
- typecheck/lint/Jest/security scan GREEN
그 다음 브라우저에서 계정을 다시 확인하고, 사용자 확인을 받은 뒤 7절 순서대로 진행한다.
## 12. 아직 완료라고 말하면 안 되는 이유
- Firebase `u/0`, `u/1` 모두 D3RO 프로젝트가 없고 production Android config도 없음
- production AdMob ID/SSV는 확정했지만 `검토 필요`·`광고 게재 제한`·결제 프로필·store 연결·실제 지급 E2E가 아직 없음
- Play 앱은 생성됐고 App Signing SHA-256도 확인했지만 첫 AAB·Console upload-certificate 대조·상품·internal track·설정 11개가 아직 없음
- upload/evidence keypair는 local에 준비됐지만 CI secret 등록·복구 백업·실제 AAB signature/evidence가 없음
- production signed APK/AAB와 CI 실행 증거가 아직 없음
- live App Links가 폐기된 과거 certificate를 가리킴
- closed tester가 0/12명이고 14일 기간을 시작하지 않았으며 production access가 disabled임
- 실제 Google OAuth callback, Play purchase/restore/cancel, production ads/SSV, FCM이 아직 외부 E2E되지 않음
- Fold6는 APK 전달만 됐고 설치·실행 확인이 없음
- 대규모 dirty/untracked 변경이 아직 릴리스 가능한 버전관리 상태로 정리되지 않음
이 항목들이 모두 증거로 닫히기 전에는 “모바일 제품화 완료” 또는 “공식 출시 완료”라고 판정하지 않는다.

View file

@ -0,0 +1,49 @@
# D3RO Voice Red E2E 500+ — AI 콘텐츠 신고
기준일: 2026-08-25
이 suite는 실패를 기대하는 **Red-path** E2E다. 테스트가 RED여야 한다는 뜻이 아니라,
잘못된 요청·권한 우회·소유권 위반·중복·rate limit·관리자 전이를 실제 local
Supabase Gateway → Edge Function → PostgreSQL 경로에서 거부하는지를 검증한다. 모든
scenario는 GREEN이어야 한다.
## 실행 경계
- 실행기: `server/supabase/tests/content-report-red.e2e.mjs`
- 명령: `npm run test:e2e:red`
- 대상: `http://127.0.0.1:55321`인 disposable local Supabase만 허용
- production/preview URL, production key, 원격 계정에는 fail-closed한다.
- 매 실행마다 임시 사용자 4개를 만들고, 생성된 report를 service role로 먼저 정리한
뒤 auth user를 삭제한다. 원문 snapshot·token·email·service key는 출력하지 않는다.
- PostgREST/RPC에는 `sb_secret_` local key를 사용한다. 현 local GoTrue admin 삭제는
legacy `service_role` JWT가 필요한 CLI 호환 경계라 fixture cleanup에만 사용하며,
remote 환경에는 실행 자체를 허용하지 않는다.
## 정확한 scenario 수: 650
| family | 수 | 실제 E2E oracle |
| ---------------------------------- | --: | ------------------------------------------------------------------------------- |
| 허용되지 않은 HTTP method | 4 | `405 method_not_allowed`, `Cache-Control: no-store` |
| 잘못된/없는 content type | 6 | `415 content_type_must_be_json` |
| 인증 우회 | 3 | `401` |
| idempotency key 없음·위조 | 96 | `400` fail-closed |
| root body 누락·추가 key | 64 | strict exact-key `400 invalid_request` |
| kind 위조 | 64 | `ai_output` 이외 차단 |
| source type·generation ID 위조 | 96 | allowlist·UUID·shape 차단 |
| 신고 사유 위조 | 96 | reason allowlist 차단 |
| snapshot 공백·초과·타입 위조 | 96 | 1~4000 code point 경계 차단 |
| comment 공백·초과·타입 위조 | 96 | optional 1~500 code point 경계 차단 |
| 유효 source 3종 | 3 | Talk/Commands/Actions receipt → `201` |
| 멱등성·source ownership·ledger ACL | 7 | replay/conflict/duplicate/cross-user/purpose mismatch/unknown/direct table 차단 |
| rate limit | 11 | 10번째까지 `201`, 11번째 `429` |
| moderation ACL·전이 | 8 | 일반 사용자 차단, manager queue/review/replay/conflict/dismiss/terminal lock |
합계는 **650**이다. 실행기는 suite 시작 전에 stateless 621개, 종료 전에 전체 650개를
강제 검증하므로 누군가 case를 줄이면 테스트가 fail한다.
## 포함하지 않는 증거
이 suite는 Edge/DB Red E2E다. Play가 서명한 실기기에서의 Talk·Commands·Actions
신고 sheet, 실제 provider response header, production moderation 운영, 광고·결제·FCM
E2E는 대체하지 않는다. 그 경계는 [Play 출시 체크리스트](../play/04-release-checklist.md)에
별도로 남아 있다.

View file

@ -0,0 +1,126 @@
# 스토어 등록정보 초안
## 공통 메타데이터
| 필드 | 입력안 | 상태·근거 |
| --------- | -------------------------- | ------------------------------------------------------------------------ |
| 앱 이름 | `D3RO Voice` | **후보**, 10자. 앱 label과 제품명에 일치 |
| 앱/게임 | 앱 | **확정**, 생산성 앱 |
| 무료/유료 | 무료 | **후보**, Free 요금제 + 선택형 구독·광고 모델 |
| 기본 언어 | 한국어 (`ko-KR`) | Play 생성 양식 준비값 |
| 카테고리 | 생산성 (`Productivity`) | **후보** |
| 태그 | 음성 녹음, 전사, 메모/노트 | **검증 필요**, Console이 제공하는 실제 태그 중 기능과 일치하는 것만 선택 |
| 패키지 | `com.d3ro.voice` | `apps/mobile-rn/android/app/build.gradle:289-302` |
Play Console이 허용하는 스토어 제목은 30자, 짧은 설명은 80자, 전체 설명은 4,000자 이하여야 한다. 아래 문자 수는 줄바꿈을 제외한 해당 필드 원문 기준이며, 입력 후 Console의 실제 카운터를 다시 확인한다.
## `ko-KR`
### 앱 이름
```text
D3RO Voice
```
### 짧은 설명 — 42자
```text
녹음·전사·AI 문서화·팀 협업까지, 음성을 바로 활용할 수 있는 기록으로.
```
### 전체 설명
```text
말한 내용을 놓치지 않고, 다시 활용할 수 있는 기록으로 바꿔보세요.
D3RO Voice는 음성 녹음과 미디어 가져오기, 전사, AI 보조 문서화, 메모와 팀 협업을 한곳에서 이어주는 생산성 앱입니다.
주요 기능
• 마이크 녹음: 지속 알림에서 녹음 상태를 확인하고 일시정지, 재개, 중지 또는 폐기할 수 있습니다.
• 음성·미디어 가져오기: 앱이 검증할 수 있는 오디오 형식과 MP4 파일을 선택해 처리할 수 있습니다.
• 전사 경로: 인증된 클라우드 전사를 사용하고, 지원되는 Android 환경에서는 제공자 장애 시 기기 내 전사를 대체 경로로 사용합니다.
• 회의와 기록: 전사문, 회의록, 메모, 템플릿과 생성 문서를 계정에 정리합니다.
• AI 보조: 대화형 AI와 문서 생성 기능으로 전사 내용을 요약하고 다음 작업을 구체화할 수 있습니다.
• 팀 협업: 초대한 팀원과 팀 회의 및 관련 기록을 공유하고 역할을 관리할 수 있습니다.
• 데이터 관리: 계정 데이터 내보내기·복원, 기기 관리와 계정 삭제 경로를 제공합니다.
요금제와 광고
Free 요금제에는 Google AdMob 배너 광고가 표시될 수 있습니다. 사용자가 선택해 보상형 광고를 완료하면 서버 검증 후 앱 내부 AI 사용량 크레딧을 받을 수 있습니다. 선택형 Google Play 구독은 추가 사용량과 광고 없는 이용 환경을 제공할 수 있습니다. 실제 가격과 갱신 조건은 구매 화면과 Google Play 결제 창에 표시됩니다.
개인정보와 책임 있는 사용
마이크 권한은 녹음을 시작할 때 요청합니다. 음성·텍스트는 사용자가 클라우드 기능을 실행한 경우 필요한 범위에서 처리되며, 광고 요청은 동의 및 개인정보 옵션을 확인한 뒤 시작합니다. 개인정보처리방침과 계정 삭제 경로는 앱 설정에서 언제든 확인할 수 있습니다.
자동 전사와 AI 생성 결과에는 오류가 있을 수 있습니다. 중요한 사실과 결정을 직접 검토하고, 의료·법률·재무 또는 안전에 중대한 전문 조언으로 사용하지 마세요. 타인의 음성을 녹음하거나 콘텐츠를 공유할 때는 관련 법률과 필요한 동의를 지켜야 합니다.
```
## `en-US`
### App name
```text
D3RO Voice
```
### Short description — 76 characters
```text
Record, transcribe, and turn conversations into practical AI-assisted notes.
```
### Full description
```text
Turn spoken ideas into records you can review, organize, and use again.
D3RO Voice brings microphone recording, supported media import, transcription, AI-assisted documents, notes, and team collaboration into one productivity app.
Key features
• Microphone recording: See an ongoing notification and pause, resume, stop, or discard a recording.
• Supported media import: Select audio formats and MP4 files that the app can independently validate and process.
• Transcription paths: Use authenticated cloud transcription, with on-device transcription as a fallback on supported Android environments when the cloud provider is unavailable.
• Meetings and records: Organize transcripts, meeting notes, memos, templates, and generated documents in your account.
• AI assistance: Use conversational AI and document generation to summarize transcripts and shape follow-up work.
• Team collaboration: Share team meetings and related records with invited members and manage member roles.
• Data controls: Export or restore account data, manage registered devices, and request account deletion.
Plans and ads
The Free plan may show Google AdMob banner ads. When a user chooses to complete a rewarded ad, in-app AI usage credits are granted only after server verification. Optional Google Play subscriptions may provide additional usage and an ad-free experience. Current price, renewal, and cancellation terms are shown in the purchase screen and Google Play billing flow.
Privacy and responsible use
Microphone permission is requested when recording starts. Audio and text are processed only as needed when the user invokes a cloud feature, and ad requests begin only after the applicable consent and privacy-options flow. The privacy policy and account-deletion path remain available from Settings.
Automated transcripts and AI-generated output can be incorrect. Review important facts and decisions yourself, and do not treat the output as professional medical, legal, financial, or safety advice. Follow applicable law and obtain any required consent before recording another person or sharing content.
```
## 지원 정보
| 필드 | 입력안 | 상태 |
| ---------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| 지원 이메일 | `yunchan8804@gmail.com` | **후보**. 공개 법적 페이지와 일치. 배포 전 수신·응답 가능 여부 확인 |
| 개인정보처리방침 | `https://d3ro.chanpaca.net/privacy/` | **확정**, 앱 Settings 링크와 일치 |
| 이용약관 | `https://d3ro.chanpaca.net/terms/` | **확정**, 앱 가입·Settings·Paywall 링크와 일치 |
| 계정 삭제 | `https://d3ro.chanpaca.net/delete-account/` | **확정**, Data safety의 삭제 URL 후보 |
| 웹사이트 | 입력 보류 | **차단 아님**. `https://d3ro.chanpaca.net/` 루트가 지원 사이트로서 200과 유효한 콘텐츠를 제공하기 전에는 등록하지 않는다 |
| 전화번호 | 비워 둠 | 선택 필드. 실제 공개 지원 번호가 생기기 전에는 임의 값 금지 |
## 등록정보 제출 전 대조
- 설명에 넣은 기능은 release AAB를 Play internal track으로 설치한 실제 화면에서 모두 접근 가능해야 한다.
- `Free` reviewer 계정에서 배너와 보상형 광고 경로를, 별도 유료 reviewer 계정에서 광고 미요청과 구독 기능을 확인한다.
- AI 신고 source와 local tests는 구현·GREEN이지만 아직 production migration/function 배포, purge scheduler·운영 담당 연결과 Play 설치본 E2E가 없다. 이 운영 경로가 GREEN이 되기 전에는 AI 보조 문구가 포함된 등록정보를 제출하지 않는다.
- Play icon 512×512와 canonical SVG source는 `docs/v3/play/assets/`에 생성됐고 Android legacy/adaptive/monochrome launcher resource에도 반영됐다. Pixel Launcher emulator의 adaptive circle mask는 `assets/proof/d3ro-icon-pixel-launcher-20260824.png`로 확인됐다. 다만 Play-signed release 설치본과 실제 Pixel/Samsung 등 OEM mask·scale 검증은 아직 필요하다.
- feature graphic 1024×500 후보 4개와 재현·해시 기록은 `assets/feature-graphic/`에 준비됐다. 현재 권장안은 A지만 사용자 선택과 Console preview 검증 전에는 최종 자산으로 확정하지 않는다.
- 실제 앱 화면 스크린샷 최소 2장(권장 4장 이상)은 아직 준비해야 한다. 로그인/초대 E2E 증거 이미지는 스토어 자산으로 사용하지 않는다.
- 자막이나 합성 UI가 실제 기능을 과장하지 않는지 확인한다. [Google의 등록정보 정확성 기준](https://support.google.com/googleplay/android-developer/answer/15191715?hl=en)을 따른다.
## 코드 근거
- 녹음·foreground service: `apps/mobile-rn/src/screens/RecordScreen.tsx`, `apps/mobile-rn/android/app/src/main/java/com/d3ro/voice/D3RORecordingService.kt`
- 미디어 가져오기 MIME 경계: `apps/mobile-rn/android/app/src/main/AndroidManifest.xml:88-116`
- 클라우드→기기 내 전사 fallback: `apps/mobile-rn/src/features/import/audio-transcription-service.ts:739-786`
- AI 대화: `apps/mobile-rn/src/screens/TalkScreen.tsx`, `apps/mobile-rn/src/features/talk/llm-stream-service.ts`
- 팀 협업: `apps/mobile-rn/src/screens/TeamsScreen.tsx`, `apps/mobile-rn/src/screens/TeamDetailScreen.tsx`
- 광고 eligibility·동의: `apps/mobile-rn/src/lib/mobile-ads-context.tsx:65-175`
- 비개인화 배너·보상형 광고: `apps/mobile-rn/src/components/FreeTierBanner.tsx:44-73`, `apps/mobile-rn/src/lib/mobile-ads-context.tsx:225-240`
- Play 구독: `apps/mobile-rn/src/lib/billing-context.tsx`, `apps/mobile-rn/src/screens/ProPaywallScreen.tsx`
- 데이터 이동성·삭제: `apps/mobile-rn/src/features/data-portability/`, `apps/mobile-rn/src/screens/AccountScreen.tsx:410-455`

View file

@ -0,0 +1,121 @@
# Data safety 초안
## 제출 원칙
이 문서는 현재 Android 코드와 공개 개인정보처리방침을 바탕으로 만든 **보수적 초안**이다. Google Play의 Data safety는 특정 사용자 한 명의 기본 흐름이 아니라 Play에서 배포하는 모든 활성 버전·지역·연령의 데이터 처리를 합산해 답해야 한다. release AAB의 최종 SDK/manifest, 운영 provider 설정, 계약상 service-provider 지위와 실제 네트워크 흐름을 확인하기 전에는 제출하지 않는다.
상태 기준:
- **확정**: 코드 또는 Google SDK 공식 disclosure로 데이터 흐름이 확인됨
- **후보**: 현재 정보로 선택할 답변안
- **검증 필요**: 실제 release/provider 증거에 따라 답이 바뀔 수 있음
- **보수적 fallback**: 검증을 끝내지 못했다면 더 넓게 공개할 답
## Console 상위 질문
| 질문 | 답변안 | 상태·근거 |
| ----------------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| 앱이 필수 사용자 데이터 유형을 수집하거나 공유하는가? | `예` | **확정**. 계정·콘텐츠·광고·결제·기기 등록이 존재 |
| 전송 중 데이터가 암호화되는가? | `예` | **후보**. release는 cleartext를 막음(`apps/mobile-rn/android/app/build.gradle:372-379`). 실제 release endpoint와 provider 전체가 HTTPS인지 재검증 |
| 사용자가 데이터 삭제를 요청할 수 있는가? | `예` | **확정**. 앱 내 삭제와 외부 요청 경로 존재 |
| 계정 생성 기능이 있는가? | `예` | **확정**. 이메일/OAuth 가입 제공 |
| 독립적인 보안 검토를 받았는가? | `아니요` | 인증된 독립 보안 표준·배지가 실제로 생기기 전에는 선택 금지 |
| Families 정책을 따르기로 약정하는가? | `아니요/해당 없음` | 타깃 연령 후보는 18세 이상이며 아동 대상 앱이 아님 |
## 데이터 유형별 입력 초안
`공유`는 단순히 외부 서버로 전송하는지와 다르다. Google이 인정하는 service-provider, 사용자 주도 전송 등 예외가 성립하는지는 계약과 실제 사용 목적을 증명해야 한다. 아래에서 `아니요*`는 그 예외가 입증될 때만 유효하다.
| Google Play 데이터 유형 | 수집 | 공유 | 목적 후보 | 선택/필수 | 일시 처리 | 판단·증거 |
| ------------------------------------------- | ------------ | ------------ | ----------------------------------- | ----------------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 대략적 위치 | 예 | 예 | 광고, 분석, 사기 방지·보안 | 필수(광고가 활성인 Free 흐름) | 아니요 | **확정**. Google Mobile Ads SDK가 IP 주소를 자동 수집·공유하고 일반 위치 추정에 사용할 수 있음 |
| 정확한 위치 | 아니요 | 아니요 | — | — | — | 위치 permission과 기능이 없음. release merged manifest 재확인 |
| 이름 | 예 | 아니요\* | 앱 기능, 계정 관리, 팀 협업 | 필수(가입 프로필) | 아니요 | 가입 표시 이름·프로필. Supabase service-provider 예외와 팀원에게 보이는 사용자 주도 공유 범위 검증 |
| 이메일 주소 | 예 | 아니요\* | 앱 기능, 계정 관리, 팀 초대, 보안 | 필수(계정) | 아니요 | 이메일 로그인/OAuth와 팀 초대. 인증·메일 provider 계약 검증 |
| 사용자 ID | 예 | 아니요\* | 앱 기능, 계정 관리, 사기 방지·보안 | 필수 | 아니요 | Supabase user UUID, 팀/결제/보상/기기 owner ID |
| 기타 개인정보 | 예 | 아니요\* | 앱 기능, 개인화, 계정 관리 | 필수/일부 선택 | 아니요 | locale, 테마, 모델 선호, tier, 팀 역할 등. Console 질문에서 이 범주 적용 여부 최종 대조 |
| 주소·전화번호·인종/민족·정치/종교·성적 지향 | 아니요 | 아니요 | — | — | — | 현재 모바일 기능에서 요청하지 않음. reviewer/support 폼과 provider가 추가 수집하지 않는지 확인 |
| 사용자 결제 정보 | 아니요 | 아니요 | — | — | — | 카드 번호는 앱이 수집·저장하지 않고 Google Play 결제 UI가 처리. Android 앱에서 별도 결제 수단을 추가하면 재평가 |
| 구매 내역 | 예 | 아니요\* | 앱 기능, 계정 관리, 사기 방지·보안 | 선택(구매 사용자) | 아니요 | 상품 ID, purchase token, 거래/구독 상태를 서버 검증·보관. Google Play와의 전송이 공유 예외인지 최종 대조 |
| 건강·피트니스 정보 | 아니요 | 아니요 | — | — | — | 관련 기능/permission 없음 |
| 이메일 또는 문자 메시지 | 아니요 | 아니요 | — | — | — | 사람 간 메신저가 없음. 팀 초대 주소와 내용이 이 유형에 해당하는지 Console 정의를 다시 확인 |
| 사진 | 아니요 | 아니요 | — | — | — | 사진 선택·업로드 기능 없음 |
| 동영상 | 예 | 아니요\* | 앱 기능 | 선택 | 아니요 | `video/mp4` 가져오기를 지원하고 전사에 사용. STT provider service-provider 지위에 따라 공유를 `예`로 바꿀 수 있음 |
| 오디오 파일 | 예 | 아니요\* | 앱 기능 | 선택 | 아니요 | 녹음/가져오기, 클라우드 전사·동기화. 기기 내 fallback만 사용한 경우와 클라우드 경로를 모두 합산해야 함 |
| 파일 및 문서 | 예 | 아니요\* | 앱 기능 | 선택 | 아니요 | knowledge 파일, 내보내기/복원, 생성 문서. storage·embedding provider 계약 검증 |
| 기타 사용자 제작 콘텐츠 | 예 | 아니요\* | 앱 기능, 개인화, 사기 방지·보안 | 선택 | 아니요 | 전사문, 메모, 프롬프트, AI 대화, 템플릿, 회의·팀 데이터. AI 신고 시 사용자가 화면에서 확인하고 제출한 AI 응답 snapshot(최대 4,000자), 사유와 선택 comment(최대 500자)를 moderation 증거로 보관. pending/reviewing 증거는 해결 전 유지하고, dismissed/actioned 증거는 최종 처리 180일 후 purge 대상이다. AI provider 전송과 초대 팀원 공유에 예외가 성립하지 않으면 `공유: 예` |
| 앱 상호작용 | 예 | 예 | 앱 기능, 광고, 분석, 사기 방지·보안 | 필수(광고 활성 Free 흐름) | 아니요 | Google Mobile Ads SDK가 앱 실행·탭·동영상 조회 등 product interaction을 자동 수집·공유. 자체 기능 사용 기록도 존재 |
| 인앱 검색 기록 | 예 | 아니요\* | 앱 기능 | 선택 | **검증 필요** | knowledge query가 서버 검색 경로로 전송됨. 서버/provider log에 남지 않는 것이 입증돼야 `일시 처리: 예` 가능; 현재 fallback은 `아니요` |
| 기타 사용자 활동/작업 | 예 | 아니요\* | 앱 기능, 사기 방지·보안 | 선택 | 아니요 | 사용자가 실행한 action history와 처리 상태. 계정 로컬/서버 저장 범위를 재확인 |
| 웹 탐색 기록·설치된 앱 | 아니요 | 아니요 | — | — | — | 관련 permission·기능 없음 |
| 비정상 종료 로그 | 아니요(후보) | 아니요(후보) | — | — | — | 전용 crash SDK는 현재 의존성에서 확인되지 않음. release SDK graph와 Google/Firebase 설정에서 자동 수집 여부 재확인 |
| 진단 | 예 | 예 | 분석, 사기 방지·보안 | 필수(광고 활성 Free 흐름) | 아니요 | Google Mobile Ads SDK가 앱/SDK 성능 정보를 자동 수집·공유. 자체 제한 로그·오류 상태도 처리 |
| 기기 또는 기타 ID | 예 | 예 | 앱 기능, 광고, 분석, 사기 방지·보안 | 필수(광고/푸시 활성 흐름) | 아니요 | 광고 ID·app set ID는 Google Mobile Ads SDK, FCM push token과 설치 등록 ID는 알림 기능에서 사용 |
## `공유: 아니요*`를 확정하기 위한 증거
다음 조건을 하나라도 충족하지 못하면 해당 데이터 유형은 보수적으로 `공유: 예`로 제출한다.
1. Supabase, STT, LLM/embedding, 알림·메일 provider가 개발자 지시에 따라서만 데이터를 처리하는 service provider인지 최신 계약/DPA로 확인한다.
2. provider가 자기 광고, 독립 분석, 모델 학습 또는 별도 제품 개선 목적으로 데이터를 사용하지 않도록 운영 설정과 계약을 확인한다.
3. 팀원에게 콘텐츠를 보여주는 흐름이 사용자의 명시적 초대와 선택에 의한 것인지 확인한다. 공개 피드나 자동 공개가 생기면 재분류한다.
4. 운영 로그에 prompt, transcript, audio URL, 이메일 또는 purchase token 원문이 들어가지 않는지 검증한다.
5. 신고 sheet가 첨부될 AI 응답을 사용자에게 미리 보여주고, 사용자가 제출한 해당 응답 snapshot·사유·선택 comment만 전송하는지 확인한다. 음성, 전체 대화와 원래 prompt는 자동 첨부하지 않는다. 30일 generation receipt, 해결 전 보관, 해결 후 180일 evidence, 계정 삭제 시 identity unlink와 purge 기준을 privacy 문서에 반영한다.
## SDK/provider 대조표
| 구성요소 | 예상 데이터 | 현재 근거 | 제출 전 확인 |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Supabase Auth/DB/Storage/Edge | 이메일, user ID, 프로필, 사용자 콘텐츠, 파일, entitlement, 기기 등록 | `apps/mobile-rn/src/lib/supabase.ts`와 각 feature service | production project의 region, retention, logs, DPA, subprocessor, 삭제 cascade |
| Google Mobile Ads + UMP | IP 기반 대략적 위치, 앱 상호작용, 진단, 광고/계정 ID, 동의 상태 | `react-native-google-mobile-ads` 16.3.4; `mobile-ads-context.tsx` | [공식 SDK disclosure](https://developers.google.com/admob/android/privacy/play-data-disclosure), 실제 SDK 버전, UMP 메시지 게시 상태 |
| Firebase Cloud Messaging | FCM token/설치·기기 식별 정보, allowlisted 알림 event | manifest의 messaging service, `notification-service.ts` | [Firebase disclosure](https://firebase.google.com/docs/android/play-data-disclosure), production `google-services.json`, provider retention |
| Google Play Billing | 상품·구매 token, 거래·구독 상태, 계정 연결용 hash | `billing-context.tsx`, `iap-verify`, RTDN | release dependency graph, 상품/base plan, Play Developer API와 RTDN 운영 설정 |
| STT provider | 선택한 음성/동영상과 언어, 전사 결과 | `audio-transcription-service.ts`, `stt-proxy` | 실제 production provider, ZDR/학습 opt-out, 로그·보관, 실패 fallback |
| LLM/embedding provider | prompt, transcript/문서 일부, AI 결과, safety identifier | `llm-proxy`, `embed-chunks`, `search-knowledge` | production model/provider, ZDR/학습 opt-out, abuse log와 보관, 신고 반영 |
| 자체 신고 원장 | reporter ID, generation receipt/target, reason, optional comment, reporter-selected AI output snapshot, status·review audit metadata | `20260824000029_content_reporting.sql`, `content-report` Edge Function, `ContentReportSheet.tsx` | source·service-only purge RPC와 local SQL/HTTP/Deno 검증은 GREEN. production migration/function 배포, purge scheduler·운영 담당, 개인정보처리방침과 E2E가 아직 필요 |
신고 데이터 contract:
- AI 요청은 `X-D3RO-Generation-Purpose``talk_response`, `command_response`, `action_response` 중 하나를 보내고 성공 응답에서 서버 발급 `X-D3RO-Generation-Id` UUID를 받는다.
- generation receipt는 user ID, purpose, model과 생성/만료 시각만 30일 동안 유지하며 raw prompt나 전체 AI 생성 결과를 저장하지 않는다.
- 신고는 UUID `Idempotency-Key`, `ai_output`, 동일 source type/generation ID, reason enum, 사용자가 확인해 제출한 snapshot 1..4,000자와 optional comment 1..500자를 처리한다.
- pending/reviewing 증거는 해결 전 보관하고, dismissed/actioned 처리 시 180일 `evidence_expires_at`을 설정한다. 계정 삭제 시 reporter/reviewer identity는 NULL로 분리한다.
- service-only purge RPC가 만료 receipt와 해결 후 180일이 지난 report를 삭제한다. 자동 scheduler와 운영 담당은 아직 production gate다.
- Edge Function과 일반 audit log는 prompt, 전체 generation 또는 신고 snapshot 원문을 기록하지 않고 제한된 code/status metadata만 남긴다.
## 개인정보처리방침 정합성 게이트
현재 `site/public/privacy/index.html:19-35`는 계정, 사용자 콘텐츠, 기기·알림, 결제·광고 검증, 운영 로그와 AdMob을 폭넓게 고지한다. 그러나 Console 제출 전 다음 구체화가 필요하다.
- AdMob 자동 데이터인 IP 기반 대략적 위치, 앱 상호작용, 진단, 광고/기기 식별자를 명시한다.
- Google Mobile Ads가 광고·분석·사기 방지 목적으로 해당 데이터를 수집·공유할 수 있음을 명시한다.
- AI·UGC 신고 시 수집하는 reporter/target/reason/optional comment/신고자가 선택한 AI 응답 snapshot/status, 검토 목적을 명시한다. generation receipt는 30일, 해결된 evidence는 180일 후 삭제 대상이며 계정 삭제 시 reporter/reviewer identity는 NULL로 분리된다는 기준도 명시한다.
- production STT/AI/embedding provider의 실제 명칭 또는 범주, 국제 이전·보관·학습 사용 여부를 정확히 맞춘다.
- 공개 방침의 수정일을 올리고 live URL의 본문·제목·HTTPS 200을 재검증한다.
## 최종 검증 절차
1. production secrets로 `bundleRelease`를 실행해 `app-release.aab`를 만든다. debug/E2E 산출물을 사용하지 않는다.
2. release merged manifest에서 `AD_ID`, Privacy Sandbox AdServices, Billing, FCM, microphone/notification/FGS permission을 기록한다.
3. Gradle release dependency report와 Play SDK Index 경고를 보존하고 SDK 버전을 위 표와 맞춘다.
4. Play internal track에 AAB를 업로드하고 **Play가 서명·생성한 설치본**으로 아래 흐름을 각각 확인한다.
- 새 사용자/기존 사용자
- Free + 동의 허용/거부/개인정보 옵션
- Pro/Pro+ entitlement + 광고 요청 없음
- 구매/복원/환불·취소 RTDN
- 마이크 녹음, 파일/MP4 가져오기, cloud 성공·실패 후 local fallback
- AI 대화/문서 생성과 신고
- 팀 공유와 탈퇴/삭제
- 계정 삭제 후 서버·로컬 데이터 경계
5. Android Network Inspector, provider dashboard와 서버의 redacted audit log를 대조해 표에 없는 endpoint/데이터가 없는지 확인한다.
6. provider 계약·운영 설정 증거와 release SHA/version/package/certificate를 함께 보관한다.
7. `purge_expired_content_reporting_data_v1()`를 호출하는 인증된 자동 scheduler와 운영 담당을 연결하고, 만료 receipt와 해결 180일 경과 report가 실제 삭제되는 production 증거를 남긴다.
8. Console preview를 개인정보처리방침과 한 줄씩 대조하고, 불명확한 항목은 보수적으로 공개한다.
## Google 공식 기준
- [Data safety form 안내와 데이터 유형](https://support.google.com/googleplay/android-developer/answer/10787469?hl=en)
- [Google Mobile Ads SDK data disclosure](https://developers.google.com/admob/android/privacy/play-data-disclosure)
- [Firebase Android data disclosure](https://firebase.google.com/docs/android/play-data-disclosure)
- [계정 삭제 요구사항](https://support.google.com/googleplay/android-developer/answer/13327111?hl=en)
- [Advertising ID 정책](https://support.google.com/googleplay/android-developer/answer/6048248?hl=en)

View file

@ -0,0 +1,255 @@
# App content 답변 초안
## 1. 광고
| 질문 | 입력안 | 근거 |
| ----------------------- | ------ | ------------------------------------------------------------------------------------ |
| 앱에 광고가 포함되는가? | **예** | Free 사용자를 위한 AdMob adaptive banner와 사용자가 직접 실행하는 rewarded ad가 있음 |
콘솔 메모:
```text
D3RO Voice uses Google AdMob banner ads for eligible Free-plan users and offers an optional rewarded ad that grants non-transferable in-app AI usage credits only after server-side verification. Paid entitlements do not request ads.
```
코드 근거:
- 광고 eligibility는 로그인 사용자이며 최신 entitlement가 `free`이고 ad-free가 아닐 때만 성립: `apps/mobile-rn/src/lib/mobile-ads-context.tsx:65-80`
- UMP 동의 결과가 광고 요청을 허용한 뒤 SDK 초기화: `apps/mobile-rn/src/lib/mobile-ads-context.tsx:89-137`
- banner는 비개인화 요청: `apps/mobile-rn/src/components/FreeTierBanner.tsx:44-73`
- rewarded ad는 사용자가 실행하고 SSV user ID/custom data를 설정: `apps/mobile-rn/src/lib/mobile-ads-context.tsx:216-240`
## 2. Advertising ID
| 질문 | 입력안 | 상태 |
| ------------------------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| 앱 또는 포함된 SDK가 Advertising ID를 사용하는가? | **예** | **후보/사실상 필수**. Google Mobile Ads SDK가 release manifest에 `com.google.android.gms.permission.AD_ID`를 병합할 수 있음 |
| 사용 목적 | 광고 또는 마케팅, 분석, 사기 방지·보안 | Google Mobile Ads 공식 disclosure와 맞춤 |
제출 직전 release merged manifest에서 아래를 다시 캡처한다.
```text
com.google.android.gms.permission.AD_ID
android.permission.ACCESS_ADSERVICES_AD_ID
android.permission.ACCESS_ADSERVICES_ATTRIBUTION
android.permission.ACCESS_ADSERVICES_TOPICS
```
소스 manifest에 직접 보이지 않더라도 SDK library manifest에서 병합될 수 있다. E2E merged manifest에는 위 항목이 확인됐지만, Console 선언은 **최종 production AAB** 기준으로 확정한다.
## 3. 앱 액세스
### Console 선택
`앱 기능의 전체 또는 일부가 로그인, 멤버십, 위치 또는 다른 형태의 인증으로 제한됨`을 선택한다. D3RO Voice의 핵심 기능은 Supabase 계정 로그인이 필요하다.
실제 reviewer credential은 저장소나 release note에 넣지 않고 Play Console의 App access 필드에만 저장한다. 아래 placeholder를 실계정으로 교체한다.
### Credential set A — Free/ads path
```text
Name: D3RO Voice reviewer — Free plan
Username/email: <PLACEHOLDER_PLAY_REVIEWER_FREE_EMAIL>
Password: <PLACEHOLDER_PLAY_REVIEWER_FREE_PASSWORD>
Instructions (English):
1. Launch D3RO Voice. On a fresh install, complete the onboarding screens and choose the existing-account sign-in path.
2. Choose email/password sign-in. Do not use Google OAuth.
3. Enter the reusable credentials above. No OTP, 2-step verification, location restriction, or expiring password is required.
4. This account is on the Free plan with sufficient quota. It can access recording, import, transcription, Talk, meetings, templates, knowledge, teams, banner ads, rewarded ads, privacy options, data export, and account settings.
5. To review rewarded ads, open the plan/paywall screen and use the rewarded-ad control. Credits appear only after server verification.
6. To review AI reporting, open Talk, generate an AI response, and tap Report next to that assistant response. The sheet previews the selected response, lets the reviewer choose a reason and optional comment, and returns a report receipt after submission.
```
### Credential set B — Pro+/ad-free and team path
```text
Name: D3RO Voice reviewer — Pro+ and team
Username/email: <PLACEHOLDER_PLAY_REVIEWER_PRO_PLUS_EMAIL>
Password: <PLACEHOLDER_PLAY_REVIEWER_PRO_PLUS_PASSWORD>
Instructions (English):
1. Follow the same onboarding and email/password sign-in steps as credential set A.
2. This reusable account has an active reviewer-only Pro+ entitlement and belongs to a seeded private review team. It does not require the reviewer to make a purchase.
3. Open Works > Teams > <PLACEHOLDER_REVIEW_TEAM_NAME> to review invited-team collaboration, roles, meetings, memos, and shared records.
4. The Pro+ entitlement is ad-free; no ad request should be made for this account.
5. Use Settings > Account for account management and the in-app deletion path. Do not delete the shared reviewer account; the separate deletion procedure is documented below.
```
### 계정 운영 게이트
- 두 계정 모두 이메일 확인 완료, 비밀번호 만료 없음, OTP/2FA/CAPTCHA 없음, 전 세계 reviewer 네트워크에서 24시간 재사용 가능해야 한다.
- seed 데이터에는 실사용자·실회의·민감정보를 넣지 않는다.
- Free 계정에는 광고와 AI 신고를 검토할 충분한 quota를 둔다.
- Pro+ entitlement는 reviewer 계정에만 운영적으로 부여하고 만료 모니터를 둔다.
- 매 release 제출 직전 깨끗한 Play 설치본으로 두 계정을 실제 로그인한다.
[Google의 로그인 정보 요구사항](https://support.google.com/googleplay/android-developer/answer/15748846?hl=en)을 따른다.
## 4. 콘텐츠 등급(IARC)과 타깃 연령
### 타깃 연령 후보
- 선택: **18세 이상만**
- 아동 대상 여부: **아니요**
- 아동에게 의도적으로 호소하는 그래픽·문구·캐릭터: **아니요**
이 선택은 앱이 성인물이라는 뜻이 아니라, 녹음·업로드·생성형 AI·초대형 팀 협업과 광고/구독을 포함하는 업무 생산성 도구를 아동 대상으로 운영하지 않겠다는 제품 경계다. 실제 target-audience 질문과 국가별 의무를 Console에서 다시 확인한다.
### IARC 답변 지침
IARC 결과 등급을 미리 단정하지 않는다. 질문에 아래 사실을 정확히 반영하고 Console이 산출한 지역별 등급을 수용한다.
| 항목 | 사실 기반 답변안 |
| --------------------------------------------------- | --------------------------------------------------------------------------- |
| 앱 유형 | 생산성/유틸리티, 게임 아님 |
| 광고 | 예 |
| 디지털 구매 | 예, 자동 갱신 Google Play 구독 가능 |
| 생성형 AI | 예, 음성/텍스트 입력에 대한 대화 및 문서 결과 생성 |
| 사용자 제작 콘텐츠 | 예, 사용자가 녹음·입력·업로드한 전사, 메모, 문서, 팀 기록 |
| 사용자 간 콘텐츠 교환/온라인 상호작용 | 예(보수적), 초대된 비공개 팀 안에서 회의와 관련 기록 공유 |
| 공개 피드·불특정 사용자 채팅 | 아니요 |
| 사용자 차단/신고 | release에서 구현·운영 검증 후 실제 질문에 답변. 미구현 상태에서는 제출 차단 |
| 개발자가 제공하는 폭력·성적·도박·약물 콘텐츠 | 아니요 |
| 실제 현금 도박·상금·베팅 | 아니요 |
| 사용자가 입력하거나 녹음할 수 있는 민감/불쾌 콘텐츠 | 가능. 신고·moderation 정책으로 처리하며 이를 숨기지 않음 |
광고는 앱의 최종 IARC 등급보다 현저히 성숙한 콘텐츠를 보여주면 안 된다. 현재 광고 SDK request configuration은 최대 광고 등급 `T`를 사용한다(`mobile-ads-context.tsx:95-98`). 최종 IARC 결과가 더 낮으면 광고 등급을 그 이하로 내리고 재검증한다.
## 5. 금융 기능 선언
후보 선택:
```text
My app doesn't provide any financial features.
```
근거:
- Google Play 구독은 앱의 디지털 기능 이용 권한을 구매하는 일반 in-app purchase다.
- rewarded ad의 `cloud_ai_tokens`는 D3RO Voice 안에서만 소비되는 AI 사용량 quota다.
- 이 quota는 현금 가치, 인출, 송금, 교환, 사용자 간 이전, 투자, 암호자산 또는 외부 상품 교환 기능이 없다.
따라서 현재 제품은 금융 서비스나 `Rewards, points, frequent flier miles, and other incentives`형 금융 프로그램으로 운영하지 않는다는 후보 판단이다. **크레딧을 양도·판매·환전·외부 보상에 사용하게 바꾸면 즉시 이 선언을 재검토한다.** 최종 저장 전 [Google의 금융 기능 선언 분류](https://support.google.com/googleplay/android-developer/answer/13849271?hl=en)와 Console의 최신 항목을 그대로 대조한다.
## 6. 계정 삭제
| 질문 | 입력안 | 근거 |
| --------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------ |
| 앱에서 계정을 만들 수 있는가? | 예 | 이메일/OAuth 가입 |
| 앱 안에서 계정 삭제를 요청할 수 있는가? | 예 | Settings > Account > Delete account |
| 앱 밖에서 삭제를 요청할 수 있는가? | 예 | 아래 공개 URL |
| 삭제 URL | `https://d3ro.chanpaca.net/delete-account/` | 앱 내·외 절차, 삭제/제한 보관 범주 기재 |
| 일부 데이터만 삭제하는 별도 경로 | 계정 전체 삭제와 데이터 내보내기 제공. 별도 부분 삭제 질문은 실제 기능별로 확인 | 데이터 이동성 및 각 기록 삭제 UI 대조 필요 |
삭제 설명 후보:
```text
Users can delete their account in D3RO Voice from Settings > Account > Delete account. The authenticated server deletes the account and associated service data, after which the app purges account-scoped local caches, queued audio, recordings, action history, preferences, entitlement caches, and push registration. Users who cannot access the app can request deletion at https://d3ro.chanpaca.net/delete-account/. Minimum payment, dispute, security, and abuse-prevention records may be retained only where required for those purposes.
```
`20260824000029_content_reporting.sql`은 계정 삭제 시 report의 `reporter_id`와 review actor identity를 NULL로 분리하고 안전 신고 증거는 유지한다. pending/reviewing 증거는 최종 처리 전에는 time purge하지 않으며, dismissed/actioned 증거에는 처리 시점부터 180일의 `evidence_expires_at`을 설정한다. service-only purge RPC도 구현됐다. 다만 자동 scheduler·운영 담당 연결과 이 기준의 privacy/deletion 페이지 반영 전에는 위 삭제 답변을 최종 제출하지 않는다.
근거: `apps/mobile-rn/src/screens/AccountScreen.tsx:69-94,410-455`, `apps/mobile-rn/src/lib/account-local-data.ts:21-49`, `site/public/delete-account/index.html:18-39`.
## 7. AI 생성 콘텐츠와 UGC
### 현재 분류
- Talk의 text-to-text AI 응답과 전사 기반 문서 생성이 있으므로 Google Play의 AI-Generated Content 정책 적용 대상이다.
- 초대된 팀 안에서 회의·전사·메모·문서를 공유하므로 제한적 UGC/online interaction도 있다.
- 공개 피드나 익명 대화방은 없다. 그렇더라도 AI 결과 신고 요구사항은 면제되지 않는다.
### release 차단 조건
현재 source에는 Talk assistant response의 `Report` 버튼, 신고 sheet, authenticated Edge Function, generation receipt, service-only moderation 원장·purge RPC와 manager/admin review RPC가 구현돼 있다. 클라이언트는 `X-D3RO-Generation-Purpose``talk_response`, `command_response`, `action_response` 중 하나를 보내고, 성공 응답의 서버 발급 `X-D3RO-Generation-Id`를 신고 source로 사용한다. 신고 POST는 UUID `Idempotency-Key`, `ai_output`, 동일 source type/generation ID, reason, 선택 snapshot/comment를 사용한다. generation receipt는 user·purpose·model·timestamps만 저장하고 raw prompt나 전체 생성 결과를 저장하지 않으며, Edge/general audit log도 snapshot 원문을 기록하지 않는다. 근거는 `apps/mobile-rn/src/screens/TalkScreen.tsx`, `apps/mobile-rn/src/components/ContentReportSheet.tsx`, `apps/mobile-rn/src/features/reporting/content-report-service.ts`, `server/supabase/functions/_shared/generation-receipt.ts`, `server/supabase/functions/content-report/`, `server/supabase/migrations/20260824000029_content_reporting.sql`이다. **source 구현과 local GREEN은 production 배포·운영 moderation·Play 설치본 E2E 완료를 의미하지 않는다.**
아래가 모두 GREEN이 되기 전에는 AI 기능을 포함한 release를 어떤 심사 트랙에도 제출하지 않는다.
- [x] generation receipt 30일 유효, source 3종, request/response header와 idempotent 신고 contract가 source에 구현됐다.
- [x] local SQL integration, HTTP Edge 15 assertions와 shared Deno 64 tests가 GREEN이다.
- [ ] 모든 AI 생성 결과 화면에서 앱을 벗어나지 않고 offensive content를 신고할 수 있다.
- [ ] Play release 설치본에서 신고자가 인증돼 있고, 신고 sheet가 선택된 AI 응답을 미리 보여주는지 확인한다. 사용자가 제출하면 generation ID·reason·선택 comment와 해당 AI 응답 snapshot만 전송하며, 음성·전체 대화·원래 prompt는 자동 첨부하지 않는다.
- [ ] production 원장에서 RLS, 허용 target/reason, 길이 제한, rate limit, 중복/남용 방어가 실제로 적용되는지 재검증한다.
- [ ] 운영자용 검토 상태, 감사 기록, 처리 절차와 책임자가 있다.
- [ ] 이용약관/사용자 정책이 금지 콘텐츠·행동, 신고·조치 가능성을 구체적으로 설명한다.
- [ ] 개인정보처리방침이 reporter/target/reason/comment/AI 응답 snapshot, 목적, 공유, 30일 receipt, 해결 전 보관, 해결 후 180일 evidence, identity unlink와 purge를 설명한다.
- [ ] service-only purge RPC에 자동 scheduler를 연결하고 운영 담당·실패 알림·runbook을 지정했다.
- [ ] production migration/API/UI 배포 후 reviewer 계정으로 신고→운영 검토 원장 도달 E2E 증거를 남긴다.
- [ ] `<PLACEHOLDER_REPORT_TEST_EVIDENCE>`를 실제 증거 링크/경로로 교체한다.
정책 근거: [AI-Generated Content](https://support.google.com/googleplay/android-developer/answer/13985936?hl=en), [Developer Program Policy](https://support.google.com/googleplay/android-developer/answer/17190352?hl=en&rd=2).
## 8. Foreground service 선언
### 유형과 사용 사례
| 필드 | 입력안 |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| FGS type | `microphone` |
| Console use case | `Background audio access` / `Voice recording` |
| permission | `android.permission.FOREGROUND_SERVICE`, `android.permission.FOREGROUND_SERVICE_MICROPHONE`, `android.permission.RECORD_AUDIO` |
| service | `.D3RORecordingService`, exported `false` |
manifest 근거: `apps/mobile-rn/android/app/src/main/AndroidManifest.xml:4-9,49-53`.
### 기능 설명 — Console 입력안
```text
D3RO Voice starts a microphone foreground service only after the signed-in user taps Record. The service captures the user-requested recording while the user may navigate to another app screen or place the app in the background. An ongoing notification clearly states that the D3RO Voice microphone is active, shows recording progress, and provides Pause/Resume, Stop, and Discard controls. The user can terminate the task at any time, and the foreground service and notification stop when the recording is stopped or discarded.
```
### 지연·중단 영향 — Console 입력안
```text
If start is deferred, the beginning of the user-requested conversation can be missed. If the task is interrupted, the recording can contain an unrecoverable gap and the resulting transcript can be incomplete. The app journals and repairs a partial WAV when possible, but it cannot reconstruct audio that the system did not capture. Immediate, user-visible foreground execution is therefore required for the core recording feature.
```
코드 근거:
- 사용자 녹음 시작과 permission 요청: `apps/mobile-rn/src/screens/RecordScreen.tsx:320-347`
- Android foreground service 시작: `apps/mobile-rn/android/app/src/main/java/com/d3ro/voice/D3RORecordingModule.kt:98-111`
- ongoing notification과 Pause/Resume, Stop, Discard: `apps/mobile-rn/android/app/src/main/java/com/d3ro/voice/D3RORecordingService.kt:528-590`
- 최대 wake lock과 종료 처리: `D3RORecordingService.kt:449-465,588-628`
### 영상 URL
```text
<PLACEHOLDER_FGS_DEMO_URL>
```
영상은 실제 release 후보를 설치한 Android 14+ 실기기에서 다음 순서를 한 번에 보여준다.
1. 앱 정보 화면 또는 화면 오버레이로 package, versionName/versionCode와 Play 설치본임을 식별한다.
2. D3RO Voice 로그인 후 Record 화면을 연다.
3. 녹음 시작을 탭하고, 처음 실행이면 앱 내 설명 뒤 microphone/notification runtime prompt를 보여준다.
4. 녹음이 시작되자마자 ongoing notification에 microphone active와 timer가 표시되는 것을 보여준다.
5. 홈으로 나가거나 화면을 잠갔다가 notification shade를 열어 녹음이 계속되고 있음을 보여준다.
6. notification의 Pause와 Resume을 각각 사용한다.
7. Stop으로 종료하고 notification이 사라진 뒤 앱에서 녹음/전사 처리 상태를 보여준다.
8. 별도 짧은 녹음을 시작해 Discard로 즉시 종료할 수 있음을 보여준다.
영상에는 실제 대화나 개인정보를 녹음하지 않는다. 비공개/로그인 제한 없이 reviewer가 열 수 있는 unlisted YouTube 또는 공개 읽기 전용 Drive URL을 사용하고 제출 직전 링크를 익명 창에서 확인한다.
[Google FGS 선언 안내](https://support.google.com/googleplay/android-developer/answer/13392821?hl=en)를 따른다.
## 9. 기타 App content 후보
| 항목 | 입력안 |
| -------------------------------- | ----------------------------------------------------------------------------- |
| 개인정보처리방침 | `https://d3ro.chanpaca.net/privacy/` |
| 뉴스/매거진 앱 | 아니요 |
| 정부 앱 | 아니요 |
| 건강 앱/의료 기능 | 아니요 |
| COVID-19 기능 | 아니요 |
| 데이터 브로커/대출/도박/암호화폐 | 아니요 |
| Families/교사 승인 | 신청하지 않음 |
| 민감 권한 | microphone, notification, microphone FGS. 위치·연락처·통화 기록·SMS 권한 없음 |
## 최종 저장 전 확인
- [ ] 실제 Console 질문 문구와 이 문서의 후보 답을 한 항목씩 대조했다.
- [ ] release AAB가 선언한 permission/SDK와 답이 일치한다.
- [ ] Data safety, 개인정보처리방침, 광고·AI·UGC·FGS 답변 사이에 모순이 없다.
- [ ] 모든 `<PLACEHOLDER_...>`를 실제 값으로 교체했다.
- [ ] 사용자 본인이 정책·수출법·Play App Signing 약관과 각 App content 선언을 검토하고 제출을 승인했다.

View file

@ -0,0 +1,343 @@
# Google Play 출시 체크리스트
기준일: 2026-08-29. 체크 표시는 이 문서를 읽은 사람이 실제 증거를 확인한 뒤에만 바꾼다. 소스 검사, debug/E2E APK, HTTP 200 하나만으로 release GREEN을 선언하지 않는다.
### 2026-08-29 Play Console live 스냅샷
| 항목 | 확인된 현재 사실 | 판정 |
|---|---|---|
| 앱 | `D3RO Voice`, package `com.d3ro.voice`, app ID `4976102237698469110` | 생성 완료 |
| Console 상태 | `임시`; 첫 AAB 업로드 0건, 설정 0/11 | 제출 불가 |
| release identity | versionName `1.1.0`, Android versionCode `1010001` | 소스 SSOT에서 확정; AAB 증거는 아직 없음 |
| Play app-signing SHA-256 | `01:00:19:21:DB:4F:33:40:85:CE:21:E4:B8:DE:CC:BD:71:DA:87:67:C5:6E:3B:59:83:2A:A1:C8:29:EA:0D:AB` | Console 실확인 |
| upload key | 외부 PKCS12·user-only ACL·Credential Manager·private-key readback GREEN; cert SHA-256 `4F:AC:69:24:82:1C:50:DA:AB:ED:76:49:32:A5:3C:48:6F:8C:6C:5F:34:B9:F1:8D:B9:20:AA:40:99:15:2B:54` | local key 준비. Console 표시·AAB signer 대조는 첫 AAB 후 |
| release evidence | Ed25519 keypair roundtrip GREEN; public key·keyId `2797d3e63affd2348941bc2871b57e520c958f7f5da1a4bbe8aa46904a890b7f` SSOT | CI private-key secret·복구 백업 미완료 |
| Firebase | Google Console `u/0`, `u/1` 모두 D3RO project 없음 | 프로젝트·Android 앱 생성은 사용자 승인 필요 |
| closed test | 0명 / 요건 12명, 연속 14일 | 미시작 |
| production access | disabled | closed test와 access 신청·승인 전까지 출시 불가 |
외부 법적·정책 선언은 일반적인 배포 지시로 대신하지 않는다. Console의 실제 문구를 사용자가 검토하고 action-time 승인하기 전에는 저장·제출하지 않는다.
## P0 — 현재 제출 차단 요약
- [x] 사용자 본인이 Play 개발자 정책과 미국 수출법 선언을 검토·승인하고 앱 생성을 실행했다: app ID `4976102237698469110`.
- [ ] AI 생성 결과의 인앱 신고와 운영 moderation 원장이 production에서 E2E 검증됐다.
- [ ] 개인정보처리방침에 AdMob 자동 데이터와 AI/UGC 신고 데이터가 구체적으로 반영됐다.
- [x] 기본 Android Studio launcher icon을 D3RO legacy/adaptive/monochrome icon source로 교체하고 512×512 Play icon을 만들었다.
- [x] JDK 17 양 ABI debug build와 기존 데이터 보존 update install 후 Pixel Launcher emulator의 adaptive circle mask를 확인했다.
- [ ] Play-signed release 설치본과 실제 Pixel/Samsung 등 OEM mask·monochrome scale을 확인했다.
- [x] 1024×500 feature graphic 후보 4개와 SVG 원본·PNG 해시·재현 절차를 준비했다.
- [ ] 사용자 선택 및 Console preview 검증으로 feature graphic 최종본을 확정했다.
- [ ] 실제 release 앱 스크린샷을 준비했다.
- [x] Console에서 Play app-signing certificate SHA-256을 확인했다.
- [ ] 첫 AAB 업로드 후 Console이 표시하는 upload certificate가 준비한 key의 SHA-256과 일치하고 Play app-signing certificate와 다름을 증명했다.
- [ ] source `assetlinks.json` 3개는 Play app-signing certificate로 교체했지만 live는 아직 구 certificate다. 같은 소스를 배포하고 public HTTPS 응답을 재검증한다.
- [ ] Firebase production 프로젝트/Android 앱/`google-services.json`과 Play app-signing fingerprint를 연결했다.
- [ ] production secrets로 signed AAB를 만들고 package/version/config/signature/ABI/16 KB page size를 검증했다.
- [ ] Play internal track에서 **Play가 서명한 설치본**으로 광고, 결제, FCM, App Links, 녹음 FGS, AI 신고, 계정 삭제를 실기기 E2E 검증했다.
- [ ] reviewer용 Free와 Pro+ 재사용 계정, FGS 영상 URL, Data safety 최종 답을 Console에 입력했다.
- [ ] 현재 확인된 요건대로 12명을 모집해 연속 14일 closed test를 완료하고 production access를 승인받았다. 현재 0명이고 access는 disabled다.
## 1. 개발자 계정과 앱 생성
### 확인된 준비값
| 항목 | 값 | 상태 |
| -------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------- |
| Google 계정 | `yunchan8804@gmail.com` | live handoff에서 확인. 제출 직전 account switcher 재확인 |
| 개발자 | `YUN CHAN` | 개인 개발자 계정 |
| 개발자 계정 ID | `7374613349497835192` | live handoff 값. Console URL/계정 화면 재확인 |
| 앱 이름 | `D3RO Voice` | 생성 완료 |
| package | `com.d3ro.voice` | 생성 시 사용 가능 확인 후 등록 |
| 기본 언어 | 한국어 (`ko-KR`) | 생성 완료 |
| 유형 | 앱 | 생성 완료 |
| 가격 | 무료 | 생성 완료. 무료 앱은 나중에 유료 다운로드 앱으로 전환할 수 없다는 경계를 이해하고 선택 |
| 정책·수출법 checkbox | 선택 완료 | 사용자 action-time 확인 후 생성 |
| `앱 만들기` | 실행 완료 | app ID `4976102237698469110`. Chrome에서 생성 후 대시보드 확인 |
| 자동 보호 | Console 기본 활성 상태로 보였으나 결정하지 않음 | release 영향 검토 후 사용자 결정 |
### 실행 체크
- [ ] account switcher에서 이메일·개발자명·계정 ID를 화면 캡처했다.
- [ ] Policy status에 계정 issue가 없는지 재확인했다.
- [ ] Android developer verification/패키지 등록 상태를 새 앱 생성 후 재확인했다.
- [x] Dashboard에서 production access가 disabled이고 12명·14일 closed-test 요구사항이 적용됨을 확인했다.
- [ ] 새 개인 계정에 기기 확인 요구가 나타나면 Android 10+의 root되지 않은 실제 기기에서 Play Console 모바일 앱으로 완료했다.
- [x] 사용자에게 정책·수출법 선언의 정확한 문구와 현재 사실을 보여주고 action-time 승인을 받았다.
- [x] 사용자 승인 후에만 두 checkbox와 `앱 만들기`를 실행했다.
- [x] 앱 생성 직후 package/name/default language/developer를 다시 기록했다: app ID `4976102237698469110`, `D3RO Voice`, `com.d3ro.voice`, `ko-KR`, `YUN CHAN`.
- [ ] 자동 보호의 장점·디버깅/비Play 배포 영향 검토 후 release별 설정을 기록했다. 기본값을 무심코 변경하지 않았다.
공식 기준: [앱 생성](https://support.google.com/googleplay/android-developer/answer/9859152?hl=en), [새 개인 계정 기기 확인](https://support.google.com/googleplay/android-developer/answer/14316361?hl=en).
## 2. 저장소·릴리스 원본 확정
- [ ] mobile productization 전체가 권위 있는 commit에 포함됐고 `origin/main` 또는 합의한 release branch에 push됐다.
- [ ] dirty/untracked 파일을 그대로 release source로 쓰지 않았다.
- [ ] unrelated 사용자 변경을 reset/clean/덮어쓰기 하지 않았다.
- [ ] release commit SHA와 tree hash를 기록했다: `<PLACEHOLDER_RELEASE_COMMIT_SHA>`.
- [ ] CI가 실제 연결된 remote에서 해당 SHA를 checkout했음을 증명했다.
- [ ] `.github/workflows/release.yml`의 Android job은 현재 GitHub Actions용이지만 저장소 remote는 GitLab/Forgejo다. 실행 가능한 GitHub private repository/runner가 실제로 연결되기 전에는 workflow 존재만으로 CI GREEN을 선언하지 않았다.
- [ ] 대안 CI를 사용한다면 release-only secrets, 동일 검증 스크립트, 제한된 AAB handoff와 provenance를 동등하게 구현했다.
- [ ] 과거 self-hosted `com.d3rovoice` APK, debug signer, TEST-DEMO APK를 Play artifact로 재사용하지 않았다.
- [x] desktop offline license는 mobile evidence key와 분리된 새 Ed25519 keypair를 사용하고, public SSOT `apps/desktop/resources/license/production-public.pem`의 keyId `5c52b765135ee2531c681b53cd4dc0e96fff8737f496c0b04ba8334fc81a887f`를 release metadata gate가 검증한다.
- [ ] desktop offline license를 운영한다면 외부 private key를 admin `ADMIN_LICENSE_PRIVATE_KEY` secret로 주입하고 실제 발급→desktop 활성화 roundtrip을 별도 검증했다. 이 항목은 Play AAB 서명/evidence key와 절대 공유하지 않는다.
## 3. Play App Signing·Firebase·App Links
### 인증서 경계
- [x] Console에서 Play app-signing certificate를 확인했다.
- [x] upload key `C:\Users\encep\.d3ro\release\d3ro-upload-key-20260821.p12` / alias `d3ro-upload-20260821`이 저장소 밖 user-only ACL로 보관되고 Credential Manager target이 존재하며 private-key readback이 GREEN임을 확인했다.
- [x] upload key certificate가 알려진 폐기 certificate와 다름을 확인했다. 실제 AAB signer 사용 증거는 아직 없다.
- [x] local upload certificate SHA-256: `4F:AC:69:24:82:1C:50:DA:AB:ED:76:49:32:A5:3C:48:6F:8C:6C:5F:34:B9:F1:8D:B9:20:AA:40:99:15:2B:54`
- [ ] 첫 AAB 업로드 후 Console에 표시된 upload certificate가 위 local certificate와 일치한다.
- [x] Play app-signing certificate SHA-256: `01:00:19:21:DB:4F:33:40:85:CE:21:E4:B8:DE:CC:BD:71:DA:87:67:C5:6E:3B:59:83:2A:A1:C8:29:EA:0D:AB`
- [ ] Play app-signing certificate SHA-1: `<PLACEHOLDER_ANDROID_PLAY_APP_SIGNING_CERT_SHA1>`
- [x] local upload certificate와 Play app-signing certificate의 SHA-256이 다름을 확인했다. 첫 AAB 후 Console upload certificate로 재확인한다.
- [ ] CI의 AAB signer 검증에는 upload SHA-256을 사용했다.
- [ ] Firebase OAuth, Google sign-in과 HTTPS App Links에는 **Play app-signing** SHA-1/SHA-256을 등록했다.
**현재 blocker:** `site/public`, `apps/web/public`, `apps/api-server/wwwroot`의 source `assetlinks.json` 3개는 확인된 Play app-signing SHA-256으로 교체했다. 그러나 live `https://d3ro.chanpaca.net/.well-known/assetlinks.json`은 아직 폐기된 과거 certificate를 반환하므로 배포와 public 재검증이 필수다. upload certificate는 `assetlinks.json`에 넣지 않는다.
코드 경계: `.github/workflows/release.yml``ANDROID_UPLOAD_CERT_SHA256``ANDROID_PLAY_APP_SIGNING_CERT_SHA256`을 분리하고, `scripts/ci/mobile-release-evidence-lib.mjs`와 App Links verifier가 폐기 certificate를 거부해야 한다.
### Firebase
- [x] Firebase Console `u/0`, `u/1`을 재확인했고 두 계정 모두 D3RO project가 없음을 확인했다.
- [ ] 사용자 action-time 승인 후 올바른 Google 계정에서 production Firebase project를 만들거나 명시적으로 선택했다: `<PLACEHOLDER_FIREBASE_PROJECT_ID>`.
- [ ] Android 앱 `com.d3ro.voice`를 등록했다.
- [ ] Play app-signing SHA-1/SHA-256을 Firebase Android 앱/OAuth client에 등록했다.
- [ ] production `google-services.json`을 CI secret으로 저장했다. 로컬/저장소에는 남기지 않았다.
- [ ] FCM sender/project가 Supabase production push 설정과 일치한다.
- [ ] Google OAuth redirect/deep link가 canonical `d3ro-voice://auth-callback`과 실제 Supabase/Firebase 설정에 일치하며, 폐기한 `d3ro://` scheme은 export되지 않는다.
- [ ] `https://d3ro.chanpaca.net/.well-known/assetlinks.json`이 Play app-signing fingerprint를 제공한다.
- [ ] Play 설치본에서 `adb shell pm get-app-links com.d3ro.voice`와 실제 invite URL 진입이 GREEN이다.
[Play App Signing 공식 설명](https://support.google.com/googleplay/android-developer/answer/9842756?hl=en)에 따라 upload key와 app-signing key를 구분한다.
## 4. Production AAB 생성과 정적 검증
### 빌드 입력
- [x] 소스는 compile/target API 36, min API 24를 선언한다: `apps/mobile-rn/android/build.gradle:21-25`.
- [x] package는 `com.d3ro.voice`: `apps/mobile-rn/android/app/build.gradle:289-302`.
- [x] release cleartext traffic은 false이고 release ABI는 arm64-v8a: `build.gradle:372-386`.
- [x] release gate는 keystore, version, Firebase config, production AdMob app/banner/rewarded ID를 요구한다: `build.gradle:45-103,157-219`.
- [x] release identity를 `D3RO_VERSION_NAME=1.1.0`, `D3RO_VERSION_CODE=1010001`로 확정했다.
- [x] Ed25519 release-evidence keypair을 생성하고 private key를 저장소 밖 user-only ACL 경로에, public key를 `release/mobile-release-evidence-public.pem`에 보존했다. keyId `2797d3e63affd2348941bc2871b57e520c958f7f5da1a4bbe8aa46904a890b7f`, sign/verify roundtrip GREEN이다.
- [ ] upload/evidence private key·Firebase·AdMob secrets를 승인된 CI secret store에 주입하고 별도 복구 백업을 검증했다.
### 산출물·검증
- [ ] `:app:bundleRelease -PreactNativeArchitectures=arm64-v8a``app-release.aab`를 만들었다.
- [ ] AAB SHA-256: `<PLACEHOLDER_RELEASE_AAB_SHA256>`
- [ ] build source SHA: `<PLACEHOLDER_RELEASE_COMMIT_SHA>`
- [ ] package/versionName/versionCode/targetSdk/minSdk가 기대값과 일치한다.
- [ ] AAB signer가 upload certificate와 일치하고 debug/폐기 certificate가 아니다.
- [ ] embedded BuildConfig는 production Supabase/Firebase/AdMob ID이며 Google test AdMob publisher가 아니다.
- [ ] `android:debuggable=false`, `usesCleartextTraffic=false`를 확인했다.
- [ ] arm64 native library만 delivery되고 각 native dependency의 license를 보존했다.
- [ ] bundled Whisper model의 size/SHA가 gate 기대값과 일치한다.
- [ ] AAB, 검증 JSON, signed evidence, SHA256SUMS를 변경 불가능한 release record로 보관했다.
- [ ] AAB는 제한된 Play Console handoff artifact로만 제공하고 public release/site에 upload-key APK/AAB를 게시하지 않았다.
현재 작업공간의 `apps/mobile-rn/android/app/build/outputs/bundle/release/app-release.aab`는 존재하지 않는다. 기존 release APK나 E2E APK를 이 항목의 증거로 인정하지 않는다.
### 16 KB page size — 필수 blocker
D3RO Voice는 React Native/Whisper 등 native library를 포함하고 NDK r27.1을 지정하므로 source 설정만으로 호환을 가정하지 않는다.
- [ ] `bundletool dump config --bundle=<AAB>` 결과가 `PAGE_ALIGNMENT_16K`다.
- [ ] AAB에서 생성한 APK set의 모든 `.so` ELF segment alignment와 zip alignment를 공식 도구로 확인했다.
- [ ] Play pre-launch report에 16 KB page-size incompatibility가 없다.
- [ ] Android 15+ 16 KB emulator 또는 실제 16 KB 기기에서 clean install/cold start, 녹음, local Whisper transcription, Talk, 결제 SDK 초기화, 광고 SDK 초기화가 GREEN이다.
- [ ] native crash/UnsatisfiedLinkError가 없고 로그를 보존했다.
공식 기준: [16 KB page size 지원](https://developer.android.com/guide/practices/page-sizes). API 36은 2026-08-31부터 새 앱/업데이트 요구사항이며 현재 소스 설정은 이를 충족하지만 AAB 업로드 결과로 다시 확인한다: [Target API](https://support.google.com/googleplay/android-developer/answer/11926878?hl=en).
## 5. 스토어 등록정보와 자산
- [ ] [`01-store-listing.md`](01-store-listing.md)의 ko-KR/en-US 제목·설명을 실제 release 기능과 대조했다.
- [ ] category `Productivity`와 실제 제공되는 태그만 선택했다.
- [ ] 지원 이메일의 수신·응답을 확인했다.
- [ ] privacy/terms/delete URL이 익명 모바일 브라우저에서 HTTPS 200, 올바른 title/본문, no-login이다.
- [ ] root website가 유효한 지원 사이트가 아니면 website 필드를 비워 뒀다.
- [x] 기본 Android robot launcher icon을 제거했다.
- [x] adaptive icon foreground/background/monochrome와 legacy density icon을 같은 canonical D3RO artwork로 만들었다.
- [x] Pixel Launcher emulator의 adaptive circle mask를 캡처했다: `docs/v3/play/assets/proof/d3ro-icon-pixel-launcher-20260824.png`, SHA-256 `7A2DA876FBFD54089B783A7FD45C2472476ED47DCEABA08AB305B7B01A18D430`.
- [ ] Play-signed release를 실제 Pixel/Samsung 등 OEM 기기에 설치해 icon scale·safe zone·monochrome를 캡처했다.
- [x] Play icon 512×512 PNG를 생성했다: `docs/v3/play/assets/d3ro-voice-play-icon-512.png`.
- [ ] Console upload 후 alpha/모서리/안전 영역 경고가 없는지 확인했다.
- [x] feature graphic 1024×500 후보 4개와 재현·해시 기록을 준비했다: `docs/v3/play/assets/feature-graphic/`.
- [ ] 후보 중 최종본을 사용자 선택 후 Console preview에서 검증했다.
- [ ] phone screenshot 최소 2장, 권장 4장 이상을 실제 Play release 설치본에서 1080×1920급으로 캡처했다.
- [ ] 녹음, 전사 결과, Talk+신고, 회의/팀, 개인정보/삭제처럼 실제 핵심 기능을 보여준다.
- [ ] reviewer credential, 실사용자 이메일, 토큰, 실제 전사·회의 내용을 screenshot에 넣지 않았다.
- [ ] 로그인/초대 경계 E2E screenshot을 기능 대표 store asset으로 재사용하지 않았다.
## 6. 개인정보·Data safety·법적 문서
- [x] Settings에서 privacy, terms, account deletion URL을 연다: `apps/mobile-rn/src/screens/SettingsScreen.tsx:472-493`.
- [x] 가입 동의 영역에서 terms와 privacy를 각각 열 수 있다: `apps/mobile-rn/src/screens/SignUpScreen.tsx:326-369`.
- [x] 앱 내 계정 삭제 성공 후 계정 범위 로컬 데이터를 purge한다: `AccountScreen.tsx:410-455`, `account-local-data.ts:21-49`.
- [x] 공개 legal/deletion URL은 2026-08-24 mount 복구 후 외부 HTTPS 200이 확인됐다. 제출 직전 재검증한다.
- [ ] privacy 문서가 AdMob의 IP 기반 대략적 위치, 앱 상호작용, 진단, 광고/기기 ID와 광고·분석·사기 방지 목적을 명시한다.
- [ ] privacy/terms가 AI·UGC 신고 데이터, 금지 콘텐츠, 검토·조치, 보관·삭제를 반영한다.
- [ ] [`02-data-safety-draft.md`](02-data-safety-draft.md)의 모든 `검증 필요`를 release/provider 증거로 해소했다.
- [ ] Console Data safety preview와 공개 privacy 문서를 한 줄씩 대조했다.
- [ ] 계정 삭제 URL, 앱 내 경로와 제한 보관 항목을 Console에 정확히 답했다.
## 7. AI·UGC 정책 gate
- [x] `X-D3RO-Generation-Purpose`/`X-D3RO-Generation-Id`, 30일 generation receipt와 `talk_response|command_response|action_response` source contract가 구현됐다.
- [x] 신고 backend의 authenticated reporter, generation receipt/target allowlist, reason allowlist, comment/snapshot 길이 제한, rate limit, RLS, idempotency와 감사 상태가 구현됐다.
- [x] 일반 사용자의 `profiles.role` self-promotion과 `profiles.tier` self-assignment를 column ACL로 차단하고 public profile 필드 수정은 유지했다.
- [x] local SQL integration, HTTP Edge 15 assertions와 shared Deno 64 tests가 GREEN이다.
- [x] disposable local Supabase Gateway→Edge→DB Red E2E 650개가 GREEN이다: [`../e2e/RED_E2E_500_CONTENT_REPORT.md`](../e2e/RED_E2E_500_CONTENT_REPORT.md).
- [x] pending→reviewing→dismissed/actioned 흐름, 해결 후 180일 `evidence_expires_at`, account deletion identity unlink와 service-only purge RPC가 구현됐다.
- [ ] Talk와 모든 생성 문서 결과에서 인앱 신고 UI가 있다.
- [ ] AI 결과가 restricted content를 생성하지 않도록 provider safety/filter와 서버 정책을 검증했다.
- [ ] Play release 설치본에서 신고 sheet가 첨부될 AI 응답을 미리 보여주는지 확인했다. 제출 시 선택된 AI 응답 snapshot만 포함하며 음성·전체 대화·원래 prompt는 자동 복사하지 않는다.
- [ ] generation receipt에 raw prompt/전체 생성 결과가 없고 Edge/general audit log에 신고 snapshot 원문이 없는 것을 production log로 재확인했다.
- [x] moderation SLA·escalation·삭제/보관·purge·감사 runbook 초안을 만들었다: [`05-content-reporting-operations.md`](05-content-reporting-operations.md).
- [ ] 자동 purge scheduler, 실제 운영 담당과 실패 알림을 runbook에 연결했다.
- [x] 30일 receipt, 해결 전 evidence, 해결 후 180일, 계정 삭제 identity unlink와 purge를 privacy/terms 로컬 초안에 반영했다.
- [ ] privacy/terms 로컬 초안을 법률 검토 후 공개 배포하고 익명 모바일 브라우저에서 재검증했다.
- [ ] 운영 moderation 책임자·on-call을 실제 사람으로 지정했다.
- [ ] migration `20260824000029`, `content-report`/`llm-proxy` function과 모바일 신고 UI를 같은 production release에 배포했다.
- [ ] 신고 제출→관리 검토 원장 도달→상태 변경까지 production E2E가 GREEN이다.
- [ ] 초대 팀 외부로 데이터가 노출되지 않는 RLS/ACL을 검증했다.
- [ ] reviewer instructions에 신고 경로가 영어로 정확히 적혀 있다.
이 gate가 RED이면 생성형 AI 기능이 central feature인 현재 앱을 제출하지 않는다. [AI-Generated Content 정책](https://support.google.com/googleplay/android-developer/answer/13985936?hl=en).
## 8. Google Play Billing과 merchant
- [x] 코드 상품 ID는 `d3ro_voice_pro_monthly`, `d3ro_voice_pro_plus_monthly`: `apps/mobile-rn/src/lib/billing-context.tsx:32-35`.
- [x] 현재 local resolved dependency evidence는 Play Billing Library 9.1.0이다. production Gradle dependency report로 재확인한다.
- [x] PBL 9는 2028-08-31까지의 현재 신규 앱/업데이트 지원 창에 있으며, 2026-08-31에 종료되는 PBL 7 경계를 넘는다. 최종 AAB manifest의 `com.google.android.play.billingclient.version`으로 다시 증명한다: [PBL deprecation timeline](https://developer.android.com/google/play/billing/deprecation-faq).
- [x] 구매는 서버 `iap-verify` 검증과 entitlement snapshot 확인 후 성공 처리하는 경계가 있다.
- [ ] Google Play payments/merchant profile을 올바른 법적 개인·국가·세금 정보로 완료했다: `<PLACEHOLDER_MERCHANT_PROFILE_ID>`.
- [ ] 두 subscription과 base plan/offer를 Console에 정확한 ID로 만들었다.
- [ ] 가격, 결제 주기, 자동 갱신, 무료 체험 여부를 ko/en 구매 화면과 일치시켰다. site 문구만 보고 trial을 생성하지 않았다.
- [ ] Google Play Developer API service account와 최소 권한을 설정했다: `<PLACEHOLDER_PLAY_SERVICE_ACCOUNT>`.
- [ ] RTDN Pub/Sub push 인증과 production Edge endpoint를 설정했다.
- [ ] license tester 계정으로 신규 구매, pending→purchased, 취소, grace/on-hold, upgrade/downgrade, restore, refund/voided purchase를 확인했다.
- [ ] purchase token replay/cross-account/package mismatch가 거부된다.
- [ ] 계정 삭제가 Play 구독을 자동 취소하지 않음을 앱·웹에서 명확히 안내한다.
- [ ] reviewer Pro+ 계정은 실제 결제를 요구하지 않고 review 기간 내 유지된다.
## 9. AdMob·UMP·SSV
- [x] `release/android-release-identity.json`과 live Console의 AdMob identity를 대조했다: app `ca-app-pub-1039714767792854~6427959892`, banner `ca-app-pub-1039714767792854/9840591290`, rewarded `ca-app-pub-1039714767792854/2255790918`.
- [x] production SSV URL과 rewarded `50 cloud_ai_tokens` 계약을 저장했다.
- [ ] AdMob의 `검토 필요`·`광고 게재 제한` 상태를 해소했다.
- [ ] AdMob 지급 프로필을 완료했다: `<PLACEHOLDER_ADMOB_PAYMENT_PROFILE_STATUS>`.
- [ ] Play store 등록정보와 AdMob app을 연결하고 app review/approval을 완료했다.
- [ ] UMP privacy message를 대상 지역에 publish하고, consent granted/denied/not-required/privacy-options-required를 각각 확인했다.
- [ ] Play internal 설치본을 AdMob test device로 등록했다.
- [ ] 실제 Google-signed rewarded callback→durable receipt→claim→`+50 cloud_ai_tokens`를 test device에서 E2E 검증했다.
- [ ] 중복 transaction, unknown user, cap/cooldown/ineligible, account deletion unlink와 retry가 의도대로 동작한다.
- [ ] Pro/Pro+에서 SDK ad request가 발생하지 않는다.
- [ ] production 광고를 개발자/테스터가 클릭하지 않았다.
AdMob 콘솔 준비를 실제 광고 게재 승인·수입·Play release 완료로 해석하지 않는다.
## 10. App content 선언과 reviewer 자료
- [ ] Ads=`Yes`, Advertising ID=`Yes`를 final merged manifest와 맞춰 저장했다.
- [ ] App access에 Free와 Pro+ reusable 계정을 영어로 등록했다.
- [ ] 두 계정 모두 OTP/2FA/CAPTCHA/지역 제한 없이 clean install에서 로그인된다.
- [ ] Target audience는 18+ only, children appeal은 No로 제품 경계와 일치한다.
- [ ] IARC에 ads, digital purchase, generative AI, invited-team UGC/interaction을 숨김없이 답했다.
- [ ] Financial features는 내부 quota가 비양도·비환금임을 확인한 뒤 `My app doesn't provide any financial features`로 저장했다.
- [ ] account deletion 답변과 공개 URL을 저장했다.
- [ ] microphone FGS type/use case/설명/지연 영향과 `<PLACEHOLDER_FGS_DEMO_URL>`을 입력했다.
- [ ] FGS 영상 URL을 로그아웃/익명 창에서 열 수 있다.
- [ ] 모든 placeholder를 제거했다.
- [ ] [`03-app-content-draft.md`](03-app-content-draft.md)와 Console preview를 최종 대조했다.
## 11. Internal track — Play-signed 실기기 E2E
- [ ] AAB를 internal track에 upload하고 processing/error/warning을 모두 검토했다.
- [ ] App Bundle Explorer에서 delivered APK의 package/version/target/ABI/permission을 확인했다.
- [ ] Play app-signing certificate가 기대값과 일치한다.
- [ ] Play internal opt-in link로 실제 기기에 clean install했다. sideload APK로 대체하지 않았다.
- [ ] Android 10+ 일반 4 KB 기기와 Android 15+ 16 KB 환경에서 검증했다.
- [ ] fresh onboarding→email login→logout→다른 계정 login의 privacy boundary가 GREEN이다.
- [ ] email signup, 약관/privacy 링크, OAuth redirect를 검증했다.
- [ ] 마이크 허용/거부, notification 허용/거부, FGS background/lock/pause/resume/stop/discard, 통화/오디오 focus interruption을 검증했다.
- [ ] cloud STT 정상/timeout/provider 장애→local fallback, offline queue/resume/cancel, low storage/process kill을 검증했다.
- [ ] MP3/M4A/AAC/OGG/FLAC/WAV/WebM/MP4 유효 파일과 invalid/oversize/mime mismatch를 검증했다.
- [ ] Talk streaming, TTS, AI 신고와 restricted prompt 처리 흐름을 검증했다.
- [ ] team invite warm/cold HTTPS App Link, 역할, 제거/탈퇴, RLS를 검증했다.
- [ ] FCM 등록·수신·logout detach·계정 삭제 detach를 검증했다.
- [ ] Free ads/UMP/rewarded SSV와 Pro+ no-ad를 검증했다.
- [ ] Play subscription purchase/restore/cancel 상태를 검증했다.
- [ ] export/restore와 계정 삭제 후 서버·로컬 데이터 제거를 검증했다.
- [ ] 앱 업데이트에서 데이터/세션/구독을 보존하고 signer lineage가 일치한다.
- [ ] Play pre-launch report의 crash, ANR, accessibility, security, permission, 16 KB 문제를 처리했다.
## 12. Closed test와 production access
개인 개발자 계정이 2023-11-13 이후 생성됐다면 production 전에 최소 12명이 closed test에 **연속 14일** opt-in 상태여야 한다. tester가 중간에 opt-out하면 연속 기간이 다시 계산된다.
- [x] Console Dashboard에서 12명·연속 14일 요구사항과 production access disabled 상태를 확인했다.
- [ ] 적용 시 최소 12명의 실제 tester를 모집하고 개인정보·NDA·피드백 경로를 안내했다.
- [ ] tester 목록과 opt-in 시각을 보존했다.
- [ ] 14일 동안 crash-free install만 세지 않고 녹음, 전사, AI, 팀, 광고/결제, 삭제 기능을 실제 사용하게 했다.
- [ ] 버그·사용성·배터리·권한·광고·AI 안전 피드백과 수정 내역을 보존했다.
- [ ] production access 질문에 실제 tester 참여/피드백/수정 내용만 답했다.
- [ ] 최소 12명이 신청 순간까지 연속 opt-in 상태임을 Console에서 확인했다.
- [ ] production access 승인 전 open testing/production을 사용할 수 있다고 가정하지 않았다.
공식 기준: [새 개인 계정 테스트 요구사항](https://support.google.com/googleplay/android-developer/answer/14151465?hl=en-GB).
## 13. Production 제출·점진 배포
- [ ] internal/closed에서 검증한 **동일 AAB SHA** 또는 정당한 수정 후 새로 전부 검증한 AAB를 사용했다.
- [ ] release notes ko/en을 작성했다.
- [ ] country/region과 가격·세금·정책 적용 범위를 검토했다.
- [ ] device catalog 제외가 기능/SDK 사실에 근거하며 불필요하게 넓지 않다.
- [ ] 모든 Console warning과 policy declaration을 검토했다.
- [ ] 사용자 본인이 production 제출 버튼과 법적 선언을 action-time 확인했다.
- [ ] 첫 production은 staged rollout으로 시작하고 비율·중단 기준을 기록했다.
- [ ] Android vitals crash/ANR, FGS/배터리, 결제 RTDN, SSV, 신고 queue, auth/STT/AI provider error를 모니터링한다.
- [ ] rollback은 versionCode를 되돌리는 것이 아니라 수정된 더 높은 versionCode로 배포한다.
- [ ] 스토어 페이지·설치·로그인·핵심 흐름을 외부 사용자 관점에서 재검증했다.
## 14. 최종 evidence pack
다음 자료가 하나의 release record에서 같은 version/SHA를 가리켜야 한다.
- [ ] release commit SHA/tree, CI run URL과 runner identity
- [ ] AAB SHA-256, versionName/versionCode, package, target/min API
- [ ] upload certificate, Play app-signing certificate SHA-1/SHA-256
- [ ] signed build evidence, dependency report, merged manifest, bundletool/16 KB 결과
- [ ] Play Console app ID, App Signing, App Bundle Explorer, track/review 상태 캡처
- [ ] Firebase app/fingerprint/FCM/App Links 증거
- [ ] Data safety/App content/IARC/FGS preview와 공개 legal page 캡처
- [ ] reviewer 계정 로그인 결과(비밀번호 제외)
- [ ] 실제 Play 설치본의 device/OS/build 정보와 기능 E2E 로그·화면
- [ ] Billing/RTDN, AdMob UMP/SSV, AI report moderation의 redacted production 증거
- [ ] closed-test tester 수·연속 기간·피드백과 production access 결과
- [ ] staged rollout과 Android vitals 모니터링 기록
## 외부 placeholder 레지스트리
| Placeholder | 실제 값 소유 위치 | 완료 조건 |
| ----------------------------------------------------------- | ---------------------------- | ---------------------------------------- |
| `<PLACEHOLDER_RELEASE_COMMIT_SHA>` | Git remote/CI | 권위 있는 release source SHA |
| versionName `1.1.0` / versionCode `1010001` | release SSOT | 확정. 실제 AAB·Console metadata 대조는 대기 |
| `<PLACEHOLDER_RELEASE_AAB_SHA256>` | restricted artifact/evidence | 실제 production AAB hash |
| upload SHA-256 `4F:AC:69:24:...:15:2B:54` | external key/Play Console | local 값 확정. AAB signer·Console 표시와 일치 대기 |
| evidence keyId `2797d3e6...4a890b7f` | release identity/CI secret | public·roundtrip 확인. private CI secret·복구 백업 대기 |
| Play app-signing SHA-1 | Play Console App integrity | SHA-256은 확인. SHA-1·Firebase 대조는 대기 |
| `<PLACEHOLDER_FIREBASE_PROJECT_ID>` | Firebase Console | 올바른 계정·package 등록 |
| `<PLACEHOLDER_PLAY_SERVICE_ACCOUNT>` | Google Cloud/Play Console | 최소 권한, RTDN/API GREEN |
| `<PLACEHOLDER_MERCHANT_PROFILE_ID>` | Google payments/Play Console | 상품 판매 가능, 법적·세금 정보 완료 |
| `<PLACEHOLDER_ADMOB_PAYMENT_PROFILE_STATUS>` | AdMob/Google payments | 지급 profile와 검토 완료 |
| reviewer credentials | Play Console App access만 | 24시간 재사용 가능, no OTP |
| `<PLACEHOLDER_FGS_DEMO_URL>` | unlisted video storage | 익명 접근 가능, 실제 release 실기기 영상 |
| `<PLACEHOLDER_REPORT_TEST_EVIDENCE>` | redacted production evidence | 신고→운영 원장 E2E GREEN |

View file

@ -0,0 +1,184 @@
# AI 콘텐츠 신고 운영 runbook
기준일: 2026-08-24
이 문서는 D3RO Voice의 AI 결과 신고를 production에서 접수·검토·종결·정리하는
절차다. 현재는 구현과 local 검증만 완료됐으며, production 배포·운영 담당 지정·자동
purge·실제 설치본 E2E는 완료되지 않았다.
## 1. 책임과 접근 경계
출시 전에 다음 값을 실제 담당자로 교체한다.
| 역할 | 책임 | 현재 상태 |
| ----------------- | -------------------------------------------------- | -------------------------------- |
| moderation owner | 매 영업일 queue 확인, SLA와 종결 품질 관리 | `<PLACEHOLDER_MODERATION_OWNER>` |
| safety escalation | 아동 안전, 자해·폭력, 명백한 불법 콘텐츠 즉시 판단 | `<PLACEHOLDER_SAFETY_ON_CALL>` |
| privacy owner | 개인정보·계정 삭제·보관 예외 검토 | `<PLACEHOLDER_PRIVACY_OWNER>` |
| purge owner | 매일 purge 실행·실패 알림·월간 보관 감사 | `<PLACEHOLDER_PURGE_OWNER>` |
- 일반 사용자는 authenticated `content-report` Edge Function으로만 신고한다.
- `content_generation_receipts`, `content_reports`,
`content_report_review_actions``anon``authenticated`의 직접 접근을 모두
거부한다.
- queue 조회·상태 변경 RPC는 `profiles.role``manager`, `admin`,
`super_admin`인 인증 사용자만 호출할 수 있다.
- `profiles.role``profiles.tier`는 일반 사용자의 column UPDATE 권한에서 제외한다.
표시 이름, 아바타, locale만 self-service 수정이 가능하다.
- service-role key는 모바일 앱, 브라우저, 로그, 티켓, Play reviewer 안내에 넣지 않는다.
근거 마이그레이션:
- `server/supabase/migrations/20260824000029_content_reporting.sql`
- `server/supabase/migrations/20260824000030_profile_managed_field_acl.sql`
## 2. Production 배포 순서
다음 순서를 한 release window 안에서 수행한다.
1. 권위 있는 release commit과 production Supabase project ref를 기록한다.
2. migration `20260824000029`, `20260824000030`의 dry-run diff를 검토한 뒤 적용한다.
3. `llm-proxy``content-report`를 같은 source SHA에서 배포한다.
4. Talk, Commands, Actions 클라이언트를 포함한 AAB를 배포한다.
5. purge job과 실패 알림을 활성화한다.
6. Free/Pro+ 테스트 계정으로 제출→queue→검토→종결→감사 원장 E2E를 수행한다.
예시 명령의 placeholder는 실행 전에 secret manager와 release record의 실제 값으로
대체한다. 이 문서의 명령 실행 자체가 production 배포 승인인 것은 아니다.
```powershell
supabase db push --linked --dry-run
supabase db push --linked
supabase functions deploy llm-proxy --project-ref '<PROJECT_REF>'
supabase functions deploy content-report --project-ref '<PROJECT_REF>'
```
배포 직후 다음을 확인한다.
- `X-D3RO-Generation-Purpose`
`talk_response|command_response|action_response`만 허용된다.
- 성공한 provider 응답에는 UUID `X-D3RO-Generation-Id`가 있다.
- receipt에는 user/model/purpose/expiry만 있고 prompt·AI 출력은 없다.
- 신고에는 사용자가 미리 확인한 snapshot, reason, 선택 comment만 있다.
- 음성, 전체 대화, 원래 prompt는 자동 첨부되지 않는다.
- 일반 사용자 role/tier 직접 UPDATE와 ledger 직접 SELECT/INSERT가 거부된다.
## 3. Queue와 처리 SLA
운영 도구는 로그인한 manager 이상 사용자의 access token으로 다음 RPC를 호출한다.
브라우저 콘솔이나 공유 스크립트에 token을 붙여 넣지 않는다.
```ts
const { data, error } = await supabase.rpc('admin_list_content_reports_v1', {
p_status: 'pending',
p_limit: 100,
p_before: null
})
```
권장 SLA:
- 아동 성적 학대·착취 또는 즉각적인 생명 위험: 발견 즉시 escalation
- 개인정보 노출, 구체적 폭력 위협, 자해 조장: 4시간 이내 1차 검토
- 혐오·괴롭힘·성적·사기·스팸·오정보·기타: 1영업일 이내 1차 검토
- 미처리 `pending`의 최장 age가 24시간을 넘으면 moderation owner와 on-call에 알림
검토자는 snapshot과 사용자가 선택한 사유·comment만 필요한 범위에서 본다. 다른
대화, 녹음, 계정 데이터는 자동으로 확장 조회하지 않는다.
## 4. 검토와 종결
각 조치는 새 UUID idempotency key와 3~1000자의 사실 기반 note가 필요하다.
```ts
await supabase.rpc('admin_act_on_content_report_v1', {
p_report_id: reportId,
p_idempotency_key: crypto.randomUUID(),
p_action: 'begin_review',
p_note: 'Initial policy review started'
})
```
허용 전이는 다음뿐이다.
| action | 결과 상태 | 사용 조건 |
| ------------------- | ----------- | -------------------------------------------- |
| `begin_review` | `reviewing` | 담당자가 증거를 확인하기 시작함 |
| `dismiss` | `dismissed` | 정책 위반이 아니거나 증거가 불충분함 |
| `confirm_violation` | `actioned` | 정책 위반을 확인하고 별도 완화 조치를 기록함 |
`confirm_violation`은 원장 상태를 확정하지만 계정 정지나 provider 차단을 자동 실행하지
않는다. 필요한 계정·모델·prompt 정책 조치는 기존 승인된 관리자 절차에서 별도로
수행하고, 신고 note에는 비밀이나 snapshot 원문을 복제하지 말고 조치 식별자만 남긴다.
의도적인 허위·보복·반복 남용 신고는 신고 자체를 삭제해 숨기지 않는다. `dismiss`
종결하고 반복 패턴은 별도 abuse case로 escalation한다. 선의의 신고자에게 불이익을
주지 않는다.
## 5. 보관과 purge
- generation receipt: 생성 후 30일
- `pending`, `reviewing` 신고 증거: 해결할 때까지
- `dismissed`, `actioned` 신고 증거: 해결 시점부터 180일
- 계정 삭제: reporter/reviewer identity와 moderation audit actor는 `NULL`로 분리하지만,
미해결 또는 180일 보관 창의 선택 증거는 moderation·분쟁 대응을 위해 남을 수 있다.
- general audit log에는 snapshot이나 전체 생성 결과를 복제하지 않는다.
정리는 `purge_expired_content_reporting_data_v1()`만 사용한다. 이 RPC는
service-role만 실행할 수 있다. scheduler는 secret manager에서 service credential을
주입하는 서버 측 job이어야 하며, 모바일·브라우저 job은 금지한다.
성공 응답 예시는 다음과 같다.
```json
{ "purgedReports": 3, "purgedReceipts": 121 }
```
매일 실행 결과에 실행 시각, release SHA, 숫자만 기록한다. report ID, snapshot,
comment, service credential은 scheduler log에 남기지 않는다. 24시간 동안 성공 실행이
없거나 RPC가 실패하면 purge owner에게 알리고, 72시간 이내 복구되지 않으면 privacy
owner에게 escalation한다.
## 6. 모니터링과 월간 감사
운영 모니터는 원문 없이 다음 집계만 보관한다.
- status별 수와 최장 pending age
- 시간·일 rate-limit 발생 수
- 제출 4xx/5xx와 idempotency conflict 수
- review action 성공·실패 수
- 마지막 purge 성공 시각과 삭제 건수
매월 다음을 표본 확인한다.
- 일반 사용자 role/tier self-update가 계속 거부되는가
- expired receipt가 남아 있지 않은가
- resolved report가 180일 이후 purge되는가
- pending/reviewing 증거가 time purge되지 않는가
- audit log에 snapshot·comment·prompt가 복제되지 않았는가
- manager/admin/super_admin 이외 계정이 moderation RPC를 호출하지 못하는가
## 7. Release 증거와 rollback
release evidence에는 비밀이나 신고 원문 없이 다음만 보관한다.
- source commit SHA와 migration/function deployment ID
- local SQL, Edge HTTP, Deno test 결과
- Play 설치본의 Talk/Commands/Actions 신고 왕복 캡처
- queue 도달, `begin_review`, terminal action의 report ID 해시와 상태 시각
- purge job 설정, 마지막 성공 시각, alert test
- privacy/terms 공개 URL과 해당 버전의 hash
배포 후 신고 제출이나 generation receipt가 실패하면 AI 생성 경로를 정상처럼 계속
노출하지 않는다. 영향 surface를 비활성화하거나 이전 검증 release로 rollback하고,
receipt와 report schema를 임의로 drop하거나 moderation evidence를 수동 삭제하지 않는다.
## 현재 미해결 gate
- production migration/Edge/mobile 동시 배포
- manager용 moderation UI 또는 승인된 내부 operator client
- 실제 운영 담당자와 on-call 지정
- 매일 purge scheduler와 실패 알림
- provider safety/filter 검증
- Play가 서명한 실기기 신고 E2E
- Talk/Commands/Actions 밖의 AI 생성 문서 신고 범위

62
docs/v3/play/README.md Normal file
View file

@ -0,0 +1,62 @@
# D3RO Voice Google Play 제출 패키지
- 기준일: 2026-08-25
- 대상 패키지: `com.d3ro.voice`
- 기본 언어: `ko-KR`
- 보조 언어: `en-US`
이 디렉터리는 Google Play Console에 입력할 후보 문구와 제출 전 검증 게이트를 모은다. **초안은 콘솔에서 자동으로 제출해도 된다는 승인이 아니다.** 실제 release AAB, Play가 서명한 설치본, 운영 provider 설정과 콘솔 질문을 다시 대조한 뒤 사람이 최종 선언해야 한다.
## 문서
1. [`01-store-listing.md`](01-store-listing.md): 한국어·영어 스토어 등록정보와 지원 정보
2. [`02-data-safety-draft.md`](02-data-safety-draft.md): Data safety 데이터 유형별 초안과 검증 항목
3. [`03-app-content-draft.md`](03-app-content-draft.md): 광고, 앱 액세스, 등급, 금융 기능, 계정 삭제, AI·UGC, FGS 답변
4. [`04-release-checklist.md`](04-release-checklist.md): 앱 생성부터 internal/closed/production까지의 출시 게이트
5. [`05-content-reporting-operations.md`](05-content-reporting-operations.md): AI 신고 검토·종결·보관·purge 운영 절차
6. [`assets/feature-graphic/README.md`](assets/feature-graphic/README.md): 1024×500 후보, 원본, 재현 절차와 해시
7. [`../e2e/RED_E2E_500_CONTENT_REPORT.md`](../e2e/RED_E2E_500_CONTENT_REPORT.md): local Gateway→Edge→DB 650개 AI 신고 Red-path E2E
## 상태 표기
- **확정**: 현재 코드·정적 설정 또는 공개 운영 URL로 근거가 확인됐다.
- **후보**: 현재 구현과 맞는 콘솔 입력안이지만 실제 질문 문구와 다시 대조해야 한다.
- **검증 필요**: release AAB, provider 계약·설정, 실제 계정 또는 실기기 증거가 필요하다.
- **차단**: 해결 전에는 해당 선언이나 트랙 제출을 진행하면 안 된다.
- `<PLACEHOLDER_...>`: 실제 계정·인증서·URL·영상 값으로 교체해야 한다. 그대로 제출하면 안 된다.
## 현재 사실 경계
- Play Console 앱은 `YUN CHAN` 개인 개발자 계정에서 생성됐다: app ID `4976102237698469110`, `D3RO Voice`, `com.d3ro.voice`, `ko-KR`, 앱·무료. 정책·수출법 선언은 사용자 action-time 확인 후 처리됐다. 앱 생성은 제출·internal track·production 승인과 다르다.
- Android 소스는 API 36을 target/compile하고 microphone foreground service, Google Mobile Ads, Firebase Cloud Messaging, Google Play Billing을 포함한다.
- 공개 개인정보처리방침, 이용약관, 계정 삭제 페이지는 아래 canonical URL을 사용한다. 제출 직전 익명 브라우저에서 다시 확인한다.
- `https://d3ro.chanpaca.net/privacy/`
- `https://d3ro.chanpaca.net/terms/`
- `https://d3ro.chanpaca.net/delete-account/`
- 현재 작업공간에는 **Play 제출용 production AAB가 없다**. debug/E2E APK 또는 과거 self-hosted APK는 Play release 증거가 아니다.
- D3RO legacy/adaptive/monochrome launcher resource와 512×512 Play icon source는 반영됐다. JDK 17 양 ABI debug build, 기존 데이터 보존 update install과 Pixel Launcher emulator의 adaptive circle mask는 GREEN이다. proof는 `docs/v3/play/assets/proof/d3ro-icon-pixel-launcher-20260824.png`(SHA-256 `7A2DA876FBFD54089B783A7FD45C2472476ED47DCEABA08AB305B7B01A18D430`)다. Play-signed release와 실제 Pixel/Samsung/OEM mask·scale 증거는 아직 남았다.
- Play App Signing의 app-signing certificate와 개발자가 보관하는 upload certificate는 서로 다른 신뢰 경계다. Firebase OAuth/App Links에는 Play app-signing fingerprint를 등록하고, AAB 업로드 검증에는 upload fingerprint를 사용한다.
- AI 신고 source는 generation receipt, authenticated `content-report` Edge Function, service-only moderation 원장과 purge RPC까지 구현됐고 local SQL/HTTP/Deno 검증이 GREEN이다. receipt는 30일 유효하며 해결된 신고 증거는 180일 후 purge 대상이다. 운영 runbook 초안도 준비됐지만 production 배포, 자동 purge scheduler, 실제 담당 지정과 Play 설치본 E2E는 아직 남았다.
- local disposable Supabase에서 AI 신고 Red-path E2E 650개가 GREEN이다. malformed body·권한 우회·source ownership·idempotency·rate limit·manager moderation 전이를 실제 Gateway→Edge→DB로 검증하며 production endpoint는 실행기에서 fail-closed한다.
## 주요 코드 근거
- 패키지·release 설정: `apps/mobile-rn/android/app/build.gradle`
- target API: `apps/mobile-rn/android/build.gradle`
- 권한·FGS·AdMob·App Links: `apps/mobile-rn/android/app/src/main/AndroidManifest.xml`
- 광고와 UMP: `apps/mobile-rn/src/lib/mobile-ads-context.tsx`, `apps/mobile-rn/src/components/FreeTierBanner.tsx`
- Play Billing: `apps/mobile-rn/src/lib/billing-context.tsx`, `apps/mobile-rn/src/screens/ProPaywallScreen.tsx`
- 계정 삭제: `apps/mobile-rn/src/screens/AccountScreen.tsx`, `apps/mobile-rn/src/lib/account-local-data.ts`
- AI 신고: `apps/mobile-rn/src/components/ContentReportSheet.tsx`, `apps/mobile-rn/src/features/reporting/content-report-service.ts`, `server/supabase/functions/content-report/`, `server/supabase/migrations/20260824000029_content_reporting.sql`, `server/supabase/migrations/20260824000030_profile_managed_field_acl.sql`, `server/supabase/migrations/20260825000031_audit_log_actor_unlink.sql`
- 법적 링크: `apps/mobile-rn/src/screens/SignUpScreen.tsx`, `apps/mobile-rn/src/screens/SettingsScreen.tsx`
- 공개 법적 문서: `site/public/privacy/index.html`, `site/public/terms/index.html`, `site/public/delete-account/index.html`
## Google 공식 기준
- [Create and set up your app](https://support.google.com/googleplay/android-developer/answer/9859152?hl=en)
- [Provide information for the Data safety section](https://support.google.com/googleplay/android-developer/answer/10787469?hl=en)
- [Prepare your app for review](https://support.google.com/googleplay/android-developer/answer/9859455?hl=en)
- [Use Play App Signing](https://support.google.com/googleplay/android-developer/answer/9842756?hl=en)
- [Target API level requirements](https://support.google.com/googleplay/android-developer/answer/11926878?hl=en)
- [App testing requirements for new personal developer accounts](https://support.google.com/googleplay/android-developer/answer/14151465?hl=en)
- [Support 16 KB page sizes](https://developer.android.com/guide/practices/page-sizes)

View file

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<rect width="512" height="512" rx="112" fill="#19191b"/>
<circle cx="256" cy="224" r="64" fill="none" stroke="#3b82f6" stroke-width="24"/>
<rect x="232" y="288" width="48" height="80" rx="24" fill="#3b82f6"/>
<path d="M160 256 Q160 352 256 352 Q352 352 352 256" fill="none" stroke="#3b82f6" stroke-width="20" stroke-linecap="round"/>
<line x1="256" y1="352" x2="256" y2="416" stroke="#3b82f6" stroke-width="20" stroke-linecap="round"/>
<line x1="208" y1="416" x2="304" y2="416" stroke="#3b82f6" stroke-width="20" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 620 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<rect width="512" height="512" fill="#19191b"/>
<circle cx="256" cy="224" r="64" fill="none" stroke="#3b82f6" stroke-width="24"/>
<rect x="232" y="288" width="48" height="80" rx="24" fill="#3b82f6"/>
<path d="M160 256 Q160 352 256 352 Q352 352 352 256" fill="none" stroke="#3b82f6" stroke-width="20" stroke-linecap="round"/>
<line x1="256" y1="352" x2="256" y2="416" stroke="#3b82f6" stroke-width="20" stroke-linecap="round"/>
<line x1="208" y1="416" x2="304" y2="416" stroke="#3b82f6" stroke-width="20" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 611 B

View file

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<circle cx="256" cy="256" r="256" fill="#19191b"/>
<circle cx="256" cy="224" r="64" fill="none" stroke="#3b82f6" stroke-width="24"/>
<rect x="232" y="288" width="48" height="80" rx="24" fill="#3b82f6"/>
<path d="M160 256 Q160 352 256 352 Q352 352 352 256" fill="none" stroke="#3b82f6" stroke-width="20" stroke-linecap="round"/>
<line x1="256" y1="352" x2="256" y2="416" stroke="#3b82f6" stroke-width="20" stroke-linecap="round"/>
<line x1="208" y1="416" x2="304" y2="416" stroke="#3b82f6" stroke-width="20" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 614 B

View file

@ -0,0 +1,57 @@
# Google Play feature graphic 후보
기준일: 2026-08-24
Google Play의 1024×500 feature graphic 규격에 맞춘 선택 후보다. 네 후보 모두
저장소의 canonical D3RO microphone motif와 `D3RO VOICE` 표기만 사용하며, 기능이나
성과를 주장하는 문구와 합성 앱 화면은 포함하지 않는다.
## 후보와 권장안
| 후보 | 방향 | PNG SHA-256 |
| ------------------------------ | --------------------------------------- | ------------------------------------------------------------------ |
| A `candidate-a-signal-grid` | dark signal grid, microphone + waveform | `3CFA86A4843339FD9C15781598E301ECE29C2B1522828F7AD1B69D8497775103` |
| B `candidate-b-voice-orbit` | centered voice orbit | `F7FCB26B063762D08AC1211A85CFDDD3C99C14936CD6DE753F5E9F88BC0B0D14` |
| C `candidate-c-blueprint-band` | light/dark blueprint split | `C484AC514966347BF4E7AA526B040B88DDA89F9150AC3677B8C74297A1373C1E` |
| D `candidate-d-monolith` | bold dark monolith | `28FC7B608454E073A29AFE2149414F4186629AE8889B3FE2FA683D786F8C3EA9` |
현재 권장안은 **A**다. 작은 스토어 카드에서도 microphone, 제품명, 파형이 가장
명확하게 분리된다. 다만 최종 자산 확정과 Play Console 업로드는 사용자 선택 및
Console preview 검증 뒤에만 진행한다.
## 원본과 재현
- brand source: `site/public/favicon.svg`
- Play icon source: `docs/v3/play/assets/d3ro-voice-play-icon-source.svg`
- 편집 원본: 각 후보의 `.svg`
- 제출 후보: 같은 basename의 `.png`
- export runtime: ImageMagick `7.1.2-25 Q16-HDRI`
- 출력 조건: 1024×500, PNG, sRGB, 8-bit, TrueColor, 완전 불투명
- machine-readable manifest: `manifest.json`
- verifier: `scripts/ci/verify-play-store-assets.mjs`
저장소 루트의 PowerShell에서 다음 명령으로 다시 내보낸다.
```powershell
Get-ChildItem docs/v3/play/assets/feature-graphic/candidate-*.svg |
ForEach-Object {
$output = [System.IO.Path]::ChangeExtension($_.FullName, '.png')
magick $_.FullName -background none -resize '1024x500!' -alpha off -colorspace sRGB "PNG24:$output"
}
```
환경이나 SVG renderer 버전이 바뀌면 raster hash가 달라질 수 있다. 제출할 PNG는
위 해시, 크기, 불투명 여부를 다시 확인하고 그 파일을 release evidence에 기록한다.
```powershell
npm run release:play:assets
```
## SVG 원본 해시
| 파일 | SHA-256 |
| -------------------------------- | ------------------------------------------------------------------ |
| `candidate-a-signal-grid.svg` | `3A39D0D61F5AE7E182149BD11C9D7F491FBEC0A542999B0516C869AE82E24B1B` |
| `candidate-b-voice-orbit.svg` | `DB7D778394419C6AF8FF43C7490B43C0CD65C61CE012A358FB37E603431082F4` |
| `candidate-c-blueprint-band.svg` | `176BEB99EA418F78BF5F9DC4E4D6A3A3700F5CEADA28697F3750B0C8883E4546` |
| `candidate-d-monolith.svg` | `6560C334C061470959D9BA120E58CCA264B5A1ED41FBC3045F804049117640FF` |

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 500">
<rect width="1024" height="500" fill="#19191b"/>
<g stroke="#2b2b31" stroke-width="1">
<path d="M0 50H1024M0 100H1024M0 150H1024M0 200H1024M0 250H1024M0 300H1024M0 350H1024M0 400H1024M0 450H1024"/>
<path d="M64 0V500M128 0V500M192 0V500M256 0V500M320 0V500M384 0V500M448 0V500M512 0V500M576 0V500M640 0V500M704 0V500M768 0V500M832 0V500M896 0V500M960 0V500"/>
</g>
<rect x="64" y="54" width="290" height="392" rx="42" fill="#111114" stroke="#3b82f6" stroke-width="2"/>
<g transform="translate(98 62) scale(4.35)" fill="none" stroke="#3b82f6" stroke-linecap="round">
<circle cx="32" cy="28" r="8" stroke-width="3"/>
<rect x="29" y="36" width="6" height="10" rx="3" fill="#3b82f6" stroke="none"/>
<path d="M20 32Q20 44 32 44Q44 44 44 32" stroke-width="2.5"/>
<path d="M32 44V52M26 52H38" stroke-width="2.5"/>
</g>
<text x="430" y="227" fill="#f5f7fb" font-family="Arial, sans-serif" font-size="78" font-weight="700" letter-spacing="7">D3RO</text>
<text x="434" y="288" fill="#3b82f6" font-family="Arial, sans-serif" font-size="40" font-weight="700" letter-spacing="15">VOICE</text>
<path d="M432 352H484L500 326L526 390L548 340L568 362H620L640 312L664 396L690 346L710 366H774L792 333L812 378L834 352H944" fill="none" stroke="#3b82f6" stroke-width="5" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View file

@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 500">
<rect width="1024" height="500" fill="#0f1014"/>
<circle cx="512" cy="250" r="205" fill="#19191b" stroke="#292c35" stroke-width="2"/>
<circle cx="512" cy="250" r="166" fill="none" stroke="#3b82f6" stroke-opacity="0.22" stroke-width="28"/>
<circle cx="512" cy="250" r="126" fill="none" stroke="#3b82f6" stroke-opacity="0.45" stroke-width="3" stroke-dasharray="5 17"/>
<g transform="translate(416 106) scale(3)" fill="none" stroke="#3b82f6" stroke-linecap="round">
<circle cx="32" cy="28" r="8" stroke-width="3"/>
<rect x="29" y="36" width="6" height="10" rx="3" fill="#3b82f6" stroke="none"/>
<path d="M20 32Q20 44 32 44Q44 44 44 32" stroke-width="2.5"/>
<path d="M32 44V52M26 52H38" stroke-width="2.5"/>
</g>
<path d="M40 250H250M774 250H984" stroke="#3b82f6" stroke-width="2"/>
<path d="M62 250L92 250L110 220L134 286L158 238L180 260L205 210L228 276L250 250M774 250L802 250L826 208L848 290L872 235L896 270L920 224L944 278L962 250" fill="none" stroke="#3b82f6" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="401" y="386" width="222" height="46" rx="23" fill="#3b82f6"/>
<text x="512" y="417" text-anchor="middle" fill="#0f1014" font-family="Arial, sans-serif" font-size="22" font-weight="700" letter-spacing="4">D3RO VOICE</text>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,27 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 500">
<rect width="1024" height="500" fill="#f4f6fa"/>
<rect x="0" y="0" width="356" height="500" fill="#19191b"/>
<rect x="356" y="0" width="28" height="500" fill="#3b82f6"/>
<g transform="translate(42 98) scale(4.25)" fill="none" stroke="#3b82f6" stroke-linecap="round">
<circle cx="32" cy="28" r="8" stroke-width="3"/>
<rect x="29" y="36" width="6" height="10" rx="3" fill="#3b82f6" stroke="none"/>
<path d="M20 32Q20 44 32 44Q44 44 44 32" stroke-width="2.5"/>
<path d="M32 44V52M26 52H38" stroke-width="2.5"/>
</g>
<text x="438" y="224" fill="#19191b" font-family="Arial, sans-serif" font-size="82" font-weight="700" letter-spacing="5">D3RO</text>
<text x="443" y="287" fill="#3b82f6" font-family="Arial, sans-serif" font-size="38" font-weight="700" letter-spacing="17">VOICE</text>
<g stroke="#cbd2df" stroke-width="2">
<path d="M444 344H954"/>
<path d="M444 372H954"/>
<path d="M444 400H954"/>
</g>
<g fill="#3b82f6">
<rect x="444" y="366" width="8" height="13" rx="4"/>
<rect x="460" y="349" width="8" height="47" rx="4"/>
<rect x="476" y="333" width="8" height="79" rx="4"/>
<rect x="492" y="357" width="8" height="31" rx="4"/>
<rect x="508" y="341" width="8" height="63" rx="4"/>
<rect x="524" y="362" width="8" height="21" rx="4"/>
<rect x="540" y="352" width="8" height="41" rx="4"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View file

@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 500">
<rect width="1024" height="500" fill="#19191b"/>
<rect x="52" y="42" width="920" height="416" rx="34" fill="#15161a" stroke="#31343d" stroke-width="2"/>
<text x="90" y="314" fill="#f7f8fb" font-family="Arial, sans-serif" font-size="210" font-weight="700" letter-spacing="-8">D3RO</text>
<rect x="90" y="354" width="378" height="58" rx="29" fill="#3b82f6"/>
<text x="279" y="394" text-anchor="middle" fill="#101115" font-family="Arial, sans-serif" font-size="28" font-weight="700" letter-spacing="11">VOICE</text>
<circle cx="826" cy="250" r="112" fill="#0e0f12" stroke="#3b82f6" stroke-width="2"/>
<g transform="translate(730 106) scale(3)" fill="none" stroke="#3b82f6" stroke-linecap="round">
<circle cx="32" cy="28" r="8" stroke-width="3"/>
<rect x="29" y="36" width="6" height="10" rx="3" fill="#3b82f6" stroke="none"/>
<path d="M20 32Q20 44 32 44Q44 44 44 32" stroke-width="2.5"/>
<path d="M32 44V52M26 52H38" stroke-width="2.5"/>
</g>
<path d="M720 404H932" stroke="#3b82f6" stroke-width="3" stroke-dasharray="3 12" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,57 @@
{
"schemaVersion": 1,
"generatedOn": "2026-08-24",
"selection": {
"recommended": "A",
"status": "pending_user_selection_and_console_preview"
},
"brandSource": "site/public/favicon.svg",
"export": {
"tool": "ImageMagick 7.1.2-25 Q16-HDRI",
"format": "PNG24",
"colorSpace": "sRGB",
"width": 1024,
"height": 500,
"bitDepth": 8,
"pngColorType": 2,
"opaque": true
},
"candidates": [
{
"id": "A",
"source": "candidate-a-signal-grid.svg",
"sourceBytes": 1411,
"sourceSha256": "3A39D0D61F5AE7E182149BD11C9D7F491FBEC0A542999B0516C869AE82E24B1B",
"output": "candidate-a-signal-grid.png",
"outputBytes": 25200,
"outputSha256": "3CFA86A4843339FD9C15781598E301ECE29C2B1522828F7AD1B69D8497775103"
},
{
"id": "B",
"source": "candidate-b-voice-orbit.svg",
"sourceBytes": 1365,
"sourceSha256": "DB7D778394419C6AF8FF43C7490B43C0CD65C61CE012A358FB37E603431082F4",
"output": "candidate-b-voice-orbit.png",
"outputBytes": 44972,
"outputSha256": "F7FCB26B063762D08AC1211A85CFDDD3C99C14936CD6DE753F5E9F88BC0B0D14"
},
{
"id": "C",
"source": "candidate-c-blueprint-band.svg",
"sourceBytes": 1443,
"sourceSha256": "176BEB99EA418F78BF5F9DC4E4D6A3A3700F5CEADA28697F3750B0C8883E4546",
"output": "candidate-c-blueprint-band.png",
"outputBytes": 14337,
"outputSha256": "C484AC514966347BF4E7AA526B040B88DDA89F9150AC3677B8C74297A1373C1E"
},
{
"id": "D",
"source": "candidate-d-monolith.svg",
"sourceBytes": 1151,
"sourceSha256": "6560C334C061470959D9BA120E58CCA264B5A1ED41FBC3045F804049117640FF",
"output": "candidate-d-monolith.png",
"outputBytes": 28737,
"outputSha256": "28FC7B608454E073A29AFE2149414F4186629AE8889B3FE2FA683D786F8C3EA9"
}
]
}

View file

@ -0,0 +1,270 @@
{
"schemaVersion": 1,
"canonicalSource": {
"path": "docs/v3/play/assets/d3ro-voice-play-icon-source.svg",
"viewBox": "0 0 512 512",
"sha256": "E2390BF8AA82EA357D61D8834F9FC58179227A87E26672699C63866A533B7E5B"
},
"playStoreIcon": {
"path": "docs/v3/play/assets/d3ro-voice-play-icon-512.png",
"width": 512,
"height": 512,
"bitDepth": 8,
"colorType": 6,
"alpha": "opaque",
"sha256": "0FAE53FB40CCE4B9FB0213CBEDB4E087CA00A8F8C438D6731C5C47C4AFD096B1"
},
"android": {
"applicationManifest": "apps/mobile-rn/android/app/src/main/AndroidManifest.xml",
"icon": "@mipmap/ic_launcher",
"roundIcon": "@mipmap/ic_launcher_round",
"background": {
"path": "apps/mobile-rn/android/app/src/main/res/values/colors.xml",
"resource": "d3ro_launcher_background",
"value": "#19191B"
},
"sourceAssets": [
{
"path": "docs/v3/play/assets/d3ro-voice-legacy-icon-source.svg",
"viewBox": "0 0 512 512",
"sha256": "8DDC2ACB6C8BC379611597A47FECA65DDC61237DE30B78C16F355D00FC264783"
},
{
"path": "docs/v3/play/assets/d3ro-voice-round-icon-source.svg",
"viewBox": "0 0 512 512",
"sha256": "978BEB618DDC8BC691090AC779CDB8177C62E70BEF538C407D0FE599D7126874"
}
],
"vectors": {
"foreground": {
"path": "apps/mobile-rn/android/app/src/main/res/drawable/d3ro_launcher_foreground.xml",
"sha256": "F2123246C93C1D11ED2DE530F17203C4A8F258DF244BC24146CDE0E09737345D"
},
"monochrome": {
"path": "apps/mobile-rn/android/app/src/main/res/drawable/d3ro_launcher_monochrome.xml",
"sha256": "035F9599E46E4AB34F30EAA73A586DFAB0C086C7A653122EE3C33307E908A650"
}
},
"adaptiveIcons": [
{
"path": "apps/mobile-rn/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
"monochrome": false
},
{
"path": "apps/mobile-rn/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
"monochrome": false
},
{
"path": "apps/mobile-rn/android/app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml",
"monochrome": true
},
{
"path": "apps/mobile-rn/android/app/src/main/res/mipmap-anydpi-v33/ic_launcher_round.xml",
"monochrome": true
}
],
"legacyIcons": [
{
"density": "mdpi",
"pixels": 48,
"iconPath": "apps/mobile-rn/android/app/src/main/res/mipmap-mdpi/ic_launcher.png",
"iconSha256": "82C702747E3BBF2A03826989D093CA99C3CBB8A125B81F2F0631FF66A2F536EA",
"roundPath": "apps/mobile-rn/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png",
"roundSha256": "202002E1C9A68035C2A2C7F1313EB73E1F0CF0578D0228F9FBCEDDA809B5D891"
},
{
"density": "hdpi",
"pixels": 72,
"iconPath": "apps/mobile-rn/android/app/src/main/res/mipmap-hdpi/ic_launcher.png",
"iconSha256": "AB5FE36F55A2E58FCB5DDEC58493D21B8A10EFC9324E1D272FCFEFD25DDB7901",
"roundPath": "apps/mobile-rn/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png",
"roundSha256": "7D73AEA182A82DAD88595FAB97A95C142E708993FAD806BBDE48E7A2B2B6F088"
},
{
"density": "xhdpi",
"pixels": 96,
"iconPath": "apps/mobile-rn/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png",
"iconSha256": "E563FA0698BB5013837C4AFF4152BF180F48997410A57986FDD02CE4F52D5818",
"roundPath": "apps/mobile-rn/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png",
"roundSha256": "1FD9924F6A201FD182A1B67FBAC8C81683A6D2450CABD02EF2EEED3D9B8B5F80"
},
{
"density": "xxhdpi",
"pixels": 144,
"iconPath": "apps/mobile-rn/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png",
"iconSha256": "5347ABF45AA423A63E60638D28310F15929BAF768E0584CA414B1B23B9B3F8F5",
"roundPath": "apps/mobile-rn/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png",
"roundSha256": "A9996374E5ED94F05DD0501BF02259B40C8EEA2F0373A85809921BD75EA1704F"
},
{
"density": "xxxhdpi",
"pixels": 192,
"iconPath": "apps/mobile-rn/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png",
"iconSha256": "A84BC25E362397527AD048EF931816021AD3C1482982FFA2E4F1F8DF52FFFB33",
"roundPath": "apps/mobile-rn/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png",
"roundSha256": "144564057C9962CC50989B3A4FCA1691B964D8EE69FF47EA37FD65F474A568A1"
}
]
},
"ios": {
"source": "docs/v3/play/assets/d3ro-voice-play-icon-source.svg",
"contents": "apps/mobile-rn/ios/D3ROVoice/Images.xcassets/AppIcon.appiconset/Contents.json",
"project": "apps/mobile-rn/ios/D3ROVoice.xcodeproj/project.pbxproj",
"targetedDeviceFamilies": [1, 2],
"bitDepth": 8,
"colorType": 2,
"alphaChannel": false,
"icons": [
{
"idiom": "iphone",
"size": "20x20",
"scale": "2x",
"filename": "AppIcon-iphone-20@2x.png",
"pixels": 40,
"sha256": "750DB19CBEC82017CA68D622FA4DF86F9A7E11EEE49390CEBEEC283C0AB10496"
},
{
"idiom": "iphone",
"size": "20x20",
"scale": "3x",
"filename": "AppIcon-iphone-20@3x.png",
"pixels": 60,
"sha256": "4715F000171E6CA1CD4F31C1BB5215F91FD86F414BFB288E42F0708474E2652B"
},
{
"idiom": "iphone",
"size": "29x29",
"scale": "2x",
"filename": "AppIcon-iphone-29@2x.png",
"pixels": 58,
"sha256": "1A13D610E1EEB1B508D496A017B611F36307EFBC50681613DADD634D415BFC75"
},
{
"idiom": "iphone",
"size": "29x29",
"scale": "3x",
"filename": "AppIcon-iphone-29@3x.png",
"pixels": 87,
"sha256": "F8A6A88020CEC4446A9B295C8696EF2D32838F9E4074C3FCF3E0A283F1A7246E"
},
{
"idiom": "iphone",
"size": "40x40",
"scale": "2x",
"filename": "AppIcon-iphone-40@2x.png",
"pixels": 80,
"sha256": "DA0796E96389EEE5D36CC390EA5850FF2DF3FC5D1B291D57BB43053FBEAC9F49"
},
{
"idiom": "iphone",
"size": "40x40",
"scale": "3x",
"filename": "AppIcon-iphone-40@3x.png",
"pixels": 120,
"sha256": "56FB0B823D72C4133E4BF1E24BBB8FA9E24E8C3721A8EF608BBF1A1097052CA7"
},
{
"idiom": "iphone",
"size": "60x60",
"scale": "2x",
"filename": "AppIcon-iphone-60@2x.png",
"pixels": 120,
"sha256": "56FB0B823D72C4133E4BF1E24BBB8FA9E24E8C3721A8EF608BBF1A1097052CA7"
},
{
"idiom": "iphone",
"size": "60x60",
"scale": "3x",
"filename": "AppIcon-iphone-60@3x.png",
"pixels": 180,
"sha256": "1A2843E454496520359D740450C699636435D641717BA963D751D817BE79F8F6"
},
{
"idiom": "ipad",
"size": "20x20",
"scale": "1x",
"filename": "AppIcon-ipad-20@1x.png",
"pixels": 20,
"sha256": "399D5A67D13569E04725DA466AAE45419595D51828C286A4424DB204C8202D7F"
},
{
"idiom": "ipad",
"size": "20x20",
"scale": "2x",
"filename": "AppIcon-ipad-20@2x.png",
"pixels": 40,
"sha256": "750DB19CBEC82017CA68D622FA4DF86F9A7E11EEE49390CEBEEC283C0AB10496"
},
{
"idiom": "ipad",
"size": "29x29",
"scale": "1x",
"filename": "AppIcon-ipad-29@1x.png",
"pixels": 29,
"sha256": "BF07A8DB710655D650F102ABA31A74BD68BA26C10A4DB1E22D6743908CDCA61A"
},
{
"idiom": "ipad",
"size": "29x29",
"scale": "2x",
"filename": "AppIcon-ipad-29@2x.png",
"pixels": 58,
"sha256": "1A13D610E1EEB1B508D496A017B611F36307EFBC50681613DADD634D415BFC75"
},
{
"idiom": "ipad",
"size": "40x40",
"scale": "1x",
"filename": "AppIcon-ipad-40@1x.png",
"pixels": 40,
"sha256": "750DB19CBEC82017CA68D622FA4DF86F9A7E11EEE49390CEBEEC283C0AB10496"
},
{
"idiom": "ipad",
"size": "40x40",
"scale": "2x",
"filename": "AppIcon-ipad-40@2x.png",
"pixels": 80,
"sha256": "DA0796E96389EEE5D36CC390EA5850FF2DF3FC5D1B291D57BB43053FBEAC9F49"
},
{
"idiom": "ipad",
"size": "76x76",
"scale": "1x",
"filename": "AppIcon-ipad-76@1x.png",
"pixels": 76,
"sha256": "460B8F8AF51D947CEA6A40A39CA10DC4E7D774846DC4AE20275402B9B25439D6"
},
{
"idiom": "ipad",
"size": "76x76",
"scale": "2x",
"filename": "AppIcon-ipad-76@2x.png",
"pixels": 152,
"sha256": "47E8DA09603F931E25E92AF77405C7C55B4CD56EA3E35DCAE3A423CE82F625B1"
},
{
"idiom": "ipad",
"size": "83.5x83.5",
"scale": "2x",
"filename": "AppIcon-ipad-83.5@2x.png",
"pixels": 167,
"sha256": "8EC9EEE448DAAD188AA6F730BCC80D14D1BD7707207A7573CF04E232F7E9C0D7"
},
{
"idiom": "ios-marketing",
"size": "1024x1024",
"scale": "1x",
"filename": "AppIcon-marketing-1024@1x.png",
"pixels": 1024,
"sha256": "E0D0357BAB6EB61628AF8FA50892BFA80D5F0293419A19E1E94A652B0037B1B0"
}
]
},
"pixelLauncherProof": {
"path": "docs/v3/play/assets/proof/d3ro-icon-pixel-launcher-20260824.png",
"width": 1080,
"height": 1920,
"sha256": "7A2DA876FBFD54089B783A7FD45C2472476ED47DCEABA08AB305B7B01A18D430"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB