# 이벤트 히스토리 UI 신설 — 상세 코딩 플랜

## 진단 보고서 (2026-05-11)

diagnosis-checklist.md 8단계 순서대로 진단한 결과 **실행 가능한 플랜**임. 중대한 오진은 없음. 아래는 단계별 확인 사항과 발견된 소규모 이슈.

### STEP 1 — 맥락 파악

- **역할**: 백엔드 API 3개는 구현·동작 중인데 UI 전무 → 접근 불가 문제 해결
- **레이어**: 순수 프론트엔드 (HTML/JS/CSS) — 백엔드 수정 없음
- **관련 문서**: AGENTS.md (camelCase 규칙), CODING_CONVENTIONS.md

### STEP 2 — 구조 탐색

| 항목 | 실제 값 | 플랜 가정 | 일치 |
|------|---------|-----------|------|
| `style.css` 총 줄수 | 1489 | 1489 | ✅ |
| `index.html` 총 줄수 | 1205 | — | — |
| `index.html` `</main>` 위치 | 1119 | 파일 말미 | ⚠️ |
| `app.js` 총 줄수 | 3075 | — | — |
| `histReset()` 위치 | 1158~1169 | ~1175 | ⚠️ |
| `data-tab="pid"` 위치 | 75-78 | 75-78 | ✅ |

### STEP 3 — 코드 읽기 (핵심 검증)

| 검증 대상 | 실제 상태 | 플랜 가정 | 일치 |
|-----------|-----------|-----------|------|
| `api()` 함수 존재 | `app.js:40` | 존재 | ✅ |
| `esc()` 함수 존재 | `app.js:22` | 존재 | ✅ |
| `dtOpen(target)` 패턴 | `hf-${target}`, `dtp-${target}-display` 참조 | 동일 | ✅ |
| `dtClearField(target)` 패턴 | 동일 패턴 | 동일 | ✅ |
| 탭 핸들러 자동 호출 없음 | `app.js:5-18` — evt 케이스 없음 | 불필요 | ✅ |
| CSS 변수 (`--s2`, `--bd`, `--r`, `--fm`, `--t0`, `--t2`) | 모두 정의됨 | 사용 | ✅ |
| `.hidden` 클래스 | `style.css:1274` | 재사용 | ✅ |
| `.nm-cls` 클래스 | `style.css:469` | 재사용 | ✅ |
| `.hist-status` 클래스 | `style.css:1216` | 재사용 | ✅ |
| `.nm-result-info` 클래스 | `style.css:454` | 재사용 | ✅ |
| 백엔드 camelCase 응답 | `ExperionControllers.cs:1192-1207` | 일치 | ✅ |
| 백엔드 digital-tags 응답 | `{ tagName: "..." }` | 일치 | ✅ |

### STEP 4 — 호출 계층 지도

```
사용자 클릭 (이벤트 조회)
  → evtQuery()
      → api('GET', '/api/event-history?params')
          → EventHistoryController.Query()
              → _db.QueryEventHistoryAsync()
                  → DB (external I/O)
      → _evtBuildTable(d.data)    ← 순수 함수, side effect 없음
      → DOM innerHTML 업데이트
```

에러 처리: Controller 레벨 try-catch 전체 포착 → `{ success: false, error: "..." }` 반환. JS 측 `if (!d.success) throw`로 캐치. **계층 적절**.

### STEP 5 — 패턴 매칭 결과

| 체크 | 항목 | 결과 |
|------|------|------|
| 미정의 변수 참조 | `api`, `esc`, `dtClearField` 모두 정의됨 | ✅ |
| XSS (내부 HTML) | `esc()`로 모든 서버 응답 문자열 이스케이프 | ✅ |
| `.innerHTML` + 서버 데이터 | `_evtBuildTable`에서 `esc()` 적용済み | ✅ |
| 날짜 변환 | `new Date(fromRaw).toISOString()` — 기존 hist 탭과 동일 패턴 | ✅ |

### STEP 6 — 교차 검증

