From 50705ab0e83541ca6f66de0c8341e4f7e1b549ab Mon Sep 17 00:00:00 2001 From: windpacer Date: Sun, 24 May 2026 21:34:24 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20P&ID=20=EC=97=B0=EA=B2=B0=20=EC=97=91?= =?UTF-8?q?=EC=85=80=20=EB=9D=BC=EC=9A=B4=EB=93=9C=ED=8A=B8=EB=A6=BD=20?= =?UTF-8?q?=E2=80=94=20id=20=EC=95=88=EC=A0=95=20=ED=82=A4=20+=20=EC=9A=B4?= =?UTF-8?q?=EC=A0=84=EC=9E=90=20=EB=AC=B8=EC=84=9C=EA=B7=9C=EC=B9=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ExportToExcelAsync: 17번째 컬럼 id(pid_equipment.id) 추가 (col1~16 위치 불변) - ImportFromExcelAsync: id 우선 매칭 — id 있으면 그 행만 in-place UPDATE (다중경로 보존), 빈 id면 INSERT, col17 헤더가 'id'가 아닌 옛 파일은 tag_no 폴백 - PidImportResult.RowsInserted 추가 + import 로그 신규건수 포함 - 구조설명-6-2차플랜트-byPBK.xlsx 문서규칙: upsert_pid_connection(9bcba0a) 연동 규칙으로 슬림화 (콤마=병렬 병합, 카테고리 매핑, 멱등/잠금/변경금지는 도구가 처리) Co-Authored-By: Claude Opus 4.7 --- src/Core/Application/DTOs/PidResponse.cs | 1 + .../Services/PidExtractorService.cs | 60 +++++++++++++----- src/Web/Controllers/PidController.cs | 4 +- 구조설명-6-2차플랜트-byPBK.xlsx | Bin 0 -> 21541 bytes 4 files changed, 48 insertions(+), 17 deletions(-) create mode 100644 구조설명-6-2차플랜트-byPBK.xlsx diff --git a/src/Core/Application/DTOs/PidResponse.cs b/src/Core/Application/DTOs/PidResponse.cs index 0c0d8a4..8e75de1 100644 --- a/src/Core/Application/DTOs/PidResponse.cs +++ b/src/Core/Application/DTOs/PidResponse.cs @@ -23,6 +23,7 @@ public record PidImportResult public int SheetsProcessed { get; init; } public int RowsRead { get; init; } public int RowsUpdated { get; init; } + public int RowsInserted { get; init; } public int Unmatched { get; init; } public List UnmatchedTags { get; init; } = new(); } diff --git a/src/Core/Application/Services/PidExtractorService.cs b/src/Core/Application/Services/PidExtractorService.cs index 02974bd..f4abc95 100644 --- a/src/Core/Application/Services/PidExtractorService.cs +++ b/src/Core/Application/Services/PidExtractorService.cs @@ -532,8 +532,9 @@ public class PidExtractorService : IPidExtractorService worksheet.Cells[1, 14].Value = "From_at"; worksheet.Cells[1, 15].Value = "To_at"; worksheet.Cells[1, 16].Value = "태그분류"; + worksheet.Cells[1, 17].Value = "id"; - using var headerRange = worksheet.Cells[1, 1, 1, 16]; + using var headerRange = worksheet.Cells[1, 1, 1, 17]; headerRange.Style.Font.Bold = true; headerRange.Style.Fill.PatternType = OfficeOpenXml.Style.ExcelFillStyle.Solid; headerRange.Style.Fill.BackgroundColor.SetColor(System.Drawing.Color.LightGray); @@ -564,6 +565,7 @@ public class PidExtractorService : IPidExtractorService PidEquipment.TagClassField => "현장", _ => "" }; + worksheet.Cells[row, 17].Value = item.Id; // 안정 키(라운드트립 매칭용) row++; } @@ -575,8 +577,9 @@ public class PidExtractorService : IPidExtractorService } /// - /// 편집 엑셀(ExportToExcelAsync 포맷, 16컬럼) → pid_equipment UPSERT. - /// 매칭 키 = 태그번호(col1, 대소문자 무시·trim). 같은 TagNo 다중 행이면 전부 갱신. + /// 편집 엑셀(ExportToExcelAsync 포맷, 17컬럼) → pid_equipment UPSERT. + /// 매칭 키 = id(col17, 안정 키). id 있으면 그 행만 in-place UPDATE(다중경로 보존), + /// id 비어있으면 신규 INSERT. col17 헤더가 "id"가 아닌 옛 파일은 태그번호(col1) 매칭으로 폴백. /// 빈 셀 → null 로 기록(엑셀에서 값을 비우면 DB에서도 삭제 = 라운드트립 교정 가능). /// 갱신 컬럼: 장비명·장비타입·라인번호·도면번호·신뢰도·상태·카테고리·Role·From·To·From_at·To_at·태그분류. /// 읽기전용(미반영): 추출일시(col8), Experion 태그(col9). @@ -592,11 +595,12 @@ public class PidExtractorService : IPidExtractorService using var package = new OfficeOpenXml.ExcelPackage(ms); - int sheets = 0, rowsRead = 0, rowsUpdated = 0; + int sheets = 0, rowsRead = 0, rowsUpdated = 0, rowsInserted = 0; var unmatched = new List(); - // TagNo → DB 레코드(들) 인덱스 + // 인덱스: id(안정 키, in-place 갱신) + TagNo(옛 파일 폴백) var all = await _dbContext.PidEquipment.ToListAsync(); + var byId = all.ToDictionary(e => e.Id); var byTag = all .Where(e => !string.IsNullOrWhiteSpace(e.TagNo)) .GroupBy(e => e.TagNo.Trim(), StringComparer.OrdinalIgnoreCase) @@ -615,6 +619,9 @@ public class PidExtractorService : IPidExtractorService if (!string.Equals(ws.Cells[1, 1].Text?.Trim(), "태그번호", StringComparison.Ordinal)) continue; + // col17 헤더가 "id" 면 안정 키 매칭, 아니면 옛 포맷(태그번호 매칭)으로 폴백 + bool hasIdCol = string.Equals(ws.Cells[1, 17].Text?.Trim(), "id", + StringComparison.Ordinal); sheets++; for (int r = 2; r <= ws.Dimension.End.Row; r++) @@ -623,12 +630,6 @@ public class PidExtractorService : IPidExtractorService if (tagNo == null) continue; rowsRead++; - if (!byTag.TryGetValue(tagNo, out var recs)) - { - if (unmatched.Count < 200) unmatched.Add(tagNo); - continue; - } - var equipName = Norm(ws, r, 2); var instType = Norm(ws, r, 3); var lineNo = Norm(ws, r, 4); @@ -657,7 +658,7 @@ public class PidExtractorService : IPidExtractorService _ => null }; - foreach (var e in recs) + void Apply(PidEquipment e) { e.EquipmentName = equipName; e.InstrumentType = instType; @@ -676,21 +677,50 @@ public class PidExtractorService : IPidExtractorService // 둘 다 비우면 잠금 해제 → 연결분석이 다시 도출 가능. e.ConnectionLocked = fromTag != null || toTag != null; e.UpdatedAt = DateTime.UtcNow; - rowsUpdated++; + } + + if (hasIdCol) + { + // id 있으면 그 행만 in-place UPDATE(다중경로 보존), 비어있으면 신규 INSERT + var idTxt = Norm(ws, r, 17); + if (idTxt != null && long.TryParse(idTxt, out var rid) && + byId.TryGetValue(rid, out var hit)) + { + Apply(hit); + rowsUpdated++; + } + else + { + var ne = new PidEquipment { TagNo = tagNo }; + Apply(ne); + _dbContext.PidEquipment.Add(ne); + rowsInserted++; + } + } + else + { + // 옛 포맷(id 컬럼 없음): TagNo 매칭 — 같은 TagNo 다중 행이면 전부 갱신 + if (!byTag.TryGetValue(tagNo, out var recs)) + { + if (unmatched.Count < 200) unmatched.Add(tagNo); + continue; + } + foreach (var e in recs) { Apply(e); rowsUpdated++; } } } } await _dbContext.SaveChangesAsync(); _logger.LogInformation( - "[PID Import] 시트 {Sheets} · 읽음 {Read} · 갱신 {Upd}레코드 · 미매칭 {Un}", - sheets, rowsRead, rowsUpdated, unmatched.Count); + "[PID Import] 시트 {Sheets} · 읽음 {Read} · 갱신 {Upd} · 신규 {Ins} · 미매칭 {Un}", + sheets, rowsRead, rowsUpdated, rowsInserted, unmatched.Count); return new PidImportResult { SheetsProcessed = sheets, RowsRead = rowsRead, RowsUpdated = rowsUpdated, + RowsInserted = rowsInserted, Unmatched = unmatched.Count, UnmatchedTags = unmatched }; diff --git a/src/Web/Controllers/PidController.cs b/src/Web/Controllers/PidController.cs index 9f4dd4c..3cc8fbe 100644 --- a/src/Web/Controllers/PidController.cs +++ b/src/Web/Controllers/PidController.cs @@ -336,8 +336,8 @@ public class PidController : ControllerBase await using var stream = file.OpenReadStream(); var result = await _pidExtractor.ImportFromExcelAsync(stream); _logger.LogInformation( - "[PID] 엑셀 import: {File} → {Upd}레코드 갱신, {Un}건 미매칭", - file.FileName, result.RowsUpdated, result.Unmatched); + "[PID] 엑셀 import: {File} → {Upd}건 갱신, {Ins}건 신규, {Un}건 미매칭", + file.FileName, result.RowsUpdated, result.RowsInserted, result.Unmatched); return Ok(result); } catch (Exception ex) diff --git a/구조설명-6-2차플랜트-byPBK.xlsx b/구조설명-6-2차플랜트-byPBK.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..4aa1014f36f168af5ddd0dcd2f4d1265bf9b2a87 GIT binary patch literal 21541 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