๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐Ÿ’ป ํ”„๋กœ์ ํŠธ/๐Ÿค– AI ๋ฐ์Šคํฌํ†ฑ ์ •๋ฆฌ ๐Ÿ–ฅ๏ธ

[Python] ํŒŒ์ด์ฌ์œผ๋กœ ๊ทœ์น™๊ธฐ๋ฐ˜์— ์˜ํ•ด ๋ฐ์Šคํฌํ†ฑ ํŒŒ์ผ์„ ์ •๋ฆฌํ•˜๋Š” ํ”„๋กœ๊ทธ๋žจ ๋งŒ๋“ค๊ธฐ (1)

by hyeong._.ing 2026. 6. 16.

 

AI ์„œ๋ฒ„, OCR, PDF ๋ถ„์„ ์—†์ด
๋กœ์ปฌ ๊ทœ์น™ ๊ธฐ๋ฐ˜ MVP๋ฅผ ๊ตฌํ˜„ํ–ˆ๋‹ค.
ํ•ต์‹ฌ ์ฝ”๋“œ, ํŒŒ์ผ ์„ค๋ช… ๋“ฑ
๊ฐ„๋‹จํ•˜๊ฒŒ๋งŒ ๊ธฐ๋ก์šฉ์œผ๋กœ ์ ์–ด๋ณด๊ฒ ๋‹ค.

 

 

 

 

[ GitHub์— ์ฝ”๋“œ ์„ค๋ช…์ด ์ž˜ ์ ํ˜€์žˆ์Šต๋‹ˆ๋‹ค ]

 

- Python ํŒŒ์ผ

 

GitHub - hyeong-ing/DesktopCleanAI at cleanAI

๋ฐ์Šคํฌํ†ฑ ์ •๋ฆฌํ•ด์ฃผ๋Š” AI . Contribute to hyeong-ing/DesktopCleanAI development by creating an account on GitHub.

github.com

 

 

- rules.txt

 

GitHub - hyeong-ing/DesktopCleanAI at deskpilot

๋ฐ์Šคํฌํ†ฑ ์ •๋ฆฌํ•ด์ฃผ๋Š” AI . Contribute to hyeong-ing/DesktopCleanAI development by creating an account on GitHub.

github.com

 

 

 

 

1. ๋ฐฐ๊ฒฝ

  • ํ…Œ์ŠคํŠธ ๋ฐ์Šคํฌํ†ฑ

์ง„์งœ ๋ฐ์Šคํฌํ†ฑ์— ํ•  ์ˆ˜๊ฐ€ ์—†์–ด์„œ ์ด๋ ‡๊ฒŒ ํ…Œ์ŠคํŠธ ๋ฐ์Šคํฌํ†ฑ์„ ๋งŒ๋“ค์—ˆ๋‹ค. 

 

 

  • ํ„ฐ๋ฏธ๋„์—์„œ ์ฝ”๋“œ ์‹คํ–‰

ํ„ฐ๋ฏธ๋„์—์„œ ํ™•์ธํ–ˆ๋‹ค.

 

 

 

 


 

 

 

 

2. ํŒŒ์ผ ์„ค๋ช…

  • ์ฃผ์š” ํŒŒ์ผ
ํด๋”๋ช… ์„ค๋ช…
config.py ํ”„๋กœ์ ํŠธ ์„ค์ •๊ฐ’ ๊ด€๋ฆฌ, ์—ฌ๊ธฐ์„œ test_desktop ์„ค์ •ํ•จ
scanner.py ํŒŒ์ผ๊ณผ ํด๋”๋ฅผ ์Šค์บ”ํ•จ
rules.py rules.txt ์ฝ์Œ
classifier.py ํƒœ๊ทธ์™€ rules.txt ๊ธฐ์ค€ ๋ถ„๋ฅ˜ํ•จ
planner.py ํŒŒ์ผ๋ณ„ ์ •๋ฆฌ ๊ณ„ํš ์ƒ์„ฑํ•จ
path_planner.py ์‹ค์ œ ์ด๋™๋  ๊ฒฝ๋กœ ๊ณ„์‚ฐํ•จ
mover.py ํŒŒ์ผ ์ด๋™์„ ์‹คํ–‰ํ•จ
log_store.py move-log.json ์ €์žฅํ•˜๊ณ  ์ฝ์Œ
undo.py ๋˜๋Œ๋ฆฌ๊ธฐ ๊ธฐ๋Šฅ
redo.py ์•ž์œผ๋กœ๊ฐ€๊ธฐ ๊ธฐ๋Šฅ
selected_mover.py ์„ ํƒํ•œ ํŒŒ์ผ๋งŒ ์ด๋™ํ•จ
doctor.py ํ”„๋กœ์ ํŠธ ์ƒํƒœ ์ ๊ฒ€ํ•จ
deskpilot.py ํ†ตํ•ฉ ์‹คํ–‰ ํŒŒ์ผ

 

 

  • rules.txt
