Files
ExperionCrawler/src/Web/wwwroot/index.html
2026-04-15 08:19:55 +00:00

610 lines
28 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>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">
서버 설정은 위 크롤링 설정을 그대로 사용합니다 &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 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>