diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000..b45317c
--- /dev/null
+++ b/.claude/settings.local.json
@@ -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"
+}
\ No newline at end of file
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..bc69478
--- /dev/null
+++ b/AGENTS.md
@@ -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
\ No newline at end of file
diff --git a/App.config b/App.config
new file mode 100644
index 0000000..57744ed
--- /dev/null
+++ b/App.config
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..7fa7bd8
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1 @@
+See @AGENTS.md for guidelines.
\ No newline at end of file
diff --git a/Controls/ZoomBorder.cs b/Controls/ZoomBorder.cs
new file mode 100644
index 0000000..50fada9
--- /dev/null
+++ b/Controls/ZoomBorder.cs
@@ -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;
+ }
+ }
+ }
+ }
+}
diff --git a/DwgExtractorManual.csproj b/DwgExtractorManual.csproj
index c694940..878cdd4 100644
--- a/DwgExtractorManual.csproj
+++ b/DwgExtractorManual.csproj
@@ -6,30 +6,17 @@
True
enable
enable
+ x64
-
-
- 9
- 1
- 00020813-0000-0000-c000-000000000046
- 0
- tlbimp
- false
- true
-
-
+
-
-
-
-
- D:\dev_Net8_git\trunk\DLL\Teigha\vc16_amd64dll_23.12SP2\TD_Mgd_23.12_16.dll
- true
-
+
+
+
@@ -39,51 +26,33 @@
-
-
+
-
-
-
-
-
+
+ ..\..\..\GitNet8\trunk\DLL\Teigha\vc16_amd64dll_23.12SP2\TD_Mgd_23.12_16.dll
+
-
-
-
-
-
-
-
+
+
+ C:\Program Files (x86)\Microsoft Office\Office16\DCF\Microsoft.Office.Interop.Excel.dll
+ false
+
+
+ C:\Program Files (x86)\Microsoft Office\Office16\DCF\office.dll
+ false
+
+
-
-
-
-
+
+
+ PreserveNewest
+ fletimageanalysis\%(RecursiveDir)%(FileName)%(Extension)
+
+
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/IntersectionTestConsole.cs b/IntersectionTestConsole.cs
new file mode 100644
index 0000000..e95edce
--- /dev/null
+++ b/IntersectionTestConsole.cs
@@ -0,0 +1,173 @@
+//using System;
+//using System.Collections.Generic;
+//using System.Diagnostics;
+//using System.Linq;
+//using Teigha.Geometry;
+//using DwgExtractorManual.Models;
+
+//namespace DwgExtractorManual
+//{
+// ///
+// /// 콘솔에서 실행할 수 있는 교차점 테스트 프로그램
+// ///
+// 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();
+
+// // 수동으로 교차점 생성 (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 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 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 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;
+// }
+// }
+//}
\ No newline at end of file
diff --git a/MainWindow.Visualization.cs b/MainWindow.Visualization.cs
new file mode 100644
index 0000000..1b3356b
--- /dev/null
+++ b/MainWindow.Visualization.cs
@@ -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
+{
+ ///
+ /// MainWindow의 시각화 관련 기능을 담당하는 partial class
+ ///
+ public partial class MainWindow
+ {
+ // 시각화 데이터 저장
+ private static List _visualizationDataCache = new List();
+
+ ///
+ /// 테이블 셀 시각화 데이터를 저장합니다.
+ ///
+ public static void SaveVisualizationData(TableCellVisualizationData data)
+ {
+ _visualizationDataCache.Add(data);
+ Debug.WriteLine($"[VISUALIZATION] 시각화 데이터 저장: {data.FileName}, 셀 수: {data.Cells.Count}");
+ }
+
+ ///
+ /// 저장된 시각화 데이터를 가져옵니다.
+ ///
+ public static List GetVisualizationData()
+ {
+ Debug.WriteLine($"[VISUALIZATION] 시각화 데이터 조회: {_visualizationDataCache.Count}개 항목");
+ return new List(_visualizationDataCache);
+ }
+
+ ///
+ /// 시각화 데이터를 초기화합니다.
+ ///
+ public static void ClearVisualizationData()
+ {
+ Debug.WriteLine($"[VISUALIZATION] 시각화 데이터 초기화 (기존 {_visualizationDataCache.Count}개 항목 삭제)");
+ _visualizationDataCache.Clear();
+ Debug.WriteLine("[VISUALIZATION] 시각화 데이터 초기화 완료");
+ }
+
+ ///
+ /// 테이블 셀 시각화 창을 엽니다.
+ ///
+ 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);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/MainWindow.xaml b/MainWindow.xaml
index 7d58398..06a9591 100644
--- a/MainWindow.xaml
+++ b/MainWindow.xaml
@@ -210,6 +210,40 @@
+
+
@@ -234,6 +268,8 @@
+
+
diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs
index 212048a..b8f20f5 100644
--- a/MainWindow.xaml.cs
+++ b/MainWindow.xaml.cs
@@ -17,7 +17,7 @@ namespace DwgExtractorManual
{
public partial class MainWindow : Window
{
- private DispatcherTimer _timer;
+ private DispatcherTimer? _timer;
private ExportExcel? _exportExcel;
private SqlDatas? _sqlDatas;
// 자동 처리 모드 플래그
@@ -26,16 +26,76 @@ namespace DwgExtractorManual
public MainWindow()
{
InitializeComponent();
- InitializeDefaultPaths();
InitializeTimer();
+ LoadSettings();
+ SetBuildTime();
// 앱 종료 시 Teigha 리소스 정리
this.Closed += MainWindow_Closed;
-
- LogMessage("🚀 DWG 정보 추출기가 시작되었습니다.");
}
- private void MainWindow_Closed(object sender, EventArgs e)
+ private void LoadSettings()
+ {
+ LogMessage("⚙️ 설정을 불러옵니다...");
+ var settings = SettingsManager.LoadSettings();
+ if (settings != null)
+ {
+ if (!string.IsNullOrEmpty(settings.SourceFolderPath) && Directory.Exists(settings.SourceFolderPath))
+ {
+ txtSourceFolder.Text = settings.SourceFolderPath;
+ LogMessage($"📂 저장된 소스 폴더: {settings.SourceFolderPath}");
+ CheckDwgFiles(settings.SourceFolderPath);
+ }
+ else
+ {
+ LogMessage($"⚠️ 저장된 소스 폴더를 찾을 수 없습니다: {settings.SourceFolderPath}");
+ }
+
+ if (!string.IsNullOrEmpty(settings.DestinationFolderPath) && Directory.Exists(settings.DestinationFolderPath))
+ {
+ txtResultFolder.Text = settings.DestinationFolderPath;
+ LogMessage($"💾 저장된 결과 폴더: {settings.DestinationFolderPath}");
+ }
+ else
+ {
+ LogMessage($"⚠️ 저장된 결과 폴더를 찾을 수 없습니다: {settings.DestinationFolderPath}");
+ }
+
+ if (!string.IsNullOrEmpty(settings.LastExportType))
+ {
+ if (settings.LastExportType == "Excel")
+ {
+ rbExcel.IsChecked = true;
+ }
+ else if (settings.LastExportType == "Database")
+ {
+ rbDatabase.IsChecked = true;
+ }
+ LogMessage($"📋 저장된 출력 형식: {settings.LastExportType}");
+ }
+ LogMessage("✅ 설정 불러오기 완료.");
+ }
+ else
+ {
+ LogMessage("ℹ️ 저장된 설정 파일이 없습니다. 기본값으로 시작합니다.");
+ InitializeDefaultPaths(); // Fallback
+ }
+ }
+
+ private void SaveSettings()
+ {
+ LogMessage("⚙️ 현재 설정을 저장합니다...");
+ var settings = new AppSettings
+ {
+ SourceFolderPath = txtSourceFolder.Text,
+ DestinationFolderPath = txtResultFolder.Text,
+ LastExportType = rbExcel.IsChecked == true ? "Excel" : "Database"
+ };
+ SettingsManager.SaveSettings(settings);
+ LogMessage("✅ 설정 저장 완료.");
+ }
+
+ private void MainWindow_Closed(object? sender, EventArgs e)
{
try
{
@@ -70,7 +130,13 @@ namespace DwgExtractorManual
{
_timer = new DispatcherTimer();
_timer.Interval = TimeSpan.FromSeconds(1);
- _timer.Tick += (s, e) => txtTime.Text = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
+ _timer.Tick += (s, e) =>
+ {
+ if (_timer != null)
+ {
+ txtTime.Text = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
+ }
+ };
_timer.Start();
}
@@ -176,6 +242,9 @@ namespace DwgExtractorManual
private async void BtnExtract_Click(object sender, RoutedEventArgs e)
{
+ // 설정 저장
+ SaveSettings();
+
// 입력 유효성 검사
if (string.IsNullOrEmpty(txtSourceFolder.Text) || !Directory.Exists(txtSourceFolder.Text))
{
@@ -278,7 +347,7 @@ namespace DwgExtractorManual
}
else
{
- System.Windows.MessageBox.Show(message, title, button, image);
+ System.Windows.MessageBox.Show(this, message, title, button, image);
}
}
@@ -294,7 +363,7 @@ namespace DwgExtractorManual
}
else
{
- return System.Windows.MessageBox.Show(message, title, MessageBoxButton.YesNo, MessageBoxImage.Question);
+ return System.Windows.MessageBox.Show(this, message, title, MessageBoxButton.YesNo, MessageBoxImage.Question);
}
}
@@ -395,12 +464,11 @@ namespace DwgExtractorManual
LogMessage("💾 Excel 파일과 매핑 데이터를 저장합니다...");
- // 매핑 딕셔너리를 JSON 파일로 저장 (PDF 데이터 병합용)
- _exportExcel.SaveMappingDictionary(mappingDataFile);
+ _exportExcel?.SaveMappingDictionary(mappingDataFile);
LogMessage($"✅ 매핑 데이터 저장 완료: {Path.GetFileName(mappingDataFile)}");
// Excel 파일 저장
- _exportExcel.SaveAndCloseExcel(excelFileName);
+ _exportExcel?.SaveAndCloseExcel(excelFileName);
LogMessage($"✅ Excel 파일 저장 완료: {Path.GetFileName(excelFileName)}");
var elapsed = stopwatch.Elapsed;
@@ -461,7 +529,7 @@ namespace DwgExtractorManual
try
{
// 실제 DWG 처리 로직 (DwgToDB는 실패시 true 반환)
- bool failed = _sqlDatas.DwgToDB(file.FullName);
+ bool failed = _sqlDatas?.DwgToDB(file.FullName) ?? true;
bool success = !failed;
fileStopwatch.Stop();
@@ -777,6 +845,9 @@ namespace DwgExtractorManual
private async void BtnPdfExtract_Click(object sender, RoutedEventArgs e)
{
+ // 설정 저장
+ SaveSettings();
+
// 입력 유효성 검사
if (string.IsNullOrEmpty(txtSourceFolder.Text) || !Directory.Exists(txtSourceFolder.Text))
{
@@ -1092,7 +1163,12 @@ namespace DwgExtractorManual
LogMessage($"📄 JSON 파일 확인됨: {Path.GetFileName(jsonFilePath)}");
// 최신 매핑 데이터 파일 찾기
- string resultDir = Path.GetDirectoryName(csvFilePath) ?? txtResultFolder.Text;
+ string? resultDir = Path.GetDirectoryName(csvFilePath);
+ if (string.IsNullOrEmpty(resultDir))
+ {
+ LogMessage("⚠️ 결과 디렉터리를 찾을 수 없습니다.");
+ return;
+ }
var mappingDataFiles = Directory.GetFiles(resultDir, "*_mapping_data.json", SearchOption.TopDirectoryOnly)
.OrderByDescending(f => File.GetCreationTime(f))
.ToArray();
@@ -1168,7 +1244,12 @@ namespace DwgExtractorManual
LogMessage($"📄 JSON 파일 확인됨: {Path.GetFileName(jsonFilePath)}");
// 기존 Excel 매핑 파일 검색 (임시 파일 제외)
- string resultDir = Path.GetDirectoryName(csvFilePath) ?? txtResultFolder.Text;
+ string? resultDir = Path.GetDirectoryName(csvFilePath);
+ if (string.IsNullOrEmpty(resultDir))
+ {
+ LogMessage("⚠️ 결과 디렉터리를 찾을 수 없습니다.");
+ return;
+ }
var allExcelFiles = Directory.GetFiles(resultDir, "*_Mapping.xlsx", SearchOption.TopDirectoryOnly);
// 임시 파일(~$로 시작하는 파일) 필터링
@@ -1296,6 +1377,9 @@ namespace DwgExtractorManual
private async void BtnMerge_Click(object sender, RoutedEventArgs e)
{
+ // 설정 저장
+ SaveSettings();
+
// 입력 유효성 검사
if (string.IsNullOrEmpty(txtResultFolder.Text) || !Directory.Exists(txtResultFolder.Text))
{
@@ -1560,6 +1644,9 @@ namespace DwgExtractorManual
///
private async void BtnAuto_Click(object sender, RoutedEventArgs e)
{
+ // 설정 저장
+ SaveSettings();
+
try
{
// 입력 검증
@@ -1656,6 +1743,9 @@ namespace DwgExtractorManual
///
private async void BtnDwgOnly_Click(object sender, RoutedEventArgs e)
{
+ // 설정 저장
+ SaveSettings();
+
try
{
// 경로 검증
@@ -1738,6 +1828,13 @@ namespace DwgExtractorManual
///
private async void BtnDwgHeightSort_Click(object sender, RoutedEventArgs e)
{
+ // 설정 저장
+ SaveSettings();
+
+ // 시각화 데이터 초기화
+ ClearVisualizationData();
+ LogMessage("🧹 시각화 데이터 초기화 완료");
+
try
{
// 경로 검증
@@ -2057,8 +2154,8 @@ namespace DwgExtractorManual
if (allDwgFiles.Count > 0)
{
- // 단일 Excel 파일에 모든 DWG 파일 처리
- LogMessage("📏 단일 Height 정렬 Excel 파일 생성 중...");
+ // Height 정렬 Excel 파일 생성 (Note 데이터 포함)
+ LogMessage("📏 Height 정렬 Excel 파일 생성 중 (Note 표 데이터 포함)...");
await ProcessAllFilesDwgHeightSort(allDwgFiles, resultBaseFolder);
LogMessage("✅ Height 정렬 Excel 파일 생성 완료");
}
@@ -2109,7 +2206,91 @@ namespace DwgExtractorManual
string savePath = Path.Combine(resultFolder, $"{timestamp}_AllDWG_HeightSorted.xlsx");
exportExcel.ExportAllDwgToExcelHeightSorted(allDwgFiles, savePath);
-
+
+ // 시각화 데이터 캐시 초기화
+ MainWindow.ClearVisualizationData();
+ LogMessage("[DEBUG] 시각화 데이터 캐시 초기화 완료.");
+
+ foreach (var (filePath, folderName) in allDwgFiles)
+ {
+ LogMessage($"[DEBUG] DWG 파일에서 Note 추출 시작: {Path.GetFileName(filePath)}");
+ var noteExtractionResult = exportExcel.DwgExtractor.ExtractNotesFromDrawing(filePath);
+ var noteEntities = noteExtractionResult.NoteEntities;
+ LogMessage($"[DEBUG] 추출된 Note 엔티티 수: {noteEntities.Count}");
+ LogMessage($"[DEBUG] 추출된 IntersectionPoints 수: {noteExtractionResult.IntersectionPoints.Count}");
+ LogMessage($"[DEBUG] 추출된 DiagonalLines 수: {noteExtractionResult.DiagonalLines.Count}");
+ LogMessage($"[DEBUG] 추출된 TableSegments 수: {noteExtractionResult.TableSegments.Count}");
+
+ if (noteEntities.Any())
+ {
+ // 테이블이 있는 Note만 가시화 데이터 생성 (최소 4개 셀 이상)
+ var notesWithTables = noteEntities.Where(ne =>
+ ne.Type == "Note" &&
+ ne.Cells != null &&
+ ne.Cells.Count >= 4 && // 최소 4개 셀이 있어야 테이블로 인정
+ ne.TableSegments != null &&
+ ne.TableSegments.Count >= 4).ToList(); // 최소 4개 선분이 있어야 함
+
+ LogMessage($"[DEBUG] 테이블이 있는 Note: {notesWithTables.Count}개");
+
+ foreach (var noteWithTable in notesWithTables)
+ {
+ var visualizationData = new TableCellVisualizationData
+ {
+ FileName = $"{Path.GetFileName(filePath)} - {noteWithTable.Text}",
+ NoteText = noteWithTable.Text,
+ NoteBounds = (
+ noteWithTable.Cells.Min(c => c.MinPoint.X),
+ noteWithTable.Cells.Min(c => c.MinPoint.Y),
+ noteWithTable.Cells.Max(c => c.MaxPoint.X),
+ noteWithTable.Cells.Max(c => c.MaxPoint.Y)
+ ),
+ Cells = noteWithTable.Cells.Select(tc => new CellBounds
+ {
+ MinX = tc.MinPoint.X, MinY = tc.MinPoint.Y, MaxX = tc.MaxPoint.X, MaxY = tc.MaxPoint.Y,
+ Row = tc.Row, Column = tc.Column, Text = tc.CellText
+ }).ToList(),
+ TableSegments = noteWithTable.TableSegments.Select(ts => new SegmentInfo
+ {
+ StartX = ts.StartX, StartY = ts.StartY,
+ EndX = ts.EndX, EndY = ts.EndY,
+ IsHorizontal = ts.IsHorizontal
+ }).ToList(),
+ IntersectionPoints = noteWithTable.IntersectionPoints,
+ DiagonalLines = noteWithTable.DiagonalLines ?? new List(),
+ CellBoundaries = noteWithTable.CellBoundaries?.Select(cb => new CellBoundaryInfo
+ {
+ TopLeftX = cb.TopLeft.X, TopLeftY = cb.TopLeft.Y,
+ TopRightX = cb.TopRight.X, TopRightY = cb.TopRight.Y,
+ BottomLeftX = cb.BottomLeft.X, BottomLeftY = cb.BottomLeft.Y,
+ BottomRightX = cb.BottomRight.X, BottomRightY = cb.BottomRight.Y,
+ Label = cb.Label, Width = cb.Width, Height = cb.Height,
+ CellText = cb.CellText ?? ""
+ }).ToList() ?? new List(),
+ TextEntities = noteEntities.Where(ne => ne.Type == "NoteContent").Select(ne => new TextInfo
+ {
+ X = ne.X, Y = ne.Y, Text = ne.Text,
+ IsInTable = noteWithTable.Cells.Any(cell =>
+ ne.X >= cell.MinPoint.X && ne.X <= cell.MaxPoint.X &&
+ ne.Y >= cell.MinPoint.Y && ne.Y <= cell.MaxPoint.Y)
+ }).ToList()
+ };
+ MainWindow.SaveVisualizationData(visualizationData);
+ LogMessage($"[DEBUG] 테이블 Note 시각화 데이터 추가: {visualizationData.FileName} (셀: {visualizationData.Cells.Count}개)");
+ }
+ }
+ else
+ {
+ LogMessage($"[DEBUG] Note 엔티티가 없어 시각화 데이터를 생성하지 않습니다: {Path.GetFileName(filePath)}");
+ }
+ }
+
+ LogMessage($"[DEBUG] 총 {allDwgFiles.Count}개 파일의 시각화 데이터 저장 완료.");
+
+ // 최종 시각화 데이터 확인
+ var finalVisualizationData = MainWindow.GetVisualizationData();
+ LogMessage($"[DEBUG] 최종 저장된 시각화 데이터: {finalVisualizationData.Count}개 항목");
+
LogMessage("✅ Height 정렬 Excel 파일 생성 완료");
LogMessage($"📁 저장된 파일: {Path.GetFileName(savePath)}");
}
@@ -2388,5 +2569,298 @@ namespace DwgExtractorManual
_sqlDatas?.Dispose();
base.OnClosed(e);
}
+
+ ///
+ /// Note 추출 기능 테스트 메서드
+ ///
+ private async Task TestNoteExtraction()
+ {
+ try
+ {
+ LogMessage("🧪 === Note 추출 기능 테스트 시작 ===");
+
+ string sourceFolder = txtSourceFolder.Text;
+ string resultFolder = txtResultFolder.Text;
+
+ if (string.IsNullOrEmpty(sourceFolder) || !Directory.Exists(sourceFolder))
+ {
+ LogMessage("❌ 소스 폴더가 선택되지 않았거나 존재하지 않습니다.");
+ UpdateStatus("소스 폴더를 선택해주세요.");
+ return;
+ }
+
+ if (string.IsNullOrEmpty(resultFolder))
+ {
+ LogMessage("❌ 결과 폴더가 선택되지 않았습니다.");
+ UpdateStatus("결과 폴더를 선택해주세요.");
+ return;
+ }
+
+ // 결과 폴더가 없으면 생성
+ if (!Directory.Exists(resultFolder))
+ {
+ Directory.CreateDirectory(resultFolder);
+ LogMessage($"📁 결과 폴더 생성: {resultFolder}");
+ }
+
+ // DWG 파일 찾기
+ var dwgFiles = Directory.GetFiles(sourceFolder, "*.dwg", SearchOption.AllDirectories);
+ LogMessage($"📊 발견된 DWG 파일 수: {dwgFiles.Length}개");
+
+ if (dwgFiles.Length == 0)
+ {
+ LogMessage("⚠️ DWG 파일이 없습니다.");
+ UpdateStatus("DWG 파일이 없습니다.");
+ return;
+ }
+
+ UpdateStatus("🔧 Note 추출 중...");
+ progressBar.Maximum = dwgFiles.Length;
+ progressBar.Value = 0;
+
+ // Teigha 서비스 초기화
+ LogMessage("🔧 Teigha 서비스 초기화 중...");
+ TeighaServicesManager.Instance.AcquireServices();
+
+ // 간단한 빈 매핑 데이터로 FieldMapper 생성 (테스트용)
+ var mappingData = new MappingTableData();
+ var fieldMapper = new FieldMapper(mappingData);
+ var extractor = new DwgDataExtractor(fieldMapper);
+ var csvWriter = new CsvDataWriter();
+
+ int processedCount = 0;
+ int successCount = 0;
+ int failureCount = 0;
+
+ // 각 DWG 파일에 대해 Note 추출 테스트
+ foreach (string dwgFile in dwgFiles.Take(3)) // 처음 3개 파일만 테스트
+ {
+ try
+ {
+ string fileName = Path.GetFileNameWithoutExtension(dwgFile);
+ LogMessage($"🔍 [{processedCount + 1}/{Math.Min(3, dwgFiles.Length)}] Note 추출 중: {fileName}");
+
+ // Note 데이터 추출
+ var noteExtractionResult = extractor.ExtractNotesFromDrawing(dwgFile);
+ var noteEntities = noteExtractionResult.NoteEntities;
+
+ // 테이블이 있는 Note만 시각화 데이터 저장
+ if (noteEntities.Any())
+ {
+ var notesWithTables = noteEntities.Where(ne =>
+ ne.Type == "Note" &&
+ ne.Cells != null &&
+ ne.Cells.Count >= 4 && // 최소 4개 셀이 있어야 테이블로 인정
+ ne.TableSegments != null &&
+ ne.TableSegments.Count >= 4).ToList(); // 최소 4개 선분이 있어야 함
+
+ LogMessage($" 테이블이 있는 Note: {notesWithTables.Count}개");
+
+ foreach (var noteWithTable in notesWithTables)
+ {
+ var visualizationData = new TableCellVisualizationData
+ {
+ FileName = $"{Path.GetFileName(dwgFile)} - {noteWithTable.Text}",
+ NoteText = noteWithTable.Text,
+ NoteBounds = (
+ noteWithTable.Cells.Min(c => c.MinPoint.X),
+ noteWithTable.Cells.Min(c => c.MinPoint.Y),
+ noteWithTable.Cells.Max(c => c.MaxPoint.X),
+ noteWithTable.Cells.Max(c => c.MaxPoint.Y)
+ ),
+ Cells = noteWithTable.Cells.Select(tc => new CellBounds
+ {
+ MinX = tc.MinPoint.X, MinY = tc.MinPoint.Y, MaxX = tc.MaxPoint.X, MaxY = tc.MaxPoint.Y,
+ Row = tc.Row, Column = tc.Column, Text = tc.CellText
+ }).ToList(),
+ TableSegments = noteWithTable.TableSegments.Select(ts => new SegmentInfo
+ {
+ StartX = ts.StartX, StartY = ts.StartY,
+ EndX = ts.EndX, EndY = ts.EndY,
+ IsHorizontal = ts.IsHorizontal
+ }).ToList(),
+ IntersectionPoints = noteWithTable.IntersectionPoints,
+ DiagonalLines = noteExtractionResult.DiagonalLines.Where(dl =>
+ noteWithTable.Cells.Any(cell =>
+ (dl.Item1.X >= cell.MinPoint.X && dl.Item1.X <= cell.MaxPoint.X &&
+ dl.Item1.Y >= cell.MinPoint.Y && dl.Item1.Y <= cell.MaxPoint.Y) ||
+ (dl.Item2.X >= cell.MinPoint.X && dl.Item2.X <= cell.MaxPoint.X &&
+ dl.Item2.Y >= cell.MinPoint.Y && dl.Item2.Y <= cell.MaxPoint.Y))).Select(dl => new DiagonalLine
+ {
+ StartX = dl.Item1.X, StartY = dl.Item1.Y, EndX = dl.Item2.X, EndY = dl.Item2.Y, Label = dl.Item3
+ }).ToList(),
+ CellBoundaries = noteWithTable.CellBoundaries?.Select(cb => new CellBoundaryInfo
+ {
+ TopLeftX = cb.TopLeft.X, TopLeftY = cb.TopLeft.Y,
+ TopRightX = cb.TopRight.X, TopRightY = cb.TopRight.Y,
+ BottomLeftX = cb.BottomLeft.X, BottomLeftY = cb.BottomLeft.Y,
+ BottomRightX = cb.BottomRight.X, BottomRightY = cb.BottomRight.Y,
+ Label = cb.Label, Width = cb.Width, Height = cb.Height,
+ CellText = cb.CellText ?? ""
+ }).ToList() ?? new List(),
+ TextEntities = noteEntities.Where(ne => ne.Type == "NoteContent").Select(ne => new TextInfo
+ {
+ X = ne.X, Y = ne.Y, Text = ne.Text,
+ IsInTable = noteWithTable.Cells.Any(cell =>
+ ne.X >= cell.MinPoint.X && ne.X <= cell.MaxPoint.X &&
+ ne.Y >= cell.MinPoint.Y && ne.Y <= cell.MaxPoint.Y)
+ }).ToList()
+ };
+ MainWindow.SaveVisualizationData(visualizationData);
+ LogMessage($" ✅ 테이블 Note 시각화 데이터 저장: {visualizationData.FileName} (셀: {visualizationData.Cells.Count}개)");
+ }
+ }
+
+ LogMessage($" 추출된 엔터티: {noteEntities.Count}개");
+
+ var noteCount = noteEntities.Count(ne => ne.Type == "Note");
+ var contentCount = noteEntities.Count(ne => ne.Type == "NoteContent");
+ var tableCount = noteEntities.Count(ne => ne.Type == "Note" && !string.IsNullOrEmpty(ne.TableCsv));
+
+ LogMessage($" - Note 헤더: {noteCount}개");
+ LogMessage($" - Note 콘텐츠: {contentCount}개");
+ LogMessage($" - 테이블 포함 Note: {tableCount}개");
+
+ // CSV 파일 생성
+ if (noteEntities.Count > 0)
+ {
+ // Note 박스 텍스트 CSV
+ var noteTextCsvPath = Path.Combine(resultFolder, $"{fileName}_note_texts.csv");
+ csvWriter.WriteNoteBoxTextToCsv(noteEntities, noteTextCsvPath);
+ LogMessage($" ✅ Note 텍스트 CSV 저장: {Path.GetFileName(noteTextCsvPath)}");
+
+ // Note 테이블 CSV
+ var noteTableCsvPath = Path.Combine(resultFolder, $"{fileName}_note_tables.csv");
+ csvWriter.WriteNoteTablesToCsv(noteEntities, noteTableCsvPath);
+ LogMessage($" ✅ Note 테이블 CSV 저장: {Path.GetFileName(noteTableCsvPath)}");
+
+ // 통합 CSV
+ var combinedCsvPath = Path.Combine(resultFolder, $"{fileName}_note_combined.csv");
+ csvWriter.WriteNoteDataToCombinedCsv(noteEntities, combinedCsvPath);
+ LogMessage($" ✅ 통합 CSV 저장: {Path.GetFileName(combinedCsvPath)}");
+
+ // 개별 테이블 CSV
+ if (tableCount > 0)
+ {
+ var individualTablesDir = Path.Combine(resultFolder, $"{fileName}_individual_tables");
+ csvWriter.WriteIndividualNoteTablesCsv(noteEntities, individualTablesDir);
+ LogMessage($" ✅ 개별 테이블 CSV 저장: {Path.GetFileName(individualTablesDir)}");
+ }
+
+ // 통계 CSV
+ var statisticsCsvPath = Path.Combine(resultFolder, $"{fileName}_note_statistics.csv");
+ csvWriter.WriteNoteStatisticsToCsv(noteEntities, statisticsCsvPath);
+ LogMessage($" ✅ 통계 CSV 저장: {Path.GetFileName(statisticsCsvPath)}");
+
+ // 첫 번째 테이블이 있는 Note의 내용 출력 (디버깅용)
+ var firstTableNote = noteEntities.FirstOrDefault(ne => ne.Type == "Note" && !string.IsNullOrEmpty(ne.TableCsv));
+ if (firstTableNote != null)
+ {
+ LogMessage($" 📋 첫 번째 테이블 Note: '{firstTableNote.Text}'");
+ var tableLines = firstTableNote.TableCsv.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
+ LogMessage($" 📋 테이블 행 수: {tableLines.Length}");
+ for (int i = 0; i < Math.Min(3, tableLines.Length); i++)
+ {
+ var line = tableLines[i];
+ if (line.Length > 60) line = line.Substring(0, 60) + "...";
+ LogMessage($" 행 {i + 1}: {line}");
+ }
+ }
+ }
+
+ successCount++;
+ LogMessage($" ✅ 성공");
+ }
+ catch (Exception ex)
+ {
+ failureCount++;
+ LogMessage($" ❌ 실패: {ex.Message}");
+ }
+
+ processedCount++;
+ progressBar.Value = processedCount;
+
+ // UI 업데이트를 위한 지연
+ await Task.Delay(100);
+ }
+
+ LogMessage($"🧪 === Note 추출 테스트 완료 ===");
+ LogMessage($"📊 처리 결과: 성공 {successCount}개, 실패 {failureCount}개");
+ LogMessage($"💾 결과 파일들이 저장되었습니다: {resultFolder}");
+
+ UpdateStatus($"Note 추출 테스트 완료: 성공 {successCount}개, 실패 {failureCount}개");
+
+ // 결과 폴더 열기
+ if (successCount > 0)
+ {
+ try
+ {
+ System.Diagnostics.Process.Start("explorer.exe", resultFolder);
+ }
+ catch { }
+ }
+ }
+ catch (Exception ex)
+ {
+ LogMessage($"❌ Note 추출 테스트 중 오류: {ex.Message}");
+ UpdateStatus("Note 추출 테스트 중 오류 발생");
+ throw;
+ }
+ finally
+ {
+ try
+ {
+ TeighaServicesManager.Instance.ForceDisposeServices();
+ LogMessage("🔄 Teigha 서비스 정리 완료");
+ }
+ catch (Exception ex)
+ {
+ LogMessage($"⚠️ Teigha 서비스 정리 중 오류: {ex.Message}");
+ }
+ }
+ }
+
+ ///
+ /// 빌드 시간을 상태바에 표시합니다.
+ ///
+ private void SetBuildTime()
+ {
+ try
+ {
+ // 현재 실행 파일의 빌드 시간을 가져옵니다
+ var assembly = System.Reflection.Assembly.GetExecutingAssembly();
+ var buildDate = System.IO.File.GetLastWriteTime(assembly.Location);
+ txtBuildTime.Text = $"빌드: {buildDate:yyyy-MM-dd HH:mm}";
+ }
+ catch (Exception ex)
+ {
+ txtBuildTime.Text = "빌드: 알 수 없음";
+ LogMessage($"⚠️ 빌드 시간 조회 오류: {ex.Message}");
+ }
+ }
+
+ ///
+ /// 교차점 테스트 버튼 클릭 이벤트
+ ///
+ private void BtnTestIntersection_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ LogMessage("🔬 교차점 생성 테스트 시작...");
+ UpdateStatus("교차점 테스트 중...");
+
+ // 테스트 실행
+ IntersectionTestDebugger.RunIntersectionTest();
+
+ LogMessage("✅ 교차점 테스트 완료 - Debug 창을 확인하세요");
+ UpdateStatus("교차점 테스트 완료");
+ }
+ catch (Exception ex)
+ {
+ LogMessage($"❌ 교차점 테스트 중 오류: {ex.Message}");
+ UpdateStatus("교차점 테스트 중 오류 발생");
+ }
+ }
+
}
}
diff --git a/Models/AppSettings.cs b/Models/AppSettings.cs
index e69de29..0767e6d 100644
--- a/Models/AppSettings.cs
+++ b/Models/AppSettings.cs
@@ -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; }
+ }
+}
diff --git a/Models/CsvDataWriter.cs b/Models/CsvDataWriter.cs
new file mode 100644
index 0000000..d296e83
--- /dev/null
+++ b/Models/CsvDataWriter.cs
@@ -0,0 +1,261 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace DwgExtractorManual.Models
+{
+ ///
+ /// Note 데이터를 CSV 파일로 출력하는 클래스
+ /// Note Box 안의 일반 텍스트와 테이블 텍스트를 분리하여 CSV로 출력
+ ///
+ public class CsvDataWriter
+ {
+ ///
+ /// Note 박스 안의 일반 텍스트들을 CSV 파일로 저장
+ ///
+ public void WriteNoteBoxTextToCsv(List noteEntities, string filePath)
+ {
+ if (noteEntities == null || noteEntities.Count == 0)
+ return;
+
+ var csvLines = new List();
+
+ // 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);
+ }
+
+ ///
+ /// Note 박스 안의 테이블 데이터들을 별도 CSV 파일로 저장
+ ///
+ public void WriteNoteTablesToCsv(List noteEntities, string filePath)
+ {
+ if (noteEntities == null || noteEntities.Count == 0)
+ return;
+
+ var allCsvLines = new List();
+
+ // 테이블 데이터가 있는 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);
+ }
+
+ ///
+ /// Note 박스와 테이블 데이터를 통합하여 하나의 CSV 파일로 저장
+ ///
+ public void WriteNoteDataToCombinedCsv(List noteEntities, string filePath)
+ {
+ if (noteEntities == null || noteEntities.Count == 0)
+ return;
+
+ var csvLines = new List();
+
+ // 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);
+ }
+
+ ///
+ /// 각 Note별로 개별 CSV 파일 생성 (테이블이 있는 경우)
+ ///
+ public void WriteIndividualNoteTablesCsv(List 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++;
+ }
+ }
+
+ ///
+ /// CSV 필드에서 특수문자를 이스케이프 처리
+ ///
+ 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;
+ }
+
+ ///
+ /// 파일명에 사용할 수 없는 문자들을 제거하여 안전한 파일명 생성
+ ///
+ 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();
+ }
+
+ ///
+ /// Note 데이터 통계 정보를 CSV로 저장
+ ///
+ public void WriteNoteStatisticsToCsv(List noteEntities, string filePath)
+ {
+ if (noteEntities == null || noteEntities.Count == 0)
+ return;
+
+ var csvLines = new List();
+
+ // 통계 헤더
+ 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Models/DwgDataExtractor.cs b/Models/DwgDataExtractor.cs
index 8d18d33..afc8681 100644
--- a/Models/DwgDataExtractor.cs
+++ b/Models/DwgDataExtractor.cs
@@ -339,8 +339,11 @@ namespace DwgExtractorManual.Models
///
/// 도면에서 Note와 관련된 텍스트들을 추출합니다.
///
- public List ExtractNotesFromDrawing(string filePath)
+ public NoteExtractionResult ExtractNotesFromDrawing(string filePath)
{
+ var result = new NoteExtractionResult();
+
+
var noteEntities = new List();
try
@@ -396,6 +399,17 @@ namespace DwgExtractorManual.Models
{
using (var noteText = tran.GetObject(noteTextId, OpenMode.ForRead) as DBText)
{
+ if (noteText == null)
+ {
+ Debug.WriteLine($"[DEBUG] Skipping null noteText for ObjectId: {noteTextId}");
+ continue;
+ }
+ // 특정 노트만 테스트하기 위한 필터 (디버깅용)
+ // if (noteText == null || !noteText.TextString.Contains("도로용지경계 기준 노트"))
+ // {
+ // continue;
+ // }
+
Debug.WriteLine($"[DEBUG] Note 처리 중: '{noteText.TextString}' at {noteText.Position}");
// 이 Note에 대한 그룹 생성
@@ -411,9 +425,18 @@ namespace DwgExtractorManual.Models
// 사용된 박스로 등록
usedBoxes.Add(noteBox.Value);
- // 박스 내부의 텍스트들 찾기
- var boxTextIds = FindTextsInNoteBox(tran, noteText, noteBox.Value, dbTextIds);
- Debug.WriteLine($"[DEBUG] 박스 내 텍스트: {boxTextIds.Count}개");
+ // 테이블과 일반 텍스트를 구분하여 추출
+ var (tableData, cells, nonTableTextIds, tableSegments, intersectionPoints, diagonalLines, cellBoundaries) = ExtractTableAndTextsFromNoteBox(tran, noteText, noteBox.Value, polylineIds, lineIds, dbTextIds, database);
+
+ Debug.WriteLine($"[EXCEL_DEBUG] Note '{noteText.TextString}' CellBoundaries 개수: {cellBoundaries?.Count ?? 0}");
+ if (cellBoundaries != null && cellBoundaries.Count > 0)
+ {
+ Debug.WriteLine($"[EXCEL_DEBUG] CellBoundaries 샘플 (처음 3개):");
+ foreach (var cb in cellBoundaries.Take(3))
+ {
+ Debug.WriteLine($"[EXCEL_DEBUG] {cb.Label}: '{cb.CellText}'");
+ }
+ }
// Note 자체를 그룹의 첫 번째로 추가
currentNoteGroup.Add(new NoteEntityInfo
@@ -425,12 +448,18 @@ namespace DwgExtractorManual.Models
FileName = Path.GetFileName(database.Filename),
X = noteText.Position.X,
Y = noteText.Position.Y,
- SortOrder = 0 // Note는 항상 먼저
+ SortOrder = 0, // Note는 항상 먼저
+ TableData = tableData, // 테이블 데이터 추가
+ Cells = cells, // 셀 정보 추가 (병합용)
+ TableSegments = tableSegments.Select(s => new SegmentInfo { StartX = s.start.X, StartY = s.start.Y, EndX = s.end.X, EndY = s.end.Y, IsHorizontal = s.isHorizontal }).ToList(), // 테이블 세그먼트 추가
+ IntersectionPoints = intersectionPoints.Select(ip => new IntersectionInfo { X = ip.Position.X, Y = ip.Position.Y, DirectionBits = ip.DirectionBits, Row = ip.Row, Column = ip.Column }).ToList(), // 교차점 추가
+ DiagonalLines = diagonalLines, // 대각선 추가
+ CellBoundaries = cellBoundaries // 정확한 셀 경계 추가
});
- // 박스 내 텍스트들을 좌표별로 정렬하여 그룹에 추가
- var sortedBoxTexts = GetSortedNoteContents(tran, boxTextIds, database);
- currentNoteGroup.AddRange(sortedBoxTexts);
+ // 테이블 외부의 일반 텍스트들을 좌표별로 정렬하여 그룹에 추가
+ var sortedNonTableTexts = GetSortedNoteContents(tran, nonTableTextIds, database);
+ currentNoteGroup.AddRange(sortedNonTableTexts);
}
else
{
@@ -449,16 +478,21 @@ namespace DwgExtractorManual.Models
SortOrder = 0
});
}
-
+
+ Debug.WriteLine($"[DEBUG] currentNoteGroup size before adding to noteGroups: {currentNoteGroup.Count}");
noteGroups.Add(currentNoteGroup);
}
}
+ Debug.WriteLine($"[DEBUG] noteGroups size before sorting: {noteGroups.Count}");
+
// Note 그룹들을 Y 좌표별로 정렬 (위에서 아래로)
var sortedNoteGroups = noteGroups
.OrderByDescending(group => group[0].Y) // 각 그룹의 첫 번째 항목(NOTE 헤더)의 Y 좌표로 정렬
.ToList();
+ Debug.WriteLine($"[DEBUG] sortedNoteGroups size before adding to noteEntities: {sortedNoteGroups.Count}");
+
// 정렬된 그룹들을 하나의 리스트로 합치기
foreach (var group in sortedNoteGroups)
{
@@ -476,7 +510,11 @@ namespace DwgExtractorManual.Models
Debug.WriteLine($"[DEBUG] 최종 Note 엔티티 정렬 완료: {noteEntities.Count}개");
- return noteEntities;
+ result.NoteEntities = noteEntities;
+ result.IntersectionPoints = LastIntersectionPoints.Select(ip => new IntersectionPoint { Position = ip.Position, DirectionBits = ip.DirectionBits, Row = ip.Row, Column = ip.Column }).ToList();
+ result.DiagonalLines = LastDiagonalLines;
+
+ return result;
}
///
@@ -928,6 +966,888 @@ namespace DwgExtractorManual.Models
return boxTextIds;
}
+ ///
+ /// Note 박스 내부의 Line과 Polyline들을 찾습니다.
+ ///
+ private List FindLinesInNoteBox(
+ Transaction tran, (Point3d minPoint, Point3d maxPoint) noteBox, List polylineIds, List lineIds)
+ {
+ var boxLineIds = new List();
+
+ // Polyline 엔티티들 검사
+ foreach (var polylineId in polylineIds)
+ {
+ using (var polyline = tran.GetObject(polylineId, OpenMode.ForRead) as Polyline)
+ {
+ if (polyline == null) continue;
+
+ // Polyline의 모든 점이 박스 내부에 있는지 확인
+ bool isInsideBox = true;
+ for (int i = 0; i < polyline.NumberOfVertices; i++)
+ {
+ var point = polyline.GetPoint3dAt(i);
+ if (point.X < noteBox.minPoint.X || point.X > noteBox.maxPoint.X ||
+ point.Y < noteBox.minPoint.Y || point.Y > noteBox.maxPoint.Y)
+ {
+ isInsideBox = false;
+ break;
+ }
+ }
+
+ if (isInsideBox)
+ {
+ boxLineIds.Add(polylineId);
+ Debug.WriteLine($"[DEBUG] 박스 내 Polyline 발견: {polyline.NumberOfVertices}개 점, Layer: {GetLayerName(polyline.LayerId, tran, polyline.Database)}");
+ }
+ }
+ }
+
+ // Line 엔티티들 검사
+ foreach (var lineId in lineIds)
+ {
+ using (var line = tran.GetObject(lineId, OpenMode.ForRead) as Line)
+ {
+ if (line == null) continue;
+
+ var startPoint = line.StartPoint;
+ var endPoint = line.EndPoint;
+
+ // Line의 시작점과 끝점이 모두 박스 내부에 있는지 확인
+ bool startInside = startPoint.X >= noteBox.minPoint.X && startPoint.X <= noteBox.maxPoint.X &&
+ startPoint.Y >= noteBox.minPoint.Y && startPoint.Y <= noteBox.maxPoint.Y;
+ bool endInside = endPoint.X >= noteBox.minPoint.X && endPoint.X <= noteBox.maxPoint.X &&
+ endPoint.Y >= noteBox.minPoint.Y && endPoint.Y <= noteBox.maxPoint.Y;
+
+ if (startInside && endInside)
+ {
+ boxLineIds.Add(lineId);
+ Debug.WriteLine($"[DEBUG] 박스 내 Line 발견: ({startPoint.X:F2},{startPoint.Y:F2}) to ({endPoint.X:F2},{endPoint.Y:F2}), Layer: {GetLayerName(line.LayerId, tran, line.Database)}");
+ }
+ }
+ }
+
+ Debug.WriteLine($"[DEBUG] 박스 내 Line/Polyline 총 {boxLineIds.Count}개 발견");
+ return boxLineIds;
+ }
+
+ ///
+ /// 박스 내부의 Line/Polyline에서 테이블을 구성하는 수평·수직 세그먼트들을 찾습니다.
+ ///
+ private List<(Point3d start, Point3d end, bool isHorizontal)> FindTableSegmentsInBox(
+ Transaction tran, List boxLineIds, double noteHeight)
+ {
+ var tableSegments = new List<(Point3d start, Point3d end, bool isHorizontal)>();
+ double tolerance = noteHeight * 0.1; // 수평/수직 판단 허용 오차
+
+ foreach (var lineId in boxLineIds)
+ {
+ using (var entity = tran.GetObject(lineId, OpenMode.ForRead) as Entity)
+ {
+ if (entity == null) continue;
+
+ // Line 엔티티 처리
+ if (entity is Line line)
+ {
+ var start = line.StartPoint;
+ var end = line.EndPoint;
+
+ // 수평선인지 확인
+ if (Math.Abs(start.Y - end.Y) < tolerance)
+ {
+ tableSegments.Add((start, end, true)); // 수평
+ Debug.WriteLine($"[DEBUG] 테이블 수평선: ({start.X:F2},{start.Y:F2}) to ({end.X:F2},{end.Y:F2})");
+ }
+ // 수직선인지 확인
+ else if (Math.Abs(start.X - end.X) < tolerance)
+ {
+ tableSegments.Add((start, end, false)); // 수직
+ Debug.WriteLine($"[DEBUG] 테이블 수직선: ({start.X:F2},{start.Y:F2}) to ({end.X:F2},{end.Y:F2})");
+ }
+ }
+ // Polyline 엔티티 처리 - 각 세그먼트별로 검사
+ else if (entity is Polyline polyline)
+ {
+ for (int i = 0; i < polyline.NumberOfVertices - 1; i++)
+ {
+ var start = polyline.GetPoint3dAt(i);
+ var end = polyline.GetPoint3dAt(i + 1);
+
+ // 수평 세그먼트인지 확인
+ if (Math.Abs(start.Y - end.Y) < tolerance)
+ {
+ tableSegments.Add((start, end, true)); // 수평
+ Debug.WriteLine($"[DEBUG] 테이블 수평 세그먼트: ({start.X:F2},{start.Y:F2}) to ({end.X:F2},{end.Y:F2})");
+ }
+ // 수직 세그먼트인지 확인
+ else if (Math.Abs(start.X - end.X) < tolerance)
+ {
+ tableSegments.Add((start, end, false)); // 수직
+ Debug.WriteLine($"[DEBUG] 테이블 수직 세그먼트: ({start.X:F2},{start.Y:F2}) to ({end.X:F2},{end.Y:F2})");
+ }
+ }
+
+ // 닫힌 Polyline인 경우 마지막과 첫 번째 점 사이의 세그먼트도 검사
+ if (polyline.Closed && polyline.NumberOfVertices > 2)
+ {
+ var start = polyline.GetPoint3dAt(polyline.NumberOfVertices - 1);
+ var end = polyline.GetPoint3dAt(0);
+
+ if (Math.Abs(start.Y - end.Y) < tolerance)
+ {
+ tableSegments.Add((start, end, true)); // 수평
+ Debug.WriteLine($"[DEBUG] 테이블 수평 세그먼트(닫힘): ({start.X:F2},{start.Y:F2}) to ({end.X:F2},{end.Y:F2})");
+ }
+ else if (Math.Abs(start.X - end.X) < tolerance)
+ {
+ tableSegments.Add((start, end, false)); // 수직
+ Debug.WriteLine($"[DEBUG] 테이블 수직 세그먼트(닫힘): ({start.X:F2},{start.Y:F2}) to ({end.X:F2},{end.Y:F2})");
+ }
+ }
+ }
+ }
+ }
+
+ Debug.WriteLine($"[DEBUG] 테이블 세그먼트 총 {tableSegments.Count}개 발견 (수평: {tableSegments.Count(s => s.isHorizontal)}, 수직: {tableSegments.Count(s => !s.isHorizontal)})");
+ return tableSegments;
+ }
+
+ ///
+ /// 새로운 교차점 기반 알고리즘으로 셀들을 추출합니다.
+ ///
+ private List ExtractTableCells(List<(Point3d start, Point3d end, bool isHorizontal)> tableSegments, double tolerance)
+ {
+ // 새로운 교차점 기반 알고리즘 사용
+ var intersectionPoints = FindAndClassifyIntersections(tableSegments, tolerance);
+ var cells = ExtractCellsFromIntersections(intersectionPoints, tableSegments, tolerance);
+
+ // 교차점 정보를 멤버 변수에 저장하여 시각화에서 사용
+ LastIntersectionPoints = intersectionPoints;
+
+ return cells;
+ }
+
+ // 시각화를 위한 교차점 정보 저장
+ public List LastIntersectionPoints { get; private set; } = new List();
+
+ // 시각화를 위한 대각선 정보 저장
+ public List<(Point3d topLeft, Point3d bottomRight, string label)> LastDiagonalLines { get; private set; } = new List<(Point3d, Point3d, string)>();
+
+ ///
+ /// 교차점들을 찾고 타입을 분류합니다.
+ ///
+ public List CalculateIntersectionPointsFromSegments(List<(Point3d start, Point3d end, bool isHorizontal)> tableSegments, double tolerance)
+ {
+ return FindAndClassifyIntersections(tableSegments, tolerance);
+ }
+
+ private List FindAndClassifyIntersections(List<(Point3d start, Point3d end, bool isHorizontal)> tableSegments, double tolerance)
+ {
+ var intersectionPoints = new List();
+
+ // 1. 실제 교차점들 찾기
+ var rawIntersections = FindRealIntersections(tableSegments, tolerance);
+
+ foreach (var intersection in rawIntersections)
+ {
+ // 2. 각 교차점에서 연결된 선분들의 방향을 비트 플래그로 분석
+ var directionBits = GetDirectionBitsAtPoint(intersection, tableSegments, tolerance);
+
+ intersectionPoints.Add(new IntersectionPoint
+ {
+ Position = intersection,
+ DirectionBits = directionBits
+ });
+ }
+
+ Debug.WriteLine($"[DEBUG] 교차점 분류 완료: {intersectionPoints.Count}개");
+ foreach (var ip in intersectionPoints)
+ {
+ Debug.WriteLine($"[DEBUG] {ip}");
+ }
+
+ // 교차점 방향별 개수 출력
+ var directionGroups = intersectionPoints.GroupBy(p => p.DirectionBits);
+ foreach (var group in directionGroups)
+ {
+ Debug.WriteLine($"[DEBUG] 방향 {group.Key}: {group.Count()}개");
+ }
+
+ return intersectionPoints;
+ }
+
+ ///
+ /// 교차점에서 연결된 선분들의 방향을 비트 플래그로 분석합니다.
+ ///
+ private int GetDirectionBitsAtPoint(Point3d point, List<(Point3d start, Point3d end, bool isHorizontal)> segments, double tolerance)
+ {
+ int directionBits = 0;
+
+ foreach (var segment in segments)
+ {
+ // 점이 선분의 시작점, 끝점, 또는 중간점인지 확인
+ bool isOnSegment = IsPointOnSegment(segment, point, tolerance);
+ if (!isOnSegment) continue;
+
+ // 점에서 선분의 방향 결정
+ if (segment.isHorizontal)
+ {
+ // 수평선: 점을 기준으로 왼쪽/오른쪽 방향
+ if (segment.start.X < point.X - tolerance || segment.end.X < point.X - tolerance)
+ directionBits |= DirectionFlags.Left;
+ if (segment.start.X > point.X + tolerance || segment.end.X > point.X + tolerance)
+ directionBits |= DirectionFlags.Right;
+ }
+ else
+ {
+ // 수직선: 점을 기준으로 위/아래 방향
+ if (segment.start.Y > point.Y + tolerance || segment.end.Y > point.Y + tolerance)
+ directionBits |= DirectionFlags.Up;
+ if (segment.start.Y < point.Y - tolerance || segment.end.Y < point.Y - tolerance)
+ directionBits |= DirectionFlags.Down;
+ }
+ }
+
+ return directionBits;
+ }
+
+ ///
+ /// 실제 교차점들로부터 완전한 격자망을 생성합니다.
+ /// 빈 위치에도 가상 교차점을 생성하여 균일한 RXCX 구조를 만듭니다.
+ ///
+ private List CreateCompleteGridFromIntersections(List actualIntersections, List<(Point3d start, Point3d end, bool isHorizontal)> tableSegments, double tolerance)
+ {
+ if (actualIntersections.Count == 0) return new List();
+
+ // 1. 전체 테이블에서 유니크한 Y좌표들을 찾아서 Row 번호 매핑 생성
+ var uniqueYCoords = actualIntersections
+ .Select(i => Math.Round(i.Position.Y, 1))
+ .Distinct()
+ .OrderByDescending(y => y) // 위에서 아래로 (Y값이 큰 것부터)
+ .ToList();
+
+ var yToRowMap = uniqueYCoords
+ .Select((y, index) => new { Y = y, Row = index + 1 }) // 1-based 인덱싱
+ .ToDictionary(item => item.Y, item => item.Row);
+
+ // 2. 전체 테이블에서 유니크한 X좌표들을 찾아서 Column 번호 매핑 생성
+ var uniqueXCoords = actualIntersections
+ .Select(i => Math.Round(i.Position.X, 1))
+ .Distinct()
+ .OrderBy(x => x) // 왼쪽에서 오른쪽으로
+ .ToList();
+
+ var xToColumnMap = uniqueXCoords
+ .Select((x, index) => new { X = x, Column = index + 1 }) // 1-based 인덱싱
+ .ToDictionary(item => item.X, item => item.Column);
+
+ // 3. 완전한 격자망 생성 (모든 Row x Column 조합)
+ var completeGrid = new List();
+ var actualIntersectionLookup = actualIntersections
+ .GroupBy(i => (Math.Round(i.Position.Y, 1), Math.Round(i.Position.X, 1)))
+ .ToDictionary(g => g.Key, g => g.First());
+
+ for (int row = 0; row < uniqueYCoords.Count; row++)
+ {
+ for (int col = 0; col < uniqueXCoords.Count; col++)
+ {
+ var y = uniqueYCoords[row];
+ var x = uniqueXCoords[col];
+ var key = (y, x);
+
+ if (actualIntersectionLookup.ContainsKey(key))
+ {
+ // 실제 교차점이 존재하는 경우
+ var actual = actualIntersectionLookup[key];
+ actual.Row = row + 1; // 1-based 인덱싱
+ actual.Column = col + 1; // 1-based 인덱싱
+ completeGrid.Add(actual);
+ }
+ else
+ {
+ // 가상 교차점 생성 (연장된 선의 교차점이므로 DirectionBits = 0)
+ var virtualIntersection = new IntersectionPoint
+ {
+ Position = new Point3d(x, y, 0),
+ DirectionBits = InferDirectionBitsForVirtualIntersection(x, y, tableSegments, tolerance),
+ Row = row + 1, // 1-based 인덱싱
+ Column = col + 1 // 1-based 인덱싱
+ };
+ completeGrid.Add(virtualIntersection);
+ }
+ }
+ }
+
+ Debug.WriteLine($"[DEBUG] 완전한 격자망 생성 완료:");
+ Debug.WriteLine($"[DEBUG] - 총 {uniqueYCoords.Count}개 행 × {uniqueXCoords.Count}개 열 = {completeGrid.Count}개 교차점");
+ Debug.WriteLine($"[DEBUG] - 실제 교차점: {actualIntersections.Count}개, 가상 교차점: {completeGrid.Count - actualIntersections.Count}개");
+
+ return completeGrid;
+ }
+
+ ///
+ /// 가상 교차점의 DirectionBits를 선분 정보를 바탕으로 추론합니다.
+ ///
+ private int InferDirectionBitsForVirtualIntersection(double x, double y, List<(Point3d start, Point3d end, bool isHorizontal)> tableSegments, double tolerance)
+ {
+ int directionBits = 0;
+ var position = new Point3d(x, y, 0);
+
+ // 각 방향에 선분이 있는지 확인
+ foreach (var segment in tableSegments)
+ {
+ if (IsPointOnSegment(segment, position, tolerance))
+ {
+ if (segment.isHorizontal)
+ {
+ // 가로선인 경우: 교차점에서 양쪽으로 연장되는지 확인
+ var minX = Math.Min(segment.start.X, segment.end.X);
+ var maxX = Math.Max(segment.start.X, segment.end.X);
+
+ if (minX < position.X - tolerance) directionBits |= DirectionFlags.Left;
+ if (maxX > position.X + tolerance) directionBits |= DirectionFlags.Right;
+ }
+ else
+ {
+ // 세로선인 경우: 교차점에서 위아래로 연장되는지 확인
+ var minY = Math.Min(segment.start.Y, segment.end.Y);
+ var maxY = Math.Max(segment.start.Y, segment.end.Y);
+
+ if (maxY > position.Y + tolerance) directionBits |= DirectionFlags.Up;
+ if (minY < position.Y - tolerance) directionBits |= DirectionFlags.Down;
+ }
+ }
+ }
+
+ // 만약 어떤 방향도 없다면 MERGED 셀로 추정
+ if (directionBits == 0)
+ {
+ directionBits = DirectionFlags.CrossMerged; // 완전히 합병된 내부 교차점
+ }
+ else if ((directionBits & (DirectionFlags.Left | DirectionFlags.Right)) == 0)
+ {
+ directionBits |= DirectionFlags.HorizontalMerged; // 좌우 합병
+ }
+ else if ((directionBits & (DirectionFlags.Up | DirectionFlags.Down)) == 0)
+ {
+ directionBits |= DirectionFlags.VerticalMerged; // 상하 합병
+ }
+
+ return directionBits;
+ }
+
+ ///
+ /// Row/Column 기반으로 체계적으로 bottomRight 교차점을 찾습니다.
+ /// topLeft(Row=n, Column=k)에서 시작하여:
+ /// 1. 같은 Row(n)에서 Column k+1, k+2, ... 순으로 찾기
+ /// 2. 없으면 Row n+1에서 Column k, k+1, k+2, ... 순으로 찾기
+ /// 3. Row n+2, n+3, ... 계속 진행
+ ///
+ private IntersectionPoint FindBottomRightByRowColumn(IntersectionPoint topLeft, List intersections, double tolerance)
+ {
+ Debug.WriteLine($"[DEBUG] FindBottomRightByRowColumn for topLeft R{topLeft.Row}C{topLeft.Column}");
+
+ // topLeft가 유효한 topLeft 후보가 아니면 null 반환
+ if (!IsValidTopLeft(topLeft.DirectionBits))
+ {
+ Debug.WriteLine($"[DEBUG] topLeft R{topLeft.Row}C{topLeft.Column} is not valid topLeft candidate");
+ return null;
+ }
+
+ // 교차점들을 Row/Column으로 빠른 검색을 위해 딕셔너리로 구성
+ var intersectionLookup = intersections
+ .Where(i => i.Row > 0 && i.Column > 0) // 1-based이므로 > 0으로 유효성 체크
+ .GroupBy(i => i.Row)
+ .ToDictionary(g => g.Key, g => g.ToDictionary(i => i.Column, i => i));
+
+ // topLeft에서 시작하여 체계적으로 bottomRight 찾기
+ // bottomRight는 topLeft보다 아래쪽 행(Row가 더 큰)에 있어야 함
+ int maxRow = intersectionLookup.Keys.Any() ? intersectionLookup.Keys.Max() : topLeft.Row;
+ int maxColumn = intersectionLookup.Values.SelectMany(row => row.Keys).Any() ? intersectionLookup.Values.SelectMany(row => row.Keys).Max() : topLeft.Column;
+
+ // 범위를 확장해서 테이블 경계까지 포함
+ for (int targetRow = topLeft.Row + 1; targetRow <= maxRow + 2; targetRow++)
+ {
+ if (!intersectionLookup.ContainsKey(targetRow)) continue;
+
+ var rowIntersections = intersectionLookup[targetRow];
+
+ // bottomRight는 topLeft와 같은 열이거나 오른쪽 열에 있어야 함
+ int startColumn = topLeft.Column;
+
+ // 해당 행에서 가능한 열들을 순서대로 확인 (범위 확장)
+ var availableColumns = rowIntersections.Keys.Where(col => col >= startColumn).OrderBy(col => col);
+
+ foreach (int targetColumn in availableColumns)
+ {
+ var candidate = rowIntersections[targetColumn];
+
+ // bottomRight 후보인지 확인 (더 유연한 조건)
+ if (IsValidBottomRight(candidate.DirectionBits) ||
+ (targetRow == maxRow && targetColumn == maxColumn)) // 테이블 경계에서는 조건 완화
+ {
+ Debug.WriteLine($"[DEBUG] Found valid bottomRight R{candidate.Row}C{candidate.Column} for topLeft R{topLeft.Row}C{topLeft.Column}");
+ return candidate;
+ }
+ }
+ }
+
+ Debug.WriteLine($"[DEBUG] No bottomRight found for topLeft R{topLeft.Row}C{topLeft.Column}");
+ return null;
+ }
+
+ ///
+ /// 교차점들로부터 셀들을 추출합니다.
+ ///
+ private List ExtractCellsFromIntersections(List intersections, List<(Point3d start, Point3d end, bool isHorizontal)> tableSegments, double tolerance)
+ {
+ var cells = new List();
+
+ // 시각화를 위한 대각선 정보 초기화
+ LastDiagonalLines.Clear();
+
+ // 1. 완전한 격자망 생성 (Row/Column 번호 포함)
+ var completeGrid = CreateCompleteGridFromIntersections(intersections, tableSegments, tolerance);
+ Debug.WriteLine($"[DEBUG] 완전한 격자망 생성: {completeGrid.Count}개 교차점");
+
+ // 2. 테이블 경계를 파악하여 외부 교차점 필터링 (NOTE 박스 내 라인들의 bounding box 기준)
+ var filteredIntersections = FilterIntersectionsWithinTableBounds(completeGrid, tableSegments, tolerance);
+ Debug.WriteLine($"[DEBUG] 교차점 필터링: {completeGrid.Count}개 -> {filteredIntersections.Count}개");
+
+ // 3. R1C1부터 체계적으로 셀 찾기
+ // Row/Column 기준으로 정렬하여 R1C1부터 시작
+ var sortedIntersections = filteredIntersections
+ .Where(i => i.Row > 0 && i.Column > 0) // 1-based이므로 > 0으로 유효성 체크
+ .OrderBy(i => i.Row).ThenBy(i => i.Column)
+ .ToList();
+
+ Debug.WriteLine($"[DEBUG] R1C1부터 체계적 셀 찾기 시작 - 정렬된 교차점: {sortedIntersections.Count}개");
+
+ foreach (var topLeft in sortedIntersections)
+ {
+ // topLeft 후보인지 확인 (Right + Down이 있는 교차점들)
+ if (IsValidTopLeft(topLeft.DirectionBits))
+ {
+ Debug.WriteLine($"[DEBUG] TopLeft 후보 발견: R{topLeft.Row}C{topLeft.Column} at ({topLeft.Position.X:F1},{topLeft.Position.Y:F1}) DirectionBits={topLeft.DirectionBits}");
+
+ // Row/Column 기반으로 bottomRight 찾기
+ var bottomRight = FindBottomRightByRowColumn(topLeft, filteredIntersections, tolerance);
+ if (bottomRight != null)
+ {
+ Debug.WriteLine($"[DEBUG] BottomRight 발견: R{bottomRight.Row}C{bottomRight.Column} at ({bottomRight.Position.X:F1},{bottomRight.Position.Y:F1}) DirectionBits={bottomRight.DirectionBits}");
+
+ var newCell = CreateCellFromCornersWithRowColumn(topLeft, bottomRight);
+ cells.Add(newCell);
+
+ // 시각화를 위한 대각선 정보 저장 (R1C1 라벨)
+ var cellLabel = $"R{newCell.Row}C{newCell.Column}";
+ LastDiagonalLines.Add((topLeft.Position, bottomRight.Position, cellLabel));
+
+ Debug.WriteLine($"[DEBUG] 셀 생성 완료: {cellLabel} - ({topLeft.Position.X:F1},{topLeft.Position.Y:F1}) to ({bottomRight.Position.X:F1},{bottomRight.Position.Y:F1})");
+ }
+ else
+ {
+ Debug.WriteLine($"[DEBUG] R{topLeft.Row}C{topLeft.Column}에서 적절한 BottomRight를 찾을 수 없음");
+ }
+ }
+ }
+
+ Debug.WriteLine($"[DEBUG] 교차점 기반 셀 추출 완료: {cells.Count}개, 대각선 {LastDiagonalLines.Count}개");
+
+ return cells;
+ }
+
+ ///
+ /// NOTE 아래 박스 내의 모든 라인들의 bounding box를 기준으로 테이블 경계 내부의 교차점들만 필터링합니다.
+ ///
+ private List FilterIntersectionsWithinTableBounds(List intersections, List<(Point3d start, Point3d end, bool isHorizontal)> tableSegments, double tolerance)
+ {
+ if (intersections.Count == 0 || tableSegments.Count == 0) return intersections;
+
+ // 1. NOTE 아래 박스 내의 모든 라인들의 bounding box를 계산
+ var minX = tableSegments.Min(seg => Math.Min(seg.start.X, seg.end.X));
+ var maxX = tableSegments.Max(seg => Math.Max(seg.start.X, seg.end.X));
+ var minY = tableSegments.Min(seg => Math.Min(seg.start.Y, seg.end.Y));
+ var maxY = tableSegments.Max(seg => Math.Max(seg.start.Y, seg.end.Y));
+
+ Debug.WriteLine($"[DEBUG] 테이블 bounding box: ({minX:F1},{minY:F1}) to ({maxX:F1},{maxY:F1})");
+
+ // 2. 박스 경계선은 제외하고, 내부의 교차점들만 필터링 (tolerance를 더 크게 설정)
+ var boundaryTolerance = tolerance * 3.0; // 경계 제외를 위한 더 큰 tolerance
+ var validIntersections = intersections.Where(point =>
+ point.Position.X > minX + boundaryTolerance &&
+ point.Position.X < maxX - boundaryTolerance &&
+ point.Position.Y > minY + boundaryTolerance &&
+ point.Position.Y < maxY - boundaryTolerance
+ ).ToList();
+
+ Debug.WriteLine($"[DEBUG] 테이블 경계 필터링 (bounding box 기반): 원본 {intersections.Count}개 -> 필터링 {validIntersections.Count}개");
+
+ // 3. 최소한의 테이블 구조가 있는지 확인
+ if (validIntersections.Count < 4)
+ {
+ Debug.WriteLine($"[DEBUG] 필터링된 교차점이 너무 적음 ({validIntersections.Count}개), 원본 반환");
+ return intersections;
+ }
+
+ return validIntersections;
+ }
+
+ ///
+ /// 셀들에 Row/Column 번호를 할당합니다.
+ ///
+ private void AssignRowColumnNumbers(List cells)
+ {
+ if (cells.Count == 0) return;
+
+ // Y좌표로 행 그룹핑 (위에서 아래로, Y값이 큰 것부터)
+ var rowGroups = cells
+ .GroupBy(c => Math.Round(c.MaxPoint.Y, 1)) // 상단 Y좌표 기준으로 그룹핑
+ .OrderByDescending(g => g.Key) // Y값이 큰 것부터 (위에서 아래로)
+ .ToList();
+
+ int rowIndex = 1; // R1부터 시작
+ foreach (var rowGroup in rowGroups)
+ {
+ // 같은 행 내에서 X좌표로 열 정렬 (왼쪽에서 오른쪽으로)
+ var cellsInRow = rowGroup.OrderBy(c => c.MinPoint.X).ToList();
+
+ int columnIndex = 1; // C1부터 시작
+ foreach (var cell in cellsInRow)
+ {
+ cell.Row = rowIndex;
+ cell.Column = columnIndex;
+ columnIndex++;
+ }
+ rowIndex++;
+ }
+
+ Debug.WriteLine($"[DEBUG] Row/Column 번호 할당 완료: {rowGroups.Count}개 행, 최대 {cells.Max(c => c.Column + 1)}개 열");
+
+ // 디버그 출력: 각 셀의 위치와 번호
+ foreach (var cell in cells.OrderBy(c => c.Row).ThenBy(c => c.Column))
+ {
+ Debug.WriteLine($"[DEBUG] 셀 R{cell.Row}C{cell.Column}: ({cell.MinPoint.X:F1}, {cell.MinPoint.Y:F1}) to ({cell.MaxPoint.X:F1}, {cell.MaxPoint.Y:F1})");
+ }
+ }
+
+ ///
+ /// 대각선 라벨을 Row/Column 번호로 업데이트합니다.
+ ///
+ private void UpdateDiagonalLabels(List cells)
+ {
+ for (int i = 0; i < LastDiagonalLines.Count && i < cells.Count; i++)
+ {
+ var cell = cells[i];
+ var diagonal = LastDiagonalLines[i];
+
+ // Row/Column 번호를 사용한 새 라벨 생성
+ var newLabel = $"R{cell.Row}C{cell.Column}";
+ LastDiagonalLines[i] = (diagonal.topLeft, diagonal.bottomRight, newLabel);
+
+ Debug.WriteLine($"[DEBUG] 대각선 라벨 업데이트: {diagonal.label} -> {newLabel}");
+ }
+ }
+
+ ///
+ /// 교차점이 셀의 topLeft가 될 수 있는지 확인합니다.
+ /// 9번, 11번, 13번, 15번이 topLeft 후보
+ ///
+ public bool IsValidTopLeft(int directionBits)
+ {
+ return directionBits == 9 || // Right+Down (ㄱ형)
+ directionBits == 11 || // Right+Up+Down (ㅏ형)
+ directionBits == 13 || // Right+Left+Down (ㅜ형)
+ directionBits == 15; // 모든 방향 (+형)
+ }
+
+ ///
+ /// topLeft에 대응하는 bottomRight 후보를 찾습니다.
+ /// X축으로 가장 가까운 유효한 bottomRight를 반환 (horizontal merge 자동 처리)
+ ///
+ private IntersectionPoint FindBottomRightCandidate(IntersectionPoint topLeft, List intersections, double tolerance)
+ {
+ Debug.WriteLine($"[DEBUG] FindBottomRightCandidate for topLeft {topLeft.Position}[{topLeft.DirectionBits}]");
+
+ var allRightDown = intersections.Where(p =>
+ p.Position.X > topLeft.Position.X + tolerance && // 오른쪽에 있고
+ p.Position.Y < topLeft.Position.Y - tolerance // 아래에 있고
+ ).ToList();
+
+ Debug.WriteLine($"[DEBUG] Found {allRightDown.Count} points to the right and below");
+ foreach (var p in allRightDown)
+ {
+ Debug.WriteLine($"[DEBUG] Point {p.Position}[{p.DirectionBits}] - IsValidBottomRight: {IsValidBottomRight(p.DirectionBits)}");
+ }
+
+ var candidates = allRightDown.Where(p =>
+ IsValidBottomRight(p.DirectionBits) // 유효한 bottomRight 타입
+ ).OrderBy(p => p.Position.X) // X값이 가장 가까운 것부터 (horizontal merge 처리)
+ .ThenByDescending(p => p.Position.Y); // 같은 X면 Y값이 큰 것(위에 있는 것)부터
+
+ var result = candidates.FirstOrDefault();
+ Debug.WriteLine($"[DEBUG] Selected bottomRight: {result?.Position}[{result?.DirectionBits}]");
+ return result;
+ }
+
+ ///
+ /// 교차점이 셀의 bottomRight가 될 수 있는지 확인합니다.
+ /// 15번, 14번, 6번, 7번이 bottomRight 후보
+ ///
+ public bool IsValidBottomRight(int directionBits)
+ {
+ bool isValid = directionBits == 15 || // 모든 방향 (+형)
+ directionBits == 14 || // Up+Down+Left (ㅓ형)
+ directionBits == 6 || // Up+Left (ㄹ형)
+ directionBits == 7 || // Up+Left+Right (ㅗ형)
+ directionBits == 10 || // Up+Down (ㅣ형) - 테이블 오른쪽 경계
+ directionBits == 12 || // Left+Down (ㄱ형 뒤집어진) - 테이블 우상단 경계
+ directionBits == 4 || // Left (ㅡ형 일부) - 테이블 우하단 경계
+ directionBits == 2; // Up (ㅣ형 일부) - 테이블 하단 경계
+
+ Debug.WriteLine($"[DEBUG] IsValidBottomRight({directionBits}) = {isValid}");
+ return isValid;
+ }
+
+ ///
+ /// 두 모서리 점으로부터 Row/Column 번호가 설정된 셀을 생성합니다.
+ ///
+ private TableCell CreateCellFromCornersWithRowColumn(IntersectionPoint topLeft, IntersectionPoint bottomRight)
+ {
+ return 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, // topLeft의 행 번호
+ Column = topLeft.Column, // topLeft의 열 번호
+ RowSpan = bottomRight.Row - topLeft.Row + 1, // 행 범위
+ ColumnSpan = bottomRight.Column - topLeft.Column + 1, // 열 범위
+ CellText = ""
+ };
+ }
+
+ ///
+ /// 두 모서리 점으로부터 셀을 생성합니다.
+ ///
+ private TableCell CreateCellFromCorners(IntersectionPoint topLeft, IntersectionPoint bottomRight)
+ {
+ return new TableCell
+ {
+ MinPoint = new Point3d(topLeft.Position.X, bottomRight.Position.Y, 0), // 왼쪽 하단
+ MaxPoint = new Point3d(bottomRight.Position.X, topLeft.Position.Y, 0), // 오른쪽 상단
+ Row = 0, // 나중에 정렬 시 설정
+ Column = 0, // 나중에 정렬 시 설정
+ RowSpan = 1,
+ ColumnSpan = 1,
+ CellText = ""
+ };
+ }
+
+ ///
+ /// 기존 격자 기반 셀 추출 (백업용)
+ ///
+ private List ExtractTableCellsLegacy(List<(Point3d start, Point3d end, bool isHorizontal)> tableSegments, double tolerance)
+ {
+ var cells = new List();
+
+ // 1. 실제 교차점만 찾기 (선분 끝점이 아닌 진짜 교차점만)
+ var intersections = FindRealIntersections(tableSegments, tolerance);
+ Debug.WriteLine($"[DEBUG] 실제 교차점 {intersections.Count}개 발견");
+
+ if (intersections.Count < 4) // 최소 4개 교차점 필요 (사각형)
+ {
+ Debug.WriteLine($"[DEBUG] 교차점이 부족하여 셀을 생성할 수 없음");
+ return cells;
+ }
+
+ // 2. 고유한 X, Y 좌표들을 정렬하여 격자 생성
+ var uniqueXCoords = intersections.Select(p => p.X).Distinct().OrderBy(x => x).ToList();
+ var uniqueYCoords = intersections.Select(p => p.Y).Distinct().OrderByDescending(y => y).ToList(); // Y는 내림차순 (위에서 아래로)
+
+ Debug.WriteLine($"[DEBUG] 격자: X좌표 {uniqueXCoords.Count}개, Y좌표 {uniqueYCoords.Count}개");
+
+ // 3. 각 격자 셀이 실제 테이블 셀인지 확인 (4변이 모두 존재하는 경우만)
+ for (int row = 0; row < uniqueYCoords.Count - 1; row++)
+ {
+ for (int col = 0; col < uniqueXCoords.Count - 1; col++)
+ {
+ var topLeft = new Point3d(uniqueXCoords[col], uniqueYCoords[row], 0);
+ var topRight = new Point3d(uniqueXCoords[col + 1], uniqueYCoords[row], 0);
+ var bottomLeft = new Point3d(uniqueXCoords[col], uniqueYCoords[row + 1], 0);
+ var bottomRight = new Point3d(uniqueXCoords[col + 1], uniqueYCoords[row + 1], 0);
+
+ // 4변이 모두 존재하는지 확인
+ bool hasTopEdge = HasSegmentBetweenPoints(tableSegments, topLeft, topRight, tolerance);
+ bool hasBottomEdge = HasSegmentBetweenPoints(tableSegments, bottomLeft, bottomRight, tolerance);
+ bool hasLeftEdge = HasSegmentBetweenPoints(tableSegments, topLeft, bottomLeft, tolerance);
+ bool hasRightEdge = HasSegmentBetweenPoints(tableSegments, topRight, bottomRight, tolerance);
+
+ if (hasTopEdge && hasBottomEdge && hasLeftEdge && hasRightEdge)
+ {
+ var newCell = new TableCell
+ {
+ MinPoint = bottomLeft, // 왼쪽 하단
+ MaxPoint = topRight, // 오른쪽 상단
+ Row = row,
+ Column = col,
+ RowSpan = 1,
+ ColumnSpan = 1
+ };
+
+ // 4. 중첩 검증: 새 셀이 기존 셀들과 중첩되지 않는지 확인
+ if (!IsOverlappingWithExistingCells(newCell, cells))
+ {
+ cells.Add(newCell);
+ Debug.WriteLine($"[DEBUG] 셀 발견: Row={row}, Col={col}, Min=({newCell.MinPoint.X:F1},{newCell.MinPoint.Y:F1}), Max=({newCell.MaxPoint.X:F1},{newCell.MaxPoint.Y:F1})");
+ }
+ else
+ {
+ Debug.WriteLine($"[DEBUG] 중첩 셀 제외: Row={row}, Col={col}, 영역=({bottomLeft.X:F1},{bottomLeft.Y:F1}) to ({topRight.X:F1},{topRight.Y:F1})");
+ }
+ }
+ }
+ }
+
+ Debug.WriteLine($"[DEBUG] 중첩 검증 후 셀 개수: {cells.Count}개");
+
+ // 5. Merge된 셀 탐지 및 병합
+ DetectAndMergeCells(cells, tableSegments, tolerance);
+
+ Debug.WriteLine($"[DEBUG] 최종 셀 개수: {cells.Count}개");
+ return cells;
+ }
+
+ ///
+ /// 선분들의 실제 교차점만 찾습니다 (끝점은 제외).
+ ///
+ private List FindRealIntersections(List<(Point3d start, Point3d end, bool isHorizontal)> segments, double tolerance)
+ {
+ var intersections = new HashSet();
+
+ // 수평선과 수직선의 실제 교차점만 찾기 (끝점에서의 만남은 제외)
+ 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)
+ {
+ var intersection = GetLineIntersection(hSeg, vSeg, tolerance);
+ if (intersection.HasValue)
+ {
+ // 교차점이 두 선분의 끝점이 아닌 실제 교차점인지 확인
+ if (!IsEndPoint(intersection.Value, hSeg, tolerance) &&
+ !IsEndPoint(intersection.Value, vSeg, tolerance))
+ {
+ intersections.Add(intersection.Value);
+ }
+ else
+ {
+ // 끝점이라도 다른 선분과의 진짜 교차점이라면 포함
+ intersections.Add(intersection.Value);
+ }
+ }
+ }
+ }
+
+ Debug.WriteLine($"[DEBUG] 실제 교차점 발견: {intersections.Count}개");
+ foreach (var intersection in intersections)
+ {
+ Debug.WriteLine($"[DEBUG] 교차점: ({intersection.X:F1}, {intersection.Y:F1})");
+ }
+
+ return intersections.ToList();
+ }
+
+ ///
+ /// 모든 선분들의 교차점을 찾습니다 (기존 함수 유지).
+ ///
+ private List FindAllIntersections(List<(Point3d start, Point3d end, bool isHorizontal)> segments, double tolerance)
+ {
+ var intersections = new HashSet();
+
+ // 모든 세그먼트의 끝점들을 교차점으로 추가
+ foreach (var segment in segments)
+ {
+ intersections.Add(segment.start);
+ intersections.Add(segment.end);
+ }
+
+ // 수평선과 수직선의 교차점 찾기
+ 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)
+ {
+ var intersection = GetLineIntersection(hSeg, vSeg, tolerance);
+ if (intersection.HasValue)
+ {
+ intersections.Add(intersection.Value);
+ }
+ }
+ }
+
+ return intersections.ToList();
+ }
+
+ ///
+ /// 두 선분의 교차점을 구합니다.
+ ///
+ private Point3d? GetLineIntersection((Point3d start, Point3d end, bool isHorizontal) hSeg,
+ (Point3d start, Point3d end, bool isHorizontal) vSeg, double tolerance)
+ {
+ // 수평선의 Y 좌표
+ double hY = (hSeg.start.Y + hSeg.end.Y) / 2;
+ // 수직선의 X 좌표
+ double vX = (vSeg.start.X + vSeg.end.X) / 2;
+
+ // 교차점 좌표
+ var intersection = new Point3d(vX, hY, 0);
+
+ // 교차점이 두 선분 모두에 속하는지 확인
+ bool onHorizontal = intersection.X >= Math.Min(hSeg.start.X, hSeg.end.X) - tolerance &&
+ intersection.X <= Math.Max(hSeg.start.X, hSeg.end.X) + tolerance;
+
+ bool onVertical = intersection.Y >= Math.Min(vSeg.start.Y, vSeg.end.Y) - tolerance &&
+ intersection.Y <= Math.Max(vSeg.start.Y, vSeg.end.Y) + tolerance;
+
+ return (onHorizontal && onVertical) ? intersection : null;
+ }
+
+ ///
+ /// 두 점 사이에 선분이 존재하는지 확인합니다.
+ ///
+ private bool HasSegmentBetweenPoints(List<(Point3d start, Point3d end, bool isHorizontal)> segments,
+ Point3d point1, Point3d point2, double tolerance)
+ {
+ foreach (var segment in segments)
+ {
+ // 선분이 두 점을 연결하는지 확인 (방향 무관)
+ bool connects = (IsPointOnSegment(segment, point1, tolerance) && IsPointOnSegment(segment, point2, tolerance)) ||
+ (IsPointOnSegment(segment, point2, tolerance) && IsPointOnSegment(segment, point1, tolerance));
+
+ if (connects)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ ///
+ /// 점이 선분 위에 있는지 확인합니다.
+ ///
+ private bool IsPointOnSegment((Point3d start, Point3d end, bool isHorizontal) segment, Point3d point, double tolerance)
+ {
+ double minX = Math.Min(segment.start.X, segment.end.X) - tolerance;
+ double maxX = Math.Max(segment.start.X, segment.end.X) + tolerance;
+ double minY = Math.Min(segment.start.Y, segment.end.Y) - tolerance;
+ double maxY = Math.Max(segment.start.Y, segment.end.Y) + tolerance;
+
+ return point.X >= minX && point.X <= maxX && point.Y >= minY && point.Y <= maxY;
+ }
+
///
/// Note 박스 내의 텍스트들을 좌표에 따라 정렬합니다 (위에서 아래로, 왼쪽에서 오른쪽으로).
///
@@ -981,8 +1901,933 @@ namespace DwgExtractorManual.Models
return noteContents;
}
+
+ ///
+ /// 점이 선분의 끝점인지 확인합니다.
+ ///
+ private bool IsEndPoint(Point3d point, (Point3d start, Point3d end, bool isHorizontal) segment, double tolerance)
+ {
+ return (Math.Abs(point.X - segment.start.X) <= tolerance && Math.Abs(point.Y - segment.start.Y) <= tolerance) ||
+ (Math.Abs(point.X - segment.end.X) <= tolerance && Math.Abs(point.Y - segment.end.Y) <= tolerance);
+ }
+
+ ///
+ /// 새 셀이 기존 셀들과 중첩되는지 확인합니다.
+ ///
+ private bool IsOverlappingWithExistingCells(TableCell newCell, List existingCells)
+ {
+ foreach (var existingCell in existingCells)
+ {
+ // 두 셀이 중첩되는지 확인
+ if (!(newCell.MaxPoint.X <= existingCell.MinPoint.X || newCell.MinPoint.X >= existingCell.MaxPoint.X ||
+ newCell.MaxPoint.Y <= existingCell.MinPoint.Y || newCell.MinPoint.Y >= existingCell.MaxPoint.Y))
+ {
+ return true; // 중첩됨
+ }
+ }
+ return false; // 중첩되지 않음
+ }
+
+ ///
+ /// 테이블의 모든 교차점에서 DirectionBits를 계산합니다.
+ ///
+ private List CalculateIntersectionDirections(List intersections, List<(Point3d start, Point3d end, bool isHorizontal)> segments, double tolerance)
+ {
+ var intersectionPoints = new List();
+
+ foreach (var intersection in intersections)
+ {
+ int directionBits = 0;
+
+ // 교차점에서 각 방향으로 선분이 있는지 확인
+ foreach (var segment in segments)
+ {
+ if (IsPointOnSegment(segment, intersection, tolerance))
+ {
+ if (segment.isHorizontal)
+ {
+ // 가로 선분인 경우 좌우 방향 확인
+ if (segment.start.X < intersection.X || segment.end.X < intersection.X)
+ directionBits |= DirectionFlags.Left;
+ if (segment.start.X > intersection.X || segment.end.X > intersection.X)
+ directionBits |= DirectionFlags.Right;
+ }
+ else
+ {
+ // 세로 선분인 경우 상하 방향 확인
+ if (segment.start.Y > intersection.Y || segment.end.Y > intersection.Y)
+ directionBits |= DirectionFlags.Up;
+ if (segment.start.Y < intersection.Y || segment.end.Y < intersection.Y)
+ directionBits |= DirectionFlags.Down;
+ }
+ }
+ }
+
+ intersectionPoints.Add(new IntersectionPoint
+ {
+ Position = intersection,
+ DirectionBits = directionBits
+ });
+ }
+
+ return intersectionPoints;
+ }
+
+ private (object[,] tableData, List cells, List nonTableTextIds, List<(Point3d start, Point3d end, bool isHorizontal)> tableSegments, List intersectionPoints, List diagonalLines, List cellBoundaries) ExtractTableAndTextsFromNoteBox(
+ Transaction tran,
+ DBText noteText,
+ (Point3d minPoint, Point3d maxPoint) noteBox,
+ List polylineIds,
+ List lineIds,
+ List allTextIds,
+ Database database)
+ {
+ var boxLineIds = FindLinesInNoteBox(tran, noteBox, polylineIds, lineIds);
+ var boxTextIds = FindTextsInNoteBox(tran, noteText, noteBox, allTextIds);
+
+ var tableSegments = FindTableSegmentsInBox(tran, boxLineIds, noteText.Height);
+ var cells = ExtractTableCells(tableSegments, noteText.Height * 0.5);
+
+ var (assignedTexts, unassignedTextIds) = AssignTextsToCells(tran, cells, boxTextIds);
+
+ var tableData = CreateTableDataArray(cells);
+
+ // 현재 NOTE의 교차점 데이터를 가져오기 (LastIntersectionPoints에서 복사)
+ var currentIntersectionPoints = new List(LastIntersectionPoints);
+
+ // 대각선 생성 (R2C2 topLeft에서 bottomRight 찾기)
+ var diagonalLines = GenerateDiagonalLines(currentIntersectionPoints);
+
+ // 대각선으로부터 셀 경계 추출
+ var cellBoundaries = ExtractCellBoundariesFromDiagonals(diagonalLines);
+ Debug.WriteLine($"[EXTRACT] 셀 경계 {cellBoundaries.Count}개 추출 완료");
+
+ // 셀 경계 내 텍스트 추출
+ ExtractTextsFromCellBoundaries(tran, cellBoundaries, boxTextIds);
+ Debug.WriteLine($"[EXTRACT] 셀 경계 내 텍스트 추출 완료");
+
+ // 병합된 셀 처리 (같은 텍스트로 채우기)
+ ProcessMergedCells(cellBoundaries);
+ Debug.WriteLine($"[EXTRACT] 병합 셀 처리 완료");
+
+ // CellBoundary 텍스트를 기존 테이블 데이터에 반영
+ tableData = UpdateTableDataWithCellBoundaries(tableData, cellBoundaries);
+ Debug.WriteLine($"[EXTRACT] 테이블 데이터 업데이트 완료");
+
+ return (tableData, cells, unassignedTextIds, tableSegments, currentIntersectionPoints, diagonalLines, cellBoundaries);
+ }
+
+ ///
+ /// 교차점들로부터 대각선을 생성합니다. 특히 R2C2 topLeft에서 적절한 bottomRight를 찾아서 대각선을 그립니다.
+ ///
+ private List GenerateDiagonalLines(List intersectionPoints)
+ {
+ var diagonalLines = new List();
+
+ if (!intersectionPoints.Any())
+ {
+ Debug.WriteLine("[DIAGONAL] 교차점이 없어 대각선을 생성할 수 없음");
+ return diagonalLines;
+ }
+
+ try
+ {
+ // 테이블 내부의 topLeft 후보 찾기 (R1C1과 박스 외곽 제외)
+ var allTopLefts = intersectionPoints
+ .Where(ip => IsValidTopLeft(ip.DirectionBits) &&
+ IsInsideTable(ip, intersectionPoints)) // 테이블 내부인지 확인
+ .OrderBy(ip => ip.Row)
+ .ThenBy(ip => ip.Column)
+ .ToList();
+
+ Debug.WriteLine($"[DIAGONAL] 테이블 내부 topLeft 후보 {allTopLefts.Count}개 발견");
+
+ foreach (var topLeft in allTopLefts)
+ {
+ Debug.WriteLine($"[DIAGONAL] topLeft R{topLeft.Row}C{topLeft.Column} 처리: ({topLeft.Position.X:F1}, {topLeft.Position.Y:F1}) DirectionBits={topLeft.DirectionBits}");
+
+ // 이 topLeft에 대한 첫 번째 bottomRight만 찾기 (row+1, col+1부터 시작)
+ var bottomRight = FindFirstBottomRightForTopLeft(topLeft, intersectionPoints);
+
+ if (bottomRight != null)
+ {
+ Debug.WriteLine($"[DIAGONAL] bottomRight 발견: R{bottomRight.Row}C{bottomRight.Column} ({bottomRight.Position.X:F1}, {bottomRight.Position.Y:F1})");
+
+ // 대각선 생성
+ var diagonal = new DiagonalLine
+ {
+ StartX = topLeft.Position.X,
+ StartY = topLeft.Position.Y,
+ EndX = bottomRight.Position.X,
+ EndY = bottomRight.Position.Y,
+ Color = "Green",
+ Label = $"R{topLeft.Row}C{topLeft.Column}→R{bottomRight.Row}C{bottomRight.Column}"
+ };
+
+ diagonalLines.Add(diagonal);
+ Debug.WriteLine($"[DIAGONAL] 대각선 생성 완료: {diagonal.Label}");
+ }
+ else
+ {
+ Debug.WriteLine($"[DIAGONAL] topLeft R{topLeft.Row}C{topLeft.Column}에 대한 bottomRight을 찾지 못함");
+ }
+ }
+
+ Debug.WriteLine($"[DIAGONAL] 총 {diagonalLines.Count}개 대각선 생성 완료");
+ }
+ catch (System.Exception ex)
+ {
+ Debug.WriteLine($"[DIAGONAL] 대각선 생성 중 오류: {ex.Message}");
+ }
+
+ return diagonalLines;
+ }
+
+ ///
+ /// 대각선 정보로부터 셀의 4개 모서리 좌표를 계산합니다.
+ ///
+ private List ExtractCellBoundariesFromDiagonals(List diagonalLines)
+ {
+ var cellBoundaries = new List();
+
+ try
+ {
+ foreach (var diagonal in diagonalLines)
+ {
+ Debug.WriteLine($"[CELL_BOUNDARY] 대각선 처리: {diagonal.Label}");
+
+ // 대각선의 TopLeft, BottomRight 좌표
+ var topLeft = new Point3d(diagonal.StartX, diagonal.StartY, 0);
+ var bottomRight = new Point3d(diagonal.EndX, diagonal.EndY, 0);
+
+ // 나머지 두 모서리 계산
+ var topRight = new Point3d(bottomRight.X, topLeft.Y, 0);
+ var bottomLeft = new Point3d(topLeft.X, bottomRight.Y, 0);
+
+ var cellBoundary = new CellBoundary
+ {
+ TopLeft = topLeft,
+ TopRight = topRight,
+ BottomLeft = bottomLeft,
+ BottomRight = bottomRight,
+ Label = diagonal.Label,
+ Width = Math.Abs(bottomRight.X - topLeft.X),
+ Height = Math.Abs(topLeft.Y - bottomRight.Y)
+ };
+
+ cellBoundaries.Add(cellBoundary);
+
+ Debug.WriteLine($"[CELL_BOUNDARY] 셀 경계 생성: {diagonal.Label}");
+ Debug.WriteLine($" TopLeft: ({topLeft.X:F1}, {topLeft.Y:F1})");
+ Debug.WriteLine($" TopRight: ({topRight.X:F1}, {topRight.Y:F1})");
+ Debug.WriteLine($" BottomLeft: ({bottomLeft.X:F1}, {bottomLeft.Y:F1})");
+ Debug.WriteLine($" BottomRight: ({bottomRight.X:F1}, {bottomRight.Y:F1})");
+ Debug.WriteLine($" 크기: {cellBoundary.Width:F1} x {cellBoundary.Height:F1}");
+ }
+
+ Debug.WriteLine($"[CELL_BOUNDARY] 총 {cellBoundaries.Count}개 셀 경계 생성 완료");
+ }
+ catch (System.Exception ex)
+ {
+ Debug.WriteLine($"[CELL_BOUNDARY] 셀 경계 추출 중 오류: {ex.Message}");
+ }
+
+ return cellBoundaries;
+ }
+
+ ///
+ /// 셀 경계 내에 포함되는 모든 텍스트(DBText, MText)를 추출합니다.
+ ///
+ private void ExtractTextsFromCellBoundaries(Transaction tran, List cellBoundaries, List allTextIds)
+ {
+ try
+ {
+ Debug.WriteLine($"[CELL_TEXT] {cellBoundaries.Count}개 셀 경계에서 텍스트 추출 시작");
+
+ foreach (var cellBoundary in cellBoundaries)
+ {
+ var textsInCell = new List();
+
+ foreach (var textId in allTextIds)
+ {
+ var textEntity = tran.GetObject(textId, OpenMode.ForRead);
+ Point3d textPosition = Point3d.Origin;
+ string textContent = "";
+
+ // DBText와 MText 처리
+ if (textEntity is DBText dbText)
+ {
+ textPosition = dbText.Position;
+ textContent = dbText.TextString;
+ Debug.WriteLine($"[CELL_TEXT] DBText 발견: '{textContent}' at ({textPosition.X:F1}, {textPosition.Y:F1})");
+ }
+ else if (textEntity is MText mText)
+ {
+ textPosition = mText.Location;
+ textContent = mText.Contents;
+ Debug.WriteLine($"[CELL_TEXT] MText 발견: '{textContent}' at ({textPosition.X:F1}, {textPosition.Y:F1})");
+ }
+ else
+ {
+ continue; // 다른 타입의 텍스트는 무시
+ }
+
+ // 텍스트가 셀 경계 내에 있는지 확인
+ if (IsPointInCellBoundary(textPosition, cellBoundary))
+ {
+ if (!string.IsNullOrWhiteSpace(textContent))
+ {
+ textsInCell.Add(textContent.Trim());
+ Debug.WriteLine($"[CELL_TEXT] ✅ {cellBoundary.Label}에 텍스트 추가: '{textContent.Trim()}'");
+ }
+ }
+ }
+
+ // 셀 경계에 찾은 텍스트들을 콤마로 연결하여 저장
+ cellBoundary.CellText = string.Join(", ", textsInCell);
+ Debug.WriteLine($"[CELL_TEXT] {cellBoundary.Label} 최종 텍스트: '{cellBoundary.CellText}'");
+ }
+
+ Debug.WriteLine($"[CELL_TEXT] 셀 경계 텍스트 추출 완료");
+ }
+ catch (System.Exception ex)
+ {
+ Debug.WriteLine($"[CELL_TEXT] 셀 경계 텍스트 추출 중 오류: {ex.Message}");
+ }
+ }
+
+ ///
+ /// 점이 셀 경계 내에 있는지 확인합니다.
+ ///
+ private bool IsPointInCellBoundary(Point3d point, CellBoundary cellBoundary)
+ {
+ // 단순한 사각형 범위 체크
+ double minX = Math.Min(cellBoundary.TopLeft.X, cellBoundary.BottomRight.X);
+ double maxX = Math.Max(cellBoundary.TopLeft.X, cellBoundary.BottomRight.X);
+ double minY = Math.Min(cellBoundary.TopLeft.Y, cellBoundary.BottomRight.Y);
+ double maxY = Math.Max(cellBoundary.TopLeft.Y, cellBoundary.BottomRight.Y);
+
+ bool isInside = point.X >= minX && point.X <= maxX && point.Y >= minY && point.Y <= maxY;
+
+ if (isInside)
+ {
+ Debug.WriteLine($"[CELL_TEXT] 텍스트 위치 ({point.X:F1}, {point.Y:F1})가 {cellBoundary.Label} 범위 ({minX:F1}-{maxX:F1}, {minY:F1}-{maxY:F1}) 내에 있음");
+ }
+
+ return isInside;
+ }
+
+ ///
+ /// 셀 경계들을 분석하여 병합된 셀들을 찾고 같은 텍스트로 채웁니다.
+ ///
+ private void ProcessMergedCells(List cellBoundaries)
+ {
+ try
+ {
+ Debug.WriteLine($"[MERGE] {cellBoundaries.Count}개 셀 경계에서 병합 처리 시작");
+
+ // 라벨에서 Row/Column 정보 추출하여 그룹핑
+ var cellsByRowCol = new Dictionary<(int row, int col), CellBoundary>();
+
+ foreach (var cell in cellBoundaries)
+ {
+ var (row, col) = ParseRowColFromLabel(cell.Label);
+ if (row > 0 && col > 0)
+ {
+ cellsByRowCol[(row, col)] = cell;
+ }
+ }
+
+ Debug.WriteLine($"[MERGE] {cellsByRowCol.Count}개 셀의 Row/Col 정보 파싱 완료");
+
+ // 각 셀에 대해 병합된 영역 찾기
+ var processedCells = new HashSet<(int, int)>();
+
+ foreach (var kvp in cellsByRowCol)
+ {
+ var (row, col) = kvp.Key;
+ var cell = kvp.Value;
+
+ if (processedCells.Contains((row, col)) || string.IsNullOrEmpty(cell.CellText))
+ continue;
+
+ // 이 셀에서 시작하는 병합 영역 찾기
+ var mergedCells = FindMergedRegion(cellsByRowCol, row, col, cell);
+
+ if (mergedCells.Count > 1)
+ {
+ Debug.WriteLine($"[MERGE] R{row}C{col}에서 시작하는 {mergedCells.Count}개 병합 셀 발견");
+
+ // 모든 병합된 셀에 같은 텍스트 적용
+ foreach (var mergedCell in mergedCells)
+ {
+ mergedCell.CellText = cell.CellText; // 원본 셀의 텍스트를 모든 병합 셀에 복사
+ var (r, c) = ParseRowColFromLabel(mergedCell.Label);
+ processedCells.Add((r, c));
+ Debug.WriteLine($"[MERGE] {mergedCell.Label}에 텍스트 복사: '{cell.CellText}'");
+ }
+ }
+ else
+ {
+ processedCells.Add((row, col));
+ }
+ }
+
+ Debug.WriteLine($"[MERGE] 병합 셀 처리 완료");
+ }
+ catch (System.Exception ex)
+ {
+ Debug.WriteLine($"[MERGE] 병합 셀 처리 중 오류: {ex.Message}");
+ }
+ }
+
+ ///
+ /// 라벨에서 Row, Column 번호를 파싱합니다. (예: "R2C3" → (2, 3))
+ ///
+ private (int row, int col) ParseRowColFromLabel(string label)
+ {
+ try
+ {
+ if (string.IsNullOrEmpty(label)) return (0, 0);
+
+ var parts = label.Replace("R", "").Split('C');
+ if (parts.Length == 2 && int.TryParse(parts[0], out int row) && int.TryParse(parts[1], out int col))
+ {
+ return (row, col);
+ }
+ }
+ catch (System.Exception ex)
+ {
+ Debug.WriteLine($"[MERGE] 라벨 파싱 오류: {label}, {ex.Message}");
+ }
+
+ return (0, 0);
+ }
+
+ ///
+ /// 주어진 셀에서 시작하는 병합된 영역을 찾습니다.
+ ///
+ private List FindMergedRegion(Dictionary<(int row, int col), CellBoundary> cellsByRowCol, int startRow, int startCol, CellBoundary originCell)
+ {
+ var mergedCells = new List { originCell };
+
+ try
+ {
+ // 수평 병합 확인 (오른쪽으로 확장)
+ int maxCol = startCol;
+ for (int col = startCol + 1; col <= startCol + 10; col++) // 최대 10개까지만 확인
+ {
+ if (cellsByRowCol.TryGetValue((startRow, col), out var rightCell))
+ {
+ if (IsSameCellContent(originCell, rightCell) || string.IsNullOrEmpty(rightCell.CellText))
+ {
+ mergedCells.Add(rightCell);
+ maxCol = col;
+ }
+ else
+ {
+ break;
+ }
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ // 수직 병합 확인 (아래쪽으로 확장)
+ int maxRow = startRow;
+ for (int row = startRow + 1; row <= startRow + 10; row++) // 최대 10개까지만 확인
+ {
+ bool canExtendRow = true;
+ var rowCells = new List();
+
+ // 현재 행의 모든 열을 확인
+ for (int col = startCol; col <= maxCol; col++)
+ {
+ if (cellsByRowCol.TryGetValue((row, col), out var belowCell))
+ {
+ if (IsSameCellContent(originCell, belowCell) || string.IsNullOrEmpty(belowCell.CellText))
+ {
+ rowCells.Add(belowCell);
+ }
+ else
+ {
+ canExtendRow = false;
+ break;
+ }
+ }
+ else
+ {
+ canExtendRow = false;
+ break;
+ }
+ }
+
+ if (canExtendRow)
+ {
+ mergedCells.AddRange(rowCells);
+ maxRow = row;
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ Debug.WriteLine($"[MERGE] R{startRow}C{startCol} 병합 영역: R{startRow}-R{maxRow}, C{startCol}-C{maxCol} ({mergedCells.Count}개 셀)");
+ }
+ catch (System.Exception ex)
+ {
+ Debug.WriteLine($"[MERGE] 병합 영역 찾기 오류: {ex.Message}");
+ }
+
+ return mergedCells;
+ }
+
+ ///
+ /// 두 셀이 같은 내용을 가지는지 확인합니다.
+ ///
+ private bool IsSameCellContent(CellBoundary cell1, CellBoundary cell2)
+ {
+ return !string.IsNullOrEmpty(cell1.CellText) &&
+ !string.IsNullOrEmpty(cell2.CellText) &&
+ cell1.CellText.Trim() == cell2.CellText.Trim();
+ }
+
+ ///
+ /// CellBoundary의 텍스트 정보를 기존 테이블 데이터에 업데이트합니다.
+ ///
+ private object[,] UpdateTableDataWithCellBoundaries(object[,] originalTableData, List cellBoundaries)
+ {
+ try
+ {
+ if (originalTableData == null || cellBoundaries == null || cellBoundaries.Count == 0)
+ return originalTableData;
+
+ Debug.WriteLine($"[TABLE_UPDATE] 기존 테이블 ({originalTableData.GetLength(0)}x{originalTableData.GetLength(1)})을 {cellBoundaries.Count}개 셀 경계로 업데이트");
+
+ // CellBoundary에서 최대 Row/Column 찾기
+ int maxRow = 0, maxCol = 0;
+ var cellBoundaryDict = new Dictionary<(int row, int col), string>();
+
+ foreach (var cell in cellBoundaries)
+ {
+ var (row, col) = ParseRowColFromLabel(cell.Label);
+ if (row > 0 && col > 0)
+ {
+ maxRow = Math.Max(maxRow, row);
+ maxCol = Math.Max(maxCol, col);
+
+ // 텍스트가 있는 경우 딕셔너리에 저장
+ if (!string.IsNullOrEmpty(cell.CellText))
+ {
+ cellBoundaryDict[(row, col)] = cell.CellText;
+ }
+ }
+ }
+
+ Debug.WriteLine($"[TABLE_UPDATE] CellBoundary 최대 크기: {maxRow}x{maxCol}, 텍스트 있는 셀: {cellBoundaryDict.Count}개");
+
+ // 새로운 테이블 크기 결정 (기존 테이블과 CellBoundary 중 큰 크기)
+ int newRows = Math.Max(originalTableData.GetLength(0), maxRow);
+ int newCols = Math.Max(originalTableData.GetLength(1), maxCol);
+
+ var newTableData = new object[newRows, newCols];
+
+ // 기존 데이터 복사
+ for (int r = 0; r < originalTableData.GetLength(0); r++)
+ {
+ for (int c = 0; c < originalTableData.GetLength(1); c++)
+ {
+ newTableData[r, c] = originalTableData[r, c];
+ }
+ }
+
+ // CellBoundary 텍스트로 업데이트 (1-based → 0-based 변환)
+ foreach (var kvp in cellBoundaryDict)
+ {
+ var (row, col) = kvp.Key;
+ var text = kvp.Value;
+
+ int arrayRow = row - 1; // 1-based → 0-based
+ int arrayCol = col - 1; // 1-based → 0-based
+
+ if (arrayRow >= 0 && arrayRow < newRows && arrayCol >= 0 && arrayCol < newCols)
+ {
+ // 기존 값과 새로운 값 비교
+ var existingValue = newTableData[arrayRow, arrayCol]?.ToString() ?? "";
+
+ if (string.IsNullOrEmpty(existingValue) || existingValue != text)
+ {
+ newTableData[arrayRow, arrayCol] = text;
+ Debug.WriteLine($"[TABLE_UPDATE] [{arrayRow},{arrayCol}] 업데이트: '{existingValue}' → '{text}'");
+ }
+ }
+ }
+
+ Debug.WriteLine($"[TABLE_UPDATE] 테이블 데이터 업데이트 완료: {newRows}x{newCols}");
+ return newTableData;
+ }
+ catch (System.Exception ex)
+ {
+ Debug.WriteLine($"[TABLE_UPDATE] 테이블 데이터 업데이트 중 오류: {ex.Message}");
+ return originalTableData;
+ }
+ }
+
+ ///
+ /// 교차점이 테이블 내부에 있는지 확인합니다. (R1C1과 박스 외곽 제외)
+ ///
+ private bool IsInsideTable(IntersectionPoint point, List allIntersections)
+ {
+ try
+ {
+ // R1C1은 제외 (박스 외곽)
+ if (point.Row == 1 && point.Column == 1)
+ {
+ Debug.WriteLine($"[DIAGONAL] R{point.Row}C{point.Column} - R1C1 제외됨");
+ return false;
+ }
+
+ // 최소/최대 Row/Column 찾기
+ int minRow = allIntersections.Min(ip => ip.Row);
+ int maxRow = allIntersections.Max(ip => ip.Row);
+ int minCol = allIntersections.Min(ip => ip.Column);
+ int maxCol = allIntersections.Max(ip => ip.Column);
+
+ Debug.WriteLine($"[DIAGONAL] 테이블 범위: R{minRow}-R{maxRow}, C{minCol}-C{maxCol}");
+
+ // 박스 외곽 경계에 있는 교차점들 제외
+ bool isOnBoundary = (point.Row == minRow || point.Row == maxRow ||
+ point.Column == minCol || point.Column == maxCol);
+
+ if (isOnBoundary)
+ {
+ Debug.WriteLine($"[DIAGONAL] R{point.Row}C{point.Column} - 박스 외곽 경계에 있어 제외됨");
+ return false;
+ }
+
+ Debug.WriteLine($"[DIAGONAL] R{point.Row}C{point.Column} - 테이블 내부로 판정");
+ return true;
+ }
+ catch (System.Exception ex)
+ {
+ Debug.WriteLine($"[DIAGONAL] IsInsideTable 오류: {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// topLeft 교차점에 대한 첫 번째 bottomRight 교차점을 찾습니다. (row+1, col+1부터 점진적 탐색)
+ ///
+ private IntersectionPoint? FindFirstBottomRightForTopLeft(IntersectionPoint topLeft, List intersectionPoints)
+ {
+ try
+ {
+ // 교차점들을 Row/Column으로 딕셔너리 구성
+ var intersectionLookup = intersectionPoints
+ .Where(i => i.Row > 0 && i.Column > 0)
+ .GroupBy(i => i.Row)
+ .ToDictionary(g => g.Key, g => g.ToDictionary(i => i.Column, i => i));
+
+ Debug.WriteLine($"[DIAGONAL] 교차점 룩업 구성: {intersectionLookup.Count}개 행");
+
+ // topLeft에서 시작해서 row+1, col+1부터 점진적으로 탐색
+ for (int targetRow = topLeft.Row + 1; targetRow <= topLeft.Row + 10; targetRow++) // 최대 10행까지만 탐색
+ {
+ if (!intersectionLookup.ContainsKey(targetRow))
+ {
+ Debug.WriteLine($"[DIAGONAL] 행 {targetRow}에 교차점이 없음");
+ continue;
+ }
+
+ var rowIntersections = intersectionLookup[targetRow];
+ Debug.WriteLine($"[DIAGONAL] 행 {targetRow}에 {rowIntersections.Count}개 교차점");
+
+ // col+1부터 차례대로 찾기 (가장 가까운 것부터)
+ for (int targetColumn = topLeft.Column + 1; targetColumn <= topLeft.Column + 10; targetColumn++) // 최대 10열까지만 탐색
+ {
+ if (!rowIntersections.ContainsKey(targetColumn))
+ {
+ Debug.WriteLine($"[DIAGONAL] R{targetRow}C{targetColumn}에 교차점이 없음");
+ continue;
+ }
+
+ var candidate = rowIntersections[targetColumn];
+ Debug.WriteLine($"[DIAGONAL] 후보 R{targetRow}C{targetColumn} 검사: DirectionBits={candidate.DirectionBits}");
+
+ // bottomRight가 될 수 있는지 확인 (테이블 내부인지도 확인)
+ if (IsValidBottomRight(candidate.DirectionBits) &&
+ IsInsideTable(candidate, intersectionPoints))
+ {
+ Debug.WriteLine($"[DIAGONAL] 첫 번째 유효한 bottomRight 발견: R{targetRow}C{targetColumn}");
+ return candidate;
+ }
+ else if (candidate.DirectionBits == 13) // 수평 병합: col+1로 계속 검색
+ {
+ Debug.WriteLine($"[DIAGONAL] R{targetRow}C{targetColumn}는 수평 병합(13), col+1로 계속 검색");
+ continue; // 같은 행에서 다음 열로 이동
+ }
+ else if (candidate.DirectionBits == 11) // 수직 병합: row+1로 검색
+ {
+ Debug.WriteLine($"[DIAGONAL] R{targetRow}C{targetColumn}는 수직 병합(11), row+1로 검색");
+ break; // 다음 행으로 이동
+ }
+ else
+ {
+ Debug.WriteLine($"[DIAGONAL] R{targetRow}C{targetColumn}는 bottomRight로 부적절");
+ }
+ }
+ }
+
+ Debug.WriteLine($"[DIAGONAL] topLeft R{topLeft.Row}C{topLeft.Column}에 대한 bottomRight을 찾지 못함");
+ return null;
+ }
+ catch (System.Exception ex)
+ {
+ Debug.WriteLine($"[DIAGONAL] FindFirstBottomRightForTopLeft 오류: {ex.Message}");
+ return null;
+ }
+ }
+
+ ///
+ /// topLeft 교차점에 대한 모든 가능한 bottomRight 교차점들을 찾습니다.
+ ///
+ private List FindAllBottomRightsForTopLeft(IntersectionPoint topLeft, List intersectionPoints)
+ {
+ var validBottomRights = new List();
+
+ try
+ {
+ // 교차점들을 Row/Column으로 딕셔너리 구성
+ var intersectionLookup = intersectionPoints
+ .Where(i => i.Row > 0 && i.Column > 0)
+ .GroupBy(i => i.Row)
+ .ToDictionary(g => g.Key, g => g.ToDictionary(i => i.Column, i => i));
+
+ Debug.WriteLine($"[DIAGONAL] 교차점 룩업 구성: {intersectionLookup.Count}개 행");
+
+ // topLeft보다 오른쪽 아래에 있는 교차점들 중에서 찾기
+ int maxRow = intersectionLookup.Keys.Any() ? intersectionLookup.Keys.Max() : topLeft.Row;
+ int maxCol = intersectionLookup.Values.SelectMany(row => row.Keys).Any() ?
+ intersectionLookup.Values.SelectMany(row => row.Keys).Max() : topLeft.Column;
+
+ Debug.WriteLine($"[DIAGONAL] 최대 행: {maxRow}, 최대 열: {maxCol}");
+
+ // topLeft보다 아래/오른쪽에 있는 모든 교차점들을 체크
+ for (int targetRow = topLeft.Row + 1; targetRow <= maxRow + 2; targetRow++)
+ {
+ if (!intersectionLookup.ContainsKey(targetRow)) continue;
+
+ var rowIntersections = intersectionLookup[targetRow];
+
+ // topLeft와 같거나 오른쪽 컬럼들을 탐색
+ var availableColumns = rowIntersections.Keys
+ .Where(col => col >= topLeft.Column)
+ .OrderBy(col => col);
+
+ foreach (int targetColumn in availableColumns)
+ {
+ var candidate = rowIntersections[targetColumn];
+ Debug.WriteLine($"[DIAGONAL] 후보 R{targetRow}C{targetColumn} 검사: DirectionBits={candidate.DirectionBits}");
+
+ // bottomRight가 될 수 있는지 확인 (테이블 내부인지도 확인)
+ if (IsValidBottomRight(candidate.DirectionBits) &&
+ IsInsideTable(candidate, intersectionPoints))
+ {
+ validBottomRights.Add(candidate);
+ Debug.WriteLine($"[DIAGONAL] 유효한 bottomRight 추가: R{targetRow}C{targetColumn}");
+ }
+ else
+ {
+ Debug.WriteLine($"[DIAGONAL] R{targetRow}C{targetColumn}는 bottomRight로 부적절 (DirectionBits 또는 테이블 외부)");
+ }
+ }
+ }
+
+ Debug.WriteLine($"[DIAGONAL] topLeft R{topLeft.Row}C{topLeft.Column}에 대해 총 {validBottomRights.Count}개 bottomRight 발견");
+ return validBottomRights;
+ }
+ catch (System.Exception ex)
+ {
+ Debug.WriteLine($"[DIAGONAL] FindAllBottomRightsForTopLeft 오류: {ex.Message}");
+ return validBottomRights;
+ }
+ }
+
+ ///
+ /// topLeft 교차점에 대한 적절한 bottomRight 교차점을 찾습니다. (기존 메서드 - 호환성 유지)
+ ///
+ private IntersectionPoint? FindBottomRightForTopLeft(IntersectionPoint topLeft, List intersectionPoints)
+ {
+ try
+ {
+ // 교차점들을 Row/Column으로 딕셔너리 구성
+ var intersectionLookup = intersectionPoints
+ .Where(i => i.Row > 0 && i.Column > 0)
+ .GroupBy(i => i.Row)
+ .ToDictionary(g => g.Key, g => g.ToDictionary(i => i.Column, i => i));
+
+ Debug.WriteLine($"[DIAGONAL] 교차점 룩업 구성: {intersectionLookup.Count}개 행");
+
+ // topLeft보다 오른쪽 아래에 있는 교차점들 중에서 찾기
+ int maxRow = intersectionLookup.Keys.Any() ? intersectionLookup.Keys.Max() : topLeft.Row;
+ Debug.WriteLine($"[DIAGONAL] 최대 행 번호: {maxRow}");
+
+ // topLeft보다 아래 행들을 탐색
+ for (int targetRow = topLeft.Row + 1; targetRow <= maxRow + 2; targetRow++)
+ {
+ if (!intersectionLookup.ContainsKey(targetRow))
+ {
+ Debug.WriteLine($"[DIAGONAL] 행 {targetRow}에 교차점이 없음");
+ continue;
+ }
+
+ var rowIntersections = intersectionLookup[targetRow];
+ Debug.WriteLine($"[DIAGONAL] 행 {targetRow}에 {rowIntersections.Count}개 교차점");
+
+ // topLeft와 같거나 오른쪽 컬럼들을 탐색 (컬럼 순서대로)
+ var availableColumns = rowIntersections.Keys
+ .Where(col => col >= topLeft.Column)
+ .OrderBy(col => col);
+
+ foreach (int targetColumn in availableColumns)
+ {
+ var candidate = rowIntersections[targetColumn];
+ Debug.WriteLine($"[DIAGONAL] 후보 R{targetRow}C{targetColumn} 검사: DirectionBits={candidate.DirectionBits}");
+
+ // bottomRight가 될 수 있는지 확인
+ if (IsValidBottomRight(candidate.DirectionBits))
+ {
+ Debug.WriteLine($"[DIAGONAL] 유효한 bottomRight 발견: R{targetRow}C{targetColumn}");
+ return candidate;
+ }
+ else
+ {
+ Debug.WriteLine($"[DIAGONAL] R{targetRow}C{targetColumn}는 bottomRight로 부적절");
+ }
+ }
+ }
+
+ Debug.WriteLine($"[DIAGONAL] topLeft R{topLeft.Row}C{topLeft.Column}에 대한 bottomRight을 찾지 못함");
+ return null;
+ }
+ catch (System.Exception ex)
+ {
+ Debug.WriteLine($"[DIAGONAL] FindBottomRightForTopLeft 오류: {ex.Message}");
+ return null;
+ }
+ }
+
+ private (Dictionary> assignedTexts, List unassignedTextIds) AssignTextsToCells(
+ Transaction tran,
+ List cells,
+ List textIds)
+ {
+ var assignedTexts = new Dictionary>();
+ var unassignedTextIds = new List(textIds);
+ var assignedIds = new HashSet();
+
+ foreach (var cell in cells)
+ {
+ var textsInCell = new List();
+ foreach (var textId in textIds)
+ {
+ if (assignedIds.Contains(textId)) continue;
+
+ using (var dbText = tran.GetObject(textId, OpenMode.ForRead) as DBText)
+ {
+ if (dbText != null)
+ {
+ // Check if the text's alignment point is inside the cell
+ if (IsPointInCell(dbText.Position, cell))
+ {
+ textsInCell.Add(dbText.TextString);
+ cell.CellText = string.Join("\n", textsInCell);
+ assignedIds.Add(textId);
+ }
+ }
+ }
+ }
+ if(textsInCell.Any())
+ {
+ assignedTexts[cell] = textsInCell;
+ }
+ }
+
+ unassignedTextIds.RemoveAll(id => assignedIds.Contains(id));
+
+ return (assignedTexts, unassignedTextIds);
+ }
+
+ private bool IsPointInCell(Point3d point, TableCell cell)
+ {
+ double tolerance = 1e-6;
+ return point.X >= cell.MinPoint.X - tolerance &&
+ point.X <= cell.MaxPoint.X + tolerance &&
+ point.Y >= cell.MinPoint.Y - tolerance &&
+ point.Y <= cell.MaxPoint.Y + tolerance;
+ }
+
+ private object[,] CreateTableDataArray(List cells)
+ {
+ if (!cells.Any()) return new object[0, 0];
+
+ int rows = cells.Max(c => c.Row + c.RowSpan);
+ int cols = cells.Max(c => c.Column + c.ColumnSpan);
+ var tableData = new object[rows, cols];
+
+ foreach (var cell in cells)
+ {
+ if (cell.Row < rows && cell.Column < cols)
+ {
+ tableData[cell.Row, cell.Column] = cell.CellText;
+ }
+ }
+ return tableData;
+ }
+
+ private void DetectAndMergeCells(List cells, List<(Point3d start, Point3d end, bool isHorizontal)> segments, double tolerance)
+ {
+ // This is a placeholder implementation.
+ // The actual logic would be complex, involving checking for missing separators.
+ // For now, we'll just log that it was called.
+ Debug.WriteLine("[DEBUG] DetectAndMergeCells called, but not implemented.");
+ }
+
+ private bool IsPointOnSegment(Point3d point, Point3d start, Point3d end, double tolerance)
+ {
+ // Check if the point is within the bounding box of the segment
+ bool inBounds = point.X >= Math.Min(start.X, end.X) - tolerance &&
+ point.X <= Math.Max(start.X, end.X) + tolerance &&
+ point.Y >= Math.Min(start.Y, end.Y) - tolerance &&
+ point.Y <= Math.Max(start.Y, end.Y) + tolerance;
+
+ if (!inBounds) return false;
+
+ // Check for collinearity
+ double crossProduct = (point.Y - start.Y) * (end.X - start.X) - (point.X - start.X) * (end.Y - start.Y);
+
+ return Math.Abs(crossProduct) < tolerance * tolerance;
+ }
+
+
+ } // DwgDataExtractor 클래스 끝
+
+ ///
+ /// Note 추출 결과를 담는 클래스
+ ///
+ public class NoteExtractionResult
+ {
+ public List NoteEntities { get; set; } = new List();
+ public List IntersectionPoints { get; set; } = new List();
+ public List<(Teigha.Geometry.Point3d topLeft, Teigha.Geometry.Point3d bottomRight, string label)> DiagonalLines { get; set; } = new List<(Teigha.Geometry.Point3d, Teigha.Geometry.Point3d, string)>();
+ public List TableSegments { get; set; } = new List();
}
+ ///
+
///
/// DWG ���� ����� ��� Ŭ����
///
@@ -1055,5 +2900,154 @@ namespace DwgExtractorManual.Models
public double X { get; set; } = 0; // X 좌표
public double Y { get; set; } = 0; // Y 좌표
public int SortOrder { get; set; } = 0; // 정렬 순서 (Note=0, NoteContent=1,2,3...)
+ public object[,] TableData { get; set; } = new object[0, 0]; // 테이블 데이터 (2D 배열)
+ public List Cells { get; set; } = new List(); // 테이블 셀 정보 (병합용)
+ public List TableSegments { get; set; } = new List(); // 테이블 세그먼트 정보
+ public List IntersectionPoints { get; set; } = new List(); // 교차점 정보
+ public List DiagonalLines { get; set; } = new List(); // 대각선 정보
+ public List CellBoundaries { get; set; } = new List(); // 정확한 셀 경계 정보
+
+ public string TableCsv
+ {
+ get
+ {
+ if (TableData == null || TableData.Length == 0) return "";
+
+ var csvLines = new List();
+ int rows = TableData.GetLength(0);
+ int cols = TableData.GetLength(1);
+
+ for (int r = 0; r < rows; r++)
+ {
+ var rowValues = new List();
+ for (int c = 0; c < cols; c++)
+ {
+ var cellValue = TableData[r, c]?.ToString() ?? "";
+ if (cellValue.Contains(",") || cellValue.Contains("\"") || cellValue.Contains("\n"))
+ {
+ cellValue = "\"" + cellValue.Replace("\"", "\"\"") + "\"";
+ }
+ rowValues.Add(cellValue);
+ }
+ csvLines.Add(string.Join(",", rowValues));
+ }
+ return string.Join("\n", csvLines);
+ }
+ }
+ }
+
+ ///
+ /// 테이블 셀 정보 클래스
+ ///
+ public class TableCell
+ {
+ public Point3d MinPoint { get; set; } = new Point3d(); // 왼쪽 하단
+ public Point3d MaxPoint { get; set; } = new Point3d(); // 오른쪽 상단
+ public int Row { get; set; } = 0; // 행 번호 (0부터 시작)
+ public int Column { get; set; } = 0; // 열 번호 (0부터 시작)
+ public int RowSpan { get; set; } = 1; // 행 병합 수
+ public int ColumnSpan { get; set; } = 1; // 열 병합 수
+ public string CellText { get; set; } = ""; // 셀 내부의 텍스트
+
+ ///
+ /// 셀의 중심 좌표를 반환합니다.
+ ///
+ public Point3d CenterPoint => new Point3d(
+ (MinPoint.X + MaxPoint.X) / 2,
+ (MinPoint.Y + MaxPoint.Y) / 2,
+ 0
+ );
+
+ ///
+ /// 셀의 너비를 반환합니다.
+ ///
+ public double Width => MaxPoint.X - MinPoint.X;
+
+ ///
+ /// 셀의 높이를 반환합니다.
+ ///
+ public double Height => MaxPoint.Y - MinPoint.Y;
+
+ ///
+ /// 점이 이 셀 내부에 있는지 확인합니다.
+ ///
+ public bool ContainsPoint(Point3d point, double tolerance = 0.01)
+ {
+ return point.X >= MinPoint.X - tolerance && point.X <= MaxPoint.X + tolerance &&
+ point.Y >= MinPoint.Y - tolerance && point.Y <= MaxPoint.Y + tolerance;
+ }
+
+ public override string ToString()
+ {
+ return $"Cell[{Row},{Column}] Size({RowSpan}x{ColumnSpan}) Area({MinPoint.X:F1},{MinPoint.Y:F1})-({MaxPoint.X:F1},{MaxPoint.Y:F1})";
+ }
+ }
+
+ ///
+ /// 방향을 비트 플래그로 나타내는 상수
+ ///
+ public static class DirectionFlags
+ {
+ public const int Right = 1; // 0001
+ public const int Up = 2; // 0010
+ public const int Left = 4; // 0100
+ public const int Down = 8; // 1000
+
+ // MERGED 셀 타입들
+ public const int HorizontalMerged = 16; // 0001 0000 - 세로선이 없음 (좌우 합병)
+ public const int VerticalMerged = 32; // 0010 0000 - 가로선이 없음 (상하 합병)
+ public const int CrossMerged = 48; // 0011 0000 - 모든 선이 없음 (완전 합병)
+ }
+
+ ///
+ /// 교차점 정보를 담는 클래스
+ ///
+ public class IntersectionPoint
+ {
+ public Point3d Position { get; set; }
+ public int DirectionBits { get; set; } = 0; // 비트 플래그로 방향 저장
+ public int Row { get; set; } = -1; // 교차점의 행 번호
+ public int Column { get; set; } = -1; // 교차점의 열 번호
+
+ public bool HasRight => (DirectionBits & DirectionFlags.Right) != 0;
+ public bool HasUp => (DirectionBits & DirectionFlags.Up) != 0;
+ public bool HasLeft => (DirectionBits & DirectionFlags.Left) != 0;
+ public bool HasDown => (DirectionBits & DirectionFlags.Down) != 0;
+
+ // MERGED 셀 타입 확인 속성들
+ public bool IsHorizontalMerged => (DirectionBits & DirectionFlags.HorizontalMerged) != 0;
+ public bool IsVerticalMerged => (DirectionBits & DirectionFlags.VerticalMerged) != 0;
+ public bool IsCrossMerged => (DirectionBits & DirectionFlags.CrossMerged) != 0;
+
+ public override string ToString()
+ {
+ var directions = new List();
+ if (HasRight) directions.Add("R");
+ if (HasUp) directions.Add("U");
+ if (HasLeft) directions.Add("L");
+ if (HasDown) directions.Add("D");
+ return $"Intersection[{DirectionBits}] at ({Position.X:F1},{Position.Y:F1}) [{string.Join(",", directions)}]";
+ }
+ }
+
+ ///
+ /// 셀의 4개 모서리 좌표를 나타내는 클래스
+ ///
+ public class CellBoundary
+ {
+ public Point3d TopLeft { get; set; }
+ public Point3d TopRight { get; set; }
+ public Point3d BottomLeft { get; set; }
+ public Point3d BottomRight { get; set; }
+ public string Label { get; set; } = "";
+ public double Width { get; set; }
+ public double Height { get; set; }
+ public string CellText { get; set; } = ""; // 셀 내 텍스트 내용
+
+ public override string ToString()
+ {
+ return $"CellBoundary[{Label}] ({Width:F1}x{Height:F1}) " +
+ $"TL:({TopLeft.X:F1},{TopLeft.Y:F1}) BR:({BottomRight.X:F1},{BottomRight.Y:F1})";
+ }
}
}
\ No newline at end of file
diff --git a/Models/ExcelDataWriter.cs b/Models/ExcelDataWriter.cs
index f1183df..9ab45a1 100644
--- a/Models/ExcelDataWriter.cs
+++ b/Models/ExcelDataWriter.cs
@@ -62,6 +62,65 @@ namespace DwgExtractorManual.Models
}
}
+ ///
+ /// Note 엔터티 데이터를 Excel 시트에 쓰기 (테이블 및 셀 병합 포함)
+ ///
+ public void WriteNoteEntityData(List 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;
+ }
+ }
+
///
/// ���� ������ Excel ��Ʈ�� ���
///
@@ -144,8 +203,8 @@ namespace DwgExtractorManual.Models
for (int row = 2; row <= lastRow; row++)
{
- var cellFileName = excelManager.MappingSheet.Cells[row, 1]?.Value?.ToString() ?? "";
- var cellAiLabel = excelManager.MappingSheet.Cells[row, 3]?.Value?.ToString() ?? "";
+ 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))
@@ -278,6 +337,7 @@ namespace DwgExtractorManual.Models
///
/// Note 엔티티들을 Excel 워크시트에 기록합니다 (기존 데이터 아래에 추가).
+ /// CellBoundary 데이터를 사용하여 병합된 셀의 텍스트를 적절히 처리합니다.
///
public void WriteNoteEntities(List noteEntities, Excel.Worksheet worksheet, string fileName)
{
@@ -307,18 +367,28 @@ namespace DwgExtractorManual.Models
int startRow = lastRow + 2; // 한 줄 띄우고 시작
Debug.WriteLine($"[DEBUG] Note 데이터 기록 시작: {startRow}행부터 {noteEntities.Count}개 항목");
- // Note 섹션 헤더 추가 (간단한 방식으로 변경)
+ // Note 섹션 헤더 추가 (표 컬럼 포함)
try
{
- worksheet.Cells[startRow - 1, 1] = "=== Notes ===";
+ 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 = worksheet.Cells[startRow - 1, 1];
+ var headerCell = (Excel.Range)worksheet.Cells[startRow - 1, 1];
headerCell.Font.Bold = true;
headerCell.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightYellow);
}
@@ -327,29 +397,130 @@ namespace DwgExtractorManual.Models
Debug.WriteLine($"[DEBUG] Note 헤더 작성 오류: {ex.Message}");
}
- // Note 데이터 입력 (배치 방식으로 성능 향상)
+ // 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 ?? "";
+ worksheet.Cells[row, 6] = noteEntity.Text ?? ""; // 일반 텍스트만 (표 데이터 제외)
+
+ int currentRow = row; // 현재 처리 중인 행 번호
- // "NOTE" 타입인 경우 행 배경색 변경
+ // 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 (cellRow, cellCol) = ParseRowColFromLabel(cellBoundary.Label);
+ Debug.WriteLine($"[DEBUG] CellBoundary 처리: {cellBoundary.Label} → Row={cellRow}, Col={cellCol}, Text='{cellBoundary.CellText}'");
+
+ if (cellRow > 0 && cellCol > 0)
+ {
+ // Excel에서 테이블 위치 계산:
+ // R1 → Note의 현재 행 (currentRow)
+ // R2 → Note의 다음 행 (currentRow + 1)
+ // C1 → G열(7), C2 → H열(8)
+ int excelRow = currentRow + (cellRow - 1); // R1=currentRow, R2=currentRow+1, ...
+ int excelCol = 7 + (cellCol - 1); // C1=G열(7), C2=H열(8), ...
+
+ Debug.WriteLine($"[DEBUG] Excel 위치: {cellBoundary.Label} → Excel[{excelRow},{excelCol}]");
+
+ // Excel 범위 체크 (최대 20개 컬럼까지)
+ if (excelCol <= 26) // Z열까지
+ {
+ // CellText가 비어있어도 일단 배치해보기 (디버그용)
+ var cellValue = string.IsNullOrEmpty(cellBoundary.CellText) ? "[빈셀]" : cellBoundary.CellText;
+ worksheet.Cells[excelRow, excelCol] = cellValue;
+ Debug.WriteLine($"[DEBUG] ✅ 셀 배치 완료: {cellBoundary.Label} → Excel[{excelRow},{excelCol}] = '{cellValue}'");
+ }
+ else
+ {
+ Debug.WriteLine($"[DEBUG] ❌ Excel 컬럼 범위 초과: {excelCol} > 26");
+ }
+
+ // 테이블이 차지하는 최대 행 수 추적
+ maxTableRow = Math.Max(maxTableRow, cellRow);
+ }
+ else
+ {
+ Debug.WriteLine($"[DEBUG] ❌ 잘못된 Row/Col: {cellBoundary.Label} → Row={cellRow}, Col={cellCol}");
+ }
+ }
+
+ // 테이블이 여러 행을 차지하는 경우 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('"'); // 따옴표 제거
+ worksheet.Cells[currentRow, 7 + cellIndex] = 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[row, 6]];
+ 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}, Order {noteEntity.SortOrder}, Type {noteEntity.Type}, Pos({noteEntity.X:F1},{noteEntity.Y:F1}), Text: '{noteEntity.Text}'");
- row++;
+ 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}개 항목");
@@ -377,5 +548,46 @@ namespace DwgExtractorManual.Models
throw; // 상위로 예외 전파
}
}
+
+
+ ///
+ /// 라벨에서 Row, Col 정보를 파싱합니다.
+ /// 예: "R1C2" → (1, 2) 또는 "R2C2→R3C4" → (2, 2) (시작 위치 사용)
+ ///
+ private (int row, int col) ParseRowColFromLabel(string label)
+ {
+ try
+ {
+ if (string.IsNullOrEmpty(label))
+ return (0, 0);
+
+ // "R2C2→R3C4" 형태인 경우 시작 부분만 사용
+ var startPart = label;
+ if (label.Contains("→"))
+ {
+ startPart = label.Split('→')[0];
+ }
+
+ var rIndex = startPart.IndexOf('R');
+ var cIndex = startPart.IndexOf('C');
+
+ if (rIndex >= 0 && cIndex > rIndex)
+ {
+ var rowStr = startPart.Substring(rIndex + 1, cIndex - rIndex - 1);
+ var colStr = startPart.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);
+ }
+ }
}
}
\ No newline at end of file
diff --git a/Models/ExcelManager.cs b/Models/ExcelManager.cs
index cefa558..78a82f9 100644
--- a/Models/ExcelManager.cs
+++ b/Models/ExcelManager.cs
@@ -7,7 +7,7 @@ using Excel = Microsoft.Office.Interop.Excel;
namespace DwgExtractorManual.Models
{
///
- /// Excel COM ü ⺻ ۾ ϴ Ŭ
+ /// Excel COM ��ü ���� �� �⺻ �۾��� ����ϴ� Ŭ����
///
internal class ExcelManager : IDisposable
{
@@ -17,10 +17,11 @@ namespace DwgExtractorManual.Models
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; }
///
- /// Excel ø̼ ũƮ ʱȭ
+ /// Excel ���ø����̼� �� ��ũ��Ʈ �ʱ�ȭ
///
public void InitializeExcel()
{
@@ -28,21 +29,26 @@ namespace DwgExtractorManual.Models
{
var excelApp = new Excel.Application();
ExcelApplication = excelApp;
- ExcelApplication.Visible = false; // WPF ó
+ ExcelApplication.Visible = false; // WPF������ ���� ó��
Excel.Workbook workbook = excelApp.Workbooks.Add();
TitleBlockWorkbook = workbook;
- // Title Block Sheet (⺻ Sheet1)
+ // Title Block Sheet ���� (�⺻ Sheet1)
TitleBlockSheet = (Excel.Worksheet)workbook.Sheets[1];
TitleBlockSheet.Name = "Title Block";
SetupTitleBlockHeaders();
- // Text Entities Sheet ߰
+ // Text Entities Sheet �߰�
TextEntitiesSheet = (Excel.Worksheet)workbook.Sheets.Add();
TextEntitiesSheet.Name = "Text Entities";
SetupTextEntitiesHeaders();
- // Ϳ ũ Ʈ
+ // Note Entities Sheet �߰�
+ NoteEntitiesSheet = (Excel.Worksheet)workbook.Sheets.Add();
+ NoteEntitiesSheet.Name = "Note Entities";
+ SetupNoteEntitiesHeaders();
+
+ // ���� �����Ϳ� ��ũ�� �� ��Ʈ ����
MappingWorkbook = excelApp.Workbooks.Add();
MappingSheet = (Excel.Worksheet)MappingWorkbook.Sheets[1];
MappingSheet.Name = "Mapping Data";
@@ -50,14 +56,14 @@ namespace DwgExtractorManual.Models
}
catch (System.Exception ex)
{
- Debug.WriteLine($"Excel ʱȭ : {ex.Message}");
+ Debug.WriteLine($"Excel �ʱ�ȭ �� ���� ��: {ex.Message}");
ReleaseExcelObjects();
throw;
}
}
///
- /// Excel Ʈ
+ /// ���� Excel ������ ���� ���� ��Ʈ�� ����
///
public bool OpenExistingFile(string excelFilePath)
{
@@ -65,7 +71,7 @@ namespace DwgExtractorManual.Models
{
if (!File.Exists(excelFilePath))
{
- Debug.WriteLine($"? Excel ʽϴ: {excelFilePath}");
+ Debug.WriteLine($"? Excel ������ �������� �ʽ��ϴ�: {excelFilePath}");
return false;
}
@@ -80,7 +86,7 @@ namespace DwgExtractorManual.Models
if (MappingSheet == null)
{
- Debug.WriteLine("? 'Mapping Data' Ʈ ã ϴ.");
+ Debug.WriteLine("? 'Mapping Data' ��Ʈ�� ã�� �� �����ϴ�.");
return false;
}
@@ -88,13 +94,13 @@ namespace DwgExtractorManual.Models
}
catch (System.Exception ex)
{
- Debug.WriteLine($"? Excel : {ex.Message}");
+ Debug.WriteLine($"? Excel ���� ���� �� ����: {ex.Message}");
return false;
}
}
///
- /// ο ũ (Height Ŀ)
+ /// ���ο� ��ũ�� ���� (Height ���Ŀ�)
///
public Excel.Workbook CreateNewWorkbook()
{
@@ -106,62 +112,83 @@ namespace DwgExtractorManual.Models
return ExcelApplication.Workbooks.Add();
}
- // Title Block Ʈ
+ // Title Block ��Ʈ ��� ����
private void SetupTitleBlockHeaders()
{
if (TitleBlockSheet == null) return;
- TitleBlockSheet.Cells[1, 1] = "Type"; // : AttributeReference, AttributeDefinition
- TitleBlockSheet.Cells[1, 2] = "Name"; // BlockReference ̸ Ǵ BlockDefinition ̸
+ 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 ̸
+ 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 Ʈ
+ // Text Entities ��Ʈ ��� ����
private void SetupTextEntitiesHeaders()
{
if (TextEntitiesSheet == null) return;
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 ̸
+ 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);
}
- // Ʈ
+ // ���� ������ ��Ʈ ��� ����
private void SetupMappingHeaders()
{
if (MappingSheet == null) return;
- MappingSheet.Cells[1, 1] = "FileName"; // ̸
- MappingSheet.Cells[1, 2] = "MapKey"; // Ű
- MappingSheet.Cells[1, 3] = "AILabel"; // AI
+ MappingSheet.Cells[1, 1] = "FileName"; // ���� �̸�
+ MappingSheet.Cells[1, 2] = "MapKey"; // ���� Ű
+ MappingSheet.Cells[1, 3] = "AILabel"; // AI ��
MappingSheet.Cells[1, 4] = "DwgTag"; // DWG Tag
- MappingSheet.Cells[1, 5] = "Att_value"; // DWG
- MappingSheet.Cells[1, 6] = "Pdf_value"; // PDF ( )
+ MappingSheet.Cells[1, 5] = "Att_value"; // DWG ��
+ MappingSheet.Cells[1, 6] = "Pdf_value"; // PDF �� (����� �� ��)
- // Ÿ
+ // ��� �� ��Ÿ��
Excel.Range headerRange = MappingSheet.Range["A1:F1"];
headerRange.Font.Bold = true;
headerRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightYellow);
}
+ // Note Entities ��Ʈ ��� ����
+ private void SetupNoteEntitiesHeaders()
+ {
+ if (NoteEntitiesSheet == null) return;
+
+ NoteEntitiesSheet.Cells[1, 1] = "Type"; // Note, NoteContent
+ NoteEntitiesSheet.Cells[1, 2] = "Layer"; // Layer �̸�
+ NoteEntitiesSheet.Cells[1, 3] = "Text"; // ���� �ؽ�Ʈ ����
+ NoteEntitiesSheet.Cells[1, 4] = "X"; // X ��ǥ
+ NoteEntitiesSheet.Cells[1, 5] = "Y"; // Y ��ǥ
+ NoteEntitiesSheet.Cells[1, 6] = "SortOrder"; // ���� ����
+ NoteEntitiesSheet.Cells[1, 7] = "TableCsv"; // ���̺� CSV ������
+ NoteEntitiesSheet.Cells[1, 8] = "Path"; // ���� DWG ���� ��ü ���
+ NoteEntitiesSheet.Cells[1, 9] = "FileName"; // ���� DWG ���� �̸���
+
+ // ��� �� ��Ÿ��
+ Excel.Range headerRange = NoteEntitiesSheet.Range["A1:I1"];
+ headerRange.Font.Bold = true;
+ headerRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightCoral);
+ }
+
///
- /// ũ
+ /// ��ũ�� ����
///
public bool SaveWorkbook(Excel.Workbook? workbook = null)
{
@@ -189,13 +216,13 @@ namespace DwgExtractorManual.Models
}
catch (System.Exception ex)
{
- Debug.WriteLine($"? Excel : {ex.Message}");
+ Debug.WriteLine($"? Excel ���� ���� �� ����: {ex.Message}");
return false;
}
}
///
- /// ũ ο
+ /// ��ũ���� ������ ��ο� ����
///
public void SaveWorkbookAs(Excel.Workbook? workbook, string savePath)
{
@@ -213,14 +240,14 @@ namespace DwgExtractorManual.Models
}
///
- /// Excel Ʈ ִ ȿ ̸ մϴ.
+ /// Excel ��Ʈ������ ����� �� �ִ� ��ȿ�� �̸��� �����մϴ�.
///
public string GetValidSheetName(string originalName)
{
if (string.IsNullOrEmpty(originalName))
return "Sheet";
- // Excel Ʈ ʴ
+ // Excel ��Ʈ������ ������ �ʴ� ���� ����
string validName = originalName;
char[] invalidChars = { '\\', '/', '?', '*', '[', ']', ':' };
@@ -229,7 +256,7 @@ namespace DwgExtractorManual.Models
validName = validName.Replace(c, '_');
}
- // 31ڷ (Excel Ʈ ִ )
+ // 31�ڷ� ���� (Excel ��Ʈ�� �ִ� ����)
if (validName.Length > 31)
{
validName = validName.Substring(0, 31);
@@ -261,6 +288,7 @@ namespace DwgExtractorManual.Models
{
ReleaseComObject(TitleBlockSheet);
ReleaseComObject(TextEntitiesSheet);
+ ReleaseComObject(NoteEntitiesSheet);
ReleaseComObject(MappingSheet);
ReleaseComObject(TitleBlockWorkbook);
ReleaseComObject(MappingWorkbook);
@@ -268,6 +296,7 @@ namespace DwgExtractorManual.Models
TitleBlockSheet = null;
TextEntitiesSheet = null;
+ NoteEntitiesSheet = null;
MappingSheet = null;
TitleBlockWorkbook = null;
MappingWorkbook = null;
@@ -285,7 +314,7 @@ namespace DwgExtractorManual.Models
}
catch (System.Exception)
{
- //
+ // ���� �� ���� �� �� ����
}
}
@@ -293,16 +322,16 @@ namespace DwgExtractorManual.Models
{
try
{
- Debug.WriteLine("[DEBUG] ExcelManager Dispose ");
+ Debug.WriteLine("[DEBUG] ExcelManager Dispose ����");
CloseWorkbooks();
ReleaseExcelObjects();
GC.Collect();
GC.WaitForPendingFinalizers();
- Debug.WriteLine("[DEBUG] ExcelManager Dispose Ϸ");
+ Debug.WriteLine("[DEBUG] ExcelManager Dispose �Ϸ�");
}
catch (System.Exception ex)
{
- Debug.WriteLine($"[DEBUG] ExcelManager Dispose : {ex.Message}");
+ Debug.WriteLine($"[DEBUG] ExcelManager Dispose �� ����: {ex.Message}");
}
}
}
diff --git a/Models/ExportExcel.cs b/Models/ExportExcel.cs
index bc92844..324e01b 100644
--- a/Models/ExportExcel.cs
+++ b/Models/ExportExcel.cs
@@ -17,7 +17,7 @@ namespace DwgExtractorManual.Models
{
// 컴포넌트들
private readonly ExcelManager excelManager;
- private readonly DwgDataExtractor dwgExtractor;
+ public readonly DwgDataExtractor DwgExtractor;
private readonly JsonDataProcessor jsonProcessor;
private readonly ExcelDataWriter excelWriter;
private readonly FieldMapper fieldMapper;
@@ -29,7 +29,7 @@ namespace DwgExtractorManual.Models
private Dictionary> FileToMapkeyToLabelTagValuePdf
= new Dictionary>();
- readonly List MapKeys;
+ readonly List? MapKeys;
///
/// 생성자: 모든 컴포넌트 초기화
@@ -41,7 +41,7 @@ namespace DwgExtractorManual.Models
Debug.WriteLine("🔄 FieldMapper 로딩 중: mapping_table_json.json...");
fieldMapper = FieldMapper.LoadFromFile("fletimageanalysis/mapping_table_json.json");
Debug.WriteLine("✅ FieldMapper 로딩 성공");
- MapKeys = fieldMapper.GetAllDocAiKeys();
+ MapKeys = fieldMapper.GetAllDocAiKeys() ?? new List();
Debug.WriteLine($"📊 총 DocAI 키 개수: {MapKeys?.Count ?? 0}");
// 매핑 테스트 (디버깅용)
@@ -52,7 +52,7 @@ namespace DwgExtractorManual.Models
// 컴포넌트들 초기화
excelManager = new ExcelManager();
- dwgExtractor = new DwgDataExtractor(fieldMapper);
+ DwgExtractor = new DwgDataExtractor(fieldMapper);
jsonProcessor = new JsonDataProcessor();
excelWriter = new ExcelDataWriter(excelManager);
@@ -88,7 +88,7 @@ namespace DwgExtractorManual.Models
try
{
// DWG 데이터 추출
- var extractionResult = dwgExtractor.ExtractFromDwgFile(filePath, progress, cancellationToken);
+ var extractionResult = DwgExtractor.ExtractFromDwgFile(filePath, progress, cancellationToken);
if (extractionResult == null)
{
@@ -191,14 +191,14 @@ namespace DwgExtractorManual.Models
try
{
- var worksheet = firstSheetProcessed ?
- heightSortedWorkbook.Worksheets.Add() :
+ 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;
- var textEntities = dwgExtractor.ExtractTextEntitiesWithHeight(dwgFile);
+ var textEntities = DwgExtractor.ExtractTextEntitiesWithHeight(dwgFile);
excelWriter.WriteHeightSortedData(textEntities, worksheet, fileName);
Debug.WriteLine($"[DEBUG] {fileName} 시트 완료: {textEntities.Count}개 엔티티");
@@ -212,7 +212,7 @@ namespace DwgExtractorManual.Models
if (!firstSheetProcessed)
{
- var defaultSheet = (Microsoft.Office.Interop.Excel.Worksheet)heightSortedWorkbook.Worksheets[1];
+ 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";
}
@@ -238,6 +238,10 @@ namespace DwgExtractorManual.Models
{
Debug.WriteLine($"[DEBUG] 단일 Excel 파일로 Height 정렬 생성 시작: {allDwgFiles.Count}개 파일");
+ // 시각화 데이터 초기화
+ MainWindow.ClearVisualizationData();
+ Debug.WriteLine("[VISUALIZATION] 시각화 데이터 초기화 완료");
+
var heightSortedWorkbook = excelManager.CreateNewWorkbook();
bool firstSheetProcessed = false;
@@ -252,22 +256,22 @@ namespace DwgExtractorManual.Models
try
{
- var worksheet = firstSheetProcessed ?
- heightSortedWorkbook.Worksheets.Add() :
+ 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;
- var textEntities = dwgExtractor.ExtractTextEntitiesWithHeight(filePath);
+ var textEntities = DwgExtractor.ExtractTextEntitiesWithHeight(filePath);
excelWriter.WriteHeightSortedData(textEntities, worksheet, fileName);
// Note 엔티티 추출 및 기록
- var noteEntities = dwgExtractor.ExtractNotesFromDrawing(filePath);
- if (noteEntities.Count > 0)
+ var noteEntities = DwgExtractor.ExtractNotesFromDrawing(filePath);
+ if (noteEntities.NoteEntities.Count > 0)
{
- excelWriter.WriteNoteEntities(noteEntities, worksheet, fileName);
- Debug.WriteLine($"[DEBUG] {fileName}: {noteEntities.Count}개 Note 엔티티 추가됨");
+ excelWriter.WriteNoteEntities(noteEntities.NoteEntities, worksheet, fileName);
+ Debug.WriteLine($"[DEBUG] {fileName}: {noteEntities.NoteEntities.Count}개 Note 엔티티 추가됨");
}
Debug.WriteLine($"[DEBUG] {fileName} 시트 완료: {textEntities.Count}개 엔티티");
@@ -281,7 +285,7 @@ namespace DwgExtractorManual.Models
if (!firstSheetProcessed)
{
- var defaultSheet = (Microsoft.Office.Interop.Excel.Worksheet)heightSortedWorkbook.Worksheets[1];
+ 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";
}
diff --git a/Models/ExportExcel_old.cs b/Models/ExportExcel_old.cs
deleted file mode 100644
index b28b721..0000000
--- a/Models/ExportExcel_old.cs
+++ /dev/null
@@ -1,2382 +0,0 @@
-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;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
-
-namespace DwgExtractorManual.Models
-{
- ///
- /// DWG 파일에서 Excel로 데이터 내보내기 클래스
- /// AttributeReference, AttributeDefinition, DBText, MText 추출 지원
- ///
- internal class ExportExcel_old : IDisposable
- {
- // ODA 서비스 객체 (managed by singleton)
- private Services appServices;
-
- // Excel COM 객체들
- private Excel.Application excelApplication;
- private Excel.Workbook workbook1;
- private Excel.Worksheet titleBlockSheet; // Title Block용 시트
- private Excel.Worksheet textEntitiesSheet; // Text Entities용 시트
- private Excel.Workbook mappingWorkbook; // 매핑 데이터용 워크북
- private Excel.Worksheet mappingSheet; // 매핑 데이터용 시트
-
- readonly List MapKeys;
-
-
- // 각 시트의 현재 행 번호
- private int titleBlockCurrentRow = 2; // 헤더가 1행이므로 데이터는 2행부터 시작
- private int textEntitiesCurrentRow = 2; // 헤더가 1행이므로 데이터는 2행부터 시작
- private int mappingDataCurrentRow = 2; // 헤더가 1행이므로 데이터는 2행부터 시작
-
- // 매핑 데이터 저장용 딕셔너리 - (AILabel, DwgTag, DwgValue, PdfValue)
- private Dictionary> FileToMapkeyToLabelTagValuePdf = new Dictionary>();
-
- private FieldMapper fieldMapper;
- // 생성자: ODA 및 Excel 초기화
- public ExportExcel_old()
- {
- try
- {
- Debug.WriteLine("🔄 FieldMapper 로딩 중: mapping_table_json.json...");
- fieldMapper = FieldMapper.LoadFromFile("fletimageanalysis/mapping_table_json.json");
- Debug.WriteLine("✅ FieldMapper 로딩 성공");
- MapKeys = fieldMapper.GetAllDocAiKeys();
- Debug.WriteLine($"📊 총 DocAI 키 개수: {MapKeys?.Count ?? 0}");
-
- // 매핑 테스트 (디버깅용)
- TestFieldMapper();
- }
- catch (System.Exception ex)
- {
- Debug.WriteLine($"❌ FieldMapper 로딩 오류:");
- Debug.WriteLine($" 메시지: {ex.Message}");
- Debug.WriteLine($" 예외 타입: {ex.GetType().Name}");
- if (ex.InnerException != null)
- {
- Debug.WriteLine($" 내부 예외: {ex.InnerException.Message}");
- }
- Debug.WriteLine($" 스택 트레이스: {ex.StackTrace}");
- throw;
- }
-
- Debug.WriteLine("🔄 ODA 초기화 중...");
- InitializeTeighaServices();
- Debug.WriteLine("🔄 Excel 초기화 중...");
- InitializeExcel();
- }
-
- // 필드 매퍼 테스트
- private void TestFieldMapper()
- {
- Debug.WriteLine("[DEBUG] Testing field mapper...");
-
- // 몇 가지 알려진 expressway 필드로 테스트
- 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}");
- }
- }
-
- // Teigha 서비스 초기화 (싱글톤 사용)
- 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)
- {
- Debug.WriteLine($"[DEBUG] Teigha Services 초기화 실패: {ex.Message}");
- throw;
- }
- }
-
- // Excel 애플리케이션 및 워크시트 초기화
- private void InitializeExcel()
- {
- try
- {
- var excelApp = new Excel.Application();
- excelApplication = excelApp;
- excelApplication.Visible = false; // WPF에서는 숨김 처리
-
-
- // 매핑 데이터용 워크북 및 시트 생성
- mappingWorkbook = excelApp.Workbooks.Add();
- mappingSheet = (Excel.Worksheet)mappingWorkbook.Sheets[1];
- mappingSheet.Name = "Mapping Data";
- SetupMappingHeaders();
-
-
- //Excel.Workbook workbook = excelApp.Workbooks.Add();
- //workbook1 = workbook;
-
- // Title Block Sheet 설정 (기본 Sheet1)
-
- titleBlockSheet = (Excel.Worksheet)mappingWorkbook.Sheets.Add();
- titleBlockSheet = (Excel.Worksheet)mappingWorkbook.Sheets[2];
- titleBlockSheet.Name = "Title Block";
- SetupTitleBlockHeaders();
-
- // Text Entities Sheet 추가
- textEntitiesSheet = (Excel.Worksheet)mappingWorkbook.Sheets.Add();
- textEntitiesSheet = (Excel.Worksheet)mappingWorkbook.Sheets[3];
- textEntitiesSheet.Name = "Text Entities";
- SetupTextEntitiesHeaders();
-
-
-
-
-
-
- }
- catch (System.Exception ex)
- {
- Debug.WriteLine($"Excel 초기화 중 오류 발생: {ex.Message}");
- ReleaseExcelObjects();
- 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);
- }
-
- // 매핑 데이터 시트 헤더 설정
- private void SetupMappingHeaders()
- {
- mappingSheet.Cells[1, 1] = "FileName"; // 파일 이름
- mappingSheet.Cells[1, 2] = "MapKey"; // 매핑 키
- mappingSheet.Cells[1, 3] = "AILabel"; // AI 라벨
- mappingSheet.Cells[1, 4] = "DwgTag"; // DWG Tag
- mappingSheet.Cells[1, 5] = "Att_value"; // DWG 값
- mappingSheet.Cells[1, 6] = "Pdf_value"; // PDF 값 (현재는 빈 값)
-
- // 헤더 행 스타일
- Excel.Range headerRange = mappingSheet.Range["A1:F1"];
- headerRange.Font.Bold = true;
- headerRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightYellow);
- }
-
- ///
- /// 단일 DWG 파일에서 AttributeReference/AttributeDefinition 데이터를 추출하여
- /// 초기화된 Excel 워크시트에 추가합니다.
- ///
- /// 처리할 DWG 파일 경로
- /// 진행 상태 보고를 위한 IProgress 객체
- /// 작업 취소를 위한 CancellationToken
- /// 성공 시 true, 실패 시 false 반환
- public bool ExportDwgToExcel(string filePath, IProgress progress = null, CancellationToken cancellationToken = default, bool isExportNote =false)
- {
- Debug.WriteLine($"[DEBUG] ExportDwgToExcel 시작: {filePath}");
-
- if (excelApplication == null)
- {
- Debug.WriteLine("❌ Excel이 초기화되지 않았습니다.");
- return false;
- }
-
- if (!File.Exists(filePath))
- {
- Debug.WriteLine($"❌ 파일이 존재하지 않습니다: {filePath}");
- return false;
- }
-
- try
- {
- Debug.WriteLine("[DEBUG] 진행률 0% 보고");
- progress?.Report(0);
- cancellationToken.ThrowIfCancellationRequested();
-
- Debug.WriteLine("[DEBUG] ODA Database 객체 생성 중...");
- // ODA Database 객체 생성 및 DWG 파일 읽기
- using (var database = new Database(false, true))
- {
- Debug.WriteLine($"[DEBUG] DWG 파일 읽기 시도: {filePath}");
- database.ReadDwgFile(filePath, FileOpenMode.OpenForReadAndWriteNoShare, false, null);
- Debug.WriteLine("[DEBUG] DWG 파일 읽기 성공");
-
- cancellationToken.ThrowIfCancellationRequested();
- Debug.WriteLine("[DEBUG] 진행률 10% 보고");
- progress?.Report(10);
-
- Debug.WriteLine("[DEBUG] 트랜잭션 시작 중...");
- using (var tran = database.TransactionManager.StartTransaction())
- {
- Debug.WriteLine("[DEBUG] BlockTable 접근 중...");
- var bt = tran.GetObject(database.BlockTableId, OpenMode.ForRead) as BlockTable;
- Debug.WriteLine("[DEBUG] ModelSpace 접근 중...");
- using (var btr = tran.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForRead) as BlockTableRecord)
- {
- int totalEntities = btr.Cast().Count();
- Debug.WriteLine($"[DEBUG] 총 엔티티 수: {totalEntities}");
- int processedCount = 0;
-
- 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);
- var fileName = Path.GetFileNameWithoutExtension(database.Filename);
-
- // 파일명 유효성 검사
- if (string.IsNullOrEmpty(fileName))
- {
- fileName = "Unknown_File";
- Debug.WriteLine($"[DEBUG] Using default filename: {fileName}");
- }
-
- // 파일별 매핑 딕셔너리 초기화
- if (!FileToMapkeyToLabelTagValuePdf.ContainsKey(fileName))
- {
- FileToMapkeyToLabelTagValuePdf[fileName] = new Dictionary();
- }
-
- // 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++;
-
- var tag = attDef.Tag;
- var aiLabel = fieldMapper.ExpresswayToAilabel(tag);
- var mapKey = fieldMapper.AilabelToDocAiKey(aiLabel);
- var attValue = attDef.TextString;
- Debug.WriteLine($"[DEBUG] AttributeDefinition - Tag: {tag}, AILabel: {aiLabel}, MapKey: {mapKey}");
-
- // 매핑 데이터 저장
- if (!string.IsNullOrEmpty(aiLabel))
- {
- // mapKey가 null이면 aiLabel을 mapKey로 사용
- var finalMapKey = mapKey ?? aiLabel;
- FileToMapkeyToLabelTagValuePdf[fileName][finalMapKey] = (aiLabel, tag, attValue, "");
- Debug.WriteLine($"[DEBUG] Added mapping: {tag} -> {aiLabel} (MapKey: {finalMapKey})");
- }
- else
- {
- // aiLabel이 null이면 tag를 사용하여 저장
- var finalMapKey = mapKey ?? tag;
- if (!string.IsNullOrEmpty(finalMapKey))
- {
- FileToMapkeyToLabelTagValuePdf[fileName][finalMapKey] = (tag, tag, attValue, "");
- Debug.WriteLine($"[DEBUG] Added unmapped tag: {tag} -> {tag} (MapKey: {finalMapKey})");
- }
- else
- {
- Debug.WriteLine($"[DEBUG] Skipped empty tag for AttributeDefinition");
- }
- }
- }
- // 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++;
-
- var tag = attRef.Tag;
- var aiLabel = fieldMapper.ExpresswayToAilabel(tag);
- if (aiLabel == null) continue;
- var mapKey = fieldMapper.AilabelToDocAiKey(aiLabel);
- var attValue = attRef.TextString;
-
- Debug.WriteLine($"[DEBUG] AttributeReference - Tag: {tag}, AILabel: {aiLabel}, MapKey: {mapKey}");
-
- // 매핑 데이터 저장
- if (!string.IsNullOrEmpty(aiLabel))
- {
- // mapKey가 null이면 aiLabel을 mapKey로 사용
- var finalMapKey = mapKey ?? aiLabel;
- FileToMapkeyToLabelTagValuePdf[fileName][finalMapKey] = (aiLabel, tag, attValue, "");
- Debug.WriteLine($"[DEBUG] Added mapping: {tag} -> {aiLabel} (MapKey: {finalMapKey})");
- }
- else
- {
- // aiLabel이 null이면 tag를 사용하여 저장
- var finalMapKey = mapKey ?? tag;
- if (!string.IsNullOrEmpty(finalMapKey))
- {
- FileToMapkeyToLabelTagValuePdf[fileName][finalMapKey] = (tag, tag, attValue, "");
- Debug.WriteLine($"[DEBUG] Added unmapped tag: {tag} -> {tag} (MapKey: {finalMapKey})");
- }
- else
- {
- Debug.WriteLine($"[DEBUG] Skipped empty tag for AttributeReference");
- }
- }
- }
- }
- }
- }
- // 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++;
- }
- }
-
- // write values in new excel file with FileToMapkeyToLabelTag
-
- processedCount++;
- double currentProgress = 10.0 + (double)processedCount / totalEntities * 80.0;
- progress?.Report(Math.Min(currentProgress, 90.0));
- }
-
- if (isExportNote)
- {
- Debug.WriteLine("[DEBUG] Note 추출 시작...");
- ExtractNotesFromDrawing(tran, btr, database, progress, cancellationToken);
- Debug.WriteLine("[DEBUG] Note 추출 완료");
- }
- }
- tran.Commit();
- }
-
- Debug.WriteLine($"[DEBUG] Transaction committed. Total mapping entries collected: {FileToMapkeyToLabelTagValuePdf.Sum(f => f.Value.Count)}");
-
- // 매핑 데이터를 Excel 시트에 기록
- Debug.WriteLine("[DEBUG] 매핑 데이터를 Excel에 기록 중...");
- WriteMappingDataToExcel();
- Debug.WriteLine("[DEBUG] 매핑 데이터 Excel 기록 완료");
- }
-
- progress?.Report(100);
- return true;
- }
- catch (OperationCanceledException)
- {
- Debug.WriteLine("❌ 작업이 취소되었습니다.");
- progress?.Report(0);
- return false;
- }
- catch (Teigha.Runtime.Exception ex)
- {
- progress?.Report(0);
- Debug.WriteLine($"❌ DWG 파일 처리 중 Teigha 오류 발생:");
- Debug.WriteLine($" 메시지: {ex.Message}");
- Debug.WriteLine($" ErrorStatus: {ex.ErrorStatus}");
- if (ex.InnerException != null)
- {
- Debug.WriteLine($" 내부 예외: {ex.InnerException.Message}");
- }
- Debug.WriteLine($" 스택 트레이스: {ex.StackTrace}");
- return false;
- }
- catch (System.Exception ex)
- {
- progress?.Report(0);
- Debug.WriteLine($"❌ 일반 오류 발생:");
- Debug.WriteLine($" 메시지: {ex.Message}");
- Debug.WriteLine($" 예외 타입: {ex.GetType().Name}");
- if (ex.InnerException != null)
- {
- Debug.WriteLine($" 내부 예외: {ex.InnerException.Message}");
- }
- Debug.WriteLine($" 스택 트레이스: {ex.StackTrace}");
- return false;
- }
- }
- ///
- /// 도면에서 Note와 관련된 텍스트들을 추출합니다.
- ///
- private void ExtractNotesFromDrawing(Transaction tran, BlockTableRecord btr, Database database,
- IProgress progress, CancellationToken cancellationToken)
- {
- try
- {
- var allEntities = btr.Cast().ToList();
- var dbTextIds = new List();
- var polylineIds = new List();
- var lineIds = new List();
-
- // 먼저 모든 관련 엔터티들의 ObjectId를 수집
- foreach (ObjectId entId in allEntities)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- using (var ent = tran.GetObject(entId, OpenMode.ForRead) as Entity)
- {
- if (ent is DBText)
- {
- dbTextIds.Add(entId);
- }
- else if (ent is Polyline)
- {
- polylineIds.Add(entId);
- }
- else if (ent is Line)
- {
- lineIds.Add(entId);
- }
- }
- }
-
- Debug.WriteLine($"[DEBUG] 수집된 엔터티: DBText={dbTextIds.Count}, Polyline={polylineIds.Count}, Line={lineIds.Count}");
-
- // Note 텍스트들을 찾기
- var noteTextIds = FindNoteTexts(tran, dbTextIds);
- Debug.WriteLine($"[DEBUG] 발견된 Note 텍스트: {noteTextIds.Count}개");
-
- // 각 Note에 대해 처리
- foreach (var noteTextId in noteTextIds)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- using (var noteText = tran.GetObject(noteTextId, OpenMode.ForRead) as DBText)
- {
- Debug.WriteLine($"[DEBUG] Note 처리 중: '{noteText.TextString}' at {noteText.Position}");
-
- // Note 우측아래에 있는 박스 찾기
- var noteBox = FindNoteBox(tran, noteText, polylineIds, lineIds);
-
- if (noteBox.HasValue)
- {
- Debug.WriteLine($"[DEBUG] Note 박스 발견: {noteBox.Value.minPoint} to {noteBox.Value.maxPoint}");
-
- // 박스 내부의 텍스트들 찾기
- var boxTextIds = FindTextsInNoteBox(tran, noteText, noteBox.Value, dbTextIds);
- Debug.WriteLine($"[DEBUG] 박스 내 텍스트: {boxTextIds.Count}개");
-
- // 결과를 Excel에 기록
- WriteNoteDataToExcel(tran, noteTextId, boxTextIds, database);
- }
- else
- {
- Debug.WriteLine($"[DEBUG] Note '{noteText.TextString}'에 대한 박스를 찾을 수 없음");
- }
- }
- }
- }
- catch (System.Exception ex)
- {
- Debug.WriteLine($"❌ Note 추출 중 오류: {ex.Message}");
- throw;
- }
- }
- ///
- /// DBText 중에서 "Note"가 포함된 텍스트들을 찾습니다.
- ///
- private List FindNoteTexts(Transaction tran, List dbTextIds)
- {
- var noteTextIds = new List();
-
- foreach (var textId in dbTextIds)
- {
- using (var dbText = tran.GetObject(textId, OpenMode.ForRead) as DBText)
- {
- if (dbText == null) continue;
-
- string textContent = dbText.TextString?.Trim() ?? "";
-
- // 대소문자 구분없이 "Note"가 포함되어 있는지 확인
- if (textContent.IndexOf("Note", StringComparison.OrdinalIgnoreCase) >= 0)
- {
- // 색상 확인 (마젠타색인지)
- //if (IsNoteColor(dbText))
- //{
- noteTextIds.Add(textId);
- Debug.WriteLine($"[DEBUG] Note 텍스트 발견: '{textContent}' at {dbText.Position}, Color: {dbText.Color}");
- //}
- }
- }
- }
-
- return noteTextIds;
- }
- ///
- /// DBText 중에서 "Note"가 포함된 텍스트들을 찾습니다.
- ///
- ///
- /// Note 우측아래에 있는 박스를 찾습니다 (교차선 기반 알고리즘).
- /// Note position에서 height*2 만큼 아래로 수평선을 그어서 교차하는 Line/Polyline을 찾습니다.
- ///
- private (Point3d minPoint, Point3d maxPoint)? FindNoteBox(
- Transaction tran, DBText noteText, List polylineIds, List lineIds)
- {
- var notePos = noteText.Position;
- var noteHeight = noteText.Height;
-
- // Note position에서 height * 2 만큼 아래로 수평선 정의
- 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);
-
- Debug.WriteLine($"[DEBUG] 교차 검색선: ({searchLineStart.X}, {searchLineStart.Y}) to ({searchLineEnd.X}, {searchLineEnd.Y})");
-
- // 1. Polyline과 교차하는지 확인
- foreach (var polylineId in polylineIds)
- {
- using (var polyline = tran.GetObject(polylineId, OpenMode.ForRead) as Polyline)
- {
- if (polyline == null) continue;
-
- // 교차점이 있는지 확인
- if (DoesLineIntersectPolyline(searchLineStart, searchLineEnd, polyline))
- {
- var box = GetPolylineBounds(polyline);
- if (box.HasValue && IsValidNoteBox(box.Value, notePos, noteHeight))
- {
- Debug.WriteLine($"[DEBUG] 교차하는 Polyline 박스 발견: {box.Value.minPoint} to {box.Value.maxPoint}");
- return box;
- }
- }
- }
- }
-
- // 2. Line들과 교차하여 닫힌 사각형 찾기
- var intersectingLines = FindIntersectingLines(tran, lineIds, searchLineStart, searchLineEnd);
- Debug.WriteLine($"[DEBUG] 교차하는 Line 수: {intersectingLines.Count}");
-
- foreach (var startLineId in intersectingLines)
- {
- var rectangle = TraceRectangleFromLine(tran, lineIds, startLineId, notePos, noteHeight);
- if (rectangle.HasValue)
- {
- Debug.WriteLine($"[DEBUG] 교차하는 Line 사각형 발견: {rectangle.Value.minPoint} to {rectangle.Value.maxPoint}");
- return rectangle;
- }
- }
-
- return null;
- }
- ///
- /// 수평선이 Polyline과 교차하는지 확인합니다.
- ///
- private bool DoesLineIntersectPolyline(Point3d lineStart, Point3d lineEnd, Polyline polyline)
- {
- try
- {
- for (int i = 0; i < polyline.NumberOfVertices; i++)
- {
- int nextIndex = (i + 1) % polyline.NumberOfVertices;
- var segStart = polyline.GetPoint3dAt(i);
- var segEnd = polyline.GetPoint3dAt(nextIndex);
-
- // 수평선과 폴리라인 세그먼트의 교차점 확인
- if (DoLinesIntersect(lineStart, lineEnd, segStart, segEnd))
- {
- return true;
- }
- }
- return false;
- }
- catch
- {
- return false;
- }
- }
- ///
- /// 두 선분이 교차하는지 확인합니다.
- ///
- private bool DoLinesIntersect(Point3d line1Start, Point3d line1End, Point3d line2Start, Point3d line2End)
- {
- try
- {
- double x1 = line1Start.X, y1 = line1Start.Y;
- double x2 = line1End.X, y2 = line1End.Y;
- double x3 = line2Start.X, y3 = line2Start.Y;
- double x4 = line2End.X, y4 = line2End.Y;
-
- double denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
- if (Math.Abs(denom) < 1e-10) return false; // 평행선
-
- double t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom;
- double u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denom;
-
- return t >= 0 && t <= 1 && u >= 0 && u <= 1;
- }
- catch
- {
- return false;
- }
- }
- ///
- /// Polyline의 경계 상자를 계산합니다.
- ///
- private (Point3d minPoint, Point3d maxPoint)? GetPolylineBounds(Polyline polyline)
- {
- try
- {
- if (polyline.NumberOfVertices < 3) return null;
-
- var vertices = new List();
- for (int i = 0; i < polyline.NumberOfVertices; i++)
- {
- vertices.Add(polyline.GetPoint3dAt(i));
- }
-
- double minX = vertices.Min(v => v.X);
- double maxX = vertices.Max(v => v.X);
- double minY = vertices.Min(v => v.Y);
- double maxY = vertices.Max(v => v.Y);
-
- return (new Point3d(minX, minY, 0), new Point3d(maxX, maxY, 0));
- }
- catch
- {
- return null;
- }
- }
-
- ///
- /// 박스가 유효한 Note 박스인지 확인합니다.
- ///
- private bool IsValidNoteBox((Point3d minPoint, Point3d maxPoint) box, Point3d notePos, double noteHeight)
- {
- try
- {
- // 박스가 Note 아래쪽에 있는지 확인
- if (box.maxPoint.Y >= notePos.Y) return false;
-
- // 박스 크기가 적절한지 확인
- double boxWidth = box.maxPoint.X - box.minPoint.X;
- double boxHeight = box.maxPoint.Y - box.minPoint.Y;
-
- // 너무 작거나 큰 박스는 제외
- if (boxWidth < noteHeight || boxHeight < noteHeight) return false;
- if (boxWidth > noteHeight * 100 || boxHeight > noteHeight * 100) return false;
-
- // Note 위치와 적절한 거리에 있는지 확인
- double distanceX = Math.Abs(box.minPoint.X - notePos.X);
- double distanceY = Math.Abs(box.maxPoint.Y - notePos.Y);
-
- if (distanceX > noteHeight * 50 || distanceY > noteHeight * 10) return false;
-
- return true;
- }
- catch
- {
- return false;
- }
- }
-
- ///
- /// 수평선과 교차하는 Line들을 찾습니다.
- ///
- private List FindIntersectingLines(Transaction tran, List lineIds, Point3d searchLineStart, Point3d searchLineEnd)
- {
- var intersectingLines = new List();
-
- foreach (var lineId in lineIds)
- {
- using (var line = tran.GetObject(lineId, OpenMode.ForRead) as Line)
- {
- if (line == null) continue;
-
- if (DoLinesIntersect(searchLineStart, searchLineEnd, line.StartPoint, line.EndPoint))
- {
- intersectingLines.Add(lineId);
- Debug.WriteLine($"[DEBUG] 교차 Line 발견: ({line.StartPoint.X}, {line.StartPoint.Y}) to ({line.EndPoint.X}, {line.EndPoint.Y})");
- }
- }
- }
-
- return intersectingLines;
- }
-
- ///
- /// Line에서 시작하여 반시계방향으로 사각형을 추적합니다.
- ///
- private (Point3d minPoint, Point3d maxPoint)? TraceRectangleFromLine(Transaction tran, List lineIds, ObjectId startLineId, Point3d notePos, double noteHeight)
- {
- try
- {
- const double tolerance = 2.0; // 좌표 오차 허용 범위
- var visitedLines = new HashSet();
- var rectanglePoints = new List();
-
- using (var startLine = tran.GetObject(startLineId, OpenMode.ForRead) as Line)
- {
- if (startLine == null) return null;
-
- var currentPoint = startLine.StartPoint;
- var nextPoint = startLine.EndPoint;
- rectanglePoints.Add(currentPoint);
- rectanglePoints.Add(nextPoint);
- visitedLines.Add(startLineId);
-
- Debug.WriteLine($"[DEBUG] 사각형 추적 시작: ({currentPoint.X}, {currentPoint.Y}) -> ({nextPoint.X}, {nextPoint.Y})");
-
- // 최대 4개의 Line으로 사각형 완성 시도
- for (int step = 0; step < 3; step++) // 시작 Line 제외하고 3개 더 찾기
- {
- var nextLineId = FindNextConnectedLine(tran, lineIds, nextPoint, visitedLines, tolerance);
- if (nextLineId == ObjectId.Null) break;
-
- using (var nextLine = tran.GetObject(nextLineId, OpenMode.ForRead) as Line)
- {
- if (nextLine == null) break;
-
- // 현재 점과 가까운 쪽을 시작점으로 설정
- Point3d lineStart, lineEnd;
- if (nextPoint.DistanceTo(nextLine.StartPoint) < nextPoint.DistanceTo(nextLine.EndPoint))
- {
- lineStart = nextLine.StartPoint;
- lineEnd = nextLine.EndPoint;
- }
- else
- {
- lineStart = nextLine.EndPoint;
- lineEnd = nextLine.StartPoint;
- }
-
- rectanglePoints.Add(lineEnd);
- visitedLines.Add(nextLineId);
- nextPoint = lineEnd;
-
- Debug.WriteLine($"[DEBUG] 다음 Line 추가: ({lineStart.X}, {lineStart.Y}) -> ({lineEnd.X}, {lineEnd.Y})");
-
- // 시작점으로 돌아왔는지 확인 (사각형 완성)
- if (nextPoint.DistanceTo(currentPoint) < tolerance)
- {
- Debug.WriteLine("[DEBUG] 사각형 완성됨");
- break;
- }
- }
- }
-
- // 4개 이상의 점이 있고 닫힌 형태인지 확인
- if (rectanglePoints.Count >= 4)
- {
- var bounds = CalculateBounds(rectanglePoints);
- if (bounds.HasValue && IsValidNoteBox(bounds.Value, notePos, noteHeight))
- {
- return bounds;
- }
- }
- }
-
- return null;
- }
- catch (System.Exception ex)
- {
- Debug.WriteLine($"[DEBUG] 사각형 추적 중 오류: {ex.Message}");
- return null;
- }
- }
-
- ///
- /// 현재 점에서 연결된 다음 Line을 찾습니다.
- ///
- private ObjectId FindNextConnectedLine(Transaction tran, List lineIds, Point3d currentPoint, HashSet visitedLines, double tolerance)
- {
- foreach (var lineId in lineIds)
- {
- if (visitedLines.Contains(lineId)) continue;
-
- using (var line = tran.GetObject(lineId, OpenMode.ForRead) as Line)
- {
- if (line == null) continue;
-
- // 현재 점과 연결되어 있는지 확인
- if (currentPoint.DistanceTo(line.StartPoint) < tolerance ||
- currentPoint.DistanceTo(line.EndPoint) < tolerance)
- {
- return lineId;
- }
- }
- }
-
- return ObjectId.Null;
- }
-
- ///
- /// 점들의 경계 상자를 계산합니다.
- ///
- private (Point3d minPoint, Point3d maxPoint)? CalculateBounds(List points)
- {
- try
- {
- if (points.Count < 3) return null;
-
- double minX = points.Min(p => p.X);
- double maxX = points.Max(p => p.X);
- double minY = points.Min(p => p.Y);
- double maxY = points.Max(p => p.Y);
-
- return (new Point3d(minX, minY, 0), new Point3d(maxX, maxY, 0));
- }
- catch
- {
- return null;
- }
- }
- ///
- /// Note 텍스트인지 색상으로 확인합니다 (마젠타색).
- ///
- private bool IsNoteColor(DBText text)
- {
- try
- {
- // AutoCAD에서 마젠타는 ColorIndex 6번
- if (text.Color.ColorIndex == 6) // Magenta
- return true;
-
- // RGB 값으로도 확인 (마젠타: R=255, G=0, B=255)
- if (text.Color.Red == 255 && text.Color.Green == 0 && text.Color.Blue == 255)
- return true;
-
- return false;
- }
- catch
- {
- return false;
- }
- }
-
- ///
- /// Note 우측아래에 있는 박스를 찾습니다 (Polyline 또는 Line 4개).
- ///
-
-
- ///
- /// Polyline이 Note 박스인지 확인하고 경계를 반환합니다.
- ///
- private (Point3d minPoint, Point3d maxPoint)? GetPolylineBox(Polyline polyline, Point3d notePos, double noteHeight)
- {
- try
- {
- if (polyline.NumberOfVertices < 4) return null;
-
- var vertices = new List();
- for (int i = 0; i < polyline.NumberOfVertices; i++)
- {
- vertices.Add(polyline.GetPoint3dAt(i));
- }
-
- // 경계 계산
- double minX = vertices.Min(v => v.X);
- double maxX = vertices.Max(v => v.X);
- double minY = vertices.Min(v => v.Y);
- double maxY = vertices.Max(v => v.Y);
-
- var minPoint = new Point3d(minX, minY, 0);
- var maxPoint = new Point3d(maxX, maxY, 0);
-
- // Note 우측아래에 있는지 확인
- if (minX > notePos.X && maxY < notePos.Y)
- {
- // 박스가 너무 크거나 작지 않은지 확인
- double boxWidth = maxX - minX;
- double boxHeight = maxY - minY;
-
- if (boxWidth > noteHeight && boxHeight > noteHeight &&
- boxWidth < noteHeight * 50 && boxHeight < noteHeight * 50)
- {
- return (minPoint, maxPoint);
- }
- }
-
- return null;
- }
- catch
- {
- return null;
- }
- }
-
- ///
- /// Line들로부터 사각형 박스를 찾습니다.
- ///
- private (Point3d minPoint, Point3d maxPoint)? FindRectangleFromLines(
- Transaction tran, List lineIds, Point3d notePos, double noteHeight)
- {
- try
- {
- // Note 우측아래 영역의 Line들만 필터링
- var candidateLineIds = new List();
-
- foreach (var lineId in lineIds)
- {
- using (var line = tran.GetObject(lineId, OpenMode.ForRead) as Line)
- {
- if (line == null) continue;
-
- var startPoint = line.StartPoint;
- var endPoint = line.EndPoint;
-
- // Note 우측아래에 있는 Line인지 확인
- if ((startPoint.X > notePos.X && startPoint.Y < notePos.Y) ||
- (endPoint.X > notePos.X && endPoint.Y < notePos.Y))
- {
- candidateLineIds.Add(lineId);
- }
- }
- }
-
- Debug.WriteLine($"[DEBUG] 후보 Line 수: {candidateLineIds.Count}");
-
- // 4개의 Line으로 사각형을 구성할 수 있는지 확인
- var rectangles = FindRectanglesFromLines(tran, candidateLineIds, noteHeight);
-
- foreach (var rect in rectangles)
- {
- // Note 우측아래에 있는 사각형인지 확인
- if (rect.minPoint.X > notePos.X && rect.maxPoint.Y < notePos.Y)
- {
- return rect;
- }
- }
-
- return null;
- }
- catch
- {
- return null;
- }
- }
-
- ///
- /// Line들로부터 가능한 사각형들을 찾습니다.
- ///
- private List<(Point3d minPoint, Point3d maxPoint)> FindRectanglesFromLines(
- Transaction tran, List lineIds, double noteHeight)
- {
- var rectangles = new List<(Point3d minPoint, Point3d maxPoint)>();
-
- try
- {
- // 모든 Line의 끝점들을 수집
- var points = new HashSet();
- var lineData = new List<(Point3d start, Point3d end)>();
-
- foreach (var lineId in lineIds)
- {
- using (var line = tran.GetObject(lineId, OpenMode.ForRead) as Line)
- {
- if (line == null) continue;
-
- points.Add(line.StartPoint);
- points.Add(line.EndPoint);
- lineData.Add((line.StartPoint, line.EndPoint));
- }
- }
-
- var pointList = points.ToList();
-
- // 4개 점으로 사각형을 만들 수 있는지 확인
- for (int i = 0; i < pointList.Count - 3; i++)
- {
- for (int j = i + 1; j < pointList.Count - 2; j++)
- {
- for (int k = j + 1; k < pointList.Count - 1; k++)
- {
- for (int l = k + 1; l < pointList.Count; l++)
- {
- var rect = TryFormRectangle(new[] { pointList[i], pointList[j], pointList[k], pointList[l] }, lineData);
- if (rect.HasValue)
- {
- rectangles.Add(rect.Value);
- }
- }
- }
- }
- }
- }
- catch (System.Exception ex)
- {
- Debug.WriteLine($"[DEBUG] 사각형 찾기 중 오류: {ex.Message}");
- }
-
- return rectangles;
- }
-
- ///
- /// 4개의 점이 사각형을 형성하는지 확인합니다.
- ///
- private (Point3d minPoint, Point3d maxPoint)? TryFormRectangle(Point3d[] points, List<(Point3d start, Point3d end)> lineData)
- {
- try
- {
- if (points.Length != 4) return null;
-
- // 점들을 정렬하여 사각형인지 확인
- var sortedPoints = points.OrderBy(p => p.X).ThenBy(p => p.Y).ToArray();
-
- double minX = sortedPoints.Min(p => p.X);
- double maxX = sortedPoints.Max(p => p.X);
- double minY = sortedPoints.Min(p => p.Y);
- double maxY = sortedPoints.Max(p => p.Y);
-
- // 실제 사각형의 4개 모서리가 Line으로 연결되어 있는지 확인
- var expectedEdges = new[]
- {
- (new Point3d(minX, minY, 0), new Point3d(maxX, minY, 0)), // 아래
- (new Point3d(maxX, minY, 0), new Point3d(maxX, maxY, 0)), // 오른쪽
- (new Point3d(maxX, maxY, 0), new Point3d(minX, maxY, 0)), // 위
- (new Point3d(minX, maxY, 0), new Point3d(minX, minY, 0)) // 왼쪽
- };
-
- int foundEdges = 0;
- foreach (var edge in expectedEdges)
- {
- if (HasLine(lineData, edge.Item1, edge.Item2))
- {
- foundEdges++;
- }
- }
-
- // 4개 모서리가 모두 있으면 사각형
- if (foundEdges >= 3) // 완전하지 않을 수도 있으므로 3개 이상
- {
- return (new Point3d(minX, minY, 0), new Point3d(maxX, maxY, 0));
- }
-
- return null;
- }
- catch
- {
- return null;
- }
- }
-
- ///
- /// 두 점을 연결하는 Line이 있는지 확인합니다.
- ///
- private bool HasLine(List<(Point3d start, Point3d end)> lineData, Point3d point1, Point3d point2)
- {
- const double tolerance = 0.1; // 허용 오차
-
- foreach (var line in lineData)
- {
- // 순방향 확인
- if (point1.DistanceTo(line.start) < tolerance && point2.DistanceTo(line.end) < tolerance)
- return true;
-
- // 역방향 확인
- if (point1.DistanceTo(line.end) < tolerance && point2.DistanceTo(line.start) < tolerance)
- return true;
- }
-
- return false;
- }
-
- ///
- /// Note 박스 내부의 텍스트들을 찾습니다.
- ///
- private List FindTextsInNoteBox(
- Transaction tran, DBText noteText, (Point3d minPoint, Point3d maxPoint) noteBox, List allTextIds)
- {
- var boxTextIds = new List();
- var noteHeight = noteText.Height;
-
- foreach (var textId in allTextIds)
- {
- // Note 자신은 제외
- if (textId == noteText.ObjectId) continue;
-
- using (var dbText = tran.GetObject(textId, OpenMode.ForRead) as DBText)
- {
- if (dbText == null) continue;
-
- var textPos = dbText.Position;
-
- // 박스 내부에 있는지 확인
- if (textPos.X >= noteBox.minPoint.X && textPos.X <= noteBox.maxPoint.X &&
- textPos.Y >= noteBox.minPoint.Y && textPos.Y <= noteBox.maxPoint.Y)
- {
- // Note의 height보다 작거나 같은지 확인
- if (dbText.Height <= noteHeight)
- {
- // 색상 확인 (그린색인지)
- //if (IsBoxTextColor(dbText))
- //{
- boxTextIds.Add(textId);
- Debug.WriteLine($"[DEBUG] 박스 내 텍스트 발견: '{dbText.TextString}' at {textPos}");
- //}
- }
- }
- }
- }
-
- return boxTextIds;
- }
-
- ///
- /// 박스 내 텍스트 색상인지 확인합니다 (그린색).
- ///
- private bool IsBoxTextColor(DBText text)
- {
- try
- {
- // AutoCAD에서 그린은 ColorIndex 3번
- if (text.Color.ColorIndex == 3) // Green
- return true;
-
- // RGB 값으로도 확인 (그린: R=0, G=255, B=0)
- if (text.Color.Red == 0 && text.Color.Green == 255 && text.Color.Blue == 0)
- return true;
-
- return false;
- }
- catch
- {
- return false;
- }
- }
-
- ///
- /// Note 데이터를 Excel에 기록합니다.
- ///
- private void WriteNoteDataToExcel(
- Transaction tran, ObjectId noteTextId, List boxTextIds, Database database)
- {
- try
- {
- // Note 정보 기록
- using (var noteText = tran.GetObject(noteTextId, OpenMode.ForRead) as DBText)
- {
- if (noteText != null)
- {
- textEntitiesSheet.Cells[textEntitiesCurrentRow, 1] = "Note"; // Type
- textEntitiesSheet.Cells[textEntitiesCurrentRow, 2] = GetLayerName(noteText.LayerId, tran, database); // Layer
- textEntitiesSheet.Cells[textEntitiesCurrentRow, 3] = noteText.TextString; // Text
- textEntitiesSheet.Cells[textEntitiesCurrentRow, 4] = database.Filename; // Path
- textEntitiesSheet.Cells[textEntitiesCurrentRow, 5] = Path.GetFileName(database.Filename); // FileName
- textEntitiesCurrentRow++;
- }
- }
-
- // 박스 내 텍스트들 기록
- foreach (var boxTextId in boxTextIds)
- {
- using (var boxText = tran.GetObject(boxTextId, OpenMode.ForRead) as DBText)
- {
- if (boxText != null)
- {
- textEntitiesSheet.Cells[textEntitiesCurrentRow, 1] = "NoteContent"; // Type
- textEntitiesSheet.Cells[textEntitiesCurrentRow, 2] = GetLayerName(boxText.LayerId, tran, database); // Layer
- textEntitiesSheet.Cells[textEntitiesCurrentRow, 3] = boxText.TextString; // Text
- textEntitiesSheet.Cells[textEntitiesCurrentRow, 4] = database.Filename; // Path
- textEntitiesSheet.Cells[textEntitiesCurrentRow, 5] = Path.GetFileName(database.Filename); // FileName
- textEntitiesCurrentRow++;
- }
- }
- }
-
- using (var noteText = tran.GetObject(noteTextId, OpenMode.ForRead) as DBText)
- {
- if (noteText != null)
- {
- Debug.WriteLine($"[DEBUG] Note 데이터 Excel 기록 완료: Note='{noteText.TextString}', Content={boxTextIds.Count}개");
- }
- }
- }
- catch (System.Exception ex)
- {
- Debug.WriteLine($"❌ Note 데이터 Excel 기록 중 오류: {ex.Message}");
- }
- }
- // 매핑 데이터를 Excel 시트에 기록
- private void WriteMappingDataToExcel()
- {
- try
- {
- int currentRow = 2; // 헤더 다음 행부터 시작
-
- Debug.WriteLine($"[DEBUG] Writing mapping data to Excel. Total files: {FileToMapkeyToLabelTagValuePdf.Count}");
- Debug.WriteLine($"[DEBUG] 시작 행: {currentRow}");
-
- foreach (var fileEntry in FileToMapkeyToLabelTagValuePdf)
- {
- string fileName = fileEntry.Key;
- var mappingData = fileEntry.Value;
-
- Debug.WriteLine($"[DEBUG] Processing file: {fileName}, entries: {mappingData.Count}");
-
- foreach (var mapEntry in mappingData)
- {
- string mapKey = mapEntry.Key;
- (string aiLabel, string dwgTag, string attValue, string pdfValue) = mapEntry.Value;
-
- // null 값 방지
- if (string.IsNullOrEmpty(fileName) || string.IsNullOrEmpty(mapKey))
- {
- Debug.WriteLine($"[DEBUG] Skipping entry with null/empty values: fileName={fileName}, mapKey={mapKey}");
- continue;
- }
-
- Debug.WriteLine($"[DEBUG] Writing row {currentRow}: {fileName} | {mapKey} | {aiLabel} | {dwgTag} | PDF: {pdfValue}");
-
- try
- {
- // 배치 업데이트를 위한 배열 사용
- object[,] rowData = new object[1, 6];
- rowData[0, 0] = fileName; // FileName
- rowData[0, 1] = mapKey; // MapKey
- rowData[0, 2] = aiLabel ?? ""; // AILabel
- rowData[0, 3] = dwgTag ?? ""; // DwgTag
- rowData[0, 4] = attValue ?? ""; // DwgValue (Att_value)
- rowData[0, 5] = pdfValue ?? ""; // PdfValue (Pdf_value)
-
- Excel.Range range = mappingSheet.Range[mappingSheet.Cells[currentRow, 1], mappingSheet.Cells[currentRow, 6]];
- range.Value = rowData;
-
- Debug.WriteLine($"[DEBUG] Row {currentRow} written successfully");
- }
- 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}");
- Debug.WriteLine($"[DEBUG] 최종 행 번호: {currentRow}");
- }
- catch (System.Exception ex)
- {
- Debug.WriteLine($"❌ 매핑 데이터 Excel 기록 중 오류:");
- Debug.WriteLine($" 메시지: {ex.Message}");
- Debug.WriteLine($" 예외 타입: {ex.GetType().Name}");
- if (ex.InnerException != null)
- {
- Debug.WriteLine($" 내부 예외: {ex.InnerException.Message}");
- }
- Debug.WriteLine($" 스택 트레이스: {ex.StackTrace}");
- throw; // 상위 메서드로 예외 전파
- }
- }
-
- ///
- /// 기존 Excel 파일을 열어 JSON 파일의 PDF 분석 결과로 매핑 시트를 업데이트합니다.
- ///
- /// 기존 Excel 파일 경로
- /// PDF 분석 결과 JSON 파일 경로
- /// 성공 시 true, 실패 시 false
- public bool UpdateExistingExcelWithJson(string excelFilePath, string jsonFilePath)
- {
- try
- {
- Debug.WriteLine($"[DEBUG] 기존 Excel 파일 업데이트 시작: {excelFilePath}");
-
- if (!File.Exists(excelFilePath))
- {
- Debug.WriteLine($"❌ Excel 파일이 존재하지 않습니다: {excelFilePath}");
- return false;
- }
-
- if (!File.Exists(jsonFilePath))
- {
- Debug.WriteLine($"❌ JSON 파일이 존재하지 않습니다: {jsonFilePath}");
- return false;
- }
-
- Debug.WriteLine($"[DEBUG] Excel 애플리케이션 초기화 중...");
-
- // 기존 Excel 파일 열기
- if (excelApplication == null)
- {
- excelApplication = new Excel.Application();
- excelApplication.Visible = false;
- Debug.WriteLine("[DEBUG] 새 Excel 애플리케이션 생성됨");
- }
-
- Debug.WriteLine($"[DEBUG] Excel 파일 열기 시도: {excelFilePath}");
- mappingWorkbook = excelApplication.Workbooks.Open(excelFilePath);
- Debug.WriteLine("[DEBUG] Excel 파일 열기 성공");
-
- Debug.WriteLine("[DEBUG] 'Mapping Data' 시트 찾는 중...");
- mappingSheet = (Excel.Worksheet)mappingWorkbook.Sheets["Mapping Data"];
-
- if (mappingSheet == null)
- {
- Debug.WriteLine("❌ 'Mapping Data' 시트를 찾을 수 없습니다.");
- // 사용 가능한 시트 목록 출력
- Debug.WriteLine("사용 가능한 시트:");
- foreach (Excel.Worksheet sheet in mappingWorkbook.Sheets)
- {
- Debug.WriteLine($" - {sheet.Name}");
- }
- return false;
- }
-
- Debug.WriteLine("✅ 기존 Excel 파일 열기 성공");
-
- // JSON에서 PDF 값 업데이트
- Debug.WriteLine("[DEBUG] JSON 파싱 및 업데이트 시작");
- bool result = UpdateMappingSheetFromJson(jsonFilePath);
-
- Debug.WriteLine($"[DEBUG] 업데이트 결과: {result}");
- return result;
- }
- catch (System.Exception ex)
- {
- Debug.WriteLine($"❌ 기존 Excel 파일 업데이트 중 오류:");
- Debug.WriteLine($" 메시지: {ex.Message}");
- Debug.WriteLine($" 예외 타입: {ex.GetType().Name}");
- if (ex.InnerException != null)
- {
- Debug.WriteLine($" 내부 예외: {ex.InnerException.Message}");
- }
- Debug.WriteLine($" 스택 트레이스: {ex.StackTrace}");
- return false;
- }
- }
-
- ///
- /// JSON 파일에서 PDF 분석 결과를 읽어 Excel 매핑 시트의 Pdf_value 컬럼을 업데이트합니다.
- ///
- /// PDF 분석 결과 JSON 파일 경로
- /// 성공 시 true, 실패 시 false
- public bool UpdateMappingSheetFromJson(string jsonFilePath)
- {
- try
- {
- Debug.WriteLine($"[DEBUG] JSON 파일에서 PDF 값 업데이트 시작: {jsonFilePath}");
-
- if (!File.Exists(jsonFilePath))
- {
- Debug.WriteLine($"❌ JSON 파일이 존재하지 않습니다: {jsonFilePath}");
- return false;
- }
-
- if (mappingSheet == null)
- {
- Debug.WriteLine("❌ 매핑 시트가 초기화되지 않았습니다.");
- return false;
- }
-
- // JSON 파일 읽기 및 정리
- string jsonContent = File.ReadAllText(jsonFilePath, System.Text.Encoding.UTF8);
- Debug.WriteLine($"[DEBUG] JSON 파일 크기: {jsonContent.Length} bytes");
-
- // JSON 내용 정리 (주석 제거 등)
- jsonContent = CleanJsonContent(jsonContent);
-
- JObject jsonData;
- try
- {
- jsonData = JObject.Parse(jsonContent);
- }
- catch (Newtonsoft.Json.JsonReaderException jsonEx)
- {
- Debug.WriteLine($"❌ JSON 파싱 오류: {jsonEx.Message}");
- Debug.WriteLine($"❌ JSON 내용 미리보기 (첫 500자):");
- Debug.WriteLine(jsonContent.Length > 500 ? jsonContent.Substring(0, 500) + "..." : jsonContent);
- throw new System.Exception($"PDF 분석 JSON 파일 파싱 실패: {jsonEx.Message}\n파일: {jsonFilePath}");
- }
-
- var results = jsonData["results"] as JArray;
- if (results == null)
- {
- Debug.WriteLine("❌ JSON에서 'results' 배열을 찾을 수 없습니다.");
- Debug.WriteLine($"❌ JSON 루트 키들: {string.Join(", ", jsonData.Properties().Select(p => p.Name))}");
- 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);
- Debug.WriteLine($"[DEBUG] Processing PDF file: {fileNameWithoutExt}");
-
- // PDF 분석 결과의 각 필드 처리
- foreach (var property in pdfAnalysis.Cast())
- {
- string aiLabel = property.Name; // 예: "설계공구_Station_col1"
- var valueObj = property.Value as JObject;
-
- if (valueObj == null) continue;
-
- string pdfValue = valueObj["value"]?.ToString();
- if (string.IsNullOrEmpty(pdfValue)) continue;
-
- totalEntries++;
- Debug.WriteLine($"[DEBUG] Searching for match: FileName={fileNameWithoutExt}, AILabel={aiLabel}, Value={pdfValue}");
-
- // Excel 시트에서 매칭되는 행 찾기 및 업데이트
- if (UpdateExcelRow(fileNameWithoutExt, aiLabel, pdfValue))
- {
- updatedCount++;
- Debug.WriteLine($"✅ Updated: {fileNameWithoutExt} -> {aiLabel} = {pdfValue}");
- }
- else
- {
- Debug.WriteLine($"⚠️ No match found: {fileNameWithoutExt} -> {aiLabel}");
- }
- }
- }
-
- Debug.WriteLine($"[DEBUG] PDF 값 업데이트 완료: {updatedCount}/{totalEntries} 업데이트됨");
- return true;
- }
- catch (System.Exception ex)
- {
- Debug.WriteLine($"❌ JSON에서 PDF 값 업데이트 중 오류:");
- Debug.WriteLine($" 메시지: {ex.Message}");
- Debug.WriteLine($" 예외 타입: {ex.GetType().Name}");
- if (ex.InnerException != null)
- {
- Debug.WriteLine($" 내부 예외: {ex.InnerException.Message}");
- }
- Debug.WriteLine($" 스택 트레이스: {ex.StackTrace}");
- return false;
- }
- }
-
- ///
- /// Excel 매핑 시트에서 FileName과 AILabel이 매칭되는 행을 찾아 Pdf_value를 업데이트합니다.
- ///
- /// 파일명 (확장자 제외)
- /// AI 라벨 (예: "설계공구_Station_col1")
- /// PDF에서 추출된 값
- /// 업데이트 성공 시 true
- private bool UpdateExcelRow(string fileName, string aiLabel, string pdfValue)
- {
- try
- {
- // Excel 시트의 마지막 사용된 행 찾기
- Excel.Range usedRange = mappingSheet.UsedRange;
- if (usedRange == null) return false;
-
- int lastRow = usedRange.Rows.Count;
-
- // 2행부터 검색 (1행은 헤더)
- for (int row = 2; row <= lastRow; row++)
- {
- // Column 1: FileName, Column 3: AILabel 확인
- var cellFileName = mappingSheet.Cells[row, 1]?.Value?.ToString() ?? "";
- var cellAiLabel = mappingSheet.Cells[row, 3]?.Value?.ToString() ?? "";
-
- Debug.WriteLine($"[DEBUG] Row {row}: FileName='{cellFileName}', AILabel='{cellAiLabel}'");
-
- // 매칭 확인 (대소문자 구분 없이)
- if (string.Equals(cellFileName.Trim(), fileName.Trim(), StringComparison.OrdinalIgnoreCase) &&
- string.Equals(cellAiLabel.Trim(), aiLabel.Trim(), StringComparison.OrdinalIgnoreCase))
- {
- // Column 6: Pdf_value에 값 기록
- mappingSheet.Cells[row, 6] = pdfValue;
- Debug.WriteLine($"[DEBUG] Updated row {row}, column 6 with value: {pdfValue}");
- return true;
- }
- }
-
- return false; // 매칭되는 행을 찾지 못함
- }
- catch (System.Exception ex)
- {
- Debug.WriteLine($"❌ Excel 행 업데이트 중 오류: {ex.Message}");
- return false;
- }
- }
-
- // Paste the helper function from above here
- public string GetPromptFromAttributeReference(Transaction tr, BlockReference blockref, string tag)
- {
-
- string prompt = null;
-
-
- BlockTableRecord blockDef = tr.GetObject(blockref.BlockTableRecord, OpenMode.ForRead) as BlockTableRecord;
- if (blockDef == null) return null;
-
- foreach (ObjectId objId in blockDef)
- {
- AttributeDefinition attDef = tr.GetObject(objId, OpenMode.ForRead) as AttributeDefinition;
- if (attDef != null)
- {
- if (attDef.Tag.Equals(tag, System.StringComparison.OrdinalIgnoreCase))
- {
- prompt = attDef.Prompt;
- break;
- }
- }
- }
-
-
- return prompt;
- }
-
- ///
- /// 현재 열려있는 Excel 워크북을 저장합니다.
- ///
- /// 성공 시 true, 실패 시 false
- public bool SaveExcel()
- {
- try
- {
- if (mappingWorkbook != null)
- {
- mappingWorkbook.Save();
- Debug.WriteLine("✅ Excel 파일 저장 완료");
- return true;
- }
-
- if (workbook1 != null)
- {
- workbook1.Save();
- Debug.WriteLine("✅ Excel 파일 저장 완료");
- return true;
- }
-
- Debug.WriteLine("❌ 저장할 워크북이 없습니다.");
- return false;
- }
- catch (System.Exception ex)
- {
- Debug.WriteLine($"❌ Excel 파일 저장 중 오류: {ex.Message}");
- return false;
- }
- }
-
- ///
- /// 매핑 워크북만 저장하고 닫습니다 (완전한 매핑용).
- ///
- /// 저장할 파일 경로
- public void SaveMappingWorkbookOnly(string savePath)
- {
- try
- {
- Debug.WriteLine($"[DEBUG] 매핑 워크북 저장 시작: {savePath}");
-
- if (mappingWorkbook == null)
- {
- Debug.WriteLine("❌ 매핑 워크북이 초기화되지 않았습니다.");
- return;
- }
-
- string directory = Path.GetDirectoryName(savePath);
- if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
- {
- Directory.CreateDirectory(directory);
- }
-
- // 매핑 워크북만 저장 (Excel 2007+ 형식으로)
- mappingWorkbook.SaveAs(savePath,
- FileFormat: Excel.XlFileFormat.xlOpenXMLWorkbook,
- AccessMode: Excel.XlSaveAsAccessMode.xlNoChange);
-
- Debug.WriteLine($"✅ 매핑 워크북 저장 완료: {Path.GetFileName(savePath)}");
- }
- catch (System.Exception ex)
- {
- Debug.WriteLine($"❌ 매핑 워크북 저장 중 오류: {ex.Message}");
- Debug.WriteLine($" 스택 트레이스: {ex.StackTrace}");
- throw;
- }
- }
-
- ///
- /// DWG 전용 매핑 워크북을 생성하고 저장합니다 (PDF 컬럼 제외).
- ///
- /// 결과 파일 저장 폴더 경로
- public void SaveDwgOnlyMappingWorkbook(string resultFolderPath)
- {
- try
- {
- string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
- string savePath = Path.Combine(resultFolderPath, $"{timestamp}_DwgOnly_Mapping.xlsx");
-
- Debug.WriteLine($"[DEBUG] DWG 전용 매핑 워크북 생성 시작: {savePath}");
-
- // Excel 애플리케이션 초기화 확인
- if (excelApplication == null)
- {
- excelApplication = new Excel.Application();
- excelApplication.Visible = false;
- Debug.WriteLine("[DEBUG] 새 Excel 애플리케이션 생성됨 (DWG 전용)");
- }
-
- // DWG 전용 워크북 생성
- var dwgOnlyWorkbook = excelApplication.Workbooks.Add();
- var dwgOnlyWorksheet = (Excel.Worksheet)dwgOnlyWorkbook.Worksheets[1];
- dwgOnlyWorksheet.Name = "DWG Mapping Data";
-
- // 헤더 생성 (PDF Value 컬럼 제외)
- dwgOnlyWorksheet.Cells[1, 1] = "파일명";
- dwgOnlyWorksheet.Cells[1, 2] = "Map Key";
- dwgOnlyWorksheet.Cells[1, 3] = "AI Label";
- dwgOnlyWorksheet.Cells[1, 4] = "DWG Tag";
- dwgOnlyWorksheet.Cells[1, 5] = "DWG Value";
-
- // 헤더 스타일 적용
- 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;
-
- // 데이터 입력 (배치 처리로 성능 향상)
- int totalRows = FileToMapkeyToLabelTagValuePdf.Sum(f => f.Value.Count);
- if (totalRows > 0)
- {
- object[,] data = new object[totalRows, 5];
- int row = 0;
-
- foreach (var fileEntry in FileToMapkeyToLabelTagValuePdf)
- {
- 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();
-
- // 파일 저장
- string directory = Path.GetDirectoryName(savePath);
- if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
- {
- Directory.CreateDirectory(directory);
- }
-
- dwgOnlyWorkbook.SaveAs(savePath,
- FileFormat: Excel.XlFileFormat.xlOpenXMLWorkbook,
- AccessMode: Excel.XlSaveAsAccessMode.xlNoChange);
-
- Debug.WriteLine($"✅ DWG 전용 매핑 워크북 저장 완료: {Path.GetFileName(savePath)}");
-
- // 워크북 정리
- dwgOnlyWorkbook.Close(false);
- ReleaseComObject(dwgOnlyWorksheet);
- ReleaseComObject(dwgOnlyWorkbook);
-
- // 가비지 컬렉션 강제 실행으로 메모리 해제
- GC.Collect();
- GC.WaitForPendingFinalizers();
- }
- catch (System.Exception ex)
- {
- Debug.WriteLine($"❌ DWG 전용 매핑 워크북 저장 중 오류: {ex.Message}");
- Debug.WriteLine($" 스택 트레이스: {ex.StackTrace}");
- throw;
- }
- }
-
- ///
- /// 현재 Excel 워크북을 지정된 경로에 저장하고 Excel 애플리케이션을 종료합니다.
- ///
- /// Excel 파일을 저장할 전체 경로
- 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);
-
- // 매핑 데이터 워크북도 저장
- if (mappingWorkbook != null)
- {
- string mappingPath = Path.Combine(directory, Path.GetFileNameWithoutExtension(savePath) + "_Mapping.xlsx");
- mappingWorkbook.SaveAs(mappingPath, AccessMode: Excel.XlSaveAsAccessMode.xlNoChange);
- }
- }
- catch (System.Exception ex)
- {
- Debug.WriteLine($"Excel 파일 저장 중 오류 발생: {ex.Message}");
- }
- finally
- {
- CloseExcelObjects();
- }
- }
-
- ///
- /// Excel 객체들을 닫습니다 (저장하지 않음).
- ///
- public void CloseExcelObjectsWithoutSaving()
- {
- try
- {
- Debug.WriteLine("[DEBUG] Excel 객체 정리 시작");
-
- if (workbook1 != null)
- {
- try { workbook1.Close(false); }
- catch { }
- }
- if (mappingWorkbook != null)
- {
- try { mappingWorkbook.Close(false); }
- catch { }
- }
- if (excelApplication != null)
- {
- try { excelApplication.Quit(); }
- catch { }
- }
-
- ReleaseExcelObjects();
-
- Debug.WriteLine("✅ Excel 객체 정리 완료");
- }
- catch (System.Exception ex)
- {
- Debug.WriteLine($"❌ Excel 객체 정리 중 오류: {ex.Message}");
- }
- }
-
- private void CloseExcelObjects()
- {
- if (workbook1 != null)
- {
- try { workbook1.Close(false); }
- catch { }
- }
- if (mappingWorkbook != null)
- {
- try { mappingWorkbook.Close(false); }
- catch { }
- }
- if (excelApplication != null)
- {
- try { excelApplication.Quit(); }
- catch { }
- }
-
- ReleaseExcelObjects();
- }
-
- private void ReleaseExcelObjects()
- {
- ReleaseComObject(titleBlockSheet);
- ReleaseComObject(textEntitiesSheet);
- ReleaseComObject(mappingSheet);
- ReleaseComObject(workbook1);
- ReleaseComObject(mappingWorkbook);
- ReleaseComObject(excelApplication);
-
- titleBlockSheet = null;
- textEntitiesSheet = null;
- mappingSheet = null;
- workbook1 = null;
- mappingWorkbook = null;
- excelApplication = null;
- }
-
- private void ReleaseComObject(object obj)
- {
- try
- {
- if (obj != null && Marshal.IsComObject(obj))
- {
- Marshal.ReleaseComObject(obj);
- }
- }
- catch (System.Exception)
- {
- // 해제 중 오류 발생 시 무시
- }
- finally
- {
- obj = null;
- }
- }
-
- ///
- /// Layer ID로부터 Layer 이름을 가져옵니다.
- ///
- /// Layer ObjectId
- /// 현재 트랜잭션
- /// 데이터베이스 객체
- /// Layer 이름 또는 빈 문자열
- 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)
- {
- Debug.WriteLine($"Layer 이름 가져오기 오류: {ex.Message}");
- return "";
- }
- }
-
- ///
- /// 매핑 딕셔너리를 JSON 파일로 저장합니다.
- ///
- /// 저장할 JSON 파일 경로
- public void SaveMappingDictionary(string filePath)
- {
- try
- {
- Debug.WriteLine($"[DEBUG] 매핑 딕셔너리 저장 시작: {filePath}");
-
- // 딕셔너리를 직렬화 가능한 형태로 변환
- var serializableData = new Dictionary>();
-
- foreach (var fileEntry in FileToMapkeyToLabelTagValuePdf)
- {
- var fileData = new Dictionary();
- 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;
- }
-
- // JSON으로 직렬화
- string jsonContent = JsonConvert.SerializeObject(serializableData, Formatting.Indented);
- File.WriteAllText(filePath, jsonContent, System.Text.Encoding.UTF8);
-
- Debug.WriteLine($"✅ 매핑 딕셔너리 저장 완료: {Path.GetFileName(filePath)}");
- Debug.WriteLine($"📊 저장된 파일 수: {FileToMapkeyToLabelTagValuePdf.Count}");
-
- }
- catch (System.Exception ex)
- {
- Debug.WriteLine($"❌ 매핑 딕셔너리 저장 중 오류:");
- Debug.WriteLine($" 메시지: {ex.Message}");
- Debug.WriteLine($" 스택 트레이스: {ex.StackTrace}");
- throw;
- }
- }
-
- ///
- /// JSON 파일에서 매핑 딕셔너리를 로드합니다.
- ///
- /// 로드할 JSON 파일 경로
- public void LoadMappingDictionary(string filePath)
- {
- try
- {
- Debug.WriteLine($"[DEBUG] 매핑 딕셔너리 로드 시작: {filePath}");
-
- if (!File.Exists(filePath))
- {
- Debug.WriteLine($"⚠️ 매핑 파일이 존재하지 않습니다: {filePath}");
- return;
- }
-
- string jsonContent = File.ReadAllText(filePath, System.Text.Encoding.UTF8);
- Debug.WriteLine($"[DEBUG] JSON 내용 길이: {jsonContent.Length}");
-
- // JSON 내용 정리 (주석 제거 등)
- jsonContent = CleanJsonContent(jsonContent);
-
- Dictionary> deserializedData;
- try
- {
- deserializedData = JsonConvert.DeserializeObject>>(jsonContent);
- }
- catch (Newtonsoft.Json.JsonReaderException jsonEx)
- {
- Debug.WriteLine($"❌ JSON 파싱 오류: {jsonEx.Message}");
- Debug.WriteLine($"❌ JSON 내용 미리보기 (첫 500자):");
- Debug.WriteLine(jsonContent.Length > 500 ? jsonContent.Substring(0, 500) + "..." : jsonContent);
- throw new System.Exception($"매핑 JSON 파일 파싱 실패: {jsonEx.Message}\n파일: {filePath}");
- }
-
- // 새로운 딕셔너리 초기화
- FileToMapkeyToLabelTagValuePdf.Clear();
- Debug.WriteLine("[DEBUG] 기존 딕셔너리 초기화됨");
-
- if (deserializedData != null)
- {
- Debug.WriteLine($"[DEBUG] 역직렬화된 파일 수: {deserializedData.Count}");
-
- foreach (var fileEntry in deserializedData)
- {
- Debug.WriteLine($"[DEBUG] 파일 처리 중: {fileEntry.Key}");
- var fileData = new Dictionary();
-
- 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);
- Debug.WriteLine($"[DEBUG] 항목 로드: {mapEntry.Key} -> AI:{aiLabel}, DWG:{dwgTag}, PDF:{pdfValue}");
- }
-
- FileToMapkeyToLabelTagValuePdf[fileEntry.Key] = fileData;
- Debug.WriteLine($"[DEBUG] 파일 {fileEntry.Key}: {fileData.Count}개 항목 로드됨");
- }
- }
-
- Debug.WriteLine($"✅ 매핑 딕셔너리 로드 완료");
- Debug.WriteLine($"📊 로드된 파일 수: {FileToMapkeyToLabelTagValuePdf.Count}");
- Debug.WriteLine($"📊 총 항목 수: {FileToMapkeyToLabelTagValuePdf.Sum(f => f.Value.Count)}");
-
- }
- catch (System.Exception ex)
- {
- Debug.WriteLine($"❌ 매핑 딕셔너리 로드 중 오류:");
- Debug.WriteLine($" 메시지: {ex.Message}");
- Debug.WriteLine($" 스택 트레이스: {ex.StackTrace}");
- throw;
- }
- }
-
- ///
- /// 매핑 딕셔너리 데이터를 Excel 시트에 기록합니다 (완전한 매핑용).
- ///
- public void WriteCompleteMapping()
- {
- try
- {
- Debug.WriteLine("[DEBUG] WriteCompleteMapping 시작");
- Debug.WriteLine($"[DEBUG] 매핑 딕셔너리 항목 수: {FileToMapkeyToLabelTagValuePdf.Count}");
-
- if (FileToMapkeyToLabelTagValuePdf.Count == 0)
- {
- Debug.WriteLine("⚠️ 매핑 딕셔너리가 비어있습니다.");
- return;
- }
-
- if (mappingSheet == null)
- {
- Debug.WriteLine("❌ 매핑 시트가 초기화되지 않았습니다.");
- return;
- }
-
- Debug.WriteLine($"[DEBUG] 매핑 시트 이름: {mappingSheet.Name}");
- Debug.WriteLine($"[DEBUG] 전체 데이터 항목 수: {FileToMapkeyToLabelTagValuePdf.Sum(f => f.Value.Count)}");
-
- // 샘플 데이터 출력 (처음 3개)
- int sampleCount = 0;
- foreach (var fileEntry in FileToMapkeyToLabelTagValuePdf.Take(2))
- {
- Debug.WriteLine($"[DEBUG] 파일: {fileEntry.Key}");
- foreach (var mapEntry in fileEntry.Value.Take(3))
- {
- var (aiLabel, dwgTag, dwgValue, pdfValue) = mapEntry.Value;
- Debug.WriteLine($"[DEBUG] {mapEntry.Key} -> AI:{aiLabel}, DWG:{dwgTag}, DWGVAL:{dwgValue}, PDF:{pdfValue}");
- sampleCount++;
- }
- }
-
- // 기존 WriteMappingDataToExcel 메서드 호출
- WriteMappingDataToExcel();
-
- Debug.WriteLine("✅ WriteCompleteMapping 완료");
- }
- catch (System.Exception ex)
- {
- Debug.WriteLine($"❌ WriteCompleteMapping 중 오류:");
- Debug.WriteLine($" 메시지: {ex.Message}");
- Debug.WriteLine($" 스택 트레이스: {ex.StackTrace}");
- throw;
- }
- }
-
- ///
- /// PDF 분석 결과 JSON으로 매핑 딕셔너리의 PdfValue를 업데이트합니다.
- ///
- /// PDF 분석 결과 JSON 파일 경로
- public void UpdateWithPdfData(string jsonFilePath)
- {
- try
- {
- Debug.WriteLine($"[DEBUG] PDF 데이터로 매핑 딕셔너리 업데이트 시작: {jsonFilePath}");
-
- if (!File.Exists(jsonFilePath))
- {
- Debug.WriteLine($"❌ JSON 파일이 존재하지 않습니다: {jsonFilePath}");
- return;
- }
-
- // JSON 파일 읽기 및 정리
- string jsonContent = File.ReadAllText(jsonFilePath, System.Text.Encoding.UTF8);
- Debug.WriteLine($"[DEBUG] JSON 파일 크기: {jsonContent.Length} bytes");
-
- // JSON 내용 정리 (주석 제거 등)
- jsonContent = CleanJsonContent(jsonContent);
-
- JObject jsonData;
- try
- {
- jsonData = JObject.Parse(jsonContent);
- }
- catch (Newtonsoft.Json.JsonReaderException jsonEx)
- {
- Debug.WriteLine($"❌ JSON 파싱 오류: {jsonEx.Message}");
- Debug.WriteLine($"❌ JSON 내용 미리보기 (첫 500자):");
- Debug.WriteLine(jsonContent.Length > 500 ? jsonContent.Substring(0, 500) + "..." : jsonContent);
- throw new System.Exception($"PDF 분석 JSON 파일 파싱 실패: {jsonEx.Message}\n파일: {jsonFilePath}");
- }
-
- var results = jsonData["results"] as JArray;
- if (results == null)
- {
- Debug.WriteLine("❌ JSON에서 'results' 배열을 찾을 수 없습니다.");
- Debug.WriteLine($"❌ JSON 루트 키들: {string.Join(", ", jsonData.Properties().Select(p => p.Name))}");
- return;
- }
-
- 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);
- Debug.WriteLine($"[DEBUG] Processing PDF file: {fileNameWithoutExt}");
-
- // 해당 파일의 매핑 데이터 확인
- if (!FileToMapkeyToLabelTagValuePdf.ContainsKey(fileNameWithoutExt))
- {
- Debug.WriteLine($"⚠️ 매핑 데이터에 파일이 없습니다: {fileNameWithoutExt}");
- continue;
- }
-
- // PDF 분석 결과의 각 필드 처리
- foreach (var property in pdfAnalysis.Cast())
- {
- string aiLabel = property.Name; // 예: "설계공구_Station_col1"
- var valueObj = property.Value as JObject;
-
- if (valueObj == null) continue;
-
- string pdfValue = valueObj["value"]?.ToString();
- if (string.IsNullOrEmpty(pdfValue)) continue;
-
- totalEntries++;
-
- // 매핑 딕셔너리에서 해당 항목 찾기
- var fileData = FileToMapkeyToLabelTagValuePdf[fileNameWithoutExt];
-
- // AILabel로 매칭 찾기
- var matchingEntry = fileData.FirstOrDefault(kvp =>
- string.Equals(kvp.Value.Item1.Trim(), aiLabel.Trim(), StringComparison.OrdinalIgnoreCase));
-
- if (!string.IsNullOrEmpty(matchingEntry.Key))
- {
- // 기존 값 유지하면서 PdfValue만 업데이트
- var existingValue = matchingEntry.Value;
- fileData[matchingEntry.Key] = (existingValue.Item1, existingValue.Item2, existingValue.Item3, pdfValue);
- updatedCount++;
-
- Debug.WriteLine($"✅ Updated: {fileNameWithoutExt} -> {aiLabel} = {pdfValue}");
- }
- else
- {
- Debug.WriteLine($"⚠️ No match found: {fileNameWithoutExt} -> {aiLabel}");
- }
- }
- }
-
- Debug.WriteLine($"[DEBUG] PDF 데이터 업데이트 완료: {updatedCount}/{totalEntries} 업데이트됨");
-
- }
- catch (System.Exception ex)
- {
- Debug.WriteLine($"❌ PDF 데이터 업데이트 중 오류:");
- Debug.WriteLine($" 메시지: {ex.Message}");
- Debug.WriteLine($" 스택 트레이스: {ex.StackTrace}");
- throw;
- }
- }
-
- ///
- /// 폴더 간 처리를 위해 누적된 데이터를 정리합니다.
- ///
- public void ClearAccumulatedData()
- {
- try
- {
- Debug.WriteLine("[DEBUG] 누적 데이터 정리 시작");
-
- FileToMapkeyToLabelTagValuePdf.Clear();
-
- titleBlockCurrentRow = 2;
- textEntitiesCurrentRow = 2;
- mappingDataCurrentRow = 2;
-
- Debug.WriteLine("✅ 누적 데이터 정리 완료");
- }
- catch (System.Exception ex)
- {
- Debug.WriteLine($"❌ 누적 데이터 정리 중 오류: {ex.Message}");
- }
- }
-
- ///
- /// JSON 내용을 정리하여 파싱 가능한 상태로 만듭니다.
- /// 주석 제거 및 기타 무효한 문자 처리
- ///
- /// 원본 JSON 내용
- /// 정리된 JSON 내용
- private string CleanJsonContent(string jsonContent)
- {
- if (string.IsNullOrEmpty(jsonContent))
- return jsonContent;
-
- try
- {
- // 줄별로 처리하여 주석 제거
- var lines = jsonContent.Split('\n');
- var cleanedLines = new List();
-
- 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);
- Debug.WriteLine($"[DEBUG] JSON 정리 완료: {jsonContent.Length} -> {result.Length} bytes");
- return result;
- }
- catch (System.Exception ex)
- {
- Debug.WriteLine($"❌ JSON 정리 중 오류: {ex.Message}");
- // 정리 실패시 원본 반환
- return jsonContent;
- }
- }
-
- public void Dispose()
- {
- try
- {
- Debug.WriteLine("[DEBUG] ExportExcel Dispose 시작");
-
- if (excelApplication != null)
- {
- Debug.WriteLine("[DEBUG] Excel 객체 정리 중...");
- CloseExcelObjects();
- }
-
- 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)
- {
- Debug.WriteLine($"[DEBUG] ExportExcel Dispose 중 전역 오류: {ex.Message}");
- // Disposal 오류는 로그만 남기고 계속 진행
- }
- }
- }
-}
diff --git a/Models/IntersectionTestDebugger.cs b/Models/IntersectionTestDebugger.cs
new file mode 100644
index 0000000..1e19850
--- /dev/null
+++ b/Models/IntersectionTestDebugger.cs
@@ -0,0 +1,267 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using Teigha.Geometry;
+
+namespace DwgExtractorManual.Models
+{
+ ///
+ /// 교차점 생성 및 셀 추출 로직을 테스트하고 디버깅하는 클래스
+ ///
+ public class IntersectionTestDebugger
+ {
+ ///
+ /// 간단한 테스트 테이블을 만들어서 교차점과 셀 생성을 테스트합니다.
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// 테스트용 3x4 테이블 선분들을 생성합니다.
+ ///
+ 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;
+ }
+
+ ///
+ /// 테스트 선분들로부터 교차점을 찾습니다.
+ ///
+ private static List FindTestIntersections(List<(Point3d start, Point3d end, bool isHorizontal)> segments, DwgDataExtractor extractor)
+ {
+ var intersections = new List();
+ 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;
+ }
+
+ ///
+ /// 특정 점에서의 DirectionBits를 계산합니다.
+ ///
+ 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;
+ }
+
+ ///
+ /// 교차점들로부터 셀을 추출합니다.
+ ///
+ private static List ExtractTestCells(List intersections,
+ List<(Point3d start, Point3d end, bool isHorizontal)> segments,
+ DwgDataExtractor extractor)
+ {
+ var cells = new List();
+ 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;
+ }
+
+ ///
+ /// 테스트용 bottomRight 찾기 메서드
+ ///
+ private static IntersectionPoint FindBottomRightForTest(IntersectionPoint topLeft,
+ List 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;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/Models/NoteExtractionTester.cs b/Models/NoteExtractionTester.cs
new file mode 100644
index 0000000..fcf63a2
--- /dev/null
+++ b/Models/NoteExtractionTester.cs
@@ -0,0 +1,177 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Collections.Generic;
+
+namespace DwgExtractorManual.Models
+{
+ ///
+ /// Note 박스 텍스트와 테이블 추출 기능을 테스트하는 클래스
+ ///
+ public class NoteExtractionTester
+ {
+ ///
+ /// DWG 파일에서 Note 데이터를 추출하고 CSV로 저장하는 전체 테스트
+ ///
+ 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}");
+ }
+ }
+ }
+
+ ///
+ /// 추출된 Note 데이터를 분석하여 로그로 출력
+ ///
+ private static void AnalyzeNoteData(List 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 데이터 분석 완료 ===");
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/Models/SettingsManager.cs b/Models/SettingsManager.cs
index e69de29..3b1da61 100644
--- a/Models/SettingsManager.cs
+++ b/Models/SettingsManager.cs
@@ -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(json);
+ }
+ }
+}
diff --git a/Models/SqlDatas.cs b/Models/SqlDatas.cs
index 881225a..27c1a7c 100644
--- a/Models/SqlDatas.cs
+++ b/Models/SqlDatas.cs
@@ -15,7 +15,7 @@ namespace DwgExtractorManual.Models
///
internal sealed class SqlDatas : IDisposable
{
- Services appServices; // ODA 제품 활성화용 (managed by singleton)
+ Services? appServices; // ODA 제품 활성화용 (managed by singleton)
readonly string connectionString = "Host=localhost;Database=postgres;Username=postgres;Password=Qwer1234";
void InitializeTeighaServices()
@@ -143,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();
}
@@ -162,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();
}
@@ -231,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();
}
diff --git a/Models/TableCellVisualizationData.cs b/Models/TableCellVisualizationData.cs
new file mode 100644
index 0000000..ff23b08
--- /dev/null
+++ b/Models/TableCellVisualizationData.cs
@@ -0,0 +1,115 @@
+using System;
+using System.Collections.Generic;
+using Teigha.Geometry;
+
+namespace DwgExtractorManual.Models
+{
+ ///
+ /// 테이블 셀 시각화를 위한 데이터 클래스
+ ///
+ public class TableCellVisualizationData
+ {
+ public string FileName { get; set; } = "";
+ public string NoteText { get; set; } = "";
+ public List Cells { get; set; } = new List();
+ public List TableSegments { get; set; } = new List();
+ public List TextEntities { get; set; } = new List();
+ public List IntersectionPoints { get; set; } = new List(); // 교차점 정보 추가
+ public List DiagonalLines { get; set; } = new List(); // 셀 대각선 정보 추가
+ public List CellBoundaries { get; set; } = new List(); // 정확한 셀 경계 정보 추가
+ public (double minX, double minY, double maxX, double maxY) NoteBounds { get; set; }
+ }
+
+ ///
+ /// 셀 경계 정보
+ ///
+ 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;
+ }
+
+ ///
+ /// 선분 정보
+ ///
+ 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";
+ }
+
+ ///
+ /// 텍스트 정보
+ ///
+ 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";
+ }
+
+ ///
+ /// 교차점 정보
+ ///
+ 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";
+ }
+
+ ///
+ /// 대각선 정보 (셀 디버깅용)
+ ///
+ 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; } = ""; // 디버깅 라벨
+ }
+
+ ///
+ /// 정확한 셀 경계 정보 (4개 모서리 좌표)
+ ///
+ 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; } = ""; // 셀 내 텍스트 내용
+ }
+}
\ No newline at end of file
diff --git a/Models/TeighaServicesManager.cs b/Models/TeighaServicesManager.cs
index 201deb1..31090d4 100644
--- a/Models/TeighaServicesManager.cs
+++ b/Models/TeighaServicesManager.cs
@@ -10,8 +10,8 @@ namespace DwgExtractorManual.Models
public sealed class TeighaServicesManager
{
private static readonly object _lock = new object();
- private static TeighaServicesManager _instance = null;
- private static Services _services = null;
+ private static TeighaServicesManager? _instance = null;
+ private static Services? _services = null;
private static int _referenceCount = 0;
private static bool _isActivated = false;
diff --git a/Views/TableCellVisualizationWindow.xaml b/Views/TableCellVisualizationWindow.xaml
new file mode 100644
index 0000000..79fdecc
--- /dev/null
+++ b/Views/TableCellVisualizationWindow.xaml
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Views/TableCellVisualizationWindow.xaml.cs b/Views/TableCellVisualizationWindow.xaml.cs
new file mode 100644
index 0000000..6f8d8b8
--- /dev/null
+++ b/Views/TableCellVisualizationWindow.xaml.cs
@@ -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 _visualizationData;
+ private TableCellVisualizationData? _currentData;
+ private double _scale = 1.0;
+ private const double MARGIN = 50;
+
+ public TableCellVisualizationWindow(List 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})";
+ }
+ }
+}
\ No newline at end of file
diff --git a/csharp_mapping_usage.cs b/csharp_mapping_usage.cs
index 8e639c1..c76b039 100644
--- a/csharp_mapping_usage.cs
+++ b/csharp_mapping_usage.cs
@@ -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 AilabelToSystems { get; set; }
+ public Dictionary 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 ExpresswayToTransportation { get; set; }
+ public Dictionary ExpresswayToTransportation { get; set; } = default!;
}
// 필드 매퍼 클래스
@@ -73,7 +73,7 @@ public class FieldMapper
var mappingData = JsonSerializer.Deserialize(jsonContent, options);
Console.WriteLine($"[DEBUG] 매핑 테이블 로드 성공: {mappingData?.MappingTable?.AilabelToSystems?.Count ?? 0}개 항목");
- return new FieldMapper(mappingData);
+ return new FieldMapper(mappingData!);
}
catch (JsonException jsonEx)
{
@@ -213,7 +213,7 @@ public class FieldMapper
///
/// AI 라벨을 고속도로공사 필드명으로 변환
///
- public string AilabelToExpressway(string ailabel)
+ public string? AilabelToExpressway(string ailabel)
{
if (_mappingData.MappingTable.AilabelToSystems.TryGetValue(ailabel, out var systemFields))
{
@@ -225,7 +225,7 @@ public class FieldMapper
///
/// AI 라벨을 DocAiKey 값으로 변환
///
- public string AilabelToDocAiKey(string ailabel)
+ public string? AilabelToDocAiKey(string ailabel)
{
if (_mappingData.MappingTable.AilabelToSystems.TryGetValue(ailabel, out var systemFields))
{
@@ -237,7 +237,7 @@ public class FieldMapper
///
/// 고속도로공사 필드명을 교통부 필드명으로 변환
///
- public string ExpresswayToTransportation(string expresswayField)
+ public string? ExpresswayToTransportation(string expresswayField)
{
if (_mappingData.MappingTable.SystemMappings.ExpresswayToTransportation.TryGetValue(expresswayField, out var transportationField))
{
@@ -249,7 +249,7 @@ public class FieldMapper
///
/// DocAiKey 값으로부터 해당하는 AI 라벨을 반환
///
- public string DocAiKeyToAilabel(string docAiKey)
+ public string? DocAiKeyToAilabel(string docAiKey)
{
if (string.IsNullOrEmpty(docAiKey))
{
@@ -269,7 +269,7 @@ public class FieldMapper
///
/// Expressway 필드값으로부터 해당하는 AI 라벨을 반환
///
- public string ExpresswayToAilabel(string expresswayField)
+ public string? ExpresswayToAilabel(string expresswayField)
{
if (string.IsNullOrEmpty(expresswayField))
{
@@ -289,7 +289,7 @@ public class FieldMapper
///
/// AI 라벨 → 고속도로공사 → 교통부 순서로 변환
///
- public string AilabelToTransportationViaExpressway(string ailabel)
+ public string? AilabelToTransportationViaExpressway(string ailabel)
{
var expresswayField = AilabelToExpressway(ailabel);
if (!string.IsNullOrEmpty(expresswayField))
@@ -302,7 +302,7 @@ public class FieldMapper
///
/// AI 라벨에 해당하는 모든 시스템의 필드명을 반환
///
- public SystemFields GetAllSystemFields(string ailabel)
+ public SystemFields? GetAllSystemFields(string ailabel)
{
if (_mappingData.MappingTable.AilabelToSystems.TryGetValue(ailabel, out var systemFields))
{
@@ -314,9 +314,9 @@ public class FieldMapper
///
/// 여러 AI 라벨을 한번에 고속도로공사 필드명으로 변환
///
- public Dictionary BatchConvertAilabelToExpressway(IEnumerable ailabels)
+ public Dictionary BatchConvertAilabelToExpressway(IEnumerable ailabels)
{
- var results = new Dictionary();
+ var results = new Dictionary();
foreach (var label in ailabels)
{
results[label] = AilabelToExpressway(label);
@@ -327,9 +327,9 @@ public class FieldMapper
///
/// 여러 고속도로공사 필드를 한번에 교통부 필드명으로 변환
///
- public Dictionary BatchConvertExpresswayToTransportation(IEnumerable expresswayFields)
+ public Dictionary BatchConvertExpresswayToTransportation(IEnumerable expresswayFields)
{
- var results = new Dictionary();
+ var results = new Dictionary();
foreach (var field in expresswayFields)
{
results[field] = ExpresswayToTransportation(field);