diff --git a/Form1.Designer.cs b/Form1.Designer.cs index 7b898f5..a021304 100644 --- a/Form1.Designer.cs +++ b/Form1.Designer.cs @@ -34,7 +34,7 @@ // this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F); 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.Text = "Form1"; this.ResumeLayout(false); diff --git a/Form1.cs b/Form1.cs index 19af638..3b69377 100644 --- a/Form1.cs +++ b/Form1.cs @@ -15,6 +15,7 @@ namespace VNCServerList public Form1() { InitializeComponent(); + this.Text = $"{Application.ProductName} ver {Application.ProductVersion}"; StartWebServer(); InitializeWebView(); diff --git a/Models/AppSettings.cs b/Models/AppSettings.cs index 206bb4b..5219a99 100644 --- a/Models/AppSettings.cs +++ b/Models/AppSettings.cs @@ -4,7 +4,9 @@ namespace VNCServerList.Models { public class AppSettings { + public string UserName { get; set; } = ""; public string VNCViewerPath { get; set; } = @"C:\Program Files\TightVNC\tvnviewer.exe"; public int WebServerPort { get; set; } = 8080; + public string Argument { get; set; } = "-useclipboard=no -scale=auto -showcontrols=yes"; } } \ No newline at end of file diff --git a/Models/VNCServer.cs b/Models/VNCServer.cs index b1720e5..d3866a8 100644 --- a/Models/VNCServer.cs +++ b/Models/VNCServer.cs @@ -7,6 +7,7 @@ namespace VNCServerList.Models public string User { get; set; } public string IP { get; set; } public string Category { get; set; } + public string Title { get; set; } public string Description { get; set; } public string Password { get; set; } public string Argument { get; set; } diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index e2e29f4..d8ac043 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -8,9 +8,9 @@ using System.Runtime.InteropServices; [assembly: AssemblyTitle("VNCServerList")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] +[assembly: AssemblyCompany("SIMP")] [assembly: AssemblyProduct("VNCServerList")] -[assembly: AssemblyCopyright("Copyright © 2025")] +[assembly: AssemblyCopyright("Copyright ©SIMP 2025")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -32,5 +32,5 @@ using System.Runtime.InteropServices; // 모든 값을 지정하거나 아래와 같이 '*'를 사용하여 빌드 번호 및 수정 번호가 자동으로 // 지정되도록 할 수 있습니다. // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyVersion("25.07.08.0000")] +[assembly: AssemblyFileVersion("25.07.08.0000")] diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs index 05bcbfa..dc02faf 100644 --- a/Services/DatabaseService.cs +++ b/Services/DatabaseService.cs @@ -32,6 +32,7 @@ namespace VNCServerList.Services [User] [varchar](50) NOT NULL, [IP] [varchar](20) NOT NULL, [Category] [varchar](20) NULL, + [Title] [varchar](50) NULL, [Description] [varchar](100) NULL, [Password] [varchar](20) NULL, [Argument] [varchar](50) NULL, @@ -68,6 +69,7 @@ namespace VNCServerList.Services User = reader["User"].ToString(), IP = reader["IP"].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(), Password = reader["Password"] == DBNull.Value ? null : reader["Password"].ToString(), Argument = reader["Argument"] == DBNull.Value ? null : reader["Argument"].ToString() @@ -100,6 +102,7 @@ namespace VNCServerList.Services User = reader["User"].ToString(), IP = reader["IP"].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(), Password = reader["Password"] == DBNull.Value ? null : reader["Password"].ToString(), Argument = reader["Argument"] == DBNull.Value ? null : reader["Argument"].ToString() @@ -118,7 +121,7 @@ namespace VNCServerList.Services { connection.Open(); 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)"; using (var command = new SqlCommand(sql, connection)) @@ -126,6 +129,7 @@ namespace VNCServerList.Services command.Parameters.AddWithValue("@User", server.User); command.Parameters.AddWithValue("@IP", server.IP); 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("@Password", (object)server.Password ?? DBNull.Value); command.Parameters.AddWithValue("@Argument", (object)server.Argument ?? DBNull.Value); @@ -142,7 +146,7 @@ namespace VNCServerList.Services connection.Open(); string sql = @" UPDATE VNC_ServerList - SET [Category] = @Category, [Description] = @Description, + SET [Category] = @Category, [Title] = @Title, [Description] = @Description, [Password] = @Password, [Argument] = @Argument WHERE [User] = @User AND [IP] = @IP"; @@ -151,6 +155,7 @@ namespace VNCServerList.Services command.Parameters.AddWithValue("@User", server.User); command.Parameters.AddWithValue("@IP", server.IP); 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("@Password", (object)server.Password ?? DBNull.Value); command.Parameters.AddWithValue("@Argument", (object)server.Argument ?? DBNull.Value); diff --git a/Services/VNCService.cs b/Services/VNCService.cs index 530969a..0420d5d 100644 --- a/Services/VNCService.cs +++ b/Services/VNCService.cs @@ -20,25 +20,44 @@ namespace VNCServerList.Services { try { - var vncViewerPath = _settingsService.GetSettings().VNCViewerPath; + var settings = _settingsService.GetSettings(); + var vncViewerPath = settings.VNCViewerPath; if (!File.Exists(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 실행 var startInfo = new ProcessStartInfo { FileName = vncViewerPath, - Arguments = $"-host={server.IP} {server.Argument}", + Arguments = arguments, UseShellExecute = true }; Process.Start(startInfo); - // 연결 성공 (마지막 연결 시간 업데이트는 현재 테이블 구조에 없으므로 제거) - return true; } catch (Exception ex) diff --git a/Web/Controllers/VNCServerController.cs b/Web/Controllers/VNCServerController.cs index 691a380..26afc55 100644 --- a/Web/Controllers/VNCServerController.cs +++ b/Web/Controllers/VNCServerController.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Web.Http; +using System.Linq; using VNCServerList.Models; using VNCServerList.Services; @@ -22,12 +23,21 @@ namespace VNCServerList.Web.Controllers [HttpGet] [Route("list")] - public IHttpActionResult GetServerList() + public IHttpActionResult GetServerList(string userName = null) { try { 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) { diff --git a/Web/Startup.cs b/Web/Startup.cs index 39eb8c1..bc64ab8 100644 --- a/Web/Startup.cs +++ b/Web/Startup.cs @@ -36,6 +36,18 @@ namespace VNCServerList.Web }; 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(); + }); } } } \ No newline at end of file diff --git a/Web/wwwroot/index.html b/Web/wwwroot/index.html index 420e25c..ed168c6 100644 --- a/Web/wwwroot/index.html +++ b/Web/wwwroot/index.html @@ -3,6 +3,9 @@ + + + VNC 서버 목록 관리 -
- -
-

VNC 서버 목록 관리

-

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

+
+ + +
+ + +
+
+ +
- -
- - -
- -
- - -
-
-

서버 목록

-
-
- -
-
@@ -79,6 +74,10 @@
+
+
+ +
@@ -109,6 +108,11 @@

설정

+
+ + +

현재 사용자의 이름을 설정하세요. (필수)

+
@@ -117,6 +121,11 @@

VNC Viewer 실행 파일의 경로를 설정하세요.

+
+ + +

VNC 연결 시 사용할 기본 인수를 설정하세요.

+
@@ -149,6 +158,84 @@
- + + + + + + + + + + \ No newline at end of file diff --git a/Web/wwwroot/js/app.js b/Web/wwwroot/js/app.js index 2b03a5f..ea32ff9 100644 --- a/Web/wwwroot/js/app.js +++ b/Web/wwwroot/js/app.js @@ -2,10 +2,26 @@ class VNCServerApp { constructor() { this.apiBase = '/api/vncserver'; 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(); + await this.loadSettings(); // 설정 먼저 로드 + + // 사용자 이름이 없으면 설정 모달 자동 열기 + const userName = document.getElementById('userName')?.value || ''; + if (!userName) { + this.showSettingsModal(); + } + this.loadServerList(); this.checkVNCStatus(); } @@ -55,11 +71,25 @@ class VNCServerApp { document.getElementById('confirmOk').addEventListener('click', () => { this.executeConfirmAction(); }); + + } async loadServerList() { 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('서버 목록을 불러올 수 없습니다.'); const servers = await response.json(); @@ -72,48 +102,66 @@ class VNCServerApp { renderServerList(servers) { const serverList = document.getElementById('serverList'); + console.log('서버 데이터:', servers); // 디버깅용 + if (servers.length === 0) { - serverList.innerHTML = ` -
- - - -

등록된 서버가 없습니다.

-

새 서버를 추가해보세요.

-
- `; + serverList.innerHTML = this.renderTemplate('emptyServerListTemplate', {}); 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(''); + // 계층적 카테고리 구조 생성 + const categoryTree = {}; + servers.forEach(server => { + console.log('서버 카테고리:', server.category, '타입:', typeof server.category); // 디버깅용 + console.log('서버 Category:', server.Category, '타입:', typeof server.Category); // 디버깅용 + + // category와 Category 둘 다 확인 + const categoryValue = server.category || server.Category || ''; + + if (categoryValue && categoryValue.trim()) { + console.log('처리할 카테고리 값:', categoryValue); // 디버깅용 + // | 기호로 구분된 카테고리들을 분리하여 계층 구조 생성 + const categoryPath = categoryValue.split('|').map(cat => cat.trim()).filter(cat => cat.length > 0); + console.log('카테고리 경로:', categoryPath); // 디버깅용 + + let currentLevel = categoryTree; + categoryPath.forEach((category, index) => { + if (!currentLevel[category]) { + currentLevel[category] = { + servers: [], + subcategories: {} + }; + } + + // 마지막 레벨에만 서버 추가 + if (index === categoryPath.length - 1) { + currentLevel[category].servers.push(server); + } + + 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() { @@ -133,20 +181,9 @@ class VNCServerApp { showVNCStatus(status) { const statusDiv = document.getElementById('status'); - if (status.isInstalled) { - statusDiv.innerHTML = ` -
-
-
- - - - VNC Viewer가 설치되어 있습니다. -
- -
-
- `; + if (status.IsInstalled) { + // VNC가 설치되어 있으면 상태 메시지를 숨김 + statusDiv.innerHTML = ''; } else { statusDiv.innerHTML = `
@@ -155,7 +192,7 @@ class VNCServerApp { - VNC Viewer를 찾을 수 없습니다. 경로: ${status.path} + VNC Viewer를 찾을 수 없습니다. 경로: ${status.Path}
@@ -174,6 +211,7 @@ class VNCServerApp { document.getElementById('serverUser').value = server.user; document.getElementById('serverIp').value = server.ip; document.getElementById('serverCategory').value = server.category || ''; + document.getElementById('serverTitle').value = server.title || ''; document.getElementById('serverDescription').value = server.description || ''; document.getElementById('serverPassword').value = server.password || ''; document.getElementById('serverArgument').value = server.argument || ''; @@ -211,17 +249,23 @@ class VNCServerApp { const settings = await response.json(); console.log('로드된 설정:', settings); // 디버깅용 - console.log('VNC 경로:', settings.vncViewerPath); // 디버깅용 - console.log('웹 포트:', settings.webServerPort); // 디버깅용 + console.log('VNC 경로:', settings.VNCViewerPath); // 디버깅용 + console.log('웹 포트:', settings.WebServerPort); // 디버깅용 + const userNameElement = document.getElementById('userName'); const vncPathElement = document.getElementById('vncViewerPath'); + const vncArgumentElement = document.getElementById('vncArgument'); const webPortElement = document.getElementById('webServerPort'); + console.log('사용자 이름 요소:', userNameElement); // 디버깅용 console.log('VNC 경로 요소:', vncPathElement); // 디버깅용 + console.log('VNC 인수 요소:', vncArgumentElement); // 디버깅용 console.log('웹 포트 요소:', webPortElement); // 디버깅용 - vncPathElement.value = settings.vncViewerPath || ''; - webPortElement.value = settings.webServerPort || 8080; + userNameElement.value = settings.UserName || ''; + vncPathElement.value = settings.VNCViewerPath || ''; + vncArgumentElement.value = settings.Argument || ''; + webPortElement.value = settings.WebServerPort || 8080; console.log('설정 로드 완료'); // 디버깅용 console.log('설정된 VNC 경로 값:', vncPathElement.value); // 디버깅용 @@ -233,12 +277,23 @@ class VNCServerApp { } async saveSettings() { + const userName = document.getElementById('userName').value.trim(); const vncPath = document.getElementById('vncViewerPath').value; + const vncArgument = document.getElementById('vncArgument').value; const webPort = parseInt(document.getElementById('webServerPort').value); + // 사용자 이름 필수 검증 + if (!userName) { + this.showError('사용자 이름을 입력해주세요.'); + document.getElementById('userName').focus(); + return; + } + const settings = { - vncViewerPath: vncPath, - webServerPort: webPort + UserName: userName, + VNCViewerPath: vncPath, + Argument: vncArgument, + WebServerPort: webPort }; console.log('저장할 설정:', settings); // 디버깅용 @@ -266,6 +321,7 @@ class VNCServerApp { this.showSuccess(result.message); this.hideSettingsModal(); this.checkVNCStatus(); // VNC 상태 다시 확인 + this.loadServerList(); // 서버 목록 다시 로드 (사용자 필터 적용) } catch (error) { console.error('설정 저장 오류:', error); // 디버깅용 this.showError('설정 저장 중 오류가 발생했습니다: ' + error.message); @@ -296,6 +352,7 @@ class VNCServerApp { user: serverUser, ip: serverIp, category: document.getElementById('serverCategory').value, + title: document.getElementById('serverTitle').value, description: document.getElementById('serverDescription').value, password: document.getElementById('serverPassword').value, argument: document.getElementById('serverArgument').value @@ -428,6 +485,95 @@ class VNCServerApp { div.textContent = text; 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 = ` +
+ ${this.renderTemplate('categoryHeaderTemplate', headerData)} +
+ `; + + // 서버 목록 렌더링 + 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 += ` +
+
+ `; + + 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)'; + } + } + + } // 앱 초기화