diff --git a/CHANGELOG.md b/CHANGELOG.md
index 95d74c9..069d5c4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,19 @@
# Enterprise Admin Studio - 변경 이력 (CHANGELOG.md)
+## [v1.6.0] - 2026-10-21
+### Added
+- **MS SQL Server AD 도메인 계정 기반 Windows 통합 인증 위장(Impersonation) 엔진 탑재**:
+ - `Modules/SqlService.ps1`: Win32 `advapi32.dll`의 `LogonUser` API(Type 9 `LOGON32_LOGON_NEW_CREDENTIALS`, `/netonly`)와 `WindowsIdentity.RunImpersonated`를 결합하여, 사용자가 입력한 AD 도메인 ID/PW를 Windows 보안 토큰으로 위장한 뒤 SQL Server에 `Integrated Security=True`로 인증하는 엔진 구현
+ - `Test-SqlConnection`: SQL Server가 실제로 누구로 인증했는지 `SELECT SUSER_SNAME()`을 질의하여 반환 메시지에 실시간 출력
+ - `Get-SqlDatabases`, `Invoke-SqlBackupAction`, `Invoke-SqlQuerySnippet`: 모든 SQL 작업이 설정된 인증 컨텍스트(`Invoke-WithSqlAuthContext`) 하에서 안전하게 실행되도록 통합
+- **환경설정 UI (`Views/SettingsView.xaml`) 및 SQL 관리 화면 (`Views/SqlDatabaseView.xaml`) 연동**:
+ - 인증 방식 선택 옵션 3종 확장:
+ 1) 현재 PC Windows 통합 인증 (Current Session)
+ 2) AD 도메인 계정 위장 Windows 인증 (AD LogonUser /netonly)
+ 3) SQL Server 자체 계정 인증 (User ID / Password)
+ - `SettingsView.xaml`: `TxtSqlDomain` (AD 도메인 입력칸), `LblSqlDomain`, 동적 설명 힌트(`TxtSqlAuthHint`) 및 선택 변경에 따른 입력 필드 실시간 활성화/비활성화 처리 연동
+ - `config.json`: `MsSql.Domain` 필드 추가 및 영속적 저장 지원
+
## [v1.5.0] - 2026-10-20
### Added
- **다중 도메인 컨트롤러(6대 DC) 환경 NetApp 활성 DC 감지 및 복제 지연(Replication Lag) 방지 전략 탑재**:
diff --git a/Modules/SqlService.ps1 b/Modules/SqlService.ps1
index 47f5dc6..bfd8acc 100644
--- a/Modules/SqlService.ps1
+++ b/Modules/SqlService.ps1
@@ -1,6 +1,99 @@
# Modules\SqlService.ps1
# Microsoft SQL Server Management Service Module (.NET System.Data.SqlClient Live & Fallback)
+# P/Invoke helper for Windows Token Impersonation (AD Domain user outbound network authentication)
+$winAuthTypeDef = @"
+using System;
+using System.Runtime.InteropServices;
+using System.Security.Principal;
+using Microsoft.Win32.SafeHandles;
+
+public class WinAuthUniversalImpersonator {
+ [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ public static extern bool LogonUser(
+ string lpszUsername,
+ string lpszDomain,
+ string lpszPassword,
+ int dwLogonType,
+ int dwLogonProvider,
+ out IntPtr phToken
+ );
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static extern bool CloseHandle(IntPtr handle);
+
+ public const int LOGON32_LOGON_NEW_CREDENTIALS = 9; // /netonly network outbound impersonation
+ public const int LOGON32_PROVIDER_DEFAULT = 0;
+
+ public static void RunImpersonatedAction(string username, string domain, string password, Action action) {
+ IntPtr tokenHandle = IntPtr.Zero;
+ bool success = LogonUser(username, domain, password, LOGON32_LOGON_NEW_CREDENTIALS, LOGON32_PROVIDER_DEFAULT, out tokenHandle);
+ if (!success || tokenHandle == IntPtr.Zero) {
+ int err = Marshal.GetLastWin32Error();
+ throw new InvalidOperationException("AD 도메인 로그온(LogonUser) 실패: Win32 에러 코드 " + err);
+ }
+
+ try {
+ using (SafeAccessTokenHandle safeHandle = new SafeAccessTokenHandle(tokenHandle)) {
+ WindowsIdentity.RunImpersonated(safeHandle, action);
+ }
+ }
+ finally {
+ // SafeAccessTokenHandle owns and closes tokenHandle
+ }
+ }
+}
+"@
+
+if (-not ([System.Management.Automation.PSTypeName]"WinAuthUniversalImpersonator").Type) {
+ Add-Type -TypeDefinition $winAuthTypeDef
+}
+
+function Invoke-WithSqlAuthContext {
+ param(
+ [string]$AuthType = "Integrated",
+ [string]$Domain = "",
+ [string]$Username = "",
+ [string]$Password = "",
+ [scriptblock]$Action
+ )
+
+ if ($AuthType -eq "AdImpersonated" -and -not [string]::IsNullOrWhiteSpace($Username) -and -not [string]::IsNullOrWhiteSpace($Password)) {
+ $cleanDomain = $Domain
+ $cleanUser = $Username
+
+ if ($cleanUser -like "*\*") {
+ $parts = $cleanUser -split "\\", 2
+ $cleanDomain = $parts[0]
+ $cleanUser = $parts[1]
+ } elseif ($cleanUser -like "*@*") {
+ $parts = $cleanUser -split "@", 2
+ $cleanUser = $parts[0]
+ $cleanDomain = $parts[1]
+ }
+ if ([string]::IsNullOrWhiteSpace($cleanDomain)) {
+ $cleanDomain = "."
+ }
+
+ $resHolder = [hashtable]@{ Result = $null; Exception = $null }
+ [WinAuthUniversalImpersonator]::RunImpersonatedAction($cleanUser, $cleanDomain, $Password, [Action]{
+ try {
+ $resHolder.Result = & $Action
+ } catch {
+ $resHolder.Exception = $_
+ }
+ })
+
+ if ($resHolder.Exception) {
+ throw $resHolder.Exception
+ }
+ return $resHolder.Result
+ } else {
+ return & $Action
+ }
+}
+
function Build-SqlConnectionString {
param(
[string]$Server,
@@ -17,7 +110,8 @@ function Build-SqlConnectionString {
$builder["Connect Timeout"] = $Timeout
$builder["Application Name"] = "EnterpriseAdminStudio"
- if ($AuthType -eq "Integrated" -or [string]::IsNullOrWhiteSpace($Username)) {
+ # Integrated(현재 세션) 및 AdImpersonated(AD 위장 토큰)는 SQL Server 관점에서 둘 다 Windows 통합 인증(SSPI)
+ if ($AuthType -eq "Integrated" -or $AuthType -eq "AdImpersonated" -or [string]::IsNullOrWhiteSpace($Username)) {
$builder["Integrated Security"] = $true
} else {
$builder["Integrated Security"] = $false
@@ -32,39 +126,54 @@ function Test-SqlConnection {
param(
[string]$Server,
[string]$AuthType = "Integrated",
+ [string]$Domain = "",
[string]$Username = "",
[string]$Password = "",
[int]$Timeout = 5
)
- $connStr = Build-SqlConnectionString -Server $Server -AuthType $AuthType -Username $Username -Password $Password -Timeout $Timeout
- $conn = New-Object System.Data.SqlClient.SqlConnection($connStr)
-
try {
- $conn.Open()
- $cmd = $conn.CreateCommand()
- $cmd.CommandText = "SELECT @@VERSION AS [Version], SERVERPROPERTY('ProductVersion') AS [ProductVersion], SERVERPROPERTY('Edition') AS [Edition]"
- $reader = $cmd.ExecuteReader()
- if ($reader.Read()) {
- $ver = $reader["ProductVersion"].ToString()
- $edition = $reader["Edition"].ToString()
- return [PSCustomObject]@{
- Success = $true
- Message = "SQL Server 연결 성공 - 버전: $ver ($edition)"
- Version = "$ver ($edition)"
+ $connStr = Build-SqlConnectionString -Server $Server -AuthType $AuthType -Username $Username -Password $Password -Timeout $Timeout
+
+ $execResult = Invoke-WithSqlAuthContext -AuthType $AuthType -Domain $Domain -Username $Username -Password $Password -Action {
+ $conn = New-Object System.Data.SqlClient.SqlConnection($connStr)
+ try {
+ $conn.Open()
+ $cmd = $conn.CreateCommand()
+ $cmd.CommandText = "SELECT SUSER_SNAME() AS [LoginName], @@VERSION AS [Version], SERVERPROPERTY('ProductVersion') AS [ProductVersion], SERVERPROPERTY('Edition') AS [Edition]"
+ $reader = $cmd.ExecuteReader()
+ if ($reader.Read()) {
+ $loginName = $reader["LoginName"].ToString()
+ $ver = $reader["ProductVersion"].ToString()
+ $edition = $reader["Edition"].ToString()
+
+ $authDesc = switch ($AuthType) {
+ "AdImpersonated" { "AD 계정 위장 Windows 통합 인증 ($loginName)" }
+ "Integrated" { "현재 세션 Windows 통합 인증 ($loginName)" }
+ default { "SQL 계정 인증 ($loginName)" }
+ }
+
+ return [PSCustomObject]@{
+ Success = $true
+ Message = "SQL Server 연결 성공!`n- 인증 방식: $authDesc`n- 서버 버전: $ver ($edition)"
+ LoginName = $loginName
+ Version = "$ver ($edition)"
+ }
+ }
+ return [PSCustomObject]@{ Success = $true; Message = "SQL Server 연결 성공" }
+ } finally {
+ if ($conn.State -eq [System.Data.ConnectionState]::Open) {
+ $conn.Close()
+ }
+ $conn.Dispose()
}
}
- return [PSCustomObject]@{ Success = $true; Message = "SQL Server 연결 성공" }
+ return $execResult
} catch {
return [PSCustomObject]@{
Success = $false
Message = "SQL Server 연결 실패: $($_.Exception.Message)"
}
- } finally {
- if ($conn.State -eq [System.Data.ConnectionState]::Open) {
- $conn.Close()
- }
- $conn.Dispose()
}
}
@@ -79,17 +188,20 @@ function Get-SqlDatabases {
if ($useLive) {
$server = $Config.MsSql.Server
$auth = $Config.MsSql.AuthType
+ $domain = if ($Config.MsSql.Domain) { $Config.MsSql.Domain } else { "" }
$user = $Config.MsSql.Username
$pass = $Config.MsSql.Password
$timeout = if ($Config.MsSql.ConnectionTimeout) { [int]$Config.MsSql.ConnectionTimeout } else { 3 }
- $connStr = Build-SqlConnectionString -Server $server -AuthType $auth -Username $user -Password $pass -Timeout $timeout
- $conn = New-Object System.Data.SqlClient.SqlConnection($connStr)
-
try {
- $conn.Open()
- $cmd = $conn.CreateCommand()
- $cmd.CommandText = @"
+ $connStr = Build-SqlConnectionString -Server $server -AuthType $auth -Username $user -Password $pass -Timeout $timeout
+
+ $dbList = Invoke-WithSqlAuthContext -AuthType $auth -Domain $domain -Username $user -Password $pass -Action {
+ $conn = New-Object System.Data.SqlClient.SqlConnection($connStr)
+ try {
+ $conn.Open()
+ $cmd = $conn.CreateCommand()
+ $cmd.CommandText = @"
SELECT
d.name AS DatabaseName,
d.state_desc AS StateDesc,
@@ -103,32 +215,35 @@ LEFT JOIN msdb.dbo.backupset b ON d.name = b.database_name AND b.type = 'D'
GROUP BY d.name, d.state_desc, d.recovery_model_desc, d.collation_name, b.type
ORDER BY d.name;
"@
- $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd)
- $table = New-Object System.Data.DataTable
- $adapter.Fill($table) | Out-Null
+ $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd)
+ $table = New-Object System.Data.DataTable
+ $adapter.Fill($table) | Out-Null
- $list = [System.Collections.Generic.List[PSCustomObject]]::new()
- foreach ($row in $table.Rows) {
- $dbName = $row["DatabaseName"]
- if ([string]::IsNullOrWhiteSpace($Filter) -or $dbName -like "*$Filter*") {
- $list.Add([PSCustomObject]@{
- DatabaseName = $dbName
- StateDesc = $row["StateDesc"]
- TotalSizeMB = $row["TotalSizeMB"]
- FreeSpaceMB = "실시간 계산"
- RecoveryModel = $row["RecoveryModel"]
- CollationName = $row["CollationName"]
- LastBackupDate = $row["LastBackupDate"]
- IsLive = $true
- })
+ $list = [System.Collections.Generic.List[PSCustomObject]]::new()
+ foreach ($row in $table.Rows) {
+ $dbName = $row["DatabaseName"]
+ if ([string]::IsNullOrWhiteSpace($Filter) -or $dbName -like "*$Filter*") {
+ $list.Add([PSCustomObject]@{
+ DatabaseName = $dbName
+ StateDesc = $row["StateDesc"]
+ TotalSizeMB = $row["TotalSizeMB"]
+ FreeSpaceMB = "실시간 계산"
+ RecoveryModel = $row["RecoveryModel"]
+ CollationName = $row["CollationName"]
+ LastBackupDate = $row["LastBackupDate"]
+ IsLive = $true
+ })
+ }
+ }
+ return $list.ToArray()
+ } finally {
+ if ($conn.State -eq [System.Data.ConnectionState]::Open) { $conn.Close() }
+ $conn.Dispose()
}
}
- return $list.ToArray()
+ return $dbList
} catch {
Write-Warning "실제 SQL Server 조회 실패: $($_.Exception.Message) (Mock 데이터로 전환합니다)"
- } finally {
- if ($conn.State -eq [System.Data.ConnectionState]::Open) { $conn.Close() }
- $conn.Dispose()
}
}
@@ -147,6 +262,7 @@ function Invoke-SqlBackupAction {
if ($useLive) {
$server = $Config.MsSql.Server
$auth = $Config.MsSql.AuthType
+ $domain = if ($Config.MsSql.Domain) { $Config.MsSql.Domain } else { "" }
$user = $Config.MsSql.Username
$pass = $Config.MsSql.Password
$backupDir = if ($Config.MsSql.BackupDirectory) { $Config.MsSql.BackupDirectory } else { "C:\Backups" }
@@ -159,31 +275,36 @@ function Invoke-SqlBackupAction {
$fileName = "$DatabaseName`_$timeStamp.bak"
$targetPath = Join-Path $backupDir $fileName
- $connStr = Build-SqlConnectionString -Server $server -AuthType $auth -Username $user -Password $pass -Timeout 30
- $conn = New-Object System.Data.SqlClient.SqlConnection($connStr)
-
try {
- $conn.Open()
- $cmd = $conn.CreateCommand()
- $cmd.CommandTimeout = 300 # 백업은 5분 타임아웃
- $cmd.CommandText = "BACKUP DATABASE [$DatabaseName] TO DISK = N'$targetPath' WITH INIT, STATS = 10, NAME = N'$DatabaseName-Full Backup'"
- $cmd.ExecuteNonQuery() | Out-Null
+ $connStr = Build-SqlConnectionString -Server $server -AuthType $auth -Username $user -Password $pass -Timeout 30
- return [PSCustomObject]@{
- Success = $true
- Database = $DatabaseName
- BackupPath = $targetPath
- Message = "실제 SQL Server 데이터베이스 [$DatabaseName] 백업이 완료되었습니다.`n저장 경로: $targetPath"
+ $res = Invoke-WithSqlAuthContext -AuthType $auth -Domain $domain -Username $user -Password $pass -Action {
+ $conn = New-Object System.Data.SqlClient.SqlConnection($connStr)
+ try {
+ $conn.Open()
+ $cmd = $conn.CreateCommand()
+ $cmd.CommandTimeout = 300 # 백업은 5분 타임아웃
+ $cmd.CommandText = "BACKUP DATABASE [$DatabaseName] TO DISK = N'$targetPath' WITH INIT, STATS = 10, NAME = N'$DatabaseName-Full Backup'"
+ $cmd.ExecuteNonQuery() | Out-Null
+
+ return [PSCustomObject]@{
+ Success = $true
+ Database = $DatabaseName
+ BackupPath = $targetPath
+ Message = "실제 SQL Server 데이터베이스 [$DatabaseName] 백업이 완료되었습니다.`n저장 경로: $targetPath"
+ }
+ } finally {
+ if ($conn.State -eq [System.Data.ConnectionState]::Open) { $conn.Close() }
+ $conn.Dispose()
+ }
}
+ return $res
} catch {
return [PSCustomObject]@{
Success = $false
Database = $DatabaseName
Message = "SQL 백업 실패: $($_.Exception.Message)"
}
- } finally {
- if ($conn.State -eq [System.Data.ConnectionState]::Open) { $conn.Close() }
- $conn.Dispose()
}
}
@@ -206,37 +327,43 @@ function Invoke-SqlQuerySnippet {
if ($useLive) {
$server = $Config.MsSql.Server
$auth = $Config.MsSql.AuthType
+ $domain = if ($Config.MsSql.Domain) { $Config.MsSql.Domain } else { "" }
$user = $Config.MsSql.Username
$pass = $Config.MsSql.Password
- $connStr = Build-SqlConnectionString -Server $server -AuthType $auth -Username $user -Password $pass -Timeout 15
- $conn = New-Object System.Data.SqlClient.SqlConnection($connStr)
-
try {
- $conn.Open()
- $cmd = $conn.CreateCommand()
- $cmd.CommandText = $Query
- $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd)
- $table = New-Object System.Data.DataTable
- $adapter.Fill($table) | Out-Null
+ $connStr = Build-SqlConnectionString -Server $server -AuthType $auth -Username $user -Password $pass -Timeout 15
- $sw = [System.IO.StringWriter]::new()
- $table | Format-Table -AutoSize | Out-String | ForEach-Object { $sw.WriteLine($_) }
-
- return [PSCustomObject]@{
- Success = $true
- RowCount = $table.Rows.Count
- Output = "(성공: $($table.Rows.Count)개 행 반환됨 - $((Get-Date).ToString('HH:mm:ss')))`n$($sw.ToString())"
+ $res = Invoke-WithSqlAuthContext -AuthType $auth -Domain $domain -Username $user -Password $pass -Action {
+ $conn = New-Object System.Data.SqlClient.SqlConnection($connStr)
+ try {
+ $conn.Open()
+ $cmd = $conn.CreateCommand()
+ $cmd.CommandText = $Query
+ $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd)
+ $table = New-Object System.Data.DataTable
+ $adapter.Fill($table) | Out-Null
+
+ $sw = [System.IO.StringWriter]::new()
+ $table | Format-Table -AutoSize | Out-String | ForEach-Object { $sw.WriteLine($_) }
+
+ return [PSCustomObject]@{
+ Success = $true
+ RowCount = $table.Rows.Count
+ Output = "(성공: $($table.Rows.Count)개 행 반환됨 - $((Get-Date).ToString('HH:mm:ss')))`n$($sw.ToString())"
+ }
+ } finally {
+ if ($conn.State -eq [System.Data.ConnectionState]::Open) { $conn.Close() }
+ $conn.Dispose()
+ }
}
+ return $res
} catch {
return [PSCustomObject]@{
Success = $false
RowCount = 0
Output = "(오류 발생)`n$($_.Exception.Message)"
}
- } finally {
- if ($conn.State -eq [System.Data.ConnectionState]::Open) { $conn.Close() }
- $conn.Dispose()
}
}
diff --git a/Start-App.ps1 b/Start-App.ps1
index d6e203a..b31f490 100644
--- a/Start-App.ps1
+++ b/Start-App.ps1
@@ -228,6 +228,7 @@ $script:loadSettingsToUI = {
$txtSqlServer = $v.FindName("TxtSqlServer")
$cmbSqlAuth = $v.FindName("CmbSqlAuthType")
$txtSqlBackupDir = $v.FindName("TxtSqlBackupDir")
+ $txtSqlDomain = $v.FindName("TxtSqlDomain")
$txtSqlUser = $v.FindName("TxtSqlUser")
$txtSqlPassword = $v.FindName("TxtSqlPassword")
@@ -250,7 +251,14 @@ $script:loadSettingsToUI = {
if ($chkSqlLive) { $chkSqlLive.IsChecked = [bool]$c.MsSql.UseLive }
if ($txtSqlServer) { $txtSqlServer.Text = $c.MsSql.Server }
- if ($cmbSqlAuth) { $cmbSqlAuth.SelectedIndex = if ($c.MsSql.AuthType -eq "Sql") { 1 } else { 0 } }
+ if ($txtSqlDomain) { $txtSqlDomain.Text = if ($c.MsSql.Domain) { $c.MsSql.Domain } else { "corp" } }
+ if ($cmbSqlAuth) {
+ $cmbSqlAuth.SelectedIndex = switch ($c.MsSql.AuthType) {
+ "AdImpersonated" { 1 }
+ "Sql" { 2 }
+ default { 0 }
+ }
+ }
if ($txtSqlBackupDir) { $txtSqlBackupDir.Text = $c.MsSql.BackupDirectory }
if ($txtSqlUser) { $txtSqlUser.Text = $c.MsSql.Username }
if ($txtSqlPassword) { $txtSqlPassword.Password = $c.MsSql.Password }
@@ -1152,12 +1160,22 @@ function Navigate-ToPage {
$btnTestConn = $view.FindName("BtnTestSqlConnection")
$btnBackup = $view.FindName("BtnBackupDb")
$btnExecQuery = $view.FindName("BtnExecuteQuery")
+ $cmbSqlAuthInDb = $view.FindName("CmbSqlAuth")
+
+ if ($cmbSqlAuthInDb) {
+ $cmbSqlAuthInDb.SelectedIndex = switch ($global:AppConfig.MsSql.AuthType) {
+ "AdImpersonated" { 1 }
+ "Sql" { 2 }
+ default { 0 }
+ }
+ }
if ($btnSqlRefresh) { $btnSqlRefresh.add_Click({ & $script:reloadSql }) }
if ($btnTestConn) {
$btnTestConn.add_Click({
- $res = Test-SqlConnection -Server $global:AppConfig.MsSql.Server -AuthType $global:AppConfig.MsSql.AuthType -Username $global:AppConfig.MsSql.Username -Password $global:AppConfig.MsSql.Password
+ $cfg = $global:AppConfig.MsSql
+ $res = Test-SqlConnection -Server $cfg.Server -AuthType $cfg.AuthType -Domain $cfg.Domain -Username $cfg.Username -Password $cfg.Password
$img = if ($res.Success) { [System.Windows.MessageBoxImage]::Information } else { [System.Windows.MessageBoxImage]::Error }
[System.Windows.MessageBox]::Show($res.Message, "MS SQL 연결 테스트", [System.Windows.MessageBoxButton]::OK, $img)
})
@@ -1209,6 +1227,58 @@ function Navigate-ToPage {
& $script:loadSettingsToUI
+ $updateSqlAuthUi = {
+ $v = $ViewCache["Settings"]
+ if (-not $v) { return }
+ $cmb = $v.FindName("CmbSqlAuthType")
+ $txtHint = $v.FindName("TxtSqlAuthHint")
+ $lblDomain = $v.FindName("LblSqlDomain")
+ $txtDomain = $v.FindName("TxtSqlDomain")
+ $lblUser = $v.FindName("LblSqlUser")
+ $txtUser = $v.FindName("TxtSqlUser")
+ $lblPass = $v.FindName("LblSqlPassword")
+ $txtPass = $v.FindName("TxtSqlPassword")
+
+ switch ($cmb.SelectedIndex) {
+ 1 {
+ # AdImpersonated
+ if ($txtHint) { $txtHint.Text = "입력한 AD 도메인 ID/PW를 Windows 토큰(/netonly)으로 위장하여 SQL Server에 Windows 통합 인증으로 접속합니다." }
+ if ($lblDomain) { $lblDomain.Text = "AD 도메인 (예: corp 또는 corp.local):" }
+ if ($lblUser) { $lblUser.Text = "AD 도메인 계정 ID (예: sql_admin):" }
+ if ($lblPass) { $lblPass.Text = "AD 도메인 계정 암호:" }
+ if ($txtDomain) { $txtDomain.IsEnabled = $true; $txtDomain.Opacity = 1.0 }
+ if ($txtUser) { $txtUser.IsEnabled = $true; $txtUser.Opacity = 1.0 }
+ if ($txtPass) { $txtPass.IsEnabled = $true; $txtPass.Opacity = 1.0 }
+ }
+ 2 {
+ # Sql Auth
+ if ($txtHint) { $txtHint.Text = "SQL Server 자체 계정(sa 또는 DB 전용 계정)과 암호로 접속합니다." }
+ if ($lblDomain) { $lblDomain.Text = "AD 도메인 (SQL 인증 시 불필요):" }
+ if ($lblUser) { $lblUser.Text = "SQL Server 계정 ID (예: sa):" }
+ if ($lblPass) { $lblPass.Text = "SQL Server 계정 암호:" }
+ if ($txtDomain) { $txtDomain.IsEnabled = $false; $txtDomain.Opacity = 0.5 }
+ if ($txtUser) { $txtUser.IsEnabled = $true; $txtUser.Opacity = 1.0 }
+ if ($txtPass) { $txtPass.IsEnabled = $true; $txtPass.Opacity = 1.0 }
+ }
+ default {
+ # Integrated (Current Windows User)
+ if ($txtHint) { $txtHint.Text = "현재 윈도우에 로그인된 사용자 자격증명(Kerberos/NTLM)으로 SQL Server에 접속합니다." }
+ if ($lblDomain) { $lblDomain.Text = "AD 도메인 (현재 세션 인증 시 불필요):" }
+ if ($lblUser) { $lblUser.Text = "로그인 계정 (현재 Windows 로그인 토큰 사용):" }
+ if ($lblPass) { $lblPass.Text = "비밀번호 (현재 세션 인증 시 불필요):" }
+ if ($txtDomain) { $txtDomain.IsEnabled = $false; $txtDomain.Opacity = 0.5 }
+ if ($txtUser) { $txtUser.IsEnabled = $false; $txtUser.Opacity = 0.5 }
+ if ($txtPass) { $txtPass.IsEnabled = $false; $txtPass.Opacity = 0.5 }
+ }
+ }
+ }
+
+ $cmbSqlAuth = $view.FindName("CmbSqlAuthType")
+ if ($cmbSqlAuth) {
+ $cmbSqlAuth.add_SelectionChanged({ & $updateSqlAuthUi })
+ }
+ & $updateSqlAuthUi
+
# Test AD
if ($btnTestAd) {
$btnTestAd.add_Click({
@@ -1246,11 +1316,16 @@ function Navigate-ToPage {
$v = $ViewCache["Settings"]
$srv = $v.FindName("TxtSqlServer").Text
$cmb = $v.FindName("CmbSqlAuthType")
- $authType = if ($cmb.SelectedIndex -eq 1) { "Sql" } else { "Integrated" }
+ $authType = switch ($cmb.SelectedIndex) {
+ 1 { "AdImpersonated" }
+ 2 { "Sql" }
+ default { "Integrated" }
+ }
+ $domain = $v.FindName("TxtSqlDomain").Text
$user = $v.FindName("TxtSqlUser").Text
$pass = $v.FindName("TxtSqlPassword").Password
- $res = Test-SqlConnection -Server $srv -AuthType $authType -Username $user -Password $pass
+ $res = Test-SqlConnection -Server $srv -AuthType $authType -Domain $domain -Username $user -Password $pass
$img = if ($res.Success) { [System.Windows.MessageBoxImage]::Information } else { [System.Windows.MessageBoxImage]::Warning }
[System.Windows.MessageBox]::Show($res.Message, "MS SQL 연결 테스트", [System.Windows.MessageBoxButton]::OK, $img)
})
@@ -1273,13 +1348,18 @@ function Navigate-ToPage {
$srv = $v.FindName("TxtSqlServer").Text
$cmb = $v.FindName("CmbSqlAuthType")
- $authType = if ($cmb.SelectedIndex -eq 1) { "Sql" } else { "Integrated" }
+ $authType = switch ($cmb.SelectedIndex) {
+ 1 { "AdImpersonated" }
+ 2 { "Sql" }
+ default { "Integrated" }
+ }
+ $sDomain = $v.FindName("TxtSqlDomain").Text
$sUser = $v.FindName("TxtSqlUser").Text
$sPass = $v.FindName("TxtSqlPassword").Password
$adRes = Test-ADConnection -DomainController $dc -BaseOU $ou -Username $user -Password $pass -UseLdaps $ldaps
$netappRes = Test-NetAppConnection -ClusterEndpoint $ep -Username $nUser -Password $nPass -IgnoreSsl $ignoreSsl
- $sqlRes = Test-SqlConnection -Server $srv -AuthType $authType -Username $sUser -Password $sPass
+ $sqlRes = Test-SqlConnection -Server $srv -AuthType $authType -Domain $sDomain -Username $sUser -Password $sPass
$report = @"
=== 전체 인프라 일괄 진단 결과 ===
@@ -1305,7 +1385,11 @@ function Navigate-ToPage {
$btnSave.add_Click({
$v = $ViewCache["Settings"]
$cmb = $v.FindName("CmbSqlAuthType")
- $authType = if ($cmb.SelectedIndex -eq 1) { "Sql" } else { "Integrated" }
+ $authType = switch ($cmb.SelectedIndex) {
+ 1 { "AdImpersonated" }
+ 2 { "Sql" }
+ default { "Integrated" }
+ }
$chkGlobalLive = $v.FindName("ChkGlobalLiveMode")
$chkAdLive = $v.FindName("ChkAdLive")
@@ -1340,6 +1424,7 @@ function Navigate-ToPage {
UseLive = [bool]($chkGlobalLive.IsChecked -or $chkSqlLive.IsChecked)
Server = $v.FindName("TxtSqlServer").Text
AuthType = $authType
+ Domain = $v.FindName("TxtSqlDomain").Text
Username = $v.FindName("TxtSqlUser").Text
Password = $v.FindName("TxtSqlPassword").Password
DefaultDatabase = "master"
@@ -1365,6 +1450,7 @@ function Navigate-ToPage {
$btnReload.add_Click({
$global:AppConfig = Get-AppConfig
& $script:loadSettingsToUI
+ & $updateSqlAuthUi
Update-ModeBadgeUI
[System.Windows.MessageBox]::Show("config.json 설정을 다시 불러왔습니다.", "알림", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
})
diff --git a/Views/SettingsView.xaml b/Views/SettingsView.xaml
index ce7200d..6e39615 100644
--- a/Views/SettingsView.xaml
+++ b/Views/SettingsView.xaml
@@ -123,19 +123,24 @@
-
-
-
+
+
+
+
+
-
+
+
+
+
-
+
diff --git a/Views/SqlDatabaseView.xaml b/Views/SqlDatabaseView.xaml
index c2ea9a9..e0e9c97 100644
--- a/Views/SqlDatabaseView.xaml
+++ b/Views/SqlDatabaseView.xaml
@@ -23,7 +23,7 @@
-
+
@@ -36,8 +36,9 @@
-
-
+
+
+
diff --git a/config.json b/config.json
index 0241506..ae6ae7e 100644
--- a/config.json
+++ b/config.json
@@ -26,6 +26,7 @@
"UseLive": false,
"Server": "localhost",
"AuthType": "Integrated",
+ "Domain": "corp",
"Username": "sa",
"Password": "",
"DefaultDatabase": "master",