feat: P&ID 연결 엑셀 라운드트립 — id 안정 키 + 운전자 문서규칙
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<string> UnmatchedTags { get; init; } = new();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 편집 엑셀(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<string>();
|
||||
|
||||
// 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
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
BIN
구조설명-6-2차플랜트-byPBK.xlsx
Normal file
BIN
구조설명-6-2차플랜트-byPBK.xlsx
Normal file
Binary file not shown.
Reference in New Issue
Block a user