docs: record Wave 3 surface consolidation and fold NAS-only whisper into compose

- docs/REFACTOR_WAVE3_REPORT.md and the Wave 3 policy: canonical map,
  production changes, verification and remaining external steps.
- Gap backlog: GAP-BILL-01 resolved; new GAP-BILL-02 (Payple renewal never ran,
  Payple client key never set), GAP-WEB-01 (tunnel host for /app), GAP-OPS-01
  (NAS compose/.env drift), GAP-CI-01, GAP-I18N-02, GAP-TEAM-02.
- design.md: hero loop decision (numbers taken from the app capsule), pricing
  mismatch closed; feature catalog SHELL-11 updated.
- docs/map, release guide and mobile release docs no longer describe the
  deleted wwwroot, binaries, Dockerfile.admin, NAS site copy or .github CI.
- docker-compose.nas.yml gains the d3ro-whisper service that only existed in
  the NAS copy, so the repository file is the complete definition.
- refactor-wave skill: Wave 3 index and lessons P10-P12.
This commit is contained in:
Yun Chan 2026-09-26 16:02:44 +09:00
parent 92978607da
commit e87ce63440
17 changed files with 187 additions and 46 deletions

View file

@ -207,11 +207,22 @@ git 충돌 + 의도 분리 불가. **R3 위반**.
### P9. lint 게이트의 pre-existing 부채 — "내 영역 errors 0"으로 재해석 ### P9. lint 게이트의 pre-existing 부채 — "내 영역 errors 0"으로 재해석
codebase가 원래 lint GREEN이 아닌 경우가 많음(루트 `eslint .`가 다른 앱의 config 문제로 붕괴, pre-existing errors 다수). "통합 후 lint GREEN"(R7 원문)이 비현실적. 게이트는 **"내가 건드린 영역의 lint errors 0"** + "pre-existing은 별도 wave로 분리"로 재해석. (검증 팁: `git diff --name-only HEAD`로 변경 파일만 lint → 내 책임 분리.) codebase가 원래 lint GREEN이 아닌 경우가 많음(루트 `eslint .`가 다른 앱의 config 문제로 붕괴, pre-existing errors 다수). "통합 후 lint GREEN"(R7 원문)이 비현실적. 게이트는 **"내가 건드린 영역의 lint errors 0"** + "pre-existing은 별도 wave로 분리"로 재해석. (검증 팁: `git diff --name-only HEAD`로 변경 파일만 lint → 내 책임 분리.)
### P10. "실행되지 않는 사본"과 "운영 서버 사본"도 중복 감사 대상 (Wave 3)
- 워크플로·스크립트는 원격·러너 기준으로 **실제로 실행되는지** 먼저 본다. `.github`는 GitHub 원격이 없어 한 번도 안 돌았고, 그 안의 Payple 갱신 cron 때문에 정기 결제가 멈춰 있었다.
- 배포 전 운영 서버의 설정 파일(NAS compose·.env 키 이름)과 저장소를 diff한다. 저장소를 그대로 덮으면 필수 변수 누락으로 서비스가 내려간다.
### P11. 직접 import할 수 없는 런타임은 "생성 사본 + --check"로만 복제 (Wave 3)
Deno Edge Function처럼 공유 패키지를 번들할 수 없으면 손으로 옮겨 적지 말고 생성 스크립트(`contract:sync`)와 CI 게이트(`contract:check`)를 둔다. `version:sync`와 같은 패턴.
### P12. 데스크톱 테스트는 Electron 런타임으로 (Wave 3)
Node로 vitest를 돌리면 better-sqlite3 ABI 불일치로 수백 건이 가짜 실패한다. `ELECTRON_RUN_AS_NODE=1 <electron.exe> <vitest.mjs> run --config vitest.config.ts`로 돌리고, 남는 실패만 회귀 후보로 본다.
## 누적 wave 인덱스 ## 누적 wave 인덱스
| Wave | 일시 | 산출 커밋 | 보고서 | | Wave | 일시 | 산출 커밋 | 보고서 |
|------|------|-----------|--------| |------|------|-----------|--------|
| Wave 1 | 2026-07-22 | b820c78(WS1)·aef4428(WS2)·078304d(WS3)·87f2dac(WS4) | `docs/REFACTOR_WAVE1_REPORT.md` | | Wave 1 | 2026-07-22 | b820c78(WS1)·aef4428(WS2)·078304d(WS3)·87f2dac(WS4) | `docs/REFACTOR_WAVE1_REPORT.md` |
| Wave 2 | 2026-07-22 | efac690(AMBER)·660e622(NATIVE)·00a99e4(POPUP)·4c25620(DEPS)·5b6e7aa(PATTERN) | `docs/REFACTOR_WAVE2_REPORT.md` | | Wave 2 | 2026-07-22 | efac690(AMBER)·660e622(NATIVE)·00a99e4(POPUP)·4c25620(DEPS)·5b6e7aa(PATTERN) | `docs/REFACTOR_WAVE2_REPORT.md` |
| Wave 3 | 2026-09-26 | 88f24d8(WS-A 계약)·b6fe588(WS-B 웹앱)·cd9d199(WS-C 중복 삭제)·dc43884(WS-D CI)·9297860(사이트) | `docs/REFACTOR_WAVE3_REPORT.md` |
다음 wave 추가 시 본 표 갱신. 다음 wave 추가 시 본 표 갱신.

View file

@ -189,6 +189,18 @@
- **기능 카드에 FREE/PRO 태그 없음.** 로컬 기능은 전부 free다(`LicenseService.FEATURE_MIN_TIER`). - **기능 카드에 FREE/PRO 태그 없음.** 로컬 기능은 전부 free다(`LicenseService.FEATURE_MIN_TIER`).
- **화자 분리·RAG를 대표 기능으로 내세우지 않음.** 화자 구분은 추정 단계라 약속하지 않는다. - **화자 분리·RAG를 대표 기능으로 내세우지 않음.** 화자 구분은 추정 단계라 약속하지 않는다.
- **CRT 움직이는 스캔 효과 제거**(정지 주사선만 히어로 예시 패널에 유지). 이전 기록의 "CRT CSS 유지"는 예시 패널 한 곳으로 좁혔다. - **CRT 움직이는 스캔 효과 제거**(정지 주사선만 히어로 예시 패널에 유지). 이전 기록의 "CRT CSS 유지"는 예시 패널 한 곳으로 좁혔다.
- **미확정:** 서버 결제 카탈로그(Payple)는 아직 Pro ₩9,900 / Pro+ ₩29,900이다. 사이트 가격과 맞추려면 서버 쪽 변경이 필요하다. - ~~미확정: 서버 가격 불일치·결제 URL 이원화~~ → 2026-09-26 Wave 3에서 해소(`packages/core` plan-catalog/web-urls 단일 정본, `/app/billing` 하나).
결제 URL `https://d3ro.chanpaca.net/billing?tier=`는 데스크톱 `payment-handlers.ts`와 같지만,
`license-handlers.ts`는 `https://d3ro.dev`를 쓴다 — 어느 쪽이 정본인지 확인 필요. ### 2026-09-26 추기 · 히어로 반복 예시(앱 녹음 캡슐 재현)
- 사용자 요청: "작동 방식 보기" CTA 대신 우측에서 앱과 똑같은 예시가 계속 돌게. 히어로 CTA는 다운로드 1개로 줄였다.
- `site/src/components/AppLoopDemo.tsx` — 메모 창 + 화면 아래 녹음 캡슐. 대기 → 오른쪽 Alt 누른 채 말하기(파형·경과 시간·부분 전사)
→ 처리 중(점근 진행 막대) → 커서 자리에 붙여넣기 → 다음 예시. 언어마다 예시 2개.
- 수치는 앱 `apps/desktop/src/renderer/popups/recording-tip`과 같다: 막대 9개 cos 가중치, 100ms 갱신, 스무딩 0.5, 무작위 ±35%,
높이 2–28px, 막대별 색, 진행 막대 120px `min(95, (1-1/(1+1.5t))*100)%`. 캡슐 CSS 값은 앱 style.css를 옮겨 적었다
(Electron 팝업 CSS는 전역 리셋을 포함해 사이트에서 직접 import할 수 없다 — 값이 바뀌면 두 곳을 함께 고친다).
- 접근성: 5초 넘게 움직이므로 일시정지 버튼(WCAG 2.2.2, aria-pressed). 화면 밖·숨은 탭에서 멈춤. 감소 모션은 정지 화면 한 장.
애니메이션은 aria-hidden, 화면 낭독용 설명문(demo.description)을 둔다. 중·일은 띄어쓰기가 없어 세 글자씩 나타난다.
- 하지 않은 것: 실제 음성 녹음·마이크 권한 데모(브라우저에서 로컬 모델을 돌릴 수 없고, 가짜 인식은 진실성 위반).
- 제거: 이전 버튼식 예시(DictationDemo), WaveBars, Crosshair, CRT 화면 CSS — 이전 기록의 "CRT CSS 유지"는 이로써 종료.
- 부수: `site/public/404.html`(없는 경로가 첫 화면으로 떨어지던 SPA 폴백 종료), 법률 페이지의 옛 주황 강조색을 파랑으로.
- 앱 쪽 발견(미수정): 녹음 캡슐의 "처리 중..."은 HTML에 한국어로 고정돼 있어 앱 언어를 따르지 않는다.

View file

@ -73,3 +73,28 @@ services:
options: options:
max-size: "10m" max-size: "10m"
max-file: "3" max-file: "3"
# ============================================================================
# 로컬 Whisper STT 서버 (faster-whisper, OpenAI 호환) — NAS 운영본에만 있던 서비스를 정본으로 합쳤다.
# large-v3/large-v3-turbo/small 모델은 캐시 볼륨에 저장된다.
# int8 양자화 + 2스레드 — J4025 2코어 기준 5초 오디오: small≈38s.
# ============================================================================
d3ro-whisper:
image: ghcr.io/speaches-ai/speaches:latest-cpu
container_name: d3ro_whisper
restart: unless-stopped
mem_limit: 5g
environment:
- WHISPER__COMPUTE_TYPE=int8
- WHISPER__CPU_THREADS=2
- TZ=${TZ:-Asia/Seoul}
volumes:
- whisper-cache:/home/ubuntu/.cache/huggingface
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
volumes:
whisper-cache:

