온도프로파일/PV일관성/PointBuilder/history 작업지시, 신호태그·스팀유량 진단, 베이직아키텍처 재설계, MSDS, LLM채팅 구조 등. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
16 KiB
16 KiB
온도 프로파일 기준 프로파일 선택/생성 기능 — 상세 설계
Problem
현재 기준 프로파일({col}_tempref.json)은 2026-02-05~2026-06-05 고정.
제품 구성·원료·계절 변화에 기준이 부정확해져 이격 감지 신뢰도 하락.
운전자가 특정 기간 기준을 여러 개 만들어 두고 전환 가능해야 함.
Solution: DB 저장 (Phase B, 권장)
Architecture
[Python: gen/profiles] ──HTTP──▶ [POST /api/steam/tempprofile/{col}/profiles]
│
[temp_ref_profiles table]
│
[Browser] ──GET /api/steam/tempprofile/{col}?profile_id=N──▶ [LoadTempRef(id)] ──▶ [ComputeStages]
파일 I/O 없음, 여러 서버에서 동기화 문제 없음, 관리 API로 확장 용이.
1. DB 테이블
CREATE TABLE hc900.temp_ref_profiles (
id SERIAL PRIMARY KEY,
column_key TEXT NOT NULL, -- "C-6111"
label TEXT NOT NULL, -- "기본", "recent-30d", "2026-05-w1"
description TEXT NOT NULL DEFAULT '', -- "최근 30일(2026-05-08~2026-06-07)"
period_from TIMESTAMPTZ NOT NULL,
period_to TIMESTAMPTZ NOT NULL,
data JSONB NOT NULL, -- Tempref 전체 {stages_order, n_products, products: [...]}
is_default BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 컬럼별 조회 + 기본값 정렬
CREATE INDEX idx_trp_column ON hc900.temp_ref_profiles(column_key);
CREATE UNIQUE INDEX idx_trp_column_default ON hc900.temp_ref_profiles(column_key) WHERE is_default = TRUE;
data 컬럼 JSONB 구조 (기존 {col}_tempref.json과 동일):
{
"stages_order": ["reb_temp", "T_B", "T_C", "T_D"],
"n_products": 3,
"products": [
{
"label": "P0",
"n_rows": 1240,
"span_AD": 42.5,
"vacuum": { "median": 48.2, "std": 1.5 },
"stages": {
"reb_temp": { "median": 176.2, "std": 2.1 },
"T_B": { "median": 112.5, "std": 3.2 },
"T_C": { "median": 88.7, "std": 4.5 },
"T_D": { "median": 66.2, "std": 2.8 }
}
}
]
}
Hc900DbContext에 DbSet<TempRefProfileEntity> 추가.
| 항목 | 값 |
|---|---|
| Entity 클래스 | TempRefProfileEntity (내부 클래스 또는 별도 파일) |
| 테이블명 | temp_ref_profiles |
| 스키마 | hc900 |
| EF Core | modelBuilder.Entity<TempRefProfileEntity>(e => { e.ToTable("temp_ref_profiles"); ... }) |
2. Entity 클래스
// Infrastructure/Database/ 경로
public sealed class TempRefProfileEntity
{
public int Id { get; set; }
public string ColumnKey { get; set; } = "";
public string Label { get; set; } = "";
public string Description { get; set; } = "";
public DateTime PeriodFrom { get; set; }
public DateTime PeriodTo { get; set; }
public string Data { get; set; } = ""; // JSON serialized TempRef
public bool IsDefault { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}
3. EF Core 매핑 (Hc900DbContext)
public DbSet<TempRefProfileEntity> TempRefProfiles => Set<TempRefProfileEntity>();
// OnModelCreating:
modelBuilder.Entity<TempRefProfileEntity>(e =>
{
e.ToTable("temp_ref_profiles");
e.HasKey(x => x.Id);
e.Property(x => x.ColumnKey).HasColumnName("column_key").IsRequired();
e.Property(x => x.Label).IsRequired();
e.Property(x => x.Description);
e.Property(x => x.PeriodFrom).HasColumnName("period_from");
e.Property(x => x.PeriodTo).HasColumnName("period_to");
e.Property(x => x.Data).HasColumnType("jsonb");
e.Property(x => x.IsDefault).HasColumnName("is_default");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
e.Property(x => x.UpdatedAt).HasColumnName("updated_at");
e.HasIndex(x => x.ColumnKey);
});
InitializeAsync()에 CREATE TABLE IF NOT EXISTS temp_ref_profiles (...) 추가.
4. 백엔드 API — 상세
4.1 프로파일 목록 조회
GET /api/steam/tempprofile/{col}/profiles
Response:
{
"success": true,
"column": "C-6111",
"profiles": [
{ "id": 1, "label": "기본", "description": "2026-02-05~2026-06-05", "isDefault": true, "nProducts": 3, "createdAt": "..." },
{ "id": 2, "label": "recent-30d","description": "최근 30일(2026-05-08~2026-06-07)", "isDefault": false, "nProducts": 2, "createdAt": "..." },
{ "id": 3, "label": "winter", "description": "2025-12-01~2026-02-28", "isDefault": false, "nProducts": 3, "createdAt": "..." }
]
}
column_key로 필터링is_default우선, 그 다음created_at DESC정렬- 기존 파일 기반
{col}_tempref.json이 DB에 없으면 최초 조회 시 자동 import
4.2 프로파일 생성
POST /api/steam/tempprofile/{col}/profiles
Content-Type: application/json
{
"label": "recent-30d",
"description": "최근 30일",
"from": "2026-05-08T00:00:00+09:00",
"to": "2026-06-07T23:59:59+09:00",
"setDefault": false
}
백엔드 동작:
- 요청받은
column_key+from~to기간 검증 TagsFor(ToSuffix(col))로 태그 목록 획득history_table에서 해당 기간 데이터 조회SELECT recorded_at, tagname, value FROM hc900.history_table WHERE tagname = ANY(...) AND recorded_at BETWEEN @from AND @to ORDER BY recorded_at- 조회된 데이터를
gen_temp_profiles.py와 동일한 로직으로 처리:- 각 스냅샷 시간별로 (reb_temp, T_B, T_C, T_D, vacuum) 한 행으로 피벗
mode == "PROD"필터 (realtime_table에서 해당 기간 mode 태그 조회)- feed > 50 필터
- NaN/null 제거
- KMeans 클러스터링 (k=3→2→1)
- 각 클러스터별 median/std 계산
TempRef객체 구성 →JsonSerializer.Serialize→dataJSONB 컬럼에 저장setDefault=true면 기존 기본값 해제 후 이 프로파일을 기본으로 설정
Response:
{
"success": true,
"profileId": 4,
"label": "recent-30d",
"nProducts": 2,
"period": "2026-05-08~2026-06-07",
"message": "기준 프로파일 생성 완료"
}
4.3 프로파일 기본값 설정
PUT /api/steam/tempprofile/{col}/profiles/{id}/default
- 해당
column_key의 다른 모든 프로파일is_default = false - 지정한
id의 프로파일is_default = true
4.4 프로파일 삭제
DELETE /api/steam/tempprofile/{col}/profiles/{id}
- 기본 프로파일(
is_default=true)은 삭제 불가 (먼저 다른 프로파일을 기본으로 설정해야 함)
4.5 프로파일 미리보기 (생성 전 검증)
POST /api/steam/tempprofile/{col}/profiles/preview
body: { "from": "...", "to": "..." }
→ { "success": true, "nRows": 3420, "nSnapshots": 57, "estimatedProducts": 3,
"stagesOrder": ["reb_temp","T_B","T_C","T_D"] }
5. 기존 TempProfile / TempProfileHistory 수정
LoadTempRef 변경
// Before: 파일 기반
private async Task<TempRef?> LoadTempRef(string col)
// After: DB 기반
private async Task<TempRef?> LoadTempRef(string col, int? profileId = null)
{
TempRefProfileEntity? entity;
if (profileId.HasValue)
{
entity = await _ctx.TempRefProfiles
.FirstOrDefaultAsync(p => p.ColumnKey == col && p.Id == profileId.Value);
}
else
{
entity = await _ctx.TempRefProfiles
.Where(p => p.ColumnKey == col && p.IsDefault)
.OrderByDescending(p => p.CreatedAt)
.FirstOrDefaultAsync();
}
if (entity == null) return null;
return JsonSerializer.Deserialize<TempRef>(
entity.Data,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
}
TempProfile 엔드포인트
GET /api/steam/tempprofile/{col}?profile_id=2
profile_id생략 시 기본 프로파일 사용- 응답에 현재 사용 중인 프로파일 정보 추가:
{ "column": "C-6111", "profile": { "id": 2, "label": "recent-30d", "description": "..." }, "period": "2026-05-08~2026-06-07", "matchedProduct": "P1", ... }
TempProfileHistory 엔드포인트
GET /api/steam/tempprofile/{col}/history?from=...&to=...&profile_id=2
- 동일한
profile_id파라미터 지원 - 히스토리 각 스냅샷도 동일한 기준 프로파일로 z-score 계산
6. Python 스크립트 — DB 직접 저장
gen_temp_profiles.py 확장
usage: gen_temp_profiles.py --loop-csv CSV --signal-csv CSV --variable-csv CSV
[--from DATE] [--to DATE] [--days N]
[--label LABEL] [--description DESC]
[--db-conn "Host=...;Database=...;Username=...;Password=..."]
[--o FILE] # 파일 출력 (기존 동작)
[--api-url http://...] # API 호출로 저장
--db-conn: 직접 DB 연결하여temp_ref_profiles에 INSERT--api-url: API 호출하여 저장 (권장, 서버 로직 재사용)--label필수 (DB 저장 시)--description: 사람이 읽을 수 있는 설명
예시:
# API로 생성
python3 gen_temp_profiles.py --loop-csv ... \
--from 2026-05-01 --to 2026-06-07 \
--label "recent-30d" --description "최근 30일 기준" \
--api-url "http://localhost:5000/api/steam/tempprofile/C-6111/profiles"
# DB 직접 입력
python3 gen_temp_profiles.py --loop-csv ... \
--days 30 --label "rolling-30d" \
--db-conn "Host=localhost;Database=iiot_platform;Username=postgres"
load_state_labels.py와 유사한 전용 import 스크립트
별도 스크립트 scripts/analysis/import_tempref_to_db.py:
- 기존
scripts/analysis/*_tempref.json파일을 DB에 일괄 등록 --col C-6111 --profile-id 1옵션으로 특정 프로파일만 업데이트 가능--set-default옵션으로 기본값 지정
7. 프론트엔드 — 상세 UI/UX
7.1 기준 프로파일 선택기
컬럼: [C-6111 ▼] 기준: [recent-30d ▼] [+ 새 기준] [조회]
├── 기본 (2026-02-05~2026-06-05)
├── recent-30d (최근 30일) ← 현재 선택
└── winter (2025-12~2026-02)
<select id="st-temp-profile">: 프로파일 목록- 첫 로딩 시
GET /profiles로 목록 조회하여 dropdown 채움 - 선택 변경 시 → 자동으로
stTempLoad()재실행 (프로파일 파라미터 포함) - 차트 제목에 현재 프로파일 표시:
"C-6111 · 기준: recent-30d"
7.2 새 기준 생성 (모달)
[+ 새 기준] 버튼 클릭 → 모달 표시:
┌─ 새 기준 프로파일 생성 ──────────────────────┐
│ │
│ 레이블: [recent-30d ] │
│ 설명: [최근 30일 기준 ] │
│ │
│ ○ 최근 N일: [30]일 (현재 ~ N일 전) │
│ ● 기간 지정: │
│ 시작: [2026-05-08] 종료: [2026-06-07] │
│ │
│ [□ 이 프로파일을 기본으로 설정] │
│ │
│ [취소] [생성] │
└───────────────────────────────────────────────┘
- "생성" 클릭 →
POST /profilesAPI 호출 - 성공 시 dropdown 갱신, 새 프로파일 선택됨
- 실패 시 오류 메시지 표시
7.3 기준 프로파일 관리
[⚙] 버튼 (또는 컨텍스트 메뉴):
┌─ 기준 프로파일 관리 ─────────────────────┐
│ │
│ ◎ 기본 (기본) [기본설정] [삭제] │
│ ○ recent-30d [기본설정] [삭제] │
│ ○ winter [기본설정] [삭제] │
│ │
│ [+ 새 기준 생성] │
└──────────────────────────────────────────┘
8. 마이그레이션: 기존 파일 → DB
최초 1회 자동 import (서버 시작 or 최초 API 호출 시)
Hc900DbContext.InitializeAsync() 또는 SteamAdvisorController 생성자에서:
private async Task EnsureDefaultProfilesAsync()
{
var dir = _config.GetValue<string>("SteamAdvisor:ModelDir")
?? "/home/windpacer/projects/hc900_ax/scripts/analysis";
foreach (var col in SUPPORTED_COLUMNS) // ["C-6111", "C-6211", ...]
{
var hasAny = await _ctx.TempRefProfiles.AnyAsync(p => p.ColumnKey == col);
if (hasAny) continue;
var path = Path.Combine(dir, $"{col}_tempref.json");
if (!System.IO.File.Exists(path)) continue;
var json = await System.IO.File.ReadAllTextAsync(path);
var tref = JsonSerializer.Deserialize<TempRef>(json,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (tref == null) continue;
_ctx.TempRefProfiles.Add(new TempRefProfileEntity
{
ColumnKey = col,
Label = "기본",
Description = tref.Period,
PeriodFrom = ParsePeriodFrom(tref.Period), // "2026-02-05~2026-06-05" → DateTime
PeriodTo = ParsePeriodTo(tref.Period),
Data = json,
IsDefault = true,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
});
await _ctx.SaveChangesAsync();
}
}
9. API 라우트 요약
| Method | Path | 설명 |
|---|---|---|
GET |
/api/steam/tempprofile/{col} |
실시간 프로파일 (profile_id query param) |
GET |
/api/steam/tempprofile/{col}/history |
과거 이력 (profile_id query param) |
GET |
/api/steam/tempprofile/{col}/profiles |
프로파일 목록 |
POST |
/api/steam/tempprofile/{col}/profiles |
새 프로파일 생성 |
POST |
/api/steam/tempprofile/{col}/profiles/preview |
생성 미리보기 |
PUT |
/api/steam/tempprofile/{col}/profiles/{id}/default |
기본값 설정 |
DELETE |
/api/steam/tempprofile/{col}/profiles/{id} |
프로파일 삭제 |
10. 구현 순서 (예상: 2~3일)
| 순서 | 작업 | 파일 | 예상시간 |
|---|---|---|---|
| 1 | TempRefProfileEntity 클래스 작성 |
Infrastructure/Database/ |
0.5h |
| 2 | Hc900DbContext에 DbSet + OnModelCreating 매핑 + DDL |
Hc900DbContext.cs |
1h |
| 3 | SteamAdvisorController에 EnsureDefaultProfilesAsync + 기존 파일 import |
SteamAdvisorController.cs |
1h |
| 4 | LoadTempRef DB 버전으로 변경 |
SteamAdvisorController.cs |
0.5h |
| 5 | GET profiles 목록 API |
SteamAdvisorController.cs |
0.5h |
| 6 | POST profiles 생성 API (history_table 조회 → KMeans → JSONB 저장) |
SteamAdvisorController.cs |
3h |
| 7 | PUT/DELETE profiles 관리 API |
SteamAdvisorController.cs |
0.5h |
| 8 | TempProfile/TempProfileHistory에 profile_id 파라미터 추가 |
SteamAdvisorController.cs |
0.5h |
| 9 | gen_temp_profiles.py DB/API 출력 옵션 |
gen_temp_profiles.py |
1h |
| 10 | steam.html 프로파일 선택 dropdown + 생성 모달 |
steam.html |
1h |
| 11 | steam.js 프로파일 로드/전환/생성 로직 |
steam.js |
2h |
| 12 | 빌드 + 통합 테스트 | — | 1h |
| 합계 | ~12h |
11. 에지 케이스
| 상황 | 처리 |
|---|---|
| 프로파일이 하나도 없음 | EnsureDefaultProfilesAsync가 파일 시스템에서 자동 import 시도, 실패시 404 |
| 기본 프로파일 삭제 요청 | 400 Bad Request — 먼저 다른 프로파일을 기본으로 설정해야 함 |
| 중복 레이블 | column_key + label에 unique 제약 or 409 Conflict |
| 생성 중 같은 기간 프로파일 존재 | 허용 (같은 기간으로 여러 번 생성 가능, 레이블로 구분) |
| DB 연결 실패 | 파일 기반 LoadTempRef로 fallback? or 명확한 503 에러 |
| 프로파일이 너무 많음 (50개+) | 기본 50개 제한, 생성 시 경고 |
| history_table 데이터 부족 (200행 미만) | 생성 API가 400 Bad Request + "데이터 부족" 메시지 (gen_temp_profiles.py의 최소 200행 조건과 동일) |