2026-06-07 16:20:55 +09:00
|
|
|
// Slim Budget 데스크톱(Electron) 메인 프로세스.
|
2026-06-07 16:39:28 +09:00
|
|
|
// 라이브 사이트(https://app.sblog.kr)를 직접 로드한다 → 프론트 변경이 자동 반영(재설치 불필요),
|
|
|
|
|
// 같은 출처라 CORS 우회도 불필요. 배포된 웹 게이트가 Electron 을 감지해 안내페이지 대신 전체 앱을 띄운다.
|
2026-06-07 17:25:21 +09:00
|
|
|
const { app, BrowserWindow, shell, session } = require('electron')
|
2026-06-07 17:17:42 +09:00
|
|
|
const fs = require('fs')
|
|
|
|
|
const path = require('path')
|
2026-06-07 16:39:28 +09:00
|
|
|
|
2026-06-23 21:02:09 +09:00
|
|
|
// 네이티브 대화상자(alert/confirm) 제목은 app.getName() 을 쓴다 → package.json name('sb_pt') 대신 표기.
|
2026-06-27 16:51:29 +09:00
|
|
|
app.setName('돈돼지 가계부')
|
2026-06-23 21:02:09 +09:00
|
|
|
|
2026-06-07 16:39:28 +09:00
|
|
|
const APP_URL = 'https://app.sblog.kr'
|
|
|
|
|
|
2026-06-07 17:17:42 +09:00
|
|
|
// 창 크기/위치를 userData 에 저장해 다음 실행 때 복원 (기본 해상도 고정 대신 사용자가 맞춘 크기 유지)
|
|
|
|
|
function stateFile() {
|
|
|
|
|
return path.join(app.getPath('userData'), 'window-state.json')
|
|
|
|
|
}
|
|
|
|
|
function loadState() {
|
|
|
|
|
try {
|
|
|
|
|
return JSON.parse(fs.readFileSync(stateFile(), 'utf-8'))
|
|
|
|
|
} catch {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
function saveState(win) {
|
|
|
|
|
try {
|
|
|
|
|
if (win.isMinimized()) return
|
|
|
|
|
const b = win.getBounds()
|
|
|
|
|
fs.writeFileSync(stateFile(), JSON.stringify({ ...b, maximized: win.isMaximized() }))
|
|
|
|
|
} catch {
|
|
|
|
|
/* 저장 실패 무시 */
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-07 16:39:28 +09:00
|
|
|
// 오프라인/로드 실패 시 안내 화면 (다시 시도 버튼)
|
|
|
|
|
function offlineHtml() {
|
2026-06-27 16:51:29 +09:00
|
|
|
const html = `<!doctype html><html><head><meta charset="utf-8"><title>돈돼지 가계부</title>
|
2026-06-07 16:39:28 +09:00
|
|
|
<style>html,body{height:100%;margin:0;display:flex;align-items:center;justify-content:center;
|
|
|
|
|
background:#1a1a1a;color:#eee;font-family:system-ui,'Segoe UI',sans-serif}
|
|
|
|
|
.box{text-align:center}h2{margin:0 0 8px}p{opacity:.7;margin:0}
|
|
|
|
|
button{margin-top:18px;padding:10px 22px;border:0;border-radius:8px;background:#00bc7e;color:#fff;
|
|
|
|
|
font-size:15px;font-weight:600;cursor:pointer}</style></head>
|
|
|
|
|
<body><div class="box"><h2>연결할 수 없습니다</h2><p>인터넷 연결을 확인해 주세요.</p>
|
|
|
|
|
<button onclick="location.href='${APP_URL}'">다시 시도</button></div></body></html>`
|
|
|
|
|
return 'data:text/html;charset=utf-8,' + encodeURIComponent(html)
|
|
|
|
|
}
|
2026-06-07 16:20:55 +09:00
|
|
|
|
|
|
|
|
function createWindow() {
|
2026-06-07 17:17:42 +09:00
|
|
|
const s = loadState()
|
2026-06-07 16:20:55 +09:00
|
|
|
const win = new BrowserWindow({
|
2026-06-07 17:17:42 +09:00
|
|
|
width: s?.width || 1024,
|
|
|
|
|
height: s?.height || 720,
|
|
|
|
|
x: s?.x,
|
|
|
|
|
y: s?.y,
|
2026-06-07 16:20:55 +09:00
|
|
|
minWidth: 360,
|
|
|
|
|
minHeight: 560,
|
|
|
|
|
backgroundColor: '#1a1a1a',
|
2026-06-27 16:51:29 +09:00
|
|
|
title: '돈돼지 가계부',
|
2026-06-07 16:20:55 +09:00
|
|
|
webPreferences: {
|
|
|
|
|
contextIsolation: true,
|
|
|
|
|
nodeIntegration: false,
|
2026-06-07 17:43:17 +09:00
|
|
|
// 한글 IME 첫 글자 중복 입력 방지 — Chromium 맞춤법 검사기가 조합을 방해하는 Electron 알려진 버그 회피
|
|
|
|
|
spellcheck: false,
|
2026-06-07 16:20:55 +09:00
|
|
|
},
|
|
|
|
|
})
|
2026-06-07 17:17:42 +09:00
|
|
|
if (s?.maximized) win.maximize()
|
2026-06-07 16:20:55 +09:00
|
|
|
|
|
|
|
|
// 기본 메뉴바 제거(앱 자체 내비게이션 사용)
|
|
|
|
|
win.removeMenu()
|
2026-06-07 16:39:28 +09:00
|
|
|
win.loadURL(APP_URL)
|
|
|
|
|
|
2026-06-07 17:17:42 +09:00
|
|
|
// 닫을 때 창 크기/위치 저장 → 다음 실행에 복원
|
|
|
|
|
win.on('close', () => saveState(win))
|
|
|
|
|
|
2026-06-07 16:39:28 +09:00
|
|
|
// 메인 프레임 로드 실패(오프라인 등) 시 안내 화면. -3(aborted, 리다이렉트 등)은 무시.
|
|
|
|
|
win.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL, isMainFrame) => {
|
|
|
|
|
if (isMainFrame && errorCode !== -3) win.loadURL(offlineHtml())
|
|
|
|
|
})
|
2026-06-07 16:20:55 +09:00
|
|
|
|
2026-06-07 16:39:28 +09:00
|
|
|
// 외부(앱 외 도메인) 링크는 기본 브라우저로, 앱 내 링크는 창에서 처리
|
2026-06-07 16:20:55 +09:00
|
|
|
win.webContents.setWindowOpenHandler(({ url }) => {
|
2026-06-27 19:01:57 +09:00
|
|
|
// 구글 로그인(GIS) 팝업은 앱 내부 자식 창으로 열어야 opener 로 로그인 결과(postMessage)가 돌아온다.
|
|
|
|
|
// 외부 브라우저로 보내면 계정 선택 후 credential 이 앱으로 전달되지 않아 로그인이 끊긴다.
|
|
|
|
|
if (/^https:\/\/accounts\.google\.com\//i.test(url)) {
|
|
|
|
|
return { action: 'allow' }
|
|
|
|
|
}
|
2026-06-07 16:39:28 +09:00
|
|
|
if (/^https?:\/\//i.test(url) && !url.startsWith(APP_URL)) {
|
|
|
|
|
shell.openExternal(url)
|
|
|
|
|
return { action: 'deny' }
|
|
|
|
|
}
|
|
|
|
|
return { action: 'allow' }
|
2026-06-07 16:20:55 +09:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-07 17:25:21 +09:00
|
|
|
app.whenReady().then(async () => {
|
|
|
|
|
// 시작 시 HTTP 캐시 비움 → 라이브 사이트의 최신 프론트를 항상 로드(옛 HTML 캐시로 인한 미반영 방지).
|
|
|
|
|
// localStorage(로그인 세션)는 캐시가 아니라 영향 없음.
|
|
|
|
|
try {
|
|
|
|
|
await session.defaultSession.clearCache()
|
|
|
|
|
} catch {
|
|
|
|
|
/* 캐시 클리어 실패 무시 */
|
|
|
|
|
}
|
2026-06-07 16:20:55 +09:00
|
|
|
createWindow()
|
|
|
|
|
app.on('activate', () => {
|
|
|
|
|
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
app.on('window-all-closed', () => {
|
|
|
|
|
if (process.platform !== 'darwin') app.quit()
|
|
|
|
|
})
|