From 0ef8a28d54353f378ecdf72d72de8be82ed00c36 Mon Sep 17 00:00:00 2001 From: jisangs Date: Mon, 18 May 2026 11:14:10 +0900 Subject: [PATCH] =?UTF-8?q?=EC=82=AC=EC=9D=B4=EB=93=9C=EB=B0=94=20?= =?UTF-8?q?=EB=84=88=EB=B9=84,=20=EC=B9=B4=EB=93=9C=20=ED=81=AC=EA=B8=B0?= =?UTF-8?q?=20=EC=A0=95=EB=B3=B4=20=EC=A0=80=EC=9E=A5,=20=EC=95=B1=20?= =?UTF-8?q?=EC=8B=A4=ED=96=89=EC=8B=9C=20=EC=A0=81=EC=9A=A9=20+=20?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C=20=EB=AA=A8=EB=93=9C=20=EC=8B=A4=ED=96=89?= =?UTF-8?q?=EC=8B=9C=20=EB=A1=9C=EA=B7=B8=20=EC=B6=9C=EB=A0=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 6 ++- AGENTS.md | 7 ++- package.json | 7 +-- src/main/config.js | 8 +-- src/main/env.js | 20 ++++++++ src/main/index.js | 5 ++ src/main/ipc/files.js | 7 ++- src/main/ipc/folder.js | 25 ++++++++- src/main/logger.js | 23 +++++++++ src/preload/index.js | 28 ++++++++++ src/renderer/folder-controller.js | 71 +++++++++++++++++--------- src/renderer/handlers/file-explorer.js | 6 +-- src/renderer/handlers/pinch-zoom.js | 17 ++++-- src/renderer/keyboard.js | 6 +++ src/renderer/logger.js | 22 ++++++++ src/renderer/renderer.js | 20 ++++++-- 16 files changed, 234 insertions(+), 44 deletions(-) create mode 100644 src/main/env.js create mode 100644 src/main/logger.js create mode 100644 src/renderer/logger.js diff --git a/.gitignore b/.gitignore index 5cd8865..a193f72 100644 --- a/.gitignore +++ b/.gitignore @@ -8,8 +8,12 @@ node_modules/ dist/ out/ +debug.log +*.log *.dmg *.pkg +.env.production +.env.product ### macOS # Finder metadata @@ -28,4 +32,4 @@ Icon .TemporaryItems .Trashes .VolumeIcon.icns -.com.apple.timemachine.donotpresent \ No newline at end of file +.com.apple.timemachine.donotpresent diff --git a/AGENTS.md b/AGENTS.md index 74218b0..6f1893c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,7 +35,7 @@ src/ │ ├── file-utils.js # Shared main-process file helpers: image listing/data URLs, │ │ # exists check, EXDEV-safe move/copy fallback │ ├── folder.js # select-folder, list-directories, get/set-last-folder, -│ │ # get/set-preview-on, get/set-sidebar-width, +│ │ # get/set-preview-on, get/set-sidebar-width, get/set-thumb-size, │ │ # get-home-dir, reveal-folder │ └── files.js # list-images, read-image, rename-files, stage/restore-delete, │ # check-files-exist, move-files @@ -90,6 +90,7 @@ Defined in `src/main/ipc/`, bridged via `src/preload/index.js` as `window.api`: | `get-last-folder` / `set-last-folder` | invoke | Persists last opened folder in userData config.json | | `get-preview-on` / `set-preview-on` | invoke | Persists hover preview toggle state in userData config.json | | `get-sidebar-width` / `set-sidebar-width` | invoke | Persists sidebar width in userData config.json | +| `get-thumb-size` / `set-thumb-size` | invoke | Persists thumbnail/card size in userData config.json | | `reveal-folder` | invoke | Opens folder in Finder via `shell.openPath` | | `fullscreen-change` | push (main→renderer) | Sent on `enter-full-screen` / `leave-full-screen` window events | @@ -184,10 +185,12 @@ Global shortcuts are registered in `keyboard.js`. They do not fire while any `.m **Sidebar tree** (`file-explorer.js`): Lazy-load 자식 폴더. 접근 불가 폴더는 `null` 반환 → "접근 불가" 표시. 행 클릭 시 항상 펼침, 화살표 클릭 시 토글. +**Thumbnail size** (`pinch-zoom.js`): Ctrl+scroll changes CSS `--thumb-size` in the 150–400px range. The latest size is persisted as `thumbSize` in config.json and restored on startup without showing the size hint. + **Styling:** CSS는 `src/renderer/style.css`에 집중. Dark theme, CSS variables in `:root`. macOS traffic-light은 `header`의 `-webkit-app-region: drag`. Avoid inline styles in `index.html`; use small semantic classes such as `.format-arrow`, `.empty-state-hint`, and modal title variants. **App icon:** 소스는 `assets/icon.png` (2048×2048). `assets/icon.icns`는 10개 해상도(16px~1024px@2x)를 포함하며 electron-builder가 사용. `app.setName('Photo Renamer')`로 개발 모드 메뉴바 이름도 지정. **Build output:** `electron-builder` targets DMG for arm64 and x64. -**Config persistence:** `app.getPath('userData')/config.json`에 `lastFolder`, `previewOn`, `sidebarWidth`, `windowState` 저장. `windowState` includes fullscreen status and normal window bounds (`x`, `y`, `width`, `height`) and is restored on startup if the saved bounds overlap an available display. +**Config persistence:** `app.getPath('userData')/config.json`에 `lastFolder`, `previewOn`, `sidebarWidth`, `thumbSize`, `windowState` 저장. `windowState` includes fullscreen status and normal window bounds (`x`, `y`, `width`, `height`) and is restored on startup if the saved bounds overlap an available display. diff --git a/package.json b/package.json index a63fcac..79f8641 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,10 @@ "description": "사진 파일 이름 정리 앱", "main": "src/main/index.js", "scripts": { - "start": "electron .", - "build": "electron-builder --mac --dir", - "dist": "electron-builder --mac" + "start": "NODE_ENV=development electron .", + "debug": "NODE_ENV=development electron --inspect=9229 --remote-debugging-port=9222 . 2>&1 | tee debug.log", + "build": "NODE_ENV=production electron-builder --mac --dir", + "dist": "NODE_ENV=production electron-builder --mac" }, "build": { "appId": "com.photorenamer.app", diff --git a/src/main/config.js b/src/main/config.js index e12eab3..6db3626 100644 --- a/src/main/config.js +++ b/src/main/config.js @@ -2,11 +2,13 @@ const { app } = require('electron'); const path = require('path'); const fs = require('fs'); -const configPath = path.join(app.getPath('userData'), 'config.json'); +function getConfigPath() { + return path.join(app.getPath('userData'), 'config.json'); +} function readConfig() { try { - return JSON.parse(fs.readFileSync(configPath, 'utf8')); + return JSON.parse(fs.readFileSync(getConfigPath(), 'utf8')); } catch { return {}; } @@ -14,7 +16,7 @@ function readConfig() { function writeConfig(data) { try { - fs.writeFileSync(configPath, JSON.stringify(data, null, 2)); + fs.writeFileSync(getConfigPath(), JSON.stringify(data, null, 2)); } catch {} } diff --git a/src/main/env.js b/src/main/env.js new file mode 100644 index 0000000..6b3a677 --- /dev/null +++ b/src/main/env.js @@ -0,0 +1,20 @@ +const nodeEnv = normalizeNodeEnv(process.env.NODE_ENV || 'production'); +const isDevelopment = nodeEnv === 'development'; +const isProduction = nodeEnv === 'production'; +const logEnabled = isDevelopment && parseBoolean(process.env.PHOTO_RENAMER_LOGS, true); + +module.exports = { + isDevelopment, + isProduction, + logEnabled, + nodeEnv, +}; + +function normalizeNodeEnv(value) { + return value === 'product' ? 'production' : value; +} + +function parseBoolean(value, fallback) { + if (value === undefined) return fallback; + return ['1', 'true', 'yes', 'on'].includes(String(value).toLowerCase()); +} diff --git a/src/main/index.js b/src/main/index.js index adc98ea..dd5e28e 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -2,6 +2,7 @@ const { app, BrowserWindow, screen } = require('electron'); const path = require('path'); const { registerAll } = require('./ipc'); const { readConfig, updateConfig } = require('./config'); +const logger = require('./logger'); app.setName('Photo Renamer'); @@ -9,6 +10,7 @@ let mainWindow; function createWindow() { const windowState = getSavedWindowState(); + logger.log('main', 'createWindow', windowState); mainWindow = new BrowserWindow({ x: windowState.bounds?.x, y: windowState.bounds?.y, @@ -30,6 +32,7 @@ function createWindow() { mainWindow.loadFile(path.join(__dirname, '../renderer/index.html')); mainWindow.once('ready-to-show', () => { + logger.log('main', 'ready-to-show'); mainWindow.show(); if (windowState.isFullScreen) mainWindow.setFullScreen(true); }); @@ -79,8 +82,10 @@ function saveWindowState() { } app.whenReady().then(() => { + logger.log('main', 'app ready'); createWindow(); registerAll(mainWindow); + logger.log('main', 'ipc registered'); }); app.on('window-all-closed', () => { diff --git a/src/main/ipc/files.js b/src/main/ipc/files.js index be41bfc..be0b236 100644 --- a/src/main/ipc/files.js +++ b/src/main/ipc/files.js @@ -8,12 +8,17 @@ const { moveFileWithCopyFallback, readImageAsDataUrl, } = require('./file-utils'); +const logger = require('../logger'); function register() { ipcMain.handle('list-images', async (_event, folderPath) => { + logger.log('ipc:files', 'list-images requested', { folderPath }); try { - return listImages(folderPath); + const files = listImages(folderPath); + logger.log('ipc:files', 'list-images result', { folderPath, count: files.length }); + return files; } catch (e) { + logger.error('ipc:files', `list-images failed: ${folderPath}`, e); return []; } }); diff --git a/src/main/ipc/folder.js b/src/main/ipc/folder.js index a02c9a8..1bf35bf 100644 --- a/src/main/ipc/folder.js +++ b/src/main/ipc/folder.js @@ -3,13 +3,19 @@ const path = require('path'); const fs = require('fs'); const os = require('os'); const { readConfig, updateConfig } = require('../config'); +const logger = require('../logger'); function register(mainWindow) { ipcMain.handle('select-folder', async () => { + logger.log('ipc:folder', 'select-folder requested'); const result = await dialog.showOpenDialog(mainWindow, { properties: ['openDirectory'], title: '이미지 폴더 선택', }); + logger.log('ipc:folder', 'select-folder result', { + canceled: result.canceled, + filePaths: result.filePaths, + }); if (result.canceled) return null; return result.filePaths[0]; }); @@ -18,13 +24,21 @@ function register(mainWindow) { ipcMain.handle('get-last-folder', () => { const cfg = readConfig(); + logger.log('ipc:folder', 'get-last-folder config', { lastFolder: cfg.lastFolder || null }); if (cfg.lastFolder) { - try { fs.accessSync(cfg.lastFolder); return cfg.lastFolder; } catch {} + try { + fs.accessSync(cfg.lastFolder); + logger.log('ipc:folder', 'get-last-folder accessible', { lastFolder: cfg.lastFolder }); + return cfg.lastFolder; + } catch (e) { + logger.error('ipc:folder', `get-last-folder inaccessible: ${cfg.lastFolder}`, e); + } } return null; }); ipcMain.handle('set-last-folder', (_e, folderPath) => { + logger.log('ipc:folder', 'set-last-folder', { folderPath }); updateConfig(cfg => { cfg.lastFolder = folderPath; }); }); @@ -65,6 +79,15 @@ function register(mainWindow) { ipcMain.handle('set-sidebar-width', (_e, width) => { updateConfig(cfg => { cfg.sidebarWidth = width; }); }); + + ipcMain.handle('get-thumb-size', () => { + const cfg = readConfig(); + return Number.isFinite(cfg.thumbSize) ? cfg.thumbSize : null; + }); + + ipcMain.handle('set-thumb-size', (_e, size) => { + updateConfig(cfg => { cfg.thumbSize = size; }); + }); } module.exports = { register }; diff --git a/src/main/logger.js b/src/main/logger.js new file mode 100644 index 0000000..63ef184 --- /dev/null +++ b/src/main/logger.js @@ -0,0 +1,23 @@ +const { logEnabled } = require('./env'); + +function log(scope, message, data) { + if (!logEnabled) return; + const suffix = data === undefined ? '' : ` ${format(data)}`; + console.log(`[PhotoRenamer:${scope}] ${message}${suffix}`); +} + +function error(scope, message, err) { + if (!logEnabled) return; + const detail = err && err.stack ? err.stack : err; + console.error(`[PhotoRenamer:${scope}] ${message}`, detail); +} + +function format(data) { + try { + return JSON.stringify(data); + } catch { + return String(data); + } +} + +module.exports = { error, log }; diff --git a/src/preload/index.js b/src/preload/index.js index f19fade..ce13838 100644 --- a/src/preload/index.js +++ b/src/preload/index.js @@ -1,6 +1,14 @@ const { contextBridge, ipcRenderer } = require('electron'); +const env = getPreloadEnv(); + contextBridge.exposeInMainWorld('api', { + env: Object.freeze({ + isDevelopment: env.isDevelopment, + isProduction: env.isProduction, + logEnabled: env.logEnabled, + nodeEnv: env.nodeEnv, + }), selectFolder: () => ipcRenderer.invoke('select-folder'), listImages: (folder) => ipcRenderer.invoke('list-images', folder), readImage: (path) => ipcRenderer.invoke('read-image', path), @@ -19,8 +27,28 @@ contextBridge.exposeInMainWorld('api', { setPreviewOn: (on) => ipcRenderer.invoke('set-preview-on', on), getSidebarWidth: () => ipcRenderer.invoke('get-sidebar-width'), setSidebarWidth: (width) => ipcRenderer.invoke('set-sidebar-width', width), + getThumbSize: () => ipcRenderer.invoke('get-thumb-size'), + setThumbSize: (size) => ipcRenderer.invoke('set-thumb-size', size), checkFilesExist: (folder, names) => ipcRenderer.invoke('check-files-exist', folder, names), moveFiles: (args) => ipcRenderer.invoke('move-files', args), stageDeleteFiles: (paths) => ipcRenderer.invoke('stage-delete-files', paths), restoreStagedFiles: (entries) => ipcRenderer.invoke('restore-staged-files', entries), }); + +function getPreloadEnv() { + const nodeEnv = normalizeNodeEnv(process.env.NODE_ENV || 'production'); + const isDevelopment = nodeEnv === 'development'; + const isProduction = nodeEnv === 'production'; + const logEnabled = isDevelopment && parseBoolean(process.env.PHOTO_RENAMER_LOGS, true); + + return { isDevelopment, isProduction, logEnabled, nodeEnv }; +} + +function normalizeNodeEnv(value) { + return value === 'product' ? 'production' : value; +} + +function parseBoolean(value, fallback) { + if (value === undefined) return fallback; + return ['1', 'true', 'yes', 'on'].includes(String(value).toLowerCase()); +} diff --git a/src/renderer/folder-controller.js b/src/renderer/folder-controller.js index 28283b7..3ebe748 100644 --- a/src/renderer/folder-controller.js +++ b/src/renderer/folder-controller.js @@ -3,6 +3,7 @@ import { showLoading, hideLoading, setStatus } from './ui.js'; import { renderGrid, showEmptyGrid } from './grid.js'; import * as history from './history.js'; import * as fileExplorer from './handlers/file-explorer.js'; +import { error, log } from './logger.js'; export function createFolderController({ btnRename, @@ -15,10 +16,19 @@ export function createFolderController({ let homeDir = null; async function openFolder() { - const folder = await window.api.selectFolder(); - if (!folder) return; + log('folder', 'openFolder start', { homeDir }); + let folder = null; + try { + folder = await window.api.selectFolder(); + } catch (e) { + error('folder', 'selectFolder failed', e); + setStatus(`폴더 열기 실패: ${e.message}`, false); + return; + } - if (folder.startsWith(homeDir)) { + log('folder', 'openFolder selected', { folder }); + if (!folder) return; + if (homeDir && folder.startsWith(homeDir)) { await fileExplorer.loadTree(homeDir, folder); } else { await fileExplorer.loadTree(folder, folder); @@ -27,6 +37,7 @@ export function createFolderController({ } async function loadFolder(folder) { + log('folder', 'loadFolder start', { folder }); if (!folder) return; state.currentFolder = folder; dirPath.textContent = folder; @@ -34,33 +45,45 @@ export function createFolderController({ await fileExplorer.setSelectedPath(folder); showLoading('이미지 목록 불러오는 중...'); - const list = await window.api.listImages(folder); - hideLoading(); + let list = []; + try { + list = await window.api.listImages(folder); + hideLoading(); + log('folder', 'loadFolder listImages complete', { folder, count: list.length }); + + if (!list.length) { + state.files.length = 0; + state.selectedIdxs.clear(); + showEmptyGrid(); + btnRename.disabled = true; + setStatus('이미지 파일 없음', false); + log('folder', 'loadFolder empty', { folder }); + return; + } - if (!list.length) { state.files.length = 0; + state.files.push(...list); state.selectedIdxs.clear(); + state.sortMode = 'name'; + updateSortButtons(); + setStatus(`${state.files.length}개 파일 로드됨`, true); + + const folderName = folder.replace(/\\/g, '/').split('/').pop(); + if (folderName) inputPrefix.value = folderName; + + renderGrid(); + updatePreview(); + btnRename.disabled = false; + history.reset(); + history.pushReorder(state.files); + log('folder', 'loadFolder done', { folder, count: state.files.length }); + } catch (e) { + hideLoading(); + error('folder', `loadFolder failed: ${folder}`, e); showEmptyGrid(); btnRename.disabled = true; - setStatus('이미지 파일 없음', false); - return; + setStatus(`폴더 로드 실패: ${e.message}`, false); } - - state.files.length = 0; - state.files.push(...list); - state.selectedIdxs.clear(); - state.sortMode = 'name'; - updateSortButtons(); - setStatus(`${state.files.length}개 파일 로드됨`, true); - - const folderName = folder.replace(/\\/g, '/').split('/').pop(); - if (folderName) inputPrefix.value = folderName; - - renderGrid(); - updatePreview(); - btnRename.disabled = false; - history.reset(); - history.pushReorder(state.files); } async function sortFiles(mode) { diff --git a/src/renderer/handlers/file-explorer.js b/src/renderer/handlers/file-explorer.js index 0e8b6ef..6a37bce 100644 --- a/src/renderer/handlers/file-explorer.js +++ b/src/renderer/handlers/file-explorer.js @@ -94,12 +94,12 @@ function nodeIcon(nodeEl) { return nodeEl.dataset.isRoot === 'true' ? ICON_HOME : ICON_OPEN; } -function selectNode(nodeEl) { +function selectNode(nodeEl, { notify = true } = {}) { const path = nodeEl.dataset.path; document.querySelectorAll('.tree-row.active').forEach(r => r.classList.remove('active')); nodeEl.querySelector(':scope > .tree-row').classList.add('active'); _selectedPath = path; - if (_onSelect) _onSelect(path); + if (notify && _onSelect) _onSelect(path); } function collapseNode(nodeEl) { @@ -149,7 +149,7 @@ async function expandNode(nodeEl, autoExpandPath) { // Reached the target — select it if (autoExpandPath === nodeEl.dataset.path) { - selectNode(nodeEl); + selectNode(nodeEl, { notify: false }); } } diff --git a/src/renderer/handlers/pinch-zoom.js b/src/renderer/handlers/pinch-zoom.js index 41e9d0f..8f69d9f 100644 --- a/src/renderer/handlers/pinch-zoom.js +++ b/src/renderer/handlers/pinch-zoom.js @@ -2,11 +2,15 @@ const THUMB_MIN = 150; const THUMB_MAX = 400; let _thumbSizeHint; +let _onChange = null; let thumbSize = 250; let thumbHintTimer = null; +let saveTimer = null; -export function init({ gridContainer, thumbSizeHint }) { +export function init({ gridContainer, thumbSizeHint, initialSize, onChange }) { _thumbSizeHint = thumbSizeHint; + _onChange = onChange; + if (initialSize) setThumbSize(initialSize, { showHint: false, notify: false }); gridContainer.addEventListener('wheel', (e) => { if (!e.ctrlKey) return; @@ -15,10 +19,11 @@ export function init({ gridContainer, thumbSizeHint }) { }, { passive: false }); } -export function setThumbSize(size) { +export function setThumbSize(size, { showHint = true, notify = true } = {}) { thumbSize = Math.round(Math.max(THUMB_MIN, Math.min(THUMB_MAX, size))); document.documentElement.style.setProperty('--thumb-size', thumbSize + 'px'); - _showHint(thumbSize); + if (showHint) _showHint(thumbSize); + if (notify) _scheduleSave(thumbSize); } function _showHint(size) { @@ -27,3 +32,9 @@ function _showHint(size) { clearTimeout(thumbHintTimer); thumbHintTimer = setTimeout(() => _thumbSizeHint.classList.remove('show'), 1000); } + +function _scheduleSave(size) { + if (!_onChange) return; + clearTimeout(saveTimer); + saveTimer = setTimeout(() => _onChange(size), 250); +} diff --git a/src/renderer/keyboard.js b/src/renderer/keyboard.js index 19054b6..0b9bfce 100644 --- a/src/renderer/keyboard.js +++ b/src/renderer/keyboard.js @@ -1,3 +1,5 @@ +import { log } from './logger.js'; + export function initKeyboardShortcuts({ btnOpen, btnRename, @@ -15,6 +17,10 @@ export function initKeyboardShortcuts({ if (e.metaKey && e.code === 'KeyO') { e.preventDefault(); + log('keyboard', 'Cmd+O detected', { + target: e.target?.tagName || null, + btnOpenDisabled: btnOpen.disabled, + }); btnOpen.click(); } else if (e.metaKey && e.code === 'KeyS') { e.preventDefault(); diff --git a/src/renderer/logger.js b/src/renderer/logger.js new file mode 100644 index 0000000..99b080e --- /dev/null +++ b/src/renderer/logger.js @@ -0,0 +1,22 @@ +export function log(scope, message, data) { + if (!isDevelopment()) return; + const suffix = data === undefined ? '' : ` ${format(data)}`; + console.log(`[PhotoRenamer:${scope}] ${message}${suffix}`); +} + +export function error(scope, message, err) { + if (!isDevelopment()) return; + console.error(`[PhotoRenamer:${scope}] ${message}`, err); +} + +function isDevelopment() { + return window.api?.env?.logEnabled === true; +} + +function format(data) { + try { + return JSON.stringify(data); + } catch { + return String(data); + } +} diff --git a/src/renderer/renderer.js b/src/renderer/renderer.js index 71d1a94..45b6e0d 100644 --- a/src/renderer/renderer.js +++ b/src/renderer/renderer.js @@ -18,6 +18,7 @@ import * as dragDrop from './handlers/drag-drop.js'; import * as hoverPreview from './handlers/image-preview.js'; import * as fileExplorer from './handlers/file-explorer.js'; import * as sidebarResize from './handlers/sidebar-resize.js'; +import { log } from './logger.js'; // ── DOM ────────────────────────────────────────────── const btnOpen = document.getElementById('btn-open'); @@ -64,7 +65,6 @@ const callbacks = { pushHistory: (affectedPaths) => history.pushReorder(state.files, affectedPaths), }; -pinchZoom.init({ gridContainer, thumbSizeHint }); selection.init({ gridContainer, rubberBand, grid, state, callbacks }); dragDrop.init({ grid, gridContainer, dragGhostEl, state, callbacks, fileExplorerEl: fileExplorerContainer, onFolderDrop: handleFolderDrop }); hoverPreview.init(); @@ -77,12 +77,15 @@ window.api.onFullscreenChange((isFullscreen) => { // ── 앱 시작 ─────────────────────────────────────── (async () => { - const [lastFolder, homeDir, previewOn, sidebarWidth] = await Promise.all([ + log('startup', 'init start'); + const [lastFolder, homeDir, previewOn, sidebarWidth, thumbSize] = await Promise.all([ window.api.getLastFolder(), window.api.getHomeDir(), window.api.getPreviewOn(), window.api.getSidebarWidth(), + window.api.getThumbSize(), ]); + log('startup', 'settings loaded', { lastFolder, homeDir, previewOn, sidebarWidth, thumbSize }); folderController.setHomeDir(homeDir); btnPreviewToggle.classList.toggle('active', previewOn); @@ -93,16 +96,27 @@ window.api.onFullscreenChange((isFullscreen) => { initialWidth: sidebarWidth, onResizeEnd: width => window.api.setSidebarWidth(width), }); + pinchZoom.init({ + gridContainer, + thumbSizeHint, + initialSize: thumbSize, + onChange: size => window.api.setThumbSize(size), + }); const target = lastFolder || homeDir; + log('startup', 'initial folder target', { target, fromLastFolder: Boolean(lastFolder) }); await fileExplorer.loadTree(homeDir, target); await folderController.loadFolder(target); + log('startup', 'init done'); })(); // ── 버튼 / 키보드 이벤트 ───────────────────────── inputPrefix.addEventListener('input', updatePreview); inputDigits.addEventListener('input', updatePreview); -btnOpen.addEventListener('click', folderController.openFolder); +btnOpen.addEventListener('click', () => { + log('ui', 'open button clicked'); + folderController.openFolder(); +}); btnSortName.addEventListener('click', () => folderController.sortFiles('name')); btnSortDate.addEventListener('click', () => folderController.sortFiles('date')); btnRename.addEventListener('click', doRename);