# ExperionCrawler — 작업 이력

## 작업 규칙
- 복잡한 작업은 항상 todo 목록 먼저 생성
- 각 단계 시작 전 todo 목록 확인
- 단계 완료 후 즉시 completed 표시

## 완료된 작업

### 운전판정 고도화 — realtime writer stall 수정 + 교차검증(corroboration) + 단위/레인지 (2026-05-24)

#### 배경
"6-1차 펌프 4대 vs 5대" 질문에서 출발 → ① v_plant_running_state가 `p-%`만 집계해 진공펌프(`vp-`) 누락, ② 펌프 enum(RUN)만으론 허위 운전(deadhead·센서이상·frozen) 미검출, ③ 진단 중 realtime_table이 09:58 KST에 **silent stall**(수집 멈춤)된 운영 장애 발견.

#### 구현 내역

| # | 항목 | 핵심 |
|---|------|------|
| 1 | 진공펌프 포함 | `v_plant_running_state` 필터 `p-%` → `(p-% OR vp-%)`. VP-6117 등 진공펌프도 운전 집계 |
| 2 | **realtime writer stall 수정** | `ExperionRealtimeService`를 단일 `SuperviseAsync` supervisor로 통일: 부팅 비블로킹(`StartAsync(ct)` 즉시 반환), `GetLinkFault()`가 `Subscription.PublishingStopped`/`Session.KeepAliveStopped`/`Connected`로 silent stall 감지 → 30초 주기 무한 재연결(3회-후-포기 제거), flush 루프 1회만 기동. `RealtimeServiceStatus`에 `LastDataAgeSeconds`/`Stalled` 추가, History는 Stalled 시 스냅샷 skip |
| 3 | 교차검증 뷰 | `pump_corroboration_manual`(수동 매핑) + `v_pump_signal_map`(토폴로지 `FT.from_tag=펌프`→FICQ 1:N + 수동) + `v_plant_running_state_corroborated`(신선도 게이트 120s + STALE + 유량·진공 임계) + `v_plant_running_state_agg`(CONFIRMED 기준 RUNNING, suspicious/stale 부가 카운트) |
| 4 | 단위/레인지 메타데이터 | 별도 테이블 없이 `tag_metadata` 재사용 — `MetadataLoaderService.MetaAttributes`에 `euhi/eulo/units` 추가(메타갱신 트리거 자동 편승). 타입 접근 `v_instrument_range` 뷰 |
| 5 | 유량 임계 보정 | FS 5%가 과대사이징 계기(FS 2000, 운전 ~11~57)엔 부적합(정상→SUSPICIOUS 오판) 발견 → `GREATEST(1.0, LEAST(eu_hi*0.05, 5.0))`로 [1~5 kg/hr] deadhead 밴드 캡 |
| 6 | MCP 연동 | `generate_status_report`에 `v_plant_running_state_agg` 조회 추가(응답 `pump_corroboration` + LLM 프롬프트). 기존 `sample`/`focus` NameError 버그도 수정 |

#### 수정 파일

| 파일 | 변경 |
|------|------|
| `src/Infrastructure/OpcUa/ExperionRealtimeService.cs` | supervisor 재설계(비블로킹·워치독·무한재시도·flush 단일화) |
| `src/Infrastructure/OpcUa/ExperionHistoryService.cs` | Stalled 시 스냅샷 skip |
| `src/Infrastructure/OpcUa/MetadataLoaderService.cs` | MetaAttributes에 euhi/eulo/units |
| `src/Core/Application/Interfaces/IExperionServices.cs` | RealtimeServiceStatus에 LastDataAgeSeconds·Stalled |
| `src/Infrastructure/Database/ExperionDbContext.cs` | v_plant_running_state(vp- 포함) + 교차검증 4객체 + v_instrument_range (boot DDL) |
| `mcp-server/server.py` | generate_status_report agg 연동 + 버그수정 |
| `prompts/plant_context.md` | 펌프 prefix(p-/vp-) + 교차검증 뷰 사용법 |
| `plans/운전판정-고도화-플랜.md` | §0 감리 진단 결과(초안 정정·구현현황) |

#### 설계 결정

| 항목 | 결정 |
|------|------|
| stall 진단 신호 | realtime.timestamp frozen + history(realtime 복사본)는 신선·값 frozen → 수집기 정지. UTC/KST 무관 |
| 생존 판정 | `_session.Connected` 단독 → SDK `PublishingStopped`/`KeepAliveStopped` 추가(silent stall 감지) |
| 단위/레인지 저장 | 별도 테이블 X — `euhi/eulo/units`가 OPC 자식노드라 `tag_metadata` EAV에 적합. 트리거 배선 0 |
| 진공 의미 | pica-6111 = mmHg(≈torr), 0~760(760=대기압). 깊은진공=저압 → `< 300` CONFIRMED |
| 유량 임계 | %FS 부적합(계기 과대사이징) → deadhead 절대 밴드 캡 |
| MCP 재적용 | MCP 서버 재시작 필요(미반영) |

