Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/main/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { app, ipcMain } from 'electron';
import { app, ipcMain, nativeTheme } from 'electron';
import { appendFile, mkdir } from 'fs/promises';
import { join } from 'path';
import './security-restrictions';
Expand Down Expand Up @@ -68,6 +68,10 @@ function setupAPIHandlers() {
}
});

/* 시스템 테마 조회 핸들러 */
ipcMain.handle('theme:getSystemTheme', () => {
return nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
});
/* Notification 핸들러 설정 */
setupNotificationHandlers();
}
Expand Down
31 changes: 31 additions & 0 deletions src/main/src/security-restrictions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,4 +159,35 @@ app.on('web-contents-created', (_, contents) => {
// Disable Node.js integration
webPreferences.nodeIntegration = false;
});

/**
* Prevent page reload (F5, Ctrl+R, Cmd+R)
* 개발 모드에서는 허용하고 프로덕션에서만 막기
*/
contents.on('before-input-event', (event, input) => {
// 개발 모드에서는 새로고침 허용
if (import.meta.env.DEV) {
return;
}

// F5 또는 새로고침 단축키 (Ctrl+R, Cmd+R) 막기
if (
input.key === 'F5' ||
(input.key === 'r' && (input.control || input.meta))
) {
event.preventDefault();
}
});

/**
* Prevent programmatic reload
* 개발 모드에서는 허용하고 프로덕션에서만 막기
*/
if (import.meta.env.PROD) {
const originalReload = contents.reload.bind(contents);
contents.reload = () => {
console.warn('Page reload is disabled in production mode');
// 새로고침 실행하지 않음
};
}
});
32 changes: 3 additions & 29 deletions src/preload/exposedInMainWorld.d.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,5 @@
interface Window {
readonly bugi: { version: number };
/**
* Safe expose crypto API
* @example
* window.nodeCrypto.sha256sum('data')
*/
readonly nodeCrypto: { sha256sum: (data: string) => Promise<string> };
/**
* Expose API functionality to renderer
* @example
* window.electronAPI.getHealth()
*/
readonly electronAPI: {
getHealth: () => Promise<any>;
getVersion: () => Promise<any>;
generateHash: (data: string) => Promise<any>;
generateBatchHash: (dataList: string[]) => Promise<any>;
getPlatform: () => Promise<any>;
writeLog: (data: string, filename?: string) => Promise<any>;
widget: {
open: () => Promise<any>;
close: () => Promise<any>;
isOpen: () => Promise<any>;
};
notification: {
show: (title: string, body: string) => Promise<any>;
requestPermission: () => Promise<any>;
};
};
readonly bugi: BugiAPI;
readonly nodeCrypto: NodeCryptoAPI;
readonly electronAPI: ElectronAPI;
}
136 changes: 114 additions & 22 deletions src/preload/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,80 @@

import { contextBridge, ipcRenderer } from 'electron';

// Health 응답 타입 (예시)
// TODO: 실제 메인 프로세스에서 보내는 데이터 구조에 맞게 수정
type HealthResponse = {
status: 'ok';
uptime: number;
};

// Version 응답 타입 (예시)
type VersionInfo = {
appVersion: string;
electron: string;
chrome: string;
node: string;
};

// 플랫폼 정보 (예시)
type PlatformInfo = {
os: NodeJS.Platform;
arch: string;
};

// 로그 작성 결과 (예시)
type WriteLogResult = {
success: boolean;
path?: string;
error?: string;
};

// 시스템 테마 타입 (예시)
type SystemTheme = 'light' | 'dark' | 'system';

// window.bugi 타입
type BugiAPI = {
version: number;
};

// window.nodeCrypto 타입
type NodeCryptoAPI = {
sha256sum: (data: string) => Promise<string>;
};

// window.electronAPI 타입
interface ElectronAPI {
// Health check
getHealth: () => Promise<HealthResponse>;

// Version info
getVersion: () => Promise<VersionInfo>;

// Hash generation
generateHash: (data: string) => Promise<string>;

// Batch hash generation
generateBatchHash: (dataList: string[]) => Promise<string[]>;

// Platform info
getPlatform: () => Promise<PlatformInfo>;

// Write log file
writeLog: (data: string, filename?: string) => Promise<WriteLogResult>;

widget: {
open: () => Promise<void>;
close: () => Promise<void>;
isOpen: () => Promise<boolean>;
};

// 시스템 테마 조회
getSystemTheme: () => Promise<SystemTheme>;
}

