Files
invyone/frontend/public/fire-alarm-demo/index.html
T
DDD1542 8eb4e8c9a2
Build & Deploy to K8s / build-and-deploy (push) Successful in 5m29s
feat: 화재 알람 데모 SCADA 스타일 경고 인디케이터 + ZONE 17 콘텐츠 정렬
- 각 zone 중앙에 SCADA 스타일 경고 비콘 자동 삽입 (펄스 링 + 빨간 배지 + 노란 경고 삼각형)
- WARN/ALARM 별 색상 분리 (CSS 변수 --b-* 로 SVG <use> shadow DOM 통과)
  - WARN: 노란 톤 + 정적 표시
  - ALARM: 빨간 톤 + drop-shadow + brightness 깜빡임
- zone-area 점선 테두리: warn(노란/얇음), alarm(빨간/굵음+pulse)
- ZONE 17 콘텐츠를 ZONE 18 비례에 맞춰 재배치 (label y 453→396) — manual-call 과 라벨 겹침 해소

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 13:34:20 +09:00

1962 lines
84 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Fire Alarm Monitoring Web System</title>
<style>
:root{
--bg:#07111d;
--bg2:#0a1d31;
--line:#d9ecff;
--line2:#6b84a8;
--panel:#d6dbe3;
--panel2:#f0f3f7;
--blue:#0f4f9c;
--blue-dark:#08294f;
--green:#0c7b39;
--green-dark:#065628;
--green-lite:#19a14f;
--zone-normal:#8bd48f;
--zone-warn:#f0c44f;
--zone-alarm:#ff4f65;
--text:#ffffff;
--muted:#cdd9e8;
--silver:#c8cdd8;
--dark:#0e1826;
}
*{box-sizing:border-box}
html,body{margin:0;height:100%;background:#10161d;font-family:Arial,"Noto Sans KR",sans-serif}
body{overflow:hidden;color:#fff}
.app{
width:100vw;height:100vh;
min-width:1320px;min-height:760px;
background:
radial-gradient(circle at 30% 10%, rgba(58,113,185,.15), transparent 28%),
radial-gradient(circle at 75% 40%, rgba(31,108,87,.18), transparent 22%),
linear-gradient(180deg,#08111b 0%,#0a1420 100%);
padding:10px;
}
.frame{
width:100%;height:100%;
border:2px solid #0f4680;
box-shadow:inset 0 0 0 1px #7ec5ff,0 0 24px rgba(24,123,255,.18);
background:#07111d;
display:grid;
grid-template-rows:38px 1fr 36px;
}
/* TOP BAR */
.topbar{
display:grid;
grid-template-columns:220px 1fr 260px;
align-items:center;
padding:0 14px;
border-bottom:2px solid #6daef1;
background:linear-gradient(180deg,#0f3160,#08203f 70%,#07172d);
letter-spacing:.3px;
transition:background .25s;
}
.topbar.alarm-active{
background:linear-gradient(180deg,#7a1216,#3d0708 70%,#220304);
border-bottom-color:#ff3552;
animation:headerPulse .8s steps(2,start) infinite;
}
@keyframes headerPulse{50%{background:linear-gradient(180deg,#a52129,#5d0e10 70%,#330505)}}
.brand{
display:flex;align-items:center;gap:12px;font-weight:900;font-size:24px;color:#eef7ff;
}
.brand .menu{
font-size:22px;line-height:1
}
.brand .accent{color:#7ec6ff}
.header-title{
text-align:center;font-size:24px;font-weight:900;color:#fff;
text-shadow:0 0 10px rgba(120,210,255,.35);
transition:color .2s,text-shadow .2s;
}
.header-title.alarm{
color:#ffe2e6;
text-shadow:0 0 18px rgba(255,81,104,.95),0 0 4px #ff3552;
animation:titleBlink .55s steps(2,start) infinite;
}
@keyframes titleBlink{50%{opacity:.5}}
.clock{
text-align:right;font-size:17px;font-weight:800;color:#eef6ff;
}
/* MAIN */
.main{
display:grid;
grid-template-columns:1fr 360px;
min-height:0;
}
.map-area{
position:relative;
overflow:hidden;
border-right:2px solid #cfe0f4;
background:
linear-gradient(rgba(255,255,255,.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,.03) 1px, transparent 1px),
linear-gradient(180deg,#061321 0%,#081927 100%);
background-size:24px 24px,24px 24px,auto;
}
.right-panel{
background:#081326;
color:#cfd3d8;
padding:8px;
display:flex;
flex-direction:column;
gap:6px;
min-height:0;
overflow-y:auto;
border-left:1px solid #1a2a45;
}
.right-panel .panel:last-child{flex:0 0 auto}
.panel{
border:1px solid #1a2a45;
background:#050a18;
overflow:hidden;
flex-shrink:0;
}
.panel-title{
height:19px;
background:linear-gradient(180deg,#0c1c38,#071326);
color:#5af9ff;
display:flex;align-items:center;
padding:0 9px;
font-size:10px;font-weight:700;
letter-spacing:1.2px;
text-transform:uppercase;
border-bottom:1px solid #1a2a45;
}
.panel-title.red{
background:linear-gradient(180deg,#3a0d18,#1f0410);
color:#ff4f9a;
border-bottom-color:#ff4f9a;
}
.panel-title.gray{
background:linear-gradient(180deg,#1a2235,#0c1220);
color:#cfd3d8;
}
/* Pressure */
.pressure-wrap{
padding:8px;
display:grid;
grid-template-columns:1.1fr .9fr;
gap:8px;
}
.pressure-left{
display:grid;
grid-template-rows:auto auto auto;
gap:6px;
}
.pressure-table{
width:100%;
border-collapse:collapse;
font-size:11px;
color:#cfd3d8;
}
.pressure-table th,.pressure-table td{
border:1px solid #1a2a45;
padding:4px 6px;
text-align:center;
}
.pressure-table th{background:#0a1c33;color:#5af9ff;font-weight:700;letter-spacing:.5px}
.pressure-table td{background:#030814;font-family:Consolas,monospace}
.digital-row{
display:grid;grid-template-columns:1fr auto;gap:8px;align-items:center;
background:#020610;border:1px solid #1a2a45;padding:6px 8px;color:#fff;
}
.digital-row .lbl{font-size:11px;font-weight:700;color:#5af9ff;letter-spacing:.5px}
.digital{
min-width:68px;
text-align:right;
font-family:Consolas,monospace;
font-size:18px;font-weight:700;
color:#ffe26c;
}
.pressure-right .spec{
width:100%;border-collapse:collapse;font-size:11px;
color:#cfd3d8;
}
.pressure-right .spec th,.pressure-right .spec td{
border:1px solid #1a2a45;padding:5px;text-align:center;
}
.pressure-right .spec th{background:#0a1c33;color:#5af9ff;letter-spacing:.5px}
.pressure-right .spec td{background:#030814;font-family:Consolas,monospace}
.indicator-row{
display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-top:6px;
}
.indicator{
border:1px solid #4d5867;
text-align:center;
padding:5px 2px;
color:#7c8896;font-size:10px;font-weight:900;
background:#1c222d;
opacity:.55;
transition:opacity .2s,box-shadow .2s,color .2s,background .2s;
}
.indicator.active{color:#fff;opacity:1;box-shadow:inset 0 0 0 1px rgba(255,255,255,.18)}
.indicator.normal.active{background:linear-gradient(180deg,#1fa041,#116f2b);box-shadow:inset 0 0 0 1px rgba(255,255,255,.18),0 0 8px rgba(31,160,65,.55)}
.indicator.alarm.active{background:linear-gradient(180deg,#d92c30,#9f1215);box-shadow:inset 0 0 0 1px rgba(255,255,255,.18),0 0 10px rgba(217,44,48,.7);animation:indicatorBlink .55s steps(2,start) infinite}
.indicator.fault.active{background:linear-gradient(180deg,#d4a115,#a67900);box-shadow:inset 0 0 0 1px rgba(255,255,255,.18),0 0 8px rgba(212,161,21,.6)}
@keyframes indicatorBlink{50%{opacity:.55}}
/* Alarm Summary */
.summary-grid{
display:grid;
grid-template-columns:1fr 1fr;
gap:6px;
padding:8px;
}
.summary-card{
border:1px solid #707f92;
background:linear-gradient(180deg,#0e2f55,#071c34);
color:#fff;
display:grid;
grid-template-columns:20px 1fr auto;
align-items:center;
gap:5px;
padding:4px 7px;
}
.summary-icon{
width:18px;height:18px;border-radius:50%;
display:flex;align-items:center;justify-content:center;
font-size:11px;font-weight:900;
border:1px solid rgba(255,255,255,.45);
}
.summary-card.fire .summary-icon{background:#c91d2e}
.summary-card.fault .summary-icon{background:#d79b00}
.summary-card.sup .summary-icon{background:#0d77c8}
.summary-card.dis .summary-icon{background:#666}
.summary-label{font-size:10px;font-weight:800}
.summary-value{font-size:15px;font-weight:900}
/* Matrix */
.zone-grid{
padding:8px;
display:grid;
grid-template-columns:repeat(6,1fr);
gap:4px;
}
.zone-btn{
border:1px solid #1a2a45;
background:#030814;
color:#5af9ff;
height:24px;
font-size:10px;
font-weight:700;
cursor:pointer;
font-family:Consolas,monospace;
letter-spacing:.5px;
transition:background .15s,color .15s,border-color .15s;
}
.zone-btn:hover{background:#11203a;color:#fff;border-color:#5af9ff}
.zone-btn.warn{
background:linear-gradient(180deg,#3a2a08,#241801);
border-color:#ff8a3a;
color:#ffb380;
}
.zone-btn.alarm{
background:linear-gradient(180deg,#3a0d18,#1f0410);
border-color:#ff4f9a;
color:#ff8eb6;
animation:blinkBg .9s infinite;
box-shadow:0 0 6px rgba(255,79,154,.45);
}
/* Control panel */
.control-wrap{
padding:8px;
display:grid;
grid-template-rows:auto auto auto;
gap:8px;
}
.sys-state{
border:1px solid #727e8e;
background:linear-gradient(180deg,#0f2e4f,#0a2038);
padding:5px 8px;
color:#dce9ff;
font-size:11px;
}
.sys-line{
display:grid;grid-template-columns:1fr auto;gap:8px;
padding:1px 0;align-items:center;
}
.dot{
width:8px;height:8px;border-radius:50%;display:inline-block;margin-right:8px;
background:#44d34d;box-shadow:0 0 8px rgba(68,211,77,.7);
}
.sys-ok{color:#69d86f;font-weight:900}
.ctrl-btn-row{
display:grid;grid-template-columns:repeat(4,1fr);gap:5px;
}
.mini-btn{
height:36px;border:1px solid #1a2a45;
font-size:10px;font-weight:700;cursor:pointer;
color:#cfd3d8;letter-spacing:.5px;
background:#030814;
transition:background .15s,color .15s,border-color .15s;
}
.mini-btn:hover{background:#11203a;color:#fff;border-color:#5af9ff}
.mini-btn:disabled,.big-btn:disabled{opacity:.4;cursor:not-allowed}
.mini-btn.red{color:#ff4f9a;border-color:rgba(255,79,154,.5);background:linear-gradient(135deg,rgba(255,79,154,.1),rgba(255,79,154,.02))}
.mini-btn.red:hover{border-color:#ff4f9a;color:#fff;background:linear-gradient(135deg,rgba(255,79,154,.22),rgba(255,79,154,.08))}
.mini-btn.auto{color:#7cff3a;border-color:rgba(124,255,58,.5);background:linear-gradient(135deg,rgba(124,255,58,.1),rgba(124,255,58,.02))}
.mini-btn.auto:hover{border-color:#7cff3a;color:#fff;background:linear-gradient(135deg,rgba(124,255,58,.22),rgba(124,255,58,.08))}
.speaker-grill{
border:1px solid #7b899a;background:#d7dce3;padding:6px;
display:grid;grid-template-columns:repeat(24,1fr);gap:2px;align-content:start;
max-height:42px;overflow:hidden;
}
.speaker-grill span{
width:3px;height:3px;border-radius:50%;background:#4d5766;display:block;transition:background .15s;
}
.speaker-grill.active span{background:#f4c84b;animation:speakerPulse .5s steps(2,start) infinite}
@keyframes speakerPulse{50%{background:#d93b45}}
.bottom-controls{
display:grid;grid-template-columns:repeat(4,1fr);gap:6px;padding-top:2px;
}
.big-btn{
height:42px;border:1px solid #1a2a45;background:#030814;
color:#cfd3d8;font-size:10px;font-weight:700;cursor:pointer;letter-spacing:.6px;
display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;
transition:background .15s,color .15s,border-color .15s;
}
.big-btn:hover{background:#11203a;color:#fff;border-color:#5af9ff}
.big-btn .ico{font-size:14px;line-height:1}
/* Bottom nav */
.footer{
display:grid;
grid-template-columns:60px repeat(6,1fr);
background:linear-gradient(180deg,#0d2f58,#08213f);
border-top:2px solid #0f4a86;
color:#dfeeff;
font-weight:800;
}
.nav-item{
display:flex;align-items:center;justify-content:center;gap:10px;
border-right:1px solid rgba(255,255,255,.25);
font-size:13px;
}
.nav-item.active{
background:linear-gradient(180deg,#1658a2,#0e447e);
color:#fff;
}
.footer .nav-item{pointer-events:none;cursor:default}
.home-cell{font-size:24px}
/* EMERGENCY panel */
.emergency-wrap{
padding:6px;
}
.fire-alarm-btn{
width:100%;
display:flex;align-items:center;gap:8px;
padding:8px 12px;
background:linear-gradient(135deg,rgba(255,79,154,.12),rgba(255,79,154,.02) 60%,transparent);
border:1px solid rgba(255,79,154,.55);
color:#ff8eb6;
font-size:12px;font-weight:700;letter-spacing:1px;
cursor:pointer;
border-radius:3px;
box-shadow:inset 0 0 14px rgba(255,79,154,.10),0 0 8px rgba(255,79,154,.20);
transition:background .2s,border-color .2s,color .2s,box-shadow .2s;
font-family:'Segoe UI',sans-serif;
}
.fire-alarm-btn:hover{
background:linear-gradient(135deg,rgba(255,79,154,.25),rgba(255,79,154,.08) 60%,transparent);
border-color:#ff4f9a;color:#fff;
box-shadow:inset 0 0 22px rgba(255,79,154,.22),0 0 22px rgba(255,79,154,.55);
}
.fire-alarm-btn:disabled{opacity:.45;cursor:not-allowed}
.fire-alarm-btn .ico{font-size:18px;line-height:1;filter:drop-shadow(0 0 4px rgba(255,79,154,.6))}
.fire-alarm-btn .lbl{flex:1;text-align:left}
.fire-alarm-btn .dot{
width:8px;height:8px;border-radius:50%;
background:#ff4f9a;
box-shadow:0 0 6px rgba(255,79,154,.8);
animation:emergencyDot 1.2s steps(2,start) infinite;
}
@keyframes emergencyDot{50%{opacity:.35}}
/* Fire alarm toast */
.fire-toast{
position:fixed;top:48px;right:18px;z-index:8500;
background:linear-gradient(180deg,#3a0d18,#1f0410);
border:1px solid #ff4f9a;
box-shadow:0 0 22px rgba(255,79,154,.55),inset 0 0 0 1px rgba(255,79,154,.18);
padding:11px 14px;
display:flex;align-items:center;gap:10px;
min-width:280px;max-width:340px;
color:#fff;font-family:'Segoe UI',sans-serif;
transform:translateX(140%);opacity:0;
transition:transform .35s cubic-bezier(.25,.8,.25,1),opacity .25s;
cursor:pointer;
}
.fire-toast.show{transform:translateX(0);opacity:1}
.fire-toast .ico{
width:34px;height:34px;border-radius:50%;
background:#ff4f9a;
display:flex;align-items:center;justify-content:center;
font-size:18px;flex-shrink:0;
box-shadow:0 0 14px rgba(255,79,154,.75);
animation:toastBlink .55s steps(2,start) infinite;
}
.fire-toast .info{flex:1;min-width:0}
.fire-toast .head{color:#ff8eb6;font-size:10px;font-weight:700;letter-spacing:1.8px}
.fire-toast .loc{color:#fff;font-size:13px;font-weight:700;font-family:Consolas,monospace;margin-top:1px}
.fire-toast .hint{font-size:9px;color:#cfd3d8;margin-top:3px;letter-spacing:.5px;opacity:.78}
@keyframes toastBlink{50%{opacity:.45}}
/* Fire alarm modal */
.fire-modal-backdrop{
display:none;position:fixed;inset:0;z-index:9000;
background:rgba(3,8,20,.78);
backdrop-filter:blur(2px);
align-items:center;justify-content:center;
padding:24px;
}
.fire-modal-backdrop.active{display:flex}
.fire-modal{
width:min(900px,96%);
max-height:92vh;
background:#050a18;
border:1px solid #ff4f9a;
box-shadow:0 0 40px rgba(255,79,154,.3),inset 0 0 0 1px rgba(255,79,154,.15);
display:flex;flex-direction:column;
font-family:'Segoe UI',sans-serif;color:#cfd3d8;
}
.fire-modal .modal-head{
display:flex;align-items:center;gap:12px;
padding:12px 16px;
background:linear-gradient(180deg,#3a0d18,#1f0410);
border-bottom:1px solid #ff4f9a;
}
.fire-modal .modal-severity{
padding:3px 10px;
background:#ff4f9a;color:#000;
font-size:11px;font-weight:800;letter-spacing:1.5px;
}
.fire-modal .modal-code{
font-family:Consolas,monospace;color:#5af9ff;font-size:13px;font-weight:700;letter-spacing:1px;
}
.fire-modal .modal-title{
flex:1;
color:#fff;font-size:15px;font-weight:700;letter-spacing:.5px;
}
.fire-modal .modal-close{
width:30px;height:30px;border:1px solid #1a2a45;background:#030814;color:#cfd3d8;
cursor:pointer;font-size:18px;line-height:1;
transition:background .15s,color .15s;
}
.fire-modal .modal-close:hover{background:#11203a;color:#fff}
.fire-modal .modal-loc{
display:flex;align-items:center;gap:8px;
padding:8px 16px;
background:#020610;
border-bottom:1px solid #1a2a45;
font-size:12px;color:#cfd3d8;
}
.fire-modal .modal-loc .loc-pin{color:#ff4f9a}
.fire-modal .modal-loc b{color:#fff;font-family:Consolas,monospace}
.fire-modal .modal-loc .loc-time{margin-left:auto;color:#5af9ff;font-family:Consolas,monospace;font-size:11px}
.fire-modal .modal-body{
display:grid;
grid-template-columns:1.6fr 1fr;
gap:12px;padding:14px 16px;
}
.modal-cctv{
display:flex;flex-direction:column;gap:6px;
}
.cctv-bar{
display:flex;align-items:center;gap:10px;
padding:6px 10px;
background:#0a1c33;
border:1px solid #1a2a45;
font-size:11px;font-family:Consolas,monospace;
}
.cctv-rec{color:#ff4f9a;font-weight:700;letter-spacing:1px}
.cctv-cam{color:#5af9ff;letter-spacing:.5px}
.cctv-time{margin-left:auto;color:#cfd3d8}
.cctv-screen{
position:relative;
aspect-ratio:16/9;
background:#020610;
border:1px solid #1a2a45;
overflow:hidden;
display:flex;align-items:center;justify-content:center;
}
.cctv-screen video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:none;background:#020610}
.cctv-screen video.cctv-active{display:block}
.cctv-placeholder{
text-align:center;color:#5a6678;
font-family:Consolas,monospace;
letter-spacing:2px;
}
.cctv-placeholder .cctv-icon{
font-size:36px;color:#3a4252;margin-bottom:6px;
}
.cctv-placeholder .cctv-status{
font-size:11px;color:#5af9ff;letter-spacing:2px;
animation:cctvBlink 1.4s steps(2,start) infinite;
}
@keyframes cctvBlink{50%{opacity:.4}}
.cctv-meta{
display:flex;gap:14px;
padding:5px 10px;
background:#0a1c33;
border:1px solid #1a2a45;
font-size:10px;color:#8590a3;font-family:Consolas,monospace;
}
.cctv-meta .cctv-live{margin-left:auto;color:#ff4f9a;font-weight:700}
.modal-info{
display:flex;flex-direction:column;gap:8px;
}
.info-block{
background:#020610;border:1px solid #1a2a45;
padding:10px;
}
.info-block h4{
margin:0 0 8px 0;font-size:10px;
color:#5af9ff;letter-spacing:1.5px;text-transform:uppercase;
border-bottom:1px solid #1a2a45;padding-bottom:5px;
}
.info-row{
display:grid;grid-template-columns:1fr auto;gap:8px;
padding:3px 0;font-size:11px;
}
.info-row span{color:#8590a3}
.info-row b{color:#fff;font-family:Consolas,monospace}
.info-row b.ok{color:#7cff3a}
.info-row b.alarm{color:#ff4f9a}
.info-row b.sprinkler-pulse{
color:#5af9ff;
animation:sprinklerPulseText 1.1s ease-in-out infinite;
}
@keyframes sprinklerPulseText{
0%,100%{text-shadow:0 0 6px rgba(90,249,255,0.45);opacity:0.85}
50%{text-shadow:0 0 14px rgba(90,249,255,0.95);opacity:1}
}
.modal-msg{
margin:0 16px 14px;
padding:10px 12px;
background:#020610;border-left:3px solid #ff4f9a;
font-size:12px;color:#cfd3d8;
}
.fire-modal .modal-foot{
display:flex;gap:8px;justify-content:flex-end;
padding:10px 16px;
background:#0a1c33;
border-top:1px solid #1a2a45;
}
.fire-modal .modal-btn{
padding:8px 18px;
background:#030814;border:1px solid #1a2a45;color:#cfd3d8;
font-size:11px;font-weight:700;letter-spacing:1px;cursor:pointer;
transition:background .15s,color .15s,border-color .15s;
}
.fire-modal .modal-btn:hover{background:#11203a;color:#fff;border-color:#5af9ff}
.fire-modal .modal-btn.ack{
color:#ff4f9a;border-color:rgba(255,79,154,.5);
background:linear-gradient(135deg,rgba(255,79,154,.1),rgba(255,79,154,.02));
}
.fire-modal .modal-btn.ack:hover{color:#fff;border-color:#ff4f9a;background:linear-gradient(135deg,rgba(255,79,154,.25),rgba(255,79,154,.08))}
.svg-wrap{width:100%;height:100%}
svg{width:100%;height:100%;display:block}
/* EVACUATION overlay */
.evac-overlay{
display:none;position:absolute;inset:0;pointer-events:none;z-index:9999;
background:rgba(201,29,46,.18);
box-shadow:inset 0 0 0 10px #ff3552;
animation:evacPulse .7s steps(2,start) infinite;
}
.evac-overlay.active{display:block}
.evac-text{
position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
color:#fff;font-size:54px;font-weight:900;letter-spacing:6px;
text-shadow:0 0 18px rgba(255,81,104,.9),0 4px 12px rgba(0,0,0,.85);
white-space:nowrap;
}
@keyframes evacPulse{50%{background:rgba(201,29,46,.42);box-shadow:inset 0 0 0 14px #ffffff}}
@keyframes blinkBg{
0%,100%{filter:brightness(1)}
50%{filter:brightness(.72)}
}
</style>
</head>
<body>
<div class="app">
<div class="evac-overlay" id="evacOverlay"><div class="evac-text">🚨 EVACUATION IN PROGRESS 🚨</div></div>
<!-- Fire detection toast -->
<div class="fire-toast" id="fireToast" onclick="openFireModal(true)">
<div class="ico">🔥</div>
<div class="info">
<div class="head">FIRE DETECTED</div>
<div class="loc">FACTORY 2 / ZONE 09</div>
<div class="hint">클릭 시 CCTV 즉시 열기...</div>
</div>
</div>
<!-- Fire alarm modal -->
<div class="fire-modal-backdrop" id="fireModal">
<div class="fire-modal">
<header class="modal-head">
<span class="modal-severity">CRITICAL</span>
<span class="modal-code">F-09-FIRE</span>
<span class="modal-title">화재 알람 발령 — FIRE ALARM TRIGGERED</span>
<button class="modal-close" id="fireModalClose" onclick="closeFireModal()" title="닫기">×</button>
</header>
<div class="modal-loc">
<span class="loc-pin">📍</span>
<span>위치: <b>FACTORY 2 · ZONE 09 · MACHINE ROOM</b></span>
<span class="loc-time" id="fireModalTime">--:--:--</span>
</div>
<div class="modal-body">
<section class="modal-cctv">
<div class="cctv-bar">
<span class="cctv-rec">● REC</span>
<span class="cctv-cam">CAM-09 / FACTORY 2 / ZONE 09</span>
<span class="cctv-time" id="fireModalCctvTime">--:--:--</span>
</div>
<div class="cctv-screen">
<video class="cctv-video cctv-active" id="cctvVideo1" src="video/fire-1-detected.mp4" muted playsinline preload="auto"></video>
<video class="cctv-video" id="cctvVideo2" src="video/fire-2-suppressed.mp4" muted playsinline preload="auto"></video>
</div>
<div class="cctv-meta">
<span>해상도: 1920×1080</span>
<span>FPS: 30</span>
<span class="cctv-live">● LIVE</span>
</div>
</section>
<section class="modal-info">
<div class="info-block">
<h4>알람 정보</h4>
<div class="info-row"><span>구역</span><b class="alarm">ZONE 09</b></div>
<div class="info-row"><span>건물</span><b>FACTORY 2</b></div>
<div class="info-row"><span>감지 장치</span><b>SMOKE DETECTOR</b></div>
<div class="info-row"><span>알람 코드</span><b>F-09-FIRE</b></div>
</div>
<div class="info-block">
<h4>대응 상태</h4>
<div class="info-row"><span>119 통보</span><b class="ok">전송 완료</b></div>
<div class="info-row"><span>스프링클러</span><b id="modalSprinklerStatus">대기 중</b></div>
<div class="info-row"><span>방화 셔터</span><b id="modalShutterStatus">대기 중</b></div>
<div class="info-row"><span>안전관리팀</span><b class="ok">호출됨</b></div>
</div>
</section>
</div>
<div class="modal-msg">
<b style="color:#ff4f9a">▲ 알람 상세</b><br>
FACTORY 2 / ZONE 09 (MACHINE ROOM) 에서 연기 감지 신호 수신. 자동 비상 시퀀스가 작동 중입니다. 현장 확인 및 작업자 대피 진행 후 ACK 처리하십시오.
</div>
<footer class="modal-foot">
<button class="modal-btn" onclick="closeFireModal()">스킵 (CLOSE)</button>
<button class="modal-btn ack" onclick="ackFireAlarm()">✓ 확인 (ACK)</button>
</footer>
</div>
</div>
<div class="frame">
<!-- TOP -->
<div class="topbar">
<div class="brand">
<span class="menu"></span>
<span>INVYONE</span>
</div>
<div class="header-title" id="headerTitle">FIRE ALARM MONITORING WEB SYSTEM</div>
<div class="clock" id="clock">--:--:-- --/--/----</div>
</div>
<!-- MAIN -->
<div class="main">
<!-- LEFT / MAP -->
<div class="map-area">
<div class="svg-wrap">
<svg viewBox="0 0 1280 820" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="CAD style detailed fire alarm monitoring map">
<defs>
<linearGradient id="screen-bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#06111c" />
<stop offset="1" stop-color="#040b13" />
</linearGradient>
<linearGradient id="board-bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#0c7d3a" />
<stop offset="0.52" stop-color="#086b32" />
<stop offset="1" stop-color="#064c25" />
</linearGradient>
<linearGradient id="building-bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#108b43" />
<stop offset="0.58" stop-color="#087033" />
<stop offset="1" stop-color="#04461f" />
</linearGradient>
<linearGradient id="building-bg-utility" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#1c8aa8" />
<stop offset="0.58" stop-color="#155f73" />
<stop offset="1" stop-color="#082c38" />
</linearGradient>
<linearGradient id="building-bg-service" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#c47018" />
<stop offset="0.58" stop-color="#8a4d0c" />
<stop offset="1" stop-color="#4d2806" />
</linearGradient>
<linearGradient id="building-bg-factory1" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#7c44b8" />
<stop offset="0.58" stop-color="#5a3088" />
<stop offset="1" stop-color="#2c1448" />
</linearGradient>
<linearGradient id="building-bg-office" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#5a6678" />
<stop offset="0.58" stop-color="#3e4858" />
<stop offset="1" stop-color="#232a36" />
</linearGradient>
<linearGradient id="core-room" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#0a6630" />
<stop offset="1" stop-color="#03391c" />
</linearGradient>
<radialGradient id="sensor-face" cx="34%" cy="28%" r="75%">
<stop offset="0" stop-color="#ffffff" />
<stop offset="0.35" stop-color="#c2c7d0" />
<stop offset="0.7" stop-color="#6b737e" />
<stop offset="1" stop-color="#303744" />
</radialGradient>
<filter id="soft-shadow" x="-18%" y="-18%" width="136%" height="136%">
<feDropShadow dx="0" dy="4" stdDeviation="4" flood-color="#000000" flood-opacity="0.42" />
</filter>
<filter id="alarm-glow" x="-90%" y="-90%" width="280%" height="280%">
<feGaussianBlur stdDeviation="4" result="blur" />
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<symbol id="sensor" viewBox="-14 -14 28 28">
<circle class="sensor-outer" r="10" />
<circle class="sensor-dot" r="3.2" />
</symbol>
<symbol id="manual-call" viewBox="-10 -10 20 20">
<rect x="-8" y="-8" width="16" height="16" rx="2" class="call-box" />
<circle r="3.3" class="call-dot" />
</symbol>
<symbol id="door-leaf" viewBox="0 0 28 20">
<rect x="0" y="10" width="18" height="8" class="door" />
<path d="M18 10A18 18 0 0 0 0 0" class="door-arc" />
</symbol>
<symbol id="panel-device" viewBox="0 0 28 48">
<rect x="1" y="1" width="26" height="46" rx="2" class="device" />
<path d="M6 8H22M6 15H22M6 22H22M6 29H22M6 36H22" class="device-line" />
<circle cx="9" cy="42" r="2" class="device-led" />
<circle cx="15" cy="42" r="2" class="device-led" />
<circle cx="21" cy="42" r="2" class="device-led warn-led" />
</symbol>
<symbol id="stairs" viewBox="0 0 42 34">
<rect x="1" y="1" width="40" height="32" class="stair-box" />
<path d="M6 7H36M6 13H36M6 19H36M6 25H36" class="stair-line" />
</symbol>
<symbol id="beacon" viewBox="0 0 100 100" overflow="visible">
<!-- 펄스 링 1 (외부 확산) -->
<circle cx="50" cy="50" r="32" fill="none" stroke="var(--b-pulse)" stroke-width="3" opacity="0">
<animate attributeName="r" values="24;52" dur="1.4s" repeatCount="indefinite" />
<animate attributeName="opacity" values=".9;0" dur="1.4s" repeatCount="indefinite" />
<animate attributeName="stroke-width" values="3;0.6" dur="1.4s" repeatCount="indefinite" />
</circle>
<!-- 펄스 링 2 (시간 지연) -->
<circle cx="50" cy="50" r="32" fill="none" stroke="var(--b-pulse)" stroke-width="3" opacity="0">
<animate attributeName="r" values="24;52" dur="1.4s" begin="0.7s" repeatCount="indefinite" />
<animate attributeName="opacity" values=".9;0" dur="1.4s" begin="0.7s" repeatCount="indefinite" />
<animate attributeName="stroke-width" values="3;0.6" dur="1.4s" begin="0.7s" repeatCount="indefinite" />
</circle>
<!-- 메인 원형 배지 (외곽) -->
<circle cx="50" cy="50" r="32" fill="var(--b-ring-fill)" stroke="var(--b-ring-stroke)" stroke-width="2.4" />
<!-- 메인 원형 배지 (내부) -->
<circle cx="50" cy="50" r="26" fill="var(--b-core-fill)" stroke="var(--b-core-stroke)" stroke-width="1.6" filter="url(#alarm-glow)" />
<!-- 경고 삼각형 -->
<path d="M50 30 L72 64 L28 64 Z" fill="var(--b-tri-fill)" stroke="var(--b-tri-stroke)" stroke-width="3.4" stroke-linejoin="round" stroke-linecap="round" />
<!-- 느낌표 -->
<line x1="50" y1="42" x2="50" y2="55" stroke="var(--b-mark)" stroke-width="3.4" stroke-linecap="round" />
<circle cx="50" cy="60" r="2.2" fill="var(--b-mark)" />
</symbol>
<style>
.screen { fill: url(#screen-bg); }
.board { fill: url(#board-bg); stroke: #edf9ff; stroke-width: 2.2; }
.board-inner { fill: none; stroke: rgba(255,255,255,.42); stroke-width: 1; }
.grid { stroke: rgba(255,255,255,.105); stroke-width: .65; }
.minor-grid { stroke: rgba(255,255,255,.055); stroke-width: .45; }
.road { fill: none; stroke: #f0fbff; stroke-width: 2.2; opacity: .9; }
.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.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; }
.wall-strong { stroke: #ffffff; stroke-width: 1.35; opacity: .84; }
.detail { stroke: #dff4ff; stroke-width: .65; opacity: .43; }
.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.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: #ffcf4e; stroke-width: 2; stroke-dasharray: 6 4; }
.zone.alarm .zone-area { fill: rgba(255,56,82,.42); stroke: #ff3852; stroke-width: 2.6; stroke-dasharray: 8 4; 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: 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; }
.sensor-dot { fill: #ffffff; }
.zone.warn .sensor-outer { fill: #f0c644; stroke: #ffffff; filter: url(#alarm-glow); }
.zone.alarm .sensor-outer { fill: #ff3552; stroke: #ffffff; filter: url(#alarm-glow); animation: alarmPulse .82s infinite; }
.call-box { fill: #203247; stroke: #f3fbff; stroke-width: 1.2; }
.call-dot { fill: #d93b45; stroke: #fff; stroke-width: .8; }
.door { fill: #727c8a; stroke: #f5fbff; stroke-width: 1; }
.door-arc { fill: none; stroke: rgba(255,255,255,.62); stroke-width: .8; }
.device { fill: #0a1d31; stroke: #f2fbff; stroke-width: 1; }
.device-line { stroke: rgba(230,247,255,.55); stroke-width: .8; }
.device-led { fill: #31d45b; }
.warn-led { fill: #f4c84b; }
.stair-box { fill: rgba(5,42,23,.55); stroke: #eef9ff; stroke-width: .9; }
.stair-line { stroke: rgba(238,249,255,.68); stroke-width: .8; }
.badge { fill: #e9eef6; stroke: #152233; stroke-width: 1; }
.badge-text { font-family: Arial, 'Noto Sans KR', sans-serif; font-size: 11px; font-weight: 900; fill: #172232; text-anchor: middle; }
.legend-box { fill: #071a31; stroke: #d6ebff; stroke-width: 1; }
.legend-text { font-family: Arial, 'Noto Sans KR', sans-serif; font-size: 8px; font-weight: 800; fill: #ffffff; }
@keyframes alarmPulse { 0%,100% { opacity: 1; } 50% { opacity: .38; } }
/* === 경고 인디케이터 (펄스 + 배지 + 경고 삼각형) === */
.zone-beacon {
visibility: hidden; opacity: 0; pointer-events: none;
transition: opacity .2s ease-out; overflow: visible;
}
/* WARN — 노란/주황 톤 (주의) */
.zone.warn .zone-beacon {
visibility: visible; opacity: .95;
--b-pulse: #ffcf4e;
--b-ring-fill: rgba(255,207,78,0.18);
--b-ring-stroke: #ffcf4e;
--b-core-fill: rgba(255,180,40,0.55);
--b-core-stroke: #ffe089;
--b-tri-fill: rgba(120,80,0,0.4);
--b-tri-stroke: #1f1300;
--b-mark: #1f1300;
}
/* ALARM — 빨간 톤 + 깜빡임 (위험) */
.zone.alarm .zone-beacon {
visibility: visible; opacity: 1;
--b-pulse: #ff3852;
--b-ring-fill: rgba(255,56,82,0.22);
--b-ring-stroke: #ff3852;
--b-core-fill: rgba(255,56,82,0.65);
--b-core-stroke: #ff5878;
--b-tri-fill: rgba(180,20,40,0.55);
--b-tri-stroke: #ffea00;
--b-mark: #ffea00;
animation: beaconAlarmFlash .55s ease-in-out infinite alternate;
}
@keyframes beaconAlarmFlash {
from { filter: drop-shadow(0 0 4px #ff3852) brightness(1); }
to { filter: drop-shadow(0 0 18px #ff3852) brightness(1.25); }
}
</style>
</defs>
<rect width="1280" height="820" class="screen" />
<!-- map board -->
<rect x="16" y="12" width="1248" height="736" class="board" />
<rect x="28" y="24" width="1224" height="712" class="board-inner" />
<!-- CAD grid -->
<g>
<path class="minor-grid" d="M44 24V736 M64 24V736 M84 24V736 M104 24V736 M124 24V736 M144 24V736 M164 24V736 M184 24V736 M204 24V736 M224 24V736 M244 24V736 M264 24V736 M284 24V736 M304 24V736 M324 24V736 M344 24V736 M364 24V736 M384 24V736 M404 24V736 M424 24V736 M444 24V736 M464 24V736 M484 24V736 M504 24V736 M524 24V736 M544 24V736 M564 24V736 M584 24V736 M604 24V736 M624 24V736 M644 24V736 M664 24V736 M684 24V736 M704 24V736 M724 24V736 M744 24V736 M764 24V736 M784 24V736 M804 24V736 M824 24V736 M844 24V736 M864 24V736 M884 24V736 M904 24V736 M924 24V736 M944 24V736 M964 24V736 M984 24V736 M1004 24V736 M1024 24V736 M1044 24V736 M1064 24V736 M1084 24V736 M1104 24V736 M1124 24V736 M1144 24V736 M1164 24V736 M1184 24V736 M1204 24V736 M1224 24V736" />
<path class="minor-grid" d="M28 44H1252 M28 64H1252 M28 84H1252 M28 104H1252 M28 124H1252 M28 144H1252 M28 164H1252 M28 184H1252 M28 204H1252 M28 224H1252 M28 244H1252 M28 264H1252 M28 284H1252 M28 304H1252 M28 324H1252 M28 344H1252 M28 364H1252 M28 384H1252 M28 404H1252 M28 424H1252 M28 444H1252 M28 464H1252 M28 484H1252 M28 504H1252 M28 524H1252 M28 544H1252 M28 564H1252 M28 584H1252 M28 604H1252 M28 624H1252 M28 644H1252 M28 664H1252 M28 684H1252 M28 704H1252 M28 724H1252" />
<path class="grid" d="M44 24V736 M84 24V736 M124 24V736 M164 24V736 M204 24V736 M244 24V736 M284 24V736 M324 24V736 M364 24V736 M404 24V736 M444 24V736 M484 24V736 M524 24V736 M564 24V736 M604 24V736 M644 24V736 M684 24V736 M724 24V736 M764 24V736 M804 24V736 M844 24V736 M884 24V736 M924 24V736 M964 24V736 M1004 24V736 M1044 24V736 M1084 24V736 M1124 24V736 M1164 24V736 M1204 24V736" />
<path class="grid" d="M28 64H1252 M28 104H1252 M28 144H1252 M28 184H1252 M28 224H1252 M28 264H1252 M28 304H1252 M28 344H1252 M28 384H1252 M28 424H1252 M28 464H1252 M28 504H1252 M28 544H1252 M28 584H1252 M28 624H1252 M28 664H1252 M28 704H1252" />
</g>
<!-- road / evacuation routes -->
<path d="M30 286H1242" class="road" />
<path d="M704 144V652" class="road" />
<path d="M22 260H1224" class="road-dash" />
<path d="M22 314H1224" class="road-dash" />
<path d="M678 34V676" class="road-dash" />
<path d="M730 34V676" class="road-dash" />
<path d="M836 224C842 214 854 208 868 208H1230C1242 208 1250 216 1250 230V650" class="route" />
<path d="M246 220C254 208 268 202 286 202H830C842 202 848 210 848 222V680" class="route" />
<path d="M74 646H1180" class="route" />
<!-- Wastewater -->
<g id="zone-01" class="zone" data-zone="ZONE 01">
<rect x="48" y="44" width="158" height="90" class="building bg-utility" />
<rect x="56" y="52" width="142" height="74" class="zone-area za-utility" />
<path d="M56 88H198 M96 52V126 M148 52V126" class="detail" />
<path d="M62 56V126 M70 56V126 M78 56V126 M184 56V126" class="hatch" />
<text x="127" y="70" class="building-name" style="font-size:16px;stroke-width:2.6px">WASTEWATER</text>
<use href="#sensor" x="110" y="78" width="34" height="34" />
<use href="#manual-call" x="79" y="80" width="24" height="24" />
<text x="127" y="130" class="zone-label">ZONE 01</text>
</g>
<!-- SBG -->
<g id="zone-02" class="zone" data-zone="ZONE 02">
<rect x="48" y="140" width="158" height="92" class="building bg-utility" />
<rect x="56" y="148" width="142" height="76" class="zone-area za-utility" />
<path d="M56 184H198 M100 148V224 M152 148V224" class="detail" />
<path d="M62 152V224 M70 152V224 M78 152V224 M184 152V224" class="hatch" />
<text x="127" y="164" class="building-name" style="font-size:15px;stroke-width:2.6px">SBG TREATMENT</text>
<use href="#sensor" x="110" y="174" width="34" height="34" />
<use href="#manual-call" x="163" y="176" width="24" height="24" />
<text x="127" y="226" class="zone-label">ZONE 02</text>
</g>
<!-- Utility building -->
<g>
<rect x="246" y="44" width="560" height="164" class="building bg-utility" />
<rect x="256" y="54" width="540" height="144" class="building-inset" />
<text x="526" y="74" class="building-name">UTILITY BUILDING</text>
<line x1="386" y1="96" x2="386" y2="198" class="wall" />
<line x1="526" y1="96" x2="526" y2="198" class="wall" />
<line x1="666" y1="96" x2="666" y2="198" class="wall" />
<line x1="256" y1="96" x2="796" y2="96" class="wall" />
<line x1="324" y1="96" x2="324" y2="198" class="detail" />
<line x1="456" y1="96" x2="456" y2="198" class="detail" />
<line x1="596" y1="96" x2="596" y2="198" class="detail" />
<line x1="736" y1="96" x2="736" y2="198" class="detail" />
<path d="M270 112h82 M270 128h82 M270 144h82 M270 160h82" class="detail-light" />
<path d="M406 112h90 M406 128h90 M406 144h90 M406 160h90" class="detail-light" />
<path d="M548 112h96 M548 128h96 M548 144h96 M548 160h96" class="detail-light" />
<path d="M688 112h86 M688 128h86 M688 144h86 M688 160h86" class="detail-light" />
<g id="zone-03" class="zone" data-zone="ZONE 03">
<rect x="256" y="96" width="130" height="102" class="zone-area za-utility" />
<text x="321" y="180" class="zone-label">ZONE 03</text>
<use href="#sensor" x="304" y="106" width="34" height="34" />
<use href="#manual-call" x="256" y="170" width="24" height="24" />
</g>
<g id="zone-04" class="zone" data-zone="ZONE 04">
<rect x="386" y="96" width="140" height="102" class="zone-area za-utility" />
<text x="456" y="180" class="zone-label">ZONE 04</text>
<use href="#sensor" x="439" y="106" width="34" height="34" />
<use href="#manual-call" x="389" y="170" width="24" height="24" />
</g>
<g id="zone-05" class="zone" data-zone="ZONE 05">
<rect x="526" y="96" width="140" height="102" class="zone-area za-utility" />
<text x="596" y="180" class="zone-label">ZONE 05</text>
<use href="#sensor" x="579" y="106" width="34" height="34" />
<use href="#manual-call" x="530" y="170" width="24" height="24" />
</g>
<g id="zone-06" class="zone" data-zone="ZONE 06">
<rect x="666" y="96" width="130" height="102" class="zone-area za-utility" />
<text x="731" y="180" class="zone-label">ZONE 06</text>
<use href="#sensor" x="714" y="106" width="34" height="34" />
<use href="#manual-call" x="668" y="170" width="24" height="24" />
</g>
<use href="#door-leaf" x="310" y="197" width="28" height="20" />
<use href="#door-leaf" x="444" y="197" width="28" height="20" />
<use href="#door-leaf" x="584" y="197" width="28" height="20" />
<use href="#door-leaf" x="736" y="197" width="28" height="20" />
</g>
<!-- Cafeteria -->
<g id="zone-07" class="zone" data-zone="ZONE 07">
<rect x="844" y="58" width="178" height="124" class="building bg-service" />
<rect x="856" y="70" width="154" height="100" class="zone-area za-service" />
<path d="M868 126h130 M916 70v100 M958 70v100" class="detail" />
<path d="M868 86h130 M868 102h130 M868 142h130" class="detail-light" />
<text x="933" y="106" class="building-name" style="font-size:17px;stroke-width:2.8px">CAFETERIA</text>
<use href="#sensor" x="916" y="121" width="34" height="34" />
<use href="#manual-call" x="861" y="145" width="24" height="24" />
<text x="933" y="170" class="zone-label">ZONE 07</text>
<use href="#door-leaf" x="904" y="170" width="28" height="20" />
<use href="#door-leaf" x="964" y="170" width="28" height="20" />
</g>
<!-- Safety center -->
<g id="zone-08" class="zone" data-zone="ZONE 08">
<rect x="1040" y="58" width="98" height="124" class="building bg-service" />
<rect x="1048" y="70" width="82" height="100" class="zone-area za-service" />
<path d="M1068 70v100 M1048 124h82" class="detail" />
<path d="M1078 70v100 M1088 70v100" class="detail-light" />
<text x="1089" y="106" class="building-name" style="font-size:13px;stroke-width:2.4px">SAFETY CENTER</text>
<use href="#sensor" x="1072" y="125" width="34" height="34" />
<use href="#manual-call" x="1050" y="144" width="24" height="24" />
<text x="1089" y="170" class="zone-label">ZONE 08</text>
<use href="#door-leaf" x="1070" y="170" width="28" height="20" />
</g>
<!-- crossing marks -->
<g opacity=".86">
<path d="M766 182h22 M766 188h22 M766 194h22 M766 200h22 M766 206h22" stroke="#eef8ff" stroke-width="2" />
<path d="M1118 186h22 M1118 192h22 M1118 198h22 M1118 204h22 M1118 210h22" stroke="#eef8ff" stroke-width="2" />
<path d="M756 648h22 M756 654h22 M756 660h22 M756 666h22 M756 672h22" stroke="#eef8ff" stroke-width="2" />
</g>
<!-- floor badge -->
<g transform="translate(704 254)">
<rect x="-26" y="-9" width="52" height="16" rx="3" class="badge" />
<text y="2" class="badge-text" style="font-size:7.5px">1st Floor</text>
</g>
<!-- Factory 2 -->
<g>
<rect x="60" y="306" width="620" height="322" class="building" />
<rect x="72" y="318" width="596" height="298" class="building-inset" />
<text x="370" y="358" class="building-name">FACTORY 2</text>
<line x1="210" y1="392" x2="210" y2="620" class="wall" />
<line x1="360" y1="392" x2="360" y2="620" class="wall" />
<line x1="510" y1="392" x2="510" y2="620" class="wall" />
<line x1="60" y1="506" x2="680" y2="506" class="wall" />
<line x1="60" y1="392" x2="680" y2="392" class="wall" />
<path d="M88 406h92 M88 422h92 M88 438h92 M88 454h92 M88 470h92" class="detail-light" />
<path d="M228 406h104 M228 422h104 M228 438h104 M228 454h104" class="detail-light" />
<path d="M378 406h104 M378 422h104 M378 438h104 M378 454h104" class="detail-light" />
<path d="M528 406h122 M528 422h122 M528 438h122 M528 454h122 M528 470h122" class="detail-light" />
<path d="M88 520h92 M88 536h92 M88 552h92 M88 568h92" class="detail-light" />
<path d="M228 520h104 M228 536h104 M228 552h104 M228 568h104" class="detail-light" />
<path d="M378 520h104 M378 536h104 M378 552h104 M378 568h104" class="detail-light" />
<path d="M528 520h122 M528 536h122 M528 552h122 M528 568h122" class="detail-light" />
<path d="M118 392v114 M160 392v114 M252 392v114 M300 392v114 M402 392v114 M450 392v114 M566 392v114 M616 392v114" class="detail" />
<path d="M118 506v114 M160 506v114 M252 506v114 M300 506v114 M402 506v114 M450 506v114 M566 506v114 M616 506v114" class="detail" />
<path d="M160 430h26 M300 430h26 M450 430h26 M612 430h26 M160 544h26 M300 544h26 M450 544h26 M612 544h26" class="detail" />
<g class="room-label">
<text x="86" y="384">MACHINE ROOM</text>
<text x="234" y="384">CNC</text>
<text x="386" y="384">PM OFFICE</text>
<text x="540" y="384">LOGISTICS</text>
<text x="86" y="520">RAW MATERIAL</text>
<text x="232" y="520">ASSEMBLY</text>
<text x="382" y="520">INSPECTION</text>
<text x="538" y="520">PACKING</text>
</g>
<use href="#stairs" x="72" y="582" width="42" height="34" opacity=".88" />
<use href="#panel-device" x="628" y="324" width="24" height="42" opacity=".86" />
<g id="zone-09" class="zone" data-zone="ZONE 09">
<rect x="60" y="392" width="150" height="114" class="zone-area" />
<text x="135" y="461" class="zone-label">ZONE 09</text>
<use href="#sensor" x="118" y="417" width="34" height="34" />
<use href="#manual-call" x="69" y="477" width="24" height="24" />
</g>
<g id="zone-10" class="zone" data-zone="ZONE 10">
<rect x="210" y="392" width="150" height="114" class="zone-area" />
<text x="285" y="461" class="zone-label">ZONE 10</text>
<use href="#sensor" x="268" y="417" width="34" height="34" />
<use href="#manual-call" x="219" y="477" width="24" height="24" />
</g>
<g id="zone-11" class="zone" data-zone="ZONE 11">
<rect x="360" y="392" width="150" height="114" class="zone-area" />
<text x="435" y="461" class="zone-label">ZONE 11</text>
<use href="#sensor" x="418" y="417" width="34" height="34" />
<use href="#manual-call" x="368" y="476" width="24" height="24" />
</g>
<g id="zone-12" class="zone" data-zone="ZONE 12">
<rect x="510" y="392" width="170" height="114" class="zone-area" />
<text x="595" y="461" class="zone-label">ZONE 12</text>
<use href="#sensor" x="578" y="417" width="34" height="34" />
<use href="#manual-call" x="519" y="477" width="24" height="24" />
</g>
<g id="zone-13" class="zone" data-zone="ZONE 13">
<rect x="60" y="506" width="150" height="122" class="zone-area" />
<text x="135" y="579" class="zone-label">ZONE 13</text>
<use href="#sensor" x="118" y="535" width="34" height="34" />
<use href="#manual-call" x="181" y="597" width="24" height="24" />
</g>
<g id="zone-14" class="zone" data-zone="ZONE 14">
<rect x="210" y="506" width="150" height="122" class="zone-area" />
<text x="285" y="579" class="zone-label">ZONE 14</text>
<use href="#sensor" x="268" y="535" width="34" height="34" />
<use href="#manual-call" x="331" y="597" width="24" height="24" />
</g>
<g id="zone-15" class="zone" data-zone="ZONE 15">
<rect x="360" y="506" width="150" height="122" class="zone-area" />
<text x="435" y="579" class="zone-label">ZONE 15</text>
<use href="#sensor" x="418" y="535" width="34" height="34" />
<use href="#manual-call" x="481" y="597" width="24" height="24" />
</g>
<g id="zone-16" class="zone" data-zone="ZONE 16">
<rect x="510" y="506" width="170" height="122" class="zone-area" />
<text x="595" y="579" class="zone-label">ZONE 16</text>
<use href="#sensor" x="578" y="535" width="34" height="34" />
<use href="#manual-call" x="647" y="597" width="24" height="24" />
</g>
<use href="#door-leaf" x="108" y="618" width="28" height="20" />
<use href="#door-leaf" x="262" y="618" width="28" height="20" />
<use href="#door-leaf" x="412" y="618" width="28" height="20" />
<use href="#door-leaf" x="568" y="618" width="28" height="20" />
</g>
<!-- Office -->
<g>
<rect x="696" y="306" width="96" height="322" class="building bg-office" />
<rect x="704" y="318" width="80" height="298" class="building-inset" />
<text x="744" y="328" class="building-name" style="font-size:13px;stroke-width:2.4px">OFFICE BLDG</text>
<line x1="696" y1="466" x2="792" y2="466" class="wall" />
<g id="zone-17" class="zone" data-zone="ZONE 17">
<rect x="696" y="306" width="96" height="160" class="zone-area za-office" />
<text x="744" y="396" class="zone-label">ZONE 17</text>
<use href="#sensor" x="727" y="353" width="34" height="34" />
<use href="#manual-call" x="701" y="435" width="24" height="24" />
</g>
<g id="zone-18" class="zone" data-zone="ZONE 18">
<rect x="696" y="466" width="96" height="162" class="zone-area za-office" />
<text x="744" y="557" class="zone-label">ZONE 18</text>
<use href="#sensor" x="727" y="513" width="34" height="34" />
<use href="#manual-call" x="701" y="597" width="24" height="24" />
</g>
<use href="#door-leaf" x="730" y="618" width="28" height="20" />
</g>
<!-- Factory 1 -->
<g>
<rect x="906" y="286" width="304" height="350" class="building bg-factory1" />
<rect x="918" y="298" width="280" height="326" class="building-inset" />
<text x="1058" y="340" class="building-name">FACTORY 1</text>
<line x1="1058" y1="382" x2="1058" y2="628" class="wall" />
<line x1="906" y1="506" x2="1210" y2="506" class="wall" />
<line x1="906" y1="382" x2="1210" y2="382" class="wall" />
<path d="M942 394h98 M942 410h98 M942 426h98 M942 442h98" class="detail-light" />
<path d="M1072 394h112 M1072 410h112 M1072 426h112 M1072 442h112" class="detail-light" />
<path d="M942 520h98 M942 536h98 M942 552h98 M942 568h98" class="detail-light" />
<path d="M1072 520h112 M1072 536h112 M1072 552h112 M1072 568h112" class="detail-light" />
<path d="M962 382v124 M1014 382v124 M1092 382v124 M1144 382v124 M962 506v122 M1014 506v122 M1092 506v122 M1144 506v122" class="detail" />
<path d="M990 432h22 M1120 432h22 M990 556h22 M1120 556h22" class="detail" />
<g class="room-label">
<text x="934" y="374">WAREHOUSE</text>
<text x="1082" y="374">LINE-A</text>
<text x="934" y="518">QUALITY</text>
<text x="1082" y="518">SHIPPING</text>
</g>
<use href="#stairs" x="914" y="590" width="42" height="34" opacity=".88" />
<g id="zone-19" class="zone" data-zone="ZONE 19">
<rect x="906" y="382" width="152" height="124" class="zone-area za-factory1" />
<text x="982" y="457" class="zone-label">ZONE 19</text>
<use href="#sensor" x="965" y="413" width="34" height="34" />
<use href="#manual-call" x="913" y="477" width="24" height="24" />
</g>
<g id="zone-20" class="zone" data-zone="ZONE 20">
<rect x="1058" y="382" width="152" height="124" class="zone-area za-factory1" />
<text x="1134" y="457" class="zone-label">ZONE 20</text>
<use href="#sensor" x="1117" y="413" width="34" height="34" />
<use href="#manual-call" x="1179" y="477" width="24" height="24" />
</g>
<g id="zone-21" class="zone" data-zone="ZONE 21">
<rect x="906" y="506" width="152" height="130" class="zone-area za-factory1" />
<text x="982" y="583" class="zone-label">ZONE 21</text>
<use href="#sensor" x="965" y="539" width="34" height="34" />
<use href="#manual-call" x="1027" y="603" width="24" height="24" />
</g>
<g id="zone-22" class="zone" data-zone="ZONE 22">
<rect x="1058" y="506" width="152" height="130" class="zone-area za-factory1" />
<text x="1134" y="583" class="zone-label">ZONE 22</text>
<use href="#sensor" x="1117" y="539" width="34" height="34" />
<use href="#manual-call" x="1179" y="603" width="24" height="24" />
</g>
<use href="#door-leaf" x="948" y="626" width="28" height="20" />
<use href="#door-leaf" x="1058" y="626" width="28" height="20" />
<use href="#door-leaf" x="1118" y="626" width="28" height="20" />
</g>
<!-- corridor labels -->
<g class="small-note">
<text x="72" y="278">MAIN CORRIDOR / FIRE DETECTION LOOP</text>
<text x="780" y="276">SAFETY ACCESS ROUTE</text>
<text x="956" y="654">ALARM AREA / PRODUCTION</text>
<text x="334" y="216">2nd FLOOR SERVICE CORRIDOR</text>
<text x="470" y="280">1st FLOOR EVACUATION ROUTE</text>
</g>
<!-- Legend -->
<g transform="translate(34 706)">
<rect width="540" height="26" class="legend-box" />
<g transform="translate(12 13)">
<use href="#sensor" x="0" y="-7" width="14" height="14" />
<text x="20" y="3" class="legend-text" style="font-size:6.5px">FIRE DETECTOR</text>
</g>
<g transform="translate(132 13)">
<use href="#manual-call" x="0" y="-6" width="12" height="12" />
<text x="18" y="3" class="legend-text" style="font-size:6.5px">MANUAL CALL</text>
</g>
<g transform="translate(238 13)">
<rect x="0" y="-6" width="16" height="12" fill="none" stroke="#eef8ff" stroke-width="1" />
<text x="22" y="3" class="legend-text" style="font-size:6.5px">CONTROL PANEL</text>
</g>
<g transform="translate(355 13)">
<line x1="0" y1="0" x2="22" y2="0" class="road-dash" />
<text x="28" y="3" class="legend-text" style="font-size:6.5px">ZONE BOUNDARY</text>
</g>
<g transform="translate(465 13)">
<rect x="0" y="-6" width="12" height="11" class="door" />
<text x="18" y="3" class="legend-text" style="font-size:6.5px">DOOR</text>
</g>
</g>
</svg>
</div>
</div>
<!-- RIGHT PANEL -->
<div class="right-panel">
<!-- 방재 시스템 -->
<section class="panel">
<div class="panel-title red">방재 시스템</div>
<div class="emergency-wrap">
<button class="fire-alarm-btn" id="btnFireAlarm" onclick="triggerFireAlarm()">
<span class="ico">🔥</span>
<span class="lbl">화재경보 발령</span>
<span class="dot"></span>
</button>
</div>
</section>
<!-- PRESSURE -->
<section class="panel">
<div class="panel-title">SPRINKLER WATER PRESSURE</div>
<div class="pressure-wrap">
<div class="pressure-left">
<table class="pressure-table">
<thead>
<tr><th>POSITION</th><th>PRESSURE(bar)</th></tr>
</thead>
<tbody>
<tr><td>MAIN TANK</td><td id="pressureMain">4.56</td></tr>
<tr><td>SPRINKLER</td><td id="pressureSprinkler">5.51</td></tr>
<tr><td>DRAIN</td><td id="pressureDrain">0.00</td></tr>
</tbody>
</table>
<div class="digital-row">
<div class="lbl">MAIN TANK</div>
<div class="digital" id="digitalMain">4.56</div>
</div>
</div>
<div class="pressure-right">
<table class="spec">
<thead>
<tr><th colspan="2">SPEC (bar)</th></tr>
</thead>
<tbody>
<tr><td>MAIN TANK</td><td>4.5 ~ 6.5</td></tr>
<tr><td>SPRINKLER</td><td>4.5 ~ 6.5</td></tr>
<tr><td>DRAIN</td><td>0.0 ~ 1.0</td></tr>
</tbody>
</table>
<div class="indicator-row">
<div class="indicator normal">NORMAL</div>
<div class="indicator alarm">ALARM</div>
<div class="indicator fault">FAULT</div>
</div>
</div>
</div>
</section>
<!-- SUMMARY -->
<section class="panel">
<div class="panel-title red">ALARM / STATUS</div>
<div class="summary-grid">
<div class="summary-card fire">
<div class="summary-icon">🔥</div>
<div class="summary-label">FIRE ALARM</div>
<div class="summary-value" id="countAlarm">0</div>
</div>
<div class="summary-card fault">
<div class="summary-icon"></div>
<div class="summary-label">FAULT</div>
<div class="summary-value" id="countFault">0</div>
</div>
<div class="summary-card sup">
<div class="summary-icon">i</div>
<div class="summary-label">SUPERVISORY</div>
<div class="summary-value" id="countSup">0</div>
</div>
<div class="summary-card dis">
<div class="summary-icon"></div>
<div class="summary-label">DISABLED</div>
<div class="summary-value" id="countDis">0</div>
</div>
</div>
</section>
<!-- MATRIX -->
<section class="panel">
<div class="panel-title">ZONE MATRIX / ALARM CONTROL</div>
<div class="zone-grid" id="zoneGrid"></div>
</section>
<!-- CONTROL -->
<section class="panel">
<div class="panel-title">ALARM CONTROL PANEL</div>
<div class="control-wrap">
<div class="sys-state">
<div class="sys-line"><span><span class="dot"></span>AC POWER</span><span class="sys-ok">NORMAL</span></div>
<div class="sys-line"><span><span class="dot"></span>BATTERY</span><span class="sys-ok">NORMAL</span></div>
<div class="sys-line"><span><span class="dot"></span>SYSTEM</span><span class="sys-ok">NORMAL</span></div>
</div>
<div class="ctrl-btn-row">
<button class="mini-btn auto" id="btnAuto" onclick="runAutoScenario()">AUTO</button>
<button class="mini-btn" onclick="triggerRandomAlarm()">TEST FIRE</button>
<button class="mini-btn" onclick="triggerRandomWarn()">TEST WARN</button>
<button class="mini-btn red" onclick="resetAll()">RESET</button>
</div>
</div>
</section>
<!-- BOTTOM CONTROL -->
<section class="panel">
<div class="panel-title gray">SYSTEM OPERATION</div>
<div style="padding:8px">
<div class="bottom-controls">
<button class="big-btn" onclick="acknowledgeAll()"><span class="ico"></span><span>ACK</span></button>
<button class="big-btn" onclick="evacuateDemo()"><span class="ico">🏃</span><span>EVACUATION</span></button>
<button class="big-btn" onclick="silenceAlarm()"><span class="ico">🔕</span><span>SILENCE</span></button>
<button class="big-btn" onclick="resetAll()"><span class="ico"></span><span>RESET</span></button>
</div>
<div id="eventLog" style="margin-top:4px;height:38px;overflow:auto;border:1px solid #1a2a45;background:#020610;color:#cfd3d8;font-size:9px;padding:3px;font-family:Consolas,monospace;line-height:1.35"></div>
</div>
</section>
</div>
</div>
<!-- FOOTER -->
<div class="footer">
<div class="nav-item home-cell"></div>
<div class="nav-item active">OVERVIEW</div>
<div class="nav-item">ALARM HISTORY</div>
<div class="nav-item">EVENT LOG</div>
<div class="nav-item">SYSTEM STATUS</div>
<div class="nav-item">REPORT</div>
<div class="nav-item">SETTING</div>
</div>
</div>
</div>
<script>
const TOTAL_ZONES = 22;
const zoneStates = {};
for (let i = 1; i <= TOTAL_ZONES; i++) {
const key = String(i).padStart(2, "0");
zoneStates[key] = "normal";
}
const zoneGrid = document.getElementById("zoneGrid");
const eventLog = document.getElementById("eventLog");
function timeText() {
const d = new Date();
const p = n => String(n).padStart(2, "0");
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
}
function fullTimeText() {
const d = new Date();
const p = n => String(n).padStart(2, "0");
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())} ${p(d.getDate())}/${p(d.getMonth()+1)}/${d.getFullYear()}`;
}
function setClock() {
document.getElementById("clock").textContent = fullTimeText();
}
function addLog(message) {
const row = document.createElement("div");
row.style.padding = "3px 0";
row.style.borderBottom = "1px solid #d3d9e1";
row.innerHTML = `<b style="display:inline-block;width:78px">${timeText()}</b> ${message}`;
eventLog.prepend(row);
while (eventLog.children.length > 14) {
eventLog.removeChild(eventLog.lastChild);
}
}
function renderSpeaker() {
const grill = document.getElementById("speakerGrill");
if (!grill) return;
for (let i = 0; i < 240; i++) {
const dot = document.createElement("span");
grill.appendChild(dot);
}
}
function renderZoneGrid() {
zoneGrid.innerHTML = "";
for (let i = 1; i <= TOTAL_ZONES; i++) {
const key = String(i).padStart(2, "0");
const btn = document.createElement("button");
btn.className = `zone-btn ${zoneStates[key]}`;
btn.textContent = `ZONE ${key}`;
btn.onclick = () => cycleZone(key);
zoneGrid.appendChild(btn);
}
}
function applyZoneVisual(key) {
const group = document.getElementById(`zone-${key}`);
if (!group) return;
group.classList.remove("warn", "alarm");
if (zoneStates[key] === "warn") group.classList.add("warn");
if (zoneStates[key] === "alarm") group.classList.add("alarm");
}
function renderMap() {
for (let i = 1; i <= TOTAL_ZONES; i++) {
const key = String(i).padStart(2, "0");
applyZoneVisual(key);
}
}
function updateCounts() {
let alarm = 0;
let fault = 0;
Object.values(zoneStates).forEach(state => {
if (state === "alarm") alarm++;
if (state === "warn") fault++;
});
document.getElementById("countAlarm").textContent = alarm;
document.getElementById("countFault").textContent = fault;
document.getElementById("countSup").textContent = alarm > 0 ? 1 : 0;
document.getElementById("countDis").textContent = 0;
}
function renderAll() {
renderZoneGrid();
renderMap();
updateCounts();
updateIndicators();
}
function setZoneState(key, state, silent = false) {
zoneStates[key] = state;
if (state === "alarm") silenced = false;
renderAll();
updateSpeaker();
updateAlarmHeader();
updateSiren();
if (!silent) {
if (state === "normal") addLog(`<span style="color:#11722a;font-weight:900">ZONE ${key}</span> restored to NORMAL.`);
if (state === "warn") addLog(`<span style="color:#a26b00;font-weight:900">ZONE ${key}</span> changed to FAULT/WARN.`);
if (state === "alarm") addLog(`<span style="color:#b10017;font-weight:900">ZONE ${key}</span> FIRE ALARM detected.`);
}
}
function cycleZone(key) {
const current = zoneStates[key];
if (current === "normal") setZoneState(key, "warn");
else if (current === "warn") setZoneState(key, "alarm");
else setZoneState(key, "normal");
}
function triggerRandomAlarm() {
const index = String(Math.floor(Math.random() * TOTAL_ZONES) + 1).padStart(2, "0");
setZoneState(index, "alarm");
}
function triggerRandomWarn() {
const index = String(Math.floor(Math.random() * TOTAL_ZONES) + 1).padStart(2, "0");
setZoneState(index, "warn");
}
let autoRunning = false;
let silenced = false;
let pressureMode = "normal";
let pressureLevel = 1;
let audioCtx = null;
let sirenOsc = null;
let sirenInterval = null;
function ensureAudio() {
if (!audioCtx) {
try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch(e) {}
}
if (audioCtx && audioCtx.state === "suspended") audioCtx.resume();
}
function startSiren() {
ensureAudio();
if (!audioCtx || sirenOsc) return;
const gain = audioCtx.createGain();
gain.gain.value = 0.035;
gain.connect(audioCtx.destination);
const osc = audioCtx.createOscillator();
osc.type = "square";
osc.frequency.value = 900;
osc.connect(gain);
osc.start();
sirenOsc = { osc, gain };
let high = true;
sirenInterval = setInterval(() => {
high = !high;
if (sirenOsc) sirenOsc.osc.frequency.value = high ? 1100 : 700;
}, 450);
}
function stopSiren() {
if (sirenOsc) {
try { sirenOsc.osc.stop(); } catch(e) {}
sirenOsc = null;
}
if (sirenInterval) { clearInterval(sirenInterval); sirenInterval = null; }
}
function updateAlarmHeader() {
const topbar = document.querySelector(".topbar");
const title = document.getElementById("headerTitle");
if (!topbar || !title) return;
const alarmZones = Object.keys(zoneStates).filter(k => zoneStates[k] === "alarm");
if (alarmZones.length > 0) {
topbar.classList.add("alarm-active");
title.classList.add("alarm");
title.textContent = `🔥 FIRE ALARM ACTIVE — ZONE ${alarmZones.join(", ")}`;
} else {
topbar.classList.remove("alarm-active");
title.classList.remove("alarm");
title.textContent = "FIRE ALARM MONITORING WEB SYSTEM";
}
}
function updateSiren() {
const hasAlarm = Object.values(zoneStates).some(s => s === "alarm");
if (hasAlarm && !silenced) startSiren(); else stopSiren();
}
function updateIndicators() {
const states = Object.values(zoneStates);
const hasAlarm = states.some(s => s === "alarm");
const hasWarn = states.some(s => s === "warn");
document.querySelectorAll(".indicator").forEach(el => el.classList.remove("active"));
let sel = ".indicator.normal";
if (hasAlarm) sel = ".indicator.alarm";
else if (hasWarn) sel = ".indicator.fault";
const el = document.querySelector(sel);
if (el) el.classList.add("active");
}
function setControlsDisabled(disabled) {
document.querySelectorAll(".mini-btn:not(.auto), .big-btn").forEach(b => { b.disabled = disabled; });
}
function updateSpeaker() {
const grill = document.getElementById("speakerGrill");
if (!grill) return;
const hasAlarm = Object.values(zoneStates).some(s => s === "alarm");
grill.classList.toggle("active", hasAlarm && !silenced);
}
function resetAll(silent = false) {
Object.keys(zoneStates).forEach(k => zoneStates[k] = "normal");
silenced = false;
pressureMode = "recover";
stopSiren();
renderAll();
updateSpeaker();
updateAlarmHeader();
if (!silent) addLog(`<span style="color:#11722a;font-weight:900">SYSTEM RESET</span> all zones returned to NORMAL.`);
}
function acknowledgeAll() {
let ackCount = 0;
Object.keys(zoneStates).forEach(k => {
if (zoneStates[k] === "alarm") { zoneStates[k] = "warn"; ackCount++; }
});
if (ackCount > 0) renderAll();
updateSpeaker();
updateAlarmHeader();
updateSiren();
addLog(`<span style="color:#0e4f93;font-weight:900">ACKNOWLEDGE</span> ${ackCount} alarm(s) acknowledged.`);
}
function silenceAlarm() {
silenced = true;
stopSiren();
updateSpeaker();
addLog(`<span style="color:#444;font-weight:900">SILENCE</span> buzzer silenced.`);
}
function evacuateDemo() {
const overlay = document.getElementById("evacOverlay");
if (overlay) {
overlay.classList.add("active");
setTimeout(() => overlay.classList.remove("active"), 6000);
}
addLog(`<span style="color:#ff8eb6;font-weight:900">EVACUATION</span> emergency evacuation broadcast issued.`);
}
function fmtTime() {
const d = new Date();
const p = n => String(n).padStart(2, "0");
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
}
let fireModalTimer = null;
const CCTV_PLAYBACK_RATE = 0.85;
const CCTV_PHASE_NOMINAL_SEC = 8.0;
const CCTV_PHASE_HARD_STOP_MS = Math.ceil((CCTV_PHASE_NOMINAL_SEC / CCTV_PLAYBACK_RATE) * 1000) + 2000;
let cctvHandlers = [];
let cctvTimers = [];
function cctvAddListener(el, event, fn) {
el.addEventListener(event, fn);
cctvHandlers.push([el, event, fn]);
}
function cctvAddTimer(fn, ms) {
cctvTimers.push(setTimeout(fn, ms));
}
function cctvCleanup() {
cctvHandlers.forEach(([el, ev, fn]) => el.removeEventListener(ev, fn));
cctvHandlers = [];
cctvTimers.forEach(id => clearTimeout(id));
cctvTimers = [];
}
function startCctvSequence() {
const v1 = document.getElementById("cctvVideo1");
const v2 = document.getElementById("cctvVideo2");
if (!v1 || !v2) return;
stopCctvSequence();
const sprinkler = document.getElementById("modalSprinklerStatus");
const shutter = document.getElementById("modalShutterStatus");
if (sprinkler) { sprinkler.textContent = "대기 중"; sprinkler.classList.remove("ok","sprinkler-pulse"); }
if (shutter) { shutter.textContent = "대기 중"; shutter.classList.remove("ok"); }
[v1, v2].forEach(v => {
v.loop = false;
v.playbackRate = CCTV_PLAYBACK_RATE;
try { v.pause(); v.currentTime = 0; } catch(e) {}
});
v1.classList.add("cctv-active");
v2.classList.remove("cctv-active");
let phase2Started = false;
let done = false;
const finalize = () => {
if (done) return;
done = true;
cctvCleanup();
try { v1.pause(); v2.pause(); } catch(e) {}
if (sprinkler) {
sprinkler.classList.remove("sprinkler-pulse");
sprinkler.classList.add("ok");
sprinkler.textContent = "진압 완료";
}
addLog(`<span style="color:#7cff3a;font-weight:900">FIRE SUPPRESSED</span> ZONE 09 area cleared.`);
};
const startPhase2 = () => {
if (phase2Started || done) return;
phase2Started = true;
if (sprinkler) {
sprinkler.textContent = "자동 작동중";
sprinkler.classList.add("sprinkler-pulse");
}
if (shutter) { shutter.textContent = "자동 차단"; shutter.classList.add("ok"); }
addLog(`<span style="color:#7cff3a;font-weight:900">SPRINKLER</span> activated — fire suppression in progress.`);
addLog(`<span style="color:#cfd3d8;font-weight:900">FIRE SHUTTER</span> closed at LINK BRIDGE.`);
try { v1.pause(); } catch(e) {}
v1.classList.remove("cctv-active");
v2.classList.add("cctv-active");
try { v2.currentTime = 0; } catch(e) {}
cctvAddListener(v2, "timeupdate", () => {
const dur = (v2.duration && !isNaN(v2.duration) && isFinite(v2.duration)) ? v2.duration : CCTV_PHASE_NOMINAL_SEC;
if (v2.currentTime >= dur - 0.15) finalize();
});
cctvAddListener(v2, "ended", finalize);
cctvAddTimer(finalize, CCTV_PHASE_HARD_STOP_MS);
v2.play().catch(()=>{});
};
cctvAddListener(v1, "timeupdate", () => {
const dur = (v1.duration && !isNaN(v1.duration) && isFinite(v1.duration)) ? v1.duration : CCTV_PHASE_NOMINAL_SEC;
if (v1.currentTime >= dur - 0.15) startPhase2();
});
cctvAddListener(v1, "ended", startPhase2);
cctvAddTimer(startPhase2, CCTV_PHASE_HARD_STOP_MS);
v1.play().catch(()=>{});
}
function stopCctvSequence() {
cctvCleanup();
const v1 = document.getElementById("cctvVideo1");
const v2 = document.getElementById("cctvVideo2");
[v1, v2].forEach(v => {
if (!v) return;
try { v.pause(); v.currentTime = 0; v.loop = false; } catch(e) {}
});
if (v1) v1.classList.add("cctv-active");
if (v2) v2.classList.remove("cctv-active");
const sprinkler = document.getElementById("modalSprinklerStatus");
if (sprinkler) sprinkler.classList.remove("sprinkler-pulse");
}
function openFireModal(immediate = false) {
const modal = document.getElementById("fireModal");
if (!modal) return;
const toast = document.getElementById("fireToast");
if (toast) toast.classList.remove("show");
if (fireModalTimer) { clearTimeout(fireModalTimer); fireModalTimer = null; }
modal.classList.add("active");
document.getElementById("fireModalTime").textContent = fmtTime();
document.getElementById("fireModalCctvTime").textContent = fmtTime();
startCctvSequence();
}
function triggerFireAlarm() {
setZoneState("09", "alarm");
pressureMode = "drop";
addLog(`<span style="color:#ff8eb6;font-weight:900">FIRE ALARM</span> triggered — FACTORY 2 / ZONE 09 (MACHINE ROOM).`);
const toast = document.getElementById("fireToast");
if (toast) toast.classList.add("show");
setTimeout(() => addLog(`<span style="color:#ff8eb6;font-weight:900">SMOKE DETECTOR</span> confirmed at ZONE 09 / MACHINE ROOM.`), 900);
setTimeout(() => addLog(`<span style="color:#7cff3a;font-weight:900">EMERGENCY DISPATCH</span> 119 fire department notified.`), 1800);
fireModalTimer = setTimeout(() => {
openFireModal();
addLog(`<span style="color:#5af9ff;font-weight:900">CCTV</span> CAM-09 feed established — confirm fire location.`);
}, 2800);
}
function closeFireModal() {
const modal = document.getElementById("fireModal");
if (modal) modal.classList.remove("active");
const toast = document.getElementById("fireToast");
if (toast) toast.classList.remove("show");
if (fireModalTimer) { clearTimeout(fireModalTimer); fireModalTimer = null; }
stopCctvSequence();
}
function ackFireAlarm() {
closeFireModal();
acknowledgeAll();
silenceAlarm();
pressureMode = "recover";
}
setInterval(() => {
const modal = document.getElementById("fireModal");
if (modal && modal.classList.contains("active")) {
const t = fmtTime();
const a = document.getElementById("fireModalTime");
const b = document.getElementById("fireModalCctvTime");
if (a) a.textContent = t;
if (b) b.textContent = t;
}
}, 1000);
function runAutoScenario() {
if (autoRunning) return;
autoRunning = true;
document.getElementById("btnAuto").disabled = true;
setControlsDisabled(true);
resetAll(true);
addLog(`<span style="color:#0e4f93;font-weight:900">AUTO SCENARIO</span> fire detection scenario start.`);
setTimeout(() => setZoneState("09", "alarm"), 2000);
setTimeout(() => addLog(`<span style="color:#b10017;font-weight:900">SMOKE DETECTOR</span> triggered at FACTORY 2 / ZONE 09.`), 3000);
setTimeout(() => {
pressureMode = "drop";
addLog(`<span style="color:#0c7a32;font-weight:900">MAIN PUMP</span> auto-started — sprinkler line pressurizing.`);
}, 4000);
setTimeout(() => setZoneState("10", "alarm"), 6000);
setTimeout(() => addLog(`<span style="color:#9c1f2b;font-weight:900">EMERGENCY DISPATCH</span> 119 fire department notified.`), 7500);
setTimeout(() => addLog(`<span style="color:#0c7a32;font-weight:900">SPRINKLER</span> activated at ZONE 09, 10.`), 8500);
setTimeout(() => addLog(`<span style="color:#444;font-weight:900">FIRE SHUTTER</span> closed at LINK BRIDGE between FACTORY 2 / OFFICE.`), 10000);
setTimeout(() => acknowledgeAll(), 12500);
setTimeout(() => silenceAlarm(), 14500);
setTimeout(() => {
pressureMode = "recover";
addLog(`<span style="color:#0c7a32;font-weight:900">PRESSURE RECOVERY</span> sprinkler line stabilizing.`);
}, 16500);
setTimeout(() => addLog(`<span style="color:#11722a;font-weight:900">FIRE SUPPRESSED</span> ZONE 09, 10 area cleared.`), 19000);
setTimeout(() => {
resetAll(true);
addLog(`<span style="color:#11722a;font-weight:900">SYSTEM NORMAL</span> all zones returned.`);
autoRunning = false;
document.getElementById("btnAuto").disabled = false;
setControlsDisabled(false);
}, 22000);
}
function updatePressure() {
if (pressureMode === "drop") pressureLevel = Math.max(0, pressureLevel - 0.04);
else if (pressureMode === "recover") {
pressureLevel = Math.min(1, pressureLevel + 0.05);
if (pressureLevel >= 1) pressureMode = "normal";
}
const mainBase = 4.5 - (1 - pressureLevel) * 1.8;
const sprBase = 5.4 - (1 - pressureLevel) * 2.4;
const main = (mainBase + Math.random() * 0.12).toFixed(2);
const sprinkler = (sprBase + Math.random() * 0.12).toFixed(2);
const drain = (pressureMode === "drop" ? 0.4 + Math.random() * 0.2 : Math.random() * 0.08).toFixed(2);
document.getElementById("pressureMain").textContent = main;
document.getElementById("pressureSprinkler").textContent = sprinkler;
document.getElementById("pressureDrain").textContent = drain;
const dm = document.getElementById("digitalMain");
if (dm) {
dm.textContent = main;
dm.style.color = parseFloat(main) < 4.5 ? "#ff5168" : "#ffe26c";
}
}
function bindSvgClick() {
document.querySelectorAll(".zone").forEach(group => {
group.addEventListener("click", () => {
const id = group.id.replace("zone-", "");
cycleZone(id);
});
});
}
// 각 zone 중앙에 경광등 자동 삽입 (alarm/warn 시 자동 표시)
function attachBeacons() {
const NS = "http://www.w3.org/2000/svg";
const W = 76, H = 76; // 정사각 viewBox 100x100
document.querySelectorAll(".zone").forEach(group => {
const rect = group.querySelector(".zone-area");
if (!rect) return;
const x = parseFloat(rect.getAttribute("x"));
const y = parseFloat(rect.getAttribute("y"));
const w = parseFloat(rect.getAttribute("width"));
const h = parseFloat(rect.getAttribute("height"));
const cx = x + w / 2;
const cy = y + h / 2;
const use = document.createElementNS(NS, "use");
use.setAttribute("href", "#beacon");
use.setAttribute("x", cx - W / 2);
use.setAttribute("y", cy - H / 2 - 6); // zone 중앙 약간 위
use.setAttribute("width", W);
use.setAttribute("height", H);
use.setAttribute("class", "zone-beacon");
group.appendChild(use);
});
}
setClock();
setInterval(() => {
setClock();
updatePressure();
}, 1000);
renderSpeaker();
renderAll();
updatePressure();
bindSvgClick();
attachBeacons();
addLog(`<span style="color:#11722a;font-weight:900">SYSTEM READY</span> fire monitoring system initialized.`);
addLog(`<span style="color:#11722a;font-weight:900">NORMAL</span> all devices communication OK.`);
</script>
</body>
</html>