#### 검증 (라이브 :5000, Web 재기동 2회)
- `dotnet build` 경고 0/에러 0, `py_compile` OK
- 재기동 후 supervisor 자동재개(929 포인트), realtime stall 복구(lag 1:13→<2s, fresh 0→929)
- 메타갱신 2946건 적재(euhi/eulo/units), `v_instrument_range` 채워짐(ficq-6113 0~2000 kg/hr, pica-6111 0~760 mmHg)
- P6 운전 5대(진공 vp-6117 포함) 전부 **CONFIRMED_RUNNING**(유량 43 kg/hr, 진공 43 mmHg), agg confirmed=5/suspicious=0

#### 잔여
- MCP 서버 재시작해야 `generate_status_report` 변경 반영
- 신버전 Web 앱은 백그라운드 `dotnet run`으로 기동 중 — 영속화는 운전원 터미널/`deploy.sh`(systemd) 권장
- 진공 임계(300)·유량 deadhead 밴드는 운전 데이터로 추가 튜닝 여지
- `active_alarms`에 SUSPICIOUS 주입은 운전원 검증 후(보류)

### 문서 탐색기 (Tab 16) 구현 (2026-05-24)

#### 배경
프로젝트 폴더의 문서를 Web UI에서 직접 보고 관리. 트리는 **프로젝트 폴더 전체**를 탐색,
뷰어는 **txt · md · pdf** 3종(그 외 다운로드), **보기 + 관리 + 인라인 편집** 범위.
사용자 결정: PDF=원본 그대로(iframe), Excel=제외, md 렌더=marked+코드강조+KaTeX+mermaid,
뷰어 바탕=흰색.

#### 구현 내역

