Compare commits
16 Commits
348ebd1158
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52fbc1c967 | ||
|
|
f114b8b642 | ||
|
|
3abb3c07ce | ||
|
|
107eab90fa | ||
|
|
9c76b624bf | ||
|
|
0278688b28 | ||
|
|
22aa118316 | ||
|
|
5ead0e8045 | ||
|
|
66dd64306c | ||
|
|
8bd5d9580c | ||
|
|
9b94b59c49 | ||
|
|
ddb4a1c408 | ||
|
|
24b5ab9686 | ||
|
|
a87644d8be | ||
|
|
b13e981d04 | ||
|
|
5282927833 |
20
.claude/settings.local.json
Normal file
20
.claude/settings.local.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(dotnet build)",
|
||||||
|
"Bash(\"C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\MSBuild\\Current\\Bin\\MSBuild.exe\" DwgExtractorManual.csproj -p:Configuration=Debug -p:Platform=x64)",
|
||||||
|
"Bash(mkdir:*)",
|
||||||
|
"Bash(where msbuild)",
|
||||||
|
"Bash(\"C:\\Program Files\\Microsoft Visual Studio\\2022\\Professional\\MSBuild\\Current\\Bin\\amd64\\MSBuild.exe\" DwgExtractorManual.csproj -p:Configuration=Debug -p:Platform=x64)",
|
||||||
|
"Bash(dotnet run:*)",
|
||||||
|
"Bash(echo $HOME)",
|
||||||
|
"Bash(find:*)",
|
||||||
|
"Bash(dotnet clean:*)",
|
||||||
|
"Bash(dotnet build:*)",
|
||||||
|
"Bash(taskkill:*)",
|
||||||
|
"Bash(wmic process where ProcessId=17428 delete:*)"
|
||||||
|
],
|
||||||
|
"deny": []
|
||||||
|
},
|
||||||
|
"contextFileName": "AGENTS.md"
|
||||||
|
}
|
||||||
85
AGENTS.md
Normal file
85
AGENTS.md
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
This is a C# WPF application that extracts data from DWG (AutoCAD) files and processes them using AI analysis. The application has two main components:
|
||||||
|
|
||||||
|
1. **C# WPF Application** (`DwgExtractorManual`) - Main GUI application for DWG processing
|
||||||
|
2. **Python Analysis Module** (`fletimageanalysis`) - AI-powered document analysis using Gemini API
|
||||||
|
|
||||||
|
## Build and Development Commands
|
||||||
|
|
||||||
|
### C# Application
|
||||||
|
```bash
|
||||||
|
# Build the application
|
||||||
|
dotnet build
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
dotnet run
|
||||||
|
|
||||||
|
# Clean build artifacts
|
||||||
|
dotnet clean
|
||||||
|
|
||||||
|
# Publish for deployment
|
||||||
|
dotnet publish -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python Module Setup
|
||||||
|
```bash
|
||||||
|
# Run the cleanup and setup script (Windows)
|
||||||
|
cleanup_and_setup.bat
|
||||||
|
|
||||||
|
# Or manually setup Python environment
|
||||||
|
cd fletimageanalysis
|
||||||
|
python -m venv venv
|
||||||
|
call venv\Scripts\activate.bat
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python CLI Usage
|
||||||
|
```bash
|
||||||
|
# Batch process files via CLI
|
||||||
|
cd fletimageanalysis
|
||||||
|
python batch_cli.py --files "file1.pdf,file2.dxf" --schema "한국도로공사" --concurrent 3 --output "results.csv"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### C# Component Structure
|
||||||
|
- **MainWindow.xaml.cs** - Main WPF window and UI logic
|
||||||
|
- **Models/DwgDataExtractor.cs** - Core DWG file processing using Teigha SDK
|
||||||
|
- **Models/ExcelDataWriter.cs** - Excel output generation using Office Interop
|
||||||
|
- **Models/TeighaServicesManager.cs** - Singleton manager for Teigha SDK lifecycle
|
||||||
|
- **Models/FieldMapper.cs** - Maps extracted data to target formats
|
||||||
|
- **Models/SettingsManager.cs** - Application configuration management
|
||||||
|
|
||||||
|
### Python Component Structure
|
||||||
|
- **batch_cli.py** - Command-line interface for batch processing
|
||||||
|
- **multi_file_processor.py** - Orchestrates multi-file processing workflows
|
||||||
|
- **gemini_analyzer.py** - AI analysis using Google Gemini API
|
||||||
|
- **pdf_processor.py** - PDF document processing
|
||||||
|
- **dxf_processor.py** - DXF file processing
|
||||||
|
- **csv_exporter.py** - CSV output generation
|
||||||
|
|
||||||
|
### Key Dependencies
|
||||||
|
- **Teigha SDK** - DWG file reading and CAD entity processing (requires DLL files in specific path)
|
||||||
|
- **Microsoft Office Interop** - Excel file generation
|
||||||
|
- **Npgsql** - PostgreSQL database connectivity
|
||||||
|
- **Google Gemini API** - AI-powered document analysis
|
||||||
|
- **PyMuPDF** - PDF processing in Python component
|
||||||
|
|
||||||
|
## Current Development Focus
|
||||||
|
|
||||||
|
The project is undergoing a **Note Detection Refactor** (see `NoteDetectionRefactor.md`):
|
||||||
|
- Replacing fragile "horizontal search line" algorithm in `DwgDataExtractor.cs`
|
||||||
|
- Implementing robust "vertical ray-casting" approach for NOTE content box detection
|
||||||
|
- Key methods being refactored: `FindNoteBox`, `GetAllLineSegments`, `TraceBoxFromTopLine`
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- Teigha DLLs must be present in the specified path for DWG processing to work
|
||||||
|
- The Python module requires Google Gemini API key configuration
|
||||||
|
- Excel output uses COM Interop and requires Microsoft Office installation
|
||||||
|
- The application supports both manual GUI operation and automated batch processing via CLI
|
||||||
14
App.config
Normal file
14
App.config
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<configuration>
|
||||||
|
<runtime>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="office"
|
||||||
|
publicKeyToken="71e9bce111e9429c"
|
||||||
|
culture="neutral" />
|
||||||
|
<bindingRedirect oldVersion="15.0.0.0"
|
||||||
|
newVersion="16.0.0.0" />
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
</runtime>
|
||||||
|
</configuration>
|
||||||
144
Controls/ZoomBorder.cs
Normal file
144
Controls/ZoomBorder.cs
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using Cursors = System.Windows.Input.Cursors;
|
||||||
|
using Point = System.Windows.Point;
|
||||||
|
|
||||||
|
namespace DwgExtractorManual.Controls
|
||||||
|
{
|
||||||
|
public class ZoomBorder : Border
|
||||||
|
{
|
||||||
|
private UIElement? child = null;
|
||||||
|
private Point origin;
|
||||||
|
private Point start;
|
||||||
|
|
||||||
|
private TranslateTransform GetTranslateTransform(UIElement element)
|
||||||
|
{
|
||||||
|
return (TranslateTransform)((TransformGroup)element.RenderTransform)
|
||||||
|
.Children.First(tr => tr is TranslateTransform);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ScaleTransform GetScaleTransform(UIElement element)
|
||||||
|
{
|
||||||
|
return (ScaleTransform)((TransformGroup)element.RenderTransform)
|
||||||
|
.Children.First(tr => tr is ScaleTransform);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override UIElement Child
|
||||||
|
{
|
||||||
|
get { return base.Child; }
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value != null && value != this.Child)
|
||||||
|
this.Initialize(value);
|
||||||
|
base.Child = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Initialize(UIElement element)
|
||||||
|
{
|
||||||
|
this.child = element;
|
||||||
|
if (child != null)
|
||||||
|
{
|
||||||
|
TransformGroup group = new TransformGroup();
|
||||||
|
ScaleTransform st = new ScaleTransform();
|
||||||
|
group.Children.Add(st);
|
||||||
|
TranslateTransform tt = new TranslateTransform();
|
||||||
|
group.Children.Add(tt);
|
||||||
|
child.RenderTransform = group;
|
||||||
|
child.RenderTransformOrigin = new Point(0.0, 0.0);
|
||||||
|
this.MouseWheel += child_MouseWheel;
|
||||||
|
this.MouseLeftButtonDown += child_MouseLeftButtonDown;
|
||||||
|
this.MouseLeftButtonUp += child_MouseLeftButtonUp;
|
||||||
|
this.MouseMove += child_MouseMove;
|
||||||
|
this.PreviewMouseRightButtonDown += new MouseButtonEventHandler(
|
||||||
|
child_PreviewMouseRightButtonDown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
if (child != null)
|
||||||
|
{
|
||||||
|
// reset zoom
|
||||||
|
var st = GetScaleTransform(child);
|
||||||
|
st.ScaleX = 1.0;
|
||||||
|
st.ScaleY = 1.0;
|
||||||
|
|
||||||
|
// reset pan
|
||||||
|
var tt = GetTranslateTransform(child);
|
||||||
|
tt.X = 0.0;
|
||||||
|
tt.Y = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void child_MouseWheel(object sender, MouseWheelEventArgs e)
|
||||||
|
{
|
||||||
|
if (child != null)
|
||||||
|
{
|
||||||
|
var st = GetScaleTransform(child);
|
||||||
|
var tt = GetTranslateTransform(child);
|
||||||
|
|
||||||
|
double zoom = e.Delta > 0 ? .2 : -.2;
|
||||||
|
if (!(e.Delta > 0) && (st.ScaleX < .4 || st.ScaleY < .4))
|
||||||
|
return;
|
||||||
|
|
||||||
|
Point relative = e.GetPosition(child);
|
||||||
|
double absoluteX;
|
||||||
|
double absoluteY;
|
||||||
|
|
||||||
|
absoluteX = relative.X * st.ScaleX + tt.X;
|
||||||
|
absoluteY = relative.Y * st.ScaleY + tt.Y;
|
||||||
|
|
||||||
|
st.ScaleX += zoom;
|
||||||
|
st.ScaleY += zoom;
|
||||||
|
|
||||||
|
tt.X = absoluteX - relative.X * st.ScaleX;
|
||||||
|
tt.Y = absoluteY - relative.Y * st.ScaleY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void child_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
if (child != null)
|
||||||
|
{
|
||||||
|
var tt = GetTranslateTransform(child);
|
||||||
|
start = e.GetPosition(this);
|
||||||
|
origin = new Point(tt.X, tt.Y);
|
||||||
|
this.Cursor = Cursors.Hand;
|
||||||
|
child.CaptureMouse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void child_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
if (child != null)
|
||||||
|
{
|
||||||
|
child.ReleaseMouseCapture();
|
||||||
|
this.Cursor = Cursors.Arrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void child_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
this.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void child_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
|
||||||
|
{
|
||||||
|
if (child != null)
|
||||||
|
{
|
||||||
|
if (child.IsMouseCaptured)
|
||||||
|
{
|
||||||
|
var tt = GetTranslateTransform(child);
|
||||||
|
Vector v = start - e.GetPosition(this);
|
||||||
|
tt.X = origin.X - v.X;
|
||||||
|
tt.Y = origin.Y - v.Y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,30 +6,17 @@
|
|||||||
<UseWindowsForms>True</UseWindowsForms>
|
<UseWindowsForms>True</UseWindowsForms>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<PlatformTarget>x64</PlatformTarget>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<COMReference Include="Microsoft.Office.Interop.Excel">
|
|
||||||
<VersionMinor>9</VersionMinor>
|
|
||||||
<VersionMajor>1</VersionMajor>
|
|
||||||
<Guid>00020813-0000-0000-c000-000000000046</Guid>
|
|
||||||
<Lcid>0</Lcid>
|
|
||||||
<WrapperTool>tlbimp</WrapperTool>
|
|
||||||
<Isolated>false</Isolated>
|
|
||||||
<EmbedInteropTypes>true</EmbedInteropTypes>
|
|
||||||
</COMReference>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Npgsql" Version="9.0.1" />
|
<PackageReference Include="Npgsql" Version="9.0.1" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Reference Include="TD_Mgd_23.12_16">
|
|
||||||
<HintPath>D:\dev_Net8_git\trunk\DLL\Teigha\vc16_amd64dll_23.12SP2\TD_Mgd_23.12_16.dll</HintPath>
|
|
||||||
<Private>true</Private>
|
|
||||||
</Reference>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Copy all Teigha DLLs including native dependencies -->
|
<!-- Copy all Teigha DLLs including native dependencies -->
|
||||||
@@ -39,68 +26,33 @@
|
|||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Define fletimageanalysis files as content to be copied -->
|
|
||||||
<!-- Separate JSON files and other files for different copy behaviors -->
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<!-- JSON files - always copy as new -->
|
<Reference Include="TD_Mgd_23.12_16">
|
||||||
<FletImageAnalysisJsonFiles Include="fletimageanalysis\**\*.json" />
|
<HintPath>..\..\..\GitNet8\trunk\DLL\Teigha\vc16_amd64dll_23.12SP2\TD_Mgd_23.12_16.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
<!-- Other files - incremental copy -->
|
|
||||||
<FletImageAnalysisOtherFiles Include="fletimageanalysis\**\*" Exclude="fletimageanalysis\**\*.json;fletimageanalysis\**\*.pyc;fletimageanalysis\**\__pycache__\**;fletimageanalysis\**\*.tmp;fletimageanalysis\**\*.log;fletimageanalysis\**\*.git\**" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Enhanced copy target that handles both incremental updates and missing folder scenarios -->
|
<ItemGroup>
|
||||||
<Target Name="CopyFletImageAnalysisFolder" AfterTargets="Build">
|
<Reference Include="Microsoft.Office.Interop.Excel">
|
||||||
|
<HintPath>C:\Program Files (x86)\Microsoft Office\Office16\DCF\Microsoft.Office.Interop.Excel.dll</HintPath>
|
||||||
<!-- Always show what we're doing -->
|
<EmbedInteropTypes>false</EmbedInteropTypes>
|
||||||
<Message Text="Copying fletimageanalysis folder contents..." Importance="normal" />
|
</Reference>
|
||||||
<Message Text="JSON files to copy: @(FletImageAnalysisJsonFiles->Count())" Importance="normal" />
|
<Reference Include="office">
|
||||||
<Message Text="Other files to copy: @(FletImageAnalysisOtherFiles->Count())" Importance="normal" />
|
<HintPath>C:\Program Files (x86)\Microsoft Office\Office16\DCF\office.dll</HintPath>
|
||||||
|
<EmbedInteropTypes>false</EmbedInteropTypes>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Copy JSON files - ALWAYS as new (SkipUnchangedFiles=false) -->
|
<ItemGroup>
|
||||||
<Copy
|
<Content Include="fletimageanalysis\**\*">
|
||||||
SourceFiles="@(FletImageAnalysisJsonFiles)"
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
DestinationFiles="@(FletImageAnalysisJsonFiles->'$(OutDir)%(RecursiveDir)%(Filename)%(Extension)')"
|
<Link>fletimageanalysis\%(RecursiveDir)%(FileName)%(Extension)</Link>
|
||||||
SkipUnchangedFiles="false"
|
</Content>
|
||||||
OverwriteReadOnlyFiles="true" />
|
</ItemGroup>
|
||||||
|
|
||||||
<Message Text="JSON files copied (always as new): @(FletImageAnalysisJsonFiles->Count())" Importance="high" />
|
|
||||||
|
|
||||||
<!-- Copy other files - incrementally (SkipUnchangedFiles=true) -->
|
|
||||||
<Copy
|
|
||||||
SourceFiles="@(FletImageAnalysisOtherFiles)"
|
|
||||||
DestinationFiles="@(FletImageAnalysisOtherFiles->'$(OutDir)%(RecursiveDir)%(Filename)%(Extension)')"
|
|
||||||
SkipUnchangedFiles="true"
|
|
||||||
OverwriteReadOnlyFiles="true" />
|
|
||||||
|
|
||||||
<Message Text="Other files copied (incremental): @(FletImageAnalysisOtherFiles->Count())" Importance="normal" />
|
|
||||||
|
|
||||||
<!-- Verify critical JSON file exists after copy -->
|
|
||||||
<Error Condition="!Exists('$(OutDir)fletimageanalysis\mapping_table_json.json')"
|
|
||||||
Text="Critical file missing after copy: mapping_table_json.json" />
|
|
||||||
|
|
||||||
<Message Text="fletimageanalysis folder copy completed successfully." Importance="high" />
|
|
||||||
</Target>
|
|
||||||
|
|
||||||
<!-- Additional target to ensure folder is copied during publish -->
|
|
||||||
<Target Name="CopyFletImageAnalysisFolderOnPublish" AfterTargets="Publish">
|
|
||||||
<Message Text="Copying fletimageanalysis folder for publish..." Importance="high" />
|
|
||||||
|
|
||||||
<!-- Copy JSON files - always as new -->
|
|
||||||
<Copy
|
|
||||||
SourceFiles="@(FletImageAnalysisJsonFiles)"
|
|
||||||
DestinationFiles="@(FletImageAnalysisJsonFiles->'$(PublishDir)%(RecursiveDir)%(Filename)%(Extension)')"
|
|
||||||
SkipUnchangedFiles="false"
|
|
||||||
OverwriteReadOnlyFiles="true" />
|
|
||||||
|
|
||||||
<!-- Copy other files - always as new for publish -->
|
|
||||||
<Copy
|
|
||||||
SourceFiles="@(FletImageAnalysisOtherFiles)"
|
|
||||||
DestinationFiles="@(FletImageAnalysisOtherFiles->'$(PublishDir)%(RecursiveDir)%(Filename)%(Extension)')"
|
|
||||||
SkipUnchangedFiles="false"
|
|
||||||
OverwriteReadOnlyFiles="true" />
|
|
||||||
|
|
||||||
<Message Text="fletimageanalysis folder publish copy completed." Importance="high" />
|
|
||||||
</Target>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
173
IntersectionTestConsole.cs
Normal file
173
IntersectionTestConsole.cs
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
//using System;
|
||||||
|
//using System.Collections.Generic;
|
||||||
|
//using System.Diagnostics;
|
||||||
|
//using System.Linq;
|
||||||
|
//using Teigha.Geometry;
|
||||||
|
//using DwgExtractorManual.Models;
|
||||||
|
|
||||||
|
//namespace DwgExtractorManual
|
||||||
|
//{
|
||||||
|
// /// <summary>
|
||||||
|
// /// 콘솔에서 실행할 수 있는 교차점 테스트 프로그램
|
||||||
|
// /// </summary>
|
||||||
|
// class IntersectionTestConsole
|
||||||
|
// {
|
||||||
|
// static void Main(string[] args)
|
||||||
|
// {
|
||||||
|
// Console.WriteLine("=== 교차점 테스트 프로그램 시작 ===");
|
||||||
|
|
||||||
|
// try
|
||||||
|
// {
|
||||||
|
// RunSimpleIntersectionTest();
|
||||||
|
// }
|
||||||
|
// catch (Exception ex)
|
||||||
|
// {
|
||||||
|
// Console.WriteLine($"오류 발생: {ex.Message}");
|
||||||
|
// Console.WriteLine(ex.StackTrace);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Console.WriteLine("테스트 완료. 아무 키나 누르세요...");
|
||||||
|
// Console.ReadKey();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// static void RunSimpleIntersectionTest()
|
||||||
|
// {
|
||||||
|
// Console.WriteLine("테스트 시작: 3x4 그리드 생성");
|
||||||
|
|
||||||
|
// // 간단한 3x4 테이블 시뮬레이션 (실제 DWG 없이)
|
||||||
|
// var intersections = new List<IntersectionPoint>();
|
||||||
|
|
||||||
|
// // 수동으로 교차점 생성 (3행 x 4열 = 12개 교차점)
|
||||||
|
// for (int row = 1; row <= 4; row++) // 4개 행
|
||||||
|
// {
|
||||||
|
// for (int col = 1; col <= 5; col++) // 5개 열
|
||||||
|
// {
|
||||||
|
// double x = (col - 1) * 10.0; // 0, 10, 20, 30, 40
|
||||||
|
// double y = (row - 1) * 10.0; // 0, 10, 20, 30
|
||||||
|
|
||||||
|
// int directionBits = CalculateDirectionBits(row, col, 4, 5);
|
||||||
|
|
||||||
|
// var intersection = new IntersectionPoint
|
||||||
|
// {
|
||||||
|
// Position = new Point3d(x, y, 0),
|
||||||
|
// DirectionBits = directionBits,
|
||||||
|
// Row = row,
|
||||||
|
// Column = col
|
||||||
|
// };
|
||||||
|
|
||||||
|
// intersections.Add(intersection);
|
||||||
|
// Console.WriteLine($"교차점 R{row}C{col}: ({x:F0},{y:F0}) - DirectionBits: {directionBits}");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Console.WriteLine($"\n총 {intersections.Count}개 교차점 생성됨");
|
||||||
|
|
||||||
|
// // DirectionBits 검증
|
||||||
|
// TestDirectionBitsValidation(intersections);
|
||||||
|
|
||||||
|
// // 셀 추출 시뮬레이션
|
||||||
|
// TestCellExtraction(intersections);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// static int CalculateDirectionBits(int row, int col, int maxRow, int maxCol)
|
||||||
|
// {
|
||||||
|
// int bits = 0;
|
||||||
|
|
||||||
|
// // Right: 1 - 오른쪽에 더 많은 열이 있으면
|
||||||
|
// if (col < maxCol) bits |= 1;
|
||||||
|
|
||||||
|
// // Up: 2 - 위쪽에 더 많은 행이 있으면
|
||||||
|
// if (row < maxRow) bits |= 2;
|
||||||
|
|
||||||
|
// // Left: 4 - 왼쪽에 열이 있으면
|
||||||
|
// if (col > 1) bits |= 4;
|
||||||
|
|
||||||
|
// // Down: 8 - 아래쪽에 행이 있으면
|
||||||
|
// if (row > 1) bits |= 8;
|
||||||
|
|
||||||
|
// return bits;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// static void TestDirectionBitsValidation(List<IntersectionPoint> intersections)
|
||||||
|
// {
|
||||||
|
// Console.WriteLine("\n=== DirectionBits 검증 ===");
|
||||||
|
|
||||||
|
// var mappingData = new MappingTableData();
|
||||||
|
// var fieldMapper = new FieldMapper(mappingData);
|
||||||
|
// var extractor = new DwgDataExtractor(fieldMapper);
|
||||||
|
|
||||||
|
// foreach (var intersection in intersections)
|
||||||
|
// {
|
||||||
|
// bool isTopLeft = extractor.IsValidTopLeft(intersection.DirectionBits);
|
||||||
|
// bool isBottomRight = extractor.IsValidBottomRight(intersection.DirectionBits);
|
||||||
|
|
||||||
|
// Console.WriteLine($"R{intersection.Row}C{intersection.Column} (bits: {intersection.DirectionBits:D2}) - " +
|
||||||
|
// $"TopLeft: {isTopLeft}, BottomRight: {isBottomRight}");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// static void TestCellExtraction(List<IntersectionPoint> intersections)
|
||||||
|
// {
|
||||||
|
// Console.WriteLine("\n=== 셀 추출 테스트 ===");
|
||||||
|
|
||||||
|
// var mappingData = new MappingTableData();
|
||||||
|
// var fieldMapper = new FieldMapper(mappingData);
|
||||||
|
// var extractor = new DwgDataExtractor(fieldMapper);
|
||||||
|
|
||||||
|
// // topLeft 후보들 찾기
|
||||||
|
// var topLeftCandidates = intersections.Where(i => extractor.IsValidTopLeft(i.DirectionBits)).ToList();
|
||||||
|
// Console.WriteLine($"TopLeft 후보: {topLeftCandidates.Count}개");
|
||||||
|
|
||||||
|
// foreach (var topLeft in topLeftCandidates)
|
||||||
|
// {
|
||||||
|
// Console.WriteLine($"\nTopLeft R{topLeft.Row}C{topLeft.Column} 처리 중...");
|
||||||
|
|
||||||
|
// // bottomRight 찾기 시뮬레이션
|
||||||
|
// var bottomRight = FindBottomRightSimulation(topLeft, intersections, extractor);
|
||||||
|
|
||||||
|
// if (bottomRight != null)
|
||||||
|
// {
|
||||||
|
// Console.WriteLine($" -> BottomRight 발견: R{bottomRight.Row}C{bottomRight.Column}");
|
||||||
|
// Console.WriteLine($" 셀 생성: ({topLeft.Position.X:F0},{bottomRight.Position.Y:F0}) to ({bottomRight.Position.X:F0},{topLeft.Position.Y:F0})");
|
||||||
|
// }
|
||||||
|
// else
|
||||||
|
// {
|
||||||
|
// Console.WriteLine(" -> BottomRight을 찾지 못함");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// static IntersectionPoint? FindBottomRightSimulation(IntersectionPoint topLeft, List<IntersectionPoint> intersections, DwgDataExtractor extractor)
|
||||||
|
// {
|
||||||
|
// // 교차점들을 Row/Column으로 딕셔너리 구성
|
||||||
|
// var intersectionLookup = intersections
|
||||||
|
// .GroupBy(i => i.Row)
|
||||||
|
// .ToDictionary(g => g.Key, g => g.ToDictionary(i => i.Column, i => i));
|
||||||
|
|
||||||
|
// int maxRow = intersectionLookup.Keys.Max();
|
||||||
|
// int maxColumn = intersectionLookup.Values.SelectMany(row => row.Keys).Max();
|
||||||
|
|
||||||
|
// for (int targetRow = topLeft.Row + 1; targetRow <= maxRow + 1; targetRow++)
|
||||||
|
// {
|
||||||
|
// if (!intersectionLookup.ContainsKey(targetRow)) continue;
|
||||||
|
|
||||||
|
// var rowIntersections = intersectionLookup[targetRow];
|
||||||
|
// var availableColumns = rowIntersections.Keys.Where(col => col >= topLeft.Column).OrderBy(col => col);
|
||||||
|
|
||||||
|
// foreach (int targetColumn in availableColumns)
|
||||||
|
// {
|
||||||
|
// var candidate = rowIntersections[targetColumn];
|
||||||
|
|
||||||
|
// // bottomRight 검증 또는 테이블 경계 조건
|
||||||
|
// if (extractor.IsValidBottomRight(candidate.DirectionBits) ||
|
||||||
|
// (targetRow == maxRow && targetColumn == maxColumn))
|
||||||
|
// {
|
||||||
|
// return candidate;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
82
MainWindow.Visualization.cs
Normal file
82
MainWindow.Visualization.cs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Windows;
|
||||||
|
using DwgExtractorManual.Models;
|
||||||
|
using DwgExtractorManual.Views;
|
||||||
|
using MessageBox = System.Windows.MessageBox;
|
||||||
|
|
||||||
|
namespace DwgExtractorManual
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// MainWindow의 시각화 관련 기능을 담당하는 partial class
|
||||||
|
/// </summary>
|
||||||
|
public partial class MainWindow
|
||||||
|
{
|
||||||
|
// 시각화 데이터 저장
|
||||||
|
private static List<TableCellVisualizationData> _visualizationDataCache = new List<TableCellVisualizationData>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 테이블 셀 시각화 데이터를 저장합니다.
|
||||||
|
/// </summary>
|
||||||
|
public static void SaveVisualizationData(TableCellVisualizationData data)
|
||||||
|
{
|
||||||
|
_visualizationDataCache.Add(data);
|
||||||
|
Debug.WriteLine($"[VISUALIZATION] 시각화 데이터 저장: {data.FileName}, 셀 수: {data.Cells.Count}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 저장된 시각화 데이터를 가져옵니다.
|
||||||
|
/// </summary>
|
||||||
|
public static List<TableCellVisualizationData> GetVisualizationData()
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[VISUALIZATION] 시각화 데이터 조회: {_visualizationDataCache.Count}개 항목");
|
||||||
|
return new List<TableCellVisualizationData>(_visualizationDataCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 시각화 데이터를 초기화합니다.
|
||||||
|
/// </summary>
|
||||||
|
public static void ClearVisualizationData()
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[VISUALIZATION] 시각화 데이터 초기화 (기존 {_visualizationDataCache.Count}개 항목 삭제)");
|
||||||
|
_visualizationDataCache.Clear();
|
||||||
|
Debug.WriteLine("[VISUALIZATION] 시각화 데이터 초기화 완료");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 테이블 셀 시각화 창을 엽니다.
|
||||||
|
/// </summary>
|
||||||
|
private void BtnVisualizeCells_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
LogMessage("🎨 테이블 셀 시각화 창을 여는 중...");
|
||||||
|
|
||||||
|
var visualizationData = GetVisualizationData();
|
||||||
|
LogMessage($"[DEBUG] 조회된 시각화 데이터: {visualizationData.Count}개");
|
||||||
|
|
||||||
|
if (visualizationData.Count == 0)
|
||||||
|
{
|
||||||
|
MessageBox.Show("시각화할 데이터가 없습니다.\n먼저 'DWG추출(Height정렬)' 버튼을 눌러 데이터를 추출해주세요.",
|
||||||
|
"데이터 없음", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
|
LogMessage("⚠️ 시각화할 데이터가 없습니다. 먼저 추출을 진행해주세요.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var visualizationWindow = new TableCellVisualizationWindow(visualizationData);
|
||||||
|
visualizationWindow.Owner = this;
|
||||||
|
visualizationWindow.Show();
|
||||||
|
|
||||||
|
LogMessage($"✅ 시각화 창 열기 완료 - {visualizationData.Count}개 파일 데이터");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogMessage($"❌ 시각화 창 열기 중 오류: {ex.Message}");
|
||||||
|
MessageBox.Show($"시각화 창을 여는 중 오류가 발생했습니다:\n{ex.Message}",
|
||||||
|
"오류", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<Window x:Class="DwgExtractorManual.MainWindow"
|
<Window x:Class="DwgExtractorManual.MainWindow"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
Title="DWG 정보 추출기" Height="700" Width="900"
|
Title="DWG 정보 추출기" Height="Auto" Width="900"
|
||||||
WindowStartupLocation="CenterScreen"
|
WindowStartupLocation="CenterScreen"
|
||||||
MinHeight="600" MinWidth="800">
|
MinHeight="600" MinWidth="800">
|
||||||
|
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
<RowDefinition Height="*"/>
|
<RowDefinition Height="*"/>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
@@ -89,6 +90,7 @@
|
|||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<!-- 진행률 바 -->
|
<!-- 진행률 바 -->
|
||||||
@@ -153,12 +155,101 @@
|
|||||||
</Style>
|
</Style>
|
||||||
</Button.Style>
|
</Button.Style>
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button x:Name="btnAuto"
|
||||||
|
Content="🤖 자동" Width="150" Height="45"
|
||||||
|
Margin="5,0"
|
||||||
|
Click="BtnAuto_Click" FontSize="14" FontWeight="Bold"
|
||||||
|
Background="#4CAF50" Foreground="White"
|
||||||
|
BorderThickness="0">
|
||||||
|
<Button.Style>
|
||||||
|
<Style TargetType="Button">
|
||||||
|
<Setter Property="Background" Value="#4CAF50"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter Property="Background" Value="#45A049"/>
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Button.Style>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Second row of buttons -->
|
||||||
|
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,5,0,10">
|
||||||
|
<Button x:Name="btnDwgOnly"
|
||||||
|
Content="🔧 DWG추출(폴더별)" Width="150" Height="45"
|
||||||
|
Margin="5,0"
|
||||||
|
Click="BtnDwgOnly_Click" FontSize="14" FontWeight="Bold"
|
||||||
|
Background="#8B4513" Foreground="White"
|
||||||
|
BorderThickness="0">
|
||||||
|
<Button.Style>
|
||||||
|
<Style TargetType="Button">
|
||||||
|
<Setter Property="Background" Value="#8B4513"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter Property="Background" Value="#A0522D"/>
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Button.Style>
|
||||||
|
</Button>
|
||||||
|
<Button x:Name="btnDwgHeightSort"
|
||||||
|
Content="📏 DWG추출(Height정렬)" Width="150" Height="45"
|
||||||
|
Margin="5,0"
|
||||||
|
Click="BtnDwgHeightSort_Click" FontSize="14" FontWeight="Bold"
|
||||||
|
Background="#FF6B35" Foreground="White"
|
||||||
|
BorderThickness="0">
|
||||||
|
<Button.Style>
|
||||||
|
<Style TargetType="Button">
|
||||||
|
<Setter Property="Background" Value="#FF6B35"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter Property="Background" Value="#E55A2B"/>
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Button.Style>
|
||||||
|
</Button>
|
||||||
|
<Button x:Name="btnVisualizeCells"
|
||||||
|
Content="🎨 셀 시각화" Width="150" Height="45"
|
||||||
|
Margin="5,0"
|
||||||
|
Click="BtnVisualizeCells_Click" FontSize="14" FontWeight="Bold"
|
||||||
|
Background="#9B59B6" Foreground="White"
|
||||||
|
BorderThickness="0">
|
||||||
|
<Button.Style>
|
||||||
|
<Style TargetType="Button">
|
||||||
|
<Setter Property="Background" Value="#9B59B6"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter Property="Background" Value="#8E44AD"/>
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Button.Style>
|
||||||
|
</Button>
|
||||||
|
<Button x:Name="btnTestIntersection"
|
||||||
|
Content="🔬 교차점 테스트" Width="150" Height="45"
|
||||||
|
Margin="5,0"
|
||||||
|
Click="BtnTestIntersection_Click" FontSize="14" FontWeight="Bold"
|
||||||
|
Background="#E74C3C" Foreground="White"
|
||||||
|
BorderThickness="0">
|
||||||
|
<Button.Style>
|
||||||
|
<Style TargetType="Button">
|
||||||
|
<Setter Property="Background" Value="#E74C3C"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter Property="Background" Value="#C0392B"/>
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Button.Style>
|
||||||
|
</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</GroupBox>
|
</GroupBox>
|
||||||
|
|
||||||
<!-- 로그 출력 -->
|
<!-- 로그 출력 -->
|
||||||
<GroupBox Grid.Row="5" Header="📋 실시간 로그" Margin="15,5,15,10"
|
<GroupBox Grid.Row="6" Header="📋 실시간 로그" Margin="15,5,15,10" Height="300"
|
||||||
FontWeight="SemiBold" FontSize="14">
|
FontWeight="SemiBold" FontSize="14">
|
||||||
<ScrollViewer Margin="5" VerticalScrollBarVisibility="Auto">
|
<ScrollViewer Margin="5" VerticalScrollBarVisibility="Auto">
|
||||||
<TextBox x:Name="txtLog"
|
<TextBox x:Name="txtLog"
|
||||||
@@ -171,12 +262,14 @@
|
|||||||
</GroupBox>
|
</GroupBox>
|
||||||
|
|
||||||
<!-- 상태바 -->
|
<!-- 상태바 -->
|
||||||
<StatusBar Grid.Row="6" Background="#3B4252" Foreground="White">
|
<StatusBar Grid.Row="7" Background="#3B4252" Foreground="White">
|
||||||
<StatusBarItem>
|
<StatusBarItem>
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<TextBlock x:Name="txtStatusBar" Text="DWG 정보 추출기 v1.0 - 준비됨"/>
|
<TextBlock x:Name="txtStatusBar" Text="DWG 정보 추출기 v1.0 - 준비됨"/>
|
||||||
<Separator Margin="10,0"/>
|
<Separator Margin="10,0"/>
|
||||||
<TextBlock x:Name="txtFileCount" Text="파일: 0개"/>
|
<TextBlock x:Name="txtFileCount" Text="파일: 0개"/>
|
||||||
|
<Separator Margin="10,0"/>
|
||||||
|
<TextBlock x:Name="txtBuildTime" Text="빌드: 로딩중..." FontSize="11" Foreground="LightGray"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StatusBarItem>
|
</StatusBarItem>
|
||||||
<StatusBarItem HorizontalAlignment="Right">
|
<StatusBarItem HorizontalAlignment="Right">
|
||||||
|
|||||||
1695
MainWindow.xaml.cs
1695
MainWindow.xaml.cs
File diff suppressed because it is too large
Load Diff
9
Models/AppSettings.cs
Normal file
9
Models/AppSettings.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace DwgExtractorManual.Models
|
||||||
|
{
|
||||||
|
public class AppSettings
|
||||||
|
{
|
||||||
|
public string? SourceFolderPath { get; set; }
|
||||||
|
public string? DestinationFolderPath { get; set; }
|
||||||
|
public string? LastExportType { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
261
Models/CsvDataWriter.cs
Normal file
261
Models/CsvDataWriter.cs
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace DwgExtractorManual.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Note 데이터를 CSV 파일로 출력하는 클래스
|
||||||
|
/// Note Box 안의 일반 텍스트와 테이블 텍스트를 분리하여 CSV로 출력
|
||||||
|
/// </summary>
|
||||||
|
public class CsvDataWriter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Note 박스 안의 일반 텍스트들을 CSV 파일로 저장
|
||||||
|
/// </summary>
|
||||||
|
public void WriteNoteBoxTextToCsv(List<NoteEntityInfo> noteEntities, string filePath)
|
||||||
|
{
|
||||||
|
if (noteEntities == null || noteEntities.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var csvLines = new List<string>();
|
||||||
|
|
||||||
|
// CSV 헤더 추가
|
||||||
|
csvLines.Add("Type,Layer,Text,X,Y,SortOrder,Path,FileName");
|
||||||
|
|
||||||
|
// Note와 NoteContent 데이터 추출 (테이블 제외)
|
||||||
|
var noteBoxTexts = noteEntities
|
||||||
|
.Where(ne => ne.Type == "Note" || ne.Type == "NoteContent")
|
||||||
|
.OrderBy(ne => ne.SortOrder)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var noteEntity in noteBoxTexts)
|
||||||
|
{
|
||||||
|
var csvLine = $"{EscapeCsvField(noteEntity.Type)}," +
|
||||||
|
$"{EscapeCsvField(noteEntity.Layer)}," +
|
||||||
|
$"{EscapeCsvField(noteEntity.Text)}," +
|
||||||
|
$"{noteEntity.X:F3}," +
|
||||||
|
$"{noteEntity.Y:F3}," +
|
||||||
|
$"{noteEntity.SortOrder}," +
|
||||||
|
$"{EscapeCsvField(noteEntity.Path)}," +
|
||||||
|
$"{EscapeCsvField(noteEntity.FileName)}";
|
||||||
|
|
||||||
|
csvLines.Add(csvLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
// UTF-8 BOM 포함하여 파일 저장 (Excel에서 한글 깨짐 방지)
|
||||||
|
var utf8WithBom = new UTF8Encoding(true);
|
||||||
|
File.WriteAllLines(filePath, csvLines, utf8WithBom);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Note 박스 안의 테이블 데이터들을 별도 CSV 파일로 저장
|
||||||
|
/// </summary>
|
||||||
|
public void WriteNoteTablesToCsv(List<NoteEntityInfo> noteEntities, string filePath)
|
||||||
|
{
|
||||||
|
if (noteEntities == null || noteEntities.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var allCsvLines = new List<string>();
|
||||||
|
|
||||||
|
// 테이블 데이터가 있는 Note들 추출
|
||||||
|
var notesWithTables = noteEntities
|
||||||
|
.Where(ne => ne.Type == "Note" && !string.IsNullOrEmpty(ne.TableCsv))
|
||||||
|
.OrderByDescending(ne => ne.Y) // Y 좌표로 정렬 (위에서 아래로)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var noteWithTable in notesWithTables)
|
||||||
|
{
|
||||||
|
// Note 정보 헤더 추가
|
||||||
|
allCsvLines.Add($"=== NOTE: {noteWithTable.Text} (at {noteWithTable.X:F1}, {noteWithTable.Y:F1}) ===");
|
||||||
|
allCsvLines.Add(""); // 빈 줄
|
||||||
|
|
||||||
|
// 테이블 CSV 데이터 추가
|
||||||
|
var tableLines = noteWithTable.TableCsv.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
allCsvLines.AddRange(tableLines);
|
||||||
|
|
||||||
|
// Note 간 구분을 위한 빈 줄들
|
||||||
|
allCsvLines.Add("");
|
||||||
|
allCsvLines.Add("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// UTF-8 BOM 포함하여 파일 저장
|
||||||
|
var utf8WithBom = new UTF8Encoding(true);
|
||||||
|
File.WriteAllLines(filePath, allCsvLines, utf8WithBom);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Note 박스와 테이블 데이터를 통합하여 하나의 CSV 파일로 저장
|
||||||
|
/// </summary>
|
||||||
|
public void WriteNoteDataToCombinedCsv(List<NoteEntityInfo> noteEntities, string filePath)
|
||||||
|
{
|
||||||
|
if (noteEntities == null || noteEntities.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var csvLines = new List<string>();
|
||||||
|
|
||||||
|
// CSV 헤더 추가
|
||||||
|
csvLines.Add("Type,Layer,Text,X,Y,SortOrder,TableData,Path,FileName");
|
||||||
|
|
||||||
|
// 모든 Note 관련 데이터를 SortOrder로 정렬
|
||||||
|
var sortedNoteEntities = noteEntities
|
||||||
|
.OrderBy(ne => ne.SortOrder)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var noteEntity in sortedNoteEntities)
|
||||||
|
{
|
||||||
|
// 테이블 데이터가 있는 경우 이를 별도 필드로 처리
|
||||||
|
var tableData = "";
|
||||||
|
if (noteEntity.Type == "Note" && !string.IsNullOrEmpty(noteEntity.TableCsv))
|
||||||
|
{
|
||||||
|
// 테이블 CSV 데이터를 하나의 필드로 압축 (줄바꿈을 |로 대체)
|
||||||
|
tableData = noteEntity.TableCsv.Replace("\n", "|").Replace("\r", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
var csvLine = $"{EscapeCsvField(noteEntity.Type)}," +
|
||||||
|
$"{EscapeCsvField(noteEntity.Layer)}," +
|
||||||
|
$"{EscapeCsvField(noteEntity.Text)}," +
|
||||||
|
$"{noteEntity.X:F3}," +
|
||||||
|
$"{noteEntity.Y:F3}," +
|
||||||
|
$"{noteEntity.SortOrder}," +
|
||||||
|
$"{EscapeCsvField(tableData)}," +
|
||||||
|
$"{EscapeCsvField(noteEntity.Path)}," +
|
||||||
|
$"{EscapeCsvField(noteEntity.FileName)}";
|
||||||
|
|
||||||
|
csvLines.Add(csvLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
// UTF-8 BOM 포함하여 파일 저장
|
||||||
|
var utf8WithBom = new UTF8Encoding(true);
|
||||||
|
File.WriteAllLines(filePath, csvLines, utf8WithBom);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 각 Note별로 개별 CSV 파일 생성 (테이블이 있는 경우)
|
||||||
|
/// </summary>
|
||||||
|
public void WriteIndividualNoteTablesCsv(List<NoteEntityInfo> noteEntities, string baseDirectory)
|
||||||
|
{
|
||||||
|
if (noteEntities == null || noteEntities.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// 디렉토리가 없으면 생성
|
||||||
|
if (!Directory.Exists(baseDirectory))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(baseDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
var notesWithTables = noteEntities
|
||||||
|
.Where(ne => ne.Type == "Note" && !string.IsNullOrEmpty(ne.TableCsv))
|
||||||
|
.OrderByDescending(ne => ne.Y)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
int noteIndex = 1;
|
||||||
|
foreach (var noteWithTable in notesWithTables)
|
||||||
|
{
|
||||||
|
// 파일명 생성 (특수문자 제거)
|
||||||
|
var safeNoteText = MakeSafeFileName(noteWithTable.Text);
|
||||||
|
var fileName = $"Note_{noteIndex:D2}_{safeNoteText}.csv";
|
||||||
|
var fullPath = Path.Combine(baseDirectory, fileName);
|
||||||
|
|
||||||
|
// 테이블 CSV 데이터를 파일로 저장
|
||||||
|
var tableLines = noteWithTable.TableCsv.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
var utf8WithBom = new UTF8Encoding(true);
|
||||||
|
File.WriteAllLines(fullPath, tableLines, utf8WithBom);
|
||||||
|
|
||||||
|
noteIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// CSV 필드에서 특수문자를 이스케이프 처리
|
||||||
|
/// </summary>
|
||||||
|
private string EscapeCsvField(string field)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(field))
|
||||||
|
return "";
|
||||||
|
|
||||||
|
// 쉼표, 따옴표, 줄바꿈이 있으면 따옴표로 감싸기
|
||||||
|
if (field.Contains(",") || field.Contains("\"") || field.Contains("\n") || field.Contains("\r"))
|
||||||
|
{
|
||||||
|
return "\"" + field.Replace("\"", "\"\"") + "\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 파일명에 사용할 수 없는 문자들을 제거하여 안전한 파일명 생성
|
||||||
|
/// </summary>
|
||||||
|
private string MakeSafeFileName(string fileName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(fileName))
|
||||||
|
return "Unknown";
|
||||||
|
|
||||||
|
var invalidChars = Path.GetInvalidFileNameChars();
|
||||||
|
var safeFileName = fileName;
|
||||||
|
|
||||||
|
foreach (var invalidChar in invalidChars)
|
||||||
|
{
|
||||||
|
safeFileName = safeFileName.Replace(invalidChar, '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 길이 제한 (Windows 파일명 제한 고려)
|
||||||
|
if (safeFileName.Length > 50)
|
||||||
|
{
|
||||||
|
safeFileName = safeFileName.Substring(0, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
return safeFileName.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Note 데이터 통계 정보를 CSV로 저장
|
||||||
|
/// </summary>
|
||||||
|
public void WriteNoteStatisticsToCsv(List<NoteEntityInfo> noteEntities, string filePath)
|
||||||
|
{
|
||||||
|
if (noteEntities == null || noteEntities.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var csvLines = new List<string>();
|
||||||
|
|
||||||
|
// 통계 헤더
|
||||||
|
csvLines.Add("Statistic,Count,Details");
|
||||||
|
|
||||||
|
// 전체 Note 개수
|
||||||
|
var totalNotes = noteEntities.Count(ne => ne.Type == "Note");
|
||||||
|
csvLines.Add($"Total Notes,{totalNotes},");
|
||||||
|
|
||||||
|
// 테이블이 있는 Note 개수
|
||||||
|
var notesWithTables = noteEntities.Count(ne => ne.Type == "Note" && !string.IsNullOrEmpty(ne.TableCsv));
|
||||||
|
csvLines.Add($"Notes with Tables,{notesWithTables},");
|
||||||
|
|
||||||
|
// 일반 텍스트만 있는 Note 개수
|
||||||
|
var notesWithTextOnly = totalNotes - notesWithTables;
|
||||||
|
csvLines.Add($"Notes with Text Only,{notesWithTextOnly},");
|
||||||
|
|
||||||
|
// 전체 Note 콘텐츠 개수
|
||||||
|
var totalNoteContents = noteEntities.Count(ne => ne.Type == "NoteContent");
|
||||||
|
csvLines.Add($"Total Note Contents,{totalNoteContents},");
|
||||||
|
|
||||||
|
// 레이어별 분포
|
||||||
|
csvLines.Add(",,");
|
||||||
|
csvLines.Add("Layer Distribution,,");
|
||||||
|
|
||||||
|
var layerGroups = noteEntities
|
||||||
|
.GroupBy(ne => ne.Layer)
|
||||||
|
.OrderByDescending(g => g.Count())
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var layerGroup in layerGroups)
|
||||||
|
{
|
||||||
|
csvLines.Add($"Layer: {layerGroup.Key},{layerGroup.Count()},");
|
||||||
|
}
|
||||||
|
|
||||||
|
var utf8WithBom = new UTF8Encoding(true);
|
||||||
|
File.WriteAllLines(filePath, csvLines, utf8WithBom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3096
Models/DwgDataExtractor.cs
Normal file
3096
Models/DwgDataExtractor.cs
Normal file
File diff suppressed because it is too large
Load Diff
639
Models/ExcelDataWriter.cs
Normal file
639
Models/ExcelDataWriter.cs
Normal file
@@ -0,0 +1,639 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using Excel = Microsoft.Office.Interop.Excel;
|
||||||
|
|
||||||
|
namespace DwgExtractorManual.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Excel <20><>Ʈ<EFBFBD><C6AE> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20>۾<EFBFBD><DBBE><EFBFBD> <20><><EFBFBD><EFBFBD>ϴ<EFBFBD> Ŭ<><C5AC><EFBFBD><EFBFBD>
|
||||||
|
/// </summary>
|
||||||
|
internal class ExcelDataWriter
|
||||||
|
{
|
||||||
|
private readonly ExcelManager excelManager;
|
||||||
|
|
||||||
|
public ExcelDataWriter(ExcelManager excelManager)
|
||||||
|
{
|
||||||
|
this.excelManager = excelManager ?? throw new ArgumentNullException(nameof(excelManager));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Title Block <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Excel <20><>Ʈ<EFBFBD><C6AE> <20><><EFBFBD>
|
||||||
|
/// </summary>
|
||||||
|
public void WriteTitleBlockData(List<TitleBlockRowData> titleBlockRows)
|
||||||
|
{
|
||||||
|
if (excelManager.TitleBlockSheet == null || titleBlockRows == null || titleBlockRows.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int currentRow = 2; // <20><><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>
|
||||||
|
|
||||||
|
foreach (var row in titleBlockRows)
|
||||||
|
{
|
||||||
|
excelManager.TitleBlockSheet.Cells[currentRow, 1] = row.Type;
|
||||||
|
excelManager.TitleBlockSheet.Cells[currentRow, 2] = row.Name;
|
||||||
|
excelManager.TitleBlockSheet.Cells[currentRow, 3] = row.Tag;
|
||||||
|
excelManager.TitleBlockSheet.Cells[currentRow, 4] = row.Prompt;
|
||||||
|
excelManager.TitleBlockSheet.Cells[currentRow, 5] = row.Value;
|
||||||
|
excelManager.TitleBlockSheet.Cells[currentRow, 6] = row.Path;
|
||||||
|
excelManager.TitleBlockSheet.Cells[currentRow, 7] = row.FileName;
|
||||||
|
currentRow++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Text Entity <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Excel <20><>Ʈ<EFBFBD><C6AE> <20><><EFBFBD>
|
||||||
|
/// </summary>
|
||||||
|
public void WriteTextEntityData(List<TextEntityRowData> textEntityRows)
|
||||||
|
{
|
||||||
|
if (excelManager.TextEntitiesSheet == null || textEntityRows == null || textEntityRows.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int currentRow = 2; // <20><><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>
|
||||||
|
|
||||||
|
foreach (var row in textEntityRows)
|
||||||
|
{
|
||||||
|
excelManager.TextEntitiesSheet.Cells[currentRow, 1] = row.Type;
|
||||||
|
excelManager.TextEntitiesSheet.Cells[currentRow, 2] = row.Layer;
|
||||||
|
excelManager.TextEntitiesSheet.Cells[currentRow, 3] = row.Text;
|
||||||
|
excelManager.TextEntitiesSheet.Cells[currentRow, 4] = row.Path;
|
||||||
|
excelManager.TextEntitiesSheet.Cells[currentRow, 5] = row.FileName;
|
||||||
|
currentRow++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Note 엔터티 데이터를 Excel 시트에 쓰기 (테이블 및 셀 병합 포함)
|
||||||
|
/// </summary>
|
||||||
|
public void WriteNoteEntityData(List<NoteEntityInfo> noteEntityRows)
|
||||||
|
{
|
||||||
|
if (excelManager.NoteEntitiesSheet == null || noteEntityRows == null || noteEntityRows.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int excelRow = 2; // 헤더 다음 행부터 시작
|
||||||
|
|
||||||
|
foreach (var note in noteEntityRows)
|
||||||
|
{
|
||||||
|
// 기본 Note 정보 쓰기
|
||||||
|
excelManager.NoteEntitiesSheet.Cells[excelRow, 1] = note.Type;
|
||||||
|
excelManager.NoteEntitiesSheet.Cells[excelRow, 2] = note.Layer;
|
||||||
|
excelManager.NoteEntitiesSheet.Cells[excelRow, 3] = note.Text;
|
||||||
|
excelManager.NoteEntitiesSheet.Cells[excelRow, 4] = note.X;
|
||||||
|
excelManager.NoteEntitiesSheet.Cells[excelRow, 5] = note.Y;
|
||||||
|
excelManager.NoteEntitiesSheet.Cells[excelRow, 6] = note.SortOrder;
|
||||||
|
excelManager.NoteEntitiesSheet.Cells[excelRow, 8] = note.Path;
|
||||||
|
excelManager.NoteEntitiesSheet.Cells[excelRow, 9] = note.FileName;
|
||||||
|
|
||||||
|
int tableRowCount = 0;
|
||||||
|
if (note.Cells != null && note.Cells.Count > 0)
|
||||||
|
{
|
||||||
|
// 테이블 데이터 처리
|
||||||
|
foreach (var cell in note.Cells)
|
||||||
|
{
|
||||||
|
int startRow = excelRow + cell.Row;
|
||||||
|
int startCol = 7 + cell.Column; // G열부터 시작
|
||||||
|
int endRow = startRow + cell.RowSpan - 1;
|
||||||
|
int endCol = startCol + cell.ColumnSpan - 1;
|
||||||
|
|
||||||
|
Excel.Range cellRange = excelManager.NoteEntitiesSheet.Range[
|
||||||
|
excelManager.NoteEntitiesSheet.Cells[startRow, startCol],
|
||||||
|
excelManager.NoteEntitiesSheet.Cells[endRow, endCol]];
|
||||||
|
|
||||||
|
// 병합 먼저 수행
|
||||||
|
if (cell.RowSpan > 1 || cell.ColumnSpan > 1)
|
||||||
|
{
|
||||||
|
cellRange.Merge();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 값 설정 및 서식 지정
|
||||||
|
cellRange.Value = cell.CellText;
|
||||||
|
cellRange.VerticalAlignment = Excel.XlVAlign.xlVAlignCenter;
|
||||||
|
cellRange.HorizontalAlignment = Excel.XlHAlign.xlHAlignCenter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이 테이블이 차지하는 총 행 수를 계산
|
||||||
|
tableRowCount = note.Cells.Max(c => c.Row + c.RowSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 다음 Note를 기록할 위치로 이동
|
||||||
|
// 테이블이 있으면 테이블 높이만큼, 없으면 한 칸만 이동
|
||||||
|
excelRow += (tableRowCount > 0) ? tableRowCount : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Excel <20><>Ʈ<EFBFBD><C6AE> <20><><EFBFBD>
|
||||||
|
/// </summary>
|
||||||
|
public void WriteMappingDataToExcel(Dictionary<string, Dictionary<string, (string, string, string, string)>> mappingData)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (excelManager.MappingSheet == null || mappingData == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int currentRow = 2; // <20><><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>
|
||||||
|
|
||||||
|
Debug.WriteLine($"[DEBUG] Writing mapping data to Excel. Total files: {mappingData.Count}");
|
||||||
|
|
||||||
|
foreach (var fileEntry in mappingData)
|
||||||
|
{
|
||||||
|
string fileName = fileEntry.Key;
|
||||||
|
var fileMappingData = fileEntry.Value;
|
||||||
|
|
||||||
|
Debug.WriteLine($"[DEBUG] Processing file: {fileName}, entries: {fileMappingData.Count}");
|
||||||
|
|
||||||
|
foreach (var mapEntry in fileMappingData)
|
||||||
|
{
|
||||||
|
string mapKey = mapEntry.Key;
|
||||||
|
(string aiLabel, string dwgTag, string attValue, string pdfValue) = mapEntry.Value;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(fileName) || string.IsNullOrEmpty(mapKey))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// <20><>ġ <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ʈ<EFBFBD><C6AE> <20><><EFBFBD><EFBFBD> <20>迭 <20><><EFBFBD>
|
||||||
|
object[,] rowData = new object[1, 6];
|
||||||
|
rowData[0, 0] = fileName;
|
||||||
|
rowData[0, 1] = mapKey;
|
||||||
|
rowData[0, 2] = aiLabel ?? "";
|
||||||
|
rowData[0, 3] = dwgTag ?? "";
|
||||||
|
rowData[0, 4] = attValue ?? "";
|
||||||
|
rowData[0, 5] = pdfValue ?? "";
|
||||||
|
|
||||||
|
Excel.Range range = excelManager.MappingSheet.Range[
|
||||||
|
excelManager.MappingSheet.Cells[currentRow, 1],
|
||||||
|
excelManager.MappingSheet.Cells[currentRow, 6]];
|
||||||
|
range.Value = rowData;
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"? Error writing row {currentRow}: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
currentRow++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.WriteLine($"[DEBUG] Mapping data written to Excel. Total rows: {currentRow - 2}");
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"? <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Excel <20><><EFBFBD> <20><> <20><><EFBFBD><EFBFBD>: {ex.Message}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Excel <20><><EFBFBD><EFBFBD> <20><>Ʈ<EFBFBD><C6AE><EFBFBD><EFBFBD> FileName<6D><65> AILabel<65><6C> <20><>Ī<EFBFBD>Ǵ<EFBFBD> <20><><EFBFBD><EFBFBD> ã<><C3A3> Pdf_value<75><65> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ʈ
|
||||||
|
/// </summary>
|
||||||
|
public bool UpdateExcelRow(string fileName, string aiLabel, string pdfValue)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (excelManager.MappingSheet == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Excel.Range usedRange = excelManager.MappingSheet.UsedRange;
|
||||||
|
if (usedRange == null) return false;
|
||||||
|
|
||||||
|
int lastRow = usedRange.Rows.Count;
|
||||||
|
|
||||||
|
for (int row = 2; row <= lastRow; row++)
|
||||||
|
{
|
||||||
|
var cellFileName = ((Excel.Range)excelManager.MappingSheet.Cells[row, 1]).Value?.ToString() ?? "";
|
||||||
|
var cellAiLabel = ((Excel.Range)excelManager.MappingSheet.Cells[row, 3]).Value?.ToString() ?? "";
|
||||||
|
|
||||||
|
if (string.Equals(cellFileName.Trim(), fileName.Trim(), StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
string.Equals(cellAiLabel.Trim(), aiLabel.Trim(), StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
excelManager.MappingSheet.Cells[row, 6] = pdfValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"? Excel <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ʈ <20><> <20><><EFBFBD><EFBFBD>: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DWG <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><>ũ<EFBFBD><C5A9><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>ϰ<EFBFBD> <20><><EFBFBD><EFBFBD> (PDF <20>÷<EFBFBD> <20><><EFBFBD><EFBFBD>)
|
||||||
|
/// </summary>
|
||||||
|
public void SaveDwgOnlyMappingWorkbook(Dictionary<string, Dictionary<string, (string, string, string, string)>> mappingData, string resultFolderPath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
||||||
|
string savePath = System.IO.Path.Combine(resultFolderPath, $"{timestamp}_DwgOnly_Mapping.xlsx");
|
||||||
|
|
||||||
|
Debug.WriteLine($"[DEBUG] DWG <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><>ũ<EFBFBD><C5A9> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>: {savePath}");
|
||||||
|
|
||||||
|
var dwgOnlyWorkbook = excelManager.CreateNewWorkbook();
|
||||||
|
var dwgOnlyWorksheet = (Excel.Worksheet)dwgOnlyWorkbook.Worksheets[1];
|
||||||
|
dwgOnlyWorksheet.Name = "DWG Mapping Data";
|
||||||
|
|
||||||
|
// <20><><EFBFBD> <20><><EFBFBD><EFBFBD> (PDF Value <20>÷<EFBFBD> <20><><EFBFBD><EFBFBD>)
|
||||||
|
dwgOnlyWorksheet.Cells[1, 1] = "<22><><EFBFBD>ϸ<EFBFBD>";
|
||||||
|
dwgOnlyWorksheet.Cells[1, 2] = "Map Key";
|
||||||
|
dwgOnlyWorksheet.Cells[1, 3] = "AI Label";
|
||||||
|
dwgOnlyWorksheet.Cells[1, 4] = "DWG Tag";
|
||||||
|
dwgOnlyWorksheet.Cells[1, 5] = "DWG Value";
|
||||||
|
|
||||||
|
// <20><><EFBFBD> <20><>Ÿ<EFBFBD><C5B8> <20><><EFBFBD><EFBFBD>
|
||||||
|
var headerRange = dwgOnlyWorksheet.Range["A1:E1"];
|
||||||
|
headerRange.Font.Bold = true;
|
||||||
|
headerRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightGray);
|
||||||
|
headerRange.Borders.LineStyle = Excel.XlLineStyle.xlContinuous;
|
||||||
|
|
||||||
|
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20>Է<EFBFBD>
|
||||||
|
int totalRows = mappingData.Sum(f => f.Value.Count);
|
||||||
|
if (totalRows > 0)
|
||||||
|
{
|
||||||
|
object[,] data = new object[totalRows, 5];
|
||||||
|
int row = 0;
|
||||||
|
|
||||||
|
foreach (var fileEntry in mappingData)
|
||||||
|
{
|
||||||
|
string fileName = fileEntry.Key;
|
||||||
|
foreach (var mapEntry in fileEntry.Value)
|
||||||
|
{
|
||||||
|
string mapKey = mapEntry.Key;
|
||||||
|
var (aiLabel, dwgTag, dwgValue, pdfValue) = mapEntry.Value;
|
||||||
|
|
||||||
|
data[row, 0] = fileName;
|
||||||
|
data[row, 1] = mapKey;
|
||||||
|
data[row, 2] = aiLabel;
|
||||||
|
data[row, 3] = dwgTag;
|
||||||
|
data[row, 4] = dwgValue;
|
||||||
|
|
||||||
|
row++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Excel.Range dataRange = dwgOnlyWorksheet.Range[
|
||||||
|
dwgOnlyWorksheet.Cells[2, 1],
|
||||||
|
dwgOnlyWorksheet.Cells[totalRows + 1, 5]];
|
||||||
|
dataRange.Value = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
dwgOnlyWorksheet.Columns.AutoFit();
|
||||||
|
excelManager.SaveWorkbookAs(dwgOnlyWorkbook, savePath);
|
||||||
|
|
||||||
|
Debug.WriteLine($"? DWG <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><>ũ<EFBFBD><C5A9> <20><><EFBFBD><EFBFBD> <20>Ϸ<EFBFBD>: {System.IO.Path.GetFileName(savePath)}");
|
||||||
|
|
||||||
|
dwgOnlyWorkbook.Close(false);
|
||||||
|
System.GC.Collect();
|
||||||
|
System.GC.WaitForPendingFinalizers();
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"? DWG <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><>ũ<EFBFBD><C5A9> <20><><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD>: {ex.Message}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Height <20><><EFBFBD>ĵ<EFBFBD> Excel <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>
|
||||||
|
/// </summary>
|
||||||
|
public void WriteHeightSortedData(List<TextEntityInfo> textEntities, Excel.Worksheet worksheet, string fileName)
|
||||||
|
{
|
||||||
|
// <20><><EFBFBD> <20><><EFBFBD><EFBFBD>
|
||||||
|
worksheet.Cells[1, 1] = "Height";
|
||||||
|
worksheet.Cells[1, 2] = "Type";
|
||||||
|
worksheet.Cells[1, 3] = "Layer";
|
||||||
|
worksheet.Cells[1, 4] = "Tag";
|
||||||
|
worksheet.Cells[1, 5] = "FileName";
|
||||||
|
worksheet.Cells[1, 6] = "Text";
|
||||||
|
|
||||||
|
// <20><><EFBFBD> <20><>Ÿ<EFBFBD><C5B8> <20><><EFBFBD><EFBFBD>
|
||||||
|
var headerRange = worksheet.Range["A1:F1"];
|
||||||
|
headerRange.Font.Bold = true;
|
||||||
|
headerRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightBlue);
|
||||||
|
|
||||||
|
// <20><><EFBFBD><EFBFBD><EFBFBD>ʹ<EFBFBD> ExtractTextEntitiesWithHeight<68><74><EFBFBD><EFBFBD> <20>̹<EFBFBD> <20><><EFBFBD>ĵǾ<C4B5><C7BE><EFBFBD><EFBFBD>Ƿ<EFBFBD> <20>ٽ<EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20>ʽ<EFBFBD><CABD>ϴ<EFBFBD>.
|
||||||
|
|
||||||
|
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20>Է<EFBFBD>
|
||||||
|
int row = 2;
|
||||||
|
foreach (var entity in textEntities) // sortedEntities<65><73> textEntities<65><73> <20><><EFBFBD><EFBFBD>
|
||||||
|
{
|
||||||
|
worksheet.Cells[row, 1] = entity.Height;
|
||||||
|
worksheet.Cells[row, 2] = entity.Type;
|
||||||
|
worksheet.Cells[row, 3] = entity.Layer;
|
||||||
|
worksheet.Cells[row, 4] = entity.Tag;
|
||||||
|
worksheet.Cells[row, 5] = fileName;
|
||||||
|
worksheet.Cells[row, 6] = entity.Text;
|
||||||
|
row++;
|
||||||
|
}
|
||||||
|
|
||||||
|
worksheet.Columns.AutoFit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Note 엔티티들을 Excel 워크시트에 기록합니다 (기존 데이터 아래에 추가).
|
||||||
|
/// CellBoundary 데이터를 사용하여 병합된 셀의 텍스트를 적절히 처리합니다.
|
||||||
|
/// </summary>
|
||||||
|
public void WriteNoteEntities(List<NoteEntityInfo> noteEntities, Excel.Worksheet worksheet, string fileName)
|
||||||
|
{
|
||||||
|
if (noteEntities == null || noteEntities.Count == 0)
|
||||||
|
{
|
||||||
|
Debug.WriteLine("[DEBUG] Note 엔티티가 없습니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 현재 워크시트의 마지막 사용된 행 찾기
|
||||||
|
Excel.Range usedRange = null;
|
||||||
|
int lastRow = 1;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
usedRange = worksheet.UsedRange;
|
||||||
|
lastRow = usedRange?.Rows.Count ?? 1;
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[DEBUG] UsedRange 접근 오류, 기본값 사용: {ex.Message}");
|
||||||
|
lastRow = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int startRow = lastRow + 2; // 한 줄 띄우고 시작
|
||||||
|
Debug.WriteLine($"[DEBUG] Note 데이터 기록 시작: {startRow}행부터 {noteEntities.Count}개 항목");
|
||||||
|
|
||||||
|
// Note 섹션 헤더 추가 (표 컬럼 포함)
|
||||||
|
try
|
||||||
|
{
|
||||||
|
worksheet.Cells[startRow - 1, 1] = "=== Notes (with Cell Boundary Tables) ===";
|
||||||
|
worksheet.Cells[startRow - 1, 2] = "";
|
||||||
|
worksheet.Cells[startRow - 1, 3] = "";
|
||||||
|
worksheet.Cells[startRow - 1, 4] = "";
|
||||||
|
worksheet.Cells[startRow - 1, 5] = "";
|
||||||
|
worksheet.Cells[startRow - 1, 6] = "";
|
||||||
|
|
||||||
|
// 표 컬럼 헤더 추가 (G열부터 최대 20개 컬럼)
|
||||||
|
for (int col = 7; col <= 26; col++) // G~Z열 (20개 컬럼)
|
||||||
|
{
|
||||||
|
worksheet.Cells[startRow - 1, col] = $"Table Col {col - 6}";
|
||||||
|
var tableHeaderCell = (Excel.Range)worksheet.Cells[startRow - 1, col];
|
||||||
|
tableHeaderCell.Font.Bold = true;
|
||||||
|
tableHeaderCell.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightBlue);
|
||||||
|
tableHeaderCell.Font.Size = 9; // 작은 폰트로 설정
|
||||||
|
}
|
||||||
|
|
||||||
|
// 헤더 스타일 적용 (개별 셀로 처리)
|
||||||
|
var headerCell = (Excel.Range)worksheet.Cells[startRow - 1, 1];
|
||||||
|
headerCell.Font.Bold = true;
|
||||||
|
headerCell.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightYellow);
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[DEBUG] Note 헤더 작성 오류: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note 데이터 입력 (CellBoundary 데이터 사용)
|
||||||
|
int row = startRow;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var noteEntity in noteEntities)
|
||||||
|
{
|
||||||
|
// 기본 Note 정보 입력 (F열까지)
|
||||||
|
worksheet.Cells[row, 1] = 0; // Height는 0으로 설정
|
||||||
|
worksheet.Cells[row, 2] = noteEntity.Type ?? "";
|
||||||
|
worksheet.Cells[row, 3] = noteEntity.Layer ?? "";
|
||||||
|
worksheet.Cells[row, 4] = ""; // Tag는 빈 값
|
||||||
|
worksheet.Cells[row, 5] = fileName ?? "";
|
||||||
|
worksheet.Cells[row, 6] = noteEntity.Text ?? ""; // 일반 텍스트만 (표 데이터 제외)
|
||||||
|
|
||||||
|
int currentRow = row; // 현재 처리 중인 행 번호
|
||||||
|
|
||||||
|
// CellBoundary 데이터가 있으면 G열부터 테이블 데이터 처리
|
||||||
|
if (noteEntity.CellBoundaries != null && noteEntity.CellBoundaries.Count > 0)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[DEBUG] CellBoundary 데이터 처리: Row {row}, 셀 수={noteEntity.CellBoundaries.Count}");
|
||||||
|
|
||||||
|
// CellBoundary의 각 셀을 해당 위치에 직접 배치
|
||||||
|
int maxTableRow = 0;
|
||||||
|
foreach (var cellBoundary in noteEntity.CellBoundaries)
|
||||||
|
{
|
||||||
|
var (sRow, sCol, eRow, eCol) = ParseCellRangeFromLabel(cellBoundary.Label);
|
||||||
|
Debug.WriteLine($"[DEBUG] CellBoundary 처리: {cellBoundary.Label} → Range=R{sRow}C{sCol}:R{eRow}C{eCol}, Text='{cellBoundary.CellText}'");
|
||||||
|
|
||||||
|
if (sRow > 0 && sCol > 0 && eRow > 0 && eCol > 0)
|
||||||
|
{
|
||||||
|
// 병합된 영역의 셀 개수 계산: (eRow - sRow) × (eCol - sCol)
|
||||||
|
int rowCount = eRow - sRow; // 행 개수 (bottomRight - topLeft)
|
||||||
|
int colCount = eCol - sCol; // 열 개수 (bottomRight - topLeft)
|
||||||
|
|
||||||
|
Debug.WriteLine($"[DEBUG] 병합 영역 크기: {rowCount+1}행 × {colCount+1}열");
|
||||||
|
|
||||||
|
// 병합된 영역의 모든 셀에 텍스트 복사 (topLeft부터 bottomRight-1까지)
|
||||||
|
for (int r = sRow; r < eRow; r++) // < eRow (bottomRight 제외)
|
||||||
|
{
|
||||||
|
for (int c = sCol; c < eCol; c++) // < eCol (bottomRight 제외)
|
||||||
|
{
|
||||||
|
// Excel에서 테이블 위치 계산:
|
||||||
|
// R1 → Note의 현재 행 (currentRow)
|
||||||
|
// R2 → Note의 다음 행 (currentRow + 1)
|
||||||
|
// C1 → G열(7), C2 → H열(8)
|
||||||
|
int excelRow = currentRow + (r - 1); // R1=currentRow, R2=currentRow+1, ...
|
||||||
|
int excelCol = 7 + (c - 1); // C1=G열(7), C2=H열(8), ...
|
||||||
|
|
||||||
|
// Excel 범위 체크 (최대 20개 컬럼까지)
|
||||||
|
if (excelCol <= 26) // Z열까지
|
||||||
|
{
|
||||||
|
// CellText가 비어있어도 일단 배치해보기 (디버그용)
|
||||||
|
var cellValue = string.IsNullOrEmpty(cellBoundary.CellText) ? "[빈셀]" : cellBoundary.CellText;
|
||||||
|
// 텍스트 형식으로 설정하여 "0:0" 같은 값이 시간으로 포맷되지 않도록 함
|
||||||
|
var cell = (Excel.Range)worksheet.Cells[excelRow, excelCol];
|
||||||
|
cell.NumberFormat = "@"; // 텍스트 형식
|
||||||
|
cell.Value = cellValue;
|
||||||
|
Debug.WriteLine($"[DEBUG] ✅ 셀 복사: R{r}C{c} → Excel[{excelRow},{excelCol}] = '{cellValue}'");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[DEBUG] ❌ Excel 컬럼 범위 초과: {excelCol} > 26");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 테이블이 차지하는 최대 행 수 추적
|
||||||
|
maxTableRow = Math.Max(maxTableRow, r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[DEBUG] ❌ 잘못된 Range: {cellBoundary.Label} → R{sRow}C{sCol}:R{eRow}C{eCol}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 테이블이 여러 행을 차지하는 경우 currentRow 업데이트
|
||||||
|
if (maxTableRow > 1)
|
||||||
|
{
|
||||||
|
currentRow += (maxTableRow - 1);
|
||||||
|
Debug.WriteLine($"[DEBUG] 테이블 행 수만큼 currentRow 업데이트: +{maxTableRow - 1} → {currentRow}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(noteEntity.TableCsv))
|
||||||
|
{
|
||||||
|
// 기존 TableCsv 방식 (백업용)
|
||||||
|
var tableRows = noteEntity.TableCsv.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
Debug.WriteLine($"[DEBUG] 기존 표 데이터 처리: Row {row}, 표 행 수={tableRows.Length}");
|
||||||
|
|
||||||
|
for (int tableRowIndex = 0; tableRowIndex < tableRows.Length; tableRowIndex++)
|
||||||
|
{
|
||||||
|
var tableCells = tableRows[tableRowIndex].Split(',');
|
||||||
|
|
||||||
|
// 각 셀을 G열부터 배치 (최대 15개 컬럼까지)
|
||||||
|
for (int cellIndex = 0; cellIndex < Math.Min(tableCells.Length, 15); cellIndex++)
|
||||||
|
{
|
||||||
|
var cellValue = tableCells[cellIndex].Trim().Trim('"'); // 따옴표 제거
|
||||||
|
// 텍스트 형식으로 설정하여 "0:0" 같은 값이 시간으로 포맷되지 않도록 함
|
||||||
|
var cell = (Excel.Range)worksheet.Cells[currentRow, 7 + cellIndex];
|
||||||
|
cell.NumberFormat = "@"; // 텍스트 형식
|
||||||
|
cell.Value = cellValue; // G열(7)부터 시작
|
||||||
|
}
|
||||||
|
|
||||||
|
// 표의 첫 번째 행이 아니면 새로운 Excel 행 추가
|
||||||
|
if (tableRowIndex > 0)
|
||||||
|
{
|
||||||
|
currentRow++;
|
||||||
|
// 새로운 행에는 기본 Note 정보 복사 (Type, Layer 등)
|
||||||
|
worksheet.Cells[currentRow, 1] = 0;
|
||||||
|
worksheet.Cells[currentRow, 2] = noteEntity.Type ?? "";
|
||||||
|
worksheet.Cells[currentRow, 3] = noteEntity.Layer ?? "";
|
||||||
|
worksheet.Cells[currentRow, 4] = "";
|
||||||
|
worksheet.Cells[currentRow, 5] = fileName ?? "";
|
||||||
|
worksheet.Cells[currentRow, 6] = "(continued)"; // 연속 표시
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.WriteLine($"[DEBUG] 표 행 {tableRowIndex + 1}/{tableRows.Length}: Excel Row {currentRow}, 셀 수={tableCells.Length}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// "NOTE" 타입인 경우 행 배경색 변경 (표 영역 포함)
|
||||||
|
if (noteEntity.Type == "Note")
|
||||||
|
{
|
||||||
|
Excel.Range noteRowRange = worksheet.Range[worksheet.Cells[row, 1], worksheet.Cells[currentRow, 26]]; // Z열까지
|
||||||
|
noteRowRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightYellow);
|
||||||
|
noteRowRange.Font.Bold = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.WriteLine($"[DEBUG] Excel 기록: Row {row}~{currentRow}, Order {noteEntity.SortOrder}, Type {noteEntity.Type}, Pos({noteEntity.X:F1},{noteEntity.Y:F1}), Text: '{noteEntity.Text}', HasCellBoundaries: {noteEntity.CellBoundaries?.Count > 0} (Count: {noteEntity.CellBoundaries?.Count ?? 0}), HasTableCsv: {!string.IsNullOrEmpty(noteEntity.TableCsv)}");
|
||||||
|
|
||||||
|
// CellBoundaries 상세 디버그
|
||||||
|
if (noteEntity.CellBoundaries != null && noteEntity.CellBoundaries.Count > 0)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[DEBUG] CellBoundaries 상세:");
|
||||||
|
foreach (var cb in noteEntity.CellBoundaries.Take(5)) // 처음 5개만 출력
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[DEBUG] {cb.Label}: '{cb.CellText}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 다음 Note는 현재 행의 다음 행부터 시작
|
||||||
|
row = currentRow + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.WriteLine($"[DEBUG] Note 데이터 기록 완료: {row - startRow}개 항목");
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[DEBUG] Note 데이터 입력 오류: {ex.Message}");
|
||||||
|
Debug.WriteLine($"[DEBUG] 처리된 행: {row - startRow}개");
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutoFit 시도 (오류 발생시 무시)
|
||||||
|
try
|
||||||
|
{
|
||||||
|
worksheet.Columns.AutoFit();
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[DEBUG] AutoFit 오류 (무시됨): {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"❌ WriteNoteEntities 전체 오류: {ex.Message}");
|
||||||
|
Debug.WriteLine($" 스택 트레이스: {ex.StackTrace}");
|
||||||
|
throw; // 상위로 예외 전파
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 라벨에서 셀 범위 정보를 파싱합니다.
|
||||||
|
/// 예: "R1C2" → (1, 2, 1, 2) 또는 "R2C2→R3C4" → (2, 2, 3, 4)
|
||||||
|
/// </summary>
|
||||||
|
private (int sRow, int sCol, int eRow, int eCol) ParseCellRangeFromLabel(string label)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(label))
|
||||||
|
return (0, 0, 0, 0);
|
||||||
|
|
||||||
|
if (label.Contains("→"))
|
||||||
|
{
|
||||||
|
// "R2C2→R3C4" 형태 - 범위 파싱
|
||||||
|
var parts = label.Split('→');
|
||||||
|
if (parts.Length == 2)
|
||||||
|
{
|
||||||
|
var (sRow, sCol) = ParseSingleCell(parts[0]);
|
||||||
|
var (eRow, eCol) = ParseSingleCell(parts[1]);
|
||||||
|
return (sRow, sCol, eRow, eCol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// "R1C2" 형태 - 단일 셀
|
||||||
|
var (row, col) = ParseSingleCell(label);
|
||||||
|
return (row, col, row, col);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return (0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 단일 셀 위치를 파싱합니다. (예: "R2C3" → (2, 3))
|
||||||
|
/// </summary>
|
||||||
|
private (int row, int col) ParseSingleCell(string cellStr)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(cellStr))
|
||||||
|
return (0, 0);
|
||||||
|
|
||||||
|
var rIndex = cellStr.IndexOf('R');
|
||||||
|
var cIndex = cellStr.IndexOf('C');
|
||||||
|
|
||||||
|
if (rIndex >= 0 && cIndex > rIndex)
|
||||||
|
{
|
||||||
|
var rowStr = cellStr.Substring(rIndex + 1, cIndex - rIndex - 1);
|
||||||
|
var colStr = cellStr.Substring(cIndex + 1);
|
||||||
|
|
||||||
|
if (int.TryParse(rowStr, out int row) && int.TryParse(colStr, out int col))
|
||||||
|
{
|
||||||
|
return (row, col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (0, 0);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return (0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
338
Models/ExcelManager.cs
Normal file
338
Models/ExcelManager.cs
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Excel = Microsoft.Office.Interop.Excel;
|
||||||
|
|
||||||
|
namespace DwgExtractorManual.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Excel COM <20><>ü <20><><EFBFBD><EFBFBD> <20><> <20>⺻ <20>۾<EFBFBD><DBBE><EFBFBD> <20><><EFBFBD><EFBFBD>ϴ<EFBFBD> Ŭ<><C5AC><EFBFBD><EFBFBD>
|
||||||
|
/// </summary>
|
||||||
|
internal class ExcelManager : IDisposable
|
||||||
|
{
|
||||||
|
public Excel.Application? ExcelApplication { get; private set; }
|
||||||
|
public Excel.Workbook? TitleBlockWorkbook { get; private set; }
|
||||||
|
public Excel.Workbook? MappingWorkbook { get; private set; }
|
||||||
|
|
||||||
|
public Excel.Worksheet? TitleBlockSheet { get; private set; }
|
||||||
|
public Excel.Worksheet? TextEntitiesSheet { get; private set; }
|
||||||
|
public Excel.Worksheet? NoteEntitiesSheet { get; private set; }
|
||||||
|
public Excel.Worksheet? MappingSheet { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Excel <20><><EFBFBD>ø<EFBFBD><C3B8><EFBFBD><EFBFBD>̼<EFBFBD> <20><> <20><>ũ<EFBFBD><C5A9>Ʈ <20>ʱ<EFBFBD>ȭ
|
||||||
|
/// </summary>
|
||||||
|
public void InitializeExcel()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var excelApp = new Excel.Application();
|
||||||
|
ExcelApplication = excelApp;
|
||||||
|
ExcelApplication.Visible = false; // WPF<50><46><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> ó<><C3B3>
|
||||||
|
Excel.Workbook workbook = excelApp.Workbooks.Add();
|
||||||
|
TitleBlockWorkbook = workbook;
|
||||||
|
|
||||||
|
// Title Block Sheet <20><><EFBFBD><EFBFBD> (<28>⺻ Sheet1)
|
||||||
|
TitleBlockSheet = (Excel.Worksheet)workbook.Sheets[1];
|
||||||
|
TitleBlockSheet.Name = "Title Block";
|
||||||
|
SetupTitleBlockHeaders();
|
||||||
|
|
||||||
|
// Text Entities Sheet <20>߰<EFBFBD>
|
||||||
|
TextEntitiesSheet = (Excel.Worksheet)workbook.Sheets.Add();
|
||||||
|
TextEntitiesSheet.Name = "Text Entities";
|
||||||
|
SetupTextEntitiesHeaders();
|
||||||
|
|
||||||
|
// Note Entities Sheet <20>߰<EFBFBD>
|
||||||
|
NoteEntitiesSheet = (Excel.Worksheet)workbook.Sheets.Add();
|
||||||
|
NoteEntitiesSheet.Name = "Note Entities";
|
||||||
|
SetupNoteEntitiesHeaders();
|
||||||
|
|
||||||
|
// <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>Ϳ<EFBFBD> <20><>ũ<EFBFBD><C5A9> <20><> <20><>Ʈ <20><><EFBFBD><EFBFBD>
|
||||||
|
MappingWorkbook = excelApp.Workbooks.Add();
|
||||||
|
MappingSheet = (Excel.Worksheet)MappingWorkbook.Sheets[1];
|
||||||
|
MappingSheet.Name = "Mapping Data";
|
||||||
|
SetupMappingHeaders();
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"Excel <20>ʱ<EFBFBD>ȭ <20><> <20><><EFBFBD><EFBFBD> <20><EFBFBD>: {ex.Message}");
|
||||||
|
ReleaseExcelObjects();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <20><><EFBFBD><EFBFBD> Excel <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><>Ʈ<EFBFBD><C6AE> <20><><EFBFBD><EFBFBD>
|
||||||
|
/// </summary>
|
||||||
|
public bool OpenExistingFile(string excelFilePath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!File.Exists(excelFilePath))
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"? Excel <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20>ʽ<EFBFBD><CABD>ϴ<EFBFBD>: {excelFilePath}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ExcelApplication == null)
|
||||||
|
{
|
||||||
|
ExcelApplication = new Excel.Application();
|
||||||
|
ExcelApplication.Visible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
MappingWorkbook = ExcelApplication.Workbooks.Open(excelFilePath);
|
||||||
|
MappingSheet = (Excel.Worksheet)MappingWorkbook.Sheets["Mapping Data"];
|
||||||
|
|
||||||
|
if (MappingSheet == null)
|
||||||
|
{
|
||||||
|
Debug.WriteLine("? 'Mapping Data' <20><>Ʈ<EFBFBD><C6AE> ã<><C3A3> <20><> <20><><EFBFBD><EFBFBD><EFBFBD>ϴ<EFBFBD>.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"? Excel <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD>: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <20><><EFBFBD>ο<EFBFBD> <20><>ũ<EFBFBD><C5A9> <20><><EFBFBD><EFBFBD> (Height <20><><EFBFBD>Ŀ<EFBFBD>)
|
||||||
|
/// </summary>
|
||||||
|
public Excel.Workbook CreateNewWorkbook()
|
||||||
|
{
|
||||||
|
if (ExcelApplication == null)
|
||||||
|
{
|
||||||
|
ExcelApplication = new Excel.Application();
|
||||||
|
ExcelApplication.Visible = false;
|
||||||
|
}
|
||||||
|
return ExcelApplication.Workbooks.Add();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Title Block <20><>Ʈ <20><><EFBFBD> <20><><EFBFBD><EFBFBD>
|
||||||
|
private void SetupTitleBlockHeaders()
|
||||||
|
{
|
||||||
|
if (TitleBlockSheet == null) return;
|
||||||
|
|
||||||
|
TitleBlockSheet.Cells[1, 1] = "Type"; // <20><>: AttributeReference, AttributeDefinition
|
||||||
|
TitleBlockSheet.Cells[1, 2] = "Name"; // BlockReference <20≯<EFBFBD> <20>Ǵ<EFBFBD> BlockDefinition <20≯<EFBFBD>
|
||||||
|
TitleBlockSheet.Cells[1, 3] = "Tag"; // Attribute Tag
|
||||||
|
TitleBlockSheet.Cells[1, 4] = "Prompt"; // Attribute Prompt
|
||||||
|
TitleBlockSheet.Cells[1, 5] = "Value"; // Attribute <20><> (TextString)
|
||||||
|
TitleBlockSheet.Cells[1, 6] = "Path"; // <20><><EFBFBD><EFBFBD> DWG <20><><EFBFBD><EFBFBD> <20><>ü <20><><EFBFBD>
|
||||||
|
TitleBlockSheet.Cells[1, 7] = "FileName"; // <20><><EFBFBD><EFBFBD> DWG <20><><EFBFBD><EFBFBD> <20≯<EFBFBD><CCB8><EFBFBD>
|
||||||
|
|
||||||
|
// <20><><EFBFBD> <20><> <20><>Ÿ<EFBFBD><C5B8>
|
||||||
|
Excel.Range headerRange = TitleBlockSheet.Range["A1:G1"];
|
||||||
|
headerRange.Font.Bold = true;
|
||||||
|
headerRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightBlue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text Entities <20><>Ʈ <20><><EFBFBD> <20><><EFBFBD><EFBFBD>
|
||||||
|
private void SetupTextEntitiesHeaders()
|
||||||
|
{
|
||||||
|
if (TextEntitiesSheet == null) return;
|
||||||
|
|
||||||
|
TextEntitiesSheet.Cells[1, 1] = "Type"; // DBText, MText
|
||||||
|
TextEntitiesSheet.Cells[1, 2] = "Layer"; // Layer <20≯<EFBFBD>
|
||||||
|
TextEntitiesSheet.Cells[1, 3] = "Text"; // <20><><EFBFBD><EFBFBD> <20>ؽ<EFBFBD>Ʈ <20><><EFBFBD><EFBFBD>
|
||||||
|
TextEntitiesSheet.Cells[1, 4] = "Path"; // <20><><EFBFBD><EFBFBD> DWG <20><><EFBFBD><EFBFBD> <20><>ü <20><><EFBFBD>
|
||||||
|
TextEntitiesSheet.Cells[1, 5] = "FileName"; // <20><><EFBFBD><EFBFBD> DWG <20><><EFBFBD><EFBFBD> <20≯<EFBFBD><CCB8><EFBFBD>
|
||||||
|
|
||||||
|
// <20><><EFBFBD> <20><> <20><>Ÿ<EFBFBD><C5B8>
|
||||||
|
Excel.Range headerRange = TextEntitiesSheet.Range["A1:E1"];
|
||||||
|
headerRange.Font.Bold = true;
|
||||||
|
headerRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightGreen);
|
||||||
|
}
|
||||||
|
|
||||||
|
// <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><>Ʈ <20><><EFBFBD> <20><><EFBFBD><EFBFBD>
|
||||||
|
private void SetupMappingHeaders()
|
||||||
|
{
|
||||||
|
if (MappingSheet == null) return;
|
||||||
|
|
||||||
|
MappingSheet.Cells[1, 1] = "FileName"; // <20><><EFBFBD><EFBFBD> <20≯<EFBFBD>
|
||||||
|
MappingSheet.Cells[1, 2] = "MapKey"; // <20><><EFBFBD><EFBFBD> Ű
|
||||||
|
MappingSheet.Cells[1, 3] = "AILabel"; // AI <20><>
|
||||||
|
MappingSheet.Cells[1, 4] = "DwgTag"; // DWG Tag
|
||||||
|
MappingSheet.Cells[1, 5] = "Att_value"; // DWG <20><>
|
||||||
|
MappingSheet.Cells[1, 6] = "Pdf_value"; // PDF <20><> (<28><><EFBFBD><EFBFBD><EFBFBD> <20><> <20><>)
|
||||||
|
|
||||||
|
// <20><><EFBFBD> <20><> <20><>Ÿ<EFBFBD><C5B8>
|
||||||
|
Excel.Range headerRange = MappingSheet.Range["A1:F1"];
|
||||||
|
headerRange.Font.Bold = true;
|
||||||
|
headerRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightYellow);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note Entities <20><>Ʈ <20><><EFBFBD> <20><><EFBFBD><EFBFBD>
|
||||||
|
private void SetupNoteEntitiesHeaders()
|
||||||
|
{
|
||||||
|
if (NoteEntitiesSheet == null) return;
|
||||||
|
|
||||||
|
NoteEntitiesSheet.Cells[1, 1] = "Type"; // Note, NoteContent
|
||||||
|
NoteEntitiesSheet.Cells[1, 2] = "Layer"; // Layer <20≯<EFBFBD>
|
||||||
|
NoteEntitiesSheet.Cells[1, 3] = "Text"; // <20><><EFBFBD><EFBFBD> <20>ؽ<EFBFBD>Ʈ <20><><EFBFBD><EFBFBD>
|
||||||
|
NoteEntitiesSheet.Cells[1, 4] = "X"; // X <20><>ǥ
|
||||||
|
NoteEntitiesSheet.Cells[1, 5] = "Y"; // Y <20><>ǥ
|
||||||
|
NoteEntitiesSheet.Cells[1, 6] = "SortOrder"; // <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>
|
||||||
|
NoteEntitiesSheet.Cells[1, 7] = "TableCsv"; // <20><><EFBFBD>̺<EFBFBD> CSV <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
NoteEntitiesSheet.Cells[1, 8] = "Path"; // <20><><EFBFBD><EFBFBD> DWG <20><><EFBFBD><EFBFBD> <20><>ü <20><><EFBFBD>
|
||||||
|
NoteEntitiesSheet.Cells[1, 9] = "FileName"; // <20><><EFBFBD><EFBFBD> DWG <20><><EFBFBD><EFBFBD> <20≯<EFBFBD><CCB8><EFBFBD>
|
||||||
|
|
||||||
|
// <20><><EFBFBD> <20><> <20><>Ÿ<EFBFBD><C5B8>
|
||||||
|
Excel.Range headerRange = NoteEntitiesSheet.Range["A1:I1"];
|
||||||
|
headerRange.Font.Bold = true;
|
||||||
|
headerRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightCoral);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <20><>ũ<EFBFBD><C5A9> <20><><EFBFBD><EFBFBD>
|
||||||
|
/// </summary>
|
||||||
|
public bool SaveWorkbook(Excel.Workbook? workbook = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (workbook != null)
|
||||||
|
{
|
||||||
|
workbook.Save();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MappingWorkbook != null)
|
||||||
|
{
|
||||||
|
MappingWorkbook.Save();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TitleBlockWorkbook != null)
|
||||||
|
{
|
||||||
|
TitleBlockWorkbook.Save();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"? Excel <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD>: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <20><>ũ<EFBFBD><C5A9><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><>ο<EFBFBD> <20><><EFBFBD><EFBFBD>
|
||||||
|
/// </summary>
|
||||||
|
public void SaveWorkbookAs(Excel.Workbook? workbook, string savePath)
|
||||||
|
{
|
||||||
|
if (workbook == null) return;
|
||||||
|
|
||||||
|
string? directory = Path.GetDirectoryName(savePath);
|
||||||
|
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
workbook.SaveAs(savePath,
|
||||||
|
FileFormat: Excel.XlFileFormat.xlOpenXMLWorkbook,
|
||||||
|
AccessMode: Excel.XlSaveAsAccessMode.xlNoChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Excel <20><>Ʈ<EFBFBD><C6AE><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><> <20>ִ<EFBFBD> <20><>ȿ<EFBFBD><C8BF> <20≯<EFBFBD><CCB8><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>մϴ<D5B4>.
|
||||||
|
/// </summary>
|
||||||
|
public string GetValidSheetName(string originalName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(originalName))
|
||||||
|
return "Sheet";
|
||||||
|
|
||||||
|
// Excel <20><>Ʈ<EFBFBD><C6AE><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20>ʴ<EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>
|
||||||
|
string validName = originalName;
|
||||||
|
char[] invalidChars = { '\\', '/', '?', '*', '[', ']', ':' };
|
||||||
|
|
||||||
|
foreach (char c in invalidChars)
|
||||||
|
{
|
||||||
|
validName = validName.Replace(c, '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 31<33>ڷ<EFBFBD> <20><><EFBFBD><EFBFBD> (Excel <20><>Ʈ<EFBFBD><C6AE> <20>ִ<EFBFBD> <20><><EFBFBD><EFBFBD>)
|
||||||
|
if (validName.Length > 31)
|
||||||
|
{
|
||||||
|
validName = validName.Substring(0, 31);
|
||||||
|
}
|
||||||
|
|
||||||
|
return validName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CloseWorkbooks()
|
||||||
|
{
|
||||||
|
if (TitleBlockWorkbook != null)
|
||||||
|
{
|
||||||
|
try { TitleBlockWorkbook.Close(false); }
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
if (MappingWorkbook != null)
|
||||||
|
{
|
||||||
|
try { MappingWorkbook.Close(false); }
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
if (ExcelApplication != null)
|
||||||
|
{
|
||||||
|
try { ExcelApplication.Quit(); }
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReleaseExcelObjects()
|
||||||
|
{
|
||||||
|
ReleaseComObject(TitleBlockSheet);
|
||||||
|
ReleaseComObject(TextEntitiesSheet);
|
||||||
|
ReleaseComObject(NoteEntitiesSheet);
|
||||||
|
ReleaseComObject(MappingSheet);
|
||||||
|
ReleaseComObject(TitleBlockWorkbook);
|
||||||
|
ReleaseComObject(MappingWorkbook);
|
||||||
|
ReleaseComObject(ExcelApplication);
|
||||||
|
|
||||||
|
TitleBlockSheet = null;
|
||||||
|
TextEntitiesSheet = null;
|
||||||
|
NoteEntitiesSheet = null;
|
||||||
|
MappingSheet = null;
|
||||||
|
TitleBlockWorkbook = null;
|
||||||
|
MappingWorkbook = null;
|
||||||
|
ExcelApplication = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReleaseComObject(object? obj)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (obj != null && Marshal.IsComObject(obj))
|
||||||
|
{
|
||||||
|
Marshal.ReleaseComObject(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (System.Exception)
|
||||||
|
{
|
||||||
|
// <20><><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD> <20><EFBFBD> <20><> <20><><EFBFBD><EFBFBD>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Debug.WriteLine("[DEBUG] ExcelManager Dispose <20><><EFBFBD><EFBFBD>");
|
||||||
|
CloseWorkbooks();
|
||||||
|
ReleaseExcelObjects();
|
||||||
|
GC.Collect();
|
||||||
|
GC.WaitForPendingFinalizers();
|
||||||
|
Debug.WriteLine("[DEBUG] ExcelManager Dispose <20>Ϸ<EFBFBD>");
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[DEBUG] ExcelManager Dispose <20><> <20><><EFBFBD><EFBFBD>: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
267
Models/IntersectionTestDebugger.cs
Normal file
267
Models/IntersectionTestDebugger.cs
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using Teigha.Geometry;
|
||||||
|
|
||||||
|
namespace DwgExtractorManual.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 교차점 생성 및 셀 추출 로직을 테스트하고 디버깅하는 클래스
|
||||||
|
/// </summary>
|
||||||
|
public class IntersectionTestDebugger
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 간단한 테스트 테이블을 만들어서 교차점과 셀 생성을 테스트합니다.
|
||||||
|
/// </summary>
|
||||||
|
public static void RunIntersectionTest()
|
||||||
|
{
|
||||||
|
Debug.WriteLine("=== 교차점 및 셀 생성 테스트 시작 ===");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 테스트용 테이블 생성 (3x4 그리드)
|
||||||
|
var testSegments = CreateTestTable();
|
||||||
|
Debug.WriteLine($"테스트 선분 개수: {testSegments.Count}");
|
||||||
|
|
||||||
|
// DwgDataExtractor 인스턴스 생성 (실제 코드와 동일)
|
||||||
|
var mappingData = new MappingTableData();
|
||||||
|
var fieldMapper = new FieldMapper(mappingData);
|
||||||
|
var extractor = new DwgDataExtractor(fieldMapper);
|
||||||
|
|
||||||
|
// 교차점 찾기 테스트
|
||||||
|
var intersections = FindTestIntersections(testSegments, extractor);
|
||||||
|
Debug.WriteLine($"발견된 교차점 개수: {intersections.Count}");
|
||||||
|
|
||||||
|
// 각 교차점의 DirectionBits 출력
|
||||||
|
for (int i = 0; i < intersections.Count; i++)
|
||||||
|
{
|
||||||
|
var intersection = intersections[i];
|
||||||
|
Debug.WriteLine($"교차점 {i}: ({intersection.Position.X:F1}, {intersection.Position.Y:F1}) - DirectionBits: {intersection.DirectionBits} - R{intersection.Row}C{intersection.Column}");
|
||||||
|
|
||||||
|
// topLeft/bottomRight 검증
|
||||||
|
bool isTopLeft = extractor.IsValidTopLeft(intersection.DirectionBits);
|
||||||
|
bool isBottomRight = extractor.IsValidBottomRight(intersection.DirectionBits);
|
||||||
|
Debug.WriteLine($" IsTopLeft: {isTopLeft}, IsBottomRight: {isBottomRight}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.WriteLine("=== 테스트 완료 ===");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"테스트 중 오류 발생: {ex.Message}");
|
||||||
|
Debug.WriteLine(ex.StackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 테스트용 3x4 테이블 선분들을 생성합니다.
|
||||||
|
/// </summary>
|
||||||
|
private static List<(Point3d start, Point3d end, bool isHorizontal)> CreateTestTable()
|
||||||
|
{
|
||||||
|
var segments = new List<(Point3d start, Point3d end, bool isHorizontal)>();
|
||||||
|
|
||||||
|
// 수평선들 (4개 - 0, 10, 20, 30 Y좌표)
|
||||||
|
for (int i = 0; i <= 3; i++)
|
||||||
|
{
|
||||||
|
double y = i * 10.0;
|
||||||
|
segments.Add((new Point3d(0, y, 0), new Point3d(40, y, 0), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 수직선들 (5개 - 0, 10, 20, 30, 40 X좌표)
|
||||||
|
for (int i = 0; i <= 4; i++)
|
||||||
|
{
|
||||||
|
double x = i * 10.0;
|
||||||
|
segments.Add((new Point3d(x, 0, 0), new Point3d(x, 30, 0), false));
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.WriteLine($"생성된 테스트 테이블: 수평선 4개, 수직선 5개");
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 테스트 선분들로부터 교차점을 찾습니다.
|
||||||
|
/// </summary>
|
||||||
|
private static List<IntersectionPoint> FindTestIntersections(List<(Point3d start, Point3d end, bool isHorizontal)> segments, DwgDataExtractor extractor)
|
||||||
|
{
|
||||||
|
var intersections = new List<IntersectionPoint>();
|
||||||
|
double tolerance = 0.1;
|
||||||
|
|
||||||
|
var horizontalSegments = segments.Where(s => s.isHorizontal).ToList();
|
||||||
|
var verticalSegments = segments.Where(s => !s.isHorizontal).ToList();
|
||||||
|
|
||||||
|
foreach (var hSeg in horizontalSegments)
|
||||||
|
{
|
||||||
|
foreach (var vSeg in verticalSegments)
|
||||||
|
{
|
||||||
|
// 교차점 계산
|
||||||
|
double intersectX = vSeg.start.X;
|
||||||
|
double intersectY = hSeg.start.Y;
|
||||||
|
var intersectPoint = new Point3d(intersectX, intersectY, 0);
|
||||||
|
|
||||||
|
// 교차점이 두 선분의 범위 내에 있는지 확인
|
||||||
|
bool onHorizontal = intersectX >= Math.Min(hSeg.start.X, hSeg.end.X) - tolerance &&
|
||||||
|
intersectX <= Math.Max(hSeg.start.X, hSeg.end.X) + tolerance;
|
||||||
|
|
||||||
|
bool onVertical = intersectY >= Math.Min(vSeg.start.Y, vSeg.end.Y) - tolerance &&
|
||||||
|
intersectY <= Math.Max(vSeg.start.Y, vSeg.end.Y) + tolerance;
|
||||||
|
|
||||||
|
if (onHorizontal && onVertical)
|
||||||
|
{
|
||||||
|
// DirectionBits 계산
|
||||||
|
int directionBits = CalculateDirectionBits(intersectPoint, segments, tolerance);
|
||||||
|
|
||||||
|
// Row, Column 계산 (1-based)
|
||||||
|
int row = (int)Math.Round(intersectY / 10.0) + 1;
|
||||||
|
int column = (int)Math.Round(intersectX / 10.0) + 1;
|
||||||
|
|
||||||
|
var intersection = new IntersectionPoint
|
||||||
|
{
|
||||||
|
Position = intersectPoint,
|
||||||
|
DirectionBits = directionBits,
|
||||||
|
Row = row,
|
||||||
|
Column = column
|
||||||
|
};
|
||||||
|
|
||||||
|
intersections.Add(intersection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return intersections;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 특정 점에서의 DirectionBits를 계산합니다.
|
||||||
|
/// </summary>
|
||||||
|
private static int CalculateDirectionBits(Point3d point, List<(Point3d start, Point3d end, bool isHorizontal)> segments, double tolerance)
|
||||||
|
{
|
||||||
|
int bits = 0;
|
||||||
|
// Right: 1, Up: 2, Left: 4, Down: 8
|
||||||
|
|
||||||
|
foreach (var segment in segments)
|
||||||
|
{
|
||||||
|
if (segment.isHorizontal)
|
||||||
|
{
|
||||||
|
// 수평선에서 점이 선분 위에 있는지 확인
|
||||||
|
if (Math.Abs(point.Y - segment.start.Y) < tolerance &&
|
||||||
|
point.X >= Math.Min(segment.start.X, segment.end.X) - tolerance &&
|
||||||
|
point.X <= Math.Max(segment.start.X, segment.end.X) + tolerance)
|
||||||
|
{
|
||||||
|
// 오른쪽으로 선분이 있는지 확인
|
||||||
|
if (Math.Max(segment.start.X, segment.end.X) > point.X + tolerance)
|
||||||
|
bits |= 1; // Right
|
||||||
|
|
||||||
|
// 왼쪽으로 선분이 있는지 확인
|
||||||
|
if (Math.Min(segment.start.X, segment.end.X) < point.X - tolerance)
|
||||||
|
bits |= 4; // Left
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 수직선에서 점이 선분 위에 있는지 확인
|
||||||
|
if (Math.Abs(point.X - segment.start.X) < tolerance &&
|
||||||
|
point.Y >= Math.Min(segment.start.Y, segment.end.Y) - tolerance &&
|
||||||
|
point.Y <= Math.Max(segment.start.Y, segment.end.Y) + tolerance)
|
||||||
|
{
|
||||||
|
// 위쪽으로 선분이 있는지 확인
|
||||||
|
if (Math.Max(segment.start.Y, segment.end.Y) > point.Y + tolerance)
|
||||||
|
bits |= 2; // Up
|
||||||
|
|
||||||
|
// 아래쪽으로 선분이 있는지 확인
|
||||||
|
if (Math.Min(segment.start.Y, segment.end.Y) < point.Y - tolerance)
|
||||||
|
bits |= 8; // Down
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bits;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 교차점들로부터 셀을 추출합니다.
|
||||||
|
/// </summary>
|
||||||
|
private static List<TableCell> ExtractTestCells(List<IntersectionPoint> intersections,
|
||||||
|
List<(Point3d start, Point3d end, bool isHorizontal)> segments,
|
||||||
|
DwgDataExtractor extractor)
|
||||||
|
{
|
||||||
|
var cells = new List<TableCell>();
|
||||||
|
double tolerance = 0.1;
|
||||||
|
|
||||||
|
// topLeft 후보들을 찾아서 각각에 대해 bottomRight를 찾기
|
||||||
|
var topLeftCandidates = intersections.Where(i => extractor.IsValidTopLeft(i.DirectionBits)).ToList();
|
||||||
|
Debug.WriteLine($"TopLeft 후보 개수: {topLeftCandidates.Count}");
|
||||||
|
|
||||||
|
foreach (var topLeft in topLeftCandidates)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"\nTopLeft 후보 R{topLeft.Row}C{topLeft.Column} 처리 중...");
|
||||||
|
|
||||||
|
// bottomRight 찾기 (실제 코드와 동일한 방식)
|
||||||
|
var bottomRight = FindBottomRightForTest(topLeft, intersections, extractor);
|
||||||
|
|
||||||
|
if (bottomRight != null)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($" BottomRight 발견: R{bottomRight.Row}C{bottomRight.Column}");
|
||||||
|
|
||||||
|
// 셀 생성
|
||||||
|
var cell = new TableCell
|
||||||
|
{
|
||||||
|
MinPoint = new Point3d(topLeft.Position.X, bottomRight.Position.Y, 0),
|
||||||
|
MaxPoint = new Point3d(bottomRight.Position.X, topLeft.Position.Y, 0),
|
||||||
|
Row = topLeft.Row,
|
||||||
|
Column = topLeft.Column,
|
||||||
|
CellText = $"R{topLeft.Row}C{topLeft.Column}"
|
||||||
|
};
|
||||||
|
|
||||||
|
cells.Add(cell);
|
||||||
|
Debug.WriteLine($" 셀 생성 완료: ({cell.MinPoint.X:F1},{cell.MinPoint.Y:F1}) to ({cell.MaxPoint.X:F1},{cell.MaxPoint.Y:F1})");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.WriteLine($" BottomRight을 찾지 못함");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cells;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 테스트용 bottomRight 찾기 메서드
|
||||||
|
/// </summary>
|
||||||
|
private static IntersectionPoint FindBottomRightForTest(IntersectionPoint topLeft,
|
||||||
|
List<IntersectionPoint> intersections,
|
||||||
|
DwgDataExtractor extractor)
|
||||||
|
{
|
||||||
|
// 교차점들을 Row/Column으로 딕셔너리 구성
|
||||||
|
var intersectionLookup = intersections
|
||||||
|
.Where(i => i.Row > 0 && i.Column > 0)
|
||||||
|
.GroupBy(i => i.Row)
|
||||||
|
.ToDictionary(g => g.Key, g => g.ToDictionary(i => i.Column, i => i));
|
||||||
|
|
||||||
|
// topLeft에서 시작하여 bottomRight 찾기
|
||||||
|
int maxRow = intersectionLookup.Keys.Any() ? intersectionLookup.Keys.Max() : topLeft.Row;
|
||||||
|
|
||||||
|
for (int targetRow = topLeft.Row + 1; targetRow <= maxRow + 2; targetRow++)
|
||||||
|
{
|
||||||
|
if (!intersectionLookup.ContainsKey(targetRow)) continue;
|
||||||
|
|
||||||
|
var rowIntersections = intersectionLookup[targetRow];
|
||||||
|
var availableColumns = rowIntersections.Keys.Where(col => col >= topLeft.Column).OrderBy(col => col);
|
||||||
|
|
||||||
|
foreach (int targetColumn in availableColumns)
|
||||||
|
{
|
||||||
|
var candidate = rowIntersections[targetColumn];
|
||||||
|
|
||||||
|
if (extractor.IsValidBottomRight(candidate.DirectionBits) ||
|
||||||
|
(targetRow == maxRow && targetColumn == intersectionLookup.Values.SelectMany(row => row.Keys).Max()))
|
||||||
|
{
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
317
Models/JsonDataProcessor.cs
Normal file
317
Models/JsonDataProcessor.cs
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace DwgExtractorManual.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// JSON 파일 처리 및 매핑 데이터 관리를 담당하는 클래스
|
||||||
|
/// </summary>
|
||||||
|
internal class JsonDataProcessor
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// JSON 파일에서 PDF 분석 결과를 읽어 매핑 데이터를 업데이트
|
||||||
|
/// </summary>
|
||||||
|
public bool UpdateMappingDataFromJson(Dictionary<string, Dictionary<string, (string, string, string, string)>> mappingData, string jsonFilePath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[DEBUG] JSON 파일에서 PDF 값 업데이트 시작: {jsonFilePath}");
|
||||||
|
|
||||||
|
if (!File.Exists(jsonFilePath))
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"? JSON 파일이 존재하지 않습니다: {jsonFilePath}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON 파일 읽기 및 정리
|
||||||
|
string jsonContent = File.ReadAllText(jsonFilePath, System.Text.Encoding.UTF8);
|
||||||
|
jsonContent = CleanJsonContent(jsonContent);
|
||||||
|
|
||||||
|
JObject jsonData;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
jsonData = JObject.Parse(jsonContent);
|
||||||
|
}
|
||||||
|
catch (Newtonsoft.Json.JsonReaderException jsonEx)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"? JSON 파싱 오류: {jsonEx.Message}");
|
||||||
|
throw new System.Exception($"PDF 분석 JSON 파일 파싱 실패: {jsonEx.Message}\n파일: {jsonFilePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var results = jsonData["results"] as JArray;
|
||||||
|
if (results == null)
|
||||||
|
{
|
||||||
|
Debug.WriteLine("? JSON에서 'results' 배열을 찾을 수 없습니다.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int updatedCount = 0;
|
||||||
|
int totalEntries = 0;
|
||||||
|
|
||||||
|
foreach (JObject result in results)
|
||||||
|
{
|
||||||
|
var fileInfo = result["file_info"];
|
||||||
|
var pdfAnalysis = result["pdf_analysis"];
|
||||||
|
|
||||||
|
if (fileInfo == null || pdfAnalysis == null) continue;
|
||||||
|
|
||||||
|
string fileName = fileInfo["name"]?.ToString();
|
||||||
|
if (string.IsNullOrEmpty(fileName)) continue;
|
||||||
|
|
||||||
|
string fileNameWithoutExt = Path.GetFileNameWithoutExtension(fileName);
|
||||||
|
|
||||||
|
if (!mappingData.ContainsKey(fileNameWithoutExt))
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"?? 매핑 데이터에 파일이 없습니다: {fileNameWithoutExt}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var property in pdfAnalysis.Cast<JProperty>())
|
||||||
|
{
|
||||||
|
string aiLabel = property.Name;
|
||||||
|
var valueObj = property.Value as JObject;
|
||||||
|
|
||||||
|
if (valueObj == null) continue;
|
||||||
|
|
||||||
|
string pdfValue = valueObj["value"]?.ToString();
|
||||||
|
if (string.IsNullOrEmpty(pdfValue)) continue;
|
||||||
|
|
||||||
|
totalEntries++;
|
||||||
|
|
||||||
|
var fileData = mappingData[fileNameWithoutExt];
|
||||||
|
var matchingEntry = fileData.FirstOrDefault(kvp =>
|
||||||
|
string.Equals(kvp.Value.Item1.Trim(), aiLabel.Trim(), StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(matchingEntry.Key))
|
||||||
|
{
|
||||||
|
var existingValue = matchingEntry.Value;
|
||||||
|
fileData[matchingEntry.Key] = (existingValue.Item1, existingValue.Item2, existingValue.Item3, pdfValue);
|
||||||
|
updatedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.WriteLine($"[DEBUG] PDF 데이터 업데이트 완료: {updatedCount}/{totalEntries} 업데이트됨");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"? JSON에서 PDF 값 업데이트 중 오류: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 매핑 딕셔너리를 JSON 파일로 저장
|
||||||
|
/// </summary>
|
||||||
|
public void SaveMappingDictionary(Dictionary<string, Dictionary<string, (string, string, string, string)>> mappingData, string filePath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[DEBUG] 매핑 딕셔너리 저장 시작: {filePath}");
|
||||||
|
|
||||||
|
var serializableData = new Dictionary<string, Dictionary<string, object>>();
|
||||||
|
|
||||||
|
foreach (var fileEntry in mappingData)
|
||||||
|
{
|
||||||
|
var fileData = new Dictionary<string, object>();
|
||||||
|
foreach (var mapEntry in fileEntry.Value)
|
||||||
|
{
|
||||||
|
fileData[mapEntry.Key] = new
|
||||||
|
{
|
||||||
|
AILabel = mapEntry.Value.Item1,
|
||||||
|
DwgTag = mapEntry.Value.Item2,
|
||||||
|
DwgValue = mapEntry.Value.Item3,
|
||||||
|
PdfValue = mapEntry.Value.Item4
|
||||||
|
};
|
||||||
|
}
|
||||||
|
serializableData[fileEntry.Key] = fileData;
|
||||||
|
}
|
||||||
|
|
||||||
|
string jsonContent = JsonConvert.SerializeObject(serializableData, Formatting.Indented);
|
||||||
|
File.WriteAllText(filePath, jsonContent, System.Text.Encoding.UTF8);
|
||||||
|
|
||||||
|
Debug.WriteLine($"? 매핑 딕셔너리 저장 완료: {Path.GetFileName(filePath)}");
|
||||||
|
Debug.WriteLine($"?? 저장된 파일 수: {mappingData.Count}");
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"? 매핑 딕셔너리 저장 중 오류: {ex.Message}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JSON 파일에서 매핑 딕셔너리를 로드
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, Dictionary<string, (string, string, string, string)>> LoadMappingDictionary(string filePath)
|
||||||
|
{
|
||||||
|
var result = new Dictionary<string, Dictionary<string, (string, string, string, string)>>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[DEBUG] 매핑 딕셔너리 로드 시작: {filePath}");
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"?? 매핑 파일이 존재하지 않습니다: {filePath}");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
string jsonContent = File.ReadAllText(filePath, System.Text.Encoding.UTF8);
|
||||||
|
jsonContent = CleanJsonContent(jsonContent);
|
||||||
|
|
||||||
|
Dictionary<string, Dictionary<string, JObject>> deserializedData;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
deserializedData = JsonConvert.DeserializeObject<Dictionary<string, Dictionary<string, JObject>>>(jsonContent);
|
||||||
|
}
|
||||||
|
catch (Newtonsoft.Json.JsonReaderException jsonEx)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"? JSON 파싱 오류: {jsonEx.Message}");
|
||||||
|
throw new System.Exception($"매핑 JSON 파일 파싱 실패: {jsonEx.Message}\n파일: {filePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deserializedData != null)
|
||||||
|
{
|
||||||
|
foreach (var fileEntry in deserializedData)
|
||||||
|
{
|
||||||
|
var fileData = new Dictionary<string, (string, string, string, string)>();
|
||||||
|
|
||||||
|
foreach (var mapEntry in fileEntry.Value)
|
||||||
|
{
|
||||||
|
var valueObj = mapEntry.Value;
|
||||||
|
string aiLabel = valueObj["AILabel"]?.ToString() ?? "";
|
||||||
|
string dwgTag = valueObj["DwgTag"]?.ToString() ?? "";
|
||||||
|
string dwgValue = valueObj["DwgValue"]?.ToString() ?? "";
|
||||||
|
string pdfValue = valueObj["PdfValue"]?.ToString() ?? "";
|
||||||
|
|
||||||
|
fileData[mapEntry.Key] = (aiLabel, dwgTag, dwgValue, pdfValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
result[fileEntry.Key] = fileData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.WriteLine($"? 매핑 딕셔너리 로드 완료");
|
||||||
|
Debug.WriteLine($"?? 로드된 파일 수: {result.Count}");
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"? 매핑 딕셔너리 로드 중 오류: {ex.Message}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JSON 내용을 정리하여 파싱 가능한 상태로 만듭니다.
|
||||||
|
/// </summary>
|
||||||
|
private string CleanJsonContent(string jsonContent)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(jsonContent))
|
||||||
|
return jsonContent;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var lines = jsonContent.Split('\n');
|
||||||
|
var cleanedLines = new List<string>();
|
||||||
|
bool inMultiLineComment = false;
|
||||||
|
|
||||||
|
foreach (string line in lines)
|
||||||
|
{
|
||||||
|
string processedLine = line;
|
||||||
|
|
||||||
|
// 멀티라인 주석 처리
|
||||||
|
if (inMultiLineComment)
|
||||||
|
{
|
||||||
|
int endIndex = processedLine.IndexOf("*/");
|
||||||
|
if (endIndex >= 0)
|
||||||
|
{
|
||||||
|
processedLine = processedLine.Substring(endIndex + 2);
|
||||||
|
inMultiLineComment = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int multiLineStart = processedLine.IndexOf("/*");
|
||||||
|
if (multiLineStart >= 0)
|
||||||
|
{
|
||||||
|
int multiLineEnd = processedLine.IndexOf("*/", multiLineStart + 2);
|
||||||
|
if (multiLineEnd >= 0)
|
||||||
|
{
|
||||||
|
processedLine = processedLine.Substring(0, multiLineStart) +
|
||||||
|
processedLine.Substring(multiLineEnd + 2);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
processedLine = processedLine.Substring(0, multiLineStart);
|
||||||
|
inMultiLineComment = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 싱글라인 주석 제거
|
||||||
|
bool inString = false;
|
||||||
|
bool escaped = false;
|
||||||
|
int commentIndex = -1;
|
||||||
|
|
||||||
|
for (int i = 0; i < processedLine.Length - 1; i++)
|
||||||
|
{
|
||||||
|
char current = processedLine[i];
|
||||||
|
char next = processedLine[i + 1];
|
||||||
|
|
||||||
|
if (escaped)
|
||||||
|
{
|
||||||
|
escaped = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current == '\\')
|
||||||
|
{
|
||||||
|
escaped = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current == '"')
|
||||||
|
{
|
||||||
|
inString = !inString;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inString && current == '/' && next == '/')
|
||||||
|
{
|
||||||
|
commentIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commentIndex >= 0)
|
||||||
|
{
|
||||||
|
processedLine = processedLine.Substring(0, commentIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(processedLine))
|
||||||
|
{
|
||||||
|
cleanedLines.Add(processedLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join("\n", cleanedLines);
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"? JSON 정리 중 오류: {ex.Message}");
|
||||||
|
return jsonContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
177
Models/NoteExtractionTester.cs
Normal file
177
Models/NoteExtractionTester.cs
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace DwgExtractorManual.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Note 박스 텍스트와 테이블 추출 기능을 테스트하는 클래스
|
||||||
|
/// </summary>
|
||||||
|
public class NoteExtractionTester
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// DWG 파일에서 Note 데이터를 추출하고 CSV로 저장하는 전체 테스트
|
||||||
|
/// </summary>
|
||||||
|
public static void TestNoteExtractionAndCsvExport(string dwgFilePath, string outputDirectory)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Debug.WriteLine("=== Note 추출 및 CSV 내보내기 테스트 시작 ===");
|
||||||
|
|
||||||
|
// 출력 디렉토리가 없으면 생성
|
||||||
|
if (!Directory.Exists(outputDirectory))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(outputDirectory);
|
||||||
|
Debug.WriteLine($"출력 디렉토리 생성: {outputDirectory}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Teigha 서비스 초기화
|
||||||
|
Debug.WriteLine("1. Teigha 서비스 초기화 중...");
|
||||||
|
TeighaServicesManager.Instance.AcquireServices();
|
||||||
|
|
||||||
|
// 2. DwgDataExtractor 인스턴스 생성
|
||||||
|
Debug.WriteLine("2. DwgDataExtractor 인스턴스 생성...");
|
||||||
|
var mappingData = new MappingTableData();
|
||||||
|
var fieldMapper = new FieldMapper(mappingData);
|
||||||
|
var extractor = new DwgDataExtractor(fieldMapper);
|
||||||
|
|
||||||
|
// 3. Note 데이터 추출
|
||||||
|
Debug.WriteLine($"3. DWG 파일에서 Note 추출 중: {Path.GetFileName(dwgFilePath)}");
|
||||||
|
var noteEntities = extractor.ExtractNotesFromDrawing(dwgFilePath);
|
||||||
|
|
||||||
|
Debug.WriteLine($" 추출된 Note 엔터티 수: {noteEntities.NoteEntities.Count}");
|
||||||
|
|
||||||
|
// 4. Note 데이터 분석
|
||||||
|
AnalyzeNoteData(noteEntities.NoteEntities);
|
||||||
|
|
||||||
|
// 5. CSV 내보내기
|
||||||
|
Debug.WriteLine("5. CSV 파일 생성 중...");
|
||||||
|
var csvWriter = new CsvDataWriter();
|
||||||
|
var baseFileName = Path.GetFileNameWithoutExtension(dwgFilePath);
|
||||||
|
|
||||||
|
// 5-1. Note 박스 텍스트만 CSV로 저장
|
||||||
|
var noteTextCsvPath = Path.Combine(outputDirectory, $"{baseFileName}_note_texts.csv");
|
||||||
|
csvWriter.WriteNoteBoxTextToCsv(noteEntities.NoteEntities, noteTextCsvPath);
|
||||||
|
Debug.WriteLine($" Note 텍스트 CSV 저장: {noteTextCsvPath}");
|
||||||
|
|
||||||
|
// 5-2. Note 테이블 데이터만 CSV로 저장
|
||||||
|
var noteTableCsvPath = Path.Combine(outputDirectory, $"{baseFileName}_note_tables.csv");
|
||||||
|
csvWriter.WriteNoteTablesToCsv(noteEntities.NoteEntities, noteTableCsvPath);
|
||||||
|
Debug.WriteLine($" Note 테이블 CSV 저장: {noteTableCsvPath}");
|
||||||
|
|
||||||
|
// 5-3. 통합 CSV 저장
|
||||||
|
var combinedCsvPath = Path.Combine(outputDirectory, $"{baseFileName}_note_combined.csv");
|
||||||
|
csvWriter.WriteNoteDataToCombinedCsv(noteEntities.NoteEntities, combinedCsvPath);
|
||||||
|
Debug.WriteLine($" 통합 CSV 저장: {combinedCsvPath}");
|
||||||
|
|
||||||
|
// 5-4. 개별 테이블 CSV 저장
|
||||||
|
var individualTablesDir = Path.Combine(outputDirectory, $"{baseFileName}_individual_tables");
|
||||||
|
csvWriter.WriteIndividualNoteTablesCsv(noteEntities.NoteEntities, individualTablesDir);
|
||||||
|
Debug.WriteLine($" 개별 테이블 CSV 저장: {individualTablesDir}");
|
||||||
|
|
||||||
|
// 5-5. 통계 정보 CSV 저장
|
||||||
|
var statisticsCsvPath = Path.Combine(outputDirectory, $"{baseFileName}_note_statistics.csv");
|
||||||
|
csvWriter.WriteNoteStatisticsToCsv(noteEntities.NoteEntities, statisticsCsvPath);
|
||||||
|
Debug.WriteLine($" 통계 정보 CSV 저장: {statisticsCsvPath}");
|
||||||
|
|
||||||
|
Debug.WriteLine("=== Note 추출 및 CSV 내보내기 테스트 완료 ===");
|
||||||
|
Debug.WriteLine($"모든 결과 파일이 저장되었습니다: {outputDirectory}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"❌ 테스트 중 오류 발생: {ex.Message}");
|
||||||
|
Debug.WriteLine($"스택 트레이스: {ex.StackTrace}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Teigha 서비스 정리
|
||||||
|
try
|
||||||
|
{
|
||||||
|
TeighaServicesManager.Instance.ForceDisposeServices();
|
||||||
|
Debug.WriteLine("Teigha 서비스 정리 완료");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"Teigha 서비스 정리 중 오류: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 추출된 Note 데이터를 분석하여 로그로 출력
|
||||||
|
/// </summary>
|
||||||
|
private static void AnalyzeNoteData(List<NoteEntityInfo> noteEntities)
|
||||||
|
{
|
||||||
|
Debug.WriteLine("=== Note 데이터 분석 ===");
|
||||||
|
|
||||||
|
var noteHeaders = noteEntities.Where(ne => ne.Type == "Note").ToList();
|
||||||
|
var noteContents = noteEntities.Where(ne => ne.Type == "NoteContent").ToList();
|
||||||
|
var notesWithTables = noteEntities.Where(ne => ne.Type == "Note" && !string.IsNullOrEmpty(ne.TableCsv)).ToList();
|
||||||
|
|
||||||
|
Debug.WriteLine($"전체 Note 헤더 수: {noteHeaders.Count}");
|
||||||
|
Debug.WriteLine($"전체 Note 콘텐츠 수: {noteContents.Count}");
|
||||||
|
Debug.WriteLine($"테이블이 있는 Note 수: {notesWithTables.Count}");
|
||||||
|
|
||||||
|
// 각 Note 분석
|
||||||
|
foreach (var note in noteHeaders)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"");
|
||||||
|
Debug.WriteLine($"Note: '{note.Text}' at ({note.X:F1}, {note.Y:F1})");
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(note.TableCsv))
|
||||||
|
{
|
||||||
|
var tableLines = note.TableCsv.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
Debug.WriteLine($" 테이블 데이터: {tableLines.Length}행");
|
||||||
|
|
||||||
|
// 테이블 내용 일부 출력
|
||||||
|
for (int i = 0; i < Math.Min(3, tableLines.Length); i++)
|
||||||
|
{
|
||||||
|
var line = tableLines[i];
|
||||||
|
if (line.Length > 50) line = line.Substring(0, 50) + "...";
|
||||||
|
Debug.WriteLine($" 행 {i + 1}: {line}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tableLines.Length > 3)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($" ... 및 {tableLines.Length - 3}개 행 더");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이 Note와 연관된 NoteContent들
|
||||||
|
var relatedContents = noteContents.Where(nc =>
|
||||||
|
Math.Abs(nc.Y - note.Y) < 50 && // Y 좌표가 비슷한 범위 (Note 아래)
|
||||||
|
nc.Y < note.Y) // Note보다 아래쪽
|
||||||
|
.OrderBy(nc => nc.SortOrder)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (relatedContents.Count > 0)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($" 관련 콘텐츠: {relatedContents.Count}개");
|
||||||
|
foreach (var content in relatedContents.Take(3))
|
||||||
|
{
|
||||||
|
Debug.WriteLine($" '{content.Text}' at ({content.X:F1}, {content.Y:F1})");
|
||||||
|
}
|
||||||
|
if (relatedContents.Count > 3)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($" ... 및 {relatedContents.Count - 3}개 더");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 레이어별 분포
|
||||||
|
Debug.WriteLine("");
|
||||||
|
Debug.WriteLine("레이어별 분포:");
|
||||||
|
var layerGroups = noteEntities.GroupBy(ne => ne.Layer).OrderByDescending(g => g.Count());
|
||||||
|
foreach (var layerGroup in layerGroups)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($" {layerGroup.Key}: {layerGroup.Count()}개");
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.WriteLine("=== Note 데이터 분석 완료 ===");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
25
Models/SettingsManager.cs
Normal file
25
Models/SettingsManager.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace DwgExtractorManual.Models
|
||||||
|
{
|
||||||
|
public static class SettingsManager
|
||||||
|
{
|
||||||
|
private static readonly string SettingsFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "settings.json");
|
||||||
|
|
||||||
|
public static void SaveSettings(AppSettings settings)
|
||||||
|
{
|
||||||
|
string json = JsonConvert.SerializeObject(settings, Formatting.Indented);
|
||||||
|
File.WriteAllText(SettingsFilePath, json);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AppSettings? LoadSettings()
|
||||||
|
{
|
||||||
|
if (!File.Exists(SettingsFilePath))
|
||||||
|
{ return null; }
|
||||||
|
|
||||||
|
string json = File.ReadAllText(SettingsFilePath);
|
||||||
|
return JsonConvert.DeserializeObject<AppSettings>(json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,15 +15,22 @@ namespace DwgExtractorManual.Models
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class SqlDatas : IDisposable
|
internal sealed class SqlDatas : IDisposable
|
||||||
{
|
{
|
||||||
Services appServices; // ODA 제품 활성화용
|
Services? appServices; // ODA 제품 활성화용 (managed by singleton)
|
||||||
readonly string connectionString = "Host=localhost;Database=postgres;Username=postgres;Password=Qwer1234";
|
readonly string connectionString = "Host=localhost;Database=postgres;Username=postgres;Password=Qwer1234";
|
||||||
|
|
||||||
void ActivateAndInitializeODA()
|
void InitializeTeighaServices()
|
||||||
{
|
{
|
||||||
var userInfo = "c2FtYW4gZW5naW5lZXJpbmc=";
|
try
|
||||||
var userSignature = "F0kuQTmtVpHtvl/TgaFVGE92/YqGmYR9SLoXckEjnOk8NoAQh7Sg6GQruVC04JqD4C/IipxJYqpqvMfMc2auRMG+cAJCKqKUE2djIMdkUdb+C5IVx2c97fcK5ba3n8DDvB54Upokajl+6j12yD8h8MKGOR3Z3zysObeXD62bFpQgp00GCYTqlxEZtTIRjHIPAfJfix8Y0jtXWWYyVJ3LYOu86as5xtx+hY1aakpYIJiQk/6pGd84qSn/9K1w8nxR7UrFzieDeQ/xM58BHSD4u/ZxVJwvv6Uy10tsdBFBTvfJMAFp05Y7yeyeCNr100tA3iOfmWoXAVRHfxnkPfiYR54aK04QI+R6OGkI+yd1oR5BtmN6BdDt3z8KYK5EpFGJGiJIGoUy5PvkYdJ2VV6xe9JWBiIJuI/tDn1Y+uyTQFA9qaDHnOURriXsRGfy8reDPf1eejybSJxWKkpilG6RXhq3xHlCkjZzh1Q45S+xYXNGatcWMm9nkn20M8Ke5JEVaI9w/p2GE36CHRtRQbt8kfPmsbWNXJCFr4svHW2MPbCKWoyn5XEyMWBnuAKi74zvczB13DKjf29SqSIgF5k/hwy2QrgvnaKzY1k8bw8w2/k0vJXcS3GKOB/ZYDle1tf/lkAD1HtnF9zE18TiXhVnqwAVjwg4ui1RPLn/LMs6b5Y=";
|
{
|
||||||
Services.odActivate(userInfo, userSignature);
|
Debug.WriteLine("[SqlDatas] TeighaServicesManager를 통한 Services 획득 중...");
|
||||||
appServices = new Services();
|
appServices = TeighaServicesManager.Instance.AcquireServices();
|
||||||
|
Debug.WriteLine($"[SqlDatas] Services 획득 성공. Reference Count: {TeighaServicesManager.Instance.ReferenceCount}");
|
||||||
|
}
|
||||||
|
catch (Teigha.Runtime.Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[SqlDatas] Teigha Services 초기화 실패: {ex.Message}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CreateTables()
|
void CreateTables()
|
||||||
@@ -78,7 +85,7 @@ namespace DwgExtractorManual.Models
|
|||||||
|
|
||||||
public SqlDatas()
|
public SqlDatas()
|
||||||
{
|
{
|
||||||
ActivateAndInitializeODA();
|
InitializeTeighaServices();
|
||||||
CreateTables();
|
CreateTables();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,8 +143,8 @@ namespace DwgExtractorManual.Models
|
|||||||
cmd.Parameters.AddWithValue("Type", "DBText");
|
cmd.Parameters.AddWithValue("Type", "DBText");
|
||||||
cmd.Parameters.AddWithValue("Layer", layerName);
|
cmd.Parameters.AddWithValue("Layer", layerName);
|
||||||
cmd.Parameters.AddWithValue("Text", dbText.TextString ?? "");
|
cmd.Parameters.AddWithValue("Text", dbText.TextString ?? "");
|
||||||
cmd.Parameters.AddWithValue("Path", database.Filename);
|
cmd.Parameters.AddWithValue("Path", database.Filename ?? "");
|
||||||
cmd.Parameters.AddWithValue("FileName", Path.GetFileName(database.Filename));
|
cmd.Parameters.AddWithValue("FileName", string.IsNullOrEmpty(database.Filename) ? "" : Path.GetFileName(database.Filename));
|
||||||
|
|
||||||
cmd.ExecuteNonQuery();
|
cmd.ExecuteNonQuery();
|
||||||
}
|
}
|
||||||
@@ -155,8 +162,8 @@ namespace DwgExtractorManual.Models
|
|||||||
cmd.Parameters.AddWithValue("Type", "MText");
|
cmd.Parameters.AddWithValue("Type", "MText");
|
||||||
cmd.Parameters.AddWithValue("Layer", layerName);
|
cmd.Parameters.AddWithValue("Layer", layerName);
|
||||||
cmd.Parameters.AddWithValue("Text", mText.Contents ?? "");
|
cmd.Parameters.AddWithValue("Text", mText.Contents ?? "");
|
||||||
cmd.Parameters.AddWithValue("Path", database.Filename);
|
cmd.Parameters.AddWithValue("Path", database.Filename ?? "");
|
||||||
cmd.Parameters.AddWithValue("FileName", Path.GetFileName(database.Filename));
|
cmd.Parameters.AddWithValue("FileName", string.IsNullOrEmpty(database.Filename) ? "" : Path.GetFileName(database.Filename));
|
||||||
|
|
||||||
cmd.ExecuteNonQuery();
|
cmd.ExecuteNonQuery();
|
||||||
}
|
}
|
||||||
@@ -224,8 +231,8 @@ namespace DwgExtractorManual.Models
|
|||||||
else
|
else
|
||||||
cmd.Parameters.AddWithValue("Value", tString);
|
cmd.Parameters.AddWithValue("Value", tString);
|
||||||
|
|
||||||
cmd.Parameters.AddWithValue("Path", database.Filename);
|
cmd.Parameters.AddWithValue("Path", database.Filename ?? "");
|
||||||
cmd.Parameters.AddWithValue("FileName", Path.GetFileName(database.Filename));
|
cmd.Parameters.AddWithValue("FileName", string.IsNullOrEmpty(database.Filename) ? "" : Path.GetFileName(database.Filename));
|
||||||
|
|
||||||
cmd.ExecuteNonQuery();
|
cmd.ExecuteNonQuery();
|
||||||
}
|
}
|
||||||
@@ -301,8 +308,20 @@ namespace DwgExtractorManual.Models
|
|||||||
{
|
{
|
||||||
if (appServices != null)
|
if (appServices != null)
|
||||||
{
|
{
|
||||||
appServices.Dispose();
|
try
|
||||||
appServices = null;
|
{
|
||||||
|
Debug.WriteLine("[SqlDatas] Teigha Services 해제 중...");
|
||||||
|
TeighaServicesManager.Instance.ReleaseServices();
|
||||||
|
Debug.WriteLine($"[SqlDatas] Teigha Services 해제 완료. Remaining ref count: {TeighaServicesManager.Instance.ReferenceCount}");
|
||||||
|
}
|
||||||
|
catch (Teigha.Runtime.Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[SqlDatas] Teigha Services 해제 중 오류 (무시됨): {ex.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
appServices = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
115
Models/TableCellVisualizationData.cs
Normal file
115
Models/TableCellVisualizationData.cs
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Teigha.Geometry;
|
||||||
|
|
||||||
|
namespace DwgExtractorManual.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 테이블 셀 시각화를 위한 데이터 클래스
|
||||||
|
/// </summary>
|
||||||
|
public class TableCellVisualizationData
|
||||||
|
{
|
||||||
|
public string FileName { get; set; } = "";
|
||||||
|
public string NoteText { get; set; } = "";
|
||||||
|
public List<CellBounds> Cells { get; set; } = new List<CellBounds>();
|
||||||
|
public List<SegmentInfo> TableSegments { get; set; } = new List<SegmentInfo>();
|
||||||
|
public List<TextInfo> TextEntities { get; set; } = new List<TextInfo>();
|
||||||
|
public List<IntersectionInfo> IntersectionPoints { get; set; } = new List<IntersectionInfo>(); // 교차점 정보 추가
|
||||||
|
public List<DiagonalLine> DiagonalLines { get; set; } = new List<DiagonalLine>(); // 셀 대각선 정보 추가
|
||||||
|
public List<CellBoundaryInfo> CellBoundaries { get; set; } = new List<CellBoundaryInfo>(); // 정확한 셀 경계 정보 추가
|
||||||
|
public (double minX, double minY, double maxX, double maxY) NoteBounds { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 셀 경계 정보
|
||||||
|
/// </summary>
|
||||||
|
public class CellBounds
|
||||||
|
{
|
||||||
|
public double MinX { get; set; }
|
||||||
|
public double MinY { get; set; }
|
||||||
|
public double MaxX { get; set; }
|
||||||
|
public double MaxY { get; set; }
|
||||||
|
public int Row { get; set; }
|
||||||
|
public int Column { get; set; }
|
||||||
|
public string Text { get; set; } = "";
|
||||||
|
public bool IsValid { get; set; } = true;
|
||||||
|
|
||||||
|
public double Width => MaxX - MinX;
|
||||||
|
public double Height => MaxY - MinY;
|
||||||
|
public double CenterX => (MinX + MaxX) / 2;
|
||||||
|
public double CenterY => (MinY + MaxY) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 선분 정보
|
||||||
|
/// </summary>
|
||||||
|
public class SegmentInfo
|
||||||
|
{
|
||||||
|
public double StartX { get; set; }
|
||||||
|
public double StartY { get; set; }
|
||||||
|
public double EndX { get; set; }
|
||||||
|
public double EndY { get; set; }
|
||||||
|
public bool IsHorizontal { get; set; }
|
||||||
|
public string Color { get; set; } = "Black";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 텍스트 정보
|
||||||
|
/// </summary>
|
||||||
|
public class TextInfo
|
||||||
|
{
|
||||||
|
public double X { get; set; }
|
||||||
|
public double Y { get; set; }
|
||||||
|
public string Text { get; set; } = "";
|
||||||
|
public bool IsInTable { get; set; }
|
||||||
|
public string Color { get; set; } = "Blue";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 교차점 정보
|
||||||
|
/// </summary>
|
||||||
|
public class IntersectionInfo
|
||||||
|
{
|
||||||
|
public double X { get; set; }
|
||||||
|
public double Y { get; set; }
|
||||||
|
public int DirectionBits { get; set; } // 비트 플래그 숫자
|
||||||
|
public int Row { get; set; } // Row 번호
|
||||||
|
public int Column { get; set; } // Column 번호
|
||||||
|
public bool IsTopLeft { get; set; } // topLeft 후보인지
|
||||||
|
public bool IsBottomRight { get; set; } // bottomRight 후보인지
|
||||||
|
public string Color { get; set; } = "Red";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 대각선 정보 (셀 디버깅용)
|
||||||
|
/// </summary>
|
||||||
|
public class DiagonalLine
|
||||||
|
{
|
||||||
|
public double StartX { get; set; }
|
||||||
|
public double StartY { get; set; }
|
||||||
|
public double EndX { get; set; }
|
||||||
|
public double EndY { get; set; }
|
||||||
|
public string Color { get; set; } = "Green";
|
||||||
|
public string Label { get; set; } = ""; // 디버깅 라벨
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 정확한 셀 경계 정보 (4개 모서리 좌표)
|
||||||
|
/// </summary>
|
||||||
|
public class CellBoundaryInfo
|
||||||
|
{
|
||||||
|
public double TopLeftX { get; set; }
|
||||||
|
public double TopLeftY { get; set; }
|
||||||
|
public double TopRightX { get; set; }
|
||||||
|
public double TopRightY { get; set; }
|
||||||
|
public double BottomLeftX { get; set; }
|
||||||
|
public double BottomLeftY { get; set; }
|
||||||
|
public double BottomRightX { get; set; }
|
||||||
|
public double BottomRightY { get; set; }
|
||||||
|
public string Label { get; set; } = "";
|
||||||
|
public double Width { get; set; }
|
||||||
|
public double Height { get; set; }
|
||||||
|
public string Color { get; set; } = "DarkBlue";
|
||||||
|
public string CellText { get; set; } = ""; // 셀 내 텍스트 내용
|
||||||
|
}
|
||||||
|
}
|
||||||
192
Models/TeighaServicesManager.cs
Normal file
192
Models/TeighaServicesManager.cs
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using Teigha.Runtime;
|
||||||
|
|
||||||
|
namespace DwgExtractorManual.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Singleton class to manage Teigha Services lifecycle and prevent disposal conflicts
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TeighaServicesManager
|
||||||
|
{
|
||||||
|
private static readonly object _lock = new object();
|
||||||
|
private static TeighaServicesManager? _instance = null;
|
||||||
|
private static Services? _services = null;
|
||||||
|
private static int _referenceCount = 0;
|
||||||
|
private static bool _isActivated = false;
|
||||||
|
|
||||||
|
private TeighaServicesManager()
|
||||||
|
{
|
||||||
|
// Private constructor for singleton
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TeighaServicesManager Instance
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_instance == null)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (_instance == null)
|
||||||
|
{
|
||||||
|
_instance = new TeighaServicesManager();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Acquires Teigha Services (creates if needed, increments reference count)
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The Services instance</returns>
|
||||||
|
public Services AcquireServices()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[TeighaManager] AcquireServices - Current ref count: {_referenceCount}");
|
||||||
|
|
||||||
|
if (!_isActivated)
|
||||||
|
{
|
||||||
|
Debug.WriteLine("[TeighaManager] Activating ODA for first time...");
|
||||||
|
ActivateODA();
|
||||||
|
_isActivated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_services == null)
|
||||||
|
{
|
||||||
|
Debug.WriteLine("[TeighaManager] Creating new Services instance...");
|
||||||
|
_services = new Services();
|
||||||
|
Debug.WriteLine("[TeighaManager] Services instance created successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
_referenceCount++;
|
||||||
|
Debug.WriteLine($"[TeighaManager] Services acquired - New ref count: {_referenceCount}");
|
||||||
|
return _services;
|
||||||
|
}
|
||||||
|
catch (Teigha.Runtime.Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[TeighaManager] Error acquiring services: {ex.Message}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Releases Teigha Services (decrements reference count, disposes when count reaches 0)
|
||||||
|
/// </summary>
|
||||||
|
public void ReleaseServices()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[TeighaManager] ReleaseServices - Current ref count: {_referenceCount}");
|
||||||
|
|
||||||
|
if (_referenceCount > 0)
|
||||||
|
{
|
||||||
|
_referenceCount--;
|
||||||
|
Debug.WriteLine($"[TeighaManager] Services released - New ref count: {_referenceCount}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't dispose Services until app shutdown to prevent conflicts
|
||||||
|
// Just track reference count for debugging
|
||||||
|
if (_referenceCount == 0)
|
||||||
|
{
|
||||||
|
Debug.WriteLine("[TeighaManager] All references released (Services kept alive for app lifetime)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Teigha.Runtime.Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[TeighaManager] Error releasing services: {ex.Message}");
|
||||||
|
// Don't throw on release to prevent cascade failures
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Force dispose Services (only call on application shutdown)
|
||||||
|
/// </summary>
|
||||||
|
public void ForceDisposeServices()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Debug.WriteLine("[TeighaManager] Force disposing Services...");
|
||||||
|
|
||||||
|
if (_services != null)
|
||||||
|
{
|
||||||
|
_services.Dispose();
|
||||||
|
Debug.WriteLine("[TeighaManager] Services disposed successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
_services = null;
|
||||||
|
_referenceCount = 0;
|
||||||
|
_isActivated = false;
|
||||||
|
|
||||||
|
Debug.WriteLine("[TeighaManager] Force dispose completed");
|
||||||
|
}
|
||||||
|
catch (Teigha.Runtime.Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[TeighaManager] Error during force dispose: {ex.Message}");
|
||||||
|
// Reset state even if disposal fails
|
||||||
|
_services = null;
|
||||||
|
_referenceCount = 0;
|
||||||
|
_isActivated = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get current reference count (for debugging)
|
||||||
|
/// </summary>
|
||||||
|
public int ReferenceCount
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return _referenceCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if Services is active and valid
|
||||||
|
/// </summary>
|
||||||
|
public bool IsServicesActive
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return _services != null && _isActivated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ActivateODA()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Debug.WriteLine("[TeighaManager] Activating ODA...");
|
||||||
|
|
||||||
|
var userInfo = "c2FtYW4gZW5naW5lZXJpbmc=";
|
||||||
|
var userSignature = "F0kuQTmtVpHtvl/TgaFVGE92/YqGmYR9SLoXckEjnOk8NoAQh7Sg6GQruVC04JqD4C/IipxJYqpqvMfMc2auRMG+cAJCKqKUE2djIMdkUdb+C5IVx2c97fcK5ba3n8DDvB54Upokajl+6j12yD8h8MKGOR3Z3zysObeXD62bFpQgp00GCYTqlxEZtTIRjHIPAfJfix8Y0jtXWWYyVJ3LYOu86as5xtx+hY1aakpYIJiQk/6pGd84qSn/9K1w8nxR7UrFzieDeQ/xM58BHSD4u/ZxVJwvv6Uy10tsdBFBTvfJMAFp05Y7yeyeCNr100tA3iOfmWoXAVRHfxnkPfiYR54aK04QI+R6OGkI+yd1oR5BtmN6BdDt3z8KYK5EpFGJGiJIGoUy5PvkYdJ2VV6xe9JWBiIJuI/tDn1Y+uyTQFA9qaDHnOURriXsRGfy8reDPf1eejybSJxWKkpilG6RXhq3xHlCkjZzh1Q45S+xYXNGatcWMm9nkn20M8Ke5JEVaI9w/p2GE36CHRtRQbt8kfPmsbWNXJCFr4svHW2MPbCKWoyn5XEyMWBnuAKi74zvczB13DKjf29SqSIgF5k/hwy2QrgvnaKzY1k8bw8w2/k0vJXcS3GKOB/ZYDle1tf/lkAD1HtnF9zE18TiXhVnqwAVjwg4ui1RPLn/LMs6b5Y=";
|
||||||
|
|
||||||
|
Services.odActivate(userInfo, userSignature);
|
||||||
|
Debug.WriteLine("[TeighaManager] ODA activation successful");
|
||||||
|
}
|
||||||
|
catch (Teigha.Runtime.Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[TeighaManager] ODA activation failed: {ex.Message}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
48
NoteDetectionRefactor.md
Normal file
48
NoteDetectionRefactor.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
Project: Refactor the NOTE Content Box Detection Algorithm
|
||||||
|
|
||||||
|
1. High-Level Goal:
|
||||||
|
The primary objective is to replace the current, fragile "horizontal search line" algorithm in
|
||||||
|
Models/DwgDataExtractor.cs with a more robust and accurate method that reliably finds the content box for
|
||||||
|
any "NOTE" text, regardless of its position or the composition of its bounding box.
|
||||||
|
|
||||||
|
2. Core Strategy: "Vertical Ray-Casting"
|
||||||
|
We will implement a new algorithm that emulates how a human would visually locate the content. This
|
||||||
|
involves a "gradual downward scan" (or vertical ray-cast) from the NOTE's position.
|
||||||
|
|
||||||
|
3. Implementation Plan (TODO List):
|
||||||
|
|
||||||
|
* Step 1: Unify All Geometry into Line Segments
|
||||||
|
* Create a single helper method, GetAllLineSegments, that processes all Line and Polyline entities from
|
||||||
|
the drawing.
|
||||||
|
* This method will decompose every Polyline into its constituent Line segments.
|
||||||
|
* It will return a single, unified List<Line> containing every potential boundary segment in the
|
||||||
|
drawing.
|
||||||
|
* Crucially: This method must ensure all temporary Line objects created during the process are properly
|
||||||
|
disposed of to prevent memory leaks.
|
||||||
|
|
||||||
|
* Step 2: Implement the Ray-Casting Logic in `FindNoteBox`
|
||||||
|
* The FindNoteBox method will be completely rewritten.
|
||||||
|
* It will first call GetAllLineSegments to get the unified geometry list.
|
||||||
|
* It will then perform the vertical ray-cast starting from the NOTE's X-coordinate and scanning
|
||||||
|
downwards.
|
||||||
|
* It will find all horizontal lines that intersect the ray and sort them by their Y-coordinate (from
|
||||||
|
top to bottom).
|
||||||
|
* It will identify the second line in this sorted list as the top edge of the content box (the first is
|
||||||
|
assumed to be the NOTE's own bounding box).
|
||||||
|
|
||||||
|
* Step 3: Implement Smart Box Tracing
|
||||||
|
* Create a new helper method, TraceBoxFromTopLine.
|
||||||
|
* This method will take the identified top line segment as its starting point.
|
||||||
|
* It will intelligently trace the remaining three sides of the rectangle by searching the unified list
|
||||||
|
of line segments for the nearest connecting corners.
|
||||||
|
* This tracing logic must be tolerant of small gaps between the endpoints of the lines forming the box.
|
||||||
|
|
||||||
|
* Step 4: Final Cleanup
|
||||||
|
* Once the new ray-casting algorithm is fully implemented and validated, all of the old, obsolete
|
||||||
|
methods related to the previous search-line approach must be deleted to keep the code clean. This
|
||||||
|
includes:
|
||||||
|
* FindIntersectingLineSegments
|
||||||
|
* TraceRectangleFromLineSegments
|
||||||
|
* FindNextConnectedLineSegment
|
||||||
|
* DoesLineIntersectPolyline
|
||||||
|
* GetPolylineBounds
|
||||||
109
Views/TableCellVisualizationWindow.xaml
Normal file
109
Views/TableCellVisualizationWindow.xaml
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<Window x:Class="DwgExtractorManual.Views.TableCellVisualizationWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:controls="clr-namespace:DwgExtractorManual.Controls"
|
||||||
|
Title="테이블 셀 시각화" Height="800" Width="1200"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
MinHeight="600" MinWidth="800">
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- 헤더 -->
|
||||||
|
<Border Grid.Row="0" Background="#34495E" Padding="15">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="테이블 셀 시각화"
|
||||||
|
FontSize="20" FontWeight="Bold"
|
||||||
|
Foreground="White" HorizontalAlignment="Center"/>
|
||||||
|
<TextBlock Text="추출된 테이블 셀들의 경계를 시각적으로 확인할 수 있습니다"
|
||||||
|
FontSize="12" Foreground="LightGray"
|
||||||
|
HorizontalAlignment="Center" Margin="0,5,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- 메인 영역 -->
|
||||||
|
<Grid Grid.Row="1">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="250"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- 좌측 패널: 파일 목록 및 설정 -->
|
||||||
|
<Border Grid.Column="0" Background="#ECF0F1" BorderBrush="#BDC3C7" BorderThickness="0,0,1,0">
|
||||||
|
<StackPanel Margin="10">
|
||||||
|
<TextBlock Text="파일 목록" FontWeight="Bold" FontSize="14" Margin="0,0,0,10"/>
|
||||||
|
|
||||||
|
<ListBox x:Name="lstFiles" Height="200"
|
||||||
|
SelectionChanged="LstFiles_SelectionChanged"
|
||||||
|
Background="White" BorderBrush="#BDC3C7">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="{Binding FileName}" FontWeight="Bold" FontSize="12"/>
|
||||||
|
<TextBlock Text="{Binding NoteText}" FontSize="10" Foreground="Gray" TextTrimming="CharacterEllipsis"/>
|
||||||
|
<TextBlock FontSize="10" Foreground="DarkBlue">
|
||||||
|
<Run Text="셀: "/>
|
||||||
|
<Run Text="{Binding Path=Cells.Count, Mode=OneWay}"/>
|
||||||
|
<Run Text="개"/>
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
|
||||||
|
<Separator Margin="0,10"/>
|
||||||
|
|
||||||
|
<TextBlock Text="표시 옵션" FontWeight="Bold" FontSize="14" Margin="0,0,0,10"/>
|
||||||
|
|
||||||
|
<CheckBox x:Name="chkShowCells" Content="셀 경계 표시 (기존)" IsChecked="False"
|
||||||
|
Checked="RefreshVisualization" Unchecked="RefreshVisualization"/>
|
||||||
|
<CheckBox x:Name="chkShowCellBoundaries" Content="정확한 셀 경계 표시" IsChecked="True" Margin="0,5,0,0"
|
||||||
|
Checked="RefreshVisualization" Unchecked="RefreshVisualization"/>
|
||||||
|
<CheckBox x:Name="chkShowSegments" Content="선분 표시" IsChecked="True" Margin="0,5,0,0"
|
||||||
|
Checked="RefreshVisualization" Unchecked="RefreshVisualization"/>
|
||||||
|
<CheckBox x:Name="chkShowTexts" Content="텍스트 표시" IsChecked="True" Margin="0,5,0,0"
|
||||||
|
Checked="RefreshVisualization" Unchecked="RefreshVisualization"/>
|
||||||
|
<CheckBox x:Name="chkShowIntersections" Content="교차점 표시" IsChecked="True" Margin="0,5,0,0"
|
||||||
|
Checked="RefreshVisualization" Unchecked="RefreshVisualization"/>
|
||||||
|
<CheckBox x:Name="chkShowDiagonals" Content="셀 대각선 표시" IsChecked="True" Margin="0,5,0,0"
|
||||||
|
Checked="RefreshVisualization" Unchecked="RefreshVisualization"/>
|
||||||
|
<CheckBox x:Name="chkShowNoteBounds" Content="Note 경계 표시" IsChecked="False" Margin="0,5,0,0"
|
||||||
|
Checked="RefreshVisualization" Unchecked="RefreshVisualization"/>
|
||||||
|
|
||||||
|
<Separator Margin="0,10"/>
|
||||||
|
|
||||||
|
<TextBlock Text="확대/축소" FontWeight="Bold" FontSize="14" Margin="0,0,0,10"/>
|
||||||
|
<Button x:Name="btnZoomFit" Content="초기화 (마우스 우클릭)" Click="BtnZoomFit_Click" Margin="0,2"/>
|
||||||
|
|
||||||
|
<Separator Margin="0,10"/>
|
||||||
|
|
||||||
|
<TextBlock x:Name="txtInfo" Text="파일을 선택하세요" FontSize="11"
|
||||||
|
Foreground="DarkGray" TextWrapping="Wrap"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- 우측 패널: 시각화 영역 -->
|
||||||
|
<Border Grid.Column="1" Background="White">
|
||||||
|
<controls:ZoomBorder x:Name="svViewer" ClipToBounds="True">
|
||||||
|
<Canvas x:Name="cnvVisualization" Background="White"
|
||||||
|
Width="800" Height="600"
|
||||||
|
MouseMove="CnvVisualization_MouseMove"/>
|
||||||
|
</controls:ZoomBorder>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- 상태바 -->
|
||||||
|
<StatusBar Grid.Row="2" Background="#95A5A6">
|
||||||
|
<StatusBarItem>
|
||||||
|
<TextBlock x:Name="txtStatus" Text="준비됨"/>
|
||||||
|
</StatusBarItem>
|
||||||
|
<StatusBarItem HorizontalAlignment="Right">
|
||||||
|
<TextBlock x:Name="txtMousePos" Text="마우스: (0, 0)"/>
|
||||||
|
</StatusBarItem>
|
||||||
|
</StatusBar>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
493
Views/TableCellVisualizationWindow.xaml.cs
Normal file
493
Views/TableCellVisualizationWindow.xaml.cs
Normal file
@@ -0,0 +1,493 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Shapes;
|
||||||
|
using DwgExtractorManual.Models;
|
||||||
|
using Brushes = System.Windows.Media.Brushes;
|
||||||
|
using MouseEventArgs = System.Windows.Input.MouseEventArgs;
|
||||||
|
using Point = System.Windows.Point;
|
||||||
|
using Rectangle = System.Windows.Shapes.Rectangle;
|
||||||
|
|
||||||
|
namespace DwgExtractorManual.Views
|
||||||
|
{
|
||||||
|
public partial class TableCellVisualizationWindow : Window
|
||||||
|
{
|
||||||
|
private List<TableCellVisualizationData> _visualizationData;
|
||||||
|
private TableCellVisualizationData? _currentData;
|
||||||
|
private double _scale = 1.0;
|
||||||
|
private const double MARGIN = 50;
|
||||||
|
|
||||||
|
public TableCellVisualizationWindow(List<TableCellVisualizationData> visualizationData)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_visualizationData = visualizationData;
|
||||||
|
LoadFileList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadFileList()
|
||||||
|
{
|
||||||
|
lstFiles.ItemsSource = _visualizationData;
|
||||||
|
if (_visualizationData.Count > 0)
|
||||||
|
{
|
||||||
|
lstFiles.SelectedIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LstFiles_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (lstFiles.SelectedItem is TableCellVisualizationData selectedData)
|
||||||
|
{
|
||||||
|
_currentData = selectedData;
|
||||||
|
RefreshVisualization();
|
||||||
|
UpdateInfo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateInfo()
|
||||||
|
{
|
||||||
|
if (_currentData == null)
|
||||||
|
{
|
||||||
|
txtInfo.Text = "파일을 선택하세요";
|
||||||
|
txtStatus.Text = "준비됨";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
txtInfo.Text = $"파일: {_currentData.FileName}\n" +
|
||||||
|
$"Note: {_currentData.NoteText}\n" +
|
||||||
|
$"셀 수: {_currentData.Cells.Count}\n" +
|
||||||
|
$"선분 수: {_currentData.TableSegments.Count}\n" +
|
||||||
|
$"텍스트 수: {_currentData.TextEntities.Count}\n" +
|
||||||
|
$"교차점 수: {_currentData.IntersectionPoints.Count}\n" +
|
||||||
|
$"대각선 수: {_currentData.DiagonalLines.Count}";
|
||||||
|
|
||||||
|
txtStatus.Text = $"{_currentData.FileName} - 셀 {_currentData.Cells.Count}개";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshVisualization()
|
||||||
|
{
|
||||||
|
if (_currentData == null) return;
|
||||||
|
|
||||||
|
cnvVisualization.Children.Clear();
|
||||||
|
|
||||||
|
// 좌표계 변환 계산
|
||||||
|
var bounds = CalculateBounds();
|
||||||
|
if (bounds == null) return;
|
||||||
|
|
||||||
|
var (minX, minY, maxX, maxY) = bounds.Value;
|
||||||
|
var dataWidth = maxX - minX;
|
||||||
|
var dataHeight = maxY - minY;
|
||||||
|
|
||||||
|
// 캔버스 크기 설정 (여백 포함)
|
||||||
|
var canvasWidth = cnvVisualization.Width;
|
||||||
|
var canvasHeight = cnvVisualization.Height;
|
||||||
|
|
||||||
|
// 스케일 계산 (데이터가 캔버스에 맞도록)
|
||||||
|
var scaleX = (canvasWidth - 2 * MARGIN) / dataWidth;
|
||||||
|
var scaleY = (canvasHeight - 2 * MARGIN) / dataHeight;
|
||||||
|
_scale = Math.Min(scaleX, scaleY) * 0.9; // 여유분 10%
|
||||||
|
|
||||||
|
// Note 경계 표시
|
||||||
|
if (chkShowNoteBounds.IsChecked == true)
|
||||||
|
{
|
||||||
|
DrawNoteBounds(minX, minY, maxX, maxY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 선분 표시
|
||||||
|
if (chkShowSegments.IsChecked == true)
|
||||||
|
{
|
||||||
|
DrawSegments();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 셀 경계 표시 (기존)
|
||||||
|
if (chkShowCells.IsChecked == true)
|
||||||
|
{
|
||||||
|
DrawCells();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 정확한 셀 경계 표시 (새로운)
|
||||||
|
if (chkShowCellBoundaries.IsChecked == true)
|
||||||
|
{
|
||||||
|
DrawCellBoundaries();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 텍스트 표시
|
||||||
|
if (chkShowTexts.IsChecked == true)
|
||||||
|
{
|
||||||
|
DrawTexts();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 교차점 표시
|
||||||
|
if (chkShowIntersections.IsChecked == true)
|
||||||
|
{
|
||||||
|
DrawIntersections();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 대각선 표시 (셀 디버깅용)
|
||||||
|
if (chkShowDiagonals.IsChecked == true)
|
||||||
|
{
|
||||||
|
DrawDiagonals();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private (double minX, double minY, double maxX, double maxY)? CalculateBounds()
|
||||||
|
{
|
||||||
|
if (_currentData == null || _currentData.Cells.Count == 0) return null;
|
||||||
|
|
||||||
|
var minX = _currentData.Cells.Min(c => c.MinX);
|
||||||
|
var minY = _currentData.Cells.Min(c => c.MinY);
|
||||||
|
var maxX = _currentData.Cells.Max(c => c.MaxX);
|
||||||
|
var maxY = _currentData.Cells.Max(c => c.MaxY);
|
||||||
|
|
||||||
|
// 선분도 고려
|
||||||
|
if (_currentData.TableSegments.Count > 0)
|
||||||
|
{
|
||||||
|
minX = Math.Min(minX, _currentData.TableSegments.Min(s => Math.Min(s.StartX, s.EndX)));
|
||||||
|
minY = Math.Min(minY, _currentData.TableSegments.Min(s => Math.Min(s.StartY, s.EndY)));
|
||||||
|
maxX = Math.Max(maxX, _currentData.TableSegments.Max(s => Math.Max(s.StartX, s.EndX)));
|
||||||
|
maxY = Math.Max(maxY, _currentData.TableSegments.Max(s => Math.Max(s.StartY, s.EndY)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 교차점도 고려
|
||||||
|
if (_currentData.IntersectionPoints.Count > 0)
|
||||||
|
{
|
||||||
|
minX = Math.Min(minX, _currentData.IntersectionPoints.Min(i => i.X));
|
||||||
|
minY = Math.Min(minY, _currentData.IntersectionPoints.Min(i => i.Y));
|
||||||
|
maxX = Math.Max(maxX, _currentData.IntersectionPoints.Max(i => i.X));
|
||||||
|
maxY = Math.Max(maxY, _currentData.IntersectionPoints.Max(i => i.Y));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 대각선도 고려
|
||||||
|
if (_currentData.DiagonalLines.Count > 0)
|
||||||
|
{
|
||||||
|
minX = Math.Min(minX, _currentData.DiagonalLines.Min(d => Math.Min(d.StartX, d.EndX)));
|
||||||
|
minY = Math.Min(minY, _currentData.DiagonalLines.Min(d => Math.Min(d.StartY, d.EndY)));
|
||||||
|
maxX = Math.Max(maxX, _currentData.DiagonalLines.Max(d => Math.Max(d.StartX, d.EndX)));
|
||||||
|
maxY = Math.Max(maxY, _currentData.DiagonalLines.Max(d => Math.Max(d.StartY, d.EndY)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 정확한 셀 경계도 고려
|
||||||
|
if (_currentData.CellBoundaries != null && _currentData.CellBoundaries.Count > 0)
|
||||||
|
{
|
||||||
|
var allCellX = _currentData.CellBoundaries.SelectMany(cb => new[] { cb.TopLeftX, cb.TopRightX, cb.BottomLeftX, cb.BottomRightX });
|
||||||
|
var allCellY = _currentData.CellBoundaries.SelectMany(cb => new[] { cb.TopLeftY, cb.TopRightY, cb.BottomLeftY, cb.BottomRightY });
|
||||||
|
|
||||||
|
minX = Math.Min(minX, allCellX.Min());
|
||||||
|
minY = Math.Min(minY, allCellY.Min());
|
||||||
|
maxX = Math.Max(maxX, allCellX.Max());
|
||||||
|
maxY = Math.Max(maxY, allCellY.Max());
|
||||||
|
}
|
||||||
|
|
||||||
|
return (minX, minY, maxX, maxY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Point TransformPoint(double x, double y)
|
||||||
|
{
|
||||||
|
var bounds = CalculateBounds();
|
||||||
|
if (bounds == null) return new Point(0, 0);
|
||||||
|
|
||||||
|
var (minX, minY, maxX, maxY) = bounds.Value;
|
||||||
|
|
||||||
|
// CAD 좌표계 -> WPF 좌표계 변환 (Y축 뒤집기)
|
||||||
|
var transformedX = (x - minX) * _scale + MARGIN;
|
||||||
|
var transformedY = cnvVisualization.Height - ((y - minY) * _scale + MARGIN);
|
||||||
|
|
||||||
|
return new Point(transformedX, transformedY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawNoteBounds(double minX, double minY, double maxX, double maxY)
|
||||||
|
{
|
||||||
|
var topLeft = TransformPoint(minX, maxY);
|
||||||
|
var bottomRight = TransformPoint(maxX, minY);
|
||||||
|
|
||||||
|
var rect = new Rectangle
|
||||||
|
{
|
||||||
|
Width = bottomRight.X - topLeft.X,
|
||||||
|
Height = bottomRight.Y - topLeft.Y,
|
||||||
|
Stroke = Brushes.Red,
|
||||||
|
StrokeThickness = 2,
|
||||||
|
StrokeDashArray = new DoubleCollection { 5, 5 },
|
||||||
|
Fill = null
|
||||||
|
};
|
||||||
|
|
||||||
|
Canvas.SetLeft(rect, topLeft.X);
|
||||||
|
Canvas.SetTop(rect, topLeft.Y);
|
||||||
|
cnvVisualization.Children.Add(rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawSegments()
|
||||||
|
{
|
||||||
|
if (_currentData == null) return;
|
||||||
|
foreach (var segment in _currentData.TableSegments)
|
||||||
|
{
|
||||||
|
var startPoint = TransformPoint(segment.StartX, segment.StartY);
|
||||||
|
var endPoint = TransformPoint(segment.EndX, segment.EndY);
|
||||||
|
|
||||||
|
var line = new Line
|
||||||
|
{
|
||||||
|
X1 = startPoint.X,
|
||||||
|
Y1 = startPoint.Y,
|
||||||
|
X2 = endPoint.X,
|
||||||
|
Y2 = endPoint.Y,
|
||||||
|
Stroke = segment.IsHorizontal ? Brushes.Blue : Brushes.Green,
|
||||||
|
StrokeThickness = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
cnvVisualization.Children.Add(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawCells()
|
||||||
|
{
|
||||||
|
if (_currentData == null) return;
|
||||||
|
var colors = new[] { Brushes.Red, Brushes.Blue, Brushes.Green, Brushes.Purple, Brushes.Orange };
|
||||||
|
|
||||||
|
for (int i = 0; i < _currentData.Cells.Count; i++)
|
||||||
|
{
|
||||||
|
var cell = _currentData.Cells[i];
|
||||||
|
var topLeft = TransformPoint(cell.MinX, cell.MaxY);
|
||||||
|
var bottomRight = TransformPoint(cell.MaxX, cell.MinY);
|
||||||
|
|
||||||
|
var rect = new Rectangle
|
||||||
|
{
|
||||||
|
Width = bottomRight.X - topLeft.X,
|
||||||
|
Height = bottomRight.Y - topLeft.Y,
|
||||||
|
Stroke = colors[i % colors.Length],
|
||||||
|
StrokeThickness = 2,
|
||||||
|
Fill = null
|
||||||
|
};
|
||||||
|
|
||||||
|
Canvas.SetLeft(rect, topLeft.X);
|
||||||
|
Canvas.SetTop(rect, topLeft.Y);
|
||||||
|
cnvVisualization.Children.Add(rect);
|
||||||
|
|
||||||
|
// 셀 번호 표시
|
||||||
|
var label = new TextBlock
|
||||||
|
{
|
||||||
|
Text = $"R{cell.Row}C{cell.Column}", // 이미 1-based 인덱싱 적용됨
|
||||||
|
FontSize = 8, // 폰트 크기 조정
|
||||||
|
Foreground = colors[i % colors.Length],
|
||||||
|
FontWeight = FontWeights.Bold
|
||||||
|
};
|
||||||
|
|
||||||
|
Canvas.SetLeft(label, topLeft.X + 2); // 좌상단에 위치 + 약간의 패딩
|
||||||
|
Canvas.SetTop(label, topLeft.Y + 2); // 좌상단에 위치 + 약간의 패딩
|
||||||
|
cnvVisualization.Children.Add(label);
|
||||||
|
|
||||||
|
// 셀 텍스트 표시
|
||||||
|
if (!string.IsNullOrEmpty(cell.Text))
|
||||||
|
{
|
||||||
|
var textLabel = new TextBlock
|
||||||
|
{
|
||||||
|
Text = cell.Text,
|
||||||
|
FontSize = 8,
|
||||||
|
Foreground = Brushes.Black,
|
||||||
|
Background = Brushes.LightYellow
|
||||||
|
};
|
||||||
|
|
||||||
|
// 셀 텍스트는 셀 중앙에 표시
|
||||||
|
var centerPoint = TransformPoint(cell.CenterX, cell.CenterY);
|
||||||
|
Canvas.SetLeft(textLabel, centerPoint.X - (textLabel.ActualWidth / 2)); // 텍스트 중앙 정렬
|
||||||
|
Canvas.SetTop(textLabel, centerPoint.Y - (textLabel.ActualHeight / 2)); // 텍스트 중앙 정렬
|
||||||
|
cnvVisualization.Children.Add(textLabel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawCellBoundaries()
|
||||||
|
{
|
||||||
|
if (_currentData == null || _currentData.CellBoundaries == null) return;
|
||||||
|
|
||||||
|
for (int i = 0; i < _currentData.CellBoundaries.Count; i++)
|
||||||
|
{
|
||||||
|
var cellBoundary = _currentData.CellBoundaries[i];
|
||||||
|
|
||||||
|
// 4개 모서리 좌표 변환
|
||||||
|
var topLeft = TransformPoint(cellBoundary.TopLeftX, cellBoundary.TopLeftY);
|
||||||
|
var topRight = TransformPoint(cellBoundary.TopRightX, cellBoundary.TopRightY);
|
||||||
|
var bottomLeft = TransformPoint(cellBoundary.BottomLeftX, cellBoundary.BottomLeftY);
|
||||||
|
var bottomRight = TransformPoint(cellBoundary.BottomRightX, cellBoundary.BottomRightY);
|
||||||
|
|
||||||
|
// 셀 경계를 Path로 그리기 (정확한 4개 모서리 연결)
|
||||||
|
var pathGeometry = new PathGeometry();
|
||||||
|
var pathFigure = new PathFigure();
|
||||||
|
pathFigure.StartPoint = topLeft;
|
||||||
|
pathFigure.Segments.Add(new LineSegment(topRight, true));
|
||||||
|
pathFigure.Segments.Add(new LineSegment(bottomRight, true));
|
||||||
|
pathFigure.Segments.Add(new LineSegment(bottomLeft, true));
|
||||||
|
pathFigure.IsClosed = true;
|
||||||
|
pathGeometry.Figures.Add(pathFigure);
|
||||||
|
|
||||||
|
var path = new Path
|
||||||
|
{
|
||||||
|
Data = pathGeometry,
|
||||||
|
Stroke = Brushes.DarkBlue,
|
||||||
|
StrokeThickness = 3,
|
||||||
|
Fill = null
|
||||||
|
};
|
||||||
|
|
||||||
|
cnvVisualization.Children.Add(path);
|
||||||
|
|
||||||
|
// 라벨 표시 (셀 중앙)
|
||||||
|
if (!string.IsNullOrEmpty(cellBoundary.Label))
|
||||||
|
{
|
||||||
|
var centerX = (topLeft.X + bottomRight.X) / 2;
|
||||||
|
var centerY = (topLeft.Y + bottomRight.Y) / 2;
|
||||||
|
|
||||||
|
var label = new TextBlock
|
||||||
|
{
|
||||||
|
Text = cellBoundary.Label,
|
||||||
|
FontSize = 10,
|
||||||
|
Foreground = Brushes.DarkBlue,
|
||||||
|
FontWeight = FontWeights.Bold,
|
||||||
|
Background = Brushes.LightCyan
|
||||||
|
};
|
||||||
|
|
||||||
|
Canvas.SetLeft(label, centerX - 15); // 대략적인 중앙 정렬
|
||||||
|
Canvas.SetTop(label, centerY - 6);
|
||||||
|
cnvVisualization.Children.Add(label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawTexts()
|
||||||
|
{
|
||||||
|
if (_currentData == null) return;
|
||||||
|
foreach (var text in _currentData.TextEntities)
|
||||||
|
{
|
||||||
|
var point = TransformPoint(text.X, text.Y);
|
||||||
|
|
||||||
|
var textBlock = new TextBlock
|
||||||
|
{
|
||||||
|
Text = text.Text,
|
||||||
|
FontSize = 9,
|
||||||
|
Foreground = text.IsInTable ? Brushes.DarkBlue : Brushes.Gray
|
||||||
|
};
|
||||||
|
|
||||||
|
Canvas.SetLeft(textBlock, point.X);
|
||||||
|
Canvas.SetTop(textBlock, point.Y);
|
||||||
|
cnvVisualization.Children.Add(textBlock);
|
||||||
|
|
||||||
|
// 텍스트 위치 점 표시
|
||||||
|
var dot = new Ellipse
|
||||||
|
{
|
||||||
|
Width = 3,
|
||||||
|
Height = 3,
|
||||||
|
Fill = text.IsInTable ? Brushes.Blue : Brushes.Gray
|
||||||
|
};
|
||||||
|
|
||||||
|
Canvas.SetLeft(dot, point.X - 1.5);
|
||||||
|
Canvas.SetTop(dot, point.Y - 1.5);
|
||||||
|
cnvVisualization.Children.Add(dot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawIntersections()
|
||||||
|
{
|
||||||
|
if (_currentData == null) return;
|
||||||
|
foreach (var intersection in _currentData.IntersectionPoints)
|
||||||
|
{
|
||||||
|
var point = TransformPoint(intersection.X, intersection.Y);
|
||||||
|
|
||||||
|
// 교차점 원 표시
|
||||||
|
var circle = new Ellipse
|
||||||
|
{
|
||||||
|
Width = 8,
|
||||||
|
Height = 8,
|
||||||
|
Fill = GetIntersectionColor(intersection),
|
||||||
|
Stroke = Brushes.Black,
|
||||||
|
StrokeThickness = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
Canvas.SetLeft(circle, point.X - 4);
|
||||||
|
Canvas.SetTop(circle, point.Y - 4);
|
||||||
|
cnvVisualization.Children.Add(circle);
|
||||||
|
|
||||||
|
// 교차점 타입 숫자 표시 (우측에)
|
||||||
|
var numberLabel = new TextBlock
|
||||||
|
{
|
||||||
|
Text = intersection.DirectionBits.ToString(),
|
||||||
|
FontSize = 12,
|
||||||
|
FontWeight = FontWeights.Bold,
|
||||||
|
Foreground = Brushes.Black,
|
||||||
|
Background = Brushes.Yellow,
|
||||||
|
Padding = new Thickness(2)
|
||||||
|
};
|
||||||
|
|
||||||
|
Canvas.SetLeft(numberLabel, point.X + 8); // 교차점 우측에 표시
|
||||||
|
Canvas.SetTop(numberLabel, point.Y - 6); // 약간 위쪽으로 조정
|
||||||
|
cnvVisualization.Children.Add(numberLabel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawDiagonals()
|
||||||
|
{
|
||||||
|
if (_currentData == null) return;
|
||||||
|
foreach (var diagonal in _currentData.DiagonalLines)
|
||||||
|
{
|
||||||
|
var startPoint = TransformPoint(diagonal.StartX, diagonal.StartY);
|
||||||
|
var endPoint = TransformPoint(diagonal.EndX, diagonal.EndY);
|
||||||
|
|
||||||
|
// 대각선 표시
|
||||||
|
var line = new Line
|
||||||
|
{
|
||||||
|
X1 = startPoint.X,
|
||||||
|
Y1 = startPoint.Y,
|
||||||
|
X2 = endPoint.X,
|
||||||
|
Y2 = endPoint.Y,
|
||||||
|
Stroke = Brushes.Green,
|
||||||
|
StrokeThickness = 2,
|
||||||
|
StrokeDashArray = new DoubleCollection { 5, 3 } // 점선으로 표시
|
||||||
|
};
|
||||||
|
cnvVisualization.Children.Add(line);
|
||||||
|
|
||||||
|
// 대각선 중앙에 라벨 표시
|
||||||
|
if (!string.IsNullOrEmpty(diagonal.Label))
|
||||||
|
{
|
||||||
|
var centerX = (startPoint.X + endPoint.X) / 2;
|
||||||
|
var centerY = (startPoint.Y + endPoint.Y) / 2;
|
||||||
|
|
||||||
|
var label = new TextBlock
|
||||||
|
{
|
||||||
|
Text = diagonal.Label,
|
||||||
|
FontSize = 9,
|
||||||
|
FontWeight = FontWeights.Bold,
|
||||||
|
Foreground = Brushes.Green,
|
||||||
|
Background = Brushes.White
|
||||||
|
};
|
||||||
|
Canvas.SetLeft(label, centerX - 20); // 중앙에서 약간 왼쪽으로
|
||||||
|
Canvas.SetTop(label, centerY - 10); // 중앙에서 약간 위쪽으로
|
||||||
|
cnvVisualization.Children.Add(label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private System.Windows.Media.Brush GetIntersectionColor(IntersectionInfo intersection)
|
||||||
|
{
|
||||||
|
if (intersection.IsTopLeft && intersection.IsBottomRight)
|
||||||
|
return Brushes.Purple; // 둘 다 가능
|
||||||
|
else if (intersection.IsTopLeft)
|
||||||
|
return Brushes.Green; // topLeft 후보
|
||||||
|
else if (intersection.IsBottomRight)
|
||||||
|
return Brushes.Blue; // bottomRight 후보
|
||||||
|
else
|
||||||
|
return Brushes.Red; // 기타
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshVisualization(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
RefreshVisualization();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BtnZoomFit_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
svViewer.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CnvVisualization_MouseMove(object sender, MouseEventArgs e)
|
||||||
|
{
|
||||||
|
var pos = e.GetPosition(cnvVisualization);
|
||||||
|
txtMousePos.Text = $"마우스: ({pos.X:F0}, {pos.Y:F0})";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,37 +8,37 @@ using System.Text.Json.Serialization;
|
|||||||
public class MappingTableData
|
public class MappingTableData
|
||||||
{
|
{
|
||||||
[JsonPropertyName("mapping_table")]
|
[JsonPropertyName("mapping_table")]
|
||||||
public MappingTable MappingTable { get; set; }
|
public MappingTable MappingTable { get; set; } = default!;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MappingTable
|
public class MappingTable
|
||||||
{
|
{
|
||||||
[JsonPropertyName("ailabel_to_systems")]
|
[JsonPropertyName("ailabel_to_systems")]
|
||||||
public Dictionary<string, SystemFields> AilabelToSystems { get; set; }
|
public Dictionary<string, SystemFields> AilabelToSystems { get; set; } = default!;
|
||||||
|
|
||||||
[JsonPropertyName("system_mappings")]
|
[JsonPropertyName("system_mappings")]
|
||||||
public SystemMappings SystemMappings { get; set; }
|
public SystemMappings SystemMappings { get; set; } = default!;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SystemFields
|
public class SystemFields
|
||||||
{
|
{
|
||||||
[JsonPropertyName("molit")]
|
[JsonPropertyName("molit")]
|
||||||
public string Molit { get; set; }
|
public string Molit { get; set; } = default!;
|
||||||
|
|
||||||
[JsonPropertyName("expressway")]
|
[JsonPropertyName("expressway")]
|
||||||
public string Expressway { get; set; }
|
public string Expressway { get; set; } = default!;
|
||||||
|
|
||||||
[JsonPropertyName("railway")]
|
[JsonPropertyName("railway")]
|
||||||
public string Railway { get; set; }
|
public string Railway { get; set; } = default!;
|
||||||
|
|
||||||
[JsonPropertyName("docaikey")]
|
[JsonPropertyName("docaikey")]
|
||||||
public string DocAiKey { get; set; }
|
public string DocAiKey { get; set; } = default!;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SystemMappings
|
public class SystemMappings
|
||||||
{
|
{
|
||||||
[JsonPropertyName("expressway_to_transportation")]
|
[JsonPropertyName("expressway_to_transportation")]
|
||||||
public Dictionary<string, string> ExpresswayToTransportation { get; set; }
|
public Dictionary<string, string> ExpresswayToTransportation { get; set; } = default!;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 필드 매퍼 클래스
|
// 필드 매퍼 클래스
|
||||||
@@ -56,21 +56,164 @@ public class FieldMapper
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static FieldMapper LoadFromFile(string jsonFilePath)
|
public static FieldMapper LoadFromFile(string jsonFilePath)
|
||||||
{
|
{
|
||||||
string jsonContent = File.ReadAllText(jsonFilePath);
|
try
|
||||||
var options = new JsonSerializerOptions
|
|
||||||
{
|
{
|
||||||
PropertyNameCaseInsensitive = true,
|
string jsonContent = File.ReadAllText(jsonFilePath, System.Text.Encoding.UTF8);
|
||||||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
Console.WriteLine($"[DEBUG] 매핑 테이블 JSON 파일 크기: {jsonContent.Length} bytes");
|
||||||
};
|
|
||||||
|
// JSON 내용 정리 (주석 제거 등)
|
||||||
var mappingData = JsonSerializer.Deserialize<MappingTableData>(jsonContent, options);
|
jsonContent = CleanJsonContent(jsonContent);
|
||||||
return new FieldMapper(mappingData);
|
|
||||||
|
var options = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||||
|
AllowTrailingCommas = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var mappingData = JsonSerializer.Deserialize<MappingTableData>(jsonContent, options);
|
||||||
|
Console.WriteLine($"[DEBUG] 매핑 테이블 로드 성공: {mappingData?.MappingTable?.AilabelToSystems?.Count ?? 0}개 항목");
|
||||||
|
return new FieldMapper(mappingData!);
|
||||||
|
}
|
||||||
|
catch (JsonException jsonEx)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"❌ 매핑 테이블 JSON 파싱 오류: {jsonEx.Message}");
|
||||||
|
Console.WriteLine($"❌ 파일: {jsonFilePath}");
|
||||||
|
if (File.Exists(jsonFilePath))
|
||||||
|
{
|
||||||
|
string content = File.ReadAllText(jsonFilePath);
|
||||||
|
Console.WriteLine($"❌ JSON 내용 미리보기 (첫 500자):");
|
||||||
|
Console.WriteLine(content.Length > 500 ? content.Substring(0, 500) + "..." : content);
|
||||||
|
}
|
||||||
|
throw new Exception($"매핑 테이블 JSON 파일 파싱 실패: {jsonEx.Message}\n파일: {jsonFilePath}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"❌ 매핑 테이블 로드 중 오류: {ex.Message}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JSON 내용을 정리하여 파싱 가능한 상태로 만듭니다.
|
||||||
|
/// 주석 제거 및 기타 무효한 문자 처리
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="jsonContent">원본 JSON 내용</param>
|
||||||
|
/// <returns>정리된 JSON 내용</returns>
|
||||||
|
private static string CleanJsonContent(string jsonContent)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(jsonContent))
|
||||||
|
return jsonContent;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 줄별로 처리하여 주석 제거
|
||||||
|
var lines = jsonContent.Split('\n');
|
||||||
|
var cleanedLines = new List<string>();
|
||||||
|
|
||||||
|
bool inMultiLineComment = false;
|
||||||
|
|
||||||
|
foreach (string line in lines)
|
||||||
|
{
|
||||||
|
string processedLine = line;
|
||||||
|
|
||||||
|
// 멀티라인 주석 처리 (/* */)
|
||||||
|
if (inMultiLineComment)
|
||||||
|
{
|
||||||
|
int endIndex = processedLine.IndexOf("*/");
|
||||||
|
if (endIndex >= 0)
|
||||||
|
{
|
||||||
|
processedLine = processedLine.Substring(endIndex + 2);
|
||||||
|
inMultiLineComment = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
continue; // 전체 라인이 주석
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 멀티라인 주석 시작 확인
|
||||||
|
int multiLineStart = processedLine.IndexOf("/*");
|
||||||
|
if (multiLineStart >= 0)
|
||||||
|
{
|
||||||
|
int multiLineEnd = processedLine.IndexOf("*/", multiLineStart + 2);
|
||||||
|
if (multiLineEnd >= 0)
|
||||||
|
{
|
||||||
|
// 같은 라인에서 시작하고 끝나는 주석
|
||||||
|
processedLine = processedLine.Substring(0, multiLineStart) +
|
||||||
|
processedLine.Substring(multiLineEnd + 2);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 멀티라인 주석 시작
|
||||||
|
processedLine = processedLine.Substring(0, multiLineStart);
|
||||||
|
inMultiLineComment = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 싱글라인 주석 제거 (//) - 문자열 내부의 //는 제외
|
||||||
|
bool inString = false;
|
||||||
|
bool escaped = false;
|
||||||
|
int commentIndex = -1;
|
||||||
|
|
||||||
|
for (int i = 0; i < processedLine.Length - 1; i++)
|
||||||
|
{
|
||||||
|
char current = processedLine[i];
|
||||||
|
char next = processedLine[i + 1];
|
||||||
|
|
||||||
|
if (escaped)
|
||||||
|
{
|
||||||
|
escaped = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current == '\\')
|
||||||
|
{
|
||||||
|
escaped = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current == '"')
|
||||||
|
{
|
||||||
|
inString = !inString;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inString && current == '/' && next == '/')
|
||||||
|
{
|
||||||
|
commentIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commentIndex >= 0)
|
||||||
|
{
|
||||||
|
processedLine = processedLine.Substring(0, commentIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 빈 라인이 아니면 추가
|
||||||
|
if (!string.IsNullOrWhiteSpace(processedLine))
|
||||||
|
{
|
||||||
|
cleanedLines.Add(processedLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string result = string.Join("\n", cleanedLines);
|
||||||
|
Console.WriteLine($"[DEBUG] 매핑 테이블 JSON 정리 완료: {jsonContent.Length} -> {result.Length} bytes");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"❌ 매핑 테이블 JSON 정리 중 오류: {ex.Message}");
|
||||||
|
// 정리 실패시 원본 반환
|
||||||
|
return jsonContent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// AI 라벨을 고속도로공사 필드명으로 변환
|
/// AI 라벨을 고속도로공사 필드명으로 변환
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string AilabelToExpressway(string ailabel)
|
public string? AilabelToExpressway(string ailabel)
|
||||||
{
|
{
|
||||||
if (_mappingData.MappingTable.AilabelToSystems.TryGetValue(ailabel, out var systemFields))
|
if (_mappingData.MappingTable.AilabelToSystems.TryGetValue(ailabel, out var systemFields))
|
||||||
{
|
{
|
||||||
@@ -82,7 +225,7 @@ public class FieldMapper
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// AI 라벨을 DocAiKey 값으로 변환
|
/// AI 라벨을 DocAiKey 값으로 변환
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string AilabelToDocAiKey(string ailabel)
|
public string? AilabelToDocAiKey(string ailabel)
|
||||||
{
|
{
|
||||||
if (_mappingData.MappingTable.AilabelToSystems.TryGetValue(ailabel, out var systemFields))
|
if (_mappingData.MappingTable.AilabelToSystems.TryGetValue(ailabel, out var systemFields))
|
||||||
{
|
{
|
||||||
@@ -94,7 +237,7 @@ public class FieldMapper
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 고속도로공사 필드명을 교통부 필드명으로 변환
|
/// 고속도로공사 필드명을 교통부 필드명으로 변환
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ExpresswayToTransportation(string expresswayField)
|
public string? ExpresswayToTransportation(string expresswayField)
|
||||||
{
|
{
|
||||||
if (_mappingData.MappingTable.SystemMappings.ExpresswayToTransportation.TryGetValue(expresswayField, out var transportationField))
|
if (_mappingData.MappingTable.SystemMappings.ExpresswayToTransportation.TryGetValue(expresswayField, out var transportationField))
|
||||||
{
|
{
|
||||||
@@ -106,7 +249,7 @@ public class FieldMapper
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// DocAiKey 값으로부터 해당하는 AI 라벨을 반환
|
/// DocAiKey 값으로부터 해당하는 AI 라벨을 반환
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string DocAiKeyToAilabel(string docAiKey)
|
public string? DocAiKeyToAilabel(string docAiKey)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(docAiKey))
|
if (string.IsNullOrEmpty(docAiKey))
|
||||||
{
|
{
|
||||||
@@ -126,7 +269,7 @@ public class FieldMapper
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Expressway 필드값으로부터 해당하는 AI 라벨을 반환
|
/// Expressway 필드값으로부터 해당하는 AI 라벨을 반환
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ExpresswayToAilabel(string expresswayField)
|
public string? ExpresswayToAilabel(string expresswayField)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(expresswayField))
|
if (string.IsNullOrEmpty(expresswayField))
|
||||||
{
|
{
|
||||||
@@ -146,7 +289,7 @@ public class FieldMapper
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// AI 라벨 → 고속도로공사 → 교통부 순서로 변환
|
/// AI 라벨 → 고속도로공사 → 교통부 순서로 변환
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string AilabelToTransportationViaExpressway(string ailabel)
|
public string? AilabelToTransportationViaExpressway(string ailabel)
|
||||||
{
|
{
|
||||||
var expresswayField = AilabelToExpressway(ailabel);
|
var expresswayField = AilabelToExpressway(ailabel);
|
||||||
if (!string.IsNullOrEmpty(expresswayField))
|
if (!string.IsNullOrEmpty(expresswayField))
|
||||||
@@ -159,7 +302,7 @@ public class FieldMapper
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// AI 라벨에 해당하는 모든 시스템의 필드명을 반환
|
/// AI 라벨에 해당하는 모든 시스템의 필드명을 반환
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SystemFields GetAllSystemFields(string ailabel)
|
public SystemFields? GetAllSystemFields(string ailabel)
|
||||||
{
|
{
|
||||||
if (_mappingData.MappingTable.AilabelToSystems.TryGetValue(ailabel, out var systemFields))
|
if (_mappingData.MappingTable.AilabelToSystems.TryGetValue(ailabel, out var systemFields))
|
||||||
{
|
{
|
||||||
@@ -171,9 +314,9 @@ public class FieldMapper
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 여러 AI 라벨을 한번에 고속도로공사 필드명으로 변환
|
/// 여러 AI 라벨을 한번에 고속도로공사 필드명으로 변환
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Dictionary<string, string> BatchConvertAilabelToExpressway(IEnumerable<string> ailabels)
|
public Dictionary<string, string?> BatchConvertAilabelToExpressway(IEnumerable<string> ailabels)
|
||||||
{
|
{
|
||||||
var results = new Dictionary<string, string>();
|
var results = new Dictionary<string, string?>();
|
||||||
foreach (var label in ailabels)
|
foreach (var label in ailabels)
|
||||||
{
|
{
|
||||||
results[label] = AilabelToExpressway(label);
|
results[label] = AilabelToExpressway(label);
|
||||||
@@ -184,9 +327,9 @@ public class FieldMapper
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 여러 고속도로공사 필드를 한번에 교통부 필드명으로 변환
|
/// 여러 고속도로공사 필드를 한번에 교통부 필드명으로 변환
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Dictionary<string, string> BatchConvertExpresswayToTransportation(IEnumerable<string> expresswayFields)
|
public Dictionary<string, string?> BatchConvertExpresswayToTransportation(IEnumerable<string> expresswayFields)
|
||||||
{
|
{
|
||||||
var results = new Dictionary<string, string>();
|
var results = new Dictionary<string, string?>();
|
||||||
foreach (var field in expresswayFields)
|
foreach (var field in expresswayFields)
|
||||||
{
|
{
|
||||||
results[field] = ExpresswayToTransportation(field);
|
results[field] = ExpresswayToTransportation(field);
|
||||||
|
|||||||
@@ -73,13 +73,7 @@
|
|||||||
"railway": "",
|
"railway": "",
|
||||||
"docaikey": "CSCOP"
|
"docaikey": "CSCOP"
|
||||||
},
|
},
|
||||||
"사업명_bot": {
|
"설계공구_공구명": {
|
||||||
"molit": "",
|
|
||||||
"expressway": "TD_CNAME",
|
|
||||||
"railway": "TD_CNAME",
|
|
||||||
"docaikey": "TDCNAME"
|
|
||||||
},
|
|
||||||
"설계공구_공구명": {
|
|
||||||
"molit": "",
|
"molit": "",
|
||||||
"expressway": "TD_DSECT",
|
"expressway": "TD_DSECT",
|
||||||
"railway": "",
|
"railway": "",
|
||||||
|
|||||||
134
notedetectproblem.txt
Normal file
134
notedetectproblem.txt
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
NOTE Detection Algorithm Context Report
|
||||||
|
|
||||||
|
Problem Summary
|
||||||
|
|
||||||
|
Successfully integrated NOTE extraction from ExportExcel_note.cs into the new modular architecture, but encountering
|
||||||
|
issues where only some NOTEs are being detected and finding their content boxes.
|
||||||
|
|
||||||
|
Current Status
|
||||||
|
|
||||||
|
✅ FIXED: No note content issue - reverted to original working cross-line intersection algorithm from ExportExcel_old.cs
|
||||||
|
|
||||||
|
🔄 ONGOING: Not detecting all NOTEs (missing notes 2 and 4 from a 4-note layout)
|
||||||
|
|
||||||
|
Architecture Overview
|
||||||
|
|
||||||
|
Key Files and Components
|
||||||
|
|
||||||
|
- Main Entry Point: Models/ExportExcel.cs:265-271 - calls note extraction in ExportAllDwgToExcelHeightSorted
|
||||||
|
- Core Algorithm: Models/DwgDataExtractor.cs:342-480 - ExtractNotesFromDrawing method
|
||||||
|
- Note Box Detection: Models/DwgDataExtractor.cs:513-569 - FindNoteBox method
|
||||||
|
- Excel Output: Models/ExcelDataWriter.cs:282-371 - WriteNoteEntities method
|
||||||
|
|
||||||
|
Current Algorithm Flow
|
||||||
|
|
||||||
|
1. Collection Phase: Gather all DBText, Polyline, and Line entities
|
||||||
|
2. NOTE Detection: Find DBText containing "NOTE" (case-insensitive)
|
||||||
|
3. Box Finding: For each NOTE, use cross-line intersection to find content box below
|
||||||
|
4. Content Extraction: Find text entities within detected boxes
|
||||||
|
5. Sorting & Grouping: Sort by coordinates (Y descending, X ascending) and group NOTE+content
|
||||||
|
6. Excel Output: Write to Excel with NOTE followed immediately by its content
|
||||||
|
|
||||||
|
Current Working Algorithm (Reverted from ExportExcel_old.cs)
|
||||||
|
|
||||||
|
FindNoteBox Method (DwgDataExtractor.cs:513-569)
|
||||||
|
|
||||||
|
// Draws horizontal search line below NOTE position
|
||||||
|
double searchY = notePos.Y - (noteHeight * 2);
|
||||||
|
var searchLineStart = new Point3d(notePos.X - noteHeight * 10, searchY, 0);
|
||||||
|
var searchLineEnd = new Point3d(notePos.X + noteHeight * 50, searchY, 0);
|
||||||
|
|
||||||
|
// 1. Check Polyline intersections
|
||||||
|
// 2. Check Line intersections and trace rectangles
|
||||||
|
// 3. Use usedBoxes HashSet to prevent duplicate assignment
|
||||||
|
|
||||||
|
IsValidNoteBox Validation (DwgDataExtractor.cs:1005-1032)
|
||||||
|
|
||||||
|
// Simple validation criteria:
|
||||||
|
// - Box must be below NOTE (box.maxPoint.Y < notePos.Y)
|
||||||
|
// - Size constraints: noteHeight < width/height < noteHeight * 100
|
||||||
|
// - Distance constraints: X distance < noteHeight * 50, Y distance < noteHeight * 10
|
||||||
|
|
||||||
|
Known Issues from Previous Sessions
|
||||||
|
|
||||||
|
Issue 1: 1/1/3/3 Duplicate Content (PREVIOUSLY FIXED)
|
||||||
|
|
||||||
|
Problem: Multiple NOTEs finding the same large spanning polyline
|
||||||
|
Root Cause: Box detection finding one large polyline spanning multiple note areas
|
||||||
|
Solution Applied: Used usedBoxes HashSet to prevent duplicate assignment
|
||||||
|
|
||||||
|
Issue 2: Reverse Note Ordering (PREVIOUSLY FIXED)
|
||||||
|
|
||||||
|
Problem: Notes written in reverse order
|
||||||
|
Solution Applied: Sort by Y descending (bigger Y = top), then X ascending
|
||||||
|
|
||||||
|
Issue 3: Wrong Note Grouping (PREVIOUSLY FIXED)
|
||||||
|
|
||||||
|
Problem: All NOTEs grouped first, then all content
|
||||||
|
Solution Applied: Group each NOTE immediately with its content
|
||||||
|
|
||||||
|
Issue 4: Missing NOTEs 2 and 4 (CURRENT ISSUE)
|
||||||
|
|
||||||
|
Problem: In a 4-note layout arranged as 1-2 (top row) and 3-4 (bottom row), only notes 1 and 3 are detected
|
||||||
|
Possible Causes:
|
||||||
|
- Search line positioning not intersecting with notes 2 and 4's content boxes
|
||||||
|
- Box validation criteria too restrictive for right-side notes
|
||||||
|
- Geometric relationship between NOTE position and content box differs for right-side notes
|
||||||
|
|
||||||
|
Debug Information Available
|
||||||
|
|
||||||
|
Last Known Debug Output (5 NOTEs detected but no content found)
|
||||||
|
|
||||||
|
[DEBUG] Note 텍스트 발견: 'NOTE' at (57.0572050838764,348.6990318186563,0)
|
||||||
|
[DEBUG] Note 텍스트 발견: 'NOTE' at (471.6194660633719,501.3393888589908,0)
|
||||||
|
[DEBUG] Note 텍스트 발견: 'NOTE' at (444.9503218738628,174.19527687737536,0)
|
||||||
|
[DEBUG] Note 텍스트 발견: 'NOTE' at (602.7327260134425,174.43523739278135,0)
|
||||||
|
[DEBUG] Note 텍스트 발견: 'NOTE' at (635.5065816693041,502.83938885945645,0)
|
||||||
|
|
||||||
|
Reference Image
|
||||||
|
|
||||||
|
- noteExample.png shows expected layout with numbered sections 1-7 in Korean text
|
||||||
|
- Shows box-structured layout where each NOTE should have corresponding content below
|
||||||
|
|
||||||
|
Key Coordinate Analysis
|
||||||
|
|
||||||
|
From debug logs, NOTEs at similar Y coordinates appear to be in pairs:
|
||||||
|
- Top Row: (444.95, 174.20) and (602.73, 174.44) - Y≈174
|
||||||
|
- Middle Row: (471.62, 501.34) and (635.51, 502.84) - Y≈502
|
||||||
|
- Single: (57.06, 348.70) - Y≈349
|
||||||
|
|
||||||
|
Pattern suggests left-right pairing where right-side NOTEs might need different search strategies.
|
||||||
|
|
||||||
|
Investigation Areas for Next Session
|
||||||
|
|
||||||
|
Priority 1: Search Line Geometry
|
||||||
|
|
||||||
|
- Analyze why horizontal search lines from right-side NOTEs don't intersect content boxes
|
||||||
|
- Consider adjusting search line direction/positioning for right-side notes
|
||||||
|
- Debug actual intersection results for missing NOTEs
|
||||||
|
|
||||||
|
Priority 2: Box Validation Criteria
|
||||||
|
|
||||||
|
- Review IsValidNoteBox distance calculations for right-side NOTEs
|
||||||
|
- Consider if content boxes for right-side NOTEs have different geometric relationships
|
||||||
|
|
||||||
|
Priority 3: Coordinate Pattern Analysis
|
||||||
|
|
||||||
|
- Investigate why NOTEs at (602.73, 174.44) and (635.51, 502.84) aren't finding content
|
||||||
|
- Compare successful vs failed NOTE positions and their content box relationships
|
||||||
|
|
||||||
|
Quick Start Commands for Next Session
|
||||||
|
|
||||||
|
1. Run existing code to see current NOTE detection results
|
||||||
|
2. Add detailed debug logging to FindNoteBox for specific coordinates: (602.73, 174.44) and (635.51, 502.84)
|
||||||
|
3. Analyze intersection results and box validation for these specific NOTEs
|
||||||
|
4. Consider geometric adjustments for right-side NOTE detection
|
||||||
|
|
||||||
|
Code State
|
||||||
|
|
||||||
|
- Current implementation in Models/DwgDataExtractor.cs uses proven cross-line intersection algorithm
|
||||||
|
- usedBoxes tracking prevents duplicate assignment
|
||||||
|
- NOTE+content grouping and Y-coordinate sorting working correctly
|
||||||
|
- Excel output formatting functional
|
||||||
|
|
||||||
|
The foundation is solid; focus should be on geometric refinements for complete NOTE detection coverage.
|
||||||
Reference in New Issue
Block a user