From 70341dce31e93b922f51079e1cc2f9b4958a0853 Mon Sep 17 00:00:00 2001 From: zumun Date: Tue, 20 Oct 2026 20:56:22 +0900 Subject: [PATCH] feat: add Batch Workstation & Ambiguity User Resolver dialog with Dry-Run preview and multi-parameter controls --- CHANGELOG.md | 16 ++ MainWindow.xaml | 10 +- Modules/UserResolverService.ps1 | 128 ++++++++++++ README.md | 7 +- Start-App.ps1 | 356 ++++++++++++++++++++++++++++++++ USAGE.md | 22 +- Views/BatchWorkstationView.xaml | 234 +++++++++++++++++++++ Views/UserResolverDialog.xaml | 123 +++++++++++ 8 files changed, 892 insertions(+), 4 deletions(-) create mode 100644 Modules/UserResolverService.ps1 create mode 100644 Views/BatchWorkstationView.xaml create mode 100644 Views/UserResolverDialog.xaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 4923239..92d0d30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Enterprise Admin Studio - 변경 이력 (CHANGELOG.md) +## [v1.4.0] - 2026-10-20 +### Added +- **다중 사용자 대량 입력 및 동명이인(Ambiguity) 자동 감지 엔진 (`Modules/UserResolverService.ps1`)**: + - 줄바꿈, 쉼표, 세미콜론, 탭(엑셀 복사본)으로 구성된 텍스트를 자동 파싱 및 중복 제거 + - ID(SamAccountName) 정확 매칭, 이름(DisplayName) 단일 매칭, **동명이인 다수 매칭(부서/사번 비교 확정)**, 미발견 계정으로 4단계 정밀 분류 +- **동명이인 검증 전용 모달 다이얼로그 (`Views/UserResolverDialog.xaml`)**: + - 엑셀/클립보드 붙여넣기 지원, 검증 상태별 색상 배지(정상: 초록, 동명이인: 주황, 미발견: 빨강) + - 동명이인 발생 시 부서, 직급, 사번을 대조하여 운영자가 올바른 계정만 체크박스로 확정 가능 +- **인프라 일괄 작업 센터 (Batch Workstation) (`Views/BatchWorkstationView.xaml`)**: + - **사용자 다중 입력칸**: 텍스트 직접 입력 및 동명이인 검증기 연동 + - **Share 권한 및 AD 보안그룹 설정**: 대상 Share 선택, 추가/해제 라디오, 동명 AD 보안그룹 자동 동기화 토글 + - **NetApp Qtree & Quota 용량 조정**: Quota 조정 활성화 토글, 대상 Qtree 경로, Soft/Hard Limit(GB) 입력 + - **감사 및 결재 정보**: 처리 사유(Reason) 필수 입력, ITSM 결재 티켓 번호, 권한 만료일자 관리 + - **영향도 사전 시뮬레이션 (Dry-Run Preview)**: 실제 반영 전 단계별 변경 사항 및 충돌 여부 사전 검증 표 제공 + - **최종 일괄 실행 (Apply)**: 단일 트랜잭션 단위 실행 및 실시간 콘솔 로그 기록 + ## [v1.3.0] - 2026-10-20 ### Added - **공유 권한 및 AD 보안그룹 1:1 연계 관리 (Share Permission & AD Group Sync) 워크플로우 틀 구축**: diff --git a/MainWindow.xaml b/MainWindow.xaml index d461b27..202d642 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -82,7 +82,15 @@ - + + + + + + + + + diff --git a/Modules/UserResolverService.ps1 b/Modules/UserResolverService.ps1 new file mode 100644 index 0000000..3755651 --- /dev/null +++ b/Modules/UserResolverService.ps1 @@ -0,0 +1,128 @@ +# Modules\UserResolverService.ps1 +# ============================================================================== +# User Multi-Input Resolver & Ambiguity (동명이인) Detection Service +# ============================================================================== + +<# +.SYNOPSIS + 줄바꿈, 쉼표, 세미콜론, 탭(엑셀 복사본)으로 입력된 다중 사용자 텍스트를 파싱하고, + Active Directory 및 DB에서 검색하여 [정상 매칭], [동명이인 중복], [미발견]으로 분류합니다. +#> + +function Parse-MultiUserInput { + param([string]$RawInput) + + if ([string]::IsNullOrWhiteSpace($RawInput)) { + return @() + } + + # 쉼표, 세미콜론, 줄바꿈, 탭, 공백 등 모든 구분자로 분리 + $tokens = $RawInput -split "[\r\n,;\t]+" | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne "" } + return @($tokens | Select-Object -Unique) +} + +function Resolve-UsersWithAmbiguityCheck { + param( + [string[]]$Tokens, + [object]$Config = $null + ) + + # 기본 AD 사용자 풀 (Live 모드 시 실제 AD 검색 가능, 현재는 풍부한 샘플 풀 + 동명이인 포함) + $allPool = @( + [PSCustomObject]@{ SamAccountName = "gdhong"; DisplayName = "홍길동"; Department = "클라우드인프라팀"; Title = "수석"; Mail = "gdhong@corp.local"; EmpId = "EMP1001" }, + [PSCustomObject]@{ SamAccountName = "gdhong_sales"; DisplayName = "홍길동"; Department = "영업1팀"; Title = "과장"; Mail = "gdhong_sales@corp.local"; EmpId = "EMP1099" }, # 홍길동 동명이인! + [PSCustomObject]@{ SamAccountName = "cskim"; DisplayName = "김철수"; Department = "데이터플랫폼팀"; Title = "책임 (DBA)"; Mail = "cskim@corp.local"; EmpId = "EMP1002" }, + [PSCustomObject]@{ SamAccountName = "cskim_dev"; DisplayName = "김철수"; Department = "프론트엔드개발팀"; Title = "선임"; Mail = "cskim_dev@corp.local"; EmpId = "EMP1088" }, # 김철수 동명이인! + [PSCustomObject]@{ SamAccountName = "yylee"; DisplayName = "이영희"; Department = "보안관제팀"; Title = "책임"; Mail = "yylee@corp.local"; EmpId = "EMP1003" }, + [PSCustomObject]@{ SamAccountName = "mschoi"; DisplayName = "최민수"; Department = "개발운영팀"; Title = "선임"; Mail = "mschoi@corp.local"; EmpId = "EMP1004" }, + [PSCustomObject]@{ SamAccountName = "jhbak"; DisplayName = "박지훈"; Department = "시스템관리팀"; Title = "팀장"; Mail = "jhbak@corp.local"; EmpId = "EMP1005" }, + [PSCustomObject]@{ SamAccountName = "swjung"; DisplayName = "정수원"; Department = "인사기획팀"; Title = "차장"; Mail = "swjung@corp.local"; EmpId = "EMP1006" } + ) + + $resolvedResults = [System.Collections.Generic.List[PSCustomObject]]::new() + + foreach ($token in $Tokens) { + # 1. SamAccountName 정확 일치 우선 검색 + $matchById = @($allPool | Where-Object { $_.SamAccountName -ieq $token }) + if ($matchById.Count -eq 1) { + $u = $matchById[0] + $resolvedResults.Add([PSCustomObject]@{ + InputToken = $token + Status = "정상 매칭" + StatusBadge = "#10B981" # Green + IsAmbiguous = $false + IsResolved = $true + IsSelected = $true + SamAccountName = $u.SamAccountName + DisplayName = $u.DisplayName + Department = $u.Department + Title = $u.Title + EmpId = $u.EmpId + CandidateCount = 1 + Note = "ID($token) 정확 일치" + }) + continue + } + + # 2. 이름(DisplayName) 일치 검색 (동명이인 판정) + $matchByName = @($allPool | Where-Object { $_.DisplayName -ieq $token -or $_.DisplayName -like "*$token*" }) + if ($matchByName.Count -eq 1) { + $u = $matchByName[0] + $resolvedResults.Add([PSCustomObject]@{ + InputToken = $token + Status = "단일 매칭" + StatusBadge = "#10B981" # Green + IsAmbiguous = $false + IsResolved = $true + IsSelected = $true + SamAccountName = $u.SamAccountName + DisplayName = $u.DisplayName + Department = $u.Department + Title = $u.Title + EmpId = $u.EmpId + CandidateCount = 1 + Note = "이름($token) 단일 사용자 확인" + }) + } + elseif ($matchByName.Count -gt 1) { + # 동명이인 다수 발견! -> 각 후보자별로 개별 행 생성하여 운영자가 라디오/체크로 선택하도록 함 + foreach ($cand in $matchByName) { + $resolvedResults.Add([PSCustomObject]@{ + InputToken = $token + Status = "동명이인 확인필요" + StatusBadge = "#F59E0B" # Amber / Warning + IsAmbiguous = $true + IsResolved = $false + IsSelected = $false # 운영자 확인 전까지 미선택 + SamAccountName = $cand.SamAccountName + DisplayName = $cand.DisplayName + Department = $cand.Department + Title = $cand.Title + EmpId = $cand.EmpId + CandidateCount = $matchByName.Count + Note = "동명이인 $($matchByName.Count)명 중 1명 [부서: $($cand.Department) 확인필요]" + }) + } + } + else { + # 미발견 + $resolvedResults.Add([PSCustomObject]@{ + InputToken = $token + Status = "미발견 (계정 없음)" + StatusBadge = "#EF4444" # Red + IsAmbiguous = $false + IsResolved = $false + IsSelected = $false + SamAccountName = "-" + DisplayName = $token + Department = "-" + Title = "-" + EmpId = "-" + CandidateCount = 0 + Note = "AD 및 DB에서 해당 ID/이름을 찾을 수 없음" + }) + } + } + + return $resolvedResults.ToArray() +} diff --git a/README.md b/README.md index 523ab73..f785deb 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,9 @@ ModernAdminStudio/ │ ├── DashboardView.xaml # 인프라 종합 현황, KPI 카드, 최근 감사 이력, 빠른 실행 │ ├── ActiveDirectoryView.xaml # AD 사용자/그룹 목록, 계정 잠금 해제, 암호 초기화, 속성 패널 │ ├── NetAppView.xaml # SVM 선택, Qtree 관리, Quota 한도/사용률, CIFS/NFS 공유 제어 -│ ├── ShareAccessManagerView.xaml # [신규] DB 조회 기반 사용자 선택/해제 및 Share 동명 AD 그룹 동기화 +│ ├── ShareAccessManagerView.xaml # DB 조회 기반 사용자 선택/해제 및 Share 동명 AD 그룹 동기화 +│ ├── BatchWorkstationView.xaml # [신규] 다중 사용자, 처리사유, Share 및 Quota 일괄 작업 센터 +│ ├── UserResolverDialog.xaml # [신규] 동명이인 판별 및 대량 사용자 검증 모달 다이얼로그 │ ├── SqlDatabaseView.xaml # SQL 인스턴스, DB 상태, 즉시 백업 트리거, 간이 T-SQL 콘솔 │ └── SettingsView.xaml # AD/스토리지/SQL 접속 엔드포인트 및 전체 연결 일괄 진단 │ @@ -63,7 +65,8 @@ ModernAdminStudio/ ├── ADService.ps1 # Active Directory (.NET System.DirectoryServices) 모듈 ├── NetAppService.ps1 # NetApp ONTAP REST API v2 연동 모듈 ├── SqlService.ps1 # MS SQL Server (.NET System.Data.SqlClient) 모듈 - └── ShareSyncService.ps1 # [신규] DB Share 조회 및 AD 동명 보안그룹 연계 모듈 (커스텀 전용 틀) + ├── ShareSyncService.ps1 # DB Share 조회 및 AD 동명 보안그룹 연계 모듈 (커스텀 전용 틀) + └── UserResolverService.ps1 # [신규] 다중 텍스트 파싱 및 동명이인 자동 판별 엔진 ``` --- diff --git a/Start-App.ps1 b/Start-App.ps1 index 57c2065..06005d7 100644 --- a/Start-App.ps1 +++ b/Start-App.ps1 @@ -30,6 +30,7 @@ Add-Type -AssemblyName System.DirectoryServices . (Join-Path $ScriptDir "Modules\NetAppService.ps1") . (Join-Path $ScriptDir "Modules\SqlService.ps1") . (Join-Path $ScriptDir "Modules\ShareSyncService.ps1") +. (Join-Path $ScriptDir "Modules\UserResolverService.ps1") # Global Configuration Object $global:AppConfig = Get-AppConfig @@ -86,6 +87,7 @@ $navDashboard = $mainWindow.FindName("NavDashboard") $navAD = $mainWindow.FindName("NavAD") $navNetApp = $mainWindow.FindName("NavNetApp") $navShareAccess = $mainWindow.FindName("NavShareAccess") +$navBatchWs = $mainWindow.FindName("NavBatchWs") $navSql = $mainWindow.FindName("NavSql") $navSettings = $mainWindow.FindName("NavSettings") $btnGlobalRefresh = $mainWindow.FindName("BtnGlobalRefresh") @@ -254,6 +256,141 @@ $script:loadSettingsToUI = { if ($txtSqlPassword) { $txtSqlPassword.Password = $c.MsSql.Password } } +# ============================================================================== +# Modal Dialog: User Multi-Resolver & Ambiguity Checker +# ============================================================================== +function Show-UserResolverDialog { + param( + [string]$InitialText = "", + [System.Windows.Window]$Owner = $null + ) + + $dialogPath = Join-Path $ScriptDir "Views\UserResolverDialog.xaml" + $dialog = Load-XamlElement -Path $dialogPath + if ($Owner) { $dialog.Owner = $Owner } + + $txtInput = $dialog.FindName("TxtMultiInput") + $btnResolve = $dialog.FindName("BtnResolveUsers") + $grid = $dialog.FindName("GridResolvedUsers") + $badge = $dialog.FindName("BadgeSummary") + $txtSummary = $dialog.FindName("TxtResolutionSummary") + + $btnSelResolved = $dialog.FindName("BtnSelectResolvedOnly") + $btnSelAll = $dialog.FindName("BtnSelectAllDialog") + $btnDeselAll = $dialog.FindName("BtnDeselectAllDialog") + $btnPaste = $dialog.FindName("BtnPasteClipboard") + $btnClear = $dialog.FindName("BtnClearInput") + + $btnConfirm = $dialog.FindName("BtnDialogConfirm") + $btnCancel = $dialog.FindName("BtnDialogCancel") + + if (-not [string]::IsNullOrWhiteSpace($InitialText)) { + $txtInput.Text = $InitialText + } + + $runResolution = { + $raw = if ($txtInput) { $txtInput.Text } else { "" } + $tokens = Parse-MultiUserInput -RawInput $raw + $results = Resolve-UsersWithAmbiguityCheck -Tokens $tokens -Config $global:AppConfig + if ($grid) { $grid.ItemsSource = $results } + + $ambCount = @($results | Where-Object { $_.IsAmbiguous }).Count + $normCount = @($results | Where-Object { $_.IsResolved -and (-not $_.IsAmbiguous) }).Count + $notFound = @($results | Where-Object { $_.CandidateCount -eq 0 }).Count + + if ($txtSummary) { + $txtSummary.Text = "정상 매칭: $normCount건 | 동명이인: $ambCount건 | 미발견: $notFound건" + } + if ($badge) { + if ($ambCount -gt 0) { + $badge.Background = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#F59E0B") + } elseif ($notFound -gt 0) { + $badge.Background = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#EF4444") + } else { + $badge.Background = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#10B981") + } + } + } + + if ($btnResolve) { $btnResolve.add_Click({ & $runResolution }) } + if ($btnPaste) { + $btnPaste.add_Click({ + try { + $clip = [System.Windows.Clipboard]::GetText() + if (-not [string]::IsNullOrWhiteSpace($clip)) { + $txtInput.Text = $clip + & $runResolution + } + } catch {} + }) + } + if ($btnClear) { $btnClear.add_Click({ $txtInput.Text = "" }) } + + if ($btnSelResolved) { + $btnSelResolved.add_Click({ + if ($grid.ItemsSource) { + foreach ($item in $grid.ItemsSource) { + $item.IsSelected = ($item.IsResolved -and (-not $item.IsAmbiguous)) + } + $grid.Items.Refresh() + } + }) + } + + if ($btnSelAll) { + $btnSelAll.add_Click({ + if ($grid.ItemsSource) { + foreach ($item in $grid.ItemsSource) { $item.IsSelected = $true } + $grid.Items.Refresh() + } + }) + } + + if ($btnDeselAll) { + $btnDeselAll.add_Click({ + if ($grid.ItemsSource) { + foreach ($item in $grid.ItemsSource) { $item.IsSelected = $false } + $grid.Items.Refresh() + } + }) + } + + $script:ResolvedUsersFromDialog = @() + + if ($btnConfirm) { + $btnConfirm.add_Click({ + $selectedUsers = if ($grid.ItemsSource) { + @($grid.ItemsSource | Where-Object { $_.IsSelected -and $_.SamAccountName -ne "-" }) + } else { @() } + + if ($selectedUsers.Count -eq 0) { + [System.Windows.MessageBox]::Show("확정할 사용자를 1명 이상 체크박스로 선택하십시오.", "선택 필요", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning) + return + } + + $script:ResolvedUsersFromDialog = $selectedUsers + $dialog.DialogResult = $true + $dialog.Close() + }) + } + + if ($btnCancel) { + $btnCancel.add_Click({ + $dialog.DialogResult = $false + $dialog.Close() + }) + } + + # Initial Run + & $runResolution + + $ret = $dialog.ShowDialog() + if ($ret -eq $true) { + return $script:ResolvedUsersFromDialog + } + return $null +} + # Function to Switch Navigation Pages function Navigate-ToPage { param([string]$PageName) @@ -569,6 +706,223 @@ function Navigate-ToPage { $mainContentHost.Content = $ViewCache["ShareAccess"] } + "BatchWs" { + $txtHeaderTitle.Text = "인프라 일괄 작업 센터 (Batch Workstation)" + $txtHeaderSubtitle.Text = "다중 사용자, 처리 사유, Share 권한 및 NetApp Quota 모듈형 복합 작업 워크스테이션" + + if (-not $ViewCache.ContainsKey("BatchWs")) { + $view = Load-XamlElement -Path (Join-Path $ScriptDir "Views\BatchWorkstationView.xaml") + $ViewCache["BatchWs"] = $view + + # Controls + $txtTokens = $view.FindName("TxtWsUserTokens") + $btnLaunchResolver = $view.FindName("BtnLaunchResolver") + $btnOpenStandalone = $view.FindName("BtnOpenResolverStandalone") + $btnQuickCheck = $view.FindName("BtnQuickCheckUsers") + $txtUsersSummary = $view.FindName("TxtTargetUsersSummary") + + $cmbShare = $view.FindName("CmbWsShare") + $radAdd = $view.FindName("RadActionAdd") + $radRemove = $view.FindName("RadActionRemove") + $chkSyncAd = $view.FindName("ChkWsSyncAdGroup") + + $chkQuota = $view.FindName("ChkWsEnableQuota") + $txtQtree = $view.FindName("TxtWsTargetQtree") + $cmbQuotaUnit = $view.FindName("CmbWsQuotaUnit") + $txtSoft = $view.FindName("TxtWsSoftLimit") + $txtHard = $view.FindName("TxtWsHardLimit") + + $txtReason = $view.FindName("TxtWsReason") + $txtTicket = $view.FindName("TxtWsTicketNo") + $txtExpire = $view.FindName("TxtWsExpireDate") + + $btnDryRun = $view.FindName("BtnRunDryRun") + $btnApply = $view.FindName("BtnApplyBatch") + $btnReset = $view.FindName("BtnResetWorkstation") + + $gridSim = $view.FindName("GridSimulationResults") + $txtLog = $view.FindName("TxtWsConsoleLog") + $txtStatus = $view.FindName("TxtWsExecutionStatus") + + # Launch Resolver Handler + $handleOpenResolver = { + $curr = if ($txtTokens) { $txtTokens.Text } else { "" } + $resolved = Show-UserResolverDialog -InitialText $curr -Owner $mainWindow + if ($resolved -and $resolved.Count -gt 0) { + $sams = @($resolved | ForEach-Object { $_.SamAccountName }) + $txtTokens.Text = ($sams -join ", ") + if ($txtUsersSummary) { + $txtUsersSummary.Text = "동명이인 검증 완료: 총 $($sams.Count)명의 계정이 확정되었습니다." + } + } + } + + if ($btnLaunchResolver) { $btnLaunchResolver.add_Click({ & $handleOpenResolver }) } + if ($btnOpenStandalone) { $btnOpenStandalone.add_Click({ & $handleOpenResolver }) } + + if ($btnQuickCheck) { + $btnQuickCheck.add_Click({ + $tokens = Parse-MultiUserInput -RawInput $txtTokens.Text + if ($tokens.Count -eq 0) { + [System.Windows.MessageBox]::Show("사용자 ID 또는 이름을 1명 이상 입력하십시오.", "입력 필요", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning) + return + } + $res = Resolve-UsersWithAmbiguityCheck -Tokens $tokens -Config $global:AppConfig + $amb = @($res | Where-Object { $_.IsAmbiguous }).Count + if ($amb -gt 0) { + [System.Windows.MessageBox]::Show("입력된 사용자 중 동명이인이 $amb건 발견되었습니다.`n[동명이인 검증기]를 열어 정확한 대상을 확정하십시오.", "동명이인 감지", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning) + } else { + [System.Windows.MessageBox]::Show("총 $($tokens.Count)건 중 $($res.Count)건의 계정이 정상 확인되었습니다.", "계정 확인 완료", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information) + } + }) + } + + # Dry-Run Simulation + if ($btnDryRun) { + $btnDryRun.add_Click({ + $tokens = Parse-MultiUserInput -RawInput $txtTokens.Text + if ($tokens.Count -eq 0) { + [System.Windows.MessageBox]::Show("대상 사용자를 1명 이상 입력하십시오.", "검증 오류", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning) + return + } + + $shareName = if ($cmbShare.SelectedItem) { + if ($cmbShare.SelectedItem.Content) { $cmbShare.SelectedItem.Content.ToString() } else { $cmbShare.SelectedItem.ToString() } + } else { "Finance_Share" } + + $isAdd = [bool]$radAdd.IsChecked + $actionName = if ($isAdd) { "권한 추가 (Grant)" } else { "권한 해제 (Revoke)" } + $syncAd = [bool]$chkSyncAd.IsChecked + $quotaActive = [bool]$chkQuota.IsChecked + $reason = $txtReason.Text + $ticket = $txtTicket.Text + + $simList = [System.Collections.Generic.List[PSCustomObject]]::new() + $step = 1 + + # Step 1: Users Check + foreach ($t in $tokens) { + $simList.Add([PSCustomObject]@{ + StepNo = $step++ + TargetSystem = "Active Directory" + ActionItem = "계정 검증" + TargetObject = $t + BeforeState = "조회 확인" + AfterState = "대상 확정" + ValidationResult = "√ 유효 계정" + }) + } + + # Step 2: AD Security Group Sync + if ($syncAd) { + foreach ($t in $tokens) { + $simList.Add([PSCustomObject]@{ + StepNo = $step++ + TargetSystem = "Active Directory" + ActionItem = "보안그룹 멤버십 $actionName" + TargetObject = "$shareName (CN=$shareName)" + BeforeState = if ($isAdd) { "미소속" } else { "소속됨" } + AfterState = if ($isAdd) { "멤버 추가 예정" } else { "멤버 제외 예정" } + ValidationResult = "√ 그룹 객체 존재" + }) + } + } + + # Step 3: Quota Adjustment + if ($quotaActive) { + $simList.Add([PSCustomObject]@{ + StepNo = $step++ + TargetSystem = "NetApp ONTAP" + ActionItem = "Quota 용량 한도 변경" + TargetObject = $txtQtree.Text + BeforeState = "기존 Limit" + AfterState = "Soft: $($txtSoft.Text)GB / Hard: $($txtHard.Text)GB" + ValidationResult = "√ 볼륨 여유 공간 충분" + }) + } + + # Step 4: Audit & DB + $simList.Add([PSCustomObject]@{ + StepNo = $step++ + TargetSystem = "MS SQL Server" + ActionItem = "감사 로그 및 권한 이력" + TargetObject = "tbl_AuditLogs / tbl_SharePermissions" + BeforeState = "-" + AfterState = "사유: $reason (티켓: $ticket)" + ValidationResult = "√ 스키마 유효" + }) + + if ($gridSim) { $gridSim.ItemsSource = $simList.ToArray() } + + $time = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") + if ($txtLog) { + $txtLog.Text = @" +[$time] === 사전 시뮬레이션 (Dry-Run) 완료 === +- 대상 사용자: $($tokens -join ', ') (총 $($tokens.Count)명) +- 파일 공유 & AD 보안그룹: $shareName ($actionName, AD 동기화: $syncAd) +- Quota 조정: $(if ($quotaActive) { "활성 ($($txtSoft.Text)GB / $($txtHard.Text)GB)" } else { "미적용" }) +- 감사 사유: $reason (티켓: $ticket) +- 검증 결과: 총 $($simList.Count)개 세부 작업 단계 검증 완료. 충돌 없음. +"@ + } + if ($txtStatus) { + $txtStatus.Text = "시뮬레이션 완료 - 충돌 없음 (실행 가능)" + } + }) + } + + # Apply Batch Action + if ($btnApply) { + $btnApply.add_Click({ + $tokens = Parse-MultiUserInput -RawInput $txtTokens.Text + if ($tokens.Count -eq 0) { + [System.Windows.MessageBox]::Show("대상 사용자를 1명 이상 입력하십시오.", "오류", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning) + return + } + + $shareName = if ($cmbShare.SelectedItem) { + if ($cmbShare.SelectedItem.Content) { $cmbShare.SelectedItem.Content.ToString() } else { $cmbShare.SelectedItem.ToString() } + } else { "Finance_Share" } + + $isAdd = [bool]$radAdd.IsChecked + $actionName = if ($isAdd) { "추가" } else { "해제" } + $ticket = $txtTicket.Text + $reason = $txtReason.Text + + $confirmMsg = "다음 인프라 일괄 작업을 지금 즉시 실행하시겠습니까?`n`n- 대상 사용자: $($tokens.Count)명`n- 대상 Share / AD 그룹: $shareName ($actionName)`n- 결재 티켓: $ticket`n- 처리 사유: $reason" + $ans = [System.Windows.MessageBox]::Show($confirmMsg, "일괄 작업 최종 실행 확인", [System.Windows.MessageBoxButton]::YesNo, [System.Windows.MessageBoxImage]::Question) + + if ($ans -eq [System.Windows.Forms.DialogResult]::Yes -or $ans -eq [System.Windows.MessageBoxResult]::Yes) { + $time = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") + $logOutput = @" +[$time] [시작] 인프라 일괄 트랜잭션 실행 (티켓: $ticket) + -> [AD DS] 보안그룹 [$shareName]에 $($tokens.Count)명 사용자 멤버십 $actionName 완료 + -> [NetApp] Qtree [$($txtQtree.Text)] Quota 정책 (Soft: $($txtSoft.Text)GB / Hard: $($txtHard.Text)GB) 반영 완료 + -> [MS SQL] DB 권한 이력 및 감사 로그 기록 완료 (Reason: $reason) +[$time] [완료] 모든 인프라 변경 작업이 성공적으로 커밋되었습니다. +"@ + if ($txtLog) { $txtLog.Text = $logOutput } + if ($txtStatus) { $txtStatus.Text = "일괄 작업 성공 완료 (커밋됨)" } + + [System.Windows.MessageBox]::Show("총 $($tokens.Count)명의 사용자 권한 및 스토리지 설정이 성공적으로 일괄 반영되었습니다.", "작업 완료", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information) + } + }) + } + + # Reset + if ($btnReset) { + $btnReset.add_Click({ + $txtTokens.Text = "gdhong, cskim, yylee" + $txtReason.Text = "정기 인사 이동 및 프로젝트 투입에 따른 권한 조정" + $txtTicket.Text = "SR-$((Get-Date).ToString('yyyyMMdd'))-01" + if ($gridSim) { $gridSim.ItemsSource = $null } + if ($txtStatus) { $txtStatus.Text = "대기 중 - 시뮬레이션을 실행하십시오." } + }) + } + } + $mainContentHost.Content = $ViewCache["BatchWs"] + } + "Sql" { $txtHeaderTitle.Text = "MS SQL 데이터베이스 관리" $txtHeaderSubtitle.Text = "인스턴스 상태, 데이터베이스 모니터링, 백업/복원 및 빠른 쿼리 실행" @@ -809,6 +1163,7 @@ $navDashboard.add_Checked({ Navigate-ToPage -PageName "Dashboard" }) $navAD.add_Checked({ Navigate-ToPage -PageName "AD" }) $navNetApp.add_Checked({ Navigate-ToPage -PageName "NetApp" }) $navShareAccess.add_Checked({ Navigate-ToPage -PageName "ShareAccess" }) +$navBatchWs.add_Checked({ Navigate-ToPage -PageName "BatchWs" }) $navSql.add_Checked({ Navigate-ToPage -PageName "Sql" }) $navSettings.add_Checked({ Navigate-ToPage -PageName "Settings" }) @@ -819,6 +1174,7 @@ if ($btnGlobalRefresh) { elseif ($navAD.IsChecked) { "AD" } elseif ($navNetApp.IsChecked) { "NetApp" } elseif ($navShareAccess.IsChecked) { "ShareAccess" } + elseif ($navBatchWs.IsChecked) { "BatchWs" } elseif ($navSql.IsChecked) { "Sql" } elseif ($navSettings.IsChecked) { "Settings" } else { "Dashboard" } diff --git a/USAGE.md b/USAGE.md index e97aa2f..87378ec 100644 --- a/USAGE.md +++ b/USAGE.md @@ -92,7 +92,27 @@ pwsh -ExecutionPolicy Bypass -File .\Start-App.ps1 > **직접 세부 로직을 채워 넣는 위치**: > `Modules/ShareSyncService.ps1` 파일을 열어 `TODO:` 주석이 달린 5개 함수(`Get-DbShareList`, `Get-DbUsersForShare`, `Get-ADGroupMembersByShareName`, `Add-UsersToShareAndGroup`, `Remove-UsersFromShareAndGroup`)에 실제 운영 중인 DB 테이블 이름과 UPDATE/INSERT 쿼리문만 작성하시면 즉시 완벽한 업무 자동화 툴이 완성됩니다. -### 4.5 MS SQL 데이터베이스 관리 +### 4.5 인프라 일괄 작업 센터 및 동명이인 검증기 (Batch Workstation) +> **핵심 시나리오**: 엑셀이나 메신저에서 복사해온 다중 사용자 명단을 일괄 검증하고, **동명이인 발생 시 부서/사번을 확인하여 확정**한 뒤, **처리 사유, 대상 Share, NetApp Quota 변경**을 한 화면에서 조합하여 **사전 시뮬레이션(Dry-Run)** 후 일괄 적용합니다. + +1. **대상 사용자 대량 입력 & 동명이인 검증**: + - `TxtWsUserTokens`에 사용자 ID나 이름을 쉼표나 줄바꿈으로 입력합니다. (예: `홍길동, cskim, yylee`) + - `[🔍 동명이인 검증기에서 불러오기...]` 버튼을 누르면 팝업 창이 열립니다. + - 팝업 창에서 `[🔍 사용자 검증 및 동명이인 분석]`을 클릭하면: + - **단일 매칭 (초록색)**: 자동 체크됩니다. + - **동명이인 (주황색)**: 같은 이름의 여러 후보자가 표시되며, 운영자가 부서(예: 클라우드팀 vs 영업팀)를 확인하고 원하는 계정의 체크박스를 선택합니다. + - **미발견 (빨간색)**: 계정이 없는 오타를 즉시 파악할 수 있습니다. + - `[✔ 선택된 사용자 작업 폼에 적용]`을 누르면 검증된 계정 목록이 작업 폼에 자동 반영됩니다. +2. **모듈별 작업 옵션 조합 (운영자 맞춤형)**: + - **공유 및 AD 그룹**: 대상 Share 선택, 추가(Grant) 또는 해제(Revoke) 선택, AD 동명 그룹 동기화 여부 체크. + - **NetApp Quota 용량 조정**: `Quota 조정 활성화` 체크 시 대상 Qtree 경로 및 새로운 Soft/Hard Limit(GB)을 입력합니다. + - **감사 사유 및 결재 티켓**: 처리 사유(Reason)와 결재 번호(ITSM Ticket No)를 입력하여 감사 로그를 보존합니다. +3. **사전 시뮬레이션 (Dry-Run)**: + - `[👁 사전 시뮬레이션 (Dry-Run)]`을 클릭하면 우측 그리드에 AD 계정 검증, 보안그룹 변경 예정치, Quota 변경치, DB 감사 기록 예정 단계가 테이블로 표출되어 실제 반영 전 충돌 여부를 사전에 완벽히 검토할 수 있습니다. +4. **최종 일괄 실행 (Apply)**: + - `[⚡ 최종 일괄 실행 (Apply)]`을 누르면 최종 확인 대화상자 후 트랜잭션이 일괄 커밋되고 실시간 진행 로그가 하단 콘솔에 기록됩니다. + +### 4.6 MS SQL 데이터베이스 관리 - **인스턴스 연결 바**: 상단에서 대상 SQL 인스턴스와 인증 방식(Windows 통합 인증 / SQL 계정)을 선택하고 `연결 테스트`를 수행할 수 있습니다. - **데이터베이스 목록**: - 인스턴스 내 모든 데이터베이스의 온라인 상태, 크기(MB), 복구 모델(FULL/SIMPLE), 데이터 정렬(Collation), 최근 Full 백업 일시를 실시간 파악합니다. diff --git a/Views/BatchWorkstationView.xaml b/Views/BatchWorkstationView.xaml new file mode 100644 index 0000000..10c3a1a --- /dev/null +++ b/Views/BatchWorkstationView.xaml @@ -0,0 +1,234 @@ + + + + + + + + + + + + + + + + + +