운전시 버그 수정

This commit is contained in:
windpacer
2026-04-15 01:43:07 +00:00
parent 68758f1bb8
commit 9325b13f2b
20 changed files with 845 additions and 32 deletions

View File

@@ -498,6 +498,91 @@ tr:last-child td { border-bottom: none; }
.pb-name-grid { grid-template-columns: repeat(2, 1fr); }
}
/* ── Custom DateTime Picker ──────────────────────────────── */
.dt-display {
cursor: pointer; user-select: none;
color: var(--t0); display: flex; align-items: center;
}
.dt-display:hover { border-color: var(--a); }
.dt-overlay {
position: fixed; inset: 0; z-index: 900;
}
.dt-popup {
position: fixed; z-index: 901;
background: var(--s2); border: 1px solid var(--bd2);
border-radius: var(--rl); padding: 16px;
box-shadow: 0 8px 32px rgba(0,0,0,.6);
width: 280px;
}
.dt-cal-nav {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 10px;
}
.dt-month-label { font-weight: 700; color: var(--t0); font-size: 14px; }
.dt-nav-btn {
background: var(--s3); border: 1px solid var(--bd);
color: var(--t1); border-radius: var(--r);
width: 28px; height: 28px; cursor: pointer; font-size: 16px;
display: flex; align-items: center; justify-content: center;
transition: all var(--tr);
}
.dt-nav-btn:hover { background: var(--s4); color: var(--t0); }
.dt-cal-grid {
display: grid; grid-template-columns: repeat(7, 1fr);
gap: 2px; margin-bottom: 12px;
}
.dt-dow {
text-align: center; font-size: 11px; font-weight: 600;
color: var(--t2); padding: 4px 0;
}
.dt-day {
text-align: center; padding: 6px 2px; font-size: 12px;
border-radius: var(--r); cursor: pointer; color: var(--t1);
transition: background var(--tr);
}
.dt-day:hover { background: var(--s4); color: var(--t0); }
.dt-day.other-month { color: var(--t2); }
.dt-day.today { color: var(--a); font-weight: 700; }
.dt-day.selected {
background: var(--a); color: #000; font-weight: 700;
}
.dt-day.selected:hover { background: var(--a2); }
.dt-time-row {
display: flex; align-items: center; gap: 8px;
padding: 10px 0; border-top: 1px solid var(--bd);
border-bottom: 1px solid var(--bd); margin-bottom: 12px;
}
.dt-time-label { color: var(--t2); font-size: 12px; flex: 1; }
.dt-time-sep { color: var(--t1); font-weight: 700; font-size: 16px; }
.dt-time-ctrl {
display: flex; align-items: center; gap: 4px;
}
.dt-time-ctrl button {
background: var(--s3); border: 1px solid var(--bd);
color: var(--t1); border-radius: var(--r);
width: 24px; height: 24px; cursor: pointer; font-size: 14px;
display: flex; align-items: center; justify-content: center;
transition: all var(--tr);
}
.dt-time-ctrl button:hover { background: var(--s4); color: var(--t0); }
.dt-time-inp {
width: 42px; text-align: center;
background: var(--s3); border: 1px solid var(--bd);
color: var(--t0); border-radius: var(--r);
padding: 3px 4px; font-size: 14px; font-family: var(--fm);
}
.dt-time-inp::-webkit-inner-spin-button,
.dt-time-inp::-webkit-outer-spin-button { -webkit-appearance: none; }
.dt-pop-btns {
display: flex; justify-content: flex-end; gap: 6px;
}
/* ── Utility ─────────────────────────────────────────────── */
.hidden { display: none !important; }

View File

