From 5a0ff2e3adf399e4111ed465116070f0dce0159e Mon Sep 17 00:00:00 2001 From: backuppc Date: Mon, 7 Jul 2025 17:05:06 +0900 Subject: [PATCH] initial commit --- .gitignore | 9 + App.config | 12 + CreateTable.sql | 13 ++ Form1.Designer.cs | 47 ++++ Form1.cs | 59 +++++ Form1.resx | 120 ++++++++++ Models/VNCServer.cs | 14 ++ Program.cs | 22 ++ Properties/AssemblyInfo.cs | 36 +++ Properties/Resources.Designer.cs | 71 ++++++ Properties/Resources.resx | 117 ++++++++++ Properties/Settings.Designer.cs | 37 +++ Properties/Settings.settings | 14 ++ ReadMe.MD | 16 ++ Services/DatabaseService.cs | 177 ++++++++++++++ Services/VNCService.cs | 69 ++++++ VNCServerList.csproj | 105 +++++++++ VNCServerList.sln | 25 ++ Web/Controllers/VNCServerController.cs | 173 ++++++++++++++ Web/Startup.cs | 41 ++++ Web/wwwroot/index.html | 117 ++++++++++ Web/wwwroot/js/app.js | 305 +++++++++++++++++++++++++ 22 files changed, 1599 insertions(+) create mode 100644 .gitignore create mode 100644 App.config create mode 100644 CreateTable.sql create mode 100644 Form1.Designer.cs create mode 100644 Form1.cs create mode 100644 Form1.resx create mode 100644 Models/VNCServer.cs create mode 100644 Program.cs create mode 100644 Properties/AssemblyInfo.cs create mode 100644 Properties/Resources.Designer.cs create mode 100644 Properties/Resources.resx create mode 100644 Properties/Settings.Designer.cs create mode 100644 Properties/Settings.settings create mode 100644 ReadMe.MD create mode 100644 Services/DatabaseService.cs create mode 100644 Services/VNCService.cs create mode 100644 VNCServerList.csproj create mode 100644 VNCServerList.sln create mode 100644 Web/Controllers/VNCServerController.cs create mode 100644 Web/Startup.cs create mode 100644 Web/wwwroot/index.html create mode 100644 Web/wwwroot/js/app.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f7f2c57 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +*.suo +*.user +*.pdb +bin +obj +desktop.ini +.vs +packages +*.zip diff --git a/App.config b/App.config new file mode 100644 index 0000000..3ea2120 --- /dev/null +++ b/App.config @@ -0,0 +1,12 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/CreateTable.sql b/CreateTable.sql new file mode 100644 index 0000000..4521f3e --- /dev/null +++ b/CreateTable.sql @@ -0,0 +1,13 @@ +CREATE TABLE [dbo].[VNC_ServerList]( + [User] [varchar](50) NOT NULL, + [IP] [varchar](20) NOT NULL, + [Category] [varchar](20) NULL, + [Description] [varchar](100) NULL, + [Password] [varchar](20) NULL, + [Argument] [varchar](50) NULL, + CONSTRAINT [PK_VNC_ServerList] PRIMARY KEY CLUSTERED +( + [User] ASC, + [IP] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY] +) ON [PRIMARY] \ No newline at end of file diff --git a/Form1.Designer.cs b/Form1.Designer.cs new file mode 100644 index 0000000..ac1c829 --- /dev/null +++ b/Form1.Designer.cs @@ -0,0 +1,47 @@ +namespace VNCServerList +{ + partial class Form1 + { + /// + /// 필수 디자이너 변수입니다. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// 사용 중인 모든 리소스를 정리합니다. + /// + /// 관리되는 리소스를 삭제해야 하면 true이고, 그렇지 않으면 false입니다. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form 디자이너에서 생성한 코드 + + /// + /// 디자이너 지원에 필요한 메서드입니다. + /// 이 메서드의 내용을 코드 편집기로 수정하지 마세요. + /// + private void InitializeComponent() + { + this.SuspendLayout(); + // + // Form1 + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(463, 310); + this.Name = "Form1"; + this.Text = "Form1"; + this.ResumeLayout(false); + + } + + #endregion + } +} + diff --git a/Form1.cs b/Form1.cs new file mode 100644 index 0000000..bacc5e1 --- /dev/null +++ b/Form1.cs @@ -0,0 +1,59 @@ +using System; +using System.Windows.Forms; +using Microsoft.Web.WebView2.WinForms; +using Microsoft.Owin.Hosting; +using VNCServerList.Web; + +namespace VNCServerList +{ + public partial class Form1 : Form + { + private WebView2 webView; + private IDisposable webApp; + + public Form1() + { + InitializeComponent(); + StartWebServer(); + InitializeWebView(); + + } + + private async void InitializeWebView() + { + webView = new WebView2(); + webView.Dock = DockStyle.Fill; + this.Controls.Add(webView); + + try + { + await webView.EnsureCoreWebView2Async(null); + webView.CoreWebView2.Navigate("http://localhost:8080"); + } + catch (Exception ex) + { + MessageBox.Show($"WebView2 초기화 중 오류가 발생했습니다: {ex.Message}", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + private void StartWebServer() + { + try + { + var options = new StartOptions("http://localhost:8080"); + + webApp = WebApp.Start(options); + } + catch (Exception ex) + { + MessageBox.Show($"웹 서버 시작 중 오류가 발생했습니다: {ex.Message}", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + protected override void OnFormClosing(FormClosingEventArgs e) + { + webApp?.Dispose(); + base.OnFormClosing(e); + } + } +} diff --git a/Form1.resx b/Form1.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/Form1.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Models/VNCServer.cs b/Models/VNCServer.cs new file mode 100644 index 0000000..b1720e5 --- /dev/null +++ b/Models/VNCServer.cs @@ -0,0 +1,14 @@ +using System; + +namespace VNCServerList.Models +{ + public class VNCServer + { + public string User { get; set; } + public string IP { get; set; } + public string Category { get; set; } + public string Description { get; set; } + public string Password { get; set; } + public string Argument { get; set; } + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..717e51e --- /dev/null +++ b/Program.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace VNCServerList +{ + static class Program + { + /// + /// 해당 응용 프로그램의 주 진입점입니다. + /// + [STAThread] + static void Main() + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.Run(new Form1()); + } + } +} diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..e2e29f4 --- /dev/null +++ b/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// 어셈블리에 대한 일반 정보는 다음 특성 집합을 통해 +// 제어됩니다. 어셈블리와 관련된 정보를 수정하려면 +// 이러한 특성 값을 변경하세요. +[assembly: AssemblyTitle("VNCServerList")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("VNCServerList")] +[assembly: AssemblyCopyright("Copyright © 2025")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// ComVisible을 false로 설정하면 이 어셈블리의 형식이 COM 구성 요소에 +// 표시되지 않습니다. COM에서 이 어셈블리의 형식에 액세스하려면 +// 해당 형식에 대해 ComVisible 특성을 true로 설정하세요. +[assembly: ComVisible(false)] + +// 이 프로젝트가 COM에 노출되는 경우 다음 GUID는 typelib의 ID를 나타냅니다. +[assembly: Guid("0403ac4c-8858-4ace-8d66-9eb307503b04")] + +// 어셈블리의 버전 정보는 다음 네 가지 값으로 구성됩니다. +// +// 주 버전 +// 부 버전 +// 빌드 번호 +// 수정 버전 +// +// 모든 값을 지정하거나 아래와 같이 '*'를 사용하여 빌드 번호 및 수정 번호가 자동으로 +// 지정되도록 할 수 있습니다. +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Properties/Resources.Designer.cs b/Properties/Resources.Designer.cs new file mode 100644 index 0000000..45f9058 --- /dev/null +++ b/Properties/Resources.Designer.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// +// 이 코드는 도구를 사용하여 생성되었습니다. +// 런타임 버전:4.0.30319.42000 +// +// 파일 내용을 변경하면 잘못된 동작이 발생할 수 있으며, 코드를 다시 생성하면 +// 이러한 변경 내용이 손실됩니다. +// +//------------------------------------------------------------------------------ + +namespace VNCServerList.Properties +{ + + + /// + /// 지역화된 문자열 등을 찾기 위한 강력한 형식의 리소스 클래스입니다. + /// + // 이 클래스는 ResGen 또는 Visual Studio와 같은 도구를 통해 StronglyTypedResourceBuilder + // 클래스에서 자동으로 생성되었습니다. + // 멤버를 추가하거나 제거하려면 .ResX 파일을 편집한 다음 /str 옵션을 사용하여 + // ResGen을 다시 실행하거나 VS 프로젝트를 다시 빌드하십시오. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources + { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() + { + } + + /// + /// 이 클래스에서 사용하는 캐시된 ResourceManager 인스턴스를 반환합니다. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if ((resourceMan == null)) + { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("VNCServerList.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// 이 강력한 형식의 리소스 클래스를 사용하여 모든 리소스 조회에 대해 현재 스레드의 CurrentUICulture 속성을 + /// 재정의합니다. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture + { + get + { + return resourceCulture; + } + set + { + resourceCulture = value; + } + } + } +} diff --git a/Properties/Resources.resx b/Properties/Resources.resx new file mode 100644 index 0000000..af7dbeb --- /dev/null +++ b/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Properties/Settings.Designer.cs b/Properties/Settings.Designer.cs new file mode 100644 index 0000000..268e022 --- /dev/null +++ b/Properties/Settings.Designer.cs @@ -0,0 +1,37 @@ +//------------------------------------------------------------------------------ +// +// 이 코드는 도구를 사용하여 생성되었습니다. +// 런타임 버전:4.0.30319.42000 +// +// 파일 내용을 변경하면 잘못된 동작이 발생할 수 있으며, 코드를 다시 생성하면 +// 이러한 변경 내용이 손실됩니다. +// +//------------------------------------------------------------------------------ + +namespace VNCServerList.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.9.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.SpecialSettingAttribute(global::System.Configuration.SpecialSetting.ConnectionString)] + [global::System.Configuration.DefaultSettingValueAttribute("Data Source=K4FASQL.kr.ds.amkor.com,50150;Initial Catalog=EE;User ID=eeadm;Passwo" + + "rd=uJnU8a8q&DJ+ug-D")] + public string VNCServerDB { + get { + return ((string)(this["VNCServerDB"])); + } + } + } +} diff --git a/Properties/Settings.settings b/Properties/Settings.settings new file mode 100644 index 0000000..ebb1cff --- /dev/null +++ b/Properties/Settings.settings @@ -0,0 +1,14 @@ + + + + + + <?xml version="1.0" encoding="utf-16"?> +<SerializableConnectionString xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> + <ConnectionString>Data Source=K4FASQL.kr.ds.amkor.com,50150;Initial Catalog=EE;User ID=eeadm;Password=uJnU8a8q&amp;DJ+ug-D</ConnectionString> + <ProviderName>System.Data.SqlClient</ProviderName> +</SerializableConnectionString> + Data Source=K4FASQL.kr.ds.amkor.com,50150;Initial Catalog=EE;User ID=eeadm;Password=uJnU8a8q&DJ+ug-D + + + \ No newline at end of file diff --git a/ReadMe.MD b/ReadMe.MD new file mode 100644 index 0000000..1265001 --- /dev/null +++ b/ReadMe.MD @@ -0,0 +1,16 @@ +## vnc runtime : C:\Program Files\TightVNC\tvnviewer.exe +## 프로그램 설명 +VNC 서버목록을 관리하고 목록을 더블클릭해서 VNC Viewer 를 표시하는 기능을 제공 +목록의 경우 MSSQL 서버에 있음 서버정보는 우선 +Net fx 4.8 winform 버젼으로 개발 +화면구성은 winform 에 Webview2 를 활용하여 구성 +tailwindcss 사용하고 자바스크립트는 필요하면 바닐라 +호스트통신은 owin self host 방식으로 처리한다. +웹페이지는 static file 이용해서 처리되며,, 서버통신은 ajax 기술로 진행하기 + +## MSSQL 서버 정보 +IP : 127.0.0.1 +포트 : 1433 +ID : id +PW : pw + diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs new file mode 100644 index 0000000..fd8e64e --- /dev/null +++ b/Services/DatabaseService.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.SqlClient; +using System.Configuration; +using VNCServerList.Models; + +namespace VNCServerList.Services +{ + public class DatabaseService + { + private readonly string _connectionString; + + public DatabaseService() + { + _connectionString = Properties.Settings.Default.VNCServerDB; + InitializeDatabase(); + } + + private void InitializeDatabase() + { + using (var connection = new SqlConnection(_connectionString)) + { + connection.Open(); + + // VNC_ServerList 테이블이 존재하는지 확인 + string checkTableSql = @" + IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='VNC_ServerList' AND xtype='U') + CREATE TABLE [dbo].[VNC_ServerList]( + [User] [varchar](50) NOT NULL, + [IP] [varchar](20) NOT NULL, + [Category] [varchar](20) NULL, + [Description] [varchar](100) NULL, + [Password] [varchar](20) NULL, + [Argument] [varchar](50) NULL, + CONSTRAINT [PK_VNC_ServerList] PRIMARY KEY CLUSTERED + ( + [User] ASC, + [IP] ASC + ) + )"; + + using (var command = new SqlCommand(checkTableSql, connection)) + { + command.ExecuteNonQuery(); + } + } + } + + public List GetAllServers() + { + var servers = new List(); + + using (var connection = new SqlConnection(_connectionString)) + { + connection.Open(); + string sql = "SELECT * FROM VNC_ServerList ORDER BY [User]"; + + using (var command = new SqlCommand(sql, connection)) + using (var reader = command.ExecuteReader()) + { + while (reader.Read()) + { + servers.Add(new VNCServer + { + User = reader["User"].ToString(), + IP = reader["IP"].ToString(), + Category = reader["Category"] == DBNull.Value ? null : reader["Category"].ToString(), + Description = reader["Description"] == DBNull.Value ? null : reader["Description"].ToString(), + Password = reader["Password"] == DBNull.Value ? null : reader["Password"].ToString(), + Argument = reader["Argument"] == DBNull.Value ? null : reader["Argument"].ToString() + }); + } + } + } + + return servers; + } + + public VNCServer GetServerByUserAndIP(string user, string ip) + { + using (var connection = new SqlConnection(_connectionString)) + { + connection.Open(); + string sql = "SELECT * FROM VNC_ServerList WHERE [User] = @User AND [IP] = @IP"; + + using (var command = new SqlCommand(sql, connection)) + { + command.Parameters.AddWithValue("@User", user); + command.Parameters.AddWithValue("@IP", ip); + + using (var reader = command.ExecuteReader()) + { + if (reader.Read()) + { + return new VNCServer + { + User = reader["User"].ToString(), + IP = reader["IP"].ToString(), + Category = reader["Category"] == DBNull.Value ? null : reader["Category"].ToString(), + Description = reader["Description"] == DBNull.Value ? null : reader["Description"].ToString(), + Password = reader["Password"] == DBNull.Value ? null : reader["Password"].ToString(), + Argument = reader["Argument"] == DBNull.Value ? null : reader["Argument"].ToString() + }; + } + } + } + } + + return null; + } + + public bool AddServer(VNCServer server) + { + using (var connection = new SqlConnection(_connectionString)) + { + connection.Open(); + string sql = @" + INSERT INTO VNC_ServerList ([User], [IP], [Category], [Description], [Password], [Argument]) + VALUES (@User, @IP, @Category, @Description, @Password, @Argument)"; + + using (var command = new SqlCommand(sql, connection)) + { + command.Parameters.AddWithValue("@User", server.User); + command.Parameters.AddWithValue("@IP", server.IP); + command.Parameters.AddWithValue("@Category", (object)server.Category ?? DBNull.Value); + command.Parameters.AddWithValue("@Description", (object)server.Description ?? DBNull.Value); + command.Parameters.AddWithValue("@Password", (object)server.Password ?? DBNull.Value); + command.Parameters.AddWithValue("@Argument", (object)server.Argument ?? DBNull.Value); + + return command.ExecuteNonQuery() > 0; + } + } + } + + public bool UpdateServer(VNCServer server) + { + using (var connection = new SqlConnection(_connectionString)) + { + connection.Open(); + string sql = @" + UPDATE VNC_ServerList + SET [Category] = @Category, [Description] = @Description, + [Password] = @Password, [Argument] = @Argument + WHERE [User] = @User AND [IP] = @IP"; + + using (var command = new SqlCommand(sql, connection)) + { + command.Parameters.AddWithValue("@User", server.User); + command.Parameters.AddWithValue("@IP", server.IP); + command.Parameters.AddWithValue("@Category", (object)server.Category ?? DBNull.Value); + command.Parameters.AddWithValue("@Description", (object)server.Description ?? DBNull.Value); + command.Parameters.AddWithValue("@Password", (object)server.Password ?? DBNull.Value); + command.Parameters.AddWithValue("@Argument", (object)server.Argument ?? DBNull.Value); + + return command.ExecuteNonQuery() > 0; + } + } + } + + public bool DeleteServer(string user, string ip) + { + using (var connection = new SqlConnection(_connectionString)) + { + connection.Open(); + string sql = "DELETE FROM VNC_ServerList WHERE [User] = @User AND [IP] = @IP"; + + using (var command = new SqlCommand(sql, connection)) + { + command.Parameters.AddWithValue("@User", user); + command.Parameters.AddWithValue("@IP", ip); + return command.ExecuteNonQuery() > 0; + } + } + } + } +} \ No newline at end of file diff --git a/Services/VNCService.cs b/Services/VNCService.cs new file mode 100644 index 0000000..3fbfc4a --- /dev/null +++ b/Services/VNCService.cs @@ -0,0 +1,69 @@ +using System; +using System.Diagnostics; +using System.IO; +using VNCServerList.Models; + +namespace VNCServerList.Services +{ + public class VNCService + { + private readonly string _vncViewerPath; + private readonly DatabaseService _databaseService; + + public VNCService(DatabaseService databaseService) + { + _vncViewerPath = @"C:\Program Files\TightVNC\tvnviewer.exe"; + _databaseService = databaseService; + } + + public bool ConnectToServer(VNCServer server) + { + try + { + if (!File.Exists(_vncViewerPath)) + { + throw new FileNotFoundException($"VNC Viewer를 찾을 수 없습니다: {_vncViewerPath}"); + } + + // VNC Viewer 실행 + var startInfo = new ProcessStartInfo + { + FileName = _vncViewerPath, + Arguments = $"-host={server.IP} {server.Argument}", + UseShellExecute = true + }; + + Process.Start(startInfo); + + // 연결 성공 (마지막 연결 시간 업데이트는 현재 테이블 구조에 없으므로 제거) + + return true; + } + catch (Exception ex) + { + throw new Exception($"VNC 연결 중 오류가 발생했습니다: {ex.Message}", ex); + } + } + + public bool ConnectToServer(string user, string ip) + { + var server = _databaseService.GetServerByUserAndIP(user, ip); + if (server == null) + { + throw new ArgumentException($"서버 {user}@{ip}를 찾을 수 없습니다."); + } + + return ConnectToServer(server); + } + + public bool IsVNCViewerInstalled() + { + return File.Exists(_vncViewerPath); + } + + public string GetVNCViewerPath() + { + return _vncViewerPath; + } + } +} \ No newline at end of file diff --git a/VNCServerList.csproj b/VNCServerList.csproj new file mode 100644 index 0000000..4e5db45 --- /dev/null +++ b/VNCServerList.csproj @@ -0,0 +1,105 @@ + + + + + Debug + AnyCPU + {0403AC4C-8858-4ACE-8D66-9EB307503B04} + WinExe + VNCServerList + VNCServerList + v4.8 + 512 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Form + + + Form1.cs + + + + + + + + + + Form1.cs + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + True + Resources.resx + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + True + Settings.settings + True + + + + + PreserveNewest + + + + + + + \ No newline at end of file diff --git a/VNCServerList.sln b/VNCServerList.sln new file mode 100644 index 0000000..83f2d6c --- /dev/null +++ b/VNCServerList.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Express 15 for Windows Desktop +VisualStudioVersion = 15.0.36123.18 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VNCServerList", "VNCServerList.csproj", "{0403AC4C-8858-4ACE-8D66-9EB307503B04}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0403AC4C-8858-4ACE-8D66-9EB307503B04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0403AC4C-8858-4ACE-8D66-9EB307503B04}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0403AC4C-8858-4ACE-8D66-9EB307503B04}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0403AC4C-8858-4ACE-8D66-9EB307503B04}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {4720988D-5D40-4AFC-8D65-C7AFD269D71B} + EndGlobalSection +EndGlobal diff --git a/Web/Controllers/VNCServerController.cs b/Web/Controllers/VNCServerController.cs new file mode 100644 index 0000000..4e9cfed --- /dev/null +++ b/Web/Controllers/VNCServerController.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.Web.Http; +using VNCServerList.Models; +using VNCServerList.Services; + +namespace VNCServerList.Web.Controllers +{ + [RoutePrefix("api/vncserver")] + public class VNCServerController : ApiController + { + private readonly DatabaseService _databaseService; + private readonly VNCService _vncService; + + public VNCServerController() + { + _databaseService = new DatabaseService(); + _vncService = new VNCService(_databaseService); + } + + [HttpGet] + [Route("list")] + public IHttpActionResult GetServerList() + { + try + { + var servers = _databaseService.GetAllServers(); + return Ok(servers); + } + catch (Exception ex) + { + return InternalServerError(ex); + } + } + + [HttpGet] + [Route("get/{user}/{ip}")] + public IHttpActionResult GetServer(string user, string ip) + { + try + { + var server = _databaseService.GetServerByUserAndIP(user, ip); + if (server == null) + { + return NotFound(); + } + return Ok(server); + } + catch (Exception ex) + { + return InternalServerError(ex); + } + } + + [HttpPost] + [Route("add")] + public IHttpActionResult AddServer([FromBody] VNCServer server) + { + try + { + if (server == null) + { + return BadRequest("서버 정보가 없습니다."); + } + + bool success = _databaseService.AddServer(server); + if (success) + { + return Ok(new { Message = "서버가 성공적으로 추가되었습니다." }); + } + else + { + return BadRequest("서버 추가에 실패했습니다."); + } + } + catch (Exception ex) + { + return InternalServerError(ex); + } + } + + [HttpPut] + [Route("update")] + public IHttpActionResult UpdateServer([FromBody] VNCServer server) + { + try + { + if (server == null) + { + return BadRequest("서버 정보가 없습니다."); + } + + bool success = _databaseService.UpdateServer(server); + if (success) + { + return Ok(new { Message = "서버가 성공적으로 업데이트되었습니다." }); + } + else + { + return NotFound(); + } + } + catch (Exception ex) + { + return InternalServerError(ex); + } + } + + [HttpDelete] + [Route("delete/{user}/{ip}")] + public IHttpActionResult DeleteServer(string user, string ip) + { + try + { + bool success = _databaseService.DeleteServer(user, ip); + if (success) + { + return Ok(new { Message = "서버가 성공적으로 삭제되었습니다." }); + } + else + { + return NotFound(); + } + } + catch (Exception ex) + { + return InternalServerError(ex); + } + } + + [HttpPost] + [Route("connect/{user}/{ip}")] + public IHttpActionResult ConnectToServer(string user, string ip) + { + try + { + bool success = _vncService.ConnectToServer(user, ip); + if (success) + { + return Ok(new { Message = "VNC 연결이 시작되었습니다." }); + } + else + { + return BadRequest("VNC 연결에 실패했습니다."); + } + } + catch (Exception ex) + { + return InternalServerError(ex); + } + } + + [HttpGet] + [Route("vnc-status")] + public IHttpActionResult GetVNCStatus() + { + try + { + bool isInstalled = _vncService.IsVNCViewerInstalled(); + string path = _vncService.GetVNCViewerPath(); + + return Ok(new { + IsInstalled = isInstalled, + Path = path + }); + } + catch (Exception ex) + { + return InternalServerError(ex); + } + } + } +} \ No newline at end of file diff --git a/Web/Startup.cs b/Web/Startup.cs new file mode 100644 index 0000000..39eb8c1 --- /dev/null +++ b/Web/Startup.cs @@ -0,0 +1,41 @@ +using Microsoft.Owin; +using Microsoft.Owin.StaticFiles; +using Owin; +using System.Web.Http; +using VNCServerList.Web.Controllers; + +namespace VNCServerList.Web +{ + public class Startup + { + public void Configuration(IAppBuilder app) + { + // Web API 설정 + var config = new HttpConfiguration(); + + // 라우팅 설정 + config.MapHttpAttributeRoutes(); + + config.Routes.MapHttpRoute( + name: "DefaultApi", + routeTemplate: "api/{controller}/{action}/{id}", + defaults: new { id = RouteParameter.Optional } + ); + + // JSON 포맷터 설정 + config.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore; + + app.UseWebApi(config); + + // 정적 파일 서빙 설정 + var options = new FileServerOptions + { + EnableDefaultFiles = true, + DefaultFilesOptions = { DefaultFileNames = { "index.html" } }, + FileSystem = new Microsoft.Owin.FileSystems.PhysicalFileSystem("Web/wwwroot") + }; + + app.UseFileServer(options); + } + } +} \ No newline at end of file diff --git a/Web/wwwroot/index.html b/Web/wwwroot/index.html new file mode 100644 index 0000000..2b352f2 --- /dev/null +++ b/Web/wwwroot/index.html @@ -0,0 +1,117 @@ + + + + + + VNC 서버 목록 관리 + + + + +
+ +
+

VNC 서버 목록 관리

+

VNC 서버를 관리하고 연결할 수 있습니다.

+
+ + +
+ + +
+ +
+ + +
+
+

서버 목록

+
+
+ +
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Web/wwwroot/js/app.js b/Web/wwwroot/js/app.js new file mode 100644 index 0000000..997ce4b --- /dev/null +++ b/Web/wwwroot/js/app.js @@ -0,0 +1,305 @@ +class VNCServerApp { + constructor() { + this.apiBase = '/api/vncserver'; + this.init(); + } + + init() { + this.bindEvents(); + this.loadServerList(); + this.checkVNCStatus(); + } + + bindEvents() { + // 서버 추가 버튼 + document.getElementById('addServerBtn').addEventListener('click', () => { + this.showServerModal(); + }); + + // 모달 취소 버튼 + document.getElementById('cancelBtn').addEventListener('click', () => { + this.hideServerModal(); + }); + + // 서버 폼 제출 + document.getElementById('serverForm').addEventListener('submit', (e) => { + e.preventDefault(); + this.saveServer(); + }); + + // 확인 모달 이벤트 + document.getElementById('confirmCancel').addEventListener('click', () => { + this.hideConfirmModal(); + }); + + document.getElementById('confirmOk').addEventListener('click', () => { + this.executeConfirmAction(); + }); + } + + async loadServerList() { + try { + const response = await fetch(`${this.apiBase}/list`); + if (!response.ok) throw new Error('서버 목록을 불러올 수 없습니다.'); + + const servers = await response.json(); + this.renderServerList(servers); + } catch (error) { + this.showError('서버 목록을 불러오는 중 오류가 발생했습니다: ' + error.message); + } + } + + renderServerList(servers) { + const serverList = document.getElementById('serverList'); + + if (servers.length === 0) { + serverList.innerHTML = ` +
+ + + +

등록된 서버가 없습니다.

+

새 서버를 추가해보세요.

+
+ `; + return; + } + + serverList.innerHTML = servers.map(server => ` +
+
+
+
+
+

${this.escapeHtml(server.user)}@${this.escapeHtml(server.ip)}

+
+
+

IP: ${this.escapeHtml(server.ip)}

+ ${server.category ? `

카테고리: ${this.escapeHtml(server.category)}

` : ''} + ${server.description ? `

설명: ${this.escapeHtml(server.description)}

` : ''} + ${server.argument ? `

인수: ${this.escapeHtml(server.argument)}

` : ''} +
+
+
+ + + +
+
+
+ `).join(''); + } + + async checkVNCStatus() { + try { + const response = await fetch(`${this.apiBase}/vnc-status`); + if (!response.ok) throw new Error('VNC 상태를 확인할 수 없습니다.'); + + const status = await response.json(); + this.showVNCStatus(status); + } catch (error) { + this.showError('VNC 상태 확인 중 오류가 발생했습니다: ' + error.message); + } + } + + showVNCStatus(status) { + const statusDiv = document.getElementById('status'); + if (status.isInstalled) { + statusDiv.innerHTML = ` +
+
+ + + + VNC Viewer가 설치되어 있습니다. +
+
+ `; + } else { + statusDiv.innerHTML = ` +
+
+ + + + VNC Viewer가 설치되어 있지 않습니다. 경로: ${status.path} +
+
+ `; + } + } + + showServerModal(server = null) { + const modal = document.getElementById('serverModal'); + const title = document.getElementById('modalTitle'); + const form = document.getElementById('serverForm'); + + if (server) { + title.textContent = '서버 편집'; + document.getElementById('serverUser').value = server.user; + document.getElementById('serverIp').value = server.ip; + document.getElementById('serverCategory').value = server.category || ''; + document.getElementById('serverDescription').value = server.description || ''; + document.getElementById('serverPassword').value = server.password || ''; + document.getElementById('serverArgument').value = server.argument || ''; + } else { + title.textContent = '서버 추가'; + form.reset(); + } + + modal.classList.remove('hidden'); + } + + hideServerModal() { + document.getElementById('serverModal').classList.add('hidden'); + } + + async saveServer() { + const serverUser = document.getElementById('serverUser').value; + const serverIp = document.getElementById('serverIp').value; + const serverData = { + user: serverUser, + ip: serverIp, + category: document.getElementById('serverCategory').value, + description: document.getElementById('serverDescription').value, + password: document.getElementById('serverPassword').value, + argument: document.getElementById('serverArgument').value + }; + + try { + const url = serverUser && serverIp ? `${this.apiBase}/update` : `${this.apiBase}/add`; + const method = serverUser && serverIp ? 'PUT' : 'POST'; + + const response = await fetch(url, { + method: method, + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(serverData) + }); + + if (!response.ok) throw new Error('서버 저장에 실패했습니다.'); + + const result = await response.json(); + this.showSuccess(result.message); + this.hideServerModal(); + this.loadServerList(); + } catch (error) { + this.showError('서버 저장 중 오류가 발생했습니다: ' + error.message); + } + } + + async editServer(user, ip) { + try { + const response = await fetch(`${this.apiBase}/get/${encodeURIComponent(user)}/${encodeURIComponent(ip)}`); + if (!response.ok) throw new Error('서버 정보를 불러올 수 없습니다.'); + + const server = await response.json(); + this.showServerModal(server); + } catch (error) { + this.showError('서버 정보를 불러오는 중 오류가 발생했습니다: ' + error.message); + } + } + + deleteServer(user, ip, name) { + this.showConfirmModal( + `"${name}" 서버를 삭제하시겠습니까?`, + () => this.executeDeleteServer(user, ip) + ); + } + + async executeDeleteServer(user, ip) { + try { + const response = await fetch(`${this.apiBase}/delete/${encodeURIComponent(user)}/${encodeURIComponent(ip)}`, { + method: 'DELETE' + }); + + if (!response.ok) throw new Error('서버 삭제에 실패했습니다.'); + + const result = await response.json(); + this.showSuccess(result.message); + this.loadServerList(); + } catch (error) { + this.showError('서버 삭제 중 오류가 발생했습니다: ' + error.message); + } + } + + async connectToServer(user, ip) { + try { + const response = await fetch(`${this.apiBase}/connect/${encodeURIComponent(user)}/${encodeURIComponent(ip)}`, { + method: 'POST' + }); + + if (!response.ok) throw new Error('VNC 연결에 실패했습니다.'); + + const result = await response.json(); + this.showSuccess(result.message); + } catch (error) { + this.showError('VNC 연결 중 오류가 발생했습니다: ' + error.message); + } + } + + showConfirmModal(message, onConfirm) { + document.getElementById('confirmMessage').textContent = message; + document.getElementById('confirmModal').classList.remove('hidden'); + this.confirmAction = onConfirm; + } + + hideConfirmModal() { + document.getElementById('confirmModal').classList.add('hidden'); + this.confirmAction = null; + } + + executeConfirmAction() { + if (this.confirmAction) { + this.confirmAction(); + } + this.hideConfirmModal(); + } + + showSuccess(message) { + this.showNotification(message, 'success'); + } + + showError(message) { + this.showNotification(message, 'error'); + } + + showNotification(message, type) { + const notification = document.createElement('div'); + notification.className = `fixed top-4 right-4 px-6 py-3 rounded-lg text-white z-50 transition-all duration-300 transform translate-x-full ${ + type === 'success' ? 'bg-green-500' : 'bg-red-500' + }`; + notification.textContent = message; + + document.body.appendChild(notification); + + // 애니메이션 + setTimeout(() => { + notification.classList.remove('translate-x-full'); + }, 100); + + // 자동 제거 + setTimeout(() => { + notification.classList.add('translate-x-full'); + setTimeout(() => { + document.body.removeChild(notification); + }, 300); + }, 3000); + } + + escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } +} + +// 앱 초기화 +const app = new VNCServerApp(); \ No newline at end of file