fix(desktop+supabase): 로컬 ID UUID 통일 + Realtime publication (빅뱅 Phase 5 Part 2)
SaaS [9] — 실증 A/B/C/D 전부 통과, 로컬→클라우드 push 최초 성공(pushed=1). ## Bug 4: 로컬 nanoid PK vs Supabase UUID PK 불일치 - 증상: Push history failed: invalid input syntax for type uuid: "fvy6bIzr..." - 원인: 로컬 drizzle schema는 text PK + nanoid() 생성, Supabase는 uuid PK. 빅뱅 사이클 내내 push가 한 번도 성공한 적 없었음 (지난 pushed=0은 데이터 0건이라서). - 픽스: 로컬을 UUID로 통일 (근본 해결, 땜질 금지). 14개 서비스 20곳 nanoid() → crypto.randomUUID() 일괄 교체. nanoid 의존성 + electron.vite.config exclude 제거. drizzle schema는 text PK 그대로 유지 (SQLite는 UUID 문자열 저장 가능). ## Bug 5: supabase_realtime publication 누락 - 증상: 로그인 직후 Realtime 채널 상태: TIMED_OUT - 원인: initial_schema.sql이 transcripts 테이블만 publication에 추가. 데스크톱이 구독하는 meetings/history/dictionary는 누락 → postgres_changes 흐르지 않음. - 픽스: 20260411000002_realtime_publication.sql 신규. pg_publication_tables 카탈로그 체크 + 조건부 ADD TABLE (meetings/meeting_memos/ meeting_documents/history/dictionary 5개). supabase db push 적용. ## Bug 6: persistSession:false에서 realtime.setAuth 자동 전파 안 됨 (부분 픽스) - 픽스: CloudSyncService.startRealtime()에 client.realtime.setAuth(access_token) 명시 호출 (채널 구성 이전). - ⚠️ Bug 5+6 적용 후에도 Realtime 여전히 TIMED_OUT. 후속 조사 필요. 블로커 아님 — 주기 pull + Phase 3.3 auto push로 최종 일관성 유지. ## 실증 결과 - A 세션 자동 복원: Restored session for yunchan8804@gmail.com → DB 재오픈 - B push 경로: HistoryService created 56a767ac-... → Sync complete pushed=1 errors=0 - C 로그아웃 복귀: Realtime 종료 → users/_local/d3ro.db 복귀 → local mode - D 재로그인 복원: 실증 A의 restore 경로와 동일, 같은 uuid DB 파일 보존 - E 웹 크로스 디바이스: Phase 3.3 이후로 지연 (Realtime 이슈 별건) 검증: desktop tsc --noEmit ✅, dev 재기동 ✅, push 최초 성공 ✅
This commit is contained in:
parent
c8338ec458
commit
e55687d298
19 changed files with 132 additions and 38 deletions
|
|
@ -33,7 +33,7 @@ export default defineConfig(({ mode }) => {
|
|||
main: {
|
||||
plugins: [
|
||||
externalizeDepsPlugin({
|
||||
exclude: ['nanoid', 'electron-store', ...workspaceExclude]
|
||||
exclude: ['electron-store', ...workspaceExclude]
|
||||
})
|
||||
],
|
||||
resolve: { alias: sharedAlias },
|
||||
|
|
|
|||
|
|
@ -61,7 +61,6 @@
|
|||
"electron-log": "^5.2.0",
|
||||
"electron-store": "^10.0.0",
|
||||
"fluent-ffmpeg": "^2.1.3",
|
||||
"nanoid": "^5.1.7",
|
||||
"node-record-lpcm16": "^1.0.1",
|
||||
"pdf-parse": "^2.4.5",
|
||||
"react": "^19.0.0",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
// 3초 청크 기반 스트리밍 전사. 싱글톤 + EventEmitter 패턴.
|
||||
|
||||
import { EventEmitter } from 'events'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { getLogger } from './LoggerService'
|
||||
import { getAudioCaptureService, calculateRMS } from './AudioCaptureService'
|
||||
import { getSoundEffectService } from './SoundEffectService'
|
||||
|
|
@ -127,7 +126,7 @@ class CaptionService extends EventEmitter {
|
|||
await sttService.initialize(modelId)
|
||||
|
||||
// 세션 초기화
|
||||
this._sessionId = nanoid()
|
||||
this._sessionId = crypto.randomUUID()
|
||||
this._sessionStartedAt = Date.now()
|
||||
this._segments = []
|
||||
this._audioBuffers = []
|
||||
|
|
@ -350,7 +349,7 @@ class CaptionService extends EventEmitter {
|
|||
}
|
||||
|
||||
const segment: CaptionSegment = {
|
||||
id: nanoid(),
|
||||
id: crypto.randomUUID(),
|
||||
text: result.text.trim(),
|
||||
timestamp: Date.now(),
|
||||
isFinal: true,
|
||||
|
|
@ -423,7 +422,7 @@ class CaptionService extends EventEmitter {
|
|||
if (!result.text || result.text.trim().length === 0) return
|
||||
|
||||
const segment: CaptionSegment = {
|
||||
id: nanoid(),
|
||||
id: crypto.randomUUID(),
|
||||
text: result.text.trim(),
|
||||
timestamp: Date.now(),
|
||||
isFinal: true,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
// Phase 10.4: Multi-LLM Chain 서비스.
|
||||
// LLMChain을 electron-store에 저장하고, 체인을 순차 실행한다.
|
||||
|
||||
import { nanoid } from 'nanoid'
|
||||
import { getLogger } from './LoggerService'
|
||||
import { configGet } from './ConfigService'
|
||||
import { getCustomInstructionService } from './CustomInstructionService'
|
||||
|
|
@ -87,7 +86,7 @@ class ChainService {
|
|||
create(params: CreateChainParams): LLMChain {
|
||||
const now = Date.now()
|
||||
const chain: LLMChain = {
|
||||
id: nanoid(),
|
||||
id: crypto.randomUUID(),
|
||||
name: params.name,
|
||||
steps: params.steps,
|
||||
createdAt: now,
|
||||
|
|
|
|||
|
|
@ -371,6 +371,17 @@ class CloudSyncService extends EventEmitter {
|
|||
|
||||
const userId = this._session.user.id
|
||||
|
||||
// persistSession: false 에서는 Supabase realtime 클라이언트가 auth state change를
|
||||
// 자동 추적하지 않는다. access_token을 명시적으로 realtime에 주입해서
|
||||
// postgres_changes 채널이 RLS를 통과하도록 한다. (TIMED_OUT 버그 픽스)
|
||||
try {
|
||||
this._client.realtime.setAuth(this._session.access_token)
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
`Realtime setAuth 실패 (계속 진행): ${err instanceof Error ? err.message : String(err)}`
|
||||
)
|
||||
}
|
||||
|
||||
// 변경 감지 debounce — 연속 이벤트가 몰릴 때 한 번만 pull
|
||||
let pullScheduled = false
|
||||
const schedulePull = (): void => {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
// 사용자 정의 LLM 명령어 관리. 설계서 01/Phase 6 참조.
|
||||
// electron-store에 저장, 프리셋 5개 기본 제공.
|
||||
|
||||
import { nanoid } from 'nanoid'
|
||||
import { getLogger } from './LoggerService'
|
||||
import { configGet } from './ConfigService'
|
||||
|
||||
|
|
@ -141,7 +140,7 @@ class CustomInstructionService {
|
|||
create(input: CreateInput): CustomInstruction {
|
||||
const now = Date.now()
|
||||
const instruction: CustomInstruction = {
|
||||
id: nanoid(),
|
||||
id: crypto.randomUUID(),
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
prompt: input.prompt,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
// 템플릿 CRUD + 세션 상태 머신 (필드별 음성 입력)
|
||||
|
||||
import { EventEmitter } from 'events'
|
||||
import { nanoid } from 'nanoid'
|
||||
import Store from 'electron-store'
|
||||
import { getLogger } from './LoggerService'
|
||||
import { getMainWindow } from '../windows/WindowManager'
|
||||
|
|
@ -103,7 +102,7 @@ class DictationTemplateService extends EventEmitter {
|
|||
|
||||
create(params: CreateTemplateParams): DictationTemplate {
|
||||
const template: DictationTemplate = {
|
||||
id: nanoid(),
|
||||
id: crypto.randomUUID(),
|
||||
name: params.name,
|
||||
description: params.description,
|
||||
fields: params.fields,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
// 사용자 커스텀 단어 사전. 설계서 01/03 IDictionaryService 구현.
|
||||
|
||||
import { eq, like, desc, count, sql } from 'drizzle-orm'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { getDatabase } from '../db'
|
||||
import { dictionary } from '../db/schema'
|
||||
import type { Dictionary, NewDictionary } from '../db/schema'
|
||||
|
|
@ -22,7 +21,7 @@ class DictionaryService {
|
|||
add(params: DictionaryAddParams): DictionaryEntry {
|
||||
const db = getDatabase()
|
||||
const now = Date.now()
|
||||
const id = nanoid()
|
||||
const id = crypto.randomUUID()
|
||||
|
||||
const entry: NewDictionary = {
|
||||
id,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
import { EventEmitter } from 'events'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { app } from 'electron'
|
||||
import { getLogger } from './LoggerService'
|
||||
import { getLocalSTTService } from './LocalSTTService'
|
||||
|
|
@ -89,7 +88,7 @@ class FileTranscriptionService extends EventEmitter {
|
|||
if (err instanceof D3ROError) throw err
|
||||
}
|
||||
|
||||
this._jobId = nanoid()
|
||||
this._jobId = crypto.randomUUID()
|
||||
this._cancelled = false
|
||||
this._tempDir = path.join(app.getPath('temp'), `d3ro-ft-${this._jobId}`)
|
||||
fs.mkdirSync(this._tempDir, { recursive: true })
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
// SQLite 기반 전사/명령 이력 저장. 설계서 01/03 IHistoryService 구현.
|
||||
|
||||
import { eq, desc, like, and, sql, count } from 'drizzle-orm'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { getDatabase } from '../db'
|
||||
import { history, stats } from '../db/schema'
|
||||
import type { History, NewHistory } from '../db/schema'
|
||||
|
|
@ -21,7 +20,7 @@ class HistoryService {
|
|||
create(input: Omit<NewHistory, 'id' | 'createdAt' | 'updatedAt'>): HistoryEntry {
|
||||
const db = getDatabase()
|
||||
const now = Date.now()
|
||||
const id = nanoid()
|
||||
const id = crypto.randomUUID()
|
||||
|
||||
const entry: NewHistory = {
|
||||
id,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
// Phase 14.5: 회의 문서 템플릿 서비스 (electron-store 기반)
|
||||
|
||||
import { EventEmitter } from 'events'
|
||||
import { nanoid } from 'nanoid'
|
||||
import Store from 'electron-store'
|
||||
import { getLogger } from './LoggerService'
|
||||
import { D3ROError, ErrorCode } from '@d3ro/core/errors'
|
||||
|
|
@ -176,7 +175,7 @@ class MeetingDocTemplateService extends EventEmitter {
|
|||
|
||||
create(params: CreateMeetingDocTemplateParams): MeetingDocTemplate {
|
||||
const template: MeetingDocTemplate = {
|
||||
id: nanoid(),
|
||||
id: crypto.randomUUID(),
|
||||
name: params.name,
|
||||
description: params.description,
|
||||
templateType: 'custom' as MeetingDocTemplateType,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
import { EventEmitter } from 'events'
|
||||
import { BrowserWindow, Notification, dialog } from 'electron'
|
||||
import fs from 'fs'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { eq, desc, sql } from 'drizzle-orm'
|
||||
import { getLogger } from './LoggerService'
|
||||
import { configGet, configSet } from './ConfigService'
|
||||
|
|
@ -117,7 +116,7 @@ class MeetingModeService extends EventEmitter {
|
|||
this._setState('recording')
|
||||
this._meetingModeActive = true
|
||||
|
||||
const sessionId = nanoid()
|
||||
const sessionId = crypto.randomUUID()
|
||||
const now = Date.now()
|
||||
|
||||
this._sessionId = sessionId
|
||||
|
|
@ -209,7 +208,7 @@ class MeetingModeService extends EventEmitter {
|
|||
}
|
||||
|
||||
const memo: MeetingMemo = {
|
||||
id: nanoid(),
|
||||
id: crypto.randomUUID(),
|
||||
sessionId: this._sessionId,
|
||||
content,
|
||||
timestampMs: Date.now() - this._sessionStartedAt,
|
||||
|
|
@ -638,7 +637,7 @@ class MeetingModeService extends EventEmitter {
|
|||
sendProgress(85)
|
||||
|
||||
const now = Date.now()
|
||||
const docId = nanoid()
|
||||
const docId = crypto.randomUUID()
|
||||
db.insert(meetingDocuments).values({
|
||||
id: docId,
|
||||
sessionId: params.sessionId,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
// Phase 10.3: 음성 메모 태그 시스템. 히스토리 항목에 태그를 부착하고 태그별 검색/내보내기를 지원한다.
|
||||
|
||||
import { eq, and, desc, count, sql } from 'drizzle-orm'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { app } from 'electron'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
|
|
@ -60,7 +59,7 @@ class MemoService {
|
|||
)
|
||||
}
|
||||
|
||||
const id = nanoid()
|
||||
const id = crypto.randomUUID()
|
||||
const now = Date.now()
|
||||
|
||||
db.insert(memoTags)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
import { EventEmitter } from 'events'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { getLogger } from './LoggerService'
|
||||
import { getLocalLLMService } from './LocalLLMService'
|
||||
|
|
@ -99,7 +98,7 @@ class RAGService extends EventEmitter {
|
|||
}
|
||||
|
||||
const fileName = path.basename(filePath)
|
||||
const docId = nanoid()
|
||||
const docId = crypto.randomUUID()
|
||||
|
||||
// 텍스트 추출
|
||||
let content: string
|
||||
|
|
@ -265,7 +264,7 @@ ${context}`
|
|||
const embedding = await this._embed(chunks[i])
|
||||
|
||||
db.insert(ragChunks).values({
|
||||
id: nanoid(),
|
||||
id: crypto.randomUUID(),
|
||||
documentId: docId,
|
||||
content: chunks[i],
|
||||
embedding: JSON.stringify(embedding),
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
import { EventEmitter } from 'events'
|
||||
import { exec } from 'child_process'
|
||||
import { shell } from 'electron'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { getLogger } from './LoggerService'
|
||||
import { getLocalLLMService } from './LocalLLMService'
|
||||
import { configGet } from './ConfigService'
|
||||
|
|
@ -147,7 +146,7 @@ class VoiceActionService extends EventEmitter {
|
|||
// 안전장치
|
||||
if (!plan.safe) {
|
||||
const entry: VoiceActionHistoryEntry = {
|
||||
id: nanoid(),
|
||||
id: crypto.randomUUID(),
|
||||
userText: text,
|
||||
plan,
|
||||
executed: false,
|
||||
|
|
@ -237,7 +236,7 @@ class VoiceActionService extends EventEmitter {
|
|||
}
|
||||
|
||||
const entry: VoiceActionHistoryEntry = {
|
||||
id: nanoid(),
|
||||
id: crypto.randomUUID(),
|
||||
userText,
|
||||
plan,
|
||||
executed: true,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
// Phase 10.5: 음성 단축키 — 전사 텍스트에서 키워드를 감지하여 명령어 자동 선택.
|
||||
// electron-store에 VoiceCommandRule[] 저장, 키워드 매칭 엔진 제공.
|
||||
|
||||
import { nanoid } from 'nanoid'
|
||||
import { getLogger } from './LoggerService'
|
||||
import { configGet, configSet } from './ConfigService'
|
||||
import type {
|
||||
|
|
@ -224,7 +223,7 @@ class VoiceCommandService {
|
|||
// 새 rule 생성
|
||||
const maxPriority = this.rules.reduce((max, r) => Math.max(max, r.priority), -1)
|
||||
this.rules.push({
|
||||
id: nanoid(),
|
||||
id: crypto.randomUUID(),
|
||||
instructionId,
|
||||
keywords,
|
||||
enabled: true,
|
||||
|
|
@ -260,7 +259,7 @@ class VoiceCommandService {
|
|||
|
||||
for (const entry of DEFAULT_KEYWORDS) {
|
||||
this.rules.push({
|
||||
id: nanoid(),
|
||||
id: crypto.randomUUID(),
|
||||
instructionId: entry.instructionId,
|
||||
keywords: [...entry.keywords],
|
||||
enabled: true,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
// 싱글톤 + EventEmitter. 대화 히스토리 최근 10턴 유지.
|
||||
|
||||
import { EventEmitter } from 'events'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { getLogger } from './LoggerService'
|
||||
import { getLocalLLMService } from './LocalLLMService'
|
||||
import { getLocalSTTService } from './LocalSTTService'
|
||||
|
|
@ -213,7 +212,7 @@ class VoiceConversationService extends EventEmitter {
|
|||
private async _processUserMessage(text: string): Promise<void> {
|
||||
// 사용자 메시지 추가
|
||||
const userMsg: ConversationMessage = {
|
||||
id: nanoid(),
|
||||
id: crypto.randomUUID(),
|
||||
role: 'user',
|
||||
content: text,
|
||||
timestamp: Date.now(),
|
||||
|
|
@ -229,7 +228,7 @@ class VoiceConversationService extends EventEmitter {
|
|||
const llmService = getLocalLLMService()
|
||||
const chatMessages = this._buildChatMessages()
|
||||
|
||||
const assistantMsgId = nanoid()
|
||||
const assistantMsgId = crypto.randomUUID()
|
||||
let accumulated = ''
|
||||
const ttsSentences: string[] = []
|
||||
let sentenceBuffer = ''
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue