Compare commits
18 Commits
d24ab69bb1
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52fbc1c967 | ||
|
|
f114b8b642 | ||
|
|
3abb3c07ce | ||
|
|
107eab90fa | ||
|
|
9c76b624bf | ||
|
|
0278688b28 | ||
|
|
22aa118316 | ||
|
|
5ead0e8045 | ||
|
|
66dd64306c | ||
|
|
8bd5d9580c | ||
|
|
9b94b59c49 | ||
|
|
ddb4a1c408 | ||
|
|
24b5ab9686 | ||
|
|
a87644d8be | ||
|
|
b13e981d04 | ||
|
|
5282927833 | ||
|
|
348ebd1158 | ||
|
|
4a4a0138da |
20
.claude/settings.local.json
Normal file
20
.claude/settings.local.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(dotnet build)",
|
||||
"Bash(\"C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\MSBuild\\Current\\Bin\\MSBuild.exe\" DwgExtractorManual.csproj -p:Configuration=Debug -p:Platform=x64)",
|
||||
"Bash(mkdir:*)",
|
||||
"Bash(where msbuild)",
|
||||
"Bash(\"C:\\Program Files\\Microsoft Visual Studio\\2022\\Professional\\MSBuild\\Current\\Bin\\amd64\\MSBuild.exe\" DwgExtractorManual.csproj -p:Configuration=Debug -p:Platform=x64)",
|
||||
"Bash(dotnet run:*)",
|
||||
"Bash(echo $HOME)",
|
||||
"Bash(find:*)",
|
||||
"Bash(dotnet clean:*)",
|
||||
"Bash(dotnet build:*)",
|
||||
"Bash(taskkill:*)",
|
||||
"Bash(wmic process where ProcessId=17428 delete:*)"
|
||||
],
|
||||
"deny": []
|
||||
},
|
||||
"contextFileName": "AGENTS.md"
|
||||
}
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -361,3 +361,6 @@ MigrationBackup/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
.venv/
|
||||
venv/
|
||||
|
||||
85
AGENTS.md
Normal file
85
AGENTS.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a C# WPF application that extracts data from DWG (AutoCAD) files and processes them using AI analysis. The application has two main components:
|
||||
|
||||
1. **C# WPF Application** (`DwgExtractorManual`) - Main GUI application for DWG processing
|
||||
2. **Python Analysis Module** (`fletimageanalysis`) - AI-powered document analysis using Gemini API
|
||||
|
||||
## Build and Development Commands
|
||||
|
||||
### C# Application
|
||||
```bash
|
||||
# Build the application
|
||||
dotnet build
|
||||
|
||||
# Run the application
|
||||
dotnet run
|
||||
|
||||
# Clean build artifacts
|
||||
dotnet clean
|
||||
|
||||
# Publish for deployment
|
||||
dotnet publish -c Release
|
||||
```
|
||||
|
||||
### Python Module Setup
|
||||
```bash
|
||||
# Run the cleanup and setup script (Windows)
|
||||
cleanup_and_setup.bat
|
||||
|
||||
# Or manually setup Python environment
|
||||
cd fletimageanalysis
|
||||
python -m venv venv
|
||||
call venv\Scripts\activate.bat
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Python CLI Usage
|
||||
```bash
|
||||
# Batch process files via CLI
|
||||
cd fletimageanalysis
|
||||
python batch_cli.py --files "file1.pdf,file2.dxf" --schema "한국도로공사" --concurrent 3 --output "results.csv"
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### C# Component Structure
|
||||
- **MainWindow.xaml.cs** - Main WPF window and UI logic
|
||||
- **Models/DwgDataExtractor.cs** - Core DWG file processing using Teigha SDK
|
||||
- **Models/ExcelDataWriter.cs** - Excel output generation using Office Interop
|
||||
- **Models/TeighaServicesManager.cs** - Singleton manager for Teigha SDK lifecycle
|
||||
- **Models/FieldMapper.cs** - Maps extracted data to target formats
|
||||
- **Models/SettingsManager.cs** - Application configuration management
|
||||
|
||||
### Python Component Structure
|
||||
- **batch_cli.py** - Command-line interface for batch processing
|
||||
- **multi_file_processor.py** - Orchestrates multi-file processing workflows
|
||||
- **gemini_analyzer.py** - AI analysis using Google Gemini API
|
||||
- **pdf_processor.py** - PDF document processing
|
||||
- **dxf_processor.py** - DXF file processing
|
||||
- **csv_exporter.py** - CSV output generation
|
||||
|
||||
### Key Dependencies
|
||||
- **Teigha SDK** - DWG file reading and CAD entity processing (requires DLL files in specific path)
|
||||
- **Microsoft Office Interop** - Excel file generation
|
||||
- **Npgsql** - PostgreSQL database connectivity
|
||||
- **Google Gemini API** - AI-powered document analysis
|
||||
- **PyMuPDF** - PDF processing in Python component
|
||||
|
||||
## Current Development Focus
|
||||
|
||||
The project is undergoing a **Note Detection Refactor** (see `NoteDetectionRefactor.md`):
|
||||
- Replacing fragile "horizontal search line" algorithm in `DwgDataExtractor.cs`
|
||||
- Implementing robust "vertical ray-casting" approach for NOTE content box detection
|
||||
- Key methods being refactored: `FindNoteBox`, `GetAllLineSegments`, `TraceBoxFromTopLine`
|
||||
|
||||
## Important Notes
|
||||
|
||||
- Teigha DLLs must be present in the specified path for DWG processing to work
|
||||
- The Python module requires Google Gemini API key configuration
|
||||
- Excel output uses COM Interop and requires Microsoft Office installation
|
||||
- The application supports both manual GUI operation and automated batch processing via CLI
|
||||
14
App.config
Normal file
14
App.config
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<configuration>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="office"
|
||||
publicKeyToken="71e9bce111e9429c"
|
||||
culture="neutral" />
|
||||
<bindingRedirect oldVersion="15.0.0.0"
|
||||
newVersion="16.0.0.0" />
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
</configuration>
|
||||
144
Controls/ZoomBorder.cs
Normal file
144
Controls/ZoomBorder.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using Cursors = System.Windows.Input.Cursors;
|
||||
using Point = System.Windows.Point;
|
||||
|
||||
namespace DwgExtractorManual.Controls
|
||||
{
|
||||
public class ZoomBorder : Border
|
||||
{
|
||||
private UIElement? child = null;
|
||||
private Point origin;
|
||||
private Point start;
|
||||
|
||||
private TranslateTransform GetTranslateTransform(UIElement element)
|
||||
{
|
||||
return (TranslateTransform)((TransformGroup)element.RenderTransform)
|
||||
.Children.First(tr => tr is TranslateTransform);
|
||||
}
|
||||
|
||||
private ScaleTransform GetScaleTransform(UIElement element)
|
||||
{
|
||||
return (ScaleTransform)((TransformGroup)element.RenderTransform)
|
||||
.Children.First(tr => tr is ScaleTransform);
|
||||
}
|
||||
|
||||
public override UIElement Child
|
||||
{
|
||||
get { return base.Child; }
|
||||
set
|
||||
{
|
||||
if (value != null && value != this.Child)
|
||||
this.Initialize(value);
|
||||
base.Child = value;
|
||||
}
|
||||
}
|
||||
|
||||
public void Initialize(UIElement element)
|
||||
{
|
||||
this.child = element;
|
||||
if (child != null)
|
||||
{
|
||||
TransformGroup group = new TransformGroup();
|
||||
ScaleTransform st = new ScaleTransform();
|
||||
group.Children.Add(st);
|
||||
TranslateTransform tt = new TranslateTransform();
|
||||
group.Children.Add(tt);
|
||||
child.RenderTransform = group;
|
||||
child.RenderTransformOrigin = new Point(0.0, 0.0);
|
||||
this.MouseWheel += child_MouseWheel;
|
||||
this.MouseLeftButtonDown += child_MouseLeftButtonDown;
|
||||
this.MouseLeftButtonUp += child_MouseLeftButtonUp;
|
||||
this.MouseMove += child_MouseMove;
|
||||
this.PreviewMouseRightButtonDown += new MouseButtonEventHandler(
|
||||
child_PreviewMouseRightButtonDown);
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
if (child != null)
|
||||
{
|
||||
// reset zoom
|
||||
var st = GetScaleTransform(child);
|
||||
st.ScaleX = 1.0;
|
||||
st.ScaleY = 1.0;
|
||||
|
||||
// reset pan
|
||||
var tt = GetTranslateTransform(child);
|
||||
tt.X = 0.0;
|
||||
tt.Y = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
private void child_MouseWheel(object sender, MouseWheelEventArgs e)
|
||||
{
|
||||
if (child != null)
|
||||
{
|
||||
var st = GetScaleTransform(child);
|
||||
var tt = GetTranslateTransform(child);
|
||||
|
||||
double zoom = e.Delta > 0 ? .2 : -.2;
|
||||
if (!(e.Delta > 0) && (st.ScaleX < .4 || st.ScaleY < .4))
|
||||
return;
|
||||
|
||||
Point relative = e.GetPosition(child);
|
||||
double absoluteX;
|
||||
double absoluteY;
|
||||
|
||||
absoluteX = relative.X * st.ScaleX + tt.X;
|
||||
absoluteY = relative.Y * st.ScaleY + tt.Y;
|
||||
|
||||
st.ScaleX += zoom;
|
||||
st.ScaleY += zoom;
|
||||
|
||||
tt.X = absoluteX - relative.X * st.ScaleX;
|
||||
tt.Y = absoluteY - relative.Y * st.ScaleY;
|
||||
}
|
||||
}
|
||||
|
||||
private void child_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (child != null)
|
||||
{
|
||||
var tt = GetTranslateTransform(child);
|
||||
start = e.GetPosition(this);
|
||||
origin = new Point(tt.X, tt.Y);
|
||||
this.Cursor = Cursors.Hand;
|
||||
child.CaptureMouse();
|
||||
}
|
||||
}
|
||||
|
||||
private void child_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (child != null)
|
||||
{
|
||||
child.ReleaseMouseCapture();
|
||||
this.Cursor = Cursors.Arrow;
|
||||
}
|
||||
}
|
||||
|
||||
void child_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
this.Reset();
|
||||
}
|
||||
|
||||
private void child_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
|
||||
{
|
||||
if (child != null)
|
||||
{
|
||||
if (child.IsMouseCaptured)
|
||||
{
|
||||
var tt = GetTranslateTransform(child);
|
||||
Vector v = start - e.GetPosition(this);
|
||||
tt.X = origin.X - v.X;
|
||||
tt.Y = origin.Y - v.Y;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,28 +6,53 @@
|
||||
<UseWindowsForms>True</UseWindowsForms>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<COMReference Include="Microsoft.Office.Interop.Excel">
|
||||
<VersionMinor>9</VersionMinor>
|
||||
<VersionMajor>1</VersionMajor>
|
||||
<Guid>00020813-0000-0000-c000-000000000046</Guid>
|
||||
<Lcid>0</Lcid>
|
||||
<WrapperTool>tlbimp</WrapperTool>
|
||||
<Isolated>false</Isolated>
|
||||
<EmbedInteropTypes>true</EmbedInteropTypes>
|
||||
</COMReference>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Npgsql" Version="9.0.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
|
||||
|
||||
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Teigha DLL 참조 - 실제 경로로 수정 필요 -->
|
||||
<!-- Copy all Teigha DLLs including native dependencies -->
|
||||
<ItemGroup>
|
||||
<None Include="D:\dev_Net8_git\trunk\DLL\Teigha\vc16_amd64dll_23.12SP2\*.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="TD_Mgd_23.12_16">
|
||||
<HintPath>..\..\..\..\GitNet8\trunk\DLL\Teigha\vc16_amd64dll_23.12SP2\TD_Mgd_23.12_16.dll</HintPath>
|
||||
<HintPath>..\..\..\GitNet8\trunk\DLL\Teigha\vc16_amd64dll_23.12SP2\TD_Mgd_23.12_16.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.Office.Interop.Excel">
|
||||
<HintPath>C:\Program Files (x86)\Microsoft Office\Office16\DCF\Microsoft.Office.Interop.Excel.dll</HintPath>
|
||||
<EmbedInteropTypes>false</EmbedInteropTypes>
|
||||
</Reference>
|
||||
<Reference Include="office">
|
||||
<HintPath>C:\Program Files (x86)\Microsoft Office\Office16\DCF\office.dll</HintPath>
|
||||
<EmbedInteropTypes>false</EmbedInteropTypes>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="fletimageanalysis\**\*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<Link>fletimageanalysis\%(RecursiveDir)%(FileName)%(Extension)</Link>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</Project>
|
||||
|
||||
173
IntersectionTestConsole.cs
Normal file
173
IntersectionTestConsole.cs
Normal file
@@ -0,0 +1,173 @@
|
||||
//using System;
|
||||
//using System.Collections.Generic;
|
||||
//using System.Diagnostics;
|
||||
//using System.Linq;
|
||||
//using Teigha.Geometry;
|
||||
//using DwgExtractorManual.Models;
|
||||
|
||||
//namespace DwgExtractorManual
|
||||
//{
|
||||
// /// <summary>
|
||||
// /// 콘솔에서 실행할 수 있는 교차점 테스트 프로그램
|
||||
// /// </summary>
|
||||
// class IntersectionTestConsole
|
||||
// {
|
||||
// static void Main(string[] args)
|
||||
// {
|
||||
// Console.WriteLine("=== 교차점 테스트 프로그램 시작 ===");
|
||||
|
||||
// try
|
||||
// {
|
||||
// RunSimpleIntersectionTest();
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// Console.WriteLine($"오류 발생: {ex.Message}");
|
||||
// Console.WriteLine(ex.StackTrace);
|
||||
// }
|
||||
|
||||
// Console.WriteLine("테스트 완료. 아무 키나 누르세요...");
|
||||
// Console.ReadKey();
|
||||
// }
|
||||
|
||||
// static void RunSimpleIntersectionTest()
|
||||
// {
|
||||
// Console.WriteLine("테스트 시작: 3x4 그리드 생성");
|
||||
|
||||
// // 간단한 3x4 테이블 시뮬레이션 (실제 DWG 없이)
|
||||
// var intersections = new List<IntersectionPoint>();
|
||||
|
||||
// // 수동으로 교차점 생성 (3행 x 4열 = 12개 교차점)
|
||||
// for (int row = 1; row <= 4; row++) // 4개 행
|
||||
// {
|
||||
// for (int col = 1; col <= 5; col++) // 5개 열
|
||||
// {
|
||||
// double x = (col - 1) * 10.0; // 0, 10, 20, 30, 40
|
||||
// double y = (row - 1) * 10.0; // 0, 10, 20, 30
|
||||
|
||||
// int directionBits = CalculateDirectionBits(row, col, 4, 5);
|
||||
|
||||
// var intersection = new IntersectionPoint
|
||||
// {
|
||||
// Position = new Point3d(x, y, 0),
|
||||
// DirectionBits = directionBits,
|
||||
// Row = row,
|
||||
// Column = col
|
||||
// };
|
||||
|
||||
// intersections.Add(intersection);
|
||||
// Console.WriteLine($"교차점 R{row}C{col}: ({x:F0},{y:F0}) - DirectionBits: {directionBits}");
|
||||
// }
|
||||
// }
|
||||
|
||||
// Console.WriteLine($"\n총 {intersections.Count}개 교차점 생성됨");
|
||||
|
||||
// // DirectionBits 검증
|
||||
// TestDirectionBitsValidation(intersections);
|
||||
|
||||
// // 셀 추출 시뮬레이션
|
||||
// TestCellExtraction(intersections);
|
||||
// }
|
||||
|
||||
// static int CalculateDirectionBits(int row, int col, int maxRow, int maxCol)
|
||||
// {
|
||||
// int bits = 0;
|
||||
|
||||
// // Right: 1 - 오른쪽에 더 많은 열이 있으면
|
||||
// if (col < maxCol) bits |= 1;
|
||||
|
||||
// // Up: 2 - 위쪽에 더 많은 행이 있으면
|
||||
// if (row < maxRow) bits |= 2;
|
||||
|
||||
// // Left: 4 - 왼쪽에 열이 있으면
|
||||
// if (col > 1) bits |= 4;
|
||||
|
||||
// // Down: 8 - 아래쪽에 행이 있으면
|
||||
// if (row > 1) bits |= 8;
|
||||
|
||||
// return bits;
|
||||
// }
|
||||
|
||||
// static void TestDirectionBitsValidation(List<IntersectionPoint> intersections)
|
||||
// {
|
||||
// Console.WriteLine("\n=== DirectionBits 검증 ===");
|
||||
|
||||
// var mappingData = new MappingTableData();
|
||||
// var fieldMapper = new FieldMapper(mappingData);
|
||||
// var extractor = new DwgDataExtractor(fieldMapper);
|
||||
|
||||
// foreach (var intersection in intersections)
|
||||
// {
|
||||
// bool isTopLeft = extractor.IsValidTopLeft(intersection.DirectionBits);
|
||||
// bool isBottomRight = extractor.IsValidBottomRight(intersection.DirectionBits);
|
||||
|
||||
// Console.WriteLine($"R{intersection.Row}C{intersection.Column} (bits: {intersection.DirectionBits:D2}) - " +
|
||||
// $"TopLeft: {isTopLeft}, BottomRight: {isBottomRight}");
|
||||
// }
|
||||
// }
|
||||
|
||||
// static void TestCellExtraction(List<IntersectionPoint> intersections)
|
||||
// {
|
||||
// Console.WriteLine("\n=== 셀 추출 테스트 ===");
|
||||
|
||||
// var mappingData = new MappingTableData();
|
||||
// var fieldMapper = new FieldMapper(mappingData);
|
||||
// var extractor = new DwgDataExtractor(fieldMapper);
|
||||
|
||||
// // topLeft 후보들 찾기
|
||||
// var topLeftCandidates = intersections.Where(i => extractor.IsValidTopLeft(i.DirectionBits)).ToList();
|
||||
// Console.WriteLine($"TopLeft 후보: {topLeftCandidates.Count}개");
|
||||
|
||||
// foreach (var topLeft in topLeftCandidates)
|
||||
// {
|
||||
// Console.WriteLine($"\nTopLeft R{topLeft.Row}C{topLeft.Column} 처리 중...");
|
||||
|
||||
// // bottomRight 찾기 시뮬레이션
|
||||
// var bottomRight = FindBottomRightSimulation(topLeft, intersections, extractor);
|
||||
|
||||
// if (bottomRight != null)
|
||||
// {
|
||||
// Console.WriteLine($" -> BottomRight 발견: R{bottomRight.Row}C{bottomRight.Column}");
|
||||
// Console.WriteLine($" 셀 생성: ({topLeft.Position.X:F0},{bottomRight.Position.Y:F0}) to ({bottomRight.Position.X:F0},{topLeft.Position.Y:F0})");
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// Console.WriteLine(" -> BottomRight을 찾지 못함");
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// static IntersectionPoint? FindBottomRightSimulation(IntersectionPoint topLeft, List<IntersectionPoint> intersections, DwgDataExtractor extractor)
|
||||
// {
|
||||
// // 교차점들을 Row/Column으로 딕셔너리 구성
|
||||
// var intersectionLookup = intersections
|
||||
// .GroupBy(i => i.Row)
|
||||
// .ToDictionary(g => g.Key, g => g.ToDictionary(i => i.Column, i => i));
|
||||
|
||||
// int maxRow = intersectionLookup.Keys.Max();
|
||||
// int maxColumn = intersectionLookup.Values.SelectMany(row => row.Keys).Max();
|
||||
|
||||
// for (int targetRow = topLeft.Row + 1; targetRow <= maxRow + 1; targetRow++)
|
||||
// {
|
||||
// if (!intersectionLookup.ContainsKey(targetRow)) continue;
|
||||
|
||||
// var rowIntersections = intersectionLookup[targetRow];
|
||||
// var availableColumns = rowIntersections.Keys.Where(col => col >= topLeft.Column).OrderBy(col => col);
|
||||
|
||||
// foreach (int targetColumn in availableColumns)
|
||||
// {
|
||||
// var candidate = rowIntersections[targetColumn];
|
||||
|
||||
// // bottomRight 검증 또는 테이블 경계 조건
|
||||
// if (extractor.IsValidBottomRight(candidate.DirectionBits) ||
|
||||
// (targetRow == maxRow && targetColumn == maxColumn))
|
||||
// {
|
||||
// return candidate;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// return null;
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
82
MainWindow.Visualization.cs
Normal file
82
MainWindow.Visualization.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using DwgExtractorManual.Models;
|
||||
using DwgExtractorManual.Views;
|
||||
using MessageBox = System.Windows.MessageBox;
|
||||
|
||||
namespace DwgExtractorManual
|
||||
{
|
||||
/// <summary>
|
||||
/// MainWindow의 시각화 관련 기능을 담당하는 partial class
|
||||
/// </summary>
|
||||
public partial class MainWindow
|
||||
{
|
||||
// 시각화 데이터 저장
|
||||
private static List<TableCellVisualizationData> _visualizationDataCache = new List<TableCellVisualizationData>();
|
||||
|
||||
/// <summary>
|
||||
/// 테이블 셀 시각화 데이터를 저장합니다.
|
||||
/// </summary>
|
||||
public static void SaveVisualizationData(TableCellVisualizationData data)
|
||||
{
|
||||
_visualizationDataCache.Add(data);
|
||||
Debug.WriteLine($"[VISUALIZATION] 시각화 데이터 저장: {data.FileName}, 셀 수: {data.Cells.Count}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 저장된 시각화 데이터를 가져옵니다.
|
||||
/// </summary>
|
||||
public static List<TableCellVisualizationData> GetVisualizationData()
|
||||
{
|
||||
Debug.WriteLine($"[VISUALIZATION] 시각화 데이터 조회: {_visualizationDataCache.Count}개 항목");
|
||||
return new List<TableCellVisualizationData>(_visualizationDataCache);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 시각화 데이터를 초기화합니다.
|
||||
/// </summary>
|
||||
public static void ClearVisualizationData()
|
||||
{
|
||||
Debug.WriteLine($"[VISUALIZATION] 시각화 데이터 초기화 (기존 {_visualizationDataCache.Count}개 항목 삭제)");
|
||||
_visualizationDataCache.Clear();
|
||||
Debug.WriteLine("[VISUALIZATION] 시각화 데이터 초기화 완료");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 테이블 셀 시각화 창을 엽니다.
|
||||
/// </summary>
|
||||
private void BtnVisualizeCells_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
LogMessage("🎨 테이블 셀 시각화 창을 여는 중...");
|
||||
|
||||
var visualizationData = GetVisualizationData();
|
||||
LogMessage($"[DEBUG] 조회된 시각화 데이터: {visualizationData.Count}개");
|
||||
|
||||
if (visualizationData.Count == 0)
|
||||
{
|
||||
MessageBox.Show("시각화할 데이터가 없습니다.\n먼저 'DWG추출(Height정렬)' 버튼을 눌러 데이터를 추출해주세요.",
|
||||
"데이터 없음", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
LogMessage("⚠️ 시각화할 데이터가 없습니다. 먼저 추출을 진행해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
var visualizationWindow = new TableCellVisualizationWindow(visualizationData);
|
||||
visualizationWindow.Owner = this;
|
||||
visualizationWindow.Show();
|
||||
|
||||
LogMessage($"✅ 시각화 창 열기 완료 - {visualizationData.Count}개 파일 데이터");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogMessage($"❌ 시각화 창 열기 중 오류: {ex.Message}");
|
||||
MessageBox.Show($"시각화 창을 여는 중 오류가 발생했습니다:\n{ex.Message}",
|
||||
"오류", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
171
MainWindow.xaml
171
MainWindow.xaml
@@ -1,7 +1,7 @@
|
||||
<Window x:Class="DwgExtractorManual.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="DWG 정보 추출기" Height="700" Width="900"
|
||||
Title="DWG 정보 추출기" Height="Auto" Width="900"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
MinHeight="600" MinWidth="800">
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
@@ -89,6 +90,7 @@
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 진행률 바 -->
|
||||
@@ -100,29 +102,154 @@
|
||||
Text="준비됨" FontSize="12"
|
||||
HorizontalAlignment="Center" Margin="0,5"/>
|
||||
|
||||
<!-- 추출 버튼 -->
|
||||
<Button x:Name="btnExtract" Grid.Row="2"
|
||||
Content="🚀 추출 시작" Width="200" Height="45"
|
||||
Margin="0,10,0,5" HorizontalAlignment="Center"
|
||||
Click="BtnExtract_Click" FontSize="16" FontWeight="Bold"
|
||||
Background="#A3BE8C" Foreground="White"
|
||||
BorderThickness="0">
|
||||
<Button.Style>
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Background" Value="#A3BE8C"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#8FAE74"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
</Button>
|
||||
<!-- 추출 버튼들 -->
|
||||
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,10,0,5">
|
||||
<Button x:Name="btnExtract"
|
||||
Content="🚀 DWG 추출" Width="150" Height="45"
|
||||
Margin="5,0"
|
||||
Click="BtnExtract_Click" FontSize="14" FontWeight="Bold"
|
||||
Background="#A3BE8C" Foreground="White"
|
||||
BorderThickness="0">
|
||||
<Button.Style>
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Background" Value="#A3BE8C"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#8FAE74"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
</Button>
|
||||
<Button x:Name="btnPdfExtract"
|
||||
Content="📄 PDF 추출" Width="150" Height="45"
|
||||
Margin="5,0"
|
||||
Click="BtnPdfExtract_Click" FontSize="14" FontWeight="Bold"
|
||||
Background="#D08770" Foreground="White"
|
||||
BorderThickness="0">
|
||||
<Button.Style>
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Background" Value="#D08770"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#C5774F"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
</Button>
|
||||
<Button x:Name="btnMerge"
|
||||
Content="🔗 합치기" Width="150" Height="45"
|
||||
Margin="5,0"
|
||||
Click="BtnMerge_Click" FontSize="14" FontWeight="Bold"
|
||||
Background="#B48EAD" Foreground="White"
|
||||
BorderThickness="0">
|
||||
<Button.Style>
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Background" Value="#B48EAD"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#A07A95"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
</Button>
|
||||
<Button x:Name="btnAuto"
|
||||
Content="🤖 자동" Width="150" Height="45"
|
||||
Margin="5,0"
|
||||
Click="BtnAuto_Click" FontSize="14" FontWeight="Bold"
|
||||
Background="#4CAF50" Foreground="White"
|
||||
BorderThickness="0">
|
||||
<Button.Style>
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Background" Value="#4CAF50"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#45A049"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Second row of buttons -->
|
||||
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,5,0,10">
|
||||
<Button x:Name="btnDwgOnly"
|
||||
Content="🔧 DWG추출(폴더별)" Width="150" Height="45"
|
||||
Margin="5,0"
|
||||
Click="BtnDwgOnly_Click" FontSize="14" FontWeight="Bold"
|
||||
Background="#8B4513" Foreground="White"
|
||||
BorderThickness="0">
|
||||
<Button.Style>
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Background" Value="#8B4513"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#A0522D"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
</Button>
|
||||
<Button x:Name="btnDwgHeightSort"
|
||||
Content="📏 DWG추출(Height정렬)" Width="150" Height="45"
|
||||
Margin="5,0"
|
||||
Click="BtnDwgHeightSort_Click" FontSize="14" FontWeight="Bold"
|
||||
Background="#FF6B35" Foreground="White"
|
||||
BorderThickness="0">
|
||||
<Button.Style>
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Background" Value="#FF6B35"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#E55A2B"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
</Button>
|
||||
<Button x:Name="btnVisualizeCells"
|
||||
Content="🎨 셀 시각화" Width="150" Height="45"
|
||||
Margin="5,0"
|
||||
Click="BtnVisualizeCells_Click" FontSize="14" FontWeight="Bold"
|
||||
Background="#9B59B6" Foreground="White"
|
||||
BorderThickness="0">
|
||||
<Button.Style>
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Background" Value="#9B59B6"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#8E44AD"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
</Button>
|
||||
<Button x:Name="btnTestIntersection"
|
||||
Content="🔬 교차점 테스트" Width="150" Height="45"
|
||||
Margin="5,0"
|
||||
Click="BtnTestIntersection_Click" FontSize="14" FontWeight="Bold"
|
||||
Background="#E74C3C" Foreground="White"
|
||||
BorderThickness="0">
|
||||
<Button.Style>
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Background" Value="#E74C3C"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#C0392B"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
|
||||
<!-- 로그 출력 -->
|
||||
<GroupBox Grid.Row="5" Header="📋 실시간 로그" Margin="15,5,15,10"
|
||||
<GroupBox Grid.Row="6" Header="📋 실시간 로그" Margin="15,5,15,10" Height="300"
|
||||
FontWeight="SemiBold" FontSize="14">
|
||||
<ScrollViewer Margin="5" VerticalScrollBarVisibility="Auto">
|
||||
<TextBox x:Name="txtLog"
|
||||
@@ -135,12 +262,14 @@
|
||||
</GroupBox>
|
||||
|
||||
<!-- 상태바 -->
|
||||
<StatusBar Grid.Row="6" Background="#3B4252" Foreground="White">
|
||||
<StatusBar Grid.Row="7" Background="#3B4252" Foreground="White">
|
||||
<StatusBarItem>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock x:Name="txtStatusBar" Text="DWG 정보 추출기 v1.0 - 준비됨"/>
|
||||
<Separator Margin="10,0"/>
|
||||
<TextBlock x:Name="txtFileCount" Text="파일: 0개"/>
|
||||
<Separator Margin="10,0"/>
|
||||
<TextBlock x:Name="txtBuildTime" Text="빌드: 로딩중..." FontSize="11" Foreground="LightGray"/>
|
||||
</StackPanel>
|
||||
</StatusBarItem>
|
||||
<StatusBarItem HorizontalAlignment="Right">
|
||||
|
||||
2485
MainWindow.xaml.cs
2485
MainWindow.xaml.cs
File diff suppressed because it is too large
Load Diff
9
Models/AppSettings.cs
Normal file
9
Models/AppSettings.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace DwgExtractorManual.Models
|
||||
{
|
||||
public class AppSettings
|
||||
{
|
||||
public string? SourceFolderPath { get; set; }
|
||||
public string? DestinationFolderPath { get; set; }
|
||||
public string? LastExportType { get; set; }
|
||||
}
|
||||
}
|
||||
261
Models/CsvDataWriter.cs
Normal file
261
Models/CsvDataWriter.cs
Normal file
@@ -0,0 +1,261 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DwgExtractorManual.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Note 데이터를 CSV 파일로 출력하는 클래스
|
||||
/// Note Box 안의 일반 텍스트와 테이블 텍스트를 분리하여 CSV로 출력
|
||||
/// </summary>
|
||||
public class CsvDataWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// Note 박스 안의 일반 텍스트들을 CSV 파일로 저장
|
||||
/// </summary>
|
||||
public void WriteNoteBoxTextToCsv(List<NoteEntityInfo> noteEntities, string filePath)
|
||||
{
|
||||
if (noteEntities == null || noteEntities.Count == 0)
|
||||
return;
|
||||
|
||||
var csvLines = new List<string>();
|
||||
|
||||
// CSV 헤더 추가
|
||||
csvLines.Add("Type,Layer,Text,X,Y,SortOrder,Path,FileName");
|
||||
|
||||
// Note와 NoteContent 데이터 추출 (테이블 제외)
|
||||
var noteBoxTexts = noteEntities
|
||||
.Where(ne => ne.Type == "Note" || ne.Type == "NoteContent")
|
||||
.OrderBy(ne => ne.SortOrder)
|
||||
.ToList();
|
||||
|
||||
foreach (var noteEntity in noteBoxTexts)
|
||||
{
|
||||
var csvLine = $"{EscapeCsvField(noteEntity.Type)}," +
|
||||
$"{EscapeCsvField(noteEntity.Layer)}," +
|
||||
$"{EscapeCsvField(noteEntity.Text)}," +
|
||||
$"{noteEntity.X:F3}," +
|
||||
$"{noteEntity.Y:F3}," +
|
||||
$"{noteEntity.SortOrder}," +
|
||||
$"{EscapeCsvField(noteEntity.Path)}," +
|
||||
$"{EscapeCsvField(noteEntity.FileName)}";
|
||||
|
||||
csvLines.Add(csvLine);
|
||||
}
|
||||
|
||||
// UTF-8 BOM 포함하여 파일 저장 (Excel에서 한글 깨짐 방지)
|
||||
var utf8WithBom = new UTF8Encoding(true);
|
||||
File.WriteAllLines(filePath, csvLines, utf8WithBom);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Note 박스 안의 테이블 데이터들을 별도 CSV 파일로 저장
|
||||
/// </summary>
|
||||
public void WriteNoteTablesToCsv(List<NoteEntityInfo> noteEntities, string filePath)
|
||||
{
|
||||
if (noteEntities == null || noteEntities.Count == 0)
|
||||
return;
|
||||
|
||||
var allCsvLines = new List<string>();
|
||||
|
||||
// 테이블 데이터가 있는 Note들 추출
|
||||
var notesWithTables = noteEntities
|
||||
.Where(ne => ne.Type == "Note" && !string.IsNullOrEmpty(ne.TableCsv))
|
||||
.OrderByDescending(ne => ne.Y) // Y 좌표로 정렬 (위에서 아래로)
|
||||
.ToList();
|
||||
|
||||
foreach (var noteWithTable in notesWithTables)
|
||||
{
|
||||
// Note 정보 헤더 추가
|
||||
allCsvLines.Add($"=== NOTE: {noteWithTable.Text} (at {noteWithTable.X:F1}, {noteWithTable.Y:F1}) ===");
|
||||
allCsvLines.Add(""); // 빈 줄
|
||||
|
||||
// 테이블 CSV 데이터 추가
|
||||
var tableLines = noteWithTable.TableCsv.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
allCsvLines.AddRange(tableLines);
|
||||
|
||||
// Note 간 구분을 위한 빈 줄들
|
||||
allCsvLines.Add("");
|
||||
allCsvLines.Add("");
|
||||
}
|
||||
|
||||
// UTF-8 BOM 포함하여 파일 저장
|
||||
var utf8WithBom = new UTF8Encoding(true);
|
||||
File.WriteAllLines(filePath, allCsvLines, utf8WithBom);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Note 박스와 테이블 데이터를 통합하여 하나의 CSV 파일로 저장
|
||||
/// </summary>
|
||||
public void WriteNoteDataToCombinedCsv(List<NoteEntityInfo> noteEntities, string filePath)
|
||||
{
|
||||
if (noteEntities == null || noteEntities.Count == 0)
|
||||
return;
|
||||
|
||||
var csvLines = new List<string>();
|
||||
|
||||
// CSV 헤더 추가
|
||||
csvLines.Add("Type,Layer,Text,X,Y,SortOrder,TableData,Path,FileName");
|
||||
|
||||
// 모든 Note 관련 데이터를 SortOrder로 정렬
|
||||
var sortedNoteEntities = noteEntities
|
||||
.OrderBy(ne => ne.SortOrder)
|
||||
.ToList();
|
||||
|
||||
foreach (var noteEntity in sortedNoteEntities)
|
||||
{
|
||||
// 테이블 데이터가 있는 경우 이를 별도 필드로 처리
|
||||
var tableData = "";
|
||||
if (noteEntity.Type == "Note" && !string.IsNullOrEmpty(noteEntity.TableCsv))
|
||||
{
|
||||
// 테이블 CSV 데이터를 하나의 필드로 압축 (줄바꿈을 |로 대체)
|
||||
tableData = noteEntity.TableCsv.Replace("\n", "|").Replace("\r", "");
|
||||
}
|
||||
|
||||
var csvLine = $"{EscapeCsvField(noteEntity.Type)}," +
|
||||
$"{EscapeCsvField(noteEntity.Layer)}," +
|
||||
$"{EscapeCsvField(noteEntity.Text)}," +
|
||||
$"{noteEntity.X:F3}," +
|
||||
$"{noteEntity.Y:F3}," +
|
||||
$"{noteEntity.SortOrder}," +
|
||||
$"{EscapeCsvField(tableData)}," +
|
||||
$"{EscapeCsvField(noteEntity.Path)}," +
|
||||
$"{EscapeCsvField(noteEntity.FileName)}";
|
||||
|
||||
csvLines.Add(csvLine);
|
||||
}
|
||||
|
||||
// UTF-8 BOM 포함하여 파일 저장
|
||||
var utf8WithBom = new UTF8Encoding(true);
|
||||
File.WriteAllLines(filePath, csvLines, utf8WithBom);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 각 Note별로 개별 CSV 파일 생성 (테이블이 있는 경우)
|
||||
/// </summary>
|
||||
public void WriteIndividualNoteTablesCsv(List<NoteEntityInfo> noteEntities, string baseDirectory)
|
||||
{
|
||||
if (noteEntities == null || noteEntities.Count == 0)
|
||||
return;
|
||||
|
||||
// 디렉토리가 없으면 생성
|
||||
if (!Directory.Exists(baseDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(baseDirectory);
|
||||
}
|
||||
|
||||
var notesWithTables = noteEntities
|
||||
.Where(ne => ne.Type == "Note" && !string.IsNullOrEmpty(ne.TableCsv))
|
||||
.OrderByDescending(ne => ne.Y)
|
||||
.ToList();
|
||||
|
||||
int noteIndex = 1;
|
||||
foreach (var noteWithTable in notesWithTables)
|
||||
{
|
||||
// 파일명 생성 (특수문자 제거)
|
||||
var safeNoteText = MakeSafeFileName(noteWithTable.Text);
|
||||
var fileName = $"Note_{noteIndex:D2}_{safeNoteText}.csv";
|
||||
var fullPath = Path.Combine(baseDirectory, fileName);
|
||||
|
||||
// 테이블 CSV 데이터를 파일로 저장
|
||||
var tableLines = noteWithTable.TableCsv.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
var utf8WithBom = new UTF8Encoding(true);
|
||||
File.WriteAllLines(fullPath, tableLines, utf8WithBom);
|
||||
|
||||
noteIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CSV 필드에서 특수문자를 이스케이프 처리
|
||||
/// </summary>
|
||||
private string EscapeCsvField(string field)
|
||||
{
|
||||
if (string.IsNullOrEmpty(field))
|
||||
return "";
|
||||
|
||||
// 쉼표, 따옴표, 줄바꿈이 있으면 따옴표로 감싸기
|
||||
if (field.Contains(",") || field.Contains("\"") || field.Contains("\n") || field.Contains("\r"))
|
||||
{
|
||||
return "\"" + field.Replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
|
||||
return field;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 파일명에 사용할 수 없는 문자들을 제거하여 안전한 파일명 생성
|
||||
/// </summary>
|
||||
private string MakeSafeFileName(string fileName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fileName))
|
||||
return "Unknown";
|
||||
|
||||
var invalidChars = Path.GetInvalidFileNameChars();
|
||||
var safeFileName = fileName;
|
||||
|
||||
foreach (var invalidChar in invalidChars)
|
||||
{
|
||||
safeFileName = safeFileName.Replace(invalidChar, '_');
|
||||
}
|
||||
|
||||
// 길이 제한 (Windows 파일명 제한 고려)
|
||||
if (safeFileName.Length > 50)
|
||||
{
|
||||
safeFileName = safeFileName.Substring(0, 50);
|
||||
}
|
||||
|
||||
return safeFileName.Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Note 데이터 통계 정보를 CSV로 저장
|
||||
/// </summary>
|
||||
public void WriteNoteStatisticsToCsv(List<NoteEntityInfo> noteEntities, string filePath)
|
||||
{
|
||||
if (noteEntities == null || noteEntities.Count == 0)
|
||||
return;
|
||||
|
||||
var csvLines = new List<string>();
|
||||
|
||||
// 통계 헤더
|
||||
csvLines.Add("Statistic,Count,Details");
|
||||
|
||||
// 전체 Note 개수
|
||||
var totalNotes = noteEntities.Count(ne => ne.Type == "Note");
|
||||
csvLines.Add($"Total Notes,{totalNotes},");
|
||||
|
||||
// 테이블이 있는 Note 개수
|
||||
var notesWithTables = noteEntities.Count(ne => ne.Type == "Note" && !string.IsNullOrEmpty(ne.TableCsv));
|
||||
csvLines.Add($"Notes with Tables,{notesWithTables},");
|
||||
|
||||
// 일반 텍스트만 있는 Note 개수
|
||||
var notesWithTextOnly = totalNotes - notesWithTables;
|
||||
csvLines.Add($"Notes with Text Only,{notesWithTextOnly},");
|
||||
|
||||
// 전체 Note 콘텐츠 개수
|
||||
var totalNoteContents = noteEntities.Count(ne => ne.Type == "NoteContent");
|
||||
csvLines.Add($"Total Note Contents,{totalNoteContents},");
|
||||
|
||||
// 레이어별 분포
|
||||
csvLines.Add(",,");
|
||||
csvLines.Add("Layer Distribution,,");
|
||||
|
||||
var layerGroups = noteEntities
|
||||
.GroupBy(ne => ne.Layer)
|
||||
.OrderByDescending(g => g.Count())
|
||||
.ToList();
|
||||
|
||||
foreach (var layerGroup in layerGroups)
|
||||
{
|
||||
csvLines.Add($"Layer: {layerGroup.Key},{layerGroup.Count()},");
|
||||
}
|
||||
|
||||
var utf8WithBom = new UTF8Encoding(true);
|
||||
File.WriteAllLines(filePath, csvLines, utf8WithBom);
|
||||
}
|
||||
}
|
||||
}
|
||||
3096
Models/DwgDataExtractor.cs
Normal file
3096
Models/DwgDataExtractor.cs
Normal file
File diff suppressed because it is too large
Load Diff
639
Models/ExcelDataWriter.cs
Normal file
639
Models/ExcelDataWriter.cs
Normal file
@@ -0,0 +1,639 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Excel = Microsoft.Office.Interop.Excel;
|
||||
|
||||
namespace DwgExtractorManual.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Excel <20><>Ʈ<EFBFBD><C6AE> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20>۾<EFBFBD><DBBE><EFBFBD> <20><><EFBFBD><EFBFBD>ϴ<EFBFBD> Ŭ<><C5AC><EFBFBD><EFBFBD>
|
||||
/// </summary>
|
||||
internal class ExcelDataWriter
|
||||
{
|
||||
private readonly ExcelManager excelManager;
|
||||
|
||||
public ExcelDataWriter(ExcelManager excelManager)
|
||||
{
|
||||
this.excelManager = excelManager ?? throw new ArgumentNullException(nameof(excelManager));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Title Block <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Excel <20><>Ʈ<EFBFBD><C6AE> <20><><EFBFBD>
|
||||
/// </summary>
|
||||
public void WriteTitleBlockData(List<TitleBlockRowData> titleBlockRows)
|
||||
{
|
||||
if (excelManager.TitleBlockSheet == null || titleBlockRows == null || titleBlockRows.Count == 0)
|
||||
return;
|
||||
|
||||
int currentRow = 2; // <20><><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>
|
||||
|
||||
foreach (var row in titleBlockRows)
|
||||
{
|
||||
excelManager.TitleBlockSheet.Cells[currentRow, 1] = row.Type;
|
||||
excelManager.TitleBlockSheet.Cells[currentRow, 2] = row.Name;
|
||||
excelManager.TitleBlockSheet.Cells[currentRow, 3] = row.Tag;
|
||||
excelManager.TitleBlockSheet.Cells[currentRow, 4] = row.Prompt;
|
||||
excelManager.TitleBlockSheet.Cells[currentRow, 5] = row.Value;
|
||||
excelManager.TitleBlockSheet.Cells[currentRow, 6] = row.Path;
|
||||
excelManager.TitleBlockSheet.Cells[currentRow, 7] = row.FileName;
|
||||
currentRow++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Text Entity <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Excel <20><>Ʈ<EFBFBD><C6AE> <20><><EFBFBD>
|
||||
/// </summary>
|
||||
public void WriteTextEntityData(List<TextEntityRowData> textEntityRows)
|
||||
{
|
||||
if (excelManager.TextEntitiesSheet == null || textEntityRows == null || textEntityRows.Count == 0)
|
||||
return;
|
||||
|
||||
int currentRow = 2; // <20><><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>
|
||||
|
||||
foreach (var row in textEntityRows)
|
||||
{
|
||||
excelManager.TextEntitiesSheet.Cells[currentRow, 1] = row.Type;
|
||||
excelManager.TextEntitiesSheet.Cells[currentRow, 2] = row.Layer;
|
||||
excelManager.TextEntitiesSheet.Cells[currentRow, 3] = row.Text;
|
||||
excelManager.TextEntitiesSheet.Cells[currentRow, 4] = row.Path;
|
||||
excelManager.TextEntitiesSheet.Cells[currentRow, 5] = row.FileName;
|
||||
currentRow++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Note 엔터티 데이터를 Excel 시트에 쓰기 (테이블 및 셀 병합 포함)
|
||||
/// </summary>
|
||||
public void WriteNoteEntityData(List<NoteEntityInfo> noteEntityRows)
|
||||
{
|
||||
if (excelManager.NoteEntitiesSheet == null || noteEntityRows == null || noteEntityRows.Count == 0)
|
||||
return;
|
||||
|
||||
int excelRow = 2; // 헤더 다음 행부터 시작
|
||||
|
||||
foreach (var note in noteEntityRows)
|
||||
{
|
||||
// 기본 Note 정보 쓰기
|
||||
excelManager.NoteEntitiesSheet.Cells[excelRow, 1] = note.Type;
|
||||
excelManager.NoteEntitiesSheet.Cells[excelRow, 2] = note.Layer;
|
||||
excelManager.NoteEntitiesSheet.Cells[excelRow, 3] = note.Text;
|
||||
excelManager.NoteEntitiesSheet.Cells[excelRow, 4] = note.X;
|
||||
excelManager.NoteEntitiesSheet.Cells[excelRow, 5] = note.Y;
|
||||
excelManager.NoteEntitiesSheet.Cells[excelRow, 6] = note.SortOrder;
|
||||
excelManager.NoteEntitiesSheet.Cells[excelRow, 8] = note.Path;
|
||||
excelManager.NoteEntitiesSheet.Cells[excelRow, 9] = note.FileName;
|
||||
|
||||
int tableRowCount = 0;
|
||||
if (note.Cells != null && note.Cells.Count > 0)
|
||||
{
|
||||
// 테이블 데이터 처리
|
||||
foreach (var cell in note.Cells)
|
||||
{
|
||||
int startRow = excelRow + cell.Row;
|
||||
int startCol = 7 + cell.Column; // G열부터 시작
|
||||
int endRow = startRow + cell.RowSpan - 1;
|
||||
int endCol = startCol + cell.ColumnSpan - 1;
|
||||
|
||||
Excel.Range cellRange = excelManager.NoteEntitiesSheet.Range[
|
||||
excelManager.NoteEntitiesSheet.Cells[startRow, startCol],
|
||||
excelManager.NoteEntitiesSheet.Cells[endRow, endCol]];
|
||||
|
||||
// 병합 먼저 수행
|
||||
if (cell.RowSpan > 1 || cell.ColumnSpan > 1)
|
||||
{
|
||||
cellRange.Merge();
|
||||
}
|
||||
|
||||
// 값 설정 및 서식 지정
|
||||
cellRange.Value = cell.CellText;
|
||||
cellRange.VerticalAlignment = Excel.XlVAlign.xlVAlignCenter;
|
||||
cellRange.HorizontalAlignment = Excel.XlHAlign.xlHAlignCenter;
|
||||
}
|
||||
|
||||
// 이 테이블이 차지하는 총 행 수를 계산
|
||||
tableRowCount = note.Cells.Max(c => c.Row + c.RowSpan);
|
||||
}
|
||||
|
||||
// 다음 Note를 기록할 위치로 이동
|
||||
// 테이블이 있으면 테이블 높이만큼, 없으면 한 칸만 이동
|
||||
excelRow += (tableRowCount > 0) ? tableRowCount : 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Excel <20><>Ʈ<EFBFBD><C6AE> <20><><EFBFBD>
|
||||
/// </summary>
|
||||
public void WriteMappingDataToExcel(Dictionary<string, Dictionary<string, (string, string, string, string)>> mappingData)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (excelManager.MappingSheet == null || mappingData == null)
|
||||
return;
|
||||
|
||||
int currentRow = 2; // <20><><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>
|
||||
|
||||
Debug.WriteLine($"[DEBUG] Writing mapping data to Excel. Total files: {mappingData.Count}");
|
||||
|
||||
foreach (var fileEntry in mappingData)
|
||||
{
|
||||
string fileName = fileEntry.Key;
|
||||
var fileMappingData = fileEntry.Value;
|
||||
|
||||
Debug.WriteLine($"[DEBUG] Processing file: {fileName}, entries: {fileMappingData.Count}");
|
||||
|
||||
foreach (var mapEntry in fileMappingData)
|
||||
{
|
||||
string mapKey = mapEntry.Key;
|
||||
(string aiLabel, string dwgTag, string attValue, string pdfValue) = mapEntry.Value;
|
||||
|
||||
if (string.IsNullOrEmpty(fileName) || string.IsNullOrEmpty(mapKey))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// <20><>ġ <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ʈ<EFBFBD><C6AE> <20><><EFBFBD><EFBFBD> <20>迭 <20><><EFBFBD>
|
||||
object[,] rowData = new object[1, 6];
|
||||
rowData[0, 0] = fileName;
|
||||
rowData[0, 1] = mapKey;
|
||||
rowData[0, 2] = aiLabel ?? "";
|
||||
rowData[0, 3] = dwgTag ?? "";
|
||||
rowData[0, 4] = attValue ?? "";
|
||||
rowData[0, 5] = pdfValue ?? "";
|
||||
|
||||
Excel.Range range = excelManager.MappingSheet.Range[
|
||||
excelManager.MappingSheet.Cells[currentRow, 1],
|
||||
excelManager.MappingSheet.Cells[currentRow, 6]];
|
||||
range.Value = rowData;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"? Error writing row {currentRow}: {ex.Message}");
|
||||
}
|
||||
|
||||
currentRow++;
|
||||
}
|
||||
}
|
||||
|
||||
Debug.WriteLine($"[DEBUG] Mapping data written to Excel. Total rows: {currentRow - 2}");
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"? <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Excel <20><><EFBFBD> <20><> <20><><EFBFBD><EFBFBD>: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Excel <20><><EFBFBD><EFBFBD> <20><>Ʈ<EFBFBD><C6AE><EFBFBD><EFBFBD> FileName<6D><65> AILabel<65><6C> <20><>Ī<EFBFBD>Ǵ<EFBFBD> <20><><EFBFBD><EFBFBD> ã<><C3A3> Pdf_value<75><65> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ʈ
|
||||
/// </summary>
|
||||
public bool UpdateExcelRow(string fileName, string aiLabel, string pdfValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (excelManager.MappingSheet == null)
|
||||
return false;
|
||||
|
||||
Excel.Range usedRange = excelManager.MappingSheet.UsedRange;
|
||||
if (usedRange == null) return false;
|
||||
|
||||
int lastRow = usedRange.Rows.Count;
|
||||
|
||||
for (int row = 2; row <= lastRow; row++)
|
||||
{
|
||||
var cellFileName = ((Excel.Range)excelManager.MappingSheet.Cells[row, 1]).Value?.ToString() ?? "";
|
||||
var cellAiLabel = ((Excel.Range)excelManager.MappingSheet.Cells[row, 3]).Value?.ToString() ?? "";
|
||||
|
||||
if (string.Equals(cellFileName.Trim(), fileName.Trim(), StringComparison.OrdinalIgnoreCase) &&
|
||||
string.Equals(cellAiLabel.Trim(), aiLabel.Trim(), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
excelManager.MappingSheet.Cells[row, 6] = pdfValue;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"? Excel <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ʈ <20><> <20><><EFBFBD><EFBFBD>: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DWG <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><>ũ<EFBFBD><C5A9><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>ϰ<EFBFBD> <20><><EFBFBD><EFBFBD> (PDF <20>÷<EFBFBD> <20><><EFBFBD><EFBFBD>)
|
||||
/// </summary>
|
||||
public void SaveDwgOnlyMappingWorkbook(Dictionary<string, Dictionary<string, (string, string, string, string)>> mappingData, string resultFolderPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
||||
string savePath = System.IO.Path.Combine(resultFolderPath, $"{timestamp}_DwgOnly_Mapping.xlsx");
|
||||
|
||||
Debug.WriteLine($"[DEBUG] DWG <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><>ũ<EFBFBD><C5A9> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>: {savePath}");
|
||||
|
||||
var dwgOnlyWorkbook = excelManager.CreateNewWorkbook();
|
||||
var dwgOnlyWorksheet = (Excel.Worksheet)dwgOnlyWorkbook.Worksheets[1];
|
||||
dwgOnlyWorksheet.Name = "DWG Mapping Data";
|
||||
|
||||
// <20><><EFBFBD> <20><><EFBFBD><EFBFBD> (PDF Value <20>÷<EFBFBD> <20><><EFBFBD><EFBFBD>)
|
||||
dwgOnlyWorksheet.Cells[1, 1] = "<22><><EFBFBD>ϸ<EFBFBD>";
|
||||
dwgOnlyWorksheet.Cells[1, 2] = "Map Key";
|
||||
dwgOnlyWorksheet.Cells[1, 3] = "AI Label";
|
||||
dwgOnlyWorksheet.Cells[1, 4] = "DWG Tag";
|
||||
dwgOnlyWorksheet.Cells[1, 5] = "DWG Value";
|
||||
|
||||
// <20><><EFBFBD> <20><>Ÿ<EFBFBD><C5B8> <20><><EFBFBD><EFBFBD>
|
||||
var headerRange = dwgOnlyWorksheet.Range["A1:E1"];
|
||||
headerRange.Font.Bold = true;
|
||||
headerRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightGray);
|
||||
headerRange.Borders.LineStyle = Excel.XlLineStyle.xlContinuous;
|
||||
|
||||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20>Է<EFBFBD>
|
||||
int totalRows = mappingData.Sum(f => f.Value.Count);
|
||||
if (totalRows > 0)
|
||||
{
|
||||
object[,] data = new object[totalRows, 5];
|
||||
int row = 0;
|
||||
|
||||
foreach (var fileEntry in mappingData)
|
||||
{
|
||||
string fileName = fileEntry.Key;
|
||||
foreach (var mapEntry in fileEntry.Value)
|
||||
{
|
||||
string mapKey = mapEntry.Key;
|
||||
var (aiLabel, dwgTag, dwgValue, pdfValue) = mapEntry.Value;
|
||||
|
||||
data[row, 0] = fileName;
|
||||
data[row, 1] = mapKey;
|
||||
data[row, 2] = aiLabel;
|
||||
data[row, 3] = dwgTag;
|
||||
data[row, 4] = dwgValue;
|
||||
|
||||
row++;
|
||||
}
|
||||
}
|
||||
|
||||
Excel.Range dataRange = dwgOnlyWorksheet.Range[
|
||||
dwgOnlyWorksheet.Cells[2, 1],
|
||||
dwgOnlyWorksheet.Cells[totalRows + 1, 5]];
|
||||
dataRange.Value = data;
|
||||
}
|
||||
|
||||
dwgOnlyWorksheet.Columns.AutoFit();
|
||||
excelManager.SaveWorkbookAs(dwgOnlyWorkbook, savePath);
|
||||
|
||||
Debug.WriteLine($"? DWG <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><>ũ<EFBFBD><C5A9> <20><><EFBFBD><EFBFBD> <20>Ϸ<EFBFBD>: {System.IO.Path.GetFileName(savePath)}");
|
||||
|
||||
dwgOnlyWorkbook.Close(false);
|
||||
System.GC.Collect();
|
||||
System.GC.WaitForPendingFinalizers();
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"? DWG <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><>ũ<EFBFBD><C5A9> <20><><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD>: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Height <20><><EFBFBD>ĵ<EFBFBD> Excel <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>
|
||||
/// </summary>
|
||||
public void WriteHeightSortedData(List<TextEntityInfo> textEntities, Excel.Worksheet worksheet, string fileName)
|
||||
{
|
||||
// <20><><EFBFBD> <20><><EFBFBD><EFBFBD>
|
||||
worksheet.Cells[1, 1] = "Height";
|
||||
worksheet.Cells[1, 2] = "Type";
|
||||
worksheet.Cells[1, 3] = "Layer";
|
||||
worksheet.Cells[1, 4] = "Tag";
|
||||
worksheet.Cells[1, 5] = "FileName";
|
||||
worksheet.Cells[1, 6] = "Text";
|
||||
|
||||
// <20><><EFBFBD> <20><>Ÿ<EFBFBD><C5B8> <20><><EFBFBD><EFBFBD>
|
||||
var headerRange = worksheet.Range["A1:F1"];
|
||||
headerRange.Font.Bold = true;
|
||||
headerRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightBlue);
|
||||
|
||||
// <20><><EFBFBD><EFBFBD><EFBFBD>ʹ<EFBFBD> ExtractTextEntitiesWithHeight<68><74><EFBFBD><EFBFBD> <20>̹<EFBFBD> <20><><EFBFBD>ĵǾ<C4B5><C7BE><EFBFBD><EFBFBD>Ƿ<EFBFBD> <20>ٽ<EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20>ʽ<EFBFBD><CABD>ϴ<EFBFBD>.
|
||||
|
||||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20>Է<EFBFBD>
|
||||
int row = 2;
|
||||
foreach (var entity in textEntities) // sortedEntities<65><73> textEntities<65><73> <20><><EFBFBD><EFBFBD>
|
||||
{
|
||||
worksheet.Cells[row, 1] = entity.Height;
|
||||
worksheet.Cells[row, 2] = entity.Type;
|
||||
worksheet.Cells[row, 3] = entity.Layer;
|
||||
worksheet.Cells[row, 4] = entity.Tag;
|
||||
worksheet.Cells[row, 5] = fileName;
|
||||
worksheet.Cells[row, 6] = entity.Text;
|
||||
row++;
|
||||
}
|
||||
|
||||
worksheet.Columns.AutoFit();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Note 엔티티들을 Excel 워크시트에 기록합니다 (기존 데이터 아래에 추가).
|
||||
/// CellBoundary 데이터를 사용하여 병합된 셀의 텍스트를 적절히 처리합니다.
|
||||
/// </summary>
|
||||
public void WriteNoteEntities(List<NoteEntityInfo> noteEntities, Excel.Worksheet worksheet, string fileName)
|
||||
{
|
||||
if (noteEntities == null || noteEntities.Count == 0)
|
||||
{
|
||||
Debug.WriteLine("[DEBUG] Note 엔티티가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 현재 워크시트의 마지막 사용된 행 찾기
|
||||
Excel.Range usedRange = null;
|
||||
int lastRow = 1;
|
||||
|
||||
try
|
||||
{
|
||||
usedRange = worksheet.UsedRange;
|
||||
lastRow = usedRange?.Rows.Count ?? 1;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[DEBUG] UsedRange 접근 오류, 기본값 사용: {ex.Message}");
|
||||
lastRow = 1;
|
||||
}
|
||||
|
||||
int startRow = lastRow + 2; // 한 줄 띄우고 시작
|
||||
Debug.WriteLine($"[DEBUG] Note 데이터 기록 시작: {startRow}행부터 {noteEntities.Count}개 항목");
|
||||
|
||||
// Note 섹션 헤더 추가 (표 컬럼 포함)
|
||||
try
|
||||
{
|
||||
worksheet.Cells[startRow - 1, 1] = "=== Notes (with Cell Boundary Tables) ===";
|
||||
worksheet.Cells[startRow - 1, 2] = "";
|
||||
worksheet.Cells[startRow - 1, 3] = "";
|
||||
worksheet.Cells[startRow - 1, 4] = "";
|
||||
worksheet.Cells[startRow - 1, 5] = "";
|
||||
worksheet.Cells[startRow - 1, 6] = "";
|
||||
|
||||
// 표 컬럼 헤더 추가 (G열부터 최대 20개 컬럼)
|
||||
for (int col = 7; col <= 26; col++) // G~Z열 (20개 컬럼)
|
||||
{
|
||||
worksheet.Cells[startRow - 1, col] = $"Table Col {col - 6}";
|
||||
var tableHeaderCell = (Excel.Range)worksheet.Cells[startRow - 1, col];
|
||||
tableHeaderCell.Font.Bold = true;
|
||||
tableHeaderCell.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightBlue);
|
||||
tableHeaderCell.Font.Size = 9; // 작은 폰트로 설정
|
||||
}
|
||||
|
||||
// 헤더 스타일 적용 (개별 셀로 처리)
|
||||
var headerCell = (Excel.Range)worksheet.Cells[startRow - 1, 1];
|
||||
headerCell.Font.Bold = true;
|
||||
headerCell.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightYellow);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[DEBUG] Note 헤더 작성 오류: {ex.Message}");
|
||||
}
|
||||
|
||||
// Note 데이터 입력 (CellBoundary 데이터 사용)
|
||||
int row = startRow;
|
||||
try
|
||||
{
|
||||
foreach (var noteEntity in noteEntities)
|
||||
{
|
||||
// 기본 Note 정보 입력 (F열까지)
|
||||
worksheet.Cells[row, 1] = 0; // Height는 0으로 설정
|
||||
worksheet.Cells[row, 2] = noteEntity.Type ?? "";
|
||||
worksheet.Cells[row, 3] = noteEntity.Layer ?? "";
|
||||
worksheet.Cells[row, 4] = ""; // Tag는 빈 값
|
||||
worksheet.Cells[row, 5] = fileName ?? "";
|
||||
worksheet.Cells[row, 6] = noteEntity.Text ?? ""; // 일반 텍스트만 (표 데이터 제외)
|
||||
|
||||
int currentRow = row; // 현재 처리 중인 행 번호
|
||||
|
||||
// CellBoundary 데이터가 있으면 G열부터 테이블 데이터 처리
|
||||
if (noteEntity.CellBoundaries != null && noteEntity.CellBoundaries.Count > 0)
|
||||
{
|
||||
Debug.WriteLine($"[DEBUG] CellBoundary 데이터 처리: Row {row}, 셀 수={noteEntity.CellBoundaries.Count}");
|
||||
|
||||
// CellBoundary의 각 셀을 해당 위치에 직접 배치
|
||||
int maxTableRow = 0;
|
||||
foreach (var cellBoundary in noteEntity.CellBoundaries)
|
||||
{
|
||||
var (sRow, sCol, eRow, eCol) = ParseCellRangeFromLabel(cellBoundary.Label);
|
||||
Debug.WriteLine($"[DEBUG] CellBoundary 처리: {cellBoundary.Label} → Range=R{sRow}C{sCol}:R{eRow}C{eCol}, Text='{cellBoundary.CellText}'");
|
||||
|
||||
if (sRow > 0 && sCol > 0 && eRow > 0 && eCol > 0)
|
||||
{
|
||||
// 병합된 영역의 셀 개수 계산: (eRow - sRow) × (eCol - sCol)
|
||||
int rowCount = eRow - sRow; // 행 개수 (bottomRight - topLeft)
|
||||
int colCount = eCol - sCol; // 열 개수 (bottomRight - topLeft)
|
||||
|
||||
Debug.WriteLine($"[DEBUG] 병합 영역 크기: {rowCount+1}행 × {colCount+1}열");
|
||||
|
||||
// 병합된 영역의 모든 셀에 텍스트 복사 (topLeft부터 bottomRight-1까지)
|
||||
for (int r = sRow; r < eRow; r++) // < eRow (bottomRight 제외)
|
||||
{
|
||||
for (int c = sCol; c < eCol; c++) // < eCol (bottomRight 제외)
|
||||
{
|
||||
// Excel에서 테이블 위치 계산:
|
||||
// R1 → Note의 현재 행 (currentRow)
|
||||
// R2 → Note의 다음 행 (currentRow + 1)
|
||||
// C1 → G열(7), C2 → H열(8)
|
||||
int excelRow = currentRow + (r - 1); // R1=currentRow, R2=currentRow+1, ...
|
||||
int excelCol = 7 + (c - 1); // C1=G열(7), C2=H열(8), ...
|
||||
|
||||
// Excel 범위 체크 (최대 20개 컬럼까지)
|
||||
if (excelCol <= 26) // Z열까지
|
||||
{
|
||||
// CellText가 비어있어도 일단 배치해보기 (디버그용)
|
||||
var cellValue = string.IsNullOrEmpty(cellBoundary.CellText) ? "[빈셀]" : cellBoundary.CellText;
|
||||
// 텍스트 형식으로 설정하여 "0:0" 같은 값이 시간으로 포맷되지 않도록 함
|
||||
var cell = (Excel.Range)worksheet.Cells[excelRow, excelCol];
|
||||
cell.NumberFormat = "@"; // 텍스트 형식
|
||||
cell.Value = cellValue;
|
||||
Debug.WriteLine($"[DEBUG] ✅ 셀 복사: R{r}C{c} → Excel[{excelRow},{excelCol}] = '{cellValue}'");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine($"[DEBUG] ❌ Excel 컬럼 범위 초과: {excelCol} > 26");
|
||||
}
|
||||
|
||||
// 테이블이 차지하는 최대 행 수 추적
|
||||
maxTableRow = Math.Max(maxTableRow, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine($"[DEBUG] ❌ 잘못된 Range: {cellBoundary.Label} → R{sRow}C{sCol}:R{eRow}C{eCol}");
|
||||
}
|
||||
}
|
||||
|
||||
// 테이블이 여러 행을 차지하는 경우 currentRow 업데이트
|
||||
if (maxTableRow > 1)
|
||||
{
|
||||
currentRow += (maxTableRow - 1);
|
||||
Debug.WriteLine($"[DEBUG] 테이블 행 수만큼 currentRow 업데이트: +{maxTableRow - 1} → {currentRow}");
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(noteEntity.TableCsv))
|
||||
{
|
||||
// 기존 TableCsv 방식 (백업용)
|
||||
var tableRows = noteEntity.TableCsv.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
Debug.WriteLine($"[DEBUG] 기존 표 데이터 처리: Row {row}, 표 행 수={tableRows.Length}");
|
||||
|
||||
for (int tableRowIndex = 0; tableRowIndex < tableRows.Length; tableRowIndex++)
|
||||
{
|
||||
var tableCells = tableRows[tableRowIndex].Split(',');
|
||||
|
||||
// 각 셀을 G열부터 배치 (최대 15개 컬럼까지)
|
||||
for (int cellIndex = 0; cellIndex < Math.Min(tableCells.Length, 15); cellIndex++)
|
||||
{
|
||||
var cellValue = tableCells[cellIndex].Trim().Trim('"'); // 따옴표 제거
|
||||
// 텍스트 형식으로 설정하여 "0:0" 같은 값이 시간으로 포맷되지 않도록 함
|
||||
var cell = (Excel.Range)worksheet.Cells[currentRow, 7 + cellIndex];
|
||||
cell.NumberFormat = "@"; // 텍스트 형식
|
||||
cell.Value = cellValue; // G열(7)부터 시작
|
||||
}
|
||||
|
||||
// 표의 첫 번째 행이 아니면 새로운 Excel 행 추가
|
||||
if (tableRowIndex > 0)
|
||||
{
|
||||
currentRow++;
|
||||
// 새로운 행에는 기본 Note 정보 복사 (Type, Layer 등)
|
||||
worksheet.Cells[currentRow, 1] = 0;
|
||||
worksheet.Cells[currentRow, 2] = noteEntity.Type ?? "";
|
||||
worksheet.Cells[currentRow, 3] = noteEntity.Layer ?? "";
|
||||
worksheet.Cells[currentRow, 4] = "";
|
||||
worksheet.Cells[currentRow, 5] = fileName ?? "";
|
||||
worksheet.Cells[currentRow, 6] = "(continued)"; // 연속 표시
|
||||
}
|
||||
|
||||
Debug.WriteLine($"[DEBUG] 표 행 {tableRowIndex + 1}/{tableRows.Length}: Excel Row {currentRow}, 셀 수={tableCells.Length}");
|
||||
}
|
||||
}
|
||||
|
||||
// "NOTE" 타입인 경우 행 배경색 변경 (표 영역 포함)
|
||||
if (noteEntity.Type == "Note")
|
||||
{
|
||||
Excel.Range noteRowRange = worksheet.Range[worksheet.Cells[row, 1], worksheet.Cells[currentRow, 26]]; // Z열까지
|
||||
noteRowRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightYellow);
|
||||
noteRowRange.Font.Bold = true;
|
||||
}
|
||||
|
||||
Debug.WriteLine($"[DEBUG] Excel 기록: Row {row}~{currentRow}, Order {noteEntity.SortOrder}, Type {noteEntity.Type}, Pos({noteEntity.X:F1},{noteEntity.Y:F1}), Text: '{noteEntity.Text}', HasCellBoundaries: {noteEntity.CellBoundaries?.Count > 0} (Count: {noteEntity.CellBoundaries?.Count ?? 0}), HasTableCsv: {!string.IsNullOrEmpty(noteEntity.TableCsv)}");
|
||||
|
||||
// CellBoundaries 상세 디버그
|
||||
if (noteEntity.CellBoundaries != null && noteEntity.CellBoundaries.Count > 0)
|
||||
{
|
||||
Debug.WriteLine($"[DEBUG] CellBoundaries 상세:");
|
||||
foreach (var cb in noteEntity.CellBoundaries.Take(5)) // 처음 5개만 출력
|
||||
{
|
||||
Debug.WriteLine($"[DEBUG] {cb.Label}: '{cb.CellText}'");
|
||||
}
|
||||
}
|
||||
|
||||
// 다음 Note는 현재 행의 다음 행부터 시작
|
||||
row = currentRow + 1;
|
||||
}
|
||||
|
||||
Debug.WriteLine($"[DEBUG] Note 데이터 기록 완료: {row - startRow}개 항목");
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[DEBUG] Note 데이터 입력 오류: {ex.Message}");
|
||||
Debug.WriteLine($"[DEBUG] 처리된 행: {row - startRow}개");
|
||||
}
|
||||
|
||||
// AutoFit 시도 (오류 발생시 무시)
|
||||
try
|
||||
{
|
||||
worksheet.Columns.AutoFit();
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[DEBUG] AutoFit 오류 (무시됨): {ex.Message}");
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"❌ WriteNoteEntities 전체 오류: {ex.Message}");
|
||||
Debug.WriteLine($" 스택 트레이스: {ex.StackTrace}");
|
||||
throw; // 상위로 예외 전파
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 라벨에서 셀 범위 정보를 파싱합니다.
|
||||
/// 예: "R1C2" → (1, 2, 1, 2) 또는 "R2C2→R3C4" → (2, 2, 3, 4)
|
||||
/// </summary>
|
||||
private (int sRow, int sCol, int eRow, int eCol) ParseCellRangeFromLabel(string label)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(label))
|
||||
return (0, 0, 0, 0);
|
||||
|
||||
if (label.Contains("→"))
|
||||
{
|
||||
// "R2C2→R3C4" 형태 - 범위 파싱
|
||||
var parts = label.Split('→');
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
var (sRow, sCol) = ParseSingleCell(parts[0]);
|
||||
var (eRow, eCol) = ParseSingleCell(parts[1]);
|
||||
return (sRow, sCol, eRow, eCol);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// "R1C2" 형태 - 단일 셀
|
||||
var (row, col) = ParseSingleCell(label);
|
||||
return (row, col, row, col);
|
||||
}
|
||||
|
||||
return (0, 0, 0, 0);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return (0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 단일 셀 위치를 파싱합니다. (예: "R2C3" → (2, 3))
|
||||
/// </summary>
|
||||
private (int row, int col) ParseSingleCell(string cellStr)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(cellStr))
|
||||
return (0, 0);
|
||||
|
||||
var rIndex = cellStr.IndexOf('R');
|
||||
var cIndex = cellStr.IndexOf('C');
|
||||
|
||||
if (rIndex >= 0 && cIndex > rIndex)
|
||||
{
|
||||
var rowStr = cellStr.Substring(rIndex + 1, cIndex - rIndex - 1);
|
||||
var colStr = cellStr.Substring(cIndex + 1);
|
||||
|
||||
if (int.TryParse(rowStr, out int row) && int.TryParse(colStr, out int col))
|
||||
{
|
||||
return (row, col);
|
||||
}
|
||||
}
|
||||
|
||||
return (0, 0);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return (0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
338
Models/ExcelManager.cs
Normal file
338
Models/ExcelManager.cs
Normal file
@@ -0,0 +1,338 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using Excel = Microsoft.Office.Interop.Excel;
|
||||
|
||||
namespace DwgExtractorManual.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Excel COM <20><>ü <20><><EFBFBD><EFBFBD> <20><> <20>⺻ <20>۾<EFBFBD><DBBE><EFBFBD> <20><><EFBFBD><EFBFBD>ϴ<EFBFBD> Ŭ<><C5AC><EFBFBD><EFBFBD>
|
||||
/// </summary>
|
||||
internal class ExcelManager : IDisposable
|
||||
{
|
||||
public Excel.Application? ExcelApplication { get; private set; }
|
||||
public Excel.Workbook? TitleBlockWorkbook { get; private set; }
|
||||
public Excel.Workbook? MappingWorkbook { get; private set; }
|
||||
|
||||
public Excel.Worksheet? TitleBlockSheet { get; private set; }
|
||||
public Excel.Worksheet? TextEntitiesSheet { get; private set; }
|
||||
public Excel.Worksheet? NoteEntitiesSheet { get; private set; }
|
||||
public Excel.Worksheet? MappingSheet { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Excel <20><><EFBFBD>ø<EFBFBD><C3B8><EFBFBD><EFBFBD>̼<EFBFBD> <20><> <20><>ũ<EFBFBD><C5A9>Ʈ <20>ʱ<EFBFBD>ȭ
|
||||
/// </summary>
|
||||
public void InitializeExcel()
|
||||
{
|
||||
try
|
||||
{
|
||||
var excelApp = new Excel.Application();
|
||||
ExcelApplication = excelApp;
|
||||
ExcelApplication.Visible = false; // WPF<50><46><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> ó<><C3B3>
|
||||
Excel.Workbook workbook = excelApp.Workbooks.Add();
|
||||
TitleBlockWorkbook = workbook;
|
||||
|
||||
// Title Block Sheet <20><><EFBFBD><EFBFBD> (<28>⺻ Sheet1)
|
||||
TitleBlockSheet = (Excel.Worksheet)workbook.Sheets[1];
|
||||
TitleBlockSheet.Name = "Title Block";
|
||||
SetupTitleBlockHeaders();
|
||||
|
||||
// Text Entities Sheet <20>߰<EFBFBD>
|
||||
TextEntitiesSheet = (Excel.Worksheet)workbook.Sheets.Add();
|
||||
TextEntitiesSheet.Name = "Text Entities";
|
||||
SetupTextEntitiesHeaders();
|
||||
|
||||
// Note Entities Sheet <20>߰<EFBFBD>
|
||||
NoteEntitiesSheet = (Excel.Worksheet)workbook.Sheets.Add();
|
||||
NoteEntitiesSheet.Name = "Note Entities";
|
||||
SetupNoteEntitiesHeaders();
|
||||
|
||||
// <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>Ϳ<EFBFBD> <20><>ũ<EFBFBD><C5A9> <20><> <20><>Ʈ <20><><EFBFBD><EFBFBD>
|
||||
MappingWorkbook = excelApp.Workbooks.Add();
|
||||
MappingSheet = (Excel.Worksheet)MappingWorkbook.Sheets[1];
|
||||
MappingSheet.Name = "Mapping Data";
|
||||
SetupMappingHeaders();
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Excel <20>ʱ<EFBFBD>ȭ <20><> <20><><EFBFBD><EFBFBD> <20><EFBFBD>: {ex.Message}");
|
||||
ReleaseExcelObjects();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <20><><EFBFBD><EFBFBD> Excel <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><>Ʈ<EFBFBD><C6AE> <20><><EFBFBD><EFBFBD>
|
||||
/// </summary>
|
||||
public bool OpenExistingFile(string excelFilePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(excelFilePath))
|
||||
{
|
||||
Debug.WriteLine($"? Excel <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20>ʽ<EFBFBD><CABD>ϴ<EFBFBD>: {excelFilePath}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ExcelApplication == null)
|
||||
{
|
||||
ExcelApplication = new Excel.Application();
|
||||
ExcelApplication.Visible = false;
|
||||
}
|
||||
|
||||
MappingWorkbook = ExcelApplication.Workbooks.Open(excelFilePath);
|
||||
MappingSheet = (Excel.Worksheet)MappingWorkbook.Sheets["Mapping Data"];
|
||||
|
||||
if (MappingSheet == null)
|
||||
{
|
||||
Debug.WriteLine("? 'Mapping Data' <20><>Ʈ<EFBFBD><C6AE> ã<><C3A3> <20><> <20><><EFBFBD><EFBFBD><EFBFBD>ϴ<EFBFBD>.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"? Excel <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD>: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <20><><EFBFBD>ο<EFBFBD> <20><>ũ<EFBFBD><C5A9> <20><><EFBFBD><EFBFBD> (Height <20><><EFBFBD>Ŀ<EFBFBD>)
|
||||
/// </summary>
|
||||
public Excel.Workbook CreateNewWorkbook()
|
||||
{
|
||||
if (ExcelApplication == null)
|
||||
{
|
||||
ExcelApplication = new Excel.Application();
|
||||
ExcelApplication.Visible = false;
|
||||
}
|
||||
return ExcelApplication.Workbooks.Add();
|
||||
}
|
||||
|
||||
// Title Block <20><>Ʈ <20><><EFBFBD> <20><><EFBFBD><EFBFBD>
|
||||
private void SetupTitleBlockHeaders()
|
||||
{
|
||||
if (TitleBlockSheet == null) return;
|
||||
|
||||
TitleBlockSheet.Cells[1, 1] = "Type"; // <20><>: AttributeReference, AttributeDefinition
|
||||
TitleBlockSheet.Cells[1, 2] = "Name"; // BlockReference <20≯<EFBFBD> <20>Ǵ<EFBFBD> BlockDefinition <20≯<EFBFBD>
|
||||
TitleBlockSheet.Cells[1, 3] = "Tag"; // Attribute Tag
|
||||
TitleBlockSheet.Cells[1, 4] = "Prompt"; // Attribute Prompt
|
||||
TitleBlockSheet.Cells[1, 5] = "Value"; // Attribute <20><> (TextString)
|
||||
TitleBlockSheet.Cells[1, 6] = "Path"; // <20><><EFBFBD><EFBFBD> DWG <20><><EFBFBD><EFBFBD> <20><>ü <20><><EFBFBD>
|
||||
TitleBlockSheet.Cells[1, 7] = "FileName"; // <20><><EFBFBD><EFBFBD> DWG <20><><EFBFBD><EFBFBD> <20≯<EFBFBD><CCB8><EFBFBD>
|
||||
|
||||
// <20><><EFBFBD> <20><> <20><>Ÿ<EFBFBD><C5B8>
|
||||
Excel.Range headerRange = TitleBlockSheet.Range["A1:G1"];
|
||||
headerRange.Font.Bold = true;
|
||||
headerRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightBlue);
|
||||
}
|
||||
|
||||
// Text Entities <20><>Ʈ <20><><EFBFBD> <20><><EFBFBD><EFBFBD>
|
||||
private void SetupTextEntitiesHeaders()
|
||||
{
|
||||
if (TextEntitiesSheet == null) return;
|
||||
|
||||
TextEntitiesSheet.Cells[1, 1] = "Type"; // DBText, MText
|
||||
TextEntitiesSheet.Cells[1, 2] = "Layer"; // Layer <20≯<EFBFBD>
|
||||
TextEntitiesSheet.Cells[1, 3] = "Text"; // <20><><EFBFBD><EFBFBD> <20>ؽ<EFBFBD>Ʈ <20><><EFBFBD><EFBFBD>
|
||||
TextEntitiesSheet.Cells[1, 4] = "Path"; // <20><><EFBFBD><EFBFBD> DWG <20><><EFBFBD><EFBFBD> <20><>ü <20><><EFBFBD>
|
||||
TextEntitiesSheet.Cells[1, 5] = "FileName"; // <20><><EFBFBD><EFBFBD> DWG <20><><EFBFBD><EFBFBD> <20≯<EFBFBD><CCB8><EFBFBD>
|
||||
|
||||
// <20><><EFBFBD> <20><> <20><>Ÿ<EFBFBD><C5B8>
|
||||
Excel.Range headerRange = TextEntitiesSheet.Range["A1:E1"];
|
||||
headerRange.Font.Bold = true;
|
||||
headerRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightGreen);
|
||||
}
|
||||
|
||||
// <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><>Ʈ <20><><EFBFBD> <20><><EFBFBD><EFBFBD>
|
||||
private void SetupMappingHeaders()
|
||||
{
|
||||
if (MappingSheet == null) return;
|
||||
|
||||
MappingSheet.Cells[1, 1] = "FileName"; // <20><><EFBFBD><EFBFBD> <20≯<EFBFBD>
|
||||
MappingSheet.Cells[1, 2] = "MapKey"; // <20><><EFBFBD><EFBFBD> Ű
|
||||
MappingSheet.Cells[1, 3] = "AILabel"; // AI <20><>
|
||||
MappingSheet.Cells[1, 4] = "DwgTag"; // DWG Tag
|
||||
MappingSheet.Cells[1, 5] = "Att_value"; // DWG <20><>
|
||||
MappingSheet.Cells[1, 6] = "Pdf_value"; // PDF <20><> (<28><><EFBFBD><EFBFBD><EFBFBD> <20><> <20><>)
|
||||
|
||||
// <20><><EFBFBD> <20><> <20><>Ÿ<EFBFBD><C5B8>
|
||||
Excel.Range headerRange = MappingSheet.Range["A1:F1"];
|
||||
headerRange.Font.Bold = true;
|
||||
headerRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightYellow);
|
||||
}
|
||||
|
||||
// Note Entities <20><>Ʈ <20><><EFBFBD> <20><><EFBFBD><EFBFBD>
|
||||
private void SetupNoteEntitiesHeaders()
|
||||
{
|
||||
if (NoteEntitiesSheet == null) return;
|
||||
|
||||
NoteEntitiesSheet.Cells[1, 1] = "Type"; // Note, NoteContent
|
||||
NoteEntitiesSheet.Cells[1, 2] = "Layer"; // Layer <20≯<EFBFBD>
|
||||
NoteEntitiesSheet.Cells[1, 3] = "Text"; // <20><><EFBFBD><EFBFBD> <20>ؽ<EFBFBD>Ʈ <20><><EFBFBD><EFBFBD>
|
||||
NoteEntitiesSheet.Cells[1, 4] = "X"; // X <20><>ǥ
|
||||
NoteEntitiesSheet.Cells[1, 5] = "Y"; // Y <20><>ǥ
|
||||
NoteEntitiesSheet.Cells[1, 6] = "SortOrder"; // <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>
|
||||
NoteEntitiesSheet.Cells[1, 7] = "TableCsv"; // <20><><EFBFBD>̺<EFBFBD> CSV <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
NoteEntitiesSheet.Cells[1, 8] = "Path"; // <20><><EFBFBD><EFBFBD> DWG <20><><EFBFBD><EFBFBD> <20><>ü <20><><EFBFBD>
|
||||
NoteEntitiesSheet.Cells[1, 9] = "FileName"; // <20><><EFBFBD><EFBFBD> DWG <20><><EFBFBD><EFBFBD> <20≯<EFBFBD><CCB8><EFBFBD>
|
||||
|
||||
// <20><><EFBFBD> <20><> <20><>Ÿ<EFBFBD><C5B8>
|
||||
Excel.Range headerRange = NoteEntitiesSheet.Range["A1:I1"];
|
||||
headerRange.Font.Bold = true;
|
||||
headerRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightCoral);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <20><>ũ<EFBFBD><C5A9> <20><><EFBFBD><EFBFBD>
|
||||
/// </summary>
|
||||
public bool SaveWorkbook(Excel.Workbook? workbook = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (workbook != null)
|
||||
{
|
||||
workbook.Save();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (MappingWorkbook != null)
|
||||
{
|
||||
MappingWorkbook.Save();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TitleBlockWorkbook != null)
|
||||
{
|
||||
TitleBlockWorkbook.Save();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"? Excel <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD>: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <20><>ũ<EFBFBD><C5A9><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><>ο<EFBFBD> <20><><EFBFBD><EFBFBD>
|
||||
/// </summary>
|
||||
public void SaveWorkbookAs(Excel.Workbook? workbook, string savePath)
|
||||
{
|
||||
if (workbook == null) return;
|
||||
|
||||
string? directory = Path.GetDirectoryName(savePath);
|
||||
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
workbook.SaveAs(savePath,
|
||||
FileFormat: Excel.XlFileFormat.xlOpenXMLWorkbook,
|
||||
AccessMode: Excel.XlSaveAsAccessMode.xlNoChange);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Excel <20><>Ʈ<EFBFBD><C6AE><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><> <20>ִ<EFBFBD> <20><>ȿ<EFBFBD><C8BF> <20≯<EFBFBD><CCB8><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>մϴ<D5B4>.
|
||||
/// </summary>
|
||||
public string GetValidSheetName(string originalName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(originalName))
|
||||
return "Sheet";
|
||||
|
||||
// Excel <20><>Ʈ<EFBFBD><C6AE><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20>ʴ<EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>
|
||||
string validName = originalName;
|
||||
char[] invalidChars = { '\\', '/', '?', '*', '[', ']', ':' };
|
||||
|
||||
foreach (char c in invalidChars)
|
||||
{
|
||||
validName = validName.Replace(c, '_');
|
||||
}
|
||||
|
||||
// 31<33>ڷ<EFBFBD> <20><><EFBFBD><EFBFBD> (Excel <20><>Ʈ<EFBFBD><C6AE> <20>ִ<EFBFBD> <20><><EFBFBD><EFBFBD>)
|
||||
if (validName.Length > 31)
|
||||
{
|
||||
validName = validName.Substring(0, 31);
|
||||
}
|
||||
|
||||
return validName;
|
||||
}
|
||||
|
||||
public void CloseWorkbooks()
|
||||
{
|
||||
if (TitleBlockWorkbook != null)
|
||||
{
|
||||
try { TitleBlockWorkbook.Close(false); }
|
||||
catch { }
|
||||
}
|
||||
if (MappingWorkbook != null)
|
||||
{
|
||||
try { MappingWorkbook.Close(false); }
|
||||
catch { }
|
||||
}
|
||||
if (ExcelApplication != null)
|
||||
{
|
||||
try { ExcelApplication.Quit(); }
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
private void ReleaseExcelObjects()
|
||||
{
|
||||
ReleaseComObject(TitleBlockSheet);
|
||||
ReleaseComObject(TextEntitiesSheet);
|
||||
ReleaseComObject(NoteEntitiesSheet);
|
||||
ReleaseComObject(MappingSheet);
|
||||
ReleaseComObject(TitleBlockWorkbook);
|
||||
ReleaseComObject(MappingWorkbook);
|
||||
ReleaseComObject(ExcelApplication);
|
||||
|
||||
TitleBlockSheet = null;
|
||||
TextEntitiesSheet = null;
|
||||
NoteEntitiesSheet = null;
|
||||
MappingSheet = null;
|
||||
TitleBlockWorkbook = null;
|
||||
MappingWorkbook = null;
|
||||
ExcelApplication = null;
|
||||
}
|
||||
|
||||
private void ReleaseComObject(object? obj)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (obj != null && Marshal.IsComObject(obj))
|
||||
{
|
||||
Marshal.ReleaseComObject(obj);
|
||||
}
|
||||
}
|
||||
catch (System.Exception)
|
||||
{
|
||||
// <20><><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD> <20><EFBFBD> <20><> <20><><EFBFBD><EFBFBD>
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
{
|
||||
Debug.WriteLine("[DEBUG] ExcelManager Dispose <20><><EFBFBD><EFBFBD>");
|
||||
CloseWorkbooks();
|
||||
ReleaseExcelObjects();
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
Debug.WriteLine("[DEBUG] ExcelManager Dispose <20>Ϸ<EFBFBD>");
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[DEBUG] ExcelManager Dispose <20><> <20><><EFBFBD><EFBFBD>: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,384 +1,441 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices; // COM 객체 해제를 위해 필요
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Teigha.DatabaseServices;
|
||||
using Teigha.Geometry;
|
||||
using Teigha.Runtime;
|
||||
using Excel = Microsoft.Office.Interop.Excel;
|
||||
|
||||
namespace DwgExtractorManual.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// DWG 파일에서 Excel로 데이터 내보내기 클래스
|
||||
/// AttributeReference, AttributeDefinition, DBText, MText 추출 지원
|
||||
/// DWG 파일에서 Excel로 데이터 내보내기 메인 클래스
|
||||
/// 리팩토링된 구조로 각 기능별 클래스를 조합하여 사용
|
||||
/// </summary>
|
||||
internal class ExportExcel : IDisposable
|
||||
{
|
||||
// ODA 서비스 객체
|
||||
private Services appServices;
|
||||
// 컴포넌트들
|
||||
private readonly ExcelManager excelManager;
|
||||
public readonly DwgDataExtractor DwgExtractor;
|
||||
private readonly JsonDataProcessor jsonProcessor;
|
||||
private readonly ExcelDataWriter excelWriter;
|
||||
private readonly FieldMapper fieldMapper;
|
||||
|
||||
// Excel COM 객체들
|
||||
private Excel.Application excelApplication;
|
||||
private Excel.Workbook workbook1;
|
||||
private Excel.Worksheet titleBlockSheet; // Title Block용 시트
|
||||
private Excel.Worksheet textEntitiesSheet; // Text Entities용 시트
|
||||
// ODA 서비스 관리
|
||||
private Services? appServices;
|
||||
|
||||
// 각 시트의 현재 행 번호
|
||||
private int titleBlockCurrentRow = 2; // 헤더가 1행이므로 데이터는 2행부터 시작
|
||||
private int textEntitiesCurrentRow = 2; // 헤더가 1행이므로 데이터는 2행부터 시작
|
||||
// 매핑 데이터 저장용
|
||||
private Dictionary<string, Dictionary<string, (string, string, string, string)>> FileToMapkeyToLabelTagValuePdf
|
||||
= new Dictionary<string, Dictionary<string, (string, string, string, string)>>();
|
||||
|
||||
// 생성자: ODA 및 Excel 초기화
|
||||
readonly List<string>? MapKeys;
|
||||
|
||||
/// <summary>
|
||||
/// 생성자: 모든 컴포넌트 초기화
|
||||
/// </summary>
|
||||
public ExportExcel()
|
||||
{
|
||||
ActivateAndInitializeODA();
|
||||
InitializeExcel();
|
||||
}
|
||||
|
||||
// ODA 제품 활성화 및 초기화
|
||||
private void ActivateAndInitializeODA()
|
||||
{
|
||||
var userInfo = "c2FtYW4gZW5naW5lZXJpbmc=";
|
||||
var userSignature = "F0kuQTmtVpHtvl/TgaFVGE92/YqGmYR9SLoXckEjnOk8NoAQh7Sg6GQruVC04JqD4C/IipxJYqpqvMfMc2auRMG+cAJCKqKUE2djIMdkUdb+C5IVx2c97fcK5ba3n8DDvB54Upokajl+6j12yD8h8MKGOR3Z3zysObeXD62bFpQgp00GCYTqlxEZtTIRjHIPAfJfix8Y0jtXWWYyVJ3LYOu86as5xtx+hY1aakpYIJiQk/6pGd84qSn/9K1w8nxR7UrFzieDeQ/xM58BHSD4u/ZxVJwvv6Uy10tsdBFBTvfJMAFp05Y7yeyeCNr100tA3iOfmWoXAVRHfxnkPfiYR54aK04QI+R6OGkI+yd1oR5BtmN6BdDt3z8KYK5EpFGJGiJIGoUy5PvkYdJ2VV6xe9JWBiIJuI/tDn1Y+uyTQFA9qaDHnOURriXsRGfy8reDPf1eejybSJxWKkpilG6RXhq3xHlCkjZzh1Q45S+xYXNGatcWMm9nkn20M8Ke5JEVaI9w/p2GE36CHRtRQbt8kfPmsbWNXJCFr4svHW2MPbCKWoyn5XEyMWBnuAKi74zvczB13DKjf29SqSIgF5k/hwy2QrgvnaKzY1k8bw8w2/k0vJXcS3GKOB/ZYDle1tf/lkAD1HtnF9zE18TiXhVnqwAVjwg4ui1RPLn/LMs6b5Y=";
|
||||
Services.odActivate(userInfo, userSignature);
|
||||
appServices = new Services();
|
||||
}
|
||||
|
||||
// Excel 애플리케이션 및 워크시트 초기화
|
||||
private void InitializeExcel()
|
||||
{
|
||||
try
|
||||
{
|
||||
var excelApp = new Excel.Application();
|
||||
excelApplication = excelApp;
|
||||
excelApplication.Visible = false; // WPF에서는 숨김 처리
|
||||
Excel.Workbook workbook = excelApp.Workbooks.Add();
|
||||
workbook1 = workbook;
|
||||
Debug.WriteLine("🔄 FieldMapper 로딩 중: mapping_table_json.json...");
|
||||
fieldMapper = FieldMapper.LoadFromFile("fletimageanalysis/mapping_table_json.json");
|
||||
Debug.WriteLine("✅ FieldMapper 로딩 성공");
|
||||
MapKeys = fieldMapper.GetAllDocAiKeys() ?? new List<string>();
|
||||
Debug.WriteLine($"📊 총 DocAI 키 개수: {MapKeys?.Count ?? 0}");
|
||||
|
||||
// Title Block Sheet 설정 (기본 Sheet1)
|
||||
titleBlockSheet = (Excel.Worksheet)workbook.Sheets[1];
|
||||
titleBlockSheet.Name = "Title Block";
|
||||
SetupTitleBlockHeaders();
|
||||
// 매핑 테스트 (디버깅용)
|
||||
TestFieldMapper();
|
||||
|
||||
// Text Entities Sheet 추가
|
||||
textEntitiesSheet = (Excel.Worksheet)workbook.Sheets.Add();
|
||||
textEntitiesSheet.Name = "Text Entities";
|
||||
SetupTextEntitiesHeaders();
|
||||
Debug.WriteLine("🔄 ODA 초기화 중...");
|
||||
InitializeTeighaServices();
|
||||
|
||||
// 컴포넌트들 초기화
|
||||
excelManager = new ExcelManager();
|
||||
DwgExtractor = new DwgDataExtractor(fieldMapper);
|
||||
jsonProcessor = new JsonDataProcessor();
|
||||
excelWriter = new ExcelDataWriter(excelManager);
|
||||
|
||||
Debug.WriteLine("🔄 Excel 초기화 중...");
|
||||
excelManager.InitializeExcel();
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Excel 초기화 중 오류 발생: {ex.Message}");
|
||||
ReleaseExcelObjects();
|
||||
Debug.WriteLine($"❌ ExportExcel 초기화 오류: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Title Block 시트 헤더 설정
|
||||
private void SetupTitleBlockHeaders()
|
||||
{
|
||||
titleBlockSheet.Cells[1, 1] = "Type"; // 예: AttributeReference, AttributeDefinition
|
||||
titleBlockSheet.Cells[1, 2] = "Name"; // BlockReference 이름 또는 BlockDefinition 이름
|
||||
titleBlockSheet.Cells[1, 3] = "Tag"; // Attribute Tag
|
||||
titleBlockSheet.Cells[1, 4] = "Prompt"; // Attribute Prompt
|
||||
titleBlockSheet.Cells[1, 5] = "Value"; // Attribute 값 (TextString)
|
||||
titleBlockSheet.Cells[1, 6] = "Path"; // 원본 DWG 파일 전체 경로
|
||||
titleBlockSheet.Cells[1, 7] = "FileName"; // 원본 DWG 파일 이름만
|
||||
|
||||
// 헤더 행 스타일
|
||||
Excel.Range headerRange = titleBlockSheet.Range["A1:G1"];
|
||||
headerRange.Font.Bold = true;
|
||||
headerRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightBlue);
|
||||
}
|
||||
|
||||
// Text Entities 시트 헤더 설정
|
||||
private void SetupTextEntitiesHeaders()
|
||||
{
|
||||
textEntitiesSheet.Cells[1, 1] = "Type"; // DBText, MText
|
||||
textEntitiesSheet.Cells[1, 2] = "Layer"; // Layer 이름
|
||||
textEntitiesSheet.Cells[1, 3] = "Text"; // 실제 텍스트 내용
|
||||
textEntitiesSheet.Cells[1, 4] = "Path"; // 원본 DWG 파일 전체 경로
|
||||
textEntitiesSheet.Cells[1, 5] = "FileName"; // 원본 DWG 파일 이름만
|
||||
|
||||
// 헤더 행 스타일
|
||||
Excel.Range headerRange = textEntitiesSheet.Range["A1:E1"];
|
||||
headerRange.Font.Bold = true;
|
||||
headerRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightGreen);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 단일 DWG 파일에서 AttributeReference/AttributeDefinition 데이터를 추출하여
|
||||
/// 초기화된 Excel 워크시트에 추가합니다.
|
||||
/// 단일 DWG 파일에서 데이터를 추출하여 Excel에 추가
|
||||
/// </summary>
|
||||
/// <param name="filePath">처리할 DWG 파일 경로</param>
|
||||
/// <param name="progress">진행 상태 보고를 위한 IProgress 객체</param>
|
||||
/// <param name="cancellationToken">작업 취소를 위한 CancellationToken</param>
|
||||
/// <returns>성공 시 true, 실패 시 false 반환</returns>
|
||||
public bool ExportDwgToExcel(string filePath, IProgress<double> progress = null, CancellationToken cancellationToken = default)
|
||||
public bool ExportDwgToExcel(string filePath, IProgress<double>? progress = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (excelApplication == null)
|
||||
Debug.WriteLine($"[DEBUG] ExportDwgToExcel 시작: {filePath}");
|
||||
|
||||
if (excelManager.ExcelApplication == null)
|
||||
{
|
||||
Console.WriteLine("Excel이 초기화되지 않았습니다.");
|
||||
Debug.WriteLine("❌ Excel이 초기화되지 않았습니다.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
Debug.WriteLine($"❌ 파일이 존재하지 않습니다: {filePath}");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
progress?.Report(0);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
// DWG 데이터 추출
|
||||
var extractionResult = DwgExtractor.ExtractFromDwgFile(filePath, progress, cancellationToken);
|
||||
|
||||
// ODA Database 객체 생성 및 DWG 파일 읽기
|
||||
using (var database = new Database(false, true))
|
||||
if (extractionResult == null)
|
||||
{
|
||||
database.ReadDwgFile(filePath, FileOpenMode.OpenForReadAndWriteNoShare, false, null);
|
||||
return false;
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
progress?.Report(10);
|
||||
// Excel에 데이터 기록
|
||||
excelWriter.WriteTitleBlockData(extractionResult.TitleBlockRows);
|
||||
excelWriter.WriteTextEntityData(extractionResult.TextEntityRows);
|
||||
|
||||
using (var tran = database.TransactionManager.StartTransaction())
|
||||
// 매핑 데이터 병합
|
||||
foreach (var fileEntry in extractionResult.FileToMapkeyToLabelTagValuePdf)
|
||||
{
|
||||
if (!FileToMapkeyToLabelTagValuePdf.ContainsKey(fileEntry.Key))
|
||||
{
|
||||
var bt = tran.GetObject(database.BlockTableId, OpenMode.ForRead) as BlockTable;
|
||||
using (var btr = tran.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForRead) as BlockTableRecord)
|
||||
{
|
||||
int totalEntities = btr.Cast<ObjectId>().Count();
|
||||
int processedCount = 0;
|
||||
FileToMapkeyToLabelTagValuePdf[fileEntry.Key] = new Dictionary<string, (string, string, string, string)>();
|
||||
}
|
||||
|
||||
foreach (ObjectId entId in btr)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
using (var ent = tran.GetObject(entId, OpenMode.ForRead) as Entity)
|
||||
{
|
||||
// Layer 이름 가져오기 (공통)
|
||||
string layerName = GetLayerName(ent.LayerId, tran, database);
|
||||
|
||||
// AttributeDefinition 추출
|
||||
if (ent is AttributeDefinition attDef)
|
||||
{
|
||||
titleBlockSheet.Cells[titleBlockCurrentRow, 1] = attDef.GetType().Name;
|
||||
titleBlockSheet.Cells[titleBlockCurrentRow, 2] = attDef.BlockName;
|
||||
titleBlockSheet.Cells[titleBlockCurrentRow, 3] = attDef.Tag;
|
||||
titleBlockSheet.Cells[titleBlockCurrentRow, 4] = attDef.Prompt;
|
||||
titleBlockSheet.Cells[titleBlockCurrentRow, 5] = attDef.TextString;
|
||||
titleBlockSheet.Cells[titleBlockCurrentRow, 6] = database.Filename;
|
||||
titleBlockSheet.Cells[titleBlockCurrentRow, 7] = Path.GetFileName(database.Filename);
|
||||
titleBlockCurrentRow++;
|
||||
}
|
||||
// BlockReference 및 그 안의 AttributeReference 추출
|
||||
else if (ent is BlockReference blr)
|
||||
{
|
||||
foreach (ObjectId attId in blr.AttributeCollection)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
using (var attRef = tran.GetObject(attId, OpenMode.ForRead) as AttributeReference)
|
||||
{
|
||||
if (attRef != null && attRef.TextString.Trim() !="")
|
||||
{
|
||||
titleBlockSheet.Cells[titleBlockCurrentRow, 1] = attRef.GetType().Name;
|
||||
titleBlockSheet.Cells[titleBlockCurrentRow, 2] = blr.Name;
|
||||
titleBlockSheet.Cells[titleBlockCurrentRow, 3] = attRef.Tag;
|
||||
titleBlockSheet.Cells[titleBlockCurrentRow, 4] = GetPromptFromAttributeReference(tran, blr, attRef.Tag);
|
||||
|
||||
titleBlockSheet.Cells[titleBlockCurrentRow, 5] = attRef.TextString;
|
||||
titleBlockSheet.Cells[titleBlockCurrentRow, 6] = database.Filename;
|
||||
titleBlockSheet.Cells[titleBlockCurrentRow, 7] = Path.GetFileName(database.Filename);
|
||||
titleBlockCurrentRow++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// DBText 엔티티 추출 (별도 시트)
|
||||
else if (ent is DBText dbText)
|
||||
{
|
||||
textEntitiesSheet.Cells[textEntitiesCurrentRow, 1] = "DBText"; // Type
|
||||
textEntitiesSheet.Cells[textEntitiesCurrentRow, 2] = layerName; // Layer
|
||||
textEntitiesSheet.Cells[textEntitiesCurrentRow, 3] = dbText.TextString; // Text
|
||||
textEntitiesSheet.Cells[textEntitiesCurrentRow, 4] = database.Filename; // Path
|
||||
textEntitiesSheet.Cells[textEntitiesCurrentRow, 5] = Path.GetFileName(database.Filename); // FileName
|
||||
textEntitiesCurrentRow++;
|
||||
}
|
||||
// MText 엔티티 추출 (별도 시트)
|
||||
else if (ent is MText mText)
|
||||
{
|
||||
textEntitiesSheet.Cells[textEntitiesCurrentRow, 1] = "MText"; // Type
|
||||
textEntitiesSheet.Cells[textEntitiesCurrentRow, 2] = layerName; // Layer
|
||||
textEntitiesSheet.Cells[textEntitiesCurrentRow, 3] = mText.Contents; // Text
|
||||
textEntitiesSheet.Cells[textEntitiesCurrentRow, 4] = database.Filename; // Path
|
||||
textEntitiesSheet.Cells[textEntitiesCurrentRow, 5] = Path.GetFileName(database.Filename); // FileName
|
||||
textEntitiesCurrentRow++;
|
||||
}
|
||||
}
|
||||
|
||||
processedCount++;
|
||||
double currentProgress = 10.0 + (double)processedCount / totalEntities * 80.0;
|
||||
progress?.Report(Math.Min(currentProgress, 90.0));
|
||||
}
|
||||
}
|
||||
|
||||
tran.Commit();
|
||||
foreach (var mapEntry in fileEntry.Value)
|
||||
{
|
||||
FileToMapkeyToLabelTagValuePdf[fileEntry.Key][mapEntry.Key] = mapEntry.Value;
|
||||
}
|
||||
}
|
||||
|
||||
progress?.Report(100);
|
||||
// 매핑 데이터를 Excel에 기록
|
||||
excelWriter.WriteMappingDataToExcel(FileToMapkeyToLabelTagValuePdf);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
progress?.Report(0);
|
||||
Debug.WriteLine($"❌ ExportDwgToExcel 오류: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 기존 Excel 파일을 열어 JSON 파일의 PDF 분석 결과로 업데이트
|
||||
/// </summary>
|
||||
public bool UpdateExistingExcelWithJson(string excelFilePath, string jsonFilePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
Debug.WriteLine($"[DEBUG] 기존 Excel 파일 업데이트 시작: {excelFilePath}");
|
||||
|
||||
if (!excelManager.OpenExistingFile(excelFilePath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Debug.WriteLine("✅ 기존 Excel 파일 열기 성공");
|
||||
return UpdateMappingSheetFromJson(jsonFilePath);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"❌ 기존 Excel 파일 업데이트 중 오류: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON 파일에서 PDF 분석 결과를 읽어 Excel 매핑 시트 업데이트
|
||||
/// </summary>
|
||||
public bool UpdateMappingSheetFromJson(string jsonFilePath)
|
||||
{
|
||||
if (!File.Exists(jsonFilePath))
|
||||
{
|
||||
Debug.WriteLine($"❌ JSON 파일이 존재하지 않습니다: {jsonFilePath}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// JSON 처리를 통해 매핑 데이터를 업데이트하고, Excel에 반영
|
||||
return jsonProcessor.UpdateMappingDataFromJson(FileToMapkeyToLabelTagValuePdf, jsonFilePath) &&
|
||||
UpdateExcelFromMappingData();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DWG 파일들을 처리하여 Height 순으로 정렬된 Excel 파일 생성
|
||||
/// </summary>
|
||||
public void ExportDwgToExcelHeightSorted(string[] dwgFiles, string resultFolder)
|
||||
{
|
||||
try
|
||||
{
|
||||
Debug.WriteLine($"[DEBUG] Height 정렬 Excel 생성 시작: {dwgFiles.Length}개 파일");
|
||||
|
||||
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
||||
string savePath = Path.Combine(resultFolder, $"{timestamp}_HeightSorted.xlsx");
|
||||
|
||||
var heightSortedWorkbook = excelManager.CreateNewWorkbook();
|
||||
bool firstSheetProcessed = false;
|
||||
|
||||
foreach (string dwgFile in dwgFiles)
|
||||
{
|
||||
if (!File.Exists(dwgFile))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string fileName = Path.GetFileNameWithoutExtension(dwgFile);
|
||||
|
||||
try
|
||||
{
|
||||
Microsoft.Office.Interop.Excel.Worksheet worksheet = firstSheetProcessed ?
|
||||
(Microsoft.Office.Interop.Excel.Worksheet)heightSortedWorkbook.Worksheets.Add() :
|
||||
(Microsoft.Office.Interop.Excel.Worksheet)heightSortedWorkbook.Worksheets[1];
|
||||
|
||||
worksheet.Name = excelManager.GetValidSheetName(fileName);
|
||||
firstSheetProcessed = true;
|
||||
|
||||
// Note 엔티티 먼저 추출
|
||||
var noteEntities = DwgExtractor.ExtractNotesFromDrawing(dwgFile);
|
||||
|
||||
// Note에서 사용된 텍스트 제외하고 일반 텍스트 추출
|
||||
var textEntities = DwgExtractor.ExtractTextEntitiesWithHeightExcluding(dwgFile, noteEntities.UsedTextIds);
|
||||
excelWriter.WriteHeightSortedData(textEntities, worksheet, fileName);
|
||||
|
||||
Debug.WriteLine($"[DEBUG] {fileName} 시트 완료: {textEntities.Count}개 엔티티");
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"❌ {fileName} 처리 중 오류: {ex.Message}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!firstSheetProcessed)
|
||||
{
|
||||
Microsoft.Office.Interop.Excel.Worksheet defaultSheet = (Microsoft.Office.Interop.Excel.Worksheet)heightSortedWorkbook.Worksheets[1];
|
||||
defaultSheet.Name = "No_DWG_Files";
|
||||
defaultSheet.Cells[1, 1] = "No DWG files found in this folder";
|
||||
}
|
||||
|
||||
excelManager.SaveWorkbookAs(heightSortedWorkbook, savePath);
|
||||
heightSortedWorkbook.Close(false);
|
||||
|
||||
Debug.WriteLine($"✅ Height 정렬 Excel 파일 저장 완료: {Path.GetFileName(savePath)}");
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"❌ Height 정렬 Excel 생성 중 오류: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 DWG 파일들을 하나의 Excel 파일로 처리하여 Height 순으로 정렬
|
||||
/// </summary>
|
||||
public void ExportAllDwgToExcelHeightSorted(List<(string filePath, string folderName)> allDwgFiles, string savePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
Debug.WriteLine($"[DEBUG] 단일 Excel 파일로 Height 정렬 생성 시작: {allDwgFiles.Count}개 파일");
|
||||
|
||||
// 시각화 데이터 초기화
|
||||
MainWindow.ClearVisualizationData();
|
||||
Debug.WriteLine("[VISUALIZATION] 시각화 데이터 초기화 완료");
|
||||
|
||||
var heightSortedWorkbook = excelManager.CreateNewWorkbook();
|
||||
bool firstSheetProcessed = false;
|
||||
|
||||
foreach (var (filePath, folderName) in allDwgFiles)
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string fileName = Path.GetFileNameWithoutExtension(filePath);
|
||||
|
||||
try
|
||||
{
|
||||
Microsoft.Office.Interop.Excel.Worksheet worksheet = firstSheetProcessed ?
|
||||
(Microsoft.Office.Interop.Excel.Worksheet)heightSortedWorkbook.Worksheets.Add() :
|
||||
(Microsoft.Office.Interop.Excel.Worksheet)heightSortedWorkbook.Worksheets[1];
|
||||
|
||||
worksheet.Name = excelManager.GetValidSheetName(fileName);
|
||||
firstSheetProcessed = true;
|
||||
|
||||
// Note 엔티티 먼저 추출
|
||||
var noteEntities = DwgExtractor.ExtractNotesFromDrawing(filePath);
|
||||
|
||||
// Note에서 사용된 텍스트 제외하고 일반 텍스트 추출
|
||||
var textEntities = DwgExtractor.ExtractTextEntitiesWithHeightExcluding(filePath, noteEntities.UsedTextIds);
|
||||
excelWriter.WriteHeightSortedData(textEntities, worksheet, fileName);
|
||||
if (noteEntities.NoteEntities.Count > 0)
|
||||
{
|
||||
excelWriter.WriteNoteEntities(noteEntities.NoteEntities, worksheet, fileName);
|
||||
Debug.WriteLine($"[DEBUG] {fileName}: {noteEntities.NoteEntities.Count}개 Note 엔티티 추가됨");
|
||||
}
|
||||
|
||||
Debug.WriteLine($"[DEBUG] {fileName} 시트 완료: {textEntities.Count}개 엔티티");
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"❌ {fileName} 처리 중 오류: {ex.Message}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!firstSheetProcessed)
|
||||
{
|
||||
Microsoft.Office.Interop.Excel.Worksheet defaultSheet = (Microsoft.Office.Interop.Excel.Worksheet)heightSortedWorkbook.Worksheets[1];
|
||||
defaultSheet.Name = "No_DWG_Files";
|
||||
defaultSheet.Cells[1, 1] = "No DWG files found in any folder";
|
||||
}
|
||||
|
||||
excelManager.SaveWorkbookAs(heightSortedWorkbook, savePath);
|
||||
heightSortedWorkbook.Close(false);
|
||||
|
||||
Debug.WriteLine($"✅ 단일 Height 정렬 Excel 파일 저장 완료: {Path.GetFileName(savePath)}");
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"❌ 단일 Height 정렬 Excel 생성 중 오류: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods and legacy support methods
|
||||
public bool SaveExcel() => excelManager.SaveWorkbook();
|
||||
|
||||
public void SaveMappingWorkbookOnly(string savePath) => excelManager.SaveWorkbookAs(excelManager.MappingWorkbook, savePath);
|
||||
|
||||
public void SaveDwgOnlyMappingWorkbook(string resultFolderPath) => excelWriter.SaveDwgOnlyMappingWorkbook(FileToMapkeyToLabelTagValuePdf, resultFolderPath);
|
||||
|
||||
public void SaveAndCloseExcel(string savePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
excelManager.SaveWorkbookAs(excelManager.TitleBlockWorkbook, savePath);
|
||||
|
||||
if (excelManager.MappingWorkbook != null)
|
||||
{
|
||||
string directory = Path.GetDirectoryName(savePath) ?? "";
|
||||
string mappingPath = Path.Combine(directory, Path.GetFileNameWithoutExtension(savePath) + "_Mapping.xlsx");
|
||||
excelManager.SaveWorkbookAs(excelManager.MappingWorkbook, mappingPath);
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Excel 파일 저장 중 오류 발생: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
excelManager.CloseWorkbooks();
|
||||
}
|
||||
}
|
||||
|
||||
public void CloseExcelObjectsWithoutSaving() => excelManager.CloseWorkbooks();
|
||||
|
||||
public void SaveMappingDictionary(string filePath) => jsonProcessor.SaveMappingDictionary(FileToMapkeyToLabelTagValuePdf, filePath);
|
||||
|
||||
public void LoadMappingDictionary(string filePath)
|
||||
{
|
||||
FileToMapkeyToLabelTagValuePdf = jsonProcessor.LoadMappingDictionary(filePath);
|
||||
}
|
||||
|
||||
public void WriteCompleteMapping() => excelWriter.WriteMappingDataToExcel(FileToMapkeyToLabelTagValuePdf);
|
||||
|
||||
public void UpdateWithPdfData(string jsonFilePath) => jsonProcessor.UpdateMappingDataFromJson(FileToMapkeyToLabelTagValuePdf, jsonFilePath);
|
||||
|
||||
public void ClearAccumulatedData() => FileToMapkeyToLabelTagValuePdf.Clear();
|
||||
|
||||
// Private helper methods
|
||||
private void TestFieldMapper()
|
||||
{
|
||||
Debug.WriteLine("[DEBUG] Testing field mapper...");
|
||||
var testFields = new[] { "TD_DNAME_MAIN", "TD_DWGNO", "TD_DWGCODE", "TB_MTITIL" };
|
||||
|
||||
foreach (var field in testFields)
|
||||
{
|
||||
var aiLabel = fieldMapper.ExpresswayToAilabel(field);
|
||||
var docAiKey = fieldMapper.AilabelToDocAiKey(aiLabel);
|
||||
Debug.WriteLine($"[DEBUG] Field: {field} -> AILabel: {aiLabel} -> DocAiKey: {docAiKey}");
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeTeighaServices()
|
||||
{
|
||||
try
|
||||
{
|
||||
Debug.WriteLine("[DEBUG] TeighaServicesManager를 통한 Services 획득 중...");
|
||||
appServices = TeighaServicesManager.Instance.AcquireServices();
|
||||
Debug.WriteLine($"[DEBUG] Services 획득 성공. Reference Count: {TeighaServicesManager.Instance.ReferenceCount}");
|
||||
}
|
||||
catch (Teigha.Runtime.Exception ex)
|
||||
{
|
||||
progress?.Report(0);
|
||||
Console.WriteLine($"DWG 파일 처리 중 오류 발생: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
progress?.Report(0);
|
||||
Console.WriteLine($"일반 오류 발생: {ex.Message}");
|
||||
return false;
|
||||
Debug.WriteLine($"[DEBUG] Teigha Services 초기화 실패: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
// Paste the helper function from above here
|
||||
public string GetPromptFromAttributeReference(Transaction tr, BlockReference blockref, string tag)
|
||||
|
||||
private bool UpdateExcelFromMappingData()
|
||||
{
|
||||
|
||||
string prompt = null;
|
||||
|
||||
|
||||
BlockTableRecord blockDef = tr.GetObject(blockref.BlockTableRecord, OpenMode.ForRead) as BlockTableRecord;
|
||||
if (blockDef == null) return null;
|
||||
|
||||
foreach (ObjectId objId in blockDef)
|
||||
try
|
||||
{
|
||||
AttributeDefinition attDef = tr.GetObject(objId, OpenMode.ForRead) as AttributeDefinition;
|
||||
if (attDef != null)
|
||||
foreach (var fileEntry in FileToMapkeyToLabelTagValuePdf)
|
||||
{
|
||||
if (attDef.Tag.Equals(tag, System.StringComparison.OrdinalIgnoreCase))
|
||||
foreach (var mapEntry in fileEntry.Value)
|
||||
{
|
||||
prompt = attDef.Prompt;
|
||||
break;
|
||||
var (aiLabel, dwgTag, dwgValue, pdfValue) = mapEntry.Value;
|
||||
if (!string.IsNullOrEmpty(pdfValue))
|
||||
{
|
||||
excelWriter.UpdateExcelRow(fileEntry.Key, aiLabel, pdfValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return prompt;
|
||||
}
|
||||
/// <summary>
|
||||
/// 현재 Excel 워크북을 지정된 경로에 저장하고 Excel 애플리케이션을 종료합니다.
|
||||
/// </summary>
|
||||
/// <param name="savePath">Excel 파일을 저장할 전체 경로</param>
|
||||
public void SaveAndCloseExcel(string savePath)
|
||||
{
|
||||
if (workbook1 == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
string directory = Path.GetDirectoryName(savePath);
|
||||
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
workbook1.SaveAs(savePath, AccessMode: Excel.XlSaveAsAccessMode.xlNoChange);
|
||||
return true;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Excel 파일 저장 중 오류 발생: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
CloseExcelObjects();
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseExcelObjects()
|
||||
{
|
||||
if (workbook1 != null)
|
||||
{
|
||||
try { workbook1.Close(false); }
|
||||
catch { }
|
||||
}
|
||||
if (excelApplication != null)
|
||||
{
|
||||
try { excelApplication.Quit(); }
|
||||
catch { }
|
||||
}
|
||||
|
||||
ReleaseExcelObjects();
|
||||
}
|
||||
|
||||
private void ReleaseExcelObjects()
|
||||
{
|
||||
ReleaseComObject(titleBlockSheet);
|
||||
ReleaseComObject(textEntitiesSheet);
|
||||
ReleaseComObject(workbook1);
|
||||
ReleaseComObject(excelApplication);
|
||||
|
||||
titleBlockSheet = null;
|
||||
textEntitiesSheet = null;
|
||||
workbook1 = null;
|
||||
excelApplication = null;
|
||||
}
|
||||
|
||||
private void ReleaseComObject(object obj)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (obj != null && Marshal.IsComObject(obj))
|
||||
{
|
||||
Marshal.ReleaseComObject(obj);
|
||||
}
|
||||
}
|
||||
catch (System.Exception)
|
||||
{
|
||||
// 해제 중 오류 발생 시 무시
|
||||
}
|
||||
finally
|
||||
{
|
||||
obj = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Layer ID로부터 Layer 이름을 가져옵니다.
|
||||
/// </summary>
|
||||
/// <param name="layerId">Layer ObjectId</param>
|
||||
/// <param name="transaction">현재 트랜잭션</param>
|
||||
/// <param name="database">데이터베이스 객체</param>
|
||||
/// <returns>Layer 이름 또는 빈 문자열</returns>
|
||||
private string GetLayerName(ObjectId layerId, Transaction transaction, Database database)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var layerTableRecord = transaction.GetObject(layerId, OpenMode.ForRead) as LayerTableRecord)
|
||||
{
|
||||
return layerTableRecord?.Name ?? "";
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Layer 이름 가져오기 오류: {ex.Message}");
|
||||
return "";
|
||||
Debug.WriteLine($"❌ Excel 업데이트 중 오류: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (excelApplication != null)
|
||||
try
|
||||
{
|
||||
CloseExcelObjects();
|
||||
}
|
||||
Debug.WriteLine("[DEBUG] ExportExcel Dispose 시작");
|
||||
|
||||
if (appServices != null)
|
||||
excelManager?.Dispose();
|
||||
|
||||
if (appServices != null)
|
||||
{
|
||||
Debug.WriteLine("[DEBUG] Teigha Services 해제 중...");
|
||||
try
|
||||
{
|
||||
TeighaServicesManager.Instance.ReleaseServices();
|
||||
Debug.WriteLine($"[DEBUG] Teigha Services 해제 완료. Remaining ref count: {TeighaServicesManager.Instance.ReferenceCount}");
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[DEBUG] Teigha Services 해제 중 오류 (무시됨): {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
appServices = null;
|
||||
}
|
||||
}
|
||||
|
||||
Debug.WriteLine("[DEBUG] ExportExcel Dispose 완료");
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
appServices.Dispose();
|
||||
appServices = null;
|
||||
Debug.WriteLine($"[DEBUG] ExportExcel Dispose 중 전역 오류: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
267
Models/IntersectionTestDebugger.cs
Normal file
267
Models/IntersectionTestDebugger.cs
Normal file
@@ -0,0 +1,267 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Teigha.Geometry;
|
||||
|
||||
namespace DwgExtractorManual.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 교차점 생성 및 셀 추출 로직을 테스트하고 디버깅하는 클래스
|
||||
/// </summary>
|
||||
public class IntersectionTestDebugger
|
||||
{
|
||||
/// <summary>
|
||||
/// 간단한 테스트 테이블을 만들어서 교차점과 셀 생성을 테스트합니다.
|
||||
/// </summary>
|
||||
public static void RunIntersectionTest()
|
||||
{
|
||||
Debug.WriteLine("=== 교차점 및 셀 생성 테스트 시작 ===");
|
||||
|
||||
try
|
||||
{
|
||||
// 테스트용 테이블 생성 (3x4 그리드)
|
||||
var testSegments = CreateTestTable();
|
||||
Debug.WriteLine($"테스트 선분 개수: {testSegments.Count}");
|
||||
|
||||
// DwgDataExtractor 인스턴스 생성 (실제 코드와 동일)
|
||||
var mappingData = new MappingTableData();
|
||||
var fieldMapper = new FieldMapper(mappingData);
|
||||
var extractor = new DwgDataExtractor(fieldMapper);
|
||||
|
||||
// 교차점 찾기 테스트
|
||||
var intersections = FindTestIntersections(testSegments, extractor);
|
||||
Debug.WriteLine($"발견된 교차점 개수: {intersections.Count}");
|
||||
|
||||
// 각 교차점의 DirectionBits 출력
|
||||
for (int i = 0; i < intersections.Count; i++)
|
||||
{
|
||||
var intersection = intersections[i];
|
||||
Debug.WriteLine($"교차점 {i}: ({intersection.Position.X:F1}, {intersection.Position.Y:F1}) - DirectionBits: {intersection.DirectionBits} - R{intersection.Row}C{intersection.Column}");
|
||||
|
||||
// topLeft/bottomRight 검증
|
||||
bool isTopLeft = extractor.IsValidTopLeft(intersection.DirectionBits);
|
||||
bool isBottomRight = extractor.IsValidBottomRight(intersection.DirectionBits);
|
||||
Debug.WriteLine($" IsTopLeft: {isTopLeft}, IsBottomRight: {isBottomRight}");
|
||||
}
|
||||
|
||||
Debug.WriteLine("=== 테스트 완료 ===");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"테스트 중 오류 발생: {ex.Message}");
|
||||
Debug.WriteLine(ex.StackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 테스트용 3x4 테이블 선분들을 생성합니다.
|
||||
/// </summary>
|
||||
private static List<(Point3d start, Point3d end, bool isHorizontal)> CreateTestTable()
|
||||
{
|
||||
var segments = new List<(Point3d start, Point3d end, bool isHorizontal)>();
|
||||
|
||||
// 수평선들 (4개 - 0, 10, 20, 30 Y좌표)
|
||||
for (int i = 0; i <= 3; i++)
|
||||
{
|
||||
double y = i * 10.0;
|
||||
segments.Add((new Point3d(0, y, 0), new Point3d(40, y, 0), true));
|
||||
}
|
||||
|
||||
// 수직선들 (5개 - 0, 10, 20, 30, 40 X좌표)
|
||||
for (int i = 0; i <= 4; i++)
|
||||
{
|
||||
double x = i * 10.0;
|
||||
segments.Add((new Point3d(x, 0, 0), new Point3d(x, 30, 0), false));
|
||||
}
|
||||
|
||||
Debug.WriteLine($"생성된 테스트 테이블: 수평선 4개, 수직선 5개");
|
||||
return segments;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 테스트 선분들로부터 교차점을 찾습니다.
|
||||
/// </summary>
|
||||
private static List<IntersectionPoint> FindTestIntersections(List<(Point3d start, Point3d end, bool isHorizontal)> segments, DwgDataExtractor extractor)
|
||||
{
|
||||
var intersections = new List<IntersectionPoint>();
|
||||
double tolerance = 0.1;
|
||||
|
||||
var horizontalSegments = segments.Where(s => s.isHorizontal).ToList();
|
||||
var verticalSegments = segments.Where(s => !s.isHorizontal).ToList();
|
||||
|
||||
foreach (var hSeg in horizontalSegments)
|
||||
{
|
||||
foreach (var vSeg in verticalSegments)
|
||||
{
|
||||
// 교차점 계산
|
||||
double intersectX = vSeg.start.X;
|
||||
double intersectY = hSeg.start.Y;
|
||||
var intersectPoint = new Point3d(intersectX, intersectY, 0);
|
||||
|
||||
// 교차점이 두 선분의 범위 내에 있는지 확인
|
||||
bool onHorizontal = intersectX >= Math.Min(hSeg.start.X, hSeg.end.X) - tolerance &&
|
||||
intersectX <= Math.Max(hSeg.start.X, hSeg.end.X) + tolerance;
|
||||
|
||||
bool onVertical = intersectY >= Math.Min(vSeg.start.Y, vSeg.end.Y) - tolerance &&
|
||||
intersectY <= Math.Max(vSeg.start.Y, vSeg.end.Y) + tolerance;
|
||||
|
||||
if (onHorizontal && onVertical)
|
||||
{
|
||||
// DirectionBits 계산
|
||||
int directionBits = CalculateDirectionBits(intersectPoint, segments, tolerance);
|
||||
|
||||
// Row, Column 계산 (1-based)
|
||||
int row = (int)Math.Round(intersectY / 10.0) + 1;
|
||||
int column = (int)Math.Round(intersectX / 10.0) + 1;
|
||||
|
||||
var intersection = new IntersectionPoint
|
||||
{
|
||||
Position = intersectPoint,
|
||||
DirectionBits = directionBits,
|
||||
Row = row,
|
||||
Column = column
|
||||
};
|
||||
|
||||
intersections.Add(intersection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return intersections;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 점에서의 DirectionBits를 계산합니다.
|
||||
/// </summary>
|
||||
private static int CalculateDirectionBits(Point3d point, List<(Point3d start, Point3d end, bool isHorizontal)> segments, double tolerance)
|
||||
{
|
||||
int bits = 0;
|
||||
// Right: 1, Up: 2, Left: 4, Down: 8
|
||||
|
||||
foreach (var segment in segments)
|
||||
{
|
||||
if (segment.isHorizontal)
|
||||
{
|
||||
// 수평선에서 점이 선분 위에 있는지 확인
|
||||
if (Math.Abs(point.Y - segment.start.Y) < tolerance &&
|
||||
point.X >= Math.Min(segment.start.X, segment.end.X) - tolerance &&
|
||||
point.X <= Math.Max(segment.start.X, segment.end.X) + tolerance)
|
||||
{
|
||||
// 오른쪽으로 선분이 있는지 확인
|
||||
if (Math.Max(segment.start.X, segment.end.X) > point.X + tolerance)
|
||||
bits |= 1; // Right
|
||||
|
||||
// 왼쪽으로 선분이 있는지 확인
|
||||
if (Math.Min(segment.start.X, segment.end.X) < point.X - tolerance)
|
||||
bits |= 4; // Left
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 수직선에서 점이 선분 위에 있는지 확인
|
||||
if (Math.Abs(point.X - segment.start.X) < tolerance &&
|
||||
point.Y >= Math.Min(segment.start.Y, segment.end.Y) - tolerance &&
|
||||
point.Y <= Math.Max(segment.start.Y, segment.end.Y) + tolerance)
|
||||
{
|
||||
// 위쪽으로 선분이 있는지 확인
|
||||
if (Math.Max(segment.start.Y, segment.end.Y) > point.Y + tolerance)
|
||||
bits |= 2; // Up
|
||||
|
||||
// 아래쪽으로 선분이 있는지 확인
|
||||
if (Math.Min(segment.start.Y, segment.end.Y) < point.Y - tolerance)
|
||||
bits |= 8; // Down
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bits;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 교차점들로부터 셀을 추출합니다.
|
||||
/// </summary>
|
||||
private static List<TableCell> ExtractTestCells(List<IntersectionPoint> intersections,
|
||||
List<(Point3d start, Point3d end, bool isHorizontal)> segments,
|
||||
DwgDataExtractor extractor)
|
||||
{
|
||||
var cells = new List<TableCell>();
|
||||
double tolerance = 0.1;
|
||||
|
||||
// topLeft 후보들을 찾아서 각각에 대해 bottomRight를 찾기
|
||||
var topLeftCandidates = intersections.Where(i => extractor.IsValidTopLeft(i.DirectionBits)).ToList();
|
||||
Debug.WriteLine($"TopLeft 후보 개수: {topLeftCandidates.Count}");
|
||||
|
||||
foreach (var topLeft in topLeftCandidates)
|
||||
{
|
||||
Debug.WriteLine($"\nTopLeft 후보 R{topLeft.Row}C{topLeft.Column} 처리 중...");
|
||||
|
||||
// bottomRight 찾기 (실제 코드와 동일한 방식)
|
||||
var bottomRight = FindBottomRightForTest(topLeft, intersections, extractor);
|
||||
|
||||
if (bottomRight != null)
|
||||
{
|
||||
Debug.WriteLine($" BottomRight 발견: R{bottomRight.Row}C{bottomRight.Column}");
|
||||
|
||||
// 셀 생성
|
||||
var cell = new TableCell
|
||||
{
|
||||
MinPoint = new Point3d(topLeft.Position.X, bottomRight.Position.Y, 0),
|
||||
MaxPoint = new Point3d(bottomRight.Position.X, topLeft.Position.Y, 0),
|
||||
Row = topLeft.Row,
|
||||
Column = topLeft.Column,
|
||||
CellText = $"R{topLeft.Row}C{topLeft.Column}"
|
||||
};
|
||||
|
||||
cells.Add(cell);
|
||||
Debug.WriteLine($" 셀 생성 완료: ({cell.MinPoint.X:F1},{cell.MinPoint.Y:F1}) to ({cell.MaxPoint.X:F1},{cell.MaxPoint.Y:F1})");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine($" BottomRight을 찾지 못함");
|
||||
}
|
||||
}
|
||||
|
||||
return cells;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 테스트용 bottomRight 찾기 메서드
|
||||
/// </summary>
|
||||
private static IntersectionPoint FindBottomRightForTest(IntersectionPoint topLeft,
|
||||
List<IntersectionPoint> intersections,
|
||||
DwgDataExtractor extractor)
|
||||
{
|
||||
// 교차점들을 Row/Column으로 딕셔너리 구성
|
||||
var intersectionLookup = intersections
|
||||
.Where(i => i.Row > 0 && i.Column > 0)
|
||||
.GroupBy(i => i.Row)
|
||||
.ToDictionary(g => g.Key, g => g.ToDictionary(i => i.Column, i => i));
|
||||
|
||||
// topLeft에서 시작하여 bottomRight 찾기
|
||||
int maxRow = intersectionLookup.Keys.Any() ? intersectionLookup.Keys.Max() : topLeft.Row;
|
||||
|
||||
for (int targetRow = topLeft.Row + 1; targetRow <= maxRow + 2; targetRow++)
|
||||
{
|
||||
if (!intersectionLookup.ContainsKey(targetRow)) continue;
|
||||
|
||||
var rowIntersections = intersectionLookup[targetRow];
|
||||
var availableColumns = rowIntersections.Keys.Where(col => col >= topLeft.Column).OrderBy(col => col);
|
||||
|
||||
foreach (int targetColumn in availableColumns)
|
||||
{
|
||||
var candidate = rowIntersections[targetColumn];
|
||||
|
||||
if (extractor.IsValidBottomRight(candidate.DirectionBits) ||
|
||||
(targetRow == maxRow && targetColumn == intersectionLookup.Values.SelectMany(row => row.Keys).Max()))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
317
Models/JsonDataProcessor.cs
Normal file
317
Models/JsonDataProcessor.cs
Normal file
@@ -0,0 +1,317 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace DwgExtractorManual.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON 파일 처리 및 매핑 데이터 관리를 담당하는 클래스
|
||||
/// </summary>
|
||||
internal class JsonDataProcessor
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON 파일에서 PDF 분석 결과를 읽어 매핑 데이터를 업데이트
|
||||
/// </summary>
|
||||
public bool UpdateMappingDataFromJson(Dictionary<string, Dictionary<string, (string, string, string, string)>> mappingData, string jsonFilePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
Debug.WriteLine($"[DEBUG] JSON 파일에서 PDF 값 업데이트 시작: {jsonFilePath}");
|
||||
|
||||
if (!File.Exists(jsonFilePath))
|
||||
{
|
||||
Debug.WriteLine($"? JSON 파일이 존재하지 않습니다: {jsonFilePath}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// JSON 파일 읽기 및 정리
|
||||
string jsonContent = File.ReadAllText(jsonFilePath, System.Text.Encoding.UTF8);
|
||||
jsonContent = CleanJsonContent(jsonContent);
|
||||
|
||||
JObject jsonData;
|
||||
try
|
||||
{
|
||||
jsonData = JObject.Parse(jsonContent);
|
||||
}
|
||||
catch (Newtonsoft.Json.JsonReaderException jsonEx)
|
||||
{
|
||||
Debug.WriteLine($"? JSON 파싱 오류: {jsonEx.Message}");
|
||||
throw new System.Exception($"PDF 분석 JSON 파일 파싱 실패: {jsonEx.Message}\n파일: {jsonFilePath}");
|
||||
}
|
||||
|
||||
var results = jsonData["results"] as JArray;
|
||||
if (results == null)
|
||||
{
|
||||
Debug.WriteLine("? JSON에서 'results' 배열을 찾을 수 없습니다.");
|
||||
return false;
|
||||
}
|
||||
|
||||
int updatedCount = 0;
|
||||
int totalEntries = 0;
|
||||
|
||||
foreach (JObject result in results)
|
||||
{
|
||||
var fileInfo = result["file_info"];
|
||||
var pdfAnalysis = result["pdf_analysis"];
|
||||
|
||||
if (fileInfo == null || pdfAnalysis == null) continue;
|
||||
|
||||
string fileName = fileInfo["name"]?.ToString();
|
||||
if (string.IsNullOrEmpty(fileName)) continue;
|
||||
|
||||
string fileNameWithoutExt = Path.GetFileNameWithoutExtension(fileName);
|
||||
|
||||
if (!mappingData.ContainsKey(fileNameWithoutExt))
|
||||
{
|
||||
Debug.WriteLine($"?? 매핑 데이터에 파일이 없습니다: {fileNameWithoutExt}");
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var property in pdfAnalysis.Cast<JProperty>())
|
||||
{
|
||||
string aiLabel = property.Name;
|
||||
var valueObj = property.Value as JObject;
|
||||
|
||||
if (valueObj == null) continue;
|
||||
|
||||
string pdfValue = valueObj["value"]?.ToString();
|
||||
if (string.IsNullOrEmpty(pdfValue)) continue;
|
||||
|
||||
totalEntries++;
|
||||
|
||||
var fileData = mappingData[fileNameWithoutExt];
|
||||
var matchingEntry = fileData.FirstOrDefault(kvp =>
|
||||
string.Equals(kvp.Value.Item1.Trim(), aiLabel.Trim(), StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!string.IsNullOrEmpty(matchingEntry.Key))
|
||||
{
|
||||
var existingValue = matchingEntry.Value;
|
||||
fileData[matchingEntry.Key] = (existingValue.Item1, existingValue.Item2, existingValue.Item3, pdfValue);
|
||||
updatedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Debug.WriteLine($"[DEBUG] PDF 데이터 업데이트 완료: {updatedCount}/{totalEntries} 업데이트됨");
|
||||
return true;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"? JSON에서 PDF 값 업데이트 중 오류: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 매핑 딕셔너리를 JSON 파일로 저장
|
||||
/// </summary>
|
||||
public void SaveMappingDictionary(Dictionary<string, Dictionary<string, (string, string, string, string)>> mappingData, string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
Debug.WriteLine($"[DEBUG] 매핑 딕셔너리 저장 시작: {filePath}");
|
||||
|
||||
var serializableData = new Dictionary<string, Dictionary<string, object>>();
|
||||
|
||||
foreach (var fileEntry in mappingData)
|
||||
{
|
||||
var fileData = new Dictionary<string, object>();
|
||||
foreach (var mapEntry in fileEntry.Value)
|
||||
{
|
||||
fileData[mapEntry.Key] = new
|
||||
{
|
||||
AILabel = mapEntry.Value.Item1,
|
||||
DwgTag = mapEntry.Value.Item2,
|
||||
DwgValue = mapEntry.Value.Item3,
|
||||
PdfValue = mapEntry.Value.Item4
|
||||
};
|
||||
}
|
||||
serializableData[fileEntry.Key] = fileData;
|
||||
}
|
||||
|
||||
string jsonContent = JsonConvert.SerializeObject(serializableData, Formatting.Indented);
|
||||
File.WriteAllText(filePath, jsonContent, System.Text.Encoding.UTF8);
|
||||
|
||||
Debug.WriteLine($"? 매핑 딕셔너리 저장 완료: {Path.GetFileName(filePath)}");
|
||||
Debug.WriteLine($"?? 저장된 파일 수: {mappingData.Count}");
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"? 매핑 딕셔너리 저장 중 오류: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON 파일에서 매핑 딕셔너리를 로드
|
||||
/// </summary>
|
||||
public Dictionary<string, Dictionary<string, (string, string, string, string)>> LoadMappingDictionary(string filePath)
|
||||
{
|
||||
var result = new Dictionary<string, Dictionary<string, (string, string, string, string)>>();
|
||||
|
||||
try
|
||||
{
|
||||
Debug.WriteLine($"[DEBUG] 매핑 딕셔너리 로드 시작: {filePath}");
|
||||
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
Debug.WriteLine($"?? 매핑 파일이 존재하지 않습니다: {filePath}");
|
||||
return result;
|
||||
}
|
||||
|
||||
string jsonContent = File.ReadAllText(filePath, System.Text.Encoding.UTF8);
|
||||
jsonContent = CleanJsonContent(jsonContent);
|
||||
|
||||
Dictionary<string, Dictionary<string, JObject>> deserializedData;
|
||||
try
|
||||
{
|
||||
deserializedData = JsonConvert.DeserializeObject<Dictionary<string, Dictionary<string, JObject>>>(jsonContent);
|
||||
}
|
||||
catch (Newtonsoft.Json.JsonReaderException jsonEx)
|
||||
{
|
||||
Debug.WriteLine($"? JSON 파싱 오류: {jsonEx.Message}");
|
||||
throw new System.Exception($"매핑 JSON 파일 파싱 실패: {jsonEx.Message}\n파일: {filePath}");
|
||||
}
|
||||
|
||||
if (deserializedData != null)
|
||||
{
|
||||
foreach (var fileEntry in deserializedData)
|
||||
{
|
||||
var fileData = new Dictionary<string, (string, string, string, string)>();
|
||||
|
||||
foreach (var mapEntry in fileEntry.Value)
|
||||
{
|
||||
var valueObj = mapEntry.Value;
|
||||
string aiLabel = valueObj["AILabel"]?.ToString() ?? "";
|
||||
string dwgTag = valueObj["DwgTag"]?.ToString() ?? "";
|
||||
string dwgValue = valueObj["DwgValue"]?.ToString() ?? "";
|
||||
string pdfValue = valueObj["PdfValue"]?.ToString() ?? "";
|
||||
|
||||
fileData[mapEntry.Key] = (aiLabel, dwgTag, dwgValue, pdfValue);
|
||||
}
|
||||
|
||||
result[fileEntry.Key] = fileData;
|
||||
}
|
||||
}
|
||||
|
||||
Debug.WriteLine($"? 매핑 딕셔너리 로드 완료");
|
||||
Debug.WriteLine($"?? 로드된 파일 수: {result.Count}");
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"? 매핑 딕셔너리 로드 중 오류: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON 내용을 정리하여 파싱 가능한 상태로 만듭니다.
|
||||
/// </summary>
|
||||
private string CleanJsonContent(string jsonContent)
|
||||
{
|
||||
if (string.IsNullOrEmpty(jsonContent))
|
||||
return jsonContent;
|
||||
|
||||
try
|
||||
{
|
||||
var lines = jsonContent.Split('\n');
|
||||
var cleanedLines = new List<string>();
|
||||
bool inMultiLineComment = false;
|
||||
|
||||
foreach (string line in lines)
|
||||
{
|
||||
string processedLine = line;
|
||||
|
||||
// 멀티라인 주석 처리
|
||||
if (inMultiLineComment)
|
||||
{
|
||||
int endIndex = processedLine.IndexOf("*/");
|
||||
if (endIndex >= 0)
|
||||
{
|
||||
processedLine = processedLine.Substring(endIndex + 2);
|
||||
inMultiLineComment = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
int multiLineStart = processedLine.IndexOf("/*");
|
||||
if (multiLineStart >= 0)
|
||||
{
|
||||
int multiLineEnd = processedLine.IndexOf("*/", multiLineStart + 2);
|
||||
if (multiLineEnd >= 0)
|
||||
{
|
||||
processedLine = processedLine.Substring(0, multiLineStart) +
|
||||
processedLine.Substring(multiLineEnd + 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
processedLine = processedLine.Substring(0, multiLineStart);
|
||||
inMultiLineComment = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 싱글라인 주석 제거
|
||||
bool inString = false;
|
||||
bool escaped = false;
|
||||
int commentIndex = -1;
|
||||
|
||||
for (int i = 0; i < processedLine.Length - 1; i++)
|
||||
{
|
||||
char current = processedLine[i];
|
||||
char next = processedLine[i + 1];
|
||||
|
||||
if (escaped)
|
||||
{
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current == '\\')
|
||||
{
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current == '"')
|
||||
{
|
||||
inString = !inString;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!inString && current == '/' && next == '/')
|
||||
{
|
||||
commentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (commentIndex >= 0)
|
||||
{
|
||||
processedLine = processedLine.Substring(0, commentIndex);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(processedLine))
|
||||
{
|
||||
cleanedLines.Add(processedLine);
|
||||
}
|
||||
}
|
||||
|
||||
return string.Join("\n", cleanedLines);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"? JSON 정리 중 오류: {ex.Message}");
|
||||
return jsonContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
177
Models/NoteExtractionTester.cs
Normal file
177
Models/NoteExtractionTester.cs
Normal file
@@ -0,0 +1,177 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DwgExtractorManual.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Note 박스 텍스트와 테이블 추출 기능을 테스트하는 클래스
|
||||
/// </summary>
|
||||
public class NoteExtractionTester
|
||||
{
|
||||
/// <summary>
|
||||
/// DWG 파일에서 Note 데이터를 추출하고 CSV로 저장하는 전체 테스트
|
||||
/// </summary>
|
||||
public static void TestNoteExtractionAndCsvExport(string dwgFilePath, string outputDirectory)
|
||||
{
|
||||
try
|
||||
{
|
||||
Debug.WriteLine("=== Note 추출 및 CSV 내보내기 테스트 시작 ===");
|
||||
|
||||
// 출력 디렉토리가 없으면 생성
|
||||
if (!Directory.Exists(outputDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
Debug.WriteLine($"출력 디렉토리 생성: {outputDirectory}");
|
||||
}
|
||||
|
||||
// 1. Teigha 서비스 초기화
|
||||
Debug.WriteLine("1. Teigha 서비스 초기화 중...");
|
||||
TeighaServicesManager.Instance.AcquireServices();
|
||||
|
||||
// 2. DwgDataExtractor 인스턴스 생성
|
||||
Debug.WriteLine("2. DwgDataExtractor 인스턴스 생성...");
|
||||
var mappingData = new MappingTableData();
|
||||
var fieldMapper = new FieldMapper(mappingData);
|
||||
var extractor = new DwgDataExtractor(fieldMapper);
|
||||
|
||||
// 3. Note 데이터 추출
|
||||
Debug.WriteLine($"3. DWG 파일에서 Note 추출 중: {Path.GetFileName(dwgFilePath)}");
|
||||
var noteEntities = extractor.ExtractNotesFromDrawing(dwgFilePath);
|
||||
|
||||
Debug.WriteLine($" 추출된 Note 엔터티 수: {noteEntities.NoteEntities.Count}");
|
||||
|
||||
// 4. Note 데이터 분석
|
||||
AnalyzeNoteData(noteEntities.NoteEntities);
|
||||
|
||||
// 5. CSV 내보내기
|
||||
Debug.WriteLine("5. CSV 파일 생성 중...");
|
||||
var csvWriter = new CsvDataWriter();
|
||||
var baseFileName = Path.GetFileNameWithoutExtension(dwgFilePath);
|
||||
|
||||
// 5-1. Note 박스 텍스트만 CSV로 저장
|
||||
var noteTextCsvPath = Path.Combine(outputDirectory, $"{baseFileName}_note_texts.csv");
|
||||
csvWriter.WriteNoteBoxTextToCsv(noteEntities.NoteEntities, noteTextCsvPath);
|
||||
Debug.WriteLine($" Note 텍스트 CSV 저장: {noteTextCsvPath}");
|
||||
|
||||
// 5-2. Note 테이블 데이터만 CSV로 저장
|
||||
var noteTableCsvPath = Path.Combine(outputDirectory, $"{baseFileName}_note_tables.csv");
|
||||
csvWriter.WriteNoteTablesToCsv(noteEntities.NoteEntities, noteTableCsvPath);
|
||||
Debug.WriteLine($" Note 테이블 CSV 저장: {noteTableCsvPath}");
|
||||
|
||||
// 5-3. 통합 CSV 저장
|
||||
var combinedCsvPath = Path.Combine(outputDirectory, $"{baseFileName}_note_combined.csv");
|
||||
csvWriter.WriteNoteDataToCombinedCsv(noteEntities.NoteEntities, combinedCsvPath);
|
||||
Debug.WriteLine($" 통합 CSV 저장: {combinedCsvPath}");
|
||||
|
||||
// 5-4. 개별 테이블 CSV 저장
|
||||
var individualTablesDir = Path.Combine(outputDirectory, $"{baseFileName}_individual_tables");
|
||||
csvWriter.WriteIndividualNoteTablesCsv(noteEntities.NoteEntities, individualTablesDir);
|
||||
Debug.WriteLine($" 개별 테이블 CSV 저장: {individualTablesDir}");
|
||||
|
||||
// 5-5. 통계 정보 CSV 저장
|
||||
var statisticsCsvPath = Path.Combine(outputDirectory, $"{baseFileName}_note_statistics.csv");
|
||||
csvWriter.WriteNoteStatisticsToCsv(noteEntities.NoteEntities, statisticsCsvPath);
|
||||
Debug.WriteLine($" 통계 정보 CSV 저장: {statisticsCsvPath}");
|
||||
|
||||
Debug.WriteLine("=== Note 추출 및 CSV 내보내기 테스트 완료 ===");
|
||||
Debug.WriteLine($"모든 결과 파일이 저장되었습니다: {outputDirectory}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"❌ 테스트 중 오류 발생: {ex.Message}");
|
||||
Debug.WriteLine($"스택 트레이스: {ex.StackTrace}");
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Teigha 서비스 정리
|
||||
try
|
||||
{
|
||||
TeighaServicesManager.Instance.ForceDisposeServices();
|
||||
Debug.WriteLine("Teigha 서비스 정리 완료");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Teigha 서비스 정리 중 오류: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 추출된 Note 데이터를 분석하여 로그로 출력
|
||||
/// </summary>
|
||||
private static void AnalyzeNoteData(List<NoteEntityInfo> noteEntities)
|
||||
{
|
||||
Debug.WriteLine("=== Note 데이터 분석 ===");
|
||||
|
||||
var noteHeaders = noteEntities.Where(ne => ne.Type == "Note").ToList();
|
||||
var noteContents = noteEntities.Where(ne => ne.Type == "NoteContent").ToList();
|
||||
var notesWithTables = noteEntities.Where(ne => ne.Type == "Note" && !string.IsNullOrEmpty(ne.TableCsv)).ToList();
|
||||
|
||||
Debug.WriteLine($"전체 Note 헤더 수: {noteHeaders.Count}");
|
||||
Debug.WriteLine($"전체 Note 콘텐츠 수: {noteContents.Count}");
|
||||
Debug.WriteLine($"테이블이 있는 Note 수: {notesWithTables.Count}");
|
||||
|
||||
// 각 Note 분석
|
||||
foreach (var note in noteHeaders)
|
||||
{
|
||||
Debug.WriteLine($"");
|
||||
Debug.WriteLine($"Note: '{note.Text}' at ({note.X:F1}, {note.Y:F1})");
|
||||
|
||||
if (!string.IsNullOrEmpty(note.TableCsv))
|
||||
{
|
||||
var tableLines = note.TableCsv.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
Debug.WriteLine($" 테이블 데이터: {tableLines.Length}행");
|
||||
|
||||
// 테이블 내용 일부 출력
|
||||
for (int i = 0; i < Math.Min(3, tableLines.Length); i++)
|
||||
{
|
||||
var line = tableLines[i];
|
||||
if (line.Length > 50) line = line.Substring(0, 50) + "...";
|
||||
Debug.WriteLine($" 행 {i + 1}: {line}");
|
||||
}
|
||||
|
||||
if (tableLines.Length > 3)
|
||||
{
|
||||
Debug.WriteLine($" ... 및 {tableLines.Length - 3}개 행 더");
|
||||
}
|
||||
}
|
||||
|
||||
// 이 Note와 연관된 NoteContent들
|
||||
var relatedContents = noteContents.Where(nc =>
|
||||
Math.Abs(nc.Y - note.Y) < 50 && // Y 좌표가 비슷한 범위 (Note 아래)
|
||||
nc.Y < note.Y) // Note보다 아래쪽
|
||||
.OrderBy(nc => nc.SortOrder)
|
||||
.ToList();
|
||||
|
||||
if (relatedContents.Count > 0)
|
||||
{
|
||||
Debug.WriteLine($" 관련 콘텐츠: {relatedContents.Count}개");
|
||||
foreach (var content in relatedContents.Take(3))
|
||||
{
|
||||
Debug.WriteLine($" '{content.Text}' at ({content.X:F1}, {content.Y:F1})");
|
||||
}
|
||||
if (relatedContents.Count > 3)
|
||||
{
|
||||
Debug.WriteLine($" ... 및 {relatedContents.Count - 3}개 더");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 레이어별 분포
|
||||
Debug.WriteLine("");
|
||||
Debug.WriteLine("레이어별 분포:");
|
||||
var layerGroups = noteEntities.GroupBy(ne => ne.Layer).OrderByDescending(g => g.Count());
|
||||
foreach (var layerGroup in layerGroups)
|
||||
{
|
||||
Debug.WriteLine($" {layerGroup.Key}: {layerGroup.Count()}개");
|
||||
}
|
||||
|
||||
Debug.WriteLine("=== Note 데이터 분석 완료 ===");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
25
Models/SettingsManager.cs
Normal file
25
Models/SettingsManager.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Newtonsoft.Json;
|
||||
using System.IO;
|
||||
|
||||
namespace DwgExtractorManual.Models
|
||||
{
|
||||
public static class SettingsManager
|
||||
{
|
||||
private static readonly string SettingsFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "settings.json");
|
||||
|
||||
public static void SaveSettings(AppSettings settings)
|
||||
{
|
||||
string json = JsonConvert.SerializeObject(settings, Formatting.Indented);
|
||||
File.WriteAllText(SettingsFilePath, json);
|
||||
}
|
||||
|
||||
public static AppSettings? LoadSettings()
|
||||
{
|
||||
if (!File.Exists(SettingsFilePath))
|
||||
{ return null; }
|
||||
|
||||
string json = File.ReadAllText(SettingsFilePath);
|
||||
return JsonConvert.DeserializeObject<AppSettings>(json);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,15 +15,22 @@ namespace DwgExtractorManual.Models
|
||||
/// </summary>
|
||||
internal sealed class SqlDatas : IDisposable
|
||||
{
|
||||
Services appServices; // ODA 제품 활성화용
|
||||
Services? appServices; // ODA 제품 활성화용 (managed by singleton)
|
||||
readonly string connectionString = "Host=localhost;Database=postgres;Username=postgres;Password=Qwer1234";
|
||||
|
||||
void ActivateAndInitializeODA()
|
||||
void InitializeTeighaServices()
|
||||
{
|
||||
var userInfo = "c2FtYW4gZW5naW5lZXJpbmc=";
|
||||
var userSignature = "F0kuQTmtVpHtvl/TgaFVGE92/YqGmYR9SLoXckEjnOk8NoAQh7Sg6GQruVC04JqD4C/IipxJYqpqvMfMc2auRMG+cAJCKqKUE2djIMdkUdb+C5IVx2c97fcK5ba3n8DDvB54Upokajl+6j12yD8h8MKGOR3Z3zysObeXD62bFpQgp00GCYTqlxEZtTIRjHIPAfJfix8Y0jtXWWYyVJ3LYOu86as5xtx+hY1aakpYIJiQk/6pGd84qSn/9K1w8nxR7UrFzieDeQ/xM58BHSD4u/ZxVJwvv6Uy10tsdBFBTvfJMAFp05Y7yeyeCNr100tA3iOfmWoXAVRHfxnkPfiYR54aK04QI+R6OGkI+yd1oR5BtmN6BdDt3z8KYK5EpFGJGiJIGoUy5PvkYdJ2VV6xe9JWBiIJuI/tDn1Y+uyTQFA9qaDHnOURriXsRGfy8reDPf1eejybSJxWKkpilG6RXhq3xHlCkjZzh1Q45S+xYXNGatcWMm9nkn20M8Ke5JEVaI9w/p2GE36CHRtRQbt8kfPmsbWNXJCFr4svHW2MPbCKWoyn5XEyMWBnuAKi74zvczB13DKjf29SqSIgF5k/hwy2QrgvnaKzY1k8bw8w2/k0vJXcS3GKOB/ZYDle1tf/lkAD1HtnF9zE18TiXhVnqwAVjwg4ui1RPLn/LMs6b5Y=";
|
||||
Services.odActivate(userInfo, userSignature);
|
||||
appServices = new Services();
|
||||
try
|
||||
{
|
||||
Debug.WriteLine("[SqlDatas] TeighaServicesManager를 통한 Services 획득 중...");
|
||||
appServices = TeighaServicesManager.Instance.AcquireServices();
|
||||
Debug.WriteLine($"[SqlDatas] Services 획득 성공. Reference Count: {TeighaServicesManager.Instance.ReferenceCount}");
|
||||
}
|
||||
catch (Teigha.Runtime.Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[SqlDatas] Teigha Services 초기화 실패: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
void CreateTables()
|
||||
@@ -78,7 +85,7 @@ namespace DwgExtractorManual.Models
|
||||
|
||||
public SqlDatas()
|
||||
{
|
||||
ActivateAndInitializeODA();
|
||||
InitializeTeighaServices();
|
||||
CreateTables();
|
||||
}
|
||||
|
||||
@@ -136,8 +143,8 @@ namespace DwgExtractorManual.Models
|
||||
cmd.Parameters.AddWithValue("Type", "DBText");
|
||||
cmd.Parameters.AddWithValue("Layer", layerName);
|
||||
cmd.Parameters.AddWithValue("Text", dbText.TextString ?? "");
|
||||
cmd.Parameters.AddWithValue("Path", database.Filename);
|
||||
cmd.Parameters.AddWithValue("FileName", Path.GetFileName(database.Filename));
|
||||
cmd.Parameters.AddWithValue("Path", database.Filename ?? "");
|
||||
cmd.Parameters.AddWithValue("FileName", string.IsNullOrEmpty(database.Filename) ? "" : Path.GetFileName(database.Filename));
|
||||
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
@@ -155,8 +162,8 @@ namespace DwgExtractorManual.Models
|
||||
cmd.Parameters.AddWithValue("Type", "MText");
|
||||
cmd.Parameters.AddWithValue("Layer", layerName);
|
||||
cmd.Parameters.AddWithValue("Text", mText.Contents ?? "");
|
||||
cmd.Parameters.AddWithValue("Path", database.Filename);
|
||||
cmd.Parameters.AddWithValue("FileName", Path.GetFileName(database.Filename));
|
||||
cmd.Parameters.AddWithValue("Path", database.Filename ?? "");
|
||||
cmd.Parameters.AddWithValue("FileName", string.IsNullOrEmpty(database.Filename) ? "" : Path.GetFileName(database.Filename));
|
||||
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
@@ -224,8 +231,8 @@ namespace DwgExtractorManual.Models
|
||||
else
|
||||
cmd.Parameters.AddWithValue("Value", tString);
|
||||
|
||||
cmd.Parameters.AddWithValue("Path", database.Filename);
|
||||
cmd.Parameters.AddWithValue("FileName", Path.GetFileName(database.Filename));
|
||||
cmd.Parameters.AddWithValue("Path", database.Filename ?? "");
|
||||
cmd.Parameters.AddWithValue("FileName", string.IsNullOrEmpty(database.Filename) ? "" : Path.GetFileName(database.Filename));
|
||||
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
@@ -301,8 +308,20 @@ namespace DwgExtractorManual.Models
|
||||
{
|
||||
if (appServices != null)
|
||||
{
|
||||
appServices.Dispose();
|
||||
appServices = null;
|
||||
try
|
||||
{
|
||||
Debug.WriteLine("[SqlDatas] Teigha Services 해제 중...");
|
||||
TeighaServicesManager.Instance.ReleaseServices();
|
||||
Debug.WriteLine($"[SqlDatas] Teigha Services 해제 완료. Remaining ref count: {TeighaServicesManager.Instance.ReferenceCount}");
|
||||
}
|
||||
catch (Teigha.Runtime.Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[SqlDatas] Teigha Services 해제 중 오류 (무시됨): {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
appServices = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
115
Models/TableCellVisualizationData.cs
Normal file
115
Models/TableCellVisualizationData.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Teigha.Geometry;
|
||||
|
||||
namespace DwgExtractorManual.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 테이블 셀 시각화를 위한 데이터 클래스
|
||||
/// </summary>
|
||||
public class TableCellVisualizationData
|
||||
{
|
||||
public string FileName { get; set; } = "";
|
||||
public string NoteText { get; set; } = "";
|
||||
public List<CellBounds> Cells { get; set; } = new List<CellBounds>();
|
||||
public List<SegmentInfo> TableSegments { get; set; } = new List<SegmentInfo>();
|
||||
public List<TextInfo> TextEntities { get; set; } = new List<TextInfo>();
|
||||
public List<IntersectionInfo> IntersectionPoints { get; set; } = new List<IntersectionInfo>(); // 교차점 정보 추가
|
||||
public List<DiagonalLine> DiagonalLines { get; set; } = new List<DiagonalLine>(); // 셀 대각선 정보 추가
|
||||
public List<CellBoundaryInfo> CellBoundaries { get; set; } = new List<CellBoundaryInfo>(); // 정확한 셀 경계 정보 추가
|
||||
public (double minX, double minY, double maxX, double maxY) NoteBounds { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 셀 경계 정보
|
||||
/// </summary>
|
||||
public class CellBounds
|
||||
{
|
||||
public double MinX { get; set; }
|
||||
public double MinY { get; set; }
|
||||
public double MaxX { get; set; }
|
||||
public double MaxY { get; set; }
|
||||
public int Row { get; set; }
|
||||
public int Column { get; set; }
|
||||
public string Text { get; set; } = "";
|
||||
public bool IsValid { get; set; } = true;
|
||||
|
||||
public double Width => MaxX - MinX;
|
||||
public double Height => MaxY - MinY;
|
||||
public double CenterX => (MinX + MaxX) / 2;
|
||||
public double CenterY => (MinY + MaxY) / 2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 선분 정보
|
||||
/// </summary>
|
||||
public class SegmentInfo
|
||||
{
|
||||
public double StartX { get; set; }
|
||||
public double StartY { get; set; }
|
||||
public double EndX { get; set; }
|
||||
public double EndY { get; set; }
|
||||
public bool IsHorizontal { get; set; }
|
||||
public string Color { get; set; } = "Black";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 텍스트 정보
|
||||
/// </summary>
|
||||
public class TextInfo
|
||||
{
|
||||
public double X { get; set; }
|
||||
public double Y { get; set; }
|
||||
public string Text { get; set; } = "";
|
||||
public bool IsInTable { get; set; }
|
||||
public string Color { get; set; } = "Blue";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 교차점 정보
|
||||
/// </summary>
|
||||
public class IntersectionInfo
|
||||
{
|
||||
public double X { get; set; }
|
||||
public double Y { get; set; }
|
||||
public int DirectionBits { get; set; } // 비트 플래그 숫자
|
||||
public int Row { get; set; } // Row 번호
|
||||
public int Column { get; set; } // Column 번호
|
||||
public bool IsTopLeft { get; set; } // topLeft 후보인지
|
||||
public bool IsBottomRight { get; set; } // bottomRight 후보인지
|
||||
public string Color { get; set; } = "Red";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 대각선 정보 (셀 디버깅용)
|
||||
/// </summary>
|
||||
public class DiagonalLine
|
||||
{
|
||||
public double StartX { get; set; }
|
||||
public double StartY { get; set; }
|
||||
public double EndX { get; set; }
|
||||
public double EndY { get; set; }
|
||||
public string Color { get; set; } = "Green";
|
||||
public string Label { get; set; } = ""; // 디버깅 라벨
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 정확한 셀 경계 정보 (4개 모서리 좌표)
|
||||
/// </summary>
|
||||
public class CellBoundaryInfo
|
||||
{
|
||||
public double TopLeftX { get; set; }
|
||||
public double TopLeftY { get; set; }
|
||||
public double TopRightX { get; set; }
|
||||
public double TopRightY { get; set; }
|
||||
public double BottomLeftX { get; set; }
|
||||
public double BottomLeftY { get; set; }
|
||||
public double BottomRightX { get; set; }
|
||||
public double BottomRightY { get; set; }
|
||||
public string Label { get; set; } = "";
|
||||
public double Width { get; set; }
|
||||
public double Height { get; set; }
|
||||
public string Color { get; set; } = "DarkBlue";
|
||||
public string CellText { get; set; } = ""; // 셀 내 텍스트 내용
|
||||
}
|
||||
}
|
||||
192
Models/TeighaServicesManager.cs
Normal file
192
Models/TeighaServicesManager.cs
Normal file
@@ -0,0 +1,192 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using Teigha.Runtime;
|
||||
|
||||
namespace DwgExtractorManual.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Singleton class to manage Teigha Services lifecycle and prevent disposal conflicts
|
||||
/// </summary>
|
||||
public sealed class TeighaServicesManager
|
||||
{
|
||||
private static readonly object _lock = new object();
|
||||
private static TeighaServicesManager? _instance = null;
|
||||
private static Services? _services = null;
|
||||
private static int _referenceCount = 0;
|
||||
private static bool _isActivated = false;
|
||||
|
||||
private TeighaServicesManager()
|
||||
{
|
||||
// Private constructor for singleton
|
||||
}
|
||||
|
||||
public static TeighaServicesManager Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_instance == null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_instance == null)
|
||||
{
|
||||
_instance = new TeighaServicesManager();
|
||||
}
|
||||
}
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Acquires Teigha Services (creates if needed, increments reference count)
|
||||
/// </summary>
|
||||
/// <returns>The Services instance</returns>
|
||||
public Services AcquireServices()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
try
|
||||
{
|
||||
Debug.WriteLine($"[TeighaManager] AcquireServices - Current ref count: {_referenceCount}");
|
||||
|
||||
if (!_isActivated)
|
||||
{
|
||||
Debug.WriteLine("[TeighaManager] Activating ODA for first time...");
|
||||
ActivateODA();
|
||||
_isActivated = true;
|
||||
}
|
||||
|
||||
if (_services == null)
|
||||
{
|
||||
Debug.WriteLine("[TeighaManager] Creating new Services instance...");
|
||||
_services = new Services();
|
||||
Debug.WriteLine("[TeighaManager] Services instance created successfully");
|
||||
}
|
||||
|
||||
_referenceCount++;
|
||||
Debug.WriteLine($"[TeighaManager] Services acquired - New ref count: {_referenceCount}");
|
||||
return _services;
|
||||
}
|
||||
catch (Teigha.Runtime.Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[TeighaManager] Error acquiring services: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases Teigha Services (decrements reference count, disposes when count reaches 0)
|
||||
/// </summary>
|
||||
public void ReleaseServices()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
try
|
||||
{
|
||||
Debug.WriteLine($"[TeighaManager] ReleaseServices - Current ref count: {_referenceCount}");
|
||||
|
||||
if (_referenceCount > 0)
|
||||
{
|
||||
_referenceCount--;
|
||||
Debug.WriteLine($"[TeighaManager] Services released - New ref count: {_referenceCount}");
|
||||
}
|
||||
|
||||
// Don't dispose Services until app shutdown to prevent conflicts
|
||||
// Just track reference count for debugging
|
||||
if (_referenceCount == 0)
|
||||
{
|
||||
Debug.WriteLine("[TeighaManager] All references released (Services kept alive for app lifetime)");
|
||||
}
|
||||
}
|
||||
catch (Teigha.Runtime.Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[TeighaManager] Error releasing services: {ex.Message}");
|
||||
// Don't throw on release to prevent cascade failures
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Force dispose Services (only call on application shutdown)
|
||||
/// </summary>
|
||||
public void ForceDisposeServices()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
try
|
||||
{
|
||||
Debug.WriteLine("[TeighaManager] Force disposing Services...");
|
||||
|
||||
if (_services != null)
|
||||
{
|
||||
_services.Dispose();
|
||||
Debug.WriteLine("[TeighaManager] Services disposed successfully");
|
||||
}
|
||||
|
||||
_services = null;
|
||||
_referenceCount = 0;
|
||||
_isActivated = false;
|
||||
|
||||
Debug.WriteLine("[TeighaManager] Force dispose completed");
|
||||
}
|
||||
catch (Teigha.Runtime.Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[TeighaManager] Error during force dispose: {ex.Message}");
|
||||
// Reset state even if disposal fails
|
||||
_services = null;
|
||||
_referenceCount = 0;
|
||||
_isActivated = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get current reference count (for debugging)
|
||||
/// </summary>
|
||||
public int ReferenceCount
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _referenceCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if Services is active and valid
|
||||
/// </summary>
|
||||
public bool IsServicesActive
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _services != null && _isActivated;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ActivateODA()
|
||||
{
|
||||
try
|
||||
{
|
||||
Debug.WriteLine("[TeighaManager] Activating ODA...");
|
||||
|
||||
var userInfo = "c2FtYW4gZW5naW5lZXJpbmc=";
|
||||
var userSignature = "F0kuQTmtVpHtvl/TgaFVGE92/YqGmYR9SLoXckEjnOk8NoAQh7Sg6GQruVC04JqD4C/IipxJYqpqvMfMc2auRMG+cAJCKqKUE2djIMdkUdb+C5IVx2c97fcK5ba3n8DDvB54Upokajl+6j12yD8h8MKGOR3Z3zysObeXD62bFpQgp00GCYTqlxEZtTIRjHIPAfJfix8Y0jtXWWYyVJ3LYOu86as5xtx+hY1aakpYIJiQk/6pGd84qSn/9K1w8nxR7UrFzieDeQ/xM58BHSD4u/ZxVJwvv6Uy10tsdBFBTvfJMAFp05Y7yeyeCNr100tA3iOfmWoXAVRHfxnkPfiYR54aK04QI+R6OGkI+yd1oR5BtmN6BdDt3z8KYK5EpFGJGiJIGoUy5PvkYdJ2VV6xe9JWBiIJuI/tDn1Y+uyTQFA9qaDHnOURriXsRGfy8reDPf1eejybSJxWKkpilG6RXhq3xHlCkjZzh1Q45S+xYXNGatcWMm9nkn20M8Ke5JEVaI9w/p2GE36CHRtRQbt8kfPmsbWNXJCFr4svHW2MPbCKWoyn5XEyMWBnuAKi74zvczB13DKjf29SqSIgF5k/hwy2QrgvnaKzY1k8bw8w2/k0vJXcS3GKOB/ZYDle1tf/lkAD1HtnF9zE18TiXhVnqwAVjwg4ui1RPLn/LMs6b5Y=";
|
||||
|
||||
Services.odActivate(userInfo, userSignature);
|
||||
Debug.WriteLine("[TeighaManager] ODA activation successful");
|
||||
}
|
||||
catch (Teigha.Runtime.Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[TeighaManager] ODA activation failed: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
48
NoteDetectionRefactor.md
Normal file
48
NoteDetectionRefactor.md
Normal file
@@ -0,0 +1,48 @@
|
||||
Project: Refactor the NOTE Content Box Detection Algorithm
|
||||
|
||||
1. High-Level Goal:
|
||||
The primary objective is to replace the current, fragile "horizontal search line" algorithm in
|
||||
Models/DwgDataExtractor.cs with a more robust and accurate method that reliably finds the content box for
|
||||
any "NOTE" text, regardless of its position or the composition of its bounding box.
|
||||
|
||||
2. Core Strategy: "Vertical Ray-Casting"
|
||||
We will implement a new algorithm that emulates how a human would visually locate the content. This
|
||||
involves a "gradual downward scan" (or vertical ray-cast) from the NOTE's position.
|
||||
|
||||
3. Implementation Plan (TODO List):
|
||||
|
||||
* Step 1: Unify All Geometry into Line Segments
|
||||
* Create a single helper method, GetAllLineSegments, that processes all Line and Polyline entities from
|
||||
the drawing.
|
||||
* This method will decompose every Polyline into its constituent Line segments.
|
||||
* It will return a single, unified List<Line> containing every potential boundary segment in the
|
||||
drawing.
|
||||
* Crucially: This method must ensure all temporary Line objects created during the process are properly
|
||||
disposed of to prevent memory leaks.
|
||||
|
||||
* Step 2: Implement the Ray-Casting Logic in `FindNoteBox`
|
||||
* The FindNoteBox method will be completely rewritten.
|
||||
* It will first call GetAllLineSegments to get the unified geometry list.
|
||||
* It will then perform the vertical ray-cast starting from the NOTE's X-coordinate and scanning
|
||||
downwards.
|
||||
* It will find all horizontal lines that intersect the ray and sort them by their Y-coordinate (from
|
||||
top to bottom).
|
||||
* It will identify the second line in this sorted list as the top edge of the content box (the first is
|
||||
assumed to be the NOTE's own bounding box).
|
||||
|
||||
* Step 3: Implement Smart Box Tracing
|
||||
* Create a new helper method, TraceBoxFromTopLine.
|
||||
* This method will take the identified top line segment as its starting point.
|
||||
* It will intelligently trace the remaining three sides of the rectangle by searching the unified list
|
||||
of line segments for the nearest connecting corners.
|
||||
* This tracing logic must be tolerant of small gaps between the endpoints of the lines forming the box.
|
||||
|
||||
* Step 4: Final Cleanup
|
||||
* Once the new ray-casting algorithm is fully implemented and validated, all of the old, obsolete
|
||||
methods related to the previous search-line approach must be deleted to keep the code clean. This
|
||||
includes:
|
||||
* FindIntersectingLineSegments
|
||||
* TraceRectangleFromLineSegments
|
||||
* FindNextConnectedLineSegment
|
||||
* DoesLineIntersectPolyline
|
||||
* GetPolylineBounds
|
||||
109
Views/TableCellVisualizationWindow.xaml
Normal file
109
Views/TableCellVisualizationWindow.xaml
Normal file
@@ -0,0 +1,109 @@
|
||||
<Window x:Class="DwgExtractorManual.Views.TableCellVisualizationWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="clr-namespace:DwgExtractorManual.Controls"
|
||||
Title="테이블 셀 시각화" Height="800" Width="1200"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
MinHeight="600" MinWidth="800">
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 헤더 -->
|
||||
<Border Grid.Row="0" Background="#34495E" Padding="15">
|
||||
<StackPanel>
|
||||
<TextBlock Text="테이블 셀 시각화"
|
||||
FontSize="20" FontWeight="Bold"
|
||||
Foreground="White" HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="추출된 테이블 셀들의 경계를 시각적으로 확인할 수 있습니다"
|
||||
FontSize="12" Foreground="LightGray"
|
||||
HorizontalAlignment="Center" Margin="0,5,0,0"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 메인 영역 -->
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="250"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 좌측 패널: 파일 목록 및 설정 -->
|
||||
<Border Grid.Column="0" Background="#ECF0F1" BorderBrush="#BDC3C7" BorderThickness="0,0,1,0">
|
||||
<StackPanel Margin="10">
|
||||
<TextBlock Text="파일 목록" FontWeight="Bold" FontSize="14" Margin="0,0,0,10"/>
|
||||
|
||||
<ListBox x:Name="lstFiles" Height="200"
|
||||
SelectionChanged="LstFiles_SelectionChanged"
|
||||
Background="White" BorderBrush="#BDC3C7">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel>
|
||||
<TextBlock Text="{Binding FileName}" FontWeight="Bold" FontSize="12"/>
|
||||
<TextBlock Text="{Binding NoteText}" FontSize="10" Foreground="Gray" TextTrimming="CharacterEllipsis"/>
|
||||
<TextBlock FontSize="10" Foreground="DarkBlue">
|
||||
<Run Text="셀: "/>
|
||||
<Run Text="{Binding Path=Cells.Count, Mode=OneWay}"/>
|
||||
<Run Text="개"/>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<Separator Margin="0,10"/>
|
||||
|
||||
<TextBlock Text="표시 옵션" FontWeight="Bold" FontSize="14" Margin="0,0,0,10"/>
|
||||
|
||||
<CheckBox x:Name="chkShowCells" Content="셀 경계 표시 (기존)" IsChecked="False"
|
||||
Checked="RefreshVisualization" Unchecked="RefreshVisualization"/>
|
||||
<CheckBox x:Name="chkShowCellBoundaries" Content="정확한 셀 경계 표시" IsChecked="True" Margin="0,5,0,0"
|
||||
Checked="RefreshVisualization" Unchecked="RefreshVisualization"/>
|
||||
<CheckBox x:Name="chkShowSegments" Content="선분 표시" IsChecked="True" Margin="0,5,0,0"
|
||||
Checked="RefreshVisualization" Unchecked="RefreshVisualization"/>
|
||||
<CheckBox x:Name="chkShowTexts" Content="텍스트 표시" IsChecked="True" Margin="0,5,0,0"
|
||||
Checked="RefreshVisualization" Unchecked="RefreshVisualization"/>
|
||||
<CheckBox x:Name="chkShowIntersections" Content="교차점 표시" IsChecked="True" Margin="0,5,0,0"
|
||||
Checked="RefreshVisualization" Unchecked="RefreshVisualization"/>
|
||||
<CheckBox x:Name="chkShowDiagonals" Content="셀 대각선 표시" IsChecked="True" Margin="0,5,0,0"
|
||||
Checked="RefreshVisualization" Unchecked="RefreshVisualization"/>
|
||||
<CheckBox x:Name="chkShowNoteBounds" Content="Note 경계 표시" IsChecked="False" Margin="0,5,0,0"
|
||||
Checked="RefreshVisualization" Unchecked="RefreshVisualization"/>
|
||||
|
||||
<Separator Margin="0,10"/>
|
||||
|
||||
<TextBlock Text="확대/축소" FontWeight="Bold" FontSize="14" Margin="0,0,0,10"/>
|
||||
<Button x:Name="btnZoomFit" Content="초기화 (마우스 우클릭)" Click="BtnZoomFit_Click" Margin="0,2"/>
|
||||
|
||||
<Separator Margin="0,10"/>
|
||||
|
||||
<TextBlock x:Name="txtInfo" Text="파일을 선택하세요" FontSize="11"
|
||||
Foreground="DarkGray" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 우측 패널: 시각화 영역 -->
|
||||
<Border Grid.Column="1" Background="White">
|
||||
<controls:ZoomBorder x:Name="svViewer" ClipToBounds="True">
|
||||
<Canvas x:Name="cnvVisualization" Background="White"
|
||||
Width="800" Height="600"
|
||||
MouseMove="CnvVisualization_MouseMove"/>
|
||||
</controls:ZoomBorder>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- 상태바 -->
|
||||
<StatusBar Grid.Row="2" Background="#95A5A6">
|
||||
<StatusBarItem>
|
||||
<TextBlock x:Name="txtStatus" Text="준비됨"/>
|
||||
</StatusBarItem>
|
||||
<StatusBarItem HorizontalAlignment="Right">
|
||||
<TextBlock x:Name="txtMousePos" Text="마우스: (0, 0)"/>
|
||||
</StatusBarItem>
|
||||
</StatusBar>
|
||||
</Grid>
|
||||
</Window>
|
||||
493
Views/TableCellVisualizationWindow.xaml.cs
Normal file
493
Views/TableCellVisualizationWindow.xaml.cs
Normal file
@@ -0,0 +1,493 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
using DwgExtractorManual.Models;
|
||||
using Brushes = System.Windows.Media.Brushes;
|
||||
using MouseEventArgs = System.Windows.Input.MouseEventArgs;
|
||||
using Point = System.Windows.Point;
|
||||
using Rectangle = System.Windows.Shapes.Rectangle;
|
||||
|
||||
namespace DwgExtractorManual.Views
|
||||
{
|
||||
public partial class TableCellVisualizationWindow : Window
|
||||
{
|
||||
private List<TableCellVisualizationData> _visualizationData;
|
||||
private TableCellVisualizationData? _currentData;
|
||||
private double _scale = 1.0;
|
||||
private const double MARGIN = 50;
|
||||
|
||||
public TableCellVisualizationWindow(List<TableCellVisualizationData> visualizationData)
|
||||
{
|
||||
InitializeComponent();
|
||||
_visualizationData = visualizationData;
|
||||
LoadFileList();
|
||||
}
|
||||
|
||||
private void LoadFileList()
|
||||
{
|
||||
lstFiles.ItemsSource = _visualizationData;
|
||||
if (_visualizationData.Count > 0)
|
||||
{
|
||||
lstFiles.SelectedIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void LstFiles_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (lstFiles.SelectedItem is TableCellVisualizationData selectedData)
|
||||
{
|
||||
_currentData = selectedData;
|
||||
RefreshVisualization();
|
||||
UpdateInfo();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateInfo()
|
||||
{
|
||||
if (_currentData == null)
|
||||
{
|
||||
txtInfo.Text = "파일을 선택하세요";
|
||||
txtStatus.Text = "준비됨";
|
||||
return;
|
||||
}
|
||||
|
||||
txtInfo.Text = $"파일: {_currentData.FileName}\n" +
|
||||
$"Note: {_currentData.NoteText}\n" +
|
||||
$"셀 수: {_currentData.Cells.Count}\n" +
|
||||
$"선분 수: {_currentData.TableSegments.Count}\n" +
|
||||
$"텍스트 수: {_currentData.TextEntities.Count}\n" +
|
||||
$"교차점 수: {_currentData.IntersectionPoints.Count}\n" +
|
||||
$"대각선 수: {_currentData.DiagonalLines.Count}";
|
||||
|
||||
txtStatus.Text = $"{_currentData.FileName} - 셀 {_currentData.Cells.Count}개";
|
||||
}
|
||||
|
||||
private void RefreshVisualization()
|
||||
{
|
||||
if (_currentData == null) return;
|
||||
|
||||
cnvVisualization.Children.Clear();
|
||||
|
||||
// 좌표계 변환 계산
|
||||
var bounds = CalculateBounds();
|
||||
if (bounds == null) return;
|
||||
|
||||
var (minX, minY, maxX, maxY) = bounds.Value;
|
||||
var dataWidth = maxX - minX;
|
||||
var dataHeight = maxY - minY;
|
||||
|
||||
// 캔버스 크기 설정 (여백 포함)
|
||||
var canvasWidth = cnvVisualization.Width;
|
||||
var canvasHeight = cnvVisualization.Height;
|
||||
|
||||
// 스케일 계산 (데이터가 캔버스에 맞도록)
|
||||
var scaleX = (canvasWidth - 2 * MARGIN) / dataWidth;
|
||||
var scaleY = (canvasHeight - 2 * MARGIN) / dataHeight;
|
||||
_scale = Math.Min(scaleX, scaleY) * 0.9; // 여유분 10%
|
||||
|
||||
// Note 경계 표시
|
||||
if (chkShowNoteBounds.IsChecked == true)
|
||||
{
|
||||
DrawNoteBounds(minX, minY, maxX, maxY);
|
||||
}
|
||||
|
||||
// 선분 표시
|
||||
if (chkShowSegments.IsChecked == true)
|
||||
{
|
||||
DrawSegments();
|
||||
}
|
||||
|
||||
// 셀 경계 표시 (기존)
|
||||
if (chkShowCells.IsChecked == true)
|
||||
{
|
||||
DrawCells();
|
||||
}
|
||||
|
||||
// 정확한 셀 경계 표시 (새로운)
|
||||
if (chkShowCellBoundaries.IsChecked == true)
|
||||
{
|
||||
DrawCellBoundaries();
|
||||
}
|
||||
|
||||
// 텍스트 표시
|
||||
if (chkShowTexts.IsChecked == true)
|
||||
{
|
||||
DrawTexts();
|
||||
}
|
||||
|
||||
// 교차점 표시
|
||||
if (chkShowIntersections.IsChecked == true)
|
||||
{
|
||||
DrawIntersections();
|
||||
}
|
||||
|
||||
// 대각선 표시 (셀 디버깅용)
|
||||
if (chkShowDiagonals.IsChecked == true)
|
||||
{
|
||||
DrawDiagonals();
|
||||
}
|
||||
}
|
||||
|
||||
private (double minX, double minY, double maxX, double maxY)? CalculateBounds()
|
||||
{
|
||||
if (_currentData == null || _currentData.Cells.Count == 0) return null;
|
||||
|
||||
var minX = _currentData.Cells.Min(c => c.MinX);
|
||||
var minY = _currentData.Cells.Min(c => c.MinY);
|
||||
var maxX = _currentData.Cells.Max(c => c.MaxX);
|
||||
var maxY = _currentData.Cells.Max(c => c.MaxY);
|
||||
|
||||
// 선분도 고려
|
||||
if (_currentData.TableSegments.Count > 0)
|
||||
{
|
||||
minX = Math.Min(minX, _currentData.TableSegments.Min(s => Math.Min(s.StartX, s.EndX)));
|
||||
minY = Math.Min(minY, _currentData.TableSegments.Min(s => Math.Min(s.StartY, s.EndY)));
|
||||
maxX = Math.Max(maxX, _currentData.TableSegments.Max(s => Math.Max(s.StartX, s.EndX)));
|
||||
maxY = Math.Max(maxY, _currentData.TableSegments.Max(s => Math.Max(s.StartY, s.EndY)));
|
||||
}
|
||||
|
||||
// 교차점도 고려
|
||||
if (_currentData.IntersectionPoints.Count > 0)
|
||||
{
|
||||
minX = Math.Min(minX, _currentData.IntersectionPoints.Min(i => i.X));
|
||||
minY = Math.Min(minY, _currentData.IntersectionPoints.Min(i => i.Y));
|
||||
maxX = Math.Max(maxX, _currentData.IntersectionPoints.Max(i => i.X));
|
||||
maxY = Math.Max(maxY, _currentData.IntersectionPoints.Max(i => i.Y));
|
||||
}
|
||||
|
||||
// 대각선도 고려
|
||||
if (_currentData.DiagonalLines.Count > 0)
|
||||
{
|
||||
minX = Math.Min(minX, _currentData.DiagonalLines.Min(d => Math.Min(d.StartX, d.EndX)));
|
||||
minY = Math.Min(minY, _currentData.DiagonalLines.Min(d => Math.Min(d.StartY, d.EndY)));
|
||||
maxX = Math.Max(maxX, _currentData.DiagonalLines.Max(d => Math.Max(d.StartX, d.EndX)));
|
||||
maxY = Math.Max(maxY, _currentData.DiagonalLines.Max(d => Math.Max(d.StartY, d.EndY)));
|
||||
}
|
||||
|
||||
// 정확한 셀 경계도 고려
|
||||
if (_currentData.CellBoundaries != null && _currentData.CellBoundaries.Count > 0)
|
||||
{
|
||||
var allCellX = _currentData.CellBoundaries.SelectMany(cb => new[] { cb.TopLeftX, cb.TopRightX, cb.BottomLeftX, cb.BottomRightX });
|
||||
var allCellY = _currentData.CellBoundaries.SelectMany(cb => new[] { cb.TopLeftY, cb.TopRightY, cb.BottomLeftY, cb.BottomRightY });
|
||||
|
||||
minX = Math.Min(minX, allCellX.Min());
|
||||
minY = Math.Min(minY, allCellY.Min());
|
||||
maxX = Math.Max(maxX, allCellX.Max());
|
||||
maxY = Math.Max(maxY, allCellY.Max());
|
||||
}
|
||||
|
||||
return (minX, minY, maxX, maxY);
|
||||
}
|
||||
|
||||
private Point TransformPoint(double x, double y)
|
||||
{
|
||||
var bounds = CalculateBounds();
|
||||
if (bounds == null) return new Point(0, 0);
|
||||
|
||||
var (minX, minY, maxX, maxY) = bounds.Value;
|
||||
|
||||
// CAD 좌표계 -> WPF 좌표계 변환 (Y축 뒤집기)
|
||||
var transformedX = (x - minX) * _scale + MARGIN;
|
||||
var transformedY = cnvVisualization.Height - ((y - minY) * _scale + MARGIN);
|
||||
|
||||
return new Point(transformedX, transformedY);
|
||||
}
|
||||
|
||||
private void DrawNoteBounds(double minX, double minY, double maxX, double maxY)
|
||||
{
|
||||
var topLeft = TransformPoint(minX, maxY);
|
||||
var bottomRight = TransformPoint(maxX, minY);
|
||||
|
||||
var rect = new Rectangle
|
||||
{
|
||||
Width = bottomRight.X - topLeft.X,
|
||||
Height = bottomRight.Y - topLeft.Y,
|
||||
Stroke = Brushes.Red,
|
||||
StrokeThickness = 2,
|
||||
StrokeDashArray = new DoubleCollection { 5, 5 },
|
||||
Fill = null
|
||||
};
|
||||
|
||||
Canvas.SetLeft(rect, topLeft.X);
|
||||
Canvas.SetTop(rect, topLeft.Y);
|
||||
cnvVisualization.Children.Add(rect);
|
||||
}
|
||||
|
||||
private void DrawSegments()
|
||||
{
|
||||
if (_currentData == null) return;
|
||||
foreach (var segment in _currentData.TableSegments)
|
||||
{
|
||||
var startPoint = TransformPoint(segment.StartX, segment.StartY);
|
||||
var endPoint = TransformPoint(segment.EndX, segment.EndY);
|
||||
|
||||
var line = new Line
|
||||
{
|
||||
X1 = startPoint.X,
|
||||
Y1 = startPoint.Y,
|
||||
X2 = endPoint.X,
|
||||
Y2 = endPoint.Y,
|
||||
Stroke = segment.IsHorizontal ? Brushes.Blue : Brushes.Green,
|
||||
StrokeThickness = 1
|
||||
};
|
||||
|
||||
cnvVisualization.Children.Add(line);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawCells()
|
||||
{
|
||||
if (_currentData == null) return;
|
||||
var colors = new[] { Brushes.Red, Brushes.Blue, Brushes.Green, Brushes.Purple, Brushes.Orange };
|
||||
|
||||
for (int i = 0; i < _currentData.Cells.Count; i++)
|
||||
{
|
||||
var cell = _currentData.Cells[i];
|
||||
var topLeft = TransformPoint(cell.MinX, cell.MaxY);
|
||||
var bottomRight = TransformPoint(cell.MaxX, cell.MinY);
|
||||
|
||||
var rect = new Rectangle
|
||||
{
|
||||
Width = bottomRight.X - topLeft.X,
|
||||
Height = bottomRight.Y - topLeft.Y,
|
||||
Stroke = colors[i % colors.Length],
|
||||
StrokeThickness = 2,
|
||||
Fill = null
|
||||
};
|
||||
|
||||
Canvas.SetLeft(rect, topLeft.X);
|
||||
Canvas.SetTop(rect, topLeft.Y);
|
||||
cnvVisualization.Children.Add(rect);
|
||||
|
||||
// 셀 번호 표시
|
||||
var label = new TextBlock
|
||||
{
|
||||
Text = $"R{cell.Row}C{cell.Column}", // 이미 1-based 인덱싱 적용됨
|
||||
FontSize = 8, // 폰트 크기 조정
|
||||
Foreground = colors[i % colors.Length],
|
||||
FontWeight = FontWeights.Bold
|
||||
};
|
||||
|
||||
Canvas.SetLeft(label, topLeft.X + 2); // 좌상단에 위치 + 약간의 패딩
|
||||
Canvas.SetTop(label, topLeft.Y + 2); // 좌상단에 위치 + 약간의 패딩
|
||||
cnvVisualization.Children.Add(label);
|
||||
|
||||
// 셀 텍스트 표시
|
||||
if (!string.IsNullOrEmpty(cell.Text))
|
||||
{
|
||||
var textLabel = new TextBlock
|
||||
{
|
||||
Text = cell.Text,
|
||||
FontSize = 8,
|
||||
Foreground = Brushes.Black,
|
||||
Background = Brushes.LightYellow
|
||||
};
|
||||
|
||||
// 셀 텍스트는 셀 중앙에 표시
|
||||
var centerPoint = TransformPoint(cell.CenterX, cell.CenterY);
|
||||
Canvas.SetLeft(textLabel, centerPoint.X - (textLabel.ActualWidth / 2)); // 텍스트 중앙 정렬
|
||||
Canvas.SetTop(textLabel, centerPoint.Y - (textLabel.ActualHeight / 2)); // 텍스트 중앙 정렬
|
||||
cnvVisualization.Children.Add(textLabel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawCellBoundaries()
|
||||
{
|
||||
if (_currentData == null || _currentData.CellBoundaries == null) return;
|
||||
|
||||
for (int i = 0; i < _currentData.CellBoundaries.Count; i++)
|
||||
{
|
||||
var cellBoundary = _currentData.CellBoundaries[i];
|
||||
|
||||
// 4개 모서리 좌표 변환
|
||||
var topLeft = TransformPoint(cellBoundary.TopLeftX, cellBoundary.TopLeftY);
|
||||
var topRight = TransformPoint(cellBoundary.TopRightX, cellBoundary.TopRightY);
|
||||
var bottomLeft = TransformPoint(cellBoundary.BottomLeftX, cellBoundary.BottomLeftY);
|
||||
var bottomRight = TransformPoint(cellBoundary.BottomRightX, cellBoundary.BottomRightY);
|
||||
|
||||
// 셀 경계를 Path로 그리기 (정확한 4개 모서리 연결)
|
||||
var pathGeometry = new PathGeometry();
|
||||
var pathFigure = new PathFigure();
|
||||
pathFigure.StartPoint = topLeft;
|
||||
pathFigure.Segments.Add(new LineSegment(topRight, true));
|
||||
pathFigure.Segments.Add(new LineSegment(bottomRight, true));
|
||||
pathFigure.Segments.Add(new LineSegment(bottomLeft, true));
|
||||
pathFigure.IsClosed = true;
|
||||
pathGeometry.Figures.Add(pathFigure);
|
||||
|
||||
var path = new Path
|
||||
{
|
||||
Data = pathGeometry,
|
||||
Stroke = Brushes.DarkBlue,
|
||||
StrokeThickness = 3,
|
||||
Fill = null
|
||||
};
|
||||
|
||||
cnvVisualization.Children.Add(path);
|
||||
|
||||
// 라벨 표시 (셀 중앙)
|
||||
if (!string.IsNullOrEmpty(cellBoundary.Label))
|
||||
{
|
||||
var centerX = (topLeft.X + bottomRight.X) / 2;
|
||||
var centerY = (topLeft.Y + bottomRight.Y) / 2;
|
||||
|
||||
var label = new TextBlock
|
||||
{
|
||||
Text = cellBoundary.Label,
|
||||
FontSize = 10,
|
||||
Foreground = Brushes.DarkBlue,
|
||||
FontWeight = FontWeights.Bold,
|
||||
Background = Brushes.LightCyan
|
||||
};
|
||||
|
||||
Canvas.SetLeft(label, centerX - 15); // 대략적인 중앙 정렬
|
||||
Canvas.SetTop(label, centerY - 6);
|
||||
cnvVisualization.Children.Add(label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawTexts()
|
||||
{
|
||||
if (_currentData == null) return;
|
||||
foreach (var text in _currentData.TextEntities)
|
||||
{
|
||||
var point = TransformPoint(text.X, text.Y);
|
||||
|
||||
var textBlock = new TextBlock
|
||||
{
|
||||
Text = text.Text,
|
||||
FontSize = 9,
|
||||
Foreground = text.IsInTable ? Brushes.DarkBlue : Brushes.Gray
|
||||
};
|
||||
|
||||
Canvas.SetLeft(textBlock, point.X);
|
||||
Canvas.SetTop(textBlock, point.Y);
|
||||
cnvVisualization.Children.Add(textBlock);
|
||||
|
||||
// 텍스트 위치 점 표시
|
||||
var dot = new Ellipse
|
||||
{
|
||||
Width = 3,
|
||||
Height = 3,
|
||||
Fill = text.IsInTable ? Brushes.Blue : Brushes.Gray
|
||||
};
|
||||
|
||||
Canvas.SetLeft(dot, point.X - 1.5);
|
||||
Canvas.SetTop(dot, point.Y - 1.5);
|
||||
cnvVisualization.Children.Add(dot);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawIntersections()
|
||||
{
|
||||
if (_currentData == null) return;
|
||||
foreach (var intersection in _currentData.IntersectionPoints)
|
||||
{
|
||||
var point = TransformPoint(intersection.X, intersection.Y);
|
||||
|
||||
// 교차점 원 표시
|
||||
var circle = new Ellipse
|
||||
{
|
||||
Width = 8,
|
||||
Height = 8,
|
||||
Fill = GetIntersectionColor(intersection),
|
||||
Stroke = Brushes.Black,
|
||||
StrokeThickness = 1
|
||||
};
|
||||
|
||||
Canvas.SetLeft(circle, point.X - 4);
|
||||
Canvas.SetTop(circle, point.Y - 4);
|
||||
cnvVisualization.Children.Add(circle);
|
||||
|
||||
// 교차점 타입 숫자 표시 (우측에)
|
||||
var numberLabel = new TextBlock
|
||||
{
|
||||
Text = intersection.DirectionBits.ToString(),
|
||||
FontSize = 12,
|
||||
FontWeight = FontWeights.Bold,
|
||||
Foreground = Brushes.Black,
|
||||
Background = Brushes.Yellow,
|
||||
Padding = new Thickness(2)
|
||||
};
|
||||
|
||||
Canvas.SetLeft(numberLabel, point.X + 8); // 교차점 우측에 표시
|
||||
Canvas.SetTop(numberLabel, point.Y - 6); // 약간 위쪽으로 조정
|
||||
cnvVisualization.Children.Add(numberLabel);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawDiagonals()
|
||||
{
|
||||
if (_currentData == null) return;
|
||||
foreach (var diagonal in _currentData.DiagonalLines)
|
||||
{
|
||||
var startPoint = TransformPoint(diagonal.StartX, diagonal.StartY);
|
||||
var endPoint = TransformPoint(diagonal.EndX, diagonal.EndY);
|
||||
|
||||
// 대각선 표시
|
||||
var line = new Line
|
||||
{
|
||||
X1 = startPoint.X,
|
||||
Y1 = startPoint.Y,
|
||||
X2 = endPoint.X,
|
||||
Y2 = endPoint.Y,
|
||||
Stroke = Brushes.Green,
|
||||
StrokeThickness = 2,
|
||||
StrokeDashArray = new DoubleCollection { 5, 3 } // 점선으로 표시
|
||||
};
|
||||
cnvVisualization.Children.Add(line);
|
||||
|
||||
// 대각선 중앙에 라벨 표시
|
||||
if (!string.IsNullOrEmpty(diagonal.Label))
|
||||
{
|
||||
var centerX = (startPoint.X + endPoint.X) / 2;
|
||||
var centerY = (startPoint.Y + endPoint.Y) / 2;
|
||||
|
||||
var label = new TextBlock
|
||||
{
|
||||
Text = diagonal.Label,
|
||||
FontSize = 9,
|
||||
FontWeight = FontWeights.Bold,
|
||||
Foreground = Brushes.Green,
|
||||
Background = Brushes.White
|
||||
};
|
||||
Canvas.SetLeft(label, centerX - 20); // 중앙에서 약간 왼쪽으로
|
||||
Canvas.SetTop(label, centerY - 10); // 중앙에서 약간 위쪽으로
|
||||
cnvVisualization.Children.Add(label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private System.Windows.Media.Brush GetIntersectionColor(IntersectionInfo intersection)
|
||||
{
|
||||
if (intersection.IsTopLeft && intersection.IsBottomRight)
|
||||
return Brushes.Purple; // 둘 다 가능
|
||||
else if (intersection.IsTopLeft)
|
||||
return Brushes.Green; // topLeft 후보
|
||||
else if (intersection.IsBottomRight)
|
||||
return Brushes.Blue; // bottomRight 후보
|
||||
else
|
||||
return Brushes.Red; // 기타
|
||||
}
|
||||
|
||||
private void RefreshVisualization(object sender, RoutedEventArgs e)
|
||||
{
|
||||
RefreshVisualization();
|
||||
}
|
||||
|
||||
private void BtnZoomFit_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
svViewer.Reset();
|
||||
}
|
||||
|
||||
private void CnvVisualization_MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
var pos = e.GetPosition(cnvVisualization);
|
||||
txtMousePos.Text = $"마우스: ({pos.X:F0}, {pos.Y:F0})";
|
||||
}
|
||||
}
|
||||
}
|
||||
67
cleanup_and_setup.bat
Normal file
67
cleanup_and_setup.bat
Normal file
@@ -0,0 +1,67 @@
|
||||
@echo off
|
||||
echo Cleaning up fletimageanalysis folder for CLI-only processing...
|
||||
|
||||
cd fletimageanalysis
|
||||
|
||||
REM Delete unnecessary test files
|
||||
del /q test_*.py 2>nul
|
||||
del /q python_mapping_usage.py 2>nul
|
||||
|
||||
REM Delete backup and alternative files
|
||||
del /q *_backup.py 2>nul
|
||||
del /q *_previous.py 2>nul
|
||||
del /q *_fixed.py 2>nul
|
||||
del /q cross_tabulated_csv_exporter_*.py 2>nul
|
||||
|
||||
REM Delete documentation files
|
||||
del /q *.md 2>nul
|
||||
del /q LICENSE 2>nul
|
||||
del /q .gitignore 2>nul
|
||||
|
||||
REM Delete directories not needed for CLI
|
||||
rmdir /s /q back_src 2>nul
|
||||
rmdir /s /q docs 2>nul
|
||||
rmdir /s /q testsample 2>nul
|
||||
rmdir /s /q uploads 2>nul
|
||||
rmdir /s /q assets 2>nul
|
||||
rmdir /s /q results 2>nul
|
||||
rmdir /s /q __pycache__ 2>nul
|
||||
rmdir /s /q .vscode 2>nul
|
||||
rmdir /s /q .git 2>nul
|
||||
rmdir /s /q .gemini 2>nul
|
||||
rmdir /s /q .venv 2>nul
|
||||
|
||||
echo Essential files for CLI processing:
|
||||
echo - batch_cli.py
|
||||
echo - config.py
|
||||
echo - multi_file_processor.py
|
||||
echo - pdf_processor.py
|
||||
echo - dxf_processor.py
|
||||
echo - gemini_analyzer.py
|
||||
echo - csv_exporter.py
|
||||
echo - requirements.txt
|
||||
echo - mapping_table_json.json
|
||||
echo - .env
|
||||
|
||||
echo.
|
||||
echo Creating virtual environment...
|
||||
python -m venv venv
|
||||
|
||||
echo.
|
||||
echo Activating virtual environment and installing packages...
|
||||
call venv\Scripts\activate.bat
|
||||
pip install --upgrade pip
|
||||
pip install PyMuPDF google-genai Pillow ezdxf numpy python-dotenv pandas requests
|
||||
|
||||
echo.
|
||||
echo Testing installation...
|
||||
python -c "import fitz; print('✓ PyMuPDF OK')"
|
||||
python -c "import google.genai; print('✓ Gemini API OK')"
|
||||
python -c "import pandas; print('✓ Pandas OK')"
|
||||
python -c "import ezdxf; print('✓ EZDXF OK')"
|
||||
|
||||
echo.
|
||||
echo Setup complete!
|
||||
echo Virtual environment created at: fletimageanalysis\venv\
|
||||
echo.
|
||||
pause
|
||||
@@ -8,37 +8,37 @@ using System.Text.Json.Serialization;
|
||||
public class MappingTableData
|
||||
{
|
||||
[JsonPropertyName("mapping_table")]
|
||||
public MappingTable MappingTable { get; set; }
|
||||
public MappingTable MappingTable { get; set; } = default!;
|
||||
}
|
||||
|
||||
public class MappingTable
|
||||
{
|
||||
[JsonPropertyName("ailabel_to_systems")]
|
||||
public Dictionary<string, SystemFields> AilabelToSystems { get; set; }
|
||||
public Dictionary<string, SystemFields> AilabelToSystems { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("system_mappings")]
|
||||
public SystemMappings SystemMappings { get; set; }
|
||||
public SystemMappings SystemMappings { get; set; } = default!;
|
||||
}
|
||||
|
||||
public class SystemFields
|
||||
{
|
||||
[JsonPropertyName("molit")]
|
||||
public string Molit { get; set; }
|
||||
public string Molit { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("expressway")]
|
||||
public string Expressway { get; set; }
|
||||
public string Expressway { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("railway")]
|
||||
public string Railway { get; set; }
|
||||
public string Railway { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("docaikey")]
|
||||
public string DocAiKey { get; set; }
|
||||
public string DocAiKey { get; set; } = default!;
|
||||
}
|
||||
|
||||
public class SystemMappings
|
||||
{
|
||||
[JsonPropertyName("expressway_to_transportation")]
|
||||
public Dictionary<string, string> ExpresswayToTransportation { get; set; }
|
||||
public Dictionary<string, string> ExpresswayToTransportation { get; set; } = default!;
|
||||
}
|
||||
|
||||
// 필드 매퍼 클래스
|
||||
@@ -56,21 +56,164 @@ public class FieldMapper
|
||||
/// </summary>
|
||||
public static FieldMapper LoadFromFile(string jsonFilePath)
|
||||
{
|
||||
string jsonContent = File.ReadAllText(jsonFilePath);
|
||||
var options = new JsonSerializerOptions
|
||||
try
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
};
|
||||
string jsonContent = File.ReadAllText(jsonFilePath, System.Text.Encoding.UTF8);
|
||||
Console.WriteLine($"[DEBUG] 매핑 테이블 JSON 파일 크기: {jsonContent.Length} bytes");
|
||||
|
||||
var mappingData = JsonSerializer.Deserialize<MappingTableData>(jsonContent, options);
|
||||
return new FieldMapper(mappingData);
|
||||
// JSON 내용 정리 (주석 제거 등)
|
||||
jsonContent = CleanJsonContent(jsonContent);
|
||||
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
AllowTrailingCommas = true
|
||||
};
|
||||
|
||||
var mappingData = JsonSerializer.Deserialize<MappingTableData>(jsonContent, options);
|
||||
Console.WriteLine($"[DEBUG] 매핑 테이블 로드 성공: {mappingData?.MappingTable?.AilabelToSystems?.Count ?? 0}개 항목");
|
||||
return new FieldMapper(mappingData!);
|
||||
}
|
||||
catch (JsonException jsonEx)
|
||||
{
|
||||
Console.WriteLine($"❌ 매핑 테이블 JSON 파싱 오류: {jsonEx.Message}");
|
||||
Console.WriteLine($"❌ 파일: {jsonFilePath}");
|
||||
if (File.Exists(jsonFilePath))
|
||||
{
|
||||
string content = File.ReadAllText(jsonFilePath);
|
||||
Console.WriteLine($"❌ JSON 내용 미리보기 (첫 500자):");
|
||||
Console.WriteLine(content.Length > 500 ? content.Substring(0, 500) + "..." : content);
|
||||
}
|
||||
throw new Exception($"매핑 테이블 JSON 파일 파싱 실패: {jsonEx.Message}\n파일: {jsonFilePath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"❌ 매핑 테이블 로드 중 오류: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON 내용을 정리하여 파싱 가능한 상태로 만듭니다.
|
||||
/// 주석 제거 및 기타 무효한 문자 처리
|
||||
/// </summary>
|
||||
/// <param name="jsonContent">원본 JSON 내용</param>
|
||||
/// <returns>정리된 JSON 내용</returns>
|
||||
private static string CleanJsonContent(string jsonContent)
|
||||
{
|
||||
if (string.IsNullOrEmpty(jsonContent))
|
||||
return jsonContent;
|
||||
|
||||
try
|
||||
{
|
||||
// 줄별로 처리하여 주석 제거
|
||||
var lines = jsonContent.Split('\n');
|
||||
var cleanedLines = new List<string>();
|
||||
|
||||
bool inMultiLineComment = false;
|
||||
|
||||
foreach (string line in lines)
|
||||
{
|
||||
string processedLine = line;
|
||||
|
||||
// 멀티라인 주석 처리 (/* */)
|
||||
if (inMultiLineComment)
|
||||
{
|
||||
int endIndex = processedLine.IndexOf("*/");
|
||||
if (endIndex >= 0)
|
||||
{
|
||||
processedLine = processedLine.Substring(endIndex + 2);
|
||||
inMultiLineComment = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
continue; // 전체 라인이 주석
|
||||
}
|
||||
}
|
||||
|
||||
// 멀티라인 주석 시작 확인
|
||||
int multiLineStart = processedLine.IndexOf("/*");
|
||||
if (multiLineStart >= 0)
|
||||
{
|
||||
int multiLineEnd = processedLine.IndexOf("*/", multiLineStart + 2);
|
||||
if (multiLineEnd >= 0)
|
||||
{
|
||||
// 같은 라인에서 시작하고 끝나는 주석
|
||||
processedLine = processedLine.Substring(0, multiLineStart) +
|
||||
processedLine.Substring(multiLineEnd + 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 멀티라인 주석 시작
|
||||
processedLine = processedLine.Substring(0, multiLineStart);
|
||||
inMultiLineComment = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 싱글라인 주석 제거 (//) - 문자열 내부의 //는 제외
|
||||
bool inString = false;
|
||||
bool escaped = false;
|
||||
int commentIndex = -1;
|
||||
|
||||
for (int i = 0; i < processedLine.Length - 1; i++)
|
||||
{
|
||||
char current = processedLine[i];
|
||||
char next = processedLine[i + 1];
|
||||
|
||||
if (escaped)
|
||||
{
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current == '\\')
|
||||
{
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current == '"')
|
||||
{
|
||||
inString = !inString;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!inString && current == '/' && next == '/')
|
||||
{
|
||||
commentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (commentIndex >= 0)
|
||||
{
|
||||
processedLine = processedLine.Substring(0, commentIndex);
|
||||
}
|
||||
|
||||
// 빈 라인이 아니면 추가
|
||||
if (!string.IsNullOrWhiteSpace(processedLine))
|
||||
{
|
||||
cleanedLines.Add(processedLine);
|
||||
}
|
||||
}
|
||||
|
||||
string result = string.Join("\n", cleanedLines);
|
||||
Console.WriteLine($"[DEBUG] 매핑 테이블 JSON 정리 완료: {jsonContent.Length} -> {result.Length} bytes");
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"❌ 매핑 테이블 JSON 정리 중 오류: {ex.Message}");
|
||||
// 정리 실패시 원본 반환
|
||||
return jsonContent;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AI 라벨을 고속도로공사 필드명으로 변환
|
||||
/// </summary>
|
||||
public string AilabelToExpressway(string ailabel)
|
||||
public string? AilabelToExpressway(string ailabel)
|
||||
{
|
||||
if (_mappingData.MappingTable.AilabelToSystems.TryGetValue(ailabel, out var systemFields))
|
||||
{
|
||||
@@ -79,10 +222,22 @@ public class FieldMapper
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AI 라벨을 DocAiKey 값으로 변환
|
||||
/// </summary>
|
||||
public string? AilabelToDocAiKey(string ailabel)
|
||||
{
|
||||
if (_mappingData.MappingTable.AilabelToSystems.TryGetValue(ailabel, out var systemFields))
|
||||
{
|
||||
return systemFields.DocAiKey;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 고속도로공사 필드명을 교통부 필드명으로 변환
|
||||
/// </summary>
|
||||
public string ExpresswayToTransportation(string expresswayField)
|
||||
public string? ExpresswayToTransportation(string expresswayField)
|
||||
{
|
||||
if (_mappingData.MappingTable.SystemMappings.ExpresswayToTransportation.TryGetValue(expresswayField, out var transportationField))
|
||||
{
|
||||
@@ -91,10 +246,50 @@ public class FieldMapper
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DocAiKey 값으로부터 해당하는 AI 라벨을 반환
|
||||
/// </summary>
|
||||
public string? DocAiKeyToAilabel(string docAiKey)
|
||||
{
|
||||
if (string.IsNullOrEmpty(docAiKey))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var kvp in _mappingData.MappingTable.AilabelToSystems)
|
||||
{
|
||||
if (kvp.Value.DocAiKey == docAiKey)
|
||||
{
|
||||
return kvp.Key;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Expressway 필드값으로부터 해당하는 AI 라벨을 반환
|
||||
/// </summary>
|
||||
public string? ExpresswayToAilabel(string expresswayField)
|
||||
{
|
||||
if (string.IsNullOrEmpty(expresswayField))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var kvp in _mappingData.MappingTable.AilabelToSystems)
|
||||
{
|
||||
if (kvp.Value.Expressway == expresswayField)
|
||||
{
|
||||
return kvp.Key;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AI 라벨 → 고속도로공사 → 교통부 순서로 변환
|
||||
/// </summary>
|
||||
public string AilabelToTransportationViaExpressway(string ailabel)
|
||||
public string? AilabelToTransportationViaExpressway(string ailabel)
|
||||
{
|
||||
var expresswayField = AilabelToExpressway(ailabel);
|
||||
if (!string.IsNullOrEmpty(expresswayField))
|
||||
@@ -107,7 +302,7 @@ public class FieldMapper
|
||||
/// <summary>
|
||||
/// AI 라벨에 해당하는 모든 시스템의 필드명을 반환
|
||||
/// </summary>
|
||||
public SystemFields GetAllSystemFields(string ailabel)
|
||||
public SystemFields? GetAllSystemFields(string ailabel)
|
||||
{
|
||||
if (_mappingData.MappingTable.AilabelToSystems.TryGetValue(ailabel, out var systemFields))
|
||||
{
|
||||
@@ -119,9 +314,9 @@ public class FieldMapper
|
||||
/// <summary>
|
||||
/// 여러 AI 라벨을 한번에 고속도로공사 필드명으로 변환
|
||||
/// </summary>
|
||||
public Dictionary<string, string> BatchConvertAilabelToExpressway(IEnumerable<string> ailabels)
|
||||
public Dictionary<string, string?> BatchConvertAilabelToExpressway(IEnumerable<string> ailabels)
|
||||
{
|
||||
var results = new Dictionary<string, string>();
|
||||
var results = new Dictionary<string, string?>();
|
||||
foreach (var label in ailabels)
|
||||
{
|
||||
results[label] = AilabelToExpressway(label);
|
||||
@@ -132,111 +327,130 @@ public class FieldMapper
|
||||
/// <summary>
|
||||
/// 여러 고속도로공사 필드를 한번에 교통부 필드명으로 변환
|
||||
/// </summary>
|
||||
public Dictionary<string, string> BatchConvertExpresswayToTransportation(IEnumerable<string> expresswayFields)
|
||||
public Dictionary<string, string?> BatchConvertExpresswayToTransportation(IEnumerable<string> expresswayFields)
|
||||
{
|
||||
var results = new Dictionary<string, string>();
|
||||
var results = new Dictionary<string, string?>();
|
||||
foreach (var field in expresswayFields)
|
||||
{
|
||||
results[field] = ExpresswayToTransportation(field);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 매핑 테이블에서 모든 DocAiKey 값의 목록을 반환합니다.
|
||||
/// </summary>
|
||||
public List<string> GetAllDocAiKeys()
|
||||
{
|
||||
var docAiKeys = new List<string>();
|
||||
|
||||
foreach (var kvp in _mappingData.MappingTable.AilabelToSystems)
|
||||
{
|
||||
var docAiKey = kvp.Value.DocAiKey;
|
||||
if (!string.IsNullOrEmpty(docAiKey))
|
||||
{
|
||||
docAiKeys.Add(docAiKey);
|
||||
}
|
||||
}
|
||||
|
||||
return docAiKeys;
|
||||
}
|
||||
}
|
||||
|
||||
// 사용 예제 프로그램
|
||||
class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 매핑 테이블 로드
|
||||
var mapper = FieldMapper.LoadFromFile("mapping_table.json");
|
||||
|
||||
Console.WriteLine("=== AI 라벨 → 고속도로공사 필드명 변환 ===");
|
||||
var testLabels = new[] { "도면명", "편철번호", "도면번호", "Main Title", "계정번호" };
|
||||
//class Program
|
||||
//{
|
||||
// static void Main(string[] args)
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// // 매핑 테이블 로드
|
||||
// var mapper = FieldMapper.LoadFromFile("mapping_table.json");
|
||||
|
||||
foreach (var label in testLabels)
|
||||
{
|
||||
var expresswayField = mapper.AilabelToExpressway(label);
|
||||
Console.WriteLine($"{label} → {expresswayField ?? "N/A"}");
|
||||
}
|
||||
// Console.WriteLine("=== AI 라벨 → 고속도로공사 필드명 변환 ===");
|
||||
// var testLabels = new[] { "도면명", "편철번호", "도면번호", "Main Title", "계정번호" };
|
||||
|
||||
Console.WriteLine("\n=== 고속도로공사 → 교통부 필드명 변환 ===");
|
||||
var expresswayFields = new[] { "TD_DNAME_MAIN", "TD_DWGNO", "TD_DWGCODE", "TR_RNUM1" };
|
||||
// foreach (var label in testLabels)
|
||||
// {
|
||||
// var expresswayField = mapper.AilabelToExpressway(label);
|
||||
// Console.WriteLine($"{label} → {expresswayField ?? "N/A"}");
|
||||
// }
|
||||
|
||||
foreach (var field in expresswayFields)
|
||||
{
|
||||
var transportationField = mapper.ExpresswayToTransportation(field);
|
||||
Console.WriteLine($"{field} → {transportationField ?? "N/A"}");
|
||||
}
|
||||
// Console.WriteLine("\n=== 고속도로공사 → 교통부 필드명 변환 ===");
|
||||
// var expresswayFields = new[] { "TD_DNAME_MAIN", "TD_DWGNO", "TD_DWGCODE", "TR_RNUM1" };
|
||||
|
||||
Console.WriteLine("\n=== AI 라벨 → 고속도로공사 → 교통부 (연속 변환) ===");
|
||||
foreach (var label in testLabels)
|
||||
{
|
||||
var expresswayField = mapper.AilabelToExpressway(label);
|
||||
var transportationField = mapper.AilabelToTransportationViaExpressway(label);
|
||||
Console.WriteLine($"{label} → {expresswayField ?? "N/A"} → {transportationField ?? "N/A"}");
|
||||
}
|
||||
// foreach (var field in expresswayFields)
|
||||
// {
|
||||
// var transportationField = mapper.ExpresswayToTransportation(field);
|
||||
// Console.WriteLine($"{field} → {transportationField ?? "N/A"}");
|
||||
// }
|
||||
|
||||
Console.WriteLine("\n=== 특정 AI 라벨의 모든 시스템 필드명 ===");
|
||||
var allFields = mapper.GetAllSystemFields("도면명");
|
||||
if (allFields != null)
|
||||
{
|
||||
Console.WriteLine("도면명에 해당하는 모든 시스템 필드:");
|
||||
Console.WriteLine($" 국토교통부: {allFields.Molit}");
|
||||
Console.WriteLine($" 고속도로공사: {allFields.Expressway}");
|
||||
Console.WriteLine($" 국가철도공단: {allFields.Railway}");
|
||||
Console.WriteLine($" 문서AI키: {allFields.DocAiKey}");
|
||||
}
|
||||
// Console.WriteLine("\n=== AI 라벨 → 고속도로공사 → 교통부 (연속 변환) ===");
|
||||
// foreach (var label in testLabels)
|
||||
// {
|
||||
// var expresswayField = mapper.AilabelToExpressway(label);
|
||||
// var transportationField = mapper.AilabelToTransportationViaExpressway(label);
|
||||
// Console.WriteLine($"{label} → {expresswayField ?? "N/A"} → {transportationField ?? "N/A"}");
|
||||
// }
|
||||
|
||||
Console.WriteLine("\n=== 배치 처리 예제 ===");
|
||||
var batchResults = mapper.BatchConvertAilabelToExpressway(testLabels);
|
||||
foreach (var result in batchResults)
|
||||
{
|
||||
Console.WriteLine($"배치 변환: {result.Key} → {result.Value ?? "N/A"}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"오류 발생: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Console.WriteLine("\n=== 특정 AI 라벨의 모든 시스템 필드명 ===");
|
||||
// var allFields = mapper.GetAllSystemFields("도면명");
|
||||
// if (allFields != null)
|
||||
// {
|
||||
// Console.WriteLine("도면명에 해당하는 모든 시스템 필드:");
|
||||
// Console.WriteLine($" 국토교통부: {allFields.Molit}");
|
||||
// Console.WriteLine($" 고속도로공사: {allFields.Expressway}");
|
||||
// Console.WriteLine($" 국가철도공단: {allFields.Railway}");
|
||||
// Console.WriteLine($" 문서AI키: {allFields.DocAiKey}");
|
||||
// }
|
||||
|
||||
// Console.WriteLine("\n=== 배치 처리 예제 ===");
|
||||
// var batchResults = mapper.BatchConvertAilabelToExpressway(testLabels);
|
||||
// foreach (var result in batchResults)
|
||||
// {
|
||||
// Console.WriteLine($"배치 변환: {result.Key} → {result.Value ?? "N/A"}");
|
||||
// }
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// Console.WriteLine($"오류 발생: {ex.Message}");
|
||||
// }
|
||||
// }
|
||||
|
||||
// 확장 메서드 (선택사항)
|
||||
public static class FieldMapperExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 특정 시스템의 필드명을 다른 시스템으로 변환
|
||||
/// </summary>
|
||||
public static string ConvertBetweenSystems(this FieldMapper mapper, string sourceField, string sourceSystem, string targetSystem)
|
||||
{
|
||||
// 역방향 조회를 위한 확장 메서드
|
||||
foreach (var kvp in mapper._mappingData.MappingTable.AilabelToSystems)
|
||||
{
|
||||
var systemFields = kvp.Value;
|
||||
string sourceFieldValue = sourceSystem switch
|
||||
{
|
||||
"molit" => systemFields.Molit,
|
||||
"expressway" => systemFields.Expressway,
|
||||
"railway" => systemFields.Railway,
|
||||
"docaikey" => systemFields.DocAiKey,
|
||||
_ => null
|
||||
};
|
||||
//public static class FieldMapperExtensions
|
||||
//{
|
||||
// /// <summary>
|
||||
// /// 특정 시스템의 필드명을 다른 시스템으로 변환
|
||||
// /// </summary>
|
||||
// public static string ConvertBetweenSystems(this FieldMapper mapper, string sourceField, string sourceSystem, string targetSystem)
|
||||
// {
|
||||
// // 역방향 조회를 위한 확장 메서드
|
||||
// foreach (var kvp in mapper._mappingData.MappingTable.AilabelToSystems)
|
||||
// {
|
||||
// var systemFields = kvp.Value;
|
||||
// string sourceFieldValue = sourceSystem switch
|
||||
// {
|
||||
// "molit" => systemFields.Molit,
|
||||
// "expressway" => systemFields.Expressway,
|
||||
// "railway" => systemFields.Railway,
|
||||
// "docaikey" => systemFields.DocAiKey,
|
||||
// _ => null
|
||||
// };
|
||||
|
||||
if (sourceFieldValue == sourceField)
|
||||
{
|
||||
return targetSystem switch
|
||||
{
|
||||
"molit" => systemFields.Molit,
|
||||
"expressway" => systemFields.Expressway,
|
||||
"railway" => systemFields.Railway,
|
||||
"docaikey" => systemFields.DocAiKey,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// if (sourceFieldValue == sourceField)LL
|
||||
// {
|
||||
// return targetSystem switch
|
||||
// {
|
||||
// "molit" => systemFields.Molit,
|
||||
// "expressway" => systemFields.Expressway,
|
||||
// "railway" => systemFields.Railway,
|
||||
// "docaikey" => systemFields.DocAiKey,
|
||||
// _ => null
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
// return null;
|
||||
// }
|
||||
//}
|
||||
19
fletimageanalysis/.env
Normal file
19
fletimageanalysis/.env
Normal file
@@ -0,0 +1,19 @@
|
||||
# 환경 변수 설정 파일
|
||||
# 실제 사용 시 이 파일을 .env로 복사하고 실제 값으로 변경하세요
|
||||
|
||||
# Gemini API 키 (필수)
|
||||
GEMINI_API_KEY=AIzaSyA4XUw9LJp5zQ5CkB3GVVAQfTL8z6BGVcs
|
||||
|
||||
# 애플리케이션 설정
|
||||
APP_TITLE=PDF 도면 분석기
|
||||
APP_VERSION=1.0.0
|
||||
DEBUG=False
|
||||
|
||||
# 파일 업로드 설정
|
||||
MAX_FILE_SIZE_MB=50
|
||||
ALLOWED_EXTENSIONS=pdf
|
||||
UPLOAD_FOLDER=uploads
|
||||
|
||||
# Gemini API 설정
|
||||
GEMINI_MODEL=gemini-2.5-flash
|
||||
DEFAULT_PROMPT=pdf 이미지 분석하여 도면인지 어떤 정보들이 있는지 알려줘.
|
||||
19
fletimageanalysis/.env.example
Normal file
19
fletimageanalysis/.env.example
Normal file
@@ -0,0 +1,19 @@
|
||||
# 환경 변수 설정 파일
|
||||
# 실제 사용 시 이 파일을 .env로 복사하고 실제 값으로 변경하세요
|
||||
|
||||
# Gemini API 키 (필수)
|
||||
GEMINI_API_KEY=your_gemini_api_key_here
|
||||
|
||||
# 애플리케이션 설정
|
||||
APP_TITLE=PDF 도면 분석기
|
||||
APP_VERSION=1.0.0
|
||||
DEBUG=False
|
||||
|
||||
# 파일 업로드 설정
|
||||
MAX_FILE_SIZE_MB=50
|
||||
ALLOWED_EXTENSIONS=pdf
|
||||
UPLOAD_FOLDER=uploads
|
||||
|
||||
# Gemini API 설정
|
||||
GEMINI_MODEL=gemini-2.5-flash
|
||||
DEFAULT_PROMPT=pdf 이미지 분석하여 도면인지 어떤 정보들이 있는지 알려줘.
|
||||
216
fletimageanalysis/batch_cli.py
Normal file
216
fletimageanalysis/batch_cli.py
Normal file
@@ -0,0 +1,216 @@
|
||||
"""
|
||||
배치 처리 명령줄 인터페이스
|
||||
WPF 애플리케이션에서 호출 가능한 간단한 배치 처리 도구
|
||||
|
||||
Usage:
|
||||
python batch_cli.py --files "file1.pdf,file2.dxf" --schema "한국도로공사" --concurrent 3 --batch-mode true --save-intermediate false --include-errors true --output "results.csv"
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
|
||||
# 프로젝트 모듈 임포트
|
||||
from config import Config
|
||||
from multi_file_processor import MultiFileProcessor, BatchProcessingConfig, generate_default_csv_filename
|
||||
|
||||
# 로깅 설정
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BatchCLI:
|
||||
"""배치 처리 명령줄 인터페이스 클래스"""
|
||||
|
||||
def __init__(self):
|
||||
self.processor = None
|
||||
self.start_time = None
|
||||
|
||||
def setup_processor(self) -> bool:
|
||||
"""다중 파일 처리기 설정"""
|
||||
try:
|
||||
# 설정 검증
|
||||
config_errors = Config.validate_config()
|
||||
if config_errors:
|
||||
for error in config_errors:
|
||||
print(f"ERROR: {error}")
|
||||
return False
|
||||
|
||||
# Gemini API 키 확인
|
||||
gemini_api_key = Config.get_gemini_api_key()
|
||||
if not gemini_api_key:
|
||||
print("ERROR: Gemini API 키가 설정되지 않았습니다")
|
||||
return False
|
||||
|
||||
# 처리기 초기화
|
||||
self.processor = MultiFileProcessor(gemini_api_key)
|
||||
print("START: 배치 처리기 초기화 완료")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"ERROR: 처리기 초기화 실패: {e}")
|
||||
return False
|
||||
|
||||
def parse_file_paths(self, files_arg: str) -> List[str]:
|
||||
"""파일 경로 문자열을 리스트로 파싱"""
|
||||
if not files_arg:
|
||||
return []
|
||||
|
||||
# 쉼표로 구분된 파일 경로들을 분리
|
||||
file_paths = [path.strip().strip('"\'') for path in files_arg.split(',')]
|
||||
|
||||
# 파일 존재 여부 확인
|
||||
valid_paths = []
|
||||
for path in file_paths:
|
||||
if os.path.exists(path):
|
||||
valid_paths.append(path)
|
||||
print(f"START: 파일 확인: {os.path.basename(path)}")
|
||||
else:
|
||||
print(f"ERROR: 파일을 찾을 수 없습니다: {path}")
|
||||
|
||||
return valid_paths
|
||||
|
||||
def parse_file_list_from_file(self, file_list_path: str) -> List[str]:
|
||||
"""파일 리스트 파일에서 파일 경로들을 읽어옴"""
|
||||
if not file_list_path or not os.path.exists(file_list_path):
|
||||
return []
|
||||
|
||||
valid_paths = []
|
||||
try:
|
||||
with open(file_list_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
path = line.strip().strip('"\'')
|
||||
if path and os.path.exists(path):
|
||||
valid_paths.append(path)
|
||||
print(f"START: 파일 확인: {os.path.basename(path)}")
|
||||
elif path:
|
||||
print(f"ERROR: 파일을 찾을 수 없음: {path}")
|
||||
|
||||
print(f"START: 총 {len(valid_paths)}개 파일 로드됨")
|
||||
return valid_paths
|
||||
|
||||
except Exception as e:
|
||||
print(f"ERROR: 파일 리스트 읽기 실패: {e}")
|
||||
return []
|
||||
|
||||
def create_batch_config(self, args) -> BatchProcessingConfig:
|
||||
"""명령줄 인수에서 배치 설정 생성"""
|
||||
config = BatchProcessingConfig(
|
||||
organization_type=args.schema,
|
||||
enable_gemini_batch_mode=args.batch_mode,
|
||||
max_concurrent_files=args.concurrent,
|
||||
save_intermediate_results=args.save_intermediate,
|
||||
output_csv_path=args.output,
|
||||
include_error_files=args.include_errors
|
||||
)
|
||||
return config
|
||||
|
||||
def progress_callback(self, current: int, total: int, status: str):
|
||||
"""진행률 콜백 함수 - WPF가 기대하는 형식으로 출력"""
|
||||
# WPF가 파싱할 수 있는 간단한 형식으로 출력
|
||||
print(f"PROGRESS: {current}/{total}")
|
||||
print(f"COMPLETED: {status}")
|
||||
|
||||
async def run_batch_processing(self, file_paths: List[str], config: BatchProcessingConfig) -> bool:
|
||||
"""배치 처리 실행"""
|
||||
try:
|
||||
self.start_time = time.time()
|
||||
total_files = len(file_paths)
|
||||
|
||||
print(f"START: 배치 처리 시작: {total_files}개 파일")
|
||||
|
||||
# 처리 실행
|
||||
results = await self.processor.process_multiple_files(
|
||||
file_paths, config, self.progress_callback
|
||||
)
|
||||
|
||||
# 처리 완료
|
||||
end_time = time.time()
|
||||
total_time = end_time - self.start_time
|
||||
|
||||
# 요약 정보
|
||||
summary = self.processor.get_processing_summary()
|
||||
|
||||
print(f"COMPLETED: 배치 처리 완료!")
|
||||
print(f"COMPLETED: 총 처리 시간: {total_time:.1f}초")
|
||||
print(f"COMPLETED: 성공: {summary['success_files']}개, 실패: {summary['failed_files']}개")
|
||||
print(f"COMPLETED: CSV 결과 저장: {config.output_csv_path}")
|
||||
print(f"COMPLETED: JSON 결과 저장: {config.output_csv_path.replace('.csv', '.json')}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"ERROR: 배치 처리 중 오류: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def str_to_bool(value: str) -> bool:
|
||||
"""문자열을 boolean으로 변환"""
|
||||
return value.lower() in ["true", "1", "yes", "on"]
|
||||
|
||||
|
||||
async def main():
|
||||
"""메인 함수"""
|
||||
parser = argparse.ArgumentParser(description="PDF/DXF 파일 배치 처리 도구")
|
||||
|
||||
# 파일 입력 방식 (둘 중 하나 필수)
|
||||
input_group = parser.add_mutually_exclusive_group(required=True)
|
||||
input_group.add_argument("--files", "-f", help="처리할 파일 경로들 (쉼표로 구분)")
|
||||
input_group.add_argument("--file-list", "-fl", help="처리할 파일 경로가 담긴 텍스트 파일")
|
||||
|
||||
# 선택적 인수들
|
||||
parser.add_argument("--schema", "-s", default="한국도로공사", help="분석 스키마")
|
||||
parser.add_argument("--concurrent", "-c", type=int, default=3, help="동시 처리할 파일 수")
|
||||
parser.add_argument("--batch-mode", "-b", default="false", help="배치 모드 사용 여부")
|
||||
parser.add_argument("--save-intermediate", "-i", default="true", help="중간 결과 저장 여부")
|
||||
parser.add_argument("--include-errors", "-e", default="true", help="오류 파일 포함 여부")
|
||||
parser.add_argument("--output", "-o", help="출력 CSV 파일 경로 (JSON 파일도 함께 생성됨)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# CLI 인스턴스 생성
|
||||
cli = BatchCLI()
|
||||
|
||||
# 처리기 설정
|
||||
if not cli.setup_processor():
|
||||
sys.exit(1)
|
||||
|
||||
# 파일 경로 파싱
|
||||
if args.files:
|
||||
input_files = cli.parse_file_paths(args.files)
|
||||
else:
|
||||
input_files = cli.parse_file_list_from_file(args.file_list)
|
||||
|
||||
if not input_files:
|
||||
print("ERROR: 처리할 파일이 없습니다.")
|
||||
sys.exit(1)
|
||||
|
||||
# boolean 변환
|
||||
args.batch_mode = str_to_bool(args.batch_mode)
|
||||
args.save_intermediate = str_to_bool(args.save_intermediate)
|
||||
args.include_errors = str_to_bool(args.include_errors)
|
||||
|
||||
# 배치 설정 생성
|
||||
config = cli.create_batch_config(args)
|
||||
|
||||
# 배치 처리 실행
|
||||
success = await cli.run_batch_processing(input_files, config)
|
||||
|
||||
if success:
|
||||
print("SUCCESS: 배치 처리가 성공적으로 완료되었습니다.")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("ERROR: 배치 처리 중 오류가 발생했습니다.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
74
fletimageanalysis/config.py
Normal file
74
fletimageanalysis/config.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
설정 관리 모듈
|
||||
환경 변수 및 애플리케이션 설정을 관리합니다.
|
||||
"""
|
||||
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from pathlib import Path
|
||||
|
||||
# .env 파일 로드
|
||||
load_dotenv()
|
||||
|
||||
class Config:
|
||||
"""애플리케이션 설정 클래스"""
|
||||
|
||||
# 기본 애플리케이션 설정
|
||||
APP_TITLE = os.getenv("APP_TITLE", "PDF/DXF 도면 분석기")
|
||||
APP_VERSION = os.getenv("APP_VERSION", "1.1.0")
|
||||
DEBUG = os.getenv("DEBUG", "False").lower() == "true"
|
||||
|
||||
# API 설정
|
||||
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
|
||||
GEMINI_MODEL = os.getenv("GEMINI_MODEL", "gemini-2.5-pro")
|
||||
DEFAULT_PROMPT = os.getenv(
|
||||
"DEFAULT_PROMPT",
|
||||
"pdf 이미지 분석하여 도면인지 어떤 정보들이 있는지 알려줘.structured_output 이외에 정보도 기타에 넣어줘."
|
||||
)
|
||||
|
||||
# 파일 업로드 설정
|
||||
MAX_FILE_SIZE_MB = int(os.getenv("MAX_FILE_SIZE_MB", "50"))
|
||||
ALLOWED_EXTENSIONS = os.getenv("ALLOWED_EXTENSIONS", "pdf,dxf").split(",")
|
||||
UPLOAD_FOLDER = os.getenv("UPLOAD_FOLDER", "uploads")
|
||||
|
||||
# 경로 설정
|
||||
BASE_DIR = Path(__file__).parent
|
||||
UPLOAD_DIR = BASE_DIR / UPLOAD_FOLDER
|
||||
ASSETS_DIR = BASE_DIR / "assets"
|
||||
RESULTS_FOLDER = BASE_DIR / "results"
|
||||
|
||||
@classmethod
|
||||
def validate_config(cls):
|
||||
"""설정 유효성 검사"""
|
||||
errors = []
|
||||
|
||||
if not cls.GEMINI_API_KEY:
|
||||
errors.append("GEMINI_API_KEY가 설정되지 않았습니다.")
|
||||
|
||||
if not cls.UPLOAD_DIR.exists():
|
||||
try:
|
||||
cls.UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||
except Exception as e:
|
||||
errors.append(f"업로드 폴더 생성 실패: {e}")
|
||||
|
||||
return errors
|
||||
|
||||
@classmethod
|
||||
def get_file_size_limit_bytes(cls):
|
||||
"""파일 크기 제한을 바이트로 반환"""
|
||||
return cls.MAX_FILE_SIZE_MB * 1024 * 1024
|
||||
|
||||
@classmethod
|
||||
def get_gemini_api_key(cls):
|
||||
"""Gemini API 키 반환"""
|
||||
return cls.GEMINI_API_KEY
|
||||
|
||||
# 설정 검증
|
||||
if __name__ == "__main__":
|
||||
config_errors = Config.validate_config()
|
||||
if config_errors:
|
||||
print("설정 오류:")
|
||||
for error in config_errors:
|
||||
print(f" - {error}")
|
||||
else:
|
||||
print("설정이 올바르게 구성되었습니다.")
|
||||
638
fletimageanalysis/cross_tabulated_csv_exporter.py
Normal file
638
fletimageanalysis/cross_tabulated_csv_exporter.py
Normal file
@@ -0,0 +1,638 @@
|
||||
"""
|
||||
Cross-Tabulated CSV 내보내기 모듈 (개선된 통합 버전)
|
||||
JSON 형태의 분석 결과를 key-value 형태의 cross-tabulated CSV로 저장하는 기능을 제공합니다.
|
||||
관련 키들(value, x, y)을 하나의 행으로 통합하여 저장합니다.
|
||||
|
||||
Author: Claude Assistant
|
||||
Created: 2025-07-15
|
||||
Updated: 2025-07-16 (키 통합 개선 버전)
|
||||
Version: 2.0.0
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Any, Optional, Union, Tuple
|
||||
import os
|
||||
import re
|
||||
from collections import defaultdict
|
||||
|
||||
# 로깅 설정
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CrossTabulatedCSVExporter:
|
||||
"""Cross-Tabulated CSV 내보내기 클래스 (개선된 통합 버전)"""
|
||||
|
||||
def __init__(self):
|
||||
"""Cross-Tabulated CSV 내보내기 초기화"""
|
||||
self.coordinate_pattern = re.compile(r'\b(\d+)\s*,\s*(\d+)\b') # x,y 좌표 패턴
|
||||
self.debug_mode = True # 디버깅 모드 활성화
|
||||
|
||||
# 키 그룹핑을 위한 패턴들
|
||||
self.value_suffixes = ['_value', '_val', '_text', '_content']
|
||||
self.x_suffixes = ['_x', '_x_coord', '_x_position', '_left']
|
||||
self.y_suffixes = ['_y', '_y_coord', '_y_position', '_top']
|
||||
|
||||
def export_cross_tabulated_csv(
|
||||
self,
|
||||
processing_results: List[Any],
|
||||
output_path: str,
|
||||
include_coordinates: bool = True,
|
||||
coordinate_source: str = "auto" # "auto", "text_blocks", "analysis_result", "none"
|
||||
) -> bool:
|
||||
"""
|
||||
처리 결과를 cross-tabulated CSV 형태로 저장 (키 통합 기능 포함)
|
||||
|
||||
Args:
|
||||
processing_results: 다중 파일 처리 결과 리스트
|
||||
output_path: 출력 CSV 파일 경로
|
||||
include_coordinates: 좌표 정보 포함 여부
|
||||
coordinate_source: 좌표 정보 출처 ("auto", "text_blocks", "analysis_result", "none")
|
||||
|
||||
Returns:
|
||||
저장 성공 여부
|
||||
"""
|
||||
try:
|
||||
if self.debug_mode:
|
||||
logger.info(f"=== Cross-tabulated CSV 저장 시작 (통합 버전) ===")
|
||||
logger.info(f"입력된 결과 수: {len(processing_results)}")
|
||||
logger.info(f"출력 경로: {output_path}")
|
||||
logger.info(f"좌표 포함: {include_coordinates}, 좌표 출처: {coordinate_source}")
|
||||
|
||||
# 입력 데이터 검증
|
||||
if not processing_results:
|
||||
logger.warning("입력된 처리 결과가 비어있습니다.")
|
||||
return False
|
||||
|
||||
# 각 결과 객체의 구조 분석
|
||||
for i, result in enumerate(processing_results):
|
||||
if self.debug_mode:
|
||||
logger.info(f"결과 {i+1}: {self._analyze_result_structure(result)}")
|
||||
|
||||
# 모든 파일의 key-value 쌍을 수집
|
||||
all_grouped_data = []
|
||||
|
||||
for i, result in enumerate(processing_results):
|
||||
try:
|
||||
if not hasattr(result, 'success'):
|
||||
logger.warning(f"결과 {i+1}: 'success' 속성이 없습니다. 스킵합니다.")
|
||||
continue
|
||||
|
||||
if not result.success:
|
||||
if self.debug_mode:
|
||||
logger.info(f"결과 {i+1}: 실패한 파일, 스킵합니다 ({getattr(result, 'error_message', 'Unknown error')})")
|
||||
continue # 실패한 파일은 제외
|
||||
|
||||
# 기본 key-value 쌍 추출
|
||||
file_data = self._extract_key_value_pairs(result, include_coordinates, coordinate_source)
|
||||
|
||||
if file_data:
|
||||
# 관련 키들을 그룹화하여 통합된 데이터 생성
|
||||
grouped_data = self._group_and_merge_keys(file_data, result)
|
||||
|
||||
if grouped_data:
|
||||
all_grouped_data.extend(grouped_data)
|
||||
if self.debug_mode:
|
||||
logger.info(f"결과 {i+1}: {len(file_data)}개 key-value 쌍 → {len(grouped_data)}개 통합 행 생성")
|
||||
else:
|
||||
if self.debug_mode:
|
||||
logger.warning(f"결과 {i+1}: 그룹화 후 데이터가 없습니다")
|
||||
else:
|
||||
if self.debug_mode:
|
||||
logger.warning(f"결과 {i+1}: key-value 쌍을 추출할 수 없습니다")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"결과 {i+1} 처리 중 오류: {str(e)}")
|
||||
continue
|
||||
|
||||
if not all_grouped_data:
|
||||
logger.warning("저장할 데이터가 없습니다. 모든 파일에서 유효한 key-value 쌍을 추출할 수 없었습니다.")
|
||||
if self.debug_mode:
|
||||
self._print_debug_summary(processing_results)
|
||||
return False
|
||||
|
||||
# DataFrame 생성
|
||||
df = pd.DataFrame(all_grouped_data)
|
||||
|
||||
# 컬럼 순서 정렬
|
||||
column_order = ['file_name', 'file_type', 'key', 'value']
|
||||
if include_coordinates and coordinate_source != "none":
|
||||
column_order.extend(['x', 'y'])
|
||||
|
||||
# 추가 컬럼들을 뒤에 배치
|
||||
existing_columns = [col for col in column_order if col in df.columns]
|
||||
additional_columns = [col for col in df.columns if col not in existing_columns]
|
||||
df = df[existing_columns + additional_columns]
|
||||
|
||||
# 출력 디렉토리 생성
|
||||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||
|
||||
# UTF-8 BOM으로 저장 (한글 호환성)
|
||||
df.to_csv(output_path, index=False, encoding='utf-8-sig')
|
||||
|
||||
logger.info(f"Cross-tabulated CSV 저장 완료: {output_path}")
|
||||
logger.info(f"총 {len(all_grouped_data)}개 통합 행 저장")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Cross-tabulated CSV 저장 오류: {str(e)}")
|
||||
return False
|
||||
|
||||
def _group_and_merge_keys(self, raw_data: List[Dict[str, Any]], result: Any) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
관련된 키들을 그룹화하고 하나의 행으로 통합
|
||||
|
||||
Args:
|
||||
raw_data: 원시 key-value 쌍 리스트
|
||||
result: 파일 처리 결과
|
||||
|
||||
Returns:
|
||||
통합된 데이터 리스트
|
||||
"""
|
||||
# 파일 기본 정보
|
||||
file_name = getattr(result, 'file_name', 'Unknown')
|
||||
file_type = getattr(result, 'file_type', 'Unknown')
|
||||
|
||||
# 키별로 데이터 그룹화
|
||||
key_groups = defaultdict(dict)
|
||||
|
||||
for data_row in raw_data:
|
||||
key = data_row.get('key', '')
|
||||
value = data_row.get('value', '')
|
||||
x = data_row.get('x', '')
|
||||
y = data_row.get('y', '')
|
||||
|
||||
# 기본 키 추출 (예: "사업명_value" -> "사업명")
|
||||
base_key = self._extract_base_key(key)
|
||||
|
||||
# 키 타입 결정 (value, x, y 등)
|
||||
key_type = self._determine_key_type(key)
|
||||
|
||||
if self.debug_mode and not key_groups[base_key]:
|
||||
logger.info(f"새 키 그룹 생성: '{base_key}' (원본: '{key}', 타입: '{key_type}')")
|
||||
|
||||
# 그룹에 데이터 추가
|
||||
if key_type == 'value':
|
||||
key_groups[base_key]['value'] = value
|
||||
# value에 좌표가 포함된 경우 사용
|
||||
if not key_groups[base_key].get('x') and x:
|
||||
key_groups[base_key]['x'] = x
|
||||
if not key_groups[base_key].get('y') and y:
|
||||
key_groups[base_key]['y'] = y
|
||||
elif key_type == 'x':
|
||||
key_groups[base_key]['x'] = value # x 값은 value 컬럼에서 가져옴
|
||||
elif key_type == 'y':
|
||||
key_groups[base_key]['y'] = value # y 값은 value 컬럼에서 가져옴
|
||||
else:
|
||||
# 일반적인 키인 경우 (suffix가 없는 경우)
|
||||
if not key_groups[base_key].get('value'):
|
||||
key_groups[base_key]['value'] = value
|
||||
if x and not key_groups[base_key].get('x'):
|
||||
key_groups[base_key]['x'] = x
|
||||
if y and not key_groups[base_key].get('y'):
|
||||
key_groups[base_key]['y'] = y
|
||||
|
||||
# 그룹화된 데이터를 최종 형태로 변환
|
||||
merged_data = []
|
||||
|
||||
for base_key, group_data in key_groups.items():
|
||||
# 빈 값이나 의미없는 데이터 제외
|
||||
if not group_data.get('value') or str(group_data.get('value')).strip() == '':
|
||||
continue
|
||||
|
||||
merged_row = {
|
||||
'file_name': file_name,
|
||||
'file_type': file_type,
|
||||
'key': base_key,
|
||||
'value': str(group_data.get('value', '')),
|
||||
'x': str(group_data.get('x', '')) if group_data.get('x') else '',
|
||||
'y': str(group_data.get('y', '')) if group_data.get('y') else '',
|
||||
}
|
||||
|
||||
merged_data.append(merged_row)
|
||||
|
||||
if self.debug_mode:
|
||||
logger.info(f"통합 행 생성: {base_key} = '{merged_row['value']}' ({merged_row['x']}, {merged_row['y']})")
|
||||
|
||||
return merged_data
|
||||
|
||||
def _extract_base_key(self, key: str) -> str:
|
||||
"""
|
||||
키에서 기본 이름 추출 (suffix 제거)
|
||||
|
||||
Args:
|
||||
key: 원본 키 (예: "사업명_value", "사업명_x")
|
||||
|
||||
Returns:
|
||||
기본 키 이름 (예: "사업명")
|
||||
"""
|
||||
if not key:
|
||||
return key
|
||||
|
||||
# 모든 가능한 suffix 확인
|
||||
all_suffixes = self.value_suffixes + self.x_suffixes + self.y_suffixes
|
||||
|
||||
for suffix in all_suffixes:
|
||||
if key.endswith(suffix):
|
||||
return key[:-len(suffix)]
|
||||
|
||||
# suffix가 없는 경우 원본 반환
|
||||
return key
|
||||
|
||||
def _determine_key_type(self, key: str) -> str:
|
||||
"""
|
||||
키의 타입 결정 (value, x, y, other)
|
||||
|
||||
Args:
|
||||
key: 키 이름
|
||||
|
||||
Returns:
|
||||
키 타입 ("value", "x", "y", "other")
|
||||
"""
|
||||
if not key:
|
||||
return "other"
|
||||
|
||||
key_lower = key.lower()
|
||||
|
||||
# value 타입 확인
|
||||
for suffix in self.value_suffixes:
|
||||
if key_lower.endswith(suffix.lower()):
|
||||
return "value"
|
||||
|
||||
# x 타입 확인
|
||||
for suffix in self.x_suffixes:
|
||||
if key_lower.endswith(suffix.lower()):
|
||||
return "x"
|
||||
|
||||
# y 타입 확인
|
||||
for suffix in self.y_suffixes:
|
||||
if key_lower.endswith(suffix.lower()):
|
||||
return "y"
|
||||
|
||||
return "other"
|
||||
|
||||
def _analyze_result_structure(self, result: Any) -> str:
|
||||
"""결과 객체의 구조를 분석하여 문자열로 반환"""
|
||||
try:
|
||||
info = []
|
||||
|
||||
# 기본 속성들 확인
|
||||
if hasattr(result, 'file_name'):
|
||||
info.append(f"file_name='{result.file_name}'")
|
||||
if hasattr(result, 'file_type'):
|
||||
info.append(f"file_type='{result.file_type}'")
|
||||
if hasattr(result, 'success'):
|
||||
info.append(f"success={result.success}")
|
||||
|
||||
# PDF 관련 속성
|
||||
if hasattr(result, 'pdf_analysis_result'):
|
||||
pdf_result = result.pdf_analysis_result
|
||||
if pdf_result:
|
||||
if isinstance(pdf_result, str):
|
||||
info.append(f"pdf_analysis_result=str({len(pdf_result)} chars)")
|
||||
else:
|
||||
info.append(f"pdf_analysis_result={type(pdf_result).__name__}")
|
||||
else:
|
||||
info.append("pdf_analysis_result=None")
|
||||
|
||||
# DXF 관련 속성
|
||||
if hasattr(result, 'dxf_title_blocks'):
|
||||
dxf_blocks = result.dxf_title_blocks
|
||||
if dxf_blocks:
|
||||
info.append(f"dxf_title_blocks=list({len(dxf_blocks)} blocks)")
|
||||
else:
|
||||
info.append("dxf_title_blocks=None")
|
||||
|
||||
return " | ".join(info) if info else "구조 분석 실패"
|
||||
|
||||
except Exception as e:
|
||||
return f"분석 오류: {str(e)}"
|
||||
|
||||
def _print_debug_summary(self, processing_results: List[Any]):
|
||||
"""디버깅을 위한 요약 정보 출력"""
|
||||
logger.info("=== 디버깅 요약 ===")
|
||||
|
||||
success_count = 0
|
||||
pdf_count = 0
|
||||
dxf_count = 0
|
||||
has_pdf_data = 0
|
||||
has_dxf_data = 0
|
||||
|
||||
for i, result in enumerate(processing_results):
|
||||
try:
|
||||
if hasattr(result, 'success') and result.success:
|
||||
success_count += 1
|
||||
|
||||
file_type = getattr(result, 'file_type', 'unknown').lower()
|
||||
if file_type == 'pdf':
|
||||
pdf_count += 1
|
||||
if getattr(result, 'pdf_analysis_result', None):
|
||||
has_pdf_data += 1
|
||||
elif file_type == 'dxf':
|
||||
dxf_count += 1
|
||||
if getattr(result, 'dxf_title_blocks', None):
|
||||
has_dxf_data += 1
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"결과 {i+1} 분석 중 오류: {str(e)}")
|
||||
|
||||
logger.info(f"총 결과: {len(processing_results)}개")
|
||||
logger.info(f"성공한 결과: {success_count}개")
|
||||
logger.info(f"PDF 파일: {pdf_count}개 (분석 데이터 있음: {has_pdf_data}개)")
|
||||
logger.info(f"DXF 파일: {dxf_count}개 (타이틀블록 데이터 있음: {has_dxf_data}개)")
|
||||
|
||||
def _extract_key_value_pairs(
|
||||
self,
|
||||
result: Any,
|
||||
include_coordinates: bool,
|
||||
coordinate_source: str
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
단일 파일 결과에서 key-value 쌍 추출
|
||||
|
||||
Args:
|
||||
result: 파일 처리 결과
|
||||
include_coordinates: 좌표 정보 포함 여부
|
||||
coordinate_source: 좌표 정보 출처
|
||||
|
||||
Returns:
|
||||
key-value 쌍 리스트
|
||||
"""
|
||||
data_rows = []
|
||||
|
||||
try:
|
||||
# 기본 정보 확인
|
||||
file_name = getattr(result, 'file_name', 'Unknown')
|
||||
file_type = getattr(result, 'file_type', 'Unknown')
|
||||
|
||||
base_info = {
|
||||
'file_name': file_name,
|
||||
'file_type': file_type,
|
||||
}
|
||||
|
||||
if self.debug_mode:
|
||||
logger.info(f"처리 중: {file_name} ({file_type})")
|
||||
|
||||
# PDF 분석 결과 처리
|
||||
if file_type.lower() == 'pdf':
|
||||
pdf_result = getattr(result, 'pdf_analysis_result', None)
|
||||
if pdf_result:
|
||||
pdf_rows = self._extract_pdf_key_values(result, base_info, include_coordinates, coordinate_source)
|
||||
data_rows.extend(pdf_rows)
|
||||
if self.debug_mode:
|
||||
logger.info(f"PDF에서 {len(pdf_rows)}개 key-value 쌍 추출")
|
||||
else:
|
||||
if self.debug_mode:
|
||||
logger.warning(f"PDF 분석 결과가 없습니다: {file_name}")
|
||||
|
||||
# DXF 분석 결과 처리
|
||||
elif file_type.lower() == 'dxf':
|
||||
dxf_blocks = getattr(result, 'dxf_title_blocks', None)
|
||||
if dxf_blocks:
|
||||
dxf_rows = self._extract_dxf_key_values(result, base_info, include_coordinates, coordinate_source)
|
||||
data_rows.extend(dxf_rows)
|
||||
if self.debug_mode:
|
||||
logger.info(f"DXF에서 {len(dxf_rows)}개 key-value 쌍 추출")
|
||||
else:
|
||||
if self.debug_mode:
|
||||
logger.warning(f"DXF 타이틀블록 데이터가 없습니다: {file_name}")
|
||||
|
||||
else:
|
||||
if self.debug_mode:
|
||||
logger.warning(f"지원하지 않는 파일 형식: {file_type}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Key-value 추출 오류 ({getattr(result, 'file_name', 'Unknown')}): {str(e)}")
|
||||
|
||||
return data_rows
|
||||
|
||||
def _extract_pdf_key_values(
|
||||
self,
|
||||
result: Any,
|
||||
base_info: Dict[str, str],
|
||||
include_coordinates: bool,
|
||||
coordinate_source: str
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""PDF 분석 결과에서 key-value 쌍 추출"""
|
||||
data_rows = []
|
||||
|
||||
try:
|
||||
# PDF 분석 결과를 JSON으로 파싱
|
||||
analysis_result = getattr(result, 'pdf_analysis_result', None)
|
||||
|
||||
if not analysis_result:
|
||||
return data_rows
|
||||
|
||||
if isinstance(analysis_result, str):
|
||||
try:
|
||||
analysis_data = json.loads(analysis_result)
|
||||
except json.JSONDecodeError:
|
||||
# JSON이 아닌 경우 텍스트로 처리
|
||||
analysis_data = {"분석결과": analysis_result}
|
||||
else:
|
||||
analysis_data = analysis_result
|
||||
|
||||
if self.debug_mode:
|
||||
logger.info(f"PDF 분석 데이터 구조: {type(analysis_data).__name__}")
|
||||
if isinstance(analysis_data, dict):
|
||||
logger.info(f"PDF 분석 데이터 키: {list(analysis_data.keys())}")
|
||||
|
||||
# 중첩된 구조를 평탄화하여 key-value 쌍 생성
|
||||
flattened_data = self._flatten_dict(analysis_data)
|
||||
|
||||
for key, value in flattened_data.items():
|
||||
if value is None or str(value).strip() == "":
|
||||
continue # 빈 값 제외
|
||||
|
||||
row_data = base_info.copy()
|
||||
row_data.update({
|
||||
'key': key,
|
||||
'value': str(value),
|
||||
})
|
||||
|
||||
# 좌표 정보 추가
|
||||
if include_coordinates and coordinate_source != "none":
|
||||
coordinates = self._extract_coordinates(key, value, coordinate_source)
|
||||
row_data.update(coordinates)
|
||||
|
||||
data_rows.append(row_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"PDF key-value 추출 오류: {str(e)}")
|
||||
|
||||
return data_rows
|
||||
|
||||
def _extract_dxf_key_values(
|
||||
self,
|
||||
result: Any,
|
||||
base_info: Dict[str, str],
|
||||
include_coordinates: bool,
|
||||
coordinate_source: str
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""DXF 분석 결과에서 key-value 쌍 추출"""
|
||||
data_rows = []
|
||||
|
||||
try:
|
||||
title_blocks = getattr(result, 'dxf_title_blocks', None)
|
||||
|
||||
if not title_blocks:
|
||||
return data_rows
|
||||
|
||||
if self.debug_mode:
|
||||
logger.info(f"DXF 타이틀블록 수: {len(title_blocks)}")
|
||||
|
||||
for block_idx, title_block in enumerate(title_blocks):
|
||||
if not isinstance(title_block, dict):
|
||||
continue
|
||||
|
||||
block_name = title_block.get('block_name', 'Unknown')
|
||||
|
||||
# 블록 정보
|
||||
row_data = base_info.copy()
|
||||
row_data.update({
|
||||
'key': f"{block_name}_블록명",
|
||||
'value': block_name,
|
||||
})
|
||||
|
||||
if include_coordinates and coordinate_source != "none":
|
||||
coordinates = self._extract_coordinates('블록명', block_name, coordinate_source)
|
||||
row_data.update(coordinates)
|
||||
|
||||
data_rows.append(row_data)
|
||||
|
||||
# 속성 정보
|
||||
attributes = title_block.get('attributes', [])
|
||||
if self.debug_mode:
|
||||
logger.info(f"블록 {block_idx+1} ({block_name}): {len(attributes)}개 속성")
|
||||
|
||||
for attr_idx, attr in enumerate(attributes):
|
||||
if not isinstance(attr, dict):
|
||||
continue
|
||||
|
||||
attr_text = attr.get('text', '')
|
||||
if not attr_text or str(attr_text).strip() == "":
|
||||
continue # 빈 속성 제외
|
||||
|
||||
# 속성별 key-value 쌍 생성
|
||||
attr_key = attr.get('tag', attr.get('prompt', f'Unknown_Attr_{attr_idx}'))
|
||||
attr_value = str(attr_text)
|
||||
|
||||
row_data = base_info.copy()
|
||||
row_data.update({
|
||||
'key': attr_key,
|
||||
'value': attr_value,
|
||||
})
|
||||
|
||||
# DXF 속성의 경우 insert 좌표 사용
|
||||
if include_coordinates and coordinate_source != "none":
|
||||
x_coord = attr.get('insert_x', '')
|
||||
y_coord = attr.get('insert_y', '')
|
||||
|
||||
if x_coord and y_coord:
|
||||
row_data.update({
|
||||
'x': round(float(x_coord), 2) if isinstance(x_coord, (int, float)) else x_coord,
|
||||
'y': round(float(y_coord), 2) if isinstance(y_coord, (int, float)) else y_coord,
|
||||
})
|
||||
else:
|
||||
row_data.update({'x': '', 'y': ''})
|
||||
|
||||
data_rows.append(row_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"DXF key-value 추출 오류: {str(e)}")
|
||||
|
||||
return data_rows
|
||||
|
||||
def _flatten_dict(self, data: Dict[str, Any], parent_key: str = '', sep: str = '_') -> Dict[str, Any]:
|
||||
"""
|
||||
중첩된 딕셔너리를 평탄화
|
||||
|
||||
Args:
|
||||
data: 평탄화할 딕셔너리
|
||||
parent_key: 부모 키
|
||||
sep: 구분자
|
||||
|
||||
Returns:
|
||||
평탄화된 딕셔너리
|
||||
"""
|
||||
items = []
|
||||
|
||||
for k, v in data.items():
|
||||
new_key = f"{parent_key}{sep}{k}" if parent_key else k
|
||||
|
||||
if isinstance(v, dict):
|
||||
# 중첩된 딕셔너리인 경우 재귀 호출
|
||||
items.extend(self._flatten_dict(v, new_key, sep=sep).items())
|
||||
elif isinstance(v, list):
|
||||
# 리스트인 경우 인덱스와 함께 처리
|
||||
for i, item in enumerate(v):
|
||||
if isinstance(item, dict):
|
||||
items.extend(self._flatten_dict(item, f"{new_key}_{i}", sep=sep).items())
|
||||
else:
|
||||
items.append((f"{new_key}_{i}", item))
|
||||
else:
|
||||
items.append((new_key, v))
|
||||
|
||||
return dict(items)
|
||||
|
||||
def _extract_coordinates(self, key: str, value: str, coordinate_source: str) -> Dict[str, str]:
|
||||
"""
|
||||
텍스트에서 좌표 정보 추출
|
||||
|
||||
Args:
|
||||
key: 키
|
||||
value: 값
|
||||
coordinate_source: 좌표 정보 출처
|
||||
|
||||
Returns:
|
||||
좌표 딕셔너리
|
||||
"""
|
||||
coordinates = {'x': '', 'y': ''}
|
||||
|
||||
try:
|
||||
# 값에서 좌표 패턴 찾기
|
||||
matches = self.coordinate_pattern.findall(str(value))
|
||||
|
||||
if matches:
|
||||
# 첫 번째 매치 사용
|
||||
x, y = matches[0]
|
||||
coordinates = {'x': x, 'y': y}
|
||||
else:
|
||||
# 키에서 좌표 정보 찾기
|
||||
key_matches = self.coordinate_pattern.findall(str(key))
|
||||
if key_matches:
|
||||
x, y = key_matches[0]
|
||||
coordinates = {'x': x, 'y': y}
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"좌표 추출 오류: {str(e)}")
|
||||
|
||||
return coordinates
|
||||
|
||||
|
||||
def generate_cross_tabulated_csv_filename(base_name: str = "cross_tabulated_analysis") -> str:
|
||||
"""기본 Cross-tabulated CSV 파일명 생성"""
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
return f"{base_name}_results_{timestamp}.csv"
|
||||
|
||||
|
||||
# 사용 예시
|
||||
if __name__ == "__main__":
|
||||
# 테스트용 예시
|
||||
exporter = CrossTabulatedCSVExporter()
|
||||
|
||||
# 샘플 처리 결과 (실제 데이터 구조에 맞게 수정)
|
||||
sample_results = []
|
||||
|
||||
# 실제 사용 시에는 processing_results를 전달
|
||||
# success = exporter.export_cross_tabulated_csv(
|
||||
# sample_results,
|
||||
# "test_cross_tabulated.csv",
|
||||
# include_coordinates=True
|
||||
# )
|
||||
|
||||
print("Cross-tabulated CSV 내보내기 모듈 (통합 버전) 테스트 완료")
|
||||
306
fletimageanalysis/csv_exporter.py
Normal file
306
fletimageanalysis/csv_exporter.py
Normal file
@@ -0,0 +1,306 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
CSV 저장 유틸리티 모듈
|
||||
DXF 타이틀블럭 Attribute 정보를 CSV 형식으로 저장
|
||||
"""
|
||||
|
||||
import csv
|
||||
import os
|
||||
import logging
|
||||
from typing import List, Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from config import Config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TitleBlockCSVExporter:
|
||||
"""타이틀블럭 속성 정보 CSV 저장 클래스"""
|
||||
|
||||
def __init__(self, output_dir: str = None):
|
||||
"""CSV 저장기 초기화"""
|
||||
self.output_dir = output_dir or Config.RESULTS_FOLDER
|
||||
os.makedirs(self.output_dir, exist_ok=True)
|
||||
|
||||
def export_title_block_attributes(
|
||||
self,
|
||||
title_block_info: Dict[str, Any],
|
||||
filename: str = None
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
타이틀블럭 속성 정보를 CSV 파일로 저장
|
||||
|
||||
Args:
|
||||
title_block_info: 타이틀블럭 정보 딕셔너리
|
||||
filename: 저장할 파일명 (없으면 자동 생성)
|
||||
|
||||
Returns:
|
||||
저장된 파일 경로 또는 None (실패시)
|
||||
"""
|
||||
try:
|
||||
if not title_block_info or not title_block_info.get('all_attributes'):
|
||||
logger.warning("타이틀블럭 속성 정보가 없습니다.")
|
||||
return None
|
||||
|
||||
# 파일명 생성
|
||||
if not filename:
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
block_name = title_block_info.get('block_name', 'Unknown_Block')
|
||||
filename = f"title_block_attributes_{block_name}_{timestamp}.csv"
|
||||
|
||||
# 확장자 확인
|
||||
if not filename.endswith('.csv'):
|
||||
filename += '.csv'
|
||||
|
||||
filepath = os.path.join(self.output_dir, filename)
|
||||
|
||||
# CSV 헤더 정의
|
||||
headers = [
|
||||
'block_name', # block_ref.name
|
||||
'attr_prompt', # attr.prompt
|
||||
'attr_text', # attr.text
|
||||
'attr_tag', # attr.tag
|
||||
'attr_insert_x', # attr.insert_x
|
||||
'attr_insert_y', # attr.insert_y
|
||||
'bounding_box_min_x', # attr.bounding_box.min_x
|
||||
'bounding_box_min_y', # attr.bounding_box.min_y
|
||||
'bounding_box_max_x', # attr.bounding_box.max_x
|
||||
'bounding_box_max_y', # attr.bounding_box.max_y
|
||||
'bounding_box_width', # attr.bounding_box.width
|
||||
'bounding_box_height', # attr.bounding_box.height
|
||||
'attr_height', # 추가: 텍스트 높이
|
||||
'attr_rotation', # 추가: 회전각
|
||||
'attr_layer', # 추가: 레이어
|
||||
'attr_style', # 추가: 스타일
|
||||
'entity_handle' # 추가: 엔티티 핸들
|
||||
]
|
||||
|
||||
# CSV 데이터 준비
|
||||
csv_rows = []
|
||||
block_name = title_block_info.get('block_name', '')
|
||||
|
||||
for attr in title_block_info.get('all_attributes', []):
|
||||
row = {
|
||||
'block_name': block_name,
|
||||
'attr_prompt': attr.get('prompt', '') or '',
|
||||
'attr_text': attr.get('text', '') or '',
|
||||
'attr_tag': attr.get('tag', '') or '',
|
||||
'attr_insert_x': attr.get('insert_x', '') or '',
|
||||
'attr_insert_y': attr.get('insert_y', '') or '',
|
||||
'attr_height': attr.get('height', '') or '',
|
||||
'attr_rotation': attr.get('rotation', '') or '',
|
||||
'attr_layer': attr.get('layer', '') or '',
|
||||
'attr_style': attr.get('style', '') or '',
|
||||
'entity_handle': attr.get('entity_handle', '') or '',
|
||||
}
|
||||
|
||||
# 바운딩 박스 정보 추가
|
||||
bbox = attr.get('bounding_box')
|
||||
if bbox:
|
||||
row.update({
|
||||
'bounding_box_min_x': bbox.get('min_x', ''),
|
||||
'bounding_box_min_y': bbox.get('min_y', ''),
|
||||
'bounding_box_max_x': bbox.get('max_x', ''),
|
||||
'bounding_box_max_y': bbox.get('max_y', ''),
|
||||
'bounding_box_width': bbox.get('max_x', 0) - bbox.get('min_x', 0) if bbox.get('max_x') and bbox.get('min_x') else '',
|
||||
'bounding_box_height': bbox.get('max_y', 0) - bbox.get('min_y', 0) if bbox.get('max_y') and bbox.get('min_y') else '',
|
||||
})
|
||||
else:
|
||||
row.update({
|
||||
'bounding_box_min_x': '',
|
||||
'bounding_box_min_y': '',
|
||||
'bounding_box_max_x': '',
|
||||
'bounding_box_max_y': '',
|
||||
'bounding_box_width': '',
|
||||
'bounding_box_height': '',
|
||||
})
|
||||
|
||||
csv_rows.append(row)
|
||||
|
||||
# CSV 파일 저장
|
||||
with open(filepath, 'w', newline='', encoding='utf-8-sig') as csvfile:
|
||||
writer = csv.DictWriter(csvfile, fieldnames=headers)
|
||||
|
||||
# 헤더 작성
|
||||
writer.writeheader()
|
||||
|
||||
# 데이터 작성
|
||||
writer.writerows(csv_rows)
|
||||
|
||||
logger.info(f"타이틀블럭 속성 CSV 저장 완료: {filepath}")
|
||||
return filepath
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"CSV 저장 중 오류: {e}")
|
||||
return None
|
||||
|
||||
def create_attribute_table_data(
|
||||
self,
|
||||
title_block_info: Dict[str, Any]
|
||||
) -> List[Dict[str, str]]:
|
||||
"""
|
||||
UI 테이블 표시용 데이터 생성
|
||||
|
||||
Args:
|
||||
title_block_info: 타이틀블럭 정보 딕셔너리
|
||||
|
||||
Returns:
|
||||
테이블 표시용 데이터 리스트
|
||||
"""
|
||||
try:
|
||||
if not title_block_info or not title_block_info.get('all_attributes'):
|
||||
return []
|
||||
|
||||
table_data = []
|
||||
block_name = title_block_info.get('block_name', '')
|
||||
|
||||
for i, attr in enumerate(title_block_info.get('all_attributes', [])):
|
||||
# 바운딩 박스 정보 포맷팅
|
||||
bbox_str = ""
|
||||
bbox = attr.get('bounding_box')
|
||||
if bbox:
|
||||
bbox_str = f"({bbox.get('min_x', 0):.1f}, {bbox.get('min_y', 0):.1f}) - ({bbox.get('max_x', 0):.1f}, {bbox.get('max_y', 0):.1f})"
|
||||
|
||||
row = {
|
||||
'No.': str(i + 1),
|
||||
'Block Name': block_name,
|
||||
'Tag': attr.get('tag', ''),
|
||||
'Text': attr.get('text', '')[:30] + ('...' if len(attr.get('text', '')) > 30 else ''), # 텍스트 길이 제한
|
||||
'Prompt': attr.get('prompt', '') or 'N/A',
|
||||
'X': f"{attr.get('insert_x', 0):.1f}",
|
||||
'Y': f"{attr.get('insert_y', 0):.1f}",
|
||||
'Bounding Box': bbox_str or 'N/A',
|
||||
'Height': f"{attr.get('height', 0):.1f}",
|
||||
'Layer': attr.get('layer', ''),
|
||||
}
|
||||
|
||||
table_data.append(row)
|
||||
|
||||
return table_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"테이블 데이터 생성 중 오류: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def main():
|
||||
"""테스트용 메인 함수"""
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
# 테스트 데이터
|
||||
test_title_block = {
|
||||
'block_name': 'TEST_TITLE_BLOCK',
|
||||
'all_attributes': [
|
||||
{
|
||||
'tag': 'DRAWING_NAME',
|
||||
'text': '테스트 도면',
|
||||
'prompt': '도면명을 입력하세요',
|
||||
'insert_x': 100.0,
|
||||
'insert_y': 200.0,
|
||||
'height': 5.0,
|
||||
'rotation': 0.0,
|
||||
'layer': '0',
|
||||
'style': 'Standard',
|
||||
'entity_handle': 'ABC123',
|
||||
'bounding_box': {
|
||||
'min_x': 100.0,
|
||||
'min_y': 200.0,
|
||||
'max_x': 180.0,
|
||||
'max_y': 210.0
|
||||
}
|
||||
},
|
||||
{
|
||||
'tag': 'DRAWING_NUMBER',
|
||||
'text': 'TEST-001',
|
||||
'prompt': '도면번호를 입력하세요',
|
||||
'insert_x': 100.0,
|
||||
'insert_y': 190.0,
|
||||
'height': 4.0,
|
||||
'rotation': 0.0,
|
||||
'layer': '0',
|
||||
'style': 'Standard',
|
||||
'entity_handle': 'DEF456',
|
||||
'bounding_box': {
|
||||
'min_x': 100.0,
|
||||
'min_y': 190.0,
|
||||
'max_x': 150.0,
|
||||
'max_y': 198.0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# CSV 저장 테스트
|
||||
exporter = TitleBlockCSVExporter()
|
||||
|
||||
# 테이블 데이터 생성 테스트
|
||||
table_data = exporter.create_attribute_table_data(test_title_block)
|
||||
print("테이블 데이터:")
|
||||
for row in table_data:
|
||||
print(row)
|
||||
|
||||
# CSV 저장 테스트
|
||||
saved_path = exporter.export_title_block_attributes(test_title_block, "test_export.csv")
|
||||
if saved_path:
|
||||
print(f"\nCSV 저장 성공: {saved_path}")
|
||||
else:
|
||||
print("\nCSV 저장 실패")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
import json
|
||||
|
||||
def export_analysis_results_to_csv(data: List[Dict[str, Any]], file_path: str):
|
||||
"""
|
||||
분석 결과를 CSV 파일로 저장합니다. pdf_analysis_result 컬럼의 JSON 데이터를 평탄화합니다.
|
||||
Args:
|
||||
data: 분석 결과 딕셔너리 리스트
|
||||
file_path: 저장할 CSV 파일 경로
|
||||
"""
|
||||
if not data:
|
||||
logger.warning("내보낼 데이터가 없습니다.")
|
||||
return
|
||||
|
||||
all_keys = set()
|
||||
processed_data = []
|
||||
|
||||
for row in data:
|
||||
new_row = row.copy()
|
||||
if 'pdf_analysis_result' in new_row and new_row['pdf_analysis_result']:
|
||||
try:
|
||||
json_data = new_row['pdf_analysis_result']
|
||||
if isinstance(json_data, str):
|
||||
json_data = json.loads(json_data)
|
||||
|
||||
if isinstance(json_data, dict):
|
||||
for k, v in json_data.items():
|
||||
new_row[f"pdf_analysis_result_{k}"] = v
|
||||
del new_row['pdf_analysis_result']
|
||||
else:
|
||||
new_row['pdf_analysis_result'] = str(json_data)
|
||||
except (json.JSONDecodeError, TypeError) as e:
|
||||
logger.warning(f"pdf_analysis_result 파싱 오류: {e}, 원본 데이터 유지: {new_row['pdf_analysis_result']}")
|
||||
new_row['pdf_analysis_result'] = str(new_row['pdf_analysis_result'])
|
||||
|
||||
processed_data.append(new_row)
|
||||
all_keys.update(new_row.keys())
|
||||
|
||||
# 'pdf_analysis_result'가 평탄화된 경우 최종 키에서 제거
|
||||
if 'pdf_analysis_result' in all_keys:
|
||||
all_keys.remove('pdf_analysis_result')
|
||||
|
||||
sorted_keys = sorted(list(all_keys))
|
||||
|
||||
try:
|
||||
with open(file_path, 'w', newline='', encoding='utf-8-sig') as output_file:
|
||||
dict_writer = csv.DictWriter(output_file, sorted_keys)
|
||||
dict_writer.writeheader()
|
||||
dict_writer.writerows(processed_data)
|
||||
logger.info(f"분석 결과 CSV 저장 완료: {file_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"분석 결과 CSV 저장 중 오류: {e}")
|
||||
|
||||
871
fletimageanalysis/dxf_processor.py
Normal file
871
fletimageanalysis/dxf_processor.py
Normal file
@@ -0,0 +1,871 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
향상된 DXF 파일 처리 모듈
|
||||
ezdxf 라이브러리를 사용하여 DXF 파일에서 도곽 정보, 텍스트 엔티티 및 모든 Block Reference/Attribute Reference를 추출
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
from typing import Dict, List, Optional, Tuple, Any
|
||||
from dataclasses import dataclass, asdict, field
|
||||
|
||||
try:
|
||||
import ezdxf
|
||||
from ezdxf.document import Drawing
|
||||
from ezdxf.entities import Insert, Attrib, AttDef, Text, MText
|
||||
from ezdxf.layouts import BlockLayout, Modelspace
|
||||
from ezdxf import bbox, disassemble
|
||||
EZDXF_AVAILABLE = True
|
||||
except ImportError:
|
||||
EZDXF_AVAILABLE = False
|
||||
logging.warning("ezdxf 라이브러리가 설치되지 않았습니다. DXF 기능이 비활성화됩니다.")
|
||||
|
||||
from config import Config
|
||||
|
||||
|
||||
@dataclass
|
||||
class BoundingBox:
|
||||
"""바운딩 박스 정보를 담는 데이터 클래스"""
|
||||
min_x: float
|
||||
min_y: float
|
||||
max_x: float
|
||||
max_y: float
|
||||
|
||||
@property
|
||||
def width(self) -> float:
|
||||
return self.max_x - self.min_x
|
||||
|
||||
@property
|
||||
def height(self) -> float:
|
||||
return self.max_y - self.min_y
|
||||
|
||||
@property
|
||||
def center(self) -> Tuple[float, float]:
|
||||
return ((self.min_x + self.max_x) / 2, (self.min_y + self.max_y) / 2)
|
||||
|
||||
def merge(self, other: 'BoundingBox') -> 'BoundingBox':
|
||||
"""다른 바운딩 박스와 병합하여 가장 큰 외곽 박스 반환"""
|
||||
return BoundingBox(
|
||||
min_x=min(self.min_x, other.min_x),
|
||||
min_y=min(self.min_y, other.min_y),
|
||||
max_x=max(self.max_x, other.max_x),
|
||||
max_y=max(self.max_y, other.max_y)
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TextInfo:
|
||||
"""텍스트 엔티티 정보를 담는 데이터 클래스"""
|
||||
entity_type: str # TEXT, MTEXT, ATTRIB
|
||||
text: str
|
||||
position: Tuple[float, float, float]
|
||||
height: float
|
||||
rotation: float
|
||||
layer: str
|
||||
bounding_box: Optional[BoundingBox] = None
|
||||
entity_handle: Optional[str] = None
|
||||
style: Optional[str] = None
|
||||
color: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class AttributeInfo:
|
||||
"""속성 정보를 담는 데이터 클래스 - 모든 DXF 속성 포함"""
|
||||
tag: str
|
||||
text: str
|
||||
position: Tuple[float, float, float] # insert point (x, y, z)
|
||||
height: float
|
||||
width: float
|
||||
rotation: float
|
||||
layer: str
|
||||
bounding_box: Optional[BoundingBox] = None
|
||||
|
||||
# 추가 DXF 속성들
|
||||
prompt: Optional[str] = None
|
||||
style: Optional[str] = None
|
||||
invisible: bool = False
|
||||
const: bool = False
|
||||
verify: bool = False
|
||||
preset: bool = False
|
||||
align_point: Optional[Tuple[float, float, float]] = None
|
||||
halign: int = 0
|
||||
valign: int = 0
|
||||
text_generation_flag: int = 0
|
||||
oblique_angle: float = 0.0
|
||||
width_factor: float = 1.0
|
||||
color: Optional[int] = None
|
||||
linetype: Optional[str] = None
|
||||
lineweight: Optional[int] = None
|
||||
|
||||
# 좌표 정보
|
||||
insert_x: float = 0.0
|
||||
insert_y: float = 0.0
|
||||
insert_z: float = 0.0
|
||||
|
||||
# 계산된 정보
|
||||
estimated_width: float = 0.0
|
||||
entity_handle: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class BlockInfo:
|
||||
"""블록 정보를 담는 데이터 클래스"""
|
||||
name: str
|
||||
position: Tuple[float, float, float]
|
||||
scale: Tuple[float, float, float]
|
||||
rotation: float
|
||||
layer: str
|
||||
attributes: List[AttributeInfo]
|
||||
bounding_box: Optional[BoundingBox] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class TitleBlockInfo:
|
||||
"""도곽 정보를 담는 데이터 클래스"""
|
||||
drawing_name: Optional[str] = None
|
||||
drawing_number: Optional[str] = None
|
||||
construction_field: Optional[str] = None
|
||||
construction_stage: Optional[str] = None
|
||||
scale: Optional[str] = None
|
||||
project_name: Optional[str] = None
|
||||
designer: Optional[str] = None
|
||||
date: Optional[str] = None
|
||||
revision: Optional[str] = None
|
||||
location: Optional[str] = None
|
||||
bounding_box: Optional[BoundingBox] = None
|
||||
block_name: Optional[str] = None
|
||||
|
||||
# 모든 attributes 정보 저장
|
||||
all_attributes: List[AttributeInfo] = field(default_factory=list)
|
||||
attributes_count: int = 0
|
||||
|
||||
# 추가 메타데이터
|
||||
block_position: Optional[Tuple[float, float, float]] = None
|
||||
block_scale: Optional[Tuple[float, float, float]] = None
|
||||
block_rotation: float = 0.0
|
||||
block_layer: Optional[str] = None
|
||||
|
||||
def __post_init__(self):
|
||||
"""초기화 후 처리"""
|
||||
self.attributes_count = len(self.all_attributes)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ComprehensiveExtractionResult:
|
||||
"""종합적인 추출 결과를 담는 데이터 클래스"""
|
||||
text_entities: List[TextInfo] = field(default_factory=list)
|
||||
all_block_references: List[BlockInfo] = field(default_factory=list)
|
||||
title_block: Optional[TitleBlockInfo] = None
|
||||
overall_bounding_box: Optional[BoundingBox] = None
|
||||
summary: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
class EnhancedDXFProcessor:
|
||||
"""향상된 DXF 파일 처리 클래스"""
|
||||
|
||||
# 도곽 식별을 위한 키워드 정의
|
||||
TITLE_BLOCK_KEYWORDS = {
|
||||
'건설분야': ['construction_field', 'field', '분야', '공사', 'category'],
|
||||
'건설단계': ['construction_stage', 'stage', '단계', 'phase'],
|
||||
'도면명': ['drawing_name', 'title', '제목', 'name', '명'],
|
||||
'축척': ['scale', '축척', 'ratio', '비율'],
|
||||
'도면번호': ['drawing_number', 'number', '번호', 'no', 'dwg'],
|
||||
'설계자': ['designer', '설계', 'design', 'drawn'],
|
||||
'프로젝트': ['project', '사업', '공사명'],
|
||||
'날짜': ['date', '일자', '작성일'],
|
||||
'리비전': ['revision', 'rev', '개정'],
|
||||
'위치': ['location', '위치', '지역']
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
"""DXF 처리기 초기화"""
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
if not EZDXF_AVAILABLE:
|
||||
raise ImportError("ezdxf 라이브러리가 필요합니다. 'pip install ezdxf'로 설치하세요.")
|
||||
|
||||
def validate_dxf_file(self, file_path: str) -> bool:
|
||||
"""DXF 파일 유효성 검사"""
|
||||
try:
|
||||
if not os.path.exists(file_path):
|
||||
self.logger.error(f"파일이 존재하지 않습니다: {file_path}")
|
||||
return False
|
||||
|
||||
if not file_path.lower().endswith('.dxf'):
|
||||
self.logger.error(f"DXF 파일이 아닙니다: {file_path}")
|
||||
return False
|
||||
|
||||
# ezdxf로 파일 읽기 시도
|
||||
doc = ezdxf.readfile(file_path)
|
||||
if doc is None:
|
||||
return False
|
||||
|
||||
self.logger.info(f"DXF 파일 유효성 검사 성공: {file_path}")
|
||||
return True
|
||||
|
||||
except ezdxf.DXFStructureError as e:
|
||||
self.logger.error(f"DXF 구조 오류: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"DXF 파일 검증 중 오류: {e}")
|
||||
return False
|
||||
|
||||
def load_dxf_document(self, file_path: str) -> Optional[Drawing]:
|
||||
"""DXF 문서 로드"""
|
||||
try:
|
||||
doc = ezdxf.readfile(file_path)
|
||||
self.logger.info(f"DXF 문서 로드 성공: {file_path}")
|
||||
return doc
|
||||
except Exception as e:
|
||||
self.logger.error(f"DXF 문서 로드 실패: {e}")
|
||||
return None
|
||||
|
||||
def _is_empty_text(self, text: str) -> bool:
|
||||
"""텍스트가 비어있는지 확인 (공백 문자만 있거나 완전히 비어있는 경우)"""
|
||||
return not text or text.strip() == ""
|
||||
|
||||
def calculate_comprehensive_bounding_box(self, doc: Drawing) -> Optional[BoundingBox]:
|
||||
"""전체 문서의 종합적인 바운딩 박스 계산 (ezdxf.bbox 사용)"""
|
||||
try:
|
||||
msp = doc.modelspace()
|
||||
|
||||
# ezdxf의 bbox 모듈을 사용하여 전체 바운딩 박스 계산
|
||||
cache = bbox.Cache()
|
||||
overall_bbox = bbox.extents(msp, cache=cache)
|
||||
|
||||
if overall_bbox:
|
||||
self.logger.info(f"전체 바운딩 박스: {overall_bbox}")
|
||||
return BoundingBox(
|
||||
min_x=overall_bbox.extmin.x,
|
||||
min_y=overall_bbox.extmin.y,
|
||||
max_x=overall_bbox.extmax.x,
|
||||
max_y=overall_bbox.extmax.y
|
||||
)
|
||||
else:
|
||||
self.logger.warning("바운딩 박스 계산 실패")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
self.logger.warning(f"바운딩 박스 계산 중 오류: {e}")
|
||||
return None
|
||||
|
||||
def extract_all_text_entities(self, doc: Drawing) -> List[TextInfo]:
|
||||
"""모든 텍스트 엔티티 추출 (TEXT, MTEXT, DBTEXT)"""
|
||||
text_entities = []
|
||||
|
||||
try:
|
||||
msp = doc.modelspace()
|
||||
|
||||
# TEXT 엔티티 추출
|
||||
for text_entity in msp.query('TEXT'):
|
||||
text_content = getattr(text_entity.dxf, 'text', '')
|
||||
if not self._is_empty_text(text_content):
|
||||
text_info = self._extract_text_info(text_entity, 'TEXT')
|
||||
if text_info:
|
||||
text_entities.append(text_info)
|
||||
|
||||
# MTEXT 엔티티 추출
|
||||
for mtext_entity in msp.query('MTEXT'):
|
||||
# MTEXT는 .text 속성 사용
|
||||
text_content = getattr(mtext_entity, 'text', '') or getattr(mtext_entity.dxf, 'text', '')
|
||||
if not self._is_empty_text(text_content):
|
||||
text_info = self._extract_text_info(mtext_entity, 'MTEXT')
|
||||
if text_info:
|
||||
text_entities.append(text_info)
|
||||
|
||||
# ATTRIB 엔티티 추출 (블록 외부의 독립적인 속성)
|
||||
for attrib_entity in msp.query('ATTRIB'):
|
||||
text_content = getattr(attrib_entity.dxf, 'text', '')
|
||||
if not self._is_empty_text(text_content):
|
||||
text_info = self._extract_text_info(attrib_entity, 'ATTRIB')
|
||||
if text_info:
|
||||
text_entities.append(text_info)
|
||||
|
||||
# 페이퍼스페이스도 확인
|
||||
for layout_name in doc.layout_names_in_taborder():
|
||||
if layout_name.startswith('*'): # 모델스페이스 제외
|
||||
continue
|
||||
try:
|
||||
layout = doc.paperspace(layout_name)
|
||||
|
||||
# TEXT, MTEXT, ATTRIB 추출
|
||||
for entity_type in ['TEXT', 'MTEXT', 'ATTRIB']:
|
||||
for entity in layout.query(entity_type):
|
||||
if entity_type == 'MTEXT':
|
||||
text_content = getattr(entity, 'text', '') or getattr(entity.dxf, 'text', '')
|
||||
else:
|
||||
text_content = getattr(entity.dxf, 'text', '')
|
||||
|
||||
if not self._is_empty_text(text_content):
|
||||
text_info = self._extract_text_info(entity, entity_type)
|
||||
if text_info:
|
||||
text_entities.append(text_info)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.warning(f"레이아웃 {layout_name} 처리 중 오류: {e}")
|
||||
|
||||
self.logger.info(f"총 {len(text_entities)}개의 텍스트 엔티티를 찾았습니다.")
|
||||
return text_entities
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"텍스트 엔티티 추출 중 오류: {e}")
|
||||
return []
|
||||
|
||||
def _extract_text_info(self, entity, entity_type: str) -> Optional[TextInfo]:
|
||||
"""텍스트 엔티티에서 정보 추출"""
|
||||
try:
|
||||
# 텍스트 내용 추출
|
||||
if entity_type == 'MTEXT':
|
||||
text_content = getattr(entity, 'text', '') or getattr(entity.dxf, 'text', '')
|
||||
else:
|
||||
text_content = getattr(entity.dxf, 'text', '')
|
||||
|
||||
# 위치 정보
|
||||
insert_point = getattr(entity.dxf, 'insert', (0, 0, 0))
|
||||
position = (
|
||||
insert_point.x if hasattr(insert_point, 'x') else insert_point[0],
|
||||
insert_point.y if hasattr(insert_point, 'y') else insert_point[1],
|
||||
insert_point.z if hasattr(insert_point, 'z') else insert_point[2]
|
||||
)
|
||||
|
||||
# 기본 속성
|
||||
height = getattr(entity.dxf, 'height', 1.0)
|
||||
rotation = getattr(entity.dxf, 'rotation', 0.0)
|
||||
layer = getattr(entity.dxf, 'layer', '0')
|
||||
entity_handle = getattr(entity.dxf, 'handle', None)
|
||||
style = getattr(entity.dxf, 'style', None)
|
||||
color = getattr(entity.dxf, 'color', None)
|
||||
|
||||
# 바운딩 박스 계산
|
||||
bounding_box = self._calculate_text_bounding_box(entity)
|
||||
|
||||
return TextInfo(
|
||||
entity_type=entity_type,
|
||||
text=text_content,
|
||||
position=position,
|
||||
height=height,
|
||||
rotation=rotation,
|
||||
layer=layer,
|
||||
bounding_box=bounding_box,
|
||||
entity_handle=entity_handle,
|
||||
style=style,
|
||||
color=color
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.warning(f"텍스트 정보 추출 중 오류: {e}")
|
||||
return None
|
||||
|
||||
def _calculate_text_bounding_box(self, entity) -> Optional[BoundingBox]:
|
||||
"""텍스트 엔티티의 바운딩 박스 계산"""
|
||||
try:
|
||||
# ezdxf bbox 모듈 사용
|
||||
entity_bbox = bbox.extents([entity])
|
||||
if entity_bbox:
|
||||
return BoundingBox(
|
||||
min_x=entity_bbox.extmin.x,
|
||||
min_y=entity_bbox.extmin.y,
|
||||
max_x=entity_bbox.extmax.x,
|
||||
max_y=entity_bbox.extmax.y
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.debug(f"바운딩 박스 계산 실패, 추정값 사용: {e}")
|
||||
|
||||
# 대안: 추정 계산
|
||||
try:
|
||||
if hasattr(entity, 'dxf'):
|
||||
insert_point = getattr(entity.dxf, 'insert', (0, 0, 0))
|
||||
height = getattr(entity.dxf, 'height', 1.0)
|
||||
|
||||
# 텍스트 내용 길이 추정
|
||||
if hasattr(entity, 'text'):
|
||||
text_content = entity.text
|
||||
elif hasattr(entity.dxf, 'text'):
|
||||
text_content = entity.dxf.text
|
||||
else:
|
||||
text_content = ""
|
||||
|
||||
# 텍스트 너비 추정 (높이의 0.6배 * 글자 수)
|
||||
estimated_width = len(text_content) * height * 0.6
|
||||
|
||||
x, y = insert_point[0], insert_point[1]
|
||||
|
||||
return BoundingBox(
|
||||
min_x=x,
|
||||
min_y=y,
|
||||
max_x=x + estimated_width,
|
||||
max_y=y + height
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.warning(f"텍스트 바운딩 박스 계산 실패: {e}")
|
||||
return None
|
||||
|
||||
def extract_all_block_references(self, doc: Drawing) -> List[BlockInfo]:
|
||||
"""모든 Block Reference 추출 (재귀적으로 중첩된 블록도 포함)"""
|
||||
block_refs = []
|
||||
|
||||
try:
|
||||
# 모델스페이스에서 INSERT 엔티티 찾기
|
||||
msp = doc.modelspace()
|
||||
|
||||
for insert in msp.query('INSERT'):
|
||||
block_info = self._process_block_reference(doc, insert)
|
||||
if block_info:
|
||||
block_refs.append(block_info)
|
||||
|
||||
# 페이퍼스페이스도 확인
|
||||
for layout_name in doc.layout_names_in_taborder():
|
||||
if layout_name.startswith('*'): # 모델스페이스 제외
|
||||
continue
|
||||
try:
|
||||
layout = doc.paperspace(layout_name)
|
||||
for insert in layout.query('INSERT'):
|
||||
block_info = self._process_block_reference(doc, insert)
|
||||
if block_info:
|
||||
block_refs.append(block_info)
|
||||
except Exception as e:
|
||||
self.logger.warning(f"레이아웃 {layout_name} 처리 중 오류: {e}")
|
||||
|
||||
# 블록 정의 내부도 재귀적으로 검사
|
||||
for block_layout in doc.blocks:
|
||||
if not block_layout.name.startswith('*'): # 시스템 블록 제외
|
||||
for insert in block_layout.query('INSERT'):
|
||||
block_info = self._process_block_reference(doc, insert)
|
||||
if block_info:
|
||||
block_refs.append(block_info)
|
||||
|
||||
self.logger.info(f"총 {len(block_refs)}개의 블록 참조를 찾았습니다.")
|
||||
return block_refs
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"블록 참조 추출 중 오류: {e}")
|
||||
return []
|
||||
|
||||
def _process_block_reference(self, doc: Drawing, insert: Insert) -> Optional[BlockInfo]:
|
||||
"""개별 Block Reference 처리 - ATTDEF 정보도 함께 수집"""
|
||||
try:
|
||||
# 블록 정보 추출
|
||||
block_name = insert.dxf.name
|
||||
position = (insert.dxf.insert.x, insert.dxf.insert.y, insert.dxf.insert.z)
|
||||
scale = (
|
||||
getattr(insert.dxf, 'xscale', 1.0),
|
||||
getattr(insert.dxf, 'yscale', 1.0),
|
||||
getattr(insert.dxf, 'zscale', 1.0)
|
||||
)
|
||||
rotation = getattr(insert.dxf, 'rotation', 0.0)
|
||||
layer = getattr(insert.dxf, 'layer', '0')
|
||||
|
||||
# ATTDEF 정보 수집 (프롬프트 정보 포함)
|
||||
attdef_info = {}
|
||||
try:
|
||||
block_layout = doc.blocks.get(block_name)
|
||||
if block_layout:
|
||||
for attdef in block_layout.query('ATTDEF'):
|
||||
tag = getattr(attdef.dxf, 'tag', '')
|
||||
prompt = getattr(attdef.dxf, 'prompt', '')
|
||||
if tag:
|
||||
attdef_info[tag] = {
|
||||
'prompt': prompt,
|
||||
'default_text': getattr(attdef.dxf, 'text', ''),
|
||||
'position': (attdef.dxf.insert.x, attdef.dxf.insert.y, attdef.dxf.insert.z),
|
||||
'height': getattr(attdef.dxf, 'height', 1.0),
|
||||
'style': getattr(attdef.dxf, 'style', 'Standard'),
|
||||
'invisible': getattr(attdef.dxf, 'invisible', False),
|
||||
'const': getattr(attdef.dxf, 'const', False),
|
||||
'verify': getattr(attdef.dxf, 'verify', False),
|
||||
'preset': getattr(attdef.dxf, 'preset', False)
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.debug(f"ATTDEF 정보 수집 실패: {e}")
|
||||
|
||||
# ATTRIB 속성 추출 및 ATTDEF 정보와 결합 (빈 텍스트 제외)
|
||||
attributes = []
|
||||
for attrib in insert.attribs:
|
||||
attr_info = self._extract_attribute_info(attrib)
|
||||
if attr_info and not self._is_empty_text(attr_info.text):
|
||||
# ATTDEF에서 프롬프트 정보 추가
|
||||
if attr_info.tag in attdef_info:
|
||||
attr_info.prompt = attdef_info[attr_info.tag]['prompt']
|
||||
attributes.append(attr_info)
|
||||
|
||||
# 블록 바운딩 박스 계산
|
||||
block_bbox = self._calculate_block_bounding_box(insert)
|
||||
|
||||
return BlockInfo(
|
||||
name=block_name,
|
||||
position=position,
|
||||
scale=scale,
|
||||
rotation=rotation,
|
||||
layer=layer,
|
||||
attributes=attributes,
|
||||
bounding_box=block_bbox
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.warning(f"블록 참조 처리 중 오류: {e}")
|
||||
return None
|
||||
|
||||
def _calculate_block_bounding_box(self, insert: Insert) -> Optional[BoundingBox]:
|
||||
"""블록의 바운딩 박스 계산"""
|
||||
try:
|
||||
# ezdxf bbox 모듈 사용
|
||||
block_bbox = bbox.extents([insert])
|
||||
if block_bbox:
|
||||
return BoundingBox(
|
||||
min_x=block_bbox.extmin.x,
|
||||
min_y=block_bbox.extmin.y,
|
||||
max_x=block_bbox.extmax.x,
|
||||
max_y=block_bbox.extmax.y
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.debug(f"블록 바운딩 박스 계산 실패: {e}")
|
||||
|
||||
return None
|
||||
|
||||
def _extract_attribute_info(self, attrib: Attrib) -> Optional[AttributeInfo]:
|
||||
"""Attribute Reference에서 모든 정보 추출 (빈 텍스트 포함)"""
|
||||
try:
|
||||
# 기본 속성
|
||||
tag = getattr(attrib.dxf, 'tag', '')
|
||||
text = getattr(attrib.dxf, 'text', '')
|
||||
|
||||
# 위치 정보
|
||||
insert_point = getattr(attrib.dxf, 'insert', (0, 0, 0))
|
||||
position = (insert_point.x if hasattr(insert_point, 'x') else insert_point[0],
|
||||
insert_point.y if hasattr(insert_point, 'y') else insert_point[1],
|
||||
insert_point.z if hasattr(insert_point, 'z') else insert_point[2])
|
||||
|
||||
# 텍스트 속성
|
||||
height = getattr(attrib.dxf, 'height', 1.0)
|
||||
width = getattr(attrib.dxf, 'width', 1.0)
|
||||
rotation = getattr(attrib.dxf, 'rotation', 0.0)
|
||||
|
||||
# 레이어 및 스타일
|
||||
layer = getattr(attrib.dxf, 'layer', '0')
|
||||
style = getattr(attrib.dxf, 'style', 'Standard')
|
||||
|
||||
# 속성 플래그
|
||||
invisible = getattr(attrib.dxf, 'invisible', False)
|
||||
const = getattr(attrib.dxf, 'const', False)
|
||||
verify = getattr(attrib.dxf, 'verify', False)
|
||||
preset = getattr(attrib.dxf, 'preset', False)
|
||||
|
||||
# 정렬 정보
|
||||
align_point_data = getattr(attrib.dxf, 'align_point', None)
|
||||
align_point = None
|
||||
if align_point_data:
|
||||
align_point = (align_point_data.x if hasattr(align_point_data, 'x') else align_point_data[0],
|
||||
align_point_data.y if hasattr(align_point_data, 'y') else align_point_data[1],
|
||||
align_point_data.z if hasattr(align_point_data, 'z') else align_point_data[2])
|
||||
|
||||
halign = getattr(attrib.dxf, 'halign', 0)
|
||||
valign = getattr(attrib.dxf, 'valign', 0)
|
||||
|
||||
# 텍스트 형식
|
||||
text_generation_flag = getattr(attrib.dxf, 'text_generation_flag', 0)
|
||||
oblique_angle = getattr(attrib.dxf, 'oblique_angle', 0.0)
|
||||
width_factor = getattr(attrib.dxf, 'width_factor', 1.0)
|
||||
|
||||
# 시각적 속성
|
||||
color = getattr(attrib.dxf, 'color', None)
|
||||
linetype = getattr(attrib.dxf, 'linetype', None)
|
||||
lineweight = getattr(attrib.dxf, 'lineweight', None)
|
||||
|
||||
# 엔티티 핸들
|
||||
entity_handle = getattr(attrib.dxf, 'handle', None)
|
||||
|
||||
# 텍스트 폭 추정
|
||||
estimated_width = len(text) * height * 0.6 * width_factor
|
||||
|
||||
# 바운딩 박스 계산
|
||||
bounding_box = self._calculate_text_bounding_box(attrib)
|
||||
|
||||
return AttributeInfo(
|
||||
tag=tag,
|
||||
text=text,
|
||||
position=position,
|
||||
height=height,
|
||||
width=width,
|
||||
rotation=rotation,
|
||||
layer=layer,
|
||||
bounding_box=bounding_box,
|
||||
prompt=None, # 나중에 ATTDEF에서 설정
|
||||
style=style,
|
||||
invisible=invisible,
|
||||
const=const,
|
||||
verify=verify,
|
||||
preset=preset,
|
||||
align_point=align_point,
|
||||
halign=halign,
|
||||
valign=valign,
|
||||
text_generation_flag=text_generation_flag,
|
||||
oblique_angle=oblique_angle,
|
||||
width_factor=width_factor,
|
||||
color=color,
|
||||
linetype=linetype,
|
||||
lineweight=lineweight,
|
||||
insert_x=position[0],
|
||||
insert_y=position[1],
|
||||
insert_z=position[2],
|
||||
estimated_width=estimated_width,
|
||||
entity_handle=entity_handle
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.warning(f"속성 정보 추출 중 오류: {e}")
|
||||
return None
|
||||
|
||||
def identify_title_block(self, block_refs: List[BlockInfo]) -> Optional[TitleBlockInfo]:
|
||||
"""블록 참조들 중에서 도곽을 식별하고 정보 추출"""
|
||||
title_block_candidates = []
|
||||
|
||||
for block_ref in block_refs:
|
||||
# 도곽 키워드를 포함한 속성이 있는지 확인
|
||||
keyword_matches = 0
|
||||
|
||||
for attr in block_ref.attributes:
|
||||
for keyword_group in self.TITLE_BLOCK_KEYWORDS.keys():
|
||||
if self._contains_keyword(attr.tag, keyword_group) or \
|
||||
self._contains_keyword(attr.text, keyword_group):
|
||||
keyword_matches += 1
|
||||
break
|
||||
|
||||
# 충분한 키워드가 매칭되면 도곽 후보로 추가
|
||||
if keyword_matches >= 2: # 최소 2개 이상의 키워드 매칭
|
||||
title_block_candidates.append((block_ref, keyword_matches))
|
||||
|
||||
if not title_block_candidates:
|
||||
self.logger.warning("도곽 블록을 찾을 수 없습니다.")
|
||||
return None
|
||||
|
||||
# 가장 많은 키워드를 포함한 블록을 도곽으로 선택
|
||||
title_block_candidates.sort(key=lambda x: x[1], reverse=True)
|
||||
best_candidate = title_block_candidates[0][0]
|
||||
|
||||
self.logger.info(f"도곽 블록 발견: {best_candidate.name} (키워드 매칭: {title_block_candidates[0][1]})")
|
||||
|
||||
return self._extract_title_block_info(best_candidate)
|
||||
|
||||
def _contains_keyword(self, text: str, keyword_group: str) -> bool:
|
||||
"""텍스트에 특정 키워드 그룹의 단어가 포함되어 있는지 확인"""
|
||||
if not text:
|
||||
return False
|
||||
|
||||
text_lower = text.lower()
|
||||
keywords = self.TITLE_BLOCK_KEYWORDS.get(keyword_group, [])
|
||||
|
||||
return any(keyword.lower() in text_lower for keyword in keywords)
|
||||
|
||||
def _extract_title_block_info(self, block_ref: BlockInfo) -> TitleBlockInfo:
|
||||
"""도곽 블록에서 상세 정보 추출"""
|
||||
# TitleBlockInfo 객체 생성
|
||||
title_block = TitleBlockInfo(
|
||||
block_name=block_ref.name,
|
||||
all_attributes=block_ref.attributes.copy(),
|
||||
block_position=block_ref.position,
|
||||
block_scale=block_ref.scale,
|
||||
block_rotation=block_ref.rotation,
|
||||
block_layer=block_ref.layer
|
||||
)
|
||||
|
||||
# 속성들을 분석하여 도곽 정보 매핑
|
||||
for attr in block_ref.attributes:
|
||||
text_value = attr.text.strip()
|
||||
|
||||
if not text_value:
|
||||
continue
|
||||
|
||||
# 각 키워드 그룹별로 매칭 시도
|
||||
if self._contains_keyword(attr.tag, '도면명') or self._contains_keyword(attr.text, '도면명'):
|
||||
title_block.drawing_name = text_value
|
||||
elif self._contains_keyword(attr.tag, '도면번호') or self._contains_keyword(attr.text, '도면번호'):
|
||||
title_block.drawing_number = text_value
|
||||
elif self._contains_keyword(attr.tag, '건설분야') or self._contains_keyword(attr.text, '건설분야'):
|
||||
title_block.construction_field = text_value
|
||||
elif self._contains_keyword(attr.tag, '건설단계') or self._contains_keyword(attr.text, '건설단계'):
|
||||
title_block.construction_stage = text_value
|
||||
elif self._contains_keyword(attr.tag, '축척') or self._contains_keyword(attr.text, '축척'):
|
||||
title_block.scale = text_value
|
||||
elif self._contains_keyword(attr.tag, '설계자') or self._contains_keyword(attr.text, '설계자'):
|
||||
title_block.designer = text_value
|
||||
elif self._contains_keyword(attr.tag, '프로젝트') or self._contains_keyword(attr.text, '프로젝트'):
|
||||
title_block.project_name = text_value
|
||||
elif self._contains_keyword(attr.tag, '날짜') or self._contains_keyword(attr.text, '날짜'):
|
||||
title_block.date = text_value
|
||||
elif self._contains_keyword(attr.tag, '리비전') or self._contains_keyword(attr.text, '리비전'):
|
||||
title_block.revision = text_value
|
||||
elif self._contains_keyword(attr.tag, '위치') or self._contains_keyword(attr.text, '위치'):
|
||||
title_block.location = text_value
|
||||
|
||||
# 도곽 바운딩 박스는 블록의 바운딩 박스 사용
|
||||
title_block.bounding_box = block_ref.bounding_box
|
||||
|
||||
# 속성 개수 업데이트
|
||||
title_block.attributes_count = len(title_block.all_attributes)
|
||||
|
||||
self.logger.info(f"도곽 '{block_ref.name}'에서 {title_block.attributes_count}개의 속성 추출 완료")
|
||||
|
||||
return title_block
|
||||
|
||||
def process_dxf_file_comprehensive(self, file_path: str) -> Dict[str, Any]:
|
||||
"""DXF 파일 종합적인 처리"""
|
||||
result = {
|
||||
'success': False,
|
||||
'error': None,
|
||||
'file_path': file_path,
|
||||
'comprehensive_result': None,
|
||||
'summary': {}
|
||||
}
|
||||
|
||||
try:
|
||||
# 파일 유효성 검사
|
||||
if not self.validate_dxf_file(file_path):
|
||||
result['error'] = "유효하지 않은 DXF 파일입니다."
|
||||
return result
|
||||
|
||||
# DXF 문서 로드
|
||||
doc = self.load_dxf_document(file_path)
|
||||
if not doc:
|
||||
result['error'] = "DXF 문서를 로드할 수 없습니다."
|
||||
return result
|
||||
|
||||
# 종합적인 추출 시작
|
||||
comprehensive_result = ComprehensiveExtractionResult()
|
||||
|
||||
# 1. 모든 텍스트 엔티티 추출
|
||||
self.logger.info("텍스트 엔티티 추출 중...")
|
||||
comprehensive_result.text_entities = self.extract_all_text_entities(doc)
|
||||
|
||||
# 2. 모든 블록 참조 추출
|
||||
self.logger.info("블록 참조 추출 중...")
|
||||
comprehensive_result.all_block_references = self.extract_all_block_references(doc)
|
||||
|
||||
# 3. 도곽 정보 추출
|
||||
self.logger.info("도곽 정보 추출 중...")
|
||||
comprehensive_result.title_block = self.identify_title_block(comprehensive_result.all_block_references)
|
||||
|
||||
# 4. 전체 바운딩 박스 계산
|
||||
self.logger.info("전체 바운딩 박스 계산 중...")
|
||||
comprehensive_result.overall_bounding_box = self.calculate_comprehensive_bounding_box(doc)
|
||||
|
||||
# 5. 요약 정보 생성
|
||||
comprehensive_result.summary = {
|
||||
'total_text_entities': len(comprehensive_result.text_entities),
|
||||
'total_block_references': len(comprehensive_result.all_block_references),
|
||||
'title_block_found': comprehensive_result.title_block is not None,
|
||||
'title_block_name': comprehensive_result.title_block.block_name if comprehensive_result.title_block else None,
|
||||
'total_attributes': sum(len(br.attributes) for br in comprehensive_result.all_block_references),
|
||||
'non_empty_attributes': sum(len([attr for attr in br.attributes if not self._is_empty_text(attr.text)])
|
||||
for br in comprehensive_result.all_block_references),
|
||||
'overall_bounding_box': comprehensive_result.overall_bounding_box.__dict__ if comprehensive_result.overall_bounding_box else None
|
||||
}
|
||||
|
||||
# 결과 저장
|
||||
result['comprehensive_result'] = asdict(comprehensive_result)
|
||||
result['summary'] = comprehensive_result.summary
|
||||
result['success'] = True
|
||||
|
||||
self.logger.info(f"DXF 파일 종합 처리 완료: {file_path}")
|
||||
self.logger.info(f"추출 요약: 텍스트 엔티티 {comprehensive_result.summary['total_text_entities']}개, "
|
||||
f"블록 참조 {comprehensive_result.summary['total_block_references']}개, "
|
||||
f"비어있지 않은 속성 {comprehensive_result.summary['non_empty_attributes']}개")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"DXF 파일 처리 중 오류: {e}")
|
||||
result['error'] = str(e)
|
||||
|
||||
return result
|
||||
|
||||
def save_analysis_result(self, result: Dict[str, Any], output_file: str) -> bool:
|
||||
"""분석 결과를 JSON 파일로 저장"""
|
||||
try:
|
||||
os.makedirs(Config.RESULTS_FOLDER, exist_ok=True)
|
||||
output_path = os.path.join(Config.RESULTS_FOLDER, output_file)
|
||||
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(result, f, ensure_ascii=False, indent=2, default=str)
|
||||
|
||||
self.logger.info(f"분석 결과 저장 완료: {output_path}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"분석 결과 저장 실패: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# 기존 클래스명과의 호환성을 위한 별칭
|
||||
DXFProcessor = EnhancedDXFProcessor
|
||||
|
||||
|
||||
def main():
|
||||
"""테스트용 메인 함수"""
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
if not EZDXF_AVAILABLE:
|
||||
print("ezdxf 라이브러리가 설치되지 않았습니다.")
|
||||
return
|
||||
|
||||
processor = EnhancedDXFProcessor()
|
||||
|
||||
# 테스트 파일 경로 (실제 파일 경로로 변경 필요)
|
||||
test_file = "test_drawing.dxf"
|
||||
|
||||
if os.path.exists(test_file):
|
||||
result = processor.process_dxf_file_comprehensive(test_file)
|
||||
|
||||
if result['success']:
|
||||
print("DXF 파일 종합 처리 성공!")
|
||||
summary = result['summary']
|
||||
print(f"텍스트 엔티티: {summary['total_text_entities']}")
|
||||
print(f"블록 참조: {summary['total_block_references']}")
|
||||
print(f"도곽 발견: {summary['title_block_found']}")
|
||||
print(f"비어있지 않은 속성: {summary['non_empty_attributes']}")
|
||||
|
||||
if summary['overall_bounding_box']:
|
||||
bbox_info = summary['overall_bounding_box']
|
||||
print(f"전체 바운딩 박스: ({bbox_info['min_x']:.2f}, {bbox_info['min_y']:.2f}) ~ "
|
||||
f"({bbox_info['max_x']:.2f}, {bbox_info['max_y']:.2f})")
|
||||
else:
|
||||
print(f"처리 실패: {result['error']}")
|
||||
else:
|
||||
print(f"테스트 파일을 찾을 수 없습니다: {test_file}")
|
||||
|
||||
|
||||
def process_dxf_file(self, file_path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
기존 코드와의 호환성을 위한 메서드
|
||||
process_dxf_file_comprehensive를 호출하고 기존 형식으로 변환
|
||||
"""
|
||||
try:
|
||||
# 새로운 종합 처리 메서드 호출
|
||||
comprehensive_result = self.process_dxf_file_comprehensive(file_path)
|
||||
|
||||
if not comprehensive_result['success']:
|
||||
return comprehensive_result
|
||||
|
||||
# 기존 형식으로 변환
|
||||
comp_data = comprehensive_result['comprehensive_result']
|
||||
|
||||
# 기존 형식으로 데이터 재구성
|
||||
result = {
|
||||
'success': True,
|
||||
'error': None,
|
||||
'file_path': file_path,
|
||||
'title_block': comp_data.get('title_block'),
|
||||
'block_references': comp_data.get('all_block_references', []),
|
||||
'summary': comp_data.get('summary', {})
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"DXF 파일 처리 중 오류: {e}")
|
||||
return {
|
||||
'success': False,
|
||||
'error': str(e),
|
||||
'file_path': file_path,
|
||||
'title_block': None,
|
||||
'block_references': [],
|
||||
'summary': {}
|
||||
}
|
||||
271
fletimageanalysis/gemini_analyzer.py
Normal file
271
fletimageanalysis/gemini_analyzer.py
Normal file
@@ -0,0 +1,271 @@
|
||||
"""
|
||||
Gemini API 연동 모듈 (좌표 추출 기능 추가)
|
||||
Google Gemini API를 사용하여 이미지와 텍스트 좌표를 함께 분석합니다.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import logging
|
||||
import json
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
from typing import Optional, Dict, Any, List
|
||||
|
||||
from config import Config
|
||||
|
||||
# 로깅 설정
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# --- 새로운 스키마 정의 ---
|
||||
|
||||
# 좌표를 포함하는 값을 위한 재사용 가능한 스키마
|
||||
ValueWithCoords = types.Schema(
|
||||
type=types.Type.OBJECT,
|
||||
properties={
|
||||
"value": types.Schema(type=types.Type.STRING, description="추출된 텍스트 값"),
|
||||
"x": types.Schema(type=types.Type.NUMBER, description="텍스트의 시작 x 좌표"),
|
||||
"y": types.Schema(type=types.Type.NUMBER, description="텍스트의 시작 y 좌표"),
|
||||
},
|
||||
required=["value", "x", "y"]
|
||||
)
|
||||
|
||||
# 모든 필드가 ValueWithCoords를 사용하도록 스키마 업데이트
|
||||
SCHEMA_EXPRESSWAY = types.Schema(
|
||||
type=types.Type.OBJECT,
|
||||
properties={
|
||||
"도면명_line0": ValueWithCoords,
|
||||
"도면명_line1": ValueWithCoords,
|
||||
"도면명_line2": ValueWithCoords,
|
||||
"편철번호": ValueWithCoords,
|
||||
"도면번호": ValueWithCoords,
|
||||
"Main_Title": ValueWithCoords,
|
||||
"Sub_Title": ValueWithCoords,
|
||||
"수평_도면_축척": ValueWithCoords,
|
||||
"수직_도면_축척": ValueWithCoords,
|
||||
"적용표준버전": ValueWithCoords,
|
||||
"사업명_top": ValueWithCoords,
|
||||
"사업명_bot": ValueWithCoords,
|
||||
"시설_공구": ValueWithCoords,
|
||||
"설계공구_공구명": ValueWithCoords,
|
||||
"설계공구_범위": ValueWithCoords,
|
||||
"시공공구_공구명": ValueWithCoords,
|
||||
"시공공구_범위": ValueWithCoords,
|
||||
"건설분야": ValueWithCoords,
|
||||
"건설단계": ValueWithCoords,
|
||||
"설계사": ValueWithCoords,
|
||||
"시공사": ValueWithCoords,
|
||||
"노선이정": ValueWithCoords,
|
||||
"개정번호_1": ValueWithCoords,
|
||||
"개정날짜_1": ValueWithCoords,
|
||||
"개정내용_1": ValueWithCoords,
|
||||
"작성자_1": ValueWithCoords,
|
||||
"검토자_1": ValueWithCoords,
|
||||
"확인자_1": ValueWithCoords
|
||||
},
|
||||
)
|
||||
|
||||
SCHEMA_TRANSPORTATION = types.Schema(
|
||||
type=types.Type.OBJECT,
|
||||
properties={
|
||||
"도면명": ValueWithCoords,
|
||||
"편철번호": ValueWithCoords,
|
||||
"도면번호": ValueWithCoords,
|
||||
"Main Title": ValueWithCoords,
|
||||
"Sub Title": ValueWithCoords,
|
||||
"수평축척": ValueWithCoords,
|
||||
"수직축척": ValueWithCoords,
|
||||
"적용표준": ValueWithCoords,
|
||||
"사업명": ValueWithCoords,
|
||||
"시설_공구": ValueWithCoords,
|
||||
"건설분야": ValueWithCoords,
|
||||
"건설단계": ValueWithCoords,
|
||||
"개정차수": ValueWithCoords,
|
||||
"개정일자": ValueWithCoords,
|
||||
"과업책임자": ValueWithCoords,
|
||||
"분야별책임자": ValueWithCoords,
|
||||
"설계자": ValueWithCoords,
|
||||
"위치정보": ValueWithCoords
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class GeminiAnalyzer:
|
||||
"""Gemini API 이미지 및 텍스트 분석 클래스"""
|
||||
|
||||
def __init__(self, api_key: Optional[str] = None, model: Optional[str] = None):
|
||||
self.api_key = api_key or Config.GEMINI_API_KEY
|
||||
self.model = model or Config.GEMINI_MODEL
|
||||
self.default_prompt = Config.DEFAULT_PROMPT
|
||||
|
||||
if not self.api_key:
|
||||
raise ValueError("Gemini API 키가 설정되지 않았습니다.")
|
||||
|
||||
try:
|
||||
self.client = genai.Client(api_key=self.api_key)
|
||||
logger.info(f"Gemini 클라이언트 초기화 완료 (모델: {self.model})")
|
||||
except Exception as e:
|
||||
logger.error(f"Gemini 클라이언트 초기화 실패: {e}")
|
||||
raise
|
||||
|
||||
def _get_schema(self, organization_type: str) -> types.Schema:
|
||||
"""조직 유형에 따른 스키마를 반환합니다."""
|
||||
return SCHEMA_EXPRESSWAY if organization_type == "한국도로공사" else SCHEMA_TRANSPORTATION
|
||||
|
||||
def analyze_pdf_page(
|
||||
self,
|
||||
base64_data: str,
|
||||
text_blocks: List[Dict[str, Any]],
|
||||
prompt: Optional[str] = None,
|
||||
mime_type: str = "image/png",
|
||||
organization_type: str = "transportation"
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Base64 이미지와 추출된 텍스트 좌표를 함께 분석합니다.
|
||||
|
||||
Args:
|
||||
base64_data: Base64로 인코딩된 이미지 데이터.
|
||||
text_blocks: PDF에서 추출된 텍스트와 좌표 정보 리스트.
|
||||
prompt: 분석 요청 텍스트 (None인 경우 기본값 사용).
|
||||
mime_type: 이미지 MIME 타입.
|
||||
organization_type: 조직 유형 ("transportation" 또는 "expressway").
|
||||
|
||||
Returns:
|
||||
분석 결과 JSON 문자열 또는 None (실패 시).
|
||||
"""
|
||||
try:
|
||||
# 텍스트 블록 정보를 JSON 문자열로 변환하여 프롬프트에 추가
|
||||
text_context = "\n".join([
|
||||
f"- text: '{block['text']}', bbox: ({block['bbox'][0]:.0f}, {block['bbox'][1]:.0f})"
|
||||
for block in text_blocks
|
||||
])
|
||||
|
||||
analysis_prompt = (
|
||||
(prompt or self.default_prompt) +
|
||||
"\n\n--- 추출된 텍스트와 좌표 정보 ---\n" +
|
||||
text_context +
|
||||
"\n\n--- 지시사항 ---\n"
|
||||
"위 텍스트와 좌표 정보를 바탕으로, 이미지의 내용을 분석하여 JSON 스키마를 채워주세요."
|
||||
"각 필드에 해당하는 텍스트를 찾고, 해당 텍스트의 'value'와 시작 'x', 'y' 좌표를 JSON에 기입하세요."
|
||||
"top은 주로 문서 상단에, bot은 주로 문서 하단입니다. "
|
||||
"특히 설계공구과 시공공구의 경우, 여러 개의 컬럼(공구명, 범위)으로 나누어진 경우가 있습니다. "
|
||||
"설계공구 | 설계공구_공구명 | 설계공구_범위"
|
||||
"시공공구 | 시공공구_공구명 | 시공공구_범위"
|
||||
"와 같은 구조입니다. 구분자 색은 항상 black이 아닐 수 있음에 주의하세요"
|
||||
"Given an image with a row like '설계공구 | 제2-1공구 | 12780.00-15860.00', the output should be:"
|
||||
"설계공구_공구명: '제2-1공구'"
|
||||
"설계공구_범위: '12780.00-15860.00'"
|
||||
"도면명_line{n}은 도면명에 해당하는 값 여러 줄을 위에서부터 0, 1, 2, ...라고 정의합니다."
|
||||
"도면명에 해당하는 값이 두 줄인 경우 line0이 생략된 경우입니다. 따라서 두 줄인 경우 line0의 값은 비어있어야 하고 line1, line2의 값은 채워져 있어야 합니다."
|
||||
"{ }_Title은 중앙 상단의 비교적 큰 폰트입니다. "
|
||||
"사업명_top에 해당하는 텍스트 아랫줄은 '시설_공구' 항목입니다."
|
||||
"개정번호_{n}의 n은 삼각형 내부의 숫자입니다."
|
||||
"각각의 컬럼에 해당하는 값을 개별적으로 추출해주세요."
|
||||
"해당하는 값이 없으면 빈 문자열을 사용하세요."
|
||||
)
|
||||
|
||||
contents = [
|
||||
types.Content(
|
||||
role="user",
|
||||
parts=[
|
||||
types.Part.from_bytes(
|
||||
mime_type=mime_type,
|
||||
data=base64.b64decode(base64_data),
|
||||
),
|
||||
types.Part.from_text(text=analysis_prompt),
|
||||
],
|
||||
)
|
||||
]
|
||||
|
||||
selected_schema = self._get_schema(organization_type)
|
||||
|
||||
generate_content_config = types.GenerateContentConfig(
|
||||
temperature=0,
|
||||
top_p=0.05,
|
||||
response_mime_type="application/json",
|
||||
response_schema=selected_schema
|
||||
)
|
||||
|
||||
logger.info("Gemini API 분석 요청 시작 (텍스트 좌표 포함)...")
|
||||
|
||||
response = self.client.models.generate_content(
|
||||
model=self.model,
|
||||
contents=contents,
|
||||
config=generate_content_config,
|
||||
)
|
||||
|
||||
if response and hasattr(response, 'text'):
|
||||
result = response.text
|
||||
# JSON 응답을 파싱하여 다시 직렬화 (일관된 포맷팅)
|
||||
parsed_json = json.loads(result)
|
||||
|
||||
# 디버깅: Gemini 응답 내용 로깅
|
||||
logger.info(f"=== Gemini 응답 디버깅 ===")
|
||||
logger.info(f"조직 유형: {organization_type}")
|
||||
logger.info(f"응답 필드 수: {len(parsed_json) if isinstance(parsed_json, dict) else 'N/A'}")
|
||||
|
||||
if isinstance(parsed_json, dict):
|
||||
# 새로운 필드들이 응답에 포함되었는지 확인
|
||||
new_fields = ["설계공구_Station_col1", "설계공구_Station_col2", "시공공구_Station_col1", "시공공구_Station_col2"]
|
||||
old_fields = ["설계공구_Station", "시공공구_Station"]
|
||||
|
||||
logger.info("=== 새 필드 확인 ===")
|
||||
for field in new_fields:
|
||||
if field in parsed_json:
|
||||
field_data = parsed_json[field]
|
||||
if isinstance(field_data, dict) and field_data.get('value'):
|
||||
logger.info(f"✅ {field}: '{field_data.get('value')}' at ({field_data.get('x', 'N/A')}, {field_data.get('y', 'N/A')})")
|
||||
else:
|
||||
logger.info(f"⚠️ {field}: 빈 값 또는 잘못된 형식 - {field_data}")
|
||||
else:
|
||||
logger.info(f"❌ {field}: 응답에 없음")
|
||||
|
||||
logger.info("=== 기존 필드 확인 ===")
|
||||
for field in old_fields:
|
||||
if field in parsed_json:
|
||||
field_data = parsed_json[field]
|
||||
if isinstance(field_data, dict) and field_data.get('value'):
|
||||
logger.info(f"⚠️ {field}: '{field_data.get('value')}' (기존 필드가 여전히 존재)")
|
||||
else:
|
||||
logger.info(f"⚠️ {field}: 빈 값 - {field_data}")
|
||||
else:
|
||||
logger.info(f"✅ {field}: 응답에 없음 (예상됨)")
|
||||
|
||||
logger.info("=== 전체 응답 필드 목록 ===")
|
||||
for key in parsed_json.keys():
|
||||
value = parsed_json[key]
|
||||
if isinstance(value, dict) and 'value' in value:
|
||||
logger.info(f"필드: {key} = '{value.get('value', '')}' at ({value.get('x', 'N/A')}, {value.get('y', 'N/A')})")
|
||||
else:
|
||||
logger.info(f"필드: {key} = {type(value).__name__}")
|
||||
|
||||
logger.info("=== 디버깅 끝 ===")
|
||||
|
||||
pretty_result = json.dumps(parsed_json, ensure_ascii=False, indent=2)
|
||||
logger.info(f"분석 완료: {len(pretty_result)} 문자")
|
||||
return pretty_result
|
||||
else:
|
||||
logger.error("API 응답에서 텍스트를 찾을 수 없습니다.")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"이미지 및 텍스트 분석 중 오류 발생: {e}")
|
||||
return None
|
||||
|
||||
# --- 기존 다른 메서드들은 필요에 따라 수정 또는 유지 ---
|
||||
# analyze_image_stream_from_base64, analyze_pdf_images 등은
|
||||
# 새로운 analyze_pdf_page 메서드와 호환되도록 수정 필요.
|
||||
# 지금은 핵심 기능에 집중.
|
||||
|
||||
def validate_api_connection(self) -> bool:
|
||||
"""API 연결 상태를 확인합니다."""
|
||||
try:
|
||||
test_response = self.client.models.generate_content("안녕하세요")
|
||||
if test_response and hasattr(test_response, 'text'):
|
||||
logger.info("Gemini API 연결 테스트 성공")
|
||||
return True
|
||||
else:
|
||||
logger.error("Gemini API 연결 테스트 실패")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Gemini API 연결 테스트 중 오류: {e}")
|
||||
return False
|
||||
@@ -1,77 +1,89 @@
|
||||
{
|
||||
"mapping_table": {
|
||||
"ailabel_to_systems": {
|
||||
"도면명": {
|
||||
"도면명_line0": {
|
||||
"molit": "",
|
||||
"expressway": "TD_DNAME_TOP",
|
||||
"railway": "",
|
||||
"docaikey": "DNAME_TOP"
|
||||
},
|
||||
"도면명_line1": {
|
||||
"molit": "DI_TITLE",
|
||||
"expressway": "TD_DNAME_MAIN",
|
||||
"railway": "TD_DNAME_MAIN",
|
||||
"docaikey": "DNAME_MAIN"
|
||||
},
|
||||
"편철번호": {
|
||||
"도면명_line2": {
|
||||
"molit": "DI_SUBTITLE",
|
||||
"expressway": "TD_DNAME_BOT",
|
||||
"railway": "TD_DNAME_BOT",
|
||||
"docaikey": "DNAME_BOT"
|
||||
},
|
||||
"도면번호": {
|
||||
"편철번호": {
|
||||
"molit": "DA_PAGENO",
|
||||
"expressway": "TD_DWGNO",
|
||||
"railway": "TD_DWGNO",
|
||||
"docaikey": "DWGNO"
|
||||
},
|
||||
"Main Title": {
|
||||
"도면번호": {
|
||||
"molit": "DI_DRWNO",
|
||||
"expressway": "TD_DWGCODE",
|
||||
"railway": "TD_DWGCODE",
|
||||
"docaikey": "DWGCODE"
|
||||
},
|
||||
"Sub Title": {
|
||||
"Main_Title": {
|
||||
"molit": "UD_TITLE",
|
||||
"expressway": "TB_MTITIL",
|
||||
"railway": "TB_MTITIL",
|
||||
"docaikey": "MTITIL"
|
||||
},
|
||||
"수평축척": {
|
||||
"Sub_Title": {
|
||||
"molit": "UD_SUBTITLE",
|
||||
"expressway": "TB_STITL",
|
||||
"railway": "TB_STITL",
|
||||
"docaikey": "STITL"
|
||||
},
|
||||
"수직축척": {
|
||||
"molit": "",
|
||||
"expressway": "TD_DWGCODE_PREV",
|
||||
"railway": "",
|
||||
"docaikey": "DWGCODE_PREV"
|
||||
},
|
||||
"도면축척": {
|
||||
"수평_도면_축척": {
|
||||
"molit": "DA_HSCALE",
|
||||
"expressway": "TD_HSCAL",
|
||||
"railway": "",
|
||||
"docaikey": "HSCAL"
|
||||
},
|
||||
"적용표준버전": {
|
||||
"molit": "DA_STDNAME",
|
||||
"expressway": "STDNAME",
|
||||
"수직_도면_축척": {
|
||||
"molit": "DA_VSCALE",
|
||||
"expressway": "TD_VSCAL",
|
||||
"railway": "",
|
||||
"docaikey": ""
|
||||
"docaikey": "VSCAL"
|
||||
},
|
||||
"사업명": {
|
||||
"적용표준버전": {
|
||||
"molit": "DA_STDVER",
|
||||
"expressway": "TD_VERSION",
|
||||
"railway": "TD_VERSION",
|
||||
"docaikey": "VERSION"
|
||||
},
|
||||
"시설_공구": {
|
||||
"사업명_top": {
|
||||
"molit": "PI_CNAME",
|
||||
"expressway": "TB_CNAME",
|
||||
"railway": "",
|
||||
"docaikey": "TBCNAME"
|
||||
},
|
||||
"설계공구_Station": {
|
||||
"시설_공구": {
|
||||
"molit": "UD_CDNAME",
|
||||
"expressway": "TB_CSCOP",
|
||||
"railway": "",
|
||||
"docaikey": "CSCOP"
|
||||
},
|
||||
"설계공구_공구명": {
|
||||
"molit": "",
|
||||
"expressway": "TD_DSECT",
|
||||
"railway": "",
|
||||
"docaikey": "DSECT"
|
||||
},
|
||||
"시공공구_공구명": {
|
||||
"molit": "",
|
||||
"expressway": "TD_CSECT",
|
||||
"railway": "",
|
||||
"docaikey": "CSECT"
|
||||
},
|
||||
"건설분야": {
|
||||
"molit": "PA_CCLASS",
|
||||
@@ -86,54 +98,66 @@
|
||||
"docaikey": "CSTEP"
|
||||
},
|
||||
"설계사": {
|
||||
"molit": "TD_DCOMP",
|
||||
"molit": "",
|
||||
"expressway": "TD_DCOMP",
|
||||
"railway": "",
|
||||
"railway": "TD_DCOMP",
|
||||
"docaikey": "DCOMP"
|
||||
},
|
||||
"시공사": {
|
||||
"molit": "TD_CCOMP",
|
||||
"molit": "",
|
||||
"expressway": "TD_CCOMP",
|
||||
"railway": "",
|
||||
"railway": "TD_CCOMP",
|
||||
"docaikey": "CCOMP"
|
||||
},
|
||||
"노선이정": {
|
||||
"molit": "TD_LNDST",
|
||||
"expressway": "",
|
||||
"molit": "",
|
||||
"expressway": "TD_LNDST",
|
||||
"railway": "",
|
||||
"docaikey": "LNDST"
|
||||
},
|
||||
"계정번호": {
|
||||
"설계공구_범위": {
|
||||
"molit": "",
|
||||
"expressway": "TD_DDIST",
|
||||
"railway": "",
|
||||
"docaikey": "DDIST"
|
||||
},
|
||||
"시공공구_범위": {
|
||||
"molit": "",
|
||||
"expressway": "TD_CDIST",
|
||||
"railway": "",
|
||||
"docaikey": "CDIST"
|
||||
},
|
||||
"개정번호_1": {
|
||||
"molit": "DC_RNUM1",
|
||||
"expressway": "TR_RNUM1",
|
||||
"railway": "TR_RNUM1",
|
||||
"docaikey": "RNUM1"
|
||||
},
|
||||
"계정날짜": {
|
||||
"개정날짜_1": {
|
||||
"molit": "DC_RDATE1",
|
||||
"expressway": "TR_RDAT1",
|
||||
"railway": "TR_RDAT1",
|
||||
"docaikey": "RDAT1"
|
||||
},
|
||||
"개정내용": {
|
||||
"개정내용_1": {
|
||||
"molit": "DC_RDES1",
|
||||
"expressway": "TR_RCON1",
|
||||
"railway": "TR_RCON1",
|
||||
"docaikey": "RCON1"
|
||||
},
|
||||
"작성자": {
|
||||
"작성자_1": {
|
||||
"molit": "DC_RDGN1",
|
||||
"expressway": "TR_DGN1",
|
||||
"railway": "TR_DGN1",
|
||||
"docaikey": "DGN1"
|
||||
},
|
||||
"검토자": {
|
||||
"검토자_1": {
|
||||
"molit": "DC_RCHK1",
|
||||
"expressway": "TR_CHK1",
|
||||
"railway": "TR_CHK1",
|
||||
"docaikey": "CHK1"
|
||||
},
|
||||
"확인자": {
|
||||
"확인자_1": {
|
||||
"molit": "DC_RAPP1",
|
||||
"expressway": "TR_APP1",
|
||||
"railway": "TR_APP1",
|
||||
588
fletimageanalysis/multi_file_processor.py
Normal file
588
fletimageanalysis/multi_file_processor.py
Normal file
@@ -0,0 +1,588 @@
|
||||
"""
|
||||
다중 파일 처리 모듈
|
||||
여러 PDF/DXF 파일을 배치로 처리하고 결과를 CSV로 저장하는 기능을 제공합니다.
|
||||
|
||||
Author: Claude Assistant
|
||||
Created: 2025-07-14
|
||||
Version: 1.0.0
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import pandas as pd
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Any, Optional, Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from pdf_processor import PDFProcessor
|
||||
from dxf_processor import EnhancedDXFProcessor
|
||||
from gemini_analyzer import GeminiAnalyzer
|
||||
from csv_exporter import TitleBlockCSVExporter
|
||||
import json # Added import
|
||||
|
||||
# 로깅 설정
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FileProcessingResult:
|
||||
"""단일 파일 처리 결과"""
|
||||
file_path: str
|
||||
file_name: str
|
||||
file_type: str
|
||||
file_size: int
|
||||
processing_time: float
|
||||
success: bool
|
||||
error_message: Optional[str] = None
|
||||
|
||||
# PDF 분석 결과
|
||||
pdf_analysis_result: Optional[str] = None
|
||||
|
||||
# DXF 분석 결과
|
||||
dxf_title_blocks: Optional[List[Dict]] = None
|
||||
dxf_total_attributes: Optional[int] = None
|
||||
dxf_total_text_entities: Optional[int] = None
|
||||
|
||||
# 공통 메타데이터
|
||||
processed_at: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class BatchProcessingConfig:
|
||||
"""배치 처리 설정"""
|
||||
organization_type: str = "한국도로공사"
|
||||
enable_gemini_batch_mode: bool = False
|
||||
max_concurrent_files: int = 3
|
||||
save_intermediate_results: bool = True
|
||||
output_csv_path: Optional[str] = None
|
||||
include_error_files: bool = True
|
||||
|
||||
|
||||
class MultiFileProcessor:
|
||||
"""다중 파일 처리기"""
|
||||
|
||||
def __init__(self, gemini_api_key: str):
|
||||
"""
|
||||
다중 파일 처리기 초기화
|
||||
|
||||
Args:
|
||||
gemini_api_key: Gemini API 키
|
||||
"""
|
||||
self.gemini_api_key = gemini_api_key
|
||||
self.pdf_processor = PDFProcessor()
|
||||
self.dxf_processor = EnhancedDXFProcessor()
|
||||
self.gemini_analyzer = GeminiAnalyzer(gemini_api_key)
|
||||
self.csv_exporter = TitleBlockCSVExporter() # CSV 내보내기 추가
|
||||
|
||||
self.processing_results: List[FileProcessingResult] = []
|
||||
self.current_progress = 0
|
||||
self.total_files = 0
|
||||
|
||||
async def process_multiple_files(
|
||||
self,
|
||||
file_paths: List[str],
|
||||
config: BatchProcessingConfig,
|
||||
progress_callback: Optional[Callable[[int, int, str], None]] = None
|
||||
) -> List[FileProcessingResult]:
|
||||
"""
|
||||
여러 파일을 배치로 처리
|
||||
|
||||
Args:
|
||||
file_paths: 처리할 파일 경로 리스트
|
||||
config: 배치 처리 설정
|
||||
progress_callback: 진행률 콜백 함수 (current, total, status)
|
||||
|
||||
Returns:
|
||||
처리 결과 리스트
|
||||
"""
|
||||
self.processing_results = []
|
||||
self.total_files = len(file_paths)
|
||||
self.current_progress = 0
|
||||
|
||||
logger.info(f"배치 처리 시작: {self.total_files}개 파일")
|
||||
|
||||
# 동시 처리 제한을 위한 세마포어
|
||||
semaphore = asyncio.Semaphore(config.max_concurrent_files)
|
||||
|
||||
# 각 파일에 대한 처리 태스크 생성
|
||||
tasks = []
|
||||
for i, file_path in enumerate(file_paths):
|
||||
task = self._process_single_file_with_semaphore(
|
||||
semaphore, file_path, config, progress_callback, i + 1
|
||||
)
|
||||
tasks.append(task)
|
||||
|
||||
# 모든 파일 처리 완료까지 대기
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
# 예외 발생한 결과 처리
|
||||
for i, result in enumerate(results):
|
||||
if isinstance(result, Exception):
|
||||
error_result = FileProcessingResult(
|
||||
file_path=file_paths[i],
|
||||
file_name=os.path.basename(file_paths[i]),
|
||||
file_type="unknown",
|
||||
file_size=0,
|
||||
processing_time=0,
|
||||
success=False,
|
||||
error_message=str(result),
|
||||
processed_at=datetime.now().isoformat()
|
||||
)
|
||||
self.processing_results.append(error_result)
|
||||
|
||||
logger.info(f"배치 처리 완료: {len(self.processing_results)}개 결과")
|
||||
|
||||
# CSV 저장
|
||||
if config.output_csv_path:
|
||||
await self.save_results_to_csv(config.output_csv_path)
|
||||
|
||||
# JSON 출력도 함께 생성 (좌표 정보 포함)
|
||||
json_output_path = config.output_csv_path.replace('.csv', '.json')
|
||||
await self.save_results_to_json(json_output_path)
|
||||
|
||||
return self.processing_results
|
||||
|
||||
async def _process_single_file_with_semaphore(
|
||||
self,
|
||||
semaphore: asyncio.Semaphore,
|
||||
file_path: str,
|
||||
config: BatchProcessingConfig,
|
||||
progress_callback: Optional[Callable[[int, int, str], None]],
|
||||
file_number: int
|
||||
) -> None:
|
||||
"""세마포어를 사용하여 단일 파일 처리"""
|
||||
async with semaphore:
|
||||
result = await self._process_single_file(file_path, config)
|
||||
self.processing_results.append(result)
|
||||
|
||||
self.current_progress += 1
|
||||
if progress_callback:
|
||||
status = f"처리 완료: {result.file_name}"
|
||||
if not result.success:
|
||||
status = f"처리 실패: {result.file_name} - {result.error_message}"
|
||||
progress_callback(self.current_progress, self.total_files, status)
|
||||
|
||||
async def _process_single_file(
|
||||
self,
|
||||
file_path: str,
|
||||
config: BatchProcessingConfig
|
||||
) -> FileProcessingResult:
|
||||
"""
|
||||
단일 파일 처리
|
||||
|
||||
Args:
|
||||
file_path: 파일 경로
|
||||
config: 처리 설정
|
||||
|
||||
Returns:
|
||||
처리 결과
|
||||
"""
|
||||
start_time = asyncio.get_event_loop().time()
|
||||
file_name = os.path.basename(file_path)
|
||||
|
||||
try:
|
||||
# 파일 정보 수집
|
||||
file_size = os.path.getsize(file_path)
|
||||
file_type = self._detect_file_type(file_path)
|
||||
|
||||
logger.info(f"파일 처리 시작: {file_name} ({file_type})")
|
||||
|
||||
result = FileProcessingResult(
|
||||
file_path=file_path,
|
||||
file_name=file_name,
|
||||
file_type=file_type,
|
||||
file_size=file_size,
|
||||
processing_time=0,
|
||||
success=False,
|
||||
processed_at=datetime.now().isoformat()
|
||||
)
|
||||
|
||||
# 파일 유형에 따른 처리
|
||||
if file_type.lower() == 'pdf':
|
||||
await self._process_pdf_file(file_path, result, config)
|
||||
elif file_type.lower() == 'dxf':
|
||||
await self._process_dxf_file(file_path, result, config)
|
||||
else:
|
||||
raise ValueError(f"지원하지 않는 파일 형식: {file_type}")
|
||||
|
||||
result.success = True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"파일 처리 오류 ({file_name}): {str(e)}")
|
||||
result.success = False
|
||||
result.error_message = str(e)
|
||||
|
||||
finally:
|
||||
# 처리 시간 계산
|
||||
end_time = asyncio.get_event_loop().time()
|
||||
result.processing_time = round(end_time - start_time, 2)
|
||||
|
||||
return result
|
||||
|
||||
async def _process_pdf_file(
|
||||
self,
|
||||
file_path: str,
|
||||
result: FileProcessingResult,
|
||||
config: BatchProcessingConfig
|
||||
) -> None:
|
||||
"""PDF 파일 처리"""
|
||||
# PDF 이미지 변환
|
||||
images = self.pdf_processor.convert_to_images(file_path)
|
||||
if not images:
|
||||
raise ValueError("PDF를 이미지로 변환할 수 없습니다")
|
||||
|
||||
# 첫 번째 페이지만 분석 (다중 페이지 처리는 향후 개선)
|
||||
first_page = images[0]
|
||||
base64_image = self.pdf_processor.image_to_base64(first_page)
|
||||
|
||||
# PDF에서 텍스트 블록 추출
|
||||
text_blocks = self.pdf_processor.extract_text_with_coordinates(file_path, 0)
|
||||
|
||||
# Gemini API로 분석
|
||||
# 실제 구현에서는 batch mode 사용 가능
|
||||
analysis_result = await self._analyze_with_gemini(
|
||||
base64_image, text_blocks, config.organization_type
|
||||
)
|
||||
|
||||
result.pdf_analysis_result = analysis_result
|
||||
|
||||
async def _process_dxf_file(
|
||||
self,
|
||||
file_path: str,
|
||||
result: FileProcessingResult,
|
||||
config: BatchProcessingConfig
|
||||
) -> None:
|
||||
"""DXF 파일 처리"""
|
||||
# DXF 파일 분석
|
||||
extraction_result = self.dxf_processor.extract_comprehensive_data(file_path)
|
||||
|
||||
# 타이틀 블록 정보를 딕셔너리 리스트로 변환
|
||||
title_blocks = []
|
||||
for tb_info in extraction_result.title_blocks:
|
||||
tb_dict = {
|
||||
'block_name': tb_info.block_name,
|
||||
'block_position': f"{tb_info.block_position[0]:.2f}, {tb_info.block_position[1]:.2f}",
|
||||
'attributes_count': tb_info.attributes_count,
|
||||
'attributes': [
|
||||
{
|
||||
'tag': attr.tag,
|
||||
'text': attr.text,
|
||||
'prompt': attr.prompt,
|
||||
'insert_x': attr.insert_x,
|
||||
'insert_y': attr.insert_y
|
||||
}
|
||||
for attr in tb_info.all_attributes
|
||||
]
|
||||
}
|
||||
title_blocks.append(tb_dict)
|
||||
|
||||
result.dxf_title_blocks = title_blocks
|
||||
result.dxf_total_attributes = sum(tb['attributes_count'] for tb in title_blocks)
|
||||
result.dxf_total_text_entities = len(extraction_result.text_entities)
|
||||
|
||||
# 상세한 title block attributes CSV 생성
|
||||
if extraction_result.title_blocks:
|
||||
await self._save_detailed_dxf_csv(file_path, extraction_result)
|
||||
|
||||
async def _analyze_with_gemini(
|
||||
self,
|
||||
base64_image: str,
|
||||
text_blocks: list,
|
||||
organization_type: str
|
||||
) -> str:
|
||||
"""Gemini API로 이미지 분석"""
|
||||
try:
|
||||
# 비동기 처리를 위해 동기 함수를 태스크로 실행
|
||||
loop = asyncio.get_event_loop()
|
||||
result = await loop.run_in_executor(
|
||||
None,
|
||||
self.gemini_analyzer.analyze_pdf_page,
|
||||
base64_image,
|
||||
text_blocks,
|
||||
None, # prompt (default 사용)
|
||||
"image/png", # mime_type
|
||||
organization_type
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Gemini 분석 오류: {str(e)}")
|
||||
return f"분석 실패: {str(e)}"
|
||||
|
||||
async def _save_detailed_dxf_csv(
|
||||
self,
|
||||
file_path: str,
|
||||
extraction_result
|
||||
) -> None:
|
||||
"""상세한 DXF title block attributes CSV 저장"""
|
||||
try:
|
||||
# 파일명에서 확장자 제거
|
||||
file_name = os.path.splitext(os.path.basename(file_path))[0]
|
||||
|
||||
# 출력 디렉토리 확인 및 생성
|
||||
output_dir = os.path.join(os.path.dirname(file_path), '..', 'results')
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# CSV 파일명 생성
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
csv_filename = f"detailed_title_blocks_{file_name}_{timestamp}.csv"
|
||||
csv_path = os.path.join(output_dir, csv_filename)
|
||||
|
||||
# TitleBlockCSVExporter를 사용하여 CSV 생성
|
||||
loop = asyncio.get_event_loop()
|
||||
await loop.run_in_executor(
|
||||
None,
|
||||
self.csv_exporter.save_title_block_info_to_csv,
|
||||
extraction_result.title_blocks,
|
||||
csv_path
|
||||
)
|
||||
|
||||
logger.info(f"상세 DXF CSV 저장 완료: {csv_path}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"상세 DXF CSV 저장 오류: {str(e)}")
|
||||
|
||||
def _detect_file_type(self, file_path: str) -> str:
|
||||
"""파일 확장자로 파일 유형 검출"""
|
||||
_, ext = os.path.splitext(file_path.lower())
|
||||
if ext == '.pdf':
|
||||
return 'PDF'
|
||||
elif ext == '.dxf':
|
||||
return 'DXF'
|
||||
else:
|
||||
return ext.upper().lstrip('.')
|
||||
|
||||
async def save_results_to_csv(self, output_path: str) -> None:
|
||||
"""
|
||||
처리 결과를 CSV 파일로 저장
|
||||
|
||||
Args:
|
||||
output_path: 출력 CSV 파일 경로
|
||||
"""
|
||||
try:
|
||||
# 결과를 DataFrame으로 변환
|
||||
data_rows = []
|
||||
|
||||
for result in self.processing_results:
|
||||
# 기본 정보
|
||||
row = {
|
||||
'file_name': result.file_name,
|
||||
'file_path': result.file_path,
|
||||
'file_type': result.file_type,
|
||||
'file_size_bytes': result.file_size,
|
||||
'file_size_mb': round(result.file_size / (1024 * 1024), 2),
|
||||
'processing_time_seconds': result.processing_time,
|
||||
'success': result.success,
|
||||
'error_message': result.error_message or '',
|
||||
'processed_at': result.processed_at
|
||||
}
|
||||
|
||||
# PDF 분석 결과
|
||||
if result.file_type.lower() == 'pdf':
|
||||
row['pdf_analysis_result'] = result.pdf_analysis_result or ''
|
||||
row['dxf_total_attributes'] = ''
|
||||
row['dxf_total_text_entities'] = ''
|
||||
row['dxf_title_blocks_summary'] = ''
|
||||
|
||||
# DXF 분석 결과
|
||||
elif result.file_type.lower() == 'dxf':
|
||||
row['pdf_analysis_result'] = ''
|
||||
row['dxf_total_attributes'] = result.dxf_total_attributes or 0
|
||||
row['dxf_total_text_entities'] = result.dxf_total_text_entities or 0
|
||||
|
||||
# 타이틀 블록 요약
|
||||
if result.dxf_title_blocks:
|
||||
summary = f"{len(result.dxf_title_blocks)}개 타이틀블록"
|
||||
for tb in result.dxf_title_blocks[:3]: # 처음 3개만 표시
|
||||
summary += f" | {tb['block_name']}({tb['attributes_count']}속성)"
|
||||
if len(result.dxf_title_blocks) > 3:
|
||||
summary += f" | ...외 {len(result.dxf_title_blocks)-3}개"
|
||||
row['dxf_title_blocks_summary'] = summary
|
||||
else:
|
||||
row['dxf_title_blocks_summary'] = '타이틀블록 없음'
|
||||
|
||||
data_rows.append(row)
|
||||
|
||||
# DataFrame 생성 및 CSV 저장
|
||||
df = pd.DataFrame(data_rows)
|
||||
|
||||
# pdf_analysis_result 컬럼 평탄화
|
||||
if 'pdf_analysis_result' in df.columns:
|
||||
# JSON 문자열을 딕셔너리로 변환 (이미 딕셔너리인 경우도 처리)
|
||||
df['pdf_analysis_result'] = df['pdf_analysis_result'].apply(lambda x: json.loads(x) if isinstance(x, str) and x.strip() else {}).fillna({})
|
||||
|
||||
# 평탄화된 데이터를 새로운 DataFrame으로 생성
|
||||
# errors='ignore'를 사용하여 JSON이 아닌 값은 무시
|
||||
# record_prefix를 사용하여 컬럼 이름에 접두사 추가
|
||||
pdf_analysis_df = pd.json_normalize(df['pdf_analysis_result'], errors='ignore', record_prefix='pdf_analysis_result_')
|
||||
|
||||
# 원본 df에서 pdf_analysis_result 컬럼 제거
|
||||
df = df.drop(columns=['pdf_analysis_result'])
|
||||
|
||||
# 원본 df와 평탄화된 DataFrame을 병합
|
||||
df = pd.concat([df, pdf_analysis_df], axis=1)
|
||||
|
||||
# 컬럼 순서 정렬을 위한 기본 순서 정의
|
||||
column_order = [
|
||||
'file_name', 'file_type', 'file_size_mb', 'processing_time_seconds',
|
||||
'success', 'error_message', 'processed_at', 'file_path', 'file_size_bytes',
|
||||
'dxf_total_attributes', 'dxf_total_text_entities', 'dxf_title_blocks_summary'
|
||||
]
|
||||
|
||||
# 기존 컬럼 순서를 유지하면서 새로운 컬럼을 추가
|
||||
existing_columns = [col for col in column_order if col in df.columns]
|
||||
new_columns = [col for col in df.columns if col not in existing_columns]
|
||||
df = df[existing_columns + sorted(new_columns)]
|
||||
|
||||
# UTF-8 BOM으로 저장 (한글 호환성)
|
||||
df.to_csv(output_path, index=False, encoding='utf-8-sig')
|
||||
|
||||
logger.info(f"CSV 저장 완료: {output_path}")
|
||||
logger.info(f"총 {len(data_rows)}개 파일 결과 저장")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"CSV 저장 오류: {str(e)}")
|
||||
raise
|
||||
|
||||
async def save_results_to_json(self, output_path: str) -> None:
|
||||
"""
|
||||
처리 결과를 JSON 파일로 저장 (좌표 정보 포함)
|
||||
|
||||
Args:
|
||||
output_path: 출력 JSON 파일 경로
|
||||
"""
|
||||
try:
|
||||
# 결과를 JSON 구조로 변환
|
||||
json_data = {
|
||||
"metadata": {
|
||||
"total_files": len(self.processing_results),
|
||||
"success_files": sum(1 for r in self.processing_results if r.success),
|
||||
"failed_files": sum(1 for r in self.processing_results if not r.success),
|
||||
"generated_at": datetime.now().isoformat(),
|
||||
"format_version": "1.0"
|
||||
},
|
||||
"results": []
|
||||
}
|
||||
|
||||
for result in self.processing_results:
|
||||
# 기본 정보
|
||||
result_data = {
|
||||
"file_info": {
|
||||
"name": result.file_name,
|
||||
"path": result.file_path,
|
||||
"type": result.file_type,
|
||||
"size_bytes": result.file_size,
|
||||
"size_mb": round(result.file_size / (1024 * 1024), 2)
|
||||
},
|
||||
"processing_info": {
|
||||
"success": result.success,
|
||||
"processing_time_seconds": result.processing_time,
|
||||
"processed_at": result.processed_at,
|
||||
"error_message": result.error_message
|
||||
}
|
||||
}
|
||||
|
||||
# PDF 분석 결과 (좌표 정보 포함)
|
||||
if result.file_type.lower() == 'pdf' and result.pdf_analysis_result:
|
||||
try:
|
||||
# JSON 문자열을 딕셔너리로 변환 (이미 딕셔너리인 경우도 처리)
|
||||
if isinstance(result.pdf_analysis_result, str):
|
||||
analysis_data = json.loads(result.pdf_analysis_result)
|
||||
else:
|
||||
analysis_data = result.pdf_analysis_result
|
||||
|
||||
result_data["pdf_analysis"] = analysis_data
|
||||
|
||||
except (json.JSONDecodeError, TypeError) as e:
|
||||
logger.warning(f"PDF 분석 결과 JSON 파싱 오류: {e}")
|
||||
result_data["pdf_analysis"] = {"error": "JSON 파싱 실패", "raw_data": str(result.pdf_analysis_result)}
|
||||
|
||||
# DXF 분석 결과
|
||||
elif result.file_type.lower() == 'dxf':
|
||||
result_data["dxf_analysis"] = {
|
||||
"total_attributes": result.dxf_total_attributes or 0,
|
||||
"total_text_entities": result.dxf_total_text_entities or 0,
|
||||
"title_blocks": result.dxf_title_blocks or []
|
||||
}
|
||||
|
||||
json_data["results"].append(result_data)
|
||||
|
||||
# JSON 파일 저장 (예쁜 포맷팅과 한글 지원)
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(json_data, f, ensure_ascii=False, indent=2, default=str)
|
||||
|
||||
logger.info(f"JSON 저장 완료: {output_path}")
|
||||
logger.info(f"총 {len(json_data['results'])}개 파일 결과 저장 (좌표 정보 포함)")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"JSON 저장 오류: {str(e)}")
|
||||
raise
|
||||
|
||||
def get_processing_summary(self) -> Dict[str, Any]:
|
||||
"""처리 결과 요약 정보 반환"""
|
||||
if not self.processing_results:
|
||||
return {}
|
||||
|
||||
total_files = len(self.processing_results)
|
||||
success_files = sum(1 for r in self.processing_results if r.success)
|
||||
failed_files = total_files - success_files
|
||||
|
||||
pdf_files = sum(1 for r in self.processing_results if r.file_type.lower() == 'pdf')
|
||||
dxf_files = sum(1 for r in self.processing_results if r.file_type.lower() == 'dxf')
|
||||
|
||||
total_processing_time = sum(r.processing_time for r in self.processing_results)
|
||||
avg_processing_time = total_processing_time / total_files if total_files > 0 else 0
|
||||
|
||||
total_file_size = sum(r.file_size for r in self.processing_results)
|
||||
|
||||
return {
|
||||
'total_files': total_files,
|
||||
'success_files': success_files,
|
||||
'failed_files': failed_files,
|
||||
'pdf_files': pdf_files,
|
||||
'dxf_files': dxf_files,
|
||||
'total_processing_time': round(total_processing_time, 2),
|
||||
'avg_processing_time': round(avg_processing_time, 2),
|
||||
'total_file_size_mb': round(total_file_size / (1024 * 1024), 2),
|
||||
'success_rate': round((success_files / total_files) * 100, 1) if total_files > 0 else 0
|
||||
}
|
||||
|
||||
|
||||
def generate_default_csv_filename() -> str:
|
||||
"""기본 CSV 파일명 생성"""
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
return f"batch_analysis_results_{timestamp}.csv"
|
||||
|
||||
|
||||
# 사용 예시
|
||||
if __name__ == "__main__":
|
||||
async def main():
|
||||
# 테스트용 예시
|
||||
processor = MultiFileProcessor("your-gemini-api-key")
|
||||
|
||||
config = BatchProcessingConfig(
|
||||
organization_type="한국도로공사",
|
||||
max_concurrent_files=2,
|
||||
output_csv_path="test_results.csv"
|
||||
)
|
||||
|
||||
# 진행률 콜백 함수
|
||||
def progress_callback(current: int, total: int, status: str):
|
||||
print(f"진행률: {current}/{total} ({current/total*100:.1f}%) - {status}")
|
||||
|
||||
# 파일 경로 리스트 (실제 파일 경로로 교체 필요)
|
||||
file_paths = [
|
||||
"sample1.pdf",
|
||||
"sample2.dxf",
|
||||
"sample3.pdf"
|
||||
]
|
||||
|
||||
results = await processor.process_multiple_files(
|
||||
file_paths, config, progress_callback
|
||||
)
|
||||
|
||||
summary = processor.get_processing_summary()
|
||||
print("처리 요약:", summary)
|
||||
|
||||
# asyncio.run(main())
|
||||
322
fletimageanalysis/pdf_processor.py
Normal file
322
fletimageanalysis/pdf_processor.py
Normal file
@@ -0,0 +1,322 @@
|
||||
"""
|
||||
PDF 처리 모듈
|
||||
PDF 파일을 이미지로 변환하고 base64로 인코딩하는 기능을 제공합니다.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import io
|
||||
import fitz # PyMuPDF
|
||||
from PIL import Image
|
||||
from typing import List, Optional, Tuple, Dict, Any
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
# 로깅 설정
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class PDFProcessor:
|
||||
"""PDF 파일 처리 클래스"""
|
||||
|
||||
def __init__(self):
|
||||
self.supported_formats = ['pdf']
|
||||
|
||||
def validate_pdf_file(self, file_path: str) -> bool:
|
||||
"""PDF 파일 유효성 검사"""
|
||||
try:
|
||||
path = Path(file_path)
|
||||
|
||||
# 파일 존재 여부 확인
|
||||
if not path.exists():
|
||||
logger.error(f"파일이 존재하지 않습니다: {file_path}")
|
||||
return False
|
||||
|
||||
# 파일 확장자 확인
|
||||
if path.suffix.lower() != '.pdf':
|
||||
logger.error(f"지원하지 않는 파일 형식입니다: {path.suffix}")
|
||||
return False
|
||||
|
||||
# PDF 파일 열기 테스트
|
||||
doc = fitz.open(file_path)
|
||||
page_count = len(doc)
|
||||
doc.close()
|
||||
|
||||
if page_count == 0:
|
||||
logger.error("PDF 파일에 페이지가 없습니다.")
|
||||
return False
|
||||
|
||||
logger.info(f"PDF 검증 완료: {page_count}페이지")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"PDF 파일 검증 중 오류 발생: {e}")
|
||||
return False
|
||||
|
||||
def get_pdf_info(self, file_path: str) -> Optional[dict]:
|
||||
"""PDF 파일 정보 조회"""
|
||||
try:
|
||||
doc = fitz.open(file_path)
|
||||
info = {
|
||||
'page_count': len(doc),
|
||||
'metadata': doc.metadata,
|
||||
'file_size': Path(file_path).stat().st_size,
|
||||
'filename': Path(file_path).name
|
||||
}
|
||||
doc.close()
|
||||
return info
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"PDF 정보 조회 중 오류 발생: {e}")
|
||||
return None
|
||||
|
||||
def convert_pdf_page_to_image(
|
||||
self,
|
||||
file_path: str,
|
||||
page_number: int = 0,
|
||||
zoom: float = 2.0,
|
||||
image_format: str = "PNG"
|
||||
) -> Optional[Image.Image]:
|
||||
"""PDF 페이지를 PIL Image로 변환"""
|
||||
try:
|
||||
doc = fitz.open(file_path)
|
||||
|
||||
if page_number >= len(doc):
|
||||
logger.error(f"페이지 번호가 범위를 벗어남: {page_number}")
|
||||
doc.close()
|
||||
return None
|
||||
|
||||
# 페이지 로드
|
||||
page = doc.load_page(page_number)
|
||||
|
||||
# 이미지 변환을 위한 매트릭스 설정 (확대/축소)
|
||||
mat = fitz.Matrix(zoom, zoom)
|
||||
pix = page.get_pixmap(matrix=mat)
|
||||
|
||||
# PIL Image로 변환
|
||||
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
|
||||
|
||||
doc.close()
|
||||
logger.info(f"페이지 {page_number + 1} 이미지 변환 완료: {img.size}")
|
||||
return img
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"PDF 페이지 이미지 변환 중 오류 발생: {e}")
|
||||
return None
|
||||
|
||||
def convert_pdf_to_images(
|
||||
self,
|
||||
file_path: str,
|
||||
max_pages: Optional[int] = None,
|
||||
zoom: float = 2.0
|
||||
) -> List[Image.Image]:
|
||||
"""PDF의 모든 페이지를 이미지로 변환"""
|
||||
images = []
|
||||
|
||||
try:
|
||||
doc = fitz.open(file_path)
|
||||
total_pages = len(doc)
|
||||
|
||||
# 최대 페이지 수 제한
|
||||
if max_pages:
|
||||
total_pages = min(total_pages, max_pages)
|
||||
|
||||
for page_num in range(total_pages):
|
||||
img = self.convert_pdf_page_to_image(file_path, page_num, zoom)
|
||||
if img:
|
||||
images.append(img)
|
||||
|
||||
doc.close()
|
||||
logger.info(f"총 {len(images)}개 페이지 이미지 변환 완료")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"PDF 전체 페이지 변환 중 오류 발생: {e}")
|
||||
|
||||
return images
|
||||
|
||||
def image_to_base64(
|
||||
self,
|
||||
image: Image.Image,
|
||||
format: str = "PNG",
|
||||
quality: int = 95
|
||||
) -> Optional[str]:
|
||||
"""PIL Image를 base64 문자열로 변환"""
|
||||
try:
|
||||
buffer = io.BytesIO()
|
||||
|
||||
# JPEG 형식인 경우 품질 설정
|
||||
if format.upper() == "JPEG":
|
||||
image.save(buffer, format=format, quality=quality)
|
||||
else:
|
||||
image.save(buffer, format=format)
|
||||
|
||||
buffer.seek(0)
|
||||
base64_string = base64.b64encode(buffer.getvalue()).decode('utf-8')
|
||||
|
||||
logger.info(f"이미지를 base64로 변환 완료 (크기: {len(base64_string)} 문자)")
|
||||
return base64_string
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"이미지 base64 변환 중 오류 발생: {e}")
|
||||
return None
|
||||
|
||||
def pdf_page_to_base64(
|
||||
self,
|
||||
file_path: str,
|
||||
page_number: int = 0,
|
||||
zoom: float = 2.0,
|
||||
format: str = "PNG"
|
||||
) -> Optional[str]:
|
||||
"""PDF 페이지를 직접 base64로 변환"""
|
||||
img = self.convert_pdf_page_to_image(file_path, page_number, zoom)
|
||||
if img:
|
||||
return self.image_to_base64(img, format)
|
||||
return None
|
||||
|
||||
def pdf_page_to_image_bytes(
|
||||
self,
|
||||
file_path: str,
|
||||
page_number: int = 0,
|
||||
zoom: float = 2.0,
|
||||
format: str = "PNG"
|
||||
) -> Optional[bytes]:
|
||||
"""PDF 페이지를 이미지 바이트로 변환 (Flet 이미지 표시용)"""
|
||||
try:
|
||||
img = self.convert_pdf_page_to_image(file_path, page_number, zoom)
|
||||
if img:
|
||||
buffer = io.BytesIO()
|
||||
img.save(buffer, format=format)
|
||||
buffer.seek(0)
|
||||
image_bytes = buffer.getvalue()
|
||||
|
||||
logger.info(f"페이지 {page_number + 1} 이미지 바이트 변환 완료 (크기: {len(image_bytes)} 바이트)")
|
||||
return image_bytes
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"PDF 페이지 이미지 바이트 변환 중 오류 발생: {e}")
|
||||
return None
|
||||
|
||||
def get_optimal_zoom_for_size(self, target_size: Tuple[int, int]) -> float:
|
||||
"""목표 크기에 맞는 최적 줌 비율 계산"""
|
||||
# 기본 PDF 페이지 크기 (A4: 595x842 points)
|
||||
default_width, default_height = 595, 842
|
||||
target_width, target_height = target_size
|
||||
|
||||
# 비율 계산
|
||||
width_ratio = target_width / default_width
|
||||
height_ratio = target_height / default_height
|
||||
|
||||
# 작은 비율을 선택하여 전체 페이지가 들어가도록 함
|
||||
zoom = min(width_ratio, height_ratio)
|
||||
|
||||
logger.info(f"최적 줌 비율 계산: {zoom:.2f}")
|
||||
return zoom
|
||||
|
||||
def extract_text_with_coordinates(self, file_path: str, page_number: int = 0) -> List[Dict[str, Any]]:
|
||||
"""PDF 페이지에서 텍스트와 좌표를 추출합니다."""
|
||||
text_blocks = []
|
||||
try:
|
||||
doc = fitz.open(file_path)
|
||||
if page_number >= len(doc):
|
||||
logger.error(f"페이지 번호가 범위를 벗어남: {page_number}")
|
||||
doc.close()
|
||||
return []
|
||||
|
||||
page = doc.load_page(page_number)
|
||||
# 'dict' 옵션은 블록, 라인, 스팬에 대한 상세 정보를 제공합니다.
|
||||
blocks = page.get_text("dict")["blocks"]
|
||||
for b in blocks: # 블록 반복
|
||||
if b['type'] == 0: # 텍스트 블록
|
||||
for l in b["lines"]: # 라인 반복
|
||||
for s in l["spans"]: # 스팬(텍스트 조각) 반복
|
||||
text_blocks.append({
|
||||
"text": s["text"],
|
||||
"bbox": s["bbox"], # (x0, y0, x1, y1)
|
||||
"font": s["font"],
|
||||
"size": s["size"]
|
||||
})
|
||||
|
||||
doc.close()
|
||||
logger.info(f"페이지 {page_number + 1}에서 {len(text_blocks)}개의 텍스트 블록 추출 완료")
|
||||
return text_blocks
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"PDF 텍스트 및 좌표 추출 중 오류 발생: {e}")
|
||||
return []
|
||||
|
||||
def convert_to_images(
|
||||
self,
|
||||
file_path: str,
|
||||
zoom: float = 2.0,
|
||||
max_pages: int = 10
|
||||
) -> List[Image.Image]:
|
||||
"""PDF의 모든 페이지(또는 지정된 수까지)를 PIL Image 리스트로 변환"""
|
||||
images = []
|
||||
try:
|
||||
doc = fitz.open(file_path)
|
||||
page_count = min(len(doc), max_pages) # 최대 페이지 수 제한
|
||||
|
||||
logger.info(f"PDF 변환 시작: {page_count}페이지")
|
||||
|
||||
for page_num in range(page_count):
|
||||
page = doc.load_page(page_num)
|
||||
|
||||
# 이미지 변환을 위한 매트릭스 설정
|
||||
mat = fitz.Matrix(zoom, zoom)
|
||||
pix = page.get_pixmap(matrix=mat)
|
||||
|
||||
# PIL Image로 변환
|
||||
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
|
||||
images.append(img)
|
||||
|
||||
logger.info(f"페이지 {page_num + 1}/{page_count} 변환 완료: {img.size}")
|
||||
|
||||
doc.close()
|
||||
logger.info(f"PDF 전체 변환 완료: {len(images)}개 이미지")
|
||||
return images
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"PDF 다중 페이지 변환 중 오류 발생: {e}")
|
||||
return []
|
||||
|
||||
def image_to_bytes(self, image: Image.Image, format: str = 'PNG') -> bytes:
|
||||
"""
|
||||
PIL Image를 바이트 데이터로 변환합니다.
|
||||
|
||||
Args:
|
||||
image: PIL Image 객체
|
||||
format: 이미지 포맷 ('PNG', 'JPEG' 등)
|
||||
|
||||
Returns:
|
||||
이미지 바이트 데이터
|
||||
"""
|
||||
try:
|
||||
buffer = io.BytesIO()
|
||||
image.save(buffer, format=format)
|
||||
image_bytes = buffer.getvalue()
|
||||
buffer.close()
|
||||
|
||||
logger.info(f"이미지를 {format} 바이트로 변환: {len(image_bytes)} bytes")
|
||||
return image_bytes
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"이미지 바이트 변환 중 오류 발생: {e}")
|
||||
return b''
|
||||
|
||||
# 사용 예시
|
||||
if __name__ == "__main__":
|
||||
processor = PDFProcessor()
|
||||
|
||||
# 테스트용 코드 (실제 PDF 파일 경로로 변경 필요)
|
||||
test_pdf = "test.pdf"
|
||||
|
||||
if processor.validate_pdf_file(test_pdf):
|
||||
info = processor.get_pdf_info(test_pdf)
|
||||
print(f"PDF 정보: {info}")
|
||||
|
||||
# 첫 번째 페이지를 base64로 변환
|
||||
base64_data = processor.pdf_page_to_base64(test_pdf, 0)
|
||||
if base64_data:
|
||||
print(f"Base64 변환 성공: {len(base64_data)} 문자")
|
||||
else:
|
||||
print("PDF 파일 검증 실패")
|
||||
9
fletimageanalysis/requirements-cli.txt
Normal file
9
fletimageanalysis/requirements-cli.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
# Essential packages for CLI batch processing only
|
||||
PyMuPDF>=1.26.3
|
||||
google-genai>=1.0.0
|
||||
Pillow>=10.0.0
|
||||
ezdxf>=1.4.2
|
||||
numpy>=1.24.0
|
||||
python-dotenv>=1.0.0
|
||||
pandas>=2.0.0
|
||||
requests>=2.31.0
|
||||
38
fletimageanalysis/requirements.txt
Normal file
38
fletimageanalysis/requirements.txt
Normal file
@@ -0,0 +1,38 @@
|
||||
# Flet 기반 PDF 이미지 분석기 - 필수 라이브러리
|
||||
|
||||
# UI 프레임워크
|
||||
flet>=0.25.1
|
||||
|
||||
# Google Generative AI SDK
|
||||
google-genai>=1.0.0
|
||||
|
||||
# PDF 처리 라이브러리 (둘 중 하나 선택)
|
||||
PyMuPDF>=1.26.3
|
||||
pdf2image>=1.17.0
|
||||
|
||||
# 이미지 처리
|
||||
Pillow>=10.0.0
|
||||
|
||||
# DXF 파일 처리 (NEW)
|
||||
ezdxf>=1.4.2
|
||||
|
||||
# 수치 계산 (NEW)
|
||||
numpy>=1.24.0
|
||||
|
||||
# 환경 변수 관리
|
||||
python-dotenv>=1.0.0
|
||||
|
||||
# 추가 유틸리티
|
||||
requests>=2.31.0
|
||||
|
||||
# 데이터 처리 (NEW - 다중 파일 CSV 출력용)
|
||||
pandas>=2.0.0
|
||||
|
||||
# Flet Material Design (선택 사항)
|
||||
flet-material>=0.3.3
|
||||
|
||||
# 개발 도구 (선택 사항)
|
||||
# black>=23.0.0
|
||||
# flake8>=6.0.0
|
||||
# pytest>=7.0.0
|
||||
# mypy>=1.0.0
|
||||
134
notedetectproblem.txt
Normal file
134
notedetectproblem.txt
Normal file
@@ -0,0 +1,134 @@
|
||||
NOTE Detection Algorithm Context Report
|
||||
|
||||
Problem Summary
|
||||
|
||||
Successfully integrated NOTE extraction from ExportExcel_note.cs into the new modular architecture, but encountering
|
||||
issues where only some NOTEs are being detected and finding their content boxes.
|
||||
|
||||
Current Status
|
||||
|
||||
✅ FIXED: No note content issue - reverted to original working cross-line intersection algorithm from ExportExcel_old.cs
|
||||
|
||||
🔄 ONGOING: Not detecting all NOTEs (missing notes 2 and 4 from a 4-note layout)
|
||||
|
||||
Architecture Overview
|
||||
|
||||
Key Files and Components
|
||||
|
||||
- Main Entry Point: Models/ExportExcel.cs:265-271 - calls note extraction in ExportAllDwgToExcelHeightSorted
|
||||
- Core Algorithm: Models/DwgDataExtractor.cs:342-480 - ExtractNotesFromDrawing method
|
||||
- Note Box Detection: Models/DwgDataExtractor.cs:513-569 - FindNoteBox method
|
||||
- Excel Output: Models/ExcelDataWriter.cs:282-371 - WriteNoteEntities method
|
||||
|
||||
Current Algorithm Flow
|
||||
|
||||
1. Collection Phase: Gather all DBText, Polyline, and Line entities
|
||||
2. NOTE Detection: Find DBText containing "NOTE" (case-insensitive)
|
||||
3. Box Finding: For each NOTE, use cross-line intersection to find content box below
|
||||
4. Content Extraction: Find text entities within detected boxes
|
||||
5. Sorting & Grouping: Sort by coordinates (Y descending, X ascending) and group NOTE+content
|
||||
6. Excel Output: Write to Excel with NOTE followed immediately by its content
|
||||
|
||||
Current Working Algorithm (Reverted from ExportExcel_old.cs)
|
||||
|
||||
FindNoteBox Method (DwgDataExtractor.cs:513-569)
|
||||
|
||||
// Draws horizontal search line below NOTE position
|
||||
double searchY = notePos.Y - (noteHeight * 2);
|
||||
var searchLineStart = new Point3d(notePos.X - noteHeight * 10, searchY, 0);
|
||||
var searchLineEnd = new Point3d(notePos.X + noteHeight * 50, searchY, 0);
|
||||
|
||||
// 1. Check Polyline intersections
|
||||
// 2. Check Line intersections and trace rectangles
|
||||
// 3. Use usedBoxes HashSet to prevent duplicate assignment
|
||||
|
||||
IsValidNoteBox Validation (DwgDataExtractor.cs:1005-1032)
|
||||
|
||||
// Simple validation criteria:
|
||||
// - Box must be below NOTE (box.maxPoint.Y < notePos.Y)
|
||||
// - Size constraints: noteHeight < width/height < noteHeight * 100
|
||||
// - Distance constraints: X distance < noteHeight * 50, Y distance < noteHeight * 10
|
||||
|
||||
Known Issues from Previous Sessions
|
||||
|
||||
Issue 1: 1/1/3/3 Duplicate Content (PREVIOUSLY FIXED)
|
||||
|
||||
Problem: Multiple NOTEs finding the same large spanning polyline
|
||||
Root Cause: Box detection finding one large polyline spanning multiple note areas
|
||||
Solution Applied: Used usedBoxes HashSet to prevent duplicate assignment
|
||||
|
||||
Issue 2: Reverse Note Ordering (PREVIOUSLY FIXED)
|
||||
|
||||
Problem: Notes written in reverse order
|
||||
Solution Applied: Sort by Y descending (bigger Y = top), then X ascending
|
||||
|
||||
Issue 3: Wrong Note Grouping (PREVIOUSLY FIXED)
|
||||
|
||||
Problem: All NOTEs grouped first, then all content
|
||||
Solution Applied: Group each NOTE immediately with its content
|
||||
|
||||
Issue 4: Missing NOTEs 2 and 4 (CURRENT ISSUE)
|
||||
|
||||
Problem: In a 4-note layout arranged as 1-2 (top row) and 3-4 (bottom row), only notes 1 and 3 are detected
|
||||
Possible Causes:
|
||||
- Search line positioning not intersecting with notes 2 and 4's content boxes
|
||||
- Box validation criteria too restrictive for right-side notes
|
||||
- Geometric relationship between NOTE position and content box differs for right-side notes
|
||||
|
||||
Debug Information Available
|
||||
|
||||
Last Known Debug Output (5 NOTEs detected but no content found)
|
||||
|
||||
[DEBUG] Note 텍스트 발견: 'NOTE' at (57.0572050838764,348.6990318186563,0)
|
||||
[DEBUG] Note 텍스트 발견: 'NOTE' at (471.6194660633719,501.3393888589908,0)
|
||||
[DEBUG] Note 텍스트 발견: 'NOTE' at (444.9503218738628,174.19527687737536,0)
|
||||
[DEBUG] Note 텍스트 발견: 'NOTE' at (602.7327260134425,174.43523739278135,0)
|
||||
[DEBUG] Note 텍스트 발견: 'NOTE' at (635.5065816693041,502.83938885945645,0)
|
||||
|
||||
Reference Image
|
||||
|
||||
- noteExample.png shows expected layout with numbered sections 1-7 in Korean text
|
||||
- Shows box-structured layout where each NOTE should have corresponding content below
|
||||
|
||||
Key Coordinate Analysis
|
||||
|
||||
From debug logs, NOTEs at similar Y coordinates appear to be in pairs:
|
||||
- Top Row: (444.95, 174.20) and (602.73, 174.44) - Y≈174
|
||||
- Middle Row: (471.62, 501.34) and (635.51, 502.84) - Y≈502
|
||||
- Single: (57.06, 348.70) - Y≈349
|
||||
|
||||
Pattern suggests left-right pairing where right-side NOTEs might need different search strategies.
|
||||
|
||||
Investigation Areas for Next Session
|
||||
|
||||
Priority 1: Search Line Geometry
|
||||
|
||||
- Analyze why horizontal search lines from right-side NOTEs don't intersect content boxes
|
||||
- Consider adjusting search line direction/positioning for right-side notes
|
||||
- Debug actual intersection results for missing NOTEs
|
||||
|
||||
Priority 2: Box Validation Criteria
|
||||
|
||||
- Review IsValidNoteBox distance calculations for right-side NOTEs
|
||||
- Consider if content boxes for right-side NOTEs have different geometric relationships
|
||||
|
||||
Priority 3: Coordinate Pattern Analysis
|
||||
|
||||
- Investigate why NOTEs at (602.73, 174.44) and (635.51, 502.84) aren't finding content
|
||||
- Compare successful vs failed NOTE positions and their content box relationships
|
||||
|
||||
Quick Start Commands for Next Session
|
||||
|
||||
1. Run existing code to see current NOTE detection results
|
||||
2. Add detailed debug logging to FindNoteBox for specific coordinates: (602.73, 174.44) and (635.51, 502.84)
|
||||
3. Analyze intersection results and box validation for these specific NOTEs
|
||||
4. Consider geometric adjustments for right-side NOTE detection
|
||||
|
||||
Code State
|
||||
|
||||
- Current implementation in Models/DwgDataExtractor.cs uses proven cross-line intersection algorithm
|
||||
- usedBoxes tracking prevents duplicate assignment
|
||||
- NOTE+content grouping and Y-coordinate sorting working correctly
|
||||
- Excel output formatting functional
|
||||
|
||||
The foundation is solid; focus should be on geometric refinements for complete NOTE detection coverage.
|
||||
Reference in New Issue
Block a user