docs: record design overhaul waves 2-3 and update feature map
Some checks failed
deploy-site / deploy (push) Waiting to run
ci / 정본·보안·린트·타입·테스트 (push) Failing after 20s
ci / 워크스페이스 빌드 검증 (push) Has been skipped
ci / Supabase Edge Functions + Cloudflare Worker (push) Successful in 29s
ci / .NET API 서버 테스트 (push) Successful in 13s
ci / 모바일 린트·타입·Jest (push) Failing after 3h12m39s

design.md gains the wave 2 (desktop IA) and wave 3 (popups) decisions and
what was deliberately not done. Feature catalog: SHELL-01 settings route,
new SHELL-13 IA, SHELL-14 home, SHELL-15 design system v4. Backlog:
GAP-I18N-02 rows resolved; GAP-UX-01..07 capture the backend follow-ups
reported during the overhaul (meeting notes sync, record query filters,
summary-to-transcript citations, mobile IA, unified search, main-side
cleanups, remaining locales).
This commit is contained in:
Yun Chan 2026-09-28 20:46:24 +09:00
parent 7b1210b5d1
commit 171be8f4e8
3 changed files with 51 additions and 3 deletions

View file

@ -33,7 +33,14 @@ Legend: `[ ]` open · `[~]` in progress · `[!]` blocked externally · `[x]` res
| GAP-OPS-01 | Ops | NAS 운영 compose가 저장소와 어긋나 있었다(JWT 기본값 폴백, 법률 wwwroot 마운트, 필수 변수 4개 누락). | `docker-compose.nas.yml`, NAS `/volume1/docker/d3ro/.env` | `[x]` 2026-09-26: NAS `.env`에 `SUPABASE_URL`·`SUPABASE_SERVICE_ROLE_KEY`(운영 service_role)·`ADMIN_BOOTSTRAP_TOKEN`(신규)·`API_SERVER_URL` 추가, 저장소 compose로 교체 후 `docker compose up -d`. 4개 컨테이너 정상, 백업 `*.before-wave3-20260926`. |
| GAP-OPS-02 | Ops | 관리자 주소가 두 개였다(`admin.chanpaca.net`, `d3ro-admin.chanpaca.net`). `admin.d3ro.chanpaca.net`은 2단계 서브도메인이라 고급 인증서(ACM, 월 $10)가 필요하다. | Cloudflare 터널 kd-nas ingress, chanpaca.net DNS | `[x]` 2026-09-26: 사용자 결정 — 관리자 주소는 `d3ro-admin.chanpaca.net` 하나(chanpaca.net에 다른 서비스가 많다). `admin.chanpaca.net` 터널 규칙·DNS 삭제로 이름을 풀었다. ACM은 지금 구독하지 않는다. 참고: `*.kite`·`*.voice` 고급 인증서는 ACM이 꺼져 2026-11-02 만료 후 갱신되지 않는다(다른 프로젝트). |
| 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-I18N-02 | Desktop | 녹음 캡슐의 "처리 중..."이 `recording-tip/index.html`에 한국어로 고정돼 앱 언어를 따르지 않는다. | `apps/desktop/src/renderer/popups/recording-tip/*`, `WindowManager.getPopupI18nStrings` | `[x]` 2026-09-28: 캡슐 문구는 main `_i18n`(popup.tip.*)으로, 오류는 `@d3ro/core/voice-error-message` 분류 → 사용자 언어 문구(bootstrap 이 error.code 를 넘기지 않던 버그 포함). 팝업 문서 lang 도 앱 언어. |
| GAP-UX-01 | Desktop | 회의 "내 메모"(상세 탭)는 이 기기 localStorage 에만 저장된다 — 기기 간 동기화되지 않는다. 끝난 회의에는 addMemo 가 거부된다(`MeetingModeService.ts:383`). | `components/meeting/meeting-notepad-storage.ts`, `MeetingModeService` | `[ ]` 2026-09-28: 끝난 회의의 사용자 노트를 받을 필드(또는 종료 후 addMemo)와 동기화 어댑터가 필요. |
| GAP-UX-02 | Desktop | 기록 조회에 모드·즐겨찾기 조건, 태그+검색 조합이 없어 화면이 불러온 페이지 위에서 거른다(조건이 까다로우면 여러 페이지를 불러와야 보인다). 오프셋 페이지라 사이 삽입·삭제 때 누락·중복 가능. 기록 제목·본문 편집 없음. 마크다운 내보내기는 태그 붙은 기록만·저장 위치 선택 불가. | `HistoryService`, `history:getAll`, `MemoService.exportMarkdown` | `[ ]` 2026-09-28 |
| GAP-UX-03 | Desktop | 회의 요약 항목 → 전사 구간 근거 링크 없음(요약·전사를 잇는 인용 데이터가 없다). | `MeetingSummaryService`, 회의 문서 생성 | `[ ]` 2026-09-28: 레퍼런스(Granola·Otter)의 핵심 패턴 — 생성 시 구간 인용을 함께 저장해야 한다. |
| GAP-UX-04 | Mobile | 모바일 정보 구조 개편(탭 4 홈·기록·회의·더보기 + 떠 있는 녹음 버튼의 시작 메뉴) 미착수. | `apps/mobile-rn/src/navigation/TabNavigator.tsx` | `[ ]` 2026-09-28: 물결 ④. |
| GAP-UX-05 | Desktop+Mobile | 기록+회의 통합 검색 없음(회의 전사는 어떤 검색으로도 못 찾는다). | 렌더러·모바일 검색, `MeetingModeService` | `[ ]` 2026-09-28: 물결 ⑤. 스키마 변경 없이 조회 추가. |
| GAP-UX-06 | Desktop | main 쪽 정리 필요: `STTManager.ts:53-129` 공급자 설명 한국어 하드코딩·마케팅 문구("100% 완전 오프라인", "Industry Benchmark") — 렌더러는 더 이상 쓰지 않는다 / audio TEST_DEVICE 가 deviceId 무시 / 사전 `incrementUsage` 호출처 없음(사용 횟수 항상 0) / 사전 발음 필드가 인식 힌트·치환에 쓰이지 않음 / 대화 세션 다시 듣기 IPC 없음 / preload `instruction.getAll` 이 `unknown[]` / 지식 추가 실패 판정이 문구 비교. | 각 파일 | `[ ]` 2026-09-28 (도구·설정 개편 에이전트 보고) |
| GAP-UX-07 | Desktop | 2026-09-28 개편 문구는 ko·en 만 — 다른 8개 로케일은 en 폴백. 온보딩 Ollama 모델 크기 표기(`onboarding.llmModelSize` 9.6GB)와 안내 모달의 크기가 다르다. | `packages/i18n/src/locales/*` | `[ ]` 2026-09-28 |
| 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으로 만든다. |
@ -74,7 +81,7 @@ Legend: `[ ]` open · `[~]` in progress · `[!]` blocked externally · `[x]` res
| GAP-KEY-03 | Key bindings | `command` 액션에 전용 핸들러가 없다. 이번에 처음으로 설정 UI 에 노출됐지만, 트리거되면 dictation 파이프라인으로 fallback 하며 `KEYBINDING_ACTIONS` 의 `holdMode:false` 대신 dictation 과 같은 hold-to-talk 로 강제된다. 개편 이전부터 같은 동작이었고 이번 작업은 그 사실을 코드에 명시화만 했다(기능 변화 없음). | `apps/desktop/src/main/services/VoiceModeService.ts:1071`(`_resolveHoldMode`), `packages/core/src/keybinding.ts:740`(액션 정의) | `command` 전용 동작을 정의하고 `_resolveHoldMode` 의 예외를 제거하거나, 액션을 카탈로그에서 뺀다. |
| GAP-QA-02 | Quality | 캡션 테스트 2건이 **개발 머신에 사이드카 venv 가 있는지에 따라 결과가 갈린다**. `LocalSTTService.initialize()`(`:239`) → `_ensureSidecarRunning()`(`:583`) → `_spawnSidecar()`(`:650`) → `_waitForHealth()`(`:794`) 경로에서 venv 가 존재하면 실제 Python 프로세스를 띄우고 health 폴링이 vitest 기본 타임아웃 10초를 넘긴다. venv 가 없으면 `getSidecarCommand()`(`apps/desktop/src/main/utils/paths.ts:174`)가 즉시 throw 해서 같은 테스트가 빠르게 통과한다. 테스트가 로컬 환경을 격리하지 못한 것이 결함이다. | `tests/red/ipc-surfaces.usecase.test.ts`(`캡션 시작 실패는 success:false 로 나온다`), `tests/red/silent-errors.usecase.test.ts:48`. **키바인딩 개편의 회귀가 아니다** — 2026-09-21 에 HEAD(`0ca9e24`) 무수정 코드를 같은 환경(venv 연결)에서 돌려 동일하게 재현했다. 같은 날 같은 머신에서도 실행 방식에 따라 결과가 갈렸다: 전체 실행은 `3 failed / 1311 passed (1314)`(`rag.usecase` + `silent-errors` 캡션 + `paths.test`)이고 `ipc-surfaces` 캡션 케이스는 통과했는데, 그 파일만 단독 실행하면 같은 케이스가 10초 타임아웃으로 실패한다. 테스트 총수 1314 는 어느 실행에서나 같고, 새로 깨진 테스트는 0건이다. | 사이드카 기동을 테스트 경계에서 주입·모킹해 환경 의존을 끊는다. 함께 실패하는 `rag.usecase`(임베딩 서버 부재)도 같은 성격이다. `tests/main/utils/paths.test.ts:78` 은 성격이 다르다 — 기대 정규식이 `사이드카를 찾을 수 없습니다` 인데 실제 메시지는 `로컬 음성 엔진이 아직 설치되지 않았습니다…` 로 바뀌어 테스트가 문구를 따라가지 못한 것이다. |
| GAP-I18N-01 | i18n | 로케일별 키 수가 크게 어긋난다. 2026-09-21 실측: `ko` 1716 / `en` 1709 / 나머지 10개 로케일 각 327. `keybinding.*` 55개는 12개 로케일 전부에 동일하게 들어갔지만, 그 밖 약 1,380개 키가 비영어 로케일에 없어 폴백 체인(locale → `en` → `ko`)으로 표시된다. 키바인딩 작업 이전부터 있던 부채이며 그 작업 범위 밖이었다. | `packages/i18n/src/locales/*.json`, 카탈로그 SHELL-03. **구체 사례 (2026-09-21 실측)**: `popup.error.default` 가 `en.json`·`ko.json` 에만 있고 나머지 10개 로케일에 없다. 소비처는 `WindowManager.ts:59`(팝업 문자열 주입, 선재)와 `CommandsPage.tsx:198`·`:201`(LLM 수정으로 추가된 파이프라인 벤치 오류 표시) 두 곳이며, 비영어 사용자에게는 오류 메시지가 영어로 폴백된다. 새 갭이 아니라 이 행이 세는 약 1,380개 중 하나다 — 별도 행을 열지 마라. | 로케일 간 키 diff 를 내는 커버리지 게이트를 만들어 회귀를 막고, 누락 키를 채운다. |
| GAP-I18N-02 | i18n | 렌더러가 `ko.json` 에 없는 `license.*` 키를 쓴다. `TranslationKey` 가 `ko.json` 에서 파생되므로 누락은 타입 에러로 드러난다. 타입 에러로만 끝나지 않는다 — 폴백 체인이 `locale → en → ko → 키 문자열` 이므로 마스터 로케일에도 없으면 **`license.team` 같은 키가 화면에 그대로 노출된다**. 2026-09-21 실측: `license.feature.premium_llm`·`license.team`·`license.enterprise` 가 없고 이로 인한 TS2345 가 4건이다. HEAD 에서도 없던 키이므로 선재 결함이며 키바인딩 작업과 무관하다. | `apps/desktop/src/renderer/components/UpgradePromptModal.tsx:47`·`:192`, `apps/desktop/src/renderer/pages/DashboardPage.tsx:481`·`:529`, `packages/i18n/src/locales/ko.json` | 세 키를 `ko.json` 에 추가하고 12개 로케일에 반영한다. 같은 타입체크에 잡히는 `LicenseTab.tsx`(6건)·`LicenseModal.tsx`(2건)는 원인이 다르다 — `TFunction` 을 `(k: string) => string` 에 넘기는 TS2322 4건과 `currentTier` 미정의 TS2304 2건으로, 후자는 컴파일이 깨지는 별개 결함이다(GAP-INFRA-04 범위). |
| GAP-I18N-02 | i18n | 렌더러가 `ko.json` 에 없는 `license.*` 키를 쓴다. `TranslationKey` 가 `ko.json` 에서 파생되므로 누락은 타입 에러로 드러난다. 타입 에러로만 끝나지 않는다 — 폴백 체인이 `locale → en → ko → 키 문자열` 이므로 마스터 로케일에도 없으면 **`license.team` 같은 키가 화면에 그대로 노출된다**. 2026-09-21 실측: `license.feature.premium_llm`·`license.team`·`license.enterprise` 가 없고 이로 인한 TS2345 가 4건이다. HEAD 에서도 없던 키이므로 선재 결함이며 키바인딩 작업과 무관하다. | `apps/desktop/src/renderer/components/UpgradePromptModal.tsx:47`·`:192`, `apps/desktop/src/renderer/pages/DashboardPage.tsx:481`·`:529`, `packages/i18n/src/locales/ko.json` | `[x]` 2026-09-28: 세 키 + `license.feature.cloud_sync`·`team_workspace` 를 ko·en 에 추가하고, 문자열 조립 대신 `renderer/utils/display-labels.ts`(featureLabelKey·tierLabelKey) 타입 매핑으로 바꿔 누락이 컴파일 오류가 되게 했다. `LicenseTab`·`LicenseModal` 의 TFunction TS2322 와 `currentTier` 미정의(팀·엔터프라이즈에서 설정 화면이 멈추던 결함)도 수정. 다른 8개 로케일은 en 폴백(GAP-UX-07). |
| GAP-LLM-01 | LLM | **번역 대상 언어를 사용자가 고를 수 없다.** 항상 `English` 고정이다. `AppConfig` 에 대상 언어 키가 없고, 기존 두 키 모두 대용할 수 없다 — `language` 는 UI 로케일이라 `'ko'` 같은 코드가 프롬프트에 그대로 들어가 문장이 깨지고, `sttLanguage` 는 입력(원문) 언어라 그 값으로 번역하면 원문이 그대로 나온다. 2026-09-21 LLM 수정(`9c2b4d4`)은 대상 언어가 호출 프레임 두 단계 밖의 기본값에 의존하던 것을 명시 인자로 바로잡았을 뿐, 선택지를 만들지는 않았다(설정 키 + 설정 UI + i18n 이 필요해 patch 범위 밖으로 뒀다). | `apps/desktop/src/main/services/llm-prompts.ts:37`(`DEFAULT_TARGET_LANGUAGE`)·`:50`(`resolveTargetLanguage`), `packages/core/src/types.ts`(`AppConfig` 에 키 없음), 내장 프리셋 `CustomInstructionService.ts:26` | `AppConfig` 에 대상 언어 키를 추가하고, 설정 UI(LLM 탭)에 노출하고, `resolveTargetLanguage()` 가 설정을 읽게 한다. 언어 목록과 라벨은 i18n 키가 필요하다. |
| GAP-LLM-02 | LLM | **2026-09-21 LLM 지시문 수정(`9c2b4d4`)이 실앱 구동으로 검증되지 않았다.** 유닛 테스트는 통과하지만(`llm-prompts.test.ts` 21, `llm-handlers.test.ts` 7, `VoiceModeService.test.ts` 22, `ChainService.test.ts` 5 — 수정 4건을 각각 되돌려 실제로 실패하는 것까지 확인), 실행 중인 Electron 에서 실제 지시문을 돌려 결과가 삽입되는 것을 본 적이 없다. 에이전트는 데스크톱 GUI 를 띄울 수 없다(`AGENTS.md` §3). **게다가 이 수정과 직접 관련된 usecase 테스트 4개가 실행조차 되지 않았다** — `tests/red/{instruction,chain,voice,config}.usecase.test.ts` 가 `better-sqlite3` ABI 불일치로 DB 생성 단계에서 먼저 죽는다(GAP-INFRA-06). 즉 그 범위는 통과도 실패도 아닌 **미검증**이다. 영향 받는 카탈로그 행: AI-04, AI-05, AI-06, AI-07(전부 데스크톱 `[~]`). | `apps/desktop/src/main/services/llm-prompts.ts`, `VoiceModeService.ts:779`·`:827-870`, `ChainService.ts:196`, `src/main/ipc/llm-handlers.ts:96`, `src/renderer/pages/CommandsPage.tsx:180-205` | 사용자가 `run-desktop.bat` 로 앱을 띄워 (1) 명령 페이지에서 내장 프리셋(번역/요약/전문 리라이트/코드 설명/자유 프롬프트)을 활성화한 뒤 받아쓰기, (2) 명령 팝업에서 선택 후 받아쓰기, (3) 음성 키워드로 명령 호출, (4) 체인 실행, (5) 명령 페이지 파이프라인 벤치를 각각 돌려 **지시문 문구가 아니라 처리 결과가** 삽입되는지 확인한다. GAP-INFRA-06 의 ABI 전환 스크립트가 생기면 usecase 4종을 함께 돌린다. |
| GAP-INFRA-06 | Dev env | `better-sqlite3` 네이티브 ABI 가 **앱 실행과 로컬 테스트에서 서로 다른 값을 요구**한다. Electron 33 은 ABI 130, 호스트 Node 23 은 ABI 131 이라 한쪽에 맞추면 다른 쪽이 깨진다. 2026-09-21 실측: `electron-rebuild -f -w better-sqlite3` 직후 vitest 가 `366 failed / 948 passed` 로 무너졌고, 리빌드 전에는 `1311 passed` 였다. 같은 날 확인한 현재 워크스페이스는 Node ABI 쪽(호스트 `node -e "require('better-sqlite3')"` 성공)이라 테스트는 돌고 앱 실행에는 재리빌드가 필요하다. **배포 차단 이슈가 아니다** — `node_modules/` 는 gitignore(`.gitignore:1`)이고 패키징 경로는 `scripts/ci/verify-native-abi.mjs` 가 이미 막는다(GAP-REL-07 `[x]`). 순수하게 로컬 개발 환경 전환 비용 문제다. **다만 전환 비용으로 끝나지 않는다 — 검증을 가린다.** Electron ABI 쪽으로 리빌드된 상태에서는 `tests/red/*.usecase.test.ts` 가 DB 생성 단계에서 먼저 죽어 그 안의 케이스가 통과도 실패도 하지 않는다. 2026-09-21 LLM 지시문 수정(`9c2b4d4`)이 그 사례다: 전체 실행이 `366 failed / 994 passed (1360)` 였고 실패 366건 중 365건이 이 ABI 로 죽은 usecase 파일들인데, 하필 `instruction`·`chain`·`voice`·`config` usecase 가 그 수정의 직접 영향 범위였다(GAP-LLM-02). 참고로 같은 날 clean tree 베이스라인은 `366 failed / 948 passed (1314)` 로 실패 수가 동일해 신규 실패는 0건이다. | `scripts/ci/verify-native-abi.mjs`, `scripts/ci/fix-native-abi.mjs`, `package.json`(현재 리빌드용 스크립트 없음), `apps/desktop/tests/red/*.usecase.test.ts` | 두 ABI 를 오가는 npm 스크립트를 둔다(예: `rebuild:app` = Electron ABI, `rebuild:test` = Node ABI). 지금은 전환 방법이 문서화도 스크립트화도 되어 있지 않아 매번 수동으로 알아내야 한다. |