싸인·직인·로고 17종 — 경로 조회와 이미지 렌더 이식(전송 계층은 재현 불가)
워크플로 조사(읽기 5 + 반박 검증 5)로 전송 계층과 태그 17종을 훑었고,
검증이 조사서의 "FTP" 단정을 반박해 실체가 드러났다: 전송 수단은 단일
프로토콜이 아니라 DB 설정(M_DtlMst 'EMR_FileTransfer' Type)이 고르는
5갈래이고, 새 진단 --db-filecfg 로 확인한 우리 사이트 값은 Type=1 =
FileServer = ActiveX OCX(AxBITSFTLib.AxbitSFT)다. OCX 내부 프로토콜은
코드에 없고 서드파티·ActiveX 는 배포 금지라 <b>다운로드는 재현할 수 없다</b>.
그래서 받지 않고 찾는다: ①레거시 EMR 이 같은 단말에 이미 받아 둔 캐시
({설치경로}\Tmp\DownLoadedFiles\Signature\ — 레거시가 쓰는 그 폴더),
②DB 경로 자체가 열리는 경우(이 병원 저장 형태는 드라이브 절대경로).
둘 다 없으면 사유 → 종이는 빈칸(어제 결정). 판정은 순수 함수
SignatureImagePath 로 두고 테스트 7건(캐시 우선·직접 경로·둘 다 없음·
빈 경로·설치경로 없음·UNC).
경로 조회는 전부 순수 SQL 이라 그대로 이식(SignatureStore): 집도의
(DTRUIDCOD)·마취(수술 ANEUIDCOD / 부서 — 개원일 2021-03-01 분기)·
영상의학(XRAY)·진단검사(LAB)·외출외박 승인자(병동/원무)·병동 수간호사·
병원로고(M_HspMst.HspLgoPth)·병원직인(주석과 달리 DB_REGISTRY 의
'직인경로'). 로그인 사용자 3종은 세션 행에 UidImgPth 를 얹어 왕복 0,
담당의·진단서 의사는 프리페치 행 재사용.
레거시 결함 보존 2건: ETC_병리판독의사싸인은 SELECT 에 UidNam 만 있고
UidImgPth 를 읽어 항상 예외→빈 값(:17434), OCM_Cosign_싸인은 저장 기록
키(EmrKey) 의존이라 미리보기에서 값이 될 수 없다 — 둘 다 사유로 완결.
렌더: PictureBox 가 자리표시자만 그리던 것을 실제 이미지로. 파일을
잠그지 않고(OnLoad) 읽으며, 인쇄가 SizeMode 를 무시하고 사각형에 늘려
그리는 레거시 동작에 맞춰 기본 Stretch=Fill(Zoom 만 비율 유지).
WithTagValue 는 이미지류에서 Text 대신 경로 속성을 채운다.
분류도 정정 — 싸인 계열이 Image(글자만 보여 준다)에서 PatientContext 로.
- dotnet test 354/354 (경로 판정 7건 추가) · --edit-smoke 실패 0
- --db-patient ①~㊲ 전건 통과 — ㊲ 신설: 9개 경로 조회 실행,
마취과 의사 싸인 경로가 실값 29자로 나온다
- --db-filecfg 신설(값 없이 모드만): Type=1 · E_FilInf 102만행 ·
UidImgPth 157행 · 경로 모양 드라이브 절대경로
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
2310971a10
commit
a0333791b6
@@ -0,0 +1,84 @@
|
||||
namespace SheetMe.Core.Catalog;
|
||||
|
||||
/// <summary>이미지 경로를 어떻게 얻었는가 — 화면·진단이 사유를 정확히 말하기 위해 구분한다</summary>
|
||||
public enum SignatureImageOrigin
|
||||
{
|
||||
/// <summary>쓸 수 있는 파일이 없다</summary>
|
||||
None,
|
||||
|
||||
/// <summary>레거시 EMR 이 이미 내려받아 둔 캐시 파일</summary>
|
||||
LegacyCache,
|
||||
|
||||
/// <summary>DB 가 준 경로 자체가 이 단말에서 바로 열린다</summary>
|
||||
DirectPath,
|
||||
}
|
||||
|
||||
/// <summary>이미지 경로 판정 결과</summary>
|
||||
/// <param name="Origin">어디서 얻었는가</param>
|
||||
/// <param name="LocalPath">열 수 있는 파일의 전체 경로 — 없으면 빈 문자열</param>
|
||||
/// <param name="FileName">원격 경로의 파일명(사유 문구·캐시 조회용)</param>
|
||||
public readonly record struct SignatureImageResult(
|
||||
SignatureImageOrigin Origin, string LocalPath, string FileName);
|
||||
|
||||
/// <summary>
|
||||
/// DB 가 준 <b>원격 경로</b>에서 실제로 열 수 있는 <b>로컬 파일</b>을 정한다.
|
||||
///
|
||||
/// <b>왜 이 규칙인가.</b> 레거시는 원격 경로를 파일서버에서 내려받아 로컬 경로를 돌려준다
|
||||
/// (bzDataInterface.vb 싸인 17종 공통). 그 전송 계층은 이 병원 설정에서 ActiveX OCX
|
||||
/// (<c>AxBITSFTLib.AxbitSFT</c>, M_DtlMst 'EMR_FileTransfer' Type=1)라 .NET 으로 재현할 수 없다 —
|
||||
/// OCX 내부 프로토콜이 코드에 없고, 우리 배포는 서드파티·ActiveX 를 금지한다.
|
||||
///
|
||||
/// 그래서 <b>받지 않고 찾는다</b>: ①레거시 EMR 이 같은 단말에 이미 받아 둔 캐시
|
||||
/// (<c>{설치경로}\Tmp\DownLoadedFiles\Signature\{파일명}</c> — 레거시가 쓰는 바로 그 폴더),
|
||||
/// ②DB 경로 자체가 이 단말에서 열리는 경우(로컬 절대경로·UNC 공유). 둘 다 아니면 없음이다.
|
||||
///
|
||||
/// 이 판정을 순수 함수로 두는 이유는 DB·파일서버 없이 규칙을 검증하기 위해서다.
|
||||
/// </summary>
|
||||
public static class SignatureImagePath
|
||||
{
|
||||
#region Methods
|
||||
/// <summary>레거시가 쓰는 캐시 폴더 — 설치 경로가 없으면 빈 문자열</summary>
|
||||
public static string CacheFolder(string? installPath)
|
||||
=> string.IsNullOrWhiteSpace(installPath)
|
||||
? string.Empty
|
||||
// 레거시는 설치 경로가 없어도 문자열을 그대로 이어 붙여 "\Tmp\..." 라는
|
||||
// 루트 상대경로를 만든다(clsCommonLib.vb:271-287). 그 결함은 옮기지 않는다.
|
||||
: System.IO.Path.Combine(installPath.Trim(), "Tmp", "DownLoadedFiles", "Signature");
|
||||
|
||||
/// <summary>
|
||||
/// 원격 경로 → 열 수 있는 로컬 파일.
|
||||
/// </summary>
|
||||
/// <param name="remotePath">DB 가 준 경로(M_UidMst.UidImgPth 등)</param>
|
||||
/// <param name="installPath">HIS 설치 경로(ServerInfo ini 의 Install\Path)</param>
|
||||
/// <param name="exists">파일 존재 판정 — 테스트가 갈아 끼운다</param>
|
||||
public static SignatureImageResult Resolve(string? remotePath, string? installPath,
|
||||
Func<string, bool>? exists = null)
|
||||
{
|
||||
var remote = (remotePath ?? string.Empty).Trim();
|
||||
if (remote.Length == 0)
|
||||
{
|
||||
return new SignatureImageResult(SignatureImageOrigin.None, string.Empty, string.Empty);
|
||||
}
|
||||
// 레거시는 역슬래시 기준으로 파일명을 뗀다(경로 구분자가 서버 규약이라 Path 로 자르지 않는다)
|
||||
var fileName = remote.Split('\\', '/').Last();
|
||||
var probe = exists ?? System.IO.File.Exists;
|
||||
|
||||
var folder = CacheFolder(installPath);
|
||||
if (folder.Length > 0 && fileName.Length > 0)
|
||||
{
|
||||
var cached = System.IO.Path.Combine(folder, fileName);
|
||||
if (probe(cached))
|
||||
{
|
||||
return new SignatureImageResult(SignatureImageOrigin.LegacyCache, cached, fileName);
|
||||
}
|
||||
}
|
||||
// 캐시에 없으면 원격 경로가 이 단말에서 바로 열리는지 본다 —
|
||||
// 이 병원의 저장 형태는 드라이브 절대경로다(진단 --db-filecfg 실측)
|
||||
if (probe(remote))
|
||||
{
|
||||
return new SignatureImageResult(SignatureImageOrigin.DirectPath, remote, fileName);
|
||||
}
|
||||
return new SignatureImageResult(SignatureImageOrigin.None, string.Empty, fileName);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -157,12 +157,16 @@ public static class TagPreviewCatalog
|
||||
{
|
||||
return TagPreviewKind.Table;
|
||||
}
|
||||
// 싸인·서명·직인·로고 — 값은 <b>이미지 파일 경로</b>다. 환자 해석기가 경로를 얻어
|
||||
// 이 단말에서 파일을 찾고(레거시 캐시 → 경로 자체), PictureBox 가 그린다.
|
||||
// 파일서버 전송(ActiveX)은 옮기지 않았으므로 캐시가 없는 단말에서는 사유가 된다.
|
||||
// 분류는 PatientContext 다 — Image 로 두면 "글자만 보여 준다"는 <b>틀린 사유</b>가 나간다.
|
||||
if (tag.Contains("싸인", StringComparison.Ordinal)
|
||||
|| tag.Contains("서명", StringComparison.Ordinal)
|
||||
|| tag.Contains("직인", StringComparison.Ordinal)
|
||||
|| tag.Contains("로고", StringComparison.Ordinal))
|
||||
{
|
||||
return TagPreviewKind.Image;
|
||||
return TagPreviewKind.PatientContext;
|
||||
}
|
||||
return TagPreviewKind.PatientContext;
|
||||
}
|
||||
|
||||
@@ -103,6 +103,31 @@ public static class ServerInfoReader
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HIS 설치 경로 — ini 의 <c>[MSYSTECHHIS\Install] Path</c>(레거시
|
||||
/// <c>GetInstallPathFromServerInfoFile</c>, clsCommonLib.vb:271-287 과 같은 값).
|
||||
/// 서명 이미지 캐시 폴더의 뿌리다. 이 값은 암호화되어 있지 않다(경로일 뿐).
|
||||
/// ini 가 없거나 값이 비면 null — 레거시는 여기서 오류창을 띄우고 빈 문자열을 돌려줘
|
||||
/// 폴더가 루트 상대경로("\Tmp\...")가 되어 버린다. 그 결함은 옮기지 않는다.
|
||||
/// </summary>
|
||||
public static string? InstallPath(string? iniPath = null)
|
||||
{
|
||||
var path = ResolveIniPath(iniPath);
|
||||
if (path is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
try
|
||||
{
|
||||
var value = ReadValue(File.ReadAllLines(path), @"MSYSTECHHIS\Install", "Path");
|
||||
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>ini 경로 결정 — 명시 경로 우선, 없으면 실행 폴더에서 상위로 탐색([000]bin\SheetMe → [000]bin)</summary>
|
||||
private static string? ResolveIniPath(string? iniPath)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
using Oracle.ManagedDataAccess.Client;
|
||||
|
||||
namespace SheetMe.Data.Stores;
|
||||
|
||||
/// <summary>
|
||||
/// 싸인·직인·로고 태그가 쓰는 <b>원격 이미지 경로</b> 조회 — 레거시 bzDataInterface.vb 의
|
||||
/// 싸인 태그 17종에서 기계 추출했다. 태그 본문은 전부 같은 골격이다:
|
||||
/// 경로를 DB 에서 한 칸 얻고 → 파일서버에서 내려받아 → 로컬 경로를 돌려준다.
|
||||
///
|
||||
/// <b>여기서는 경로만 얻는다.</b> 내려받는 계층(clsFileTransfer)은 이 병원 설정에서
|
||||
/// ActiveX OCX(M_DtlMst 'EMR_FileTransfer' Type=1)라 재현할 수 없다 — 그 판정과
|
||||
/// 대안(레거시 캐시 사용)은 <see cref="Core.Catalog.SignatureImagePath"/> 에 있다.
|
||||
///
|
||||
/// 대부분의 원문이 ORDER BY 없는 Rows(0) 이라 정렬을 명시해 한 행으로 고정했다
|
||||
/// (담당의 때 정한 규칙). 각 메서드 주석에 근거 줄번호를 남긴다.
|
||||
/// </summary>
|
||||
public sealed class SignatureStore
|
||||
{
|
||||
#region Member Fields
|
||||
private readonly string connectionString;
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
public SignatureStore(string connectionString)
|
||||
{
|
||||
this.connectionString = connectionString;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
/// <summary>
|
||||
/// 수술 집도의 싸인(OCM_수술집도의_싸인, :3385) — 최신 수술(OprKey DESC)의 집도의
|
||||
/// (S_OprTyp 'DTRUIDCOD')를 오늘 기준 사용자·부서 마스터로 풀어 UidImgPth.
|
||||
/// 원문의 OprStt 무필터·SYSDATE 유효기간을 그대로 옮긴다.
|
||||
/// </summary>
|
||||
public string SurgeonSignature(decimal comNum)
|
||||
=> Scalar(
|
||||
"SELECT v FROM (SELECT UidImgPth v FROM S_OprInf"
|
||||
+ " INNER JOIN S_OprTyp ON OprKey = OprKeyNum AND OprComNum = :m AND OprTyp = 'DTRUIDCOD'"
|
||||
+ " INNER JOIN M_UidMst ON UidCod = OprCod"
|
||||
+ " AND TO_CHAR(SYSDATE, 'YYYYMMDD') BETWEEN UidStrDte AND UidEndDte"
|
||||
+ " INNER JOIN M_DepMst ON DepCod = UidDepCod"
|
||||
+ " AND TO_CHAR(SYSDATE, 'YYYYMMDD') BETWEEN DepStrDte AND DepEndDte"
|
||||
+ " ORDER BY OprKey DESC) WHERE ROWNUM = 1",
|
||||
("m", comNum));
|
||||
|
||||
/// <summary>
|
||||
/// 수술 마취의사 싸인(OCM_수술마취의사싸인_수술접수연동, :3235) — 같은 골격에
|
||||
/// 오더 타입만 'ANEUIDCOD'(부서 조인 없음).
|
||||
/// </summary>
|
||||
public string AnesthesiaSignatureBySurgery(decimal comNum)
|
||||
=> Scalar(
|
||||
"SELECT v FROM (SELECT UidImgPth v FROM S_OprInf"
|
||||
+ " INNER JOIN S_OprTyp ON OprKey = OprKeyNum AND OprComNum = :m AND OprTyp = 'ANEUIDCOD'"
|
||||
+ " INNER JOIN M_UidMst ON UidCod = OprCod"
|
||||
+ " AND TO_CHAR(SYSDATE, 'YYYYMMDD') BETWEEN UidStrDte AND UidEndDte"
|
||||
+ " ORDER BY OprKey DESC) WHERE ROWNUM = 1",
|
||||
("m", comNum));
|
||||
|
||||
/// <summary>
|
||||
/// 마취과 의사 싸인(OCM_수술마취의사싸인, :3311) — 환자와 무관하게 <b>마취과 소속</b>
|
||||
/// 재직자 하나. 개원일 2021-03-01 이후 병원은 청구·그룹 코드로, 그 전은 부서명 LIKE
|
||||
/// '%마취%' 로 찾는다(원문 주석: "코드로 하기엔 병원마다 다를거같아서").
|
||||
/// 원문에 ORDER BY 가 없어 임의 행이었다 — 사용자 코드 순 첫 행으로 고정한다.
|
||||
/// </summary>
|
||||
public string AnesthesiaDepartmentSignature(bool byCode)
|
||||
=> Scalar(
|
||||
"SELECT v FROM (SELECT UidImgPth v FROM M_UidMst"
|
||||
+ " INNER JOIN M_DepMst ON DepCod = UidDepCod"
|
||||
+ " AND TO_CHAR(SYSDATE, 'YYYYMMDD') BETWEEN DepStrDte AND DepEndDte"
|
||||
+ " WHERE TO_CHAR(SYSDATE, 'YYYYMMDD') BETWEEN UidStrDte AND UidEndDte"
|
||||
+ (byCode
|
||||
? " AND DepBilCod = '09' AND DepGrpCod = 'ANS'"
|
||||
: " AND DepKorNam LIKE '%마취%'")
|
||||
+ " ORDER BY UidCod) WHERE ROWNUM = 1");
|
||||
|
||||
/// <summary>
|
||||
/// 부서 코드로 찾는 의사 싸인 — 영상의학과(ETC_영상의학과의사싸인 :17353, 'XRAY')와
|
||||
/// 진단검사(ETC_진단검사의사싸인 :17578) 계열의 공통형. 기준일은 작업일(없으면 오늘).
|
||||
/// 원문 ORDER BY 없음 → 사용자 코드 순 고정.
|
||||
/// </summary>
|
||||
public string DepartmentDoctorSignature(string depCod, string day8)
|
||||
=> depCod.Trim().Length == 0 ? string.Empty
|
||||
: Scalar(
|
||||
"SELECT v FROM (SELECT UidImgPth v FROM M_UidMst"
|
||||
+ " WHERE UidDtrYon = 'Y' AND UidDepCod = :d"
|
||||
+ " AND :y BETWEEN UidStrDte AND UidEndDte"
|
||||
+ " ORDER BY UidCod) WHERE ROWNUM = 1",
|
||||
("d", depCod.Trim()), ("y", day8));
|
||||
|
||||
/// <summary>
|
||||
/// 외출·외박 승인자 싸인(ETC_외출외박_확인간호사싸인 :17683 / _확인원무과싸인 :17765) —
|
||||
/// 신청 행의 승인자 코드(병동 OutAdmWrdAppUid / 원무 OutAdmOffAppUid)를 오늘 기준
|
||||
/// 사용자 마스터로 푼다. 원문 두 조회 모두 ORDER BY 없음 → 신청 시작일 최신으로 고정.
|
||||
/// </summary>
|
||||
public string OutingApproverSignature(decimal comNum, bool officeApprover)
|
||||
=> Scalar(
|
||||
"SELECT v FROM (SELECT UidImgPth v FROM O_OutAdmInf"
|
||||
+ " INNER JOIN M_UidMst ON UidCod = "
|
||||
+ (officeApprover ? "OutAdmOffAppUid" : "OutAdmWrdAppUid")
|
||||
+ " AND TO_CHAR(SYSDATE, 'YYYYMMDD') BETWEEN UidStrDte AND UidEndDte"
|
||||
+ " WHERE OutAdmComNum = :m"
|
||||
+ " ORDER BY NVL(OutAdmStrDtm, OutAdmExpStrDtm) DESC) WHERE ROWNUM = 1",
|
||||
("m", comNum));
|
||||
|
||||
/// <summary>
|
||||
/// 병동 수간호사 싸인(ETC_로그인사용자_병동_수간호사_싸인, :15562) — 이름 태그와 같은
|
||||
/// 조회의 이미지 컬럼. 원문은 부서 코드를 문자열 연결로 넣는다 — 바인드로 정상화.
|
||||
/// </summary>
|
||||
public string WardHeadNurseSignature(string depCod)
|
||||
=> depCod.Trim().Length == 0 ? string.Empty
|
||||
: Scalar(
|
||||
"SELECT v FROM (SELECT UidImgPth v FROM M_UidMst"
|
||||
+ " WHERE TRIM(UidDepCod) = :d"
|
||||
+ " AND UidPstCod IN (SELECT c FROM (SELECT DtsCod c FROM M_DtsMst"
|
||||
+ " WHERE TO_CHAR(SYSDATE, 'YYYYMMDD') BETWEEN DtsStrDte AND DtsExpDte"
|
||||
+ " AND DtsDtlCod = 'PSTCOD' AND DtsCodNam IN ('수간호사','간호수선생')"
|
||||
+ " ORDER BY DtsCod) WHERE ROWNUM = 1)"
|
||||
+ " AND TO_CHAR(SYSDATE, 'YYYYMMDD') BETWEEN UidStrDte AND UidEndDte"
|
||||
+ " ORDER BY UidCod) WHERE ROWNUM = 1",
|
||||
("d", depCod.Trim()));
|
||||
|
||||
/// <summary>
|
||||
/// 병원 로고(ETC_요양기관명칭_병원로고, :16538) — M_HspMst.HspLgoPth, 무기한 유효
|
||||
/// (HspEndDte='29991231') 행. 원문은 0행이면 Rows(0) 예외였다 — 여기는 빈 값.
|
||||
/// </summary>
|
||||
public string HospitalLogo()
|
||||
=> Scalar(
|
||||
"SELECT v FROM (SELECT HspLgoPth v FROM M_HspMst"
|
||||
+ " WHERE HspEndDte = '29991231' ORDER BY HspCod) WHERE ROWNUM = 1");
|
||||
|
||||
/// <summary>
|
||||
/// 병원 직인(ETC_요양기관명칭_병원직인, :16580) — <b>병원 마스터가 아니라</b>
|
||||
/// 설정 표(DB_REGISTRY, MnuNam='직인경로')의 SelYon 이다(원문에서 M_HspMst.HspStpPth
|
||||
/// 조회는 주석 처리돼 있다). SCHILDREN·HSYUIL·JAH 는 즉시 빈 값 — 미이식 병원이다.
|
||||
/// </summary>
|
||||
public string HospitalStamp()
|
||||
=> Scalar(
|
||||
"SELECT v FROM (SELECT SelYon v FROM DB_REGISTRY"
|
||||
+ " WHERE MnuNam = '직인경로' ORDER BY UidCod) WHERE ROWNUM = 1");
|
||||
|
||||
/// <summary>스칼라 1칸 — 0행이거나 NULL 이면 빈 문자열</summary>
|
||||
private string Scalar(string sql, params (string Name, object Value)[] binds)
|
||||
{
|
||||
using var connection = new OracleConnection(connectionString);
|
||||
connection.Open();
|
||||
using var command = connection.CreateCommand();
|
||||
command.BindByName = true;
|
||||
command.CommandText = sql;
|
||||
foreach (var (name, value) in binds)
|
||||
{
|
||||
command.Parameters.Add(new OracleParameter(name, value));
|
||||
}
|
||||
var result = command.ExecuteScalar();
|
||||
return result is null or DBNull ? string.Empty : result.ToString()!.Trim();
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -25,7 +25,9 @@ public sealed record HisUser(
|
||||
/// <summary>영문 이름(UidEngNam) — 진단서의사_영문의 로그인 의사 갈래</summary>
|
||||
string EnglishName = "",
|
||||
/// <summary>세부전문의번호(UidDtlSpcLic) — 진단서의사세부전문의번호</summary>
|
||||
string DetailSpecialistNumber = "")
|
||||
string DetailSpecialistNumber = "",
|
||||
/// <summary>싸인 이미지 원격 경로(UidImgPth) — 로그인 사용자 싸인 계열</summary>
|
||||
string SignaturePath = "")
|
||||
{
|
||||
/// <summary>미인증 세션(인자 없이 단독 실행) — DB 쓰기가 차단된다</summary>
|
||||
public static readonly HisUser Unknown = new(string.Empty, string.Empty, string.Empty, string.Empty, string.Empty, string.Empty);
|
||||
@@ -85,7 +87,8 @@ public sealed class UserContextStore
|
||||
" NVL(u.UidHspCod,' '), NVL(h.HspInsNam,' '), " +
|
||||
" NVL(u.UidDtrYon,' '), NVL(d.DepOfcNam,' '), " +
|
||||
" NVL(u.UidLicNum,' '), NVL(u.UidSpcLic,' '), NVL(j.DtsCodNam,' '), " +
|
||||
" NVL(d.DepTelNum,' '), NVL(u.UidEngNam,' '), NVL(u.UidDtlSpcLic,' ') " +
|
||||
" NVL(d.DepTelNum,' '), NVL(u.UidEngNam,' '), NVL(u.UidDtlSpcLic,' '), " +
|
||||
" NVL(u.UidImgPth,' ') " +
|
||||
"FROM M_UidMst u " +
|
||||
"LEFT JOIN M_DepMst d ON d.DepCod = u.UidDepCod " +
|
||||
" AND TO_CHAR(SYSDATE,'YYYYMMDD') BETWEEN d.DepStrDte AND d.DepEndDte " +
|
||||
@@ -109,7 +112,8 @@ public sealed class UserContextStore
|
||||
reader.GetString(4).Trim(), reader.GetString(5).Trim(),
|
||||
reader.GetString(6).Trim(), reader.GetString(7).Trim(),
|
||||
reader.GetString(8).Trim(), reader.GetString(9).Trim(), reader.GetString(10).Trim(),
|
||||
reader.GetString(11).Trim(), reader.GetString(12).Trim(), reader.GetString(13).Trim());
|
||||
reader.GetString(11).Trim(), reader.GetString(12).Trim(), reader.GetString(13).Trim(),
|
||||
reader.GetString(14).Trim());
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -223,6 +223,12 @@ public partial class App : Application
|
||||
return Diagnostics.DbSmoke.RunSample(args[1], args[2]);
|
||||
}
|
||||
|
||||
// 싸인·이미지 태그의 전송 수단을 확정한다(값은 안 찍는다)
|
||||
if (args.Length >= 2 && args[0] == "--db-filecfg")
|
||||
{
|
||||
return Diagnostics.DbSmoke.RunFileTransferConfig(args[1]);
|
||||
}
|
||||
|
||||
if (args.Length >= 3 && args[0] == "--db-word-smoke")
|
||||
{
|
||||
return Diagnostics.DbSmoke.RunWordSmoke(args[1], args[2]);
|
||||
|
||||
@@ -229,13 +229,28 @@
|
||||
<Rectangle Fill="{Binding LineBrush}" IsHitTestVisible="False"/>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- 이미지 -->
|
||||
<!-- 이미지 — 해석기가 파일을 찾았으면 그 이미지를, 아니면 자리표시자를 그린다.
|
||||
종이(미리보기·인쇄)에서는 자리표시자 테두리가 보이면 안 되므로 이미지가 있으면
|
||||
테두리 없이 이미지만 남긴다. -->
|
||||
<DataTemplate DataType="{x:Type vmc:PictureBoxViewModel}">
|
||||
<Border Background="#F5F7FA" BorderBrush="#C8D0DA" BorderThickness="1" IsHitTestVisible="False">
|
||||
<TextBlock Text="{Binding Hint}" FontSize="10" Foreground="#8090A0"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
</Border>
|
||||
<Grid>
|
||||
<Border Background="#F5F7FA" BorderBrush="#C8D0DA" BorderThickness="1" IsHitTestVisible="False">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding HasImage}" Value="True">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<TextBlock Text="{Binding Hint}" FontSize="10" Foreground="#8090A0"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
</Border>
|
||||
<Image Source="{Binding Image}" Stretch="{Binding ImageStretch}" IsHitTestVisible="False"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- 계산박스 -->
|
||||
|
||||
@@ -1570,6 +1570,113 @@ public static class DbSmoke
|
||||
}
|
||||
|
||||
/// <summary>테이블 샘플 행 조회(--db-sample) — 읽기 전용, 스키마 규약 확인용</summary>
|
||||
/// <summary>
|
||||
/// 파일 전송 설정 진단(<c>--db-filecfg</c>) — 싸인·이미지 태그가 쓰는 전송 수단을 확정한다.
|
||||
///
|
||||
/// 레거시 <c>clsFileTransfer</c> 는 단일 프로토콜이 아니라 <b>5갈래</b>이고
|
||||
/// (FileServer=1 · Database=2 · FileServerNDataBase=3 · SFTP=4 · FTP=5 ·
|
||||
/// LCTech_Ver_Windows=6 · LCTech_Ver_Linux=7, clsFileTransfer.vb:188-196)
|
||||
/// 어느 갈래인지는 소스가 아니라 <b>병원별 DB 설정</b>이 정한다
|
||||
/// (M_DtlMst 'EMR_FileTransfer' 의 'Type', 기본값 3).
|
||||
///
|
||||
/// 이식 가능성이 여기서 갈린다 — Database(2)만 순수 .NET 으로 재현할 수 있고,
|
||||
/// FileServer 계열은 ActiveX OCX, SFTP 는 WinSCP 어셈블리라 우리 배포 규칙
|
||||
/// (서드파티 DLL 금지)과 충돌한다.
|
||||
///
|
||||
/// <b>비밀값은 찍지 않는다</b> — 어떤 키가 있는지와 값의 길이만 남긴다.
|
||||
/// </summary>
|
||||
public static int RunFileTransferConfig(string reportPath)
|
||||
{
|
||||
var lines = new List<string>();
|
||||
var failed = 0;
|
||||
void Check(string title, bool ok, string detail = "")
|
||||
{
|
||||
lines.Add((ok ? "PASS " : "FAIL ") + title + (detail.Length > 0 ? $" — {detail}" : string.Empty));
|
||||
if (!ok)
|
||||
{
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
try
|
||||
{
|
||||
var config = ConfigService.Current;
|
||||
using var connection = new Oracle.ManagedDataAccess.Client.OracleConnection(config.ConnectionString);
|
||||
connection.Open();
|
||||
|
||||
// ① 전송 모드 — 이 한 줄이 이식 가능성을 가른다
|
||||
var mode = string.Empty;
|
||||
using (var command = connection.CreateCommand())
|
||||
{
|
||||
command.CommandText =
|
||||
"SELECT DtlCod, DtlCodNam FROM M_DtlMst WHERE DtlTblCod = 'EMR_FileTransfer' ORDER BY DtlCod";
|
||||
using var reader = command.ExecuteReader();
|
||||
while (reader.Read())
|
||||
{
|
||||
var key = reader.GetValue(0)?.ToString()?.Trim() ?? string.Empty;
|
||||
var value = reader.IsDBNull(1) ? string.Empty : reader.GetValue(1).ToString()!.Trim();
|
||||
// Type 만 값을 찍는다 — 나머지(IP·Port·User·Pwd)는 접속 정보라 길이만
|
||||
lines.Add(string.Equals(key, "Type", StringComparison.OrdinalIgnoreCase)
|
||||
? $" 설정 {key} = {value}"
|
||||
: $" 설정 {key} = ({value.Length}자, 값은 기록하지 않는다)");
|
||||
if (string.Equals(key, "Type", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
mode = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
var modeName = mode switch
|
||||
{
|
||||
"1" => "FileServer(ActiveX OCX)",
|
||||
"2" => "Database(E_FilInf BLOB)",
|
||||
"3" => "FileServerNDataBase(ActiveX OCX)",
|
||||
"4" => "SFTP(WinSCP 어셈블리)",
|
||||
"5" => "FTP(다운로드 분기 없음 — 항상 실패)",
|
||||
"6" => "LCTech_Windows(ActiveX OCX)",
|
||||
"7" => "LCTech_Linux(ActiveX OCX)",
|
||||
"" => "설정 없음 → 레거시 기본값 3(FileServerNDataBase, ActiveX OCX)",
|
||||
_ => $"알 수 없는 값({mode})",
|
||||
};
|
||||
lines.Add($" 전송 모드: {modeName}");
|
||||
Check("① 전송 설정을 읽는다", true);
|
||||
|
||||
// ② 이미지 원본이 DB 에 있는가 — Database 모드면 여기 실체가 있다
|
||||
var filCount = FirstText(config.ConnectionString, "SELECT COUNT(*) FROM E_FilInf");
|
||||
lines.Add(filCount.Length == 0
|
||||
? " E_FilInf: 없음(ORA-00942) — DB 보관 경로가 이 DB 엔 없다"
|
||||
: $" E_FilInf: {filCount}행");
|
||||
|
||||
// ③ 서명 경로가 실제로 채워져 있는가 — 태그가 값을 낼 수 있는 환자가 있는지
|
||||
var signed = FirstText(config.ConnectionString,
|
||||
"SELECT COUNT(*) FROM M_UidMst WHERE UidImgPth IS NOT NULL AND TRIM(UidImgPth) <> ' '");
|
||||
var sample = FirstText(config.ConnectionString,
|
||||
"SELECT v FROM (SELECT UidImgPth v FROM M_UidMst"
|
||||
+ " WHERE UidImgPth IS NOT NULL AND TRIM(UidImgPth) <> ' ' ORDER BY UidCod) WHERE ROWNUM = 1");
|
||||
lines.Add($" M_UidMst.UidImgPth 채워진 행: {signed}");
|
||||
// 경로 <b>모양</b>만 남긴다 — 사람 이름이 섞일 수 있어 값 자체는 안 찍는다
|
||||
var shape = sample.Length == 0 ? "없음"
|
||||
: sample.StartsWith(@"\\", StringComparison.Ordinal) ? "UNC 공유(\\\\서버\\...)"
|
||||
: sample.Contains(':') ? "드라이브 절대경로(C:\\...)"
|
||||
: sample.Contains('\\') ? "역슬래시 상대경로"
|
||||
: sample.Contains('/') ? "슬래시 상대경로" : "구분자 없음";
|
||||
lines.Add($" 경로 모양: {shape} (길이 {sample.Length}자, 값은 기록하지 않는다)");
|
||||
|
||||
// ④ 병원 로고·직인도 같은 축이다
|
||||
var logo = FirstText(config.ConnectionString,
|
||||
"SELECT COUNT(*) FROM M_HspMst WHERE HspLgoPth IS NOT NULL AND TRIM(HspLgoPth) <> ' '");
|
||||
lines.Add($" M_HspMst.HspLgoPth 채워진 행: {(logo.Length == 0 ? "컬럼 없음" : logo)}");
|
||||
Check("② 이미지 원본·경로 실태를 읽는다", true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Check("① 전송 설정을 읽는다", false, $"{ex.GetType().Name}: {ex.Message.Split('\n')[0]}");
|
||||
}
|
||||
lines.Add(string.Empty);
|
||||
lines.Add(failed == 0 ? "결과: 전건 통과" : $"결과: 실패 {failed}건");
|
||||
System.IO.File.WriteAllText(reportPath, string.Join(Environment.NewLine, lines));
|
||||
Console.WriteLine(string.Join(Environment.NewLine, lines));
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
public static int RunSample(string tableName, string reportPath, int max = 12)
|
||||
{
|
||||
try
|
||||
@@ -2387,6 +2494,59 @@ public static class DbSmoke
|
||||
"RPAD 매칭이나 NATCOD 조인을 의심할 것");
|
||||
}
|
||||
|
||||
// ㊲ 싸인·직인·로고 경로 조회 — 파일서버 전송(ActiveX)은
|
||||
// 옮기지 않았지만 <b>경로 조회는 전부 순수 SQL</b>이다.
|
||||
// 컬럼·조인 이름이 틀리면 여기서 죽는다.
|
||||
try
|
||||
{
|
||||
var signs = new SignatureStore(conn);
|
||||
var probes = new (string Name, Func<string> Run)[]
|
||||
{
|
||||
("집도의", () => owner2 is null ? "-" : signs.SurgeonSignature(owner2.Value).Length.ToString()),
|
||||
("마취(수술)", () => owner2 is null ? "-" : signs.AnesthesiaSignatureBySurgery(owner2.Value).Length.ToString()),
|
||||
("마취(부서)", () => signs.AnesthesiaDepartmentSignature(true).Length.ToString()),
|
||||
("영상의학", () => signs.DepartmentDoctorSignature("XRAY", "20260820").Length.ToString()),
|
||||
("진단검사", () => signs.DepartmentDoctorSignature("LAB", "20260820").Length.ToString()),
|
||||
("외출승인", () => owner2 is null ? "-" : signs.OutingApproverSignature(owner2.Value, false).Length.ToString()),
|
||||
("수간호사", () => signs.WardHeadNurseSignature("0506").Length.ToString()),
|
||||
("병원로고", () => signs.HospitalLogo().Length.ToString()),
|
||||
("병원직인", () => signs.HospitalStamp().Length.ToString()),
|
||||
};
|
||||
var signShapes = new List<string>();
|
||||
var failedSigns = new List<string>();
|
||||
foreach (var (signName, run) in probes)
|
||||
{
|
||||
try
|
||||
{
|
||||
signShapes.Add($"{signName} {run()}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var first = ex.Message.Split('\n')[0];
|
||||
if (first.Contains("ORA-00942"))
|
||||
{
|
||||
signShapes.Add($"{signName} [표없음]");
|
||||
}
|
||||
else
|
||||
{
|
||||
failedSigns.Add($"{signName}: {first}");
|
||||
}
|
||||
}
|
||||
}
|
||||
lines.Add($" 싸인 경로: {string.Join(" · ", signShapes)} (값은 기록하지 않는다)");
|
||||
foreach (var f in failedSigns)
|
||||
{
|
||||
lines.Add($" [진단] {f}");
|
||||
}
|
||||
Check("㊲ 싸인·직인·로고 경로 조회가 예외 없이 돈다", failedSigns.Count == 0,
|
||||
"위 [진단] 줄의 컬럼·조인 이름을 의심할 것");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Check("㊲ 싸인·직인·로고 경로 조회가 예외 없이 돈다", false,
|
||||
$"{ex.GetType().Name}: {ex.Message.Split('\n')[0]}");
|
||||
}
|
||||
|
||||
// 나이 UDF — 주민번호가 있는 차트로 물으면 값이 나와야 한다.
|
||||
// (통합 대표행의 AgeMonth/AGE 파생 컬럼과 같은 식 —
|
||||
// UDF 이름·접두가 틀리면 여기서 바로 죽는다)
|
||||
|
||||
@@ -204,6 +204,16 @@ public static class PatientIdentity
|
||||
return () => connection.Length == 0 ? null : new PatientExtraStore(connection);
|
||||
}
|
||||
|
||||
/// <summary>싸인·이미지 경로 조회 공급자 — 파일서버 전송은 옮기지 않았고 경로만 얻는다</summary>
|
||||
public static Func<SignatureStore?> SignatureStoreSource()
|
||||
{
|
||||
var connection = ConfigService.Current.ConnectionString;
|
||||
return () => connection.Length == 0 ? null : new SignatureStore(connection);
|
||||
}
|
||||
|
||||
/// <summary>HIS 설치 경로 — 레거시 싸인 캐시 폴더(ServerInfo ini 의 Install\Path)</summary>
|
||||
public static string? InstallPath() => SheetMe.Data.Config.ServerInfoReader.InstallPath();
|
||||
|
||||
/// <summary>문맥 저장소 공급자 — 입원과·퇴원과의 시점별 진료과 조회용</summary>
|
||||
public static Func<PatientContextStore?> ContextStoreSource()
|
||||
{
|
||||
|
||||
@@ -76,7 +76,8 @@ public static class PatientSession
|
||||
PatientIdentity.ScheduledSurgerySourceFor(picked),
|
||||
PatientIdentity.PathologyDoctorSource(), PatientIdentity.LabDoctorSource(),
|
||||
PatientIdentity.ExtraStoreSource(), PatientIdentity.ContextStoreSource(),
|
||||
PatientIdentity.BstSourceFor(picked), PatientIdentity.ChartVitalSourceFor(picked))
|
||||
PatientIdentity.BstSourceFor(picked), PatientIdentity.ChartVitalSourceFor(picked),
|
||||
PatientIdentity.SignatureStoreSource(), PatientIdentity.InstallPath())
|
||||
: session;
|
||||
return (tags, new MDataTableRunner(document, Variables()));
|
||||
}
|
||||
|
||||
@@ -228,6 +228,12 @@ public sealed class PatientTagResolver : ITagValueResolver
|
||||
|
||||
private readonly Dictionary<string, IReadOnlyDictionary<string, string>> specialDoctorCache = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>싸인·이미지 경로 조회 공급자 — 태그가 물을 때 만든다</summary>
|
||||
private readonly Func<SignatureStore?>? signatureStoreSource;
|
||||
|
||||
/// <summary>HIS 설치 경로 — 레거시 싸인 캐시 폴더의 뿌리(ServerInfo ini)</summary>
|
||||
private readonly string? installPath;
|
||||
|
||||
/// <summary>환자 부가정보(자기 SQL 7종) 공급자 — 태그가 물을 때 만든다</summary>
|
||||
private readonly Func<PatientExtraStore?>? extraStoreSource;
|
||||
|
||||
@@ -265,7 +271,9 @@ public sealed class PatientTagResolver : ITagValueResolver
|
||||
Func<PatientExtraStore?>? extraStoreSource = null,
|
||||
Func<PatientContextStore?>? contextStoreSource = null,
|
||||
Func<string, string>? bstSource = null,
|
||||
Func<string, string, string[], string, string>? chartVitalSource = null)
|
||||
Func<string, string, string[], string, string>? chartVitalSource = null,
|
||||
Func<SignatureStore?>? signatureStoreSource = null,
|
||||
string? installPath = null)
|
||||
{
|
||||
this.context = context;
|
||||
this.next = next;
|
||||
@@ -286,6 +294,8 @@ public sealed class PatientTagResolver : ITagValueResolver
|
||||
this.contextStoreSource = contextStoreSource;
|
||||
this.bstSource = bstSource;
|
||||
this.chartVitalSource = chartVitalSource;
|
||||
this.signatureStoreSource = signatureStoreSource;
|
||||
this.installPath = installPath;
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -357,6 +367,10 @@ public sealed class PatientTagResolver : ITagValueResolver
|
||||
{
|
||||
return ageValue;
|
||||
}
|
||||
if (FromSignature(tag) is { } signValue)
|
||||
{
|
||||
return signValue;
|
||||
}
|
||||
if (Computed.TryGetValue(tag, out var compute))
|
||||
{
|
||||
var made = compute(context);
|
||||
@@ -1025,6 +1039,134 @@ public sealed class PatientTagResolver : ITagValueResolver
|
||||
: new TagValue(false, "수술일자 값이 형식에 맞지 않습니다");
|
||||
}
|
||||
|
||||
/// <summary><see cref="FromSignature"/> 가 담당하는 이미지 태그 17종</summary>
|
||||
private static readonly HashSet<string> SignatureTags = new(StringComparer.Ordinal)
|
||||
{
|
||||
"OCM_담당의사싸인", "OCM_진단서의사싸인", "OCM_로그인_사용자싸인",
|
||||
"ETC_로그인사용자_서명", "ECT_로그인사용자싸인",
|
||||
"OCM_수술집도의_싸인", "OCM_수술마취의사싸인", "OCM_수술마취의사싸인_수술접수연동",
|
||||
"OCM_Cosign_싸인", "ETC_병리판독의사싸인", "ETC_영상의학과의사싸인", "ETC_진단검사의사싸인",
|
||||
"ETC_요양기관명칭_병원로고", "ETC_요양기관명칭_병원직인",
|
||||
"ETC_외출외박_확인간호사싸인", "ETC_외출외박_확인원무과싸인",
|
||||
"ETC_로그인사용자_병동_수간호사_싸인",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 싸인·직인·로고 17종 — <b>DB 는 경로만 준다</b>. 레거시는 그 경로를 파일서버에서
|
||||
/// 내려받아 로컬 경로를 돌려주지만, 이 병원 설정의 전송 계층은 ActiveX OCX
|
||||
/// (M_DtlMst 'EMR_FileTransfer' Type=1 — 진단 <c>--db-filecfg</c> 실측)라
|
||||
/// 프로토콜이 코드에 없어 .NET 으로 재현할 수 없다(서드파티·ActiveX 배포 금지이기도 하다).
|
||||
///
|
||||
/// 그래서 <b>받지 않고 찾는다</b>: 레거시 EMR 이 같은 단말에 이미 받아 둔 캐시
|
||||
/// 폴더를 먼저 보고, 없으면 DB 경로 자체가 열리는지 본다
|
||||
/// (<see cref="SignatureImagePath"/>). 둘 다 없으면 사유 — 종이에는 빈칸이다.
|
||||
///
|
||||
/// 값(경로)이 나오면 <b>PictureBox 가 그 파일을 그린다</b>(PrintService.WithTagValue).
|
||||
/// </summary>
|
||||
private TagValue? FromSignature(string tag)
|
||||
{
|
||||
if (!SignatureTags.Contains(tag))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (extraCache.TryGetValue(tag, out var cached))
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
var value = Compute();
|
||||
extraCache[tag] = value;
|
||||
return value;
|
||||
|
||||
TagValue Compute()
|
||||
{
|
||||
// ① 원격 경로를 얻는다 — 여기까지는 전부 순수 SQL 이라 이식이 끝난다
|
||||
string remote;
|
||||
try
|
||||
{
|
||||
if (RemotePath(tag) is not { } path)
|
||||
{
|
||||
return new TagValue(false, "이 태그의 이미지 경로 조회가 연결되지 않았습니다");
|
||||
}
|
||||
remote = path;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new TagValue(false, $"이미지 경로 조회에 실패했습니다: {ex.GetType().Name}");
|
||||
}
|
||||
if (remote.Length == 0)
|
||||
{
|
||||
return new TagValue(false, "이 태그가 가리키는 이미지 경로가 등록되어 있지 않습니다");
|
||||
}
|
||||
|
||||
// ② 그 경로의 파일을 이 단말에서 찾는다(레거시 캐시 → 경로 자체)
|
||||
var found = SignatureImagePath.Resolve(remote, installPath);
|
||||
return found.Origin switch
|
||||
{
|
||||
SignatureImageOrigin.None => new TagValue(false,
|
||||
$"이미지 파일을 이 단말에서 찾지 못했습니다({found.FileName})"
|
||||
+ " — 파일서버 전송(ActiveX)은 옮기지 않았습니다"),
|
||||
_ => new TagValue(true, found.LocalPath),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>태그별 원격 경로 — 조회가 연결되지 않았으면 null</summary>
|
||||
private string? RemotePath(string tag)
|
||||
{
|
||||
// 로그인 사용자 계열 3종은 세션이 이미 들고 있다(왕복 0)
|
||||
if (tag is "OCM_로그인_사용자싸인" or "ETC_로그인사용자_서명" or "ECT_로그인사용자싸인")
|
||||
{
|
||||
return UserSession.Current.SignaturePath;
|
||||
}
|
||||
// 담당의는 프리페치한 행에 있다(왕복 0)
|
||||
if (tag == "OCM_담당의사싸인")
|
||||
{
|
||||
return doctor.TryGetValue("UidImgPth", out var v) ? v.Trim() : string.Empty;
|
||||
}
|
||||
// 진단서 의사 = 로그인 의사면 본인, 아니면 담당의(:4904-5017 — 이름 계열과 같은 분기)
|
||||
if (tag == "OCM_진단서의사싸인")
|
||||
{
|
||||
return UserSession.Current.DoctorYn == "Y"
|
||||
? UserSession.Current.SignaturePath
|
||||
: doctor.TryGetValue("UidImgPth", out var v) ? v.Trim() : string.Empty;
|
||||
}
|
||||
// 레거시 확정 결함 — 조회 SELECT 에 UidNam 만 있는데 UidImgPth 를 읽어
|
||||
// 항상 예외→빈 값이었다(:17434-17513). 고치면 없던 싸인이 갑자기 찍힌다.
|
||||
if (tag == "ETC_병리판독의사싸인")
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
// 저장된 기록 키(EmrKey)가 필요한 태그 — 미리보기에는 그 키가 없다
|
||||
if (tag == "OCM_Cosign_싸인")
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
if (signatureStoreSource?.Invoke() is not { } store)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var comNum = PatientContext.Value(context.ComInf, "ComNum");
|
||||
var hasVisit = decimal.TryParse(comNum, out var m);
|
||||
var today = PatientContext.Value(hospital, "ServerToday");
|
||||
var open = PatientContext.Value(hospital, "HspStrDte");
|
||||
return tag switch
|
||||
{
|
||||
"OCM_수술집도의_싸인" => hasVisit ? store.SurgeonSignature(m) : string.Empty,
|
||||
"OCM_수술마취의사싸인_수술접수연동" => hasVisit ? store.AnesthesiaSignatureBySurgery(m) : string.Empty,
|
||||
// 개원일 2021-03-01 이후면 코드로, 그 전이면 부서명 LIKE(:3311-3384)
|
||||
"OCM_수술마취의사싸인" => store.AnesthesiaDepartmentSignature(
|
||||
string.CompareOrdinal(open, "20210301") > 0),
|
||||
"ETC_영상의학과의사싸인" => store.DepartmentDoctorSignature("XRAY", today),
|
||||
"ETC_진단검사의사싸인" => store.DepartmentDoctorSignature("LAB", today),
|
||||
"ETC_외출외박_확인간호사싸인" => hasVisit ? store.OutingApproverSignature(m, false) : string.Empty,
|
||||
"ETC_외출외박_확인원무과싸인" => hasVisit ? store.OutingApproverSignature(m, true) : string.Empty,
|
||||
"ETC_로그인사용자_병동_수간호사_싸인" => store.WardHeadNurseSignature(UserSession.Current.DepCod),
|
||||
"ETC_요양기관명칭_병원로고" => store.HospitalLogo(),
|
||||
"ETC_요양기관명칭_병원직인" => store.HospitalStamp(),
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 나이 계열 7종(bzDataInterface.vb:359-508 + AgeCheck clsCommonLib.vb:665-974).
|
||||
///
|
||||
@@ -1742,6 +1884,14 @@ public sealed class PatientTagResolver : ITagValueResolver
|
||||
"ETC_승인의사", "OCM_수술소독간호사",
|
||||
"OCM_적용진료과", "OCM_적용진료의사", "OCM_전과", "OCM_간호정보조사지_과거력",
|
||||
"OCM_영상촬영이력",
|
||||
// 싸인·직인·로고 17종 — 값은 이미지 파일 경로다(FromSignature)
|
||||
"OCM_담당의사싸인", "OCM_진단서의사싸인", "OCM_로그인_사용자싸인",
|
||||
"ETC_로그인사용자_서명", "ECT_로그인사용자싸인",
|
||||
"OCM_수술집도의_싸인", "OCM_수술마취의사싸인", "OCM_수술마취의사싸인_수술접수연동",
|
||||
"OCM_Cosign_싸인", "ETC_병리판독의사싸인", "ETC_영상의학과의사싸인", "ETC_진단검사의사싸인",
|
||||
"ETC_요양기관명칭_병원로고", "ETC_요양기관명칭_병원직인",
|
||||
"ETC_외출외박_확인간호사싸인", "ETC_외출외박_확인원무과싸인",
|
||||
"ETC_로그인사용자_병동_수간호사_싸인",
|
||||
"OCM_수술주상병", "OCM_수술집도의_진료과", "OCM_수술집도의_그룹진료과",
|
||||
"OCM_수술명칭", "OCM_수술진단명", "OCM_수술처치",
|
||||
"OCM_퇴원약_코드", "OCM_퇴원약_명칭", "OCM_퇴원약_용량", "OCM_퇴원약_횟수",
|
||||
|
||||
@@ -218,6 +218,19 @@ public static class PrintService
|
||||
{
|
||||
return control;
|
||||
}
|
||||
// 이미지류(싸인·직인·로고)의 값은 <b>파일 경로</b>다 — Text 가 아니라 경로 속성에 넣어
|
||||
// 템플릿이 그 파일을 그리게 한다(레거시도 경로 문자열을 SetValue 로 넘겨 그렸다).
|
||||
// 못 찾았으면 아무것도 넣지 않는다 — 자리표시자만 남고 종이에는 글자가 없다.
|
||||
if (control is PictureBoxViewModel)
|
||||
{
|
||||
if (!value.Resolved)
|
||||
{
|
||||
return control;
|
||||
}
|
||||
var picture = control.Model.Clone();
|
||||
picture.Props.SetText("ResolvedImagePath", value.Text);
|
||||
return DocumentMapper.CreateControl(picture, control.EffectiveFont, control.Foreground);
|
||||
}
|
||||
var model = control.Model.Clone();
|
||||
if (value.Resolved && control is CheckBoxViewModel or RadioButtonViewModel)
|
||||
{
|
||||
|
||||
@@ -180,6 +180,59 @@ public sealed class PictureBoxViewModel : ControlViewModel
|
||||
/// <summary>디자인 표시명 — DataInterfaceTag(런타임 바인딩 이미지) 힌트</summary>
|
||||
public string Hint => Prop("DataInterfaceTag") ?? "이미지";
|
||||
|
||||
/// <summary>
|
||||
/// 그릴 이미지 — 해석기가 정한 파일 경로(<c>ResolvedImagePath</c>)를 읽는다.
|
||||
/// 캔버스(편집 화면)에는 그 속성이 없어 null 이고 자리표시자가 남는다.
|
||||
///
|
||||
/// <b>파일을 잠그지 않는다</b>(OnLoad + 스트림 복사). 레거시도 같은 이유로
|
||||
/// Image.FromFile 대신 바이트를 읽어 메모리에서 디코딩한다(MPictureBox.vb:429-572) —
|
||||
/// 잠그면 그 파일을 쓰는 다른 프로그램이 막힌다.
|
||||
/// 못 읽으면 null 이다(깨진 파일·권한) — 레거시도 빈 catch 로 삼켜 빈칸이 됐다.
|
||||
/// </summary>
|
||||
public System.Windows.Media.ImageSource? Image
|
||||
{
|
||||
get
|
||||
{
|
||||
var path = Prop("ResolvedImagePath");
|
||||
if (path is not { Length: > 0 })
|
||||
{
|
||||
return null;
|
||||
}
|
||||
try
|
||||
{
|
||||
var image = new System.Windows.Media.Imaging.BitmapImage();
|
||||
image.BeginInit();
|
||||
image.CacheOption = System.Windows.Media.Imaging.BitmapCacheOption.OnLoad;
|
||||
image.CreateOptions = System.Windows.Media.Imaging.BitmapCreateOptions.IgnoreImageCache;
|
||||
image.UriSource = new Uri(path, UriKind.Absolute);
|
||||
image.EndInit();
|
||||
image.Freeze();
|
||||
return image;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 채우기 방식 — 레거시 SizeMode 대응.
|
||||
///
|
||||
/// <b>인쇄는 레거시가 SizeMode 를 무시하고 컨트롤 사각형에 늘려 그린다</b>
|
||||
/// (MPictureBox.vb:707-783 PrintMe/DrawingControl). 종이를 레거시와 맞추는 것이
|
||||
/// 이 프로그램의 기준이므로 기본을 Fill 로 두고, 비율 유지를 명시한 값만 존중한다.
|
||||
/// </summary>
|
||||
public System.Windows.Media.Stretch ImageStretch => Prop("SizeMode") switch
|
||||
{
|
||||
"Zoom" => System.Windows.Media.Stretch.Uniform,
|
||||
"CenterImage" or "AutoSize" or "Normal" => System.Windows.Media.Stretch.None,
|
||||
_ => System.Windows.Media.Stretch.Fill,
|
||||
};
|
||||
|
||||
/// <summary>이미지가 없을 때만 자리표시자를 보인다</summary>
|
||||
public bool HasImage => Image is not null;
|
||||
|
||||
public PictureBoxViewModel(ControlElement model) : base(model) { }
|
||||
}
|
||||
|
||||
|
||||
@@ -233,7 +233,8 @@ public partial class PreviewWindow : Window
|
||||
PatientIdentity.ScheduledSurgerySourceFor(picked),
|
||||
PatientIdentity.PathologyDoctorSource(), PatientIdentity.LabDoctorSource(),
|
||||
PatientIdentity.ExtraStoreSource(), PatientIdentity.ContextStoreSource(),
|
||||
PatientIdentity.BstSourceFor(picked), PatientIdentity.ChartVitalSourceFor(picked));
|
||||
PatientIdentity.BstSourceFor(picked), PatientIdentity.ChartVitalSourceFor(picked),
|
||||
PatientIdentity.SignatureStoreSource(), PatientIdentity.InstallPath());
|
||||
// 러너를 <b>새로</b> 만든다. 같은 러너를 재사용하면 옛 환자의 조회 결과가 캐시에 남아
|
||||
// 환자를 바꿨는데 표는 그대로가 된다 — 태그만 바뀌고 표는 안 바뀌면 아무도 눈치채지 못한다.
|
||||
fields = new MDataTableRunner(designer.Document,
|
||||
|
||||
Reference in New Issue
Block a user