feat(desktop): SaaS [4] 온보딩 — Cloud Sync 티저 step + AppConfig 타입 정리 (빅뱅 Phase 2)
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가 자동 다운로드
This commit is contained in:
parent
1a01cea33c
commit
f1321df83c
8 changed files with 130 additions and 13 deletions
|
|
@ -95,6 +95,7 @@ const CONFIG_DEFAULTS: AppConfig = {
|
||||||
supabaseUrl: '',
|
supabaseUrl: '',
|
||||||
supabaseAnonKey: '',
|
supabaseAnonKey: '',
|
||||||
cloudSyncLastAt: null,
|
cloudSyncLastAt: null,
|
||||||
|
onboardingCompleted: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
let store: ElectronStore<AppConfig> | null = null
|
let store: ElectronStore<AppConfig> | null = null
|
||||||
|
|
|
||||||
|
|
@ -63,14 +63,11 @@ export function AppLayout(): React.ReactElement {
|
||||||
const [licenseModalOpen, setLicenseModalOpen] = useState(false)
|
const [licenseModalOpen, setLicenseModalOpen] = useState(false)
|
||||||
const [currentTier, setCurrentTier] = useState<LicenseTier>('free')
|
const [currentTier, setCurrentTier] = useState<LicenseTier>('free')
|
||||||
|
|
||||||
// 첫 실행 감지
|
// 첫 실행 감지 — 로컬 모드 entry point에서 온보딩 자동 표시
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.electronAPI.config.getAll().then((r) => {
|
window.electronAPI.config.getAll().then((r) => {
|
||||||
if (r.success) {
|
if (r.success && !r.data.onboardingCompleted) {
|
||||||
const cfg = r.data as Record<string, unknown>
|
setOnboardingOpen(true)
|
||||||
if (!cfg['onboardingCompleted']) {
|
|
||||||
setOnboardingOpen(true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import MicIcon from '@mui/icons-material/Mic'
|
||||||
import KeyboardIcon from '@mui/icons-material/Keyboard'
|
import KeyboardIcon from '@mui/icons-material/Keyboard'
|
||||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
|
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
|
||||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
|
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
|
||||||
|
import CloudIcon from '@mui/icons-material/Cloud'
|
||||||
import { d3roPalette, d3roFontMono, d3roShadow } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roFontMono, d3roShadow } from '@d3ro/ui/theme'
|
||||||
import { Led } from '@d3ro/ui/components/ds'
|
import { Led } from '@d3ro/ui/components/ds'
|
||||||
import { HotkeyRecordModal } from './HotkeyRecordModal'
|
import { HotkeyRecordModal } from './HotkeyRecordModal'
|
||||||
|
|
@ -29,11 +30,13 @@ interface OnboardingModalProps {
|
||||||
|
|
||||||
export function OnboardingModal({ open, onClose }: OnboardingModalProps): React.ReactElement {
|
export function OnboardingModal({ open, onClose }: OnboardingModalProps): React.ReactElement {
|
||||||
const { t } = useI18n()
|
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<AudioDevice[]>([])
|
const [devices, setDevices] = useState<AudioDevice[]>([])
|
||||||
const [selectedDevice, setSelectedDevice] = useState('default')
|
const [selectedDevice, setSelectedDevice] = useState('default')
|
||||||
const [hotkeyBinding, setHotkeyBinding] = useState<HotkeyBinding | null>(null)
|
const [hotkeyBinding, setHotkeyBinding] = useState<HotkeyBinding | null>(null)
|
||||||
const [hotkeyModalOpen, setHotkeyModalOpen] = useState(false)
|
const [hotkeyModalOpen, setHotkeyModalOpen] = useState(false)
|
||||||
|
const [saasMode, setSaasMode] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return
|
if (!open) return
|
||||||
|
|
@ -44,14 +47,28 @@ export function OnboardingModal({ open, onClose }: OnboardingModalProps): React.
|
||||||
window.electronAPI.hotkey.getDictationShortcut().then((r) => {
|
window.electronAPI.hotkey.getDictationShortcut().then((r) => {
|
||||||
if (r.success && r.data) setHotkeyBinding(r.data)
|
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])
|
}, [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()
|
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) => {
|
const handleHotkeySave = (binding: HotkeyBinding) => {
|
||||||
setHotkeyBinding(binding)
|
setHotkeyBinding(binding)
|
||||||
window.electronAPI.hotkey.setDictationShortcut({ binding })
|
window.electronAPI.hotkey.setDictationShortcut({ binding })
|
||||||
|
|
@ -234,13 +251,70 @@ export function OnboardingModal({ open, onClose }: OnboardingModalProps): React.
|
||||||
</Box>
|
</Box>
|
||||||
<Stack direction="row" justifyContent="space-between">
|
<Stack direction="row" justifyContent="space-between">
|
||||||
<Button onClick={() => setStep(2)} sx={{ color: d3roPalette.text.inactive }}>{t('onboarding.back')}</Button>
|
<Button onClick={() => setStep(2)} sx={{ color: d3roPalette.text.inactive }}>{t('onboarding.back')}</Button>
|
||||||
<Button variant="contained" onClick={() => setStep(4)}>{t('onboarding.next')}</Button>
|
<Button variant="contained" onClick={nextAfterOllama}>{t('onboarding.next')}</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Step 4: 완료 */}
|
{/* Step 4: Cloud Sync (선택, saasMode에서만 노출) */}
|
||||||
{step === 4 && (
|
{step === 4 && (
|
||||||
|
<Box>
|
||||||
|
<Stack direction="row" alignItems="center" gap={1} mb={3}>
|
||||||
|
<CloudIcon sx={{ color: d3roPalette.accent.amber }} />
|
||||||
|
<Typography sx={{ fontFamily: d3roFontMono, fontWeight: 700, fontSize: '14px' }}>
|
||||||
|
{t('onboarding.cloud.title')}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Typography variant="body2" sx={{ color: d3roPalette.text.secondary, mb: 2 }}>
|
||||||
|
{t('onboarding.cloud.tagline')}
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
p: 2,
|
||||||
|
borderRadius: '10px',
|
||||||
|
bgcolor: d3roPalette.bg.inset,
|
||||||
|
boxShadow: d3roShadow.inset,
|
||||||
|
mb: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack spacing={1.25}>
|
||||||
|
{[
|
||||||
|
t('onboarding.cloud.benefit1'),
|
||||||
|
t('onboarding.cloud.benefit2'),
|
||||||
|
t('onboarding.cloud.benefit3'),
|
||||||
|
].map((b, idx) => (
|
||||||
|
<Stack key={idx} direction="row" spacing={1} alignItems="center">
|
||||||
|
<Led color="green" size={8} />
|
||||||
|
<Typography sx={{ fontSize: '12px', color: d3roPalette.text.primary }}>
|
||||||
|
{b}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: '11px',
|
||||||
|
color: d3roPalette.text.inactive,
|
||||||
|
mb: 3,
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('onboarding.cloud.signInLater')}
|
||||||
|
</Typography>
|
||||||
|
<Stack direction="row" justifyContent="space-between">
|
||||||
|
<Button onClick={backToOllama} sx={{ color: d3roPalette.text.inactive }}>
|
||||||
|
{t('onboarding.back')}
|
||||||
|
</Button>
|
||||||
|
<Button variant="contained" onClick={() => setStep(5)}>
|
||||||
|
{t('onboarding.next')}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step 5: 완료 */}
|
||||||
|
{step === 5 && (
|
||||||
<Box sx={{ textAlign: 'center', py: 3 }}>
|
<Box sx={{ textAlign: 'center', py: 3 }}>
|
||||||
<CheckCircleIcon sx={{ fontSize: 48, color: d3roPalette.tag.green, mb: 2 }} />
|
<CheckCircleIcon sx={{ fontSize: 48, color: d3roPalette.tag.green, mb: 2 }} />
|
||||||
<Typography sx={{ fontFamily: d3roFontMono, fontSize: '18px', fontWeight: 700, mb: 1 }}>
|
<Typography sx={{ fontFamily: d3roFontMono, fontSize: '18px', fontWeight: 700, mb: 1 }}>
|
||||||
|
|
|
||||||
|
|
@ -849,8 +849,8 @@ export function SettingsModal({ open, onClose }: SettingsModalProps): React.Reac
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
window.electronAPI.config.set({
|
window.electronAPI.config.set({
|
||||||
key: 'onboardingCompleted' as keyof import('@d3ro/core/types').AppConfig,
|
key: 'onboardingCompleted',
|
||||||
value: false as never,
|
value: false,
|
||||||
})
|
})
|
||||||
onClose()
|
onClose()
|
||||||
setTimeout(() => window.location.reload(), 300)
|
setTimeout(() => window.location.reload(), 300)
|
||||||
|
|
|
||||||
|
|
@ -208,6 +208,37 @@ OAuth provider도 아직 Supabase에 설정 안 된 상태. 강제 게이트는
|
||||||
- `~/Library/Application Support/d3ro-voice/users/${uuid}/d3ro.db` 파일 생성 확인
|
- `~/Library/Application Support/d3ro-voice/users/${uuid}/d3ro.db` 파일 생성 확인
|
||||||
- 기존 `d3ro-voice.db`가 있는 환경에서 archive rename 동작 확인
|
- 기존 `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)
|
### SaaS [3.5] 로컬 모드 = entry point (Phase 1.5, 2026-04-11)
|
||||||
|
|
||||||
> 비즈니스 모델: 무료 로컬 모드 = onboarding funnel 입구 (킬러 피처),
|
> 비즈니스 모델: 무료 로컬 모드 = onboarding funnel 입구 (킬러 피처),
|
||||||
|
|
|
||||||
|
|
@ -394,6 +394,8 @@ export interface AppConfig {
|
||||||
supabaseAnonKey: string
|
supabaseAnonKey: string
|
||||||
/** V2-4: 마지막 동기화 epoch ms */
|
/** V2-4: 마지막 동기화 epoch ms */
|
||||||
cloudSyncLastAt: number | null
|
cloudSyncLastAt: number | null
|
||||||
|
/** 빅뱅 Phase 2: 첫 실행 온보딩 완료 여부 (로컬 모드 entry point) */
|
||||||
|
onboardingCompleted: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfigGetParams {
|
export interface ConfigGetParams {
|
||||||
|
|
|
||||||
|
|
@ -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.desc": "Ollama is required for LLM post-processing like translation and summarization. Voice dictation works without it.",
|
||||||
"onboarding.ollama.download": "Download Ollama",
|
"onboarding.ollama.download": "Download Ollama",
|
||||||
"onboarding.ollama.modelHint": "After installation, download a model from your terminal",
|
"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.title": "You're all set!",
|
||||||
"onboarding.done.descWithKey": "Hold {{key}} and speak — your voice will be converted to text.",
|
"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.",
|
"onboarding.done.descNoKey": "Set a shortcut in Settings to start voice dictation.",
|
||||||
|
|
|
||||||
|
|
@ -160,6 +160,12 @@
|
||||||
"onboarding.ollama.desc": "번역, 요약 등 LLM 후처리를 사용하려면 Ollama가 필요합니다. 음성 받아쓰기 자체는 Ollama 없이도 동작합니다.",
|
"onboarding.ollama.desc": "번역, 요약 등 LLM 후처리를 사용하려면 Ollama가 필요합니다. 음성 받아쓰기 자체는 Ollama 없이도 동작합니다.",
|
||||||
"onboarding.ollama.download": "Ollama 다운로드",
|
"onboarding.ollama.download": "Ollama 다운로드",
|
||||||
"onboarding.ollama.modelHint": "설치 후 터미널에서 모델을 다운로드하세요",
|
"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.title": "설정 완료!",
|
||||||
"onboarding.done.descWithKey": "{{key}} 키를 누르고 말하면 음성이 텍스트로 변환됩니다.",
|
"onboarding.done.descWithKey": "{{key}} 키를 누르고 말하면 음성이 텍스트로 변환됩니다.",
|
||||||
"onboarding.done.descNoKey": "설정에서 단축키를 지정하면 음성 받아쓰기를 시작할 수 있습니다.",
|
"onboarding.done.descNoKey": "설정에서 단축키를 지정하면 음성 받아쓰기를 시작할 수 있습니다.",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue