=== 민감단온도(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: 각 패널 확장
25 KiB
작업지시서 — 측류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.Check가 AdvisoryOnly면 버튼 쓰기도 차단 — 증상은 사실. 그러나 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. 목표 (한 줄)
- 측류추출 권장 유량(Advisory) 화면에서 버튼 클릭 시에만 해당 스트림의 권장 SP를 실제 HC900 컨트롤러 SP로 쓴다. (자동 쓰기 아님 — 운전원 명시적 인가)
- FEED 목표 변경 시 단번에 점프시키지 않고, 램프 계산기 결과(rate·ceiling)에 따라 FEED SP를 천천히 단계적으로 올린다. 버튼으로 시작/취소.
핵심 원칙: 두 기능 모두 "운전원이 버튼을 눌러야만" 실제 쓰기가 시작된다. 백그라운드 자동쓰기(AdvisoryOnly=false 경로)는 이 작업에서 사용하지 않으며 기본값 그대로 둔다.
1. 현재 코드 자산 (재사용 — 새로 만들지 말 것)
1-A. 측류 SP 수동 쓰기 — 백엔드 이미 존재
src/Hc900Crawler/Controllers/FeedforwardController.csPOST 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.cs—Check():- Commanded 스트림만 /
AdvisoryOnly면 차단 / 권장SP 존재 /Valid/ Grade≠C /Transient아님 /SpMin~SpMax범위 / NaN·Inf 아님.
- Commanded 스트림만 /
src/Infrastructure/Hc900/Hc900WriteService.cs—WriteTagAsync(controllerId, tagName, value)→ gRPCWriteTag.- 감사:
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.jsffLoadDash()(L145) →GET /api/ff/advisory→ffCard(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.cs—Compute(...)순수함수. 산출: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.csGET 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.csColumnConfig: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. 안전 / 도메인 요구사항 (반드시 준수)
실제 공정 컨트롤러에 쓰는 작업이다. 아래는 협상 불가.
- 명시적 인가: 모든 실쓰기는 버튼 →
confirm()다이얼로그 → 1회 실행. 폴링/타이머가 스스로 첫 쓰기를 시작하면 안 됨. (램프는 시작 후 단계 진행은 자동, 그러나 "시작"은 버튼.) - WriteGuard 통과 필수: 측류 쓰기는 기존
FeedforwardWriteGuard.Check통과한 경우만. 차단 사유는 사용자에게 표시. - 범위 클램프: 모든 쓰기 값은
[SpMin, SpMax](측류) /[FeedSpMin, FeedSpMax](피드) 내로 강제 클램프. 범위 밖이면 쓰기 거부 + 사유 표시. - 레이트 리밋: 동일 대상 최소 간격(측류: 기존 endpoint는 즉시 1회라 OK / 피드 램프:
≥ ScanSec*2, 권장 5~15s) 미만 재쓰기 금지. - 피드 불량 시 HOLD: Feed PV가 bad/stale/≤0이면 램프 즉시 정지(다음 단계 안 씀), 상태에 HOLD 표시. (
FeedRampCalculator가 이미Hold=true반환 — 이를 실행기에서 존중.) - 단조 진행: 업램프 중 계산된
ClampedTarget이 ceiling 하락으로 줄어들면, 현재 SP보다 낮은 값으로 갑자기 점프 금지 — 단계 크기는RampRate × Δt로 제한. - 모드 전제(반드시 확인): HC900 루프가 외부 SP를 수용하는 모드가 아니면 SP 쓰기는 무효/무시될 수 있다. (이 저장소 메모리
mode-write-mechanism: 루프 mode auto/man·LSP/RSP=CASC는 별도 0xBA/0xBC 레지스터로 씀.) → §4 확인필요. 최소한 쓰기 전 현재 모드를 읽어 경고하거나, 모드 불일치 시 차단할 것. - 감사로그: 측류·피드 모든 실쓰기·차단은
IFeedforwardAuditService.LogAsync로 남긴다(operator, value, result, 사유). 쓰기 실패 시 전송오류 메시지는 그대로 보존(추적성 — D-10 철회: 이 경로에 토큰/비밀번호는 흐르지 않음). - 취소 즉시성: 램프 취소 버튼은 다음 단계 쓰기를 즉시 멈춘다(현재 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.Check에manualOverride인자를 추가하고WriteSp는manualOverride: true로 호출한다. 이때 건너뛰는 것은AdvisoryOnly체크 한 줄뿐이며 아래는 반드시 유지:s.Role == Commandedadv.Valid(stale/무효 advisory 차단)adv.Grade != C(저신뢰 차단)!column.Transient(과도상태 차단)spVal ∈ [sc.SpMin, sc.SpMax](범위밖 차단/클램프)spValNaN/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):confirm(\${key} 스트림 SP를 ${sp} 로 컨트롤러에 쓰시겠습니까?`)` — 취소 시 중단.await ffApi('POST', \/api/ff/write/${columnId}/${encodeURIComponent(key)}`, {})(value 비움 → 서버가 권장값 사용. 또는{value: sp}` 명시.)- 성공: 토스트/메시지 표시 후
ffLoadDash()로 갱신(기존lastWriteSp/lastWriteError가 표에 반영됨 — L171). - 실패(특히 401):
ffApi가 던지는 메시지 표시("RAG 관리 탭 로그인 필요" 포함).
- D-8 수정: HTML(
ff.htmlL3~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/audit에sp_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.csDB 컬럼 추가 필요:ff_column_config테이블에feed_sp_node_id,feed_sp_min,feed_sp_max,controller_id컬럼 INSERT.LoadAllAsyncSQL에 포함 + 매핑.SaveColumnAsyncINSERT/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.cs — FeedforwardSupervisor.cs의 패턴(스코프 생성·주기 루프·rate-limit·감사) 참고.
- 주기
Δt(기본 10s) 루프. 각 활성 Job마다:FeedRampAdvisorService.ComputeAsync로 최신CurrentFeed/ClampedTarget/RampRate/Ceiling/Hold/Warnings획득.Hold==true(피드 불량) → Job.State=Hold, 쓰기 안 함, 다음 주기.- 목표 도달(
|CurrentFeed - min(TargetFeed,Ceiling)| ≤ ε또는 LastWrittenSp가 ClampedTarget 도달) → State=Reached, 쓰기 종료. - 단계 SP 계산:
step = RampRate(kg/hr·min) × (elapsedMin since LastStepAt).nextSp = clamp(LastWrittenSp + step, .., min(TargetFeed, Ceiling)). (다운램프는 현재 계산기 미구현 — §3.6/calculator L87 경고; B에서는 업램프만, 다운 요청은 거부.) - 가드:
nextSp를[FeedSpMin, FeedSpMax]클램프, NaN/Inf 거부, 직전 쓰기와 간격≥ ScanSec*2확인. Hc900WriteService.WriteTagAsync(controllerId, cfg.FeedSpNodeId, nextSp)→ 성공 시LastWrittenSp=nextSp,LastStepAt=now. 감사로그(action="feed_ramp_write").- 실패 → Warnings에 기록, State 유지(다음 주기 재시도, 단 연속 실패 N회 시 Hold).
- DRY-RUN 플래그:
appsettings의Feedforward:FeedRampDryRun(기본 true) — true면 실제 쓰기 대신 로그만. C-1~C-4 확정·현장 합의 후 false. SimOverride활성 시 입력이 가짜이므로 실쓰기 억제(Supervisor L83·88 패턴과 동일).Program.cs에AddHostedService<FeedRampExecutorService>()+FeedRampJobStoreDI 등록.
B-4. 엔드포인트 (신규, FeedforwardController)
POST api/ff/feed-ramp/{columnId}/startbody{ 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.AutoWriteAsyncL141의"HC1"도 동일하게 컨트롤러 ID 파라미터화. 단 이 작업은AdvisoryOnly=true유지로 AutoWrite가 실행되지 않으므로 死경로 — 본 작업의 차단 요소가 아니며 일관성 차원의 후속 정리.
7. 테스트 계획
- 단위:
FeedRampExecutorService단계 계산(보폭·클램프·Hold·도달) —FeedRampCalculator는 순수함수라 기존 패턴대로 모킹. - 시뮬레이터:
test/modbus_sim.py(port 5020) +test/read_tags.py로 SP 쓰기 반영 확인. 게이트웨이를 sim 대상으로 띄워 gRPC 경로 검증. - SimOverride/DryRun: 가짜 입력으로 UI·상태머신 흐름 검증(실쓰기 0건 확인).
- WriteGuard 경계: Grade C / Transient / Valid 무효 / 범위밖에서 측류 버튼 차단 확인. AdvisoryOnly 컬럼에서는 (다른 가드 통과 시) 버튼 쓰기 허용됨을 확인(manualOverride 분리 동작).
- 실컨트롤러(현장, 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 |