diff --git a/electron/main.cjs b/electron/main.cjs index fd79574..c5953cd 100644 --- a/electron/main.cjs +++ b/electron/main.cjs @@ -1,7 +1,13 @@ -// Slim Budget 데스크톱(Electron) 메인 프로세스. +// 돈돼지 가계부 데스크톱(Electron) 메인 프로세스. // 라이브 사이트(https://app.sblog.kr)를 직접 로드한다 → 프론트 변경이 자동 반영(재설치 불필요), // 같은 출처라 CORS 우회도 불필요. 배포된 웹 게이트가 Electron 을 감지해 안내페이지 대신 전체 앱을 띄운다. -const { app, BrowserWindow, shell, session, ipcMain, Menu } = require('electron') +// +// 로딩 UX: 예전에는 시작 시 HTTP 캐시를 통째로 비워 매번 전체 자산을 재다운로드 → 흰 화면이 오래 노출됐다. +// 이제 index.html 만 캐시버스트(?_=t)로 항상 최신을 받고, 해시된 정적 자산(JS/CSS)은 캐시를 유지해 +// 재시작이 빠르다. 로드되는 동안에는 스플래시(로딩) 창을 띄워 빈 화면을 가린다. +// 또한 단일 인스턴스 잠금으로 중복 실행을 막아(두 번째 실행 시 사이트를 다시 로드하며 빈 화면이 보이던 문제), +// 이미 실행 중이면 기존 창을 활성화한다. +const { app, BrowserWindow, shell, ipcMain, Menu } = require('electron') const fs = require('fs') const path = require('path') @@ -14,6 +20,24 @@ app.setName('돈돼지 가계부') const APP_URL = 'https://app.sblog.kr' +// 현재 메인 창 참조(중복 실행 시 활성화 대상). +let mainWindow = null + +// 중복 실행 방지: 잠금을 못 얻으면 이미 실행 중 → 새 인스턴스를 즉시 종료한다. +// (두 번째 실행에서 라이브 사이트를 다시 로드하느라 빈 화면이 보이던 현상 해결) +if (!app.requestSingleInstanceLock()) { + app.quit() +} else { + // 두 번째 실행 시도가 들어오면 기존 창을 복원/활성화. + app.on('second-instance', () => { + if (mainWindow && !mainWindow.isDestroyed()) { + if (mainWindow.isMinimized()) mainWindow.restore() + if (!mainWindow.isVisible()) mainWindow.show() + mainWindow.focus() + } + }) +} + // 창 크기/위치를 userData 에 저장해 다음 실행 때 복원 (기본 해상도 고정 대신 사용자가 맞춘 크기 유지) function stateFile() { return path.join(app.getPath('userData'), 'window-state.json') @@ -35,6 +59,23 @@ function saveState(win) { } } +// 스플래시(로딩) 화면 — 라이브 사이트가 첫 렌더될 때까지 빈 흰 화면 대신 표시. +function splashHtml() { + const html = `돈돼지 가계부 + +
🐷
돈돼지 가계부
+
불러오는 중…
` + return 'data:text/html;charset=utf-8,' + encodeURIComponent(html) +} + // 오프라인/로드 실패 시 안내 화면 (다시 시도 버튼) function offlineHtml() { const html = `돈돼지 가계부 @@ -57,6 +98,7 @@ function createWindow() { y: s?.y, minWidth: 360, minHeight: 560, + show: false, // 첫 렌더(ready-to-show) 전까지 숨김 → 빈 흰 화면 노출 방지(스플래시가 대신 보임) backgroundColor: '#1a1a1a', title: '돈돼지 가계부', webPreferences: { @@ -68,17 +110,49 @@ function createWindow() { }, }) if (s?.maximized) win.maximize() + mainWindow = win + + // 로딩 스플래시(프레임 없는 작은 창) — 메인이 준비되면 닫는다. + const splash = new BrowserWindow({ + width: 300, + height: 300, + frame: false, + resizable: false, + movable: false, + skipTaskbar: true, + alwaysOnTop: true, + backgroundColor: '#1a1a1a', + title: '돈돼지 가계부', + }) + splash.loadURL(splashHtml()) + splash.center() + + // 메인 창을 한 번만 노출하고 스플래시를 정리(이벤트 중복/지연 대비). + let revealed = false + const reveal = () => { + if (revealed) return + revealed = true + if (!win.isDestroyed()) win.show() + if (!splash.isDestroyed()) splash.destroy() + } + win.once('ready-to-show', reveal) + // 안전장치: 로드가 비정상적으로 지연돼도 12초 후엔 메인 창을 띄운다. + setTimeout(reveal, 12000) // 기본 메뉴바 제거(앱 자체 내비게이션 사용) win.removeMenu() - win.loadURL(APP_URL) + // index.html 만 캐시버스트(?_=t) → 항상 최신 프론트. 해시 자산은 캐시 유지 → 빠른 재시작. + win.loadURL(APP_URL + '/?_=' + Date.now()) // 닫을 때 창 크기/위치 저장 → 다음 실행에 복원 win.on('close', () => saveState(win)) // 메인 프레임 로드 실패(오프라인 등) 시 안내 화면. -3(aborted, 리다이렉트 등)은 무시. win.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL, isMainFrame) => { - if (isMainFrame && errorCode !== -3) win.loadURL(offlineHtml()) + if (isMainFrame && errorCode !== -3) { + win.loadURL(offlineHtml()) + reveal() // 오프라인 안내도 보이도록 메인 창 노출 + } }) // 외부(앱 외 도메인) 링크는 기본 브라우저로, 앱 내 링크는 창에서 처리 @@ -110,14 +184,8 @@ ipcMain.handle('window:getSize', (e) => { return win ? win.getSize() : [0, 0] }) -app.whenReady().then(async () => { - // 시작 시 HTTP 캐시 비움 → 라이브 사이트의 최신 프론트를 항상 로드(옛 HTML 캐시로 인한 미반영 방지). - // localStorage(로그인 세션)는 캐시가 아니라 영향 없음. - try { - await session.defaultSession.clearCache() - } catch { - /* 캐시 클리어 실패 무시 */ - } +app.whenReady().then(() => { + // 캐시는 비우지 않는다(빠른 재시작). 최신 프론트는 index.html 캐시버스트로 보장. createWindow() app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow()