Compare commits

...

16 Commits

Author SHA1 Message Date
horu2day
52fbc1c967 1차 완료 2025-08-13 13:57:46 +09:00
horu2day
f114b8b642 도면에서 표 추출 2025-08-12 14:33:18 +09:00
박승우우
3abb3c07ce raycasting note content box detect 2025-07-31 11:23:46 +09:00
박승우우
107eab90fa NoteDetectionRefactor plan 2025-07-31 10:29:56 +09:00
박승우우
9c76b624bf note detection v2 2025-07-31 10:03:48 +09:00
박승우우
0278688b28 노트 블럭별 추출 2025-07-30 17:20:00 +09:00
박승우우
22aa118316 note detect rollback 2025-07-30 15:18:59 +09:00
박승우우
5ead0e8045 노트별 분리 2025-07-30 13:50:41 +09:00
박승우우
66dd64306c ExportExcel 정리, height정렬 attRef 분리 2025-07-29 14:06:12 +09:00
박승우우
8bd5d9580c Height sort, 한 파일에 2025-07-28 11:20:21 +09:00
박승우우
9b94b59c49 excel 메모리누수 2025-07-25 10:30:39 +09:00
박승우우
ddb4a1c408 un-revert 2025-07-23 13:50:02 +09:00
박승우우
24b5ab9686 Revert "dwg only 버튼 추가"
This reverts commit a87644d8be.
2025-07-23 13:16:00 +09:00
박승우우
a87644d8be dwg only 버튼 추가 2025-07-23 13:15:27 +09:00
박승우우
b13e981d04 teigha 오류 수정 2025-07-22 10:45:51 +09:00
박승우우
5282927833 TD_CNAME 삭제 2025-07-21 16:30:13 +09:00
29 changed files with 8992 additions and 1255 deletions

View File

@@ -0,0 +1,20 @@
{
"permissions": {
"allow": [
"Bash(dotnet build)",
"Bash(\"C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\MSBuild\\Current\\Bin\\MSBuild.exe\" DwgExtractorManual.csproj -p:Configuration=Debug -p:Platform=x64)",
"Bash(mkdir:*)",
"Bash(where msbuild)",
"Bash(\"C:\\Program Files\\Microsoft Visual Studio\\2022\\Professional\\MSBuild\\Current\\Bin\\amd64\\MSBuild.exe\" DwgExtractorManual.csproj -p:Configuration=Debug -p:Platform=x64)",
"Bash(dotnet run:*)",
"Bash(echo $HOME)",
"Bash(find:*)",
"Bash(dotnet clean:*)",
"Bash(dotnet build:*)",
"Bash(taskkill:*)",
"Bash(wmic process where ProcessId=17428 delete:*)"
],
"deny": []
},
"contextFileName": "AGENTS.md"
}

85
AGENTS.md Normal file
View File

@@ -0,0 +1,85 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is a C# WPF application that extracts data from DWG (AutoCAD) files and processes them using AI analysis. The application has two main components:
1. **C# WPF Application** (`DwgExtractorManual`) - Main GUI application for DWG processing
2. **Python Analysis Module** (`fletimageanalysis`) - AI-powered document analysis using Gemini API
## Build and Development Commands
### C# Application
```bash
# Build the application
dotnet build
# Run the application
dotnet run
# Clean build artifacts
dotnet clean
# Publish for deployment
dotnet publish -c Release
```
### Python Module Setup
```bash
# Run the cleanup and setup script (Windows)
cleanup_and_setup.bat
# Or manually setup Python environment
cd fletimageanalysis
python -m venv venv
call venv\Scripts\activate.bat
pip install -r requirements.txt
```
### Python CLI Usage
```bash
# Batch process files via CLI
cd fletimageanalysis
python batch_cli.py --files "file1.pdf,file2.dxf" --schema "한국도로공사" --concurrent 3 --output "results.csv"
```
## Architecture
### C# Component Structure
- **MainWindow.xaml.cs** - Main WPF window and UI logic
- **Models/DwgDataExtractor.cs** - Core DWG file processing using Teigha SDK
- **Models/ExcelDataWriter.cs** - Excel output generation using Office Interop
- **Models/TeighaServicesManager.cs** - Singleton manager for Teigha SDK lifecycle
- **Models/FieldMapper.cs** - Maps extracted data to target formats
- **Models/SettingsManager.cs** - Application configuration management
### Python Component Structure
- **batch_cli.py** - Command-line interface for batch processing
- **multi_file_processor.py** - Orchestrates multi-file processing workflows
- **gemini_analyzer.py** - AI analysis using Google Gemini API
- **pdf_processor.py** - PDF document processing
- **dxf_processor.py** - DXF file processing
- **csv_exporter.py** - CSV output generation
### Key Dependencies
- **Teigha SDK** - DWG file reading and CAD entity processing (requires DLL files in specific path)
- **Microsoft Office Interop** - Excel file generation
- **Npgsql** - PostgreSQL database connectivity
- **Google Gemini API** - AI-powered document analysis
- **PyMuPDF** - PDF processing in Python component
## Current Development Focus
The project is undergoing a **Note Detection Refactor** (see `NoteDetectionRefactor.md`):
- Replacing fragile "horizontal search line" algorithm in `DwgDataExtractor.cs`
- Implementing robust "vertical ray-casting" approach for NOTE content box detection
- Key methods being refactored: `FindNoteBox`, `GetAllLineSegments`, `TraceBoxFromTopLine`
## Important Notes
- Teigha DLLs must be present in the specified path for DWG processing to work
- The Python module requires Google Gemini API key configuration
- Excel output uses COM Interop and requires Microsoft Office installation
- The application supports both manual GUI operation and automated batch processing via CLI

14
App.config Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="office"
publicKeyToken="71e9bce111e9429c"
culture="neutral" />
<bindingRedirect oldVersion="15.0.0.0"
newVersion="16.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

1
CLAUDE.md Normal file
View File

@@ -0,0 +1 @@
See @AGENTS.md for guidelines.

144
Controls/ZoomBorder.cs Normal file
View File

@@ -0,0 +1,144 @@
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using Cursors = System.Windows.Input.Cursors;
using Point = System.Windows.Point;
namespace DwgExtractorManual.Controls
{
public class ZoomBorder : Border
{
private UIElement? child = null;
private Point origin;
private Point start;
private TranslateTransform GetTranslateTransform(UIElement element)
{
return (TranslateTransform)((TransformGroup)element.RenderTransform)
.Children.First(tr => tr is TranslateTransform);
}
private ScaleTransform GetScaleTransform(UIElement element)
{
return (ScaleTransform)((TransformGroup)element.RenderTransform)
.Children.First(tr => tr is ScaleTransform);
}
public override UIElement Child
{
get { return base.Child; }
set
{
if (value != null && value != this.Child)
this.Initialize(value);
base.Child = value;
}
}
public void Initialize(UIElement element)
{
this.child = element;
if (child != null)
{
TransformGroup group = new TransformGroup();
ScaleTransform st = new ScaleTransform();
group.Children.Add(st);
TranslateTransform tt = new TranslateTransform();
group.Children.Add(tt);
child.RenderTransform = group;
child.RenderTransformOrigin = new Point(0.0, 0.0);
this.MouseWheel += child_MouseWheel;
this.MouseLeftButtonDown += child_MouseLeftButtonDown;
this.MouseLeftButtonUp += child_MouseLeftButtonUp;
this.MouseMove += child_MouseMove;
this.PreviewMouseRightButtonDown += new MouseButtonEventHandler(
child_PreviewMouseRightButtonDown);
}
}
public void Reset()
{
if (child != null)
{
// reset zoom
var st = GetScaleTransform(child);
st.ScaleX = 1.0;
st.ScaleY = 1.0;
// reset pan
var tt = GetTranslateTransform(child);
tt.X = 0.0;
tt.Y = 0.0;
}
}
private void child_MouseWheel(object sender, MouseWheelEventArgs e)
{
if (child != null)
{
var st = GetScaleTransform(child);
var tt = GetTranslateTransform(child);
double zoom = e.Delta > 0 ? .2 : -.2;
if (!(e.Delta > 0) && (st.ScaleX < .4 || st.ScaleY < .4))
return;
Point relative = e.GetPosition(child);
double absoluteX;
double absoluteY;
absoluteX = relative.X * st.ScaleX + tt.X;
absoluteY = relative.Y * st.ScaleY + tt.Y;
st.ScaleX += zoom;
st.ScaleY += zoom;
tt.X = absoluteX - relative.X * st.ScaleX;
tt.Y = absoluteY - relative.Y * st.ScaleY;
}
}
private void child_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (child != null)
{
var tt = GetTranslateTransform(child);
start = e.GetPosition(this);
origin = new Point(tt.X, tt.Y);
this.Cursor = Cursors.Hand;
child.CaptureMouse();
}
}
private void child_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (child != null)
{
child.ReleaseMouseCapture();
this.Cursor = Cursors.Arrow;
}
}
void child_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
this.Reset();
}
private void child_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
{
if (child != null)
{
if (child.IsMouseCaptured)
{
var tt = GetTranslateTransform(child);
Vector v = start - e.GetPosition(this);
tt.X = origin.X - v.X;
tt.Y = origin.Y - v.Y;
}
}
}
}
}

View File

@@ -6,30 +6,17 @@
<UseWindowsForms>True</UseWindowsForms> <UseWindowsForms>True</UseWindowsForms>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<COMReference Include="Microsoft.Office.Interop.Excel">
<VersionMinor>9</VersionMinor>
<VersionMajor>1</VersionMajor>
<Guid>00020813-0000-0000-c000-000000000046</Guid>
<Lcid>0</Lcid>
<WrapperTool>tlbimp</WrapperTool>
<Isolated>false</Isolated>
<EmbedInteropTypes>true</EmbedInteropTypes>
</COMReference>
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Npgsql" Version="9.0.1" /> <PackageReference Include="Npgsql" Version="9.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
<Reference Include="TD_Mgd_23.12_16">
<HintPath>D:\dev_Net8_git\trunk\DLL\Teigha\vc16_amd64dll_23.12SP2\TD_Mgd_23.12_16.dll</HintPath>
<Private>true</Private>
</Reference>
</ItemGroup> </ItemGroup>
<!-- Copy all Teigha DLLs including native dependencies --> <!-- Copy all Teigha DLLs including native dependencies -->
@@ -39,68 +26,33 @@
</None> </None>
</ItemGroup> </ItemGroup>
<!-- Define fletimageanalysis files as content to be copied -->
<!-- Separate JSON files and other files for different copy behaviors -->
<ItemGroup> <ItemGroup>
<!-- JSON files - always copy as new --> <Reference Include="TD_Mgd_23.12_16">
<FletImageAnalysisJsonFiles Include="fletimageanalysis\**\*.json" /> <HintPath>..\..\..\GitNet8\trunk\DLL\Teigha\vc16_amd64dll_23.12SP2\TD_Mgd_23.12_16.dll</HintPath>
</Reference>
<!-- Other files - incremental copy -->
<FletImageAnalysisOtherFiles Include="fletimageanalysis\**\*" Exclude="fletimageanalysis\**\*.json;fletimageanalysis\**\*.pyc;fletimageanalysis\**\__pycache__\**;fletimageanalysis\**\*.tmp;fletimageanalysis\**\*.log;fletimageanalysis\**\*.git\**" />
</ItemGroup> </ItemGroup>
<!-- Enhanced copy target that handles both incremental updates and missing folder scenarios --> <ItemGroup>
<Target Name="CopyFletImageAnalysisFolder" AfterTargets="Build"> <Reference Include="Microsoft.Office.Interop.Excel">
<HintPath>C:\Program Files (x86)\Microsoft Office\Office16\DCF\Microsoft.Office.Interop.Excel.dll</HintPath>
<!-- Always show what we're doing --> <EmbedInteropTypes>false</EmbedInteropTypes>
<Message Text="Copying fletimageanalysis folder contents..." Importance="normal" /> </Reference>
<Message Text="JSON files to copy: @(FletImageAnalysisJsonFiles->Count())" Importance="normal" /> <Reference Include="office">
<Message Text="Other files to copy: @(FletImageAnalysisOtherFiles->Count())" Importance="normal" /> <HintPath>C:\Program Files (x86)\Microsoft Office\Office16\DCF\office.dll</HintPath>
<EmbedInteropTypes>false</EmbedInteropTypes>
</Reference>
</ItemGroup>
<!-- Copy JSON files - ALWAYS as new (SkipUnchangedFiles=false) --> <ItemGroup>
<Copy <Content Include="fletimageanalysis\**\*">
SourceFiles="@(FletImageAnalysisJsonFiles)" <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
DestinationFiles="@(FletImageAnalysisJsonFiles->'$(OutDir)%(RecursiveDir)%(Filename)%(Extension)')" <Link>fletimageanalysis\%(RecursiveDir)%(FileName)%(Extension)</Link>
SkipUnchangedFiles="false" </Content>
OverwriteReadOnlyFiles="true" /> </ItemGroup>
<Message Text="JSON files copied (always as new): @(FletImageAnalysisJsonFiles->Count())" Importance="high" />
<!-- Copy other files - incrementally (SkipUnchangedFiles=true) -->
<Copy
SourceFiles="@(FletImageAnalysisOtherFiles)"
DestinationFiles="@(FletImageAnalysisOtherFiles->'$(OutDir)%(RecursiveDir)%(Filename)%(Extension)')"
SkipUnchangedFiles="true"
OverwriteReadOnlyFiles="true" />
<Message Text="Other files copied (incremental): @(FletImageAnalysisOtherFiles->Count())" Importance="normal" />
<!-- Verify critical JSON file exists after copy -->
<Error Condition="!Exists('$(OutDir)fletimageanalysis\mapping_table_json.json')"
Text="Critical file missing after copy: mapping_table_json.json" />
<Message Text="fletimageanalysis folder copy completed successfully." Importance="high" />
</Target>
<!-- Additional target to ensure folder is copied during publish -->
<Target Name="CopyFletImageAnalysisFolderOnPublish" AfterTargets="Publish">
<Message Text="Copying fletimageanalysis folder for publish..." Importance="high" />
<!-- Copy JSON files - always as new -->
<Copy
SourceFiles="@(FletImageAnalysisJsonFiles)"
DestinationFiles="@(FletImageAnalysisJsonFiles->'$(PublishDir)%(RecursiveDir)%(Filename)%(Extension)')"
SkipUnchangedFiles="false"
OverwriteReadOnlyFiles="true" />
<!-- Copy other files - always as new for publish -->
<Copy
SourceFiles="@(FletImageAnalysisOtherFiles)"
DestinationFiles="@(FletImageAnalysisOtherFiles->'$(PublishDir)%(RecursiveDir)%(Filename)%(Extension)')"
SkipUnchangedFiles="false"
OverwriteReadOnlyFiles="true" />
<Message Text="fletimageanalysis folder publish copy completed." Importance="high" />
</Target>
</Project> </Project>

173
IntersectionTestConsole.cs Normal file
View File

@@ -0,0 +1,173 @@
//using System;
//using System.Collections.Generic;
//using System.Diagnostics;
//using System.Linq;
//using Teigha.Geometry;
//using DwgExtractorManual.Models;
//namespace DwgExtractorManual
//{
// /// <summary>
// /// 콘솔에서 실행할 수 있는 교차점 테스트 프로그램
// /// </summary>
// class IntersectionTestConsole
// {
// static void Main(string[] args)
// {
// Console.WriteLine("=== 교차점 테스트 프로그램 시작 ===");
// try
// {
// RunSimpleIntersectionTest();
// }
// catch (Exception ex)
// {
// Console.WriteLine($"오류 발생: {ex.Message}");
// Console.WriteLine(ex.StackTrace);
// }
// Console.WriteLine("테스트 완료. 아무 키나 누르세요...");
// Console.ReadKey();
// }
// static void RunSimpleIntersectionTest()
// {
// Console.WriteLine("테스트 시작: 3x4 그리드 생성");
// // 간단한 3x4 테이블 시뮬레이션 (실제 DWG 없이)
// var intersections = new List<IntersectionPoint>();
// // 수동으로 교차점 생성 (3행 x 4열 = 12개 교차점)
// for (int row = 1; row <= 4; row++) // 4개 행
// {
// for (int col = 1; col <= 5; col++) // 5개 열
// {
// double x = (col - 1) * 10.0; // 0, 10, 20, 30, 40
// double y = (row - 1) * 10.0; // 0, 10, 20, 30
// int directionBits = CalculateDirectionBits(row, col, 4, 5);
// var intersection = new IntersectionPoint
// {
// Position = new Point3d(x, y, 0),
// DirectionBits = directionBits,
// Row = row,
// Column = col
// };
// intersections.Add(intersection);
// Console.WriteLine($"교차점 R{row}C{col}: ({x:F0},{y:F0}) - DirectionBits: {directionBits}");
// }
// }
// Console.WriteLine($"\n총 {intersections.Count}개 교차점 생성됨");
// // DirectionBits 검증
// TestDirectionBitsValidation(intersections);
// // 셀 추출 시뮬레이션
// TestCellExtraction(intersections);
// }
// static int CalculateDirectionBits(int row, int col, int maxRow, int maxCol)
// {
// int bits = 0;
// // Right: 1 - 오른쪽에 더 많은 열이 있으면
// if (col < maxCol) bits |= 1;
// // Up: 2 - 위쪽에 더 많은 행이 있으면
// if (row < maxRow) bits |= 2;
// // Left: 4 - 왼쪽에 열이 있으면
// if (col > 1) bits |= 4;
// // Down: 8 - 아래쪽에 행이 있으면
// if (row > 1) bits |= 8;
// return bits;
// }
// static void TestDirectionBitsValidation(List<IntersectionPoint> intersections)
// {
// Console.WriteLine("\n=== DirectionBits 검증 ===");
// var mappingData = new MappingTableData();
// var fieldMapper = new FieldMapper(mappingData);
// var extractor = new DwgDataExtractor(fieldMapper);
// foreach (var intersection in intersections)
// {
// bool isTopLeft = extractor.IsValidTopLeft(intersection.DirectionBits);
// bool isBottomRight = extractor.IsValidBottomRight(intersection.DirectionBits);
// Console.WriteLine($"R{intersection.Row}C{intersection.Column} (bits: {intersection.DirectionBits:D2}) - " +
// $"TopLeft: {isTopLeft}, BottomRight: {isBottomRight}");
// }
// }
// static void TestCellExtraction(List<IntersectionPoint> intersections)
// {
// Console.WriteLine("\n=== 셀 추출 테스트 ===");
// var mappingData = new MappingTableData();
// var fieldMapper = new FieldMapper(mappingData);
// var extractor = new DwgDataExtractor(fieldMapper);
// // topLeft 후보들 찾기
// var topLeftCandidates = intersections.Where(i => extractor.IsValidTopLeft(i.DirectionBits)).ToList();
// Console.WriteLine($"TopLeft 후보: {topLeftCandidates.Count}개");
// foreach (var topLeft in topLeftCandidates)
// {
// Console.WriteLine($"\nTopLeft R{topLeft.Row}C{topLeft.Column} 처리 중...");
// // bottomRight 찾기 시뮬레이션
// var bottomRight = FindBottomRightSimulation(topLeft, intersections, extractor);
// if (bottomRight != null)
// {
// Console.WriteLine($" -> BottomRight 발견: R{bottomRight.Row}C{bottomRight.Column}");
// Console.WriteLine($" 셀 생성: ({topLeft.Position.X:F0},{bottomRight.Position.Y:F0}) to ({bottomRight.Position.X:F0},{topLeft.Position.Y:F0})");
// }
// else
// {
// Console.WriteLine(" -> BottomRight을 찾지 못함");
// }
// }
// }
// static IntersectionPoint? FindBottomRightSimulation(IntersectionPoint topLeft, List<IntersectionPoint> intersections, DwgDataExtractor extractor)
// {
// // 교차점들을 Row/Column으로 딕셔너리 구성
// var intersectionLookup = intersections
// .GroupBy(i => i.Row)
// .ToDictionary(g => g.Key, g => g.ToDictionary(i => i.Column, i => i));
// int maxRow = intersectionLookup.Keys.Max();
// int maxColumn = intersectionLookup.Values.SelectMany(row => row.Keys).Max();
// for (int targetRow = topLeft.Row + 1; targetRow <= maxRow + 1; targetRow++)
// {
// if (!intersectionLookup.ContainsKey(targetRow)) continue;
// var rowIntersections = intersectionLookup[targetRow];
// var availableColumns = rowIntersections.Keys.Where(col => col >= topLeft.Column).OrderBy(col => col);
// foreach (int targetColumn in availableColumns)
// {
// var candidate = rowIntersections[targetColumn];
// // bottomRight 검증 또는 테이블 경계 조건
// if (extractor.IsValidBottomRight(candidate.DirectionBits) ||
// (targetRow == maxRow && targetColumn == maxColumn))
// {
// return candidate;
// }
// }
// }
// return null;
// }
// }
//}

View File

@@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Windows;
using DwgExtractorManual.Models;
using DwgExtractorManual.Views;
using MessageBox = System.Windows.MessageBox;
namespace DwgExtractorManual
{
/// <summary>
/// MainWindow의 시각화 관련 기능을 담당하는 partial class
/// </summary>
public partial class MainWindow
{
// 시각화 데이터 저장
private static List<TableCellVisualizationData> _visualizationDataCache = new List<TableCellVisualizationData>();
/// <summary>
/// 테이블 셀 시각화 데이터를 저장합니다.
/// </summary>
public static void SaveVisualizationData(TableCellVisualizationData data)
{
_visualizationDataCache.Add(data);
Debug.WriteLine($"[VISUALIZATION] 시각화 데이터 저장: {data.FileName}, 셀 수: {data.Cells.Count}");
}
/// <summary>
/// 저장된 시각화 데이터를 가져옵니다.
/// </summary>
public static List<TableCellVisualizationData> GetVisualizationData()
{
Debug.WriteLine($"[VISUALIZATION] 시각화 데이터 조회: {_visualizationDataCache.Count}개 항목");
return new List<TableCellVisualizationData>(_visualizationDataCache);
}
/// <summary>
/// 시각화 데이터를 초기화합니다.
/// </summary>
public static void ClearVisualizationData()
{
Debug.WriteLine($"[VISUALIZATION] 시각화 데이터 초기화 (기존 {_visualizationDataCache.Count}개 항목 삭제)");
_visualizationDataCache.Clear();
Debug.WriteLine("[VISUALIZATION] 시각화 데이터 초기화 완료");
}
/// <summary>
/// 테이블 셀 시각화 창을 엽니다.
/// </summary>
private void BtnVisualizeCells_Click(object sender, RoutedEventArgs e)
{
try
{
LogMessage("🎨 테이블 셀 시각화 창을 여는 중...");
var visualizationData = GetVisualizationData();
LogMessage($"[DEBUG] 조회된 시각화 데이터: {visualizationData.Count}개");
if (visualizationData.Count == 0)
{
MessageBox.Show("시각화할 데이터가 없습니다.\n먼저 'DWG추출(Height정렬)' 버튼을 눌러 데이터를 추출해주세요.",
"데이터 없음", MessageBoxButton.OK, MessageBoxImage.Information);
LogMessage("⚠️ 시각화할 데이터가 없습니다. 먼저 추출을 진행해주세요.");
return;
}
var visualizationWindow = new TableCellVisualizationWindow(visualizationData);
visualizationWindow.Owner = this;
visualizationWindow.Show();
LogMessage($"✅ 시각화 창 열기 완료 - {visualizationData.Count}개 파일 데이터");
}
catch (Exception ex)
{
LogMessage($"❌ 시각화 창 열기 중 오류: {ex.Message}");
MessageBox.Show($"시각화 창을 여는 중 오류가 발생했습니다:\n{ex.Message}",
"오류", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
}

View File

@@ -1,7 +1,7 @@
<Window x:Class="DwgExtractorManual.MainWindow" <Window x:Class="DwgExtractorManual.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="DWG 정보 추출기" Height="700" Width="900" Title="DWG 정보 추출기" Height="Auto" Width="900"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
MinHeight="600" MinWidth="800"> MinHeight="600" MinWidth="800">
@@ -12,6 +12,7 @@
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/> <RowDefinition Height="*"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
@@ -89,6 +90,7 @@
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- 진행률 바 --> <!-- 진행률 바 -->
@@ -153,12 +155,101 @@
</Style> </Style>
</Button.Style> </Button.Style>
</Button> </Button>
<Button x:Name="btnAuto"
Content="🤖 자동" Width="150" Height="45"
Margin="5,0"
Click="BtnAuto_Click" FontSize="14" FontWeight="Bold"
Background="#4CAF50" Foreground="White"
BorderThickness="0">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Background" Value="#4CAF50"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#45A049"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</StackPanel>
<!-- Second row of buttons -->
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,5,0,10">
<Button x:Name="btnDwgOnly"
Content="🔧 DWG추출(폴더별)" Width="150" Height="45"
Margin="5,0"
Click="BtnDwgOnly_Click" FontSize="14" FontWeight="Bold"
Background="#8B4513" Foreground="White"
BorderThickness="0">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Background" Value="#8B4513"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#A0522D"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
<Button x:Name="btnDwgHeightSort"
Content="📏 DWG추출(Height정렬)" Width="150" Height="45"
Margin="5,0"
Click="BtnDwgHeightSort_Click" FontSize="14" FontWeight="Bold"
Background="#FF6B35" Foreground="White"
BorderThickness="0">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Background" Value="#FF6B35"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#E55A2B"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
<Button x:Name="btnVisualizeCells"
Content="🎨 셀 시각화" Width="150" Height="45"
Margin="5,0"
Click="BtnVisualizeCells_Click" FontSize="14" FontWeight="Bold"
Background="#9B59B6" Foreground="White"
BorderThickness="0">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Background" Value="#9B59B6"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#8E44AD"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
<Button x:Name="btnTestIntersection"
Content="🔬 교차점 테스트" Width="150" Height="45"
Margin="5,0"
Click="BtnTestIntersection_Click" FontSize="14" FontWeight="Bold"
Background="#E74C3C" Foreground="White"
BorderThickness="0">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Background" Value="#E74C3C"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#C0392B"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</StackPanel> </StackPanel>
</Grid> </Grid>
</GroupBox> </GroupBox>
<!-- 로그 출력 --> <!-- 로그 출력 -->
<GroupBox Grid.Row="5" Header="📋 실시간 로그" Margin="15,5,15,10" <GroupBox Grid.Row="6" Header="📋 실시간 로그" Margin="15,5,15,10" Height="300"
FontWeight="SemiBold" FontSize="14"> FontWeight="SemiBold" FontSize="14">
<ScrollViewer Margin="5" VerticalScrollBarVisibility="Auto"> <ScrollViewer Margin="5" VerticalScrollBarVisibility="Auto">
<TextBox x:Name="txtLog" <TextBox x:Name="txtLog"
@@ -171,12 +262,14 @@
</GroupBox> </GroupBox>
<!-- 상태바 --> <!-- 상태바 -->
<StatusBar Grid.Row="6" Background="#3B4252" Foreground="White"> <StatusBar Grid.Row="7" Background="#3B4252" Foreground="White">
<StatusBarItem> <StatusBarItem>
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<TextBlock x:Name="txtStatusBar" Text="DWG 정보 추출기 v1.0 - 준비됨"/> <TextBlock x:Name="txtStatusBar" Text="DWG 정보 추출기 v1.0 - 준비됨"/>
<Separator Margin="10,0"/> <Separator Margin="10,0"/>
<TextBlock x:Name="txtFileCount" Text="파일: 0개"/> <TextBlock x:Name="txtFileCount" Text="파일: 0개"/>
<Separator Margin="10,0"/>
<TextBlock x:Name="txtBuildTime" Text="빌드: 로딩중..." FontSize="11" Foreground="LightGray"/>
</StackPanel> </StackPanel>
</StatusBarItem> </StatusBarItem>
<StatusBarItem HorizontalAlignment="Right"> <StatusBarItem HorizontalAlignment="Right">

File diff suppressed because it is too large Load Diff

9
Models/AppSettings.cs Normal file
View File

@@ -0,0 +1,9 @@
namespace DwgExtractorManual.Models
{
public class AppSettings
{
public string? SourceFolderPath { get; set; }
public string? DestinationFolderPath { get; set; }
public string? LastExportType { get; set; }
}
}

261
Models/CsvDataWriter.cs Normal file
View File

@@ -0,0 +1,261 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace DwgExtractorManual.Models
{
/// <summary>
/// Note 데이터를 CSV 파일로 출력하는 클래스
/// Note Box 안의 일반 텍스트와 테이블 텍스트를 분리하여 CSV로 출력
/// </summary>
public class CsvDataWriter
{
/// <summary>
/// Note 박스 안의 일반 텍스트들을 CSV 파일로 저장
/// </summary>
public void WriteNoteBoxTextToCsv(List<NoteEntityInfo> noteEntities, string filePath)
{
if (noteEntities == null || noteEntities.Count == 0)
return;
var csvLines = new List<string>();
// CSV 헤더 추가
csvLines.Add("Type,Layer,Text,X,Y,SortOrder,Path,FileName");
// Note와 NoteContent 데이터 추출 (테이블 제외)
var noteBoxTexts = noteEntities
.Where(ne => ne.Type == "Note" || ne.Type == "NoteContent")
.OrderBy(ne => ne.SortOrder)
.ToList();
foreach (var noteEntity in noteBoxTexts)
{
var csvLine = $"{EscapeCsvField(noteEntity.Type)}," +
$"{EscapeCsvField(noteEntity.Layer)}," +
$"{EscapeCsvField(noteEntity.Text)}," +
$"{noteEntity.X:F3}," +
$"{noteEntity.Y:F3}," +
$"{noteEntity.SortOrder}," +
$"{EscapeCsvField(noteEntity.Path)}," +
$"{EscapeCsvField(noteEntity.FileName)}";
csvLines.Add(csvLine);
}
// UTF-8 BOM 포함하여 파일 저장 (Excel에서 한글 깨짐 방지)
var utf8WithBom = new UTF8Encoding(true);
File.WriteAllLines(filePath, csvLines, utf8WithBom);
}
/// <summary>
/// Note 박스 안의 테이블 데이터들을 별도 CSV 파일로 저장
/// </summary>
public void WriteNoteTablesToCsv(List<NoteEntityInfo> noteEntities, string filePath)
{
if (noteEntities == null || noteEntities.Count == 0)
return;
var allCsvLines = new List<string>();
// 테이블 데이터가 있는 Note들 추출
var notesWithTables = noteEntities
.Where(ne => ne.Type == "Note" && !string.IsNullOrEmpty(ne.TableCsv))
.OrderByDescending(ne => ne.Y) // Y 좌표로 정렬 (위에서 아래로)
.ToList();
foreach (var noteWithTable in notesWithTables)
{
// Note 정보 헤더 추가
allCsvLines.Add($"=== NOTE: {noteWithTable.Text} (at {noteWithTable.X:F1}, {noteWithTable.Y:F1}) ===");
allCsvLines.Add(""); // 빈 줄
// 테이블 CSV 데이터 추가
var tableLines = noteWithTable.TableCsv.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
allCsvLines.AddRange(tableLines);
// Note 간 구분을 위한 빈 줄들
allCsvLines.Add("");
allCsvLines.Add("");
}
// UTF-8 BOM 포함하여 파일 저장
var utf8WithBom = new UTF8Encoding(true);
File.WriteAllLines(filePath, allCsvLines, utf8WithBom);
}
/// <summary>
/// Note 박스와 테이블 데이터를 통합하여 하나의 CSV 파일로 저장
/// </summary>
public void WriteNoteDataToCombinedCsv(List<NoteEntityInfo> noteEntities, string filePath)
{
if (noteEntities == null || noteEntities.Count == 0)
return;
var csvLines = new List<string>();
// CSV 헤더 추가
csvLines.Add("Type,Layer,Text,X,Y,SortOrder,TableData,Path,FileName");
// 모든 Note 관련 데이터를 SortOrder로 정렬
var sortedNoteEntities = noteEntities
.OrderBy(ne => ne.SortOrder)
.ToList();
foreach (var noteEntity in sortedNoteEntities)
{
// 테이블 데이터가 있는 경우 이를 별도 필드로 처리
var tableData = "";
if (noteEntity.Type == "Note" && !string.IsNullOrEmpty(noteEntity.TableCsv))
{
// 테이블 CSV 데이터를 하나의 필드로 압축 (줄바꿈을 |로 대체)
tableData = noteEntity.TableCsv.Replace("\n", "|").Replace("\r", "");
}
var csvLine = $"{EscapeCsvField(noteEntity.Type)}," +
$"{EscapeCsvField(noteEntity.Layer)}," +
$"{EscapeCsvField(noteEntity.Text)}," +
$"{noteEntity.X:F3}," +
$"{noteEntity.Y:F3}," +
$"{noteEntity.SortOrder}," +
$"{EscapeCsvField(tableData)}," +
$"{EscapeCsvField(noteEntity.Path)}," +
$"{EscapeCsvField(noteEntity.FileName)}";
csvLines.Add(csvLine);
}
// UTF-8 BOM 포함하여 파일 저장
var utf8WithBom = new UTF8Encoding(true);
File.WriteAllLines(filePath, csvLines, utf8WithBom);
}
/// <summary>
/// 각 Note별로 개별 CSV 파일 생성 (테이블이 있는 경우)
/// </summary>
public void WriteIndividualNoteTablesCsv(List<NoteEntityInfo> noteEntities, string baseDirectory)
{
if (noteEntities == null || noteEntities.Count == 0)
return;
// 디렉토리가 없으면 생성
if (!Directory.Exists(baseDirectory))
{
Directory.CreateDirectory(baseDirectory);
}
var notesWithTables = noteEntities
.Where(ne => ne.Type == "Note" && !string.IsNullOrEmpty(ne.TableCsv))
.OrderByDescending(ne => ne.Y)
.ToList();
int noteIndex = 1;
foreach (var noteWithTable in notesWithTables)
{
// 파일명 생성 (특수문자 제거)
var safeNoteText = MakeSafeFileName(noteWithTable.Text);
var fileName = $"Note_{noteIndex:D2}_{safeNoteText}.csv";
var fullPath = Path.Combine(baseDirectory, fileName);
// 테이블 CSV 데이터를 파일로 저장
var tableLines = noteWithTable.TableCsv.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
var utf8WithBom = new UTF8Encoding(true);
File.WriteAllLines(fullPath, tableLines, utf8WithBom);
noteIndex++;
}
}
/// <summary>
/// CSV 필드에서 특수문자를 이스케이프 처리
/// </summary>
private string EscapeCsvField(string field)
{
if (string.IsNullOrEmpty(field))
return "";
// 쉼표, 따옴표, 줄바꿈이 있으면 따옴표로 감싸기
if (field.Contains(",") || field.Contains("\"") || field.Contains("\n") || field.Contains("\r"))
{
return "\"" + field.Replace("\"", "\"\"") + "\"";
}
return field;
}
/// <summary>
/// 파일명에 사용할 수 없는 문자들을 제거하여 안전한 파일명 생성
/// </summary>
private string MakeSafeFileName(string fileName)
{
if (string.IsNullOrEmpty(fileName))
return "Unknown";
var invalidChars = Path.GetInvalidFileNameChars();
var safeFileName = fileName;
foreach (var invalidChar in invalidChars)
{
safeFileName = safeFileName.Replace(invalidChar, '_');
}
// 길이 제한 (Windows 파일명 제한 고려)
if (safeFileName.Length > 50)
{
safeFileName = safeFileName.Substring(0, 50);
}
return safeFileName.Trim();
}
/// <summary>
/// Note 데이터 통계 정보를 CSV로 저장
/// </summary>
public void WriteNoteStatisticsToCsv(List<NoteEntityInfo> noteEntities, string filePath)
{
if (noteEntities == null || noteEntities.Count == 0)
return;
var csvLines = new List<string>();
// 통계 헤더
csvLines.Add("Statistic,Count,Details");
// 전체 Note 개수
var totalNotes = noteEntities.Count(ne => ne.Type == "Note");
csvLines.Add($"Total Notes,{totalNotes},");
// 테이블이 있는 Note 개수
var notesWithTables = noteEntities.Count(ne => ne.Type == "Note" && !string.IsNullOrEmpty(ne.TableCsv));
csvLines.Add($"Notes with Tables,{notesWithTables},");
// 일반 텍스트만 있는 Note 개수
var notesWithTextOnly = totalNotes - notesWithTables;
csvLines.Add($"Notes with Text Only,{notesWithTextOnly},");
// 전체 Note 콘텐츠 개수
var totalNoteContents = noteEntities.Count(ne => ne.Type == "NoteContent");
csvLines.Add($"Total Note Contents,{totalNoteContents},");
// 레이어별 분포
csvLines.Add(",,");
csvLines.Add("Layer Distribution,,");
var layerGroups = noteEntities
.GroupBy(ne => ne.Layer)
.OrderByDescending(g => g.Count())
.ToList();
foreach (var layerGroup in layerGroups)
{
csvLines.Add($"Layer: {layerGroup.Key},{layerGroup.Count()},");
}
var utf8WithBom = new UTF8Encoding(true);
File.WriteAllLines(filePath, csvLines, utf8WithBom);
}
}
}

3096
Models/DwgDataExtractor.cs Normal file

File diff suppressed because it is too large Load Diff

639
Models/ExcelDataWriter.cs Normal file
View File

@@ -0,0 +1,639 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Excel = Microsoft.Office.Interop.Excel;
namespace DwgExtractorManual.Models
{
/// <summary>
/// Excel <20><>Ʈ<EFBFBD><C6AE> <20><><EFBFBD><EFBFBD><EFBFBD>͸<EFBFBD> <20><><EFBFBD><EFBFBD> <20>۾<EFBFBD><DBBE><EFBFBD> <20><><EFBFBD><EFBFBD>ϴ<EFBFBD> Ŭ<><C5AC><EFBFBD><EFBFBD>
/// </summary>
internal class ExcelDataWriter
{
private readonly ExcelManager excelManager;
public ExcelDataWriter(ExcelManager excelManager)
{
this.excelManager = excelManager ?? throw new ArgumentNullException(nameof(excelManager));
}
/// <summary>
/// Title Block <20><><EFBFBD><EFBFBD><EFBFBD>͸<EFBFBD> Excel <20><>Ʈ<EFBFBD><C6AE> <20><><EFBFBD>
/// </summary>
public void WriteTitleBlockData(List<TitleBlockRowData> titleBlockRows)
{
if (excelManager.TitleBlockSheet == null || titleBlockRows == null || titleBlockRows.Count == 0)
return;
int currentRow = 2; // <20><><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>
foreach (var row in titleBlockRows)
{
excelManager.TitleBlockSheet.Cells[currentRow, 1] = row.Type;
excelManager.TitleBlockSheet.Cells[currentRow, 2] = row.Name;
excelManager.TitleBlockSheet.Cells[currentRow, 3] = row.Tag;
excelManager.TitleBlockSheet.Cells[currentRow, 4] = row.Prompt;
excelManager.TitleBlockSheet.Cells[currentRow, 5] = row.Value;
excelManager.TitleBlockSheet.Cells[currentRow, 6] = row.Path;
excelManager.TitleBlockSheet.Cells[currentRow, 7] = row.FileName;
currentRow++;
}
}
/// <summary>
/// Text Entity <20><><EFBFBD><EFBFBD><EFBFBD>͸<EFBFBD> Excel <20><>Ʈ<EFBFBD><C6AE> <20><><EFBFBD>
/// </summary>
public void WriteTextEntityData(List<TextEntityRowData> textEntityRows)
{
if (excelManager.TextEntitiesSheet == null || textEntityRows == null || textEntityRows.Count == 0)
return;
int currentRow = 2; // <20><><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>
foreach (var row in textEntityRows)
{
excelManager.TextEntitiesSheet.Cells[currentRow, 1] = row.Type;
excelManager.TextEntitiesSheet.Cells[currentRow, 2] = row.Layer;
excelManager.TextEntitiesSheet.Cells[currentRow, 3] = row.Text;
excelManager.TextEntitiesSheet.Cells[currentRow, 4] = row.Path;
excelManager.TextEntitiesSheet.Cells[currentRow, 5] = row.FileName;
currentRow++;
}
}
/// <summary>
/// Note 엔터티 데이터를 Excel 시트에 쓰기 (테이블 및 셀 병합 포함)
/// </summary>
public void WriteNoteEntityData(List<NoteEntityInfo> noteEntityRows)
{
if (excelManager.NoteEntitiesSheet == null || noteEntityRows == null || noteEntityRows.Count == 0)
return;
int excelRow = 2; // 헤더 다음 행부터 시작
foreach (var note in noteEntityRows)
{
// 기본 Note 정보 쓰기
excelManager.NoteEntitiesSheet.Cells[excelRow, 1] = note.Type;
excelManager.NoteEntitiesSheet.Cells[excelRow, 2] = note.Layer;
excelManager.NoteEntitiesSheet.Cells[excelRow, 3] = note.Text;
excelManager.NoteEntitiesSheet.Cells[excelRow, 4] = note.X;
excelManager.NoteEntitiesSheet.Cells[excelRow, 5] = note.Y;
excelManager.NoteEntitiesSheet.Cells[excelRow, 6] = note.SortOrder;
excelManager.NoteEntitiesSheet.Cells[excelRow, 8] = note.Path;
excelManager.NoteEntitiesSheet.Cells[excelRow, 9] = note.FileName;
int tableRowCount = 0;
if (note.Cells != null && note.Cells.Count > 0)
{
// 테이블 데이터 처리
foreach (var cell in note.Cells)
{
int startRow = excelRow + cell.Row;
int startCol = 7 + cell.Column; // G열부터 시작
int endRow = startRow + cell.RowSpan - 1;
int endCol = startCol + cell.ColumnSpan - 1;
Excel.Range cellRange = excelManager.NoteEntitiesSheet.Range[
excelManager.NoteEntitiesSheet.Cells[startRow, startCol],
excelManager.NoteEntitiesSheet.Cells[endRow, endCol]];
// 병합 먼저 수행
if (cell.RowSpan > 1 || cell.ColumnSpan > 1)
{
cellRange.Merge();
}
// 값 설정 및 서식 지정
cellRange.Value = cell.CellText;
cellRange.VerticalAlignment = Excel.XlVAlign.xlVAlignCenter;
cellRange.HorizontalAlignment = Excel.XlHAlign.xlHAlignCenter;
}
// 이 테이블이 차지하는 총 행 수를 계산
tableRowCount = note.Cells.Max(c => c.Row + c.RowSpan);
}
// 다음 Note를 기록할 위치로 이동
// 테이블이 있으면 테이블 높이만큼, 없으면 한 칸만 이동
excelRow += (tableRowCount > 0) ? tableRowCount : 1;
}
}
/// <summary>
/// <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>͸<EFBFBD> Excel <20><>Ʈ<EFBFBD><C6AE> <20><><EFBFBD>
/// </summary>
public void WriteMappingDataToExcel(Dictionary<string, Dictionary<string, (string, string, string, string)>> mappingData)
{
try
{
if (excelManager.MappingSheet == null || mappingData == null)
return;
int currentRow = 2; // <20><><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>
Debug.WriteLine($"[DEBUG] Writing mapping data to Excel. Total files: {mappingData.Count}");
foreach (var fileEntry in mappingData)
{
string fileName = fileEntry.Key;
var fileMappingData = fileEntry.Value;
Debug.WriteLine($"[DEBUG] Processing file: {fileName}, entries: {fileMappingData.Count}");
foreach (var mapEntry in fileMappingData)
{
string mapKey = mapEntry.Key;
(string aiLabel, string dwgTag, string attValue, string pdfValue) = mapEntry.Value;
if (string.IsNullOrEmpty(fileName) || string.IsNullOrEmpty(mapKey))
{
continue;
}
try
{
// <20><>ġ <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ʈ<EFBFBD><C6AE> <20><><EFBFBD><EFBFBD> <20><20><><EFBFBD>
object[,] rowData = new object[1, 6];
rowData[0, 0] = fileName;
rowData[0, 1] = mapKey;
rowData[0, 2] = aiLabel ?? "";
rowData[0, 3] = dwgTag ?? "";
rowData[0, 4] = attValue ?? "";
rowData[0, 5] = pdfValue ?? "";
Excel.Range range = excelManager.MappingSheet.Range[
excelManager.MappingSheet.Cells[currentRow, 1],
excelManager.MappingSheet.Cells[currentRow, 6]];
range.Value = rowData;
}
catch (System.Exception ex)
{
Debug.WriteLine($"? Error writing row {currentRow}: {ex.Message}");
}
currentRow++;
}
}
Debug.WriteLine($"[DEBUG] Mapping data written to Excel. Total rows: {currentRow - 2}");
}
catch (System.Exception ex)
{
Debug.WriteLine($"? <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Excel <20><><EFBFBD> <20><> <20><><EFBFBD><EFBFBD>: {ex.Message}");
throw;
}
}
/// <summary>
/// Excel <20><><EFBFBD><EFBFBD> <20><>Ʈ<EFBFBD><C6AE><EFBFBD><EFBFBD> FileName<6D><65> AILabel<65><6C> <20><>Ī<EFBFBD>Ǵ<EFBFBD> <20><><EFBFBD><EFBFBD> ã<><C3A3> Pdf_value<75><65> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ʈ
/// </summary>
public bool UpdateExcelRow(string fileName, string aiLabel, string pdfValue)
{
try
{
if (excelManager.MappingSheet == null)
return false;
Excel.Range usedRange = excelManager.MappingSheet.UsedRange;
if (usedRange == null) return false;
int lastRow = usedRange.Rows.Count;
for (int row = 2; row <= lastRow; row++)
{
var cellFileName = ((Excel.Range)excelManager.MappingSheet.Cells[row, 1]).Value?.ToString() ?? "";
var cellAiLabel = ((Excel.Range)excelManager.MappingSheet.Cells[row, 3]).Value?.ToString() ?? "";
if (string.Equals(cellFileName.Trim(), fileName.Trim(), StringComparison.OrdinalIgnoreCase) &&
string.Equals(cellAiLabel.Trim(), aiLabel.Trim(), StringComparison.OrdinalIgnoreCase))
{
excelManager.MappingSheet.Cells[row, 6] = pdfValue;
return true;
}
}
return false;
}
catch (System.Exception ex)
{
Debug.WriteLine($"? Excel <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ʈ <20><> <20><><EFBFBD><EFBFBD>: {ex.Message}");
return false;
}
}
/// <summary>
/// DWG <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><>ũ<EFBFBD><C5A9><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>ϰ<EFBFBD> <20><><EFBFBD><EFBFBD> (PDF <20>÷<EFBFBD> <20><><EFBFBD><EFBFBD>)
/// </summary>
public void SaveDwgOnlyMappingWorkbook(Dictionary<string, Dictionary<string, (string, string, string, string)>> mappingData, string resultFolderPath)
{
try
{
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
string savePath = System.IO.Path.Combine(resultFolderPath, $"{timestamp}_DwgOnly_Mapping.xlsx");
Debug.WriteLine($"[DEBUG] DWG <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><>ũ<EFBFBD><C5A9> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>: {savePath}");
var dwgOnlyWorkbook = excelManager.CreateNewWorkbook();
var dwgOnlyWorksheet = (Excel.Worksheet)dwgOnlyWorkbook.Worksheets[1];
dwgOnlyWorksheet.Name = "DWG Mapping Data";
// <20><><EFBFBD> <20><><EFBFBD><EFBFBD> (PDF Value <20>÷<EFBFBD> <20><><EFBFBD><EFBFBD>)
dwgOnlyWorksheet.Cells[1, 1] = "<22><><EFBFBD>ϸ<EFBFBD>";
dwgOnlyWorksheet.Cells[1, 2] = "Map Key";
dwgOnlyWorksheet.Cells[1, 3] = "AI Label";
dwgOnlyWorksheet.Cells[1, 4] = "DWG Tag";
dwgOnlyWorksheet.Cells[1, 5] = "DWG Value";
// <20><><EFBFBD> <20><>Ÿ<EFBFBD><C5B8> <20><><EFBFBD><EFBFBD>
var headerRange = dwgOnlyWorksheet.Range["A1:E1"];
headerRange.Font.Bold = true;
headerRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightGray);
headerRange.Borders.LineStyle = Excel.XlLineStyle.xlContinuous;
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20>Է<EFBFBD>
int totalRows = mappingData.Sum(f => f.Value.Count);
if (totalRows > 0)
{
object[,] data = new object[totalRows, 5];
int row = 0;
foreach (var fileEntry in mappingData)
{
string fileName = fileEntry.Key;
foreach (var mapEntry in fileEntry.Value)
{
string mapKey = mapEntry.Key;
var (aiLabel, dwgTag, dwgValue, pdfValue) = mapEntry.Value;
data[row, 0] = fileName;
data[row, 1] = mapKey;
data[row, 2] = aiLabel;
data[row, 3] = dwgTag;
data[row, 4] = dwgValue;
row++;
}
}
Excel.Range dataRange = dwgOnlyWorksheet.Range[
dwgOnlyWorksheet.Cells[2, 1],
dwgOnlyWorksheet.Cells[totalRows + 1, 5]];
dataRange.Value = data;
}
dwgOnlyWorksheet.Columns.AutoFit();
excelManager.SaveWorkbookAs(dwgOnlyWorkbook, savePath);
Debug.WriteLine($"? DWG <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><>ũ<EFBFBD><C5A9> <20><><EFBFBD><EFBFBD> <20>Ϸ<EFBFBD>: {System.IO.Path.GetFileName(savePath)}");
dwgOnlyWorkbook.Close(false);
System.GC.Collect();
System.GC.WaitForPendingFinalizers();
}
catch (System.Exception ex)
{
Debug.WriteLine($"? DWG <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><>ũ<EFBFBD><C5A9> <20><><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD>: {ex.Message}");
throw;
}
}
/// <summary>
/// Height <20><><EFBFBD>ĵ<EFBFBD> Excel <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>
/// </summary>
public void WriteHeightSortedData(List<TextEntityInfo> textEntities, Excel.Worksheet worksheet, string fileName)
{
// <20><><EFBFBD> <20><><EFBFBD><EFBFBD>
worksheet.Cells[1, 1] = "Height";
worksheet.Cells[1, 2] = "Type";
worksheet.Cells[1, 3] = "Layer";
worksheet.Cells[1, 4] = "Tag";
worksheet.Cells[1, 5] = "FileName";
worksheet.Cells[1, 6] = "Text";
// <20><><EFBFBD> <20><>Ÿ<EFBFBD><C5B8> <20><><EFBFBD><EFBFBD>
var headerRange = worksheet.Range["A1:F1"];
headerRange.Font.Bold = true;
headerRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightBlue);
// <20><><EFBFBD><EFBFBD><EFBFBD>ʹ<EFBFBD> ExtractTextEntitiesWithHeight<68><74><EFBFBD><EFBFBD> <20>̹<EFBFBD> <20><><EFBFBD>ĵǾ<C4B5><C7BE><EFBFBD><EFBFBD>Ƿ<EFBFBD> <20>ٽ<EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20>ʽ<EFBFBD><CABD>ϴ<EFBFBD>.
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20>Է<EFBFBD>
int row = 2;
foreach (var entity in textEntities) // sortedEntities<65><73> textEntities<65><73> <20><><EFBFBD><EFBFBD>
{
worksheet.Cells[row, 1] = entity.Height;
worksheet.Cells[row, 2] = entity.Type;
worksheet.Cells[row, 3] = entity.Layer;
worksheet.Cells[row, 4] = entity.Tag;
worksheet.Cells[row, 5] = fileName;
worksheet.Cells[row, 6] = entity.Text;
row++;
}
worksheet.Columns.AutoFit();
}
/// <summary>
/// Note 엔티티들을 Excel 워크시트에 기록합니다 (기존 데이터 아래에 추가).
/// CellBoundary 데이터를 사용하여 병합된 셀의 텍스트를 적절히 처리합니다.
/// </summary>
public void WriteNoteEntities(List<NoteEntityInfo> noteEntities, Excel.Worksheet worksheet, string fileName)
{
if (noteEntities == null || noteEntities.Count == 0)
{
Debug.WriteLine("[DEBUG] Note 엔티티가 없습니다.");
return;
}
try
{
// 현재 워크시트의 마지막 사용된 행 찾기
Excel.Range usedRange = null;
int lastRow = 1;
try
{
usedRange = worksheet.UsedRange;
lastRow = usedRange?.Rows.Count ?? 1;
}
catch (System.Exception ex)
{
Debug.WriteLine($"[DEBUG] UsedRange 접근 오류, 기본값 사용: {ex.Message}");
lastRow = 1;
}
int startRow = lastRow + 2; // 한 줄 띄우고 시작
Debug.WriteLine($"[DEBUG] Note 데이터 기록 시작: {startRow}행부터 {noteEntities.Count}개 항목");
// Note 섹션 헤더 추가 (표 컬럼 포함)
try
{
worksheet.Cells[startRow - 1, 1] = "=== Notes (with Cell Boundary Tables) ===";
worksheet.Cells[startRow - 1, 2] = "";
worksheet.Cells[startRow - 1, 3] = "";
worksheet.Cells[startRow - 1, 4] = "";
worksheet.Cells[startRow - 1, 5] = "";
worksheet.Cells[startRow - 1, 6] = "";
// 표 컬럼 헤더 추가 (G열부터 최대 20개 컬럼)
for (int col = 7; col <= 26; col++) // G~Z열 (20개 컬럼)
{
worksheet.Cells[startRow - 1, col] = $"Table Col {col - 6}";
var tableHeaderCell = (Excel.Range)worksheet.Cells[startRow - 1, col];
tableHeaderCell.Font.Bold = true;
tableHeaderCell.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightBlue);
tableHeaderCell.Font.Size = 9; // 작은 폰트로 설정
}
// 헤더 스타일 적용 (개별 셀로 처리)
var headerCell = (Excel.Range)worksheet.Cells[startRow - 1, 1];
headerCell.Font.Bold = true;
headerCell.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightYellow);
}
catch (System.Exception ex)
{
Debug.WriteLine($"[DEBUG] Note 헤더 작성 오류: {ex.Message}");
}
// Note 데이터 입력 (CellBoundary 데이터 사용)
int row = startRow;
try
{
foreach (var noteEntity in noteEntities)
{
// 기본 Note 정보 입력 (F열까지)
worksheet.Cells[row, 1] = 0; // Height는 0으로 설정
worksheet.Cells[row, 2] = noteEntity.Type ?? "";
worksheet.Cells[row, 3] = noteEntity.Layer ?? "";
worksheet.Cells[row, 4] = ""; // Tag는 빈 값
worksheet.Cells[row, 5] = fileName ?? "";
worksheet.Cells[row, 6] = noteEntity.Text ?? ""; // 일반 텍스트만 (표 데이터 제외)
int currentRow = row; // 현재 처리 중인 행 번호
// CellBoundary 데이터가 있으면 G열부터 테이블 데이터 처리
if (noteEntity.CellBoundaries != null && noteEntity.CellBoundaries.Count > 0)
{
Debug.WriteLine($"[DEBUG] CellBoundary 데이터 처리: Row {row}, 셀 수={noteEntity.CellBoundaries.Count}");
// CellBoundary의 각 셀을 해당 위치에 직접 배치
int maxTableRow = 0;
foreach (var cellBoundary in noteEntity.CellBoundaries)
{
var (sRow, sCol, eRow, eCol) = ParseCellRangeFromLabel(cellBoundary.Label);
Debug.WriteLine($"[DEBUG] CellBoundary 처리: {cellBoundary.Label} → Range=R{sRow}C{sCol}:R{eRow}C{eCol}, Text='{cellBoundary.CellText}'");
if (sRow > 0 && sCol > 0 && eRow > 0 && eCol > 0)
{
// 병합된 영역의 셀 개수 계산: (eRow - sRow) × (eCol - sCol)
int rowCount = eRow - sRow; // 행 개수 (bottomRight - topLeft)
int colCount = eCol - sCol; // 열 개수 (bottomRight - topLeft)
Debug.WriteLine($"[DEBUG] 병합 영역 크기: {rowCount+1}행 × {colCount+1}열");
// 병합된 영역의 모든 셀에 텍스트 복사 (topLeft부터 bottomRight-1까지)
for (int r = sRow; r < eRow; r++) // < eRow (bottomRight 제외)
{
for (int c = sCol; c < eCol; c++) // < eCol (bottomRight 제외)
{
// Excel에서 테이블 위치 계산:
// R1 → Note의 현재 행 (currentRow)
// R2 → Note의 다음 행 (currentRow + 1)
// C1 → G열(7), C2 → H열(8)
int excelRow = currentRow + (r - 1); // R1=currentRow, R2=currentRow+1, ...
int excelCol = 7 + (c - 1); // C1=G열(7), C2=H열(8), ...
// Excel 범위 체크 (최대 20개 컬럼까지)
if (excelCol <= 26) // Z열까지
{
// CellText가 비어있어도 일단 배치해보기 (디버그용)
var cellValue = string.IsNullOrEmpty(cellBoundary.CellText) ? "[빈셀]" : cellBoundary.CellText;
// 텍스트 형식으로 설정하여 "0:0" 같은 값이 시간으로 포맷되지 않도록 함
var cell = (Excel.Range)worksheet.Cells[excelRow, excelCol];
cell.NumberFormat = "@"; // 텍스트 형식
cell.Value = cellValue;
Debug.WriteLine($"[DEBUG] ✅ 셀 복사: R{r}C{c} → Excel[{excelRow},{excelCol}] = '{cellValue}'");
}
else
{
Debug.WriteLine($"[DEBUG] ❌ Excel 컬럼 범위 초과: {excelCol} > 26");
}
// 테이블이 차지하는 최대 행 수 추적
maxTableRow = Math.Max(maxTableRow, r);
}
}
}
else
{
Debug.WriteLine($"[DEBUG] ❌ 잘못된 Range: {cellBoundary.Label} → R{sRow}C{sCol}:R{eRow}C{eCol}");
}
}
// 테이블이 여러 행을 차지하는 경우 currentRow 업데이트
if (maxTableRow > 1)
{
currentRow += (maxTableRow - 1);
Debug.WriteLine($"[DEBUG] 테이블 행 수만큼 currentRow 업데이트: +{maxTableRow - 1} → {currentRow}");
}
}
else if (!string.IsNullOrEmpty(noteEntity.TableCsv))
{
// 기존 TableCsv 방식 (백업용)
var tableRows = noteEntity.TableCsv.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
Debug.WriteLine($"[DEBUG] 기존 표 데이터 처리: Row {row}, 표 행 수={tableRows.Length}");
for (int tableRowIndex = 0; tableRowIndex < tableRows.Length; tableRowIndex++)
{
var tableCells = tableRows[tableRowIndex].Split(',');
// 각 셀을 G열부터 배치 (최대 15개 컬럼까지)
for (int cellIndex = 0; cellIndex < Math.Min(tableCells.Length, 15); cellIndex++)
{
var cellValue = tableCells[cellIndex].Trim().Trim('"'); // 따옴표 제거
// 텍스트 형식으로 설정하여 "0:0" 같은 값이 시간으로 포맷되지 않도록 함
var cell = (Excel.Range)worksheet.Cells[currentRow, 7 + cellIndex];
cell.NumberFormat = "@"; // 텍스트 형식
cell.Value = cellValue; // G열(7)부터 시작
}
// 표의 첫 번째 행이 아니면 새로운 Excel 행 추가
if (tableRowIndex > 0)
{
currentRow++;
// 새로운 행에는 기본 Note 정보 복사 (Type, Layer 등)
worksheet.Cells[currentRow, 1] = 0;
worksheet.Cells[currentRow, 2] = noteEntity.Type ?? "";
worksheet.Cells[currentRow, 3] = noteEntity.Layer ?? "";
worksheet.Cells[currentRow, 4] = "";
worksheet.Cells[currentRow, 5] = fileName ?? "";
worksheet.Cells[currentRow, 6] = "(continued)"; // 연속 표시
}
Debug.WriteLine($"[DEBUG] 표 행 {tableRowIndex + 1}/{tableRows.Length}: Excel Row {currentRow}, 셀 수={tableCells.Length}");
}
}
// "NOTE" 타입인 경우 행 배경색 변경 (표 영역 포함)
if (noteEntity.Type == "Note")
{
Excel.Range noteRowRange = worksheet.Range[worksheet.Cells[row, 1], worksheet.Cells[currentRow, 26]]; // Z열까지
noteRowRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightYellow);
noteRowRange.Font.Bold = true;
}
Debug.WriteLine($"[DEBUG] Excel 기록: Row {row}~{currentRow}, Order {noteEntity.SortOrder}, Type {noteEntity.Type}, Pos({noteEntity.X:F1},{noteEntity.Y:F1}), Text: '{noteEntity.Text}', HasCellBoundaries: {noteEntity.CellBoundaries?.Count > 0} (Count: {noteEntity.CellBoundaries?.Count ?? 0}), HasTableCsv: {!string.IsNullOrEmpty(noteEntity.TableCsv)}");
// CellBoundaries 상세 디버그
if (noteEntity.CellBoundaries != null && noteEntity.CellBoundaries.Count > 0)
{
Debug.WriteLine($"[DEBUG] CellBoundaries 상세:");
foreach (var cb in noteEntity.CellBoundaries.Take(5)) // 처음 5개만 출력
{
Debug.WriteLine($"[DEBUG] {cb.Label}: '{cb.CellText}'");
}
}
// 다음 Note는 현재 행의 다음 행부터 시작
row = currentRow + 1;
}
Debug.WriteLine($"[DEBUG] Note 데이터 기록 완료: {row - startRow}개 항목");
}
catch (System.Exception ex)
{
Debug.WriteLine($"[DEBUG] Note 데이터 입력 오류: {ex.Message}");
Debug.WriteLine($"[DEBUG] 처리된 행: {row - startRow}개");
}
// AutoFit 시도 (오류 발생시 무시)
try
{
worksheet.Columns.AutoFit();
}
catch (System.Exception ex)
{
Debug.WriteLine($"[DEBUG] AutoFit 오류 (무시됨): {ex.Message}");
}
}
catch (System.Exception ex)
{
Debug.WriteLine($"❌ WriteNoteEntities 전체 오류: {ex.Message}");
Debug.WriteLine($" 스택 트레이스: {ex.StackTrace}");
throw; // 상위로 예외 전파
}
}
/// <summary>
/// 라벨에서 셀 범위 정보를 파싱합니다.
/// 예: "R1C2" → (1, 2, 1, 2) 또는 "R2C2→R3C4" → (2, 2, 3, 4)
/// </summary>
private (int sRow, int sCol, int eRow, int eCol) ParseCellRangeFromLabel(string label)
{
try
{
if (string.IsNullOrEmpty(label))
return (0, 0, 0, 0);
if (label.Contains("→"))
{
// "R2C2→R3C4" 형태 - 범위 파싱
var parts = label.Split('→');
if (parts.Length == 2)
{
var (sRow, sCol) = ParseSingleCell(parts[0]);
var (eRow, eCol) = ParseSingleCell(parts[1]);
return (sRow, sCol, eRow, eCol);
}
}
else
{
// "R1C2" 형태 - 단일 셀
var (row, col) = ParseSingleCell(label);
return (row, col, row, col);
}
return (0, 0, 0, 0);
}
catch
{
return (0, 0, 0, 0);
}
}
/// <summary>
/// 단일 셀 위치를 파싱합니다. (예: "R2C3" → (2, 3))
/// </summary>
private (int row, int col) ParseSingleCell(string cellStr)
{
try
{
if (string.IsNullOrEmpty(cellStr))
return (0, 0);
var rIndex = cellStr.IndexOf('R');
var cIndex = cellStr.IndexOf('C');
if (rIndex >= 0 && cIndex > rIndex)
{
var rowStr = cellStr.Substring(rIndex + 1, cIndex - rIndex - 1);
var colStr = cellStr.Substring(cIndex + 1);
if (int.TryParse(rowStr, out int row) && int.TryParse(colStr, out int col))
{
return (row, col);
}
}
return (0, 0);
}
catch
{
return (0, 0);
}
}
}
}

