Compare commits

...

7 Commits

Author SHA1 Message Date
ab1efceb62 Merge pull request 'main' (#21) from seonkyu.kim/AcaMate_Web:main into debug
All checks were successful
AcaMate_FO/pipeline/head This commit looks good
Reviewed-on: https://git.ipstein.myds.me/AcaMate/AcaMate_Web/pulls/21
2025-06-17 07:19:16 +00:00
3e31f9f46b [] am/Intro 화면 동작 구현
1. 로그인 연동
2. 화면 변환 연동
2.1. 아직 Academy 테이블 연동은 안되어있는 상황
2025-06-17 16:00:56 +09:00
2665dcbf64 [♻️] 로그인 동작 로직 개편
1. 로그인 동작을 위해서 viewmodel 로 관련 뷰에서 동작할 모든 로직을 viewmodel에서 관리
1.1. view 와 viewmodel의 관계는 1:N으로 동작하는것을 기반으로 두고 있음
2. API 접근하는 방식도 웹만의 접근 방법에서 수정
3. 로그인 동작 정보 받는 로직 수정
2025-06-16 17:47:35 +09:00
c371700e78 [] 학원용 입장페이지 작성
1. /am/...  도메인 생성
2. /am/intro 페이지 생성
3. 메인 레이아웃 상황 따라 변경
4. 로그인 정보 받아오는 로직 변경 중
4.1. 유저 정보에 대한 JSON은 받아왔으나 이를 저장하는 로직 구현 중
4.2. 로그인 정보를 받아오는 로직을 어디서 구현해야 할 지 고민 하는 중
2025-06-13 17:53:58 +09:00
05e8fcd0b0 [] 로그인 후 레이아웃 변경 2025-06-12 17:55:50 +09:00
9621169d57 [] 로그인 동작 및 Repository 동작
1. 쿠키만 사용하던 저장 동작을 다른 저장소도 사용하게 변경
1.1. 쿠키 대신 세션 레포지토리를 기본적으로 사용하게 Service 코드 구현
2. 로그인 되었을 경우 화면 표기 변경
2.1. 시작하기 버튼 hidden 처리
2.2. 사용자 이름 불러오기
2.3. 로그인 동작 관련 변수 스토리지 저장
2.4. 서버에서 직접적인 크라이언트 쿠키 저장이 아닌 서버는 뒤의 값으로 간섭하게 변경
2025-06-11 15:12:22 +09:00
3ffec93958 [] 로그인 및 화면 구조 변경
1. 회원가입 후 자동 로그인
2. 로그인 후 페이지 처리
3. 로딩 인디케이터 동작 구조 변경
2025-06-09 17:45:53 +09:00
47 changed files with 1319 additions and 249 deletions

View File

@ -8,17 +8,17 @@ namespace Front;
public partial class App : ComponentBase public partial class App : ComponentBase
{ {
[Inject] private APIService API { get; set; } = default!; [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) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
if (firstRender) if (firstRender)
{ {
var cookie = await Cookie.GetCookieAsync("Web_AM_Connect_Key"); var headerValue = await StorageService.GetItemAsync("Web_AM_Connect_Key");
// 값 없으면 API 호출 // 값 없으면 API 호출
if (string.IsNullOrEmpty(cookie)) if (string.IsNullOrEmpty(headerValue))
{ {
var response = await API.GetJsonAsync<APIHeader, AppHeader>( var response = await API.GetJsonAsync<APIHeader, AppHeader>(
"/api/v1/in/app", "/api/v1/in/app",
@ -30,7 +30,7 @@ public partial class App : ComponentBase
}); });
if (!string.IsNullOrEmpty(response.data.header)) 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> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.8"/> <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.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.8" PrivateAssets="all"/>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.10" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -27,7 +28,7 @@
<ItemGroup> <ItemGroup>
<Folder Include="Program\ViewModels\" /> <Folder Include="Program\ViewModels\" />
<Folder Include="Program\Views\Academy\" /> <Folder Include="Program\Views\Project\FooterLink\" />
<Folder Include="wwwroot\Resources\" /> <Folder Include="wwwroot\Resources\" />
</ItemGroup> </ItemGroup>

View File

@ -4,12 +4,16 @@ using Microsoft.Extensions.Configuration;
using Front; using Front;
using Front.Program.Services; using Front.Program.Services;
using Front.Program.ViewModels;
var builder = WebAssemblyHostBuilder.CreateDefault(args); var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app"); builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after"); 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 builder.Services.AddScoped(sp => //new HttpClient
{ {
@ -30,10 +34,11 @@ builder.Services.AddScoped(sp => //new HttpClient
// SCOPED 으로 등록된 서비스는 DI 컨테이너에 등록된 서비스의 인스턴스를 사용합니다. // SCOPED 으로 등록된 서비스는 DI 컨테이너에 등록된 서비스의 인스턴스를 사용합니다.
builder.Services.AddScoped<APIService>(); builder.Services.AddScoped<APIService>();
builder.Services.AddScoped<CookieService>(); builder.Services.AddScoped<SecureService>();
// builder.Services.AddRazorPages(); builder.Services.AddScoped<StorageService>();
// builder.Services.AddServerSideBlazor(); builder.Services.AddScoped<QueryParamService>();
builder.Services.AddScoped<LoadingService>();
builder.Services.AddScoped<LoadingService>();
builder.Services.AddScoped<UserStateService>();
await builder.Build().RunAsync(); await builder.Build().RunAsync();

View File

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

View File

@ -1,31 +1,37 @@
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Components.Routing;
using Front.Program.Views.Project; using Front.Program.Views.Project;
using Front.Program.Views.Academy;
using Front.Program.Services; using Front.Program.Services;
using Front.Program.ViewModels;
namespace Front.Program.Layout; namespace Front.Program.Layout;
public partial class MainLayout : LayoutComponentBase, IDisposable public partial class MainLayout : LayoutComponentBase, IDisposable
{ {
[Inject] [Inject] NavigationManager Navigation { get; set; } = default!;
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 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 isHideTop => Navigation.Uri.Contains("/auth"); // 경로 일치
protected bool isHideTop => Navigation.ToBaseRelativePath(Navigation.Uri).Equals("auth", StringComparison.OrdinalIgnoreCase); protected bool isIntro => Navigation.ToBaseRelativePath(Navigation.Uri).Equals("am/intro", StringComparison.OrdinalIgnoreCase);
public static bool IsLoading { get; set; }
public static IndicateType CurrentType { get; set; } = IndicateType.Page;
protected override void OnInitialized() protected override void OnInitialized()
{ {
LoadingService.OnChange += StateHasChanged; LoadingService.OnChange += StateHasChanged;
Navigation.LocationChanged += HandleLocationChanged; 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(); LoadingService.HideNavigationLoading();
} }
@ -35,15 +41,4 @@ public partial class MainLayout : LayoutComponentBase, IDisposable
LoadingService.OnChange -= StateHasChanged; LoadingService.OnChange -= StateHasChanged;
Navigation.LocationChanged -= HandleLocationChanged; 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}"); var response = await _http.GetFromJsonAsync<APIResponseStatus<TResponse>>($"{url}?{parameter}");
return response; 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; using Microsoft.AspNetCore.Components;
namespace Front.Program.Services; namespace Front.Program.Services;
// 뷰를 참조 안하고도 로딩 상태를 알 수 있게 바꾸기
public class LoadingService public class LoadingService
{ {
public bool IsLoading { get; private set; } public bool IsLoading { get; private set; }
public IndicateType CurrentType { get; private set; } = IndicateType.Page;
private bool isNavigationLoading { get; set; } private bool isNavigationLoading { get; set; }
public event Action? OnChange; public event Action? OnChange;
public void ShowLoading(IndicateType type = IndicateType.Page, bool isNavigation = false) public void ShowLoading(bool isNavigation = false)
{ {
IsLoading = true; IsLoading = true;
CurrentType = type;
isNavigationLoading = isNavigation; isNavigationLoading = isNavigation;
NotifyStateChanged(); 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

@ -168,7 +168,7 @@
</div> </div>
</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"> <div class="flex max-w-[960px] flex-1 flex-col">
<footer class="flex flex-col gap-6 px-5 py-10 text-center container"> <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"> <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> <p class="text-[#60758a] text-base font-normal leading-normal">© 2024 AcaMate. All rights reserved.</p>
</footer> </footer>
</div> </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>
</div> </div>

View File

@ -1,16 +1,71 @@
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
using Front.Program.ViewModels;
using Front.Program.Services; using Front.Program.Services;
namespace Front.Program.Views.Project; namespace Front.Program.Views.Project;
public partial class About : ComponentBase public partial class About : ComponentBase, IDisposable
{ {
[Inject] [Inject]
NavigationManager NavigationManager { get; set; } = default!; NavigationManager Navigation { get; set; } = default!;
private async Task OnClickEvent()
{
// NavigationManager.NavigateTo("/redirectpage");
Console.WriteLine("Redirecting to redirect page");
[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; using Microsoft.AspNetCore.Components;
namespace Front.Program.Views.Project; namespace Front.Program.Views.Project.Common;
public partial class RedirectPage : ComponentBase public partial class RedirectPage : ComponentBase
{ {
@ -9,6 +9,6 @@ public partial class RedirectPage : ComponentBase
protected override void OnInitialized() protected override void OnInitialized()
{ {
NavigationManager.NavigateTo("/about",true); NavigationManager.NavigateTo("/am/intro",true);
} }
} }

View File

@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
namespace Front.Program.Views.Project; namespace Front.Program.Views.Project.Common;
public partial class TopBanner : ComponentBase 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="/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> <a class="text-text-title font-medium leading-normal hover:text-blue-600" href="/new" @onclick="() => isOpen = !isOpen">What's New</a>
</div> </div>
@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" <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"> @onclick="OnClickLogin">
<span class="truncate">시작하기</span> <span class="truncate">시작하기</span>
</button> </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>
</div> </div>
<button class="md:hidden mr-4" @onclick="OnClickMenuDown"> <button class="md:hidden mr-4" @onclick="OnClickMenuDown">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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" <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="/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="/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> <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>
@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]" <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"> @onclick="OnClickLogin">
<span class="truncate">시작하기</span> <span class="truncate">시작하기</span>
</button> </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>
</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 <input
pattern="\d{16}" pattern="\d{16}"
maxlength="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" 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" @bind-value="@name"
/> />
@ -31,15 +31,41 @@
<div class="flex w-full flex-wrap items-end gap-4 px-4 py-3"> <div class="flex w-full flex-wrap items-end gap-4 px-4 py-3">
<label class="flex flex-col w-full"> <label class="flex flex-col w-full">
<p class="text-text-title text-base font-medium leading-normal pb-2">생일</p> <p class="text-text-title text-base font-medium leading-normal pb-2">생일</p>
<div class="flex w-full max-w-[480px] items-center gap-2">
<input <input
type="date" maxlength="4"
@ref="dateInputRef" pattern="\d{4}"
placeholder="YYYYMMDD" placeholder="YYYY"
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" 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="birth" @bind-value="birthYear"
@onclick="OpenDatePicker" inputmode="numeric"
readonly 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> </label>
</div> </div>
@ -49,7 +75,7 @@
<span class="text-red-600">*</span> <span class="text-red-600">*</span>
</p> </p>
<input <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" 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"/> @bind-value="email"/>
</label> </label>

View File

@ -5,8 +5,9 @@ using System.Net.Http.Json;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Net.Http.Json; using System.Net.Http.Json;
using Front.Program.Layout; using Front.Program.Layout;
using Front.Program.Services;
namespace Front.Program.Views.Project; namespace Front.Program.Views.Project.ConnectUser;
public partial class Register : ComponentBase public partial class Register : ComponentBase
{ {
@ -14,10 +15,15 @@ public partial class Register : ComponentBase
[Inject] private NavigationManager NavigationManager { get; set; } = default!; [Inject] private NavigationManager NavigationManager { get; set; } = default!;
[Inject] private HttpClient Http { get; set; } = default!; [Inject] private HttpClient Http { get; set; } = default!;
[Inject] private IConfiguration Configuration { 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 ElementReference dateInputRef;
private string name = ""; private string name = "";
private string birthYear = "";
private string birthMonth = "";
private string birthDay = "";
private DateTime? birth; private DateTime? birth;
private string email = ""; private string email = "";
private string phone = ""; private string phone = "";
@ -34,8 +40,7 @@ public partial class Register : ComponentBase
objRef = DotNetObjectReference.Create(this); objRef = DotNetObjectReference.Create(this);
try try
{ {
// 쿠키에서 토큰 가져오기 var token = await CookieService.GetItemAsync("Web_AM_Connect_Key");
var token = await JS.InvokeAsync<string>("eval", "document.cookie.split('; ').find(row => row.startsWith('Web_AM_Connect_Key='))?.split('=')[1]");
if (string.IsNullOrEmpty(token)) if (string.IsNullOrEmpty(token))
{ {
await JS.InvokeVoidAsync("alert", "인증 정보가 없습니다."); 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() private async Task ConfirmData()
{ {
if (string.IsNullOrWhiteSpace(name)) if (string.IsNullOrWhiteSpace(name))
@ -103,6 +129,9 @@ public partial class Register : ComponentBase
return; return;
} }
// 생일 업데이트
UpdateBirthDate();
// 전화번호 조합 // 전화번호 조합
if (phonePart1.Length == 3 && phonePart2.Length == 4 && phonePart3.Length == 4) if (phonePart1.Length == 3 && phonePart2.Length == 4 && phonePart3.Length == 4)
{ {
@ -139,9 +168,8 @@ public partial class Register : ComponentBase
try try
{ {
MainLayout.ShowLoading(); LoadingService.ShowLoading();
// 쿠키에서 토큰 가져오기 var token = await CookieService.GetItemAsync("Web_AM_Connect_Key");
var token = await JS.InvokeAsync<string>("eval", "document.cookie.split('; ').find(row => row.startsWith('Web_AM_Connect_Key='))?.split('=')[1] || ''");
Console.WriteLine($"쿠키에서 가져온 토큰: '{token}'"); Console.WriteLine($"쿠키에서 가져온 토큰: '{token}'");
if (string.IsNullOrEmpty(token)) if (string.IsNullOrEmpty(token))
@ -149,7 +177,7 @@ public partial class Register : ComponentBase
await JS.InvokeVoidAsync("alert", "인증 정보가 없습니다."); await JS.InvokeVoidAsync("alert", "인증 정보가 없습니다.");
NavigationManager.NavigateTo("/"); NavigationManager.NavigateTo("/");
MainLayout.HideLoading(); LoadingService.HideLoading();
return; return;
} }
@ -186,30 +214,27 @@ public partial class Register : ComponentBase
// 세션 스토리지 정리 // 세션 스토리지 정리
await JS.InvokeVoidAsync("sessionStorage.removeItem", "snsId"); await JS.InvokeVoidAsync("sessionStorage.removeItem", "snsId");
MainLayout.HideLoading(); LoadingService.HideLoading();
await JS.InvokeVoidAsync("alert", "회원가입이 완료되었습니다."); await JS.InvokeVoidAsync("alert", "회원가입이 완료되었습니다.");
NavigationManager.NavigateTo("/"); NavigationManager.NavigateTo("/");
} }
else else
{ {
MainLayout.HideLoading(); LoadingService.HideLoading();
await JS.InvokeVoidAsync("alert", $"회원가입 실패: {message}"); await JS.InvokeVoidAsync("alert", $"회원가입에 실패하였습니다.\n잠시 후 다시 시도해주세요.");
} }
} }
else else
{ {
MainLayout.HideLoading(); LoadingService.HideLoading();
Console.WriteLine($"API 호출 실패: {response.StatusCode}"); await JS.InvokeVoidAsync("alert", "회원가입 중 오류가 발생했습니다.\n잠시 후 다시 시도해주세요.");
var errorContent = await response.Content.ReadAsStringAsync();
Console.WriteLine($"에러 내용: {errorContent}");
await JS.InvokeVoidAsync("alert", "서버 오류가 발생했습니다.");
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
MainLayout.HideLoading(); LoadingService.HideLoading();
Console.WriteLine($"예외 발생: {ex.Message}"); Console.WriteLine($"예외 발생: {ex.Message}");
await JS.InvokeVoidAsync("alert", $"오류가 발생했습니다: {ex.Message}"); await JS.InvokeVoidAsync("alert", "회원가입 중 오류가 발생했습니다.\n잠시 후 다시 시도해주세요.");
} }
} }
@ -227,9 +252,17 @@ public partial class Register : ComponentBase
} }
private async Task OpenDatePicker() private async Task OpenDatePicker()
{
try
{ {
if (birth == null) birth = DateTime.Now; if (birth == null) birth = DateTime.Now;
await JS.InvokeVoidAsync("openDatePicker", dateInputRef); // showPicker 대신 click() 이벤트를 발생시켜 달력을 표시
await JS.InvokeVoidAsync("eval", $"document.querySelector('input[type=\"date\"]').click()");
}
catch (Exception ex)
{
Console.WriteLine($"달력 표시 오류: {ex.Message}");
}
} }
private void OnBirthChanged(ChangeEventArgs e) 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
@using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http @using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop @using Microsoft.JSInterop
@using Front @using Front
@using Front.Program.Layout @using Front.Program.Layout
@using Front.Program.Views.Project @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"> <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="flex flex-col items-center">
<div class="relative w-48 h-48 overflow-hidden"> <div class="relative w-48 h-48 overflow-hidden">
@ -48,12 +48,20 @@
Blazor.start().then(() => { Blazor.start().then(() => {
const elapsed = performance.now() - loadingStart; const elapsed = performance.now() - loadingStart;
const remaining = Math.max(0, MIN_LOADING_MS - elapsed); const remaining = Math.max(0, MIN_LOADING_MS - elapsed);
setTimeout(() => {
const overlay = document.getElementById('loading-overlay'); 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(); if (overlay) overlay.remove();
}, remaining); }, remaining);
} else if (overlay) {
overlay.remove();
}
}).catch(err => { }).catch(err => {
console.error("❌ Blazor 로딩 실패", err); console.error("Blazor 로딩 실패", err);
}); });
</script> </script>
@ -62,6 +70,7 @@
const v = document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)'); const v = document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)');
return v ? v.pop() : ''; return v ? v.pop() : '';
}; };
window.setCookie = function (name, value, days) { window.setCookie = function (name, value, days) {
let expires = ""; let expires = "";
if (days) { if (days) {

View File

@ -11,13 +11,101 @@ window.postWithHeader = function(url, method, headerKey, headerValue) {
}); });
}; };
window.fetchWithHeaderAndReturnUrl = async function(url, method, 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 response = await fetch(url, { const response = await fetch(url, {
method: method, method: args.method,
headers: { headers: {
[headerKey]: headerValue [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(); const data = await response.json();
return data.url; 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;
}
}; };