HIS 사용자 컨텍스트 + 서식생성기 권한 게이트 + 서버 시각 전환
[사용자 컨텍스트] 감사 컬럼에 Windows 계정명이 들어가던 6지점을 HIS UidCod 로 교체했다. 레거시 이력 화면(ucSheetHistory.vb:51)이 SdgUidCod 를 수정자로 표시하므로 지금까지는 감사 추적이 끊겨 있었다. - UserContextStore — M_UidMst 단독 조회로 인증 판정(레거시 bzLogIn 자동로그인과 동일 의미론, 비밀번호를 보지 않고 행 존재 + 유효기간만 확인). 부서/병원은 LEFT JOIN best-effort 표시용. 적용일자는 서버 시각을 쓴다 — 단말 시계가 밀리면 유효 계정이 거부될 수 있다. - UserSession — 정적 set-once. 생성자 주입을 택하지 않은 이유는 진단 모드(--db-*)가 ViewModel 그래프 밖이라 주입으로는 감사 기록 지점에 닿을 수 없기 때문이다. 대신 읽는 곳을 App 초기화 / FormDesignDataBusiness / MainViewModel 표시 세 군데로 제한했고, 저장소는 지금처럼 uid 를 파라미터로만 받는다. 진단 모드는 "SMOKE" 식별자를 쓴다. - StartupArguments — 레거시 규약 "UidCod,ComNum,ShtCod" 관용 파싱(PadRight 후 split) 이식. ComNum 은 레거시에서도 선언·대입뿐인 죽은 인자라 보존만 하고 쓰지 않는다. - App.OnStartup 재구성 — 진단 플래그 12종을 RunDiagnostic 으로 분리(각 분기가 종료 코드를 반환하도록 정리), 세션 확정 후 MainView 표시. 기동 인자에 ShtCod 가 있으면 PendingSheetCode 로 넘겨 빈 새 문서를 만들지 않고 그 서식만 연다(열기 실패 시에는 빈 문서로 폴백). - 인자로 받은 UidCod 조회에 실패하면 기동을 중단한다(레거시 :892 와 동형). 인자 없이 단독 실행하면 미인증으로 계속하되 DB 쓰기를 차단하고, 개발 편의는 His:DevUidCod 설정으로 뺐다. [권한 게이트] E_ShtMst.ShtUsrDesYon — 'Y' 또는 미설정이면 허용, 그 외만 거부(레거시 frmSheetDesigner.vb:71-84 규칙, 거부 문구도 그대로). 레거시는 게이트가 열기 진입점마다 흩어져 있어 이력 노드 클릭·MessageQueue 두 경로로 우회가 가능했는데, SheetMe 는 모든 열기가 OpenDbSheet 로 수렴하므로 거기 한 곳이면 구조적으로 우회가 불가능하다. 추가로 이력 열람과 DB 저장에도 넣었다 — 후자는 레거시에 아예 없던 구멍이다(열기 이후 플래그가 꺼질 수 있고, 파일에서 연 문서를 DB 에 저장하는 경로는 열기 게이트를 거치지 않는다). 목록에서 거르지 않고 잠금 아이콘으로 표시한다(안 보이면 사용자가 진단할 수 없다). --db-gate 진단 신설 — 실측 결과 차단되는 디자인 보유 서식 0건(N 은 1건뿐이고 디자인 없음). [서버 시각] ServerClock 신설(SELECT SYSTIMESTAMP 왕복 1회로 8/12/17자리 확보, 포맷은 invariant 로 C# 에서 — NLS 의존 제거). 감사 정확성뿐 아니라 ResolveActiveSdgKey 가 ORDER BY SdgUpdDtm 으로 활성 버전을 고르기 때문에, 시계가 뒤로 밀린 단말이 저장하면 활성 버전 판정이 뒤집힐 수 있었다. Reorder 는 루프 밖 1회로 바꿔 한 번의 순서 변경이 행마다 다른 시각을 남기지 않게 했다. 실패 시 폴백하지 않는다. RecordWordStore.Remove 에 auditUid 인자 추가 — 남길 컬럼은 없지만(런타임 조회 쿼리에 ShtStt 필터가 없어 논리 삭제로 바꾸면 지운 문구가 계속 노출된다) "모든 쓰기 API 는 uid 를 명시한다"는 규약을 컴파일 타임에 강제한다. 상용구 대화상자는 uid 를 생성자로 받고, 미인증이면 읽기 전용으로 연다. 검증: 사전 실측으로 M_UidMst/M_DepMst/M_HspMst 접근 확인(2단계 최대 리스크 해소, 유효 사용자 539명). 테스트 70/70, edit-smoke 실패 0(기동 인자 파싱 8건 추가), 왕복 1,271건 diff 0/예외 0, db-save-smoke 제자리 갱신 통과, db-word-smoke 통과. DB 직접 확인: P163 저장분 SdgUidCod='SMOKE', SdgUpdDtm 이 서버 시각과 일치 (변경 전 저장분 S999 는 'Msystech' 로 대비됨). 남은 항목: 단일 인스턴스 + Named Pipe IPC 는 미구현. 기동 인자·게이트가 먼저 필요했고, 바로가기 운영에서는 중복 실행 억제가 덜 급하다. 다중 사용자 병행 운영 전에 처리한다. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
5c97bf1440
commit
0f77cb717a
@@ -31,6 +31,7 @@ public static class ConfigLoader
|
||||
config.Provider = root["His:Provider"] ?? "Oracle";
|
||||
config.SaveMode = root["FormStore:SaveMode"] ?? "File";
|
||||
config.XmlFolder = root["FormStore:XmlFolder"] ?? "forms";
|
||||
config.DevUidCod = root["His:DevUidCod"] ?? string.Empty;
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
using SheetMe.Data.Stores;
|
||||
|
||||
namespace SheetMe.Designer.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 기동 시 사용자 세션 확정. 레거시 frmSheetDesigner.vb:892 와 동일하게
|
||||
/// "인자로 받은 UidCod 가 유효하지 않으면 기동 중단" 이다 — 호출자가 HIS 이므로
|
||||
/// 조용히 읽기 전용으로 떨어뜨리면 사용자가 저장이 안 되는 이유를 알 수 없다.
|
||||
/// </summary>
|
||||
public static class SessionBootstrap
|
||||
{
|
||||
#region Methods
|
||||
/// <summary>
|
||||
/// 세션 초기화. 진행 가능하면 true, 기동을 중단해야 하면 false 와 함께 사유를 돌려준다.
|
||||
///
|
||||
/// 인자에 UidCod 있음 → 조회 성공 시 정상 세션 / 실패 시 기동 중단.
|
||||
/// 인자 없음 → 개발 폴백(His:DevUidCod)을 시도하되 실패해도 미인증으로 계속(DB 쓰기만 차단).
|
||||
/// 접속 문자열이 없으면(파일 전용 모드) 조회 자체를 건너뛴다.
|
||||
/// </summary>
|
||||
public static bool TryInitialize(HisLaunch? launch, string connectionString, string devUidCod, out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
var requestedUid = launch?.UidCod ?? string.Empty;
|
||||
|
||||
if (connectionString.Length == 0)
|
||||
{
|
||||
// DB 접속 자체가 설정되지 않음 — 파일 전용 모드. 사용자 확인은 의미가 없다.
|
||||
UserSession.Initialize(HisUser.Unknown);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (requestedUid.Length == 0)
|
||||
{
|
||||
// 단독 실행: 개발 폴백만 시도하고, 실패해도 기동은 계속한다(DB 쓰기는 차단됨)
|
||||
if (devUidCod.Length > 0)
|
||||
{
|
||||
UserSession.Initialize(SafeResolve(connectionString, devUidCod) ?? HisUser.Unknown);
|
||||
}
|
||||
else
|
||||
{
|
||||
UserSession.Initialize(HisUser.Unknown);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
var user = SafeResolve(connectionString, requestedUid);
|
||||
if (user is null)
|
||||
{
|
||||
error = $"사용자 [{requestedUid}] 를 확인할 수 없습니다.\n" +
|
||||
"HIS 사용자 코드가 올바른지, 사용 기간이 유효한지 확인해 주세요.";
|
||||
return false;
|
||||
}
|
||||
|
||||
UserSession.Initialize(user);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>조회 실패(접속 오류 등)를 null 로 흡수 — 기동 경로에서 예외로 죽지 않게</summary>
|
||||
private static HisUser? SafeResolve(string connectionString, string uidCod)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new UserContextStore(connectionString).Resolve(uidCod);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
namespace SheetMe.Designer.Services;
|
||||
|
||||
/// <summary>HIS 기동 인자 — 레거시 규약 <c>"UidCod,ComNum,ShtCod"</c> (콤마 구분 단일 argv)</summary>
|
||||
/// <param name="UidCod">HIS 사용자 코드 — 감사 컬럼의 출처</param>
|
||||
/// <param name="ComNum">내원번호 — 레거시에서도 선언·대입만 있고 사용처가 없는 죽은 인자. 형상 보존용</param>
|
||||
/// <param name="ShtCod">기동 시 자동으로 열 서식 코드</param>
|
||||
/// <param name="Raw">원문 — 단일 인스턴스 포워딩에 그대로 전달</param>
|
||||
public sealed record HisLaunch(string UidCod, int? ComNum, string ShtCod, string Raw);
|
||||
|
||||
/// <summary>
|
||||
/// 기동 인자 파싱. 레거시 frmSheetDesigner.vb:877 의 관용 파싱
|
||||
/// (<c>(Command() & "".PadRight(10, ",")).Split(",")</c>)을 이식했다 —
|
||||
/// 필드가 모자라도 인덱스가 보장되므로 인자 형상이 변해도 깨지지 않는다.
|
||||
/// </summary>
|
||||
public static class StartupArguments
|
||||
{
|
||||
#region Methods
|
||||
/// <summary>진단 플래그 기동인지 — 첫 인자가 "--" 로 시작</summary>
|
||||
public static bool IsDiagnostic(string[] args)
|
||||
=> args.Length > 0 && args[0].StartsWith("--", StringComparison.Ordinal);
|
||||
|
||||
/// <summary>HIS 기동 인자 — 진단 모드이거나 인자가 없으면 null</summary>
|
||||
public static HisLaunch? Parse(string[] args)
|
||||
=> IsDiagnostic(args) || args.Length == 0 ? null : ParseRaw(string.Join(" ", args));
|
||||
|
||||
/// <summary>
|
||||
/// 원문 파싱 — 단일 인스턴스 포워딩 수신부와 공용.
|
||||
/// ComNum 은 레거시가 CInt 로 변환하다 비숫자에서 예외를 냈지만, 죽은 값 때문에 기동이
|
||||
/// 실패하면 안 되므로 여기서는 TryParse 로 관용 처리한다.
|
||||
/// </summary>
|
||||
public static HisLaunch ParseRaw(string raw)
|
||||
{
|
||||
var parts = ((raw ?? string.Empty) + new string(',', 10)).Split(',');
|
||||
var comNum = int.TryParse(parts[1].Trim(), out var n) ? n : (int?)null;
|
||||
return new HisLaunch(parts[0].Trim(), comNum, parts[2].Trim(), raw ?? string.Empty);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using SheetMe.Data.Stores;
|
||||
|
||||
namespace SheetMe.Designer.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 프로세스 수명 사용자 세션 — 레거시 <c>clsCommonLib.GetInstance.UidMst</c> 싱글턴의 대응물.
|
||||
///
|
||||
/// 생성자 주입 대신 정적으로 둔 이유: MainViewModel 이 XAML DataContext 로 무인자 생성되고,
|
||||
/// 무엇보다 진단 모드(--db-*)는 ViewModel 그래프 밖이라 주입으로는 감사 컬럼 기록 지점에
|
||||
/// 도달할 수 없다. 대신 이 정적을 읽는 곳을 App 초기화 / FormDesignDataBusiness /
|
||||
/// MainViewModel 표시 세 군데로 제한하고, 저장소는 지금처럼 uid 를 파라미터로만 받는다.
|
||||
/// </summary>
|
||||
public static class UserSession
|
||||
{
|
||||
#region Member Fields
|
||||
/// <summary>진단 모드 감사 식별자 — DB 에서 즉시 구분되도록(개발자 윈도우 계정명 대신)</summary>
|
||||
private const string DiagnosticUid = "SMOKE";
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>현재 세션 사용자 — 초기화 전/미인증이면 <see cref="HisUser.Unknown"/></summary>
|
||||
public static HisUser Current { get; private set; } = HisUser.Unknown;
|
||||
|
||||
/// <summary>HIS 사용자로 확인되었는지 — DB 쓰기의 전제</summary>
|
||||
public static bool IsAuthenticated => Current.IsAuthenticated;
|
||||
|
||||
/// <summary>진단 모드 세션인지</summary>
|
||||
public static bool IsDiagnostic { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 감사 컬럼에 기록할 사용자 코드. 미인증 상태에서 접근하면 예외 —
|
||||
/// DB 쓰기는 그 전에 <see cref="IsAuthenticated"/> 로 차단되어야 한다.
|
||||
/// </summary>
|
||||
public static string AuditUid => IsAuthenticated
|
||||
? Current.UidCod
|
||||
: throw new InvalidOperationException("사용자 컨텍스트가 없어 DB 쓰기를 수행할 수 없습니다.");
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
/// <summary>세션 확정 — 기동 시 1회</summary>
|
||||
public static void Initialize(HisUser user)
|
||||
{
|
||||
Current = user;
|
||||
IsDiagnostic = false;
|
||||
}
|
||||
|
||||
/// <summary>진단 모드 식별자 부여 — --db-* 스모크 전용</summary>
|
||||
public static void InitializeDiagnostic()
|
||||
{
|
||||
Current = new HisUser(DiagnosticUid, "진단", string.Empty, string.Empty, string.Empty, string.Empty);
|
||||
IsDiagnostic = true;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user