Files
s-canvas/ARCHITECTURE_PLAN.md
HYUNJUNGLEE e9cc6bfcf4 Phase 0 of expert feedback (#1~#11): infrastructure + design + 1차 fixes
Implementations (즉시 동작):
- #1 crash logging: harness/crash_logger.py (sys.excepthook + threading +
  faulthandler, 회전 파일 logs/scanvas.log). main 진입점 통합.
- #2 smooth curves (1차): gate_3d_builder ogee profile를 arc-length parametric
  CubicSpline로 4× densify (8pt→32pt, 36→132 cells, 60 FPS 안전).
- #3 TIN colormap: matplotlib "terrain"의 파란색 범위 제거 → 짙은갈색→황토→
  모래→능선 LinearSegmentedColormap. 9 사이트 교체. 회귀 테스트 추가.
- #5 uv: pyproject.toml + UV_GUIDE.md. base/[py313]/[dev]/[build] extras + hatchling.
- #6,#7,#8 dev cycle infra: .pre-commit-config.yaml (ruff+secrets+위생),
  .gitea/workflows/ci.yml (Py3.11+3.13 matrix), tests/test_regressions.py
  (18 회귀 테스트, iter=1~7 fix 박제), CONTRIBUTING.md (Red→Green 알고리즘).

Design docs (다음 세션 마이그레이션 청사진):
- #4 UI/UX 전면 수정: UI_REDESIGN_PLAN.md (12 popup→1 inspector, vtkTkRenderWidget
  embedding 게이트, 4 phase × 7 sessions).
- #10 Core/Plugin: ARCHITECTURE_PLAN.md (Core 14 / Plugin 7 구조물 + 2 렌더 + 1 QA,
  STRUCTURE_REGISTRY 확장, manifest 기반 디스커버리).
- #11 perf hotspots: PERFORMANCE_BASELINE.md (19 핫스팟, P1: 타일 직렬DL 5~30s,
  캡처 직렬 4.5~15s, numpy 벡터화 가능 Python loops, 텍스처 4회 반복read).

Behavior preservation: ruff 0 errors, pytest 17 passed/1 skipped(bpy),
import 33/33 OK on Py3.13.13.

Item #2 P2/P3 곡선, #4 UI 마이그레이션, #10 Phase 1 추출, #11 P1 최적화는 차기 세션.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 11:45:30 +09:00

