# 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 { get; set; } public DbSet PidAuditLog { get; set; } // 기존 ExperionDbContext와 통합 public DbSet TagInfo { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { // PidEquipment 설정 modelBuilder.Entity(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(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 { get; set; } public DbSet 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 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(); 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 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 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 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 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`) ### 단계 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. 모델 최적화 및 테스트