Files
ExperionCrawler/dxf-graph/P&ID_AX_Plan.md
2026-05-08 17:22:10 +09:00

20 KiB

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 (새 파일)

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 구조 생성

  1. PidDbContext.cs 생성
  2. PidEquipment.cs 엔티티 생성
  3. PidAuditLog.cs 엔티티 생성
  4. Program.cs에 서비스 등록 (AddDbContext<PidDbContext>)

단계 2: 커맨드라인 도구 개발

  1. PidExtractorService.cs 개발
  2. CLIP 기반 추출기 연동 (Python pid_extractor.py)
  3. 테스트용 DXF/PDF 파일 생성
  4. 통합 테스트 수행

단계 3: Web API 개발

  1. IPidExtractorService.cs 인터페이스 정의
  2. 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. 모델 최적화 및 테스트