From e9d8f7356e9bd2a69953c476a71b460c2d2e7c84 Mon Sep 17 00:00:00 2001 From: Msystech Date: Tue, 18 Aug 2026 10:09:10 +0900 Subject: [PATCH] =?UTF-8?q?=ED=99=98=EC=9E=90=20=EB=AF=B8=EB=A6=AC?= =?UTF-8?q?=EB=B3=B4=EA=B8=B0=20=EC=A0=91=EA=B7=BC=20=EA=B7=9C=EC=B9=99?= =?UTF-8?q?=EC=9D=84=20=EB=A8=BC=EC=A0=80=20=EC=84=B8=EC=9A=B4=EB=8B=A4=20?= =?UTF-8?q?=E2=80=94=20=EB=A0=88=EA=B1=B0=EC=8B=9C=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=98=AE=EA=B8=B0=EC=A7=80=20=EC=95=8A=EC=9D=84=20=EA=B2=83?= =?UTF-8?q?=EB=93=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 미리보기 레거시 호환 계획의 A3 첫 조각. DB 조회는 조사 중이라 규칙과 감사부터 세운다. 레거시에서 옮기지 않는 것이 요점이다. · 폼이 뜰 때마다 인가코드를 InputBox 로 묻는데 통과값 3개가 소스에 평문이다 (frmSheetDesigner.vb:913) · 환자를 검색조건 없이 운영 전체에서 찾는다(:636) · 누가 어느 환자를 열었는지 남기지 않는다(감사 0건) 셋을 그대로 옮기면 신규 앱이 최악을 물려받는다. 검색 범위는 레거시와 같게 두기로 했다(전체 검색 + 감사 로그). 그래서 남기는 쪽이 안전장치의 본체다 — 조회를 막지 않는 대신 흔적을 남긴다. AppLog.Audit 은 FIXED 레벨이라 어느 로그 설정에서도 남는다. 권한 축은 ModifyPolicy 와 같은 것을 쓴다. 환자 기록을 보는 일은 지금 쓰이는 서식을 제자리에서 고치는 일과 무게가 다르지 않다. 미인증이면 막는다 — 주체가 없으면 감사에 남길 것이 없고, 남길 수 없는 조회는 하지 않는다. 감사 한 줄에 환자 이름은 넣지 않는다. 내원번호로 되짚을 수 있고, 로그는 오래 남고 접근 통제가 DB 보다 느슨하다. 검사로 못 박았다. 게이트: 테스트 303/303(신규 4), --edit-smoke 0실패, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일. Co-Authored-By: Claude Opus 5 --- .../Catalog/PatientPreviewPolicy.cs | 49 +++++++++++++++++ .../PatientPreviewPolicyTests.cs | 54 +++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 src/SheetMe.Core/Catalog/PatientPreviewPolicy.cs create mode 100644 tests/SheetMe.Core.Tests/PatientPreviewPolicyTests.cs diff --git a/src/SheetMe.Core/Catalog/PatientPreviewPolicy.cs b/src/SheetMe.Core/Catalog/PatientPreviewPolicy.cs new file mode 100644 index 0000000..2ed1892 --- /dev/null +++ b/src/SheetMe.Core/Catalog/PatientPreviewPolicy.cs @@ -0,0 +1,49 @@ +namespace SheetMe.Core.Catalog; + +/// +/// 환자 데이터로 미리보기를 볼 수 있는 조건. +/// +/// 레거시에서 옮기지 않는 것이 요점이다. 레거시 서식생성기는 +/// · 폼이 뜰 때마다 인가코드를 InputBox 로 묻고 통과값 3개가 소스에 평문으로 박혀 있고 +/// (frmSheetDesigner.vb:913), +/// · 환자를 검색조건 없이 운영 전체에서 찾을 수 있고(:636), +/// · 누가 어느 환자를 열었는지 남기지 않는다(감사 0건). +/// 그 셋을 그대로 옮기면 신규 앱이 최악을 물려받는다. +/// +/// 여기서 정하는 것은 누가 볼 수 있는가뿐이다. 검색 범위는 레거시와 같게 두기로 했고 +/// (전체 검색 + 감사 로그), 그래서 남기는 쪽이 안전장치의 본체다 — +/// 조회 자체를 막지 않는 대신 흔적을 남긴다. +/// +/// 조건은 와 같은 축을 쓴다. 환자 기록을 보는 일은 +/// 지금 쓰이는 서식을 제자리에서 고치는 일과 무게가 다르지 않다. +/// +public static class PatientPreviewPolicy +{ + #region Methods + /// + /// 이 사용자가 환자 데이터로 미리보기를 볼 수 있는가. + /// + /// 인증된 HIS 사용자여야 한다 — 미인증이면 감사 로그에 남길 주체가 없고, + /// 주체 없는 환자 조회는 그 자체로 남기면 안 되는 일이다. + /// + public static bool CanPreviewPatient(string hospitalCode, string departmentCode, string uidCod, + string hospitalStartDate) + => uidCod.Length > 0 + && ModifyPolicy.CanModify(hospitalCode, departmentCode, uidCod, hospitalStartDate); + + /// 왜 막혔는지 — "권한이 없습니다"만으로는 누구에게 물어야 할지 모른다 + public static string DenyReason(string hospitalCode, string uidCod, string hospitalStartDate) + => uidCod.Length == 0 + ? "HIS 사용자가 확인되지 않았습니다. 기록지정보 화면에서 실행하거나 사용자 코드를 전달해 주세요." + : ModifyPolicy.DenyReason(hospitalCode, uidCod, hospitalStartDate); + + /// + /// 감사 로그 한 줄 — 누가·언제·어느 내원을 열었는가. + /// + /// 문구를 여기서 만드는 이유: 호출부마다 다르게 적으면 나중에 로그를 찾을 수 없다. + /// 환자 이름은 넣지 않는다 — 내원번호로 되짚을 수 있고, 로그 파일에 이름을 흘릴 이유가 없다. + /// + public static string AuditLine(string uidCod, string comNum) + => $"[환자 미리보기] 사용자={uidCod} 내원={comNum}"; + #endregion +} diff --git a/tests/SheetMe.Core.Tests/PatientPreviewPolicyTests.cs b/tests/SheetMe.Core.Tests/PatientPreviewPolicyTests.cs new file mode 100644 index 0000000..d58d224 --- /dev/null +++ b/tests/SheetMe.Core.Tests/PatientPreviewPolicyTests.cs @@ -0,0 +1,54 @@ +using SheetMe.Core.Catalog; + +namespace SheetMe.Core.Tests; + +/// +/// 환자 미리보기 접근 규칙 고정. +/// +/// 이 표가 느슨해지면 서식을 만드는 사람 누구나 운영 환자 기록을 열 수 있다. +/// 레거시가 그 상태였다 — 인가코드가 소스에 평문이고, 검색 범위 무제한이고, 감사 0건이다. +/// 검색 범위는 레거시와 같게 두기로 했으므로 감사와 권한이 유일한 장치다. +/// +[TestClass] +public sealed class PatientPreviewPolicyTests +{ + #region Methods + [TestMethod] + public void 미인증이면_볼_수_없다() + { + // 주체가 없으면 감사 로그에 남길 것이 없다 — 남길 수 없는 조회는 하지 않는다. + Assert.IsFalse(PatientPreviewPolicy.CanPreviewPatient("ABC", "EDPS", string.Empty, string.Empty)); + StringAssert.Contains( + PatientPreviewPolicy.DenyReason("ABC", string.Empty, string.Empty), "HIS 사용자"); + } + + [TestMethod] + public void 수정_권한과_같은_축을_쓴다() + { + // 환자 기록을 보는 일은 지금 쓰이는 서식을 제자리에서 고치는 일과 무게가 다르지 않다. + Assert.IsTrue(PatientPreviewPolicy.CanPreviewPatient("ABC", "EDPS", "011825", string.Empty)); + Assert.IsFalse(PatientPreviewPolicy.CanPreviewPatient("ABC", "NURS", "011825", string.Empty)); + Assert.IsTrue(PatientPreviewPolicy.CanPreviewPatient("HANKOOK", "SALO", "011825", string.Empty)); + Assert.IsFalse(PatientPreviewPolicy.CanPreviewPatient("XYZ", "EDPS", "011825", "20250501")); + } + + [TestMethod] + public void 감사_한_줄에_주체와_내원이_들어간다() + { + var line = PatientPreviewPolicy.AuditLine("011825", "C12345"); + StringAssert.Contains(line, "011825"); + StringAssert.Contains(line, "C12345"); + StringAssert.Contains(line, "환자 미리보기"); + } + + [TestMethod] + public void 감사_한_줄에_환자_이름은_넣지_않는다() + { + // 내원번호로 되짚을 수 있다. 로그 파일에 이름을 흘릴 이유가 없다 — + // 로그는 오래 남고 접근 통제가 DB 보다 느슨하다. + var line = PatientPreviewPolicy.AuditLine("011825", "C12345"); + Assert.IsFalse(line.Contains("이름", StringComparison.Ordinal)); + Assert.AreEqual(2, line.Split('=').Length - 1, $"항목이 둘(사용자·내원)이어야 한다: {line}"); + } + #endregion +}