@@ -149,7 +149,7 @@
<div class="card-cap">단일 태그 읽기</div>
<div class="row-inp">
<input id="x-node" class="inp flex1"
value="ns=1;s=shinam:p-6102.hzset.fieldvalue"
value="ns=1;s=sinamserver:p-6102.hzset.fieldvalue"
placeholder="ns=1;s=..."/>
<button class="btn-b" onclick="connRead()">읽기</button>
</div>
@@ -194,7 +194,7 @@
<div class="card">
<div class="card-cap">수집 노드 목록 <em>(한 줄에 하나씩)</em></div>
<textarea id="w-nodes" class="ta" rows="9"
placeholder="ns=1;s=...">ns=1;s=shinam:p-6102.hzset.fieldvalue</textarea>
placeholder="ns=1;s=...">ns=1;s=sinamserver:p-6102.hzset.fieldvalue</textarea>
<button class="btn-a" id="crawl-btn" onclick="crawlStart()"
style="margin-top:14px">📡 크롤링 시작</button>
</div>
@@ -430,7 +430,7 @@
<div class="card-cap">수동 포인트 추가</div>
<div class="fg">
<label>Node ID 직접 입력</label>
<input id="pb-manual-nid" class="inp" placeholder="ns=2;s=Honeywell.Experion..."/>
<input id="pb-manual-nid" class="inp" placeholder="ns=1;s=tagname.pv..."/>
</div>
<button class="btn-b" onclick="pbAddManual()"> 추가</button>
<div id="pb-manual-log" class="logbox hidden" style="margin-top:10px"></div>
@@ -512,11 +512,13 @@
<div class="cols-3">
<div class="fg">
<label>시작 시간</label>
<input id="hf-from" class="inp" type="datetime-local"/>
<input type="hidden" id="hf-from"/>
<div class="dt-display inp" id="dtp-from-display" onclick="dtOpen('from')">— 선택 안 함 —</div>
</div>
<div class="fg">
<label>종료 시간</label>
<input id="hf-to" class="inp" type="datetime-local"/>
<input type="hidden" id="hf-to"/>
<div class="dt-display inp" id="dtp-to-display" onclick="dtOpen('to')">— 선택 안 함 —</div>
</div>
<div class="fg">
<label>최대 행 수</label>
@@ -536,6 +538,36 @@
</main>
</div>
<!-- ── 날짜/시간 선택 팝업 ──────────────────────────────────── -->
<div id="dt-overlay" class="dt-overlay hidden" onclick="dtCancel()"></div>
<div id="dt-popup" class="dt-popup hidden">
<div class="dt-cal-nav">
<button class="dt-nav-btn" onclick="dtPrevMonth()"></button>
<span id="dt-month-label" class="dt-month-label"></span>
<button class="dt-nav-btn" onclick="dtNextMonth()"></button>
</div>
<div class="dt-cal-grid" id="dt-cal-grid"></div>
<div class="dt-time-row">
<span class="dt-time-label">시간</span>
<div class="dt-time-ctrl">
<button onclick="dtAdjTime('h',-1)"></button>
<input id="dt-hour" class="dt-time-inp" type="number" min="0" max="23" value="0" oninput="dtClampTime('h',this)"/>
<button onclick="dtAdjTime('h', 1)">+</button>
</div>
<span class="dt-time-sep">:</span>
<div class="dt-time-ctrl">
<button onclick="dtAdjTime('m',-1)"></button>
<input id="dt-min" class="dt-time-inp" type="number" min="0" max="59" value="0" oninput="dtClampTime('m',this)"/>
<button onclick="dtAdjTime('m', 1)">+</button>
</div>
</div>
<div class="dt-pop-btns">
<button class="btn-b btn-sm" onclick="dtClear()">지우기</button>
<button class="btn-b btn-sm" onclick="dtCancel()">취소</button>
<button class="btn-a btn-sm" onclick="dtConfirm()">확인</button>
</div>
</div>
<script src="/js/app.js"></script>
</body>
</html>

View File