| # | 항목 | 핵심 |
|---|------|------|
| 1 | 안전 파일트리 | 루트 자동탐색(.git/*.sln) → 상대경로만 취급, `Path.GetFullPath` 정규화 후 루트이탈·심볼릭이탈 차단. 제외 디렉토리(.git/bin/obj/node_modules/storage…)·민감파일(*.pfx/.env/appsettings*.json…) 숨김+차단 |
| 2 | 뷰어 디스패치 | `md`=marked→DOMPurify→hljs→KaTeX→mermaid / `pdf`=원본 iframe(`Content-Type: application/pdf`) / `txt`=pre / 그 외=다운로드. md 라이브러리는 첫 진입 시 지연로딩 |
| 3 | 인라인 편집 | admin이면 `✎ 편집` → textarea(md는 실시간 분할 미리보기) → `PUT /api/docs/text` 저장 |
| 4 | 관리 | 새 폴더·업로드·이름변경·재귀삭제 — KB admin 토큰(X-Kb-Token) 재사용 |
| 5 | UI | 다크 트리(지연확장·필터·노드별 hover 액션) + **흰색 종이 뷰어**, 상대링크 클릭 시 탐색기 내 이동, 토스트 |

#### 수정 파일

| 파일 | 변경 요약 |
|------|----------|
| `src/Infrastructure/Docs/DocBrowserService.cs` (신규) | 루트결정·`SafeResolve`(이탈/심볼릭/제외/민감 가드)·List/ReadText(바이너리·BOM·크기상한)/OpenRaw(MIME)/WriteText/Rename/Delete/MakeDir/SaveUploadAsync |
| `src/Web/Controllers/DocsController.cs` (신규) | `/api/docs` — config·tree·text·raw(공개) / PUT text·rename·mkdir·upload·DELETE(admin, `IKbAuthService.ValidateAsync`) |
| `src/Web/Program.cs` | `DocBrowserService` 싱글톤 등록 |
| `src/Web/appsettings.json` | `DocBrowser` 섹션(Root="", MaxTextBytes=2MB, MaxUploadBytes=50MB) |
| `src/Web/wwwroot/index.html` | nav-item(16 문서 탐색기), `#pane-docs`(트리+뷰어), docs.css 링크, docs.js 스크립트 |
| `src/Web/wwwroot/js/app.js` | 탭 전환부 `if (tab === 'docs') docsInit()` |
| `src/Web/wwwroot/js/docs.js` (신규) | 트리(지연확장·필터·관리액션 위임), 뷰어 디스패치, md 렌더 파이프라인, 지연 라이브러리 로더, 인라인 에디터, 관리(mkdir/upload/rename/delete), KB 토큰 재사용 잠금해제, 토스트 |
| `src/Web/wwwroot/css/docs.css` (신규) | 다크 트리 + 흰색 종이 뷰어, GitHub-라이트 마크다운 타이포, 편집 분할 |
| `src/Web/wwwroot/lib/` (신규) | marked@12, dompurify@3.1.6, highlight.js@11.10(+github 테마), katex@0.16.11(js/css/auto-render + woff2 20종), mermaid@10.9.1 — 전부 로컬 번들 |

#### 설계 결정

| 항목 | 결정 |
|------|------|
| 적용 환경 | **소스 트리가 있는 개발 환경 전용** (배포본 `/opt/ExperionCrawler`엔 소스 없음). 루트는 `DocBrowser:Root`로 재설정 가능 |
| 인증 | 조회=공개, 변경계열=KB admin 토큰 재사용(별도 비번 없음). 프론트는 `sessionStorage.kbToken` 공유 |
| md 라이브러리 로딩 | 페이지 로드 시점이 아닌 **md 첫 열람 시 지연로딩**(mermaid 3.3MB 등 무게 회피) |
| XSS | marked 출력은 DOMPurify 살균. pdf/원본은 iframe·MIME 한정 |
| download 파라미터 | ASP.NET bool 바인딩은 `true/false`만 허용 — 프론트 `download=true` 사용(초기 `download=1`은 400, 수정함) |
| 뷰어 대상 | 우선 txt/md/pdf만. `DOCS_TEXT_EXT`/`DOCS_MD_EXT` 상수로 추후 확장 용이 |

#### 빌드/검증 (라이브 :5000)
- `dotnet build` 경고 0 / 에러 0, `node -c docs.js/app.js` OK, 정적자산 11종 200 서빙
- 루트 자동탐색 → 프로젝트 루트, 트리 `.git/bin/obj` 제외, 경로이탈·민감파일(appsettings/.env)·`.git` 진입 차단
- md/txt 읽기·하위트리·PDF inline(`application/pdf`)·다운로드 첨부 정상
- 미인증 PUT/DELETE → 401, admin 로그인 후 mkdir/쓰기/읽기/이름변경/재귀삭제 정상, `.env` 쓰기 차단, 정리 확인

#### 잔여
- 브라우저 실물 렌더(트리 클릭·md/mermaid/KaTeX·편집 UI)는 미확인 — 사용자 `Ctrl+F5` 후 탭 진입으로 확인 필요
- 검증 위해 기존 `dotnet run`(터미널)을 백그라운드 새 빌드로 교체 기동

### Phase 7 + Phase 5 후순위 일괄 구현 (2026-05-14)

#### 배경
`plans/빅피클-잔여작업-코딩계획.md`의 1~6번 항목을 일괄 구현. Phase 7 옵션 4종 + Phase 5
후순위 2종으로 채팅 UX·관리 편의·운영자 분석 능력을 강화.

#### 구현 내역

| # | 항목 | 핵심 |
|---|------|------|
| 1 | 툴 카드 영구 보존 | `sess.messages[*].toolCalls[]` 저장 → `llmRenderMessages`에서 재렌더링. F5 새로고침 후에도 툴 카드 유지 |
| 2 | KB 청크 미리보기 UI | Qdrant Scroll API → `/api/kb/documents/{id}/chunks` → 모달에 청크 카드 (접기/펼침) |
| 3 | 시계열 미니 스파클라인 | `llmDetectTimeSeries` (timestamp+value 키 자동 감지) → uPlot 90px 차트가 표 위에 자동 렌더링 |
| 4 | NL2SQL 의도 라우터 | `_classify_intent` 정규식 6규칙 → `query_with_nl` 진입 시 알람/요약/태그검색/이벤트로 위임. `classify_intent` MCP 도구로도 노출 |
| 5 | 대화 요약 | `sess.summary` + `summarizedUpTo` 인덱스, `LLM_MAX_HISTORY=20` 초과 시 `/api/ollama/summarize` 호출 → systemPrompt에 누적 요약 prepend |
| 6 | 에이전트 모드 | `#llm-agent-mode` 토글 → `AgentMode` 요청 필드 → `ComposeSystemPrompt`에 `AgentModeGuideKo` (ReAct 사이클) 주입 |

#### 수정 파일

| 파일 | 변경 요약 |
|------|----------|
| `src/Web/wwwroot/js/app.js` | `llmRenderToolCardsHtml`(영구 렌더), `llmDetectTimeSeries`+`llmBuildSparklineHtml`+`llmMountSparkline`(uPlot), `kbShowChunks`+`kbRenderChunks`(모달), `LLM_MAX_HISTORY`+`llmEnsureSummary`+`sess.summary` 표시, `llmAgentMode`+`llmToggleAgentMode`, 툴 카드 표시 + 요약 prepend 로직 |
| `src/Web/wwwroot/index.html` | `#kb-chunk-modal` 모달, `#llm-agent-row`/`#llm-agent-mode` 토글 |
| `src/Web/wwwroot/css/style.css` | `.kb-chunk-*`(청크 카드/뱃지/locator), `.llm-sparkline-box/-chart`, `.llm-summary-card`(접기/펼침) |
| `src/Web/Controllers/OllamaController.cs` | `AgentMode` request 필드, `ComposeSystemPrompt(agentMode)`, `AgentModeGuideKo` ReAct 가이드, `POST /api/ollama/summarize` 엔드포인트, `OllamaSummarizeRequest` DTO, `ToolGuideKo`에 `classify_intent` 추가 |
| `src/Web/Controllers/KbController.cs` | `GET /api/kb/documents/{id}/chunks` (admin) — KbQdrantClient Scroll 호출 |
| `src/Infrastructure/Kb/KbQdrantClient.cs` | `GetChunksByDocIdAsync(collection, docId, limit)` — payload-only Scroll API |
| `mcp-server/server.py` | `_CLASSIFY_RULES`+`_classify_intent`+`@mcp.tool() classify_intent`, `query_with_nl` 진입부에서 라우팅 후 5개 전용 도구로 위임 (실패 시 SQL fallback) |

#### 의도 라우터 규칙

| 정규식 | 라우팅 대상 |
|--------|------------|
| `활성.*알람\|현재.*알람\|지금.*알람\|active.*alarm` | `active_alarms` |
| `트립\|trip` | `active_alarms` |
| `상태\s*보고서\|교대.*보고\|status.*report\|운전.*보고` | `generate_status_report` |
| `요약\|보고서\|리포트\|summary\|summarize\|report` | `summarize_events` |
| `태그.*찾\|tag.*찾\|찾아\s*줘\|find.*tag\|어떤.*태그` | `find_tags` |
| `이벤트.*조회\|이벤트.*목록\|event.*list\|event.*query\|로그.*조회` | `query_events` |
| (그 외) | `query_with_nl` (기본 SQL 경로) |

스모크 테스트 10건 모두 통과 — "지금 알람 알려줘"→active_alarms / "FIC-6113.PV 값 보여줘"→query_with_nl / "안녕"→query_with_nl 등.

#### 설계 결정

| 항목 | 결정 |
|------|------|
| 툴 카드 영구화 | `assistantMsg.toolCalls`에 `{id,name,args,ok,preview,length,payload}` 누적 저장. 기존 세션에 `toolCalls` 없으면 렌더링 생략(역호환) |
| AbortError 시 | `content` 또는 `toolCalls`가 비어있지 않으면 메시지 유지 (도구만 호출하고 중단된 경우도 보존) |
| 시계열 감지 | `timestamp/recorded_at/ts/time/datetime` + `value/pv/val/fieldvalue/sp/op` 페어. 3건 이상이어야 차트 생성. uPlot 미로딩 시 렌더 생략 |
| 스파클라인 부착 | innerHTML로 컨테이너만 만든 후 `requestAnimationFrame`에서 uPlot 생성 (DOM 마운트 후 실행 보장) |
| 요약 임계 | `LLM_MAX_HISTORY=20` 초과 시 오래된 절반을 요약. 누적 요약은 이전 요약을 system 메시지로 함께 전송 |
| 요약 송신 | sess.summary가 있으면 systemPrompt 맨 앞에 `[이전 대화 요약]` 블록 prepend. 사용자에게는 접이식 카드로 표시 |
| 의도 라우터 fallback | 라우팅 시도 실패(예외) 시 조용히 SQL 경로로 fallback |
| 에이전트 모드 조건부 표시 | `llmType==='vllm' && llmUseTools` 일 때만 토글 노출. localStorage 영속 |
| 청크 미리보기 권한 | admin 토큰 필요. payload만 조회(vector 제외), 최대 500개/문서 |

#### 빌드/검증
- `dotnet build src/Web/ExperionCrawler.csproj` — 경고 0건, **에러 0건**
- `python3 -m py_compile mcp-server/server.py mcp-server/worker/nl2sql_worker.py` — OK
- `python3 -c "import server"` — 9개 도구 모두 attribute로 노출 확인
- `node -c src/Web/wwwroot/js/app.js` — syntax OK
- 의도 분류기 10/10 통과

#### 런타임 셋업
- `mcp-server` 재시작 — `classify_intent` 신규 도구 인식
- 브라우저 캐시 무효화 (Ctrl+F5) — 신규 JS/CSS 적용
- 사용자 첫 진입 시 에이전트 모드 토글은 OFF (옵트인)

#### 잔여
- **결정 보류**: 현장 재고 데이터 출처 (KB 엑셀 업로드로 즉시 가능, 별도 개발 불필요), 임베딩 모델 BGE-M3 마이그레이션 (위험 대비 임계값 평가 필요)
- 모두 코드 작업 아닌 분석/결정 항목
