main #21

Merged
seonkyu.kim merged 6 commits from seonkyu.kim/AcaMate_Web:main into debug 2025-06-17 07:19:18 +00:00
47 changed files with 1319 additions and 249 deletions

View File

@ -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<APIHeader, AppHeader>(
"/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);
}
}
}

View File

@ -11,6 +11,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.8"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.8" PrivateAssets="all"/>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.10" />
</ItemGroup>
<ItemGroup>
@ -27,7 +28,7 @@
<ItemGroup>
<Folder Include="Program\ViewModels\" />
<Folder Include="Program\Views\Academy\" />
<Folder Include="Program\Views\Project\FooterLink\" />
<Folder Include="wwwroot\Resources\" />
</ItemGroup>

View File

@ -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>("#app");
builder.RootComponents.Add<HeadOutlet>("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<APIService>();
builder.Services.AddScoped<CookieService>();
// builder.Services.AddRazorPages();
// builder.Services.AddServerSideBlazor();
builder.Services.AddScoped<LoadingService>();
builder.Services.AddScoped<SecureService>();
builder.Services.AddScoped<StorageService>();
builder.Services.AddScoped<QueryParamService>();
builder.Services.AddScoped<LoadingService>();
builder.Services.AddScoped<UserStateService>();
await builder.Build().RunAsync();

View File

@ -5,25 +5,43 @@
<!-- Top 영역 -->
@* <TopBanner /> *@
@if (!isHideTop)
@if(isAcademy)
{
<TopNav />
<div class="flex flex-1 flex-col md:flex-row">
@if (!isIntro && UserStateService.isLogin)
{
<div class="hidden md:block w-64 bg-white shadow-lg border-r border-gray-200 fixed top-0 bottom-0">
<LeftSideAcademy/>
</div>
<div class="flex flex-1 flex-col md:ml-64">
<div class="fixed top-0 right-0 left-64 z-10">
<TopNavAcademy/>
</div>
<div class="flex-1 mt-16">
@Body
</div>
</div>
}
else
{
<main class="flex-1 w-full w-max-960 mx-auto">
@Body
</main>
}
</div>
}
<!-- 본문 영역 -->
<div class="flex flex-1 flex-col md:flex-row">
@* <!-- 사이드 메뉴 --> *@
@* <div class="hidden md:block md:w-64 bg-white shadow"> *@
@* <SideNav /> *@
@* </div> *@
else
{
@if (!isHidePrjTop)
{
<TopProjectNav />
}
<!-- 본문 컨텐츠 -->
@* <main class="flex-1 p-4 sm:p-6 md:p-8"> *@
<main class="flex-1 w-full w-max-960 mx-auto">
@Body
</main>
</div>
}
<!-- 플로팅 버튼 -->
<FloatingButton />

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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; }
}

View File

@ -28,4 +28,16 @@ public class APIService
var response = await _http.GetFromJsonAsync<APIResponseStatus<TResponse>>($"{url}?{parameter}");
return response;
}
}
}
/*
dynamic :
Register.razor.cs에서 JsonElement를 . :
JSON (TryGetProperty )
*/

View File

@ -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<string?> GetCookieAsync(string key)
{
return await _js.InvokeAsync<string>("getCookie", key);
}
public async Task DeleteCookieAsync(string key)
{
await _js.InvokeVoidAsync("deleteCookie", key);
}
}

View File

@ -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();
}

View File

@ -0,0 +1,42 @@
using Microsoft.AspNetCore.Components;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
namespace Front.Program.Services;
public class QueryParamService
{
public Dictionary<string, string> ParseQueryParam(System.Uri uri)
{
var result = new Dictionary<string, string>();
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<string, string> 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("로그인 상태를 제거했습니다.");
}
}
}
}

View File

