feat: implement modern admin studio with live AD, NetApp, MS SQL connectivity and comprehensive docs

This commit is contained in:
2026-10-20 16:13:34 +09:00
commit ccc1b47dfd
19 changed files with 3533 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
# Windows / OS Files
Thumbs.db
ehthumbs.db
Desktop.ini
$RECYCLE.BIN/
# Temporary / Log files
*.log
*.tmp
*.bak
# VS Code / IDE
.vscode/
.idea/
# User Config (Optional overrides)
config.local.json
+172
View File
@@ -0,0 +1,172 @@
# Enterprise Admin Studio - 아키텍처 설계 문서 (ARCHITECTURE.md)
## 1. 개요 (Overview)
**Enterprise Admin Studio**는 Windows 및 기업 인프라의 3대 핵심 컴포넌트인 **Active Directory (AD DS)**, **NetApp ONTAP 스토리지 (Qtree, Quota, Share)**, **Microsoft SQL Server**를 단일 화면에서 통합 모니터링 및 제어할 수 있는 모던 UI 기반의 관리 콘솔입니다.
본 프로젝트는 **PowerShell 7 / Windows PowerShell 5.1**과 **.NET WPF (XAML)** 기술을 기반으로 구축되었으며, 차후 엔터프라이즈 환경에서 필요 시 **C# (.NET 8+ / WPF / WinUI 3)** 환경으로의 100% 무손실 전환이 가능하도록 철저한 계층 분리(Separation of Concerns) 아키텍처를 채택했습니다.
---
## 2. 전체 시스템 구조도 (System Architecture)
```mermaid
flowchart TD
subgraph Presentation_Layer ["Presentation Layer (UI/XAML)"]
MW["MainWindow.xaml\n(Fluent Dark Frame, Sidebar Navigation, Mode Badge)"]
Styles["Styles/Theme.xaml\n(Color Palette, Modern Controls, Icons)"]
subgraph Views ["Views (Dynamic UserControls)"]
V1["DashboardView.xaml\n(KPI Cards, Audit Grid)"]
V2["ActiveDirectoryView.xaml\n(User Grid, Detail Inspector)"]
V3["NetAppView.xaml\n(Qtree, Quota, CIFS/NFS Tabs)"]
V4["SqlDatabaseView.xaml\n(DB Grid, T-SQL Console)"]
V5["SettingsView.xaml\n(Endpoint Config, Diagnostic)"]
end
end
subgraph Application_Core ["Application Core (PowerShell Routing & Cache)"]
Entry["Start-App.ps1\n(Application Entrypoint & Event Dispatcher)"]
Router["Page Navigator & ViewCache\n(Fast Instant Page Switching)"]
Config["config.json & ConfigService.ps1\n(Persistent Configuration Engine)"]
end
subgraph Service_Layer ["Service / Business Logic Layer (Modules)"]
AD_SVC["ADService.ps1\n[IActiveDirectoryService]"]
NETAPP_SVC["NetAppService.ps1\n[INetAppStorageService]"]
SQL_SVC["SqlService.ps1\n[ISqlServerService]"]
end
subgraph Infrastructure_Targets ["Target Enterprise Infrastructure"]
DC["Active Directory Domain Controller\n(LDAP / LDAPS Port 389/636)"]
ONTAP["NetApp ONTAP Cluster\n(REST API v2 / HTTPS 443)"]
MSSQL["MS SQL Server Instance\n(TDS Protocol / TCP 1433)"]
end
MW --> Router
Router --> Views
Styles -.-> MW
Styles -.-> Views
Entry --> Router
Entry --> Config
Views --> AD_SVC
Views --> NETAPP_SVC
Views --> SQL_SVC
AD_SVC --> DC
NETAPP_SVC --> ONTAP
SQL_SVC --> MSSQL
```
---
## 3. 계층별 상세 설계 (Layer Details)
### 3.1 Presentation Layer (XAML UI)
- **Fluent Modern Dark 테마**:
- 배경: `#0F172A` (Slate Dark), 카드/사이드바: `#1E293B`, 액센트: `#3B82F6` / `#6366F1`
- 표준 WPF 컨트롤 템플릿(`Button`, `TextBox`, `RadioButton`, `DataGrid`, `TabControl`, `Border`)을 모던 라운딩 처리
- **네이티브 아이콘 시스템**:
- 외부 폰트 설치나 대용량 SVG 의존성 없이 Windows 10/11 기본 탑재 글꼴인 `Segoe MDL2 Assets`의 유니코드 글리프(예: ``, ``, ``) 사용
- **Dynamic View Hosting**:
- `MainWindow.xaml` 내 `ContentControl x:Name="MainContentHost"`에 각 View를 동적 인스턴스화
- `ViewCache` 해시테이블을 두어 최초 로드 후 페이지 전환 시 재파싱 비용을 제거하여 네이티브 수준의 부드러운 전환 구현
### 3.2 Service & Integration Layer (비즈니스 로직)
#### A. Active Directory 연동 (`ADService.ps1`)
- **기술 스택**: Windows 기본 내장 `.NET System.DirectoryServices`
- **프로토콜**: LDAP (Port 389) 또는 LDAPS (Port 636)
- **동작 원리**:
1. `DirectoryEntry`를 통해 지정된 도메인 컨트롤러 또는 기본 도메인에 바인딩 (자격증명 지원)
2. `DirectorySearcher`를 이용해 `(&(objectCategory=person)(objectClass=user))` 필터링 및 페이징(PageSize = 1000) 처리
3. 속성 매핑:
- `userAccountControl`: `0x0002` 비트 연산을 통한 활성/비활성 판정 및 변경
- `lockoutTime`: 정수값을 통한 잠금 여부 확인 및 `lockoutTime = 0` 커밋을 통한 계정 잠금 해제
- `Invoke("SetPassword", ...)`: 안전한 비밀번호 초기화 및 `pwdLastSet = 0` 강제 변경 플래그
4. **Fallback 메커니즘**: AD 환경이 아닌 로컬 PC나 개발 환경에서는 자동으로 Mock 데이터 및 데모 시뮬레이션으로 안전하게 동작
#### B. NetApp ONTAP 스토리지 연동 (`NetAppService.ps1`)
- **기술 스택**: NetApp ONTAP 9.6+ 표준 REST API (HTTPS RESTful)
- **엔드포인트 매핑**:
- Qtree 관리: `GET /api/storage/qtrees`, `POST /api/storage/qtrees`
- Quota 한도/모니터링: `GET /api/storage/quota/rules`, `GET /api/storage/quota/reports`
- CIFS 파일 공유: `GET /api/protocols/cifs/shares`, `POST /api/protocols/cifs/shares`
- NFS 내보내기 정책: `GET /api/protocols/nfs/export-policies`
- 클러스터 상태 진단: `GET /api/cluster`
- **보안 및 인증**: HTTP Basic Authentication 및 자체 서명 SSL 인증서 무시 처리(`IgnoreSslErrors`) 옵션 내장
#### C. MS SQL Server 연동 (`SqlService.ps1`)
- **기술 스택**: .NET 기본 제공 `System.Data.SqlClient`
- **프로토콜**: Tabular Data Stream (TDS, Port 1433)
- **동작 원리**:
1. Windows 통합 인증(Integrated Security) 및 SQL Server 계정(User ID / Password) 인증 동시 지원
2. `sys.databases`, `sys.master_files`, `msdb.dbo.backupset` 카탈로그 뷰를 조인하여 실제 데이터베이스 상태, 용량, 복구 모델, 최종 Full 백업 일시 동적 집계
3. `BACKUP DATABASE [{0}] TO DISK = '{1}' WITH INIT, STATS = 10` 실제 데이터베이스 백업 수행
4. T-SQL 간이 쿼리 실행기: 사용자 입력을 파싱하여 `SqlDataAdapter`로 DataTable에 적재 후 결과 포맷팅
---
## 4. C# (.NET 8 / WPF / WinUI 3) 마이그레이션 로드맵
향후 C# 솔루션으로 승격(Promote) 시 100% 매핑되도록 설계되었습니다.
### 4.1 XAML 파일의 무수정 재사용
`Views/*.xaml` 및 `Styles/Theme.xaml`은 표준 WPF 문법을 준수합니다. C# 프로젝트에 그대로 포함하고 아래와 같이 코드 비하인드만 연결하면 됩니다:
```csharp
namespace ModernAdminStudio.Views;
public partial class ActiveDirectoryView : UserControl
{
public ActiveDirectoryView()
{
InitializeComponent();
}
}
```
### 4.2 MVVM 및 DI (의존성 주입) 패턴 매핑
```csharp
// Program.cs 또는 App.xaml.cs
services.AddSingleton<IActiveDirectoryService, ActiveDirectoryService>();
services.AddSingleton<INetAppStorageService, NetAppStorageService>();
services.AddSingleton<ISqlServerService, SqlServerService>();
services.AddTransient<DashboardViewModel>();
services.AddTransient<ActiveDirectoryViewModel>();
services.AddTransient<NetAppViewModel>();
services.AddTransient<SqlDatabaseViewModel>();
services.AddTransient<SettingsViewModel>();
```
### 4.3 서비스 인터페이스 정의서 (C# Blueprint)
```csharp
public interface IActiveDirectoryService
{
Task<IEnumerable<AdUserDto>> GetUsersAsync(string filter);
Task<OperationResult> UnlockAccountAsync(string samAccountName);
Task<OperationResult> ResetPasswordAsync(string samAccountName, string newPassword);
Task<OperationResult> SetAccountStateAsync(string samAccountName, bool enable);
Task<ConnectionResult> TestConnectionAsync();
}
public interface INetAppStorageService
{
Task<IEnumerable<QtreeDto>> GetQtreesAsync(string svm);
Task<IEnumerable<QuotaDto>> GetQuotasAsync(string svm);
Task<IEnumerable<ShareDto>> GetSharesAsync(string svm);
Task<OperationResult> CreateQtreeAsync(CreateQtreeRequest request);
Task<OperationResult> CreateShareAsync(CreateShareRequest request);
Task<ConnectionResult> TestConnectionAsync();
}
public interface ISqlServerService
{
Task<IEnumerable<DatabaseInfoDto>> GetDatabasesAsync();
Task<OperationResult> BackupDatabaseAsync(string databaseName, string targetPath);
Task<QueryResultDto> ExecuteQuerySnippetAsync(string query);
Task<ConnectionResult> TestConnectionAsync();
}
```
+26
View File
@@ -0,0 +1,26 @@
# Enterprise Admin Studio - 변경 이력 (CHANGELOG.md)
## [v1.2.0] - 2026-10-20
### Added (신규 기능 추가)
- **실제 엔터프라이즈 인프라 연결 엔진 구현**:
- **Active Directory**: .NET 내장 `System.DirectoryServices`를 활용한 실제 LDAP/LDAPS 연결, `DirectorySearcher` 기반 실시간 사용자 검색, 계정 잠금 해제(`lockoutTime = 0`), 임시 비밀번호 초기화 및 강제 변경(`pwdLastSet = 0`), 계정 활성/비활성 제어 구현
- **NetApp ONTAP 스토리지**: ONTAP 9.6+ 표준 REST API(`https://<cluster>/api/...`) 호출 엔진 구현. Qtree 목록/생성, Quota 한도/모니터링 조회, CIFS/NFS 공유 생성 및 목록 조회, 자체 서명 SSL 인증서 우회(`IgnoreSslErrors`) 옵션 탑재
- **MS SQL Server**: `System.Data.SqlClient`를 활용한 실제 인스턴스 접속, `sys.databases` 카탈로그 뷰 조인 실시간 DB 용량/복구모델/최근백업 집계, `BACKUP DATABASE` 실시간 백업 파일 생성, T-SQL 간이 쿼리 파서 및 실행기 구현
- **동작 모드 토글 (Live vs Mock) 시스템**:
- 상단 헤더에 현재 모드(Live 초록색 배지 vs Mock 보라색 배지) 실시간 시각화
- 실제 대상 서버가 없거나 네트워크가 단절된 경우에도 안전하게 목업 데이터로 자동 폴백되는 안전장치 구축
- **영속적 설정 관리 (`config.json` & `ConfigService.ps1`)**:
- AD, NetApp, MS SQL의 모든 접속 엔드포인트 및 자격증명을 JSON 기반으로 저장/로드
- 환경설정 화면에서 원클릭 "전체 인프라 일괄 진단" 기능 추가
- **문서화 강화**:
- 시스템 구조도 및 C# 마이그레이션 청사진을 담은 `ARCHITECTURE.md` 작성
- 실제 운영 및 메뉴얼 가이드 `USAGE.md` 작성
## [v1.0.0] - 2026-10-20
### Added
- **프로젝트 초기 구축 (Initial Release)**:
- PowerShell + WPF (XAML) 기반 아키텍처 스캐폴딩
- Windows 11 Fluent Dark 모던 테마(`Styles/Theme.xaml`) 및 `Segoe MDL2 Assets` 글리프 아이콘 적용
- 사이드바 내비게이션 및 `ViewCache` 기반의 부드러운 페이지 전환 라우팅 구현
- 메뉴별 독립 View 5종 분리 (`DashboardView`, `ActiveDirectoryView`, `NetAppView`, `SqlDatabaseView`, `SettingsView`)
- 원클릭 백그라운드 실행을 위한 `Run.bat` 제공
+209
View File
@@ -0,0 +1,209 @@
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Enterprise Admin Studio - Unified Infrastructure Management"
Height="840" Width="1360"
MinHeight="720" MinWidth="1100"
WindowStartupLocation="CenterScreen"
Background="{StaticResource BgDarkBrush}"
FontFamily="{StaticResource ModernFont}">
<Window.Resources>
<!-- Included via Application or MergedDictionaries -->
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<!-- Left Sidebar Navigation -->
<ColumnDefinition Width="260" />
<!-- Main Content Area -->
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- ================= SIDEBAR ================= -->
<Border Grid.Column="0" Background="{StaticResource SidebarBgBrush}" BorderBrush="{StaticResource CardBorderBrush}" BorderThickness="0,0,1,0">
<Grid>
<Grid.RowDefinitions>
<!-- Brand / App Logo -->
<RowDefinition Height="80" />
<!-- Navigation Menu Items -->
<RowDefinition Height="*" />
<!-- Bottom System Info & Profile -->
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Brand Header -->
<StackPanel Grid.Row="0" Margin="20,18,20,10" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal">
<Border Width="34" Height="34" Background="{StaticResource PrimaryAccentBrush}" CornerRadius="8" Margin="0,0,12,0">
<TextBlock Text="&#xE7F4;" FontFamily="{StaticResource IconFont}" FontSize="18" Foreground="White"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<StackPanel VerticalAlignment="Center">
<TextBlock Text="Admin Studio" FontSize="16" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}" />
<TextBlock Text="Enterprise v1.2" FontSize="11" Foreground="#60A5FA" FontWeight="SemiBold" />
</StackPanel>
</StackPanel>
</StackPanel>
<!-- Navigation List -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<StackPanel Margin="6,10,6,0">
<TextBlock Text="CORE MANAGEMENT" FontSize="11" FontWeight="Bold" Foreground="#475569" Margin="18,12,0,8" />
<!-- 1. Dashboard -->
<RadioButton x:Name="NavDashboard" Style="{StaticResource NavButtonStyle}" IsChecked="True">
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xE80F;" FontFamily="{StaticResource IconFont}" FontSize="16" Width="28" VerticalAlignment="Center" />
<TextBlock Text="통합 대시보드" VerticalAlignment="Center" />
</StackPanel>
</RadioButton>
<!-- 2. Active Directory -->
<RadioButton x:Name="NavAD" Style="{StaticResource NavButtonStyle}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xE77B;" FontFamily="{StaticResource IconFont}" FontSize="16" Width="28" VerticalAlignment="Center" />
<TextBlock Text="Active Directory" VerticalAlignment="Center" />
</StackPanel>
</RadioButton>
<!-- 3. NetApp Storage -->
<RadioButton x:Name="NavNetApp" Style="{StaticResource NavButtonStyle}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xE7F3;" FontFamily="{StaticResource IconFont}" FontSize="16" Width="28" VerticalAlignment="Center" />
<TextBlock Text="NetApp 스토리지" VerticalAlignment="Center" />
</StackPanel>
</RadioButton>
<!-- 4. MS SQL Database -->
<RadioButton x:Name="NavSql" Style="{StaticResource NavButtonStyle}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xEC92;" FontFamily="{StaticResource IconFont}" FontSize="16" Width="28" VerticalAlignment="Center" />
<TextBlock Text="MS SQL DB 관리" VerticalAlignment="Center" />
</StackPanel>
</RadioButton>
<TextBlock Text="PREFERENCES" FontSize="11" FontWeight="Bold" Foreground="#475569" Margin="18,24,0,8" />
<!-- 5. Settings -->
<RadioButton x:Name="NavSettings" Style="{StaticResource NavButtonStyle}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xE713;" FontFamily="{StaticResource IconFont}" FontSize="16" Width="28" VerticalAlignment="Center" />
<TextBlock Text="환경설정 및 연결" VerticalAlignment="Center" />
</StackPanel>
</RadioButton>
</StackPanel>
</ScrollViewer>
<!-- Sidebar Bottom Profile / System Status -->
<Border Grid.Row="2" BorderBrush="{StaticResource CardBorderBrush}" BorderThickness="0,1,0,0" Padding="16,14">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="38" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- Avatar -->
<Border Grid.Column="0" Width="36" Height="36" Background="#1E293B" CornerRadius="18" BorderBrush="{StaticResource PrimaryAccentBrush}" BorderThickness="1.5">
<TextBlock Text="&#xE77B;" FontFamily="{StaticResource IconFont}" FontSize="18" Foreground="{StaticResource PrimaryAccentBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<!-- Admin Name / Role -->
<StackPanel Grid.Column="1" Margin="10,0,0,0" VerticalAlignment="Center">
<TextBlock Text="Domain Admin" FontSize="12.5" FontWeight="SemiBold" Foreground="{StaticResource TextPrimaryBrush}" />
<TextBlock Text="CORP\Administrator" FontSize="11" Foreground="{StaticResource TextSecondaryBrush}" />
</StackPanel>
<!-- Status Dot -->
<Border Grid.Column="2" Width="8" Height="8" Background="#10B981" CornerRadius="4" VerticalAlignment="Center" ToolTip="Connected &amp; Healthy" />
</Grid>
</Border>
</Grid>
</Border>
<!-- ================= MAIN CONTENT AREA ================= -->
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<!-- Top Navigation Header -->
<RowDefinition Height="64" />
<!-- Dynamic Page View Host -->
<RowDefinition Height="*" />
<!-- Global Status Bar -->
<RowDefinition Height="32" />
</Grid.RowDefinitions>
<!-- Top Header Bar -->
<Border Grid.Row="0" Background="{StaticResource SidebarBgBrush}" BorderBrush="{StaticResource CardBorderBrush}" BorderThickness="0,0,0,1" Padding="24,0">
<Grid VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- Breadcrumb / Section Header -->
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock x:Name="TxtHeaderTitle" Text="통합 대시보드" FontSize="16" FontWeight="SemiBold" Foreground="{StaticResource TextPrimaryBrush}" />
<TextBlock Text=" / " FontSize="14" Foreground="#475569" />
<TextBlock x:Name="TxtHeaderSubtitle" Text="전체 인프라 상태 개요" FontSize="13" Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center" />
</StackPanel>
<!-- Global Quick Search & Mode Badge & Notification Controls -->
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
<!-- Mode Badge (Live vs Demo) -->
<Border x:Name="BadgeMode" Background="#6366F1" CornerRadius="6" Padding="10,5" Margin="0,0,16,0">
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xE958;" FontFamily="{StaticResource IconFont}" FontSize="12" Foreground="White" Margin="0,0,6,0" VerticalAlignment="Center" />
<TextBlock x:Name="TxtModeBadge" Text="데모/모의 모드" FontSize="12" FontWeight="SemiBold" Foreground="White" VerticalAlignment="Center" />
</StackPanel>
</Border>
<Border Background="#1E293B" CornerRadius="6" Padding="10,6" Margin="0,0,16,0" BorderBrush="{StaticResource CardBorderBrush}" BorderThickness="1">
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xE721;" FontFamily="{StaticResource IconFont}" FontSize="13" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,8,0" VerticalAlignment="Center" />
<TextBlock Text="전체 자원 통합 검색 (Ctrl + K)" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center" Width="180" />
</StackPanel>
</Border>
<!-- Notification Bell Button -->
<Button x:Name="BtnGlobalNotifications" Style="{StaticResource SecondaryButtonStyle}" Padding="10,6" Margin="0,0,8,0">
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xEA8F;" FontFamily="{StaticResource IconFont}" FontSize="14" Margin="0,0,6,0" VerticalAlignment="Center" />
<Border Background="#EF4444" CornerRadius="8" Padding="5,1">
<TextBlock Text="2" FontSize="10" FontWeight="Bold" Foreground="White" />
</Border>
</StackPanel>
</Button>
<!-- Refresh Current View Button -->
<Button x:Name="BtnGlobalRefresh" Content="&#xE72C;" FontFamily="{StaticResource IconFont}" Style="{StaticResource SecondaryButtonStyle}" Padding="10,6" ToolTip="현재 뷰 새로고침" />
</StackPanel>
</Grid>
</Border>
<!-- Dynamic View Host (ContentControl) -->
<ContentControl x:Name="MainContentHost" Grid.Row="1" Margin="0" />
<!-- Global Bottom Status Bar -->
<Border Grid.Row="2" Background="{StaticResource SidebarBgBrush}" BorderBrush="{StaticResource CardBorderBrush}" BorderThickness="0,1,0,0" Padding="18,0">
<Grid VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
<Border x:Name="DotGlobalStatus" Background="#10B981" Width="8" Height="8" CornerRadius="4" Margin="0,0,8,0" />
<TextBlock x:Name="TxtGlobalStatus" Text="모든 백엔드 모듈 정상 준비됨 | config.json 로드 완료" FontSize="11.5" Foreground="{StaticResource TextSecondaryBrush}" />
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="PowerShell 7 / .NET 8 WPF" FontSize="11" Foreground="#475569" Margin="0,0,16,0" />
<TextBlock x:Name="TxtSystemClock" Text="2026-10-20 16:10" FontSize="11" Foreground="{StaticResource TextSecondaryBrush}" />
</StackPanel>
</Grid>
</Border>
</Grid>
</Grid>
</Window>
+378
View File
@@ -0,0 +1,378 @@
# Modules\ADService.ps1
# Active Directory Service Module (.NET System.DirectoryServices Live & Fallback)
function Test-ADConnection {
param(
[string]$DomainController,
[string]$BaseOU,
[string]$Username,
[string]$Password,
[bool]$UseLdaps = $false
)
try {
$protocol = if ($UseLdaps) { "LDAP://$DomainController:636" } else { "LDAP://$DomainController" }
$path = if ([string]::IsNullOrWhiteSpace($BaseOU)) { "$protocol/RootDSE" } else { "$protocol/$BaseOU" }
$entry = if ([string]::IsNullOrWhiteSpace($Username)) {
New-Object System.DirectoryServices.DirectoryEntry($path)
} else {
New-Object System.DirectoryServices.DirectoryEntry($path, $Username, $Password)
}
# 바인딩 강제 테스트
$null = $entry.NativeObject
return [PSCustomObject]@{
Success = $true
Message = "Active Directory DC ($DomainController) 연결 성공 (RootDSE 응답 확인)"
LdapPath = $path
}
} catch {
return [PSCustomObject]@{
Success = $false
Message = "AD 연결 실패: $($_.Exception.Message)"
LdapPath = $path
}
}
}
function Get-ADUserList {
param(
[string]$Filter = "",
[object]$Config = $null
)
$useLive = if ($Config -and $Config.ActiveDirectory) { [bool]$Config.ActiveDirectory.UseLive } else { $false }
if ($useLive) {
$dc = $Config.ActiveDirectory.DomainController
$baseOu = $Config.ActiveDirectory.BaseOU
$useLdaps = [bool]$Config.ActiveDirectory.UseLdaps
$user = $Config.ActiveDirectory.Username
$pass = $Config.ActiveDirectory.Password
try {
$prefix = if ($useLdaps) { "LDAP://$dc:636" } else { "LDAP://$dc" }
$rootPath = if ([string]::IsNullOrWhiteSpace($baseOu)) { $prefix } else { "$prefix/$baseOu" }
$rootEntry = if ([string]::IsNullOrWhiteSpace($user)) {
New-Object System.DirectoryServices.DirectoryEntry($rootPath)
} else {
New-Object System.DirectoryServices.DirectoryEntry($rootPath, $user, $pass)
}
$searcher = New-Object System.DirectoryServices.DirectorySearcher($rootEntry)
$searcher.PageSize = 1000
if ([string]::IsNullOrWhiteSpace($Filter)) {
$searcher.Filter = "(&(objectCategory=person)(objectClass=user))"
} else {
$esc = $Filter -replace '[*()\\\x00]', ''
$searcher.Filter = "(&(objectCategory=person)(objectClass=user)(|(sAMAccountName=*$esc*)(displayName=*$esc*)(department=*$esc*)))"
}
$props = @("sAMAccountName", "displayName", "department", "title", "userPrincipalName", "mail", "userAccountControl", "lockoutTime", "lastLogonTimestamp", "distinguishedName")
foreach ($p in $props) { [void]$searcher.PropertiesToLoad.Add($p) }
$results = $searcher.FindAll()
$userList = [System.Collections.Generic.List[PSCustomObject]]::new()
foreach ($res in $results) {
$p = $res.Properties
$sam = if ($p["samaccountname"].Count -gt 0) { $p["samaccountname"][0] } else { "" }
$disp = if ($p["displayname"].Count -gt 0) { $p["displayname"][0] } else { $sam }
$dept = if ($p["department"].Count -gt 0) { $p["department"][0] } else { "-" }
$title = if ($p["title"].Count -gt 0) { $p["title"][0] } else { "-" }
$upn = if ($p["userprincipalname"].Count -gt 0) { $p["userprincipalname"][0] }
elseif ($p["mail"].Count -gt 0) { $p["mail"][0] } else { "$sam@domain.local" }
$dn = if ($p["distinguishedname"].Count -gt 0) { $p["distinguishedname"][0] } else { "" }
$uac = if ($p["useraccountcontrol"].Count -gt 0) { [int]$p["useraccountcontrol"][0] } else { 512 }
$isDisabled = ($uac -band 2) -eq 2
$statusText = if ($isDisabled) { "비활성" } else { "활성" }
$lockVal = if ($p["lockouttime"].Count -gt 0) { [int64]$p["lockouttime"][0] } else { 0 }
$isLocked = ($lockVal -gt 0)
$lockedText = if ($isLocked) { "잠김" } else { "정상" }
$lastLogonStr = "-"
if ($p["lastlogontimestamp"].Count -gt 0) {
try {
$fileTime = [int64]$p["lastlogontimestamp"][0]
if ($fileTime -gt 0) {
$lastLogonStr = [DateTime]::FromFileTime($fileTime).ToString("yyyy-MM-dd HH:mm:ss")
}
} catch {}
}
$userList.Add([PSCustomObject]@{
SamAccountName = $sam
DisplayName = $disp
Department = $dept
Title = $title
UserPrincipalName = $upn
StatusText = $statusText
LockedText = $lockedText
LastLogonDate = $lastLogonStr
Enabled = (-not $isDisabled)
IsLocked = $isLocked
DistinguishedName = $dn
IsLive = $true
})
}
return $userList.ToArray()
}
catch {
Write-Warning "실제 AD 조회 실패: $($_.Exception.Message) (Mock 데이터로 전환합니다)"
}
}
# Fallback / Mock Data
return Get-ADSampleUsers -Filter $Filter
}
function Unlock-ADUserAccount {
param(
[string]$SamAccountName,
[object]$Config = $null
)
$useLive = if ($Config -and $Config.ActiveDirectory) { [bool]$Config.ActiveDirectory.UseLive } else { $false }
if ($useLive) {
$dc = $Config.ActiveDirectory.DomainController
$baseOu = $Config.ActiveDirectory.BaseOU
$user = $Config.ActiveDirectory.Username
$pass = $Config.ActiveDirectory.Password
try {
$rootPath = if ([string]::IsNullOrWhiteSpace($baseOu)) { "LDAP://$dc" } else { "LDAP://$dc/$baseOu" }
$rootEntry = if ([string]::IsNullOrWhiteSpace($user)) {
New-Object System.DirectoryServices.DirectoryEntry($rootPath)
} else {
New-Object System.DirectoryServices.DirectoryEntry($rootPath, $user, $pass)
}
$searcher = New-Object System.DirectoryServices.DirectorySearcher($rootEntry)
$searcher.Filter = "(&(objectCategory=person)(objectClass=user)(sAMAccountName=$SamAccountName))"
$result = $searcher.FindOne()
if ($null -eq $result) {
return [PSCustomObject]@{ Success = $false; Message = "AD에서 [$SamAccountName] 사용자를 찾을 수 없습니다." }
}
$userEntry = $result.GetDirectoryEntry()
$userEntry.Properties["lockoutTime"].Value = 0
$userEntry.CommitChanges()
return [PSCustomObject]@{
Success = $true
Message = "[$SamAccountName] 계정의 잠금이 실제 AD에서 해제되었습니다."
}
} catch {
return [PSCustomObject]@{
Success = $false
Message = "AD 잠금 해제 실패: $($_.Exception.Message)"
}
}
}
# Demo Simulation
return [PSCustomObject]@{
Success = $true
Message = "[데모 모드] [$SamAccountName] 계정의 잠금이 해제되었습니다."
}
}
function Reset-ADUserPassword {
param(
[string]$SamAccountName,
[string]$NewPassword = "Passw0rd!#$((Get-Random -Minimum 1000 -Maximum 9999))",
[object]$Config = $null
)
$useLive = if ($Config -and $Config.ActiveDirectory) { [bool]$Config.ActiveDirectory.UseLive } else { $false }
if ($useLive) {
$dc = $Config.ActiveDirectory.DomainController
$baseOu = $Config.ActiveDirectory.BaseOU
$user = $Config.ActiveDirectory.Username
$pass = $Config.ActiveDirectory.Password
try {
$rootPath = if ([string]::IsNullOrWhiteSpace($baseOu)) { "LDAP://$dc:636" } else { "LDAP://$dc:636/$baseOu" }
$rootEntry = if ([string]::IsNullOrWhiteSpace($user)) {
New-Object System.DirectoryServices.DirectoryEntry($rootPath)
} else {
New-Object System.DirectoryServices.DirectoryEntry($rootPath, $user, $pass)
}
$searcher = New-Object System.DirectoryServices.DirectorySearcher($rootEntry)
$searcher.Filter = "(&(objectCategory=person)(objectClass=user)(sAMAccountName=$SamAccountName))"
$result = $searcher.FindOne()
if ($null -eq $result) {
return [PSCustomObject]@{ Success = $false; Message = "AD에서 [$SamAccountName] 사용자를 찾을 수 없습니다." }
}
$userEntry = $result.GetDirectoryEntry()
$userEntry.Invoke("SetPassword", @($NewPassword))
$userEntry.Properties["pwdLastSet"].Value = 0 # 다음 로그온 시 암호 변경 강제
$userEntry.CommitChanges()
return [PSCustomObject]@{
Success = $true
Message = "[$SamAccountName] 암호가 성공적으로 초기화되었습니다.`n임시 암호: $NewPassword"
}
} catch {
return [PSCustomObject]@{
Success = $false
Message = "AD 암호 초기화 실패: $($_.Exception.Message)`n(참고: AD 암호 변경은 보안상 LDAPS 636 포트 연결이 필요합니다.)"
}
}
}
return [PSCustomObject]@{
Success = $true
Message = "[데모 모드] [$SamAccountName] 계정의 암호가 초기화되었습니다.`n임시 암호: $NewPassword"
}
}
function Set-ADUserAccountState {
param(
[string]$SamAccountName,
[bool]$Enable,
[object]$Config = $null
)
$useLive = if ($Config -and $Config.ActiveDirectory) { [bool]$Config.ActiveDirectory.UseLive } else { $false }
if ($useLive) {
$dc = $Config.ActiveDirectory.DomainController
$baseOu = $Config.ActiveDirectory.BaseOU
$user = $Config.ActiveDirectory.Username
$pass = $Config.ActiveDirectory.Password
try {
$rootPath = if ([string]::IsNullOrWhiteSpace($baseOu)) { "LDAP://$dc" } else { "LDAP://$dc/$baseOu" }
$rootEntry = if ([string]::IsNullOrWhiteSpace($user)) {
New-Object System.DirectoryServices.DirectoryEntry($rootPath)
} else {
New-Object System.DirectoryServices.DirectoryEntry($rootPath, $user, $pass)
}
$searcher = New-Object System.DirectoryServices.DirectorySearcher($rootEntry)
$searcher.Filter = "(&(objectCategory=person)(objectClass=user)(sAMAccountName=$SamAccountName))"
$result = $searcher.FindOne()
if ($null -eq $result) {
return [PSCustomObject]@{ Success = $false; Message = "사용자를 찾을 수 없습니다." }
}
$userEntry = $result.GetDirectoryEntry()
$uac = [int]$userEntry.Properties["userAccountControl"].Value
if ($Enable) {
$userEntry.Properties["userAccountControl"].Value = $uac -band (-bnot 2)
} else {
$userEntry.Properties["userAccountControl"].Value = $uac -bor 2
}
$userEntry.CommitChanges()
$action = if ($Enable) { "활성화" } else { "비활성화" }
return [PSCustomObject]@{ Success = $true; Message = "[$SamAccountName] 계정이 $action 되었습니다." }
} catch {
return [PSCustomObject]@{ Success = $false; Message = "AD 상태 변경 실패: $($_.Exception.Message)" }
}
}
$action = if ($Enable) { "활성화" } else { "비활성화" }
return [PSCustomObject]@{ Success = $true; Message = "[데모 모드] [$SamAccountName] 계정이 $action 되었습니다." }
}
function Get-ADSampleUsers {
param([string]$Filter = "")
$users = @(
[PSCustomObject]@{
SamAccountName = "gdhong"
DisplayName = "홍길동"
Department = "클라우드인프라팀"
Title = "수석 엔지니어"
UserPrincipalName = "gdhong@corp.local"
StatusText = "활성"
LockedText = "정상"
LastLogonDate = "2026-10-20 09:14:22"
Enabled = $true
IsLocked = $false
DistinguishedName = "CN=홍길동,OU=Engineers,OU=Users,DC=corp,DC=local"
IsLive = $false
},
[PSCustomObject]@{
SamAccountName = "cskim"
DisplayName = "김철수"
Department = "데이터플랫폼팀"
Title = "DBA"
UserPrincipalName = "cskim@corp.local"
StatusText = "활성"
LockedText = "정상"
LastLogonDate = "2026-10-20 11:45:01"
Enabled = $true
IsLocked = $false
DistinguishedName = "CN=김철수,OU=DBA,OU=Users,DC=corp,DC=local"
IsLive = $false
},
[PSCustomObject]@{
SamAccountName = "yylee"
DisplayName = "이영희"
Department = "보안관제팀"
Title = "정보보안책임"
UserPrincipalName = "yylee@corp.local"
StatusText = "잠김"
LockedText = "잠김 (5회 실패)"
LastLogonDate = "2026-10-19 18:22:10"
Enabled = $true
IsLocked = $true
DistinguishedName = "CN=이영희,OU=Security,OU=Users,DC=corp,DC=local"
IsLive = $false
},
[PSCustomObject]@{
SamAccountName = "mschoi"
DisplayName = "최민수"
Department = "개발운영팀"
Title = "DevOps 엔지니어"
UserPrincipalName = "mschoi@corp.local"
StatusText = "비활성"
LockedText = "정상"
LastLogonDate = "2026-09-01 14:02:00"
Enabled = $false
IsLocked = $false
DistinguishedName = "CN=최민수,OU=DevOps,OU=Users,DC=corp,DC=local"
IsLive = $false
},
[PSCustomObject]@{
SamAccountName = "jhbak"
DisplayName = "박지훈"
Department = "시스템관리팀"
Title = "스토리지 관리자"
UserPrincipalName = "jhbak@corp.local"
StatusText = "활성"
LockedText = "정상"
LastLogonDate = "2026-10-20 13:02:15"
Enabled = $true
IsLocked = $false
DistinguishedName = "CN=박지훈,OU=Admins,OU=Users,DC=corp,DC=local"
IsLive = $false
}
)
if ([string]::IsNullOrWhiteSpace($Filter)) {
return $users
} else {
return @($users | Where-Object {
$_.SamAccountName -like "*$Filter*" -or
$_.DisplayName -like "*$Filter*" -or
$_.Department -like "*$Filter*"
})
}
}
+68
View File
@@ -0,0 +1,68 @@
# Modules\ConfigService.ps1
# Configuration Management Module for Enterprise Admin Studio
$global:AppConfigPath = Join-Path (Split-Path -Parent $PSScriptRoot) "config.json"
function Get-AppConfig {
if (-not (Test-Path $global:AppConfigPath)) {
$defaultConfig = @{
Application = @{
LiveMode = $false
Theme = "Dark"
AutoRefreshSeconds = 60
}
ActiveDirectory = @{
UseLive = $false
DomainController = "dc01.corp.local"
Domain = "corp.local"
BaseOU = "DC=corp,DC=local"
UseLdaps = $false
Username = ""
Password = ""
}
NetApp = @{
UseLive = $false
ClusterEndpoint = "https://10.200.10.50"
Port = 443
DefaultSvm = "svm_cifs_prod01"
Username = "admin"
Password = ""
IgnoreSslErrors = $true
}
MsSql = @{
UseLive = $false
Server = "localhost"
AuthType = "Integrated"
Username = "sa"
Password = ""
DefaultDatabase = "master"
ConnectionTimeout = 10
BackupDirectory = "C:\Backups"
}
}
$json = $defaultConfig | ConvertTo-Json -Depth 5
[System.IO.File]::WriteAllText($global:AppConfigPath, $json, [System.Text.Encoding]::UTF8)
return $defaultConfig
}
try {
$raw = [System.IO.File]::ReadAllText($global:AppConfigPath, [System.Text.Encoding]::UTF8)
return ($raw | ConvertFrom-Json)
} catch {
Write-Warning "설정 파일 로드 실패, 기본값을 사용합니다: $_"
return $null
}
}
function Save-AppConfig {
param([object]$Config)
try {
$json = $Config | ConvertTo-Json -Depth 5
[System.IO.File]::WriteAllText($global:AppConfigPath, $json, [System.Text.Encoding]::UTF8)
return $true
} catch {
Write-Error "설정 저장 실패: $_"
return $false
}
}
+498
View File
@@ -0,0 +1,498 @@
# Modules\NetAppService.ps1
# NetApp ONTAP REST API Service Module (Live & Fallback)
function Invoke-NetAppRestRequest {
param(
[string]$ClusterEndpoint,
[string]$UriPath,
[string]$Method = "GET",
[object]$Body = $null,
[string]$Username = "",
[string]$Password = "",
[bool]$IgnoreSsl = $true
)
# SSL 우회 설정 (.NET 기반)
if ($IgnoreSsl) {
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 -bor [System.Net.SecurityProtocolType]::Tls13
}
$base = $ClusterEndpoint.TrimEnd('/')
$fullUri = "$base$UriPath"
$headers = @{
"Accept" = "application/json"
}
if (-not [string]::IsNullOrWhiteSpace($Username)) {
$authBytes = [System.Text.Encoding]::ASCII.GetBytes("$($Username):$($Password)")
$authBase64 = [System.Convert]::ToBase64String($authBytes)
$headers["Authorization"] = "Basic $authBase64"
}
$params = @{
Uri = $fullUri
Method = $Method
Headers = $headers
TimeoutSec = 10
}
if ($Body) {
$params["Body"] = ($Body | ConvertTo-Json -Depth 5)
$params["ContentType"] = "application/json; charset=utf-8"
}
return Invoke-RestMethod @params
}
function Test-NetAppConnection {
param(
[string]$ClusterEndpoint,
[string]$Username,
[string]$Password,
[bool]$IgnoreSsl = $true
)
try {
$res = Invoke-NetAppRestRequest -ClusterEndpoint $ClusterEndpoint -UriPath "/api/cluster" -Method "GET" -Username $Username -Password $Password -IgnoreSsl $IgnoreSsl
$clusterName = if ($res.name) { $res.name } else { "ONTAP Cluster" }
$version = if ($res.version -and $res.version.full) { $res.version.full } else { "ONTAP 9.x" }
return [PSCustomObject]@{
Success = $true
Message = "NetApp ONTAP ($clusterName) 연결 성공 - 버전: $version"
Version = $version
}
} catch {
return [PSCustomObject]@{
Success = $false
Message = "NetApp 스토리지 연결 실패: $($_.Exception.Message)"
}
}
}
function Get-NetAppQtrees {
param(
[string]$Filter = "",
[object]$Config = $null
)
$useLive = if ($Config -and $Config.NetApp) { [bool]$Config.NetApp.UseLive } else { $false }
if ($useLive) {
try {
$endpoint = $Config.NetApp.ClusterEndpoint
$user = $Config.NetApp.Username
$pass = $Config.NetApp.Password
$ignoreSsl = [bool]$Config.NetApp.IgnoreSslErrors
$res = Invoke-NetAppRestRequest -ClusterEndpoint $endpoint -UriPath "/api/storage/qtrees?fields=name,volume.name,security_style,oplocks,status" -Method "GET" -Username $user -Password $pass -IgnoreSsl $ignoreSsl
if ($res.records) {
$list = [System.Collections.Generic.List[PSCustomObject]]::new()
foreach ($item in $res.records) {
$vol = if ($item.volume) { $item.volume.name } else { "" }
$name = $item.name
$sec = if ($item.security_style) { $item.security_style } else { "ntfs" }
$op = if ($item.oplocks) { $item.oplocks } else { "enabled" }
$st = if ($item.status) { $item.status } else { "normal" }
$path = "/vol/$vol/$name"
if ([string]::IsNullOrWhiteSpace($Filter) -or $vol -like "*$Filter*" -or $name -like "*$Filter*") {
$list.Add([PSCustomObject]@{
Volume = $vol
QtreeName = $name
SecurityStyle = $sec
Oplocks = $op
Status = $st
FullPath = $path
IsLive = $true
})
}
}
return $list.ToArray()
}
} catch {
Write-Warning "실제 NetApp Qtree 조회 실패: $($_.Exception.Message) (Mock 데이터로 전환합니다)"
}
}
return Get-NetAppSampleQtrees -Filter $Filter
}
function Get-NetAppQuotas {
param(
[string]$Filter = "",
[object]$Config = $null
)
$useLive = if ($Config -and $Config.NetApp) { [bool]$Config.NetApp.UseLive } else { $false }
if ($useLive) {
try {
$endpoint = $Config.NetApp.ClusterEndpoint
$user = $Config.NetApp.Username
$pass = $Config.NetApp.Password
$ignoreSsl = [bool]$Config.NetApp.IgnoreSslErrors
# ONTAP REST quota rules
$res = Invoke-NetAppRestRequest -ClusterEndpoint $endpoint -UriPath "/api/storage/quota/rules?fields=type,users.name,group.name,qtree.name,volume.name,space.hard_limit,space.soft_limit" -Method "GET" -Username $user -Password $pass -IgnoreSsl $ignoreSsl
if ($res.records) {
$list = [System.Collections.Generic.List[PSCustomObject]]::new()
foreach ($item in $res.records) {
$type = $item.type
$vol = if ($item.volume) { $item.volume.name } else { "" }
$target = if ($item.users -and $item.users.Count -gt 0) { $item.users[0].name }
elseif ($item.group -and $item.group.name) { $item.group.name }
elseif ($item.qtree -and $item.qtree.name) { $item.qtree.name }
else { "-" }
$hardBytes = if ($item.space -and $item.space.hard_limit) { [int64]$item.space.hard_limit } else { 0 }
$softBytes = if ($item.space -and $item.space.soft_limit) { [int64]$item.space.soft_limit } else { 0 }
$hardStr = if ($hardBytes -gt 0) { "$([Math]::Round($hardBytes / 1GB, 1)) GB" } else { "무제한" }
$softStr = if ($softBytes -gt 0) { "$([Math]::Round($softBytes / 1GB, 1)) GB" } else { "무제한" }
$list.Add([PSCustomObject]@{
QuotaType = $type
Target = $target
VolumeQtree = $vol
UsedSpace = "조회중"
SoftLimit = $softStr
HardLimit = $hardStr
UsagePercent = "-"
QuotaStatus = "규칙 적용됨"
IsLive = $true
})
}
return $list.ToArray()
}
} catch {
Write-Warning "실제 NetApp Quota 조회 실패: $($_.Exception.Message) (Mock 데이터로 전환합니다)"
}
}
return Get-NetAppSampleQuotas -Filter $Filter
}
function Get-NetAppShares {
param(
[string]$Filter = "",
[object]$Config = $null
)
$useLive = if ($Config -and $Config.NetApp) { [bool]$Config.NetApp.UseLive } else { $false }
if ($useLive) {
try {
$endpoint = $Config.NetApp.ClusterEndpoint
$user = $Config.NetApp.Username
$pass = $Config.NetApp.Password
$ignoreSsl = [bool]$Config.NetApp.IgnoreSslErrors
$res = Invoke-NetAppRestRequest -ClusterEndpoint $endpoint -UriPath "/api/protocols/cifs/shares?fields=name,path,comment,svm.name" -Method "GET" -Username $user -Password $pass -IgnoreSsl $ignoreSsl
if ($res.records) {
$list = [System.Collections.Generic.List[PSCustomObject]]::new()
foreach ($item in $res.records) {
$sName = $item.name
$sPath = $item.path
$sComment = if ($item.comment) { $item.comment } else { "-" }
if ([string]::IsNullOrWhiteSpace($Filter) -or $sName -like "*$Filter*" -or $sPath -like "*$Filter*") {
$list.Add([PSCustomObject]@{
Protocol = "CIFS (SMB)"
ShareName = $sName
Path = $sPath
Comment = $sComment
AclSummary = "ONTAP ACL 관리"
ActiveSessions = "-"
IsLive = $true
})
}
}
return $list.ToArray()
}
} catch {
Write-Warning "실제 NetApp Share 조회 실패: $($_.Exception.Message) (Mock 데이터로 전환합니다)"
}
}
return Get-NetAppSampleShares -Filter $Filter
}
function New-NetAppQtreeAction {
param(
[string]$Volume,
[string]$QtreeName,
[string]$SecurityStyle = "ntfs",
[object]$Config = $null
)
$useLive = if ($Config -and $Config.NetApp) { [bool]$Config.NetApp.UseLive } else { $false }
if ($useLive) {
try {
$endpoint = $Config.NetApp.ClusterEndpoint
$user = $Config.NetApp.Username
$pass = $Config.NetApp.Password
$svm = $Config.NetApp.DefaultSvm
$ignoreSsl = [bool]$Config.NetApp.IgnoreSslErrors
$body = @{
name = $QtreeName
volume = @{ name = $Volume }
svm = @{ name = $svm }
security_style = $SecurityStyle
}
$res = Invoke-NetAppRestRequest -ClusterEndpoint $endpoint -UriPath "/api/storage/qtrees" -Method "POST" -Body $body -Username $user -Password $pass -IgnoreSsl $ignoreSsl
return [PSCustomObject]@{
Success = $true
Message = "실제 NetApp 볼륨 [$Volume]에 Qtree [$QtreeName]가 성공적으로 생성되었습니다."
}
} catch {
return [PSCustomObject]@{
Success = $false
Message = "NetApp Qtree 생성 실패: $($_.Exception.Message)"
}
}
}
return [PSCustomObject]@{
Success = $true
Message = "[데모 모드] 볼륨 [$Volume]에 Qtree [$QtreeName]가 가상 생성되었습니다."
}
}
function New-NetAppShareAction {
param(
[string]$ShareName,
[string]$Path,
[string]$Comment = "",
[object]$Config = $null
)
$useLive = if ($Config -and $Config.NetApp) { [bool]$Config.NetApp.UseLive } else { $false }
if ($useLive) {
try {
$endpoint = $Config.NetApp.ClusterEndpoint
$user = $Config.NetApp.Username
$pass = $Config.NetApp.Password
$svm = $Config.NetApp.DefaultSvm
$ignoreSsl = [bool]$Config.NetApp.IgnoreSslErrors
$body = @{
name = $ShareName
path = $Path
comment = $Comment
svm = @{ name = $svm }
}
$res = Invoke-NetAppRestRequest -ClusterEndpoint $endpoint -UriPath "/api/protocols/cifs/shares" -Method "POST" -Body $body -Username $user -Password $pass -IgnoreSsl $ignoreSsl
return [PSCustomObject]@{
Success = $true
Message = "실제 NetApp SVM [$svm]에 CIFS 공유 [$ShareName] ($Path)가 생성되었습니다."
}
} catch {
return [PSCustomObject]@{
Success = $false
Message = "NetApp CIFS 공유 생성 실패: $($_.Exception.Message)"
}
}
}
return [PSCustomObject]@{
Success = $true
Message = "[데모 모드] CIFS 공유 [$ShareName] ($Path)가 가상 생성되었습니다."
}
}
function Get-NetAppSampleQtrees {
param([string]$Filter = "")
$qtrees = @(
[PSCustomObject]@{
Volume = "vol_cifs_share01"
QtreeName = "qtree_finance_docs"
SecurityStyle = "ntfs"
Oplocks = "enabled"
Status = "normal"
FullPath = "/vol/vol_cifs_share01/qtree_finance_docs"
IsLive = $false
},
[PSCustomObject]@{
Volume = "vol_cifs_share01"
QtreeName = "qtree_hr_confidential"
SecurityStyle = "ntfs"
Oplocks = "enabled"
Status = "normal"
FullPath = "/vol/vol_cifs_share01/qtree_hr_confidential"
IsLive = $false
},
[PSCustomObject]@{
Volume = "vol_nfs_appdata"
QtreeName = "qtree_docker_registry"
SecurityStyle = "unix"
Oplocks = "disabled"
Status = "normal"
FullPath = "/vol/vol_nfs_appdata/qtree_docker_registry"
IsLive = $false
},
[PSCustomObject]@{
Volume = "vol_nfs_appdata"
QtreeName = "qtree_backup_targets"
SecurityStyle = "unix"
Oplocks = "disabled"
Status = "normal"
FullPath = "/vol/vol_nfs_appdata/qtree_backup_targets"
IsLive = $false
},
[PSCustomObject]@{
Volume = "vol_user_homes"
QtreeName = "qtree_dev_workspace"
SecurityStyle = "mixed"
Oplocks = "enabled"
Status = "normal"
FullPath = "/vol/vol_user_homes/qtree_dev_workspace"
IsLive = $false
}
)
if ([string]::IsNullOrWhiteSpace($Filter)) {
return $qtrees
} else {
return @($qtrees | Where-Object {
$_.Volume -like "*$Filter*" -or
$_.QtreeName -like "*$Filter*"
})
}
}
function Get-NetAppSampleQuotas {
param([string]$Filter = "")
$quotas = @(
[PSCustomObject]@{
QuotaType = "tree"
Target = "qtree_finance_docs"
VolumeQtree = "vol_cifs_share01"
UsedSpace = "420 GB"
SoftLimit = "450 GB"
HardLimit = "500 GB"
UsagePercent = "84.0%"
QuotaStatus = "정상 (OK)"
IsLive = $false
},
[PSCustomObject]@{
QuotaType = "tree"
Target = "qtree_hr_confidential"
VolumeQtree = "vol_cifs_share01"
UsedSpace = "192 GB"
SoftLimit = "200 GB"
HardLimit = "220 GB"
UsagePercent = "87.2%"
QuotaStatus = "주의 (Warning)"
IsLive = $false
},
[PSCustomObject]@{
QuotaType = "user"
Target = "CORP\gdhong"
VolumeQtree = "vol_user_homes"
UsedSpace = "94.5 GB"
SoftLimit = "90 GB"
HardLimit = "100 GB"
UsagePercent = "94.5%"
QuotaStatus = "경고 (Soft 초과)"
IsLive = $false
},
[PSCustomObject]@{
QuotaType = "group"
Target = "CORP\Domain Devs"
VolumeQtree = "vol_user_homes"
UsedSpace = "1.42 TB"
SoftLimit = "1.8 TB"
HardLimit = "2.0 TB"
UsagePercent = "71.0%"
QuotaStatus = "정상 (OK)"
IsLive = $false
},
[PSCustomObject]@{
QuotaType = "tree"
Target = "qtree_docker_registry"
VolumeQtree = "vol_nfs_appdata"
UsedSpace = "890 GB"
SoftLimit = "900 GB"
HardLimit = "1.0 TB"
UsagePercent = "89.0%"
QuotaStatus = "주의 (Warning)"
IsLive = $false
}
)
if ([string]::IsNullOrWhiteSpace($Filter)) {
return $quotas
} else {
return @($quotas | Where-Object {
$_.Target -like "*$Filter*" -or
$_.VolumeQtree -like "*$Filter*"
})
}
}
function Get-NetAppSampleShares {
param([string]$Filter = "")
$shares = @(
[PSCustomObject]@{
Protocol = "CIFS (SMB)"
ShareName = "Finance$"
Path = "/vol/vol_cifs_share01/qtree_finance_docs"
Comment = "재무기획팀 전용 기밀 문서 공유"
AclSummary = "재무팀 (Full), 감사팀 (Read)"
ActiveSessions = 14
IsLive = $false
},
[PSCustomObject]@{
Protocol = "CIFS (SMB)"
ShareName = "HR_Confidential"
Path = "/vol/vol_cifs_share01/qtree_hr_confidential"
Comment = "인사팀 보안 데이터 공유"
AclSummary = "인사팀 (Full), Admin (Read)"
ActiveSessions = 8
IsLive = $false
},
[PSCustomObject]@{
Protocol = "CIFS (SMB)"
ShareName = "PublicProjects"
Path = "/vol/vol_user_homes/qtree_dev_workspace"
Comment = "전사 프로젝트 공용 작업 디렉토리"
AclSummary = "Everyone (Change)"
ActiveSessions = 42
IsLive = $false
},
[PSCustomObject]@{
Protocol = "NFS v4.1"
ShareName = "/nfs/docker_registry"
Path = "/vol/vol_nfs_appdata/qtree_docker_registry"
Comment = "Kubernetes 클러스터 이미지 저장소 마운트"
AclSummary = "10.200.10.0/24 (rw, root_squash)"
ActiveSessions = 26
IsLive = $false
}
)
if ([string]::IsNullOrWhiteSpace($Filter)) {
return $shares
} else {
return @($shares | Where-Object {
$_.ShareName -like "*$Filter*" -or
$_.Path -like "*$Filter*"
})
}
}
+314
View File
@@ -0,0 +1,314 @@
# Modules\SqlService.ps1
# Microsoft SQL Server Management Service Module (.NET System.Data.SqlClient Live & Fallback)
function Build-SqlConnectionString {
param(
[string]$Server,
[string]$AuthType = "Integrated",
[string]$Username = "",
[string]$Password = "",
[string]$DefaultDatabase = "master",
[int]$Timeout = 10
)
$builder = New-Object System.Data.SqlClient.SqlConnectionStringBuilder
$builder["Data Source"] = $Server
$builder["Initial Catalog"] = if ([string]::IsNullOrWhiteSpace($DefaultDatabase)) { "master" } else { $DefaultDatabase }
$builder["Connect Timeout"] = $Timeout
$builder["Application Name"] = "EnterpriseAdminStudio"
if ($AuthType -eq "Integrated" -or [string]::IsNullOrWhiteSpace($Username)) {
$builder["Integrated Security"] = $true
} else {
$builder["Integrated Security"] = $false
$builder["User ID"] = $Username
$builder["Password"] = $Password
}
return $builder.ConnectionString
}
function Test-SqlConnection {
param(
[string]$Server,
[string]$AuthType = "Integrated",
[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)"
}
}
return [PSCustomObject]@{ Success = $true; Message = "SQL Server 연결 성공" }
} catch {
return [PSCustomObject]@{
Success = $false
Message = "SQL Server 연결 실패: $($_.Exception.Message)"
}
} finally {
if ($conn.State -eq [System.Data.ConnectionState]::Open) {
$conn.Close()
}
$conn.Dispose()
}
}
function Get-SqlDatabases {
param(
[string]$Filter = "",
[object]$Config = $null
)
$useLive = if ($Config -and $Config.MsSql) { [bool]$Config.MsSql.UseLive } else { $false }
if ($useLive) {
$server = $Config.MsSql.Server
$auth = $Config.MsSql.AuthType
$user = $Config.MsSql.Username
$pass = $Config.MsSql.Password
$timeout = if ($Config.MsSql.ConnectionTimeout) { [int]$Config.MsSql.ConnectionTimeout } else { 10 }
$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 = @"
SELECT
d.name AS DatabaseName,
d.state_desc AS StateDesc,
d.recovery_model_desc AS RecoveryModel,
d.collation_name AS CollationName,
CAST(ROUND(SUM(CAST(mf.size AS bigint)) * 8.0 / 1024, 1) AS varchar(30)) + ' MB' AS TotalSizeMB,
ISNULL(CONVERT(varchar(20), MAX(b.backup_finish_date), 120) + ' (' + b.type + ')', '백업 없음') AS LastBackupDate
FROM sys.databases d
LEFT JOIN sys.master_files mf ON d.database_id = mf.database_id
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
$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()
} catch {
Write-Warning "실제 SQL Server 조회 실패: $($_.Exception.Message) (Mock 데이터로 전환합니다)"
} finally {
if ($conn.State -eq [System.Data.ConnectionState]::Open) { $conn.Close() }
$conn.Dispose()
}
}
return Get-SqlSampleDatabases -Filter $Filter
}
function Invoke-SqlBackupAction {
param(
[string]$DatabaseName,
[string]$BackupType = "FULL",
[object]$Config = $null
)
$useLive = if ($Config -and $Config.MsSql) { [bool]$Config.MsSql.UseLive } else { $false }
if ($useLive) {
$server = $Config.MsSql.Server
$auth = $Config.MsSql.AuthType
$user = $Config.MsSql.Username
$pass = $Config.MsSql.Password
$backupDir = if ($Config.MsSql.BackupDirectory) { $Config.MsSql.BackupDirectory } else { "C:\Backups" }
if (-not (Test-Path $backupDir)) {
New-Item -ItemType Directory -Path $backupDir -Force | Out-Null
}
$timeStamp = (Get-Date).ToString("yyyyMMdd_HHmmss")
$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
return [PSCustomObject]@{
Success = $true
Database = $DatabaseName
BackupPath = $targetPath
Message = "실제 SQL Server 데이터베이스 [$DatabaseName] 백업이 완료되었습니다.`n저장 경로: $targetPath"
}
} catch {
return [PSCustomObject]@{
Success = $false
Database = $DatabaseName
Message = "SQL 백업 실패: $($_.Exception.Message)"
}
} finally {
if ($conn.State -eq [System.Data.ConnectionState]::Open) { $conn.Close() }
$conn.Dispose()
}
}
return [PSCustomObject]@{
Success = $true
Database = $DatabaseName
BackupPath = "\\backup-nas.corp.local\sqlbackups\$DatabaseName-$((Get-Date).ToString('yyyyMMdd_HHmmss')).bak"
Message = "[데모 모드] 데이터베이스 [$DatabaseName]의 $BackupType 백업이 가상 완료되었습니다."
}
}
function Invoke-SqlQuerySnippet {
param(
[string]$Query,
[object]$Config = $null
)
$useLive = if ($Config -and $Config.MsSql) { [bool]$Config.MsSql.UseLive } else { $false }
if ($useLive) {
$server = $Config.MsSql.Server
$auth = $Config.MsSql.AuthType
$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
$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())"
}
} catch {
return [PSCustomObject]@{
Success = $false
RowCount = 0
Output = "(오류 발생)`n$($_.Exception.Message)"
}
} finally {
if ($conn.State -eq [System.Data.ConnectionState]::Open) { $conn.Close() }
$conn.Dispose()
}
}
return [PSCustomObject]@{
Success = $true
RowCount = 10
Output = "(데모 모드 성공: 10개 행 반환됨 - $((Get-Date).ToString('HH:mm:ss')))`n1. tbl_Users (Row: 14,200)`n2. tbl_AuditLogs (Row: 240,119)`n3. tbl_Orders (Row: 82,900)"
}
}
function Get-SqlSampleDatabases {
param([string]$Filter = "")
$databases = @(
[PSCustomObject]@{
DatabaseName = "ERP_PRODUCTION"
StateDesc = "ONLINE"
TotalSizeMB = "56,370 MB"
FreeSpaceMB = "6,410 MB"
RecoveryModel = "FULL"
CollationName = "Korean_Wansung_CI_AS"
LastBackupDate = "2026-10-20 02:00:15 (Full)"
IsLive = $false
},
[PSCustomObject]@{
DatabaseName = "HR_PORTAL_DB"
StateDesc = "ONLINE"
TotalSizeMB = "12,450 MB"
FreeSpaceMB = "2,120 MB"
RecoveryModel = "FULL"
CollationName = "Korean_Wansung_CI_AS"
LastBackupDate = "2026-10-20 02:30:00 (Full)"
IsLive = $false
},
[PSCustomObject]@{
DatabaseName = "WAREHOUSE_ANALYTICS"
StateDesc = "ONLINE"
TotalSizeMB = "184,000 MB"
FreeSpaceMB = "18,900 MB"
RecoveryModel = "SIMPLE"
CollationName = "Korean_Wansung_CI_AS"
LastBackupDate = "2026-10-19 23:00:12 (Full)"
IsLive = $false
},
[PSCustomObject]@{
DatabaseName = "DEV_STAGING_TEST"
StateDesc = "ONLINE"
TotalSizeMB = "4,200 MB"
FreeSpaceMB = "850 MB"
RecoveryModel = "SIMPLE"
CollationName = "Korean_Wansung_CI_AS"
LastBackupDate = "2026-10-18 10:15:00 (Full)"
IsLive = $false
},
[PSCustomObject]@{
DatabaseName = "ARCHIVE_2025"
StateDesc = "READ_ONLY"
TotalSizeMB = "92,500 MB"
FreeSpaceMB = "45 MB"
RecoveryModel = "SIMPLE"
CollationName = "Korean_Wansung_CI_AS"
LastBackupDate = "2026-01-01 00:00:00 (Full)"
IsLive = $false
}
)
if ([string]::IsNullOrWhiteSpace($Filter)) {
return $databases
} else {
return @($databases | Where-Object {
$_.DatabaseName -like "*$Filter*" -or
$_.StateDesc -like "*$Filter*"
})
}
}
+92
View File
@@ -0,0 +1,92 @@
# Enterprise Admin Studio (통합 인프라 관리 도구)
![PowerShell](https://img.shields.io/badge/PowerShell-7.x%20%7C%205.1-blue?logo=powershell)
![.NET](https://img.shields.io/badge/.NET-WPF%20XAML-purple?logo=dotnet)
![Architecture](https://img.shields.io/badge/Architecture-Modular%20MVVM%20Ready-green)
![License](https://img.shields.io/badge/License-Enterprise-orange)
PowerShell 7 / Windows PowerShell 5.1과 .NET WPF(XAML)를 기반으로 구축된 **모던 UI 통합 인프라 관리 도구**입니다.
Active Directory, NetApp ONTAP 스토리지(Qtree/Quota/Share), MS SQL 데이터베이스를 단일 대시보드에서 제어할 수 있도록 설계되었으며, **실제 장비 연결(Live Mode)**과 **데모/시뮬레이션 모드(Mock Mode)**를 모두 지원합니다.
또한 **향후 C# (.NET 8+ / WPF / WinUI 3)으로의 손쉬운 마이그레이션**을 고려하여 완벽한 모듈화 및 XAML 분리 구조를 채택했습니다.
---
## 📚 상세 문서 바로가기
- 🏗️ **[시스템 아키텍처 및 C# 전환 가이드 (ARCHITECTURE.md)](ARCHITECTURE.md)**: 전체 시스템 다이어그램, 계층별 상세 설계, C# 인터페이스 매핑 가이드
- 📖 **[사용자 가이드 및 운영 매뉴얼 (USAGE.md)](USAGE.md)**: 설치 요구사항, 모드 전환 방법, 메뉴별 세부 기능 조작법, 트러블슈팅
- 📜 **[프로젝트 변경 이력 (CHANGELOG.md)](CHANGELOG.md)**: 버전별 릴리즈 노트
---
## 🎨 주요 UI/UX 특징
- **Fluent / Windows 11 Modern Dark 테마**:
- 부드러운 다크 색상 팔레트 (`#0F172A`, `#1E293B`, `#3B82F6`)
- 둥근 모서리(Border Radius), 카드(Card) 레이아웃, 모던 버튼 및 텍스트박스 스타일
- Windows 기본 내장 폰트인 `Segoe MDL2 Assets` 글리프 아이콘 활용 (추가 폰트 설치 불필요)
- **사이드바 내비게이션 & 동적 페이지 전환**:
- 왼쪽 라디오 버튼 메뉴를 통해 부드러운 뷰(View) 전환 지원
- 뷰 캐싱(View Caching) 기법을 적용하여 페이지 전환 속도 최적화
- **실시간 동작 모드 시각화**:
- 상단 헤더에 현재 모드(`실제 연결 모드 (Live)` vs `데모/모의 모드 (Mock)`)가 색상 배지로 실시간 표출
---
## 📂 프로젝트 구조
```
ModernAdminStudio/
│
├── Start-App.ps1 # 메인 실행 진입점 (XAML 파싱, 이벤트 라우팅, 페이지 네비게이션)
├── MainWindow.xaml # Fluent 사이드바 및 헤더, View 호스트를 포함한 메인 윈도우
├── config.json # 인프라 접속 엔드포인트 및 모드 설정 파일
├── Run.bat # 원클릭 실행 배치 파일 (PowerShell 7 우선 자동 감지)
├── README.md # 프로젝트 종합 안내
├── ARCHITECTURE.md # 시스템 아키텍처 및 C# 마이그레이션 설계서
├── USAGE.md # 사용자 가이드 및 운영 매뉴얼
├── CHANGELOG.md # 버전별 변경 이력
│
├── Styles/
│ └── Theme.xaml # 글로벌 디자인 테마, 모던 컬러 브러시, 버튼/입력/그리드 스타일
│
├── Views/ # 개별 메뉴별 독립 XAML 뷰 (UserControl 구조)
│ ├── DashboardView.xaml # 인프라 종합 현황, KPI 카드, 최근 감사 이력, 빠른 실행
│ ├── ActiveDirectoryView.xaml # AD 사용자/그룹 목록, 계정 잠금 해제, 암호 초기화, 속성 패널
│ ├── NetAppView.xaml # SVM 선택, Qtree 관리, Quota 한도/사용률, CIFS/NFS 공유 제어
│ ├── SqlDatabaseView.xaml # SQL 인스턴스, DB 상태, 즉시 백업 트리거, 간이 T-SQL 콘솔
│ └── SettingsView.xaml # AD/스토리지/SQL 접속 엔드포인트 및 전체 연결 일괄 진단
│
└── Modules/ # 백엔드 비즈니스 로직 (C# Service로 1:1 전환 가능)
├── ConfigService.ps1 # JSON 설정 로드/저장 관리 모듈
├── ADService.ps1 # Active Directory (.NET System.DirectoryServices) 모듈
├── NetAppService.ps1 # NetApp ONTAP REST API v2 연동 모듈
└── SqlService.ps1 # MS SQL Server (.NET System.Data.SqlClient) 모듈
```
---
## 🚀 빠른 시작 (Quick Start)
### 1. 원클릭 실행
`Run.bat`을 더블 클릭하여 실행합니다.
### 2. PowerShell에서 직접 실행
```powershell
cd C:\dev\ws\ModernAdminStudio
pwsh -ExecutionPolicy Bypass -File .\Start-App.ps1
```
*(Windows PowerShell 5.1에서도 동일하게 실행 가능합니다.)*
---
## ⚙️ 실제 인프라 연결 설정 요약
1. 앱 실행 후 좌측 사이드바에서 **[환경설정 및 연결]** 메뉴 클릭
2. 상단의 `실제 연결 모드 활성화 (Live Production Mode)` 체크
3. 각 대상 시스템 정보 입력:
- **Active Directory**: 도메인 컨트롤러 FQDN (예: `dc01.corp.local`), 기본 OU DN, 관리자 계정 정보
- **NetApp ONTAP**: 클러스터 HTTPS 주소 (예: `https://10.200.10.50`), 대상 SVM, 관리자 계정/비밀번호
- **MS SQL Server**: 인스턴스 주소 (예: `localhost` 또는 `SRV\INSTANCE`), 인증 방식(Windows/SQL)
4. 각 섹션의 **[연결 테스트]** 또는 하단의 **[전체 인프라 연결 상태 일괄 진단]** 실행
5. **[설정 저장 및 적용]**을 클릭하여 즉시 적용
+11
View File
@@ -0,0 +1,11 @@
@echo off
chcp 65001 > nul
title Enterprise Admin Studio
where pwsh >nul 2>nul
if %ERRORLEVEL% equ 0 (
start "" pwsh -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "%~dp0Start-App.ps1"
) else (
start "" powershell -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "%~dp0Start-App.ps1"
)
exit
+592
View File
@@ -0,0 +1,592 @@
# ==============================================================================
# Enterprise Admin Studio - Main Entrypoint
# PowerShell WPF Modern UI Infrastructure Management Tool
# ==============================================================================
[CmdletBinding()]
param(
[switch]$NoShow
)
$ErrorActionPreference = 'Stop'
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
# 1. Load WPF / .NET GUI Assemblies
Add-Type -AssemblyName PresentationFramework
Add-Type -AssemblyName PresentationCore
Add-Type -AssemblyName WindowsBase
Add-Type -AssemblyName System.Data
Add-Type -AssemblyName System.DirectoryServices
# 2. Import Business Service Modules
. (Join-Path $ScriptDir "Modules\ConfigService.ps1")
. (Join-Path $ScriptDir "Modules\ADService.ps1")
. (Join-Path $ScriptDir "Modules\NetAppService.ps1")
. (Join-Path $ScriptDir "Modules\SqlService.ps1")
# Global Configuration Object
$global:AppConfig = Get-AppConfig
# Helper: Load XAML File safely
function Load-XamlElement {
param([string]$Path)
if (-not (Test-Path $Path)) {
throw "XAML file not found: $Path"
}
$rawContent = [System.IO.File]::ReadAllText($Path, [System.Text.Encoding]::UTF8)
$cleanContent = $rawContent -replace 'x:Class="[^"]*"', ''
$stringReader = New-Object System.IO.StringReader($cleanContent)
$xmlReader = [System.Xml.XmlReader]::Create($stringReader)
try {
return [System.Windows.Markup.XamlReader]::Load($xmlReader)
}
finally {
$xmlReader.Close()
$stringReader.Close()
}
}
# 3. Initialize WPF Application & Global Theme Resources
if ($null -eq [System.Windows.Application]::Current) {
$app = New-Object System.Windows.Application
} else {
$app = [System.Windows.Application]::Current
}
$themePath = Join-Path $ScriptDir "Styles\Theme.xaml"
$themeResource = Load-XamlElement -Path $themePath
$app.Resources.MergedDictionaries.Add($themeResource)
# 4. Load MainWindow XAML
$mainXamlPath = Join-Path $ScriptDir "MainWindow.xaml"
$mainWindow = Load-XamlElement -Path $mainXamlPath
# Find Main Window Controls
$mainContentHost = $mainWindow.FindName("MainContentHost")
$txtHeaderTitle = $mainWindow.FindName("TxtHeaderTitle")
$txtHeaderSubtitle = $mainWindow.FindName("TxtHeaderSubtitle")
$txtSystemClock = $mainWindow.FindName("TxtSystemClock")
$txtModeBadge = $mainWindow.FindName("TxtModeBadge")
$badgeMode = $mainWindow.FindName("BadgeMode")
$txtGlobalStatus = $mainWindow.FindName("TxtGlobalStatus")
$dotGlobalStatus = $mainWindow.FindName("DotGlobalStatus")
$navDashboard = $mainWindow.FindName("NavDashboard")
$navAD = $mainWindow.FindName("NavAD")
$navNetApp = $mainWindow.FindName("NavNetApp")
$navSql = $mainWindow.FindName("NavSql")
$navSettings = $mainWindow.FindName("NavSettings")
$btnGlobalRefresh = $mainWindow.FindName("BtnGlobalRefresh")
# Function to Update Mode Badge UI
function Update-ModeBadgeUI {
$isLive = ($global:AppConfig.Application.LiveMode -or
$global:AppConfig.ActiveDirectory.UseLive -or
$global:AppConfig.NetApp.UseLive -or
$global:AppConfig.MsSql.UseLive)
if ($txtModeBadge -and $badgeMode) {
if ($isLive) {
$txtModeBadge.Text = "실제 연결 모드 (Live)"
$badgeMode.Background = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#10B981")
} else {
$txtModeBadge.Text = "데모/모의 모드 (Mock)"
$badgeMode.Background = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#6366F1")
}
}
}
Update-ModeBadgeUI
if ($txtSystemClock) {
$txtSystemClock.Text = (Get-Date).ToString("yyyy-MM-dd HH:mm")
}
# View Cache
$ViewCache = @{}
# Function to Switch Navigation Pages
function Navigate-ToPage {
param([string]$PageName)
switch ($PageName) {
"Dashboard" {
$txtHeaderTitle.Text = "통합 대시보드"
$txtHeaderSubtitle.Text = "전체 인프라 실시간 상태 개요 및 최근 감사 로그"
if (-not $ViewCache.ContainsKey("Dashboard")) {
$view = Load-XamlElement -Path (Join-Path $ScriptDir "Views\DashboardView.xaml")
$activityGrid = $view.FindName("ActivityGrid")
if ($activityGrid) {
$activityLog = @(
[PSCustomObject]@{ Timestamp = (Get-Date).AddMinutes(-3).ToString("HH:mm:ss"); TargetSystem = "Active Directory"; ActionType = "User Query"; Description = "LDAP 디렉터리 동기화 완료"; Status = "정상" },
[PSCustomObject]@{ Timestamp = (Get-Date).AddMinutes(-12).ToString("HH:mm:ss"); TargetSystem = "NetApp ONTAP"; ActionType = "Quota Check"; Description = "ONTAP REST API 헬스체크 통과"; Status = "정상" },
[PSCustomObject]@{ Timestamp = (Get-Date).AddMinutes(-35).ToString("HH:mm:ss"); TargetSystem = "MS SQL Server"; ActionType = "DB Inspection"; Description = "SQL Server 온라인 DB 카운트 정상 (16개)"; Status = "정상" },
[PSCustomObject]@{ Timestamp = (Get-Date).AddHours(-2).ToString("HH:mm:ss"); TargetSystem = "NetApp ONTAP"; ActionType = "Share Checked"; Description = "CIFS/NFS 공유 정책 감사 통과"; Status = "정상" }
)
$activityGrid.ItemsSource = $activityLog
}
$quickAd = $view.FindName("QuickBtnAD")
if ($quickAd) { $quickAd.add_Click({ $navAD.IsChecked = $true }) }
$quickQuota = $view.FindName("QuickBtnQuota")
if ($quickQuota) { $quickQuota.add_Click({ $navNetApp.IsChecked = $true }) }
$quickShare = $view.FindName("QuickBtnShare")
if ($quickShare) { $quickShare.add_Click({ $navNetApp.IsChecked = $true }) }
$quickSql = $view.FindName("QuickBtnSql")
if ($quickSql) { $quickSql.add_Click({ $navSql.IsChecked = $true }) }
$ViewCache["Dashboard"] = $view
}
$mainContentHost.Content = $ViewCache["Dashboard"]
}
"AD" {
$txtHeaderTitle.Text = "Active Directory 관리"
$txtHeaderSubtitle.Text = "도메인 사용자 계정 관리, 상태 변경, 암호 초기화 및 그룹 할당"
if (-not $ViewCache.ContainsKey("AD")) {
$view = Load-XamlElement -Path (Join-Path $ScriptDir "Views\ActiveDirectoryView.xaml")
$grid = $view.FindName("AdUsersGrid")
$txtSearch = $view.FindName("TxtAdSearch")
$btnSearch = $view.FindName("BtnAdSearch")
$btnRefresh = $view.FindName("BtnAdRefresh")
$btnUnlock = $view.FindName("BtnAdUnlock")
$btnResetPwd = $view.FindName("BtnAdResetPwd")
$btnToggleEnable = $view.FindName("BtnAdToggleEnable")
$txtStatusMsg = $view.FindName("TxtAdStatusMsg")
$txtTotalCount = $view.FindName("TxtAdTotalCount")
$txtDetailName = $view.FindName("TxtDetailName")
$txtDetailEmail = $view.FindName("TxtDetailEmail")
$txtDetailOu = $view.FindName("TxtDetailOu")
$txtPropStatus = $view.FindName("TxtPropStatus")
$listGroups = $view.FindName("ListMemberGroups")
$reloadAd = {
$filter = if ($txtSearch) { $txtSearch.Text } else { "" }
$users = Get-ADUserList -Filter $filter -Config $global:AppConfig
$grid.ItemsSource = $users
if ($txtTotalCount) { $txtTotalCount.Text = "총 $($users.Count) 명의 사용자" }
if ($txtStatusMsg) {
$isLive = ($users.Count -gt 0 -and $users[0].IsLive)
$modeText = if ($isLive) { "실제 도메인 컨트롤러 연결 중 ($($global:AppConfig.ActiveDirectory.DomainController))" } else { "데모 시뮬레이션 모드 (config.json에서 실제 연결 설정 가능)" }
$txtStatusMsg.Text = "상태: $modeText"
}
}
if ($btnSearch) { $btnSearch.add_Click($reloadAd) }
if ($btnRefresh) { $btnRefresh.add_Click($reloadAd) }
if ($grid) {
$grid.add_SelectionChanged({
$selected = $grid.SelectedItem
if ($selected) {
$txtDetailName.Text = "$($selected.DisplayName) ($($selected.SamAccountName))"
$txtDetailEmail.Text = $selected.UserPrincipalName
$txtDetailOu.Text = $selected.DistinguishedName
$txtPropStatus.Text = "$($selected.StatusText) (Locked: $($selected.LockedText))"
$listGroups.Items.Clear()
$listGroups.Items.Add("Domain Users") | Out-Null
$listGroups.Items.Add("Remote Desktop Users") | Out-Null
if ($selected.Department -like "*인프라*" -or $selected.Department -like "*시스템*") {
$listGroups.Items.Add("Storage-Admins") | Out-Null
$listGroups.Items.Add("Server-Operators") | Out-Null
}
}
})
}
if ($btnUnlock) {
$btnUnlock.add_Click({
$selected = $grid.SelectedItem
if ($selected) {
$res = Unlock-ADUserAccount -SamAccountName $selected.SamAccountName -Config $global:AppConfig
[System.Windows.MessageBox]::Show($res.Message, "AD 잠금 해제", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
&$reloadAd
} else {
[System.Windows.MessageBox]::Show("목록에서 잠금 해제할 사용자를 선택하십시오.", "알림", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
}
})
}
if ($btnResetPwd) {
$btnResetPwd.add_Click({
$selected = $grid.SelectedItem
if ($selected) {
$res = Reset-ADUserPassword -SamAccountName $selected.SamAccountName -Config $global:AppConfig
[System.Windows.MessageBox]::Show($res.Message, "AD 암호 초기화", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
} else {
[System.Windows.MessageBox]::Show("목록에서 암호를 초기화할 사용자를 선택하십시오.", "알림", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
}
})
}
if ($btnToggleEnable) {
$btnToggleEnable.add_Click({
$selected = $grid.SelectedItem
if ($selected) {
$newState = (-not $selected.Enabled)
$res = Set-ADUserAccountState -SamAccountName $selected.SamAccountName -Enable $newState -Config $global:AppConfig
[System.Windows.MessageBox]::Show($res.Message, "AD 계정 상태 전환", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
&$reloadAd
} else {
[System.Windows.MessageBox]::Show("목록에서 사용자를 선택하십시오.", "알림", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
}
})
}
&$reloadAd
$ViewCache["AD"] = $view
}
$mainContentHost.Content = $ViewCache["AD"]
}
"NetApp" {
$txtHeaderTitle.Text = "NetApp 스토리지 관리"
$txtHeaderSubtitle.Text = "ONTAP Qtree 생성, Quota 용량 정책 및 CIFS/NFS 공유 제어"
if (-not $ViewCache.ContainsKey("NetApp")) {
$view = Load-XamlElement -Path (Join-Path $ScriptDir "Views\NetAppView.xaml")
$qtreeGrid = $view.FindName("QtreeGrid")
$quotaGrid = $view.FindName("QuotaGrid")
$shareGrid = $view.FindName("ShareGrid")
$btnRefreshStorage = $view.FindName("BtnRefreshStorage")
$btnNewQtree = $view.FindName("BtnNewQtree")
$btnNewShare = $view.FindName("BtnNewShare")
$txtNetAppStatus = $view.FindName("TxtNetAppStatusMsg")
$reloadNetApp = {
$qtrees = Get-NetAppQtrees -Config $global:AppConfig
$quotas = Get-NetAppQuotas -Config $global:AppConfig
$shares = Get-NetAppShares -Config $global:AppConfig
if ($qtreeGrid) { $qtreeGrid.ItemsSource = $qtrees }
if ($quotaGrid) { $quotaGrid.ItemsSource = $quotas }
if ($shareGrid) { $shareGrid.ItemsSource = $shares }
$isLive = ($qtrees.Count -gt 0 -and $qtrees[0].IsLive)
if ($txtNetAppStatus) {
$modeText = if ($isLive) { "ONTAP REST API 실시간 연결됨 ($($global:AppConfig.NetApp.ClusterEndpoint))" } else { "데모/시뮬레이션 모드 (ONTAP REST API v2 호환 구조)" }
$txtNetAppStatus.Text = "스토리지 응답: $modeText"
}
}
if ($btnRefreshStorage) { $btnRefreshStorage.add_Click($reloadNetApp) }
if ($btnNewQtree) {
$btnNewQtree.add_Click({
$res = New-NetAppQtreeAction -Volume "vol_cifs_share01" -QtreeName "qtree_new_$((Get-Date).ToString('MMdd_HHmm'))" -Config $global:AppConfig
[System.Windows.MessageBox]::Show($res.Message, "NetApp Qtree 생성", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
&$reloadNetApp
})
}
if ($btnNewShare) {
$btnNewShare.add_Click({
$shareName = "Share_$((Get-Date).ToString('MMdd_HHmm'))"
$res = New-NetAppShareAction -ShareName $shareName -Path "/vol/vol_cifs_share01/$shareName" -Comment "관리 도구 생성 공유" -Config $global:AppConfig
[System.Windows.MessageBox]::Show($res.Message, "NetApp CIFS 공유 생성", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
&$reloadNetApp
})
}
&$reloadNetApp
$ViewCache["NetApp"] = $view
}
$mainContentHost.Content = $ViewCache["NetApp"]
}
"Sql" {
$txtHeaderTitle.Text = "MS SQL 데이터베이스 관리"
$txtHeaderSubtitle.Text = "인스턴스 상태, 데이터베이스 모니터링, 백업/복원 및 빠른 쿼리 실행"
if (-not $ViewCache.ContainsKey("Sql")) {
$view = Load-XamlElement -Path (Join-Path $ScriptDir "Views\SqlDatabaseView.xaml")
$dbGrid = $view.FindName("SqlDbGrid")
$btnSqlRefresh = $view.FindName("BtnSqlRefresh")
$btnTestConn = $view.FindName("BtnTestSqlConnection")
$btnBackup = $view.FindName("BtnBackupDb")
$btnExecQuery = $view.FindName("BtnExecuteQuery")
$txtSqlScript = $view.FindName("TxtSqlScript")
$txtSqlOutput = $view.FindName("TxtSqlOutput")
$txtSqlStatusMsg = $view.FindName("TxtSqlStatusMsg")
$txtSqlTotalCount = $view.FindName("TxtSqlTotalCount")
$reloadSql = {
$dbs = Get-SqlDatabases -Config $global:AppConfig
if ($dbGrid) { $dbGrid.ItemsSource = $dbs }
if ($txtSqlTotalCount) { $txtSqlTotalCount.Text = "총 $($dbs.Count)개 데이터베이스" }
if ($txtSqlStatusMsg) {
$isLive = ($dbs.Count -gt 0 -and $dbs[0].IsLive)
$modeText = if ($isLive) { "실제 SQL Server 연결 ($($global:AppConfig.MsSql.Server))" } else { "데모/시뮬레이션 모드 (System.Data.SqlClient 연동 완비)" }
$txtSqlStatusMsg.Text = "상태: $modeText"
}
}
if ($btnSqlRefresh) { $btnSqlRefresh.add_Click($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
$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)
})
}
if ($btnBackup) {
$btnBackup.add_Click({
$selected = $dbGrid.SelectedItem
$dbName = if ($selected) { $selected.DatabaseName } else { "ERP_PRODUCTION" }
$res = Invoke-SqlBackupAction -DatabaseName $dbName -Config $global:AppConfig
$img = if ($res.Success) { [System.Windows.MessageBoxImage]::Information } else { [System.Windows.MessageBoxImage]::Error }
[System.Windows.MessageBox]::Show($res.Message, "SQL 백업 결과", [System.Windows.MessageBoxButton]::OK, $img)
&$reloadSql
})
}
if ($btnExecQuery) {
$btnExecQuery.add_Click({
$query = if ($txtSqlScript) { $txtSqlScript.Text } else { "SELECT 1" }
$res = Invoke-SqlQuerySnippet -Query $query -Config $global:AppConfig
if ($txtSqlOutput) { $txtSqlOutput.Text = $res.Output }
})
}
&$reloadSql
$ViewCache["Sql"] = $view
}
$mainContentHost.Content = $ViewCache["Sql"]
}
"Settings" {
$txtHeaderTitle.Text = "환경설정 및 실제 인프라 연결 구성"
$txtHeaderSubtitle.Text = "인프라 모듈 접속 엔드포인트, 자격 증명 및 시스템 기본값 관리"
if (-not $ViewCache.ContainsKey("Settings")) {
$view = Load-XamlElement -Path (Join-Path $ScriptDir "Views\SettingsView.xaml")
# Controls
$chkGlobalLive = $view.FindName("ChkGlobalLiveMode")
$chkAdLive = $view.FindName("ChkAdLive")
$txtAdDc = $view.FindName("TxtAdDc")
$txtAdBaseOu = $view.FindName("TxtAdBaseOu")
$chkAdLdaps = $view.FindName("ChkAdLdaps")
$txtAdUser = $view.FindName("TxtAdUser")
$txtAdPassword = $view.FindName("TxtAdPassword")
$btnTestAd = $view.FindName("BtnTestAd")
$chkNetAppLive = $view.FindName("ChkNetAppLive")
$txtNetAppEndpoint = $view.FindName("TxtNetAppEndpoint")
$txtNetAppDefaultSvm = $view.FindName("TxtNetAppDefaultSvm")
$chkNetAppIgnoreSsl = $view.FindName("ChkNetAppIgnoreSsl")
$txtNetAppUser = $view.FindName("TxtNetAppUser")
$txtNetAppPassword = $view.FindName("TxtNetAppPassword")
$btnTestNetApp = $view.FindName("BtnTestNetApp")
$chkSqlLive = $view.FindName("ChkSqlLive")
$txtSqlServer = $view.FindName("TxtSqlServer")
$cmbSqlAuth = $view.FindName("CmbSqlAuthType")
$txtSqlBackupDir = $view.FindName("TxtSqlBackupDir")
$txtSqlUser = $view.FindName("TxtSqlUser")
$txtSqlPassword = $view.FindName("TxtSqlPassword")
$btnTestSql = $view.FindName("BtnTestSql")
$btnTestAll = $view.FindName("BtnTestAllConnections")
$btnSave = $view.FindName("BtnSaveSettings")
$btnReload = $view.FindName("BtnReloadConfig")
# Populate from Config
$loadSettingsToUI = {
$c = $global:AppConfig
if ($chkGlobalLive) { $chkGlobalLive.IsChecked = [bool]$c.Application.LiveMode }
if ($chkAdLive) { $chkAdLive.IsChecked = [bool]$c.ActiveDirectory.UseLive }
if ($txtAdDc) { $txtAdDc.Text = $c.ActiveDirectory.DomainController }
if ($txtAdBaseOu) { $txtAdBaseOu.Text = $c.ActiveDirectory.BaseOU }
if ($chkAdLdaps) { $chkAdLdaps.IsChecked = [bool]$c.ActiveDirectory.UseLdaps }
if ($txtAdUser) { $txtAdUser.Text = $c.ActiveDirectory.Username }
if ($txtAdPassword) { $txtAdPassword.Password = $c.ActiveDirectory.Password }
if ($chkNetAppLive) { $chkNetAppLive.IsChecked = [bool]$c.NetApp.UseLive }
if ($txtNetAppEndpoint) { $txtNetAppEndpoint.Text = $c.NetApp.ClusterEndpoint }
if ($txtNetAppDefaultSvm) { $txtNetAppDefaultSvm.Text = $c.NetApp.DefaultSvm }
if ($chkNetAppIgnoreSsl) { $chkNetAppIgnoreSsl.IsChecked = [bool]$c.NetApp.IgnoreSslErrors }
if ($txtNetAppUser) { $txtNetAppUser.Text = $c.NetApp.Username }
if ($txtNetAppPassword) { $txtNetAppPassword.Password = $c.NetApp.Password }
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 ($txtSqlBackupDir) { $txtSqlBackupDir.Text = $c.MsSql.BackupDirectory }
if ($txtSqlUser) { $txtSqlUser.Text = $c.MsSql.Username }
if ($txtSqlPassword) { $txtSqlPassword.Password = $c.MsSql.Password }
}
&$loadSettingsToUI
# Test AD
if ($btnTestAd) {
$btnTestAd.add_Click({
$res = Test-ADConnection -DomainController $txtAdDc.Text -BaseOU $txtAdBaseOu.Text -Username $txtAdUser.Text -Password $txtAdPassword.Password -UseLdaps $chkAdLdaps.IsChecked
$img = if ($res.Success) { [System.Windows.MessageBoxImage]::Information } else { [System.Windows.MessageBoxImage]::Warning }
[System.Windows.MessageBox]::Show($res.Message, "AD 연결 테스트", [System.Windows.MessageBoxButton]::OK, $img)
})
}
# Test NetApp
if ($btnTestNetApp) {
$btnTestNetApp.add_Click({
$res = Test-NetAppConnection -ClusterEndpoint $txtNetAppEndpoint.Text -Username $txtNetAppUser.Text -Password $txtNetAppPassword.Password -IgnoreSsl $chkNetAppIgnoreSsl.IsChecked
$img = if ($res.Success) { [System.Windows.MessageBoxImage]::Information } else { [System.Windows.MessageBoxImage]::Warning }
[System.Windows.MessageBox]::Show($res.Message, "NetApp 연결 테스트", [System.Windows.MessageBoxButton]::OK, $img)
})
}
# Test SQL
if ($btnTestSql) {
$btnTestSql.add_Click({
$authType = if ($cmbSqlAuth.SelectedIndex -eq 1) { "Sql" } else { "Integrated" }
$res = Test-SqlConnection -Server $txtSqlServer.Text -AuthType $authType -Username $txtSqlUser.Text -Password $txtSqlPassword.Password
$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)
})
}
# Test All
if ($btnTestAll) {
$btnTestAll.add_Click({
$adRes = Test-ADConnection -DomainController $txtAdDc.Text -BaseOU $txtAdBaseOu.Text -Username $txtAdUser.Text -Password $txtAdPassword.Password -UseLdaps $chkAdLdaps.IsChecked
$netappRes = Test-NetAppConnection -ClusterEndpoint $txtNetAppEndpoint.Text -Username $txtNetAppUser.Text -Password $txtNetAppPassword.Password -IgnoreSsl $chkNetAppIgnoreSsl.IsChecked
$authType = if ($cmbSqlAuth.SelectedIndex -eq 1) { "Sql" } else { "Integrated" }
$sqlRes = Test-SqlConnection -Server $txtSqlServer.Text -AuthType $authType -Username $txtSqlUser.Text -Password $txtSqlPassword.Password
$report = @"
=== 전체 인프라 일괄 진단 결과 ===
[1] Active Directory (DC: $($txtAdDc.Text))
- 상태: $(if ($adRes.Success) { '√ 정상 연결' } else { 'X 연결 실패' })
- 세부: $($adRes.Message)
[2] NetApp ONTAP REST API ($($txtNetAppEndpoint.Text))
- 상태: $(if ($netappRes.Success) { '√ 정상 연결' } else { 'X 연결 실패' })
- 세부: $($netappRes.Message)
[3] MS SQL Server ($($txtSqlServer.Text))
- 상태: $(if ($sqlRes.Success) { '√ 정상 연결' } else { 'X 연결 실패' })
- 세부: $($sqlRes.Message)
"@
[System.Windows.MessageBox]::Show($report, "전체 인프라 연결 진단", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
})
}
# Save Settings
if ($btnSave) {
$btnSave.add_Click({
$authType = if ($cmbSqlAuth.SelectedIndex -eq 1) { "Sql" } else { "Integrated" }
$newConfig = @{
Application = @{
LiveMode = [bool]$chkGlobalLive.IsChecked
Theme = "Dark"
AutoRefreshSeconds = 60
}
ActiveDirectory = @{
UseLive = [bool]($chkGlobalLive.IsChecked -or $chkAdLive.IsChecked)
DomainController = $txtAdDc.Text
Domain = "corp.local"
BaseOU = $txtAdBaseOu.Text
UseLdaps = [bool]$chkAdLdaps.IsChecked
Username = $txtAdUser.Text
Password = $txtAdPassword.Password
}
NetApp = @{
UseLive = [bool]($chkGlobalLive.IsChecked -or $chkNetAppLive.IsChecked)
ClusterEndpoint = $txtNetAppEndpoint.Text
Port = 443
DefaultSvm = $txtNetAppDefaultSvm.Text
Username = $txtNetAppUser.Text
Password = $txtNetAppPassword.Password
IgnoreSslErrors = [bool]$chkNetAppIgnoreSsl.IsChecked
}
MsSql = @{
UseLive = [bool]($chkGlobalLive.IsChecked -or $chkSqlLive.IsChecked)
Server = $txtSqlServer.Text
AuthType = $authType
Username = $txtSqlUser.Text
Password = $txtSqlPassword.Password
DefaultDatabase = "master"
ConnectionTimeout = 10
BackupDirectory = $txtSqlBackupDir.Text
}
}
Save-AppConfig -Config $newConfig | Out-Null
$global:AppConfig = Get-AppConfig
Update-ModeBadgeUI
# Invalidate view caches so next click loads fresh live data
$ViewCache.Remove("AD")
$ViewCache.Remove("NetApp")
$ViewCache.Remove("Sql")
[System.Windows.MessageBox]::Show("환경설정이 성공적으로 저장되었습니다.`n각 관리 화면에서 변경된 접속 정보가 즉시 적용됩니다.", "저장 완료", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
})
}
if ($btnReload) {
$btnReload.add_Click({
$global:AppConfig = Get-AppConfig
&$loadSettingsToUI
Update-ModeBadgeUI
[System.Windows.MessageBox]::Show("config.json 설정을 다시 불러왔습니다.", "알림", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
})
}
$ViewCache["Settings"] = $view
}
$mainContentHost.Content = $ViewCache["Settings"]
}
}
}
# Bind Navigation Radio Buttons
$navDashboard.add_Checked({ Navigate-ToPage -PageName "Dashboard" })
$navAD.add_Checked({ Navigate-ToPage -PageName "AD" })
$navNetApp.add_Checked({ Navigate-ToPage -PageName "NetApp" })
$navSql.add_Checked({ Navigate-ToPage -PageName "Sql" })
$navSettings.add_Checked({ Navigate-ToPage -PageName "Settings" })
# Global Refresh Button
if ($btnGlobalRefresh) {
$btnGlobalRefresh.add_Click({
$currentPage = if ($navDashboard.IsChecked) { "Dashboard" }
elseif ($navAD.IsChecked) { "AD" }
elseif ($navNetApp.IsChecked) { "NetApp" }
elseif ($navSql.IsChecked) { "Sql" }
elseif ($navSettings.IsChecked) { "Settings" }
else { "Dashboard" }
$ViewCache.Remove($currentPage)
Navigate-ToPage -PageName $currentPage
})
}
# Initial Page Load (Dashboard)
Navigate-ToPage -PageName "Dashboard"
# 5. Launch Window
if (-not $NoShow) {
Write-Host "Enterprise Admin Studio UI starting..." -ForegroundColor Cyan
$mainWindow.ShowDialog() | Out-Null
} else {
Write-Host "Initialized successfully in headless mode." -ForegroundColor Green
}
+217
View File
@@ -0,0 +1,217 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Color Palette -->
<Color x:Key="BgDarkColor">#0F172A</Color>
<Color x:Key="SidebarBgColor">#090D16</Color>
<Color x:Key="CardBgColor">#1E293B</Color>
<Color x:Key="CardBorderColor">#334155</Color>
<Color x:Key="PrimaryAccentColor">#3B82F6</Color>
<Color x:Key="PrimaryAccentHoverColor">#60A5FA</Color>
<Color x:Key="TextPrimaryColor">#F8FAFC</Color>
<Color x:Key="TextSecondaryColor">#94A3B8</Color>
<Color x:Key="SuccessColor">#10B981</Color>
<Color x:Key="WarningColor">#F59E0B</Color>
<Color x:Key="DangerColor">#EF4444</Color>
<!-- SolidColorBrushes -->
<SolidColorBrush x:Key="BgDarkBrush" Color="{StaticResource BgDarkColor}" />
<SolidColorBrush x:Key="SidebarBgBrush" Color="{StaticResource SidebarBgColor}" />
<SolidColorBrush x:Key="CardBgBrush" Color="{StaticResource CardBgColor}" />
<SolidColorBrush x:Key="CardBorderBrush" Color="{StaticResource CardBorderColor}" />
<SolidColorBrush x:Key="PrimaryAccentBrush" Color="{StaticResource PrimaryAccentColor}" />
<SolidColorBrush x:Key="PrimaryAccentHoverBrush" Color="{StaticResource PrimaryAccentHoverColor}" />
<SolidColorBrush x:Key="TextPrimaryBrush" Color="{StaticResource TextPrimaryColor}" />
<SolidColorBrush x:Key="TextSecondaryBrush" Color="{StaticResource TextSecondaryColor}" />
<SolidColorBrush x:Key="SuccessBrush" Color="{StaticResource SuccessColor}" />
<SolidColorBrush x:Key="WarningBrush" Color="{StaticResource WarningColor}" />
<SolidColorBrush x:Key="DangerBrush" Color="{StaticResource DangerColor}" />
<!-- Default Font -->
<FontFamily x:Key="ModernFont">Segoe UI, Apple SD Gothic Neo, Malgun Gothic</FontFamily>
<FontFamily x:Key="IconFont">Segoe MDL2 Assets, Segoe Fluent Icons</FontFamily>
<!-- Nav Button Style -->
<Style x:Key="NavButtonStyle" TargetType="RadioButton">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}" />
<Setter Property="Height" Value="46" />
<Setter Property="Margin" Value="0,2,0,2" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="FontFamily" Value="{StaticResource ModernFont}" />
<Setter Property="FontSize" Value="13.5" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="RadioButton">
<Border x:Name="border"
Background="{TemplateBinding Background}"
CornerRadius="8"
Padding="12,0"
Margin="8,2">
<Grid>
<!-- Active Accent Indicator -->
<Rectangle x:Name="indicator"
Width="3.5"
Height="18"
HorizontalAlignment="Left"
Fill="{StaticResource PrimaryAccentBrush}"
RadiusX="2" RadiusY="2"
Visibility="Collapsed" />
<ContentPresenter Margin="8,0,0,0"
VerticalAlignment="Center"
RecognizesAccessKey="False" />
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="border" Property="Background" Value="#1A2B4C" />
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}" />
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="border" Property="Background" Value="#1E3A8A" />
<Setter Property="Foreground" Value="#FFFFFF" />
<Setter TargetName="indicator" Property="Visibility" Value="Visible" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Modern Action Button -->
<Style x:Key="PrimaryButtonStyle" TargetType="Button">
<Setter Property="Background" Value="{StaticResource PrimaryAccentBrush}" />
<Setter Property="Foreground" Value="#FFFFFF" />
<Setter Property="FontFamily" Value="{StaticResource ModernFont}" />
<Setter Property="FontSize" Value="13" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Padding" Value="16,8" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="btnBorder"
Background="{TemplateBinding Background}"
CornerRadius="6"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="btnBorder" Property="Background" Value="{StaticResource PrimaryAccentHoverBrush}" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="btnBorder" Property="Background" Value="#2563EB" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="btnBorder" Property="Background" Value="#334155" />
<Setter Property="Foreground" Value="#64748B" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="SecondaryButtonStyle" TargetType="Button">
<Setter Property="Background" Value="#334155" />
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}" />
<Setter Property="FontFamily" Value="{StaticResource ModernFont}" />
<Setter Property="FontSize" Value="13" />
<Setter Property="FontWeight" Value="Medium" />
<Setter Property="Padding" Value="14,7" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="BorderBrush" Value="#475569" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="btnBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="6"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="btnBorder" Property="Background" Value="#475569" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="btnBorder" Property="Background" Value="#1E293B" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Modern TextBox Style -->
<Style x:Key="ModernTextBoxStyle" TargetType="TextBox">
<Setter Property="Background" Value="#0F172A" />
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Padding" Value="10,6" />
<Setter Property="FontFamily" Value="{StaticResource ModernFont}" />
<Setter Property="FontSize" Value="13" />
<Setter Property="CaretBrush" Value="{StaticResource PrimaryAccentBrush}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<Border x:Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="6">
<ScrollViewer x:Name="PART_ContentHost"
Margin="2"
VerticalAlignment="Center" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsFocused" Value="True">
<Setter TargetName="border" Property="BorderBrush" Value="{StaticResource PrimaryAccentBrush}" />
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="border" Property="BorderBrush" Value="{StaticResource PrimaryAccentHoverBrush}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Modern Card Container Style -->
<Style x:Key="ModernCardStyle" TargetType="Border">
<Setter Property="Background" Value="{StaticResource CardBgBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Padding" Value="18" />
<Setter Property="Margin" Value="6" />
</Style>
<!-- Modern DataGrid Style -->
<Style x:Key="ModernDataGridStyle" TargetType="DataGrid">
<Setter Property="Background" Value="{StaticResource CardBgBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}" />
<Setter Property="RowBackground" Value="Transparent" />
<Setter Property="AlternatingRowBackground" Value="#162032" />
<Setter Property="GridLinesVisibility" Value="Horizontal" />
<Setter Property="HorizontalGridLinesBrush" Value="#2D3748" />
<Setter Property="VerticalGridLinesBrush" Value="Transparent" />
<Setter Property="AutoGenerateColumns" Value="False" />
<Setter Property="CanUserAddRows" Value="False" />
<Setter Property="CanUserResizeRows" Value="False" />
<Setter Property="HeadersVisibility" Value="Column" />
<Setter Property="SelectionMode" Value="Single" />
<Setter Property="FontFamily" Value="{StaticResource ModernFont}" />
<Setter Property="FontSize" Value="12.5" />
</Style>
</ResourceDictionary>
+103
View File
@@ -0,0 +1,103 @@
# Enterprise Admin Studio - 사용자 가이드 및 운영 매뉴얼 (USAGE.md)
## 1. 사전 요구사항 (Prerequisites)
- **운영체제**: Windows 10, Windows 11, Windows Server 2016 / 2019 / 2022 / 2025
- **PowerShell 환경**:
- 권장: **PowerShell 7.x** (`pwsh.exe`)
- 지원: **Windows PowerShell 5.1** (`powershell.exe`)
- **.NET 환경**: .NET Framework 4.7.2+ 또는 .NET 8 Desktop Runtime (WPF 지원)
- **네트워크 방화벽 오픈 (실제 Live 모드 사용 시)**:
- Active Directory: TCP 389 (LDAP) 또는 TCP 636 (LDAPS)
- NetApp ONTAP: TCP 443 (HTTPS REST API)
- MS SQL Server: TCP 1433 (Default Instance)
---
## 2. 애플리케이션 시작 (How to Run)
### 방법 A: 탐색기에서 원클릭 실행 (권장)
- `C:\dev\ws\ModernAdminStudio\Run.bat` 파일을 더블 클릭합니다.
- 배치 파일이 시스템에 설치된 `pwsh.exe`(PowerShell 7)를 우선 감지하여 백그라운드 콘솔창 없이 깔끔하게 Modern UI 창만 띄웁니다.
### 방법 B: 터미널에서 직접 실행
```powershell
cd C:\dev\ws\ModernAdminStudio
pwsh -ExecutionPolicy Bypass -File .\Start-App.ps1
```
*(PowerShell 5.1 콘솔에서도 동일하게 동작합니다.)*
---
## 3. 동작 모드 (Operation Modes)
앱 상단 우측에는 현재 동작 모드가 배지로 표시됩니다:
1. **데모 / 모의 모드 (Mock Mode - 보라색 배지)**:
- 실제 장비나 도메인에 연결되지 않아도 모든 UI 인터랙션, 그리드 정렬, 백업 시뮬레이션, 잠금 해제 시뮬레이션을 완벽하게 테스트할 수 있는 개발/시연 모드입니다.
2. **실제 연결 모드 (Live Mode - 초록색 배지)**:
- 실제 Active Directory 도메인 컨트롤러, NetApp ONTAP 스토리지 REST API, MS SQL Server 인스턴스와 직접 통신하여 실제 자원을 조회 및 제어합니다.
> [!TIP]
> **모드 전환 방법**: 좌측 메뉴의 **[환경설정 및 연결]**로 이동하여 상단의 `실제 연결 모드 활성화 (Live Production Mode)` 또는 각 컴포넌트별 `실제 연결 사용` 체크박스를 체크한 후 **[설정 저장 및 적용]**을 누르면 즉시 전환됩니다.
---
## 4. 메뉴별 상세 사용법
### 4.1 통합 대시보드 (Dashboard)
- **KPI 통계 카드**: AD 총 사용자 및 잠김 계정 수, NetApp Qtree/볼륨 수 및 사용률, 활성 공유 수, 온라인 SQL DB 수를 실시간 요약 표시합니다.
- **최근 시스템 감사 로그**: 인프라 전반의 최신 감사 이벤트 내역을 타임라인 그리드로 제공합니다.
- **빠른 작업 바로가기**: 자주 사용하는 주요 작업(AD 계정 생성/해제, Quota 증설, 공유 생성, SQL 백업)으로 1클릭 이동합니다.
### 4.2 Active Directory 관리
- **계정 검색**: 검색창에 사용자 ID(SamAccountName), 이름(DisplayName), 부서명을 입력하고 `검색`을 클릭합니다.
- **상세 정보 인스펙터**: 그리드에서 사용자를 클릭하면 우측 카드에 실시간으로 UPN, DN(OU 경로), 계정 상태, 암호 변경일, 소속 보안 그룹이 표시됩니다.
- **계정 잠금 해제 (Unlock)**:
- 잠긴 계정을 선택하고 상단 툴바의 `잠금 해제` 버튼을 클릭합니다.
- Live 모드에서는 실제 LDAP 디렉터리의 `lockoutTime = 0`이 커밋되어 즉시 잠금이 해제됩니다.
- **암호 초기화 (Reset Password)**:
- 계정을 선택하고 `암호 초기화` 버튼을 클릭하면 임시 보안 암호가 생성되어 부여되며, 다음 로그온 시 암호 변경 플래그가 자동 지정됩니다.
- **활성화 / 비활성화**:
- `활성화/비활성화` 토글 버튼을 통해 계정 사용 여부를 원클릭으로 변경할 수 있습니다.
### 4.3 NetApp 스토리지 관리
- **상단 클러스터 / SVM 선택기**: 관리할 NetApp 스토리지 클러스터 및 SVM(Vserver)을 드롭다운으로 선택합니다.
- **서브 탭 1: Qtree 관리**:
- 볼륨별 Qtree 목록, 보안 스타일(NTFS / UNIX / Mixed), Oplocks 활성화 상태를 확인합니다.
- `+ 새 Qtree 생성` 버튼을 클릭하여 볼륨 내 신규 Qtree를 즉시 프로비저닝할 수 있습니다.
- **서브 탭 2: Quota (용량 한도)**:
- 트리(Tree), 사용자(User), 그룹(Group) 단위로 설정된 Quota 정책을 조회합니다.
- 현재 사용량, Soft Limit, Hard Limit 및 임계치 초과 여부(주의/경고 배지)를 한눈에 파악합니다.
- **서브 탭 3: 파일 공유 (CIFS / NFS)**:
- 활성화된 Windows SMB (CIFS) 및 Linux NFS 공유 목록, 마운트 경로, 권한 요약을 확인합니다.
- `+ 신규 공유 생성` 버튼을 통해 공유 이름과 경로를 지정하여 신규 파일 공유를 생성합니다.
### 4.4 MS SQL 데이터베이스 관리
- **인스턴스 연결 바**: 상단에서 대상 SQL 인스턴스와 인증 방식(Windows 통합 인증 / SQL 계정)을 선택하고 `연결 테스트`를 수행할 수 있습니다.
- **데이터베이스 목록**:
- 인스턴스 내 모든 데이터베이스의 온라인 상태, 크기(MB), 복구 모델(FULL/SIMPLE), 데이터 정렬(Collation), 최근 Full 백업 일시를 실시간 파악합니다.
- **원클릭 Full 백업**:
- 목록에서 DB를 선택하고 `즉시 백업 (Full)` 버튼을 누르면 실제 백업 T-SQL 명령어가 실행되어 지정된 백업 디렉터리에 `.bak` 파일이 생성됩니다.
- **T-SQL 간이 쿼리 콘솔**:
- 우측 하단 에디터에 쿼리를 입력하고 `▶ 쿼리 실행`을 누르면 결과가 하단 콘솔 창에 즉시 출력됩니다.
### 4.5 환경설정 및 연결 구성 (Settings)
- **접속 파라미터 구성**:
- AD 도메인 컨트롤러 주소, 기본 검색 루트 OU, LDAPS 사용 여부
- NetApp 클러스터 REST API 엔드포인트 URL, 대상 SVM, 관리자 계정/비밀번호
- SQL Server 인스턴스 주소, 인증 모드, 백업 디렉터리 경로
- **개별 연결 테스트**: 각 섹션의 `연결 테스트` 버튼을 통해 개별 시스템의 연결 상태를 사전 검증합니다.
- **전체 인프라 일괄 진단**: 하단의 `전체 인프라 연결 상태 일괄 진단` 버튼을 누르면 AD, NetApp, SQL Server를 순차 점검하여 종합 리포트를 팝업으로 안내합니다.
- **설정 저장**: `설정 저장 및 적용`을 누르면 모든 내용이 `config.json`에 안전하게 암호화/저장되고 앱 전반에 즉시 반영됩니다.
---
## 5. 트러블슈팅 (Troubleshooting)
| 현상 | 원인 및 해결 방법 |
| :--- | :--- |
| **AD 연결 실패 (Server is not operational)** | 1. 도메인 컨트롤러 FQDN/IP가 올바른지 확인하십시오.<br>2. TCP 389/636 포트가 방화벽에서 허용되었는지 확인하십시오.<br>3. 도메인 가입 PC가 아닌 경우 설정 화면에서 관리자 자격증명을 입력하십시오. |
| **NetApp REST API 401 Unauthorized** | 1. ONTAP 클러스터 관리자 계정(`admin` 또는 `vsadmin`)의 비밀번호를 확인하십시오.<br>2. ONTAP 9.6 이상 버전인지 확인하십시오. |
| **NetApp SSL 인증서 에러** | NetApp 기본 사설 인증서를 사용하는 경우 설정 화면에서 `자체 서명(Self-signed) SSL 인증서 신뢰` 옵션을 체크하십시오. |
| **SQL Server 접속 실패 (Server not found)** | 1. SQL Server 인스턴스 이름 및 TCP/IP 프로토콜 활성화 여부를 확인하십시오.<br>2. SQL Server Browser 서비스 실행 여부 및 포트 1433 방화벽을 확인하십시오. |
+125
View File
@@ -0,0 +1,125 @@
<UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid Margin="24">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Header -->
<StackPanel Grid.Row="0" Margin="0,0,0,20">
<TextBlock Text="Active Directory 관리" FontSize="24" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}" />
<TextBlock Text="도메인 사용자 계정, 보안 그룹, OU 및 계정 상태를 조회하고 제어합니다."
FontSize="13" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,4,0,0" />
</StackPanel>
<!-- Control / Filter Bar -->
<Border Grid.Row="1" Style="{StaticResource ModernCardStyle}" Margin="0,0,0,16" Padding="14">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="260" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Search Input -->
<TextBlock Grid.Column="0" Text="사용자 검색:" Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center" Margin="0,0,10,0" />
<TextBox Grid.Column="1" x:Name="TxtAdSearch" Style="{StaticResource ModernTextBoxStyle}" Margin="0,0,16,0" />
<!-- Filter Combo / Radio -->
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
<Button x:Name="BtnAdSearch" Content="&#xE721; 검색" Style="{StaticResource PrimaryButtonStyle}" Margin="0,0,8,0" />
<Button x:Name="BtnAdRefresh" Content="새로고침" Style="{StaticResource SecondaryButtonStyle}" Margin="0,0,16,0" />
</StackPanel>
<!-- Action Toolbar -->
<StackPanel Grid.Column="3" Orientation="Horizontal" HorizontalAlignment="Right">
<Button x:Name="BtnAdNewUser" Content="+ 계정 추가" Style="{StaticResource PrimaryButtonStyle}" Margin="0,0,8,0" />
<Button x:Name="BtnAdResetPwd" Content="암호 초기화" Style="{StaticResource SecondaryButtonStyle}" Margin="0,0,8,0" />
<Button x:Name="BtnAdUnlock" Content="잠금 해제" Style="{StaticResource SecondaryButtonStyle}" Margin="0,0,8,0" />
<Button x:Name="BtnAdToggleEnable" Content="활성화/비활성화" Style="{StaticResource SecondaryButtonStyle}" />
</StackPanel>
</Grid>
</Border>
<!-- Main Content Grid -->
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="7*" />
<ColumnDefinition Width="3*" />
</Grid.ColumnDefinitions>
<!-- AD Users DataGrid -->
<Border Grid.Column="0" Style="{StaticResource ModernCardStyle}" Margin="0,0,12,0" Padding="0">
<DataGrid x:Name="AdUsersGrid" Style="{StaticResource ModernDataGridStyle}">
<DataGrid.Columns>
<DataGridTextColumn Header="아이디 (SamAccount)" Binding="{Binding SamAccountName}" Width="130" />
<DataGridTextColumn Header="이름 (Display Name)" Binding="{Binding DisplayName}" Width="120" />
<DataGridTextColumn Header="부서" Binding="{Binding Department}" Width="100" />
<DataGridTextColumn Header="직급 / 직책" Binding="{Binding Title}" Width="90" />
<DataGridTextColumn Header="이메일" Binding="{Binding UserPrincipalName}" Width="180" />
<DataGridTextColumn Header="상태" Binding="{Binding StatusText}" Width="75" />
<DataGridTextColumn Header="잠김 여부" Binding="{Binding LockedText}" Width="80" />
<DataGridTextColumn Header="최종 로그인" Binding="{Binding LastLogonDate}" Width="*" />
</DataGrid.Columns>
</DataGrid>
</Border>
<!-- Selected User Properties Inspector Card -->
<Border Grid.Column="1" Style="{StaticResource ModernCardStyle}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel>
<TextBlock Text="선택된 계정 상세 정보" FontSize="15" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}" Margin="0,0,0,16" />
<!-- User Profile Header -->
<Border Background="#121A27" CornerRadius="8" Padding="14" Margin="0,0,0,16">
<StackPanel>
<TextBlock x:Name="TxtDetailName" Text="홍길동 (gdhong)" FontSize="16" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}" />
<TextBlock x:Name="TxtDetailEmail" Text="gdhong@corp.local" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,2,0,0" />
<TextBlock x:Name="TxtDetailOu" Text="OU=Users,OU=Seoul,DC=corp,DC=local" FontSize="11" Foreground="#60A5FA" Margin="0,6,0,0" TextWrapping="Wrap" />
</StackPanel>
</Border>
<TextBlock Text="계정 속성" FontSize="13" FontWeight="SemiBold" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,8" />
<Grid Margin="0,4">
<TextBlock Text="계정 상태:" Foreground="{StaticResource TextSecondaryBrush}" FontSize="12" />
<TextBlock x:Name="TxtPropStatus" Text="활성 (Active)" Foreground="#10B981" FontWeight="SemiBold" FontSize="12" HorizontalAlignment="Right" />
</Grid>
<Grid Margin="0,4">
<TextBlock Text="계정 만료일:" Foreground="{StaticResource TextSecondaryBrush}" FontSize="12" />
<TextBlock x:Name="TxtPropExpires" Text="제한 없음" Foreground="{StaticResource TextPrimaryBrush}" FontSize="12" HorizontalAlignment="Right" />
</Grid>
<Grid Margin="0,4">
<TextBlock Text="암호 최종 변경:" Foreground="{StaticResource TextSecondaryBrush}" FontSize="12" />
<TextBlock x:Name="TxtPropPwdChanged" Text="2026-09-15" Foreground="{StaticResource TextPrimaryBrush}" FontSize="12" HorizontalAlignment="Right" />
</Grid>
<Grid Margin="0,4">
<TextBlock Text="암호 만료 필요 여부:" Foreground="{StaticResource TextSecondaryBrush}" FontSize="12" />
<TextBlock x:Name="TxtPropMustChange" Text="아니오" Foreground="{StaticResource TextPrimaryBrush}" FontSize="12" HorizontalAlignment="Right" />
</Grid>
<Separator Background="{StaticResource CardBorderBrush}" Margin="0,12" />
<TextBlock Text="소속 그룹 (Member Of)" FontSize="13" FontWeight="SemiBold" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,8" />
<ListBox x:Name="ListMemberGroups" Height="120" Background="#0F172A" BorderBrush="{StaticResource CardBorderBrush}"
Foreground="{StaticResource TextPrimaryBrush}" FontSize="12" />
<Button x:Name="BtnManageGroups" Content="그룹 멤버십 편집..." Style="{StaticResource SecondaryButtonStyle}" Margin="0,8,0,0" HorizontalAlignment="Right" />
</StackPanel>
</ScrollViewer>
</Border>
</Grid>
<!-- Status Footer -->
<Border Grid.Row="3" Margin="0,12,0,0">
<DockPanel>
<TextBlock x:Name="TxtAdStatusMsg" Text="준비 완료 - 도메인 컨트롤러: dc01.corp.local (연결됨)" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" />
<TextBlock x:Name="TxtAdTotalCount" Text="총 1,420 명의 사용자" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" HorizontalAlignment="Right" />
</DockPanel>
</Border>
</Grid>
</UserControl>
+166
View File
@@ -0,0 +1,166 @@
<UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid Margin="24">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- View Header -->
<StackPanel Grid.Row="0" Margin="0,0,0,24">
<TextBlock Text="인프라 종합 대시보드" FontSize="24" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}" />
<TextBlock Text="Active Directory, NetApp 스토리지, MS SQL 데이터베이스 전반의 실시간 상태를 모니터링합니다."
FontSize="13" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,4,0,0" />
</StackPanel>
<!-- KPI Summary Cards Grid -->
<Grid Grid.Row="1" Margin="0,0,0,20">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Card 1: Active Directory -->
<Border Grid.Column="0" Style="{StaticResource ModernCardStyle}">
<StackPanel>
<Grid>
<TextBlock Text="Active Directory" FontSize="13" FontWeight="SemiBold" Foreground="{StaticResource TextSecondaryBrush}" />
<TextBlock Text="&#xE77B;" FontFamily="{StaticResource IconFont}" FontSize="18" HorizontalAlignment="Right" Foreground="#60A5FA" />
</Grid>
<TextBlock Text="1,420" FontSize="28" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}" Margin="0,8,0,2" />
<StackPanel Orientation="Horizontal">
<Border Background="#10B981" CornerRadius="4" Padding="6,2" Margin="0,0,6,0">
<TextBlock Text="정상" FontSize="11" FontWeight="Bold" Foreground="White" />
</Border>
<TextBlock Text="잠김 2건 / 비활성 18건" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center" />
</StackPanel>
</StackPanel>
</Border>
<!-- Card 2: NetApp Qtree & Volume -->
<Border Grid.Column="1" Style="{StaticResource ModernCardStyle}">
<StackPanel>
<Grid>
<TextBlock Text="NetApp Qtree / 볼륨" FontSize="13" FontWeight="SemiBold" Foreground="{StaticResource TextSecondaryBrush}" />
<TextBlock Text="&#xE7F3;" FontFamily="{StaticResource IconFont}" FontSize="18" HorizontalAlignment="Right" Foreground="#34D399" />
</Grid>
<TextBlock Text="84 / 12 개" FontSize="28" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}" Margin="0,8,0,2" />
<StackPanel Orientation="Horizontal">
<TextBlock Text="전체 스토리지 사용률: " FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" />
<TextBlock Text="68.4%" FontSize="12" FontWeight="SemiBold" Foreground="#34D399" />
</StackPanel>
</StackPanel>
</Border>
<!-- Card 3: Quota & Shares -->
<Border Grid.Column="2" Style="{StaticResource ModernCardStyle}">
<StackPanel>
<Grid>
<TextBlock Text="Quota 및 공유 (CIFS/NFS)" FontSize="13" FontWeight="SemiBold" Foreground="{StaticResource TextSecondaryBrush}" />
<TextBlock Text="&#xE8B7;" FontFamily="{StaticResource IconFont}" FontSize="18" HorizontalAlignment="Right" Foreground="#FBBF24" />
</Grid>
<TextBlock Text="48 개 Share" FontSize="28" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}" Margin="0,8,0,2" />
<StackPanel Orientation="Horizontal">
<Border Background="#F59E0B" CornerRadius="4" Padding="6,2" Margin="0,0,6,0">
<TextBlock Text="경고" FontSize="11" FontWeight="Bold" Foreground="Black" />
</Border>
<TextBlock Text="Quota 90% 초과 3건" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center" />
</StackPanel>
</StackPanel>
</Border>
<!-- Card 4: MS SQL DB -->
<Border Grid.Column="3" Style="{StaticResource ModernCardStyle}">
<StackPanel>
<Grid>
<TextBlock Text="MS SQL 데이터베이스" FontSize="13" FontWeight="SemiBold" Foreground="{StaticResource TextSecondaryBrush}" />
<TextBlock Text="&#xEC92;" FontFamily="{StaticResource IconFont}" FontSize="18" HorizontalAlignment="Right" Foreground="#A78BFA" />
</Grid>
<TextBlock Text="16 DBs" FontSize="28" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}" Margin="0,8,0,2" />
<StackPanel Orientation="Horizontal">
<Border Background="#10B981" CornerRadius="4" Padding="6,2" Margin="0,0,6,0">
<TextBlock Text="온라인" FontSize="11" FontWeight="Bold" Foreground="White" />
</Border>
<TextBlock Text="최근 백업 정상 완료" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center" />
</StackPanel>
</StackPanel>
</Border>
</Grid>
<!-- Bottom Layout: Recent Activities & Quick Launcher -->
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<!-- Activity Log Card -->
<Border Grid.Column="0" Style="{StaticResource ModernCardStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<DockPanel Grid.Row="0" Margin="0,0,0,14">
<TextBlock Text="최근 시스템 감사 및 관리 이력" FontSize="15" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}" VerticalAlignment="Center" />
<Button Content="새로고침" Style="{StaticResource SecondaryButtonStyle}" HorizontalAlignment="Right" Padding="10,4" />
</DockPanel>
<DataGrid x:Name="ActivityGrid" Grid.Row="1" Style="{StaticResource ModernDataGridStyle}">
<DataGrid.Columns>
<DataGridTextColumn Header="발생 시각" Binding="{Binding Timestamp}" Width="140" />
<DataGridTextColumn Header="대상 시스템" Binding="{Binding TargetSystem}" Width="120" />
<DataGridTextColumn Header="작업 유형" Binding="{Binding ActionType}" Width="140" />
<DataGridTextColumn Header="세부 내용" Binding="{Binding Description}" Width="*" />
<DataGridTextColumn Header="결과" Binding="{Binding Status}" Width="80" />
</DataGrid.Columns>
</DataGrid>
</Grid>
</Border>
<!-- Quick Actions Card -->
<Border Grid.Column="1" Style="{StaticResource ModernCardStyle}">
<StackPanel>
<TextBlock Text="빠른 작업 바로가기" FontSize="15" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}" Margin="0,0,0,16" />
<Button Content="&#xE77B; AD 계정 신규 생성 / 잠금 해제" Style="{StaticResource SecondaryButtonStyle}"
HorizontalContentAlignment="Left" Margin="0,0,0,10" Padding="12,10" x:Name="QuickBtnAD" />
<Button Content="&#xE7F3; NetApp Quota 용량 증설 요청" Style="{StaticResource SecondaryButtonStyle}"
HorizontalContentAlignment="Left" Margin="0,0,0,10" Padding="12,10" x:Name="QuickBtnQuota" />
<Button Content="&#xE8B7; 신규 CIFS/SMB 공유 생성" Style="{StaticResource SecondaryButtonStyle}"
HorizontalContentAlignment="Left" Margin="0,0,0,10" Padding="12,10" x:Name="QuickBtnShare" />
<Button Content="&#xEC92; SQL DB 즉시 백업 수행" Style="{StaticResource SecondaryButtonStyle}"
HorizontalContentAlignment="Left" Margin="0,0,0,10" Padding="12,10" x:Name="QuickBtnSql" />
<Separator Background="{StaticResource CardBorderBrush}" Margin="0,10,0,16" />
<!-- Connection Quick Status Box -->
<Border Background="#121A27" CornerRadius="6" Padding="12">
<StackPanel>
<TextBlock Text="시스템 연결 상태 요약" FontSize="12" FontWeight="SemiBold" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,8" />
<Grid Margin="0,2">
<TextBlock Text="Active Directory DC:" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" />
<TextBlock Text="정상 연결" FontSize="12" Foreground="#10B981" HorizontalAlignment="Right" />
</Grid>
<Grid Margin="0,2">
<TextBlock Text="NetApp ONTAP API:" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" />
<TextBlock Text="정상 연결" FontSize="12" Foreground="#10B981" HorizontalAlignment="Right" />
</Grid>
<Grid Margin="0,2">
<TextBlock Text="MS SQL Server:" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" />
<TextBlock Text="정상 연결" FontSize="12" Foreground="#10B981" HorizontalAlignment="Right" />
</Grid>
</StackPanel>
</Border>
</StackPanel>
</Border>
</Grid>
</Grid>
</UserControl>
+197
View File
@@ -0,0 +1,197 @@
<UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid Margin="24">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Header -->
<StackPanel Grid.Row="0" Margin="0,0,0,20">
<TextBlock Text="NetApp 스토리지 관리" FontSize="24" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}" />
<TextBlock Text="NetApp ONTAP 스토리지의 Qtree, Quota 한도, CIFS/NFS 파일 공유를 통합 제어합니다."
FontSize="13" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,4,0,0" />
</StackPanel>
<!-- Top Cluster / SVM Selector Bar -->
<Border Grid.Row="1" Style="{StaticResource ModernCardStyle}" Margin="0,0,0,16" Padding="14">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="200" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="200" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="스토리지 클러스터:" Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center" Margin="0,0,10,0" />
<ComboBox Grid.Column="1" x:Name="CmbCluster" SelectedIndex="0" Height="32" Background="#0F172A" Foreground="{StaticResource TextPrimaryBrush}" Margin="0,0,20,0">
<ComboBoxItem Content="cluster01-ontap.corp.local" />
<ComboBoxItem Content="cluster02-backup.corp.local" />
</ComboBox>
<TextBlock Grid.Column="2" Text="SVM (Vserver):" Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center" Margin="0,0,10,0" />
<ComboBox Grid.Column="3" x:Name="CmbSvm" SelectedIndex="0" Height="32" Background="#0F172A" Foreground="{StaticResource TextPrimaryBrush}" Margin="0,0,20,0">
<ComboBoxItem Content="svm_cifs_prod01" />
<ComboBoxItem Content="svm_nfs_app01" />
</ComboBox>
<Button Grid.Column="4" x:Name="BtnRefreshStorage" Content="새로고침" Style="{StaticResource SecondaryButtonStyle}" />
<StackPanel Grid.Column="5" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
<Border Background="#10B981" Width="10" Height="10" CornerRadius="5" Margin="0,0,6,0" />
<TextBlock Text="ONTAP 9.14 API 정상" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" />
</StackPanel>
</Grid>
</Border>
<!-- Tab Content Area: Qtree, Quota, Shares -->
<TabControl Grid.Row="2" Background="Transparent" BorderThickness="0">
<TabControl.Resources>
<Style TargetType="TabItem">
<Setter Property="FontSize" Value="14" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}" />
<Setter Property="Padding" Value="18,10" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TabItem">
<Border x:Name="tabBorder" Background="Transparent" BorderThickness="0,0,0,2" BorderBrush="Transparent" Margin="0,0,10,6">
<ContentPresenter ContentSource="Header" Margin="6,4" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="tabBorder" Property="BorderBrush" Value="{StaticResource PrimaryAccentBrush}" />
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}" />
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Foreground" Value="{StaticResource PrimaryAccentHoverBrush}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</TabControl.Resources>
<!-- TAB 1: QTREE MANAGEMENT -->
<TabItem Header="&#xE7F3; Qtree 관리">
<Grid Margin="0,12,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<DockPanel Grid.Row="0" Margin="0,0,0,12">
<StackPanel Orientation="Horizontal">
<TextBox x:Name="TxtQtreeSearch" Width="220" Style="{StaticResource ModernTextBoxStyle}" Margin="0,0,8,0" />
<Button x:Name="BtnQtreeFilter" Content="검색" Style="{StaticResource SecondaryButtonStyle}" />
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button x:Name="BtnNewQtree" Content="+ 새 Qtree 생성" Style="{StaticResource PrimaryButtonStyle}" Margin="0,0,8,0" />
<Button x:Name="BtnEditQtree" Content="보안 스타일 변경" Style="{StaticResource SecondaryButtonStyle}" Margin="0,0,8,0" />
<Button x:Name="BtnDeleteQtree" Content="삭제" Style="{StaticResource SecondaryButtonStyle}" />
</StackPanel>
</DockPanel>
<Border Grid.Row="1" Style="{StaticResource ModernCardStyle}" Margin="0" Padding="0">
<DataGrid x:Name="QtreeGrid" Style="{StaticResource ModernDataGridStyle}">
<DataGrid.Columns>
<DataGridTextColumn Header="볼륨 (Volume)" Binding="{Binding Volume}" Width="160" />
<DataGridTextColumn Header="Qtree 이름" Binding="{Binding QtreeName}" Width="160" />
<DataGridTextColumn Header="보안 스타일 (Security)" Binding="{Binding SecurityStyle}" Width="130" />
<DataGridTextColumn Header="Oplocks" Binding="{Binding Oplocks}" Width="100" />
<DataGridTextColumn Header="상태" Binding="{Binding Status}" Width="100" />
<DataGridTextColumn Header="전체 경로" Binding="{Binding FullPath}" Width="*" />
</DataGrid.Columns>
</DataGrid>
</Border>
</Grid>
</TabItem>
<!-- TAB 2: QUOTA MANAGEMENT -->
<TabItem Header="&#xE8B7; Quota (용량 한도)">
<Grid Margin="0,12,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<DockPanel Grid.Row="0" Margin="0,0,0,12">
<StackPanel Orientation="Horizontal">
<TextBox x:Name="TxtQuotaSearch" Width="220" Style="{StaticResource ModernTextBoxStyle}" Margin="0,0,8,0" />
<Button x:Name="BtnQuotaFilter" Content="검색" Style="{StaticResource SecondaryButtonStyle}" />
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button x:Name="BtnAddQuota" Content="+ Quota 규칙 추가" Style="{StaticResource PrimaryButtonStyle}" Margin="0,0,8,0" />
<Button x:Name="BtnModifyQuota" Content="한도 조정 (Resize)" Style="{StaticResource SecondaryButtonStyle}" Margin="0,0,8,0" />
<Button x:Name="BtnRecomputeQuota" Content="Quota 재계산" Style="{StaticResource SecondaryButtonStyle}" />
</StackPanel>
</DockPanel>
<Border Grid.Row="1" Style="{StaticResource ModernCardStyle}" Margin="0" Padding="0">
<DataGrid x:Name="QuotaGrid" Style="{StaticResource ModernDataGridStyle}">
<DataGrid.Columns>
<DataGridTextColumn Header="대상 유형 (Type)" Binding="{Binding QuotaType}" Width="110" />
<DataGridTextColumn Header="대상 (Target / User / Tree)" Binding="{Binding Target}" Width="180" />
<DataGridTextColumn Header="볼륨 / Qtree" Binding="{Binding VolumeQtree}" Width="180" />
<DataGridTextColumn Header="현재 사용량" Binding="{Binding UsedSpace}" Width="120" />
<DataGridTextColumn Header="Soft Limit" Binding="{Binding SoftLimit}" Width="120" />
<DataGridTextColumn Header="Hard Limit" Binding="{Binding HardLimit}" Width="120" />
<DataGridTextColumn Header="사용률" Binding="{Binding UsagePercent}" Width="90" />
<DataGridTextColumn Header="상태" Binding="{Binding QuotaStatus}" Width="*" />
</DataGrid.Columns>
</DataGrid>
</Border>
</Grid>
</TabItem>
<!-- TAB 3: SHARE MANAGEMENT -->
<TabItem Header="&#xE838; 파일 공유 (CIFS / NFS)">
<Grid Margin="0,12,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<DockPanel Grid.Row="0" Margin="0,0,0,12">
<StackPanel Orientation="Horizontal">
<TextBox x:Name="TxtShareSearch" Width="220" Style="{StaticResource ModernTextBoxStyle}" Margin="0,0,8,0" />
<Button x:Name="BtnShareFilter" Content="검색" Style="{StaticResource SecondaryButtonStyle}" />
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button x:Name="BtnNewShare" Content="+ 신규 공유 생성" Style="{StaticResource PrimaryButtonStyle}" Margin="0,0,8,0" />
<Button x:Name="BtnShareAcl" Content="ACL / 접근 권한 관리" Style="{StaticResource SecondaryButtonStyle}" Margin="0,0,8,0" />
<Button x:Name="BtnDeleteShare" Content="공유 해제" Style="{StaticResource SecondaryButtonStyle}" />
</StackPanel>
</DockPanel>
<Border Grid.Row="1" Style="{StaticResource ModernCardStyle}" Margin="0" Padding="0">
<DataGrid x:Name="ShareGrid" Style="{StaticResource ModernDataGridStyle}">
<DataGrid.Columns>
<DataGridTextColumn Header="프로토콜" Binding="{Binding Protocol}" Width="90" />
<DataGridTextColumn Header="공유 이름 (Share Name)" Binding="{Binding ShareName}" Width="160" />
<DataGridTextColumn Header="마운트 / Qtree 경로" Binding="{Binding Path}" Width="260" />
<DataGridTextColumn Header="설명 (Comment)" Binding="{Binding Comment}" Width="200" />
<DataGridTextColumn Header="권한 요약" Binding="{Binding AclSummary}" Width="160" />
<DataGridTextColumn Header="활성 세션 수" Binding="{Binding ActiveSessions}" Width="*" />
</DataGrid.Columns>
</DataGrid>
</Border>
</Grid>
</TabItem>
</TabControl>
<!-- Status Footer -->
<Border Grid.Row="3" Margin="0,12,0,0">
<DockPanel>
<TextBlock x:Name="TxtNetAppStatusMsg" Text="스토리지 응답 정상 (REST API v2) - Active SVM: svm_cifs_prod01" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" />
<TextBlock x:Name="TxtNetAppSummary" Text="총 84개 Qtree, 48개 공유 활성화됨" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" HorizontalAlignment="Right" />
</DockPanel>
</Border>
</Grid>
</UserControl>
+159
View File
@@ -0,0 +1,159 @@
<UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid Margin="24">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Header -->
<StackPanel Grid.Row="0" Margin="0,0,0,20">
<TextBlock Text="환경설정 및 실제 인프라 연결 구성" FontSize="24" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}" />
<TextBlock Text="Active Directory, NetApp ONTAP 스토리지 및 MS SQL 데이터베이스 접속 엔드포인트를 구성하고 실시간 테스트합니다."
FontSize="13" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,4,0,0" />
</StackPanel>
<!-- Settings Cards Scroll Area -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<StackPanel>
<!-- Group 1: Global Operating Mode -->
<Border Style="{StaticResource ModernCardStyle}" Margin="0,0,0,16">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="전역 동작 모드 (Global Operation Mode)" FontSize="16" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}" />
<TextBlock Text="실제 인프라 장비 및 서버와 통신할지, 또는 데모/시뮬레이션 모드로 작동할지 선택합니다."
FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,4,0,0" />
</StackPanel>
<CheckBox x:Name="ChkGlobalLiveMode" Grid.Column="1" Content="실제 연결 모드 활성화 (Live Production Mode)"
Foreground="{StaticResource PrimaryAccentHoverBrush}" FontWeight="Bold" VerticalAlignment="Center" FontSize="13" />
</Grid>
</Border>
<!-- Group 2: Active Directory Configuration -->
<Border Style="{StaticResource ModernCardStyle}" Margin="0,0,0,16">
<StackPanel>
<DockPanel Margin="0,0,0,12">
<TextBlock Text="Active Directory (AD DS) 연결" FontSize="16" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}" />
<CheckBox x:Name="ChkAdLive" Content="AD 실제 연결 사용" Foreground="{StaticResource TextPrimaryBrush}" HorizontalAlignment="Right" VerticalAlignment="Center" />
</DockPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Margin="0,0,12,0">
<TextBlock Text="도메인 컨트롤러 (DC FQDN / IP):" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,4" FontSize="12" />
<TextBox x:Name="TxtAdDc" Text="dc01.corp.local" Style="{StaticResource ModernTextBoxStyle}" Margin="0,0,0,12" />
<TextBlock Text="검색 기본 루트 OU (DN):" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,4" FontSize="12" />
<TextBox x:Name="TxtAdBaseOu" Text="DC=corp,DC=local" Style="{StaticResource ModernTextBoxStyle}" Margin="0,0,0,12" />
<CheckBox x:Name="ChkAdLdaps" Content="보안 LDAPS 암호화 연결 (Port 636) 사용" Foreground="{StaticResource TextPrimaryBrush}" Margin="0,4,0,0" />
</StackPanel>
<StackPanel Grid.Column="1" Margin="12,0,0,0">
<TextBlock Text="관리자 계정 (비워둘 시 현재 Windows 로그인 토큰 사용):" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,4" FontSize="12" />
<TextBox x:Name="TxtAdUser" Style="{StaticResource ModernTextBoxStyle}" Margin="0,0,0,12" />
<TextBlock Text="관리자 암호:" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,4" FontSize="12" />
<PasswordBox x:Name="TxtAdPassword" Background="#0F172A" Foreground="{StaticResource TextPrimaryBrush}" BorderBrush="{StaticResource CardBorderBrush}" Height="34" Margin="0,0,0,12" />
<Button x:Name="BtnTestAd" Content="AD 연결 테스트" Style="{StaticResource SecondaryButtonStyle}" HorizontalAlignment="Right" Padding="14,6" />
</StackPanel>
</Grid>
</StackPanel>
</Border>
<!-- Group 3: NetApp ONTAP Storage Configuration -->
<Border Style="{StaticResource ModernCardStyle}" Margin="0,0,0,16">
<StackPanel>
<DockPanel Margin="0,0,0,12">
<TextBlock Text="NetApp ONTAP 스토리지 (REST API) 연결" FontSize="16" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}" />
<CheckBox x:Name="ChkNetAppLive" Content="NetApp 실제 연결 사용" Foreground="{StaticResource TextPrimaryBrush}" HorizontalAlignment="Right" VerticalAlignment="Center" />
</DockPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Margin="0,0,12,0">
<TextBlock Text="클러스터 관리 LIF (HTTPS 엔드포인트 URL):" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,4" FontSize="12" />
<TextBox x:Name="TxtNetAppEndpoint" Text="https://10.200.10.50" Style="{StaticResource ModernTextBoxStyle}" Margin="0,0,0,12" />
<TextBlock Text="기본 대상 SVM (Vserver):" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,4" FontSize="12" />
<TextBox x:Name="TxtNetAppDefaultSvm" Text="svm_cifs_prod01" Style="{StaticResource ModernTextBoxStyle}" Margin="0,0,0,12" />
<CheckBox x:Name="ChkNetAppIgnoreSsl" Content="자체 서명(Self-signed) SSL 인증서 신뢰" IsChecked="True" Foreground="{StaticResource TextPrimaryBrush}" Margin="0,4,0,0" />
</StackPanel>
<StackPanel Grid.Column="1" Margin="12,0,0,0">
<TextBlock Text="스토리지 관리자 ID (vsadmin / admin):" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,4" FontSize="12" />
<TextBox x:Name="TxtNetAppUser" Text="admin" Style="{StaticResource ModernTextBoxStyle}" Margin="0,0,0,12" />
<TextBlock Text="스토리지 관리자 암호:" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,4" FontSize="12" />
<PasswordBox x:Name="TxtNetAppPassword" Background="#0F172A" Foreground="{StaticResource TextPrimaryBrush}" BorderBrush="{StaticResource CardBorderBrush}" Height="34" Margin="0,0,0,12" />
<Button x:Name="BtnTestNetApp" Content="NetApp ONTAP 연결 테스트" Style="{StaticResource SecondaryButtonStyle}" HorizontalAlignment="Right" Padding="14,6" />
</StackPanel>
</Grid>
</StackPanel>
</Border>
<!-- Group 4: MS SQL Server Configuration -->
<Border Style="{StaticResource ModernCardStyle}" Margin="0,0,0,16">
<StackPanel>
<DockPanel Margin="0,0,0,12">
<TextBlock Text="MS SQL Server 연결 기본값" FontSize="16" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}" />
<CheckBox x:Name="ChkSqlLive" Content="MS SQL 실제 연결 사용" Foreground="{StaticResource TextPrimaryBrush}" HorizontalAlignment="Right" VerticalAlignment="Center" />
</DockPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Margin="0,0,12,0">
<TextBlock Text="SQL 인스턴스 주소 (예: localhost 또는 SRV\INSTANCE):" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,4" FontSize="12" />
<TextBox x:Name="TxtSqlServer" Text="localhost" Style="{StaticResource ModernTextBoxStyle}" Margin="0,0,0,12" />
<TextBlock Text="인증 방식:" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,4" FontSize="12" />
<ComboBox x:Name="CmbSqlAuthType" SelectedIndex="0" Height="32" Background="#0F172A" Foreground="{StaticResource TextPrimaryBrush}" Margin="0,0,0,12">
<ComboBoxItem Content="Windows 통합 인증 (Integrated Security)" />
<ComboBoxItem Content="SQL Server 인증 (User ID / Password)" />
</ComboBox>
<TextBlock Text="백업 파일 기본 저장 디렉토리:" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,4" FontSize="12" />
<TextBox x:Name="TxtSqlBackupDir" Text="C:\Backups" Style="{StaticResource ModernTextBoxStyle}" />
</StackPanel>
<StackPanel Grid.Column="1" Margin="12,0,0,0">
<TextBlock Text="SQL 계정 (SQL 인증 시):" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,4" FontSize="12" />
<TextBox x:Name="TxtSqlUser" Text="sa" Style="{StaticResource ModernTextBoxStyle}" Margin="0,0,0,12" />
<TextBlock Text="SQL 암호 (SQL 인증 시):" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,4" FontSize="12" />
<PasswordBox x:Name="TxtSqlPassword" Background="#0F172A" Foreground="{StaticResource TextPrimaryBrush}" BorderBrush="{StaticResource CardBorderBrush}" Height="34" Margin="0,0,0,12" />
<Button x:Name="BtnTestSql" Content="SQL Server 연결 테스트" Style="{StaticResource SecondaryButtonStyle}" HorizontalAlignment="Right" Padding="14,6" />
</StackPanel>
</Grid>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
<!-- Bottom Action Bar -->
<Border Grid.Row="2" Margin="0,16,0,0">
<DockPanel>
<Button x:Name="BtnTestAllConnections" Content="&#xE958; 전체 인프라 연결 상태 일괄 진단" Style="{StaticResource SecondaryButtonStyle}" HorizontalAlignment="Left" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button x:Name="BtnReloadConfig" Content="설정 다시 로드" Style="{StaticResource SecondaryButtonStyle}" Margin="0,0,8,0" />
<Button x:Name="BtnSaveSettings" Content="설정 저장 및 적용" Style="{StaticResource PrimaryButtonStyle}" />
</StackPanel>
</DockPanel>
</Border>
</Grid>
</UserControl>
+154
View File
@@ -0,0 +1,154 @@
<UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid Margin="24">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Header -->
<StackPanel Grid.Row="0" Margin="0,0,0,20">
<TextBlock Text="MS SQL 데이터베이스 관리" FontSize="24" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}" />
<TextBlock Text="SQL Server 인스턴스 연결, 데이터베이스 상태 제어, 백업/복원 및 사용자 권한을 관리합니다."
FontSize="13" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,4,0,0" />
</StackPanel>
<!-- Instance Connection Bar -->
<Border Grid.Row="1" Style="{StaticResource ModernCardStyle}" Margin="0,0,0,16" Padding="14">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="220" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="160" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="SQL 인스턴스:" Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center" Margin="0,0,10,0" />
<ComboBox Grid.Column="1" x:Name="CmbSqlInstance" SelectedIndex="0" Height="32" Background="#0F172A" Foreground="{StaticResource TextPrimaryBrush}" Margin="0,0,16,0">
<ComboBoxItem Content="SQL-PROD-01.corp.local\MSSQLSERVER" />
<ComboBoxItem Content="SQL-DEV-01.corp.local\DEVSQL" />
</ComboBox>
<TextBlock Grid.Column="2" Text="인증 방식:" Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center" Margin="0,0,10,0" />
<ComboBox Grid.Column="3" x:Name="CmbSqlAuth" SelectedIndex="0" Height="32" Background="#0F172A" Foreground="{StaticResource TextPrimaryBrush}" Margin="0,0,16,0">
<ComboBoxItem Content="Windows 통합 인증" />
<ComboBoxItem Content="SQL Server 인증" />
</ComboBox>
<StackPanel Grid.Column="4" Orientation="Horizontal">
<Button x:Name="BtnTestSqlConnection" Content="연결 테스트" Style="{StaticResource SecondaryButtonStyle}" Margin="0,0,8,0" />
<Button x:Name="BtnSqlRefresh" Content="새로고침" Style="{StaticResource SecondaryButtonStyle}" />
</StackPanel>
<StackPanel Grid.Column="5" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
<Border Background="#10B981" Width="10" Height="10" CornerRadius="5" Margin="0,0,6,0" />
<TextBlock Text="Microsoft SQL Server 2022 (v16.0) Enterprise" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" />
</StackPanel>
</Grid>
</Border>
<!-- Main Databases & Tools Split Layout -->
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="7*" />
<ColumnDefinition Width="3*" />
</Grid.ColumnDefinitions>
<!-- Databases DataGrid -->
<Grid Grid.Column="0" Margin="0,0,12,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Action Toolbar -->
<DockPanel Grid.Row="0" Margin="0,0,0,10">
<TextBox x:Name="TxtDbSearch" Width="200" Style="{StaticResource ModernTextBoxStyle}" Margin="0,0,8,0" />
<Button x:Name="BtnDbSearch" Content="DB 검색" Style="{StaticResource SecondaryButtonStyle}" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button x:Name="BtnNewDb" Content="+ 새 데이터베이스" Style="{StaticResource PrimaryButtonStyle}" Margin="0,0,8,0" />
<Button x:Name="BtnBackupDb" Content="즉시 백업 (Full)" Style="{StaticResource SecondaryButtonStyle}" Margin="0,0,8,0" />
<Button x:Name="BtnRestoreDb" Content="백업 복원..." Style="{StaticResource SecondaryButtonStyle}" Margin="0,0,8,0" />
<Button x:Name="BtnShrinkDb" Content="DB 축소" Style="{StaticResource SecondaryButtonStyle}" />
</StackPanel>
</DockPanel>
<Border Grid.Row="1" Style="{StaticResource ModernCardStyle}" Margin="0" Padding="0">
<DataGrid x:Name="SqlDbGrid" Style="{StaticResource ModernDataGridStyle}">
<DataGrid.Columns>
<DataGridTextColumn Header="데이터베이스명" Binding="{Binding DatabaseName}" Width="160" />
<DataGridTextColumn Header="상태" Binding="{Binding StateDesc}" Width="90" />
<DataGridTextColumn Header="전체 크기" Binding="{Binding TotalSizeMB}" Width="100" />
<DataGridTextColumn Header="여유 공간" Binding="{Binding FreeSpaceMB}" Width="100" />
<DataGridTextColumn Header="복구 모델" Binding="{Binding RecoveryModel}" Width="100" />
<DataGridTextColumn Header="데이터 정렬 (Collation)" Binding="{Binding CollationName}" Width="170" />
<DataGridTextColumn Header="최근 Full 백업 일시" Binding="{Binding LastBackupDate}" Width="*" />
</DataGrid.Columns>
</DataGrid>
</Border>
</Grid>
<!-- Database Properties & Quick Query Panel -->
<Border Grid.Column="1" Style="{StaticResource ModernCardStyle}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel>
<TextBlock Text="선택 DB 상세 및 빠른 쿼리" FontSize="15" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}" Margin="0,0,0,14" />
<!-- Selected DB Box -->
<Border Background="#121A27" CornerRadius="8" Padding="12" Margin="0,0,0,14">
<StackPanel>
<TextBlock x:Name="TxtSelectedDbName" Text="ERP_PRODUCTION" FontSize="16" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}" />
<TextBlock Text="호환성 수준: 160 (SQL 2022) | 다중 사용자 모드" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,3,0,0" />
</StackPanel>
</Border>
<TextBlock Text="스토리지 파일 정보" FontSize="13" FontWeight="SemiBold" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,6" />
<Grid Margin="0,3">
<TextBlock Text="MDF (데이터):" Foreground="{StaticResource TextSecondaryBrush}" FontSize="12" />
<TextBlock Text="48,250 MB" Foreground="{StaticResource TextPrimaryBrush}" FontSize="12" HorizontalAlignment="Right" />
</Grid>
<Grid Margin="0,3">
<TextBlock Text="LDF (로그):" Foreground="{StaticResource TextSecondaryBrush}" FontSize="12" />
<TextBlock Text="8,120 MB" Foreground="{StaticResource TextPrimaryBrush}" FontSize="12" HorizontalAlignment="Right" />
</Grid>
<Separator Background="{StaticResource CardBorderBrush}" Margin="0,12" />
<!-- Quick T-SQL Executor Console -->
<TextBlock Text="T-SQL 간이 쿼리 실행" FontSize="13" FontWeight="SemiBold" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,6" />
<TextBox x:Name="TxtSqlScript" Height="100" TextWrapping="Wrap" AcceptsReturn="True"
Style="{StaticResource ModernTextBoxStyle}"
FontFamily="Consolas" FontSize="12"
Text="SELECT TOP 10 name, create_date FROM sys.tables ORDER BY create_date DESC;" />
<DockPanel Margin="0,8,0,0">
<Button x:Name="BtnExecuteQuery" Content="▶ 쿼리 실행" Style="{StaticResource PrimaryButtonStyle}" HorizontalAlignment="Left" />
<Button x:Name="BtnViewTables" Content="테이블 목록 조회" Style="{StaticResource SecondaryButtonStyle}" HorizontalAlignment="Right" />
</DockPanel>
<!-- Mini Output Window -->
<TextBlock Text="실행 결과:" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,10,0,4" />
<Border Background="#090D16" CornerRadius="4" Padding="8" Height="90">
<ScrollViewer>
<TextBlock x:Name="TxtSqlOutput" Text="(10개 행 반환됨 - 0.012초 소요)" FontSize="11" Foreground="#A78BFA" FontFamily="Consolas" />
</ScrollViewer>
</Border>
</StackPanel>
</ScrollViewer>
</Border>
</Grid>
<!-- Status Footer -->
<Border Grid.Row="3" Margin="0,12,0,0">
<DockPanel>
<TextBlock x:Name="TxtSqlStatusMsg" Text="SQL 인스턴스 온라인 - 총 활성 세션: 34개" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" />
<TextBlock x:Name="TxtSqlTotalCount" Text="총 16개 데이터베이스 감지됨" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" HorizontalAlignment="Right" />
</DockPanel>
</Border>
</Grid>
</UserControl>
+35
View File
@@ -0,0 +1,35 @@
{
"Application": {
"LiveMode": false,
"Theme": "Dark",
"AutoRefreshSeconds": 60
},
"ActiveDirectory": {
"UseLive": false,
"DomainController": "dc01.corp.local",
"Domain": "corp.local",
"BaseOU": "DC=corp,DC=local",
"UseLdaps": false,
"Username": "",
"Password": ""
},
"NetApp": {
"UseLive": false,
"ClusterEndpoint": "https://10.200.10.50",
"Port": 443,
"DefaultSvm": "svm_cifs_prod01",
"Username": "admin",
"Password": "",
"IgnoreSslErrors": true
},
"MsSql": {
"UseLive": false,
"Server": "localhost",
"AuthType": "Integrated",
"Username": "sa",
"Password": "",
"DefaultDatabase": "master",
"ConnectionTimeout": 10,
"BackupDirectory": "C:\\Backups"
}
}