| 의심 항목 | Q1-Q4 결과 | 결론 |
|-----------|-----------|------|
| `histReset()` 줄번호 불일치 (1175 vs 1169) | Q3: 의도적 아님, 단순 추정치 차이 | LOW, 수정 제안만 |
| `</main>`가 "파일 말미"가 아님 (1119/1205) | Q3: 설명상 불명확하나 삽입 로직은 정확 | LOW, 명시화 제안 |

### 발견 사항

### 1. `histReset()` 종료 줄번호 불일치 (LOW)

**문제**: 플랜 439줄 "histReset() 함수 끝 (현재 ~1175줄) 뒤"라고 했으나 실제 `histReset()`은 `app.js:1158-1169`에서 종료됨. 1171줄부터 "07-2 하이퍼테이블 관리" 섹션 시작.
**근거**: `app.js:1158-1173`
**영향**: 동작에는 영향 없음. 삽입 위치를 "histReset() 뒤"로 이해하면 정확함. 줄번호만 불일치.
**수정**: "~1175줄" → "1169줄"로 정정

### 2. `</main>`가 파일 말미가 아님 (LOW)

**문제**: 플랜 181줄 "#pane-pid 섹션 (현재 파일 말미) 바로 뒤, </main> 닫는 태그 앞"이라고 했으나 `</main>`는 `index.html:1119`에 위치하고 파일은 1205줄. 1119~1205줄 사이에 fastRecord 모달, dt-popup 등 외부 요소가 있음.
**근거**: `index.html:1117-1205`
**영향**: 삽입 위치 자체는 정확함 (`</main>` 앞). "파일 말미"라는 표현이 혼동을 줄 수 있음.
**수정**: "파일 말미" → "1117줄 (#pane-pid 종료)"로 정정

---

**종합**: 플랜 실행 가능. CSS 변수, 헬퍼 함수, 클래스, 백엔드 응답 형식 등 모든 전제가 검증됨. 발견된 2개 이슈 모두 LOW (표현 명확화 수준).

---

## 0. 현황 진단

### UI 반영 여부
| 항목 | 상태 |
|------|------|
| 사이드바 탭 (`data-tab="evt"`) | ❌ 없음 |
| `#pane-evt` 섹션 | ❌ 없음 |
| JS 함수 (`evtQuery` 등) | ❌ 없음 |
| CSS 이벤트 배지 스타일 | ❌ 없음 |

**결론**: 백엔드 API 3개(`GET /api/event-history`, `/summary`, `/digital-tags`)는 완전 구현·동작 중이나 **UI가 전무**하여 접근 불가.

### 기존 API 엔드포인트 (구현 완료)
| 엔드포인트 | 설명 | 주요 파라미터 |
|-----------|------|--------------|
| `GET /api/event-history` | 이벤트 조회 | tagName, area, section, eventType, from, to, limit |
| `GET /api/event-history/summary` | 구간별 집계 | area, section, from, to |
| `GET /api/event-history/digital-tags` | 디지털 태그 목록 | — |

### 응답 형식
```json
// GET /api/event-history
{
  "success": true,
  "count": 42,
  "data": [
    {
      "id": 1,
      "tagName": "P6-FIC101.instate0",
      "nodeId": "...",
      "prevValue": "RUN",
      "currValue": "L-STOP",
      "eventType": "TRIP",
      "eventTime": "2026-05-11T03:00:00Z",
      "area": "P6",
      "section": "1-2차",
      "durationSeconds": 3600,
      "metadata": null
    }
  ]
}

// GET /api/event-history/summary
{
  "success": true,
  "count": 3,
  "data": [
    {
      "section": "1-2차",
      "totalEvents": 10,
      "tripCount": 3,
      "runCount": 3,
      "alarmCount": 2,
      "changeCount": 2
    }
  ]
}
```

---

## 1. 구현 대상 파일

| 파일 | 수정 내용 |
|------|----------|
| `src/Web/wwwroot/index.html` | 탭 항목(12번) + `#pane-evt` 섹션 추가 |
| `src/Web/wwwroot/js/app.js` | `evtLoadTags`, `evtQuery`, `evtSummary`, `evtBuildTable`, `evtBuildSummary`, `evtReset` 함수 추가 |
| `src/Web/wwwroot/css/style.css` | `.evt-badge`, `.evt-summary-*` 스타일 추가 |

---