@ -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<SecureService> _logger;
private readonly IJSRuntime _jsRuntime;
private readonly IWebAssemblyHostEnvironment _environment;
private string? _key;
private string? _iv;
private Task? _initializationTask;
public SecureService(
ILogger<SecureService> 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<JsonElement>("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<string> EncryptAsync(string plainText)
{
await EnsureInitializedAsync();
if (_key == null || _iv == null)
throw new InvalidOperationException("암호화 키가 초기화되지 않았습니다.");
try
{
return await _jsRuntime.InvokeAsync<string>("encryptText", plainText, _key, _iv);
}
catch (Exception ex)
{
_logger.LogError($"암호화 중 오류 발생: {ex.Message}");
throw;
}
}
public async Task<string> DecryptAsync(string cipherText)
{
await EnsureInitializedAsync();
if (_key == null || _iv == null)
throw new InvalidOperationException("암호화 키가 초기화되지 않았습니다.");
try
{
return await _jsRuntime.InvokeAsync<string>("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();
}

View File

@ -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<string?> GetItemAsync(string key, STORAGE_TYPE type = STORAGE_TYPE.Session)
{
return type switch
{
STORAGE_TYPE.Cookie => await _js.InvokeAsync<string>("getCookie", key),
STORAGE_TYPE.Local => await _js.InvokeAsync<string>("localStorage.getItem", key),
STORAGE_TYPE.Session => await _js.InvokeAsync<string>("sessionStorage.getItem", key),
_ => null
};
}
public async Task<bool> 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. .
*/

View File

@ -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<UserData>(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<JsonElement>(
"fetchWithHeaderAndReturnUrl",
args
);
// var response = await _js.InvokeAsync<JsonElement>("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<UserData>(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<bool> 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;
}
}
}

View File

@ -0,0 +1,68 @@
@page "/am/intro"
<div class="relative flex size-full min-h-screen flex-col bg-normal-normal group/design-root overflow-x-hidden" style='font-family: "Public Sans", "Noto Sans", sans-serif;'>
<div class="layout-container flex h-full grow flex-col">
<div class="px-4 sm:px-6 md:px-10 lg:px-24 xl:px-40 flex flex-1 justify-center py-5">
<div class="layout-content-container flex flex-col max-w-[960px] flex-1">
<div class="container pt-24">
<div class="px-4 md:px-8 py-4">
<div class="w-full bg-center bg-no-repeat bg-contain md:bg-contain flex flex-col justify-center items-center overflow-hidden bg-white/35 rounded-xl min-h-[218px] md:h-[300px]"
style="background-image: url('/Resources/Images/Logo/Crystal_Icon.png');">
</div>
</div>
</div>
@if (!UserStateService.isLogin)
{
<h3 class="text-[#111518] tracking-light text-2xl font-bold leading-tight px-4 text-center pb-2 pt-3">
학원을 위한 통합 플랫폼
</h3>
<div class="flex px-4 py-2 justify-center">
<button class="bg-second-normal hover:bg-second-hover transition-all duration-200 shadow-md text-white font-bold py-3 px-6 rounded-xl"
@onclick="OnClickLogin">
<span class="truncate">시작하기</span>
</button>
</div>
}
else
{
<div class="flex flex-col px-4 py-2 gap-2">
<h3 class="text-[#111518] tracking-light text-xl font-bold leading-tight text-center py-12">
@UserStateService.UserData.Name 님, 안녕하세요!<br />
학원을 선택해주세요.<br />
</h3>
<div class="max-h-[180px] overflow-y-auto rounded-xl bg-second-normal/10 border-2 border-text-detail">
@foreach (var academy in academyItems)
{
<a href="/am/@academy.bid"
class="block w-full px-4 py-3 hover:bg-second-dark border-b border-gray-200 last:border-b-0 group">
<div class="flex items-center justify-between">
<span class="text-text-black group-hover:text-text-white font-medium">@academy.business_name</span>
<svg class="w-5 h-5 text-text-detail group-hover:text-text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</div>
</a>
}
</div>
</div>
}
@* <div class="w-full" style="height: 100px;"></div> *@
<div class="border-b border-text-disabled my-4 pt-12"></div>
<div class="flex flex-col md:flex-row justify-center items-center gap-4 px-4">
<a href="/terms" class="text-text-detail text-sm font-normal leading-normal hover:text-text-black hover:font-bold">이용약관</a>
<span class="hidden md:inline text-text-black">·</span>
<a href="/privacy" class="text-text-detail text-sm font-normal leading-normal hover:text-text-black hover:font-bold">개인정보처리방침</a>
<span class="hidden md:inline text-text-black">·</span>
<a href="/contact" class="text-text-detail text-sm font-normal leading-normal hover:text-text-black hover:font-bold">문의하기</a>
</div>
<p class="text-text-detail text-sm font-normal leading-normal py-4 px-4 text-center">
© 2024 AcaMate. All rights reserved.
</p>
</div>
</div>
</div>
</div>

View File

@ -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<Models.SimpleAcademy>();
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");
}
}

