const { app, BrowserWindow, shell, Menu, session } = require("electron"); const path = require("node:path"); const fs = require("node:fs"); // Gatekeeper / ad-hoc 서명 환경에서 GPU Helper SIGTRAP 완화 app.disableHardwareAcceleration(); app.commandLine.appendSwitch("disable-gpu"); app.commandLine.appendSwitch("disable-gpu-compositing"); app.commandLine.appendSwitch("disable-software-rasterizer"); app.commandLine.appendSwitch("no-sandbox"); app.commandLine.appendSwitch("disable-dev-shm-usage"); const WINDOW_WIDTH = 390; const WINDOW_HEIGHT = 720; const DEFAULT_URL = process.env.CHATBOT_APP_URL || "https://chatbot.exdev.co.kr/chatbot/ai/?desktop=1"; const logFile = path.join(app.getPath("userData"), "ex-ai-chatbot.log"); function log(msg) { const line = `[${new Date().toISOString()}] ${msg}\n`; try { fs.appendFileSync(logFile, line); } catch { /* ignore */ } console.log(msg); } /** 챗봇 주소에서 온 마이크 요청은 매번 묻지 않고 허용한다. (음성인식 반복 권한창 방지) */ function allowMicrophoneForChatbot() { const allowed = new Set(["media", "audioCapture", "mediaKeySystem"]); const appOrigin = (() => { try { return new URL(DEFAULT_URL).origin; } catch { return null; } })(); const isChatbotOrigin = (url) => { if (!appOrigin || !url) return false; try { return new URL(url).origin === appOrigin; } catch { return false; } }; const ses = session.defaultSession; ses.setPermissionRequestHandler((webContents, permission, callback, details) => { const url = details?.requestingUrl || webContents?.getURL(); callback(allowed.has(permission) && isChatbotOrigin(url)); }); ses.setPermissionCheckHandler((_webContents, permission, requestingOrigin, details) => { const url = requestingOrigin || details?.requestingUrl; return allowed.has(permission) && isChatbotOrigin(url); }); if (typeof ses.setDevicePermissionHandler === "function") { ses.setDevicePermissionHandler((details) => isChatbotOrigin(details?.origin)); } } function createWindow() { const iconPath = path.join(__dirname, "build", process.platform === "win32" ? "icon.ico" : "icon.png"); const winOpts = { width: WINDOW_WIDTH, height: WINDOW_HEIGHT, minWidth: 360, minHeight: 520, title: "한국도로공사 AI 챗봇", backgroundColor: "#ffffff", autoHideMenuBar: true, show: false, webPreferences: { preload: path.join(__dirname, "preload.js"), contextIsolation: true, nodeIntegration: false, sandbox: false, webSecurity: true, }, }; if (fs.existsSync(iconPath)) { winOpts.icon = iconPath; } const win = new BrowserWindow(winOpts); Menu.setApplicationMenu(null); win.once("ready-to-show", () => { win.show(); if (process.platform === "darwin" && app.dock) { const dockIcon = path.join(__dirname, "build", "icon.png"); if (fs.existsSync(dockIcon)) { try { app.dock.setIcon(dockIcon); } catch (e) { log(`dock.setIcon failed: ${e}`); } } app.dock.show(); } }); win.webContents.on("did-fail-load", (_e, code, desc, url) => { log(`did-fail-load code=${code} desc=${desc} url=${url}`); // 네트워크 실패 시에도 빈 창으로 종료되지 않도록 안내 페이지 const html = `
챗봇 서버에 연결하지 못했습니다.
${code}: ${desc}
`; win.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`); }); win.webContents.on("render-process-gone", (_e, details) => { log(`render-process-gone ${JSON.stringify(details)}`); }); win.on("unresponsive", () => log("window unresponsive")); log(`loadURL ${DEFAULT_URL}`); win.loadURL(DEFAULT_URL).catch((err) => log(`loadURL error: ${err}`)); win.webContents.setWindowOpenHandler(({ url }) => { shell.openExternal(url); return { action: "deny" }; }); return win; } const gotLock = app.requestSingleInstanceLock(); if (!gotLock) { app.quit(); } else { app.on("second-instance", () => { const wins = BrowserWindow.getAllWindows(); if (wins.length > 0) { if (wins[0].isMinimized()) wins[0].restore(); wins[0].focus(); } else { createWindow(); } }); app.whenReady().then(() => { app.setName("EX AI Chatbot"); log(`ready userData=${app.getPath("userData")}`); allowMicrophoneForChatbot(); createWindow(); app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } }); }); app.on("window-all-closed", () => { if (process.platform !== "darwin") { app.quit(); } }); process.on("uncaughtException", (err) => { log(`uncaughtException ${err?.stack || err}`); }); }