diff --git a/.claude/skills/refactor-wave/SKILL.md b/.claude/skills/refactor-wave/SKILL.md index 8259e50..9aebd3b 100644 --- a/.claude/skills/refactor-wave/SKILL.md +++ b/.claude/skills/refactor-wave/SKILL.md @@ -207,11 +207,22 @@ git 충돌 + 의도 분리 불가. **R3 위반**. ### 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 → 내 책임 분리.) +### 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 run --config vitest.config.ts`로 돌리고, 남는 실패만 회귀 후보로 본다. + ## 누적 wave 인덱스 | Wave | 일시 | 산출 커밋 | 보고서 | |------|------|-----------|--------| | 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 3 | 2026-09-26 | 88f24d8(WS-A 계약)·b6fe588(WS-B 웹앱)·cd9d199(WS-C 중복 삭제)·dc43884(WS-D CI)·9297860(사이트) | `docs/REFACTOR_WAVE3_REPORT.md` | 다음 wave 추가 시 본 표 갱신. diff --git a/design.md b/design.md index cef9884..de26590 100644 --- a/design.md +++ b/design.md @@ -189,6 +189,18 @@ - **기능 카드에 FREE/PRO 태그 없음.** 로컬 기능은 전부 free다(`LicenseService.FEATURE_MIN_TIER`). - **화자 분리·RAG를 대표 기능으로 내세우지 않음.** 화자 구분은 추정 단계라 약속하지 않는다. - **CRT 움직이는 스캔 효과 제거**(정지 주사선만 히어로 예시 패널에 유지). 이전 기록의 "CRT CSS 유지"는 예시 패널 한 곳으로 좁혔다. -- **미확정:** 서버 결제 카탈로그(Payple)는 아직 Pro ₩9,900 / Pro+ ₩29,900이다. 사이트 가격과 맞추려면 서버 쪽 변경이 필요하다. - 결제 URL `https://d3ro.chanpaca.net/billing?tier=`는 데스크톱 `payment-handlers.ts`와 같지만, - `license-handlers.ts`는 `https://d3ro.dev`를 쓴다 — 어느 쪽이 정본인지 확인 필요. +- ~~미확정: 서버 가격 불일치·결제 URL 이원화~~ → 2026-09-26 Wave 3에서 해소(`packages/core` plan-catalog/web-urls 단일 정본, `/app/billing` 하나). + +### 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에 한국어로 고정돼 있어 앱 언어를 따르지 않는다. diff --git a/docker-compose.nas.yml b/docker-compose.nas.yml index 6e25f77..0006943 100644 --- a/docker-compose.nas.yml +++ b/docker-compose.nas.yml @@ -73,3 +73,28 @@ services: options: max-size: "10m" 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: diff --git a/docs/REFACTOR_POLICY.md b/docs/REFACTOR_POLICY.md index d092bc1..7bec747 100644 --- a/docs/REFACTOR_POLICY.md +++ b/docs/REFACTOR_POLICY.md @@ -103,3 +103,22 @@ D3RO는 이미 3계층 강제 규칙 체계를 가진다. 본 정책은 이를 * | DP7 DRY 임계치 | 3회 | 2026-07-22 | | DP9 데드코드 강도 | 공격적 DELETE (외부 API 노출 export는 grep 후 KEEP) | 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` 패턴과 같음). 손으로 옮겨 적은 사본은 금지. diff --git a/docs/REFACTOR_WAVE3_REPORT.md b/docs/REFACTOR_WAVE3_REPORT.md new file mode 100644 index 0000000..5491af2 --- /dev/null +++ b/docs/REFACTOR_WAVE3_REPORT.md @@ -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`). diff --git a/docs/deployment/release-guide.md b/docs/deployment/release-guide.md index 2fa3e6d..85a820f 100644 --- a/docs/deployment/release-guide.md +++ b/docs/deployment/release-guide.md @@ -45,10 +45,12 @@ authoritative release commit - `publish-forgejo-release.mjs`는 설치 자산을 먼저, `latest.yml`을 마지막에 게시하고, 공개 URL에서 재검증한 뒤 Release 자산을 첨부한다. - 설치 바이너리는 저장소에 커밋하지 않는다. 배포 정본은 Forgejo feed이며, - `site/public/releases/*/`, `apps/web/public/releases/*/`, - `apps/api-server/wwwroot/releases/*/`는 build graph 밖이라 배포 산출물에 - 포함되지 않는다. 사이트·웹 다운로드 센터는 로컬 경로가 아니라 feed URL을 - 링크한다. (역사적 `1.0.0` 자산만 추적 상태로 남아 있다.) + `site/public/releases/*/`, `apps/web/public/releases/*/`는 `.gitignore`로 + 막혀 있어 build graph 밖이고 배포 산출물에 포함되지 않는다(추적 상태로 남아 + 있던 19개 설치본 ~568MiB는 2026-09-26 Wave 3, cd9d199에서 삭제됐고 그 뒤로는 + 추적 자산이 없다). `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/verify-release-metadata.mjs`는 배포 메타데이터와 CI/publisher 계약을 검증한다. diff --git a/docs/map/02-infrastructure.md b/docs/map/02-infrastructure.md index 129eb81..3c0b828 100644 --- a/docs/map/02-infrastructure.md +++ b/docs/map/02-infrastructure.md @@ -11,7 +11,7 @@ D:/workspace/D3ROVoice ├── apps/ │ ├── 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) │ ├── mobile-rn/ React Native product mobile app (NOT an npm workspace) │ ├── 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 ├── server/ │ ├── supabase/ Supabase project: config.toml, migrations/, functions/, tests/ -│ └── cloudflare-worker/ Edge gateway (wrangler.toml + src/index.ts) -├── site/ Vite landing site (deployed to Cloudflare Pages + GitHub Pages) +│ ├── cloudflare-worker/ Edge gateway (wrangler.toml + src/index.ts) +│ └── 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) ├── release/ Version identity SSOT + license/evidence public keys ├── 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 ├── scratch/ Large local evidence (APKs, screenshots, DBs) — not build input ├── 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) -├── .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 -├── 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 ├── package.json monorepo root, npm workspaces ├── CLAUDE.md Claude-specific project rules @@ -126,25 +126,22 @@ See [`03-shared-packages.md`](./03-shared-packages.md). Summary: ## 5. CI/CD -### GitHub Actions (`.github/workflows/`) - -| Workflow | Purpose | -|---|---| -| `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 | +> `.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. +> CI now runs only on Forgejo Actions (`.forgejo/workflows/`); see below. ### 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/`) +`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는 건드리지 않는다. -`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. --- @@ -153,15 +150,15 @@ Stages `validate → test → build → e2e → package → publish → deploy`. | 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` | -| `apps/api-server/Dockerfile` | Multi-stage .NET 10 (sdk → aspnet runtime), port 5000, `VOLUME /app/data` | -| `docker-compose.yml` | Dev/self-host: `d3ro-api-server` (5050→5000, `./data` volume), `d3ro-admin` (3001), 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 | +| `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/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/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), `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` / `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 -- **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). -- **Deploy/release:** `deploy-nas.ps1/.sh`, `deploy-site-to-nas.js`, `nas-control.sh`, `publish-gh.ps1`, `gen-keystore.js`. +- **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`, `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. - **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`. @@ -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/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) | -| `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 | | `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: - `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). diff --git a/docs/map/07-api-server.md b/docs/map/07-api-server.md index 1b5e4ca..8601a1e 100644 --- a/docs/map/07-api-server.md +++ b/docs/map/07-api-server.md @@ -13,9 +13,8 @@ - **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. - **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}`. -- **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): - Legacy SHA-256 users force-disabled. - 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. - This backend holds a **separate identity** from Supabase; see `11-gap-backlog.md` `ID-01`. diff --git a/docs/map/08-admin-console.md b/docs/map/08-admin-console.md index 1cab0ac..efe264f 100644 --- a/docs/map/08-admin-console.md +++ b/docs/map/08-admin-console.md @@ -88,4 +88,4 @@ Root files: `instrumentation.ts` (startup security validation), `proxy.ts` (edge - `admin-payments` Payple live history → `501`. - `UnavailableAdminPanel` whenever Supabase env absent; writes disabled, no sample metrics. - `/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. diff --git a/docs/map/09-supabase-backend.md b/docs/map/09-supabase-backend.md index f527a4c..95c139a 100644 --- a/docs/map/09-supabase-backend.md +++ b/docs/map/09-supabase-backend.md @@ -47,7 +47,7 @@ Migration numbering referenced in SSOT goes up to `00028`; CI verifies `migratio | `send-push` | Push delivery | | `account-delete` | Account deletion cascade + provider unlink | | `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 | | `payple-checkout` / `payple-manage` / `payple-renew` / `payple-webhook` | Payple billing (Korea) | | `iap-verify` | Google Play / App Store purchase verification | diff --git a/docs/map/10-feature-catalog.md b/docs/map/10-feature-catalog.md index c38c2a4..389f35c 100644 --- a/docs/map/10-feature-catalog.md +++ b/docs/map/10-feature-catalog.md @@ -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-09 | Support / diagnostics | [x] | [ ] | [ ] | [~] | Desktop `SupportModal`; admin `/support` stub | | 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 | --- diff --git a/docs/map/11-gap-backlog.md b/docs/map/11-gap-backlog.md index 6c93cf0..52bd827 100644 --- a/docs/map/11-gap-backlog.md +++ b/docs/map/11-gap-backlog.md @@ -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-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`으로 바꿔 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-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-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개 파일 모두 로컬 해시와 일치함을 확인했다. | diff --git a/docs/phases/phase-7.md b/docs/phases/phase-7.md index 643cd91..247d246 100644 --- a/docs/phases/phase-7.md +++ b/docs/phases/phase-7.md @@ -1,5 +1,7 @@ # Phase 7: 테스트 + 빌드 + 배포 +> 2026-09-26: `.github/workflows`는 삭제되었고 CI는 `.forgejo/workflows`가 정본이다 — 아래 `7.9 CI/CD (GitHub Actions)` 절은 최초 계획 시점의 스냅샷이다. + ## 목표 테스트 커버리지를 확보하고, 프로덕션 빌드/패키징 파이프라인을 구축하여 Windows 설치 파일을 생성한다. CI/CD를 설정하여 자동 빌드 및 배포를 가능하게 한다. diff --git a/docs/v2/phase-V2-5-mac-guide.md b/docs/v2/phase-V2-5-mac-guide.md index 06624ac..7b5cd8f 100644 --- a/docs/v2/phase-V2-5-mac-guide.md +++ b/docs/v2/phase-V2-5-mac-guide.md @@ -2,6 +2,7 @@ > 본인 Mac에서 D3RO Voice를 dev 실행하거나 dist(.dmg/.zip) 빌드하기 위한 단계별 가이드. > Windows에서 작성된 코드는 V2-5에서 모든 플랫폼 분기를 추가했습니다 — Mac에서는 별도 코드 수정 없이 아래 단계만 따르면 됩니다. +> 2026-09-26: `.github/workflows`는 삭제되었고 CI는 `.forgejo/workflows`가 정본이다 — 아래 `9. CI 빌드 (GitHub Actions)` 절은 작성 시점의 스냅샷이다. --- diff --git a/docs/v3/MOBILE_APP_COMPLETION_SSOT.md b/docs/v3/MOBILE_APP_COMPLETION_SSOT.md index 4eba412..f53132e 100644 --- a/docs/v3/MOBILE_APP_COMPLETION_SSOT.md +++ b/docs/v3/MOBILE_APP_COMPLETION_SSOT.md @@ -8,6 +8,7 @@ > 데이터·권한 정본: `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`의 미래 로드맵을 대체하는 구현·검증 정본이다. +> 2026-09-26: `.github/workflows`는 삭제되었고 CI는 `.forgejo/workflows`가 정본이다 — 아래 날짜별 기록에 남은 `.github/workflows` 언급은 해당 시점의 스냅샷이다. ## 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 | | 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 | -| 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 | | 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 | diff --git a/docs/v3/MOBILE_PRODUCTIZATION_HANDOFF_2026-08-21.md b/docs/v3/MOBILE_PRODUCTIZATION_HANDOFF_2026-08-21.md index 01ae826..df6d68d 100644 --- a/docs/v3/MOBILE_PRODUCTIZATION_HANDOFF_2026-08-21.md +++ b/docs/v3/MOBILE_PRODUCTIZATION_HANDOFF_2026-08-21.md @@ -7,6 +7,7 @@ > 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** +> 2026-09-26: `.github/workflows`는 삭제되었고 CI는 `.forgejo/workflows`가 정본이다 — 아래 `.github/workflows` 언급은 작성 시점의 스냅샷이다. ## 0. 2026-08-29 release readiness 스냅샷 diff --git a/docs/v3/play/04-release-checklist.md b/docs/v3/play/04-release-checklist.md index 76a9a1b..349745a 100644 --- a/docs/v3/play/04-release-checklist.md +++ b/docs/v3/play/04-release-checklist.md @@ -2,6 +2,8 @@ 기준일: 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 스냅샷 | 항목 | 확인된 현재 사실 | 판정 | @@ -99,9 +101,9 @@ - [ ] 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`에 넣지 않는다. +**현재 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