"""S-CANVAS 런타임 경로 해석. PyInstaller 로 .exe 패키징 시: - **읽기 전용 자산**(Design/, prompt_templates/, structure_types/) 은 `sys._MEIPASS` (onefile) 또는 .exe 옆 폴더(onedir) 에서 읽음. - **쓰기 데이터**(scanvas_jobs.db, *.log, cache/) 는 사용자별 영구 폴더 `%LOCALAPPDATA%\\S-CANVAS\\` 에 저장. 설치 폴더 쓰기 권한 불필요(Program Files 설치 OK). 개발 모드(소스 직접 실행) 에서는: - 자산: 소스 트리 옆 (`Path(__file__).parent / "Design"` 등) - 쓰기 데이터: 동일하게 `%LOCALAPPDATA%\\S-CANVAS\\` 사용. 개발 중에도 설치 배포본과 같은 경로에 쓰므로 행동이 일관됨. 단, 개발 편의를 위해 환경변수 `SCANVAS_DEV_LOCAL=1` 설정 시 소스 트리 옆에 쓴다(과거 호환). 환경변수 오버라이드: - `SCANVAS_USER_DATA` — 쓰기 데이터 루트를 임의 경로로 강제(테스트/CI 용) - `SCANVAS_DEV_LOCAL=1` — 쓰기 데이터를 소스 트리 옆에 (개발 모드) """ from __future__ import annotations import os import sys from pathlib import Path # ──────────────────────────── 자산(읽기) 경로 ──────────────────────────── def _bundled() -> bool: """PyInstaller 번들 안인가?""" return getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS") def asset_root() -> Path: """읽기 전용 자산(Design/, prompt_templates/ 등) 의 부모 디렉토리. - PyInstaller onefile: `sys._MEIPASS` (임시 추출 폴더) - PyInstaller onedir: `sys._MEIPASS` 또는 .exe 옆 - dev: `Path(__file__).resolve().parent` """ if _bundled(): return Path(sys._MEIPASS) # type: ignore[attr-defined] return Path(__file__).resolve().parent def resource_path(*parts: str) -> Path: """asset_root 아래 상대경로 해석. e.g. resource_path("Design", "Logo.png").""" return asset_root().joinpath(*parts) # ──────────────────────────── 쓰기 데이터 경로 ──────────────────────────── def _windows_localappdata() -> Path: base = os.environ.get("LOCALAPPDATA") if base: return Path(base) # XDG-ish 폴백 home = Path.home() return home / "AppData" / "Local" def user_data_dir() -> Path: """사용자별 영구 쓰기 폴더. 없으면 생성. 우선순위: 1. `SCANVAS_USER_DATA` 환경변수 2. `SCANVAS_DEV_LOCAL=1` 인 경우 → 소스 트리 옆 (`./` 아래) 3. Windows: `%LOCALAPPDATA%\\S-CANVAS\\` 4. 그 외 OS: `~/.scanvas/` """ override = os.environ.get("SCANVAS_USER_DATA") if override: p = Path(override) p.mkdir(parents=True, exist_ok=True) return p if os.environ.get("SCANVAS_DEV_LOCAL") == "1" and not _bundled(): p = Path(__file__).resolve().parent # 소스 트리에는 cache/ 가 이미 있으니 그대로 사용 return p if sys.platform == "win32": p = _windows_localappdata() / "S-CANVAS" elif sys.platform == "darwin": p = Path.home() / "Library" / "Application Support" / "S-CANVAS" else: p = Path.home() / ".scanvas" p.mkdir(parents=True, exist_ok=True) return p def db_path() -> Path: """`scanvas_jobs.db` 절대경로 (생성 위치).""" return user_data_dir() / "scanvas_jobs.db" def log_path(name: str) -> Path: """`scanvas_*.log` 절대경로. e.g. log_path('harness') → .../scanvas_harness.log.""" return user_data_dir() / f"scanvas_{name}.log" def cache_dir(*sub: str) -> Path: """`cache/` 하위 폴더. 없으면 생성. e.g. cache_dir('dem'), cache_dir('icons').""" p = user_data_dir() / "cache" if sub: p = p.joinpath(*sub) p.mkdir(parents=True, exist_ok=True) return p def diagnostic_log_path() -> Path: return log_path("diagnostic") def harness_log_path() -> Path: return log_path("harness") # ──────────────────────────── 디버그 헬퍼 ──────────────────────────── def describe() -> str: """현재 경로 설정을 한 문자열로 요약 (런타임 진단용).""" return ( f"asset_root={asset_root()} (bundled={_bundled()})\n" f"user_data_dir={user_data_dir()}\n" f"db={db_path()}\n" f"harness_log={harness_log_path()}\n" f"diagnostic_log={diagnostic_log_path()}\n" f"cache_root={cache_dir()}" ) if __name__ == "__main__": print(describe())