initial commit

This commit is contained in:
backuppc
2025-07-07 17:05:06 +09:00
commit 5a0ff2e3ad
22 changed files with 1599 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
*.suo
*.user
*.pdb
bin
obj
desktop.ini
.vs
packages
*.zip

12
App.config Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
</configSections>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
</startup>
<connectionStrings>
<add name="VNCServerList.Properties.Settings.VNCServerDB" connectionString="Data Source=K4FASQL.kr.ds.amkor.com,50150;Initial Catalog=EE;User ID=eeadm;Password=uJnU8a8q&amp;DJ+ug-D"
providerName="System.Data.SqlClient" />
</connectionStrings>
</configuration>

13
CreateTable.sql Normal file
View File

@@ -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]

47
Form1.Designer.cs generated Normal file
View File

@@ -0,0 +1,47 @@
namespace VNCServerList
{
partial class Form1
{
/// <summary>
/// 필수 디자이너 변수입니다.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// 사용 중인 모든 리소스를 정리합니다.
/// </summary>
/// <param name="disposing">관리되는 리소스를 삭제해야 하면 true이고, 그렇지 않으면 false입니다.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form
/// <summary>
/// 디자이너 지원에 필요한 메서드입니다.
/// 이 메서드의 내용을 코드 편집기로 수정하지 마세요.
/// </summary>
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
}
}

59
Form1.cs Normal file
View File

@@ -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<Startup>(options);
}
catch (Exception ex)
{
MessageBox.Show($"웹 서버 시작 중 오류가 발생했습니다: {ex.Message}", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
webApp?.Dispose();
base.OnFormClosing(e);
}
}
}

120
Form1.resx Normal file
View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

14
Models/VNCServer.cs Normal file
View File

@@ -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; }
}
}

22
Program.cs Normal file
View File

@@ -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
{
/// <summary>
/// 해당 응용 프로그램의 주 진입점입니다.
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
}
}

View File

@@ -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")]

71
Properties/Resources.Designer.cs generated Normal file
View File

@@ -0,0 +1,71 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 이 코드는 도구를 사용하여 생성되었습니다.
// 런타임 버전:4.0.30319.42000
//
// 파일 내용을 변경하면 잘못된 동작이 발생할 수 있으며, 코드를 다시 생성하면
// 이러한 변경 내용이 손실됩니다.
// </auto-generated>
//------------------------------------------------------------------------------
namespace VNCServerList.Properties
{
/// <summary>
/// 지역화된 문자열 등을 찾기 위한 강력한 형식의 리소스 클래스입니다.
/// </summary>
// 이 클래스는 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()
{
}
/// <summary>
/// 이 클래스에서 사용하는 캐시된 ResourceManager 인스턴스를 반환합니다.
/// </summary>
[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;
}
}
/// <summary>
/// 이 강력한 형식의 리소스 클래스를 사용하여 모든 리소스 조회에 대해 현재 스레드의 CurrentUICulture 속성을
/// 재정의합니다.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture
{
get
{
return resourceCulture;
}
set
{
resourceCulture = value;
}
}
}
}

117
Properties/Resources.resx Normal file
View File

@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

37
Properties/Settings.Designer.cs generated Normal file
View File

@@ -0,0 +1,37 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 이 코드는 도구를 사용하여 생성되었습니다.
// 런타임 버전:4.0.30319.42000
//
// 파일 내용을 변경하면 잘못된 동작이 발생할 수 있으며, 코드를 다시 생성하면
// 이러한 변경 내용이 손실됩니다.
// </auto-generated>
//------------------------------------------------------------------------------
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"]));
}
}
}
}

View File

@@ -0,0 +1,14 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)" GeneratedClassNamespace="VNCServerList.Properties" GeneratedClassName="Settings">
<Profiles />
<Settings>
<Setting Name="VNCServerDB" Type="(Connection string)" Scope="Application">
<DesignTimeValue Profile="(Default)">&lt;?xml version="1.0" encoding="utf-16"?&gt;
&lt;SerializableConnectionString xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"&gt;
&lt;ConnectionString&gt;Data Source=K4FASQL.kr.ds.amkor.com,50150;Initial Catalog=EE;User ID=eeadm;Password=uJnU8a8q&amp;amp;DJ+ug-D&lt;/ConnectionString&gt;
&lt;ProviderName&gt;System.Data.SqlClient&lt;/ProviderName&gt;
&lt;/SerializableConnectionString&gt;</DesignTimeValue>
<Value Profile="(Default)">Data Source=K4FASQL.kr.ds.amkor.com,50150;Initial Catalog=EE;User ID=eeadm;Password=uJnU8a8q&amp;DJ+ug-D</Value>
</Setting>
</Settings>
</SettingsFile>

