This commit is contained in:
backuppc
2025-07-08 11:16:43 +09:00
parent d537030eb3
commit 8efe357430
11 changed files with 380 additions and 97 deletions

2
Form1.Designer.cs generated
View File

@@ -34,7 +34,7 @@
// //
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F); this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(784, 561); this.ClientSize = new System.Drawing.Size(584, 561);
this.Name = "Form1"; this.Name = "Form1";
this.Text = "Form1"; this.Text = "Form1";
this.ResumeLayout(false); this.ResumeLayout(false);

View File

@@ -15,6 +15,7 @@ namespace VNCServerList
public Form1() public Form1()
{ {
InitializeComponent(); InitializeComponent();
this.Text = $"{Application.ProductName} ver {Application.ProductVersion}";
StartWebServer(); StartWebServer();
InitializeWebView(); InitializeWebView();

View File

@@ -4,7 +4,9 @@ namespace VNCServerList.Models
{ {
public class AppSettings public class AppSettings
{ {
public string UserName { get; set; } = "";
public string VNCViewerPath { get; set; } = @"C:\Program Files\TightVNC\tvnviewer.exe"; public string VNCViewerPath { get; set; } = @"C:\Program Files\TightVNC\tvnviewer.exe";
public int WebServerPort { get; set; } = 8080; public int WebServerPort { get; set; } = 8080;
public string Argument { get; set; } = "-useclipboard=no -scale=auto -showcontrols=yes";
} }
} }

View File

@@ -7,6 +7,7 @@ namespace VNCServerList.Models
public string User { get; set; } public string User { get; set; }
public string IP { get; set; } public string IP { get; set; }
public string Category { get; set; } public string Category { get; set; }
public string Title { get; set; }
public string Description { get; set; } public string Description { get; set; }
public string Password { get; set; } public string Password { get; set; }
public string Argument { get; set; } public string Argument { get; set; }

View File

@@ -8,9 +8,9 @@ using System.Runtime.InteropServices;
[assembly: AssemblyTitle("VNCServerList")] [assembly: AssemblyTitle("VNCServerList")]
[assembly: AssemblyDescription("")] [assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")] [assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")] [assembly: AssemblyCompany("SIMP")]
[assembly: AssemblyProduct("VNCServerList")] [assembly: AssemblyProduct("VNCServerList")]
[assembly: AssemblyCopyright("Copyright © 2025")] [assembly: AssemblyCopyright("Copyright ©SIMP 2025")]
[assembly: AssemblyTrademark("")] [assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")] [assembly: AssemblyCulture("")]
@@ -32,5 +32,5 @@ using System.Runtime.InteropServices;
// 모든 값을 지정하거나 아래와 같이 '*'를 사용하여 빌드 번호 및 수정 번호가 자동으로 // 모든 값을 지정하거나 아래와 같이 '*'를 사용하여 빌드 번호 및 수정 번호가 자동으로
// 지정되도록 할 수 있습니다. // 지정되도록 할 수 있습니다.
// [assembly: AssemblyVersion("1.0.*")] // [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyVersion("25.07.08.0000")]
[assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyFileVersion("25.07.08.0000")]

View File

@@ -32,6 +32,7 @@ namespace VNCServerList.Services
[User] [varchar](50) NOT NULL, [User] [varchar](50) NOT NULL,
[IP] [varchar](20) NOT NULL, [IP] [varchar](20) NOT NULL,
[Category] [varchar](20) NULL, [Category] [varchar](20) NULL,
[Title] [varchar](50) NULL,
[Description] [varchar](100) NULL, [Description] [varchar](100) NULL,
[Password] [varchar](20) NULL, [Password] [varchar](20) NULL,
[Argument] [varchar](50) NULL, [Argument] [varchar](50) NULL,
@@ -68,6 +69,7 @@ namespace VNCServerList.Services
User = reader["User"].ToString(), User = reader["User"].ToString(),
IP = reader["IP"].ToString(), IP = reader["IP"].ToString(),
Category = reader["Category"] == DBNull.Value ? null : reader["Category"].ToString(), Category = reader["Category"] == DBNull.Value ? null : reader["Category"].ToString(),
Title = reader["Title"] == DBNull.Value ? null : reader["Title"].ToString(),
Description = reader["Description"] == DBNull.Value ? null : reader["Description"].ToString(), Description = reader["Description"] == DBNull.Value ? null : reader["Description"].ToString(),
Password = reader["Password"] == DBNull.Value ? null : reader["Password"].ToString(), Password = reader["Password"] == DBNull.Value ? null : reader["Password"].ToString(),
Argument = reader["Argument"] == DBNull.Value ? null : reader["Argument"].ToString() Argument = reader["Argument"] == DBNull.Value ? null : reader["Argument"].ToString()
@@ -100,6 +102,7 @@ namespace VNCServerList.Services
User = reader["User"].ToString(), User = reader["User"].ToString(),
IP = reader["IP"].ToString(), IP = reader["IP"].ToString(),
Category = reader["Category"] == DBNull.Value ? null : reader["Category"].ToString(), Category = reader["Category"] == DBNull.Value ? null : reader["Category"].ToString(),
Title = reader["Title"] == DBNull.Value ? null : reader["Title"].ToString(),
Description = reader["Description"] == DBNull.Value ? null : reader["Description"].ToString(), Description = reader["Description"] == DBNull.Value ? null : reader["Description"].ToString(),
Password = reader["Password"] == DBNull.Value ? null : reader["Password"].ToString(), Password = reader["Password"] == DBNull.Value ? null : reader["Password"].ToString(),
Argument = reader["Argument"] == DBNull.Value ? null : reader["Argument"].ToString() Argument = reader["Argument"] == DBNull.Value ? null : reader["Argument"].ToString()
@@ -118,7 +121,7 @@ namespace VNCServerList.Services
{ {
connection.Open(); connection.Open();
string sql = @" string sql = @"
INSERT INTO VNC_ServerList ([User], [IP], [Category], [Description], [Password], [Argument]) INSERT INTO VNC_ServerList ([User], [IP], [Category], [Title],[Description], [Password], [Argument])
VALUES (@User, @IP, @Category, @Description, @Password, @Argument)"; VALUES (@User, @IP, @Category, @Description, @Password, @Argument)";
using (var command = new SqlCommand(sql, connection)) using (var command = new SqlCommand(sql, connection))
@@ -126,6 +129,7 @@ namespace VNCServerList.Services
command.Parameters.AddWithValue("@User", server.User); command.Parameters.AddWithValue("@User", server.User);
command.Parameters.AddWithValue("@IP", server.IP); command.Parameters.AddWithValue("@IP", server.IP);
command.Parameters.AddWithValue("@Category", (object)server.Category ?? DBNull.Value); command.Parameters.AddWithValue("@Category", (object)server.Category ?? DBNull.Value);
command.Parameters.AddWithValue("@Title", (object)server.Title ?? DBNull.Value);
command.Parameters.AddWithValue("@Description", (object)server.Description ?? DBNull.Value); command.Parameters.AddWithValue("@Description", (object)server.Description ?? DBNull.Value);
command.Parameters.AddWithValue("@Password", (object)server.Password ?? DBNull.Value); command.Parameters.AddWithValue("@Password", (object)server.Password ?? DBNull.Value);
command.Parameters.AddWithValue("@Argument", (object)server.Argument ?? DBNull.Value); command.Parameters.AddWithValue("@Argument", (object)server.Argument ?? DBNull.Value);
@@ -142,7 +146,7 @@ namespace VNCServerList.Services
connection.Open(); connection.Open();
string sql = @" string sql = @"
UPDATE VNC_ServerList UPDATE VNC_ServerList
SET [Category] = @Category, [Description] = @Description, SET [Category] = @Category, [Title] = @Title, [Description] = @Description,
[Password] = @Password, [Argument] = @Argument [Password] = @Password, [Argument] = @Argument
WHERE [User] = @User AND [IP] = @IP"; WHERE [User] = @User AND [IP] = @IP";
@@ -151,6 +155,7 @@ namespace VNCServerList.Services
command.Parameters.AddWithValue("@User", server.User); command.Parameters.AddWithValue("@User", server.User);
command.Parameters.AddWithValue("@IP", server.IP); command.Parameters.AddWithValue("@IP", server.IP);
command.Parameters.AddWithValue("@Category", (object)server.Category ?? DBNull.Value); command.Parameters.AddWithValue("@Category", (object)server.Category ?? DBNull.Value);
command.Parameters.AddWithValue("@Title", (object)server.Title ?? DBNull.Value);
command.Parameters.AddWithValue("@Description", (object)server.Description ?? DBNull.Value); command.Parameters.AddWithValue("@Description", (object)server.Description ?? DBNull.Value);
command.Parameters.AddWithValue("@Password", (object)server.Password ?? DBNull.Value); command.Parameters.AddWithValue("@Password", (object)server.Password ?? DBNull.Value);
command.Parameters.AddWithValue("@Argument", (object)server.Argument ?? DBNull.Value); command.Parameters.AddWithValue("@Argument", (object)server.Argument ?? DBNull.Value);

View File

@@ -20,25 +20,44 @@ namespace VNCServerList.Services
{ {
try try
{ {
var vncViewerPath = _settingsService.GetSettings().VNCViewerPath; var settings = _settingsService.GetSettings();
var vncViewerPath = settings.VNCViewerPath;
if (!File.Exists(vncViewerPath)) if (!File.Exists(vncViewerPath))
{ {
throw new FileNotFoundException($"VNC Viewer를 찾을 수 없습니다: {vncViewerPath}"); throw new FileNotFoundException($"VNC Viewer를 찾을 수 없습니다: {vncViewerPath}");
} }
// 서버의 argument가 없으면 설정의 기본 argument 사용
var arguments = $"-host={server.IP}";
if (!string.IsNullOrEmpty(server.Argument))
{
arguments += $" {server.Argument}";
}
else if (!string.IsNullOrEmpty(settings.Argument))
{
arguments += $" {settings.Argument}";
}
// 비밀번호가 있으면 추가
if (!string.IsNullOrEmpty(server.Password))
{
arguments += $" -password={server.Password}";
}
System.Diagnostics.Debug.WriteLine($"VNC 실행: 경로='{vncViewerPath}', 인수='{arguments}'");
// VNC Viewer 실행 // VNC Viewer 실행
var startInfo = new ProcessStartInfo var startInfo = new ProcessStartInfo
{ {
FileName = vncViewerPath, FileName = vncViewerPath,
Arguments = $"-host={server.IP} {server.Argument}", Arguments = arguments,
UseShellExecute = true UseShellExecute = true
}; };
Process.Start(startInfo); Process.Start(startInfo);
// 연결 성공 (마지막 연결 시간 업데이트는 현재 테이블 구조에 없으므로 제거)
return true; return true;
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Web.Http; using System.Web.Http;
using System.Linq;
using VNCServerList.Models; using VNCServerList.Models;
using VNCServerList.Services; using VNCServerList.Services;
@@ -22,12 +23,21 @@ namespace VNCServerList.Web.Controllers
[HttpGet] [HttpGet]
[Route("list")] [Route("list")]
public IHttpActionResult GetServerList() public IHttpActionResult GetServerList(string userName = null)
{ {
try try
{ {
var servers = _databaseService.GetAllServers(); var servers = _databaseService.GetAllServers();
return Ok(servers);
// 사용자 이름이 있으면 필터링
if (!string.IsNullOrEmpty(userName))
{
servers = servers.Where(s => s.User.Equals(userName, StringComparison.OrdinalIgnoreCase)).ToList();
}
// 사용자명으로 오름차순 정렬
var sortedServers = servers.OrderBy(s => s.User).ThenBy(s => s.IP).ToList();
return Ok(sortedServers);
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -36,6 +36,18 @@ namespace VNCServerList.Web
}; };
app.UseFileServer(options); app.UseFileServer(options);
// 캐시 방지 미들웨어 추가
app.Use(async (context, next) =>
{
if (context.Request.Path.Value.EndsWith(".js") || context.Request.Path.Value.EndsWith(".css"))
{
context.Response.Headers["Cache-Control"] = "no-cache, no-store, must-revalidate";
context.Response.Headers["Pragma"] = "no-cache";
context.Response.Headers["Expires"] = "0";
}
await next();
});
} }
} }
} }

View File

@@ -3,6 +3,9 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>VNC 서버 목록 관리</title> <title>VNC 서버 목록 관리</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<script> <script>
@@ -22,25 +25,27 @@
</script> </script>
</head> </head>
<body class="bg-gray-50 min-h-screen"> <body class="bg-gray-50 min-h-screen">
<div class="container mx-auto px-4 py-8"> <div class="container mx-auto px-4 py-2">
<!-- 헤더 -->
<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> <div id="status" class="mb-4"></div>
<p class="text-gray-600">VNC 서버를 관리하고 연결할 수 있습니다.</p>
<!-- 서버 목록 -->
<div class="bg-white rounded-lg shadow-md overflow-hidden mb-4">
<div id="serverList">
<!-- 서버 목록이 여기에 동적으로 로드됩니다 -->
</div>
</div> </div>
<!-- 상태 표시 --> <!-- 툴바 -->
<div id="status" class="mb-6"></div> <div class="flex space-x-4 mb-4">
<button id="addServerBtn" class="bg-primary hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-lg transition duration-200 shadow-sm">
<!-- 버튼 그룹 -->
<div class="mb-6 flex space-x-4">
<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"> <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> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg> </svg>
새 서버 추가 새 서버 추가
</button> </button>
<button id="settingsBtn" class="bg-secondary hover:bg-gray-600 text-white font-bold py-2 px-4 rounded-lg transition duration-200"> <button id="settingsBtn" class="bg-secondary hover:bg-gray-600 text-white font-bold py-2 px-4 rounded-lg transition duration-200 shadow-sm">
<svg class="w-5 h-5 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
@@ -48,16 +53,6 @@
설정 설정
</button> </button>
</div> </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>
<!-- 서버 추가/편집 모달 --> <!-- 서버 추가/편집 모달 -->
@@ -79,6 +74,10 @@
<div class="mb-4"> <div class="mb-4">
<label for="serverCategory" class="block text-sm font-medium text-gray-700 mb-2">카테고리</label> <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"> <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="serverTitle" class="block text-sm font-medium text-gray-700 mb-2">제목</label>
<input type="text" id="serverTitle" 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>
<div class="mb-4"> <div class="mb-4">
<label for="serverDescription" class="block text-sm font-medium text-gray-700 mb-2">설명</label> <label for="serverDescription" class="block text-sm font-medium text-gray-700 mb-2">설명</label>
@@ -109,6 +108,11 @@
<h3 class="text-lg font-semibold text-gray-800">설정</h3> <h3 class="text-lg font-semibold text-gray-800">설정</h3>
</div> </div>
<form id="settingsForm" class="p-6"> <form id="settingsForm" class="p-6">
<div class="mb-4">
<label for="userName" class="block text-sm font-medium text-gray-700 mb-2">사용자 이름 <span class="text-red-500">*</span></label>
<input type="text" id="userName" 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" placeholder="사용자 이름을 입력하세요" required>
<p class="text-sm text-gray-500 mt-1">현재 사용자의 이름을 설정하세요. (필수)</p>
</div>
<div class="mb-4"> <div class="mb-4">
<label for="vncViewerPath" class="block text-sm font-medium text-gray-700 mb-2">VNC Viewer 경로</label> <label for="vncViewerPath" class="block text-sm font-medium text-gray-700 mb-2">VNC Viewer 경로</label>
<div class="flex space-x-2"> <div class="flex space-x-2">
@@ -117,6 +121,11 @@
</div> </div>
<p class="text-sm text-gray-500 mt-1">VNC Viewer 실행 파일의 경로를 설정하세요.</p> <p class="text-sm text-gray-500 mt-1">VNC Viewer 실행 파일의 경로를 설정하세요.</p>
</div> </div>
<div class="mb-4">
<label for="vncArgument" class="block text-sm font-medium text-gray-700 mb-2">VNC 기본 인수</label>
<input type="text" id="vncArgument" 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" placeholder="-useclipboard=no -scale=auto -showcontrols=yes">
<p class="text-sm text-gray-500 mt-1">VNC 연결 시 사용할 기본 인수를 설정하세요.</p>
</div>
<div class="mb-6"> <div class="mb-6">
<label for="webServerPort" class="block text-sm font-medium text-gray-700 mb-2">웹 서버 포트</label> <label for="webServerPort" class="block text-sm font-medium text-gray-700 mb-2">웹 서버 포트</label>
<input type="number" id="webServerPort" value="8080" 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"> <input type="number" id="webServerPort" value="8080" 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">
@@ -149,6 +158,84 @@
</div> </div>
</div> </div>
<script src="js/app.js"></script> <!-- 숨겨진 템플릿들 -->
<template id="serverItemTemplate">
<div class="server-item px-4 py-3 hover:bg-gray-50 transition duration-200 border-b border-gray-100 relative overflow-hidden group">
<div class="flex items-center justify-between">
<div class="flex-1 cursor-pointer" onclick="app.connectToServer('{{userName}}', '{{serverIP}}')">
<div class="flex items-center space-x-3">
<div class="w-3 h-3 bg-green-500 rounded-full"></div>
<h4 class="text-lg font-medium text-gray-900 hover:text-blue-600 transition duration-200">{{serverName}}</h4>
<span class="text-xs text-gray-400 opacity-0 group-hover:opacity-100 transition duration-200">클릭하여 연결</span>
</div>
<div class="mt-2 text-sm text-gray-600">
<p><span class="font-medium">IP:</span> {{serverIP}}</p>
{{#if serverCategory}}<p class="mt-1"><span class="font-medium">카테고리:</span> {{serverCategory}}</p>{{/if}}
{{#if serverDescription}}<p class="mt-1"><span class="font-medium">설명:</span> {{serverDescription}}</p>{{/if}}
{{#if serverArgument}}<p class="mt-1"><span class="font-medium">인수:</span> {{serverArgument}}</p>{{/if}}
</div>
</div>
<div class="action-buttons flex space-x-2 transform translate-x-full transition-transform duration-300 ease-in-out group-hover:translate-x-0">
<button onclick="app.connectToServer('{{userName}}', '{{serverIP}}')" class="bg-primary hover:bg-blue-700 text-white px-3 py-1 rounded text-sm transition duration-200 shadow-sm">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
</button>
<button onclick="app.editServer('{{userName}}', '{{serverIP}}')" class="bg-secondary hover:bg-gray-600 text-white px-3 py-1 rounded text-sm transition duration-200 shadow-sm">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
</svg>
</button>
<button onclick="app.deleteServer('{{userName}}', '{{serverIP}}', '{{serverName}}')" class="bg-danger hover:bg-red-700 text-white px-3 py-1 rounded text-sm transition duration-200 shadow-sm">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
</div>
</div>
</div>
</template>
<template id="categoryHeaderTemplate">
<div class="category-header px-4 py-2 {{bgClass}} border-b border-gray-200 cursor-pointer hover:bg-gray-100 transition duration-200" onclick="app.toggleCategory('{{categoryName}}', {{depth}})">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2">
{{#if hasContent}}
<svg class="w-4 h-4 text-gray-500 category-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
{{/if}}
<h3 class="font-medium text-gray-700">{{categoryName}}</h3>
<span class="text-sm text-gray-500">({{totalItems}})</span>
</div>
</div>
</div>
</template>
<template id="emptyServerListTemplate">
<div class="px-4 py-6 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>
</template>
<template id="userNameRequiredTemplate">
<div class="px-4 py-6 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="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
<p class="text-lg font-medium mb-2">사용자 설정이 필요합니다</p>
<p class="mb-4">서버 목록을 보려면 먼저 사용자 이름을 설정해주세요.</p>
<button onclick="app.showSettingsModal()" class="bg-primary hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition duration-200">
설정하기
</button>
</div>
</template>
<script src="js/app.js?v=1.0"></script>
</body> </body>
</html> </html>

View File

@@ -2,10 +2,26 @@ class VNCServerApp {
constructor() { constructor() {
this.apiBase = '/api/vncserver'; this.apiBase = '/api/vncserver';
this.init(); this.init();
// 개발용: Ctrl+R로 강제 새로고침
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'r') {
e.preventDefault();
window.location.reload(true);
}
});
} }
init() { async init() {
this.bindEvents(); this.bindEvents();
await this.loadSettings(); // 설정 먼저 로드
// 사용자 이름이 없으면 설정 모달 자동 열기
const userName = document.getElementById('userName')?.value || '';
if (!userName) {
this.showSettingsModal();
}
this.loadServerList(); this.loadServerList();
this.checkVNCStatus(); this.checkVNCStatus();
} }
@@ -55,11 +71,25 @@ class VNCServerApp {
document.getElementById('confirmOk').addEventListener('click', () => { document.getElementById('confirmOk').addEventListener('click', () => {
this.executeConfirmAction(); this.executeConfirmAction();
}); });
} }
async loadServerList() { async loadServerList() {
try { try {
const response = await fetch(`${this.apiBase}/list`); // 설정에서 사용자 이름 가져오기
const userName = document.getElementById('userName')?.value || '';
// 사용자 이름이 없으면 서버 목록을 로드하지 않고 설정 안내 메시지 표시
if (!userName) {
const serverList = document.getElementById('serverList');
serverList.innerHTML = this.renderTemplate('userNameRequiredTemplate', {});
return;
}
let url = `${this.apiBase}/list?userName=${encodeURIComponent(userName)}`;
const response = await fetch(url);
if (!response.ok) throw new Error('서버 목록을 불러올 수 없습니다.'); if (!response.ok) throw new Error('서버 목록을 불러올 수 없습니다.');
const servers = await response.json(); const servers = await response.json();
@@ -72,48 +102,66 @@ class VNCServerApp {
renderServerList(servers) { renderServerList(servers) {
const serverList = document.getElementById('serverList'); const serverList = document.getElementById('serverList');
console.log('서버 데이터:', servers); // 디버깅용
if (servers.length === 0) { if (servers.length === 0) {
serverList.innerHTML = ` serverList.innerHTML = this.renderTemplate('emptyServerListTemplate', {});
<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; return;
} }
serverList.innerHTML = servers.map(server => ` // 계층적 카테고리 구조 생성
<div class="px-6 py-4 hover:bg-gray-50 transition duration-200"> const categoryTree = {};
<div class="flex items-center justify-between"> servers.forEach(server => {
<div class="flex-1"> console.log('서버 카테고리:', server.category, '타입:', typeof server.category); // 디버깅용
<div class="flex items-center space-x-3"> console.log('서버 Category:', server.Category, '타입:', typeof server.Category); // 디버깅용
<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> // category와 Category 둘 다 확인
</div> const categoryValue = server.category || server.Category || '';
<div class="mt-2 text-sm text-gray-600">
<p><span class="font-medium">IP:</span> ${this.escapeHtml(server.ip)}</p> if (categoryValue && categoryValue.trim()) {
${server.category ? `<p class="mt-1"><span class="font-medium">카테고리:</span> ${this.escapeHtml(server.category)}</p>` : ''} console.log('처리할 카테고리 값:', categoryValue); // 디버깅용
${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>` : ''} const categoryPath = categoryValue.split('|').map(cat => cat.trim()).filter(cat => cat.length > 0);
</div> console.log('카테고리 경로:', categoryPath); // 디버깅용
</div>
<div class="flex space-x-2"> let currentLevel = categoryTree;
<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"> categoryPath.forEach((category, index) => {
연결 if (!currentLevel[category]) {
</button> currentLevel[category] = {
<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"> servers: [],
편집 subcategories: {}
</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> if (index === categoryPath.length - 1) {
</div> currentLevel[category].servers.push(server);
</div> }
`).join('');
currentLevel = currentLevel[category].subcategories;
});
} else {
// 카테고리가 없으면 '기타'로 분류
if (!categoryTree['기타']) {
categoryTree['기타'] = {
servers: [],
subcategories: {}
};
}
categoryTree['기타'].servers.push(server);
}
});
console.log('카테고리 트리:', categoryTree); // 디버깅용
let html = '';
const sortedCategories = Object.keys(categoryTree).sort();
sortedCategories.forEach(category => {
html += this.renderCategoryNode(category, categoryTree[category], 0);
});
serverList.innerHTML = html;
} }
async checkVNCStatus() { async checkVNCStatus() {
@@ -133,20 +181,9 @@ class VNCServerApp {
showVNCStatus(status) { showVNCStatus(status) {
const statusDiv = document.getElementById('status'); const statusDiv = document.getElementById('status');
if (status.isInstalled) { if (status.IsInstalled) {
statusDiv.innerHTML = ` // VNC가 설치되어 있으면 상태 메시지를 숨김
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded"> statusDiv.innerHTML = '';
<div class="flex items-center justify-between">
<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>
<button onclick="app.showSettingsModal()" class="text-green-600 hover:text-green-800 text-sm underline">설정 변경</button>
</div>
</div>
`;
} else { } else {
statusDiv.innerHTML = ` statusDiv.innerHTML = `
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded"> <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
@@ -155,7 +192,7 @@ class VNCServerApp {
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20"> <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> <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> </svg>
VNC Viewer를 찾을 수 없습니다. 경로: ${status.path} VNC Viewer를 찾을 수 없습니다. 경로: ${status.Path}
</div> </div>
<button onclick="app.showSettingsModal()" class="text-red-600 hover:text-red-800 text-sm underline">설정</button> <button onclick="app.showSettingsModal()" class="text-red-600 hover:text-red-800 text-sm underline">설정</button>
</div> </div>
@@ -174,6 +211,7 @@ class VNCServerApp {
document.getElementById('serverUser').value = server.user; document.getElementById('serverUser').value = server.user;
document.getElementById('serverIp').value = server.ip; document.getElementById('serverIp').value = server.ip;
document.getElementById('serverCategory').value = server.category || ''; document.getElementById('serverCategory').value = server.category || '';
document.getElementById('serverTitle').value = server.title || '';
document.getElementById('serverDescription').value = server.description || ''; document.getElementById('serverDescription').value = server.description || '';
document.getElementById('serverPassword').value = server.password || ''; document.getElementById('serverPassword').value = server.password || '';
document.getElementById('serverArgument').value = server.argument || ''; document.getElementById('serverArgument').value = server.argument || '';
@@ -211,17 +249,23 @@ class VNCServerApp {
const settings = await response.json(); const settings = await response.json();
console.log('로드된 설정:', settings); // 디버깅용 console.log('로드된 설정:', settings); // 디버깅용
console.log('VNC 경로:', settings.vncViewerPath); // 디버깅용 console.log('VNC 경로:', settings.VNCViewerPath); // 디버깅용
console.log('웹 포트:', settings.webServerPort); // 디버깅용 console.log('웹 포트:', settings.WebServerPort); // 디버깅용
const userNameElement = document.getElementById('userName');
const vncPathElement = document.getElementById('vncViewerPath'); const vncPathElement = document.getElementById('vncViewerPath');
const vncArgumentElement = document.getElementById('vncArgument');
const webPortElement = document.getElementById('webServerPort'); const webPortElement = document.getElementById('webServerPort');
console.log('사용자 이름 요소:', userNameElement); // 디버깅용
console.log('VNC 경로 요소:', vncPathElement); // 디버깅용 console.log('VNC 경로 요소:', vncPathElement); // 디버깅용
console.log('VNC 인수 요소:', vncArgumentElement); // 디버깅용
console.log('웹 포트 요소:', webPortElement); // 디버깅용 console.log('웹 포트 요소:', webPortElement); // 디버깅용
vncPathElement.value = settings.vncViewerPath || ''; userNameElement.value = settings.UserName || '';
webPortElement.value = settings.webServerPort || 8080; vncPathElement.value = settings.VNCViewerPath || '';
vncArgumentElement.value = settings.Argument || '';
webPortElement.value = settings.WebServerPort || 8080;
console.log('설정 로드 완료'); // 디버깅용 console.log('설정 로드 완료'); // 디버깅용
console.log('설정된 VNC 경로 값:', vncPathElement.value); // 디버깅용 console.log('설정된 VNC 경로 값:', vncPathElement.value); // 디버깅용
@@ -233,12 +277,23 @@ class VNCServerApp {
} }
async saveSettings() { async saveSettings() {
const userName = document.getElementById('userName').value.trim();
const vncPath = document.getElementById('vncViewerPath').value; const vncPath = document.getElementById('vncViewerPath').value;
const vncArgument = document.getElementById('vncArgument').value;
const webPort = parseInt(document.getElementById('webServerPort').value); const webPort = parseInt(document.getElementById('webServerPort').value);
// 사용자 이름 필수 검증
if (!userName) {
this.showError('사용자 이름을 입력해주세요.');
document.getElementById('userName').focus();
return;
}
const settings = { const settings = {
vncViewerPath: vncPath, UserName: userName,
webServerPort: webPort VNCViewerPath: vncPath,
Argument: vncArgument,
WebServerPort: webPort
}; };
console.log('저장할 설정:', settings); // 디버깅용 console.log('저장할 설정:', settings); // 디버깅용
@@ -266,6 +321,7 @@ class VNCServerApp {
this.showSuccess(result.message); this.showSuccess(result.message);
this.hideSettingsModal(); this.hideSettingsModal();
this.checkVNCStatus(); // VNC 상태 다시 확인 this.checkVNCStatus(); // VNC 상태 다시 확인
this.loadServerList(); // 서버 목록 다시 로드 (사용자 필터 적용)
} catch (error) { } catch (error) {
console.error('설정 저장 오류:', error); // 디버깅용 console.error('설정 저장 오류:', error); // 디버깅용
this.showError('설정 저장 중 오류가 발생했습니다: ' + error.message); this.showError('설정 저장 중 오류가 발생했습니다: ' + error.message);
@@ -296,6 +352,7 @@ class VNCServerApp {
user: serverUser, user: serverUser,
ip: serverIp, ip: serverIp,
category: document.getElementById('serverCategory').value, category: document.getElementById('serverCategory').value,
title: document.getElementById('serverTitle').value,
description: document.getElementById('serverDescription').value, description: document.getElementById('serverDescription').value,
password: document.getElementById('serverPassword').value, password: document.getElementById('serverPassword').value,
argument: document.getElementById('serverArgument').value argument: document.getElementById('serverArgument').value
@@ -428,6 +485,95 @@ class VNCServerApp {
div.textContent = text; div.textContent = text;
return div.innerHTML; return div.innerHTML;
} }
// 간단한 템플릿 엔진
renderTemplate(templateId, data) {
const template = document.getElementById(templateId);
if (!template) {
console.error(`Template not found: ${templateId}`);
return '';
}
let html = template.innerHTML;
// 변수 치환
Object.keys(data).forEach(key => {
const regex = new RegExp(`{{${key}}}`, 'g');
html = html.replace(regex, this.escapeHtml(data[key] || ''));
});
// 조건부 렌더링 ({{#if variable}}...{{/if}})
html = html.replace(/\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (match, variable, content) => {
return data[variable] ? content : '';
});
return html;
}
renderCategoryNode(categoryName, categoryData, depth) {
const indent = depth * 20; // 들여쓰기
const hasSubcategories = Object.keys(categoryData.subcategories).length > 0;
const hasServers = categoryData.servers.length > 0;
const totalItems = categoryData.servers.length + Object.keys(categoryData.subcategories).length;
const headerData = {
categoryName: categoryName,
depth: depth,
bgClass: depth === 0 ? 'bg-gray-50' : 'bg-gray-25',
hasContent: hasSubcategories || hasServers,
totalItems: totalItems
};
let html = `
<div class="category-group" style="margin-left: ${indent}px;">
${this.renderTemplate('categoryHeaderTemplate', headerData)}
<div class="category-content" id="category-${this.escapeHtml(categoryName)}-${depth}">
`;
// 서버 목록 렌더링
if (hasServers) {
categoryData.servers.forEach(server => {
const serverData = {
serverName: `${server.User || server.user}@${server.Title || server.title}`,
userName: server.User || server.user,
serverIP: server.IP || server.ip,
serverCategory: server.Category || server.category,
serverTitle: server.Title || server.title,
serverDescription: server.Description || server.description,
serverArgument: server.Argument || server.argument
};
html += this.renderTemplate('serverItemTemplate', serverData);
});
}
// 하위 카테고리 렌더링
const sortedSubcategories = Object.keys(categoryData.subcategories).sort();
sortedSubcategories.forEach(subcategoryName => {
html += this.renderCategoryNode(subcategoryName, categoryData.subcategories[subcategoryName], depth + 1);
});
html += `
</div>
</div>
`;
return html;
}
toggleCategory(category, depth = 0) {
const content = document.getElementById(`category-${category}-${depth}`);
const icon = content.previousElementSibling.querySelector('.category-icon');
if (content.style.display === 'none') {
content.style.display = 'block';
icon.style.transform = 'rotate(0deg)';
} else {
content.style.display = 'none';
icon.style.transform = 'rotate(-90deg)';
}
}
} }
// 앱 초기화 // 앱 초기화