The PHP app runs on Cafe24 shared web hosting -- calling it 'cloud' was misleading. Renames the directory, the edge client module, and every doc/script/identifier that used the term. - cloud/ -> server/ (incl. server/deploy/build_cafe24_package.sh paths) - edge/cloudclient.py -> edge/serverclient.py, CloudClient -> ServerClient - cloud/docs/cloud-deploy.md -> server/docs/server-deploy.md - CLOUD_* env/config keys -> SERVER_* - tests renamed to match Tests: PHP 109 pass (4 skipped), pytest 167 pass. dist/ regenerated.
123 lines
4.2 KiB
JavaScript
123 lines
4.2 KiB
JavaScript
/**
|
||
* Incident detail view: parses the event id from /events/{id}/view, fetches
|
||
* GET /events/{id} (JSON {event, snapshots}), and renders the event metadata
|
||
* plus each snapshot as an <img src="/snap/{file_uuid}">.
|
||
*
|
||
* XSS-safe: every dynamic value is written via textContent or a validated URL
|
||
* attribute, never innerHTML. Severity is shown as icon + text (never color
|
||
* alone, per NF9). On 401 the browser is redirected to /login, mirroring app.js.
|
||
*/
|
||
(function () {
|
||
'use strict';
|
||
|
||
// Mount prefix: '' at the web root, '/move_detect' when the app shares a www/ with another
|
||
// site. Injected by src/View/layout.php from the server's SCRIPT_NAME.
|
||
var B = window.APP_BASE || '';
|
||
|
||
var SEVERITY_LABELS = {
|
||
critical: { icon: '⛔', text: '위급', cls: 'sev-critical' },
|
||
high: { icon: '⚠', text: '높음', cls: 'sev-high' },
|
||
warning: { icon: '△', text: '경고', cls: 'sev-warning' },
|
||
info: { icon: 'ℹ', text: '정보', cls: 'sev-info' }
|
||
};
|
||
|
||
var meta = document.getElementById('event-detail-meta');
|
||
var snapBox = document.getElementById('event-detail-snapshots');
|
||
if (!meta) return;
|
||
|
||
var m = location.pathname.match(/\/events\/(\d+)\/view/);
|
||
if (!m) {
|
||
meta.textContent = '잘못된 주소입니다.';
|
||
meta.removeAttribute('aria-busy');
|
||
return;
|
||
}
|
||
var id = m[1];
|
||
|
||
function severityInfo(sev) {
|
||
return SEVERITY_LABELS[sev] || { icon: 'ℹ', text: sev || '알수없음', cls: 'sev-info' };
|
||
}
|
||
|
||
function formatTime(occurredAt) {
|
||
if (!occurredAt && occurredAt !== 0) return '-';
|
||
var d = new Date(Number(occurredAt) * 1000);
|
||
if (isNaN(d.getTime())) return '-';
|
||
return d.toLocaleString('ko-KR');
|
||
}
|
||
|
||
function buildBadge(sev) {
|
||
var info = severityInfo(sev);
|
||
var badge = document.createElement('span');
|
||
badge.className = 'severity-badge ' + info.cls;
|
||
var icon = document.createElement('span');
|
||
icon.className = 'icon';
|
||
icon.setAttribute('aria-hidden', 'true');
|
||
icon.textContent = info.icon;
|
||
var label = document.createElement('span');
|
||
label.className = 'label';
|
||
label.textContent = info.text;
|
||
badge.appendChild(icon);
|
||
badge.appendChild(label);
|
||
return badge;
|
||
}
|
||
|
||
function textNode(value) {
|
||
var span = document.createElement('span');
|
||
span.textContent = (value === null || value === undefined || value === '') ? '-' : String(value);
|
||
return span;
|
||
}
|
||
|
||
function addRow(label, valueNode) {
|
||
var row = document.createElement('div');
|
||
row.className = 'detail-row';
|
||
var k = document.createElement('span');
|
||
k.className = 'detail-label';
|
||
k.textContent = label + ': ';
|
||
row.appendChild(k);
|
||
row.appendChild(valueNode);
|
||
meta.appendChild(row);
|
||
}
|
||
|
||
function render(data) {
|
||
var ev = data.event || {};
|
||
meta.textContent = '';
|
||
meta.removeAttribute('aria-busy');
|
||
|
||
addRow('심각도', buildBadge(ev.severity));
|
||
addRow('유형', textNode(ev.event_type));
|
||
addRow('카메라', textNode(ev.camera_id));
|
||
addRow('발생 시각', textNode(formatTime(ev.occurred_at)));
|
||
addRow('지속(초)', textNode(ev.duration_s));
|
||
addRow('확인', textNode(ev.acknowledged_by || '미확인'));
|
||
|
||
if (!snapBox) return;
|
||
snapBox.textContent = '';
|
||
var snaps = data.snapshots || [];
|
||
if (snaps.length === 0) {
|
||
var none = document.createElement('p');
|
||
none.textContent = '스냅샷이 없습니다.';
|
||
snapBox.appendChild(none);
|
||
return;
|
||
}
|
||
snaps.forEach(function (s) {
|
||
if (!s || !s.file_uuid) return;
|
||
var img = document.createElement('img');
|
||
img.src = B + '/snap/' + encodeURIComponent(s.file_uuid);
|
||
img.alt = '사고 스냅샷';
|
||
img.className = 'detail-snapshot';
|
||
img.loading = 'lazy';
|
||
snapBox.appendChild(img);
|
||
});
|
||
}
|
||
|
||
fetch(B + '/events/' + encodeURIComponent(id), { headers: { Accept: 'application/json' } })
|
||
.then(function (res) {
|
||
if (res.status === 401) { window.location.href = B + '/login'; return null; }
|
||
if (!res.ok) throw new Error('detail_failed:' + res.status);
|
||
return res.json();
|
||
})
|
||
.then(function (data) { if (data) render(data); })
|
||
.catch(function () {
|
||
meta.textContent = '불러오기 실패';
|
||
meta.removeAttribute('aria-busy');
|
||
});
|
||
})();
|