## 2. 구현 순서 (Dependency Graph)

```
Step 1: CSS — .evt-badge, .evt-summary-grid, .evt-summary-item
   ↓
Step 2: HTML — 사이드바 탭 항목(12번) 추가
   ↓
Step 3: HTML — #pane-evt 섹션 전체 구조
   ↓
Step 4: JS — evtLoadTags(), evtQuery(), evtSummary()
   ↓
Step 5: JS — evtBuildTable(), evtBuildSummary(), evtReset()
   ↓
Step 6: 검증
```

---

## 3. Step-by-Step 코딩 계획

### Step 1: CSS 추가

**파일**: `src/Web/wwwroot/css/style.css`
**위치**: 파일 말미 (현재 1489줄 끝)

```css
/* ── Event History ─────────────────────────────────────────── */
.evt-badge {
  display: inline-block;
  font-family: var(--fm); font-size: 10px; font-weight: 700;
  letter-spacing: .06em; padding: 2px 8px; border-radius: 3px;
  text-transform: uppercase; white-space: nowrap;
}
.evt-badge.trip   { background: rgba(239,68,68,.18);   color: #f87171; }
.evt-badge.run    { background: rgba(16,185,129,.18);  color: #34d399; }
.evt-badge.alarm  { background: rgba(245,158,11,.18);  color: #fbbf24; }
.evt-badge.normal { background: rgba(148,163,184,.18); color: #94a3b8; }
.evt-badge.change { background: rgba(96,165,250,.18);  color: #60a5fa; }

.evt-summary-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 12px;
  margin-top: 12px;
}

.evt-summary-item {
  background: var(--s2);
  border: 1px solid var(--bd);
  border-radius: var(--r);
  padding: 14px 16px;
}

.evt-summary-section {
  font-family: var(--fm); font-size: 13px; font-weight: 700;
  color: var(--t0); margin-bottom: 10px;
}

.evt-summary-counts {
  display: flex; gap: 10px; flex-wrap: wrap;
}

.evt-count {
  display: flex; align-items: center; gap: 5px;
  font-family: var(--fm); font-size: 11px; color: var(--t2);
}

.evt-count strong { font-size: 14px; color: var(--t0); }

.evt-total {
  font-family: var(--fm); font-size: 11px; color: var(--t2);
  margin-top: 8px; padding-top: 8px;
  border-top: 1px solid var(--bd);
}

.evt-total strong { color: var(--t0); }
```

**검증**:
- [ ] `.evt-badge.trip` 의 색상이 `#f87171` (red-400) 계열인지 확인
- [ ] `.evt-summary-grid`가 반응형으로 동작하는지 확인 (`auto-fill, minmax`)

---

### Step 2: HTML — 사이드바 탭 항목 추가

**파일**: `src/Web/wwwroot/index.html`
**위치**: `<li class="nav-item" data-tab="pid">` 블록 (현재 75-78줄) **뒤**

```html
      <li class="nav-item" data-tab="evt">
        <span class="ni">12</span>
        <span class="nl">이벤트 히스토리</span>
      </li>
```

**검증**:
- [ ] 탭 번호가 기존 11번(P&ID 추출) 다음인지 확인
- [ ] `data-tab="evt"`가 `#pane-evt`와 매핑되는지 확인 (탭 클릭 핸들러가 `pane-${tab}` 패턴 사용)

---

### Step 3: HTML — #pane-evt 섹션 추가

**파일**: `src/Web/wwwroot/index.html`
**위치**: `#pane-pid` 섹션 (현재 파일 말미) 바로 뒤, `</main>` 닫는 태그 앞

