From 3ffec93958dc6295f5e02adab535763669712609 Mon Sep 17 00:00:00 2001 From: "Seonkyu.kim" Date: Mon, 9 Jun 2025 17:45:53 +0900 Subject: [PATCH 1/6] =?UTF-8?q?[=E2=9C=A8]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=EB=B0=8F=20=ED=99=94=EB=A9=B4=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 회원가입 후 자동 로그인 2. 로그인 후 페이지 처리 3. 로딩 인디케이터 동작 구조 변경 --- Program/Layout/MainLayout.razor | 5 +- Program/Layout/MainLayout.razor.cs | 19 ++---- Program/Views/Project/Auth.razor | 24 +++++-- Program/Views/Project/Auth.razor.cs | 50 +++++++++++---- Program/Views/Project/Register.razor | 48 ++++++++++---- Program/Views/Project/Register.razor.cs | 62 ++++++++++++++----- .../{TopNav.razor => TopProjectNav.razor} | 25 +++++--- ...TopNav.razor.cs => TopProjectNav.razor.cs} | 16 +++-- wwwroot/index.html | 20 ++++-- 9 files changed, 192 insertions(+), 77 deletions(-) rename Program/Views/Project/{TopNav.razor => TopProjectNav.razor} (75%) rename Program/Views/Project/{TopNav.razor.cs => TopProjectNav.razor.cs} (59%) diff --git a/Program/Layout/MainLayout.razor b/Program/Layout/MainLayout.razor index 337b557..d784598 100644 --- a/Program/Layout/MainLayout.razor +++ b/Program/Layout/MainLayout.razor @@ -5,9 +5,10 @@ @* *@ - @if (!isHideTop) + + @if (!isHidePrjTop) { - + } diff --git a/Program/Layout/MainLayout.razor.cs b/Program/Layout/MainLayout.razor.cs index 766970f..4f8cb25 100644 --- a/Program/Layout/MainLayout.razor.cs +++ b/Program/Layout/MainLayout.razor.cs @@ -13,11 +13,11 @@ public partial class MainLayout : LayoutComponentBase, IDisposable [Inject] LoadingService LoadingService { get; set; } = default!; - // protected bool isHideTop => Navigation.Uri.Contains("/auth"); - protected bool isHideTop => Navigation.ToBaseRelativePath(Navigation.Uri).Equals("auth", StringComparison.OrdinalIgnoreCase); + // 경로의 시작 부분 + // protected bool isHidePrjTop => Navigation.ToBaseRelativePath(Navigation.Uri).StartsWith("auth", StringComparison.OrdinalIgnoreCase); - public static bool IsLoading { get; set; } - public static IndicateType CurrentType { get; set; } = IndicateType.Page; + // 경로의 끝 부분 + protected bool isHidePrjTop => Navigation.ToBaseRelativePath(Navigation.Uri).EndsWith("auth", StringComparison.OrdinalIgnoreCase); protected override void OnInitialized() { @@ -35,15 +35,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/Views/Project/Auth.razor b/Program/Views/Project/Auth.razor index 51f2e88..64926a8 100644 --- a/Program/Views/Project/Auth.razor +++ b/Program/Views/Project/Auth.razor @@ -1,12 +1,26 @@ @page "/auth" +@page "/academy/auth"

로그인