// Expose version number to renderer
contextBridge.exposeInMainWorld('bugi', { version: 0.1 });
const bugiAPI: BugiAPI = { version: 0.1 };
contextBridge.exposeInMainWorld('bugi', bugiAPI);

/**
* The "Main World" is the JavaScript context that your main renderer code runs in.
Expand All @@ -28,55 +100,75 @@ contextBridge.exposeInMainWorld('bugi', { version: 0.1 });
* @example
* window.nodeCrypto.sha256sum('data')
*/
contextBridge.exposeInMainWorld('nodeCrypto', {
const nodeCryptoAPI: NodeCryptoAPI = {
sha256sum: async (data: string) => {
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(data);
const hashBuffer = await crypto.subtle.digest('SHA-256', dataBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
},
});
};
contextBridge.exposeInMainWorld('nodeCrypto', nodeCryptoAPI);

/**
* Expose API functionality to renderer
* @example
* window.electronAPI.getHealth()
*/
contextBridge.exposeInMainWorld('electronAPI', {
const electronAPI: ElectronAPI = {
// Health check
getHealth: () => ipcRenderer.invoke('api:health'),
getHealth: () =>
ipcRenderer.invoke('api:health') as ReturnType<ElectronAPI['getHealth']>,

// Version info
getVersion: () => ipcRenderer.invoke('api:version'),
getVersion: () =>
ipcRenderer.invoke('api:version') as ReturnType<ElectronAPI['getVersion']>,

// Hash generation
generateHash: (data: string) => ipcRenderer.invoke('api:hash', data),
generateHash: (data: string) =>
ipcRenderer.invoke('api:hash', data) as ReturnType<
ElectronAPI['generateHash']
>,

// Batch hash generation
generateBatchHash: (dataList: string[]) =>
ipcRenderer.invoke('api:hash:batch', dataList),
ipcRenderer.invoke('api:hash:batch', dataList) as ReturnType<
ElectronAPI['generateBatchHash']
>,

// Platform info
getPlatform: () => ipcRenderer.invoke('api:platform'),
getPlatform: () =>
ipcRenderer.invoke('api:platform') as ReturnType<
ElectronAPI['getPlatform']
>,

// Write log file
writeLog: (data: string, filename?: string) =>
ipcRenderer.invoke('api:writeLog', data, filename),
ipcRenderer.invoke('api:writeLog', data, filename) as ReturnType<
ElectronAPI['writeLog']
>,

/*electronAPI 객체에 widget 추가해서 리액트에서 접근 가능하도록 설정(리액트와 main process의 다리 역할) */
widget: {
open: () => ipcRenderer.invoke('widget:open'),
close: () => ipcRenderer.invoke('widget:close'),
isOpen: () => ipcRenderer.invoke('widget:isOpen'),
open: () =>
ipcRenderer.invoke('widget:open') as ReturnType<
ElectronAPI['widget']['open']
>,
close: () =>
ipcRenderer.invoke('widget:close') as ReturnType<
ElectronAPI['widget']['close']
>,
isOpen: () =>
ipcRenderer.invoke('widget:isOpen') as ReturnType<
ElectronAPI['widget']['isOpen']
>,
},

/* 시스템 알림 기능 */
notification: {
show: (title: string, body: string) =>
ipcRenderer.invoke('notification:show', title, body),
/* 알림 권한 요청 */
requestPermission: () =>
ipcRenderer.invoke('notification:requestPermission'),
},
});
// 시스템 테마 조회
getSystemTheme: () =>
ipcRenderer.invoke('theme:getSystemTheme') as ReturnType<
ElectronAPI['getSystemTheme']
>,
};
contextBridge.exposeInMainWorld('electronAPI', electronAPI);
4 changes: 2 additions & 2 deletions src/renderer/src/assets/widget.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading