환자 태그 매핑을 레거시 함수 본문으로 다시 세운다 — 셋은 존재하지 않는 이름이었다

"환자 태그이지만 아직 옮기지 않았습니다 엄청 많은데?" — 맞다. 그리고
옮겼다고 적어 둔 것도 절반이 거짓이었다.

## 지어낸 이름 셋

PAT_휴대전화 · PAT_전화번호 · PAT_주소 는 <b>실존하지 않는 태그</b>다.
컬럼 이름만 보고 태그 이름을 추측해 표에 넣었고, 이름이 안 맞으면 매칭이 안 되므로
값도 안 나오고 오류도 안 난다 — 화면에는 "아직 안 옮겼습니다"로 보인다.
실제 이름은 PAT_휴대전화번호 · PAT_자택전화번호 이고 PAT_주소 는 없다.

그래서 진단에 이름 검사를 넣었다 — 옮긴 이름이 전부
LegacyTagCatalog.DataInterfaceTags 에 있는지 본다. 대조군(없는 이름이 실제로 걸리는지)도 함께 둔다.

## PAT_주민번호 는 빼야 했다

레거시는 PatResNum_DEC(복호화 컬럼)를 읽는다(bzDataInterface.vb:87).
P_PatInf 의 PatResNum 을 그대로 쓰면 <b>암호화 병원에서 암호문이 종이에 찍힌다</b>.
빈칸보다 나쁜 값이라 표에서 뺐다.

## 실제로 맞게 돌던 것은 2종이었다

PAT_이름 · PAT_차트번호. 앞서 "6종"이라고 보고한 것은 틀렸다.

## 이번에는 함수 본문을 읽고 넣었다

기계적으로 옮길 수 있는 것은 <b>본문이 컬럼 하나를 그대로 돌려주는</b> 5종뿐이다
(추출해서 셌다). 거기에 형식이 하나 붙는 생년월일을 더해 7종:

  PAT_이름 · PAT_영문이름 · PAT_휴대전화번호 · PAT_자택전화번호 · PAT_여권번호
  PAT_차트번호(내원의 ComChtNum — moPatInfoBiz.ChtNum 이 그것이다, :70)
  PAT_생년월일(PatBthDay → YYYY-MM-DD, :188-198)

차트번호를 PatInf 에서 ComInf 로 고쳤고, 진단의 가상 문맥도 같이 고쳤다 —
가상 문맥이 옛 매핑을 쓰고 있으면 실제와 다른 것을 검사한다.

## 왜 나머지가 많이 남는가 (노력 부족이 아니다)

- PAT_성별/한글성별 : CoiCalCod 가 "24" 로 시작하면 PatSexTyp, 아니면
  GetSexAge(PatResNum_DEC) 로 주민번호에서 뽑는다(:283-316) — 복호화가 선행 조건이다.
- PAT_나이 계열 : 병원 코드로 갈라지고 CGCH 는 자기 SQL 을 들고 있다(:373-).
- 그 밖 238종 : 함수 본문에 자기 SQL 이 있다.

즉 다음 관문은 태그를 더 옮기는 것이 아니라 <b>주민번호 복호화</b>다.
그것이 풀리면 성별 계열이 따라 열린다. 짐작으로 채우면 종이에 그럴듯한 값이 찍히고,
그건 아무도 의심하지 않는다.

## 게이트

