P&ID: export GetEquipmentAsync null 파라미터 추가 + 오래된 plan 문서 삭제

This commit is contained in:
windpacer
2026-05-29 09:49:36 +09:00
parent c7da3f9735
commit d8095d0c8d
2 changed files with 2 additions and 394 deletions

View File

@@ -1,392 +0,0 @@
# P&ID 추출 PREFIX 분류 — `tag_dcs` 컬럼 도입 플랜
> **작성일**: 2026-05-27
> **작성자**: BigPickle
> **목적**: `pid_prefix_rules`와 `pid_equipment` 두 테이블에 `tag_dcs BOOLEAN` 컬럼을 추가해,
> P&ID 추출 **시작 시점**부터 현장 계기(field instrument)와 DCS 태그(DCS function block)를 구별한다.
---
## 0. 배경 및 문제
### 현재 구조의 문제
현재 `pid_prefix_rules.category = 'instrument'` 아래에 두 종류가 혼재:
| 종류 | 예시 prefix | 실제 의미 |
|------|------------|---------|
| **현장 계기** (field) | FT, PT, LT, TT, FCV, PCV, PSV, XV, FG, PG | 물리적 기기, 현장 설치 |
| **DCS 함수블록** (system) | FIC, TIC, PIC, LIC, FY, TY, PY, LY | DCS/SCADA 내부 연산 블록, 물리 기기 없음 |
기존 `tag_class = 'field'/'system'` 컬럼이 이를 구별하려 했으나:
- **추출 후 후처리**에서 판정 (ISA 후속문자 분석 + Experion 연결 여부)
- **PREFIX 정의 UI**에서는 전혀 보이지 않아 운전원이 구별 불가
- LLM이 pid_equipment 조회 시 instrument를 한꺼번에 가져와 혼동
### 목표
`pid_prefix_rules` 테이블에 `tag_dcs BOOLEAN` 추가 → PREFIX 분류 정의 시점부터 DCS 여부 명시.
`pid_equipment` 테이블에도 동일 컬럼 전파 → 추출 결과 전체에 flag 유지.
---
## 1. DCS vs Field 분류 기준
### DCS 태그 (`tag_dcs = TRUE`) — DCS/Experion DB 포인트, 물리 기기 없음
| Prefix | 설명 | 비고 |
|--------|------|------|
| FIC | Flow Indicator Controller | 제어루프 함수블록 |
| TIC | Temperature Indicator Controller | |
| PIC | Pressure Indicator Controller | |
| LIC | Level Indicator Controller | |
| FY | Flow Relay/Converter/Computing | DCS 연산요소 |
| TY | Temperature Relay/Converter | |
| PY | Pressure Relay/Converter | |
| LY | Level Relay/Converter | |
| FV | Flow Valve (function block) | DCS 출력 함수블록 (주의: 물리 FCV와 구별) |
| TV | Temperature Valve (function block) | |
| PV | Pressure Valve (function block) | |
| LV | Level Valve (function block) | |
> **주의**: FCV/PCV/LCV/TCV는 물리적 제어밸브 → `tag_dcs = FALSE` (field 유지)
### 현장 계기 (`tag_dcs = FALSE`) — 물리 기기
| Prefix | 설명 |
|--------|------|
| FT, TT, PT, LT | 1차 측정 전송기 (Transmitter) |
| FG, TG, PG, LG | 게이지류 (Gauge) |
| FCV, TCV, PCV, LCV | 제어밸브 (물리 기기) |
| PSV | 안전밸브 |
| XV | 차단밸브 |
| VIP, VIT | 진동 프로브/전송기 |
| DP | 차압계 |
| BV | 볼/버터플라이 밸브 |
---
## 2. 영향 범위 전체 목록
### 2.1 데이터베이스 (4곳)
| 대상 | 변경 내용 |
|------|---------|
| `pid_prefix_rules` 테이블 | `tag_dcs BOOLEAN NOT NULL DEFAULT FALSE` 컬럼 추가 |
| `pid_prefix_rules` 시드 | DCS prefix에 `tag_dcs = TRUE` UPDATE |
| `pid_equipment` 테이블 | `tag_dcs BOOLEAN NOT NULL DEFAULT FALSE` 컬럼 추가 |
| `pid_equipment` 기존 행 | prefix rule로 backfill |
**마이그레이션 SQL**:
```sql
-- pid_prefix_rules 컬럼 추가
ALTER TABLE pid_prefix_rules ADD COLUMN IF NOT EXISTS tag_dcs BOOLEAN NOT NULL DEFAULT FALSE;
-- DCS prefix 마킹
UPDATE pid_prefix_rules
SET tag_dcs = TRUE
WHERE prefix IN ('FIC','TIC','PIC','LIC','FY','TY','PY','LY','FV','TV','PV','LV');
-- pid_equipment 컬럼 추가
ALTER TABLE pid_equipment ADD COLUMN IF NOT EXISTS tag_dcs BOOLEAN NOT NULL DEFAULT FALSE;
-- 기존 행 backfill (prefix rule 기반)
UPDATE pid_equipment pe
SET tag_dcs = pr.tag_dcs
FROM pid_prefix_rules pr
WHERE pr.prefix = pe.instrument_type
AND pr.tag_dcs = TRUE;
```
---
### 2.2 C# 도메인 엔티티 (2파일)
#### `src/Core/Domain/Entities/PidPrefixRule.cs`
```csharp
// 추가
[Column("tag_dcs")]
public bool TagDcs { get; set; } = false;
```
#### `src/Core/Domain/Entities/PidEquipment.cs`
```csharp
// 추가 (tag_class 아래)
[Column("tag_dcs")]
public bool TagDcs { get; set; } = false;
```
---
### 2.3 DTOs (1파일, 3개 record)
#### `src/Core/Application/DTOs/PidPrefixRuleDto.cs`
```csharp
// 기존
public record PidPrefixRuleDto(int Id, string Prefix, string Category, string? Description, int SortOrder, DateTime CreatedAt);
public record CreatePidPrefixRuleRequest(string Prefix, string Category, string? Description, int SortOrder = 0);
public record UpdatePidPrefixRuleRequest(string Prefix, string Category, string? Description, int SortOrder = 0);
// 수정 후 (TagDcs 추가)
public record PidPrefixRuleDto(int Id, string Prefix, string Category, bool TagDcs, string? Description, int SortOrder, DateTime CreatedAt);
public record CreatePidPrefixRuleRequest(string Prefix, string Category, bool TagDcs = false, string? Description = null, int SortOrder = 0);
public record UpdatePidPrefixRuleRequest(string Prefix, string Category, bool TagDcs = false, string? Description = null, int SortOrder = 0);
```
---
### 2.4 Application Services (1파일)
#### `src/Core/Application/Services/PidExtractorService.cs`
**변경 1**: `MatchCategoryAsync()` → prefix rule에서 `tag_dcs`도 반환
현재는 `string? category`만 반환. `(string? category, bool tagDcs)` 튜플로 변경하거나,
별도 `GetPrefixRuleByTagAsync(tagNo)` 호출로 tag_dcs 획득.
**변경 2**: `ClassifyTagClass()` 단순화
기존 로직: `hasExperionLink → system`, ISA 후속문자 분석 → `system/field`
수정 후: `tag_dcs = TRUE``TagClass = "system"` (prefix rule이 ground truth)
Experion 연결 여부는 여전히 보완 신호로 유지 가능.
**변경 3**: 추출 저장 시 `TagDcs` 채우기
```csharp
// 기존
item.Category = category;
item.TagClass = tagClass;
// 수정
item.Category = category;
item.TagDcs = tagDcs; // prefix rule에서 가져온 값
item.TagClass = tagDcs ? PidEquipment.TagClassSystem : tagClass; // 파생 또는 별도 로직
```
**변경 4**: CSV/Excel export에 `TagDcs` 열 추가
- CSV 헤더: `TagNo,...,TagClass,TagDcs`
- Excel 열 추가 (17번 열): "DCS태그" 불리언 → "DCS"/"현장" 표시
**변경 5**: Excel import에서 `tag_dcs` 처리
- Excel "DCS태그" 열 → `"DCS" → true, "현장" → false`
**변경 6**: `BackfillTagClassAsync()``BackfillTagDcsAsync()` 추가
기존 backfill 로직에서 `tag_dcs` 미지정 행도 함께 backfill.
**변경 7**: `CreatePrefixRuleAsync` / `UpdatePrefixRuleAsync`
`request.TagDcs``rule.TagDcs` 저장.
---
### 2.5 인터페이스 (1파일)
#### `src/Core/Application/Interfaces/IExperionServices.cs`
`IPidExtractorService` 인터페이스 시그니처 수정:
- `CreatePrefixRuleAsync(CreatePidPrefixRuleRequest)` — DTO 변경으로 자동 반영
- `UpdatePrefixRuleAsync(int, UpdatePidPrefixRuleRequest)` — 동일
---
### 2.6 EF Core DbContext (1파일)
#### `src/Infrastructure/Database/ExperionDbContext.cs`
**변경 1**: Boot DDL에 `ALTER TABLE` 추가
```csharp
await _ctx.Database.ExecuteSqlRawAsync(
"ALTER TABLE pid_prefix_rules ADD COLUMN IF NOT EXISTS tag_dcs BOOLEAN NOT NULL DEFAULT FALSE");
await _ctx.Database.ExecuteSqlRawAsync(
"ALTER TABLE pid_equipment ADD COLUMN IF NOT EXISTS tag_dcs BOOLEAN NOT NULL DEFAULT FALSE");
```
**변경 2**: 시드 INSERT 수정
기존 INSERT는 `ON CONFLICT DO NOTHING` → 기존 행에 반영 안 됨.
마이그레이션 UPDATE 별도 실행 필요 (§2.1 마이그레이션 SQL).
**변경 3**: EF 모델 바인딩 (필요시)
`modelBuilder.Entity<PidPrefixRule>()` 블록에 `tag_dcs` 명시 없어도 Column attribute로 자동 매핑.
---
### 2.7 Web Controllers (1파일)
#### `src/Web/Controllers/PidController.cs`
**변경 1**: `GetPrefixRules` 응답
현재 `PidPrefixRule` 엔티티를 직접 직렬화 → `TagDcs` 필드 자동 포함 (Column attribute 추가로 충분).
**변경 2**: `CreatePrefixRule` / `UpdatePrefixRule`
`request.TagDcs` 가 DTO에 추가되므로 컨트롤러 수정 불필요 (서비스에서 처리).
**변경 3**: `[JsonPropertyName("tagDcs")]` 확인
익명객체 대신 DTO 반환 시 camelCase 보장 필요. (기존 패턴 확인 후 적용)
---
### 2.8 Web UI (2파일)
#### `src/Web/wwwroot/js/pid.js`
**변경 1**: `CATEGORY_LABELS` / `CATEGORY_ORDER`
```javascript
// 기존
instrument: { label: 'Instrument', badge: 'ok' },
// 수정 — DCS는 별도 배지
// (카테고리가 'instrument'로 유지되고 tag_dcs로 구별하는 방식)
```
**변경 2**: PREFIX 그룹 렌더링 (`pidRenderPrefixGroups`)
각 prefix rule 행에 `tag_dcs` 체크박스/배지 추가:
```javascript
// 테이블 열 추가
<td><span class="badge ${r.tagDcs ? 'warn' : 'ok'}">${r.tagDcs ? 'DCS' : '현장'}</span></td>
// 편집 행에도 tag_dcs 토글 추가
<input type="checkbox" ${r.tagDcs ? 'checked' : ''} data-field="tagDcs" />
```
**변경 3**: `pidAddPrefixRule(category)` 요청 body에 `tagDcs` 추가
**변경 4**: `pidUpdatePrefixRule(id)` 요청 body에 `tagDcs` 추가
**변경 5**: 장비 목록 테이블에 `tag_dcs` 배지 추가 (선택사항)
#### `src/Web/wwwroot/panes/pid.html`
- PREFIX 분류 정의 패널에 열 헤더 "DCS태그" 추가
- 도움말 텍스트 갱신
---
### 2.9 MCP Server Python (2파일)
#### `mcp-server/server.py`
**변경 1**: `_classify_pid_tag()` 반환에 `tag_dcs` 필드 추가
```python
# 기존
return {"kind": "instrument", "prefix": prefix, "type": type_name}
# 수정 — DCS prefix 목록 상수 추가
_DCS_PREFIXES = {"FIC","TIC","PIC","LIC","FY","TY","PY","LY","FV","TV","PV","LV"}
return {
"kind": "instrument",
"prefix": prefix,
"type": type_name,
"tag_dcs": prefix in _DCS_PREFIXES
}
```
**변경 2**: `_DB_SCHEMA` 상수에 `pid_equipment.tag_dcs` 컬럼 설명 추가
```python
_DB_SCHEMA = """
...
테이블: pid_equipment (P&ID 추출 태그/장비)
tag_no TEXT - 태그번호
category TEXT - 'instrument' / 'power_equipment' / ...
tag_dcs BOOL - TRUE=DCS 함수블록(FIC/TIC/PIC 등), FALSE=현장 물리 계기(FT/PT/FCV 등)
tag_class TEXT - 'field' / 'system' (tag_dcs 기반 + Experion 연결 보완)
instrument_type TEXT - prefix (FT/FIC/P 등)
...
"""
```
**변경 3**: `upsert_pid_connection` 함수
현재 허용 컬럼 목록: `from_tag, to_tag, from_at, to_at, role, category, tag_class, connection_locked`
`tag_dcs` 추가 허용 여부 검토 (운전원이 수동 override 가능하도록)
#### `mcp-server/worker/sql_prompt.py`
`DB_SCHEMA` 상수 — 현재 `pid_equipment`가 직접 언급되지 않으나,
향후 NL2SQL에서 "DCS 태그인지" 질문 처리를 위해 다음 추가:
```
테이블: pid_equipment(tag_no TEXT, category TEXT, tag_dcs BOOL, tag_class TEXT, instrument_type TEXT, from_tag TEXT, to_tag TEXT)
※ tag_dcs=TRUE: DCS 함수블록(FIC/TIC/PIC류), FALSE: 현장 물리 계기(FT/FCV류)
```
---
### 2.10 프롬프트 / 지식 파일 (1파일)
#### `prompts/plant_context.md`
현재 계기/태그 분류 설명에 다음 추가:
```markdown
## pid_equipment.tag_dcs
- tag_dcs = TRUE: DCS 내부 함수블록 (FIC, TIC, PIC, LIC, FY, TY, PY, LY 등)
- 물리 기기 없음, Experion 데이터베이스 포인트로만 존재
- tag_dcs = FALSE: 현장 물리 계기 (FT, PT, LT, FCV, PSV, XV 등)
- P&ID 도면에 기기 심벌로 표시되는 실물
- "DCS 태그 몇 개?" → pid_equipment WHERE tag_dcs=TRUE COUNT
- "현장 계기 목록" → pid_equipment WHERE tag_dcs=FALSE AND category='instrument'
```
---
### 2.11 instrument_inference (검토 필요, 1파일)
#### `mcp-server/instrument_inference/infer.py`
현재 `_dcs_internal_roles` 집합으로 내부적으로 DCS/field 구별 중.
`tag_dcs` 컬럼 도입 후 이 로직이 중복될 수 있으나, infer.py는 **추론 단계**이므로
`pid_prefix_rules.tag_dcs`를 직접 참조할 수 없음 (독립 실행 모듈).
**변경 불필요**`_dcs_internal_roles` 로직은 infer 내부 용도로 유지.
---
## 3. 단계별 구현 순서
### Phase 1: DB 스키마 (선행 필수)
1. `ExperionDbContext.cs` Boot DDL에 `ALTER TABLE` 추가
2. 마이그레이션 SQL 실행 (직접 또는 재기동 시 자동 적용)
### Phase 2: 도메인/DTO/서비스 (C# 코어)
1. `PidPrefixRule.cs``TagDcs` 프로퍼티 추가
2. `PidEquipment.cs``TagDcs` 프로퍼티 추가
3. `PidPrefixRuleDto.cs` — 3개 record 수정
4. `PidExtractorService.cs` — 추출/CRUD/export/import/backfill 수정
### Phase 3: Web Controller
1. `PidController.cs` — camelCase 직렬화 확인 (필요시 `[JsonPropertyName]`)
### Phase 4: Web UI
1. `pid.js` — PREFIX 그룹 렌더링 + Add/Update 폼
2. `panes/pid.html` — 열 헤더
### Phase 5: MCP / LLM 경로
1. `server.py``_classify_pid_tag` + `_DB_SCHEMA` + `upsert_pid_connection`
2. `worker/sql_prompt.py` — DB_SCHEMA pid_equipment 항목
3. `prompts/plant_context.md` — tag_dcs 설명
### Phase 6: 검증
1. `dotnet build` — 경고 0/에러 0
2. `python3 -m py_compile mcp-server/server.py` — OK
3. 웹 UI: PREFIX 분류 탭에서 DCS/현장 배지 확인
4. pid_equipment 추출 후 `SELECT tag_dcs, COUNT(*) FROM pid_equipment GROUP BY tag_dcs` 확인
5. LLM 채팅: "FIC-6113이 DCS 태그야?" 질문 → 정상 답변 확인
---
## 4. 설계 결정
| 항목 | 결정 | 이유 |
|------|------|------|
| 컬럼 타입 | `tag_dcs BOOLEAN` (별도 카테고리 X) | 카테고리 변경 시 하위 의존(뷰·필터) 전파 범위 과도. Boolean이 최소 침습적 |
| `tag_class` 유지 | 유지 (deprecated 아님) | Experion 연결 ground truth 포함, 더 정밀. `tag_dcs`는 prefix 기반 빠른 flag |
| `tag_class` 파생 | `tag_dcs=TRUE → TagClass='system'` | 기존 ISA 분석 로직 보완이 아닌 override |
| FCV/PCV/LCV/TCV | `tag_dcs = FALSE` (현장 유지) | 물리 제어밸브. DCS가 제어하지만 기기 자체는 현장 |
| FV/TV/PV/LV | `tag_dcs = TRUE` | ISA 표준상 "Valve(function block output)" — 물리 기기 아닌 DCS 출력 |
| UI 표시 | category 컬럼 유지, tag_dcs 배지 추가 | 카테고리 탭 구조(instrument/power_equipment…) 그대로 유지 |
| seed UPDATE 시점 | Boot DDL 이후 별도 UPDATE | INSERT ON CONFLICT DO NOTHING은 기존 행 미반영 |
| backfill | 재기동 시 자동 실행 (Boot DDL에 포함) | 수동 실행 의존성 제거 |
---
## 5. 잔여/고려사항
- **FICQ, TICQ 등 "Q" suffix prefix**: 현재 시드에 없음. 추출 시 FIC의 변형으로 처리되므로
`instrument_type = 'FICQ'`로 저장되면 prefix rule 미매칭 → `tag_dcs = FALSE`(default) 오류 가능.
→ 시드에 `('FICQ','instrument','Flow IC Totalizer',10, TRUE)` 등 추가 필요 여부 검토.
- **누락 prefix 처리**: `pid_equipment.instrument_type`이 prefix rule에 없으면 backfill 불가.
`tag_class = 'system'`인 행으로 보조 매핑 가능.
- **P&ID 도면 재추출 여부**: 기존 추출 결과는 backfill SQL로 충분. 재추출 불필요.

View File

@@ -1128,7 +1128,7 @@ public class ExperionPidController : ControllerBase
[HttpGet("export/csv")]
public async Task<IActionResult> ExportCsv([FromQuery] string? tagNo)
{
var (_, items) = await _extractor.GetEquipmentAsync(tagNo, 1, int.MaxValue);
var (_, items) = await _extractor.GetEquipmentAsync(tagNo, null, 1, int.MaxValue);
var csv = await _extractor.ExportToCsvAsync(items);
var bytes = System.Text.Encoding.UTF8.GetBytes(csv);
return File(bytes, "text/csv", $"pid-equipment-{DateTime.Now:yyyyMMdd}.csv");
@@ -1137,7 +1137,7 @@ public class ExperionPidController : ControllerBase
[HttpGet("export/excel")]
public async Task<IActionResult> ExportExcel([FromQuery] string? tagNo)
{
var (_, items) = await _extractor.GetEquipmentAsync(tagNo, 1, int.MaxValue);
var (_, items) = await _extractor.GetEquipmentAsync(tagNo, null, 1, int.MaxValue);
var excelBytes = await _extractor.ExportToExcelAsync(items);
return File(excelBytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", $"pid-equipment-{DateTime.Now:yyyyMMdd}.xlsx");
}