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 0000000..4aa1014 Binary files /dev/null and b/구조설명-6-2차플랜트-byPBK.xlsx differ