120 lines
3.4 KiB
JavaScript
120 lines
3.4 KiB
JavaScript
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
|
|
let mainWindow;
|
|
|
|
function createWindow() {
|
|
mainWindow = new BrowserWindow({
|
|
width: 1200,
|
|
height: 820,
|
|
minWidth: 900,
|
|
minHeight: 600,
|
|
titleBarStyle: 'hiddenInset',
|
|
backgroundColor: '#0f0f17',
|
|
webPreferences: {
|
|
preload: path.join(__dirname, 'preload.js'),
|
|
contextIsolation: true,
|
|
nodeIntegration: false,
|
|
},
|
|
show: false,
|
|
});
|
|
|
|
mainWindow.loadFile(path.join(__dirname, 'index.html'));
|
|
|
|
mainWindow.once('ready-to-show', () => {
|
|
mainWindow.show();
|
|
});
|
|
}
|
|
|
|
app.whenReady().then(createWindow);
|
|
|
|
app.on('window-all-closed', () => {
|
|
if (process.platform !== 'darwin') app.quit();
|
|
});
|
|
|
|
app.on('activate', () => {
|
|
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
|
});
|
|
|
|
// ── IPC 핸들러 ──────────────────────────────────────
|
|
|
|
ipcMain.handle('select-folder', async () => {
|
|
const result = await dialog.showOpenDialog(mainWindow, {
|
|
properties: ['openDirectory'],
|
|
title: '이미지 폴더 선택',
|
|
});
|
|
if (result.canceled) return null;
|
|
return result.filePaths[0];
|
|
});
|
|
|
|
ipcMain.handle('list-images', async (_event, folderPath) => {
|
|
const EXTS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.tif', '.webp', '.heic', '.heif']);
|
|
try {
|
|
const files = fs.readdirSync(folderPath)
|
|
.filter(f => EXTS.has(path.extname(f).toLowerCase()))
|
|
.map(f => {
|
|
const fullPath = path.join(folderPath, f);
|
|
const stat = fs.statSync(fullPath);
|
|
return { name: f, path: fullPath, mtime: stat.mtimeMs, size: stat.size };
|
|
})
|
|
.sort((a, b) => a.name.localeCompare(b.name, 'ko'));
|
|
return files;
|
|
} catch (e) {
|
|
return [];
|
|
}
|
|
});
|
|
|
|
ipcMain.handle('read-image', async (_event, filePath) => {
|
|
try {
|
|
const data = fs.readFileSync(filePath);
|
|
const ext = path.extname(filePath).toLowerCase().replace('.', '');
|
|
const mime = (ext === 'jpg' || ext === 'jpeg') ? 'jpeg'
|
|
: ext === 'png' ? 'png'
|
|
: ext === 'gif' ? 'gif'
|
|
: ext === 'webp' ? 'webp'
|
|
: 'jpeg';
|
|
return `data:image/${mime};base64,${data.toString('base64')}`;
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
});
|
|
|
|
ipcMain.handle('rename-files', async (_event, { folderPath, files, prefix, digits }) => {
|
|
const errors = [];
|
|
const pad = (n) => String(n + 1).padStart(digits, '0');
|
|
|
|
// 1단계: 임시 이름으로 이동 (충돌 방지)
|
|
const tmpMap = [];
|
|
for (let i = 0; i < files.length; i++) {
|
|
const src = files[i].path;
|
|
const ext = path.extname(files[i].name).toLowerCase();
|
|
const tmp = path.join(folderPath, `__tmp_rename_${i}_${Date.now()}${ext}`);
|
|
try {
|
|
fs.renameSync(src, tmp);
|
|
tmpMap.push({ tmp, ext, idx: i });
|
|
} catch (e) {
|
|
errors.push(`${files[i].name}: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
// 2단계: 최종 이름으로 이동
|
|
const renamed = [];
|
|
for (const { tmp, ext, idx } of tmpMap) {
|
|
const newName = `${prefix}_${pad(idx)}${ext}`;
|
|
const dest = path.join(folderPath, newName);
|
|
try {
|
|
fs.renameSync(tmp, dest);
|
|
renamed.push({ oldPath: tmp, newPath: dest, newName });
|
|
} catch (e) {
|
|
errors.push(`${newName}: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
return { renamed, errors };
|
|
});
|
|
|
|
ipcMain.handle('reveal-folder', async (_event, folderPath) => {
|
|
shell.openPath(folderPath);
|
|
});
|