- + @if (NavigationManager.Uri.Contains("/academy/auth")) + { + + } + else + { + + }
\ No newline at end of file diff --git a/Program/Views/Project/Auth.razor.cs b/Program/Views/Project/Auth.razor.cs index 6861e31..56ae901 100644 --- a/Program/Views/Project/Auth.razor.cs +++ b/Program/Views/Project/Auth.razor.cs @@ -6,24 +6,52 @@ using Microsoft.JSInterop; namespace Front.Program.Views.Project; -public partial class Auth : ComponentBase +public partial class Auth : ComponentBase, IDisposable { [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!; + [Inject] IJSRuntime JS { get; set; } = default!; - public async Task KakaoLogin() + protected override void OnInitialized() { - 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)) + // 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 { - NavigationManager.NavigateTo(kakaoUrl, true); + 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/Register.razor index 76b6dae..1b4b74e 100644 --- a/Program/Views/Project/Register.razor +++ b/Program/Views/Project/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/Register.razor.cs index 49e1a94..903cdce 100644 --- a/Program/Views/Project/Register.razor.cs +++ b/Program/Views/Project/Register.razor.cs @@ -5,6 +5,7 @@ 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; @@ -14,10 +15,14 @@ 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!; private ElementReference dateInputRef; private string name = ""; + private string birthYear = ""; + private string birthMonth = ""; + private string birthDay = ""; private DateTime? birth; private string email = ""; private string phone = ""; @@ -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,7 +168,7 @@ public partial class Register : ComponentBase try { - MainLayout.ShowLoading(); + LoadingService.ShowLoading(); // 쿠키에서 토큰 가져오기 var token = await JS.InvokeAsync("eval", "document.cookie.split('; ').find(row => row.startsWith('Web_AM_Connect_Key='))?.split('=')[1] || ''"); Console.WriteLine($"쿠키에서 가져온 토큰: '{token}'"); @@ -149,7 +178,7 @@ public partial class Register : ComponentBase await JS.InvokeVoidAsync("alert", "인증 정보가 없습니다."); NavigationManager.NavigateTo("/"); - MainLayout.HideLoading(); + LoadingService.HideLoading(); return; } @@ -186,30 +215,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 +254,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/TopNav.razor b/Program/Views/Project/TopProjectNav.razor similarity index 75% rename from Program/Views/Project/TopNav.razor rename to Program/Views/Project/TopProjectNav.razor index a1d9c43..00d8958 100644 --- a/Program/Views/Project/TopNav.razor +++ b/Program/Views/Project/TopProjectNav.razor @@ -11,13 +11,19 @@ Join What's New - + @if (!isLoggedIn) + { + + } + + + + @if (!isLoggedIn) + { + + } } diff --git a/Program/Views/Project/TopNav.razor.cs b/Program/Views/Project/TopProjectNav.razor.cs similarity index 59% rename from Program/Views/Project/TopNav.razor.cs rename to Program/Views/Project/TopProjectNav.razor.cs index 100f5cb..5279b02 100644 --- a/Program/Views/Project/TopNav.razor.cs +++ b/Program/Views/Project/TopProjectNav.razor.cs @@ -3,14 +3,23 @@ using Microsoft.JSInterop; namespace Front.Program.Views.Project; -public partial class TopNav : ComponentBase +public partial class TopProjectNav : ComponentBase { - //로그인버튼을 누르면 페이지를 이동할거야 [Inject] NavigationManager NavigationManager { get; set; } = default!; + + [Inject] IJSRuntime JS { get; set; } = default!; protected bool isOpen = false; + protected bool isLoggedIn = false; + + protected override async Task OnInitializedAsync() + { + // 쿠키에서 로그인 상태 확인 + var isLoginCookie = await JS.InvokeAsync("eval", "document.cookie.split('; ').find(row => row.startsWith('IsLogin='))?.split('=')[1]"); + isLoggedIn = isLoginCookie == "true"; + } public void OnClickMenuDown() { @@ -28,7 +37,4 @@ public partial class TopNav : ComponentBase if (isOpen) isOpen = !isOpen; NavigationManager.NavigateTo("/auth"); } - - - } \ No newline at end of file diff --git a/wwwroot/index.html b/wwwroot/index.html index 71445ee..f3eeec4 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -12,7 +12,7 @@ -
+ @@ -45,6 +54,13 @@ 시작하기 } + else + { + + }
} diff --git a/Program/Views/Project/TopProjectNav.razor.cs b/Program/Views/Project/TopProjectNav.razor.cs index a8533a1..00bf7bf 100644 --- a/Program/Views/Project/TopProjectNav.razor.cs +++ b/Program/Views/Project/TopProjectNav.razor.cs @@ -24,11 +24,12 @@ public partial class TopProjectNav : ComponentBase protected override async Task OnInitializedAsync() { Console.WriteLine("TopNAV 확인"); - isLogin = isLogin = bool.TryParse(await StorageService.GetItemAsync("IsLogin"), out var result) && result; - if (isLogin) return; + isLogin = bool.TryParse(await StorageService.GetItemAsync("IsLogin"), out var result) && result; + // if (!isLogin && !string.IsNullOrEmpty(UserName)) return; + if (!isLogin) return; // && !string.IsNullOrEmpty(UserName)) return; + try { - var encryptedName = await StorageService.GetItemAsync("USER"); Console.WriteLine($"{encryptedName}"); if (!string.IsNullOrEmpty(encryptedName)) @@ -103,6 +104,14 @@ public partial class TopProjectNav : ComponentBase } } } + else { + if (isLogin) { + isLogin = false; + await StorageService.SetItemAsync("IsLogin","false"); + } + + + } } } Console.WriteLine("로그인되지 않은 상태"); @@ -136,4 +145,19 @@ public partial class TopProjectNav : ComponentBase if (isOpen) isOpen = !isOpen; NavigationManager.NavigateTo("/auth"); } + public async Task OnClickLogout() + { + // var isRemoved = await StorageService.RemoveItemAsync("USER"); + if (await StorageService.RemoveItemAsync("IsLogin")) { + isLogin = false; + if (await StorageService.RemoveItemAsync("USER")) { + UserName = null; + Console.WriteLine("LOGOUT"); + } + } + else + { + Console.WriteLine("로그아웃 처리 중 오류가 발생했습니다."); + } + } } \ 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 0000000000000000000000000000000000000000..f115553602421eca01ac3003bcadab78789b86cf GIT binary patch literal 636 zcmV-?0)zdDP)K~#7F?b<<3 z!ax*&;Wv$O%@ObhUcm!M>IuX|h&x+WW;y^$DW#NBN^N1vT%2CVjgPmyXD!6V%VS_LoMxrI6So8Eg*R7m z^Zhk{D*kLjKqBgeW))r>L<*F2PTLS*97G&JrU!u{1+q*NvIOKwdYgT_<8UBP(xK8o z4jRZo1373Q2ho9&%T_<>w)((GY#@wd4Wz}wIpvHbGEmZqdl={EHD@Gof$q9z=N9tu z=KEZc#0APBPj`LB5lLJi8<`U@X1Vk8iX({&G_BQV;lIS0bJ|c-< zYH&xY#afIr(!X{T%ew8fEO1%n&P%={!Tlr~08wzO`LZJ^$nB0Yk%HY1%88HN!yG{d z2BS%qQmzee4k{4`dA*BqK1ajD6|VCGW#gb)j3BN!NPHm9I7oCL?l?$nARlp%$Uwg0 zAaQ|w#zCS2rR})kGY*m!Xf}Vb5ba!vR~-k*+q9p~XGNro&G$LtAeo!?7|Fu#NaBiv zBLOo#ALO8c95j%F26E6q4x}AE*5_Fp_U-gOX#^GD_!$C4 z`xmpYfIyJ~9ezgRzn**787a_u740}2P*>!;7@nXQP8LVFk~Uz1os*PON-3q3TKfW; W6$thA25w3K0000: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}.right-4{right:1rem}.top-0{top:0}.top-1\/2{top:50%}.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-10{width:2.5rem}.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}.-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%}.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-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-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))}.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-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)}@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 From c371700e78a9e913b4519728757b0119be949d69 Mon Sep 17 00:00:00 2001 From: SEAN-59 Date: Fri, 13 Jun 2025 17:53:58 +0900 Subject: [PATCH 4/6] =?UTF-8?q?[=E2=9C=A8]=20=ED=95=99=EC=9B=90=EC=9A=A9?= =?UTF-8?q?=20=EC=9E=85=EC=9E=A5=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. /am/... 도메인 생성 2. /am/intro 페이지 생성 3. 메인 레이아웃 상황 따라 변경 4. 로그인 정보 받아오는 로직 변경 중 4.1. 유저 정보에 대한 JSON은 받아왔으나 이를 저장하는 로직 구현 중 4.2. 로그인 정보를 받아오는 로직을 어디서 구현해야 할 지 고민 하는 중 --- Program.cs | 1 + Program/Layout/MainLayout.razor | 39 ++-- Program/Layout/MainLayout.razor.cs | 2 + Program/Services/UserService.cs | 133 +++++++++++ Program/Views/Academy/AcademyIntro.razor | 30 +++ Program/Views/Academy/AcademyIntro.razor.cs | 7 + Program/Views/Academy/LeftSideAcademy.razor | 70 ++++++ .../Views/Academy/LeftSideAcademy.razor.cs | 7 + Program/Views/Academy/TopNavAcademy.razor | 18 ++ Program/Views/Academy/TopNavAcademy.razor.cs | 6 + Program/Views/Project/About.razor | 19 +- Program/Views/Project/Auth.razor | 2 +- Program/Views/Project/PageIndicator.razor | 2 - Program/Views/Project/RedirectPage.razor.cs | 2 +- Program/Views/Project/TopProjectNav.razor.cs | 220 +++++++++--------- _Imports.razor | 2 +- 16 files changed, 430 insertions(+), 130 deletions(-) create mode 100644 Program/Services/UserService.cs create mode 100644 Program/Views/Academy/AcademyIntro.razor create mode 100644 Program/Views/Academy/AcademyIntro.razor.cs create mode 100644 Program/Views/Academy/LeftSideAcademy.razor create mode 100644 Program/Views/Academy/LeftSideAcademy.razor.cs create mode 100644 Program/Views/Academy/TopNavAcademy.razor create mode 100644 Program/Views/Academy/TopNavAcademy.razor.cs diff --git a/Program.cs b/Program.cs index 5ad3930..8608b7e 100644 --- a/Program.cs +++ b/Program.cs @@ -38,6 +38,7 @@ builder.Services.AddScoped(); // builder.Services.AddRazorPages(); // builder.Services.AddServerSideBlazor(); builder.Services.AddScoped(); +builder.Services.AddScoped(); await builder.Build().RunAsync(); diff --git a/Program/Layout/MainLayout.razor b/Program/Layout/MainLayout.razor index d784598..d99eecd 100644 --- a/Program/Layout/MainLayout.razor +++ b/Program/Layout/MainLayout.razor @@ -1,30 +1,39 @@ -@inherits LayoutComponentBase +@inherits LayoutComponentBase @implements IDisposable
@* *@ - - @if (!isHidePrjTop) + + @if(isAcademy) { - +
+ + +
+
+ +
+
+ @Body +
+
+
} - - -
- - @* *@ - @* *@ - + else + { + @if (!isHidePrjTop) + { + + } - @*
*@
@Body
-
+ } diff --git a/Program/Layout/MainLayout.razor.cs b/Program/Layout/MainLayout.razor.cs index cdfeb2a..bc7af06 100644 --- a/Program/Layout/MainLayout.razor.cs +++ b/Program/Layout/MainLayout.razor.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Routing; using Front.Program.Views.Project; +using Front.Program.Views.Academy; using Front.Program.Services; namespace Front.Program.Layout; @@ -22,6 +23,7 @@ public partial class MainLayout : LayoutComponentBase, IDisposable // 경로의 끝 부분 protected bool isHidePrjTop => Navigation.ToBaseRelativePath(Navigation.Uri).EndsWith("auth", StringComparison.OrdinalIgnoreCase); + protected bool isAcademy => Navigation.ToBaseRelativePath(Navigation.Uri).StartsWith("am", StringComparison.OrdinalIgnoreCase); protected override void OnInitialized() { diff --git a/Program/Services/UserService.cs b/Program/Services/UserService.cs new file mode 100644 index 0000000..29595fc --- /dev/null +++ b/Program/Services/UserService.cs @@ -0,0 +1,133 @@ +using System.Text.Json; +using Microsoft.JSInterop; + +namespace Front.Program.Services; + +public class UserService +{ + private readonly StorageService _storageService; + private readonly SecureService _secureService; + private readonly IJSRuntime _js; + private readonly ILogger _logger; + + public UserService( + StorageService storageService, + SecureService secureService, + IJSRuntime js, + ILogger logger) + { + _storageService = storageService; + _secureService = secureService; + _js = js; + _logger = logger; + } + + 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; } + } + + public async Task<(bool success, UserData? userData)> GetUserData() + { + try + { + // 1. 먼저 저장된 데이터 확인 + var encryptedUserData = await _storageService.GetItemAsync("USER_DATA"); + if (!string.IsNullOrEmpty(encryptedUserData)) + { + try + { + var decryptedUserData = await _secureService.DecryptAsync(encryptedUserData); + var userData = JsonSerializer.Deserialize(decryptedUserData); + if (userData != null && !string.IsNullOrEmpty(userData.Name)) + { + _logger.LogInformation("저장된 사용자 데이터 로드 성공"); + return (true, userData); + } + } + catch (Exception ex) + { + _logger.LogWarning($"저장된 사용자 데이터 복호화 실패: {ex.Message}"); + await _storageService.RemoveItemAsync("USER_DATA"); + } + } + + // 2. API 호출 + var headerValue = await _storageService.GetItemAsync("Web_AM_Connect_Key"); + if (string.IsNullOrEmpty(headerValue)) + { + _logger.LogWarning("연결 키가 없습니다"); + return (false, null); + } + + _logger.LogInformation("세션 API 호출 시작"); + var response = await _js.InvokeAsync("fetchWithHeaderAndReturnUrl", + "/api/v1/in/user/auth/session", + "GET", + "Web_AM_Connect_Key", + headerValue); + + if (response.TryGetProperty("data", out var dataElement)) + { + try + { + // 전체 데이터 암호화 저장 + var userDataJson = dataElement.ToString(); + var userData = JsonSerializer.Deserialize(userDataJson); + + if (userData != null && !string.IsNullOrEmpty(userData.Name)) + { + var encryptedData = await _secureService.EncryptAsync(userDataJson); + await _storageService.SetItemAsync("USER_DATA", encryptedData); + await _storageService.SetItemAsync("Web_AM_Connect_Key", headerValue); + + _logger.LogInformation($"사용자 데이터 저장 성공: {userData.Name}"); + return (true, userData); + } + else + { + _logger.LogWarning("사용자 데이터에 필수 정보가 없습니다"); + return (false, null); + } + } + catch (Exception ex) + { + _logger.LogError($"사용자 데이터 처리 중 오류: {ex.Message}"); + return (false, null); + } + } + + _logger.LogWarning("사용자 데이터를 찾을 수 없습니다"); + return (false, null); + } + catch (Exception ex) + { + _logger.LogError($"사용자 데이터 조회 중 오류: {ex.Message}"); + return (false, null); + } + } + + public async Task ClearUserData() + { + try + { + await _storageService.RemoveItemAsync("USER_DATA"); + await _storageService.RemoveItemAsync("USER"); + await _storageService.RemoveItemAsync("Web_AM_Connect_Key"); + _logger.LogInformation("사용자 데이터 삭제 성공"); + return true; + } + catch (Exception ex) + { + _logger.LogError($"사용자 데이터 삭제 중 오류: {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..1a0ef2b --- /dev/null +++ b/Program/Views/Academy/AcademyIntro.razor @@ -0,0 +1,30 @@ +@page "/am/intro" + +
+
+
+
+
+
+
+
+
+
+

학원을 위한 통합 플랫폼

+
+ +
+
+

Terms of Service · Privacy Policy · Contact Us

+

© 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..d123ded --- /dev/null +++ b/Program/Views/Academy/AcademyIntro.razor.cs @@ -0,0 +1,7 @@ +using Microsoft.AspNetCore.Components; + +namespace Front.Program.Views.Academy; + +public partial class AcademyIntro : ComponentBase +{ +} \ 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..75ea618 --- /dev/null +++ b/Program/Views/Academy/TopNavAcademy.razor @@ -0,0 +1,18 @@ +
+
+
+ Icon +
+ +
+ + +
\ No newline at end of file diff --git a/Program/Views/Academy/TopNavAcademy.razor.cs b/Program/Views/Academy/TopNavAcademy.razor.cs new file mode 100644 index 0000000..3379259 --- /dev/null +++ b/Program/Views/Academy/TopNavAcademy.razor.cs @@ -0,0 +1,6 @@ +using Microsoft.AspNetCore.Components; + +namespace Front.Program.Views.Academy; + +public partial class TopNavAcademy : ComponentBase +{} \ 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/Auth.razor b/Program/Views/Project/Auth.razor index 64926a8..253f87f 100644 --- a/Program/Views/Project/Auth.razor +++ b/Program/Views/Project/Auth.razor @@ -1,5 +1,5 @@ @page "/auth" -@page "/academy/auth" +@page "/am/auth"

로그인

diff --git a/Program/Views/Project/PageIndicator.razor b/Program/Views/Project/PageIndicator.razor index 0ba87a6..66a6365 100644 --- a/Program/Views/Project/PageIndicator.razor +++ b/Program/Views/Project/PageIndicator.razor @@ -1,5 +1,3 @@ -

PageIndicator

- @if (Type == IndicateType.Page) {
diff --git a/Program/Views/Project/RedirectPage.razor.cs b/Program/Views/Project/RedirectPage.razor.cs index 05c2e91..94f9f91 100644 --- a/Program/Views/Project/RedirectPage.razor.cs +++ b/Program/Views/Project/RedirectPage.razor.cs @@ -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/TopProjectNav.razor.cs b/Program/Views/Project/TopProjectNav.razor.cs index 00bf7bf..7257127 100644 --- a/Program/Views/Project/TopProjectNav.razor.cs +++ b/Program/Views/Project/TopProjectNav.razor.cs @@ -10,8 +10,7 @@ namespace Front.Program.Views.Project; public partial class TopProjectNav : ComponentBase { [Inject] NavigationManager NavigationManager { get; set; } = default!; - [Inject] StorageService StorageService { get; set; } = default!; - [Inject] SecureService SecureService { get; set; } = default!; + [Inject] UserService UserService { get; set; } = default!; [Inject] IJSRuntime JS { get; set; } = default!; @@ -23,110 +22,123 @@ public partial class TopProjectNav : ComponentBase protected override async Task OnInitializedAsync() { - Console.WriteLine("TopNAV 확인"); - isLogin = bool.TryParse(await StorageService.GetItemAsync("IsLogin"), out var result) && result; - // if (!isLogin && !string.IsNullOrEmpty(UserName)) return; - if (!isLogin) return; // && !string.IsNullOrEmpty(UserName)) return; + var (success, userData) = await UserService.GetUserData(); + if (success && userData != null) + { + Console.WriteLine(userData.Name); + UserName = userData.Name; + isLogin = true; + } + // isLogin = bool.TryParse(await StorageService.GetItemAsync("IsLogin"), out var result) && result; + // // if (!isLogin && !string.IsNullOrEmpty(UserName)) return; + // if (!isLogin) return; // && !string.IsNullOrEmpty(UserName)) return; - try - { - var encryptedName = await StorageService.GetItemAsync("USER"); - Console.WriteLine($"{encryptedName}"); - if (!string.IsNullOrEmpty(encryptedName)) - { - try - { - UserName = await SecureService.DecryptAsync(encryptedName); - Console.WriteLine($"세션 스토리지에서 가져온 사용자 이름: '{UserName}'"); + // try + // { + // var encryptedName = await StorageService.GetItemAsync("USER"); + // Console.WriteLine($"{encryptedName}"); + // if (!string.IsNullOrEmpty(encryptedName)) + // { + // try + // { + // UserName = await SecureService.DecryptAsync(encryptedName); + // Console.WriteLine($"세션 스토리지에서 가져온 사용자 이름: '{UserName}'"); - return; - } - catch (Exception ex) - { - Console.WriteLine($"이름 복호화 중 오류 발생: {ex.Message}"); - await StorageService.RemoveItemAsync("USER"); - } - } + // return; + // } + // catch (Exception ex) + // { + // Console.WriteLine($"이름 복호화 중 오류 발생: {ex.Message}"); + // await StorageService.RemoveItemAsync("USER"); + // } + // } - // apiSender.js의 함수를 사용하여 연결 키 가져오기 - var headerValue = await StorageService.GetItemAsync("Web_AM_Connect_Key"); - Console.WriteLine($"세션 스토리지에서 가져온 헤더 값: '{headerValue}'"); + // // apiSender.js의 함수를 사용하여 연결 키 가져오기 + // var headerValue = await StorageService.GetItemAsync("Web_AM_Connect_Key"); + // Console.WriteLine($"세션 스토리지에서 가져온 헤더 값: '{headerValue}'"); - if (string.IsNullOrEmpty(headerValue)) - { - Console.WriteLine("연결 키가 없습니다"); - return; - } + // if (string.IsNullOrEmpty(headerValue)) + // { + // Console.WriteLine("연결 키가 없습니다"); + // return; + // } - // apiSender.js의 함수를 사용하여 API 호출 - Console.WriteLine("세션 API 호출 시작"); - var response = await JS.InvokeAsync("fetchWithHeaderAndReturnUrl", - "/api/v1/in/user/auth/session", - "GET", - "Web_AM_Connect_Key", - headerValue); + // // apiSender.js의 함수를 사용하여 API 호출 + // Console.WriteLine("세션 API 호출 시작"); + // var response = await JS.InvokeAsync("fetchWithHeaderAndReturnUrl", + // "/api/v1/in/user/auth/session", + // "GET", + // "Web_AM_Connect_Key", + // headerValue); - Console.WriteLine($"세션 API 응답 타입: {response.ValueKind}"); - Console.WriteLine($"세션 API 응답 내용: {response}"); + // Console.WriteLine($"세션 API 응답 타입: {response.ValueKind}"); + // Console.WriteLine($"세션 API 응답 내용: {response}"); - if (response.ValueKind == JsonValueKind.Null || response.ValueKind == JsonValueKind.Undefined) - { - Console.WriteLine("응답이 null이거나 undefined입니다"); - return; - } + // if (response.ValueKind == JsonValueKind.Null || response.ValueKind == JsonValueKind.Undefined) + // { + // Console.WriteLine("응답이 null이거나 undefined입니다"); + // return; + // } - try - { - if (response.TryGetProperty("status", out var statusElement)) - { - Console.WriteLine($"status 요소 타입: {statusElement.ValueKind}"); - if (statusElement.TryGetProperty("code", out var codeElement)) - { - var code = codeElement.GetString(); - Console.WriteLine($"응답 코드: {code}"); + // try + // { + // if (response.TryGetProperty("status", out var statusElement)) + // { + // Console.WriteLine($"status 요소 타입: {statusElement.ValueKind}"); + // if (statusElement.TryGetProperty("code", out var codeElement)) + // { + // var code = codeElement.GetString(); + // Console.WriteLine($"응답 코드: {code}"); - if (code == "000") - { - if (response.TryGetProperty("data", out var dataElement)) - { - Console.WriteLine($"data 요소 타입: {dataElement.ValueKind}"); - if (dataElement.TryGetProperty("name", out var nameElement)) - { - UserName = nameElement.GetString() ?? "이름 없음"; - isLogin = true; - Console.WriteLine($"NM: {UserName}"); - var encryptedUserName = await SecureService.EncryptAsync(UserName); - Console.WriteLine($"NM: {encryptedUserName}"); - await StorageService.SetItemAsync("USER", encryptedUserName); - await StorageService.SetItemAsync("Web_AM_Connect_Key", headerValue); - Console.WriteLine($"로그인된 사용자: {UserName}"); - return; - } - } - } - else { - if (isLogin) { - isLogin = false; - await StorageService.SetItemAsync("IsLogin","false"); - } - - - } - } - } - Console.WriteLine("로그인되지 않은 상태"); - } - catch (Exception ex) - { - Console.WriteLine($"응답 처리 중 오류 발생: {ex.Message}"); - Console.WriteLine($"응답 처리 스택 트레이스: {ex.StackTrace}"); - } - } - catch (Exception ex) - { - Console.WriteLine($"세션 확인 중 오류 발생: {ex.Message}"); - Console.WriteLine($"스택 트레이스: {ex.StackTrace}"); - } + // if (code == "000") + // { + // if (response.TryGetProperty("data", out var dataElement)) + // { + // Console.WriteLine($"data 요소 타입: {dataElement.ValueKind}"); + + // // 전체 data를 JSON 문자열로 변환 + // var userDataJson = dataElement.ToString(); + + // // 전체 데이터 암호화 + // var encryptedUserData = await SecureService.EncryptAsync(userDataJson); + // await StorageService.SetItemAsync("USER_DATA", encryptedUserData); + + // // 기존 name 처리 로직 + // if (dataElement.TryGetProperty("name", out var nameElement)) + // { + // UserName = nameElement.GetString() ?? "이름 없음"; + // isLogin = true; + // Console.WriteLine($"NM: {UserName}"); + // var encryptedUserName = await SecureService.EncryptAsync(UserName); + // Console.WriteLine($"NM: {encryptedUserName}"); + // await StorageService.SetItemAsync("USER", encryptedUserName); + // await StorageService.SetItemAsync("Web_AM_Connect_Key", headerValue); + // Console.WriteLine($"로그인된 사용자: {UserName}"); + // return; + // } + // } + // } + // else { + // if (isLogin) { + // isLogin = false; + // await StorageService.SetItemAsync("IsLogin","false"); + // } + // } + // } + // } + // Console.WriteLine("로그인되지 않은 상태"); + // } + // catch (Exception ex) + // { + // Console.WriteLine($"응답 처리 중 오류 발생: {ex.Message}"); + // Console.WriteLine($"응답 처리 스택 트레이스: {ex.StackTrace}"); + // } + // } + // catch (Exception ex) + // { + // Console.WriteLine($"세션 확인 중 오류 발생: {ex.Message}"); + // Console.WriteLine($"스택 트레이스: {ex.StackTrace}"); + // } } public void OnClickMenuDown() @@ -145,19 +157,13 @@ public partial class TopProjectNav : ComponentBase if (isOpen) isOpen = !isOpen; NavigationManager.NavigateTo("/auth"); } + public async Task OnClickLogout() { - // var isRemoved = await StorageService.RemoveItemAsync("USER"); - if (await StorageService.RemoveItemAsync("IsLogin")) { - isLogin = false; - if (await StorageService.RemoveItemAsync("USER")) { - UserName = null; - Console.WriteLine("LOGOUT"); - } - } - else + if (await UserService.ClearUserData()) { - Console.WriteLine("로그아웃 처리 중 오류가 발생했습니다."); + isLogin = false; + UserName = null; } } } \ No newline at end of file diff --git a/_Imports.razor b/_Imports.razor index 90b8058..bf9db61 100644 --- a/_Imports.razor +++ b/_Imports.razor @@ -10,6 +10,6 @@ @using Front.Program.Layout @using Front.Program.Views.Project -@* @using Front.Program.Views.Academy *@ +@using Front.Program.Views.Academy From 2665dcbf64882c4751e3c69e8085a6154dcde024 Mon Sep 17 00:00:00 2001 From: SEAN-59 Date: Mon, 16 Jun 2025 17:47:35 +0900 Subject: [PATCH 5/6] =?UTF-8?q?[=E2=99=BB=EF=B8=8F]=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EB=8F=99=EC=9E=91=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C?= =?UTF-8?q?=ED=8E=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 로그인 동작을 위해서 viewmodel 로 관련 뷰에서 동작할 모든 로직을 viewmodel에서 관리 1.1. view 와 viewmodel의 관계는 1:N으로 동작하는것을 기반으로 두고 있음 2. API 접근하는 방식도 웹만의 접근 방법에서 수정 3. 로그인 동작 정보 받는 로직 수정 --- Front.csproj | 3 +- Program.cs | 5 +- Program/Layout/MainLayout.razor | 2 +- Program/Layout/MainLayout.razor.cs | 63 ++++---- Program/Models/UserData.cs | 13 ++ Program/Services/APIService.cs | 1 + Program/Services/QueryParamService.cs | 42 +++++ Program/Services/UserService.cs | 14 +- Program/ViewModels/UserViewModel.cs | 158 +++++++++++++++++++ Program/Views/Academy/TopNavAcademy.razor | 7 +- Program/Views/Academy/TopNavAcademy.razor.cs | 18 ++- Program/Views/Project/About.razor.cs | 65 +++++++- Program/Views/Project/TopProjectNav.razor | 4 +- Program/Views/Project/TopProjectNav.razor.cs | 141 ++--------------- wwwroot/css/tailwind.css | 2 +- wwwroot/scripts/apiSender.js | 25 ++- 16 files changed, 374 insertions(+), 189 deletions(-) create mode 100644 Program/Models/UserData.cs create mode 100644 Program/Services/QueryParamService.cs create mode 100644 Program/ViewModels/UserViewModel.cs diff --git a/Front.csproj b/Front.csproj index 7227ec8..2548224 100644 --- a/Front.csproj +++ b/Front.csproj @@ -11,6 +11,7 @@ + @@ -26,8 +27,6 @@ - - diff --git a/Program.cs b/Program.cs index 8608b7e..e7ddd79 100644 --- a/Program.cs +++ b/Program.cs @@ -4,7 +4,8 @@ using Microsoft.Extensions.Configuration; using Front; using Front.Program.Services; - +using Front.Program.ViewModels; + var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add("#app"); @@ -35,10 +36,12 @@ builder.Services.AddScoped(sp => //new HttpClient builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // builder.Services.AddRazorPages(); // builder.Services.AddServerSideBlazor(); 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 d99eecd..fe1c403 100644 --- a/Program/Layout/MainLayout.razor +++ b/Program/Layout/MainLayout.razor @@ -1,4 +1,4 @@ -@inherits LayoutComponentBase +@inherits LayoutComponentBase @implements IDisposable
diff --git a/Program/Layout/MainLayout.razor.cs b/Program/Layout/MainLayout.razor.cs index bc7af06..71f7924 100644 --- a/Program/Layout/MainLayout.razor.cs +++ b/Program/Layout/MainLayout.razor.cs @@ -32,40 +32,41 @@ public partial class MainLayout : LayoutComponentBase, IDisposable HandleLocationChanged(this, new LocationChangedEventArgs(Navigation.Uri, false)); } + // 페이지의 URL이 변경될 때마다 실행되는 이벤트 핸들러 private async void HandleLocationChanged(object? sender, LocationChangedEventArgs e) { LoadingService.HideNavigationLoading(); - - var uri = Navigation.ToAbsoluteUri(Navigation.Uri); - Console.WriteLine($"리다이렉트된 URI: {uri}"); - - if (uri.Query.Contains("auth=")) - { - 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]); - - 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("로그인 상태를 제거했습니다."); - } - - // 파라미터를 제거하고 리다이렉트 - var baseUri = uri.GetLeftPart(UriPartial.Path); - Navigation.NavigateTo(baseUri, forceLoad: false); - } - } + // + // var uri = Navigation.ToAbsoluteUri(Navigation.Uri); + // Console.WriteLine($"리다이렉트된 URI: {uri}"); + // + // if (uri.Query.Contains("auth=")) + // { + // 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]); + // + // 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("로그인 상태를 제거했습니다."); + // } + // + // // 파라미터를 제거하고 리다이렉트 + // var baseUri = uri.GetLeftPart(UriPartial.Path); + // Navigation.NavigateTo(baseUri, forceLoad: false); + // } + // } } public void Dispose() 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 861d64e..2c11090 100644 --- a/Program/Services/APIService.cs +++ b/Program/Services/APIService.cs @@ -28,6 +28,7 @@ public class APIService var response = await _http.GetFromJsonAsync>($"{url}?{parameter}"); return response; } + } /* 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/UserService.cs b/Program/Services/UserService.cs index 29595fc..6fef32d 100644 --- a/Program/Services/UserService.cs +++ b/Program/Services/UserService.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using Front.Program.Models; using Microsoft.JSInterop; namespace Front.Program.Services; @@ -22,18 +23,7 @@ public class UserService _logger = logger; } - 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; } - } - + public async Task<(bool success, UserData? userData)> GetUserData() { try diff --git a/Program/ViewModels/UserViewModel.cs b/Program/ViewModels/UserViewModel.cs new file mode 100644 index 0000000..91388fd --- /dev/null +++ b/Program/ViewModels/UserViewModel.cs @@ -0,0 +1,158 @@ +using System.Text.Json; +using Front.Program.Services; +using Front.Program.Models; +using Microsoft.JSInterop; + +namespace Front.Program.ViewModels; + +public class UserViewModel(StorageService _storageService,SecureService _secureService, IJSRuntime _js) +{ + public UserData UserData { get; set; } = new UserData(); + + 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() + { + 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; + } + } + } + + 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/TopNavAcademy.razor b/Program/Views/Academy/TopNavAcademy.razor index 75ea618..405f9e3 100644 --- a/Program/Views/Academy/TopNavAcademy.razor +++ b/Program/Views/Academy/TopNavAcademy.razor @@ -1,17 +1,12 @@
-
- Icon -
- - @if (!isLogin) + @if (!UserViewModel.isLogin) { + + @if (!UserStateService.isLogin) + { +

+ 학원을 위한 통합 플랫폼 +

+ +
+ +
+ } + else + { +
+

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

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

Terms of Service · Privacy Policy · Contact Us

-

© 2024 AcaMate. All rights reserved.

+ +

+ © 2024 AcaMate. All rights reserved. +

diff --git a/Program/Views/Academy/AcademyIntro.razor.cs b/Program/Views/Academy/AcademyIntro.razor.cs index d123ded..3f99336 100644 --- a/Program/Views/Academy/AcademyIntro.razor.cs +++ b/Program/Views/Academy/AcademyIntro.razor.cs @@ -1,7 +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 +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/TopNavAcademy.razor b/Program/Views/Academy/TopNavAcademy.razor index 405f9e3..0d18900 100644 --- a/Program/Views/Academy/TopNavAcademy.razor +++ b/Program/Views/Academy/TopNavAcademy.razor @@ -1,13 +1,36 @@
-
- +
+ + + @if (isAcademyDropdownOpen) + { +
+
+

학원 정보

+
+
+ @foreach (var academy in academyItems) + { + + @academy.business_name + + } +
+
+ }
- \ No newline at end of file +
diff --git a/Program/Views/Academy/TopNavAcademy.razor.cs b/Program/Views/Academy/TopNavAcademy.razor.cs index 43f1486..5dfce84 100644 --- a/Program/Views/Academy/TopNavAcademy.razor.cs +++ b/Program/Views/Academy/TopNavAcademy.razor.cs @@ -1,22 +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] UserViewModel UserViewModel { get; set; } = default!; + [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"); - - if (string.IsNullOrEmpty(UserViewModel.UserData.Name)) - { - await UserViewModel.GetUserDataAsync(); - } + } + + protected void ToggleAcademyDropdown() { + isAcademyDropdownOpen = !isAcademyDropdownOpen; } } \ No newline at end of file diff --git a/Program/Views/Project/About.razor.cs b/Program/Views/Project/About.razor.cs index 087f938..6c5e0df 100644 --- a/Program/Views/Project/About.razor.cs +++ b/Program/Views/Project/About.razor.cs @@ -17,7 +17,7 @@ public partial class About : ComponentBase, IDisposable [Inject] QueryParamService QueryParamService { get; set; } = default!; - [Inject] UserViewModel UserViewModel { get; set; } = default!; + [Inject] UserStateService UserStateService { get; set; } = default!; private bool _isProcessing = false; @@ -32,12 +32,6 @@ public partial class About : ComponentBase, IDisposable Navigation.LocationChanged -= HandleLocationChanged; } - private async Task OnClickEvent() - { - // NavigationManager.NavigateTo("/redirectpage"); - Console.WriteLine("Redirecting to redirect page"); - } - private async void HandleLocationChanged(object? sender, LocationChangedEventArgs e) { try @@ -56,7 +50,7 @@ public partial class About : ComponentBase, IDisposable await QueryParamService.AuthCheck(queryParam, StorageService); // 유저 정보 확인하는거 (로그인 했으니 값 가져와야지) - await UserViewModel.GetUserDataAsync(); + await UserStateService.GetUserDataAsync(); // 쿼리 파라미터를 제거한 기본 URI로 리다이렉트 var baseUri = uri.GetLeftPart(UriPartial.Path); 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 86% rename from Program/Views/Project/RedirectPage.razor.cs rename to Program/Views/Project/Common/RedirectPage.razor.cs index 94f9f91..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 { 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/TopProjectNav.razor b/Program/Views/Project/Common/TopProjectNav.razor similarity index 97% rename from Program/Views/Project/TopProjectNav.razor rename to Program/Views/Project/Common/TopProjectNav.razor index 9332365..b3636f3 100644 --- a/Program/Views/Project/TopProjectNav.razor +++ b/Program/Views/Project/Common/TopProjectNav.razor @@ -11,7 +11,7 @@ Join What's New
- @if (!UserViewModel.isLogin) + @if (!UserStateService.isLogin) {