View file

@ -103,3 +103,22 @@ D3RO는 이미 3계층 강제 규칙 체계를 가진다. 본 정책은 이를 *
| DP7 DRY 임계치 | 3회 | 2026-07-22 | | DP7 DRY 임계치 | 3회 | 2026-07-22 |
| DP9 데드코드 강도 | 공격적 DELETE (외부 API 노출 export는 grep 후 KEEP) | 2026-07-22 | | DP9 데드코드 강도 | 공격적 DELETE (외부 API 노출 export는 grep 후 KEEP) | 2026-07-22 |
| DP6 typecheck 범위 | 전체 모노레포 typecheck 강제 (ui/i18n/core 공유) | 2026-07-22 | | DP6 typecheck 범위 | 전체 모노레포 typecheck 강제 (ui/i18n/core 공유) | 2026-07-22 |
---
## Wave 3 (2026-09-26) — 표면·계약 단일화
사용자 결정: "두 벌씩 만든 것을 전부 하나로." 확정 사항과 정본은 아래와 같다.
| # | 결정 포인트 | 정본 | 파생(자동·검사) |
|---|---|---|---|
| W3-1 | 요금 | `packages/core/src/plan-catalog.ts` `PLAN_PRICE_KRW` (Free 0 / Pro 2,900 / Pro+ 8,900, 월). 기존 구독자도 다음 갱신부터 적용 | Deno `_shared/core-contract.generated.ts` (`npm run contract:sync`, CI `contract:check`) |
| W3-2 | 공개 URL | `packages/core/src/web-urls.ts` — 사이트 `/`, 웹앱 `/app`(Next basePath), `billingUrl()`, `SITE_URLS` | 같은 생성 파일 |
| W3-3 | 결제 진입 | 웹앱 `/app/billing` 하나. 데스크톱·모바일·사이트·Stripe 복귀는 모두 `billingUrl()`. 복귀 쿼리는 `success=1`/`canceled=1` | — |
| W3-4 | 웹앱 배포 | NAS Docker(`d3ro_voice_web`) → Cloudflare Tunnel 호스트 → 사이트 브리지 워커가 `/app/*`를 전달 | — |
| W3-5 | 랜딩·다운로드·법률·초대·assetlinks | `site/` 한 벌. apps/web·api-server wwwroot 사본 삭제, 웹앱 `/download`는 사이트로 리다이렉트 | — |
| W3-6 | 설치 파일 | Forgejo feed만. 저장소에 추적된 바이너리 사본 삭제 | — |
| W3-7 | CI | Forgejo(`.forgejo/workflows`) 한 벌. `.github`는 GitHub 원격이 없어 실행된 적이 없으므로 필요한 잡만 옮기고 삭제 | — |
| W3-8 | 사이트 배포 | Cloudflare Pages 한 경로. GitHub Pages·NAS wwwroot 복사 경로 삭제 | — |
DRY 원칙 보강: Deno처럼 정본을 직접 import할 수 없는 런타임은 **생성 사본 + `--check` 게이트**로만 복제를 허용한다(`version:sync` 패턴과 같음). 손으로 옮겨 적은 사본은 금지.

View file

@ -0,0 +1,63 @@
# Refactor Wave 3 — 표면·계약 단일화 (2026-09-26)
사용자 지시: "두 벌씩 만든 것을 전부 하나로 합쳐 정리." 정책은 `docs/REFACTOR_POLICY.md` Wave 3 절(W3-1~W3-8).
## 커밋
| 커밋 | WS | 내용 |
|---|---|---|
| `e689683` | 선행 | 랜딩 사이트를 코드로 확인한 사실 기준으로 재작성(접근성·10개 언어) |
| `88f24d8` | WS-A | 가격·쿼터·공개 URL 정본 `packages/core` 두 파일 + Deno 생성 사본(`contract:check`) |
| `b6fe588` | WS-B | 웹앱 `/app` basePath·standalone·NAS 컨테이너, 모든 결제 진입을 `billingUrl()`로, 브리지 워커 `/app` 라우팅 |
| `cd9d199` | WS-C | wwwroot·apps/web·바이너리 중복 삭제(바이너리 19개 ≈ 568 MiB), API 정적 서빙 제거 |
| `dc43884` | WS-D | `.github` 삭제, Forgejo로 CI·payple-renew 이식, 사이트 배포 워크플로 하나로 |
| `9297860` | 사이트 | 히어로 반복 예시(앱 녹음 캡슐 재현), 404.html, 법률 페이지 색 |
| (이 커밋) | 문서 | 보고서, 백로그, NAS compose 드리프트 병합(whisper) |
## 정본 지도 (전 → 후)
| 개념 | 전 | 후 |
|---|---|---|
| 요금 | Payple 9,900/29,900 · admin ₩12,900/₩24,900·$9.9/$19.9 · 사이트 2,900/8,900 | `plan-catalog.ts` 하나(2,900/8,900) → Deno 생성 사본 |
| 쿼터 | core `PREMIUM_MODEL_LIMITS` · Deno `quota.ts` · desktop `QUOTA_LIMITS` · 사이트 | `PLAN_QUOTA` 하나에서 파생 |
| 결제 URL | `d3ro.dev/billing`(해석 불가) · `d3ro.chanpaca.net/billing`(랜딩으로 폴백) | `billingUrl()` → `/app/billing` |
| 랜딩·다운로드·법률·초대·assetlinks | site · api-server wwwroot · apps/web | site 한 벌 |
| 설치 파일 | 저장소 3곳 추적 + Forgejo feed | Forgejo feed만 |
| CI | `.github`(한 번도 실행 안 됨) · `.forgejo` 2벌 사이트 배포 · GitLab | Forgejo(+ Android 서명 릴리스는 GitLab) |
| 사이트 배포 | Cloudflare Pages · GitHub Pages · NAS wwwroot 복사 | Cloudflare Pages |
| NAS compose | 운영본(whisper 포함) ≠ 저장소 | 저장소에 whisper 병합(교체는 GAP-OPS-01) |
## 운영 반영
- Supabase Edge Functions: `payple-checkout/renew/webhook/manage` v9, `billing-catalog` v5 — 청구 금액 ₩2,900/₩8,900.
새 CLI는 `functions/deno.json` 임포트 맵을 자동으로 쓰지 않아 `--import-map supabase/functions/deno.json`이 필요했다.
- Cloudflare Pages `d3ro`: 커밋 `9297860` 게시(`release-identity.json` 확인), 없는 경로 404.
- NAS: `d3ro_voice_web`(3002) 기동, `/app/login` 200. 공개는 터널 호스트 추가 대기(GAP-WEB-01).
## 검증
- `npm run typecheck`(모노레포) EXIT 0 · 변경 파일 eslint errors 0.
- `contract:check`·`version:check`·`release:metadata:test`(음성 14)·`release:mobile:boundary:test`·`security:secrets`·assetlinks GREEN.
- Deno `_shared/`+`payple-webhook/` 81/81. core vitest 131/131. api-server `dotnet test` 28/28.
- 데스크톱 vitest 1520/1522 — 실패 2건은 기록된 환경 의존 건(캡션 타임아웃, 임베딩 서버 부재).
**Node 23으로 돌리면 better-sqlite3 ABI 불일치(130/131)로 365건이 가짜 실패한다. Electron 런타임으로 실행해야 한다**:
`ELECTRON_RUN_AS_NODE=1 ../../node_modules/electron/dist/electron.exe ../../node_modules/vitest/vitest.mjs run --config vitest.config.ts`.
- web·admin 프로덕션 빌드 GREEN, web 이미지 로컬 스모크(`/app`→login, `/app/download`→사이트 308).
- 사이트: 10개 언어 × 320/390/1440 가로 넘침 0, axe(wcag2a·aa·21aa·22aa·best-practice) 위반 0, 감소 모션·일시정지 확인.
## SKIP / 남은 일
- 외부 설정(사용자): 터널 공개 호스트 + `WEB_APP_ORIGIN`, Supabase Redirect URLs `/app/**`, Forgejo 시크릿 `SUPABASE_URL`·`CRON_SECRET`,
Payple 클라이언트 키, Stripe·Play 가격, NAS `.env` 누락 변수 → `docs/map/11-gap-backlog.md` GAP-BILL-02, GAP-WEB-01, GAP-OPS-01.
- `packages/i18n`(12개 언어 JSON)과 `site/src/i18n`(10개 언어 TS)은 체계가 다르다. 사이트는 정적 번들 크기·독립 배포 때문에 유지 — 다음 wave 후보.
- 티어 타입(`'pro' | 'pro_plus'`) 정의가 core·api-client·Deno에 여러 벌 남아 있다 — 다음 wave 후보.
- 캡슐 CSS 값은 사이트가 앱 style.css를 옮겨 적었다(Electron 팝업 CSS 직접 import 불가). `design.md`에 두 곳 동시 수정 규칙을 적었다.
## 메타 학습
1. **"안 돌던 것"도 중복이다.** `.github`는 원격이 없어 실행된 적이 없었고, 그 안의 Payple 갱신 cron 때문에 정기 결제가 멈춰 있었다.
정리 전에 "이 파일은 실제로 실행되는가"를 원격·러너 기준으로 확인한다.
2. **운영 서버의 사본도 중복이다.** NAS compose가 저장소와 달라져 있었다(서비스 추가, 필수 변수 누락). 배포 전 운영본과 diff한다.
3. **Windows에서 Python 텍스트 쓰기는 CRLF를 만든다.** `version:check`가 바이트 비교라 깨졌다. `newline=''`로 쓰거나 Edit 도구를 쓴다.
비 raw 문자열의 `\\b`는 백스페이스 문자가 될 수 있다 — 정규식 편집은 Edit 도구로.
4. **Tailwind 내장 유틸리티와 이름이 같은 사용자 클래스는 무력화된다**(`break-keep`). 프로젝트 전용 이름을 쓴다(`keep-ko`).

View file

