Files
HC900-Crawler/docs/작업지시서-측류SP쓰기-피드램프실행.md
windpacer 7b21c35af6 feat: 민감단온도 전환복귀제어 + SteamAdvisor + FeedRamp 전면 구현
=== 민감단온도(T_C) 전환복귀제어 (작업플랜 구현) ===
- FeedforwardModels: TempLowLimit, TcReturnRebTarget/Band, TcReturnDeltaAdRef/Band 추가
- FeedforwardEngine: sigTLow (T_C 하한 트리거, -1e9=비활성) + 온도기반 복귀게이트(tcRecovered)
  -> Recovering→Returning 전이: mbRecovered(물질수지) OR tcRecovered(reb-A+ΔT+T_C)
- FeedRampCalculator: 하강 램프 전면 구현 (RateUpPerMin/RateDnPerMin 분리, θ_up/θ_dn 분기, floor clamp)
- FeedRampExecutorService: 하강 램프 step 방향 지원
- FeedforwardConfigStore: 신규 6개 컬럼 SELECT/INSERT/UPDATE
- Hc900DbContext: temp_low_limit, tc_return_reb_target/band, tc_return_delta_ad_ref/band
- FeedforwardController: API 노출 + feed-ramp start/cancel/status

=== SteamAdvisor ===
- SteamAdvisorController: steam map 로드/시각화/제품매칭/온도프로파일
- steam.js, steam.html: SteamAdvisor 전용 UI 패널

=== Feed Ramp 실행 ===
- FeedRampExecutorService: BG service (BackgroundService)
- FeedRampJobStore: in-memory job store
- FfTrackingStore: ramp tracking DB
- FeedforwardSupervisor/WriteGuard: SP 쓰기 advisory + rate-limit

=== 분석 스크립트 ===
- gen_temp_profiles.py: 컬럼 온도 프로파일 기준 산출 → c{prefix}_tempref.json
- export_plotdata.py: analysis 결과 plot data export
- gen_instrument_ranges.py: 계기 범위 생성
- c6111_extract.py: C-6111 추출/운전모드 분류
- run_column.py: 전체 분석 파이프라인

=== Web UI ===
- ff.js/ff.html/ff.css: 전환류 상태기계 UI, TagBrowser, config save
- fast.js: Fast 조작 패널
- trend.js, pb.js, llmchat.js: 각 패널 확장
2026-06-06 18:33:56 +09:00

25 KiB
Raw Blame History

작업지시서 — 측류SP쓰기-피드램프실행 (진단 재검토 v3)

대상: 구현 LLM / 개발자 작성 기준 코드: src/Hc900Crawler, src/Infrastructure/Control, src/Core/Application/Feedforward 전제: 이 저장소(hc900_ax)의 FF(Feedforward) 모듈 구조를 그대로 따른다. 진단일: 2026-06-04 → v3 재검토: 진단 10건을 코드와 대조해 재분류(아래 "재검토 결과")


진단 재검토 결과

원 진단(v2)은 코드와 대조 시 실재 결함 1건 + 위험한 오처방 1건 + 사실오류/비이슈 2건 + "신규 구현을 결함으로 오분류" 6건으로 정리된다. 아래 표가 정정본이며, 이후 본문은 이 분류를 따른다.

(1) 실재 결함 — 반드시 수정

# 항목 정정 심각도 처리
D-3 WriteSp L102 "HC1" 하드코딩. 실 컨트롤러 ID는 C1~C4이므로 "HC1"은 어느 설정에도 매칭 안 됨 → GetClient"컨트롤러 HC1 설정 없음" 예외 → 현재 측류 쓰기는 사실상 전부 실패 중일 개연성. "멀티컨트롤러 미지원"이 아니라 "단일 쓰기조차 깨짐" 🔴 HIGH §5 A-1

(2) 증상은 맞으나 처방이 위험 — 처방 교체