school: ์ •๋ณด์ฒ˜๋ฆฌ๊ธฐ์‚ฌ, ์„œ์ˆ ํ˜•, ๊ธฐ์‚ฌ 
study: ์Šคํ”„๋ง, ๊ฐ•์˜, ๊ณต๋ถ€, ๊ธฐ๋ณธํŽธ 
project: project, api, test, lotto
์ด ํŒŒ์ผ์€ ํ…Œ์ŠคํŠธ ํด๋”์ธ test_desktop์— ๋งŒ๋“ ๋‹ค. ๊ทธ๋ƒฅ ๋‘๋ฉด ์•ˆ๋˜๊ณ  .์„ ๋ถ™์ธ ์ˆจ๊ฒจ์ง„ ํด๋”๋ฅผ ๋งŒ๋“ค์–ด์„œ ๋‘ฌ์•ผํ•œ๋‹ค. ๊ทธ ๋ถ€๋ถ„์€ ์•„๋ž˜ ๋งํฌ๋กœ ๊ฑธ์–ด๋‘๊ฒ ๋‹ค. ์ˆจ๊น€ ํด๋” ์ด๋ฆ„์€ .deskpilot์ด๊ณ  ๊ทธ ์•ˆ์— rules.txt๋ฅผ ๋„ฃ์—ˆ๋‹ค.

rules.txt๋Š” ๋ง๊ทธ๋Œ€๋กœ ๊ทœ์น™์„ ์ ์–ด๋‘”๋‹ค. ์ •๋ณด์ฒ˜๋ฆฌ๊ธฐ์‚ฌ, ์„œ์ˆ ํ˜•, ๊ธฐ์‚ฌ๋ผ๋Š” ๋‹จ์–ด๊ฐ€ ์ œ๋ชฉ์— ๋ถ™์–ด์žˆ์œผ๋ฉด school ํด๋”๋กœ ์ด๋™ํ•œ๋‹ค.




- ์ˆจ๊น€ ํด๋”

 

[macOS] Finder ์ˆจ๊น€ ํŒŒ์ผ ๋ณด๊ธฐ & ์ˆจ๊น€ ์„ค์ • & ์ˆจ๊น€ ํ•ด์ œ

