ExperionCrawler First Commit

This commit is contained in:
windpacer
2026-04-14 04:02:43 +00:00
commit 323aec34af
158 changed files with 539535 additions and 0 deletions

385
src/Web/wwwroot/index.html Normal file
View File

@@ -0,0 +1,385 @@
<!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>
</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=shinam: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=shinam: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">
서버 설정은 위 크롤링 설정을 그대로 사용합니다 &nbsp;·&nbsp;
노드 수에 따라 수 분이 소요될 수 있습니다 &nbsp;·&nbsp;
결과는 <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 id="nm-stat-row" class="nm-stat-row hidden"></div>
<!-- 필터 카드 -->
<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>데이터 타입</label>
<select id="nf-dtype" class="inp">
<option value="">전체</option>
</select>
</div>
</div>
<!-- 이름 OR 조건 선택 (최대 4개) -->
<div class="fg nm-name-row">
<label>이름 선택 <em>(OR 조건, 최대 4개)</em></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>
</main>
</div>
<script src="/js/app.js"></script>
</body>
</html>