싸인 이미지 SMB 직접 읽기 — 공유 매핑 구현(서버측 SMB 개방 대기)
사용자 결정(②SMB 직접 접근)에 따라 구현. 실측 확인:
- 파일서버 = DB 서버와 같은 호스트, 전송 데몬 포트 2002 는 열림
- <b>SMB 445·139 는 닫힘</b>, UNC·관리공유 접근 모두 실패 → 서버측 조치 필요
- 서명 경로는 서버측 절대경로 D:\MsystechHIS\{사이트}\... (설정 루트 E:\msys\ 와 다름)
그래서 공유가 열리는 즉시 동작하도록 매핑을 넣었다:
Images:SignatureShareMap 에 "서버접두=UNC접두" 를 두면 해석 순서가
①레거시 캐시 ②공유(UNC) ③직접 경로가 된다. 접두는 대소문자 무시로
<b>긴 것부터</b> 맞추고(짧은 접두가 정확한 대응을 묻지 않게), '=' 없는
설정 오타는 무시한다(반쪽 경로를 만들면 조용히 틀린다). 자격증명은
설정에 담지 않는다 — 공유 접근은 단말 로그인 계정으로 붙는다.
진단 --db-filecfg 확장: 접속 <b>대상</b>(Type·IP·Port·FileDirectory)과
경로 접두 분포를 찍고(자격증명은 길이만), 공유 매핑별 접근 가능 여부와
파일서버 SMB(445) 개방 여부를 판정해 무엇을 열어야 하는지 알려 준다.
- dotnet test 358/358 (매핑 4건 추가: 치환·긴 접두 우선·오타 무시·해석 순서)
- --edit-smoke 실패 0 · --db-patient ①~㊲ 전건 통과
- --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
a0333791b6
commit
345b2e1da1
@@ -9,6 +9,9 @@ public enum SignatureImageOrigin
|
||||
/// <summary>레거시 EMR 이 이미 내려받아 둔 캐시 파일</summary>
|
||||
LegacyCache,
|
||||
|
||||
/// <summary>파일서버의 공유 폴더(UNC)로 직접 읽었다</summary>
|
||||
Share,
|
||||
|
||||
/// <summary>DB 가 준 경로 자체가 이 단말에서 바로 열린다</summary>
|
||||
DirectPath,
|
||||
}
|
||||
@@ -51,8 +54,12 @@ public static class SignatureImagePath
|
||||
/// <param name="remotePath">DB 가 준 경로(M_UidMst.UidImgPth 등)</param>
|
||||
/// <param name="installPath">HIS 설치 경로(ServerInfo ini 의 Install\Path)</param>
|
||||
/// <param name="exists">파일 존재 판정 — 테스트가 갈아 끼운다</param>
|
||||
/// <param name="shareMap">
|
||||
/// 서버 경로 → UNC 공유 대응(<c>"D:\MsystechHIS=\\서버\MsystechHIS"</c> 형태).
|
||||
/// 접두가 <b>긴 것부터</b> 맞춰 본다 — 짧은 접두가 먼저 걸리면 더 정확한 대응이 묻힌다.
|
||||
/// </param>
|
||||
public static SignatureImageResult Resolve(string? remotePath, string? installPath,
|
||||
Func<string, bool>? exists = null)
|
||||
Func<string, bool>? exists = null, IReadOnlyList<string>? shareMap = null)
|
||||
{
|
||||
var remote = (remotePath ?? string.Empty).Trim();
|
||||
if (remote.Length == 0)
|
||||
@@ -72,13 +79,54 @@ public static class SignatureImagePath
|
||||
return new SignatureImageResult(SignatureImageOrigin.LegacyCache, cached, fileName);
|
||||
}
|
||||
}
|
||||
// 캐시에 없으면 원격 경로가 이 단말에서 바로 열리는지 본다 —
|
||||
// 이 병원의 저장 형태는 드라이브 절대경로다(진단 --db-filecfg 실측)
|
||||
// 캐시에 없으면 파일서버 공유(UNC)로 직접 읽는다 — 설정된 접두 대응을 쓴다.
|
||||
// 이 경로가 이 작업의 목적이다: OCX 전송을 재현하는 대신 같은 파일을 공유로 본다.
|
||||
if (MapToShare(remote, shareMap) is { Length: > 0 } shared && probe(shared))
|
||||
{
|
||||
return new SignatureImageResult(SignatureImageOrigin.Share, shared, fileName);
|
||||
}
|
||||
// 그래도 없으면 원격 경로가 이 단말에서 그대로 열리는지 본다
|
||||
// (서버에서 실행하거나 같은 구조로 마운트된 단말)
|
||||
if (probe(remote))
|
||||
{
|
||||
return new SignatureImageResult(SignatureImageOrigin.DirectPath, remote, fileName);
|
||||
}
|
||||
return new SignatureImageResult(SignatureImageOrigin.None, string.Empty, fileName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 서버 경로를 UNC 로 바꾼다 — 대응이 없으면 빈 문자열.
|
||||
/// 대소문자를 무시하고, 접두가 <b>긴 것부터</b> 맞춘다.
|
||||
/// </summary>
|
||||
public static string MapToShare(string? remotePath, IReadOnlyList<string>? shareMap)
|
||||
{
|
||||
var remote = (remotePath ?? string.Empty).Trim();
|
||||
if (remote.Length == 0 || shareMap is null || shareMap.Count == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
var best = string.Empty;
|
||||
var bestLen = -1;
|
||||
foreach (var entry in shareMap)
|
||||
{
|
||||
var at = entry.IndexOf('=', StringComparison.Ordinal);
|
||||
if (at <= 0 || at == entry.Length - 1)
|
||||
{
|
||||
continue; // "서버접두=UNC접두" 가 아니면 무시한다(설정 오타를 값으로 바꾸지 않는다)
|
||||
}
|
||||
var from = entry[..at].Trim().TrimEnd('\\');
|
||||
var to = entry[(at + 1)..].Trim().TrimEnd('\\');
|
||||
if (from.Length == 0 || to.Length == 0 || from.Length <= bestLen)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (remote.StartsWith(from + "\\", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
best = to + remote[from.Length..];
|
||||
bestLen = from.Length;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -43,6 +43,20 @@ public sealed class DataConfig
|
||||
/// 여기 담는 것은 설정값(UidCod 문자열)뿐이며, 해석된 사용자 정보는 담지 않는다.
|
||||
/// </summary>
|
||||
public string DevUidCod { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 싸인·직인 이미지의 <b>서버 경로 → UNC 공유</b> 대응 — <c>"서버접두=UNC접두"</c> 목록.
|
||||
/// 예: <c>D:\MsystechHIS=\\192.168.100.222\MsystechHIS</c>
|
||||
///
|
||||
/// <b>왜 설정인가.</b> DB 는 이미지 위치를 파일서버의 <b>로컬 절대경로</b>로 들고 있고
|
||||
/// (실측: <c>D:\MsystechHIS\{사이트}\…</c>), 레거시는 그 경로를 ActiveX OCX 데몬
|
||||
/// (기본 포트 2002)에 넘겨 받아 온다. 그 프로토콜은 재현할 수 없으므로 우리는 같은 파일을
|
||||
/// <b>공유 폴더로 직접</b> 읽는다 — 접두 대응은 병원마다 다르니 코드가 아니라 설정이다.
|
||||
///
|
||||
/// 비어 있으면(기본) 매핑을 시도하지 않는다 — 지금까지와 같이 로컬 캐시만 본다.
|
||||
/// <b>자격증명은 담지 않는다</b>: 공유 접근 권한은 단말 로그인 계정으로 해결한다.
|
||||
/// </summary>
|
||||
public string[] SignatureShareMap { get; set; } = Array.Empty<string>();
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
@@ -1614,10 +1614,13 @@ public static class DbSmoke
|
||||
{
|
||||
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}자, 값은 기록하지 않는다)");
|
||||
// 접속 <b>대상</b>(Type·IP·Port·FileDirectory)은 값을 찍는다 — SMB 직접 접근
|
||||
// 가능성을 판단하려면 주소와 공유 이름이 필요하다.
|
||||
// <b>자격증명(User·Pwd)은 길이만</b> — 값은 어디에도 남기지 않는다.
|
||||
var secret = key is "Pwd" or "User" or "Password" or "Account";
|
||||
lines.Add(secret
|
||||
? $" 설정 {key} = ({value.Length}자, 값은 기록하지 않는다)"
|
||||
: $" 설정 {key} = {value}");
|
||||
if (string.Equals(key, "Type", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
mode = value;
|
||||
@@ -1652,6 +1655,23 @@ public static class DbSmoke
|
||||
"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}");
|
||||
// 폴더 구조는 개인정보가 아니다 — 파일명(사용자 코드가 섞일 수 있다)만 뗀다.
|
||||
// 이 접두가 FileDirectory(서버측 루트)와 어떻게 대응하는지가 SMB 매핑의 근거다.
|
||||
using (var command = connection.CreateCommand())
|
||||
{
|
||||
command.CommandText =
|
||||
"SELECT folder, COUNT(*) c FROM (SELECT SUBSTR(UidImgPth, 1,"
|
||||
+ " INSTR(UidImgPth, '\\', -1)) folder FROM M_UidMst"
|
||||
+ " WHERE UidImgPth IS NOT NULL AND TRIM(UidImgPth) <> ' ')"
|
||||
+ " GROUP BY folder ORDER BY COUNT(*) DESC";
|
||||
using var reader = command.ExecuteReader();
|
||||
var shown = 0;
|
||||
while (reader.Read() && shown < 5)
|
||||
{
|
||||
lines.Add($" 경로 접두: {reader.GetValue(0)} ({reader.GetValue(1)}건)");
|
||||
shown++;
|
||||
}
|
||||
}
|
||||
// 경로 <b>모양</b>만 남긴다 — 사람 이름이 섞일 수 있어 값 자체는 안 찍는다
|
||||
var shape = sample.Length == 0 ? "없음"
|
||||
: sample.StartsWith(@"\\", StringComparison.Ordinal) ? "UNC 공유(\\\\서버\\...)"
|
||||
@@ -1660,7 +1680,58 @@ public static class DbSmoke
|
||||
: sample.Contains('/') ? "슬래시 상대경로" : "구분자 없음";
|
||||
lines.Add($" 경로 모양: {shape} (길이 {sample.Length}자, 값은 기록하지 않는다)");
|
||||
|
||||
// ④ 병원 로고·직인도 같은 축이다
|
||||
// ④ 공유(SMB) 직접 접근이 실제로 되는가 — OCX 전송을 대신할 유일한 경로다.
|
||||
// 설정이 없으면 "무엇을 설정해야 하는지"를 알려 준다.
|
||||
var map = config.SignatureShareMap;
|
||||
if (map.Length == 0)
|
||||
{
|
||||
lines.Add(" 공유 매핑: 설정 없음(Images:SignatureShareMap)");
|
||||
lines.Add(" → 예: \"D:\\MsystechHIS=\\\\<파일서버>\\<공유이름>\" (서버 경로 접두 = UNC 접두)");
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var entry in map)
|
||||
{
|
||||
var at = entry.IndexOf('=', StringComparison.Ordinal);
|
||||
var unc = at > 0 ? entry[(at + 1)..].Trim() : string.Empty;
|
||||
var reachable = unc.Length > 0 && System.IO.Directory.Exists(unc);
|
||||
lines.Add($" 공유 매핑: {entry} → 접근 {(reachable ? "가능" : "불가")}");
|
||||
}
|
||||
// 실제 경로 하나로 끝까지 확인한다 — 매핑이 맞아도 파일이 없으면 소용없다
|
||||
if (sample.Length > 0)
|
||||
{
|
||||
var mapped = SheetMe.Core.Catalog.SignatureImagePath.MapToShare(sample, map);
|
||||
lines.Add(mapped.Length == 0
|
||||
? " 표본 경로: 매핑되는 접두가 없다(접두를 실제 저장 경로에 맞출 것)"
|
||||
: $" 표본 경로 매핑 결과: 파일 {(System.IO.File.Exists(mapped) ? "있음" : "없음")}"
|
||||
+ " (경로는 기록하지 않는다)");
|
||||
}
|
||||
}
|
||||
// 파일서버가 SMB 를 열어 두었는지 — 닫혀 있으면 매핑을 아무리 맞춰도 안 된다.
|
||||
// 주소는 접속 설정의 IP 를 그대로 쓴다(자격증명은 쓰지 않는다 — 단말 계정으로 붙는다).
|
||||
var serverIp = FirstText(config.ConnectionString,
|
||||
"SELECT DtlCodNam FROM M_DtlMst WHERE DtlTblCod = 'EMR_FileTransfer' AND DtlCod = 'IP'");
|
||||
if (serverIp.Length > 0)
|
||||
{
|
||||
var open = false;
|
||||
try
|
||||
{
|
||||
using var probe = new System.Net.Sockets.TcpClient();
|
||||
open = probe.ConnectAsync(serverIp, 445).Wait(TimeSpan.FromSeconds(3));
|
||||
}
|
||||
catch
|
||||
{
|
||||
open = false;
|
||||
}
|
||||
lines.Add($" 파일서버 {serverIp} SMB(445): {(open ? "열림" : "닫힘")}");
|
||||
if (!open)
|
||||
{
|
||||
lines.Add(" → 서버에서 SMB 공유를 열어야 이미지 직접 읽기가 가능하다"
|
||||
+ "(전송 데몬 포트만 열려 있으면 우리는 못 받는다)");
|
||||
}
|
||||
}
|
||||
|
||||
// ⑤ 병원 로고·직인도 같은 축이다
|
||||
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)}");
|
||||
|
||||
@@ -50,6 +50,15 @@ public static class ConfigLoader
|
||||
config.DevUidCod = root["His:DevUidCod"] ?? string.Empty;
|
||||
config.GridSize = ReadInt(root["Designer:GridSize"], 4);
|
||||
config.SnapThreshold = ReadInt(root["Designer:SnapThreshold"], 6);
|
||||
// 싸인 이미지 공유 매핑 — 배열(Images:SignatureShareMap:0…) 또는 세미콜론 한 줄 둘 다 받는다.
|
||||
// 병원 배포는 ini·json 을 손으로 고치는 일이 많아 한 줄 형태가 실수를 줄인다.
|
||||
config.SignatureShareMap = root.GetSection("Images:SignatureShareMap").GetChildren()
|
||||
.Select(c => c.Value ?? string.Empty)
|
||||
.Concat((root["Images:SignatureShareMap"] ?? string.Empty)
|
||||
.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
.Where(v => v.Contains('=', StringComparison.Ordinal))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
@@ -214,6 +214,9 @@ public static class PatientIdentity
|
||||
/// <summary>HIS 설치 경로 — 레거시 싸인 캐시 폴더(ServerInfo ini 의 Install\Path)</summary>
|
||||
public static string? InstallPath() => SheetMe.Data.Config.ServerInfoReader.InstallPath();
|
||||
|
||||
/// <summary>파일서버 공유 매핑 — 설정(Images:SignatureShareMap)</summary>
|
||||
public static IReadOnlyList<string> ShareMap() => ConfigService.Current.SignatureShareMap;
|
||||
|
||||
/// <summary>문맥 저장소 공급자 — 입원과·퇴원과의 시점별 진료과 조회용</summary>
|
||||
public static Func<PatientContextStore?> ContextStoreSource()
|
||||
{
|
||||
|
||||
@@ -77,7 +77,8 @@ public static class PatientSession
|
||||
PatientIdentity.PathologyDoctorSource(), PatientIdentity.LabDoctorSource(),
|
||||
PatientIdentity.ExtraStoreSource(), PatientIdentity.ContextStoreSource(),
|
||||
PatientIdentity.BstSourceFor(picked), PatientIdentity.ChartVitalSourceFor(picked),
|
||||
PatientIdentity.SignatureStoreSource(), PatientIdentity.InstallPath())
|
||||
PatientIdentity.SignatureStoreSource(), PatientIdentity.InstallPath(),
|
||||
PatientIdentity.ShareMap())
|
||||
: session;
|
||||
return (tags, new MDataTableRunner(document, Variables()));
|
||||
}
|
||||
|
||||
@@ -234,6 +234,9 @@ public sealed class PatientTagResolver : ITagValueResolver
|
||||
/// <summary>HIS 설치 경로 — 레거시 싸인 캐시 폴더의 뿌리(ServerInfo ini)</summary>
|
||||
private readonly string? installPath;
|
||||
|
||||
/// <summary>파일서버 공유 매핑(서버경로=UNC) — 설정에서 온다</summary>
|
||||
private readonly IReadOnlyList<string>? shareMap;
|
||||
|
||||
/// <summary>환자 부가정보(자기 SQL 7종) 공급자 — 태그가 물을 때 만든다</summary>
|
||||
private readonly Func<PatientExtraStore?>? extraStoreSource;
|
||||
|
||||
@@ -273,7 +276,8 @@ public sealed class PatientTagResolver : ITagValueResolver
|
||||
Func<string, string>? bstSource = null,
|
||||
Func<string, string, string[], string, string>? chartVitalSource = null,
|
||||
Func<SignatureStore?>? signatureStoreSource = null,
|
||||
string? installPath = null)
|
||||
string? installPath = null,
|
||||
IReadOnlyList<string>? shareMap = null)
|
||||
{
|
||||
this.context = context;
|
||||
this.next = next;
|
||||
@@ -296,6 +300,7 @@ public sealed class PatientTagResolver : ITagValueResolver
|
||||
this.chartVitalSource = chartVitalSource;
|
||||
this.signatureStoreSource = signatureStoreSource;
|
||||
this.installPath = installPath;
|
||||
this.shareMap = shareMap;
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -1099,7 +1104,7 @@ public sealed class PatientTagResolver : ITagValueResolver
|
||||
}
|
||||
|
||||
// ② 그 경로의 파일을 이 단말에서 찾는다(레거시 캐시 → 경로 자체)
|
||||
var found = SignatureImagePath.Resolve(remote, installPath);
|
||||
var found = SignatureImagePath.Resolve(remote, installPath, null, shareMap);
|
||||
return found.Origin switch
|
||||
{
|
||||
SignatureImageOrigin.None => new TagValue(false,
|
||||
|
||||
@@ -234,7 +234,8 @@ public partial class PreviewWindow : Window
|
||||
PatientIdentity.PathologyDoctorSource(), PatientIdentity.LabDoctorSource(),
|
||||
PatientIdentity.ExtraStoreSource(), PatientIdentity.ContextStoreSource(),
|
||||
PatientIdentity.BstSourceFor(picked), PatientIdentity.ChartVitalSourceFor(picked),
|
||||
PatientIdentity.SignatureStoreSource(), PatientIdentity.InstallPath());
|
||||
PatientIdentity.SignatureStoreSource(), PatientIdentity.InstallPath(),
|
||||
PatientIdentity.ShareMap());
|
||||
// 러너를 <b>새로</b> 만든다. 같은 러너를 재사용하면 옛 환자의 조회 결과가 캐시에 남아
|
||||
// 환자를 바꿨는데 표는 그대로가 된다 — 태그만 바뀌고 표는 안 바뀌면 아무도 눈치채지 못한다.
|
||||
fields = new MDataTableRunner(designer.Document,
|
||||
|
||||
@@ -11,5 +11,10 @@
|
||||
"Designer": {
|
||||
"GridSize": 4,
|
||||
"SnapThreshold": 6
|
||||
},
|
||||
"Images": {
|
||||
"//": "싸인·직인 이미지를 파일서버 공유(UNC)에서 직접 읽기 위한 경로 대응. DB 는 서버측 절대경로(예: D:\\MsystechHIS\\002\\...)를 들고 있고, 레거시는 그 경로를 ActiveX 전송 데몬으로 받아 온다 — 우리는 같은 파일을 공유로 읽는다. 병원마다 접두가 다르므로 설치 시 채운다. 진단 --db-filecfg 가 현재 상태와 필요한 설정을 알려 준다.",
|
||||
"//예시": "\"SignatureShareMap\": [ \"D:\\\\MsystechHIS=\\\\\\\\192.168.100.222\\\\MsystechHIS\" ] 또는 한 줄로 \"D:\\\\MsystechHIS=\\\\\\\\서버\\\\공유;E:\\\\msys=\\\\\\\\서버\\\\msys\"",
|
||||
"SignatureShareMap": []
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user