533 lines
20 KiB
Markdown
533 lines
20 KiB
Markdown
# P&ID 데이터베이스화 기능 통합 설계
|
|
|
|
## 📋 개요
|
|
|
|
DXF/PDF 형식의 P&ID 도면에서 장비 및 계기 정보를 AI로 자동 추출하여 ExperionCrawler 데이터베이스와 연동하는 기능입니다.
|
|
|
|
---
|
|
|
|
## 🎯 목표
|
|
|
|
1. P&ID 도면에서 장비 정보를 추출
|
|
2. 추출된 정보를 PostgreSQL 로 저장
|
|
3. 기존 Experion 데이터와 연동
|
|
4. 웹에서 시각화 및 관리
|
|
|
|
---
|
|
|
|
## 🏗️ 아키텍처 설계
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────────┐
|
|
│ ExperionCrawler │
|
|
├─────────────────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │
|
|
│ │ Frontend UI │◄────►│ Web API │◄────►│ DB │ │
|
|
│ │ (app.js, .html)│ │ (Controllers) │ │ (Experion │ │
|
|
│ └─────────────────┘ └─────────────────┘ │ DbContext)│ │
|
|
│ │ │ └─────────────┘ │
|
|
│ └──────────────────────────┼────────────────────────────┘ │
|
|
│ │ │
|
|
│ ┌───────────────┴───────────────┐ │
|
|
│ │ P&ID Extraction Service │ │
|
|
│ │ (AI 기반 추출) │ │
|
|
│ └───────────────┬───────────────┘ │
|
|
│ │ │
|
|
│ ┌───────────────▼───────────────┐ │
|
|
│ │ Image/Text Preprocessing │ │
|
|
│ │ (PDF → PNG → OCR) │ │
|
|
│ └───────────────┬───────────────┘ │
|
|
│ │ │
|
|
│ ┌───────────────▼───────────────┐ │
|
|
│ │ Claude Vision API │ │
|
|
│ │ (필드 추출) │ │
|
|
│ └───────────────┬───────────────┘ │
|
|
└────────────────────────────────────┼────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────┐
|
|
│ PostgreSQL DB │
|
|
│ ┌───────────────┐ │
|
|
│ │ pid_equipment │ │
|
|
│ │ Active │ │
|
|
│ │ Audit Log │ │
|
|
│ └───────────────┘ │
|
|
│ ┌───────────────┐ │
|
|
│ │ experion_tags │ │
|
|
│ │ Active │ │
|
|
│ └───────────────┘ │
|
|
└─────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## 📁 폴더 구조
|
|
|
|
```
|
|
ExperionCrawler/
|
|
├── src/
|
|
│ ├── Web/
|
|
│ │ └── Controllers/
|
|
│ │ ├── ExperionControllers.cs (기존)
|
|
│ │ └── PidController.cs (추가)
|
|
│ ├── Core/
|
|
│ │ ├── Application/
|
|
│ │ │ ├── Interfaces/
|
|
│ │ │ │ ├── IExperionServices.cs (기존)
|
|
│ │ │ │ ├── IPidExtractorService.cs (추가)
|
|
│ │ │ │ └── ITagMappingService.cs (추가)
|
|
│ │ │ ├── Services/
|
|
│ │ │ │ ├── TextToSqlService.cs (기존)
|
|
│ │ │ │ ├── PidExtractorService.cs (추가)
|
|
│ │ │ │ ├── AxImportGenerator.cs (추가)
|
|
│ │ │ │ └── TagMappingService.cs (추가)
|
|
│ │ │ └── Dtos/
|
|
│ │ │ ├── PidEquipmentDto.cs (추가)
|
|
│ │ │ └── TagCountDto.cs (추가)
|
|
│ │ └── Domain/
|
|
│ │ ├── Entities/
|
|
│ │ │ ├── PidEquipment.cs (추가)
|
|
│ │ │ └── PidAuditLog.cs (추가)
|
|
│ │ └── ValueObjects/
|
|
│ │ ├── ConfidenceScore.cs (추가)
|
|
│ │ └── MeasurementUnit.cs (추가)
|
|
│ └── Infrastructure/
|
|
│ ├── Database/
|
|
│ │ ├── ExperionDbContext.cs (기존 - 확장)
|
|
│ │ └── PidDbContext.cs (추가)
|
|
│ └── OpcUa/
|
|
│ └── (기존)
|
|
├── futurePlan/
|
|
│ ├── temp/
|
|
│ │ ├── pid_extractor.py (AI 추출기)
|
|
│ │ ├── schema.sql (추구용 DB 스키마)
|
|
│ │ └── requirements.txt (Python 의존성)
|
|
│ └── P&ID_데이터베이스화_통합_설계.md
|
|
├── src/Web/wwwroot/
|
|
│ └── js/
|
|
│ └── app.js (기존 - 확장)
|
|
```
|
|
|
|
---
|
|
|
|
## 🔌 데이터베이스 스키마 확장
|
|
|
|
### PidDbContext.cs (새 파일)
|
|
|
|
```csharp
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace ExperionCrawler.Infrastructure.Database;
|
|
|
|
public class PidDbContext : DbContext
|
|
{
|
|
public DbSet<PidEquipment> PidEquipment { get; set; }
|
|
public DbSet<PidAuditLog> PidAuditLog { get; set; }
|
|
|
|
// 기존 ExperionDbContext와 통합
|
|
public DbSet<TagInfo> TagInfo { get; set; }
|
|
|
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
{
|
|
// PidEquipment 설정
|
|
modelBuilder.Entity<PidEquipment>(entity =>
|
|
{
|
|
entity.HasKey(e => e.Id);
|
|
entity.Property(e => e.TagNo).IsRequired().HasMaxLength(50);
|
|
entity.Property(e => e.EquipmentName).HasMaxLength(200);
|
|
entity.Property(e => e.InstrumentType).HasMaxLength(10);
|
|
entity.Property(e => e.LineNumber).HasMaxLength(100);
|
|
entity.Property(e => e.PidDrawingNo).HasMaxLength(50);
|
|
entity.Property(e => x => x.Confidence).HasPrecision(3, 2);
|
|
entity.Property(e => x => x.IsActive).HasDefaultValue(true);
|
|
|
|
// 태그 번호로 Experion과 연동
|
|
entity.HasOne(e => e.ExperionTag)
|
|
.WithMany(t => t.PidEquipments)
|
|
.HasForeignKey(e => e.ExperionTagId)
|
|
.OnDelete(DeleteBehavior.SetNull);
|
|
});
|
|
|
|
// PidAuditLog 설정
|
|
modelBuilder.Entity<PidAuditLog>(entity =>
|
|
{
|
|
entity.HasKey(e => e.Id);
|
|
entity.Property(e => e.UserId).HasMaxLength(100);
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
### 기존 ExperionDbContext.cs 확장
|
|
|
|
```csharp
|
|
public class ExperionDbContext : DbContext
|
|
{
|
|
// 기존 DbSet
|
|
|
|
// P&ID 데이터베이스용 DbSet 추가
|
|
public DbSet<PidEquipment> PidEquipment { get; set; }
|
|
public DbSet<PidAuditLog> PidAuditLog { get; set; }
|
|
|
|
// Expose PidDbContext connection string if needed
|
|
public string PidConnectionString => Configuration.GetConnectionString("PidDb");
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🎯 필드 매핑
|
|
|
|
### P&ID 추출 필드 ↔ DB 필드
|
|
|
|
| 추출 필드 (AI) | DB 필드 (PidEquipment) | 설명 |
|
|
|---------------------|--------------------------|----------------------------|
|
|
| Tag No. | TagNo | 태그번호 (FT-1001, PT-2003) |
|
|
| Equipment Name | EquipmentName | 장비명 (Flow Transmitter) |
|
|
| Instrument Type | InstrumentType | 계기타입 (FT, PT, LT) |
|
|
| Line Number | LineNumber | 라인번호 (6"-P-1001-A1A) |
|
|
| P&ID Drawing No. | PidDrawingNo | 도면번호 (P&ID-100-001) |
|
|
| Confidence | Confidence | 신뢰도 (0.0~1.0) |
|
|
|
|
---
|
|
|
|
## 💻 PidExtractorService.cs (핵심 서비스)
|
|
|
|
```csharp
|
|
using Azure.AI.Vision.ImageAnalysis;
|
|
using ExperionCrawler.Core.Application.Interfaces;
|
|
using ExperionCrawler.Core.Domain.Entities;
|
|
using ExperionCrawler.Infrastructure.Database;
|
|
|
|
namespace ExperionCrawler.Core.Application.Services;
|
|
|
|
public class PidExtractorService : IPidExtractorService
|
|
{
|
|
private readonly string _anthropicApiKey;
|
|
private readonly BinaryData _systemPrompt;
|
|
private readonly PidDbContext _pidDbContext;
|
|
|
|
public PidExtractorService(
|
|
IConfiguration configuration,
|
|
PidDbContext pidDbContext)
|
|
{
|
|
_anthropicApiKey = configuration["Anthropic:ApiKey"]!;
|
|
_pidDbContext = pidDbContext;
|
|
_systemPrompt = BinaryData.FromString(GetPrompt());
|
|
}
|
|
|
|
public async Task<PidExtractionResult> ExtractFromFile(string filePath, bool useImageMode = false)
|
|
{
|
|
// 1. 파일 텍스트/이미지 변환
|
|
var imageData = await PreprocessFile(filePath, useImageMode);
|
|
|
|
// 2. Claude Vision API 분석
|
|
using var client = new ImageAnalysisClient(new Uri("https://vision.api.anthropic.com"),
|
|
new System.ClientModel.ApiKeyCredential(_anthropicApiKey));
|
|
|
|
var result = await client.AnalyzeAsync(ImageAnalyzerOptions.Create(
|
|
BinaryData.FromBytes(imageData),
|
|
ImageAnalysisFeature.RecognizedText | ImageAnalysisFeature.DenseCaption
|
|
));
|
|
|
|
// 3. JSON 파싱 및 검증
|
|
var extractedItems = ParseExtractedData(result.Value.Text);
|
|
|
|
// 4. DB 저장
|
|
var dbItems = new List<PidEquipment>();
|
|
foreach (var item in extractedItems)
|
|
{
|
|
// 기존 태그와 매핑 확인
|
|
var existingTag = await FindMatchingExperionTag(item.TagNo);
|
|
var pidEquipment = new PidEquipment
|
|
{
|
|
TagNo = item.TagNo,
|
|
EquipmentName = item.EquipmentName,
|
|
InstrumentType = item.InstrumentType,
|
|
LineNumber = item.LineNumber,
|
|
PidDrawingNo = item.PidDrawingNo,
|
|
Confidence = item.Confidence,
|
|
ExperionTagId = existingTag?.Id,
|
|
ExtractedAt = DateTime.UtcNow,
|
|
UpdatedAt = DateTime.UtcNow
|
|
};
|
|
dbItems.Add(pidEquipment);
|
|
}
|
|
|
|
await _pidDbContext.PidEquipment.AddRangeAsync(dbItems);
|
|
await _pidDbContext.SaveChangesAsync();
|
|
|
|
return new PidExtractionResult
|
|
{
|
|
TotalCount = dbItems.Count,
|
|
ConfidenceItems = dbItems.Count(i => i.Confidence >= 0.7),
|
|
LowConfidenceItems = dbItems.Count(i => i.Confidence < 0.5),
|
|
CsvPath = $"output/pid_extracted_{DateTime.UtcNow:yyyyMMdd_HHmmss}.csv",
|
|
ExcelPath = $"output/pid_AX_import_{DateTime.UtcNow:yyyyMMdd_HHmmss}.xlsx"
|
|
};
|
|
}
|
|
|
|
private string GetPrompt()
|
|
{
|
|
return @"
|
|
Analyze the P&ID (Piping and Instrumentation Diagram) drawing and extract the following information.
|
|
|
|
Return ONLY pure JSON (no markdown, no explanations):
|
|
{
|
|
""items"": [
|
|
{
|
|
""tagNo"": ""Tag number (e.g., FT-1001, PT-2003, E-101, CV-123)"",
|
|
""equipmentName"": ""Full equipment name (e.g., ""Flow Transmitter"")"",
|
|
""instrumentType"": ""Short type code (FT, PT, LT, CV, E, V, P, etc.)"",
|
|
""lineNumber"": ""Line reference (e.g., ""6\""-P-1001-A1A"")"",
|
|
""pidDrawingNo"": ""P&ID drawing number (if identifiable)""
|
|
}
|
|
],
|
|
""note"": ""Any items that cannot be clearly identified"" // optional
|
|
}";
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🌐 PidController.cs (Web API)
|
|
|
|
```csharp
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using ExperionCrawler.Core.Application.Interfaces;
|
|
using ExperionCrawler.Core.Application.Dtos;
|
|
|
|
namespace ExperionCrawler.Web.Controllers;
|
|
|
|
[ApiController]
|
|
[Route("api/[controller]")]
|
|
public class PidController : ControllerBase
|
|
{
|
|
private readonly IPidExtractorService _pidExtractor;
|
|
private readonly IExperionServices _experionServices;
|
|
|
|
public PidController(IPidExtractorService pidExtractor,
|
|
IExperionServices experionServices)
|
|
{
|
|
_pidExtractor = pidExtractor;
|
|
_experionServices = experionServices;
|
|
}
|
|
|
|
[HttpPost("extract")]
|
|
public async Task<IActionResult> ExtractFromFile(IFormFile file, bool useImageMode = false)
|
|
{
|
|
if (file == null || file.Length == 0)
|
|
return BadRequest("파일이 없습니다.");
|
|
|
|
using var stream = file.OpenReadStream();
|
|
var result = await _pidExtractor.ExtractFromStream(stream, file.FileName, useImageMode);
|
|
|
|
return Ok(new
|
|
{
|
|
totalCount = result.TotalCount,
|
|
confidenceItems = result.ConfidenceItems,
|
|
lowConfidenceItems = result.LowConfidenceItems,
|
|
csvPath = result.CsvPath,
|
|
excelPath = result.ExcelPath
|
|
});
|
|
}
|
|
|
|
[HttpGet("equipment")]
|
|
public async Task<IActionResult> GetEquipment(string tagNo = null, int page = 1, int pageSize = 50)
|
|
{
|
|
var query = _pidExtractor.GetQueryable();
|
|
if (!string.IsNullOrEmpty(tagNo))
|
|
query = query.Where(e => e.TagNo.Contains(tagNo));
|
|
|
|
var total = await query.CountAsync();
|
|
var items = await query
|
|
.OrderByDescending(e => e.ExtractedAt)
|
|
.Skip((page - 1) * pageSize)
|
|
.Take(pageSize)
|
|
.ToListAsync();
|
|
|
|
return Ok(new
|
|
{
|
|
total,
|
|
page,
|
|
pageSize,
|
|
confidenceRate = items.Sum(e => e.Confidence) / (items.Count > 0 ? items.Count : 1),
|
|
items = items.Select(e => new
|
|
{
|
|
id = e.Id,
|
|
tagNo = e.TagNo,
|
|
equipmentName = e.EquipmentName,
|
|
instrumentType = e.InstrumentType,
|
|
lineNumber = e.LineNumber,
|
|
pidDrawingNo = e.PidDrawingNo,
|
|
confidence = e.Confidence,
|
|
isActive = e.IsActive
|
|
})
|
|
});
|
|
}
|
|
|
|
[HttpGet("statistics")]
|
|
public async Task<IActionResult> GetStatistics()
|
|
{
|
|
var typeCount = await _pidExtractor.GetInstrumentTypeCount();
|
|
var confidenceRange = await _pidExtractor.GetConfidenceDistribution();
|
|
var drawingCount = await _pidExtractor.GetDrawingCount();
|
|
|
|
return Ok(new
|
|
{
|
|
typeCount,
|
|
confidenceRange,
|
|
drawingCount
|
|
});
|
|
}
|
|
|
|
[HttpPut("{id}/confidence")]
|
|
public async Task<IActionResult> UpdateConfidence(long id, decimal confidence)
|
|
{
|
|
if (confidence < 0 || confidence > 1)
|
|
return BadRequest("신뢰도는 0~1 사이어야 합니다.");
|
|
|
|
await _pidExtractor.UpdateConfidence(id, confidence);
|
|
return Ok(new { message = "신뢰도가 업데이트되었습니다." });
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🎨 Frontend UI 확장 (app.js)
|
|
|
|
```javascript
|
|
// P&ID 추출 및 관리 기능
|
|
|
|
class PidManager {
|
|
constructor() {
|
|
this.extractorFileInput = document.getElementById('pid-file-input');
|
|
this.extractActionBtn = document.getElementById('extract-pid-btn');
|
|
this.useImageMode = document.getElementById('use-image-mode');
|
|
|
|
this.bindEvents();
|
|
}
|
|
|
|
bindEvents() {
|
|
this.extractActionBtn.addEventListener('click', () => this.handleExtract());
|
|
|
|
this.useImageMode.addEventListener('change', (e) => {
|
|
this.extractActionBtn.textContent =
|
|
e.target.checked ? '이미지 모드로 추출' : '텍스트 모드로 추출';
|
|
});
|
|
}
|
|
|
|
async handleExtract() {
|
|
const file = this.extractorFileInput.files[0];
|
|
if (!file) {
|
|
alert('선택된 파일이 없습니다.');
|
|
return;
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
formData.append('useImageMode', this.useImageMode.checked);
|
|
|
|
this.extractActionBtn.disabled = true;
|
|
this.extractActionBtn.textContent = '추출 중...';
|
|
|
|
try {
|
|
const response = await fetch('/api/pid/extract', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
this.showResult(result);
|
|
this.loadEquipmentList();
|
|
this.loadStatistics();
|
|
|
|
alert(`추출 완료! 총 ${result.totalCount}건 처리됨`);
|
|
} catch (error) {
|
|
console.error('추출 실패:', error);
|
|
alert('추출 중 오류가 발생했습니다.');
|
|
} finally {
|
|
this.extractActionBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
showResult(result) {
|
|
// 결과 표시 UI
|
|
alert(`${result.totalCount}건 ${result.confidenceItems}건 신뢰도 높음`);
|
|
}
|
|
}
|
|
|
|
// 애플리케이션 초기화
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
new PidManager();
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 📝 작업 순서
|
|
|
|
### 단계 1: DB 구조 생성
|
|
1. [`PidDbContext.cs`](../src/Infrastructure/Database/PidDbContext.cs) 생성
|
|
2. [`PidEquipment.cs`](../src/Core/Domain/Entities/PidEquipment.cs) 엔티티 생성
|
|
3. [`PidAuditLog.cs`](../src/Core/Domain/Entities/PidAuditLog.cs) 엔티티 생성
|
|
4. [`Program.cs`](../src/Web/Program.cs)에 서비스 등록 (`AddDbContext<PidDbContext>`)
|
|
|
|
### 단계 2: 커맨드라인 도구 개발
|
|
1. [`PidExtractorService.cs`](../src/Core/Application/Services/PidExtractorService.cs) 개발
|
|
2. CLIP 기반 추출기 연동 (Python `pid_extractor.py`)
|
|
3. 테스트용 DXF/PDF 파일 생성
|
|
4. 통합 테스트 수행
|
|
|
|
### 단계 3: Web API 개발
|
|
1. [`IPidExtractorService.cs`](../src/Core/Application/Interfaces/IPidExtractorService.cs) 인터페이스 정의
|
|
2. [`PidController.cs`](../src/Web/Controllers/PidController.cs) 개발
|
|
3. CSV/Excel 다운로드 엔드포인트
|
|
4. 검증된 데이터 필터링 기능
|
|
|
|
### 단계 4: Firebase 연동
|
|
1. P&ID 추출된 태그와 Experion 실시간 태그 매핑
|
|
2. 실시간 값 업데이트 동기화
|
|
|
|
### 단계 5: Frontend UI
|
|
1. P&ID 추출 화면 추가
|
|
2. 장비 목록 표시 및 필터링
|
|
3. 신뢰도 시각화
|
|
4. 검토 필요 항목 표시
|
|
|
|
### 단계 6: 최적화 및 모듈화
|
|
1. PDF→이미지 변환 속도 최적화
|
|
2. 대용량 파일 처리 스트리밍
|
|
3. API 응답 최적화
|
|
|
|
---
|
|
|
|
## ⚠️ 주의사항
|
|
|
|
1. **권한 문제**: `/temp/` 디렉토리에 PDF 변환된 이미지를 저장하므로 쓰기 권한 확인 필요
|
|
2. **API 비용**: Claude Vision API 사용 시 비용 발생 가능 → 캐싱 전략 필요
|
|
3. **대용량 파일**: DXF 이미지 모드는 느림 → 사용자에게 선택권 제공
|
|
4. **네트워크**: Anthropic API 사용을 위해 외부 연결 필요
|
|
|
|
---
|
|
|
|
## 📊 성공 지표
|
|
|
|
- DXF/PDF 파일로부터 평균 성공 추출률 80% 이상
|
|
- 100MB 이하 파일 처리 시 응답 시간 30초 이내
|
|
- 신뢰도 0.7 이상 항목 자동 검증 기능
|
|
- Redis 캐싱으로 API 요청 50% 감소
|
|
|
|
---
|
|
|
|
## 🚀 다음 단계
|
|
|
|
1. 현재 코드 베이스 검토 (`Program.cs`, `ExperionDbContext.cs`)
|
|
2. `PID REST API` 기능 우선 구현
|
|
3. Frontend 인터페이스
|
|
4. Firebase 실시간 연동
|
|
5. 모델 최적화 및 테스트 |