# 항목 판정 처리
D-1 WriteGuard.CheckAdvisoryOnly면 버튼 쓰기도 차단 — 증상은 사실. 그러나 v2 처방("WriteGuard 우회, SpMin/Max·Commanded·SpNodeId만 검증")은 채택 불가: Valid(stale/무효)·Grade==C(저신뢰)·Transient(과도상태) 가드를 통째로 제거 → 라이브 컬럼 수동쓰기에서 가장 중요한 안전검증을 버리는 HIGH급 회귀. 진짜 원인은 auto-write/manual-write가 AdvisoryOnly 한 플래그에 결합된 것 🔴 HIGH(오처방) §3·§5 A-1 = 가드 분리(manualOverride)

(3) 사실오류·비이슈 — 철회

# 항목 판정
D-2 "warning 4줄 하드코딩" → 사실오류. 무조건 추가는 2줄(L49 steam ceiling, L73 energyLoop)뿐이고 나머지는 조건부. 게다가 이는 버그가 아니라 미구현 제약을 표시하는 의도적 TODO 마커. → 진단 철회(원하면 선택적 정리만, §6 부기)
D-10 "ex.Message 감사로그 → 비밀번호/토큰 누출" → 위협 부재. 이 경로 예외는 Modbus/gRPC 전송오류(host:port)일 뿐, 토큰은 쓰기 호출 전 검증되고 gRPC로 전달되지 않음. 메시지를 가리면 제어쓰기 실패 원인 추적성만 손상. → 진단 철회

(4) "신규 구현(스펙)"을 결함으로 오분류 — 진단 아님, 그냥 작업 항목

아래는 이 작업지시서가 "만들라고 지시한 것 그 자체"다. 아직 짓지 않은 집의 벽이 없다고 HIGH를 매긴 격 → 진단표에서 제외, 해당 작업 절로 이동.

# 항목 실제 위치
D-4 AutoWriteAsync L141 "HC1" — 코드사실은 맞으나 이 작업은 AdvisoryOnly=true 유지로 AutoWrite 미실행(死경로). HIGH 아님, 일관성 후속정리 §6 부기
D-5 FeedSpNodeId 등 DB 컬럼 없음 = 신규 필드 추가 지시(§6 B-1)
D-6 MapConfig 신규 필드 노출 = D-5의 귀결(§6 B-1)
D-7 ffCard 쓰기 버튼 없음 = 작업 A 그 자체(§5 A-2)
D-8 배지 "읽기 전용" 변경 = 작업 A 일부(§5 A-2)
D-9 램프 실행기/저장소/엔드포인트 없음 = 작업 B 전체(§6)

0. 목표 (한 줄)

  1. 측류추출 권장 유량(Advisory) 화면에서 버튼 클릭 시에만 해당 스트림의 권장 SP를 실제 HC900 컨트롤러 SP로 쓴다. (자동 쓰기 아님 — 운전원 명시적 인가)
  2. FEED 목표 변경 시 단번에 점프시키지 않고, 램프 계산기 결과(rate·ceiling)에 따라 FEED SP를 천천히 단계적으로 올린다. 버튼으로 시작/취소.

핵심 원칙: 두 기능 모두 "운전원이 버튼을 눌러야만" 실제 쓰기가 시작된다. 백그라운드 자동쓰기(AdvisoryOnly=false 경로)는 이 작업에서 사용하지 않으며 기본값 그대로 둔다.


1. 현재 코드 자산 (재사용 — 새로 만들지 말 것)

1-A. 측류 SP 수동 쓰기 — 백엔드 이미 존재

  • src/Hc900Crawler/Controllers/FeedforwardController.cs
    • POST api/ff/write/{columnId}/{streamKey}WriteSp(...) (L78~114)
    • 동작: X-Kb-Token 인증 → advisory/config/stream 조회 → SpNodeId 필수 검사 → body.value ?? adv.RecommendedSp_writeGuard.Check(...)_writeClient.WriteTagAsync("HC1", sc.SpNodeId, spVal) → 감사로그.
    • 요청 바디: WriteSpBody { double? value } (L310). value 비우면 권장값 사용.
  • src/Infrastructure/Control/FeedforwardWriteGuard.csCheck():
    • Commanded 스트림만 / AdvisoryOnly면 차단 / 권장SP 존재 / Valid / Grade≠C / Transient 아님 / SpMin~SpMax 범위 / NaN·Inf 아님.
  • src/Infrastructure/Hc900/Hc900WriteService.csWriteTagAsync(controllerId, tagName, value) → gRPC WriteTag.
  • 감사: IFeedforwardAuditService.LogAsync(FfActionLogEntry(...)), 조회 GET api/ff/audit.

