diff --git a/build/MacIcon-dev.icns b/build/MacIcon-dev.icns new file mode 100644 index 0000000..6d8cd9a Binary files /dev/null and b/build/MacIcon-dev.icns differ diff --git a/build/MacIcon-dev.iconset/icon_1024x1024.png b/build/MacIcon-dev.iconset/icon_1024x1024.png new file mode 100644 index 0000000..c8e9bd9 Binary files /dev/null and b/build/MacIcon-dev.iconset/icon_1024x1024.png differ diff --git a/build/MacIcon-dev.iconset/icon_128x128.png b/build/MacIcon-dev.iconset/icon_128x128.png new file mode 100644 index 0000000..e411f02 Binary files /dev/null and b/build/MacIcon-dev.iconset/icon_128x128.png differ diff --git a/build/MacIcon-dev.iconset/icon_16x16.png b/build/MacIcon-dev.iconset/icon_16x16.png new file mode 100644 index 0000000..626d5df Binary files /dev/null and b/build/MacIcon-dev.iconset/icon_16x16.png differ diff --git a/build/MacIcon-dev.iconset/icon_256x256.png b/build/MacIcon-dev.iconset/icon_256x256.png new file mode 100644 index 0000000..fc8492d Binary files /dev/null and b/build/MacIcon-dev.iconset/icon_256x256.png differ diff --git a/build/MacIcon-dev.iconset/icon_32x32.png b/build/MacIcon-dev.iconset/icon_32x32.png new file mode 100644 index 0000000..2b009ea Binary files /dev/null and b/build/MacIcon-dev.iconset/icon_32x32.png differ diff --git a/build/MacIcon-dev.iconset/icon_512x512.png b/build/MacIcon-dev.iconset/icon_512x512.png new file mode 100644 index 0000000..eb0280d Binary files /dev/null and b/build/MacIcon-dev.iconset/icon_512x512.png differ diff --git a/build/MacIcon-dev.iconset/icon_64x64.png b/build/MacIcon-dev.iconset/icon_64x64.png new file mode 100644 index 0000000..712dc48 Binary files /dev/null and b/build/MacIcon-dev.iconset/icon_64x64.png differ diff --git a/build/Macicon.icns b/build/Macicon.icns new file mode 100644 index 0000000..1af38ae Binary files /dev/null and b/build/Macicon.icns differ diff --git a/build/Macicon.iconset/icon_1024x1024.png b/build/Macicon.iconset/icon_1024x1024.png new file mode 100644 index 0000000..57fc6f6 Binary files /dev/null and b/build/Macicon.iconset/icon_1024x1024.png differ diff --git a/build/Macicon.iconset/icon_128x128.png b/build/Macicon.iconset/icon_128x128.png new file mode 100644 index 0000000..08eac63 Binary files /dev/null and b/build/Macicon.iconset/icon_128x128.png differ diff --git a/build/Macicon.iconset/icon_16x16.png b/build/Macicon.iconset/icon_16x16.png new file mode 100644 index 0000000..4ad3d79 Binary files /dev/null and b/build/Macicon.iconset/icon_16x16.png differ diff --git a/build/Macicon.iconset/icon_256x256.png b/build/Macicon.iconset/icon_256x256.png new file mode 100644 index 0000000..691eed0 Binary files /dev/null and b/build/Macicon.iconset/icon_256x256.png differ diff --git a/build/Macicon.iconset/icon_32x32.png b/build/Macicon.iconset/icon_32x32.png new file mode 100644 index 0000000..18040d5 Binary files /dev/null and b/build/Macicon.iconset/icon_32x32.png differ diff --git a/build/Macicon.iconset/icon_512x512.png b/build/Macicon.iconset/icon_512x512.png new file mode 100644 index 0000000..8b09856 Binary files /dev/null and b/build/Macicon.iconset/icon_512x512.png differ diff --git a/build/Macicon.iconset/icon_64x64.png b/build/Macicon.iconset/icon_64x64.png new file mode 100644 index 0000000..a69f663 Binary files /dev/null and b/build/Macicon.iconset/icon_64x64.png differ diff --git a/index.html b/index.html index 08cfdd6..ace74f9 100644 --- a/index.html +++ b/index.html @@ -9,18 +9,57 @@ body { font-family: Arial, sans-serif; text-align: center; - padding: 50px; + padding: 20px; } - #selected-directory { + #selected-directories { margin-top: 20px; + text-align: left; + max-width: 500px; + margin-left: auto; + margin-right: auto; + } + + .directory-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px; + border-bottom: 1px solid #ccc; + } + + .directory-path { word-break: break-all; } + .remove-button { + background-color: #ff4d4d; + border: none; + color: white; + padding: 5px 10px; + border-radius: 3px; + cursor: pointer; + } + + #log { + margin-top: 20px; + text-align: left; + white-space: pre-wrap; + background-color: #f0f0f0; + padding: 10px; + border-radius: 5px; + height: 200px; + overflow-y: scroll; + max-width: 500px; + margin-left: auto; + margin-right: auto; + } + button { padding: 10px 20px; font-size: 16px; cursor: pointer; + margin-bottom: 10px; } @@ -28,20 +67,74 @@

