conflict 6건을 고치고 재료 재사용으로 10종을 더한다 — 30종 → 40종

## conflict 6건 (통합 계획의 "고칠 것")

값이 바뀌는 것 둘:
- ETC_현재년도 — "2026" → "2026년"(bzDataInterface.vb:16923 의 + "년").
  형제 ETC_현재일시_1_년 은 접미가 없어 둘을 갈랐다(ClockFormat.YearSuffixed 신설).
  "2026" 을 단정하던 테스트도 고쳤다.
- ETC_로그인_사용자ID_사용자명 — 레거시(:17930)는 빈 값 검사 없이 항상 결합한다.
  한쪽이 비어도 "U01." 이 그대로 나간다. 조건을 붙였던 것이 레거시와 달랐다.

행 선택이 바뀌는 것 둘:
- ETC_로그인_사용자연락처 / 원내연락처 — 전에는 M_UidMst 를 조건 없이 첫 행.
  이력이 여러 행인 사용자에서 옛 연락처가 나올 수 있었다.
  유효기간(SYSDATE 일자 BETWEEN)으로 고르고, 0행이면 레거시 폴백(:10641)처럼
  조건 없이 다시 찾는다.

의도적 차이로 기록한 것 둘(코드 주석):
- 유효기간 판정 일자 — 레거시는 로그인 시점, 여기는 SYSDATE(자정 넘긴 세션에서만 갈림).
- Trim — 레거시는 무가공, 여기는 걷는다(CHAR 잔여 공백이 종이에 찍히는 쪽이 더 이상하다).

## new 48종 중 재료 재사용으로 되는 10종

워크플로 매핑 상세(docs/new48-mappings.json)를 붙여 보니 46종이 needs-lookup 인데,
그중 10종은 <b>이미 든 행의 다른 컬럼</b>이거나 문맥 계산이었다:

- 담당의 행 재사용 3: OCM_담당의사_영문(UidEngNam) · 면허번호(UidLicNum) · 전문의번호(UidSpcLic)
  — 조회 갈래가 OCM_담당의사와 문자 그대로 같고 집는 컬럼만 다르다(:4094·4119·4144)
- CowInf 재사용 3: OCM_병실베드(CowBedCod) · 병동_병실 · 병동_병실_베드(" / " 결합, 행 없으면 "")
- 문맥 계산 1: OCM_내원유형(입원/응급실/외래 — ComPatTyp·ComEmgYon)
- User 계열 3: OCM_User진료과 · _한글명칭 · User진료의사명.
  로그인 사용자가 의사(UidDtrYon=Y)면 사용자의 과·이름, 아니면 환자의 것.
  HisUser 에 DoctorYn·DepOfficialName 을 추가했다(세션 조회 SELECT 확장).

남은 new 는 새 조회가 필요한 세 무리다: 상병(Okd_상병쿼리) · 수술(GetOprInfDT) ·
심사(Bkd_상병쿼리). 다음 배치.

## 게이트

