Files
HC900-Crawler/docs/diagnosis-signal-tag-pv-missing.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

19 KiB

진단 보고서 — 신호태그 .PV 누락으로 인한 history_table 분절

진단일: 2026-06-08
발생일: 2026-06-03 (commit 7409fab 컬럼명칭 통일) 이후 신규 데이터부터
영향: history_table 1억 7,900만 행 중 5,180만 행 (19GB) 이력 분절
등급: 🔴 HIGH — 데이터 무결성 결함


STEP 1 — 맥락 파악

질문: 이 문제는 무엇을 하는 시스템의 문제인가?

HC900 프로세스 컨트롤러 → Modbus TCP → C++ 게이트웨이 → gRPC → C# 크롤러 → PostgreSQL

HC900 ──Modbus TCP──▶ C++ Gateway ──gRPC──▶ C# Hc900Crawler ──▶ PostgreSQL (hc900.history_table)

세 가지 Python 스크립트가 태그명 생성의 근원:

스크립트 역할 파일
build_register_map.py HC Designer CSV → register-map.json scripts/build_register_map.py
load_map_master.py register-map.json → hc900_map_master 테이블 scripts/load_map_master.py
Hc900RealtimeService.cs 게이트웨이 폴링 → realtime_tablehistory_table src/Infrastructure/Hc900/Hc900RealtimeService.cs

이 단계를 건너뛰면 "의도적 설계"를 "버그"로 오인한다.


STEP 2 — 구조 탐색

의존 파일 및 데이터 흐름:

docs/SignalTags.csv          ← HC Designer export (원본)
docs/SummaryFucntionBlockReport.csv   ← 루프 정의 (원본)
docs/Variables.csv           ← 변수 정의 (원본)
        │
        ▼  build_register_map.py
docs/register-map.json       ← 게이트웨이 레지스터 맵
        │
        ▼  load_map_master.py
