# ================================================================================ # 파일: Scripts/UI-Tab-DeleteUser.ps1 # 역할: '계정 삭제' 탭 UI 및 기능 구현 # # 작성자: 양범진 # 버전: 1.13 # 생성일자: 2025-06-05 # 최종 수정일자: 2025-06-12 # # 설명: 안정화된 동기 실행 방식에 맞춰 코드 수정 및 가독성 개선, PSScriptAnalyzer 경고 수정 # ================================================================================ #region '계정 삭제' 탭 UI 초기화 함수 function Initialize-DeleteUserTab { Param( [System.Windows.Forms.TabPage]$parentTab ) # ========================================================== # 탭 전체 레이아웃 설정 # ========================================================== $delTabMainLayout = New-Object System.Windows.Forms.TableLayoutPanel $delTabMainLayout.Dock = "Fill" $delTabMainLayout.Padding = [System.Windows.Forms.Padding](10) $delTabMainLayout.ColumnCount = 1 $delTabMainLayout.RowCount = 3 $delTabMainLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 80))) | Out-Null $delTabMainLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 250))) | Out-Null $delTabMainLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Percent, 100))) | Out-Null $parentTab.Controls.Add($delTabMainLayout) | Out-Null # ========================================================== # 1. 작업 모드 선택 그룹박스 # ========================================================== $groupMode = New-Object System.Windows.Forms.GroupBox $groupMode.Text = "작업 모드 선택" $groupMode.Dock = "Fill" $delTabMainLayout.Controls.Add($groupMode, 0, 0) | Out-Null # '온프레미스 AD 삭제 및 Azure AD 동기화' 라디오 버튼 $script:del_radioSync = New-Object System.Windows.Forms.RadioButton $script:del_radioSync.Text = "온프레미스 AD 삭제 및 Azure AD 동기화" $script:del_radioSync.Location = New-Object System.Drawing.Point(25, 30) $script:del_radioSync.AutoSize = $true $script:del_radioSync.Checked = $true # '온프레미스 AD에서만 삭제' 라디오 버튼 $script:del_radioOnPremOnly = New-Object System.Windows.Forms.RadioButton $script:del_radioOnPremOnly.Text = "온프레미스 AD에서만 삭제 (동기화 안함)" $script:del_radioOnPremOnly.Location = New-Object System.Drawing.Point(450, 30) $script:del_radioOnPremOnly.AutoSize = $true $groupMode.Controls.AddRange(@($script:del_radioSync, $script:del_radioOnPremOnly)) | Out-Null # ========================================================== # 2. 사용자 검색 및 선택 그룹박스 # ========================================================== $groupSearch = New-Object System.Windows.Forms.GroupBox $groupSearch.Text = "1. 삭제할 사용자 검색 및 선택" $groupSearch.Dock = "Fill" $delTabMainLayout.Controls.Add($groupSearch, 0, 1) | Out-Null # 검색 그룹박스 내부 레이아웃 $searchLayout = New-Object System.Windows.Forms.TableLayoutPanel $searchLayout.Dock = "Fill" $searchLayout.Padding = [System.Windows.Forms.Padding](10) $searchLayout.ColumnCount = 2 $searchLayout.RowCount = 4 $searchLayout.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Percent, 100))) | Out-Null $searchLayout.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Absolute, 150))) | Out-Null $searchLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 35))) | Out-Null $searchLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Percent, 100))) | Out-Null $searchLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 35))) | Out-Null $searchLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 45))) | Out-Null $groupSearch.Controls.Add($searchLayout) | Out-Null # 사용자 검색 텍스트박스 및 검색 버튼 $script:del_textBoxSearchUser = New-Object System.Windows.Forms.TextBox $script:del_textBoxSearchUser.Dock = "Fill" $buttonSearchUserOnPrem = New-Object System.Windows.Forms.Button $buttonSearchUserOnPrem.Text = "온프레미스 검색(&S)" $buttonSearchUserOnPrem.Dock = "Fill" # 검색된 사용자 목록을 보여주는 리스트박스 (다중 선택 활성화) $script:del_listBoxFoundUsers = New-Object System.Windows.Forms.ListBox $script:del_listBoxFoundUsers.Dock = "Fill" $script:del_listBoxFoundUsers.SelectionMode = "MultiExtended" # 선택된 사용자의 상세 정보를 표시하는 레이아웃 $userInfoLayout = New-Object System.Windows.Forms.TableLayoutPanel $userInfoLayout.Dock = "Fill" $userInfoLayout.ColumnCount = 2 $userInfoLayout.RowCount = 1 $userInfoLayout.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Percent, 50))) | Out-Null $userInfoLayout.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Percent, 50))) | Out-Null $script:del_textBoxFoundUserDisplay = New-Object System.Windows.Forms.TextBox $script:del_textBoxFoundUserDisplay.Dock = "Fill" $script:del_textBoxFoundUserDisplay.ReadOnly = $true $script:del_textBoxFoundUserDisplay.Text = "표시 이름:" $script:del_textBoxFoundUserUPN = New-Object System.Windows.Forms.TextBox $script:del_textBoxFoundUserUPN.Dock = "Fill" $script:del_textBoxFoundUserUPN.ReadOnly = $true $script:del_textBoxFoundUserUPN.Text = "UPN:" $userInfoLayout.Controls.AddRange(@($script:del_textBoxFoundUserDisplay, $script:del_textBoxFoundUserUPN)) | Out-Null # '삭제 목록에 추가' 버튼 $script:del_buttonAddToDeleteList = New-Object System.Windows.Forms.Button $script:del_buttonAddToDeleteList.Text = "삭제 목록에 추가(&A) ⬇" $script:del_buttonAddToDeleteList.Dock = "Fill" $script:del_buttonAddToDeleteList.Enabled = $false # 검색 레이아웃에 컨트롤 추가 $searchLayout.Controls.Add($script:del_textBoxSearchUser, 0, 0) | Out-Null $searchLayout.Controls.Add($buttonSearchUserOnPrem, 1, 0) | Out-Null $searchLayout.Controls.Add($script:del_listBoxFoundUsers, 0, 1) | Out-Null $searchLayout.SetColumnSpan($script:del_listBoxFoundUsers, 2) $searchLayout.Controls.Add($userInfoLayout, 0, 2) | Out-Null $searchLayout.SetColumnSpan($userInfoLayout, 2) $searchLayout.Controls.Add($script:del_buttonAddToDeleteList, 0, 3) | Out-Null $searchLayout.SetColumnSpan($script:del_buttonAddToDeleteList, 2) # ========================================================== # 3. 삭제 대기 목록 그룹박스 # ========================================================== $groupQueue = New-Object System.Windows.Forms.GroupBox $groupQueue.Text = "2. 삭제 대기 목록 (우클릭으로 항목 관리)" $groupQueue.Dock = "Fill" $delTabMainLayout.Controls.Add($groupQueue, 0, 2) | Out-Null # 대기 목록 그룹박스 내부 레이아웃 $queueLayout = New-Object System.Windows.Forms.TableLayoutPanel $queueLayout.Dock = "Fill" $queueLayout.Padding = [System.Windows.Forms.Padding](10) $queueLayout.ColumnCount = 1 $queueLayout.RowCount = 2 $queueLayout.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Percent, 100))) | Out-Null $queueLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Percent, 100))) | Out-Null $queueLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 50))) | Out-Null $groupQueue.Controls.Add($queueLayout) | Out-Null # 삭제 대기열 리스트박스 및 컨텍스트 메뉴 $script:del_listBoxDeleteQueue = New-Object System.Windows.Forms.ListBox $script:del_listBoxDeleteQueue.Dock = "Fill" $contextMenu = New-Object System.Windows.Forms.ContextMenuStrip $menuItemRemove = New-Object System.Windows.Forms.ToolStripMenuItem("선택 항목 제거") $menuItemRemove.Name = "menuItemRemove" $menuItemClear = New-Object System.Windows.Forms.ToolStripMenuItem("목록 전체 비우기") $contextMenu.Items.AddRange(@($menuItemRemove, $menuItemClear)) | Out-Null $script:del_listBoxDeleteQueue.ContextMenuStrip = $contextMenu # '삭제 실행' 버튼 $script:del_buttonExecuteDelete = New-Object System.Windows.Forms.Button $script:del_buttonExecuteDelete.Dock = "Fill" $script:del_buttonExecuteDelete.Enabled = $false $script:del_buttonExecuteDelete.Font = New-Object System.Drawing.Font("Segoe UI", 12, [System.Drawing.FontStyle]::Bold) # 대기 목록 레이아웃에 컨트롤 추가 $queueLayout.Controls.Add($script:del_listBoxDeleteQueue, 0, 0) | Out-Null $queueLayout.Controls.Add($script:del_buttonExecuteDelete, 0, 1) | Out-Null # ========================================================== # 이벤트 핸들러 연결 # ========================================================== $script:del_radioSync.add_CheckedChanged($del_ModeChangedHandler) $script:del_radioOnPremOnly.add_CheckedChanged($del_ModeChangedHandler) $buttonSearchUserOnPrem.Add_Click($del_buttonSearchUserOnPrem_Click) $script:del_listBoxFoundUsers.Add_SelectedIndexChanged($del_listBoxFoundUsers_SelectedIndexChanged) $script:del_listBoxFoundUsers.Add_DoubleClick($del_listBoxFoundUsers_DoubleClick) $script:del_buttonAddToDeleteList.Add_Click({ Move-SelectedUsersToDeleteQueue }) $script:del_buttonExecuteDelete.Add_Click($del_buttonExecuteDelete_Click) $menuItemRemove.Add_Click($del_contextMenuRemove_Click) $menuItemClear.Add_Click($del_contextMenuClear_Click) # 컨텍스트 메뉴가 열릴 때 '선택 항목 제거' 메뉴의 활성화 여부 결정 $contextMenu.Add_Opening({ param($sourceItem, $e) $menuItem = $sourceItem.Items["menuItemRemove"] if ($menuItem) { $menuItem.Enabled = ($null -ne $script:del_listBoxDeleteQueue.SelectedItem) } }) # 초기 모드 설정에 따라 버튼 텍스트 업데이트 $del_ModeChangedHandler.Invoke($script:del_radioSync, $null) } #endregion #region '계정 삭제' 탭 이벤트 핸들러 및 함수 # 작업 모드 라디오 버튼 변경 시 '삭제 실행' 버튼의 텍스트를 변경하는 핸들러 $del_ModeChangedHandler = { param($sourceControl, $e) # 체크된 라디오 버튼에 대해서만 동작 if (-not $sourceControl.Checked) { return } if ($script:del_radioSync.Checked) { $script:del_buttonExecuteDelete.Text = "목록 삭제 및 동기화 실행(&E)" } else { $script:del_buttonExecuteDelete.Text = "목록 전체 삭제 실행 (On-Prem Only)(&E)" } } # '온프레미스 검색' 버튼 클릭 이벤트 핸들러 $del_buttonSearchUserOnPrem_Click = { $searchTerm = $script:del_textBoxSearchUser.Text.Trim() if ([string]::IsNullOrEmpty($searchTerm)) { return } # 동기 함수 호출 래퍼를 사용하여 사용자 검색 실행 (UI 응답성 유지) $users = Invoke-Synchronous -TriggerControl $this -StatusMessage "온프레미스 AD에서 사용자 '$searchTerm' 검색 중..." -ScriptBlock { # SamAccountName 또는 DisplayName에 검색어가 포함된 사용자를 찾음 Get-ADUser -Filter "(SamAccountName -like '*$searchTerm*') -or (DisplayName -like '*$searchTerm*')" -Properties DisplayName, UserPrincipalName, SamAccountName -Server $script:Configuration.OnPremDomainController -ErrorAction SilentlyContinue } # UI 업데이트 전 BeginUpdate() 호출로 깜빡임 방지 $script:del_listBoxFoundUsers.BeginUpdate() $script:del_listBoxFoundUsers.Items.Clear() | Out-Null if ($users) { # 찾은 사용자들을 이름순으로 정렬하여 리스트박스에 추가 foreach ($user in @($users) | Sort-Object DisplayName) { $display = "$($user.DisplayName) (sAM: $($user.SamAccountName))" $item = [PSCustomObject]@{ DisplayText = $display ADObject = $user } $script:del_listBoxFoundUsers.Items.Add($item) | Out-Null } } $script:del_listBoxFoundUsers.DisplayMember = "DisplayText" $script:del_listBoxFoundUsers.EndUpdate() Write-Log "$(@($users).Count)명의 사용자를 찾았습니다." } # 검색된 사용자 리스트박스에서 선택이 변경될 때의 이벤트 핸들러 $del_listBoxFoundUsers_SelectedIndexChanged = { # 다중 선택을 지원하므로 SelectedItems 속성 사용 $selectedItems = $this.SelectedItems if ($selectedItems.Count -gt 0) { # 정보 표시는 첫 번째 선택된 항목 기준으로 함 $firstSelectedItem = $selectedItems[0] $script:del_selectedADUser = $firstSelectedItem.ADObject $script:del_textBoxFoundUserDisplay.Text = "표시 이름: $($script:del_selectedADUser.DisplayName)" $script:del_textBoxFoundUserUPN.Text = "UPN: $($script:del_selectedADUser.UserPrincipalName)" $script:del_buttonAddToDeleteList.Enabled = $true } else { # 선택된 항목이 없으면 정보 초기화 및 버튼 비활성화 $script:del_selectedADUser = $null $script:del_textBoxFoundUserDisplay.Text = "표시 이름:" $script:del_textBoxFoundUserUPN.Text = "UPN:" $script:del_buttonAddToDeleteList.Enabled = $false } } # 검색된 사용자 리스트박스에서 항목을 더블클릭할 때의 이벤트 핸들러 $del_listBoxFoundUsers_DoubleClick = { # 더블클릭 시 해당 항목 하나만 삭제 대기 목록으로 이동 $selectedItem = $script:del_listBoxFoundUsers.SelectedItem if ($null -eq $selectedItem) { return } $userObject = $selectedItem.ADObject # 이미 대기 목록에 있는지 확인 (고유 식별자인 DistinguishedName 기준) $isAlreadyInQueue = $script:del_deleteQueue.DistinguishedName -contains $userObject.DistinguishedName if (-not $isAlreadyInQueue) { $script:del_deleteQueue.Add($userObject) | Out-Null $script:del_listBoxDeleteQueue.Items.Add($selectedItem.DisplayText) | Out-Null $script:del_buttonExecuteDelete.Enabled = $true $script:del_listBoxFoundUsers.Items.Remove($selectedItem) | Out-Null } else { [System.Windows.Forms.MessageBox]::Show("해당 사용자는 이미 삭제 대기 목록에 있습니다.", "중복 추가", "OK", "Information") | Out-Null } } # 검색 리스트에서 선택된 사용자들을 삭제 대기 목록으로 옮기는 함수 function Move-SelectedUsersToDeleteQueue { $selectedItems = $script:del_listBoxFoundUsers.SelectedItems if ($selectedItems.Count -eq 0) { return } # 여러 항목 변경 시 UI 깜빡임 방지를 위해 BeginUpdate/EndUpdate 사용 $script:del_listBoxFoundUsers.BeginUpdate() $script:del_listBoxDeleteQueue.BeginUpdate() # 반복 중 컬렉션에서 항목을 제거하면 문제가 발생하므로, 복사본을 만들어 순회 $itemsToMove = @($selectedItems) foreach ($item in $itemsToMove) { $userObject = $item.ADObject $isAlreadyInQueue = $script:del_deleteQueue.DistinguishedName -contains $userObject.DistinguishedName if (-not $isAlreadyInQueue) { $script:del_deleteQueue.Add($userObject) | Out-Null $script:del_listBoxDeleteQueue.Items.Add($item.DisplayText) | Out-Null $script:del_listBoxFoundUsers.Items.Remove($item) | Out-Null } } $script:del_buttonExecuteDelete.Enabled = ($script:del_deleteQueue.Count -gt 0) $script:del_listBoxFoundUsers.EndUpdate() $script:del_listBoxDeleteQueue.EndUpdate() } # 삭제 대기 목록 컨텍스트 메뉴 - '선택 항목 제거' 클릭 이벤트 핸들러 $del_contextMenuRemove_Click = { $selectedIndex = $script:del_listBoxDeleteQueue.SelectedIndex if ($selectedIndex -ne -1) { # 데이터 목록과 UI 목록에서 모두 제거 $script:del_deleteQueue.RemoveAt($selectedIndex) $script:del_listBoxDeleteQueue.Items.RemoveAt($selectedIndex) | Out-Null $script:del_buttonExecuteDelete.Enabled = ($script:del_deleteQueue.Count -gt 0) } } # 삭제 대기 목록 컨텍스트 메뉴 - '목록 전체 비우기' 클릭 이벤트 핸들러 $del_contextMenuClear_Click = { $script:del_deleteQueue.Clear() $script:del_listBoxDeleteQueue.Items.Clear() | Out-Null $script:del_buttonExecuteDelete.Enabled = $false } # '삭제 실행' 버튼 클릭 이벤트 핸들러 $del_buttonExecuteDelete_Click = { if ($script:del_deleteQueue.Count -eq 0) { return } # 삭제할 사용자 목록을 만들어 사용자에게 최종 확인 $userList = ($script:del_deleteQueue | ForEach-Object { "- $($_.DisplayName)" }) -join "`n" $message = "다음 $($script:del_deleteQueue.Count)명의 사용자를 영구적으로 삭제하시겠습니까?`n이 작업은 되돌릴 수 없습니다.`n`n$userList" $confirmResult = [System.Windows.Forms.MessageBox]::Show($message, "최종 삭제 확인", "YesNo", "Warning") if ($confirmResult -ne "Yes") { Write-Log "사용자가 삭제 작업을 취소했습니다." -Level "WARNING" return } # 동기 함수 래퍼를 통해 실제 삭제 작업 수행 $deletedCount = Invoke-Synchronous -TriggerControl $this -StatusMessage "사용자 삭제 작업을 실행합니다..." -ScriptBlock { $count = 0 # 반복 중 컬렉션 변경에 안전하도록 ToArray()로 복사본 사용 foreach ($userToDelete in $script:del_deleteQueue.ToArray()) { try { Write-Log "AD 사용자 '$($userToDelete.SamAccountName)' 삭제 시도..." Remove-ADUser -Identity $userToDelete.DistinguishedName -Confirm:$false -Server $script:Configuration.OnPremDomainController -ErrorAction Stop Write-Log " -> AD 사용자 '$($userToDelete.SamAccountName)' 삭제 성공." $count++ } catch { Write-Log " -> AD 사용자 '$($userToDelete.SamAccountName)' 삭제 실패: $($_.Exception.Message)" -Level "ERROR" } } # '동기화' 모드이고 한 명 이상 성공적으로 삭제된 경우, Azure AD Connect 델타 동기화 실행 if ($script:del_radioSync.Checked -and $count -gt 0) { Invoke-AadConnectSync -PolicyType Delta } # 성공적으로 삭제된 사용자 수 반환 return $count } # 작업 완료 후 결과 메시지 표시 및 탭 초기화 # 수정: $null을 비교 연산자 왼쪽에 배치 (PSScriptAnalyzer 규칙 준수) if ($null -ne $deletedCount) { [System.Windows.Forms.MessageBox]::Show("$deletedCount 명의 사용자에 대한 삭제 작업이 완료되었습니다.", "작업 완료", "OK", "Information") | Out-Null Reset-DeleteUserTab } } #endregion Write-Host "UI-Tab-DeleteUser.ps1 로드 완료." -ForegroundColor Cyan