온도프로파일/PV일관성/PointBuilder/history 작업지시, 신호태그·스팀유량 진단, 베이직아키텍처 재설계, MSDS, LLM채팅 구조 등. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
19 KiB
진단 체크리스트 — 온도 프로파일 메타 영역을 표 형식으로 재구성 + 유량 표시
규칙: diagnosis-checklist.md의 8단계를 준수. STEP 3(코드 읽기)과 STEP 6(교차 검증)을 반드시 거침.
0. 요구사항 요약
현재: 온도 프로파일 탭(st-temp)에서 차트 아래 st-temp-meta에 단별 온도/진공을 텍스트로 표시.
목표: st-temp-meta를 **두 개의 표(table)**로 재구성:
왼쪽 표 — 온도 (기존 텍스트 → 표 형식):
┌──────────────┬────────┬──────────────┐
│ 항목 │ 현재값 │ 기준 │
├──────────────┼────────┼──────────────┤
│ eb-A(보텀) │ 85.6℃ │ 85.1±2.3 │
│ T_B │ 84.0℃ │ 83.3±2.1 │
│ T_C(민감단) │ 77.2℃ │ 77.0±1.4 │
│ T_D(탑) │ 74.2℃ │ 74.2±1.6 │
│ ΔT(A-D) │ 11.4℃ │ — │
│ 진공 │ 39.7 │ 40.1±3.8 │
└──────────────┴────────┴──────────────┘
오른쪽 표 — 유량 (신규):
┌────────┬────────┬────────┬──────────┬──────────┬──────────┐
│ │ FEED │ REFLUX │ 제품추출 │ 경비물 │ 중비물 │
│ │FICQ-610│ FICQ-6 │ FICQ-611 │ FICQ-611 │ FICQ-611 │
│ 태그명 │ 1.PV │ 13.PV │ 8.PV │ 4.PV │ 6.PV │
├────────┼────────┼────────┼──────────┼──────────┼──────────┤
│ PV │ 896.4 │ 652.4 │ 816.4 │ 14.4 │ 23.4 │
│ SP │ 900 │ 656 │ 820 │ 18.0 │ 27.0 │
│ OP │ 53.5 │ 64.5 │ 43.9 │ 33.7 │ 20.8 │
└────────┴────────┴────────┴──────────┴──────────┴──────────┘
실시간/과거 스냅샷 모두 동일 형식.
STEP 1 — 맥락 파악
| 항목 | 내용 |
|---|---|
| 파일 | src/Hc900Crawler/wwwroot/js/steam.js (프론트엔드), SteamAdvisorController.cs (백엔드) |
| 역할 | Steam Advisory 대시보드 — 온도 프로파일 이격 모니터 |
| 아키텍처 | 프론트 → API → C# 백엔드가 realtime_table + tempref.json 처리 |
| 관련 DB | Experion realtime_table (v_tag_summary 기반) |
STEP 2 — 구조 탐색
src/Hc900Crawler/
├── wwwroot/
│ ├── js/steam.js ← stRenderTemp(), stTempScrub()
│ └── panes/steam.html ← st-temp-meta DOM
├── Controllers/
│ └── SteamAdvisorController.cs ← TempProfile, TempProfileHistory API
└── appsettings.json ← SteamAdvisor.Columns
STEP 3 — 코드 읽기 (핵심 부분만)
3.1 steam.js — stRenderTemp(snap) (line 216-286)
- line 279-285:
st-temp-metaDOM에 온도 텍스트 렌더링const lines = snapSrc.stages.map(s => `${(ST_STAGE_LABEL[s.stage] || s.stage).padEnd(12)} ${stFmt(s.current)}℃ 기준 ...`); if (snapSrc.spanAD != null) lines.push(`ΔT(A-D) ${stFmt(snapSrc.spanAD)}℃`); if (snapSrc.vacuum) lines.push(`진공 ${stFmt(snapSrc.vacuum.current)} ...`); meta.textContent = lines.join('\n'); - 현재
textContent사용 → 표 형식 변경 시innerHTML로 변경 필요
3.2 SteamAdvisorController.cs — TempProfile (line 183-206)
FetchRealtimeValues(col, tref)→ 5개 온도/진공 태그 조회ComputeStages(cur, tref)→ 제품 매칭 + z-score 계산- 반환:
stages,vacuum,spanAD
3.3 SteamAdvisorController.cs — TagsFor(p) (line 352-371)
- 온도/진공 태그 매핑만 포함. FICQ 태그 없음
3.4 SteamAdvisorController.cs — TempProfileHistory (line 211-273)
- history_table 조회 → 각 스냅샷에
stages,vacuum,spanAD포함
STEP 4 — 호출 계층 지도
[Browser] steam.js stTempLoad() / stTempScrub()
└─ GET /api/steam/tempprofile/{col} ← 실시간
└─ GET /api/steam/tempprofile/{col}/history ← 과거
└─ SteamAdvisorController.TempProfile() / TempProfileHistory()
├─ FetchRealtimeValues() → HC900 DB (온도 4 + 진공 1)
└─ ComputeStages() → stages + vacuum + spanAD
└─ _db.QueryHistoryWithIntervalAsync() → history_table
└─ ComputeStages() for each snapshot
[Browser] steam.js stRenderTemp(snap)
└─ stTempUpdateBadges()
└─ ECharts 렌더
└─ st-temp-meta.innerHTML = ... ← 표 형식 (변경 필요)
STEP 5 — 패턴 매칭 (체크리스트 순회)
🔴 런타임 즉시 실패
| 체크 | 항목 | 판단 기준 |
|---|---|---|
| [ ] | textContent → innerHTML 변경 |
XSS 위험 없음 (모든 값은 stFmt()로 포맷된 숫자) |
| [ ] | API 응답 필드 누락 | flow 필드가 백엔드/프론트에서 일관되게 사용되는가? |
| [ ] | 잘못된 타입 | flow 값이 null/NaN일 때 stFmt()가 올바르게 처리하는가? |
🟠 동시성 / 비동기
| 체크 | 항목 | 판단 기준 |
|---|---|---|
| [ ] | API 응답 구조 변경 | 기존 stages/vacuum/spanAD 필드 유지 (breaking change 아님) |
🟢 코드 구조
| 체크 | 항목 | 판단 기준 |
|---|---|---|
| [ ] | FICQ 태그명 하드코딩 | TagsFor()에서 동적 생성 (코드 중복 없음) |
| [ ] | 미사용 import·변수 | 신규 코드에 미사용 import 없음 |
STEP 6 — 교차 검증
| 질문 | 확인 방법 | 결과 |
|---|---|---|
| Q1. 이미 수정된 문제인가? | steam.js에 표 형식 로직이 이미 있는가? |
아니오 — 현재 텍스트 기반 |
| Q2. 다른 레이어에서 처리되고 있는가? | Live 패널에 표 형식이 있는가? | 아니오 — Live 패널은 별도 레이아웃 |
| Q3. 의도적 설계인가? | 표 형식 재구성이 요구사항에 명시된 기능인가? | 예 — 사용자가 명시적으로 요청 |
| Q4. 실제 장애 시나리오가 있는가? | 표 형식이 없으면 운전원 판단에 영향이 있는가? | 예 — 유량 데이터 없으면 물질수지 판단 불가 |
STEP 7 — 심각도 분류
| 등급 | 항목 |
|---|---|
| 🟡 LOW | 신규 UI 표시 기능 — 기존 동작에 영향 없음 |
| 🟡 LOW | 백엔드 API에 신규 필드 추가 — 기존 필드 유지 |
STEP 8 — 실행 플랜
작업 1 — 백엔드: TagsFor()에 FICQ 태그 추가
파일: SteamAdvisorController.cs:352-371
변경 내용: TagsFor(p)에 FICQ 5개 태그 매핑 추가
private static Dictionary<string, string> TagsFor(string p)
{
var m = new Dictionary<string, string>
{
["reb_temp"] = $"TICA-{p}A.PV",
["T_B"] = $"TI-{p}B.PV",
["T_C"] = $"TI-{p}C.PV",
["T_D"] = $"TI-{p}D.PV",
["vacuum"] = $"PICA-{p}.PV",
// 추가:
["feed"] = $"FICQ-{p}01.PV",
["reflux"] = $"FICQ-{p}13.PV",
["overhead"] = $"FICQ-{p}14.PV",
["bottom"] = $"FICQ-{p}16.PV",
["product"] = $"FICQ-{p}18.PV",
};
// ... 기존 switch (8111, 9111 등) 유지
return m;
}
교차 검증: v_tag_summary에서 ficq-6101, ficq-6113, ficq-6114, ficq-6116, ficq-6118이 존재하고 PV 값을 가짐 확인 완료.
작업 2 — 백엔드: FetchRealtimeValues() 변경 없음
TagsFor()가 모든 태그(온도 + 유량)를 반환하므로 FetchRealtimeValues()는 자동 반영. 변경 없음.
작업 3 — 백엔드: TempProfile API 응답에 flow + metadata 필드 추가
파일: SteamAdvisorController.cs:193-205
변경 내용: 반환 객체에 flow + flowTags 필드 추가
return Ok(new
{
column = tref.Column,
period = tref.Period,
matchedProduct = prod?.Label,
nProducts = tref.NProducts,
stages,
vacuum,
spanAD,
spanRef = prod?.SpanAD,
products = tref.Products,
timestamp = DateTime.UtcNow,
// 추가:
flow = new {
feed = cur.GetValueOrDefault("feed"),
reflux = cur.GetValueOrDefault("reflux"),
overhead = cur.GetValueOrDefault("overhead"),
bottom = cur.GetValueOrDefault("bottom"),
product = cur.GetValueOrDefault("product"),
},
flowTags = new {
feed = $"FICQ-{ToSuffix(col)}01.PV",
reflux = $"FICQ-{ToSuffix(col)}13.PV",
overhead = $"FICQ-{ToSuffix(col)}14.PV",
bottom = $"FICQ-{ToSuffix(col)}16.PV",
product = $"FICQ-{ToSuffix(col)}18.PV",
},
});
동일 변경: TempProfileHistory (line 247-254)에서도 각 스냅샷에 flow + flowTags 추가.
작업 4 — 백엔드: ComputeStages()에서 온도/진공 값 구조화
파일: SteamAdvisorController.cs:308-348
변경 내용: ComputeStages() 반환값에 온도 현재값/기준값을 표 형식으로 렌더링할 수 있도록 구조화
현재 stages는 { stage, current, refMedian, refStd, z, deviated } 객체 배열.
vacuum은 { current, refMedian, refStd, z, deviated } 객체.
이 구조를 그대로 사용하되, 프론트엔드에서 표 형식으로 렌더링할 때:
stages[].current→ "현재값" 열stages[].refMedian ± stages[].refStd→ "기준" 열vacuum.current→ "현재값" 열vacuum.refMedian ± vacuum.refStd→ "기준" 열
백엔드 변경 없음. 프론트엔드에서 기존 구조로 표 렌더링.
작업 5 — 프론트엔드: stRenderTemp()를 표 형식으로 재작성
파일: steam.js:216-286
변경 내용: textContent → innerHTML, 두 개의 <table> 생성
function stRenderTemp(snap) {
stTempUpdateBadges();
if (!stTempLive) {
const el = document.getElementById('st-chart-temp');
if (el) el.innerHTML = '<div style="padding:40px;text-align:center;color:#555">데이터 없음</div>';
return;
}
stRenderTempCol = document.getElementById('st-temp-col').value;
const stages = stTempLive.stages;
if (!stages || !stages.length) return;
// ... (ECharts 렌더링 기존 유지 — line 235-276) ...
// ── 메타 영역을 표 형식으로 렌더링 ──
const meta = document.getElementById('st-temp-meta');
meta.style.display = 'block';
const snapSrc = snap || stTempLive;
// 왼쪽 표: 온도
let tempRows = stages.map(s => {
const label = ST_STAGE_LABEL[s.stage] || s.stage;
const cur = stFmt(s.current) + '℃';
const ref = s.refMedian != null ? `${stFmt(s.refMedian)}±${stFmt(s.refStd)}` : '—';
return `<tr><td>${label}</td><td>${cur}</td><td>${ref}</td></tr>`;
});
// ΔT(A-D) 추가
if (snapSrc.spanAD != null) {
tempRows.push(`<tr><td>ΔT(A-D)</td><td>${stFmt(snapSrc.spanAD)}℃</td><td>—</td></tr>`);
}
// 진공 추가
if (snapSrc.vacuum) {
const v = snapSrc.vacuum;
const cur = stFmt(v.current);
const ref = v.refMedian != null ? `${stFmt(v.refMedian)}±${stFmt(v.refStd)}` : '—';
tempRows.push(`<tr><td>진공</td><td>${cur}</td><td>${ref}</td></tr>`);
}
// 오른쪽 표: 유량
let flowHeaders, flowRows;
if (snapSrc.flow) {
const f = snapSrc.flow;
const ft = snapSrc.flowTags || {};
flowHeaders = [
'<th></th>',
`<th>FEED<br><small>${esc(ft.feed || 'FICQ-??01.PV')}</small></th>`,
`<th>REFLUX<br><small>${esc(ft.reflux || 'FICQ-??13.PV')}</small></th>`,
`<th>제품추출<br><small>${esc(ft.product || 'FICQ-??18.PV')}</small></th>`,
`<th>경비물<br><small>${esc(ft.overhead || 'FICQ-??14.PV')}</small></th>`,
`<th>중비물<br><small>${esc(ft.bottom || 'FICQ-??16.PV')}</small></th>`,
].join('');
flowRows = [
`<tr><td>PV</td><td>${stFmt(f.feed?.pv)}</td><td>${stFmt(f.reflux?.pv)}</td><td>${stFmt(f.product?.pv)}</td><td>${stFmt(f.overhead?.pv)}</td><td>${stFmt(f.bottom?.pv)}</td></tr>`,
`<tr><td>SP</td><td>${stFmt(f.feed?.sp)}</td><td>${stFmt(f.reflux?.sp)}</td><td>${stFmt(f.product?.sp)}</td><td>${stFmt(f.overhead?.sp)}</td><td>${stFmt(f.bottom?.sp)}</td></tr>`,
`<tr><td>OP</td><td>${stFmt(f.feed?.op)}</td><td>${stFmt(f.reflux?.op)}</td><td>${stFmt(f.product?.op)}</td><td>${stFmt(f.overhead?.op)}</td><td>${stFmt(f.bottom?.op)}</td></tr>`,
].join('');
}
meta.innerHTML = `
<div class="st-meta-tables">
<table class="st-meta-temp">${tempRows.join('')}</table>
${flowHeaders ? `<table class="st-meta-flow"><thead><tr>${flowHeaders}</tr></thead><tbody>${flowRows}</tbody></table>` : ''}
</div>
`;
}
참고: SP/OP 값은 현재 API 응답에 없음. 작업 6에서 API 확장이 필요.
작업 6 — 백엔드: API 응답에 SP/OP 추가 (필수)
파일: SteamAdvisorController.cs
현재 상황: RealtimePoint 엔티티에는 LiveValue만 있음. SP/OP는 v_tag_summary 뷰에서 조회.
변경 내용: FICQ 태그의 PV/SP/OP를 v_tag_summary에서 조회
// v_tag_summary에서 base_tag 기반 조회 (예: ficq-6101, ficq-6113 등)
private async Task<Dictionary<string, (double? pv, double? sp, double? op)>> FetchFlowValues(string col)
{
var p = ToSuffix(col);
var baseTags = new[] {
("feed", $"ficq-{p}01"),
("reflux", $"ficq-{p}13"),
("overhead", $"ficq-{p}14"),
("bottom", $"ficq-{p}16"),
("product", $"ficq-{p}18"),
};
var placeholders = string.Join(",", baseTags.Select((_, i) => $"@p{i}"));
var sql = $"SELECT LOWER(base_tag) as base_tag, pv, sp, op FROM v_tag_summary WHERE LOWER(base_tag) IN ({placeholders})";
var params = baseTags.Select((bt, i) => new Npgsql.NpgsqlParameter($"p{i}", bt.Item2)).ToArray();
// Raw SQL 조회 결과 파싱
var rows = await _ctx.Database.SqlQueryRaw<(string base_tag, string? pv, string? sp, string? op)>(sql, params).ToListAsync();
var result = new Dictionary<string, (double? pv, double? sp, double? op)>();
foreach (var kv in baseTags)
{
var row = rows.FirstOrDefault(r => r.base_tag == kv.Item2);
result[kv.Item1] = (
pv: double.TryParse(row?.pv, out var v) ? (double?)v : null,
sp: double.TryParse(row?.sp, out var s) ? (double?)s : null,
op: double.TryParse(row?.op, out var o) ? (double?)o : null
);
}
return result;
}
API 응답 구조:
var flow = await FetchFlowValues(col);
return Ok(new {
// ... 기존 필드 ...
flow = new {
feed = flow["feed"],
reflux = flow["reflux"],
overhead = flow["overhead"],
bottom = flow["bottom"],
product = flow["product"],
},
flowTags = new {
feed = $"FICQ-{p}01.PV",
reflux = $"FICQ-{p}13.PV",
overhead = $"FICQ-{p}14.PV",
bottom = $"FICQ-{p}16.PV",
product = $"FICQ-{p}18.PV",
},
});
작업 7 — CSS 추가
파일: steam.html (style 블록) 또는 별도 CSS
.st-meta-tables {
display: flex;
gap: 16px;
margin-top: 8px;
font-size: 12px;
font-family: monospace;
}
.st-meta-temp, .st-meta-flow {
border-collapse: collapse;
}
.st-meta-temp td, .st-meta-temp th {
padding: 2px 8px;
border-bottom: 1px solid #1a2a3a;
text-align: left;
white-space: nowrap;
}
.st-meta-flow {
font-size: 11px;
}
.st-meta-flow th {
padding: 2px 6px;
border-bottom: 1px solid #2a3a4a;
color: #888;
font-weight: normal;
}
.st-meta-flow th small {
display: block;
font-size: 9px;
color: #555;
margin-top: 2px;
}
.st-meta-flow td {
padding: 2px 6px;
border-bottom: 1px solid #1a2a3a;
color: #ccc;
}
.st-meta-flow td:first-child {
color: #666;
font-weight: bold;
}
작업 8 — 테스트
| 테스트 | 방법 |
|---|---|
| 실시간 조회 | GET /api/steam/tempprofile/C-6111 → flow + flowTags 필드 확인 |
| 과거 조회 | GET /api/steam/tempprofile/C-6111/history?from=...&to=... → 각 스냅샷에 flow 포함 확인 |
| UI 렌더링 | 온도 프로파일 탭 → st-temp-meta에 두 표 표시 확인 |
| Scrubber | 슬라이더 이동 → 각 시점의 표 값 변경 확인 |
| 실시간 모드 | stTempLive.flow 표시 확인 |
| null 처리 | PV가 null인 태그 (예: C-9111) → — 표시 확인 |
| CSS 렌더링 | 두 표가 나란히 배치 확인 |
완료상황 체크
- 작업 1:
TagsFor()에 FICQ 태그 5개 추가 (feed/reflux/overhead/bottom/product) - 작업 2:
FetchFlowValues()신규 추가 —v_tag_summary에서 PV/SP/OP 조회 - 작업 3:
TempProfileAPI 응답에flow+flowTags필드 추가 - 작업 3-H:
TempProfileHistoryAPI 응답에 각 스냅샷flow+flowTags추가 - 작업 4:
ComputeStages()변경 없음 확인 - 작업 5:
stRenderTemp()재작성 —textContent→innerHTML, 두 개의 table 나란히 배치 - 작업 6: SP/OP 표시 — API + 프론트엔드 모두 구현 완료 (PV/SP/OP 3행)
- 작업 7: CSS 추가 (
.st-meta-tables,.st-meta-temp,.st-meta-flow) - 빌드:
dotnet build성공 (0 Error, 8 Warning — 기존 경고만) - 테스트 1: 실시간 API —
GET /api/steam/tempprofile/C-6111→flow+flowTags확인 - 테스트 2: 과거 API —
GET /api/steam/tempprofile/C-6111/history?from=...&to=...→ 스냅샷에flow포함 확인 - 테스트 3: UI 렌더링 — 온도 프로파일 탭 → 두 표 나란히 표시 확인
- 테스트 4: Scrubber — 슬라이더 이동 → 각 시점 표 값 변경 확인
- 테스트 5: null 처리 — PV가 null인 태그 (예: C-9111) →
—표시 확인 - 작업 8: 실시간 조회 API 테스트 —
flow+flowTags필드 정상 응답 - 작업 8: 과거 조회 API 테스트 — 스냅샷에
flow포함 - 작업 8: UI 렌더링 테스트 — 두 표 나란히 표시
- 작업 8: Scrubber 테스트 — 과거 시점 표 값 변경
- 작업 8: null/미폴링 태그 테스트 —
—표시 확인
이해 안 되는 부분 / 확인 사항
- SP/OP 표시: 현재 API 응답에 SP/OP가 없음.
v_tag_summary에는 존재하므로 API 확장이 필요. 표시할까요? (표에 PV만 표시하면 작업 6 스킵) - FICQ-{NN}14 = Overhead(경비물), FICQ-{NN}16 = Bottom(중비물) — 확인 완료