631 lines
29 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# S-CANVAS Core/Plugin 분리 설계 (Phase 0 — 분석)
> **상태**: 분석/설계 단계 (Phase 0). 코드 변경 없음.
> **작성**: library-architect subagent (read-only survey).
> **기준 일자**: 2026-05-08
> **이전 단계**: 사용자 피드백 #10 — "코드 구조를 Core(핵심), plug-in(추가)으로 구분 필요".
---
## 0. 설계 원칙
이 계획은 다음 4가지를 동시에 만족해야 한다.
1. **현재 동작 깨지 않음**`python scanvas_maker.py` 가 모든 단계에서 그대로 동작.
2. **점진적 마이그레이션** — 한 번에 모든 파일을 옮기지 않음. 2~3 세션에 걸쳐.
3. **`STRUCTURE_REGISTRY` 확장(replace 아님)** — 이미 잘 짜인 싱글톤 레지스트리를
plugin entry-point 의 첫 번째 사용처로 만든다.
4. **library-architect.md 의 Phase 2 비전과 정렬**`manifest.json` 기반 동적
디스커버리, `.scanvas-lib` ZIP 배포가 최종 목표.
> **비목표**: PyPI 엔트리포인트, 온라인 마켓, Phase 3 의 "꿈" 단계는 본 문서 범위가
> 아니다. 로컬 폴더/ZIP 디스커버리만 다룬다.
---
## 1. 현재 구조 진단
### 1.1 모듈 그래프 (의존 방향)
```
┌──────────────────────────────┐
│ scanvas_maker.py (~7000 LOC)│ ← GUI 진입점, 거의 모든 것 import
│ CustomTkinter SCanvasApp │
└─────────────┬────────────────┘
┌─────────────┬───────┼───────┬─────────────┬─────────────┐
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
resource_paths splash dem_extender geo_referencing tile_downloader dxf_geometry
│ (공유)
┌────────────────────────────────────────────────────────────────────────┘
structure_templates.py ── REGISTRY (싱글톤)
│ ├── SpillwayGateTemplate ──→ gate_parser, gate_3d_builder
│ ├── IntakeTowerTemplate ──→ intake_tower_parser, intake_tower_3d_builder
│ ├── ValveChamberTemplate ──→ valve_chamber_parser, valve_chamber_3d_builder
│ ├── DetailedRetainingWallTemplate ──→ retaining_wall_parser, retaining_wall_3d_builder
│ ├── BuildingTemplate / BridgeTemplate / TunnelPortalTemplate (placeholder)
│ └── GenericStructureTemplate
structure_placement / structure_vlm_feedback / filename_classifier
view_detector / view_reconstructor / polygon_reconstructor / detail_parser / optional_detector
렌더링 백엔드 (선택 가능):
├── gemini_renderer.py (Gemini Vertex/API)
└── blender_renderer.py (Blender 헤드리스 + bpy 빌더)
└── gate_3d_builder_bpy.py + apply_blender_patch.py + fix_bpy_import.py
+ validate_gate_params.py + params_to_json.py
품질·이력:
harness/
├── seed_manager.py
├── quality_validator.py
├── prompt_registry.py
└── logger.py (SQLite ORM JobRecord)
런타임 자산 (읽기):
Design/, prompt_templates/, structure_types/
사용자 데이터 (쓰기, %LOCALAPPDATA%\S-CANVAS\):
cache/dem/, scanvas_jobs.db, scanvas_*.log
```
### 1.2 Core 후보 vs Plugin 후보
판정 기준: "이걸 빼면 GUI 가 떠도 1·1.5·2 단계 파이프라인이 되는가?"
| 모듈 | 분류 | 사유 |
|---|---|---|
| `scanvas_maker.py` | **Core** | GUI 엔트리·이벤트 루프·상태 보유 |
| `splash.py` | **Core** | 부팅 1회 호출, 의존성도 cv2/PIL 만 |
| `resource_paths.py` | **Core** | 모든 IO 경로의 진실. 대체 불가 |
| `dxf_geometry.py` | **Core** | 모든 파서/빌더의 공통 전처리 |
| `geo_referencing.py` | **Core** | 4점 매칭은 파이프라인 필수 |
| `dem_extender.py` | **Core** | 1.5 단계의 핵심. AWS 의존이지만 옵션 분기 있음 |
| `tile_downloader.py` | **Core** | 2 단계 (위성 결합) 필수 |
| `polygon_reconstructor.py` | **Core (util)** | 여러 파서가 공유 |
| `optional_detector.py` | **Core (util)** | 부속 컴포넌트 검출, 파서 공통 |
| `filename_classifier.py` | **Core (util)** | 키워드 → template_id 매핑, GUI 가 직접 호출 |
| `detail_parser.py` | **Core (util)** | 모든 구조물 파서가 공유하는 치수 추출 |
| `view_detector.py` / `view_reconstructor.py` | **Core (util)** | template 의 `try_view_based_*` 가 호출 |
| `structure_placement.py` | **Core (util)** | 파이프라인의 "위치인식 → 굴착 → 배치" 4단계 중 핵심 |
| `harness/*` | **Core** (선택적) | 이미 try/except 로 import 보호되어 있음. 항상 로드는 하지만 없어도 동작 |
| `structure_templates.py` | **Core (host)** + Plugin | `StructureTemplate` ABC 와 `TemplateRegistry` 는 Core, 7개 구체 클래스는 Plugin |
| `gate_*`, `intake_tower_*`, `valve_chamber_*`, `retaining_wall_*` (parser+builder 쌍) | **Plugin (structure)** | 각각 1종의 구조물. 빠져도 다른 구조물은 작동 |
| `gate_3d_builder_bpy.py` | **Plugin (structure-renderer variant)** | Blender 백엔드용 같은 구조물의 변종 |
| `validate_gate_params.py` | **Plugin (helper)** | gate-bpy 와 같이 묶여야 의미가 있음 |
| `apply_blender_patch.py`, `fix_bpy_import.py` | **Plugin (tooling)** | 일회성 패치 도구. 마이그레이션 대상 아님 |
| `params_to_json.py` | **Core (util)** | 모든 파서 결과를 JSON 직렬화. plugin 도 사용 |
| `gemini_renderer.py` | **Plugin (render)** | AI 렌더 백엔드의 한 종류. 다른 백엔드로 교체 가능 |
| `blender_renderer.py` | **Plugin (render)** | 같은 위치, 다른 엔진 |
| `structure_vlm_feedback.py` | **Plugin (qa)** | Gemini Vision 기반 QA. 빠져도 빌드 자체는 됨 |
| `structure_types/*.yaml`, `prompt_templates/*.yaml` | **데이터** | 자체로는 코드 아님. 각 plugin 이 자기 yaml 동봉 가능 |
### 1.3 발견된 사실
- `STRUCTURE_REGISTRY`**이미** 잘 설계된 plugin host 다 (Singleton + ABC + dict).
단지 `_register_defaults()` 가 import-bound 하드코딩이라는 점만 동적으로
바꾸면 된다.
- `harness/`**이미 plugin-스타일** (`try: from harness... except ImportError`).
이 패턴을 다른 옵션 모듈(`structure_vlm_feedback`, 렌더러들)이 모방한다.
- `scanvas_maker.py` 의 import 블록(line 31-80) 자체가 **암묵적 디스커버리**다:
각 모듈을 try-import 해서 가용성 플래그(`HARNESS_AVAILABLE`,
`STRUCTURE_TEMPLATES_AVAILABLE`, `DEM_EXTENDER_AVAILABLE`,
`STRUCTURE_VLM_AVAILABLE` 등)를 세운다. → 명시적 plugin manager 로 통합 가능.
- 구조물 plugin 은 `parser + builder` 2-파일 쌍이 표준. 일관성 양호.
- Blender 트랙은 `*_bpy.py` 접미사로 구분되어 있지만, 같은 디렉토리에 섞여 있다.
---
## 2. 제안 레이아웃
### 2.1 디렉토리 트리 (목표)
```
D:\2026\PROGRAM\1_S-CANVAS\
├── scanvas_maker.py # 메인 진입점 (그대로)
├── splash.py # (그대로)
├── resource_paths.py # (그대로)
├── core/ # ★ 신규 패키지
│ ├── __init__.py
│ ├── plugin_manager.py # ★ 플러그인 로더 (신규)
│ ├── manifest.py # ★ Manifest dataclass + 검증 (신규)
│ ├── interfaces.py # ★ Plugin abstract bases (신규)
│ │
│ ├── geo/ # 좌표·DXF 공통
│ │ ├── __init__.py
│ │ ├── dxf_geometry.py ← 이동
│ │ ├── geo_referencing.py ← 이동
│ │ ├── tile_downloader.py ← 이동
│ │ ├── dem_extender.py ← 이동
│ │ └── polygon_reconstructor.py ← 이동
│ │
│ ├── parsing/ # 공통 파서·검출
│ │ ├── __init__.py
│ │ ├── detail_parser.py ← 이동
│ │ ├── view_detector.py ← 이동
│ │ ├── view_reconstructor.py ← 이동
│ │ ├── optional_detector.py ← 이동
│ │ └── filename_classifier.py ← 이동
│ │
│ ├── structures/ # 구조물 호스트 (ABC + Registry)
│ │ ├── __init__.py
│ │ ├── base.py ← structure_templates.py 의 추상부
│ │ ├── registry.py ← TemplateRegistry + REGISTRY
│ │ ├── placement.py ← structure_placement.py
│ │ └── params_io.py ← params_to_json.py
│ │
│ └── harness/ ← 그대로 (이미 패키지)
│ ├── __init__.py
│ ├── seed_manager.py
│ ├── quality_validator.py
│ ├── prompt_registry.py
│ └── logger.py
├── plugins/ # ★ 신규 패키지 (디스커버리 루트)
│ ├── __init__.py
│ │
│ ├── structures/ # ── 구조물 라이브러리 ──
│ │ ├── spillway_gate/
│ │ │ ├── manifest.json # ★ 신규
│ │ │ ├── plugin.py # SpillwayGateTemplate (구 structure_templates.py 의 클래스)
│ │ │ ├── parser.py ← gate_parser.py
│ │ │ ├── builder_pyvista.py ← gate_3d_builder.py
│ │ │ ├── builder_blender.py ← gate_3d_builder_bpy.py
│ │ │ ├── validate.py ← validate_gate_params.py
│ │ │ ├── parameters.json # ★ 신규 (UI 폼 자동 생성용)
│ │ │ └── samples/ # ★ 신규 (1~3개 sample params)
│ │ │
│ │ ├── intake_tower/
│ │ │ ├── manifest.json
│ │ │ ├── plugin.py
│ │ │ ├── parser.py ← intake_tower_parser.py
│ │ │ └── builder_pyvista.py ← intake_tower_3d_builder.py
│ │ │
│ │ ├── valve_chamber/ … (동일 구조)
│ │ ├── retaining_wall/ … (동일 구조)
│ │ │
│ │ ├── building/ # 현재 placeholder. 같은 레이아웃에 진입
│ │ ├── bridge/
│ │ └── tunnel_portal/
│ │
│ ├── render/ # ── 렌더 백엔드 ──
│ │ ├── gemini/
│ │ │ ├── manifest.json
│ │ │ └── plugin.py ← gemini_renderer.py
│ │ └── blender/
│ │ ├── manifest.json
│ │ ├── plugin.py ← blender_renderer.py
│ │ └── tooling/ ← apply_blender_patch.py, fix_bpy_import.py
│ │
│ └── qa/ # ── 품질/피드백 ──
│ └── vlm_feedback/
│ ├── manifest.json
│ └── plugin.py ← structure_vlm_feedback.py
├── structure_types/ # (그대로) — yaml은 plugin 외부 공유 데이터
│ └── structure_v1.yaml
├── prompt_templates/ # (그대로)
│ └── prompt_v1.yaml
├── Design/ # (그대로)
├── user_plugins/ # ★ 신규 — 사용자 설치 라이브러리 (외부 ZIP 풀어 놓는 곳)
│ └── (ex: SpillwayGate_v2.scanvas-lib 풀린 폴더)
├── _unused/ # (그대로) — 격리된 옛 코드
└── venv313/ # (그대로)
```
### 2.2 핵심 결정
- **Core 는 단일 파이썬 패키지**(`core/`) 로 통일. 모든 공유 유틸·ABC·레지스트리는
여기 들어간다.
- **Plugins 는 디렉토리 한 개 + manifest.json 한 개** = 한 plugin. 파일 수는
그 안에서 자유롭게.
- **두 디스커버리 루트**:
1. `plugins/` (소스 트리, 번들 plugin) — 항상 로드
2. `user_plugins/` (사용자 데이터 폴더) — 옵트인. ZIP 압축 해제 위치
- **렌더링 백엔드도 plugin** — Gemini/Blender 가 동급. 미래에 ComfyUI·Stability 등을
추가할 수 있게.
---
## 3. 플러그인 인터페이스
### 3.1 Abstract Bases (`core/interfaces.py`)
```python
# core/interfaces.py
from abc import ABC, abstractmethod
from pathlib import Path
import pyvista as pv
# (1) 구조물 plugin — 기존 StructureTemplate 의 외부 노출 이름
class StructurePlugin(ABC):
plugin_id: str # "spillway_gate" — manifest.id 와 일치
template_id: str # registry 키 (legacy 호환을 위해 별도 유지)
name_ko: str
description: str
icon_hint: str = ""
required_files: tuple[int, int, int] = (1, 2, 1)
supports_view_based: bool = True
@abstractmethod
def get_parameter_schema(self) -> "list[ParamField]": ...
@abstractmethod
def parse(self, dxf_paths: list[str]) -> "StructureParams": ...
@abstractmethod
def build_meshes(self, params) -> list[tuple[pv.PolyData, str, float]]: ...
def validate_params(self, params) -> tuple[bool, str]:
return True, "" # 기본 통과
# (2) 렌더 백엔드 plugin
class RenderPlugin(ABC):
plugin_id: str # "gemini", "blender"
name_ko: str
requires_credentials: bool
@abstractmethod
def is_available(self) -> tuple[bool, str]:
"""라이브러리/외부 의존이 갖춰졌는지 검사."""
@abstractmethod
def render(self, app, *, control_map: Path, prompt: str, **kwargs) -> Path:
"""제어맵 + 프롬프트 → PNG 경로."""
# (3) QA plugin
class QAPlugin(ABC):
plugin_id: str
@abstractmethod
def evaluate(self, *, build_meshes, original_dxf, current_params) -> dict:
"""JSON-able 결과 (missing/incorrect/excess 등)."""
# (4) 데이터 클래스 (기존 그대로 노출)
# StructureParams, ParamField — core/structures/base.py 에서 재export
```
> **핵심**: `StructurePlugin` 은 **현재의 `StructureTemplate` 와 100% 호환** 되도록
> 같은 메서드 시그니처를 유지. 즉 기존 클래스에 부모 하나만 바꾸는 것으로 충분.
### 3.2 Manifest 스키마 (`core/manifest.py`)
library-architect.md 의 비전을 그대로 차용하되, **현재 단계에 필요한 필드만**:
```json
{
"schema_version": "1.0",
"id": "spillway_gate",
"name": "여수로 래디얼 수문",
"name_en": "Spillway Radial Gate",
"kind": "structure",
"category": "hydraulic_structure",
"subcategory": "gate",
"version": "1.2.0",
"author": "Saman Corp.",
"license": "Proprietary",
"description": "ogee 여수로 + 래디얼 수문 + 공도교 + 개폐장치",
"min_scanvas_version": "0.5.0",
"entry": {
"type": "python_module",
"module": "plugin",
"class": "SpillwayGatePlugin",
"registry_id": "spillway_gate"
},
"parameters_schema": "parameters.json",
"samples": ["samples/sample_default.json"],
"metadata": {
"complexity": "high",
"polygon_estimate": 35000,
"supports_pyvista": true,
"supports_blender": true,
"requires_gpu": false
}
}
```
`kind` 값:
- `structure``StructurePlugin` 로딩, `STRUCTURE_REGISTRY` 에 등록
- `render``RenderPlugin` 로딩, `RENDER_REGISTRY` 에 등록
- `qa``QAPlugin` 로딩
`Manifest` 데이터클래스는 `core/manifest.py` 에서 정의하고, JSON 스키마 검증을
거친 뒤 plugin manager 에 넘긴다.
---
## 4. 디스커버리 메커니즘
### 4.1 단계별 디스커버리 정책
`core/plugin_manager.py``discover()` 알고리즘:
```python
# 의사코드
def discover(roots: list[Path]) -> dict[str, LoadedPlugin]:
found = {}
for root in roots:
for plugin_dir in root.iterdir():
if not plugin_dir.is_dir():
continue
mf = plugin_dir / "manifest.json"
if not mf.exists():
continue
try:
manifest = Manifest.load_validated(mf)
except ValidationError as e:
log.warning(f"[plugin] {plugin_dir.name}: invalid manifest: {e}")
continue
if not _version_compatible(manifest.min_scanvas_version):
log.warning(f"[plugin] {plugin_dir.name}: needs >= {manifest.min_scanvas_version}")
continue
try:
cls = _import_entry(plugin_dir, manifest.entry)
instance = cls()
except Exception as e:
log.warning(f"[plugin] {plugin_dir.name}: failed to load: {e}")
continue
found[manifest.id] = LoadedPlugin(manifest, instance, plugin_dir)
return found
```
**디스커버리 루트 우선순위**:
1. `<asset_root>/plugins/` — 번들 plugin (소스 트리 또는 PyInstaller `_MEIPASS`)
2. `<user_data_dir>/user_plugins/` — 사용자 설치 plugin
3. `(미래)` ZIP 경로 직접 — `*.scanvas-lib` 파일 압축 해제 후 (2) 로 이동
같은 `manifest.id` 가 둘 이상 발견되면 **버전 비교 후 최신 우선**, 동률이면
**user_plugins 우선** (사용자 override).
### 4.2 진입점 로딩
`plugin_dir / manifest.entry.module + ".py"` 에서 `manifest.entry.class` 를 import.
파이썬 import path 충돌 회피를 위해 `importlib.util.spec_from_file_location` 으로
**고유 모듈 이름**(`scanvas_plugin_<id>_<sha8>`) 을 부여한다.
### 4.3 등록
- `kind == "structure"``core/structures/registry.py``REGISTRY._templates[mf.id] = instance`
(기존 `_register_defaults()` 가 하던 일)
- `kind == "render"``core/render/registry.py` (신규) 의 `RENDER_REGISTRY[mf.id] = instance`
- `kind == "qa"``QA_REGISTRY[mf.id] = instance`
`scanvas_maker.py` 의 부팅 코드 1줄만 추가:
```python
from core.plugin_manager import bootstrap_plugins
bootstrap_plugins() # discover → register, 모든 REGISTRY 가 이 시점 이후 사용 가능
```
---
## 5. 마이그레이션 단계
### Phase 1 — Core 추출 (1 세션, 2~3시간)
**범위**: 디렉토리 이동 + import 경로 갱신만.
**작업**:
1. `core/` 패키지 생성 + 빈 `__init__.py`
2. 다음 파일들을 `core/<subdir>/` 로 git mv (또는 단순 이동):
- `dxf_geometry.py`, `geo_referencing.py`, `tile_downloader.py`,
`dem_extender.py`, `polygon_reconstructor.py``core/geo/`
- `detail_parser.py`, `view_detector.py`, `view_reconstructor.py`,
`optional_detector.py`, `filename_classifier.py``core/parsing/`
- `structure_placement.py`, `params_to_json.py``core/structures/`
- `harness/``core/harness/`
3. 각 파일의 상대 import 갱신 (e.g., `from view_detector import ...`
`from core.parsing.view_detector import ...`)
4. `scanvas_maker.py` 의 import 블록 갱신
5. **`structure_templates.py` 는 아직 그대로 둔다.** 거대 파일이라 분할은
Phase 2 에서.
**Risk**: 순환 import. `dxf_geometry``view_detector` 같은 사이클이 잠재.
**Mitigation**: 이동 전 `python -c "import scanvas_maker"` smoke test. 한 모듈씩
이동하면서 매번 검증.
**Rollback**: Phase 1 은 단순 이동이라 git revert 로 즉시 복구.
**Backwards compat**: 다음 5개 파일을 **shim 으로 유지**:
```python
# /dxf_geometry.py (1줄짜리 shim)
from core.geo.dxf_geometry import * # noqa: F401, F403
from core.geo.dxf_geometry import __all__ # noqa: F401
```
→ 외부 사용자(있다면)나 일회성 스크립트가 깨지지 않음. Phase 3 에 제거.
---
### Phase 2 — Structure plugin 추출 (1~2 세션)
**범위**: `structure_templates.py` 의 7개 구체 클래스를 `plugins/structures/<id>/`
로 분할 + manifest.json 작성.
**작업** (1 plugin 씩, 가장 단순한 것부터):
1. `plugins/structures/spillway_gate/` 생성
2. `gate_parser.py`, `gate_3d_builder.py`, `gate_3d_builder_bpy.py`,
`validate_gate_params.py` 를 그 안으로 이동
3. `structure_templates.py``SpillwayGateTemplate` 클래스를 추출하여
`plugins/structures/spillway_gate/plugin.py` 로 이동, 부모를 `StructurePlugin`
으로 변경
4. `manifest.json`, `parameters.json`, `samples/sample_default.json` 작성
5. `core/structures/registry.py``_register_defaults()` 에서 `SpillwayGateTemplate`
import 를 제거 — plugin manager 가 대신 등록
6. **레거시 import 호환**: `gate_parser.py` 등 기존 경로 import 가 있는 파일
(현재 `gemini_renderer.py`, `apply_blender_patch.py`, `params_to_json.py` 등)을
확인하여 새 경로로 갱신. 외부 사용자용 shim 은 아래 backward compat 섹션 참조.
7. 동일 절차로 IntakeTower → ValveChamber → RetainingWall → Building/Bridge/Tunnel
→ Generic 순서.
**Risk**: 6번 항목. 특히 `from gate_parser import GateParams` 같은 import 가
plugin 외부 코드에서 발견될 가능성.
**Mitigation**: 각 plugin 추출 후 grep 으로 잔존 import 확인. shim 추가.
**Rollback**: plugin 단위로 git tag 를 찍어 두면 한 plugin 만 되돌리기 가능.
---
### Phase 3 — Render/QA plugin 추출 + 사용자 plugin 디스커버리 (1 세션)
**범위**:
1. `gemini_renderer.py``plugins/render/gemini/plugin.py` (RenderPlugin 인터페이스 적용)
2. `blender_renderer.py``plugins/render/blender/plugin.py`
3. `structure_vlm_feedback.py``plugins/qa/vlm_feedback/plugin.py`
4. `core/plugin_manager.py``bootstrap_plugins()``user_data_dir() / "user_plugins"`
를 두 번째 디스커버리 루트로 사용하도록 활성화
5. `*.scanvas-lib` (ZIP) → `user_plugins/<id>/` 풀어주는 헬퍼 `import_library_zip(path)`
6. GUI 사이드바에 "라이브러리 가져오기" 버튼 (드래그 앤 드롭 → import_library_zip)
**Risk**: PyInstaller 번들에서 `_MEIPASS` 안의 plugin 디렉토리를 importlib 가
정확히 찾는지. **Mitigation**: Phase 1 의 smoke test 를 PyInstaller 빌드 후에도
재실행. spec 파일에 `--add-data "plugins/;plugins/"` 추가.
**Rollback**: plugin manager bootstrap 호출 1줄을 주석 처리하면 즉시 옛 동작
복귀.
---
## 6. Backward Compatibility
### 6.1 기존 import 가 살아남는 방법
| 옛 경로 | 새 경로 | shim 전략 |
|---|---|---|
| `from gate_parser import GateParams, parse_gate_dxf` | `plugins.structures.spillway_gate.parser` | **별도 shim 모듈 안 둠**. 호출자 모두 내부 코드라 직접 갱신 |
| `from gate_3d_builder import GateBuilder` | `plugins.structures.spillway_gate.builder_pyvista` | 위와 동일 |
| `from structure_templates import REGISTRY as STRUCTURE_REGISTRY` | `from core.structures.registry import REGISTRY as STRUCTURE_REGISTRY` | `structure_templates.py` 를 **shim 한 줄**로 축소 |
| `from harness.logger import ...` | `from core.harness.logger import ...` | `/harness/__init__.py` 안에 `from core.harness import *` |
| `from dxf_geometry import ...` | `from core.geo.dxf_geometry import ...` | 루트의 같은 이름 .py 를 1줄 shim 으로 유지 |
| `from gemini_renderer import run_gemini_render` | `plugins.render.gemini.plugin` | 루트 shim |
### 6.2 shim 수명
- Phase 1·2 동안: shim 적극 사용 → 단계마다 외부 진입점이 안 깨짐.
- Phase 3 이후 1 release: shim 유지하되 `DeprecationWarning` 한 줄 발사.
- Phase 3 + 1 release 후: shim 제거.
### 6.3 데이터 호환
- `scanvas_jobs.db` (SQLite) — 스키마 변경 없음. ORM 모듈 위치만 이동.
- `prompt_templates/*.yaml`, `structure_types/*.yaml` — 파일 위치/포맷 변경 없음.
- 사용자 입력 파라미터 (StructureParams) — dataclass 형태 그대로.
---
## 7. 영향 받는 파일 (예상)
### 7.1 이동 (총 ~25 파일)
```
[Phase 1, Core 추출, 14 파일]
dxf_geometry.py, geo_referencing.py, tile_downloader.py, dem_extender.py,
polygon_reconstructor.py
detail_parser.py, view_detector.py, view_reconstructor.py,
optional_detector.py, filename_classifier.py
structure_placement.py, params_to_json.py
harness/seed_manager.py, harness/quality_validator.py,
harness/prompt_registry.py, harness/logger.py
[Phase 2, Structure plugin, 8 파일 + 7 manifest]
gate_parser.py, gate_3d_builder.py, gate_3d_builder_bpy.py, validate_gate_params.py
intake_tower_parser.py, intake_tower_3d_builder.py
valve_chamber_parser.py, valve_chamber_3d_builder.py
retaining_wall_parser.py, retaining_wall_3d_builder.py
+ structure_templates.py 에서 7 클래스 추출 분리
[Phase 3, Render/QA plugin, 3 파일]
gemini_renderer.py
blender_renderer.py (+ apply_blender_patch.py, fix_bpy_import.py 동반)
structure_vlm_feedback.py
```
### 7.2 신규 (총 ~30 파일)
```
core/__init__.py, core/plugin_manager.py, core/manifest.py, core/interfaces.py
core/geo/__init__.py, core/parsing/__init__.py, core/structures/__init__.py
core/structures/base.py, core/structures/registry.py
core/render/__init__.py, core/render/registry.py
core/qa/__init__.py, core/qa/registry.py
plugins/__init__.py
plugins/structures/<id>/manifest.json × 7
plugins/structures/<id>/parameters.json × 7
plugins/structures/<id>/samples/sample_default.json × 7
plugins/render/gemini/manifest.json
plugins/render/blender/manifest.json
plugins/qa/vlm_feedback/manifest.json
```
### 7.3 수정 (그대로 두지만 import 갱신)
```
scanvas_maker.py — import 블록 + bootstrap_plugins() 호출 1줄 추가
structure_templates.py — 7 클래스 추출 후 shim 으로 축소 (선택)
splash.py — 변경 없음
resource_paths.py — user_plugins() 헬퍼 추가
```
### 7.4 삭제 후보 (이미 `_unused/` 가 있으므로 동일 정책)
```
apply_blender_patch.py, fix_bpy_import.py
→ plugins/render/blender/tooling/ 로 이동 (아카이브 성격)
```
### 7.5 변경 없음
```
Design/, prompt_templates/, structure_types/, cache/, venv313/, _unused/
```
---
## 8. 검증 체크리스트 (각 Phase 완료 시)
- [ ] `python scanvas_maker.py` 가 정상 부팅 (스플래시 → GUI)
- [ ] DXF 로드 → Step 1 TIN 생성 → Step 1.5 DEM 확장 → Step 2 위성 결합 → Step 3 캡처
- [ ] 7종 구조물 모두 `STRUCTURE_REGISTRY.list_choices()` 에 나타남
- [ ] 1종 구조물 빌드 (예: spillway_gate) 후 PyVista 미리보기 정상
- [ ] Step 4 AI 렌더 (Gemini 또는 Stability) 1회 성공
- [ ] `harness` SQLite 작업 이력 1건 기록 확인
- [ ] PyInstaller `pyinstaller scanvas.spec` 빌드 성공 (Phase 3 종료 시)
---
## 9. 미결 / 추가 논의 필요
1. **새 카테고리 트리** (`hydraulic_structure / transportation / building / landscape /
terrain`) 와 **기존 `structure_v1.yaml` 의 type 키** 의 연결 — yaml 의 `terrain`,
`road`, `embankment` 같은 type 은 카테고리가 아니라 *render mode* 다. 따라서
카테고리는 **manifest 에서만** 사용하고, yaml type 은 별개로 둔다.
2. **PyPI 엔트리포인트 활용 여부** — 본 계획은 manifest 스캔만 채택. 진짜 외부
배포가 필요해지면 별도 phase 4 로 검토.
3. **테스트 전략** — 현재 코드에 unit test 가 거의 없다. Phase 1 이전에 최소
smoke test (`python scanvas_maker.py --self-test`) 를 추가해 마이그레이션 안전망
확보 권장.
---
## 10. 요약 (의사결정자용 한 페이지)
- **Core**: GUI · 파이프라인 · DXF I/O · TIN · DEM · 위성타일 · harness · structure ABC/Registry.
→ `core/` 단일 패키지로 격리.
- **Plugin**: 7종 구조물 빌더 · 2종 렌더(Gemini/Blender) · 1종 QA(VLM).
→ `plugins/<kind>/<id>/manifest.json + plugin.py` 표준.
- **호스트**: 기존 `STRUCTURE_REGISTRY` (싱글톤) 가 그대로 plugin host. 단지
`_register_defaults()` 를 `plugin_manager.discover()` 로 교체.
- **마이그레이션**: 3 phase, 각 1~2 세션, 각 phase 완료 시 동작 검증 가능.
- **호환성**: 모든 기존 import 는 1~2줄 shim 으로 살아남음 → 사용자 임팩트 0.
- **최종 비전**: 사용자가 `*.scanvas-lib` ZIP 을 GUI 에 드롭 → 즉시 새 구조물 사용.
Phase 3 끝나면 도달.