338
Models/ExcelManager.cs Normal file
View File

@@ -0,0 +1,338 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using Excel = Microsoft.Office.Interop.Excel;
namespace DwgExtractorManual.Models
{
/// <summary>
/// Excel COM <20><>ü <20><><EFBFBD><EFBFBD> <20><> <20><20>۾<EFBFBD><DBBE><EFBFBD> <20><><EFBFBD><EFBFBD>ϴ<EFBFBD> Ŭ<><C5AC><EFBFBD><EFBFBD>
/// </summary>
internal class ExcelManager : IDisposable
{
public Excel.Application? ExcelApplication { get; private set; }
public Excel.Workbook? TitleBlockWorkbook { get; private set; }
public Excel.Workbook? MappingWorkbook { get; private set; }
public Excel.Worksheet? TitleBlockSheet { get; private set; }
public Excel.Worksheet? TextEntitiesSheet { get; private set; }
public Excel.Worksheet? NoteEntitiesSheet { get; private set; }
public Excel.Worksheet? MappingSheet { get; private set; }
/// <summary>
/// Excel <20><><EFBFBD>ø<EFBFBD><C3B8><EFBFBD><EFBFBD>̼<EFBFBD> <20><> <20><>ũ<EFBFBD><C5A9>Ʈ <20>ʱ<EFBFBD>ȭ
/// </summary>
public void InitializeExcel()
{
try
{
var excelApp = new Excel.Application();
ExcelApplication = excelApp;
ExcelApplication.Visible = false; // WPF<50><46><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> ó<><C3B3>
Excel.Workbook workbook = excelApp.Workbooks.Add();
TitleBlockWorkbook = workbook;
// Title Block Sheet <20><><EFBFBD><EFBFBD> (<28>⺻ Sheet1)
TitleBlockSheet = (Excel.Worksheet)workbook.Sheets[1];
TitleBlockSheet.Name = "Title Block";
SetupTitleBlockHeaders();
// Text Entities Sheet <20>߰<EFBFBD>
TextEntitiesSheet = (Excel.Worksheet)workbook.Sheets.Add();
TextEntitiesSheet.Name = "Text Entities";
SetupTextEntitiesHeaders();
// Note Entities Sheet <20>߰<EFBFBD>
NoteEntitiesSheet = (Excel.Worksheet)workbook.Sheets.Add();
NoteEntitiesSheet.Name = "Note Entities";
SetupNoteEntitiesHeaders();
// <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>Ϳ<EFBFBD> <20><>ũ<EFBFBD><C5A9> <20><> <20><>Ʈ <20><><EFBFBD><EFBFBD>
MappingWorkbook = excelApp.Workbooks.Add();
MappingSheet = (Excel.Worksheet)MappingWorkbook.Sheets[1];
MappingSheet.Name = "Mapping Data";
SetupMappingHeaders();
}
catch (System.Exception ex)
{
Debug.WriteLine($"Excel <20>ʱ<EFBFBD>ȭ <20><> <20><><EFBFBD><EFBFBD> <20>߻<EFBFBD>: {ex.Message}");
ReleaseExcelObjects();
throw;
}
}
/// <summary>
/// <20><><EFBFBD><EFBFBD> Excel <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><>Ʈ<EFBFBD><C6AE> <20><><EFBFBD><EFBFBD>
/// </summary>
public bool OpenExistingFile(string excelFilePath)
{
try
{
if (!File.Exists(excelFilePath))
{
Debug.WriteLine($"? Excel <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20>ʽ<EFBFBD><CABD>ϴ<EFBFBD>: {excelFilePath}");
return false;
}
if (ExcelApplication == null)
{
ExcelApplication = new Excel.Application();
ExcelApplication.Visible = false;
}
MappingWorkbook = ExcelApplication.Workbooks.Open(excelFilePath);
MappingSheet = (Excel.Worksheet)MappingWorkbook.Sheets["Mapping Data"];
if (MappingSheet == null)
{
Debug.WriteLine("? 'Mapping Data' <20><>Ʈ<EFBFBD><C6AE> ã<><C3A3> <20><> <20><><EFBFBD><EFBFBD><EFBFBD>ϴ<EFBFBD>.");
return false;
}
return true;
}
catch (System.Exception ex)
{
Debug.WriteLine($"? Excel <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD>: {ex.Message}");
return false;
}
}
/// <summary>
/// <20><><EFBFBD>ο<EFBFBD> <20><>ũ<EFBFBD><C5A9> <20><><EFBFBD><EFBFBD> (Height <20><><EFBFBD>Ŀ<EFBFBD>)
/// </summary>
public Excel.Workbook CreateNewWorkbook()
{
if (ExcelApplication == null)
{
ExcelApplication = new Excel.Application();
ExcelApplication.Visible = false;
}
return ExcelApplication.Workbooks.Add();
}
// Title Block <20><>Ʈ <20><><EFBFBD> <20><><EFBFBD><EFBFBD>
private void SetupTitleBlockHeaders()
{
if (TitleBlockSheet == null) return;
TitleBlockSheet.Cells[1, 1] = "Type"; // <20><>: AttributeReference, AttributeDefinition
TitleBlockSheet.Cells[1, 2] = "Name"; // BlockReference <20≯<EFBFBD> <20>Ǵ<EFBFBD> BlockDefinition <20≯<EFBFBD>
TitleBlockSheet.Cells[1, 3] = "Tag"; // Attribute Tag
TitleBlockSheet.Cells[1, 4] = "Prompt"; // Attribute Prompt
TitleBlockSheet.Cells[1, 5] = "Value"; // Attribute <20><> (TextString)
TitleBlockSheet.Cells[1, 6] = "Path"; // <20><><EFBFBD><EFBFBD> DWG <20><><EFBFBD><EFBFBD> <20><>ü <20><><EFBFBD>
TitleBlockSheet.Cells[1, 7] = "FileName"; // <20><><EFBFBD><EFBFBD> DWG <20><><EFBFBD><EFBFBD> <20≯<EFBFBD><CCB8><EFBFBD>
// <20><><EFBFBD> <20><> <20><>Ÿ<EFBFBD><C5B8>
Excel.Range headerRange = TitleBlockSheet.Range["A1:G1"];
headerRange.Font.Bold = true;
headerRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightBlue);
}
// Text Entities <20><>Ʈ <20><><EFBFBD> <20><><EFBFBD><EFBFBD>
private void SetupTextEntitiesHeaders()
{
if (TextEntitiesSheet == null) return;
TextEntitiesSheet.Cells[1, 1] = "Type"; // DBText, MText
TextEntitiesSheet.Cells[1, 2] = "Layer"; // Layer <20≯<EFBFBD>
TextEntitiesSheet.Cells[1, 3] = "Text"; // <20><><EFBFBD><EFBFBD> <20>ؽ<EFBFBD>Ʈ <20><><EFBFBD><EFBFBD>
TextEntitiesSheet.Cells[1, 4] = "Path"; // <20><><EFBFBD><EFBFBD> DWG <20><><EFBFBD><EFBFBD> <20><>ü <20><><EFBFBD>
TextEntitiesSheet.Cells[1, 5] = "FileName"; // <20><><EFBFBD><EFBFBD> DWG <20><><EFBFBD><EFBFBD> <20≯<EFBFBD><CCB8><EFBFBD>
// <20><><EFBFBD> <20><> <20><>Ÿ<EFBFBD><C5B8>
Excel.Range headerRange = TextEntitiesSheet.Range["A1:E1"];
headerRange.Font.Bold = true;
headerRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightGreen);
}
// <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><>Ʈ <20><><EFBFBD> <20><><EFBFBD><EFBFBD>
private void SetupMappingHeaders()
{
if (MappingSheet == null) return;
MappingSheet.Cells[1, 1] = "FileName"; // <20><><EFBFBD><EFBFBD> <20≯<EFBFBD>
MappingSheet.Cells[1, 2] = "MapKey"; // <20><><EFBFBD><EFBFBD> Ű
MappingSheet.Cells[1, 3] = "AILabel"; // AI <20><>
MappingSheet.Cells[1, 4] = "DwgTag"; // DWG Tag
MappingSheet.Cells[1, 5] = "Att_value"; // DWG <20><>
MappingSheet.Cells[1, 6] = "Pdf_value"; // PDF <20><> (<28><><EFBFBD><EFBFBD><EFBFBD> <20><> <20><>)
// <20><><EFBFBD> <20><> <20><>Ÿ<EFBFBD><C5B8>
Excel.Range headerRange = MappingSheet.Range["A1:F1"];
headerRange.Font.Bold = true;
headerRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightYellow);
}
// Note Entities <20><>Ʈ <20><><EFBFBD> <20><><EFBFBD><EFBFBD>
private void SetupNoteEntitiesHeaders()
{
if (NoteEntitiesSheet == null) return;
NoteEntitiesSheet.Cells[1, 1] = "Type"; // Note, NoteContent
NoteEntitiesSheet.Cells[1, 2] = "Layer"; // Layer <20≯<EFBFBD>
NoteEntitiesSheet.Cells[1, 3] = "Text"; // <20><><EFBFBD><EFBFBD> <20>ؽ<EFBFBD>Ʈ <20><><EFBFBD><EFBFBD>
NoteEntitiesSheet.Cells[1, 4] = "X"; // X <20><>ǥ
NoteEntitiesSheet.Cells[1, 5] = "Y"; // Y <20><>ǥ
NoteEntitiesSheet.Cells[1, 6] = "SortOrder"; // <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>
NoteEntitiesSheet.Cells[1, 7] = "TableCsv"; // <20><><EFBFBD>̺<EFBFBD> CSV <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
NoteEntitiesSheet.Cells[1, 8] = "Path"; // <20><><EFBFBD><EFBFBD> DWG <20><><EFBFBD><EFBFBD> <20><>ü <20><><EFBFBD>
NoteEntitiesSheet.Cells[1, 9] = "FileName"; // <20><><EFBFBD><EFBFBD> DWG <20><><EFBFBD><EFBFBD> <20≯<EFBFBD><CCB8><EFBFBD>
// <20><><EFBFBD> <20><> <20><>Ÿ<EFBFBD><C5B8>
Excel.Range headerRange = NoteEntitiesSheet.Range["A1:I1"];
headerRange.Font.Bold = true;
headerRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightCoral);
}
/// <summary>
/// <20><>ũ<EFBFBD><C5A9> <20><><EFBFBD><EFBFBD>
/// </summary>
public bool SaveWorkbook(Excel.Workbook? workbook = null)
{
try
{
if (workbook != null)
{
workbook.Save();
return true;
}
if (MappingWorkbook != null)
{
MappingWorkbook.Save();
return true;
}
if (TitleBlockWorkbook != null)
{
TitleBlockWorkbook.Save();
return true;
}
return false;
}
catch (System.Exception ex)
{
Debug.WriteLine($"? Excel <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD>: {ex.Message}");
return false;
}
}
/// <summary>
/// <20><>ũ<EFBFBD><C5A9><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><>ο<EFBFBD> <20><><EFBFBD><EFBFBD>
/// </summary>
public void SaveWorkbookAs(Excel.Workbook? workbook, string savePath)
{
if (workbook == null) return;
string? directory = Path.GetDirectoryName(savePath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
workbook.SaveAs(savePath,
FileFormat: Excel.XlFileFormat.xlOpenXMLWorkbook,
AccessMode: Excel.XlSaveAsAccessMode.xlNoChange);
}
/// <summary>
/// Excel <20><>Ʈ<EFBFBD><C6AE><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><> <20>ִ<EFBFBD> <20><>ȿ<EFBFBD><C8BF> <20≯<EFBFBD><CCB8><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>մϴ<D5B4>.
/// </summary>
public string GetValidSheetName(string originalName)
{
if (string.IsNullOrEmpty(originalName))
return "Sheet";
// Excel <20><>Ʈ<EFBFBD><C6AE><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20>ʴ<EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>
string validName = originalName;
char[] invalidChars = { '\\', '/', '?', '*', '[', ']', ':' };
foreach (char c in invalidChars)
{
validName = validName.Replace(c, '_');
}
// 31<33>ڷ<EFBFBD> <20><><EFBFBD><EFBFBD> (Excel <20><>Ʈ<EFBFBD><C6AE> <20>ִ<EFBFBD> <20><><EFBFBD><EFBFBD>)
if (validName.Length > 31)
{
validName = validName.Substring(0, 31);
}
return validName;
}
public void CloseWorkbooks()
{
if (TitleBlockWorkbook != null)
{
try { TitleBlockWorkbook.Close(false); }
catch { }
}
if (MappingWorkbook != null)
{
try { MappingWorkbook.Close(false); }
catch { }
}
if (ExcelApplication != null)
{
try { ExcelApplication.Quit(); }
catch { }
}
}
private void ReleaseExcelObjects()
{
ReleaseComObject(TitleBlockSheet);
ReleaseComObject(TextEntitiesSheet);
ReleaseComObject(NoteEntitiesSheet);
ReleaseComObject(MappingSheet);
ReleaseComObject(TitleBlockWorkbook);
ReleaseComObject(MappingWorkbook);
ReleaseComObject(ExcelApplication);
TitleBlockSheet = null;
TextEntitiesSheet = null;
NoteEntitiesSheet = null;
MappingSheet = null;
TitleBlockWorkbook = null;
MappingWorkbook = null;
ExcelApplication = null;
}
private void ReleaseComObject(object? obj)
{
try
{
if (obj != null && Marshal.IsComObject(obj))
{
Marshal.ReleaseComObject(obj);
}
}
catch (System.Exception)
{
// <20><><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD> <20>߻<EFBFBD> <20><> <20><><EFBFBD><EFBFBD>
}
}
public void Dispose()
{
try
{
Debug.WriteLine("[DEBUG] ExcelManager Dispose <20><><EFBFBD><EFBFBD>");
CloseWorkbooks();
ReleaseExcelObjects();
GC.Collect();
GC.WaitForPendingFinalizers();
Debug.WriteLine("[DEBUG] ExcelManager Dispose <20>Ϸ<EFBFBD>");
}
catch (System.Exception ex)
{
Debug.WriteLine($"[DEBUG] ExcelManager Dispose <20><> <20><><EFBFBD><EFBFBD>: {ex.Message}");
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,267 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Teigha.Geometry;
namespace DwgExtractorManual.Models
{
/// <summary>
/// 교차점 생성 및 셀 추출 로직을 테스트하고 디버깅하는 클래스
/// </summary>
public class IntersectionTestDebugger
{
/// <summary>
/// 간단한 테스트 테이블을 만들어서 교차점과 셀 생성을 테스트합니다.
/// </summary>
public static void RunIntersectionTest()
{
Debug.WriteLine("=== 교차점 및 셀 생성 테스트 시작 ===");
try
{
// 테스트용 테이블 생성 (3x4 그리드)
var testSegments = CreateTestTable();
Debug.WriteLine($"테스트 선분 개수: {testSegments.Count}");
// DwgDataExtractor 인스턴스 생성 (실제 코드와 동일)
var mappingData = new MappingTableData();
var fieldMapper = new FieldMapper(mappingData);
var extractor = new DwgDataExtractor(fieldMapper);
// 교차점 찾기 테스트
var intersections = FindTestIntersections(testSegments, extractor);
Debug.WriteLine($"발견된 교차점 개수: {intersections.Count}");
// 각 교차점의 DirectionBits 출력
for (int i = 0; i < intersections.Count; i++)
{
var intersection = intersections[i];
Debug.WriteLine($"교차점 {i}: ({intersection.Position.X:F1}, {intersection.Position.Y:F1}) - DirectionBits: {intersection.DirectionBits} - R{intersection.Row}C{intersection.Column}");
// topLeft/bottomRight 검증
bool isTopLeft = extractor.IsValidTopLeft(intersection.DirectionBits);
bool isBottomRight = extractor.IsValidBottomRight(intersection.DirectionBits);
Debug.WriteLine($" IsTopLeft: {isTopLeft}, IsBottomRight: {isBottomRight}");
}
Debug.WriteLine("=== 테스트 완료 ===");
}
catch (Exception ex)
{
Debug.WriteLine($"테스트 중 오류 발생: {ex.Message}");
Debug.WriteLine(ex.StackTrace);
}
}
/// <summary>
/// 테스트용 3x4 테이블 선분들을 생성합니다.
/// </summary>
private static List<(Point3d start, Point3d end, bool isHorizontal)> CreateTestTable()
{
var segments = new List<(Point3d start, Point3d end, bool isHorizontal)>();
// 수평선들 (4개 - 0, 10, 20, 30 Y좌표)
for (int i = 0; i <= 3; i++)
{
double y = i * 10.0;
segments.Add((new Point3d(0, y, 0), new Point3d(40, y, 0), true));
}
// 수직선들 (5개 - 0, 10, 20, 30, 40 X좌표)
for (int i = 0; i <= 4; i++)
{
double x = i * 10.0;
segments.Add((new Point3d(x, 0, 0), new Point3d(x, 30, 0), false));
}
Debug.WriteLine($"생성된 테스트 테이블: 수평선 4개, 수직선 5개");
return segments;
}
/// <summary>
/// 테스트 선분들로부터 교차점을 찾습니다.
/// </summary>
private static List<IntersectionPoint> FindTestIntersections(List<(Point3d start, Point3d end, bool isHorizontal)> segments, DwgDataExtractor extractor)
{
var intersections = new List<IntersectionPoint>();
double tolerance = 0.1;
var horizontalSegments = segments.Where(s => s.isHorizontal).ToList();
var verticalSegments = segments.Where(s => !s.isHorizontal).ToList();
foreach (var hSeg in horizontalSegments)
{
foreach (var vSeg in verticalSegments)
{
// 교차점 계산
double intersectX = vSeg.start.X;
double intersectY = hSeg.start.Y;
var intersectPoint = new Point3d(intersectX, intersectY, 0);
// 교차점이 두 선분의 범위 내에 있는지 확인
bool onHorizontal = intersectX >= Math.Min(hSeg.start.X, hSeg.end.X) - tolerance &&
intersectX <= Math.Max(hSeg.start.X, hSeg.end.X) + tolerance;
bool onVertical = intersectY >= Math.Min(vSeg.start.Y, vSeg.end.Y) - tolerance &&
intersectY <= Math.Max(vSeg.start.Y, vSeg.end.Y) + tolerance;
if (onHorizontal && onVertical)
{
// DirectionBits 계산
int directionBits = CalculateDirectionBits(intersectPoint, segments, tolerance);
// Row, Column 계산 (1-based)
int row = (int)Math.Round(intersectY / 10.0) + 1;
int column = (int)Math.Round(intersectX / 10.0) + 1;
var intersection = new IntersectionPoint
{
Position = intersectPoint,
DirectionBits = directionBits,
Row = row,
Column = column
};
intersections.Add(intersection);
}
}
}
return intersections;
}
/// <summary>
/// 특정 점에서의 DirectionBits를 계산합니다.
/// </summary>
private static int CalculateDirectionBits(Point3d point, List<(Point3d start, Point3d end, bool isHorizontal)> segments, double tolerance)
{
int bits = 0;
// Right: 1, Up: 2, Left: 4, Down: 8
foreach (var segment in segments)
{
if (segment.isHorizontal)
{
// 수평선에서 점이 선분 위에 있는지 확인
if (Math.Abs(point.Y - segment.start.Y) < tolerance &&
point.X >= Math.Min(segment.start.X, segment.end.X) - tolerance &&
point.X <= Math.Max(segment.start.X, segment.end.X) + tolerance)
{
// 오른쪽으로 선분이 있는지 확인
if (Math.Max(segment.start.X, segment.end.X) > point.X + tolerance)
bits |= 1; // Right
// 왼쪽으로 선분이 있는지 확인
if (Math.Min(segment.start.X, segment.end.X) < point.X - tolerance)
bits |= 4; // Left
}
}
else
{
// 수직선에서 점이 선분 위에 있는지 확인
if (Math.Abs(point.X - segment.start.X) < tolerance &&
point.Y >= Math.Min(segment.start.Y, segment.end.Y) - tolerance &&
point.Y <= Math.Max(segment.start.Y, segment.end.Y) + tolerance)
{
// 위쪽으로 선분이 있는지 확인
if (Math.Max(segment.start.Y, segment.end.Y) > point.Y + tolerance)
bits |= 2; // Up
// 아래쪽으로 선분이 있는지 확인
if (Math.Min(segment.start.Y, segment.end.Y) < point.Y - tolerance)
bits |= 8; // Down
}
}
}
return bits;
}
/// <summary>
/// 교차점들로부터 셀을 추출합니다.
/// </summary>
private static List<TableCell> ExtractTestCells(List<IntersectionPoint> intersections,
List<(Point3d start, Point3d end, bool isHorizontal)> segments,
DwgDataExtractor extractor)
{
var cells = new List<TableCell>();
double tolerance = 0.1;
// topLeft 후보들을 찾아서 각각에 대해 bottomRight를 찾기
var topLeftCandidates = intersections.Where(i => extractor.IsValidTopLeft(i.DirectionBits)).ToList();
Debug.WriteLine($"TopLeft 후보 개수: {topLeftCandidates.Count}");
foreach (var topLeft in topLeftCandidates)
{
Debug.WriteLine($"\nTopLeft 후보 R{topLeft.Row}C{topLeft.Column} 처리 중...");
// bottomRight 찾기 (실제 코드와 동일한 방식)
var bottomRight = FindBottomRightForTest(topLeft, intersections, extractor);
if (bottomRight != null)
{
Debug.WriteLine($" BottomRight 발견: R{bottomRight.Row}C{bottomRight.Column}");
// 셀 생성
var cell = new TableCell
{
MinPoint = new Point3d(topLeft.Position.X, bottomRight.Position.Y, 0),
MaxPoint = new Point3d(bottomRight.Position.X, topLeft.Position.Y, 0),
Row = topLeft.Row,
Column = topLeft.Column,
CellText = $"R{topLeft.Row}C{topLeft.Column}"
};
cells.Add(cell);
Debug.WriteLine($" 셀 생성 완료: ({cell.MinPoint.X:F1},{cell.MinPoint.Y:F1}) to ({cell.MaxPoint.X:F1},{cell.MaxPoint.Y:F1})");
}
else
{
Debug.WriteLine($" BottomRight을 찾지 못함");
}
}
return cells;
}
/// <summary>
/// 테스트용 bottomRight 찾기 메서드
/// </summary>
private static IntersectionPoint FindBottomRightForTest(IntersectionPoint topLeft,
List<IntersectionPoint> intersections,
DwgDataExtractor extractor)
{
// 교차점들을 Row/Column으로 딕셔너리 구성
var intersectionLookup = intersections
.Where(i => i.Row > 0 && i.Column > 0)
.GroupBy(i => i.Row)
.ToDictionary(g => g.Key, g => g.ToDictionary(i => i.Column, i => i));
// topLeft에서 시작하여 bottomRight 찾기
int maxRow = intersectionLookup.Keys.Any() ? intersectionLookup.Keys.Max() : topLeft.Row;
for (int targetRow = topLeft.Row + 1; targetRow <= maxRow + 2; targetRow++)
{
if (!intersectionLookup.ContainsKey(targetRow)) continue;
var rowIntersections = intersectionLookup[targetRow];
var availableColumns = rowIntersections.Keys.Where(col => col >= topLeft.Column).OrderBy(col => col);
foreach (int targetColumn in availableColumns)
{
var candidate = rowIntersections[targetColumn];
if (extractor.IsValidBottomRight(candidate.DirectionBits) ||
(targetRow == maxRow && targetColumn == intersectionLookup.Values.SelectMany(row => row.Keys).Max()))
{
return candidate;
}
}
}
return null;
}
}
}

317
Models/JsonDataProcessor.cs Normal file
View File

@@ -0,0 +1,317 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace DwgExtractorManual.Models
{
/// <summary>
/// JSON 파일 처리 및 매핑 데이터 관리를 담당하는 클래스
/// </summary>
internal class JsonDataProcessor
{
/// <summary>
/// JSON 파일에서 PDF 분석 결과를 읽어 매핑 데이터를 업데이트
/// </summary>
public bool UpdateMappingDataFromJson(Dictionary<string, Dictionary<string, (string, string, string, string)>> mappingData, string jsonFilePath)
{
try
{
Debug.WriteLine($"[DEBUG] JSON 파일에서 PDF 값 업데이트 시작: {jsonFilePath}");
if (!File.Exists(jsonFilePath))
{
Debug.WriteLine($"? JSON 파일이 존재하지 않습니다: {jsonFilePath}");
return false;
}
// JSON 파일 읽기 및 정리
string jsonContent = File.ReadAllText(jsonFilePath, System.Text.Encoding.UTF8);
jsonContent = CleanJsonContent(jsonContent);
JObject jsonData;
try
{
jsonData = JObject.Parse(jsonContent);
}
catch (Newtonsoft.Json.JsonReaderException jsonEx)
{
Debug.WriteLine($"? JSON 파싱 오류: {jsonEx.Message}");
throw new System.Exception($"PDF 분석 JSON 파일 파싱 실패: {jsonEx.Message}\n파일: {jsonFilePath}");
}
var results = jsonData["results"] as JArray;
if (results == null)
{
Debug.WriteLine("? JSON에서 'results' 배열을 찾을 수 없습니다.");
return false;
}
int updatedCount = 0;
int totalEntries = 0;
foreach (JObject result in results)
{
var fileInfo = result["file_info"];
var pdfAnalysis = result["pdf_analysis"];
if (fileInfo == null || pdfAnalysis == null) continue;
string fileName = fileInfo["name"]?.ToString();
if (string.IsNullOrEmpty(fileName)) continue;
string fileNameWithoutExt = Path.GetFileNameWithoutExtension(fileName);
if (!mappingData.ContainsKey(fileNameWithoutExt))
{
Debug.WriteLine($"?? 매핑 데이터에 파일이 없습니다: {fileNameWithoutExt}");
continue;
}
foreach (var property in pdfAnalysis.Cast<JProperty>())
{
string aiLabel = property.Name;
var valueObj = property.Value as JObject;
if (valueObj == null) continue;
string pdfValue = valueObj["value"]?.ToString();
if (string.IsNullOrEmpty(pdfValue)) continue;
totalEntries++;
var fileData = mappingData[fileNameWithoutExt];
var matchingEntry = fileData.FirstOrDefault(kvp =>
string.Equals(kvp.Value.Item1.Trim(), aiLabel.Trim(), StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrEmpty(matchingEntry.Key))
{
var existingValue = matchingEntry.Value;
fileData[matchingEntry.Key] = (existingValue.Item1, existingValue.Item2, existingValue.Item3, pdfValue);
updatedCount++;
}
}
}
Debug.WriteLine($"[DEBUG] PDF 데이터 업데이트 완료: {updatedCount}/{totalEntries} 업데이트됨");
return true;
}
catch (System.Exception ex)
{
Debug.WriteLine($"? JSON에서 PDF 값 업데이트 중 오류: {ex.Message}");
return false;
}
}
/// <summary>
/// 매핑 딕셔너리를 JSON 파일로 저장
/// </summary>
public void SaveMappingDictionary(Dictionary<string, Dictionary<string, (string, string, string, string)>> mappingData, string filePath)
{
try
{
Debug.WriteLine($"[DEBUG] 매핑 딕셔너리 저장 시작: {filePath}");
var serializableData = new Dictionary<string, Dictionary<string, object>>();
foreach (var fileEntry in mappingData)
{
var fileData = new Dictionary<string, object>();
foreach (var mapEntry in fileEntry.Value)
{
fileData[mapEntry.Key] = new
{
AILabel = mapEntry.Value.Item1,
DwgTag = mapEntry.Value.Item2,
DwgValue = mapEntry.Value.Item3,
PdfValue = mapEntry.Value.Item4
};
}
serializableData[fileEntry.Key] = fileData;
}
string jsonContent = JsonConvert.SerializeObject(serializableData, Formatting.Indented);
File.WriteAllText(filePath, jsonContent, System.Text.Encoding.UTF8);
Debug.WriteLine($"? 매핑 딕셔너리 저장 완료: {Path.GetFileName(filePath)}");
Debug.WriteLine($"?? 저장된 파일 수: {mappingData.Count}");
}
catch (System.Exception ex)
{
Debug.WriteLine($"? 매핑 딕셔너리 저장 중 오류: {ex.Message}");
throw;
}
}
/// <summary>
/// JSON 파일에서 매핑 딕셔너리를 로드
/// </summary>
public Dictionary<string, Dictionary<string, (string, string, string, string)>> LoadMappingDictionary(string filePath)
{
var result = new Dictionary<string, Dictionary<string, (string, string, string, string)>>();
try
{
Debug.WriteLine($"[DEBUG] 매핑 딕셔너리 로드 시작: {filePath}");
if (!File.Exists(filePath))
{
Debug.WriteLine($"?? 매핑 파일이 존재하지 않습니다: {filePath}");
return result;
}
string jsonContent = File.ReadAllText(filePath, System.Text.Encoding.UTF8);
jsonContent = CleanJsonContent(jsonContent);
Dictionary<string, Dictionary<string, JObject>> deserializedData;
try
{
deserializedData = JsonConvert.DeserializeObject<Dictionary<string, Dictionary<string, JObject>>>(jsonContent);
}
catch (Newtonsoft.Json.JsonReaderException jsonEx)
{
Debug.WriteLine($"? JSON 파싱 오류: {jsonEx.Message}");
throw new System.Exception($"매핑 JSON 파일 파싱 실패: {jsonEx.Message}\n파일: {filePath}");
}
if (deserializedData != null)
{
foreach (var fileEntry in deserializedData)
{
var fileData = new Dictionary<string, (string, string, string, string)>();
foreach (var mapEntry in fileEntry.Value)
{
var valueObj = mapEntry.Value;
string aiLabel = valueObj["AILabel"]?.ToString() ?? "";
string dwgTag = valueObj["DwgTag"]?.ToString() ?? "";
string dwgValue = valueObj["DwgValue"]?.ToString() ?? "";
string pdfValue = valueObj["PdfValue"]?.ToString() ?? "";
fileData[mapEntry.Key] = (aiLabel, dwgTag, dwgValue, pdfValue);
}
result[fileEntry.Key] = fileData;
}
}
Debug.WriteLine($"? 매핑 딕셔너리 로드 완료");
Debug.WriteLine($"?? 로드된 파일 수: {result.Count}");
return result;
}
catch (System.Exception ex)
{
Debug.WriteLine($"? 매핑 딕셔너리 로드 중 오류: {ex.Message}");
throw;
}
}
/// <summary>
/// JSON 내용을 정리하여 파싱 가능한 상태로 만듭니다.
/// </summary>
private string CleanJsonContent(string jsonContent)
{
if (string.IsNullOrEmpty(jsonContent))
return jsonContent;
try
{
var lines = jsonContent.Split('\n');
var cleanedLines = new List<string>();
bool inMultiLineComment = false;
foreach (string line in lines)
{
string processedLine = line;
// 멀티라인 주석 처리
if (inMultiLineComment)
{
int endIndex = processedLine.IndexOf("*/");
if (endIndex >= 0)
{
processedLine = processedLine.Substring(endIndex + 2);
inMultiLineComment = false;
}
else
{
continue;
}
}
int multiLineStart = processedLine.IndexOf("/*");
if (multiLineStart >= 0)
{
int multiLineEnd = processedLine.IndexOf("*/", multiLineStart + 2);
if (multiLineEnd >= 0)
{
processedLine = processedLine.Substring(0, multiLineStart) +
processedLine.Substring(multiLineEnd + 2);
}
else
{
processedLine = processedLine.Substring(0, multiLineStart);
inMultiLineComment = true;
}
}
// 싱글라인 주석 제거
bool inString = false;
bool escaped = false;
int commentIndex = -1;
for (int i = 0; i < processedLine.Length - 1; i++)
{
char current = processedLine[i];
char next = processedLine[i + 1];
if (escaped)
{
escaped = false;
continue;
}
if (current == '\\')
{
escaped = true;
continue;
}
if (current == '"')
{
inString = !inString;
continue;
}
if (!inString && current == '/' && next == '/')
{
commentIndex = i;
break;
}
}
if (commentIndex >= 0)
{
processedLine = processedLine.Substring(0, commentIndex);
}
if (!string.IsNullOrWhiteSpace(processedLine))
{
cleanedLines.Add(processedLine);
}
}
return string.Join("\n", cleanedLines);
}
catch (System.Exception ex)
{
Debug.WriteLine($"? JSON 정리 중 오류: {ex.Message}");
return jsonContent;
}
}
}
}

View File

@@ -0,0 +1,177 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Collections.Generic;
namespace DwgExtractorManual.Models
{
/// <summary>
/// Note 박스 텍스트와 테이블 추출 기능을 테스트하는 클래스
/// </summary>
public class NoteExtractionTester
{
/// <summary>
/// DWG 파일에서 Note 데이터를 추출하고 CSV로 저장하는 전체 테스트
/// </summary>
public static void TestNoteExtractionAndCsvExport(string dwgFilePath, string outputDirectory)
{
try
{
Debug.WriteLine("=== Note 추출 및 CSV 내보내기 테스트 시작 ===");
// 출력 디렉토리가 없으면 생성
if (!Directory.Exists(outputDirectory))
{
Directory.CreateDirectory(outputDirectory);
Debug.WriteLine($"출력 디렉토리 생성: {outputDirectory}");
}
// 1. Teigha 서비스 초기화
Debug.WriteLine("1. Teigha 서비스 초기화 중...");
TeighaServicesManager.Instance.AcquireServices();
// 2. DwgDataExtractor 인스턴스 생성
Debug.WriteLine("2. DwgDataExtractor 인스턴스 생성...");
var mappingData = new MappingTableData();
var fieldMapper = new FieldMapper(mappingData);
var extractor = new DwgDataExtractor(fieldMapper);
// 3. Note 데이터 추출
Debug.WriteLine($"3. DWG 파일에서 Note 추출 중: {Path.GetFileName(dwgFilePath)}");
var noteEntities = extractor.ExtractNotesFromDrawing(dwgFilePath);
Debug.WriteLine($" 추출된 Note 엔터티 수: {noteEntities.NoteEntities.Count}");
// 4. Note 데이터 분석
AnalyzeNoteData(noteEntities.NoteEntities);
// 5. CSV 내보내기
Debug.WriteLine("5. CSV 파일 생성 중...");
var csvWriter = new CsvDataWriter();
var baseFileName = Path.GetFileNameWithoutExtension(dwgFilePath);
// 5-1. Note 박스 텍스트만 CSV로 저장
var noteTextCsvPath = Path.Combine(outputDirectory, $"{baseFileName}_note_texts.csv");
csvWriter.WriteNoteBoxTextToCsv(noteEntities.NoteEntities, noteTextCsvPath);
Debug.WriteLine($" Note 텍스트 CSV 저장: {noteTextCsvPath}");
// 5-2. Note 테이블 데이터만 CSV로 저장
var noteTableCsvPath = Path.Combine(outputDirectory, $"{baseFileName}_note_tables.csv");
csvWriter.WriteNoteTablesToCsv(noteEntities.NoteEntities, noteTableCsvPath);
Debug.WriteLine($" Note 테이블 CSV 저장: {noteTableCsvPath}");
// 5-3. 통합 CSV 저장
var combinedCsvPath = Path.Combine(outputDirectory, $"{baseFileName}_note_combined.csv");
csvWriter.WriteNoteDataToCombinedCsv(noteEntities.NoteEntities, combinedCsvPath);
Debug.WriteLine($" 통합 CSV 저장: {combinedCsvPath}");
// 5-4. 개별 테이블 CSV 저장
var individualTablesDir = Path.Combine(outputDirectory, $"{baseFileName}_individual_tables");
csvWriter.WriteIndividualNoteTablesCsv(noteEntities.NoteEntities, individualTablesDir);
Debug.WriteLine($" 개별 테이블 CSV 저장: {individualTablesDir}");
// 5-5. 통계 정보 CSV 저장
var statisticsCsvPath = Path.Combine(outputDirectory, $"{baseFileName}_note_statistics.csv");
csvWriter.WriteNoteStatisticsToCsv(noteEntities.NoteEntities, statisticsCsvPath);
Debug.WriteLine($" 통계 정보 CSV 저장: {statisticsCsvPath}");
Debug.WriteLine("=== Note 추출 및 CSV 내보내기 테스트 완료 ===");
Debug.WriteLine($"모든 결과 파일이 저장되었습니다: {outputDirectory}");
}
catch (Exception ex)
{
Debug.WriteLine($"❌ 테스트 중 오류 발생: {ex.Message}");
Debug.WriteLine($"스택 트레이스: {ex.StackTrace}");
throw;
}
finally
{
// Teigha 서비스 정리
try
{
TeighaServicesManager.Instance.ForceDisposeServices();
Debug.WriteLine("Teigha 서비스 정리 완료");
}
catch (Exception ex)
{
Debug.WriteLine($"Teigha 서비스 정리 중 오류: {ex.Message}");
}
}
}
/// <summary>
/// 추출된 Note 데이터를 분석하여 로그로 출력
/// </summary>
private static void AnalyzeNoteData(List<NoteEntityInfo> noteEntities)
{
Debug.WriteLine("=== Note 데이터 분석 ===");
var noteHeaders = noteEntities.Where(ne => ne.Type == "Note").ToList();
var noteContents = noteEntities.Where(ne => ne.Type == "NoteContent").ToList();
var notesWithTables = noteEntities.Where(ne => ne.Type == "Note" && !string.IsNullOrEmpty(ne.TableCsv)).ToList();
Debug.WriteLine($"전체 Note 헤더 수: {noteHeaders.Count}");
Debug.WriteLine($"전체 Note 콘텐츠 수: {noteContents.Count}");
Debug.WriteLine($"테이블이 있는 Note 수: {notesWithTables.Count}");
// 각 Note 분석
foreach (var note in noteHeaders)
{
Debug.WriteLine($"");
Debug.WriteLine($"Note: '{note.Text}' at ({note.X:F1}, {note.Y:F1})");
if (!string.IsNullOrEmpty(note.TableCsv))
{
var tableLines = note.TableCsv.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
Debug.WriteLine($" 테이블 데이터: {tableLines.Length}행");
// 테이블 내용 일부 출력
for (int i = 0; i < Math.Min(3, tableLines.Length); i++)
{
var line = tableLines[i];
if (line.Length > 50) line = line.Substring(0, 50) + "...";
Debug.WriteLine($" 행 {i + 1}: {line}");
}
if (tableLines.Length > 3)
{
Debug.WriteLine($" ... 및 {tableLines.Length - 3}개 행 더");
}
}
// 이 Note와 연관된 NoteContent들
var relatedContents = noteContents.Where(nc =>
Math.Abs(nc.Y - note.Y) < 50 && // Y 좌표가 비슷한 범위 (Note 아래)
nc.Y < note.Y) // Note보다 아래쪽
.OrderBy(nc => nc.SortOrder)
.ToList();
if (relatedContents.Count > 0)
{
Debug.WriteLine($" 관련 콘텐츠: {relatedContents.Count}개");
foreach (var content in relatedContents.Take(3))
{
Debug.WriteLine($" '{content.Text}' at ({content.X:F1}, {content.Y:F1})");
}
if (relatedContents.Count > 3)
{
Debug.WriteLine($" ... 및 {relatedContents.Count - 3}개 더");
}
}
}
// 레이어별 분포
Debug.WriteLine("");
Debug.WriteLine("레이어별 분포:");
var layerGroups = noteEntities.GroupBy(ne => ne.Layer).OrderByDescending(g => g.Count());
foreach (var layerGroup in layerGroups)
{
Debug.WriteLine($" {layerGroup.Key}: {layerGroup.Count()}개");
}
Debug.WriteLine("=== Note 데이터 분석 완료 ===");
}
}
}

25
Models/SettingsManager.cs Normal file
View File

@@ -0,0 +1,25 @@
using Newtonsoft.Json;
using System.IO;
namespace DwgExtractorManual.Models
{
public static class SettingsManager
{
private static readonly string SettingsFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "settings.json");
public static void SaveSettings(AppSettings settings)
{
string json = JsonConvert.SerializeObject(settings, Formatting.Indented);
File.WriteAllText(SettingsFilePath, json);
}
public static AppSettings? LoadSettings()
{
if (!File.Exists(SettingsFilePath))
{ return null; }
string json = File.ReadAllText(SettingsFilePath);
return JsonConvert.DeserializeObject<AppSettings>(json);
}
}
}

View File

@@ -15,15 +15,22 @@ namespace DwgExtractorManual.Models
/// </summary> /// </summary>
internal sealed class SqlDatas : IDisposable internal sealed class SqlDatas : IDisposable
{ {
Services appServices; // ODA 제품 활성화용 Services? appServices; // ODA 제품 활성화용 (managed by singleton)
readonly string connectionString = "Host=localhost;Database=postgres;Username=postgres;Password=Qwer1234"; readonly string connectionString = "Host=localhost;Database=postgres;Username=postgres;Password=Qwer1234";
void ActivateAndInitializeODA() void InitializeTeighaServices()
{ {
var userInfo = "c2FtYW4gZW5naW5lZXJpbmc="; try
var userSignature = "F0kuQTmtVpHtvl/TgaFVGE92/YqGmYR9SLoXckEjnOk8NoAQh7Sg6GQruVC04JqD4C/IipxJYqpqvMfMc2auRMG+cAJCKqKUE2djIMdkUdb+C5IVx2c97fcK5ba3n8DDvB54Upokajl+6j12yD8h8MKGOR3Z3zysObeXD62bFpQgp00GCYTqlxEZtTIRjHIPAfJfix8Y0jtXWWYyVJ3LYOu86as5xtx+hY1aakpYIJiQk/6pGd84qSn/9K1w8nxR7UrFzieDeQ/xM58BHSD4u/ZxVJwvv6Uy10tsdBFBTvfJMAFp05Y7yeyeCNr100tA3iOfmWoXAVRHfxnkPfiYR54aK04QI+R6OGkI+yd1oR5BtmN6BdDt3z8KYK5EpFGJGiJIGoUy5PvkYdJ2VV6xe9JWBiIJuI/tDn1Y+uyTQFA9qaDHnOURriXsRGfy8reDPf1eejybSJxWKkpilG6RXhq3xHlCkjZzh1Q45S+xYXNGatcWMm9nkn20M8Ke5JEVaI9w/p2GE36CHRtRQbt8kfPmsbWNXJCFr4svHW2MPbCKWoyn5XEyMWBnuAKi74zvczB13DKjf29SqSIgF5k/hwy2QrgvnaKzY1k8bw8w2/k0vJXcS3GKOB/ZYDle1tf/lkAD1HtnF9zE18TiXhVnqwAVjwg4ui1RPLn/LMs6b5Y="; {
Services.odActivate(userInfo, userSignature); Debug.WriteLine("[SqlDatas] TeighaServicesManager를 통한 Services 획득 중...");
appServices = new Services(); appServices = TeighaServicesManager.Instance.AcquireServices();
Debug.WriteLine($"[SqlDatas] Services 획득 성공. Reference Count: {TeighaServicesManager.Instance.ReferenceCount}");
}
catch (Teigha.Runtime.Exception ex)
{
Debug.WriteLine($"[SqlDatas] Teigha Services 초기화 실패: {ex.Message}");
throw;
}
} }
void CreateTables() void CreateTables()
@@ -78,7 +85,7 @@ namespace DwgExtractorManual.Models
public SqlDatas() public SqlDatas()
{ {
ActivateAndInitializeODA(); InitializeTeighaServices();
CreateTables(); CreateTables();
} }
@@ -136,8 +143,8 @@ namespace DwgExtractorManual.Models
cmd.Parameters.AddWithValue("Type", "DBText"); cmd.Parameters.AddWithValue("Type", "DBText");
cmd.Parameters.AddWithValue("Layer", layerName); cmd.Parameters.AddWithValue("Layer", layerName);
cmd.Parameters.AddWithValue("Text", dbText.TextString ?? ""); cmd.Parameters.AddWithValue("Text", dbText.TextString ?? "");
cmd.Parameters.AddWithValue("Path", database.Filename); cmd.Parameters.AddWithValue("Path", database.Filename ?? "");
cmd.Parameters.AddWithValue("FileName", Path.GetFileName(database.Filename)); cmd.Parameters.AddWithValue("FileName", string.IsNullOrEmpty(database.Filename) ? "" : Path.GetFileName(database.Filename));
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();
} }
@@ -155,8 +162,8 @@ namespace DwgExtractorManual.Models
cmd.Parameters.AddWithValue("Type", "MText"); cmd.Parameters.AddWithValue("Type", "MText");
cmd.Parameters.AddWithValue("Layer", layerName); cmd.Parameters.AddWithValue("Layer", layerName);
cmd.Parameters.AddWithValue("Text", mText.Contents ?? ""); cmd.Parameters.AddWithValue("Text", mText.Contents ?? "");
cmd.Parameters.AddWithValue("Path", database.Filename); cmd.Parameters.AddWithValue("Path", database.Filename ?? "");
cmd.Parameters.AddWithValue("FileName", Path.GetFileName(database.Filename)); cmd.Parameters.AddWithValue("FileName", string.IsNullOrEmpty(database.Filename) ? "" : Path.GetFileName(database.Filename));
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();
} }
@@ -224,8 +231,8 @@ namespace DwgExtractorManual.Models
else else
cmd.Parameters.AddWithValue("Value", tString); cmd.Parameters.AddWithValue("Value", tString);
cmd.Parameters.AddWithValue("Path", database.Filename); cmd.Parameters.AddWithValue("Path", database.Filename ?? "");
cmd.Parameters.AddWithValue("FileName", Path.GetFileName(database.Filename)); cmd.Parameters.AddWithValue("FileName", string.IsNullOrEmpty(database.Filename) ? "" : Path.GetFileName(database.Filename));
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();
} }
@@ -301,8 +308,20 @@ namespace DwgExtractorManual.Models
{ {
if (appServices != null) if (appServices != null)
{ {
appServices.Dispose(); try
appServices = null; {
Debug.WriteLine("[SqlDatas] Teigha Services 해제 중...");
TeighaServicesManager.Instance.ReleaseServices();
Debug.WriteLine($"[SqlDatas] Teigha Services 해제 완료. Remaining ref count: {TeighaServicesManager.Instance.ReferenceCount}");
}
catch (Teigha.Runtime.Exception ex)
{
Debug.WriteLine($"[SqlDatas] Teigha Services 해제 중 오류 (무시됨): {ex.Message}");
}
finally
{
appServices = null;
}
} }
} }
} }

