- 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.
124 lines
8.3 KiB
Markdown
124 lines
8.3 KiB
Markdown
# D3RO-VOICE 리팩토링 정책 (SSOT)
|
|
|
|
> 마지막 갱신: 2026-07-22 — Wave 1 정책 수립
|
|
> 적용 범위: `apps/desktop` + `packages/{core,ui,i18n}` (web/admin/site/server/mobile은 별도 wave)
|
|
|
|
## 0. 전제: 기존 3계층 규칙 SSOT와의 관계
|
|
|
|
D3RO는 이미 3계층 강제 규칙 체계를 가진다. 본 정책은 이를 **재정의하지 않고 인용**한다.
|
|
|
|
| 계층 | 위치 | 성격 |
|
|
|------|------|------|
|
|
| L1 코딩 규칙 | `CLAUDE.md` | 사람이 읽는 규칙 (헌법) |
|
|
| L2 강제 규칙 훅 | `.claude/settings.json` (UserPromptSubmit) | 매 프롬프트마다 자동 주입 |
|
|
| L3 피드백 | `memory/*.md` | 사용자 피드백 축적 |
|
|
|
|
**본 문서(L0)** 는 리팩토링 Wave 한정 **의사결정 기록(decision log)** 역할. L1/L2와 충돌 시 L1/L2가 우선하고 본 문서를 갱신한다. (`harness-gate` 스킬이 이 충돌을 감시.)
|
|
|
|
---
|
|
|
|
## 1. 8개 결정포인트 (표준)
|
|
|
|
### DP1. 디자인 토큰 SSOT — `packages/ui/src/theme.ts`
|
|
- 색상 = `d3roPalette`, 그림자 = `d3roShadow`(CSS var), 타이포 = `d3roTypo`, 라디우스 = `d3roRadius`
|
|
- **금지**: `theme.ts`/`theme-vars.ts` 제외 모든 `.ts/.tsx`에서 hex 하드코딩 (`#3b82f6` 등). L1/L2 강제.
|
|
- 매직 넘버: 화면 1개에 고유한 수치(`WAVE_BAR_COUNT = 9` 등)는 토큰화 **금지**, file-private 명명상수로 격리. (이식 인사이트 P4)
|
|
|
|
### DP2. IPC 채널·타입 SSOT — `packages/core/src/ipc-channels.ts` + `types.ts` + `errors.ts`
|
|
- 채널명 = `IPC_CHANNELS` 객체, 형식 `${feature}:${action}`. 설계서 02 준수.
|
|
- 에러 = `D3ROError` + `ErrorCode` enum (errors.ts). `throw new Error()` 금지.
|
|
- 새 채널/타입은 SSOT 파일에 먼저 추가 → apps가 import.
|
|
|
|
### DP3. i18n — `packages/i18n` + t() 함수
|
|
- UI 문자열은 반드시 `t('key')`. 12 locale(ko/en/ja/zh/zh-TW/es/fr/de/pt/ru/vi/th). L1/L2 강제.
|
|
- 서비스/유틸/팝업이 한국어·영어 **리터럴을 반환**하면 다국어 불가 → 함수는 **i18n 키**(또는 enum)를 반환, 렌더링 시점에 t()로 풀이. 메인 프로세스 문자열(팝업/트레이 메뉴)도 동일. (이식 인사이트 P5)
|
|
- 고아 locale 키(코드에서 미사용)는 정리 대상.
|
|
|
|
### DP4. 서비스 경계 — 싱글톤 + EventEmitter
|
|
- 모든 서비스: `getInstance()` 싱글톤 + `EventEmitter`. 설계서 01 인터페이스 준수.
|
|
- `apps/desktop/src/main/services/`에 **32개 서비스** 존재 (CLAUDE.md 명시분보다 다수 — 감사에서 실제 목록 정합).
|
|
|
|
### DP5. 상태 관리 — RecognitionState(9) + AudioState(4) 이중 상태머신
|
|
- VoiceModeService 오케스트레이터. 설계서 01 준수. (이번 wave는 상태머신 재설계 X, 정합 점검만)
|
|
|
|
### DP6. packages ↔ apps 의존 방향 — 단방향 (apps → packages)
|
|
- packages는 apps에 의존 금지. (이미 구조적으로 보장.)
|
|
- **주의**: `packages/ui`, `packages/i18n`은 web/admin/site/mobile도 공유 → 변경 시 **영향 받는 모든 앱 typecheck** 필수 (R3 모노레포). 이번 wave는 desktop+packages라도, ui/i18n/core 변경은 모노레포 전체 typecheck로 검증.
|
|
|
|
### DP7. DRY 임계치 — **3회 (제안)**
|
|
- 동일 로직/상수/매직스트링이 **3곳 이상** 중복 → 추출(SSOT로). (사례: PREMIUM_MODEL_LIMITS 3곳 → `@d3ro/core/constants` 통합 완료.)
|
|
- 2곳 중복은 맥락에 따라 판단(강제 X). 임계치 아래는 의도적 중복 허용.
|
|
- **확정: 3회** (2026-07-22 합의)
|
|
|
|
### DP8. 자동화 — typecheck + lint + test + 강제 규칙 훅
|
|
- WS 통합 후 반드시 GREEN: `npm run typecheck`(turbo) · `npm run lint` · `npm run test` · `npm run build`(빌드 영향 WS).
|
|
- UI 변경: `npm run dev`로 화면 1회 확인. (WS3은 화면별 즉시)
|
|
|
|
---
|
|
|
|
## 2. 본 Wave 한정 추가 결정 (감사 후 확정 가능)
|
|
|
|
### DP9. 미사용/데드코드 처리 강도 — **공격적 (제안)**
|
|
- R2 공격적 모드: "false negative > false positive". export/컴포넌트/서비스/의존성 미사용은 DELETE 우선.
|
|
- 단, 외부 API(라이브러리 entry, IPC handler)로 노출된 export는 KEEP. 감사에서 `grep references`로 확인 후 판정.
|
|
- 알려진 잔여: `MetalDial`(CLAUDE 명시 미사용), `@mui/icons-material` 의존성(lucide 전환 후 잔여), LemonSqueezy 코드 잔존(LicenseService activate/deactivate).
|
|
- **확정: 공격적 DELETE** (2026-07-22 합의) — 단 외부 API 노출 export는 grep references 후 KEEP.
|
|
|
|
### DP10. 워크스트림 분할 (R3) — 파일 영역 비충돌
|
|
| WS | 영역 | 의존 |
|
|
|----|------|------|
|
|
| WS1 | 토큰/SSOT 정의 — `packages/ui/src/{theme,theme-vars}.ts`, DS 컴포넌트 | — |
|
|
| WS2 | 메인 프로세스 — `apps/desktop/src/main/`, preload, `packages/core` 타입/IPC | — |
|
|
| WS3 | 렌더러 UI — `apps/desktop/src/renderer/` (화면별 분담 가능) | **WS1 의존** |
|
|
| WS4 | i18n — `packages/i18n/src/`, t() 키 정리 | — |
|
|
| WS5 | 빌드/설정/팝업 — `package.json`, `electron.vite.config`, `electron-builder.yml`, Vanilla JS 팝업 | — |
|
|
|
|
- 1차 wave: WS1/WS2/WS4/WS5 병렬. WS3는 WS1 완료 후 발사.
|
|
- 워크스트림당 **1 커밋** (R5). `git add -A` 금지, 정밀 add.
|
|
|
|
### DP11. SKIP / 이월 기준
|
|
- "복잡해서 SKIP" 금지 (안티패턴 A6). SKIP은 **기술적 제약 + 별도 Phase 필요** 명시.
|
|
- 대규모 재설계(상태머신 재구성, 서비스 쪼개기)는 이번 wave에서 **순수 이동(P3)** 먼저 시도, 진짜 재설계는 압력(props 20+개 등)이 명백할 때만.
|
|
|
|
---
|
|
|
|
## 3. 이식 검증 인사이트 (HaramLog wave 1~10)
|
|
|
|
- **P1**: HIGH 확신도 보고도 착수 전 30초 재검증(Read/Grep). false positive 기록.
|
|
- **P2**: "순수 이동" 분할에도 동작 변경이 숨는다. diff 1:1 대조(훅 체인, useEffect deps, 구독/해제).
|
|
- **P3**: 거대 파일 분할은 순수 이동이 90%. 재설계는 분리 후 남는 압력에서만.
|
|
- **P4**: 화면 고유 수치는 토큰화 금지, file-private 상수.
|
|
- **P5**: 비컴포넌트 함수의 한국어 반환은 i18n 키 반환으로 전환.
|
|
- **P6**: 이름 충돌은 역할 접미어 예방(`RecordingState` enum vs `RecordingStateColor` 토큰).
|
|
|
|
---
|
|
|
|
## 4. 합의 이력
|
|
|
|
| 항목 | 결정 | 일시 |
|
|
|------|------|------|
|
|
| Wave 1 범위 | Desktop + 공유 packages | 2026-07-22 |
|
|
| Wave 1 초점 | 데드코드/토큰SSOT/i18n/타입-IPC-아키텍처 전부 | 2026-07-22 |
|
|
| 커밋 주기 | WS별 분리 커밋 + 즉시 검증 (R5/R7) | 2026-07-22 |
|
|
| 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` 패턴과 같음). 손으로 옮겨 적은 사본은 금지.
|