refactor: saasMode 분기 제거 — 항상 SaaS(OAuth only)로 동작
Mac/Windows 환경 불일치 해소. BUILD_TIME 상수, CONFIGURE 채널, 개발자 모드 UI 전부 제거. Supabase 설정은 supabase-config.ts SSOT.
This commit is contained in:
parent
feb82abbf7
commit
541f04d02b
12 changed files with 566 additions and 9587 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
import { resolve } from 'path'
|
import { resolve } from 'path'
|
||||||
import { defineConfig, externalizeDepsPlugin, loadEnv } from 'electron-vite'
|
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
const sharedAlias = {
|
const sharedAlias = {
|
||||||
|
|
@ -10,34 +10,14 @@ const sharedAlias = {
|
||||||
|
|
||||||
const workspaceExclude = ['@d3ro/core', '@d3ro/ui', '@d3ro/i18n']
|
const workspaceExclude = ['@d3ro/core', '@d3ro/ui', '@d3ro/i18n']
|
||||||
|
|
||||||
// SaaS 데스크톱 클라이언트: Supabase 연결 정보를 빌드 타임에 박는다.
|
export default defineConfig({
|
||||||
// `apps/desktop/.env` (committed shared defaults) + `.env.local` (gitignored, 개인/비공개 키)
|
|
||||||
// 우선순위는 vite/electron-vite의 dotenv 로드 규칙: .env.local > .env.[mode] > .env
|
|
||||||
// 환경 변수에 값이 없으면 빈 문자열로 박혀, 런타임에 ConfigService가 fallback(설정 UI 입력)으로 동작.
|
|
||||||
function loadSupabaseEnv(mode: string): { url: string; anonKey: string } {
|
|
||||||
const env = loadEnv(mode, process.cwd(), 'D3RO_')
|
|
||||||
return {
|
|
||||||
url: env.D3RO_SUPABASE_URL ?? '',
|
|
||||||
anonKey: env.D3RO_SUPABASE_ANON_KEY ?? ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineConfig(({ mode }) => {
|
|
||||||
const supabase = loadSupabaseEnv(mode)
|
|
||||||
const supabaseDefine = {
|
|
||||||
'process.env.D3RO_SUPABASE_URL': JSON.stringify(supabase.url),
|
|
||||||
'process.env.D3RO_SUPABASE_ANON_KEY': JSON.stringify(supabase.anonKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
main: {
|
main: {
|
||||||
plugins: [
|
plugins: [
|
||||||
externalizeDepsPlugin({
|
externalizeDepsPlugin({
|
||||||
exclude: ['electron-store', ...workspaceExclude]
|
exclude: ['electron-store', ...workspaceExclude]
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
resolve: { alias: sharedAlias },
|
resolve: { alias: sharedAlias }
|
||||||
define: supabaseDefine
|
|
||||||
},
|
},
|
||||||
preload: {
|
preload: {
|
||||||
plugins: [externalizeDepsPlugin({ exclude: workspaceExclude })],
|
plugins: [externalizeDepsPlugin({ exclude: workspaceExclude })],
|
||||||
|
|
@ -82,5 +62,4 @@ export default defineConfig(({ mode }) => {
|
||||||
resolve: { alias: sharedAlias },
|
resolve: { alias: sharedAlias },
|
||||||
plugins: [react()]
|
plugins: [react()]
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
# apps/desktop SaaS 빌드 타임 환경변수 템플릿.
|
|
||||||
# 실제 값은 .env.local로 복사 (gitignored).
|
|
||||||
#
|
|
||||||
# cp env.example .env.local
|
|
||||||
#
|
|
||||||
# Supabase 프로젝트 — 데스크톱 클라이언트가 연결할 SaaS 인스턴스.
|
|
||||||
# 빌드 시점에 main 번들에 inline 박혀 사용자가 Settings에서 입력할 필요 없음.
|
|
||||||
D3RO_SUPABASE_URL=https://your-project-ref.supabase.co
|
|
||||||
D3RO_SUPABASE_ANON_KEY=your-anon-or-publishable-key
|
|
||||||
|
|
@ -6,7 +6,6 @@ import { IPC_CHANNELS } from '@d3ro/core/ipc-channels'
|
||||||
import { D3ROError, ErrorCode } from '@d3ro/core/errors'
|
import { D3ROError, ErrorCode } from '@d3ro/core/errors'
|
||||||
import type { IPCResult } from '@d3ro/core/errors'
|
import type { IPCResult } from '@d3ro/core/errors'
|
||||||
import { getCloudSyncService } from '../services/CloudSyncService'
|
import { getCloudSyncService } from '../services/CloudSyncService'
|
||||||
import { configSet } from '../services/ConfigService'
|
|
||||||
import { getLogger } from '../services/LoggerService'
|
import { getLogger } from '../services/LoggerService'
|
||||||
|
|
||||||
const logger = getLogger('cloud-sync-handlers')
|
const logger = getLogger('cloud-sync-handlers')
|
||||||
|
|
@ -15,11 +14,6 @@ interface SignInParams {
|
||||||
provider: 'google' | 'github'
|
provider: 'google' | 'github'
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ConfigureParams {
|
|
||||||
url: string
|
|
||||||
anonKey: string
|
|
||||||
}
|
|
||||||
|
|
||||||
function ok<T>(data: T): IPCResult<T> {
|
function ok<T>(data: T): IPCResult<T> {
|
||||||
return { success: true, data }
|
return { success: true, data }
|
||||||
}
|
}
|
||||||
|
|
@ -46,18 +40,6 @@ export function registerCloudSyncHandlers(): void {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.CLOUD_SYNC.CONFIGURE, async (_e, params: ConfigureParams) => {
|
|
||||||
try {
|
|
||||||
configSet('supabaseUrl' as never, params.url as never)
|
|
||||||
configSet('supabaseAnonKey' as never, params.anonKey as never)
|
|
||||||
// CloudSyncService 재초기화
|
|
||||||
await sync.init()
|
|
||||||
return ok(sync.getState())
|
|
||||||
} catch (e) {
|
|
||||||
return fail(e)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.CLOUD_SYNC.SIGN_IN, async (_e, params: SignInParams) => {
|
ipcMain.handle(IPC_CHANNELS.CLOUD_SYNC.SIGN_IN, async (_e, params: SignInParams) => {
|
||||||
try {
|
try {
|
||||||
await sync.startSignIn(params.provider)
|
await sync.startSignIn(params.provider)
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@ import {
|
||||||
} from '@supabase/supabase-js'
|
} from '@supabase/supabase-js'
|
||||||
import { eq, gt } from 'drizzle-orm'
|
import { eq, gt } from 'drizzle-orm'
|
||||||
import { getLogger } from './LoggerService'
|
import { getLogger } from './LoggerService'
|
||||||
import { configGet, isSupabaseBuildTimeConfigured } from './ConfigService'
|
import { configGet } from './ConfigService'
|
||||||
|
import { SUPABASE_URL, SUPABASE_ANON_KEY } from '@d3ro/core/supabase-config'
|
||||||
import { getDatabase, openForUser, openLocal, closeCurrent } from '../db'
|
import { getDatabase, openForUser, openLocal, closeCurrent } from '../db'
|
||||||
import { history, dictionary, meetingSessions, meetingMemos, meetingDocuments } from '../db/schema'
|
import { history, dictionary, meetingSessions, meetingMemos, meetingDocuments } from '../db/schema'
|
||||||
import { D3ROError, ErrorCode } from '@d3ro/core/errors'
|
import { D3ROError, ErrorCode } from '@d3ro/core/errors'
|
||||||
|
|
@ -41,11 +42,6 @@ interface CloudSyncState {
|
||||||
userEmail: string | null
|
userEmail: string | null
|
||||||
lastSyncAt: number | null
|
lastSyncAt: number | null
|
||||||
syncing: boolean
|
syncing: boolean
|
||||||
/**
|
|
||||||
* SaaS 빌드 타임 모드: Supabase URL/Key가 빌드 시점에 박혀있는지 여부.
|
|
||||||
* true이면 사용자는 OAuth 로그인만 하면 됨 (URL/Key 입력 화면 노출 금지).
|
|
||||||
*/
|
|
||||||
saasMode: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CloudSyncEvents {
|
interface CloudSyncEvents {
|
||||||
|
|
@ -78,15 +74,7 @@ class CloudSyncService extends EventEmitter {
|
||||||
if (this._initialized) return
|
if (this._initialized) return
|
||||||
this._initialized = true
|
this._initialized = true
|
||||||
|
|
||||||
const url = configGet('supabaseUrl') as string | undefined
|
this._client = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
||||||
const anonKey = configGet('supabaseAnonKey') as string | undefined
|
|
||||||
|
|
||||||
if (!url || !anonKey) {
|
|
||||||
logger.info('CloudSync disabled — Supabase URL/key not configured')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this._client = createClient(url, anonKey, {
|
|
||||||
auth: {
|
auth: {
|
||||||
persistSession: false, // 직접 관리
|
persistSession: false, // 직접 관리
|
||||||
autoRefreshToken: true,
|
autoRefreshToken: true,
|
||||||
|
|
@ -486,8 +474,7 @@ class CloudSyncService extends EventEmitter {
|
||||||
authenticated: this.isAuthenticated(),
|
authenticated: this.isAuthenticated(),
|
||||||
userEmail: this._session?.user?.email ?? null,
|
userEmail: this._session?.user?.email ?? null,
|
||||||
lastSyncAt: this._lastSyncAt,
|
lastSyncAt: this._lastSyncAt,
|
||||||
syncing: this._syncing,
|
syncing: this._syncing
|
||||||
saasMode: isSupabaseBuildTimeConfigured()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,28 +7,6 @@ import { getLogger } from './LoggerService'
|
||||||
|
|
||||||
const logger = getLogger('ConfigService')
|
const logger = getLogger('ConfigService')
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// SaaS 빌드 타임 주입 (electron.vite.config.ts의 define으로 박힘)
|
|
||||||
// ============================================================
|
|
||||||
// D3RO_SUPABASE_URL / D3RO_SUPABASE_ANON_KEY가 비어있지 않으면
|
|
||||||
// 데스크톱 사용자가 Settings에서 직접 입력할 필요 없이 SaaS 인스턴스에 자동 연결.
|
|
||||||
// 빈 문자열이면 기존 동작(사용자 입력) fallback.
|
|
||||||
|
|
||||||
const BUILD_TIME_SUPABASE_URL: string = process.env.D3RO_SUPABASE_URL ?? ''
|
|
||||||
const BUILD_TIME_SUPABASE_ANON_KEY: string = process.env.D3RO_SUPABASE_ANON_KEY ?? ''
|
|
||||||
|
|
||||||
export function isSupabaseBuildTimeConfigured(): boolean {
|
|
||||||
return BUILD_TIME_SUPABASE_URL.length > 0 && BUILD_TIME_SUPABASE_ANON_KEY.length > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getBuildTimeSupabaseUrl(): string {
|
|
||||||
return BUILD_TIME_SUPABASE_URL
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getBuildTimeSupabaseAnonKey(): string {
|
|
||||||
return BUILD_TIME_SUPABASE_ANON_KEY
|
|
||||||
}
|
|
||||||
|
|
||||||
// electron-store v10은 ESM 전용이므로 동적 import 필요
|
// electron-store v10은 ESM 전용이므로 동적 import 필요
|
||||||
interface ElectronStore<T extends Record<string, unknown>> {
|
interface ElectronStore<T extends Record<string, unknown>> {
|
||||||
get<K extends keyof T>(key: K): T[K]
|
get<K extends keyof T>(key: K): T[K]
|
||||||
|
|
@ -118,14 +96,6 @@ export function getConfigService(): ElectronStore<AppConfig> | null {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function configGet<K extends keyof AppConfig>(key: K): AppConfig[K] {
|
export function configGet<K extends keyof AppConfig>(key: K): AppConfig[K] {
|
||||||
// SaaS 빌드 타임 주입은 사용자 설정보다 우선.
|
|
||||||
if (key === 'supabaseUrl' && BUILD_TIME_SUPABASE_URL.length > 0) {
|
|
||||||
return BUILD_TIME_SUPABASE_URL as AppConfig[K]
|
|
||||||
}
|
|
||||||
if (key === 'supabaseAnonKey' && BUILD_TIME_SUPABASE_ANON_KEY.length > 0) {
|
|
||||||
return BUILD_TIME_SUPABASE_ANON_KEY as AppConfig[K]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!store) {
|
if (!store) {
|
||||||
logger.warn(`ConfigService not initialized, returning default for "${key}"`)
|
logger.warn(`ConfigService not initialized, returning default for "${key}"`)
|
||||||
return CONFIG_DEFAULTS[key]
|
return CONFIG_DEFAULTS[key]
|
||||||
|
|
@ -134,15 +104,6 @@ export function configGet<K extends keyof AppConfig>(key: K): AppConfig[K] {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function configSet<K extends keyof AppConfig>(key: K, value: AppConfig[K]): void {
|
export function configSet<K extends keyof AppConfig>(key: K, value: AppConfig[K]): void {
|
||||||
// SaaS 빌드 타임 주입된 값은 변경 차단 (사용자가 잘못된 값으로 덮어쓰는 것 방지).
|
|
||||||
if (
|
|
||||||
(key === 'supabaseUrl' || key === 'supabaseAnonKey') &&
|
|
||||||
isSupabaseBuildTimeConfigured()
|
|
||||||
) {
|
|
||||||
logger.warn(`Refusing to override build-time SaaS config: "${key}"`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!store) {
|
if (!store) {
|
||||||
logger.warn(`ConfigService not initialized, cannot set "${key}"`)
|
logger.warn(`ConfigService not initialized, cannot set "${key}"`)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -715,8 +715,6 @@ const electronAPI = {
|
||||||
lastSyncAt: number | null
|
lastSyncAt: number | null
|
||||||
syncing: boolean
|
syncing: boolean
|
||||||
}>(IPC_CHANNELS.CLOUD_SYNC.GET_STATE),
|
}>(IPC_CHANNELS.CLOUD_SYNC.GET_STATE),
|
||||||
configure: (params: { url: string; anonKey: string }) =>
|
|
||||||
invoke<unknown>(IPC_CHANNELS.CLOUD_SYNC.CONFIGURE, params),
|
|
||||||
signIn: (params: { provider: 'google' | 'github' }) =>
|
signIn: (params: { provider: 'google' | 'github' }) =>
|
||||||
invoke<{ started: boolean }>(IPC_CHANNELS.CLOUD_SYNC.SIGN_IN, params),
|
invoke<{ started: boolean }>(IPC_CHANNELS.CLOUD_SYNC.SIGN_IN, params),
|
||||||
signOut: () => invoke<unknown>(IPC_CHANNELS.CLOUD_SYNC.SIGN_OUT),
|
signOut: () => invoke<unknown>(IPC_CHANNELS.CLOUD_SYNC.SIGN_OUT),
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
// 로그인 상태 / Sync Now 버튼 / 마지막 동기화 / 환경변수 설정
|
// 로그인 상태 / Sync Now 버튼 / 마지막 동기화 / 환경변수 설정
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Box, Button, Stack, TextField, Alert, CircularProgress } from '@mui/material'
|
import { Box, Button, Stack, Alert, CircularProgress } from '@mui/material'
|
||||||
import CloudIcon from '@mui/icons-material/Cloud'
|
import CloudIcon from '@mui/icons-material/Cloud'
|
||||||
import CloudDoneIcon from '@mui/icons-material/CloudDone'
|
import CloudDoneIcon from '@mui/icons-material/CloudDone'
|
||||||
import GoogleIcon from '@mui/icons-material/Google'
|
import GoogleIcon from '@mui/icons-material/Google'
|
||||||
|
|
@ -16,7 +16,6 @@ interface CloudSyncState {
|
||||||
userEmail: string | null
|
userEmail: string | null
|
||||||
lastSyncAt: number | null
|
lastSyncAt: number | null
|
||||||
syncing: boolean
|
syncing: boolean
|
||||||
saasMode: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SyncProgress {
|
interface SyncProgress {
|
||||||
|
|
@ -31,11 +30,8 @@ export function CloudSyncSection(): React.ReactElement {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
userEmail: null,
|
userEmail: null,
|
||||||
lastSyncAt: null,
|
lastSyncAt: null,
|
||||||
syncing: false,
|
syncing: false
|
||||||
saasMode: false
|
|
||||||
})
|
})
|
||||||
const [supabaseUrl, setSupabaseUrl] = useState('')
|
|
||||||
const [anonKey, setAnonKey] = useState('')
|
|
||||||
const [progress, setProgress] = useState<SyncProgress | null>(null)
|
const [progress, setProgress] = useState<SyncProgress | null>(null)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [info, setInfo] = useState<string | null>(null)
|
const [info, setInfo] = useState<string | null>(null)
|
||||||
|
|
@ -46,13 +42,6 @@ export function CloudSyncSection(): React.ReactElement {
|
||||||
void window.electronAPI.cloudSync.getState().then((r) => {
|
void window.electronAPI.cloudSync.getState().then((r) => {
|
||||||
if (r.success) setState(r.data)
|
if (r.success) setState(r.data)
|
||||||
})
|
})
|
||||||
// 저장된 supabase 설정 로드
|
|
||||||
void window.electronAPI.config.get({ key: 'supabaseUrl' as never }).then((r) => {
|
|
||||||
if (r.success && typeof r.data === 'string') setSupabaseUrl(r.data)
|
|
||||||
})
|
|
||||||
void window.electronAPI.config.get({ key: 'supabaseAnonKey' as never }).then((r) => {
|
|
||||||
if (r.success && typeof r.data === 'string') setAnonKey(r.data)
|
|
||||||
})
|
|
||||||
|
|
||||||
const unsubAuth = window.electronAPI.cloudSync.onAuthChanged((payload) => {
|
const unsubAuth = window.electronAPI.cloudSync.onAuthChanged((payload) => {
|
||||||
setState((prev) => ({
|
setState((prev) => ({
|
||||||
|
|
@ -84,21 +73,6 @@ export function CloudSyncSection(): React.ReactElement {
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
async function handleConfigure(): Promise<void> {
|
|
||||||
setError(null)
|
|
||||||
setBusy(true)
|
|
||||||
try {
|
|
||||||
const r = await window.electronAPI.cloudSync.configure({ url: supabaseUrl, anonKey })
|
|
||||||
if (!r.success) {
|
|
||||||
setError(r.error.message)
|
|
||||||
} else {
|
|
||||||
setInfo('Supabase 연결 설정 저장됨')
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setBusy(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSignIn(provider: 'google' | 'github'): Promise<void> {
|
async function handleSignIn(provider: 'google' | 'github'): Promise<void> {
|
||||||
setError(null)
|
setError(null)
|
||||||
setBusy(true)
|
setBusy(true)
|
||||||
|
|
@ -161,8 +135,7 @@ export function CloudSyncSection(): React.ReactElement {
|
||||||
<Box sx={{ ...typoSx('heading'), color: d3roPalette.text.primary }}>Cloud Sync</Box>
|
<Box sx={{ ...typoSx('heading'), color: d3roPalette.text.primary }}>Cloud Sync</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{!state.authenticated && state.saasMode && (
|
{!state.authenticated && (
|
||||||
// SaaS 빌드 타임 모드: URL/Key는 빌드에 박혀있으므로 OAuth 버튼만 노출.
|
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
<Box sx={{ ...typoSx('label'), color: d3roPalette.text.label, mb: 1 }}>OAuth 로그인</Box>
|
<Box sx={{ ...typoSx('label'), color: d3roPalette.text.label, mb: 1 }}>OAuth 로그인</Box>
|
||||||
<Stack direction="row" spacing={1}>
|
<Stack direction="row" spacing={1}>
|
||||||
|
|
@ -186,57 +159,6 @@ export function CloudSyncSection(): React.ReactElement {
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!state.authenticated && !state.saasMode && (
|
|
||||||
// Legacy 개발자 모드: 빌드 타임 env 미설정 → 사용자가 직접 입력 가능.
|
|
||||||
<Stack spacing={2}>
|
|
||||||
<Box sx={{ ...typoSx('label'), color: d3roPalette.text.label }}>
|
|
||||||
SUPABASE 설정 (개발자 모드)
|
|
||||||
</Box>
|
|
||||||
<TextField
|
|
||||||
label="Supabase URL"
|
|
||||||
size="small"
|
|
||||||
value={supabaseUrl}
|
|
||||||
onChange={(e) => setSupabaseUrl(e.target.value)}
|
|
||||||
placeholder="https://your-project.supabase.co"
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label="Anon Key"
|
|
||||||
size="small"
|
|
||||||
value={anonKey}
|
|
||||||
onChange={(e) => setAnonKey(e.target.value)}
|
|
||||||
placeholder="eyJ..."
|
|
||||||
type="password"
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
<Button variant="outlined" onClick={() => void handleConfigure()} disabled={busy}>
|
|
||||||
저장
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Box sx={{ borderTop: `1px solid ${d3roPalette.border.subtle}`, pt: 2 }}>
|
|
||||||
<Box sx={{ ...typoSx('label'), color: d3roPalette.text.label, mb: 1 }}>OAuth 로그인</Box>
|
|
||||||
<Stack direction="row" spacing={1}>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
startIcon={<GoogleIcon />}
|
|
||||||
onClick={() => void handleSignIn('google')}
|
|
||||||
disabled={busy || !supabaseUrl || !anonKey}
|
|
||||||
>
|
|
||||||
Google
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
startIcon={<GitHubIcon />}
|
|
||||||
onClick={() => void handleSignIn('github')}
|
|
||||||
disabled={busy || !supabaseUrl || !anonKey}
|
|
||||||
>
|
|
||||||
GitHub
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{state.authenticated && (
|
{state.authenticated && (
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
<Box sx={{ color: d3roPalette.text.primary, fontSize: 13 }}>
|
<Box sx={{ color: d3roPalette.text.primary, fontSize: 13 }}>
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,6 @@ export function OnboardingModal({ open, onClose }: OnboardingModalProps): React.
|
||||||
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
|
||||||
setStep(0)
|
setStep(0)
|
||||||
|
|
@ -47,10 +45,6 @@ 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 = (): void => {
|
const handleFinish = (): void => {
|
||||||
|
|
@ -59,9 +53,9 @@ export function OnboardingModal({ open, onClose }: OnboardingModalProps): React.
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ollama step(3) 다음 — saasMode면 Cloud Sync step(4), 아니면 바로 완료(5)로
|
// Ollama step(3) 다음 → Cloud Sync step(4)로
|
||||||
const nextAfterOllama = (): void => {
|
const nextAfterOllama = (): void => {
|
||||||
setStep(saasMode ? 4 : 5)
|
setStep(4)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cloud Sync step에서 Back 누르면 Ollama(3)로 복귀
|
// Cloud Sync step에서 Back 누르면 Ollama(3)로 복귀
|
||||||
|
|
@ -256,7 +250,7 @@ export function OnboardingModal({ open, onClose }: OnboardingModalProps): React.
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Step 4: Cloud Sync (선택, saasMode에서만 노출) */}
|
{/* Step 4: Cloud Sync (선택) */}
|
||||||
{step === 4 && (
|
{step === 4 && (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack direction="row" alignItems="center" gap={1} mb={3}>
|
<Stack direction="row" alignItems="center" gap={1} mb={3}>
|
||||||
|
|
|
||||||
|
|
@ -542,21 +542,19 @@ V2 6차 고도화 (2026-04-11):
|
||||||
> 사용자가 Settings에서 Supabase URL/Key를 직접 입력해야 하는 UX였음.
|
> 사용자가 Settings에서 Supabase URL/Key를 직접 입력해야 하는 UX였음.
|
||||||
> Notion/Linear/Slack 데스크톱 패턴으로 정정.
|
> Notion/Linear/Slack 데스크톱 패턴으로 정정.
|
||||||
|
|
||||||
### SaaS [1] 빌드 타임 Supabase env 주입
|
### SaaS [1] Supabase 연결 — 소스 하드코딩 (2026-04-13 리팩토링)
|
||||||
- `electron.vite.config.ts`: `defineConfig(({ mode }) => ...)`로 변경, `loadEnv(mode, cwd, 'D3RO_')`
|
- **이전**: `.env.local` + `BUILD_TIME_SUPABASE_*` + `saasMode` 분기 → Mac/Windows UI 불일치
|
||||||
로 `.env.local` 자동 로드 → main 번들에 `define`으로 inline 박음
|
- **현재**: `packages/core/src/supabase-config.ts`에 URL/Anon Key 하드코딩 (SSOT)
|
||||||
- `apps/desktop/.env.local` 생성 (gitignored): `D3RO_SUPABASE_URL`, `D3RO_SUPABASE_ANON_KEY`
|
- Anon key는 클라이언트용 공개 키(RLS 보호)이므로 소스 포함 안전
|
||||||
- `apps/desktop/env.example` 템플릿 (committed)
|
- `saasMode` 분기 전체 제거, 항상 OAuth(Google/GitHub) only
|
||||||
- `ConfigService`:
|
- `env.example` 삭제, `loadSupabaseEnv()` 삭제, `isSupabaseBuildTimeConfigured()` 삭제
|
||||||
- `BUILD_TIME_SUPABASE_URL` / `BUILD_TIME_SUPABASE_ANON_KEY` 상수 (process.env에서 읽음)
|
- `CLOUD_SYNC.CONFIGURE` IPC 채널 삭제
|
||||||
- `isSupabaseBuildTimeConfigured()` 헬퍼 export
|
- `CloudSyncSection`: 개발자 모드 UI(URL/Key 입력) 완전 제거
|
||||||
- `configGet('supabaseUrl'/'supabaseAnonKey')`은 빌드 타임 값 절대 우선
|
|
||||||
- `configSet`는 빌드 타임 모드일 때 supabaseUrl/AnonKey 변경 거부
|
|
||||||
|
|
||||||
### SaaS [2] OAuth 첫 실행 강제 게이트 → **철회 (Phase 1.5에서 로컬 모드 entry point로 변경)**
|
### SaaS [2] OAuth 로그인 전략
|
||||||
- `CloudSyncState`에 `saasMode: boolean` 필드 추가 (유지)
|
- 로그인 없으면 로컬 Ollama로 동작 (무료)
|
||||||
- `CloudSyncSection`: saasMode에서 OAuth 버튼만, legacy에서는 URL/Key 입력 UI (유지)
|
- 로그인은 Google/GitHub OAuth only (Supabase Auth)
|
||||||
- ~~`LoginScreen.tsx` 강제 게이트~~ → 제거 (App.tsx에서 사용 안 함, 파일 자체는 유지)
|
- ~~`LoginScreen.tsx` 강제 게이트~~ → 제거
|
||||||
- ~~`App.tsx` AuthGate 상태머신~~ → 제거 (항상 메인 UI 렌더)
|
- ~~`App.tsx` AuthGate 상태머신~~ → 제거 (항상 메인 UI 렌더)
|
||||||
|
|
||||||
**철회 이유** (2026-04-11 빅뱅 Phase 1.5):
|
**철회 이유** (2026-04-11 빅뱅 Phase 1.5):
|
||||||
|
|
|
||||||
9908
package-lock.json
generated
9908
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -388,7 +388,6 @@ export const IPC_CHANNELS = {
|
||||||
PUSH_ALL: 'cloudSync:pushAll',
|
PUSH_ALL: 'cloudSync:pushAll',
|
||||||
PULL_ALL: 'cloudSync:pullAll',
|
PULL_ALL: 'cloudSync:pullAll',
|
||||||
HANDLE_CALLBACK: 'cloudSync:handleCallback',
|
HANDLE_CALLBACK: 'cloudSync:handleCallback',
|
||||||
CONFIGURE: 'cloudSync:configure',
|
|
||||||
// events
|
// events
|
||||||
AUTH_CHANGED: 'cloudSync:authChanged',
|
AUTH_CHANGED: 'cloudSync:authChanged',
|
||||||
SYNC_PROGRESS: 'cloudSync:syncProgress',
|
SYNC_PROGRESS: 'cloudSync:syncProgress',
|
||||||
|
|
|
||||||
6
packages/core/src/supabase-config.ts
Normal file
6
packages/core/src/supabase-config.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
// packages/core/src/supabase-config.ts
|
||||||
|
// Supabase 연결 정보 SSOT — 데스크톱, 웹, 모바일 모두 여기서 참조.
|
||||||
|
// Anon key는 클라이언트용 공개 키(RLS 보호)이므로 소스에 포함해도 안전.
|
||||||
|
|
||||||
|
export const SUPABASE_URL = 'https://llnocwyqvhgwpdjcqqyw.supabase.co'
|
||||||
|
export const SUPABASE_ANON_KEY = 'sb_publishable_0uo4UYYvUO2y-sVMFdYylA_hHv9qRt5'
|
||||||
Loading…
Add table
Add a link
Reference in a new issue