20 KiB
20 KiB
P&ID 데이터베이스화 기능 통합 설계
📋 개요
DXF/PDF 형식의 P&ID 도면에서 장비 및 계기 정보를 AI로 자동 추출하여 ExperionCrawler 데이터베이스와 연동하는 기능입니다.
🎯 목표
- P&ID 도면에서 장비 정보를 추출
- 추출된 정보를 PostgreSQL 로 저장
- 기존 Experion 데이터와 연동
- 웹에서 시각화 및 관리
🏗️ 아키텍처 설계
┌─────────────────────────────────────────────────────────────────────┐
│ 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 (새 파일)
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 확장
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 (핵심 서비스)
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)
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)
// 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 구조 생성
PidDbContext.cs생성PidEquipment.cs엔티티 생성PidAuditLog.cs엔티티 생성Program.cs에 서비스 등록 (AddDbContext<PidDbContext>)
단계 2: 커맨드라인 도구 개발
PidExtractorService.cs개발- CLIP 기반 추출기 연동 (Python
pid_extractor.py) - 테스트용 DXF/PDF 파일 생성
- 통합 테스트 수행
단계 3: Web API 개발
IPidExtractorService.cs인터페이스 정의PidController.cs개발- CSV/Excel 다운로드 엔드포인트
- 검증된 데이터 필터링 기능
단계 4: Firebase 연동
- P&ID 추출된 태그와 Experion 실시간 태그 매핑
- 실시간 값 업데이트 동기화
단계 5: Frontend UI
- P&ID 추출 화면 추가
- 장비 목록 표시 및 필터링
- 신뢰도 시각화
- 검토 필요 항목 표시
단계 6: 최적화 및 모듈화
- PDF→이미지 변환 속도 최적화
- 대용량 파일 처리 스트리밍
- API 응답 최적화
⚠️ 주의사항
- 권한 문제:
/temp/디렉토리에 PDF 변환된 이미지를 저장하므로 쓰기 권한 확인 필요 - API 비용: Claude Vision API 사용 시 비용 발생 가능 → 캐싱 전략 필요
- 대용량 파일: DXF 이미지 모드는 느림 → 사용자에게 선택권 제공
- 네트워크: Anthropic API 사용을 위해 외부 연결 필요
📊 성공 지표
- DXF/PDF 파일로부터 평균 성공 추출률 80% 이상
- 100MB 이하 파일 처리 시 응답 시간 30초 이내
- 신뢰도 0.7 이상 항목 자동 검증 기능
- Redis 캐싱으로 API 요청 50% 감소
🚀 다음 단계
- 현재 코드 베이스 검토 (
Program.cs,ExperionDbContext.cs) PID REST API기능 우선 구현- Frontend 인터페이스
- Firebase 실시간 연동
- 모델 최적화 및 테스트