View File

@ -0,0 +1,70 @@
<div class="h-[72px] p-4 border-b border-gray-200 flex items-center">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-gray-200 overflow-hidden">
<img src="Resources/Images/Logo/Crystal_Icon.png" alt="프로필" class="w-full h-full object-cover" />
</div>
<div>
<!-- 회원 이름이 오는 곳 -->
<div class="text-gray-900 text-base font-medium">AcaMate</div>
<!-- 회원 유형이 올 곳 -->
<p class="text-gray-600 text-sm">Parent</p>
</div>
</div>
</div>
<!-- 메뉴 섹션 -->
<nav class="flex-1 p-4 space-y-2 ">
<a href="/academy/children" class="flex items-center gap-3 px-3 py-2 rounded-lg bg-gray-100">
<div class="text-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256">
<path d="M164.47,195.63a8,8,0,0,1-6.7,12.37H10.23a8,8,0,0,1-6.7-12.37,95.83,95.83,0,0,1,47.22-37.71,60,60,0,1,1,66.5,0A95.83,95.83,0,0,1,164.47,195.63Z"></path>
</svg>
</div>
<span class="text-gray-700 text-sm font-medium">My Children</span>
</a>
<a href="/academy/attendance" class="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-gray-100">
<div class="text-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256">
<path d="M208,32H184V24a8,8,0,0,0-16,0v8H88V24a8,8,0,0,0-16,0v8H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32Z"></path>
</svg>
</div>
<span class="text-gray-700 text-sm font-medium">Attendance</span>
</a>
<a href="/academy/announcements" class="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-gray-100">
<div class="text-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256">
<path d="M240,120a48.05,48.05,0,0,0-48-48H152.2c-2.91-.17-53.62-3.74-101.91-44.24A16,16,0,0,0,24,40V200a16,16,0,0,0,26.29,12.25c37.77-31.68,77-40.76,93.71-43.3v31.72A16,16,0,0,0,151.12,214l11,7.33A16,16,0,0,0,186.5,212l11.77-44.36A48.07,48.07,0,0,0,240,120Z"></path>
</svg>
</div>
<span class="text-gray-700 text-sm font-medium">Announcements</span>
</a>
<a href="/academy/messages" class="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-gray-100">
<div class="text-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256">
<path d="M140,128a12,12,0,1,1-12-12A12,12,0,0,1,140,128ZM84,116a12,12,0,1,0,12,12A12,12,0,0,0,84,116Zm88,0a12,12,0,1,0,12,12A12,12,0,0,0,172,116Z"></path>
</svg>
</div>
<span class="text-gray-700 text-sm font-medium">Messages</span>
</a>
<a href="/academy/bus-tracking" class="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-gray-100">
<div class="text-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256">
<path d="M184,32H72A32,32,0,0,0,40,64V208a16,16,0,0,0,16,16H80a16,16,0,0,0,16-16V192h64v16a16,16,0,0,0,16,16h24a16,16,0,0,0,16-16V64A32,32,0,0,0,184,32Z"></path>
</svg>
</div>
<span class="text-gray-700 text-sm font-medium">Bus Tracking</span>
</a>
<a href="/academy/settings" class="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-gray-100">
<div class="text-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256">
<path d="M128,80a48,48,0,1,0,48,48A48.05,48.05,0,0,0,128,80Zm0,80a32,32,0,1,1,32-32A32,32,0,0,1,128,160Z"></path>
</svg>
</div>
<span class="text-gray-700 text-sm font-medium">Settings</span>
</a>
</nav>

View File

@ -0,0 +1,7 @@
using Microsoft.AspNetCore.Components;
namespace Front.Program.Views.Academy;
public partial class LeftSideAcademy : ComponentBase
{
}

View File

@ -0,0 +1,36 @@
<div class="flex items-center justify-between whitespace-nowrap border-b border-solid border-b-[#f0f2f5] h-[72px] bg-white">
<div id="AcademyDrop" class="relative flex items-center gap-4 text-[#111418] ml-4">
<button class="flex items-center gap-2 hover:text-blue-600" @onclick="ToggleAcademyDropdown">
<h2 class="hidden md:block text-text-title text-lg font-bold leading-tight tracking-[-0.015em]">AcaMate</h2>
<svg class="w-4 h-4 transition-transform duration-200 @(isAcademyDropdownOpen ? "rotate-180" : "")"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
@if (isAcademyDropdownOpen)
{
<div class="absolute top-full left-0 mt-2 w-64 bg-white rounded-lg shadow-lg py-2 z-50">
<div class="px-4 py-2 border-b border-gray-200">
<h3 class="font-semibold text-gray-900">학원 정보</h3>
</div>
<div class="max-h-60 overflow-y-auto">
@foreach (var academy in academyItems)
{
<a href="/am/@academy.bid" class="block px-4 py-2 text-text-title hover:bg-gray-100">
@academy.business_name
</a>
}
</div>
</div>
}
</div>
<div class="hidden md:flex flex-1 justify-end gap-8">
<div class="flex flex-1 justify-end gap-8">
<div class="flex items-center gap-9">
<a class="text-text-title font-medium leading-normal hover:text-blue-600" href="/about">About</a>
</div>
</div>
</div>
</div>

View File

@ -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<Models.SimpleAcademy>();
protected override async Task OnInitializedAsync()
{
Console.WriteLine("TOPNAV_OnInitializedAsync");
}
protected void ToggleAcademyDropdown() {
isAcademyDropdownOpen = !isAcademyDropdownOpen;
}
}

View File

@ -167,8 +167,8 @@
</div>
</div>
<footer class="flex justify-center border-t border-text-border">
<div class="flex justify-center border-t border-text-border">
<div class="flex max-w-[960px] flex-1 flex-col">
<footer class="flex flex-col gap-6 px-5 py-10 text-center container">
<div class="flex flex-wrap items-center justify-center gap-6 xs:flex-row xs:justify-around">
@ -179,6 +179,19 @@
<p class="text-[#60758a] text-base font-normal leading-normal">© 2024 AcaMate. All rights reserved.</p>
</footer>
</div>
</footer>
</div>
@* <footer class="flex justify-center border-t border-text-border"> *@
@* <div class="flex max-w-[960px] flex-1 flex-col"> *@
@* <footer class="flex flex-col gap-6 px-5 py-10 text-center container"> *@
@* <div class="flex flex-wrap items-center justify-center gap-6 xs:flex-row xs:justify-around"> *@
@* <a class="text-[#60758a] text-base font-normal leading-normal min-w-40" href="#">Terms of Service</a> *@
@* <a class="text-[#60758a] text-base font-normal leading-normal min-w-40" href="#">Privacy Policy</a> *@
@* <a class="text-[#60758a] text-base font-normal leading-normal min-w-40" href="#">Contact Us</a> *@
@* </div> *@
@* <p class="text-[#60758a] text-base font-normal leading-normal">© 2024 AcaMate. All rights reserved.</p> *@
@* </footer> *@
@* </div> *@
@* </footer> *@
</div>
</div>

View File

@ -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;
}
}
}

View File

@ -1,12 +0,0 @@
@page "/auth"
<div class="flex flex-col items-center justify-center min-h-screen bg-gray-100">
<h1 class="text-2xl font-bold mb-4">로그인</h1>
<div class="w-full max-w-xs">
<button type="button" class="w-full mb-4 p-2 rounded focus:outline-none" @onclick="KakaoLogin">
<img src="//k.kakaocdn.net/14/dn/btqCn0WEmI3/nijroPfbpCa4at5EIsjyf0/o.jpg"
alt="카카오 로그인"
class="rounded w-full transition duration-150 hover:brightness-90" />
</button>
</div>
</div>

View File

@ -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<JsonElement>(url);
var kakaoUrl = response.GetProperty("url").GetString();
Console.WriteLine(kakaoUrl);
if (!string.IsNullOrEmpty(kakaoUrl))
{
NavigationManager.NavigateTo(kakaoUrl, true);
}
}
}

View File

@ -0,0 +1,5 @@
<div class="fixed top-0 left-0 w-full h-14 bg-black/70 flex items-center justify-center z-50">
<div class="bg-gray-200/80 px-4 py-2 rounded-lg">
<div class="animate-spin h-5 w-5 border-2 border-gray-600 border-t-transparent rounded-full"></div>
</div>
</div>

View File

@ -0,0 +1,7 @@
using Front.Program.Models;
using Microsoft.AspNetCore.Components;
namespace Front.Program.Views.Project.Common;
public partial class PageIndicator : ComponentBase
{ }

View File

@ -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);
}
}

View File

@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Components;
namespace Front.Program.Views.Project;
namespace Front.Program.Views.Project.Common;
public partial class TopBanner : ComponentBase
{

View File

@ -11,13 +11,28 @@
<a class="text-text-title font-medium leading-normal hover:text-blue-600" href="/join" @onclick="() => isOpen = !isOpen">Join</a>
<a class="text-text-title font-medium leading-normal hover:text-blue-600" href="/new" @onclick="() => isOpen = !isOpen">What's New</a>
</div>
<button class="flex min-w-[84px] max-w-[480px] cursor-pointer items-center justify-center overflow-hidden rounded-lg h-10 px-4 bg-second-normal hover:bg-second-dark text-white text-sm font-bold leading-normal tracking-[0.015em] mr-4"
@onclick="OnClickLogin">
<span class="truncate">시작하기</span>
</button>
@if (!UserStateService.isLogin)
{
<button class="flex min-w-[84px] max-w-[480px] cursor-pointer items-center justify-center overflow-hidden rounded-lg h-10 px-4 bg-second-normal hover:bg-second-dark text-white text-sm font-bold leading-normal tracking-[0.015em] mr-4"
@onclick="OnClickLogin">
<span class="truncate">시작하기</span>
</button>
}
else
{
<button class="flex items-center justify-center w-10 h-10 rounded-full text-white text-sm font-bold leading-normal tracking-[0.015em] mr-4"
@onclick="OnClickLogout">
<div class="relative w-8 h-8">
<img src="Resources/Images/Icon/Logout.png" alt="로그아웃" class="w-6 h-6 absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 hover:brightness-0 hover:saturate-100 hover:invert hover:sepia hover:hue-rotate-[180deg] hover:brightness-[0.7] hover:contrast-[0.8] transition-all duration-200" />
</div>
</button>
}
</div>
</div>
<button class="md:hidden mr-4" @onclick="OnClickMenuDown">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
@ -32,10 +47,20 @@
<a class="block w-full gap-y-2 text-text-title font-medium leading-normal hover:text-blue-600" href="/about" @onclick="() => isOpen = !isOpen">About</a>
<a class="block w-full gap-y-2 text-text-title font-medium leading-normal hover:text-blue-600" href="/join" @onclick="() => isOpen = !isOpen">Join</a>
<a class="block w-full gap-y-2 text-text-title font-medium leading-normal hover:text-blue-600" href="/new" @onclick="() => isOpen = !isOpen">What's New</a>
<button class="flex w-full cursor-pointer items-center justify-center rounded-lg h-10 px-4 bg-second-normal hover:bg-second-dark text-white text-sm font-bold leading-normal tracking-[0.015em]"
@onclick="OnClickLogin">
<span class="truncate">시작하기</span>
</button>
@if (!UserStateService.isLogin)
{
<button class="flex w-full cursor-pointer items-center justify-center rounded-lg h-10 px-4 bg-second-normal hover:bg-second-dark text-white text-sm font-bold leading-normal tracking-[0.015em]"
@onclick="OnClickLogin">
<span class="truncate">시작하기</span>
</button>
}
else
{
<button class="flex w-full cursor-pointer items-center justify-center rounded-lg h-10 px-4 bg-second-normal hover:bg-second-dark text-white text-sm font-bold leading-normal tracking-[0.015em]"
@onclick="OnClickLogout">
<span class="truncate">로그아웃</span>
</button>
}
</div>
</div>
}

