싸인 이미지 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
@@ -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