@ -45,10 +45,12 @@ authoritative release commit
- `publish-forgejo-release.mjs`는 설치 자산을 먼저, `latest.yml`을 마지막에 - `publish-forgejo-release.mjs`는 설치 자산을 먼저, `latest.yml`을 마지막에
게시하고, 공개 URL에서 재검증한 뒤 Release 자산을 첨부한다. 게시하고, 공개 URL에서 재검증한 뒤 Release 자산을 첨부한다.
- 설치 바이너리는 저장소에 커밋하지 않는다. 배포 정본은 Forgejo feed이며, - 설치 바이너리는 저장소에 커밋하지 않는다. 배포 정본은 Forgejo feed이며,
`site/public/releases/*/`, `apps/web/public/releases/*/`, `site/public/releases/*/`, `apps/web/public/releases/*/`는 `.gitignore`로
`apps/api-server/wwwroot/releases/*/`는 build graph 밖이라 배포 산출물에 막혀 있어 build graph 밖이고 배포 산출물에 포함되지 않는다(추적 상태로 남아
포함되지 않는다. 사이트·웹 다운로드 센터는 로컬 경로가 아니라 feed URL을 있던 19개 설치본 ~568MiB는 2026-09-26 Wave 3, cd9d199에서 삭제됐고 그 뒤로는
링크한다. (역사적 `1.0.0` 자산만 추적 상태로 남아 있다.) 추적 자산이 없다). `apps/api-server/wwwroot/`는 정적 파일 서빙 자체와 함께
통째로 삭제됐다(같은 커밋) — API는 더 이상 다운로드/릴리스 자산을 서빙하지
않는다. 사이트·웹 다운로드 센터는 로컬 경로가 아니라 feed URL을 링크한다.
- `scripts/ci/sync-version.mjs --check --tag v1.3.0`는 태그, `release/product-version.json`, package/lockfile, Android/iOS 버전 면의 일치를 fail-closed로 검증한다. - `scripts/ci/sync-version.mjs --check --tag v1.3.0`는 태그, `release/product-version.json`, package/lockfile, Android/iOS 버전 면의 일치를 fail-closed로 검증한다.
- `scripts/ci/verify-release-metadata.mjs`는 배포 메타데이터와 CI/publisher 계약을 검증한다. - `scripts/ci/verify-release-metadata.mjs`는 배포 메타데이터와 CI/publisher 계약을 검증한다.

View file

