온도프로파일/PV일관성/PointBuilder/history 작업지시, 신호태그·스팀유량 진단, 베이직아키텍처 재설계, MSDS, LLM채팅 구조 등. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
14 KiB
작업플랜 — 온도 프로파일 과거 이력 구현 (2026-06-07)
진단 결과 (2026-06-07 checklist 기반)
| # | 항목 | 심각도 | 결론 |
|---|---|---|---|
| 1 | IExperionDbService DI 등록 — 계획서 "변경 없음" 주석 오류 |
LOW | 생성자에 IExperionDbService db 추가 필요 |
| 2 | QueryHistoryWithIntervalAsync 시그니처 — 계획서 서술과 DTO 불일치 |
MED | HistoryIntervalQueryRequest DTO 객체 생성 필요. 서술만 명확히 하면 됨 |
| 3 | stRenderTemp() 시그니처 변경 — 기존 호환성 |
LOW | JS optional 인자 처리로 기존 동작 유지 |
| 4 | stTempLoad() 버튼 — 초기 히스토리 미로드 |
LOW | "조회" 버튼 클릭 시 stTempHistoryLoad()도 함께 호출 권장 |
| 5 | ECharts 64 series 5초 재렌더링 — GPU 메모리 누수 가능성 | LOW | history series는 한 번만 추가, 5초 폴링에서는 data만 업데이트하는 분리 필요 |
| 6 | 타임존 직렬화 — KST/UTC | LOW | toISOString() 자동 보정으로 실제 문제 없음 |
| 7 | 헬퍼 메서드 추출 — 계획서 서술 누락 | LOW | MatchProduct(), ComputeStages() 시그니처 명시 추가 필요 |
| 8 | time_bucket 비어있는 bucket — null 보간 필요 |
LOW | null 값 전후 값으로 보간하거나 차트에서 연결 |
HIGH 항목 없음. 계획서 전체적으로 구현 가능성 높음. 주요 수정사항 3건: #1(DI 생성자 변경), #4(조회 버튼 시 초기 히스토리 로드), #5(ECharts series 관리 분리).
STEP 1 — 맥락 파악
현재 동작: steam.html:62-73 "온도 프로파일" 탭 → steam.js:90-151 5초 주기 GET /api/steam/tempprofile/{col} → SteamAdvisorController.cs:180-247가 realtime_table에서 현재값 5종(reb_temp/T_B/T_C/T_D/vacuum)만 조회 → ECharts에 "현재" 단일 라인으로 표시. 기준밴드(±2σ)는 gen_temp_profiles.py가 산출한 {col}_tempref.json 정적 파일.
한계: 과거 시점의 프로파일을 볼 수 없음. 운전자는 "30분 전 프로파일 vs 지금" 비교 불가. 이격(drift)이 언제 시작되었는지 추적 불가.
사용 가능한 이력 인프라: history_table(TimeScaleDB hypertable, 60초 스냅샷), POST /api/history/query-interval (time_bucket 집계), IExperionDbService.QueryHistoryWithIntervalAsync
STEP 2 — 호출 계층 지도 (변경 대상)
steam.html (st-temp pane)
└─ steam.js stTempTick() ← 5s setInterval, GET /api/steam/tempprofile/{col}
└─ stRenderTemp(d) ← ECharts option.set ({기준밴드, 기준, 현재} 3 series)
SteamAdvisorController:
GET /api/steam/tempprofile/{col}
├─ read tempref.json ← 정적 기준밴드
├─ TagsFor(suffix) → 5개 태그명
├─ realtime_table 조회 (현재값)
└─ 제품매칭 + z-score → 단일 snapshot 반환
IExperionDbService (기존):
POST /api/history/query ← raw 조회
POST /api/history/query-interval ← time_bucket 집계
STEP 3 — 요구사항
| # | 요구사항 | 우선순위 |
|---|---|---|
| R1 | 과거 특정 시점의 온도 프로파일을 현재와 중첩 표시 | P0 |
| R2 | 시간 범위 선택 UI (30분/1시간/4시간/오늘/사용자지정) | P0 |
| R3 | 단계별 온도 추세를 미니 차트로 함께 표시 | P1 |
| R4 | z-score 추세 (어느 단계에서 이격이 시작되었는가) | P2 |
| R5 | 애니메이션 재생 (시간 경과에 따른 프로파일 변화) | P3 |
STEP 4 — 구현 구성
4-1. 백엔드: 신규 엔드포인트 GET /api/steam/tempprofile/{col}/history
파일: SteamAdvisorController.cs (lines 180-247 인접)
시그니처:
[HttpGet("tempprofile/{col}/history")]
public async Task<IActionResult> TempProfileHistory(
string col,
[FromQuery] DateTime? from = null,
[FromQuery] DateTime? to = null,
[FromQuery] int limit = 100)
동작:
from/to기본값: from=now-1h, to=nowTagsFor(ToSuffix(col))로 5개 태그명 획득IExperionDbService.QueryHistoryWithIntervalAsync()호출 — interval="1 minute", tags=5개 태그- 각 time_bucket 행마다 현재 TempProfile과 동일한 제품매칭+z-score 로직 적용
- profile 스냅샷 배열 반환
응답 구조:
{
"column": "C-6111",
"from": "...",
"to": "...",
"interval": "1 minute",
"n": 60,
"time_snapshots": [
{
"ts": "2026-06-07T10:00:00Z",
"matchedProduct": "P0",
"stages": [
{"stage": "reb_temp", "value": 128.5, "z": 0.3, "deviated": false},
...
],
"vacuum": {"value": 120, "z": -0.5, "deviated": false},
"spanAD": 45.2
}
]
}
변경 최소화: 제품매칭 로직은 기존 TempProfile과 공유. 헬퍼 메서드(MatchProduct, ComputeZ, ComputeStage)로 추출.
4-2. 프론트엔드: 시간 범위 선택 UI
파일: steam.html (st-temp pane, lines 62-73)
변경:
- "조회" 버튼 우측에 시간 범위 버튼 그룹 추가:
[30분] [1시간] [4시간] [오늘] - "과거 프로파일" legend 항목 추가 (캡션용)
<div class="st-bt-bar">
컬럼: <select id="st-temp-col">...</select>
<button class="btn-a" id="st-temp-load">조회</button>
<span class="st-temp-range-label">과거:</span>
<button class="st-temp-range active" data-range="30m">30분</button>
<button class="st-temp-range" data-range="1h">1시간</button>
<button class="st-temp-range" data-range="4h">4시간</button>
<button class="st-temp-range" data-range="today">오늘</button>
<span id="st-temp-status" style="margin-left:8px;font-size:11px;color:#888"></span>
</div>
4-3. 프론트엔드: stRenderTemp() 개선 + 과거 히스토리 렌더러
파일: steam.js (lines 77-151)
변경:
4-3a. stTempTick() — 5초 폴링 유지, 현재 스냅샷만 갱신
기존 5초 폴링 유지. 단, 히스토리 데이터는 분리 관리.
let stTempHistory = null; // { time_snapshots: [...] }
let stTempHistoryRange = '30m'; // 현재 선택된 범위
async function stTempTick() {
const col = document.getElementById('st-temp-col').value;
const st = document.getElementById('st-temp-status');
try {
const d = await api('GET', `/api/steam/tempprofile/${col}`);
stRenderTemp(d, stTempHistory);
st.textContent = '갱신: ' + new Date().toLocaleTimeString();
} catch (e) {
st.textContent = '오류: ' + e.message;
}
}
4-3b. stTempHistoryLoad(col, range) — 히스토리 로드 신규
범위 변경/초기 로드시만 호출. 응답 캐싱.
async function stTempHistoryLoad() {
const col = document.getElementById('st-temp-col').value;
const range = stTempHistoryRange;
const now = new Date();
let from;
if (range === '30m') from = new Date(now - 30*60000);
else if (range === '1h') from = new Date(now - 3600000);
else if (range === '4h') from = new Date(now - 4*3600000);
else if (range === 'today') {
from = new Date(now); from.setHours(0,0,0,0);
}
try {
const h = await api('GET',
`/api/steam/tempprofile/${col}/history?from=${from.toISOString()}&to=${now.toISOString()}&limit=500`);
stTempHistory = h;
// 현재 스냅샷과 함께 재렌더링
const d = await api('GET', `/api/steam/tempprofile/${col}`);
stRenderTemp(d, stTempHistory);
} catch (e) {
console.warn('[steam] history load fail:', e);
}
}
4-3c. stRenderTemp(d, history?) — ECharts series 확장
기존 3 series(기준밴드/기준/현재) 유지 + history.time_snapshots를 추가 series로:
- 방식 A (단순): N개 스냅샷의 각 stage 평균을 구해 "과거 평균" 하나의 라인 + 반투명 밴드
- 방식 B (상세): N개 스냅샷을 개별 반투명 라인으로 표시 (hover 시 툴팁)
- 방식 C (추천): "과거 평균" 라인 + "최소/최대 밴드" + "현재" 강조 라인
function stRenderTemp(d, history) {
const cats = d.stages.map(s => ST_STAGE_LABEL[s.stage] || s.stage);
const lo = d.stages.map(s => ...); // 기준 -2σ
const band = d.stages.map(s => ...); // 4σ
const med = d.stages.map(s => ...); // 기준 median
const cur = d.stages.map(s => ...); // 현재값 (굵은 파랑)
const series = [
{ name: '_lo', type: 'line', data: lo, ... },
{ name: '기준밴드', type: 'line', data: band, ... },
{ name: '기준', type: 'line', data: med, ... },
{ name: '현재', type: 'line', data: cur, ... },
];
// 과거 히스토리 overlay
if (history && history.time_snapshots?.length > 1) {
const snapshots = history.time_snapshots;
// 과거 스냅샷별 라인 (최대 60개 제한, 투명도 0.1)
const maxHistory = Math.min(snapshots.length, 60);
for (let i = 0; i < maxHistory; i++) {
const s = snapshots[i];
const snapData = cats.map((_, j) => s.stages[j]?.value ?? null);
series.push({
name: s.ts, type: 'line', data: snapData,
lineStyle: { color: '#446', width: 1, opacity: 0.15 },
symbol: 'none', silent: true,
});
}
// 과거 평균 라인 (선택)
const avgData = cats.map((_, j) => {
const vals = snapshots.map(s => s.stages[j]?.value).filter(v => v != null);
return vals.length ? vals.reduce((a,b) => a+b, 0) / vals.length : null;
});
series.push({
name: '과거평균', type: 'line', data: avgData,
lineStyle: { color: '#888', width: 1, type: 'dashed' }, symbol: 'none',
});
}
chart.setOption({ ...series, ... });
}
4-3d. (P1) 단계별 추세 미니차트 stRenderTempTrends(history)
별도 ECharts 인스턴스 4~5개. x축=시간, y축=온도, 수평 기준밴드±2σ, 현재값 점 강조.
4-4. DI 등록
파일: Program.cs
SteamAdvisorController에 IExperionDbService 주입 필요 (현재는 Hc900DbContext만 주입).
// 변경 없음 — IExperionDbService는 이미 Program.cs에 등록되어 있음
STEP 5 — 주의사항 (교차 검증)
| Q | 질문 | 확인 |
|---|---|---|
| Q1 | 기존 Tempprofile 5초 폴링 유지? | 유지. 히스토리 로드는 최초/범위변경시만 별도 호출 — 트래픽 증가 없음 |
| Q2 | 제품매칭 로직 중복? | 헬퍼로 추출하여 TempProfile과 TempProfileHistory가 공유 |
| Q3 | history_table 데이터 볼륨? | 60초×5태그×1h=300행, 4h=1200행 — 부하 무시 가능. interval은 1분 고정 |
| Q4 | OOD 기간 처리? | OOD 기간의 profile은 z-score가 커도 "deviated=true"로만 표시, 제외하지 않음 |
| Q5 | 컬럼명칭 통일 선행 필요? | 작업플랜-스팀컬럼명칭통일.md 완료 후 이 작업 시작. TagsFor("6111") 정상 동작 전제 |
| Q6 | ECharts series 과다? | 60개 history 라인 + 4개 기준 series = 64 series. ECharts는 수백 series까지 무리 없음. 단, tooltip trigger 최적화 필요(cursor로 변경) |
STEP 6 — 작업 순서
1. 컬럼명칭 통일 완료 (선행)
└─ 작업플랜-스팀컬럼명칭통일.md
2. SteamAdvisorController — 헬퍼 추출
└─ MatchProduct(), ComputeStages()를 TempProfile에서 분리
└─ IExperionDbService 생성자 주입
3. SteamAdvisorController — GET /api/steam/tempprofile/{col}/history 신규
└─ QueryHistoryWithIntervalAsync → profile snapshot 배열
└─ 제품매칭+z-score 루프
4. steam.js — 시간범위 선택 UI + stTempHistoryLoad()
└─ 범위 버튼, 이벤트 핸들러, API 호출, 캐싱
5. steam.js — stRenderTemp() 확장 (과거 overlay)
└─ history.time_snapshots → 반투명/ECharts series
6. steam.html — 범위 선택 마크업 추가
└─ st-temp-range 버튼 그룹, CSS 스타일
7. (P1) steam.js — 단계별 추세 미니차트 (stRenderTempTrends)
8. 검증
└─ dotnet build 성공
└─ /api/steam/tempprofile/C-6111/history?from=...&to=... 정상 응답
└─ UI 30분 클릭 → 과거 라인 overlay 확인
└─ 5초 폴링 유지 → 현재 라인만 갱신
STEP 7 — 검증 시나리오
| # | 시나리오 | 기대 결과 |
|---|---|---|
| 1 | GET /api/steam/tempprofile/C-6111/history?from=2026-06-07T09:00:00Z&to=2026-06-07T10:00:00Z |
60개 스냅샷 + 각 스냅샷의 stages/matchedProduct 반환 |
| 2 | "30분" 버튼 클릭 | 30개 스냅샷 로드 → 차트에 반투명 과거 라인들 |
| 3 | 현재 5초 폴링 유지 확인 | "현재" 라인만 갱신, 히스토리 라인 변화 없음 |
| 4 | 범위 변경 (30분→4시간) | 히스토리 재조회 → overlay 업데이트 |
| 5 | missing_tags 상태에서 히스토리 | 빈 배열 반환, 차트는 기존 3 series만 표시 |
STEP 8 — 파일 변경 요약
| 파일 | 변경 내용 |
|---|---|
SteamAdvisorController.cs |
+DI IExperionDbService, +헬퍼 추출, +TempProfileHistory endpoint |
steam.html |
+st-temp-range 버튼 그룹, CSS |
steam.js |
+stTempHistory, +stTempHistoryRange, +stTempHistoryLoad(), stRenderTemp() 확장, (P1)stRenderTempTrends() |
STEP 9 — 리스크
| 항목 | 수준 | 대응 |
|---|---|---|
| history_table에 60초 간격보다 더 상세한 데이터 없음 | 낮 | 1분 간격이면 충분. 추세/이격 감지에 무리 없음 |
| ECharts series 60+개 렌더링 성능 | 낮 | canvas 기반, opacity 0.15로 가벼움. 필요시 sampling: 'lttb' |
| 제품매칭이 history snapshot마다 달라짐 | 중 | 동일 snapshot 내에선 일관됨. UI에 matchedProduct 변화 표시 |
| TagsFor 태그가 register-map에 없는 컬럼 (9·10차) | 중 | 컬럼명칭 통일 후에도 해당 태그가 없으면 missing_tags — 사전 등록 필요 |