Metadata-Version: 2.4
Name: file-tidy
Version: 0.2.1
Summary: 규칙 기반 파일 정리 CLI — 드라이런 기본, 완전 undo, watch 모드. Rule-based file organizer with dry-run by default, full undo, and watch mode.
Author: minjunyeah
License: MIT
Keywords: file organizer,downloads,cleanup,automation,dry-run,undo
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: End Users/Desktop
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Desktop Environment :: File Managers
Classifier: Topic :: Utilities
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# file-tidy

규칙 기반 파일 정리 CLI. 폴더 하나를 지정하면 규칙 JSON에 따라 흩어진 파일을
카테고리 폴더로 자동 분류·이동한다. AI 없이 순수 결정론적 로직으로 동작하며,
기본값은 **드라이런**(아무것도 이동하지 않음)이라 실수로 인한 데이터 손실이 없다.

```text
정리 전                          정리 후 (python3 tidy.py ~/Downloads --apply)
~/Downloads/                     ~/Downloads/
├── song.wav          ─┐         ├── 01_Audio/       song.wav
├── My Stems/          ├──────→  │                   My Stems/drums.wav
│   └── drums.wav     ─┘         ├── 02_Documents/   report.pdf
├── report.pdf        ─┼──────→  ├── 03_Images/      photo.jpg
├── photo.jpg         ─┼──────→  ├── 05_Archives/    archive.zip
├── archive.zip       ─┼──────→  └── _unsorted/      unknown.xyz
└── unknown.xyz       ─┘
```

---

## 요구 사항

- Python 3.10 이상 (표준 라이브러리만 사용, 외부 의존성 없음)
- macOS / Linux / Windows 공통 동작

## 빠른 시작

```bash
git clone <이 저장소> && cd file-tidy

# 1. 미리보기 (아무것도 이동하지 않음)
python3 tidy.py ~/Downloads

# 2. 실제 실행
python3 tidy.py ~/Downloads --apply
```

---

## 사용법

### 명령어

```bash
python3 tidy.py <대상폴더>                    # 드라이런 (기본)
python3 tidy.py <대상폴더> --apply            # 실제 이동 (y/N 확인 프롬프트)
python3 tidy.py <대상폴더> --apply -r         # 하위 폴더까지 재귀 정리
python3 tidy.py <대상폴더> --undo             # 마지막 1회 실행 되돌리기
python3 tidy.py <대상폴더> --undo 3           # 최근 3회 실행 되돌리기
python3 tidy.py <대상폴더> --history          # 정리 이력 조회
python3 tidy.py <대상폴더> --watch            # 폴더 상시 감시 → 새 파일 자동 분류
python3 tidy.py <대상폴더> --stats            # 카테고리별 용량 리포트 (읽기 전용)
python3 tidy.py <대상폴더> --larger-than 100MB  # 이 크기 초과 파일만
python3 tidy.py <대상폴더> --exclude "*.tmp"  # 패턴에 맞는 파일·폴더 제외
python3 tidy.py <대상폴더> --preset music     # 내장 프리셋 규칙 사용
python3 tidy.py --init --rules my.json       # 대화형 규칙 생성기
python3 tidy.py <대상폴더> --rules my.json    # 커스텀 규칙 파일 사용
```

| 옵션 | 설명 |
|------|------|
| `target` | 정리할 대상 폴더 (필수) |
| `--apply` | 실제 파일 이동. 없으면 드라이런 |
| `--dry-run` | 드라이런 명시 지정. `--apply`와 함께 써도 드라이런이 우선 |
| `--exclude PATTERN` | 이 와일드카드 패턴에 맞는 파일·폴더 제외 (여러 번 지정 가능) |
| `--preset NAME` | 내장 프리셋 규칙 사용: `developer` `music` `student` `office` (`--rules`보다 우선) |
| `--recursive`, `-r` | 하위 폴더까지 재귀 스캔 |
| `--undo [N]` | 최근 N회 실행 되돌리기 (기본 1) |
| `--history` | `.tidy-manifest.json` 기반 이동 이력 조회 |
| `--stats` | 카테고리별 용량 리포트 (이동 없음) |
| `--init` | 대화형 규칙 JSON 생성 (`--rules` 로 저장 경로 지정, 기존 파일엔 `--force`) |
| `--watch` | 폴더를 폴링 감시해 새 파일을 자동 분류 (기존 파일은 건드리지 않음) |
| `--watch-interval SEC` | watch 폴링 간격 초 (기본 5) |
| `--larger-than SIZE` | 이 크기 초과 파일만 처리 (예: `100MB`, `2GB`) |
| `--rules <path>` | 규칙 JSON 경로 지정 (기본 `rules/default.json`) |

