초기 커밋: SheetMe 서식생성기 (P0~P5 완료 상태)
레거시 서식생성기(VB.NET WinForms) 대체용 C#/.NET 10 WPF 디자이너. 기준선: 실DB 활성 디자인 1,271건 왕복 의미론 diff 0 / 예외 0, 단위 테스트 49/49. 이 커밋에 함께 포함된 자격증명 분리: - appsettings.json 을 __HOST__/__PASSWORD__ 플레이스홀더로 전환 - 실접속 정보는 appsettings.Development.json 으로 분리(.gitignore 제외, csproj Debug 조건부 복사라 Release 산출물에 실리지 않음) - ConfigLoader 를 환경변수 > Development > appsettings 순 레이어링으로 변경, 미치환 플레이스홀더는 '미설정'으로 간주 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,393 @@
|
||||
using System.IO;
|
||||
using SheetMe.Core.Serialization;
|
||||
using SheetMe.Data.Stores;
|
||||
using SheetMe.Designer.Services;
|
||||
|
||||
namespace SheetMe.Designer.Diagnostics;
|
||||
|
||||
/// <summary>
|
||||
/// 실DB 검증 진단.
|
||||
/// --db-smoke <리포트> [N] : 읽기 전용 — 활성 디자인 N건 일괄 Read→Write 의미론 diff 통계.
|
||||
/// --db-save-smoke <서식코드> <리포트> : 쓰기 — 무변경 재저장 후 재로드 비교 + E_SctMst 행 수 검증.
|
||||
/// </summary>
|
||||
public static class DbSmoke
|
||||
{
|
||||
#region Methods
|
||||
/// <summary>읽기 전용 일괄 왕복 스모크</summary>
|
||||
public static int RunReadSmoke(string reportPath, int maxSheets)
|
||||
{
|
||||
var lines = new List<string>();
|
||||
try
|
||||
{
|
||||
var config = ConfigLoader.Load();
|
||||
var store = new OracleLegacyFormStore(config.ConnectionString);
|
||||
var serializer = new LegacyXmlSerializer();
|
||||
|
||||
var sheets = store.ListSheets(null, max: 3000);
|
||||
lines.Add($"E_ShtMst 서식 {sheets.Count}건, 디자인 보유 {sheets.Count(s => s.HasDesign)}건");
|
||||
|
||||
var targets = sheets.Where(s => s.HasDesign).Take(maxSheets).ToList();
|
||||
var perfect = 0;
|
||||
var withWarnings = 0;
|
||||
var diffSheets = new List<string>();
|
||||
var failed = new List<string>();
|
||||
var placeholderTypes = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var sheet in targets)
|
||||
{
|
||||
try
|
||||
{
|
||||
var raw = store.LoadActiveDesignRaw(sheet.ShtCod);
|
||||
if (raw is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var document = serializer.Read(raw.Value.Xml);
|
||||
var rewritten = serializer.Write(document);
|
||||
var diffs = XmlSemanticDiff.Compare(raw.Value.Xml, rewritten, maxDiffs: 5);
|
||||
|
||||
foreach (var warning in document.Meta.ReadWarnings.Where(w => w.StartsWith("미지원")))
|
||||
{
|
||||
var type = warning[(warning.IndexOf('(') + 1)..].TrimEnd(')');
|
||||
placeholderTypes[type] = placeholderTypes.GetValueOrDefault(type) + 1;
|
||||
}
|
||||
|
||||
if (diffs.Count == 0)
|
||||
{
|
||||
perfect++;
|
||||
if (document.Meta.ReadWarnings.Count > 0)
|
||||
{
|
||||
withWarnings++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
diffSheets.Add($" {sheet.ShtCod} {sheet.Name}: {diffs[0]}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failed.Add($" {sheet.ShtCod} {sheet.Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
lines.Add($"왕복 검사 {targets.Count}건 → 의미론 diff 0: {perfect}건 (미지원 컨트롤 보존 포함 {withWarnings}건), diff 발생: {diffSheets.Count}건, 예외: {failed.Count}건");
|
||||
if (placeholderTypes.Count > 0)
|
||||
{
|
||||
lines.Add("미지원(자리표시 보존) 타입 분포: " + string.Join(", ",
|
||||
placeholderTypes.OrderByDescending(kv => kv.Value).Select(kv => $"{kv.Key}×{kv.Value}")));
|
||||
}
|
||||
if (diffSheets.Count > 0)
|
||||
{
|
||||
lines.Add("[diff 발생 서식]");
|
||||
lines.AddRange(diffSheets.Take(20));
|
||||
}
|
||||
if (failed.Count > 0)
|
||||
{
|
||||
lines.Add("[예외 서식]");
|
||||
lines.AddRange(failed.Take(20));
|
||||
}
|
||||
|
||||
File.WriteAllText(reportPath, string.Join(Environment.NewLine, lines));
|
||||
return diffSheets.Count == 0 && failed.Count == 0 ? 0 : 1;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lines.Add("실패: " + ex);
|
||||
File.WriteAllText(reportPath, string.Join(Environment.NewLine, lines));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>테이블 컬럼 스키마 조회(--db-columns) — NOT NULL/타입 확인(읽기 전용)</summary>
|
||||
public static int RunColumns(string tableName, string reportPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var config = ConfigLoader.Load();
|
||||
using var connection = new Oracle.ManagedDataAccess.Client.OracleConnection(config.ConnectionString);
|
||||
connection.Open();
|
||||
using var command = connection.CreateCommand();
|
||||
command.BindByName = true;
|
||||
command.CommandText =
|
||||
"SELECT COLUMN_NAME, DATA_TYPE, NVL(DATA_LENGTH,0), NULLABLE " +
|
||||
"FROM USER_TAB_COLUMNS WHERE TABLE_NAME = UPPER(:t) ORDER BY COLUMN_ID";
|
||||
command.Parameters.Add(new Oracle.ManagedDataAccess.Client.OracleParameter("t", tableName));
|
||||
var lines = new List<string>();
|
||||
using var reader = command.ExecuteReader();
|
||||
while (reader.Read())
|
||||
{
|
||||
lines.Add($"{reader.GetString(0),-16} {reader.GetString(1),-10} {reader.GetDecimal(2),5} " +
|
||||
$"{(reader.GetString(3) == "N" ? "NOT NULL" : "")}");
|
||||
}
|
||||
File.WriteAllText(reportPath, string.Join(Environment.NewLine, lines));
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
File.WriteAllText(reportPath, "실패: " + ex.Message);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>서식 마스터 행 주요 플래그 조회(--db-row) — 신규 등록 기본값 참조용(읽기 전용)</summary>
|
||||
public static int RunRow(string shtCod, string reportPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var config = ConfigLoader.Load();
|
||||
using var connection = new Oracle.ManagedDataAccess.Client.OracleConnection(config.ConnectionString);
|
||||
connection.Open();
|
||||
using var command = connection.CreateCommand();
|
||||
command.BindByName = true;
|
||||
command.CommandText =
|
||||
"SELECT NVL(ShtTyp,'-'), NVL(ShtUseYon,'-'), NVL(ShtUsrDesYon,'-'), NVL(ShtUseSdg,'-'), NVL(ShtUseSct,'-'), " +
|
||||
"NVL(ShtUseEmr,'-'), NVL(ShtEleSig,'-'), NVL(ShtEdtAth,'-'), NVL(ShtRedAth,'-'), NVL(ShtWrtTyp,'-'), " +
|
||||
"NVL(ShtEdtTyp,'-'), NVL(ShtViwTyp,'-'), NVL(ShtClsCod,'-'), NVL(ShtCneYon,'-'), NVL(ShtTraSto,'-') " +
|
||||
"FROM E_ShtMst WHERE ShtCod = :s";
|
||||
command.Parameters.Add(new Oracle.ManagedDataAccess.Client.OracleParameter("s", shtCod));
|
||||
using var reader = command.ExecuteReader();
|
||||
if (!reader.Read())
|
||||
{
|
||||
File.WriteAllText(reportPath, "행 없음");
|
||||
return 1;
|
||||
}
|
||||
var names = new[] { "ShtTyp", "ShtUseYon", "ShtUsrDesYon", "ShtUseSdg", "ShtUseSct", "ShtUseEmr",
|
||||
"ShtEleSig", "ShtEdtAth", "ShtRedAth", "ShtWrtTyp", "ShtEdtTyp", "ShtViwTyp", "ShtClsCod", "ShtCneYon", "ShtTraSto" };
|
||||
var lines = names.Select((n, i) => $"{n}={reader.GetString(i)}");
|
||||
File.WriteAllText(reportPath, string.Join(Environment.NewLine, lines));
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
File.WriteAllText(reportPath, "실패: " + ex.Message);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>테이블 샘플 행 조회(--db-sample) — 읽기 전용, 스키마 규약 확인용</summary>
|
||||
public static int RunSample(string tableName, string reportPath, int max = 12)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!System.Text.RegularExpressions.Regex.IsMatch(tableName, "^[A-Za-z0-9_]{1,30}$"))
|
||||
{
|
||||
throw new ArgumentException("유효하지 않은 테이블명");
|
||||
}
|
||||
var config = ConfigLoader.Load();
|
||||
using var connection = new Oracle.ManagedDataAccess.Client.OracleConnection(config.ConnectionString);
|
||||
connection.Open();
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = $"SELECT * FROM {tableName} WHERE ROWNUM <= {max}";
|
||||
var lines = new List<string>();
|
||||
using var reader = command.ExecuteReader();
|
||||
while (reader.Read())
|
||||
{
|
||||
var parts = new List<string>();
|
||||
for (var i = 0; i < reader.FieldCount; i++)
|
||||
{
|
||||
var value = reader.IsDBNull(i) ? "∅" : reader.GetValue(i).ToString() ?? "";
|
||||
if (value.Length > 30)
|
||||
{
|
||||
value = value[..30] + "…";
|
||||
}
|
||||
parts.Add($"{reader.GetName(i)}={value}");
|
||||
}
|
||||
lines.Add(string.Join(" | ", parts));
|
||||
}
|
||||
File.WriteAllText(reportPath, string.Join(Environment.NewLine, lines));
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
File.WriteAllText(reportPath, "실패: " + ex.Message);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Spread 디자인 조회(--db-spd) — 목록 또는 특정 건 원문 덤프(읽기 전용)</summary>
|
||||
public static int RunSpreadDump(string shtCodOrAll, string reportPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var config = ConfigLoader.Load();
|
||||
using var connection = new Oracle.ManagedDataAccess.Client.OracleConnection(config.ConnectionString);
|
||||
connection.Open();
|
||||
using var command = connection.CreateCommand();
|
||||
command.BindByName = true;
|
||||
if (shtCodOrAll == "*")
|
||||
{
|
||||
command.CommandText =
|
||||
"SELECT SpdShtCod, SpdName, LENGTH(SpdDesign), NVL(SpdDelYon,' ') FROM E_SpdMst ORDER BY SpdShtCod";
|
||||
var lines = new List<string>();
|
||||
using var reader = command.ExecuteReader();
|
||||
while (reader.Read())
|
||||
{
|
||||
lines.Add($"{reader.GetString(0)}\t{reader.GetString(1)}\t{reader.GetDecimal(2):N0}자\t{(reader.GetString(3) == "Y" ? "이력" : "활성")}");
|
||||
}
|
||||
File.WriteAllText(reportPath, string.Join(Environment.NewLine, lines));
|
||||
}
|
||||
else
|
||||
{
|
||||
command.CommandText =
|
||||
"SELECT SpdDesign FROM E_SpdMst WHERE TRIM(SpdShtCod) = :s AND NVL(SpdDelYon,' ') <> 'Y' AND ROWNUM = 1";
|
||||
command.Parameters.Add(new Oracle.ManagedDataAccess.Client.OracleParameter("s", shtCodOrAll));
|
||||
File.WriteAllText(reportPath, command.ExecuteScalar() as string ?? "(없음)");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
File.WriteAllText(reportPath, "실패: " + ex.Message);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>활성 디자인 XML 원문 덤프(--db-xml) — 읽기 전용</summary>
|
||||
public static int RunDesignXml(string shtCod, string reportPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var config = ConfigLoader.Load();
|
||||
var store = new OracleLegacyFormStore(config.ConnectionString);
|
||||
var raw = store.LoadActiveDesignRaw(shtCod);
|
||||
File.WriteAllText(reportPath,
|
||||
raw is null ? "(없음)" : $"SdgKey={raw.Value.SdgKey}{Environment.NewLine}{raw.Value.Xml}");
|
||||
return raw is null ? 1 : 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
File.WriteAllText(reportPath, "실패: " + ex.Message);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>서식 검색(--db-find) — 키워드 목록 출력</summary>
|
||||
public static int RunFind(string keyword, string reportPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var config = ConfigLoader.Load();
|
||||
var store = new OracleLegacyFormStore(config.ConnectionString);
|
||||
var sheets = store.ListSheets(keyword, max: 50);
|
||||
File.WriteAllText(reportPath, string.Join(Environment.NewLine,
|
||||
sheets.Select(s => $"{s.ShtCod}\t{(s.HasDesign ? "디자인O" : "-")}\t{s.Name}")));
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
File.WriteAllText(reportPath, "실패: " + ex.Message);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>상용구(E_SHTWRDMST) CRUD 왕복 스모크(--db-word-smoke) — 등록→조회→수정→순서→삭제(흔적 없음)</summary>
|
||||
public static int RunWordSmoke(string shtCod, string reportPath)
|
||||
{
|
||||
var lines = new List<string>();
|
||||
try
|
||||
{
|
||||
var config = ConfigLoader.Load();
|
||||
var store = new RecordWordStore(config.ConnectionString);
|
||||
|
||||
// 이전 스모크 잔여 정리
|
||||
foreach (var stale in store.List(shtCod).Where(w => w.Value.StartsWith("스모크 문구")))
|
||||
{
|
||||
store.Remove(stale.Key);
|
||||
}
|
||||
|
||||
var before = store.List(shtCod).Count;
|
||||
lines.Add($"[사전] {shtCod} 상용구 {before}건");
|
||||
|
||||
var key1 = store.Add(shtCod, "스모크 문구 1 (특이사항 없음)", Environment.UserName);
|
||||
var key2 = store.Add(shtCod, "스모크 문구 2 (경과 양호)", Environment.UserName);
|
||||
var afterAdd = store.List(shtCod);
|
||||
var okAdd = afterAdd.Count == before + 2
|
||||
&& afterAdd.Any(w => w.Key == key1) && afterAdd.Any(w => w.Key == key2);
|
||||
lines.Add($"[추가] 2건 → {afterAdd.Count}건 (키 {key1}, {key2}) : {(okAdd ? "통과" : "실패")}");
|
||||
|
||||
store.Update(key1, "스모크 문구 1 (수정됨)", Environment.UserName);
|
||||
var updatedValue = store.List(shtCod).FirstOrDefault(w => w.Key == key1)?.Value;
|
||||
var okUpdate = updatedValue == "스모크 문구 1 (수정됨)";
|
||||
lines.Add($"[수정] 값=\"{updatedValue}\" : {(okUpdate ? "통과" : "실패")}");
|
||||
|
||||
var reordered = afterAdd.Select(w => w.Key).ToList();
|
||||
reordered.Reverse();
|
||||
store.Reorder(reordered, Environment.UserName);
|
||||
var firstKey = store.List(shtCod).FirstOrDefault()?.Key;
|
||||
var okReorder = firstKey == reordered[0];
|
||||
lines.Add($"[순서] 첫 키={firstKey} 기대={reordered[0]} : {(okReorder ? "통과" : "실패")}");
|
||||
|
||||
store.Remove(key1);
|
||||
store.Remove(key2);
|
||||
var afterRemove = store.List(shtCod).Count;
|
||||
var okRemove = afterRemove == before;
|
||||
lines.Add($"[삭제] 원상복구 → {afterRemove}건 : {(okRemove ? "통과" : "실패")}");
|
||||
|
||||
var ok = okAdd && okUpdate && okReorder && okRemove;
|
||||
lines.Add(ok ? "결과: 통과" : "결과: 실패");
|
||||
File.WriteAllText(reportPath, string.Join(Environment.NewLine, lines));
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lines.Add("실패: " + ex);
|
||||
File.WriteAllText(reportPath, string.Join(Environment.NewLine, lines));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>무변경 재저장 스모크 — 저장 경로 전체(트랜잭션/버저닝/E_SctMst) 검증</summary>
|
||||
public static int RunSaveSmoke(string shtCod, string reportPath)
|
||||
{
|
||||
var lines = new List<string>();
|
||||
try
|
||||
{
|
||||
var config = ConfigLoader.Load();
|
||||
var store = new OracleLegacyFormStore(config.ConnectionString);
|
||||
var serializer = new LegacyXmlSerializer();
|
||||
|
||||
var before = store.LoadActiveDesignRaw(shtCod)
|
||||
?? throw new InvalidOperationException($"활성 디자인이 없습니다: {shtCod}");
|
||||
var cneYon = store.GetShtCneYon(shtCod);
|
||||
var versionsBefore = store.ListVersions(shtCod);
|
||||
lines.Add($"[사전] {shtCod} ShtCneYon='{cneYon}' 활성 SdgKey={before.SdgKey}, 버전 {versionsBefore.Count}건, XML {before.Xml.Length:N0}자");
|
||||
|
||||
// 무변경 재저장(문서 그대로) — 레거시 저장 시와 동일하게 새 버전 생성(또는 제자리 갱신)
|
||||
var document = serializer.Read(before.Xml);
|
||||
document.FormId = shtCod;
|
||||
var savedKey = store.SaveDesign(document, Environment.UserName);
|
||||
|
||||
var after = store.LoadActiveDesignRaw(shtCod)
|
||||
?? throw new InvalidOperationException("저장 후 활성 디자인 조회 실패");
|
||||
var versionsAfter = store.ListVersions(shtCod);
|
||||
lines.Add($"[저장] SdgKey {before.SdgKey} → {savedKey} ({(cneYon == "Y" ? "제자리 갱신" : "신규 버전")}), 버전 {versionsAfter.Count}건");
|
||||
|
||||
var diffs = XmlSemanticDiff.Compare(before.Xml, after.Xml, maxDiffs: 10);
|
||||
lines.Add($"[비교] 원본 vs 재저장 활성본 의미론 diff: {diffs.Count}건");
|
||||
lines.AddRange(diffs.Select(d => " " + d));
|
||||
|
||||
var sctCount = store.CountSctRows(savedKey);
|
||||
var walkerCount = SctMstXmlWalker.Walk(after.Xml).Count;
|
||||
lines.Add($"[E_SctMst] 저장본 행 {sctCount}건 / 워커 기대 {walkerCount}건 → {(sctCount == walkerCount ? "일치" : "불일치")}");
|
||||
|
||||
if (cneYon != "Y")
|
||||
{
|
||||
var oldVersion = versionsAfter.FirstOrDefault(v => v.SdgKey == before.SdgKey);
|
||||
lines.Add($"[이력] 이전 버전 SdgKey={before.SdgKey} SdgDelYon='Y' 처리: {(oldVersion?.Deleted == true ? "확인" : "미확인!")}");
|
||||
}
|
||||
|
||||
var ok = diffs.Count == 0 && sctCount == walkerCount;
|
||||
lines.Add(ok ? "결과: 통과 — 레거시 디자이너/EMR 미리보기로 열어 최종 육안 확인 권장" : "결과: 실패");
|
||||
File.WriteAllText(reportPath, string.Join(Environment.NewLine, lines));
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lines.Add("실패: " + ex);
|
||||
File.WriteAllText(reportPath, string.Join(Environment.NewLine, lines));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user