- 작업지시서-API-필드-PascalCase-통일.md: PascalCase 통일 작업 지시서. - 논의-AI운전원-제어-아이디어.md: AI 운전원 제어 아이디어 논의 메모. - ControlEdge HC900 IO Modules Specifications.pdf: RTD/AI 모듈 스펙(레인지·정확도) 데이터시트 — PT100 양자화 분석 근거 자료. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
44 KiB
구현 검증 보고서 — 작업지시서-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
// 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
[JsonPropertyName("analogmon1")] // ← 미사용 DTO. PointBuilderBuildDto 자체가 dead code
public PointBuilderGroupDto AnalogMonitor1 { get; set; } = new();
→ Hc900PointBuilderBuildDto(같은 파일 L106-113)가 실제 사용 중이므로, 위 클래스 전체 삭제 권장.
추가 발견: TrendDtos.cs — TrendMemberDto [JsonPropertyName] 3개 제거됨
// 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 자가 검증
- 각 지적 사항을 "현재 파일 몇 번 줄"로 직접 가리킬 수 있는가? — trend.js L659·L676·L698-708·L751-758 등
- HIGH 항목은 재현 가능한 시나리오를 한 문장으로 말할 수 있는가? — 트렌드 탭에서 그룹 선택 시 빈 차트, 그룹 빌더에서 모든 필드 undefined
- 교차 검증 4개 질문을 모두 통과한 항목만 포함되었는가? — 통과
- 보고서의 수정 예시가 현재 코드에 아직 적용되지 않은 내용인가? — 적용되지 않음 (현재 버그 상태임)
- 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): 근본 메커니즘 설명 + "PascalCase 통일" 결정
- 범위(§2): 변경 대상 vs 절대 변경 금지 대상을 표로 정리
- 현황 인벤토리(§3): 컨트롤러별 익명객체 소문자 응답 라인 수 + JS 파일별 주 컨트롤러 매핑
- 표준 규약(§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)
// 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)
// 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', `<span class="t2s-error">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폴백이 적용돼 있어 현재 동작.
그러나 실제 코드는 폴백이 없었다 (본 진단 작성자가 수정하기 전):
// hist.js L122-127 (수정 전) — 폴백 없음
if (!d.success) { throw new Error(d.error || '조회 실패'); }
const rows = d.rows || []; // API는 { Rows, TagNames } (PascalCase) 반환
const tNames = d.tagNames || [];
// 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 자가 검증
- 각 지적 사항을 "현재 파일 몇 번 줄"로 직접 가리킬 수 있는가? — TextToSqlController.cs:44, t2s.js:118·432·592
- HIGH 항목은 재현 가능한 시나리오를 한 문장으로 말할 수 있는가? — Text-to-SQL 탭에서 질문 입력 → "변환" 버튼 → "SQL 변환 실패" 메시지
- 교차 검증 4개 질문을 모두 통과한 항목만 포함되었는가? — §3.3 항목은 Q1(이미 수정)으로 본 항목에서 제외, 기록용으로만 기재
- 보고서의 수정 예시가 현재 코드에 아직 적용되지 않은 내용인가? — parse 수정은 Plan §5.0에 명시되어 있으나 아직 적용되지 않음
- "더 좋은 방법 제안"과 "현재 코드가 틀렸다"를 혼동하지 않았는가? — 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 직렬화 설정:
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. 표준 규약 (확정)
- 내부 REST 응답 필드명 = PascalCase.
Success,Error,Data,Message,Sql,Tools,Rows,Columns,Count,TagNames,Items,Id,Path,Token,ExpiresAt등. - 공통 봉투(envelope) 통일: 모든 응답은 최상위에
Success(bool)를 갖는다. 실패 시Error(string). 데이터는Data또는 의미있는 명사형 PascalCase 키. - 요청 바디는
PropertyNameCaseInsensitive = true덕에 케이싱 무관하나, 신규/수정 시 JS도 PascalCase 키로 전송해 일관성 유지({ Sql, Limit }). 단 외부 스펙 키는 예외. - 응답 DTO 클래스의 내부용
[JsonPropertyName("snake")]는 제거(자연 PascalCase 직렬화). 외부 계약용[JsonPropertyName]은 유지 (McpClient, Ollama/vLLM payload, ExperionDtos 등 — §2.2). - 폴백 금지: 통일 완료 후
?? d.success같은 이중 케이싱 폴백은 모두 제거(한 가지 진실).
5. 실행 계획 (단계별)
원칙: 엔드포인트 단위로 백엔드+프론트를 짝지어 변경하고 즉시 검증. 빅뱅 일괄 변경 금지(무음 버그 재발 위험).
Phase 0 — 즉시 핫픽스 (이미 진행 중, 우선 머지)
llmchat.js/tools케이싱 폴백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 소문자 응답 잔존 탐지 (백엔드)
# 익명객체에서 소문자로 시작하는 필드 = 변경 후보
grep -rnE "new \{[^}]*\b[a-z][a-zA-Z]* =" src/Hc900Crawler/Controllers/
# 단, 외부 payload 키(model, messages, stream, tools 등)는 제외 — 수동 판별
6.2 소문자 응답 읽기 탐지 (프론트)
# 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): 값 쓰기, 이벤트 목록
빌드/정적 점검
cd src/Hc900Crawler && dotnet build # 0 Error
# §6.1/§6.2 grep 결과가 외부 키만 남았는지 확인 (내부 소문자 0건)
8. 리스크 및 주의사항
- 무음 실패가 본질: 케이싱 불일치는 컴파일·런타임 에러 없이
undefined로 흐른다. 반드시 실제 UI 클릭 검증. 빌드 통과 ≠ 동작. - OllamaController 혼재: 한 파일 안에 (a) vLLM/Ollama로 나가는 외부 payload, (b) SSE 스트리밍 shape, (c) 우리 내부 응답(
config,ping,models,vllmModel)이 섞여 있다. (c)만 변경. (a)(b)는 §2.2. - MCP 도구 결과 테이블:
llmRenderTable/t2sRenderTable은r[col]로 snake_case 컬럼을 동적 렌더 → 건드리지 말 것. server.py·SQL 동반 변경은 별도 작업. - 폴백의 함정: 전환 중간 상태에서
?? d.success폴백을 남기면 버그가 숨는다. Phase 완료 시 폴백 제거가 완료 기준. - 요청 바디:
PropertyNameCaseInsensitive=true라 깨지지 않지만, 일관성 위해 JS 송신 키도 PascalCase로(외부 스펙 제외). - 부분 머지 금지: 컨트롤러만 PascalCase로 머지하고 JS를 안 바꾸면 그 탭이 죽는다. 백엔드+프론트 짝 커밋.
9. 산출물 / 완료 정의 (DoD)
- 내부 REST 응답·요청 필드가 전부 PascalCase, 이중 케이싱 폴백 0건
- 외부 계약(vLLM/Ollama/MCP)·DB 컬럼·SSE shape는 불변
dotnet build0 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 정리).