View File

@@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using Teigha.Geometry;
namespace DwgExtractorManual.Models
{
/// <summary>
/// 테이블 셀 시각화를 위한 데이터 클래스
/// </summary>
public class TableCellVisualizationData
{
public string FileName { get; set; } = "";
public string NoteText { get; set; } = "";
public List<CellBounds> Cells { get; set; } = new List<CellBounds>();
public List<SegmentInfo> TableSegments { get; set; } = new List<SegmentInfo>();
public List<TextInfo> TextEntities { get; set; } = new List<TextInfo>();
public List<IntersectionInfo> IntersectionPoints { get; set; } = new List<IntersectionInfo>(); // 교차점 정보 추가
public List<DiagonalLine> DiagonalLines { get; set; } = new List<DiagonalLine>(); // 셀 대각선 정보 추가
public List<CellBoundaryInfo> CellBoundaries { get; set; } = new List<CellBoundaryInfo>(); // 정확한 셀 경계 정보 추가
public (double minX, double minY, double maxX, double maxY) NoteBounds { get; set; }
}
/// <summary>
/// 셀 경계 정보
/// </summary>
public class CellBounds
{
public double MinX { get; set; }
public double MinY { get; set; }
public double MaxX { get; set; }
public double MaxY { get; set; }
public int Row { get; set; }
public int Column { get; set; }
public string Text { get; set; } = "";
public bool IsValid { get; set; } = true;
public double Width => MaxX - MinX;
public double Height => MaxY - MinY;
public double CenterX => (MinX + MaxX) / 2;
public double CenterY => (MinY + MaxY) / 2;
}
/// <summary>
/// 선분 정보
/// </summary>
public class SegmentInfo
{
public double StartX { get; set; }
public double StartY { get; set; }
public double EndX { get; set; }
public double EndY { get; set; }
public bool IsHorizontal { get; set; }
public string Color { get; set; } = "Black";
}
/// <summary>
/// 텍스트 정보
/// </summary>
public class TextInfo
{
public double X { get; set; }
public double Y { get; set; }
public string Text { get; set; } = "";
public bool IsInTable { get; set; }
public string Color { get; set; } = "Blue";
}
/// <summary>
/// 교차점 정보
/// </summary>
public class IntersectionInfo
{
public double X { get; set; }
public double Y { get; set; }
public int DirectionBits { get; set; } // 비트 플래그 숫자
public int Row { get; set; } // Row 번호
public int Column { get; set; } // Column 번호
public bool IsTopLeft { get; set; } // topLeft 후보인지
public bool IsBottomRight { get; set; } // bottomRight 후보인지
public string Color { get; set; } = "Red";
}
/// <summary>
/// 대각선 정보 (셀 디버깅용)
/// </summary>
public class DiagonalLine
{
public double StartX { get; set; }
public double StartY { get; set; }
public double EndX { get; set; }
public double EndY { get; set; }
public string Color { get; set; } = "Green";
public string Label { get; set; } = ""; // 디버깅 라벨
}
/// <summary>
/// 정확한 셀 경계 정보 (4개 모서리 좌표)
/// </summary>
public class CellBoundaryInfo
{
public double TopLeftX { get; set; }
public double TopLeftY { get; set; }
public double TopRightX { get; set; }
public double TopRightY { get; set; }
public double BottomLeftX { get; set; }
public double BottomLeftY { get; set; }
public double BottomRightX { get; set; }
public double BottomRightY { get; set; }
public string Label { get; set; } = "";
public double Width { get; set; }
public double Height { get; set; }
public string Color { get; set; } = "DarkBlue";
public string CellText { get; set; } = ""; // 셀 내 텍스트 내용
}
}

