문제 해결: 1. ReferenceError 수정 (controls 초기화 순서) 2. 불필요한 디버깅 코드 제거 3. 코드 구조 단순화 유지된 기능: ✓ OrbitControls (향상된 카메라 제어) ✓ 샘플 부재 (테스트용 녹색 박스) ✓ 부재 로드 (stats.json에서 588개) ✓ 완벽한 레이아웃 (16:9 대응) ✓ 정보 패널 (자동 폭 조정) ✓ 상세한 콘솔 로그 이제 3D 모델이 정상적으로 렌더링될 것입니다. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
385 lines
12 KiB
HTML
385 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>가시설 3D 모델 뷰어</title>
|
|
<style>
|
|
html, body {
|
|
margin: 0;
|
|
padding: 0;
|
|
border: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
}
|
|
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
background: #0f1419;
|
|
display: flex;
|
|
color: #fff;
|
|
}
|
|
|
|
.container {
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
overflow: hidden;
|
|
}
|
|
|
|
#canvas {
|
|
flex: 1;
|
|
background: linear-gradient(135deg, #1a3a52 0%, #0d1f2d 100%);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.panel {
|
|
width: fit-content;
|
|
min-width: 300px;
|
|
max-width: 650px;
|
|
height: 100%;
|
|
background: #1a1f2e;
|
|
padding: 12px;
|
|
overflow-y: auto;
|
|
overflow-x: hidden;
|
|
border-left: 1px solid #2d3748;
|
|
display: flex;
|
|
flex-direction: column;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.panel::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
|
|
.panel::-webkit-scrollbar-track {
|
|
background: #1a1f2e;
|
|
}
|
|
|
|
.panel::-webkit-scrollbar-thumb {
|
|
background: #4a5568;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.panel::-webkit-scrollbar-thumb:hover {
|
|
background: #718096;
|
|
}
|
|
|
|
.panel h2 {
|
|
color: #10b981;
|
|
font-size: 13px;
|
|
margin-bottom: 6px;
|
|
padding-bottom: 4px;
|
|
border-bottom: 2px solid #10b981;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.panel h3 {
|
|
color: #cbd5e0;
|
|
font-size: 10px;
|
|
margin-bottom: 4px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.3px;
|
|
}
|
|
|
|
.badge {
|
|
display: inline-block;
|
|
background: #10b981;
|
|
color: #1a1f2e;
|
|
padding: 3px 8px;
|
|
border-radius: 14px;
|
|
font-size: 9px;
|
|
font-weight: 700;
|
|
margin-bottom: 6px;
|
|
width: fit-content;
|
|
}
|
|
|
|
.loading {
|
|
padding: 8px;
|
|
background: #252d3d;
|
|
border-radius: 6px;
|
|
color: #fbbf24;
|
|
font-size: 11px;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.section {
|
|
margin-bottom: 8px;
|
|
padding: 8px;
|
|
background: #252d3d;
|
|
border-radius: 6px;
|
|
border-left: 3px solid #10b981;
|
|
}
|
|
|
|
.stat-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
font-size: 11px;
|
|
color: #cbd5e0;
|
|
margin: 3px 0;
|
|
}
|
|
|
|
.stat-label {
|
|
color: #a0aec0;
|
|
}
|
|
|
|
.stat-value {
|
|
font-weight: 600;
|
|
color: #10b981;
|
|
}
|
|
|
|
.legend {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 8px;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.legend-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: 10px;
|
|
}
|
|
|
|
.legend-color {
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.controls {
|
|
padding: 8px;
|
|
background: #252d3d;
|
|
border-radius: 6px;
|
|
margin-top: auto;
|
|
font-size: 10px;
|
|
color: #cbd5e0;
|
|
line-height: 1.5;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<canvas id="canvas"></canvas>
|
|
<div class="panel">
|
|
<h2>📊 모델 정보</h2>
|
|
<span class="badge">가시설 계획도 3D</span>
|
|
|
|
<div id="loadingMsg" class="loading">
|
|
⏳ 모델 로드 중...
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h3>📈 부재 수량</h3>
|
|
<div class="stat-row">
|
|
<span class="stat-label">총 부재:</span>
|
|
<span class="stat-value" id="totalParts">-</span>
|
|
</div>
|
|
<div class="stat-row">
|
|
<span class="stat-label">총 부피:</span>
|
|
<span class="stat-value" id="totalVolume">-</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h3>🏗️ 부재 구성</h3>
|
|
<div style="font-size: 10px; line-height: 1.5; color: #cbd5e0;">
|
|
<div style="margin-bottom: 4px;"><strong style="color: #ff6b6b;">가시설4</strong> 52개</div>
|
|
<div style="margin-bottom: 4px;"><strong style="color: #7ed321;">h-pile2</strong> 244개</div>
|
|
<div style="margin-bottom: 4px;"><strong style="color: #4a90e2;">토류판</strong> 240개</div>
|
|
<div style="margin-bottom: 4px;"><strong style="color: #7ed321;">H-PILE</strong> 26개</div>
|
|
<div><strong style="color: #f5a623;">스터럿</strong> 26개</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h3>🎨 범례</h3>
|
|
<div class="legend">
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background: #ff6b6b;"></div>
|
|
<span>H-빔</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background: #7ed321;"></div>
|
|
<span>파일</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background: #4a90e2;"></div>
|
|
<span>판재</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background: #f5a623;"></div>
|
|
<span>버팀목</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="controls">
|
|
<h3>⌨️ 조작</h3>
|
|
좌클릭 드래그: 회전<br>
|
|
우클릭 드래그: 이동<br>
|
|
휠: 확대/축소
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/controls/OrbitControls.min.js"></script>
|
|
<script>
|
|
const canvas = document.getElementById('canvas');
|
|
const width = canvas.clientWidth;
|
|
const height = canvas.clientHeight;
|
|
|
|
const scene = new THREE.Scene();
|
|
scene.background = new THREE.Color(0x1a3a52);
|
|
|
|
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 500000);
|
|
camera.position.set(-216, -2554, 3000);
|
|
camera.lookAt(-216, -2554, 0);
|
|
|
|
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
|
renderer.setSize(width, height);
|
|
renderer.setPixelRatio(window.devicePixelRatio);
|
|
renderer.shadowMap.enabled = true;
|
|
|
|
// 조명
|
|
const directional = new THREE.DirectionalLight(0xffffff, 1.0);
|
|
directional.position.set(100000, 100000, 100000);
|
|
directional.castShadow = true;
|
|
scene.add(directional);
|
|
|
|
const ambient = new THREE.AmbientLight(0xffffff, 0.6);
|
|
scene.add(ambient);
|
|
|
|
const hemis = new THREE.HemisphereLight(0x87ceeb, 0x333333, 0.4);
|
|
scene.add(hemis);
|
|
|
|
// 추가 조명
|
|
const pointLight = new THREE.PointLight(0xffffff, 0.8, 10000);
|
|
pointLight.position.set(500, 500, 500);
|
|
scene.add(pointLight);
|
|
|
|
// OrbitControls (먼저 초기화!)
|
|
const controls = new THREE.OrbitControls(camera, canvas);
|
|
controls.target.set(-216, -2554, 0);
|
|
controls.autoRotate = false;
|
|
controls.enableDamping = true;
|
|
controls.dampingFactor = 0.05;
|
|
controls.enableZoom = true;
|
|
controls.zoomSpeed = 1.0;
|
|
controls.enablePan = true;
|
|
controls.panSpeed = 0.5;
|
|
controls.update();
|
|
|
|
console.log('=== 3D 뷰어 초기화 ===');
|
|
console.log('✓ OrbitControls 준비 완료');
|
|
|
|
// 재질
|
|
const getMaterial = (type) => {
|
|
const colors = {
|
|
'H-Beam': 0xff6b6b,
|
|
'Pile': 0x7ed321,
|
|
'Panel': 0x4a90e2,
|
|
'Strut': 0xf5a623
|
|
};
|
|
const color = colors[type] || 0x999999;
|
|
return new THREE.MeshStandardMaterial({ color, roughness: 0.7, metalness: 0.2 });
|
|
};
|
|
|
|
const DISPLAY_SCALE = 0.01;
|
|
|
|
// 샘플 부재 (테스트용)
|
|
const testGeo = new THREE.BoxGeometry(4, 4, 80);
|
|
const testMat = new THREE.MeshStandardMaterial({ color: 0x7ed321, roughness: 0.7, emissive: 0x2d5a2d });
|
|
const testMesh = new THREE.Mesh(testGeo, testMat);
|
|
testMesh.position.set(0, -2554, 100);
|
|
scene.add(testMesh);
|
|
console.log('✓ 샘플 부재 추가됨');
|
|
|
|
// 그리드 & 축
|
|
const gridHelper = new THREE.GridHelper(3000, 20, 0x444444, 0x222222);
|
|
gridHelper.position.y = -2300;
|
|
scene.add(gridHelper);
|
|
|
|
const axisHelper = new THREE.AxesHelper(500);
|
|
scene.add(axisHelper);
|
|
|
|
// stats.json 로드
|
|
console.log('stats.json 로드 시작...');
|
|
fetch('gasiseol-correct-v2-stats.json')
|
|
.then(r => {
|
|
console.log('응답:', r.status);
|
|
return r.json();
|
|
})
|
|
.then(data => {
|
|
console.log('✓ JSON 파싱 완료');
|
|
console.log('총 부재:', data.totalParts);
|
|
|
|
let count = 0;
|
|
data.parts.forEach(part => {
|
|
let geometry;
|
|
const { blockType, type, spec, position, volume } = part;
|
|
|
|
if (type === 'Pile') {
|
|
geometry = new THREE.BoxGeometry(4, 4, 80);
|
|
} else if (type === 'Panel') {
|
|
geometry = new THREE.BoxGeometry(7, 3.6, 0.14);
|
|
} else if (type === 'H-Beam') {
|
|
geometry = new THREE.BoxGeometry(4, 4, 60);
|
|
} else if (type === 'Strut') {
|
|
geometry = new THREE.BoxGeometry(2, 2, 40);
|
|
} else {
|
|
geometry = new THREE.BoxGeometry(3, 3, 30);
|
|
}
|
|
|
|
const mesh = new THREE.Mesh(geometry, getMaterial(type));
|
|
mesh.position.set(position.x, position.y, position.z);
|
|
scene.add(mesh);
|
|
count++;
|
|
|
|
if (count <= 3 || count % 100 === 0) {
|
|
console.log(`[${count}] ${blockType} @ (${position.x.toFixed(0)}, ${position.y.toFixed(0)})`);
|
|
}
|
|
});
|
|
|
|
console.log('✓ ' + count + '개 부재 추가됨');
|
|
|
|
document.getElementById('totalParts').textContent = data.totalParts + '개';
|
|
document.getElementById('totalVolume').textContent = Math.round(data.totalVolume).toLocaleString() + ' m³';
|
|
document.getElementById('loadingMsg').style.display = 'none';
|
|
|
|
console.log('✅ 완료!');
|
|
})
|
|
.catch(err => {
|
|
console.error('❌ 오류:', err);
|
|
document.getElementById('loadingMsg').innerHTML = '❌ 로드 실패<br>' + err.message;
|
|
});
|
|
|
|
// 렌더링
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
controls.update();
|
|
renderer.render(scene, camera);
|
|
}
|
|
animate();
|
|
|
|
// 리사이징
|
|
window.addEventListener('resize', () => {
|
|
const newWidth = canvas.clientWidth;
|
|
const newHeight = canvas.clientHeight;
|
|
camera.aspect = newWidth / newHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(newWidth, newHeight);
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|