따라서 측류 SP 쓰기는 "UI 버튼 + 소수 백엔드 보정"만 하면 된다. 새 엔드포인트 불필요.

1-B. UI (현재 읽기 전용)

  • src/Hc900Crawler/wwwroot/panes/ff.html<h2>측류추출 권장 유량 설정값 (Advisory · 보조지표)</h2>, 대시보드 컨테이너 #ff-dash.
  • src/Hc900Crawler/wwwroot/js/ff.js
    • ffLoadDash() (L145) → GET /api/ff/advisoryffCard(c) 렌더.
    • ffCard() (L165) 스트림 행 렌더 (L166~182): 권장 SP 칸은 s.recommendedSp 표시만, 버튼 없음.
    • 인증 헬퍼: ffApi(method, path, body) (L9) — X-Kb-Token 자동 첨부, 401 처리. 읽기는 api(...) 사용.
    • 폴링: ffInit()에서 setInterval(ffLoadDash, 3000).
  • 람프 계산기 UI: #ff-ramp 패널 + ffRampCompute() (L42) → GET /api/ff/ramp-advisor. 계산 표시만, 실행 없음.

1-C. 피드 램프 — 계산기만 존재, 실행기 없음

  • src/Infrastructure/Control/FeedRampCalculator.csCompute(...) 순수함수. 산출: CurrentFeed, ClampedTarget, Ceiling(value,binding), RampRate(value=kg/hr·min, binding), RampTimeMin, Steam, Hold, Warnings.
  • src/Infrastructure/Control/FeedRampAdvisorService.cs — 라이브값 수집 후 Compute 호출 (읽기 전용).
  • src/Hc900Crawler/Controllers/FeedforwardController.cs GET api/ff/ramp-advisor (L154).
  • DTO: src/Core/Application/Feedforward/FeedRampModels.cs (FeedRampAdvisory).
  • 없는 것: ① Feed SP를 쓸 태그(ColumnConfig에 FeedTag=PV만 있고 SP 없음) ② 시간에 따라 SP를 단계적으로 써 나가는 상태머신/백그라운드 실행기 ③ 시작/취소 버튼·상태 표시.

1-D. 설정 모델

  • src/Core/Application/Feedforward/FeedforwardModels.cs
    • ColumnConfig: FeedTag(PV), Streams[], ScanSec, StaleSec, Enabled, AdvisoryOnly(기본 true) 등. Feed SP 노드/태그 없음.
    • StreamConfig: SpNodeId(쓰기 대상 태그명), SpMin/SpMax, RateUpPerMin/RateDnPerMin, TargetCoeff, Role.
  • 저장: IFeedforwardConfigStore.SaveColumnAsync/LoadAllAsync (FeedforwardConfigStore.cs).

1-E. 컨트롤러 라우팅(중요)

  • 현재 쓰기는 "HC1" 하드코딩 (FeedforwardController.cs L102, FeedforwardSupervisor.cs L141).
  • 멀티컨트롤러 구조: 컨트롤러당 게이트웨이 1개(C1~C4), ControllerGrpcClientPool.GetClient(controllerId)로 라우팅. → 컬럼이 어느 컨트롤러에 속하는지 결정해야 한다 (§4 확인필요 참고).

2. 범위

# 작업 신규/수정
A 측류 권장 SP 쓰기 버튼 (스트림별) UI 수정 + 백엔드 소폭
B FEED 램프 실행기 (시작·단계쓰기·취소·상태) 신규 서비스 + 설정필드 + 엔드포인트 + UI

비범위: 자동 연속제어, 루프 모드 자동전환, OPC UA 복원.


3. 안전 / 도메인 요구사항 (반드시 준수)

