Deploy: exdev sync 2026-08-13 13:38:36

This commit is contained in:
bkyungee
2026-08-13 13:38:36 +09:00
parent fab7a5ba49
commit f15b666da9
47 changed files with 17539 additions and 17539 deletions
+181 -181
View File
@@ -1,181 +1,181 @@
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",
"geolocation",
]);
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 = `<!doctype html><meta charset="utf-8"/>
<body style="font:14px -apple-system,sans-serif;padding:40px;line-height:1.6">
<h2>연결 실패</h2>
<p>챗봇 서버에 연결하지 못했습니다.</p>
<p style="color:#666">${code}: ${desc}</p>
<p><a href="${DEFAULT_URL}">다시 시도</a></p>
</body>`;
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}`);
});
}
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",
"geolocation",
]);
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 = `<!doctype html><meta charset="utf-8"/>
<body style="font:14px -apple-system,sans-serif;padding:40px;line-height:1.6">
<h2>연결 실패</h2>
<p>챗봇 서버에 연결하지 못했습니다.</p>
<p style="color:#666">${code}: ${desc}</p>
<p><a href="${DEFAULT_URL}">다시 시도</a></p>
</body>`;
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}`);
});
}