# ================================================================================ # 파일: 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