7635412b7b
- 작업자 폰(/mobile)을 SCADA 데모와 ws 로 연결, 알람 발생 시 풀스크린 푸시
· v5 솔리드+글로우 톤, 진동/Web Audio 비프/Wake Lock/auto reconnect
· 시연 안전망: ?test=1 자동 발동, 우상단 hidden 트리거
- backend: com.erp.alarm 신규 패키지 (WebSocketConfig + Handshake + Handler + Controller)
· JWT 토큰 핸드셰이크 검증, userId 기반 채널 매핑 (멀티 디바이스 지원)
· spring-boot-starter-websocket 의존성 추가
· path 를 /api/demo/* 안에 두어 Traefik 라우트 추가 불필요 + 정식 알람과 분리
- SCADA scenario.js 의 emergency 시퀀스(2700ms)에 fetch('/api/demo/alarm/trigger') 배선
· /scada?worker=<user_id> query 로 target user 지정 (iframe src 로 전달)
- 운영 시연 URL: siflex.invyone.com/mobile (siflex_user) ↔ /scada?worker=siflex_user
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
354 lines
17 KiB
HTML
354 lines
17 KiB
HTML
<!doctype html>
|
||
<html lang="ko">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<title>INVYONE Stage-2 — Water Treatment SCADA Demo</title>
|
||
<link rel="stylesheet" href="css/invyone-stage2.css" />
|
||
</head>
|
||
<body>
|
||
|
||
<header class="topbar">
|
||
<h1>INVYONE</h1>
|
||
<span class="subtitle">Stage-2 · Water Treatment Digital Twin Demo</span>
|
||
<span class="spacer"></span>
|
||
<div class="mode-display"><span class="label">MODE</span><span id="current-mode">STOP</span></div>
|
||
<div class="clock" id="clock">--:--:--</div>
|
||
</header>
|
||
|
||
<!-- 시연용 상단 알람 배너 (평상시 숨김, 경고 시 슬라이드 다운) -->
|
||
<div class="alarm-banner" id="alarm-banner">
|
||
<span class="alarm-banner-icon">⚠</span>
|
||
<span class="alarm-banner-severity">CRITICAL</span>
|
||
<span class="alarm-banner-code" id="alarm-banner-code">P-IN-HH</span>
|
||
<span class="alarm-banner-msg" id="alarm-banner-msg">BW-A1 토출 압력 — 정상범위(4.0~5.0 bar) 초과 / 누설 의심</span>
|
||
<span class="alarm-banner-loc">
|
||
<span class="loc-pin">📍</span>
|
||
<b id="alarm-banner-loc-text">펌프룸 A · BW-A1 (원수 취수펌프 #1)</b>
|
||
</span>
|
||
</div>
|
||
|
||
<main class="main">
|
||
|
||
<div class="scene-wrap">
|
||
<svg id="scene" viewBox="0 0 2000 880" preserveAspectRatio="xMidYMid meet">
|
||
<defs>
|
||
<linearGradient id="metal-h" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="0" y2="20">
|
||
<stop offset="0" stop-color="#d9d9d9"/>
|
||
<stop offset="0.18" stop-color="#f5f5f5"/>
|
||
<stop offset="0.5" stop-color="#8f8f8f"/>
|
||
<stop offset="0.82" stop-color="#d7d7d7"/>
|
||
<stop offset="1" stop-color="#6f6f6f"/>
|
||
</linearGradient>
|
||
<linearGradient id="metal-v" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="20" y2="0">
|
||
<stop offset="0" stop-color="#d9d9d9"/>
|
||
<stop offset="0.18" stop-color="#f5f5f5"/>
|
||
<stop offset="0.5" stop-color="#8f8f8f"/>
|
||
<stop offset="0.82" stop-color="#d7d7d7"/>
|
||
<stop offset="1" stop-color="#6f6f6f"/>
|
||
</linearGradient>
|
||
<linearGradient id="metal-tank" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="100" y2="0">
|
||
<stop offset="0" stop-color="#666"/>
|
||
<stop offset="0.18" stop-color="#ddd"/>
|
||
<stop offset="0.42" stop-color="#fff"/>
|
||
<stop offset="0.72" stop-color="#666"/>
|
||
<stop offset="1" stop-color="#ddd"/>
|
||
</linearGradient>
|
||
<linearGradient id="glass-grad" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="100" y2="0">
|
||
<stop offset="0" stop-color="#fff" stop-opacity="0.18"/>
|
||
<stop offset="0.14" stop-color="#fff" stop-opacity="0.04"/>
|
||
<stop offset="0.5" stop-color="#071326" stop-opacity="0.4"/>
|
||
<stop offset="0.88" stop-color="#fff" stop-opacity="0.06"/>
|
||
<stop offset="1" stop-color="#fff" stop-opacity="0.16"/>
|
||
</linearGradient>
|
||
</defs>
|
||
<g id="scene-content"></g>
|
||
</svg>
|
||
</div>
|
||
|
||
<aside class="sidebar">
|
||
|
||
<section class="sidebar-section">
|
||
<h2>운전 모드</h2>
|
||
<div class="modes">
|
||
<button data-mode="NORMAL">NORMAL</button>
|
||
<button data-mode="BACKWASH">BACKWASH</button>
|
||
<button data-mode="CIP">CIP</button>
|
||
<button data-mode="STOP" class="active">STOP</button>
|
||
</div>
|
||
<div class="controls-row">
|
||
<button data-mode="CHEM-DOSE">+ 약품 투입</button>
|
||
</div>
|
||
<div class="controls-row">
|
||
<button id="btn-reset">⟲ RESET</button>
|
||
<button id="btn-pause">⏸ 일시정지</button>
|
||
<button id="btn-alarm" class="warn">⚠ 알람 데모</button>
|
||
</div>
|
||
<div class="controls-row" style="margin-top:10px">
|
||
<button id="btn-emergency" class="emergency">🚨 경고시스템 시연</button>
|
||
</div>
|
||
<div class="global-slider">
|
||
<span>유량 배율</span>
|
||
<input type="range" id="flow-multiplier" min="0.5" max="50" step="0.5" value="5" />
|
||
<span id="flow-multiplier-val">5.0x</span>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="sidebar-section inspector" id="inspector">
|
||
<div class="inspector-empty">
|
||
<div class="muted">컴포넌트를 클릭해 제어 패널을 여세요</div>
|
||
</div>
|
||
</section>
|
||
|
||
</aside>
|
||
|
||
</main>
|
||
|
||
<!-- ============== EMERGENCY SCENARIO OVERLAY ============== -->
|
||
<!-- 회사 맵 패널 (우측 별도, 평상시 화면 밖) -->
|
||
<aside class="map-panel" id="map-panel">
|
||
<header class="map-panel-head">
|
||
<span class="map-panel-title">🗺 SI FLEX 베트남 사업장 맵</span>
|
||
<span class="map-panel-status">실시간 위치 추적</span>
|
||
</header>
|
||
<div class="map-panel-body">
|
||
<!-- 실제 사업장 평면도 반영 (SI FLEX 베트남) — 사용자 제공 디자인 (시안 테두리 + glow + 입구) -->
|
||
<svg class="map-svg" viewBox="0 0 928 577" preserveAspectRatio="xMidYMid meet">
|
||
<!-- 외곽 부지 (점선만, 라벨) -->
|
||
<rect x="14" y="18" width="900" height="540" fill="none" stroke="#3a5278" stroke-width="1.2" stroke-dasharray="6 4"/>
|
||
<text x="22" y="38" fill="#5af9ff" font-size="11" font-family="Arial">SITE BOUNDARY</text>
|
||
|
||
<!-- 좌상: 오·폐수처리장 (알람 위치) -->
|
||
<g class="map-building" data-bid="pump-room-a">
|
||
<rect x="30" y="42" width="160" height="78" fill="#0d1a2e" stroke="#5af9ff" stroke-width="1.5"/>
|
||
<rect x="100" y="118" width="22" height="6" fill="#0d1a2e" stroke="#5af9ff" stroke-width="1"/>
|
||
<text x="110" y="74" text-anchor="middle" fill="#fff" font-size="11" font-weight="600">오·폐수처리장</text>
|
||
<text x="110" y="89" text-anchor="middle" fill="#cfd3d8" font-size="9">WASTEWATER</text>
|
||
<text x="110" y="108" text-anchor="middle" fill="#ff8a3a" font-size="9">BW-A1 (원수 취수펌프)</text>
|
||
</g>
|
||
|
||
<!-- 좌중상: SBG 종말처리장 -->
|
||
<g class="map-building" data-bid="treatment">
|
||
<rect x="30" y="135" width="160" height="78" fill="#0d1a2e" stroke="#5af9ff" stroke-width="1.5"/>
|
||
<rect x="100" y="211" width="22" height="6" fill="#0d1a2e" stroke="#5af9ff" stroke-width="1"/>
|
||
<text x="110" y="170" text-anchor="middle" fill="#fff" font-size="11" font-weight="600">SBG 종말처리장</text>
|
||
<text x="110" y="186" text-anchor="middle" fill="#cfd3d8" font-size="9">SBG TREATMENT</text>
|
||
</g>
|
||
|
||
<!-- 중상: 라벨 없는 큰 빈 영역 (우측 하단 입구 표시) -->
|
||
<g>
|
||
<rect x="210" y="42" width="440" height="171" fill="#0d1a2e" stroke="#5af9ff" stroke-width="1.5"/>
|
||
<!-- 우측 하단 햄버거 입구 표시 -->
|
||
<line x1="615" y1="200" x2="640" y2="200" stroke="#5af9ff" stroke-width="1"/>
|
||
<line x1="615" y1="205" x2="640" y2="205" stroke="#5af9ff" stroke-width="1"/>
|
||
<line x1="615" y1="210" x2="640" y2="210" stroke="#5af9ff" stroke-width="1"/>
|
||
</g>
|
||
|
||
<!-- 우상 좌: 식당 -->
|
||
<g class="map-building" data-bid="chemical">
|
||
<rect x="665" y="60" width="125" height="100" fill="#0d1a2e" stroke="#5af9ff" stroke-width="1.5"/>
|
||
<rect x="715" y="158" width="22" height="6" fill="#0d1a2e" stroke="#5af9ff" stroke-width="1"/>
|
||
<text x="727" y="105" text-anchor="middle" fill="#fff" font-size="13" font-weight="600">식당</text>
|
||
<text x="727" y="125" text-anchor="middle" fill="#cfd3d8" font-size="10">CAFETERIA</text>
|
||
</g>
|
||
|
||
<!-- 우상 우: 방재센터 (담당자 시작) -->
|
||
<g class="map-building" data-bid="control-room">
|
||
<rect x="800" y="60" width="115" height="100" fill="#0d1a2e" stroke="#5af9ff" stroke-width="1.8"/>
|
||
<rect x="845" y="158" width="22" height="6" fill="#0d1a2e" stroke="#5af9ff" stroke-width="1"/>
|
||
<text x="857" y="100" text-anchor="middle" fill="#fff" font-size="12" font-weight="600">방재센터</text>
|
||
<text x="857" y="115" text-anchor="middle" fill="#cfd3d8" font-size="9">SAFETY</text>
|
||
<text x="857" y="128" text-anchor="middle" fill="#cfd3d8" font-size="9">CENTER</text>
|
||
<text x="857" y="148" text-anchor="middle" fill="#7cff3a" font-size="9">● 24h</text>
|
||
</g>
|
||
|
||
<!-- 좌하: 2공장 -->
|
||
<g class="map-building" data-bid="factory-2">
|
||
<rect x="30" y="240" width="475" height="305" fill="#0d1a2e" stroke="#5af9ff" stroke-width="1.5"/>
|
||
<!-- 하단 입구 4개 -->
|
||
<rect x="100" y="543" width="22" height="6" fill="#0d1a2e" stroke="#5af9ff" stroke-width="1"/>
|
||
<rect x="200" y="543" width="22" height="6" fill="#0d1a2e" stroke="#5af9ff" stroke-width="1"/>
|
||
<rect x="310" y="543" width="22" height="6" fill="#0d1a2e" stroke="#5af9ff" stroke-width="1"/>
|
||
<rect x="420" y="543" width="22" height="6" fill="#0d1a2e" stroke="#5af9ff" stroke-width="1"/>
|
||
<text x="267" y="390" text-anchor="middle" fill="#fff" font-size="20" font-weight="600">2공장</text>
|
||
<text x="267" y="415" text-anchor="middle" fill="#cfd3d8" font-size="12">FACTORY 2</text>
|
||
</g>
|
||
|
||
<!-- 중하: 사무동 (좁고 긴 박스) -->
|
||
<g class="map-building" data-bid="office">
|
||
<rect x="515" y="240" width="60" height="305" fill="#0d1a2e" stroke="#5af9ff" stroke-width="1.5"/>
|
||
<rect x="535" y="455" width="20" height="20" fill="#0d1a2e" stroke="#5af9ff" stroke-width="1"/>
|
||
<text x="545" y="388" text-anchor="middle" fill="#fff" font-size="11" font-weight="600">사무동</text>
|
||
<text x="545" y="404" text-anchor="middle" fill="#cfd3d8" font-size="9">OFFICE BLDG</text>
|
||
</g>
|
||
|
||
<!-- 우하: 1공장 -->
|
||
<g class="map-building" data-bid="utility">
|
||
<rect x="585" y="240" width="330" height="305" fill="#0d1a2e" stroke="#5af9ff" stroke-width="1.5"/>
|
||
<!-- 하단 입구 2개 -->
|
||
<rect x="660" y="543" width="22" height="6" fill="#0d1a2e" stroke="#5af9ff" stroke-width="1"/>
|
||
<rect x="820" y="543" width="22" height="6" fill="#0d1a2e" stroke="#5af9ff" stroke-width="1"/>
|
||
<text x="750" y="390" text-anchor="middle" fill="#fff" font-size="20" font-weight="600">1공장</text>
|
||
<text x="750" y="415" text-anchor="middle" fill="#cfd3d8" font-size="12">FACTORY 1</text>
|
||
</g>
|
||
|
||
<!-- 담당자 이동 경로 (방재센터 → 오·폐수처리장, 평상시 숨김) -->
|
||
<path id="map-route" d="M 857 110 L 500 110 L 110 110" fill="none" stroke="#ff8a3a" stroke-width="2" stroke-dasharray="6 4" opacity="0"/>
|
||
|
||
<!-- 알람 위치 (오·폐수처리장 안, 평상시 숨김) -->
|
||
<g id="map-alarm-pin" transform="translate(110 81)" opacity="0">
|
||
<circle r="22" fill="#ff4f9a" opacity="0.25"/>
|
||
<circle r="14" fill="#ff4f9a" opacity="0.5"/>
|
||
<circle r="7" fill="#ff4f9a"/>
|
||
<text y="-32" text-anchor="middle" fill="#ff4f9a" font-size="12" font-weight="700">★ ALARM</text>
|
||
</g>
|
||
|
||
<!-- 담당자 아이콘 (평상시 방재센터 안) -->
|
||
<g id="map-officer" transform="translate(857 110)">
|
||
<circle r="14" fill="#5af9ff" stroke="#fff" stroke-width="2"/>
|
||
<text text-anchor="middle" dy="4" fill="#000" font-size="10" font-weight="700">KY</text>
|
||
<text y="-22" text-anchor="middle" fill="#5af9ff" font-size="10" font-weight="600">김영수</text>
|
||
</g>
|
||
|
||
<!-- 범례 -->
|
||
<g transform="translate(22 568)">
|
||
<circle r="5" fill="#5af9ff" cx="6" cy="0"/>
|
||
<text x="18" y="4" fill="#cfd3d8" font-size="10">담당자</text>
|
||
<circle r="5" fill="#ff4f9a" cx="80" cy="0"/>
|
||
<text x="92" y="4" fill="#cfd3d8" font-size="10">알람 위치</text>
|
||
<line x1="160" y1="0" x2="190" y2="0" stroke="#ff8a3a" stroke-width="2" stroke-dasharray="4 3"/>
|
||
<text x="200" y="4" fill="#cfd3d8" font-size="10">이동 경로</text>
|
||
</g>
|
||
</svg>
|
||
</div>
|
||
<footer class="map-panel-foot">
|
||
<span class="map-status-dot"></span>
|
||
<span id="map-officer-status">김영수 책임자 — 관제실 대기</span>
|
||
</footer>
|
||
</aside>
|
||
|
||
<!-- 핸드폰 알림 (우상단 fixed) -->
|
||
<div class="phone-notify" id="phone-notify">
|
||
<div class="phone-frame">
|
||
<div class="phone-notch"></div>
|
||
<div class="phone-screen">
|
||
<div class="phone-time" id="phone-time">--:--</div>
|
||
<div class="phone-card">
|
||
<div class="phone-card-head">
|
||
<span class="phone-app-icon">🚨</span>
|
||
<span class="phone-app-name">INVYONE EHS</span>
|
||
<span class="phone-time-dim">지금</span>
|
||
</div>
|
||
<div class="phone-card-title">긴급 알람 발생</div>
|
||
<div class="phone-card-msg" id="phone-msg">P-IN 토출압 HH — 즉시 점검 요망</div>
|
||
<div class="phone-card-actions">
|
||
<button class="phone-btn phone-btn-dismiss">나중에</button>
|
||
<button class="phone-btn phone-btn-confirm" id="phone-confirm">확인</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 모달 (CCTV + 담당자) -->
|
||
<div class="emergency-modal-backdrop" id="emergency-modal">
|
||
<div class="emergency-modal">
|
||
<header class="modal-head">
|
||
<div class="modal-head-left">
|
||
<span class="modal-severity">CRITICAL</span>
|
||
<span class="modal-code" id="modal-alarm-code">P-IN-HH</span>
|
||
</div>
|
||
<div class="modal-head-title" id="modal-alarm-title">BW-A1 펌프 과압 / 누설 의심</div>
|
||
<button class="modal-close" id="modal-close" title="닫기">×</button>
|
||
</header>
|
||
<!-- 어디서 발생했는지 명확히 표시 -->
|
||
<div class="modal-location">
|
||
<span class="loc-icon">📍</span>
|
||
<span class="loc-text">위치: <b id="modal-alarm-location">펌프룸 A · BW-A1 (원수 취수펌프 #1)</b></span>
|
||
<span class="loc-pointer">↙ P&ID에서 ⚠ 깜빡이는 설비 확인</span>
|
||
</div>
|
||
<div class="modal-body">
|
||
<!-- 좌: CCTV (placeholder, 실제 영상 자산 도착하면 교체) -->
|
||
<section class="modal-cctv">
|
||
<div class="cctv-bar">
|
||
<span class="cctv-rec">● REC</span>
|
||
<span class="cctv-cam" id="modal-cctv-cam">CAM-01 / PUMP ROOM A / BW-A1</span>
|
||
<span class="cctv-time" id="modal-cctv-time">--:--:--</span>
|
||
</div>
|
||
<div class="cctv-screen">
|
||
<div class="cctv-noise"></div>
|
||
<div class="cctv-scanline"></div>
|
||
<div class="cctv-placeholder">
|
||
<video class="cctv-video cctv-video-going" src="video/cctv.mp4" muted playsinline preload="auto"></video>
|
||
<video class="cctv-video cctv-video-arrived" src="video/cctv-arrived.mp4" muted playsinline preload="auto"></video>
|
||
<div class="cctv-target-box">
|
||
<span class="cctv-target-label">TARGET: BW-A1</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="cctv-meta">
|
||
<span>해상도: 1920×1080</span>
|
||
<span>FPS: 30</span>
|
||
<span class="cctv-live">● LIVE</span>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 우: 담당자 카드 -->
|
||
<section class="modal-officer">
|
||
<div class="officer-head">담당자 정보</div>
|
||
<div class="officer-card">
|
||
<div class="officer-avatar" id="modal-officer-avatar"><img src="img/officer.png" alt="김영수"/></div>
|
||
<div class="officer-info">
|
||
<div class="officer-name" id="modal-officer-name">김영수</div>
|
||
<div class="officer-title" id="modal-officer-title">안전관리책임자</div>
|
||
<div class="officer-role" id="modal-officer-role">EHS 1차 대응자</div>
|
||
</div>
|
||
</div>
|
||
<div class="officer-contact">
|
||
<div class="officer-row"><span>휴대폰</span><b id="modal-officer-phone">010-7842-3019</b></div>
|
||
<div class="officer-row"><span>현재 위치</span><b id="modal-officer-loc">관제실</b></div>
|
||
<div class="officer-row"><span>응답 상태</span><b class="officer-status" id="modal-officer-status">📡 알림 발송 중...</b></div>
|
||
</div>
|
||
<button class="officer-action" id="modal-dispatch">
|
||
<span class="dispatch-icon">📞</span>
|
||
<span>즉시 출동 요청</span>
|
||
</button>
|
||
</section>
|
||
</div>
|
||
<div class="modal-alarm-message">
|
||
<div class="alarm-msg-label">▲ 알람 상세</div>
|
||
<div class="alarm-msg-text" id="modal-alarm-message">BW-A1 토출 압력이 정상 운전 범위(4.0~5.0 bar)를 초과했습니다. 현장 점검이 즉시 필요합니다.</div>
|
||
</div>
|
||
<footer class="modal-foot">
|
||
<button class="modal-btn modal-btn-skip" id="modal-skip">⏭ 스킵</button>
|
||
<button class="modal-btn modal-btn-ack" id="modal-ack" disabled>✓ 확인 (ACK) — 담당자 도착 후 활성화</button>
|
||
</footer>
|
||
</div>
|
||
</div>
|
||
|
||
<section class="eventlog-wrap">
|
||
<h2 style="display:flex;justify-content:space-between;align-items:center">
|
||
<span>EVENT LOG</span>
|
||
<span style="display:flex;gap:6px">
|
||
<button id="btn-clear-log" style="padding:2px 8px;font-size:10px;background:#030814;color:#cfd3d8;border:1px solid #2a3f5a;cursor:pointer">CLEAR</button>
|
||
<button id="btn-deselect" style="padding:2px 8px;font-size:10px;background:#030814;color:#cfd3d8;border:1px solid #2a3f5a;cursor:pointer">선택해제</button>
|
||
</span>
|
||
</h2>
|
||
<div id="event-log"></div>
|
||
</section>
|
||
|
||
<script>
|
||
// SCADA 데모 → 작업자 폰 push 의 target user_id.
|
||
// 부모 라우트(/scada) 가 ?worker=<id> 를 iframe src 로 전달한다.
|
||
// 미지정이면 fetch 호출 자체를 스킵 (로컬 시연 모드).
|
||
window.SCADA_ALARM_TARGET = new URLSearchParams(location.search).get('worker') || '';
|
||
</script>
|
||
<script src="js/components.js"></script>
|
||
<script src="js/topology.js"></script>
|
||
<script src="js/engine.js"></script>
|
||
<script src="js/scenario.js"></script>
|
||
<script src="js/ui.js"></script>
|
||
<script src="js/main.js"></script>
|
||
|
||
</body>
|
||
</html>
|