From f1321df83ccb27704b2ff084a3eebce49fd08377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A4=EC=B0=AC?= Date: Sat, 11 Apr 2026 10:16:06 +0900 Subject: [PATCH] =?UTF-8?q?feat(desktop):=20SaaS=20[4]=20=EC=98=A8?= =?UTF-8?q?=EB=B3=B4=EB=94=A9=20=E2=80=94=20Cloud=20Sync=20=ED=8B=B0?= =?UTF-8?q?=EC=A0=80=20step=20+=20AppConfig=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC=20(=EB=B9=85=EB=B1=85=20Phase=202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AppConfig 정식화: - packages/core types.ts: onboardingCompleted: boolean 추가 - ConfigService CONFIG_DEFAULTS 동기화 - 타입 캐스팅 3곳 제거 (AppLayout/OnboardingModal/SettingsModal) OnboardingModal 6 step 확장: - 기존: 환영 → 마이크 → 핫키 → Ollama → 완료 - 신규: 환영 → 마이크 → 핫키 → Ollama → Cloud Sync(선택) → 완료 - saasMode === true 일 때만 Cloud Sync step 노출 - Step 내용: tagline + 3 benefit (sync/premium LLM/team share) + "설정에서 언제든지 로그인" 안내 - 온보딩 scope 안에서는 로그인 버튼 미노출 — 부담 제로 유지 (Settings의 CloudSyncSection이 유일한 OAuth 진입점) i18n: - ko/en에 onboarding.cloud.* 6개 키 추가 - 나머지 10개 locale은 추후 일괄 (Phase 2 scope 밖) 의도적 제외: - Supabase profiles.onboarded_at — OAuth provider 설정 전까진 의미 없음 - OAuth 후 환영 페이지 — Phase 4 티어 enforcement와 함께 - STT 모델 선택 — sidecar가 자동 다운로드 --- .../src/main/services/ConfigService.ts | 1 + .../src/renderer/components/AppLayout.tsx | 9 +- .../renderer/components/OnboardingModal.tsx | 84 +++++++++++++++++-- .../src/renderer/components/SettingsModal.tsx | 4 +- memory/project_status.md | 31 +++++++ packages/core/src/types.ts | 2 + packages/i18n/src/locales/en.json | 6 ++ packages/i18n/src/locales/ko.json | 6 ++ 8 files changed, 130 insertions(+), 13 deletions(-) diff --git a/apps/desktop/src/main/services/ConfigService.ts b/apps/desktop/src/main/services/ConfigService.ts index a6b38b3..51fc521 100644 --- a/apps/desktop/src/main/services/ConfigService.ts +++ b/apps/desktop/src/main/services/ConfigService.ts @@ -95,6 +95,7 @@ const CONFIG_DEFAULTS: AppConfig = { supabaseUrl: '', supabaseAnonKey: '', cloudSyncLastAt: null, + onboardingCompleted: false, } let store: ElectronStore | null = null diff --git a/apps/desktop/src/renderer/components/AppLayout.tsx b/apps/desktop/src/renderer/components/AppLayout.tsx index e91bc34..0db4985 100644 --- a/apps/desktop/src/renderer/components/AppLayout.tsx +++ b/apps/desktop/src/renderer/components/AppLayout.tsx @@ -63,14 +63,11 @@ export function AppLayout(): React.ReactElement { const [licenseModalOpen, setLicenseModalOpen] = useState(false) const [currentTier, setCurrentTier] = useState('free') - // 첫 실행 감지 + // 첫 실행 감지 — 로컬 모드 entry point에서 온보딩 자동 표시 useEffect(() => { window.electronAPI.config.getAll().then((r) => { - if (r.success) { - const cfg = r.data as Record - if (!cfg['onboardingCompleted']) { - setOnboardingOpen(true) - } + if (r.success && !r.data.onboardingCompleted) { + setOnboardingOpen(true) } }) }, []) diff --git a/apps/desktop/src/renderer/components/OnboardingModal.tsx b/apps/desktop/src/renderer/components/OnboardingModal.tsx index badca8f..df6fd13 100644 --- a/apps/desktop/src/renderer/components/OnboardingModal.tsx +++ b/apps/desktop/src/renderer/components/OnboardingModal.tsx @@ -15,6 +15,7 @@ import MicIcon from '@mui/icons-material/Mic' import KeyboardIcon from '@mui/icons-material/Keyboard' import CheckCircleIcon from '@mui/icons-material/CheckCircle' import OpenInNewIcon from '@mui/icons-material/OpenInNew' +import CloudIcon from '@mui/icons-material/Cloud' import { d3roPalette, d3roFontMono, d3roShadow } from '@d3ro/ui/theme' import { Led } from '@d3ro/ui/components/ds' import { HotkeyRecordModal } from './HotkeyRecordModal' @@ -29,11 +30,13 @@ interface OnboardingModalProps { export function OnboardingModal({ open, onClose }: OnboardingModalProps): React.ReactElement { const { t } = useI18n() - const [step, setStep] = useState(0) // 0: 환영, 1: 마이크, 2: 핫키, 3: Ollama, 4: 완료 + // 0: 환영, 1: 마이크, 2: 핫키, 3: Ollama, 4: Cloud Sync(선택), 5: 완료 + const [step, setStep] = useState(0) const [devices, setDevices] = useState([]) const [selectedDevice, setSelectedDevice] = useState('default') const [hotkeyBinding, setHotkeyBinding] = useState(null) const [hotkeyModalOpen, setHotkeyModalOpen] = useState(false) + const [saasMode, setSaasMode] = useState(false) useEffect(() => { if (!open) return @@ -44,14 +47,28 @@ export function OnboardingModal({ open, onClose }: OnboardingModalProps): React. window.electronAPI.hotkey.getDictationShortcut().then((r) => { if (r.success && r.data) setHotkeyBinding(r.data) }) + // Cloud Sync step은 빌드 타임 Supabase env가 있을 때만 의미가 있음 + window.electronAPI.cloudSync.getState().then((r) => { + if (r.success) setSaasMode(r.data.saasMode) + }) }, [open]) - const handleFinish = () => { + const handleFinish = (): void => { // 온보딩 완료 플래그 저장 - window.electronAPI.config.set({ key: 'onboardingCompleted' as keyof import('@d3ro/core/types').AppConfig, value: true as never }) + window.electronAPI.config.set({ key: 'onboardingCompleted', value: true }) onClose() } + // Ollama step(3) 다음 — saasMode면 Cloud Sync step(4), 아니면 바로 완료(5)로 + const nextAfterOllama = (): void => { + setStep(saasMode ? 4 : 5) + } + + // Cloud Sync step에서 Back 누르면 Ollama(3)로 복귀 + const backToOllama = (): void => { + setStep(3) + } + const handleHotkeySave = (binding: HotkeyBinding) => { setHotkeyBinding(binding) window.electronAPI.hotkey.setDictationShortcut({ binding }) @@ -234,13 +251,70 @@ export function OnboardingModal({ open, onClose }: OnboardingModalProps): React. - + )} - {/* Step 4: 완료 */} + {/* Step 4: Cloud Sync (선택, saasMode에서만 노출) */} {step === 4 && ( + + + + + {t('onboarding.cloud.title')} + + + + {t('onboarding.cloud.tagline')} + + + + {[ + t('onboarding.cloud.benefit1'), + t('onboarding.cloud.benefit2'), + t('onboarding.cloud.benefit3'), + ].map((b, idx) => ( + + + + {b} + + + ))} + + + + {t('onboarding.cloud.signInLater')} + + + + + + + )} + + {/* Step 5: 완료 */} + {step === 5 && ( diff --git a/apps/desktop/src/renderer/components/SettingsModal.tsx b/apps/desktop/src/renderer/components/SettingsModal.tsx index ca5457d..9fe9215 100644 --- a/apps/desktop/src/renderer/components/SettingsModal.tsx +++ b/apps/desktop/src/renderer/components/SettingsModal.tsx @@ -849,8 +849,8 @@ export function SettingsModal({ open, onClose }: SettingsModalProps): React.Reac size="small" onClick={() => { window.electronAPI.config.set({ - key: 'onboardingCompleted' as keyof import('@d3ro/core/types').AppConfig, - value: false as never, + key: 'onboardingCompleted', + value: false, }) onClose() setTimeout(() => window.location.reload(), 300) diff --git a/memory/project_status.md b/memory/project_status.md index b715e19..182818d 100644 --- a/memory/project_status.md +++ b/memory/project_status.md @@ -208,6 +208,37 @@ OAuth provider도 아직 Supabase에 설정 안 된 상태. 강제 게이트는 - `~/Library/Application Support/d3ro-voice/users/${uuid}/d3ro.db` 파일 생성 확인 - 기존 `d3ro-voice.db`가 있는 환경에서 archive rename 동작 확인 +### SaaS [4] 회원가입 / 온보딩 (Phase 2, 2026-04-11) + +> 기존 `OnboardingModal`을 로컬 모드 entry point에 맞게 정리 + Cloud Sync 티저 step 추가. + +- **AppConfig 타입 정식화** + - `packages/core/src/types.ts` `AppConfig`에 `onboardingCompleted: boolean` 추가 + - `ConfigService.CONFIG_DEFAULTS`에 `onboardingCompleted: false` 추가 + - 타입 캐스팅(`as keyof AppConfig`, `as never`) 3곳 제거: + - `AppLayout.tsx` 첫 실행 감지: `cfg.onboardingCompleted` 직접 접근 + - `OnboardingModal.tsx` handleFinish: `key: 'onboardingCompleted'` 리터럴 + - `SettingsModal.tsx` "온보딩 다시 보기" 버튼: 동일 +- **온보딩 Step 확장 (5 → 6 step)** + - 기존 flow: 환영 → 마이크 → 핫키 → Ollama → 완료 + - 새 flow: 환영 → 마이크 → 핫키 → Ollama → **Cloud Sync(선택)** → 완료 + - Cloud Sync step은 `saasMode === true`일 때만 노출: + - `getState()`로 saasMode 조회 + - Ollama "다음" → `nextAfterOllama()`: saasMode면 step 4, 아니면 step 5로 분기 + - Cloud Sync step 내용: tagline + 3개 benefit (동기화/Premium LLM/팀공유) + "설정에서 언제든지 로그인" 안내. **로그인 버튼 없음 — onboarding scope 밖** (Settings의 CloudSyncSection이 유일한 OAuth 진입점). +- **i18n ko/en 키 추가** (`packages/i18n/src/locales/`): + - `onboarding.cloud.title` / `tagline` / `benefit1~3` / `signInLater` + - 나머지 10개 locale은 fallback (Phase 2 scope 밖, 추후 일괄) +- **검증** + - desktop `tsc --noEmit` ✅ + - desktop `npm run build` ✅ (renderer 2,129kB, +3kB) + - 수동 검증 대기: Settings → About → "온보딩 다시 보기" 눌러서 신규 시퀀스 확인 +- **Phase 2에서 의도적으로 뺀 것** (추후 사이클) + - Supabase `profiles.onboarded_at` 컬럼 — OAuth provider(Google) 설정 전까진 의미 없음 + - OAuth 로그인 후 "환영" 페이지 — provider 연결 후 Phase 4와 함께 + - STT 모델 선택 step — faster-whisper sidecar가 첫 사용 시 자동 다운로드하므로 불필요 + - 온보딩 키의 나머지 10개 locale 번역 + ### SaaS [3.5] 로컬 모드 = entry point (Phase 1.5, 2026-04-11) > 비즈니스 모델: 무료 로컬 모드 = onboarding funnel 입구 (킬러 피처), diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 32b95f8..d827823 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -394,6 +394,8 @@ export interface AppConfig { supabaseAnonKey: string /** V2-4: 마지막 동기화 epoch ms */ cloudSyncLastAt: number | null + /** 빅뱅 Phase 2: 첫 실행 온보딩 완료 여부 (로컬 모드 entry point) */ + onboardingCompleted: boolean } export interface ConfigGetParams { diff --git a/packages/i18n/src/locales/en.json b/packages/i18n/src/locales/en.json index 14a551d..b222e3f 100644 --- a/packages/i18n/src/locales/en.json +++ b/packages/i18n/src/locales/en.json @@ -159,6 +159,12 @@ "onboarding.ollama.desc": "Ollama is required for LLM post-processing like translation and summarization. Voice dictation works without it.", "onboarding.ollama.download": "Download Ollama", "onboarding.ollama.modelHint": "After installation, download a model from your terminal", + "onboarding.cloud.title": "Cloud Sync (Optional)", + "onboarding.cloud.tagline": "D3RO works great offline. Sign in whenever you want to unlock more.", + "onboarding.cloud.benefit1": "Sync history & meetings across all your devices", + "onboarding.cloud.benefit2": "Premium LLMs (Claude, GPT-4 class)", + "onboarding.cloud.benefit3": "Team sharing · backups · web app access", + "onboarding.cloud.signInLater": "Sign in later from Settings", "onboarding.done.title": "You're all set!", "onboarding.done.descWithKey": "Hold {{key}} and speak — your voice will be converted to text.", "onboarding.done.descNoKey": "Set a shortcut in Settings to start voice dictation.", diff --git a/packages/i18n/src/locales/ko.json b/packages/i18n/src/locales/ko.json index 016d029..7e6375d 100644 --- a/packages/i18n/src/locales/ko.json +++ b/packages/i18n/src/locales/ko.json @@ -160,6 +160,12 @@ "onboarding.ollama.desc": "번역, 요약 등 LLM 후처리를 사용하려면 Ollama가 필요합니다. 음성 받아쓰기 자체는 Ollama 없이도 동작합니다.", "onboarding.ollama.download": "Ollama 다운로드", "onboarding.ollama.modelHint": "설치 후 터미널에서 모델을 다운로드하세요", + "onboarding.cloud.title": "Cloud Sync (선택)", + "onboarding.cloud.tagline": "로컬만으로도 충분히 작동합니다. 원할 때 로그인하면 더 많은 기능이 잠금 해제됩니다.", + "onboarding.cloud.benefit1": "여러 기기에서 히스토리 · 회의록 자동 동기화", + "onboarding.cloud.benefit2": "Premium LLM (Claude, GPT-4) 사용", + "onboarding.cloud.benefit3": "팀 공유 · 백업 · 웹 앱 접속", + "onboarding.cloud.signInLater": "설정에서 언제든지 로그인", "onboarding.done.title": "설정 완료!", "onboarding.done.descWithKey": "{{key}} 키를 누르고 말하면 음성이 텍스트로 변환됩니다.", "onboarding.done.descNoKey": "설정에서 단축키를 지정하면 음성 받아쓰기를 시작할 수 있습니다.",