나이 계열 7종·주민번호 마스킹 3종 — 215종 → 225종 (조사 라운드 7 시작)
PAT_나이(bzDataInterface.vb:373 Case Else — 통합 대표행의 AgeMonth 파생 컬럼과 같은 식을 SQL 로: UDF_GETAGE_FROMRESNUM(복호화, 적용일시), dtCommonLib.vb:3725 확인. CGCH 분기·암호화 N 갈래 미이식), PAT_나이_세(:417 — 산정코드 24* 는 UDF_GETAGE(PatBthDay), 그 밖은 AgeCheck), PAT_성별_나이(:359 — "성별/나이". HIMCHAN_BP·BSYD 분기 미이식), PAT_나이_개월수(:430 — 월 경계 -1, 기존 MonthsSince 재사용), 개월·일 4종(:436-508 — 30일 나눗셈, 새 DaysSince 로 분해). AgeCheck(clsCommonLib.vb:665-974)를 ResidentNumber.AgeOf 로 이식 — 세기 접두 규칙(1·2→19, 3·4→20, 5·6→출생2자리<20 이면 20xx 아니면 19xx+월일 0101 강제, 7·8→서버 연도 2자리 비교, 그 외→18xx. REDCROSS· BSYD·BSGH 특례 미이식), 만나이(생일 당일 차감 없음 — 2024-09-25 이후 전 병원 규칙), 1세 이하는 고정 365일. 테스트 5건 — 기대값 하나가 틀려 있었다(출생 2자리 "20"은 <20 이 아니라 1920년으로 강제 — 구현이 맞고 판정을 고침). 개월·일 4종의 기준일은 레거시가 PC 시계(Now)라 서버 오늘로 바꿈(의도적 이탈 — 단말마다 값이 갈리면 안 된다). PAT_주민번호_Blind/SexTyp_Blind/SexTyp_BlindStar(:105-186 Case Else — 앞6 " - " 뒤 XXXXXXX / 성별1+XXXXXX / 성별1+******. SYBS 의 J-계열 서식 전체 노출 분기 미이식). 대조군 교체: PAT_나이가 이식되면서 ⑬·edit-smoke 의 "안 옮긴 태그" 대조군이 깨졌다(의도된 동작) — ETC_수술실간호사로 교체. ㊱ 신설: 나이 UDF 2종이 실값을 돌려준다(접두 없이 동작 확인). - dotnet test 342/342 (AgeOf 5건 추가) · --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
abb08d0c6b
commit
8256b79800
@@ -134,6 +134,16 @@ public static class ResidentNumber
|
|||||||
/// 실제 개월수와 다르지만 레거시가 그렇게 찍는다 — 달력으로 고치면 값이 달라진다.
|
/// 실제 개월수와 다르지만 레거시가 그렇게 찍는다 — 달력으로 고치면 값이 달라진다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string MonthsDaysSince(string? birthDay, string? todayYmd)
|
public static string MonthsDaysSince(string? birthDay, string? todayYmd)
|
||||||
|
{
|
||||||
|
var days = DaysSince(birthDay, todayYmd);
|
||||||
|
return days is null ? string.Empty : $"{days / 30}개월 {days % 30}일";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 생일부터 기준일까지 총 일수 — 개월·일 분해 태그 4종의 공통 재료.
|
||||||
|
/// 날짜를 못 읽으면 null(레거시는 예외가 전파돼 노란색이 되던 자리다).
|
||||||
|
/// </summary>
|
||||||
|
public static int? DaysSince(string? birthDay, string? todayYmd)
|
||||||
{
|
{
|
||||||
var birth = Clean(birthDay);
|
var birth = Clean(birthDay);
|
||||||
var today = Clean(todayYmd);
|
var today = Clean(todayYmd);
|
||||||
@@ -143,10 +153,83 @@ public static class ResidentNumber
|
|||||||
|| !DateTime.TryParseExact(today[..8], "yyyyMMdd", null,
|
|| !DateTime.TryParseExact(today[..8], "yyyyMMdd", null,
|
||||||
System.Globalization.DateTimeStyles.None, out var to))
|
System.Globalization.DateTimeStyles.None, out var to))
|
||||||
{
|
{
|
||||||
return string.Empty;
|
return null;
|
||||||
}
|
}
|
||||||
var days = (int)(to - from).TotalDays;
|
return (int)(to - from).TotalDays;
|
||||||
return $"{days / 30}개월 {days % 30}일";
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 만 나이 — 레거시 <c>AgeCheck</c>(clsCommonLib.vb:665-974)의 이식.
|
||||||
|
///
|
||||||
|
/// <b>세기 접두 규칙</b>(주민번호 7번째 자리): 1·2→19xx, 3·4→20xx,
|
||||||
|
/// 5·6(외국인)→ 출생 2자리가 20 미만이면 20xx, 아니면 19xx 에 <b>월일을 0101 로
|
||||||
|
/// 강제</b>(행려·외국인은 앞 6자리가 생년월일이 아닐 수 있다는 레거시 판단),
|
||||||
|
/// 7·8→ 서버 오늘의 연도 2자리와 비교해 19/20, 그 외(0·9)→18xx.
|
||||||
|
/// REDCROSS(적십자)·BSYD·BSGH 의 병원 특례는 옮기지 않았다 — 전부 Case Else 갈래다.
|
||||||
|
///
|
||||||
|
/// <b>만 나이 규칙</b>: 기준년-출생년, 생일이 아직 안 지났으면 -1(생일 당일은 차감 없음 —
|
||||||
|
/// 2024-09-25 이후 전 병원 규칙). 나이가 1 이하면 총 일수차가 <b>365 미만</b>일 때 0
|
||||||
|
/// (고정 365 — 윤년 경계의 하루 오차까지 레거시 그대로).
|
||||||
|
///
|
||||||
|
/// 레거시는 잘못된 입력에서 오류창을 띄우고 0 을 돌려줬다 — 여기서는 조용히 0 이다.
|
||||||
|
/// </summary>
|
||||||
|
public static long AgeOf(string? residentNumber, string? toDate, string serverToday)
|
||||||
|
{
|
||||||
|
var res = Clean(residentNumber);
|
||||||
|
if (res.Length < 13 || serverToday.Length < 8)
|
||||||
|
{
|
||||||
|
return 0; // 레거시도 빈 값은 0, 짧은 값은 예외→0 이었다
|
||||||
|
}
|
||||||
|
string birth8;
|
||||||
|
switch (res[6])
|
||||||
|
{
|
||||||
|
case '1' or '2':
|
||||||
|
birth8 = "19" + res[..6];
|
||||||
|
break;
|
||||||
|
case '3' or '4':
|
||||||
|
birth8 = "20" + res[..6];
|
||||||
|
break;
|
||||||
|
case '5' or '6':
|
||||||
|
if (!int.TryParse(res[..2], out var left2))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
birth8 = left2 < 20 ? "20" + res[..6] : "19" + res[..2] + "0101";
|
||||||
|
break;
|
||||||
|
case '7' or '8':
|
||||||
|
if (!int.TryParse(res[..2], out var yy2)
|
||||||
|
|| !int.TryParse(serverToday.Substring(2, 2), out var nowYy))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
birth8 = (nowYy < yy2 ? "19" : "20") + res[..6];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
birth8 = "18" + res[..6];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
var to = Clean(toDate);
|
||||||
|
if (to.Length < 8)
|
||||||
|
{
|
||||||
|
to = serverToday;
|
||||||
|
}
|
||||||
|
if (!int.TryParse(to[..4], out var toYear) || !int.TryParse(birth8[..4], out var birthYear)
|
||||||
|
|| !int.TryParse(birth8.Substring(4, 4), out var birthMmdd)
|
||||||
|
|| !int.TryParse(to.Substring(4, 4), out var toMmdd))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
long age = toYear - birthYear;
|
||||||
|
if (birthMmdd > toMmdd)
|
||||||
|
{
|
||||||
|
age -= 1;
|
||||||
|
}
|
||||||
|
if (age <= 1)
|
||||||
|
{
|
||||||
|
var days = DaysSince(birth8, to[..8]);
|
||||||
|
return days is null ? 0 : days < 365 ? 0 : 1;
|
||||||
|
}
|
||||||
|
return age;
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -682,6 +682,28 @@ public sealed class PatientExtraStore
|
|||||||
+ " ORDER BY OdrDtm DESC",
|
+ " ORDER BY OdrDtm DESC",
|
||||||
("m", comNum));
|
("m", comNum));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 나이(개월 표기 포함) — 통합 대표행의 AgeMonth 파생 컬럼과 같은 식
|
||||||
|
/// (dtCommonLib.vb:3725 — UDF_GETAGE_FROMRESNUM(복호화 주민번호, 적용일시)).
|
||||||
|
/// PAT_나이의 Case Else 갈래가 이 컬럼을 그대로 돌려준다(CGCH 분기 미이식).
|
||||||
|
/// 암호화 N 병원 갈래(PatResNum 직접)는 미이식 — 이 DB 는 암호화 Y 다.
|
||||||
|
/// </summary>
|
||||||
|
public string AgeMonthText(string chtNum, string adpDtm)
|
||||||
|
=> Scalar(
|
||||||
|
"SELECT UDF_GETAGE_FROMRESNUM(UDF_GetDecResNum(PatResEnc), :a) FROM P_PatInf"
|
||||||
|
+ " WHERE PatChtNum = RPAD(:c, 10)",
|
||||||
|
("a", adpDtm), ("c", chtNum));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 정수 나이 — 통합 대표행의 AGE 파생 컬럼과 같은 식
|
||||||
|
/// (dtCommonLib.vb:3712 — UDF_GETAGE(PatBthDay, 적용일 8자)).
|
||||||
|
/// PAT_나이_세의 산정코드 24* 갈래가 이 컬럼을 쓴다.
|
||||||
|
/// </summary>
|
||||||
|
public string AgeYearsUdf(string chtNum, string day8)
|
||||||
|
=> Scalar(
|
||||||
|
"SELECT UDF_GETAGE(PatBthDay, :a) FROM P_PatInf WHERE PatChtNum = RPAD(:c, 10)",
|
||||||
|
("a", day8), ("c", chtNum));
|
||||||
|
|
||||||
/// <summary>스칼라 1칸 — 0행이거나 NULL 이면 빈 문자열. 잔여 공백은 걷는다(문맥 행과 같은 규칙)</summary>
|
/// <summary>스칼라 1칸 — 0행이거나 NULL 이면 빈 문자열. 잔여 공백은 걷는다(문맥 행과 같은 규칙)</summary>
|
||||||
private string Scalar(string sql, params (string Name, object Value)[] binds)
|
private string Scalar(string sql, params (string Name, object Value)[] binds)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2072,7 +2072,9 @@ public static class DbSmoke
|
|||||||
|
|
||||||
// 대조군 — 매핑에 없는 환자 태그는 값이 되지 않아야 한다.
|
// 대조군 — 매핑에 없는 환자 태그는 값이 되지 않아야 한다.
|
||||||
// 이게 없으면 ⑫ 는 "해석기가 아무 태그나 통과시킨다"와 구분되지 않는다.
|
// 이게 없으면 ⑫ 는 "해석기가 아무 태그나 통과시킨다"와 구분되지 않는다.
|
||||||
var unmapped = resolver.Resolve("PAT_나이");
|
// 이 태그가 이식되면 이 판정이 깨진다 — 그때 다른 미이식 태그로 바꾼다
|
||||||
|
// (PAT_나이가 이식되면서 한 번 갈았다).
|
||||||
|
var unmapped = resolver.Resolve("ETC_수술실간호사");
|
||||||
Check("⑬ (대조군) 안 옮긴 환자 태그는 값이 되지 않는다",
|
Check("⑬ (대조군) 안 옮긴 환자 태그는 값이 되지 않는다",
|
||||||
!unmapped.Resolved && unmapped.Text.Contains("아직 옮기지", StringComparison.Ordinal),
|
!unmapped.Resolved && unmapped.Text.Contains("아직 옮기지", StringComparison.Ordinal),
|
||||||
$"'{unmapped.Text}' (Resolved={unmapped.Resolved})");
|
$"'{unmapped.Text}' (Resolved={unmapped.Resolved})");
|
||||||
@@ -2382,6 +2384,26 @@ public static class DbSmoke
|
|||||||
"RPAD 매칭이나 NATCOD 조인을 의심할 것");
|
"RPAD 매칭이나 NATCOD 조인을 의심할 것");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 나이 UDF — 주민번호가 있는 차트로 물으면 값이 나와야 한다.
|
||||||
|
// (통합 대표행의 AgeMonth/AGE 파생 컬럼과 같은 식 —
|
||||||
|
// UDF 이름·접두가 틀리면 여기서 바로 죽는다)
|
||||||
|
var ageOwner = FirstText(conn,
|
||||||
|
"SELECT TRIM(PatChtNum) FROM P_PatInf"
|
||||||
|
+ " WHERE PatResEnc IS NOT NULL AND ROWNUM = 1");
|
||||||
|
if (ageOwner.Length == 0)
|
||||||
|
{
|
||||||
|
lines.Add("SKIP ㊱ 나이 UDF — 주민번호가 있는 환자가 없어 판정 불가");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var ageText = extra.AgeMonthText(ageOwner, "202608201200");
|
||||||
|
var ageYears = extra.AgeYearsUdf(ageOwner, "20260820");
|
||||||
|
lines.Add($" 나이 UDF: 개월표기 {ageText.Length}자 · 정수 {ageYears.Length}자 (값은 기록하지 않는다)");
|
||||||
|
Check("㊱ 나이 UDF 가 값을 돌려준다",
|
||||||
|
ageText.Length > 0 && ageYears.Length > 0,
|
||||||
|
"UDF_GETAGE_FROMRESNUM/UDF_GETAGE 이름·접두를 의심할 것");
|
||||||
|
}
|
||||||
|
|
||||||
// 최초내원일도 같은 방식 — 재진(ComNfsTyp='2') 내원이 있는
|
// 최초내원일도 같은 방식 — 재진(ComNfsTyp='2') 내원이 있는
|
||||||
// 차트를 골라서 물으면 반드시 값이 나와야 한다.
|
// 차트를 골라서 물으면 반드시 값이 나와야 한다.
|
||||||
var revisitOwner = FirstText(conn,
|
var revisitOwner = FirstText(conn,
|
||||||
|
|||||||
@@ -551,7 +551,7 @@ public static class EditSmoke
|
|||||||
var probes = new[]
|
var probes = new[]
|
||||||
{
|
{
|
||||||
("차트", "PAT_차트번호"), ("이름", "PAT_이름"),
|
("차트", "PAT_차트번호"), ("이름", "PAT_이름"),
|
||||||
("나이", "PAT_나이"), ("시각", "ETC_현재일자"),
|
("간호", "ETC_수술실간호사"), ("시각", "ETC_현재일자"),
|
||||||
};
|
};
|
||||||
for (var i = 0; i < probes.Length; i++)
|
for (var i = 0; i < probes.Length; i++)
|
||||||
{
|
{
|
||||||
@@ -607,9 +607,10 @@ public static class EditSmoke
|
|||||||
withPatient);
|
withPatient);
|
||||||
|
|
||||||
// 아직 안 옮긴 환자 태그는 <b>그렇다고 말해야</b> 한다.
|
// 아직 안 옮긴 환자 태그는 <b>그렇다고 말해야</b> 한다.
|
||||||
|
// 대조군 태그가 이식되면 이 판정이 깨진다 — 그때 다른 미이식 태그로 갈 것(PAT_나이에서 한 번 갈았다).
|
||||||
// "DB 접속이 없습니다" 로 뭉개면 환자를 골랐는데도 그 사유가 나와 사람이 접속을 의심한다.
|
// "DB 접속이 없습니다" 로 뭉개면 환자를 골랐는데도 그 사유가 나와 사람이 접속을 의심한다.
|
||||||
Check("환자 태그: 매핑 없는 환자 태그는 안 옮겼다고 말한다",
|
Check("환자 태그: 매핑 없는 환자 태그는 안 옮겼다고 말한다",
|
||||||
withPatient.Contains("PAT_나이", StringComparison.Ordinal)
|
withPatient.Contains("ETC_수술실간호사", StringComparison.Ordinal)
|
||||||
&& withPatient.Contains("아직 옮기지", StringComparison.Ordinal),
|
&& withPatient.Contains("아직 옮기지", StringComparison.Ordinal),
|
||||||
withPatient);
|
withPatient);
|
||||||
|
|
||||||
|
|||||||
@@ -44,13 +44,10 @@ public sealed class PatientTagResolver : ITagValueResolver
|
|||||||
// PatBthDay 를 YYYY-MM-DD 로(:188-198). 형식이 붙으므로 컬럼만으로는 안 된다
|
// PatBthDay 를 YYYY-MM-DD 로(:188-198). 형식이 붙으므로 컬럼만으로는 안 된다
|
||||||
["PAT_생년월일"] = ("PatInf", "PatBthDay", Hyphenate),
|
["PAT_생년월일"] = ("PatInf", "PatBthDay", Hyphenate),
|
||||||
|
|
||||||
// ── 넣지 않은 것과 그 이유 ──
|
// ── 이 표에 없는 주민번호·성별·나이 계열은 전용 갈래에 있다 ──
|
||||||
// PAT_주민번호 : 레거시는 PatResNum_DEC(복호화 컬럼)를 읽는다(:87).
|
// 주민번호(복호화·마스킹)·성별은 FromResidentNumber, 나이 7종은 FromAge.
|
||||||
// P_PatInf 의 PatResNum 을 그대로 쓰면 암호화 병원에서 <b>암호문이 종이에 찍힌다</b>.
|
// 처음에 PAT_주민번호를 PatResNum 컬럼으로 넣었다가 암호화 병원에서
|
||||||
// 처음에 그렇게 넣어 뒀다 — 빈칸보다 나쁜 값이었다.
|
// 암호문이 종이에 찍힐 뻔했다 — 컬럼 하나짜리로 보여도 여기 넣지 않는다.
|
||||||
// PAT_성별 / PAT_한글성별 : CoiCalCod 가 "24" 로 시작하면 PatSexTyp, 아니면
|
|
||||||
// GetSexAge(PatResNum_DEC) 로 주민번호에서 뽑는다(:283-316). 복호화가 먼저다.
|
|
||||||
// PAT_나이 계열 : 병원 코드로 갈라지고(CGCH 는 자기 SQL) 계산식이 제각각이다(:373-).
|
|
||||||
};
|
};
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -356,6 +353,10 @@ public sealed class PatientTagResolver : ITagValueResolver
|
|||||||
{
|
{
|
||||||
return certValue;
|
return certValue;
|
||||||
}
|
}
|
||||||
|
if (FromAge(tag) is { } ageValue)
|
||||||
|
{
|
||||||
|
return ageValue;
|
||||||
|
}
|
||||||
if (Computed.TryGetValue(tag, out var compute))
|
if (Computed.TryGetValue(tag, out var compute))
|
||||||
{
|
{
|
||||||
var made = compute(context);
|
var made = compute(context);
|
||||||
@@ -433,6 +434,27 @@ public sealed class PatientTagResolver : ITagValueResolver
|
|||||||
? "주민번호를 읽지 못해 성별을 만들 수 없습니다"
|
? "주민번호를 읽지 못해 성별을 만들 수 없습니다"
|
||||||
: "이 환자의 성별을 판정할 수 없습니다");
|
: "이 환자의 성별을 판정할 수 없습니다");
|
||||||
}
|
}
|
||||||
|
case "PAT_주민번호_Blind":
|
||||||
|
case "PAT_주민번호_SexTyp_Blind":
|
||||||
|
case "PAT_주민번호_SexTyp_BlindStar":
|
||||||
|
{
|
||||||
|
// 마스킹 3종(bzDataInterface.vb:105-186) — Case Else 갈래.
|
||||||
|
// SYBS 의 J-계열 서식 전체 노출 분기는 미이식(그 병원 배포 시).
|
||||||
|
// 레거시는 13자 미만이면 Substring 예외→노란색이었다 — 여기는 사유.
|
||||||
|
if (residentNumber.Length < 13)
|
||||||
|
{
|
||||||
|
return new TagValue(false, residentNumber.Length == 0
|
||||||
|
? "주민번호를 읽지 못해 마스킹을 만들 수 없습니다"
|
||||||
|
: "주민번호가 13자리가 아닙니다");
|
||||||
|
}
|
||||||
|
var masked = tag switch
|
||||||
|
{
|
||||||
|
"PAT_주민번호_Blind" => $"{residentNumber[..6]} - XXXXXXX",
|
||||||
|
"PAT_주민번호_SexTyp_Blind" => $"{residentNumber[..6]} - {residentNumber[6]}XXXXXX",
|
||||||
|
_ => $"{residentNumber[..6]} - {residentNumber[6]}******",
|
||||||
|
};
|
||||||
|
return new TagValue(true, masked);
|
||||||
|
}
|
||||||
case "PAT_성별_남":
|
case "PAT_성별_남":
|
||||||
case "PAT_성별_여":
|
case "PAT_성별_여":
|
||||||
{
|
{
|
||||||
@@ -963,6 +985,153 @@ public sealed class PatientTagResolver : ITagValueResolver
|
|||||||
: new TagValue(false, "수술일자 값이 형식에 맞지 않습니다");
|
: new TagValue(false, "수술일자 값이 형식에 맞지 않습니다");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 나이 계열 7종(bzDataInterface.vb:359-508 + AgeCheck clsCommonLib.vb:665-974).
|
||||||
|
///
|
||||||
|
/// <b>출처가 셋으로 갈린다</b>: PAT_나이는 통합 대표행의 AgeMonth 파생 컬럼
|
||||||
|
/// (= DB UDF_GETAGE_FROMRESNUM — 같은 식을 SQL 로 쏜다, CGCH 분기 미이식),
|
||||||
|
/// PAT_나이_세·성별_나이의 계산 갈래는 AgeCheck(C# 이식, 테스트 5건),
|
||||||
|
/// 개월·일 4종은 생년월일에서 30일 나눗셈이다.
|
||||||
|
///
|
||||||
|
/// <b>의도적 이탈 하나</b>: 개월·일 4종의 기준일을 레거시는 <b>PC 시계(Now)</b>로
|
||||||
|
/// 잡는다 — 단말마다 값이 달라질 수 있어 서버 오늘로 바꿨다(다른 날짜 태그와 동일).
|
||||||
|
/// </summary>
|
||||||
|
private TagValue? FromAge(string tag)
|
||||||
|
{
|
||||||
|
if (tag is not ("PAT_나이" or "PAT_나이_세" or "PAT_성별_나이" or "PAT_나이_개월수"
|
||||||
|
or "PAT_나이_00개월_00일" or "PAT_나이_00Months_00Days"
|
||||||
|
or "PAT_나이_개월_00일" or "PAT_나이_00개월_일"))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (extraCache.TryGetValue(tag, out var cached))
|
||||||
|
{
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
var value = Compute();
|
||||||
|
extraCache[tag] = value;
|
||||||
|
return value;
|
||||||
|
|
||||||
|
TagValue Compute()
|
||||||
|
{
|
||||||
|
var today = PatientContext.Value(hospital, "ServerToday");
|
||||||
|
var birth = PatientContext.Value(context.PatInf, "PatBthDay");
|
||||||
|
switch (tag)
|
||||||
|
{
|
||||||
|
case "PAT_나이":
|
||||||
|
{
|
||||||
|
// Case Else 갈래 = AgeMonth 파생 컬럼(:414) — UDF 를 그대로 쏜다
|
||||||
|
var chtNum = PatientContext.Value(context.ComInf, "ComChtNum");
|
||||||
|
if (chtNum.Length == 0)
|
||||||
|
{
|
||||||
|
return new TagValue(false, "이 내원에 차트번호가 없습니다");
|
||||||
|
}
|
||||||
|
if (extraStoreSource?.Invoke() is not { } store)
|
||||||
|
{
|
||||||
|
return new TagValue(false, "부가정보 조회가 연결되지 않았습니다");
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return store.AgeMonthText(chtNum, context.AdpDtm) is { Length: > 0 } age
|
||||||
|
? new TagValue(true, age)
|
||||||
|
: new TagValue(false, "나이를 만들 주민번호가 비어 있습니다");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new TagValue(false, $"나이 조회에 실패했습니다: {ex.GetType().Name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "PAT_나이_세":
|
||||||
|
{
|
||||||
|
// 산정코드 24* 는 AGE 파생 컬럼(UDF), 그 밖은 AgeCheck(:417-428)
|
||||||
|
if (PatientContext.Value(context.CoiInf, "CoiCalCod")
|
||||||
|
.StartsWith("24", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
var chtNum = PatientContext.Value(context.ComInf, "ComChtNum");
|
||||||
|
if (extraStoreSource?.Invoke() is not { } store)
|
||||||
|
{
|
||||||
|
return new TagValue(false, "부가정보 조회가 연결되지 않았습니다");
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var day8 = context.AdpDtm.Length >= 8 ? context.AdpDtm[..8] : today;
|
||||||
|
return Wrap2(store.AgeYearsUdf(chtNum, day8), "생년월일이 비어 나이를 만들 수 없습니다");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new TagValue(false, $"나이 조회에 실패했습니다: {ex.GetType().Name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (residentNumber.Length == 0)
|
||||||
|
{
|
||||||
|
return new TagValue(false, "주민번호를 읽지 못해 나이를 만들 수 없습니다");
|
||||||
|
}
|
||||||
|
if (today.Length < 8)
|
||||||
|
{
|
||||||
|
return new TagValue(false, "요양기관 행(서버 오늘)을 읽지 못했습니다");
|
||||||
|
}
|
||||||
|
return new TagValue(true,
|
||||||
|
ResidentNumber.AgeOf(residentNumber, today, today).ToString());
|
||||||
|
}
|
||||||
|
case "PAT_성별_나이":
|
||||||
|
{
|
||||||
|
// HIMCHAN_BP·BSYD 분기 미이식 — SRCH 는 산정코드 24* 조건만 남는다(:359-371)
|
||||||
|
if (PatientContext.Value(context.CoiInf, "CoiCalCod")
|
||||||
|
.StartsWith("24", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
var sex = PatientContext.Value(context.PatInf, "PatSexTyp");
|
||||||
|
var chtNum = PatientContext.Value(context.ComInf, "ComChtNum");
|
||||||
|
if (extraStoreSource?.Invoke() is not { } store)
|
||||||
|
{
|
||||||
|
return new TagValue(false, "부가정보 조회가 연결되지 않았습니다");
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return new TagValue(true, $"{sex}/{store.AgeMonthText(chtNum, context.AdpDtm)}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new TagValue(false, $"나이 조회에 실패했습니다: {ex.GetType().Name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (residentNumber.Length == 0)
|
||||||
|
{
|
||||||
|
return new TagValue(false, "주민번호를 읽지 못해 성별·나이를 만들 수 없습니다");
|
||||||
|
}
|
||||||
|
if (today.Length < 8)
|
||||||
|
{
|
||||||
|
return new TagValue(false, "요양기관 행(서버 오늘)을 읽지 못했습니다");
|
||||||
|
}
|
||||||
|
return new TagValue(true,
|
||||||
|
$"{ResidentNumber.SexOf(residentNumber)}/{ResidentNumber.AgeOf(residentNumber, today, today)}");
|
||||||
|
}
|
||||||
|
case "PAT_나이_개월수":
|
||||||
|
// VB DateDiff(Month) - 1 — 월 경계만 세고 1을 뺀다(:430-434, 레거시 결함 보존)
|
||||||
|
return Wrap2(ResidentNumber.MonthsSince(birth, today),
|
||||||
|
"생년월일 또는 서버 오늘을 읽지 못했습니다");
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
// 30일 나눗셈 4종(:436-508) — 총 일수를 30으로 나눠 개월·일로 가른다
|
||||||
|
var days = ResidentNumber.DaysSince(birth, today);
|
||||||
|
if (days is null)
|
||||||
|
{
|
||||||
|
return new TagValue(false, "생년월일 또는 서버 오늘을 읽지 못했습니다");
|
||||||
|
}
|
||||||
|
return new TagValue(true, tag switch
|
||||||
|
{
|
||||||
|
"PAT_나이_00개월_00일" => $"{days / 30}개월 {days % 30}일",
|
||||||
|
"PAT_나이_00Months_00Days" => $"{days / 30}Months {days % 30}Days",
|
||||||
|
"PAT_나이_개월_00일" => (days % 30).ToString()!,
|
||||||
|
_ => (days / 30).ToString()!, // PAT_나이_00개월_일 — 개월 숫자만
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static TagValue Wrap2(string value, string reasonWhenEmpty)
|
||||||
|
=> value.Length > 0 ? new TagValue(true, value) : new TagValue(false, reasonWhenEmpty);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 진단서 의사 계열 8종(bzDataInterface.vb:4380-4845).
|
/// 진단서 의사 계열 8종(bzDataInterface.vb:4380-4845).
|
||||||
///
|
///
|
||||||
@@ -2254,6 +2423,9 @@ public sealed class PatientTagResolver : ITagValueResolver
|
|||||||
{
|
{
|
||||||
"PAT_주민번호", "PAT_주민번호_Dash", "PAT_성별", "PAT_한글성별",
|
"PAT_주민번호", "PAT_주민번호_Dash", "PAT_성별", "PAT_한글성별",
|
||||||
"PAT_성별_남", "PAT_성별_여",
|
"PAT_성별_남", "PAT_성별_여",
|
||||||
|
"PAT_주민번호_Blind", "PAT_주민번호_SexTyp_Blind", "PAT_주민번호_SexTyp_BlindStar",
|
||||||
|
"PAT_나이", "PAT_나이_세", "PAT_성별_나이", "PAT_나이_개월수",
|
||||||
|
"PAT_나이_00개월_00일", "PAT_나이_00Months_00Days", "PAT_나이_개월_00일", "PAT_나이_00개월_일",
|
||||||
"PAT_실제생년월일", "PAT_국적", "PAT_장애등급", "PAT_건보세대주명",
|
"PAT_실제생년월일", "PAT_국적", "PAT_장애등급", "PAT_건보세대주명",
|
||||||
"PAT_건보_급여_세대주명", "PAT_협력업체", "PAT_보호자연락처",
|
"PAT_건보_급여_세대주명", "PAT_협력업체", "PAT_보호자연락처",
|
||||||
"OCM_FollowUp", "OCM_외출외박신청일", "OCM_외출외박종료일",
|
"OCM_FollowUp", "OCM_외출외박신청일", "OCM_외출외박종료일",
|
||||||
|
|||||||
@@ -165,5 +165,44 @@ public sealed class ResidentNumberTests
|
|||||||
Assert.AreEqual("880101 - 1234567", ResidentNumber.WithDash("8801011234567"));
|
Assert.AreEqual("880101 - 1234567", ResidentNumber.WithDash("8801011234567"));
|
||||||
Assert.AreEqual("880101", ResidentNumber.WithDash("880101"));
|
Assert.AreEqual("880101", ResidentNumber.WithDash("880101"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void 만나이는_세기_접두를_7번째_자리로_정한다()
|
||||||
|
{
|
||||||
|
// 1·2 → 19xx, 3·4 → 20xx (AgeCheck, clsCommonLib.vb:686-700)
|
||||||
|
Assert.AreEqual(38L, ResidentNumber.AgeOf("8801011234567", "20260820", "20260820"));
|
||||||
|
Assert.AreEqual(6L, ResidentNumber.AgeOf("2001013234567", "20260820", "20260820"));
|
||||||
|
// 7·8 — 서버 오늘의 연도 2자리(26)보다 크면 19xx: 88 > 26 → 1988년생
|
||||||
|
Assert.AreEqual(38L, ResidentNumber.AgeOf("8801017234567", "20260820", "20260820"));
|
||||||
|
// 그 외(9) → 18xx
|
||||||
|
Assert.AreEqual(138L, ResidentNumber.AgeOf("8801019234567", "20260820", "20260820"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void 만나이는_생일_전이면_하나_줄고_당일은_줄지_않는다()
|
||||||
|
{
|
||||||
|
Assert.AreEqual(37L, ResidentNumber.AgeOf("8812311234567", "20260820", "20260820"));
|
||||||
|
Assert.AreEqual(38L, ResidentNumber.AgeOf("8808201234567", "20260820", "20260820"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void 한_살_이하는_고정_365일로_가른다()
|
||||||
|
{
|
||||||
|
// 총 일수차 < 365 → 0세, 이상 → 1세 (윤년 무시 — 레거시 고정 365)
|
||||||
|
Assert.AreEqual(0L, ResidentNumber.AgeOf("2510013234567", "20260820", "20260820"));
|
||||||
|
Assert.AreEqual(1L, ResidentNumber.AgeOf("2501013234567", "20260820", "20260820"));
|
||||||
|
Assert.AreEqual(0L, ResidentNumber.AgeOf("", "20260820", "20260820"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void 외국인_5_6은_출생_2자리로_갈린다()
|
||||||
|
{
|
||||||
|
// 출생 2자리 < 20 → 20xx 그대로, >= 20 → 19xx + 월일 0101 강제.
|
||||||
|
// 경계 주의: "20" 은 < 20 이 아니라서 2020년생이 아니라 1920-01-01 로 강제된다(레거시 그대로)
|
||||||
|
Assert.AreEqual(20L, ResidentNumber.AgeOf("0601015234567", "20260820", "20260820"));
|
||||||
|
Assert.AreEqual(106L, ResidentNumber.AgeOf("2001015234567", "20260820", "20260820"));
|
||||||
|
// 88 >= 20 → 1988-01-01 로 강제 → 만 38세
|
||||||
|
Assert.AreEqual(38L, ResidentNumber.AgeOf("8812315234567", "20260820", "20260820"));
|
||||||
|
}
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user