실제 공정 컨트롤러에 쓰는 작업이다. 아래는 협상 불가.

  1. 명시적 인가: 모든 실쓰기는 버튼 → confirm() 다이얼로그 → 1회 실행. 폴링/타이머가 스스로 첫 쓰기를 시작하면 안 됨. (램프는 시작 후 단계 진행은 자동, 그러나 "시작"은 버튼.)
  2. WriteGuard 통과 필수: 측류 쓰기는 기존 FeedforwardWriteGuard.Check 통과한 경우만. 차단 사유는 사용자에게 표시.
  3. 범위 클램프: 모든 쓰기 값은 [SpMin, SpMax](측류) / [FeedSpMin, FeedSpMax](피드) 내로 강제 클램프. 범위 밖이면 쓰기 거부 + 사유 표시.
  4. 레이트 리밋: 동일 대상 최소 간격(측류: 기존 endpoint는 즉시 1회라 OK / 피드 램프: ≥ ScanSec*2, 권장 5~15s) 미만 재쓰기 금지.
  5. 피드 불량 시 HOLD: Feed PV가 bad/stale/≤0이면 램프 즉시 정지(다음 단계 안 씀), 상태에 HOLD 표시. (FeedRampCalculator가 이미 Hold=true 반환 — 이를 실행기에서 존중.)
  6. 단조 진행: 업램프 중 계산된 ClampedTarget이 ceiling 하락으로 줄어들면, 현재 SP보다 낮은 값으로 갑자기 점프 금지 — 단계 크기는 RampRate × Δt로 제한.
  7. 모드 전제(반드시 확인): HC900 루프가 외부 SP를 수용하는 모드가 아니면 SP 쓰기는 무효/무시될 수 있다. (이 저장소 메모리 mode-write-mechanism: 루프 mode auto/man·LSP/RSP=CASC는 별도 0xBA/0xBC 레지스터로 씀.) → §4 확인필요. 최소한 쓰기 전 현재 모드를 읽어 경고하거나, 모드 불일치 시 차단할 것.
  8. 감사로그: 측류·피드 모든 실쓰기·차단은 IFeedforwardAuditService.LogAsync로 남긴다(operator, value, result, 사유). 쓰기 실패 시 전송오류 메시지는 그대로 보존(추적성 — D-10 철회: 이 경로에 토큰/비밀번호는 흐르지 않음).
  9. 취소 즉시성: 램프 취소 버튼은 다음 단계 쓰기를 즉시 멈춘다(현재 SP 유지, 되돌리지 않음).

D-1(정정). WriteGuard 결합 분리 — 가드 유지, AdvisoryOnly만 분리

증상(사실): FeedforwardWriteGuard.Check (L12~13)는 cfg.AdvisoryOnly가 true면 모든 쓰기를 차단한다. 그런데 이 작업은 auto-write를 막으려 AdvisoryOnly=true를 유지하므로 버튼 수동쓰기까지 같이 막힌다. 원인은 auto/manual 두 경로가 AdvisoryOnly 한 플래그에 결합된 것.

채택 금지(v2 오처방): "버튼 쓰기는 WriteGuard를 우회하고 SpMin/Max·Commanded·SpNodeId만 검증" — 이는 Valid(stale/무효 advisory)·Grade==C(저신뢰)·Transient(과도상태) 가드를 제거한다. 사람이 라이브 컬럼에 값을 미는 순간일수록 이 셋이 가장 중요하다(되돌릴 기회 1회). 우회는 §3.2와 정면충돌하는 안전회귀다.

채택(결합 분리): 가드는 그대로 두고 AdvisoryOnly 의미만 분리한다. 택1:

  • (권장) WriteGuard.Check(cfg, adv, sc, column, bool manualOverride=false)manualOverride==true일 때 AdvisoryOnly 체크 한 줄만 건너뛰고 Valid/Grade/Transient/SpMin~SpMax/NaN·Inf는 전부 유지. WriteSp(버튼)만 manualOverride: true로 호출.
  • (대안) auto-write 게이트를 별도 플래그 AutoWriteEnabled로 옮기고 AdvisoryOnly는 "자동쓰기 금지·버튼 허용"으로 재정의.