```html
    <!-- ══════════════════════════════════════════════════════
         12  이벤트 히스토리
    ═══════════════════════════════════════════════════════ -->
    <section class="pane" id="pane-evt">
      <header class="pane-hdr">
        <div>
          <h1>이벤트 히스토리</h1>
          <p>디지털 포인트 상태 변경 이벤트를 조회합니다. (event_history_table)</p>
        </div>
        <div class="pane-tag">EVENT / DIGITAL</div>
      </header>

      <!-- 조회 조건 카드 -->
      <div class="card">
        <div class="card-cap">조회 조건</div>

        <!-- 태그 필터 -->
        <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:13px">
          <span>태그 필터</span>
          <button class="btn-b btn-sm" onclick="evtLoadTags()">▼ 태그 목록 불러오기</button>
          <span id="evt-tag-status" class="hist-status"></span>
        </div>
        <div class="fg">
          <select id="ef-tag" class="inp">
            <option value="">— 전체 태그 —</option>
          </select>
        </div>

        <!-- 필터 4열 -->
        <div class="cols-4">
          <div class="fg">
            <label>이벤트 타입</label>
            <select id="ef-event-type" class="inp">
              <option value="">전체</option>
              <option value="TRIP">TRIP</option>
              <option value="RUN">RUN</option>
              <option value="ALARM">ALARM</option>
              <option value="NORMAL">NORMAL</option>
              <option value="CHANGE">CHANGE</option>
            </select>
          </div>
          <div class="fg">
            <label>Area <em>(예: P6)</em></label>
            <input id="ef-area" class="inp" type="text" placeholder="비워두면 전체"/>
          </div>
          <div class="fg">
            <label>Section <em>(예: 1-2차)</em></label>
            <input id="ef-section" class="inp" type="text" placeholder="비워두면 전체"/>
          </div>
          <div class="fg">
            <label>최대 행 수</label>
            <input id="ef-limit" class="inp" type="number" value="500" min="10" max="5000"/>
          </div>
        </div>

        <!-- 시간 범위 -->
        <div class="cols-2">
          <div class="fg">
            <label>시작 시간</label>
            <input type="hidden" id="hf-evt-from"/>
            <div class="dt-display inp" id="dtp-evt-from-display" onclick="dtOpen('evt-from')">— 선택 안 함 —</div>
          </div>
          <div class="fg">
            <label>종료 시간</label>
            <input type="hidden" id="hf-evt-to"/>
            <div class="dt-display inp" id="dtp-evt-to-display" onclick="dtOpen('evt-to')">— 선택 안 함 —</div>
          </div>
        </div>

        <div class="btn-row">
          <button class="btn-a" onclick="evtQuery()">🔍 이벤트 조회</button>
          <button class="btn-b" onclick="evtSummary()">📊 구간 요약</button>
          <button class="btn-b" onclick="evtReset()">초기화</button>
        </div>
      </div>

      <!-- 요약 결과 카드 (처음에는 숨김) -->
      <div id="evt-summary-card" class="card hidden">
        <div class="card-cap">구간별 이벤트 요약</div>
        <div id="evt-summary-content"></div>
      </div>

      <!-- 조회 결과 -->
      <div id="evt-result-info" class="nm-result-info hidden" style="margin:8px 0"></div>
      <div id="evt-table" class="tbl-wrap hidden"></div>
    </section>
```

**설계 결정**:
- 날짜 피커: 기존 `dtOpen(target)` 재사용. target이 `'evt-from'`이면 `hf-evt-from`, `dtp-evt-from-display` ID를 자동으로 참조 — **기존 코드 수정 없음**
- `hist-status` 클래스 재사용 (기존 스타일 이미 존재)
- `.hidden` 클래스 재사용 (기존 스타일 이미 존재)
- `nm-result-info` 클래스 재사용 (조회 결과 건수 표시용)

**검증**:
- [ ] `dtOpen('evt-from')` 호출 시 `hf-evt-from`, `dtp-evt-from-display` 를 정확히 참조하는지 확인
- [ ] `class="pane"` (active 없음) — 탭 클릭 시 JS가 active 추가하므로 정상
- [ ] `</main>` 닫는 태그 앞에 삽입했는지 확인

---

### Step 4+5: JS 함수 추가

**파일**: `src/Web/wwwroot/js/app.js`
**위치**: `histReset()` 함수 (현재 1158줄) 블록 **뒤**에 추가

