# hmwebviewer — 3D 뷰어

Three.js 기반 하이브리드(SSR + CSR) 웹 3D 모델 뷰어. 설계 원본: `3d_viewer_architecture_spec.pdf`

지원 포맷: GLB · GLTF · OBJ · FBX · DAE(Collada) · IFC(BIM) | 로드 경로: SSR + CSR | 성능: 화면 FPS 표시 + 60fps 미만 시 적응형 품질 강등

문서 갱신: 2026-06-19 — 매 작업 프롬프트 종료 시 이 파일을 함께 업데이트합니다.

## 1. 개요 / 두 가지 로드 경로

### Path A — 서버 에셋 (SSR + CSR + Hydration)

서버가 미리 렌더링한 360° Animated WebP 자리표시자 + HTML/CSS 골조를 먼저 보냄(빠른 체감 로드). 뒤에서 `modelLoader.loadModel(url)`이 확장자로 디스패치하여 실제 모델을 비동기 로드/디코드. WebGL 준비 and 모델 로드 완료 둘 다 충족 시 WebP → 캔버스로 opacity 0.5s 페이드.

트리거: `?model=<url>` · 프리뷰: `/previews/<base>.webp` (포맷 무관)

### Path B — 로컬 파일 (Drag & Drop, CSR)

사용자가 `.glb .gltf .obj .fbx .dae .ifc` 드롭. `URL.createObjectURL(file)` → `loadModel` → 디코드 → 씬 추가 → `URL.revokeObjectURL`(성공/에러 `.finally()` 양쪽). 검증: `extOf()` 확장자.

트리거: 파일 드롭 / 클릭-탐색

두 경로 모두 단일 디스패처 `src/viewer/modelLoader.ts`의 `loadModel(url, renderer, onProgress)`를 거쳐 `THREE.Object3D`로 정규화 → `ThreeDViewer.onLoaded()`는 원본 포맷을 알 필요 없음.

## 2. 지원 파일 포맷

포맷 | 확장자 | 로더 | 비고 | SSR | CSR | 검증(체감 로드)
--- | --- | --- | --- | --- | --- | ---
glTF (Binary/JSON) | `.glb .gltf` | GLTFLoader (+DRACO +KTX2 싱글톤) | Draco 지오메트리 + KTX2/Basis·WebP 텍스처. 기준 경로. | ✔ | ✔ | PASS 229ms
Wavefront OBJ | `.obj` | OBJLoader (지연 청크 8.8KB) | 지오메트리 전용. `blob:` URL에선 외부 `.mtl`/텍스처 미해결 → 기본 머티리얼. | ✔ | ✔ | PASS 173ms
Autodesk FBX | `.fbx` | FBXLoader (지연 청크 47.8KB) | 바이너리(자체 포함). 애니메이션 가능(`.animations`) — 현재 정적 표시. | ✔ | ✔ | PASS 158ms
COLLADA | `.dae` | ColladaLoader (지연 청크 41KB) | XML(DOMParser 필요). 외부 텍스처는 OBJ와 동일 제약. | ✔ | ✔ | PASS 178ms
IFC (BIM) | `.ifc` | web-ifc `IfcAPI` (단일스레드 wasm, 지연 청크 3.5MB) | `StreamAllMeshes` → `BufferGeometry`(인터리브 stride-6 pos+normal). 단일스레드라 COOP/COEP 헤더 불필요. `geom.delete()`+`CloseModel`로 wasm 메모리 해제. | ✔ | ✔ | PASS 463ms wasm Init 포함

검증: 빌드 `dist` + `vite preview`에 대해 헤드리스 Chrome으로 포맷별 1개 샘플 로드, 네비게이션→`hmw:ready` 측정. 전체 <3000ms PASS (`tools/perf-smoke.mjs`).

## 3. 기술 스택 (확정)