View File

@ -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;
// }
}
}

View File

@ -0,0 +1,26 @@
@page "/auth"
@page "/am/auth"
<div class="flex flex-col items-center justify-center min-h-screen bg-gray-100">
<h1 class="text-2xl font-bold mb-4">로그인</h1>
<div class="w-full max-w-xs">
@if (NavigationManager.Uri.Contains("/am/auth"))
{
<button type="button" class="w-full mb-4 p-2 rounded focus:outline-none"
@onclick="@(() => KakaoLogin("/am/intro"))" >
<img src="//k.kakaocdn.net/14/dn/btqCn0WEmI3/nijroPfbpCa4at5EIsjyf0/o.jpg"
alt="카카오 로그인"
class="rounded w-full transition duration-150 hover:brightness-90" />
</button>
}
else
{
<button type="button" class="w-full mb-4 p-2 rounded focus:outline-none"
@onclick="@(() => KakaoLogin("/about"))" >
<img src="//k.kakaocdn.net/14/dn/btqCn0WEmI3/nijroPfbpCa4at5EIsjyf0/o.jpg"
alt="카카오 로그인"
class="rounded w-full transition duration-150 hover:brightness-90" />
</button>
}
</div>
</div>

View File

@ -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<JsonElement>(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();
}
}
}

View File

