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
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
|
|
|||
|
|
@ -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 }}>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue