Compare commits

...

3 Commits

Author SHA1 Message Date
jisangs c34fa580cd 버전 수정 반영 2026-07-03 18:22:37 +09:00
jisangs fd600001a0 Windows 메뉴바 제거, Windows 빌드 환경 적용
- Windows 빌드(npm run build) 실행을 위해서는 VScode를 관리자 권한으로 실행해야 함
2026-06-28 20:48:32 +09:00
jisangs d60832120e Windows 플랫폼 추가, VS Code 개발 환경 설정 추가 2026-06-28 18:40:58 +09:00
12 changed files with 275 additions and 60 deletions
+5
View File
@@ -1,3 +1,8 @@
### VS Code
.vscode/*
!.vscode/tasks.json
!.vscode/extensions.json
### IntelliJ ### IntelliJ
.idea/ .idea/
*.iml *.iml
+28
View File
@@ -0,0 +1,28 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "앱 실행",
"type": "shell",
"command": "npm start",
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"reveal": "always",
"panel": "shared"
}
},
{
"label": "앱 실행 (디버그)",
"type": "shell",
"command": "npm run debug",
"group": "build",
"presentation": {
"reveal": "always",
"panel": "shared"
}
}
]
}
+28 -2
View File
@@ -1,13 +1,14 @@
{ {
"name": "photo-renamer", "name": "photo-renamer",
"version": "2.0.0", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "photo-renamer", "name": "photo-renamer",
"version": "2.0.0", "version": "1.0.0",
"devDependencies": { "devDependencies": {
"cross-env": "^10.1.0",
"electron": "^28.0.0", "electron": "^28.0.0",
"electron-builder": "^24.9.1" "electron-builder": "^24.9.1"
} }
@@ -303,6 +304,13 @@
"node": ">= 10.0.0" "node": ">= 10.0.0"
} }
}, },
"node_modules/@epic-web/invariant": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz",
"integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==",
"dev": true,
"license": "MIT"
},
"node_modules/@isaacs/cliui": { "node_modules/@isaacs/cliui": {
"version": "8.0.2", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -1496,6 +1504,24 @@
"node": ">= 10" "node": ">= 10"
} }
}, },
"node_modules/cross-env": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz",
"integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@epic-web/invariant": "^1.0.0",
"cross-spawn": "^7.0.6"
},
"bin": {
"cross-env": "dist/bin/cross-env.js",
"cross-env-shell": "dist/bin/cross-env-shell.js"
},
"engines": {
"node": ">=20"
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+32 -6
View File
@@ -1,22 +1,47 @@
{ {
"name": "photo-renamer", "name": "photo-renamer",
"version": "2.0.0", "version": "1.0.0",
"description": "사진 파일 이름 정리 앱", "description": "사진 파일 이름 정리 앱",
"main": "src/main/index.js", "main": "src/main/index.js",
"scripts": { "scripts": {
"start": "NODE_ENV=development electron .", "start": "cross-env NODE_ENV=development electron .",
"debug": "NODE_ENV=development electron --inspect=9229 --remote-debugging-port=9222 . 2>&1 | tee debug.log", "debug": "cross-env NODE_ENV=development electron --inspect=9229 --remote-debugging-port=9222 .",
"build": "NODE_ENV=production electron-builder --mac --dir", "build": "cross-env NODE_ENV=production CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --win --dir",
"dist": "NODE_ENV=production electron-builder --mac" "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"
}, },
"build": { "build": {
"appId": "com.photorenamer.app", "appId": "com.photorenamer.app",
"productName": "Photo Renamer", "productName": "Photo Renamer",
"win": {
"icon": "assets/icon.png",
"target": [
{
"target": "nsis",
"arch": [
"x64"
]
}
]
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true
},
"mac": { "mac": {
"category": "public.app-category.photography", "category": "public.app-category.photography",
"icon": "assets/icon.icns", "icon": "assets/icon.icns",
"target": [ "target": [
{ "target": "dmg", "arch": ["arm64", "x64"] } {
"target": "dmg",
"arch": [
"arm64",
"x64"
]
}
] ]
}, },
"files": [ "files": [
@@ -26,6 +51,7 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"cross-env": "^10.1.0",
"electron": "^28.0.0", "electron": "^28.0.0",
"electron-builder": "^24.9.1" "electron-builder": "^24.9.1"
} }
+41 -31
View File
@@ -1,16 +1,16 @@
const { app, BrowserWindow, screen } = require('electron'); const { app, BrowserWindow, screen, Menu } = require("electron");
const path = require('path'); const path = require("path");
const { registerAll } = require('./ipc'); const { registerAll } = require("./ipc");
const { readConfig, updateConfig } = require('./config'); const { readConfig, updateConfig } = require("./config");
const logger = require('./logger'); const logger = require("./logger");
app.setName('Photo Renamer'); app.setName("Photo Renamer");
let mainWindow; let mainWindow;
function createWindow() { function createWindow() {
const windowState = getSavedWindowState(); const windowState = getSavedWindowState();
logger.log('main', 'createWindow', windowState); logger.log("main", "createWindow", windowState);
mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
x: windowState.bounds?.x, x: windowState.bounds?.x,
y: windowState.bounds?.y, y: windowState.bounds?.y,
@@ -18,33 +18,36 @@ function createWindow() {
height: windowState.bounds?.height || 820, height: windowState.bounds?.height || 820,
minWidth: 900, minWidth: 900,
minHeight: 600, minHeight: 600,
titleBarStyle: 'hiddenInset', titleBarStyle: process.platform === "darwin" ? "hiddenInset" : "hidden",
backgroundColor: '#0f0f17', backgroundColor: "#0f0f17",
icon: path.join(__dirname, '../../assets/icon.icns'), icon: path.join(__dirname, "../../assets/icon.icns"),
webPreferences: { webPreferences: {
preload: path.join(__dirname, '../preload/index.js'), preload: path.join(__dirname, "../preload/index.js"),
contextIsolation: true, contextIsolation: true,
nodeIntegration: false, nodeIntegration: false,
}, },
show: false, show: false,
}); });
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html')); mainWindow.loadFile(path.join(__dirname, "../renderer/index.html"));
mainWindow.once('ready-to-show', () => { mainWindow.once("ready-to-show", () => {
logger.log('main', 'ready-to-show'); logger.log("main", "ready-to-show");
mainWindow.show(); mainWindow.show();
if (windowState.isFullScreen) mainWindow.setFullScreen(true); if (windowState.isFullScreen) mainWindow.setFullScreen(true);
}); });
mainWindow.on('close', saveWindowState); mainWindow.on("close", saveWindowState);
mainWindow.on('enter-full-screen', () => { mainWindow.on("enter-full-screen", () => {
mainWindow.webContents.send('fullscreen-change', true); mainWindow.webContents.send("fullscreen-change", true);
}); });
mainWindow.on('leave-full-screen', () => { mainWindow.on("leave-full-screen", () => {
mainWindow.webContents.send('fullscreen-change', false); 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() { function getSavedWindowState() {
@@ -62,36 +65,43 @@ function isUsableBounds(bounds) {
const values = [bounds.x, bounds.y, bounds.width, bounds.height]; const values = [bounds.x, bounds.y, bounds.width, bounds.height];
if (!values.every(Number.isFinite)) return false; if (!values.every(Number.isFinite)) return false;
if (bounds.width < 900 || bounds.height < 600) 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) { function rectanglesOverlap(a, b) {
return a.x < b.x + b.width && return (
a.x + a.width > b.x && a.x < b.x + b.width &&
a.y < b.y + b.height && a.x + a.width > b.x &&
a.y + a.height > b.y; a.y < b.y + b.height &&
a.y + a.height > b.y
);
} }
function saveWindowState() { function saveWindowState() {
if (!mainWindow) return; if (!mainWindow) return;
const isFullScreen = mainWindow.isFullScreen(); const isFullScreen = mainWindow.isFullScreen();
const bounds = isFullScreen ? mainWindow.getNormalBounds() : mainWindow.getBounds(); const bounds = isFullScreen
updateConfig(cfg => { ? mainWindow.getNormalBounds()
: mainWindow.getBounds();
updateConfig((cfg) => {
cfg.windowState = { isFullScreen, bounds }; cfg.windowState = { isFullScreen, bounds };
}); });
} }
app.whenReady().then(() => { app.whenReady().then(() => {
logger.log('main', 'app ready'); logger.log("main", "app ready");
Menu.setApplicationMenu(null);
createWindow(); createWindow();
registerAll(mainWindow); registerAll(mainWindow);
logger.log('main', 'ipc registered'); logger.log("main", "ipc registered");
}); });
app.on('window-all-closed', () => { app.on("window-all-closed", () => {
if (process.platform !== 'darwin') app.quit(); if (process.platform !== "darwin") app.quit();
}); });
app.on('activate', () => { app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow(); if (BrowserWindow.getAllWindows().length === 0) createWindow();
}); });
+21
View File
@@ -6,6 +6,14 @@ const { readConfig, updateConfig } = require('../config');
const logger = require('../logger'); const logger = require('../logger');
function register(mainWindow) { 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 () => { ipcMain.handle('select-folder', async () => {
logger.log('ipc:folder', 'select-folder requested'); logger.log('ipc:folder', 'select-folder requested');
const result = await dialog.showOpenDialog(mainWindow, { const result = await dialog.showOpenDialog(mainWindow, {
@@ -22,6 +30,19 @@ function register(mainWindow) {
ipcMain.handle('get-home-dir', () => os.homedir()); 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', () => { ipcMain.handle('get-last-folder', () => {
const cfg = readConfig(); const cfg = readConfig();
logger.log('ipc:folder', 'get-last-folder config', { lastFolder: cfg.lastFolder || null }); logger.log('ipc:folder', 'get-last-folder config', { lastFolder: cfg.lastFolder || null });
+11
View File
@@ -8,7 +8,17 @@ contextBridge.exposeInMainWorld('api', {
isProduction: env.isProduction, isProduction: env.isProduction,
logEnabled: env.logEnabled, logEnabled: env.logEnabled,
nodeEnv: env.nodeEnv, 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'), selectFolder: () => ipcRenderer.invoke('select-folder'),
listImages: (folder) => ipcRenderer.invoke('list-images', folder), listImages: (folder) => ipcRenderer.invoke('list-images', folder),
readImage: (path) => ipcRenderer.invoke('read-image', path), readImage: (path) => ipcRenderer.invoke('read-image', path),
@@ -19,6 +29,7 @@ contextBridge.exposeInMainWorld('api', {
return (callback) => { _cb = callback; }; return (callback) => { _cb = callback; };
})(), })(),
getHomeDir: () => ipcRenderer.invoke('get-home-dir'), getHomeDir: () => ipcRenderer.invoke('get-home-dir'),
getDrives: () => ipcRenderer.invoke('get-drives'),
getLastFolder: () => ipcRenderer.invoke('get-last-folder'), getLastFolder: () => ipcRenderer.invoke('get-last-folder'),
setLastFolder: (p) => ipcRenderer.invoke('set-last-folder', p), setLastFolder: (p) => ipcRenderer.invoke('set-last-folder', p),
listDirectories: (p) => ipcRenderer.invoke('list-directories', p), listDirectories: (p) => ipcRenderer.invoke('list-directories', p),
+3 -5
View File
@@ -14,6 +14,7 @@ export function createFolderController({
updatePreview, updatePreview,
}) { }) {
let homeDir = null; let homeDir = null;
let _drives = [];
async function openFolder() { async function openFolder() {
log('folder', 'openFolder start', { homeDir }); log('folder', 'openFolder start', { homeDir });
@@ -28,11 +29,7 @@ export function createFolderController({
log('folder', 'openFolder selected', { folder }); log('folder', 'openFolder selected', { folder });
if (!folder) return; if (!folder) return;
if (homeDir && folder.startsWith(homeDir)) { await fileExplorer.loadTree(homeDir, folder, _drives);
await fileExplorer.loadTree(homeDir, folder);
} else {
await fileExplorer.loadTree(folder, folder);
}
await loadFolder(folder); await loadFolder(folder);
} }
@@ -112,6 +109,7 @@ export function createFolderController({
loadFolder, loadFolder,
openFolder, openFolder,
setHomeDir: value => { homeDir = value; }, setHomeDir: value => { homeDir = value; },
setDrives: value => { _drives = value; },
sortFiles, sortFiles,
updateSortButtons, updateSortButtons,
}; };
+34 -13
View File
@@ -8,6 +8,7 @@ let _selectedPath = null;
const ICON_CLOSED = `<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 3.5C1 2.67 1.67 2 2.5 2H5.38l1.5 1.5H11.5C12.33 3.5 13 4.17 13 5v5.5C13 11.33 12.33 12 11.5 12h-9C1.67 12 1 11.33 1 10.5V3.5Z" fill="#7c6af7" fill-opacity=".75"/><path d="M1 5.5h12v5C13 11.33 12.33 12 11.5 12h-9C1.67 12 1 11.33 1 10.5V5.5Z" fill="#9180ff" fill-opacity=".9"/></svg>`; const ICON_CLOSED = `<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 3.5C1 2.67 1.67 2 2.5 2H5.38l1.5 1.5H11.5C12.33 3.5 13 4.17 13 5v5.5C13 11.33 12.33 12 11.5 12h-9C1.67 12 1 11.33 1 10.5V3.5Z" fill="#7c6af7" fill-opacity=".75"/><path d="M1 5.5h12v5C13 11.33 12.33 12 11.5 12h-9C1.67 12 1 11.33 1 10.5V5.5Z" fill="#9180ff" fill-opacity=".9"/></svg>`;
const ICON_OPEN = `<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 3.5C1 2.67 1.67 2 2.5 2H5.38l1.5 1.5H11.5C12.33 3.5 13 4.17 13 5v5.5C13 11.33 12.33 12 11.5 12h-9C1.67 12 1 11.33 1 10.5V3.5Z" fill="#c9b8ff" fill-opacity=".7"/><path d="M1 5.5h12v5C13 11.33 12.33 12 11.5 12h-9C1.67 12 1 11.33 1 10.5V5.5Z" fill="#e0d5ff" fill-opacity=".85"/></svg>`; const ICON_OPEN = `<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 3.5C1 2.67 1.67 2 2.5 2H5.38l1.5 1.5H11.5C12.33 3.5 13 4.17 13 5v5.5C13 11.33 12.33 12 11.5 12h-9C1.67 12 1 11.33 1 10.5V3.5Z" fill="#c9b8ff" fill-opacity=".7"/><path d="M1 5.5h12v5C13 11.33 12.33 12 11.5 12h-9C1.67 12 1 11.33 1 10.5V5.5Z" fill="#e0d5ff" fill-opacity=".85"/></svg>`;
const ICON_HOME = `<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7 1.5L1 6.5V12.5H5.5V9H8.5V12.5H13V6.5L7 1.5Z" fill="#6ac8f7" fill-opacity=".9"/></svg>`; const ICON_HOME = `<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7 1.5L1 6.5V12.5H5.5V9H8.5V12.5H13V6.5L7 1.5Z" fill="#6ac8f7" fill-opacity=".9"/></svg>`;
const ICON_DRIVE = `<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="3.5" width="12" height="7" rx="1.5" fill="#7c6af7" fill-opacity=".75"/><rect x="1" y="7" width="12" height="3.5" rx="1.5" fill="#9180ff" fill-opacity=".9"/><circle cx="10.5" cy="9" r="1" fill="white" fill-opacity=".7"/></svg>`;
export function init(container, onSelect) { export function init(container, onSelect) {
_onSelect = onSelect; _onSelect = onSelect;
@@ -15,29 +16,47 @@ export function init(container, onSelect) {
container.classList.add('tree-root'); container.classList.add('tree-root');
} }
export async function loadTree(rootPath, selectedPath) { export async function loadTree(rootPath, selectedPath, drives = []) {
_selectedPath = selectedPath; _selectedPath = selectedPath;
const container = document.querySelector('.file-explorer'); const container = document.querySelector('.file-explorer');
if (!container) return; if (!container) return;
container.innerHTML = ''; container.innerHTML = '';
const rootNode = createNode({ if (drives.length > 0) {
name: rootPath.split('/').pop() || rootPath, for (const drive of drives) {
path: rootPath, const driveNode = createNode({
hasChildren: true, name: drive.replace(/\\$/, ''),
isRoot: true, path: drive,
}); hasChildren: true,
container.appendChild(rootNode); isRoot: true,
await expandNode(rootNode, selectedPath); 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'); const wrap = document.createElement('div');
wrap.className = 'tree-node'; wrap.className = 'tree-node';
wrap.dataset.path = nodePath; wrap.dataset.path = nodePath;
wrap.dataset.expanded = 'false'; wrap.dataset.expanded = 'false';
wrap.dataset.hasChildren = hasChildren ? 'true' : 'false'; wrap.dataset.hasChildren = hasChildren ? 'true' : 'false';
wrap.dataset.isRoot = isRoot ? 'true' : 'false'; wrap.dataset.isRoot = isRoot ? 'true' : 'false';
wrap.dataset.isDrive = isDrive ? 'true' : 'false';
const row = document.createElement('div'); const row = document.createElement('div');
row.className = 'tree-row'; row.className = 'tree-row';
@@ -50,7 +69,7 @@ function createNode({ name, path: nodePath, hasChildren, isRoot = false }) {
const icon = document.createElement('span'); const icon = document.createElement('span');
icon.className = 'tree-icon'; 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'); const label = document.createElement('span');
label.className = 'tree-label'; label.className = 'tree-label';
@@ -91,7 +110,9 @@ function createNode({ name, path: nodePath, hasChildren, isRoot = false }) {
} }
function nodeIcon(nodeEl) { 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 } = {}) { function selectNode(nodeEl, { notify = true } = {}) {
@@ -140,7 +161,7 @@ async function expandNode(nodeEl, autoExpandPath) {
const childNodes = childrenEl.querySelectorAll(':scope > .tree-node'); const childNodes = childrenEl.querySelectorAll(':scope > .tree-node');
for (const child of childNodes) { for (const child of childNodes) {
const cp = child.dataset.path; const cp = child.dataset.path;
if (autoExpandPath === cp || autoExpandPath.startsWith(cp + '/')) { if (autoExpandPath === cp || autoExpandPath.startsWith(cp + '/') || autoExpandPath.startsWith(cp + '\\')) {
await expandNode(child, autoExpandPath); await expandNode(child, autoExpandPath);
break; break;
} }
+11
View File
@@ -10,6 +10,17 @@
<header> <header>
<h1>Photo Renamer</h1> <h1>Photo Renamer</h1>
<div class="window-controls" id="window-controls">
<button class="wc-btn" id="btn-wc-minimize" title="최소화">
<svg width="10" height="1" viewBox="0 0 10 1"><rect width="10" height="1" fill="currentColor"/></svg>
</button>
<button class="wc-btn" id="btn-wc-maximize" title="최대화">
<svg width="10" height="10" viewBox="0 0 10 10"><rect x="0.5" y="0.5" width="9" height="9" stroke="currentColor" fill="none"/></svg>
</button>
<button class="wc-btn wc-close" id="btn-wc-close" title="닫기">
<svg width="10" height="10" viewBox="0 0 10 10"><line x1="0" y1="0" x2="10" y2="10" stroke="currentColor" stroke-width="1.2"/><line x1="10" y1="0" x2="0" y2="10" stroke="currentColor" stroke-width="1.2"/></svg>
</button>
</div>
</header> </header>
<section class="app-workspace" aria-label="사진 이름 변경 작업 영역"> <section class="app-workspace" aria-label="사진 이름 변경 작업 영역">
+27 -3
View File
@@ -78,19 +78,43 @@ window.api.onFullscreenChange((isFullscreen) => {
document.body.classList.toggle('fullscreen', isFullscreen); document.body.classList.toggle('fullscreen', isFullscreen);
}); });
// ── 플랫폼 / 창 컨트롤 ──────────────────────────────
if (window.api.env.platform === 'win32') {
document.body.classList.add('platform-win32');
const ICON_MAXIMIZE = `<svg width="10" height="10" viewBox="0 0 10 10"><rect x="0.5" y="0.5" width="9" height="9" stroke="currentColor" fill="none"/></svg>`;
const ICON_RESTORE = `<svg width="10" height="10" viewBox="0 0 10 10"><rect x="2" y="0.5" width="7.5" height="7.5" stroke="currentColor" fill="none"/><polyline points="0.5,2 0.5,9.5 8,9.5" stroke="currentColor" fill="none"/></svg>`;
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 () => { (async () => {
log('startup', 'init start'); 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.getLastFolder(),
window.api.getHomeDir(), window.api.getHomeDir(),
window.api.getPreviewOn(), window.api.getPreviewOn(),
window.api.getSidebarWidth(), window.api.getSidebarWidth(),
window.api.getFileExplorerVisible(), window.api.getFileExplorerVisible(),
window.api.getThumbSize(), 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.setHomeDir(homeDir);
folderController.setDrives(drives);
btnPreviewToggle.classList.toggle('active', previewOn); btnPreviewToggle.classList.toggle('active', previewOn);
hoverPreview.setEnabled(previewOn); hoverPreview.setEnabled(previewOn);
@@ -110,7 +134,7 @@ window.api.onFullscreenChange((isFullscreen) => {
const target = lastFolder || homeDir; const target = lastFolder || homeDir;
log('startup', 'initial folder target', { target, fromLastFolder: Boolean(lastFolder) }); log('startup', 'initial folder target', { target, fromLastFolder: Boolean(lastFolder) });
await fileExplorer.loadTree(homeDir, target); await fileExplorer.loadTree(homeDir, target, drives);
await folderController.loadFolder(target); await folderController.loadFolder(target);
log('startup', 'init done'); log('startup', 'init done');
})(); })();
+34
View File
@@ -67,6 +67,40 @@ header h1 {
font-size: 13px; font-weight: 600; color: var(--muted); font-size: 13px; font-weight: 600; color: var(--muted);
letter-spacing: .08em; text-transform: uppercase; 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 { .dir-path {
font-family: 'JetBrains Mono', monospace; font-family: 'JetBrains Mono', monospace;
font-size: 11px; color: var(--muted); opacity: .7; font-size: 11px; color: var(--muted); opacity: .7;