디렉토리 감시 애플리케이션

-
선택된 디렉토리 없음
+
+

감시 중인 디렉토리

+
+ +
+
+
+ 로그가 여기에 표시됩니다. +
diff --git a/main.js b/main.js index 7776c4c..b3d91c4 100644 --- a/main.js +++ b/main.js @@ -1,15 +1,40 @@ // main.js -const { app, BrowserWindow, dialog, ipcMain } = require("electron"); +const { + app, + BrowserWindow, + dialog, + ipcMain, + Notification, + Tray, + Menu, +} = require("electron"); const path = require("path"); const chokidar = require("chokidar"); const fs = require("fs").promises; +let tray = null; +let mainWindow = null; + +// 로그 파일 경로 지정 +const logFilePath = path.join(app.getPath("userData"), "watcher.log"); + +// 로그 기록 함수 +async function log(message) { + const timestamp = new Date().toISOString(); + const logMessage = `[${timestamp}] ${message}\n`; + await fs.appendFile(logFilePath, logMessage); +} + // 무시할 파일/디렉토리를 결정하는 함수 function shouldIgnore(itemName) { const ignoredItems = [".git", "node_modules", ".env"]; return ignoredItems.includes(itemName); } +// 현재 감시 중인 디렉토리 목록 +let watchedDirectories = []; +let watchers = {}; + // 파일 이름을 정규화하는 함수 async function normalizeFileName(filePath) { const dir = path.dirname(filePath); @@ -20,19 +45,49 @@ async function normalizeFileName(filePath) { const newPath = path.join(dir, newName); try { await fs.rename(filePath, newPath); - console.log(`이름 변경: "${oldName}" -> "${newName}"`); + await log(`이름 변경: "${oldName}" -> "${newName}"`); + + // 알림 생성 + new Notification({ + title: "이름 변경 완료", + body: `"${oldName}"이 "${newName}"으로 변경되었습니다.`, + }).show(); + + // 렌더러 프로세스로 알림 전송 + if (mainWindow) { + mainWindow.webContents.send( + "log-message", + `이름 변경: "${oldName}" -> "${newName}"` + ); + } + return newPath; } catch (error) { - console.error(`이름 변경 실패 ("${oldName}"):`, error); + await log(`이름 변경 실패 ("${oldName}"): ${error}`); + if (mainWindow) { + mainWindow.webContents.send( + "log-message", + `이름 변경 실패 ("${oldName}"): ${error}` + ); + } return filePath; } } + return filePath; } // 디렉토리를 재귀적으로 처리하는 함수 async function processDirectory(dirPath) { try { + await log(`디렉토리 처리 시작: "${dirPath}"`); + if (mainWindow) { + mainWindow.webContents.send( + "log-message", + `디렉토리 처리 시작: "${dirPath}"` + ); + } + const entries = await fs.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { @@ -47,16 +102,29 @@ async function processDirectory(dirPath) { } } await normalizeFileName(dirPath); + await log(`디렉토리 처리 완료: "${dirPath}"`); + if (mainWindow) { + mainWindow.webContents.send( + "log-message", + `디렉토리 처리 완료: "${dirPath}"` + ); + } } catch (error) { - console.error(`디렉토리 처리 중 오류 발생 ("${dirPath}"):`, error); + await log(`디렉토리 처리 중 오류 발생 ("${dirPath}"): ${error}`); + if (mainWindow) { + mainWindow.webContents.send( + "log-message", + `디렉토리 처리 중 오류 발생 ("${dirPath}"): ${error}` + ); + } } } // 창을 생성하는 함수 function createWindow() { - const win = new BrowserWindow({ + mainWindow = new BrowserWindow({ width: 600, - height: 400, + height: 500, webPreferences: { preload: path.join(__dirname, "preload.js"), // 보안상 추천 nodeIntegration: false, @@ -64,15 +132,56 @@ function createWindow() { }, }); - win.loadFile("index.html"); + mainWindow.loadFile("index.html"); // 개발자 도구 열기 (배포 시 제거 권장) - // win.webContents.openDevTools(); + // mainWindow.webContents.openDevTools(); + + mainWindow.on("closed", function () { + mainWindow = null; + }); } -// 애플리케이션 준비 시 창 생성 +// 트레이 메뉴 설정 +function setTray() { + tray = new Tray(path.join(__dirname, "icon.png")); // 메뉴 바 아이콘 경로 (.png 또는 .icns) + + const contextMenu = Menu.buildFromTemplate([ + { + label: "열기", + click: () => { + if (mainWindow === null) { + createWindow(); + } else { + mainWindow.show(); + } + }, + }, + { + label: "종료", + click: () => { + app.quit(); + }, + }, + ]); + + tray.setToolTip("Directory Watcher App"); + tray.setContextMenu(contextMenu); + + // 더블 클릭 시 창 열기 + tray.on("double-click", () => { + if (mainWindow === null) { + createWindow(); + } else { + mainWindow.show(); + } + }); +} + +// 애플리케이션 준비 시 창 및 트레이 설정 app.whenReady().then(() => { createWindow(); + setTray(); app.on("activate", function () { if (BrowserWindow.getAllWindows().length === 0) createWindow(); @@ -85,64 +194,138 @@ app.on("window-all-closed", function () { }); // 디렉토리 감시 로직 처리 -ipcMain.handle("select-directory", async () => { +ipcMain.handle("select-directories", async () => { const result = await dialog.showOpenDialog({ - properties: ["openDirectory"], + properties: ["openDirectory", "multiSelections"], }); if (result.canceled) { return { canceled: true }; } else { - const selectedPath = result.filePaths[0]; - console.log(`선택된 디렉토리: ${selectedPath}`); - - // 기존 감시자 종료 (필요 시) - if (global.watcher) { - global.watcher.close(); + const selectedPaths = result.filePaths; + console.log(`선택된 디렉토리: ${selectedPaths.join(", ")}`); + await log(`선택된 디렉토리: "${selectedPaths.join('", "')}"`); + if (mainWindow) { + mainWindow.webContents.send( + "log-message", + `선택된 디렉토리: "${selectedPaths.join('", "')}"` + ); } - // 디렉토리 초기 처리 - await processDirectory(selectedPath); + for (const selectedPath of selectedPaths) { + if (!watchedDirectories.includes(selectedPath)) { + watchedDirectories.push(selectedPath); + await processDirectory(selectedPath); - // 디렉토리 감시 시작 - global.watcher = chokidar.watch(selectedPath, { - ignored: (pathStr) => { - const baseName = path.basename(pathStr); - return shouldIgnore(baseName); - }, - persistent: true, - ignoreInitial: false, - awaitWriteFinish: { - stabilityThreshold: 200, - pollInterval: 100, - }, - depth: Infinity, - }); + // chokidar watcher 설정 + const watcher = chokidar.watch(selectedPath, { + ignored: (pathStr) => { + const baseName = path.basename(pathStr); + return shouldIgnore(baseName); + }, + persistent: true, + ignoreInitial: false, + awaitWriteFinish: { + stabilityThreshold: 200, + pollInterval: 100, + }, + depth: Infinity, + }); - global.watcher - .on("add", async (filePath) => { - console.log(`파일 추가됨: "${filePath}"`); - await normalizeFileName(filePath); - }) - .on("change", async (filePath) => { - console.log(`파일 변경됨: "${filePath}"`); - await normalizeFileName(filePath); - }) - .on("unlink", (filePath) => { - console.log(`파일 삭제됨: "${filePath}"`); - }) - .on("addDir", async (dirPath) => { - console.log(`디렉토리 추가됨: "${dirPath}"`); - await processDirectory(dirPath); - }) - .on("unlinkDir", (dirPath) => { - console.log(`디렉토리 삭제됨: "${dirPath}"`); - }) - .on("error", (error) => console.error(`Watcher error: ${error}`)) - .on("ready", () => { - console.log("초기 스캔 완료. 변경 감시 중..."); - }); + watchers[selectedPath] = watcher; - return { canceled: false, path: selectedPath }; + watcher + .on("add", async (filePath) => { + await log(`파일 추가됨: "${filePath}"`); + if (mainWindow) { + mainWindow.webContents.send( + "log-message", + `파일 추가됨: "${filePath}"` + ); + } + await normalizeFileName(filePath); + }) + .on("change", async (filePath) => { + await log(`파일 변경됨: "${filePath}"`); + if (mainWindow) { + mainWindow.webContents.send( + "log-message", + `파일 변경됨: "${filePath}"` + ); + } + await normalizeFileName(filePath); + }) + .on("unlink", async (filePath) => { + await log(`파일 삭제됨: "${filePath}"`); + if (mainWindow) { + mainWindow.webContents.send( + "log-message", + `파일 삭제됨: "${filePath}"` + ); + } + }) + .on("addDir", async (dirPath) => { + await log(`디렉토리 추가됨: "${dirPath}"`); + if (mainWindow) { + mainWindow.webContents.send( + "log-message", + `디렉토리 추가됨: "${dirPath}"` + ); + } + await processDirectory(dirPath); + }) + .on("unlinkDir", async (dirPath) => { + await log(`디렉토리 삭제됨: "${dirPath}"`); + if (mainWindow) { + mainWindow.webContents.send( + "log-message", + `디렉토리 삭제됨: "${dirPath}"` + ); + } + }) + .on("error", async (error) => { + await log(`Watcher error: ${error}`); + if (mainWindow) { + mainWindow.webContents.send( + "log-message", + `Watcher error: ${error}` + ); + } + }) + .on("ready", () => { + log("초기 스캔 완료. 변경 감시 중..."); + if (mainWindow) { + mainWindow.webContents.send( + "log-message", + "초기 스캔 완료. 변경 감시 중..." + ); + } + }); + } + } + + // 반환값으로 선택된 경로들을 전달 + return { canceled: false, paths: selectedPaths }; + } +}); + +// 디렉토리 제거 핸들러 +ipcMain.handle("remove-directory", async (event, dirPath) => { + if (watchedDirectories.includes(dirPath)) { + watchedDirectories = watchedDirectories.filter((path) => path !== dirPath); + if (watchers[dirPath]) { + await watchers[dirPath].close(); + delete watchers[dirPath]; + await log(`디렉토리 감시 중지: "${dirPath}"`); + if (mainWindow) { + mainWindow.webContents.send( + "log-message", + `디렉토리 감시 중지: "${dirPath}"` + ); + } + } + return { success: true }; + } else { + return { success: false, message: "디렉토리가 감시 목록에 없습니다." }; } }); diff --git a/package.json b/package.json index fafb0a0..b8818d7 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ }, "scripts": { "start": "electron .", - "package": "electron-packager . NFD2NF --platform=darwin --arch=x64 --icon=build/icons/MacIcon.icns --overwrite --prune=true --out=dist", + "package": "electron-packager . NFD2NF --platform=darwin --arch=arm64,x64 --icon=build/icons/MacIcon.icns --overwrite --prune=true --out=dist", + "package dev": "electron-packager . NFD2NF --platform=darwin --arch=arm64,x64 --icon=build/icons/MacIcon-dev.icns --overwrite --prune=true --out=dist --asar --app-bundle-id=com.pieroot.nfd2nfc", "pkg": "pkg normalize.js --target node16-macos-x64,node16-linux-x64,node16-win-x64 --output ./dist/NFD2NFC" }, "directories": { diff --git a/preload.js b/preload.js index b7b251d..7d1260d 100644 --- a/preload.js +++ b/preload.js @@ -2,5 +2,8 @@ const { contextBridge, ipcRenderer } = require("electron"); contextBridge.exposeInMainWorld("electronAPI", { - selectDirectory: () => ipcRenderer.invoke("select-directory"), + selectDirectories: () => ipcRenderer.invoke("select-directories"), + removeDirectory: (dirPath) => ipcRenderer.invoke("remove-directory", dirPath), + onLog: (callback) => + ipcRenderer.on("log-message", (event, message) => callback(message)), });