View File

@@ -0,0 +1,192 @@
using System;
using System.Diagnostics;
using Teigha.Runtime;
namespace DwgExtractorManual.Models
{
/// <summary>
/// Singleton class to manage Teigha Services lifecycle and prevent disposal conflicts
/// </summary>
public sealed class TeighaServicesManager
{
private static readonly object _lock = new object();
private static TeighaServicesManager? _instance = null;
private static Services? _services = null;
private static int _referenceCount = 0;
private static bool _isActivated = false;
private TeighaServicesManager()
{
// Private constructor for singleton
}
public static TeighaServicesManager Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
{
_instance = new TeighaServicesManager();
}
}
}
return _instance;
}
}
/// <summary>
/// Acquires Teigha Services (creates if needed, increments reference count)
/// </summary>
/// <returns>The Services instance</returns>
public Services AcquireServices()
{
lock (_lock)
{
try
{
Debug.WriteLine($"[TeighaManager] AcquireServices - Current ref count: {_referenceCount}");
if (!_isActivated)
{
Debug.WriteLine("[TeighaManager] Activating ODA for first time...");
ActivateODA();
_isActivated = true;
}
if (_services == null)
{
Debug.WriteLine("[TeighaManager] Creating new Services instance...");
_services = new Services();
Debug.WriteLine("[TeighaManager] Services instance created successfully");
}
_referenceCount++;
Debug.WriteLine($"[TeighaManager] Services acquired - New ref count: {_referenceCount}");
return _services;
}
catch (Teigha.Runtime.Exception ex)
{
Debug.WriteLine($"[TeighaManager] Error acquiring services: {ex.Message}");
throw;
}
}
}
/// <summary>
/// Releases Teigha Services (decrements reference count, disposes when count reaches 0)
/// </summary>
public void ReleaseServices()
{
lock (_lock)
{
try
{
Debug.WriteLine($"[TeighaManager] ReleaseServices - Current ref count: {_referenceCount}");
if (_referenceCount > 0)
{
_referenceCount--;
Debug.WriteLine($"[TeighaManager] Services released - New ref count: {_referenceCount}");
}
// Don't dispose Services until app shutdown to prevent conflicts
// Just track reference count for debugging
if (_referenceCount == 0)
{
Debug.WriteLine("[TeighaManager] All references released (Services kept alive for app lifetime)");
}
}
catch (Teigha.Runtime.Exception ex)
{
Debug.WriteLine($"[TeighaManager] Error releasing services: {ex.Message}");
// Don't throw on release to prevent cascade failures
}
}
}
/// <summary>
/// Force dispose Services (only call on application shutdown)
/// </summary>
public void ForceDisposeServices()
{
lock (_lock)
{
try
{
Debug.WriteLine("[TeighaManager] Force disposing Services...");
if (_services != null)
{
_services.Dispose();
Debug.WriteLine("[TeighaManager] Services disposed successfully");
}
_services = null;
_referenceCount = 0;
_isActivated = false;
Debug.WriteLine("[TeighaManager] Force dispose completed");
}
catch (Teigha.Runtime.Exception ex)
{
Debug.WriteLine($"[TeighaManager] Error during force dispose: {ex.Message}");
// Reset state even if disposal fails
_services = null;
_referenceCount = 0;
_isActivated = false;
}
}
}
/// <summary>
/// Get current reference count (for debugging)
/// </summary>
public int ReferenceCount
{
get
{
lock (_lock)
{
return _referenceCount;
}
}
}
/// <summary>
/// Check if Services is active and valid
/// </summary>
public bool IsServicesActive
{
get
{
lock (_lock)
{
return _services != null && _isActivated;
}
}
}
private void ActivateODA()
{
try
{
Debug.WriteLine("[TeighaManager] Activating ODA...");
var userInfo = "c2FtYW4gZW5naW5lZXJpbmc=";
var userSignature = "F0kuQTmtVpHtvl/TgaFVGE92/YqGmYR9SLoXckEjnOk8NoAQh7Sg6GQruVC04JqD4C/IipxJYqpqvMfMc2auRMG+cAJCKqKUE2djIMdkUdb+C5IVx2c97fcK5ba3n8DDvB54Upokajl+6j12yD8h8MKGOR3Z3zysObeXD62bFpQgp00GCYTqlxEZtTIRjHIPAfJfix8Y0jtXWWYyVJ3LYOu86as5xtx+hY1aakpYIJiQk/6pGd84qSn/9K1w8nxR7UrFzieDeQ/xM58BHSD4u/ZxVJwvv6Uy10tsdBFBTvfJMAFp05Y7yeyeCNr100tA3iOfmWoXAVRHfxnkPfiYR54aK04QI+R6OGkI+yd1oR5BtmN6BdDt3z8KYK5EpFGJGiJIGoUy5PvkYdJ2VV6xe9JWBiIJuI/tDn1Y+uyTQFA9qaDHnOURriXsRGfy8reDPf1eejybSJxWKkpilG6RXhq3xHlCkjZzh1Q45S+xYXNGatcWMm9nkn20M8Ke5JEVaI9w/p2GE36CHRtRQbt8kfPmsbWNXJCFr4svHW2MPbCKWoyn5XEyMWBnuAKi74zvczB13DKjf29SqSIgF5k/hwy2QrgvnaKzY1k8bw8w2/k0vJXcS3GKOB/ZYDle1tf/lkAD1HtnF9zE18TiXhVnqwAVjwg4ui1RPLn/LMs6b5Y=";
Services.odActivate(userInfo, userSignature);
Debug.WriteLine("[TeighaManager] ODA activation successful");
}
catch (Teigha.Runtime.Exception ex)
{
Debug.WriteLine($"[TeighaManager] ODA activation failed: {ex.Message}");
throw;
}
}
}
}

