opencode 로 바꾸고 작업전 커밋
This commit is contained in:
804
plans/digital_tag_adding_action_plan_phase1.md
Normal file
804
plans/digital_tag_adding_action_plan_phase1.md
Normal file
@@ -0,0 +1,804 @@
|
||||
# 디지털 태그 추가 작업 계획 (Phase 1)
|
||||
|
||||
## 개요
|
||||
|
||||
`realtime-tag-expansion-design.md` 설계안을 기반으로 Phase 1 기본 확장을 구현합니다.
|
||||
기존 `realtime_table` 구조를 유지하면서 디지털 장비(Pump, XV) 태그와 메타데이터 지원을 추가합니다.
|
||||
|
||||
---
|
||||
|
||||
## Todo List (작업 진행 상태 추적)
|
||||
|
||||
> 각 단계는 독립적으로 실행 가능하며, 완료 상태를 표시하여 다음 작업에서 바로 읽을 수 있습니다.
|
||||
|
||||
- [x] **Step 1**: DB Migration — `tag_metadata` 테이블 + `v_tag_summary` 뷰 생성
|
||||
- [x] **Step 2**: TagMetadata Entity — 신규 Entity 클래스 + DbContext 등록
|
||||
- [x] **Step 3**: MetadataLoaderService — OPC UA에서 메타데이터 읽기/저장 서비스
|
||||
- [x] **Step 4**: API Endpoint — `POST /api/tags/metadata/reload` 컨트롤러 추가
|
||||
- [x] **Step 5**: Frontend UI 변경 — 포인트빌더 최대 10개, 메타데이터 재로드 버튼
|
||||
- [x] **Step 6**: NL2SQL DB_SCHEMA 업데이트 — `tag_metadata`, `v_tag_summary` 추가 + `Program.cs` SqlValidatorOptions `AllowedTables` 업데이트
|
||||
- [ ] **Step 7**: 테스트 계획 — xv-6124 등록 및 OPC UA 구독 확인 (수동 테스트 필요)
|
||||
|
||||
---
|
||||
|
||||
## Step 1: DB Migration — `tag_metadata` 테이블 + `v_tag_summary` 뷰 생성
|
||||
|
||||
### 상태: [x] 완료
|
||||
|
||||
### 변경 파일
|
||||
- `src/Infrastructure/Database/ExperionDbContext.cs`
|
||||
|
||||
### 변경 내용
|
||||
|
||||
[`ExperionDbService.InitializeAsync()`](src/Infrastructure/Database/ExperionDbContext.cs:176) 메서드 내에 다음 DDL을 추가합니다.
|
||||
기존 DDL(`realtime_table` 생성 이후, `EnsureCreatedAsync` 설명 주석 이전)에 추가합니다.
|
||||
|
||||
**추가 위치:** 275번 라인 (`// realtime_table은 실시간 값 저장용이므로 하이퍼테이블로 변환하지 않음`) 직후
|
||||
|
||||
```sql
|
||||
// tag_metadata 테이블 생성 (메타데이터 - 변경 드묾)
|
||||
await _ctx.Database.ExecuteSqlRawAsync("""
|
||||
CREATE TABLE IF NOT EXISTS tag_metadata (
|
||||
id SERIAL PRIMARY KEY,
|
||||
base_tag TEXT NOT NULL,
|
||||
attribute TEXT NOT NULL,
|
||||
value TEXT,
|
||||
node_id TEXT,
|
||||
loaded_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(base_tag, attribute)
|
||||
)
|
||||
""");
|
||||
|
||||
await _ctx.Database.ExecuteSqlRawAsync("""
|
||||
CREATE INDEX IF NOT EXISTS idx_tag_metadata_base ON tag_metadata(base_tag)
|
||||
""");
|
||||
|
||||
// v_tag_summary 뷰 생성 (실시간 + 메타데이터 통합)
|
||||
await _ctx.Database.ExecuteSqlRawAsync("""
|
||||
CREATE OR REPLACE VIEW v_tag_summary AS
|
||||
SELECT
|
||||
rt_base.base_tag,
|
||||
pv_rt.livevalue AS pv,
|
||||
sp_rt.livevalue AS sp,
|
||||
op_rt.livevalue AS op,
|
||||
instate0_rt.livevalue AS instate0,
|
||||
instate1_rt.livevalue AS instate1,
|
||||
instate2_rt.livevalue AS instate2,
|
||||
desc_md.value AS description,
|
||||
area_md.value AS area,
|
||||
s0d_md.value AS state0_descriptor,
|
||||
s1d_md.value AS state1_descriptor,
|
||||
s2d_md.value AS state2_descriptor
|
||||
FROM (SELECT DISTINCT split_part(tagname, '.', 1) AS base_tag FROM realtime_table) rt_base
|
||||
LEFT JOIN realtime_table pv_rt ON pv_rt.tagname = rt_base.base_tag || '.pv'
|
||||
LEFT JOIN realtime_table sp_rt ON sp_rt.tagname = rt_base.base_tag || '.sp'
|
||||
LEFT JOIN realtime_table op_rt ON op_rt.tagname = rt_base.base_tag || '.op'
|
||||
LEFT JOIN realtime_table instate0_rt ON instate0_rt.tagname = rt_base.base_tag || '.instate0'
|
||||
LEFT JOIN realtime_table instate1_rt ON instate1_rt.tagname = rt_base.base_tag || '.instate1'
|
||||
LEFT JOIN realtime_table instate2_rt ON instate2_rt.tagname = rt_base.base_tag || '.instate2'
|
||||
LEFT JOIN tag_metadata desc_md ON desc_md.base_tag = rt_base.base_tag AND desc_md.attribute = 'desc'
|
||||
LEFT JOIN tag_metadata area_md ON area_md.base_tag = rt_base.base_tag AND area_md.attribute = 'area'
|
||||
LEFT JOIN tag_metadata s0d_md ON s0d_md.base_tag = rt_base.base_tag AND s0d_md.attribute = 'state0descriptor'
|
||||
LEFT JOIN tag_metadata s1d_md ON s1d_md.base_tag = rt_base.base_tag AND s1d_md.attribute = 'state1descriptor'
|
||||
LEFT JOIN tag_metadata s2d_md ON s2d_md.base_tag = rt_base.base_tag AND s2d_md.attribute = 'state2descriptor'
|
||||
""");
|
||||
```
|
||||
|
||||
### 검증 방법
|
||||
1. 앱을 실행하여 DB 초기화가 자동으로 수행되는지 확인
|
||||
2. psql로 접속하여 `SELECT * FROM tag_metadata LIMIT 1;` 실행
|
||||
3. `SELECT * FROM v_tag_summary LIMIT 5;` 실행하여 뷰가 정상 동작하는지 확인
|
||||
|
||||
---
|
||||
|
||||
## Step 2: TagMetadata Entity — 신규 Entity 클래스 + DbContext 등록
|
||||
|
||||
### 상태: [x] 완료
|
||||
|
||||
### 변경 파일
|
||||
- `src/Core/Domain/Entities/ExperionEntities.cs` (신규 Entity 추가)
|
||||
- `src/Infrastructure/Database/ExperionDbContext.cs` (DbSet + ModelBuilder 등록)
|
||||
|
||||
### 2.1 신규 Entity 클래스 추가
|
||||
|
||||
**파일:** [`src/Core/Domain/Entities/ExperionEntities.cs`](src/Core/Domain/Entities/ExperionEntities.cs:138)
|
||||
|
||||
[`FastRecord`](src/Core/Domain/Entities/ExperionEntities.cs:129) 클래스 직후에 다음 Entity를 추가합니다.
|
||||
|
||||
```csharp
|
||||
/// <summary>tag_metadata — 태그 메타데이터 (변경 드묾)</summary>
|
||||
[Table("tag_metadata")]
|
||||
public class TagMetadata
|
||||
{
|
||||
[Column("id")] public int Id { get; set; }
|
||||
[Column("base_tag")] public string BaseTag { get; set; } = string.Empty;
|
||||
[Column("attribute")] public string Attribute { get; set; } = string.Empty;
|
||||
[Column("value")] public string? Value { get; set; }
|
||||
[Column("node_id")] public string? NodeId { get; set; }
|
||||
[Column("loaded_at")] public DateTime LoadedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 DbContext 에 DbSet 등록
|
||||
|
||||
**파일:** [`src/Infrastructure/Database/ExperionDbContext.cs`](src/Infrastructure/Database/ExperionDbContext.cs:20)
|
||||
|
||||
기존 DbSet 목록 ([`FastRecords`](src/Infrastructure/Database/ExperionDbContext.cs:23) 이후, P&ID DbSet 이전) 에 추가:
|
||||
|
||||
```csharp
|
||||
public DbSet<TagMetadata> TagMetadata => Set<TagMetadata>();
|
||||
```
|
||||
|
||||
### 2.3 ModelBuilder 설정 추가
|
||||
|
||||
**파일:** [`src/Infrastructure/Database/ExperionDbContext.cs`](src/Infrastructure/Database/ExperionDbContext.cs:30)
|
||||
|
||||
[`OnModelCreating()`](src/Infrastructure/Database/ExperionDbContext.cs:30) 메서드 내, [`FastRecord`](src/Infrastructure/Database/ExperionDbContext.cs:75) 설정 이후, P&ID 엔티티 설정 이전에 추가:
|
||||
|
||||
```csharp
|
||||
// DDL에서 UNIQUE(base_tag, attribute)와 idx_tag_metadata_base 인덱스를 이미 생성하므로
|
||||
// ModelBuilder 설정은 EF Core Migration 호환성을 위해 유지하되 실제 인덱스 생성은 DDL이 담당
|
||||
modelBuilder.Entity<TagMetadata>(e =>
|
||||
{
|
||||
e.HasKey(x => x.Id);
|
||||
e.HasIndex(x => new { x.BaseTag, x.Attribute }).IsUnique(); // DDL UNIQUE 제약과 대응
|
||||
e.HasIndex(x => x.BaseTag); // DDL idx_tag_metadata_base와 대응
|
||||
});
|
||||
```
|
||||
|
||||
### 2.4 변경 후 파일 구조 확인
|
||||
|
||||
[`ExperionEntities.cs`](src/Core/Domain/Entities/ExperionEntities.cs) 최종 구조:
|
||||
```
|
||||
ExperionTag (line 7)
|
||||
ExperionRecord (line 21)
|
||||
ExperionServerConfig (line 32)
|
||||
RawNodeMap (line 47)
|
||||
NodeMapMaster (line 59)
|
||||
RealtimePoint (line 71)
|
||||
HistoryRecord (line 82)
|
||||
ExperionStatusCodeInfo (line 92)
|
||||
PidGraphStatus (line 102)
|
||||
FastSession (line 113)
|
||||
FastRecord (line 130)
|
||||
TagMetadata (신규 추가) ← 여기
|
||||
```
|
||||
|
||||
[`ExperionDbContext.cs`](src/Infrastructure/Database/ExperionDbContext.cs) DbSet 최종 구조:
|
||||
```csharp
|
||||
public DbSet<ExperionRecord> ExperionRecords => Set<ExperionRecord>();
|
||||
public DbSet<RawNodeMap> RawNodeMaps => Set<RawNodeMap>();
|
||||
public DbSet<NodeMapMaster> NodeMapMasters => Set<NodeMapMaster>();
|
||||
public DbSet<RealtimePoint> RealtimePoints => Set<RealtimePoint>();
|
||||
public DbSet<HistoryRecord> HistoryRecords => Set<HistoryRecord>();
|
||||
public DbSet<FastSession> FastSessions => Set<FastSession>();
|
||||
public DbSet<FastRecord> FastRecords => Set<FastRecord>();
|
||||
public DbSet<TagMetadata> TagMetadata => Set<TagMetadata>(); // ← 신규
|
||||
// P&ID DbSet ...
|
||||
```
|
||||
|
||||
### 검증 방법
|
||||
1. `dotnet build` 컴파일 오류 없는지 확인
|
||||
2. Entity 클래스가 `[Table("tag_metadata")]` 속성으로 DB 테이블과 매핑되는지 확인
|
||||
3. DbContext에서 `TagMetadata` DbSet를 통해 CRUD가 가능한지 확인
|
||||
|
||||
---
|
||||
|
||||
## Step 3: MetadataLoaderService — OPC UA에서 메타데이터 읽기/저장 서비스
|
||||
|
||||
### 상태: [x] 완료
|
||||
|
||||
### 신규 파일
|
||||
- `src/Infrastructure/OpcUa/MetadataLoaderService.cs`
|
||||
|
||||
### 변경 파일
|
||||
- `src/Core/Application/Interfaces/IExperionServices.cs` (인터페이스 추가)
|
||||
- `src/Web/Program.cs` (DI 등록)
|
||||
|
||||
### 3.1 인터페이스 정의
|
||||
|
||||
**파일:** [`src/Core/Application/Interfaces/IExperionServices.cs`](src/Core/Application/Interfaces/IExperionServices.cs)
|
||||
|
||||
파일 끝에 다음 인터페이스 추가:
|
||||
|
||||
```csharp
|
||||
// ── Metadata Loader ──────────────────────────────────────────────────────────
|
||||
|
||||
public interface IMetadataLoaderService
|
||||
{
|
||||
/// <summary>
|
||||
/// 태그 등록 시 메타데이터(desc, area, state0~7descriptor)를 OPC UA에서 한 번 읽어서 tag_metadata 저장
|
||||
/// </summary>
|
||||
Task<int> LoadMetadataAsync(ExperionServerConfig cfg, IEnumerable<string> baseTags);
|
||||
|
||||
/// <summary>
|
||||
/// UI 재로드 버튼 클릭 시 메타데이터 재조회 + tag_metadata UPDATE
|
||||
/// 참고: node_map_master UPSERT는 Phase 2 자동 속성 제안 기능에서 처리 예정
|
||||
/// </summary>
|
||||
Task<int> ReloadMetadataAsync(ExperionServerConfig cfg, IEnumerable<string>? baseTags = null);
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 서비스 구현
|
||||
|
||||
**파일:** `src/Infrastructure/OpcUa/MetadataLoaderService.cs` (신규)
|
||||
|
||||
> **수정 이력 (진단):**
|
||||
> - `using Npgsql;` 추가 (누락 시 빌드 오류)
|
||||
> - `NpgsqlTypes.NpgsqlParameter` → `Npgsql.NpgsqlParameter` (잘못된 네임스페이스)
|
||||
> - CTE 컬럼 목록에 `loaded_at` 추가 (4열 선언 vs 5값 불일치 → SQL 런타임 오류)
|
||||
> - 사용되지 않는 `updateSql` 변수 제거 (데드코드)
|
||||
> - `baseTags`를 메서드 진입 시 `ToList()`로 구체화 (이중 열거 방지)
|
||||
|
||||
```csharp
|
||||
using ExperionCrawler.Core.Application.Interfaces;
|
||||
using ExperionCrawler.Core.Domain.Entities;
|
||||
using ExperionCrawler.Infrastructure.Database;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Npgsql;
|
||||
|
||||
namespace ExperionCrawler.Infrastructure.OpcUa;
|
||||
|
||||
/// <summary>
|
||||
/// 메타데이터(desc, area, state0~7descriptor)를 OPC UA에서 읽어서 tag_metadata 테이블에 저장/갱신
|
||||
/// </summary>
|
||||
public class MetadataLoaderService : IMetadataLoaderService
|
||||
{
|
||||
private readonly IExperionOpcClient _opcClient;
|
||||
private readonly ExperionDbContext _ctx;
|
||||
private readonly ILogger<MetadataLoaderService> _logger;
|
||||
|
||||
// 로드할 메타데이터 속성 목록
|
||||
private static readonly string[] MetaAttributes =
|
||||
{
|
||||
"desc", "area",
|
||||
"state0descriptor", "state1descriptor", "state2descriptor",
|
||||
"state3descriptor", "state4descriptor", "state5descriptor",
|
||||
"state6descriptor", "state7descriptor"
|
||||
};
|
||||
|
||||
public MetadataLoaderService(
|
||||
IExperionOpcClient opcClient,
|
||||
ExperionDbContext ctx,
|
||||
ILogger<MetadataLoaderService> logger)
|
||||
{
|
||||
_opcClient = opcClient;
|
||||
_ctx = ctx;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<int> LoadMetadataAsync(ExperionServerConfig cfg, IEnumerable<string> baseTags)
|
||||
{
|
||||
var baseTagList = baseTags.ToList(); // 이중 열거 방지
|
||||
|
||||
// ── Step 1: 모든 노드 ID 수집 ──────────────────────────────────────
|
||||
var nodeMap = new Dictionary<string, (string baseTag, string attr)>();
|
||||
foreach (var baseTag in baseTagList)
|
||||
{
|
||||
foreach (var attr in MetaAttributes)
|
||||
{
|
||||
var nodeId = $"{cfg.ServerHostName}:{baseTag}.{attr}";
|
||||
var fullNodeId = $"ns=1;s={nodeId}";
|
||||
nodeMap[fullNodeId] = (baseTag.ToLowerInvariant(), attr);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Step 2: 배치 읽기 (ReadTagsAsync 사용) ────────────────────────
|
||||
var results = await _opcClient.ReadTagsAsync(cfg, nodeMap.Keys);
|
||||
var entries = new List<(string baseTag, string attr, string? value, string nodeId)>();
|
||||
|
||||
foreach (var result in results)
|
||||
{
|
||||
if (result.Success && result.Value != null && nodeMap.TryGetValue(result.NodeId, out var meta))
|
||||
{
|
||||
entries.Add((meta.baseTag, meta.attr, result.Value?.ToString(), result.NodeId));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Step 3: 단일 배치 UPSERT ──────────────────────────────────────
|
||||
if (entries.Count > 0)
|
||||
{
|
||||
// VALUES 절을 동적으로 생성하여 한 번에 INSERT
|
||||
// CTE 컬럼(5개)과 VALUES 값(5개) 일치: base_tag, attribute, value, node_id, loaded_at
|
||||
var valuesSql = string.Join(", ", entries.Select((e, i) =>
|
||||
$"(@bt{i}, @attr{i}, @val{i}, @nid{i}, NOW())"));
|
||||
|
||||
await _ctx.Database.ExecuteSqlRawAsync(@"
|
||||
WITH new_data (base_tag, attribute, value, node_id, loaded_at) AS (
|
||||
VALUES " + valuesSql + @"
|
||||
)
|
||||
INSERT INTO tag_metadata (base_tag, attribute, value, node_id, loaded_at)
|
||||
SELECT base_tag, attribute, value, node_id, loaded_at FROM new_data
|
||||
ON CONFLICT (base_tag, attribute)
|
||||
DO UPDATE SET value = excluded.value, node_id = excluded.node_id, loaded_at = NOW()",
|
||||
entries.SelectMany((e, i) => new object[] {
|
||||
new NpgsqlParameter($"@bt{i}", e.baseTag),
|
||||
new NpgsqlParameter($"@attr{i}", e.attr),
|
||||
new NpgsqlParameter($"@val{i}", (object?)e.value ?? DBNull.Value),
|
||||
new NpgsqlParameter($"@nid{i}", e.nodeId)
|
||||
}).ToArray());
|
||||
}
|
||||
|
||||
// v_tag_summary는 일반 VIEW이므로 REFRESH 불필요 (조회 시 실시간 JOIN)
|
||||
_logger.LogInformation("[Metadata] 로드 완료: {Count}개 속성 ({TagCount}개 태그)", entries.Count, baseTagList.Count);
|
||||
return entries.Count;
|
||||
}
|
||||
|
||||
public async Task<int> ReloadMetadataAsync(ExperionServerConfig cfg, IEnumerable<string>? baseTags = null)
|
||||
{
|
||||
// baseTags가 null이면 tag_metadata에서 전체 base_tag 조회
|
||||
var tags = baseTags?.ToList() ?? await _ctx.TagMetadata.Select(t => t.BaseTag).Distinct().ToListAsync();
|
||||
return await LoadMetadataAsync(cfg, tags);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 DI 등록
|
||||
|
||||
**파일:** `src/Web/Program.cs`
|
||||
|
||||
기존 서비스 등록 부분 근처에 추가:
|
||||
|
||||
```csharp
|
||||
builder.Services.AddScoped<IMetadataLoaderService, MetadataLoaderService>();
|
||||
```
|
||||
|
||||
### 검증 방법
|
||||
1. `dotnet build` 컴파일 오류 없는지 확인
|
||||
2. 테스트용 코드에서 `LoadMetadataAsync` 호출 시 OPC UA에서 desc/area 값이 정상 읽히는지 로그 확인
|
||||
3. DB에서 `SELECT * FROM tag_metadata;` 실행하여 데이터 저장 확인
|
||||
|
||||
---
|
||||
|
||||
## Step 4: API Endpoint — `POST /api/tags/metadata/reload` 컨트롤러 추가
|
||||
|
||||
### 상태: [x] 완료
|
||||
|
||||
### 변경 파일
|
||||
- `src/Web/Controllers/ExperionControllers.cs` (신규 컨트롤러 추가)
|
||||
|
||||
### 4.1 컨트롤러 추가 (최종 형태)
|
||||
|
||||
**파일:** [`src/Web/Controllers/ExperionControllers.cs`](src/Web/Controllers/ExperionControllers.cs)
|
||||
|
||||
[`ExperionPointBuilderController`](src/Web/Controllers/ExperionControllers.cs:272) 직후, [`ExperionRealtimeController`](src/Web/Controllers/ExperionControllers.cs:348) 이전에 추가:
|
||||
|
||||
```csharp
|
||||
// ── 태그 메타데이터 관리 ──────────────────────────────────────────────────────
|
||||
|
||||
[ApiController]
|
||||
[Route("api/tags/metadata")]
|
||||
public class TagMetadataController : ControllerBase
|
||||
{
|
||||
private readonly IMetadataLoaderService _metaSvc;
|
||||
private readonly ExperionDbContext _ctx;
|
||||
private readonly IConfiguration _config;
|
||||
|
||||
public TagMetadataController(
|
||||
IMetadataLoaderService metaSvc,
|
||||
ExperionDbContext ctx,
|
||||
IConfiguration config)
|
||||
{
|
||||
_metaSvc = metaSvc;
|
||||
_ctx = ctx;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 태그(또는 전체)의 메타데이터를 OPC UA에서 재조회하여 갱신
|
||||
/// 설정은 appsettings.json의 "Experion:Default" 섹션에서 읽음
|
||||
/// </summary>
|
||||
[HttpPost("reload")]
|
||||
public async Task<IActionResult> Reload([FromBody] MetadataReloadRequest? req)
|
||||
{
|
||||
// appsettings.json에서 기본 설정 읽기 (비밀번호 하드코딩 금지)
|
||||
var section = _config.GetSection("Experion:Default");
|
||||
var cfg = new ExperionServerConfig
|
||||
{
|
||||
ServerHostName = req?.ServerHostName ?? section["ServerHostName"] ?? throw new InvalidOperationException("ServerHostName이 설정되지 않았습니다."),
|
||||
Port = req?.Port ?? section.GetValue<int?>("Port") ?? 4840,
|
||||
ClientHostName = req?.ClientHostName ?? section["ClientHostName"] ?? "dbsvr",
|
||||
UserName = req?.UserName ?? section["UserName"] ?? "",
|
||||
Password = req?.Password ?? section["Password"] ?? ""
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var count = await _metaSvc.ReloadMetadataAsync(cfg, req?.BaseTags);
|
||||
return Ok(new { success = true, count, message = $"{count}개 메타데이터 갱신 완료" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new { success = false, message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// tag_metadata 전체 조회
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetMetadata([FromQuery] string? baseTag = null)
|
||||
{
|
||||
var query = _ctx.TagMetadata.AsQueryable();
|
||||
if (!string.IsNullOrEmpty(baseTag))
|
||||
query = query.Where(t => t.BaseTag == baseTag.ToLowerInvariant());
|
||||
|
||||
var items = await query.Select(t => new
|
||||
{
|
||||
t.BaseTag, t.Attribute, t.Value, t.NodeId, t.LoadedAt
|
||||
}).ToListAsync();
|
||||
|
||||
return Ok(new { count = items.Count, items });
|
||||
}
|
||||
}
|
||||
|
||||
public record MetadataReloadRequest(
|
||||
string? ServerHostName,
|
||||
int? Port,
|
||||
string? ClientHostName,
|
||||
string? UserName,
|
||||
string? Password,
|
||||
IEnumerable<string>? BaseTags);
|
||||
```
|
||||
|
||||
> **주의**: `appsettings.json`에 다음 섹션이 있어야 합니다:
|
||||
> ```json
|
||||
> {
|
||||
> "Experion": {
|
||||
> "Default": {
|
||||
> "ServerHostName": "192.168.0.20",
|
||||
> "Port": 4840,
|
||||
> "ClientHostName": "dbsvr",
|
||||
> "UserName": "mngr",
|
||||
> "Password": "mngr"
|
||||
> }
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
|
||||
### 검증 방법
|
||||
1. `dotnet build` 컴파일 오류 없는지 확인
|
||||
2. Postman/curl로 `POST /api/tags/metadata/reload` 호출하여 메타데이터 갱신 확인
|
||||
3. `GET /api/tags/metadata?baseTag=xv-6124` 호출하여 조회 확인
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Frontend UI 변경 — 포인트빌더 최대 10개, 메타데이터 재로드 버튼
|
||||
|
||||
### 상태: [x] 완료
|
||||
|
||||
### 변경 파일
|
||||
- `src/Web/wwwroot/index.html` (포인트빌더 탭 HTML 변경)
|
||||
- `src/Web/wwwroot/js/app.js` (JS 로직 변경)
|
||||
|
||||
### 5.1 HTML 변경 — 포인트빌더 최대 10개로 확장
|
||||
|
||||
**파일:** [`src/Web/wwwroot/index.html`](src/Web/wwwroot/index.html:422)
|
||||
|
||||
현재 포인트빌더 탭(`pane-pb`) 내 name 선택 드롭다운이 8개(`pb-n1`~`pb-n8`)입니다. 10개로 확장:
|
||||
|
||||
**변경 위치:** `pb-name-grid` 내부 select 요소 2개 추가
|
||||
|
||||
```html
|
||||
<!-- 신규 추가 -->
|
||||
<select id="pb-n9" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||||
<select id="pb-n10" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||||
```
|
||||
|
||||
**레이블 텍스트 변경:**
|
||||
```html
|
||||
<label>이름(name) 선택 <em>(OR 조건, 최대 10개)</em>
|
||||
```
|
||||
|
||||
### 5.2 HTML 변경 — 메타데이터 재로드 버튼 추가
|
||||
|
||||
**파일:** [`src/Web/wwwroot/index.html`](src/Web/wwwroot/index.html:482)
|
||||
|
||||
`pb-rt-status` 로그 박스 직후, 포인트 목록 카드 이전에 추가:
|
||||
|
||||
```html
|
||||
<div class="card" style="margin-top:18px">
|
||||
<div class="card-cap">메타데이터 관리</div>
|
||||
<p style="color:var(--t2);font-size:13px;margin-bottom:12px">
|
||||
태그의 desc, area, state descriptor 정보를 OPC UA에서 조회하여 저장합니다.
|
||||
</p>
|
||||
<div class="btn-row">
|
||||
<button class="btn-a" onclick="metaReload()">🔄 메타데이터 재로드</button>
|
||||
<button class="btn-b" onclick="metaView()">📋 메타데이터 조회</button>
|
||||
</div>
|
||||
<div id="meta-log" class="logbox hidden" style="margin-top:8px"></div>
|
||||
<div id="meta-view" class="hidden" style="margin-top:10px;max-height:300px;overflow:auto"></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 5.3 JS 변경 — PB_NAME_IDS 배열 확장
|
||||
|
||||
**파일:** [`src/Web/wwwroot/js/app.js`](src/Web/wwwroot/js/app.js:575)
|
||||
|
||||
```javascript
|
||||
// 변경 전:
|
||||
const PB_NAME_IDS = ['pb-n1','pb-n2','pb-n3','pb-n4','pb-n5','pb-n6','pb-n7','pb-n8'];
|
||||
|
||||
// 변경 후:
|
||||
const PB_NAME_IDS = ['pb-n1','pb-n2','pb-n3','pb-n4','pb-n5','pb-n6','pb-n7','pb-n8','pb-n9','pb-n10'];
|
||||
```
|
||||
|
||||
### 5.4 JS 변경 — 메타데이터 재로드/조회 함수 추가
|
||||
|
||||
**파일:** [`src/Web/wwwroot/js/app.js`](src/Web/wwwroot/js/app.js:723)
|
||||
|
||||
`rtStatus()` 함수 직후에 추가:
|
||||
|
||||
```javascript
|
||||
/* ── 메타데이터 관리 ─────────────────────────────────────────── */
|
||||
|
||||
async function metaReload() {
|
||||
const body = {
|
||||
serverHostName: document.getElementById('pb-rt-ip').value.trim(),
|
||||
port: parseInt(document.getElementById('pb-rt-port').value) || 4840,
|
||||
clientHostName: document.getElementById('pb-rt-client').value.trim(),
|
||||
userName: document.getElementById('pb-rt-user').value.trim(),
|
||||
password: document.getElementById('pb-rt-pw').value
|
||||
};
|
||||
const logEl = document.getElementById('meta-log');
|
||||
logEl.classList.remove('hidden');
|
||||
logEl.innerHTML = '<div class="ll inf">⏳ 메타데이터 재로드 중...</div>';
|
||||
try {
|
||||
const d = await api('POST', '/api/tags/metadata/reload', body);
|
||||
logEl.innerHTML = `<div class="ll ${d.success ? 'ok' : 'err'}">${d.success ? '✅' : '❌'} ${esc(d.message)}</div>`;
|
||||
} catch (e) {
|
||||
logEl.innerHTML = `<div class="ll err">❌ ${esc(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function metaView() {
|
||||
const viewEl = document.getElementById('meta-view');
|
||||
viewEl.classList.remove('hidden');
|
||||
viewEl.innerHTML = '<div class="ll inf">⏳ 조회 중...</div>';
|
||||
try {
|
||||
const d = await api('GET', '/api/tags/metadata');
|
||||
const items = d.items || [];
|
||||
if (items.length === 0) {
|
||||
viewEl.innerHTML = '<div class="ll inf">메타데이터가 없습니다.</div>';
|
||||
return;
|
||||
}
|
||||
viewEl.innerHTML = `
|
||||
<table style="width:100%;font-size:12px">
|
||||
<thead><tr><th>BaseTag</th><th>Attribute</th><th>Value</th><th>LoadedAt</th></tr></thead>
|
||||
<tbody>
|
||||
${items.map(m => `
|
||||
<tr>
|
||||
<td style="font-weight:600">${esc(m.baseTag)}</td>
|
||||
<td>${esc(m.attribute)}</td>
|
||||
<td>${esc(m.value || '-')}</td>
|
||||
<td class="mut">${m.loadedAt ? new Date(m.loadedAt).toLocaleString('ko-KR') : '-'}</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
} catch (e) {
|
||||
viewEl.innerHTML = `<div class="ll err">❌ ${esc(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 검증 방법
|
||||
1. 브라우저에서 포인트빌더 탭 확인 — 드롭다운이 10개인지 확인
|
||||
2. "메타데이터 재로드" 버튼 클릭 시 API 호출되고 결과 로그 표시 확인
|
||||
3. "메타데이터 조회" 버튼 클릭 시 테이블 형태로 메타데이터 표시 확인
|
||||
|
||||
---
|
||||
|
||||
## Step 6: NL2SQL DB_SCHEMA 업데이트 — `tag_metadata`, `v_tag_summary` 추가
|
||||
|
||||
### 상태: [x] 완료
|
||||
|
||||
### 변경 파일
|
||||
- `mcp-server/worker/nl2sql_worker.py` (DB_SCHEMA 변수 업데이트)
|
||||
- `mcp-server/server.py` (_DB_SCHEMA 변수 업데이트)
|
||||
|
||||
### 6.1 nl2sql_worker.py 변경
|
||||
|
||||
**파일:** [`mcp-server/worker/nl2sql_worker.py`](mcp-server/worker/nl2sql_worker.py:61)
|
||||
|
||||
기존 `DB_SCHEMA` 변수에 `tag_metadata` 테이블과 `v_tag_summary` 뷰 정의를 추가:
|
||||
|
||||
```python
|
||||
DB_SCHEMA = """
|
||||
PostgreSQL 시계열 데이터베이스 스키마
|
||||
|
||||
테이블: history_table (시계열 이력)
|
||||
tagname TEXT - 태그명 (모두 소문자, 예: 'ficq-6113.pv')
|
||||
node_id TEXT - OPC UA 노드 ID
|
||||
value TEXT - 측정값, 수치 연산 시 ::double precision 캐스트 필요
|
||||
recorded_at TIMESTAMPTZ - 기록 시각(UTC), 스냅샷 주기 약 60초
|
||||
|
||||
테이블: realtime_table (실시간 최신값)
|
||||
tagname TEXT - 태그명 (모두 소문자)
|
||||
node_id TEXT - OPC UA 노드 ID
|
||||
livevalue TEXT - 현재값
|
||||
timestamp TIMESTAMPTZ - 최종 갱신 시각
|
||||
|
||||
테이블: tag_metadata (태그 메타데이터 - 변경 드묾)
|
||||
base_tag TEXT - 기본 태그명 (예: 'ficq-6101', 'xv-6124')
|
||||
attribute TEXT - 속성명 ('desc', 'area', 'state0descriptor', ...)
|
||||
value TEXT - 메타데이터 값
|
||||
node_id TEXT - OPC UA 노드 ID
|
||||
loaded_at TIMESTAMPTZ - 마지막 로드 시각
|
||||
|
||||
뷰: v_tag_summary (실시간값 + 메타데이터 통합 뷰)
|
||||
base_tag TEXT - 기본 태그명
|
||||
pv TEXT - 현재 프로세스 값
|
||||
sp TEXT - 설정값
|
||||
op TEXT - 출력값
|
||||
instate0 TEXT - 상태 비트 0 (true/false)
|
||||
instate1 TEXT - 상태 비트 1 (true/false)
|
||||
instate2 TEXT - 상태 비트 2 (true/false)
|
||||
description TEXT - 장비 설명 (tag_metadata.desc)
|
||||
area TEXT - 소속 플랜트 (tag_metadata.area)
|
||||
state0_descriptor TEXT - 비트 0 의미 (예: "Run/Stop")
|
||||
state1_descriptor TEXT - 비트 1 의미 (예: "Remote/Local")
|
||||
state2_descriptor TEXT - 비트 2 의미 (예: "Trip/Normal")
|
||||
|
||||
새로운 태그 타입:
|
||||
- 아날로그: ficq-6101.pv/sp/op (Double)
|
||||
- 디지털 XV: xv-6124.pv/op (Int32), xv-6124.instate0~7 (Boolean)
|
||||
- Pump: p-6102.pv/op (Int32), p-6102.instate0~7 (Boolean)
|
||||
- 메타데이터: desc (String), area (Enum), state0descriptor~7 (String)
|
||||
|
||||
BCD 상태 조회 팁:
|
||||
- instate0~7은 Boolean (true/false)
|
||||
- state0descriptor~7은 해당 비트의 의미 설명
|
||||
- instate0=true이고 state0descriptor="Run/Stop"이면 → "Run" 상태
|
||||
- v_tag_summary 뷰를 사용하면 실시간값+메타데이터 한 번에 조회 가능
|
||||
|
||||
N분 간격 집계 공식 (time_bucket 금지, date_trunc 사용):
|
||||
1분 버킷: date_trunc('minute', recorded_at) AS bucket
|
||||
5분 버킷: to_timestamp(FLOOR(EXTRACT(EPOCH FROM recorded_at)/300)*300) AS bucket
|
||||
N분 버킷: to_timestamp(FLOOR(EXTRACT(EPOCH FROM recorded_at)/(N*60))*(N*60)) AS bucket
|
||||
|
||||
규칙:
|
||||
- SELECT만 허용 (INSERT/UPDATE/DELETE/DROP 등 불가)
|
||||
- tagname은 모두 소문자로 정확히 입력
|
||||
- value 컬럼은 TEXT이므로 집계 시 ::double precision 캐스트 필수
|
||||
- time_bucket 함수 사용 금지
|
||||
"""
|
||||
```
|
||||
|
||||
### 6.2 server.py 변경
|
||||
|
||||
**파일:** [`mcp-server/server.py`](mcp-server/server.py:430)
|
||||
|
||||
`_DB_SCHEMA` 변수도 동일하게 업데이트 (nl2sql_worker.py와 동일한 내용).
|
||||
|
||||
### 6.3 Program.cs SqlValidatorOptions 업데이트
|
||||
|
||||
> **진단에서 발견:** `Program.cs`의 C# `SqlValidator`가 `AllowedTables`를 화이트리스트로 관리함.
|
||||
> `tag_metadata`, `v_tag_summary`, `realtime_table`이 없으면 C# NL2SQL 경로(`ITextToSqlService`)에서 이 테이블을 조회하는 SQL이 거부됨.
|
||||
> MCP Python 경로는 자체 검증 로직을 사용하므로 별도 영향 없음.
|
||||
|
||||
**파일:** `src/Web/Program.cs` (68~73번 라인)
|
||||
|
||||
```csharp
|
||||
// 변경 전:
|
||||
builder.Services.AddSingleton<SqlValidatorOptions>(_ => new SqlValidatorOptions
|
||||
{
|
||||
RequiredTables = ["history_table"],
|
||||
AllowedTables = ["history_table", "node_map_master"],
|
||||
MaxSubqueryDepth = 4
|
||||
});
|
||||
|
||||
// 변경 후:
|
||||
builder.Services.AddSingleton<SqlValidatorOptions>(_ => new SqlValidatorOptions
|
||||
{
|
||||
RequiredTables = ["history_table"],
|
||||
AllowedTables = ["history_table", "node_map_master", "realtime_table", "tag_metadata", "v_tag_summary"],
|
||||
MaxSubqueryDepth = 4
|
||||
});
|
||||
```
|
||||
|
||||
### 6.4 NL2SQL 사용 예시 (새로운 시나리오)
|
||||
|
||||
| 자연어 질문 | 생성 SQL |
|
||||
|------------|----------|
|
||||
| "xv-6124의 현재 상태와 설명을 알려줘" | `SELECT instate0, instate1, state0_descriptor, state1_descriptor, description FROM v_tag_summary WHERE base_tag = 'xv-6124'` |
|
||||
| "Unit-A에 있는 모든 pump의 상태를 보여줘" | `SELECT base_tag, instate0, state0_descriptor, description FROM v_tag_summary WHERE area = 'Unit-A' AND base_tag LIKE 'p-%'` |
|
||||
| "모든 XV 중 instate0이 true인 것" | `SELECT base_tag, instate0, state0_descriptor FROM v_tag_summary WHERE instate0 = 'True' AND base_tag LIKE 'xv-%'` |
|
||||
|
||||
### 검증 방법
|
||||
1. MCP 서버 재시작 후 NL2SQL 도구 호출 시 새로운 스키마가 반영되는지 확인
|
||||
2. "xv-6124 상태 알려줘" 같은 질문으로 v_tag_summary 조회 테스트
|
||||
3. tag_metadata 테이블 조회가 정상적으로 되는지 확인
|
||||
4. `Program.cs` SqlValidatorOptions 업데이트 후 C# NL2SQL 경로에서도 `tag_metadata`, `v_tag_summary` 쿼리 허용 확인
|
||||
|
||||
---
|
||||
|
||||
## Step 7: 테스트 계획 — xv-6124 등록 및 OPC UA 구독 확인
|
||||
|
||||
### 상태: [ ] 수동 테스트 필요
|
||||
|
||||
### 테스트 시나리오
|
||||
|
||||
#### 7.1 디지털 태그 등록 테스트
|
||||
|
||||
**목표:** xv-6124의 pv, op, instate0~2를 realtime_table에 등록하고 OPC UA 구독 확인
|
||||
|
||||
**단계:**
|
||||
1. 포인트빌더 탭에서 이름에 `xv-6124` 선택
|
||||
2. 데이터 타입에 `Int32`, `Boolean` 입력
|
||||
3. "🔨 테이블 작성하기" 클릭
|
||||
4. 포인트 목록에 xv-6124.pv, xv-6124.op, xv-6124.instate0~2가 생성되는지 확인
|
||||
|
||||
**기대 결과:**
|
||||
- `realtime_table`에 xv-6124 관련 행이 INSERT됨
|
||||
- 포인트 목록 UI에 표시됨
|
||||
|
||||
#### 7.2 OPC UA 구독 테스트
|
||||
|
||||
**목표:** 실시간 값 업데이트 확인
|
||||
|
||||
**단계:**
|
||||
1. "▶ 구독 시작" 클릭
|
||||
2. 구독 상태 로그에 xv-6124 관련 노드가 포함된지 확인
|
||||
3. 몇 분 후 포인트 목록 새로고침
|
||||
4. `livevalue` 컬럼에 값이 업데이트되는지 확인
|
||||
|
||||
**기대 결과:**
|
||||
- xv-6124.pv → Int32 BCD 값 (예: 0, 1, 3, 5 등)
|
||||
- xv-6124.instate0 → Boolean (true/false)
|
||||
- 구독 카운터에 xv-6124 태그가 포함됨
|
||||
|
||||
#### 7.3 메타데이터 로드 테스트
|
||||
|
||||
**목표:** desc, area, state descriptor 값이 tag_metadata에 저장되는지 확인
|
||||
|
||||
**단계:**
|
||||
1. "🔄 메타데이터 재로드" 버튼 클릭
|
||||
2. 로그에 갱신된 속성 수 확인
|
||||
3. "📋 메타데이터 조회" 버튼 클릭
|
||||
4. xv-6124 관련 메타데이터 행 확인
|
||||
|
||||
**기대 결과:**
|
||||
- `tag_metadata`에 xv-6124 관련 행 INSERT됨
|
||||
- desc: 장비 설명 문자열
|
||||
- area: 소속 플랜트 (예: "Unit-A")
|
||||
- state0descriptor: "Open/Close" 또는 유사한 문자열
|
||||
|
||||
#### 7.4 v_tag_summary 뷰 테스트
|
||||
|
||||
**목표:** 실시간값 + 메타데이터 통합 조회 확인
|
||||
|
||||
**단계:**
|
||||
```sql
|
||||
-- psql에서 실행
|
||||
SELECT * FROM v_tag_summary WHERE base_tag = 'xv-6124';
|
||||
```
|
||||
|
||||
**기대 결과:**
|
||||
| base_tag | pv | op | instate0 | instate1 | instate2 | description | area | state0_descriptor | state1_descriptor | state2_descriptor |
|
||||
|----------|-----|-----|----------|----------|----------|-------------|------|-------------------|-------------------|-------------------|
|
||||
| xv-6124 | 1 | 1 | true | false | false | XV-6124 설명 | Unit-A | Open/Close | Remote/Local | Fault/Normal |
|
||||
|
||||
#### 7.5 NL2SQL 통합 테스트
|
||||
|
||||
**목표:** 자연어 질문으로 디지털 태그 상태 조회
|
||||
|
||||
**단계:**
|
||||
1. Text-to-SQL 탭에서 "xv-6124 현재 상태 알려줘" 입력
|
||||
2. 생성된 SQL이 v_tag_summary를 사용하는지 확인
|
||||
3. 결과에 instate0~2 값과 state descriptor가 포함되는지 확인
|
||||
|
||||
**기대 결과:**
|
||||
- SQL: `SELECT instate0, instate1, state0_descriptor, state1_descriptor, description FROM v_tag_summary WHERE base_tag = 'xv-6124'`
|
||||
- 결과 테이블에 상태 비트와 의미 표시됨
|
||||
|
||||
### 테스트 체크리스트
|
||||
|
||||
- [ ] xv-6124 포인트 realtime_table에 등록됨
|
||||
- [ ] OPC UA 구독 시 livevalue 업데이트됨
|
||||
- [ ] 메타데이터 재로드 시 tag_metadata에 저장됨
|
||||
- [ ] v_tag_summary 뷰에서 통합 조회 가능
|
||||
- [ ] NL2SQL에서 자연어 질문으로 상태 조회 가능
|
||||
- [ ] 포인트빌더에서 최대 10개 태그 선택 가능
|
||||
188
plans/enum-metadata-optimization.md
Normal file
188
plans/enum-metadata-optimization.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# pvC UA EnumValueType 메타데이터 저장 최적화 제안서
|
||||
|
||||
> 작성일: 2026-05-07
|
||||
> 상태: 분석 중
|
||||
> 목적: xv, p-xxxxx 등 디지털 장비 태그의 pv 값 (i=7594) 저장 방식 최적화
|
||||
|
||||
---
|
||||
|
||||
## 1. 현재 pv 값 저장 방식 분석
|
||||
|
||||
### 1.1 pvC UA EnumValueType (i=7594) 데이터 흐름
|
||||
|
||||
디지털 장비(xv 밸브, p-xxxxx 펌프 등)의 pv(Output) 값은 pvC UA 표준 데이터 타입인 `EnumValueType (i=7594)`를 사용한다. 이 값은 pvC UA 서버에서 읽을 때 `{정수 | DisplayName | }` 형식의 문자열로 반환된다.
|
||||
|
||||
### 1.2 실제 DB 저장 예시
|
||||
|
||||
`realtime_table`에 pv 값이 저장되는 실제 데이터:
|
||||
|
||||
| tagname | livevalue | node_id |
|
||||
|---------|-----------|---------|
|
||||
| xv-3202.pv | `{0 \| L-MID \| }` | ns=1;s=sinamserver:xv-3202.pv |
|
||||
| xv-3202_hs.pv | `{0 \| CLOSE \| }` | ns=1;s=sinamserver:xv-3202_hs.pv |
|
||||
| xv-360.pv | `{0 \| MID \| }` | ns=1;s=sinamserver:xv-360.pv |
|
||||
| p-10602a.pv | `{0 \| L-STpv \| }` | ns=1;s=sinamserver:p-10602a.pv |
|
||||
| p-2816a.pv | `{0 \| L-STpv \| }` | ns=1;s=sinamserver:p-2816a.pv |
|
||||
| p-203_hs.pv | `{0 \| STpv \| }` | ns=1;s=sinamserver:p-203_hs.pv |
|
||||
|
||||
### 1.3 pv 값의 구조
|
||||
|
||||
pv 값 `{0 \| L-STpv \| }`는 다음 3가지 정보를 포함:
|
||||
- **정수 코드 (0)**: 실제 상태 코드 (0~7 범위)
|
||||
- **DisplayName (L-STpv)**: 화면에 표시될 상태 이름
|
||||
- **빈 설명 영역**: 현재 사용되지 않음
|
||||
|
||||
### 1.4 각 태그당 존재하는 속성 수
|
||||
|
||||
`node_map_master`에서 한 태그(예: p-10214)당 **426개 속성**이 존재한다. 주요 속성:
|
||||
- 핵심 데이터: pv, pv, sp, md
|
||||
- 상태 플래그: instate0~instate7
|
||||
- 상태 설명: state0descriptor~state7descriptor
|
||||
- 메타데이터: desc, area
|
||||
- 품질/시간: pvquality, pvlastscannedtime 등
|
||||
- 설정값: state0ondelay, alarmpriority 등
|
||||
- 기타: equipprpverties, templateid, guid 등
|
||||
|
||||
---
|
||||
|
||||
## 2. tag_metadata 테이블 분석
|
||||
|
||||
### 2.1 현재 tag_metadata 테이블 스키마
|
||||
|
||||
```sql
|
||||
CREATE TABLE tag_metadata (
|
||||
id SERIAL PRIMARY KEY,
|
||||
base_tag TEXT NOT NULL,
|
||||
attribute TEXT NOT NULL,
|
||||
value TEXT,
|
||||
node_id TEXT,
|
||||
loaded_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(base_tag, attribute)
|
||||
)
|
||||
```
|
||||
|
||||
### 2.2 MetadataLoaderService가 로드하는 속성
|
||||
|
||||
| 속성 | 데이터 타입 | 설명 |
|
||||
|------|------------|------|
|
||||
| desc | String | 태그 설명 |
|
||||
| area | i=7594 | 소속 플랜트/Asset (EnumValueType) |
|
||||
| state0descriptor | String | 상태 0 설명 |
|
||||
| state1descriptor | String | 상태 1 설명 |
|
||||
| state2descriptor | String | 상태 2 설명 |
|
||||
| state3descriptor | String | 상태 3 설명 |
|
||||
| state4descriptor | String | 상태 4 설명 |
|
||||
| state5descriptor | String | 상태 5 설명 |
|
||||
| state6descriptor | String | 상태 6 설명 |
|
||||
| state7descriptor | String | 상태 7 설명 |
|
||||
|
||||
### 2.3 현재 tag_metadata 테이블 상태
|
||||
|
||||
**테이블이 비어 있음** — 메타데이터 로드가 아직 실행되지 않았거나 실패한 상태.
|
||||
|
||||
### 2.4 문제점: pv 값과 state descriptor의 중복
|
||||
|
||||
pv 값 `{0 | L-STpv | }` 자체가 **상태 코드 + DisplayName**을 포함하고 있다.
|
||||
즉, `state0descriptor` 등의 값을 별도로 저장할 필요 없이 pv 값에서 파싱하면 된다.
|
||||
|
||||
| 데이터 소스 | 포함 정보 |
|
||||
|------------|----------|
|
||||
| `realtime_table`의 pv livevalue | `{0 \| L-STpv \| }` → 코드=0, 이름=L-STpv |
|
||||
| `tag_metadata`의 state0descriptor | "L-STpv" (pv 값과 동일한 정보) |
|
||||
|
||||
**결론: pv 값에 이미 state descriptor 정보가 포함되어 있으므로 중복 저장.**
|
||||
|
||||
---
|
||||
|
||||
## 3. 메타데이터 컬럼 유지 여부 제안
|
||||
|
||||
### 3.1 컬럼 분류
|
||||
|
||||
tag_metadata 테이블에서 각 속성을 3가지 카테고리로 분류:
|
||||
|
||||
| 카테고리 | 속성 | 필요성 | 이유 |
|
||||
|----------|------|--------|------|
|
||||
| **필수 유지** | desc | ⭐ 필수 | 태그 설명은 pv 값에 포함되지 않음 |
|
||||
| **필수 유지** | area | ⭐ 필수 | 소속 플랜트 정보는 pv 값에 포함되지 않음 |
|
||||
| **불필요** | state0~7descriptor | ❌ 중복 | pv 값 `{코드 \| 이름 \| }`에 이미 포함 |
|
||||
|
||||
### 3.2 제안: desc, area만 저장
|
||||
|
||||
**tag_metadata 테이블에는 `desc`와 `area`만 저장하고, `state0~7descriptor`는 저장하지 말 것.**
|
||||
|
||||
#### 근거
|
||||
|
||||
1. **정보 중복 제거**: pv 값 `{0 | L-STpv | }`에서 상태 이름 "L-STpv"을 파싱 가능
|
||||
2. **저장소 절감**: 태그당 8개 state descriptor 행 제거 (10개 → 2개 속성)
|
||||
3. **로드 시간 단축**: pvC UA에서 읽어야 할 속성 10개 → 2개로 감소
|
||||
4. **데이터 일관성**: state descriptor는 pvC 서버 설정에 따라 변경될 수 있음. pv 값이 실시간 상태이므로 더 신뢰할 수 있음
|
||||
|
||||
#### pv 값 파싱 방법
|
||||
|
||||
```
|
||||
pv 값: "{0 | L-STpv | }"
|
||||
파싱: 코드=0, 상태이름="L-STpv"
|
||||
|
||||
pv 값: "{0 | MID | }"
|
||||
파싱: 코드=0, 상태이름="MID"
|
||||
```
|
||||
|
||||
프론트엔드에서 pv 값 표시 시 `{`와 `|`를 파싱하여 상태 이름만 표시하면 됨.
|
||||
|
||||
### 3.3 수정이 필요한 코드
|
||||
|
||||
| 파일 | 수정 내용 |
|
||||
|------|----------|
|
||||
| `MetadataLoaderService.cs` | `MetaAttributes` 배열에서 state0~7descriptor 제거 |
|
||||
| `ExperionDbContext.cs` | `v_tag_summary` 뷰에서 state descriptor JOIN 제거 |
|
||||
| `app.js` | pv 값 표시 시 파싱 로직 추가 |
|
||||
|
||||
### 3.4 tag_metadata 테이블의 node_id, loaded_at 컬럼 유지 여부
|
||||
|
||||
| 컬럼 | 유지 제안 | 이유 |
|
||||
|------|----------|------|
|
||||
| node_id | 유지 | pvC UA 노드 ID는 재로드 시 변경될 수 있으므로 추적 필요 |
|
||||
| loaded_at | 유지 | 메타데이터 갱신 시점 확인에 유용 |
|
||||
|
||||
**결론: node_id와 loaded_at은 유지하는 것이 좋음.**
|
||||
|
||||
---
|
||||
|
||||
## 4. 요약 및 다음 단계
|
||||
|
||||
### 4.1 핵심 발견
|
||||
|
||||
1. **pv 값 저장 방식**: `realtime_table`의 `livevalue`에 `{코드 | DisplayName | }` 형식으로 저장됨
|
||||
2. **정보 중복**: pv 값에 state descriptor 정보가 이미 포함되어 있음
|
||||
3. **tag_metadata 비어있음**: 메타데이터 로드가 아직 실행되지 않은 상태
|
||||
4. **속성 과다**: 각 태그당 426개 속성 중 실제로 필요한 것은 desc, area 2개뿐
|
||||
|
||||
### 4.2 최종 제안
|
||||
|
||||
| 항목 | 결정 |
|
||||
|------|------|
|
||||
| tag_metadata 저장 속성 | desc, area 만 |
|
||||
| state0~7descriptor | 저장하지 않음 (pv 값에서 파싱) |
|
||||
| node_id 컬럼 | 유지 (추적용) |
|
||||
| loaded_at 컬럼 | 유지 (갱신 시점 확인) |
|
||||
|
||||
### 4.3 기대 효과
|
||||
|
||||
- **pvC UA 읽기 요청 80% 감소**: 10개 속성 → 2개 속성
|
||||
- **tag_metadata 행 80% 감소**: 태그당 10행 → 2행
|
||||
- **메타데이터 로드 시간 단축**: 네트워크 트래픽과 DB 삽입량 대폭 감소
|
||||
- **데이터 일관성 향상**: pv 값이 실시간 상태이므로 더 신뢰할 수 있음
|
||||
|
||||
### 4.4 다음 단계
|
||||
1. `MetadataLoaderService.cs` 수정: state descriptor 제거
|
||||
2. `ExperionDbContext.cs` 수정: v_tag_summary 뷰 단순화
|
||||
3. 프론트엔드 pv 값 파싱 로직 추가
|
||||
4. 메타데이터 로드 테스트 실행
|
||||
5. P-XXXXX, XV-XXXXX.pv 등의 상태변화를 감지하여 EVENT TABLE에 기록하여 LLM의 플랜트 상태보고에 활용하게 하고, NL2SQL에서도 언제부터 언제까지의 이벤트 정보를 표시 할 수있게 하자(UI 'Event 조회' 별도 페이지 만들어서 )
|
||||
|
||||
EVENT TABLE은 컬럼 TimeStamp, Tagname, Value, StateText 를 p-xxxxx, xv-xxxxx등의 pv 값{ 값 | 상태 텍스트 | }에서 가져오고 값의 변경이 일어났을때만 , event history 테이블에 기록.
|
||||
|
||||
event 테이블을 아날로그값에도 적용할지는 나중에 생각하고 일단 Brain Storming 기록만
|
||||
- ficq-6113 : pid controller 류 들의 Manual : pv 기준, Auto : sp 기준으로 값이 변경되면 event 히스토리에 변경값 기록 이전값->변경값 두개다 기록
|
||||
- Auto <-> Manual 전환도 기록 : ficq-6113.md 기준 -> 실시간 테이블 스키마 변경
|
||||
- user 전환 (근무교대 시프트 기록) ---> 이건 아직 파악 못했슴 (node_map_master 에 정보 없슴)
|
||||
443
plans/enum-metadata-optimize-coding-plan.md
Normal file
443
plans/enum-metadata-optimize-coding-plan.md
Normal file
@@ -0,0 +1,443 @@
|
||||
# Enum Metadata 최적화 - 코딩 계획
|
||||
|
||||
> 작성일: 2026-05-08
|
||||
> 상태: 진행 중
|
||||
> 기반 문서: [`plans/enum-metadata-optimization.md`](plans/enum-metadata-optimization.md)
|
||||
> 목적: `state0~7descriptor` 제거, `desc`/`area`만 유지, pv 값 파싱 로직 추가
|
||||
|
||||
---
|
||||
|
||||
## 작업 Todo 리스트
|
||||
|
||||
각 단계는 독립적으로 완료 여부를 추적할 수 있다. 체크박스를 사용하여 진행 상황을 기록한다.
|
||||
|
||||
- [ ] STEP 1 — 백업: 수정 대상 파일들을 `.rooBackup/`에 백업
|
||||
- [ ] STEP 2 — `MetadataLoaderService.cs`: `MetaAttributes` 배열에서 state0~7descriptor 제거
|
||||
- [ ] STEP 3 — `MetadataLoaderService.cs`: 빌드 검증
|
||||
- [ ] STEP 4 — `ExperionDbContext.cs`: `v_tag_summary` 뷰에서 state descriptor JOIN 제거
|
||||
- [ ] STEP 5 — `ExperionDbContext.cs`: 빌드 검증
|
||||
- [ ] STEP 6 — `app.js`: pv 값 파싱 헬퍼 함수 `parseEnumPv()` 추가
|
||||
- [ ] STEP 7 — `app.js`: pv 값 표시 관련 코드 모두 `parseEnumPv()` 적용
|
||||
- [ ] STEP 8 — End-to-End 검증: 전체 빌드 + UI 테스트
|
||||
- [ ] STEP 9 — git 커밋 및 정리
|
||||
|
||||
---
|
||||
|
||||
## 변경 대상 파일 목록
|
||||
|
||||
| # | 파일 | 변경 내용 | 영향 범위 |
|
||||
|---|------|-----------|-----------|
|
||||
| 1 | `src/Infrastructure/OpcUa/MetadataLoaderService.cs` | `MetaAttributes` 배열 수정 | 메타데이터 로딩 |
|
||||
| 2 | `src/Infrastructure/Database/ExperionDbContext.cs` | `v_tag_summary` 뷰 단순화 | DB 뷰 |
|
||||
| 3 | `src/Web/wwwroot/js/app.js` | pv 파싱 헬퍼 + 표시 로직 변경 | 프론트엔드 UI |
|
||||
|
||||
---
|
||||
|
||||
## 각 단계 상세 계획
|
||||
|
||||
---
|
||||
|
||||
### STEP 1 — 백업: 수정 대상 파일들을 `.rooBackup/`에 백업
|
||||
|
||||
**목적**: 수정 전 원본 보존. 실패 시 복원 가능.
|
||||
|
||||
**실행 명령**:
|
||||
```bash
|
||||
TIMESTAMP=$(date +%Y%m%d%H%M)
|
||||
mkdir -p .rooBackup/enum-opt-$TIMESTAMP/src/Infrastructure/OpcUa
|
||||
mkdir -p .rooBackup/enum-opt-$TIMESTAMP/src/Infrastructure/Database
|
||||
mkdir -p .rooBackup/enum-opt-$TIMESTAMP/src/Web/wwwroot/js
|
||||
|
||||
cp src/Infrastructure/OpcUa/MetadataLoaderService.cs .rooBackup/enum-opt-$TIMESTAMP/src/Infrastructure/OpcUa/
|
||||
cp src/Infrastructure/Database/ExperionDbContext.cs .rooBackup/enum-opt-$TIMESTAMP/src/Infrastructure/Database/
|
||||
cp src/Web/wwwroot/js/app.js .rooBackup/enum-opt-$TIMESTAMP/src/Web/wwwroot/js/
|
||||
```
|
||||
|
||||
**검증 기준**:
|
||||
- [ ] `.rooBackup/enum-opt-YYYYMMDDHHMM/` 폴더가 생성되고 3개 파일이 복사됨
|
||||
- [ ] 원본 파일과 백업 파일의 체크섬이 일치함 (`md5sum` 비교)
|
||||
|
||||
**enum-metadata-optimization.md 규칙 매핑**:
|
||||
- 섹션 3.3: 수정이 필요한 코드 — `MetadataLoaderService.cs`, `ExperionDbContext.cs`, `app.js`
|
||||
|
||||
---
|
||||
|
||||
### STEP 2 — `MetadataLoaderService.cs`: `MetaAttributes` 배열에서 state0~7descriptor 제거
|
||||
|
||||
**파일**: [`MetadataLoaderService.cs`](src/Infrastructure/OpcUa/MetadataLoaderService.cs:20)
|
||||
**변경 위치**: 20~26줄 (`MetaAttributes` 배열 정의)
|
||||
|
||||
**변경 전 코드**:
|
||||
```csharp
|
||||
// 로드할 메타데이터 속성 목록
|
||||
private static readonly string[] MetaAttributes =
|
||||
{
|
||||
"desc", "area",
|
||||
"state0descriptor", "state1descriptor", "state2descriptor",
|
||||
"state3descriptor", "state4descriptor", "state5descriptor",
|
||||
"state6descriptor", "state7descriptor"
|
||||
};
|
||||
```
|
||||
|
||||
**변경 후 코드**:
|
||||
```csharp
|
||||
// 로드할 메타데이터 속성 목록 (state0~7descriptor 제거 — pv 값에서 파싱)
|
||||
private static readonly string[] MetaAttributes =
|
||||
{
|
||||
"desc", "area"
|
||||
};
|
||||
```
|
||||
|
||||
**diff**:
|
||||
```diff
|
||||
-// 로드할 메타데이터 속성 목록
|
||||
+// 로드할 메타데이터 속성 목록 (state0~7descriptor 제거 — pv 값에서 파싱)
|
||||
private static readonly string[] MetaAttributes =
|
||||
{
|
||||
- "desc", "area",
|
||||
- "state0descriptor", "state1descriptor", "state2descriptor",
|
||||
- "state3descriptor", "state4descriptor", "state5descriptor",
|
||||
- "state6descriptor", "state7descriptor"
|
||||
+ "desc", "area"
|
||||
};
|
||||
```
|
||||
|
||||
**변경 이유**:
|
||||
- pv 값 `{코드 | DisplayName | }`에 이미 state descriptor 정보가 포함되어 있으므로 중복 저장 제거
|
||||
- OPC UA 읽기 요청 80% 감소 (10개 속성 → 2개 속성)
|
||||
- tag_metadata 행 80% 감소 (태그당 10행 → 2행)
|
||||
|
||||
**영향 분석**:
|
||||
- `LoadMetadataAsync()` 메서드: `MetaAttributes.Contains(n.Name)`으로 `node_map_master`에서 필터링하므로 state descriptor 노드는 더 이상 조회되지 않음
|
||||
- `ReadTagsAsync()`: 8개 menos의 nodeId로 배치 읽기 → 네트워크 트래픽 감소
|
||||
- UPSERT 쿼리: 8개 menos의 행 삽입 → DB 부하 감소
|
||||
|
||||
**검증 기준**:
|
||||
- [ ] `MetaAttributes` 배열이 `["desc", "area"]` 두 개만 포함
|
||||
- [ ] 컴파일 오류 없음
|
||||
- [ ] `LoadMetadataAsync()` 메서드의 로직 변경 없이 배열 변경만으로 동작
|
||||
|
||||
**enum-metadata-optimization.md 규칙 매핑**:
|
||||
- 섹션 3.2: desc, area만 저장
|
||||
- 섹션 3.3: `MetadataLoaderService.cs` 수정 — `MetaAttributes` 배열에서 state0~7descriptor 제거
|
||||
- 섹션 4.2: tag_metadata 저장 속성 — desc, area 만
|
||||
|
||||
---
|
||||
|
||||
### STEP 3 — `MetadataLoaderService.cs` 변경 후 빌드 검증
|
||||
|
||||
**목적**: STEP 2 변경이 컴파일 오류 없이 통과하는지 확인
|
||||
|
||||
**실행 명령**:
|
||||
```bash
|
||||
dotnet build src/Web/ExperionCrawler.csproj --no-restore -v q
|
||||
```
|
||||
|
||||
**검증 기준**:
|
||||
- [ ] 빌드 성공 (exit code 0)
|
||||
- [ ] 경고 없음 (또는 기존 경고만)
|
||||
- [ ] `MetaAttributes` 배열 참조하는 다른 코드가 없는지 확인
|
||||
|
||||
**실패 시 대응**:
|
||||
- 컴파일 오류가 발생하면 오류 메시지를 읽고 원인 분석
|
||||
- `MetaAttributes` 길이에 의존하는 코드가 있으면 해당 코드도 수정
|
||||
|
||||
---
|
||||
|
||||
### STEP 4 — `ExperionDbContext.cs`: `v_tag_summary` 뷰에서 state descriptor JOIN 제거
|
||||
|
||||
**파일**: [`ExperionDbContext.cs`](src/Infrastructure/Database/ExperionDbContext.cs:302)
|
||||
**변경 위치**: 302~330줄 (`v_tag_summary` 뷰 생성 SQL)
|
||||
|
||||
**변경 전 코드** (315~329줄):
|
||||
```sql
|
||||
desc_md.value AS description,
|
||||
area_md.value AS area,
|
||||
s0d_md.value AS state0_descriptor,
|
||||
s1d_md.value AS state1_descriptor,
|
||||
s2d_md.value AS state2_descriptor
|
||||
FROM (SELECT DISTINCT split_part(tagname, '.', 1) AS base_tag FROM realtime_table) rt_base
|
||||
LEFT JOIN realtime_table pv_rt ON pv_rt.tagname = rt_base.base_tag || '.pv'
|
||||
LEFT JOIN realtime_table sp_rt ON sp_rt.tagname = rt_base.base_tag || '.sp'
|
||||
LEFT JOIN realtime_table op_rt ON op_rt.tagname = rt_base.base_tag || '.op'
|
||||
LEFT JOIN realtime_table instate0_rt ON instate0_rt.tagname = rt_base.base_tag || '.instate0'
|
||||
LEFT JOIN realtime_table instate1_rt ON instate1_rt.tagname = rt_base.base_tag || '.instate1'
|
||||
LEFT JOIN realtime_table instate2_rt ON instate2_rt.tagname = rt_base.base_tag || '.instate2'
|
||||
LEFT JOIN tag_metadata desc_md ON desc_md.base_tag = rt_base.base_tag AND desc_md.attribute = 'desc'
|
||||
LEFT JOIN tag_metadata area_md ON area_md.base_tag = rt_base.base_tag AND area_md.attribute = 'area'
|
||||
LEFT JOIN tag_metadata s0d_md ON s0d_md.base_tag = rt_base.base_tag AND s0d_md.attribute = 'state0descriptor'
|
||||
LEFT JOIN tag_metadata s1d_md ON s1d_md.base_tag = rt_base.base_tag AND s1d_md.attribute = 'state1descriptor'
|
||||
LEFT JOIN tag_metadata s2d_md ON s2d_md.base_tag = rt_base.base_tag AND s2d_md.attribute = 'state2descriptor'
|
||||
```
|
||||
|
||||
**변경 후 코드**:
|
||||
```sql
|
||||
desc_md.value AS description,
|
||||
area_md.value AS area
|
||||
FROM (SELECT DISTINCT split_part(tagname, '.', 1) AS base_tag FROM realtime_table) rt_base
|
||||
LEFT JOIN realtime_table pv_rt ON pv_rt.tagname = rt_base.base_tag || '.pv'
|
||||
LEFT JOIN realtime_table sp_rt ON sp_rt.tagname = rt_base.base_tag || '.sp'
|
||||
LEFT JOIN realtime_table op_rt ON op_rt.tagname = rt_base.base_tag || '.op'
|
||||
LEFT JOIN realtime_table instate0_rt ON instate0_rt.tagname = rt_base.base_tag || '.instate0'
|
||||
LEFT JOIN realtime_table instate1_rt ON instate1_rt.tagname = rt_base.base_tag || '.instate1'
|
||||
LEFT JOIN realtime_table instate2_rt ON instate2_rt.tagname = rt_base.base_tag || '.instate2'
|
||||
LEFT JOIN tag_metadata desc_md ON desc_md.base_tag = rt_base.base_tag AND desc_md.attribute = 'desc'
|
||||
LEFT JOIN tag_metadata area_md ON area_md.base_tag = rt_base.base_tag AND area_md.attribute = 'area'
|
||||
```
|
||||
|
||||
**diff**:
|
||||
```diff
|
||||
desc_md.value AS description,
|
||||
area_md.value AS area,
|
||||
- s0d_md.value AS state0_descriptor,
|
||||
- s1d_md.value AS state1_descriptor,
|
||||
- s2d_md.value AS state2_descriptor
|
||||
+ area_md.value AS area
|
||||
FROM (SELECT DISTINCT split_part(tagname, '.', 1) AS base_tag FROM realtime_table) rt_base
|
||||
LEFT JOIN realtime_table pv_rt ON pv_rt.tagname = rt_base.base_tag || '.pv'
|
||||
LEFT JOIN realtime_table sp_rt ON sp_rt.tagname = rt_base.base_tag || '.sp'
|
||||
LEFT JOIN realtime_table op_rt ON op_rt.tagname = rt_base.base_tag || '.op'
|
||||
LEFT JOIN realtime_table instate0_rt ON instate0_rt.tagname = rt_base.base_tag || '.instate0'
|
||||
LEFT JOIN realtime_table instate1_rt ON instate1_rt.tagname = rt_base.base_tag || '.instate1'
|
||||
LEFT JOIN realtime_table instate2_rt ON instate2_rt.tagname = rt_base.base_tag || '.instate2'
|
||||
LEFT JOIN tag_metadata desc_md ON desc_md.base_tag = rt_base.base_tag AND desc_md.attribute = 'desc'
|
||||
LEFT JOIN tag_metadata area_md ON area_md.base_tag = rt_base.base_tag AND area_md.attribute = 'area'
|
||||
- LEFT JOIN tag_metadata s0d_md ON s0d_md.base_tag = rt_base.base_tag AND s0d_md.attribute = 'state0descriptor'
|
||||
- LEFT JOIN tag_metadata s1d_md ON s1d_md.base_tag = rt_base.base_tag AND s1d_md.attribute = 'state1descriptor'
|
||||
- LEFT JOIN tag_metadata s2d_md ON s2d_md.base_tag = rt_base.base_tag AND s2d_md.attribute = 'state2descriptor'
|
||||
""");
|
||||
```
|
||||
|
||||
**변경 이유**:
|
||||
- state0~2descriptor가 더 이상 tag_metadata에 저장되지 않으므로 JOIN 제거
|
||||
- 뷰 조회 성능 향상 (3개 LEFT JOIN 제거)
|
||||
|
||||
**영향 분석**:
|
||||
- `v_tag_summary` 뷰를 조회하는 코드가 `state0_descriptor`, `state1_descriptor`, `state2_descriptor` 컬럼을 참조하면 NULL 반환
|
||||
- 이 뷰를 사용하는 곳이 있는지 검색 필요 (현재는 DB 초기화 시에만 사용)
|
||||
|
||||
**검증 기준**:
|
||||
- [ ] `v_tag_summary` 뷰 SQL에서 state descriptor 관련 JOIN 3개 제거됨
|
||||
- [ ] `area_md.value AS area` 뒤 쉼표 제거 (마지막 SELECT 컬럼)
|
||||
- [ ] SQL 문법 오류 없음
|
||||
|
||||
**enum-metadata-optimization.md 규칙 매핑**:
|
||||
- 섹션 3.3: `ExperionDbContext.cs` 수정 — v_tag_summary 뷰에서 state descriptor JOIN 제거
|
||||
|
||||
---
|
||||
|
||||
### STEP 5 — `ExperionDbContext.cs` 변경 후 빌드 검증
|
||||
|
||||
**목적**: STEP 4 변경이 컴파일 오류 없이 통과하는지 확인
|
||||
|
||||
**실행 명령**:
|
||||
```bash
|
||||
dotnet build src/Web/ExperionCrawler.csproj --no-restore -v q
|
||||
```
|
||||
|
||||
**검증 기준**:
|
||||
- [ ] 빌드 성공 (exit code 0)
|
||||
- [ ] SQL 문자열 문법 오류 없음 (raw string literal 안에서 SQL 구문 확인)
|
||||
- [ ] 쉼표 누락/과잉 없음
|
||||
|
||||
**실패 시 대응**:
|
||||
- SQL 구문 오류가 발생하면 SELECT 컬럼 목록의 쉼표 확인
|
||||
- `area_md.value AS area`가 마지막 컬럼이므로 쉼표 제거했는지 확인
|
||||
|
||||
---
|
||||
|
||||
### STEP 6 — `app.js`: pv 값 파싱 헬퍼 함수 `parseEnumPv()` 추가
|
||||
|
||||
**파일**: [`app.js`](src/Web/wwwroot/js/app.js:2126)
|
||||
**삽입 위치**: `fmtVal()` 함수 바로 아래 (2132줄 이후)
|
||||
|
||||
**추가할 코드**:
|
||||
```javascript
|
||||
/**
|
||||
* OPC UA EnumValueType pv 값 파싱
|
||||
* "{0 | L-STpv | }" → "L-STpv"
|
||||
* "{0 | MID | }" → "MID"
|
||||
* 일반 값은 그대로 반환
|
||||
*/
|
||||
function parseEnumPv(v) {
|
||||
if (v == null) return v;
|
||||
const s = String(v);
|
||||
// "{코드 | DisplayName | }" 패턴 매칭
|
||||
const m = s.match(/^\{\s*\d+\s*\|\s*([^|]+?)\s*\|\s*\}$/);
|
||||
return m ? m[1].trim() : s;
|
||||
}
|
||||
```
|
||||
|
||||
**삽입 위치 상세**:
|
||||
- 2132줄 (`}` — `fmtVal` 함수 종료) 바로 다음에 삽입
|
||||
- 기존 `fmtVal` 함수는 변경하지 않음
|
||||
|
||||
**기능 설명**:
|
||||
- 입력: pv 값 문자열 (예: `"{0 | L-STpv | }"`)
|
||||
- 출력: DisplayName 부분만 (예: `"L-STpv"`)
|
||||
- EnumValueType 형식이 아닌 일반 값은 그대로 반환
|
||||
|
||||
**정규식 설명**:
|
||||
- `^\{` — `{`로 시작
|
||||
- `\s*\d+\s*` — 정수 코드 (공백 허용)
|
||||
- `\|\s*` — `|` 구분자
|
||||
- `([^|]+?)` — DisplayName (첫 번째 캡처 그룹)
|
||||
- `\s*\|\s*\}$` — `| }`로 끝남
|
||||
|
||||
**검증 기준**:
|
||||
- [ ] `parseEnumPv("{0 | L-STpv | }")` → `"L-STpv"` 반환
|
||||
- [ ] `parseEnumPv("{0 | MID | }")` → `"MID"` 반환
|
||||
- [ ] `parseEnumPv("123.45")` → `"123.45"` 반환 (변경 없음)
|
||||
- [ ] `parseEnumPv(null)` → `null` 반환
|
||||
- [ ] 브라우저 콘솔에서 수동 테스트 가능
|
||||
|
||||
**enum-metadata-optimization.md 규칙 매핑**:
|
||||
- 섹션 3.2: pv 값 파싱 방법 — `{코드 | 이름 | }`에서 DisplayName 추출
|
||||
- 섹션 3.3: `app.js` — pv 값 표시 시 파싱 로직 추가
|
||||
|
||||
---
|
||||
|
||||
### STEP 7 — `app.js`: pv 값 표시 관련 코드 모두 `parseEnumPv()` 적용
|
||||
|
||||
**파일**: [`app.js`](src/Web/wwwroot/js/app.js:633)
|
||||
**변경 위치**: `pbRender()` 함수 내 liveValue 표시 부분 (633줄)
|
||||
|
||||
**변경 전 코드** (633줄):
|
||||
```javascript
|
||||
<td class="val">${p?.liveValue != null ? esc(String(fmtVal(p.liveValue))) : '<span style="color:var(--t3)">—</span>'}</td>
|
||||
```
|
||||
|
||||
**변경 후 코드**:
|
||||
```javascript
|
||||
<td class="val">${p?.liveValue != null ? esc(String(fmtVal(parseEnumPv(p.liveValue)))) : '<span style="color:var(--t3)">—</span>'}</td>
|
||||
```
|
||||
|
||||
**diff**:
|
||||
```diff
|
||||
-<td class="val">${p?.liveValue != null ? esc(String(fmtVal(p.liveValue))) : '<span style="color:var(--t3)">—</span>'}</td>
|
||||
+<td class="val">${p?.liveValue != null ? esc(String(fmtVal(parseEnumPv(p.liveValue)))) : '<span style="color:var(--t3)">—</span>'}</td>
|
||||
```
|
||||
|
||||
**변경 이유**:
|
||||
- `liveValue`가 EnumValueType 형식(`{0 | L-STpv | }`)이면 DisplayName만 표시
|
||||
- 일반 숫자 값은 `parseEnumPv()`가 그대로 반환하므로 영향 없음
|
||||
- `fmtVal()`은 숫자 포맷팅만 담당, `parseEnumPv()`는 EnumValueType 파싱만 담당 (단일 책임)
|
||||
|
||||
**다른 적용 위치 확인**:
|
||||
- `fmtVal()`을 사용하는 모든 곳을 검색하여 pv 값 표시에 사용되는 곳에 `parseEnumPv()` 적용
|
||||
- `t2sRenderTable()` (1606줄): `fmtVal(val)` — 시계열 데이터이므로 변경 불필요 (아날로그 값 중심)
|
||||
- `histQuery()` 결과 렌더링 (956줄): `fmtVal(raw)` — 이력 데이터이므로 변경 불필요
|
||||
|
||||
**검증 기준**:
|
||||
- [ ] 포인트빌더 테이블에서 xv-3202.pv 값이 `"{0 | L-MID | }"` 대신 `"L-MID"`로 표시됨
|
||||
- [ ] 일반 숫자 값(예: ficq-6113.pv = "123.45")은 여전히 `"123.45"`로 표시됨
|
||||
- [ ] 브라우저 콘솔에서 JS 오류 없음
|
||||
|
||||
**enum-metadata-optimization.md 규칙 매핑**:
|
||||
- 섹션 3.2: 프론트엔드에서 pv 값 표시 시 `{`와 `|`를 파싱하여 상태 이름만 표시
|
||||
|
||||
---
|
||||
|
||||
### STEP 8 — End-to-End 검증: 전체 빌드 + UI 테스트
|
||||
|
||||
**목적**: 모든 변경 사항이 함께 동작하는지 최종 확인
|
||||
|
||||
**실행 명령**:
|
||||
```bash
|
||||
# 1. 전체 솔루션 빌드
|
||||
dotnet build ExperionCrawler.sln -v q
|
||||
|
||||
# 2. 애플리케이션 시작
|
||||
dotnet run --project src/Web/ExperionCrawler.csproj
|
||||
```
|
||||
|
||||
**검증 체크리스트**:
|
||||
|
||||
#### 빌드 검증
|
||||
- [ ] `dotnet build` 성공 (exit code 0)
|
||||
- [ ] 컴파일 오류 0개
|
||||
- [ ] 경고 수 증가 없음
|
||||
|
||||
#### 백엔드 검증
|
||||
- [ ] 애플리케이션 시작 시 DB 초기화 성공
|
||||
- [ ] `v_tag_summary` 뷰 생성 성공 (state descriptor JOIN 없음)
|
||||
- [ ] 메타데이터 로드 시 `desc`, `area`만 조회됨 (로그 확인)
|
||||
- [ ] `tag_metadata` 테이블에 state0~7descriptor 행 없음
|
||||
|
||||
#### 프론트엔드 검증
|
||||
- [ ] 브라우저 콘솔 JS 오류 없음
|
||||
- [ ] 포인트빌더 테이블에서 digital 태그 pv 값이 DisplayName만 표시됨
|
||||
- 예: `xv-3202.pv` → `"L-MID"` (기존: `"{0 | L-MID | }"`)
|
||||
- [ ] 아날로그 태그 pv 값은 정상 표시됨
|
||||
- 예: `ficq-6113.pv` → `"123.45"`
|
||||
- [ ] 메타데이터 조회 시 `desc`, `area`만 반환됨
|
||||
|
||||
#### 성능 검증
|
||||
- [ ] 메타데이터 로드 시간 감소 확인 (기존 대비)
|
||||
- [ ] `tag_metadata` 테이블 행 수 감소 확인 (기존 10행/태그 → 2행/태그)
|
||||
|
||||
**enum-metadata-optimization.md 규칙 매핑**:
|
||||
- 섹션 4.3: 기대 효과 — OPC UA 읽기 요청 80% 감소, tag_metadata 행 80% 감소
|
||||
- 섹션 4.4: 다음 단계 — 메타데이터 로드 테스트 실행
|
||||
|
||||
---
|
||||
|
||||
### STEP 9 — git 커밋 및 정리
|
||||
|
||||
**목적**: 변경 사항을 커밋하고 작업 기록 남기기
|
||||
|
||||
**커밋 전략**: 각 파일 변경을 별도 커밋으로 관리
|
||||
|
||||
```bash
|
||||
# 1. MetadataLoaderService.cs 커밋
|
||||
git add src/Infrastructure/OpcUa/MetadataLoaderService.cs
|
||||
git commit -m "feat: MetaAttributes에서 state0~7descriptor 제거 (pv 값 파싱으로 대체)"
|
||||
|
||||
# 2. ExperionDbContext.cs 커밋
|
||||
git add src/Infrastructure/Database/ExperionDbContext.cs
|
||||
git commit -m "feat: v_tag_summary 뷰에서 state descriptor JOIN 제거"
|
||||
|
||||
# 3. app.js 커밋
|
||||
git add src/Web/wwwroot/js/app.js
|
||||
git commit -m "feat: pv 값 파싱 헬퍼 parseEnumPv() 추가, 포인트빌더 테이블 적용"
|
||||
|
||||
# 4. 계획 문서 커밋
|
||||
git add plans/enum-metadata-optimize-coding-plan.md
|
||||
git commit -m "docs: enum metadata 최적화 코딩 계획 작성"
|
||||
```
|
||||
|
||||
**검증 기준**:
|
||||
- [ ] 각 커밋 메시지가 변경 내용을 명확히 설명함
|
||||
- [ ] `git log`에서 커밋 순서가 논리적임
|
||||
- [ ] `.rooBackup/` 폴더에 gitignore 적용되어 있음 (백업 파일 커밋 제외)
|
||||
|
||||
---
|
||||
|
||||
## 부록: rollback 절차
|
||||
|
||||
작업 중 문제가 발생하면 백업 파일로 복원:
|
||||
|
||||
```bash
|
||||
TIMESTAMP=$(ls -d .rooBackup/enum-opt-* | tail -1 | xargs basename)
|
||||
cp .rooBackup/$TIMESTAMP/src/Infrastructure/OpcUa/MetadataLoaderService.cs src/Infrastructure/OpcUa/
|
||||
cp .rooBackup/$TIMESTAMP/src/Infrastructure/Database/ExperionDbContext.cs src/Infrastructure/Database/
|
||||
cp .rooBackup/$TIMESTAMP/src/Web/wwwroot/js/app.js src/Web/wwwroot/js/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 검증 요약
|
||||
|
||||
각 STEP의 검증 기준을 한눈에 확인:
|
||||
|
||||
| STEP | 파일 | 핵심 검증 항목 |
|
||||
|------|------|----------------|
|
||||
| 1 | — | 백업 파일 3개 생성됨 |
|
||||
| 2 | MetadataLoaderService.cs | `MetaAttributes` = `["desc", "area"]` |
|
||||
| 3 | — | 빌드 성공 |
|
||||
| 4 | ExperionDbContext.cs | state descriptor JOIN 3개 제거됨 |
|
||||
| 5 | — | 빌드 성공 |
|
||||
| 6 | app.js | `parseEnumPv()` 함수 추가됨 |
|
||||
| 7 | app.js | `pbRender()`에서 `parseEnumPv()` 적용됨 |
|
||||
| 8 | 전체 | End-to-End 테스트 통과 |
|
||||
| 9 | — | git 커밋 완료 |
|
||||
Binary file not shown.
2565
plans/hc900/c3-all modbus partition.csv
Normal file
2565
plans/hc900/c3-all modbus partition.csv
Normal file
File diff suppressed because it is too large
Load Diff
110
plans/hc900/loop1-analysis.md
Normal file
110
plans/hc900/loop1-analysis.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# loop1.cde 파일 분석 보고서
|
||||
|
||||
## 1. 파일 기본 정보
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| 파일 경로 | `plans/hc900/loop1.cde` |
|
||||
| 파일 크기 | 593,557 바이트 (~580KB) |
|
||||
| 파일 형식 | 바이너리 (Honeywell proprietary) |
|
||||
| 추출 가능한 문자열 | 198 줄 |
|
||||
|
||||
## 2. 파일 형식 식별
|
||||
|
||||
`.cde` 파일은 **Honeywell Experion HS / Foxboro I/A Series**의 **Control Definition (제어 정의)** 파일입니다.
|
||||
HC900 컨트롤러의 제어 루프 구성 정보를 포함하는 바이너리 포맷입니다.
|
||||
|
||||
### 헤더 구조 (초기 바이트 분석)
|
||||
|
||||
```
|
||||
Offset 0x0000: 0202 2000 4800 0b00 ... (파일 마직 / 버전)
|
||||
Offset 0x0018: 0001 006c (식별자: 0x6c01)
|
||||
Offset 0x001C: 0200 532b (Unicode "S+")
|
||||
Offset 0x0088: 69f7 21e4 69f7 2282 (타임스탬프 또는 체크섬)
|
||||
Offset 0x0098: 0100 484a 0032 0063 (Unicode "HJ" + "2c")
|
||||
```
|
||||
|
||||
## 3. 추출된 핵심 내용
|
||||
|
||||
### 3.1 제어 루프 정보
|
||||
|
||||
| 태그명 | 설명 |
|
||||
|--------|------|
|
||||
| `PID102` | PID 컨트롤러 블록 이름 |
|
||||
| `PID102_WSP` | PID 컨트롤러 설정값 (Work Setpoint) |
|
||||
|
||||
### 3.2 컨트롤러 구성 요소
|
||||
|
||||
추출된 문자열에서 확인된 구성 영역:
|
||||
|
||||
| 영역 | 원문 | 설명 |
|
||||
|------|------|------|
|
||||
| CONFIG | CONFIG | 컨트롤러 일반 설정 |
|
||||
| PROFIL | PROFIL | 프로파일 (운전 모드별 설정) |
|
||||
| RECIPES | RECIPES | 레시피 관리 |
|
||||
| SCHEDL | SCHEDL | 스케줄러 |
|
||||
| SEQUEN | SEQUEN | 시퀀서 |
|
||||
| STORAG | STORAG | 데이터 저장소 |
|
||||
| FILE | FILE (x19) | 파일 관리 블록 |
|
||||
|
||||
### 3.3 기타 설정
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| 컨트롤러 | CONTROLLER |
|
||||
| 프로세스 구성 | Process Configuration#1 |
|
||||
| 통신 | Custom Modbus Map |
|
||||
| 파티션 | Undefined Partition |
|
||||
| 신호 단위 | mA (4-20mA 아날로그 신호) |
|
||||
|
||||
## 4. 파일 구조 추정
|
||||
|
||||
```
|
||||
loop1.cde (593,557 bytes)
|
||||
├── Header (0x000 - 0x0FF) : 파일 마직, 버전, 메타데이터
|
||||
├── Block Definitions (0x100 - 0x31F) : 블록 정의 영역
|
||||
├── Tag Index (0x320 - 0x4FF) : 태그 인덱스 (e.(, e.) 등 순차적 인덱스)
|
||||
├── Configuration Data : CONFIG, PROFIL, RECIPES 등 구성 데이터
|
||||
├── Controller Properties : CONTROLLER, Process Configuration#1
|
||||
├── Communication Settings : Custom Modbus Map
|
||||
└── Padding/Reserved : 나머지 영역
|
||||
```
|
||||
|
||||
## 5. 분석 가능성 평가
|
||||
|
||||
### 가능한 분석
|
||||
- ✅ 파일 형식 식별 (Honeywell Control Definition 파일)
|
||||
- ✅ 주요 태그명 추출 (PID102, PID102_WSP)
|
||||
- ✅ 구성 영역 식별 (CONFIG, PROFIL, RECIPES, SCHEDL, SEQUEN, STORAG)
|
||||
- ✅ 컨트롤러 정보 확인 (Process Configuration#1, Custom Modbus Map)
|
||||
|
||||
### 제한사항
|
||||
- ❌ **완전한 구조 파싱 불가**: Honeywell proprietary 바이너리 포맷으로 공식 스펙이 공개되지 않음
|
||||
- ❌ **PID 파라미터 추출 불가**: Kp, Ki, Kd 등 제어 게인 값은 바이너리 인코딩되어 있음
|
||||
- ❌ **블록 연결 관계 파악 불가**: 신호 흐름/와이어링 정보는 구조화되어 있음
|
||||
- ❌ **모든 태그 목록 추출 불가**: 198줄의 문자열만 추출 가능 (전체 데이터의 일부)
|
||||
|
||||
## 6. 완전 파싱을 위한 방안
|
||||
|
||||
### 옵션 1: Honeywell 공식 도구 사용
|
||||
- **System Management Application (SMA)**: Honeywell Experion HS 표준 관리 도구
|
||||
- **Experion PKS Builder**: 제어 루프 구성을 시각적으로 확인 가능
|
||||
- **CDE Export 기능**: SMA에서 XML/CSV로 내보내기 가능
|
||||
|
||||
### 옵션 2: 바이너리 파서 개발
|
||||
- 파일 구조를 reverse engineering 하여 파서 작성 필요
|
||||
- 동일한 HC900 시스템에서 추출한 여러 CDE 파일을 비교 분석
|
||||
- Honeywell 문서 (Experion HS R530 HTM) 에서 힌트 수집
|
||||
|
||||
### 옵션 3: OPC UA를 통한 실시간 접근
|
||||
- 이미 구축된 ExperionCrawler를 통해 OPC UA로 실시간 태그 값 접근
|
||||
- CDE 파일 대신 OPC UA Address Space에서 메타데이터 조회
|
||||
- 더 신뢰성 있는 데이터 소스
|
||||
|
||||
## 7. 결론
|
||||
|
||||
`loop1.cde` 파일은 **Honeywell HC900 컨트롤러의 제어 루프 정의 파일**로, PID102 컨트롤러 블록과 관련 설정을 포함합니다.
|
||||
|
||||
바이너리 포맷의 특성상 `strings` 명령어로 일부 텍스트만 추출 가능하고, 완전한 파싱은 Honeywell 공식 도구가 필요합니다.
|
||||
|
||||
ExperionCrawler 프로젝트에서는 이 파일 대신 **OPC UA를 통한 실시간 데이터 접근**이 더 실용적인 접근 방식입니다.
|
||||
BIN
plans/hc900/loop1.cde
Normal file
BIN
plans/hc900/loop1.cde
Normal file
Binary file not shown.
78
plans/hc900/modbus-scan-time-analysis.md
Normal file
78
plans/hc900/modbus-scan-time-analysis.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# HC900 Modbus TCP 스캔 시간 분석
|
||||
|
||||
## 1. 측정 환경
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| 대상 | HC900 C3 컨트롤러 (192.168.0.240) |
|
||||
| 네트워크 | 100Mbps LAN |
|
||||
| Ping RTT | 최소 0ms, 최대 1ms, 평균 0ms |
|
||||
| 바이트 순서 | Big Endian (HC900 기본) |
|
||||
| 통신 프로토콜 | Modbus TCP (FC03 Read Holding Registers) |
|
||||
|
||||
## 2. 데이터 양
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| 총 태그 항목 | 2,554개 |
|
||||
| float 32 (4바이트) | 2,320개 → 4,640 레지스터 |
|
||||
| unsigned 16 (2바이트) | 234개 → 234 레지스터 |
|
||||
| **총 레지스터** | **4,874개** |
|
||||
| 총 바이트 | 9,748 bytes |
|
||||
| 주소 범위 | 0x0001 ~ 0x7FFF (32,767 스팬) |
|
||||
| 주소 갭 | 273개 (불연속) |
|
||||
|
||||
## 3. Modbus TCP 읽기 요청 수
|
||||
|
||||
- FC03 최대 125 레지스터/요청
|
||||
- 불연속 주소로 인해 **최소 290회** 읽기 요청 필요
|
||||
|
||||
### Partition별 읽기 횟수
|
||||
|
||||
| Partition | 항목 수 | 주소 스팬 | 읽기 횟수 |
|
||||
|-----------|---------|----------|----------|
|
||||
| Loops 1-24 | 960 | 6,079 | 192회 |
|
||||
| Loops 25-32 | 329 | 1,983 | 65회 |
|
||||
| Signal Tags 1-1000 | 549 | 1,125 | 11회 |
|
||||
| Signal Tags 1-4000 | 549 | 1,125 | 11회 |
|
||||
| Variables 1-600 | 157 | 337 | 9회 |
|
||||
| Time | 7 | 7 | 1회 |
|
||||
| Misc. Parameters | 3 | 5 | 1회 |
|
||||
|
||||
## 4. 스캔 시간 계산
|
||||
|
||||
### 단일 스레드 순차 읽기
|
||||
|
||||
| 조건 | RTT | 전송/요청 | 총 시간 | 1초당 스캔 |
|
||||
|------|-----|----------|---------|-----------|
|
||||
| **최적** (Ping 그대로) | 0.2ms | 0.028ms | **0.066초** | 15.1회/s |
|
||||
| **보수** (Ping+처리) | 0.5ms | 0.028ms | **0.153초** | 6.5회/s |
|
||||
| **안전** (Ping 최대) | 1.0ms | 0.028ms | **0.298초** | 3.4회/s |
|
||||
|
||||
### 7개 Partition 병렬 읽기
|
||||
|
||||
| RTT | 총 시간 | 비고 |
|
||||
|-----|---------|------|
|
||||
| 0.2ms | 43.7ms | Loops 1-24 (192회)가 병목 |
|
||||
| 0.5ms | 101.3ms | |
|
||||
| 1.0ms | 197.3ms | |
|
||||
|
||||
## 5. 실시간 모니터링 가능 여부
|
||||
|
||||
| 샘플링 주기 | RTT=0.2ms | RTT=0.5ms | RTT=1.0ms |
|
||||
|-------------|-----------|-----------|-----------|
|
||||
| 10초 | ✓ 여유 99% | ✓ 여유 98% | ✓ 여유 97% |
|
||||
| 5초 | ✓ 여유 99% | ✓ 여유 97% | ✓ 여유 94% |
|
||||
| 2초 | ✓ 여유 97% | ✓ 여유 92% | ✓ 여유 85% |
|
||||
| 1초 | ✓ 여유 93% | ✓ 여유 85% | ✓ 여유 70% |
|
||||
| 0.5초 | ✓ 여유 87% | ✓ 여유 69% | ✓ 여유 40% |
|
||||
| 100ms | ✓ 여유 34% | ✗ 불가 | ✗ 불가 |
|
||||
|
||||
## 6. 결론
|
||||
|
||||
- **290회 Modbus TCP 읽기**로 HC900 C3 컨트롤러의 전체 데이터 수집 가능
|
||||
- 최적 조건에서 **0.066초**, 안전 조건에서 **0.298초**
|
||||
- **1초 단위 실시간 모니터링 충분히 가능** (여유 70~93%)
|
||||
- 100ms 단위는 최적 조건(RTT 0.2ms)에서만 가능
|
||||
- Big Endian는 HC900 기본이므로 바이트 변환 불필요
|
||||
- TCP keep-alive 필수 (handshake 오버헤드 제거)
|
||||
464
plans/realtime-tag-expansion-design.md
Normal file
464
plans/realtime-tag-expansion-design.md
Normal file
@@ -0,0 +1,464 @@
|
||||
# 실시간 태그 확장 설계안
|
||||
|
||||
## 1. 배경
|
||||
|
||||
현재 ExperionCrawler는 아날로그 태그의 `.pv`, `.sp`, `.op`, `.qv.value`만 `realtime_table`에 저장하고 있음.
|
||||
디지털 장비(Pump, XV)의 상태 정보와 모든 태그의 `.desc`, `.area` 메타데이터를 추가하고자 함.
|
||||
|
||||
## 2. 현재 시스템 아키텍처
|
||||
|
||||
```
|
||||
Experion HS R530 OPC Server
|
||||
↓ (1. 크롤링 - 전체 노드 스캔)
|
||||
CSV 파일 저장
|
||||
↓ (2. CSV 로드)
|
||||
node_map_master (530,080 행)
|
||||
↓ (3. UI에서 태그+속성 선택 - 최대 8개)
|
||||
realtime_table (등록된 태그만)
|
||||
↓ (4. OPC UA 구독)
|
||||
livevalue 실시간 업데이트
|
||||
↓ (5. 히스토리 저장)
|
||||
history_table
|
||||
```
|
||||
|
||||
### node_map_master 구조 특징
|
||||
- `name`: 속성명만 (`pv`, `sp`, `desc`, `area`, `instate0` 등)
|
||||
- `node_id`: 전체 경로 (`ns=1;s=sinamserver:xv-6124.pv`)
|
||||
- 상위 Object (`xv-6124`)와 하위 속성 (`pv`)이 별도 행으로 존재
|
||||
|
||||
## 3. 추가할 태그
|
||||
|
||||
### 3.1 아날로그 태그 (기존 확장)
|
||||
| 태그 | 데이터 타입 | 설명 |
|
||||
|------|------------|------|
|
||||
| `ficq-6101.area` | Enum(i=7594) | 소속 플랜트/Asset |
|
||||
|
||||
> 참고: ficq-6101은 `.desc` 속성이 없음 (DB 확인 결과)
|
||||
|
||||
### 3.2 디지털 태그 (신규 추가) (0~7까지 모두 추가)
|
||||
| 태그 | 데이터 타입 | 설명 |
|
||||
|------|------------|------|
|
||||
| `xv-6124.pv` | Int32 (BCD 조합값) | 프로세스 값 |
|
||||
| `xv-6124.op` | Int32 | 출력값 |
|
||||
| `xv-6124.desc` | String | 장비 설명 |
|
||||
| `xv-6124.area` | Enum(i=7594) | 소속 플랜트/Asset |
|
||||
| `xv-6124.instate0` | Boolean | 상태 비트 0 (현재 값) |
|
||||
| `xv-6124.instate1` | Boolean | 상태 비트 1 (현재 값) |
|
||||
| `xv-6124.instate2` | Boolean | 상태 비트 2 (현재 값) |
|
||||
| `xv-6124.state0descriptor` | String | 비트 0 의미 (예: "Run/Stop") |
|
||||
| `xv-6124.state1descriptor` | String | 비트 1 의미 (예: "Remote/Local") |
|
||||
| `xv-6124.state2descriptor` | String | 비트 2 의미 (예: "Trip/Normal") |
|
||||
|
||||
> **핵심 발견:** Experion HS에 `state0descriptor`~`state7descriptor`가 이미 존재합니다.
|
||||
> 이는 사용자가 정의한 BCD 상태 의미를 String으로 저장한 태그로,
|
||||
> OPC UA에서 구독하면 `realtime_table`에 저장 가능하므로 **별도 테이블 불필요**.
|
||||
|
||||
### 3.3 BCD 상태값 해석
|
||||
|
||||
`pv`는 HC900 컨트롤러에서 Digital Input을 BCD로 조합하여 32bit Float로 전송.
|
||||
**직접 `pv` 값을 파싱하지 않고 `instate0~7` Boolean 태그를 사용.**
|
||||
|
||||
| 사용 비트 | 현재 프로젝트 예시 (Pump) | 현재 프로젝트 예시 (Valve) |
|
||||
|-----------|---------------------------|----------------------------|
|
||||
| instate0 | Run(1)/Stop(0) | Open(1)/Close(0) |
|
||||
| instate0+1 | Run/Stop + R/L | Open/Close + R/L |
|
||||
| instate0+1+2 | Run/Stop + R/L + Trip/Normal | Open/Close + R/L + Fault/Normal |
|
||||
|
||||
> **중요**: 위 값은 현재 프로젝트에서만 알려진 사용자 정의 값입니다.
|
||||
> 다른 프로젝트에서는 BCD 상태 의미가 다를 수 있으므로 정형화 불가능합니다.
|
||||
> 각 장비의 실제 비트 사용 수와 의미는Experion HS에서 사용자 정의됩니다.
|
||||
|
||||
## 4. 설계 결정
|
||||
|
||||
### 4.1 DB 스키마 변경: **tag_metadata 테이블 추가**
|
||||
|
||||
메타데이터(desc, area, state0descriptor~7)는 실시간으로 자주 변경되지 않으므로,
|
||||
별도 테이블에 저장하고 OPC UA 통신 부담을 줄입니다.
|
||||
|
||||
```sql
|
||||
CREATE TABLE tag_metadata (
|
||||
id SERIAL PRIMARY KEY,
|
||||
base_tag TEXT NOT NULL, -- ficq-6101, xv-6124
|
||||
attribute TEXT NOT NULL, -- desc, area, state0descriptor, ...
|
||||
value TEXT, -- 실제 값
|
||||
node_id TEXT, -- OPC UA node_id (재로드용)
|
||||
loaded_at TIMESTAMP DEFAULT NOW(), -- 마지막 로드 시간
|
||||
UNIQUE(base_tag, attribute)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_tag_metadata_base ON tag_metadata(base_tag);
|
||||
```
|
||||
|
||||
**데이터 분리 전략:**
|
||||
|
||||
| 테이블 | 저장 내용 | OPC UA 구독 |
|
||||
|--------|----------|-------------|
|
||||
| `realtime_table` | pv, sp, op, instate0~7 등 **실시간 데이터** | ✅ 지속 구독 |
|
||||
| `tag_metadata` | desc, area, state0descriptor~7 등 **메타데이터** | ❌ 한 번 로드 |
|
||||
|
||||
**장점:**
|
||||
- OPC UA 통신 부담 대폭 감소 (메타데이터는 한 번만 읽음)
|
||||
- 수천 개 태그에 대해 desc/area를 실시간 구독하지 않음
|
||||
- UI에서 "재로드" 버튼으로 필요시 갱신 가능
|
||||
|
||||
### 4.2 메타데이터 로드/재로드 시나리오
|
||||
|
||||
**초기 로드:**
|
||||
1. UI에서 태그 선택 후 등록 시, 실시간 태그(pv, sp, op, instate0~7)는 `realtime_table`에 등록
|
||||
2. 동시에 메타데이터(desc, area, state0descriptor~7)는 OPC UA에서 **한 번 읽어서** `tag_metadata` 테이블 저장
|
||||
3. 읽은 메타데이터 정보로 `node_map_master`도 동시 갱신
|
||||
|
||||
**재로드 (UI 버튼):**
|
||||
1. UI에서 "메타데이터 재로드" 버튼 클릭
|
||||
2. Backend가 OPC UA 서버에 직접 연결
|
||||
3. 선택된 태그(또는 전체 등록 태그)의 메타데이터 재조회
|
||||
4. `tag_metadata` 테이블 UPDATE + `node_map_master` 갱신
|
||||
|
||||
```
|
||||
UI 재로드 버튼
|
||||
↓
|
||||
Backend API: POST /api/tags/metadata/reload
|
||||
↓
|
||||
OPC UA Read (desc, area, state0descriptor~7)
|
||||
↓
|
||||
tag_metadata UPDATE
|
||||
node_map_master UPDATE (UPSERT)
|
||||
↓
|
||||
Response: 갱신된 태그 수
|
||||
```
|
||||
|
||||
### 4.3 SQL 뷰 추가
|
||||
|
||||
`realtime_table` + `tag_metadata`를 JOIN하여 태그별 속성을 한눈에 보기 위한 뷰:
|
||||
|
||||
```sql
|
||||
CREATE OR REPLACE VIEW v_tag_summary AS
|
||||
SELECT
|
||||
rt_base.base_tag,
|
||||
pv_rt.livevalue AS pv,
|
||||
sp_rt.livevalue AS sp,
|
||||
op_rt.livevalue AS op,
|
||||
instate0_rt.livevalue AS instate0,
|
||||
instate1_rt.livevalue AS instate1,
|
||||
instate2_rt.livevalue AS instate2,
|
||||
desc_md.value AS description,
|
||||
area_md.value AS area,
|
||||
s0d_md.value AS state0_descriptor,
|
||||
s1d_md.value AS state1_descriptor,
|
||||
s2d_md.value AS state2_descriptor
|
||||
FROM (SELECT DISTINCT split_part(tagname, '.', 1) AS base_tag FROM realtime_table) rt_base
|
||||
LEFT JOIN realtime_table pv_rt ON pv_rt.tagname = rt_base.base_tag || '.pv'
|
||||
LEFT JOIN realtime_table sp_rt ON sp_rt.tagname = rt_base.base_tag || '.sp'
|
||||
LEFT JOIN realtime_table op_rt ON op_rt.tagname = rt_base.base_tag || '.op'
|
||||
LEFT JOIN realtime_table instate0_rt ON instate0_rt.tagname = rt_base.base_tag || '.instate0'
|
||||
LEFT JOIN realtime_table instate1_rt ON instate1_rt.tagname = rt_base.base_tag || '.instate1'
|
||||
LEFT JOIN realtime_table instate2_rt ON instate2_rt.tagname = rt_base.base_tag || '.instate2'
|
||||
LEFT JOIN tag_metadata desc_md ON desc_md.base_tag = rt_base.base_tag AND desc_md.attribute = 'desc'
|
||||
LEFT JOIN tag_metadata area_md ON area_md.base_tag = rt_base.base_tag AND area_md.attribute = 'area'
|
||||
LEFT JOIN tag_metadata s0d_md ON s0d_md.base_tag = rt_base.base_tag AND s0d_md.attribute = 'state0descriptor'
|
||||
LEFT JOIN tag_metadata s1d_md ON s1d_md.base_tag = rt_base.base_tag AND s1d_md.attribute = 'state1descriptor'
|
||||
LEFT JOIN tag_metadata s2d_md ON s2d_md.base_tag = rt_base.base_tag AND s2d_md.attribute = 'state2descriptor';
|
||||
```
|
||||
|
||||
### 4.4 UI 변경
|
||||
|
||||
| 항목 | 현재 | 변경 |
|
||||
|------|------|------|
|
||||
| 최대 선택 태그 수 | 8개 | 10개 |
|
||||
| 태그 검색 | name 기준 | name + node_id 기준 |
|
||||
| 메타데이터 재로드 | 없음 | "메타데이터 재로드" 버튼 추가 |
|
||||
| 속성 미리보기 | 없음 | 선택 시 하위 속성 표시 (선택사항) |
|
||||
|
||||
## 5. 변경 사항 요약
|
||||
|
||||
### 5.1 Database
|
||||
- **신규 테이블**: `tag_metadata` (base_tag, attribute, value, node_id, loaded_at)
|
||||
- **신규 뷰**: `v_tag_summary` (realtime_table + tag_metadata JOIN)
|
||||
|
||||
### 5.2 Backend (`src/Infrastructure/OpcUa/`)
|
||||
- **신규 서비스**: `MetadataLoaderService`
|
||||
- `LoadMetadataAsync(base_tags)` : OPC UA에서 desc, area, state0descriptor~7 읽어서 `tag_metadata` 저장
|
||||
- `ReloadMetadataAsync(base_tags)` : 재로드 시 OPC UA 재조회 + `tag_metadata` UPDATE + `node_map_master` UPSERT
|
||||
- **신규 API**: `POST /api/tags/metadata/reload`
|
||||
|
||||
### 5.3 Frontend (`src/Web/wwwroot/js/app.js`)
|
||||
- 태그 선택 최대 수 8 → 10개로 변경
|
||||
- "메타데이터 재로드" 버튼 추가
|
||||
- (선택사항) 상위 Object 선택 시 하위 속성 자동 표시
|
||||
|
||||
### 5.4 Entity/DbContext
|
||||
- **신규 Entity**: `TagMetadata` class
|
||||
- **DbContext**: `DbSet<TagMetadata>` 추가, `OnModelCreating` 설정 추가
|
||||
|
||||
### 5.5 사용자 작업
|
||||
- UI에서 새로운 태그(Pump, XV)와 속성(pv, op, instate0~2) 선택하여 등록
|
||||
- 메타데이터(desc, area, state descriptor)는 자동 로드
|
||||
- 필요시 "메타데이터 재로드" 버튼으로 갱신
|
||||
|
||||
## 6. 대규모 리팩토링이 필요한 시나리오 (향후 고려사항)
|
||||
|
||||
현재 설계안은 기존 `realtime_table` 구조를 유지하므로 저위험입니다.
|
||||
하지만 아래 기능들을 추가하려면 **DB 스키마 변경 + 코드 리팩토링**이 필요합니다.
|
||||
|
||||
### 6.1 자동 속성 제안 기능
|
||||
|
||||
**문제:** 현재 UI에서 각 태그의 속성(pv, sp, op, desc, area, instate0~2)을 수동으로 선택해야 함.
|
||||
Pump/XV 같은 디지털 장비는 7개 이상의 속성을 선택해야 하므로 번거로움.
|
||||
|
||||
**해결 방안:**
|
||||
- 상위 Object (예: `xv-6124`) 선택 시, node_map_master에서 하위 속성을 자동으로 조회하여 제안
|
||||
- 장비 타입에 따라 기본 속성 조합 제안:
|
||||
- 아날로그 (ficq-6101): `.pv`, `.sp`, `.op`, `.area`
|
||||
- 디지털 XV (xv-6124): `.pv`, `.op`, `.desc`, `.area`, `.instate0`, `.instate1`, `.instate2`
|
||||
- Pump (p-6102): `.pv`, `.op`, `.desc`, `.area`, `.instate0`, `.instate1`, `.instate2`
|
||||
|
||||
**변경 범위:**
|
||||
- Frontend: 상위 Object 선택 시 하위 속성 자동 로딩 UI
|
||||
- Backend: `/api/nodemap/children?parent=xv-6124` API 추가
|
||||
- node_map_master에서 `node_id LIKE '%xv-6124.%'`로 하위 속성 조회
|
||||
|
||||
**리팩토링 위험:** 낮음 (기존 API 추가만 필요)
|
||||
|
||||
### 6.2 tag_registry 테이블 도입
|
||||
|
||||
**문제:** BCD 상태 의미(instate0=Run/Stop, instate1=R/L 등)는 사용자 정의이므로
|
||||
시스템에서 자동으로 해석할 수 없음. Text-to-SQL에서 "pump-6102 상태 알려줘" 같은
|
||||
자연어 질문에 의미 있는 답변을 제공하려면 상태 의미 정보가 필요함.
|
||||
|
||||
**해결 방안:**
|
||||
```sql
|
||||
CREATE TABLE tag_registry (
|
||||
id SERIAL PRIMARY KEY,
|
||||
base_tag TEXT NOT NULL UNIQUE, -- xv-6124, p-6102
|
||||
equipment_type TEXT, -- pump, xv, ai, ao, fi, ti
|
||||
bit_count INT DEFAULT 1, -- 사용하는 instate 비트 수 (1~3)
|
||||
state0_meaning TEXT, -- Run/Stop, Open/Close
|
||||
state1_meaning TEXT, -- Remote/Local
|
||||
state2_meaning TEXT, -- Trip/Normal, Fault/Normal
|
||||
area TEXT, -- 소속 플랜트/Asset
|
||||
description TEXT, -- 장비 설명
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
**활용 시나리오:**
|
||||
- BCD 상태 의미 저장: 사용자가 UI에서 상태 의미 정의 → DB 저장
|
||||
- 자동 속성 제안: `equipment_type`에 따라 기본 속성 조합 제안
|
||||
- Text-to-SQL: "pump-6102 현재 상태" → instate0~2를 Boolean으로 읽어서 상태 의미와 매핑하여 "Run, Remote, Normal" 표시
|
||||
- P&ID 매핑: P&ID에서 추출한 장비와 Experion 태그를 `tag_registry`를 통해 연결
|
||||
|
||||
**리팩토링 위험:** 중간 (새 테이블 + Entity + Service 추가)
|
||||
|
||||
### 6.3 장비 타입별 다른 처리 로직
|
||||
|
||||
**문제:** 현재 `realtime_table`은 모든 태그를 동일하게 처리.
|
||||
하지만 장비 타입에 따라 다른 처리가 필요할 수 있음:
|
||||
- 아날로그: pv/sp/op를 숫자로 표시, 단위 표시
|
||||
- 디지털 XV: instate0~2를 Boolean으로 읽어서 "Open/Close", "R/L"로 표시
|
||||
- Pump: instate0~2를 Boolean으로 읽어서 "Run/Stop", "R/L", "Trip/Normal"로 표시
|
||||
- 알람: alarmflags, alarmvalue를 파싱해서 알람 상태 표시
|
||||
|
||||
**해결 방안:**
|
||||
- `tag_registry.equipment_type`에 따라 UI에서 다른 컴포넌트 렌더링
|
||||
- Backend에서 장비 타입별 상태 해석 서비스 추가
|
||||
- BCD 상태값을 Boolean 배열로 파싱하는 유틸리티 함수
|
||||
|
||||
**변경 범위:**
|
||||
- Frontend: 장비 타입별 상태 표시 컴포넌트
|
||||
- Backend: `EquipmentStateService` 추가 (BCD → Boolean 배열 → 상태 문자열)
|
||||
- `tag_registry` 테이블과 연동
|
||||
|
||||
**리팩토링 위험:** 높음 (Frontend 컴포넌트 + Backend 서비스 + DB 연동)
|
||||
|
||||
### 6.4 desc/area 별도 테이블
|
||||
|
||||
**문제:** desc/area는 자주 변경되지 않지만, 현재는 `realtime_table`에 실시간 데이터로 저장.
|
||||
수천 개의 태그에 대해 desc/area를 OPC UA에서 계속 구독하면 불필요한 트래픽 발생.
|
||||
|
||||
**해결 방안:**
|
||||
```sql
|
||||
CREATE TABLE tag_metadata (
|
||||
tagname TEXT PRIMARY KEY, -- ficq-6101.area, xv-6124.desc
|
||||
value TEXT NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
- 초기 로드 시 OPC UA에서 desc/area 한 번 읽어서 저장
|
||||
- 변경 감지: 주기적 폴링 또는 이벤트 기반 업데이트
|
||||
- UI 조회 시 `tag_metadata`에서 읽되, 변경 의심 시 OPC UA에서 재조회
|
||||
|
||||
**리팩토링 위험:** 중간 (새 테이블 + 폴링 서비스 추가)
|
||||
|
||||
## 7. 권장 진행 순서
|
||||
|
||||
### Phase 1: 기본 확장 (현재 설계안)
|
||||
1. **UI 변경**: 태그 선택 최대 수 8 → 10개
|
||||
2. **테스트**: xv-6124의 pv, op, desc, area, instate0~2 등록 및 OPC UA 구독 확인
|
||||
3. **SQL 뷰**: `v_tag_summary` 생성 (선택사항)
|
||||
|
||||
### Phase 2: 자동화 (향후)
|
||||
4. **자동 속성 제안**: 상위 Object 선택 시 하위 속성 자동 제안
|
||||
5. **tag_registry 테이블**: BCD 상태 의미 저장, 장비 타입 관리
|
||||
|
||||
### Phase 3: 지능형 처리 (장기)
|
||||
6. **장비 타입별 처리**: BCD 상태 해석, UI 컴포넌트별 렌더링
|
||||
7. **desc/area 최적화**: 별도 테이블 + 변경 감지 폴링
|
||||
8. **Text-to-SQL 연동**: 자연어 질문 → BCD 상태 해석 → 의미 있는 답변
|
||||
|
||||
## 8. NL2SQL 변경 사항
|
||||
|
||||
현재 NL2SQL worker(`mcp-server/worker/nl2sql_worker.py`)는 `history_table`과 `realtime_table`만 인식합니다.
|
||||
새로운 테이블과 뷰, 태그 타입을 인식하도록 `DB_SCHEMA`를 업데이트해야 합니다.
|
||||
|
||||
### 8.1 DB_SCHEMA 업데이트 필요 항목
|
||||
|
||||
**추가할 테이블 정의:**
|
||||
```
|
||||
테이블: tag_metadata (태그 메타데이터 - 변경 드묾)
|
||||
base_tag TEXT - 기본 태그명 (예: 'ficq-6101', 'xv-6124')
|
||||
attribute TEXT - 속성명 ('desc', 'area', 'state0descriptor', ...)
|
||||
value TEXT - 메타데이터 값
|
||||
node_id TEXT - OPC UA 노드 ID
|
||||
loaded_at TIMESTAMPTZ - 마지막 로드 시각
|
||||
|
||||
뷰: v_tag_summary (실시간 + 메타데이터 통합)
|
||||
base_tag TEXT - 기본 태그명
|
||||
pv TEXT - 현재 프로세스 값
|
||||
sp TEXT - 설정값
|
||||
op TEXT - 출력값
|
||||
instate0 TEXT - 상태 비트 0 (true/false)
|
||||
instate1 TEXT - 상태 비트 1 (true/false)
|
||||
instate2 TEXT - 상태 비트 2 (true/false)
|
||||
description TEXT - 장비 설명 (tag_metadata.desc)
|
||||
area TEXT - 소속 플랜트 (tag_metadata.area)
|
||||
state0_descriptor TEXT - 비트 0 의미 (예: "Run/Stop")
|
||||
state1_descriptor TEXT - 비트 1 의미 (예: "Remote/Local")
|
||||
state2_descriptor TEXT - 비트 2 의미 (예: "Trip/Normal")
|
||||
```
|
||||
|
||||
### 8.2 NL2SQL 사용 예시 (새로운 시나리오)
|
||||
|
||||
| 자연어 질문 | 생성 SQL |
|
||||
|------------|----------|
|
||||
| "xv-6124의 현재 상태와 설명을 알려줘" | `SELECT instate0, instate1, state0_descriptor, state1_descriptor, description FROM v_tag_summary WHERE base_tag = 'xv-6124'` |
|
||||
| "Unit-A에 있는 모든 pump의 상태를 보여줘" | `SELECT base_tag, instate0, state0_descriptor, description FROM v_tag_summary WHERE area = 'Unit-A' AND base_tag LIKE 'p-%'` |
|
||||
| "ficq-6101의 현재 값과 설명은?" | `SELECT pv, description FROM v_tag_summary WHERE base_tag = 'ficq-6101'` |
|
||||
| "모든 XV 중 instate0이 true인 것" | `SELECT base_tag, instate0, state0_descriptor FROM v_tag_summary WHERE instate0 = 'True' AND base_tag LIKE 'xv-%'` |
|
||||
| "p-6102의 3개 상태 비트와 의미를 보여줘" | `SELECT instate0, instate1, instate2, state0_descriptor, state1_descriptor, state2_descriptor FROM v_tag_summary WHERE base_tag = 'p-6102'` |
|
||||
|
||||
### 8.3 변경 파일 목록
|
||||
|
||||
| 파일 | 변경 내용 |
|
||||
|------|----------|
|
||||
| `mcp-server/worker/nl2sql_worker.py` | `DB_SCHEMA`에 `tag_metadata`, `v_tag_summary` 추가 |
|
||||
| `mcp-server/server.py` | `_DB_SCHEMA`에 동일하게 추가 (동기용) |
|
||||
|
||||
### 8.4 DB_SCHEMA 추가 내용 예시
|
||||
|
||||
```
|
||||
테이블: tag_metadata (태그 메타데이터 - 변경 드묾, OPC UA 폴링으로 갱신)
|
||||
base_tag TEXT - 기본 태그명 (예: 'ficq-6101', 'xv-6124')
|
||||
attribute TEXT - 속성명 ('desc', 'area', 'state0descriptor'~'state7descriptor')
|
||||
value TEXT - 메타데이터 값
|
||||
loaded_at TIMESTAMPTZ - 마지막 로드 시각
|
||||
|
||||
뷰: v_tag_summary (실시간값 + 메타데이터 통합 뷰)
|
||||
base_tag, pv, sp, op, instate0~2, description, area, state0~2_descriptor
|
||||
|
||||
새로운 태그 타입:
|
||||
- 아날로그: ficq-6101.pv/sp/op (Double), ficq-6101.area
|
||||
- 디지털 XV: xv-6124.pv/op (Int32), xv-6124.instate0~7 (Boolean)
|
||||
- Pump: p-6102.pv/op (Int32), p-6102.instate0~7 (Boolean)
|
||||
- 메타데이터: desc (String), area (Enum), state0descriptor~7 (String)
|
||||
|
||||
BCD 상태 조회 팁:
|
||||
- instate0~7은 Boolean (true/false)
|
||||
- state0descriptor~7은 해당 비트의 의미 설명 (예: "Run/Stop")
|
||||
- instate0=true이고 state0descriptor="Run/Stop"이면 → "Run" 상태
|
||||
- v_tag_summary 뷰를 사용하면 실시간값+메타데이터 한 번에 조회 가능
|
||||
```
|
||||
|
||||
## 9. Event & Alarm 테이블 (지능형 가동 상태 보고용)
|
||||
|
||||
### 9.1 필요성
|
||||
|
||||
"어제 18시부터 6차 플랜트의 가동상태 알려줘" 같은 지능형 보고를 하려면:
|
||||
- 펌프/밸브 상태 변화 이력
|
||||
- 알람 발생/해제 시점
|
||||
- 비상정지(ESHUTDOWN) 발생/해제 시점
|
||||
- 유량계 적산값(qv.value)으로 생산량 계산
|
||||
|
||||
이 정보들은 **실시간 값이 아닌 이벤트 이력**이므로 별도 테이블 필요.
|
||||
|
||||
### 9.2 테이블 구조
|
||||
|
||||
```sql
|
||||
CREATE TABLE event_alarm_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
base_tag TEXT NOT NULL, -- xv-6124, p-6102
|
||||
event_type TEXT NOT NULL, -- state_change, alarm, shutdown
|
||||
bit_index INT, -- instate0~7 중 어떤 비트 (state_change용)
|
||||
old_value TEXT, -- 이전 값
|
||||
new_value TEXT, -- 새 값
|
||||
state_descriptor TEXT, -- state0descriptor 등 의미
|
||||
alarm_priority TEXT, -- Urgent/High/Low/Journal
|
||||
alarm_message TEXT, -- 알람 메시지
|
||||
occurred_at TIMESTAMPTZ NOT NULL, -- 발생 시각
|
||||
acknowledged_at TIMESTAMPTZ, -- 확인 시각
|
||||
source TEXT DEFAULT 'opcua' -- opcua, manual
|
||||
);
|
||||
|
||||
CREATE INDEX idx_eal_base_tag ON event_alarm_log(base_tag);
|
||||
CREATE INDEX idx_eal_occurred ON event_alarm_log(occurred_at);
|
||||
CREATE INDEX idx_eal_type ON event_alarm_log(event_type);
|
||||
```
|
||||
|
||||
### 9.3 수집 방법
|
||||
|
||||
**방법 1: OPC UA Event Subscription (권장)**
|
||||
- Experion HS의 OPC UA Event Mechanism 구독
|
||||
- 알람 발생 시 실시간으로 이벤트 수신 → DB 저장
|
||||
|
||||
**방법 2: 폴링 기반 상태 변화 감지**
|
||||
- instate0~7 값을 주기적 폴링
|
||||
- 이전 값과 비교하여 변화 감지 → event_alarm_log INSERT
|
||||
- alarmflags, alarmvalue 변화도 함께 감지
|
||||
|
||||
### 9.4 활용 시나리오
|
||||
|
||||
| 질문 | 필요한 데이터 |
|
||||
|------|--------------|
|
||||
| "p6 플랜트 가동상태 알려줘" | pump instate0 변화 이력 + shutdown 이벤트 |
|
||||
| "00시에 왜 정지됐어?" | event_alarm_log WHERE event_type='shutdown' |
|
||||
| "오늘 생산량?" | qv.value 적산 (history_table) |
|
||||
| "xv-6124 알람 이력" | event_alarm_log WHERE base_tag='xv-6124' |
|
||||
|
||||
### 9.5 NL2SQL DB_SCHEMA 추가 내용
|
||||
|
||||
```
|
||||
테이블: event_alarm_log (이벤트/알람 이력)
|
||||
base_tag TEXT - 기본 태그명
|
||||
event_type TEXT - state_change, alarm, shutdown
|
||||
bit_index INT - instate 비트 인덱스 (0~7)
|
||||
old_value TEXT - 이전 값
|
||||
new_value TEXT - 새 값
|
||||
state_descriptor TEXT - 상태 의미 (예: "Trip, Fault")
|
||||
alarm_priority TEXT - Urgent/High/Low/Journal
|
||||
occurred_at TIMESTAMPTZ - 발생 시각
|
||||
area - 그룹명 (플랜트 'p1~10' 에 그룹 알람 정보 찾기 쉬움)
|
||||
|
||||
예시: "오늘 p6 플랜트 알람 목록"
|
||||
SELECT base_tag, event_type, new_value, state_descriptor, alarm_priority, occurred_at
|
||||
FROM event_alarm_log
|
||||
WHERE base_tag IN (SELECT base_tag FROM tag_metadata WHERE attribute='area' AND value='p6')
|
||||
AND occurred_at >= date_trunc('day', NOW())
|
||||
ORDER BY occurred_at
|
||||
```
|
||||
|
||||
|
||||
##### future Plan 항목
|
||||
이 디자인 플랜의 후순위
|
||||
|
||||
- 알람 관련 작업 :Experion HS 알람 구성 파악, 알람 테이블 긁어올 가능성 있는지 체크
|
||||
- WebUI 도입관련 : 진정한 AX도입의 FINAL WORK가 될 것이며, 위의 모든 기능은 최종 AX화를 위한 걸 기본으로 설계되어야 한다.
|
||||
Reference in New Issue
Block a user