diff --git a/frontend/public/fire-alarm-demo/index.html b/frontend/public/fire-alarm-demo/index.html index 8733d0b8..411bd325 100644 --- a/frontend/public/fire-alarm-demo/index.html +++ b/frontend/public/fire-alarm-demo/index.html @@ -1,4 +1,4 @@ - + @@ -713,6 +713,29 @@ + + + + + + + + + + + + + + + + + + + + + + + @@ -777,7 +800,11 @@ .road-dash { fill: none; stroke: #d6ecff; stroke-width: 1.05; stroke-dasharray: 8 6; opacity: .58; } .route { fill: none; stroke: rgba(234,250,255,.65); stroke-width: .9; stroke-dasharray: 4 4; } - .building { fill: url(#building-bg); stroke: #f3fbff; stroke-width: 1.45; filter: url(#soft-shadow); } + .building { fill: url(#building-bg); stroke: #f3fbff; stroke-width: 1.45; filter: url(#soft-shadow); } + .building.bg-utility { fill: url(#building-bg-utility); } + .building.bg-service { fill: url(#building-bg-service); } + .building.bg-factory1 { fill: url(#building-bg-factory1); } + .building.bg-office { fill: url(#building-bg-office); } .building-inset { fill: none; stroke: rgba(255,255,255,.36); stroke-width: .9; } .room-core { fill: url(#core-room); stroke: rgba(255,255,255,.58); stroke-width: .8; } .wall { stroke: #edf9ff; stroke-width: 1; opacity: .66; } @@ -786,16 +813,20 @@ .detail-light { stroke: #dff4ff; stroke-width: .45; opacity: .31; } .hatch { stroke: #dff4ff; stroke-width: .55; opacity: .28; } - .zone-area { fill: rgba(27,146,63,.12); stroke: rgba(255,255,255,.26); stroke-width: .78; cursor: pointer; } + .zone-area { fill: rgba(27,146,63,.12); stroke: rgba(255,255,255,.26); stroke-width: .78; cursor: pointer; } + .zone-area.za-utility { fill: rgba(40,170,200,.16); } + .zone-area.za-service { fill: rgba(220,140,40,.18); } + .zone-area.za-factory1 { fill: rgba(150,90,210,.18); } + .zone-area.za-office { fill: rgba(140,160,190,.14); } .zone:hover .zone-area { fill: rgba(255,255,255,.075); } .zone.warn .zone-area { fill: rgba(255,207,78,.32); stroke: #ffe595; } .zone.alarm .zone-area { fill: rgba(255,56,82,.42); stroke: #ffffff; animation: alarmPulse .82s infinite; } .building-name { font-family: Arial, 'Noto Sans KR', sans-serif; font-size: 18px; font-weight: 900; fill: #ffffff; text-anchor: middle; paint-order: stroke; stroke: rgba(0,0,0,.58); stroke-width: 3px; } .building-sub { font-family: Arial, 'Noto Sans KR', sans-serif; font-size: 8px; font-weight: 800; fill: #e8fff1; text-anchor: middle; letter-spacing: .55px; } - .room-label { font-family: Arial, 'Noto Sans KR', sans-serif; font-size: 5.7px; font-weight: 700; fill: rgba(240,255,248,.82); text-transform: uppercase; } - .room-label.dim { fill: rgba(240,255,248,.54); } - .zone-label { font-family: Arial, 'Noto Sans KR', sans-serif; font-size: 10px; font-weight: 900; fill: #ffffff; text-anchor: middle; paint-order: stroke; stroke: rgba(0,0,0,.68); stroke-width: 3px; } + .room-label { font-family: Arial, 'Noto Sans KR', sans-serif; font-size: 7.5px; font-weight: 700; fill: rgba(240,255,248,.88); text-transform: uppercase; } + .room-label.dim { fill: rgba(240,255,248,.6); } + .zone-label { font-family: Arial, 'Noto Sans KR', sans-serif; font-size: 13.5px; font-weight: 900; fill: #ffffff; text-anchor: middle; paint-order: stroke; stroke: rgba(0,0,0,.78); stroke-width: 3.4px; } .small-note { font-family: Arial, 'Noto Sans KR', sans-serif; font-size: 5.5px; font-weight: 700; fill: rgba(237,252,255,.58); } .sensor-outer { fill: #0e1826; stroke: #ffffff; stroke-width: 1.4; } @@ -851,31 +882,31 @@ - - + + - WASTEWATER - - + WASTEWATER + + ZONE 01 - - + + - SBG TREATMENT - - + SBG TREATMENT + + ZONE 02 - + UTILITY BUILDING @@ -894,31 +925,31 @@ - - ZONE 03 - - + + ZONE 03 + + - - ZONE 04 - - + + ZONE 04 + + - - ZONE 05 - - + + ZONE 05 + + - - ZONE 06 - - + + ZONE 06 + + @@ -929,13 +960,13 @@ - - + + - CAFETERIA - - + CAFETERIA + + ZONE 07 @@ -943,13 +974,13 @@ - - + + - SAFETY CENTER - - + SAFETY CENTER + + ZONE 08 @@ -1007,58 +1038,58 @@ - ZONE 09 - - + ZONE 09 + + - ZONE 10 - - + ZONE 10 + + - ZONE 11 - - + ZONE 11 + + - ZONE 12 - - + ZONE 12 + + - ZONE 13 - - + ZONE 13 + + - ZONE 14 - - + ZONE 14 + + - ZONE 15 - - + ZONE 15 + + - ZONE 16 - - + ZONE 16 + + @@ -1069,23 +1100,23 @@ - + - OFFICE BLDG + OFFICE BLDG - - ZONE 17 - - + + ZONE 17 + + - - ZONE 18 - - + + ZONE 18 + + @@ -1093,7 +1124,7 @@ - + FACTORY 1 @@ -1118,31 +1149,31 @@ - - ZONE 19 - - + + ZONE 19 + + - - ZONE 20 - - + + ZONE 20 + + - - ZONE 21 - - + + ZONE 21 + + - - ZONE 22 - - + + ZONE 22 + + diff --git a/frontend/public/scada-demo/index.html b/frontend/public/scada-demo/index.html index 051ecdb0..9ddc1724 100644 --- a/frontend/public/scada-demo/index.html +++ b/frontend/public/scada-demo/index.html @@ -30,7 +30,7 @@
- + @@ -380,6 +380,7 @@ + diff --git a/frontend/public/scada-demo/js/components.js b/frontend/public/scada-demo/js/components.js index d1f88488..b928a499 100644 --- a/frontend/public/scada-demo/js/components.js +++ b/frontend/public/scada-demo/js/components.js @@ -43,7 +43,7 @@ }); return ` - ${label} + ${label} ${minorTicks} ${ticks} ${nozzleSVG} @@ -64,7 +64,7 @@ const w = 50, h = 70; return ` - ${label} + ${label} @@ -83,7 +83,7 @@ const bodyH = h - coneH - skirtH; return ` - ${label} + ${label} @@ -101,7 +101,7 @@ const { id, x, y, label = '', accent = '#7cff3a' } = p; return ` - ${label ? `${label}` : ''} + ${label ? `${label}` : ''} @@ -127,7 +127,7 @@ const { id, x, y, label = '' } = p; return ` - ${label ? `${label}` : ''} + ${label ? `${label}` : ''} @@ -154,7 +154,7 @@ const { id, x, y, label = 'AIR BLOWER' } = p; return ` - ${label} + ${label} @@ -304,8 +304,8 @@ COMP.pressureGauge = function (p) { const { id, x, y, label = 'P', max = 8, unit = 'bar' } = p; return ` - - ${label ? `${label}` : ''} + + ${label ? `${label}` : ''} @@ -319,29 +319,29 @@ - PRESS - ${unit} + PRESS + ${unit} - - 0.0 + + 0.0 `; }; // ===== SENSOR READOUT ===== COMP.sensor = function (p) { const { id, x, y, label = 'SENSOR', value = 0, unit = '' } = p; - const w = 110, h = 36; + const w = 130, h = 52; return ` - ${label} - ${value} - ${unit} + ${label} + ${value} + ${unit} `; }; @@ -369,7 +369,7 @@ const midY = h / 2; return ` - ${label ? `${label}` : ''} + ${label ? `${label}` : ''} @@ -469,7 +469,7 @@ `; return ` - ${label} + ${label} ▶ STANDBY @@ -506,7 +506,7 @@ } return ` - ${label} + ${label} ${cells} @@ -533,7 +533,7 @@ } return ` - ${label} + ${label} ${cells} `; @@ -541,13 +541,13 @@ // ===== STATUS TILE (KPI mini panel) ===== COMP.statusTile = function (p) { - const { id, x, y, label = 'TILE', value = '--', unit = '', w = 130, h = 50 } = p; + const { id, x, y, label = 'TILE', value = '--', unit = '', w = 150, h = 64 } = p; return ` - ${label} - ${value} - ${unit} + ${label} + ${value} + ${unit} `; }; diff --git a/frontend/public/scada-demo/js/dev-drag.js b/frontend/public/scada-demo/js/dev-drag.js new file mode 100644 index 00000000..0aa9e410 --- /dev/null +++ b/frontend/public/scada-demo/js/dev-drag.js @@ -0,0 +1,427 @@ +// SCADA 데모 — 컴포넌트 드래그 좌표 인스펙터 (개발용) +// 사용법: Shift+D 로 드래그 모드 토글. 컴포넌트 잡고 옮기면 새 좌표가 화면 + 콘솔 + 클립보드. +// 좌표 자동 적용은 안 함 — 사용자가 보고 코드(topology.js)에 반영. + +(function () { + let DRAG_MODE = false; + let dragging = null; + let svg = null; + + window.addEventListener('keydown', (e) => { + // e.code 는 키보드 레이아웃/한영 입력기와 무관하게 동작 + if (e.shiftKey && e.code === 'KeyD') { + e.preventDefault(); + DRAG_MODE = !DRAG_MODE; + updateUI(); + console.log('[DRAG] mode:', DRAG_MODE ? 'ON' : 'OFF'); + } + if (e.code === 'Escape' && dragging) { + dragging.el.setAttribute('transform', dragging.origTransform); + dragging.el.style.opacity = ''; + dragging = null; + } + }, true); + + function updateUI() { + let ind = document.getElementById('drag-mode-indicator'); + if (DRAG_MODE) { + if (!ind) { + ind = document.createElement('div'); + ind.id = 'drag-mode-indicator'; + ind.style.cssText = `position:fixed;top:10px;left:50%;transform:translateX(-50%); + background:#d63031;color:#fff;padding:6px 16px;border-radius:4px; + font:700 13px/1.2 system-ui;z-index:9999;box-shadow:0 2px 8px rgba(0,0,0,0.4);`; + ind.textContent = 'DRAG MODE — 컴포넌트 잡고 옮기기 (Shift+D 끄기 / Esc 취소)'; + document.body.appendChild(ind); + } + svg.style.cursor = 'grab'; + drawPipeHandles(); + } else { + if (ind) ind.remove(); + svg.style.cursor = ''; + removePipeHandles(); + } + } + + // 파이프 핸들 (via 점 + 절대 to 끝점) + function drawPipeHandles() { + if (!window.INVYONE_TOPO) return; + removePipeHandles(); + const sceneContent = document.getElementById('scene-content'); + if (!sceneContent) return; + const NS = 'http://www.w3.org/2000/svg'; + const layer = document.createElementNS(NS, 'g'); + layer.setAttribute('id', 'pipe-handles-layer'); + + let ports; + try { ports = window.INVYONE_TOPO.computePorts(); } catch { ports = {}; } + + (window.INVYONE_TOPO.PIPES || []).forEach(pipe => { + // 1) 절대 to 끝점 + if (pipe.to && typeof pipe.to === 'object' && typeof pipe.to.x === 'number' && typeof pipe.to.y === 'number') { + layer.appendChild(makeHandle(pipe.to.x, pipe.to.y, '#ff8a3a', { pipeId: pipe.id, kind: 'to-abs' })); + } + // 2) via 점들 — via 가 있으면 path 의 corner 가 via 좌표에 정확히 떨어짐 + if (Array.isArray(pipe.via)) { + pipe.via.forEach((v, i) => { + // via.x 만 있으면 핸들 위치는 (v.x, 시작점 y 또는 다음 corner y) 추정 + // via.y 만 있으면 핸들 위치는 (시작점 x, v.y) 추정 + const fromXY = portXY(pipe.from, ports); + const toXY = (typeof pipe.to === 'string') ? portXY(pipe.to, ports) : + (pipe.to && typeof pipe.to.x === 'number') ? { x: pipe.to.x, y: pipe.to.y } : null; + let hx, hy; + if (typeof v.x === 'number' && typeof v.y === 'number') { + hx = v.x; hy = v.y; + } else if (typeof v.x === 'number') { + hx = v.x; + hy = fromXY ? fromXY.y : (toXY ? toXY.y : 0); + } else if (typeof v.y === 'number') { + hy = v.y; + hx = fromXY ? fromXY.x : (toXY ? toXY.x : 0); + } + if (hx == null || hy == null) return; + layer.appendChild(makeHandle(hx, hy, '#5af9ff', { pipeId: pipe.id, kind: 'via', viaIdx: i })); + }); + } + }); + + sceneContent.appendChild(layer); + } + + function makeHandle(x, y, color, data) { + const NS = 'http://www.w3.org/2000/svg'; + const c = document.createElementNS(NS, 'circle'); + c.setAttribute('cx', x); + c.setAttribute('cy', y); + c.setAttribute('r', 7); + c.setAttribute('fill', color); + c.setAttribute('stroke', '#fff'); + c.setAttribute('stroke-width', 2); + c.setAttribute('class', 'pipe-handle'); + c.style.cursor = 'grab'; + c.style.opacity = '0.85'; + Object.entries(data).forEach(([k, v]) => c.dataset[k] = v); + return c; + } + + function removePipeHandles() { + const old = document.getElementById('pipe-handles-layer'); + if (old) old.remove(); + } + + function portXY(ref, ports) { + if (!ref || typeof ref !== 'string') return null; + const [compId, portName] = ref.split('.'); + return ports[compId] && ports[compId][portName] ? ports[compId][portName] : null; + } + + function toSvg(clientX, clientY) { + const pt = svg.createSVGPoint(); + pt.x = clientX; + pt.y = clientY; + return pt.matrixTransform(svg.getScreenCTM().inverse()); + } + + function parseTranslate(transform) { + const m = (transform || '').match(/translate\(([^)]+)\)/); + if (!m) return { x: 0, y: 0 }; + const parts = m[1].split(/[ ,]+/).map(Number); + return { x: parts[0] || 0, y: parts[1] || 0 }; + } + + function onMouseDown(e) { + if (!DRAG_MODE) return; + if (!svg.contains(e.target)) return; + + // 1) 파이프 핸들 우선 처리 + if (e.target.classList && e.target.classList.contains('pipe-handle')) { + e.preventDefault(); + e.stopPropagation(); + const sp = toSvg(e.clientX, e.clientY); + dragging = { + isHandle: true, + el: e.target, + pipeId: e.target.dataset.pipeId, + kind: e.target.dataset.kind, + viaIdx: e.target.dataset.viaIdx ? parseInt(e.target.dataset.viaIdx, 10) : null, + startMouseX: sp.x, + startMouseY: sp.y, + origCx: parseFloat(e.target.getAttribute('cx')), + origCy: parseFloat(e.target.getAttribute('cy')), + }; + e.target.style.cursor = 'grabbing'; + return; + } + + const compEl = e.target.closest('.comp'); + if (!compEl) { + console.log('[DRAG] mousedown target:', e.target.tagName, e.target.getAttribute && e.target.getAttribute('class'), '— .comp 못 찾음'); + return; + } + console.log('[DRAG] grabbed:', compEl.getAttribute('data-comp-id')); + e.preventDefault(); + e.stopPropagation(); + + const sp = toSvg(e.clientX, e.clientY); + const tr = compEl.getAttribute('transform') || ''; + const orig = parseTranslate(tr); + + dragging = { + el: compEl, + compId: compEl.getAttribute('data-comp-id'), + compType: compEl.getAttribute('data-comp-type'), + origTransform: tr, + origX: orig.x, + origY: orig.y, + startMouseX: sp.x, + startMouseY: sp.y, + }; + compEl.style.opacity = '0.65'; + } + + function onMouseMove(e) { + if (!dragging) return; + e.preventDefault(); + const sp = toSvg(e.clientX, e.clientY); + const dx = sp.x - dragging.startMouseX; + const dy = sp.y - dragging.startMouseY; + if (dragging.isHandle) { + const newCx = dragging.origCx + dx; + const newCy = dragging.origCy + dy; + dragging.el.setAttribute('cx', newCx.toFixed(1)); + dragging.el.setAttribute('cy', newCy.toFixed(1)); + return; + } + const newX = dragging.origX + dx; + const newY = dragging.origY + dy; + const newT = dragging.origTransform.replace( + /translate\([^)]+\)/, + `translate(${newX.toFixed(1)} ${newY.toFixed(1)})` + ); + dragging.el.setAttribute('transform', newT); + } + + function onMouseUp(e) { + if (!dragging) return; + e.preventDefault(); + const sp = toSvg(e.clientX, e.clientY); + const dx = sp.x - dragging.startMouseX; + const dy = sp.y - dragging.startMouseY; + + if (dragging.isHandle) { + const newX = dragging.origCx + dx; + const newY = dragging.origCy + dy; + const moved = (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5); + if (moved) applyHandleToTopology(dragging.pipeId, dragging.kind, dragging.viaIdx, newX, newY); + showHandleResult(dragging.pipeId, dragging.kind, dragging.viaIdx, newX, newY); + dragging.el.style.cursor = 'grab'; + dragging = null; + if (moved) { + rebuildScene(); + drawPipeHandles(); + } + return; + } + + showResult(dragging.compId, dragging.compType, dx, dy, dragging.origX, dragging.origY); + if (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5) { + const applied = applyToTopology(dragging.compId, dx, dy); + if (applied) { + rebuildScene(); + drawPipeHandles(); + } + } + + dragging.el.style.opacity = ''; + dragging = null; + } + + function applyHandleToTopology(pipeId, kind, viaIdx, newX, newY) { + const PIPES = (window.INVYONE_TOPO && window.INVYONE_TOPO.PIPES) || []; + const pipe = PIPES.find(p => p.id === pipeId); + if (!pipe) return; + if (kind === 'to-abs' && pipe.to && typeof pipe.to === 'object') { + pipe.to.x = Math.round(newX); + pipe.to.y = Math.round(newY); + } else if (kind === 'via' && Array.isArray(pipe.via) && pipe.via[viaIdx]) { + const v = pipe.via[viaIdx]; + if (typeof v.x === 'number') v.x = Math.round(newX); + if (typeof v.y === 'number') v.y = Math.round(newY); + } + } + + function showHandleResult(pipeId, kind, viaIdx, x, y) { + let msg; + if (kind === 'to-abs') { + msg = `▶ ${pipeId} (pipe to)\n새 끝점: { x: ${Math.round(x)}, y: ${Math.round(y)} }`; + } else { + const v = window.INVYONE_TOPO.PIPES.find(p => p.id === pipeId).via[viaIdx]; + const parts = []; + if (typeof v.x === 'number') parts.push(`x: ${Math.round(x)}`); + if (typeof v.y === 'number') parts.push(`y: ${Math.round(y)}`); + msg = `▶ ${pipeId} (pipe via[${viaIdx}])\n새 좌표: { ${parts.join(', ')} }`; + } + console.log('%c[DRAG]\n' + msg, 'color:#ff8a3a;font-weight:700;'); + appendToPanel(msg, '#ff8a3a'); + } + + // 결과 누적 패널 — 모든 변경 사항이 한 곳에 쌓임 + const HISTORY = []; + function appendToPanel(msg, color) { + HISTORY.push({ time: new Date().toLocaleTimeString('ko-KR'), msg, color }); + let panel = document.getElementById('drag-result-panel'); + if (!panel) { + panel = document.createElement('div'); + panel.id = 'drag-result-panel'; + panel.style.cssText = `position:fixed;top:60px;right:10px;background:#11102a; + color:#fff;padding:0;border-radius:4px;border:1px solid #5af9ff; + font:700 11px/1.5 monospace;z-index:9999;width:380px;max-height:80vh; + display:flex;flex-direction:column;box-shadow:0 2px 10px rgba(0,0,0,0.5);`; + const header = document.createElement('div'); + header.style.cssText = `padding:8px 12px;background:#1a1936;border-bottom:1px solid #5af9ff; + display:flex;justify-content:space-between;align-items:center;flex-shrink:0;`; + header.innerHTML = `📋 변경 기록 (0) + + + `; + const body = document.createElement('div'); + body.id = 'drag-history-body'; + body.style.cssText = `padding:8px 12px;overflow-y:auto;flex:1;white-space:pre-wrap;`; + panel.appendChild(header); + panel.appendChild(body); + document.body.appendChild(panel); + document.getElementById('drag-copy-all').addEventListener('click', () => { + const all = HISTORY.map(h => h.msg).join('\n\n'); + if (navigator.clipboard) navigator.clipboard.writeText(all).then(() => { + const btn = document.getElementById('drag-copy-all'); + const orig = btn.textContent; + btn.textContent = '복사됨!'; + setTimeout(() => { btn.textContent = orig; }, 1200); + }); + }); + document.getElementById('drag-clear').addEventListener('click', () => { + HISTORY.length = 0; + renderHistory(); + }); + document.getElementById('drag-close').addEventListener('click', () => panel.remove()); + } + renderHistory(); + if (navigator.clipboard) navigator.clipboard.writeText(msg).catch(() => {}); + } + + function renderHistory() { + const body = document.getElementById('drag-history-body'); + const count = document.getElementById('drag-history-count'); + if (!body) return; + count.textContent = HISTORY.length; + body.innerHTML = HISTORY.map((h, i) => { + return `
+
#${i + 1} · ${h.time}
+
${h.msg.replace(/ +
`; + }).join(''); + body.scrollTop = body.scrollHeight; + } + + function applyToTopology(compId, dx, dy) { + const meta = findCompMeta(compId); + if (!meta) return false; + const c = meta.comp; + if (c.anchor && c.anchor.absolute) { + c.anchor.absolute.x = (c.anchor.absolute.x || 0) + dx; + c.anchor.absolute.y = (c.anchor.absolute.y || 0) + dy; + return true; + } + if (c.anchor && !c.anchor.absolute) { + c.anchor.dx = (c.anchor.dx ?? 0) + dx; + c.anchor.dy = (c.anchor.dy ?? 0) + dy; + return true; + } + if (typeof c.x === 'number' && typeof c.y === 'number') { + c.x += dx; + c.y += dy; + return true; + } + return false; + } + + function rebuildScene() { + const sceneEl = document.getElementById('scene-content'); + if (!sceneEl || !window.INVYONE_TOPO) return; + // anchor 기반 컴포넌트의 _resolved 캐시 리셋 (dx/dy/at 등 변경 사항 재계산되도록) + const T = window.INVYONE_TOPO; + ['TANKS','PUMPS','VALVES','FILTERS','MODULES','GAUGES','SENSORS','LEDS'].forEach(b => { + (T[b] || []).forEach(c => { + if (c.anchor) { + c._resolved = false; + if (!c.anchor.absolute) { c.x = undefined; c.y = undefined; } + } + }); + }); + sceneEl.innerHTML = window.INVYONE_TOPO.buildScene(); + if (window.INVYONE_ENGINE && typeof window.INVYONE_ENGINE.init === 'function') { + try { window.INVYONE_ENGINE.init(); } catch (err) { console.warn('[DRAG] engine.init re-call failed:', err); } + } + } + + function findCompMeta(compId) { + if (!window.INVYONE_TOPO) return null; + const T = window.INVYONE_TOPO; + const buckets = ['TANKS', 'PUMPS', 'GAUGES', 'SENSORS', 'FILTERS', 'MODULES', 'LEDS', 'VALVES', 'TILES', 'PANELS']; + for (const b of buckets) { + const arr = T[b] || []; + const c = arr.find((x) => x.id === compId); + if (c) return { bucket: b, comp: c }; + } + return null; + } + + function showResult(compId, compType, dx, dy, origX, origY) { + const meta = findCompMeta(compId); + const isAnchor = !!(meta && meta.comp && meta.comp.anchor && !meta.comp.anchor.absolute); + + // 게이지는 transform 안에 (x-20, y-20) 적용돼 있으니 anchor 절대 좌표는 +20 보정 + const isGauge = compType === 'gauge'; + const baseX = isGauge ? origX + 20 : origX; + const baseY = isGauge ? origY + 20 : origY; + const newAnchorX = baseX + dx; + const newAnchorY = baseY + dy; + + const dxStr = (dx >= 0 ? '+' : '') + dx.toFixed(1); + const dyStr = (dy >= 0 ? '+' : '') + dy.toFixed(1); + + let lines = [`▶ ${compId} (${compType || '-'})`, `이동량: dx=${dxStr}, dy=${dyStr}`]; + + if (isAnchor) { + const a = meta.comp.anchor; + const oldDx = a.dx ?? 0; + const oldDy = a.dy ?? 0; + lines.push(`anchor 기반 → 기존 dx:${oldDx}, dy:${oldDy}`); + lines.push(`수정 후: dx:${(oldDx + dx).toFixed(1)}, dy:${(oldDy + dy).toFixed(1)}`); + } else { + lines.push(`절대 좌표 → 새 위치: x=${newAnchorX.toFixed(0)}, y=${newAnchorY.toFixed(0)}`); + } + + const msg = lines.join('\n'); + console.log('%c[DRAG]\n' + msg, 'color:#5af9ff;font-weight:700;'); + appendToPanel(msg, '#5af9ff'); + } + + function init() { + svg = document.getElementById('scene'); + if (!svg) { + setTimeout(init, 100); + return; + } + window.addEventListener('mousedown', onMouseDown, true); + window.addEventListener('mousemove', onMouseMove, true); + window.addEventListener('mouseup', onMouseUp, true); + console.log('%c[DEV] Drag mode 사용 가능: Shift+D 로 토글', 'color:#5af9ff;font-weight:700;'); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); diff --git a/frontend/public/scada-demo/js/topology.js b/frontend/public/scada-demo/js/topology.js index 59e26096..077127d6 100644 --- a/frontend/public/scada-demo/js/topology.js +++ b/frontend/public/scada-demo/js/topology.js @@ -5,14 +5,14 @@ (function (global) { // ===== TANKS ===== const TANKS = [ - { id: 'sand', x: 340, y: 120, w: 96, h: 150, type: 'ac', label: 'SAND TANK', capacity: 5000, level: 62.0, color: '#5af9ff', alarms: { hh: 95, hl: 85, ll: 15, lh: 5 } }, - { id: 'ac', x: 500, y: 120, w: 96, h: 150, type: 'ac', label: 'AC TANK', capacity: 5000, level: 58.4, color: '#5af9ff', alarms: { hh: 95, hl: 85, ll: 15, lh: 5 } }, - { id: 'raw', x: 660, y: 90, w: 120, h: 230, type: 'tank', label: 'RAW WATER TANK', capacity: 30000, level: 75.0, color: '#3aa3ff', alarms: { hh: 95, hl: 85, ll: 20, lh: 10 } }, - { id: 'chem', x: 895, y: 110, w: 50, h: 70, type: 'dosing', label: 'Chem', capacity: 500, level: 71.0, color: '#7cff3a', alarms: { hh: 95, hl: 85, ll: 15, lh: 5 } }, - { id: 'water-filter', x: 1600, y: 90, w: 120, h: 230, type: 'tank', label: 'WATER FILTER TANK', capacity: 25000, level: 48.0, color: '#3aa3ff', alarms: { hh: 95, hl: 85, ll: 20, lh: 10 }, nozzles: [{ side: 'left', pos: 0.30 }, { side: 'bot', pos: 0.5 }] }, - { id: 'chem-cep-1', x: 80, y: 545, w: 50, h: 70, type: 'dosing', label: 'CEP-1', capacity: 500, level: 64.2, color: '#ff8a3a', alarms: { hh: 95, hl: 85, ll: 15, lh: 5 } }, - { id: 'chem-cep-2', x: 150, y: 545, w: 50, h: 70, type: 'dosing', label: 'CEP-2', capacity: 500, level: 55.8, color: '#ff4f9a', alarms: { hh: 95, hl: 85, ll: 15, lh: 5 } }, - { id: 'cip-tank', x: 80, y: 765, w: 60, h: 80, type: 'dosing', label: 'CIP TANK', capacity: 1500, level: 80.5, color: '#5af9ff', alarms: { hh: 95, hl: 85, ll: 15, lh: 5 } }, + { id: 'sand', x: 340, y: 90, w: 96, h: 150, type: 'ac', label: 'SAND TANK', capacity: 5000, level: 62.0, color: '#5af9ff', alarms: { hh: 95, hl: 85, ll: 15, lh: 5 } }, + { id: 'ac', x: 500, y: 90, w: 96, h: 150, type: 'ac', label: 'AC TANK', capacity: 5000, level: 58.4, color: '#5af9ff', alarms: { hh: 95, hl: 85, ll: 15, lh: 5 } }, + { id: 'raw', x: 661, y: 51, w: 120, h: 230, type: 'tank', label: 'RAW WATER TANK', capacity: 30000, level: 75.0, color: '#3aa3ff', alarms: { hh: 95, hl: 85, ll: 20, lh: 10 } }, + { id: 'chem', x: 895, y: 80, w: 50, h: 70, type: 'dosing', label: 'Chem', capacity: 500, level: 71.0, color: '#7cff3a', alarms: { hh: 95, hl: 85, ll: 15, lh: 5 } }, + { id: 'water-filter', x: 1600, y: 60, w: 120, h: 230, type: 'tank', label: 'WATER FILTER TANK', capacity: 25000, level: 48.0, color: '#3aa3ff', alarms: { hh: 95, hl: 85, ll: 20, lh: 10 }, nozzles: [{ side: 'left', pos: 0.30 }, { side: 'bot', pos: 0.5 }] }, + { id: 'chem-cep-1', x: 60, y: 545, w: 50, h: 70, type: 'dosing', label: 'CEP-1', capacity: 500, level: 64.2, color: '#ff8a3a', alarms: { hh: 95, hl: 85, ll: 15, lh: 5 } }, + { id: 'chem-cep-2', x: 130, y: 545, w: 50, h: 70, type: 'dosing', label: 'CEP-2', capacity: 500, level: 55.8, color: '#ff4f9a', alarms: { hh: 95, hl: 85, ll: 15, lh: 5 } }, + { id: 'cip-tank', x: 80, y: 845, w: 60, h: 80, type: 'dosing', label: 'CIP TANK', capacity: 1500, level: 80.5, color: '#5af9ff', alarms: { hh: 95, hl: 85, ll: 15, lh: 5 } }, { id: 'mbr', x: 760, y: 600, w: 500, h: 200, type: 'mbr', label: 'MBR TANK', capacity: 40000, level: 52.0, color: '#ffd682', alarms: { hh: 92, hl: 80, ll: 20, lh: 10 } }, { id: 'dp', x: 1320, y: 545, w: 90, h: 180, type: 'tank', label: 'DP TANK', capacity: 8000, level: 87.0, color: '#3aa3ff', alarms: { hh: 95, hl: 85, ll: 20, lh: 10 } }, { id: 'regulating', x: 1580, y: 640, w: 100, h: 180, type: 'tank', label: 'REGULATING TANK', capacity: 12000, level: 54.06, color: '#ff8a3a', alarms: { hh: 95, hl: 85, ll: 20, lh: 10 } }, @@ -26,7 +26,7 @@ ]; const MODULES = [ - { id: 'uf-system', anchor: { to: 'uf-filter.right', myPort: 'topLeft', dx: 62, dy: -122 }, type: 'membrane', count: 5, label: 'UF SYSTEM', accent: '#5af9ff', orientation: 'vertical' }, + { id: 'uf-system', anchor: { to: 'uf-filter.right', myPort: 'topLeft', dx: 82, dy: -87 }, type: 'membrane', count: 5, label: 'UF SYSTEM', accent: '#5af9ff', orientation: 'vertical' }, { id: 'ro-system', anchor: { to: 'ro-press.out', myPort: 'in', dx: 55, dy: -47 }, type: 'membrane', count: 5, label: 'RO SYSTEM', accent: '#5af9ff', orientation: 'horizontal' }, { id: 'mbr-carb-l', anchor: { to: 'mbr.top', myPort: 'topLeft', dx: -183, dy: 106 }, type: 'carbon', count: 5, label: 'CARBON L', accent: '#5af9ff' }, { id: 'mbr-carb-r', anchor: { to: 'mbr.top', myPort: 'topLeft', dx: 77, dy: 106 }, type: 'carbon', count: 5, label: 'CARBON R', accent: '#5af9ff' }, @@ -34,8 +34,8 @@ const PUMPS = [ // Root pumps (no natural upstream tank — keep absolute) - { id: 'bw-a1', x: 78, y: 220, label: 'FEED-A', source: null, dest: 'sand', lpm: 80, valves: ['v-input'], pipes: ['p-input-bwa1', 'p-bwa1-pf', 'p-pf-sand'] }, - { id: 'air-blower', x: 380, y: 785, label: 'AIR BLOWER', source: null, dest: 'mbr', lpm: 0, valves: [], pipes: ['p-blower-mbr'] }, + { id: 'bw-a1', x: 78, y: 190, label: 'FEED-A', source: null, dest: 'sand', lpm: 80, valves: ['v-input'], pipes: ['p-input-bwa1', 'p-bwa1-pf', 'p-pf-sand'] }, + { id: 'air-blower', x: 380, y: 865, label: 'AIR BLOWER', source: null, dest: 'mbr', lpm: 0, valves: [], pipes: ['p-blower-mbr'] }, // Tank-foot pumps (anchor to tank bottom, my top inlet aligns) { id: 'sand-out', anchor: { to: 'sand.bot', myPort: 'in', dy: 14 }, label: 'SAND PUMP', source: 'sand', dest: 'ac', lpm: 80, valves: ['v-sand-out'], pipes: ['p-sand-down', 'p-sand-ac'], orient: 'v' }, @@ -61,11 +61,11 @@ // All valves placed on their gating pipe — onPipe + at fraction { id: 'v-input', anchor: { onPipe: 'p-bwa1-pf', at: 0.30 }, label: 'V-IN', open: false }, { id: 'v-sand-out', anchor: { onPipe: 'p-sand-ac', at: 0.55 }, label: '', open: false }, - { id: 'v-ac-out', anchor: { onPipe: 'p-ac-raw', at: 0.78 }, label: '', open: false }, + { id: 'v-ac-out', anchor: { onPipe: 'p-ac-raw', at: 0.78, dx: -46, dy: -1 }, label: '', open: false }, { id: 'v-raw-out', anchor: { onPipe: 'p-raw-bw2', at: 0.78 }, label: '', open: false }, { id: 'v-chem-uf', anchor: { onPipe: 'p-chem-uf', at: 0.5 }, label: '', open: false }, { id: 'v-uf-in', anchor: { onPipe: 'p-bw2-uf', at: 0.75 }, label: 'V-UF-IN', open: false }, - { id: 'v-uf-out', anchor: { onPipe: 'p-uf-wf', at: 0.20 }, label: 'V-UF-OUT', open: false }, + { id: 'v-uf-out', anchor: { onPipe: 'p-uf-wf', at: 0.20, dx: -24, dy: 0 }, label: 'V-UF-OUT', open: false }, { id: 'v-ro-in', anchor: { onPipe: 'p-rofeed-filter', at: 0.5 }, label: '', open: false }, { id: 'v-ro-press-out', anchor: { onPipe: 'p-ropress-ro', at: 0.4 }, label: '', open: false }, { id: 'v-cip-out', anchor: { onPipe: 'p-cip-ro', at: 0.4 }, label: '', open: false }, @@ -433,18 +433,18 @@ { id: 'p-bwa1-pf', from: 'bw-a1.out', to: 'pump-filter.left' }, { id: 'p-pf-sand', from: 'pump-filter.right', to: 'sand.left', via: [{ y: 200 }] }, { id: 'p-sand-down', from: 'sand.bot', to: 'sand-out.top' }, - { id: 'p-sand-ac', from: 'sand-out.out', to: 'ac-out.in', via: [{ y: 290 }] }, + { id: 'p-sand-ac', from: 'sand-out.out', to: 'ac-out.in', via: [{ y: 279 }] }, { id: 'p-ac-down', from: 'ac.bot', to: 'ac-out.top' }, { id: 'p-ac-raw', from: 'ac-out.out', to: 'raw.top', via: [{ x: 605 }, { y: 90 }] }, - { id: 'p-raw-bw2', from: 'raw.right', to: 'bw-a2.in', via: [{ y: 250 }] }, + { id: 'p-raw-bw2', from: 'raw.right', to: 'bw-a2.in', via: [{ y: 211 }] }, { id: 'p-bw2-uf', from: 'bw-a2.out', to: 'uf-pump-a.in' }, { id: 'p-chem-uf', from: 'chem.bot', to: 'chem-uf.in', hint: 'v-first' }, - { id: 'p-chemuf-out', from: 'chem-uf.out', to: { x: 970, y: 250 } }, - { id: 'p-raw-uf', from: 'raw.rightLow', to: 'uf-pump-b.in', via: [{ x: 990 }] }, + { id: 'p-chemuf-out', from: 'chem-uf.out', to: { x: 980, y: 209 } }, + { id: 'p-raw-uf', from: 'raw.rightLow', to: 'uf-pump-b.in', via: [{ x: 999 }] }, { id: 'p-uf-filter', from: 'uf-pump-a.out', to: 'uf-filter.left' }, { id: 'p-ufpb-filter',from: 'uf-pump-b.out', to: 'uf-filter.left' }, { id: 'p-filter-uf', from: 'uf-filter.right', to: 'uf-system.topLeft' }, - { id: 'p-uf-wf', from: 'uf-system.botRight', to: 'water-filter.leftHigh', via: [{ x: 1490 }, { y: 159 }] }, + { id: 'p-uf-wf', from: 'uf-system.botRight', to: 'water-filter.leftHigh', via: [{ x: 1490 }, { y: 130 }] }, // Bottom row — RO / MBR / DP / REG { id: 'p-wf-ro', from: 'water-filter.bot', to: 'ro-feed.in', via: [{ y: 420 }, { x: 200 }, { y: 660 }] }, @@ -452,13 +452,13 @@ { id: 'p-filter-ropress',from: 'ro-filter.right', to: 'ro-press.in' }, { id: 'p-ropress-ro', from: 'ro-press.out', to: 'ro-system.in' }, { id: 'p-ro-mbr', from: 'ro-system.out', to: 'mbr.leftHigh' }, - { id: 'p-cip-ro', from: 'cip-pump.out', to: 'ro-system.bot', via: [{ y: 730 }] }, - { id: 'p-ciptank-pump', from: 'cip-tank.bot', to: 'cip-pump.in', via: [{ y: 820 }] }, + { id: 'p-cip-ro', from: 'cip-pump.out', to: 'ro-system.bot', via: [{ y: 834 }] }, + { id: 'p-ciptank-pump', from: 'cip-tank.bot', to: 'cip-pump.in', via: [{ y: 900 }] }, { id: 'p-blower-mbr', from: 'air-blower.out', to: 'mbr.bot', via: [{ x: 700 }] }, // MBR → AERATION DRAIN → DP TANK { id: 'p-mbr-aer', from: 'mbr.botRight', to: 'aer-drain.in' }, - { id: 'p-aer-dp', from: 'aer-drain.out', to: 'dp.bot', via: [{ x: 1290 }, { y: 760 }, { x: 1365 }] }, + { id: 'p-aer-dp', from: 'aer-drain.out', to: 'dp.bot', via: [{ x: 1289 }, { y: 760 }, { x: 1365 }] }, // DP TANK → DP PUMP → REGULATING { id: 'p-dp-down', from: 'dp.bot', to: 'dp-out.in' }, @@ -468,23 +468,23 @@ const GAUGES = [ { id: 'p-in-1', anchor: { to: 'bw-a1.top', myPort: 'center', dx: -23, dy: -110 }, label: 'P-IN', max: 8, unit: 'bar', source: 'bw-a1' }, { id: 'p-in-2', anchor: { to: 'bw-a1.top', myPort: 'center', dx: 87, dy: -110 }, label: 'P-PF', max: 8, unit: 'bar', source: 'bw-a1' }, - { id: 'p-raw', anchor: { to: 'raw.bot', myPort: 'center', dx: -35, dy: 60 }, label: 'P-RAW', max: 8, unit: 'bar', source: 'bw-a2' }, - { id: 'p-uf-1', anchor: { to: 'uf-pump-a.bot', myPort: 'center', dx: 210, dy: 75 }, label: 'P-UF-IN', max: 10, unit: 'bar', source: 'uf-pump-a' }, - { id: 'p-uf-2', anchor: { to: 'uf-pump-a.bot', myPort: 'center', dx: 515, dy: 75 }, label: 'P-UF-OUT', max: 10, unit: 'bar', source: 'uf-pump-a' }, + { id: 'p-raw', anchor: { to: 'raw.bot', myPort: 'center', dx: -31, dy: 64 }, label: 'P-RAW', max: 8, unit: 'bar', source: 'bw-a2' }, + { id: 'p-uf-1', anchor: { to: 'uf-pump-a.bot', myPort: 'center', dx: 218, dy: 100 }, label: 'P-UF-IN', max: 10, unit: 'bar', source: 'uf-pump-a' }, + { id: 'p-uf-2', anchor: { to: 'uf-pump-a.bot', myPort: 'center', dx: 497, dy: 100 }, label: 'P-UF-OUT', max: 10, unit: 'bar', source: 'uf-pump-a' }, { id: 'p-wf', anchor: { to: 'water-filter.right', myPort: 'center', dx: 90, dy: -45 }, label: 'P-WF', max: 8, unit: 'bar', source: 'bw-a2' }, - { id: 'p-cep-1', anchor: { to: 'chem-cep-1.bot', myPort: 'center', dx: -15, dy: 65 }, label: 'P1', max: 8, unit: 'bar', source: 'ro-feed' }, - { id: 'p-cep-2', anchor: { to: 'chem-cep-2.bot', myPort: 'center', dx: -5, dy: 65 }, label: 'P2', max: 8, unit: 'bar', source: 'ro-feed' }, - { id: 'p-ro-in', anchor: { to: 'ro-feed.top', myPort: 'center', dx: 20, dy: -45 }, label: 'P-RO-IN', max: 16, unit: 'bar', source: 'ro-feed' }, - { id: 'p-ro-out', anchor: { to: 'ro-press.top', myPort: 'center', dx: 315, dy: -45 }, label: 'P-RO-OUT', max: 16, unit: 'bar', source: 'ro-press' }, - { id: 'p-dp-1', anchor: { to: 'dp.top', myPort: 'center', dx: 0, dy: -50 }, label: 'P-DP', max: 8, unit: 'bar', source: 'aer-drain' }, + { id: 'p-cep-1', anchor: { to: 'chem-cep-1.bot', myPort: 'center', dx: -15, dy: 95 }, label: 'P1', max: 8, unit: 'bar', source: 'ro-feed' }, + { id: 'p-cep-2', anchor: { to: 'chem-cep-2.bot', myPort: 'center', dx: -5, dy: 95 }, label: 'P2', max: 8, unit: 'bar', source: 'ro-feed' }, + { id: 'p-ro-in', anchor: { to: 'ro-feed.top', myPort: 'center', dx: 20, dy: -57 }, label: 'P-RO-IN', max: 16, unit: 'bar', source: 'ro-feed' }, + { id: 'p-ro-out', anchor: { to: 'ro-press.top', myPort: 'center', dx: 315, dy: -130 }, label: 'P-RO-OUT', max: 16, unit: 'bar', source: 'ro-press' }, + { id: 'p-dp-1', anchor: { to: 'dp.top', myPort: 'center', dx: 95, dy: -50 }, label: 'P-DP', max: 8, unit: 'bar', source: 'aer-drain' }, ]; const SENSORS = [ - { id: 'do', anchor: { to: 'mbr.top', myPort: 'topLeft', dx: -250, dy: -80 }, label: 'DO', value: 8.5, unit: 'ppm', min: 4, max: 12, alarms: { lo: 5, hi: 11 } }, - { id: 'temp', anchor: { to: 'mbr.top', myPort: 'topLeft', dx: -130, dy: -80 }, label: 'Temp', value: 25.0, unit: 'C', min: 0, max: 50, alarms: { lo: 10, hi: 35 } }, - { id: 'tss', anchor: { to: 'mbr.top', myPort: 'topLeft', dx: -10, dy: -80 }, label: 'TSS', value: 0.28, unit: 'mg/L', min: 0, max: 5, alarms: { lo: 0, hi: 2 } }, - { id: 'ph', anchor: { to: 'mbr.top', myPort: 'topLeft', dx: 110, dy: -80 }, label: 'pH', value: 8.5, unit: 'pH', min: 0, max: 14, alarms: { lo: 6.5, hi: 8.5 } }, - { id: 'tss-raw', anchor: { to: 'bw-a1.bot', myPort: 'topLeft', dx: -68, dy: 75 }, label: 'TSS-RAW', value: 1.42, unit: 'mg/L', min: 0, max: 5, alarms: { lo: 0, hi: 3 } }, + { id: 'do', anchor: { to: 'mbr.top', myPort: 'topLeft', dx: -250, dy: -90 }, label: 'DO', value: 8.5, unit: 'ppm', min: 4, max: 12, alarms: { lo: 5, hi: 11 } }, + { id: 'temp', anchor: { to: 'mbr.top', myPort: 'topLeft', dx: -110, dy: -90 }, label: 'Temp', value: 25.0, unit: 'C', min: 0, max: 50, alarms: { lo: 10, hi: 35 } }, + { id: 'tss', anchor: { to: 'mbr.top', myPort: 'topLeft', dx: 30, dy: -90 }, label: 'TSS', value: 0.28, unit: 'mg/L', min: 0, max: 5, alarms: { lo: 0, hi: 2 } }, + { id: 'ph', anchor: { to: 'mbr.top', myPort: 'topLeft', dx: 170, dy: -90 }, label: 'pH', value: 8.5, unit: 'pH', min: 0, max: 14, alarms: { lo: 6.5, hi: 8.5 } }, + { id: 'tss-raw', anchor: { to: 'bw-a1.bot', myPort: 'topLeft', dx: -68, dy: 105 }, label: 'TSS-RAW', value: 1.42, unit: 'mg/L', min: 0, max: 5, alarms: { lo: 0, hi: 3 } }, ]; const LEDS = [ @@ -493,15 +493,15 @@ ]; const TILES = [ - { id: 'flow-in', x: 1830, y: 520, label: 'FLOW IN', value: '0.0', unit: 'L/min', w: 130, h: 50 }, - { id: 'flow-out', x: 1830, y: 580, label: 'FLOW OUT', value: '0.0', unit: 'L/min', w: 130, h: 50 }, - { id: 'recovery', x: 1830, y: 640, label: 'RECOVERY', value: '0.0', unit: '%', w: 130, h: 50 }, - { id: 'alarms', x: 1830, y: 700, label: 'ACTIVE ALARMS', value: '0', unit: '', w: 130, h: 50 }, + { id: 'flow-in', x: 1830, y: 520, label: 'FLOW IN', value: '0.0', unit: 'L/min', w: 150, h: 64 }, + { id: 'flow-out', x: 1830, y: 594, label: 'FLOW OUT', value: '0.0', unit: 'L/min', w: 150, h: 64 }, + { id: 'recovery', x: 1830, y: 668, label: 'RECOVERY', value: '0.0', unit: '%', w: 150, h: 64 }, + { id: 'alarms', x: 1830, y: 742, label: 'ACTIVE ALARMS', value: '0', unit: '', w: 150, h: 64 }, ]; const PANELS = [ - { id: 'cep', x: 40, y: 510, w: 410, h: 210, label: 'CEP SYSTEM' }, - { id: 'cip', x: 40, y: 740, w: 470, h: 125, label: 'CIP SYSTEM' }, + { id: 'cep', x: 40, y: 510, w: 410, h: 300, label: 'CEP SYSTEM' }, + { id: 'cip', x: 40, y: 820, w: 470, h: 125, label: 'CIP SYSTEM' }, { id: 'mbr-frame', x: 760, y: 600, w: 500, h: 200, label: '' }, ]; @@ -569,7 +569,7 @@ const bubbles = bubbleSpots.map((bx, i) => ``).join('\n '); out.push(` - ${t.label} + ${t.label} diff --git a/package-lock.json b/package-lock.json index e6ada6f1..8ec08b36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,10 +1,11 @@ { - "name": "ERP-node", + "name": "invyone", "lockfileVersion": 3, "requires": true, "packages": { "": { "dependencies": { + "@anthropic-ai/claude-code": "^2.1.132", "@prisma/client": "^6.16.2", "@react-three/drei": "^10.7.6", "@react-three/fiber": "^9.4.0", @@ -21,6 +22,133 @@ "playwright": "^1.58.2" } }, + "node_modules/@anthropic-ai/claude-code": { + "version": "2.1.132", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-2.1.132.tgz", + "integrity": "sha512-y0Exs+/jST92n0Hr+GLVCxQ54TYoLnphnwnXztgGAgkrOVMiGJ1uzvmqOds69eO7SQwKwBc7RiLzMZKQjvoiQA==", + "hasInstallScript": true, + "license": "SEE LICENSE IN README.md", + "bin": { + "claude": "bin/claude.exe" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "@anthropic-ai/claude-code-darwin-arm64": "2.1.132", + "@anthropic-ai/claude-code-darwin-x64": "2.1.132", + "@anthropic-ai/claude-code-linux-arm64": "2.1.132", + "@anthropic-ai/claude-code-linux-arm64-musl": "2.1.132", + "@anthropic-ai/claude-code-linux-x64": "2.1.132", + "@anthropic-ai/claude-code-linux-x64-musl": "2.1.132", + "@anthropic-ai/claude-code-win32-arm64": "2.1.132", + "@anthropic-ai/claude-code-win32-x64": "2.1.132" + } + }, + "node_modules/@anthropic-ai/claude-code-darwin-arm64": { + "version": "2.1.132", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code-darwin-arm64/-/claude-code-darwin-arm64-2.1.132.tgz", + "integrity": "sha512-imBlTl4dJ+IqGPMLTbLjSefBSd7c3iUjHGkz7/Q3RVDQJvcb2G83LFsaQGR3JwZ+fEwC7T6E6B8Mprwe9En08g==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@anthropic-ai/claude-code-darwin-x64": { + "version": "2.1.132", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code-darwin-x64/-/claude-code-darwin-x64-2.1.132.tgz", + "integrity": "sha512-CnsFT78zXkcR2wcEVivP6aqOMcIKSnegPZZierlSQ/zaXWCVNoW+3OHloMnDm+7BX6BT41K2bz7GckCr0pHiyg==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@anthropic-ai/claude-code-linux-arm64": { + "version": "2.1.132", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code-linux-arm64/-/claude-code-linux-arm64-2.1.132.tgz", + "integrity": "sha512-VJgVycbS6u/lD05vGKmd+mMFypYt098jYt6yYGibqIvFPQcNkmMZJQfPIJBuVB/+XeySNlI4Pplo6jhwRM0HUA==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@anthropic-ai/claude-code-linux-arm64-musl": { + "version": "2.1.132", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code-linux-arm64-musl/-/claude-code-linux-arm64-musl-2.1.132.tgz", + "integrity": "sha512-RhGYFnGf6IE3PpOG8IWkrYoW1uS1i23cqvGKBHxlLhWsbvxxWUSOHd5IlzCS7udG7L4/EQU2UEAcdLaFEF8ZdQ==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@anthropic-ai/claude-code-linux-x64": { + "version": "2.1.132", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code-linux-x64/-/claude-code-linux-x64-2.1.132.tgz", + "integrity": "sha512-ElubH7haoKIXy9SNn8dTGMym0n+zy6A70wWXhhcdAFo7P/M8bdLT6zRyp5TttrkhAZcA8xPVxEB/zNU3sPCMLg==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@anthropic-ai/claude-code-linux-x64-musl": { + "version": "2.1.132", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code-linux-x64-musl/-/claude-code-linux-x64-musl-2.1.132.tgz", + "integrity": "sha512-Xu6kajnzX5igVtjuqTdohGoat8bXMccs9TsS3n14Ith+3mgB0IX+zTxiR/0V9TCv7nkYe1tRrO2DHEOl9GzR4A==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@anthropic-ai/claude-code-win32-arm64": { + "version": "2.1.132", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code-win32-arm64/-/claude-code-win32-arm64-2.1.132.tgz", + "integrity": "sha512-FuNMUO6tERKJZrayqDf1ScAcFhzhs2joQ9PE8POc2Y7Bv2E+aq/bmaj1Nxff+RKgpYSnoGfru9Sk3lSLtIUzGQ==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@anthropic-ai/claude-code-win32-x64": { + "version": "2.1.132", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code-win32-x64/-/claude-code-win32-x64-2.1.132.tgz", + "integrity": "sha512-prA22ML+CCHsMI0jj9BD5EVjzhZ38vPc+Udz/GvPs6h+HPH+EeNSjGDNzn9YvkiYpjUun2HYF24ztEXMJigGVw==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@azure-rest/core-client": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@azure-rest/core-client/-/core-client-2.5.1.tgz", @@ -471,7 +599,6 @@ "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.4.0.tgz", "integrity": "sha512-k4iu1R6e5D54918V4sqmISUkI5OgTw3v7/sDRKEC632Wd5g2WBtUS5gyG63X0GJO/HZUj1tsjSXfyzwrUHZl1g==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.17.8", "@types/react-reconciler": "^0.32.0", @@ -712,7 +839,6 @@ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.180.0.tgz", "integrity": "sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg==", "license": "MIT", - "peer": true, "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", @@ -1079,7 +1205,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/d3-color": { "version": "3.1.0", @@ -1138,7 +1265,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -2166,7 +2292,6 @@ "integrity": "sha512-aRvldGE5UUJTtVmFiH3WfNFNiqFlAtePUxcI0UEGlnXCX7DqhiMT5TRYwncHFeA/Reca5W6ToXXyCMTeFPdSXA==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@prisma/config": "6.16.2", "@prisma/engines": "6.16.2" @@ -2382,7 +2507,8 @@ "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/semver": { "version": "7.7.2", @@ -2501,8 +2627,7 @@ "version": "0.180.0", "resolved": "https://registry.npmjs.org/three/-/three-0.180.0.tgz", "integrity": "sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/three-mesh-bvh": { "version": "0.8.3", @@ -2598,7 +2723,6 @@ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", - "peer": true, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } diff --git a/package.json b/package.json index 16b3b1e8..785c4aec 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "dependencies": { + "@anthropic-ai/claude-code": "^2.1.132", "@prisma/client": "^6.16.2", "@react-three/drei": "^10.7.6", "@react-three/fiber": "^9.4.0",