feat: multi-format 3D viewer with large-file OBJ streaming, Z-up, projection/outline UI
Three.js viewer supporting glb/gltf/obj/fbx/dae/ifc/ply via both server (SSR) and drag&drop (CSR) paths. - Streaming OBJ parser (src/viewer/objStream.ts) for files past the V8 max string length (>~1GB text) that OBJLoader can't handle; indexed geometry, per-vertex color from MTL Kd, float64 recenter baked in. - In-viewer float64 recenter (objRecenter.ts) for huge CAD/survey coordinates (~1e8) so float32 vertex buffers keep precision (no cracked faces). - Z-up right-handed world; Y-up formats rotated on load. - OBJ+MTL+texture drag&drop (LoadingManager URL-modifier maps dropped images). - OrbitControls ground-plane panning (road/rail alignment workflow). - UI: Zoom Fit, perspective/orthographic toggle, feature-edge outline. - DoubleSide for CAD OBJ; PLY mesh + point-cloud support. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>hmwebviewer — 아키텍처 / 구조 / 계획</title>
|
||||
<style>
|
||||
:root { color-scheme: dark; }
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; padding: 32px; max-width: 1000px; margin: 0 auto;
|
||||
font: 15px/1.6 system-ui, "Segoe UI", "Malgun Gothic", sans-serif;
|
||||
background: #0f1115; color: #e6e9ef;
|
||||
}
|
||||
h1 { font-size: 26px; border-bottom: 2px solid #4aa3ff; padding-bottom: 10px; }
|
||||
h2 { font-size: 19px; margin-top: 36px; color: #4aa3ff; border-left: 4px solid #4aa3ff; padding-left: 10px; }
|
||||
h3 { font-size: 16px; margin-top: 24px; color: #cfe3ff; }
|
||||
code, pre { font-family: "Cascadia Code", Consolas, monospace; }
|
||||
code { background: #1c2230; padding: 1px 5px; border-radius: 4px; color: #ffd479; font-size: 13px; }
|
||||
pre { background: #161a24; border: 1px solid #262c3a; border-radius: 8px; padding: 14px; overflow-x: auto; font-size: 13px; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 13.5px; }
|
||||
th, td { border: 1px solid #2a3142; padding: 7px 10px; text-align: left; vertical-align: top; }
|
||||
th { background: #1a2030; color: #9fc4ff; }
|
||||
tr:nth-child(even) td { background: #141821; }
|
||||
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
|
||||
.card { background: #161a24; border: 1px solid #262c3a; border-radius: 8px; padding: 14px 16px; }
|
||||
.badge { display: inline-block; padding: 2px 9px; border-radius: 10px; font-size: 12px; font-weight: 600; }
|
||||
.ok { background: #143a23; color: #57d98a; }
|
||||
.wip { background: #3a3014; color: #e4b35a; }
|
||||
.blk { background: #3a1414; color: #e48080; }
|
||||
.meta { color: #8b93a7; font-size: 13px; }
|
||||
.arrow { color: #4aa3ff; font-weight: 700; }
|
||||
.sep { border: 0; border-top: 1px solid #262c3a; margin: 28px 0; }
|
||||
.yes { color: #57d98a; font-weight: 700; }
|
||||
.no { color: #e48080; font-weight: 700; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>hmwebviewer — 3D 뷰어</h1>
|
||||
<p class="meta">Three.js 기반 하이브리드(SSR + CSR) 웹 3D 모델 뷰어. 설계 원본: <code>3d_viewer_architecture_spec.pdf</code></p>
|
||||
<p class="meta"><strong>지원 포맷:</strong> GLB · GLTF · OBJ · FBX · DAE(Collada) · IFC(BIM) | <strong>로드 경로:</strong> SSR + CSR | <strong>성능:</strong> 화면 FPS 표시 + 60fps 미만 시 적응형 품질 강등</p>
|
||||
<p class="meta"><strong>문서 갱신:</strong> <span id="updated">2026-06-19</span> — 매 작업 프롬프트 종료 시 이 파일을 함께 업데이트합니다.</p>
|
||||
|
||||
<h2>1. 개요 / 두 가지 로드 경로</h2>
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h3>Path A — 서버 에셋 (SSR + CSR + Hydration)</h3>
|
||||
<p>서버가 미리 렌더링한 360° Animated WebP 자리표시자 + HTML/CSS 골조를 먼저 보냄(빠른 체감 로드). 뒤에서 <code>modelLoader.loadModel(url)</code>이 확장자로 디스패치하여 실제 모델을 비동기 로드/디코드. WebGL 준비 <strong>and</strong> 모델 로드 완료 둘 다 충족 시 WebP <span class="arrow">→</span> 캔버스로 opacity 0.5s 페이드.</p>
|
||||
<p class="meta">트리거: <code>?model=<url></code> · 프리뷰: <code>/previews/<base>.webp</code> (포맷 무관)</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Path B — 로컬 파일 (Drag & Drop, CSR)</h3>
|
||||
<p>사용자가 <code>.glb .gltf .obj .fbx .dae .ifc</code> 드롭. <code>URL.createObjectURL(file)</code> <span class="arrow">→</span> <code>loadModel</code> <span class="arrow">→</span> 디코드 <span class="arrow">→</span> 씬 추가 <span class="arrow">→</span> <code>URL.revokeObjectURL</code>(성공/에러 <code>.finally()</code> 양쪽). 검증: <code>extOf()</code> 확장자.</p>
|
||||
<p class="meta">트리거: 파일 드롭 / 클릭-탐색</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="meta">두 경로 모두 단일 디스패처 <code>src/viewer/modelLoader.ts</code>의 <code>loadModel(url, renderer, onProgress)</code>를 거쳐 <code>THREE.Object3D</code>로 정규화 → <code>ThreeDViewer.onLoaded()</code>는 원본 포맷을 알 필요 없음.</p>
|
||||
|
||||
<h2>2. 지원 파일 포맷</h2>
|
||||
<table>
|
||||
<tr><th>포맷</th><th>확장자</th><th>로더</th><th>비고</th><th>SSR</th><th>CSR</th><th>검증(체감 로드)</th></tr>
|
||||
<tr>
|
||||
<td>glTF (Binary/JSON)</td><td><code>.glb .gltf</code></td>
|
||||
<td>GLTFLoader<br>(+DRACO +KTX2 싱글톤)</td>
|
||||
<td>Draco 지오메트리 + KTX2/Basis·WebP 텍스처. 기준 경로.</td>
|
||||
<td class="yes">✔</td><td class="yes">✔</td><td><span class="badge ok">PASS 229ms</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Wavefront OBJ</td><td><code>.obj</code></td>
|
||||
<td>OBJLoader (지연 청크 8.8KB)</td>
|
||||
<td>지오메트리 전용. <code>blob:</code> URL에선 외부 <code>.mtl</code>/텍스처 미해결 → 기본 머티리얼.</td>
|
||||
<td class="yes">✔</td><td class="yes">✔</td><td><span class="badge ok">PASS 173ms</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Autodesk FBX</td><td><code>.fbx</code></td>
|
||||
<td>FBXLoader (지연 청크 47.8KB)</td>
|
||||
<td>바이너리(자체 포함). 애니메이션 가능(<code>.animations</code>) — 현재 정적 표시.</td>
|
||||
<td class="yes">✔</td><td class="yes">✔</td><td><span class="badge ok">PASS 158ms</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>COLLADA</td><td><code>.dae</code></td>
|
||||
<td>ColladaLoader (지연 청크 41KB)</td>
|
||||
<td>XML(DOMParser 필요). 외부 텍스처는 OBJ와 동일 제약.</td>
|
||||
<td class="yes">✔</td><td class="yes">✔</td><td><span class="badge ok">PASS 178ms</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IFC (BIM)</td><td><code>.ifc</code></td>
|
||||
<td>web-ifc <code>IfcAPI</code><br>(단일스레드 wasm, 지연 청크 3.5MB)</td>
|
||||
<td><code>StreamAllMeshes</code> <span class="arrow">→</span> <code>BufferGeometry</code>(인터리브 stride-6 pos+normal). 단일스레드라 <strong>COOP/COEP 헤더 불필요</strong>. <code>geom.delete()</code>+<code>CloseModel</code>로 wasm 메모리 해제.</td>
|
||||
<td class="yes">✔</td><td class="yes">✔</td><td><span class="badge ok">PASS 463ms</span><br><span class="meta">wasm Init 포함</span></td>
|
||||
</tr>
|
||||
</table>
|
||||
<p class="meta">검증: 빌드 <code>dist</code> + <code>vite preview</code>에 대해 헤드리스 Chrome으로 포맷별 1개 샘플 로드, 네비게이션→<code>hmw:ready</code> 측정. 전체 <3000ms PASS (<code>tools/perf-smoke.mjs</code>).</p>
|
||||
|
||||
<h2>3. 기술 스택 (확정)</h2>
|
||||
<table>
|
||||
<tr><th>영역</th><th>선택</th><th>비고</th></tr>
|
||||
<tr><td>렌더러</td><td>three.js WebGLRenderer (r0.169)</td><td>WebGPU는 차후 옵션</td></tr>
|
||||
<tr><td>로더(기준)</td><td>GLTFLoader + DRACOLoader + KTX2Loader</td><td><strong>각 1개 싱글톤</strong> — 다중 인스턴스 크래시(#22445)</td></tr>
|
||||
<tr><td>로더(확장)</td><td>OBJ / FBX / Collada — three example loaders</td><td>지연 <code>import()</code> → 별도 Vite 청크(초기 번들 미증가)</td></tr>
|
||||
<tr><td>로더(IFC)</td><td>web-ifc <code>IfcAPI</code> (v0.0.77)</td><td>단일스레드 <code>web-ifc.wasm</code> 자가호스팅(<code>/web-ifc/</code>), <code>SetWasmPath(...,true)</code></td></tr>
|
||||
<tr><td>적응형 품질</td><td>FPS 측정 + pixelRatio 사다리</td><td>외부 의존성 0 — <code>ui/fps.ts</code> + <code>viewer/adaptiveQuality.ts</code></td></tr>
|
||||
<tr><td>지오메트리 압축</td><td>Draco</td><td>~73% 축소 검증됨</td></tr>
|
||||
<tr><td>텍스처 압축</td><td>WebP(런타임) / KTX2·Basis(고급)</td><td>KTX2는 별도 CLI 단계(<code>toktx</code>)</td></tr>
|
||||
<tr><td>빌드</td><td>Vite + TypeScript (strict)</td><td>three vendor + 포맷별 로더 청크 분리</td></tr>
|
||||
<tr><td>디코더 호스팅</td><td><code>public/draco</code> + <code>public/basis</code> + <code>public/web-ifc</code></td><td>postinstall로 자동 복사</td></tr>
|
||||
<tr><td>에셋 파이프라인</td><td>gltf-transform + draco3d</td><td><code>tools/preprocess.mjs</code> (GLB 전용)</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>4. 런타임 파일 구조</h2>
|
||||
<pre>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↓ 강등 실증
|
||||
</pre>
|
||||
|
||||
<h2>5. 멀티에이전트 + 다이나믹 워크플로우</h2>
|
||||
<p>에이전트가 <code>PLAN.md</code> + <code>PROGRESS.md</code>를 매 시작 시 읽어 다음 작업을 결정. 병렬 안전(파일 스코프 분리).</p>
|
||||
<p class="card"><strong>Phase 6/7은 다이나믹 Workflow <code>multiformat-viewer</code>로 구현</strong> (7 에이전트, 296k 토큰): Research(3 병렬·읽기전용: web-ifc API / 적응형 성능 / three 로더 형태) <span class="arrow">→</span> Core(viewer-core) <span class="arrow">→</span> Enhance(FPS ‖ 에셋) <span class="arrow">→</span> Verify(빌드 green). git 미사용 환경이라 worktree 격리 대신 병렬 웨이브별 파일 스코프 분리로 충돌 방지.</p>
|
||||
<div class="grid">
|
||||
<div>
|
||||
<h3>에이전트</h3>
|
||||
<table>
|
||||
<tr><th>이름</th><th>역할</th></tr>
|
||||
<tr><td>task-lead</td><td>PLAN/PROGRESS 읽고 다음 작업 분배</td></tr>
|
||||
<tr><td>viewer-core</td><td>src/viewer/* 씬·로더·하이드레이션·FPS</td></tr>
|
||||
<tr><td>dnd-handler</td><td>src/dnd/* 드래그드롭·검증</td></tr>
|
||||
<tr><td>asset-pipeline</td><td>tools/* 압축·프리렌더·샘플</td></tr>
|
||||
<tr><td>hydration</td><td>SSR 자리표시자→캔버스 페이드</td></tr>
|
||||
<tr><td>reviewer</td><td>누수/에러/성능 감사(읽기 위주)</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<div>
|
||||
<h3>명령 / 스킬 / 훅</h3>
|
||||
<p><strong>명령:</strong> <code>/bootstrap</code> <code>/next-task</code> <code>/report</code> <code>/optimize-asset</code> <code>/verify-viewer</code></p>
|
||||
<p><strong>스킬:</strong> <code>threejs-viewer</code>(로드 패턴), <code>task-graph</code>(조정 프로토콜)</p>
|
||||
<p><strong>훅:</strong> <code>session-start.sh</code>(PLAN/PROGRESS 읽기 알림), <code>progress-nudge.sh</code>(src 편집 시 알림)</p>
|
||||
<p class="meta">CLAUDE.md가 두 파일 선독을 강제 + 에이전트/스킬/명령 매핑표 + 세션 런북 포함.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>6. 계획 진행 상태 (PLAN.md)</h2>
|
||||
<table>
|
||||
<tr><th>Phase</th><th>범위</th><th>상태</th></tr>
|
||||
<tr><td>P0 Bootstrap</td><td>Vite/TS 셋업, 디코더, 기본 씬</td><td><span class="badge ok">done · 빌드 검증</span></td></tr>
|
||||
<tr><td>P1 Core 뷰어</td><td>싱글톤 로더, loadServer/Local, OrbitControls, 프레이밍</td><td><span class="badge ok">done · 빌드 검증</span></td></tr>
|
||||
<tr><td>P2 Drag & Drop</td><td>드롭존 UI, 검증, loadLocalFile 와이어링</td><td><span class="badge ok">done · 빌드 검증</span></td></tr>
|
||||
<tr><td>P3 Hydration</td><td>WebP 자리표시자, 게이트, UI 토글</td><td><span class="badge ok">done</span></td></tr>
|
||||
<tr><td>P4 에셋 파이프라인</td><td>preprocess.mjs, 샘플셋, 프리렌더</td><td><span class="badge ok">P4-1/2/3 done</span></td></tr>
|
||||
<tr><td>P5 Hardening</td><td>누수/에러 감사, 퍼포먼스, 리뷰</td><td><span class="badge ok">P5-1/2/3/4 done</span> 0 critical</td></tr>
|
||||
<tr><td>P6 멀티포맷 로더</td><td>OBJ/FBX/DAE/IFC, SSR+CSR, modelLoader 디스패치, 샘플+프리뷰</td><td><span class="badge ok">P6-1..5 done</span> 6포맷 검증</td></tr>
|
||||
<tr><td>P7 FPS + 적응형 품질</td><td>화면 FPS, pixelRatio 사다리, animate 와이어링</td><td><span class="badge ok">P7-1/2/3 done</span> 강등 실증</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>7. FPS 표시 + 적응형 품질 (<60fps → 최적화)</h2>
|
||||
<div class="card">
|
||||
<ul>
|
||||
<li><strong>FPS 오버레이</strong> (<code>ui/fps.ts</code>): 좌상단, 프레임 델타 EMA 평활, ~250ms 스로틀 DOM 갱신. 색상 — <span class="yes">≥60 녹색</span> / 30–59 주황 / <span class="no"><30 빨강</span>.</li>
|
||||
<li><strong>적응형 사다리</strong> (<code>viewer/adaptiveQuality.ts</code>): pixelRatio 5단계 <code>[min(DPR,2) · 1.5 · 1 · 0.75 · 0.5]</code>. 가장 고효율·저위험 런타임 노브(드로잉 버퍼 재할당, CSS 리플로우 없음).</li>
|
||||
<li><strong>히스테리시스</strong>(진동 방지): 평균 <60fps가 <strong>60프레임 지속</strong> 시 1단계 강등 / 평균 >72fps가 <strong>180프레임 지속</strong> 시 1단계 복귀 / 60–72 데드밴드는 카운터 리셋.</li>
|
||||
<li><strong>실증</strong>(<code>tools/adaptive-smoke.mjs</code>): 헤드리스 swiftshader + CDP CPU 스로틀 6×로 ABeautifulGame(11.5MB) 로드 → 실측 FPS <strong>19→4</strong>, 적응형 <strong>tier 0→1→2→3</strong> (pr 1.5→1→0.75) 로그 캡처. 로직 미패치, 실제 프레임 측정으로 발화.</li>
|
||||
</ul>
|
||||
<p class="meta">참고: CPU 스로틀 하에선 병목이 JS/지오메트리(필레이트 아님)라 pixelRatio 강등의 FPS 회복폭이 작음(4→7) — 메커니즘은 정상 발화하나, 이 노브는 GPU 필레이트 병목 씬에서 효과가 큼.</p>
|
||||
</div>
|
||||
|
||||
<h2>8. 현재 구현 상태</h2>
|
||||
<div class="card">
|
||||
<ul>
|
||||
<li><strong>빌드:</strong> <code>tsc --noEmit</code> 클린, <code>npm run build</code> 통과 (vite 5.4.21, 11.92s). 청크: 앱 138KB(gz 47) · three vendor 533KB(gz 136) · web-ifc 3.5MB(gz 398, <em>지연 로드</em>) · OBJ 8.8 / Collada 41 / FBX 48KB.</li>
|
||||
<li><strong>포맷별 인브라우저 로드(헤드리스 Chrome):</strong> GLB 229ms · OBJ 173ms · FBX 158ms · DAE 178ms · <strong>IFC 463ms</strong> · Duck.optimized 182ms — 전체 <3000ms PASS.</li>
|
||||
<li><strong>적응형 품질:</strong> 60fps 미만 지속 시 pixelRatio 3단계 강등 실측 확인(위 7절).</li>
|
||||
<li><strong>샘플:</strong> Box/Duck/Avocado/Duck.optimized(GLB) + Cube.obj(793B)/Cube.dae(2.1K)/Cube.fbx(16.2K)/Cube.ifc(2.3K). 각 24프레임 360° WebP 프리뷰.</li>
|
||||
<li><strong>KTX2 런타임 경로:</strong> KTX2Loader+DRACOLoader 동시 디코드 확인(ABeautifulGame, 626ms). 인코딩은 toktx 필요(미설치).</li>
|
||||
<li><strong>하드닝:</strong> WebGL 미지원 가드, 로드 실패 메시지(<code>#status</code>), DnD 검증 피드백, IFC wasm 메모리 해제 — reviewer 0 critical.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2>9. 실행 명령</h2>
|
||||
<pre># 개발 서버 (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 프리뷰 생성
|
||||
</pre>
|
||||
|
||||
<h2>10. 다음 단계 (선택)</h2>
|
||||
<ul>
|
||||
<li>✅ 전체 PLAN 완료 (P0–P7). 6포맷 로드 · SSR+CSR · 화면 FPS · 적응형 품질 전부 인브라우저 실증.</li>
|
||||
<li>OBJ/DAE 외부 텍스처: 다중 파일 드롭(.obj+.mtl+이미지) 또는 zip 수용 — 현재는 <code>blob:</code> 제약으로 지오메트리 전용.</li>
|
||||
<li>FBX/Collada 애니메이션: <code>AnimationMixer</code> 재생(현재 정적). <code>document.hidden</code> 배경탭 FPS 가드.</li>
|
||||
<li>KTX2 인코딩 파이프라인: KTX-Software(toktx) 설치 후 <code>gltf-transform etc1s|uastc</code>.</li>
|
||||
<li>실서버 SSR 연결 + 실기기 체감 로드 측정(현재 수치는 헤드리스).</li>
|
||||
</ul>
|
||||
|
||||
<hr class="sep" />
|
||||
<p class="meta">작업 로그: <code>PROGRESS.md</code> · 작업 그래프: <code>PLAN.md</code> · 지침: <code>CLAUDE.md</code></p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user