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

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 }
      }
    }
  ]
}

Hc900DbContextDbSet<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
}

백엔드 동작:

  1. 요청받은 column_key + from~to 기간 검증
  2. TagsFor(ToSuffix(col))로 태그 목록 획득
  3. 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
    
  4. 조회된 데이터를 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 계산
  5. TempRef 객체 구성 → JsonSerializer.Serializedata JSONB 컬럼에 저장
  6. 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 /profiles API 호출
  • 성공 시 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 Hc900DbContextDbSet + OnModelCreating 매핑 + DDL Hc900DbContext.cs 1h
3 SteamAdvisorControllerEnsureDefaultProfilesAsync + 기존 파일 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/TempProfileHistoryprofile_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행 조건과 동일)