```javascript
// ── Event History ─────────────────────────────────────────────────────────────

async function evtLoadTags() {
  const statusEl = document.getElementById('evt-tag-status');
  statusEl.textContent = '⏳ 조회 중...';
  try {
    const d = await api('GET', '/api/event-history/digital-tags');
    const tags = d.data || [];
    const sel = document.getElementById('ef-tag');
    sel.innerHTML = '<option value="">— 전체 태그 —</option>' +
      tags.map(t => `<option value="${esc(t.tagName)}">${esc(t.tagName)}</option>`).join('');
    statusEl.textContent = `✅ ${tags.length}개`;
  } catch (e) {
    statusEl.textContent = `❌ ${e.message}`;
  }
}

async function evtQuery() {
  const tag       = document.getElementById('ef-tag').value;
  const eventType = document.getElementById('ef-event-type').value;
  const area      = document.getElementById('ef-area').value.trim();
  const section   = document.getElementById('ef-section').value.trim();
  const limit     = document.getElementById('ef-limit').value || 500;
  const fromRaw   = document.getElementById('hf-evt-from').value;
  const toRaw     = document.getElementById('hf-evt-to').value;

  const params = new URLSearchParams();
  if (tag)       params.set('tagName',   tag);
  if (eventType) params.set('eventType', eventType);
  if (area)      params.set('area',      area);
  if (section)   params.set('section',   section);
  params.set('limit', limit);
  if (fromRaw) params.set('from', new Date(fromRaw).toISOString());
  if (toRaw)   params.set('to',   new Date(toRaw).toISOString());

  const infoEl  = document.getElementById('evt-result-info');
  const tableEl = document.getElementById('evt-table');
  infoEl.textContent = '⏳ 조회 중...';
  infoEl.classList.remove('hidden');
  tableEl.classList.add('hidden');

  try {
    const d = await api('GET', `/api/event-history?${params}`);
    if (!d.success) throw new Error(d.error || '조회 실패');
    infoEl.textContent = `총 ${d.count}건`;
    tableEl.innerHTML = _evtBuildTable(d.data);
    tableEl.classList.remove('hidden');
  } catch (e) {
    infoEl.textContent = `❌ ${e.message}`;
  }
}

async function evtSummary() {
  const area    = document.getElementById('ef-area').value.trim();
  const section = document.getElementById('ef-section').value.trim();
  const fromRaw = document.getElementById('hf-evt-from').value;
  const toRaw   = document.getElementById('hf-evt-to').value;

  const params = new URLSearchParams();
  if (area)    params.set('area',    area);
  if (section) params.set('section', section);
  if (fromRaw) params.set('from', new Date(fromRaw).toISOString());
  if (toRaw)   params.set('to',   new Date(toRaw).toISOString());

  const card    = document.getElementById('evt-summary-card');
  const content = document.getElementById('evt-summary-content');
  content.textContent = '⏳ 집계 중...';
  card.classList.remove('hidden');

  try {
    const d = await api('GET', `/api/event-history/summary?${params}`);
    if (!d.success) throw new Error(d.error || '조회 실패');
    content.innerHTML = _evtBuildSummary(d.data);
  } catch (e) {
    content.textContent = `❌ ${e.message}`;
  }
}

function _evtBadge(t) {
  const cls = { TRIP:'trip', RUN:'run', ALARM:'alarm', NORMAL:'normal', CHANGE:'change' }[t] || 'change';
  return `<span class="evt-badge ${cls}">${esc(t)}</span>`;
}

function _evtFmtTime(dt) {
  if (!dt) return '—';
  return new Date(dt).toLocaleString('ko-KR', {
    year:'numeric', month:'2-digit', day:'2-digit',
    hour:'2-digit', minute:'2-digit', second:'2-digit', hour12:false
  });
}

function _evtBuildTable(rows) {
  if (!rows || !rows.length)
    return '<div style="padding:24px;text-align:center;color:var(--t2)">데이터 없음</div>';
  const html = rows.map(r => `
    <tr>
      <td style="white-space:nowrap;color:var(--t2);font-family:var(--fm);font-size:11px">${_evtFmtTime(r.eventTime)}</td>
      <td><code style="font-size:11px;color:var(--blu)">${esc(r.tagName)}</code></td>
      <td>${_evtBadge(r.eventType)}</td>
      <td style="color:var(--t2)">${esc(r.prevValue ?? '—')}</td>
      <td style="color:var(--t0);font-weight:600">${esc(r.currValue)}</td>
      <td>${r.area    ? `<span class="nm-cls">${esc(r.area)}</span>`    : '—'}</td>
      <td>${r.section ? `<span class="nm-cls">${esc(r.section)}</span>` : '—'}</td>
      <td style="font-family:var(--fm);font-size:11px;color:var(--t2)">${r.durationSeconds != null ? r.durationSeconds + 's' : '—'}</td>
    </tr>`).join('');
  return `
    <table>
      <thead><tr>
        <th>시간</th>
        <th>태그명</th>
        <th>이벤트</th>
        <th>이전값</th>
        <th>현재값</th>
        <th>Area</th>
        <th>Section</th>
        <th>지속(초)</th>
      </tr></thead>
      <tbody>${html}</tbody>
    </table>`;
}

function _evtBuildSummary(data) {
  if (!data || !data.length)
    return '<div style="padding:12px;color:var(--t2)">데이터 없음</div>';
  return `<div class="evt-summary-grid">${data.map(s => `
    <div class="evt-summary-item">
      <div class="evt-summary-section">${esc(s.section)}</div>
      <div class="evt-summary-counts">
        <div class="evt-count">${_evtBadge('TRIP')} <strong>${s.tripCount}</strong></div>
        <div class="evt-count">${_evtBadge('RUN')} <strong>${s.runCount}</strong></div>
        <div class="evt-count">${_evtBadge('ALARM')} <strong>${s.alarmCount}</strong></div>
        <div class="evt-count">${_evtBadge('CHANGE')} <strong>${s.changeCount}</strong></div>
      </div>
      <div class="evt-total">합계 <strong>${s.totalEvents}</strong>건</div>
    </div>`).join('')}</div>`;
}

function evtReset() {
  document.getElementById('ef-tag').value        = '';
  document.getElementById('ef-event-type').value = '';
  document.getElementById('ef-area').value        = '';
  document.getElementById('ef-section').value     = '';
  document.getElementById('ef-limit').value       = '500';
  dtClearField('evt-from');
  dtClearField('evt-to');
  document.getElementById('evt-result-info').classList.add('hidden');
  document.getElementById('evt-table').classList.add('hidden');
  document.getElementById('evt-summary-card').classList.add('hidden');
  document.getElementById('evt-tag-status').textContent = '';
}
```