### 실행 흐름 예시

```text
$ python3 tidy.py ~/Downloads
[DRY-RUN] 대상: /Users/me/Downloads  (규칙: rules/default.json)

이동 예정 42개:
  01_Audio: 11개
  02_Documents: 20개
  03_Images: 8개
  _unsorted: 3개

  /Users/me/Downloads/song.wav → 01_Audio/
  /Users/me/Downloads/report.pdf → 02_Documents/
  ...

드라이런입니다. 실제 이동하려면 --apply 를 붙여 실행하세요.

$ python3 tidy.py ~/Downloads --apply
...
42개를 이동합니다. 계속할까요? [y/N] y
  이동: song.wav → 01_Audio/
  ...
42개 이동 완료. 기록: /Users/me/Downloads/.tidy-manifest.json
되돌리려면: python3 tidy.py /Users/me/Downloads --undo
```

---

## 분류 로직

각 항목은 아래 순서로 판정된다. 앞에서 매칭되면 뒤는 건너뛴다.

```text
1. 건너뜀 검사
   - 숨김 파일/폴더 (이름이 . 으로 시작)
   - 심볼릭 링크
   - .git, node_modules, __pycache__ 자체
   - git 저장소 폴더 (.git 을 품은 폴더 전체 — 내용물 포함 보호)
   - rules.settings.skip_names 에 있는 이름
   - 이미 카테고리 폴더(01_Audio 등) 안에 있는 파일

2. 폴더 키워드 매칭 (폴더만, 최상위 레벨)
   - rules.folder_rules[].match_keywords 중 하나가 폴더 이름에 포함되면
   - 폴더째로 target 으로 이동 (내용물 통째로)

3. 확장자 매칭 (파일만)
   - rules.categories[].extensions 에 있으면 해당 카테고리로

4. 어디에도 해당 없는 파일
   - settings.uncategorized_folder (기본 _unsorted) 로 이동

5. 폴더 중 키워드 매칭이 안 된 것
   - 그대로 제자리에 둠 (임의로 재분류하지 않음)
```

### 이동 시 충돌 처리

목적지에 같은 이름이 있으면 자동으로 번호를 붙인다:

```text
song.wav  →  01_Audio/song.wav       (이미 있음)
          →  01_Audio/song (2).wav   (자동 번호)
```

### 재귀 모드(`-r`)의 동작

- 하위 폴더의 **파일**은 각자 확장자에 따라 최상위 카테고리로 이동한다.
- 폴더 키워드 매칭은 최상위 레벨 폴더만 대상으로 한다 (깊은 폴더를 통째로 옮기는 사고 방지).
- 스캔 최대 깊이는 10단계로 제한된다.

---

## 규칙 커스터마이징

### 내장 프리셋

바로 쓸 수 있는 프리셋 4종을 제공한다 (`presets/` 디렉터리, pip 설치 시 번들):

| 프리셋 | 용도 | 카테고리 예 |
|--------|------|------------|
| `--preset developer` | 개발 작업 폴더 | Code / Web / Data / Notebooks / Archives |
| `--preset music` | 음악 작업 폴더 | Audio / Projects(DAW) / Samples / MIDI / Sheets |
| `--preset student` | 학생 과제 폴더 | 과제 / 발표 / 데이터 / 압축 (한글 폴더명) |
| `--preset office` | 회사 문서 폴더 | 계약서 / 문서 / 재무 / 발표 |

```bash
python3 tidy.py ~/Projects --preset developer --apply
```

프리셋은 `--rules`보다 우선하며, undo 시에도 같은 `--preset`을 지정하면 된다
(매니페스트 위치는 어느 규칙이든 동일: `.tidy-manifest.json`).

### 규칙 파일 구조

`rules/default.json` 구조:

```jsonc
{
  "categories": [
    {
      "name": "01_Audio",              // 폴더 이름 = 카테고리 이름
      "extensions": [".wav", ".mp3"],  // 이 확장자 파일들을 이 폴더로
      "description": "오디오 파일"
    }
    // ...
  ],
  "folder_rules": [
    {
      "match_keywords": ["stem", "stems"],  // 폴더 이름에 포함되면 (부분 일치)
      "target": "01_Audio",                 // 이 폴더째로 이동
      "description": "Stems 관련 폴더"
    }
  ],
  "settings": {
    "default_category": null,
    "uncategorized_folder": "_unsorted",   // 매칭 실패 파일의 행선지
    "skip_names": ["Work", "Homework"],    // 건드리지 않을 이름 (폴더 경로 일부 포함)
    "manifest_filename": ".tidy-manifest.json"
  }
}
```

### 커스텀 규칙 예시 — 사진 라이브러리용

```json
{
  "categories": [
    { "name": "RAW",  "extensions": [".cr2", ".nef", ".arw"], "description": "RAW" },
    { "name": "JPEG", "extensions": [".jpg", ".jpeg"],        "description": "JPEG" }
  ],
  "folder_rules": [],
  "settings": {
    "uncategorized_folder": "_기타",
    "skip_names": ["원본보관"],
    "manifest_filename": ".tidy-manifest.json"
  }
}
```

```bash
python3 tidy.py ~/Pictures --rules my-photo-rules.json
```

주의: 확장자를 두 카테고리에 중복 정의하면 실행 시 경고가 출력되고
먼저 정의된 쪽이 이긴다.

---

## 안전장치

| 장치 | 동작 |
|------|------|
| 기본 드라이런 | `--apply` 가 없으면 아무것도 이동하지 않음 (`--dry-run` 으로 명시 가능, `--apply` 보다 우선) |
| 확인 프롬프트 | apply 시 `y/N` 확인 (대문자 N = 기본값 취소) |
| 이동 매니페스트 | 모든 이동을 `.tidy-manifest.json` 에 기록 → `--undo` 로 완전 복원 |
| 보호 경로 거부 | `/`, `/System`, `/Library`, `/private`, `/etc`, `/usr`, `/bin`, `/sbin`, `/var`, `/Volumes` 를 대상으로 받으면 즉시 종료 (exit 2). `--undo` 도 동일 검사 적용 |
| git 저장소 보호 | `.git` 이 있는 폴더는 그 위치에 관계없이 통째로 건너뜀 |
| 심볼릭 링크 보호 | 링크는 이동하지 않음 (끊어진 링크·외부 참조 파손 방지) |
| 이름 충돌 처리 | 덮어쓰기 없이 `이름 (2).확장자` 로 번호 부여 |
| 스캔 깊이 제한 | 최대 10단계 (무한 루프·과다 스캔 방지) |
| 패턴 제외 | `--exclude "*.tmp"` 로 특정 파일·폴더를 정리 대상에서 제외 |

### 되돌리기 (undo)

```bash
python3 tidy.py ~/Downloads --undo       # 마지막 실행 복원
python3 tidy.py ~/Downloads --undo 3     # 최근 3회치를 한 번에 복원
```

- 매니페스트에 기록된 역순으로 파일을 원래 자리로 되돌린다.
- 원본이 이미 없으면(수동 삭제 등) 건너뛰고 나머지는 복원한다.
- undo 후 매니페스트에서 해당 run 기록은 제거된다.

---

## 테스트

```bash
python3 tests/test_tidy.py   # 기본 5종
python3 tests/test_v02.py    # v0.2: history / larger-than / watch
python3 tests/test_v021.py   # v0.2.1: exclude / dry-run / 날짜 하위폴더 / undo 검사
python3 tests/test_v03.py    # v0.3: --preset 내장 프리셋, --stats, --init
```

검증 항목:

