chore: .gitignore에 Python 캐시 및 가상환경 무시 규칙 추가

This commit is contained in:
windpacer
2026-05-03 03:58:23 +09:00
parent f71ec310e4
commit a0404b1fee
254 changed files with 33 additions and 5083599 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,53 +0,0 @@
# 클로드 코드 검수 가이드
> GLM-4.7-Flash 수정 완료 후 Claude Code에서 실행
---
## Step 1 — 변경 범위 파악
```bash
git log --oneline # GLM이 남긴 fix 커밋 목록 확인
git diff main HEAD # 전체 변경사항 한눈에 보기
git diff main HEAD --stat # 파일별 변경 라인 수
```
## Step 2 — REVIEW_REQUEST.md 읽기
GLM이 작성한 `REVIEW_REQUEST.md`를 열어
- 수정 완료 목록과 우려사항 확인
- 수정 보류(needs-review) 항목 특별 주의
## Step 3 — 파일별 diff 검토
```bash
git show HEAD~N:src/파일.cs # 수정 전 원본
git diff HEAD~1 -- src/파일.cs # 해당 커밋 단일 변경
```
Claude Code에게 물어볼 것:
- "이 변경이 기존 동작을 바꾸는가?"
- "edge case가 있는가?"
- "OPC UA 프로토콜 관점에서 올바른가?"
## Step 4 — 빌드 및 동작 확인
```bash
dotnet build src/Web/ExperionCrawler.csproj
dotnet test ExperionCrawler.Tests/ --no-build 2>/dev/null || echo "테스트 없음"
```
## Step 5 — 판정
| 결과 | 처리 |
|------|------|
| 승인 | `git checkout main && git merge --no-ff fix/glm-review` |
| 부분 승인 | 문제 커밋만 `git revert`, 나머지 병합 |
| 거부 | `git reset --hard HEAD~N` 후 Claude Code가 직접 재수정 |
## needs-review 항목 처리
GLM이 판단 보류한 항목은 Claude Code가 직접 검토 후:
```
rag_query("해당 문제 설명", search_code=True) # MCP로 관련 코드 컨텍스트 확인
```
판단 후 직접 수정하거나 issues.md에 wont-fix 표시

View File

