도면에서 표 추출

This commit is contained in:
horu2day
2025-08-12 14:33:18 +09:00
parent 3abb3c07ce
commit f114b8b642
26 changed files with 4877 additions and 2566 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,51 +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-&gt;Count())" Importance="normal" /> <Reference Include="office">
<Message Text="Other files to copy: @(FletImageAnalysisOtherFiles-&gt;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 SourceFiles="@(FletImageAnalysisJsonFiles)" DestinationFiles="@(FletImageAnalysisJsonFiles->'$(OutDir)%(RecursiveDir)%(Filename)%(Extension)')" SkipUnchangedFiles="false" OverwriteReadOnlyFiles="true" /> <Content Include="fletimageanalysis\**\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Message Text="JSON files copied (always as new): @(FletImageAnalysisJsonFiles-&gt;Count())" Importance="high" /> <Link>fletimageanalysis\%(RecursiveDir)%(FileName)%(Extension)</Link>
</Content>
</ItemGroup>
<!-- 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-&gt;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

@@ -210,6 +210,40 @@
</Style> </Style>
</Button.Style> </Button.Style>
</Button> </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>
@@ -234,6 +268,8 @@
<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">

View File

@@ -17,7 +17,7 @@ namespace DwgExtractorManual
{ {
public partial class MainWindow : Window public partial class MainWindow : Window
{ {
private DispatcherTimer _timer; private DispatcherTimer? _timer;
private ExportExcel? _exportExcel; private ExportExcel? _exportExcel;
private SqlDatas? _sqlDatas; private SqlDatas? _sqlDatas;
// 자동 처리 모드 플래그 // 자동 처리 모드 플래그
@@ -26,16 +26,76 @@ namespace DwgExtractorManual
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
InitializeDefaultPaths();
InitializeTimer(); InitializeTimer();
LoadSettings();
SetBuildTime();
// 앱 종료 시 Teigha 리소스 정리 // 앱 종료 시 Teigha 리소스 정리
this.Closed += MainWindow_Closed; this.Closed += MainWindow_Closed;
LogMessage("🚀 DWG 정보 추출기가 시작되었습니다.");
} }
private void MainWindow_Closed(object sender, EventArgs e) private void LoadSettings()
{
LogMessage("⚙️ 설정을 불러옵니다...");
var settings = SettingsManager.LoadSettings();
if (settings != null)
{
if (!string.IsNullOrEmpty(settings.SourceFolderPath) && Directory.Exists(settings.SourceFolderPath))
{
txtSourceFolder.Text = settings.SourceFolderPath;
LogMessage($"📂 저장된 소스 폴더: {settings.SourceFolderPath}");
CheckDwgFiles(settings.SourceFolderPath);
}
else
{
LogMessage($"⚠️ 저장된 소스 폴더를 찾을 수 없습니다: {settings.SourceFolderPath}");
}
if (!string.IsNullOrEmpty(settings.DestinationFolderPath) && Directory.Exists(settings.DestinationFolderPath))
{
txtResultFolder.Text = settings.DestinationFolderPath;
LogMessage($"💾 저장된 결과 폴더: {settings.DestinationFolderPath}");
}
else
{
LogMessage($"⚠️ 저장된 결과 폴더를 찾을 수 없습니다: {settings.DestinationFolderPath}");
}
if (!string.IsNullOrEmpty(settings.LastExportType))
{
if (settings.LastExportType == "Excel")
{
rbExcel.IsChecked = true;
}
else if (settings.LastExportType == "Database")
{
rbDatabase.IsChecked = true;
}
LogMessage($"📋 저장된 출력 형식: {settings.LastExportType}");
}
LogMessage("✅ 설정 불러오기 완료.");
}
else
{
LogMessage(" 저장된 설정 파일이 없습니다. 기본값으로 시작합니다.");
InitializeDefaultPaths(); // Fallback
}
}
private void SaveSettings()
{
LogMessage("⚙️ 현재 설정을 저장합니다...");
var settings = new AppSettings
{
SourceFolderPath = txtSourceFolder.Text,
DestinationFolderPath = txtResultFolder.Text,
LastExportType = rbExcel.IsChecked == true ? "Excel" : "Database"
};
SettingsManager.SaveSettings(settings);
LogMessage("✅ 설정 저장 완료.");
}
private void MainWindow_Closed(object? sender, EventArgs e)
{ {
try try
{ {
@@ -70,7 +130,13 @@ namespace DwgExtractorManual
{ {
_timer = new DispatcherTimer(); _timer = new DispatcherTimer();
_timer.Interval = TimeSpan.FromSeconds(1); _timer.Interval = TimeSpan.FromSeconds(1);
_timer.Tick += (s, e) => txtTime.Text = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); _timer.Tick += (s, e) =>
{
if (_timer != null)
{
txtTime.Text = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
}
};
_timer.Start(); _timer.Start();
} }
@@ -176,6 +242,9 @@ namespace DwgExtractorManual
private async void BtnExtract_Click(object sender, RoutedEventArgs e) private async void BtnExtract_Click(object sender, RoutedEventArgs e)
{ {
// 설정 저장
SaveSettings();
// 입력 유효성 검사 // 입력 유효성 검사
if (string.IsNullOrEmpty(txtSourceFolder.Text) || !Directory.Exists(txtSourceFolder.Text)) if (string.IsNullOrEmpty(txtSourceFolder.Text) || !Directory.Exists(txtSourceFolder.Text))
{ {
@@ -278,7 +347,7 @@ namespace DwgExtractorManual
} }
else else
{ {
System.Windows.MessageBox.Show(message, title, button, image); System.Windows.MessageBox.Show(this, message, title, button, image);
} }
} }
@@ -294,7 +363,7 @@ namespace DwgExtractorManual
} }
else else
{ {
return System.Windows.MessageBox.Show(message, title, MessageBoxButton.YesNo, MessageBoxImage.Question); return System.Windows.MessageBox.Show(this, message, title, MessageBoxButton.YesNo, MessageBoxImage.Question);
} }
} }
@@ -395,12 +464,11 @@ namespace DwgExtractorManual
LogMessage("💾 Excel 파일과 매핑 데이터를 저장합니다..."); LogMessage("💾 Excel 파일과 매핑 데이터를 저장합니다...");
// 매핑 딕셔너리를 JSON 파일로 저장 (PDF 데이터 병합용) _exportExcel?.SaveMappingDictionary(mappingDataFile);
_exportExcel.SaveMappingDictionary(mappingDataFile);
LogMessage($"✅ 매핑 데이터 저장 완료: {Path.GetFileName(mappingDataFile)}"); LogMessage($"✅ 매핑 데이터 저장 완료: {Path.GetFileName(mappingDataFile)}");
// Excel 파일 저장 // Excel 파일 저장
_exportExcel.SaveAndCloseExcel(excelFileName); _exportExcel?.SaveAndCloseExcel(excelFileName);
LogMessage($"✅ Excel 파일 저장 완료: {Path.GetFileName(excelFileName)}"); LogMessage($"✅ Excel 파일 저장 완료: {Path.GetFileName(excelFileName)}");
var elapsed = stopwatch.Elapsed; var elapsed = stopwatch.Elapsed;
@@ -461,7 +529,7 @@ namespace DwgExtractorManual
try try
{ {
// 실제 DWG 처리 로직 (DwgToDB는 실패시 true 반환) // 실제 DWG 처리 로직 (DwgToDB는 실패시 true 반환)
bool failed = _sqlDatas.DwgToDB(file.FullName); bool failed = _sqlDatas?.DwgToDB(file.FullName) ?? true;
bool success = !failed; bool success = !failed;
fileStopwatch.Stop(); fileStopwatch.Stop();
@@ -777,6 +845,9 @@ namespace DwgExtractorManual
private async void BtnPdfExtract_Click(object sender, RoutedEventArgs e) private async void BtnPdfExtract_Click(object sender, RoutedEventArgs e)
{ {
// 설정 저장
SaveSettings();
// 입력 유효성 검사 // 입력 유효성 검사
if (string.IsNullOrEmpty(txtSourceFolder.Text) || !Directory.Exists(txtSourceFolder.Text)) if (string.IsNullOrEmpty(txtSourceFolder.Text) || !Directory.Exists(txtSourceFolder.Text))
{ {
@@ -1092,7 +1163,12 @@ namespace DwgExtractorManual
LogMessage($"📄 JSON 파일 확인됨: {Path.GetFileName(jsonFilePath)}"); LogMessage($"📄 JSON 파일 확인됨: {Path.GetFileName(jsonFilePath)}");
// 최신 매핑 데이터 파일 찾기 // 최신 매핑 데이터 파일 찾기
string resultDir = Path.GetDirectoryName(csvFilePath) ?? txtResultFolder.Text; string? resultDir = Path.GetDirectoryName(csvFilePath);
if (string.IsNullOrEmpty(resultDir))
{
LogMessage("⚠️ 결과 디렉터리를 찾을 수 없습니다.");
return;
}
var mappingDataFiles = Directory.GetFiles(resultDir, "*_mapping_data.json", SearchOption.TopDirectoryOnly) var mappingDataFiles = Directory.GetFiles(resultDir, "*_mapping_data.json", SearchOption.TopDirectoryOnly)
.OrderByDescending(f => File.GetCreationTime(f)) .OrderByDescending(f => File.GetCreationTime(f))
.ToArray(); .ToArray();
@@ -1168,7 +1244,12 @@ namespace DwgExtractorManual
LogMessage($"📄 JSON 파일 확인됨: {Path.GetFileName(jsonFilePath)}"); LogMessage($"📄 JSON 파일 확인됨: {Path.GetFileName(jsonFilePath)}");
// 기존 Excel 매핑 파일 검색 (임시 파일 제외) // 기존 Excel 매핑 파일 검색 (임시 파일 제외)
string resultDir = Path.GetDirectoryName(csvFilePath) ?? txtResultFolder.Text; string? resultDir = Path.GetDirectoryName(csvFilePath);
if (string.IsNullOrEmpty(resultDir))
{
LogMessage("⚠️ 결과 디렉터리를 찾을 수 없습니다.");
return;
}
var allExcelFiles = Directory.GetFiles(resultDir, "*_Mapping.xlsx", SearchOption.TopDirectoryOnly); var allExcelFiles = Directory.GetFiles(resultDir, "*_Mapping.xlsx", SearchOption.TopDirectoryOnly);
// 임시 파일(~$로 시작하는 파일) 필터링 // 임시 파일(~$로 시작하는 파일) 필터링
@@ -1296,6 +1377,9 @@ namespace DwgExtractorManual
private async void BtnMerge_Click(object sender, RoutedEventArgs e) private async void BtnMerge_Click(object sender, RoutedEventArgs e)
{ {
// 설정 저장
SaveSettings();
// 입력 유효성 검사 // 입력 유효성 검사
if (string.IsNullOrEmpty(txtResultFolder.Text) || !Directory.Exists(txtResultFolder.Text)) if (string.IsNullOrEmpty(txtResultFolder.Text) || !Directory.Exists(txtResultFolder.Text))
{ {
@@ -1560,6 +1644,9 @@ namespace DwgExtractorManual
/// </summary> /// </summary>
private async void BtnAuto_Click(object sender, RoutedEventArgs e) private async void BtnAuto_Click(object sender, RoutedEventArgs e)
{ {
// 설정 저장
SaveSettings();
try try
{ {
// 입력 검증 // 입력 검증
@@ -1656,6 +1743,9 @@ namespace DwgExtractorManual
/// </summary> /// </summary>
private async void BtnDwgOnly_Click(object sender, RoutedEventArgs e) private async void BtnDwgOnly_Click(object sender, RoutedEventArgs e)
{ {
// 설정 저장
SaveSettings();
try try
{ {
// 경로 검증 // 경로 검증
@@ -1738,6 +1828,13 @@ namespace DwgExtractorManual
/// </summary> /// </summary>
private async void BtnDwgHeightSort_Click(object sender, RoutedEventArgs e) private async void BtnDwgHeightSort_Click(object sender, RoutedEventArgs e)
{ {
// 설정 저장
SaveSettings();
// 시각화 데이터 초기화
ClearVisualizationData();
LogMessage("🧹 시각화 데이터 초기화 완료");
try try
{ {
// 경로 검증 // 경로 검증
@@ -2057,8 +2154,8 @@ namespace DwgExtractorManual
if (allDwgFiles.Count > 0) if (allDwgFiles.Count > 0)
{ {
// 단일 Excel 파일에 모든 DWG 파일 처리 // Height 정렬 Excel 파일 생성 (Note 데이터 포함)
LogMessage("📏 단일 Height 정렬 Excel 파일 생성 중..."); LogMessage("📏 Height 정렬 Excel 파일 생성 중 (Note 표 데이터 포함)...");
await ProcessAllFilesDwgHeightSort(allDwgFiles, resultBaseFolder); await ProcessAllFilesDwgHeightSort(allDwgFiles, resultBaseFolder);
LogMessage("✅ Height 정렬 Excel 파일 생성 완료"); LogMessage("✅ Height 정렬 Excel 파일 생성 완료");
} }
@@ -2109,7 +2206,91 @@ namespace DwgExtractorManual
string savePath = Path.Combine(resultFolder, $"{timestamp}_AllDWG_HeightSorted.xlsx"); string savePath = Path.Combine(resultFolder, $"{timestamp}_AllDWG_HeightSorted.xlsx");
exportExcel.ExportAllDwgToExcelHeightSorted(allDwgFiles, savePath); exportExcel.ExportAllDwgToExcelHeightSorted(allDwgFiles, savePath);
// 시각화 데이터 캐시 초기화
MainWindow.ClearVisualizationData();
LogMessage("[DEBUG] 시각화 데이터 캐시 초기화 완료.");
foreach (var (filePath, folderName) in allDwgFiles)
{
LogMessage($"[DEBUG] DWG 파일에서 Note 추출 시작: {Path.GetFileName(filePath)}");
var noteExtractionResult = exportExcel.DwgExtractor.ExtractNotesFromDrawing(filePath);
var noteEntities = noteExtractionResult.NoteEntities;
LogMessage($"[DEBUG] 추출된 Note 엔티티 수: {noteEntities.Count}");
LogMessage($"[DEBUG] 추출된 IntersectionPoints 수: {noteExtractionResult.IntersectionPoints.Count}");
LogMessage($"[DEBUG] 추출된 DiagonalLines 수: {noteExtractionResult.DiagonalLines.Count}");
LogMessage($"[DEBUG] 추출된 TableSegments 수: {noteExtractionResult.TableSegments.Count}");
if (noteEntities.Any())
{
// 테이블이 있는 Note만 가시화 데이터 생성 (최소 4개 셀 이상)
var notesWithTables = noteEntities.Where(ne =>
ne.Type == "Note" &&
ne.Cells != null &&
ne.Cells.Count >= 4 && // 최소 4개 셀이 있어야 테이블로 인정
ne.TableSegments != null &&
ne.TableSegments.Count >= 4).ToList(); // 최소 4개 선분이 있어야 함
LogMessage($"[DEBUG] 테이블이 있는 Note: {notesWithTables.Count}개");
foreach (var noteWithTable in notesWithTables)
{
var visualizationData = new TableCellVisualizationData
{
FileName = $"{Path.GetFileName(filePath)} - {noteWithTable.Text}",
NoteText = noteWithTable.Text,
NoteBounds = (
noteWithTable.Cells.Min(c => c.MinPoint.X),
noteWithTable.Cells.Min(c => c.MinPoint.Y),
noteWithTable.Cells.Max(c => c.MaxPoint.X),
noteWithTable.Cells.Max(c => c.MaxPoint.Y)
),
Cells = noteWithTable.Cells.Select(tc => new CellBounds
{
MinX = tc.MinPoint.X, MinY = tc.MinPoint.Y, MaxX = tc.MaxPoint.X, MaxY = tc.MaxPoint.Y,
Row = tc.Row, Column = tc.Column, Text = tc.CellText
}).ToList(),
TableSegments = noteWithTable.TableSegments.Select(ts => new SegmentInfo
{
StartX = ts.StartX, StartY = ts.StartY,
EndX = ts.EndX, EndY = ts.EndY,
IsHorizontal = ts.IsHorizontal
}).ToList(),
IntersectionPoints = noteWithTable.IntersectionPoints,
DiagonalLines = noteWithTable.DiagonalLines ?? new List<DiagonalLine>(),
CellBoundaries = noteWithTable.CellBoundaries?.Select(cb => new CellBoundaryInfo
{
TopLeftX = cb.TopLeft.X, TopLeftY = cb.TopLeft.Y,
TopRightX = cb.TopRight.X, TopRightY = cb.TopRight.Y,
BottomLeftX = cb.BottomLeft.X, BottomLeftY = cb.BottomLeft.Y,
BottomRightX = cb.BottomRight.X, BottomRightY = cb.BottomRight.Y,
Label = cb.Label, Width = cb.Width, Height = cb.Height,
CellText = cb.CellText ?? ""
}).ToList() ?? new List<CellBoundaryInfo>(),
TextEntities = noteEntities.Where(ne => ne.Type == "NoteContent").Select(ne => new TextInfo
{
X = ne.X, Y = ne.Y, Text = ne.Text,
IsInTable = noteWithTable.Cells.Any(cell =>
ne.X >= cell.MinPoint.X && ne.X <= cell.MaxPoint.X &&
ne.Y >= cell.MinPoint.Y && ne.Y <= cell.MaxPoint.Y)
}).ToList()
};
MainWindow.SaveVisualizationData(visualizationData);
LogMessage($"[DEBUG] 테이블 Note 시각화 데이터 추가: {visualizationData.FileName} (셀: {visualizationData.Cells.Count}개)");
}
}
else
{
LogMessage($"[DEBUG] Note 엔티티가 없어 시각화 데이터를 생성하지 않습니다: {Path.GetFileName(filePath)}");
}
}
LogMessage($"[DEBUG] 총 {allDwgFiles.Count}개 파일의 시각화 데이터 저장 완료.");
// 최종 시각화 데이터 확인
var finalVisualizationData = MainWindow.GetVisualizationData();
LogMessage($"[DEBUG] 최종 저장된 시각화 데이터: {finalVisualizationData.Count}개 항목");
LogMessage("✅ Height 정렬 Excel 파일 생성 완료"); LogMessage("✅ Height 정렬 Excel 파일 생성 완료");
LogMessage($"📁 저장된 파일: {Path.GetFileName(savePath)}"); LogMessage($"📁 저장된 파일: {Path.GetFileName(savePath)}");
} }
@@ -2388,5 +2569,298 @@ namespace DwgExtractorManual
_sqlDatas?.Dispose(); _sqlDatas?.Dispose();
base.OnClosed(e); base.OnClosed(e);
} }
/// <summary>
/// Note 추출 기능 테스트 메서드
/// </summary>
private async Task TestNoteExtraction()
{
try
{
LogMessage("🧪 === Note 추출 기능 테스트 시작 ===");
string sourceFolder = txtSourceFolder.Text;
string resultFolder = txtResultFolder.Text;
if (string.IsNullOrEmpty(sourceFolder) || !Directory.Exists(sourceFolder))
{
LogMessage("❌ 소스 폴더가 선택되지 않았거나 존재하지 않습니다.");
UpdateStatus("소스 폴더를 선택해주세요.");
return;
}
if (string.IsNullOrEmpty(resultFolder))
{
LogMessage("❌ 결과 폴더가 선택되지 않았습니다.");
UpdateStatus("결과 폴더를 선택해주세요.");
return;
}
// 결과 폴더가 없으면 생성
if (!Directory.Exists(resultFolder))
{
Directory.CreateDirectory(resultFolder);
LogMessage($"📁 결과 폴더 생성: {resultFolder}");
}
// DWG 파일 찾기
var dwgFiles = Directory.GetFiles(sourceFolder, "*.dwg", SearchOption.AllDirectories);
LogMessage($"📊 발견된 DWG 파일 수: {dwgFiles.Length}개");
if (dwgFiles.Length == 0)
{
LogMessage("⚠️ DWG 파일이 없습니다.");
UpdateStatus("DWG 파일이 없습니다.");
return;
}
UpdateStatus("🔧 Note 추출 중...");
progressBar.Maximum = dwgFiles.Length;
progressBar.Value = 0;
// Teigha 서비스 초기화
LogMessage("🔧 Teigha 서비스 초기화 중...");
TeighaServicesManager.Instance.AcquireServices();
// 간단한 빈 매핑 데이터로 FieldMapper 생성 (테스트용)
var mappingData = new MappingTableData();
var fieldMapper = new FieldMapper(mappingData);
var extractor = new DwgDataExtractor(fieldMapper);
var csvWriter = new CsvDataWriter();
int processedCount = 0;
int successCount = 0;
int failureCount = 0;
// 각 DWG 파일에 대해 Note 추출 테스트
foreach (string dwgFile in dwgFiles.Take(3)) // 처음 3개 파일만 테스트
{
try
{
string fileName = Path.GetFileNameWithoutExtension(dwgFile);
LogMessage($"🔍 [{processedCount + 1}/{Math.Min(3, dwgFiles.Length)}] Note 추출 중: {fileName}");
// Note 데이터 추출
var noteExtractionResult = extractor.ExtractNotesFromDrawing(dwgFile);
var noteEntities = noteExtractionResult.NoteEntities;
// 테이블이 있는 Note만 시각화 데이터 저장
if (noteEntities.Any())
{
var notesWithTables = noteEntities.Where(ne =>
ne.Type == "Note" &&
ne.Cells != null &&
ne.Cells.Count >= 4 && // 최소 4개 셀이 있어야 테이블로 인정
ne.TableSegments != null &&
ne.TableSegments.Count >= 4).ToList(); // 최소 4개 선분이 있어야 함
LogMessage($" 테이블이 있는 Note: {notesWithTables.Count}개");
foreach (var noteWithTable in notesWithTables)
{
var visualizationData = new TableCellVisualizationData
{
FileName = $"{Path.GetFileName(dwgFile)} - {noteWithTable.Text}",
NoteText = noteWithTable.Text,
NoteBounds = (
noteWithTable.Cells.Min(c => c.MinPoint.X),
noteWithTable.Cells.Min(c => c.MinPoint.Y),
noteWithTable.Cells.Max(c => c.MaxPoint.X),
noteWithTable.Cells.Max(c => c.MaxPoint.Y)
),
Cells = noteWithTable.Cells.Select(tc => new CellBounds
{
MinX = tc.MinPoint.X, MinY = tc.MinPoint.Y, MaxX = tc.MaxPoint.X, MaxY = tc.MaxPoint.Y,
Row = tc.Row, Column = tc.Column, Text = tc.CellText
}).ToList(),
TableSegments = noteWithTable.TableSegments.Select(ts => new SegmentInfo
{
StartX = ts.StartX, StartY = ts.StartY,
EndX = ts.EndX, EndY = ts.EndY,
IsHorizontal = ts.IsHorizontal
}).ToList(),
IntersectionPoints = noteWithTable.IntersectionPoints,
DiagonalLines = noteExtractionResult.DiagonalLines.Where(dl =>
noteWithTable.Cells.Any(cell =>
(dl.Item1.X >= cell.MinPoint.X && dl.Item1.X <= cell.MaxPoint.X &&
dl.Item1.Y >= cell.MinPoint.Y && dl.Item1.Y <= cell.MaxPoint.Y) ||
(dl.Item2.X >= cell.MinPoint.X && dl.Item2.X <= cell.MaxPoint.X &&
dl.Item2.Y >= cell.MinPoint.Y && dl.Item2.Y <= cell.MaxPoint.Y))).Select(dl => new DiagonalLine
{
StartX = dl.Item1.X, StartY = dl.Item1.Y, EndX = dl.Item2.X, EndY = dl.Item2.Y, Label = dl.Item3
}).ToList(),
CellBoundaries = noteWithTable.CellBoundaries?.Select(cb => new CellBoundaryInfo
{
TopLeftX = cb.TopLeft.X, TopLeftY = cb.TopLeft.Y,
TopRightX = cb.TopRight.X, TopRightY = cb.TopRight.Y,
BottomLeftX = cb.BottomLeft.X, BottomLeftY = cb.BottomLeft.Y,
BottomRightX = cb.BottomRight.X, BottomRightY = cb.BottomRight.Y,
Label = cb.Label, Width = cb.Width, Height = cb.Height,
CellText = cb.CellText ?? ""
}).ToList() ?? new List<CellBoundaryInfo>(),
TextEntities = noteEntities.Where(ne => ne.Type == "NoteContent").Select(ne => new TextInfo
{
X = ne.X, Y = ne.Y, Text = ne.Text,
IsInTable = noteWithTable.Cells.Any(cell =>
ne.X >= cell.MinPoint.X && ne.X <= cell.MaxPoint.X &&
ne.Y >= cell.MinPoint.Y && ne.Y <= cell.MaxPoint.Y)
}).ToList()
};
MainWindow.SaveVisualizationData(visualizationData);
LogMessage($" ✅ 테이블 Note 시각화 데이터 저장: {visualizationData.FileName} (셀: {visualizationData.Cells.Count}개)");
}
}
LogMessage($" 추출된 엔터티: {noteEntities.Count}개");
var noteCount = noteEntities.Count(ne => ne.Type == "Note");
var contentCount = noteEntities.Count(ne => ne.Type == "NoteContent");
var tableCount = noteEntities.Count(ne => ne.Type == "Note" && !string.IsNullOrEmpty(ne.TableCsv));
LogMessage($" - Note 헤더: {noteCount}개");
LogMessage($" - Note 콘텐츠: {contentCount}개");
LogMessage($" - 테이블 포함 Note: {tableCount}개");
// CSV 파일 생성
if (noteEntities.Count > 0)
{
// Note 박스 텍스트 CSV
var noteTextCsvPath = Path.Combine(resultFolder, $"{fileName}_note_texts.csv");
csvWriter.WriteNoteBoxTextToCsv(noteEntities, noteTextCsvPath);
LogMessage($" ✅ Note 텍스트 CSV 저장: {Path.GetFileName(noteTextCsvPath)}");
// Note 테이블 CSV
var noteTableCsvPath = Path.Combine(resultFolder, $"{fileName}_note_tables.csv");
csvWriter.WriteNoteTablesToCsv(noteEntities, noteTableCsvPath);
LogMessage($" ✅ Note 테이블 CSV 저장: {Path.GetFileName(noteTableCsvPath)}");
// 통합 CSV
var combinedCsvPath = Path.Combine(resultFolder, $"{fileName}_note_combined.csv");
csvWriter.WriteNoteDataToCombinedCsv(noteEntities, combinedCsvPath);
LogMessage($" ✅ 통합 CSV 저장: {Path.GetFileName(combinedCsvPath)}");
// 개별 테이블 CSV
if (tableCount > 0)
{
var individualTablesDir = Path.Combine(resultFolder, $"{fileName}_individual_tables");
csvWriter.WriteIndividualNoteTablesCsv(noteEntities, individualTablesDir);
LogMessage($" ✅ 개별 테이블 CSV 저장: {Path.GetFileName(individualTablesDir)}");
}
// 통계 CSV
var statisticsCsvPath = Path.Combine(resultFolder, $"{fileName}_note_statistics.csv");
csvWriter.WriteNoteStatisticsToCsv(noteEntities, statisticsCsvPath);
LogMessage($" ✅ 통계 CSV 저장: {Path.GetFileName(statisticsCsvPath)}");
// 첫 번째 테이블이 있는 Note의 내용 출력 (디버깅용)
var firstTableNote = noteEntities.FirstOrDefault(ne => ne.Type == "Note" && !string.IsNullOrEmpty(ne.TableCsv));
if (firstTableNote != null)
{
LogMessage($" 📋 첫 번째 테이블 Note: '{firstTableNote.Text}'");
var tableLines = firstTableNote.TableCsv.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
LogMessage($" 📋 테이블 행 수: {tableLines.Length}");
for (int i = 0; i < Math.Min(3, tableLines.Length); i++)
{
var line = tableLines[i];
if (line.Length > 60) line = line.Substring(0, 60) + "...";
LogMessage($" 행 {i + 1}: {line}");
}
}
}
successCount++;
LogMessage($" ✅ 성공");
}
catch (Exception ex)
{
failureCount++;
LogMessage($" ❌ 실패: {ex.Message}");
}
processedCount++;
progressBar.Value = processedCount;
// UI 업데이트를 위한 지연
await Task.Delay(100);
}
LogMessage($"🧪 === Note 추출 테스트 완료 ===");
LogMessage($"📊 처리 결과: 성공 {successCount}개, 실패 {failureCount}개");
LogMessage($"💾 결과 파일들이 저장되었습니다: {resultFolder}");
UpdateStatus($"Note 추출 테스트 완료: 성공 {successCount}개, 실패 {failureCount}개");
// 결과 폴더 열기
if (successCount > 0)
{
try
{
System.Diagnostics.Process.Start("explorer.exe", resultFolder);
}
catch { }
}
}
catch (Exception ex)
{
LogMessage($"❌ Note 추출 테스트 중 오류: {ex.Message}");
UpdateStatus("Note 추출 테스트 중 오류 발생");
throw;
}
finally
{
try
{
TeighaServicesManager.Instance.ForceDisposeServices();
LogMessage("🔄 Teigha 서비스 정리 완료");
}
catch (Exception ex)
{
LogMessage($"⚠️ Teigha 서비스 정리 중 오류: {ex.Message}");
}
}
}
/// <summary>
/// 빌드 시간을 상태바에 표시합니다.
/// </summary>
private void SetBuildTime()
{
try
{
// 현재 실행 파일의 빌드 시간을 가져옵니다
var assembly = System.Reflection.Assembly.GetExecutingAssembly();
var buildDate = System.IO.File.GetLastWriteTime(assembly.Location);
txtBuildTime.Text = $"빌드: {buildDate:yyyy-MM-dd HH:mm}";
}
catch (Exception ex)
{
txtBuildTime.Text = "빌드: 알 수 없음";
LogMessage($"⚠️ 빌드 시간 조회 오류: {ex.Message}");
}
}
/// <summary>
/// 교차점 테스트 버튼 클릭 이벤트
/// </summary>
private void BtnTestIntersection_Click(object sender, RoutedEventArgs e)
{
try
{
LogMessage("🔬 교차점 생성 테스트 시작...");
UpdateStatus("교차점 테스트 중...");
// 테스트 실행
IntersectionTestDebugger.RunIntersectionTest();
LogMessage("✅ 교차점 테스트 완료 - Debug 창을 확인하세요");
UpdateStatus("교차점 테스트 완료");
}
catch (Exception ex)
{
LogMessage($"❌ 교차점 테스트 중 오류: {ex.Message}");
UpdateStatus("교차점 테스트 중 오류 발생");
}
}
} }
} }

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);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -62,6 +62,65 @@ namespace DwgExtractorManual.Models
} }
} }
/// <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> /// <summary>
/// <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>͸<EFBFBD> Excel <20><>Ʈ<EFBFBD><C6AE> <20><><EFBFBD> /// <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>͸<EFBFBD> Excel <20><>Ʈ<EFBFBD><C6AE> <20><><EFBFBD>
/// </summary> /// </summary>
@@ -144,8 +203,8 @@ namespace DwgExtractorManual.Models
for (int row = 2; row <= lastRow; row++) for (int row = 2; row <= lastRow; row++)
{ {
var cellFileName = excelManager.MappingSheet.Cells[row, 1]?.Value?.ToString() ?? ""; var cellFileName = ((Excel.Range)excelManager.MappingSheet.Cells[row, 1]).Value?.ToString() ?? "";
var cellAiLabel = excelManager.MappingSheet.Cells[row, 3]?.Value?.ToString() ?? ""; var cellAiLabel = ((Excel.Range)excelManager.MappingSheet.Cells[row, 3]).Value?.ToString() ?? "";
if (string.Equals(cellFileName.Trim(), fileName.Trim(), StringComparison.OrdinalIgnoreCase) && if (string.Equals(cellFileName.Trim(), fileName.Trim(), StringComparison.OrdinalIgnoreCase) &&
string.Equals(cellAiLabel.Trim(), aiLabel.Trim(), StringComparison.OrdinalIgnoreCase)) string.Equals(cellAiLabel.Trim(), aiLabel.Trim(), StringComparison.OrdinalIgnoreCase))
@@ -278,6 +337,7 @@ namespace DwgExtractorManual.Models
/// <summary> /// <summary>
/// Note 엔티티들을 Excel 워크시트에 기록합니다 (기존 데이터 아래에 추가). /// Note 엔티티들을 Excel 워크시트에 기록합니다 (기존 데이터 아래에 추가).
/// CellBoundary 데이터를 사용하여 병합된 셀의 텍스트를 적절히 처리합니다.
/// </summary> /// </summary>
public void WriteNoteEntities(List<NoteEntityInfo> noteEntities, Excel.Worksheet worksheet, string fileName) public void WriteNoteEntities(List<NoteEntityInfo> noteEntities, Excel.Worksheet worksheet, string fileName)
{ {
@@ -307,18 +367,28 @@ namespace DwgExtractorManual.Models
int startRow = lastRow + 2; // 한 줄 띄우고 시작 int startRow = lastRow + 2; // 한 줄 띄우고 시작
Debug.WriteLine($"[DEBUG] Note 데이터 기록 시작: {startRow}행부터 {noteEntities.Count}개 항목"); Debug.WriteLine($"[DEBUG] Note 데이터 기록 시작: {startRow}행부터 {noteEntities.Count}개 항목");
// Note 섹션 헤더 추가 (간단한 방식으로 변경) // Note 섹션 헤더 추가 (표 컬럼 포함)
try try
{ {
worksheet.Cells[startRow - 1, 1] = "=== Notes ==="; worksheet.Cells[startRow - 1, 1] = "=== Notes (with Cell Boundary Tables) ===";
worksheet.Cells[startRow - 1, 2] = ""; worksheet.Cells[startRow - 1, 2] = "";
worksheet.Cells[startRow - 1, 3] = ""; worksheet.Cells[startRow - 1, 3] = "";
worksheet.Cells[startRow - 1, 4] = ""; worksheet.Cells[startRow - 1, 4] = "";
worksheet.Cells[startRow - 1, 5] = ""; worksheet.Cells[startRow - 1, 5] = "";
worksheet.Cells[startRow - 1, 6] = ""; worksheet.Cells[startRow - 1, 6] = "";
// 표 컬럼 헤더 추가 (G열부터 최대 20개 컬럼)
for (int col = 7; col <= 26; col++) // G~Z열 (20개 컬럼)
{
worksheet.Cells[startRow - 1, col] = $"Table Col {col - 6}";
var tableHeaderCell = (Excel.Range)worksheet.Cells[startRow - 1, col];
tableHeaderCell.Font.Bold = true;
tableHeaderCell.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightBlue);
tableHeaderCell.Font.Size = 9; // 작은 폰트로 설정
}
// 헤더 스타일 적용 (개별 셀로 처리) // 헤더 스타일 적용 (개별 셀로 처리)
var headerCell = worksheet.Cells[startRow - 1, 1]; var headerCell = (Excel.Range)worksheet.Cells[startRow - 1, 1];
headerCell.Font.Bold = true; headerCell.Font.Bold = true;
headerCell.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightYellow); headerCell.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightYellow);
} }
@@ -327,29 +397,130 @@ namespace DwgExtractorManual.Models
Debug.WriteLine($"[DEBUG] Note 헤더 작성 오류: {ex.Message}"); Debug.WriteLine($"[DEBUG] Note 헤더 작성 오류: {ex.Message}");
} }
// Note 데이터 입력 (배치 방식으로 성능 향상) // Note 데이터 입력 (CellBoundary 데이터 사용)
int row = startRow; int row = startRow;
try try
{ {
foreach (var noteEntity in noteEntities) foreach (var noteEntity in noteEntities)
{ {
// 기본 Note 정보 입력 (F열까지)
worksheet.Cells[row, 1] = 0; // Height는 0으로 설정 worksheet.Cells[row, 1] = 0; // Height는 0으로 설정
worksheet.Cells[row, 2] = noteEntity.Type ?? ""; worksheet.Cells[row, 2] = noteEntity.Type ?? "";
worksheet.Cells[row, 3] = noteEntity.Layer ?? ""; worksheet.Cells[row, 3] = noteEntity.Layer ?? "";
worksheet.Cells[row, 4] = ""; // Tag는 빈 값 worksheet.Cells[row, 4] = ""; // Tag는 빈 값
worksheet.Cells[row, 5] = fileName ?? ""; worksheet.Cells[row, 5] = fileName ?? "";
worksheet.Cells[row, 6] = noteEntity.Text ?? ""; worksheet.Cells[row, 6] = noteEntity.Text ?? ""; // 일반 텍스트만 (표 데이터 제외)
int currentRow = row; // 현재 처리 중인 행 번호
// "NOTE" 타입인 경우 행 배경색 변경 // CellBoundary 데이터가 있으면 G열부터 테이블 데이터 처리
if (noteEntity.CellBoundaries != null && noteEntity.CellBoundaries.Count > 0)
{
Debug.WriteLine($"[DEBUG] CellBoundary 데이터 처리: Row {row}, 셀 수={noteEntity.CellBoundaries.Count}");
// CellBoundary의 각 셀을 해당 위치에 직접 배치
int maxTableRow = 0;
foreach (var cellBoundary in noteEntity.CellBoundaries)
{
var (cellRow, cellCol) = ParseRowColFromLabel(cellBoundary.Label);
Debug.WriteLine($"[DEBUG] CellBoundary 처리: {cellBoundary.Label} → Row={cellRow}, Col={cellCol}, Text='{cellBoundary.CellText}'");
if (cellRow > 0 && cellCol > 0)
{
// Excel에서 테이블 위치 계산:
// R1 → Note의 현재 행 (currentRow)
// R2 → Note의 다음 행 (currentRow + 1)
// C1 → G열(7), C2 → H열(8)
int excelRow = currentRow + (cellRow - 1); // R1=currentRow, R2=currentRow+1, ...
int excelCol = 7 + (cellCol - 1); // C1=G열(7), C2=H열(8), ...
Debug.WriteLine($"[DEBUG] Excel 위치: {cellBoundary.Label} → Excel[{excelRow},{excelCol}]");
// Excel 범위 체크 (최대 20개 컬럼까지)
if (excelCol <= 26) // Z열까지
{
// CellText가 비어있어도 일단 배치해보기 (디버그용)
var cellValue = string.IsNullOrEmpty(cellBoundary.CellText) ? "[빈셀]" : cellBoundary.CellText;
worksheet.Cells[excelRow, excelCol] = cellValue;
Debug.WriteLine($"[DEBUG] ✅ 셀 배치 완료: {cellBoundary.Label} → Excel[{excelRow},{excelCol}] = '{cellValue}'");
}
else
{
Debug.WriteLine($"[DEBUG] ❌ Excel 컬럼 범위 초과: {excelCol} > 26");
}
// 테이블이 차지하는 최대 행 수 추적
maxTableRow = Math.Max(maxTableRow, cellRow);
}
else
{
Debug.WriteLine($"[DEBUG] ❌ 잘못된 Row/Col: {cellBoundary.Label} → Row={cellRow}, Col={cellCol}");
}
}
// 테이블이 여러 행을 차지하는 경우 currentRow 업데이트
if (maxTableRow > 1)
{
currentRow += (maxTableRow - 1);
Debug.WriteLine($"[DEBUG] 테이블 행 수만큼 currentRow 업데이트: +{maxTableRow - 1} → {currentRow}");
}
}
else if (!string.IsNullOrEmpty(noteEntity.TableCsv))
{
// 기존 TableCsv 방식 (백업용)
var tableRows = noteEntity.TableCsv.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
Debug.WriteLine($"[DEBUG] 기존 표 데이터 처리: Row {row}, 표 행 수={tableRows.Length}");
for (int tableRowIndex = 0; tableRowIndex < tableRows.Length; tableRowIndex++)
{
var tableCells = tableRows[tableRowIndex].Split(',');
// 각 셀을 G열부터 배치 (최대 15개 컬럼까지)
for (int cellIndex = 0; cellIndex < Math.Min(tableCells.Length, 15); cellIndex++)
{
var cellValue = tableCells[cellIndex].Trim().Trim('"'); // 따옴표 제거
worksheet.Cells[currentRow, 7 + cellIndex] = cellValue; // G열(7)부터 시작
}
// 표의 첫 번째 행이 아니면 새로운 Excel 행 추가
if (tableRowIndex > 0)
{
currentRow++;
// 새로운 행에는 기본 Note 정보 복사 (Type, Layer 등)
worksheet.Cells[currentRow, 1] = 0;
worksheet.Cells[currentRow, 2] = noteEntity.Type ?? "";
worksheet.Cells[currentRow, 3] = noteEntity.Layer ?? "";
worksheet.Cells[currentRow, 4] = "";
worksheet.Cells[currentRow, 5] = fileName ?? "";
worksheet.Cells[currentRow, 6] = "(continued)"; // 연속 표시
}
Debug.WriteLine($"[DEBUG] 표 행 {tableRowIndex + 1}/{tableRows.Length}: Excel Row {currentRow}, 셀 수={tableCells.Length}");
}
}
// "NOTE" 타입인 경우 행 배경색 변경 (표 영역 포함)
if (noteEntity.Type == "Note") if (noteEntity.Type == "Note")
{ {
Excel.Range noteRowRange = worksheet.Range[worksheet.Cells[row, 1], worksheet.Cells[row, 6]]; Excel.Range noteRowRange = worksheet.Range[worksheet.Cells[row, 1], worksheet.Cells[currentRow, 26]]; // Z열까지
noteRowRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightYellow); noteRowRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightYellow);
noteRowRange.Font.Bold = true; noteRowRange.Font.Bold = true;
} }
Debug.WriteLine($"[DEBUG] Excel 기록: Row {row}, Order {noteEntity.SortOrder}, Type {noteEntity.Type}, Pos({noteEntity.X:F1},{noteEntity.Y:F1}), Text: '{noteEntity.Text}'"); 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)}");
row++;
// 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}개 항목"); Debug.WriteLine($"[DEBUG] Note 데이터 기록 완료: {row - startRow}개 항목");
@@ -377,5 +548,46 @@ namespace DwgExtractorManual.Models
throw; // 상위로 예외 전파 throw; // 상위로 예외 전파
} }
} }
/// <summary>
/// 라벨에서 Row, Col 정보를 파싱합니다.
/// 예: "R1C2" → (1, 2) 또는 "R2C2→R3C4" → (2, 2) (시작 위치 사용)
/// </summary>
private (int row, int col) ParseRowColFromLabel(string label)
{
try
{
if (string.IsNullOrEmpty(label))
return (0, 0);
// "R2C2→R3C4" 형태인 경우 시작 부분만 사용
var startPart = label;
if (label.Contains("→"))
{
startPart = label.Split('→')[0];
}
var rIndex = startPart.IndexOf('R');
var cIndex = startPart.IndexOf('C');
if (rIndex >= 0 && cIndex > rIndex)
{
var rowStr = startPart.Substring(rIndex + 1, cIndex - rIndex - 1);
var colStr = startPart.Substring(cIndex + 1);
if (int.TryParse(rowStr, out int row) && int.TryParse(colStr, out int col))
{
return (row, col);
}
}
return (0, 0);
}
catch
{
return (0, 0);
}
}
} }
} }

View File

@@ -7,7 +7,7 @@ using Excel = Microsoft.Office.Interop.Excel;
namespace DwgExtractorManual.Models namespace DwgExtractorManual.Models
{ {
/// <summary> /// <summary>
/// Excel COM <20><>ü <20><><EFBFBD><EFBFBD> <20><> <20><20>۾<EFBFBD><DBBE><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>ϴ<EFBFBD> Ŭ<><C5AC><EFBFBD><EFBFBD> /// Excel COM <20><>ü <20><><EFBFBD><EFBFBD> <20><> <20><20>۾<EFBFBD><DBBE><EFBFBD> <20><><EFBFBD><EFBFBD>ϴ<EFBFBD> Ŭ<><C5AC><EFBFBD><EFBFBD>
/// </summary> /// </summary>
internal class ExcelManager : IDisposable internal class ExcelManager : IDisposable
{ {
@@ -17,10 +17,11 @@ namespace DwgExtractorManual.Models
public Excel.Worksheet? TitleBlockSheet { get; private set; } public Excel.Worksheet? TitleBlockSheet { get; private set; }
public Excel.Worksheet? TextEntitiesSheet { get; private set; } public Excel.Worksheet? TextEntitiesSheet { get; private set; }
public Excel.Worksheet? NoteEntitiesSheet { get; private set; }
public Excel.Worksheet? MappingSheet { get; private set; } public Excel.Worksheet? MappingSheet { get; private set; }
/// <summary> /// <summary>
/// Excel <20><><EFBFBD>ø<EFBFBD><C3B8><EFBFBD><EFBFBD>̼<EFBFBD> <20><> <20><>ũ<EFBFBD><C5A9>Ʈ <20>ʱ<EFBFBD>ȭ /// Excel <20><><EFBFBD>ø<EFBFBD><C3B8><EFBFBD><EFBFBD>̼<EFBFBD> <20><> <20><>ũ<EFBFBD><C5A9>Ʈ <20>ʱ<EFBFBD>ȭ
/// </summary> /// </summary>
public void InitializeExcel() public void InitializeExcel()
{ {
@@ -28,21 +29,26 @@ namespace DwgExtractorManual.Models
{ {
var excelApp = new Excel.Application(); var excelApp = new Excel.Application();
ExcelApplication = excelApp; ExcelApplication = excelApp;
ExcelApplication.Visible = false; // WPF<50><46><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> ó<><C3B3> ExcelApplication.Visible = false; // WPF<50><46><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> ó<><C3B3>
Excel.Workbook workbook = excelApp.Workbooks.Add(); Excel.Workbook workbook = excelApp.Workbooks.Add();
TitleBlockWorkbook = workbook; TitleBlockWorkbook = workbook;
// Title Block Sheet <20><><EFBFBD><EFBFBD> (<28>⺻ Sheet1) // Title Block Sheet <20><><EFBFBD><EFBFBD> (<28>⺻ Sheet1)
TitleBlockSheet = (Excel.Worksheet)workbook.Sheets[1]; TitleBlockSheet = (Excel.Worksheet)workbook.Sheets[1];
TitleBlockSheet.Name = "Title Block"; TitleBlockSheet.Name = "Title Block";
SetupTitleBlockHeaders(); SetupTitleBlockHeaders();
// Text Entities Sheet <20>߰<EFBFBD> // Text Entities Sheet <20>߰<EFBFBD>
TextEntitiesSheet = (Excel.Worksheet)workbook.Sheets.Add(); TextEntitiesSheet = (Excel.Worksheet)workbook.Sheets.Add();
TextEntitiesSheet.Name = "Text Entities"; TextEntitiesSheet.Name = "Text Entities";
SetupTextEntitiesHeaders(); SetupTextEntitiesHeaders();
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>Ϳ<EFBFBD> <20><>ũ<EFBFBD><C5A9> <20><> <20><>Ʈ <20><><EFBFBD><EFBFBD> // 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(); MappingWorkbook = excelApp.Workbooks.Add();
MappingSheet = (Excel.Worksheet)MappingWorkbook.Sheets[1]; MappingSheet = (Excel.Worksheet)MappingWorkbook.Sheets[1];
MappingSheet.Name = "Mapping Data"; MappingSheet.Name = "Mapping Data";
@@ -50,14 +56,14 @@ namespace DwgExtractorManual.Models
} }
catch (System.Exception ex) catch (System.Exception ex)
{ {
Debug.WriteLine($"Excel <20>ʱ<EFBFBD>ȭ <20><> <20><><EFBFBD><EFBFBD> <20>߻<EFBFBD>: {ex.Message}"); Debug.WriteLine($"Excel <20>ʱ<EFBFBD>ȭ <20><> <20><><EFBFBD><EFBFBD> <20>߻<EFBFBD>: {ex.Message}");
ReleaseExcelObjects(); ReleaseExcelObjects();
throw; throw;
} }
} }
/// <summary> /// <summary>
/// <20><><EFBFBD><EFBFBD> Excel <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><>Ʈ<EFBFBD><C6AE> <20><><EFBFBD><EFBFBD> /// <20><><EFBFBD><EFBFBD> Excel <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><>Ʈ<EFBFBD><C6AE> <20><><EFBFBD><EFBFBD>
/// </summary> /// </summary>
public bool OpenExistingFile(string excelFilePath) public bool OpenExistingFile(string excelFilePath)
{ {
@@ -65,7 +71,7 @@ namespace DwgExtractorManual.Models
{ {
if (!File.Exists(excelFilePath)) if (!File.Exists(excelFilePath))
{ {
Debug.WriteLine($"? Excel <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20>ʽ<EFBFBD><CABD>ϴ<EFBFBD>: {excelFilePath}"); Debug.WriteLine($"? Excel <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20>ʽ<EFBFBD><CABD>ϴ<EFBFBD>: {excelFilePath}");
return false; return false;
} }
@@ -80,7 +86,7 @@ namespace DwgExtractorManual.Models
if (MappingSheet == null) if (MappingSheet == null)
{ {
Debug.WriteLine("? 'Mapping Data' <20><>Ʈ<EFBFBD><C6AE> ã<><C3A3> <20><> <20><><EFBFBD><EFBFBD><EFBFBD>ϴ<EFBFBD>."); Debug.WriteLine("? 'Mapping Data' <20><>Ʈ<EFBFBD><C6AE> ã<><C3A3> <20><> <20><><EFBFBD><EFBFBD><EFBFBD>ϴ<EFBFBD>.");
return false; return false;
} }
@@ -88,13 +94,13 @@ namespace DwgExtractorManual.Models
} }
catch (System.Exception ex) catch (System.Exception ex)
{ {
Debug.WriteLine($"? Excel <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD>: {ex.Message}"); Debug.WriteLine($"? Excel <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD>: {ex.Message}");
return false; return false;
} }
} }
/// <summary> /// <summary>
/// <20><><EFBFBD>ο<EFBFBD> <20><>ũ<EFBFBD><C5A9> <20><><EFBFBD><EFBFBD> (Height <20><><EFBFBD>Ŀ<EFBFBD>) /// <20><><EFBFBD>ο<EFBFBD> <20><>ũ<EFBFBD><C5A9> <20><><EFBFBD><EFBFBD> (Height <20><><EFBFBD>Ŀ<EFBFBD>)
/// </summary> /// </summary>
public Excel.Workbook CreateNewWorkbook() public Excel.Workbook CreateNewWorkbook()
{ {
@@ -106,62 +112,83 @@ namespace DwgExtractorManual.Models
return ExcelApplication.Workbooks.Add(); return ExcelApplication.Workbooks.Add();
} }
// Title Block <20><>Ʈ <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> // Title Block <20><>Ʈ <20><><EFBFBD> <20><><EFBFBD><EFBFBD>
private void SetupTitleBlockHeaders() private void SetupTitleBlockHeaders()
{ {
if (TitleBlockSheet == null) return; if (TitleBlockSheet == null) return;
TitleBlockSheet.Cells[1, 1] = "Type"; // <20><>: AttributeReference, AttributeDefinition TitleBlockSheet.Cells[1, 1] = "Type"; // <20><>: AttributeReference, AttributeDefinition
TitleBlockSheet.Cells[1, 2] = "Name"; // BlockReference <20≯<EFBFBD> <20>Ǵ<EFBFBD> BlockDefinition <20≯<EFBFBD> TitleBlockSheet.Cells[1, 2] = "Name"; // BlockReference <20≯<EFBFBD> <20>Ǵ<EFBFBD> BlockDefinition <20≯<EFBFBD>
TitleBlockSheet.Cells[1, 3] = "Tag"; // Attribute Tag TitleBlockSheet.Cells[1, 3] = "Tag"; // Attribute Tag
TitleBlockSheet.Cells[1, 4] = "Prompt"; // Attribute Prompt TitleBlockSheet.Cells[1, 4] = "Prompt"; // Attribute Prompt
TitleBlockSheet.Cells[1, 5] = "Value"; // Attribute <20><> (TextString) TitleBlockSheet.Cells[1, 5] = "Value"; // Attribute <20><> (TextString)
TitleBlockSheet.Cells[1, 6] = "Path"; // <20><><EFBFBD><EFBFBD> DWG <20><><EFBFBD><EFBFBD> <20><>ü <20><><EFBFBD><EFBFBD> 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> TitleBlockSheet.Cells[1, 7] = "FileName"; // <20><><EFBFBD><EFBFBD> DWG <20><><EFBFBD><EFBFBD> <20≯<EFBFBD><CCB8><EFBFBD>
// <20><><EFBFBD><EFBFBD> <20><> <20><>Ÿ<EFBFBD><C5B8> // <20><><EFBFBD> <20><> <20><>Ÿ<EFBFBD><C5B8>
Excel.Range headerRange = TitleBlockSheet.Range["A1:G1"]; Excel.Range headerRange = TitleBlockSheet.Range["A1:G1"];
headerRange.Font.Bold = true; headerRange.Font.Bold = true;
headerRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightBlue); headerRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightBlue);
} }
// Text Entities <20><>Ʈ <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> // Text Entities <20><>Ʈ <20><><EFBFBD> <20><><EFBFBD><EFBFBD>
private void SetupTextEntitiesHeaders() private void SetupTextEntitiesHeaders()
{ {
if (TextEntitiesSheet == null) return; if (TextEntitiesSheet == null) return;
TextEntitiesSheet.Cells[1, 1] = "Type"; // DBText, MText TextEntitiesSheet.Cells[1, 1] = "Type"; // DBText, MText
TextEntitiesSheet.Cells[1, 2] = "Layer"; // Layer <20≯<EFBFBD> TextEntitiesSheet.Cells[1, 2] = "Layer"; // Layer <20≯<EFBFBD>
TextEntitiesSheet.Cells[1, 3] = "Text"; // <20><><EFBFBD><EFBFBD> <20>ؽ<EFBFBD>Ʈ <20><><EFBFBD><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><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> TextEntitiesSheet.Cells[1, 5] = "FileName"; // <20><><EFBFBD><EFBFBD> DWG <20><><EFBFBD><EFBFBD> <20≯<EFBFBD><CCB8><EFBFBD>
// <20><><EFBFBD><EFBFBD> <20><> <20><>Ÿ<EFBFBD><C5B8> // <20><><EFBFBD> <20><> <20><>Ÿ<EFBFBD><C5B8>
Excel.Range headerRange = TextEntitiesSheet.Range["A1:E1"]; Excel.Range headerRange = TextEntitiesSheet.Range["A1:E1"];
headerRange.Font.Bold = true; headerRange.Font.Bold = true;
headerRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightGreen); headerRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightGreen);
} }
// <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><>Ʈ <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> // <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><>Ʈ <20><><EFBFBD> <20><><EFBFBD><EFBFBD>
private void SetupMappingHeaders() private void SetupMappingHeaders()
{ {
if (MappingSheet == null) return; if (MappingSheet == null) return;
MappingSheet.Cells[1, 1] = "FileName"; // <20><><EFBFBD><EFBFBD> <20≯<EFBFBD> MappingSheet.Cells[1, 1] = "FileName"; // <20><><EFBFBD><EFBFBD> <20≯<EFBFBD>
MappingSheet.Cells[1, 2] = "MapKey"; // <20><><EFBFBD><EFBFBD> Ű MappingSheet.Cells[1, 2] = "MapKey"; // <20><><EFBFBD><EFBFBD> Ű
MappingSheet.Cells[1, 3] = "AILabel"; // AI <20><><EFBFBD><EFBFBD> MappingSheet.Cells[1, 3] = "AILabel"; // AI <20><>
MappingSheet.Cells[1, 4] = "DwgTag"; // DWG Tag MappingSheet.Cells[1, 4] = "DwgTag"; // DWG Tag
MappingSheet.Cells[1, 5] = "Att_value"; // DWG <20><> MappingSheet.Cells[1, 5] = "Att_value"; // DWG <20><>
MappingSheet.Cells[1, 6] = "Pdf_value"; // PDF <20><> (<28><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><>) MappingSheet.Cells[1, 6] = "Pdf_value"; // PDF <20><> (<28><><EFBFBD><EFBFBD><EFBFBD> <20><> <20><>)
// <20><><EFBFBD><EFBFBD> <20><> <20><>Ÿ<EFBFBD><C5B8> // <20><><EFBFBD> <20><> <20><>Ÿ<EFBFBD><C5B8>
Excel.Range headerRange = MappingSheet.Range["A1:F1"]; Excel.Range headerRange = MappingSheet.Range["A1:F1"];
headerRange.Font.Bold = true; headerRange.Font.Bold = true;
headerRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightYellow); 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> /// <summary>
/// <20><>ũ<EFBFBD><C5A9> <20><><EFBFBD><EFBFBD> /// <20><>ũ<EFBFBD><C5A9> <20><><EFBFBD><EFBFBD>
/// </summary> /// </summary>
public bool SaveWorkbook(Excel.Workbook? workbook = null) public bool SaveWorkbook(Excel.Workbook? workbook = null)
{ {
@@ -189,13 +216,13 @@ namespace DwgExtractorManual.Models
} }
catch (System.Exception ex) catch (System.Exception ex)
{ {
Debug.WriteLine($"? Excel <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD>: {ex.Message}"); Debug.WriteLine($"? Excel <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD>: {ex.Message}");
return false; return false;
} }
} }
/// <summary> /// <summary>
/// <20><>ũ<EFBFBD><C5A9><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD>ο<EFBFBD> <20><><EFBFBD><EFBFBD> /// <20><>ũ<EFBFBD><C5A9><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><>ο<EFBFBD> <20><><EFBFBD><EFBFBD>
/// </summary> /// </summary>
public void SaveWorkbookAs(Excel.Workbook? workbook, string savePath) public void SaveWorkbookAs(Excel.Workbook? workbook, string savePath)
{ {
@@ -213,14 +240,14 @@ namespace DwgExtractorManual.Models
} }
/// <summary> /// <summary>
/// Excel <20><>Ʈ<EFBFBD><C6AE><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20>ִ<EFBFBD> <20><>ȿ<EFBFBD><C8BF> <20≯<EFBFBD><CCB8><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>մϴ<D5B4>. /// 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> /// </summary>
public string GetValidSheetName(string originalName) public string GetValidSheetName(string originalName)
{ {
if (string.IsNullOrEmpty(originalName)) if (string.IsNullOrEmpty(originalName))
return "Sheet"; return "Sheet";
// Excel <20><>Ʈ<EFBFBD><C6AE><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20>ʴ<EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> // Excel <20><>Ʈ<EFBFBD><C6AE><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20>ʴ<EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>
string validName = originalName; string validName = originalName;
char[] invalidChars = { '\\', '/', '?', '*', '[', ']', ':' }; char[] invalidChars = { '\\', '/', '?', '*', '[', ']', ':' };
@@ -229,7 +256,7 @@ namespace DwgExtractorManual.Models
validName = validName.Replace(c, '_'); validName = validName.Replace(c, '_');
} }
// 31<33>ڷ<EFBFBD> <20><><EFBFBD><EFBFBD> (Excel <20><>Ʈ<EFBFBD><C6AE> <20>ִ<EFBFBD> <20><><EFBFBD><EFBFBD>) // 31<33>ڷ<EFBFBD> <20><><EFBFBD><EFBFBD> (Excel <20><>Ʈ<EFBFBD><C6AE> <20>ִ<EFBFBD> <20><><EFBFBD><EFBFBD>)
if (validName.Length > 31) if (validName.Length > 31)
{ {
validName = validName.Substring(0, 31); validName = validName.Substring(0, 31);
@@ -261,6 +288,7 @@ namespace DwgExtractorManual.Models
{ {
ReleaseComObject(TitleBlockSheet); ReleaseComObject(TitleBlockSheet);
ReleaseComObject(TextEntitiesSheet); ReleaseComObject(TextEntitiesSheet);
ReleaseComObject(NoteEntitiesSheet);
ReleaseComObject(MappingSheet); ReleaseComObject(MappingSheet);
ReleaseComObject(TitleBlockWorkbook); ReleaseComObject(TitleBlockWorkbook);
ReleaseComObject(MappingWorkbook); ReleaseComObject(MappingWorkbook);
@@ -268,6 +296,7 @@ namespace DwgExtractorManual.Models
TitleBlockSheet = null; TitleBlockSheet = null;
TextEntitiesSheet = null; TextEntitiesSheet = null;
NoteEntitiesSheet = null;
MappingSheet = null; MappingSheet = null;
TitleBlockWorkbook = null; TitleBlockWorkbook = null;
MappingWorkbook = null; MappingWorkbook = null;
@@ -285,7 +314,7 @@ namespace DwgExtractorManual.Models
} }
catch (System.Exception) catch (System.Exception)
{ {
// <20><><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD> <20>߻<EFBFBD> <20><> <20><><EFBFBD><EFBFBD> // <20><><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD> <20>߻<EFBFBD> <20><> <20><><EFBFBD><EFBFBD>
} }
} }
@@ -293,16 +322,16 @@ namespace DwgExtractorManual.Models
{ {
try try
{ {
Debug.WriteLine("[DEBUG] ExcelManager Dispose <20><><EFBFBD><EFBFBD>"); Debug.WriteLine("[DEBUG] ExcelManager Dispose <20><><EFBFBD><EFBFBD>");
CloseWorkbooks(); CloseWorkbooks();
ReleaseExcelObjects(); ReleaseExcelObjects();
GC.Collect(); GC.Collect();
GC.WaitForPendingFinalizers(); GC.WaitForPendingFinalizers();
Debug.WriteLine("[DEBUG] ExcelManager Dispose <20>Ϸ<EFBFBD>"); Debug.WriteLine("[DEBUG] ExcelManager Dispose <20>Ϸ<EFBFBD>");
} }
catch (System.Exception ex) catch (System.Exception ex)
{ {
Debug.WriteLine($"[DEBUG] ExcelManager Dispose <20><> <20><><EFBFBD><EFBFBD>: {ex.Message}"); Debug.WriteLine($"[DEBUG] ExcelManager Dispose <20><> <20><><EFBFBD><EFBFBD>: {ex.Message}");
} }
} }
} }