@ -21,7 +21,7 @@
<input
pattern="\d{16}"
maxlength="16"
placeholder="실명을 입력해주세요. 최대 16글자"
placeholder="실명을 입력해주세요. (최대 16글자)"
class="form-input flex w-full min-w-0 flex-1 resize-none overflow-hidden rounded-xl text-text-title focus:outline-0 focus:ring-0 border border-[#dde0e3] bg-white focus:border-[#dde0e3] h-14 placeholder:text-[#6a7581] p-[15px] text-base font-normal leading-normal"
@bind-value="@name"
/>
@ -31,15 +31,41 @@
<div class="flex w-full flex-wrap items-end gap-4 px-4 py-3">
<label class="flex flex-col w-full">
<p class="text-text-title text-base font-medium leading-normal pb-2">생일</p>
<input
type="date"
@ref="dateInputRef"
placeholder="YYYYMMDD"
class="form-input flex w-full min-w-0 flex-1 resize-none overflow-hidden rounded-xl text-text-title focus:outline-0 focus:ring-0 border border-[#dde0e3] bg-white focus:border-[#dde0e3] h-14 placeholder:text-[#6a7581] p-[15px] text-base font-normal leading-normal"
@bind-value="birth"
@onclick="OpenDatePicker"
readonly
/>
<div class="flex w-full max-w-[480px] items-center gap-2">
<input
maxlength="4"
pattern="\d{4}"
placeholder="YYYY"
class="form-input flex-1 min-w-0 h-12 text-center rounded-xl border border-[#dde0e3] bg-white focus:border-[#dde0e3] placeholder:text-[#6a7581] p-[15px] text-base font-normal leading-normal"
@bind-value="birthYear"
inputmode="numeric"
autocomplete="off"
oninput="this.value = this.value.replace(/[^0-9]/g, '')"
/>
<span class="self-center">년</span>
<input
maxlength="2"
pattern="\d{2}"
placeholder="MM"
class="form-input flex-1 min-w-0 h-12 text-center rounded-xl border border-[#dde0e3] bg-white focus:border-[#dde0e3] placeholder:text-[#6a7581] p-[15px] text-base font-normal leading-normal"
@bind-value="birthMonth"
inputmode="numeric"
autocomplete="off"
oninput="this.value = this.value.replace(/[^0-9]/g, '')"
/>
<span class="self-center">월</span>
<input
maxlength="2"
pattern="\d{2}"
placeholder="DD"
class="form-input flex-1 min-w-0 h-12 text-center rounded-xl border border-[#dde0e3] bg-white focus:border-[#dde0e3] placeholder:text-[#6a7581] p-[15px] text-base font-normal leading-normal"
@bind-value="birthDay"
inputmode="numeric"
autocomplete="off"
oninput="this.value = this.value.replace(/[^0-9]/g, '')"
/>
<span class="self-center">일</span>
</div>
</label>
</div>
@ -49,7 +75,7 @@
<span class="text-red-600">*</span>
</p>
<input
placeholder="Enter your email"
placeholder="E-mail을 입력해주세요."
class="form-input flex w-full min-w-0 flex-1 resize-none overflow-hidden rounded-xl text-text-title focus:outline-0 focus:ring-0 border border-[#dde0e3] bg-white focus:border-[#dde0e3] h-14 placeholder:text-[#6a7581] p-[15px] text-base font-normal leading-normal"
@bind-value="email"/>
</label>

View File

@ -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<string>("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<string>("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)
{

View File

@ -1,11 +0,0 @@
<h3>PageIndicator</h3>
@if (Type == IndicateType.Page)
{
<div class="fixed top-0 left-0 w-full h-14 bg-black/70 flex items-center justify-center z-50">
<div class="bg-gray-200/80 px-4 py-2 rounded-lg">
<div class="animate-spin h-5 w-5 border-2 border-gray-600 border-t-transparent rounded-full"></div>
</div>
</div>
}

View File

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

View File

@ -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");
}
}

View File

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

13
appsettings.json Normal file
View File

@ -0,0 +1,13 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Security": {
"EncryptionKey": "AcaMate2025SecureKeySeanForEncryption19940509",
"EncryptionIV": "AcaMate2025IV9459"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

View File

@ -1,3 +1,7 @@
{
"ApiBaseUrl": "https://devacamate.ipstein.myds.me/"
"ApiBaseUrl": "https://devacamate.ipstein.myds.me/",
"Security": {
"EncryptionKey": "AcaMate2025SecureKeyForEncryptionSean1994",
"EncryptionIV": "AcaMate2025IV9459"
}
}

View File

@ -1,3 +1,7 @@
{
"ApiBaseUrl": "https://acamate.ipstein.myds.me/"
"ApiBaseUrl": "https://acamate.ipstein.myds.me/",
"Security": {
"EncryptionKey": "AcaMate2025SecureKeyForEncryptionSean1994",
"EncryptionIV": "AcaMate2025IV9459"
}
}

File diff suppressed because one or more lines are too long

View File