@@ -1,169 +0,0 @@
# ExperionCrawler 코드 분석 및 수정 태스크
> Roo Code(GLM-4.7-Flash 모드)에 이 파일 내용을 그대로 붙여넣어 실행
---
## 지시사항
당신은 ExperionCrawler (.NET 8 C#, PostgreSQL/TimescaleDB, OPC UA) 프로젝트의
코드 품질 담당 엔지니어입니다.
아래 Phase 순서대로 작업하고, 각 단계 완료 시 `task_state.md`에 기록하세요.
---
## Phase 1 — 분석: issues.md 생성
### 1-1. 분석 대상 파일 (우선순위 순)
**[HIGH 우선순위]**
- `src/Infrastructure/Database/ExperionDbContext.cs`
- `src/Infrastructure/OpcUa/ExperionRealtimeService.cs`
- `src/Core/Application/Services/TextToSqlService.cs`
- `src/Infrastructure/OpcUa/ExperionOpcServerService.cs`
- `src/Infrastructure/OpcUa/ExperionOpcServerNodeManager.cs`
**[MED 우선순위]**
- `src/Web/Controllers/ExperionControllers.cs`
- `src/Web/Controllers/TextToSqlController.cs`
- `src/Core/Application/Services/SqlValidator.cs`
- `src/Core/Application/Services/KoreanTimeRangeExtractor.cs`
- `src/Infrastructure/OpcUa/ExperionOpcClient.cs`
**[LOW 우선순위]**
- `src/Core/Application/Interfaces/IExperionServices.cs`
- `src/Core/Application/DTOs/ExperionDtos.cs`
- `src/Core/Application/DTOs/TextToSqlDtos.cs`
- `src/Web/Program.cs`
- `src/Infrastructure/OpcUa/ExperionHistoryService.cs`
### 1-2. 각 파일에서 확인할 항목
```
□ null 참조 예외 가능성 (NullReferenceException)
□ async/await 오용 (deadlock, fire-and-forget 미처리)
□ IDisposable 미해제 (DbContext, HttpClient, Connection 등)
□ 예외 삼킴 (catch(Exception){} 빈 블록)
□ CancellationToken 미전파
□ SQL Injection 가능성 (raw string interpolation)
□ 경쟁 조건 (Race condition) — 특히 ConcurrentDictionary, lock 누락
□ 불필요한 await (Task.Result, .Wait() 블로킹)
□ 메모리 누수 (이벤트 핸들러 미구독 해제)
□ 하드코딩된 값 (IP, 포트, 문자열 상수)
□ 도메인 로직 오류 (KST/UTC 변환, OPC UA 상태 코드 처리)
```
### 1-3. MCP 도구 활용
각 파일 분석 시 다음을 활용하세요:
```
search_codebase("파일명 또는 핵심 패턴") → 관련 구현 컨텍스트 확인
ask_iiot_llm("OPC UA 관련 판단이 필요한 경우") → 도메인 전문 판단
```
### 1-4. 결과물
`issues.md` 파일을 프로젝트 루트에 생성하세요:
```markdown
# ExperionCrawler 코드 이슈 목록
> 생성일: YYYY-MM-DD | 분석 모델: GLM-4.7-Flash
## 요약
- HIGH: N건 / MED: N건 / LOW: N건
## 이슈 목록
| # | 파일 | 라인 | 심각도 | 분류 | 문제 설명 | 수정 방향 | 상태 |
|---|------|------|--------|------|-----------|-----------|------|
| 1 | src/.../파일.cs | 42 | HIGH | bug | 설명 | 수정 방향 | pending |
...
```
---
## Phase 2 — 수정: HIGH → MED → LOW 순서
### 2-1. 수정 규칙
1. **한 번에 이슈 1개씩** 수정
2. 수정 전: `read_file`로 현재 내용 확인
3. 수정 후: `dotnet build src/Web/ExperionCrawler.csproj --no-restore -v q` 빌드 확인
4. 빌드 성공 시: `issues.md`에서 해당 이슈 상태를 `fixed`로 변경
5. 빌드 실패 시: 즉시 원인 분석 후 수정, 다음 이슈로 넘어가지 않음
### 2-2. 수정 불가 판단 기준
아래 경우 수정하지 말고 `issues.md``needs-review`로 표시하세요:
- 아키텍처 변경이 필요한 경우
- 비즈니스 로직 판단이 불명확한 경우
- 테스트 없이 검증 불가한 경우
### 2-3. 각 이슈 수정 후 커밋
```bash
git add [수정된 파일]
git commit -m "fix(#N): [이슈 요약]"
```
---
## Phase 3 — 검수 요청서 작성
모든 수정 완료 후 `REVIEW_REQUEST.md`를 생성하세요:
```markdown
# 클로드 코드 검수 요청
## 작업 요약
- 분석 파일: N개
- 발견 이슈: HIGH N / MED N / LOW N
- 수정 완료: N건
- 검수 필요(needs-review): N건
## 검수 항목
### ✅ 수정 완료 (확인 요청)
| # | 파일:라인 | 수정 내용 | 우려사항 |
|---|-----------|-----------|---------|
...
### ⚠️ 수정 보류 (판단 요청)
| # | 파일:라인 | 문제 | 보류 이유 |
|---|-----------|------|-----------|
...
## 빌드 상태
- 최종 빌드: ✅ 성공 / ❌ 실패
- 경고: N건
## 검수 방법
\`\`\`bash
git log --oneline # 수정 커밋 목록
git diff HEAD~N # 전체 변경사항
\`\`\`
```
---
## Phase 4 — task_state.md 최신화
작업 중 및 완료 시 `task_state.md`를 아래 형식으로 유지하세요:
```markdown
## 작업명: ExperionCrawler 코드 분석 및 수정
## 시작시각: YYYY-MM-DD HH:MM
## 진행 상태: Phase N / 4
### Phase 1 완료 파일
- [x] ExperionDbContext.cs → 이슈 N건 발견
- [x] ExperionRealtimeService.cs → 이슈 N건 발견
- [ ] TextToSqlService.cs
### Phase 2 수정 현황
- [x] #1 (HIGH) ExperionDbContext.cs:42 → fixed
- [ ] #2 (HIGH) ExperionRealtimeService.cs:156 → in-progress
### 발견된 이슈 누적
| # | 파일 | 심각도 | 내용 |
|---|------|--------|------|
```

View File

@@ -1,52 +0,0 @@
# 이력 조회 상태 표시기 구현
## 개요
이력 조회 페이지의 '▼ 옵션 불러오기' 버튼 오른쪽에 상태 표시기를 추가하여, 태그 목록 조회 상태를 시각적으로 표시합니다.
## 구현 내용
### 1. HTML 구조 ([`index.html`](src/Web/wwwroot/index.html:503))
```html
<div class="fg">
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
<span>태그 선택 <em>(최대 8개, OR 조건)</em></span>
<button class="btn-b btn-sm" onclick="histLoad()">▼ 옵션 불러오기</button>
<span id="hist-load-status" class="hist-status">대기 중</span>
</div>
<div class="pb-name-grid">
<!-- select 8개 -->
</div>
</div>
```
### 2. CSS 스타일 ([`style.css`](src/Web/wwwroot/css/style.css:815))
- `.hist-status`: 상태 표시기 컨테이너
- `.hist-status.loading`: 조회 중 (파란색)
- `.hist-status.success`: 조회 완료 (초록색)
- `.hist-status.error`: 조회 실패 (빨간색)
- 성능 최적화: `contain: layout style`, `transform: translateZ(0)`
### 3. JavaScript 로직 ([`app.js`](src/Web/wwwroot/js/app.js:688))
- `histUpdateStatus(state, message)`: 상태 표시기 업데이트
- `histLoad()`: 태그 목록 조회 및 상태 업데이트
- 조회 시작: `⏳ 조회 중...`
- 조회 완료: `✅ 조회 완료 (개수, 초)`
- 데이터 없음: `❌ 조회 데이터 없음 (0개)`
- 오류: `❌ 조회 실패: 메시지`
## 수정된 문제들
1. 초기 상태 "준비됨" → "대기 중"으로 변경
2. 레이아웃 구조 개선 (flex 컨테이너 사용)
3. CSS 애니메이션 제거 및 성능 최적화
4. `requestAnimationFrame`으로 DOM 업데이트 지연
## 남은 문제 (추가 작업 필요)
- 태그 선택 시 페이지 hang 현상 원인 파악 필요
- `pb-name-grid`의 CSS grid 레이아웃과 select 요소 8개의 상호작용
- 브라우저 호환성 문제 가능성
- 메모리 누락 가능성
## 테스트 방법
1. 이력 조회 탭 진입 → "대기 중" 표시 확인
2. '▼ 옵션 불러오기' 클릭 → 상태 변화 확인
3. 태그 선택 시 페이지 반응성 확인

View File

@@ -1,102 +0,0 @@
# 폐쇄 네트워크 로컬 LLM 채팅 웹페이지 제작 계획
## 1. 분석 요약
### Ollama (로컬 LLM 런타임)
- **기본 API 엔드포인트**: `http://localhost:11434/api`
- **OpenAI 호환 API**: `http://localhost:11434/v1/`
- **주요 API**:
- `POST /api/chat` - 채팅 (메시지 배열 기반)
- `POST /api/generate` - 텍스트 생성 (프롬프트 기반)
- `GET /api/tags` - 로컬 모델 목록 조회
- `POST /api/pull` - 모델 다운로드 (폐쇄 네트워크에서는 사전 다운로드 필요)
- **스트리밍 응답**: NDJSON 형식 (`application/x-ndjson`), `"stream": false`로 비활성화 가능
- **모델**: 폐쇄 네트워크에서는 `ollama pull`로 사전 다운로드 필요 (예: `llama3`, `gemma3`, `qwen3` 등)
### Open WebUI (참용 웹 UI)
- **기술 스택**: Svelte (프론트엔드) + FastAPI (백엔드)
- **설치**: Docker 또는 pip (`pip install open-webui`)
- **Ollama 연결**: `OLLAMA_BASE_URL` 환경변수
- **참고**: 폐쇄 네트워크에서 바로 사용 가능한 완성된 솔루션이지만, 커스텀 웹페이지 제작을 위한 참고용으로 활용
---
## 2. 필요한 정보 요약
### 필수 조건
1. **Ollama 설치 및 실행** (폐쇄 네트워크에 사전 설치)
2. **로컬 모델 다운로드** (사전 `ollama pull <model_name>` 실행)
3. **웹 서버** (정적 파일 서빙 + API 프록시, 또는 프론트엔드에서 직접 Ollama API 호출)
### API 요청/응답 형식
```json
// 채팅 요청 (POST /api/chat)
{
"model": "llama3",
"messages": [
{"role": "user", "content": "안녕하세요"}
],
"stream": false
}
// 채팅 응답
{
"model": "llama3",
"message": {"role": "assistant", "content": "안녕하세요! 어떻게 도와드릴까요?"}
}
```
### 스트리밍 응답 처리 (선택사항)
- NDJSON 형식으로 각 줄이独立的 JSON 객체
- `done: true`로 응답 종료 신호
- `ReadableStream` + `TextDecoder`로 처리 가능
---
## 3. Todo List
### Phase 1: 환경 준비
- [ ] 1.1 폐쇄 네트워크 서버에 Ollama 설치
- [ ] 1.2 필요한 LLM 모델 사전 다운로드 (`ollama pull`)
- [ ] 1.3 Ollama 서비스 실행 및 `localhost:11434` 접근 확인
### Phase 2: 프론트엔드 기본 구조
- [ ] 2.1 HTML/CSS/JavaScript 기반 채팅 UI 스키레션 작성
- [ ] 2.2 채팅 메시지 표시 영역 (사용자/보조 구분)
- [ ] 2.3 입력 필드 및 전송 버튼 구현
- [ ] 2.4 반응형 디자인 (모바일/데스크톱)
### Phase 3: Ollama API 연동
- [ ] 3.1 `fetch()`로 Ollama `/api/chat` 엔드포인트 호출 구현
- [ ] 3.2 메시지 히스토리 관리 (배열 유지)
- [ ] 3.3 모델 선택 기능 (`/api/tags`로 모델 목록 조회)
- [ ] 3.4 로딩 상태 및 에러 처리
### Phase 4: 스트리밍 응답 (선택사항)
- [ ] 4.1 NDJSON 스트리밍 파싱 구현
- [ ] 4.2 실시간 텍스트 표시 (타이핑 효과)
- [ ] 4.3 스트리밍 중단 기능
### Phase 5: 추가 기능
- [ ] 5.1 채팅 기록 저장 (localStorage)
- [ ] 5.2 새 채팅 시작 / 채팅 초기화
- [ ] 5.3 Markdown 렌더링 (코드 블록, 수식 등)
- [ ] 5.4 시스템 프롬프트 설정 기능
### Phase 6: 배포
- [ ] 6.1 정적 파일 빌드
- [ ] 6.2 폐쇄 네트워크 서버에 배포
- [ ] 6.3 CORS 설정 (Ollama `OLLAMA_HOST` 환경변수)
- [ ] 6.4 최종 테스트
---
## 4. 기술 선택 가이드
| 옵션 | 설명 | 추천도 |
|------|------|--------|
| 순수 HTML/JS | 의존성 없음, 폐쇄 네트워크에 적합 | ⭐⭐⭐ |
| Vue/React SPA | 빌드 필요, 하지만 풍부한 생태계 | ⭐⭐ |
| Open WebUI 그대로 사용 | 별도 개발 불필요, Docker로 배포 | ⭐⭐⭐ |
**폐쇄 네트워크 권장**: 순수 HTML/CSS/JavaScript 또는 Open WebUI Docker 배포

File diff suppressed because it is too large Load Diff

View File

@@ -1,110 +0,0 @@
# Roo 작업 지시: 노드맵 대시보드 undefined 필드 수정
## 배경 및 원인
`src/Web/Program.cs` 에 다음 설정이 있음:
```csharp
opt.JsonSerializerOptions.PropertyNamingPolicy = null; // PascalCase 직렬화
```
이로 인해 C# 익명 객체 shorthand `new { x.Id, x.NodeId }` 등은 PascalCase로 직렬화됨.
프론트엔드(`app.js`)는 camelCase(`r.id`, `r.nodeId`)로 접근 → **모든 값이 `undefined`로 표시됨**.
같은 문제를 Browse 엔드포인트에서도 확인했으며, 명시적 camelCase 익명 객체로 수정 완료:
```csharp
// 수정 전
return Ok(new { success = r.Success, nodes = r.Nodes, error = r.ErrorMessage });
// 수정 후
return Ok(new {
success = r.Success,
error = r.ErrorMessage,
nodes = r.Nodes.Select(n => new {
nodeId = n.NodeId,
displayName = n.DisplayName,
nodeClass = n.NodeClass,
hasChildren = n.HasChildren
})
});
```
---
## 수정 대상 파일
**`src/Web/Controllers/ExperionControllers.cs`**
클래스: `ExperionNodeMapController`
메서드: `Query()` (약 571번째 줄)
---
## 현재 코드 (문제)
```csharp
return Ok(new
{
total = r.Total,
items = r.Items.Select(x => new
{
x.Id, x.Level, x.Class, x.Name, x.NodeId, x.DataType
})
});
```
`x.Id` → JSON `"Id"` (PascalCase) → JS `r.id` = undefined
---
## 수정 후 코드 (목표)
```csharp
return Ok(new
{
total = r.Total,
items = r.Items.Select(x => new
{
id = x.Id,
level = x.Level,
@class = x.Class,
name = x.Name,
nodeId = x.NodeId,
dataType = x.DataType
})
});
```
`@class` 는 C# 예약어 회피용이며, JSON 직렬화 시 `"class"` 로 정상 출력됨.
---
## 추가 확인 사항 (같은 패턴이 있는지 전수 검사)
`ExperionControllers.cs` 전체에서 `PropertyNamingPolicy = null` 환경에서 PascalCase로 직렬화될 수 있는 패턴을 모두 찾아 수정:
1. `new { x.PropertyName }` 형태의 shorthand 익명 객체
2. 직접 typed record/class 인스턴스를 `Ok(...)` 에 넣는 경우
단, 다음은 이미 lowercase이므로 수정 불필요:
- `new { success = ..., nodes = ... }` — 명시적 소문자 키
- `new { total = ..., names = ... }` — 명시적 소문자 키
---
## 빌드 검증
수정 후 반드시:
```bash
dotnet build src/Web/ExperionCrawler.csproj --no-restore -v q
```
- `Build succeeded` 확인
- 에러 0건 확인
---
## 클로드 코드 검수 항목
수정 완료 후 아래 내용을 검수 요청:
1. `ExperionNodeMapController.Query()` 응답 필드가 모두 camelCase인지
2. 같은 패턴(`{ x.Prop }`)이 다른 컨트롤러에도 있는지 확인 여부
3. 빌드 성공 여부
4. `@class` → JSON `"class"` 직렬화 정상 작동 여부

View File

@@ -1,416 +0,0 @@
# Text-to-SQL (PostgreSQL + TimeScaleDB) 추가 계획
## 1. 개요
ExperionCrawler 프로젝트에 **TimeScaleDB 하이퍼테이블**을 적용하여 시계열 데이터(`realtime_table`, `history_table`)의 성능과 확장성을 대폭 개선합니다. 또한 **Text-to-SQL 기능**을 추가하여 API를 통해 자연어/쿼리 입력으로 시계열 데이터를 조회·분석할 수 있도록 합니다.
---
## 2. 현재 아키텍처 분석
```mermaid
graph TB
subgraph WebLayer
Controller[Controllers]
Program[Program.cs]
end
subgraph Infrastructure
DbContext[ExperionDbContext]
DbService[ExperionDbService]
end
subgraph Core
CrawlService[ExperionCrawlService]
RealtimeSvc[ExperionRealtimeService]
HistorySvc[ExperionHistoryService]
end
subgraph Data
PostgreSQL[(PostgreSQL)]
CSV[CSV Files]
end
Controller --> DbContext
Controller --> DbService
DbContext --> PostgreSQL
RealtimeSvc --> DbService
HistorySvc --> DbService
DbService --> CSV
```
### 현재 사용 테이블
| 테이블명 | 용도 | 현재 방식 |
|----------|------|-----------|
| `realtime_table` | 실시간 모니터링 포인트 | 일반 PostgreSQL 테이블 |
| `history_table` | 시계열 이력 스냅샷 | 일반 PostgreSQL 테이블 |
| `raw_node_map` | OPC UA 노드맵 원시 데이터 | 일반 PostgreSQL 테이블 |
| `node_map_master` | 빌드된 노드맵 | 일반 PostgreSQL 테이블 |
| `experion_records` | 크롤링 기록 | EF Core DbSet |
---
## 3. 목표 아키텍처
```mermaid
graph TB
subgraph WebLayer
Controller[Controllers]
TextToSql[Text-to-SQL Service]
Program[Program.cs]
end
subgraph Infrastructure
DbContext[ExperionDbContext]
DbService[ExperionDbService]
TimeSeriesSvc[TimeSeries Service]
end
subgraph Core
CrawlService[ExperionCrawlService]
RealtimeSvc[ExperionRealtimeService]
HistorySvc[ExperionHistoryService]
end
subgraph Data
TimeScaleDB[(TimeScaleDB<br/>HyperTables)]
CSV[CSV Files]
end
Controller --> TextToSql
TextToSql --> DbService
Controller --> DbContext
Controller --> DbService
DbContext --> TimeScaleDB
RealtimeSvc --> TimeSeriesSvc
HistorySvc --> TimeSeriesSvc
TimeSeriesSvc --> DbService
DbService --> CSV
```
---
## 4. 구현 단계별 계획
### 단계 1: TimeScaleDB 패키지 추가 및 초기화
**목표**: TimeScaleDB 확장 활성화 및 하이퍼테이블 생성
#### 4.1.1 NuGet 패키지 추가
- `Npgsql.EntityFrameworkCore.PostgreSQL` (이미 존재)
- `Npgsql.TimeScaleDB` (새로 추가)
#### 4.1.2 데이터베이스 초기화 수정
- [`ExperionDbContext`](src/Infrastructure/Database/ExperionDbContext.cs:1)에 TimeScaleDB 확장 활성화 코드 추가
- `history_table``history_hypertable` (하이퍼테이블)로 마이그레이션
- `realtime_table``realtime_hypertable` (하이퍼테이블)로 마이그레이션
#### 4.1.3 마이그레이션 스크립트
```sql
-- TimeScaleDB 확장 활성화
CREATE EXTENSION IF NOT EXISTS timescaledb;
-- 기존 history_table 데이터를 백업
CREATE TABLE history_table_backup AS SELECT * FROM history_table;
-- 기존 테이블 삭제 후 하이퍼테이블 생성
DROP TABLE history_table;
SELECT create_hypertable('history_hypertable', 'recorded_at',
chunk_time_interval => INTERVAL '1 day',
create_default_indexes => true);
-- 데이터 복원
INSERT INTO history_hypertable (id, tagname, node_id, value, recorded_at)
SELECT id, tagname, node_id, value, recorded_at FROM history_table_backup;
DROP TABLE history_table_backup;
```
---
### 단계 2: Text-to-SQL 서비스 구현
**목표**: 자연어/쿼리 입력으로 시계열 데이터를 조회하는 서비스 계층 구현
#### 4.2.1 인터페이스 정의 (`ITextToSqlService`)
```
namespace ExperionCrawler.Core.Application.Interfaces;
public interface ITextToSqlService
{
/// <summary>자연어 질의를 SQL로 변환</summary>
Task<string> ParseNaturalLanguageAsync(string input);
/// <summary>SQL 쿼리 실행 및 결과 반환</summary>
Task<SqlQueryResult> ExecuteQueryAsync(string sql);
/// <summary>쿼리 제안 (자동 완성)</summary>
Task<IEnumerable<string>> SuggestQueriesAsync(string partialInput);
/// <summary>시계열 분석 (평균, 최대, 최소, 추세)</summary>
Task<TimeSeriesAnalysisResult> AnalyzeAsync(string tagName, DateTime? from, DateTime? to);
}
```
#### 4.2.2 구현체 (`TextToSqlService`)
- **자연어 파싱**: 키워드 기반 파서 (tag name, 시간 범위, 집계 함수)
- **SQL 생성**: TimeScaleDB 함수 (`time_bucket`, `avg`, `max`, `min`, `last`) 활용
- **결과 매핑**: 동적 SQL → DTO 매핑
#### 4.2.3 자연어 파싱 예시
| 입력 | 생성된 SQL |
|------|-----------|
| "PV001 온도 최근 1시간 평균" | `SELECT time_bucket('5 min', recorded_at) AS bucket, AVG(value::float) FROM history_hypertable WHERE tagname = 'PV001' AND recorded_at > NOW() - INTERVAL '1 hour' GROUP BY bucket ORDER BY bucket` |
| "전체 태그 현재 값" | `SELECT DISTINCT ON (tagname) tagname, livevalue, timestamp FROM realtime_hypertable ORDER BY tagname, timestamp DESC` |
| "PV001, PV002 최근 24시간 최대값" | `SELECT time_bucket('1 hour', recorded_at) AS bucket, tagname, MAX(value::float) FROM history_hypertable WHERE tagname IN ('PV001', 'PV002') AND recorded_at > NOW() - INTERVAL '24 hour' GROUP BY bucket, tagname ORDER BY bucket, tagname` |
---
### 단계 3: API 엔드포인트 추가
**목표**: Text-to-SQL 기능을 REST API로 노출
#### 4.3.1 컨트롤러 (`TextToSqlController`)
```
[ApiController]
[Route("api/text-to-sql")]
public class TextToSqlController : ControllerBase
{
private readonly ITextToSqlService _service;
[HttpPost("parse")]
public Task<IActionResult> Parse([FromBody] NaturalLanguageQueryDto dto);
[HttpPost("execute")]
public Task<IActionResult> Execute([FromBody] SqlQueryDto dto);
[HttpGet("suggest")]
public Task<IActionResult> Suggest([FromQuery] string input);
[HttpPost("analyze")]
public Task<IActionResult> Analyze([FromBody] AnalysisRequestDto dto);
}
```
#### 4.3.2 DTO 정의
```
public class NaturalLanguageQueryDto
{
public string Query { get; set; } = string.Empty;
public string Language { get; set; } = "ko"; // "ko" or "en"
}
public class SqlQueryDto
{
public string Sql { get; set; } = string.Empty;
public int? Limit { get; set; } = 1000;
}
public class AnalysisRequestDto
{
public List<string> TagNames { get; set; } = new();
public DateTime? From { get; set; }
public DateTime? To { get; set; }
public string Interval { get; set; } = "5 min"; // time_bucket interval
}
```
---
### 단계 4: TimeSeries 서비스 개선
**목표**: TimeScaleDB 특화 함수 활용하여 성능 최적화
#### 4.4.1 기존 `ExperionDbService` 수정
- `history_table``history_hypertable` 참조로 변경
- `realtime_table``realtime_hypertable` 참조로 변경
- TimeScaleDB 함수 (`time_bucket`, `continuous aggregates`) 활용
#### 4.4.2 연속 집계 (Continuous Aggregates) 생성
```sql
-- 5분 단위 집계 뷰 생성
CREATE MATERIALIZED VIEW history_5min_agg
WITH (timescaledb.continuous) AS
SELECT
time_bucket('5 min', recorded_at) AS bucket,
tagname,
AVG(value::float) AS avg_value,
MIN(value::float) AS min_value,
MAX(value::float) AS max_value,
FIRST(value::float, recorded_at) AS open_value,
LAST(value::float, recorded_at) AS close_value,
COUNT(*) AS point_count
FROM history_hypertable
GROUP BY bucket, tagname;
-- 기존 데이터 리프레시
REFRESH MATERIALIZED VIEW history_5min_agg;
```
---
### 단계 5: Program.cs 설정 업데이트
**목표**: 서비스 등록 및 초기화 플로우 수정
#### 4.5.1 서비스 등록
```csharp
// Text-to-SQL 서비스
builder.Services.AddScoped<ITextToSqlService, TextToSqlService>();
// TimeSeries 서비스
builder.Services.AddScoped<ITimeSeriesService, TimeSeriesService>();
```
#### 4.5.2 연결 문자열 확인
```json
{
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Port=5432;Database=experion_crawler;Username=postgres;Password=postgres"
}
}
```
---
## 5. 파일 구조 변경
```
src/
├── Core/
│ ├── Application/
│ │ ├── DTOs/
│ │ │ ├── ExperionDtos.cs (수정: Text-to-SQL DTO 추가)
│ │ │ └── TextToSqlDtos.cs (신규)
│ │ ├── Interfaces/
│ │ │ ├── IExperionServices.cs (수정: ITextToSqlService, ITimeSeriesService 추가)
│ │ │ └── ITextToSqlService.cs (신규)
│ │ └── Services/
│ │ ├── ExperionCrawlService.cs (변경 없음)
│ │ └── TextToSqlService.cs (신규)
│ └── Domain/
│ └── Entities/
│ ├── ExperionEntities.cs (수정: TimeScaleDB 엔티티 추가)
│ └── TimeSeriesEntities.cs (신규)
├── Infrastructure/
│ ├── Database/
│ │ ├── ExperionDbContext.cs (수정: 하이퍼테이블 설정)
│ │ ├── TimeSeriesService.cs (신규)
│ │ └── Migrations/ (신규: TimeScaleDB 마이그레이션)
│ └── ... (기존 파일 유지)
└── Web/
├── Controllers/
│ ├── ExperionControllers.cs (수정: Text-to-SQL 엔드포인트 추가)
│ └── TextToSqlController.cs (신규)
├── appsettings.json (수정: 연결 문자열)
└── ExperionCrawler.csproj (수정: NuGet 패키지 추가)
```
---
## 6. TimeScaleDB 특화 기능
### 6.1 체킹 간격 설정
| 테이블 | chunk_time_interval | 설명 |
|--------|-------------------|------|
| `history_hypertable` | 1일 | 이력 데이터는 1일 단위 청크 |
| `realtime_hypertable` | 1시간 | 실시간 데이터는 1시간 단위 청크 |
### 6.2 데이터 보존 정책
```sql
-- 90일 이전 데이터 자동 삭제
SELECT add_drop_chunks_policy('history_hypertable', INTERVAL '90 days');
```
### 6.3 압축 설정
```sql
-- 1일 이전 데이터 자동 압축
SELECT add_compression_policy('history_hypertable', INTERVAL '1 day');
```
---
## 7. NuGet 패키지 변경
### 추가 패키지
| 패키지 | 버전 | 용도 |
|--------|------|------|
| `Npgsql.TimeScaleDB` | 최신 | TimeScaleDB 전용 확장 |
### 기존 패키지 (변경 없음)
| 패키지 | 현재 버전 |
|--------|----------|
| `Npgsql.EntityFrameworkCore.PostgreSQL` | 8.0.11 |
| `Microsoft.EntityFrameworkCore.Design` | 8.0.13 |
---
## 8. 구현 체크리스트
- [ ] **단계 1**: TimeScaleDB 패키지 추가 및 초기화
- [ ] `Npgsql.TimeScaleDB` NuGet 패키지 추가
- [ ] `ExperionDbContext`에 TimeScaleDB 확장 코드 추가
- [ ] `history_table``history_hypertable` 마이그레이션
- [ ] `realtime_table``realtime_hypertable` 마이그레이션
- [ ] **단계 2**: Text-to-SQL 서비스 구현
- [ ] `ITextToSqlService` 인터페이스 정의
- [ ] `TextToSqlService` 구현체 작성
- [ ] 자연어 파서 (한국어/영어 지원)
- [ ] SQL 생성 로직 (TimeScaleDB 함수 활용)
- [ ] 시계열 분석 기능 (평균, 최대, 최소, 추세)
- [ ] **단계 3**: API 엔드포인트 추가
- [ ] `TextToSqlController` 생성
- [ ] `/api/text-to-sql/parse` 엔드포인트
- [ ] `/api/text-to-sql/execute` 엔드포인트
- [ ] `/api/text-to-sql/suggest` 엔드포인트
- [ ] `/api/text-to-sql/analyze` 엔드포인트
- [ ] DTO 정의
- [ ] **단계 4**: TimeSeries 서비스 개선
- [ ] `ExperionDbService` 수정 (하이퍼테이블 참조)
- [ ] 연속 집계 (Continuous Aggregates) 생성
- [ ] TimeScaleDB 함수 활용 쿼리 최적화
- [ ] **단계 5**: Program.cs 설정 업데이트
- [ ] 서비스 등록
- [ ] 연결 문자열 확인
- [ ] 초기화 플로우 수정
- [ ] **단계 6**: 테스트 및 문서화
- [ ] API 테스트 (Swagger)
- [ ] 자연어 파싱 테스트
- [ ] TimeScaleDB 성능 벤치마크
- [ ] README.md 업데이트
---
## 9. 예상 효과
| 항목 | 기존 | 개선 후 |
|------|------|--------|
| 시계열 데이터 삽입 속도 | 일반 테이블 | TimeScaleDB 청킹으로 10x 이상 향상 |
| 시간 범위 쿼리 | 전체 테이블 스캔 | 청크 제거로 빠른 응답 |
| 데이터 압축 | 없음 | 자동 압축 (5-10x 저장공간 절약) |
| 자동 보존 | 수동 삭제 | 정책 기반 자동 삭제 |
| Text-to-SQL | 없음 | 자연어 기반 시계열 쿼리 |
---
## 10. 주의사항
1. **TimeScaleDB 설치 prerequisite**: 대상 서버에 TimeScaleDB가 설치되어 있어야 함
```bash
# Ubuntu/Debian
sudo apt-get install timescaledb-postgresql-16
```
2. **기존 데이터 마이그레이션**: `history_table`과 `realtime_table`의 기존 데이터를 새 하이퍼테이블로 이동해야 함
3. **연결 문자열**: `Trust Server Certificate=true`는 TimeScaleDB에서 필요하지 않을 수 있음
4. **EF Core 제한**: EF Core는 TimeScaleDB 하이퍼테이블을 직접 지원하지 않음
- Raw SQL 쿼리 활용 필요
- `ExecuteSqlRawAsync`, `FromSqlRaw` 사용
5. **백호환성**: 기존 API 엔드포인트는 변경되지 않도록 유지