diff --git a/docs/ControlEdge HC900 IO Modules Specifications.pdf b/docs/ControlEdge HC900 IO Modules Specifications.pdf new file mode 100644 index 0000000..7256b6b Binary files /dev/null and b/docs/ControlEdge HC900 IO Modules Specifications.pdf differ diff --git a/docs/논의-AI운전원-제어-아이디어.md b/docs/논의-AI운전원-제어-아이디어.md new file mode 100644 index 0000000..9ac1def --- /dev/null +++ b/docs/논의-AI운전원-제어-아이디어.md @@ -0,0 +1,76 @@ +# 논의 정리: AI 운전원 / 직접제어 아이디어 + +> 2026-06-11 대화 정리. 스마트폰에서 진행한 브레인스토밍을 다시 논의하기 위해 기록. + +## 출발 질문 +- 히스토리컬 데이터 기준으로 **현재 제어출력(OP)에서 앞으로 5분간 유량 예측**이 가능한가? + +### 결론 +가능. 단 "예측"을 둘로 나눠야 함: +- **OP → 유량 정상상태**: 유량 루프는 응답 빠른 계통(시정수 수 초~수십 초). 5분(300초)은 과도응답이 아니라 거의 정상상태. → OP에 대한 정상상태 게인 맵으로 예측 쉬움. (이미 learned-control의 steady-state map과 동일한 물건) +- **5분 동안의 변동 요인**: 예측 오차의 진짜 원인. + - OP 자체가 5분 안에 바뀜 (자동=컨트롤러, 수동=운전원) → OP 미래궤적부터 예측해야 함 + - 미측정 외란 (헤더 압력, 상류 공급, 다른 루프 간섭) + +### 데이터 제약 +| 소스 | 주기 | 5분=점개수 | 용도 | +|---|---|---|---| +| `history_table` | 60초 | ~5점 | 정상상태 예측엔 충분, 동특성 식별엔 거침 | +| `realtime_table` | ~1초 | ~300점 | FOPDT 동특성 식별에 필요 | + +--- + +## 우리 프로젝트의 진짜 자산 (moat) +이 셋을 **한 시스템 안에** 다 가진 게 차별점 (대부분 시스템은 하나만 가짐): +1. **컨트롤러 직접 쓰기 가능** — gateway WriteTag + mode write가 C3에서 실측 검증됨 +2. **운전원 행동 히스토리 + P&ID 그래프 + KB 문서** — trace_connections, build_pid_graph +3. **LLM/RAG 추론 레이어** + +## 후보 아이디어 (임팩트 순) +1. 🔥 **"AI 운전원" — 자문에서 자율로**: 학습 모델이 선택된 MANUAL 루프에 직접 OP 쓰기. 쓰기 경로 이미 검증됨. 단계적(자문→1클릭 승인→화이트리스트 자율). 안전장치: 그래프/KB로 하류 영향 사전 체크. +2. **그래프 기반 자동 근본원인 추적**: 값 비정상 시 P&ID 그래프 따라 상류 전파 추적 → 원인 노드 식별. 알람 홍수를 한 줄 인과로 압축. +3. **외란 도착 예측 → 선제 피드포워드**: 그래프에 시간 입혀서 상류 외란 도착 시각 계산 → PV 움직이기 전에 OP 선제 조정. 5분 예측이 보고용이 아니라 선제 제어입력이 됨. +4. **소프트 센서**: 필드계기는 고유 태그 없음. 측정 안 되는 양을 상관 태그+모델로 추정 → 비용 0으로 계측점 늘리기. + +**추천 방향:** #1을 목표로, #2·#3을 안전·예측 엔진으로 깔기 = "설명 가능하고(그래프) 앞을 내다보는(예측) AI 운전원". + +--- + +## ⚠️ 핵심 반론 (사용자 지적) +지난 **4개월 운전원 수동 데이터** 온도 프로파일을 보면 **민감단 온도가 크게 안 흔들림 → 운전원이 이미 제어를 매우 잘함**. +→ 우리가 직접 제어해서 "더 정밀하게" 이겨봐야 어필이 안 됨. 운전원의 정상상태 제어를 타이트하게 이기는 건 어렵고, 이겨도 감동이 없음. + +### 대응 전략: 채점표를 바꿔라 +운전원이 이기는 축(정상상태 온도 타이트함)에서 싸우지 말고, 운전원이 **구조적으로 못 이기는** 세 축에서 싸운다. + +**① 경제 마진 — 진짜 팔리는 숫자** +- 민감단 온도가 안 흔들린다 = 운전원이 **보수적 마진**을 깔고 운전(환류/스팀 여유). 그 여유 = 매일 버려지는 에너지/처리량. +- 어필: "온도 안정도는 운전원과 **동일**, 셋포인트를 최적점으로 당겨 스팀 X% 절감." +- 제어를 이기는 게 아니라 **같은 안정도로 더 싸게**. APC가 30년간 돈 번 방식. 운전원은 다변수 동시 최적화 못 하니 구조적으로 못 따라옴. + +**② 선제성(앞먹임) — 운전원은 PV 움직인 뒤에 반응** +- 우리는 그래프로 상류 외란 도착 미리 계산 → 온도 움직이기 전 OP 조정. +- 어필: "공급 변동 시 민감단 온도 최대 편차, 운전원 대비 절반." 운전원이 원리적으로 못 이기는 영역. + +**③ 분산(variance) — 4개월 프로파일의 함정** +- 깔끔한 프로파일은 잘하는 운전원의 좋은 날 평균. 교대조별·시간대별(새벽 3시)·신참으로 쪼개면 표준편차 다름. +- 가치 = "항상 최고 운전원 수준, 24시간, 모든 교대조." 평균이 아니라 최악 시간대를 끌어올림. + +--- + +## 다음 액션: 제어 건드리기 전 오프라인 검증 +4개월 히스토리로 **카운터팩추얼 분석** (컨트롤러 안 건드림, 데이터로만): +1. 운전원이 깔고 있는 보수 마진 측정 (민감단 온도 vs 제약한계 거리, 환류/스팀 여유분) +2. "온도 안정도 그대로 두고 마진만 당겼다면 절감액 = ?" 정량 추정 +3. 외란 구간 운전원 반응 지연 측정 → 앞먹임으로 줄일 수 있는 편차 정량화 + +→ 이 세 숫자가 나오면 제어 켜기 전에 어필 자료 완성. 셋 다 "운전원보다 정밀"이라는 어려운 약속이 아니라 "운전원이 못 하는 것"이라는 방어 가능한 약속. + +### 첫 단추 (추천) +**①(마진)부터.** 필요한 것: 민감단 온도 태그 + 환류/스팀 관련 태그. → 4개월치로 "버려지는 마진"이 실제 존재하는지 데이터로 확인. + +## 다시 논의할 때 정할 것 +- [ ] 어느 축부터 갈지 (추천: ① 마진) +- [ ] 대상 컬럼/루프 및 민감단 온도 태그명 +- [ ] 환류/스팀 등 마진 관련 태그명 +- [ ] realtime 1초급 데이터가 실제로 쌓이는지 확인 (동특성 식별용) diff --git a/docs/작업지시서-API-필드-PascalCase-통일.md b/docs/작업지시서-API-필드-PascalCase-통일.md new file mode 100644 index 0000000..33f7121 --- /dev/null +++ b/docs/작업지시서-API-필드-PascalCase-통일.md @@ -0,0 +1,796 @@ +# 구현 검증 보고서 — 작업지시서-API-필드-PascalCase-통일 + +**검증일**: 2026-06-11 +**검증 대상**: 작업지시서 §5 Phase 0–3 구현 결과 +**검증 방법**: `diagnosis-checklist.md` 8단계 + 전수 코드 스캔 (28개 파일 변경, +631/-544줄) +**빌드 상태**: ✅ `dotnet build` 0 Error, 0 Warning +**전체 등급**: 🟠 MED — Phase 0(PascalCase parse) 완료, Phase 1·2 다수 누락, Phase 3 양호 + +--- + +## STEP 1 — 맥락 파악 + +**질문: 무엇이 구현되었고, 무엇을 검증하는가?** + +작업지시서는 4개 Phase로 구성: +- **Phase 0** — `TextToSqlController.parse` 핫픽스 (PascalCase 응답) +- **Phase 1** — 15개 컨트롤러 익명객체 응답 PascalCase 통일 +- **Phase 2** — 17개 JS 파일 응답 읽기 PascalCase 통일 +- **Phase 3** — 내부용 `[JsonPropertyName]` 제거 + +구현 결과 28개 파일이 변경됨 — 컨트롤러 15개 전부, JS 10개, DTO 2개, 서비스 1개 수정. 본 검증은 각 Phase가 **완전히** 구현되었는지, **누락 및 회귀**가 없는지를 진단한다. + +--- + +## STEP 2 — 구조 탐색 + +변경된 파일 계층: + +``` +Controllers/*.cs (15) ──응답 PascalCase──▶ wwwroot/js/*.js (10) ──소비 + │ │ + └── DTOs/*.cs (2) ──[JsonPropertyName] 제거──┘ + │ + └── Service/*.cs (1) ──InputSchema 추가 (관련 없음) +``` + +**28개 변경 파일 중 `git diff --stat` 기준**: +- 컨트롤러: 15개 (모든 컨트롤러) +- JS: 10개 (core.js·app.js·evt.js 제외한 나머지) +- DTO: `ExperionDtos.cs`, `TrendDtos.cs` +- 서비스: `McpService.cs`, `IMcpService.cs` +- 기타: `prompts/plant_context.md` + +--- + +## STEP 3 — 코드 읽기 + +### Phase 0: TextToSqlController.parse — ✅ PASS + +```csharp +// TextToSqlController.cs L44 — 변경됨 +return Ok(new { Success = true, Sql = sql }); // ← PascalCase ✅ +return Ok(new { Success = false, Error = ex.Message }); +``` + +이전: `new { success = true, sql }` → 현재: `new { Success = true, Sql = sql }`. +`t2s.js`가 이미 `parseRes.Sql`·`parseRes.Success`로 읽고 있으므로 즉시 복구됨. + +--- + +### Phase 1: 컨트롤러 통일 — ❌ 11/15 누락 + +| 컨트롤러 | 상태 | 잔여 소문자 | +|----------|------|-----------| +| `FastController.cs` | ✅ CLEAN | 0 | +| `TrendController.cs` | ✅ CLEAN | 0 | +| `DocsController.cs` | ✅ CLEAN | 0 | +| `PidGraphController.cs` | ✅ CLEAN | 0 | +| `TextToSqlController.cs` | ✅ CLEAN | 0 | +| `HypertableController.cs` | ⚠️ 1건 | `statusMessage = info.StatusMessage` L22 | +| `KbAuthController.cs` | ⚠️ 1건 | `new { valid }` L50 | +| `Hc900Controllers.cs` | ⚠️ 5건 | Gateway health/status/tags L36-79, `{ total, tags }` L415 | +| `SetupController.cs` | ⚠️ ~6건 | controllers status/config L40-80 (주석: "camelCase 보장") | +| `OllamaController.cs` | ⚠️ ~8건 | `GetModels`/`GetVllmModels` 응답 L291-594 (`success`, `error`, `models`, `excluded`), SSE 툴 이벤트 페이로드 L165-194 | +| `SteamAdvisorController.cs` | ⚠️ ~5건 | ComputeStages 내부 익명객체 L555-577 | +| `PointBuilderController.cs` | ⚠️ ~8건 | Items 매핑 L426-444, Sinam 파일 목록 L207-214 | +| `KbController.cs` | ⚠️ ~12건 | Document/Job Select L155-264, error 응답 | +| `PidController.cs` | ⚠️ ~22건 | Equipment DTO 매핑 L115-141, prefix rule CRUD | +| `FeedforwardController.cs` | ⚠️ **150+건** | `MapConfig`·`MapRampJob`·`MapRamp`·`MapColumn` 전부 미변경 L213-519 | + +> **Phase 1 판정**: 4/15만 CLEAN, 나머지 11개에 소문자 잔존. 특히 `FeedforwardController`는 전체 응답 객체 트리가 100% 소문자여서 효과가 전혀 없음. `PidController`·`KbController`도 대량 미변경. + +--- + +### Phase 2: JS 파일 통일 — ❌ 12/17 파일에 소문자 잔존 + +**🔴 HIGH — trend.js (4개 버그)** + +| 위치 | 문제 | 영향 | +|------|------|------| +| L659 | `pt.value` → `pt.Value` 미변경 | 라이브 틱 데이터가 차트에 반영 안 됨 | +| L676 | `p.Desc` → `p.Description` (DTO 속성명 불일치) | 그룹 빌더 설명 필드 `undefined` | +| L751 | `g.members` → `g.Members` 미변경 | 그룹 멤버 로딩 전면 실패 | +| L752-758 | `m.tag`·`m.color`·`m.axis`·`m.desc` 등 전부 소문자 | 멤버 속성 전부 `undefined` | +| L755-758 | `trAnalogMap[m.tag]?.desc` — map 저장은 `Desc`, 조회는 `desc` | 키 불일치로 항상 `undefined` | +| L698-708 | `p.tagName`·`p.description`·`p.area`·`p.value` 등 전부 소문자 | 그룹 빌더 UI 전면 미동작 | + +**🟠 MED — pb.js (~19건)** + +| 패턴 | 위치 | +|------|------| +| `res.success` → `res.Success` | L135, 370, 434, 469 | +| `res.error` → `res.Error` | L143, 434, 470 | +| `res.message` → `res.Message` | L84, 100, 349, 362 | +| `res.count` → `res.Count` | L84, 100, 111, 112, 238, 349, 362 | +| `res.items` → `res.Items` | L230, 240, 275, 283 | + +**🟠 MED — kbadmin.js (~12건)** + +`data.success`·`data.error`·`data.chunks`·`data.affected`·`data.total` 등 전부 소문자 (L202-341) + +**🟡 LOW — 기타 미변경 파일** + +| 파일 | 건수 | 예시 | +|------|------|------| +| `setup.js` | ~6건 | `r.success ?? r.ok`, `r.message` | +| `write.js` | ~4건 | `d.success`, `d.error`, `d.controllers` | +| `steam.js` | ~3건 | `d.columns`, `data.suggestions`, `d.message` | +| `fast.js` | ~2건 | `data.items`, 요청바디 `{ name, samplingMs }` | +| `ff.js` | ~2건 | `data.columns` | +| `pid.js` | ~5건 | `data.items`, `err.error` | +| `llmchat.js` | 0건 (기존 폴백 유지) | `d.Success ?? d.success` — 방어적, 통일 후 제거 대상 | + +> **Phase 2 판정**: `hist.js`·`evt.js`·`docs.js`·`core.js`·`app.js`는 CLEAN. `t2s.js`는 PascalCase로 올바름. 나머지 12개 파일에 소문자 잔존. 특히 **trend.js는 4개 런타임 버그**(그룹 로딩·라이브 틱·그룹 빌더·아날로그 맵)가 공존. + +--- + +### Phase 3: `[JsonPropertyName]` 정리 — ✅ 양호 (1건 REMOVE 권장) + +25개 `[JsonPropertyName]` 중 24개가 외부 계약으로 KEEP 판정 정확함. + +**REMOVE 권장 1건**: `ExperionDtos.cs:64` +```csharp +[JsonPropertyName("analogmon1")] // ← 미사용 DTO. PointBuilderBuildDto 자체가 dead code +public PointBuilderGroupDto AnalogMonitor1 { get; set; } = new(); +``` +→ `Hc900PointBuilderBuildDto`(같은 파일 L106-113)가 실제 사용 중이므로, 위 클래스 전체 삭제 권장. + +--- + +### 추가 발견: `TrendDtos.cs` — `TrendMemberDto` `[JsonPropertyName]` 3개 제거됨 + +```csharp +// BEFORE +[JsonPropertyName("tag")] public string Tag { get; set; } = ""; +[JsonPropertyName("color")] public string Color { get; set; } = ""; +[JsonPropertyName("axis")] public string Axis { get; set; } = ""; +// AFTER +public string Tag { get; set; } = ""; +public string Color { get; set; } = ""; +public string Axis { get; set; } = ""; +``` + +**의도**: `PropertyNamingPolicy = null`에서 자연스럽게 PascalCase 직렬화. +**영향**: `TrendGroupDto.Members`가 `[{"Tag":"FICQ-6101.PV","Color":"#e41a1c","Axis":"left"}, ...]`로 나감. +**JS 영향**: `trend.js` L751-758이 `g.members`를 `g.Members`로, `m.tag`를 `m.Tag`로 바꾸지 않으면 그룹 기능 중단. 현재 둘 다 소문자여서 **그룹 기능 동작 안 함**. + +--- + +## STEP 4 — 호출 계층 지도 + +Phase 0은 완료되었으나 Phase 1과 Phase 2의 **미완성으로 인한 의존 체인 단절**: + +``` +Phase 1 미완료 (11/15 컨트롤러) + ↓ +Phase 2에서 수정한 JS가 PascalCase API를 호출 + ↓ +미변경 컨트롤러가 camelCase → JS undefined + ↓ +Silent failure — 빈 테이블, 동작하지 않는 버튼 +``` + +반대 방향도 존재: + +``` +Phase 1 완료 (4/15 컨트롤러 — Fast, Trend, Docs, PidGraph) + ↓ +Phase 2 미완료 (trend.js 등이 여전히 camelCase로 읽음) + ↓ +PascalCase API 응답 → JS undefined + ↓ +Silent failure +``` + +**즉, 어떤 방향이든 현재 코드는 **컨트롤러 11개 + JS 12개에 걸쳐 미스매치 상태**다.** + +--- + +## STEP 5 — 패턴 매칭 (체크리스트) + +### 🔴 HIGH — 미변경 camelCase 변수 참조 + +| 체크 | 항목 | 판단 | +|------|------|------| +| [x] | **trend.js `g.members` → `g.Members`** | L751 — `TrendGroupDto.Members`는 PascalClass, API는 `"Members"`, JS는 `g.members` → `undefined`. 그룹 로딩 전면 실패 | +| [x] | **trend.js `m.tag` → `m.Tag` 등** | L752-758 — `TrendMemberDto.Tag`/`Color`/`Axis`가 PascalCase인데 JS가 소문자로 읽음 | +| [x] | **trend.js `p.Desc` → `p.Description`** | L676 — `AnalogPointDto.Description`을 `Desc`로 오기입 | +| [x] | **trend.js `pt.value` → `pt.Value`** | L659 — `TrendLivePointDto.Value`인데 소문자 `pt.value`는 `undefined` | +| [x] | **trend.js 아날로그 맵 키 불일치** | L676 `{ Desc, Unit, ... }`로 저장 → L755-758 `.desc`, `.unit`으로 조회 | +| [x] | **trRenderAnalog 전면 미변경** | L698-708 — 8개 필드 전부 소문자 | + +### 🟠 MED — 불완전한 변경 + +`pb.js`·`kbadmin.js`·`setup.js`·`write.js`·`steam.js`·`ff.js`·`fast.js`·`pid.js` — 변경 자체가 누락. + +### 🟢 LOW — Phase 3 DTO 정리 + +`ExperionDtos.cs`의 dead code 제거는 미이행되었으나 실행에 영향 없음. + +--- + +## STEP 6 — 교차 검증 + +| Q | 질문 | 결과 | +|---|------|------| +| Q1 | 이미 수정된 문제인가? | trend.js 4개 버그는 **아직 수정되지 않음** (현재 파일 상태). pb.js 등 8개 파일도 미수정. | +| Q2 | 다른 레이어에서 처리? | trend.js 그룹 로딩(`g.members`) → 우회 불가. JS에서 유일한 소비 계층. | +| Q3 | 의도적 설계? | trend.js L676 `p.Desc`는 오기입(오타). 나머지는 단순 누락. | +| Q4 | 재현 시나리오? | trend.js: 트렌드 탭 → 그룹 선택 → 빈 화면(멤버 로딩 실패). 그룹 빌더 → 모든 태그 필드 `undefined`. | + +--- + +## STEP 7 — 심각도 분류 및 STEP 8 — 보고서 + +### 🔴 HIGH 1. trend.js 그룹 기능 전면 불능 + +**문제**: `TrendGroupDto.Members`·`TrendMemberDto.Tag/Color/Axis`가 PascalCase로 직렬화되지만, `trApplyGroup`(L751)이 `g.members`·`m.tag` 등 소문자로 읽음. `trAnalogMap`도 저장 키(Desc)와 조회 키(desc)가 불일치. + +**근거**: `TrendDtos.cs` L11-16: `[JsonPropertyName]` 3개 제거, 속성명 `Tag`·`Color`·`Axis`. +`trend.js` L751-758: `g.members.map(m => ({ tag: m.tag, color: m.color, axis: m.axis, desc: trAnalogMap[m.tag]?.desc }))`. + +**영향**: 트렌드 탭 → 그룹 선택 시 멤버 0명 로드 → 차트 빈 화면. 라이브 틱(`pt.value` L659)도 누락되어 데이터 갱신 안 됨. + +**수정**: L751 `g.members` → `g.Members`, L752-758 `m.tag` → `m.Tag`, `m.color` → `m.Color`, `m.axis` → `m.Axis`. L676 `p.Desc` → `p.Description`. L659 `pt.value` → `pt.Value`. L755-758 `.desc` → `.Desc`, `.unit` → `.Unit`, `.euLo` → `.EuLo`, `.euHi` → `.EuHi`. L698-708 `p.tagName` → `p.TagName`, `p.description` → `p.Description`, `p.area` → `p.Area`, `p.value` → `p.Value`, `p.unit` → `p.Unit`, `p.euLo` → `p.EuLo`, `p.euHi` → `p.EuHi`. + +--- + +### 🟠 MED 2. Phase 1 컨트롤러 11개 소문자 잔존 + +**문제**: 15개 컨트롤러 중 11개에 소문자 익명객체가 잔존. 특히 `FeedforwardController`(150+ 속성), `PidController`(22), `KbController`(12)가 대량. + +**근거**: STEP 3 Phase 1 표 참조. + +**영향**: 해당 컨트롤러의 JS(`ff.js`, `pid.js`, `kbadmin.js` 등)가 소문자를 읽는다면 현재는 우연히 맞지만, **Phase 2에서 JS를 PascalCase로 바꾸면 깨짐**. 또는 반대로 **JS가 PascalCase를 읽으면 현재부터 깨져 있음**. + +**수정**: 각 컨트롤러의 모든 `return Ok(new { ... })`·`return BadRequest(new { ... })`에서 소문자 키를 PascalCase로 변경. 작업지시서 §6.1 grep 명령어로 탐색 후 일괄 수정. + +--- + +### 🟠 MED 3. Phase 2 JS 12개 파일 소문자 잔존 + +**문제**: `pb.js`(~19건), `kbadmin.js`(~12건), `setup.js`·`write.js`·`steam.js`·`ff.js`·`fast.js`·`pid.js` 등 12개 파일이 PascalCase API 응답을 소문자로 읽는 코드 잔존. + +**근거**: STEP 3 Phase 2 표 참조. + +**영향**: 해당 탭의 기능이 현재 작동 중이라면, 그것은 해당 컨트롤러가 아직 PascalCase로 바뀌지 않았기 때문(Phase 1도 미완료). **Phase 1이 완료되는 순간 이 JS들은 깨진다.** + +**수정**: 각 파일에서 `.success` → `.Success`, `.error` → `.Error`, `.items` → `.Items` 등 일괄 치환. 작업지시서 §6.2 grep 명령어로 탐색 후 수정. + +--- + +### 🟡 LOW 4. `ExperionDtos.cs` dead code `[JsonPropertyName]` 잔존 + +**문제**: `[JsonPropertyName("analogmon1")]`이 미사용 DTO에 남아 있음. + +**근거**: `ExperionDtos.cs:64`. `PointBuilderBuildDto` 클래스 전체가 `Hc900PointBuilderBuildDto`로 대체되어 미참조. + +**영향**: 없음 (dead code). + +**수정**: 해당 `[JsonPropertyName]` 라인 및 미사용 DTO 클래스 전체 삭제. + +--- + +## STEP 8 자가 검증 + +- [x] 각 지적 사항을 "현재 파일 몇 번 줄"로 직접 가리킬 수 있는가? — trend.js L659·L676·L698-708·L751-758 등 +- [x] HIGH 항목은 재현 가능한 시나리오를 한 문장으로 말할 수 있는가? — 트렌드 탭에서 그룹 선택 시 빈 차트, 그룹 빌더에서 모든 필드 undefined +- [x] 교차 검증 4개 질문을 모두 통과한 항목만 포함되었는가? — 통과 +- [x] 보고서의 수정 예시가 현재 코드에 아직 적용되지 않은 내용인가? — 적용되지 않음 (현재 버그 상태임) +- [x] Phase 1·2의 미완료는 "구현 중단"이 아닌 "구현 누락"으로 판단 — trend.js의 오기입(p.Desc)은 오타, 나머지는 단순 미변경 + +--- + +# 작업지시서 — 내부 REST API 필드명 PascalCase 전면 통일 + +**진단일**: 2026-06-11 +**진단 방법**: `diagnosis-checklist.md` 8단계 순차 적용 +**등급**: 🟠 MED — Plan 작성자의 현황 파악에 오류 1건 확인 + +--- + +## STEP 1 — 맥락 파악 + +**질문: 이 파일은 무엇을 하는 파일인가?** + +작업지시서(이하 "Plan") — 백엔드 내부 REST API 응답 JSON 필드명을 PascalCase로 전면 통일하기 위한 실행 계획. `Program.cs`의 `PropertyNamingPolicy = null` 설정 아래에서 C# 코드의 속성명이 JSON으로 그대로 직렬화됨을 전제로, 15개 컨트롤러와 17개 JS 파일을 대상으로 카탈로그화·단계별 전환을 정의한다. 트리거는 커밋 `dbad4a5`가 `TextToSqlController`만 PascalCase로 바꾸고 `llmchat.js` 등 소비자를 누락해 발생한 무음 버그(silent failure). + +Plan은 아래와 같은 구조로 읽힌다: +1. 배경(§1): 근본 메커니즘 설명 + "PascalCase 통일" 결정 +2. 범위(§2): 변경 대상 vs 절대 변경 금지 대상을 표로 정리 +3. 현황 인벤토리(§3): 컨트롤러별 익명객체 소문자 응답 라인 수 + JS 파일별 주 컨트롤러 매핑 +4. 표준 규약(§4) + 실행 계획(§5, 4Phase) + 작업자 가이드(§6) + 검증 체크리스트(§7) + 리스크(§8) + DoD(§9) + +> **STEP 1 결론**: Plan의 목적과 구조는 명확하며, §2(변경 금지 외부 계약)와 §4(표준 규약)의 판단 기준이 구체적이어서 실행자의 혼란을 방지한다. 다만 §3 현황 인벤토리가 **"이미 수정된 사항"과 "아직 수정되지 않은 사항"을 구분하지 않고 혼합 기재**하여, 문서만 읽는 실행자가 실제 현재 상태를 오인할 위험이 있다. + +--- + +## STEP 2 — 구조 탐색 + +**도움이 될 관련 파일 목록 (Plan이 참조하는 실제 코드):** + +| 파일 | Plan § | Plan에서 주장하는 역할 | +|------|--------|----------------------| +| `src/Hc900Crawler/Program.cs` | §1.1 | `PropertyNamingPolicy = null` (JSON 직렬화 원인) | +| `src/Hc900Crawler/Controllers/TextToSqlController.cs` | §3.1, §5.0 | `parse`만 소문자 잔존 | +| `src/Hc900Crawler/wwwroot/js/t2s.js` | §3.2, §5.0 | 이미 `parseRes.Sql` PascalCase 기대 | +| `src/Hc900Crawler/wwwroot/js/hist.js` | §3.2, §3.3 | `d.Success ?? d.success` 폴백 있음 ← **미확인** | +| `src/Hc900Crawler/wwwroot/js/trend.js` | §3.2, §3.3 | `d.Success ?? d.success` 폴백 있음 ← **미확인** | +| `src/Hc900Crawler/Controllers/*.cs` (14개) | §3.1 현황 인벤토리 | 전수 조사 대상 | +| `src/Hc900Crawler/wwwroot/js/*.js` (17개) | §3.2 JS 매핑 | 전수 조사 대상 | + +> **STEP 2 결론**: 의존 파일 목록은 완전하다. 다만 hist.js·trend.js에 대한 §3.3의 주장(폴백 존재)은 **검증되지 않은 채 작성**되었다( 아래 STEP 3에서 확인). + +--- + +## STEP 3 — 코드 읽기 ★ 주요 오류 발견 + +Plan의 핵심 주장 3가지를 실제 코드와 대조했다. + +### 3.1 ✅ `TextToSqlController.parse` — 소문자 잔존 (Plan §3.1 L86) + +```csharp +// TextToSqlController.cs L44 (실제) +return Ok(new { success = true, sql }); +return Ok(new { success = false, error = ex.Message }); +``` + +Plan의 주장과 **일치**. `parse` 엔드포인트만 `{ success, sql, error }` (camelCase)를 반환하고, 나머지(9개) 엔드포인트는 전부 `{ Success, Sql, Error }` (PascalCase). Plan은 이 차이를 정확히 지적했다. + +### 3.2 ✅ `t2s.js` — 이미 PascalCase 기대 (Plan §5.0 L150) + +```javascript +// t2s.js L118-120 (실제 — parse 버튼 핸들러) +const res = await api('POST', '/api/text-to-sql/parse', { query: input }); +if (res.Success) { // ← PascalCase를 읽지만 + sqlTextarea.value = res.Sql; +} else { + sqlTextarea.value = `오류: ${res.Error || '알 수 없는 오류'}`; +} +// t2s.js L428-432 (실제 — T2S 채팅 핸들러) +const parseRes = await api('POST', '/api/text-to-sql/parse', { query: message }); +if (!parseRes.Success || !parseRes.Sql) { // ← PascalCase를 읽지만 + t2sAddChatMessage('system', `SQL 변환 실패: ...`); +``` + +Plan의 주장과 **일치**. t2s.js는 3곳(버튼·T2S 채팅·API 채팅) 모두 `res.Success`, `res.Sql`, `res.Error`로 PascalCase를 읽고 있다. 그러나 실제 API는 `{ success, sql, error }`(camelCase)를 반환하므로, **지금 이 순간 `parse` 버튼과 T2S 채팅의 SQL 변환 기능은 조용히 깨져 있다**: +- `res.Success`는 항상 `undefined` → if문이 항상 else로 빠짐 +- 화면에 "SQL 변환 실패: 알 수 없는 오류" 표시 +- 실제 SQL 생성은 정상이지만 **UI가 결과를 보여주지 못함** + +### 3.3 ❌ `hist.js`·`trend.js` — 폴백 부재 (Plan §3.3 L119) + +Plan §3.3(이미 적용된 수정)은 다음과 같이 기술한다: + +> `hist.js`·`trend.js`는 `query-history-interval` 읽을 때 이미 `d.Success ?? d.success` 폴백이 적용돼 있어 현재 동작. + +그러나 **실제 코드는 폴백이 없었다** (본 진단 작성자가 수정하기 전): + +```javascript +// hist.js L122-127 (수정 전) — 폴백 없음 +if (!d.success) { throw new Error(d.error || '조회 실패'); } +const rows = d.rows || []; // API는 { Rows, TagNames } (PascalCase) 반환 +const tNames = d.tagNames || []; +``` + +```javascript +// trend.js L358 (수정 전) — 폴백 없음 +rows = (d && d.success !== false) ? (d.rows || []) : []; +``` + +Plan 수립 당시 **이 코드는 동작하지 않았다**. `query-history-interval`은 `{ Success, Rows, TagNames }`(PascalCase)를 반환하지만 JS는 `d.success`, `d.rows`, `d.tagNames`(camelCase)로 읽어 모두 `undefined`였다. 즉: +- hist.js의 **간격 조회 탭이 정상 동작하지 않았다** — 빈 테이블 렌더 +- trend.js의 **집계 경로(interval !== '1 minute')가 동작하지 않았다** — rows가 항상 빈 배열 → rawQuery로 폴백 + +> **STEP 3 결론**: Plan은 사실 확인을 충분히 하지 않은 주장(§3.3)을 포함하고 있다. 최소한 `grep "d\.success" hist.js trend.js` 한 줄이면 발견할 수 있었던 오류. §3.3은 "이미 적용된 수정"을 기술하는 섹션인데, 실제로는 적용되지 않은 상태였다. 여기에 기재된 3개 항목 중 **llmchat.js·McpToolDto·OllamaController 폴백만 적용**되어 있었고 hist.js·trend.js는 적용되지 않았다. + +--- + +## STEP 4 — 호출 계층 지도 작성 + +Plan이 제안한 5개 Phase의 의존 관계 분석: + +``` +Phase 0 — 핫픽스 (parse 엔드포인트) + ↓ 성공해야 +Phase 1 — 컨트롤러 백엔드 통일 (15개, 단위별 작업) + ↓ 각 컨트롤러 수정 직후 +Phase 2 — 프론트엔드 통일 (대응 JS) + ↓ 반복 +Phase 1↔2 완료 후 +Phase 3 — DTO [JsonPropertyName] 정리 + ↓ +Phase 4 — 전체 회귀 검증 (체크리스트 §7) +``` + +> **STEP 4 결론**: Phase 순서와 의존 관계는 타당하다. 특히 "백엔드+프론트 짝 커밋" 원칙(§8.6)이 강조된 점은 과거 실패(dbadd4a5의 부분 머지)를 반영한 조치로 적절하다. 다만 Phase 1의 우선순위(라인 수 오름차순)는 위험도 기반이 아니라 **작업량 기반**이라서, **영향 범위가 큰 컨트롤러**(예: OllamaController는 혼재로 인해 리스크가 높음)가 후순위로 밀릴 수 있다. 위험도 기반으로 `FeedforwardController`(44 lines, 단순)는 먼저, `OllamaController`(22 lines, ⚠️혼재)는 중간에 넣는 등 보강이 권장된다. + +--- + +## STEP 5 — 패턴 매칭 (체크리스트 순회) + +Plan을 8개 체크리스트 영역으로 평가. Plan은 **이미 존재하는 버그가 아니라 앞으로의 작업 계획**이므로, 아래는 Plan 자체의 완전성·정확성을 평가한다. + +### 🟡 LOW — 미정의 변수·함수 없음 (양호) + +Plan의 변수 참조(§6 grep 명령어, §7 체크리스트 항목)는 모두 정의되어 있다. + +### 🟡 LOW — 하드코딩 + +Plan은 의도적으로 **하드코딩된 현황 인벤토리**(§3)를 포함한다. 이는 문서가 스냅샷 성격을 가지므로 적절하다. grep 명령어를 함께 제공(§6)해 독자가 최신 상태를 재생성할 수 있도록 한 점은 좋은 설계. + +### 🟠 MED — 에러 응답 형식 불일치 + +Plan §4.2는 "공통 봉투 통일: 모든 응답은 최상위에 `Success`(bool)"라고 규정하지만, §3.1 현황 인벤토리 기준으로 **6개 컨트롤러**가 응답 형식이 파편화되어 있다: + +| 컨트롤러 | 응답 형식 | 문제 | +|----------|----------|------| +| `HypertableController` | `{ info.IsHypertable, info.TableName }` | 최상위에 `Success` 없음, 엔티티 직접 반환 | +| `FastController` | `{ items }` 또는 `{ session.Id, status }` | 혼재 — 일부만 `{ success = true }` | +| `TrendController` | `{ items }` 또는 `{ error }` | 최상위 `Success` 없음 | +| `OllamaController` | `{ success, models }` 또는 `{ reply }` | 내부 응답도 camelCase | +| `PidGraphController` | `{ success, data, message }` | `data`가 내포 객체 | +| `Hc900Controllers` | 엔티티 직접 반환(`Ok(points)` 또는 `Ok(result)`) | 래핑 없음, 형식 다양 | + +Plan은 이 차이를 인지하고 §4.2에서 통일을 규정했으나, Phase 1의 작업 우선순위는 현황 복잡성을 반영하지 않았다. 응답 형식이 이미 `{ Success, Error }` 구조에 가까운 컨트롤러(HypertableController·FastController)는 변경량이 적고, 엔티티를 직접 반환하는 컨트롤러(Hc900Controllers)는 래핑이 추가로 필요하다. + +### 🟡 LOW — 동시성 / async 관련 + +Plan의 Phase 1-2 작업은 순차적 엔드포인트 변경으로 동시성 이슈 없음. + +### 🟡 LOW — 보안 + +SQL Injection 경로 트래버설 관련 논의는 Plan 범위 밖. 단, §2.2 변경 금지 목록은 적절하다. + +> **STEP 5 결론**: Plan의 가장 큰 위험은 §3.3의 사실 오류(hist.js·trend.js 폴백 주장)와 Phase 1 우선순위가 위험도보다 작업량 중심인 점. §6 grep 명령어도 `new \{[^}]*`로는 **다중 라인 익명객체**를 잡지 못하는 한계가 있다. + +--- + +## STEP 6 — 교차 검증 + +### Q1. 이미 수정된 문제인가? + +| 지적 사항 | 수정 여부 | 판단 | +|-----------|----------|------| +| §3.3 hist.js/trend.js 폴백 주장 오류 | **본 진단 작성 시점에 수정 완료** | 진단 항목에서 **제거** (현재는 폴백 있음) | +| §3.1 parse 소문자 잔존 | 미수정 | **🔴 HIGH — 유지** | +| Phase 1 우선순위 작업량 중심 | N/A (Plan 설계 결정) | **🟡 LOW — 제안** | +| 응답 형식 파편화 | 미수정 (Phase 1에서 처리 예정) | **유지** | + +### Q2. 다른 레이어에서 처리되는가? + +| 항목 | 레이어 처리 | 판단 | +|------|-----------|------| +| parse 소문자 문제 | JS에서 폴백 없이 직접 PascalCase 읽음 — 우회 없음 | **🔴 HIGH** | +| hist.js/trend.js | 현재는 폴백 추가되어 우회됨 | **해소됨** | + +### Q3. 의도적 설계인가? + +| 항목 | 의도적? | 판단 | +|------|---------|------| +| Plan의 §3.3 오류 | 의도적 아님 — 검증 생략으로 인한 실수 | **보고** | +| Phase 1 작업량 중심 우선순위 | 의도적 — 문서에 별도 위험 평가 없음 | **개선 제안** | +| §4 PascalCase 규약 | 의도적 — 문서 §1.3에서 근거 제시 | **수용** | + +### Q4. 재현 시나리오? + +| 항목 | 재현 시나리오 | 판단 | +|------|-------------|------| +| parse 소문자 | Text-to-SQL 탭에서 SQL 질문 입력 → "변환" 버튼 클릭 → "SQL 변환 실패: 알 수 없는 오류" 표시. 브라우저 콘솔에서 `res.Success`가 `undefined`임 확인 가능 | **🔴 HIGH** | +| hist.js (수정 전) | 이력 탭 → 간격 조회 → 빈 테이블 | **수정됨** | + +> **STEP 6 결론**: 교차 검증 후 **1건 🔴 HIGH** (parse 소문자)와 **1건 🟡 LOW** (Phase 1 우선순위 개선 제안)가 남았다. §3.3의 hist.js/trend.js 오류는 현재 수정되어 문서에 반영할 필요가 없으나, **문서의 신뢰성에 영향을 준 사례로 기록**한다. + +--- + +## STEP 7 — 심각도 분류 및 STEP 8 — 보고서 + +### 🔴 HIGH 1. TextToSqlController.parse() 응답 케이싱 불일치 + +**문제**: `POST /api/text-to-sql/parse`가 camelCase `{ success, sql, error }`를 반환하지만, `t2s.js`의 3개 호출 지점(§3.2 확인)이 전부 PascalCase `res.Success`·`res.Sql`·`res.Error`를 읽는다. t2s.js는 이미 PascalCase로 올바르게 작성되어 있으므로 **백엔드만 수정**하면 된다. + +**근거**: `TextToSqlController.cs:44` — `return Ok(new { success = true, sql });` 대 `t2s.js:118-120, 428-432, 592-598` — `res.Success`, `parseRes.Sql`. + +**영향**: Text-to-SQL 탭에서 "변환" 버튼 클릭 시 항상 "SQL 변환 실패: 알 수 없는 오류" 메시지. LLM 챗의 API 채팅/T2S 채팅에서도 동일. 실제 SQL 생성은 정상(로깅 가능)이나 **UI가 사용자에게 결과를 전달하지 못함**. + +**수정 방향**: `TextToSqlController.cs:44,48`에서 `new { success = true, sql }` → `new { Success = true, Sql = sql }`로 변경. 이 수정은 즉시 전체 3개 호출 지점을 동시에 복구한다(Plan §5.0 Phase 0에 명시된 수정과 동일). + +### 🟡 LOW 2. Phase 1 우선순위 — 작업량 vs 위험도 불일치 + +**문제**: Plan §5 Phase 1은 익명객체 라인 수 오름차순(1→2→5→…→44)으로 컨트롤러를 정렬했다. 이는 **작업량 중심** 순서로, **위험도 중심이 아니다**. 예를 들어 `OllamaController`(22줄)는 외부 API payload(변경 금지)와 내부 응답(변경 대상)이 혼재되어 있어 변경 시 리스크가 높지만, `KbController`(33줄)보다 먼저 배치되었다. + +**근거**: Plan §5 Phase 1의 순서에 위험도 평가나 난이도 표시가 없음. + +**영향**: OllamaController에서 실수로 외부 payload(`model`, `messages`, `stream`, `tools` 등 §2.2 변경 금지 키)를 건드리면 **LLM 채팅 전체 장애**. 작업자가 변경 금지 목록(§2.2)과 대조하며 작업해야 함. + +**수정 제안**: Phase 1에 **리스트 업데이트** 주석 추가 — `OllamaController`는 §2.2 변경 금지 혼재로 **위험도: HIGH** 표시. 난이도·위험도에 따라 재정렬하거나, 각 컨트롤러 옆에 ⚠️ 표시 추가. + +### 🟡 LOW 3. §3.3 사실 오류 (이미 수정 — 기록용) + +**문제**: Plan §3.3은 "hist.js·trend.js에 `d.Success ?? d.success` 폴백 적용됨"이라고 주장하나, 실제 코드에는 폴백이 없었다. + +**영향**: 문서 신뢰성 저하. Plan을 읽고 작업 순서를 결정하는 실행자가 hist.js·trend.js를 "이미 완료"로 오인할 수 있다. + +**조치**: §3.3은 별도 수정 없이 본 진단 보고서를 문서 상단에 삽입함으로써 사실 관계를 정정한다. hist.js·trend.js는 본 진단 과정에서 dual-casing 폴백을 추가 완료. + +--- + +## STEP 8 자가 검증 + +- [x] 각 지적 사항을 "현재 파일 몇 번 줄"로 직접 가리킬 수 있는가? — TextToSqlController.cs:44, t2s.js:118·432·592 +- [x] HIGH 항목은 재현 가능한 시나리오를 한 문장으로 말할 수 있는가? — Text-to-SQL 탭에서 질문 입력 → "변환" 버튼 → "SQL 변환 실패" 메시지 +- [x] 교차 검증 4개 질문을 모두 통과한 항목만 포함되었는가? — §3.3 항목은 Q1(이미 수정)으로 본 항목에서 제외, 기록용으로만 기재 +- [x] 보고서의 수정 예시가 현재 코드에 아직 적용되지 않은 내용인가? — parse 수정은 Plan §5.0에 명시되어 있으나 아직 적용되지 않음 +- [x] "더 좋은 방법 제안"과 "현재 코드가 틀렸다"를 혼동하지 않았는가? — Phase 1 우선순위는 "개선 제안"으로 명시 + +--- + +# 작업지시서 — 내부 REST API 필드명 PascalCase 전면 통일 + +| 항목 | 값 | +|---|---| +| 작성일 | 2026-06-11 | +| 대상 | `src/Hc900Crawler` 백엔드 컨트롤러 응답·요청 DTO + `wwwroot/js/*.js` 프론트엔드 | +| 목적 | 백엔드↔프론트 JSON 필드 케이싱 불일치로 인한 무음 버그(silent failure) 제거 | +| 트리거 | 커밋 `dbad4a5`(PascalCase 부분 통일)이 `TextToSqlController`만 바꾸고 일부 소비자(`llmchat.js`, `parse` 엔드포인트)를 누락 → LLM 채팅에서 도구가 vLLM에 전달되지 않아 도구 호출이 텍스트로 노출되는 버그 발생 | + +--- + +## 1. 배경 — 왜 이 작업이 필요한가 + +### 1.1 근본 메커니즘 + +`Program.cs`의 JSON 직렬화 설정: + +```csharp +builder.Services.AddControllers() + .AddJsonOptions(opt => + { + opt.JsonSerializerOptions.PropertyNamingPolicy = null; // ← C# 속성명을 그대로 직렬화 + opt.JsonSerializerOptions.PropertyNameCaseInsensitive = true; // ← 역직렬화(요청)는 케이싱 무시 + }); +``` + +- **`PropertyNamingPolicy = null`** → C# 코드에 쓴 속성명이 **그대로** JSON 필드명이 된다. + - `return Ok(new { success = true })` → `{"success": true}` (소문자) + - `return Ok(new { Success = true })` → `{"Success": true}` (PascalCase) +- **`PropertyNameCaseInsensitive = true`** → **요청 바디**(C#가 받는 쪽)는 케이싱을 무시하고 바인딩한다. 따라서 **요청 방향은 깨지지 않는다.** 문제가 되는 것은 항상 **응답 방향**(JS가 읽는 쪽)이다. + +### 1.2 현재 상태 — "우연히 동작 중" + +현재 시스템은 **대부분의 컨트롤러가 소문자 익명객체(`new { success, data, error }`)를 반환**하고, **프론트도 소문자(`d.success`, `d.data`)로 읽어** 우연히 일치한다. 즉 사실상 컨벤션은 **camelCase/소문자**다. + +커밋 `dbad4a5`가 `TextToSqlController`만 PascalCase로 바꾸면서 **컨벤션이 둘로 갈라졌고**, 한쪽만 바뀐 지점에서 `undefined` 무음 버그가 발생했다. + +### 1.3 결정 + +> **전체를 PascalCase로 통일한다.** (커밋 `dbad4a5`의 방향을 끝까지 밀어붙임) + +소문자로 되돌리는 선택지도 있으나, 이미 `TextToSqlController` + `t2s.js`가 PascalCase로 전환됐고 C# 속성/DTO는 본래 PascalCase가 관례이므로 **PascalCase 통일이 코드 일관성·유지보수 측면에서 우월**하다. + +--- + +## 2. 범위 — 무엇을 바꾸고 무엇을 절대 건드리지 않는가 + +### 2.1 ✅ 변경 대상 (PascalCase로 통일) + +**Hc900Crawler 자체 REST API의 응답/요청 JSON 필드명** — 즉 `Controllers/*.cs`가 만들어 `wwwroot/js/*.js`가 소비하는 우리 내부 계약. + +- 컨트롤러 익명객체 응답: `new { success = … }` → `new { Success = … }` +- 응답 DTO 클래스 속성: 이미 PascalCase면 OK, `[JsonPropertyName("snake")]`로 강제 소문자된 내부 DTO는 검토 후 제거 +- 프론트의 응답 필드 읽기: `d.success` → `d.Success` 등 + +### 2.2 ⛔ 절대 변경 금지 (외부 계약·고정 식별자) + +아래는 **외부 시스템·표준·저장소**와의 계약이므로 케이싱을 바꾸면 즉시 깨진다. **반드시 그대로 유지**한다. + +| 영역 | 예시 필드 | 이유 | +|---|---|---| +| **vLLM / OpenAI API** | `model`, `messages`, `role`, `content`, `stream`, `tools`, `tool_choice`, `tool_calls`, `function`, `arguments`, `finish_reason`, `choices`, `delta`, `max_tokens`, `temperature` | OpenAI 호환 API 스펙. `OllamaController`의 vLLM 호출 payload/파싱 전부 | +| **Ollama API** | `/api/chat`, `/api/tags`, `/api/show`, `name`, `model`, `system`, `messages`, `capabilities`, `done`, `response` | Ollama 네이티브 API 스펙 | +| **MCP JSON-RPC** | `jsonrpc`, `method`, `params`, `name`, `arguments`, `tools`, `inputSchema`, `content` | MCP 프로토콜 스펙. `McpClient.cs`의 `[JsonPropertyName]`은 **유지** | +| **MCP 도구 결과 데이터** | `base_tag`, `tag_name`, `event_type`, `recorded_at`, `livevalue`, `pv`, `sp`, `op`, `area`, `sub_area` 등 | Python `mcp-server/server.py`가 반환하는 snake_case 데이터. SQL 컬럼명과 동일. 프론트는 `r[col]`로 **동적 렌더**하므로 키 이름을 바꾸면 server.py + SQL까지 연쇄 변경 필요 → **이번 작업 범위 밖** | +| **DB 컬럼 / SQL 식별자** | 모든 `snake_case` 컬럼 | PostgreSQL 스키마 | +| **SSE 스트리밍 내부 shape** | `{ message: { content } }`, `json.response`, `event: message/tool_start/tool_result/done` | LLM 스트리밍 API shape를 그대로 미러링. `OllamaController` ↔ `llmchat.js` 양쪽이 동일 규약. **이 shape는 LLM API 정렬을 위해 소문자 유지** (단 `tool_start`/`tool_result`의 `id/name/ok/preview/payload`는 우리 내부 필드 → 선택적 PascalCase 가능하나 양쪽 동시 변경 필수) | +| **localStorage 키** | `llmSessions`, `llmType` 등 | 브라우저 저장 키, JSON 필드 아님 | +| **HTTP 헤더** | `X-Kb-Token`, `Content-Type` 등 | 표준/관례 | + +> **판단 기준**: "이 필드가 우리 C# ↔ 우리 JS 사이에서만 오가는가?" → **Yes면 PascalCase 변경 대상**, 외부(LLM/MCP/DB/브라우저)와 닿으면 **제외**. + +--- + +## 3. 현황 인벤토리 (전수 조사 결과) + +### 3.1 백엔드 — 컨트롤러별 소문자 익명객체 응답 (변경 필요) + +| 컨트롤러 | 익명객체 응답 라인 수 | 비고 | +|---|---:|---| +| `FeedforwardController.cs` | 44 | `new { error }`, `new { success }` 다수 | +| `KbController.cs` | 33 | | +| `PidController.cs` | 32 | | +| `PointBuilderController.cs` | 29 | 최근 작업 영역, 주의 | +| `OllamaController.cs` | 22 | ⚠️ **외부(vLLM/Ollama) payload와 내부 응답 혼재** — 내부 응답만 선별 변경 | +| `TextToSqlController.cs` | 19 | 🔶 **부분 완료**. `parse`(L38–50)만 소문자 잔존 → 즉시 수정 대상 | +| `SteamAdvisorController.cs` | 15 | | +| `DocsController.cs` | 13 | | +| `TrendController.cs` | 13 | | +| `Hc900Controllers.cs` | 10 | | +| `KbAuthController.cs` | 9 | `token`, `expiresAt` 포함 | +| `SetupController.cs` | 7 | | +| `FastController.cs` | 5 | | +| `PidGraphController.cs` | 2 | `error`, `details` | +| `HypertableController.cs` | 1 | | + +### 3.2 프론트엔드 — 응답 필드를 소문자로 읽는 JS (변경 필요) + +전 `wwwroot/js/*.js`(벤더 `xlsx.full.min.js` 제외)가 대상. 탭 ↔ 파일 ↔ 주 컨트롤러 매핑: + +| 탭/기능 | JS 파일 | 주 컨트롤러 | +|---|---|---| +| LLM 채팅 | `llmchat.js` (1031) | `OllamaController`, `TextToSqlController(/tools)` 🔶**부분완료** | +| Text-to-SQL | `t2s.js` (740) | `TextToSqlController` 🔶**부분완료**(단 `parse` 응답 불일치 잔존) | +| 트렌드 | `trend.js` (810) | `TrendController`, `TextToSqlController(/query-history-interval)` ※ 폴백 있음 | +| 스팀 어드바이저 | `steam.js` (811) | `SteamAdvisorController` | +| P&ID | `pid.js` (737), `pid-viewer.js` (416) | `PidController`, `PidGraphController` | +| 피드포워드 | `ff.js` (739) | `FeedforwardController` | +| 문서 | `docs.js` (713) | `DocsController` | +| Point Builder | `pb.js` (546) | `PointBuilderController` | +| Fast(고속) | `fast.js` (499) | `FastController` | +| KB 관리 | `kbadmin.js` (397) | `KbController`, `KbAuthController` | +| 이력 | `hist.js` (383) | `TextToSqlController(/query-history-interval)` ※ 폴백 있음 | +| 설정 | `setup.js` (278) | `SetupController` | +| 공통 | `core.js` (215), `app.js` (7) | (api 헬퍼) | +| 쓰기 | `write.js` (101) | `Hc900Controllers` | +| 이벤트 | `evt.js` (144) | `Hc900Controllers` | + +> `hist.js`·`trend.js`는 `query-history-interval` 읽을 때 이미 `d.Success ?? d.success` 폴백이 적용돼 있어 현재 동작. 통일 작업 시 폴백 제거하고 PascalCase 단일화. + +### 3.3 이미 적용된 수정 (작업 트리, 미커밋) + +이번 디버깅 중 선행 적용된 변경 — 본 작업에 흡수/계승: + +- `llmchat.js`: `/tools` 응답 `d.Success ?? d.success`, `d.Tools ?? d.tools` 폴백 추가 ✅ +- `IMcpService.cs` / `McpService.cs`: `McpToolDto`에 `InputSchema` 추가·전달 ✅ +- `OllamaController.cs`: Python 스타일 텍스트 도구 호출 감지 폴백 ✅ (방어선, 유지) +- `prompts/plant_context.md`: 도구 예시의 Python 함수 문법 제거 ✅ + +--- + +## 4. 표준 규약 (확정) + +1. **내부 REST 응답 필드명 = PascalCase.** `Success`, `Error`, `Data`, `Message`, `Sql`, `Tools`, `Rows`, `Columns`, `Count`, `TagNames`, `Items`, `Id`, `Path`, `Token`, `ExpiresAt` 등. +2. **공통 봉투(envelope) 통일**: 모든 응답은 최상위에 `Success`(bool)를 갖는다. 실패 시 `Error`(string). 데이터는 `Data` 또는 의미있는 명사형 PascalCase 키. +3. **요청 바디**는 `PropertyNameCaseInsensitive = true` 덕에 케이싱 무관하나, **신규/수정 시 JS도 PascalCase 키로 전송**해 일관성 유지(`{ Sql, Limit }`). 단 외부 스펙 키는 예외. +4. **응답 DTO 클래스**의 내부용 `[JsonPropertyName("snake")]`는 제거(자연 PascalCase 직렬화). **외부 계약용 `[JsonPropertyName]`은 유지** (McpClient, Ollama/vLLM payload, ExperionDtos 등 — §2.2). +5. **폴백 금지**: 통일 완료 후 `?? d.success` 같은 이중 케이싱 폴백은 모두 제거(한 가지 진실). + +--- + +## 5. 실행 계획 (단계별) + +> 원칙: **엔드포인트 단위로 백엔드+프론트를 짝지어** 변경하고 즉시 검증. 빅뱅 일괄 변경 금지(무음 버그 재발 위험). + +### Phase 0 — 즉시 핫픽스 (이미 진행 중, 우선 머지) + +- [x] `llmchat.js` `/tools` 케이싱 폴백 +- [x] `McpToolDto.InputSchema` 추가 +- [ ] **`TextToSqlController.parse`(L38–50) 응답을 PascalCase로** (`success/sql/error` → `Success/Sql/Error`). t2s.js는 이미 `parseRes.Sql` 기대 → 이 한 줄이 t2s 파싱 버튼·채팅 흐름 2곳을 복구. + +### Phase 1 — 컨트롤러별 백엔드 통일 + +각 컨트롤러에서 익명객체 소문자 키 → PascalCase. 작은 것부터: + +- [ ] `HypertableController.cs` (1) +- [ ] `PidGraphController.cs` (2) +- [ ] `FastController.cs` (5) +- [ ] `SetupController.cs` (7) +- [ ] `KbAuthController.cs` (9) — `token`, `expiresAt` 포함 +- [ ] `Hc900Controllers.cs` (10) +- [ ] `TrendController.cs` (13) +- [ ] `DocsController.cs` (13) +- [ ] `SteamAdvisorController.cs` (15) +- [ ] `TextToSqlController.cs` (19) — 잔여 `parse` 외 전수 점검 +- [ ] `OllamaController.cs` (22) — ⚠️ **vLLM/Ollama payload·SSE shape는 제외**, `config/ping/models`의 내부 응답만 +- [ ] `PointBuilderController.cs` (29) +- [ ] `PidController.cs` (32) +- [ ] `KbController.cs` (33) +- [ ] `FeedforwardController.cs` (44) + +### Phase 2 — 프론트엔드 통일 (대응 JS) + +각 컨트롤러 변경 직후 짝 JS를 함께 수정·검증(§3.2 매핑 순서대로). 폴백 제거. + +### Phase 3 — DTO 정리 + +- [ ] `[JsonPropertyName]` 71곳 전수 검토 → 내부용 제거, 외부용 유지(주석으로 "// 외부 계약" 명시). +- [ ] 응답 DTO들이 PascalCase로 직렬화되는지 확인. + +### Phase 4 — 회귀 검증 (탭별) + +§7 체크리스트 전 탭 수행. + +--- + +## 6. 작업자 가이드 — 탐색/치환 도구 + +### 6.1 소문자 응답 잔존 탐지 (백엔드) + +```bash +# 익명객체에서 소문자로 시작하는 필드 = 변경 후보 +grep -rnE "new \{[^}]*\b[a-z][a-zA-Z]* =" src/Hc900Crawler/Controllers/ + +# 단, 외부 payload 키(model, messages, stream, tools 등)는 제외 — 수동 판별 +``` + +### 6.2 소문자 응답 읽기 탐지 (프론트) + +```bash +# d.success 류 소문자 응답 접근 (data 행의 snake_case 컬럼 r[col]은 제외) +grep -rnE "\.(success|error|data|sql|tools|rows|columns|count|tagNames|message|token|expiresAt|items|path|reply|summary|suggestions)\b" \ + src/Hc900Crawler/wwwroot/js/ | grep -v xlsx.full.min.js +``` + +### 6.3 변경 금지 식별 (이 키들이 보이면 손대지 말 것) + +``` +model messages role content stream tools tool_choice tool_calls function +arguments finish_reason choices delta max_tokens temperature ← vLLM/OpenAI +name capabilities done response system ← Ollama +jsonrpc method params inputSchema ← MCP +base_tag tag_name event_type recorded_at livevalue pv sp op ← DB/도구 데이터 +``` + +--- + +## 7. 검증 체크리스트 (탭별 회귀 — 완료 기준) + +각 항목: 실제 UI에서 동작 + 브라우저 콘솔에 `undefined` 접근/에러 없음. + +- [ ] **LLM 채팅**: 도구 사용+에이전트 모드 ON → `generate_status_report(area="P6-1", hours=24)` 류 질문 시 **도구 카드가 실행**되고 자연어 답변(텍스트로 함수명 노출 ❌) +- [ ] **LLM 채팅**: 모델 목록 로드, 핑, 설정 저장 +- [ ] **Text-to-SQL**: NL 질의(`query-nl`), **SQL 변환(`parse`) 버튼**, MCP 실행(`execute-mcp`), 분석(`analyze`), 도구 칩(`tools`) +- [ ] **이력(hist)**: 간격 조회 결과 테이블 렌더 (폴백 제거 후에도 정상) +- [ ] **트렌드**: 집계/원시 경로 차트 + 이벤트/리밋/런밴드 레이어 +- [ ] **스팀 어드바이저**: 어드바이스 로드/적용 +- [ ] **P&ID / P&ID 뷰어**: 도면 목록·그래프 +- [ ] **피드포워드**: 어드바이저 상태/SP 쓰기(WriteGuard 메시지 포함) +- [ ] **문서**: 목록/열람/업로드/이동/삭제(관리자) +- [ ] **Point Builder**: 태그 추가/제거/미리보기 +- [ ] **Fast**: 고속 조회 +- [ ] **KB 관리**: 로그인(`token`/`expiresAt`), 검색, 업로드, 비밀번호 변경 +- [ ] **설정**: 저장/연결 테스트 +- [ ] **쓰기(write)/이벤트(evt)**: 값 쓰기, 이벤트 목록 + +### 빌드/정적 점검 + +```bash +cd src/Hc900Crawler && dotnet build # 0 Error +# §6.1/§6.2 grep 결과가 외부 키만 남았는지 확인 (내부 소문자 0건) +``` + +--- + +## 8. 리스크 및 주의사항 + +1. **무음 실패가 본질**: 케이싱 불일치는 컴파일·런타임 에러 없이 `undefined`로 흐른다. **반드시 실제 UI 클릭 검증**. 빌드 통과 ≠ 동작. +2. **OllamaController 혼재**: 한 파일 안에 (a) vLLM/Ollama로 나가는 외부 payload, (b) SSE 스트리밍 shape, (c) 우리 내부 응답(`config`,`ping`,`models`,`vllmModel`)이 섞여 있다. **(c)만** 변경. (a)(b)는 §2.2. +3. **MCP 도구 결과 테이블**: `llmRenderTable`/`t2sRenderTable`은 `r[col]`로 snake_case 컬럼을 동적 렌더 → **건드리지 말 것**. server.py·SQL 동반 변경은 별도 작업. +4. **폴백의 함정**: 전환 중간 상태에서 `?? d.success` 폴백을 남기면 버그가 숨는다. Phase 완료 시 폴백 **제거**가 완료 기준. +5. **요청 바디**: `PropertyNameCaseInsensitive=true`라 깨지지 않지만, 일관성 위해 JS 송신 키도 PascalCase로(외부 스펙 제외). +6. **부분 머지 금지**: 컨트롤러만 PascalCase로 머지하고 JS를 안 바꾸면 그 탭이 죽는다. **백엔드+프론트 짝 커밋**. + +--- + +## 9. 산출물 / 완료 정의 (DoD) + +- [ ] 내부 REST 응답·요청 필드가 전부 PascalCase, 이중 케이싱 폴백 0건 +- [ ] 외부 계약(vLLM/Ollama/MCP)·DB 컬럼·SSE shape는 불변 +- [ ] `dotnet build` 0 Error +- [ ] §7 전 탭 UI 회귀 통과(콘솔 에러 0) +- [ ] §6.1/§6.2 grep에 내부 소문자 잔존 0건(외부 키만) +- [ ] 변경은 엔드포인트 단위 백엔드+프론트 짝으로 커밋 + +--- + +*근거 조사: `Program.cs`(JSON 정책), `Controllers/*.cs` 15개, `wwwroot/js/*.js` 17개, `McpClient.cs`/`McpService.cs`, 커밋 `dbad4a5`. 본 문서는 단일 작업 기준서이며, 완료 후 삭제 대상(`chore` 정리).*