Files
HC900-Crawler/docs/포인트빌더-페이지-구현-명세.md
windpacer d88784635e docs: 작업지시·진단·아키텍처 설계 문서 추가
온도프로파일/PV일관성/PointBuilder/history 작업지시, 신호태그·스팀유량 진단, 베이직아키텍처 재설계, MSDS, LLM채팅 구조 등.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 08:12:01 +09:00

23 KiB
Raw Blame History

포인트빌더 페이지 구현 명세

ExperionCrawler 웹 UI의 Tab #06 — OPC UA node_map_master에서 실시간 모니터링할 포인트를 선택해 realtime_table을 구성하는 페이지


1. 개요

목적: OPC UA 서버의 노드맵(node_map_master 테이블)에서 조건(태그명 패턴, 속성, 데이터타입)으로 필터링하여 실시간 구독할 포인트를 realtime_table에 등록/관리한다.

사용자 플로우:

  1. 그룹별 태그 패턴 입력 → 미리보기로 대상 확인
  2. 원하는 포인트 선택 → 적용(전체 교체) 또는 추가(기존 유지)
  3. 등록된 포인트 목록 확인 / 개별 삭제
  4. 실시간 구독 시작/중지
  5. 메타데이터(desc/area) 갱신
  6. Sub-Area 분류 관리

2. API 엔드포인트

Base: /api/pointbuilder

2.1 POST /api/pointbuilder/preview

조건에 맞는 포인트를 미리보기 (읽기 전용, DB 변경 없음)

Request Body (PointBuilderBuildDto):

{
  "controller1":  { "tagPatterns": ["%ctl-61%.pv","%ctl-62%.sp"], "attributes": ["pv","sp"], "dataType": null },
  "analogmon1":   { "tagPatterns": ["%ti-61%.pv","%pi-62%.pv"],   "attributes": ["pv"],       "dataType": null },
  "digital1":     { "tagPatterns": ["%ys-61%.pv","%yt-62%.pv"],   "attributes": ["pv"],       "dataType": "Boolean" },
  "digital2":     { "tagPatterns": [],                             "attributes": [],           "dataType": null },
  "custom":       { "tagPatterns": [],                             "attributes": [],           "dataType": null }
}
필드 타입 설명
tagPatterns string[] SQL LIKE 패턴 (쉼표 구분 아님 — 프론트에서 분할하여 배열로 전달). node_map_master.NodeId에 대해 LIKE 검색
attributes string[] node_map_master.Name 필터 (pv/sp/op/md 등). 커스텀 속성 입력도 여기에 병합
dataType string? node_map_master.DataType 필터. null이면 전체

group keys: controller1, analogmon1, digital1, digital2, custom

DB 조건:

SELECT ... FROM node_map_master WHERE Level = 3
  AND (NodeId LIKE <pattern1> OR NodeId LIKE <pattern2> ...)
  AND Name IN (<attrs>)
  AND DataType = <dataType>  -- dataType이 null이면 생략

Response:

{
  "count": 42,
  "items": [
    { "nodeId": "ns=1;s=ti-6101.pv", "tagName": "ti-6101.pv", "name": "pv", "dataType": "Double", "group": "analogmon1" }
  ]
}

2.2 POST /api/pointbuilder/build

조건에 맞는 포인트로 기존 realtime_table 전체 교체 (TRUNCATE → INSERT)

  • Request Body: PointBuilderBuildDto (preview와 동일한 구조)
  • 동작: TRUNCATE TABLE realtime_table RESTART IDENTITY 후 조건 매칭된 포인트 INSERT
  • 메타데이터 자동 로드: 빌드 완료 후 IMetadataLoaderService.ReloadMetadataAsync() 호출 (실패해도 Warning 로그만, 빌드는 완료)

Response:

{
  "success": true,
  "count": 42,
  "metaCount": 40,
  "message": "42개 포인트 생성 완료 (메타데이터: 40개)"
}

2.3 POST /api/pointbuilder/apply

미리보기에서 선택한 포인트만으로 realtime_table 전체 교체

  • Request Body: { "selectedNodeIds": ["ns=1;s=ti-6101.pv", "ns=1;s=ficq-6113.pv"] }
  • 동작: TRUNCATE → 선택된 NodeId만 INSERT
  • 메타데이터 자동 로드 포함

