# 프로젝트 경로 유틸리티 — 개발/EXE 환경 모두에서 상대↔절대 경로 일관 처리 # # 두 종류의 루트가 있음: # PROJECT_ROOT : 사용자가 보는 실제 폴더. 로그/설정/모델 등 쓰기 가능한 위치. # - dev 모드: 이 파일이 있는 폴더 (E:\ANT) # - EXE(--onefile) 모드: EXE가 놓인 폴더 (예: E:\ANT\dist) # BUNDLE_ROOT : 번들에 포함된 읽기 전용 자원 위치. # - dev 모드: PROJECT_ROOT 와 동일 # - EXE 모드: PyInstaller 임시 추출 폴더 (sys._MEIPASS) import os import sys _FROZEN = getattr(sys, "frozen", False) # 번들된 자원(읽기 전용): config.json, ai/models, assets 등 BUNDLE_ROOT = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__))) # 사용자 데이터(읽기/쓰기): logs, 변경된 config, 새로 저장된 모델 등 if _FROZEN: PROJECT_ROOT = os.path.dirname(os.path.abspath(sys.executable)) else: PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) def resolve_path(path: str) -> str: """상대경로 → 절대경로 변환. 1) 절대경로/빈 값은 그대로 반환 2) PROJECT_ROOT 기준 경로가 존재하면 사용 (사용자가 변경한 사본 우선) 3) BUNDLE_ROOT 기준 경로가 존재하면 폴백 (번들 기본 사본) 4) 둘 다 없으면 PROJECT_ROOT 기준 경로 반환 (신규 생성 시 사용자 영역에 만들도록) """ if not path: return path if os.path.isabs(path): return os.path.normpath(path) primary = os.path.normpath(os.path.join(PROJECT_ROOT, path)) if os.path.exists(primary): return primary if BUNDLE_ROOT and BUNDLE_ROOT != PROJECT_ROOT: fallback = os.path.normpath(os.path.join(BUNDLE_ROOT, path)) if os.path.exists(fallback): return fallback return primary def to_project_relative(path: str) -> str: """PROJECT_ROOT 또는 BUNDLE_ROOT 하위면 슬래시 구분 상대경로로, 외부면 슬래시 구분 절대경로로.""" if not path: return path abs_path = os.path.abspath(path) for root in (PROJECT_ROOT, BUNDLE_ROOT): if not root: continue try: rel = os.path.relpath(abs_path, root) except ValueError: continue if not rel.startswith(".."): return rel.replace("\\", "/") return abs_path.replace("\\", "/")