→ 결과: AdvisoryOnly 컬럼이라도 버튼은 가능하되, 과도·저신뢰·stale 상태에서는 여전히 차단된다.


4. 착수 전 확인 필요 (구현자가 사용자/현장에 질문)

이 항목들은 값/정책이 코드에 없으므로 추정 금지, 확정 후 진행:

  • C-1. 컨트롤러 ID 소스: 컬럼→컨트롤러(C1~C4) 매핑을 어디서? (권장: ColumnConfig.ControllerId 필드 추가, 기본 "C1". 현재 하드코딩 "HC1"이 실제 존재 ID인지 config/gateway-config.json과 대조.)
  • C-2. SP 태그 명명: SpNodeId가 OPC UA식(ns=3;s=ficq-6113.sp)인지 HC900 태그명(FICQ-6113.SP, 레지스터맵 대문자)인지. 게이트웨이는 태그명을 그대로 받으므로 HC900 태그명이어야 함. 기존 설정값 점검·정규화 필요.
  • C-3. Feed SP 태그: FEED 루프의 SP 쓰기 대상 태그명(예: FICQ-6101.SP)과 FeedSpMin/Max. register-map에서 access=RW 확인.
  • C-4. 루프 모드 전제(§3.7): SP 쓰기가 유효하려면 필요한 모드와, 모드 불일치 시 "차단" vs "경고 후 진행" 정책.
  • C-5. 램프 단계 주기/스텝: 쓰기 간격(기본 10s 제안)과 한 컬럼만/다중 컬럼 동시 램프 허용 여부.

확인 전에는 **DRY-RUN 모드(실쓰기 대신 로그만)**로 개발/검증할 것 (§7).


5. 작업 A — 측류 권장 SP 쓰기 버튼

A-1. 백엔드 보정 (최소)

파일: src/Hc900Crawler/Controllers/FeedforwardController.cs

  • D-3 수정: L102 WriteTagAsync("HC1", ...)의 컨트롤러 ID를 C-1 결정값으로 교체 (cfg.ControllerId 또는 설정). 하드코딩 제거. (현재 "HC1"은 매칭 안 돼 쓰기 자체가 실패할 개연성 — 우선순위 높음.)
  • D-1(정정) 적용 — WriteGuard 우회 / 분리 : WriteGuard.CheckmanualOverride 인자를 추가하고 WriteSpmanualOverride: true로 호출한다. 이때 건너뛰는 것은 AdvisoryOnly 체크 한 줄뿐이며 아래는 반드시 유지:
    • s.Role == Commanded
    • adv.Valid (stale/무효 advisory 차단)
    • adv.Grade != C (저신뢰 차단)
    • !column.Transient (과도상태 차단)
    • spVal ∈ [sc.SpMin, sc.SpMax] (범위밖 차단/클램프)
    • spVal NaN/Inf 아님
  • 응답에 WriteGuard 차단 사유가 명확히 내려가는지 확인(현재 BadRequest{error} OK).
  • (선택) 쓰기 전 현재 SP 읽어 응답에 previousSp 포함(UI 표시용) — IHc900GatewayService/ListTags/realtime로 조회.

엔드포인트 시그니처·검증 로직은 그대로 재사용. 새 엔드포인트 만들지 말 것.

A-2. UI — 버튼 추가