- dotnet test 323/323
- --edit-smoke 실패 0 (이름 검사 + 대조군 2건 신규)
- --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 10:56:42 +09:00
co-authored by Claude Opus 5
parent 5ecc7b514a
commit 128aedc34f
2 changed files with 69 additions and 15 deletions
@@ -19,18 +19,45 @@ namespace SheetMe.Designer.Services;
public sealed class PatientTagResolver : ITagValueResolver
{
#region Member Fields
/// <summary>태그 → (문맥 행, 컬럼). 전부 조회 SQL 에서 컬럼을 확인한 것이다.</summary>
private static readonly Dictionary<string, (string Row, string Column)> Direct =
/// <summary>
/// 태그 → (문맥 행, 컬럼, 형식). 전부 <b>레거시 함수 본문을 읽어</b> 확인한 것이다
/// (<c>[001]Common\[012]DataTag\M.CMM.DataTag\bzDataInterface.vb</c>).
///
/// <b>처음에 컬럼 이름만 보고 여섯 개를 넣었는데 셋은 태그 이름 자체가 없었다</b> —
/// PAT_휴대전화·PAT_전화번호·PAT_주소 는 내가 지어낸 이름이라 아무것도 매칭되지 않았다.
/// 실제 이름은 PAT_휴대전화번호·PAT_자택전화번호 이고 PAT_주소 는 존재하지 않는다.
/// 그래서 이름은 <see cref="LegacyTagCatalog.DataInterfaceTags"/> 에 있는지 진단이 검사한다.
/// </summary>
private static readonly Dictionary<string, (string Row, string Column, Func<string, string>? Format)> Direct =
new(StringComparer.Ordinal)
{
// P_PatInf — dtPatVisitList.vb:51 SELECT 목록에서 확인
["PAT_이름"] = ("PatInf", "PatNam"),
["PAT_차트번호"] = ("PatInf", "PatChtNum"),
["PAT_주민번호"] = ("PatInf", "PatResNum"),
["PAT_휴대전화"] = ("PatInf", "PatMblPhn"),
["PAT_전화번호"] = ("PatInf", "PatHomPhn"),
["PAT_주소"] = ("PatInf", "PatAdrDtl"),
// 본문이 컬럼 하나를 그대로 돌려주는 것들(bzDataInterface.vb:74, 79, 그리고 동형 3개)
["PAT_이름"] = ("PatInf", "PatNam", null),
["PAT_영문이름"] = ("PatInf", "PatEngNam", null),
["PAT_휴대전화번호"] = ("PatInf", "PatMblPhn", null),
["PAT_자택전화번호"] = ("PatInf", "PatHomPhn", null),
["PAT_여권번호"] = ("PatInf", "PatPspNum", null),
// moPatInfoBiz.ChtNum — 내원의 차트번호(:70)
["PAT_차트번호"] = ("ComInf", "ComChtNum", null),
// PatBthDay 를 YYYY-MM-DD 로(:188-198). 형식이 붙으므로 컬럼만으로는 안 된다
["PAT_생년월일"] = ("PatInf", "PatBthDay", Hyphenate),
// ── 넣지 않은 것과 그 이유 ──
// PAT_주민번호 : 레거시는 PatResNum_DEC(복호화 컬럼)를 읽는다(:87).
// P_PatInf 의 PatResNum 을 그대로 쓰면 암호화 병원에서 <b>암호문이 종이에 찍힌다</b>.
// 처음에 그렇게 넣어 뒀다 — 빈칸보다 나쁜 값이었다.
// PAT_성별 / PAT_한글성별 : CoiCalCod 가 "24" 로 시작하면 PatSexTyp, 아니면
// GetSexAge(PatResNum_DEC) 로 주민번호에서 뽑는다(:283-316). 복호화가 먼저다.
// PAT_나이 계열 : 병원 코드로 갈라지고(CGCH 는 자기 SQL) 계산식이 제각각이다(:373-).
};
#endregion
#region Member Fields
/// <summary>YYYYMMDD → YYYY-MM-DD. 8자리가 아니면 <b>손대지 않는다</b> — 잘라서 그럴듯하게 만들지 않는다</summary>
private static string Hyphenate(string value)
=> value.Length == 8 ? $"{value[..4]}-{value[4..6]}-{value[6..8]}" : value;
private readonly ITagValueResolver next;
private readonly PatientContext context;
@@ -67,13 +94,22 @@ public sealed class PatientTagResolver : ITagValueResolver
"CoiInf" => context.CoiInf,
_ => context.CowInf,
};
var value = PatientContext.Value(row, map.Column);
return value.Length > 0
? new TagValue(true, value)
: new TagValue(false, $"이 환자에게는 {map.Column} 값이 비어 있습니다");
var raw = PatientContext.Value(row, map.Column);
if (raw.Length == 0)
{
return new TagValue(false, $"이 환자에게는 {map.Column} 값이 비어 있습니다");
}
var value = map.Format is null ? raw : map.Format(raw);
return new TagValue(true, value);
}
/// <summary>지금 옮겨진 환자 태그 수 — 화면이 커버리지를 정직하게 말할 때 쓴다</summary>
public static int MappedCount => Direct.Count;
/// <summary>
/// 옮긴 태그 이름들 — 진단이 <b>실존하는 태그인지</b> 검사한다.
/// 이 검사가 없어서 지어낸 이름 셋이 조용히 아무것도 안 하고 있었다.
/// </summary>
public static IReadOnlyCollection<string> MappedTags => Direct.Keys;
#endregion
}