OCM_담당의사 3종을 옮긴다 — 처음으로 레거시와 의도적으로 다르게 간다
23종 → 26종. 조회 둘(P_CodInf 선택 + M_UidMst)이 새로 붙었다. ## 원칙을 처음 깬다 지금까지는 "레거시가 이상해 보여도 그대로 옮긴다"였다 (주민번호의 DateValidCheck 가 그랬다 — 조건이 항상 참이지만 그대로 뒀다). 여기서는 그럴 수 없다. GetCodInfDT 는 ORDER BY 없이 CodMtiSeq='0' 만 걸고 호출부가 Rows(0) 을 집는데, 이 DB 에서 그런 내원이 <b>25,488건</b>이고 최대 41행이다. 41행 중 첫 행은 사실상 무작위다 — <b>재현할 결정적 동작이 존재하지 않는다.</b> 그래서 적용일시로 거르고 최신순 1행을 집는다(문맥의 CodInf 와 같은 규칙). 레거시도 이 시각을 넘기기는 한다 — 마스터 유효기간 조인에만 쓸 뿐이고 P_CodInf 의 CodStrDtm/CodEndDtm 은 WHERE 에 없다. "그 시점의 진료과"를 뽑으려던 의도가 반쯤만 구현된 것으로 보인다. 근거는 docs/TAG-PORTABLE-48.md 에 남겼다. ## 폴백을 빠뜨리지 않았다 GetUidMst(clsCommonLib.vb:10615)는 유효기간으로 먼저 찾고 <b>0행이면 코드만으로 다시 찾는다</b>. 그 폴백을 빼면 유효기간이 지난 의사가 통째로 빈칸이 된다. 두 번째 조회까지 옮겼다. ## 세 갈래 시각 입원 · ComPrgStt='ILV' → 퇴원일시 입원 · 그 밖 → 문맥의 적용일시 외래 → 접수일시 담당의 유효기간을 볼 날짜도 갈린다(입원은 진료과 행의 CodEndDtm, 외래는 접수일자). ## 또 같은 함정을 밟을 뻔했다 ㉒ 를 처음 돌렸을 때 0열이 나왔다. 쿼리 문제로 보였지만 아니었다 — 시험 내원(1988년)에 P_CodInf 행이 아예 없었다. 앞서 "내원 0건인 환자로 판정하려 한" 것과 같은 함정이다. 행을 가진 내원을 찾아 걸도록 고쳤다: 내원 5000323 → 52열, 이름 3자. ## 게이트 - dotnet test 336/336 - --edit-smoke 실패 0 — 이름 검사가 26종 전부 실제 태그임을 확인 - --db-patient ①~㉒ 전건 통과 (㉒ 담당의 조회 신규) - --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
942780ae97
commit
5bf2cf034d
@@ -125,6 +125,54 @@ public sealed class PatientContextStore
|
|||||||
|
|
||||||
private static string Day(string stamp) => stamp.Length >= 8 ? stamp[..8] : stamp;
|
private static string Day(string stamp) => stamp.Length >= 8 ? stamp[..8] : stamp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 이 내원의 담당의 — <c>OCM_담당의사</c> 계열이 쓴다. 없으면 빈 사전.
|
||||||
|
///
|
||||||
|
/// <b>레거시와 의도적으로 다른 곳이 한 군데 있다.</b>
|
||||||
|
/// 원본 <c>GetCodInfDT(..., With_Join)</c> 는 <c>CodComNum</c> + <c>CodMtiSeq='0'</c> 으로만 거르고
|
||||||
|
/// <b>ORDER BY 가 없는데</b> 호출부는 <c>Rows(0)</c> 을 집는다.
|
||||||
|
/// 이 DB 에서 세어 보니 그런 내원이 <b>25,488건</b>이고 최대 <b>41행</b>이다 —
|
||||||
|
/// 즉 레거시는 그 내원들의 담당의를 확정적으로 찍지 못한다. 재현할 대상이 없으므로
|
||||||
|
/// "그대로 옮긴다"가 성립하지 않는다.
|
||||||
|
///
|
||||||
|
/// 그래서 적용일시로 거르고 최신순 1행을 집는다 — 문맥의 <c>CodInf</c> 와 같은 규칙이다.
|
||||||
|
/// 레거시도 이 시각을 <b>넘기기는 한다</b>(마스터 유효기간 조인에만 쓸 뿐이다).
|
||||||
|
/// 근거는 <c>docs/TAG-PORTABLE-48.md</c>.
|
||||||
|
///
|
||||||
|
/// 담당의 조회는 레거시 <c>GetUidMst</c>(clsCommonLib.vb:10615)를 그대로 옮긴다 —
|
||||||
|
/// 유효기간으로 먼저 찾고, <b>0행이면 코드만으로 다시 찾는다.</b>
|
||||||
|
/// 그 폴백을 빼면 유효기간이 지난 의사가 통째로 빈칸이 된다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="moment">진료과 행을 고를 적용일시 12자리</param>
|
||||||
|
/// <param name="doctorDay">담당의 유효기간을 볼 날짜 8자리. 비면 <paramref name="moment"/> 의 앞 8자리</param>
|
||||||
|
public IReadOnlyDictionary<string, string> Doctor(decimal comNum, string moment, string? doctorDay = null)
|
||||||
|
{
|
||||||
|
using var connection = new OracleConnection(connectionString);
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
var cod = Row(connection,
|
||||||
|
"SELECT * FROM (SELECT * FROM P_CodInf WHERE CodComNum = :k AND CodMtiSeq = 0"
|
||||||
|
+ " AND :t BETWEEN CodStrDtm AND CodEndDtm ORDER BY CodStrDtm DESC) WHERE ROWNUM = 1",
|
||||||
|
("k", comNum), ("t", moment));
|
||||||
|
var code = PatientContext.Value(cod, "CodDtrCod");
|
||||||
|
if (code.Length == 0)
|
||||||
|
{
|
||||||
|
return cod;
|
||||||
|
}
|
||||||
|
// 레거시는 입원이면 CodEndDtm 의 날짜로, 외래면 접수일자로 본다 — 호출부가 정해 넘긴다
|
||||||
|
var day = Day((doctorDay ?? string.Empty).Trim().Length > 0 ? doctorDay!.Trim() : moment);
|
||||||
|
|
||||||
|
var uid = Row(connection,
|
||||||
|
"SELECT * FROM (SELECT * FROM M_UidMst WHERE UidCod = :c"
|
||||||
|
+ " AND :d BETWEEN UidStrDte AND UidEndDte) WHERE ROWNUM = 1",
|
||||||
|
("c", code), ("d", day));
|
||||||
|
return uid.Count > 0
|
||||||
|
? uid
|
||||||
|
// 폴백 — 유효기간 밖이어도 그 코드의 행을 쓴다(레거시 :10641-10644)
|
||||||
|
: Row(connection, "SELECT * FROM (SELECT * FROM M_UidMst WHERE UidCod = :c) WHERE ROWNUM = 1",
|
||||||
|
("c", code));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 건물관리번호로 주소 — 레거시 <c>GetZipInfoByBuildingNumber</c>(clsCommonLib.vb:16600).
|
/// 건물관리번호로 주소 — 레거시 <c>GetZipInfoByBuildingNumber</c>(clsCommonLib.vb:16600).
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -2168,6 +2168,25 @@ public static class DbSmoke
|
|||||||
{
|
{
|
||||||
lines.Add($" [조사] CodMtiSeq=0 이 2행 이상인 내원: {line}");
|
lines.Add($" [조사] CodMtiSeq=0 이 2행 이상인 내원: {line}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ㉒ 담당의 조회가 실제로 행을 돌려주는가.
|
||||||
|
// 적용일시 BETWEEN + 최신순 1행으로 골라야 결정적이다(위 조사 근거).
|
||||||
|
// 0행이면 그 조건이 어긋난 것이라 태그 셋이 통째로 빈다.
|
||||||
|
{
|
||||||
|
// 시험 내원에 진료과 행이 <b>아예 없으면</b> 0열이 나오는데
|
||||||
|
// 그건 쿼리가 아니라 데이터다. P_CodInf 에 행을 가진 내원을 찾아 건다 —
|
||||||
|
// 앞서 이 함정을 밟은 적이 있다(내원 0건인 환자로 판정하려 했다).
|
||||||
|
var owner = FirstComNum(conn, "P_CodInf", "CodComNum", "CodStrDtm") ?? target;
|
||||||
|
var ownerCtx = owner == target ? ctx : contexts.Load(owner);
|
||||||
|
var doctor = ownerCtx is null
|
||||||
|
? new Dictionary<string, string>()
|
||||||
|
: (IReadOnlyDictionary<string, string>)contexts.Doctor(owner, ownerCtx.AdpDtm);
|
||||||
|
var uidNam = PatientContext.Value(doctor, "UidNam");
|
||||||
|
lines.Add($" 담당의 조회: 내원 {owner:F0} → {doctor.Count}열,"
|
||||||
|
+ $" 이름 {uidNam.Length}자 (값은 기록하지 않는다)");
|
||||||
|
Check("㉒ 담당의 조회가 예외 없이 돌고 행을 돌려준다", doctor.Count > 0,
|
||||||
|
"0열 — 적용일시 BETWEEN 조건이나 CodDtrCod 를 의심할 것");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,45 @@ public static class PatientIdentity
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 이 내원의 담당의 행 — 못 읽으면 빈 사전.
|
||||||
|
///
|
||||||
|
/// 어느 시각의 진료과를 볼지는 레거시 <c>OCM_담당의사</c> 가 세 갈래로 정한다
|
||||||
|
/// (bzDataInterface.vb):
|
||||||
|
/// · 입원이고 <c>ComPrgStt='ILV'</c>(퇴원) → 퇴원일시
|
||||||
|
/// · 입원이고 그 밖 → 지금(= 문맥이 정한 적용일시)
|
||||||
|
/// · 외래 → 접수일시
|
||||||
|
/// 담당의 유효기간을 볼 날짜도 갈린다 — 입원은 진료과 행의 <c>CodEndDtm</c>, 외래는 접수일자.
|
||||||
|
/// </summary>
|
||||||
|
public static IReadOnlyDictionary<string, string> DoctorOf(PatientContext context)
|
||||||
|
{
|
||||||
|
var connection = ConfigService.Current.ConnectionString;
|
||||||
|
var empty = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
if (connection.Length == 0)
|
||||||
|
{
|
||||||
|
return empty;
|
||||||
|
}
|
||||||
|
var inpatient = PatientContext.Value(context.ComInf, "ComPatTyp") == "I";
|
||||||
|
var accepted = PatientContext.Value(context.ComInf, "ComAcpDtm");
|
||||||
|
var left = PatientContext.Value(context.ComInf, "ComLevDtm");
|
||||||
|
var discharged = PatientContext.Value(context.ComInf, "ComPrgStt") == "ILV";
|
||||||
|
|
||||||
|
var moment = inpatient
|
||||||
|
? (discharged && left.Length >= 12 ? left[..12] : context.AdpDtm)
|
||||||
|
: (accepted.Length >= 12 ? accepted[..12] : context.AdpDtm);
|
||||||
|
// 입원은 진료과 행의 종료일시로 담당의 유효기간을 본다 — 그 값은 저장소가 행을 읽은 뒤에야 안다.
|
||||||
|
// 여기서는 외래만 날짜를 지정하고, 입원은 저장소가 적용일시에서 뽑게 둔다.
|
||||||
|
var doctorDay = inpatient ? null : (accepted.Length >= 8 ? accepted[..8] : null);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return new PatientContextStore(connection).Doctor(context.ComNum, moment, doctorDay);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 이 환자의 주소 조각들(구역번호·도로명·지번·영문) — 못 읽으면 빈 사전.
|
/// 이 환자의 주소 조각들(구역번호·도로명·지번·영문) — 못 읽으면 빈 사전.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -131,13 +131,18 @@ public sealed class PatientTagResolver : ITagValueResolver
|
|||||||
/// <summary>M_ZipMst 에서 만든 주소 조각들 — 없으면 상세주소만 쓴다</summary>
|
/// <summary>M_ZipMst 에서 만든 주소 조각들 — 없으면 상세주소만 쓴다</summary>
|
||||||
private readonly IReadOnlyDictionary<string, string> address;
|
private readonly IReadOnlyDictionary<string, string> address;
|
||||||
|
|
||||||
|
/// <summary>이 내원의 담당의 행(M_UidMst) — 없으면 빈 사전</summary>
|
||||||
|
private readonly IReadOnlyDictionary<string, string> doctor;
|
||||||
|
|
||||||
public PatientTagResolver(PatientContext context, ITagValueResolver next,
|
public PatientTagResolver(PatientContext context, ITagValueResolver next,
|
||||||
string residentNumber = "", IReadOnlyDictionary<string, string>? address = null)
|
string residentNumber = "", IReadOnlyDictionary<string, string>? address = null,
|
||||||
|
IReadOnlyDictionary<string, string>? doctor = null)
|
||||||
{
|
{
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.next = next;
|
this.next = next;
|
||||||
this.residentNumber = residentNumber;
|
this.residentNumber = residentNumber;
|
||||||
this.address = address ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
this.address = address ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
this.doctor = doctor ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -153,6 +158,10 @@ public sealed class PatientTagResolver : ITagValueResolver
|
|||||||
{
|
{
|
||||||
return located;
|
return located;
|
||||||
}
|
}
|
||||||
|
if (FromDoctor(tag) is { } doctorValue)
|
||||||
|
{
|
||||||
|
return doctorValue;
|
||||||
|
}
|
||||||
if (Computed.TryGetValue(tag, out var compute))
|
if (Computed.TryGetValue(tag, out var compute))
|
||||||
{
|
{
|
||||||
var made = compute(context);
|
var made = compute(context);
|
||||||
@@ -260,6 +269,32 @@ public sealed class PatientTagResolver : ITagValueResolver
|
|||||||
: new TagValue(false, "이 환자에게는 주소 정보가 없습니다");
|
: new TagValue(false, "이 환자에게는 주소 정보가 없습니다");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 담당의 3종 — 해당 태그가 아니면 null.
|
||||||
|
/// 세 태그의 차이는 마지막 한 줄뿐이다(이름 / 이름(코드) / 휴대전화).
|
||||||
|
/// </summary>
|
||||||
|
private TagValue? FromDoctor(string tag)
|
||||||
|
{
|
||||||
|
if (tag is not ("OCM_담당의사" or "OCM_담당의사_담당의사코드" or "OCM_담당의사_연락처"))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
string Field(string column) => doctor.TryGetValue(column, out var v) ? v.Trim() : string.Empty;
|
||||||
|
|
||||||
|
var name = Field("UidNam");
|
||||||
|
var value = tag switch
|
||||||
|
{
|
||||||
|
"OCM_담당의사" => name,
|
||||||
|
"OCM_담당의사_담당의사코드" => name.Length > 0 ? $"{name}({Field("UidCod")})" : string.Empty,
|
||||||
|
_ => Field("UidMblPhn"),
|
||||||
|
};
|
||||||
|
return value.Length > 0
|
||||||
|
? new TagValue(true, value)
|
||||||
|
: new TagValue(false, doctor.Count == 0
|
||||||
|
? "이 내원의 담당의를 찾지 못했습니다"
|
||||||
|
: "담당의에게 그 값이 없습니다");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>주민번호에서 나온 값 — 비면 <b>왜</b> 비었는지 말한다</summary>
|
/// <summary>주민번호에서 나온 값 — 비면 <b>왜</b> 비었는지 말한다</summary>
|
||||||
private TagValue Wrap(string value) => value.Length > 0
|
private TagValue Wrap(string value) => value.Length > 0
|
||||||
? new TagValue(true, value)
|
? new TagValue(true, value)
|
||||||
@@ -273,6 +308,7 @@ public sealed class PatientTagResolver : ITagValueResolver
|
|||||||
{
|
{
|
||||||
"PAT_주민번호", "PAT_주민번호_Dash", "PAT_성별", "PAT_한글성별",
|
"PAT_주민번호", "PAT_주민번호_Dash", "PAT_성별", "PAT_한글성별",
|
||||||
"PAT_우편번호", "PAT_도로명주소", "PAT_지번주소", "PAT_영문도로명주소",
|
"PAT_우편번호", "PAT_도로명주소", "PAT_지번주소", "PAT_영문도로명주소",
|
||||||
|
"OCM_담당의사", "OCM_담당의사_담당의사코드", "OCM_담당의사_연락처",
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -222,7 +222,8 @@ public partial class PreviewWindow : Window
|
|||||||
tags = picked is null
|
tags = picked is null
|
||||||
? session
|
? session
|
||||||
: new PatientTagResolver(picked, session,
|
: new PatientTagResolver(picked, session,
|
||||||
PatientIdentity.ResidentNumberOf(picked), PatientIdentity.AddressOf(picked));
|
PatientIdentity.ResidentNumberOf(picked), PatientIdentity.AddressOf(picked),
|
||||||
|
PatientIdentity.DoctorOf(picked));
|
||||||
// 러너를 <b>새로</b> 만든다. 같은 러너를 재사용하면 옛 환자의 조회 결과가 캐시에 남아
|
// 러너를 <b>새로</b> 만든다. 같은 러너를 재사용하면 옛 환자의 조회 결과가 캐시에 남아
|
||||||
// 환자를 바꿨는데 표는 그대로가 된다 — 태그만 바뀌고 표는 안 바뀌면 아무도 눈치채지 못한다.
|
// 환자를 바꿨는데 표는 그대로가 된다 — 태그만 바뀌고 표는 안 바뀌면 아무도 눈치채지 못한다.
|
||||||
fields = new MDataTableRunner(designer.Document,
|
fields = new MDataTableRunner(designer.Document,
|
||||||
|
|||||||
Reference in New Issue
Block a user