**설계 결정**:
- `_evtBuildTable`, `_evtBuildSummary`, `_evtBadge`, `_evtFmtTime`에 `_` 접두사 → 내부 헬퍼임을 명시
- `dtClearField('evt-from')` 재사용 — 기존 함수가 `hf-${target}`, `dtp-${target}-display` 패턴으로 동작하므로 수정 없음
- 탭 진입 시 자동 API 호출 없음 (탭 핸들러에 `if (tab === 'evt')` 추가 불필요)
- `api()` 헬퍼 함수 재사용 (기존 코드에 이미 정의됨)

**검증**:
- [ ] `api('GET', '/api/event-history/digital-tags')` 응답의 `d.data` 배열 안에 `{ tagName: "..." }` 형태인지 확인
- [ ] `new Date(fromRaw).toISOString()` — `fromRaw`는 `"2026-05-11T03:00"` 형태 → ISO 8601 UTC로 변환됨 (기존 hist 탭과 동일 패턴)
- [ ] `_evtBuildTable(d.data)` — `d.data`가 빈 배열이면 "데이터 없음" 메시지 표시
- [ ] `evtReset()`이 summary 카드도 숨기는지 확인

---

## 4. 파일별 추가 위치 요약

### `style.css`
```
파일 말미 (1489줄 끝) → .evt-badge ~ .evt-total 블록 추가
```

### `index.html`
```
[위치 1] 75-78줄 <li data-tab="pid"> 블록 뒤:
  → <li data-tab="evt"> 탭 항목 추가

[위치 2] </main> 닫는 태그 앞 (현재 파일 말미):
  → #pane-evt 섹션 전체 추가
```

### `app.js`
```
histReset() 함수 끝 (현재 ~1175줄) 뒤:
  → evtLoadTags ~ evtReset 전체 블록 추가
```

**탭 핸들러 수정 없음**: `evt` 탭은 진입 시 API 자동 호출 없음 (사용자가 버튼 클릭 시에만 실행).