파일: src/Hc900Crawler/wwwroot/js/ff.js (ffCard 스트림 행, L166~182)

  • 조건: s.role === 'Commanded' 그리고 s.recommendedSp != null 그리고 컬럼이 advisory 유효(!c.transient, s.valid, s.grade !== 'C')일 때만 행에 버튼 노출. (가드와 동일 조건 — 안 되는 버튼은 비활성/숨김.)
  • 버튼: 권장 SP 칸 옆 또는 신뢰칸에 <button class="btn sm" onclick="ffWriteSp(${c.columnId},'${s.key}',${s.recommendedSp})">SP 반영</button>.
  • 핸들러 ffWriteSp(columnId, key, sp):
    1. confirm(\${key} 스트림 SP를 ${sp} 로 컨트롤러에 쓰시겠습니까?`)` — 취소 시 중단.
    2. await ffApi('POST', \/api/ff/write/${columnId}/${encodeURIComponent(key)}`, {})(value 비움 → 서버가 권장값 사용. 또는{value: sp}` 명시.)
    3. 성공: 토스트/메시지 표시 후 ffLoadDash()로 갱신(기존 lastWriteSp/lastWriteError가 표에 반영됨 — L171).
    4. 실패(특히 401): ffApi가 던지는 메시지 표시("RAG 관리 탭 로그인 필요" 포함).
  • D-8 수정: HTML(ff.html L3~4) 헤더 배지 문구 "읽기 전용" → "권장값 · 버튼으로 인가 시 쓰기" 로 수정.

A-3. 인증

  • 쓰기는 X-Kb-Token 필요(기존). 토큰 없으면 401 → UI에서 로그인 안내. 버튼을 토큰 보유시에만 활성화할지(ffToken() 체크)는 UX 선택.

A-4. 수용 기준 (A)

  • Commanded·유효 스트림에만 "SP 반영" 버튼이 보인다.
  • 클릭 → confirm → 실제 gRPC 쓰기 1회. AdvisoryOnly 컬럼이라도 버튼 쓰기는 허용되지만, Transient/Grade C/Valid 무효/범위밖이면 차단 메시지(가드 유지).
  • 쓰기 결과가 표에 쓰기됨/오류로 표시(lastWriteSp).
  • GET api/ff/auditsp_write 항목 기록.
  • 폴링(3s)이 스스로 쓰기를 트리거하지 않음(버튼만).

6. 작업 B — FEED 램프 실행기

B-1. 설정 모델 확장

파일: src/Core/Application/Feedforward/FeedforwardModels.cs (ColumnConfig)

  • 추가:
    • string? FeedSpNodeId — FEED SP 쓰기 대상 HC900 태그명 (C-3). null이면 램프 실행 불가.
    • double FeedSpMin, double FeedSpMax — 클램프 한계.
    • (선택) string? ControllerId — C-1.
  • D-5 수정: FeedforwardConfigStore.cs DB 컬럼 추가 필요:
    • ff_column_config 테이블에 feed_sp_node_id, feed_sp_min, feed_sp_max, controller_id 컬럼 INSERT.
    • LoadAllAsync SQL에 포함 + 매핑.
    • SaveColumnAsync INSERT/UPDATE에 포함.
  • FeedforwardController.MapConfig(L125) 출력에 추가.
  • D-6 수정: MapConfig 익명객체에 feedSpNodeId, feedSpMin, feedSpMax, controllerId 필드 추가.
  • 설정 UI(ff.js 컬럼 에디터, ffEditColumn/g('ff-f-...') 영역): 위 필드 입력란 추가, POST api/ff/config 페이로드(L461 인근)에 포함.

B-2. 램프 상태 저장소 (신규)

신규: src/Infrastructure/Control/FeedRampJobStore.cs (+ Core/Application/Feedforward/IFeedRampStores.cs에 인터페이스)

  • 컬럼별 활성 램프 1개: record FeedRampJob { int ColumnId; double TargetFeed; double LastWrittenSp; string State; string? Hold; DateTime StartedAt; DateTime LastStepAt; string Operator; string[] Warnings; }
  • State: Idle | Ramping | Hold | Reached | Canceled.
  • ConcurrentDictionary<int, FeedRampJob>; Start/Get/GetAll/Cancel/Update.

B-3. 램프 실행 서비스 (신규, BackgroundService)

신규: src/Infrastructure/Control/FeedRampExecutorService.csFeedforwardSupervisor.cs의 패턴(스코프 생성·주기 루프·rate-limit·감사) 참고.

  • 주기 Δt(기본 10s) 루프. 각 활성 Job마다:
    1. FeedRampAdvisorService.ComputeAsync로 최신 CurrentFeed/ClampedTarget/RampRate/Ceiling/Hold/Warnings 획득.
    2. Hold==true(피드 불량) → Job.State=Hold, 쓰기 안 함, 다음 주기.
    3. 목표 도달(|CurrentFeed - min(TargetFeed,Ceiling)| ≤ ε 또는 LastWrittenSp가 ClampedTarget 도달) → State=Reached, 쓰기 종료.
    4. 단계 SP 계산: step = RampRate(kg/hr·min) × (elapsedMin since LastStepAt). nextSp = clamp(LastWrittenSp + step, .., min(TargetFeed, Ceiling)). (다운램프는 현재 계산기 미구현 — §3.6/calculator L87 경고; B에서는 업램프만, 다운 요청은 거부.)
    5. 가드: nextSp[FeedSpMin, FeedSpMax] 클램프, NaN/Inf 거부, 직전 쓰기와 간격 ≥ ScanSec*2 확인.
    6. Hc900WriteService.WriteTagAsync(controllerId, cfg.FeedSpNodeId, nextSp) → 성공 시 LastWrittenSp=nextSp, LastStepAt=now. 감사로그(action="feed_ramp_write").
    7. 실패 → Warnings에 기록, State 유지(다음 주기 재시도, 단 연속 실패 N회 시 Hold).
  • DRY-RUN 플래그: appsettingsFeedforward:FeedRampDryRun(기본 true) — true면 실제 쓰기 대신 로그만. C-1~C-4 확정·현장 합의 후 false.
  • SimOverride 활성 시 입력이 가짜이므로 실쓰기 억제(Supervisor L83·88 패턴과 동일).
  • Program.csAddHostedService<FeedRampExecutorService>() + FeedRampJobStore DI 등록.

B-4. 엔드포인트 (신규, FeedforwardController)

  • POST api/ff/feed-ramp/{columnId}/start body { double targetFeed }X-Kb-Token 필수. 검증: 컬럼 존재·FeedSpNodeId 설정·targetFeed > currentFeed(업램프)·DryRun 여부 응답. Job 생성(State=Ramping). 즉시 쓰기 금지(다음 실행기 주기에 첫 단계).
  • POST api/ff/feed-ramp/{columnId}/cancel — Job.State=Canceled, 다음 단계 중단.
  • GET api/ff/feed-ramp/{columnId} / GET api/ff/feed-ramp — 상태 조회(진행률·LastWrittenSp·target·ceiling·hold·warnings·dryRun).

B-5. UI — 램프 시작/상태

파일: ff.js / ff.html

  • 램프 계산기 패널(#ff-ramp, ffRampCompute)에 "램프 시작" 버튼 추가:
    • 클릭 → confirm(\FEED를 ${target}까지 ${rampTimeMin}분에 걸쳐 단계적으로 올립니다. 시작?`)POST .../start`.
    • DryRun이면 버튼 라벨/배지에 "모의(DryRun)" 표기.
  • 컬럼 카드(ffCard)에 활성 램프 상태 줄 추가: GET api/ff/feed-ramp 결과로 진행 ▶ FEED SP ${LastWrittenSp} → ${target} (ceiling ${ceiling}) [Hold/경고] + 취소 버튼.
  • 폴링 주기에 램프 상태도 함께 갱신.

B-6. 수용 기준 (B)

  • FeedSpNodeId 미설정 컬럼은 램프 시작 불가(명확한 사유).
  • 시작 버튼 → 실행기가 RampRate 보폭으로 FEED SP를 단계적으로 증가(단번 점프 없음). 로그/상태에서 단계 확인.
  • Ceiling/SpMax에서 멈춤. TargetFeed 도달 시 State=Reached, 쓰기 종료.
  • Feed PV bad/stale → 즉시 Hold, 쓰기 정지.
  • 취소 → 즉시 정지(현재 SP 유지).
  • DryRun=true에서 실제 쓰기 0건(로그만), false 전환 후에만 실쓰기.
  • 모든 단계/차단 감사로그 기록.

B-7(부기). D-2 철회 + D-4 후속정리 (선택)

  • D-2 철회: "warning 4줄 하드코딩"은 사실오류 — FeedRampCalculator.Compute에서 무조건 추가되는 건 2줄(L49 steam ceiling, L73 energyLoop)이고, 이는 버그가 아니라 미구현 제약(스팀·에너지루프 ceiling)을 명시하는 의도적 TODO 마커다. 본 작업의 필수 항목 아님. 정 거슬리면 UI에서 미산정 계열 정보성 문구를 실제 경고와 분리 표시(별도 스타일)하는 정도의 선택적 정리만.
  • D-4 후속정리(선택): FeedforwardSupervisor.AutoWriteAsync L141의 "HC1"도 동일하게 컨트롤러 ID 파라미터화. 단 이 작업은 AdvisoryOnly=true 유지로 AutoWrite가 실행되지 않으므로 死경로 — 본 작업의 차단 요소가 아니며 일관성 차원의 후속 정리.

7. 테스트 계획

  1. 단위: FeedRampExecutorService 단계 계산(보폭·클램프·Hold·도달) — FeedRampCalculator는 순수함수라 기존 패턴대로 모킹.
  2. 시뮬레이터: test/modbus_sim.py(port 5020) + test/read_tags.py로 SP 쓰기 반영 확인. 게이트웨이를 sim 대상으로 띄워 gRPC 경로 검증.
  3. SimOverride/DryRun: 가짜 입력으로 UI·상태머신 흐름 검증(실쓰기 0건 확인).
  4. WriteGuard 경계: Grade C / Transient / Valid 무효 / 범위밖에서 측류 버튼 차단 확인. AdvisoryOnly 컬럼에서는 (다른 가드 통과 시) 버튼 쓰기 허용됨을 확인(manualOverride 분리 동작).
  5. 실컨트롤러(현장, C-1~C-4 확정 후): DryRun=false, 단일 스트림·소폭으로 1회 측류 쓰기 → 실제 SP 변화 확인 → 그 후 피드 램프 소구간 검증.

8. 산출물 체크리스트

  • ColumnConfig + 저장소 + 설정 UI: FeedSpNodeId, FeedSpMin/Max, (ControllerId).
  • FeedRampJobStore + IFeedRampStores.
  • FeedRampExecutorService(BackgroundService) + Program.cs 등록 + DryRun 설정.
  • FeedforwardController: feed-ramp start/cancel/status 3개 + 측류 쓰기 컨트롤러ID 보정 + WriteGuard manualOverride 분리(우회 아님).
  • WriteGuard.Check 시그니처에 manualOverride 추가 — Valid/Grade/Transient/범위 가드 유지.
  • ff.js/ff.html: 측류 "SP 반영" 버튼, 램프 "시작/취소/상태" UI, 배지 문구.
  • 감사로그 액션: sp_write(기존), feed_ramp_write(신규). 실패 메시지 보존(D-10 철회).
  • 단위·sim 테스트.
  • §4 확인필요 항목 모두 확정·문서화.
  • (선택) D-4 AutoWriteAsync 컨트롤러ID 파라미터화. D-2는 철회(필수 아님).

부록 — 핵심 파일 인덱스

역할 경로
측류 쓰기 엔드포인트 src/Hc900Crawler/Controllers/FeedforwardController.cs (WriteSp L78)
쓰기 가드 src/Infrastructure/Control/FeedforwardWriteGuard.cs
gRPC 쓰기 src/Infrastructure/Hc900/Hc900WriteService.cs / ControllerGrpcClientPool.cs
자동쓰기(참고·미사용) src/Infrastructure/Control/FeedforwardSupervisor.cs (AutoWriteAsync L107)
램프 계산기(순수함수) src/Infrastructure/Control/FeedRampCalculator.cs
램프 입력수집 src/Infrastructure/Control/FeedRampAdvisorService.cs
램프 DTO src/Core/Application/Feedforward/FeedRampModels.cs
설정 모델 src/Core/Application/Feedforward/FeedforwardModels.cs
설정 저장소 src/Infrastructure/Control/FeedforwardConfigStore.cs
UI src/Hc900Crawler/wwwroot/panes/ff.html, wwwroot/js/ff.js
감사 IFeedforwardAuditService, GET api/ff/audit