48
NoteDetectionRefactor.md Normal file
View File

@@ -0,0 +1,48 @@
Project: Refactor the NOTE Content Box Detection Algorithm
1. High-Level Goal:
The primary objective is to replace the current, fragile "horizontal search line" algorithm in
Models/DwgDataExtractor.cs with a more robust and accurate method that reliably finds the content box for
any "NOTE" text, regardless of its position or the composition of its bounding box.
2. Core Strategy: "Vertical Ray-Casting"
We will implement a new algorithm that emulates how a human would visually locate the content. This
involves a "gradual downward scan" (or vertical ray-cast) from the NOTE's position.
3. Implementation Plan (TODO List):
* Step 1: Unify All Geometry into Line Segments
* Create a single helper method, GetAllLineSegments, that processes all Line and Polyline entities from
the drawing.
* This method will decompose every Polyline into its constituent Line segments.
* It will return a single, unified List<Line> containing every potential boundary segment in the
drawing.
* Crucially: This method must ensure all temporary Line objects created during the process are properly
disposed of to prevent memory leaks.
* Step 2: Implement the Ray-Casting Logic in `FindNoteBox`
* The FindNoteBox method will be completely rewritten.
* It will first call GetAllLineSegments to get the unified geometry list.
* It will then perform the vertical ray-cast starting from the NOTE's X-coordinate and scanning
downwards.
* It will find all horizontal lines that intersect the ray and sort them by their Y-coordinate (from
top to bottom).
* It will identify the second line in this sorted list as the top edge of the content box (the first is
assumed to be the NOTE's own bounding box).
* Step 3: Implement Smart Box Tracing
* Create a new helper method, TraceBoxFromTopLine.
* This method will take the identified top line segment as its starting point.
* It will intelligently trace the remaining three sides of the rectangle by searching the unified list
of line segments for the nearest connecting corners.
* This tracing logic must be tolerant of small gaps between the endpoints of the lines forming the box.
* Step 4: Final Cleanup
* Once the new ray-casting algorithm is fully implemented and validated, all of the old, obsolete
methods related to the previous search-line approach must be deleted to keep the code clean. This
includes:
* FindIntersectingLineSegments
* TraceRectangleFromLineSegments
* FindNextConnectedLineSegment
* DoesLineIntersectPolyline
* GetPolylineBounds

View File

@@ -0,0 +1,109 @@
<Window x:Class="DwgExtractorManual.Views.TableCellVisualizationWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:DwgExtractorManual.Controls"
Title="테이블 셀 시각화" Height="800" Width="1200"
WindowStartupLocation="CenterOwner"
MinHeight="600" MinWidth="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 헤더 -->
<Border Grid.Row="0" Background="#34495E" Padding="15">
<StackPanel>
<TextBlock Text="테이블 셀 시각화"
FontSize="20" FontWeight="Bold"
Foreground="White" HorizontalAlignment="Center"/>
<TextBlock Text="추출된 테이블 셀들의 경계를 시각적으로 확인할 수 있습니다"
FontSize="12" Foreground="LightGray"
HorizontalAlignment="Center" Margin="0,5,0,0"/>
</StackPanel>
</Border>
<!-- 메인 영역 -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 좌측 패널: 파일 목록 및 설정 -->
<Border Grid.Column="0" Background="#ECF0F1" BorderBrush="#BDC3C7" BorderThickness="0,0,1,0">
<StackPanel Margin="10">
<TextBlock Text="파일 목록" FontWeight="Bold" FontSize="14" Margin="0,0,0,10"/>
<ListBox x:Name="lstFiles" Height="200"
SelectionChanged="LstFiles_SelectionChanged"
Background="White" BorderBrush="#BDC3C7">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding FileName}" FontWeight="Bold" FontSize="12"/>
<TextBlock Text="{Binding NoteText}" FontSize="10" Foreground="Gray" TextTrimming="CharacterEllipsis"/>
<TextBlock FontSize="10" Foreground="DarkBlue">
<Run Text="셀: "/>
<Run Text="{Binding Path=Cells.Count, Mode=OneWay}"/>
<Run Text="개"/>
</TextBlock>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Separator Margin="0,10"/>
<TextBlock Text="표시 옵션" FontWeight="Bold" FontSize="14" Margin="0,0,0,10"/>
<CheckBox x:Name="chkShowCells" Content="셀 경계 표시 (기존)" IsChecked="False"
Checked="RefreshVisualization" Unchecked="RefreshVisualization"/>
<CheckBox x:Name="chkShowCellBoundaries" Content="정확한 셀 경계 표시" IsChecked="True" Margin="0,5,0,0"
Checked="RefreshVisualization" Unchecked="RefreshVisualization"/>
<CheckBox x:Name="chkShowSegments" Content="선분 표시" IsChecked="True" Margin="0,5,0,0"
Checked="RefreshVisualization" Unchecked="RefreshVisualization"/>
<CheckBox x:Name="chkShowTexts" Content="텍스트 표시" IsChecked="True" Margin="0,5,0,0"
Checked="RefreshVisualization" Unchecked="RefreshVisualization"/>
<CheckBox x:Name="chkShowIntersections" Content="교차점 표시" IsChecked="True" Margin="0,5,0,0"
Checked="RefreshVisualization" Unchecked="RefreshVisualization"/>
<CheckBox x:Name="chkShowDiagonals" Content="셀 대각선 표시" IsChecked="True" Margin="0,5,0,0"
Checked="RefreshVisualization" Unchecked="RefreshVisualization"/>
<CheckBox x:Name="chkShowNoteBounds" Content="Note 경계 표시" IsChecked="False" Margin="0,5,0,0"
Checked="RefreshVisualization" Unchecked="RefreshVisualization"/>
<Separator Margin="0,10"/>
<TextBlock Text="확대/축소" FontWeight="Bold" FontSize="14" Margin="0,0,0,10"/>
<Button x:Name="btnZoomFit" Content="초기화 (마우스 우클릭)" Click="BtnZoomFit_Click" Margin="0,2"/>
<Separator Margin="0,10"/>
<TextBlock x:Name="txtInfo" Text="파일을 선택하세요" FontSize="11"
Foreground="DarkGray" TextWrapping="Wrap"/>
</StackPanel>
</Border>
<!-- 우측 패널: 시각화 영역 -->
<Border Grid.Column="1" Background="White">
<controls:ZoomBorder x:Name="svViewer" ClipToBounds="True">
<Canvas x:Name="cnvVisualization" Background="White"
Width="800" Height="600"
MouseMove="CnvVisualization_MouseMove"/>
</controls:ZoomBorder>
</Border>
</Grid>
<!-- 상태바 -->
<StatusBar Grid.Row="2" Background="#95A5A6">
<StatusBarItem>
<TextBlock x:Name="txtStatus" Text="준비됨"/>
</StatusBarItem>
<StatusBarItem HorizontalAlignment="Right">
<TextBlock x:Name="txtMousePos" Text="마우스: (0, 0)"/>
</StatusBarItem>
</StatusBar>
</Grid>
</Window>

