사이드바 너비, 카드 크기 정보 저장, 앱 실행시 적용 + 개발 모드 실행시 로그 출력

This commit is contained in:
2026-05-18 11:14:10 +09:00
parent d74c2e055d
commit 0ef8a28d54
16 changed files with 234 additions and 44 deletions
+4
View File
@@ -8,8 +8,12 @@
node_modules/ node_modules/
dist/ dist/
out/ out/
debug.log
*.log
*.dmg *.dmg
*.pkg *.pkg
.env.production
.env.product
### macOS ### macOS
# Finder metadata # Finder metadata
+5 -2
View File
@@ -35,7 +35,7 @@ src/
│ ├── file-utils.js # Shared main-process file helpers: image listing/data URLs, │ ├── file-utils.js # Shared main-process file helpers: image listing/data URLs,
│ │ # exists check, EXDEV-safe move/copy fallback │ │ # exists check, EXDEV-safe move/copy fallback
│ ├── folder.js # select-folder, list-directories, get/set-last-folder, │ ├── 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 │ │ # get-home-dir, reveal-folder
│ └── files.js # list-images, read-image, rename-files, stage/restore-delete, │ └── files.js # list-images, read-image, rename-files, stage/restore-delete,
│ # check-files-exist, move-files │ # 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-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-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-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` | | `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 | | `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` 반환 → "접근 불가" 표시. 행 클릭 시 항상 펼침, 화살표 클릭 시 토글. **Sidebar tree** (`file-explorer.js`): Lazy-load 자식 폴더. 접근 불가 폴더는 `null` 반환 → "접근 불가" 표시. 행 클릭 시 항상 펼침, 화살표 클릭 시 토글.
**Thumbnail size** (`pinch-zoom.js`): Ctrl+scroll changes CSS `--thumb-size` in the 150400px 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. **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')`로 개발 모드 메뉴바 이름도 지정. **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. **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.
+4 -3
View File
@@ -4,9 +4,10 @@
"description": "사진 파일 이름 정리 앱", "description": "사진 파일 이름 정리 앱",
"main": "src/main/index.js", "main": "src/main/index.js",
"scripts": { "scripts": {
"start": "electron .", "start": "NODE_ENV=development electron .",
"build": "electron-builder --mac --dir", "debug": "NODE_ENV=development electron --inspect=9229 --remote-debugging-port=9222 . 2>&1 | tee debug.log",
"dist": "electron-builder --mac" "build": "NODE_ENV=production electron-builder --mac --dir",
"dist": "NODE_ENV=production electron-builder --mac"
}, },
"build": { "build": {
"appId": "com.photorenamer.app", "appId": "com.photorenamer.app",
+5 -3
View File
@@ -2,11 +2,13 @@ const { app } = require('electron');
const path = require('path'); const path = require('path');
const fs = require('fs'); 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() { function readConfig() {
try { try {
return JSON.parse(fs.readFileSync(configPath, 'utf8')); return JSON.parse(fs.readFileSync(getConfigPath(), 'utf8'));
} catch { } catch {
return {}; return {};
} }
@@ -14,7 +16,7 @@ function readConfig() {
function writeConfig(data) { function writeConfig(data) {
try { try {
fs.writeFileSync(configPath, JSON.stringify(data, null, 2)); fs.writeFileSync(getConfigPath(), JSON.stringify(data, null, 2));
} catch {} } catch {}
} }
+20
View File
@@ -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());
}
+5
View File
@@ -2,6 +2,7 @@ const { app, BrowserWindow, screen } = 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');
app.setName('Photo Renamer'); app.setName('Photo Renamer');
@@ -9,6 +10,7 @@ let mainWindow;
function createWindow() { function createWindow() {
const windowState = getSavedWindowState(); const windowState = getSavedWindowState();
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,
@@ -30,6 +32,7 @@ function createWindow() {
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');
mainWindow.show(); mainWindow.show();
if (windowState.isFullScreen) mainWindow.setFullScreen(true); if (windowState.isFullScreen) mainWindow.setFullScreen(true);
}); });
@@ -79,8 +82,10 @@ function saveWindowState() {
} }
app.whenReady().then(() => { app.whenReady().then(() => {
logger.log('main', 'app ready');
createWindow(); createWindow();
registerAll(mainWindow); registerAll(mainWindow);
logger.log('main', 'ipc registered');
}); });
app.on('window-all-closed', () => { app.on('window-all-closed', () => {
+6 -1
View File
@@ -8,12 +8,17 @@ const {
moveFileWithCopyFallback, moveFileWithCopyFallback,
readImageAsDataUrl, readImageAsDataUrl,
} = require('./file-utils'); } = require('./file-utils');
const logger = require('../logger');
function register() { function register() {
ipcMain.handle('list-images', async (_event, folderPath) => { ipcMain.handle('list-images', async (_event, folderPath) => {
logger.log('ipc:files', 'list-images requested', { folderPath });
try { try {
return listImages(folderPath); const files = listImages(folderPath);
logger.log('ipc:files', 'list-images result', { folderPath, count: files.length });
return files;
} catch (e) { } catch (e) {
logger.error('ipc:files', `list-images failed: ${folderPath}`, e);
return []; return [];
} }
}); });
+24 -1
View File
@@ -3,13 +3,19 @@ const path = require('path');
const fs = require('fs'); const fs = require('fs');
const os = require('os'); const os = require('os');
const { readConfig, updateConfig } = require('../config'); const { readConfig, updateConfig } = require('../config');
const logger = require('../logger');
function register(mainWindow) { function register(mainWindow) {
ipcMain.handle('select-folder', async () => { ipcMain.handle('select-folder', async () => {
logger.log('ipc:folder', 'select-folder requested');
const result = await dialog.showOpenDialog(mainWindow, { const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openDirectory'], properties: ['openDirectory'],
title: '이미지 폴더 선택', title: '이미지 폴더 선택',
}); });
logger.log('ipc:folder', 'select-folder result', {
canceled: result.canceled,
filePaths: result.filePaths,
});
if (result.canceled) return null; if (result.canceled) return null;
return result.filePaths[0]; return result.filePaths[0];
}); });
@@ -18,13 +24,21 @@ function register(mainWindow) {
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 });
if (cfg.lastFolder) { 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; return null;
}); });
ipcMain.handle('set-last-folder', (_e, folderPath) => { ipcMain.handle('set-last-folder', (_e, folderPath) => {
logger.log('ipc:folder', 'set-last-folder', { folderPath });
updateConfig(cfg => { cfg.lastFolder = folderPath; }); updateConfig(cfg => { cfg.lastFolder = folderPath; });
}); });
@@ -65,6 +79,15 @@ function register(mainWindow) {
ipcMain.handle('set-sidebar-width', (_e, width) => { ipcMain.handle('set-sidebar-width', (_e, width) => {
updateConfig(cfg => { cfg.sidebarWidth = 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 }; module.exports = { register };
+23
View File
@@ -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 };
+28
View File
@@ -1,6 +1,14 @@
const { contextBridge, ipcRenderer } = require('electron'); const { contextBridge, ipcRenderer } = require('electron');
const env = getPreloadEnv();
contextBridge.exposeInMainWorld('api', { contextBridge.exposeInMainWorld('api', {
env: Object.freeze({
isDevelopment: env.isDevelopment,
isProduction: env.isProduction,
logEnabled: env.logEnabled,
nodeEnv: env.nodeEnv,
}),
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,8 +27,28 @@ contextBridge.exposeInMainWorld('api', {
setPreviewOn: (on) => ipcRenderer.invoke('set-preview-on', on), setPreviewOn: (on) => ipcRenderer.invoke('set-preview-on', on),
getSidebarWidth: () => ipcRenderer.invoke('get-sidebar-width'), getSidebarWidth: () => ipcRenderer.invoke('get-sidebar-width'),
setSidebarWidth: (width) => ipcRenderer.invoke('set-sidebar-width', 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), checkFilesExist: (folder, names) => ipcRenderer.invoke('check-files-exist', folder, names),
moveFiles: (args) => ipcRenderer.invoke('move-files', args), moveFiles: (args) => ipcRenderer.invoke('move-files', args),
stageDeleteFiles: (paths) => ipcRenderer.invoke('stage-delete-files', paths), stageDeleteFiles: (paths) => ipcRenderer.invoke('stage-delete-files', paths),
restoreStagedFiles: (entries) => ipcRenderer.invoke('restore-staged-files', entries), 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());
}
+27 -4
View File
@@ -3,6 +3,7 @@ import { showLoading, hideLoading, setStatus } from './ui.js';
import { renderGrid, showEmptyGrid } from './grid.js'; import { renderGrid, showEmptyGrid } from './grid.js';
import * as history from './history.js'; import * as history from './history.js';
import * as fileExplorer from './handlers/file-explorer.js'; import * as fileExplorer from './handlers/file-explorer.js';
import { error, log } from './logger.js';
export function createFolderController({ export function createFolderController({
btnRename, btnRename,
@@ -15,10 +16,19 @@ export function createFolderController({
let homeDir = null; let homeDir = null;
async function openFolder() { async function openFolder() {
const folder = await window.api.selectFolder(); log('folder', 'openFolder start', { homeDir });
if (!folder) return; 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); await fileExplorer.loadTree(homeDir, folder);
} else { } else {
await fileExplorer.loadTree(folder, folder); await fileExplorer.loadTree(folder, folder);
@@ -27,6 +37,7 @@ export function createFolderController({
} }
async function loadFolder(folder) { async function loadFolder(folder) {
log('folder', 'loadFolder start', { folder });
if (!folder) return; if (!folder) return;
state.currentFolder = folder; state.currentFolder = folder;
dirPath.textContent = folder; dirPath.textContent = folder;
@@ -34,8 +45,11 @@ export function createFolderController({
await fileExplorer.setSelectedPath(folder); await fileExplorer.setSelectedPath(folder);
showLoading('이미지 목록 불러오는 중...'); showLoading('이미지 목록 불러오는 중...');
const list = await window.api.listImages(folder); let list = [];
try {
list = await window.api.listImages(folder);
hideLoading(); hideLoading();
log('folder', 'loadFolder listImages complete', { folder, count: list.length });
if (!list.length) { if (!list.length) {
state.files.length = 0; state.files.length = 0;
@@ -43,6 +57,7 @@ export function createFolderController({
showEmptyGrid(); showEmptyGrid();
btnRename.disabled = true; btnRename.disabled = true;
setStatus('이미지 파일 없음', false); setStatus('이미지 파일 없음', false);
log('folder', 'loadFolder empty', { folder });
return; return;
} }
@@ -61,6 +76,14 @@ export function createFolderController({
btnRename.disabled = false; btnRename.disabled = false;
history.reset(); history.reset();
history.pushReorder(state.files); 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(`폴더 로드 실패: ${e.message}`, false);
}
} }
async function sortFiles(mode) { async function sortFiles(mode) {
+3 -3
View File
@@ -94,12 +94,12 @@ function nodeIcon(nodeEl) {
return nodeEl.dataset.isRoot === 'true' ? ICON_HOME : ICON_OPEN; return nodeEl.dataset.isRoot === 'true' ? ICON_HOME : ICON_OPEN;
} }
function selectNode(nodeEl) { function selectNode(nodeEl, { notify = true } = {}) {
const path = nodeEl.dataset.path; const path = nodeEl.dataset.path;
document.querySelectorAll('.tree-row.active').forEach(r => r.classList.remove('active')); document.querySelectorAll('.tree-row.active').forEach(r => r.classList.remove('active'));
nodeEl.querySelector(':scope > .tree-row').classList.add('active'); nodeEl.querySelector(':scope > .tree-row').classList.add('active');
_selectedPath = path; _selectedPath = path;
if (_onSelect) _onSelect(path); if (notify && _onSelect) _onSelect(path);
} }
function collapseNode(nodeEl) { function collapseNode(nodeEl) {
@@ -149,7 +149,7 @@ async function expandNode(nodeEl, autoExpandPath) {
// Reached the target — select it // Reached the target — select it
if (autoExpandPath === nodeEl.dataset.path) { if (autoExpandPath === nodeEl.dataset.path) {
selectNode(nodeEl); selectNode(nodeEl, { notify: false });
} }
} }
+14 -3
View File
@@ -2,11 +2,15 @@ const THUMB_MIN = 150;
const THUMB_MAX = 400; const THUMB_MAX = 400;
let _thumbSizeHint; let _thumbSizeHint;
let _onChange = null;
let thumbSize = 250; let thumbSize = 250;
let thumbHintTimer = null; let thumbHintTimer = null;
let saveTimer = null;
export function init({ gridContainer, thumbSizeHint }) { export function init({ gridContainer, thumbSizeHint, initialSize, onChange }) {
_thumbSizeHint = thumbSizeHint; _thumbSizeHint = thumbSizeHint;
_onChange = onChange;
if (initialSize) setThumbSize(initialSize, { showHint: false, notify: false });
gridContainer.addEventListener('wheel', (e) => { gridContainer.addEventListener('wheel', (e) => {
if (!e.ctrlKey) return; if (!e.ctrlKey) return;
@@ -15,10 +19,11 @@ export function init({ gridContainer, thumbSizeHint }) {
}, { passive: false }); }, { 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))); thumbSize = Math.round(Math.max(THUMB_MIN, Math.min(THUMB_MAX, size)));
document.documentElement.style.setProperty('--thumb-size', thumbSize + 'px'); document.documentElement.style.setProperty('--thumb-size', thumbSize + 'px');
_showHint(thumbSize); if (showHint) _showHint(thumbSize);
if (notify) _scheduleSave(thumbSize);
} }
function _showHint(size) { function _showHint(size) {
@@ -27,3 +32,9 @@ function _showHint(size) {
clearTimeout(thumbHintTimer); clearTimeout(thumbHintTimer);
thumbHintTimer = setTimeout(() => _thumbSizeHint.classList.remove('show'), 1000); thumbHintTimer = setTimeout(() => _thumbSizeHint.classList.remove('show'), 1000);
} }
function _scheduleSave(size) {
if (!_onChange) return;
clearTimeout(saveTimer);
saveTimer = setTimeout(() => _onChange(size), 250);
}
+6
View File
@@ -1,3 +1,5 @@
import { log } from './logger.js';
export function initKeyboardShortcuts({ export function initKeyboardShortcuts({
btnOpen, btnOpen,
btnRename, btnRename,
@@ -15,6 +17,10 @@ export function initKeyboardShortcuts({
if (e.metaKey && e.code === 'KeyO') { if (e.metaKey && e.code === 'KeyO') {
e.preventDefault(); e.preventDefault();
log('keyboard', 'Cmd+O detected', {
target: e.target?.tagName || null,
btnOpenDisabled: btnOpen.disabled,
});
btnOpen.click(); btnOpen.click();
} else if (e.metaKey && e.code === 'KeyS') { } else if (e.metaKey && e.code === 'KeyS') {
e.preventDefault(); e.preventDefault();
+22
View File
@@ -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);
}
}
+17 -3
View File
@@ -18,6 +18,7 @@ import * as dragDrop from './handlers/drag-drop.js';
import * as hoverPreview from './handlers/image-preview.js'; import * as hoverPreview from './handlers/image-preview.js';
import * as fileExplorer from './handlers/file-explorer.js'; import * as fileExplorer from './handlers/file-explorer.js';
import * as sidebarResize from './handlers/sidebar-resize.js'; import * as sidebarResize from './handlers/sidebar-resize.js';
import { log } from './logger.js';
// ── DOM ────────────────────────────────────────────── // ── DOM ──────────────────────────────────────────────
const btnOpen = document.getElementById('btn-open'); const btnOpen = document.getElementById('btn-open');
@@ -64,7 +65,6 @@ const callbacks = {
pushHistory: (affectedPaths) => history.pushReorder(state.files, affectedPaths), pushHistory: (affectedPaths) => history.pushReorder(state.files, affectedPaths),
}; };
pinchZoom.init({ gridContainer, thumbSizeHint });
selection.init({ gridContainer, rubberBand, grid, state, callbacks }); selection.init({ gridContainer, rubberBand, grid, state, callbacks });
dragDrop.init({ grid, gridContainer, dragGhostEl, state, callbacks, fileExplorerEl: fileExplorerContainer, onFolderDrop: handleFolderDrop }); dragDrop.init({ grid, gridContainer, dragGhostEl, state, callbacks, fileExplorerEl: fileExplorerContainer, onFolderDrop: handleFolderDrop });
hoverPreview.init(); hoverPreview.init();
@@ -77,12 +77,15 @@ window.api.onFullscreenChange((isFullscreen) => {
// ── 앱 시작 ─────────────────────────────────────── // ── 앱 시작 ───────────────────────────────────────
(async () => { (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.getLastFolder(),
window.api.getHomeDir(), window.api.getHomeDir(),
window.api.getPreviewOn(), window.api.getPreviewOn(),
window.api.getSidebarWidth(), window.api.getSidebarWidth(),
window.api.getThumbSize(),
]); ]);
log('startup', 'settings loaded', { lastFolder, homeDir, previewOn, sidebarWidth, thumbSize });
folderController.setHomeDir(homeDir); folderController.setHomeDir(homeDir);
btnPreviewToggle.classList.toggle('active', previewOn); btnPreviewToggle.classList.toggle('active', previewOn);
@@ -93,16 +96,27 @@ window.api.onFullscreenChange((isFullscreen) => {
initialWidth: sidebarWidth, initialWidth: sidebarWidth,
onResizeEnd: width => window.api.setSidebarWidth(width), onResizeEnd: width => window.api.setSidebarWidth(width),
}); });
pinchZoom.init({
gridContainer,
thumbSizeHint,
initialSize: thumbSize,
onChange: size => window.api.setThumbSize(size),
});
const target = lastFolder || homeDir; const target = lastFolder || homeDir;
log('startup', 'initial folder target', { target, fromLastFolder: Boolean(lastFolder) });
await fileExplorer.loadTree(homeDir, target); await fileExplorer.loadTree(homeDir, target);
await folderController.loadFolder(target); await folderController.loadFolder(target);
log('startup', 'init done');
})(); })();
// ── 버튼 / 키보드 이벤트 ───────────────────────── // ── 버튼 / 키보드 이벤트 ─────────────────────────
inputPrefix.addEventListener('input', updatePreview); inputPrefix.addEventListener('input', updatePreview);
inputDigits.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')); btnSortName.addEventListener('click', () => folderController.sortFiles('name'));
btnSortDate.addEventListener('click', () => folderController.sortFiles('date')); btnSortDate.addEventListener('click', () => folderController.sortFiles('date'));
btnRename.addEventListener('click', doRename); btnRename.addEventListener('click', doRename);