diff --git a/App.razor.cs b/App.razor.cs
index b39d420..055c043 100644
--- a/App.razor.cs
+++ b/App.razor.cs
@@ -8,17 +8,17 @@ namespace Front;
public partial class App : ComponentBase
{
[Inject] private APIService API { get; set; } = default!;
- [Inject] private CookieService Cookie { get; set; } = default!;
+ [Inject] private StorageService StorageService { get; set; } = default!;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
- var cookie = await Cookie.GetCookieAsync("Web_AM_Connect_Key");
+ var headerValue = await StorageService.GetItemAsync("Web_AM_Connect_Key");
// 값 없으면 API 호출
- if (string.IsNullOrEmpty(cookie))
+ if (string.IsNullOrEmpty(headerValue))
{
var response = await API.GetJsonAsync(
"/api/v1/in/app",
@@ -30,7 +30,7 @@ public partial class App : ComponentBase
});
if (!string.IsNullOrEmpty(response.data.header))
{
- await Cookie.SetCookieAsync("Web_AM_Connect_Key", response.data.header);
+ await StorageService.SetItemAsync("Web_AM_Connect_Key", response.data.header);
}
}
}
diff --git a/Front.csproj b/Front.csproj
index 7227ec8..08512f3 100644
--- a/Front.csproj
+++ b/Front.csproj
@@ -11,6 +11,7 @@
+
@@ -27,7 +28,7 @@
-
+
diff --git a/Program.cs b/Program.cs
index 8e520df..8b29c5e 100644
--- a/Program.cs
+++ b/Program.cs
@@ -4,12 +4,16 @@ using Microsoft.Extensions.Configuration;
using Front;
using Front.Program.Services;
+using Front.Program.ViewModels;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add("#app");
builder.RootComponents.Add("head::after");
+// 설정 파일 로드
+// builder.Configuration.AddJsonFile("appsettings.json", optional: false);
+builder.Configuration.AddJsonFile($"appsettings.{builder.HostEnvironment.Environment}.json", optional: true);
builder.Services.AddScoped(sp => //new HttpClient
{
@@ -30,10 +34,11 @@ builder.Services.AddScoped(sp => //new HttpClient
// SCOPED 으로 등록된 서비스는 DI 컨테이너에 등록된 서비스의 인스턴스를 사용합니다.
builder.Services.AddScoped();
-builder.Services.AddScoped();
-// builder.Services.AddRazorPages();
-// builder.Services.AddServerSideBlazor();
-builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
await builder.Build().RunAsync();
diff --git a/Program/Layout/MainLayout.razor b/Program/Layout/MainLayout.razor
index 337b557..03b549c 100644
--- a/Program/Layout/MainLayout.razor
+++ b/Program/Layout/MainLayout.razor
@@ -5,25 +5,43 @@
@* *@
- @if (!isHideTop)
+
+ @if(isAcademy)
{
-
+
+ @if (!isIntro && UserStateService.isLogin)
+ {
+
+
+
+
+ }
+ else
+ {
+
+ @Body
+
+ }
+
}
-
-
-
-
- @* *@
- @*
*@
- @* *@
- @*
*@
-
+ else
+ {
+ @if (!isHidePrjTop)
+ {
+
+ }
- @*
*@
@Body
-
+ }
diff --git a/Program/Layout/MainLayout.razor.cs b/Program/Layout/MainLayout.razor.cs
index 766970f..56b053c 100644
--- a/Program/Layout/MainLayout.razor.cs
+++ b/Program/Layout/MainLayout.razor.cs
@@ -1,31 +1,37 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
+
using Front.Program.Views.Project;
+using Front.Program.Views.Academy;
using Front.Program.Services;
+using Front.Program.ViewModels;
namespace Front.Program.Layout;
public partial class MainLayout : LayoutComponentBase, IDisposable
{
- [Inject]
- NavigationManager Navigation { get; set; } = default!;
+ [Inject] NavigationManager Navigation { get; set; } = default!;
+ [Inject] LoadingService LoadingService { get; set; } = default!;
+ [Inject] UserStateService UserStateService { get; set; } = default!;
- [Inject]
- LoadingService LoadingService { get; set; } = default!;
-
- // protected bool isHideTop => Navigation.Uri.Contains("/auth");
- protected bool isHideTop => Navigation.ToBaseRelativePath(Navigation.Uri).Equals("auth", StringComparison.OrdinalIgnoreCase);
-
- public static bool IsLoading { get; set; }
- public static IndicateType CurrentType { get; set; } = IndicateType.Page;
+ // 경로의 시작 부분
+ // protected bool isHidePrjTop => Navigation.ToBaseRelativePath(Navigation.Uri).StartsWith("auth", StringComparison.OrdinalIgnoreCase);
+ // 경로의 끝 부분
+ protected bool isHidePrjTop => Navigation.ToBaseRelativePath(Navigation.Uri).EndsWith("auth", StringComparison.OrdinalIgnoreCase);
+ protected bool isAcademy => Navigation.ToBaseRelativePath(Navigation.Uri).StartsWith("am", StringComparison.OrdinalIgnoreCase);
+
+ // 경로 일치
+ protected bool isIntro => Navigation.ToBaseRelativePath(Navigation.Uri).Equals("am/intro", StringComparison.OrdinalIgnoreCase);
protected override void OnInitialized()
{
LoadingService.OnChange += StateHasChanged;
Navigation.LocationChanged += HandleLocationChanged;
+ HandleLocationChanged(this, new LocationChangedEventArgs(Navigation.Uri, false));
}
- private void HandleLocationChanged(object? sender, LocationChangedEventArgs e)
+ // 페이지의 URL이 변경될 때마다 실행되는 이벤트 핸들러
+ private async void HandleLocationChanged(object? sender, LocationChangedEventArgs e)
{
LoadingService.HideNavigationLoading();
}
@@ -35,15 +41,4 @@ public partial class MainLayout : LayoutComponentBase, IDisposable
LoadingService.OnChange -= StateHasChanged;
Navigation.LocationChanged -= HandleLocationChanged;
}
-
- public static void ShowLoading(IndicateType type = IndicateType.Page)
- {
- IsLoading = true;
- CurrentType = type;
- }
-
- public static void HideLoading()
- {
- IsLoading = false;
- }
}
\ No newline at end of file
diff --git a/Program/Models/AcademyModels.cs b/Program/Models/AcademyModels.cs
new file mode 100644
index 0000000..3623fa1
--- /dev/null
+++ b/Program/Models/AcademyModels.cs
@@ -0,0 +1,22 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Front.Program.Models;
+
+public class Academy
+{
+ public required string bid { get; set; }
+ public string business_name { get; set; } = string.Empty;
+ public string business_owner { get; set; } = string.Empty;
+ public string businessOwnerUID { get; set; } = string.Empty;
+ public string business_number { get; set; } = string.Empty;
+ public DateTime business_date { get; set; }
+ public string business_address { get; set; } = string.Empty;
+ public string business_contact { get; set; } = string.Empty;
+ public string uid { get; set; } = string.Empty;
+}
+
+public class SimpleAcademy
+{
+ public required string bid { get; set; }
+ public string business_name { get; set; } = string.Empty;
+}
\ No newline at end of file
diff --git a/Program/Models/UserData.cs b/Program/Models/UserData.cs
new file mode 100644
index 0000000..532f4fa
--- /dev/null
+++ b/Program/Models/UserData.cs
@@ -0,0 +1,13 @@
+namespace Front.Program.Models;
+
+public class UserData
+{
+ public string Uid { get; set; } = string.Empty;
+ public string Name { get; set; } = string.Empty;
+ public DateTime? Birth { get; set; }
+ public string Type { get; set; } = string.Empty;
+ public string? DeviceId { get; set; }
+ public bool AutoLoginYn { get; set; }
+ public DateTime LoginDate { get; set; }
+ public string? PushToken { get; set; }
+}
diff --git a/Program/Services/APIService.cs b/Program/Services/APIService.cs
index 51f3845..2c11090 100644
--- a/Program/Services/APIService.cs
+++ b/Program/Services/APIService.cs
@@ -28,4 +28,16 @@ public class APIService
var response = await _http.GetFromJsonAsync>($"{url}?{parameter}");
return response;
}
-}
\ No newline at end of file
+
+}
+
+/*
+dynamic 타입을 사용하면:
+타입 안전성이 떨어집니다
+컴파일 타임에 오류를 잡을 수 없습니다
+런타임에 예상치 못한 오류가 발생할 수 있습니다
+현재 Register.razor.cs에서 JsonElement를 사용하는 방식이 더 안전할 수 있습니다. 왜냐하면:
+JSON 응답의 구조를 명시적으로 확인할 수 있습니다 (TryGetProperty 사용)
+각 속성의 타입을 명확하게 처리할 수 있습니다
+예상치 못한 데이터 구조에 대해 더 안전하게 대응할 수 있습니다
+*/
\ No newline at end of file
diff --git a/Program/Services/CookieService.cs b/Program/Services/CookieService.cs
deleted file mode 100644
index 5fc16ba..0000000
--- a/Program/Services/CookieService.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using Microsoft.AspNetCore.Components;
-using Microsoft.JSInterop;
-
-namespace Front.Program.Services;
-
-public class CookieService
-{
- private readonly IJSRuntime _js;
-
- public CookieService(IJSRuntime js)
- {
- _js = js;
- }
-
- public async Task SetCookieAsync(string key, string value)
- {
- await _js.InvokeVoidAsync("setCookie", key, value, 1);
- }
-
- public async Task GetCookieAsync(string key)
- {
- return await _js.InvokeAsync("getCookie", key);
- }
-
- public async Task DeleteCookieAsync(string key)
- {
- await _js.InvokeVoidAsync("deleteCookie", key);
- }
-}
\ No newline at end of file
diff --git a/Program/Services/LoadingService.cs b/Program/Services/LoadingService.cs
index 2e55f18..07e8a2c 100644
--- a/Program/Services/LoadingService.cs
+++ b/Program/Services/LoadingService.cs
@@ -1,20 +1,21 @@
-using Front.Program.Views.Project;
+// using Front.Program.Views.Project.Common;
+
+using Front.Program.Models;
using Microsoft.AspNetCore.Components;
namespace Front.Program.Services;
+// 뷰를 참조 안하고도 로딩 상태를 알 수 있게 바꾸기
public class LoadingService
{
public bool IsLoading { get; private set; }
- public IndicateType CurrentType { get; private set; } = IndicateType.Page;
private bool isNavigationLoading { get; set; }
public event Action? OnChange;
- public void ShowLoading(IndicateType type = IndicateType.Page, bool isNavigation = false)
+ public void ShowLoading(bool isNavigation = false)
{
IsLoading = true;
- CurrentType = type;
isNavigationLoading = isNavigation;
NotifyStateChanged();
}
diff --git a/Program/Services/QueryParamService.cs b/Program/Services/QueryParamService.cs
new file mode 100644
index 0000000..c6693ae
--- /dev/null
+++ b/Program/Services/QueryParamService.cs
@@ -0,0 +1,42 @@
+using Microsoft.AspNetCore.Components;
+using Microsoft.EntityFrameworkCore.Metadata.Internal;
+
+namespace Front.Program.Services;
+
+public class QueryParamService
+{
+ public Dictionary ParseQueryParam(System.Uri uri)
+ {
+ var result = new Dictionary();
+ if (!string.IsNullOrEmpty(uri.Query))
+ {
+ var query = uri.Query.TrimStart('?');
+ var parameters = query.Split('&')
+ .Select(p => p.Split('='))
+ .Where(p => p.Length == 2)
+ .ToDictionary(p => p[0], p => p[1]);
+ result = parameters;
+ }
+
+ return result;
+ }
+
+ public async Task AuthCheck(Dictionary parameters, StorageService storageService)
+ {
+ if (parameters.TryGetValue("auth", out var auth))
+ {
+ Console.WriteLine($"auth 파라미터 값: {auth}");
+ if (auth == "true")
+ {
+ await storageService.SetItemAsync("IsLogin", "true");
+ Console.WriteLine("로그인 상태를 true로 설정했습니다.");
+ }
+ else
+ {
+ await storageService.RemoveItemAsync("IsLogin");
+ Console.WriteLine("로그인 상태를 제거했습니다.");
+
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Program/Services/SecureService.cs b/Program/Services/SecureService.cs
new file mode 100644
index 0000000..42977f2
--- /dev/null
+++ b/Program/Services/SecureService.cs
@@ -0,0 +1,120 @@
+using System.Security.Cryptography;
+using System.Text;
+using Microsoft.JSInterop;
+using System.Text.Json;
+using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
+
+namespace Front.Program.Services;
+
+public class SecureService
+{
+ private readonly ILogger _logger;
+ private readonly IJSRuntime _jsRuntime;
+ private readonly IWebAssemblyHostEnvironment _environment;
+ private string? _key;
+ private string? _iv;
+ private Task? _initializationTask;
+
+ public SecureService(
+ ILogger logger,
+ IJSRuntime jsRuntime,
+ IWebAssemblyHostEnvironment environment)
+ {
+ _logger = logger;
+ _jsRuntime = jsRuntime;
+ _environment = environment;
+ _initializationTask = InitializeAsync();
+ }
+
+ private async Task InitializeAsync()
+ {
+ try
+ {
+ var configFile = $"appsettings.{_environment.Environment}.json";
+ _logger.LogInformation($"설정 파일 로드: {configFile}");
+
+ var config = await _jsRuntime.InvokeAsync("loadConfig", configFile);
+ if (config.ValueKind == JsonValueKind.Null)
+ {
+ throw new InvalidOperationException($"설정 파일을 로드할 수 없습니다: {configFile}");
+ }
+
+ var security = config.GetProperty("Security");
+ _key = security.GetProperty("EncryptionKey").GetString()
+ ?? throw new ArgumentNullException("Security:EncryptionKey");
+ _iv = security.GetProperty("EncryptionIV").GetString()
+ ?? throw new ArgumentNullException("Security:EncryptionIV");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError($"설정 로드 중 오류 발생: {ex.Message}");
+ throw;
+ }
+ }
+
+ private async Task EnsureInitializedAsync()
+ {
+ if (_initializationTask != null)
+ {
+ await _initializationTask;
+ _initializationTask = null;
+ }
+ }
+
+ private byte[] GetKeyBytes(string key)
+ {
+ using (var sha256 = SHA256.Create())
+ {
+ return sha256.ComputeHash(Encoding.UTF8.GetBytes(key));
+ }
+ }
+
+ private byte[] GetIVBytes(string iv)
+ {
+ using (var sha256 = SHA256.Create())
+ {
+ var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(iv));
+ return hash.Take(16).ToArray(); // IV는 16바이트만 필요
+ }
+ }
+
+ public async Task EncryptAsync(string plainText)
+ {
+ await EnsureInitializedAsync();
+
+ if (_key == null || _iv == null)
+ throw new InvalidOperationException("암호화 키가 초기화되지 않았습니다.");
+
+ try
+ {
+ return await _jsRuntime.InvokeAsync("encryptText", plainText, _key, _iv);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError($"암호화 중 오류 발생: {ex.Message}");
+ throw;
+ }
+ }
+
+ public async Task DecryptAsync(string cipherText)
+ {
+ await EnsureInitializedAsync();
+
+ if (_key == null || _iv == null)
+ throw new InvalidOperationException("암호화 키가 초기화되지 않았습니다.");
+
+ try
+ {
+ return await _jsRuntime.InvokeAsync("decryptText", cipherText, _key, _iv);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError($"복호화 중 오류 발생: {ex.Message}");
+ throw;
+ }
+ }
+
+ // 동기 메서드는 비동기 메서드를 호출하도록 수정
+ public string Encrypt(string plainText) => EncryptAsync(plainText).GetAwaiter().GetResult();
+ public string Decrypt(string cipherText) => DecryptAsync(cipherText).GetAwaiter().GetResult();
+}
\ No newline at end of file
diff --git a/Program/Services/StorageService.cs b/Program/Services/StorageService.cs
new file mode 100644
index 0000000..4aa7f76
--- /dev/null
+++ b/Program/Services/StorageService.cs
@@ -0,0 +1,83 @@
+using Microsoft.AspNetCore.Components;
+using Microsoft.JSInterop;
+
+namespace Front.Program.Services;
+
+public enum STORAGE_TYPE
+{
+ Cookie,
+ Local,
+ Session
+}
+
+public class StorageService
+{
+ private readonly IJSRuntime _js;
+
+ public StorageService(IJSRuntime js)
+ {
+ _js = js;
+ }
+
+ public async Task SetItemAsync(string key, string value, STORAGE_TYPE type = STORAGE_TYPE.Session, int expire_time = 1)
+ {
+ await (type switch
+ {
+ STORAGE_TYPE.Cookie => _js.InvokeVoidAsync("setCookie", key, value, expire_time),
+ STORAGE_TYPE.Local => _js.InvokeVoidAsync("localStorage.setItem", key, value),
+ STORAGE_TYPE.Session => _js.InvokeVoidAsync("sessionStorage.setItem", key, value),
+ _ => throw new ArgumentException("Invalid storage type")
+ });
+ }
+
+ public async Task GetItemAsync(string key, STORAGE_TYPE type = STORAGE_TYPE.Session)
+ {
+ return type switch
+ {
+ STORAGE_TYPE.Cookie => await _js.InvokeAsync("getCookie", key),
+ STORAGE_TYPE.Local => await _js.InvokeAsync("localStorage.getItem", key),
+ STORAGE_TYPE.Session => await _js.InvokeAsync("sessionStorage.getItem", key),
+ _ => null
+ };
+ }
+
+ public async Task RemoveItemAsync(string key, STORAGE_TYPE type = STORAGE_TYPE.Session)
+ {
+ try
+ {
+ await (type switch
+ {
+ STORAGE_TYPE.Cookie => _js.InvokeVoidAsync("deleteCookie", key),
+ STORAGE_TYPE.Local => _js.InvokeVoidAsync("localStorage.removeItem", key),
+ STORAGE_TYPE.Session => _js.InvokeVoidAsync("sessionStorage.removeItem", key),
+ _ => throw new ArgumentException("Invalid storage type")
+ });
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+}
+
+/* 스토리지 종류
+1. Cookie
+ 만료일 : 명시적으로 설정이 가능 (설정에 따라 그 값을 변경 가능)
+ 접근성 : 서버와 클라이언트 모두 접근이 가능함
+ 특징
+ 1. HTTP 요청마다 서버로 전송이 되며 도메인/경로 제한이 가능하다.
+ 2. HttpOnly나 secure 옵션으로 보안 설정이 가능하다.
+2. Local
+ 만료일 : 브라우저 데이터 삭제 전까지는 영구 보관 가능
+ 접근성 : 클라이언트 측에서 JS 를 통해서만 접근이 가능
+ 특징
+ 1. 도메인별로 분리된 저장소로 동기적인 동작이 된다.
+ 2. 문자열만 저장이 가능하다.
+3. Sesson
+ 만료일 : 브라우저의 탭/창 닫을 때 까지 보관 (닫게 되면 삭제)
+ 접근성 : 클라이언트 측에서 JS 를 통해서만 접근이 가능
+ 특징
+ 1. 탭/창 별로 불리된 저장소며 동기적인 동작이 가능하다.
+ 2. 문자열만 저장이 가능하다.
+ */
diff --git a/Program/Services/UserStateService.cs b/Program/Services/UserStateService.cs
new file mode 100644
index 0000000..e209bef
--- /dev/null
+++ b/Program/Services/UserStateService.cs
@@ -0,0 +1,167 @@
+using System.Text.Json;
+using Front.Program.Services;
+using Front.Program.Models;
+using Microsoft.JSInterop;
+
+namespace Front.Program.ViewModels;
+
+public class UserStateService(StorageService _storageService,SecureService _secureService, IJSRuntime _js)
+{
+ public UserData UserData { get; set; } = new UserData();
+
+ public bool isFirstCheck { get; set; } = false;
+ public bool isLogin { get; set; } = false;
+
+
+ public async Task<(bool success, UserData? userData)> GetUserDataFromStorageAsync()
+ {
+ try
+ {
+ var encUserData = await _storageService.GetItemAsync("USER_DATA");
+
+ if (!string.IsNullOrEmpty(encUserData))
+ {
+
+ var decUserData = await _secureService.DecryptAsync(encUserData);
+
+ var userData = JsonSerializer.Deserialize(decUserData,
+ new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
+
+ Console.WriteLine($"UserData: {userData.Name}, {userData.Type}");
+
+
+ return (true, userData);
+ }
+ else
+ {
+ return (false, null);
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error [GetUserDataFormAsync] : {ex.Message}");
+ return (false, null);
+ }
+ }
+
+ public async Task<(bool success, UserData? userData)> GetUserDataFromServerAsync()
+ {
+
+ var headerValue = await _storageService.GetItemAsync("Web_AM_Connect_Key");
+ if (string.IsNullOrEmpty(headerValue)) return (false, null);
+
+ var args = new {
+ url = "/api/v1/in/user",
+ method = "GET",
+ headerKey = "Web_AM_Connect_Key",
+ headerValue = headerValue,
+ token = "VO00"
+ };
+
+ var response = await _js.InvokeAsync(
+ "fetchWithHeaderAndReturnUrl",
+ args
+ );
+
+ // var response = await _js.InvokeAsync("fetchWithHeaderAndReturnUrl",
+ // "/api/v1/in/user/",
+ // "GET",
+ // "Web_AM_Connect_Key",
+ // headerValue);
+
+ Console.WriteLine($"JSON 응답: {response.ToString()}");
+
+ if (response.TryGetProperty("data", out var dataElement))
+ {
+ try
+ {
+ // 전체 데이터 암호화 저장
+ var userDataJson = dataElement.ToString();
+ var userData = JsonSerializer.Deserialize(userDataJson,
+ new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
+
+ if (userData != null && !string.IsNullOrEmpty(userData.Name))
+ {
+ var encryptedData = await _secureService.EncryptAsync(userDataJson);
+ await _storageService.SetItemAsync("USER_DATA", encryptedData);
+ return (true, userData);
+ }
+ else
+ {
+ Console.WriteLine("사용자 데이터에 필수 정보가 없습니다");
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"사용자 데이터 처리 중 오류: {ex.Message}");
+ }
+ }
+
+ Console.WriteLine("사용자 데이터를 찾을 수 없습니다");
+ return (false, null);
+ }
+
+ public async Task GetUserDataAsync()
+ {
+ try
+ {
+ Console.WriteLine("GetUserDataAsync 호출됨");
+ // 로그인 상태가 아니라면 애초에 할 필요 없음
+ if (await _storageService.GetItemAsync("IsLogin") != "true")
+ {
+ isLogin = false;
+ return false;
+ }
+
+ var userDataForm = await GetUserDataFromStorageAsync();
+
+ if (userDataForm.success && userDataForm.userData != null)
+ {
+ // 사용자 데이터가 성공적으로 로드되었을 때의 로직
+ UserData = userDataForm.userData;
+ isLogin = true;
+ return true;
+ }
+ else
+ {
+ var userDataFromServer = await GetUserDataFromServerAsync();
+ if (userDataFromServer.success && userDataFromServer.userData != null)
+ {
+ // 서버에서 사용자 데이터를 성공적으로 로드했을 때의 로직
+ UserData = userDataFromServer.userData;
+ isLogin = true;
+ return true;
+ }
+ else
+ {
+ // 사용자 데이터를 로드하지 못했을 때의 로직
+ Console.WriteLine("사용자 데이터를 로드하지 못했습니다.");
+ isLogin = false;
+ return false;
+ }
+ }
+ }
+ finally
+ {
+ if (!isFirstCheck) isFirstCheck = true;
+ }
+ }
+
+ public async Task ClearUserData()
+ {
+ try
+ {
+ await _storageService.RemoveItemAsync("USER_DATA");
+ await _storageService.RemoveItemAsync("IsLogin");
+ Console.WriteLine("사용자 데이터 삭제 성공");
+ isLogin = false;
+ // return true;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"사용자 데이터 삭제 중 오류: {ex.Message}");
+ // return false;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/Program/Views/Academy/AcademyIntro.razor b/Program/Views/Academy/AcademyIntro.razor
new file mode 100644
index 0000000..b52829e
--- /dev/null
+++ b/Program/Views/Academy/AcademyIntro.razor
@@ -0,0 +1,68 @@
+@page "/am/intro"
+
+
+
+
+
+
+
+ @if (!UserStateService.isLogin)
+ {
+
+ 학원을 위한 통합 플랫폼
+
+
+
+
+
+ }
+ else
+ {
+
+ }
+
+ @*
*@
+
+
+
+
+ © 2024 AcaMate. All rights reserved.
+
+
+
+
+
diff --git a/Program/Views/Academy/AcademyIntro.razor.cs b/Program/Views/Academy/AcademyIntro.razor.cs
new file mode 100644
index 0000000..3f99336
--- /dev/null
+++ b/Program/Views/Academy/AcademyIntro.razor.cs
@@ -0,0 +1,88 @@
+using Front.Program.Models;
+using Front.Program.Services;
+using Front.Program.ViewModels;
+using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.Components.Routing;
+
+namespace Front.Program.Views.Academy;
+
+public partial class AcademyIntro : ComponentBase, IDisposable
+{
+ [Inject] NavigationManager Navigation { get; set; } = default!;
+ [Inject] StorageService StorageService { get; set; } = default!;
+ [Inject] QueryParamService QueryParamService { get; set; } = default!;
+ [Inject] UserStateService UserStateService { get; set; } = default!;
+
+ private bool _isProcessing = false;
+
+
+ protected Models.SimpleAcademy[] academyItems = Array.Empty();
+
+ protected override async void OnInitialized()
+ {
+ Navigation.LocationChanged += HandleLocationChanged;
+ HandleLocationChanged(this, new LocationChangedEventArgs(Navigation.Uri, false));
+ if (!UserStateService.isFirstCheck) await UserStateService.GetUserDataAsync();
+
+ academyItems = new[]
+ {
+ new SimpleAcademy{ bid = "AA0000", business_name = "테스트 학원1"},
+ new SimpleAcademy{ bid = "AA0001", business_name = "테스트 학원2"},
+ new SimpleAcademy{ bid = "AA0002", business_name = "테스트 학원3"},
+ new SimpleAcademy{ bid = "AA0003", business_name = "테스트 학원4"},
+ new SimpleAcademy{ bid = "AA0004", business_name = "테스트 학원5"},
+ new SimpleAcademy{ bid = "AA0005", business_name = "테스트 학원6"},
+ new SimpleAcademy{ bid = "AA0006", business_name = "테스트 학원7"},
+
+ };
+
+ await InvokeAsync(StateHasChanged);
+ }
+
+ public void Dispose()
+ {
+ Navigation.LocationChanged -= HandleLocationChanged;
+ }
+
+ private async void HandleLocationChanged(object? sender, LocationChangedEventArgs e)
+ {
+ try
+ {
+ // 다중 실행 방지
+ if (_isProcessing) return;
+ _isProcessing = true;
+
+ var uri = Navigation.ToAbsoluteUri(Navigation.Uri);
+ Console.WriteLine($"리다이렉트된 URI: {uri}");
+
+ // 쿼리 파라미터가 있는 경우에만 처리
+ if (!string.IsNullOrEmpty(uri.Query))
+ {
+ var queryParam = QueryParamService.ParseQueryParam(uri);
+ await QueryParamService.AuthCheck(queryParam, StorageService);
+
+ // 유저 정보 확인하는거 (로그인 했으니 값 가져와야지)
+ await UserStateService.GetUserDataAsync();
+
+ // 쿼리 파라미터를 제거한 기본 URI로 리다이렉트
+ var baseUri = uri.GetLeftPart(UriPartial.Path);
+ Console.WriteLine($"리다이렉트할 URI: {baseUri}");
+ await InvokeAsync(StateHasChanged); // StateHasChanged를 호출하여 UI 업데이트
+ Navigation.NavigateTo(baseUri, forceLoad: false);
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error in HandleLocationChanged: {ex.Message}");
+ }
+ finally
+ {
+ _isProcessing = false;
+ }
+ }
+
+ protected void OnClickLogin()
+ {
+ Navigation.NavigateTo("/am/auth");
+ }
+}
\ No newline at end of file
diff --git a/Program/Views/Academy/LeftSideAcademy.razor b/Program/Views/Academy/LeftSideAcademy.razor
new file mode 100644
index 0000000..30f4a05
--- /dev/null
+++ b/Program/Views/Academy/LeftSideAcademy.razor
@@ -0,0 +1,70 @@
+
+
+
+

+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Program/Views/Academy/LeftSideAcademy.razor.cs b/Program/Views/Academy/LeftSideAcademy.razor.cs
new file mode 100644
index 0000000..c8b78ca
--- /dev/null
+++ b/Program/Views/Academy/LeftSideAcademy.razor.cs
@@ -0,0 +1,7 @@
+using Microsoft.AspNetCore.Components;
+
+namespace Front.Program.Views.Academy;
+
+public partial class LeftSideAcademy : ComponentBase
+{
+}
\ No newline at end of file
diff --git a/Program/Views/Academy/TopNavAcademy.razor b/Program/Views/Academy/TopNavAcademy.razor
new file mode 100644
index 0000000..0d18900
--- /dev/null
+++ b/Program/Views/Academy/TopNavAcademy.razor
@@ -0,0 +1,36 @@
+
+
+
+
+ @if (isAcademyDropdownOpen)
+ {
+
+ }
+
+
+
+
diff --git a/Program/Views/Academy/TopNavAcademy.razor.cs b/Program/Views/Academy/TopNavAcademy.razor.cs
new file mode 100644
index 0000000..5dfce84
--- /dev/null
+++ b/Program/Views/Academy/TopNavAcademy.razor.cs
@@ -0,0 +1,27 @@
+using Front.Program.ViewModels;
+using Front.Program.Services;
+using Microsoft.AspNetCore.Components;
+using System.Net.Http.Json;
+using System.Runtime.InteropServices.JavaScript;
+using System.Text.Json;
+using Front.Program.Models;
+
+namespace Front.Program.Views.Academy;
+
+public partial class TopNavAcademy : ComponentBase
+{
+ [Inject] UserStateService UserStateService { get; set; } = default!;
+
+ protected bool isOpen = false;
+ protected bool isAcademyDropdownOpen = false;
+ protected Models.SimpleAcademy[] academyItems = Array.Empty();
+
+ protected override async Task OnInitializedAsync()
+ {
+ Console.WriteLine("TOPNAV_OnInitializedAsync");
+ }
+
+ protected void ToggleAcademyDropdown() {
+ isAcademyDropdownOpen = !isAcademyDropdownOpen;
+ }
+}
\ No newline at end of file
diff --git a/Program/Views/Project/About.razor b/Program/Views/Project/About.razor
index 253bb73..be214b3 100644
--- a/Program/Views/Project/About.razor
+++ b/Program/Views/Project/About.razor
@@ -167,8 +167,8 @@
-
-
diff --git a/Program/Views/Project/Register.razor.cs b/Program/Views/Project/ConnectUser/Register.razor.cs
similarity index 79%
rename from Program/Views/Project/Register.razor.cs
rename to Program/Views/Project/ConnectUser/Register.razor.cs
index 49e1a94..2d5c3b8 100644
--- a/Program/Views/Project/Register.razor.cs
+++ b/Program/Views/Project/ConnectUser/Register.razor.cs
@@ -5,8 +5,9 @@ using System.Net.Http.Json;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using Front.Program.Layout;
+using Front.Program.Services;
-namespace Front.Program.Views.Project;
+namespace Front.Program.Views.Project.ConnectUser;
public partial class Register : ComponentBase
{
@@ -14,10 +15,15 @@ public partial class Register : ComponentBase
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
[Inject] private HttpClient Http { get; set; } = default!;
[Inject] private IConfiguration Configuration { get; set; } = default!;
+ [Inject] private LoadingService LoadingService { get; set; } = default!;
+ [Inject] private StorageService CookieService { get; set; } = default!;
private ElementReference dateInputRef;
private string name = "";
+ private string birthYear = "";
+ private string birthMonth = "";
+ private string birthDay = "";
private DateTime? birth;
private string email = "";
private string phone = "";
@@ -34,8 +40,7 @@ public partial class Register : ComponentBase
objRef = DotNetObjectReference.Create(this);
try
{
- // 쿠키에서 토큰 가져오기
- var token = await JS.InvokeAsync("eval", "document.cookie.split('; ').find(row => row.startsWith('Web_AM_Connect_Key='))?.split('=')[1]");
+ var token = await CookieService.GetItemAsync("Web_AM_Connect_Key");
if (string.IsNullOrEmpty(token))
{
await JS.InvokeVoidAsync("alert", "인증 정보가 없습니다.");
@@ -89,6 +94,27 @@ public partial class Register : ComponentBase
}
}
+ private void UpdateBirthDate()
+ {
+ if (int.TryParse(birthYear, out int year) &&
+ int.TryParse(birthMonth, out int month) &&
+ int.TryParse(birthDay, out int day))
+ {
+ try
+ {
+ birth = new DateTime(year, month, day);
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ birth = null;
+ }
+ }
+ else
+ {
+ birth = null;
+ }
+ }
+
private async Task ConfirmData()
{
if (string.IsNullOrWhiteSpace(name))
@@ -103,6 +129,9 @@ public partial class Register : ComponentBase
return;
}
+ // 생일 업데이트
+ UpdateBirthDate();
+
// 전화번호 조합
if (phonePart1.Length == 3 && phonePart2.Length == 4 && phonePart3.Length == 4)
{
@@ -139,9 +168,8 @@ public partial class Register : ComponentBase
try
{
- MainLayout.ShowLoading();
- // 쿠키에서 토큰 가져오기
- var token = await JS.InvokeAsync("eval", "document.cookie.split('; ').find(row => row.startsWith('Web_AM_Connect_Key='))?.split('=')[1] || ''");
+ LoadingService.ShowLoading();
+ var token = await CookieService.GetItemAsync("Web_AM_Connect_Key");
Console.WriteLine($"쿠키에서 가져온 토큰: '{token}'");
if (string.IsNullOrEmpty(token))
@@ -149,7 +177,7 @@ public partial class Register : ComponentBase
await JS.InvokeVoidAsync("alert", "인증 정보가 없습니다.");
NavigationManager.NavigateTo("/");
- MainLayout.HideLoading();
+ LoadingService.HideLoading();
return;
}
@@ -186,30 +214,27 @@ public partial class Register : ComponentBase
// 세션 스토리지 정리
await JS.InvokeVoidAsync("sessionStorage.removeItem", "snsId");
- MainLayout.HideLoading();
+ LoadingService.HideLoading();
await JS.InvokeVoidAsync("alert", "회원가입이 완료되었습니다.");
NavigationManager.NavigateTo("/");
}
else
{
- MainLayout.HideLoading();
- await JS.InvokeVoidAsync("alert", $"회원가입 실패: {message}");
+ LoadingService.HideLoading();
+ await JS.InvokeVoidAsync("alert", $"회원가입에 실패하였습니다.\n잠시 후 다시 시도해주세요.");
}
}
else
{
- MainLayout.HideLoading();
- Console.WriteLine($"API 호출 실패: {response.StatusCode}");
- var errorContent = await response.Content.ReadAsStringAsync();
- Console.WriteLine($"에러 내용: {errorContent}");
- await JS.InvokeVoidAsync("alert", "서버 오류가 발생했습니다.");
+ LoadingService.HideLoading();
+ await JS.InvokeVoidAsync("alert", "회원가입 중 오류가 발생했습니다.\n잠시 후 다시 시도해주세요.");
}
}
catch (Exception ex)
{
- MainLayout.HideLoading();
+ LoadingService.HideLoading();
Console.WriteLine($"예외 발생: {ex.Message}");
- await JS.InvokeVoidAsync("alert", $"오류가 발생했습니다: {ex.Message}");
+ await JS.InvokeVoidAsync("alert", "회원가입 중 오류가 발생했습니다.\n잠시 후 다시 시도해주세요.");
}
}
@@ -228,8 +253,16 @@ public partial class Register : ComponentBase
private async Task OpenDatePicker()
{
- if (birth == null) birth = DateTime.Now;
- await JS.InvokeVoidAsync("openDatePicker", dateInputRef);
+ try
+ {
+ if (birth == null) birth = DateTime.Now;
+ // showPicker 대신 click() 이벤트를 발생시켜 달력을 표시
+ await JS.InvokeVoidAsync("eval", $"document.querySelector('input[type=\"date\"]').click()");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"달력 표시 오류: {ex.Message}");
+ }
}
private void OnBirthChanged(ChangeEventArgs e)
{
diff --git a/Program/Views/Project/PageIndicator.razor b/Program/Views/Project/PageIndicator.razor
deleted file mode 100644
index 0ba87a6..0000000
--- a/Program/Views/Project/PageIndicator.razor
+++ /dev/null
@@ -1,11 +0,0 @@
-PageIndicator
-
-@if (Type == IndicateType.Page)
-{
-
-}
-
diff --git a/Program/Views/Project/PageIndicator.razor.cs b/Program/Views/Project/PageIndicator.razor.cs
deleted file mode 100644
index c116027..0000000
--- a/Program/Views/Project/PageIndicator.razor.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using Microsoft.AspNetCore.Components;
-
-namespace Front.Program.Views.Project;
-
-public partial class PageIndicator : ComponentBase
-{
- [Parameter]
- public IndicateType Type { get; set; } = IndicateType.Page;
-}
-
-public enum IndicateType
-{
- Page,
- Circle,
- Progress
-}
\ No newline at end of file
diff --git a/Program/Views/Project/TopNav.razor.cs b/Program/Views/Project/TopNav.razor.cs
deleted file mode 100644
index 100f5cb..0000000
--- a/Program/Views/Project/TopNav.razor.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using Microsoft.AspNetCore.Components;
-using Microsoft.JSInterop;
-
-namespace Front.Program.Views.Project;
-
-public partial class TopNav : ComponentBase
-{
- //로그인버튼을 누르면 페이지를 이동할거야
- [Inject]
- NavigationManager NavigationManager { get; set; } = default!;
- IJSRuntime JS { get; set; } = default!;
-
- protected bool isOpen = false;
-
- public void OnClickMenuDown()
- {
- isOpen = !isOpen;
- }
-
- public void OnClickRedirect()
- {
- if (isOpen) isOpen = !isOpen;
- NavigationManager.NavigateTo("/about");
- }
-
- public void OnClickLogin()
- {
- if (isOpen) isOpen = !isOpen;
- NavigationManager.NavigateTo("/auth");
- }
-
-
-
-}
\ No newline at end of file
diff --git a/_Imports.razor b/_Imports.razor
index 90b8058..bee8c13 100644
--- a/_Imports.razor
+++ b/_Imports.razor
@@ -5,11 +5,15 @@
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
+
@using Microsoft.JSInterop
@using Front
@using Front.Program.Layout
@using Front.Program.Views.Project
-@* @using Front.Program.Views.Academy *@
+@using Front.Program.Views.Project.Common
+@using Front.Program.Views.Project.ConnectUser
+
+@using Front.Program.Views.Academy
diff --git a/appsettings.json b/appsettings.json
new file mode 100644
index 0000000..c4e96df
--- /dev/null
+++ b/appsettings.json
@@ -0,0 +1,13 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "Security": {
+ "EncryptionKey": "AcaMate2025SecureKeySeanForEncryption19940509",
+ "EncryptionIV": "AcaMate2025IV9459"
+ }
+}
\ No newline at end of file
diff --git a/wwwroot/Resources/Images/Icon/Logout.png b/wwwroot/Resources/Images/Icon/Logout.png
new file mode 100644
index 0000000..f115553
Binary files /dev/null and b/wwwroot/Resources/Images/Icon/Logout.png differ
diff --git a/wwwroot/appsettings.Development.json b/wwwroot/appsettings.Development.json
index 2c5e8e1..9ec9044 100644
--- a/wwwroot/appsettings.Development.json
+++ b/wwwroot/appsettings.Development.json
@@ -1,3 +1,7 @@
{
- "ApiBaseUrl": "https://devacamate.ipstein.myds.me/"
+ "ApiBaseUrl": "https://devacamate.ipstein.myds.me/",
+ "Security": {
+ "EncryptionKey": "AcaMate2025SecureKeyForEncryptionSean1994",
+ "EncryptionIV": "AcaMate2025IV9459"
+ }
}
\ No newline at end of file
diff --git a/wwwroot/appsettings.Production.json b/wwwroot/appsettings.Production.json
index 2441834..bbfe0ea 100644
--- a/wwwroot/appsettings.Production.json
+++ b/wwwroot/appsettings.Production.json
@@ -1,3 +1,7 @@
{
- "ApiBaseUrl": "https://acamate.ipstein.myds.me/"
+ "ApiBaseUrl": "https://acamate.ipstein.myds.me/",
+ "Security": {
+ "EncryptionKey": "AcaMate2025SecureKeyForEncryptionSean1994",
+ "EncryptionIV": "AcaMate2025IV9459"
+ }
}
\ No newline at end of file
diff --git a/wwwroot/css/tailwind.css b/wwwroot/css/tailwind.css
index bad781b..21c5f5f 100644
--- a/wwwroot/css/tailwind.css
+++ b/wwwroot/css/tailwind.css
@@ -1 +1 @@
-*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width:480px){.container{max-width:480px}}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.bottom-0{bottom:0}.left-0{left:0}.left-4{left:1rem}.right-4{right:1rem}.top-0{top:0}.top-16{top:4rem}.z-50{z-index:50}.mx-auto{margin-left:auto;margin-right:auto}.my-2{margin-top:.5rem;margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.ml-2{margin-left:.5rem}.ml-4{margin-left:1rem}.mr-4{margin-right:1rem}.mt-4{margin-top:1rem}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.size-full{width:100%;height:100%}.h-10{height:2.5rem}.h-12{height:3rem}.h-14{height:3.5rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-8{height:2rem}.h-full{height:100%}.min-h-\[480px\]{min-height:480px}.min-h-screen{min-height:100vh}.w-48{width:12rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-full{width:100%}.min-w-0{min-width:0}.min-w-40{min-width:10rem}.min-w-\[84px\]{min-width:84px}.max-w-\[480px\]{max-width:480px}.max-w-\[720px\]{max-width:720px}.max-w-\[960px\]{max-width:960px}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.grow{flex-grow:1}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize-none{resize:none}.grid-cols-\[repeat\(auto-fit\2c minmax\(158px\2c 1fr\)\)\]{grid-template-columns:repeat(auto-fit,minmax(158px,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-10{gap:2.5rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.gap-9{gap:2.25rem}.gap-y-2{row-gap:.5rem}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.self-center{align-self:center}.overflow-hidden{overflow:hidden}.overflow-x-hidden{overflow-x:hidden}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-4{border-width:4px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-solid{border-style:solid}.border-\[\#dbe0e6\]{--tw-border-opacity:1;border-color:rgb(219 224 230/var(--tw-border-opacity,1))}.border-\[\#dde0e3\]{--tw-border-opacity:1;border-color:rgb(221 224 227/var(--tw-border-opacity,1))}.border-gray-600{--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity,1))}.border-text-border{--tw-border-opacity:1;border-color:rgb(198 198 198/var(--tw-border-opacity,1))}.border-b-\[\#f0f2f5\]{--tw-border-opacity:1;border-bottom-color:rgb(240 242 245/var(--tw-border-opacity,1))}.border-t-transparent{border-top-color:transparent}.bg-\[\#0c7ff2\]{--tw-bg-opacity:1;background-color:rgb(12 127 242/var(--tw-bg-opacity,1))}.bg-black\/70{background-color:rgba(0,0,0,.7)}.bg-black\/80{background-color:rgba(0,0,0,.8)}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity,1))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity,1))}.bg-gray-200\/60{background-color:rgba(229,231,235,.6)}.bg-gray-200\/80{background-color:rgba(229,231,235,.8)}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity,1))}.bg-second-darker{--tw-bg-opacity:1;background-color:rgb(42 35 27/var(--tw-bg-opacity,1))}.bg-second-normal{--tw-bg-opacity:1;background-color:rgb(121 101 78/var(--tw-bg-opacity,1))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.bg-yellow-100{--tw-bg-opacity:1;background-color:rgb(254 249 195/var(--tw-bg-opacity,1))}.bg-cover{background-size:cover}.bg-center{background-position:50%}.bg-no-repeat{background-repeat:no-repeat}.p-0{padding:0}.p-2{padding:.5rem}.p-4{padding:1rem}.p-\[12px\]{padding:12px}.p-\[15px\]{padding:15px}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-10{padding-top:2.5rem;padding-bottom:2.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.pb-10{padding-bottom:2.5rem}.pb-2{padding-bottom:.5rem}.pb-3{padding-bottom:.75rem}.pt-5{padding-top:1.25rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-sans{font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.text-2xl{font-size:1.5rem;line-height:2rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-6xl{font-size:3.75rem;line-height:1}.text-\[28px\]{font-size:28px}.text-\[32px\]{font-size:32px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-black{font-weight:900}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.leading-normal{line-height:1.5}.leading-tight{line-height:1.25}.tracking-\[-0\.015em\]{letter-spacing:-.015em}.tracking-\[-0\.033em\]{letter-spacing:-.033em}.tracking-\[0\.015em\]{letter-spacing:.015em}.tracking-widest{letter-spacing:.1em}.text-\[\#111418\]{--tw-text-opacity:1;color:rgb(17 20 24/var(--tw-text-opacity,1))}.text-\[\#60758a\]{--tw-text-opacity:1;color:rgb(96 117 138/var(--tw-text-opacity,1))}.text-blue-600{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity,1))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity,1))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity,1))}.text-normal-normal{--tw-text-opacity:1;color:rgb(235 223 210/var(--tw-text-opacity,1))}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity,1))}.text-red-800{--tw-text-opacity:1;color:rgb(153 27 27/var(--tw-text-opacity,1))}.text-text-border{--tw-text-opacity:1;color:rgb(198 198 198/var(--tw-text-opacity,1))}.text-text-title{--tw-text-opacity:1;color:rgb(29 29 29/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.underline{text-decoration-line:underline}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-150,.transition-all{transition-duration:.15s}.duration-300{transition-duration:.3s}@keyframes clipReveal{0%{clip-path:inset(0 100% 0 0)}to{clip-path:inset(0 0 0 0)}}.clip-reveal{animation:clipReveal 2.5s ease forwards}@keyframes blink{0%,to{opacity:1}50%{opacity:0}}.animate-blink{animation:blink 1.2s infinite}.mask-logo{-webkit-mask-image:url(/logo.png);-webkit-mask-size:cover;-webkit-mask-repeat:no-repeat;mask-image:url(/logo.png);mask-size:cover;mask-repeat:no-repeat;-webkit-mask-position:center;mask-position:center}.placeholder\:text-\[\#6a7581\]::-moz-placeholder{--tw-text-opacity:1;color:rgb(106 117 129/var(--tw-text-opacity,1))}.placeholder\:text-\[\#6a7581\]::placeholder{--tw-text-opacity:1;color:rgb(106 117 129/var(--tw-text-opacity,1))}.hover\:bg-second-dark:hover{--tw-bg-opacity:1;background-color:rgb(93 76 59/var(--tw-bg-opacity,1))}.hover\:text-blue-600:hover{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity,1))}.hover\:brightness-90:hover{--tw-brightness:brightness(.9);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.focus\:border-\[\#dde0e3\]:focus{--tw-border-opacity:1;border-color:rgb(221 224 227/var(--tw-border-opacity,1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:outline-0:focus{outline-width:0}.focus\:ring-0:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}@media (min-width:480px){.xs\:h-12{height:3rem}.xs\:flex-row{flex-direction:row}.xs\:justify-around{justify-content:space-around}.xs\:gap-8{gap:2rem}.xs\:rounded-lg{border-radius:.5rem}.xs\:px-10{padding-left:2.5rem;padding-right:2.5rem}.xs\:px-5{padding-left:1.25rem;padding-right:1.25rem}.xs\:text-4xl{font-size:2.25rem;line-height:2.5rem}.xs\:text-5xl{font-size:3rem;line-height:1}.xs\:text-base{font-size:1rem;line-height:1.5rem}.xs\:font-black{font-weight:900}.xs\:font-bold{font-weight:700}.xs\:leading-normal{line-height:1.5}.xs\:leading-tight{line-height:1.25}.xs\:tracking-\[-0\.033em\]{letter-spacing:-.033em}.xs\:tracking-\[0\.015em\]{letter-spacing:.015em}}@media (min-width:640px){.sm\:p-6{padding:1.5rem}}@media (min-width:768px){.md\:block{display:block}.md\:flex{display:flex}.md\:hidden{display:none}.md\:w-1\/2{width:50%}.md\:w-64{width:16rem}.md\:flex-row{flex-direction:row}.md\:flex-row-reverse{flex-direction:row-reverse}.md\:p-8{padding:2rem}.md\:px-40{padding-left:10rem;padding-right:10rem}}
\ No newline at end of file
+*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width:480px){.container{max-width:480px}}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.bottom-0{bottom:0}.left-0{left:0}.left-1\/2{left:50%}.left-4{left:1rem}.left-64{left:16rem}.right-0{right:0}.right-4{right:1rem}.top-0{top:0}.top-1\/2{top:50%}.top-16{top:4rem}.top-full{top:100%}.z-10{z-index:10}.z-50{z-index:50}.mx-auto{margin-left:auto;margin-right:auto}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-4{margin-top:1rem}.mb-4,.my-4{margin-bottom:1rem}.ml-2{margin-left:.5rem}.ml-4{margin-left:1rem}.mr-4{margin-right:1rem}.mt-16{margin-top:4rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.size-full{width:100%;height:100%}.h-10{height:2.5rem}.h-12{height:3rem}.h-14{height:3.5rem}.h-4{height:1rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-8{height:2rem}.h-\[72px\]{height:72px}.h-full{height:100%}.max-h-60{max-height:15rem}.max-h-\[180px\]{max-height:180px}.min-h-\[218px\]{min-height:218px}.min-h-\[480px\]{min-height:480px}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-4{width:1rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-8{width:2rem}.w-full{width:100%}.min-w-0{min-width:0}.min-w-40{min-width:10rem}.min-w-\[84px\]{min-width:84px}.max-w-\[480px\]{max-width:480px}.max-w-\[720px\]{max-width:720px}.max-w-\[960px\]{max-width:960px}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.grow{flex-grow:1}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-y-1\/2{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y:-50%}.rotate-180{--tw-rotate:180deg}.rotate-180,.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize-none{resize:none}.grid-cols-\[repeat\(auto-fit\2c minmax\(158px\2c 1fr\)\)\]{grid-template-columns:repeat(auto-fit,minmax(158px,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-10{gap:2.5rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.gap-9{gap:2.25rem}.gap-y-2{row-gap:.5rem}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.self-center{align-self:center}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-4{border-width:4px}.border-b{border-bottom-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-solid{border-style:solid}.border-\[\#dbe0e6\]{--tw-border-opacity:1;border-color:rgb(219 224 230/var(--tw-border-opacity,1))}.border-\[\#dde0e3\]{--tw-border-opacity:1;border-color:rgb(221 224 227/var(--tw-border-opacity,1))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity,1))}.border-gray-600{--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity,1))}.border-text-border{--tw-border-opacity:1;border-color:rgb(198 198 198/var(--tw-border-opacity,1))}.border-text-detail{--tw-border-opacity:1;border-color:rgb(84 84 84/var(--tw-border-opacity,1))}.border-text-disabled{--tw-border-opacity:1;border-color:rgb(142 142 142/var(--tw-border-opacity,1))}.border-b-\[\#f0f2f5\]{--tw-border-opacity:1;border-bottom-color:rgb(240 242 245/var(--tw-border-opacity,1))}.border-t-transparent{border-top-color:transparent}.bg-\[\#0c7ff2\]{--tw-bg-opacity:1;background-color:rgb(12 127 242/var(--tw-bg-opacity,1))}.bg-black\/70{background-color:rgba(0,0,0,.7)}.bg-black\/80{background-color:rgba(0,0,0,.8)}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity,1))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity,1))}.bg-gray-200\/60{background-color:rgba(229,231,235,.6)}.bg-gray-200\/80{background-color:rgba(229,231,235,.8)}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity,1))}.bg-normal-normal{--tw-bg-opacity:1;background-color:rgb(235 223 210/var(--tw-bg-opacity,1))}.bg-second-darker{--tw-bg-opacity:1;background-color:rgb(42 35 27/var(--tw-bg-opacity,1))}.bg-second-normal{--tw-bg-opacity:1;background-color:rgb(121 101 78/var(--tw-bg-opacity,1))}.bg-second-normal\/10{background-color:rgba(121,101,78,.1)}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.bg-white\/35{background-color:hsla(0,0%,100%,.35)}.bg-yellow-100{--tw-bg-opacity:1;background-color:rgb(254 249 195/var(--tw-bg-opacity,1))}.bg-contain{background-size:contain}.bg-cover{background-size:cover}.bg-center{background-position:50%}.bg-no-repeat{background-repeat:no-repeat}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-2{padding:.5rem}.p-4{padding:1rem}.p-\[12px\]{padding:12px}.p-\[15px\]{padding:15px}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-10{padding-top:2.5rem;padding-bottom:2.5rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.pb-10{padding-bottom:2.5rem}.pb-2{padding-bottom:.5rem}.pb-3{padding-bottom:.75rem}.pt-12{padding-top:3rem}.pt-24{padding-top:6rem}.pt-3{padding-top:.75rem}.pt-5{padding-top:1.25rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-sans{font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.text-2xl{font-size:1.5rem;line-height:2rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-6xl{font-size:3.75rem;line-height:1}.text-\[28px\]{font-size:28px}.text-\[32px\]{font-size:32px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-black{font-weight:900}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.leading-normal{line-height:1.5}.leading-tight{line-height:1.25}.tracking-\[-0\.015em\]{letter-spacing:-.015em}.tracking-\[-0\.033em\]{letter-spacing:-.033em}.tracking-\[0\.015em\]{letter-spacing:.015em}.tracking-widest{letter-spacing:.1em}.text-\[\#111418\]{--tw-text-opacity:1;color:rgb(17 20 24/var(--tw-text-opacity,1))}.text-\[\#111518\]{--tw-text-opacity:1;color:rgb(17 21 24/var(--tw-text-opacity,1))}.text-\[\#60758a\]{--tw-text-opacity:1;color:rgb(96 117 138/var(--tw-text-opacity,1))}.text-blue-600{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity,1))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity,1))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity,1))}.text-normal-normal{--tw-text-opacity:1;color:rgb(235 223 210/var(--tw-text-opacity,1))}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity,1))}.text-red-800{--tw-text-opacity:1;color:rgb(153 27 27/var(--tw-text-opacity,1))}.text-text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity,1))}.text-text-border{--tw-text-opacity:1;color:rgb(198 198 198/var(--tw-text-opacity,1))}.text-text-detail{--tw-text-opacity:1;color:rgb(84 84 84/var(--tw-text-opacity,1))}.text-text-title{--tw-text-opacity:1;color:rgb(29 29 29/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.underline{text-decoration-line:underline}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-150,.transition-transform{transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}@keyframes clipReveal{0%{clip-path:inset(0 100% 0 0)}to{clip-path:inset(0 0 0 0)}}.clip-reveal{animation:clipReveal 2.5s ease forwards}@keyframes blink{0%,to{opacity:1}50%{opacity:0}}.animate-blink{animation:blink 1.2s infinite}.mask-logo{-webkit-mask-image:url(/logo.png);-webkit-mask-size:cover;-webkit-mask-repeat:no-repeat;mask-image:url(/logo.png);mask-size:cover;mask-repeat:no-repeat;-webkit-mask-position:center;mask-position:center}.placeholder\:text-\[\#6a7581\]::-moz-placeholder{--tw-text-opacity:1;color:rgb(106 117 129/var(--tw-text-opacity,1))}.placeholder\:text-\[\#6a7581\]::placeholder{--tw-text-opacity:1;color:rgb(106 117 129/var(--tw-text-opacity,1))}.last\:border-b-0:last-child{border-bottom-width:0}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity,1))}.hover\:bg-second-dark:hover{--tw-bg-opacity:1;background-color:rgb(93 76 59/var(--tw-bg-opacity,1))}.hover\:font-bold:hover{font-weight:700}.hover\:text-blue-600:hover{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity,1))}.hover\:text-text-black:hover{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity,1))}.hover\:brightness-0:hover{--tw-brightness:brightness(0)}.hover\:brightness-0:hover,.hover\:brightness-90:hover{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.hover\:brightness-90:hover{--tw-brightness:brightness(.9)}.hover\:brightness-\[0\.7\]:hover{--tw-brightness:brightness(0.7)}.hover\:brightness-\[0\.7\]:hover,.hover\:contrast-\[0\.8\]:hover{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.hover\:contrast-\[0\.8\]:hover{--tw-contrast:contrast(0.8)}.hover\:hue-rotate-\[180deg\]:hover{--tw-hue-rotate:hue-rotate(180deg)}.hover\:hue-rotate-\[180deg\]:hover,.hover\:invert:hover{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.hover\:invert:hover{--tw-invert:invert(100%)}.hover\:saturate-100:hover{--tw-saturate:saturate(1)}.hover\:saturate-100:hover,.hover\:sepia:hover{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.hover\:sepia:hover{--tw-sepia:sepia(100%)}.focus\:border-\[\#dde0e3\]:focus{--tw-border-opacity:1;border-color:rgb(221 224 227/var(--tw-border-opacity,1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:outline-0:focus{outline-width:0}.focus\:ring-0:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.group:hover .group-hover\:text-text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}@media (min-width:480px){.xs\:h-12{height:3rem}.xs\:flex-row{flex-direction:row}.xs\:justify-around{justify-content:space-around}.xs\:gap-8{gap:2rem}.xs\:rounded-lg{border-radius:.5rem}.xs\:px-10{padding-left:2.5rem;padding-right:2.5rem}.xs\:px-5{padding-left:1.25rem;padding-right:1.25rem}.xs\:text-4xl{font-size:2.25rem;line-height:2.5rem}.xs\:text-5xl{font-size:3rem;line-height:1}.xs\:text-base{font-size:1rem;line-height:1.5rem}.xs\:font-black{font-weight:900}.xs\:font-bold{font-weight:700}.xs\:leading-normal{line-height:1.5}.xs\:leading-tight{line-height:1.25}.xs\:tracking-\[-0\.033em\]{letter-spacing:-.033em}.xs\:tracking-\[0\.015em\]{letter-spacing:.015em}}@media (min-width:640px){.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}}@media (min-width:768px){.md\:ml-64{margin-left:16rem}.md\:block{display:block}.md\:inline{display:inline}.md\:flex{display:flex}.md\:hidden{display:none}.md\:h-\[300px\]{height:300px}.md\:w-1\/2{width:50%}.md\:flex-row{flex-direction:row}.md\:flex-row-reverse{flex-direction:row-reverse}.md\:bg-contain{background-size:contain}.md\:px-10{padding-left:2.5rem;padding-right:2.5rem}.md\:px-40{padding-left:10rem;padding-right:10rem}.md\:px-8{padding-left:2rem;padding-right:2rem}}@media (min-width:1024px){.lg\:px-24{padding-left:6rem;padding-right:6rem}}@media (min-width:1280px){.xl\:px-40{padding-left:10rem;padding-right:10rem}}
\ No newline at end of file
diff --git a/wwwroot/index.html b/wwwroot/index.html
index 71445ee..ce6093d 100644
--- a/wwwroot/index.html
+++ b/wwwroot/index.html
@@ -12,7 +12,7 @@
-
+
@@ -48,12 +48,20 @@
Blazor.start().then(() => {
const elapsed = performance.now() - loadingStart;
const remaining = Math.max(0, MIN_LOADING_MS - elapsed);
- setTimeout(() => {
- const overlay = document.getElementById('loading-overlay');
- if (overlay) overlay.remove();
- }, remaining);
+
+ const overlay = document.getElementById('loading-overlay');
+ const showOnPath = overlay?.getAttribute('data-show-on');
+
+ if (overlay && showOnPath && window.location.pathname === showOnPath) {
+ overlay.classList.remove('hidden');
+ setTimeout(() => {
+ if (overlay) overlay.remove();
+ }, remaining);
+ } else if (overlay) {
+ overlay.remove();
+ }
}).catch(err => {
- console.error("❌ Blazor 로딩 실패", err);
+ console.error("Blazor 로딩 실패", err);
});
@@ -62,6 +70,7 @@
const v = document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)');
return v ? v.pop() : '';
};
+
window.setCookie = function (name, value, days) {
let expires = "";
if (days) {
diff --git a/wwwroot/scripts/apiSender.js b/wwwroot/scripts/apiSender.js
index 0afdaf1..b25c113 100644
--- a/wwwroot/scripts/apiSender.js
+++ b/wwwroot/scripts/apiSender.js
@@ -11,13 +11,101 @@ window.postWithHeader = function(url, method, headerKey, headerValue) {
});
};
-window.fetchWithHeaderAndReturnUrl = async function(url, method, headerKey, headerValue) {
- const response = await fetch(url, {
- method: method,
- headers: {
- [headerKey]: headerValue
+window.fetchWithHeaderAndReturnUrl = async function(args) {
+ try {
+ let url = args.url;
+ const queryParams = Object.entries(args)
+ .filter(([key]) => !['url', 'method', 'headerKey', 'headerValue'].includes(key))
+ .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
+ .join('&');
+ if (queryParams) {
+ url += (url.includes('?') ? '&' : '?') + queryParams;
}
- });
- const data = await response.json();
- return data.url;
-};
\ No newline at end of file
+
+ const response = await fetch(url, {
+ method: args.method,
+ headers: {
+ [args.headerKey]: args.headerValue
+ }
+ });
+
+ const contentType = response.headers.get('content-type');
+ if (!response.ok) {
+ console.error('API 호출 실패:', response.status, response.statusText);
+ return null;
+ }
+ if (!contentType || !contentType.includes('application/json')) {
+ const text = await response.text();
+ console.error('JSON이 아닌 응답:', text);
+ return null;
+ }
+
+ const data = await response.json();
+ console.log('API 응답 데이터:', data);
+ return data;
+ } catch (error) {
+ console.error('API 호출 중 오류 발생:', error);
+ return null;
+ }
+};
+
+window.loadConfig = async function(configFile) {
+ try {
+ console.log('설정 파일 로드 시도:', configFile);
+ const response = await fetch(configFile);
+ if (!response.ok) {
+ console.error('설정 파일 로드 실패:', response.status, response.statusText);
+ return null;
+ }
+ const config = await response.json();
+ console.log('설정 파일 로드 성공:', configFile);
+ return config;
+ } catch (error) {
+ console.error('설정 파일 로드 중 오류 발생:', error);
+ return null;
+ }
+};
+
+window.encryptText = async function(text, key, iv) {
+ try {
+ // XOR 암호화 구현
+ let result = '';
+ for (let i = 0; i < text.length; i++) {
+ const charCode = text.charCodeAt(i) ^ key.charCodeAt(i % key.length);
+ result += String.fromCharCode(charCode);
+ }
+
+ // UTF-8로 인코딩 후 Base64로 변환
+ const utf8Encoder = new TextEncoder();
+ const bytes = utf8Encoder.encode(result);
+ const base64 = btoa(String.fromCharCode.apply(null, bytes));
+ return base64;
+ } catch (error) {
+ console.error('암호화 중 오류 발생:', error);
+ throw error;
+ }
+};
+
+window.decryptText = async function(encryptedText, key, iv) {
+ try {
+ // Base64 디코딩 후 UTF-8 디코딩
+ const binary = atob(encryptedText);
+ const bytes = new Uint8Array(binary.length);
+ for (let i = 0; i < binary.length; i++) {
+ bytes[i] = binary.charCodeAt(i);
+ }
+ const utf8Decoder = new TextDecoder();
+ const text = utf8Decoder.decode(bytes);
+
+ // XOR 복호화
+ let result = '';
+ for (let i = 0; i < text.length; i++) {
+ const charCode = text.charCodeAt(i) ^ key.charCodeAt(i % key.length);
+ result += String.fromCharCode(charCode);
+ }
+ return result;
+ } catch (error) {
+ console.error('복호화 중 오류 발생:', error);
+ throw error;
+ }
+};