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 SheetsProcessed { get; init; }
|
||||||
public int RowsRead { get; init; }
|
public int RowsRead { get; init; }
|
||||||
public int RowsUpdated { get; init; }
|
public int RowsUpdated { get; init; }
|
||||||
|
public int RowsInserted { get; init; }
|
||||||
public int Unmatched { get; init; }
|
public int Unmatched { get; init; }
|
||||||
public List<string> UnmatchedTags { get; init; } = new();
|
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, 14].Value = "From_at";
|
||||||
worksheet.Cells[1, 15].Value = "To_at";
|
worksheet.Cells[1, 15].Value = "To_at";
|
||||||
worksheet.Cells[1, 16].Value = "태그분류";
|
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.Font.Bold = true;
|
||||||
headerRange.Style.Fill.PatternType = OfficeOpenXml.Style.ExcelFillStyle.Solid;
|
headerRange.Style.Fill.PatternType = OfficeOpenXml.Style.ExcelFillStyle.Solid;
|
||||||
headerRange.Style.Fill.BackgroundColor.SetColor(System.Drawing.Color.LightGray);
|
headerRange.Style.Fill.BackgroundColor.SetColor(System.Drawing.Color.LightGray);
|
||||||
@@ -564,6 +565,7 @@ public class PidExtractorService : IPidExtractorService
|
|||||||
PidEquipment.TagClassField => "현장",
|
PidEquipment.TagClassField => "현장",
|
||||||
_ => ""
|
_ => ""
|
||||||
};
|
};
|
||||||
|
worksheet.Cells[row, 17].Value = item.Id; // 안정 키(라운드트립 매칭용)
|
||||||
row++;
|
row++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,8 +577,9 @@ public class PidExtractorService : IPidExtractorService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 편집 엑셀(ExportToExcelAsync 포맷, 16컬럼) → pid_equipment UPSERT.
|
/// 편집 엑셀(ExportToExcelAsync 포맷, 17컬럼) → pid_equipment UPSERT.
|
||||||
/// 매칭 키 = 태그번호(col1, 대소문자 무시·trim). 같은 TagNo 다중 행이면 전부 갱신.
|
/// 매칭 키 = id(col17, 안정 키). id 있으면 그 행만 in-place UPDATE(다중경로 보존),
|
||||||
|
/// id 비어있으면 신규 INSERT. col17 헤더가 "id"가 아닌 옛 파일은 태그번호(col1) 매칭으로 폴백.
|
||||||
/// 빈 셀 → null 로 기록(엑셀에서 값을 비우면 DB에서도 삭제 = 라운드트립 교정 가능).
|
/// 빈 셀 → null 로 기록(엑셀에서 값을 비우면 DB에서도 삭제 = 라운드트립 교정 가능).
|
||||||
/// 갱신 컬럼: 장비명·장비타입·라인번호·도면번호·신뢰도·상태·카테고리·Role·From·To·From_at·To_at·태그분류.
|
/// 갱신 컬럼: 장비명·장비타입·라인번호·도면번호·신뢰도·상태·카테고리·Role·From·To·From_at·To_at·태그분류.
|
||||||
/// 읽기전용(미반영): 추출일시(col8), Experion 태그(col9).
|
/// 읽기전용(미반영): 추출일시(col8), Experion 태그(col9).
|
||||||
@@ -592,11 +595,12 @@ public class PidExtractorService : IPidExtractorService
|
|||||||
|
|
||||||
using var package = new OfficeOpenXml.ExcelPackage(ms);
|
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>();
|
var unmatched = new List<string>();
|
||||||
|
|
||||||
// TagNo → DB 레코드(들) 인덱스
|
// 인덱스: id(안정 키, in-place 갱신) + TagNo(옛 파일 폴백)
|
||||||
var all = await _dbContext.PidEquipment.ToListAsync();
|
var all = await _dbContext.PidEquipment.ToListAsync();
|
||||||
|
var byId = all.ToDictionary(e => e.Id);
|
||||||
var byTag = all
|
var byTag = all
|
||||||
.Where(e => !string.IsNullOrWhiteSpace(e.TagNo))
|
.Where(e => !string.IsNullOrWhiteSpace(e.TagNo))
|
||||||
.GroupBy(e => e.TagNo.Trim(), StringComparer.OrdinalIgnoreCase)
|
.GroupBy(e => e.TagNo.Trim(), StringComparer.OrdinalIgnoreCase)
|
||||||
@@ -615,6 +619,9 @@ public class PidExtractorService : IPidExtractorService
|
|||||||
if (!string.Equals(ws.Cells[1, 1].Text?.Trim(), "태그번호",
|
if (!string.Equals(ws.Cells[1, 1].Text?.Trim(), "태그번호",
|
||||||
StringComparison.Ordinal))
|
StringComparison.Ordinal))
|
||||||
continue;
|
continue;
|
||||||
|
// col17 헤더가 "id" 면 안정 키 매칭, 아니면 옛 포맷(태그번호 매칭)으로 폴백
|
||||||
|
bool hasIdCol = string.Equals(ws.Cells[1, 17].Text?.Trim(), "id",
|
||||||
|
StringComparison.Ordinal);
|
||||||
sheets++;
|
sheets++;
|
||||||
|
|
||||||
for (int r = 2; r <= ws.Dimension.End.Row; r++)
|
for (int r = 2; r <= ws.Dimension.End.Row; r++)
|
||||||
@@ -623,12 +630,6 @@ public class PidExtractorService : IPidExtractorService
|
|||||||
if (tagNo == null) continue;
|
if (tagNo == null) continue;
|
||||||
rowsRead++;
|
rowsRead++;
|
||||||
|
|
||||||
if (!byTag.TryGetValue(tagNo, out var recs))
|
|
||||||
{
|
|
||||||
if (unmatched.Count < 200) unmatched.Add(tagNo);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var equipName = Norm(ws, r, 2);
|
var equipName = Norm(ws, r, 2);
|
||||||
var instType = Norm(ws, r, 3);
|
var instType = Norm(ws, r, 3);
|
||||||
var lineNo = Norm(ws, r, 4);
|
var lineNo = Norm(ws, r, 4);
|
||||||
@@ -657,7 +658,7 @@ public class PidExtractorService : IPidExtractorService
|
|||||||
_ => null
|
_ => null
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var e in recs)
|
void Apply(PidEquipment e)
|
||||||
{
|
{
|
||||||
e.EquipmentName = equipName;
|
e.EquipmentName = equipName;
|
||||||
e.InstrumentType = instType;
|
e.InstrumentType = instType;
|
||||||
@@ -676,21 +677,50 @@ public class PidExtractorService : IPidExtractorService
|
|||||||
// 둘 다 비우면 잠금 해제 → 연결분석이 다시 도출 가능.
|
// 둘 다 비우면 잠금 해제 → 연결분석이 다시 도출 가능.
|
||||||
e.ConnectionLocked = fromTag != null || toTag != null;
|
e.ConnectionLocked = fromTag != null || toTag != null;
|
||||||
e.UpdatedAt = DateTime.UtcNow;
|
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();
|
await _dbContext.SaveChangesAsync();
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"[PID Import] 시트 {Sheets} · 읽음 {Read} · 갱신 {Upd}레코드 · 미매칭 {Un}",
|
"[PID Import] 시트 {Sheets} · 읽음 {Read} · 갱신 {Upd} · 신규 {Ins} · 미매칭 {Un}",
|
||||||
sheets, rowsRead, rowsUpdated, unmatched.Count);
|
sheets, rowsRead, rowsUpdated, rowsInserted, unmatched.Count);
|
||||||
|
|
||||||
return new PidImportResult
|
return new PidImportResult
|
||||||
{
|
{
|
||||||
SheetsProcessed = sheets,
|
SheetsProcessed = sheets,
|
||||||
RowsRead = rowsRead,
|
RowsRead = rowsRead,
|
||||||
RowsUpdated = rowsUpdated,
|
RowsUpdated = rowsUpdated,
|
||||||
|
RowsInserted = rowsInserted,
|
||||||
Unmatched = unmatched.Count,
|
Unmatched = unmatched.Count,
|
||||||
UnmatchedTags = unmatched
|
UnmatchedTags = unmatched
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -336,8 +336,8 @@ public class PidController : ControllerBase
|
|||||||
await using var stream = file.OpenReadStream();
|
await using var stream = file.OpenReadStream();
|
||||||
var result = await _pidExtractor.ImportFromExcelAsync(stream);
|
var result = await _pidExtractor.ImportFromExcelAsync(stream);
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"[PID] 엑셀 import: {File} → {Upd}레코드 갱신, {Un}건 미매칭",
|
"[PID] 엑셀 import: {File} → {Upd}건 갱신, {Ins}건 신규, {Un}건 미매칭",
|
||||||
file.FileName, result.RowsUpdated, result.Unmatched);
|
file.FileName, result.RowsUpdated, result.RowsInserted, result.Unmatched);
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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