View File

@@ -0,0 +1,493 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using DwgExtractorManual.Models;
using Brushes = System.Windows.Media.Brushes;
using MouseEventArgs = System.Windows.Input.MouseEventArgs;
using Point = System.Windows.Point;
using Rectangle = System.Windows.Shapes.Rectangle;
namespace DwgExtractorManual.Views
{
public partial class TableCellVisualizationWindow : Window
{
private List<TableCellVisualizationData> _visualizationData;
private TableCellVisualizationData? _currentData;
private double _scale = 1.0;
private const double MARGIN = 50;
public TableCellVisualizationWindow(List<TableCellVisualizationData> visualizationData)
{
InitializeComponent();
_visualizationData = visualizationData;
LoadFileList();
}
private void LoadFileList()
{
lstFiles.ItemsSource = _visualizationData;
if (_visualizationData.Count > 0)
{
lstFiles.SelectedIndex = 0;
}
}
private void LstFiles_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (lstFiles.SelectedItem is TableCellVisualizationData selectedData)
{
_currentData = selectedData;
RefreshVisualization();
UpdateInfo();
}
}
private void UpdateInfo()
{
if (_currentData == null)
{
txtInfo.Text = "파일을 선택하세요";
txtStatus.Text = "준비됨";
return;
}
txtInfo.Text = $"파일: {_currentData.FileName}\n" +
$"Note: {_currentData.NoteText}\n" +
$"셀 수: {_currentData.Cells.Count}\n" +
$"선분 수: {_currentData.TableSegments.Count}\n" +
$"텍스트 수: {_currentData.TextEntities.Count}\n" +
$"교차점 수: {_currentData.IntersectionPoints.Count}\n" +
$"대각선 수: {_currentData.DiagonalLines.Count}";
txtStatus.Text = $"{_currentData.FileName} - 셀 {_currentData.Cells.Count}개";
}
private void RefreshVisualization()
{
if (_currentData == null) return;
cnvVisualization.Children.Clear();
// 좌표계 변환 계산
var bounds = CalculateBounds();
if (bounds == null) return;
var (minX, minY, maxX, maxY) = bounds.Value;
var dataWidth = maxX - minX;
var dataHeight = maxY - minY;
// 캔버스 크기 설정 (여백 포함)
var canvasWidth = cnvVisualization.Width;
var canvasHeight = cnvVisualization.Height;
// 스케일 계산 (데이터가 캔버스에 맞도록)
var scaleX = (canvasWidth - 2 * MARGIN) / dataWidth;
var scaleY = (canvasHeight - 2 * MARGIN) / dataHeight;
_scale = Math.Min(scaleX, scaleY) * 0.9; // 여유분 10%
// Note 경계 표시
if (chkShowNoteBounds.IsChecked == true)
{
DrawNoteBounds(minX, minY, maxX, maxY);
}
// 선분 표시
if (chkShowSegments.IsChecked == true)
{
DrawSegments();
}
// 셀 경계 표시 (기존)
if (chkShowCells.IsChecked == true)
{
DrawCells();
}
// 정확한 셀 경계 표시 (새로운)
if (chkShowCellBoundaries.IsChecked == true)
{
DrawCellBoundaries();
}
// 텍스트 표시
if (chkShowTexts.IsChecked == true)
{
DrawTexts();
}
// 교차점 표시
if (chkShowIntersections.IsChecked == true)
{
DrawIntersections();
}
// 대각선 표시 (셀 디버깅용)
if (chkShowDiagonals.IsChecked == true)
{
DrawDiagonals();
}
}
private (double minX, double minY, double maxX, double maxY)? CalculateBounds()
{
if (_currentData == null || _currentData.Cells.Count == 0) return null;
var minX = _currentData.Cells.Min(c => c.MinX);
var minY = _currentData.Cells.Min(c => c.MinY);
var maxX = _currentData.Cells.Max(c => c.MaxX);
var maxY = _currentData.Cells.Max(c => c.MaxY);
// 선분도 고려
if (_currentData.TableSegments.Count > 0)
{
minX = Math.Min(minX, _currentData.TableSegments.Min(s => Math.Min(s.StartX, s.EndX)));
minY = Math.Min(minY, _currentData.TableSegments.Min(s => Math.Min(s.StartY, s.EndY)));
maxX = Math.Max(maxX, _currentData.TableSegments.Max(s => Math.Max(s.StartX, s.EndX)));
maxY = Math.Max(maxY, _currentData.TableSegments.Max(s => Math.Max(s.StartY, s.EndY)));
}
// 교차점도 고려
if (_currentData.IntersectionPoints.Count > 0)
{
minX = Math.Min(minX, _currentData.IntersectionPoints.Min(i => i.X));
minY = Math.Min(minY, _currentData.IntersectionPoints.Min(i => i.Y));
maxX = Math.Max(maxX, _currentData.IntersectionPoints.Max(i => i.X));
maxY = Math.Max(maxY, _currentData.IntersectionPoints.Max(i => i.Y));
}
// 대각선도 고려
if (_currentData.DiagonalLines.Count > 0)
{
minX = Math.Min(minX, _currentData.DiagonalLines.Min(d => Math.Min(d.StartX, d.EndX)));
minY = Math.Min(minY, _currentData.DiagonalLines.Min(d => Math.Min(d.StartY, d.EndY)));
maxX = Math.Max(maxX, _currentData.DiagonalLines.Max(d => Math.Max(d.StartX, d.EndX)));
maxY = Math.Max(maxY, _currentData.DiagonalLines.Max(d => Math.Max(d.StartY, d.EndY)));
}
// 정확한 셀 경계도 고려
if (_currentData.CellBoundaries != null && _currentData.CellBoundaries.Count > 0)
{
var allCellX = _currentData.CellBoundaries.SelectMany(cb => new[] { cb.TopLeftX, cb.TopRightX, cb.BottomLeftX, cb.BottomRightX });
var allCellY = _currentData.CellBoundaries.SelectMany(cb => new[] { cb.TopLeftY, cb.TopRightY, cb.BottomLeftY, cb.BottomRightY });
minX = Math.Min(minX, allCellX.Min());
minY = Math.Min(minY, allCellY.Min());
maxX = Math.Max(maxX, allCellX.Max());
maxY = Math.Max(maxY, allCellY.Max());
}
return (minX, minY, maxX, maxY);
}
private Point TransformPoint(double x, double y)
{
var bounds = CalculateBounds();
if (bounds == null) return new Point(0, 0);
var (minX, minY, maxX, maxY) = bounds.Value;
// CAD 좌표계 -> WPF 좌표계 변환 (Y축 뒤집기)
var transformedX = (x - minX) * _scale + MARGIN;
var transformedY = cnvVisualization.Height - ((y - minY) * _scale + MARGIN);
return new Point(transformedX, transformedY);
}
private void DrawNoteBounds(double minX, double minY, double maxX, double maxY)
{
var topLeft = TransformPoint(minX, maxY);
var bottomRight = TransformPoint(maxX, minY);
var rect = new Rectangle
{
Width = bottomRight.X - topLeft.X,
Height = bottomRight.Y - topLeft.Y,
Stroke = Brushes.Red,
StrokeThickness = 2,
StrokeDashArray = new DoubleCollection { 5, 5 },
Fill = null
};
Canvas.SetLeft(rect, topLeft.X);
Canvas.SetTop(rect, topLeft.Y);
cnvVisualization.Children.Add(rect);
}
private void DrawSegments()
{
if (_currentData == null) return;
foreach (var segment in _currentData.TableSegments)
{
var startPoint = TransformPoint(segment.StartX, segment.StartY);
var endPoint = TransformPoint(segment.EndX, segment.EndY);
var line = new Line
{
X1 = startPoint.X,
Y1 = startPoint.Y,
X2 = endPoint.X,
Y2 = endPoint.Y,
Stroke = segment.IsHorizontal ? Brushes.Blue : Brushes.Green,
StrokeThickness = 1
};
cnvVisualization.Children.Add(line);
}
}
private void DrawCells()
{
if (_currentData == null) return;
var colors = new[] { Brushes.Red, Brushes.Blue, Brushes.Green, Brushes.Purple, Brushes.Orange };
for (int i = 0; i < _currentData.Cells.Count; i++)
{
var cell = _currentData.Cells[i];
var topLeft = TransformPoint(cell.MinX, cell.MaxY);
var bottomRight = TransformPoint(cell.MaxX, cell.MinY);
var rect = new Rectangle
{
Width = bottomRight.X - topLeft.X,
Height = bottomRight.Y - topLeft.Y,
Stroke = colors[i % colors.Length],
StrokeThickness = 2,
Fill = null
};
Canvas.SetLeft(rect, topLeft.X);
Canvas.SetTop(rect, topLeft.Y);
cnvVisualization.Children.Add(rect);
// 셀 번호 표시
var label = new TextBlock
{
Text = $"R{cell.Row}C{cell.Column}", // 이미 1-based 인덱싱 적용됨
FontSize = 8, // 폰트 크기 조정
Foreground = colors[i % colors.Length],
FontWeight = FontWeights.Bold
};
Canvas.SetLeft(label, topLeft.X + 2); // 좌상단에 위치 + 약간의 패딩
Canvas.SetTop(label, topLeft.Y + 2); // 좌상단에 위치 + 약간의 패딩
cnvVisualization.Children.Add(label);
// 셀 텍스트 표시
if (!string.IsNullOrEmpty(cell.Text))
{
var textLabel = new TextBlock
{
Text = cell.Text,
FontSize = 8,
Foreground = Brushes.Black,
Background = Brushes.LightYellow
};
// 셀 텍스트는 셀 중앙에 표시
var centerPoint = TransformPoint(cell.CenterX, cell.CenterY);
Canvas.SetLeft(textLabel, centerPoint.X - (textLabel.ActualWidth / 2)); // 텍스트 중앙 정렬
Canvas.SetTop(textLabel, centerPoint.Y - (textLabel.ActualHeight / 2)); // 텍스트 중앙 정렬
cnvVisualization.Children.Add(textLabel);
}
}
}
private void DrawCellBoundaries()
{
if (_currentData == null || _currentData.CellBoundaries == null) return;
for (int i = 0; i < _currentData.CellBoundaries.Count; i++)
{
var cellBoundary = _currentData.CellBoundaries[i];
// 4개 모서리 좌표 변환
var topLeft = TransformPoint(cellBoundary.TopLeftX, cellBoundary.TopLeftY);
var topRight = TransformPoint(cellBoundary.TopRightX, cellBoundary.TopRightY);
var bottomLeft = TransformPoint(cellBoundary.BottomLeftX, cellBoundary.BottomLeftY);
var bottomRight = TransformPoint(cellBoundary.BottomRightX, cellBoundary.BottomRightY);
// 셀 경계를 Path로 그리기 (정확한 4개 모서리 연결)
var pathGeometry = new PathGeometry();
var pathFigure = new PathFigure();
pathFigure.StartPoint = topLeft;
pathFigure.Segments.Add(new LineSegment(topRight, true));
pathFigure.Segments.Add(new LineSegment(bottomRight, true));
pathFigure.Segments.Add(new LineSegment(bottomLeft, true));
pathFigure.IsClosed = true;
pathGeometry.Figures.Add(pathFigure);
var path = new Path
{
Data = pathGeometry,
Stroke = Brushes.DarkBlue,
StrokeThickness = 3,
Fill = null
};
cnvVisualization.Children.Add(path);
// 라벨 표시 (셀 중앙)
if (!string.IsNullOrEmpty(cellBoundary.Label))
{
var centerX = (topLeft.X + bottomRight.X) / 2;
var centerY = (topLeft.Y + bottomRight.Y) / 2;
var label = new TextBlock
{
Text = cellBoundary.Label,
FontSize = 10,
Foreground = Brushes.DarkBlue,
FontWeight = FontWeights.Bold,
Background = Brushes.LightCyan
};
Canvas.SetLeft(label, centerX - 15); // 대략적인 중앙 정렬
Canvas.SetTop(label, centerY - 6);
cnvVisualization.Children.Add(label);
}
}
}
private void DrawTexts()
{
if (_currentData == null) return;
foreach (var text in _currentData.TextEntities)
{
var point = TransformPoint(text.X, text.Y);
var textBlock = new TextBlock
{
Text = text.Text,
FontSize = 9,
Foreground = text.IsInTable ? Brushes.DarkBlue : Brushes.Gray
};
Canvas.SetLeft(textBlock, point.X);
Canvas.SetTop(textBlock, point.Y);
cnvVisualization.Children.Add(textBlock);
// 텍스트 위치 점 표시
var dot = new Ellipse
{
Width = 3,
Height = 3,
Fill = text.IsInTable ? Brushes.Blue : Brushes.Gray
};
Canvas.SetLeft(dot, point.X - 1.5);
Canvas.SetTop(dot, point.Y - 1.5);
cnvVisualization.Children.Add(dot);
}
}
private void DrawIntersections()
{
if (_currentData == null) return;
foreach (var intersection in _currentData.IntersectionPoints)
{
var point = TransformPoint(intersection.X, intersection.Y);
// 교차점 원 표시
var circle = new Ellipse
{
Width = 8,
Height = 8,
Fill = GetIntersectionColor(intersection),
Stroke = Brushes.Black,
StrokeThickness = 1
};
Canvas.SetLeft(circle, point.X - 4);
Canvas.SetTop(circle, point.Y - 4);
cnvVisualization.Children.Add(circle);
// 교차점 타입 숫자 표시 (우측에)
var numberLabel = new TextBlock
{
Text = intersection.DirectionBits.ToString(),
FontSize = 12,
FontWeight = FontWeights.Bold,
Foreground = Brushes.Black,
Background = Brushes.Yellow,
Padding = new Thickness(2)
};
Canvas.SetLeft(numberLabel, point.X + 8); // 교차점 우측에 표시
Canvas.SetTop(numberLabel, point.Y - 6); // 약간 위쪽으로 조정
cnvVisualization.Children.Add(numberLabel);
}
}
private void DrawDiagonals()
{
if (_currentData == null) return;
foreach (var diagonal in _currentData.DiagonalLines)
{
var startPoint = TransformPoint(diagonal.StartX, diagonal.StartY);
var endPoint = TransformPoint(diagonal.EndX, diagonal.EndY);
// 대각선 표시
var line = new Line
{
X1 = startPoint.X,
Y1 = startPoint.Y,
X2 = endPoint.X,
Y2 = endPoint.Y,
Stroke = Brushes.Green,
StrokeThickness = 2,
StrokeDashArray = new DoubleCollection { 5, 3 } // 점선으로 표시
};
cnvVisualization.Children.Add(line);
// 대각선 중앙에 라벨 표시
if (!string.IsNullOrEmpty(diagonal.Label))
{
var centerX = (startPoint.X + endPoint.X) / 2;
var centerY = (startPoint.Y + endPoint.Y) / 2;
var label = new TextBlock
{
Text = diagonal.Label,
FontSize = 9,
FontWeight = FontWeights.Bold,
Foreground = Brushes.Green,
Background = Brushes.White
};
Canvas.SetLeft(label, centerX - 20); // 중앙에서 약간 왼쪽으로
Canvas.SetTop(label, centerY - 10); // 중앙에서 약간 위쪽으로
cnvVisualization.Children.Add(label);
}
}
}
private System.Windows.Media.Brush GetIntersectionColor(IntersectionInfo intersection)
{
if (intersection.IsTopLeft && intersection.IsBottomRight)
return Brushes.Purple; // 둘 다 가능
else if (intersection.IsTopLeft)
return Brushes.Green; // topLeft 후보
else if (intersection.IsBottomRight)
return Brushes.Blue; // bottomRight 후보
else
return Brushes.Red; // 기타
}
private void RefreshVisualization(object sender, RoutedEventArgs e)
{
RefreshVisualization();
}
private void BtnZoomFit_Click(object sender, RoutedEventArgs e)
{
svViewer.Reset();
}
private void CnvVisualization_MouseMove(object sender, MouseEventArgs e)
{
var pos = e.GetPosition(cnvVisualization);
txtMousePos.Text = $"마우스: ({pos.X:F0}, {pos.Y:F0})";
}
}
}