Response: { "success": true, "count": 42, "metaCount": 40, "message": "..." }

2.4 POST /api/pointbuilder/append

미리보기에서 선택한 포인트를 기존 데이터에 추가 (중복 제외, 기존 유지)

  • Request Body: { "selectedNodeIds": [...] }
  • 동작: NodeId 기준 중복 체크 후 없는 것만 INSERT
  • 메타데이터 자동 로드 포함

2.5 GET /api/pointbuilder/points

등록된 모든 realtime 포인트 조회

Response:

{
  "total": 42,
  "items": [
    { "id": 1, "tagName": "ti-6101.pv", "nodeId": "ns=1;s=ti-6101.pv", "liveValue": "25.3", "timestamp": "2026-06-08T12:34:56Z" }
  ]
}

2.6 POST /api/pointbuilder/add

Node ID를 직접 입력하여 수동 포인트 추가

Request: { "nodeId": "ns=1;s=my-custom-tag.pv" }

동작:

  1. IExperionDbService.AddRealtimePointAsync() — DB에 INSERT (중복이면 기존 반환)
  2. IExperionRealtimeService.AddMonitoredItemAsync() — 구독 중이면 OPC UA에 핫 추가 + Node ID 유효성 검증
  3. OPC UA 서버가 거부하면 DB 롤백

Response:

{ "success": true, "point": { "id": 43, "tagName": "my-custom-tag.pv", "nodeId": "ns=1;s=my-custom-tag.pv" } }

2.7 DELETE /api/pointbuilder/{id}?purgeHistory=false

포인트 삭제

파라미터 기본값 설명
purgeHistory false true이면 해당 tagname의 history_table 이력까지 영구 삭제 (복구 불가, 명시적 opt-in)

동작:

  1. realtime_table 행 삭제
  2. 같은 base_tag의 잔여 행이 0이면 → tag_metadata(desc/area/sub_area) 고아 정리
  3. purgeHistory=true → DELETE FROM history_table WHERE tagname = ?

Response:

{
  "success": true,
  "message": "삭제 완료",
  "baseTag": "ti-6101",
  "metadataPurged": true,
  "historyRowsDeleted": 1440
}

3. 보조 API 엔드포인트 (같은 페이지에서 사용)

3.1 실시간 구독 제어

Method Endpoint 설명 Request
POST /api/realtime/start 구독 시작 { serverHostName, port, clientHostName, userName, password }
POST /api/realtime/stop 구독 중지
GET /api/realtime/status 상태 조회

status response: { "running": true, "subscribedCount": 42, "message": "..." }

3.2 메타데이터 관리

Method Endpoint 설명
POST /api/tags/metadata/reload OPC UA에서 desc/area 재조회 → tag_metadata UPDATE
GET /api/tags/metadata 모든 tag_metadata 조회

3.3 Sub-Area 관리

Method Endpoint 설명
GET /api/tags/sub-area?area=P6&page=1&pageSize=500 area별 sub_area 현황 조회
PUT /api/tags/sub-area 단일 태그 sub_area 수정. Body: { baseTag, subArea } (null=미분류)
POST /api/tags/sub-area/seed 번호 prefix + pid_equipment로 일괄 분류. Body: { dryRun: bool }

4. UI 레이아웃 (2-Column Grid + 하단 포인트 목록)

