수정 경로를 실DB 로 돌려 본다 — 바꾸고, 확인하고, 원복한다
ModifyDesign 은 운영 테이블에 쓰는데 한 번도 실행해 본 적이 없었다.
저장 경로에는 이 검증이 있고(RunSaveSmoke) 수정 경로에만 없었다 —
"코드는 맞아 보이는데" 상태이고, 이 저장소에서 그건 여러 번 틀렸다.
--db-modify-smoke <서식코드> <리포트>. 단정 8건:
① 활성 SdgKey 가 바뀌지 않는다 — EMR 이 참조하는 키 유지가 이 동작의 존재 이유다
② 활성 행이 새 디자인으로 갱신된다 (문구를 실제로 바꿔서 본다 —
무변경 재저장으로는 "갱신됐다"를 증명할 수 없다)
③ 이력 행이 하나 생기고, SdgDelYon='Y' 이고, <b>옛</b> XML 을 담는다
(저장과 방향이 반대다. 뒤집혔으면 ③-c 가 잡는다)
④ 감사 컬럼이 활성 행에 기록된다 — 레거시는 이걸 이력 행에 썼다(버그, 미복제)
⑤ E_SctMst 를 건드리지 않는다
+ 원복 2건
S999 실측: SdgKey 52451 유지, 버전 511 → 512 → 511, 감사 202608120855 → 202608180917.
<b>DB 를 원상태로 되돌린다.</b> 활성 행의 XML·감사값을 원문으로 UPDATE 하고
이 진단이 만든 이력 행을 지운다. DELETE 에 SdgDelYon='Y' 를 함께 걸어
<b>SdgKey 를 잘못 짚어도 활성 행은 어떤 경우에도 지워지지 않게</b> 했다.
<b>⑤ 가 공허하게 통과하고 있었다.</b> S999·P062 둘 다 E_SctMst 가 0건이라
"0 → 0" 을 비교했는데 PASS 로 찍혔다. 읽는 사람은 검증됐다고 믿는다.
행이 0건이면 SKIP 으로 적고 "행이 있는 서식으로 다시 돌려야 한다"를 남긴다.
이번 세션에만 같은 함정에 네 번 걸렸다 — 대조군 없는 0 은 증거가 아니다.
<b>남은 것</b>: ⑤ 를 실제로 재려면 E_SctMst 행이 있는 서식이 필요하다.
테스트 접속에서 아직 못 찾았다.
게이트: --db-modify-smoke 8/8(⑤ SKIP), 테스트 298/298, --edit-smoke 0실패,
--db-smoke 1271건 diff 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ae2f8f2f61
commit
3af7b3a071
@@ -214,6 +214,17 @@ public sealed class OracleLegacyFormStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>특정 버전(SdgKey) 디자인 로드 — 이력 열람/복원용. 없으면 null</summary>
|
/// <summary>특정 버전(SdgKey) 디자인 로드 — 이력 열람/복원용. 없으면 null</summary>
|
||||||
|
/// <summary>특정 버전의 원문 XML — 이력 행이 옛 내용을 담았는지 확인하는 데 쓴다</summary>
|
||||||
|
public string? LoadDesignRawByKey(string shtCod, decimal sdgKey)
|
||||||
|
{
|
||||||
|
using var connection = Open();
|
||||||
|
using var command = connection.CreateCommand();
|
||||||
|
command.BindByName = true;
|
||||||
|
command.CommandText = "SELECT SdgDesign FROM E_SdgMst WHERE SdgKey = :k";
|
||||||
|
command.Parameters.Add(new OracleParameter("k", sdgKey));
|
||||||
|
return command.ExecuteScalar() as string;
|
||||||
|
}
|
||||||
|
|
||||||
public FormDocument? LoadDesignByKey(string shtCod, decimal sdgKey)
|
public FormDocument? LoadDesignByKey(string shtCod, decimal sdgKey)
|
||||||
{
|
{
|
||||||
using var connection = Open();
|
using var connection = Open();
|
||||||
|
|||||||
@@ -158,6 +158,11 @@ public partial class App : Application
|
|||||||
return Diagnostics.DbSmoke.RunReadSmoke(args[1], max);
|
return Diagnostics.DbSmoke.RunReadSmoke(args[1], max);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args.Length >= 3 && args[0] == "--db-modify-smoke")
|
||||||
|
{
|
||||||
|
return Diagnostics.DbSmoke.RunModifySmoke(args[1], args[2]);
|
||||||
|
}
|
||||||
|
|
||||||
if (args.Length >= 3 && args[0] == "--db-save-smoke")
|
if (args.Length >= 3 && args[0] == "--db-save-smoke")
|
||||||
{
|
{
|
||||||
return Diagnostics.DbSmoke.RunSaveSmoke(args[1], args[2]);
|
return Diagnostics.DbSmoke.RunSaveSmoke(args[1], args[2]);
|
||||||
|
|||||||
@@ -1798,6 +1798,202 @@ public static class DbSmoke
|
|||||||
private static string Trim30(string value) => value.Length > 30 ? value[..30] + "…" : value;
|
private static string Trim30(string value) => value.Length > 30 ? value[..30] + "…" : value;
|
||||||
|
|
||||||
/// <summary>무변경 재저장 스모크 — 저장 경로 전체(트랜잭션/버저닝/E_SctMst) 검증</summary>
|
/// <summary>무변경 재저장 스모크 — 저장 경로 전체(트랜잭션/버저닝/E_SctMst) 검증</summary>
|
||||||
|
/// <summary>리포트를 파일과 stderr 에 남기고 종료코드를 돌려준다</summary>
|
||||||
|
private static int Finish(List<string> lines, string reportPath, int code)
|
||||||
|
{
|
||||||
|
var text = string.Join(Environment.NewLine, lines);
|
||||||
|
File.WriteAllText(reportPath, text);
|
||||||
|
Console.Error.WriteLine(text);
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 제자리 정정(TK_MODIFY) 왕복 검증 — <c>--db-modify-smoke <서식코드> <리포트></c>.
|
||||||
|
///
|
||||||
|
/// <b>왜 필요한가.</b> ModifyDesign 은 운영 테이블에 쓰는데 한 번도 실행해 본 적이 없다.
|
||||||
|
/// 저장 경로에는 이런 검증이 있고(RunSaveSmoke) 수정 경로에만 없었다 —
|
||||||
|
/// "코드는 맞아 보이는데" 상태이고, 이 저장소에서 그건 여러 번 틀렸다.
|
||||||
|
///
|
||||||
|
/// <b>무엇을 단정하는가.</b> 수정은 저장과 <b>이력 방향이 반대</b>다.
|
||||||
|
/// ① 활성 SdgKey 가 <b>바뀌지 않는다</b>(EMR 이 참조하는 키가 유지되는 것이 이 동작의 존재 이유다)
|
||||||
|
/// ② 활성 행의 디자인이 <b>새 XML</b> 로 바뀐다
|
||||||
|
/// ③ 옛 XML 을 담은 이력 행이 <b>하나</b> 생긴다(SdgDelYon='Y')
|
||||||
|
/// ④ 활성 행의 감사 컬럼이 <b>지금 사용자·시각</b>이다 — 레거시는 이걸 이력 행에 썼다(버그, 미복제)
|
||||||
|
/// ⑤ <c>E_SctMst</c> 행 수가 <b>변하지 않는다</b>(수정은 건드리지 않는다)
|
||||||
|
///
|
||||||
|
/// <b>DB 를 원상태로 되돌린다.</b> 활성 행의 XML·감사값을 원문으로 UPDATE 하고,
|
||||||
|
/// 이 진단이 만든 이력 행을 SdgKey 로 지운다. 지우는 것은 <b>방금 만든 그 행 하나</b>뿐이고
|
||||||
|
/// SdgDelYon='Y' 조건을 함께 걸어 활성 행은 어떤 경우에도 지워지지 않게 한다.
|
||||||
|
/// </summary>
|
||||||
|
public static int RunModifySmoke(string shtCod, string reportPath)
|
||||||
|
{
|
||||||
|
var lines = new List<string>();
|
||||||
|
var ok = true;
|
||||||
|
void Check(string name, bool pass, string? detail = null)
|
||||||
|
{
|
||||||
|
ok &= pass;
|
||||||
|
lines.Add($"{(pass ? "PASS" : "FAIL")} {name}{(pass || detail is null ? string.Empty : " — " + detail)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var config = ConfigService.Current;
|
||||||
|
var store = new OracleLegacyFormStore(config.ConnectionString);
|
||||||
|
var serializer = new LegacyXmlSerializer();
|
||||||
|
|
||||||
|
var before = store.LoadActiveDesignRaw(shtCod)
|
||||||
|
?? throw new InvalidOperationException($"활성 디자인이 없습니다: {shtCod}");
|
||||||
|
var versionsBefore = store.ListVersions(shtCod);
|
||||||
|
var sctBefore = store.CountSctRows(before.SdgKey);
|
||||||
|
var auditBefore = ReadAudit(config.ConnectionString, before.SdgKey);
|
||||||
|
lines.Add($"[사전] {shtCod} 활성 SdgKey={before.SdgKey}, 버전 {versionsBefore.Count}건,"
|
||||||
|
+ $" E_SctMst {sctBefore}건, XML {before.Xml.Length:N0}자, 감사=({auditBefore.Uid},{auditBefore.Stamp})");
|
||||||
|
|
||||||
|
// 문구를 실제로 바꾼다 — 무변경 재저장으로는 "디자인이 갱신됐다"를 증명할 수 없다
|
||||||
|
var document = serializer.Read(before.Xml);
|
||||||
|
document.FormId = shtCod;
|
||||||
|
var marker = "수정검증-" + before.SdgKey;
|
||||||
|
var target = FirstTextControl(document);
|
||||||
|
if (target is null)
|
||||||
|
{
|
||||||
|
lines.Add("글자를 담은 컨트롤이 없어 갱신을 증명할 수 없습니다 — 다른 서식으로 시도하세요.");
|
||||||
|
return Finish(lines, reportPath, 2);
|
||||||
|
}
|
||||||
|
var originalText = target.Props.GetText("Text") ?? string.Empty;
|
||||||
|
target.Props.SetText("Text", marker);
|
||||||
|
|
||||||
|
var returnedKey = store.ModifyDesign(document, UserSession.AuditUid);
|
||||||
|
|
||||||
|
var after = store.LoadActiveDesignRaw(shtCod)
|
||||||
|
?? throw new InvalidOperationException("수정 후 활성 디자인 조회 실패");
|
||||||
|
var versionsAfter = store.ListVersions(shtCod);
|
||||||
|
var auditAfter = ReadAudit(config.ConnectionString, before.SdgKey);
|
||||||
|
lines.Add($"[수정] 반환 SdgKey={returnedKey}, 활성 SdgKey={after.SdgKey},"
|
||||||
|
+ $" 버전 {versionsBefore.Count} → {versionsAfter.Count}건, 감사=({auditAfter.Uid},{auditAfter.Stamp})");
|
||||||
|
|
||||||
|
Check("① 활성 SdgKey 가 바뀌지 않는다",
|
||||||
|
after.SdgKey == before.SdgKey && returnedKey == before.SdgKey,
|
||||||
|
$"{before.SdgKey} → {after.SdgKey} (반환 {returnedKey})");
|
||||||
|
Check("② 활성 행이 새 디자인으로 갱신된다", after.Xml.Contains(marker, StringComparison.Ordinal));
|
||||||
|
Check("③ 이력 행이 하나 생긴다", versionsAfter.Count == versionsBefore.Count + 1,
|
||||||
|
$"{versionsBefore.Count} → {versionsAfter.Count}");
|
||||||
|
|
||||||
|
var added = versionsAfter.Select(v => v.SdgKey).Except(versionsBefore.Select(v => v.SdgKey)).ToList();
|
||||||
|
var historyKey = added.Count == 1 ? added[0] : 0m;
|
||||||
|
Check("③-b 새 행이 이력(SdgDelYon='Y')이다",
|
||||||
|
historyKey > 0 && versionsAfter.First(v => v.SdgKey == historyKey).Deleted);
|
||||||
|
if (historyKey > 0)
|
||||||
|
{
|
||||||
|
var historyXml = store.LoadDesignRawByKey(shtCod, historyKey);
|
||||||
|
Check("③-c 이력 행이 <b>옛</b> XML 을 담는다",
|
||||||
|
historyXml is not null && !historyXml.Contains(marker, StringComparison.Ordinal),
|
||||||
|
"이력 행에 새 내용이 들어갔다 — 방향이 뒤집혔다");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 레거시는 활성 행에 감사값을 안 쓰고 이력 행에 현재 사용자·시각을 썼다. 그 버그를 복제하지 않았는지 본다.
|
||||||
|
Check("④ 감사 컬럼이 활성 행에 기록된다",
|
||||||
|
auditAfter.Uid == UserSession.AuditUid && auditAfter.Stamp != auditBefore.Stamp,
|
||||||
|
$"활성 행 감사=({auditAfter.Uid},{auditAfter.Stamp}), 기대 uid={UserSession.AuditUid}");
|
||||||
|
|
||||||
|
// 행이 0건이면 "0 → 0" 을 비교하는 것이라 아무것도 증명하지 못한다.
|
||||||
|
// 통과로 적으면 읽는 사람이 검증됐다고 믿는다 — 판정 불가라고 밝힌다.
|
||||||
|
var sctAfter = store.CountSctRows(before.SdgKey);
|
||||||
|
if (sctBefore == 0)
|
||||||
|
{
|
||||||
|
lines.Add("SKIP ⑤ E_SctMst 를 건드리지 않는다 — 이 서식은 행이 0건이라 판정 불가"
|
||||||
|
+ " (행이 있는 서식으로 다시 돌려야 한다)");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Check("⑤ E_SctMst 를 건드리지 않는다", sctAfter == sctBefore, $"{sctBefore} → {sctAfter}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 원복 — 검사 결과와 무관하게 DB 를 원상태로 되돌린다
|
||||||
|
target.Props.SetText("Text", originalText);
|
||||||
|
RestoreDesign(config.ConnectionString, before.SdgKey, before.Xml, auditBefore.Uid, auditBefore.Stamp);
|
||||||
|
var removed = historyKey > 0 ? DeleteHistoryRow(config.ConnectionString, historyKey) : 0;
|
||||||
|
var restored = store.LoadActiveDesignRaw(shtCod);
|
||||||
|
var versionsRestored = store.ListVersions(shtCod);
|
||||||
|
Check("원복: 활성 XML 이 원문으로 돌아온다",
|
||||||
|
restored is not null && restored.Value.Xml.Length == before.Xml.Length
|
||||||
|
&& !restored.Value.Xml.Contains(marker, StringComparison.Ordinal));
|
||||||
|
Check("원복: 이력 행이 지워진다", removed == 1 && versionsRestored.Count == versionsBefore.Count,
|
||||||
|
$"삭제 {removed}행, 버전 {versionsRestored.Count} (기대 {versionsBefore.Count})");
|
||||||
|
|
||||||
|
lines.Add(string.Empty);
|
||||||
|
lines.Add($"결과: {(ok ? "전건 통과" : "실패 있음")}");
|
||||||
|
return Finish(lines, reportPath, ok ? 0 : 1);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
lines.Add($"EXCEPTION {ex.GetType().Name}: {ex.Message}");
|
||||||
|
return Finish(lines, reportPath, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>글자를 담은 첫 컨트롤 — 갱신을 증명할 대상</summary>
|
||||||
|
private static SheetMe.Core.Models.ControlElement? FirstTextControl(SheetMe.Core.Models.FormDocument document)
|
||||||
|
{
|
||||||
|
foreach (var page in document.Pages)
|
||||||
|
{
|
||||||
|
foreach (var control in page.Controls)
|
||||||
|
{
|
||||||
|
if ((control.Props.GetText("Text") ?? string.Empty).Length > 0)
|
||||||
|
{
|
||||||
|
return control;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (string Uid, string Stamp) ReadAudit(string connectionString, decimal sdgKey)
|
||||||
|
{
|
||||||
|
using var connection = new Oracle.ManagedDataAccess.Client.OracleConnection(connectionString);
|
||||||
|
connection.Open();
|
||||||
|
using var command = connection.CreateCommand();
|
||||||
|
command.BindByName = true;
|
||||||
|
command.CommandText =
|
||||||
|
"SELECT NVL(SdgUidCod,' '), NVL(SdgUpdDtm,' ') FROM E_SdgMst WHERE SdgKey = :k";
|
||||||
|
command.Parameters.Add(new Oracle.ManagedDataAccess.Client.OracleParameter("k", sdgKey));
|
||||||
|
using var reader = command.ExecuteReader();
|
||||||
|
return reader.Read()
|
||||||
|
? (reader.GetString(0).Trim(), reader.GetString(1).Trim())
|
||||||
|
: (string.Empty, string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>원복 전용 — 활성 행을 원문 XML·원 감사값으로 되돌린다</summary>
|
||||||
|
private static void RestoreDesign(string connectionString, decimal sdgKey, string xml, string uid, string stamp)
|
||||||
|
{
|
||||||
|
using var connection = new Oracle.ManagedDataAccess.Client.OracleConnection(connectionString);
|
||||||
|
connection.Open();
|
||||||
|
using var command = connection.CreateCommand();
|
||||||
|
command.BindByName = true;
|
||||||
|
command.CommandText =
|
||||||
|
"UPDATE E_SdgMst SET SdgDesign = :d, SdgUidCod = :u, SdgUpdDtm = :t WHERE SdgKey = :k";
|
||||||
|
command.Parameters.Add(new Oracle.ManagedDataAccess.Client.OracleParameter("d",
|
||||||
|
Oracle.ManagedDataAccess.Client.OracleDbType.Clob) { Value = xml });
|
||||||
|
command.Parameters.Add(new Oracle.ManagedDataAccess.Client.OracleParameter("u", uid));
|
||||||
|
command.Parameters.Add(new Oracle.ManagedDataAccess.Client.OracleParameter("t", stamp));
|
||||||
|
command.Parameters.Add(new Oracle.ManagedDataAccess.Client.OracleParameter("k", sdgKey));
|
||||||
|
command.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 이 진단이 만든 이력 행 하나를 지운다.
|
||||||
|
/// <c>SdgDelYon='Y'</c> 를 조건에 함께 건다 — SdgKey 를 잘못 짚어도 <b>활성 행은 지워지지 않는다</b>.
|
||||||
|
/// </summary>
|
||||||
|
private static int DeleteHistoryRow(string connectionString, decimal sdgKey)
|
||||||
|
{
|
||||||
|
using var connection = new Oracle.ManagedDataAccess.Client.OracleConnection(connectionString);
|
||||||
|
connection.Open();
|
||||||
|
using var command = connection.CreateCommand();
|
||||||
|
command.BindByName = true;
|
||||||
|
command.CommandText = "DELETE FROM E_SdgMst WHERE SdgKey = :k AND SdgDelYon = 'Y'";
|
||||||
|
command.Parameters.Add(new Oracle.ManagedDataAccess.Client.OracleParameter("k", sdgKey));
|
||||||
|
return command.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
|
||||||
public static int RunSaveSmoke(string shtCod, string reportPath)
|
public static int RunSaveSmoke(string shtCod, string reportPath)
|
||||||
{
|
{
|
||||||
var lines = new List<string>();
|
var lines = new List<string>();
|
||||||
|
|||||||
Reference in New Issue
Block a user