feat: implement modern admin studio with live AD, NetApp, MS SQL connectivity and comprehensive docs
This commit is contained in:
+17
@@ -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
@@ -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();
|
||||
}
|
||||
```
|
||||
@@ -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
@@ -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="" 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="" 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="" 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="" 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="" 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="" 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="" 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 & 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="" 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="" 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="" 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="" 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>
|
||||
@@ -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*"
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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*"
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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*"
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
# Enterprise Admin Studio (통합 인프라 관리 도구)
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
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. **[설정 저장 및 적용]**을 클릭하여 즉시 적용
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 방화벽을 확인하십시오. |
|
||||
@@ -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=" 검색" 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>
|
||||
@@ -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="" 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="" 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="" 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="" 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=" AD 계정 신규 생성 / 잠금 해제" Style="{StaticResource SecondaryButtonStyle}"
|
||||
HorizontalContentAlignment="Left" Margin="0,0,0,10" Padding="12,10" x:Name="QuickBtnAD" />
|
||||
|
||||
<Button Content=" NetApp Quota 용량 증설 요청" Style="{StaticResource SecondaryButtonStyle}"
|
||||
HorizontalContentAlignment="Left" Margin="0,0,0,10" Padding="12,10" x:Name="QuickBtnQuota" />
|
||||
|
||||
<Button Content=" 신규 CIFS/SMB 공유 생성" Style="{StaticResource SecondaryButtonStyle}"
|
||||
HorizontalContentAlignment="Left" Margin="0,0,0,10" Padding="12,10" x:Name="QuickBtnShare" />
|
||||
|
||||
<Button Content=" 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>
|
||||
@@ -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=" 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=" 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=" 파일 공유 (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>
|
||||
@@ -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=" 전체 인프라 연결 상태 일괄 진단" 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>
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user