View File

@@ -8,37 +8,37 @@ using System.Text.Json.Serialization;
public class MappingTableData public class MappingTableData
{ {
[JsonPropertyName("mapping_table")] [JsonPropertyName("mapping_table")]
public MappingTable MappingTable { get; set; } public MappingTable MappingTable { get; set; } = default!;
} }
public class MappingTable public class MappingTable
{ {
[JsonPropertyName("ailabel_to_systems")] [JsonPropertyName("ailabel_to_systems")]
public Dictionary<string, SystemFields> AilabelToSystems { get; set; } public Dictionary<string, SystemFields> AilabelToSystems { get; set; } = default!;
[JsonPropertyName("system_mappings")] [JsonPropertyName("system_mappings")]
public SystemMappings SystemMappings { get; set; } public SystemMappings SystemMappings { get; set; } = default!;
} }
public class SystemFields public class SystemFields
{ {
[JsonPropertyName("molit")] [JsonPropertyName("molit")]
public string Molit { get; set; } public string Molit { get; set; } = default!;
[JsonPropertyName("expressway")] [JsonPropertyName("expressway")]
public string Expressway { get; set; } public string Expressway { get; set; } = default!;
[JsonPropertyName("railway")] [JsonPropertyName("railway")]
public string Railway { get; set; } public string Railway { get; set; } = default!;
[JsonPropertyName("docaikey")] [JsonPropertyName("docaikey")]
public string DocAiKey { get; set; } public string DocAiKey { get; set; } = default!;
} }
public class SystemMappings public class SystemMappings
{ {
[JsonPropertyName("expressway_to_transportation")] [JsonPropertyName("expressway_to_transportation")]
public Dictionary<string, string> ExpresswayToTransportation { get; set; } public Dictionary<string, string> ExpresswayToTransportation { get; set; } = default!;
} }
// 필드 매퍼 클래스 // 필드 매퍼 클래스
@@ -56,21 +56,164 @@ public class FieldMapper
/// </summary> /// </summary>
public static FieldMapper LoadFromFile(string jsonFilePath) public static FieldMapper LoadFromFile(string jsonFilePath)
{ {
string jsonContent = File.ReadAllText(jsonFilePath); try
var options = new JsonSerializerOptions
{ {
PropertyNameCaseInsensitive = true, string jsonContent = File.ReadAllText(jsonFilePath, System.Text.Encoding.UTF8);
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping Console.WriteLine($"[DEBUG] 매핑 테이블 JSON 파일 크기: {jsonContent.Length} bytes");
};
// JSON 내용 정리 (주석 제거 등)
var mappingData = JsonSerializer.Deserialize<MappingTableData>(jsonContent, options); jsonContent = CleanJsonContent(jsonContent);
return new FieldMapper(mappingData);
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
AllowTrailingCommas = true
};
var mappingData = JsonSerializer.Deserialize<MappingTableData>(jsonContent, options);
Console.WriteLine($"[DEBUG] 매핑 테이블 로드 성공: {mappingData?.MappingTable?.AilabelToSystems?.Count ?? 0}개 항목");
return new FieldMapper(mappingData!);
}
catch (JsonException jsonEx)
{
Console.WriteLine($"❌ 매핑 테이블 JSON 파싱 오류: {jsonEx.Message}");
Console.WriteLine($"❌ 파일: {jsonFilePath}");
if (File.Exists(jsonFilePath))
{
string content = File.ReadAllText(jsonFilePath);
Console.WriteLine($"❌ JSON 내용 미리보기 (첫 500자):");
Console.WriteLine(content.Length > 500 ? content.Substring(0, 500) + "..." : content);
}
throw new Exception($"매핑 테이블 JSON 파일 파싱 실패: {jsonEx.Message}\n파일: {jsonFilePath}");
}
catch (Exception ex)
{
Console.WriteLine($"❌ 매핑 테이블 로드 중 오류: {ex.Message}");
throw;
}
}
/// <summary>
/// JSON 내용을 정리하여 파싱 가능한 상태로 만듭니다.
/// 주석 제거 및 기타 무효한 문자 처리
/// </summary>
/// <param name="jsonContent">원본 JSON 내용</param>
/// <returns>정리된 JSON 내용</returns>
private static string CleanJsonContent(string jsonContent)
{
if (string.IsNullOrEmpty(jsonContent))
return jsonContent;
try
{
// 줄별로 처리하여 주석 제거
var lines = jsonContent.Split('\n');
var cleanedLines = new List<string>();
bool inMultiLineComment = false;
foreach (string line in lines)
{
string processedLine = line;
// 멀티라인 주석 처리 (/* */)
if (inMultiLineComment)
{
int endIndex = processedLine.IndexOf("*/");
if (endIndex >= 0)
{
processedLine = processedLine.Substring(endIndex + 2);
inMultiLineComment = false;
}
else
{
continue; // 전체 라인이 주석
}
}
// 멀티라인 주석 시작 확인
int multiLineStart = processedLine.IndexOf("/*");
if (multiLineStart >= 0)
{
int multiLineEnd = processedLine.IndexOf("*/", multiLineStart + 2);
if (multiLineEnd >= 0)
{
// 같은 라인에서 시작하고 끝나는 주석
processedLine = processedLine.Substring(0, multiLineStart) +
processedLine.Substring(multiLineEnd + 2);
}
else
{
// 멀티라인 주석 시작
processedLine = processedLine.Substring(0, multiLineStart);
inMultiLineComment = true;
}
}
// 싱글라인 주석 제거 (//) - 문자열 내부의 //는 제외
bool inString = false;
bool escaped = false;
int commentIndex = -1;
for (int i = 0; i < processedLine.Length - 1; i++)
{
char current = processedLine[i];
char next = processedLine[i + 1];
if (escaped)
{
escaped = false;
continue;
}
if (current == '\\')
{
escaped = true;
continue;
}
if (current == '"')
{
inString = !inString;
continue;
}
if (!inString && current == '/' && next == '/')
{
commentIndex = i;
break;
}
}
if (commentIndex >= 0)
{
processedLine = processedLine.Substring(0, commentIndex);
}
// 빈 라인이 아니면 추가
if (!string.IsNullOrWhiteSpace(processedLine))
{
cleanedLines.Add(processedLine);
}
}
string result = string.Join("\n", cleanedLines);
Console.WriteLine($"[DEBUG] 매핑 테이블 JSON 정리 완료: {jsonContent.Length} -> {result.Length} bytes");
return result;
}
catch (Exception ex)
{
Console.WriteLine($"❌ 매핑 테이블 JSON 정리 중 오류: {ex.Message}");
// 정리 실패시 원본 반환
return jsonContent;
}
} }
/// <summary> /// <summary>
/// AI 라벨을 고속도로공사 필드명으로 변환 /// AI 라벨을 고속도로공사 필드명으로 변환
/// </summary> /// </summary>
public string AilabelToExpressway(string ailabel) public string? AilabelToExpressway(string ailabel)
{ {
if (_mappingData.MappingTable.AilabelToSystems.TryGetValue(ailabel, out var systemFields)) if (_mappingData.MappingTable.AilabelToSystems.TryGetValue(ailabel, out var systemFields))
{ {
@@ -82,7 +225,7 @@ public class FieldMapper
/// <summary> /// <summary>
/// AI 라벨을 DocAiKey 값으로 변환 /// AI 라벨을 DocAiKey 값으로 변환
/// </summary> /// </summary>
public string AilabelToDocAiKey(string ailabel) public string? AilabelToDocAiKey(string ailabel)
{ {
if (_mappingData.MappingTable.AilabelToSystems.TryGetValue(ailabel, out var systemFields)) if (_mappingData.MappingTable.AilabelToSystems.TryGetValue(ailabel, out var systemFields))
{ {
@@ -94,7 +237,7 @@ public class FieldMapper
/// <summary> /// <summary>
/// 고속도로공사 필드명을 교통부 필드명으로 변환 /// 고속도로공사 필드명을 교통부 필드명으로 변환
/// </summary> /// </summary>
public string ExpresswayToTransportation(string expresswayField) public string? ExpresswayToTransportation(string expresswayField)
{ {
if (_mappingData.MappingTable.SystemMappings.ExpresswayToTransportation.TryGetValue(expresswayField, out var transportationField)) if (_mappingData.MappingTable.SystemMappings.ExpresswayToTransportation.TryGetValue(expresswayField, out var transportationField))
{ {
@@ -106,7 +249,7 @@ public class FieldMapper
/// <summary> /// <summary>
/// DocAiKey 값으로부터 해당하는 AI 라벨을 반환 /// DocAiKey 값으로부터 해당하는 AI 라벨을 반환
/// </summary> /// </summary>
public string DocAiKeyToAilabel(string docAiKey) public string? DocAiKeyToAilabel(string docAiKey)
{ {
if (string.IsNullOrEmpty(docAiKey)) if (string.IsNullOrEmpty(docAiKey))
{ {
@@ -126,7 +269,7 @@ public class FieldMapper
/// <summary> /// <summary>
/// Expressway 필드값으로부터 해당하는 AI 라벨을 반환 /// Expressway 필드값으로부터 해당하는 AI 라벨을 반환
/// </summary> /// </summary>
public string ExpresswayToAilabel(string expresswayField) public string? ExpresswayToAilabel(string expresswayField)
{ {
if (string.IsNullOrEmpty(expresswayField)) if (string.IsNullOrEmpty(expresswayField))
{ {
@@ -146,7 +289,7 @@ public class FieldMapper
/// <summary> /// <summary>
/// AI 라벨 → 고속도로공사 → 교통부 순서로 변환 /// AI 라벨 → 고속도로공사 → 교통부 순서로 변환
/// </summary> /// </summary>
public string AilabelToTransportationViaExpressway(string ailabel) public string? AilabelToTransportationViaExpressway(string ailabel)
{ {
var expresswayField = AilabelToExpressway(ailabel); var expresswayField = AilabelToExpressway(ailabel);
if (!string.IsNullOrEmpty(expresswayField)) if (!string.IsNullOrEmpty(expresswayField))
@@ -159,7 +302,7 @@ public class FieldMapper
/// <summary> /// <summary>
/// AI 라벨에 해당하는 모든 시스템의 필드명을 반환 /// AI 라벨에 해당하는 모든 시스템의 필드명을 반환
/// </summary> /// </summary>
public SystemFields GetAllSystemFields(string ailabel) public SystemFields? GetAllSystemFields(string ailabel)
{ {
if (_mappingData.MappingTable.AilabelToSystems.TryGetValue(ailabel, out var systemFields)) if (_mappingData.MappingTable.AilabelToSystems.TryGetValue(ailabel, out var systemFields))
{ {
@@ -171,9 +314,9 @@ public class FieldMapper
/// <summary> /// <summary>
/// 여러 AI 라벨을 한번에 고속도로공사 필드명으로 변환 /// 여러 AI 라벨을 한번에 고속도로공사 필드명으로 변환
/// </summary> /// </summary>
public Dictionary<string, string> BatchConvertAilabelToExpressway(IEnumerable<string> ailabels) public Dictionary<string, string?> BatchConvertAilabelToExpressway(IEnumerable<string> ailabels)
{ {
var results = new Dictionary<string, string>(); var results = new Dictionary<string, string?>();
foreach (var label in ailabels) foreach (var label in ailabels)
{ {
results[label] = AilabelToExpressway(label); results[label] = AilabelToExpressway(label);
@@ -184,9 +327,9 @@ public class FieldMapper
/// <summary> /// <summary>
/// 여러 고속도로공사 필드를 한번에 교통부 필드명으로 변환 /// 여러 고속도로공사 필드를 한번에 교통부 필드명으로 변환
/// </summary> /// </summary>
public Dictionary<string, string> BatchConvertExpresswayToTransportation(IEnumerable<string> expresswayFields) public Dictionary<string, string?> BatchConvertExpresswayToTransportation(IEnumerable<string> expresswayFields)
{ {
var results = new Dictionary<string, string>(); var results = new Dictionary<string, string?>();
foreach (var field in expresswayFields) foreach (var field in expresswayFields)
{ {
results[field] = ExpresswayToTransportation(field); results[field] = ExpresswayToTransportation(field);

View File

@@ -73,13 +73,7 @@
"railway": "", "railway": "",
"docaikey": "CSCOP" "docaikey": "CSCOP"
}, },
"사업명_bot": { "설계공구_공구명": {
"molit": "",
"expressway": "TD_CNAME",
"railway": "TD_CNAME",
"docaikey": "TDCNAME"
},
"설계공구_공구명": {
"molit": "", "molit": "",
"expressway": "TD_DSECT", "expressway": "TD_DSECT",
"railway": "", "railway": "",

134
notedetectproblem.txt Normal file
View File

@@ -0,0 +1,134 @@
NOTE Detection Algorithm Context Report
Problem Summary
Successfully integrated NOTE extraction from ExportExcel_note.cs into the new modular architecture, but encountering
issues where only some NOTEs are being detected and finding their content boxes.
Current Status
✅ FIXED: No note content issue - reverted to original working cross-line intersection algorithm from ExportExcel_old.cs
🔄 ONGOING: Not detecting all NOTEs (missing notes 2 and 4 from a 4-note layout)
Architecture Overview
Key Files and Components
- Main Entry Point: Models/ExportExcel.cs:265-271 - calls note extraction in ExportAllDwgToExcelHeightSorted
- Core Algorithm: Models/DwgDataExtractor.cs:342-480 - ExtractNotesFromDrawing method
- Note Box Detection: Models/DwgDataExtractor.cs:513-569 - FindNoteBox method
- Excel Output: Models/ExcelDataWriter.cs:282-371 - WriteNoteEntities method
Current Algorithm Flow
1. Collection Phase: Gather all DBText, Polyline, and Line entities
2. NOTE Detection: Find DBText containing "NOTE" (case-insensitive)
3. Box Finding: For each NOTE, use cross-line intersection to find content box below
4. Content Extraction: Find text entities within detected boxes
5. Sorting & Grouping: Sort by coordinates (Y descending, X ascending) and group NOTE+content
6. Excel Output: Write to Excel with NOTE followed immediately by its content
Current Working Algorithm (Reverted from ExportExcel_old.cs)
FindNoteBox Method (DwgDataExtractor.cs:513-569)
// Draws horizontal search line below NOTE position
double searchY = notePos.Y - (noteHeight * 2);
var searchLineStart = new Point3d(notePos.X - noteHeight * 10, searchY, 0);
var searchLineEnd = new Point3d(notePos.X + noteHeight * 50, searchY, 0);
// 1. Check Polyline intersections
// 2. Check Line intersections and trace rectangles
// 3. Use usedBoxes HashSet to prevent duplicate assignment
IsValidNoteBox Validation (DwgDataExtractor.cs:1005-1032)
// Simple validation criteria:
// - Box must be below NOTE (box.maxPoint.Y < notePos.Y)
// - Size constraints: noteHeight < width/height < noteHeight * 100
// - Distance constraints: X distance < noteHeight * 50, Y distance < noteHeight * 10
Known Issues from Previous Sessions
Issue 1: 1/1/3/3 Duplicate Content (PREVIOUSLY FIXED)
Problem: Multiple NOTEs finding the same large spanning polyline
Root Cause: Box detection finding one large polyline spanning multiple note areas
Solution Applied: Used usedBoxes HashSet to prevent duplicate assignment
Issue 2: Reverse Note Ordering (PREVIOUSLY FIXED)
Problem: Notes written in reverse order
Solution Applied: Sort by Y descending (bigger Y = top), then X ascending
Issue 3: Wrong Note Grouping (PREVIOUSLY FIXED)
Problem: All NOTEs grouped first, then all content
Solution Applied: Group each NOTE immediately with its content
Issue 4: Missing NOTEs 2 and 4 (CURRENT ISSUE)
Problem: In a 4-note layout arranged as 1-2 (top row) and 3-4 (bottom row), only notes 1 and 3 are detected
Possible Causes:
- Search line positioning not intersecting with notes 2 and 4's content boxes
- Box validation criteria too restrictive for right-side notes
- Geometric relationship between NOTE position and content box differs for right-side notes
Debug Information Available
Last Known Debug Output (5 NOTEs detected but no content found)
[DEBUG] Note 텍스트 발견: 'NOTE' at (57.0572050838764,348.6990318186563,0)
[DEBUG] Note 텍스트 발견: 'NOTE' at (471.6194660633719,501.3393888589908,0)
[DEBUG] Note 텍스트 발견: 'NOTE' at (444.9503218738628,174.19527687737536,0)
[DEBUG] Note 텍스트 발견: 'NOTE' at (602.7327260134425,174.43523739278135,0)
[DEBUG] Note 텍스트 발견: 'NOTE' at (635.5065816693041,502.83938885945645,0)
Reference Image
- noteExample.png shows expected layout with numbered sections 1-7 in Korean text
- Shows box-structured layout where each NOTE should have corresponding content below
Key Coordinate Analysis
From debug logs, NOTEs at similar Y coordinates appear to be in pairs:
- Top Row: (444.95, 174.20) and (602.73, 174.44) - Y≈174
- Middle Row: (471.62, 501.34) and (635.51, 502.84) - Y≈502
- Single: (57.06, 348.70) - Y≈349
Pattern suggests left-right pairing where right-side NOTEs might need different search strategies.
Investigation Areas for Next Session
Priority 1: Search Line Geometry
- Analyze why horizontal search lines from right-side NOTEs don't intersect content boxes
- Consider adjusting search line direction/positioning for right-side notes
- Debug actual intersection results for missing NOTEs
Priority 2: Box Validation Criteria
- Review IsValidNoteBox distance calculations for right-side NOTEs
- Consider if content boxes for right-side NOTEs have different geometric relationships
Priority 3: Coordinate Pattern Analysis
- Investigate why NOTEs at (602.73, 174.44) and (635.51, 502.84) aren't finding content
- Compare successful vs failed NOTE positions and their content box relationships
Quick Start Commands for Next Session
1. Run existing code to see current NOTE detection results
2. Add detailed debug logging to FindNoteBox for specific coordinates: (602.73, 174.44) and (635.51, 502.84)
3. Analyze intersection results and box validation for these specific NOTEs
4. Consider geometric adjustments for right-side NOTE detection
Code State
- Current implementation in Models/DwgDataExtractor.cs uses proven cross-line intersection algorithm
- usedBoxes tracking prevents duplicate assignment
- NOTE+content grouping and Y-coordinate sorting working correctly
- Excel output formatting functional
The foundation is solid; focus should be on geometric refinements for complete NOTE detection coverage.