feat: add Batch Workstation & Ambiguity User Resolver dialog with Dry-Run preview and multi-parameter controls
This commit is contained in:
@@ -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) 워크플로우 틀 구축**:
|
||||
|
||||
+9
-1
@@ -82,7 +82,15 @@
|
||||
</StackPanel>
|
||||
</RadioButton>
|
||||
|
||||
<!-- 5. MS SQL Database -->
|
||||
<!-- 5. Batch Workstation (Multi-user, Reason, Share & Quota) -->
|
||||
<RadioButton x:Name="NavBatchWs" Style="{StaticResource NavButtonStyle}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="" FontFamily="{StaticResource IconFont}" FontSize="16" Width="28" VerticalAlignment="Center" />
|
||||
<TextBlock Text="일괄 작업 센터 (Batch)" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</RadioButton>
|
||||
|
||||
<!-- 6. MS SQL Database -->
|
||||
<RadioButton x:Name="NavSql" Style="{StaticResource NavButtonStyle}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="" FontFamily="{StaticResource IconFont}" FontSize="16" Width="28" VerticalAlignment="Center" />
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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 # [신규] 다중 텍스트 파싱 및 동명이인 자동 판별 엔진
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
+356
@@ -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" }
|
||||
|
||||
@@ -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 백업 일시를 실시간 파악합니다.
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
<UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Background="{StaticResource BgDarkBrush}">
|
||||
<Grid Margin="24">
|
||||
<Grid.RowDefinitions>
|
||||
<!-- 0. Header -->
|
||||
<RowDefinition Height="Auto" />
|
||||
<!-- 1. Main 2-Column Split Layout -->
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 0. Header -->
|
||||
<DockPanel Grid.Row="0" Margin="0,0,0,18">
|
||||
<StackPanel>
|
||||
<TextBlock Text="인프라 일괄 작업 센터 (Batch Workstation)" FontSize="24" FontWeight="Bold" Foreground="#FFFFFF" />
|
||||
<TextBlock Text="다중 사용자 계정, Share 권한, AD 보안그룹 및 NetApp Quota 변경을 단일 트랜잭션으로 구성하여 시뮬레이션 후 일괄 실행합니다."
|
||||
FontSize="13" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,4,0,0" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
|
||||
<Button x:Name="BtnResetWorkstation" Content="작업 폼 초기화" Style="{StaticResource SecondaryButtonStyle}" Margin="0,0,8,0" Padding="12,6" />
|
||||
<Button x:Name="BtnOpenResolverStandalone" Content="🔍 동명이인 / 계정 검증기" Style="{StaticResource PrimaryButtonStyle}" Padding="12,6" />
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
|
||||
<!-- 1. Main Split Layout -->
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<!-- Left: Modular Parameter Inputs (운영자가 자유롭게 조합하는 폼) -->
|
||||
<ColumnDefinition Width="5*" />
|
||||
<!-- Right: Simulation (Dry-Run) Preview & Execution Grid -->
|
||||
<ColumnDefinition Width="6*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- LEFT: Action Config Form (Scrollable) -->
|
||||
<ScrollViewer Grid.Column="0" VerticalScrollBarVisibility="Auto" Margin="0,0,12,0">
|
||||
<StackPanel>
|
||||
<!-- Card 1: Target Users Specification -->
|
||||
<Border Style="{StaticResource ModernCardStyle}" Margin="0,0,0,14">
|
||||
<StackPanel>
|
||||
<DockPanel Margin="0,0,0,10">
|
||||
<TextBlock Text="1. 대상 사용자 지정 (Multi-User Input)" FontSize="14.5" FontWeight="Bold" Foreground="#FFFFFF" />
|
||||
<Button x:Name="BtnLaunchResolver" Content="🔍 동명이인 검증기에서 불러오기..." Style="{StaticResource PrimaryButtonStyle}" Padding="10,4" FontSize="11.5" HorizontalAlignment="Right" />
|
||||
</DockPanel>
|
||||
|
||||
<TextBlock Text="사용자 ID 또는 이름 (줄바꿈 / 쉼표로 다중 입력 가능):" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,4" />
|
||||
<TextBox x:Name="TxtWsUserTokens" Height="70" AcceptsReturn="True" TextWrapping="Wrap"
|
||||
Style="{StaticResource ModernTextBoxStyle}" Margin="0,0,0,8"
|
||||
Text="gdhong, cskim, yylee" />
|
||||
|
||||
<DockPanel>
|
||||
<TextBlock x:Name="TxtTargetUsersSummary" Text="현재 3명의 사용자가 지정되었습니다." FontSize="11.5" Foreground="#60A5FA" VerticalAlignment="Center" />
|
||||
<Button x:Name="BtnQuickCheckUsers" Content="빠른 계정 확인" Style="{StaticResource SecondaryButtonStyle}" Padding="8,2" FontSize="11" HorizontalAlignment="Right" />
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Card 2: Share & AD Security Group Association -->
|
||||
<Border Style="{StaticResource ModernCardStyle}" Margin="0,0,0,14">
|
||||
<StackPanel>
|
||||
<TextBlock Text="2. 공유 권한 및 AD 보안그룹 설정" FontSize="14.5" FontWeight="Bold" Foreground="#FFFFFF" Margin="0,0,0,10" />
|
||||
|
||||
<Grid Margin="0,0,0,8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0" Margin="0,0,8,0">
|
||||
<TextBlock Text="대상 Share (공유) 선택 / 입력:" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,4" />
|
||||
<ComboBox x:Name="CmbWsShare" Height="34">
|
||||
<ComboBoxItem Content="Finance_Share" IsSelected="True" />
|
||||
<ComboBoxItem Content="HR_Share" />
|
||||
<ComboBoxItem Content="DevOps_Share" />
|
||||
<ComboBoxItem Content="Public_Projects" />
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="1" Margin="8,0,0,0">
|
||||
<TextBlock Text="작업 유형 (Action):" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,4" />
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="0,6,0,0">
|
||||
<RadioButton x:Name="RadActionAdd" Content="권한 추가 (Grant)" IsChecked="True" Foreground="#FFFFFF" Margin="0,0,16,0" />
|
||||
<RadioButton x:Name="RadActionRemove" Content="권한 해제 (Revoke)" Foreground="#FFFFFF" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<CheckBox x:Name="ChkWsSyncAdGroup" Content="Share 이름과 동일한 AD 보안그룹 멤버십 자동 동기화" IsChecked="True"
|
||||
Foreground="#93C5FD" FontWeight="SemiBold" Margin="0,4,0,0" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Card 3: NetApp Qtree & Quota Limit (Optional Module) -->
|
||||
<Border Style="{StaticResource ModernCardStyle}" Margin="0,0,0,14">
|
||||
<StackPanel>
|
||||
<DockPanel Margin="0,0,0,10">
|
||||
<TextBlock Text="3. NetApp Qtree 및 Quota 용량 조정 (선택 사항)" FontSize="14.5" FontWeight="Bold" Foreground="#FFFFFF" />
|
||||
<CheckBox x:Name="ChkWsEnableQuota" Content="Quota 조정 활성화" IsChecked="True"
|
||||
Foreground="#10B981" FontWeight="Bold" HorizontalAlignment="Right" />
|
||||
</DockPanel>
|
||||
|
||||
<Grid Margin="0,0,0,8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0" Margin="0,0,8,0">
|
||||
<TextBlock Text="대상 볼륨 / Qtree:" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,4" />
|
||||
<TextBox x:Name="TxtWsTargetQtree" Text="/vol/vol_cifs_share01/Finance_Share" Style="{StaticResource ModernTextBoxStyle}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="1" Margin="8,0,0,0">
|
||||
<TextBlock Text="Quota 적용 단위:" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,4" />
|
||||
<ComboBox x:Name="CmbWsQuotaUnit" SelectedIndex="0" Height="34">
|
||||
<ComboBoxItem Content="사용자별 (User Quota)" />
|
||||
<ComboBoxItem Content="트리 전체 (Tree Quota)" />
|
||||
<ComboBoxItem Content="그룹별 (Group Quota)" />
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0" Margin="0,0,8,0">
|
||||
<TextBlock Text="새 Soft Limit (GB):" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,4" />
|
||||
<TextBox x:Name="TxtWsSoftLimit" Text="450" Style="{StaticResource ModernTextBoxStyle}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="1" Margin="8,0,0,0">
|
||||
<TextBlock Text="새 Hard Limit (GB):" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,4" />
|
||||
<TextBox x:Name="TxtWsHardLimit" Text="500" Style="{StaticResource ModernTextBoxStyle}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Card 4: Audit Reason & Ticket Information -->
|
||||
<Border Style="{StaticResource ModernCardStyle}" Margin="0,0,0,14">
|
||||
<StackPanel>
|
||||
<TextBlock Text="4. 작업 사유 및 감사/승인 정보 (Audit Information)" FontSize="14.5" FontWeight="Bold" Foreground="#FFFFFF" Margin="0,0,0,10" />
|
||||
|
||||
<TextBlock Text="처리 사유 (필수 입력):" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,4" />
|
||||
<TextBox x:Name="TxtWsReason" Text="2026 하반기 신규 프로젝트 투입에 따른 권한 및 Quota 일괄 부여"
|
||||
Style="{StaticResource ModernTextBoxStyle}" Margin="0,0,0,10" />
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0" Margin="0,0,8,0">
|
||||
<TextBlock Text="ITSM / 결재 티켓 번호:" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,4" />
|
||||
<TextBox x:Name="TxtWsTicketNo" Text="SR-20261020-088" Style="{StaticResource ModernTextBoxStyle}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="1" Margin="8,0,0,0">
|
||||
<TextBlock Text="권한 만료일 (임시 권한 시):" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,4" />
|
||||
<TextBox x:Name="TxtWsExpireDate" Text="9999-12-31 (무기한)" Style="{StaticResource ModernTextBoxStyle}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- RIGHT: Simulation (Dry-Run) & Execution Console -->
|
||||
<Border Grid.Column="1" Style="{StaticResource ModernCardStyle}" Margin="12,0,0,0" Padding="14">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<!-- Simulation Action Bar -->
|
||||
<RowDefinition Height="Auto" />
|
||||
<!-- Simulation Grid Preview -->
|
||||
<RowDefinition Height="*" />
|
||||
<!-- Execution Console Log -->
|
||||
<RowDefinition Height="160" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 1. Simulation Controls -->
|
||||
<DockPanel Grid.Row="0" Margin="0,0,0,12">
|
||||
<StackPanel>
|
||||
<TextBlock Text="실행 영향도 분석 및 시뮬레이션 (Dry-Run Preview)" FontSize="14.5" FontWeight="Bold" Foreground="#FFFFFF" />
|
||||
<TextBlock Text="실제 인프라에 반영하기 전 변경 예정 항목을 안전하게 사전 검증합니다." FontSize="11.5" Foreground="{StaticResource TextSecondaryBrush}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button x:Name="BtnRunDryRun" Content="👁 사전 시뮬레이션 (Dry-Run)" Style="{StaticResource SecondaryButtonStyle}" Margin="0,0,8,0" Padding="12,6" />
|
||||
<Button x:Name="BtnApplyBatch" Content="⚡ 최종 일괄 실행 (Apply)" Style="{StaticResource PrimaryButtonStyle}" Padding="16,6" FontWeight="Bold" />
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
|
||||
<!-- 2. Simulation Items DataGrid -->
|
||||
<DataGrid x:Name="GridSimulationResults" Grid.Row="1" Style="{StaticResource ModernDataGridStyle}" Margin="0,0,0,12">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="순번" Binding="{Binding StepNo}" Width="50" />
|
||||
<DataGridTextColumn Header="대상 시스템" Binding="{Binding TargetSystem}" Width="120" />
|
||||
<DataGridTextColumn Header="작업 항목" Binding="{Binding ActionItem}" Width="130" />
|
||||
<DataGridTextColumn Header="대상 객체 / 사용자" Binding="{Binding TargetObject}" Width="150" />
|
||||
<DataGridTextColumn Header="변경 전 상태" Binding="{Binding BeforeState}" Width="110" />
|
||||
<DataGridTextColumn Header="변경 예정 상태" Binding="{Binding AfterState}" Width="120" />
|
||||
<DataGridTextColumn Header="사전 검증" Binding="{Binding ValidationResult}" Width="*" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<!-- 3. Bottom Execution Log Console -->
|
||||
<Border Grid.Row="2" Background="#090D16" CornerRadius="6" Padding="10">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<DockPanel Grid.Row="0" Margin="0,0,0,4">
|
||||
<TextBlock Text="일괄 트랜잭션 실행 로그 (Execution Output):" FontSize="11.5" FontWeight="SemiBold" Foreground="#94A3B8" />
|
||||
<TextBlock x:Name="TxtWsExecutionStatus" Text="대기 중 - 시뮬레이션을 실행하십시오." FontSize="11.5" Foreground="#60A5FA" HorizontalAlignment="Right" />
|
||||
</DockPanel>
|
||||
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
|
||||
<TextBox x:Name="TxtWsConsoleLog" Background="Transparent" Foreground="#A78BFA" BorderThickness="0"
|
||||
IsReadOnly="True" FontFamily="Consolas" FontSize="11.5" TextWrapping="Wrap"
|
||||
Text="[준비 완료] 좌측 폼에 필요한 파라미터를 입력하고 [사전 시뮬레이션(Dry-Run)]을 실행하십시오." />
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,123 @@
|
||||
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="다중 사용자 계정 검증 및 동명이인 식별기 (User Multi-Resolver)"
|
||||
Height="680" Width="980"
|
||||
MinHeight="540" MinWidth="800"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Background="#0F172A"
|
||||
FontFamily="{StaticResource ModernFont}">
|
||||
|
||||
<Grid Margin="20">
|
||||
<Grid.RowDefinitions>
|
||||
<!-- 0. Header -->
|
||||
<RowDefinition Height="Auto" />
|
||||
<!-- 1. Input Box & Action Area -->
|
||||
<RowDefinition Height="Auto" />
|
||||
<!-- 2. Ambiguity & Resolution Results Grid -->
|
||||
<RowDefinition Height="*" />
|
||||
<!-- 3. Bottom Confirm Bar -->
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 0. Header -->
|
||||
<DockPanel Grid.Row="0" Margin="0,0,0,16">
|
||||
<StackPanel>
|
||||
<TextBlock Text="다중 사용자 입력 및 동명이인 검증기" FontSize="20" FontWeight="Bold" Foreground="#FFFFFF" />
|
||||
<TextBlock Text="사용자 ID(sAMAccountName) 또는 이름을 줄바꿈/쉼표로 복사-붙여넣기하여 동명이인 여부를 확인하고 최종 대상을 확정합니다."
|
||||
FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,3,0,0" />
|
||||
</StackPanel>
|
||||
<Border Background="#1E293B" CornerRadius="6" Padding="10,4" HorizontalAlignment="Right" VerticalAlignment="Center">
|
||||
<TextBlock Text="동명이인 자동 감지 엔진 활성" FontSize="11" FontWeight="SemiBold" Foreground="#60A5FA" />
|
||||
</Border>
|
||||
</DockPanel>
|
||||
|
||||
<!-- 1. Input Box Area -->
|
||||
<Border Grid.Row="1" Style="{StaticResource ModernCardStyle}" Margin="0,0,0,14" Padding="14">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<DockPanel Grid.Row="0" Margin="0,0,0,8">
|
||||
<TextBlock Text="사용자 ID 또는 이름 대량 입력 (줄바꿈 / 쉼표 / 탭 구분):" FontSize="12.5" FontWeight="SemiBold" Foreground="{StaticResource TextPrimaryBrush}" />
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button x:Name="BtnPasteClipboard" Content="📋 클립보드 붙여넣기" Style="{StaticResource SecondaryButtonStyle}" Padding="8,2" FontSize="11" Margin="0,0,6,0" />
|
||||
<Button x:Name="BtnClearInput" Content="입력 지우기" Style="{StaticResource SecondaryButtonStyle}" Padding="8,2" FontSize="11" />
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="150" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBox x:Name="TxtMultiInput" Height="75" AcceptsReturn="True" TextWrapping="Wrap" VerticalScrollBarVisibility="Auto"
|
||||
Style="{StaticResource ModernTextBoxStyle}" Margin="0,0,10,0"
|
||||
Text="gdhong
홍길동
김철수
yylee
unknown_user" />
|
||||
|
||||
<Button x:Name="BtnResolveUsers" Grid.Column="1" Content="🔍 사용자 검증
및 동명이인 분석" Style="{StaticResource PrimaryButtonStyle}"
|
||||
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" FontSize="13" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 2. Results Grid -->
|
||||
<Border Grid.Row="2" Style="{StaticResource ModernCardStyle}" Margin="0,0,0,14" Padding="12">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Filter / Quick Action Bar -->
|
||||
<DockPanel Grid.Row="0" Margin="0,0,0,10">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="검증 및 식별 결과:" FontSize="13.5" FontWeight="Bold" Foreground="#FFFFFF" Margin="0,0,12,0" VerticalAlignment="Center" />
|
||||
<Border x:Name="BadgeSummary" Background="#1E3A8A" CornerRadius="4" Padding="8,2">
|
||||
<TextBlock x:Name="TxtResolutionSummary" Text="검증 대기 중" FontSize="11.5" Foreground="#FFFFFF" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button x:Name="BtnSelectResolvedOnly" Content="정상 매칭건만 선택" Style="{StaticResource SecondaryButtonStyle}" Padding="8,3" FontSize="11" Margin="0,0,6,0" />
|
||||
<Button x:Name="BtnSelectAllDialog" Content="전체 선택" Style="{StaticResource SecondaryButtonStyle}" Padding="8,3" FontSize="11" Margin="0,0,6,0" />
|
||||
<Button x:Name="BtnDeselectAllDialog" Content="선택 해제" Style="{StaticResource SecondaryButtonStyle}" Padding="8,3" FontSize="11" />
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
|
||||
<!-- Resolver DataGrid -->
|
||||
<DataGrid x:Name="GridResolvedUsers" Grid.Row="1" Style="{StaticResource ModernDataGridStyle}">
|
||||
<DataGrid.Columns>
|
||||
<DataGridCheckBoxColumn Header="확정" Binding="{Binding IsSelected, UpdateSourceTrigger=PropertyChanged}" Width="50" />
|
||||
<DataGridTextColumn Header="검증 상태" Binding="{Binding Status}" Width="130" />
|
||||
<DataGridTextColumn Header="입력 원문" Binding="{Binding InputToken}" Width="100" />
|
||||
<DataGridTextColumn Header="확정 ID (SamAccount)" Binding="{Binding SamAccountName}" Width="140" />
|
||||
<DataGridTextColumn Header="성명" Binding="{Binding DisplayName}" Width="90" />
|
||||
<DataGridTextColumn Header="부서 (동명이인 판별)" Binding="{Binding Department}" Width="140" />
|
||||
<DataGridTextColumn Header="직급" Binding="{Binding Title}" Width="70" />
|
||||
<DataGridTextColumn Header="사번" Binding="{Binding EmpId}" Width="85" />
|
||||
<DataGridTextColumn Header="식별 메모 / 안내" Binding="{Binding Note}" Width="*" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<TextBlock Grid.Row="2" Text="* 동명이인 항목은 부서 및 사번을 확인하신 후 올바른 대상의 [확정] 체크박스를 1개만 체크하십시오."
|
||||
FontSize="11" Foreground="#F59E0B" Margin="2,6,0,0" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 3. Bottom Confirm Bar -->
|
||||
<Border Grid.Row="3" Margin="0">
|
||||
<DockPanel>
|
||||
<TextBlock x:Name="TxtSelectedCountMsg" Text="현재 0명의 사용자가 확정 선택되었습니다."
|
||||
FontSize="12.5" Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center" />
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button x:Name="BtnDialogCancel" Content="취소" Style="{StaticResource SecondaryButtonStyle}" Padding="16,8" Margin="0,0,8,0" />
|
||||
<Button x:Name="BtnDialogConfirm" Content="✔ 선택된 사용자 작업 폼에 적용" Style="{StaticResource PrimaryButtonStyle}" Padding="18,8" />
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
Reference in New Issue
Block a user