| 테스트 | 내용 |
|--------|------|
| dry-run moves nothing | 드라이런에서 파일이 이동하지 않음 |
| apply + manifest | 카테고리별 이동, 매니페스트 기록, 폴더 키워드 이동 |
| undo restores files | 이동 전 상태로 완전 복원 |
| recursive mode | `-r` 로 하위 파일까지 정리 |
| protected roots rejected | `/`, `/Library` 등 거부 |
| history 조회 | 매니페스트 요약 출력 |
| --larger-than 크기 필터 | 작은 파일 제외 |
| watch 자동 분류 + undo | 새 파일 감지 → 분류 → 되돌리기 |
| watch 기존 파일 무시 | 감시 시작 전 파일은 건드리지 않음 |
| --exclude 패턴 제외 | 와일드카드 매칭 파일·폴더 제외 |
| --dry-run 플래그 | 명시 드라이런, `--apply` 보다 우선 |
| 날짜 하위폴더 규칙 | `{{YYYY-MM}}` / `{{YYYY}}` 치환 이동 + undo |
| --undo 루트 검사 | 없는 폴더·보호 경로에서 --undo 거부 |
| --preset 프리셋 4종 | 스키마 검증, 분류·undo, 한글 카테고리, `--rules` 우선순위 |
| --stats 용량 리포트 | 카테고리별 집계, 재귀 옵션, 읽기 전용 보장 |
| --init 규칙 생성기 | 대화형 JSON 생성 + 생성 규칙으로 실제 정리 |
| --init 안전장치 | `--rules` 경로 필수, 기존 파일은 `--force` 요구 |

CI: GitHub Actions가 `ubuntu`/`macos` × Python 3.10–3.13 매트릭스로 매 푸시마다 실행한다.

---

## 프로젝트 구조

```text
file-tidy/
├── tidy.py            # CLI 본체 (단일 파일)
├── rules/
│   └── default.json   # 기본 규칙 (카테고리 10종 + 폴더 키워드)
├── presets/           # 내장 프리셋 4종 (developer / music / student / office)
├── file_tidy/         # pip 패키지용 (규칙·프리셋 번들 포함)
├── tests/             # 통합 테스트 4종 (기본 / v0.2 / v0.2.1 / v0.3 프리셋)
├── docs/              # 로드맵·릴리스 노트·마스터 플랜·랜딩페이지
└── README.md
```

### tidy.py 내부 구성

| 함수 | 역할 |
|------|------|
| `load_rules()` | 규칙 JSON 로드 및 필수 키 검증 |
| `build_extension_map()` | 확장자 → 카테고리 사전 생성 (중복 정의 경고) |
| `category_for()` | 폴더 키워드 → 확장자 순 판정 |
| `is_protected()` | 시스템 경로 거부 판정 |
| `is_git_repo()` | `.git` 존재 여부로 저장소 판별 |
| `collect_entries()` | 이동 후보 수집 (숨김/링크/저장소 제외) |
| `plan_moves()` | 실제 이동 목록 생성 (충돌 번호 포함) |
| `do_apply()` | 이동 실행 + 매니페스트 기록 |
| `do_undo()` | 매니페스트 기반 복원 |

---

## FAQ

**Q. 실수로 중요한 폴더를 지정했는데 이미 apply 해버렸어요.**
`python3 tidy.py <그 폴더> --undo` 로 전부 복원됩니다. 매니페스트(`.tidy-manifest.json`)가
그 폴더 안에 있으니 삭제하지 마세요.

**Q. 특정 폴더는 영구적으로 제외하고 싶어요.**
`settings.skip_names` 에 이름을 추가하세요. 경로의 어느 위치에 있어도 해당 이름은 건너뜁니다.

**Q. 파일을 삭제하나요?**
아니요. 이 도구는 **이동만** 합니다. 삭제는 절대 하지 않습니다.

**Q. 같은 폴더를 두 번 실행하면?**
이미 카테고리 폴더 안에 있는 파일은 건너뛰므로 멱등(idempotent)합니다. 두 번째 실행은
"옮길 파일이 없습니다"로 끝납니다.

**Q. 왜 폴더 키워드 매칭은 최상위만 되나요?**
깊은 곳의 폴더를 통째로 옮기면 상대 경로 구조가 깨지고, 파일 단위 이동과 충돌하기
쉽기 때문입니다. 재귀 모드에서는 하위 파일들이 각자 카테고리로 이동하므로 충분합니다.

---

## 라이선스

개인 사용 목적. 자유롭게 수정·배포 가능.
