From fd600001a09d7d4adbe39caddd03590d026c3d18 Mon Sep 17 00:00:00 2001 From: jisangs Date: Sun, 28 Jun 2026 20:48:32 +0900 Subject: [PATCH] =?UTF-8?q?Windows=20=EB=A9=94=EB=89=B4=EB=B0=94=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0,=20Windows=20=EB=B9=8C=EB=93=9C=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Windows 빌드(npm run build) 실행을 위해서는 VScode를 관리자 권한으로 실행해야 함 --- package.json | 8 ++- src/main/index.js | 72 +++++++++++++++----------- src/main/ipc/folder.js | 21 ++++++++ src/preload/index.js | 11 ++++ src/renderer/folder-controller.js | 8 ++- src/renderer/handlers/file-explorer.js | 47 ++++++++++++----- src/renderer/index.html | 11 ++++ src/renderer/renderer.js | 30 +++++++++-- src/renderer/style.css | 34 ++++++++++++ 9 files changed, 185 insertions(+), 57 deletions(-) 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;