View File

@@ -17,7 +17,7 @@ namespace DwgExtractorManual.Models
{ {
// 컴포넌트들 // 컴포넌트들
private readonly ExcelManager excelManager; private readonly ExcelManager excelManager;
private readonly DwgDataExtractor dwgExtractor; public readonly DwgDataExtractor DwgExtractor;
private readonly JsonDataProcessor jsonProcessor; private readonly JsonDataProcessor jsonProcessor;
private readonly ExcelDataWriter excelWriter; private readonly ExcelDataWriter excelWriter;
private readonly FieldMapper fieldMapper; private readonly FieldMapper fieldMapper;
@@ -29,7 +29,7 @@ namespace DwgExtractorManual.Models
private Dictionary<string, Dictionary<string, (string, string, string, string)>> FileToMapkeyToLabelTagValuePdf private Dictionary<string, Dictionary<string, (string, string, string, string)>> FileToMapkeyToLabelTagValuePdf
= new Dictionary<string, Dictionary<string, (string, string, string, string)>>(); = new Dictionary<string, Dictionary<string, (string, string, string, string)>>();
readonly List<string> MapKeys; readonly List<string>? MapKeys;
/// <summary> /// <summary>
/// 생성자: 모든 컴포넌트 초기화 /// 생성자: 모든 컴포넌트 초기화
@@ -41,7 +41,7 @@ namespace DwgExtractorManual.Models
Debug.WriteLine("🔄 FieldMapper 로딩 중: mapping_table_json.json..."); Debug.WriteLine("🔄 FieldMapper 로딩 중: mapping_table_json.json...");
fieldMapper = FieldMapper.LoadFromFile("fletimageanalysis/mapping_table_json.json"); fieldMapper = FieldMapper.LoadFromFile("fletimageanalysis/mapping_table_json.json");
Debug.WriteLine("✅ FieldMapper 로딩 성공"); Debug.WriteLine("✅ FieldMapper 로딩 성공");
MapKeys = fieldMapper.GetAllDocAiKeys(); MapKeys = fieldMapper.GetAllDocAiKeys() ?? new List<string>();
Debug.WriteLine($"📊 총 DocAI 키 개수: {MapKeys?.Count ?? 0}"); Debug.WriteLine($"📊 총 DocAI 키 개수: {MapKeys?.Count ?? 0}");
// 매핑 테스트 (디버깅용) // 매핑 테스트 (디버깅용)
@@ -52,7 +52,7 @@ namespace DwgExtractorManual.Models
// 컴포넌트들 초기화 // 컴포넌트들 초기화
excelManager = new ExcelManager(); excelManager = new ExcelManager();
dwgExtractor = new DwgDataExtractor(fieldMapper); DwgExtractor = new DwgDataExtractor(fieldMapper);
jsonProcessor = new JsonDataProcessor(); jsonProcessor = new JsonDataProcessor();
excelWriter = new ExcelDataWriter(excelManager); excelWriter = new ExcelDataWriter(excelManager);
@@ -88,7 +88,7 @@ namespace DwgExtractorManual.Models
try try
{ {
// DWG 데이터 추출 // DWG 데이터 추출
var extractionResult = dwgExtractor.ExtractFromDwgFile(filePath, progress, cancellationToken); var extractionResult = DwgExtractor.ExtractFromDwgFile(filePath, progress, cancellationToken);
if (extractionResult == null) if (extractionResult == null)
{ {
@@ -191,14 +191,14 @@ namespace DwgExtractorManual.Models
try try
{ {
var worksheet = firstSheetProcessed ? Microsoft.Office.Interop.Excel.Worksheet worksheet = firstSheetProcessed ?
heightSortedWorkbook.Worksheets.Add() : (Microsoft.Office.Interop.Excel.Worksheet)heightSortedWorkbook.Worksheets.Add() :
(Microsoft.Office.Interop.Excel.Worksheet)heightSortedWorkbook.Worksheets[1]; (Microsoft.Office.Interop.Excel.Worksheet)heightSortedWorkbook.Worksheets[1];
worksheet.Name = excelManager.GetValidSheetName(fileName); worksheet.Name = excelManager.GetValidSheetName(fileName);
firstSheetProcessed = true; firstSheetProcessed = true;
var textEntities = dwgExtractor.ExtractTextEntitiesWithHeight(dwgFile); var textEntities = DwgExtractor.ExtractTextEntitiesWithHeight(dwgFile);
excelWriter.WriteHeightSortedData(textEntities, worksheet, fileName); excelWriter.WriteHeightSortedData(textEntities, worksheet, fileName);
Debug.WriteLine($"[DEBUG] {fileName} 시트 완료: {textEntities.Count}개 엔티티"); Debug.WriteLine($"[DEBUG] {fileName} 시트 완료: {textEntities.Count}개 엔티티");
@@ -212,7 +212,7 @@ namespace DwgExtractorManual.Models
if (!firstSheetProcessed) if (!firstSheetProcessed)
{ {
var defaultSheet = (Microsoft.Office.Interop.Excel.Worksheet)heightSortedWorkbook.Worksheets[1]; Microsoft.Office.Interop.Excel.Worksheet defaultSheet = (Microsoft.Office.Interop.Excel.Worksheet)heightSortedWorkbook.Worksheets[1];
defaultSheet.Name = "No_DWG_Files"; defaultSheet.Name = "No_DWG_Files";
defaultSheet.Cells[1, 1] = "No DWG files found in this folder"; defaultSheet.Cells[1, 1] = "No DWG files found in this folder";
} }
@@ -238,6 +238,10 @@ namespace DwgExtractorManual.Models
{ {
Debug.WriteLine($"[DEBUG] 단일 Excel 파일로 Height 정렬 생성 시작: {allDwgFiles.Count}개 파일"); Debug.WriteLine($"[DEBUG] 단일 Excel 파일로 Height 정렬 생성 시작: {allDwgFiles.Count}개 파일");
// 시각화 데이터 초기화
MainWindow.ClearVisualizationData();
Debug.WriteLine("[VISUALIZATION] 시각화 데이터 초기화 완료");
var heightSortedWorkbook = excelManager.CreateNewWorkbook(); var heightSortedWorkbook = excelManager.CreateNewWorkbook();
bool firstSheetProcessed = false; bool firstSheetProcessed = false;
@@ -252,22 +256,22 @@ namespace DwgExtractorManual.Models
try try
{ {
var worksheet = firstSheetProcessed ? Microsoft.Office.Interop.Excel.Worksheet worksheet = firstSheetProcessed ?
heightSortedWorkbook.Worksheets.Add() : (Microsoft.Office.Interop.Excel.Worksheet)heightSortedWorkbook.Worksheets.Add() :
(Microsoft.Office.Interop.Excel.Worksheet)heightSortedWorkbook.Worksheets[1]; (Microsoft.Office.Interop.Excel.Worksheet)heightSortedWorkbook.Worksheets[1];
worksheet.Name = excelManager.GetValidSheetName(fileName); worksheet.Name = excelManager.GetValidSheetName(fileName);
firstSheetProcessed = true; firstSheetProcessed = true;
var textEntities = dwgExtractor.ExtractTextEntitiesWithHeight(filePath); var textEntities = DwgExtractor.ExtractTextEntitiesWithHeight(filePath);
excelWriter.WriteHeightSortedData(textEntities, worksheet, fileName); excelWriter.WriteHeightSortedData(textEntities, worksheet, fileName);
// Note 엔티티 추출 및 기록 // Note 엔티티 추출 및 기록
var noteEntities = dwgExtractor.ExtractNotesFromDrawing(filePath); var noteEntities = DwgExtractor.ExtractNotesFromDrawing(filePath);
if (noteEntities.Count > 0) if (noteEntities.NoteEntities.Count > 0)
{ {
excelWriter.WriteNoteEntities(noteEntities, worksheet, fileName); excelWriter.WriteNoteEntities(noteEntities.NoteEntities, worksheet, fileName);
Debug.WriteLine($"[DEBUG] {fileName}: {noteEntities.Count}개 Note 엔티티 추가됨"); Debug.WriteLine($"[DEBUG] {fileName}: {noteEntities.NoteEntities.Count}개 Note 엔티티 추가됨");
} }
Debug.WriteLine($"[DEBUG] {fileName} 시트 완료: {textEntities.Count}개 엔티티"); Debug.WriteLine($"[DEBUG] {fileName} 시트 완료: {textEntities.Count}개 엔티티");
@@ -281,7 +285,7 @@ namespace DwgExtractorManual.Models
if (!firstSheetProcessed) if (!firstSheetProcessed)
{ {
var defaultSheet = (Microsoft.Office.Interop.Excel.Worksheet)heightSortedWorkbook.Worksheets[1]; Microsoft.Office.Interop.Excel.Worksheet defaultSheet = (Microsoft.Office.Interop.Excel.Worksheet)heightSortedWorkbook.Worksheets[1];
defaultSheet.Name = "No_DWG_Files"; defaultSheet.Name = "No_DWG_Files";
defaultSheet.Cells[1, 1] = "No DWG files found in any folder"; defaultSheet.Cells[1, 1] = "No DWG files found in any folder";
} }

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;
}
}
}

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 데이터 분석 완료 ===");
}
}
}

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,7 +15,7 @@ namespace DwgExtractorManual.Models
/// </summary> /// </summary>
internal sealed class SqlDatas : IDisposable internal sealed class SqlDatas : IDisposable
{ {
Services appServices; // ODA 제품 활성화용 (managed by singleton) Services? appServices; // ODA 제품 활성화용 (managed by singleton)
readonly string connectionString = "Host=localhost;Database=postgres;Username=postgres;Password=Qwer1234"; readonly string connectionString = "Host=localhost;Database=postgres;Username=postgres;Password=Qwer1234";
void InitializeTeighaServices() void InitializeTeighaServices()
@@ -143,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();
} }
@@ -162,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();
} }
@@ -231,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();
} }

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

@@ -10,8 +10,8 @@ namespace DwgExtractorManual.Models
public sealed class TeighaServicesManager public sealed class TeighaServicesManager
{ {
private static readonly object _lock = new object(); private static readonly object _lock = new object();
private static TeighaServicesManager _instance = null; private static TeighaServicesManager? _instance = null;
private static Services _services = null; private static Services? _services = null;
private static int _referenceCount = 0; private static int _referenceCount = 0;
private static bool _isActivated = false; private static bool _isActivated = false;

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!;
} }
// 필드 매퍼 클래스 // 필드 매퍼 클래스
@@ -73,7 +73,7 @@ public class FieldMapper
var mappingData = JsonSerializer.Deserialize<MappingTableData>(jsonContent, options); var mappingData = JsonSerializer.Deserialize<MappingTableData>(jsonContent, options);
Console.WriteLine($"[DEBUG] 매핑 테이블 로드 성공: {mappingData?.MappingTable?.AilabelToSystems?.Count ?? 0}개 항목"); Console.WriteLine($"[DEBUG] 매핑 테이블 로드 성공: {mappingData?.MappingTable?.AilabelToSystems?.Count ?? 0}개 항목");
return new FieldMapper(mappingData); return new FieldMapper(mappingData!);
} }
catch (JsonException jsonEx) catch (JsonException jsonEx)
{ {
@@ -213,7 +213,7 @@ public class FieldMapper
/// <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))
{ {
@@ -225,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))
{ {
@@ -237,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))
{ {
@@ -249,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))
{ {
@@ -269,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))
{ {
@@ -289,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))
@@ -302,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))
{ {
@@ -314,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);
@@ -327,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);