[♻️] 로그인 동작 로직 개편

1. 로그인 동작을 위해서 viewmodel 로 관련 뷰에서 동작할 모든 로직을 viewmodel에서 관리
1.1. view 와 viewmodel의 관계는 1:N으로 동작하는것을 기반으로 두고 있음
2. API 접근하는 방식도 웹만의 접근 방법에서 수정
3. 로그인 동작 정보 받는 로직 수정
This commit is contained in:
SEAN-59 2025-06-16 17:47:35 +09:00
parent c371700e78
commit 2665dcbf64
16 changed files with 374 additions and 189 deletions

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>
@ -26,8 +27,6 @@
</ItemGroup>
<ItemGroup>
<Folder Include="Program\ViewModels\" />
<Folder Include="Program\Views\Academy\" />
<Folder Include="wwwroot\Resources\" />
</ItemGroup>

View File

@ -4,7 +4,8 @@ using Microsoft.Extensions.Configuration;
using Front;
using Front.Program.Services;
using Front.Program.ViewModels;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
@ -35,10 +36,12 @@ builder.Services.AddScoped(sp => //new HttpClient
builder.Services.AddScoped<APIService>();
builder.Services.AddScoped<SecureService>();
builder.Services.AddScoped<StorageService>();
builder.Services.AddScoped<QueryParamService>();
// builder.Services.AddRazorPages();
// builder.Services.AddServerSideBlazor();
builder.Services.AddScoped<LoadingService>();
builder.Services.AddScoped<UserService>();
builder.Services.AddScoped<UserViewModel>();
await builder.Build().RunAsync();

View File

@ -1,4 +1,4 @@
@inherits LayoutComponentBase
@inherits LayoutComponentBase
@implements IDisposable
<div class="min-h-screen flex flex-col bg-gray-50 text-gray-900">

View File

@ -32,40 +32,41 @@ public partial class MainLayout : LayoutComponentBase, IDisposable
HandleLocationChanged(this, new LocationChangedEventArgs(Navigation.Uri, false));
}
// 페이지의 URL이 변경될 때마다 실행되는 이벤트 핸들러
private async void HandleLocationChanged(object? sender, LocationChangedEventArgs e)
{
LoadingService.HideNavigationLoading();
var uri = Navigation.ToAbsoluteUri(Navigation.Uri);
Console.WriteLine($"리다이렉트된 URI: {uri}");
if (uri.Query.Contains("auth="))
{
var query = uri.Query.TrimStart('?');
var parameters = query.Split('&')
.Select(p => p.Split('='))
.Where(p => p.Length == 2)
.ToDictionary(p => p[0], p => p[1]);
if (parameters.TryGetValue("auth", out var auth))
{
Console.WriteLine($"auth 파라미터 값: {auth}");
if (auth == "true")
{
await StorageService.SetItemAsync("IsLogin", "true");
Console.WriteLine("로그인 상태를 true로 설정했습니다.");
}
else
{
await StorageService.RemoveItemAsync("IsLogin");
Console.WriteLine("로그인 상태를 제거했습니다.");
}
// 파라미터를 제거하고 리다이렉트
var baseUri = uri.GetLeftPart(UriPartial.Path);
Navigation.NavigateTo(baseUri, forceLoad: false);
}
}
//
// var uri = Navigation.ToAbsoluteUri(Navigation.Uri);
// Console.WriteLine($"리다이렉트된 URI: {uri}");
//
// if (uri.Query.Contains("auth="))
// {
// var query = uri.Query.TrimStart('?');
// var parameters = query.Split('&')
// .Select(p => p.Split('='))
// .Where(p => p.Length == 2)
// .ToDictionary(p => p[0], p => p[1]);
//
// if (parameters.TryGetValue("auth", out var auth))
// {
// Console.WriteLine($"auth 파라미터 값: {auth}");
// if (auth == "true")
// {
// await StorageService.SetItemAsync("IsLogin", "true");
// Console.WriteLine("로그인 상태를 true로 설정했습니다.");
// }
// else
// {
// await StorageService.RemoveItemAsync("IsLogin");
// Console.WriteLine("로그인 상태를 제거했습니다.");
// }
//
// // 파라미터를 제거하고 리다이렉트
// var baseUri = uri.GetLeftPart(UriPartial.Path);
// Navigation.NavigateTo(baseUri, forceLoad: false);
// }
// }
}
public void Dispose()

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,6 +28,7 @@ public class APIService
var response = await _http.GetFromJsonAsync<APIResponseStatus<TResponse>>($"{url}?{parameter}");
return response;
}
}
/*

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

@ -1,4 +1,5 @@
using System.Text.Json;
using Front.Program.Models;
using Microsoft.JSInterop;
namespace Front.Program.Services;
@ -22,18 +23,7 @@ public class UserService
_logger = logger;
}
public class UserData
{
public string Uid { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public DateTime? Birth { get; set; }
public string Type { get; set; } = string.Empty;
public string? DeviceId { get; set; }
public bool AutoLoginYn { get; set; }
public DateTime LoginDate { get; set; }
public string? PushToken { get; set; }
}
public async Task<(bool success, UserData? userData)> GetUserData()
{
try

View File

@ -0,0 +1,158 @@
using System.Text.Json;
using Front.Program.Services;
using Front.Program.Models;
using Microsoft.JSInterop;
namespace Front.Program.ViewModels;
public class UserViewModel(StorageService _storageService,SecureService _secureService, IJSRuntime _js)
{
public UserData UserData { get; set; } = new UserData();
public bool isLogin { get; set; } = false;
public async Task<(bool success, UserData? userData)> GetUserDataFromStorageAsync()
{
try
{
var encUserData = await _storageService.GetItemAsync("USER_DATA");
if (!string.IsNullOrEmpty(encUserData))
{
var decUserData = await _secureService.DecryptAsync(encUserData);
var userData = JsonSerializer.Deserialize<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()
{
Console.WriteLine("GetUserDataAsync 호출됨");
// 로그인 상태가 아니라면 애초에 할 필요 없음
if (await _storageService.GetItemAsync("IsLogin") != "true")
{
isLogin = false;
return false;
}
var userDataForm = await GetUserDataFromStorageAsync();
if (userDataForm.success && userDataForm.userData != null)
{
// 사용자 데이터가 성공적으로 로드되었을 때의 로직
UserData = userDataForm.userData;
isLogin = true;
return true;
}
else
{
var userDataFromServer = await GetUserDataFromServerAsync();
if (userDataFromServer.success && userDataFromServer.userData != null)
{
// 서버에서 사용자 데이터를 성공적으로 로드했을 때의 로직
UserData = userDataFromServer.userData;
isLogin = true;
return true;
}
else
{
// 사용자 데이터를 로드하지 못했을 때의 로직
Console.WriteLine("사용자 데이터를 로드하지 못했습니다.");
isLogin = false;
return false;
}
}
}
public async Task ClearUserData()
{
try
{
await _storageService.RemoveItemAsync("USER_DATA");
await _storageService.RemoveItemAsync("IsLogin");
Console.WriteLine("사용자 데이터 삭제 성공");
isLogin = false;
// return true;
}
catch (Exception ex)
{
Console.WriteLine($"사용자 데이터 삭제 중 오류: {ex.Message}");
// return false;
}
}
}

View File

@ -1,17 +1,12 @@
<div class="flex items-center justify-between whitespace-nowrap border-b border-solid border-b-[#f0f2f5] h-[72px] bg-white">
<div class="flex items-center gap-4 text-[#111418] ml-4">
<div class="w-10 h-10 rounded-full bg-gray-200 overflow-hidden">
<img src="/logo.png" alt="Icon" class="w-full h-full object-cover">
</div>
<h2 class="hidden md:block text-text-title text-lg font-bold leading-tight tracking-[-0.015em]">AcaMate</h2>
</div>
<div class="hidden md:flex flex-1 justify-end gap-8">
<div class="hidden md:flex flex-1 justify- 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>
<a class="text-text-title font-medium leading-normal hover:text-blue-600" href="/join">Join</a>
<a class="text-text-title font-medium leading-normal hover:text-blue-600" href="/new">What's New</a>
</div>
</div>
</div>

View File

@ -1,6 +1,22 @@
using Front.Program.ViewModels;
using Microsoft.AspNetCore.Components;
namespace Front.Program.Views.Academy;
public partial class TopNavAcademy : ComponentBase
{}
{
[Inject] UserViewModel UserViewModel { get; set; } = default!;
protected bool isOpen = false;
protected override async Task OnInitializedAsync()
{
Console.WriteLine("TOPNAV_OnInitializedAsync");
if (string.IsNullOrEmpty(UserViewModel.UserData.Name))
{
await UserViewModel.GetUserDataAsync();
}
}
}

View File

@ -1,16 +1,77 @@
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!;
NavigationManager Navigation { get; set; } = default!;
[Inject]
StorageService StorageService { get; set; } = default!;
[Inject]
QueryParamService QueryParamService { get; set; } = default!;
[Inject] UserViewModel UserViewModel { 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 Task OnClickEvent()
{
// NavigationManager.NavigateTo("/redirectpage");
Console.WriteLine("Redirecting to redirect page");
}
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 UserViewModel.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

@ -11,7 +11,7 @@
<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>
@if (!isLogin)
@if (!UserViewModel.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">
@ -47,7 +47,7 @@
<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>
@if (!isLogin)
@if (!UserViewModel.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">

View File

@ -3,142 +3,32 @@ 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;
public partial class TopProjectNav : ComponentBase
{
[Inject] NavigationManager NavigationManager { get; set; } = default!;
[Inject] UserService UserService { get; set; } = default!;
[Inject] UserViewModel UserViewModel { get; set; } = default!;
[Inject] IJSRuntime JS { get; set; } = default!;
[Inject] HttpClient Http { get; set; } = default!;
string UserName { get; set; } = default!;
protected bool isOpen = false;
bool isLogin = false;
protected override async Task OnInitializedAsync()
{
var (success, userData) = await UserService.GetUserData();
if (success && userData != null)
{
Console.WriteLine(userData.Name);
UserName = userData.Name;
isLogin = true;
}
// isLogin = bool.TryParse(await StorageService.GetItemAsync("IsLogin"), out var result) && result;
// // if (!isLogin && !string.IsNullOrEmpty(UserName)) return;
// if (!isLogin) return; // && !string.IsNullOrEmpty(UserName)) return;
Console.WriteLine("TOPNAV_OnInitializedAsync");
// try
// {
// var encryptedName = await StorageService.GetItemAsync("USER");
// Console.WriteLine($"{encryptedName}");
// if (!string.IsNullOrEmpty(encryptedName))
// {
// try
// {
// UserName = await SecureService.DecryptAsync(encryptedName);
// Console.WriteLine($"세션 스토리지에서 가져온 사용자 이름: '{UserName}'");
// return;
// }
// catch (Exception ex)
// {
// Console.WriteLine($"이름 복호화 중 오류 발생: {ex.Message}");
// await StorageService.RemoveItemAsync("USER");
// }
// }
// // apiSender.js의 함수를 사용하여 연결 키 가져오기
// var headerValue = await StorageService.GetItemAsync("Web_AM_Connect_Key");
// Console.WriteLine($"세션 스토리지에서 가져온 헤더 값: '{headerValue}'");
// if (string.IsNullOrEmpty(headerValue))
// {
// Console.WriteLine("연결 키가 없습니다");
// return;
// }
// // apiSender.js의 함수를 사용하여 API 호출
// Console.WriteLine("세션 API 호출 시작");
// var response = await JS.InvokeAsync<JsonElement>("fetchWithHeaderAndReturnUrl",
// "/api/v1/in/user/auth/session",
// "GET",
// "Web_AM_Connect_Key",
// headerValue);
// Console.WriteLine($"세션 API 응답 타입: {response.ValueKind}");
// Console.WriteLine($"세션 API 응답 내용: {response}");
// if (response.ValueKind == JsonValueKind.Null || response.ValueKind == JsonValueKind.Undefined)
// {
// Console.WriteLine("응답이 null이거나 undefined입니다");
// return;
// }
// try
// {
// if (response.TryGetProperty("status", out var statusElement))
// {
// Console.WriteLine($"status 요소 타입: {statusElement.ValueKind}");
// if (statusElement.TryGetProperty("code", out var codeElement))
// {
// var code = codeElement.GetString();
// Console.WriteLine($"응답 코드: {code}");
// if (code == "000")
// {
// if (response.TryGetProperty("data", out var dataElement))
// {
// Console.WriteLine($"data 요소 타입: {dataElement.ValueKind}");
// // 전체 data를 JSON 문자열로 변환
// var userDataJson = dataElement.ToString();
// // 전체 데이터 암호화
// var encryptedUserData = await SecureService.EncryptAsync(userDataJson);
// await StorageService.SetItemAsync("USER_DATA", encryptedUserData);
// // 기존 name 처리 로직
// if (dataElement.TryGetProperty("name", out var nameElement))
// {
// UserName = nameElement.GetString() ?? "이름 없음";
// isLogin = true;
// Console.WriteLine($"NM: {UserName}");
// var encryptedUserName = await SecureService.EncryptAsync(UserName);
// Console.WriteLine($"NM: {encryptedUserName}");
// await StorageService.SetItemAsync("USER", encryptedUserName);
// await StorageService.SetItemAsync("Web_AM_Connect_Key", headerValue);
// Console.WriteLine($"로그인된 사용자: {UserName}");
// return;
// }
// }
// }
// else {
// if (isLogin) {
// isLogin = false;
// await StorageService.SetItemAsync("IsLogin","false");
// }
// }
// }
// }
// Console.WriteLine("로그인되지 않은 상태");
// }
// catch (Exception ex)
// {
// Console.WriteLine($"응답 처리 중 오류 발생: {ex.Message}");
// Console.WriteLine($"응답 처리 스택 트레이스: {ex.StackTrace}");
// }
// }
// catch (Exception ex)
// {
// Console.WriteLine($"세션 확인 중 오류 발생: {ex.Message}");
// Console.WriteLine($"스택 트레이스: {ex.StackTrace}");
// }
if (string.IsNullOrEmpty(UserViewModel.UserData.Name))
{
await UserViewModel.GetUserDataAsync();
}
}
public void OnClickMenuDown()
@ -158,12 +48,13 @@ public partial class TopProjectNav : ComponentBase
NavigationManager.NavigateTo("/auth");
}
public async Task OnClickLogout()
public async Task OnClickLogout()
{
if (await UserService.ClearUserData())
{
isLogin = false;
UserName = null;
}
await UserViewModel.ClearUserData();
//
// if (await UserViewModel.ClearUserData())
// {
// isLogin = false;
// }
}
}

File diff suppressed because one or more lines are too long

View File

@ -11,20 +11,35 @@ 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, {
method: method,
method: args.method,
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();
console.log('API 응답 데이터:', data);
return data;