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

14 KiB
Raw Blame History

작업플랜 — 온도 프로파일 과거 이력 구현 (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-247realtime_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)

동작:

  1. from/to 기본값: from=now-1h, to=now
  2. TagsFor(ToSuffix(col))로 5개 태그명 획득
  3. IExperionDbService.QueryHistoryWithIntervalAsync() 호출 — interval="1 minute", tags=5개 태그
  4. 각 time_bucket 행마다 현재 TempProfile과 동일한 제품매칭+z-score 로직 적용
  5. 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

SteamAdvisorControllerIExperionDbService 주입 필요 (현재는 Hc900DbContext만 주입).

// 변경 없음 — IExperionDbService는 이미 Program.cs에 등록되어 있음

STEP 5 — 주의사항 (교차 검증)

Q 질문 확인
Q1 기존 Tempprofile 5초 폴링 유지? 유지. 히스토리 로드는 최초/범위변경시만 별도 호출 — 트래픽 증가 없음
Q2 제품매칭 로직 중복? 헬퍼로 추출하여 TempProfileTempProfileHistory가 공유
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 — 사전 등록 필요