Import S-CANVAS source + iter=1~7 lint cleanup

S-CANVAS (Saman Corp.) — DXF + DEM + AI 기반 3D 조감도 생성 엔진.
~24k LOC Python (scanvas_maker.py 7072 LOC GUI + 구조물 파서/빌더 다수).

이 커밋은 7-iter cleanup이 적용된 상태로 import:
- F821 8 + B023 6: 비동기 lambda + except/loop 변수 캡처 NameError
  (Py3.13에서 reproduce 확인된 진짜 버그)
- RUF012 4 + RUF013 1: ClassVar / implicit Optional 명시화
- F811/B905/B904/F401/F841/W293/F541/UP/SIM/RUF/PLR 700+ cleanup/modernization

신규 파일:
- ruff.toml: target=py313, Korean unicode/저자 스타일/도메인 복잡도 무력화
- requirements-py313.txt: pyproj>=3.7, scipy>=1.14, numpy>=2.0.2 (Py3.13 wheel)
- .gitignore: gcp-key.json, 캐시, 백업, 생성 이미지 제외

검증: ruff 0 errors, py_compile 0 errors, import 33/33 OK on Py3.13.13.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-08 10:29:08 +09:00
parent 53d8b53c2f
commit b9342f6726
92 changed files with 3413501 additions and 0 deletions

134
resource_paths.py Normal file
View File

@@ -0,0 +1,134 @@
"""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())