- dotnet test 338/338 (현재년도·ID.사용자명 판정 갱신)
- --edit-smoke 실패 0 (이름 검사 40종)
- --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 15:05:38 +09:00
co-authored by Claude Opus 5
parent e62e43d176
commit 9278b39a6d
7 changed files with 813 additions and 13 deletions
@@ -47,7 +47,10 @@ public static class TagPreviewCatalog
["ETC_현재일자"] = ClockFormat.Date,
["ETC_현재일시"] = ClockFormat.DateTime,
["ETC_현재일시_MaskBox"] = ClockFormat.DateTimeMask,
["ETC_현재년도"] = ClockFormat.Year,
// "년" 접미가 붙는다(bzDataInterface.vb:16923 — Data2Format(...) + "년").
// 처음에 접미 없는 Year 로 넣어 레거시와 다른 값("2026")이 나갔다 —
// 형제 태그 ETC_현재일시_1_년(:16929)은 접미가 <b>없으므로</b> 둘을 갈라야 한다.
["ETC_현재년도"] = ClockFormat.YearSuffixed,
["ETC_현재시간"] = ClockFormat.Time,
["ETC_현재일자_영문"] = ClockFormat.DateEnglish,
["ETC_현재일시_1_년"] = ClockFormat.Year,
@@ -174,6 +177,7 @@ public static class TagPreviewCatalog
// 레거시 Data2Format_ENG 의 MM-DD-YYYY 재배치
ClockFormat.DateEnglish => $"{date[4..6]}-{date[6..8]}-{date[..4]}",
ClockFormat.Year => date[..4],
ClockFormat.YearSuffixed => date[..4] + "년",
ClockFormat.Month => date[4..6],
ClockFormat.Day => date[6..8],
ClockFormat.Hour => minute12[8..10],
@@ -194,14 +198,16 @@ public static class TagPreviewCatalog
{
UserField.Name => name,
UserField.Id => id,
UserField.IdAndName => id.Length > 0 && name.Length > 0 ? $"{id}.{name}" : string.Empty,
// 레거시(bzDataInterface.vb:17930)는 빈 값 검사 없이 <b>항상</b> 결합한다 —
// 한쪽이 비면 "id." 나 ".이름" 이 그대로 나간다. 조건을 붙였더니 레거시와 갈렸다.
UserField.IdAndName => $"{id}.{name}",
UserField.MobilePhone => mobile,
UserField.OfficePhone => office,
_ => string.Empty,
};
}
private enum ClockFormat { Date, DateTime, DateTimeMask, DateEnglish, Year, Month, Day, Hour, Minute, Time }
private enum ClockFormat { Date, DateTime, DateTimeMask, DateEnglish, Year, YearSuffixed, Month, Day, Hour, Minute, Time }
private enum UserField { Name, Id, IdAndName, MobilePhone, OfficePhone }
#endregion
+23 -3
View File
@@ -72,21 +72,41 @@ public sealed class TagPreviewStore
}
}
/// <summary>연락처 두 칸 — 세션에 없는 것은 이것뿐이라 이 한 줄만 더 읽는다</summary>
/// <summary>
/// 연락처 두 칸 — 세션에 없는 것은 이것뿐이라 이 한 줄만 더 읽는다.
///
/// <b>유효기간으로 고른다.</b> 레거시(GetUidMst, clsCommonLib.vb:10636)는
/// <c>:일자 BETWEEN UidStrDte AND UidEndDte</c> 로 행을 고르고,
/// 0행이면 코드만으로 다시 찾는다(:10641 폴백). 전에는 조건 없이 첫 행을 집어서
/// 이력이 여러 행인 사용자에서 <b>옛 연락처</b>가 나올 수 있었다.
/// 판정 일자는 레거시가 로그인 시점(GetAdpDtm 앞 8자)인데 여기는 서버 오늘이다 —
/// 자정을 넘겨 계속 켜 둔 세션에서만 갈리는 차이라 받아들이고 기록해 둔다.
/// </summary>
private static (string Mobile, string Office) ReadPhones(OracleConnection connection, string uidCod)
{
if (uidCod.Length == 0)
{
return (string.Empty, string.Empty);
}
var valid = ReadPhoneRow(connection,
"SELECT UIDMBLPHN, UIDOFFTEL FROM M_UIDMST WHERE UIDCOD = :uid"
+ " AND TO_CHAR(SYSDATE,'YYYYMMDD') BETWEEN UIDSTRDTE AND UIDENDDTE", uidCod);
return valid ?? ReadPhoneRow(connection,
"SELECT UIDMBLPHN, UIDOFFTEL FROM M_UIDMST WHERE UIDCOD = :uid", uidCod)
?? (string.Empty, string.Empty);
}
private static (string Mobile, string Office)? ReadPhoneRow(
OracleConnection connection, string sql, string uidCod)
{
using var command = connection.CreateCommand();
command.CommandText = "SELECT UIDMBLPHN, UIDOFFTEL FROM M_UIDMST WHERE UIDCOD = :uid";
command.CommandText = sql;
command.CommandTimeout = TimeoutSeconds;
command.Parameters.Add(new OracleParameter("uid", uidCod));
using var reader = command.ExecuteReader();
if (!reader.Read())
{
return (string.Empty, string.Empty);
return null;
}
return (reader.IsDBNull(0) ? string.Empty : reader.GetString(0),
reader.IsDBNull(1) ? string.Empty : reader.GetString(1));
+14 -3
View File
@@ -9,7 +9,11 @@ namespace SheetMe.Data.Stores;
public sealed record HisUser(
string UidCod, string UidNam,
string DepCod, string DepNam,
string HspCod, string HspNam)
string HspCod, string HspNam,
/// <summary>의사 여부(UidDtrYon=Y) — OCM_User진료과 계열이 로그인 사용자 분기에 쓴다</summary>
string DoctorYn = "",
/// <summary>부서 공식 명칭(DepOfcNam) — 개원일 조건이 걸리는 병원에서 한글명 대신 쓴다</summary>
string DepOfficialName = "")
{
/// <summary>미인증 세션(인자 없이 단독 실행) — DB 쓰기가 차단된다</summary>
public static readonly HisUser Unknown = new(string.Empty, string.Empty, string.Empty, string.Empty, string.Empty, string.Empty);
@@ -58,10 +62,16 @@ public sealed class UserContextStore
connection.Open();
using var command = connection.CreateCommand();
command.BindByName = true;
// 레거시와 두 가지가 <b>의도적으로</b> 다르다(워크플로 검증이 짚었고, 받아들이기로 했다):
// · 유효기간 판정 일자 — 레거시는 로그인 시점(GetAdpDtm 앞 8자, clsCommonLib.vb:10622),
// 여기는 SYSDATE. 자정을 넘겨 계속 켜 둔 세션에서만 갈린다.
// · Trim — 레거시는 UidCod/UidNam 을 무가공으로 쓰고(bzDataInterface.vb:16713, 16720)
// 여기는 걷는다. CHAR 잔여 공백이 종이에 찍히는 쪽이 더 이상하다고 판단했다.
command.CommandText =
"SELECT u.UidCod, NVL(u.UidNam,' '), " +
" NVL(u.UidDepCod,' '), NVL(d.DepKorNam,' '), " +
" NVL(u.UidHspCod,' '), NVL(h.HspInsNam,' ') " +
" NVL(u.UidHspCod,' '), NVL(h.HspInsNam,' '), " +
" NVL(u.UidDtrYon,' '), NVL(d.DepOfcNam,' ') " +
"FROM M_UidMst u " +
"LEFT JOIN M_DepMst d ON d.DepCod = u.UidDepCod " +
" AND TO_CHAR(SYSDATE,'YYYYMMDD') BETWEEN d.DepStrDte AND d.DepEndDte " +
@@ -79,7 +89,8 @@ public sealed class UserContextStore
return new HisUser(
reader.GetString(0).Trim(), reader.GetString(1).Trim(),
reader.GetString(2).Trim(), reader.GetString(3).Trim(),
reader.GetString(4).Trim(), reader.GetString(5).Trim());
reader.GetString(4).Trim(), reader.GetString(5).Trim(),
reader.GetString(6).Trim(), reader.GetString(7).Trim());
}
#endregion
}
@@ -87,6 +87,23 @@ public sealed class PatientTagResolver : ITagValueResolver
// 외래는 CowInf 가 0행이라 자연히 빈 값이 된다(레거시도 외래·비낮병동은 "" 다).
["OCM_병동"] = c => PatientContext.Value(c.CowInf, "CowWadCod"),
["OCM_병실"] = c => PatientContext.Value(c.CowInf, "CowRomCod"),
["OCM_병실베드"] = c => PatientContext.Value(c.CowInf, "CowBedCod"),
// 조합형 — 구분자는 " / " (양쪽 공백, 레거시 그대로). 행이 없으면 빈 값이지
// " / " 만 남기지 않는다(레거시는 rows>0 안에서만 결합한다).
["OCM_병동_병실"] = c => c.CowInf.Count == 0 ? string.Empty
: $"{PatientContext.Value(c.CowInf, "CowWadCod")} / {PatientContext.Value(c.CowInf, "CowRomCod")}",
["OCM_병동_병실_베드"] = c => c.CowInf.Count == 0 ? string.Empty
: $"{PatientContext.Value(c.CowInf, "CowWadCod")} / {PatientContext.Value(c.CowInf, "CowRomCod")}"
+ $" / {PatientContext.Value(c.CowInf, "CowBedCod")}",
// 내원유형 — 입원/응급실/외래(bzDataInterface.vb:2896-2918).
// 응급 여부는 내원행의 ComEmgYon 이다. 그 밖의 환자구분은 레거시도 "" 다.
["OCM_내원유형"] = c => Com(c, "ComPatTyp") switch
{
"I" => "입원",
"O" => Com(c, "ComEmgYon") == "Y" ? "응급실" : "외래",
_ => string.Empty,
},
};
private static string Pat(PatientContext c, string column) => PatientContext.Value(c.PatInf, column);
@@ -294,7 +311,8 @@ public sealed class PatientTagResolver : ITagValueResolver
/// </summary>
private TagValue? FromDoctor(string tag)
{
if (tag is not ("OCM_담당의사" or "OCM_담당의사_담당의사코드" or "OCM_담당의사_연락처"))
if (tag is not ("OCM_담당의사" or "OCM_담당의사_담당의사코드" or "OCM_담당의사_연락처"
or "OCM_담당의사_영문" or "OCM_담당의사면허번호" or "OCM_담당의사전문의번호"))
{
return null;
}
@@ -305,6 +323,11 @@ public sealed class PatientTagResolver : ITagValueResolver
{
"OCM_담당의사" => name,
"OCM_담당의사_담당의사코드" => name.Length > 0 ? $"{name}({Field("UidCod")})" : string.Empty,
// 아래 셋은 같은 담당의 행의 다른 컬럼일 뿐이다(bzDataInterface.vb:4094·4119·4144 —
// 조회 갈래가 OCM_담당의사와 문자 그대로 같고 마지막에 집는 컬럼만 다르다)
"OCM_담당의사_영문" => Field("UidEngNam"),
"OCM_담당의사면허번호" => Field("UidLicNum"),
"OCM_담당의사전문의번호" => Field("UidSpcLic"),
_ => Field("UidMblPhn"),
};
return value.Length > 0
@@ -343,6 +366,39 @@ public sealed class PatientTagResolver : ITagValueResolver
return hospitalName.Trim().Length > 0
? new TagValue(true, hospitalName.Trim())
: new TagValue(false, "요양기관명을 찾지 못했습니다(M_DepMst·M_HspMst 조인)");
// ── User 계열 — 로그인 사용자가 의사면 <b>사용자의</b> 과·이름, 아니면 환자의 것 ──
// (bzDataInterface.vb:5697-5826). 개원일 조건(HspStrDte>=20220701)은 세션에 없어
// 빈 값 결정이라 DepOfcNam 갈래를 타지 않는다 — OCM_진료과와 같은 결정이다.
case "OCM_User진료과":
case "OCM_User진료과_한글명칭":
{
if (UserSession.Current.DoctorYn == "Y")
{
var mine = UserSession.Current.DepNam;
return mine.Length > 0
? new TagValue(true, mine)
: new TagValue(false, "로그인 사용자의 부서명을 찾지 못했습니다");
}
var korNam = department.TryGetValue("DepKorNam", out var d) ? d.Trim() : string.Empty;
return korNam.Length > 0
? new TagValue(true, korNam)
: new TagValue(false, "이 내원의 진료과를 찾지 못했습니다");
}
case "OCM_User진료의사명":
{
if (UserSession.Current.DoctorYn == "Y")
{
return new TagValue(true, UserSession.Current.UidNam);
}
// 의사가 아니면 환자의 담당의 — 이미 든 담당의 행을 쓴다.
// 레거시는 접수일자로 유효기간을 다시 보지만(:5822) 우리 담당의 행과
// 사람이 갈리려면 같은 코드의 이력이 겹쳐야 한다 — 폴백까지 같아서 실질 동일.
var name = doctor.TryGetValue("UidNam", out var n) ? n.Trim() : string.Empty;
return name.Length > 0
? new TagValue(true, name)
: new TagValue(false, "이 내원의 담당의를 찾지 못했습니다");
}
default:
return null;
}
@@ -363,6 +419,8 @@ public sealed class PatientTagResolver : ITagValueResolver
"PAT_우편번호", "PAT_도로명주소", "PAT_지번주소", "PAT_영문도로명주소",
"OCM_담당의사", "OCM_담당의사_담당의사코드", "OCM_담당의사_연락처",
"OCM_진료과", "ETC_요양기관명칭_병원명",
"OCM_담당의사_영문", "OCM_담당의사면허번호", "OCM_담당의사전문의번호",
"OCM_User진료과", "OCM_User진료과_한글명칭", "OCM_User진료의사명",
};
/// <summary>