온도프로파일/PV일관성/PointBuilder/history 작업지시, 신호태그·스팀유량 진단, 베이직아키텍처 재설계, MSDS, LLM채팅 구조 등. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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_table → history_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 필드를 tagname과 hc900_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_table → history_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.cs — realtime_table → history_table 복사 시 tagname 변환 로직 없음. Hc900DbContext.cs SnapshotToHistoryAsync도 tagname 그대로 복사.
영향: 시계열 조회 시 두 이름 모두 검색 필요. 운영상 실수 유발.
수정: 단계별 진행 필요 (아래 STEP 9~11 참조).
STEP 9 — 수정 실행 계획 (완료 서명)
단계 1: build_register_map.py 수정
- 신호태그 처리부(line 171)에
.PVsuffix 추가 - 변수태그 처리부(line 183)도 Experion point name 규칙 적용 여부 확인
- 수정 후 register-map 재생성 테스트
수정자: ____________________
작성일: //____
검토자: ____________________
검토일: //____
단계 2: register-map 재생성 및 DB 반영
- 수정된
build_register_map.py로 register-map 재생성 load_map_master.py로hc900_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을 생성한다.