┌──────────────────────────────────────────────────────────────┐
│ [pane-hdr] 포인트빌더 — node_map_master → realtime_table 구성 │
├─────────────────────────────┬────────────────────────────────┤
│  [card: 빌더]               │  [card: 수동 추가]              │
│  ┌────────────────────────┐ │  ┌──────────────────────────┐ │
│  │ 카드 제목              │ │  │ Node ID 직접 입력        │ │
│  │ 조건으로 테이블 작성   │ │  │ [inp]                    │ │
│  │                        │ │  │ [btn]  추가            │ │
│  │ 그룹 카드 × 5개       │ │  │ [log]                    │ │
│  │  · 컨트롤러 #1        │ │  └──────────────────────────┘ │
│  │  · 아날로그 모니터 #2 │ │  [card: 구독 제어]            │
│  │  · 디지털 #1          │ │  ┌──────────────────────────┐ │
│  │  · 디지털 #2          │ │  │ 서버IP/포트/계정/비번   │ │
│  │  · 사용자 정의        │ │  │ [btn] ▶ 구독 시작       │ │
│  │  ┌─────────────────┐   │ │  │ [btn] ■ 구독 중지       │ │
│  │  │ 태그명 패턴(inp)│   │ │  │ [btn] 상태 확인         │ │
│  │  │ [pv] [op] [sp]  │   │ │  │ [log: 상태]             │ │
│  │  │ [md] 체크박스    │   │ │  └──────────────────────────┘ │
│  │  │ [추가속성1][추가2]│  │ │                                │
│  │  │ [데이터타입 선택] │   │ │  [card: 메타데이터 관리]      │
│  │  └─────────────────┘   │ │  ┌──────────────────────────┐ │
│  │                        │ │  │ [btn] 🔄 메타데이터 갱신 │ │
│  │  [btn] 미리보기        │ │  │ [btn] 📋 메타데이터 조회 │ │
│  │  [btn] 테이블 작성하기  │ │  │ [log]                    │ │
│  │  [btn] 테이블 조회     │ │  │ [view: metadata table]   │ │
│  │                        │ │  └──────────────────────────┘ │
│  │  [preview: 미리보기]   │ │                                │
│  │  ┌────────────────┐    │ │  [card: Sub-Area 관리]         │
│  │  │ 전체선택/해제   │    │ │  ┌──────────────────────────┐ │
│  │  │ 역전 | 검색창   │    │ │  │ Area 선택 | 조회       │ │
│  │  │ ┌─── 테이블 ──┐│    │ │  │ Seed DryRun | Seed 실행 │ │
│  │  │ │ ☑ ID TagNam ││    │ │  │ [log]                    │ │
│  │  │ │ ☑ 1 ti-6101 ││    │ │  │ [view: subarea table]    │ │
│  │  │ │ ☑ 2 pi-6102 ││    │ │  └──────────────────────────┘ │
│  │  │ └─────────────┘│    │ │                                │
│  │  │ [취소][적용][추가]│   │ │                                │
│  │  └────────────────┘    │ │                                │
│  └────────────────────────┘ │                                │
├─────────────────────────────┴────────────────────────────────┤
│  [card: 포인트 목록 (전체 width)]                            │
│  ┌──────────────────────────────────────────────────────────┐│
│  │ ID | TagName      | LiveValue  | Timestamp    | 이력/삭제││
│  │  1 | TI-6101.PV   | 25.3       | 06-08 12:34 | ☐이력 ✕  ││
│  │  2 | FICQ-6113.PV | 152.7      | 06-08 12:34 | ☐이력 ✕  ││
│  └──────────────────────────────────────────────────────────┘│
└──────────────────────────────────────────────────────────────┘

4.1 그룹 카드 (pb-group-card)

총 5개의 그룹 카드가 존재하며, 각 카드는 동일한 구조:

