레거시 대비 전수조사에서 나온 출시 차단 9건 중, 하루 안에 끝나는 국소 수정 7건. 저장 데이터를 망가뜨리는 ③④ 를 먼저 고쳤다. ③ 페이지 복제·추가가 루트 이름을 중복 발급했다 Clone 이 Root.Id 까지 베끼는데 자식만 재발급해서 한 서식에 MDesignerHost1 이 둘 생겼다. AddPage 도 '페이지 수 + 1' 로 지어서 루트가 1·3 인 2장짜리 서식에서 곧바로 3 이 겹쳤다. E_SctMst 동기화는 SctObjNam 사전이라 둘째 루트가 첫째 루트 행에 매핑되고, 그 페이지 자식들의 SctParObj 가 없는 ObjID 를 가리킨 채 남는다. → 두 경로 모두 RenamePageRoot 로 이름과 Name 속성을 함께 새로 받는다. ④ 이름 채번이 빈 번호를 메웠다 TextBox3 을 지우고 새로 놓으면 다시 TextBox3 이 나왔다. 이름은 저장된 답변 (E_SctDta.SctObjNam)이 붙는 열쇠라서, 지운 칸의 과거 입력이 새 칸에 그대로 나타난다. 액션 대상과 CalcBox 수식도 이름으로 서로를 가리키므로 조용히 오배선된다. → 레거시 clsNameCreationService 규약대로 '사용 중 최대번호 + 1'. 숫자가 아닌 접미사(TextBoxTotal)는 번호로 보지 않는다. ① 탭순서 적용이 문서 전체를 밀었다 한 페이지를 편집해도 전 페이지 컨트롤이 999999+i 로 초기화됐다. 레거시는 페이지마다 별도 디자인 서피스라 다른 페이지는 건드리지 않는다. → 초기화 범위를 이번에 클릭한 페이지로 한정. ② 이력본이 열린 상태에서 현재본을 열면 이력 탭이 활성화됐다 FindOpenDbDocument 가 ShtCod 만 보고 있었다. 사용자는 현재본을 고친다고 믿은 채 읽기 전용 스냅샷을 편집하게 된다. → HistorySdgKey is null 조건 추가. ⑤ 레거시에 없는 태그 3종을 고를 수 있었다 PAT_BMI · PAT_IBW · PAT_IBW_퍼센트 는 bzDataInterface.vb:922/957/970 에서 주석 처리된 채로 남아 있어 런타임에 해석되지 않는다. 고르면 배포 후에야 빈칸으로 드러난다. → 카탈로그에서 제거(값은 자유 입력이라 기존 서식에는 영향 없다). ⑥ 신규 서식 등록이 ShtHspYon·ShtExpDte 를 비웠다 --db-shtmst 진단을 신설해 실측한 결과 운영 E_ShtMst 1,737행 중 두 컬럼이 NULL 인 행은 0건이다. ShtExpDte 는 dtEMRLoader.vb:1229 가 NVL(ShtExpDte,' ')='29991231' 로 못박아 거르고, 여러 조회가 sysdate BETWEEN ShtAdpDte AND ShtExpDte 를 쓴다 — NULL 이면 BETWEEN 이 거짓이라 등록은 성공했는데 EMR 어디에서도 안 보이는 서식이 된다(29991231 이 1,718행 98.9%). ShtHspYon 은 기록지정보·자동생성 목록이 'Y' 로 거르며, 디자이너가 만든 서식 169건 중 160건이 'Y'. → 두 값을 INSERT 에 넣는다. ⑦ 빈 선택 상태의 Ctrl+드래그가 예외로 죽었다 Ctrl/Shift 클릭은 선택을 바꾸지 않고 업에서 토글하므로 주선택이 빈 채 드래그가 시작될 수 있는데, BeginMove 가 Selection.Primary! 를 그대로 넘겨 RootAncestorOf 가 즉시 역참조했다. → 앵커를 Primary ?? 첫 선택으로 완화. 편집 스모크 9건 추가(루트 이름 유일 2 · 채번 4 · 탭순서 페이지 격리 1 · 빈 선택 드래그 1 · 태그 3종 부재 1). 채번 검사는 '가운데 번호'를 지운다 — 최대 번호를 지우면 최대값 자체가 내려가므로 같은 이름이 다시 나오는 것이 레거시와 같은 정상 동작이다. 회귀: 테스트 124/124, 편집 스모크 169건 실패 0, DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일(HEAD 대비 md5 일치). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
559 lines
26 KiB
C#
559 lines
26 KiB
C#
using Oracle.ManagedDataAccess.Client;
|
|
using SheetMe.Core.Models;
|
|
using SheetMe.Core.Serialization;
|
|
|
|
namespace SheetMe.Data.Stores;
|
|
|
|
/// <summary>서식 목록 1건(열기 대화상자)</summary>
|
|
/// <param name="DesignAllowed">서식생성기 사용 허용(E_ShtMst.ShtUsrDesYon) — 목록 잠금 표시용</param>
|
|
/// <param name="ClassCode">분류 코드(E_ShtMst.ShtClsCod) — 그룹 정렬 키</param>
|
|
/// <param name="ClassName">분류 한글명(M_DtlMst.DtlCodNam) — 그룹 헤더 표시</param>
|
|
public sealed record SheetSummary(
|
|
string ShtCod,
|
|
string Name,
|
|
bool HasDesign,
|
|
bool DesignAllowed,
|
|
string ClassCode,
|
|
string ClassName);
|
|
|
|
/// <summary>서식생성기 사용 권한 판정 결과</summary>
|
|
public enum SheetDesignPermission
|
|
{
|
|
/// <summary>E_ShtMst 에 서식 코드가 없음</summary>
|
|
NotRegistered,
|
|
|
|
/// <summary>허용 — ShtUsrDesYon 이 'Y' 또는 미설정</summary>
|
|
Allowed,
|
|
|
|
/// <summary>거부 — ShtUsrDesYon 이 'N' 등</summary>
|
|
Denied,
|
|
}
|
|
|
|
/// <summary>디자인 버전 1건</summary>
|
|
public sealed record DesignVersionInfo(decimal SdgKey, string UpdDtm, string UpdUid, bool Deleted);
|
|
|
|
/// <summary>
|
|
/// 레거시 테이블(E_ShtMst/E_SdgMst/E_SctMst) 기반 서식 저장소 — 레거시 디자이너와 동일 저장 의미론.
|
|
/// 저장 흐름(bzSaveSheetDesignNControlInfo 이식, 단일 트랜잭션):
|
|
/// ShtCneYon='Y'+활성행 존재 → 제자리 갱신(E_SctMst 는 신규 이름만 추가 + 기존 행 SctObjTxt 갱신),
|
|
/// 그 외 → 기존 활성행 SdgDelYon='Y' 후 신규 행 INSERT + E_SctMst 전체 재생성.
|
|
/// 채번: 전용 시퀀스 NEXTVAL 우선, 부재 시 MAX+1 폴백(트랜잭션 내 — [200]SheetMe 검증 방식).
|
|
/// </summary>
|
|
public sealed class OracleLegacyFormStore
|
|
{
|
|
#region Member Fields
|
|
/// <summary>Spread AsTemplate 항목 행의 SctObjTyp — 이 행은 ObjName 이 항목 텍스트라 컨트롤 이름과 구분해야 한다</summary>
|
|
private const string SpreadItemType = "ItemOfSpreadAsTemplate";
|
|
|
|
private readonly string connectionString;
|
|
private readonly LegacyXmlSerializer serializer = new();
|
|
#endregion
|
|
|
|
#region Constructors
|
|
public OracleLegacyFormStore(string connectionString)
|
|
{
|
|
this.connectionString = connectionString;
|
|
}
|
|
#endregion
|
|
|
|
#region Methods - 조회
|
|
/// <summary>
|
|
/// 서식 목록 검색 — 공백제거 UPPER LIKE 토큰 AND 매칭, 스캔서식(ShtTyp='S') 제외.
|
|
///
|
|
/// 분류(ShtClsCod)를 함께 읽어 목록을 묶는다. 코드→한글명은 <c>M_DtlMst</c> 의
|
|
/// <c>DtlTblCod='EMR_SHEET_GROUP'</c> 행이 소유하며, EMR 뷰어 트리가 쓰는 것과 같은 조인이라
|
|
/// (`[200]SheetMe/src/Data/SheetMe.Data/EmrTreeDataBusiness.cs:55`) 사용자가 이미 아는 분류가 그대로 나온다.
|
|
/// TRIM 비교 필수 — 두 컬럼 다 고정폭 잔여 공백이 섞여 있다.
|
|
/// 실측(운영 1,628건): 코드 24종 전부 마스터에 존재, 미분류는 1건뿐.
|
|
/// </summary>
|
|
public List<SheetSummary> ListSheets(string? keyword, int max = 300)
|
|
{
|
|
using var connection = Open();
|
|
using var command = connection.CreateCommand();
|
|
command.BindByName = true;
|
|
|
|
var where = "WHERE NVL(s.ShtTyp,' ') <> 'S'";
|
|
var tokens = (keyword ?? string.Empty)
|
|
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
|
for (var i = 0; i < tokens.Length; i++)
|
|
{
|
|
// 분류명도 검색 대상 — 헤더에 보이는 '간호기록'을 그대로 쳤는데 0건이면 배신감이 크다
|
|
where += $" AND (UPPER(REPLACE(s.ShtKorNam,' ','')) LIKE :k{i} OR UPPER(s.ShtCod) LIKE :k{i}" +
|
|
$" OR UPPER(REPLACE(dtl.DtlCodNam,' ','')) LIKE :k{i})";
|
|
command.Parameters.Add(new OracleParameter($"k{i}", $"%{tokens[i].ToUpperInvariant()}%"));
|
|
}
|
|
|
|
command.CommandText =
|
|
"SELECT * FROM (" +
|
|
" SELECT s.ShtCod, s.ShtKorNam," +
|
|
" (SELECT COUNT(*) FROM E_SdgMst d WHERE d.SdgShtCod = s.ShtCod AND NVL(d.SdgDelYon,' ') <> 'Y') AS DesignCount," +
|
|
" NVL(TRIM(s.ShtUsrDesYon),' ') AS UsrDesYon," +
|
|
" NVL(TRIM(s.ShtClsCod),' ') AS ClsCod," +
|
|
" NVL(dtl.DtlCodNam,' ') AS ClsNam" +
|
|
" FROM E_ShtMst s" +
|
|
// 코드당 1행으로 접어서 조인한다 — M_DtlMst 는 PK 에 DtlStrDte 가 들어가 같은 분류 코드가
|
|
// 기간별 다중행일 수 있고, 그대로 조인하면 서식이 목록에 두 번 뜬다(운영 실측은 현재 증식 0건이지만
|
|
// 스키마상 가능하므로 데이터 운에 기대지 않는다). 적용일 최신 행을 택한다.
|
|
" LEFT JOIN (SELECT TRIM(DtlCod) AS Cod, DtlCodNam, DtlDspSeq," +
|
|
" ROW_NUMBER() OVER (PARTITION BY TRIM(DtlCod) ORDER BY DtlStrDte DESC) AS Rn" +
|
|
" FROM M_DtlMst WHERE DtlTblCod = 'EMR_SHEET_GROUP') dtl" +
|
|
" ON dtl.Cod = TRIM(s.ShtClsCod) AND dtl.Rn = 1" +
|
|
// 그룹 순서는 이 정렬이 그대로 정한다(CollectionView 는 등장 순서로 그룹을 만든다).
|
|
// 병원이 정한 표시순서(DtlDspSeq)를 1순위로 — 레거시 기록지정보 분류 탭이 이 순서다
|
|
// (`[002]Master/[024]기록지정보/M.MST.ShtMst/dtShtMst.vb:361-368`). 이 병원은 DspSeq 가 전부
|
|
// 비어 있어 결과적으로 코드순이 되지만, 채워 쓰는 병원에서 순서가 어긋나지 않는다.
|
|
// LPAD 는 레거시(2자리)보다 넉넉한 4자리 — '100' 과 '99' 가 뒤집히는 것을 막는다.
|
|
$" {where} ORDER BY CASE WHEN TRIM(s.ShtClsCod) IS NULL THEN 1 ELSE 0 END," +
|
|
" LPAD(NVL(dtl.DtlDspSeq,'9999'), 4, '0'), s.ShtClsCod, s.ShtCod" +
|
|
$") WHERE ROWNUM <= {max}";
|
|
|
|
var result = new List<SheetSummary>();
|
|
using var reader = command.ExecuteReader();
|
|
while (reader.Read())
|
|
{
|
|
var classCode = reader.GetString(4).Trim();
|
|
var className = reader.GetString(5).Trim();
|
|
result.Add(new SheetSummary(
|
|
reader.GetString(0),
|
|
reader.IsDBNull(1) ? string.Empty : reader.GetString(1),
|
|
reader.GetDecimal(2) > 0,
|
|
IsDesignAllowed(reader.GetString(3)),
|
|
classCode,
|
|
// 마스터에 없는 코드는 코드 자체를 헤더로 — 사이트가 코드를 추가했는데 마스터를 안 채운 경우
|
|
// '(미분류)'로 뭉뚱그리면 서로 다른 분류가 한 덩어리가 된다.
|
|
className.Length > 0 ? className : classCode));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 서식생성기 사용 허용 여부(E_ShtMst.ShtUsrDesYon) — 레거시 frmSheetDesigner.vb:71-84 규칙.
|
|
/// 'Y' 또는 빈 문자열/NULL(미설정 = 레거시 데이터 호환)이면 허용, 그 외('N' 등)만 거부한다.
|
|
/// </summary>
|
|
public SheetDesignPermission GetDesignPermission(string shtCod)
|
|
{
|
|
using var connection = Open();
|
|
var value = Scalar(connection, null,
|
|
"SELECT NVL(TRIM(ShtUsrDesYon),' ') FROM E_ShtMst WHERE ShtCod = :s",
|
|
new OracleParameter("s", shtCod)) as string;
|
|
if (value is null)
|
|
{
|
|
return SheetDesignPermission.NotRegistered;
|
|
}
|
|
return IsDesignAllowed(value) ? SheetDesignPermission.Allowed : SheetDesignPermission.Denied;
|
|
}
|
|
|
|
/// <summary>ShtUsrDesYon 판정 — CHAR 패딩을 흡수하려 TRIM 후 비교(레거시보다 관대한 방향이라 오탐만 줄인다)</summary>
|
|
private static bool IsDesignAllowed(string flag)
|
|
{
|
|
var trimmed = flag.Trim();
|
|
return trimmed is "Y" or "";
|
|
}
|
|
|
|
/// <summary>활성(비삭제 최신) 디자인 로드 — 없으면 null. FormId/Title/SourceSdgKey 채움</summary>
|
|
public FormDocument? LoadActiveDesign(string shtCod)
|
|
{
|
|
using var connection = Open();
|
|
|
|
var sdgKey = ResolveActiveSdgKey(connection, null, shtCod);
|
|
if (sdgKey == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
using var command = connection.CreateCommand();
|
|
command.BindByName = true;
|
|
command.CommandText = "SELECT SdgDesign FROM E_SdgMst WHERE SdgKey = :k";
|
|
command.Parameters.Add(new OracleParameter("k", sdgKey));
|
|
var xml = command.ExecuteScalar() as string;
|
|
if (string.IsNullOrWhiteSpace(xml))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var document = serializer.Read(xml);
|
|
document.FormId = shtCod;
|
|
document.Title = QueryTitle(connection, shtCod) ?? shtCod;
|
|
document.Meta.SourceSdgKey = sdgKey;
|
|
return document;
|
|
}
|
|
|
|
/// <summary>활성 디자인 원문 XML — (SdgKey, XML), 없으면 null (일괄 왕복 스모크용)</summary>
|
|
public (decimal SdgKey, string Xml)? LoadActiveDesignRaw(string shtCod)
|
|
{
|
|
using var connection = Open();
|
|
var sdgKey = ResolveActiveSdgKey(connection, null, shtCod);
|
|
if (sdgKey == 0)
|
|
{
|
|
return null;
|
|
}
|
|
using var command = connection.CreateCommand();
|
|
command.BindByName = true;
|
|
command.CommandText = "SELECT SdgDesign FROM E_SdgMst WHERE SdgKey = :k";
|
|
command.Parameters.Add(new OracleParameter("k", sdgKey));
|
|
return command.ExecuteScalar() is string xml && !string.IsNullOrWhiteSpace(xml)
|
|
? (sdgKey, xml)
|
|
: null;
|
|
}
|
|
|
|
/// <summary>디자인 버전의 E_SctMst 행 수(검증용)</summary>
|
|
public int CountSctRows(decimal sdgKey)
|
|
{
|
|
using var connection = Open();
|
|
return Convert.ToInt32(Scalar(connection, null,
|
|
"SELECT COUNT(*) FROM E_SctMst WHERE SctSdgKey = :k", new OracleParameter("k", sdgKey))!);
|
|
}
|
|
|
|
/// <summary>서식의 제자리 갱신 여부(ShtCneYon)</summary>
|
|
public string GetShtCneYon(string shtCod)
|
|
{
|
|
using var connection = Open();
|
|
return Scalar(connection, null,
|
|
"SELECT NVL(ShtCneYon,' ') FROM E_ShtMst WHERE ShtCod = :s",
|
|
new OracleParameter("s", shtCod)) as string ?? " ";
|
|
}
|
|
|
|
/// <summary>특정 버전(SdgKey) 디자인 로드 — 이력 열람/복원용. 없으면 null</summary>
|
|
public FormDocument? LoadDesignByKey(string shtCod, decimal sdgKey)
|
|
{
|
|
using var connection = Open();
|
|
using var command = connection.CreateCommand();
|
|
command.BindByName = true;
|
|
command.CommandText = "SELECT SdgDesign FROM E_SdgMst WHERE SdgKey = :k";
|
|
command.Parameters.Add(new OracleParameter("k", sdgKey));
|
|
if (command.ExecuteScalar() is not string xml || string.IsNullOrWhiteSpace(xml))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var document = serializer.Read(xml);
|
|
document.FormId = shtCod;
|
|
document.Title = QueryTitle(connection, shtCod) ?? shtCod;
|
|
document.Meta.SourceSdgKey = sdgKey;
|
|
return document;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 서식의 Spread 디자인들(E_SpdMst) — 컨트롤명(SpdName) → FarPoint XML.
|
|
/// SpdShtCod 는 CHAR 패딩 컬럼이라 TRIM 비교(실측).
|
|
/// </summary>
|
|
public Dictionary<string, string> LoadSpreadDesigns(string shtCod)
|
|
{
|
|
var result = new Dictionary<string, string>(StringComparer.Ordinal);
|
|
using var connection = Open();
|
|
using var command = connection.CreateCommand();
|
|
command.BindByName = true;
|
|
command.CommandText =
|
|
"SELECT TRIM(SpdName), SpdDesign FROM E_SpdMst " +
|
|
"WHERE TRIM(SpdShtCod) = :s AND NVL(SpdDelYon,' ') <> 'Y'";
|
|
command.Parameters.Add(new OracleParameter("s", shtCod));
|
|
using var reader = command.ExecuteReader();
|
|
while (reader.Read())
|
|
{
|
|
if (!reader.IsDBNull(1))
|
|
{
|
|
result[reader.GetString(0)] = reader.GetString(1);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary>디자인 버전 이력(본문 제외)</summary>
|
|
public List<DesignVersionInfo> ListVersions(string shtCod)
|
|
{
|
|
using var connection = Open();
|
|
using var command = connection.CreateCommand();
|
|
command.BindByName = true;
|
|
command.CommandText =
|
|
"SELECT SdgKey, NVL(SdgUpdDtm,' '), NVL(SdgUidCod,' '), NVL(SdgDelYon,' ') " +
|
|
"FROM E_SdgMst WHERE SdgShtCod = :s ORDER BY SdgKey DESC";
|
|
command.Parameters.Add(new OracleParameter("s", shtCod));
|
|
|
|
var result = new List<DesignVersionInfo>();
|
|
using var reader = command.ExecuteReader();
|
|
while (reader.Read())
|
|
{
|
|
result.Add(new DesignVersionInfo(
|
|
reader.GetDecimal(0), reader.GetString(1).Trim(), reader.GetString(2).Trim(),
|
|
reader.GetString(3) == "Y"));
|
|
}
|
|
return result;
|
|
}
|
|
#endregion
|
|
|
|
#region Methods - 저장
|
|
/// <summary>
|
|
/// 디자인 저장 — 레거시 동일 의미론(제자리 갱신 vs 버저닝), 단일 트랜잭션.
|
|
/// 반환: 저장된 SdgKey.
|
|
/// </summary>
|
|
public decimal SaveDesign(FormDocument document, string uid)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(document.FormId))
|
|
{
|
|
throw new InvalidOperationException("서식 코드(FormId)가 없습니다.");
|
|
}
|
|
|
|
var mergedXml = serializer.Write(document);
|
|
|
|
using var connection = Open();
|
|
using var transaction = connection.BeginTransaction();
|
|
try
|
|
{
|
|
// 레거시 SystemDateTime 과 동일하게 DB 서버 시각을 쓴다 — 단말 시계가 밀리면
|
|
// ResolveActiveSdgKey 의 ORDER BY SdgUpdDtm 이 활성 버전을 잘못 고를 수 있다.
|
|
var now = ServerClock.Read(connection, transaction).Minute12;
|
|
|
|
var shtCneYon = Scalar(connection, transaction,
|
|
"SELECT NVL(ShtCneYon,' ') FROM E_ShtMst WHERE ShtCod = :s",
|
|
new OracleParameter("s", document.FormId)) as string;
|
|
if (shtCneYon is null)
|
|
{
|
|
throw new InvalidOperationException($"E_ShtMst 에 서식이 없습니다: {document.FormId}");
|
|
}
|
|
|
|
var activeSdgKey = ResolveActiveSdgKey(connection, transaction, document.FormId);
|
|
var updateInPlace = shtCneYon == "Y" && activeSdgKey > 0;
|
|
|
|
decimal sdgKey;
|
|
if (updateInPlace)
|
|
{
|
|
// 제자리 갱신 — 레거시 ShtCneYon='Y' 경로
|
|
sdgKey = activeSdgKey;
|
|
NonQuery(connection, transaction,
|
|
"UPDATE E_SdgMst SET SdgDesign = :d, SdgUidCod = :u, SdgUpdDtm = :t WHERE SdgKey = :k",
|
|
new OracleParameter("d", OracleDbType.Clob) { Value = mergedXml },
|
|
new OracleParameter("u", uid),
|
|
new OracleParameter("t", now),
|
|
new OracleParameter("k", sdgKey));
|
|
}
|
|
else
|
|
{
|
|
// 기존 활성행 히스토리 처리 후 신규 행
|
|
if (activeSdgKey > 0)
|
|
{
|
|
NonQuery(connection, transaction,
|
|
"UPDATE E_SdgMst SET SdgDelYon = 'Y' WHERE SdgKey = :k",
|
|
new OracleParameter("k", activeSdgKey));
|
|
}
|
|
sdgKey = NextVal(connection, transaction, "E_SDGMST_SDGKEY",
|
|
"SELECT NVL(MAX(SdgKey),0)+1 FROM E_SdgMst");
|
|
NonQuery(connection, transaction,
|
|
"INSERT INTO E_SdgMst (SdgKey, SdgShtCod, SdgDesign, SdgUidCod, SdgUpdDtm, SdgDelYon, SdgDelUid, SdgDelDtm) " +
|
|
"VALUES (:k, :s, :d, :u, :t, '', '', '')",
|
|
new OracleParameter("k", sdgKey),
|
|
new OracleParameter("s", document.FormId),
|
|
new OracleParameter("d", OracleDbType.Clob) { Value = mergedXml },
|
|
new OracleParameter("u", uid),
|
|
new OracleParameter("t", now));
|
|
}
|
|
|
|
SaveSctMst(connection, transaction, sdgKey, mergedXml, updateInPlace);
|
|
|
|
transaction.Commit();
|
|
document.Meta.SourceSdgKey = sdgKey;
|
|
document.Meta.UpdatedBy = uid;
|
|
document.Meta.UpdatedAt = now;
|
|
return sdgKey;
|
|
}
|
|
catch
|
|
{
|
|
transaction.Rollback();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>E_ShtMst 존재 여부</summary>
|
|
public bool SheetExists(string shtCod)
|
|
{
|
|
using var connection = Open();
|
|
return Convert.ToInt32(Scalar(connection, null,
|
|
"SELECT COUNT(*) FROM E_ShtMst WHERE ShtCod = :s", new OracleParameter("s", shtCod))!) > 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 신규 서식 최소 등록 — 실DB 디자이너 서식 관행값(ShtTyp='D', 사용/디자인/컨트롤/EMR 'Y', 열람권한 'A' 등).
|
|
/// 상세 속성(권한/출력 등)은 레거시 기록지정보 마스터에서 후속 설정.
|
|
/// </summary>
|
|
public void RegisterSheet(string shtCod, string korName, string? clsCod, string uid)
|
|
{
|
|
if (shtCod.Length is 0 or > 10)
|
|
{
|
|
throw new ArgumentException("서식 코드는 1~10자여야 합니다.");
|
|
}
|
|
|
|
using var connection = Open();
|
|
using var transaction = connection.BeginTransaction();
|
|
try
|
|
{
|
|
var stamp = ServerClock.Read(connection, transaction);
|
|
// ShtUsrDesYon='Y' 하드코딩 — 여기서 만든 서식은 서식생성기가 만든 것이므로 열기 게이트를 자동 통과한다
|
|
//
|
|
// ShtExpDte='29991231' / ShtHspYon='Y' 는 빼면 안 되는 값이다(--db-shtmst 실측).
|
|
// · 운영 E_ShtMst 1,737행 중 두 컬럼이 NULL 인 행은 0건 — 비워 두면 우리 행만 예외가 된다.
|
|
// · ShtExpDte 는 dtEMRLoader.vb:1229 가 NVL(ShtExpDte,' ')='29991231' 로 못박아 걸러내고,
|
|
// 여러 조회가 sysdate BETWEEN ShtAdpDte AND ShtExpDte 를 쓴다 — NULL 이면 BETWEEN 이 거짓이라
|
|
// 등록은 성공했는데 EMR 어디에서도 안 보이는 서식이 된다(29991231 이 1,718행 98.9%).
|
|
// · ShtHspYon 은 기록지정보·자동생성 등 목록이 ShtHspYon='Y' 로 거르며,
|
|
// 디자이너가 만든 서식(ShtUsrDesYon='Y') 169건 중 160건이 'Y' 다.
|
|
NonQuery(connection, transaction,
|
|
"INSERT INTO E_ShtMst (ShtCod, ShtAdpDte, ShtExpDte, ShtKorNam, ShtClsCod, ShtTyp, ShtUseYon, " +
|
|
"ShtHspYon, ShtUsrDesYon, ShtUseSdg, ShtUseSct, ShtUseEmr, ShtRedAth, ShtWrtTyp, ShtCneYon, ShtTraSto, " +
|
|
"ShtEleSig, ShtUpdUid, ShtUpdDtm) " +
|
|
"VALUES (:cod, :adp, '29991231', :nam, :cls, 'D', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'A', '9A', 'N', 'N', 'N', :uid, :dtm)",
|
|
new OracleParameter("cod", shtCod),
|
|
new OracleParameter("adp", stamp.Date8),
|
|
new OracleParameter("nam", korName),
|
|
new OracleParameter("cls", string.IsNullOrWhiteSpace(clsCod) ? (object)DBNull.Value : clsCod),
|
|
new OracleParameter("uid", uid),
|
|
new OracleParameter("dtm", stamp.Minute12));
|
|
transaction.Commit();
|
|
}
|
|
catch
|
|
{
|
|
transaction.Rollback();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private void SaveSctMst(OracleConnection connection, OracleTransaction transaction,
|
|
decimal sdgKey, string mergedXml, bool updateInPlace)
|
|
{
|
|
var rows = SctMstXmlWalker.Walk(mergedXml);
|
|
|
|
// 제자리 갱신일 때만 기존 행을 읽는다. 컨트롤 행은 SctKey 로 갱신해야 하므로 키까지 가져오고,
|
|
// Spread AsTemplate 항목 행은 ObjName 이 항목 텍스트라 컨트롤 이름과 섞이면 안 되므로 따로 담는다.
|
|
Dictionary<string, decimal>? existingControls = null;
|
|
HashSet<string>? existingSpreadItems = null;
|
|
if (updateInPlace)
|
|
{
|
|
existingControls = new Dictionary<string, decimal>(StringComparer.Ordinal);
|
|
existingSpreadItems = new HashSet<string>(StringComparer.Ordinal);
|
|
using var query = connection.CreateCommand();
|
|
query.Transaction = transaction;
|
|
query.BindByName = true;
|
|
query.CommandText =
|
|
"SELECT SctKey, SctObjNam, NVL(SctObjTyp,' ') FROM E_SctMst WHERE SctSdgKey = :k ORDER BY SctKey";
|
|
query.Parameters.Add(new OracleParameter("k", sdgKey));
|
|
using var reader = query.ExecuteReader();
|
|
while (reader.Read())
|
|
{
|
|
var key = reader.GetDecimal(0);
|
|
var name = reader.GetString(1);
|
|
var type = reader.GetString(2).Trim();
|
|
if (string.Equals(type, SpreadItemType, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
existingSpreadItems.Add(name);
|
|
}
|
|
else if (!existingControls.ContainsKey(name))
|
|
{
|
|
// 동명 행이 이미 중복된 오염 데이터에서는 레거시 Select(...)(0) 과 동일하게 최초 1건만 채택
|
|
existingControls[name] = key;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (var row in rows)
|
|
{
|
|
if (updateInPlace)
|
|
{
|
|
if (string.Equals(row.ObjType, SpreadItemType, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// 레거시(bzSaveSheetDesignNControlInfo.vb:355-370)는 존재 검사 없이 매 저장마다 항목 행을
|
|
// 중복 INSERT 한다. 런타임은 개수를 세지 않으므로 그 버그는 복제하지 않고 행 수를 안정시킨다.
|
|
if (existingSpreadItems!.Contains(row.ObjName))
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
else if (existingControls!.TryGetValue(row.ObjName, out var existingKey))
|
|
{
|
|
// 레거시 bzSaveSheetDesignNControlInfo.vb:378 — 기존 행은 SctObjTxt 만 덮어쓴다.
|
|
// SctObjTyp/SctParObj/SctObjSeq/SctObjID 는 의도적으로 갱신하지 않는다(SctKey 보존 의미론).
|
|
// 삭제된 컨트롤의 잔존 행도 레거시와 동일하게 지우지 않는다.
|
|
NonQuery(connection, transaction,
|
|
"UPDATE E_SctMst SET SctObjTxt = :txt WHERE SctKey = :k",
|
|
new OracleParameter("txt", row.ObjText.Length == 0 ? (object)DBNull.Value : row.ObjText),
|
|
new OracleParameter("k", existingKey));
|
|
continue;
|
|
}
|
|
}
|
|
|
|
var sctKey = NextVal(connection, transaction, "E_SCTMST_SCTKEY",
|
|
"SELECT NVL(MAX(SctKey),0)+1 FROM E_SctMst");
|
|
NonQuery(connection, transaction,
|
|
"INSERT INTO E_SctMst (SctKey, SctSdgKey, SctObjNam, SctObjID, SctObjTyp, SctParObj, SctObjSeq, SctObjTxt) " +
|
|
"VALUES (:k, :sdg, :nam, :oid, :typ, :par, :seq, :txt)",
|
|
new OracleParameter("k", sctKey),
|
|
new OracleParameter("sdg", sdgKey),
|
|
new OracleParameter("nam", row.ObjName),
|
|
new OracleParameter("oid", row.ObjId),
|
|
new OracleParameter("typ", row.ObjType),
|
|
new OracleParameter("par", row.ParentId),
|
|
new OracleParameter("seq", row.ObjSeq.Length == 0 ? "0" : row.ObjSeq),
|
|
new OracleParameter("txt", row.ObjText.Length == 0 ? (object)DBNull.Value : row.ObjText));
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Methods - Private
|
|
private OracleConnection Open()
|
|
{
|
|
var connection = new OracleConnection(connectionString);
|
|
connection.Open();
|
|
return connection;
|
|
}
|
|
|
|
/// <summary>활성(비삭제 최신) SdgKey — 없으면 0 ([200] SdgKeyResolver 의미론)</summary>
|
|
private static decimal ResolveActiveSdgKey(OracleConnection connection, OracleTransaction? transaction, string shtCod)
|
|
{
|
|
var value = Scalar(connection, transaction,
|
|
"SELECT NVL((SELECT SdgKey FROM (SELECT SdgKey FROM E_SdgMst " +
|
|
"WHERE SdgShtCod = :s AND NVL(SdgDelYon,' ') <> 'Y' ORDER BY SdgUpdDtm DESC, SdgKey DESC) " +
|
|
"WHERE ROWNUM = 1), 0) FROM DUAL",
|
|
new OracleParameter("s", shtCod));
|
|
return value is null or DBNull ? 0m : Convert.ToDecimal(value);
|
|
}
|
|
|
|
private static string? QueryTitle(OracleConnection connection, string shtCod)
|
|
=> Scalar(connection, null,
|
|
"SELECT ShtKorNam FROM E_ShtMst WHERE ShtCod = :s", new OracleParameter("s", shtCod)) as string;
|
|
|
|
/// <summary>채번 — 전용 시퀀스 NEXTVAL 우선, 시퀀스 부재(ORA-02289/00942) 시 MAX+1 폴백</summary>
|
|
private static decimal NextVal(OracleConnection connection, OracleTransaction transaction, string sequence, string fallbackSql)
|
|
{
|
|
try
|
|
{
|
|
return Convert.ToDecimal(Scalar(connection, transaction, $"SELECT {sequence}.NEXTVAL FROM DUAL")!);
|
|
}
|
|
catch (OracleException ex) when (ex.Number is 2289 or 942)
|
|
{
|
|
return Convert.ToDecimal(Scalar(connection, transaction, fallbackSql)!);
|
|
}
|
|
}
|
|
|
|
private static object? Scalar(OracleConnection connection, OracleTransaction? transaction, string sql, params OracleParameter[] parameters)
|
|
{
|
|
using var command = connection.CreateCommand();
|
|
if (transaction is not null)
|
|
{
|
|
command.Transaction = transaction;
|
|
}
|
|
command.BindByName = true;
|
|
command.CommandText = sql;
|
|
command.Parameters.AddRange(parameters);
|
|
return command.ExecuteScalar();
|
|
}
|
|
|
|
/// <summary>DML 실행 — 영향 행 수 반환(호출부가 무시해도 무방)</summary>
|
|
private static int NonQuery(OracleConnection connection, OracleTransaction transaction, string sql, params OracleParameter[] parameters)
|
|
{
|
|
using var command = connection.CreateCommand();
|
|
command.Transaction = transaction;
|
|
command.BindByName = true;
|
|
command.CommandText = sql;
|
|
command.Parameters.AddRange(parameters);
|
|
return command.ExecuteNonQuery();
|
|
}
|
|
#endregion
|
|
}
|