@ -11,7 +11,7 @@
D:/workspace/D3ROVoice D:/workspace/D3ROVoice
├── apps/ ├── apps/
│ ├── desktop/ Electron app (npm workspace @d3ro/desktop) │ ├── desktop/ Electron app (npm workspace @d3ro/desktop)
│ ├── web/ Next.js console (npm workspace @d3ro/web) │ ├── web/ Next.js console (npm workspace @d3ro/web); basePath `/app`, served at `d3ro.chanpaca.net/app`
│ ├── admin/ Next.js back office (npm workspace @d3ro/admin) │ ├── admin/ Next.js back office (npm workspace @d3ro/admin)
│ ├── mobile-rn/ React Native product mobile app (NOT an npm workspace) │ ├── mobile-rn/ React Native product mobile app (NOT an npm workspace)
│ ├── api-server/ ASP.NET Core 10 API (D3ROVoice.Api) │ ├── api-server/ ASP.NET Core 10 API (D3ROVoice.Api)
@ -25,8 +25,9 @@ D:/workspace/D3ROVoice
│ └── api-client/ @d3ro/api-client — Supabase wrapper + shared types │ └── api-client/ @d3ro/api-client — Supabase wrapper + shared types
├── server/ ├── server/
│ ├── supabase/ Supabase project: config.toml, migrations/, functions/, tests/ │ ├── supabase/ Supabase project: config.toml, migrations/, functions/, tests/
│ └── cloudflare-worker/ Edge gateway (wrangler.toml + src/index.ts) │ ├── cloudflare-worker/ Edge gateway (wrangler.toml + src/index.ts)
├── site/ Vite landing site (deployed to Cloudflare Pages + GitHub Pages) │ └── cloudflare-site-bridge/ Site bridge worker: forwards `/app/*` to WEB_APP_ORIGIN, rewrites upstream redirects, else proxies to Pages
├── site/ Vite landing site (deployed to Cloudflare Pages; GitHub Pages removed 2026-09-26, Wave 3)
├── resources/ icons/ (empty), sox/ (bundled Windows SoX binaries) ├── resources/ icons/ (empty), sox/ (bundled Windows SoX binaries)
├── release/ Version identity SSOT + license/evidence public keys ├── release/ Version identity SSOT + license/evidence public keys
├── scripts/ ~145 automation scripts + scripts/ci/ (31) + scripts/lib/ ├── scripts/ ~145 automation scripts + scripts/ci/ (31) + scripts/lib/
@ -35,11 +36,10 @@ D:/workspace/D3ROVoice
├── tests/e2e/, test-results/ Root-level e2e + last-run artifacts ├── tests/e2e/, test-results/ Root-level e2e + last-run artifacts
├── scratch/ Large local evidence (APKs, screenshots, DBs) — not build input ├── scratch/ Large local evidence (APKs, screenshots, DBs) — not build input
├── supabase/ Empty scaffolding (.branches/, snippets/) — real project is server/supabase ├── supabase/ Empty scaffolding (.branches/, snippets/) — real project is server/supabase
├── .github/workflows/ GitHub Actions (6)
├── .gitlab-ci.yml GitLab CI (primary desktop/mobile release pipeline) ├── .gitlab-ci.yml GitLab CI (primary desktop/mobile release pipeline)
├── .forgejo/workflows/ Forgejo Actions (site deploy to Cloudflare Pages) ├── .forgejo/workflows/ Forgejo Actions (CI, site deploy to Cloudflare Pages, payple-renew cron, portable/release)
├── docker-compose.yml, docker-compose.nas.yml ├── docker-compose.yml, docker-compose.nas.yml
├── Dockerfile.admin, apps/api-server/Dockerfile, apps/admin/Dockerfile ├── apps/api-server/Dockerfile, apps/admin/Dockerfile, apps/web/Dockerfile
├── turbo.json, tsconfig.json, tsconfig.base.json, pnpm-workspace.yaml ├── turbo.json, tsconfig.json, tsconfig.base.json, pnpm-workspace.yaml
├── package.json monorepo root, npm workspaces ├── package.json monorepo root, npm workspaces
├── CLAUDE.md Claude-specific project rules ├── CLAUDE.md Claude-specific project rules
@ -126,25 +126,22 @@ See [`03-shared-packages.md`](./03-shared-packages.md). Summary:
## 5. CI/CD ## 5. CI/CD
### GitHub Actions (`.github/workflows/`) > `.github/workflows/` was deleted 2026-09-26 (Wave 3, dc43884): the repository has Forgejo and GitLab
> remotes but no GitHub remote, so nothing under it ever ran — including the daily Payple renewal cron.
| Workflow | Purpose | > CI now runs only on Forgejo Actions (`.forgejo/workflows/`); see below.
|---|---|
| `ci.yml` | Main CI: `code-quality` (secret scan, mobile release/config/build-config self-tests, Play asset contract, lint, typecheck), `api-server-tests`, `edge-functions-quality` (Deno), `test-matrix` (win/mac/ubuntu vitest), `build-validation` (desktop win, admin ubuntu), `mobile-android` (debug/CSPRNG/E2E APKs + verifiers), `mobile-emulator-e2e` (API 35 + Maestro 2.7.0) |
| `release.yml` | On tag `v*.*.*`: preflight → `package-windows` (NSIS) → `package-macos` (DMG/ZIP arm64) → `package-android` (signed APK/AAB + evidence) → `package-admin-docker` (GHCR) → `publish-release` (checksums + GitHub Release + Forgejo canonical publish) |
| `deploy-site.yml` | On `site/**`: build Vite site, boundary self-test, deploy GitHub Pages |
| `build-mac.yml` | Manual macOS build (arm64/x64), sox + PyInstaller sidecar + electron-rebuild |
| `payple-renew.yml` | Daily cron → `payple-renew` edge function |
| `release-signing-ca.yml` | Manual Windows (Azure Trusted Signing) / macOS notarize build+sign |
### GitLab CI (`.gitlab-ci.yml`) ### GitLab CI (`.gitlab-ci.yml`)
Stages `validate → test → build → e2e → package → publish → deploy`. Primary pipeline for desktop Windows/macOS releases (Forgejo Generic Registry is the canonical updater feed; GitLab project 1172 is a legacy mirror) and production mobile releases (`mobile-production-release`, manual/protected). Admin NAS deploy job is intentionally **disabled**. `package-windows`/`package-macos` build the faster-whisper sidecar (`sidecar:setup` → `sidecar:build`) and run `scripts/ci/verify-sidecar-bundle.mjs` before electron-builder, so a release can never ship without the local STT engine. Every pipeline that runs `npm run build --workspace=@d3ro/desktop` (`.forgejo` release/portable, `.github` CI/release) then runs `scripts/ci/verify-desktop-renderer-bundles.mjs`, which fails packaging when a renderer page references an asset the build did not emit (GAP-INFRA-05). Stages `validate → test → build → e2e → package → publish → deploy`. Primary pipeline for desktop Windows/macOS releases (Forgejo Generic Registry is the canonical updater feed; GitLab project 1172 is a legacy mirror) and production mobile releases (`mobile-production-release`, manual/protected). Admin NAS deploy job is intentionally **disabled**. `package-windows`/`package-macos` build the faster-whisper sidecar (`sidecar:setup` → `sidecar:build`) and run `scripts/ci/verify-sidecar-bundle.mjs` before electron-builder, so a release can never ship without the local STT engine. Every pipeline that runs `npm run build --workspace=@d3ro/desktop` (`.forgejo` CI/release/portable) then runs `scripts/ci/verify-desktop-renderer-bundles.mjs`, which fails packaging when a renderer page references an asset the build did not emit (GAP-INFRA-05).
### Forgejo Actions (`.forgejo/workflows/`) ### Forgejo Actions (`.forgejo/workflows/`)
`ci.yml` — CI is now Forgejo-only (`.github/workflows` deleted 2026-09-26, Wave 3, dc43884). Jobs: `quality` (`version:check`, `contract:check`, `release:metadata:test`, secret scan, mobile release/config/build-config self-tests, Play asset contract, lint, typecheck, Vitest), `build-validation`, `mobile-quality`, `edge-functions` (Deno), `api-server-tests`.
`payple-renew.yml` — daily cron (+ manual dispatch) → `payple-renew` edge function; ported from the never-run `.github/workflows/payple-renew.yml` because the repository has no GitHub remote. Requires Forgejo secrets `SUPABASE_URL`, `CRON_SECRET`.
`portable.yml` — 태그/수동 실행으로 **서명 없이** portable 채널(95MiB 7z 분할 볼륨 + Scoop 매니페스트 + 설치 스크립트)을 게시한다. `WIN_CSC_*` 불필요, updater feed는 건드리지 않는다. `portable.yml` — 태그/수동 실행으로 **서명 없이** portable 채널(95MiB 7z 분할 볼륨 + Scoop 매니페스트 + 설치 스크립트)을 게시한다. `WIN_CSC_*` 불필요, updater feed는 건드리지 않는다.
`deploy-site.yml` / `deploy-site-windows.yml` — build `site`, write release identity, deploy to Cloudflare Pages `d3ro` (`d3ro.pages.dev`), verify live commit/version, app-links, legal URLs. 커스텀 도메인 `d3ro.chanpaca.net` 은 Pages 커스텀 도메인이 DNS CNAME을 요구하므로, DNS를 건드릴 수 없는 동안은 Workers 라우트 브리지 `server/cloudflare-site-bridge/`(`d3ro.chanpaca.net/*` → Pages 프록시, 수동 `npx wrangler deploy`)가 서빙한다. CNAME을 추가한 뒤 브리지를 삭제하면 Pages 커스텀 도메인으로 직접 서빙된다(GAP-REL-09b). `deploy-site.yml` — 유일한 사이트 배포 경로(Linux, Cloudflare Pages). Windows 수동 중복 워크플로(`deploy-site-windows.yml`)는 2026-09-26 삭제됐다(dc43884). build `site`, write release identity, deploy to Cloudflare Pages `d3ro` (`d3ro.pages.dev`), verify live commit/version, app-links, legal URLs. 커스텀 도메인 `d3ro.chanpaca.net` 은 Pages 커스텀 도메인이 DNS CNAME을 요구하므로, DNS를 건드릴 수 없는 동안은 Workers 라우트 브리지 `server/cloudflare-site-bridge/`(`d3ro.chanpaca.net/*` → Pages 프록시, 수동 `npx wrangler deploy`)가 서빙한다. CNAME을 추가한 뒤 브리지를 삭제하면 Pages 커스텀 도메인으로 직접 서빙된다(GAP-REL-09b).
`release.yml` — tag-triggered Windows build (signed) + `publish-forgejo-release.mjs` to the canonical Forgejo feed/release hub. `release.yml` — tag-triggered Windows build (signed) + `publish-forgejo-release.mjs` to the canonical Forgejo feed/release hub.
--- ---
@ -153,15 +150,15 @@ Stages `validate → test → build → e2e → package → publish → deploy`.
| File | Purpose | | File | Purpose |
|---|---| |---|---|
| `Dockerfile.admin` | 3-stage Next.js admin build (node 24.19.0-alpine, port 3001) | | `apps/admin/Dockerfile` | Next.js standalone runner for `.next/standalone` (the old root `Dockerfile.admin` 3-stage build was a duplicate, deleted 2026-09-26, Wave 3, cd9d199) |
| `apps/admin/Dockerfile` | Next.js standalone runner for `.next/standalone` | | `apps/api-server/Dockerfile` | Multi-stage .NET 10 (sdk → aspnet runtime), port 5000, `VOLUME /app/data`; no longer serves static files (`wwwroot` removed — see `07-api-server.md`) |
| `apps/api-server/Dockerfile` | Multi-stage .NET 10 (sdk → aspnet runtime), port 5000, `VOLUME /app/data` | | `apps/web/Dockerfile` | Next.js standalone runner for `@d3ro/web`, basePath `/app`, port 3002 (added 2026-09-26, Wave 3, b6fe588) |
| `docker-compose.yml` | Dev/self-host: `d3ro-api-server` (5050→5000, `./data` volume), `d3ro-admin` (3001), optional `ollama` (profile `ai`, 11434) | | `docker-compose.yml` | Dev/self-host: `d3ro-api-server` (5050→5000, `./data` volume), `d3ro-admin` (3001), `d3ro-web` (3002, `@d3ro/web` basePath `/app`, Supabase/Payple keys as build args), optional `ollama` (profile `ai`, 11434) |
| `docker-compose.nas.yml` | NAS: prebuilt `d3ro-voice-api:latest` + `d3ro-voice-admin:latest`; API mounts `/volume1/docker/d3ro/wwwroot/{privacy,terms,delete-account,legal.css}` read-only | | `docker-compose.nas.yml` | NAS: prebuilt `d3ro-voice-api:latest` / `d3ro-voice-admin:latest` / `d3ro-voice-web:latest` (3002) / `d3ro-whisper` (faster-whisper, OpenAI-compatible). No legal-doc `wwwroot` mount and no `NEXT_PUBLIC_API_URL` — both were dropped 2026-09-26 (Wave 3, b6fe588); legal pages are served only from `site/public`. |
Deploy scripts: `scripts/deploy-nas.ps1`, `scripts/deploy-nas.sh`, `scripts/deploy-site-to-nas.js`, `scripts/nas-control.sh` (start/stop/restart/status/logs/backup/update). Deploy scripts: `scripts/deploy-nas.ps1`, `scripts/deploy-nas.sh`, `scripts/nas-control.sh` (start/stop/restart/status/logs/backup/update). `scripts/deploy-site-to-nas.js` was deleted 2026-09-26 (Wave 3, cd9d199) — site deploy is Cloudflare Pages only (`.forgejo/workflows/deploy-site.yml`).
Public endpoints (production): `https://d3ro.chanpaca.net` — **랜딩/다운로드 센터**(2026-09-19부터 Pages `d3ro` 배포본을 Workers 라우트 브리지가 서빙; 그 이전에는 바인딩이 없어 빈 404였다), `https://admin.chanpaca.net` (admin CRM). Edge: `server/cloudflare-worker` proxying to the NAS origin, plus a **Cron Trigger** (`* * * * *`) that drains the Supabase push outbox via `send-push?mode=drain` (`src/push-drain.ts`; needs `SUPABASE_URL` var + `SUPABASE_SERVICE_ROLE_KEY` secret). Tunnel: Cloudflare Tunnel `kd-nas` (NAS 포털/API는 현재 이 호스트네임에 바인딩되어 있지 않다). Public endpoints (production): `https://d3ro.chanpaca.net` — **랜딩/다운로드 센터**(site/, Pages `d3ro` 배포본을 Workers 라우트 브리지가 서빙), `https://d3ro.chanpaca.net/app` — **웹앱**(`apps/web`, basePath `/app`; 사이트 브리지 워커가 `WEB_APP_ORIGIN`(터널 호스트)으로 전달하며, 호스트가 비어 있으면 `/app`은 503을 반환한다 — 터널 호스트 설정 대기), `https://admin.chanpaca.net` (admin CRM). Edge: `server/cloudflare-worker` proxying to the NAS origin, plus a **Cron Trigger** (`* * * * *`) that drains the Supabase push outbox via `send-push?mode=drain` (`src/push-drain.ts`; needs `SUPABASE_URL` var + `SUPABASE_SERVICE_ROLE_KEY` secret). Tunnel: Cloudflare Tunnel `kd-nas` (NAS 포털/API는 현재 이 호스트네임에 바인딩되어 있지 않다).
--- ---
@ -178,8 +175,8 @@ Full detail: [`09-supabase-backend.md`](./09-supabase-backend.md).
## 8. `scripts/` groups ## 8. `scripts/` groups
- **CI (`scripts/ci/`, 33 files):** build/version/release (`build-all`, `sync-version`, `generate-checksums`, `verify-release-metadata`, `create-release-tag`, `extract-release-notes`), security (`check-no-hardcoded-secrets`), mobile release gates (`verify-mobile-release-boundary/-config/-build-config`, `verify-android-artifact/-app-links`, `verify-play-store-assets`, `prepare-whisper-model`, `create-mobile-release-evidence`, `prepare-mobile-release-publication`, emulator/CSPRNG gates), keys (`create-desktop-license-keypair`, `create-release-evidence-key`, etc.), publish (`publish-forgejo-release` canonical, `publish-gitlab-release` mirror; legacy `sync-and-publish-forgejo-release`, `upload-asset-to-forgejo-release`), env/tooling (`bootstrap-linux-toolchain.sh`, `audit-nas-stt-config.ps1`, mobile local E2E scripts). - **CI (`scripts/ci/`, 33 files):** build/version/release (`build-all`, `sync-version`, `generate-checksums`, `verify-release-metadata`, `create-release-tag`, `extract-release-notes`), security (`check-no-hardcoded-secrets`), mobile release gates (`verify-mobile-release-boundary/-config/-build-config`, `verify-android-artifact/-app-links`, `verify-play-store-assets`, `prepare-whisper-model`, `create-mobile-release-evidence`, `prepare-mobile-release-publication`, emulator/CSPRNG gates), keys (`create-desktop-license-keypair`, `create-release-evidence-key`, etc.), publish (`publish-forgejo-release` canonical, `publish-gitlab-release` mirror; the legacy `sync-and-publish-forgejo-release`/`upload-asset-to-forgejo-release` scripts were deleted 2026-09-26, Wave 3, cd9d199), env/tooling (`bootstrap-linux-toolchain.sh`, `audit-nas-stt-config.ps1`, mobile local E2E scripts).
- **Deploy/release:** `deploy-nas.ps1/.sh`, `deploy-site-to-nas.js`, `nas-control.sh`, `publish-gh.ps1`, `gen-keystore.js`. - **Deploy/release:** `deploy-nas.ps1/.sh`, `nas-control.sh`, `publish-gh.ps1`, `gen-keystore.js` (`deploy-site-to-nas.js` deleted 2026-09-26, Wave 3, cd9d199 — site deploy is Cloudflare Pages only).
- **GCP/Google OAuth automation + inspection (~70 `*.mjs`):** `auto-configure-oauth`, `automate-google-oauth`, `setup-consent`, `create-*-client`, `check-*`, `inspect-*` — mostly one-off/browser-driven console automation. - **GCP/Google OAuth automation + inspection (~70 `*.mjs`):** `auto-configure-oauth`, `automate-google-oauth`, `setup-consent`, `create-*-client`, `check-*`, `inspect-*` — mostly one-off/browser-driven console automation.
- **AdMob console automation:** `admob-probe.mjs` (read-only login/app/ad-unit probe), `admob-login.mjs` + `run-admob-login.bat` (one interactive headful Chrome login into a persistent profile), `admob-automate.mjs` (dry-run by default; `--apply` creates/verifies banner+rewarded units and reports Play-store link). Uses `playwright` with `channel: 'chrome'` and the gitignored `.chrome-playwright-profile`. - **AdMob console automation:** `admob-probe.mjs` (read-only login/app/ad-unit probe), `admob-login.mjs` + `run-admob-login.bat` (one interactive headful Chrome login into a persistent profile), `admob-automate.mjs` (dry-run by default; `--apply` creates/verifies banner+rewarded units and reports Play-store link). Uses `playwright` with `channel: 'chrome'` and the gitignored `.chrome-playwright-profile`.
- **E2E / verification:** `e2e-desktop-*.js`, `real-app-multi-tab-e2e.js`, `test-and-capture-all-10-ad-services.js`, `verify-live-production-d3ro.js`. - **E2E / verification:** `e2e-desktop-*.js`, `real-app-multi-tab-e2e.js`, `test-and-capture-all-10-ad-services.js`, `verify-live-production-d3ro.js`.
@ -201,7 +198,7 @@ Full detail: [`09-supabase-backend.md`](./09-supabase-backend.md).
| `apps/desktop/electron-builder.yml` | appId `com.d3ro.voice`, NSIS x64 (forced code signing), macOS DMG/ZIP arm64, generic Forgejo publish feed, asarUnpack native modules + `@ffmpeg-installer`, extraResources (icons, sounds, sox, **sidecar**, ffmpeg, ollama) | | `apps/desktop/electron-builder.yml` | appId `com.d3ro.voice`, NSIS x64 (forced code signing), macOS DMG/ZIP arm64, generic Forgejo publish feed, asarUnpack native modules + `@ffmpeg-installer`, extraResources (icons, sounds, sox, **sidecar**, ffmpeg, ollama) |
| `apps/desktop/src/main/update-feed.ts` | Auto-update feed SSOT (canonical Forgejo + legacy GitLab mirror, channels) | | `apps/desktop/src/main/update-feed.ts` | Auto-update feed SSOT (canonical Forgejo + legacy GitLab mirror, channels) |
| `release/update-policy.json` | Update policy SSOT (channels, minimum supported version, forced update, delta/full, staged rollout, kill switch) | | `release/update-policy.json` | Update policy SSOT (channels, minimum supported version, forced update, delta/full, staged rollout, kill switch) |
| `apps/web/src/lib/desktop-release.ts`, `site/src/release.ts` | Download-center desktop release contract (installer filename + release date); version and date are kept on the SSOT by `npm run version:sync` (drifted to 1.2.0 once — GAP-REL-08) | | `site/src/release.ts` | Download-center desktop release contract (installer filename + release date); version and date are kept on the SSOT by `npm run version:sync` (drifted to 1.2.0 once — GAP-REL-08). `apps/web/src/lib/desktop-release.ts` and the `apps/web` download/releases pages were deleted 2026-09-26 (Wave 3, cd9d199) — `site/` is the only download surface, and `/download`/`/releases` on `apps/web` now redirect to it |
| `apps/desktop/src/main/update-policy.ts` | Policy parsing/decision logic | | `apps/desktop/src/main/update-policy.ts` | Policy parsing/decision logic |
| `scripts/ci/publish-forgejo-release.mjs` | Canonical Forgejo registry + Release + feed publisher | | `scripts/ci/publish-forgejo-release.mjs` | Canonical Forgejo registry + Release + feed publisher |
@ -227,5 +224,5 @@ Version sync is enforced by `scripts/ci/sync-version.mjs` and `verify-release-me
See [`11-gap-backlog.md`](./11-gap-backlog.md) for the maintained list (`INFRA-*`). Headlines: See [`11-gap-backlog.md`](./11-gap-backlog.md) for the maintained list (`INFRA-*`). Headlines:
- `apps/mobile-rn` is not an npm workspace member; use `typecheck:mobile`/`lint:mobile`/`test:mobile` or `verify:all`. - `apps/mobile-rn` is not an npm workspace member; use `typecheck:mobile`/`lint:mobile`/`test:mobile` or `verify:all`.
- Admin NAS deploy job disabled in GitLab CI; admin ships via GitHub/GHCR + manual NAS compose. - Admin NAS deploy job disabled in GitLab CI; the `package-admin-docker`→GHCR publish step lived only in the now-deleted `.github/workflows/release.yml` (2026-09-26, dc43884) and was not ported to Forgejo, so admin image publish + NAS compose update is manual (`GAP-INFRA-02`).
- Two identity systems (.NET JWT/SQLite vs Supabase); a canonical resolver now exists in `@d3ro/core/entitlement` but web/mobile/.NET adoption is incremental (`11` GAP-ID-02). - Two identity systems (.NET JWT/SQLite vs Supabase); a canonical resolver now exists in `@d3ro/core/entitlement` but web/mobile/.NET adoption is incremental (`11` GAP-ID-02).

View file

@ -13,9 +13,8 @@
- **Policies:** `ManagerOrAbove` (manager/admin/superadmin), `AdminOrAbove` (admin/superadmin), `SuperAdminOnly` (superadmin). Role normalization strips `_`/`-`, lowercases. - **Policies:** `ManagerOrAbove` (manager/admin/superadmin), `AdminOrAbove` (admin/superadmin), `SuperAdminOnly` (superadmin). Role normalization strips `_`/`-`, lowercases.
- **CORS/Hosts:** strict origin validation (`CORS_ALLOWED_ORIGINS`), `ALLOWED_HOSTS` required outside Development. - **CORS/Hosts:** strict origin validation (`CORS_ALLOWED_ORIGINS`), `ALLOWED_HOSTS` required outside Development.
- **Startup DB init:** `EnsureCreated()`, raw `CREATE TABLE IF NOT EXISTS` for admin operation/audit tables, legacy SHA-256 password lockdown (`IsActive=false`, `Role="LegacyDisabled"`), idempotent env admin provisioning (`ADMIN_EMAIL`/`ADMIN_PASSWORD`, only when no active user), default LLM/STT endpoints seeded. - **Startup DB init:** `EnsureCreated()`, raw `CREATE TABLE IF NOT EXISTS` for admin operation/audit tables, legacy SHA-256 password lockdown (`IsActive=false`, `Role="LegacyDisabled"`), idempotent env admin provisioning (`ADMIN_EMAIL`/`ADMIN_PASSWORD`, only when no active user), default LLM/STT endpoints seeded.
- **Middleware order:** Swagger (dev) → CORS → invite-page hardening (CSP/no-store) → default files → mobile/legacy asset block (404 for `.apk`/`.aab`/signed zips + legacy bundles) → static files → rate limiter → authentication → authorization. - **Middleware order:** Swagger (dev) → CORS → rate limiter → authentication → authorization. The API no longer serves static files: `wwwroot` (stale site build, download/invite pages, `.well-known` copy, legacy embedded admin SPA, 1.0.0 binaries) was deleted 2026-09-26 (Wave 3, cd9d199), and `UseStaticFiles`/the mobile-asset-block fallbacks were removed from `Program.cs` along with it. The Next.js admin (`apps/admin`) is the only admin UI.
- **Health:** `GET /health`, `GET /api/health` → `{status, service, version, uptimeSeconds, database, timestamp}`. - **Health:** `GET /health`, `GET /api/health` → `{status, service, version, uptimeSeconds, database, timestamp}`.
- **Fallbacks:** `/accept-invite` → `accept-invite.html`; `/admin/{*path}` → legacy embedded admin SPA in `wwwroot/admin`.
--- ---
@ -102,6 +101,6 @@ DTOs (`Dtos/Dtos.cs`): auth, license audit, LLM, admin/model endpoints, STT (tra
- Known intentional states (not bugs): - Known intentional states (not bugs):
- Legacy SHA-256 users force-disabled. - Legacy SHA-256 users force-disabled.
- No hardcoded/seeded admin credentials. - No hardcoded/seeded admin credentials.
- Mobile/legacy release assets blocked with 404. - No static file serving — `wwwroot` and its mobile/legacy release asset block were deleted 2026-09-26 (Wave 3, cd9d199); downloads/releases are served only from `site/`.
- `LlmProxyService` Mock fallback. - `LlmProxyService` Mock fallback.
- This backend holds a **separate identity** from Supabase; see `11-gap-backlog.md` `ID-01`. - This backend holds a **separate identity** from Supabase; see `11-gap-backlog.md` `ID-01`.

View file

@ -88,4 +88,4 @@ Root files: `instrumentation.ts` (startup security validation), `proxy.ts` (edge
- `admin-payments` Payple live history → `501`. - `admin-payments` Payple live history → `501`.
- `UnavailableAdminPanel` whenever Supabase env absent; writes disabled, no sample metrics. - `UnavailableAdminPanel` whenever Supabase env absent; writes disabled, no sample metrics.
- `/pipelines` and dashboard node/error sections render only measured data. - `/pipelines` and dashboard node/error sections render only measured data.
- Deploy: `Dockerfile.admin` / `apps/admin/Dockerfile` → GHCR + NAS compose; GitLab admin NAS deploy job disabled. - Deploy: `apps/admin/Dockerfile` → GHCR + NAS compose; the duplicate root `Dockerfile.admin` was deleted 2026-09-26 (Wave 3, cd9d199) since `apps/admin/Dockerfile` is the one actually used. GitLab admin NAS deploy job disabled.

View file

@ -47,7 +47,7 @@ Migration numbering referenced in SSOT goes up to `00028`; CI verifies `migratio
| `send-push` | Push delivery | | `send-push` | Push delivery |
| `account-delete` | Account deletion cascade + provider unlink | | `account-delete` | Account deletion cascade + provider unlink |
| `admin-users` / `admin-subscriptions` / `admin-payments` / `admin-audit-log` | Admin operations | | `admin-users` / `admin-subscriptions` / `admin-payments` / `admin-audit-log` | Admin operations |
| `billing-catalog` | Server pricing catalog | | `billing-catalog` | Server pricing catalog. Prices/quotas read `PLAN_PRICE_KRW`/`PLAN_QUOTA` from `functions/_shared/core-contract.generated.ts`, generated from `packages/core/src/plan-catalog.ts` by `scripts/ci/sync-core-contract.mjs` (`npm run contract:check` fails on drift; added 2026-09-26, Wave 3, 88f24d8) |
| `stripe-checkout` / `stripe-portal` / `stripe-webhook` | Stripe billing | | `stripe-checkout` / `stripe-portal` / `stripe-webhook` | Stripe billing |
| `payple-checkout` / `payple-manage` / `payple-renew` / `payple-webhook` | Payple billing (Korea) | | `payple-checkout` / `payple-manage` / `payple-renew` / `payple-webhook` | Payple billing (Korea) |
| `iap-verify` | Google Play / App Store purchase verification | | `iap-verify` | Google Play / App Store purchase verification |