요소 CSS 셀렉터 설명
헤더 .pb-group-header > .card-sub-cap 그룹명 (컨트롤러 포인트 #1, 아날로그 모니터링 포인트 #2, ...)
태그 패턴 input[data-group="{key}"][data-field="tagPatterns"] 쉼표 구분 LIKE 패턴. placeholder에 예시 표시
속성 체크박스 .pb-attr-checkboxes > label > input[type=checkbox] pv/op/sp/md 4개. data-group, data-field="attributes"
커스텀 속성 .pb-custom-attr-inputs > input[data-field="customAttrs"] 추가 속성 텍스트 입력 2개 (선택)
데이터타입 선택 select[data-field="dataType"] Double / i=7594 / Boolean / String / Int16 / Int32 / UInt16 / UInt32 / Float / DateTime + "(전체)"

data 속성으로 그룹 연결:

  • data-group: controller1 / analogmon1 / digital1 / digital2 / custom
  • data-field: 식별

4.2 미리보기 영역 (pb-preview)

  • 초기 상태: .hidden 클래스로 숨김
  • "미리보기" 버튼 클릭 시 표시
  • 헤더: "미리보기 결과 (N개)" + 액션 버튼들 (전체 선택 / 전체 해제 / 역전 / 선택: X/Y)
  • 검색창: 태그명/NodeId/Name 실시간 필터링 (oninput)
  • 테이블: ☑ 선택체크박스 | # | TagName | NodeType | DataType | Group
    • 선택되지 않은 행: opacity: 0.5
    • Group 뱃지: .group-badge (파란색 배경)
  • 하단 버튼: [취소] [✓ 선택된 포인트 적용하기] [ 기존 데이터에 추가하기]

4.3 포인트 목록 (하단, 전체 폭)

  • 빈 상태: "포인트가 없습니다. 위에서 테이블을 작성하세요." (회색)
  • 테이블: ID | TagName (uppercase, bold) | LiveValue (fmtVal+parseEnumPv 적용) | Timestamp (fmtTs 적용) | 이력체크박스 / 삭제
  • LiveValue가 null이면 표시
  • 삭제 버튼: 붉은색 , confirm dialog 표시
    • "이력" 체크박스 체크 시: ⚠️ 이력(history_table)까지 영구 삭제 메시지
    • 미체크 시: 이력 보존 안내
  • 삭제 완료 후 alert: "삭제 완료: TI-6101 (#1) (메타데이터 정리됨 · 이력 1,440행 삭제)" 등의 상세 정보

4.4 실시간 구독 제어

4개 필드 입력: server IP / port / client host / 계정 / 비밀번호 (password type)

  • 2×2 CSS grid (비밀번호는 grid-column: 1/-1 전체 폭)
  • 버튼: ▶ 구독 시작 / ■ 구독 중지 / 상태 확인
  • 구독 상태 표시: logbox에 running 여부 + 구독 포인트 개수

4.5 Sub-Area 관리

  • Area 선택 드롭다운: P6 / P9 / P10 / P1 / P2
  • 버튼: 📋 조회 / 🧪 Seed DryRun / ⚙️ Seed 실행
  • 조회 결과: 테이블 (BaseTag | Description | Sub-Area select)
    • Sub-Area: <select>에 area별 옵션(예: P6-1, P6-2, P6-1,P6-2(공용)) + 현재값 표시
    • 변경 시 onchange → PUT /api/tags/sub-area

5. 데이터 흐름

5.1 미리보기 → 적용 플로우

[사용자] 그룹 카드에 패턴/속성 입력
    ↓
pbPreview() → POST /api/pointbuilder/preview
    ↓
{ items: [{nodeId, tagName, name, dataType, group}] }
    ↓
pbPreviewData 배열 생성 (각 item에 selected:true, idx 추가)
    ↓
pbRenderPreview() → 체크박스 있는 테이블 렌더링
    ↓
[사용자] 체크박스 조작 / 검색 필터 / 선택/해제
    ↓
pbApplySelected() / pbAppendSelected()
    ↓
POST /api/pointbuilder/apply (또는 /append)
  body: { selectedNodeIds: [...] }
    ↓
DB TRUNCATE + INSERT (apply) / 중복제외 INSERT (append)
    ↓
pbRefresh() → 포인트 목록 재조회
    ↓
포인트 목록 갱신 + 실시간 상태 갱신

5.2 그룹 데이터 수집 (pbCollectGroupData)

function pbCollectGroupData(groupKey) {
  const tagPatterns = document.querySelector(`input[data-group="${groupKey}"][data-field="tagPatterns"]`)
    .value.split(',').map(s => s.trim()).filter(Boolean);

  const checkedAttrs = Array.from(
    document.querySelectorAll(`input[data-group="${groupKey}"][data-field="attributes"]:checked`)
  ).map(cb => cb.value);

  const customInputs = document.querySelectorAll(`input[data-group="${groupKey}"][data-field="customAttrs"]`);
  customInputs.forEach(inp => {
    if (inp.value.trim()) checkedAttrs.push(inp.value.trim());
  });

  const dataType = document.querySelector(`select[data-group="${groupKey}"][data-field="dataType"]`)?.value || null;

  return { tagPatterns, attributes: checkedAttrs, dataType };
}

5.3 삭제 플로우

[사용자] "이력" 체크박스 선택 여부 → confirm 대화상자
    ↓
pbDelete(id, tagName) → DELETE /api/pointbuilder/{id}?purgeHistory={bool}
    ↓
{ success, baseTag, metadataPurged, historyRowsDeleted }
    ↓
alert("삭제 완료: TI-6101.PV (메타데이터 정리됨 · 이력 1,440행 삭제)")
    ↓
pbRefresh() → 목록 갱신

6. UI 상태 관리

6.1 전역 상태 (core.js)

함수 용도
setGlobal(type, msg) 하단 상태바 업데이트. type: 'busy'/'ok'/'err'
esc(str) HTML 이스케이프
api(method, url, body?) fetch wrapper + JSON 파싱
fmtVal(val) 숫자 포맷팅
fmtTs(iso) UTC ISO → KST locale 문자열 변환
parseEnumPv(val) enum 값 파싱
log(elId, entries) logbox에 메시지 추가. entries: `[{c: 'ok'
setGlobal('busy', '포인트 빌드 중') 빌드/적용 중 busy 표시
setGlobal('ok', '구독 중') 성공 표시

6.2 지역 상태 (pb.js)

const PB_GROUPS = ['controller1', 'analogmon1', 'digital1', 'digital2', 'custom'];
let pbPreviewData = [];  // [{ nodeId, tagName, name, dataType, group, selected, idx }]

7. CSS 구조

7.1 pb.css 전용 스타일

.pb-group-card       /* 그룹 카드 배경/테두리 */
.pb-group-header     /* 그룹 헤더 flex */
.pb-pattern-input    /* 패턴 입력창 full width */
.pb-attr-checkboxes  /* 체크박스 가로 배치 */
.pb-custom-attr-inputs  /* 커스텀 속성 입력 flex */
.pb-datatype-select  /* 데이터타입 선택 (max-width 260px) */
.pb-preview          /* 미리보기 영역 */
.pb-preview-header   /* 미리보기 헤더 */
.pb-preview-actions  /* 액션 버튼 flex */
.pb-preview table th:first-child, td:first-child  /* 체크박스 컬럼 36px */
.pb-preview .group-badge  /* 그룹명 뱃지 */

7.2 공유 CSS 클래스 (style.css)

클래스 용도
.card 카드 컨테이너
.card-cap 섹션 제목
.card-sub-cap 그룹 서브제목
.fg 폼 그룹 (label + input)
.inp 입력 필드
.btn-a 주요 버튼 (파랑)
.btn-b 보조 버튼 (테두리)
.btn-sm 작은 버튼
.btn-row 버튼 행 flex
.cols-2 2열 CSS grid
.tbl-wrap 테이블 래퍼 (overflow-x: auto)
.logbox 로그 출력 영역
.hidden display: none !important
.mut 회색 텍스트
.mono 고정폭 폰트

8. DB 테이블 구조 (참조)

8.1 node_map_master (소스)

컬럼: Id, Level, NodeClass, Name, NodeId, DataType, DisplayName, HasChildren

PointBuilder 쿼리 조건: Level = 3 (Variable), NodeId LIKE, Name IN, DataType =

8.2 realtime_table (대상)

컬럼: Id, TagName, NodeId, LiveValue, Timestamp

TagName = node_map_master.NodeId 에서 ns=1;s= prefix 제거 후 . 포함 전체 (예: ti-6101.pv)

8.3 tag_metadata

컬럼: Id, BaseTag, Attribute, Value, LoadedAt

EAV 패턴: attribute = desc / area / sub_area / state0descriptor ~ state7descriptor


9. 중요 구현 규칙

9.1 JSON camelCase 필수

PropertyNamingPolicy = null이므로 C# DTO의 PascalCase가 그대로 JSON 키가 된다. 프론트는 모든 응답을 camelCase로 가정하므로 Controller의 Ok()는 반드시 익명 객체에 camelCase 키를 명시:

// ✅ 올바름
return Ok(new { success = true, count = x, items = ... });

// ❌ 금지 (JS가 undefined 받음)
return Ok(new { Success = true, Count = x });
return Ok(myDto);   // typed DTO 그대로 반환 금지

9.2 미리보기 데이터의 selected 상태

프론트에서만 관리: pbPreviewData[i].selected (초기값 true). 서버에 저장하지 않음.

9.3 삭제 시 "이력" 체크박스 의미

  • 기본값: false (이력 보존, purgeHistory=false)
  • 체크 시: purgeHistory=trueDELETE FROM history_table WHERE tagname = ? (복구 불가)
  • confirm() 대화상자에 미리 경고문 표시

9.4 메타데이터 자동 로드 실패 허용

Build/Apply/Append 후 메타데이터 로드는 실패해도 Warning 로그만 남기고 사용자에게는 빌드 성공으로 표시. 프론트 응답의 metaCount 필드로 정보 제공.

9.5 수동 추가 시 OPC UA 검증

수동 추가 → AddMonitoredItemAsync()에서 OPC UA 서버가 NodeId를 거부하면:

  1. DB에 먼저 INSERT
  2. OPC UA 검증 실패 시 DeleteRealtimePointAsync()로 롤백
  3. 프론트에 실패 메시지 반환

9.6 Sub-Area 토큰 매칭

공용 설비는 "P6-1,P6-2" 형식으로 저장. 매칭은 항상:

'P6-1' = ANY(string_to_array(value, ','))

직접 비교 (value = 'P6-1') 금지 — 공용 태그 누락 방지.


10. 전체 함수 목록 (pb.js)

함수 설명
pbCollectGroupData(groupKey) 그룹 카드 데이터 수집 → {tagPatterns, attributes, dataType}
pbBuild() POST /build (전체 교체)
pbPreview() POST /preview + 미리보기 렌더링
pbRenderPreview(data) 미리보기 테이블 렌더링
pbPreviewToggleItem(idx) 개별 체크박스 토글
pbPreviewToggleAll(checked) 전체 선택/해제
pbPreviewSelectAll() 모두 선택
pbPreviewDeselectAll() 모두 해제
pbPreviewInvert() 역전
pbGetFilteredPreview() 검색어로 필터링
pbPreviewFilter() oninput 핸들러
pbUpdatePreviewCount() 선택 개수 업데이트
pbCancelPreview() 미리보기 닫기 + 데이터 초기화
pbApplySelected() POST /apply (선택만 적용)
pbAppendSelected() POST /append (선택만 추가)
pbRefresh() GET /points → 목록 갱신
pbRender(points) 포인트 목록 테이블 렌더링
pbAddManual() POST /add (수동 추가)
pbDelete(id, tagName) DELETE (이력 체크 포함)
rtStart() POST /realtime/start
rtStop() POST /realtime/stop
rtStatus() GET /realtime/status
metaReload() POST /tags/metadata/reload
metaView() GET /tags/metadata
subAreaLoad() GET /tags/sub-area
subAreaUpdate(baseTag, subArea) PUT /tags/sub-area
subAreaSeed(dryRun) POST /tags/sub-area/seed
subAreaLabel(code) sub_area 코드 → 라벨 변환

11. HTML/JS/CSS 파일 목록

파일 역할
src/Web/wwwroot/index.html shell — nav item #06 "포인트빌더", pane #pane-pb, script 로드
src/Web/wwwroot/panes/pb.html pane HTML (311 lines)
src/Web/wwwroot/js/pb.js pane JS (503 lines)
src/Web/wwwroot/css/pb.css pane CSS (106 lines)
src/Web/Controllers/ExperionControllers.cs ExperionPointBuilderController (190 lines)
src/Core/Application/DTOs/ExperionDtos.cs PointBuilderGroupDto, PointBuilderBuildDto, PointBuilderPreviewItem, PointBuilderPreviewResult, PointBuilderApplyDto, PointBuilderAddDto
src/Core/Application/DTOs/SubAreaDtos.cs PointDeleteResult
src/Infrastructure/Database/ExperionDbContext.cs BuildRealtimeTableAsync, PreviewRealtimeBuildAsync, ApplySelectedPointsAsync, AppendPointsAsync, GetRealtimePointsAsync, AddRealtimePointAsync, DeleteRealtimePointAsync
src/Core/Application/Interfaces/IExperionServices.cs IExperionDbService (lines 82-92)
src/Web/wwwroot/js/core.js 공유 유틸: api, esc, setGlobal, fmtVal, fmtTs, parseEnumPv, log
src/Web/wwwroot/css/style.css 디자인 시스템: 카드, 폼, 버튼, 테이블, 로그박스, CSS 커스텀 속성