16
ReadMe.MD Normal file
View File

@@ -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

177
Services/DatabaseService.cs Normal file
View File

@@ -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<VNCServer> GetAllServers()
{
var servers = new List<VNCServer>();
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;
}
}
}
}
}

69
Services/VNCService.cs Normal file
View File

@@ -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;
}
}
}

105
VNCServerList.csproj Normal file
View File

@@ -0,0 +1,105 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{0403AC4C-8858-4ACE-8D66-9EB307503B04}</ProjectGuid>
<OutputType>WinExe</OutputType>
<RootNamespace>VNCServerList</RootNamespace>
<AssemblyName>VNCServerList</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Deployment" />
<Reference Include="System.Drawing" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
<Reference Include="System.Web" />
<Reference Include="System.Configuration" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.2151.40" />
<PackageReference Include="Microsoft.Owin.Hosting" Version="4.2.2" />
<PackageReference Include="Microsoft.Owin.Host.HttpListener" Version="4.2.2" />
<PackageReference Include="Microsoft.Owin.StaticFiles" Version="4.2.2" />
<PackageReference Include="Microsoft.AspNet.WebApi.Core" Version="5.2.9" />
<PackageReference Include="Microsoft.AspNet.WebApi.Owin" Version="5.2.9" />
<PackageReference Include="System.Data.SqlClient" Version="4.8.5" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
<Compile Include="Form1.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Form1.Designer.cs">
<DependentUpon>Form1.cs</DependentUpon>
</Compile>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Models\VNCServer.cs" />
<Compile Include="Services\DatabaseService.cs" />
<Compile Include="Services\VNCService.cs" />
<Compile Include="Web\Startup.cs" />
<Compile Include="Web\Controllers\VNCServerController.cs" />
<EmbeddedResource Include="Form1.resx">
<DependentUpon>Form1.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
<SubType>Designer</SubType>
</EmbeddedResource>
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
<Compile Include="Properties\Settings.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
</ItemGroup>
<ItemGroup>
<Content Include="Web\wwwroot\**\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

25
VNCServerList.sln Normal file
View File

@@ -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

View File

@@ -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);
}
}
}
}

41
Web/Startup.cs Normal file
View File

@@ -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);
}
}
}

117
Web/wwwroot/index.html Normal file
View File

@@ -0,0 +1,117 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VNC 서버 목록 관리</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#3B82F6',
secondary: '#6B7280',
success: '#10B981',
danger: '#EF4444',
warning: '#F59E0B'
}
}
}
}
</script>
</head>
<body class="bg-gray-50 min-h-screen">
<div class="container mx-auto px-4 py-8">
<!-- 헤더 -->
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
<h1 class="text-3xl font-bold text-gray-800 mb-2">VNC 서버 목록 관리</h1>
<p class="text-gray-600">VNC 서버를 관리하고 연결할 수 있습니다.</p>
</div>
<!-- 상태 표시 -->
<div id="status" class="mb-6"></div>
<!-- 서버 추가 버튼 -->
<div class="mb-6">
<button id="addServerBtn" class="bg-primary hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-lg transition duration-200">
<svg class="w-5 h-5 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
새 서버 추가
</button>
</div>
<!-- 서버 목록 -->
<div class="bg-white rounded-lg shadow-md overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-xl font-semibold text-gray-800">서버 목록</h2>
</div>
<div id="serverList" class="divide-y divide-gray-200">
<!-- 서버 목록이 여기에 동적으로 로드됩니다 -->
</div>
</div>
</div>
<!-- 서버 추가/편집 모달 -->
<div id="serverModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white rounded-lg shadow-xl max-w-md w-full">
<div class="px-6 py-4 border-b border-gray-200">
<h3 id="modalTitle" class="text-lg font-semibold text-gray-800">서버 추가</h3>
</div>
<form id="serverForm" class="p-6">
<div class="mb-4">
<label for="serverUser" class="block text-sm font-medium text-gray-700 mb-2">사용자</label>
<input type="text" id="serverUser" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" required>
</div>
<div class="mb-4">
<label for="serverIp" class="block text-sm font-medium text-gray-700 mb-2">IP 주소</label>
<input type="text" id="serverIp" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" required>
</div>
<div class="mb-4">
<label for="serverCategory" class="block text-sm font-medium text-gray-700 mb-2">카테고리</label>
<input type="text" id="serverCategory" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
</div>
<div class="mb-4">
<label for="serverDescription" class="block text-sm font-medium text-gray-700 mb-2">설명</label>
<textarea id="serverDescription" rows="2" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"></textarea>
</div>
<div class="mb-4">
<label for="serverPassword" class="block text-sm font-medium text-gray-700 mb-2">비밀번호</label>
<input type="password" id="serverPassword" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
</div>
<div class="mb-6">
<label for="serverArgument" class="block text-sm font-medium text-gray-700 mb-2">VNC 인수</label>
<input type="text" id="serverArgument" placeholder="-port=5900" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
</div>
<div class="flex justify-end space-x-3">
<button type="button" id="cancelBtn" class="px-4 py-2 text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300 transition duration-200">취소</button>
<button type="submit" class="px-4 py-2 bg-primary text-white rounded-md hover:bg-blue-700 transition duration-200">저장</button>
</div>
</form>
</div>
</div>
</div>
<!-- 확인 모달 -->
<div id="confirmModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white rounded-lg shadow-xl max-w-md w-full">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-semibold text-gray-800">확인</h3>
</div>
<div class="p-6">
<p id="confirmMessage" class="text-gray-700 mb-6"></p>
<div class="flex justify-end space-x-3">
<button id="confirmCancel" class="px-4 py-2 text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300 transition duration-200">취소</button>
<button id="confirmOk" class="px-4 py-2 bg-danger text-white rounded-md hover:bg-red-700 transition duration-200">확인</button>
</div>
</div>
</div>
</div>
</div>
<script src="js/app.js"></script>
</body>
</html>

