19종 → 23종. M_ZipMst 를 건물관리번호(PatZipBdg)로 찾는 조회 하나가 추가됐다. ## 결합식을 그대로 옮긴다 레거시(clsCommonLib.vb:16600)는 DECODE 로 빈 조각을 건너뛰며 구분자를 붙인다. 시도·시군구·읍면·도로명·지하여부·건물번호 본번/부번·건물명이 각각 다른 규칙으로 이어진다. 손으로 다시 짜면 주소가 미묘하게 달라지므로 SQL 을 그대로 가져왔다(바인드는 유지). ## 눈으로 보면 오타 같은 것 셋 PAT_도로명주소 도로명주소 + " " + 상세 (공백 있음) PAT_지번주소 지번주소 + 상세 (공백 <b>없음</b>) PAT_영문도로명주소 상세 + " " + 영문주소 (순서가 <b>뒤바뀐다</b>) 레거시가 그렇게 찍는다. 맞춰야 같은 종이가 된다. 그리고 PAT_우편번호 가 돌려주는 것은 <b>우편번호 컬럼이 아니라 구역번호</b>다. 조회는 둘 다 가져오지만 레거시가 쓰는 것은 구역번호다. 주소를 못 찾으면 빈 값이 아니라 <b>상세주소만</b> 돌려준다 — 레거시와 같다. ## 왕복은 환자당 한 번 주소 태그가 넷이라 태그마다 읽으면 왕복이 넷이 된다. 주민번호와 같이 환자를 붙이는 시점에 한 번 읽어 해석기에 넘긴다. 읽기에 실패해도 던지지 않는다 — 상세주소만으로 값을 만들 수 있고 레거시도 그렇게 한다. ## 게이트 - dotnet test 336/336 - --edit-smoke 실패 0 — 이름 검사가 23종 전부 실제 태그임을 확인 - --db-patient ①~㉑ 전건 통과 - --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
85 lines
3.4 KiB
C#
85 lines
3.4 KiB
C#
using SheetMe.Core.Catalog;
|
|
using SheetMe.Data.Stores;
|
|
|
|
namespace SheetMe.Designer.Services;
|
|
|
|
/// <summary>
|
|
/// 보정을 마친 주민번호를 만드는 <b>유일한 자리</b>.
|
|
///
|
|
/// 왜 한 곳으로 좁히는가: 이 값은 이 앱이 다루는 것 중 가장 민감하다.
|
|
/// 만드는 경로가 여러 곳이면 어디서 새는지 추적할 수 없고, 보정 순서가 어긋난 사본이 생긴다.
|
|
///
|
|
/// <b>값을 저장하지 않는다.</b> 필요할 때 만들어 <see cref="PatientTagResolver"/> 에 넘기고 끝낸다.
|
|
/// 로그에도 남기지 않는다 — 어떤 진단 리포트에도 값이 아니라 <b>길이</b>만 적는다.
|
|
///
|
|
/// 복호화는 DB 가 한다(<see cref="PersonalDataStore"/>). 앱은 키를 갖지 않는다.
|
|
/// 자세한 것은 <c>docs/RESNUM-DECRYPT.md</c>.
|
|
/// </summary>
|
|
public static class PatientIdentity
|
|
{
|
|
#region Methods
|
|
/// <summary>
|
|
/// 이 환자의 주민번호 — 못 만들면 빈 문자열.
|
|
///
|
|
/// <b>암호화 설정을 못 읽으면 만들지 않는다.</b> 설정을 "암호화 아님"으로 가정하면
|
|
/// <c>PatResNum</c> 을 평문처럼 써서 암호문을 종이에 찍는다. 빈칸이 훨씬 낫다.
|
|
/// </summary>
|
|
public static string ResidentNumberOf(PatientContext context)
|
|
{
|
|
var connection = ConfigService.Current.ConnectionString;
|
|
if (connection.Length == 0)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
var hospital = UserSession.Current.HspCod;
|
|
try
|
|
{
|
|
var store = new PersonalDataStore(connection);
|
|
var flags = store.Flags();
|
|
var raw = flags.ResidentNumberEncrypted
|
|
// 암호화 병원 — PatResNum 은 쓰지 않는다. PatResEnc 를 DB 가 풀어 준다
|
|
? store.Decrypt(PatientContext.Value(context.PatInf, "PatResEnc"), hospital)
|
|
: PatientContext.Value(context.PatInf, "PatResNum");
|
|
|
|
return ResidentNumber.Normalize(
|
|
raw,
|
|
PatientContext.Value(context.PatInf, "PatBthDay"),
|
|
PatientContext.Value(context.PatInf, "PatNatCod"),
|
|
hospital);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// 사유는 남기지만 값은 남기지 않는다 — 예외 메시지에 SQL 이 섞여도 값은 바인드였다
|
|
AppLog.Audit($"[주민번호] 만들지 못했습니다: {ex.GetType().Name}");
|
|
return string.Empty;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 이 환자의 주소 조각들(구역번호·도로명·지번·영문) — 못 읽으면 빈 사전.
|
|
///
|
|
/// 주민번호와 같은 이유로 <b>환자를 붙일 때 한 번만</b> 읽는다 —
|
|
/// 주소 태그가 넷이라 태그마다 읽으면 왕복이 넷이 된다.
|
|
/// 실패해도 던지지 않는다: 호출부가 상세주소만으로 값을 만들 수 있고, 레거시도 그렇게 한다.
|
|
/// </summary>
|
|
public static IReadOnlyDictionary<string, string> AddressOf(PatientContext context)
|
|
{
|
|
var connection = ConfigService.Current.ConnectionString;
|
|
var empty = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
if (connection.Length == 0)
|
|
{
|
|
return empty;
|
|
}
|
|
try
|
|
{
|
|
return new PatientContextStore(connection)
|
|
.Address(PatientContext.Value(context.PatInf, "PatZipBdg"));
|
|
}
|
|
catch
|
|
{
|
|
return empty;
|
|
}
|
|
}
|
|
#endregion
|
|
}
|