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) + { + +
+
+ +
+
+ @Body +
+
+ } + 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 + { +
+

+ @UserStateService.UserData.Name 님, 안녕하세요!
+ 학원을 선택해주세요.
+

+
+ @foreach (var academy in academyItems) + { + +
+ @academy.business_name + + + +
+
+ } +
+
+ } + + @*
*@ +
+ + +

+ © 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 @@ +
+
+
+ 프로필 +
+
+ +
AcaMate
+ +

Parent

+
+
+
+ + + \ 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) + { +
+
+

학원 정보

+
+
+ @foreach (var academy in academyItems) + { + + @academy.business_name + + } +
+
+ } +
+ + +
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 @@ - -
+ +
@@ -179,6 +179,19 @@

© 2024 AcaMate. All rights reserved.

-
+ + + @* *@ diff --git a/Program/Views/Project/About.razor.cs b/Program/Views/Project/About.razor.cs index 3c24ebd..6c5e0df 100644 --- a/Program/Views/Project/About.razor.cs +++ b/Program/Views/Project/About.razor.cs @@ -1,16 +1,71 @@ using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Routing; + +using Front.Program.ViewModels; using Front.Program.Services; namespace Front.Program.Views.Project; -public partial class About : ComponentBase +public partial class About : ComponentBase, IDisposable { [Inject] - NavigationManager NavigationManager { get; set; } = default!; - private async Task OnClickEvent() - { - // NavigationManager.NavigateTo("/redirectpage"); - Console.WriteLine("Redirecting to redirect page"); + 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 override void OnInitialized() + { + Navigation.LocationChanged += HandleLocationChanged; + HandleLocationChanged(this, new LocationChangedEventArgs(Navigation.Uri, false)); + } + + 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; + } } } \ No newline at end of file diff --git a/Program/Views/Project/Auth.razor b/Program/Views/Project/Auth.razor deleted file mode 100644 index 51f2e88..0000000 --- a/Program/Views/Project/Auth.razor +++ /dev/null @@ -1,12 +0,0 @@ -@page "/auth" - -
-

로그인

-
- -
-
\ No newline at end of file diff --git a/Program/Views/Project/Auth.razor.cs b/Program/Views/Project/Auth.razor.cs deleted file mode 100644 index 6861e31..0000000 --- a/Program/Views/Project/Auth.razor.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Net.Http.Json; -using System.Text.Json; -using Front.Program.Services; -using Microsoft.AspNetCore.Components; -using Microsoft.JSInterop; - -namespace Front.Program.Views.Project; - -public partial class Auth : ComponentBase -{ - [Inject] NavigationManager NavigationManager { get; set; } = default!; - [Inject] LoadingService LoadingService { get; set; } = default!; - // [Inject] IJSRuntime JS { get; set; } = default!; - // [Inject] CookieService Cookie { get; set; } = default!; - [Inject] HttpClient Http { get; set; } = default!; - - public async Task KakaoLogin() - { - LoadingService.ShowLoading(); - var url = "/api/v1/out/user/kakao/auth"; - var response = await Http.GetFromJsonAsync(url); - var kakaoUrl = response.GetProperty("url").GetString(); - Console.WriteLine(kakaoUrl); - if (!string.IsNullOrEmpty(kakaoUrl)) - { - NavigationManager.NavigateTo(kakaoUrl, true); - } - } -} \ No newline at end of file diff --git a/Program/Views/Project/Common/PageIndicator.razor b/Program/Views/Project/Common/PageIndicator.razor new file mode 100644 index 0000000..0d4fb88 --- /dev/null +++ b/Program/Views/Project/Common/PageIndicator.razor @@ -0,0 +1,5 @@ +
+
+
+
+
\ No newline at end of file diff --git a/Program/Views/Project/Common/PageIndicator.razor.cs b/Program/Views/Project/Common/PageIndicator.razor.cs new file mode 100644 index 0000000..3669762 --- /dev/null +++ b/Program/Views/Project/Common/PageIndicator.razor.cs @@ -0,0 +1,7 @@ +using Front.Program.Models; +using Microsoft.AspNetCore.Components; + +namespace Front.Program.Views.Project.Common; + +public partial class PageIndicator : ComponentBase +{ } diff --git a/Program/Views/Project/RedirectPage.razor b/Program/Views/Project/Common/RedirectPage.razor similarity index 100% rename from Program/Views/Project/RedirectPage.razor rename to Program/Views/Project/Common/RedirectPage.razor diff --git a/Program/Views/Project/RedirectPage.razor.cs b/Program/Views/Project/Common/RedirectPage.razor.cs similarity index 70% rename from Program/Views/Project/RedirectPage.razor.cs rename to Program/Views/Project/Common/RedirectPage.razor.cs index 05c2e91..2f79166 100644 --- a/Program/Views/Project/RedirectPage.razor.cs +++ b/Program/Views/Project/Common/RedirectPage.razor.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Components; -namespace Front.Program.Views.Project; +namespace Front.Program.Views.Project.Common; public partial class RedirectPage : ComponentBase { @@ -9,6 +9,6 @@ public partial class RedirectPage : ComponentBase protected override void OnInitialized() { - NavigationManager.NavigateTo("/about",true); + NavigationManager.NavigateTo("/am/intro",true); } } \ No newline at end of file diff --git a/Program/Views/Project/TopBanner.razor b/Program/Views/Project/Common/TopBanner.razor similarity index 100% rename from Program/Views/Project/TopBanner.razor rename to Program/Views/Project/Common/TopBanner.razor diff --git a/Program/Views/Project/TopBanner.razor.cs b/Program/Views/Project/Common/TopBanner.razor.cs similarity index 66% rename from Program/Views/Project/TopBanner.razor.cs rename to Program/Views/Project/Common/TopBanner.razor.cs index e677b5a..3cf6671 100644 --- a/Program/Views/Project/TopBanner.razor.cs +++ b/Program/Views/Project/Common/TopBanner.razor.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Components; -namespace Front.Program.Views.Project; +namespace Front.Program.Views.Project.Common; public partial class TopBanner : ComponentBase { diff --git a/Program/Views/Project/TopNav.razor b/Program/Views/Project/Common/TopProjectNav.razor similarity index 54% rename from Program/Views/Project/TopNav.razor rename to Program/Views/Project/Common/TopProjectNav.razor index a1d9c43..b3636f3 100644 --- a/Program/Views/Project/TopNav.razor +++ b/Program/Views/Project/Common/TopProjectNav.razor @@ -11,13 +11,28 @@ Join What's New - + @if (!UserStateService.isLogin) + { + + } + else + { + + } + + + + @if (!UserStateService.isLogin) + { + + } + else + { + + } } diff --git a/Program/Views/Project/Common/TopProjectNav.razor.cs b/Program/Views/Project/Common/TopProjectNav.razor.cs new file mode 100644 index 0000000..1f0d4d4 --- /dev/null +++ b/Program/Views/Project/Common/TopProjectNav.razor.cs @@ -0,0 +1,57 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; +using System; +using System.Net.Http.Json; +using System.Text.Json; +using Front.Program.Models; +using Front.Program.Services; +using Front.Program.ViewModels; + +namespace Front.Program.Views.Project.Common; + +public partial class TopProjectNav : ComponentBase +{ + [Inject] NavigationManager NavigationManager { get; set; } = default!; + + [Inject] UserStateService UserStateService { get; set; } = default!; + + [Inject] IJSRuntime JS { get; set; } = default!; + + [Inject] HttpClient Http { get; set; } = default!; + + protected bool isOpen = false; + + protected override async Task OnInitializedAsync() + { + Console.WriteLine("TOPNAV_OnInitializedAsync"); + + if (!UserStateService.isFirstCheck) await UserStateService.GetUserDataAsync(); + } + + 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"); + } + + public async Task OnClickLogout() + { + await UserStateService.ClearUserData(); + // + // if (await UserViewModel.ClearUserData()) + // { + // isLogin = false; + // } + } +} \ No newline at end of file diff --git a/Program/Views/Project/ConnectUser/Auth.razor b/Program/Views/Project/ConnectUser/Auth.razor new file mode 100644 index 0000000..cdb9e14 --- /dev/null +++ b/Program/Views/Project/ConnectUser/Auth.razor @@ -0,0 +1,26 @@ +@page "/auth" +@page "/am/auth" + +
+

로그인

+
+ @if (NavigationManager.Uri.Contains("/am/auth")) + { + + } + else + { + + } +
+
\ No newline at end of file diff --git a/Program/Views/Project/ConnectUser/Auth.razor.cs b/Program/Views/Project/ConnectUser/Auth.razor.cs new file mode 100644 index 0000000..b603f61 --- /dev/null +++ b/Program/Views/Project/ConnectUser/Auth.razor.cs @@ -0,0 +1,57 @@ +using System.Net.Http.Json; +using System.Text.Json; +using Front.Program.Services; +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; + +namespace Front.Program.Views.Project.ConnectUser; + +public partial class Auth : ComponentBase, IDisposable +{ + [Inject] NavigationManager NavigationManager { get; set; } = default!; + [Inject] LoadingService LoadingService { get; set; } = default!; + [Inject] HttpClient Http { get; set; } = default!; + [Inject] IJSRuntime JS { get; set; } = default!; + + protected override void OnInitialized() + { + // LocationChanged 이벤트 구독 + NavigationManager.LocationChanged += HandleLocationChanged; + } + + private void HandleLocationChanged(object? sender, Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs e) + { + // 페이지 이동이 발생했을 때 로딩 상태 해제 + Console.WriteLine($"페이지 이동 감지: {NavigationManager.Uri}"); + LoadingService.HideLoading(); + } + + public void Dispose() + { + // 이벤트 구독 해제 + NavigationManager.LocationChanged -= HandleLocationChanged; + } + + public async Task KakaoLogin(string? path = null) + { + try + { + LoadingService.ShowLoading(); + + var url = $"/api/v1/out/user/kakao/auth?redirectPath={Uri.EscapeDataString(path ?? "/about")}"; + var response = await Http.GetFromJsonAsync(url); + var kakaoUrl = response.GetProperty("url").GetString(); + + if (!string.IsNullOrEmpty(kakaoUrl)) + { + // JavaScript를 통해 페이지 이동 + await JS.InvokeVoidAsync("eval", $"window.location.replace('{kakaoUrl}')"); + } + } + catch (Exception ex) + { + Console.WriteLine($"카카오 로그인 오류: {ex.Message}"); + LoadingService.HideLoading(); + } + } +} \ No newline at end of file diff --git a/Program/Views/Project/Register.razor b/Program/Views/Project/ConnectUser/Register.razor similarity index 75% rename from Program/Views/Project/Register.razor rename to Program/Views/Project/ConnectUser/Register.razor index 76b6dae..1b4b74e 100644 --- a/Program/Views/Project/Register.razor +++ b/Program/Views/Project/ConnectUser/Register.razor @@ -21,7 +21,7 @@ @@ -31,15 +31,41 @@
@@ -49,7 +75,7 @@ *

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