add icon(128*128)
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,3 +7,4 @@ desktop.ini
|
|||||||
.vs
|
.vs
|
||||||
packages
|
packages
|
||||||
*.zip
|
*.zip
|
||||||
|
deploy
|
||||||
|
|||||||
@@ -11,5 +11,7 @@ namespace VNCServerList.Models
|
|||||||
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; }
|
||||||
|
public byte[] Icon { get; set; }
|
||||||
|
public string IconBase64 { get; set; } // Base64 인코딩된 아이콘 데이터
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,6 +36,7 @@ namespace VNCServerList.Services
|
|||||||
[Description] [varchar](100) NULL,
|
[Description] [varchar](100) NULL,
|
||||||
[Password] [varchar](20) NULL,
|
[Password] [varchar](20) NULL,
|
||||||
[Argument] [varchar](50) NULL,
|
[Argument] [varchar](50) NULL,
|
||||||
|
[Icon] [image] NULL,
|
||||||
CONSTRAINT [PK_VNC_ServerList] PRIMARY KEY CLUSTERED
|
CONSTRAINT [PK_VNC_ServerList] PRIMARY KEY CLUSTERED
|
||||||
(
|
(
|
||||||
[User] ASC,
|
[User] ASC,
|
||||||
@@ -47,6 +48,16 @@ namespace VNCServerList.Services
|
|||||||
{
|
{
|
||||||
command.ExecuteNonQuery();
|
command.ExecuteNonQuery();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Icon 컬럼이 존재하는지 확인하고 없으면 추가
|
||||||
|
string checkIconColumnSql = @"
|
||||||
|
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[VNC_ServerList]') AND name = 'Icon')
|
||||||
|
ALTER TABLE [dbo].[VNC_ServerList] ADD [Icon] [image] NULL";
|
||||||
|
|
||||||
|
using (var command = new SqlCommand(checkIconColumnSql, connection))
|
||||||
|
{
|
||||||
|
command.ExecuteNonQuery();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +83,8 @@ namespace VNCServerList.Services
|
|||||||
Title = reader["Title"] == DBNull.Value ? null : reader["Title"].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(),
|
||||||
|
Icon = reader["Icon"] == DBNull.Value ? null : (byte[])reader["Icon"]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,7 +117,8 @@ namespace VNCServerList.Services
|
|||||||
Title = reader["Title"] == DBNull.Value ? null : reader["Title"].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(),
|
||||||
|
Icon = reader["Icon"] == DBNull.Value ? null : (byte[])reader["Icon"]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,8 +134,8 @@ namespace VNCServerList.Services
|
|||||||
{
|
{
|
||||||
connection.Open();
|
connection.Open();
|
||||||
string sql = @"
|
string sql = @"
|
||||||
INSERT INTO VNC_ServerList ([User], [IP], [Category], [Title],[Description], [Password], [Argument])
|
INSERT INTO VNC_ServerList ([User], [IP], [Category], [Title],[Description], [Password], [Argument], [Icon])
|
||||||
VALUES (@User, @IP, @Category, @Title,@Description, @Password, @Argument)";
|
VALUES (@User, @IP, @Category, @Title,@Description, @Password, @Argument, @Icon)";
|
||||||
|
|
||||||
using (var command = new SqlCommand(sql, connection))
|
using (var command = new SqlCommand(sql, connection))
|
||||||
{
|
{
|
||||||
@@ -133,6 +146,7 @@ namespace VNCServerList.Services
|
|||||||
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);
|
||||||
|
command.Parameters.AddWithValue("@Icon", (object)server.Icon ?? DBNull.Value);
|
||||||
|
|
||||||
return command.ExecuteNonQuery() > 0;
|
return command.ExecuteNonQuery() > 0;
|
||||||
}
|
}
|
||||||
@@ -147,7 +161,7 @@ namespace VNCServerList.Services
|
|||||||
string sql = @"
|
string sql = @"
|
||||||
UPDATE VNC_ServerList
|
UPDATE VNC_ServerList
|
||||||
SET [Category] = @Category, [Title] = @Title, [Description] = @Description,
|
SET [Category] = @Category, [Title] = @Title, [Description] = @Description,
|
||||||
[Password] = @Password, [Argument] = @Argument
|
[Password] = @Password, [Argument] = @Argument, [Icon] = @Icon
|
||||||
WHERE [User] = @User AND [IP] = @IP";
|
WHERE [User] = @User AND [IP] = @IP";
|
||||||
|
|
||||||
using (var command = new SqlCommand(sql, connection))
|
using (var command = new SqlCommand(sql, connection))
|
||||||
@@ -159,6 +173,7 @@ namespace VNCServerList.Services
|
|||||||
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);
|
||||||
|
command.Parameters.AddWithValue("@Icon", (object)server.Icon ?? DBNull.Value);
|
||||||
|
|
||||||
return command.ExecuteNonQuery() > 0;
|
return command.ExecuteNonQuery() > 0;
|
||||||
}
|
}
|
||||||
@@ -173,7 +188,7 @@ namespace VNCServerList.Services
|
|||||||
string sql = @"
|
string sql = @"
|
||||||
UPDATE VNC_ServerList
|
UPDATE VNC_ServerList
|
||||||
SET [IP] = @NewIP, [Category] = @Category, [Title] = @Title, [Description] = @Description,
|
SET [IP] = @NewIP, [Category] = @Category, [Title] = @Title, [Description] = @Description,
|
||||||
[Password] = @Password, [Argument] = @Argument
|
[Password] = @Password, [Argument] = @Argument, [Icon] = @Icon
|
||||||
WHERE [User] = @User AND [IP] = @OriginalIP";
|
WHERE [User] = @User AND [IP] = @OriginalIP";
|
||||||
|
|
||||||
using (var command = new SqlCommand(sql, connection))
|
using (var command = new SqlCommand(sql, connection))
|
||||||
@@ -186,6 +201,7 @@ namespace VNCServerList.Services
|
|||||||
command.Parameters.AddWithValue("@Description", (object)newServerData.Description ?? DBNull.Value);
|
command.Parameters.AddWithValue("@Description", (object)newServerData.Description ?? DBNull.Value);
|
||||||
command.Parameters.AddWithValue("@Password", (object)newServerData.Password ?? DBNull.Value);
|
command.Parameters.AddWithValue("@Password", (object)newServerData.Password ?? DBNull.Value);
|
||||||
command.Parameters.AddWithValue("@Argument", (object)newServerData.Argument ?? DBNull.Value);
|
command.Parameters.AddWithValue("@Argument", (object)newServerData.Argument ?? DBNull.Value);
|
||||||
|
command.Parameters.AddWithValue("@Icon", (object)newServerData.Icon ?? DBNull.Value);
|
||||||
|
|
||||||
return command.ExecuteNonQuery() > 0;
|
return command.ExecuteNonQuery() > 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Web.Http;
|
using System.Web.Http;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net;
|
||||||
|
using System.IO;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Drawing.Drawing2D;
|
||||||
|
using System.Drawing.Imaging;
|
||||||
using VNCServerList.Models;
|
using VNCServerList.Models;
|
||||||
using VNCServerList.Services;
|
using VNCServerList.Services;
|
||||||
|
|
||||||
@@ -75,6 +81,20 @@ namespace VNCServerList.Web.Controllers
|
|||||||
return BadRequest("서버 정보가 없습니다.");
|
return BadRequest("서버 정보가 없습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 아이콘이 Base64로 전송된 경우 처리
|
||||||
|
if (!string.IsNullOrEmpty(server.IconBase64))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var iconData = Convert.FromBase64String(server.IconBase64);
|
||||||
|
server.Icon = ResizeImage(iconData, 128, 128);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return BadRequest("아이콘 데이터 형식이 올바르지 않습니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool success = _databaseService.AddServer(server);
|
bool success = _databaseService.AddServer(server);
|
||||||
if (success)
|
if (success)
|
||||||
{
|
{
|
||||||
@@ -102,6 +122,20 @@ namespace VNCServerList.Web.Controllers
|
|||||||
return BadRequest("서버 정보가 없습니다.");
|
return BadRequest("서버 정보가 없습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 아이콘이 Base64로 전송된 경우 처리
|
||||||
|
if (!string.IsNullOrEmpty(server.IconBase64))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var iconData = Convert.FromBase64String(server.IconBase64);
|
||||||
|
server.Icon = ResizeImage(iconData, 128, 128);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return BadRequest("아이콘 데이터 형식이 올바르지 않습니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 원본 사용자명과 IP로 서버를 찾아서 업데이트
|
// 원본 사용자명과 IP로 서버를 찾아서 업데이트
|
||||||
bool success = _databaseService.UpdateServerByUserAndIP(originalUser, originalIp, server);
|
bool success = _databaseService.UpdateServerByUserAndIP(originalUser, originalIp, server);
|
||||||
if (success)
|
if (success)
|
||||||
@@ -264,5 +298,154 @@ namespace VNCServerList.Web.Controllers
|
|||||||
return InternalServerError(ex);
|
return InternalServerError(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("icon/{user}/{ip}")]
|
||||||
|
public HttpResponseMessage GetServerIcon(string user, string ip)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var server = _databaseService.GetServerByUserAndIP(user, ip);
|
||||||
|
if (server?.Icon == null || server.Icon.Length == 0)
|
||||||
|
{
|
||||||
|
return Request.CreateResponse(HttpStatusCode.NotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = Request.CreateResponse(HttpStatusCode.OK);
|
||||||
|
response.Content = new ByteArrayContent(server.Icon);
|
||||||
|
response.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("image/png");
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Request.CreateResponse(HttpStatusCode.InternalServerError, ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] ResizeImage(byte[] imageData, int maxWidth = 128, int maxHeight = 128)
|
||||||
|
{
|
||||||
|
using (var originalStream = new MemoryStream(imageData))
|
||||||
|
using (var originalImage = Image.FromStream(originalStream))
|
||||||
|
{
|
||||||
|
// 원본 이미지 크기
|
||||||
|
int originalWidth = originalImage.Width;
|
||||||
|
int originalHeight = originalImage.Height;
|
||||||
|
|
||||||
|
// 리사이즈가 필요한지 확인
|
||||||
|
if (originalWidth <= maxWidth && originalHeight <= maxHeight)
|
||||||
|
{
|
||||||
|
return imageData; // 리사이즈 불필요
|
||||||
|
}
|
||||||
|
|
||||||
|
// 새로운 크기 계산 (비율 유지)
|
||||||
|
double ratioX = (double)maxWidth / originalWidth;
|
||||||
|
double ratioY = (double)maxHeight / originalHeight;
|
||||||
|
double ratio = Math.Min(ratioX, ratioY);
|
||||||
|
|
||||||
|
int newWidth = (int)(originalWidth * ratio);
|
||||||
|
int newHeight = (int)(originalHeight * ratio);
|
||||||
|
|
||||||
|
// 새 이미지 생성
|
||||||
|
using (var resizedImage = new Bitmap(newWidth, newHeight))
|
||||||
|
using (var graphics = Graphics.FromImage(resizedImage))
|
||||||
|
{
|
||||||
|
// 고품질 리사이즈 설정
|
||||||
|
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||||||
|
graphics.SmoothingMode = SmoothingMode.HighQuality;
|
||||||
|
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
||||||
|
|
||||||
|
// 이미지 그리기
|
||||||
|
graphics.DrawImage(originalImage, 0, 0, newWidth, newHeight);
|
||||||
|
|
||||||
|
// PNG로 변환하여 반환
|
||||||
|
using (var outputStream = new MemoryStream())
|
||||||
|
{
|
||||||
|
resizedImage.Save(outputStream, ImageFormat.Png);
|
||||||
|
return outputStream.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("icon/{user}/{ip}")]
|
||||||
|
public IHttpActionResult UploadServerIcon(string user, string ip)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var httpRequest = System.Web.HttpContext.Current.Request;
|
||||||
|
if (httpRequest.Files.Count == 0)
|
||||||
|
{
|
||||||
|
return BadRequest("업로드된 파일이 없습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var file = httpRequest.Files[0];
|
||||||
|
if (file.ContentLength > 1024 * 1024) // 1MB 제한
|
||||||
|
{
|
||||||
|
return BadRequest("파일 크기는 1MB를 초과할 수 없습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var memoryStream = new MemoryStream())
|
||||||
|
{
|
||||||
|
file.InputStream.CopyTo(memoryStream);
|
||||||
|
var originalIconData = memoryStream.ToArray();
|
||||||
|
|
||||||
|
// 이미지 리사이즈 (128x128로 제한)
|
||||||
|
var resizedIconData = ResizeImage(originalIconData, 128, 128);
|
||||||
|
|
||||||
|
var server = _databaseService.GetServerByUserAndIP(user, ip);
|
||||||
|
if (server == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
server.Icon = resizedIconData;
|
||||||
|
bool success = _databaseService.UpdateServer(server);
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
return Ok(new { Message = "아이콘이 성공적으로 업로드되었습니다." });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return BadRequest("아이콘 업로드에 실패했습니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return InternalServerError(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete]
|
||||||
|
[Route("icon/{user}/{ip}")]
|
||||||
|
public IHttpActionResult DeleteServerIcon(string user, string ip)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var server = _databaseService.GetServerByUserAndIP(user, ip);
|
||||||
|
if (server == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
server.Icon = null;
|
||||||
|
bool success = _databaseService.UpdateServer(server);
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
return Ok(new { Message = "아이콘이 성공적으로 삭제되었습니다." });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return BadRequest("아이콘 삭제에 실패했습니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return InternalServerError(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,6 +25,9 @@ namespace VNCServerList.Web
|
|||||||
// JSON 포맷터 설정
|
// JSON 포맷터 설정
|
||||||
config.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
|
config.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
|
||||||
|
|
||||||
|
// 파일 업로드 설정
|
||||||
|
config.Formatters.Remove(config.Formatters.XmlFormatter);
|
||||||
|
|
||||||
app.UseWebApi(config);
|
app.UseWebApi(config);
|
||||||
|
|
||||||
// 정적 파일 서빙 설정
|
// 정적 파일 서빙 설정
|
||||||
|
|||||||
@@ -63,6 +63,29 @@
|
|||||||
<h3 id="modalTitle" class="text-lg font-semibold text-gray-800">서버 추가</h3>
|
<h3 id="modalTitle" class="text-lg font-semibold text-gray-800">서버 추가</h3>
|
||||||
</div>
|
</div>
|
||||||
<form id="serverForm" class="p-6">
|
<form id="serverForm" class="p-6">
|
||||||
|
<!-- 아이콘 업로드 섹션 -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">서버 아이콘</label>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<div class="w-16 h-16 border-2 border-dashed border-gray-300 rounded-lg flex items-center justify-center bg-gray-50">
|
||||||
|
<img id="iconPreview" src="" alt="" class="w-12 h-12 object-contain hidden">
|
||||||
|
<svg id="iconPlaceholder" class="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<input type="file" id="iconFile" accept="image/*" class="hidden">
|
||||||
|
<button type="button" id="selectIconBtn" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
||||||
|
아이콘 선택
|
||||||
|
</button>
|
||||||
|
<button type="button" id="removeIconBtn" class="w-full mt-1 px-3 py-1 text-xs text-red-600 hover:text-red-800 focus:outline-none hidden">
|
||||||
|
아이콘 제거
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">PNG, JPG, GIF 형식 지원 (최대 1MB, 128x128 자동 리사이즈)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4 mb-4">
|
<div class="grid grid-cols-2 gap-4 mb-4">
|
||||||
<div>
|
<div>
|
||||||
<label for="serverIp" class="block text-sm font-medium text-gray-700 mb-2">IP 주소</label>
|
<label for="serverIp" class="block text-sm font-medium text-gray-700 mb-2">IP 주소</label>
|
||||||
@@ -167,6 +190,15 @@
|
|||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex-1 cursor-pointer" onclick="app.connectToServer('{{userName}}', '{{serverIP}}')">
|
<div class="flex-1 cursor-pointer" onclick="app.connectToServer('{{userName}}', '{{serverIP}}')">
|
||||||
<div class="flex items-center space-x-3">
|
<div class="flex items-center space-x-3">
|
||||||
|
<!-- 서버 아이콘 -->
|
||||||
|
<div class="w-8 h-8 flex-shrink-0">
|
||||||
|
<img id="icon-{{userName}}-{{serverIP}}" src="{{iconUrl}}" alt="" class="w-8 h-8 object-contain rounded" onerror="this.style.display='none'; this.nextElementSibling.style.display='block';">
|
||||||
|
<div class="w-8 h-8 bg-gray-200 rounded flex items-center justify-center" style="display: none;">
|
||||||
|
<svg class="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9v-9m0-9v9"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="w-3 h-3 bg-green-500 rounded-full"></div>
|
<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>
|
<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>
|
<span class="text-xs text-gray-400 opacity-0 group-hover:opacity-100 transition duration-200">클릭하여 연결</span>
|
||||||
|
|||||||
@@ -72,7 +72,18 @@ class VNCServerApp {
|
|||||||
this.executeConfirmAction();
|
this.executeConfirmAction();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 아이콘 관련 이벤트
|
||||||
|
document.getElementById('selectIconBtn').addEventListener('click', () => {
|
||||||
|
document.getElementById('iconFile').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('iconFile').addEventListener('change', (e) => {
|
||||||
|
this.handleIconFileSelect(e);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('removeIconBtn').addEventListener('click', () => {
|
||||||
|
this.removeIconPreview();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadServerList() {
|
async loadServerList() {
|
||||||
@@ -201,35 +212,101 @@ class VNCServerApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleIconFileSelect(event) {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
// 파일 크기 체크 (1MB)
|
||||||
|
if (file.size > 1024 * 1024) {
|
||||||
|
this.showError('파일 크기는 1MB를 초과할 수 없습니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이미지 크기 체크 (클라이언트 측에서 미리 확인)
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
if (img.width > 128 || img.height > 128) {
|
||||||
|
this.showSuccess('이미지가 128x128로 자동 리사이즈됩니다.');
|
||||||
|
}
|
||||||
|
URL.revokeObjectURL(img.src); // 메모리 정리
|
||||||
|
};
|
||||||
|
img.src = URL.createObjectURL(file);
|
||||||
|
|
||||||
|
// 파일 타입 체크
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
this.showError('이미지 파일만 선택할 수 있습니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const iconPreview = document.getElementById('iconPreview');
|
||||||
|
const iconPlaceholder = document.getElementById('iconPlaceholder');
|
||||||
|
const removeIconBtn = document.getElementById('removeIconBtn');
|
||||||
|
|
||||||
|
iconPreview.src = e.target.result;
|
||||||
|
iconPreview.classList.remove('hidden');
|
||||||
|
iconPlaceholder.classList.add('hidden');
|
||||||
|
removeIconBtn.classList.remove('hidden');
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeIconPreview() {
|
||||||
|
const iconPreview = document.getElementById('iconPreview');
|
||||||
|
const iconPlaceholder = document.getElementById('iconPlaceholder');
|
||||||
|
const removeIconBtn = document.getElementById('removeIconBtn');
|
||||||
|
const iconFile = document.getElementById('iconFile');
|
||||||
|
|
||||||
|
iconPreview.src = '';
|
||||||
|
iconPreview.classList.add('hidden');
|
||||||
|
iconPlaceholder.classList.remove('hidden');
|
||||||
|
removeIconBtn.classList.add('hidden');
|
||||||
|
iconFile.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
showServerModal(server = null) {
|
showServerModal(server = null) {
|
||||||
const modal = document.getElementById('serverModal');
|
const modal = document.getElementById('serverModal');
|
||||||
const title = document.getElementById('modalTitle');
|
const modalTitle = document.getElementById('modalTitle');
|
||||||
const form = document.getElementById('serverForm');
|
const form = document.getElementById('serverForm');
|
||||||
|
|
||||||
console.log('서버 정보2:', server); // 디버깅용
|
// 폼 초기화
|
||||||
|
form.reset();
|
||||||
|
this.removeIconPreview();
|
||||||
|
|
||||||
if (server) {
|
if (server) {
|
||||||
title.textContent = '서버 편집';
|
// 편집 모드
|
||||||
|
modalTitle.textContent = '서버 편집';
|
||||||
document.getElementById('serverIp').value = server.IP;
|
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('serverPassword').value = server.Password || '';
|
||||||
|
document.getElementById('serverTitle').value = server.Title || '';
|
||||||
|
document.getElementById('serverCategory').value = server.Category || '';
|
||||||
|
document.getElementById('serverDescription').value = server.Description || '';
|
||||||
document.getElementById('serverArgument').value = server.Argument || '';
|
document.getElementById('serverArgument').value = server.Argument || '';
|
||||||
// 편집 모드 플래그 설정
|
|
||||||
modal.dataset.editMode = 'true';
|
// 아이콘 미리보기 설정
|
||||||
modal.dataset.editUser = server.User;
|
if (server.Icon) {
|
||||||
modal.dataset.editIp = server.IP;
|
const iconPreview = document.getElementById('iconPreview');
|
||||||
|
const iconPlaceholder = document.getElementById('iconPlaceholder');
|
||||||
|
const removeIconBtn = document.getElementById('removeIconBtn');
|
||||||
|
|
||||||
|
iconPreview.src = `${this.apiBase}/icon/${encodeURIComponent(server.User)}/${encodeURIComponent(server.IP)}`;
|
||||||
|
iconPreview.classList.remove('hidden');
|
||||||
|
iconPlaceholder.classList.add('hidden');
|
||||||
|
removeIconBtn.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
form.dataset.editMode = 'true';
|
||||||
|
form.dataset.originalUser = server.User;
|
||||||
|
form.dataset.originalIp = server.IP;
|
||||||
} else {
|
} else {
|
||||||
title.textContent = '서버 추가';
|
// 추가 모드
|
||||||
form.reset();
|
modalTitle.textContent = '서버 추가';
|
||||||
// 추가 모드 플래그 설정
|
delete form.dataset.editMode;
|
||||||
modal.dataset.editMode = 'false';
|
delete form.dataset.originalUser;
|
||||||
delete modal.dataset.editUser;
|
delete form.dataset.originalIp;
|
||||||
delete modal.dataset.editIp;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
modal.classList.remove('hidden');
|
modal.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,53 +433,77 @@ class VNCServerApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async saveServer() {
|
async saveServer() {
|
||||||
const serverIp = document.getElementById('serverIp').value;
|
|
||||||
const userName = document.getElementById('userName')?.value || '';
|
|
||||||
const modal = document.getElementById('serverModal');
|
|
||||||
const isEditMode = modal.dataset.editMode === 'true';
|
|
||||||
|
|
||||||
if (!userName) {
|
|
||||||
this.showError('사용자 이름이 설정되지 않았습니다. 먼저 설정에서 사용자 이름을 입력해주세요.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const serverData = {
|
|
||||||
user: userName,
|
|
||||||
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
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let url, method;
|
const form = document.getElementById('serverForm');
|
||||||
|
const userName = document.getElementById('userName')?.value || '';
|
||||||
|
|
||||||
if (isEditMode) {
|
if (!userName) {
|
||||||
// 편집 모드: 원본 사용자명과 IP를 쿼리 파라미터로 전달
|
this.showError('사용자 이름을 먼저 설정해주세요.');
|
||||||
const originalUser = modal.dataset.editUser;
|
return;
|
||||||
const originalIp = modal.dataset.editIp;
|
|
||||||
url = `${this.apiBase}/update/${encodeURIComponent(originalUser)}/${encodeURIComponent(originalIp)}`;
|
|
||||||
method = 'PUT';
|
|
||||||
} else {
|
|
||||||
// 추가 모드
|
|
||||||
url = `${this.apiBase}/add`;
|
|
||||||
method = 'POST';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const serverData = {
|
||||||
method: method,
|
User: userName,
|
||||||
headers: {
|
IP: document.getElementById('serverIp').value,
|
||||||
'Content-Type': 'application/json'
|
Title: document.getElementById('serverTitle').value,
|
||||||
},
|
Category: document.getElementById('serverCategory').value,
|
||||||
body: JSON.stringify(serverData)
|
Description: document.getElementById('serverDescription').value,
|
||||||
});
|
Password: document.getElementById('serverPassword').value,
|
||||||
|
Argument: document.getElementById('serverArgument').value
|
||||||
|
};
|
||||||
|
|
||||||
if (!response.ok) throw new Error('서버 저장에 실패했습니다.');
|
// 아이콘 파일이 선택된 경우 Base64로 변환
|
||||||
|
const iconFile = document.getElementById('iconFile');
|
||||||
const result = await response.json();
|
if (iconFile.files.length > 0) {
|
||||||
this.showSuccess(result.message);
|
const file = iconFile.files[0];
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = async (e) => {
|
||||||
|
// Base64 데이터에서 헤더 제거 (data:image/png;base64, 부분)
|
||||||
|
const base64Data = e.target.result.split(',')[1];
|
||||||
|
serverData.IconBase64 = base64Data;
|
||||||
|
|
||||||
|
await this.submitServerData(serverData, form);
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
} else {
|
||||||
|
await this.submitServerData(serverData, form);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.showError('서버 저장 중 오류가 발생했습니다: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async submitServerData(serverData, form) {
|
||||||
|
try {
|
||||||
|
let response;
|
||||||
|
const isEditMode = form.dataset.editMode === 'true';
|
||||||
|
|
||||||
|
if (isEditMode) {
|
||||||
|
// 편집 모드
|
||||||
|
const originalUser = form.dataset.originalUser;
|
||||||
|
const originalIp = form.dataset.originalIp;
|
||||||
|
response = await fetch(`${this.apiBase}/update/${encodeURIComponent(originalUser)}/${encodeURIComponent(originalIp)}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(serverData)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 추가 모드
|
||||||
|
response = await fetch(`${this.apiBase}/add`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(serverData)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.Message || '서버 저장에 실패했습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showSuccess(isEditMode ? '서버가 성공적으로 수정되었습니다.' : '서버가 성공적으로 추가되었습니다.');
|
||||||
this.hideServerModal();
|
this.hideServerModal();
|
||||||
this.loadServerList();
|
this.loadServerList();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -543,54 +644,69 @@ class VNCServerApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderCategoryNode(categoryName, categoryData, depth) {
|
renderCategoryNode(categoryName, categoryData, depth) {
|
||||||
const indent = depth * 20; // 들여쓰기
|
const indent = ' '.repeat(depth);
|
||||||
const hasSubcategories = Object.keys(categoryData.subcategories).length > 0;
|
let html = '';
|
||||||
const hasServers = categoryData.servers.length > 0;
|
|
||||||
const totalItems = categoryData.servers.length + Object.keys(categoryData.subcategories).length;
|
|
||||||
|
|
||||||
const headerData = {
|
// 카테고리 헤더
|
||||||
categoryName: categoryName,
|
if (depth === 0) {
|
||||||
depth: depth,
|
html += `<div class="category-header bg-gray-100 px-4 py-2 border-b border-gray-200">
|
||||||
bgClass: depth === 0 ? 'bg-gray-50' : 'bg-gray-25',
|
<div class="flex items-center justify-between">
|
||||||
hasContent: hasSubcategories || hasServers,
|
<h3 class="text-lg font-semibold text-gray-800 flex items-center">
|
||||||
totalItems: totalItems
|
<svg class="w-5 h-5 mr-2 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
};
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
|
||||||
|
</svg>
|
||||||
|
${this.escapeHtml(categoryName)}
|
||||||
|
</h3>
|
||||||
|
<span class="text-sm text-gray-500">${categoryData.servers.length}개 서버</span>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
} else {
|
||||||
|
html += `<div class="subcategory-header bg-gray-50 px-4 py-2 border-b border-gray-200 ml-${depth * 4}">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h4 class="text-md font-medium text-gray-700 flex items-center">
|
||||||
|
<svg class="w-4 h-4 mr-2 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path>
|
||||||
|
</svg>
|
||||||
|
${this.escapeHtml(categoryName)}
|
||||||
|
</h4>
|
||||||
|
<span class="text-sm text-gray-500">${categoryData.servers.length}개 서버</span>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
let html = `
|
// 서버 목록
|
||||||
<div class="category-group" style="margin-left: ${indent}px;">
|
if (categoryData.servers.length > 0) {
|
||||||
${this.renderTemplate('categoryHeaderTemplate', headerData)}
|
|
||||||
<div class="category-content" id="category-${this.escapeHtml(categoryName)}-${depth}">
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 서버 목록 렌더링
|
|
||||||
if (hasServers) {
|
|
||||||
categoryData.servers.forEach(server => {
|
categoryData.servers.forEach(server => {
|
||||||
console.log('서버 데이터:', server); // 디버깅용
|
const serverName = server.Title || server.title || `${server.User || server.user} - ${server.IP || server.ip}`;
|
||||||
|
const serverIP = server.IP || server.ip;
|
||||||
|
const userName = server.User || server.user;
|
||||||
|
const serverCategory = server.Category || server.category;
|
||||||
|
const serverDescription = server.Description || server.description;
|
||||||
|
const serverArgument = server.Argument || server.argument;
|
||||||
|
|
||||||
|
// 아이콘 URL 생성
|
||||||
|
const iconUrl = `${this.apiBase}/icon/${encodeURIComponent(userName)}/${encodeURIComponent(serverIP)}`;
|
||||||
|
|
||||||
const serverData = {
|
const serverData = {
|
||||||
serverName: `${server.User || server.user}@${server.Title || server.title}`,
|
userName: userName,
|
||||||
userName: server.User || server.user,
|
serverIP: serverIP,
|
||||||
serverIP: server.IP || server.ip,
|
serverName: this.escapeHtml(serverName),
|
||||||
serverCategory: server.Category || server.category,
|
serverCategory: this.escapeHtml(serverCategory),
|
||||||
serverTitle: server.Title || server.title,
|
serverDescription: this.escapeHtml(serverDescription),
|
||||||
serverDescription: server.Description || server.description,
|
serverArgument: this.escapeHtml(serverArgument),
|
||||||
serverArgument: server.Argument || server.argument
|
iconUrl: iconUrl
|
||||||
};
|
};
|
||||||
console.log('렌더링할 서버 데이터:', serverData); // 디버깅용
|
|
||||||
html += this.renderTemplate('serverItemTemplate', serverData);
|
html += this.renderTemplate('serverItemTemplate', serverData);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 하위 카테고리 렌더링
|
// 하위 카테고리
|
||||||
const sortedSubcategories = Object.keys(categoryData.subcategories).sort();
|
const subcategories = Object.keys(categoryData.subcategories).sort();
|
||||||
sortedSubcategories.forEach(subcategoryName => {
|
subcategories.forEach(subcategory => {
|
||||||
html += this.renderCategoryNode(subcategoryName, categoryData.subcategories[subcategoryName], depth + 1);
|
html += this.renderCategoryNode(subcategory, categoryData.subcategories[subcategory], depth + 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
html += `
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user