---

## 5. 검증 절차

### Stage A — 빌드/렌더링
- [ ] 브라우저에서 사이드바에 "12 이벤트 히스토리" 탭이 보이는지 확인
- [ ] 탭 클릭 시 `#pane-evt`가 활성화되는지 확인

### Stage B — 태그 목록 로드
```
▼ 태그 목록 불러오기 버튼 클릭
→ GET /api/event-history/digital-tags
→ ef-tag 드롭다운에 디지털 태그 목록 채워짐
→ 태그 상태에 "✅ N개" 표시
```

### Stage C — 이벤트 조회
```
시작/종료 시간 선택 → 🔍 이벤트 조회 버튼
→ GET /api/event-history?from=...&to=...&limit=500
→ 결과 테이블 렌더링 (시간, 태그, 배지, 이전값/현재값, Area, Section, 지속시간)
→ 이벤트 타입별 배지 색상 확인 (TRIP:red, RUN:green, ALARM:amber)
```

### Stage D — 구간 요약
```
📊 구간 요약 버튼
→ GET /api/event-history/summary?from=...&to=...
→ evt-summary-card 표시
→ Section별 카드 그리드 렌더링 (TRIP/RUN/ALARM/CHANGE 각 건수)
```

### Stage E — 필터 조합
```
- eventType=TRIP 선택 후 조회 → TRIP 이벤트만 표시
- area=P6 입력 후 조회 → P6 area만 표시
- tagName 선택 후 조회 → 해당 태그만 표시
```

### Stage F — 초기화
```
초기화 버튼 → 모든 필터 리셋, 결과 테이블 숨김, 요약 카드 숨김
```

---

## 6. 주의사항

1. **날짜 피커 재사용**: `dtOpen('evt-from')` 호출 시 `hf-evt-from`(hidden input), `dtp-evt-from-display`(표시 div) ID를 자동 참조. **기존 dt-picker 코드 수정 불필요**.

2. **탭 진입 시 API 없음**: 탭 핸들러 (`app.js` 7-18줄)에 `evt` 케이스 추가 불필요. 사이드 이펙트 없음.

3. **from/to 기본값**: from/to 미입력 시 백엔드에서 자동으로 최근 1일 (`DateTime.UtcNow.AddDays(-1)` ~ `DateTime.UtcNow`) 적용. 빈 파라미터 전송 시에도 정상 동작.

4. **`esc()` 필수**: XSS 방지를 위해 서버 응답의 모든 문자열 필드에 `esc()` 적용. 특히 tagName, currValue, prevValue.

5. **`nm-cls` 재사용**: Area/Section 표시에 기존 `.nm-cls` 배지 스타일 재사용 (별도 CSS 불필요).

6. **`hist-status` 재사용**: 태그 로드 상태 표시에 기존 `.hist-status` 클래스 재사용.

---

## 7. Todo List

| # | 작업 | 파일 | 상태 | 검증 방법 |
|---|------|------|------|-----------|
| 1 | `.evt-badge` ~ `.evt-total` CSS 추가 | `style.css` | ⬜ | 브라우저 개발자도구 CSS 확인 |
| 2 | `<li data-tab="evt">` 탭 항목 추가 | `index.html` | ⬜ | 사이드바 탭 노출 확인 |
| 3 | `#pane-evt` 섹션 전체 추가 | `index.html` | ⬜ | 탭 클릭 시 pane 활성화 확인 |
| 4 | `evtLoadTags()` ~ `evtReset()` 함수 추가 | `app.js` | ⬜ | 버튼 클릭 → API 호출 확인 |
| 5 | 태그 목록 로드 테스트 | — | ⬜ | 드롭다운에 디지털 태그 표시 |
| 6 | 이벤트 조회 테스트 | — | ⬜ | 결과 테이블 + 배지 렌더링 확인 |
| 7 | 구간 요약 테스트 | — | ⬜ | 요약 카드 그리드 렌더링 확인 |
| 8 | 필터 조합 테스트 | — | ⬜ | TRIP 전용, Area 전용 등 확인 |
| 9 | 초기화 테스트 | — | ⬜ | 모든 필드 리셋 확인 |
