태그를 11종 → 19종으로. "전부"의 규모를 먼저 재 봤다

"나머지도 전부 옮기자" — 추측하지 않고 386종을 본문 모양으로 분류했다.

## 실제 분포 (bzDataInterface.vb 386종)

  자기 SQL 을 든 것        232  각각 별도 이식 — 본문 SQL 을 읽고 바인드로 옮기고 실DB 로 확인
  MessageBox 를 띄우는 것    48  대화형이라 미리보기에서 <b>돌 수 없다</b>
  문맥만 읽는 것             48  옮길 수 있는 것
  기타(다른 라이브러리 호출) 55  하나씩 봐야 한다
  HspCod 로 갈리는 것         3  병원별 분기

즉 "전부"는 태그 늘리기가 아니라 <b>232건의 데이터 접근 이식</b>이다.
48종 목록은 docs/TAG-PORTABLE-48.md 에 본문째로 뽑아 뒀다 — 다음 작업이 기계적이 된다.

## 이번에 옮긴 8종

순수 형식 가공만 하는 것들. Computed 표(태그 → 문맥 함수)를 새로 뒀다 —
컬럼 하나로 안 되는 것(두 컬럼을 보거나 잘라 붙이는 것)을 담는다.

  PAT_생년월일_영문 · PAT_생년월일_영문_일월년도   자리만 바꿔 붙인다
  PAT_연락처                                      휴대전화가 있으면 그것, 없으면 자택
  PAT_연락처_뒷4자리 · PAT_휴대전화번호_뒷4자리    -, . 을 걷고 뒤 4자리
  OCM_외래내원일자 · OCM_입원전환일자             ComAcpDtm·ComTrsDtm 앞 8자리
  OCM_입원전환일시                                12자리를 일시 형태로

자리수가 모자라면 <b>빈 값</b>이다. 잘라서 그럴듯하게 만들지 않는다.

## 이번에 뺀 것과 이유

  PAT_우편번호 · PAT_도로명주소 · PAT_지번주소 · PAT_영문도로명주소
    → GetZipInfoByBuildingNumber_Simple 조회가 먼저 필요하다(왕복 추가)
  OCM_담당의사 계열
    → GetCodInfDT + GetUidMst 두 조회를 거치고 입원/외래로 갈린다
  OCM_외래내원일시
    → 별도 헬퍼(외래내원일시)의 형식을 아직 안 봤다

## 게이트

- dotnet test 336/336
- --edit-smoke 실패 0 — 이름 검사가 19종 전부 실제 태그임을 확인
  (지어낸 이름 셋이 조용히 앉아 있던 적이 있어서 이 검사가 있다)
