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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +