convert to gitea
This commit is contained in:
387
Main.ps1
Normal file
387
Main.ps1
Normal file
@ -0,0 +1,387 @@
|
||||
# ================================================================================
|
||||
# 파일: Main.ps1
|
||||
# 역할: AD/M365 통합 관리 도구 메인 스크립트
|
||||
#
|
||||
# 작성자: 양범진
|
||||
# 버전: 1.15
|
||||
# 생성일자: 2025-06-05
|
||||
# 최종 수정일자: 2025-06-12
|
||||
#
|
||||
# 설명: 모든 기능 및 오류 수정이 완료된 최종 안정화 버전
|
||||
# 가독성 향상을 위해 코드 포매팅 및 주석 재정리
|
||||
# ================================================================================
|
||||
|
||||
# --- 1단계: 필수 어셈블리 및 모든 스크립트 파일 로드 ---
|
||||
# Windows Forms 및 Drawing 어셈블리를 로드하여 GUI를 생성
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
|
||||
# 스크립트 파일이 위치한 경로를 기준으로 관련 스크립트들을 로드
|
||||
$scriptRoot = $PSScriptRoot
|
||||
$ScriptsPath = Join-Path -Path $scriptRoot -ChildPath "Scripts"
|
||||
. (Join-Path -Path $ScriptsPath -ChildPath "Common.ps1")
|
||||
. (Join-Path -Path $ScriptsPath -ChildPath "UI-Tab-AddUser.ps1")
|
||||
. (Join-Path -Path $ScriptsPath -ChildPath "UI-Tab-DeleteUser.ps1")
|
||||
. (Join-Path -Path $ScriptsPath -ChildPath "UI-Tab-HardMatch.ps1")
|
||||
. (Join-Path -Path $ScriptsPath -ChildPath "UI-Tab-BatchTask.ps1")
|
||||
|
||||
# --- 2단계: 전역 변수 선언 및 초기화 ---
|
||||
# 스크립트 전체에서 사용될 변수들을 'script' 스코프로 선언
|
||||
|
||||
# 설정 및 로그 파일 경로
|
||||
$script:configFilePath = Join-Path -Path $scriptRoot -ChildPath "config.json"
|
||||
$script:logDir = Join-Path -Path $scriptRoot -ChildPath "logs"
|
||||
if (-not (Test-Path $script:logDir)) {
|
||||
New-Item -Path $script:logDir -ItemType Directory | Out-Null
|
||||
}
|
||||
$script:logFilePath = Join-Path -Path $script:logDir -ChildPath "ad_tool_$(Get-Date -Format 'yyyy-MM-dd').log"
|
||||
|
||||
# 메인 UI 컨트롤
|
||||
$script:mainForm = $null
|
||||
$script:richTextBoxLog = $null
|
||||
$script:connectAzureADMenu = $null
|
||||
$script:statusStrip = $null
|
||||
$script:statusLabelAzure = $null
|
||||
$script:statusLabelOnPrem = $null
|
||||
$script:statusLabelJob = $null
|
||||
$script:iconGreen = $null
|
||||
$script:iconRed = $null
|
||||
$script:iconGray = $null
|
||||
|
||||
# 환경 설정 및 상태 변수
|
||||
$script:Configuration = $null
|
||||
$script:CurrentADDomainInfo = $null
|
||||
$script:CurrentADDomainDN = $null
|
||||
$script:SynchronizedOURoots = @()
|
||||
|
||||
# '계정 생성' 탭 컨트롤
|
||||
$script:add_textBoxLastNameKr = $null
|
||||
$script:add_textBoxFirstNameKr = $null
|
||||
$script:add_textBoxAccountNameEn = $null
|
||||
$script:add_textBoxPassword = $null
|
||||
$script:add_textBoxSelectedOU = $null
|
||||
$script:add_treeViewOUs = $null
|
||||
$script:add_labelOUSyncStatus = $null
|
||||
$script:add_listBoxLicenses = $null
|
||||
$script:add_buttonCreate = $null
|
||||
$script:add_radioSync = $null
|
||||
$script:add_radioOnPremOnly = $null
|
||||
$script:add_groupLicense = $null
|
||||
$script:add_pictureBoxAccountValidation = $null
|
||||
$script:add_toolTip = $null
|
||||
$script:add_checkedListBoxServicePlans = $null
|
||||
|
||||
# '계정 삭제' 탭 컨트롤
|
||||
$script:del_textBoxSearchUser = $null
|
||||
$script:del_listBoxFoundUsers = $null
|
||||
$script:del_buttonAddToDeleteList = $null
|
||||
$script:del_listBoxDeleteQueue = $null
|
||||
$script:del_buttonExecuteDelete = $null
|
||||
$script:del_radioSync = $null
|
||||
$script:del_radioOnPremOnly = $null
|
||||
$script:del_selectedADUser = $null
|
||||
$script:del_deleteQueue = ([System.Collections.ArrayList]::new())
|
||||
$script:del_textBoxFoundUserDisplay = $null
|
||||
$script:del_textBoxFoundUserUPN = $null
|
||||
|
||||
# '하드 매칭' 탭 컨트롤
|
||||
$script:hm_textBoxSearchAzure = $null
|
||||
$script:hm_listBoxFoundAzure = $null
|
||||
$script:hm_textBoxOnPremUser = $null
|
||||
$script:hm_textBoxObjectGuid = $null
|
||||
$script:hm_textBoxConvertedImmutableId = $null
|
||||
$script:hm_textBoxAzureUser = $null
|
||||
$script:hm_textBoxImmutableId = $null
|
||||
$script:hm_labelGuidMatchStatus = $null
|
||||
$script:hm_groupCreate = $null
|
||||
$script:hm_textBoxSelectedOU = $null
|
||||
$script:hm_treeViewOUs = $null
|
||||
$script:hm_buttonExecuteAction = $null
|
||||
$script:hm_labelOUSyncStatus = $null
|
||||
$script:hm_textBoxFirstNameKr = $null
|
||||
$script:hm_textBoxLastNameKr = $null
|
||||
$script:hm_textBoxPassword = $null
|
||||
$script:hm_selectedAzureUser = $null
|
||||
$script:hm_foundOnPremUser = $null
|
||||
$script:hm_currentMode = "Idle"
|
||||
|
||||
# '일괄 작업' 탭 컨트롤
|
||||
$script:batch_comboBoxTaskType = $null
|
||||
$script:batch_buttonDownloadTemplate = $null
|
||||
$script:batch_textBoxCsvPath = $null
|
||||
$script:batch_buttonBrowseCsv = $null
|
||||
$script:batch_dataGridView = $null
|
||||
$script:batch_buttonExecute = $null
|
||||
$script:batch_progressBar = $null
|
||||
$script:batch_loadedCsvData = $null
|
||||
|
||||
# --- 3단계: 함수 정의 ---
|
||||
|
||||
# 메인 폼 및 모든 UI 컨트롤을 생성하고 초기화하는 함수
|
||||
function Initialize-MainForm {
|
||||
# 메인 폼 생성
|
||||
$script:mainForm = New-Object System.Windows.Forms.Form
|
||||
$script:mainForm.Text = "AD/M365 통합 관리 도구 v1.0"
|
||||
$script:mainForm.Size = New-Object System.Drawing.Size(950, 850)
|
||||
$script:mainForm.StartPosition = "CenterScreen"
|
||||
$script:mainForm.MinimumSize = New-Object System.Drawing.Size(900, 750)
|
||||
$script:mainForm.ShowInTaskbar = $false # 초기 로딩 시 작업 표시줄에 보이지 않도록 설정
|
||||
$script:mainForm.Opacity = 0 # 초기 로딩 시 투명하게 설정하여 깜빡임 방지
|
||||
|
||||
# 상단 메뉴 스트립 생성
|
||||
$mainMenu = New-Object System.Windows.Forms.MenuStrip
|
||||
$fileMenu = New-Object System.Windows.Forms.ToolStripMenuItem("파일(&F)")
|
||||
$script:connectAzureADMenu = New-Object System.Windows.Forms.ToolStripMenuItem("Azure AD 수동 연결/재연결(&C)")
|
||||
$configBackupMenu = New-Object System.Windows.Forms.ToolStripMenuItem("설정 백업(&B)")
|
||||
$configRestoreMenu = New-Object System.Windows.Forms.ToolStripMenuItem("설정 복원(&R)")
|
||||
$exitMenu = New-Object System.Windows.Forms.ToolStripMenuItem("종료(&X)")
|
||||
$fileMenu.DropDownItems.AddRange(@(
|
||||
$script:connectAzureADMenu,
|
||||
(New-Object System.Windows.Forms.ToolStripSeparator),
|
||||
$configBackupMenu,
|
||||
$configRestoreMenu,
|
||||
(New-Object System.Windows.Forms.ToolStripSeparator),
|
||||
$exitMenu
|
||||
)) | Out-Null
|
||||
|
||||
$toolsMenu = New-Object System.Windows.Forms.ToolStripMenuItem("도구(&T)")
|
||||
$syncNowMenu = New-Object System.Windows.Forms.ToolStripMenuItem("AAD Connect 즉시 동기화 실행(&S)...")
|
||||
$openLogFolderMenu = New-Object System.Windows.Forms.ToolStripMenuItem("로그 폴더 열기(&L)")
|
||||
$openConfigFileMenu = New-Object System.Windows.Forms.ToolStripMenuItem("설정 파일 열기(&O)")
|
||||
$toolsMenu.DropDownItems.AddRange(@(
|
||||
$syncNowMenu,
|
||||
(New-Object System.Windows.Forms.ToolStripSeparator),
|
||||
$openLogFolderMenu,
|
||||
$openConfigFileMenu
|
||||
)) | Out-Null
|
||||
|
||||
$mainMenu.Items.AddRange(@($fileMenu, $toolsMenu)) | Out-Null
|
||||
$script:mainForm.Controls.Add($mainMenu) | Out-Null
|
||||
|
||||
# 탭 컨트롤 생성
|
||||
$tabControl = New-Object System.Windows.Forms.TabControl
|
||||
$tabControl.Location = New-Object System.Drawing.Point(10, 30)
|
||||
$tabControl.Size = New-Object System.Drawing.Size(915, 600)
|
||||
$tabControl.Anchor = 'Top, Left, Right'
|
||||
$script:mainForm.Controls.Add($tabControl) | Out-Null
|
||||
|
||||
# 각 기능 탭 페이지 생성
|
||||
$tabPageAdd = New-Object System.Windows.Forms.TabPage("계정 생성")
|
||||
$tabPageDel = New-Object System.Windows.Forms.TabPage("계정 삭제")
|
||||
$tabPageHardMatch = New-Object System.Windows.Forms.TabPage("하드 매칭")
|
||||
$tabPageBatch = New-Object System.Windows.Forms.TabPage("일괄 작업 (CSV)")
|
||||
$tabControl.Controls.AddRange(@($tabPageAdd, $tabPageDel, $tabPageHardMatch, $tabPageBatch)) | Out-Null
|
||||
|
||||
# 각 탭의 UI를 초기화하는 함수 호출 (별도 스크립트 파일에 정의)
|
||||
Initialize-AddUserTab -parentTab $tabPageAdd
|
||||
Initialize-DeleteUserTab -parentTab $tabPageDel
|
||||
Initialize-HardMatchTab -parentTab $tabPageHardMatch
|
||||
Initialize-BatchTaskTab -parentTab $tabPageBatch
|
||||
|
||||
# 하단 로그 출력 영역 생성
|
||||
$labelLog = New-Object System.Windows.Forms.Label
|
||||
$labelLog.Text = "처리 로그:"
|
||||
$labelLog.Location = New-Object System.Drawing.Point(10, 640)
|
||||
$labelLog.Anchor = 'Top, Left'
|
||||
$labelLog.AutoSize = $true
|
||||
$script:mainForm.Controls.Add($labelLog) | Out-Null
|
||||
|
||||
$script:richTextBoxLog = New-Object System.Windows.Forms.RichTextBox
|
||||
$script:richTextBoxLog.Location = New-Object System.Drawing.Point(10, 660)
|
||||
$script:richTextBoxLog.Size = New-Object System.Drawing.Size(915, 125)
|
||||
$script:richTextBoxLog.ReadOnly = $true
|
||||
$script:richTextBoxLog.ScrollBars = "Vertical"
|
||||
$script:richTextBoxLog.Anchor = 'Top, Left, Right, Bottom'
|
||||
$script:mainForm.Controls.Add($script:richTextBoxLog) | Out-Null
|
||||
|
||||
# 하단 상태 표시줄 생성
|
||||
$script:iconGreen = New-StatusIcon -Color ([System.Drawing.Color]::LimeGreen)
|
||||
$script:iconRed = New-StatusIcon -Color ([System.Drawing.Color]::Red)
|
||||
$script:iconGray = New-Object System.Drawing.Bitmap(1, 1) # 회색 아이콘 대신 기본 비트맵
|
||||
|
||||
$script:statusStrip = New-Object System.Windows.Forms.StatusStrip
|
||||
$script:statusStrip.SizingGrip = $false
|
||||
|
||||
$script:statusLabelAzure = New-Object System.Windows.Forms.ToolStripStatusLabel
|
||||
$script:statusLabelAzure.Image = $script:iconRed
|
||||
$script:statusLabelAzure.Text = "Azure AD: 확인 중..."
|
||||
|
||||
$script:statusLabelOnPrem = New-Object System.Windows.Forms.ToolStripStatusLabel
|
||||
$script:statusLabelOnPrem.Image = $script:iconGray
|
||||
$script:statusLabelOnPrem.Text = "On-Prem DC: 확인 중..."
|
||||
|
||||
$script:statusLabelJob = New-Object System.Windows.Forms.ToolStripStatusLabel
|
||||
$script:statusLabelJob.Spring = $true # 남은 공간을 모두 차지하도록 설정
|
||||
$script:statusLabelJob.TextAlign = "MiddleRight"
|
||||
|
||||
$script:statusStrip.Items.AddRange(@(
|
||||
$script:statusLabelAzure,
|
||||
(New-Object System.Windows.Forms.ToolStripSeparator),
|
||||
$script:statusLabelOnPrem,
|
||||
$script:statusLabelJob
|
||||
)) | Out-Null
|
||||
$script:mainForm.Controls.Add($script:statusStrip) | Out-Null
|
||||
|
||||
# 메뉴 항목에 대한 이벤트 핸들러 연결
|
||||
$syncNowMenu.Add_Click($syncNowMenu_Click)
|
||||
$script:connectAzureADMenu.Add_Click({ Connect-AzureAD-WithInteraction })
|
||||
$configBackupMenu.Add_Click($configBackupMenu_Click)
|
||||
$configRestoreMenu.Add_Click($configRestoreMenu_Click)
|
||||
$exitMenu.Add_Click({ if ($script:mainForm.IsHandleCreated) { $script:mainForm.Close() } })
|
||||
$openLogFolderMenu.Add_Click({ Invoke-Item $script:logDir })
|
||||
$openConfigFileMenu.Add_Click({ if (Test-Path $script:configFilePath) { Invoke-Item $script:configFilePath } })
|
||||
}
|
||||
|
||||
# '설정 백업' 메뉴 클릭 이벤트 핸들러
|
||||
$configBackupMenu_Click = {
|
||||
$sfd = New-Object System.Windows.Forms.SaveFileDialog
|
||||
$sfd.Filter = "JSON 파일 (*.json)|*.json"
|
||||
$sfd.FileName = "config.backup.json"
|
||||
if ($sfd.ShowDialog() -eq "OK") {
|
||||
try {
|
||||
Copy-Item -Path $script:configFilePath -Destination $sfd.FileName -Force
|
||||
[System.Windows.Forms.MessageBox]::Show("설정 파일이 백업되었습니다.", "백업 완료", "OK", "Information") | Out-Null
|
||||
}
|
||||
catch {
|
||||
Show-DetailedErrorDialog -ErrorRecord $_
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# '설정 복원' 메뉴 클릭 이벤트 핸들러
|
||||
$configRestoreMenu_Click = {
|
||||
$ofd = New-Object System.Windows.Forms.OpenFileDialog
|
||||
$ofd.Filter = "JSON 파일 (*.json)|*.json"
|
||||
if ($ofd.ShowDialog() -eq "OK") {
|
||||
try {
|
||||
Copy-Item -Path $ofd.FileName -Destination $script:configFilePath -Force
|
||||
[System.Windows.Forms.MessageBox]::Show("설정을 복원했습니다. 프로그램을 다시 시작해주세요.", "복원 완료", "OK", "Information") | Out-Null
|
||||
$script:mainForm.Close()
|
||||
}
|
||||
catch {
|
||||
Show-DetailedErrorDialog -ErrorRecord $_
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 'AAD Connect 즉시 동기화' 메뉴 클릭 이벤트 핸들러
|
||||
$syncNowMenu_Click = {
|
||||
# 동기화 유형을 선택하는 작은 대화상자 생성
|
||||
$syncForm = New-Object System.Windows.Forms.Form
|
||||
$syncForm.Text = "동기화 유형 선택"
|
||||
$syncForm.Size = New-Object System.Drawing.Size(300, 180)
|
||||
$syncForm.StartPosition = "CenterParent"
|
||||
$syncForm.FormBorderStyle = "FixedDialog"
|
||||
$syncForm.MaximizeBox = $false
|
||||
$syncForm.MinimizeBox = $false
|
||||
|
||||
$radioDelta = New-Object System.Windows.Forms.RadioButton
|
||||
$radioDelta.Text = "증분 동기화 (Delta) - 권장"
|
||||
$radioDelta.Location = New-Object System.Drawing.Point(20, 20)
|
||||
$radioDelta.AutoSize = $true
|
||||
$radioDelta.Checked = $true
|
||||
|
||||
$radioFull = New-Object System.Windows.Forms.RadioButton
|
||||
$radioFull.Text = "전체 동기화 (Initial/Full)"
|
||||
$radioFull.Location = New-Object System.Drawing.Point(20, 50)
|
||||
$radioFull.AutoSize = $true
|
||||
|
||||
$runButton = New-Object System.Windows.Forms.Button
|
||||
$runButton.Text = "실행"
|
||||
$runButton.DialogResult = "OK"
|
||||
$runButton.Location = New-Object System.Drawing.Point(50, 100)
|
||||
$runButton.Size = New-Object System.Drawing.Size(90, 30)
|
||||
|
||||
$cancelButton = New-Object System.Windows.Forms.Button
|
||||
$cancelButton.Text = "취소"
|
||||
$cancelButton.DialogResult = "Cancel"
|
||||
$cancelButton.Location = New-Object System.Drawing.Point(150, 100)
|
||||
$cancelButton.Size = New-Object System.Drawing.Size(90, 30)
|
||||
|
||||
$syncForm.Controls.AddRange(@($radioDelta, $radioFull, $runButton, $cancelButton)) | Out-Null
|
||||
$syncForm.AcceptButton = $runButton
|
||||
$syncForm.CancelButton = $cancelButton
|
||||
|
||||
# 사용자가 '실행'을 누르면 동기화 실행
|
||||
if ($syncForm.ShowDialog($script:mainForm) -eq "OK") {
|
||||
$policyType = if ($radioDelta.Checked) { "Delta" } else { "Initial" }
|
||||
Invoke-AadConnectSync -PolicyType $policyType
|
||||
}
|
||||
$syncForm.Dispose()
|
||||
}
|
||||
|
||||
# ================================================================================
|
||||
# 프로그램 실행 시작
|
||||
# ================================================================================
|
||||
|
||||
# 1. 관리자 권한 확인
|
||||
if (-Not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
||||
[System.Windows.Forms.MessageBox]::Show("관리자 권한으로 실행해야 합니다.", "권한 오류", "OK", "Error") | Out-Null
|
||||
Exit
|
||||
}
|
||||
|
||||
# 2. 필수 PowerShell 모듈 확인
|
||||
if (-not (Test-RequiredModules)) {
|
||||
[System.Windows.Forms.MessageBox]::Show("필수 PowerShell 모듈이 없어 프로그램을 종료합니다.", "모듈 오류", "OK", "Error") | Out-Null
|
||||
Exit
|
||||
}
|
||||
|
||||
# 3. 설정 파일(config.json) 로드
|
||||
if (Test-Path $script:configFilePath) {
|
||||
try {
|
||||
$script:Configuration = Get-Content -Path $script:configFilePath -Raw | ConvertFrom-Json
|
||||
# 이전 버전과의 호환성을 위해 불필요한 속성 제거
|
||||
if ($script:Configuration.PSObject.Properties.Name.Contains('AccountNameFormat')) {
|
||||
$script:Configuration.PSObject.Properties.Remove('AccountNameFormat')
|
||||
}
|
||||
}
|
||||
catch {
|
||||
[System.Windows.Forms.MessageBox]::Show("설정 파일 읽기 오류. 기본 설정으로 시작합니다.", "설정 오류", "OK", "Warning") | Out-Null
|
||||
$script:Configuration = $null
|
||||
}
|
||||
}
|
||||
|
||||
# 4. 설정 파일이 없거나 오류 발생 시 기본값으로 초기화
|
||||
if ($null -eq $script:Configuration) {
|
||||
$script:Configuration = [ordered]@{
|
||||
OnPremDomainController = "dc.example.com"
|
||||
AADConnectServerName = ""
|
||||
AzureTenantId = ""
|
||||
UPNSuffix = "example.com"
|
||||
SynchronizedOUs = "OU=Users,DC=example,DC=com"
|
||||
DefaultPassword = "Password123!"
|
||||
DefaultUsageLocation = "KR"
|
||||
}
|
||||
}
|
||||
|
||||
# 5. 메인 폼 UI 초기화
|
||||
Initialize-MainForm
|
||||
Write-Log "AD/M365 통합 관리 도구를 시작합니다."
|
||||
|
||||
# 6. 설정 대화상자 표시 및 사용자 설정 확인
|
||||
if (-not (Show-ConfigurationDialog)) {
|
||||
Write-Log "사용자가 설정을 취소하여 프로그램을 종료합니다."
|
||||
Exit
|
||||
}
|
||||
|
||||
# 7. 스크립트 실행 환경(On-Prem AD 연결 등) 초기화
|
||||
if (-not (Initialize-ScriptEnvironment)) {
|
||||
[System.Windows.Forms.MessageBox]::Show("온프레미스 DC에 연결할 수 없습니다. 설정을 확인하고 다시 시작해주세요.", "초기화 오류", "OK", "Error") | Out-Null
|
||||
Exit
|
||||
}
|
||||
|
||||
# 8. 메인 폼 표시 및 이벤트 핸들러 등록
|
||||
$script:mainForm.ShowInTaskbar = $true
|
||||
$script:mainForm.Opacity = 1
|
||||
$script:mainForm.Activate()
|
||||
|
||||
# 폼이 처음 표시된 후 자동으로 Azure AD에 연결하고 OU 트리를 로드
|
||||
$script:mainForm.Add_Shown({
|
||||
Connect-AzureAD-Silently
|
||||
Update-OU-TreeView -TriggerControl $null
|
||||
})
|
||||
|
||||
# 메인 폼을 대화상자 형태로 실행하여 사용자가 닫을 때까지 대기
|
||||
$script:mainForm.ShowDialog() | Out-Null
|
||||
|
||||
Write-Log "프로그램을 종료합니다."
|
||||
131
README.md
Normal file
131
README.md
Normal file
@ -0,0 +1,131 @@
|
||||
## AD/M365 통합 관리 도구
|
||||
|
||||
---
|
||||
|
||||
### 1. 개요
|
||||
|
||||
하이브리드 Active Directory (On-Premise AD + Azure AD) 환경에서 사용자 계정의 생성, 삭제, 동기화 관리를 자동화하고 간소화하기 위한 PowerShell 기반의 GUI 도구이다. IT 관리자는 이 도구를 통해 반복적인 작업을 줄이고 휴먼 에러를 최소화할 수 있다.
|
||||
|
||||
모든 작업은 UI가 멈추지 않는 동기 방식으로 처리되며, 스크립트 실행에 필요한 모듈이나 기능은 최초 실행 시 자동으로 설치를 안내하여 사용자 편의성을 높였다.
|
||||
|
||||
### 2. 주요 기능
|
||||
|
||||
- **계정 생성**:
|
||||
- On-Premise AD 사용자 생성.
|
||||
- AAD Connect 동기화 (Delta) 자동 실행.
|
||||
- Azure AD 사용자 확인 후 M365 라이선스 및 서비스 플랜 할당.
|
||||
- On-Premise AD에만 사용자를 생성하는 옵션 제공.
|
||||
- **계정 삭제**:
|
||||
- On-Premise AD에서 사용자 검색 및 다중 선택.
|
||||
- 삭제 대기열에 사용자를 추가하여 일괄 삭제.
|
||||
- AAD Connect 동기화 (Delta) 자동 실행.
|
||||
- **하드 매칭 (Hard Match)**:
|
||||
- Azure AD에만 존재하는 사용자(Cloud-Only)를 On-Premise AD 사용자와 연결.
|
||||
- **시나리오 1**: 기존 On-Premise AD 계정과 매칭 (ImmutableId 설정).
|
||||
- **시나리오 2**: 신규 On-Premise AD 계정을 생성하여 즉시 매칭.
|
||||
- 매칭 후 즉시 동기화를 실행하여 상태를 바로잡음.
|
||||
- **일괄 작업 (CSV)**:
|
||||
- CSV 파일을 이용한 대량 계정 생성 및 삭제.
|
||||
- 작업별 CSV 템플릿 다운로드 기능 제공.
|
||||
- 실행 전 데이터 유효성 검사 (OU 존재 여부, 계정명 중복 등)를 통해 작업 실패율 최소화.
|
||||
- **편의 기능**:
|
||||
- 직관적인 GUI 및 작업 상태 실시간 로깅.
|
||||
- 스크립트 실행에 필요한 모듈(ActiveDirectory, Microsoft.Graph) 자동 검사 및 설치 안내.
|
||||
- 주요 설정(`config.json`)을 GUI 대화상자를 통해 쉽게 변경 및 저장.
|
||||
|
||||
### 3. 요구 사항 및 환경
|
||||
|
||||
- **운영 체제**: Windows 10, Windows 11, Windows Server 2016 이상
|
||||
- **PowerShell**: 버전 5.1 이상
|
||||
- **권한**: 스크립트를 실행하는 사용자는 **로컬 관리자** 권한 및 **Domain Admins** 권한이 필요.
|
||||
- **필수 모듈/기능**:
|
||||
- RSAT: Active Directory Domain Services 및 LDS(Lightweight Directory Services) 도구
|
||||
- PowerShell Module: `Microsoft.Graph`
|
||||
> *위 항목들은 스크립트 최초 실행 시 자동으로 설치 여부를 확인하고 설치를 안내함.*
|
||||
- **네트워크**: On-Premise Domain Controller 및 Azure AD (graph.microsoft.com)와 통신이 가능해야 함.
|
||||
|
||||
### 4. 설치 및 설정
|
||||
|
||||
1. **파일 다운로드**
|
||||
- 리포지토리의 모든 파일을 다운로드하여 원하는 위치에 압축을 해제한다.
|
||||
|
||||
2. **설정 파일 (`config.json`) 수정**
|
||||
- 프로그램을 처음 실행하면 설정 대화상자가 나타나지만, 미리 `config.json` 파일을 자신의 환경에 맞게 수정할 수도 있다.
|
||||
|
||||
| 키 | 설명 | 예시 |
|
||||
| ----------------------- | -------------------------------------------------------------------------------------------------- | --------------------------------------- |
|
||||
| `OnPremDomainController` | On-Premise Active Directory 도메인 컨트롤러의 FQDN. | `dc01.mydomain.local` |
|
||||
| `AADConnectServerName` | Azure AD Connect가 설치된 서버의 이름. 비워두면 로컬에서 동기화 명령을 실행. | `aadc01` |
|
||||
| `AzureTenantId` | Azure AD 테넌트의 ID. | `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` |
|
||||
| `UPNSuffix` | 사용자 계정의 UPN(User Principal Name)으로 사용할 접미사. | `mydomain.com` |
|
||||
| `SynchronizedOUs` | AAD Connect로 동기화되는 OU의 **이름** 목록. 세미콜론(;)으로 구분. | `Sales;Marketing;Developers` |
|
||||
| `DefaultPassword` | 계정 생성 시 사용할 기본 초기 비밀번호. | `Password123!` |
|
||||
| `DefaultUsageLocation` | M365 라이선스 할당 시 필요한 사용 위치 (ISO 3166-1 alpha-2 국가 코드). | `KR` |
|
||||
|
||||
3. **스크립트 실행**
|
||||
- `Main.ps1` 파일을 마우스 오른쪽 버튼으로 클릭하여 **'PowerShell에서 실행'**을 선택한다.
|
||||
- **반드시 관리자 권한으로 실행해야 한다.**
|
||||
- 최초 실행 시 필수 모듈 설치 여부를 확인하며, 설치가 필요할 경우 관리자 권한으로 설치를 진행한다.
|
||||
- 설정 대화상자가 나타나면 내용을 확인하고 '시작' 버튼을 클릭한다.
|
||||
|
||||
### 5. 사용 방법
|
||||
|
||||
#### 5.1. 계정 생성 탭
|
||||
|
||||
1. **작업 모드 선택**: 'AD/Azure AD 동기화' 또는 '온프레미스 AD에만 생성' 중 하나를 선택한다.
|
||||
2. **사용자 정보 입력**: 한글 성/이름, 영문 계정명, 초기 비밀번호를 입력한다.
|
||||
- 영문 계정명은 입력이 끝나면 On-Premise 및 Azure AD에 중복되는 계정이 있는지 자동으로 검사한다.
|
||||
3. **대상 OU 선택**: 사용자를 생성할 On-Premise AD의 조직 단위(OU)를 트리에서 선택한다.
|
||||
- 선택한 OU가 AD Connect 동기화 대상인지 여부가 하단에 표시된다.
|
||||
4. **M365 라이선스 할당** (동기화 모드 시):
|
||||
- 할당할 라이선스를 목록에서 선택한다.
|
||||
- 선택한 라이선스에 포함된 서비스 플랜 중 비활성화할 항목은 체크를 해제한다.
|
||||
5. **계정 생성 실행**: 버튼을 클릭하여 작업을 시작한다. 작업 과정은 하단 로그 창에 출력된다.
|
||||
|
||||
#### 5.2. 계정 삭제 탭
|
||||
|
||||
1. **사용자 검색**: 삭제할 사용자의 이름 또는 계정명 일부를 입력하고 '온프레미스 검색' 버튼을 클릭한다.
|
||||
2. **삭제 목록에 추가**: 검색 결과에서 삭제할 사용자를 선택(다중 선택 가능)하고 '삭제 목록에 추가' 버튼을 클릭하거나, 항목을 더블클릭한다.
|
||||
3. **삭제 실행**: 삭제 대기 목록을 확인한 후, '목록 삭제 실행' 버튼을 클릭한다. 최종 확인 대화상자에서 '예'를 누르면 작업이 시작된다.
|
||||
|
||||
#### 5.3. 하드 매칭 탭
|
||||
|
||||
1. **Azure AD 사용자 검색**: 매칭할 Azure AD 사용자의 이름 또는 UPN을 입력하고 'Azure AD 검색' 버튼을 클릭한다.
|
||||
2. **사용자 선택**: 검색 결과 목록에서 대상 사용자를 선택한다.
|
||||
3. **상태 확인 및 작업 선택**:
|
||||
- **On-Premise 계정이 없는 경우**: '계정 생성 및 하드 매칭' 모드로 전환된다. 신규 생성에 필요한 정보를 입력하고 대상 OU를 선택한다.
|
||||
- **On-Premise 계정이 있는 경우**: '기존 계정과 하드 매칭' 모드로 전환된다. Azure AD의 `ImmutableId`와 On-Premise AD의 `ObjectGUID`를 비교하여 현재 동기화 상태를 진단하고 필요한 작업을 안내한다.
|
||||
4. **작업 실행**: 버튼을 클릭하여 매칭 작업을 시작한다.
|
||||
|
||||
#### 5.4. 일괄 작업 (CSV) 탭
|
||||
|
||||
1. **작업 유형 선택**: '계정 생성' 또는 '계정 삭제'를 선택한다.
|
||||
2. **템플릿 다운로드**: '템플릿 다운로드' 버튼을 클릭하여 작업에 맞는 CSV 파일을 받는다.
|
||||
3. **CSV 파일 작성**: 다운로드한 템플릿에 맞춰 작업할 데이터를 입력한다.
|
||||
4. **파일 불러오기**: '파일 찾아보기' 버튼으로 작성한 CSV 파일을 선택하면 데이터가 그리드에 표시된다.
|
||||
5. **유효성 검사**: '유효성 검사' 버튼을 클릭하여 데이터의 정합성(필수 값, OU 경로, 계정 중복 등)을 미리 확인한다.
|
||||
6. **일괄 작업 실행**: 유효성 검사를 통과하면 '일괄 작업 실행' 버튼이 활성화된다. 버튼을 클릭하여 작업을 시작한다.
|
||||
|
||||
### 6. 주요 아키텍처 및 로직
|
||||
|
||||
- **GUI**: PowerShell에서 `System.Windows.Forms` .NET 어셈블리를 직접 호출하여 모든 UI 요소를 동적으로 생성한다.
|
||||
- **비동기 처리**: `Invoke-Synchronous` 라는 공용 함수를 통해 시간이 오래 걸리는 작업(AD/Graph API 호출, 동기화 등)을 실행한다. 이 함수는 작업 중 UI 컨트롤을 비활성화하고 커서를 대기 상태로 변경하여 사용자에게 작업 상태를 명확히 알려주고, UI가 멈추는 현상을 방지한다.
|
||||
- **모듈화**: 기능별로 스크립트 파일을 분리하여 코드의 가독성과 유지보수성을 높였다.
|
||||
- `Main.ps1`: 프로그램 진입점, 전역 변수 및 메인 폼 초기화.
|
||||
- `Common.ps1`: 모든 기능에서 공통으로 사용하는 함수 라이브러리 (로그, API 호출 래퍼, 설정 관리 등).
|
||||
- `UI-Tab-*.ps1`: 각 탭의 UI 생성 및 이벤트 핸들러 로직을 담당.
|
||||
|
||||
### 7. 파일 구조
|
||||
|
||||
```
|
||||
.
|
||||
├── Main.ps1 # 메인 스크립트 (시작 파일)
|
||||
├── config.json # 환경 설정 파일
|
||||
├── logs/ # 로그 파일 저장 디렉터리
|
||||
└── Scripts/
|
||||
├── Common.ps1 # 공용 함수 라이브러리
|
||||
├── UI-Tab-AddUser.ps1 # '계정 생성' 탭 UI 및 로직
|
||||
├── UI-Tab-DeleteUser.ps1 # '계정 삭제' 탭 UI 및 로직
|
||||
├── UI-Tab-HardMatch.ps1 # '하드 매칭' 탭 UI 및 로직
|
||||
└── UI-Tab-BatchTask.ps1 # '일괄 작업' 탭 UI 및 로직
|
||||
```
|
||||
654
Scripts/Common.ps1
Normal file
654
Scripts/Common.ps1
Normal file
@ -0,0 +1,654 @@
|
||||
# ================================================================================
|
||||
# 파일: Scripts/Common.ps1
|
||||
# 역할: 공용 함수 라이브러리
|
||||
#
|
||||
# 작성자: 양범진
|
||||
# 버전: 1.15
|
||||
# 생성일자: 2025-06-05
|
||||
# 최종 수정일자: 2025-06-17
|
||||
#
|
||||
# 설명:
|
||||
# - 프로그램 전반에서 사용되는 공통 함수들을 정의
|
||||
# - 스레드 관련 복잡성을 제거하고 안정적인 동기 실행 방식을 사용
|
||||
# - OS 종류에 따라 모듈 설치를 다르게 진행(Windows 10/11 선택적 기능 추가)
|
||||
# ================================================================================
|
||||
|
||||
#region 공용 유틸리티 함수
|
||||
# --- 필수 모듈 검사 및 설치 함수 ---
|
||||
# 스크립트 실행에 필요한 PowerShell 모듈 및 기능이 있는지 확인하고, 없을 경우 사용자에게 설치를 안내
|
||||
function Test-RequiredModules {
|
||||
# --- OS 종류 확인 ---
|
||||
# ProductType 1이 일반, 그 외(2,3) 서버. 편의성을 위해 1만으로 구분하여 판단.
|
||||
$osInfo = Get-CimInstance -ClassName Win32_OperatingSystem
|
||||
$isClientOS = ($osInfo.ProductType -eq 1)
|
||||
|
||||
# 설치 명령어 정의
|
||||
$installCommands = @{
|
||||
"ActiveDirectory" = if ($isClientOS) {
|
||||
"Add-WindowsCapability -Online -Name 'Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0'"
|
||||
} else {
|
||||
"Install-WindowsFeature -Name 'RSAT-AD-PowerShell' -IncludeAllSubFeature"
|
||||
}
|
||||
"Microsoft.Graph" = "Install-Module Microsoft.Graph -Repository PSGallery -Force -AllowClobber -Scope AllUsers -Confirm:`$false"
|
||||
}
|
||||
|
||||
$missingModules = @()
|
||||
|
||||
# --- 1. ActiveDirectory 기능(RSAT) 설치 여부 확인 ---
|
||||
$adModuleInstalled = $false
|
||||
try {
|
||||
if ($isClientOS) {
|
||||
Write-Host "일반 OS 감지. 'RSAT: Active Directory...' 기능 설치 여부 확인 중..."
|
||||
$capability = Get-WindowsCapability -Online -Name 'Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0' -ErrorAction SilentlyContinue
|
||||
if ($capability -and $capability.State -eq 'Installed') {
|
||||
$adModuleInstalled = $true
|
||||
}
|
||||
} else {
|
||||
Write-Host "서버 OS 감지. 'RSAT-AD-PowerShell' 기능 설치 여부 확인 중..."
|
||||
$feature = Get-WindowsFeature -Name 'RSAT-AD-PowerShell' -ErrorAction SilentlyContinue
|
||||
if ($feature -and $feature.Installed) {
|
||||
$adModuleInstalled = $true
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Write-Warning "Active Directory 기능 확인 중 오류 발생: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
if (-not $adModuleInstalled) {
|
||||
$missingModules += "ActiveDirectory"
|
||||
Write-Host "결과: Active Directory 관리 도구가 설치되지 않았습니다." -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host "결과: Active Directory 관리 도구가 이미 설치되어 있습니다." -ForegroundColor Green
|
||||
}
|
||||
|
||||
|
||||
# --- 2. Microsoft.Graph 모듈 설치 여부 확인 ---
|
||||
Write-Host "'Microsoft.Graph' 모듈 설치 여부 확인 중..."
|
||||
if (-not (Get-Module -ListAvailable -Name "Microsoft.Graph")) {
|
||||
$missingModules += "Microsoft.Graph"
|
||||
Write-Host "결과: Microsoft.Graph 모듈이 설치되지 않았습니다." -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host "결과: Microsoft.Graph 모듈이 이미 설치되어 있습니다." -ForegroundColor Green
|
||||
}
|
||||
|
||||
|
||||
# --- 3. 누락된 모듈/기능이 있으면 설치 진행 ---
|
||||
if ($missingModules.Count -gt 0) {
|
||||
$message = "필수 구성 요소가 설치되지 않았습니다:`n`n- $($missingModules -join "`n- ")`n`n지금 설치하시겠습니까? (시간이 소요될 수 있습니다)"
|
||||
$response = [System.Windows.Forms.MessageBox]::Show($message, "필수 구성 요소 설치", "YesNo", "Question")
|
||||
|
||||
if ($response -eq "Yes") {
|
||||
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
||||
[System.Windows.Forms.MessageBox]::Show("설치에는 관리자 권한이 필요합니다. PowerShell을 '관리자 권한으로 실행'한 후 다시 시도해주세요.", "권한 필요", "OK", "Warning") | Out-Null
|
||||
return $false
|
||||
}
|
||||
|
||||
foreach ($moduleToInstall in $missingModules) {
|
||||
Write-Host "'$moduleToInstall' 설치 시작..."
|
||||
if ($moduleToInstall -eq "ActiveDirectory" -and $isClientOS) {
|
||||
Write-Host "Windows 기능 설치 중입니다. PC 환경에 따라 수 분 이상 소요될 수 있습니다. 잠시만 기다려주세요..." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
try {
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
Invoke-Expression -Command $installCommands[$moduleToInstall] -ErrorAction Stop
|
||||
Write-Host "'$moduleToInstall' 설치 성공!" -ForegroundColor Green
|
||||
} catch {
|
||||
[System.Windows.Forms.MessageBox]::Show("'$moduleToInstall' 자동 설치 실패: $($_.Exception.Message)`n`nPowerShell을 관리자 권한으로 실행했는지 확인해주세요.", "설치 실패", "OK", "Error") | Out-Null
|
||||
return $false
|
||||
}
|
||||
}
|
||||
[System.Windows.Forms.MessageBox]::Show("설치가 완료되었습니다. 스크립트를 다시 시작해주세요.", "설치 완료", "OK", "Information") | Out-Null
|
||||
Exit
|
||||
} else {
|
||||
Write-Warning "설치를 취소하여 종료합니다."
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
# --- 4. 설치된 모듈 현재 세션으로 가져오기 ---
|
||||
try {
|
||||
Import-Module ActiveDirectory -ErrorAction Stop
|
||||
Import-Module Microsoft.Graph.Authentication -ErrorAction Stop
|
||||
Import-Module Microsoft.Graph.Users -ErrorAction Stop
|
||||
Import-Module Microsoft.Graph.Identity.DirectoryManagement -ErrorAction SilentlyContinue
|
||||
return $true
|
||||
} catch {
|
||||
[System.Windows.Forms.MessageBox]::Show("모듈 로드 오류: $($_.Exception.Message)", "오류", "OK", "Error") | Out-Null
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
# 상세 오류 정보 대화상자 표시 함수
|
||||
# 예외 발생 시, 사용자에게 기본 오류 메시지와 함께 상세 정보를 볼 수 있는 대화상자를 표시
|
||||
function Show-DetailedErrorDialog {
|
||||
Param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Management.Automation.ErrorRecord]$ErrorRecord,
|
||||
[string]$Message = "작업 중 오류 발생"
|
||||
)
|
||||
$errorForm = New-Object System.Windows.Forms.Form
|
||||
$errorForm.Text = "오류"
|
||||
$errorForm.Size = New-Object System.Drawing.Size(500, 200)
|
||||
$errorForm.FormBorderStyle = "FixedDialog"
|
||||
$errorForm.StartPosition = "CenterParent"
|
||||
|
||||
$iconLabel = New-Object System.Windows.Forms.Label
|
||||
$iconLabel.Image = [System.Drawing.SystemIcons]::Error.ToBitmap()
|
||||
$iconLabel.Location = New-Object System.Drawing.Point(20, 20)
|
||||
$iconLabel.Size = New-Object System.Drawing.Size(32, 32)
|
||||
|
||||
$msgLabel = New-Object System.Windows.Forms.Label
|
||||
$msgLabel.Text = "$Message`n$($ErrorRecord.Exception.Message)"
|
||||
$msgLabel.Location = New-Object System.Drawing.Point(60, 20)
|
||||
$msgLabel.Size = New-Object System.Drawing.Size(420, 60)
|
||||
|
||||
$okButton = New-Object System.Windows.Forms.Button
|
||||
$okButton.Text = "확인"
|
||||
$okButton.Location = New-Object System.Drawing.Point(210, 120)
|
||||
$okButton.Size = New-Object System.Drawing.Size(80, 25)
|
||||
$okButton.DialogResult = "OK"
|
||||
|
||||
$detailsButton = New-Object System.Windows.Forms.Button
|
||||
$detailsButton.Text = "자세히(&D) >>"
|
||||
$detailsButton.Location = New-Object System.Drawing.Point(380, 120)
|
||||
$detailsButton.Size = New-Object System.Drawing.Size(100, 25)
|
||||
|
||||
$detailsTextBox = New-Object System.Windows.Forms.TextBox
|
||||
$detailsTextBox.Multiline = $true
|
||||
$detailsTextBox.ScrollBars = "Both"
|
||||
$detailsTextBox.ReadOnly = $true
|
||||
$detailsTextBox.Text = $ErrorRecord.ToString()
|
||||
$detailsTextBox.Location = New-Object System.Drawing.Point(20, 160)
|
||||
$detailsTextBox.Size = New-Object System.Drawing.Size(460, 150)
|
||||
$detailsTextBox.Visible = $false
|
||||
|
||||
$detailsButton.Add_Click({
|
||||
if ($detailsTextBox.Visible) {
|
||||
$errorForm.Height = 200
|
||||
$detailsTextBox.Visible = $false
|
||||
$detailsButton.Text = "자세히(&D) >>"
|
||||
}
|
||||
else {
|
||||
$errorForm.Height = 360
|
||||
$detailsTextBox.Visible = $true
|
||||
$detailsButton.Text = "<< 간단히(&L)"
|
||||
}
|
||||
})
|
||||
|
||||
$errorForm.Controls.AddRange(@($iconLabel, $msgLabel, $okButton, $detailsButton, $detailsTextBox)) | Out-Null
|
||||
$errorForm.AcceptButton = $okButton
|
||||
$errorForm.ShowDialog($script:mainForm) | Out-Null
|
||||
$errorForm.Dispose()
|
||||
}
|
||||
|
||||
# --- 로그 기록 함수 ---
|
||||
# 메시지를 화면(콘솔), UI(RichTextBox), 파일 세 곳에 동시에 기록
|
||||
function Write-Log {
|
||||
Param(
|
||||
[string]$Message,
|
||||
[ValidateSet("INFO", "WARNING", "ERROR", "SUCCESS")]
|
||||
[string]$Level = "INFO"
|
||||
)
|
||||
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||
$logEntry = "$timestamp [$Level]: $Message"
|
||||
|
||||
# 1. 콘솔에 출력
|
||||
Write-Host $logEntry
|
||||
|
||||
# 2. UI의 RichTextBox에 출력
|
||||
if ($script:richTextBoxLog -and $script:richTextBoxLog.IsHandleCreated -and -not $script:richTextBoxLog.IsDisposed) {
|
||||
$script:richTextBoxLog.AppendText("$logEntry`r`n")
|
||||
$script:richTextBoxLog.ScrollToCaret()
|
||||
[System.Windows.Forms.Application]::DoEvents()
|
||||
}
|
||||
|
||||
# 3. 로그 파일에 기록
|
||||
try {
|
||||
Add-Content -Path $script:logFilePath -Value $logEntry -Encoding UTF8
|
||||
}
|
||||
catch {
|
||||
Write-Warning "로그 파일($script:logFilePath) 쓰기 실패: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
# --- 동기식 작업 실행 함수 ---
|
||||
# 시간이 걸리는 작업을 실행하는 동안 UI가 멈추지 않도록 처리하고, 작업 상태를 표시
|
||||
function Invoke-Synchronous {
|
||||
Param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ScriptBlock]$ScriptBlock,
|
||||
[System.Object]$TriggerControl,
|
||||
[string]$StatusMessage = "작업 중...",
|
||||
[switch]$RequiresAzureAD
|
||||
)
|
||||
# Azure AD 연결이 필요한 작업인지 확인
|
||||
if ($RequiresAzureAD -and -not (Get-MgContext)) {
|
||||
Write-Log "Azure AD 연결이 필요합니다. 연결을 시도합니다." -Level "WARNING"
|
||||
if (-not (Connect-AzureAD-WithInteraction)) {
|
||||
Write-Log "Azure AD 연결이 취소되었거나 실패하여 작업을 중단합니다." -Level "ERROR"
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
# 작업 시작 전 UI 상태 설정
|
||||
if ($TriggerControl) {
|
||||
$TriggerControl.Enabled = $false
|
||||
}
|
||||
$originalCursor = $script:mainForm.Cursor
|
||||
$script:mainForm.Cursor = [System.Windows.Forms.Cursors]::WaitCursor
|
||||
$script:statusLabelJob.Text = $StatusMessage
|
||||
Write-Log $StatusMessage
|
||||
[System.Windows.Forms.Application]::DoEvents()
|
||||
|
||||
try {
|
||||
# 전달받은 스크립트 블록 실행
|
||||
return (& $ScriptBlock)
|
||||
}
|
||||
catch {
|
||||
# 오류 발생 시 로그 기록 및 상세 오류 대화상자 표시
|
||||
Write-Log "작업 실패: $($_.Exception.Message)" -Level "ERROR"
|
||||
Show-DetailedErrorDialog -ErrorRecord $_
|
||||
return $null
|
||||
}
|
||||
finally {
|
||||
# 작업 완료 후 UI 상태 복원
|
||||
if ($TriggerControl -and $TriggerControl.IsHandleCreated) {
|
||||
$TriggerControl.Enabled = $true
|
||||
}
|
||||
if ($script:mainForm.IsHandleCreated) {
|
||||
$script:mainForm.Cursor = $originalCursor
|
||||
}
|
||||
$script:statusLabelJob.Text = ""
|
||||
}
|
||||
}
|
||||
|
||||
# --- SamAccountName 유효성 검사 함수 ---
|
||||
# 계정명의 유효성(형식, 길이)과 중복 여부(On-Prem AD, Azure AD)를 검사
|
||||
function Test-SamAccountName {
|
||||
param(
|
||||
[string]$AccountName,
|
||||
[switch]$CheckAzureAD
|
||||
)
|
||||
if ([string]::IsNullOrWhiteSpace($AccountName)) {
|
||||
return @{ IsValid = $false; Reason = "계정명은 비워둘 수 없습니다." }
|
||||
}
|
||||
if ($AccountName -notmatch '^[a-z0-9]+$') {
|
||||
return @{ IsValid = $false; Reason = "계정명은 소문자 영문과 숫자만 사용할 수 있습니다." }
|
||||
}
|
||||
if ($AccountName.Length -lt 3) {
|
||||
return @{ IsValid = $false; Reason = "계정명은 3자 이상이어야 합니다." }
|
||||
}
|
||||
if ($AccountName.Length -gt 20) {
|
||||
return @{ IsValid = $false; Reason = "계정명은 20자를 초과할 수 없습니다." }
|
||||
}
|
||||
|
||||
# 1. On-Prem AD 중복 확인
|
||||
try {
|
||||
if (Get-ADUser -Filter "SamAccountName -eq '$AccountName'" -Server $script:Configuration.OnPremDomainController) {
|
||||
return @{ IsValid = $false; Reason = "이미 사용 중인 계정명입니다 (On-Prem AD)." }
|
||||
}
|
||||
}
|
||||
catch {
|
||||
# DC 연결 실패 등 예외 발생 시, 검사를 통과시키되 경고성 메시지를 반환
|
||||
return @{ IsValid = $true; Reason = "On-Prem AD 유효성 검사 중 오류 발생 (DC 확인 불가)" }
|
||||
}
|
||||
|
||||
# 2. Azure AD 중복 확인 (스위치가 제공된 경우)
|
||||
if ($CheckAzureAD) {
|
||||
if (-not (Get-MgContext)) {
|
||||
return @{ IsValid = $true; Reason = "Azure AD 연결이 안 되어 UPN 중복 확인을 건너뜁니다." }
|
||||
}
|
||||
$upn = "$($AccountName)@$($script:Configuration.UPNSuffix)"
|
||||
try {
|
||||
$azureUser = Get-MgUser -Filter "userPrincipalName eq '$upn'" -ErrorAction Stop
|
||||
if ($azureUser) {
|
||||
return @{ IsValid = $false; Reason = "Azure AD에 동일한 UPN의 계정이 이미 존재합니다. 하드 매칭을 사용하세요." }
|
||||
}
|
||||
}
|
||||
catch {
|
||||
return @{ IsValid = $true; Reason = "Azure AD UPN 확인 중 오류 발생." }
|
||||
}
|
||||
}
|
||||
|
||||
return @{ IsValid = $true; Reason = "사용 가능한 계정명입니다." }
|
||||
}
|
||||
|
||||
# --- 상태 표시줄 아이콘 생성 함수 ---
|
||||
# 상태 표시줄에 사용할 동그란 색상 아이콘을 생성
|
||||
function New-StatusIcon {
|
||||
Param([System.Drawing.Color]$Color)
|
||||
$bmp = New-Object System.Drawing.Bitmap(16, 16)
|
||||
$graphics = [System.Drawing.Graphics]::FromImage($bmp)
|
||||
$graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
|
||||
$brush = New-Object System.Drawing.SolidBrush($Color)
|
||||
$graphics.FillEllipse($brush, 2, 2, 12, 12)
|
||||
$graphics.Dispose()
|
||||
$brush.Dispose()
|
||||
return $bmp
|
||||
}
|
||||
|
||||
# --- 설정 대화상자 표시 함수 ---
|
||||
# 프로그램 시작 시 사용자에게 주요 설정 값을 확인하고 수정할 수 있는 대화상자를 표시
|
||||
function Show-ConfigurationDialog {
|
||||
$form = New-Object System.Windows.Forms.Form
|
||||
$form.Text = "스크립트 설정 확인"
|
||||
$form.Size = New-Object System.Drawing.Size(550, 250)
|
||||
$form.StartPosition = "CenterParent"
|
||||
$form.FormBorderStyle = "FixedDialog"
|
||||
$form.MaximizeBox = $false
|
||||
$form.MinimizeBox = $false
|
||||
|
||||
$controls = @{}
|
||||
$yPos = 20
|
||||
$labels = @{
|
||||
OnPremDomainController = "온프레미스 DC:"
|
||||
AADConnectServerName = "AAD Connect 서버:"
|
||||
AzureTenantId = "Azure AD 테넌트 ID:"
|
||||
UPNSuffix = "UPN 접미사:"
|
||||
SynchronizedOUs = "동기화 OU (세미콜론; 구분):"
|
||||
}
|
||||
|
||||
foreach ($key in $labels.Keys) {
|
||||
$lbl = New-Object System.Windows.Forms.Label
|
||||
$lbl.Text = $labels[$key]
|
||||
$lbl.Location = New-Object System.Drawing.Point(20, $yPos)
|
||||
$lbl.Size = New-Object System.Drawing.Size(200, 20)
|
||||
$form.Controls.Add($lbl) | Out-Null
|
||||
|
||||
$txt = New-Object System.Windows.Forms.TextBox
|
||||
$txt.Location = New-Object System.Drawing.Point(230, $yPos)
|
||||
$txt.Size = New-Object System.Drawing.Size(280, 20)
|
||||
if ($script:Configuration -and $script:Configuration.PSObject.Properties.Name -contains $key) {
|
||||
$txt.Text = $script:Configuration.$key
|
||||
}
|
||||
$form.Controls.Add($txt) | Out-Null
|
||||
$controls[$key] = $txt
|
||||
$yPos += 30
|
||||
}
|
||||
|
||||
$ok = New-Object System.Windows.Forms.Button
|
||||
$ok.Text = "시작"
|
||||
$ok.DialogResult = "OK"
|
||||
$ok.Location = New-Object System.Drawing.Point(170, ($yPos + 10))
|
||||
$ok.Size = New-Object System.Drawing.Size(90, 30)
|
||||
$form.AcceptButton = $ok
|
||||
$form.Controls.Add($ok) | Out-Null
|
||||
|
||||
$cancel = New-Object System.Windows.Forms.Button
|
||||
$cancel.Text = "취소"
|
||||
$cancel.DialogResult = "Cancel"
|
||||
$cancel.Location = New-Object System.Drawing.Point(270, ($yPos + 10))
|
||||
$cancel.Size = New-Object System.Drawing.Size(90, 30)
|
||||
$form.CancelButton = $cancel
|
||||
$form.Controls.Add($cancel) | Out-Null
|
||||
|
||||
$res = $form.ShowDialog($script:mainForm)
|
||||
|
||||
if ($res -eq [System.Windows.Forms.DialogResult]::OK) {
|
||||
foreach ($key in $labels.Keys) {
|
||||
# AAD Connect 서버 이름은 필수가 아님
|
||||
if ([string]::IsNullOrWhiteSpace($controls[$key].Text) -and $key -ne 'AADConnectServerName') {
|
||||
[System.Windows.Forms.MessageBox]::Show("'$($labels[$key])' 값은 비워둘 수 없습니다.", "입력 오류", "OK", "Warning") | Out-Null
|
||||
return $false
|
||||
}
|
||||
$script:Configuration.$key = $controls[$key].Text.Trim()
|
||||
}
|
||||
try {
|
||||
$script:Configuration | ConvertTo-Json -Depth 5 | Set-Content -Path $script:configFilePath -Encoding UTF8
|
||||
Write-Log "설정 정보 저장 완료."
|
||||
}
|
||||
catch {
|
||||
Show-DetailedErrorDialog -ErrorRecord $_ -Message "설정 파일 저장 오류"
|
||||
}
|
||||
return $true
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
# --- 스크립트 환경 초기화 함수 ---
|
||||
# On-Premise AD에 연결하여 도메인 정보를 가져오고, 설정된 동기화 OU가 유효한지 확인
|
||||
function Initialize-ScriptEnvironment {
|
||||
Write-Log "스크립트 환경 초기화 시작."
|
||||
try {
|
||||
$script:CurrentADDomainInfo = Get-ADDomain -Server $script:Configuration.OnPremDomainController -ErrorAction Stop
|
||||
if ($script:CurrentADDomainInfo) {
|
||||
$script:CurrentADDomainDN = $script:CurrentADDomainInfo.DistinguishedName
|
||||
Write-Log "온프레미스 AD 도메인 정보 로드 성공: $($script:CurrentADDomainInfo.Name)"
|
||||
$script:statusLabelOnPrem.Image = $script:iconGreen
|
||||
$script:statusLabelOnPrem.Text = "On-Prem DC: $($script:Configuration.OnPremDomainController)"
|
||||
|
||||
# 설정에 있는 동기화 OU 목록을 배열로 변환
|
||||
$ouNames = $script:Configuration.SynchronizedOUs -split ';' | ForEach-Object { $_.Trim() }
|
||||
$script:SynchronizedOURoots = @()
|
||||
|
||||
# 각 OU 이름에 대해 AD에서 실제 DistinguishedName을 찾아 저장
|
||||
foreach ($ouName in $ouNames) {
|
||||
try {
|
||||
$ouObject = Get-ADOrganizationalUnit -Filter "Name -eq '$ouName'" -SearchBase $script:CurrentADDomainDN -SearchScope Subtree -Server $script:Configuration.OnPremDomainController -ErrorAction Stop
|
||||
if ($ouObject) {
|
||||
$ouObject | ForEach-Object {
|
||||
$script:SynchronizedOURoots += $_.DistinguishedName
|
||||
Write-Log "동기화 OU 루트 추가: $($_.DistinguishedName)"
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Log "설정에 명시된 OU '$ouName'을(를) AD에서 찾을 수 없습니다." -Level WARNING
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Log "설정에 명시된 OU '$ouName'을(를) 찾는 중 오류 발생: $($_.Exception.Message)" -Level WARNING
|
||||
}
|
||||
}
|
||||
return $true
|
||||
}
|
||||
else {
|
||||
throw "Get-ADDomain cmdlet이 도메인 정보를 반환하지 않았습니다."
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$msg = "치명적 오류: 온프레미스 AD 도메인 정보를 가져올 수 없습니다."
|
||||
Write-Log $msg -Level "ERROR"
|
||||
$script:statusLabelOnPrem.Image = $script:iconRed
|
||||
$script:statusLabelOnPrem.Text = "On-Prem DC: 연결 오류"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
# --- Azure AD 자동 연결 함수 ---
|
||||
# 캐시된 자격 증명을 사용하여 사용자 개입 없이 Azure AD(Microsoft Graph)에 연결 시도
|
||||
function Connect-AzureAD-Silently {
|
||||
Write-Log "Azure AD 자동 연결을 시도합니다..."
|
||||
$context = try {
|
||||
Connect-MgGraph -TenantId $script:Configuration.AzureTenantId
|
||||
Get-MgContext
|
||||
}
|
||||
catch {
|
||||
$null
|
||||
}
|
||||
|
||||
if ($context) {
|
||||
Write-Log "Azure AD 자동 연결 성공."
|
||||
$script:statusLabelAzure.Image = $script:iconGreen
|
||||
$script:statusLabelAzure.Text = "Azure AD: 연결됨 ($($context.Account))"
|
||||
Update-LicenseList
|
||||
}
|
||||
else {
|
||||
Write-Log "캐시된 토큰이 없거나 만료되었습니다. 수동 연결이 필요합니다." -Level "WARNING"
|
||||
$script:statusLabelAzure.Image = $script:iconRed
|
||||
$script:statusLabelAzure.Text = "Azure AD: 수동 연결 필요"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Azure AD 수동 연결 함수 ---
|
||||
# 사용자에게 로그인 창을 표시하여 Azure AD(Microsoft Graph)에 대화형으로 연결(2단계 인증 고려)
|
||||
function Connect-AzureAD-WithInteraction {
|
||||
$context = Invoke-Synchronous -TriggerControl $script:connectAzureADMenu -StatusMessage "Azure AD에 연결하는 중..." -ScriptBlock {
|
||||
$requiredScopes = @("User.Read.All", "User.ReadWrite.All", "Directory.Read.All", "Organization.Read.All")
|
||||
Connect-MgGraph -TenantId $script:Configuration.AzureTenantId -Scopes $requiredScopes
|
||||
return Get-MgContext
|
||||
}
|
||||
|
||||
if ($context) {
|
||||
Write-Log "Azure AD 연결 성공."
|
||||
$script:statusLabelAzure.Image = $script:iconGreen
|
||||
$script:statusLabelAzure.Text = "Azure AD: 연결됨 ($($context.Account))"
|
||||
Update-LicenseList
|
||||
return $true
|
||||
}
|
||||
else {
|
||||
Write-Log "Azure AD 연결 실패 또는 취소됨." -Level "ERROR"
|
||||
$script:statusLabelAzure.Image = $script:iconRed
|
||||
$script:statusLabelAzure.Text = "Azure AD: 연결 오류"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
# --- M365 라이선스 목록 갱신 함수 ---
|
||||
# Azure AD에서 구독 중인 SKU 정보를 가져와 '계정 생성' 탭의 라이선스 목록 업데이트
|
||||
function Update-LicenseList {
|
||||
$licenses = Invoke-Synchronous -TriggerControl $null -StatusMessage "라이선스 목록 조회 중..." -ScriptBlock {
|
||||
Get-MgSubscribedSku -All | Where-Object { $_.CapabilityStatus -eq "Enabled" }
|
||||
} -RequiresAzureAD
|
||||
|
||||
if ($licenses) {
|
||||
$script:add_listBoxLicenses.Items.Clear()
|
||||
|
||||
# 라이선스를 할당하지 않는 옵션을 기본으로 추가
|
||||
$noLicense = [PSCustomObject]@{
|
||||
DisplayName = "(라이선스 할당 안함)"
|
||||
SkuId = $null
|
||||
SkuObject = $null
|
||||
}
|
||||
$script:add_listBoxLicenses.Items.Add($noLicense) | Out-Null
|
||||
|
||||
# 사용 가능한 라이선스를 목록에 추가 (남은 수량 표시)
|
||||
$licenses | Where-Object { -not [string]::IsNullOrWhiteSpace($_.SkuPartNumber) } | Sort-Object SkuPartNumber | ForEach-Object {
|
||||
$skuItem = $_
|
||||
$availableUnits = ($skuItem.PrepaidUnits.Enabled - $skuItem.ConsumedUnits)
|
||||
$licenseDisplayName = "$($skuItem.SkuPartNumber) (남음: $availableUnits)"
|
||||
$licenseObject = [PSCustomObject]@{
|
||||
DisplayName = $licenseDisplayName
|
||||
SkuId = $skuItem.SkuId
|
||||
SkuObject = $skuItem
|
||||
}
|
||||
$script:add_listBoxLicenses.Items.Add($licenseObject) | Out-Null
|
||||
}
|
||||
$script:add_listBoxLicenses.DisplayMember = "DisplayName"
|
||||
Write-Log "[계정 생성] 라이선스 목록 갱신 완료."
|
||||
}
|
||||
}
|
||||
|
||||
# --- AAD Connect 동기화 실행 함수 ---
|
||||
# 로컬 또는 원격 AAD Connect 서버에서 동기화 주기를 시작하는 명령 실행
|
||||
function Invoke-AadConnectSync {
|
||||
Param ([string]$PolicyType = "Delta")
|
||||
|
||||
$syncSuccess = Invoke-Synchronous -TriggerControl $null -StatusMessage "AAD Connect 동기화 실행 중..." -ScriptBlock {
|
||||
$server = $script:Configuration.AADConnectServerName
|
||||
# 서버 이름이 없으면 로컬에서 실행, 있으면 원격으로 실행
|
||||
if ([string]::IsNullOrWhiteSpace($server)) {
|
||||
Import-Module ADSync -ErrorAction SilentlyContinue
|
||||
Start-ADSyncSyncCycle -PolicyType $PolicyType
|
||||
}
|
||||
else {
|
||||
Invoke-Command -ComputerName $server -ScriptBlock {
|
||||
param($p)
|
||||
Import-Module ADSync -ErrorAction SilentlyContinue
|
||||
Start-ADSyncSyncCycle -PolicyType $p
|
||||
} -ArgumentList $PolicyType -ErrorAction Stop
|
||||
}
|
||||
return $true # 성공 시 true 반환
|
||||
}
|
||||
|
||||
if ($syncSuccess) {
|
||||
Write-Log "AAD Connect 동기화 명령 전송 완료. 클라우드 적용까지 시간이 소요될 수 있습니다." -Level SUCCESS
|
||||
}
|
||||
else {
|
||||
Write-Log "AAD Connect 동기화 명령 실행에 실패했습니다." -Level ERROR
|
||||
}
|
||||
}
|
||||
|
||||
# --- OU TreeView 빌드 함수 (재귀) ---
|
||||
# AD의 OU 구조를 바탕으로 UI의 TreeView 컨트롤을 재귀적 구성
|
||||
function Build-OU-TreeView {
|
||||
param (
|
||||
[string]$ParentDN,
|
||||
[System.Windows.Forms.TreeNode]$ParentUiNode,
|
||||
[hashtable]$OUHierarchy
|
||||
)
|
||||
if (-not $OUHierarchy.ContainsKey($ParentDN)) {
|
||||
return
|
||||
}
|
||||
foreach ($ouObject in $OUHierarchy[$ParentDN] | Sort-Object Name) {
|
||||
$uiNode = New-Object System.Windows.Forms.TreeNode($ouObject.Name)
|
||||
$uiNode.Tag = $ouObject.DistinguishedName
|
||||
$ParentUiNode.Nodes.Add($uiNode) | Out-Null
|
||||
# 자식 OU에 대해 재귀 호출
|
||||
Build-OU-TreeView -ParentDN $ouObject.DistinguishedName -ParentUiNode $uiNode -OUHierarchy $OUHierarchy
|
||||
}
|
||||
}
|
||||
|
||||
# --- '계정 생성' 탭 초기화 함수 ---
|
||||
# 계정 생성 탭의 모든 입력 필드를 초기 상태로 되돌리기
|
||||
function Reset-AddUserTab {
|
||||
$script:add_textBoxLastNameKr.Text = ""
|
||||
$script:add_textBoxFirstNameKr.Text = ""
|
||||
$script:add_textBoxAccountNameEn.Text = ""
|
||||
$script:add_textBoxSelectedOU.Text = ""
|
||||
$script:add_checkedListBoxServicePlans.Items.Clear()
|
||||
$script:add_labelOUSyncStatus.Text = "OU를 선택하면 동기화 상태를 안내합니다."
|
||||
$script:add_labelOUSyncStatus.ForeColor = [System.Drawing.Color]::Black
|
||||
$script:add_pictureBoxAccountValidation.Visible = $false
|
||||
if ($script:add_treeViewOUs.Nodes.Count -gt 0 -and $script:add_treeViewOUs.SelectedNode) {
|
||||
$script:add_treeViewOUs.SelectedNode = $null
|
||||
}
|
||||
Update-LicenseList
|
||||
}
|
||||
|
||||
# --- '계정 삭제' 탭 초기화 함수 ---
|
||||
# 계정 삭제 탭의 모든 입력 필드와 목록을 초기 상태로 되돌리기
|
||||
function Reset-DeleteUserTab {
|
||||
$script:del_textBoxSearchUser.Text = ""
|
||||
$script:del_listBoxFoundUsers.Items.Clear()
|
||||
$script:del_textBoxFoundUserDisplay.Text = "표시 이름:"
|
||||
$script:del_textBoxFoundUserUPN.Text = "UPN:"
|
||||
$script:del_deleteQueue.Clear()
|
||||
$script:del_listBoxDeleteQueue.Items.Clear()
|
||||
$script:del_buttonExecuteDelete.Enabled = $false
|
||||
$script:del_buttonAddToDeleteList.Enabled = $false
|
||||
}
|
||||
|
||||
# --- '하드 매칭' 탭 초기화 함수 ---
|
||||
# 하드 매칭 탭의 모든 입력 필드와 상태 표시를 초기 상태로 되돌리기
|
||||
function Reset-HardMatchTab {
|
||||
$script:hm_textBoxSearchAzure.Text = ""
|
||||
$script:hm_listBoxFoundAzure.Items.Clear()
|
||||
$script:hm_textBoxAzureUser.Text = ""
|
||||
$script:hm_textBoxImmutableId.Text = ""
|
||||
$script:hm_labelGuidMatchStatus.Visible = $false
|
||||
$script:hm_textBoxOnPremUser.Text = ""
|
||||
$script:hm_textBoxObjectGuid.Text = ""
|
||||
$script:hm_textBoxConvertedImmutableId.Text = ""
|
||||
if ($script:hm_textBoxOnPremUser.Parent) {
|
||||
$script:hm_textBoxOnPremUser.Parent.Visible = $false
|
||||
}
|
||||
$script:hm_textBoxFirstNameKr.Text = ""
|
||||
$script:hm_textBoxLastNameKr.Text = ""
|
||||
$script:hm_textBoxSelectedOU.Text = ""
|
||||
if ($script:hm_textBoxSelectedOU) {
|
||||
$script:hm_textBoxSelectedOU.BackColor = [System.Drawing.Color]::White
|
||||
}
|
||||
if ($script:hm_treeViewOUs) {
|
||||
$script:hm_treeViewOUs.Nodes.Clear()
|
||||
}
|
||||
if ($script:hm_groupCreate) {
|
||||
$script:hm_groupCreate.Visible = $false
|
||||
}
|
||||
$script:hm_buttonExecuteAction.Enabled = $false
|
||||
$script:hm_buttonExecuteAction.Text = "작업 실행"
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
Write-Host "Common.ps1 로드 완료." -ForegroundColor Cyan
|
||||
490
Scripts/UI-Tab-AddUser.ps1
Normal file
490
Scripts/UI-Tab-AddUser.ps1
Normal file
@ -0,0 +1,490 @@
|
||||
# ================================================================================
|
||||
# 파일: Scripts/UI-Tab-AddUser.ps1
|
||||
# 역할: '계정 생성' 탭 UI 및 기능 구현
|
||||
#
|
||||
# 작성자: 양범진
|
||||
# 버전: 1.13
|
||||
# 생성일자: 2025-06-05
|
||||
# 최종 수정일자: 2025-06-12
|
||||
#
|
||||
# 설명:
|
||||
# - '계정 생성' 탭의 UI 컨트롤들을 생성하고 배치
|
||||
# - 사용자 정보 입력, OU 선택, 라이선스 할당 등 계정 생성과 관련된 모든 로직 처리
|
||||
# - 코드 가독성 및 유지보수 편의성을 위해 전체적으로 포매팅 및 주석 재정리
|
||||
# ================================================================================
|
||||
|
||||
#region '계정 생성' 탭 UI 초기화 함수
|
||||
|
||||
# --- '계정 생성' 탭의 모든 UI 컨트롤을 생성하고 배치하는 메인 함수 ---
|
||||
function Initialize-AddUserTab {
|
||||
Param([System.Windows.Forms.TabPage]$parentTab)
|
||||
|
||||
# 메인 레이아웃: 전체 탭을 2열 3행으로 구성
|
||||
$addTabMainLayout = New-Object System.Windows.Forms.TableLayoutPanel
|
||||
$addTabMainLayout.Dock = "Fill"
|
||||
$addTabMainLayout.Padding = [System.Windows.Forms.Padding](10)
|
||||
$addTabMainLayout.ColumnCount = 2
|
||||
$addTabMainLayout.RowCount = 3
|
||||
$addTabMainLayout.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Percent, 50))) | Out-Null
|
||||
$addTabMainLayout.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Percent, 50))) | Out-Null
|
||||
$addTabMainLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 80))) | Out-Null
|
||||
$addTabMainLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Percent, 100))) | Out-Null
|
||||
$addTabMainLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 60))) | Out-Null
|
||||
$parentTab.Controls.Add($addTabMainLayout) | Out-Null
|
||||
|
||||
# 상단: 작업 모드 선택 (AD/Azure AD 동기화 vs On-Premise AD만 생성)
|
||||
$groupMode = New-Object System.Windows.Forms.GroupBox
|
||||
$groupMode.Text = "작업 모드 선택"
|
||||
$groupMode.Dock = "Fill"
|
||||
$addTabMainLayout.Controls.Add($groupMode, 0, 0) | Out-Null
|
||||
$addTabMainLayout.SetColumnSpan($groupMode, 2)
|
||||
|
||||
$script:add_radioSync = New-Object System.Windows.Forms.RadioButton
|
||||
$script:add_radioSync.Text = "AD/Azure AD 동기화 및 라이선스 할당"
|
||||
$script:add_radioSync.Location = New-Object System.Drawing.Point(25, 30)
|
||||
$script:add_radioSync.AutoSize = $true
|
||||
$script:add_radioSync.Checked = $true
|
||||
$groupMode.Controls.Add($script:add_radioSync) | Out-Null
|
||||
|
||||
$script:add_radioOnPremOnly = New-Object System.Windows.Forms.RadioButton
|
||||
$script:add_radioOnPremOnly.Text = "온프레미스 AD에만 생성"
|
||||
$script:add_radioOnPremOnly.Location = New-Object System.Drawing.Point(450, 30)
|
||||
$script:add_radioOnPremOnly.AutoSize = $true
|
||||
$groupMode.Controls.Add($script:add_radioOnPremOnly) | Out-Null
|
||||
|
||||
# 왼쪽 패널: 사용자 정보 입력 및 OU 선택 영역
|
||||
$leftPanelLayout = New-Object System.Windows.Forms.TableLayoutPanel
|
||||
$leftPanelLayout.Dock = "Fill"
|
||||
$leftPanelLayout.RowCount = 2
|
||||
$leftPanelLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 180))) | Out-Null
|
||||
$leftPanelLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Percent, 100))) | Out-Null
|
||||
$addTabMainLayout.Controls.Add($leftPanelLayout, 0, 1) | Out-Null
|
||||
|
||||
# 왼쪽 패널 (상단): 사용자 정보 입력 그룹박스
|
||||
$groupUserInfo = New-Object System.Windows.Forms.GroupBox
|
||||
$groupUserInfo.Text = "1. 사용자 정보"
|
||||
$groupUserInfo.Dock = "Fill"
|
||||
$leftPanelLayout.Controls.Add($groupUserInfo, 0, 0) | Out-Null
|
||||
|
||||
$userInfoLayout = New-Object System.Windows.Forms.TableLayoutPanel
|
||||
$userInfoLayout.Dock = "Fill"
|
||||
$userInfoLayout.Padding = [System.Windows.Forms.Padding](10)
|
||||
$userInfoLayout.ColumnCount = 2
|
||||
$userInfoLayout.RowCount = 4
|
||||
$userInfoLayout.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Absolute, 110))) | Out-Null
|
||||
$userInfoLayout.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Percent, 100))) | Out-Null
|
||||
$rowHeight = 35
|
||||
$userInfoLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, $rowHeight))) | Out-Null
|
||||
$userInfoLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, $rowHeight))) | Out-Null
|
||||
$userInfoLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, $rowHeight))) | Out-Null
|
||||
$userInfoLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, $rowHeight))) | Out-Null
|
||||
$groupUserInfo.Controls.Add($userInfoLayout) | Out-Null
|
||||
|
||||
# 사용자 정보 입력 컨트롤들 (한글 성/이름, 영문 계정명, 비밀번호)
|
||||
$labelLastNameKr = New-Object System.Windows.Forms.Label
|
||||
$labelLastNameKr.Text = "한글 성:"
|
||||
$labelLastNameKr.Dock = "Fill"
|
||||
$labelLastNameKr.TextAlign = "MiddleLeft"
|
||||
|
||||
$script:add_textBoxLastNameKr = New-Object System.Windows.Forms.TextBox
|
||||
$script:add_textBoxLastNameKr.Dock = "Fill"
|
||||
|
||||
$labelFirstNameKr = New-Object System.Windows.Forms.Label
|
||||
$labelFirstNameKr.Text = "한글 이름:"
|
||||
$labelFirstNameKr.Dock = "Fill"
|
||||
$labelFirstNameKr.TextAlign = "MiddleLeft"
|
||||
|
||||
$script:add_textBoxFirstNameKr = New-Object System.Windows.Forms.TextBox
|
||||
$script:add_textBoxFirstNameKr.Dock = "Fill"
|
||||
|
||||
$labelAccountNameEn = New-Object System.Windows.Forms.Label
|
||||
$labelAccountNameEn.Text = "영문 계정명:"
|
||||
$labelAccountNameEn.Dock = "Fill"
|
||||
$labelAccountNameEn.TextAlign = "MiddleLeft"
|
||||
|
||||
# 영문 계정명 입력란 + 유효성 검사 아이콘 패널
|
||||
$accountNamePanel = New-Object System.Windows.Forms.TableLayoutPanel
|
||||
$accountNamePanel.Dock = "Fill"
|
||||
$accountNamePanel.ColumnCount = 2
|
||||
$accountNamePanel.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Percent, 100))) | Out-Null
|
||||
$accountNamePanel.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Absolute, 22))) | Out-Null
|
||||
$accountNamePanel.Margin = [System.Windows.Forms.Padding](0)
|
||||
|
||||
$script:add_textBoxAccountNameEn = New-Object System.Windows.Forms.TextBox
|
||||
$script:add_textBoxAccountNameEn.Dock = "Fill"
|
||||
|
||||
$script:add_pictureBoxAccountValidation = New-Object System.Windows.Forms.PictureBox
|
||||
$script:add_pictureBoxAccountValidation.Dock = "Fill"
|
||||
$script:add_pictureBoxAccountValidation.SizeMode = "CenterImage"
|
||||
$script:add_pictureBoxAccountValidation.Visible = $false
|
||||
|
||||
$script:add_toolTip = New-Object System.Windows.Forms.ToolTip
|
||||
|
||||
$accountNamePanel.Controls.AddRange(@($script:add_textBoxAccountNameEn, $script:add_pictureBoxAccountValidation)) | Out-Null
|
||||
|
||||
$labelPassword = New-Object System.Windows.Forms.Label
|
||||
$labelPassword.Text = "초기 비밀번호:"
|
||||
$labelPassword.Dock = "Fill"
|
||||
$labelPassword.TextAlign = "MiddleLeft"
|
||||
|
||||
$script:add_textBoxPassword = New-Object System.Windows.Forms.TextBox
|
||||
$script:add_textBoxPassword.Dock = "Fill"
|
||||
$script:add_textBoxPassword.Text = $script:Configuration.DefaultPassword
|
||||
|
||||
$userInfoLayout.Controls.Add($labelLastNameKr, 0, 0) | Out-Null
|
||||
$userInfoLayout.Controls.Add($script:add_textBoxLastNameKr, 1, 0) | Out-Null
|
||||
$userInfoLayout.Controls.Add($labelFirstNameKr, 0, 1) | Out-Null
|
||||
$userInfoLayout.Controls.Add($script:add_textBoxFirstNameKr, 1, 1) | Out-Null
|
||||
$userInfoLayout.Controls.Add($labelAccountNameEn, 0, 2) | Out-Null
|
||||
$userInfoLayout.Controls.Add($accountNamePanel, 1, 2) | Out-Null
|
||||
$userInfoLayout.Controls.Add($labelPassword, 0, 3) | Out-Null
|
||||
$userInfoLayout.Controls.Add($script:add_textBoxPassword, 1, 3) | Out-Null
|
||||
|
||||
# 왼쪽 패널 (하단): OU 선택 그룹박스
|
||||
$groupOU = New-Object System.Windows.Forms.GroupBox
|
||||
$groupOU.Text = "2. 대상 OU 선택"
|
||||
$groupOU.Dock = "Fill"
|
||||
$leftPanelLayout.Controls.Add($groupOU, 0, 1) | Out-Null
|
||||
|
||||
$ouLayout = New-Object System.Windows.Forms.TableLayoutPanel
|
||||
$ouLayout.Dock = "Fill"
|
||||
$ouLayout.Padding = [System.Windows.Forms.Padding](10)
|
||||
$ouLayout.ColumnCount = 2
|
||||
$ouLayout.RowCount = 3
|
||||
$ouLayout.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Percent, 100))) | Out-Null
|
||||
$ouLayout.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Absolute, 40))) | Out-Null
|
||||
$ouLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 35))) | Out-Null
|
||||
$ouLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Percent, 100))) | Out-Null
|
||||
$ouLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 25))) | Out-Null
|
||||
$groupOU.Controls.Add($ouLayout) | Out-Null
|
||||
|
||||
# OU 선택 컨트롤들 (선택된 OU 표시, 새로고침 버튼, TreeView, 동기화 상태 라벨)
|
||||
$script:add_textBoxSelectedOU = New-Object System.Windows.Forms.TextBox
|
||||
$script:add_textBoxSelectedOU.Dock = "Fill"
|
||||
$script:add_textBoxSelectedOU.ReadOnly = $true
|
||||
$script:add_textBoxSelectedOU.Text = "아래에서 OU를 선택하세요."
|
||||
|
||||
$buttonRefreshOUs = New-Object System.Windows.Forms.Button
|
||||
$buttonRefreshOUs.Text = "🔄"
|
||||
$buttonRefreshOUs.Dock = "Fill"
|
||||
$buttonRefreshOUs.Font = New-Object System.Drawing.Font("Segoe UI Emoji", 10)
|
||||
|
||||
$script:add_treeViewOUs = New-Object System.Windows.Forms.TreeView
|
||||
$script:add_treeViewOUs.Dock = "Fill"
|
||||
$script:add_treeViewOUs.HideSelection = $false
|
||||
|
||||
$script:add_labelOUSyncStatus = New-Object System.Windows.Forms.Label
|
||||
$script:add_labelOUSyncStatus.Dock = "Fill"
|
||||
$script:add_labelOUSyncStatus.TextAlign = "MiddleLeft"
|
||||
$script:add_labelOUSyncStatus.Text = "OU를 선택하면 동기화 상태를 안내합니다."
|
||||
|
||||
$ouLayout.Controls.Add($script:add_textBoxSelectedOU, 0, 0) | Out-Null
|
||||
$ouLayout.Controls.Add($buttonRefreshOUs, 1, 0) | Out-Null
|
||||
$ouLayout.Controls.Add($script:add_treeViewOUs, 0, 1) | Out-Null
|
||||
$ouLayout.SetColumnSpan($script:add_treeViewOUs, 2)
|
||||
$ouLayout.Controls.Add($script:add_labelOUSyncStatus, 0, 2) | Out-Null
|
||||
$ouLayout.SetColumnSpan($script:add_labelOUSyncStatus, 2)
|
||||
|
||||
# 오른쪽 패널: M365 라이선스 및 서비스 플랜 할당 영역
|
||||
$script:add_groupLicense = New-Object System.Windows.Forms.GroupBox
|
||||
$script:add_groupLicense.Text = "3. M365 라이선스 및 서비스 플랜 할당"
|
||||
$script:add_groupLicense.Dock = "Fill"
|
||||
$addTabMainLayout.Controls.Add($script:add_groupLicense, 1, 1) | Out-Null
|
||||
|
||||
$licenseLayout = New-Object System.Windows.Forms.TableLayoutPanel
|
||||
$licenseLayout.Dock = "Fill"
|
||||
$licenseLayout.Padding = [System.Windows.Forms.Padding](10)
|
||||
$licenseLayout.RowCount = 2
|
||||
$licenseLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Percent, 50))) | Out-Null
|
||||
$licenseLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Percent, 50))) | Out-Null
|
||||
$script:add_groupLicense.Controls.Add($licenseLayout) | Out-Null
|
||||
|
||||
# 라이선스 목록 및 서비스 플랜 목록 컨트롤
|
||||
$script:add_listBoxLicenses = New-Object System.Windows.Forms.ListBox
|
||||
$script:add_listBoxLicenses.Dock = "Fill"
|
||||
|
||||
$script:add_checkedListBoxServicePlans = New-Object System.Windows.Forms.CheckedListBox
|
||||
$script:add_checkedListBoxServicePlans.Dock = "Fill"
|
||||
$script:add_checkedListBoxServicePlans.CheckOnClick = $true
|
||||
|
||||
$licenseLayout.Controls.Add($script:add_listBoxLicenses, 0, 0) | Out-Null
|
||||
$licenseLayout.Controls.Add($script:add_checkedListBoxServicePlans, 0, 1) | Out-Null
|
||||
|
||||
# 하단: 계정 생성 실행 버튼
|
||||
$script:add_buttonCreate = New-Object System.Windows.Forms.Button
|
||||
$script:add_buttonCreate.Text = "계정 생성 실행(&R)"
|
||||
$script:add_buttonCreate.Dock = "Fill"
|
||||
$script:add_buttonCreate.Font = New-Object System.Drawing.Font("Segoe UI", 12, [System.Drawing.FontStyle]::Bold)
|
||||
$addTabMainLayout.Controls.Add($script:add_buttonCreate, 0, 2) | Out-Null
|
||||
$addTabMainLayout.SetColumnSpan($script:add_buttonCreate, 2)
|
||||
|
||||
# 이벤트 핸들러 연결
|
||||
$script:add_radioSync.add_CheckedChanged($add_ModeChangedHandler)
|
||||
$script:add_radioOnPremOnly.add_CheckedChanged($add_ModeChangedHandler)
|
||||
$script:add_textBoxAccountNameEn.Add_TextChanged($add_AccountNameEn_TextChanged)
|
||||
$script:add_textBoxAccountNameEn.Add_Leave($add_AccountNameEn_Leave)
|
||||
$buttonRefreshOUs.Add_Click({ Update-OU-TreeView -TriggerControl $buttonRefreshOUs })
|
||||
$script:add_treeViewOUs.Add_AfterSelect($add_treeViewOUs_AfterSelect)
|
||||
$script:add_listBoxLicenses.Add_SelectedIndexChanged($add_listBoxLicenses_SelectedIndexChanged)
|
||||
$script:add_buttonCreate.Add_Click($add_buttonCreate_Click)
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region '계정 생성' 탭 이벤트 핸들러 및 함수
|
||||
|
||||
# --- OU TreeView를 로드 및 갱신하는 함수 ---
|
||||
function Update-OU-TreeView {
|
||||
Param($TriggerControl)
|
||||
|
||||
$allOUs = Invoke-Synchronous -TriggerControl $TriggerControl -StatusMessage "[계정 생성] OU 목록을 조회합니다..." -ScriptBlock {
|
||||
Get-ADOrganizationalUnit -Filter * -SearchBase $script:CurrentADDomainDN -SearchScope Subtree -Server $script:Configuration.OnPremDomainController -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
if ($allOUs) {
|
||||
# OU 목록을 부모-자식 관계의 해시테이블로 재구성
|
||||
$ouHierarchy = @{}
|
||||
foreach ($ou in $allOUs) {
|
||||
$parentDN = $ou.DistinguishedName -replace '^OU=[^,]+,', ''
|
||||
if (-not $ouHierarchy.ContainsKey($parentDN)) {
|
||||
$ouHierarchy[$parentDN] = [System.Collections.ArrayList]::new()
|
||||
}
|
||||
$ouHierarchy[$parentDN].Add($ou) | Out-Null
|
||||
}
|
||||
|
||||
# TreeView 업데이트 시작 (UI 깜빡임 방지)
|
||||
$script:add_treeViewOUs.BeginUpdate()
|
||||
$script:add_treeViewOUs.Nodes.Clear()
|
||||
|
||||
# 최상위 루트 노드 추가
|
||||
$rootNode = New-Object System.Windows.Forms.TreeNode($script:CurrentADDomainInfo.DnsRoot)
|
||||
$rootNode.Tag = $script:CurrentADDomainDN
|
||||
$script:add_treeViewOUs.Nodes.Add($rootNode) | Out-Null
|
||||
|
||||
# 재귀 함수를 통해 OU 계층 구조 빌드
|
||||
Build-OU-TreeView -ParentDN $script:CurrentADDomainDN -ParentUiNode $rootNode -OUHierarchy $ouHierarchy
|
||||
|
||||
$rootNode.Expand()
|
||||
|
||||
# 로드 완료 후 선택된 노드 해제
|
||||
if ($script:add_treeViewOUs.SelectedNode) {
|
||||
$script:add_treeViewOUs.SelectedNode = $null
|
||||
}
|
||||
$script:add_treeViewOUs.EndUpdate()
|
||||
Write-Log "[계정 생성] OU 목록 로드/갱신 완료."
|
||||
}
|
||||
}
|
||||
|
||||
# --- 작업 모드 변경 이벤트 핸들러 ---
|
||||
# 라디오 버튼 선택에 따라 라이선스 할당 영역의 활성화 여부와 실행 버튼 텍스트 변경
|
||||
$add_ModeChangedHandler = {
|
||||
param($src, $e)
|
||||
if (-not $src.Checked) { return }
|
||||
|
||||
$isSyncMode = $script:add_radioSync.Checked
|
||||
$script:add_groupLicense.Enabled = $isSyncMode
|
||||
|
||||
if ($isSyncMode) {
|
||||
$script:add_buttonCreate.Text = "계정 생성 및 할당 실행(&R)"
|
||||
}
|
||||
else {
|
||||
$script:add_buttonCreate.Text = "온프레미스 계정 생성 실행(&R)"
|
||||
}
|
||||
}
|
||||
|
||||
# --- 영문 계정명 텍스트 변경 이벤트 핸들러 ---
|
||||
# 사용자가 계정명을 입력할 때 유효성 검사 아이콘을 '확인 중' 상태로 표시
|
||||
$add_AccountNameEn_TextChanged = {
|
||||
$accountName = $script:add_textBoxAccountNameEn.Text
|
||||
if ([string]::IsNullOrWhiteSpace($accountName)) {
|
||||
$script:add_pictureBoxAccountValidation.Visible = $false
|
||||
return
|
||||
}
|
||||
$script:add_pictureBoxAccountValidation.Image = $script:iconGreen
|
||||
$script:add_toolTip.SetToolTip($script:add_pictureBoxAccountValidation, "확인 중...")
|
||||
$script:add_pictureBoxAccountValidation.Visible = $true
|
||||
}
|
||||
|
||||
# --- 영문 계정명 입력란 포커스 잃음 이벤트 핸들러 ---
|
||||
# 입력이 완료되면 계정명의 유효성과 중복 여부를 검사하고 결과를 아이콘과 툴팁으로 표시
|
||||
$add_AccountNameEn_Leave = {
|
||||
$accountName = $script:add_textBoxAccountNameEn.Text
|
||||
if ([string]::IsNullOrWhiteSpace($accountName)) {
|
||||
$script:add_pictureBoxAccountValidation.Visible = $false
|
||||
return
|
||||
}
|
||||
|
||||
# Azure AD 동기화 모드일 때만 Azure AD 중복 확인을 수행
|
||||
$isSyncMode = $script:add_radioSync.Checked
|
||||
$validationResult = Test-SamAccountName -AccountName $accountName -CheckAzureAD:$isSyncMode
|
||||
|
||||
if ($validationResult.IsValid) {
|
||||
$script:add_pictureBoxAccountValidation.Image = $script:iconGreen
|
||||
}
|
||||
else {
|
||||
$script:add_pictureBoxAccountValidation.Image = $script:iconRed
|
||||
}
|
||||
$script:add_toolTip.SetToolTip($script:add_pictureBoxAccountValidation, $validationResult.Reason)
|
||||
$script:add_pictureBoxAccountValidation.Visible = $true
|
||||
}
|
||||
|
||||
# --- OU TreeView 노드 선택 이벤트 핸들러 ---
|
||||
# OU를 선택하면 해당 OU의 DN을 텍스트박스에 표시하고, 동기화 대상 여부를 확인하여 상태 라벨에 표시
|
||||
$add_treeViewOUs_AfterSelect = {
|
||||
param($source, $e)
|
||||
if ($e.Node -ne $null -and $e.Node.Tag) {
|
||||
$selectedOUDN = $e.Node.Tag.ToString()
|
||||
# 루트 도메인은 선택 불가 처리
|
||||
if ($selectedOUDN -eq $script:CurrentADDomainDN) {
|
||||
$script:add_textBoxSelectedOU.Text = "OU를 선택해야 합니다."
|
||||
$script:add_labelOUSyncStatus.Text = "루트 도메인은 선택할 수 없습니다."
|
||||
$script:add_labelOUSyncStatus.ForeColor = [System.Drawing.Color]::Red
|
||||
return
|
||||
}
|
||||
|
||||
$script:add_textBoxSelectedOU.Text = $selectedOUDN
|
||||
$isSynchronized = $false
|
||||
# 설정된 동기화 OU 목록과 비교
|
||||
foreach ($syncRootOU in $script:SynchronizedOURoots) {
|
||||
if ($selectedOUDN -eq $syncRootOU -or $selectedOUDN.EndsWith(",$syncRootOU", "OrdinalIgnoreCase")) {
|
||||
$isSynchronized = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ($isSynchronized) {
|
||||
$script:add_labelOUSyncStatus.Text = "선택된 OU는 동기화 대상입니다."
|
||||
$script:add_labelOUSyncStatus.ForeColor = [System.Drawing.Color]::Green
|
||||
}
|
||||
else {
|
||||
$script:add_labelOUSyncStatus.Text = "주의: 선택된 OU는 동기화 대상이 아닐 수 있습니다!"
|
||||
$script:add_labelOUSyncStatus.ForeColor = [System.Drawing.Color]::Red
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- 라이선스 목록 선택 이벤트 핸들러 ---
|
||||
# 라이선스를 선택하면 해당 라이선스에 포함된 서비스 플랜 목록을 가져와 아래쪽 체크리스트에 표시
|
||||
$add_listBoxLicenses_SelectedIndexChanged = {
|
||||
$script:add_checkedListBoxServicePlans.Items.Clear()
|
||||
$selectedItem = $script:add_listBoxLicenses.SelectedItem
|
||||
|
||||
# "(라이선스 할당 안함)"을 선택했거나 선택된 항목이 없으면 함수 종료
|
||||
if ($null -eq $selectedItem -or $null -eq $selectedItem.SkuId) {
|
||||
return
|
||||
}
|
||||
|
||||
Write-Log "'$($selectedItem.DisplayName)' 라이선스의 서비스 플랜을 조회합니다."
|
||||
$allPlans = $selectedItem.SkuObject.ServicePlans | Sort-Object ServicePlanName
|
||||
foreach ($plan in $allPlans) {
|
||||
# 모든 플랜을 기본적으로 체크된 상태로 추가
|
||||
$script:add_checkedListBoxServicePlans.Items.Add($plan, $true) | Out-Null
|
||||
}
|
||||
$script:add_checkedListBoxServicePlans.DisplayMember = "ServicePlanName"
|
||||
}
|
||||
|
||||
# --- '계정 생성 실행' 버튼 클릭 이벤트 핸들러 ---
|
||||
# 입력된 정보를 바탕으로 실제 계정 생성 작업을 수행하는 메인 로직
|
||||
$add_buttonCreate_Click = {
|
||||
# 1. 필수 값 확인
|
||||
$lastNameKr = $script:add_textBoxLastNameKr.Text.Trim()
|
||||
$firstNameKr = $script:add_textBoxFirstNameKr.Text.Trim()
|
||||
$accountNameEn = $script:add_textBoxAccountNameEn.Text.Trim()
|
||||
$targetOUDN = $script:add_textBoxSelectedOU.Text.Trim()
|
||||
$passwordInput = $script:add_textBoxPassword.Text
|
||||
$isSyncMode = $script:add_radioSync.Checked
|
||||
|
||||
if (-not ($lastNameKr -and $firstNameKr -and $accountNameEn -and $passwordInput)) {
|
||||
[System.Windows.Forms.MessageBox]::Show("사용자 정보(한글 성/이름, 영문 계정명)와 비밀번호는 필수 입력 항목입니다.", "입력 오류", "OK", "Warning") | Out-Null
|
||||
return
|
||||
}
|
||||
if ($targetOUDN -eq "아래에서 OU를 선택하세요." -or $targetOUDN -eq "OU를 선택해야 합니다." -or [string]::IsNullOrWhiteSpace($targetOUDN)) {
|
||||
[System.Windows.Forms.MessageBox]::Show("대상 OU를 선택해야 합니다.", "입력 오류", "OK", "Warning") | Out-Null
|
||||
return
|
||||
}
|
||||
|
||||
# 생성 직전 최종 유효성 검사 (Azure AD 중복 확인 포함)
|
||||
$validationResult = Test-SamAccountName -AccountName $accountNameEn -CheckAzureAD:$isSyncMode
|
||||
if (-not $validationResult.IsValid) {
|
||||
$message = "계정명 유효성 검사를 통과하지 못했습니다.`n사유: $($validationResult.Reason)"
|
||||
[System.Windows.Forms.MessageBox]::Show($message, "입력 오류", "OK", "Warning") | Out-Null
|
||||
return
|
||||
}
|
||||
|
||||
# 2. 메인 작업 실행 (Invoke-Synchronous 래퍼 함수 사용)
|
||||
$success = Invoke-Synchronous -TriggerControl $this -StatusMessage "계정 생성 작업을 실행합니다..." -ScriptBlock {
|
||||
# 2.1. On-Premise AD 사용자 생성
|
||||
$userPrincipalName = "$accountNameEn@$($script:Configuration.UPNSuffix)"
|
||||
$commonName = "$lastNameKr$firstNameKr [$accountNameEn]"
|
||||
Write-Log "온프레미스 AD 사용자 생성을 시작합니다: $accountNameEn"
|
||||
|
||||
New-ADUser -SamAccountName $accountNameEn `
|
||||
-UserPrincipalName $userPrincipalName `
|
||||
-Name $commonName `
|
||||
-DisplayName $commonName `
|
||||
-GivenName $firstNameKr `
|
||||
-Surname $lastNameKr `
|
||||
-Path $targetOUDN `
|
||||
-AccountPassword (ConvertTo-SecureString $passwordInput -AsPlainText -Force) `
|
||||
-Enabled $true `
|
||||
-ChangePasswordAtLogon $false `
|
||||
-EmailAddress $userPrincipalName `
|
||||
-Server $script:Configuration.OnPremDomainController `
|
||||
-PassThru `
|
||||
-ErrorAction Stop
|
||||
|
||||
Write-Log "온프레미스 AD 사용자 '$accountNameEn' 생성 성공."
|
||||
|
||||
if (-not $isSyncMode) { return $true } # On-Premise 전용 모드일 경우 여기서 작업 종료
|
||||
|
||||
# 2.2. 동기화 실행 및 Azure AD에서 사용자 확인
|
||||
Invoke-AadConnectSync -PolicyType Delta
|
||||
Write-Log "Azure AD에서 사용자 '$userPrincipalName' 확인 시도 (최대 120초 대기)..."
|
||||
$azureAdUser = $null
|
||||
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
|
||||
while ($stopwatch.Elapsed.TotalSeconds -lt 120) {
|
||||
Write-Log "Azure AD에서 사용자 확인 중... ($([math]::Round($stopwatch.Elapsed.TotalSeconds))초 경과)"
|
||||
$azureAdUser = Get-MgUser -Filter "userPrincipalName eq '$userPrincipalName'" -ErrorAction SilentlyContinue
|
||||
if ($azureAdUser) { break }
|
||||
Start-Sleep -Seconds 10
|
||||
}
|
||||
$stopwatch.Stop()
|
||||
if (-not $azureAdUser) { throw "제한 시간(120초) 내에 Azure AD에서 사용자를 찾지 못했습니다." }
|
||||
|
||||
$elapsedSeconds = [math]::Round($stopwatch.Elapsed.TotalSeconds)
|
||||
Write-Log "사용자 '$userPrincipalName'을(를) Azure AD에서 찾았습니다! (소요 시간: ${elapsedSeconds}초)" -Level SUCCESS
|
||||
|
||||
# 2.3. 사용 위치(Usage Location) 및 라이선스 할당
|
||||
Write-Log "사용 위치 설정 중: $($script:Configuration.DefaultUsageLocation)"
|
||||
Update-MgUser -UserId $azureAdUser.UserPrincipalName -UsageLocation $script:Configuration.DefaultUsageLocation -ErrorAction Stop
|
||||
|
||||
$selectedLicenseItem = $script:add_listBoxLicenses.SelectedItem
|
||||
if ($selectedLicenseItem -and $selectedLicenseItem.SkuId) {
|
||||
Write-Log "라이선스 할당 중: $($selectedLicenseItem.DisplayName)"
|
||||
$disabledPlans = [System.Collections.Generic.List[string]]::new()
|
||||
# 체크 해제된 서비스 플랜 목록을 구성
|
||||
for ($i = 0; $i -lt $script:add_checkedListBoxServicePlans.Items.Count; $i++) {
|
||||
if (-not $script:add_checkedListBoxServicePlans.GetItemChecked($i)) {
|
||||
$servicePlanId = $script:add_checkedListBoxServicePlans.Items[$i].ServicePlanId
|
||||
$disabledPlans.Add($servicePlanId)
|
||||
}
|
||||
}
|
||||
# 라이선스 할당 API 호출
|
||||
$licenseBody = @{
|
||||
AddLicenses = @(@{SkuId = $selectedLicenseItem.SkuId; DisabledPlans = $disabledPlans})
|
||||
RemoveLicenses = @()
|
||||
}
|
||||
Set-MgUserLicense -UserId $azureAdUser.UserPrincipalName -BodyParameter $licenseBody -ErrorAction Stop
|
||||
Write-Log "라이선스 및 서비스 플랜 할당 완료."
|
||||
}
|
||||
return $true
|
||||
} -RequiresAzureAD:$isSyncMode
|
||||
|
||||
# 3. 최종 결과 처리
|
||||
if ($success) {
|
||||
[System.Windows.Forms.MessageBox]::Show("모든 작업이 성공적으로 완료되었습니다.", "작업 완료", "OK", "Information") | Out-Null
|
||||
Reset-AddUserTab
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
Write-Host "UI-Tab-AddUser.ps1 로드 완료." -ForegroundColor Cyan
|
||||
619
Scripts/UI-Tab-BatchTask.ps1
Normal file
619
Scripts/UI-Tab-BatchTask.ps1
Normal file
@ -0,0 +1,619 @@
|
||||
# ================================================================================
|
||||
# 파일: Scripts/UI-Tab-BatchTask.ps1
|
||||
# 역할: '일괄 작업 (CSV)' 탭 UI 및 기능 구현 (리팩토링 최종 완전판)
|
||||
#
|
||||
# 작성자: 양범진
|
||||
# 버전: 1.13
|
||||
# 생성일자: 2025-06-05
|
||||
# 최종 수정일자: 2025-06-12
|
||||
#
|
||||
# 설명:
|
||||
# - 가독성 향상을 위해 코드 포매팅, 주석 추가, 불필요한 콘솔 출력 제거.
|
||||
# - 누락된 함수 정의를 복원하여 스크립트 실행 오류 최종 해결.
|
||||
# - 모든 기능이 포함된 최종 안정화 버전.
|
||||
# ================================================================================
|
||||
|
||||
#region '일괄 작업' 탭 UI 초기화 및 이벤트 핸들러
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# 헬퍼 함수 정의
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
# 선택된 작업 유형에 따라 안내 메시지를 업데이트하는 함수
|
||||
function Update-BatchGuide {
|
||||
param($taskType)
|
||||
|
||||
# 작업 유형에 따라 다른 안내 텍스트를 설정
|
||||
$guideText = switch ($taskType) {
|
||||
"계정 생성" {
|
||||
@"
|
||||
** CSV로 계정 생성하기 **
|
||||
|
||||
'템플릿 다운로드'로 받은 CSV 파일을 아래 형식에 맞게 작성하세요.
|
||||
|
||||
- **LastNameKr**: 사용자의 한글 성 (예: 홍)
|
||||
- **FirstNameKr**: 사용자의 한글 이름 (예: 길동)
|
||||
- **AccountNameEn**: 로그인 ID (SamAccountName). 아래 규칙을 따라야 합니다.
|
||||
- 소문자 영문과 숫자만 사용 가능
|
||||
- 3자 이상, 20자 이하
|
||||
- **Password**: 사용자의 초기 비밀번호.
|
||||
- **OU_Name**: 사용자가 생성될 조직 단위(OU)의 경로.
|
||||
- 최상위 OU: `Dev`
|
||||
- 하위 OU: `상위OU/하위OU` (예: `Dev/Test001`)
|
||||
"@
|
||||
}
|
||||
"계정 삭제" {
|
||||
@"
|
||||
** CSV로 계정 삭제하기 **
|
||||
|
||||
'템플릿 다운로드'로 받은 CSV 파일을 아래 형식에 맞게 작성하세요.
|
||||
|
||||
- **SamAccountName**: 삭제할 사용자의 로그인 ID.
|
||||
"@
|
||||
}
|
||||
}
|
||||
# 안내 레이블의 텍스트를 업데이트
|
||||
$script:batch_labelGuide.Text = $guideText.Trim()
|
||||
}
|
||||
|
||||
# 'Dev/Test'와 같은 OU 경로를 'OU=Test,OU=Dev,DC=...' 형식의 DN(Distinguished Name)으로 변환하는 함수
|
||||
function Convert-OUPathToDN {
|
||||
param([string]$OUPath)
|
||||
if (-not $OUPath) {
|
||||
return $null
|
||||
}
|
||||
# '/'를 기준으로 경로를 분리하고, 빈 항목은 제거
|
||||
$ouParts = $OUPath.Split('/') | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
|
||||
# AD DN 형식에 맞게 순서를 뒤집음 (하위 OU가 먼저 오도록)
|
||||
[array]::Reverse($ouParts)
|
||||
# 각 부분을 'OU=이름' 형식으로 변환
|
||||
$ouDNs = $ouParts | ForEach-Object { "OU=$_" }
|
||||
# 쉼표로 연결하고 기본 도메인 DN을 추가하여 최종 DN 완성
|
||||
return ($ouDNs -join ",") + ",$($script:CurrentADDomainDN)"
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# 이벤트 핸들러 정의 (스크립트 블록)
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
# '작업 유형' 콤보박스 선택이 변경될 때 호출
|
||||
$batch_TaskType_Changed = {
|
||||
# UI 컨트롤 상태 초기화
|
||||
$script:batch_dataGridView.DataSource = $null
|
||||
$script:batch_textBoxCsvPath.Text = ""
|
||||
$script:batch_buttonValidate.Enabled = $false
|
||||
$script:batch_buttonExecute.Enabled = $false
|
||||
$script:batch_progressBar.Value = 0
|
||||
$script:batch_dataGridView.Visible = $false
|
||||
$script:batch_labelGuide.Visible = $true
|
||||
|
||||
# 새 작업 유형에 맞는 안내 가이드 표시
|
||||
Update-BatchGuide -taskType $script:batch_comboBoxTaskType.SelectedItem.ToString()
|
||||
}
|
||||
|
||||
# '템플릿 다운로드' 버튼 클릭 시 호출
|
||||
$batch_buttonDownloadTemplate_Click = {
|
||||
$taskType = $script:batch_comboBoxTaskType.SelectedItem.ToString()
|
||||
$headers = ""
|
||||
$sampleData = ""
|
||||
|
||||
# 작업 유형에 따라 CSV 헤더와 샘플 데이터 설정
|
||||
switch ($taskType) {
|
||||
"계정 생성" {
|
||||
$headers = "LastNameKr,FirstNameKr,AccountNameEn,Password,OU_Name"
|
||||
$sampleData = "김,가네,mrkim,Password123!,Dev/Test001"
|
||||
}
|
||||
"계정 삭제" {
|
||||
$headers = "SamAccountName"
|
||||
$sampleData = "testuser"
|
||||
}
|
||||
default {
|
||||
return # 지원하지 않는 작업 유형이면 종료
|
||||
}
|
||||
}
|
||||
|
||||
# 파일 저장 대화상자 생성
|
||||
$sfd = New-Object System.Windows.Forms.SaveFileDialog
|
||||
$sfd.Filter = "CSV 파일 (*.csv)|*.csv"
|
||||
$sfd.FileName = "$($taskType)_template.csv"
|
||||
|
||||
if ($sfd.ShowDialog() -eq "OK") {
|
||||
try {
|
||||
# 헤더와 샘플 데이터를 포함한 CSV 콘텐츠 생성
|
||||
$csvContent = @($headers, $sampleData)
|
||||
# UTF8 인코딩으로 파일 저장
|
||||
Set-Content -Path $sfd.FileName -Value $csvContent -Encoding UTF8
|
||||
[System.Windows.Forms.MessageBox]::Show("샘플 데이터가 포함된 템플릿 파일이 저장되었습니다.`n$($sfd.FileName)", "저장 완료", "OK", "Information")
|
||||
}
|
||||
catch {
|
||||
# 파일 저장 중 오류 발생 시 상세 오류 대화상자 표시
|
||||
Show-DetailedErrorDialog -ErrorRecord $_
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# '파일 찾아보기' 버튼 클릭 시 호출
|
||||
$batch_buttonBrowseCsv_Click = {
|
||||
# 파일 열기 대화상자 생성
|
||||
$ofd = New-Object System.Windows.Forms.OpenFileDialog
|
||||
$ofd.Filter = "CSV 파일 (*.csv)|*.csv"
|
||||
|
||||
if ($ofd.ShowDialog() -eq "OK") {
|
||||
try {
|
||||
# CSV 파일의 첫 줄(헤더)을 읽어옴
|
||||
$headersInFile = (Get-Content -Path $ofd.FileName -TotalCount 1).Split(',') | ForEach-Object { $_.Trim('"') }
|
||||
$taskType = $script:batch_comboBoxTaskType.SelectedItem.ToString()
|
||||
|
||||
# 현재 선택된 작업에 필요한 헤더 정의
|
||||
$requiredHeaders = if ($taskType -eq "계정 생성") { @("LastNameKr", "FirstNameKr", "AccountNameEn", "Password", "OU_Name") } else { @("SamAccountName") }
|
||||
|
||||
# 파일의 헤더와 필요한 헤더가 일치하는지 비교
|
||||
if (($headersInFile | Compare-Object -ReferenceObject $requiredHeaders -SyncWindow 0).Length -ne 0) {
|
||||
[System.Windows.Forms.MessageBox]::Show("CSV 파일의 헤더가 선택된 작업 유형과 일치하지 않습니다.`n템플릿을 다운로드하여 형식을 확인해주세요.", "CSV 형식 오류", "OK", "Warning")
|
||||
return
|
||||
}
|
||||
|
||||
# 파일 경로 텍스트박스 업데이트 및 CSV 파일 가져오기
|
||||
$script:batch_textBoxCsvPath.Text = $ofd.FileName
|
||||
$csvData = Import-Csv -Path $ofd.FileName -Encoding UTF8
|
||||
|
||||
if ($csvData.Count -eq 0) {
|
||||
[System.Windows.Forms.MessageBox]::Show("CSV 파일에 데이터가 없습니다.", "알림", "OK", "Information")
|
||||
return
|
||||
}
|
||||
|
||||
# CSV 데이터를 표시할 DataTable 객체 생성
|
||||
$dt = New-Object System.Data.DataTable
|
||||
$csvData[0].PSObject.Properties.Name | ForEach-Object { $dt.Columns.Add($_) } | Out-Null
|
||||
$dt.Columns.Add("결과") | Out-Null # 결과 표시를 위한 열 추가
|
||||
|
||||
# CSV의 각 행을 DataTable에 추가
|
||||
foreach ($row in $csvData) {
|
||||
$dr = $dt.NewRow()
|
||||
foreach ($prop in $row.PSObject.Properties) {
|
||||
$dr[$prop.Name] = $prop.Value
|
||||
}
|
||||
$dt.Rows.Add($dr)
|
||||
}
|
||||
|
||||
# 데이터 그리드뷰에 DataTable 바인딩 및 UI 상태 업데이트
|
||||
$script:batch_dataGridView.DataSource = $dt
|
||||
$script:batch_dataGridView.Columns["결과"].ReadOnly = $true
|
||||
$script:batch_dataGridView.Columns["결과"].DefaultCellStyle.BackColor = [System.Drawing.Color]::Gainsboro
|
||||
|
||||
Write-Log "$($csvData.Count)개의 항목을 CSV 파일에서 로드했습니다."
|
||||
$script:batch_buttonValidate.Enabled = $true
|
||||
$script:batch_buttonExecute.Enabled = $false
|
||||
$script:batch_progressBar.Value = 0
|
||||
$script:batch_dataGridView.Visible = $true
|
||||
$script:batch_labelGuide.Visible = $false
|
||||
}
|
||||
catch {
|
||||
# 파일 읽기 실패 등 예외 발생 시 상세 오류 대화상자 표시
|
||||
Show-DetailedErrorDialog -ErrorRecord $_
|
||||
$script:batch_buttonValidate.Enabled = $false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 데이터 그리드뷰의 셀 값이 변경될 때 호출
|
||||
$batch_dataGridView_CellValueChanged = {
|
||||
param($src, $e)
|
||||
$rowIndex = $e.RowIndex
|
||||
$colIndex = $e.ColumnIndex
|
||||
|
||||
if ($rowIndex -ge 0 -and $colIndex -ge 0) {
|
||||
$dataGridView = $src
|
||||
# '결과' 열은 시스템이 업데이트하므로 사용자의 수정에 반응하지 않음
|
||||
if ($dataGridView.Columns[$colIndex].Name -eq "결과") {
|
||||
return
|
||||
}
|
||||
|
||||
# 사용자가 데이터를 수정했음을 시각적으로 표시
|
||||
$row = $dataGridView.Rows[$rowIndex]
|
||||
$resultCell = $row.Cells["결과"]
|
||||
if ($resultCell.Value -notlike "수정됨*") {
|
||||
$resultCell.Value = "수정됨 (검사 필요)"
|
||||
$row.DefaultCellStyle.BackColor = [System.Drawing.Color]::LightYellow
|
||||
$script:batch_buttonExecute.Enabled = $false # 수정 후에는 다시 유효성 검사가 필요함
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 데이터 그리드뷰에 새 행을 추가할 때 호출 (컨텍스트 메뉴)
|
||||
$batch_buttonAddRow_Click = {
|
||||
param($src, $e)
|
||||
$dataTable = $script:batch_dataGridView.DataSource
|
||||
|
||||
# 그리드에 데이터가 아직 없는 경우, DataTable을 새로 생성
|
||||
if ($null -eq $dataTable) {
|
||||
$taskType = $script:batch_comboBoxTaskType.SelectedItem.ToString()
|
||||
$headers = if ($taskType -eq "계정 생성") { @("LastNameKr", "FirstNameKr", "AccountNameEn", "Password", "OU_Name") } else { @("SamAccountName") }
|
||||
|
||||
$dataTable = New-Object System.Data.DataTable
|
||||
$headers | ForEach-Object { $dataTable.Columns.Add($_) } | Out-Null
|
||||
$dataTable.Columns.Add("결과") | Out-Null
|
||||
|
||||
$script:batch_dataGridView.DataSource = $dataTable
|
||||
$script:batch_dataGridView.Columns["결과"].ReadOnly = $true
|
||||
$script:batch_dataGridView.Columns["결과"].DefaultCellStyle.BackColor = [System.Drawing.Color]::Gainsboro
|
||||
$script:batch_dataGridView.Visible = $true
|
||||
$script:batch_labelGuide.Visible = $false
|
||||
$script:batch_buttonValidate.Enabled = $true
|
||||
}
|
||||
|
||||
# 새 행을 추가하고 초기 상태 설정
|
||||
$newRow = $dataTable.NewRow()
|
||||
$newRow["결과"] = "신규 (검사 필요)"
|
||||
$dataTable.Rows.Add($newRow)
|
||||
|
||||
# 새로 추가된 행으로 스크롤 이동
|
||||
$script:batch_dataGridView.FirstDisplayedScrollingRowIndex = $script:batch_dataGridView.Rows.Count - 1
|
||||
}
|
||||
|
||||
# 데이터 그리드뷰에서 선택된 행을 삭제할 때 호출 (컨텍스트 메뉴)
|
||||
$batch_buttonDeleteRow_Click = {
|
||||
param($src, $e)
|
||||
$selectedRows = $script:batch_dataGridView.SelectedRows
|
||||
if ($selectedRows.Count -eq 0) {
|
||||
return
|
||||
}
|
||||
# 여러 행 삭제 시 인덱스 문제를 피하기 위해 역순으로 정렬 후 삭제
|
||||
foreach ($row in $selectedRows | Sort-Object Index -Descending) {
|
||||
if (-not $row.IsNewRow) {
|
||||
$script:batch_dataGridView.Rows.Remove($row)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# '유효성 검사' 버튼 클릭 시 호출
|
||||
$batch_buttonValidate_Click = {
|
||||
$dataTable = $script:batch_dataGridView.DataSource
|
||||
if ($null -eq $dataTable -or $dataTable.Rows.Count -eq 0) {
|
||||
return
|
||||
}
|
||||
|
||||
$taskType = $script:batch_comboBoxTaskType.SelectedItem.ToString()
|
||||
$script:batch_buttonExecute.Enabled = $false
|
||||
|
||||
# 동기식으로 유효성 검사 스크립트 블록을 실행하고, 오류가 발생한 행의 수를 반환받음
|
||||
$invalidRowsCount = Invoke-Synchronous -TriggerControl $this -StatusMessage "사전 유효성 검사를 수행합니다..." -RequiresAzureAD -ScriptBlock {
|
||||
$invalidCount = 0
|
||||
$totalCount = $dataTable.Rows.Count
|
||||
$processedCount = 0
|
||||
|
||||
foreach ($dataRow in $dataTable.Rows) {
|
||||
$processedCount++
|
||||
$script:batch_progressBar.Value = ($processedCount / $totalCount) * 100
|
||||
|
||||
try {
|
||||
if ($taskType -eq "계정 생성") {
|
||||
# 필수 값, OU 경로, 계정명 중복 여부 등 검사
|
||||
if (-not ($dataRow.LastNameKr -and $dataRow.FirstNameKr -and $dataRow.AccountNameEn -and $dataRow.OU_Name -and $dataRow.Password)) { throw "필수 열이 비어있습니다." }
|
||||
$ouDN = Convert-OUPathToDN -OUPath $dataRow.OU_Name
|
||||
if (-not (Get-ADOrganizationalUnit -Identity $ouDN -Server $script:Configuration.OnPremDomainController -ErrorAction SilentlyContinue)) { throw "OU 경로 '$($dataRow.OU_Name)'를 찾을 수 없습니다." }
|
||||
$validationResult = Test-SamAccountName -AccountName $dataRow.AccountNameEn -CheckAzureAD
|
||||
if (-not $validationResult.IsValid) { throw $validationResult.Reason }
|
||||
}
|
||||
elseif ($taskType -eq "계정 삭제") {
|
||||
# 필수 값, 계정 존재 여부 검사
|
||||
if (-not $dataRow.SamAccountName) { throw "필수 열(SamAccountName)이 비어있습니다." }
|
||||
if (-not (Get-ADUser -Filter "SamAccountName -eq '$($dataRow.SamAccountName)'" -Server $script:Configuration.OnPremDomainController)) { throw "존재하지 않는 계정입니다." }
|
||||
}
|
||||
# 유효성 검사 통과 시 결과 업데이트
|
||||
$dataRow["결과"] = "✅ 유효"
|
||||
}
|
||||
catch {
|
||||
# 오류 발생 시 결과에 오류 메시지 기록
|
||||
$errorMessage = $_.Exception.Message.Trim()
|
||||
$dataRow["결과"] = "❌ 오류: $errorMessage"
|
||||
$invalidCount++
|
||||
}
|
||||
}
|
||||
# 스크립트 블록의 결과로 오류 개수 반환
|
||||
return $invalidCount
|
||||
}
|
||||
|
||||
# 그리드의 각 행을 결과에 따라 색상으로 구분
|
||||
foreach ($row in $script:batch_dataGridView.Rows) {
|
||||
$resultCell = $row.Cells["결과"]
|
||||
if ($resultCell.Value -like "❌*") {
|
||||
$row.DefaultCellStyle.BackColor = [System.Drawing.Color]::LightPink
|
||||
}
|
||||
else {
|
||||
$row.DefaultCellStyle.BackColor = [System.Drawing.Color]::White
|
||||
}
|
||||
}
|
||||
|
||||
if ($invalidRowsCount -gt 0) {
|
||||
$msg = "$($invalidRowsCount)개의 유효하지 않은 항목이 발견되었습니다. 그리드에서 오류 내용을 확인하고 CSV 파일을 수정한 후 다시 시도하세요."
|
||||
[System.Windows.Forms.MessageBox]::Show($msg, "유효성 검사 실패", "OK", "Warning")
|
||||
}
|
||||
elseif ($invalidRowsCount -eq 0) { # 0일 경우에만 성공으로 간주
|
||||
$script:batch_buttonExecute.Enabled = $true
|
||||
[System.Windows.Forms.MessageBox]::Show("모든 항목이 유효합니다. '일괄 작업 실행' 버튼을 눌러 작업을 시작하세요.", "검사 완료", "OK", "Information")
|
||||
}
|
||||
}
|
||||
|
||||
# '일괄 작업 실행' 버튼 클릭 시 호출
|
||||
$batch_buttonExecute_Click = {
|
||||
$dataTable = $script:batch_dataGridView.DataSource
|
||||
if ($null -eq $dataTable -or $dataTable.Rows.Count -eq 0) {
|
||||
return
|
||||
}
|
||||
|
||||
# 유효성 검사를 먼저 수행했는지 확인
|
||||
if ([string]::IsNullOrWhiteSpace($dataTable.Rows[0]["결과"])) {
|
||||
[System.Windows.Forms.MessageBox]::Show("먼저 '유효성 검사'를 실행하여 데이터의 유효성을 확인해야 합니다.", "검사 필요", "OK", "Warning")
|
||||
return
|
||||
}
|
||||
# 오류가 있는 항목이 있는지 확인
|
||||
$invalidRows = $dataTable.Select("[결과] LIKE '❌*'")
|
||||
if ($invalidRows.Count -gt 0) {
|
||||
[System.Windows.Forms.MessageBox]::Show("오류 항목이 존재합니다. 유효성 검사를 다시 수행하거나 CSV 파일을 수정해주세요.", "오류 발견", "OK", "Error")
|
||||
return
|
||||
}
|
||||
# 실행할 유효한 항목이 있는지 확인
|
||||
$validRows = $dataTable.Select("[결과] = '✅ 유효'")
|
||||
if ($validRows.Count -eq 0) {
|
||||
[System.Windows.Forms.MessageBox]::Show("실행할 유효한 항목이 없습니다.", "작업 불가", "OK", "Information")
|
||||
return
|
||||
}
|
||||
|
||||
# 최종 실행 확인
|
||||
$taskType = $script:batch_comboBoxTaskType.SelectedItem.ToString()
|
||||
$confirmMsg = "유효한 $($validRows.Length)개 항목에 대해 '$taskType' 일괄 작업을 시작하시겠습니까?"
|
||||
if ([System.Windows.Forms.MessageBox]::Show($confirmMsg, "최종 확인", "YesNo", "Question") -ne "Yes") {
|
||||
return
|
||||
}
|
||||
|
||||
# 동기식으로 일괄 작업 실행
|
||||
Invoke-Synchronous -TriggerControl $this -StatusMessage "일괄 작업을 실행합니다..." -ScriptBlock {
|
||||
$successfulSyncCount = 0
|
||||
foreach ($dataRow in $validRows) {
|
||||
try {
|
||||
if ($taskType -eq "계정 생성") {
|
||||
# AD 사용자 생성을 위한 파라미터 설정
|
||||
$ouDN = Convert-OUPathToDN -OUPath $dataRow.OU_Name
|
||||
$userPrincipalName = "$($dataRow.AccountNameEn)@$($script:Configuration.UPNSuffix)"
|
||||
$params = @{
|
||||
Name = "$($dataRow.LastNameKr)$($dataRow.FirstNameKr) [$($dataRow.AccountNameEn)]"
|
||||
DisplayName = "$($dataRow.LastNameKr)$($dataRow.FirstNameKr) [$($dataRow.AccountNameEn)]"
|
||||
SamAccountName = $dataRow.AccountNameEn
|
||||
UserPrincipalName = $userPrincipalName
|
||||
EmailAddress = $userPrincipalName
|
||||
Path = $ouDN
|
||||
GivenName = $dataRow.FirstNameKr
|
||||
Surname = $dataRow.LastNameKr
|
||||
AccountPassword = (ConvertTo-SecureString ($dataRow.Password) -AsPlainText -Force)
|
||||
Enabled = $true
|
||||
ChangePasswordAtLogon = $false
|
||||
Server = $script:Configuration.OnPremDomainController
|
||||
}
|
||||
New-ADUser @params -ErrorAction Stop
|
||||
}
|
||||
elseif ($taskType -eq "계정 삭제") {
|
||||
Remove-ADUser -Identity $dataRow.SamAccountName.Trim() -Confirm:$false -Server $script:Configuration.OnPremDomainController -ErrorAction Stop
|
||||
}
|
||||
|
||||
# 작업 성공 시 결과 업데이트
|
||||
$dataRow["결과"] = "✅ 처리 완료"
|
||||
$successfulSyncCount++
|
||||
}
|
||||
catch {
|
||||
# 작업 실패 시 결과에 오류 메시지 기록
|
||||
$errorMessage = $_.Exception.Message.Trim()
|
||||
$dataRow["결과"] = "❌ 실행 실패: $errorMessage"
|
||||
}
|
||||
}
|
||||
# 성공적으로 처리된 건이 하나라도 있으면 AAD Connect Delta 동기화 실행
|
||||
if ($successfulSyncCount -gt 0) {
|
||||
Write-Log "$successfulSyncCount 건의 성공적인 작업에 대해 AAD Connect 동기화(Delta)를 시작합니다..."
|
||||
Invoke-AadConnectSync -PolicyType Delta
|
||||
}
|
||||
}
|
||||
|
||||
# 그리드의 각 행을 최종 결과에 따라 색상으로 구분
|
||||
foreach ($row in $script:batch_dataGridView.Rows) {
|
||||
if ($row.Cells["결과"].Value -eq "✅ 처리 완료") {
|
||||
$row.DefaultCellStyle.BackColor = [System.Drawing.Color]::PaleGreen
|
||||
}
|
||||
elseif ($row.Cells["결과"].Value -like "❌ 실행 실패*") {
|
||||
$row.DefaultCellStyle.BackColor = [System.Drawing.Color]::OrangeRed
|
||||
}
|
||||
}
|
||||
|
||||
[System.Windows.Forms.MessageBox]::Show("일괄 작업이 완료되었습니다. 최종 결과는 그리드를 확인하세요.`n새로운 작업을 하시려면 다시 CSV 파일을 로드해주세요.", "작업 완료", "OK", "Information")
|
||||
|
||||
# 작업 완료 후 UI 상태 초기화
|
||||
$script:batch_textBoxCsvPath.Text = ""
|
||||
$script:batch_progressBar.Value = 0
|
||||
$script:batch_buttonExecute.Enabled = $false
|
||||
$script:batch_buttonValidate.Enabled = $false
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# 이벤트 등록 함수
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
function Register-BatchTaskEvents {
|
||||
# 각 컨트롤에 정의된 이벤트 핸들러(스크립트 블록)를 연결
|
||||
$script:batch_comboBoxTaskType.add_SelectedIndexChanged($batch_TaskType_Changed)
|
||||
$script:batch_buttonDownloadTemplate.add_Click($batch_buttonDownloadTemplate_Click)
|
||||
$script:batch_buttonBrowseCsv.add_Click($batch_buttonBrowseCsv_Click)
|
||||
$script:batch_buttonValidate.add_Click($batch_buttonValidate_Click)
|
||||
$script:batch_buttonExecute.add_Click($batch_buttonExecute_Click)
|
||||
$script:batch_dataGridView.add_CellValueChanged($batch_dataGridView_CellValueChanged)
|
||||
|
||||
# 데이터 그리드뷰의 우클릭 컨텍스트 메뉴 설정
|
||||
$contextMenu = New-Object System.Windows.Forms.ContextMenuStrip
|
||||
$menuItemAdd = New-Object System.Windows.Forms.ToolStripMenuItem("새 행 추가")
|
||||
$menuItemDelete = New-Object System.Windows.Forms.ToolStripMenuItem("선택 행 삭제")
|
||||
|
||||
$menuItemAdd.Add_Click($batch_buttonAddRow_Click)
|
||||
$menuItemDelete.Add_Click($batch_buttonDeleteRow_Click)
|
||||
|
||||
$contextMenu.Items.AddRange(@($menuItemAdd, $menuItemDelete)) | Out-Null
|
||||
|
||||
# 컨텍스트 메뉴가 열릴 때, 행이 선택된 경우에만 '삭제' 메뉴를 활성화
|
||||
$contextMenu.Add_Opening({
|
||||
param($src, $e)
|
||||
$src.Items[1].Enabled = ($script:batch_dataGridView.SelectedRows.Count -gt 0)
|
||||
})
|
||||
|
||||
$script:batch_dataGridView.ContextMenuStrip = $contextMenu
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# 탭 UI 초기화 함수
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
function Initialize-BatchTaskTab {
|
||||
Param(
|
||||
[System.Windows.Forms.TabPage]$parentTab
|
||||
)
|
||||
|
||||
# 메인 레이아웃 (TableLayoutPanel) 설정
|
||||
$layout = New-Object System.Windows.Forms.TableLayoutPanel
|
||||
$layout.Dock = "Fill"
|
||||
$layout.Padding = [System.Windows.Forms.Padding](10)
|
||||
$layout.ColumnCount = 4
|
||||
$layout.RowCount = 4
|
||||
$layout.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Absolute, 100))) | Out-Null
|
||||
$layout.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Percent, 100))) | Out-Null
|
||||
$layout.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Absolute, 150))) | Out-Null
|
||||
$layout.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Absolute, 160))) | Out-Null
|
||||
$layout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 40))) | Out-Null
|
||||
$layout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 40))) | Out-Null
|
||||
$layout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Percent, 100))) | Out-Null
|
||||
$layout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 50))) | Out-Null
|
||||
$parentTab.Controls.Add($layout) | Out-Null
|
||||
|
||||
# UI 컨트롤 생성
|
||||
# 0행: 작업 유형 선택 영역
|
||||
$labelTaskType = New-Object System.Windows.Forms.Label
|
||||
$script:batch_comboBoxTaskType = New-Object System.Windows.Forms.ComboBox
|
||||
$script:batch_buttonDownloadTemplate = New-Object System.Windows.Forms.Button
|
||||
|
||||
# 1행: CSV 파일 선택 영역
|
||||
$labelCsvPath = New-Object System.Windows.Forms.Label
|
||||
$script:batch_textBoxCsvPath = New-Object System.Windows.Forms.TextBox
|
||||
$script:batch_buttonBrowseCsv = New-Object System.Windows.Forms.Button
|
||||
|
||||
# 2행: 데이터 표시 영역 (그리드뷰, 안내문)
|
||||
$gridPanel = New-Object System.Windows.Forms.Panel
|
||||
$script:batch_dataGridView = New-Object System.Windows.Forms.DataGridView
|
||||
$script:batch_labelGuide = New-Object System.Windows.Forms.Label
|
||||
|
||||
# 3행: 진행률 및 실행 버튼 영역
|
||||
$script:batch_progressBar = New-Object System.Windows.Forms.ProgressBar
|
||||
$execButtonLayout = New-Object System.Windows.Forms.TableLayoutPanel
|
||||
$script:batch_buttonValidate = New-Object System.Windows.Forms.Button
|
||||
$script:batch_buttonExecute = New-Object System.Windows.Forms.Button
|
||||
|
||||
# UI 컨트롤 속성 설정
|
||||
# - 작업 유형 레이블
|
||||
$labelTaskType.Text = "작업 유형:"
|
||||
$labelTaskType.Dock = "Fill"
|
||||
$labelTaskType.TextAlign = "MiddleLeft"
|
||||
|
||||
# - 작업 유형 콤보박스
|
||||
$script:batch_comboBoxTaskType.Dock = "Fill"
|
||||
$script:batch_comboBoxTaskType.DropDownStyle = "DropDownList"
|
||||
$script:batch_comboBoxTaskType.Items.AddRange(@("계정 생성", "계정 삭제")) | Out-Null
|
||||
$script:batch_comboBoxTaskType.SelectedIndex = 0
|
||||
|
||||
# - 템플릿 다운로드 버튼
|
||||
$script:batch_buttonDownloadTemplate.Text = "템플릿 다운로드(&D)"
|
||||
$script:batch_buttonDownloadTemplate.Dock = "Fill"
|
||||
|
||||
# - CSV 파일 경로 레이블
|
||||
$labelCsvPath.Text = "CSV 파일 경로:"
|
||||
$labelCsvPath.Dock = "Fill"
|
||||
$labelCsvPath.TextAlign = "MiddleLeft"
|
||||
|
||||
# - CSV 파일 경로 텍스트박스
|
||||
$script:batch_textBoxCsvPath.Dock = "Fill"
|
||||
$script:batch_textBoxCsvPath.ReadOnly = $true
|
||||
|
||||
# - 파일 찾아보기 버튼
|
||||
$script:batch_buttonBrowseCsv.Text = "파일 찾아보기(&B)..."
|
||||
$script:batch_buttonBrowseCsv.Dock = "Fill"
|
||||
|
||||
# - 그리드뷰를 담을 패널
|
||||
$gridPanel.Dock = "Fill"
|
||||
|
||||
# - 데이터 그리드뷰
|
||||
$script:batch_dataGridView.Dock = "Fill"
|
||||
$script:batch_dataGridView.ReadOnly = $false
|
||||
$script:batch_dataGridView.AllowUserToAddRows = $false
|
||||
$script:batch_dataGridView.AllowUserToDeleteRows = $false
|
||||
$script:batch_dataGridView.AutoSizeColumnsMode = "Fill"
|
||||
$script:batch_dataGridView.Visible = $false
|
||||
$script:batch_dataGridView.SelectionMode = "FullRowSelect"
|
||||
|
||||
# - 안내 가이드 레이블
|
||||
$script:batch_labelGuide.Dock = "Fill"
|
||||
$script:batch_labelGuide.TextAlign = "MiddleLeft"
|
||||
$script:batch_labelGuide.Padding = New-Object System.Windows.Forms.Padding(20, 0, 0, 0)
|
||||
$script:batch_labelGuide.Font = New-Object System.Drawing.Font("Malgun Gothic", 9.5)
|
||||
$script:batch_labelGuide.ForeColor = [System.Drawing.Color]::DimGray
|
||||
|
||||
# - 진행률 표시줄
|
||||
$script:batch_progressBar.Dock = "Fill"
|
||||
$script:batch_progressBar.Maximum = 100
|
||||
$script:batch_progressBar.Value = 0
|
||||
|
||||
# - 실행 버튼들을 담을 레이아웃
|
||||
$execButtonLayout.Dock = "Fill"
|
||||
$execButtonLayout.ColumnCount = 2
|
||||
$execButtonLayout.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Percent, 50))) | Out-Null
|
||||
$execButtonLayout.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Percent, 50))) | Out-Null
|
||||
|
||||
# - 유효성 검사 버튼
|
||||
$script:batch_buttonValidate.Text = "유효성 검사(&V)"
|
||||
$script:batch_buttonValidate.Dock = "Fill"
|
||||
$script:batch_buttonValidate.Enabled = $false
|
||||
|
||||
# - 일괄 작업 실행 버튼
|
||||
$script:batch_buttonExecute.Text = "일괄 작업 실행(&X)"
|
||||
$script:batch_buttonExecute.Dock = "Fill"
|
||||
$script:batch_buttonExecute.Enabled = $false
|
||||
$script:batch_buttonExecute.Font = New-Object System.Drawing.Font("Segoe UI", 10, [System.Drawing.FontStyle]::Bold)
|
||||
|
||||
# 컨트롤들을 메인 레이아웃에 배치
|
||||
# 0행: 작업 유형 선택
|
||||
$layout.Controls.Add($labelTaskType, 0, 0) | Out-Null
|
||||
$layout.Controls.Add($script:batch_comboBoxTaskType, 1, 0) | Out-Null
|
||||
$layout.SetColumnSpan($script:batch_comboBoxTaskType, 2)
|
||||
$layout.Controls.Add($script:batch_buttonDownloadTemplate, 3, 0) | Out-Null
|
||||
|
||||
# 1행: CSV 파일 경로
|
||||
$layout.Controls.Add($labelCsvPath, 0, 1) | Out-Null
|
||||
$layout.Controls.Add($script:batch_textBoxCsvPath, 1, 1) | Out-Null
|
||||
$layout.SetColumnSpan($script:batch_textBoxCsvPath, 2)
|
||||
$layout.Controls.Add($script:batch_buttonBrowseCsv, 3, 1) | Out-Null
|
||||
|
||||
# 2행: 데이터 그리드 및 안내문
|
||||
$gridPanel.Controls.Add($script:batch_dataGridView) | Out-Null
|
||||
$gridPanel.Controls.Add($script:batch_labelGuide) | Out-Null
|
||||
$layout.Controls.Add($gridPanel, 0, 2) | Out-Null
|
||||
$layout.SetColumnSpan($gridPanel, 4)
|
||||
|
||||
# 3행: 진행률 및 실행 버튼
|
||||
$execButtonLayout.Controls.Add($script:batch_buttonValidate, 0, 0) | Out-Null
|
||||
$execButtonLayout.Controls.Add($script:batch_buttonExecute, 1, 0) | Out-Null
|
||||
$layout.Controls.Add($script:batch_progressBar, 0, 3) | Out-Null
|
||||
$layout.SetColumnSpan($script:batch_progressBar, 2)
|
||||
$layout.Controls.Add($execButtonLayout, 2, 3) | Out-Null
|
||||
$layout.SetColumnSpan($execButtonLayout, 2)
|
||||
|
||||
# 이벤트 핸들러 등록
|
||||
Register-BatchTaskEvents
|
||||
|
||||
# 탭 초기화 시 '작업 유형 변경' 이벤트를 수동으로 호출하여 UI 초기 상태를 설정
|
||||
$batch_TaskType_Changed.Invoke($null, $null)
|
||||
}
|
||||
#endregion
|
||||
|
||||
Write-Host "UI-Tab-BatchTask.ps1 로드 완료." -ForegroundColor Cyan
|
||||
375
Scripts/UI-Tab-DeleteUser.ps1
Normal file
375
Scripts/UI-Tab-DeleteUser.ps1
Normal file
@ -0,0 +1,375 @@
|
||||
# ================================================================================
|
||||
# 파일: 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
|
||||
530
Scripts/UI-Tab-HardMatch.ps1
Normal file
530
Scripts/UI-Tab-HardMatch.ps1
Normal file
@ -0,0 +1,530 @@
|
||||
# ================================================================================
|
||||
# 파일: Scripts/UI-Tab-HardMatch.ps1
|
||||
# 역할: '하드 매칭' 탭 UI 및 기능 구현 (리팩토링 최종 완전판)
|
||||
#
|
||||
# 작성자: 양범진
|
||||
# 버전: 1.13
|
||||
# 생성일자: 2025-06-05
|
||||
# 최종 수정일자: 2025-06-12
|
||||
#
|
||||
# 설명:
|
||||
# - '3. 작업 실행' 영역의 레이아웃을 2열 구조로 변경하여 UI 잘림 문제 해결.
|
||||
# - 하드 매칭으로 신규 계정 생성 시, SamAccountName 중복 확인 로직 추가.
|
||||
# ================================================================================
|
||||
|
||||
#region '하드 매칭' 탭 UI 초기화 함수
|
||||
function Initialize-HardMatchTab {
|
||||
Param(
|
||||
[System.Windows.Forms.TabPage]$parentTab
|
||||
)
|
||||
|
||||
# 탭 페이지의 메인 레이아웃 (3행 1열)
|
||||
$hmTabMainLayout = New-Object System.Windows.Forms.TableLayoutPanel
|
||||
$hmTabMainLayout.Dock = "Fill"
|
||||
$hmTabMainLayout.Padding = [System.Windows.Forms.Padding](10)
|
||||
$hmTabMainLayout.ColumnCount = 1
|
||||
$hmTabMainLayout.RowCount = 3
|
||||
$hmTabMainLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 160))) | Out-Null
|
||||
$hmTabMainLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 160))) | Out-Null
|
||||
$hmTabMainLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Percent, 100))) | Out-Null
|
||||
$parentTab.Controls.Add($hmTabMainLayout) | Out-Null
|
||||
|
||||
# 1. Azure AD 사용자 검색 그룹박스
|
||||
$groupSearchAzure = New-Object System.Windows.Forms.GroupBox
|
||||
$groupSearchAzure.Text = "1. Azure AD 사용자 검색 (Cloud-Only 또는 동기화 오류 사용자)"
|
||||
$groupSearchAzure.Dock = "Fill"
|
||||
$hmTabMainLayout.Controls.Add($groupSearchAzure, 0, 0) | Out-Null
|
||||
|
||||
$searchAzureLayout = New-Object System.Windows.Forms.TableLayoutPanel
|
||||
$searchAzureLayout.Dock = "Fill"
|
||||
$searchAzureLayout.Padding = [System.Windows.Forms.Padding](10)
|
||||
$searchAzureLayout.ColumnCount = 2
|
||||
$searchAzureLayout.RowCount = 2
|
||||
$searchAzureLayout.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Percent, 100))) | Out-Null
|
||||
$searchAzureLayout.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Absolute, 150))) | Out-Null
|
||||
$searchAzureLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 35))) | Out-Null
|
||||
$searchAzureLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Percent, 100))) | Out-Null
|
||||
$groupSearchAzure.Controls.Add($searchAzureLayout) | Out-Null
|
||||
|
||||
$script:hm_textBoxSearchAzure = New-Object System.Windows.Forms.TextBox
|
||||
$script:hm_textBoxSearchAzure.Dock = "Fill"
|
||||
|
||||
$buttonSearchAzure = New-Object System.Windows.Forms.Button
|
||||
$buttonSearchAzure.Text = "Azure AD 검색(&Z)"
|
||||
$buttonSearchAzure.Dock = "Fill"
|
||||
|
||||
$script:hm_listBoxFoundAzure = New-Object System.Windows.Forms.ListBox
|
||||
$script:hm_listBoxFoundAzure.Dock = "Fill"
|
||||
|
||||
$searchAzureLayout.Controls.Add($script:hm_textBoxSearchAzure, 0, 0) | Out-Null
|
||||
$searchAzureLayout.Controls.Add($buttonSearchAzure, 1, 0) | Out-Null
|
||||
$searchAzureLayout.Controls.Add($script:hm_listBoxFoundAzure, 0, 1) | Out-Null
|
||||
$searchAzureLayout.SetColumnSpan($script:hm_listBoxFoundAzure, 2)
|
||||
|
||||
# 2. 사용자 정보 확인 그룹박스
|
||||
$groupInfo = New-Object System.Windows.Forms.GroupBox
|
||||
$groupInfo.Text = "2. 사용자 정보 확인"
|
||||
$groupInfo.Dock = "Fill"
|
||||
$hmTabMainLayout.Controls.Add($groupInfo, 0, 1) | Out-Null
|
||||
|
||||
$infoLayout = New-Object System.Windows.Forms.TableLayoutPanel
|
||||
$infoLayout.Dock = "Fill"
|
||||
$infoLayout.ColumnCount = 2
|
||||
$infoLayout.RowCount = 1
|
||||
$infoLayout.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Percent, 50))) | Out-Null
|
||||
$infoLayout.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Percent, 50))) | Out-Null
|
||||
$groupInfo.Controls.Add($infoLayout) | Out-Null
|
||||
|
||||
# 2-1. Azure AD 사용자 정보
|
||||
$groupAzure = New-Object System.Windows.Forms.GroupBox
|
||||
$groupAzure.Text = "Azure AD 사용자"
|
||||
$groupAzure.Dock = "Fill"
|
||||
$infoLayout.Controls.Add($groupAzure, 0, 0) | Out-Null
|
||||
|
||||
$script:hm_textBoxAzureUser = New-Object System.Windows.Forms.TextBox
|
||||
$script:hm_textBoxAzureUser.Location = New-Object System.Drawing.Point(15, 25)
|
||||
$script:hm_textBoxAzureUser.Width = $groupAzure.ClientSize.Width - 30
|
||||
$script:hm_textBoxAzureUser.Anchor = 'Top, Left, Right'
|
||||
$script:hm_textBoxAzureUser.ReadOnly = $true
|
||||
|
||||
$script:hm_textBoxImmutableId = New-Object System.Windows.Forms.TextBox
|
||||
$script:hm_textBoxImmutableId.Location = New-Object System.Drawing.Point(15, 55)
|
||||
$script:hm_textBoxImmutableId.Width = $groupAzure.ClientSize.Width - 30
|
||||
$script:hm_textBoxImmutableId.Anchor = 'Top, Left, Right'
|
||||
$script:hm_textBoxImmutableId.ReadOnly = $true
|
||||
|
||||
$script:hm_labelGuidMatchStatus = New-Object System.Windows.Forms.Label
|
||||
$script:hm_labelGuidMatchStatus.Location = New-Object System.Drawing.Point(15, 85)
|
||||
$script:hm_labelGuidMatchStatus.Width = $groupAzure.ClientSize.Width - 30
|
||||
$script:hm_labelGuidMatchStatus.Anchor = 'Top, Left, Right'
|
||||
$script:hm_labelGuidMatchStatus.Font = New-Object System.Drawing.Font("Segoe UI", 9, [System.Drawing.FontStyle]::Bold)
|
||||
$script:hm_labelGuidMatchStatus.TextAlign = "MiddleLeft"
|
||||
$script:hm_labelGuidMatchStatus.Visible = $false
|
||||
|
||||
$groupAzure.Controls.AddRange(@(
|
||||
$script:hm_textBoxAzureUser,
|
||||
$script:hm_textBoxImmutableId,
|
||||
$script:hm_labelGuidMatchStatus
|
||||
)) | Out-Null
|
||||
|
||||
# 2-2. 온프레미스 AD 사용자 정보
|
||||
$groupOnPrem = New-Object System.Windows.Forms.GroupBox
|
||||
$groupOnPrem.Text = "온프레미스 AD 사용자"
|
||||
$groupOnPrem.Dock = "Fill"
|
||||
$groupOnPrem.Visible = $false
|
||||
$infoLayout.Controls.Add($groupOnPrem, 1, 0) | Out-Null
|
||||
|
||||
$script:hm_textBoxOnPremUser = New-Object System.Windows.Forms.TextBox
|
||||
$script:hm_textBoxOnPremUser.Location = New-Object System.Drawing.Point(15, 25)
|
||||
$script:hm_textBoxOnPremUser.Width = $groupOnPrem.ClientSize.Width - 30
|
||||
$script:hm_textBoxOnPremUser.Anchor = 'Top, Left, Right'
|
||||
$script:hm_textBoxOnPremUser.ReadOnly = $true
|
||||
|
||||
$script:hm_textBoxObjectGuid = New-Object System.Windows.Forms.TextBox
|
||||
$script:hm_textBoxObjectGuid.Location = New-Object System.Drawing.Point(15, 55)
|
||||
$script:hm_textBoxObjectGuid.Width = $groupOnPrem.ClientSize.Width - 30
|
||||
$script:hm_textBoxObjectGuid.Anchor = 'Top, Left, Right'
|
||||
$script:hm_textBoxObjectGuid.ReadOnly = $true
|
||||
|
||||
$script:hm_textBoxConvertedImmutableId = New-Object System.Windows.Forms.TextBox
|
||||
$script:hm_textBoxConvertedImmutableId.Location = New-Object System.Drawing.Point(15, 85)
|
||||
$script:hm_textBoxConvertedImmutableId.Width = $groupOnPrem.ClientSize.Width - 30
|
||||
$script:hm_textBoxConvertedImmutableId.Anchor = 'Top, Left, Right'
|
||||
$script:hm_textBoxConvertedImmutableId.ReadOnly = $true
|
||||
|
||||
$groupOnPrem.Controls.AddRange(@(
|
||||
$script:hm_textBoxOnPremUser,
|
||||
$script:hm_textBoxObjectGuid,
|
||||
$script:hm_textBoxConvertedImmutableId
|
||||
)) | Out-Null
|
||||
|
||||
# 3. 작업 실행 그룹박스
|
||||
$groupAction = New-Object System.Windows.Forms.GroupBox
|
||||
$groupAction.Text = "3. 작업 실행"
|
||||
$groupAction.Dock = "Fill"
|
||||
$hmTabMainLayout.Controls.Add($groupAction, 0, 2) | Out-Null
|
||||
|
||||
# '작업 실행' 그룹박스 내부의 외부 레이아웃 (컨텐츠 영역 + 실행 버튼)
|
||||
$actionOuterLayout = New-Object System.Windows.Forms.TableLayoutPanel
|
||||
$actionOuterLayout.Dock = "Fill"
|
||||
$actionOuterLayout.ColumnCount = 1
|
||||
$actionOuterLayout.RowCount = 2
|
||||
$actionOuterLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Percent, 100))) | Out-Null
|
||||
$actionOuterLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 50))) | Out-Null
|
||||
$groupAction.Controls.Add($actionOuterLayout) | Out-Null
|
||||
|
||||
# '계정 생성' 옵션이 표시될 패널 (기본적으로 숨김)
|
||||
$script:hm_groupCreate = New-Object System.Windows.Forms.Panel
|
||||
$script:hm_groupCreate.Dock = "Fill"
|
||||
$script:hm_groupCreate.Visible = $false
|
||||
$actionOuterLayout.Controls.Add($script:hm_groupCreate, 0, 0) | Out-Null
|
||||
|
||||
# '계정 생성' 패널 내부 레이아웃 (2열)
|
||||
$createInnerLayout = New-Object System.Windows.Forms.TableLayoutPanel
|
||||
$createInnerLayout.Dock = "Fill"
|
||||
$createInnerLayout.ColumnCount = 2
|
||||
$createInnerLayout.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Percent, 50))) | Out-Null
|
||||
$createInnerLayout.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Percent, 50))) | Out-Null
|
||||
$script:hm_groupCreate.Controls.Add($createInnerLayout) | Out-Null
|
||||
|
||||
# 3-1. 신규 온프레미스 계정 정보 입력 영역
|
||||
$groupCreateInfo = New-Object System.Windows.Forms.GroupBox
|
||||
$groupCreateInfo.Text = "신규 온프레미스 계정 정보"
|
||||
$groupCreateInfo.Dock = "Fill"
|
||||
$createInnerLayout.Controls.Add($groupCreateInfo, 0, 0) | Out-Null
|
||||
$createInnerLayout.SetRowSpan($groupCreateInfo, 2)
|
||||
|
||||
$hmCreateInfoLayout = New-Object System.Windows.Forms.TableLayoutPanel
|
||||
$hmCreateInfoLayout.Dock = "Fill"
|
||||
$hmCreateInfoLayout.Padding = [System.Windows.Forms.Padding](10)
|
||||
$hmCreateInfoLayout.ColumnCount = 2
|
||||
$hmCreateInfoLayout.RowCount = 3
|
||||
$hmCreateInfoLayout.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Absolute, 100))) | Out-Null
|
||||
$hmCreateInfoLayout.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Percent, 100))) | Out-Null
|
||||
$rowHeight = 35
|
||||
$hmCreateInfoLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, $rowHeight))) | Out-Null
|
||||
$hmCreateInfoLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, $rowHeight))) | Out-Null
|
||||
$hmCreateInfoLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, $rowHeight))) | Out-Null
|
||||
$groupCreateInfo.Controls.Add($hmCreateInfoLayout) | Out-Null
|
||||
|
||||
$labelHMLastNameKr = New-Object System.Windows.Forms.Label
|
||||
$labelHMLastNameKr.Text = "한글 성:"
|
||||
$labelHMLastNameKr.Dock = "Fill"
|
||||
$labelHMLastNameKr.TextAlign = "MiddleLeft"
|
||||
|
||||
$script:hm_textBoxLastNameKr = New-Object System.Windows.Forms.TextBox
|
||||
$script:hm_textBoxLastNameKr.Dock = "Fill"
|
||||
|
||||
$labelHMFirstNameKr = New-Object System.Windows.Forms.Label
|
||||
$labelHMFirstNameKr.Text = "한글 이름:"
|
||||
$labelHMFirstNameKr.Dock = "Fill"
|
||||
$labelHMFirstNameKr.TextAlign = "MiddleLeft"
|
||||
|
||||
$script:hm_textBoxFirstNameKr = New-Object System.Windows.Forms.TextBox
|
||||
$script:hm_textBoxFirstNameKr.Dock = "Fill"
|
||||
|
||||
$labelHMPassword = New-Object System.Windows.Forms.Label
|
||||
$labelHMPassword.Text = "초기 암호:"
|
||||
$labelHMPassword.Dock = "Fill"
|
||||
$labelHMPassword.TextAlign = "MiddleLeft"
|
||||
|
||||
$script:hm_textBoxPassword = New-Object System.Windows.Forms.TextBox
|
||||
$script:hm_textBoxPassword.Dock = "Fill"
|
||||
$script:hm_textBoxPassword.Text = $script:Configuration.DefaultPassword
|
||||
|
||||
$hmCreateInfoLayout.Controls.Add($labelHMLastNameKr, 0, 0) | Out-Null
|
||||
$hmCreateInfoLayout.Controls.Add($script:hm_textBoxLastNameKr, 1, 0) | Out-Null
|
||||
$hmCreateInfoLayout.Controls.Add($labelHMFirstNameKr, 0, 1) | Out-Null
|
||||
$hmCreateInfoLayout.Controls.Add($script:hm_textBoxFirstNameKr, 1, 1) | Out-Null
|
||||
$hmCreateInfoLayout.Controls.Add($labelHMPassword, 0, 2) | Out-Null
|
||||
$hmCreateInfoLayout.Controls.Add($script:hm_textBoxPassword, 1, 2) | Out-Null
|
||||
|
||||
# 3-2. 대상 OU 선택 영역
|
||||
$groupCreateOU = New-Object System.Windows.Forms.GroupBox
|
||||
$groupCreateOU.Text = "대상 OU 선택"
|
||||
$groupCreateOU.Dock = "Fill"
|
||||
$createInnerLayout.Controls.Add($groupCreateOU, 1, 0) | Out-Null
|
||||
$createInnerLayout.SetRowSpan($groupCreateOU, 2)
|
||||
|
||||
# OU 그룹박스 내부 레이아웃 (선택된 OU 표시 + TreeView)
|
||||
$hmCreateOU_Layout = New-Object System.Windows.Forms.TableLayoutPanel
|
||||
$hmCreateOU_Layout.Dock = "Fill"
|
||||
$hmCreateOU_Layout.Padding = [System.Windows.Forms.Padding](10)
|
||||
$hmCreateOU_Layout.ColumnCount = 1
|
||||
$hmCreateOU_Layout.RowCount = 2
|
||||
$hmCreateOU_Layout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 35))) | Out-Null
|
||||
$hmCreateOU_Layout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Percent, 100))) | Out-Null
|
||||
$groupCreateOU.Controls.Add($hmCreateOU_Layout) | Out-Null
|
||||
|
||||
$script:hm_textBoxSelectedOU = New-Object System.Windows.Forms.TextBox
|
||||
$script:hm_textBoxSelectedOU.Dock = "Fill"
|
||||
$script:hm_textBoxSelectedOU.ReadOnly = $true
|
||||
|
||||
$script:hm_treeViewOUs = New-Object System.Windows.Forms.TreeView
|
||||
$script:hm_treeViewOUs.Dock = "Fill"
|
||||
$script:hm_treeViewOUs.HideSelection = $false
|
||||
|
||||
$hmCreateOU_Layout.Controls.Add($script:hm_textBoxSelectedOU, 0, 0) | Out-Null
|
||||
$hmCreateOU_Layout.Controls.Add($script:hm_treeViewOUs, 0, 1) | Out-Null
|
||||
|
||||
# 최종 실행 버튼
|
||||
$script:hm_buttonExecuteAction = New-Object System.Windows.Forms.Button
|
||||
$script:hm_buttonExecuteAction.Text = "작업 실행"
|
||||
$script:hm_buttonExecuteAction.Dock = "Fill"
|
||||
$script:hm_buttonExecuteAction.Enabled = $false
|
||||
$script:hm_buttonExecuteAction.Font = New-Object System.Drawing.Font("Segoe UI", 12, [System.Drawing.FontStyle]::Bold)
|
||||
$actionOuterLayout.Controls.Add($script:hm_buttonExecuteAction, 0, 1) | Out-Null
|
||||
|
||||
# 이벤트 핸들러 연결
|
||||
$buttonSearchAzure.Add_Click($hm_buttonSearchAzure_Click)
|
||||
$script:hm_listBoxFoundAzure.Add_SelectedIndexChanged($hm_listBoxFoundAzure_SelectedIndexChanged)
|
||||
$script:hm_listBoxFoundAzure.Add_DoubleClick({
|
||||
if ($script:hm_buttonExecuteAction.Enabled) {
|
||||
$script:hm_buttonExecuteAction.PerformClick()
|
||||
}
|
||||
})
|
||||
$script:hm_treeViewOUs.Add_AfterSelect($hm_treeViewOUs_AfterSelect)
|
||||
$script:hm_buttonExecuteAction.Add_Click($hm_buttonExecuteAction_Click)
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region '하드 매칭' 탭 이벤트 핸들러
|
||||
|
||||
# Azure AD 사용자 검색 버튼 클릭 이벤트
|
||||
$hm_buttonSearchAzure_Click = {
|
||||
$searchTerm = $script:hm_textBoxSearchAzure.Text.Trim()
|
||||
if (-not $searchTerm) {
|
||||
return
|
||||
}
|
||||
|
||||
# 비동기 호출로 Azure AD 사용자 검색
|
||||
$users = Invoke-Synchronous -TriggerControl $this -StatusMessage "Azure AD의 모든 사용자를 조회하여 필터링합니다..." -ScriptBlock {
|
||||
$allAzureUsers = Get-MgUser -All -Property "Id,DisplayName,UserPrincipalName,onPremisesImmutableId,Mail,GivenName,Surname" -ErrorAction Stop
|
||||
return $allAzureUsers | Where-Object {
|
||||
$_.DisplayName -like "*$searchTerm*" -or
|
||||
$_.UserPrincipalName -like "*$searchTerm*" -or
|
||||
$_.Mail -like "*$searchTerm*"
|
||||
}
|
||||
} -RequiresAzureAD
|
||||
|
||||
# 검색 결과를 리스트박스에 추가
|
||||
$script:hm_listBoxFoundAzure.BeginUpdate()
|
||||
$script:hm_listBoxFoundAzure.Items.Clear()
|
||||
if ($users) {
|
||||
foreach ($user in @($users) | Sort-Object DisplayName) {
|
||||
$item = [PSCustomObject]@{
|
||||
DisplayText = "$($user.DisplayName) ($($user.UserPrincipalName))"
|
||||
AzureObject = $user
|
||||
}
|
||||
$script:hm_listBoxFoundAzure.Items.Add($item) | Out-Null
|
||||
}
|
||||
}
|
||||
$script:hm_listBoxFoundAzure.DisplayMember = "DisplayText"
|
||||
$script:hm_listBoxFoundAzure.EndUpdate()
|
||||
Write-Log "총 $(@($users).Count)명의 사용자를 찾았습니다."
|
||||
}
|
||||
|
||||
# 검색된 Azure AD 사용자 리스트에서 항목 선택 시 이벤트
|
||||
$hm_listBoxFoundAzure_SelectedIndexChanged = {
|
||||
$selectedItem = $this.SelectedItem
|
||||
if ($null -eq $selectedItem) {
|
||||
return
|
||||
}
|
||||
|
||||
# UI 상태 초기화
|
||||
$groupOnPrem = $script:hm_textBoxOnPremUser.Parent
|
||||
$groupCreate = $script:hm_groupCreate
|
||||
$groupOnPrem.Visible = $false
|
||||
$groupCreate.Visible = $false
|
||||
$script:hm_buttonExecuteAction.Enabled = $false
|
||||
$script:hm_labelGuidMatchStatus.Visible = $false
|
||||
|
||||
# 선택된 Azure AD 사용자 정보 표시
|
||||
$script:hm_selectedAzureUser = $selectedItem.AzureObject
|
||||
$script:hm_textBoxAzureUser.Text = "$($script:hm_selectedAzureUser.DisplayName) ($($script:hm_selectedAzureUser.UserPrincipalName))"
|
||||
$script:hm_textBoxImmutableId.Text = "ImmutableId: $($script:hm_selectedAzureUser.onPremisesImmutableId)"
|
||||
|
||||
# UPN을 기준으로 온프레미스 AD에서 동일한 SamAccountName을 가진 사용자 검색
|
||||
$samAccountName = $script:hm_selectedAzureUser.UserPrincipalName.Split('@')[0]
|
||||
$onPremUser = Invoke-Synchronous -TriggerControl $this -StatusMessage "온프레미스에서 '$samAccountName' 확인 중..." -ScriptBlock {
|
||||
Get-ADUser -Filter "SamAccountName -eq '$samAccountName'" -Properties objectGUID, DisplayName, SamAccountName -Server $script:Configuration.OnPremDomainController -ErrorAction SilentlyContinue
|
||||
}
|
||||
$script:hm_foundOnPremUser = $onPremUser
|
||||
|
||||
# 분기 처리: 온프레미스에 계정이 있는 경우 vs 없는 경우
|
||||
if ($script:hm_foundOnPremUser) {
|
||||
# [CASE 1] 온프레미스 계정이 존재할 경우: 정보 표시 및 상태 진단
|
||||
$groupOnPrem.Visible = $true
|
||||
$script:hm_groupCreate.Visible = $false
|
||||
$script:hm_textBoxOnPremUser.Text = "$($script:hm_foundOnPremUser.DisplayName) (sAM: $($script:hm_foundOnPremUser.SamAccountName))"
|
||||
$script:hm_textBoxObjectGuid.Text = "ObjectGUID: $($script:hm_foundOnPremUser.objectGUID)"
|
||||
|
||||
$immutableIdFromGuid = [System.Convert]::ToBase64String($script:hm_foundOnPremUser.objectGUID.ToByteArray())
|
||||
$script:hm_textBoxConvertedImmutableId.Text = "변환된 ImmutableId: $immutableIdFromGuid"
|
||||
$script:hm_labelGuidMatchStatus.Visible = $true
|
||||
|
||||
# ImmutableId 값 비교를 통해 매칭 상태 진단
|
||||
if ([string]::IsNullOrEmpty($script:hm_selectedAzureUser.onPremisesImmutableId)) {
|
||||
$script:hm_labelGuidMatchStatus.Text = "⚠️ 매칭 필요 (Azure AD ImmutableId 없음)"
|
||||
$script:hm_labelGuidMatchStatus.ForeColor = [System.Drawing.Color]::Orange
|
||||
$script:hm_currentMode = "MatchExisting"
|
||||
$script:hm_buttonExecuteAction.Text = "기존 계정과 하드 매칭 실행"
|
||||
$script:hm_buttonExecuteAction.Enabled = $true
|
||||
}
|
||||
elseif ($script:hm_selectedAzureUser.onPremisesImmutableId -eq $immutableIdFromGuid) {
|
||||
$script:hm_labelGuidMatchStatus.Text = "✅ ID 일치 (정상 동기화 상태)"
|
||||
$script:hm_labelGuidMatchStatus.ForeColor = [System.Drawing.Color]::Green
|
||||
$script:hm_currentMode = "Idle"
|
||||
$script:hm_buttonExecuteAction.Text = "작업 불필요"
|
||||
$script:hm_buttonExecuteAction.Enabled = $false
|
||||
}
|
||||
else {
|
||||
$script:hm_labelGuidMatchStatus.Text = "❌ ID 불일치 (강제 매칭 필요)"
|
||||
$script:hm_labelGuidMatchStatus.ForeColor = [System.Drawing.Color]::Red
|
||||
$script:hm_currentMode = "MatchExisting"
|
||||
$script:hm_buttonExecuteAction.Text = "기존 계정과 하드 매칭 실행 (덮어쓰기)"
|
||||
$script:hm_buttonExecuteAction.Enabled = $true
|
||||
}
|
||||
} else {
|
||||
# [CASE 2] 온프레미스 계정이 없을 경우: 신규 생성 및 매칭 준비
|
||||
$groupCreate.Visible = $true
|
||||
$script:hm_currentMode = "CreateAndMatch"
|
||||
$script:hm_buttonExecuteAction.Text = "계정 생성 및 하드 매칭 실행"
|
||||
$script:hm_buttonExecuteAction.Enabled = $true
|
||||
|
||||
# Azure AD 사용자 정보를 기반으로 생성 정보 자동 채우기
|
||||
$script:hm_textBoxLastNameKr.Text = $script:hm_selectedAzureUser.Surname
|
||||
$script:hm_textBoxFirstNameKr.Text = $script:hm_selectedAzureUser.GivenName
|
||||
|
||||
# OU 목록을 TreeView로 로드
|
||||
$allOUs = Invoke-Synchronous -TriggerControl $this -StatusMessage "[하드 매칭] OU 목록을 조회합니다..." -ScriptBlock {
|
||||
Get-ADOrganizationalUnit -Filter * -SearchBase $script:CurrentADDomainDN -SearchScope Subtree -Server $script:Configuration.OnPremDomainController -ErrorAction SilentlyContinue
|
||||
}
|
||||
if ($allOUs) {
|
||||
$ouHierarchy = @{}
|
||||
foreach ($ou in $allOUs) {
|
||||
$parentDN = $ou.DistinguishedName -replace '^OU=[^,]+,', ''
|
||||
if (-not $ouHierarchy.ContainsKey($parentDN)) {
|
||||
$ouHierarchy[$parentDN] = [System.Collections.ArrayList]::new()
|
||||
}
|
||||
$ouHierarchy[$parentDN].Add($ou) | Out-Null
|
||||
}
|
||||
$script:hm_treeViewOUs.BeginUpdate()
|
||||
$script:hm_treeViewOUs.Nodes.Clear()
|
||||
$rootNode = New-Object System.Windows.Forms.TreeNode($script:CurrentADDomainInfo.DnsRoot)
|
||||
$rootNode.Tag = $script:CurrentADDomainDN
|
||||
$script:hm_treeViewOUs.Nodes.Add($rootNode) | Out-Null
|
||||
Build-OU-TreeView -ParentDN $script:CurrentADDomainDN -ParentUiNode $rootNode -OUHierarchy $ouHierarchy
|
||||
$rootNode.Expand()
|
||||
$script:hm_treeViewOUs.EndUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# OU TreeView 에서 노드 선택 시 이벤트
|
||||
$hm_treeViewOUs_AfterSelect = {
|
||||
if ($this.SelectedNode -and $this.SelectedNode.Tag) {
|
||||
$selectedOUDN = $this.SelectedNode.Tag.ToString()
|
||||
$script:hm_textBoxSelectedOU.Text = $selectedOUDN
|
||||
|
||||
# 기본 배경색으로 초기화
|
||||
$script:hm_textBoxSelectedOU.BackColor = [System.Drawing.Color]::White
|
||||
|
||||
# 루트 도메인은 생성 경로로 선택할 수 없음
|
||||
if ($selectedOUDN -eq $script:CurrentADDomainDN) {
|
||||
Write-Log "루트 도메인은 선택할 수 없습니다." -Level "WARNING"
|
||||
$script:hm_textBoxSelectedOU.BackColor = [System.Drawing.Color]::LightGray
|
||||
return
|
||||
}
|
||||
|
||||
# 선택된 OU가 AD Connect 동기화 범위에 포함되는지 확인
|
||||
$isSynchronized = $false
|
||||
foreach ($syncRootOU in $script:SynchronizedOURoots) {
|
||||
if ($selectedOUDN -eq $syncRootOU -or $selectedOUDN.EndsWith(",$syncRootOU", "OrdinalIgnoreCase")) {
|
||||
$isSynchronized = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# 동기화 여부에 따라 배경색 변경으로 사용자에게 시각적 피드백 제공
|
||||
if ($isSynchronized) {
|
||||
$script:hm_textBoxSelectedOU.BackColor = [System.Drawing.Color]::LightGreen
|
||||
} else {
|
||||
Write-Log "주의: 선택된 OU($selectedOUDN)는 동기화 대상이 아닐 수 있습니다!" -Level "WARNING"
|
||||
$script:hm_textBoxSelectedOU.BackColor = [System.Drawing.Color]::LightPink
|
||||
}
|
||||
} else {
|
||||
# 선택 해제 시 텍스트박스 초기화
|
||||
$script:hm_textBoxSelectedOU.Text = ""
|
||||
$script:hm_textBoxSelectedOU.BackColor = [System.Drawing.Color]::White
|
||||
}
|
||||
}
|
||||
|
||||
# '작업 실행' 버튼 클릭 이벤트 (모든 하드 매칭 작업의 시작점)
|
||||
$hm_buttonExecuteAction_Click = {
|
||||
$isSyncMode = $script:hm_currentMode -ne "Idle"
|
||||
$success = Invoke-Synchronous -TriggerControl $this -StatusMessage "하드 매칭 작업을 실행합니다..." -ScriptBlock {
|
||||
if (-not $script:hm_selectedAzureUser) {
|
||||
return $false
|
||||
}
|
||||
|
||||
$onPremUserToMatch = $null
|
||||
|
||||
# 모드 1: 신규 계정 생성 후 매칭
|
||||
if ($script:hm_currentMode -eq "CreateAndMatch") {
|
||||
Write-Log "하드 매칭 모드: 신규 계정 생성 및 매칭"
|
||||
$sam = $script:hm_selectedAzureUser.UserPrincipalName.Split('@')[0]
|
||||
|
||||
# SamAccountName 유효성 및 중복 검사
|
||||
$validationResult = Test-SamAccountName -AccountName $sam
|
||||
if (-not $validationResult.IsValid) {
|
||||
if ($validationResult.Reason -like "*이미 사용 중인 계정명*") {
|
||||
throw "온프레미스에 SamAccountName이 '$sam'인 계정이 이미 존재합니다. 해당 계정과 직접 매칭을 시도하거나, Azure AD 사용자의 UPN을 변경한 후 다시 시도하세요."
|
||||
} else {
|
||||
throw "생성하려는 계정명 '$sam'이 규칙에 맞지 않습니다: $($validationResult.Reason)"
|
||||
}
|
||||
}
|
||||
|
||||
$upn = "$sam@$($script:Configuration.UPNSuffix)"
|
||||
$password = $script:hm_textBoxPassword.Text
|
||||
$targetOU = $script:hm_textBoxSelectedOU.Text
|
||||
if (-not ($password -and $targetOU)) {
|
||||
throw "신규 생성 시 비밀번호와 대상 OU는 필수입니다."
|
||||
}
|
||||
|
||||
# 신규 AD 사용자 생성을 위한 파라미터 준비
|
||||
$newUserParams = @{
|
||||
SamAccountName = $sam
|
||||
UserPrincipalName = $upn
|
||||
Name = $script:hm_selectedAzureUser.DisplayName
|
||||
DisplayName = $script:hm_selectedAzureUser.DisplayName
|
||||
GivenName = $script:hm_textBoxFirstNameKr.Text
|
||||
Surname = $script:hm_textBoxLastNameKr.Text
|
||||
Path = $targetOU
|
||||
EmailAddress = $script:hm_selectedAzureUser.Mail
|
||||
AccountPassword = (ConvertTo-SecureString $password -AsPlainText -Force)
|
||||
Enabled = $true
|
||||
}
|
||||
$onPremUserToMatch = New-ADUser @newUserParams -PassThru -Server $script:Configuration.OnPremDomainController -ErrorAction Stop
|
||||
Write-Log "온프레미스 AD 사용자 '$($onPremUserToMatch.Name)' 생성 성공."
|
||||
|
||||
# 모드 2: 기존 계정과 매칭
|
||||
} elseif ($script:hm_currentMode -eq "MatchExisting") {
|
||||
Write-Log "하드 매칭 모드: 기존 계정과 매칭"
|
||||
$onPremUserToMatch = $script:hm_foundOnPremUser
|
||||
} else {
|
||||
throw "알 수 없는 작업 모드입니다: $($script:hm_currentMode)"
|
||||
}
|
||||
|
||||
if (-not $onPremUserToMatch) {
|
||||
throw "매칭할 온프레미스 AD 계정을 식별할 수 없습니다."
|
||||
}
|
||||
|
||||
# 대상 온프레미스 계정의 ObjectGUID를 ImmutableId로 변환하여 Azure AD에 설정
|
||||
$immutableId = [System.Convert]::ToBase64String((Get-ADUser -Identity $onPremUserToMatch.SamAccountName -Properties objectGUID).ObjectGUID.ToByteArray())
|
||||
Write-Log "하드 매칭 실행: '$($script:hm_selectedAzureUser.UserPrincipalName)'의 ImmutableId를 '$immutableId'(으)로 설정합니다."
|
||||
Set-MgUser -UserId $script:hm_selectedAzureUser.Id -OnPremisesImmutableId $immutableId -ErrorAction Stop
|
||||
|
||||
# 즉시 델타 동기화 실행
|
||||
Invoke-AadConnectSync -PolicyType Delta
|
||||
|
||||
# 최종 검증: ImmutableId가 올바르게 업데이트되었는지 확인 (최대 30초 대기)
|
||||
Write-Log "검증 단계: 업데이트된 ImmutableId를 다시 조회합니다 (최대 30초 대기)..."
|
||||
for ($i = 0; $i -lt 3; $i++) {
|
||||
Start-Sleep -Seconds 10
|
||||
$refreshedUser = Get-MgUser -UserId $script:hm_selectedAzureUser.Id -Property "onPremisesImmutableId" -ErrorAction SilentlyContinue
|
||||
if ($refreshedUser.onPremisesImmutableId -eq $immutableId) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
throw "검증 실패: ImmutableId가 업데이트되지 않았습니다."
|
||||
} -RequiresAzureAD:$isSyncMode
|
||||
|
||||
if ($success) {
|
||||
[System.Windows.Forms.MessageBox]::Show("하드 매칭 및 동기화, 최종 검증까지 성공적으로 완료되었습니다.", "모든 작업 성공", "OK", "Information") | Out-Null
|
||||
Reset-HardMatchTab
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
Write-Host "UI-Tab-HardMatch.ps1 로드 완료." -ForegroundColor Cyan
|
||||
9
config.json
Normal file
9
config.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"OnPremDomainController": "oneunivrs.corp",
|
||||
"AADConnectServerName": "DC001",
|
||||
"AzureTenantId": "1e8605cc-8007-46b0-993f-b388917f9499",
|
||||
"UPNSuffix": "oneunivrs.com",
|
||||
"SynchronizedOUs": "Dev;Newbie;액션스퀘어;원유니버스",
|
||||
"DefaultPassword": "action!1234",
|
||||
"DefaultUsageLocation": "KR"
|
||||
}
|
||||
64
logs/ad_tool_2025-06-17.log
Normal file
64
logs/ad_tool_2025-06-17.log
Normal file
@ -0,0 +1,64 @@
|
||||
2025-06-17 12:33:05 [INFO]: AD/M365 통합 관리 도구(v1.13)를 시작합니다.
|
||||
2025-06-17 12:33:12 [INFO]: 설정 정보 저장 완료.
|
||||
2025-06-17 12:33:12 [INFO]: 스크립트 환경 초기화 시작.
|
||||
2025-06-17 12:33:12 [INFO]: 온프레미스 AD 도메인 정보 로드 성공: oneunivrs
|
||||
2025-06-17 12:33:12 [INFO]: 동기화 OU 루트 추가: OU=Dev,DC=oneunivrs,DC=corp
|
||||
2025-06-17 12:33:12 [INFO]: 동기화 OU 루트 추가: OU=Newbie,DC=oneunivrs,DC=corp
|
||||
2025-06-17 12:33:12 [INFO]: 동기화 OU 루트 추가: OU=액션스퀘어,DC=oneunivrs,DC=corp
|
||||
2025-06-17 12:33:12 [INFO]: 동기화 OU 루트 추가: OU=원유니버스,DC=oneunivrs,DC=corp
|
||||
2025-06-17 12:33:12 [INFO]: Azure AD 자동 연결을 시도합니다...
|
||||
2025-06-17 12:33:16 [INFO]: Azure AD 자동 연결 성공.
|
||||
2025-06-17 12:33:16 [INFO]: 라이선스 목록 조회 중...
|
||||
2025-06-17 12:33:19 [INFO]: [계정 생성] 라이선스 목록 갱신 완료.
|
||||
2025-06-17 12:33:19 [INFO]: [계정 생성] OU 목록을 조회합니다...
|
||||
2025-06-17 12:33:19 [INFO]: [계정 생성] OU 목록 로드/갱신 완료.
|
||||
2025-06-17 12:33:39 [INFO]: 온프레미스 AD에서 사용자 '철민' 검색 중...
|
||||
2025-06-17 12:33:39 [INFO]: 1명의 사용자를 찾았습니다.
|
||||
2025-06-17 12:33:44 [INFO]: 온프레미스 AD에서 사용자 '범진' 검색 중...
|
||||
2025-06-17 12:33:44 [INFO]: 1명의 사용자를 찾았습니다.
|
||||
2025-06-17 12:34:28 [INFO]: 프로그램을 종료합니다.
|
||||
2025-06-17 12:40:30 [INFO]: AD/M365 통합 관리 도구(v1.13)를 시작합니다.
|
||||
2025-06-17 12:40:42 [INFO]: 설정 정보 저장 완료.
|
||||
2025-06-17 12:40:42 [INFO]: 스크립트 환경 초기화 시작.
|
||||
2025-06-17 12:40:43 [INFO]: 온프레미스 AD 도메인 정보 로드 성공: oneunivrs
|
||||
2025-06-17 12:40:43 [INFO]: 동기화 OU 루트 추가: OU=Dev,DC=oneunivrs,DC=corp
|
||||
2025-06-17 12:40:43 [INFO]: 동기화 OU 루트 추가: OU=Newbie,DC=oneunivrs,DC=corp
|
||||
2025-06-17 12:40:43 [INFO]: 동기화 OU 루트 추가: OU=액션스퀘어,DC=oneunivrs,DC=corp
|
||||
2025-06-17 12:40:43 [INFO]: 동기화 OU 루트 추가: OU=원유니버스,DC=oneunivrs,DC=corp
|
||||
2025-06-17 12:40:43 [INFO]: Azure AD 자동 연결을 시도합니다...
|
||||
2025-06-17 12:40:43 [INFO]: Azure AD 자동 연결 성공.
|
||||
2025-06-17 12:40:43 [INFO]: 라이선스 목록 조회 중...
|
||||
2025-06-17 12:40:43 [INFO]: [계정 생성] 라이선스 목록 갱신 완료.
|
||||
2025-06-17 12:40:43 [INFO]: [계정 생성] OU 목록을 조회합니다...
|
||||
2025-06-17 12:40:43 [INFO]: [계정 생성] OU 목록 로드/갱신 완료.
|
||||
2025-06-17 12:40:57 [INFO]: 프로그램을 종료합니다.
|
||||
2025-06-17 12:52:43 [INFO]: AD/M365 통합 관리 도구를 시작합니다.
|
||||
2025-06-17 12:52:59 [INFO]: 설정 정보 저장 완료.
|
||||
2025-06-17 12:52:59 [INFO]: 스크립트 환경 초기화 시작.
|
||||
2025-06-17 12:52:59 [INFO]: 온프레미스 AD 도메인 정보 로드 성공: oneunivrs
|
||||
2025-06-17 12:52:59 [INFO]: 동기화 OU 루트 추가: OU=Dev,DC=oneunivrs,DC=corp
|
||||
2025-06-17 12:52:59 [INFO]: 동기화 OU 루트 추가: OU=Newbie,DC=oneunivrs,DC=corp
|
||||
2025-06-17 12:52:59 [INFO]: 동기화 OU 루트 추가: OU=액션스퀘어,DC=oneunivrs,DC=corp
|
||||
2025-06-17 12:52:59 [INFO]: 동기화 OU 루트 추가: OU=원유니버스,DC=oneunivrs,DC=corp
|
||||
2025-06-17 12:52:59 [INFO]: Azure AD 자동 연결을 시도합니다...
|
||||
2025-06-17 12:52:59 [INFO]: Azure AD 자동 연결 성공.
|
||||
2025-06-17 12:52:59 [INFO]: 라이선스 목록 조회 중...
|
||||
2025-06-17 12:53:00 [INFO]: [계정 생성] 라이선스 목록 갱신 완료.
|
||||
2025-06-17 12:53:00 [INFO]: [계정 생성] OU 목록을 조회합니다...
|
||||
2025-06-17 12:53:00 [INFO]: [계정 생성] OU 목록 로드/갱신 완료.
|
||||
2025-06-17 12:53:15 [INFO]: 계정 생성 작업을 실행합니다...
|
||||
2025-06-17 12:53:15 [INFO]: 온프레미스 AD 사용자 생성을 시작합니다: mrkim
|
||||
2025-06-17 12:53:16 [INFO]: 온프레미스 AD 사용자 'mrkim' 생성 성공.
|
||||
2025-06-17 12:53:16 [INFO]: AAD Connect 동기화 실행 중...
|
||||
2025-06-17 12:53:18 [SUCCESS]: AAD Connect 동기화 명령 전송 완료. 클라우드 적용까지 시간이 소요될 수 있습니다.
|
||||
2025-06-17 12:53:18 [INFO]: Azure AD에서 사용자 'mrkim@oneunivrs.com' 확인 시도 (최대 120초 대기)...
|
||||
2025-06-17 12:53:18 [INFO]: Azure AD에서 사용자 확인 중... (0초 경과)
|
||||
2025-06-17 12:53:28 [INFO]: Azure AD에서 사용자 확인 중... (10초 경과)
|
||||
2025-06-17 12:53:38 [INFO]: Azure AD에서 사용자 확인 중... (20초 경과)
|
||||
2025-06-17 12:53:38 [SUCCESS]: 사용자 'mrkim@oneunivrs.com'을(를) Azure AD에서 찾았습니다! (소요 시간: 20초)
|
||||
2025-06-17 12:53:39 [INFO]: 사용 위치 설정 중: KR
|
||||
2025-06-17 12:53:55 [INFO]: 라이선스 목록 조회 중...
|
||||
2025-06-17 12:53:55 [INFO]: [계정 생성] 라이선스 목록 갱신 완료.
|
||||
2025-06-17 12:54:07 [INFO]: 온프레미스 AD에서 사용자 '가네' 검색 중...
|
||||
2025-06-17 12:54:08 [INFO]: 1명의 사용자를 찾았습니다.
|
||||
2025-06-17 12:57:47 [INFO]: 프로그램을 종료합니다.
|
||||
Reference in New Issue
Block a user