diff --git a/resources/views/rd/fire-shutter-drawing/index.blade.php b/resources/views/rd/fire-shutter-drawing/index.blade.php
index 9f126863..1386e94e 100644
--- a/resources/views/rd/fire-shutter-drawing/index.blade.php
+++ b/resources/views/rd/fire-shutter-drawing/index.blade.php
@@ -32,6 +32,12 @@
.fs-calc-row { display: flex; justify-content: space-between; align-items: center; padding: 0.375rem 0; border-bottom: 1px solid rgba(51,65,85,0.3); }
.fs-calc-label { color: #64748b; font-size: 0.75rem; }
.fs-calc-value { color: #f8fafc; font-size: 0.875rem; font-weight: 900; }
+.fs-ctx-menu { position: fixed; display: none; z-index: 10000; background: rgba(15,23,42,0.95); border: 1px solid #334155; border-radius: 0.5rem; padding: 0.25rem; backdrop-filter: blur(12px); box-shadow: 0 10px 25px rgba(0,0,0,0.5); min-width: 160px; }
+.fs-ctx-btn { display: flex; align-items: center; gap: 0.5rem; width: 100%; padding: 0.5rem 0.75rem; border: none; background: transparent; color: #e2e8f0; font-size: 0.8125rem; font-weight: 700; cursor: pointer; border-radius: 0.375rem; white-space: nowrap; text-align: left; }
+.fs-ctx-btn:hover { background: rgba(59,130,246,0.2); color: #60a5fa; }
+.fs-ctx-sep { height: 1px; background: #334155; margin: 0.25rem 0; }
+.fs-iso-badge { position: absolute; top: 12px; left: 12px; display: none; padding: 6px 14px; background: rgba(59,130,246,0.9); color: white; font-size: 0.75rem; font-weight: 900; border-radius: 0.5rem; z-index: 100; cursor: pointer; box-shadow: 0 4px 12px rgba(59,130,246,0.4); }
+.fs-iso-badge:hover { background: rgba(37,99,235,1); }
@@ -436,6 +442,12 @@
// 3D objects
let scene, camera, renderer, controls, animId;
let meshes = {};
+ let fs3dIsolated = null; // 현재 단품 보기 중인 키
+ const meshLabels = {
+ case: '셔터박스', shaft: '샤프트 ASSY', motor: '모터/체인',
+ rails: '가이드레일', slats: '슬랫 커튼', slatRoll: '감긴 슬랫',
+ bottomBar: '하부바', wall: '벽체',
+ };
// ============================
// TAB SWITCHING
@@ -1008,6 +1020,90 @@ function animate() {
}
});
ro.observe(container);
+
+ // === 우클릭 컨텍스트 메뉴 (단품 보기 / 전체 보기) ===
+ const ctxMenu = document.createElement('div');
+ ctxMenu.className = 'fs-ctx-menu';
+ document.body.appendChild(ctxMenu);
+
+ // 단품 보기 상태 배지
+ container.style.position = 'relative';
+ const isoBadge = document.createElement('div');
+ isoBadge.className = 'fs-iso-badge';
+ isoBadge.onclick = () => { fs3dShowAll(); };
+ container.appendChild(isoBadge);
+
+ const raycaster = new THREE.Raycaster();
+ const mouse = new THREE.Vector2();
+
+ // 클릭된 메시에서 meshes 키 찾기
+ function findMeshKey(hitObj) {
+ let target = hitObj;
+ while (target.parent && target.parent !== scene) target = target.parent;
+ for (const [key, obj] of Object.entries(meshes)) {
+ if (obj === target) return key;
+ }
+ return null;
+ }
+
+ // 우클릭 이벤트
+ renderer.domElement.addEventListener('contextmenu', (e) => {
+ e.preventDefault();
+ ctxMenu.style.display = 'none';
+
+ const rect = renderer.domElement.getBoundingClientRect();
+ mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
+ mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
+ raycaster.setFromCamera(mouse, camera);
+
+ const sceneObjects = Object.values(meshes).filter(Boolean);
+ const intersects = raycaster.intersectObjects(sceneObjects, true);
+
+ let hitKey = null;
+ if (intersects.length > 0) hitKey = findMeshKey(intersects[0].object);
+
+ // 메뉴 항목 구성
+ ctxMenu.innerHTML = '';
+ let hasItem = false;
+
+ if (hitKey) {
+ const label = meshLabels[hitKey] || hitKey;
+ const btn = document.createElement('button');
+ btn.className = 'fs-ctx-btn';
+ btn.innerHTML = `
'${label}' 단품 보기`;
+ btn.onclick = () => { fs3dIsolate(hitKey); ctxMenu.style.display = 'none'; };
+ ctxMenu.appendChild(btn);
+ hasItem = true;
+ }
+
+ if (fs3dIsolated) {
+ if (hasItem) {
+ const sep = document.createElement('div');
+ sep.className = 'fs-ctx-sep';
+ ctxMenu.appendChild(sep);
+ }
+ const btn = document.createElement('button');
+ btn.className = 'fs-ctx-btn';
+ btn.innerHTML = '
전체 보기';
+ btn.onclick = () => { fs3dShowAll(); ctxMenu.style.display = 'none'; };
+ ctxMenu.appendChild(btn);
+ hasItem = true;
+ }
+
+ if (!hasItem) return;
+
+ // 화면 밖으로 넘어가지 않도록 위치 조정
+ ctxMenu.style.display = 'block';
+ const mw = ctxMenu.offsetWidth, mh = ctxMenu.offsetHeight;
+ const mx = Math.min(e.clientX, window.innerWidth - mw - 8);
+ const my = Math.min(e.clientY, window.innerHeight - mh - 8);
+ ctxMenu.style.left = mx + 'px';
+ ctxMenu.style.top = my + 'px';
+ });
+
+ // 좌클릭/스크롤 시 메뉴 닫기
+ document.addEventListener('click', () => { ctxMenu.style.display = 'none'; });
+ renderer.domElement.addEventListener('wheel', () => { ctxMenu.style.display = 'none'; });
}
let fs3dCameraInit = false;
@@ -1455,10 +1551,15 @@ function fs3dBuild() {
scene.add(meshes.wall);
- // 재빌드 후 토글 상태 동기화 (새로 생성된 mesh에 visibility 적용)
- Object.keys(S.td.show).forEach(key => {
- if (meshes[key]) meshes[key].visible = S.td.show[key];
- });
+ // 재빌드 후 토글/단품 상태 동기화
+ if (fs3dIsolated) {
+ // 단품 보기 모드 유지
+ Object.entries(meshes).forEach(([k, obj]) => { if (obj) obj.visible = (k === fs3dIsolated); });
+ } else {
+ Object.keys(S.td.show).forEach(key => {
+ if (meshes[key]) meshes[key].visible = S.td.show[key];
+ });
+ }
// Camera: 최초 빌드 시에만 위치 설정, 이후 재빌드 시 현재 시점 유지
if (!fs3dCameraInit) {
@@ -1469,6 +1570,45 @@ function fs3dBuild() {
}
}
+ // 단품 보기 (Isolation)
+ function fs3dIsolate(key) {
+ fs3dIsolated = key;
+ Object.entries(meshes).forEach(([k, obj]) => {
+ if (obj) obj.visible = (k === key);
+ });
+ // 배지 표시
+ const badge = document.querySelector('.fs-iso-badge');
+ if (badge) {
+ const label = meshLabels[key] || key;
+ badge.innerHTML = `${label} 단품 보기 중 ✕`;
+ badge.style.display = 'block';
+ }
+ // 카메라를 객체 중심으로 포커스
+ const obj = meshes[key];
+ if (obj && controls) {
+ const box = new THREE.Box3().setFromObject(obj);
+ const center = box.getCenter(new THREE.Vector3());
+ const size = box.getSize(new THREE.Vector3());
+ const maxDim = Math.max(size.x, size.y, size.z);
+ controls.target.copy(center);
+ const dist = maxDim * 2.5;
+ camera.position.set(center.x + dist * 0.6, center.y + dist * 0.4, center.z + dist * 0.8);
+ controls.update();
+ }
+ }
+
+ // 전체 보기 (Show All)
+ function fs3dShowAll() {
+ fs3dIsolated = null;
+ Object.entries(meshes).forEach(([k, obj]) => {
+ if (!obj) return;
+ obj.visible = S.td.show[k] !== undefined ? S.td.show[k] : true;
+ });
+ // 배지 숨기기
+ const badge = document.querySelector('.fs-iso-badge');
+ if (badge) badge.style.display = 'none';
+ }
+
// 3D Controls
window.fs3dShutterPos = function(v) {
S.td.shutterPos = Number(v);