From ae2f8f2f619f7d64dfaf227175a42a17fe59fdd5 Mon Sep 17 00:00:00 2001 From: Msystech Date: Mon, 17 Aug 2026 21:17:55 +0900 Subject: [PATCH] =?UTF-8?q?'=EC=88=98=EC=A0=95'=EC=9D=84=20=EB=90=98?= =?UTF-8?q?=EC=82=B4=EB=A6=B0=EB=8B=A4=20=E2=80=94=20=EC=A7=80=EA=B8=88=20?= =?UTF-8?q?=EC=93=B0=EC=9D=B4=EB=8A=94=20=EB=B2=84=EC=A0=84=EC=9D=84=20?= =?UTF-8?q?=EC=A0=9C=EC=9E=90=EB=A6=AC=EC=97=90=EC=84=9C=20=EA=B3=A0?= =?UTF-8?q?=EC=B9=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 레거시 TK_MODIFY('기록지 수정'). SheetMe 에는 저장만 있었고 이것이 없었다. DbSmoke.cs:1072 가 그 부재를 이미 적어 뒀다 — 실측 ShtCneYon='Y' 40건, 현역 19건. 먼저 내 앞선 설명을 고친다. 나는 수정이 ShtCneYon 제자리 갱신 경로라고 했는데 틀렸다. 수정 경로는 ShtCneYon 을 읽지도 않는다. 그 컬럼은 저장이 버저닝이냐 제자리냐만 가른다. 저장과 수정은 이력 방향이 반대다. 저장: 옛 행을 SdgDelYon='Y' 로 내리고 새 행이 활성이 된다(SdgKey 바뀜) 수정: 활성 행을 제자리에서 갱신하고 옛 XML 을 담은 새 행이 이력이 된다(SdgKey 유지) SdgKey 가 유지되므로 EMR 이 참조하던 키가 그대로다 — 그게 이 동작의 존재 이유다. E_SctMst 를 건드리지 않는다(레거시 :170-174 주석 처리). 그것이 "글자만 수정 가능"이라는 경고문의 기술적 근거다 — 기존 매핑 행은 SdgKey 가 그대로라 유효하지만 새 컨트롤은 행이 안 생긴다. 레거시 버그 둘은 복제하지 않는다. ① 감사 컬럼이 뒤집혀 있었다 — 활성 행에는 아무것도 안 쓰고 옛 디자인을 담은 이력 행이 현재 사용자·시각을 받았다(:162-163). 이력 패널이 "누가 언제 이 버전을 만들었는가"를 거꾸로 보여 준다. 활성 행에 기록하고 이력 행은 원래 값을 지킨다. ② Rows(0) 을 개수 검사보다 먼저 읽어 활성 행이 없으면 IndexOutOfRange 였다(:150). 먼저 확인하고 "수정 대신 저장을 쓰세요"로 안내한다. 새 컨트롤은 막는다. 레거시는 산문으로만 경고하고 막지 않았다 — 경고문을 읽지 않으면 그대로 번진다. SctMstXmlWalker 로 문서의 컨트롤 이름을 뽑아 활성 SdgKey 의 E_SctMst 행과 대조한다. 판정 근거를 저장이 행을 만드는 규칙과 같은 것으로 통일해서, "행이 원래 안 생기는 종류"를 새 컨트롤로 오인하지 않는다. 권한 게이트는 유지한다. 레거시가 이 버튼만 전산실에 묶어 둔 이유가 위와 같다 (frmSheetDesigner.vb:1029-1060, 주석 '전산실만 사용가능'). 저장 버튼에는 그런 분기가 없다. 규칙을 Core 의 순수 함수로 옮겨 표로 고정했다 — 병원·부서 조합이 세 갈래라 화면에서 즉석 판정하면 어느 갈래가 왜 막혔는지 확인할 방법이 없다. 막힌 이유도 갈래마다 다르게 말한다("권한 없음"만으로는 누구에게 물어야 할지 모른다). HspStrDte 는 세션에 없다. 레거시는 그 값으로 "2025-05-01 이후 개원 병원은 사내 계정만"을 가른다. 빈 문자열로 두면 그 갈래가 안 걸리고 부서 규칙(EDPS)으로 떨어진다 — 레거시의 다수 경로와 같다. 없는 값을 지어내 더 조이지 않고, 이 사실을 코드와 테스트에 적어 뒀다. 게이트: 테스트 298/298(신규 7), --edit-smoke 0실패, --dialog-shots FAIL 0 대조군 4/4, --modal-check 0실패, --cleartype 11/11, --maxrect 0실패, --scale-budget 5/5, 빌드 경고 0, --db-smoke 1271건 diff 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일. 실DB 수정 경로는 아직 안 돌려 봤다 — 운영 테이블에 쓰는 동작이라 --db-* 진단으로 왕복 검증을 붙이는 것이 다음 일이다. Co-Authored-By: Claude Opus 5 --- src/SheetMe.Core/Catalog/ModifyPolicy.cs | 66 +++++++++ .../Stores/OracleLegacyFormStore.cs | 128 ++++++++++++++++++ .../DataBusiness/FormDesignDataBusiness.cs | 35 ++++- .../ViewModels/MainViewModel.cs | 90 ++++++++++++ src/SheetMe.Designer/Views/MainView.xaml | 13 +- tests/SheetMe.Core.Tests/ModifyPolicyTests.cs | 82 +++++++++++ 6 files changed, 412 insertions(+), 2 deletions(-) create mode 100644 src/SheetMe.Core/Catalog/ModifyPolicy.cs create mode 100644 tests/SheetMe.Core.Tests/ModifyPolicyTests.cs diff --git a/src/SheetMe.Core/Catalog/ModifyPolicy.cs b/src/SheetMe.Core/Catalog/ModifyPolicy.cs new file mode 100644 index 0000000..0aa3e7d --- /dev/null +++ b/src/SheetMe.Core/Catalog/ModifyPolicy.cs @@ -0,0 +1,66 @@ +namespace SheetMe.Core.Catalog; + +/// +/// '수정'(제자리 정정)을 누가 쓸 수 있는가 — 레거시 TK_MODIFY 가시성 규칙을 옮긴 것. +/// +/// 왜 저장과 다른가. 수정은 활성 SdgKey 를 그대로 두고 그 행의 디자인만 바꾼다. +/// EMR 이 참조하던 키가 유지되므로 되돌릴 표식이 남지 않는 쓰기에 가깝고, +/// E_SctMst 를 건드리지 않아 새 컨트롤은 매핑 행이 생기지 않는다. +/// 레거시가 이 버튼만 전산실에 묶어 둔 이유가 그것이다 +/// (frmSheetDesigner.vb:1029-1060, 주석 '[추가] 박성한 20150302 전산실만 사용가능). +/// 저장 버튼에는 이런 분기가 없다. +/// +/// 규칙을 여기 순수 함수로 두는 이유: 병원 코드·부서 코드 조합이 세 갈래라 화면에서 즉석 판정하면 +/// 어느 갈래가 왜 막혔는지 확인할 방법이 없다. 여기라면 표로 고정할 수 있다. +/// +public static class ModifyPolicy +{ + #region Member Fields + /// 이 두 병원은 전산실 외에 총무·구매 부서도 쓴다(레거시 :1033-1038) + private static readonly string[] WideHospitals = { "HANKOOK", "GNRH" }; + + private static readonly string[] WideDepartments = { "EDPS", "SALO", "SAVE" }; + + /// 이 날짜 이후 개원한 병원은 사내 계정만(레거시 :1042-1048) + private const string VendorOnlyFrom = "20250501"; + + private const string VendorUid = "MSYS"; + + private const string ItDepartment = "EDPS"; + #endregion + + #region Methods + /// + /// 이 사용자가 '수정'을 쓸 수 있는가. + /// HspStrDte(YYYYMMDD) — 모르면 빈 문자열. + /// + public static bool CanModify(string hospitalCode, string departmentCode, string uidCod, + string hospitalStartDate) + { + if (Array.Exists(WideHospitals, h => string.Equals(h, hospitalCode, StringComparison.OrdinalIgnoreCase))) + { + return Array.Exists(WideDepartments, + d => string.Equals(d, departmentCode, StringComparison.OrdinalIgnoreCase)); + } + if (string.CompareOrdinal(hospitalStartDate, VendorOnlyFrom) >= 0) + { + return string.Equals(uidCod, VendorUid, StringComparison.OrdinalIgnoreCase); + } + return string.Equals(departmentCode, ItDepartment, StringComparison.OrdinalIgnoreCase); + } + + /// 왜 막혔는지 — 화면이 "권한이 없습니다"만 말하면 누구에게 물어야 할지 모른다 + public static string DenyReason(string hospitalCode, string uidCod, string hospitalStartDate) + { + if (Array.Exists(WideHospitals, h => string.Equals(h, hospitalCode, StringComparison.OrdinalIgnoreCase))) + { + return "이 병원에서는 전산실·총무·구매 부서만 수정할 수 있습니다."; + } + if (string.CompareOrdinal(hospitalStartDate, VendorOnlyFrom) >= 0) + { + return $"이 병원에서는 {VendorUid} 계정만 수정할 수 있습니다."; + } + return "전산실 소속만 수정할 수 있습니다."; + } + #endregion +} diff --git a/src/SheetMe.Data/Stores/OracleLegacyFormStore.cs b/src/SheetMe.Data/Stores/OracleLegacyFormStore.cs index 01730a4..61c02c9 100644 --- a/src/SheetMe.Data/Stores/OracleLegacyFormStore.cs +++ b/src/SheetMe.Data/Stores/OracleLegacyFormStore.cs @@ -286,6 +286,134 @@ public sealed class OracleLegacyFormStore /// 디자인 저장 — 레거시 동일 의미론(제자리 갱신 vs 버저닝), 단일 트랜잭션. /// 반환: 저장된 SdgKey. /// + /// + /// 지금 쓰이는 버전을 제자리에서 정정한다 — 레거시 TK_MODIFY(3-인자 오버로드). + /// + /// 저장과 이력 방향이 반대다. 저장은 옛 행을 SdgDelYon='Y' 로 내리고 새 행이 활성이 된다. + /// 수정은 활성 행이 제자리에서 갱신되고, 옛 XML 을 담은 새 행이 이력으로 들어간다. + /// 그래서 SdgKey 가 바뀌지 않는다 — EMR 이 참조하던 키가 그대로 유지된다. + /// + /// E_SctMst 를 건드리지 않는다. 레거시가 그 부분을 주석 처리해 두었고 + /// (bzSaveSheetDesignNControlInfo.vb:170-174), 그것이 "글자만 수정 가능"이라는 경고문의 + /// 기술적 근거다. SdgKey 가 그대로라 기존 매핑 행은 유효하지만 새 컨트롤은 매핑 행이 안 생긴다. + /// 호출부가 로 먼저 막는다. + /// + /// 레거시 버그 둘은 복제하지 않는다. + /// ① 감사 컬럼이 뒤집혀 있었다 — 활성 행에는 아무것도 안 쓰고 + /// 옛 디자인을 담은 이력 행이 현재 사용자·시각을 받았다(:162-163). + /// 그러면 이력 패널이 "누가 언제 이 버전을 만들었는가"를 거꾸로 보여 준다. + /// 여기서는 활성 행에 기록하고, 이력 행은 원래 값을 지킨다. + /// ② Rows(0) 을 개수 검사보다 먼저 읽어 활성 행이 없으면 예외였다(:150). + /// 여기서는 먼저 확인하고 뜻이 통하는 오류를 낸다. + /// + public decimal ModifyDesign(FormDocument document, string uid) + { + if (string.IsNullOrWhiteSpace(document.FormId)) + { + throw new InvalidOperationException("서식 코드(FormId)가 없습니다."); + } + var mergedXml = serializer.Write(document); + + using var connection = Open(); + using var transaction = connection.BeginTransaction(); + try + { + var now = ServerClock.Read(connection, transaction).Minute12; + var activeSdgKey = ResolveActiveSdgKey(connection, transaction, document.FormId); + if (activeSdgKey <= 0) + { + throw new InvalidOperationException( + $"서식 [{document.FormId}] 에 지금 쓰이는 디자인이 없습니다. 수정 대신 저장을 쓰세요."); + } + + // 옛 디자인과 그 감사 값을 먼저 떠 둔다 — 이력 행이 그 값을 그대로 가져가야 + // "그 버전을 누가 언제 만들었는지"가 보존된다. + string oldXml = string.Empty, oldUid = uid, oldStamp = now; + using (var read = connection.CreateCommand()) + { + read.Transaction = transaction; + read.BindByName = true; + read.CommandText = + "SELECT SdgDesign, NVL(SdgUidCod,' '), NVL(SdgUpdDtm,' ') FROM E_SdgMst WHERE SdgKey = :k"; + read.Parameters.Add(new OracleParameter("k", activeSdgKey)); + using var reader = read.ExecuteReader(); + if (reader.Read()) + { + oldXml = reader.IsDBNull(0) ? string.Empty : reader.GetString(0); + oldUid = reader.GetString(1).Trim(); + oldStamp = reader.GetString(2).Trim(); + } + } + + NonQuery(connection, transaction, + "UPDATE E_SdgMst SET SdgDesign = :d, SdgUidCod = :u, SdgUpdDtm = :t WHERE SdgKey = :k", + new OracleParameter("d", OracleDbType.Clob) { Value = mergedXml }, + new OracleParameter("u", uid), + new OracleParameter("t", now), + new OracleParameter("k", activeSdgKey)); + + var historyKey = Convert.ToDecimal(Scalar(connection, transaction, + "SELECT NVL(MAX(SdgKey),0)+1 FROM E_SdgMst")); + NonQuery(connection, transaction, + "INSERT INTO E_SdgMst (SdgKey, SdgShtCod, SdgDesign, SdgUidCod, SdgUpdDtm," + + " SdgDelYon, SdgDelUid, SdgDelDtm)" + + " VALUES (:k, :s, :d, :u, :t, 'Y', '', '')", + new OracleParameter("k", historyKey), + new OracleParameter("s", document.FormId), + new OracleParameter("d", OracleDbType.Clob) { Value = oldXml }, + new OracleParameter("u", oldUid.Length > 0 ? oldUid : uid), + new OracleParameter("t", oldStamp.Length > 0 ? oldStamp : now)); + + transaction.Commit(); + return activeSdgKey; + } + catch + { + transaction.Rollback(); + throw; + } + } + + /// + /// 이 문서에 있는데 지금 활성 버전의 E_SctMst 에는 없는 컨트롤 이름들. + /// + /// 수정은 E_SctMst 를 안 쓰므로 이 이름들은 매핑 행 없이 저장되고 + /// EMR 에서 조용히 값이 안 채워진다. 그래서 호출부가 수정을 막는 근거로 쓴다. + /// 판정 근거를 로 통일한다 — 저장이 행을 만드는 규칙과 같은 것이라 + /// "행이 원래 안 생기는 종류"를 새 컨트롤로 오인하지 않는다. + /// + public List NewControlNames(FormDocument document) + { + var names = new List(); + using var connection = Open(); + var activeSdgKey = ResolveActiveSdgKey(connection, null, document.FormId); + if (activeSdgKey <= 0) + { + return names; + } + var existing = new HashSet(StringComparer.Ordinal); + using (var query = connection.CreateCommand()) + { + query.BindByName = true; + query.CommandText = "SELECT SctObjNam FROM E_SctMst WHERE SctSdgKey = :k"; + query.Parameters.Add(new OracleParameter("k", activeSdgKey)); + using var reader = query.ExecuteReader(); + while (reader.Read()) + { + existing.Add(reader.GetString(0)); + } + } + foreach (var row in SctMstXmlWalker.Walk(serializer.Write(document))) + { + if (!string.Equals(row.ObjType, SpreadItemType, StringComparison.OrdinalIgnoreCase) + && !existing.Contains(row.ObjName) && !names.Contains(row.ObjName)) + { + names.Add(row.ObjName); + } + } + return names; + } + public decimal SaveDesign(FormDocument document, string uid) { if (string.IsNullOrWhiteSpace(document.FormId)) diff --git a/src/SheetMe.Designer/DataBusiness/FormDesignDataBusiness.cs b/src/SheetMe.Designer/DataBusiness/FormDesignDataBusiness.cs index b0f6bbd..f97a245 100644 --- a/src/SheetMe.Designer/DataBusiness/FormDesignDataBusiness.cs +++ b/src/SheetMe.Designer/DataBusiness/FormDesignDataBusiness.cs @@ -1,4 +1,5 @@ -using SheetMe.Core.Models; +using SheetMe.Core.Catalog; +using SheetMe.Core.Models; using SheetMe.Data.Config; using SheetMe.Data.Stores; using SheetMe.Designer.Services; @@ -105,6 +106,38 @@ internal sealed class FormDesignDataBusiness return DbStore().SaveDesign(document, AuditUid); } + /// + /// 제자리 정정 — 레거시 '기록지 수정'. 지금 쓰이는 SdgKey 를 유지한 채 디자인만 바꾼다. + /// 와 같은 게이트를 쓴다 — 이쪽이 더 되돌리기 어려우므로 더 느슨할 이유가 없다. + /// + public decimal ModifyInDb(FormDocument document) + { + if (!CanSaveToDb) + { + throw new InvalidOperationException( + "DB 저장이 비활성화되어 있습니다. appsettings.json 의 FormStore:SaveMode 를 'Db' 로 설정하세요."); + } + return DbStore().ModifyDesign(document, AuditUid); + } + + /// 수정을 막아야 하는 새 컨트롤 이름들 — 비어 있으면 진행해도 된다 + public List NewControlNames(FormDocument document) => DbStore().NewControlNames(document); + + /// 이 사용자가 수정을 쓸 수 있는가 — 레거시는 전산실에만 열어 두었다 + public static bool CanModify() => ModifyPolicy.CanModify( + UserSession.Current.HspCod, UserSession.Current.DepCod, UserSession.Current.UidCod, + HospitalStartDate); + + public static string ModifyDenyReason() => ModifyPolicy.DenyReason( + UserSession.Current.HspCod, UserSession.Current.UidCod, HospitalStartDate); + + /// + /// HspStrDte 는 세션에 없다 — 레거시는 이 값으로 "2025-05-01 이후 개원 병원은 사내 계정만"을 가른다. + /// 모르는 값을 빈 문자열로 두면 그 갈래가 안 걸리고 부서 규칙(EDPS)으로 떨어진다. + /// 없는 값을 지어내 더 조이는 것보다, 레거시의 다수 경로와 같게 두고 이 사실을 적어 두는 쪽을 택한다. + /// + private const string HospitalStartDate = ""; + /// 서식 코드 등록 여부(E_ShtMst) public bool SheetExists(string shtCod) => DbStore().SheetExists(shtCod); diff --git a/src/SheetMe.Designer/ViewModels/MainViewModel.cs b/src/SheetMe.Designer/ViewModels/MainViewModel.cs index 6f6b5bc..e4c60f8 100644 --- a/src/SheetMe.Designer/ViewModels/MainViewModel.cs +++ b/src/SheetMe.Designer/ViewModels/MainViewModel.cs @@ -179,6 +179,14 @@ internal sealed class MainViewModel : ViewModelBase /// public bool CanSaveToDb => dataBusiness.CanSaveToDb; + /// + /// '수정'(제자리 정정)을 이 단말·이 사용자가 쓸 수 있는가. + /// 레거시는 이 버튼만 전산실에 묶어 두었다 — 저장 버튼에는 그런 분기가 없다. + /// + public bool CanModifyInDb => dataBusiness.CanSaveToDb && FormDesignDataBusiness.CanModify(); + + public ICustomCommand? ModifyInDbCommand { get; set; } + /// 좌측 패널 탭(0=서식 목록, 1=레이어, 2=도구 상자) — 서식 열면 레이어로 자동 전환([200] 관행) public int SelectedLeftTabIndex { @@ -215,6 +223,7 @@ internal sealed class MainViewModel : ViewModelBase }); SaveAsFileCommand = new Command((sender, e) => OnSaveFile(saveAs: true)); SaveToDbCommand = new Command((sender, e) => OnSaveToDb()); + ModifyInDbCommand = new Command((sender, e) => OnModifyInDb()); ExportJsonCommand = new Command((sender, e) => OnExportJson()); ImportJsonCommand = new Command((sender, e) => OnImportJson()); PreviewCommand = new Command((sender, e) => OnPreview()); @@ -591,6 +600,87 @@ internal sealed class MainViewModel : ViewModelBase } } + /// + /// 제자리 정정 — 레거시 '기록지 수정'. + /// + /// 저장과 무엇이 다른가: 저장은 새 버전을 만들어 그것이 활성이 되고 옛 것이 이력이 된다. + /// 수정은 지금 쓰이는 버전을 그 자리에서 고친다 — SdgKey 가 안 바뀌므로 + /// EMR 이 참조하던 키가 유지되고, 옛 내용만 이력으로 떨어진다. + /// + /// 그래서 새 컨트롤을 막는다. 수정은 E_SctMst 를 안 건드리므로 새 컨트롤은 매핑 행 없이 + /// 저장되고 EMR 에서 조용히 값이 안 채워진다. 레거시는 이것을 산문으로만 경고했고 + /// ("새로운 컨트롤이 추가될 경우 수정이 아닌 저장을 이용하시기 바랍니다") 막지는 않았다. + /// 경고문을 읽지 않으면 그대로 번지므로 여기서는 이름을 대조해 막는다. + /// + private bool OnModifyInDb() + { + if (CurrentDesigner is not { } designer) + { + return false; + } + try + { + if (!EnsureDb() || !dataBusiness.CanWriteDb) + { + DialogService.Notify(DialogKind.Warning, "수정", + "HIS 사용자가 확인되지 않아 수정할 수 없습니다.", + "이력에 남길 수정자를 특정할 수 없습니다."); + return false; + } + // 저장 경로와 같은 게이트 — 열기 이후 마스터에서 플래그가 꺼졌을 수 있다 + if (!EnsureDesignAllowed(designer.Document.FormId)) + { + return false; + } + if (!FormDesignDataBusiness.CanModify()) + { + DialogService.Notify(DialogKind.Warning, "수정 권한 없음", + FormDesignDataBusiness.ModifyDenyReason(), + "저장(새 버전 만들기)은 그대로 쓸 수 있습니다."); + return false; + } + CommitPendingEdits(); + var document = designer.Document; + + var added = dataBusiness.NewControlNames(document); + if (added.Count > 0) + { + var sample = string.Join(", ", added.Take(5)); + DialogService.Notify(DialogKind.Warning, "수정할 수 없습니다", + $"이 서식에 새 컨트롤 {added.Count}개가 추가되었습니다 — {sample}" + + (added.Count > 5 ? " 외" : string.Empty), + "수정은 지금 버전의 글자만 고칩니다. 새 컨트롤은 EMR 연결 정보가 만들어지지 않아 " + + "화면에서 값이 비게 됩니다. '저장'으로 새 버전을 만드세요."); + return false; + } + + if (!DialogService.Confirm("수정 확인", + $"[{document.FormId}] {document.Title} — 지금 쓰이는 버전을 그 자리에서 고칩니다.", + "새 버전을 만들지 않으므로 EMR 이 참조하는 키가 그대로 유지됩니다.\n" + + "지금 내용은 이력으로 남습니다.\n\n" + + "기술 정보 — 활성 SdgKey 의 SdgDesign 을 갱신하고, 옛 XML 을 SdgDelYon='Y' 행으로 넣습니다. " + + "E_SctMst 는 건드리지 않습니다.", + yes: "수정", no: "취소")) + { + return false; + } + + var sdgKey = dataBusiness.ModifyInDb(document); + designer.IsFromDb = true; + designer.Undo.MarkSaved(); + designer.NotifyDisplayNameChanged(); + StatusText = $"수정 완료: {document.FormId} → SdgKey {sdgKey} 제자리 갱신"; + _ = LoadSheetListAsync(); + return true; + } + catch (Exception ex) + { + DialogService.Notify(DialogKind.Error, "오류", + "수정 중 오류가 발생했습니다. (트랜잭션 롤백됨)", ex.Message); + return false; + } + } + // 'DB에서 열기' 대화상자(680×560)를 지웠다. // // 좌측 '서식 목록' 탭이 같은 조회를 상시로 한다(둘 다 ListSheets, WHERE·ORDER BY·상한 동일). diff --git a/src/SheetMe.Designer/Views/MainView.xaml b/src/SheetMe.Designer/Views/MainView.xaml index 44950ce..8c8da3b 100644 --- a/src/SheetMe.Designer/Views/MainView.xaml +++ b/src/SheetMe.Designer/Views/MainView.xaml @@ -168,7 +168,18 @@ + IsEnabled="{Binding CanSaveToDb}" + ToolTip="새 버전을 만듭니다. 지금 버전은 이력으로 남습니다"/> + + diff --git a/tests/SheetMe.Core.Tests/ModifyPolicyTests.cs b/tests/SheetMe.Core.Tests/ModifyPolicyTests.cs new file mode 100644 index 0000000..0eb7dd6 --- /dev/null +++ b/tests/SheetMe.Core.Tests/ModifyPolicyTests.cs @@ -0,0 +1,82 @@ +using SheetMe.Core.Catalog; + +namespace SheetMe.Core.Tests; + +/// +/// '수정' 권한 규칙 고정 — 레거시 TK_MODIFY 가시성 분기를 옮긴 것. +/// +/// 이 표가 느슨해지면 전산실이 아닌 사람이 지금 쓰이는 버전을 제자리에서 고치게 된다. +/// 수정은 새 버전을 만들지 않으므로 되돌릴 표식이 이력 한 행뿐이고, +/// E_SctMst 를 건드리지 않아 새 컨트롤은 조용히 배선에서 빠진다. +/// 반대로 너무 조이면 지금 이 일을 하고 있는 전산실이 못 쓴다. +/// +[TestClass] +public sealed class ModifyPolicyTests +{ + #region Methods + [TestMethod] + public void 기본_규칙은_전산실만() + { + Assert.IsTrue(ModifyPolicy.CanModify("ABC", "EDPS", "011825", string.Empty)); + Assert.IsFalse(ModifyPolicy.CanModify("ABC", "NURS", "011825", string.Empty)); + Assert.IsFalse(ModifyPolicy.CanModify("ABC", string.Empty, "011825", string.Empty)); + } + + [TestMethod] + public void 두_병원은_총무_구매도_쓴다() + { + // 레거시 frmSheetDesigner.vb:1033-1038 + foreach (var dep in new[] { "EDPS", "SALO", "SAVE" }) + { + Assert.IsTrue(ModifyPolicy.CanModify("HANKOOK", dep, "011825", string.Empty), dep); + Assert.IsTrue(ModifyPolicy.CanModify("GNRH", dep, "011825", string.Empty), dep); + } + Assert.IsFalse(ModifyPolicy.CanModify("HANKOOK", "NURS", "011825", string.Empty)); + } + + [TestMethod] + public void 넓은_병원_규칙이_전산실_규칙보다_먼저다() + { + // HANKOOK 에서 SALO 는 전산실이 아니지만 허용된다 — 순서가 뒤집히면 이 사람이 막힌다. + Assert.IsTrue(ModifyPolicy.CanModify("HANKOOK", "SALO", "누구든", string.Empty)); + } + + [TestMethod] + public void 신규_개원_병원은_사내_계정만() + { + // 레거시 :1042-1048 — HspStrDte >= 20250501 + Assert.IsTrue(ModifyPolicy.CanModify("XYZ", "NURS", "MSYS", "20250501")); + Assert.IsTrue(ModifyPolicy.CanModify("XYZ", "NURS", "MSYS", "20260101")); + // 그 병원에서는 전산실이어도 안 된다 + Assert.IsFalse(ModifyPolicy.CanModify("XYZ", "EDPS", "011825", "20250501")); + // 경계 하루 전은 부서 규칙으로 떨어진다 + Assert.IsTrue(ModifyPolicy.CanModify("XYZ", "EDPS", "011825", "20250430")); + Assert.IsFalse(ModifyPolicy.CanModify("XYZ", "NURS", "MSYS", "20250430")); + } + + [TestMethod] + public void 개원일을_모르면_부서_규칙으로_떨어진다() + { + // 세션에 HspStrDte 가 없다. 빈 문자열은 20250501 보다 작으므로 그 갈래가 안 걸린다 — + // 레거시의 다수 경로와 같은 판정이다. 이 동작이 바뀌면 전산실이 통째로 막힌다. + Assert.IsTrue(ModifyPolicy.CanModify("XYZ", "EDPS", "011825", string.Empty)); + Assert.IsFalse(ModifyPolicy.CanModify("XYZ", "NURS", "011825", string.Empty)); + } + + [TestMethod] + public void 막힌_이유가_갈래마다_다르다() + { + // "권한이 없습니다"만 말하면 누구에게 물어야 할지 모른다. + StringAssert.Contains(ModifyPolicy.DenyReason("HANKOOK", "011825", string.Empty), "총무"); + StringAssert.Contains(ModifyPolicy.DenyReason("XYZ", "011825", "20250501"), "MSYS"); + StringAssert.Contains(ModifyPolicy.DenyReason("XYZ", "011825", string.Empty), "전산실"); + } + + [TestMethod] + public void 대소문자를_가리지_않는다() + { + Assert.IsTrue(ModifyPolicy.CanModify("hankook", "edps", "x", string.Empty)); + Assert.IsTrue(ModifyPolicy.CanModify("XYZ", "NURS", "msys", "20250501")); + } + #endregion +}