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