macOS Finder ์ˆจ๊น€ ํŒŒ์ผ ๋ณด๊ธฐ & ์ˆจ๊น€ ์„ค์ • & ์ˆจ๊น€ ํ•ด์ œ1. ํ„ฐ๋ฏธ๋„์„ ์ด์šฉํ•˜์—ฌ ์ˆจ๊ธฐ๊ธฐํ„ฐ๋ฏธ๋„ ์—ด๊ธฐํ„ฐ๋ฏธ๋„ ์•ฑ์„ ์—ฝ๋‹ˆ๋‹ค. ํ„ฐ๋ฏธ๋„์€ ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ > ์œ ํ‹ธ๋ฆฌํ‹ฐ ํด๋”์—์„œ ์ฐพ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.Spotlight(CommandโŒ˜ + S

learn-dev.tistory.com

 

 

 


 

 

 

3. ์ค‘์š”ํ•œ ์ฝ”๋“œ๋งŒ

  • config.py
TEST_DESKTOP_FOLDER_NAME = "test_desktop"
APP_FOLDER_NAME = ".deskpilot"
ARCHIVE_FOLDER_NAME = "archive"

MOVE_CONFIRM_TEXT = "์ •๋ฆฌํ•˜๊ธฐ"
UNDO_CONFIRM_TEXT = "๋˜๋Œ๋ฆฌ๊ธฐ"
REDO_CONFIRM_TEXT = "์•ž์œผ๋กœ๊ฐ€๊ธฐ"

EXCLUDED_FILENAMES = {
    ".DS_Store",
    "desktop.ini",
    "Thumbs.db",
    "rules.txt",
    "move-log.json",
}

EXCLUDED_FOLDER_NAMES = {
    APP_FOLDER_NAME,
    "deskpilot",
    "__pycache__",
}
๋‚˜์ค‘์— ํ…Œ์ŠคํŠธ ํด๋”๋„ ๋ฐ”๊ฟ”์•ผํ•˜ํ•˜๊ณ  ์ œ์™ธ ์‹œํ‚จ ํŒŒ์ผ์ด ์ถ”๊ฐ€๋˜๊ฑฐ๋‚˜ ์ œ๊ฑฐ๋  ์—ฌ๋Ÿฌ ๊ฐ€๋Šฅ์„ฑ์„ ๊ณ ๋ คํ•ด์„œ ํ•œ๊ณณ์— ๊ธฐ๋Šฅ์„ ๊ด€๋ฆฌํ•˜๋„๋ก ๋ชจ์•˜๋‹ค. ์ด ํŒŒ์ผ์—์„œ ํ…Œ์ŠคํŠธ ํด๋” ์ด๋ฆ„, ํ™•์ธ ๋ฌธ๊ตฌ(ํ™•์ธ๋ฌธ๊ตฌ๋ฅผ ์ œ๋Œ€๋กœ ์ ์–ด์•ผ ๊ธฐ๋Šฅ์ด ์‹คํ–‰๋˜๋„๋ก ๋งŒ๋“ค์—ˆ์Œ), ์ œ์™ธ ํŒŒ์ผ ๋ชฉ๋ก์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

 

 

  • scanner.py
def get_desktop_path():
    return Path.home() / "Desktop" / TEST_DESKTOP_FOLDER_NAME
ํ…Œ์ŠคํŠธ์šฉ ๊ฐ€์งœ Desktop ํด๋” ๊ฒฝ๋กœ๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.
def scan_desktop_files():
    desktop_path = get_desktop_path()
    validate_desktop_path(desktop_path)

    files = []

    for item in desktop_path.iterdir():
        if not item.is_file():
            continue

        if is_hidden_path(item):
            continue

        if is_excluded_file(item):
            continue

        files.append(item)

    return sorted(files, key=lambda path: path.name.lower())
test_desktop ๋ฐ”๋กœ ์•„๋ž˜์— ์žˆ๋Š” ํŒŒ์ผ๋งŒ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜์ด๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ์ด๋•Œ ํด๋”, ์ˆจ๊น€ ํŒŒ์ผ, ์‹œ์Šคํ…œ ํŒŒ์ผ, rules.txt, move-log.json์„ ์ œ์™ธํ•œ๋‹ค.
def scan_destination_folders():
    desktop_path = get_desktop_path()
    validate_desktop_path(desktop_path)

    folders = []

    for item in desktop_path.iterdir():
        if not item.is_dir():
            continue

        if is_hidden_path(item):
            continue

        if is_excluded_folder(item):
            continue

        folders.append(item)

    return sorted(folders, key=lambda path: path.name.lower())
test_desktop ๋ฐ”๋กœ ์•„๋ž˜์— ์žˆ๋Š” ํด๋”๋งŒ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜์ด๋‹ค. ์ด ํด๋”๋“ค์€ ๋‚˜์ค‘์— ํŒŒ์ผ์„ ์ด๋™ํ•  ๋ชฉ์ ์ง€ ํ›„๋ณด๊ฐ€ ๋œ๋‹ค. ํŒŒ์ผ, ์ˆจ๊น€ ํด๋”, ์•ฑ ๋‚ด๋ถ€ ์„ค์ • ํด๋”, ํŒŒ์ด์ฌ ์บ์‹œ ํด๋”๋Š” ์ œ์™ธ๋œ๋‹ค.

 

 

  • rules.py
def get_rules_path():
    return get_desktop_path() / APP_FOLDER_NAME / "rules.txt"
rules.txt ํŒŒ์ผ ๊ฒฝ๋กœ๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.
def read_rules():
    rules_path = get_rules_path()

    if not rules_path.exists():
        return {}

    rules = {}

    with open(rules_path, "r", encoding="utf-8") as file:
        for line in file:
            line = line.strip()

            if line == "" or line.startswith("#"):
                continue

            if ":" not in line:
                continue

            folder_name, keywords_text = line.split(":", 1)
            folder_name = folder_name.strip()

            keywords = []

            for keyword in keywords_text.split(","):
                keyword = keyword.strip()

                if keyword != "":
                    keywords.append(keyword)

            if folder_name != "" and keywords:
                rules[folder_name] = keywords

    return rules
rules.txt๋ฅผ ์ฝ์–ด์„œ ํŒŒ์ด์ฌ ๋”•์…”๋„ˆ๋ฆฌ๋กœ ๋ฐ”๊พธ๋Š” ํ•จ์ˆ˜์ด๋‹ค. 
์ด txt๋ฅผ ์ฝ์–ด์„œ

    rules.txt ์˜ˆ:
    school: ์ •๋ณด์ฒ˜๋ฆฌ๊ธฐ์‚ฌ, ์„œ์ˆ ํ˜•, ๊ธฐ์‚ฌ
    study: ์Šคํ”„๋ง, ๊ฐ•์˜, ๊ณต๋ถ€โ€‹

 


์ด๋ ‡๊ฒŒ ๋ณ€ํ™˜ํ•œ๋‹ค.

    {
        "school": ["์ •๋ณด์ฒ˜๋ฆฌ๊ธฐ์‚ฌ", "์„œ์ˆ ํ˜•", "๊ธฐ์‚ฌ"],
        "study": ["์Šคํ”„๋ง", "๊ฐ•์˜", "๊ณต๋ถ€"]
    }โ€‹

 

 

  • classifier.py
def classify_by_tag(file_path, destination_folder_names):
    filename = file_path.name
    tag_name = extract_front_tag(filename)

    if tag_name is None:
        return None

    if tag_name not in destination_folder_names:
        return None

    return {
        "filename": filename,
        "targetFolder": tag_name,
        "reason": "ํŒŒ์ผ๋ช… ์•ž ํƒœ๊ทธ์™€ ๊ฐ™์€ ์ด๋ฆ„์˜ ํด๋”๋ฅผ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค.",
        "source": "FILENAME_TAG",
    }
ํŒŒ์ผ๋ช… ์•ž์˜ [ํƒœ๊ทธ]๋ฅผ ๋ณด๊ณ  ์ด๋™ํ•  ํด๋”๋ฅผ ์ถ”์ฒœํ•˜๋Š” ํ•จ์ˆ˜์ด๋‹ค.
def classify_by_rules(file_path, rules, destination_folder_names):
    filename = file_path.name
    normalized_filename = normalize_text(filename)

    for folder_name, keywords in rules.items():
        if folder_name not in destination_folder_names:
            continue

        for keyword in keywords:
            normalized_keyword = normalize_text(keyword)

            if normalized_keyword in normalized_filename:
                return {
                    "filename": filename,
                    "targetFolder": folder_name,
                    "reason": f"ํŒŒ์ผ๋ช…์— rules.txt ํ‚ค์›Œ๋“œ '{keyword}'๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.",
                    "source": "RULES_TXT",
                }

    return None
rules.txt ๊ทœ์น™์„ ๋ณด๊ณ  ์ด๋™ํ•  ํด๋”๋ฅผ ์ถ”์ฒœํ•˜๋Š” ํ•จ์ˆ˜์ด๋‹ค. ์กฐ๊ฑด์€ ํŒŒ์ผ๋ช… ์•ˆ์— rules.txt์˜ ํ‚ค์›Œ๋“œ๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์–ด์•ผ ํ•˜๊ณ  ๊ทœ์น™์˜ ํด๋” ์ด๋ฆ„์ด ์‹ค์ œ ์ด๋™ ๋ชฉ์ ์ง€ ํด๋”์— ์กด์žฌํ•ด์•ผ ํ•œ๋‹ค.

 

 

  • planner.py
def create_plan_for_file(file_path, destination_folder_names, rules):
    result = classify_by_tag(file_path, destination_folder_names)

    if result is not None:
        result["action"] = "MOVE"
        result["confidence"] = 1.0
        return result

    result = classify_by_rules(file_path, rules, destination_folder_names)

    if result is not None:
        result["action"] = "MOVE"
        result["confidence"] = 0.8
        return result

    if ARCHIVE_FOLDER_NAME in destination_folder_names:
        return {
            "filename": file_path.name,
            "targetFolder": ARCHIVE_FOLDER_NAME,
            "reason": "ํƒœ๊ทธ์™€ rules.txt๋กœ ๋ถ„๋ฅ˜๋˜์ง€ ์•Š์•„ archive ํด๋”๋กœ ์ถ”์ฒœํ•ฉ๋‹ˆ๋‹ค.",
            "source": "FALLBACK_ARCHIVE",
            "action": "MOVE",
            "confidence": 0.3,
        }

    return {
        "filename": file_path.name,
        "targetFolder": None,
        "reason": "๋ถ„๋ฅ˜๋˜์ง€ ์•Š์•˜๊ณ  archive ํด๋”๊ฐ€ ์—†์–ด Desktop์— ๋‚จ๊น๋‹ˆ๋‹ค.",
        "source": "UNCLASSIFIED",
        "action": "KEEP",
        "confidence": 0.0,
    }
๋ชจ๋“  ํŒŒ์ผ์— ๋Œ€ํ•ด ์ตœ์ข… ์ •๋ฆฌ ๊ณ„ํš์„ ๋งŒ๋“ ๋‹ค. ํƒœ๊ทธ → rules.txt → archive → Desktop ์œ ์ง€ ์ˆœ์„œ๋กœ ๋งŒ๋“ ๋‹ค.

 

 

  • path_planner.py
def remove_front_tag(filename):
    cleaned_name = re.sub(r"^\[[^\]]+\]\s*", "", filename)

    if cleaned_name.strip() == "":
        return filename

    return cleaned_name.strip()
ํŒŒ์ผ๋ช… ๋งจ ์•ž์˜ [ํƒœ๊ทธ]๋ฅผ ์ œ๊ฑฐํ•œ๋‹ค. ์˜ˆ์‹œ๋กœ [school]์ •๋ณด์ฒ˜๋ฆฌ๊ธฐ์‚ฌ ์ •๋ฆฌ๋ณธ.pdf๊ฐ€  ์ •๋ณด์ฒ˜๋ฆฌ๊ธฐ์‚ฌ ์ •๋ฆฌ๋ณธ.pdf๋กœ ๋ณ€๊ฒฝ๋œ๋‹ค.
def make_unique_target_path(target_path, reserved_target_paths):
    candidate_path = target_path

    parent = target_path.parent
    stem = target_path.stem
    suffix = target_path.suffix

    number = 1

    while candidate_path.exists() or candidate_path in reserved_target_paths:
        candidate_filename = f"{stem}({number}){suffix}"
        candidate_path = parent / candidate_filename
        number += 1

    return candidate_path
์ด๋™ํ•˜๋ ค๋Š” ํด๋” ์•ˆ์— ๊ฐ™์€ ์ด๋ฆ„์˜ ํŒŒ์ผ์ด ์žˆ์œผ๋ฉด ํŒŒ์ผ๋ช… ๋’ค์— ์ˆซ์ž๋ฅผ ๋ถ™์—ฌ์„œ ์ค‘๋ณต์„ ํ”ผํ•œ๋‹ค. ์˜ˆ์‹œ๋กœ school/์ •๋ฆฌ๋ณธ.pdf ๊ฐ€ ์ด๋ฏธ ์žˆ์œผ๋ฉด school/์ •๋ฆฌ๋ณธ(1).pdf ๋กœ ๋ณ€๊ฒฝํ•œ๋‹ค.

 

 

  • mover.py
def is_inside_base_path(path, base_path):
    try:
        path.resolve().relative_to(base_path.resolve())
        return True
    except ValueError:
        return False
์ฃผ์–ด์ง„ path๊ฐ€ base_path ๋‚ด๋ถ€์— ์žˆ๋Š”์ง€ ํ™•์ธํ•˜๋Š” ํ•จ์ˆ˜์ด๋‹ค. test_desktop ๋ฐ–์˜ ํŒŒ์ผ์„ ์‹ค์ˆ˜๋กœ ์ด๋™ํ•˜์ง€ ์•Š๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•œ๋‹ค.
if dry_run:
    results.append(
        create_result(
            item,
            "DRY_RUN",
            "์‹ค์ œ ์ด๋™ ์—†์ด ์ด๋™ ์˜ˆ์ •๋งŒ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค.",
        )
    )
    continue
๊ธฐ๋ณธ์€ dry-run์ด๊ณ , ์‹ค์ œ ์ด๋™์€ --apply์™€ ํ™•์ธ ๋ฌธ๊ตฌ๋ฅผ ํ†ต๊ณผํ•ด์•ผ ์‹คํ–‰๋˜๋„๋ก ํ–ˆ๋‹ค. ๋‚˜์ค‘์— ์‚ฌ์šฉ์ž๊ฐ€ ์„ ํƒํ•œ ํŒŒ์ผ๋งŒ ์ด๋™ํ•˜๋„๋ก ํ•  ๊ฒƒ์ด๋‹ค.

 

 

  • log_store.py
def save_move_session(results):
    successful_moves = []

    for result in results:
        if result.get("status") != "SUCCESS":
            continue

        successful_moves.append(
            {
                "filename": result.get("filename"),
                "originalPath": result.get("originalPath"),
                "movedPath": result.get("targetPath"),
                "source": result.get("source"),
                "status": result.get("status"),
            }
        )

    if not successful_moves:
        return None

    log_data = load_log()

    session = {
        "sessionId": create_session_id(),
        "executedAt": datetime.now().isoformat(timespec="seconds"),
        "basePath": str(get_desktop_path()),
        "moves": successful_moves,
    }

    log_data["undoStack"].append(session)
    log_data["redoStack"] = []

    save_log(log_data)

    return session
์‹ค์ œ ์ด๋™์— ์„ฑ๊ณตํ•œ ํŒŒ์ผ๋งŒ ๋กœ๊ทธ์— ์ €์žฅํ•ด์„œ undo/redo์˜ ๊ธฐ์ค€์œผ๋กœ ์‚ฌ์šฉํ•œ๋‹ค.

 

 

  • undo.py
def create_undo_plan(session):
    moves = session.get("moves", [])

    undo_plan = []

    for move in reversed(moves):
        undo_plan.append(
            {
                "filename": move.get("filename"),
                "fromPath": move.get("movedPath"),
                "toPath": move.get("originalPath"),
                "source": move.get("source"),
            }
        )

    return undo_plan
์ตœ๊ทผ ์ด๋™ ์„ธ์…˜์„ ๋ฐ”ํƒ•์œผ๋กœ ๋˜๋Œ๋ฆฌ๊ธฐ ๊ณ„ํš์„ ๋งŒ๋“ ๋‹ค. movedPath -> originalPath ๋ฐฉํ–ฅ์œผ๋กœ ๋˜๋Œ๋ฆฐ๋‹ค.
def update_log_after_undo(log_data):
    session = log_data["undoStack"].pop()
    log_data["redoStack"].append(session)

    save_log(log_data)

    return session
๋˜๋Œ๋ฆฌ๊ธฐ ์„ฑ๊ณต ํ›„ ๋กœ๊ทธ๋ฅผ ์—…๋ฐ์ดํŠธํ•œ๋‹ค. undoStack์˜ ๊ฐ€์žฅ ์ตœ๊ทผ ์„ธ์…˜์„ ๊บผ๋‚ด redoStack์œผ๋กœ ์˜ฎ๊ธด๋‹ค.

 

 

  • redo.py
def create_redo_plan(session):
    moves = session.get("moves", [])

    redo_plan = []

    for move in moves:
        redo_plan.append(
            {
                "filename": move.get("filename"),
                "fromPath": move.get("originalPath"),
                "toPath": move.get("movedPath"),
                "source": move.get("source"),
            }
        )

    return redo_plan
redoStack์˜ ์„ธ์…˜์„ ๋ฐ”ํƒ•์œผ๋กœ ์•ž์œผ๋กœ๊ฐ€๊ธฐ ๊ณ„ํš์„ ๋งŒ๋“ ๋‹ค. originalPath -> movedPath ๋ฐฉํ–ฅ์œผ๋กœ ๋‹ค์‹œ ์ด๋™ํ•œ๋‹ค.
def update_log_after_redo(log_data):
    session = log_data["redoStack"].pop()
    log_data["undoStack"].append(session)

    save_log(log_data)

    return session
์•ž์œผ๋กœ๊ฐ€๊ธฐ ์„ฑ๊ณต ํ›„ ๋กœ๊ทธ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ํ•จ์ˆ˜์ด๋‹ค. redoStack์˜ ๊ฐ€์žฅ ์ตœ๊ทผ ์„ธ์…˜์„ ๊บผ๋‚ด undoStack์œผ๋กœ ์˜ฎ๊ธด๋‹ค.

 

 

  • selected_mover.py
def parse_selection(selection_text, max_number):
    selection_text = selection_text.strip().lower()

    if selection_text == "all":
        return set(range(1, max_number + 1))

    if selection_text == "none" or selection_text == "":
        return set()

    selected_numbers = set()

    parts = selection_text.split(",")

    for part in parts:
        part = part.strip()

        if "-" in part:
            start_text, end_text = part.split("-", 1)

            start = int(start_text.strip())
            end = int(end_text.strip())

            for number in range(start, end + 1):
                selected_numbers.add(number)

        else:
            number = int(part)
            selected_numbers.add(number)

    return selected_numbers
์ „์ฒด ์ด๋™์ด ์•„๋‹ˆ๋ผ ์‚ฌ์šฉ์ž๊ฐ€ ์„ ํƒํ•œ ํŒŒ์ผ๋งŒ ์ด๋™ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค.

 

 

  • deskpilot.py
if args.command == "preview":
    run_preview()

elif args.command == "move":
    run_move(apply=args.apply)

elif args.command == "undo":
    run_undo(apply=args.apply)

elif args.command == "redo":
    run_redo(apply=args.apply)

elif args.command == "log":
    run_log()

elif args.command == "doctor":
    checks = run_checks()
    print_checks(checks)
๋ชจ๋“  ๊ธฐ๋Šฅ์„ ํ•˜๋‚˜์˜ ๋ช…๋ น์–ด๋กœ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฌถ์—ˆ๋‹ค. preview, move, undo, redo, log, doctor ๊ธฐ๋Šฅ์„ deskpilot.py ํ•˜๋‚˜๋กœ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค.

 

 

  • doctor.py
def check_move_log_json():
    log_path = get_log_path()

    if not log_path.exists():
        return create_check_result(
            "WARN",
            "move-log.json ํŒŒ์ผ์ด ์•„์ง ์—†์Šต๋‹ˆ๋‹ค. ์‹ค์ œ ์ด๋™์„ ํ•˜๋ฉด ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.",
        )

    try:
        with open(log_path, "r", encoding="utf-8") as file:
            log_data = json.load(file)

    except json.JSONDecodeError:
        return create_check_result(
            "ERROR",
            "move-log.json ํŒŒ์ผ์˜ JSON ํ˜•์‹์ด ๊นจ์ ธ ์žˆ์Šต๋‹ˆ๋‹ค.",
        )

    if "undoStack" not in log_data:
        return create_check_result(
            "ERROR",
            "move-log.json์— undoStack์ด ์—†์Šต๋‹ˆ๋‹ค.",
        )

    if "redoStack" not in log_data:
        return create_check_result(
            "ERROR",
            "move-log.json์— redoStack์ด ์—†์Šต๋‹ˆ๋‹ค.",
        )

    return create_check_result(
        "OK",
        "move-log.json ํŒŒ์ผ์ด ์ •์ƒ์ž…๋‹ˆ๋‹ค.",
    )
ํ”„๋กœ์ ํŠธ ์ƒํƒœ๋ฅผ ๊ฒ€์‚ฌํ•œ๋‹ค. ์‹คํ–‰ ์ „ ํด๋”, rules.txt, archive, move-log.json ์ƒํƒœ๋ฅผ ์ ๊ฒ€ํ•ด์„œ ๋ฌธ์ œ๋ฅผ ๋ฏธ๋ฆฌ ์ฐพ๋Š”๋‹ค.

 

 

 


 

 

 

4. ์‹คํ–‰ํ™”๋ฉด

  • doctor ์‹คํ–‰

 

 

  • ์„ ํƒํ•ด์„œ ํŒŒ์ผ ์˜ฎ๊ธฐ๊ธฐ

 

 

  • ๋˜๋Œ๋ฆฌ๊ธฐ

 

 

  • ์•ž์œผ๋กœ ๋˜๋Œ๋ฆฌ๊ธฐ