diff --git a/package.json b/package.json
index 0793aa7..465d7a6 100644
--- a/package.json
+++ b/package.json
@@ -1,13 +1,13 @@
{
"name": "photo-renamer",
- "version": "2.0.0",
+ "version": "1.0.0",
"description": "사진 파일 이름 정리 앱",
"main": "src/main/index.js",
"scripts": {
"start": "cross-env NODE_ENV=development electron .",
"debug": "cross-env NODE_ENV=development electron --inspect=9229 --remote-debugging-port=9222 .",
- "build": "cross-env NODE_ENV=production electron-builder --win --dir",
- "dist": "cross-env NODE_ENV=production electron-builder --win",
+ "build": "cross-env NODE_ENV=production CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --win --dir",
+ "dist": "cross-env NODE_ENV=production CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --win",
"build:mac": "cross-env NODE_ENV=production electron-builder --mac --dir",
"dist:mac": "cross-env NODE_ENV=production electron-builder --mac"
},
@@ -28,8 +28,6 @@
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
- "installerIcon": "assets/icon.png",
- "uninstallerIcon": "assets/icon.png",
"createDesktopShortcut": true,
"createStartMenuShortcut": true
},
diff --git a/src/main/index.js b/src/main/index.js
index dd5e28e..bc63f43 100644
--- a/src/main/index.js
+++ b/src/main/index.js
@@ -1,16 +1,16 @@
-const { app, BrowserWindow, screen } = require('electron');
-const path = require('path');
-const { registerAll } = require('./ipc');
-const { readConfig, updateConfig } = require('./config');
-const logger = require('./logger');
+const { app, BrowserWindow, screen, Menu } = require("electron");
+const path = require("path");
+const { registerAll } = require("./ipc");
+const { readConfig, updateConfig } = require("./config");
+const logger = require("./logger");
-app.setName('Photo Renamer');
+app.setName("Photo Renamer");
let mainWindow;
function createWindow() {
const windowState = getSavedWindowState();
- logger.log('main', 'createWindow', windowState);
+ logger.log("main", "createWindow", windowState);
mainWindow = new BrowserWindow({
x: windowState.bounds?.x,
y: windowState.bounds?.y,
@@ -18,33 +18,36 @@ function createWindow() {
height: windowState.bounds?.height || 820,
minWidth: 900,
minHeight: 600,
- titleBarStyle: 'hiddenInset',
- backgroundColor: '#0f0f17',
- icon: path.join(__dirname, '../../assets/icon.icns'),
+ titleBarStyle: process.platform === "darwin" ? "hiddenInset" : "hidden",
+ backgroundColor: "#0f0f17",
+ icon: path.join(__dirname, "../../assets/icon.icns"),
webPreferences: {
- preload: path.join(__dirname, '../preload/index.js'),
+ preload: path.join(__dirname, "../preload/index.js"),
contextIsolation: true,
nodeIntegration: false,
},
show: false,
});
- mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'));
+ mainWindow.loadFile(path.join(__dirname, "../renderer/index.html"));
- mainWindow.once('ready-to-show', () => {
- logger.log('main', 'ready-to-show');
+ mainWindow.once("ready-to-show", () => {
+ logger.log("main", "ready-to-show");
mainWindow.show();
if (windowState.isFullScreen) mainWindow.setFullScreen(true);
});
- mainWindow.on('close', saveWindowState);
+ mainWindow.on("close", saveWindowState);
- mainWindow.on('enter-full-screen', () => {
- mainWindow.webContents.send('fullscreen-change', true);
+ mainWindow.on("enter-full-screen", () => {
+ mainWindow.webContents.send("fullscreen-change", true);
});
- mainWindow.on('leave-full-screen', () => {
- mainWindow.webContents.send('fullscreen-change', false);
+ mainWindow.on("leave-full-screen", () => {
+ mainWindow.webContents.send("fullscreen-change", false);
});
+
+ mainWindow.on("maximize", () => mainWindow.webContents.send("maximize-change", true));
+ mainWindow.on("unmaximize", () => mainWindow.webContents.send("maximize-change", false));
}
function getSavedWindowState() {
@@ -62,36 +65,43 @@ function isUsableBounds(bounds) {
const values = [bounds.x, bounds.y, bounds.width, bounds.height];
if (!values.every(Number.isFinite)) return false;
if (bounds.width < 900 || bounds.height < 600) return false;
- return screen.getAllDisplays().some(display => rectanglesOverlap(bounds, display.workArea));
+ return screen
+ .getAllDisplays()
+ .some((display) => rectanglesOverlap(bounds, display.workArea));
}
function rectanglesOverlap(a, b) {
- return a.x < b.x + b.width &&
- a.x + a.width > b.x &&
- a.y < b.y + b.height &&
- a.y + a.height > b.y;
+ return (
+ a.x < b.x + b.width &&
+ a.x + a.width > b.x &&
+ a.y < b.y + b.height &&
+ a.y + a.height > b.y
+ );
}
function saveWindowState() {
if (!mainWindow) return;
const isFullScreen = mainWindow.isFullScreen();
- const bounds = isFullScreen ? mainWindow.getNormalBounds() : mainWindow.getBounds();
- updateConfig(cfg => {
+ const bounds = isFullScreen
+ ? mainWindow.getNormalBounds()
+ : mainWindow.getBounds();
+ updateConfig((cfg) => {
cfg.windowState = { isFullScreen, bounds };
});
}
app.whenReady().then(() => {
- logger.log('main', 'app ready');
+ logger.log("main", "app ready");
+ Menu.setApplicationMenu(null);
createWindow();
registerAll(mainWindow);
- logger.log('main', 'ipc registered');
+ logger.log("main", "ipc registered");
});
-app.on('window-all-closed', () => {
- if (process.platform !== 'darwin') app.quit();
+app.on("window-all-closed", () => {
+ if (process.platform !== "darwin") app.quit();
});
-app.on('activate', () => {
+app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
diff --git a/src/main/ipc/folder.js b/src/main/ipc/folder.js
index 474191d..ffcb724 100644
--- a/src/main/ipc/folder.js
+++ b/src/main/ipc/folder.js
@@ -6,6 +6,14 @@ const { readConfig, updateConfig } = require('../config');
const logger = require('../logger');
function register(mainWindow) {
+ ipcMain.handle('window-minimize', () => mainWindow.minimize());
+ ipcMain.handle('window-maximize', () => {
+ if (mainWindow.isMaximized()) mainWindow.unmaximize();
+ else mainWindow.maximize();
+ });
+ ipcMain.handle('window-close', () => mainWindow.close());
+ ipcMain.handle('window-is-maximized', () => mainWindow.isMaximized());
+
ipcMain.handle('select-folder', async () => {
logger.log('ipc:folder', 'select-folder requested');
const result = await dialog.showOpenDialog(mainWindow, {
@@ -22,6 +30,19 @@ function register(mainWindow) {
ipcMain.handle('get-home-dir', () => os.homedir());
+ ipcMain.handle('get-drives', () => {
+ if (process.platform !== 'win32') return [];
+ const drives = [];
+ for (let i = 67; i <= 90; i++) { // C-Z
+ const drive = `${String.fromCharCode(i)}:\\`;
+ try {
+ fs.accessSync(drive);
+ drives.push(drive);
+ } catch {}
+ }
+ return drives;
+ });
+
ipcMain.handle('get-last-folder', () => {
const cfg = readConfig();
logger.log('ipc:folder', 'get-last-folder config', { lastFolder: cfg.lastFolder || null });
diff --git a/src/preload/index.js b/src/preload/index.js
index 12e571d..786372c 100644
--- a/src/preload/index.js
+++ b/src/preload/index.js
@@ -8,7 +8,17 @@ contextBridge.exposeInMainWorld('api', {
isProduction: env.isProduction,
logEnabled: env.logEnabled,
nodeEnv: env.nodeEnv,
+ platform: process.platform,
}),
+ windowMinimize: () => ipcRenderer.invoke('window-minimize'),
+ windowMaximize: () => ipcRenderer.invoke('window-maximize'),
+ windowClose: () => ipcRenderer.invoke('window-close'),
+ windowIsMaximized: () => ipcRenderer.invoke('window-is-maximized'),
+ onMaximizeChange: (() => {
+ let _cb = null;
+ ipcRenderer.on('maximize-change', (_e, v) => { if (_cb) _cb(v); });
+ return (callback) => { _cb = callback; };
+ })(),
selectFolder: () => ipcRenderer.invoke('select-folder'),
listImages: (folder) => ipcRenderer.invoke('list-images', folder),
readImage: (path) => ipcRenderer.invoke('read-image', path),
@@ -19,6 +29,7 @@ contextBridge.exposeInMainWorld('api', {
return (callback) => { _cb = callback; };
})(),
getHomeDir: () => ipcRenderer.invoke('get-home-dir'),
+ getDrives: () => ipcRenderer.invoke('get-drives'),
getLastFolder: () => ipcRenderer.invoke('get-last-folder'),
setLastFolder: (p) => ipcRenderer.invoke('set-last-folder', p),
listDirectories: (p) => ipcRenderer.invoke('list-directories', p),
diff --git a/src/renderer/folder-controller.js b/src/renderer/folder-controller.js
index 5226119..ae182d1 100644
--- a/src/renderer/folder-controller.js
+++ b/src/renderer/folder-controller.js
@@ -14,6 +14,7 @@ export function createFolderController({
updatePreview,
}) {
let homeDir = null;
+ let _drives = [];
async function openFolder() {
log('folder', 'openFolder start', { homeDir });
@@ -28,11 +29,7 @@ export function createFolderController({
log('folder', 'openFolder selected', { folder });
if (!folder) return;
- if (homeDir && folder.startsWith(homeDir)) {
- await fileExplorer.loadTree(homeDir, folder);
- } else {
- await fileExplorer.loadTree(folder, folder);
- }
+ await fileExplorer.loadTree(homeDir, folder, _drives);
await loadFolder(folder);
}
@@ -112,6 +109,7 @@ export function createFolderController({
loadFolder,
openFolder,
setHomeDir: value => { homeDir = value; },
+ setDrives: value => { _drives = value; },
sortFiles,
updateSortButtons,
};
diff --git a/src/renderer/handlers/file-explorer.js b/src/renderer/handlers/file-explorer.js
index 6a37bce..94b0720 100644
--- a/src/renderer/handlers/file-explorer.js
+++ b/src/renderer/handlers/file-explorer.js
@@ -8,6 +8,7 @@ let _selectedPath = null;
const ICON_CLOSED = ``;
const ICON_OPEN = ``;
const ICON_HOME = ``;
+const ICON_DRIVE = ``;
export function init(container, onSelect) {
_onSelect = onSelect;
@@ -15,29 +16,47 @@ export function init(container, onSelect) {
container.classList.add('tree-root');
}
-export async function loadTree(rootPath, selectedPath) {
+export async function loadTree(rootPath, selectedPath, drives = []) {
_selectedPath = selectedPath;
const container = document.querySelector('.file-explorer');
if (!container) return;
container.innerHTML = '';
- const rootNode = createNode({
- name: rootPath.split('/').pop() || rootPath,
- path: rootPath,
- hasChildren: true,
- isRoot: true,
- });
- container.appendChild(rootNode);
- await expandNode(rootNode, selectedPath);
+ if (drives.length > 0) {
+ for (const drive of drives) {
+ const driveNode = createNode({
+ name: drive.replace(/\\$/, ''),
+ path: drive,
+ hasChildren: true,
+ isRoot: true,
+ isDrive: true,
+ });
+ container.appendChild(driveNode);
+ const selectedUpper = (selectedPath || '').toUpperCase();
+ if (selectedUpper.startsWith(drive.toUpperCase())) {
+ await expandNode(driveNode, selectedPath);
+ }
+ }
+ } else {
+ const rootNode = createNode({
+ name: rootPath.split(/[\\/]/).filter(Boolean).pop() || rootPath,
+ path: rootPath,
+ hasChildren: true,
+ isRoot: true,
+ });
+ container.appendChild(rootNode);
+ await expandNode(rootNode, selectedPath);
+ }
}
-function createNode({ name, path: nodePath, hasChildren, isRoot = false }) {
+function createNode({ name, path: nodePath, hasChildren, isRoot = false, isDrive = false }) {
const wrap = document.createElement('div');
wrap.className = 'tree-node';
wrap.dataset.path = nodePath;
wrap.dataset.expanded = 'false';
wrap.dataset.hasChildren = hasChildren ? 'true' : 'false';
wrap.dataset.isRoot = isRoot ? 'true' : 'false';
+ wrap.dataset.isDrive = isDrive ? 'true' : 'false';
const row = document.createElement('div');
row.className = 'tree-row';
@@ -50,7 +69,7 @@ function createNode({ name, path: nodePath, hasChildren, isRoot = false }) {
const icon = document.createElement('span');
icon.className = 'tree-icon';
- icon.innerHTML = isRoot ? ICON_HOME : ICON_CLOSED;
+ icon.innerHTML = isDrive ? ICON_DRIVE : (isRoot ? ICON_HOME : ICON_CLOSED);
const label = document.createElement('span');
label.className = 'tree-label';
@@ -91,7 +110,9 @@ function createNode({ name, path: nodePath, hasChildren, isRoot = false }) {
}
function nodeIcon(nodeEl) {
- return nodeEl.dataset.isRoot === 'true' ? ICON_HOME : ICON_OPEN;
+ if (nodeEl.dataset.isDrive === 'true') return ICON_DRIVE;
+ if (nodeEl.dataset.isRoot === 'true') return ICON_HOME;
+ return ICON_OPEN;
}
function selectNode(nodeEl, { notify = true } = {}) {
@@ -140,7 +161,7 @@ async function expandNode(nodeEl, autoExpandPath) {
const childNodes = childrenEl.querySelectorAll(':scope > .tree-node');
for (const child of childNodes) {
const cp = child.dataset.path;
- if (autoExpandPath === cp || autoExpandPath.startsWith(cp + '/')) {
+ if (autoExpandPath === cp || autoExpandPath.startsWith(cp + '/') || autoExpandPath.startsWith(cp + '\\')) {
await expandNode(child, autoExpandPath);
break;
}
diff --git a/src/renderer/index.html b/src/renderer/index.html
index 21bc1a6..6af79dc 100644
--- a/src/renderer/index.html
+++ b/src/renderer/index.html
@@ -10,6 +10,17 @@
Photo Renamer
+
+
+
+
+
diff --git a/src/renderer/renderer.js b/src/renderer/renderer.js
index 97687c8..56ac47b 100644
--- a/src/renderer/renderer.js
+++ b/src/renderer/renderer.js
@@ -78,19 +78,43 @@ window.api.onFullscreenChange((isFullscreen) => {
document.body.classList.toggle('fullscreen', isFullscreen);
});
+// ── 플랫폼 / 창 컨트롤 ──────────────────────────────
+if (window.api.env.platform === 'win32') {
+ document.body.classList.add('platform-win32');
+
+ const ICON_MAXIMIZE = ``;
+ const ICON_RESTORE = ``;
+
+ const btnMaximize = document.getElementById('btn-wc-maximize');
+
+ function updateMaximizeIcon(isMaximized) {
+ btnMaximize.innerHTML = isMaximized ? ICON_RESTORE : ICON_MAXIMIZE;
+ btnMaximize.title = isMaximized ? '이전 크기로' : '최대화';
+ }
+
+ window.api.windowIsMaximized().then(updateMaximizeIcon);
+ window.api.onMaximizeChange(updateMaximizeIcon);
+
+ document.getElementById('btn-wc-minimize').addEventListener('click', () => window.api.windowMinimize());
+ btnMaximize.addEventListener('click', () => window.api.windowMaximize());
+ document.getElementById('btn-wc-close').addEventListener('click', () => window.api.windowClose());
+}
+
// ── 앱 시작 ───────────────────────────────────────
(async () => {
log('startup', 'init start');
- const [lastFolder, homeDir, previewOn, sidebarWidth, fileExplorerVisible, thumbSize] = await Promise.all([
+ const [lastFolder, homeDir, previewOn, sidebarWidth, fileExplorerVisible, thumbSize, drives] = await Promise.all([
window.api.getLastFolder(),
window.api.getHomeDir(),
window.api.getPreviewOn(),
window.api.getSidebarWidth(),
window.api.getFileExplorerVisible(),
window.api.getThumbSize(),
+ window.api.getDrives(),
]);
- log('startup', 'settings loaded', { lastFolder, homeDir, previewOn, sidebarWidth, fileExplorerVisible, thumbSize });
+ log('startup', 'settings loaded', { lastFolder, homeDir, previewOn, sidebarWidth, fileExplorerVisible, thumbSize, drives });
folderController.setHomeDir(homeDir);
+ folderController.setDrives(drives);
btnPreviewToggle.classList.toggle('active', previewOn);
hoverPreview.setEnabled(previewOn);
@@ -110,7 +134,7 @@ window.api.onFullscreenChange((isFullscreen) => {
const target = lastFolder || homeDir;
log('startup', 'initial folder target', { target, fromLastFolder: Boolean(lastFolder) });
- await fileExplorer.loadTree(homeDir, target);
+ await fileExplorer.loadTree(homeDir, target, drives);
await folderController.loadFolder(target);
log('startup', 'init done');
})();
diff --git a/src/renderer/style.css b/src/renderer/style.css
index 8adf47e..617b1ec 100644
--- a/src/renderer/style.css
+++ b/src/renderer/style.css
@@ -67,6 +67,40 @@ header h1 {
font-size: 13px; font-weight: 600; color: var(--muted);
letter-spacing: .08em; text-transform: uppercase;
}
+
+/* 커스텀 창 컨트롤 버튼 (Windows) */
+.window-controls {
+ display: none;
+ position: absolute;
+ right: 0;
+ top: 0;
+ height: 44px;
+ -webkit-app-region: no-drag;
+}
+body.platform-win32 .window-controls {
+ display: flex;
+}
+.wc-btn {
+ width: 46px;
+ height: 44px;
+ border: none;
+ background: transparent;
+ color: var(--muted);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background 0.1s, color 0.1s;
+}
+.wc-btn:hover {
+ background: rgba(255,255,255,0.1);
+ color: var(--fg);
+}
+.wc-close:hover {
+ background: #c42b1c;
+ color: #fff;
+}
+
.dir-path {
font-family: 'JetBrains Mono', monospace;
font-size: 11px; color: var(--muted); opacity: .7;