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
+}