View file

@ -199,7 +199,7 @@ end-to-end behaviour has **not been verified by typing in a real app** (`11` GAP
| SHELL-08 | Auto-update | [x] | [-] | [!] | [-] | Desktop electron-updater; Forgejo canonical feed; channels + mandatory/major-vs-delta policy (`release/update-policy.json`); mobile store updates | | SHELL-08 | Auto-update | [x] | [-] | [!] | [-] | Desktop electron-updater; Forgejo canonical feed; channels + mandatory/major-vs-delta policy (`release/update-policy.json`); mobile store updates |
| SHELL-09 | Support / diagnostics | [x] | [ ] | [ ] | [~] | Desktop `SupportModal`; admin `/support` stub | | SHELL-09 | Support / diagnostics | [x] | [ ] | [ ] | [~] | Desktop `SupportModal`; admin `/support` stub |
| SHELL-10 | Download center / releases | [-] | [x] | [-] | [x] | Web `/download`, admin `/releases`, Forgejo | | SHELL-10 | Download center / releases | [-] | [x] | [-] | [x] | Web `/download`, admin `/releases`, Forgejo |
| SHELL-11 | Landing site / legal pages | [-] | [-] | [-] | [-] | `site/` — privacy/terms/delete-account live; 2026-09-26 truthfulness/a11y cleanup (facts from code SSOT, pricing `site/src/pricing.ts`, 10 locales fully translated, footer links legal pages) — see `design.md` | | SHELL-11 | Landing site / legal pages | [-] | [-] | [-] | [-] | `site/` — the only landing/download/legal/invite/assetlinks surface (Wave 3, 2026-09-26: copies in api-server wwwroot and apps/web removed; 404.html; hero loops a copy of the app recording capsule). Prices/URLs from `packages/core` plan-catalog/web-urls — see `design.md` |
| SHELL-12 | Notifications (in-app / desktop) | [x] | [ ] | [x] | [x] | Desktop events; mobile push | | SHELL-12 | Notifications (in-app / desktop) | [x] | [ ] | [x] | [x] | Desktop events; mobile push |
--- ---

View file

@ -26,7 +26,13 @@ Legend: `[ ]` open · `[~]` in progress · `[!]` blocked externally · `[x]` res
| GAP-REL-08 | Release | 다운로드 센터가 **존재하지 않는 설치 파일**을 가리켰다. `apps/web/src/lib/desktop-release.ts`와 `site/src/release.ts`의 `DESKTOP_VERSION`이 `1.2.0`에 멈춰 있어 설치 URL이 `D3RO-Voice-Setup-1.2.0-x64.exe`였고, 그 경로는 피드에서 404다(실측: 1.2.0=404, 1.3.7=206). `version:sync`가 이 두 표면을 덮지 않아 계속 어긋났다. | `scripts/ci/sync-version.mjs`, `apps/web/src/lib/desktop-release.ts`, `site/src/release.ts` | `[x]` 2026-09-19: 두 다운로드 계약 파일을 `sync-version.mjs` 대상에 추가해 버전·릴리스일이 SSOT에서 자동 반영되도록 하고, 현재 값(1.3.7 / 2026-09-19)으로 정정했다. `version:check`·typecheck·site 빌드 GREEN. | | GAP-REL-08 | Release | 다운로드 센터가 **존재하지 않는 설치 파일**을 가리켰다. `apps/web/src/lib/desktop-release.ts`와 `site/src/release.ts`의 `DESKTOP_VERSION`이 `1.2.0`에 멈춰 있어 설치 URL이 `D3RO-Voice-Setup-1.2.0-x64.exe`였고, 그 경로는 피드에서 404다(실측: 1.2.0=404, 1.3.7=206). `version:sync`가 이 두 표면을 덮지 않아 계속 어긋났다. | `scripts/ci/sync-version.mjs`, `apps/web/src/lib/desktop-release.ts`, `site/src/release.ts` | `[x]` 2026-09-19: 두 다운로드 계약 파일을 `sync-version.mjs` 대상에 추가해 버전·릴리스일이 SSOT에서 자동 반영되도록 하고, 현재 값(1.3.7 / 2026-09-19)으로 정정했다. `version:check`·typecheck·site 빌드 GREEN. |
| GAP-REL-09 | Release | 랜딩 사이트가 **재배포되지 않는다**. `deploy` 워크플로가 main push마다 실패한다. 실측 원인(run#66 로그): `site/src/sections/Hero.tsx`가 타이머 ref를 `NodeJS.Timeout`으로 타이핑해 `@types/node` 네임스페이스가 필요했고, 배포 잡은 `npm ci --prefix site`만 하므로 조상 `node_modules`의 hoisted 타입이 없어 `tsc -b`가 `TS2503: Cannot find namespace 'NodeJS'`로 실패한다. 그래서 `https://d3ro.chanpaca.net/release-identity.json`이 404다(공개 버전 검증 불가). | `.forgejo/workflows/deploy-site.yml`, `site/src/sections/Hero.tsx` | `[x]` 2026-09-19: ref를 `ReturnType<typeof setTimeout>`으로 바꿔 hoisted 타입 의존을 제거했다(격리 `--typeRoots`로 CI 조건 재현 → 수정 전 TS2503, 수정 후 clean). 같은 수정을 push하자 `deploy` run#67이 사이트 빌드를 통과해 `dist/`를 만들었고, 실패는 다음 단계(Cloudflare)로 이동했다. | | GAP-REL-09 | Release | 랜딩 사이트가 **재배포되지 않는다**. `deploy` 워크플로가 main push마다 실패한다. 실측 원인(run#66 로그): `site/src/sections/Hero.tsx`가 타이머 ref를 `NodeJS.Timeout`으로 타이핑해 `@types/node` 네임스페이스가 필요했고, 배포 잡은 `npm ci --prefix site`만 하므로 조상 `node_modules`의 hoisted 타입이 없어 `tsc -b`가 `TS2503: Cannot find namespace 'NodeJS'`로 실패한다. 그래서 `https://d3ro.chanpaca.net/release-identity.json`이 404다(공개 버전 검증 불가). | `.forgejo/workflows/deploy-site.yml`, `site/src/sections/Hero.tsx` | `[x]` 2026-09-19: ref를 `ReturnType<typeof setTimeout>`으로 바꿔 hoisted 타입 의존을 제거했다(격리 `--typeRoots`로 CI 조건 재현 → 수정 전 TS2503, 수정 후 clean). 같은 수정을 push하자 `deploy` run#67이 사이트 빌드를 통과해 `dist/`를 만들었고, 실패는 다음 단계(Cloudflare)로 이동했다. |
| GAP-REL-09b | Release | `d3ro.chanpaca.net`이 404였던 직접 원인: 이 Cloudflare 계정에 Pages 프로젝트 `d3ro`/`d3ro-voice`가 **존재하지 않아** 커스텀 도메인 바인딩이 없었다(빈 본문 404, `cf-ray`만 반환). Pages 커스텀 도메인은 존 DNS CNAME(`d3ro → d3ro.pages.dev`)을 요구하는데 기존 `d3ro` 레코드가 남아 있어 `CNAME record not set`으로 pending에 머물렀고, 로컬 wrangler 자격증명에는 DNS 스코프가 없다(403 Authentication error). `deploy-site.yml`은 `CF_API_TOKEN` 시크릿이 없어 마지막 게시 단계에서도 `exit 1`이다. | `server/cloudflare-site-bridge/`, `.forgejo/workflows/deploy-site.yml`, `docs/map/02-infrastructure.md` | `[x]` 2026-09-19: Pages 프로젝트 `d3ro` 생성 + `site/dist` production 배포(`d3ro.pages.dev` 200, `release-identity.json` = commit `2407f5a` / 1.3.7) + 커스텀 도메인 연결. DNS 없이 도메인을 살리기 위해 Workers 라우트 브리지(`server/cloudflare-site-bridge`, `d3ro.chanpaca.net/*` → Pages 프록시, `npx wrangler deploy`)를 배포 → 라이브 확인: `/`·`/privacy/`·`/terms/`·`/delete-account/` 200, 라이브 번들이 설치 파일명을 `1.3.7`로 계산, `/download.html` → `/#download`. 남은 정리 2건: (1) 대시보드에 CNAME을 추가한 뒤 브리지 워커 삭제, (2) CI 자동 게시를 위해 `CF_API_TOKEN`(Pages/Workers Edit) + `CF_ACCOUNT_ID`=`8e83cc130e7329c160cf2b88d6b4c20a`를 Forgejo 시크릿에 등록. **2026-09-23:** CI `deploy`(deploy-site.yml)는 `CF_API_TOKEN` 부재로 run#71까지 여전히 실패한다. 로컬 인증 wrangler(pages:write)로 `npm run build --prefix site` → release-identity 작성 → `npx wrangler pages deploy site/dist --project-name d3ro --branch main`을 수동 배포했고, 라이브 `https://d3ro.chanpaca.net/release-identity.json`이 commit `5c11ee2` / version `1.5.0`을, 라이브 번들이 설치 파일명 `D3RO-Voice-Setup-1.5.0-x64.exe`를 보고한다. 자동 게시에는 여전히 시크릿 등록이 필요하다. | | GAP-REL-09b | Release | `d3ro.chanpaca.net`이 404였던 직접 원인: 이 Cloudflare 계정에 Pages 프로젝트 `d3ro`/`d3ro-voice`가 **존재하지 않아** 커스텀 도메인 바인딩이 없었다(빈 본문 404, `cf-ray`만 반환). Pages 커스텀 도메인은 존 DNS CNAME(`d3ro → d3ro.pages.dev`)을 요구하는데 기존 `d3ro` 레코드가 남아 있어 `CNAME record not set`으로 pending에 머물렀고, 로컬 wrangler 자격증명에는 DNS 스코프가 없다(403 Authentication error). `deploy-site.yml`은 `CF_API_TOKEN` 시크릿이 없어 마지막 게시 단계에서도 `exit 1`이다. | `server/cloudflare-site-bridge/`, `.forgejo/workflows/deploy-site.yml`, `docs/map/02-infrastructure.md` | `[x]` 2026-09-19: Pages 프로젝트 `d3ro` 생성 + `site/dist` production 배포(`d3ro.pages.dev` 200, `release-identity.json` = commit `2407f5a` / 1.3.7) + 커스텀 도메인 연결. DNS 없이 도메인을 살리기 위해 Workers 라우트 브리지(`server/cloudflare-site-bridge`, `d3ro.chanpaca.net/*` → Pages 프록시, `npx wrangler deploy`)를 배포 → 라이브 확인: `/`·`/privacy/`·`/terms/`·`/delete-account/` 200, 라이브 번들이 설치 파일명을 `1.3.7`로 계산, `/download.html` → `/#download`. 남은 정리 2건: (1) 대시보드에 CNAME을 추가한 뒤 브리지 워커 삭제, (2) CI 자동 게시를 위해 `CF_API_TOKEN`(Pages/Workers Edit) + `CF_ACCOUNT_ID`=`8e83cc130e7329c160cf2b88d6b4c20a`를 Forgejo 시크릿에 등록. **2026-09-23:** CI `deploy`(deploy-site.yml)는 `CF_API_TOKEN` 부재로 run#71까지 여전히 실패한다. 로컬 인증 wrangler(pages:write)로 `npm run build --prefix site` → release-identity 작성 → `npx wrangler pages deploy site/dist --project-name d3ro --branch main`을 수동 배포했고, 라이브 `https://d3ro.chanpaca.net/release-identity.json`이 commit `5c11ee2` / version `1.5.0`을, 라이브 번들이 설치 파일명 `D3RO-Voice-Setup-1.5.0-x64.exe`를 보고한다. 자동 게시에는 여전히 시크릿 등록이 필요하다. |
| GAP-BILL-01 | Billing | 랜딩 사이트 가격(Pro ₩2,900 / Pro+ ₩8,900, 2026-09-26 사용자 확정)과 서버 결제 카탈로그(Payple Pro ₩9,900 / Pro+ ₩29,900)가 다르다. 결제 진입 URL도 `payment-handlers.ts`(`d3ro.chanpaca.net/billing`)와 `license-handlers.ts`(`d3ro.dev/billing`)로 갈린다. | `site/src/pricing.ts`, `server/supabase/functions/_shared/payple.ts`, `server/supabase/functions/_shared/billing-catalog.ts`, `apps/desktop/src/main/ipc/license-handlers.ts` | `[ ]` 2026-09-26: 사이트는 새 가격을 표시하고 `d3ro.chanpaca.net/billing?tier=`로 연결한다. 서버 카탈로그 금액 변경(Payple·Stripe 가격 ID)과 결제 URL 정본 확정이 남았다. | | GAP-BILL-01 | Billing | 사이트 가격과 서버 청구 금액·결제 진입 URL이 제각각이었다. | `packages/core/src/plan-catalog.ts`, `packages/core/src/web-urls.ts`, `scripts/ci/sync-core-contract.mjs` | `[x]` 2026-09-26 (Wave 3, 88f24d8·b6fe588): 가격·쿼터·공개 URL 정본을 core 두 파일로 합치고 Deno는 생성 사본(`contract:check`). Payple 함수 4종+billing-catalog 운영 재배포(payple-* v9, billing-catalog v5) → 신규·갱신 모두 ₩2,900/₩8,900. 결제 진입은 `/app/billing` 하나. |
| GAP-BILL-02 | Billing | **Payple 정기 갱신이 한 번도 실행되지 않았다.** 갱신 cron(`payple-renew`)이 `.github/workflows`에만 있었는데 GitHub 원격이 없다. 또 웹 Payple 결제용 `NEXT_PUBLIC_PAYPLE_CLIENT_KEY`가 어디에도 설정된 적이 없어 웹 Payple 결제 버튼이 꺼져 있다. | `.forgejo/workflows/payple-renew.yml`, `apps/web/src/components/billing/payple-checkout-button.tsx`, `apps/web/Dockerfile` | `[!]` EXT 2026-09-26: 워크플로는 Forgejo로 이식(dc43884). 남은 일 — Forgejo 시크릿 `SUPABASE_URL`·`CRON_SECRET` 등록, Payple 클라이언트 키를 웹 이미지 build arg로 넣어 재빌드, Stripe(USD)·Google Play 가격을 각 콘솔에서 새 가격으로. |
| GAP-WEB-01 | Web | 웹앱(`apps/web`)이 공개되지 않았다(배포 경로가 없었음). | `apps/web/Dockerfile`, `docker-compose.nas.yml`, `server/cloudflare-site-bridge` | `[~]` 2026-09-26: `/app` basePath·standalone 이미지·NAS 컨테이너 `d3ro_voice_web`(3002). 남은 일(EXT) — Cloudflare Tunnel 공개 호스트(예: `d3ro-app.chanpaca.net` → NAS 3002) 추가 후 브리지 워커 `WEB_APP_ORIGIN` 설정·배포, Supabase 대시보드 Redirect URLs에 `https://d3ro.chanpaca.net/app/**` 추가. 그 전까지 `/app/*`는 사이트 404. |
| GAP-OPS-01 | Ops | NAS 운영 compose가 저장소와 어긋나 있다. 운영본은 JWT 비밀값 기본값 폴백·법률 wwwroot 마운트를 쓰고, `.env`에 `ADMIN_BOOTSTRAP_TOKEN`·`API_SERVER_URL`·`SUPABASE_URL`·`SUPABASE_SERVICE_ROLE_KEY`가 없어 저장소 compose(`:?` 필수)로 바꾸면 api/admin이 기동하지 않는다. | `docker-compose.nas.yml`, NAS `/volume1/docker/d3ro/.env` | `[ ]` 2026-09-26: 운영본에만 있던 `d3ro-whisper`는 저장소로 합쳤다. 누락 변수를 채운 뒤 저장소 compose로 교체해야 한다. |
| GAP-CI-01 | CI | macOS 빌드·서명 러너가 없다. `.github`의 build-mac·release-signing-ca는 실행된 적 없이 삭제됐다. | `.forgejo/workflows/*`, `.gitlab-ci.yml` | `[!]` EXT: Mac 호스트에 Forgejo runner(`macos` 라벨)를 붙이거나 GitLab `package-macos` 사용. 서명된 Android 릴리스는 GitLab `mobile-production-release`가 정본. |
| GAP-I18N-02 | Desktop | 녹음 캡슐의 "처리 중..."이 `recording-tip/index.html`에 한국어로 고정돼 앱 언어를 따르지 않는다. | `apps/desktop/src/renderer/popups/recording-tip/*`, `WindowManager.getPopupI18nStrings` | `[ ]` 2026-09-26 발견. |
| GAP-TEAM-02 | Team | 브라우저에서 초대를 수락하는 경로가 없다. 웹 `accept-invite` 페이지는 발급 링크가 가리키지 않고 로그인 후 토큰을 읽는 곳이 없어 끊겨 있었으므로 삭제했다(사이트 `/accept-invite/`는 앱 딥링크만). | `site/public/accept-invite/`, `server/supabase/functions/team-accept` | `[ ]` 2026-09-26: 데스크톱 전용 사용자를 위한 웹 수락 흐름이 필요하면 `/app` 아래에 다시 설계. |
| GAP-REL-10 | Release | `release-windows`(태그 파이프라인)는 서명 가드에 도달하기 **전에** sidecar 단계에서 죽는다. 이 러너 컨텍스트에서는 `sidecar:setup`이 Python 3.11+를 찾지 못한다(`Python 3.11+ 를 찾을 수 없습니다`) → `sidecar:build` → `verify-sidecar-bundle.mjs` 연쇄 실패(실측: run#65 `v1.3.7`, run#61 `v1.3.6`). 같은 러너의 portable 잡은 `py -3.11 → Python 3.11.9`를 찾아 사이드카 빌드에 성공하므로, 워크플로/컨테이너 간 PATH 차이다. | `.forgejo/workflows/release.yml`, `apps/desktop/scripts/setup-sidecar.mjs` | `[!]` 2026-09-19: 러너에 Python 3.11+(`py` 런처 포함)를 보장하거나 워크플로에 `actions/setup-python` 단계를 추가한다. 그 전까지 서명 게시는 불가능하다(GAP-REL-02와 별개 선행 차단). | | GAP-REL-10 | Release | `release-windows`(태그 파이프라인)는 서명 가드에 도달하기 **전에** sidecar 단계에서 죽는다. 이 러너 컨텍스트에서는 `sidecar:setup`이 Python 3.11+를 찾지 못한다(`Python 3.11+ 를 찾을 수 없습니다`) → `sidecar:build` → `verify-sidecar-bundle.mjs` 연쇄 실패(실측: run#65 `v1.3.7`, run#61 `v1.3.6`). 같은 러너의 portable 잡은 `py -3.11 → Python 3.11.9`를 찾아 사이드카 빌드에 성공하므로, 워크플로/컨테이너 간 PATH 차이다. | `.forgejo/workflows/release.yml`, `apps/desktop/scripts/setup-sidecar.mjs` | `[!]` 2026-09-19: 러너에 Python 3.11+(`py` 런처 포함)를 보장하거나 워크플로에 `actions/setup-python` 단계를 추가한다. 그 전까지 서명 게시는 불가능하다(GAP-REL-02와 별개 선행 차단). |
| GAP-REL-11 | Release | portable 워크플로의 마지막 `actions/upload-artifact@v4` 단계가 Forgejo 러너에서 `GHESNotSupportedError`로 실패한다(증거 보존만 실패, 게시는 성공). | `.forgejo/workflows/portable.yml` | `[x]` 2026-09-19: `v1.3.7` portable 게시는 run#64에서 성공(7z 단일 볼륨 83.7MB + zip 2부, `portable-latest/portable.json`이 1.3.7 보고). 남은 조치: upload-artifact 단계를 제거하거나 v3/다른 보존 방식으로 바꿔 워크플로를 GREEN으로 만든다. | | GAP-REL-11 | Release | portable 워크플로의 마지막 `actions/upload-artifact@v4` 단계가 Forgejo 러너에서 `GHESNotSupportedError`로 실패한다(증거 보존만 실패, 게시는 성공). | `.forgejo/workflows/portable.yml` | `[x]` 2026-09-19: `v1.3.7` portable 게시는 run#64에서 성공(7z 단일 볼륨 83.7MB + zip 2부, `portable-latest/portable.json`이 1.3.7 보고). 남은 조치: upload-artifact 단계를 제거하거나 v3/다른 보존 방식으로 바꿔 워크플로를 GREEN으로 만든다. |
| GAP-REL-12 | Release | Forgejo generic registry는 파일 단위 덮어쓰기를 거부(HTTP 409)하는데, portable 게시 스크립트의 "동일 파일 건너뛰기"가 1MiB 초과 파일을 **크기만** 비교했다. 그래서 새 runtime의 sidecar 볼륨 `.001`이 이전 볼륨과 정확히 같은 94,371,840 bytes였고, 내용이 다른데도 "동일"로 오판해 `runtime-latest`가 낡은 `.001` + 새 `.002`로 갈라졌다(다운로드 시 손상). | `scripts/ci/publish-portable-release.mjs` | `[x]` 2026-09-23: 파일 목록 API(`GET /api/v1/packages/{owner}/generic/{name}/{version}/files`)의 sha256으로 정확히 비교하고, 내용이 다른 파일이 하나라도 있으면 해당 별칭 버전 전체를 삭제 후 재업로드하도록 수정했다. 이미 발생한 `runtime-latest`는 버전을 지우고 재게시해 4개 파일 모두 로컬 해시와 일치함을 확인했다. | | GAP-REL-12 | Release | Forgejo generic registry는 파일 단위 덮어쓰기를 거부(HTTP 409)하는데, portable 게시 스크립트의 "동일 파일 건너뛰기"가 1MiB 초과 파일을 **크기만** 비교했다. 그래서 새 runtime의 sidecar 볼륨 `.001`이 이전 볼륨과 정확히 같은 94,371,840 bytes였고, 내용이 다른데도 "동일"로 오판해 `runtime-latest`가 낡은 `.001` + 새 `.002`로 갈라졌다(다운로드 시 손상). | `scripts/ci/publish-portable-release.mjs` | `[x]` 2026-09-23: 파일 목록 API(`GET /api/v1/packages/{owner}/generic/{name}/{version}/files`)의 sha256으로 정확히 비교하고, 내용이 다른 파일이 하나라도 있으면 해당 별칭 버전 전체를 삭제 후 재업로드하도록 수정했다. 이미 발생한 `runtime-latest`는 버전을 지우고 재게시해 4개 파일 모두 로컬 해시와 일치함을 확인했다. |

View file

@ -1,5 +1,7 @@
# Phase 7: 테스트 + 빌드 + 배포 # Phase 7: 테스트 + 빌드 + 배포
> 2026-09-26: `.github/workflows`는 삭제되었고 CI는 `.forgejo/workflows`가 정본이다 — 아래 `7.9 CI/CD (GitHub Actions)` 절은 최초 계획 시점의 스냅샷이다.
## 목표 ## 목표
테스트 커버리지를 확보하고, 프로덕션 빌드/패키징 파이프라인을 구축하여 Windows 설치 파일을 생성한다. CI/CD를 설정하여 자동 빌드 및 배포를 가능하게 한다. 테스트 커버리지를 확보하고, 프로덕션 빌드/패키징 파이프라인을 구축하여 Windows 설치 파일을 생성한다. CI/CD를 설정하여 자동 빌드 및 배포를 가능하게 한다.

View file

@ -2,6 +2,7 @@
> 본인 Mac에서 D3RO Voice를 dev 실행하거나 dist(.dmg/.zip) 빌드하기 위한 단계별 가이드. > 본인 Mac에서 D3RO Voice를 dev 실행하거나 dist(.dmg/.zip) 빌드하기 위한 단계별 가이드.
> Windows에서 작성된 코드는 V2-5에서 모든 플랫폼 분기를 추가했습니다 — Mac에서는 별도 코드 수정 없이 아래 단계만 따르면 됩니다. > Windows에서 작성된 코드는 V2-5에서 모든 플랫폼 분기를 추가했습니다 — Mac에서는 별도 코드 수정 없이 아래 단계만 따르면 됩니다.
> 2026-09-26: `.github/workflows`는 삭제되었고 CI는 `.forgejo/workflows`가 정본이다 — 아래 `9. CI 빌드 (GitHub Actions)` 절은 작성 시점의 스냅샷이다.
--- ---

View file

@ -8,6 +8,7 @@
> 데이터·권한 정본: `server/supabase/migrations` + Supabase Auth/Storage/Edge Functions > 데이터·권한 정본: `server/supabase/migrations` + Supabase Auth/Storage/Edge Functions
> 제품화 재개 핸드오프: [`MOBILE_PRODUCTIZATION_HANDOFF_2026-08-21.md`](./MOBILE_PRODUCTIZATION_HANDOFF_2026-08-21.md) > 제품화 재개 핸드오프: [`MOBILE_PRODUCTIZATION_HANDOFF_2026-08-21.md`](./MOBILE_PRODUCTIZATION_HANDOFF_2026-08-21.md)
> 이 문서는 이전 `docs/v3/00-mobile-master-plan.md`의 미래 로드맵을 대체하는 구현·검증 정본이다. > 이 문서는 이전 `docs/v3/00-mobile-master-plan.md`의 미래 로드맵을 대체하는 구현·검증 정본이다.
> 2026-09-26: `.github/workflows`는 삭제되었고 CI는 `.forgejo/workflows`가 정본이다 — 아래 날짜별 기록에 남은 `.github/workflows` 언급은 해당 시점의 스냅샷이다.
## 0. 2026-09-16 출시 게이트 스냅샷 ## 0. 2026-09-16 출시 게이트 스냅샷
@ -367,7 +368,7 @@
| 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 | | 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 | | 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.2.0`/`1020001`, 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 | | Production Android release gate | release/config/artifact/provenance self-tests + Play/Firebase/AdMob live 재확인 | release identity `1.2.0`/`1020001`, 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 | | 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. **2026-09-26 Wave 3 갱신(cd9d199):** `apps/web/public`과 `apps/api-server/wwwroot` 사본은 삭제됐고, `site/public`이 유일한 source가 됐다(`scripts/ci/verify-android-app-links.mjs`도 `site/public`만 검사) — live 재검증 필요성은 그대로다 | `site/public/.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 | | 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 | | 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 | | 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 |

View file

@ -7,6 +7,7 @@
> 2026-08-21 Git 기준선: `main` @ `a9c9a1ca6efdfc83ae7a12a0c1f2a05f15ef9138` (역사 증거, 현재 release SHA 아님) > 2026-08-21 Git 기준선: `main` @ `a9c9a1ca6efdfc83ae7a12a0c1f2a05f15ef9138` (역사 증거, 현재 release SHA 아님)
> release identity: version `1.1.0`, Android versionCode / iOS build `1010001` > 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** > 판정: **내부 기능·local upload/evidence key·AdMob identity는 준비됐지만 production AAB·Firebase·CI secret 주입·closed test가 없어 Play 출시는 RED**
> 2026-09-26: `.github/workflows`는 삭제되었고 CI는 `.forgejo/workflows`가 정본이다 — 아래 `.github/workflows` 언급은 작성 시점의 스냅샷이다.
## 0. 2026-08-29 release readiness 스냅샷 ## 0. 2026-08-29 release readiness 스냅샷

View file

@ -2,6 +2,8 @@
기준일: 2026-09-16. 체크 표시는 이 문서를 읽은 사람이 실제 증거를 확인한 뒤에만 바꾼다. 소스 검사, debug/E2E APK, HTTP 200 하나만으로 release GREEN을 선언하지 않는다. 기준일: 2026-09-16. 체크 표시는 이 문서를 읽은 사람이 실제 증거를 확인한 뒤에만 바꾼다. 소스 검사, debug/E2E APK, HTTP 200 하나만으로 release GREEN을 선언하지 않는다.
> 2026-09-26: `.github/workflows`는 삭제되었고 CI는 `.forgejo/workflows`가 정본이다(dc43884) — 아래 `.github/workflows` 언급은 기준일(2026-09-16) 시점의 스냅샷이다. 서명 Android production release job은 Forgejo로 이식되지 않고 `.gitlab-ci.yml`의 `mobile-production-release`(수동/protected)에 그대로 남았다.
### 2026-08-29 Play Console live 스냅샷 ### 2026-08-29 Play Console live 스냅샷
| 항목 | 확인된 현재 사실 | 판정 | | 항목 | 확인된 현재 사실 | 판정 |
@ -99,9 +101,9 @@
- [ ] CI의 AAB signer 검증에는 upload SHA-256을 사용했다. - [ ] CI의 AAB signer 검증에는 upload SHA-256을 사용했다.
- [ ] Firebase OAuth, Google sign-in과 HTTPS App Links에는 **Play app-signing** SHA-1/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`에 넣지 않는다. **현재 blocker:** `site/public`의 source `assetlinks.json`은 확인된 Play app-signing SHA-256으로 교체했다. (기준일 당시엔 `apps/web/public`, `apps/api-server/wwwroot`에도 사본이 있어 source가 3곳이었으나, 2026-09-26 Wave 3(cd9d199)에서 그 두 사본이 삭제되어 `site/public`이 유일한 source다.) 그러나 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를 거부해야 한다. 코드 경계: Android production release job(`.gitlab-ci.yml`의 `mobile-production-release`)은 `ANDROID_UPLOAD_CERT_SHA256`과 `PLAY_APP_SIGNING_CERT_SHA256`을 분리하고, `scripts/ci/mobile-release-evidence-lib.mjs`와 App Links verifier(`scripts/ci/verify-android-app-links.mjs`, `site/public`만 검사)가 폐기 certificate를 거부해야 한다.
### Firebase ### Firebase