@ -12,7 +12,7 @@
<body class="bg-gray-50 text-gray-900 font-sans">
<!-- 로딩 오버레이 -->
<div class="fixed inset-0 z-50 flex items-center justify-center bg-gray-100" id="loading-overlay">
<div class="fixed inset-0 z-50 flex items-center justify-center bg-gray-100 hidden" id="loading-overlay" data-show-on="/about">
<!-- 로딩 오버레이 내부 -->
<div class="flex flex-col items-center">
<div class="relative w-48 h-48 overflow-hidden">
@ -48,12 +48,20 @@
Blazor.start().then(() => {
const elapsed = performance.now() - loadingStart;
const remaining = Math.max(0, MIN_LOADING_MS - elapsed);
setTimeout(() => {
const overlay = document.getElementById('loading-overlay');
if (overlay) overlay.remove();
}, remaining);
const overlay = document.getElementById('loading-overlay');
const showOnPath = overlay?.getAttribute('data-show-on');
if (overlay && showOnPath && window.location.pathname === showOnPath) {
overlay.classList.remove('hidden');
setTimeout(() => {
if (overlay) overlay.remove();
}, remaining);
} else if (overlay) {
overlay.remove();
}
}).catch(err => {
console.error("❌ Blazor 로딩 실패", err);
console.error("Blazor 로딩 실패", err);
});
</script>
@ -62,6 +70,7 @@
const v = document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)');
return v ? v.pop() : '';
};
window.setCookie = function (name, value, days) {
let expires = "";
if (days) {

View File

@ -11,13 +11,101 @@ window.postWithHeader = function(url, method, headerKey, headerValue) {
});
};
window.fetchWithHeaderAndReturnUrl = async function(url, method, headerKey, headerValue) {
const response = await fetch(url, {
method: method,
headers: {
[headerKey]: headerValue
window.fetchWithHeaderAndReturnUrl = async function(args) {
try {
let url = args.url;
const queryParams = Object.entries(args)
.filter(([key]) => !['url', 'method', 'headerKey', 'headerValue'].includes(key))
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
if (queryParams) {
url += (url.includes('?') ? '&' : '?') + queryParams;
}
});
const data = await response.json();
return data.url;
};
const response = await fetch(url, {
method: args.method,
headers: {
[args.headerKey]: args.headerValue
}
});
const contentType = response.headers.get('content-type');
if (!response.ok) {
console.error('API 호출 실패:', response.status, response.statusText);
return null;
}
if (!contentType || !contentType.includes('application/json')) {
const text = await response.text();
console.error('JSON이 아닌 응답:', text);
return null;
}
const data = await response.json();
console.log('API 응답 데이터:', data);
return data;
} catch (error) {
console.error('API 호출 중 오류 발생:', error);
return null;
}
};
window.loadConfig = async function(configFile) {
try {
console.log('설정 파일 로드 시도:', configFile);
const response = await fetch(configFile);
if (!response.ok) {
console.error('설정 파일 로드 실패:', response.status, response.statusText);
return null;
}
const config = await response.json();
console.log('설정 파일 로드 성공:', configFile);
return config;
} catch (error) {
console.error('설정 파일 로드 중 오류 발생:', error);
return null;
}
};
window.encryptText = async function(text, key, iv) {
try {
// XOR 암호화 구현
let result = '';
for (let i = 0; i < text.length; i++) {
const charCode = text.charCodeAt(i) ^ key.charCodeAt(i % key.length);
result += String.fromCharCode(charCode);
}
// UTF-8로 인코딩 후 Base64로 변환
const utf8Encoder = new TextEncoder();
const bytes = utf8Encoder.encode(result);
const base64 = btoa(String.fromCharCode.apply(null, bytes));
return base64;
} catch (error) {
console.error('암호화 중 오류 발생:', error);
throw error;
}
};
window.decryptText = async function(encryptedText, key, iv) {
try {
// Base64 디코딩 후 UTF-8 디코딩
const binary = atob(encryptedText);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
const utf8Decoder = new TextDecoder();
const text = utf8Decoder.decode(bytes);
// XOR 복호화
let result = '';
for (let i = 0; i < text.length; i++) {
const charCode = text.charCodeAt(i) ^ key.charCodeAt(i % key.length);
result += String.fromCharCode(charCode);
}
return result;
} catch (error) {
console.error('복호화 중 오류 발생:', error);
throw error;
}
};