- --db-patient ①~㉑ 전건 통과
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Msystech
2026-08-19 11:27:33 +09:00
co-authored by Claude Opus 5
parent 222d1987d5
commit 510663c5fc
2 changed files with 130 additions and 2 deletions
@@ -59,6 +59,61 @@ public sealed class PatientTagResolver : ITagValueResolver
private static string Hyphenate(string value)
=> value.Length == 8 ? $"{value[..4]}-{value[4..6]}-{value[6..8]}" : value;
/// <summary>
/// 컬럼 하나로 안 되는 것들 — 두 컬럼을 보거나 잘라 붙인다.
/// 전부 <b>본문을 읽고</b> 옮겼다(docs/TAG-PORTABLE-48.md 의 목록).
/// 외부 조회가 필요한 것(우편번호·담당의)은 여기 없다 — 왕복이 더 붙어야 한다.
/// </summary>
private static readonly Dictionary<string, Func<PatientContext, string>> Computed =
new(StringComparer.Ordinal)
{
// 생년월일 세 형태 — 자리만 바꿔 붙인다(bzDataInterface.vb:188-226)
["PAT_생년월일_영문"] = c => Reorder(Pat(c, "PatBthDay"), 4, 6, 0),
["PAT_생년월일_영문_일월년도"] = c => Reorder(Pat(c, "PatBthDay"), 6, 4, 0),
// 연락처 — 휴대전화가 있으면 그것, 없으면 자택
["PAT_연락처"] = c => Contact(c),
// 뒤 4자리. 레거시는 -, . 을 걷고 Right(4) 한다
["PAT_연락처_뒷4자리"] = c => Last4(Contact(c).Replace("-", string.Empty).Replace(".", string.Empty)),
["PAT_휴대전화번호_뒷4자리"] = c => Last4(Pat(c, "PatMblPhn")),
// 내원일시 — ComAcpDtm·ComTrsDtm 을 자리수만 맞춰 찍는다
["OCM_외래내원일자"] = c => Hyphenate(Head(Com(c, "ComAcpDtm"), 8)),
["OCM_입원전환일자"] = c => Hyphenate(Head(Com(c, "ComTrsDtm"), 8)),
["OCM_입원전환일시"] = c => Stamp(Com(c, "ComTrsDtm")),
};
private static string Pat(PatientContext c, string column) => PatientContext.Value(c.PatInf, column);
private static string Com(PatientContext c, string column) => PatientContext.Value(c.ComInf, column);
/// <summary>휴대전화가 비어 있지 않으면 그것, 아니면 자택(bzDataInterface.vb 연락처)</summary>
private static string Contact(PatientContext c)
=> Pat(c, "PatMblPhn").Length > 0 ? Pat(c, "PatMblPhn") : Pat(c, "PatHomPhn");
/// <summary>뒤 4자리 — 4자리가 안 되면 빈 값(레거시도 그렇다)</summary>
private static string Last4(string value) => value.Length >= 4 ? value[^4..] : string.Empty;
/// <summary>앞 n자리 — 모자라면 빈 값. 잘라서 그럴듯하게 만들지 않는다</summary>
private static string Head(string value, int n) => value.Length >= n ? value[..n] : string.Empty;
/// <summary>YYYYMMDD 를 순서만 바꿔 하이픈으로. 8자리가 아니면 빈 값</summary>
private static string Reorder(string value, int a, int b, int c)
{
if (value.Length != 8)
{
return string.Empty;
}
static string Cut(string s, int at) => at == 0 ? s[..4] : s.Substring(at, 2);
return $"{Cut(value, a)}-{Cut(value, b)}-{Cut(value, c)}";
}
/// <summary>YYYYMMDDHHMM → YYYY-MM-DD HH:MM. 12자리가 아니면 빈 값</summary>
private static string Stamp(string value)
=> value.Length >= 12
? $"{value[..4]}-{value[4..6]}-{value[6..8]} {value[8..10]}:{value[10..12]}"
: string.Empty;
private readonly ITagValueResolver next;
private readonly PatientContext context;
@@ -90,6 +145,13 @@ public sealed class PatientTagResolver : ITagValueResolver
{
return derived;
}
if (Computed.TryGetValue(tag, out var compute))
{
var made = compute(context);
return made.Length > 0
? new TagValue(true, made)
: new TagValue(false, "이 환자에게는 그 값을 만들 재료가 없습니다");
}
if (!Direct.TryGetValue(tag, out var map))
{
// 환자 문맥이 <b>있는데도</b> 못 만든 것과, 문맥이 없어서 못 만든 것은 다른 일이다.
@@ -162,7 +224,7 @@ public sealed class PatientTagResolver : ITagValueResolver
: new TagValue(false, "주민번호를 읽지 못했습니다(암호화 설정 또는 권한을 확인하세요)");
/// <summary>지금 옮겨진 환자 태그 수 — 화면이 커버리지를 정직하게 말할 때 쓴다</summary>
public static int MappedCount => Direct.Count + DerivedTags.Length;
public static int MappedCount => Direct.Count + Computed.Count + DerivedTags.Length;
/// <summary>주민번호에서 파생되는 태그들 — 이름 검사가 이것도 봐야 한다</summary>
public static readonly string[] DerivedTags =
@@ -174,6 +236,7 @@ public sealed class PatientTagResolver : ITagValueResolver
/// 옮긴 태그 이름들 — 진단이 <b>실존하는 태그인지</b> 검사한다.
/// 이 검사가 없어서 지어낸 이름 셋이 조용히 아무것도 안 하고 있었다.
/// </summary>
public static IReadOnlyCollection<string> MappedTags => Direct.Keys.Concat(DerivedTags).ToList();
public static IReadOnlyCollection<string> MappedTags
=> Direct.Keys.Concat(Computed.Keys).Concat(DerivedTags).ToList();
#endregion
}