# ================================================================================ # 파일: 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 "프로그램을 종료합니다."