PostgreSQL hc900_map_master  ← DB 매핑 테이블
        │
        ▼  Hc900RealtimeService (C#)
PostgreSQL realtime_table → history_table

핵심 발견: build_register_map.py는 루프와 신호태그를 서로 다른 로직으로 처리한다.


STEP 3 — 코드 읽기

build_register_map.py — 루프 처리부 (line 153-168)

# line 159: 루프는 Experion point name을 재구성
tag_name = f"{loop['tag']}.{name}"
# 결과: "FICQ-6101.PV", "FICQ-6101.SP", "FICQ-6101.OP"  ← Experion ItemName과 일치 ✓

LOOP_PARAM_OFFSETS에서 .PV, .SP, .OP 등을 붙여 Experion 시스템의 point name 형식을 재구성한다.

build_register_map.py — 신호태그 처리부 (line 171-180)

# line 171-180: 신호태그는 CSV raw tag를 그대로 사용
for sig in signals:
    registers.append({
        "tag": sig["tag"],    문제: CSV row[2] 그대로, .PV suffix 없음
        "addr": sig["addr"],
        ...
    })
# 결과: "TI-6111B"  ← Experion의 "TI-6111B.PV"와 불일치 ✗

근거: parse_signal_or_variable_csv() (line 102-140)에서 tag = row[2].strip() — HC Designer CSV의 3번째 컬럼을 그대로 가져온다. HC Designer export에는 .PV suffix가 붙어있지 않다.

load_map_master.py — DB 로딩 (line 59-65)

# line 64: tagname == hc900_tag == e["tag"]
cur.execute(
    "INSERT INTO hc900_map_master (tagname, hc900_tag, ...) VALUES (%s, %s, ...)",
    (e["tag"], e["tag"], ...))

register-map의 tag 필드를 tagnamehc900_tag동일하게 저장. register-map tag가 .PV 붙으면 자동으로 따라옴. 수정 불필요.

Hc900RealtimeService.cs — 데이터 기록 (line 203, 227-231)

// line 203: 게이트웨이 응답을 tagLookup으로 매칭
if (tagLookup.TryGetValue(tv.TagName, out var tagnames))
{
    foreach (var tagname in tagnames)
    {
        rows.Add((tagname, livevalue, ts));  // ← tagname이 DB에 기록됨
    }
}

// line 227-231: realtime_table INSERT
INSERT INTO realtime_table (controller_id, tagname, ..., livevalue, timestamp)
  VALUES ($1, $2, ..., $3, $4)
  ON CONFLICT (controller_id, tagname) DO UPDATE ...

전구간 tag name 변형 없음. register-map tag가 그대로 realtime_tablehistory_table에 기록된다.


STEP 4 — 호출 계층 지도

HC Designer CSV (SignalTags.csv)
  │  row[2] = "TI-6111B"  ← .PV 없음
  ▼
build_register_map.py:131  tag = row[2].strip()
  │  line 159 (루프): f"{loop['tag']}.{name}"  → "FICQ-6101.PV"  ✓
  │  line 171 (신호): sig["tag"]               → "TI-6111B"      ✗
  ▼
register-map.json: "tag": "TI-6111B"
  ▼
load_map_master.py:64  tagname == hc900_tag == "TI-6111B"
  ▼
C++ gateway (gateway.cpp:80, 169, 189)  ← tag field 그대로 전달, 변형 없음
  ▼
C# Hc900RealtimeService (line 203, 227)  ← tagname 그대로 realtime_table INSERT
  ▼
history_table (SnapshotToHistoryAsync)  ← realtime_table에서 복사

결론: tag name 결정은 build_register_map.py line 171에서 한 번에 발생. 이후 전구간 무변형 전달.


STEP 5 — 패턴 매칭 (체크리스트 순회)

🔴 런타임 즉시 실패

체크 항목 판단
[x] 데이터 불일치 Experion dump 원본(TI-6111B.PV) vs 우리 추출(TI-6111B)
[ ] 미정의 변수·함수 참조 해당 없음
[ ] 잘못된 타입 해당 없음

🟠 데이터 무결성

체크 항목 판단
[x] 이력 분절 같은 물리 신호가 두 이름으로 history_table에 분산 저장
[x] 과거 데이터 권위 Experion dump(.PV 포함)가 현장 권위 원본
[ ] Race Condition 해당 없음

🟡 코드 구조

체크 항목 판단
[x] 처리 로직 불일치 루프는 Experion point name 재구성, 신호태그는 raw 사용
[ ] 설정 하드코딩 해당 없음

STEP 6 — 교차 검증

STEP 5에서 발견한 각 항목에 대해 4개 질문 모두 확인:

항목 1: 신호태그 .PV 누락 (HIGH)

질문 확인 결과
Q1. 이미 수정된 문제인가? build_register_map.py 현재 상태 grep 아니오 — line 171 여전히 sig["tag"]
Q2. 다른 레이어에서 처리되고 있는가? C++ gateway, C# RealtimeService grep 아니오 — 전구간 무변형 전달
Q3. 의도적 설계인가? 문서·주석 확인 아니오 — 루프는 .PV 붙이는데 신호는 안 붙이는 이유 없음
Q4. 실제 장애 시나리오가 있는가? history_table에서 TI-6111B.PV vs TI-6111B 분산 — 5,180만 행 분절, 시계열 조회 시 끊김

→ Q1~Q4 통과. 보고서 포함.

항목 2: 이력 분절 (HIGH)

질문 확인 결과
Q1. 이미 수정된 문제인가? history_table 행 수 확인 아니오 — 5,180만 행 여전히 옛 이름으로 존재
Q2. 다른 레이어에서 처리되고 있는가? rename 훅, 데이터 이관 로직 grep 아니오 — tagname 변경 시 이관 코드 없음
Q3. 의도적 설계인가? 아키텍처 문서 확인 아니오 — 단일 신호가 두 이름으로 저장되는 것은 의도 아님
Q4. 실제 장애 시나리오가 있는가? 시계열 조회 시 두 이름 모두 검색 필요 — 운영상 실수 유발

→ Q1~Q4 통과. 보고서 포함.


STEP 7 — 심각도 분류

등급 항목 기준
🔴 HIGH 신호태그 .PV 누락 데이터 무결성 결함. Experion dump 원본과 불일치. 재현 가능
🔴 HIGH history_table 이력 분절 5,180만 행 분산. 시계열 조회 시 끊김. 데이터 손실과 동등

STEP 8 — 진단 결과 및 수정 계획

[1]. 신호태그 .PV 누락 (HIGH)

문제: build_register_map.py가 신호태그(CSV row[2])를 Experion point name 규칙(TAGNAME.PV) 없이 그대로 register-map에 기록. 루프는 .PV 등을 붙이는데 신호태그만 누락.

근거: scripts/build_register_map.py:171"tag": sig["tag"] (CSV raw tag, .PV suffix 없음)

영향: Experion dump 원본(TI-6111B.PV)과 불일치 → history_table에 같은 물리 신호가 두 이름으로 분산 저장. 5,180만 행 분절.

수정: build_register_map.py line 171 부근에서 신호태그에 .PV suffix 추가.

# 수정 전 (line 171-180):
for sig in signals:
    registers.append({
        "tag": sig["tag"],  # "TI-6111B"
        ...
    })

# 수정 후:
for sig in signals:
    # Experion point naming convention: TI-6111B → TI-6111B.PV
    experion_tag = sig["tag"] + ".PV"
    registers.append({
        "tag": experion_tag,  # "TI-6111B.PV"
        ...
    })

변수태그 확인 필요: Variables.csv의 tag name이 Experion에서 어떻게 매핑되는지 확인. 변수는 R/W 태그로 HC900 내부 변수이므로 .PV 규칙이 적용되는지 별도 검증 필요.


[2]. history_table 이력 분절 (HIGH)

문제: tagname 변경 시 과거 데이터 이관 훅이 없음. 6/3 이후 신규 데이터가 옛 이름(.PV 없음)으로 쌓이면서 같은 물리 신호가 두 이름으로 분절.

근거: Hc900RealtimeService.csrealtime_tablehistory_table 복사 시 tagname 변환 로직 없음. Hc900DbContext.cs SnapshotToHistoryAsync도 tagname 그대로 복사.

영향: 시계열 조회 시 두 이름 모두 검색 필요. 운영상 실수 유발.

수정: 단계별 진행 필요 (아래 STEP 9~11 참조).


STEP 9 — 수정 실행 계획 (완료 서명)

단계 1: build_register_map.py 수정

  • 신호태그 처리부(line 171)에 .PV suffix 추가
  • 변수태그 처리부(line 183)도 Experion point name 규칙 적용 여부 확인
  • 수정 후 register-map 재생성 테스트

수정자: ____________________
작성일: //____
검토자: ____________________
검토일: //____


단계 2: register-map 재생성 및 DB 반영

  • 수정된 build_register_map.py로 register-map 재생성
  • load_map_master.pyhc900_map_master 갱신
  • hc900_map_master에서 신호태그 tagname에 .PV가 붙었는지 확인
-- 확인 쿼리
SELECT tagname, hc900_tag FROM hc900_map_master
WHERE tagname ~ '^[TFDLI]P?[A-Z]?-\d{4,5}$'
  AND tagname NOT LIKE '%.%'
LIMIT 20;
-- 결과가 없어야 정상 (모든 신호태그에 .PV 붙음)

실행자: ____________________
실행일: //____
검토자: ____________________
검토일: //____


단계 3: history_table 과거 데이터 정합화

  • history_table에서 옛 이름(.PV 없음) 행 수 확인
-- 옛 이름 잔존 행 수 확인
SELECT COUNT(*) FROM hc900.history_table
WHERE tagname ~ '^[TFDLI]P?[A-Z]?-\d{4,5}$'
  AND tagname NOT LIKE '%.%'
  AND tagname NOT IN (
    -- 변수태그 등 .PV 없이 정상인 태그 제외
    SELECT tagname FROM hc900.map_master WHERE ...
  );
  • 옛 이름 → 새 이름(.PV 붙임) UPDATE 실행
  • UPDATE 후 행 수 확인 (분절 없이 단일 이름으로 통합)
-- UPDATE 예시 (본격 실행 전 반드시 트랜잭션으로 테스트)
BEGIN;
UPDATE hc900.history_table
SET tagname = tagname || '.PV'
WHERE tagname = 'TI-6111B';
-- 확인: SELECT COUNT(*) WHERE tagname = 'TI-6111B.PV'
COMMIT;

실행자: ____________________
실행일: //____
검토자: ____________________
검토일: //____


단계 4: 재발 방지 설계

  • hc900_map_master.tagname 변경 시 history_table 이관을 강제하는 절차 문서화
  • 또는: 장기 저장 식별자를 tagname이 아닌 안정 키(Modbus addr / hc900_tag)로 변경 검토
  • display name은 read 시점에 map_master로 해석하는 아키텍처로 전환 검토

설계자: ____________________
작성일: //____
검토자: ____________________
검토일: //____


부록: 데이터 흐름 전체 도식

┌─────────────────────────────────────────────────────────────────────┐
│  HC Designer CSV Export (현장 dump)                                  │
│  SignalTags.csv: row[2] = "TI-6111B"  ← .PV 없음                    │
│  SummaryFucntionBlockReport.csv: row[2] = "FICQ-6101"               │
└──────────────────────────┬──────────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────────────┐
│  build_register_map.py                                               │
│                                                                      │
│  루프 (line 159):                                                    │
│    tag_name = f"{loop['tag']}.{name}"                                │
│    → "FICQ-6101.PV"  ✓ Experion ItemName과 일치                      │
│                                                                      │
│  신호태그 (line 171) ← 문제:                                          │
│    tag = sig["tag"]  ← CSV row[2] 그대로                             │
│    → "TI-6111B"  ✗ Experion의 "TI-6111B.PV"와 불일치                 │
│                                                                      │
│  변수 (line 183):                                                    │
│    tag = var["tag"]  ← CSV row[2] 그대로                             │
│    → "VAR-xxxx"  (Experion 매핑 확인 필요)                            │
└──────────────────────────┬──────────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────────────┐
│  register-map.json                                                   │
│  {"tag": "TI-6111B"}  ← .PV 없음                                     │
└──────────────────────────┬──────────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────────────┐
│  load_map_master.py (line 64)                                        │
│  tagname == hc900_tag == e["tag"]                                    │
│  → hc900_map_master: tagname="TI-6111B", hc900_tag="TI-6111B"       │
│  (register-map tag를 그대로 복사. register-map 수정하면 자동 따라옴)  │
└──────────────────────────┬──────────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────────────┐
│  C++ Gateway (gateway.cpp)                                           │
│  line 80:  e.tag = item["tag"]   ← register-map tag 그대로 저장      │
│  line 169: fresh[entry.tag] = cv     ← cache key로 사용              │
│  line 189: tv->set_tag_name(name)  ← gRPC 응답에 그대로 포함          │
│  (변형 없음, register-map tag가 100% tag name이 됨)                   │
└──────────────────────────┬──────────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────────────┐
│  C# Hc900RealtimeService                                             │
│  line 203: tagLookup.TryGetValue(tv.TagName, ...)  ← 매칭            │
│  line 227: INSERT INTO realtime_table (tagname, ...)                 │
│  → tagname = map_master의 tagname = register-map의 tag               │
│  (변형 없음)                                                          │
└──────────────────────────┬──────────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────────────┐
│  PostgreSQL hc900                                                    │
│                                                                      │
│  realtime_table: tagname="TI-6111B"  ← 신규 데이터                   │
│  history_table:  tagname="TI-6111B.PV" ← Experion dump 원본 (5,180만 행) │
│               :  tagname="TI-6111B"    ← 신규 데이터 (분절)            │
│                                                                      │
│  → 같은 물리 신호가 두 이름으로 분산                                   │
└─────────────────────────────────────────────────────────────────────┘

부록: Experion Point Name 규칙

태그 종류 HC Designer CSV tag Experion point name register-map tag 상태
루프 (PID) FICQ-6101 FICQ-6101.PV FICQ-6101.PV ✓ 일치
신호태그 TI-6111B TI-6111B.PV TI-6111B ✗ 불일치
신호태그 FI-6102 FI-6102.PV FI-6102 ✗ 불일치
변수 VAR-xxxx 확인 필요 VAR-xxxx ? 미확인

Experion 규칙: 모든 신호태그(Tag Type = TI/FI/LI/PI/DI/AI/AO 등)는 .PV suffix를 붙여 point name을 생성한다.