using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; // COM 객체 해제를 위해 필요 using System.Threading; using System.Threading.Tasks; using Teigha.DatabaseServices; using Teigha.Geometry; using Teigha.Runtime; using Excel = Microsoft.Office.Interop.Excel; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace DwgExtractorManual.Models { /// /// DWG 파일에서 Excel로 데이터 내보내기 클래스 /// AttributeReference, AttributeDefinition, DBText, MText 추출 지원 /// internal class ExportExcel : IDisposable { // ODA 서비스 객체 (managed by singleton) private Services appServices; // Excel COM 객체들 private Excel.Application excelApplication; private Excel.Workbook workbook1; private Excel.Worksheet titleBlockSheet; // Title Block용 시트 private Excel.Worksheet textEntitiesSheet; // Text Entities용 시트 private Excel.Workbook mappingWorkbook; // 매핑 데이터용 워크북 private Excel.Worksheet mappingSheet; // 매핑 데이터용 시트 readonly List MapKeys; // 각 시트의 현재 행 번호 private int titleBlockCurrentRow = 2; // 헤더가 1행이므로 데이터는 2행부터 시작 private int textEntitiesCurrentRow = 2; // 헤더가 1행이므로 데이터는 2행부터 시작 private int mappingDataCurrentRow = 2; // 헤더가 1행이므로 데이터는 2행부터 시작 // 매핑 데이터 저장용 딕셔너리 - (AILabel, DwgTag, DwgValue, PdfValue) private Dictionary> FileToMapkeyToLabelTagValuePdf = new Dictionary>(); private FieldMapper fieldMapper; // 생성자: ODA 및 Excel 초기화 public ExportExcel() { try { Debug.WriteLine("🔄 FieldMapper 로딩 중: mapping_table_json.json..."); fieldMapper = FieldMapper.LoadFromFile("fletimageanalysis/mapping_table_json.json"); Debug.WriteLine("✅ FieldMapper 로딩 성공"); MapKeys = fieldMapper.GetAllDocAiKeys(); Debug.WriteLine($"📊 총 DocAI 키 개수: {MapKeys?.Count ?? 0}"); // 매핑 테스트 (디버깅용) TestFieldMapper(); } catch (System.Exception ex) { Debug.WriteLine($"❌ FieldMapper 로딩 오류:"); Debug.WriteLine($" 메시지: {ex.Message}"); Debug.WriteLine($" 예외 타입: {ex.GetType().Name}"); if (ex.InnerException != null) { Debug.WriteLine($" 내부 예외: {ex.InnerException.Message}"); } Debug.WriteLine($" 스택 트레이스: {ex.StackTrace}"); throw; } Debug.WriteLine("🔄 ODA 초기화 중..."); InitializeTeighaServices(); Debug.WriteLine("🔄 Excel 초기화 중..."); InitializeExcel(); } // 필드 매퍼 테스트 private void TestFieldMapper() { Debug.WriteLine("[DEBUG] Testing field mapper..."); // 몇 가지 알려진 expressway 필드로 테스트 var testFields = new[] { "TD_DNAME_MAIN", "TD_DWGNO", "TD_DWGCODE", "TB_MTITIL"}; foreach (var field in testFields) { var aiLabel = fieldMapper.ExpresswayToAilabel(field); var docAiKey = fieldMapper.AilabelToDocAiKey(aiLabel); Debug.WriteLine($"[DEBUG] Field: {field} -> AILabel: {aiLabel} -> DocAiKey: {docAiKey}"); } } // Teigha 서비스 초기화 (싱글톤 사용) private void InitializeTeighaServices() { try { Debug.WriteLine("[DEBUG] TeighaServicesManager를 통한 Services 획득 중..."); appServices = TeighaServicesManager.Instance.AcquireServices(); Debug.WriteLine($"[DEBUG] Services 획득 성공. Reference Count: {TeighaServicesManager.Instance.ReferenceCount}"); } catch (Teigha.Runtime.Exception ex) { Debug.WriteLine($"[DEBUG] Teigha Services 초기화 실패: {ex.Message}"); throw; } } // Excel 애플리케이션 및 워크시트 초기화 private void InitializeExcel() { try { var excelApp = new Excel.Application(); excelApplication = excelApp; excelApplication.Visible = false; // WPF에서는 숨김 처리 Excel.Workbook workbook = excelApp.Workbooks.Add(); workbook1 = workbook; // Title Block Sheet 설정 (기본 Sheet1) titleBlockSheet = (Excel.Worksheet)workbook.Sheets[1]; titleBlockSheet.Name = "Title Block"; SetupTitleBlockHeaders(); // Text Entities Sheet 추가 textEntitiesSheet = (Excel.Worksheet)workbook.Sheets.Add(); textEntitiesSheet.Name = "Text Entities"; SetupTextEntitiesHeaders(); // 매핑 데이터용 워크북 및 시트 생성 mappingWorkbook = excelApp.Workbooks.Add(); mappingSheet = (Excel.Worksheet)mappingWorkbook.Sheets[1]; mappingSheet.Name = "Mapping Data"; SetupMappingHeaders(); } catch (System.Exception ex) { Debug.WriteLine($"Excel 초기화 중 오류 발생: {ex.Message}"); ReleaseExcelObjects(); throw; } } // Title Block 시트 헤더 설정 private void SetupTitleBlockHeaders() { titleBlockSheet.Cells[1, 1] = "Type"; // 예: AttributeReference, AttributeDefinition titleBlockSheet.Cells[1, 2] = "Name"; // BlockReference 이름 또는 BlockDefinition 이름 titleBlockSheet.Cells[1, 3] = "Tag"; // Attribute Tag titleBlockSheet.Cells[1, 4] = "Prompt"; // Attribute Prompt titleBlockSheet.Cells[1, 5] = "Value"; // Attribute 값 (TextString) titleBlockSheet.Cells[1, 6] = "Path"; // 원본 DWG 파일 전체 경로 titleBlockSheet.Cells[1, 7] = "FileName"; // 원본 DWG 파일 이름만 // 헤더 행 스타일 Excel.Range headerRange = titleBlockSheet.Range["A1:G1"]; headerRange.Font.Bold = true; headerRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightBlue); } // Text Entities 시트 헤더 설정 private void SetupTextEntitiesHeaders() { textEntitiesSheet.Cells[1, 1] = "Type"; // DBText, MText textEntitiesSheet.Cells[1, 2] = "Layer"; // Layer 이름 textEntitiesSheet.Cells[1, 3] = "Text"; // 실제 텍스트 내용 textEntitiesSheet.Cells[1, 4] = "Path"; // 원본 DWG 파일 전체 경로 textEntitiesSheet.Cells[1, 5] = "FileName"; // 원본 DWG 파일 이름만 // 헤더 행 스타일 Excel.Range headerRange = textEntitiesSheet.Range["A1:E1"]; headerRange.Font.Bold = true; headerRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightGreen); } // 매핑 데이터 시트 헤더 설정 private void SetupMappingHeaders() { mappingSheet.Cells[1, 1] = "FileName"; // 파일 이름 mappingSheet.Cells[1, 2] = "MapKey"; // 매핑 키 mappingSheet.Cells[1, 3] = "AILabel"; // AI 라벨 mappingSheet.Cells[1, 4] = "DwgTag"; // DWG Tag mappingSheet.Cells[1, 5] = "Att_value"; // DWG 값 mappingSheet.Cells[1, 6] = "Pdf_value"; // PDF 값 (현재는 빈 값) // 헤더 행 스타일 Excel.Range headerRange = mappingSheet.Range["A1:F1"]; headerRange.Font.Bold = true; headerRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.LightYellow); } /// /// 단일 DWG 파일에서 AttributeReference/AttributeDefinition 데이터를 추출하여 /// 초기화된 Excel 워크시트에 추가합니다. /// /// 처리할 DWG 파일 경로 /// 진행 상태 보고를 위한 IProgress 객체 /// 작업 취소를 위한 CancellationToken /// 성공 시 true, 실패 시 false 반환 public bool ExportDwgToExcel(string filePath, IProgress progress = null, CancellationToken cancellationToken = default) { Debug.WriteLine($"[DEBUG] ExportDwgToExcel 시작: {filePath}"); if (excelApplication == null) { Debug.WriteLine("❌ Excel이 초기화되지 않았습니다."); return false; } if (!File.Exists(filePath)) { Debug.WriteLine($"❌ 파일이 존재하지 않습니다: {filePath}"); return false; } try { Debug.WriteLine("[DEBUG] 진행률 0% 보고"); progress?.Report(0); cancellationToken.ThrowIfCancellationRequested(); Debug.WriteLine("[DEBUG] ODA Database 객체 생성 중..."); // ODA Database 객체 생성 및 DWG 파일 읽기 using (var database = new Database(false, true)) { Debug.WriteLine($"[DEBUG] DWG 파일 읽기 시도: {filePath}"); database.ReadDwgFile(filePath, FileOpenMode.OpenForReadAndWriteNoShare, false, null); Debug.WriteLine("[DEBUG] DWG 파일 읽기 성공"); cancellationToken.ThrowIfCancellationRequested(); Debug.WriteLine("[DEBUG] 진행률 10% 보고"); progress?.Report(10); Debug.WriteLine("[DEBUG] 트랜잭션 시작 중..."); using (var tran = database.TransactionManager.StartTransaction()) { Debug.WriteLine("[DEBUG] BlockTable 접근 중..."); var bt = tran.GetObject(database.BlockTableId, OpenMode.ForRead) as BlockTable; Debug.WriteLine("[DEBUG] ModelSpace 접근 중..."); using (var btr = tran.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForRead) as BlockTableRecord) { int totalEntities = btr.Cast().Count(); Debug.WriteLine($"[DEBUG] 총 엔티티 수: {totalEntities}"); int processedCount = 0; foreach (ObjectId entId in btr) { cancellationToken.ThrowIfCancellationRequested(); using (var ent = tran.GetObject(entId, OpenMode.ForRead) as Entity) { // Layer 이름 가져오기 (공통) string layerName = GetLayerName(ent.LayerId, tran, database); var fileName = Path.GetFileNameWithoutExtension(database.Filename); // 파일명 유효성 검사 if (string.IsNullOrEmpty(fileName)) { fileName = "Unknown_File"; Debug.WriteLine($"[DEBUG] Using default filename: {fileName}"); } // 파일별 매핑 딕셔너리 초기화 if (!FileToMapkeyToLabelTagValuePdf.ContainsKey(fileName)) { FileToMapkeyToLabelTagValuePdf[fileName] = new Dictionary(); } // AttributeDefinition 추출 if (ent is AttributeDefinition attDef) { titleBlockSheet.Cells[titleBlockCurrentRow, 1] = attDef.GetType().Name; titleBlockSheet.Cells[titleBlockCurrentRow, 2] = attDef.BlockName; titleBlockSheet.Cells[titleBlockCurrentRow, 3] = attDef.Tag; titleBlockSheet.Cells[titleBlockCurrentRow, 4] = attDef.Prompt; titleBlockSheet.Cells[titleBlockCurrentRow, 5] = attDef.TextString; titleBlockSheet.Cells[titleBlockCurrentRow, 6] = database.Filename; titleBlockSheet.Cells[titleBlockCurrentRow, 7] = Path.GetFileName(database.Filename); titleBlockCurrentRow++; var tag = attDef.Tag; var aiLabel = fieldMapper.ExpresswayToAilabel(tag); var mapKey = fieldMapper.AilabelToDocAiKey(aiLabel); var attValue = attDef.TextString; Debug.WriteLine($"[DEBUG] AttributeDefinition - Tag: {tag}, AILabel: {aiLabel}, MapKey: {mapKey}"); // 매핑 데이터 저장 if (!string.IsNullOrEmpty(aiLabel)) { // mapKey가 null이면 aiLabel을 mapKey로 사용 var finalMapKey = mapKey ?? aiLabel; FileToMapkeyToLabelTagValuePdf[fileName][finalMapKey] = (aiLabel, tag, attValue, ""); Debug.WriteLine($"[DEBUG] Added mapping: {tag} -> {aiLabel} (MapKey: {finalMapKey})"); } else { // aiLabel이 null이면 tag를 사용하여 저장 var finalMapKey = mapKey ?? tag; if (!string.IsNullOrEmpty(finalMapKey)) { FileToMapkeyToLabelTagValuePdf[fileName][finalMapKey] = (tag, tag, attValue, ""); Debug.WriteLine($"[DEBUG] Added unmapped tag: {tag} -> {tag} (MapKey: {finalMapKey})"); } else { Debug.WriteLine($"[DEBUG] Skipped empty tag for AttributeDefinition"); } } } // BlockReference 및 그 안의 AttributeReference 추출 else if (ent is BlockReference blr) { foreach (ObjectId attId in blr.AttributeCollection) { cancellationToken.ThrowIfCancellationRequested(); using (var attRef = tran.GetObject(attId, OpenMode.ForRead) as AttributeReference) { if (attRef != null && attRef.TextString.Trim() != "") { titleBlockSheet.Cells[titleBlockCurrentRow, 1] = attRef.GetType().Name; titleBlockSheet.Cells[titleBlockCurrentRow, 2] = blr.Name; titleBlockSheet.Cells[titleBlockCurrentRow, 3] = attRef.Tag; titleBlockSheet.Cells[titleBlockCurrentRow, 4] = GetPromptFromAttributeReference(tran, blr, attRef.Tag); titleBlockSheet.Cells[titleBlockCurrentRow, 5] = attRef.TextString; titleBlockSheet.Cells[titleBlockCurrentRow, 6] = database.Filename; titleBlockSheet.Cells[titleBlockCurrentRow, 7] = Path.GetFileName(database.Filename); titleBlockCurrentRow++; var tag = attRef.Tag; var aiLabel = fieldMapper.ExpresswayToAilabel(tag); if (aiLabel == null) continue; var mapKey = fieldMapper.AilabelToDocAiKey(aiLabel); var attValue = attRef.TextString; Debug.WriteLine($"[DEBUG] AttributeReference - Tag: {tag}, AILabel: {aiLabel}, MapKey: {mapKey}"); // 매핑 데이터 저장 if (!string.IsNullOrEmpty(aiLabel)) { // mapKey가 null이면 aiLabel을 mapKey로 사용 var finalMapKey = mapKey ?? aiLabel; FileToMapkeyToLabelTagValuePdf[fileName][finalMapKey] = (aiLabel, tag, attValue, ""); Debug.WriteLine($"[DEBUG] Added mapping: {tag} -> {aiLabel} (MapKey: {finalMapKey})"); } else { // aiLabel이 null이면 tag를 사용하여 저장 var finalMapKey = mapKey ?? tag; if (!string.IsNullOrEmpty(finalMapKey)) { FileToMapkeyToLabelTagValuePdf[fileName][finalMapKey] = (tag, tag, attValue, ""); Debug.WriteLine($"[DEBUG] Added unmapped tag: {tag} -> {tag} (MapKey: {finalMapKey})"); } else { Debug.WriteLine($"[DEBUG] Skipped empty tag for AttributeReference"); } } } } } } // DBText 엔티티 추출 (별도 시트) else if (ent is DBText dbText) { textEntitiesSheet.Cells[textEntitiesCurrentRow, 1] = "DBText"; // Type textEntitiesSheet.Cells[textEntitiesCurrentRow, 2] = layerName; // Layer textEntitiesSheet.Cells[textEntitiesCurrentRow, 3] = dbText.TextString; // Text textEntitiesSheet.Cells[textEntitiesCurrentRow, 4] = database.Filename; // Path textEntitiesSheet.Cells[textEntitiesCurrentRow, 5] = Path.GetFileName(database.Filename); // FileName textEntitiesCurrentRow++; } // MText 엔티티 추출 (별도 시트) else if (ent is MText mText) { textEntitiesSheet.Cells[textEntitiesCurrentRow, 1] = "MText"; // Type textEntitiesSheet.Cells[textEntitiesCurrentRow, 2] = layerName; // Layer textEntitiesSheet.Cells[textEntitiesCurrentRow, 3] = mText.Contents; // Text textEntitiesSheet.Cells[textEntitiesCurrentRow, 4] = database.Filename; // Path textEntitiesSheet.Cells[textEntitiesCurrentRow, 5] = Path.GetFileName(database.Filename); // FileName textEntitiesCurrentRow++; } } // write values in new excel file with FileToMapkeyToLabelTag processedCount++; double currentProgress = 10.0 + (double)processedCount / totalEntities * 80.0; progress?.Report(Math.Min(currentProgress, 90.0)); } } tran.Commit(); } Debug.WriteLine($"[DEBUG] Transaction committed. Total mapping entries collected: {FileToMapkeyToLabelTagValuePdf.Sum(f => f.Value.Count)}"); // 매핑 데이터를 Excel 시트에 기록 Debug.WriteLine("[DEBUG] 매핑 데이터를 Excel에 기록 중..."); WriteMappingDataToExcel(); Debug.WriteLine("[DEBUG] 매핑 데이터 Excel 기록 완료"); } progress?.Report(100); return true; } catch (OperationCanceledException) { Debug.WriteLine("❌ 작업이 취소되었습니다."); progress?.Report(0); return false; } catch (Teigha.Runtime.Exception ex) { progress?.Report(0); Debug.WriteLine($"❌ DWG 파일 처리 중 Teigha 오류 발생:"); Debug.WriteLine($" 메시지: {ex.Message}"); Debug.WriteLine($" ErrorStatus: {ex.ErrorStatus}"); if (ex.InnerException != null) { Debug.WriteLine($" 내부 예외: {ex.InnerException.Message}"); } Debug.WriteLine($" 스택 트레이스: {ex.StackTrace}"); return false; } catch (System.Exception ex) { progress?.Report(0); Debug.WriteLine($"❌ 일반 오류 발생:"); Debug.WriteLine($" 메시지: {ex.Message}"); Debug.WriteLine($" 예외 타입: {ex.GetType().Name}"); if (ex.InnerException != null) { Debug.WriteLine($" 내부 예외: {ex.InnerException.Message}"); } Debug.WriteLine($" 스택 트레이스: {ex.StackTrace}"); return false; } } // 매핑 데이터를 Excel 시트에 기록 private void WriteMappingDataToExcel() { try { int currentRow = 2; // 헤더 다음 행부터 시작 Debug.WriteLine($"[DEBUG] Writing mapping data to Excel. Total files: {FileToMapkeyToLabelTagValuePdf.Count}"); Debug.WriteLine($"[DEBUG] 시작 행: {currentRow}"); foreach (var fileEntry in FileToMapkeyToLabelTagValuePdf) { string fileName = fileEntry.Key; var mappingData = fileEntry.Value; Debug.WriteLine($"[DEBUG] Processing file: {fileName}, entries: {mappingData.Count}"); foreach (var mapEntry in mappingData) { string mapKey = mapEntry.Key; (string aiLabel, string dwgTag, string attValue, string pdfValue) = mapEntry.Value; // null 값 방지 if (string.IsNullOrEmpty(fileName) || string.IsNullOrEmpty(mapKey)) { Debug.WriteLine($"[DEBUG] Skipping entry with null/empty values: fileName={fileName}, mapKey={mapKey}"); continue; } Debug.WriteLine($"[DEBUG] Writing row {currentRow}: {fileName} | {mapKey} | {aiLabel} | {dwgTag} | PDF: {pdfValue}"); try { mappingSheet.Cells[currentRow, 1] = fileName; // FileName mappingSheet.Cells[currentRow, 2] = mapKey; // MapKey mappingSheet.Cells[currentRow, 3] = aiLabel ?? ""; // AILabel mappingSheet.Cells[currentRow, 4] = dwgTag ?? ""; // DwgTag mappingSheet.Cells[currentRow, 5] = attValue ?? ""; // DwgValue (Att_value) mappingSheet.Cells[currentRow, 6] = pdfValue ?? ""; // PdfValue (Pdf_value) Debug.WriteLine($"[DEBUG] Row {currentRow} written successfully"); } catch (System.Exception ex) { Debug.WriteLine($"❌ Error writing row {currentRow}: {ex.Message}"); } currentRow++; } } Debug.WriteLine($"[DEBUG] Mapping data written to Excel. Total rows: {currentRow - 2}"); Debug.WriteLine($"[DEBUG] 최종 행 번호: {currentRow}"); } catch (System.Exception ex) { Debug.WriteLine($"❌ 매핑 데이터 Excel 기록 중 오류:"); Debug.WriteLine($" 메시지: {ex.Message}"); Debug.WriteLine($" 예외 타입: {ex.GetType().Name}"); if (ex.InnerException != null) { Debug.WriteLine($" 내부 예외: {ex.InnerException.Message}"); } Debug.WriteLine($" 스택 트레이스: {ex.StackTrace}"); throw; // 상위 메서드로 예외 전파 } } /// /// 기존 Excel 파일을 열어 JSON 파일의 PDF 분석 결과로 매핑 시트를 업데이트합니다. /// /// 기존 Excel 파일 경로 /// PDF 분석 결과 JSON 파일 경로 /// 성공 시 true, 실패 시 false public bool UpdateExistingExcelWithJson(string excelFilePath, string jsonFilePath) { try { Debug.WriteLine($"[DEBUG] 기존 Excel 파일 업데이트 시작: {excelFilePath}"); if (!File.Exists(excelFilePath)) { Debug.WriteLine($"❌ Excel 파일이 존재하지 않습니다: {excelFilePath}"); return false; } if (!File.Exists(jsonFilePath)) { Debug.WriteLine($"❌ JSON 파일이 존재하지 않습니다: {jsonFilePath}"); return false; } Debug.WriteLine($"[DEBUG] Excel 애플리케이션 초기화 중..."); // 기존 Excel 파일 열기 if (excelApplication == null) { excelApplication = new Excel.Application(); excelApplication.Visible = false; Debug.WriteLine("[DEBUG] 새 Excel 애플리케이션 생성됨"); } Debug.WriteLine($"[DEBUG] Excel 파일 열기 시도: {excelFilePath}"); mappingWorkbook = excelApplication.Workbooks.Open(excelFilePath); Debug.WriteLine("[DEBUG] Excel 파일 열기 성공"); Debug.WriteLine("[DEBUG] 'Mapping Data' 시트 찾는 중..."); mappingSheet = (Excel.Worksheet)mappingWorkbook.Sheets["Mapping Data"]; if (mappingSheet == null) { Debug.WriteLine("❌ 'Mapping Data' 시트를 찾을 수 없습니다."); // 사용 가능한 시트 목록 출력 Debug.WriteLine("사용 가능한 시트:"); foreach (Excel.Worksheet sheet in mappingWorkbook.Sheets) { Debug.WriteLine($" - {sheet.Name}"); } return false; } Debug.WriteLine("✅ 기존 Excel 파일 열기 성공"); // JSON에서 PDF 값 업데이트 Debug.WriteLine("[DEBUG] JSON 파싱 및 업데이트 시작"); bool result = UpdateMappingSheetFromJson(jsonFilePath); Debug.WriteLine($"[DEBUG] 업데이트 결과: {result}"); return result; } catch (System.Exception ex) { Debug.WriteLine($"❌ 기존 Excel 파일 업데이트 중 오류:"); Debug.WriteLine($" 메시지: {ex.Message}"); Debug.WriteLine($" 예외 타입: {ex.GetType().Name}"); if (ex.InnerException != null) { Debug.WriteLine($" 내부 예외: {ex.InnerException.Message}"); } Debug.WriteLine($" 스택 트레이스: {ex.StackTrace}"); return false; } } /// /// JSON 파일에서 PDF 분석 결과를 읽어 Excel 매핑 시트의 Pdf_value 컬럼을 업데이트합니다. /// /// PDF 분석 결과 JSON 파일 경로 /// 성공 시 true, 실패 시 false public bool UpdateMappingSheetFromJson(string jsonFilePath) { try { Debug.WriteLine($"[DEBUG] JSON 파일에서 PDF 값 업데이트 시작: {jsonFilePath}"); if (!File.Exists(jsonFilePath)) { Debug.WriteLine($"❌ JSON 파일이 존재하지 않습니다: {jsonFilePath}"); return false; } if (mappingSheet == null) { Debug.WriteLine("❌ 매핑 시트가 초기화되지 않았습니다."); return false; } // JSON 파일 읽기 및 정리 string jsonContent = File.ReadAllText(jsonFilePath, System.Text.Encoding.UTF8); Debug.WriteLine($"[DEBUG] JSON 파일 크기: {jsonContent.Length} bytes"); // JSON 내용 정리 (주석 제거 등) jsonContent = CleanJsonContent(jsonContent); JObject jsonData; try { jsonData = JObject.Parse(jsonContent); } catch (Newtonsoft.Json.JsonReaderException jsonEx) { Debug.WriteLine($"❌ JSON 파싱 오류: {jsonEx.Message}"); Debug.WriteLine($"❌ JSON 내용 미리보기 (첫 500자):"); Debug.WriteLine(jsonContent.Length > 500 ? jsonContent.Substring(0, 500) + "..." : jsonContent); throw new System.Exception($"PDF 분석 JSON 파일 파싱 실패: {jsonEx.Message}\n파일: {jsonFilePath}"); } var results = jsonData["results"] as JArray; if (results == null) { Debug.WriteLine("❌ JSON에서 'results' 배열을 찾을 수 없습니다."); Debug.WriteLine($"❌ JSON 루트 키들: {string.Join(", ", jsonData.Properties().Select(p => p.Name))}"); return false; } int updatedCount = 0; int totalEntries = 0; foreach (JObject result in results) { var fileInfo = result["file_info"]; var pdfAnalysis = result["pdf_analysis"]; if (fileInfo == null || pdfAnalysis == null) continue; string fileName = fileInfo["name"]?.ToString(); if (string.IsNullOrEmpty(fileName)) continue; // 파일 확장자 제거 string fileNameWithoutExt = Path.GetFileNameWithoutExtension(fileName); Debug.WriteLine($"[DEBUG] Processing PDF file: {fileNameWithoutExt}"); // PDF 분석 결과의 각 필드 처리 foreach (var property in pdfAnalysis.Cast()) { string aiLabel = property.Name; // 예: "설계공구_Station_col1" var valueObj = property.Value as JObject; if (valueObj == null) continue; string pdfValue = valueObj["value"]?.ToString(); if (string.IsNullOrEmpty(pdfValue)) continue; totalEntries++; Debug.WriteLine($"[DEBUG] Searching for match: FileName={fileNameWithoutExt}, AILabel={aiLabel}, Value={pdfValue}"); // Excel 시트에서 매칭되는 행 찾기 및 업데이트 if (UpdateExcelRow(fileNameWithoutExt, aiLabel, pdfValue)) { updatedCount++; Debug.WriteLine($"✅ Updated: {fileNameWithoutExt} -> {aiLabel} = {pdfValue}"); } else { Debug.WriteLine($"⚠️ No match found: {fileNameWithoutExt} -> {aiLabel}"); } } } Debug.WriteLine($"[DEBUG] PDF 값 업데이트 완료: {updatedCount}/{totalEntries} 업데이트됨"); return true; } catch (System.Exception ex) { Debug.WriteLine($"❌ JSON에서 PDF 값 업데이트 중 오류:"); Debug.WriteLine($" 메시지: {ex.Message}"); Debug.WriteLine($" 예외 타입: {ex.GetType().Name}"); if (ex.InnerException != null) { Debug.WriteLine($" 내부 예외: {ex.InnerException.Message}"); } Debug.WriteLine($" 스택 트레이스: {ex.StackTrace}"); return false; } } /// /// Excel 매핑 시트에서 FileName과 AILabel이 매칭되는 행을 찾아 Pdf_value를 업데이트합니다. /// /// 파일명 (확장자 제외) /// AI 라벨 (예: "설계공구_Station_col1") /// PDF에서 추출된 값 /// 업데이트 성공 시 true private bool UpdateExcelRow(string fileName, string aiLabel, string pdfValue) { try { // Excel 시트의 마지막 사용된 행 찾기 Excel.Range usedRange = mappingSheet.UsedRange; if (usedRange == null) return false; int lastRow = usedRange.Rows.Count; // 2행부터 검색 (1행은 헤더) for (int row = 2; row <= lastRow; row++) { // Column 1: FileName, Column 3: AILabel 확인 var cellFileName = mappingSheet.Cells[row, 1]?.Value?.ToString() ?? ""; var cellAiLabel = mappingSheet.Cells[row, 3]?.Value?.ToString() ?? ""; Debug.WriteLine($"[DEBUG] Row {row}: FileName='{cellFileName}', AILabel='{cellAiLabel}'"); // 매칭 확인 (대소문자 구분 없이) if (string.Equals(cellFileName.Trim(), fileName.Trim(), StringComparison.OrdinalIgnoreCase) && string.Equals(cellAiLabel.Trim(), aiLabel.Trim(), StringComparison.OrdinalIgnoreCase)) { // Column 6: Pdf_value에 값 기록 mappingSheet.Cells[row, 6] = pdfValue; Debug.WriteLine($"[DEBUG] Updated row {row}, column 6 with value: {pdfValue}"); return true; } } return false; // 매칭되는 행을 찾지 못함 } catch (System.Exception ex) { Debug.WriteLine($"❌ Excel 행 업데이트 중 오류: {ex.Message}"); return false; } } // Paste the helper function from above here public string GetPromptFromAttributeReference(Transaction tr, BlockReference blockref, string tag) { string prompt = null; BlockTableRecord blockDef = tr.GetObject(blockref.BlockTableRecord, OpenMode.ForRead) as BlockTableRecord; if (blockDef == null) return null; foreach (ObjectId objId in blockDef) { AttributeDefinition attDef = tr.GetObject(objId, OpenMode.ForRead) as AttributeDefinition; if (attDef != null) { if (attDef.Tag.Equals(tag, System.StringComparison.OrdinalIgnoreCase)) { prompt = attDef.Prompt; break; } } } return prompt; } /// /// 현재 열려있는 Excel 워크북을 저장합니다. /// /// 성공 시 true, 실패 시 false public bool SaveExcel() { try { if (mappingWorkbook != null) { mappingWorkbook.Save(); Debug.WriteLine("✅ Excel 파일 저장 완료"); return true; } if (workbook1 != null) { workbook1.Save(); Debug.WriteLine("✅ Excel 파일 저장 완료"); return true; } Debug.WriteLine("❌ 저장할 워크북이 없습니다."); return false; } catch (System.Exception ex) { Debug.WriteLine($"❌ Excel 파일 저장 중 오류: {ex.Message}"); return false; } } /// /// 매핑 워크북만 저장하고 닫습니다 (완전한 매핑용). /// /// 저장할 파일 경로 public void SaveMappingWorkbookOnly(string savePath) { try { Debug.WriteLine($"[DEBUG] 매핑 워크북 저장 시작: {savePath}"); if (mappingWorkbook == null) { Debug.WriteLine("❌ 매핑 워크북이 초기화되지 않았습니다."); return; } string directory = Path.GetDirectoryName(savePath); if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) { Directory.CreateDirectory(directory); } // 매핑 워크북만 저장 (Excel 2007+ 형식으로) mappingWorkbook.SaveAs(savePath, FileFormat: Excel.XlFileFormat.xlOpenXMLWorkbook, AccessMode: Excel.XlSaveAsAccessMode.xlNoChange); Debug.WriteLine($"✅ 매핑 워크북 저장 완료: {Path.GetFileName(savePath)}"); } catch (System.Exception ex) { Debug.WriteLine($"❌ 매핑 워크북 저장 중 오류: {ex.Message}"); Debug.WriteLine($" 스택 트레이스: {ex.StackTrace}"); throw; } } /// /// 현재 Excel 워크북을 지정된 경로에 저장하고 Excel 애플리케이션을 종료합니다. /// /// Excel 파일을 저장할 전체 경로 public void SaveAndCloseExcel(string savePath) { if (workbook1 == null) return; try { string directory = Path.GetDirectoryName(savePath); if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) { Directory.CreateDirectory(directory); } workbook1.SaveAs(savePath, AccessMode: Excel.XlSaveAsAccessMode.xlNoChange); // 매핑 데이터 워크북도 저장 if (mappingWorkbook != null) { string mappingPath = Path.Combine(directory, Path.GetFileNameWithoutExtension(savePath) + "_Mapping.xlsx"); mappingWorkbook.SaveAs(mappingPath, AccessMode: Excel.XlSaveAsAccessMode.xlNoChange); } } catch (System.Exception ex) { Debug.WriteLine($"Excel 파일 저장 중 오류 발생: {ex.Message}"); } finally { CloseExcelObjects(); } } /// /// Excel 객체들을 닫습니다 (저장하지 않음). /// public void CloseExcelObjectsWithoutSaving() { try { Debug.WriteLine("[DEBUG] Excel 객체 정리 시작"); if (workbook1 != null) { try { workbook1.Close(false); } catch { } } if (mappingWorkbook != null) { try { mappingWorkbook.Close(false); } catch { } } if (excelApplication != null) { try { excelApplication.Quit(); } catch { } } ReleaseExcelObjects(); Debug.WriteLine("✅ Excel 객체 정리 완료"); } catch (System.Exception ex) { Debug.WriteLine($"❌ Excel 객체 정리 중 오류: {ex.Message}"); } } private void CloseExcelObjects() { if (workbook1 != null) { try { workbook1.Close(false); } catch { } } if (mappingWorkbook != null) { try { mappingWorkbook.Close(false); } catch { } } if (excelApplication != null) { try { excelApplication.Quit(); } catch { } } ReleaseExcelObjects(); } private void ReleaseExcelObjects() { ReleaseComObject(titleBlockSheet); ReleaseComObject(textEntitiesSheet); ReleaseComObject(mappingSheet); ReleaseComObject(workbook1); ReleaseComObject(mappingWorkbook); ReleaseComObject(excelApplication); titleBlockSheet = null; textEntitiesSheet = null; mappingSheet = null; workbook1 = null; mappingWorkbook = null; excelApplication = null; } private void ReleaseComObject(object obj) { try { if (obj != null && Marshal.IsComObject(obj)) { Marshal.ReleaseComObject(obj); } } catch (System.Exception) { // 해제 중 오류 발생 시 무시 } finally { obj = null; } } /// /// Layer ID로부터 Layer 이름을 가져옵니다. /// /// Layer ObjectId /// 현재 트랜잭션 /// 데이터베이스 객체 /// Layer 이름 또는 빈 문자열 private string GetLayerName(ObjectId layerId, Transaction transaction, Database database) { try { using (var layerTableRecord = transaction.GetObject(layerId, OpenMode.ForRead) as LayerTableRecord) { return layerTableRecord?.Name ?? ""; } } catch (System.Exception ex) { Debug.WriteLine($"Layer 이름 가져오기 오류: {ex.Message}"); return ""; } } /// /// 매핑 딕셔너리를 JSON 파일로 저장합니다. /// /// 저장할 JSON 파일 경로 public void SaveMappingDictionary(string filePath) { try { Debug.WriteLine($"[DEBUG] 매핑 딕셔너리 저장 시작: {filePath}"); // 딕셔너리를 직렬화 가능한 형태로 변환 var serializableData = new Dictionary>(); foreach (var fileEntry in FileToMapkeyToLabelTagValuePdf) { var fileData = new Dictionary(); foreach (var mapEntry in fileEntry.Value) { fileData[mapEntry.Key] = new { AILabel = mapEntry.Value.Item1, DwgTag = mapEntry.Value.Item2, DwgValue = mapEntry.Value.Item3, PdfValue = mapEntry.Value.Item4 }; } serializableData[fileEntry.Key] = fileData; } // JSON으로 직렬화 string jsonContent = JsonConvert.SerializeObject(serializableData, Formatting.Indented); File.WriteAllText(filePath, jsonContent, System.Text.Encoding.UTF8); Debug.WriteLine($"✅ 매핑 딕셔너리 저장 완료: {Path.GetFileName(filePath)}"); Debug.WriteLine($"📊 저장된 파일 수: {FileToMapkeyToLabelTagValuePdf.Count}"); } catch (System.Exception ex) { Debug.WriteLine($"❌ 매핑 딕셔너리 저장 중 오류:"); Debug.WriteLine($" 메시지: {ex.Message}"); Debug.WriteLine($" 스택 트레이스: {ex.StackTrace}"); throw; } } /// /// JSON 파일에서 매핑 딕셔너리를 로드합니다. /// /// 로드할 JSON 파일 경로 public void LoadMappingDictionary(string filePath) { try { Debug.WriteLine($"[DEBUG] 매핑 딕셔너리 로드 시작: {filePath}"); if (!File.Exists(filePath)) { Debug.WriteLine($"⚠️ 매핑 파일이 존재하지 않습니다: {filePath}"); return; } string jsonContent = File.ReadAllText(filePath, System.Text.Encoding.UTF8); Debug.WriteLine($"[DEBUG] JSON 내용 길이: {jsonContent.Length}"); // JSON 내용 정리 (주석 제거 등) jsonContent = CleanJsonContent(jsonContent); Dictionary> deserializedData; try { deserializedData = JsonConvert.DeserializeObject>>(jsonContent); } catch (Newtonsoft.Json.JsonReaderException jsonEx) { Debug.WriteLine($"❌ JSON 파싱 오류: {jsonEx.Message}"); Debug.WriteLine($"❌ JSON 내용 미리보기 (첫 500자):"); Debug.WriteLine(jsonContent.Length > 500 ? jsonContent.Substring(0, 500) + "..." : jsonContent); throw new System.Exception($"매핑 JSON 파일 파싱 실패: {jsonEx.Message}\n파일: {filePath}"); } // 새로운 딕셔너리 초기화 FileToMapkeyToLabelTagValuePdf.Clear(); Debug.WriteLine("[DEBUG] 기존 딕셔너리 초기화됨"); if (deserializedData != null) { Debug.WriteLine($"[DEBUG] 역직렬화된 파일 수: {deserializedData.Count}"); foreach (var fileEntry in deserializedData) { Debug.WriteLine($"[DEBUG] 파일 처리 중: {fileEntry.Key}"); var fileData = new Dictionary(); foreach (var mapEntry in fileEntry.Value) { var valueObj = mapEntry.Value; string aiLabel = valueObj["AILabel"]?.ToString() ?? ""; string dwgTag = valueObj["DwgTag"]?.ToString() ?? ""; string dwgValue = valueObj["DwgValue"]?.ToString() ?? ""; string pdfValue = valueObj["PdfValue"]?.ToString() ?? ""; fileData[mapEntry.Key] = (aiLabel, dwgTag, dwgValue, pdfValue); Debug.WriteLine($"[DEBUG] 항목 로드: {mapEntry.Key} -> AI:{aiLabel}, DWG:{dwgTag}, PDF:{pdfValue}"); } FileToMapkeyToLabelTagValuePdf[fileEntry.Key] = fileData; Debug.WriteLine($"[DEBUG] 파일 {fileEntry.Key}: {fileData.Count}개 항목 로드됨"); } } Debug.WriteLine($"✅ 매핑 딕셔너리 로드 완료"); Debug.WriteLine($"📊 로드된 파일 수: {FileToMapkeyToLabelTagValuePdf.Count}"); Debug.WriteLine($"📊 총 항목 수: {FileToMapkeyToLabelTagValuePdf.Sum(f => f.Value.Count)}"); } catch (System.Exception ex) { Debug.WriteLine($"❌ 매핑 딕셔너리 로드 중 오류:"); Debug.WriteLine($" 메시지: {ex.Message}"); Debug.WriteLine($" 스택 트레이스: {ex.StackTrace}"); throw; } } /// /// 매핑 딕셔너리 데이터를 Excel 시트에 기록합니다 (완전한 매핑용). /// public void WriteCompleteMapping() { try { Debug.WriteLine("[DEBUG] WriteCompleteMapping 시작"); Debug.WriteLine($"[DEBUG] 매핑 딕셔너리 항목 수: {FileToMapkeyToLabelTagValuePdf.Count}"); if (FileToMapkeyToLabelTagValuePdf.Count == 0) { Debug.WriteLine("⚠️ 매핑 딕셔너리가 비어있습니다."); return; } if (mappingSheet == null) { Debug.WriteLine("❌ 매핑 시트가 초기화되지 않았습니다."); return; } Debug.WriteLine($"[DEBUG] 매핑 시트 이름: {mappingSheet.Name}"); Debug.WriteLine($"[DEBUG] 전체 데이터 항목 수: {FileToMapkeyToLabelTagValuePdf.Sum(f => f.Value.Count)}"); // 샘플 데이터 출력 (처음 3개) int sampleCount = 0; foreach (var fileEntry in FileToMapkeyToLabelTagValuePdf.Take(2)) { Debug.WriteLine($"[DEBUG] 파일: {fileEntry.Key}"); foreach (var mapEntry in fileEntry.Value.Take(3)) { var (aiLabel, dwgTag, dwgValue, pdfValue) = mapEntry.Value; Debug.WriteLine($"[DEBUG] {mapEntry.Key} -> AI:{aiLabel}, DWG:{dwgTag}, DWGVAL:{dwgValue}, PDF:{pdfValue}"); sampleCount++; } } // 기존 WriteMappingDataToExcel 메서드 호출 WriteMappingDataToExcel(); Debug.WriteLine("✅ WriteCompleteMapping 완료"); } catch (System.Exception ex) { Debug.WriteLine($"❌ WriteCompleteMapping 중 오류:"); Debug.WriteLine($" 메시지: {ex.Message}"); Debug.WriteLine($" 스택 트레이스: {ex.StackTrace}"); throw; } } /// /// PDF 분석 결과 JSON으로 매핑 딕셔너리의 PdfValue를 업데이트합니다. /// /// PDF 분석 결과 JSON 파일 경로 public void UpdateWithPdfData(string jsonFilePath) { try { Debug.WriteLine($"[DEBUG] PDF 데이터로 매핑 딕셔너리 업데이트 시작: {jsonFilePath}"); if (!File.Exists(jsonFilePath)) { Debug.WriteLine($"❌ JSON 파일이 존재하지 않습니다: {jsonFilePath}"); return; } // JSON 파일 읽기 및 정리 string jsonContent = File.ReadAllText(jsonFilePath, System.Text.Encoding.UTF8); Debug.WriteLine($"[DEBUG] JSON 파일 크기: {jsonContent.Length} bytes"); // JSON 내용 정리 (주석 제거 등) jsonContent = CleanJsonContent(jsonContent); JObject jsonData; try { jsonData = JObject.Parse(jsonContent); } catch (Newtonsoft.Json.JsonReaderException jsonEx) { Debug.WriteLine($"❌ JSON 파싱 오류: {jsonEx.Message}"); Debug.WriteLine($"❌ JSON 내용 미리보기 (첫 500자):"); Debug.WriteLine(jsonContent.Length > 500 ? jsonContent.Substring(0, 500) + "..." : jsonContent); throw new System.Exception($"PDF 분석 JSON 파일 파싱 실패: {jsonEx.Message}\n파일: {jsonFilePath}"); } var results = jsonData["results"] as JArray; if (results == null) { Debug.WriteLine("❌ JSON에서 'results' 배열을 찾을 수 없습니다."); Debug.WriteLine($"❌ JSON 루트 키들: {string.Join(", ", jsonData.Properties().Select(p => p.Name))}"); return; } int updatedCount = 0; int totalEntries = 0; foreach (JObject result in results) { var fileInfo = result["file_info"]; var pdfAnalysis = result["pdf_analysis"]; if (fileInfo == null || pdfAnalysis == null) continue; string fileName = fileInfo["name"]?.ToString(); if (string.IsNullOrEmpty(fileName)) continue; // 파일 확장자 제거 string fileNameWithoutExt = Path.GetFileNameWithoutExtension(fileName); Debug.WriteLine($"[DEBUG] Processing PDF file: {fileNameWithoutExt}"); // 해당 파일의 매핑 데이터 확인 if (!FileToMapkeyToLabelTagValuePdf.ContainsKey(fileNameWithoutExt)) { Debug.WriteLine($"⚠️ 매핑 데이터에 파일이 없습니다: {fileNameWithoutExt}"); continue; } // PDF 분석 결과의 각 필드 처리 foreach (var property in pdfAnalysis.Cast()) { string aiLabel = property.Name; // 예: "설계공구_Station_col1" var valueObj = property.Value as JObject; if (valueObj == null) continue; string pdfValue = valueObj["value"]?.ToString(); if (string.IsNullOrEmpty(pdfValue)) continue; totalEntries++; // 매핑 딕셔너리에서 해당 항목 찾기 var fileData = FileToMapkeyToLabelTagValuePdf[fileNameWithoutExt]; // AILabel로 매칭 찾기 var matchingEntry = fileData.FirstOrDefault(kvp => string.Equals(kvp.Value.Item1.Trim(), aiLabel.Trim(), StringComparison.OrdinalIgnoreCase)); if (!string.IsNullOrEmpty(matchingEntry.Key)) { // 기존 값 유지하면서 PdfValue만 업데이트 var existingValue = matchingEntry.Value; fileData[matchingEntry.Key] = (existingValue.Item1, existingValue.Item2, existingValue.Item3, pdfValue); updatedCount++; Debug.WriteLine($"✅ Updated: {fileNameWithoutExt} -> {aiLabel} = {pdfValue}"); } else { Debug.WriteLine($"⚠️ No match found: {fileNameWithoutExt} -> {aiLabel}"); } } } Debug.WriteLine($"[DEBUG] PDF 데이터 업데이트 완료: {updatedCount}/{totalEntries} 업데이트됨"); } catch (System.Exception ex) { Debug.WriteLine($"❌ PDF 데이터 업데이트 중 오류:"); Debug.WriteLine($" 메시지: {ex.Message}"); Debug.WriteLine($" 스택 트레이스: {ex.StackTrace}"); throw; } } /// /// JSON 내용을 정리하여 파싱 가능한 상태로 만듭니다. /// 주석 제거 및 기타 무효한 문자 처리 /// /// 원본 JSON 내용 /// 정리된 JSON 내용 private string CleanJsonContent(string jsonContent) { if (string.IsNullOrEmpty(jsonContent)) return jsonContent; try { // 줄별로 처리하여 주석 제거 var lines = jsonContent.Split('\n'); var cleanedLines = new List(); bool inMultiLineComment = false; foreach (string line in lines) { string processedLine = line; // 멀티라인 주석 처리 (/* */) if (inMultiLineComment) { int endIndex = processedLine.IndexOf("*/"); if (endIndex >= 0) { processedLine = processedLine.Substring(endIndex + 2); inMultiLineComment = false; } else { continue; // 전체 라인이 주석 } } // 멀티라인 주석 시작 확인 int multiLineStart = processedLine.IndexOf("/*"); if (multiLineStart >= 0) { int multiLineEnd = processedLine.IndexOf("*/", multiLineStart + 2); if (multiLineEnd >= 0) { // 같은 라인에서 시작하고 끝나는 주석 processedLine = processedLine.Substring(0, multiLineStart) + processedLine.Substring(multiLineEnd + 2); } else { // 멀티라인 주석 시작 processedLine = processedLine.Substring(0, multiLineStart); inMultiLineComment = true; } } // 싱글라인 주석 제거 (//) - 문자열 내부의 //는 제외 bool inString = false; bool escaped = false; int commentIndex = -1; for (int i = 0; i < processedLine.Length - 1; i++) { char current = processedLine[i]; char next = processedLine[i + 1]; if (escaped) { escaped = false; continue; } if (current == '\\') { escaped = true; continue; } if (current == '"') { inString = !inString; continue; } if (!inString && current == '/' && next == '/') { commentIndex = i; break; } } if (commentIndex >= 0) { processedLine = processedLine.Substring(0, commentIndex); } // 빈 라인이 아니면 추가 if (!string.IsNullOrWhiteSpace(processedLine)) { cleanedLines.Add(processedLine); } } string result = string.Join("\n", cleanedLines); Debug.WriteLine($"[DEBUG] JSON 정리 완료: {jsonContent.Length} -> {result.Length} bytes"); return result; } catch (System.Exception ex) { Debug.WriteLine($"❌ JSON 정리 중 오류: {ex.Message}"); // 정리 실패시 원본 반환 return jsonContent; } } public void Dispose() { try { Debug.WriteLine("[DEBUG] ExportExcel Dispose 시작"); if (excelApplication != null) { Debug.WriteLine("[DEBUG] Excel 객체 정리 중..."); CloseExcelObjects(); } if (appServices != null) { Debug.WriteLine("[DEBUG] Teigha Services 해제 중..."); try { TeighaServicesManager.Instance.ReleaseServices(); Debug.WriteLine($"[DEBUG] Teigha Services 해제 완료. Remaining ref count: {TeighaServicesManager.Instance.ReferenceCount}"); } catch (System.Exception ex) { Debug.WriteLine($"[DEBUG] Teigha Services 해제 중 오류 (무시됨): {ex.Message}"); } finally { appServices = null; } } Debug.WriteLine("[DEBUG] ExportExcel Dispose 완료"); } catch (System.Exception ex) { Debug.WriteLine($"[DEBUG] ExportExcel Dispose 중 전역 오류: {ex.Message}"); // Disposal 오류는 로그만 남기고 계속 진행 } } } }