From 345b2e1da189117f88d3cf05b744bee3539ecf62 Mon Sep 17 00:00:00 2001 From: Msystech Date: Thu, 20 Aug 2026 12:40:56 +0900 Subject: [PATCH] =?UTF-8?q?=EC=8B=B8=EC=9D=B8=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20SMB=20=EC=A7=81=EC=A0=91=20=EC=9D=BD=EA=B8=B0=20?= =?UTF-8?q?=E2=80=94=20=EA=B3=B5=EC=9C=A0=20=EB=A7=A4=ED=95=91=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84(=EC=84=9C=EB=B2=84=EC=B8=A1=20SMB=20=EA=B0=9C?= =?UTF-8?q?=EB=B0=A9=20=EB=8C=80=EA=B8=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자 결정(②SMB 직접 접근)에 따라 구현. 실측 확인: - 파일서버 = DB 서버와 같은 호스트, 전송 데몬 포트 2002 는 열림 - SMB 445·139 는 닫힘, UNC·관리공유 접근 모두 실패 → 서버측 조치 필요 - 서명 경로는 서버측 절대경로 D:\MsystechHIS\{사이트}\... (설정 루트 E:\msys\ 와 다름) 그래서 공유가 열리는 즉시 동작하도록 매핑을 넣었다: Images:SignatureShareMap 에 "서버접두=UNC접두" 를 두면 해석 순서가 ①레거시 캐시 ②공유(UNC) ③직접 경로가 된다. 접두는 대소문자 무시로 긴 것부터 맞추고(짧은 접두가 정확한 대응을 묻지 않게), '=' 없는 설정 오타는 무시한다(반쪽 경로를 만들면 조용히 틀린다). 자격증명은 설정에 담지 않는다 — 공유 접근은 단말 로그인 계정으로 붙는다. 진단 --db-filecfg 확장: 접속 대상(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 --- .../Catalog/SignatureImagePath.cs | 54 ++++++++++++- src/SheetMe.Data/Config/DataConfig.cs | 14 ++++ src/SheetMe.Designer/Diagnostics/DbSmoke.cs | 81 +++++++++++++++++-- src/SheetMe.Designer/Services/ConfigLoader.cs | 9 +++ .../Services/PatientIdentity.cs | 3 + .../Services/PatientSession.cs | 3 +- .../Services/PatientTagResolver.cs | 9 ++- .../Views/PreviewWindow.xaml.cs | 3 +- src/SheetMe.Designer/appsettings.json | 5 ++ .../SignatureImagePathTests.cs | 45 +++++++++++ 10 files changed, 214 insertions(+), 12 deletions(-) diff --git a/src/SheetMe.Core/Catalog/SignatureImagePath.cs b/src/SheetMe.Core/Catalog/SignatureImagePath.cs index 3aa48ac..976120e 100644 --- a/src/SheetMe.Core/Catalog/SignatureImagePath.cs +++ b/src/SheetMe.Core/Catalog/SignatureImagePath.cs @@ -9,6 +9,9 @@ public enum SignatureImageOrigin /// 레거시 EMR 이 이미 내려받아 둔 캐시 파일 LegacyCache, + /// 파일서버의 공유 폴더(UNC)로 직접 읽었다 + Share, + /// DB 가 준 경로 자체가 이 단말에서 바로 열린다 DirectPath, } @@ -51,8 +54,12 @@ public static class SignatureImagePath /// DB 가 준 경로(M_UidMst.UidImgPth 등) /// HIS 설치 경로(ServerInfo ini 의 Install\Path) /// 파일 존재 판정 — 테스트가 갈아 끼운다 + /// + /// 서버 경로 → UNC 공유 대응("D:\MsystechHIS=\\서버\MsystechHIS" 형태). + /// 접두가 긴 것부터 맞춰 본다 — 짧은 접두가 먼저 걸리면 더 정확한 대응이 묻힌다. + /// public static SignatureImageResult Resolve(string? remotePath, string? installPath, - Func? exists = null) + Func? exists = null, IReadOnlyList? 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); } + + /// + /// 서버 경로를 UNC 로 바꾼다 — 대응이 없으면 빈 문자열. + /// 대소문자를 무시하고, 접두가 긴 것부터 맞춘다. + /// + public static string MapToShare(string? remotePath, IReadOnlyList? 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 } diff --git a/src/SheetMe.Data/Config/DataConfig.cs b/src/SheetMe.Data/Config/DataConfig.cs index 43e06de..272b46f 100644 --- a/src/SheetMe.Data/Config/DataConfig.cs +++ b/src/SheetMe.Data/Config/DataConfig.cs @@ -43,6 +43,20 @@ public sealed class DataConfig /// 여기 담는 것은 설정값(UidCod 문자열)뿐이며, 해석된 사용자 정보는 담지 않는다. /// public string DevUidCod { get; set; } = string.Empty; + + /// + /// 싸인·직인 이미지의 서버 경로 → UNC 공유 대응 — "서버접두=UNC접두" 목록. + /// 예: D:\MsystechHIS=\\192.168.100.222\MsystechHIS + /// + /// 왜 설정인가. DB 는 이미지 위치를 파일서버의 로컬 절대경로로 들고 있고 + /// (실측: D:\MsystechHIS\{사이트}\…), 레거시는 그 경로를 ActiveX OCX 데몬 + /// (기본 포트 2002)에 넘겨 받아 온다. 그 프로토콜은 재현할 수 없으므로 우리는 같은 파일을 + /// 공유 폴더로 직접 읽는다 — 접두 대응은 병원마다 다르니 코드가 아니라 설정이다. + /// + /// 비어 있으면(기본) 매핑을 시도하지 않는다 — 지금까지와 같이 로컬 캐시만 본다. + /// 자격증명은 담지 않는다: 공유 접근 권한은 단말 로그인 계정으로 해결한다. + /// + public string[] SignatureShareMap { get; set; } = Array.Empty(); #endregion #region Methods diff --git a/src/SheetMe.Designer/Diagnostics/DbSmoke.cs b/src/SheetMe.Designer/Diagnostics/DbSmoke.cs index a75de89..9ffe707 100644 --- a/src/SheetMe.Designer/Diagnostics/DbSmoke.cs +++ b/src/SheetMe.Designer/Diagnostics/DbSmoke.cs @@ -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}자, 값은 기록하지 않는다)"); + // 접속 대상(Type·IP·Port·FileDirectory)은 값을 찍는다 — SMB 직접 접근 + // 가능성을 판단하려면 주소와 공유 이름이 필요하다. + // 자격증명(User·Pwd)은 길이만 — 값은 어디에도 남기지 않는다. + 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++; + } + } // 경로 모양만 남긴다 — 사람 이름이 섞일 수 있어 값 자체는 안 찍는다 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)}"); diff --git a/src/SheetMe.Designer/Services/ConfigLoader.cs b/src/SheetMe.Designer/Services/ConfigLoader.cs index f25af14..eb1d04e 100644 --- a/src/SheetMe.Designer/Services/ConfigLoader.cs +++ b/src/SheetMe.Designer/Services/ConfigLoader.cs @@ -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; } diff --git a/src/SheetMe.Designer/Services/PatientIdentity.cs b/src/SheetMe.Designer/Services/PatientIdentity.cs index 14014c8..4c7ef11 100644 --- a/src/SheetMe.Designer/Services/PatientIdentity.cs +++ b/src/SheetMe.Designer/Services/PatientIdentity.cs @@ -214,6 +214,9 @@ public static class PatientIdentity /// HIS 설치 경로 — 레거시 싸인 캐시 폴더(ServerInfo ini 의 Install\Path) public static string? InstallPath() => SheetMe.Data.Config.ServerInfoReader.InstallPath(); + /// 파일서버 공유 매핑 — 설정(Images:SignatureShareMap) + public static IReadOnlyList ShareMap() => ConfigService.Current.SignatureShareMap; + /// 문맥 저장소 공급자 — 입원과·퇴원과의 시점별 진료과 조회용 public static Func ContextStoreSource() { diff --git a/src/SheetMe.Designer/Services/PatientSession.cs b/src/SheetMe.Designer/Services/PatientSession.cs index 93e80d5..eaf98a7 100644 --- a/src/SheetMe.Designer/Services/PatientSession.cs +++ b/src/SheetMe.Designer/Services/PatientSession.cs @@ -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())); } diff --git a/src/SheetMe.Designer/Services/PatientTagResolver.cs b/src/SheetMe.Designer/Services/PatientTagResolver.cs index 7152f09..27f3774 100644 --- a/src/SheetMe.Designer/Services/PatientTagResolver.cs +++ b/src/SheetMe.Designer/Services/PatientTagResolver.cs @@ -234,6 +234,9 @@ public sealed class PatientTagResolver : ITagValueResolver /// HIS 설치 경로 — 레거시 싸인 캐시 폴더의 뿌리(ServerInfo ini) private readonly string? installPath; + /// 파일서버 공유 매핑(서버경로=UNC) — 설정에서 온다 + private readonly IReadOnlyList? shareMap; + /// 환자 부가정보(자기 SQL 7종) 공급자 — 태그가 물을 때 만든다 private readonly Func? extraStoreSource; @@ -273,7 +276,8 @@ public sealed class PatientTagResolver : ITagValueResolver Func? bstSource = null, Func? chartVitalSource = null, Func? signatureStoreSource = null, - string? installPath = null) + string? installPath = null, + IReadOnlyList? 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, diff --git a/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs b/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs index 5c59da8..92542c9 100644 --- a/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs +++ b/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs @@ -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()); // 러너를 새로 만든다. 같은 러너를 재사용하면 옛 환자의 조회 결과가 캐시에 남아 // 환자를 바꿨는데 표는 그대로가 된다 — 태그만 바뀌고 표는 안 바뀌면 아무도 눈치채지 못한다. fields = new MDataTableRunner(designer.Document, diff --git a/src/SheetMe.Designer/appsettings.json b/src/SheetMe.Designer/appsettings.json index 1a5d925..3ee9a2d 100644 --- a/src/SheetMe.Designer/appsettings.json +++ b/src/SheetMe.Designer/appsettings.json @@ -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": [] } } diff --git a/tests/SheetMe.Core.Tests/SignatureImagePathTests.cs b/tests/SheetMe.Core.Tests/SignatureImagePathTests.cs index ee17e54..4600414 100644 --- a/tests/SheetMe.Core.Tests/SignatureImagePathTests.cs +++ b/tests/SheetMe.Core.Tests/SignatureImagePathTests.cs @@ -66,6 +66,51 @@ public sealed class SignatureImagePathTests Assert.AreEqual(SignatureImageOrigin.DirectPath, result.Origin); } + [TestMethod] + public void 공유_매핑이_서버경로를_UNC_로_바꾼다() + { + var map = new[] { @"D:\MsystechHIS=\\fs\MsystechHIS" }; + Assert.AreEqual(@"\\fs\MsystechHIS\002\a.tif", + SignatureImagePath.MapToShare(@"D:\MsystechHIS\002\a.tif", map)); + // 대소문자 무시 — 병원 데이터의 표기가 제각각이다 + Assert.AreEqual(@"\\fs\MsystechHIS\002\a.tif", + SignatureImagePath.MapToShare(@"d:\msystechhis\002\a.tif", map)); + // 대응 없는 접두는 빈 문자열(엉뚱한 경로를 만들지 않는다) + Assert.AreEqual(string.Empty, SignatureImagePath.MapToShare(@"E:\other\a.tif", map)); + } + + [TestMethod] + public void 접두가_겹치면_긴_쪽이_이긴다() + { + // 짧은 접두가 먼저 걸리면 더 정확한 대응이 묻힌다 + var map = new[] { @"D:\HIS=\\fs\all", @"D:\HIS\Sign=\\fs\sign" }; + Assert.AreEqual(@"\\fs\sign\a.tif", SignatureImagePath.MapToShare(@"D:\HIS\Sign\a.tif", map)); + Assert.AreEqual(@"\\fs\all\Etc\b.tif", SignatureImagePath.MapToShare(@"D:\HIS\Etc\b.tif", map)); + } + + [TestMethod] + public void 설정_오타는_값으로_바꾸지_않는다() + { + // '=' 가 없거나 한쪽이 비면 그 항목은 무시한다 — 반쪽 경로를 만들면 조용히 틀린다 + var map = new[] { "그냥문자열", @"D:\HIS=", @"=\\fs\x" }; + Assert.AreEqual(string.Empty, SignatureImagePath.MapToShare(@"D:\HIS\a.tif", map)); + } + + [TestMethod] + public void 캐시가_없으면_공유를_먼저_보고_직접경로는_그_다음이다() + { + var map = new[] { @"D:\MsystechHIS=\\fs\MsystechHIS" }; + var remote = @"D:\MsystechHIS\002\a.tif"; + var shared = @"\\fs\MsystechHIS\002\a.tif"; + // 공유에 있으면 공유 + var viaShare = SignatureImagePath.Resolve(remote, Install, p => p == shared, map); + Assert.AreEqual(SignatureImageOrigin.Share, viaShare.Origin); + Assert.AreEqual(shared, viaShare.LocalPath); + // 공유에 없고 원격 경로가 열리면 직접 + var viaDirect = SignatureImagePath.Resolve(remote, Install, p => p == remote, map); + Assert.AreEqual(SignatureImageOrigin.DirectPath, viaDirect.Origin); + } + [TestMethod] public void UNC_공유_경로도_파일명을_뗀다() {