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:
윤찬 2026-04-11 10:16:06 +09:00
parent 1a01cea33c
commit f1321df83c
8 changed files with 130 additions and 13 deletions

View file

@ -63,14 +63,11 @@ export function AppLayout(): React.ReactElement {
const [licenseModalOpen, setLicenseModalOpen] = useState(false)
const [currentTier, setCurrentTier] = useState<LicenseTier>('free')
// 첫 실행 감지
// 첫 실행 감지 — 로컬 모드 entry point에서 온보딩 자동 표시
useEffect(() => {
window.electronAPI.config.getAll().then((r) => {
if (r.success) {
const cfg = r.data as Record<string, unknown>
if (!cfg['onboardingCompleted']) {
setOnboardingOpen(true)
}
if (r.success && !r.data.onboardingCompleted) {
setOnboardingOpen(true)
}
})
}, [])

View file

@ -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<AudioDevice[]>([])
const [selectedDevice, setSelectedDevice] = useState('default')
const [hotkeyBinding, setHotkeyBinding] = useState<HotkeyBinding | null>(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.
</Box>
<Stack direction="row" justifyContent="space-between">
<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>
</Box>
)}
{/* Step 4: 완료 */}
{/* Step 4: Cloud Sync (선택, saasMode에서만 노출) */}
{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 }}>
<CheckCircleIcon sx={{ fontSize: 48, color: d3roPalette.tag.green, mb: 2 }} />
<Typography sx={{ fontFamily: d3roFontMono, fontSize: '18px', fontWeight: 700, mb: 1 }}>

View file

@ -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)