diff --git a/.github/scripts/firebase-wif-canary-policy.test.mjs b/.github/scripts/firebase-wif-canary-policy.test.mjs index f1653a5..cd7c4f6 100644 --- a/.github/scripts/firebase-wif-canary-policy.test.mjs +++ b/.github/scripts/firebase-wif-canary-policy.test.mjs @@ -75,6 +75,16 @@ test("WIF canary is manual, protected, and branch-restricted", () => { assert.match(canaryWorkflow, /CONFIRM_UPLOAD/); }); +test("WIF canary overrides the normal tester recipients and identifies its build", async () => { + assert.match(canaryWorkflow, /SLOWCLOCK_DISTRIBUTION_UPLOAD_ONLY: 'true'/); + assert.doesNotMatch(canaryWorkflow, /--(?:groups|testers)/); + const appBuild = await readFile(new URL("../../app/build.gradle.kts", import.meta.url), "utf8"); + assert.match(appBuild, /groups = if \(System.getenv\("SLOWCLOCK_DISTRIBUTION_UPLOAD_ONLY"\) == "true"\) "" else "slowclock"/); + assert.doesNotMatch(appBuild, /(?:testers|testersFile|groupsFile)\s*=/); + assert.match(canaryWorkflow, /SLOWCLOCK_VERSION_CODE: \$\{\{ github\.run_number \}\}/); + assert.match(canaryWorkflow, /SLOWCLOCK_VERSION_NAME_SUFFIX="canary\./); +}); + test("WIF credentials are issued only after the release APK build", () => { const buildIndex = canaryWorkflow.indexOf("./gradlew assembleRelease"); const attestationIndex = canaryWorkflow.indexOf( diff --git a/.github/workflows/firebase-wif-canary.yml b/.github/workflows/firebase-wif-canary.yml index 47cdaff..2bce266 100644 --- a/.github/workflows/firebase-wif-canary.yml +++ b/.github/workflows/firebase-wif-canary.yml @@ -115,7 +115,10 @@ jobs: # public 저장소이므로 signed APK를 Actions artifact로 게시하거나 job 사이에 전달하지 않는다. - name: Build signed release APK without Google credentials id: release_apk + env: + SLOWCLOCK_VERSION_CODE: ${{ github.run_number }} run: | + export SLOWCLOCK_VERSION_NAME_SUFFIX="canary.${GITHUB_RUN_NUMBER}.${GITHUB_SHA:0:7}" ./gradlew assembleRelease --no-daemon mapfile -t apk_files < <(find app/build/outputs/apk/release -maxdepth 1 -type f -name '*.apk' -print) @@ -179,6 +182,7 @@ jobs: - name: Upload the already-built APK to Firebase App Distribution env: + SLOWCLOCK_DISTRIBUTION_UPLOAD_ONLY: 'true' ATTESTATION_URL: ${{ steps.provenance.outputs.attestation-url }} EXPECTED_SHA256: ${{ steps.release_apk.outputs.sha256 }} RELEASE_APK_PATH: ${{ steps.release_apk.outputs.path }} diff --git a/.github/workflows/release-distribution.yml b/.github/workflows/release-distribution.yml index 9cf9598..0ef7742 100644 --- a/.github/workflows/release-distribution.yml +++ b/.github/workflows/release-distribution.yml @@ -7,6 +7,11 @@ name: Release Distribution (Firebase App Distribution) on: push: branches: [main] + # 문서만 바뀐 머지는 같은 앱을 다시 빌드하거나 테스터에게 알리지 않는다. + paths-ignore: + - 'docs/**' + - '*.md' + - 'LICENSE' # 같은 Firebase App Distribution 앱에 올리는 경로를 전부 이 group 하나로 직렬화한다 — main 릴리스 # push 에 WIF canary(firebase-wif-canary.yml)가 겹치면 러너 둘이 같은 앱에 업로드한다. group 문자열은 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 185947f..4deac9d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -106,7 +106,8 @@ android { firebaseAppDistribution { // 릴리스 노트는 배포 워크플로가 머지된 PR 본문에서 만들어 --releaseNotesFile 로 넘긴다. // 여기서 releaseNotes 를 지정하면 그 파일을 덮어써 모든 배포가 같은 문구로 나간다(#79). - groups = "slowclock" + // WIF canary는 인증·업로드만 확인하며 테스터에게 전달하지 않는다. + groups = if (System.getenv("SLOWCLOCK_DISTRIBUTION_UPLOAD_ONLY") == "true") "" else "slowclock" } } } diff --git a/docs/firestore.md b/docs/firestore.md index 83beb34..13a5ee3 100644 --- a/docs/firestore.md +++ b/docs/firestore.md @@ -67,7 +67,11 @@ npm test ## 배포 -2026-09-06 작업 기준으로 운영 규칙은 배포하지 않았으며, 운영 공유 등록부의 inventory와 충돌 정리도 아직 확인하지 않았다. 저장소·에뮬레이터 검증 통과를 운영 전환 완료로 간주하지 않는다. +2026-09-06 19:08 KST 운영 전환: 사용자 4건·일정 26건에서 공유 코드 소유 관계를 점검했다. 소유자 충돌은 없었고 누락된 등록부 4건을 읽기·쓰기 transaction 및 `exists=false` 사전조건으로 생성했다. 기존 사용자·일정·공유 코드는 보존했다. 공개 프로필 1건에 남아 있던 `shareCode` 필드는 문서 updateTime 사전조건과 필드 마스크로 제거했다. + +규칙·transaction 에뮬레이터 테스트 62개 통과 후 main `c00e38f`의 규칙을 운영에 배포했고, Rules API 재조회에서 원문 SHA-256 `ba96459964e412a068d2f8a4550fc996aecf517790c6f9082a78419c4bd0375c` 일치를 확인했다. 등록부 4건의 소유 관계도 재조회했고 공개 프로필의 공유 코드 잔존은 0건이다. 기존 일정 인덱스는 READY여서 새 인덱스 생성은 필요하지 않았다. 결제 연결은 변경하지 않았다. + +이 결과는 관리자 API로 확인한 운영 데이터·규칙 적용 증거다. 사용자 요청으로 실기기 로그인·일정·공유 검증은 미뤘으며, Functions의 새 data-only 배포는 #102의 과금 제약 때문에 수행하지 않았다. 전체 앱 출시나 운영 클라이언트 검증 완료를 뜻하지 않는다. 이후 전환 때도 다음 순서를 따른다. 1. 기존 사용자·일정의 공유 코드와 `shareCodes` 소유 관계를 점검한다. 누락·중복·소유자 불일치를 구분하고, 기존 사용자의 코드가 바뀌거나 다른 소유자의 등록부를 덮지 않도록 충돌을 정리한다. 새 앱의 자동 복구만으로 구버전 사용자의 등록부까지 정리됐다고 가정하지 않는다. 2. 점검·정리 결과를 확인한 뒤 아래 명령으로 새 규칙을 적용한다. 새 앱을 구규칙에 먼저 연결하면 등록부 동일값 확인이나 첫 transaction의 미존재 일정 조회가 거절될 수 있다. 반대로 등록부가 맞지 않는 구버전 앱은 새 규칙 아래 공유 일정 생성·편집·완료가 거절될 수 있다. diff --git a/docs/play-release.md b/docs/play-release.md index 529e8e3..3b20fc5 100644 --- a/docs/play-release.md +++ b/docs/play-release.md @@ -15,7 +15,9 @@ Firebase App Distribution 과 Google Play 의 목적을 분리하고, 첫 Play ### Play Console 확인 상태 -2026-09-05 기준 Play 개발자 계정이 없다. 2023-11-13 이후 만든 개인 계정은 테스터 12명이 14일 연속 옵트인한 비공개 테스트를 거쳐야 production 신청이 가능하고, 심사는 최대 7일이다. 계정 생성과 테스터 모집이 일정의 하한이므로 앱 작업과 무관하게 먼저 시작한다. +2026-09-06 실제 Console을 다시 확인했다. 로그인 계정이 접근 가능한 개인 개발자 계정은 있으나, 신원 확인 진행 중이며 소유자 연락처 전화번호 인증이 남아 있다. 앱이 없고 「앱 만들기」가 비활성화되어 있으므로 SlowClock의 앱 생성·등록정보 저장·AAB 업로드·선언 제출은 아직 실행할 수 없다. 접근 가능한 개발자 계정이 SlowClock을 게시할 계정으로 확정된 것은 아니다. 계정 소유자의 확인이 끝난 뒤 사용할 게시 계정에서 이어간다. + +등록정보·이미지·앱 콘텐츠 답안은 저장소에서 준비한다. 계정 생성 수수료를 결제하거나 소유자 본인 인증을 대신하지 않는다. 신규 개인 계정의 비공개 테스트 요건과 심사 일정은 실제 계정에 표시되는 안내 및 아래 공식 문서로 확인한다. 공식 근거: [App testing requirements for new personal developer accounts](https://support.google.com/googleplay/android-developer/answer/14151465) @@ -65,13 +67,13 @@ PR #199에서 Functions의 `qs`를 `6.16.0`으로 제한 업데이트하고 Node |---|---|---| | 개인정보처리방침 URL | `https://1hyok.me/SlowClock/privacy.html` | GitHub Pages. github.io 주소는 이 도메인으로 리다이렉트된다 | | 광고 | 광고 없음 | 광고 SDK 없음, `AD_ID` 권한 제거 | -| 앱 액세스 | 일부 기능 제한(로그인 필요). 안내문: "Google 계정으로 로그인하면 모든 기능을 쓸 수 있습니다. 별도의 테스트 계정이나 자격 증명은 필요 없습니다." | Firebase UI Google 로그인만 제공 | +| 앱 액세스 | 일부 기능 제한(로그인 필요). 안내문: "Google 계정으로 로그인하면 일정·알람·공유 코드 열람을 사용할 수 있습니다. 별도 가입 승인, 유료 구독이나 초대 제한은 없습니다. 공유를 확인하려면 한 계정의 내 정보에 표시되는 코드를 다른 계정의 공유 화면에 입력하세요." | Firebase UI Google 로그인만 제공. 현재 동작하지 않는 보호자 푸시까지 모든 기능 사용 가능 문구로 약속하지 않음 | | 콘텐츠 등급 | 앱 유형 「유틸리티·생산성·커뮤니케이션·기타」. 폭력·성적 내용·욕설·약물·도박 전부 「아니오」. 사용자 간 상호작용 「예」(공유 코드를 받은 사람이 그 사람의 일정과 이름을 본다), 개인정보 공유 「예」(이름·일정을 본인이 코드를 알려 준 사람과 공유), 위치 공유 「아니오」, 디지털 구매 「아니오」 | 공유 코드 기능(정보 화면). 가족 그룹을 만드는 화면은 앱에 없다 — `familyGroups` 컬렉션은 계정 삭제 때 정리하는 용도로만 남아 있다 | | 타겟층 | 18세 이상만 선택. 어린이 대상 아님 | 어르신·보호자용 앱 | | 뉴스 앱 | 아니오 | 정보 탭은 메디컬타임즈로 이동하는 버튼 하나뿐이고 기사를 앱 안에 표시하지 않는다(#51) | | 정부 앱 | 아니오 | | | 금융 기능 | 없음 | | -| 건강 | 제출 전 범주 확인 필요. 현재 기능을 그대로 출시하면서 「건강 기능 없음」으로 단정하지 않는다 | 앱 안에 ADHD 분류와 감정 일기·회피 행동 돌아보기·명상 일정 추천이 있다. 기사 스크래핑 제거(#51)만으로 이 추천까지 없어지지는 않는다 | +| 건강 | 제출 답안: Stress Management, Relaxation, Mental Acuity / Mental and Behavioral Health / Medication and Treatment Management / Healthcare Services and Management | 명상·ADHD 감정 일기 및 회피 행동 돌아보기·약 먹기·병원 예약 일정 예시를 공식 범주에 대조한 판단. 의료기기·진단·처방·치료·측정·Health Connect 기능은 제공하지 않음. Console 제출 및 Google 심사 완료를 뜻하지 않음 | | 데이터 보안: 수집 | 개인 정보(이름, 이메일 주소, 사용자 ID), 앱 활동(앱 상호작용, 기타 사용자 생성 콘텐츠: 일정·메모), 기기 또는 기타 ID(FCM 토큰, Firebase 설치 ID, Analytics 앱 인스턴스 ID, Crashlytics 설치 식별자), 위치(대략적인 위치: IP 기반), 앱 정보 및 성능(비정상 종료 로그, 진단) | Firebase Auth·Firestore·FCM·Analytics·Crashlytics | | 데이터 보안: 공유 | 제3자 공유 없음. Firebase 는 서비스 제공업체. 공유 코드를 통한 열람은 사용자가 시작한 행동 | privacy.html 4절, [firestore.md](firestore.md) 의 컬렉션 표 | | 데이터 보안: 처리 | 전송 중 암호화 「예」. 계정 삭제 요청 방법 「예」: 앱 안 내 정보 화면과 `https://1hyok.me/SlowClock/delete-account.html`(#46) | HTTPS, #46 | @@ -83,9 +85,11 @@ PR #199에서 Functions의 `qs`를 `6.16.0`으로 제한 업데이트하고 Node Analytics는 광고 ID 권한을 제거해도 앱 인스턴스 ID와 IP 기반 대략적 위치를 자동 수집한다. 선언 근거는 [Analytics 공식 안내](https://support.google.com/analytics/answer/11582702?hl=en)와 [Firebase SDK별 안내](https://firebase.google.com/docs/android/play-data-disclosure)다. 현재 앱에는 새 그룹 생성 기능이 없고 과거 그룹 기록은 계정 삭제 때 정리한다. -건강 선언은 데이터 수집 여부와 별개로 실제 제공 기능을 본다. [Play 공식 선언 안내](https://support.google.com/googleplay/android-developer/answer/14738291?hl=en)는 명상·인지 건강 안내를 스트레스 관리/이완/정신적 예민함 범주로, 정신 건강 지원 도구를 정신 및 행동 건강 범주로 설명한다. 현재 추천 내용은 이 범주와 대조해야 한다는 판단이며 Google의 심사 결과를 확인한 것은 아니다. 제출 전 실제 추천 화면과 스토어 문구를 함께 보고 해당 범주를 선택하고, 확정 전에는 선언을 초안으로 남긴다. 앱에는 진단·치료·측정 기능이나 Health Connect 연동이 구현되어 있지 않지만 그것만으로 건강 기능 전체가 없다고 판단하지 않는다. [건강 콘텐츠 정책](https://support.google.com/googleplay/android-developer/answer/16679511?hl=en)도 함께 확인한다. +건강 선언은 데이터 수집 여부와 별개로 실제 제공 기능을 본다. [Play 공식 선언 안내](https://support.google.com/googleplay/android-developer/answer/14738291?hl=en)를 `RecommendationScreen.kt`의 추천 내용에 대조해 위 답안을 정했다. 범주는 명상·인지 건강 안내, 정신 건강 지원, 복약 알림, 의료 예약 일정에 각각 대응한다. 앱은 의료 판단을 하지 않고 사용자가 정한 일정과 알람을 제공한다. 계정 확인이 끝나 Console 입력이 열리면 위 답안을 옮기고 추가 질문을 실제 기능에 맞게 작성한다. [건강 콘텐츠 정책](https://support.google.com/googleplay/android-developer/answer/16679511?hl=en)에 따라 스토어 설명에는 의료기기가 아니며 질환을 진단·치료·치유·예방하지 않는다는 안내와 의료 전문가 상담 안내를 포함한다. + +2026-09-06 연결된 `slow-clock-scheduler` Analytics 속성에서 이벤트 보관 2개월, 사용자 데이터 보관 14개월, 새 사용자 활동 시 보관 기간 재설정 켜짐을 확인했다. Google Signals는 꺼져 있고 Google Ads 연결은 0건이다. 광고 개인화는 전체 307개 지역 허용에서 0개 허용으로 변경·저장했으며 이후 수집 데이터에 적용된다. 기존 데이터에 소급되는 삭제 조치는 아니다. [광고 개인화 설정의 적용 범위](https://support.google.com/analytics/answer/9626162?hl=en). -제출 전에 Analytics Console의 보유 기간·Google Signals·광고 계정 연결·데이터 공유 설정을 실제로 확인해 선언과 맞춘다. 저장소만으로 이 설정의 현재 값을 확인한 것으로 취급하지 않는다. 계정 삭제와 별도 설치 식별자의 보유 범위는 개인정보처리방침 및 계정 삭제 안내에 함께 설명한다. +Analytics 계정 데이터 공유는 Google 제품 및 서비스 꺼짐, 참여 모델링 및 비즈니스 통계·기술 지원·비즈니스 추천 켜짐을 확인했다. 계정 전체에 적용되는 값은 변경하지 않았다. 제출할 때 같은 속성과 계정의 최신 값을 다시 대조한다. 계정 삭제와 별도 설치 식별자의 보유 범위는 개인정보처리방침 및 계정 삭제 안내에 함께 설명한다. ### 스크린샷 촬영 절차 diff --git a/docs/play/listing.md b/docs/play/listing.md index fa34038..0437dc7 100644 --- a/docs/play/listing.md +++ b/docs/play/listing.md @@ -32,6 +32,10 @@ - Google 계정으로 로그인합니다. 일정과 계정 정보는 Firebase에 저장되며 광고는 없습니다. 앱 사용 통계에는 IP 주소에서 추정한 대략적인 위치가 포함됩니다. - 계정 삭제는 앱의 내 정보 화면에서 할 수 있습니다. +건강 관련 안내 + +느린 시계는 의료기기가 아니며 질환을 진단·치료·치유·예방하지 않습니다. 추천은 일상 일정의 예시입니다. 의료 조언, 진단 또는 치료가 필요하면 의료 전문가와 상담해 주세요. + 개인정보처리방침: https://1hyok.me/SlowClock/privacy.html 이용약관: https://1hyok.me/SlowClock/terms.html 문의: dnfjddk2@gmail.com diff --git a/docs/privacy.html b/docs/privacy.html index 0bb69b0..6312df8 100644 --- a/docs/privacy.html +++ b/docs/privacy.html @@ -37,6 +37,7 @@
Firebase Analytics의 이벤트 데이터 보관 기간은 2개월, 사용자 데이터 보관 기간은 14개월입니다. 새 사용자 활동이 발생하면 해당 사용자의 보관 기간이 재설정됩니다. 합산 데이터를 사용하는 표준 보고서에는 이 기간이 동일하게 적용되지 않을 수 있습니다. Google Signals는 사용하지 않으며, 2026년 9월 6일부터 새로 수집하는 Analytics 데이터의 광고 개인화를 모든 지역에서 허용하지 않습니다.
앱은 Firebase 계정 UID를 Analytics나 Crashlytics의 사용자 ID로 설정하지 않습니다. 이 서비스들은 별도의 앱·설치 식별자를 사용하므로 모든 원본 기록이 익명 집계 자료인 것은 아닙니다. Crashlytics는 비정상 종료 기록과 관련 식별자를 90일 동안 보관한 뒤 운영 시스템과 백업에서 삭제하는 절차를 시작합니다.
계정과 연결된 일정·공유 기록·알림 기록·계정 정보는 회원 탈퇴(계정 삭제)가 완료되면 삭제합니다. Analytics 및 설치 식별자에 연결된 기록은 계정 삭제만으로 즉시 삭제되는 항목이 아니며 각 서비스의 보유·삭제 정책이 적용됩니다. 해당 기록의 삭제 요청도 아래 문의처로 접수할 수 있습니다. 다만 관련 법령에 따라 보존이 필요한 경우 해당 기간 동안 보관합니다. 삭제 요청은 아래 문의처로 보내 주시면 7일 이내에 처리합니다.
diff --git a/docs/release/distribution.md b/docs/release/distribution.md index 65bed1f..0b6d57b 100644 --- a/docs/release/distribution.md +++ b/docs/release/distribution.md @@ -1,6 +1,6 @@ # 테스터 APK 배포 (Firebase App Distribution) -`main` 에 머지된 빌드를 Firebase App Distribution 으로 테스터에게 자동 전달하는 흐름. Firebase 프로젝트 `slow-clock-scheduler`, 테스터 그룹 `slowclock`. +`main` 에 머지된 앱 변경을 Firebase App Distribution 으로 테스터에게 자동 전달하는 흐름. `docs/**`·루트 Markdown·LICENSE만 바뀐 push는 제외한다. Firebase 프로젝트 `slow-clock-scheduler`, 테스터 그룹 `slowclock`. 이 배포는 Google Play 출시가 아니다. Play 내부 테스트 트랙 배포는 [Play release runbook](../play-release.md) 을 따른다. @@ -9,7 +9,7 @@ | 채널 | 트리거 | 워크플로 | 산출물 | 자격 | |---|---|---|---|---| | Firebase App Distribution | `main` push | [`release-distribution.yml`](../../.github/workflows/release-distribution.yml) | release APK | `release-distribution` environment | -| WIF canary | 수동 실행 + environment 승인 | [`firebase-wif-canary.yml`](../../.github/workflows/firebase-wif-canary.yml) | release APK | `release-distribution` environment | +| WIF canary | 수동 실행 + environment 승인 | [`firebase-wif-canary.yml`](../../.github/workflows/firebase-wif-canary.yml) | 업로드만 하는 canary APK, 테스터 배포 없음 | `release-distribution` environment | `feat/