305
Web/wwwroot/js/app.js Normal file
View File

@@ -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 = `
<div class="px-6 py-8 text-center text-gray-500">
<svg class="w-12 h-12 mx-auto mb-4 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<p>등록된 서버가 없습니다.</p>
<p class="text-sm">새 서버를 추가해보세요.</p>
</div>
`;
return;
}
serverList.innerHTML = servers.map(server => `
<div class="px-6 py-4 hover:bg-gray-50 transition duration-200">
<div class="flex items-center justify-between">
<div class="flex-1">
<div class="flex items-center space-x-3">
<div class="w-3 h-3 bg-green-500 rounded-full"></div>
<h3 class="text-lg font-medium text-gray-900">${this.escapeHtml(server.user)}@${this.escapeHtml(server.ip)}</h3>
</div>
<div class="mt-2 text-sm text-gray-600">
<p><span class="font-medium">IP:</span> ${this.escapeHtml(server.ip)}</p>
${server.category ? `<p class="mt-1"><span class="font-medium">카테고리:</span> ${this.escapeHtml(server.category)}</p>` : ''}
${server.description ? `<p class="mt-1"><span class="font-medium">설명:</span> ${this.escapeHtml(server.description)}</p>` : ''}
${server.argument ? `<p class="mt-1"><span class="font-medium">인수:</span> ${this.escapeHtml(server.argument)}</p>` : ''}
</div>
</div>
<div class="flex space-x-2">
<button onclick="app.connectToServer('${this.escapeHtml(server.user)}', '${this.escapeHtml(server.ip)}')" class="bg-primary hover:bg-blue-700 text-white px-3 py-1 rounded text-sm transition duration-200">
연결
</button>
<button onclick="app.editServer('${this.escapeHtml(server.user)}', '${this.escapeHtml(server.ip)}')" class="bg-secondary hover:bg-gray-600 text-white px-3 py-1 rounded text-sm transition duration-200">
편집
</button>
<button onclick="app.deleteServer('${this.escapeHtml(server.user)}', '${this.escapeHtml(server.ip)}', '${this.escapeHtml(server.user)}@${this.escapeHtml(server.ip)}')" class="bg-danger hover:bg-red-700 text-white px-3 py-1 rounded text-sm transition duration-200">
삭제
</button>
</div>
</div>
</div>
`).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 = `
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
<div class="flex items-center">
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
</svg>
VNC Viewer가 설치되어 있습니다.
</div>
</div>
`;
} else {
statusDiv.innerHTML = `
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
<div class="flex items-center">
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path>
</svg>
VNC Viewer가 설치되어 있지 않습니다. 경로: ${status.path}
</div>
</div>
`;
}
}
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();