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)),
});