610 lines
28 KiB
HTML
610 lines
28 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ko">
|
||
<head>
|
||
<meta charset="UTF-8"/>
|
||
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
||
<title>ExperionCrawler</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;700&family=Barlow:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||
<link rel="stylesheet" href="/css/style.css"/>
|
||
</head>
|
||
<body>
|
||
<div class="shell">
|
||
|
||
<!-- ── Sidebar ───────────────────────────────────────────── -->
|
||
<nav class="sidebar">
|
||
<div class="brand">
|
||
<svg class="brand-icon" viewBox="0 0 40 40" fill="none">
|
||
<rect x="4" y="4" width="14" height="14" rx="2" stroke="currentColor" stroke-width="1.5"/>
|
||
<rect x="22" y="4" width="14" height="14" rx="2" stroke="currentColor" stroke-width="1.5"/>
|
||
<rect x="4" y="22" width="14" height="14" rx="2" stroke="currentColor" stroke-width="1.5"/>
|
||
<rect x="22" y="22" width="14" height="14" rx="2" stroke="currentColor" stroke-width="1.5"/>
|
||
<circle cx="11" cy="11" r="3" fill="currentColor" opacity=".6"/>
|
||
<circle cx="29" cy="11" r="3" fill="currentColor" opacity=".6"/>
|
||
<circle cx="11" cy="29" r="3" fill="currentColor" opacity=".6"/>
|
||
<circle cx="29" cy="29" r="3" fill="currentColor" opacity="1"/>
|
||
</svg>
|
||
<div>
|
||
<div class="brand-name">EXPERION</div>
|
||
<div class="brand-sub">CRAWLER v1.0</div>
|
||
</div>
|
||
</div>
|
||
|
||
<ul class="nav">
|
||
<li class="nav-item active" data-tab="cert">
|
||
<span class="ni">01</span>
|
||
<span class="nl">인증서 관리</span>
|
||
<span class="nb" id="cert-dot"></span>
|
||
</li>
|
||
<li class="nav-item" data-tab="conn">
|
||
<span class="ni">02</span>
|
||
<span class="nl">서버 접속 테스트</span>
|
||
</li>
|
||
<li class="nav-item" data-tab="crawl">
|
||
<span class="ni">03</span>
|
||
<span class="nl">데이터 크롤링</span>
|
||
</li>
|
||
<li class="nav-item" data-tab="db">
|
||
<span class="ni">04</span>
|
||
<span class="nl">DB 저장</span>
|
||
</li>
|
||
<li class="nav-item" data-tab="nm-dash">
|
||
<span class="ni">05</span>
|
||
<span class="nl">노드맵 대시보드</span>
|
||
</li>
|
||
<li class="nav-item" data-tab="pb">
|
||
<span class="ni">06</span>
|
||
<span class="nl">포인트빌더</span>
|
||
</li>
|
||
<li class="nav-item" data-tab="hist">
|
||
<span class="ni">07</span>
|
||
<span class="nl">이력 조회</span>
|
||
</li>
|
||
<li class="nav-item" data-tab="opcsvr">
|
||
<span class="ni">08</span>
|
||
<span class="nl">OPC UA 서버</span>
|
||
<span class="nb" id="opcsvr-dot"></span>
|
||
</li>
|
||
</ul>
|
||
|
||
<div class="sb-foot">
|
||
<span class="dot" id="g-dot"></span>
|
||
<span id="g-txt" class="mono">READY</span>
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- ── Main ──────────────────────────────────────────────── -->
|
||
<main class="content">
|
||
|
||
<!-- ══════════════════════════════════════════════════════
|
||
01 인증서 관리
|
||
═══════════════════════════════════════════════════════ -->
|
||
<section class="pane active" id="pane-cert">
|
||
<header class="pane-hdr">
|
||
<div>
|
||
<h1>인증서 관리</h1>
|
||
<p>OPC UA 클라이언트 인증서를 생성합니다. 기존 파일이 있으면 재사용됩니다.</p>
|
||
</div>
|
||
<div class="pane-tag">PKI / X.509</div>
|
||
</header>
|
||
|
||
<div class="cols-2">
|
||
<div class="card">
|
||
<div class="card-cap">인증서 생성</div>
|
||
<div class="fg">
|
||
<label>Client Hostname</label>
|
||
<input id="c-host" class="inp" value="dbsvr"/>
|
||
</div>
|
||
<div class="fg">
|
||
<label>Subject Alt Names <em>(쉼표 구분)</em></label>
|
||
<input id="c-san" class="inp" value="localhost,192.168.0.50"/>
|
||
</div>
|
||
<div class="fg">
|
||
<label>PFX Password <em>(없으면 비워 두세요)</em></label>
|
||
<input id="c-pw" class="inp" type="password" placeholder=""/>
|
||
</div>
|
||
<button class="btn-a" onclick="certCreate()">🔑 인증서 생성</button>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="card-cap">현재 인증서 상태</div>
|
||
<button class="btn-b" onclick="certStatus()" style="margin-bottom:14px">상태 확인</button>
|
||
<div id="cert-disp" class="kv-box">
|
||
<span class="placeholder">상태 확인 버튼을 눌러 주세요</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="cert-log" class="logbox hidden"></div>
|
||
</section>
|
||
|
||
<!-- ══════════════════════════════════════════════════════
|
||
02 서버 접속 테스트
|
||
═══════════════════════════════════════════════════════ -->
|
||
<section class="pane" id="pane-conn">
|
||
<header class="pane-hdr">
|
||
<div>
|
||
<h1>서버 접속 테스트</h1>
|
||
<p>Experion OPC UA 서버에 연결하고 노드 값을 읽습니다.</p>
|
||
</div>
|
||
<div class="pane-tag">OPC UA / TCP</div>
|
||
</header>
|
||
|
||
<div class="card" style="margin-bottom:18px">
|
||
<div class="card-cap">서버 설정</div>
|
||
<div class="cols-3">
|
||
<div class="fg"><label>Server IP</label>
|
||
<input id="x-server" class="inp" value="192.168.0.20"/></div>
|
||
<div class="fg"><label>Port</label>
|
||
<input id="x-port" class="inp" type="number" value="4840"/></div>
|
||
<div class="fg"><label>Client Hostname</label>
|
||
<input id="x-client" class="inp" value="dbsvr"/></div>
|
||
<div class="fg"><label>Username</label>
|
||
<input id="x-user" class="inp" value="mngr"/></div>
|
||
<div class="fg"><label>Password</label>
|
||
<input id="x-pass" class="inp" type="password" value="mngr"/></div>
|
||
</div>
|
||
<div class="btn-row">
|
||
<button class="btn-a" onclick="connTest()">🔌 접속 테스트</button>
|
||
<button class="btn-b" onclick="connBrowse()">🌲 노드 탐색</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="card-cap">단일 태그 읽기</div>
|
||
<div class="row-inp">
|
||
<input id="x-node" class="inp flex1"
|
||
value="ns=1;s=sinamserver:p-6102.hzset.fieldvalue"
|
||
placeholder="ns=1;s=..."/>
|
||
<button class="btn-b" onclick="connRead()">읽기</button>
|
||
</div>
|
||
<div id="tag-box" class="tag-box hidden"></div>
|
||
</div>
|
||
|
||
<div id="conn-log" class="logbox hidden"></div>
|
||
<div id="browse-wrap" class="bwrap hidden"></div>
|
||
</section>
|
||
|
||
<!-- ══════════════════════════════════════════════════════
|
||
03 데이터 크롤링
|
||
═══════════════════════════════════════════════════════ -->
|
||
<section class="pane" id="pane-crawl">
|
||
<header class="pane-hdr">
|
||
<div>
|
||
<h1>데이터 크롤링</h1>
|
||
<p>지정한 노드 값을 주기적으로 수집하여 CSV 파일로 저장합니다.</p>
|
||
</div>
|
||
<div class="pane-tag">CRAWL / CSV</div>
|
||
</header>
|
||
|
||
<div class="cols-2">
|
||
<div class="card">
|
||
<div class="card-cap">서버 설정</div>
|
||
<div class="fg"><label>Server IP</label>
|
||
<input id="w-server" class="inp" value="192.168.0.20"/></div>
|
||
<div class="fg"><label>Port</label>
|
||
<input id="w-port" class="inp" type="number" value="4840"/></div>
|
||
<div class="fg"><label>Client Hostname</label>
|
||
<input id="w-client" class="inp" value="dbsvr"/></div>
|
||
<div class="fg"><label>Username</label>
|
||
<input id="w-user" class="inp" value="mngr"/></div>
|
||
<div class="fg"><label>Password</label>
|
||
<input id="w-pass" class="inp" type="password" value="mngr"/></div>
|
||
<div class="fg"><label>수집 간격 (초)</label>
|
||
<input id="w-interval" class="inp" type="number" value="1" min="1"/></div>
|
||
<div class="fg"><label>수집 시간 (초)</label>
|
||
<input id="w-duration" class="inp" type="number" value="30" min="1"/></div>
|
||
</div>
|
||
|
||
<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=sinamserver:p-6102.hzset.fieldvalue</textarea>
|
||
<button class="btn-a" id="crawl-btn" onclick="crawlStart()"
|
||
style="margin-top:14px">📡 크롤링 시작</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="crawl-prog" class="prog-wrap hidden">
|
||
<div class="prog-hdr">
|
||
<span id="crawl-ptxt">수집 중...</span>
|
||
<span id="crawl-cnt" class="mono">0</span>
|
||
</div>
|
||
<div class="prog-track"><div id="crawl-bar" class="prog-fill" style="width:0%"></div></div>
|
||
</div>
|
||
|
||
<div id="crawl-log" class="logbox hidden"></div>
|
||
|
||
<!-- ── 노드맵 수집 ──────────────────────────────────────── -->
|
||
<div class="section-div"></div>
|
||
|
||
<header class="pane-hdr" style="margin-bottom:16px">
|
||
<div>
|
||
<h2 class="sub-hdr">노드맵 수집</h2>
|
||
<p>서버 전체 노드를 재귀 탐색하여 AssetLoader 용 CSV 파일로 저장합니다.</p>
|
||
</div>
|
||
<div class="pane-tag">NODE MAP / CSV</div>
|
||
</header>
|
||
|
||
<div class="card">
|
||
<div class="card-cap">전체 노드 탐색 설정</div>
|
||
<div class="nm-row">
|
||
<div class="fg" style="margin-bottom:0;width:200px">
|
||
<label>최대 탐색 깊이</label>
|
||
<input id="nm-depth" class="inp" type="number" value="10" min="1" max="20"/>
|
||
</div>
|
||
<button class="btn-a" id="nm-btn" onclick="nodeMapCrawl()">🗺 전체 노드맵 수집</button>
|
||
</div>
|
||
<p class="nm-hint">
|
||
서버 설정은 위 크롤링 설정을 그대로 사용합니다 ·
|
||
노드 수에 따라 수 분이 소요될 수 있습니다 ·
|
||
결과는 <code>data/csv/{서버명}_*.csv</code> 에 저장됩니다
|
||
</p>
|
||
</div>
|
||
|
||
<div id="nm-prog" class="prog-wrap hidden">
|
||
<div class="prog-hdr">
|
||
<span id="nm-ptxt">탐색 중...</span>
|
||
<span id="nm-cnt" class="mono"></span>
|
||
</div>
|
||
<div class="prog-track"><div id="nm-bar" class="prog-fill" style="width:0%"></div></div>
|
||
</div>
|
||
|
||
<div id="nm-log" class="logbox hidden"></div>
|
||
</section>
|
||
|
||
<!-- ══════════════════════════════════════════════════════
|
||
04 DB 저장
|
||
═══════════════════════════════════════════════════════ -->
|
||
<section class="pane" id="pane-db">
|
||
<header class="pane-hdr">
|
||
<div>
|
||
<h1>DB 저장</h1>
|
||
<p>수집된 CSV 파일을 PostgreSQL DB에 저장하고 레코드를 조회합니다.</p>
|
||
</div>
|
||
<div class="pane-tag">PostgreSQL / EF</div>
|
||
</header>
|
||
|
||
<div class="cols-2">
|
||
<div class="card">
|
||
<div class="card-cap">CSV → DB 임포트</div>
|
||
<button class="btn-b" onclick="dbLoadFiles()" style="margin-bottom:10px">
|
||
🔄 파일 목록 갱신
|
||
</button>
|
||
<div id="file-list" class="flist">
|
||
<span class="placeholder">갱신 버튼을 눌러 주세요</span>
|
||
</div>
|
||
<div class="fg" style="margin-top:12px">
|
||
<label>선택된 파일</label>
|
||
<input id="sel-csv" class="inp" readonly placeholder="위 목록에서 파일을 선택하세요"/>
|
||
</div>
|
||
<div class="fg">
|
||
<label>저장 방식</label>
|
||
<div class="mode-group">
|
||
<label class="mode-opt">
|
||
<input type="radio" name="import-mode" value="append" checked/>
|
||
<span>추가 저장</span>
|
||
</label>
|
||
<label class="mode-opt mode-opt-danger">
|
||
<input type="radio" name="import-mode" value="truncate"/>
|
||
<span>초기화 후 저장</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<button class="btn-a" onclick="dbImport()">💾 DB에 저장</button>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="card-cap">DB 레코드 조회</div>
|
||
<div class="row-inp" style="margin-bottom:12px">
|
||
<input id="db-limit" class="inp" type="number" value="100"
|
||
min="1" max="10000" style="width:110px"/>
|
||
<button class="btn-b" onclick="dbQuery()">조회</button>
|
||
</div>
|
||
<div id="db-stats" class="stats hidden"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="db-log" class="logbox hidden"></div>
|
||
<div id="db-table" class="tbl-wrap hidden"></div>
|
||
</section>
|
||
|
||
<!-- ══════════════════════════════════════════════════════
|
||
05 노드맵 대시보드
|
||
═══════════════════════════════════════════════════════ -->
|
||
<section class="pane" id="pane-nm-dash">
|
||
<header class="pane-hdr">
|
||
<div>
|
||
<h1>노드맵 대시보드</h1>
|
||
<p>node_map_master 테이블을 조회합니다.</p>
|
||
</div>
|
||
<div class="pane-tag">NODE MAP / MASTER</div>
|
||
</header>
|
||
|
||
<!-- 필터 카드 -->
|
||
<div class="card">
|
||
<div class="card-cap">필터 조건</div>
|
||
<div class="cols-3">
|
||
<div class="fg">
|
||
<label>Level 최소</label>
|
||
<input id="nf-lv-min" class="inp" type="number" min="0" placeholder="0"/>
|
||
</div>
|
||
<div class="fg">
|
||
<label>Level 최대</label>
|
||
<input id="nf-lv-max" class="inp" type="number" min="0" placeholder=""/>
|
||
</div>
|
||
<div class="fg">
|
||
<label>클래스</label>
|
||
<select id="nf-class" class="inp">
|
||
<option value="">전체</option>
|
||
<option value="Object">Object</option>
|
||
<option value="Variable">Variable</option>
|
||
</select>
|
||
</div>
|
||
<div class="fg">
|
||
<label>Node ID 검색</label>
|
||
<input id="nf-nid" class="inp" placeholder="포함 검색"/>
|
||
</div>
|
||
<div class="fg">
|
||
<label>데이터 타입 <em>(직접 입력)</em></label>
|
||
<input id="nf-dtype" class="inp" placeholder="예: Double, Int32"/>
|
||
</div>
|
||
</div>
|
||
<!-- 이름 OR 조건 선택 (최대 4개) — 불러오기 버튼으로 옵션 채움 -->
|
||
<div class="fg nm-name-row">
|
||
<label style="display:flex;align-items:center;gap:8px">
|
||
이름 선택 <em>(OR 조건, 최대 4개)</em>
|
||
<button class="btn-b btn-sm" onclick="nmLoadNames()" style="margin-left:4px">▼ 옵션 불러오기</button>
|
||
</label>
|
||
<div class="nm-name-selects">
|
||
<select id="nf-name-1" class="inp nm-name-sel"><option value="">— 선택 안 함 —</option></select>
|
||
<select id="nf-name-2" class="inp nm-name-sel"><option value="">— 선택 안 함 —</option></select>
|
||
<select id="nf-name-3" class="inp nm-name-sel"><option value="">— 선택 안 함 —</option></select>
|
||
<select id="nf-name-4" class="inp nm-name-sel"><option value="">— 선택 안 함 —</option></select>
|
||
</div>
|
||
</div>
|
||
<div class="btn-row" style="align-items:center">
|
||
<button class="btn-a" onclick="nmQuery(0)">🔍 조회</button>
|
||
<button class="btn-b" onclick="nmReset()">초기화</button>
|
||
<div style="display:flex;align-items:center;gap:8px;margin-left:auto">
|
||
<label style="font-size:11px;color:var(--t2);white-space:nowrap">페이지당</label>
|
||
<input id="nf-limit" class="inp" type="number" value="100" min="10" max="500" style="width:80px"/>
|
||
<label style="font-size:11px;color:var(--t2)">건</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 결과 통계 + 페이지네이션 -->
|
||
<div id="nm-result-bar" class="nm-result-bar hidden">
|
||
<span id="nm-result-info" class="nm-result-info"></span>
|
||
<div class="pg">
|
||
<button class="btn-b btn-sm" id="nm-pg-prev" onclick="nmPrev()">← 이전</button>
|
||
<span id="nm-pg-info" class="pg-info"></span>
|
||
<button class="btn-b btn-sm" id="nm-pg-next" onclick="nmNext()">다음 →</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 테이블 -->
|
||
<div id="nm-table" class="tbl-wrap hidden"></div>
|
||
</section>
|
||
|
||
<!-- ══════════════════════════════════════════════════════
|
||
06 포인트빌더
|
||
═══════════════════════════════════════════════════════ -->
|
||
<section class="pane" id="pane-pb">
|
||
<header class="pane-hdr">
|
||
<div>
|
||
<h1>포인트빌더</h1>
|
||
<p>node_map_master 에서 실시간 모니터링할 포인트를 선택해 realtime_table 을 구성합니다.</p>
|
||
</div>
|
||
<div class="pane-tag">REALTIME / BUILD</div>
|
||
</header>
|
||
|
||
<!-- 빌더 카드 -->
|
||
<div class="cols-2">
|
||
<div class="card">
|
||
<div class="card-cap">조건으로 테이블 작성</div>
|
||
<div class="fg">
|
||
<label>이름(name) 선택 <em>(OR 조건, 최대 8개)</em>
|
||
<button class="btn-b btn-sm" onclick="pbLoad()" style="margin-left:4px">▼ 옵션 불러오기</button>
|
||
</label>
|
||
<div class="pb-name-grid" id="pb-name-grid">
|
||
<!-- JS 에서 드롭다운 동적 생성 -->
|
||
<select id="pb-n1" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||
<select id="pb-n2" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||
<select id="pb-n3" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||
<select id="pb-n4" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||
<select id="pb-n5" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||
<select id="pb-n6" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||
<select id="pb-n7" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||
<select id="pb-n8" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||
</div>
|
||
</div>
|
||
<div class="fg">
|
||
<label>데이터 타입(data_type) 직접 입력 <em>(OR 조건, 최대 2개)</em></label>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
|
||
<input id="pb-dt1" class="inp" placeholder="예: Double"/>
|
||
<input id="pb-dt2" class="inp" placeholder="예: Int32"/>
|
||
</div>
|
||
</div>
|
||
<button class="btn-a" onclick="pbBuild()">🔨 테이블 작성하기</button>
|
||
<div id="pb-build-log" class="logbox hidden" style="margin-top:10px"></div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="card-cap">수동 포인트 추가</div>
|
||
<div class="fg">
|
||
<label>Node ID 직접 입력</label>
|
||
<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>
|
||
|
||
<div class="card-cap" style="margin-top:20px">실시간 구독 제어</div>
|
||
<div class="cols-2" style="gap:8px;margin-bottom:10px">
|
||
<div class="fg">
|
||
<label>서버 IP</label>
|
||
<input id="pb-rt-ip" class="inp" value="192.168.0.20"/>
|
||
</div>
|
||
<div class="fg">
|
||
<label>포트</label>
|
||
<input id="pb-rt-port" class="inp" type="number" value="4840"/>
|
||
</div>
|
||
<div class="fg">
|
||
<label>클라이언트 호스트</label>
|
||
<input id="pb-rt-client" class="inp" value="dbsvr"/>
|
||
</div>
|
||
<div class="fg">
|
||
<label>계정</label>
|
||
<input id="pb-rt-user" class="inp" value="mngr"/>
|
||
</div>
|
||
<div class="fg" style="grid-column:1/-1">
|
||
<label>비밀번호</label>
|
||
<input id="pb-rt-pw" class="inp" type="password" value="mngr"/>
|
||
</div>
|
||
</div>
|
||
<div class="btn-row">
|
||
<button class="btn-a" onclick="rtStart()">▶ 구독 시작</button>
|
||
<button class="btn-b" onclick="rtStop()">■ 구독 중지</button>
|
||
<button class="btn-b btn-sm" onclick="rtStatus()">상태 확인</button>
|
||
</div>
|
||
<div id="pb-rt-status" class="logbox hidden" style="margin-top:8px"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 포인트 목록 -->
|
||
<div class="card" style="margin-top:0">
|
||
<div class="card-cap" style="display:flex;justify-content:space-between;align-items:center">
|
||
<span>포인트 목록 <span id="pb-count" class="mut">(0개)</span></span>
|
||
<button class="btn-b btn-sm" onclick="pbRefresh()">↻ 새로 고침</button>
|
||
</div>
|
||
<div id="pb-table" class="tbl-wrap">
|
||
<div style="padding:20px;color:var(--t2)">포인트가 없습니다. 위에서 테이블을 작성하세요.</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ══════════════════════════════════════════════════════
|
||
07 이력 조회
|
||
═══════════════════════════════════════════════════════ -->
|
||
<section class="pane" id="pane-hist">
|
||
<header class="pane-hdr">
|
||
<div>
|
||
<h1>이력 조회</h1>
|
||
<p>history_table 의 시계열 데이터를 조회합니다.</p>
|
||
</div>
|
||
<div class="pane-tag">HISTORY / TREND</div>
|
||
</header>
|
||
|
||
<div class="card">
|
||
<div class="card-cap">조회 조건</div>
|
||
<div class="fg">
|
||
<label style="display:flex;align-items:center;gap:8px">
|
||
태그 선택 <em>(최대 8개, OR 조건)</em>
|
||
<button class="btn-b btn-sm" onclick="histLoad()" style="margin-left:4px">▼ 옵션 불러오기</button>
|
||
</label>
|
||
<div class="pb-name-grid">
|
||
<select id="hf-t1" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||
<select id="hf-t2" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||
<select id="hf-t3" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||
<select id="hf-t4" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||
<select id="hf-t5" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||
<select id="hf-t6" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||
<select id="hf-t7" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||
<select id="hf-t8" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||
</div>
|
||
</div>
|
||
<div class="cols-3">
|
||
<div class="fg">
|
||
<label>시작 시간</label>
|
||
<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 type="hidden" id="hf-to"/>
|
||
<div class="dt-display inp" id="dtp-to-display" onclick="dtOpen('to')">— 선택 안 함 —</div>
|
||
</div>
|
||
<div class="fg">
|
||
<label>최대 행 수</label>
|
||
<input id="hf-limit" class="inp" type="number" value="500" min="10" max="5000"/>
|
||
</div>
|
||
</div>
|
||
<div class="btn-row">
|
||
<button class="btn-a" onclick="histQuery()">🔍 조회</button>
|
||
<button class="btn-b" onclick="histReset()">초기화</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="hist-result-info" class="nm-result-info hidden" style="margin:8px 0"></div>
|
||
<div id="hist-table" class="tbl-wrap hidden"></div>
|
||
</section>
|
||
|
||
</main>
|
||
</div>
|
||
|
||
<!-- ══════════════════════════════════════════════════════
|
||
08 OPC UA 서버
|
||
═══════════════════════════════════════════════════════ -->
|
||
<section class="pane" id="pane-opcsvr">
|
||
<header class="pane-hdr">
|
||
<div>
|
||
<h1>OPC UA 서버</h1>
|
||
<p class="sub">ExperionCrawler를 OPC UA 서버로 동작시켜 외부 클라이언트에 실시간 값을 제공합니다.</p>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- 상태 카드 -->
|
||
<div class="srv-status-card" id="srv-status-card">
|
||
<div class="srv-status-row">
|
||
<span class="dot" id="srv-dot"></span>
|
||
<span id="srv-status-txt" class="srv-label">상태 조회 중...</span>
|
||
</div>
|
||
<div class="srv-meta" id="srv-meta"></div>
|
||
</div>
|
||
|
||
<!-- 버튼 행 -->
|
||
<div class="row-btns" style="margin-top:12px">
|
||
<button class="btn-a" onclick="srvStart()">▶ 서버 시작</button>
|
||
<button class="btn-b" onclick="srvStop()">■ 서버 중지</button>
|
||
<button class="btn-b" onclick="srvRebuild()">↺ 주소공간 재구성</button>
|
||
<button class="btn-b" onclick="srvLoad()">↻ 상태 새로고침</button>
|
||
</div>
|
||
|
||
<div id="srv-log" class="log-box hidden" style="margin-top:16px"></div>
|
||
</section>
|
||
|
||
<!-- ── 날짜/시간 선택 팝업 ──────────────────────────────────── -->
|
||
<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>
|