영역 | 선택 | 비고
--- | --- | ---
렌더러 | three.js WebGLRenderer (r0.169) | WebGPU는 차후 옵션
로더(기준) | GLTFLoader + DRACOLoader + KTX2Loader | 각 1개 싱글톤 — 다중 인스턴스 크래시(#22445)
로더(확장) | OBJ / FBX / Collada — three example loaders | 지연 `import()` → 별도 Vite 청크(초기 번들 미증가)
로더(IFC) | web-ifc `IfcAPI` (v0.0.77) | 단일스레드 `web-ifc.wasm` 자가호스팅(`/web-ifc/`), `SetWasmPath(...,true)`
적응형 품질 | FPS 측정 + pixelRatio 사다리 | 외부 의존성 0 — `ui/fps.ts` + `viewer/adaptiveQuality.ts`
지오메트리 압축 | Draco | ~73% 축소 검증됨
텍스처 압축 | WebP(런타임) / KTX2·Basis(고급) | KTX2는 별도 CLI 단계(`toktx`)
빌드 | Vite + TypeScript (strict) | three vendor + 포맷별 로더 청크 분리
디코더 호스팅 | `public/draco` + `public/basis` + `public/web-ifc` | postinstall로 자동 복사
에셋 파이프라인 | gltf-transform + draco3d | `tools/preprocess.mjs` (GLB 전용)

## 4. 런타임 파일 구조

    src/
      main.ts                 # 진입점 — ThreeDViewer + Dropzone 와이어링, ?model= Path A
      viewer/
        loaders.ts            # getLoaders() 싱글톤 (GLTF+DRACO+KTX2)
        modelLoader.ts        # ★ loadModel() 포맷 디스패치 (glb/gltf/obj/fbx/dae/ifc) → Object3D
        ThreeDViewer.ts       # 핵심 클래스: 렌더러/씬/카메라/OrbitControls/로드/dispose + FPS·적응형 와이어링
        adaptiveQuality.ts    # ★ pixelRatio 사다리 + 히스테리시스 (60fps↓ 강등)
        hydration.ts          # createHydrationGate() — 양쪽 마크 시 페이드
      dnd/dropzone.ts         # HTML5 DnD + 클릭탐색 + extOf 검증 → loadLocalFile
      ui/
        progress.ts           # showProgress/hideProgress/setProgress/setStatus
        fps.ts                # ★ 화면 FPS 오버레이 (EMA, 색상코딩)
    public/
      draco/ basis/           # 디코더 WASM (자동 복사)
      web-ifc/                # ★ web-ifc.wasm (단일스레드, 자동 복사)
      samples/                # Box/Duck/Avocado/Duck.optimized + Cube.obj/dae/fbx/ifc
      previews/               # Duck.webp + Cube.{obj,dae,fbx,ifc}.webp (24프레임 360°)
    tools/
      preprocess.mjs          # gltf-transform Draco+WebP 압축 (GLB 전용)
      copy-decoders.mjs       # postinstall: draco+basis+web-ifc 복사
      prerender.mjs           # 360° 턴테이블 → Animated WebP (포맷 무관)
      perf-smoke.mjs          # 헤드리스 체감 로드 측정 (<3s)
      adaptive-smoke.mjs      # ★ CPU 스로틀로 60fps↓ 강등 실증

## 5. 멀티에이전트 + 다이나믹 워크플로우

에이전트가 `PLAN.md` + `PROGRESS.md`를 매 시작 시 읽어 다음 작업을 결정. 병렬 안전(파일 스코프 분리).

Phase 6/7은 다이나믹 Workflow `multiformat-viewer`로 구현 (7 에이전트, 296k 토큰): Research(3 병렬·읽기전용: web-ifc API / 적응형 성능 / three 로더 형태) → Core(viewer-core) → Enhance(FPS ‖ 에셋) → Verify(빌드 green). git 미사용 환경이라 worktree 격리 대신 병렬 웨이브별 파일 스코프 분리로 충돌 방지.

### 에이전트

이름 | 역할
--- | ---
task-lead | PLAN/PROGRESS 읽고 다음 작업 분배
viewer-core | src/viewer/* 씬·로더·하이드레이션·FPS
dnd-handler | src/dnd/* 드래그드롭·검증
asset-pipeline | tools/* 압축·프리렌더·샘플
hydration | SSR 자리표시자→캔버스 페이드
reviewer | 누수/에러/성능 감사(읽기 위주)

### 명령 / 스킬 / 훅

명령: `/bootstrap` `/next-task` `/report` `/optimize-asset` `/verify-viewer`

스킬: `threejs-viewer`(로드 패턴), `task-graph`(조정 프로토콜)

훅: `session-start.sh`(PLAN/PROGRESS 읽기 알림), `progress-nudge.sh`(src 편집 시 알림)

CLAUDE.md가 두 파일 선독을 강제 + 에이전트/스킬/명령 매핑표 + 세션 런북 포함.

## 6. 계획 진행 상태 (PLAN.md)

Phase | 범위 | 상태
--- | --- | ---
P0 Bootstrap | Vite/TS 셋업, 디코더, 기본 씬 | done · 빌드 검증
P1 Core 뷰어 | 싱글톤 로더, loadServer/Local, OrbitControls, 프레이밍 | done · 빌드 검증
P2 Drag & Drop | 드롭존 UI, 검증, loadLocalFile 와이어링 | done · 빌드 검증
P3 Hydration | WebP 자리표시자, 게이트, UI 토글 | done
P4 에셋 파이프라인 | preprocess.mjs, 샘플셋, 프리렌더 | P4-1/2/3 done
P5 Hardening | 누수/에러 감사, 퍼포먼스, 리뷰 | P5-1/2/3/4 done 0 critical
P6 멀티포맷 로더 | OBJ/FBX/DAE/IFC, SSR+CSR, modelLoader 디스패치, 샘플+프리뷰 | P6-1..5 done 6포맷 검증
P7 FPS + 적응형 품질 | 화면 FPS, pixelRatio 사다리, animate 와이어링 | P7-1/2/3 done 강등 실증

## 7. FPS 표시 + 적응형 품질 (<60fps → 최적화)

- FPS 오버레이 (`ui/fps.ts`): 좌상단, 프레임 델타 EMA 평활, ~250ms 스로틀 DOM 갱신. 색상 — ≥60 녹색 / 30–59 주황 / <30 빨강.
- 적응형 사다리 (`viewer/adaptiveQuality.ts`): pixelRatio 5단계 `[min(DPR,2) · 1.5 · 1 · 0.75 · 0.5]`. 가장 고효율·저위험 런타임 노브(드로잉 버퍼 재할당, CSS 리플로우 없음).
- 히스테리시스(진동 방지): 평균 <60fps가 60프레임 지속 시 1단계 강등 / 평균 >72fps가 180프레임 지속 시 1단계 복귀 / 60–72 데드밴드는 카운터 리셋.
- 실증(`tools/adaptive-smoke.mjs`): 헤드리스 swiftshader + CDP CPU 스로틀 6×로 ABeautifulGame(11.5MB) 로드 → 실측 FPS 19→4, 적응형 tier 0→1→2→3 (pr 1.5→1→0.75) 로그 캡처. 로직 미패치, 실제 프레임 측정으로 발화.

참고: CPU 스로틀 하에선 병목이 JS/지오메트리(필레이트 아님)라 pixelRatio 강등의 FPS 회복폭이 작음(4→7) — 메커니즘은 정상 발화하나, 이 노브는 GPU 필레이트 병목 씬에서 효과가 큼.

## 8. 현재 구현 상태

- 빌드: `tsc --noEmit` 클린, `npm run build` 통과 (vite 5.4.21, 11.92s). 청크: 앱 138KB(gz 47) · three vendor 533KB(gz 136) · web-ifc 3.5MB(gz 398, 지연 로드) · OBJ 8.8 / Collada 41 / FBX 48KB.
- 포맷별 인브라우저 로드(헤드리스 Chrome): GLB 229ms · OBJ 173ms · FBX 158ms · DAE 178ms · IFC 463ms · Duck.optimized 182ms — 전체 <3000ms PASS.
- 적응형 품질: 60fps 미만 지속 시 pixelRatio 3단계 강등 실측 확인(위 7절).
- 샘플: Box/Duck/Avocado/Duck.optimized(GLB) + Cube.obj(793B)/Cube.dae(2.1K)/Cube.fbx(16.2K)/Cube.ifc(2.3K). 각 24프레임 360° WebP 프리뷰.
- KTX2 런타임 경로: KTX2Loader+DRACOLoader 동시 디코드 확인(ABeautifulGame, 626ms). 인코딩은 toktx 필요(미설치).
- 하드닝: WebGL 미지원 가드, 로드 실패 메시지(`#status`), DnD 검증 피드백, IFC wasm 메모리 해제 — reviewer 0 critical.

## 9. 실행 명령

    # 개발 서버 (HMR)
    npm run dev            # → http://127.0.0.1:3333

    # 프로덕션 빌드 + 미리보기
    npm run build         # tsc --noEmit && vite build → dist/
    npm run preview       # → http://127.0.0.1:4173

    # 사용
    #  CSR(Path B): .glb .gltf .obj .fbx .dae .ifc 를 드롭존에 드롭 / 클릭 선택
    #  SSR(Path A): http://127.0.0.1:4173/?model=/samples/Cube.ifc

    # 검증/도구 (Git-Bash는 MSYS_NO_PATHCONV=1 접두 필요)
    node tools/perf-smoke.mjs                                   # 포맷별 체감 로드 <3s
    node tools/adaptive-smoke.mjs --throttle 6 --seconds 30    # 60fps↓ 강등 실증
    node tools/preprocess.mjs <file.glb>                        # Draco+WebP 압축(GLB)
    node tools/prerender.mjs                                    # 360° WebP 프리뷰 생성

## 10. 다음 단계 (선택)

- ✅ 전체 PLAN 완료 (P0–P7). 6포맷 로드 · SSR+CSR · 화면 FPS · 적응형 품질 전부 인브라우저 실증.
- OBJ/DAE 외부 텍스처: 다중 파일 드롭(.obj+.mtl+이미지) 또는 zip 수용 — 현재는 `blob:` 제약으로 지오메트리 전용.
- FBX/Collada 애니메이션: `AnimationMixer` 재생(현재 정적). `document.hidden` 배경탭 FPS 가드.
- KTX2 인코딩 파이프라인: KTX-Software(toktx) 설치 후 `gltf-transform etc1s|uastc`.
- 실서버 SSR 연결 + 실기기 체감 로드 측정(현재 수치는 헤드리스).

작업 로그: `PROGRESS.md` · 작업 그래프: `PLAN.md` · 지침: `CLAUDE.md`