@@ -748,12 +748,134 @@ async function histQuery() {
function histReset() {
HIST_TAG_IDS.forEach(id => { document.getElementById(id).value = ''; });
document.getElementById('hf-from').value = '';
document.getElementById('hf-to').value = '';
dtClearField('from');
dtClearField('to');
document.getElementById('hf-limit').value = '500';
document.getElementById('hist-result-info').classList.add('hidden');
document.getElementById('hist-table').classList.add('hidden');
}
/* ── Custom DateTime Picker ──────────────────────────────────── */
const _dtp = { target: null, year: 0, month: 0,
selYear: null, selMonth: null, selDay: null,
selHour: 0, selMin: 0 };
const _DTP_DAYS = ['일','월','화','수','목','금','토'];
function dtOpen(target) {
_dtp.target = target;
const hidden = document.getElementById(`hf-${target}`).value;
const d = hidden ? new Date(hidden) : new Date();
_dtp.year = d.getFullYear();
_dtp.month = d.getMonth();
if (hidden && !isNaN(d)) {
_dtp.selYear = d.getFullYear(); _dtp.selMonth = d.getMonth();
_dtp.selDay = d.getDate();
_dtp.selHour = d.getHours(); _dtp.selMin = d.getMinutes();
} else {
_dtp.selYear = null; _dtp.selMonth = null; _dtp.selDay = null;
_dtp.selHour = 0; _dtp.selMin = 0;
}
document.getElementById('dt-hour').value = String(_dtp.selHour).padStart(2,'0');
document.getElementById('dt-min').value = String(_dtp.selMin).padStart(2,'0');
dtRenderCal();
// 팝업 위치 계산
const popup = document.getElementById('dt-popup');
const display = document.getElementById(`dtp-${target}-display`);
const rect = display.getBoundingClientRect();
popup.classList.remove('hidden');
document.getElementById('dt-overlay').classList.remove('hidden');
// 화면 아래 공간 부족하면 위쪽으로
const spaceBelow = window.innerHeight - rect.bottom;
if (spaceBelow < popup.offsetHeight + 8) {
popup.style.top = (rect.top - popup.offsetHeight - 4) + 'px';
} else {
popup.style.top = (rect.bottom + 4) + 'px';
}
popup.style.left = Math.min(rect.left, window.innerWidth - 296) + 'px';
}
function dtRenderCal() {
document.getElementById('dt-month-label').textContent =
`${_dtp.year}${_dtp.month + 1}`;
const first = new Date(_dtp.year, _dtp.month, 1).getDay();
const daysInMon = new Date(_dtp.year, _dtp.month + 1, 0).getDate();
const daysInPrev = new Date(_dtp.year, _dtp.month, 0).getDate();
const today = new Date();
let html = _DTP_DAYS.map(d => `<div class="dt-dow">${d}</div>`).join('');
for (let i = first - 1; i >= 0; i--)
html += `<div class="dt-day other-month">${daysInPrev - i}</div>`;
for (let d = 1; d <= daysInMon; d++) {
let cls = 'dt-day';
if (_dtp.year === today.getFullYear() && _dtp.month === today.getMonth() && d === today.getDate())
cls += ' today';
if (_dtp.selYear === _dtp.year && _dtp.selMonth === _dtp.month && _dtp.selDay === d)
cls += ' selected';
html += `<div class="${cls}" onclick="dtSelectDay(${d})">${d}</div>`;
}
const trailing = (first + daysInMon) % 7;
for (let d = 1; d <= (trailing ? 7 - trailing : 0); d++)
html += `<div class="dt-day other-month">${d}</div>`;
document.getElementById('dt-cal-grid').innerHTML = html;
}
function dtSelectDay(day) {
_dtp.selYear = _dtp.year; _dtp.selMonth = _dtp.month; _dtp.selDay = day;
dtRenderCal();
}
function dtPrevMonth() {
if (--_dtp.month < 0) { _dtp.month = 11; _dtp.year--; }
dtRenderCal();
}
function dtNextMonth() {
if (++_dtp.month > 11) { _dtp.month = 0; _dtp.year++; }
dtRenderCal();
}
function dtAdjTime(part, delta) {
if (part === 'h') {
_dtp.selHour = ((_dtp.selHour + delta) + 24) % 24;
document.getElementById('dt-hour').value = String(_dtp.selHour).padStart(2,'0');
} else {
_dtp.selMin = ((_dtp.selMin + delta) + 60) % 60;
document.getElementById('dt-min').value = String(_dtp.selMin).padStart(2,'0');
}
}
function dtClampTime(part, el) {
const max = part === 'h' ? 23 : 59;
let v = parseInt(el.value);
if (isNaN(v) || v < 0) v = 0;
if (v > max) v = max;
el.value = String(v).padStart(2,'0');
if (part === 'h') _dtp.selHour = v; else _dtp.selMin = v;
}
function dtConfirm() {
if (_dtp.selDay === null) { alert('날짜를 선택하세요.'); return; }
_dtp.selHour = parseInt(document.getElementById('dt-hour').value) || 0;
_dtp.selMin = parseInt(document.getElementById('dt-min').value) || 0;
const p = n => String(n).padStart(2,'0');
const val = `${_dtp.selYear}-${p(_dtp.selMonth+1)}-${p(_dtp.selDay)}T${p(_dtp.selHour)}:${p(_dtp.selMin)}`;
document.getElementById(`hf-${_dtp.target}`).value = val;
document.getElementById(`dtp-${_dtp.target}-display`).textContent =
`${_dtp.selYear}-${p(_dtp.selMonth+1)}-${p(_dtp.selDay)} ${p(_dtp.selHour)}:${p(_dtp.selMin)}`;
dtClose();
}
function dtClear() { dtClearField(_dtp.target); dtClose(); }
function dtClearField(target) {
if (!target) return;
document.getElementById(`hf-${target}`).value = '';
document.getElementById(`dtp-${target}-display`).textContent = '— 선택 안 함 —';
}
function dtCancel() { dtClose(); }
function dtClose() {
document.getElementById('dt-popup').classList.add('hidden');
document.getElementById('dt-overlay').classList.add('hidden');
_dtp.target = null;
}
/* ── 초기 실행 ───────────────────────────────────────────────── */
certStatus();