src/core: 플랫폼 독립 정규화 로직 + Vitest 테스트

filter.ts: 한글 자모 코드포인트 정확 필터 (U+1100-11FF, U+A960-A97F, U+D7B0-D7FF).
normalizer.ts: APFS normalization-insensitive 정확 처리 (inode 비교).
scanner.ts: 재귀 스캔 결과 깊이 역순 정렬 (자식 먼저 rename).
types.ts: WatchedDir, RenameResult, ActivityEvent, AppSchema 등 공유 타입.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-09 15:40:53 +09:00
parent 8e67d25b3b
commit 4e92bb2690
7 changed files with 361 additions and 0 deletions

50
src/core/scanner.ts Normal file
View File

@@ -0,0 +1,50 @@
import fs, { Dirent } from 'fs';
import fsPromises from 'fs/promises';
import path from 'path';
import { shouldNormalize } from './filter';
import type { FilterOptions } from './filter';
export interface ScanEntry {
path: string;
type: 'file' | 'directory';
}
/**
* 디렉토리를 재귀적으로 스캔해 NFD→NFC 변환이 필요한 항목을 반환한다.
* 깊이 역순으로 정렬해 자식부터 rename할 수 있게 한다 (부모 경로 무효화 방지).
*/
export async function scan(
dirPath: string,
recursive: boolean,
opts?: FilterOptions
): Promise<ScanEntry[]> {
const entries: ScanEntry[] = [];
await walk(dirPath, recursive, entries, opts);
// 경로 깊이 역순: 하위 항목이 먼저 오도록
entries.sort((a, b) => b.path.split(path.sep).length - a.path.split(path.sep).length);
return entries;
}
async function walk(
dir: string,
recursive: boolean,
entries: ScanEntry[],
opts?: FilterOptions
): Promise<void> {
let dirents: Dirent[];
try {
dirents = await fsPromises.readdir(dir, { withFileTypes: true });
} catch {
return;
}
for (const dirent of dirents) {
const fullPath = path.join(dir, dirent.name);
if (shouldNormalize(dirent.name, opts)) {
entries.push({ path: fullPath, type: dirent.isDirectory() ? 'directory' : 'file' });
}
if (recursive && dirent.isDirectory()) {
await walk(fullPath, recursive, entries, opts);
}
}
}