Compare commits
39 Commits
Author | SHA1 | Date | |
---|---|---|---|
92feb9bbe2 | |||
6342324108 | |||
3cbe24216d | |||
e8b942a633 | |||
def25d2206 | |||
c04152dac8 | |||
250aac4b29 | |||
1e563eb576 | |||
d89db8c890 | |||
e8580650cc | |||
3e31f9f46b | |||
2665dcbf64 | |||
c371700e78 | |||
05e8fcd0b0 | |||
9621169d57 | |||
3ffec93958 | |||
1649818434 | |||
0698f65ddf | |||
f3fee47c28 | |||
c178a17c04 | |||
a76e5fd574 | |||
e207e246cd | |||
a03aabc8fb | |||
f4d0138fec | |||
070e15ae70 | |||
9e2151cbd7 | |||
c1fdf2773d | |||
899a563aac | |||
9167e2a9d6 | |||
1f3b6b3217 | |||
d51449aa36 | |||
8da46338e5 | |||
02d5797223 | |||
9ca480a134 | |||
7a34c9f95c | |||
1d2f8db794 | |||
9447bc3053 | |||
fc5e072697 | |||
96b4937653 |
10
App.razor
|
@ -27,9 +27,11 @@ AppAssembly: 현재 애플리케이션의 어셈블리를 지정
|
|||
role="alert": 스크린 리더와 같은 보조 기술에 의해 읽히는 경고 메시지
|
||||
-->
|
||||
<NotFound>
|
||||
<PageTitle>Not found</PageTitle>
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
<p role="alert">Sorry, there's nothing at this address.</p>
|
||||
</LayoutView>
|
||||
@* <PageTitle>Not found</PageTitle> *@
|
||||
@* <LayoutView Layout="@typeof(MainLayout)"> *@
|
||||
@* <p role="alert">Sorry, there's nothing at this address.</p> *@
|
||||
@* *@
|
||||
@* </LayoutView> *@
|
||||
<RedirectPage />
|
||||
</NotFound>
|
||||
</Router>
|
||||
|
|
38
App.razor.cs
Normal file
|
@ -0,0 +1,38 @@
|
|||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
using Front.Program.Models;
|
||||
using Front.Program.Services;
|
||||
|
||||
namespace Front;
|
||||
|
||||
public partial class App : ComponentBase
|
||||
{
|
||||
[Inject] private APIService API { get; set; } = default!;
|
||||
[Inject] private StorageService StorageService { get; set; } = default!;
|
||||
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
var headerValue = await StorageService.GetItemAsync("Web-AM-Connect-Key");
|
||||
|
||||
// 값 없으면 API 호출
|
||||
if (string.IsNullOrEmpty(headerValue))
|
||||
{
|
||||
var response = await API.GetJsonAsync<APIHeader, AppHeader>(
|
||||
"/api/v1/in/app",
|
||||
new AppHeader()
|
||||
{
|
||||
type = "W",
|
||||
specific = "Web_Connect",
|
||||
project = "AcaMate"
|
||||
});
|
||||
if (!string.IsNullOrEmpty(response.data.header))
|
||||
{
|
||||
await StorageService.SetItemAsync("Web-AM-Connect-Key", response.data.header);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,15 +27,10 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Program\Models\" />
|
||||
<Folder Include="Program\ViewModels\" />
|
||||
<Folder Include="Program\Views\Academy\" />
|
||||
<Folder Include="Program\Views\Project\FooterLink\" />
|
||||
<Folder Include="wwwroot\Resources\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="wwwroot\Resources\Images\.DS_Store" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
</Project>
|
||||
|
|
16
Jenkinsfile
vendored
|
@ -5,8 +5,22 @@ pipeline {
|
|||
DOCKER_DEBUG_CONTAINER = 'acamate-front-build-debug'
|
||||
APP_VOLUME = '/src'
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Clear Repository') {
|
||||
steps {
|
||||
script {
|
||||
sh """
|
||||
echo 'Clearing Front directory'
|
||||
docker run --rm -v /volume1/AcaMate/PROJECT/Application/Front:/front alpine \
|
||||
sh -c "find /front -mindepth 1 -maxdepth 1 \\
|
||||
! -name 'privacy' \\
|
||||
! -name 'publish' \\
|
||||
-exec rm -rf {} +"
|
||||
echo 'Clean complete'
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Clone Repository') {
|
||||
steps {
|
||||
git url: 'https://git.ipstein.myds.me/AcaMate/AcaMate_Web.git', branch: env.GIT_BRANCH
|
||||
|
|
41
Program.cs
|
@ -4,22 +4,57 @@ using Microsoft.Extensions.Configuration;
|
|||
|
||||
using Front;
|
||||
using Front.Program.Services;
|
||||
using Front.Program.ViewModels;
|
||||
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
|
||||
// 로컬 또는 Dev 환경인지 확인
|
||||
var currentUri = new Uri(builder.HostEnvironment.BaseAddress);
|
||||
bool isLocal = currentUri.Host == "0.0.0.0" || currentUri.Port.ToString() == "5144";
|
||||
bool isDev = isLocal ||
|
||||
builder.HostEnvironment.IsDevelopment();
|
||||
|
||||
Uri CheckLocal()
|
||||
{
|
||||
if (isLocal)
|
||||
return new Uri("http://0.0.0.0:5144");
|
||||
else
|
||||
return builder.HostEnvironment.IsDevelopment() ?
|
||||
new Uri("https://devacamate.ipstein.myds.me") :
|
||||
new Uri("https://acamate.ipstein.myds.me");
|
||||
}
|
||||
|
||||
LoggerService.Initialize(isDev);
|
||||
|
||||
|
||||
builder.RootComponents.Add<App>("#app");
|
||||
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||
|
||||
// builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||
// 설정 파일 로드
|
||||
builder.Configuration.AddJsonFile($"appsettings.{builder.HostEnvironment.Environment}.json", optional: true);
|
||||
|
||||
|
||||
builder.Services.AddScoped(sp => //new HttpClient
|
||||
{
|
||||
// BaseAddress = new Uri("https://localhost:5144")
|
||||
var config = builder.Configuration;
|
||||
var http = new HttpClient();
|
||||
var http = new HttpClient
|
||||
{
|
||||
BaseAddress = CheckLocal()
|
||||
};
|
||||
return http;
|
||||
});
|
||||
|
||||
// SCOPED 으로 등록된 서비스는 DI 컨테이너에 등록된 서비스의 인스턴스를 사용합니다.
|
||||
builder.Services.AddScoped<APIService>();
|
||||
builder.Services.AddScoped<SecureService>();
|
||||
builder.Services.AddScoped<StorageService>();
|
||||
builder.Services.AddScoped<QueryParamService>();
|
||||
builder.Services.AddScoped<LoadingService>();
|
||||
|
||||
builder.Services.AddSingleton<UserStateService>();
|
||||
|
||||
|
||||
// builder.Services.AddSingleton<LoggerService>(sp
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
|
|
|
@ -1,45 +1,45 @@
|
|||
@* @inherits LayoutComponentBase *@
|
||||
@* *@
|
||||
@* *@
|
||||
@* <div class="min-h-screen bg-gray-50 text-gray-900"> *@
|
||||
@* <TopBanner /> *@
|
||||
@* <TopNav /> *@
|
||||
@* *@
|
||||
@* <div class="flex flex-1"> *@
|
||||
@* <SideNav /> *@
|
||||
@* *@
|
||||
@* <main class="flex-1 p-6"> *@
|
||||
@* $1$ <!-- Body는 URL 뒤에 입력할 페이지에 따라서 그거에 맞는 @page를 찾아서 열어준다. --> #1# *@
|
||||
@* @Body *@
|
||||
@* </main> *@
|
||||
@* </div> *@
|
||||
@* *@
|
||||
@* <FloatingButton /> *@
|
||||
@* <BottomNav /> *@
|
||||
@* <Footer/> *@
|
||||
@* </div> *@
|
||||
|
||||
@inherits LayoutComponentBase
|
||||
@inherits LayoutComponentBase
|
||||
@implements IDisposable
|
||||
|
||||
<div class="min-h-screen flex flex-col bg-gray-50 text-gray-900">
|
||||
|
||||
<!-- Top 영역 -->
|
||||
<TopBanner />
|
||||
<TopNav />
|
||||
|
||||
<!-- 본문 영역 -->
|
||||
@* <TopBanner /> *@
|
||||
@if(isAcademy)
|
||||
{
|
||||
<div class="flex flex-1 flex-col md:flex-row">
|
||||
|
||||
@* <!-- 사이드 메뉴 --> *@
|
||||
@* <div class="hidden md:block md:w-64 bg-white shadow"> *@
|
||||
@* <SideNav /> *@
|
||||
@* </div> *@
|
||||
|
||||
<!-- 본문 컨텐츠 -->
|
||||
<main class="flex-1 p-4 sm:p-6 md:p-8">
|
||||
@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 md:left-64 left-0 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>
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (!isHidePrjTop)
|
||||
{
|
||||
<TopProjectNav />
|
||||
}
|
||||
<!-- 본문 컨텐츠 -->
|
||||
<main class="flex-1 w-full w-max-960 mx-auto">
|
||||
@Body
|
||||
</main>
|
||||
}
|
||||
|
||||
<!-- 플로팅 버튼 -->
|
||||
<FloatingButton />
|
||||
|
@ -47,4 +47,13 @@
|
|||
<!-- 하단 메뉴 -->
|
||||
<BottomNav />
|
||||
<Footer />
|
||||
|
||||
@if (LoadingService.IsLoading)
|
||||
{
|
||||
<div class="fixed inset-0 bg-black/30 flex items-center justify-center z-50">
|
||||
<div class="bg-gray-200/60 px-6 py-4 rounded-lg">
|
||||
<div class="animate-spin h-8 w-8 border-4 border-gray-600 border-t-transparent rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
92
Program/Layout/MainLayout.razor.cs
Normal file
|
@ -0,0 +1,92 @@
|
|||
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] LoadingService LoadingService { get; set; } = default!;
|
||||
[Inject] UserStateService UserStateService { get; set; } = default!;
|
||||
|
||||
|
||||
private bool _isLoading = false;
|
||||
|
||||
// 경로의 시작 부분
|
||||
// protected bool isHidePrjTop => Navigation.ToBaseRelativePath(Navigation.Uri).StartsWith("auth", StringComparison.OrdinalIgnoreCase);
|
||||
protected bool isIntro => Navigation.ToBaseRelativePath(Navigation.Uri).StartsWith("am/intro", StringComparison.OrdinalIgnoreCase)
|
||||
|| Navigation.ToBaseRelativePath(Navigation.Uri).StartsWith("/", 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 async Task OnInitializedAsync()
|
||||
{
|
||||
LoggerService.Write("MainLayout OnInitializedAsync 시작");
|
||||
LoadingService.OnChange += StateHasChanged;
|
||||
Navigation.LocationChanged += HandleLocationChanged;
|
||||
HandleLocationChanged(this, new LocationChangedEventArgs(Navigation.Uri, false));
|
||||
|
||||
LoggerService.Write("MainLayout: UserStateService.GetUserDataAsync 호출 전");
|
||||
await UserStateService.GetUserDataAsync();
|
||||
LoggerService.Write($"MainLayout: UserStateService.isLogin 상태: {UserStateService.isLogin}");
|
||||
|
||||
// 학원 정보 로드 및 실패 시 처리
|
||||
var academyResult = await UserStateService.GetAcademy();
|
||||
LoadingService.ShowLoading();
|
||||
if (academyResult.success)
|
||||
{
|
||||
if (academyResult.simpleAcademy != null && academyResult.simpleAcademy.Any())
|
||||
{
|
||||
UserStateService.academyItems = academyResult.simpleAcademy.ToArray();
|
||||
LoggerService.Write($"MainLayout: academyItems 로드 성공. {UserStateService.academyItems.Length}개");
|
||||
}
|
||||
else
|
||||
{
|
||||
LoggerService.Write("MainLayout: 로드된 학원 정보가 없습니다.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LoggerService.Write("MainLayout: 서버 세션에서 학원 정보 불러오기 실패. 사용자 상태 초기화.");
|
||||
await UserStateService.ClearUserStateAsnyc();
|
||||
}
|
||||
|
||||
if (isAcademy)
|
||||
{
|
||||
if(!UserStateService.isLogin || UserStateService.UserData == null)
|
||||
{
|
||||
LoggerService.Write("로그인 상태가 아닙니다. 초기로 돌립니다.");
|
||||
if (isIntro) { }
|
||||
else
|
||||
{
|
||||
await UserStateService.ClearUserStateAsnyc();
|
||||
LoadingService.HideLoading();
|
||||
Navigation.NavigateTo("/");
|
||||
}
|
||||
}
|
||||
LoadingService.HideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
// 페이지의 URL이 변경될 때마다 실행되는 이벤트 핸들러
|
||||
private async void HandleLocationChanged(object? sender, LocationChangedEventArgs e)
|
||||
{
|
||||
LoadingService.HideNavigationLoading();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
LoadingService.OnChange -= StateHasChanged;
|
||||
Navigation.LocationChanged -= HandleLocationChanged;
|
||||
}
|
||||
}
|
32
Program/Models/APIResponse.cs
Normal file
|
@ -0,0 +1,32 @@
|
|||
using System.Text.Json;
|
||||
|
||||
namespace Front.Program.Models
|
||||
{
|
||||
public class APIResponseStatus<T>
|
||||
{
|
||||
public Status status { get; set; }
|
||||
public T? data { get; set; }
|
||||
|
||||
public string JsonToString()
|
||||
{
|
||||
return JsonSerializer.Serialize(this);
|
||||
}
|
||||
}
|
||||
|
||||
public class Status
|
||||
{
|
||||
public string code { get; set; }
|
||||
public string message { get; set; }
|
||||
|
||||
}
|
||||
|
||||
public class AppHeader
|
||||
{
|
||||
public string type { get; set; }
|
||||
public string specific { get; set; }
|
||||
public string project { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
22
Program/Models/AcademyModels.cs
Normal 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 name { get; set; }
|
||||
}
|
6
Program/Models/AppModel.cs
Normal file
|
@ -0,0 +1,6 @@
|
|||
namespace Front.Program.Models;
|
||||
|
||||
public class APIHeader
|
||||
{
|
||||
public string header { get; set; } = string.Empty;
|
||||
}
|
13
Program/Models/UserData.cs
Normal 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; }
|
||||
}
|
|
@ -1,12 +1,93 @@
|
|||
using System.Net.Http.Json;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using Front.Program.Models;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Front.Program.Services;
|
||||
|
||||
public class APIService
|
||||
public class APIService(HttpClient http,
|
||||
StorageService storageService,
|
||||
SecureService secureService,
|
||||
IJSRuntime js)
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
|
||||
public APIService(HttpClient http)
|
||||
|
||||
private string ChangeToString<T>(T data)
|
||||
{
|
||||
_http = http;
|
||||
if (data == null) return string.Empty;
|
||||
var properties = typeof(T).GetProperties();
|
||||
var value = properties.Select(p => $"{p.Name}={p.GetValue(data)}");
|
||||
return string.Join("&", value);
|
||||
}
|
||||
|
||||
public async Task<APIResponseStatus<TResponse>?> GetJsonAsync<TResponse,TRequest>(string url, TRequest value)
|
||||
{
|
||||
string parameter = ChangeToString(value);
|
||||
var response = await http.GetFromJsonAsync<APIResponseStatus<TResponse>>($"{url}?{parameter}");
|
||||
return response;
|
||||
}
|
||||
|
||||
public async Task<(bool success, string? json, T? data)> GetConnectServerAsnyc<T>(string url) {
|
||||
var headerValue = await storageService.GetItemAsync("Web-AM-Connect-Key");
|
||||
if (string.IsNullOrEmpty(headerValue)) return (false, null,default);
|
||||
|
||||
var args = new {
|
||||
url = $"{url}",
|
||||
method = "GET",
|
||||
headerKey = "Web-AM-Connect-Key",
|
||||
headerValue = headerValue,
|
||||
token = "VO00"
|
||||
};
|
||||
|
||||
var response = await js.InvokeAsync<JsonElement>(
|
||||
"fetchWithHeaderAndReturnUrl",
|
||||
args
|
||||
);
|
||||
|
||||
LoggerService.Write($"JSON 응답: {response.ToString()}");
|
||||
|
||||
if (response.TryGetProperty("data", out var dataElement))
|
||||
{
|
||||
try
|
||||
{
|
||||
// 전체 데이터 암호화 저장
|
||||
var dataJson = dataElement.ToString();
|
||||
var serialData = JsonSerializer.Deserialize<T>(dataJson,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
LoggerService.Write("[GetConnectServerAsnyc] 사용자 데이터 Json: " + dataJson + dataJson.GetType());
|
||||
LoggerService.Write("[GetConnectServerAsnyc] 사용자 데이터 변환: " + serialData.ToString());;
|
||||
|
||||
if (serialData != null)
|
||||
{
|
||||
var encryptedData = await secureService.EncryptAsync(dataJson);
|
||||
// await storageService.SetItemAsync("USER_DATA", encryptedData);
|
||||
return (true, encryptedData, serialData);
|
||||
}
|
||||
else
|
||||
{
|
||||
LoggerService.Write("사용자 데이터에 필수 정보가 없습니다");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerService.Write($"사용자 데이터 처리 중 오류: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
LoggerService.Write("데이터를 찾을 수 없습니다");
|
||||
return (false, null, default);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
dynamic 타입을 사용하면:
|
||||
타입 안전성이 떨어집니다
|
||||
컴파일 타임에 오류를 잡을 수 없습니다
|
||||
런타임에 예상치 못한 오류가 발생할 수 있습니다
|
||||
현재 Register.razor.cs에서 JsonElement를 사용하는 방식이 더 안전할 수 있습니다. 왜냐하면:
|
||||
JSON 응답의 구조를 명시적으로 확인할 수 있습니다 (TryGetProperty 사용)
|
||||
각 속성의 타입을 명확하게 처리할 수 있습니다
|
||||
예상치 못한 데이터 구조에 대해 더 안전하게 대응할 수 있습니다
|
||||
*/
|
70
Program/Services/LoadingService.cs
Normal file
|
@ -0,0 +1,70 @@
|
|||
// using Front.Program.Views.Project.Common;
|
||||
|
||||
using Front.Program.Models;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Front.Program.Services;
|
||||
// 뷰를 참조 안하고도 로딩 상태를 알 수 있게 바꾸기
|
||||
|
||||
public class LoadingService
|
||||
{
|
||||
private readonly IJSRuntime _jsRuntime;
|
||||
public bool IsLoading { get; private set; }
|
||||
private bool isNavigationLoading { get; set; }
|
||||
|
||||
public event Action? OnChange;
|
||||
|
||||
public void ShowLoading(bool isNavigation = false)
|
||||
{
|
||||
IsLoading = true;
|
||||
isNavigationLoading = isNavigation;
|
||||
NotifyStateChanged();
|
||||
}
|
||||
|
||||
public void HideLoading()
|
||||
{
|
||||
if (!isNavigationLoading)
|
||||
{
|
||||
IsLoading = false;
|
||||
NotifyStateChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void HideNavigationLoading()
|
||||
{
|
||||
if (isNavigationLoading)
|
||||
{
|
||||
IsLoading = false;
|
||||
isNavigationLoading = false;
|
||||
NotifyStateChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void NotifyStateChanged() => OnChange?.Invoke();
|
||||
|
||||
|
||||
public LoadingService(IJSRuntime jsRuntime)
|
||||
{
|
||||
_jsRuntime = jsRuntime;
|
||||
}
|
||||
|
||||
public async Task ShowLoadingAsync(bool isNavigation = false)
|
||||
{
|
||||
IsLoading = true;
|
||||
isNavigationLoading = isNavigation;
|
||||
NotifyStateChanged();
|
||||
await _jsRuntime.InvokeVoidAsync("setBodyOverflowHidden", true);
|
||||
}
|
||||
|
||||
public async Task HideLoadingAsync()
|
||||
{
|
||||
if (!isNavigationLoading)
|
||||
{
|
||||
IsLoading = false;
|
||||
NotifyStateChanged();
|
||||
await _jsRuntime.InvokeVoidAsync("setBodyOverflowHidden", false);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
35
Program/Services/LoggerService.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
using System.Diagnostics;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Front.Program.Services;
|
||||
|
||||
/// 개발기에서는 Debug 로그부터 모든 로그를 사용할 수 있지만, 운영에서는 Error 로그만 사용합니다.
|
||||
// enum SystemLogLevel
|
||||
// {
|
||||
// Debug, // 0 : 상세 로그 - 파란색(#0000FF)
|
||||
// Info, // 1 : 일반 로그 - 회색(#808080)
|
||||
// Success, // 2 : 성공 로그 - 초록색(#008000)
|
||||
// Warning, // 3 : 경고 로그 - 노란색(#FFFF00)
|
||||
// Error // 4 : 오류 로그 - 빨간색(#FF0000)
|
||||
// }
|
||||
|
||||
public class LoggerService
|
||||
{
|
||||
private static bool _isDev;
|
||||
|
||||
public static void Initialize(bool isDev)
|
||||
{
|
||||
_isDev = isDev;
|
||||
}
|
||||
|
||||
private static void LogToConsole(string message)//, string fontWeight = "normal")
|
||||
{
|
||||
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] - {message}");
|
||||
}
|
||||
|
||||
public static void Write(string message)
|
||||
{
|
||||
if (_isDev)
|
||||
LogToConsole(message);
|
||||
}
|
||||
}
|
42
Program/Services/QueryParamService.cs
Normal 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))
|
||||
{
|
||||
LoggerService.Write($"auth 파라미터 값: {auth}");
|
||||
if (auth == "true")
|
||||
{
|
||||
await storageService.SetItemAsync("IsLogin", "true");
|
||||
LoggerService.Write("로그인 상태를 true로 설정했습니다.");
|
||||
}
|
||||
else
|
||||
{
|
||||
await storageService.RemoveItemAsync("IsLogin");
|
||||
LoggerService.Write("로그인 상태를 제거했습니다.");
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
120
Program/Services/SecureService.cs
Normal 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();
|
||||
}
|
83
Program/Services/StorageService.cs
Normal 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. 문자열만 저장이 가능하다.
|
||||
*/
|
170
Program/Services/UserStateService.cs
Normal file
|
@ -0,0 +1,170 @@
|
|||
using System.Text.Json;
|
||||
using Front.Program.Services;
|
||||
using Front.Program.Models;
|
||||
using Microsoft.JSInterop;
|
||||
using System.ComponentModel; // INotifyPropertyChanged를 위해 추가
|
||||
using System.Runtime.CompilerServices; // CallerMemberName을 위해 추가
|
||||
|
||||
namespace Front.Program.ViewModels;
|
||||
|
||||
public class UserStateService(StorageService _storageService, SecureService _secureService, APIService _APIService,
|
||||
IJSRuntime _js) : INotifyPropertyChanged
|
||||
{
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
private Models.SimpleAcademy? _currentAcademy;
|
||||
public Models.SimpleAcademy? CurrentAcademy
|
||||
{
|
||||
get => _currentAcademy;
|
||||
set
|
||||
{
|
||||
if (_currentAcademy != value)
|
||||
{
|
||||
_currentAcademy = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public UserData UserData { get; set; } = new UserData();
|
||||
|
||||
public bool isFirstCheck { get; set; } = false;
|
||||
public bool isLogin { get; set; } = false;
|
||||
|
||||
public Models.SimpleAcademy[] academyItems = Array.Empty<Models.SimpleAcademy>();
|
||||
|
||||
|
||||
public async Task<(bool success,UserData? userData)> GetUserDataFromStorageAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var encUserData = await _storageService.GetItemAsync("UsDt");
|
||||
|
||||
if (!string.IsNullOrEmpty(encUserData))
|
||||
{
|
||||
|
||||
var decUserData = await _secureService.DecryptAsync(encUserData);
|
||||
|
||||
var userData = JsonSerializer.Deserialize<UserData>(decUserData,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
|
||||
LoggerService.Write($"UserData: {userData.Name}, {userData.Type}");
|
||||
|
||||
|
||||
return (true, userData);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (false, null);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerService.Write($"Error [GetUserDataFormAsync] : {ex.Message}");
|
||||
return (false, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> GetUserDataAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
LoggerService.Write("GetUserDataAsync 호출됨");
|
||||
var isLoginFromStorage = await _storageService.GetItemAsync("IsLogin");
|
||||
LoggerService.Write($"Storage에서 읽은 IsLogin 값: {isLoginFromStorage ?? "null"}");
|
||||
|
||||
// 로그인 상태가 아니라면 애초에 할 필요 없음
|
||||
if (isLoginFromStorage != "true")
|
||||
{
|
||||
isLogin = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
isLogin = true;
|
||||
|
||||
var userDataForm = await GetUserDataFromStorageAsync();
|
||||
|
||||
if (userDataForm.success && userDataForm.userData != null)
|
||||
{
|
||||
// 사용자 데이터가 성공적으로 로드되었을 때의 로직
|
||||
UserData = userDataForm.userData;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
var userDataFromServer = await GetUserDataFromServerAsync();
|
||||
if (userDataFromServer.success && userDataFromServer.userData != null)
|
||||
{
|
||||
// 서버에서 사용자 데이터를 성공적으로 로드했을 때의 로직
|
||||
UserData = userDataFromServer.userData;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 사용자 데이터를 로드하지 못했을 때의 로직
|
||||
LoggerService.Write("사용자 데이터를 로드하지 못했습니다.");
|
||||
isLogin = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!isFirstCheck) isFirstCheck = true;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ClearUserStateAsnyc()
|
||||
{
|
||||
try
|
||||
{
|
||||
string[] keys = { "UsDt", "IsLogin", "UsAcDt" };
|
||||
foreach (var key in keys)
|
||||
{
|
||||
var remove = await _storageService.RemoveItemAsync(key);
|
||||
LoggerService.Write("사용자 데이터 삭제" + (remove ? "성공 :" : "실패 :") + key);
|
||||
}
|
||||
ClearStateData();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerService.Write($"사용자 데이터 삭제 중 오류: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearStateData()
|
||||
{
|
||||
isLogin = false;
|
||||
UserData = new UserData();
|
||||
academyItems = Array.Empty<Models.SimpleAcademy>();
|
||||
}
|
||||
|
||||
public async Task<(bool success, UserData? userData)> GetUserDataFromServerAsync()
|
||||
{
|
||||
var data = await _APIService.GetConnectServerAsnyc<UserData>("/api/v1/in/user");
|
||||
if (data is { success: true, json: not null })
|
||||
await _storageService.SetItemAsync("UsDt", data.json);
|
||||
return (data.success, data.data);
|
||||
}
|
||||
|
||||
public async Task<(bool success, List<SimpleAcademy>? simpleAcademy)> GetAcademy()
|
||||
{
|
||||
var data = await _APIService.GetConnectServerAsnyc<List<SimpleAcademy>>("/api/v1/in/user/academy");
|
||||
if (data is { success: true, json: not null })
|
||||
{
|
||||
await _storageService.SetItemAsync("UsAcDt", data.json);
|
||||
if (data.data != null && data.data.Any())
|
||||
{
|
||||
academyItems = data.data.ToArray();
|
||||
CurrentAcademy = data.data.First(); // 첫 번째 학원을 현재 학원으로 설정
|
||||
}
|
||||
}
|
||||
return (data.success, data.data);
|
||||
}
|
||||
}
|
66
Program/Views/Academy/AcademyIntro.razor
Normal file
|
@ -0,0 +1,66 @@
|
|||
@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="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>
|
||||
|
||||
@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 UserStateService.academyItems)
|
||||
{
|
||||
<a href="/am/main?bid=@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.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>
|
120
Program/Views/Academy/AcademyIntro.razor.cs
Normal file
|
@ -0,0 +1,120 @@
|
|||
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!;
|
||||
[Inject] LoadingService LoadingService { get; set; } = default!;
|
||||
|
||||
private bool _isProcessing = false;
|
||||
|
||||
protected override async void OnInitialized()
|
||||
{
|
||||
try
|
||||
{
|
||||
Navigation.LocationChanged += HandleLocationChanged;
|
||||
HandleLocationChanged(this, new LocationChangedEventArgs(Navigation.Uri, false));
|
||||
if (!UserStateService.isFirstCheck)
|
||||
{
|
||||
LoadingService.ShowLoading();
|
||||
await UserStateService.GetUserDataAsync();
|
||||
var aca = await UserStateService.GetAcademy();
|
||||
|
||||
if (aca.success)
|
||||
{
|
||||
if (aca.simpleAcademy.Count > 0)
|
||||
{
|
||||
UserStateService.academyItems = aca.simpleAcademy.ToArray();
|
||||
LoggerService.Write($"academyItems: {UserStateService.academyItems.Length}개");
|
||||
}
|
||||
LoggerService.Write("아카데미 정보가 없습니다. 로그인 상태를 확인해주세요.");
|
||||
}
|
||||
else
|
||||
{
|
||||
LoggerService.Write("서버 세션에서 불러오기 실패");
|
||||
await UserStateService.ClearUserStateAsnyc();
|
||||
|
||||
}
|
||||
LoggerService.Write($"academy: {string.Join(", ", UserStateService.academyItems.Select(a => a.name))}");
|
||||
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
LoadingService.HideLoading();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
// 유저 값 가져오면서 같이 academy 정보도 가져와야지
|
||||
}
|
||||
|
||||
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);
|
||||
LoggerService.Write($"리다이렉트된 URI: {uri}");
|
||||
|
||||
// 쿼리 파라미터가 있는 경우에만 처리
|
||||
if (!string.IsNullOrEmpty(uri.Query))
|
||||
{
|
||||
LoadingService.ShowLoading();
|
||||
var queryParam = QueryParamService.ParseQueryParam(uri);
|
||||
await QueryParamService.AuthCheck(queryParam, StorageService);
|
||||
|
||||
// 유저 정보 확인하는거 (로그인 했으니 값 가져와야지)
|
||||
await UserStateService.GetUserDataAsync();
|
||||
var aca = await UserStateService.GetAcademy();
|
||||
|
||||
if (aca.success)
|
||||
{
|
||||
if (aca.simpleAcademy.Count > 0)
|
||||
{
|
||||
UserStateService.academyItems = aca.simpleAcademy.ToArray();
|
||||
|
||||
LoggerService.Write($"academyItems: {UserStateService.academyItems.Length}개");
|
||||
}
|
||||
LoggerService.Write("아카데미 정보가 없습니다. 로그인 상태를 확인해주세요.");
|
||||
}
|
||||
LoggerService.Write($"academy: {string.Join(", ", UserStateService.academyItems.Select(a => a.name))}");
|
||||
|
||||
// // 쿼리 파라미터를 제거한 기본 URI로 리다이렉트
|
||||
var baseUri = uri.GetLeftPart(UriPartial.Path);
|
||||
LoggerService.Write($"리다이렉트할 URI: {baseUri}");
|
||||
// await InvokeAsync(StateHasChanged); // StateHasChanged를 호출하여 UI 업데이트
|
||||
Navigation.NavigateTo(baseUri, forceLoad: false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerService.Write($"Error in HandleLocationChanged: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
LoadingService.HideLoading();
|
||||
_isProcessing = false;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
protected void OnClickLogin()
|
||||
{
|
||||
Navigation.NavigateTo("/am/auth");
|
||||
}
|
||||
}
|
2
Program/Views/Academy/AcademyMain.razor
Normal file
|
@ -0,0 +1,2 @@
|
|||
@page "/am/main"
|
||||
<h3>AcademyMain</h3>
|
47
Program/Views/Academy/AcademyMain.razor.cs
Normal file
|
@ -0,0 +1,47 @@
|
|||
using Front.Program.Services;
|
||||
using Front.Program.ViewModels;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Front.Program.Views.Academy;
|
||||
|
||||
public partial class AcademyMain : ComponentBase
|
||||
{
|
||||
[Inject] NavigationManager Navigation { get; set; } = default!;
|
||||
[Inject] UserStateService UserStateService { get; set; } = default!;
|
||||
[Inject] QueryParamService QueryParamService { get; set; } = default!;
|
||||
[Inject] StorageService StorageService { get; set; } = default!;
|
||||
[Inject] LoadingService LoadingService { get; set; } = default!;
|
||||
|
||||
// [Inject] LoggerService Logger {get; set;} = default!;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// 초기화 작업
|
||||
// await UserStateService.GetUserDataAsync();
|
||||
// if (UserStateService.isFirstCheck)
|
||||
// {
|
||||
// // 첫 번째 체크 후에만 Academy 정보를 가져옴
|
||||
// var academyResult = await UserStateService.GetAcademy();
|
||||
//
|
||||
// if (academyResult.success && academyResult.simpleAcademy.Count > 0)
|
||||
// {
|
||||
// UserStateService.academyItems = academyResult.simpleAcademy.ToArray();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// URL 파라미터 처리
|
||||
LoggerService.Write("로거 테스트");
|
||||
|
||||
var uri = Navigation.ToAbsoluteUri(Navigation.Uri);
|
||||
|
||||
LoggerService.Write("쿼리 있나?");
|
||||
// 쿼리 파라미터가 있는 경우에만 처리
|
||||
if (!string.IsNullOrEmpty(uri.Query))
|
||||
{
|
||||
var queryParam = QueryParamService.ParseQueryParam(uri);
|
||||
LoggerService.Write($"Parsed Query Parameters: {string.Join(", ", queryParam.Select(kv => $"{kv.Key}={kv.Value}"))}");
|
||||
}
|
||||
LoggerService.Write("쿼리 검사");
|
||||
|
||||
}
|
||||
}
|
70
Program/Views/Academy/Common/LeftSideAcademy.razor
Normal 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">@UserName</div>
|
||||
<!-- 회원 유형이 올 곳 -->
|
||||
<p class="text-gray-600 text-sm">@UserType</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>
|
13
Program/Views/Academy/Common/LeftSideAcademy.razor.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
using Front.Program.ViewModels;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Front.Program.Views.Academy.Common;
|
||||
|
||||
public partial class LeftSideAcademy : ComponentBase
|
||||
{
|
||||
[Inject]
|
||||
private UserStateService UserStateService { get; set; }
|
||||
|
||||
private string UserName => UserStateService.UserData?.Name ?? "AcaMate";
|
||||
private string UserType => UserStateService.UserData?.Type ?? "Parent";
|
||||
}
|
58
Program/Views/Academy/Common/TopNavAcademy.razor
Normal file
|
@ -0,0 +1,58 @@
|
|||
<div class="flex items-center justify-between whitespace-nowrap border-b border-solid border-b-[#f0f2f5] h-[72px] w-full bg-white px-4">
|
||||
<div id="AcademyDrop" class="relative flex items-center gap-4 text-[#111418] flex-1">
|
||||
|
||||
|
||||
<button class="md:hidden mr-4">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-16 6h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
@* 드롭다운을 내리는 버튼 *@
|
||||
<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]">@currentAcademyName</h2> *@
|
||||
<h2 class="md:block text-text-title text-lg font-bold leading-tight tracking-[-0.015em]">@currentAcademyName</h2>
|
||||
<img src="Resources/Images/Icon/Down.png" alt="아래"
|
||||
class="w-6 h-6 object-cover transition-transform duration-200 @(isAcademyDropdownOpen ? "rotate-180" : "")" />
|
||||
</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="max-h-60 overflow-y-auto">
|
||||
@foreach (var academy in academyItems)
|
||||
{
|
||||
<div @onclick="() => SelectAcademy(academy)" class="cursor-pointer block px-4 py-2 text-text-title hover:bg-gray-100">
|
||||
@academy.name
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
@if (isAcademyDropdownOpen)
|
||||
{
|
||||
<div @onclick="OnClickOutside" class="fixed inset-0 z-40" style="background: transparent;"></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-6">
|
||||
<img src="Resources/Images/Icon/Notification_SET.png" alt="알림" class="w-6 h-6 object-cover" />
|
||||
<img src="Resources/Images/Icon/Setting.png" alt="설정" class="w-6 h-6 object-cover"/>
|
||||
<img src="Resources/Images/Icon/Link.png" alt="바로가기" class="w-6 h-6 object-cover"/>
|
||||
|
||||
<img src="Resources/Images/Icon/Logout.png" alt="로그아웃" class="w-6 h-6 object-cover"/>
|
||||
@* <div class="w-10 h-10 rounded-full bg-gray-200 overflow-hidden"> *@
|
||||
@* <img src="Resources/Images/Icon/Logout.png" alt="로그아웃" class="w-full h-full object-cover"/> *@
|
||||
@* <a class="text-text-title font-medium leading-normal hover:text-blue-600" href="/about">About</a> *@
|
||||
@* </div> *@
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
61
Program/Views/Academy/Common/TopNavAcademy.razor.cs
Normal file
|
@ -0,0 +1,61 @@
|
|||
|
||||
using System.ComponentModel;
|
||||
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.Common;
|
||||
|
||||
public partial class TopNavAcademy : ComponentBase, IDisposable
|
||||
{
|
||||
[Inject] UserStateService UserStateService { get; set; } = default!;
|
||||
[Inject] NavigationManager NavigationManager { get; set; } = default!;
|
||||
|
||||
|
||||
protected bool isOpen = false;
|
||||
protected bool isAcademyDropdownOpen = false;
|
||||
|
||||
// 계산된 속성으로 변경
|
||||
protected Models.SimpleAcademy[] academyItems => UserStateService.academyItems;
|
||||
protected string currentAcademyName => UserStateService.CurrentAcademy?.name ?? "학원을 선택하세요";
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
UserStateService.PropertyChanged += OnUserStateServicePropertyChanged;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
UserStateService.PropertyChanged -= OnUserStateServicePropertyChanged;
|
||||
}
|
||||
|
||||
private void OnUserStateServicePropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(UserStateService.CurrentAcademy))
|
||||
{
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
protected void ToggleAcademyDropdown() {
|
||||
isAcademyDropdownOpen = !isAcademyDropdownOpen;
|
||||
}
|
||||
|
||||
private void SelectAcademy(SimpleAcademy academy)
|
||||
{
|
||||
UserStateService.CurrentAcademy = academy; // 현재 선택된 학원 업데이트
|
||||
isAcademyDropdownOpen = false;
|
||||
NavigationManager.NavigateTo($"/am/main?bid={academy.bid}");
|
||||
|
||||
}
|
||||
|
||||
private void OnClickOutside()
|
||||
{
|
||||
if (isAcademyDropdownOpen) isAcademyDropdownOpen = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
|
@ -1,16 +1,197 @@
|
|||
@page "/about"
|
||||
|
||||
<div class="bg-blue-500 text-white p-4">
|
||||
<h2 class="text-2xl font-bold">About Us</h2>
|
||||
<p class="mt-2">We are a team of passionate developers.</p>
|
||||
<p>이건 그냥 일반 텍스트야</p>
|
||||
<div
|
||||
class="relative flex size-full w-full min-h-screen flex-col items-center bg-white group/design-root overflow-x-hidden"
|
||||
style='font-family: "Plus Jakarta Sans", "Noto Sans", sans-serif;'>
|
||||
<div class="layout-container flex h-full grow flex-col">
|
||||
<div class="flex flex-1 justify-center">
|
||||
<div class="layout-content-container flex flex-col w-full max-w-[960px] flex-1 mx-auto">
|
||||
|
||||
@* 클릭시 onClick 이벤트 발생 *@
|
||||
<!-- 상단 배너 영역 -->
|
||||
<div class="container">
|
||||
<div
|
||||
class="w-full flex min-h-[480px] flex-col gap-6 bg-cover bg-center bg-no-repeat xs:gap-8 xs:rounded-lg items-start justify-end px-4 pb-10 xs:px-10"
|
||||
style='background-image: linear-gradient(rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.4) 100%), url("https://lh3.googleusercontent.com/aida-public/AB6AXuDffY0eohqZnXKdWwIoiHloiYxZ1igyaJWtn7zdpfeze7pDhQuxV3MqvGueO1m-a38SDlXVEZUkd8es8cB8YvKHf8J-FIzTPFEHU4UnTNRBnX0XGv0EoZGrutA0kHnI_IA0k0SLtlU7NUK_7EcLabUHzALRgVZhcP98Bd-2GZl85-6ODqSHpe11pHXpyMz3RDDEzhDuHAwBBfJJaJG7zFC22X8Cc0K_V97Vf7jXfs-WYJ5CCrKAE3JpT2RUbdEJfiJHOkDh9yGWU1TB");'>
|
||||
<div class="flex flex-col w-full gap-2 text-right items-end">
|
||||
<h1
|
||||
class="text-white text-4xl font-black leading-tight tracking-[-0.033em] xs:text-5xl xs:font-black xs:leading-tight xs:tracking-[-0.033em] outline-none">
|
||||
모든 순간<br/>
|
||||
더 쉽고<br/>
|
||||
더 똑똑하게
|
||||
</h1>
|
||||
<h2 class="hidden md:block text-white text-sm font-normal leading-normal">
|
||||
AcaMate는 학원 관리를 더 쉽고, 학부모와의 소통을 더 가깝게, 학생의 성장을 더 든든하게 지원하는 통합 플랫폼입니다.
|
||||
</h2>
|
||||
<h2 class="block md:hidden text-white text-base font-normal leading-normal">
|
||||
AcaMate는 학원 관리를 더 쉽고, 학부모와의 소통을 더 가깝게,<br/>학생의 성장을 더 든든하게 지원하는 통합 플랫폼입니다.
|
||||
</h2>
|
||||
|
||||
@* <button *@
|
||||
@* class="flex min-w-[84px] max-w-[480px] cursor-pointer items-center justify-center overflow-hidden rounded-lg h-10 px-4 xs:h-12 xs:px-5 bg-[#0c7ff2] text-white text-sm font-bold leading-normal tracking-[0.015em] xs:text-base xs:font-bold xs:leading-normal xs:tracking-[0.015em]"> *@
|
||||
@* <span class="truncate">Get Started</span> *@
|
||||
@* </button> *@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 상단 배너 영역 끝 -->
|
||||
|
||||
<button class="bg-blue-700 text-white px-4 py-2 rounded hover:bg-blue-800"
|
||||
@onclick="OnClickEvent">
|
||||
Click Me
|
||||
</button>
|
||||
<!-- 플랫폼 설명 영역 -->
|
||||
<div class="flex flex-col gap-10 px-4 py-10 container">
|
||||
<div class="flex flex-col gap-4">
|
||||
<h1
|
||||
class="hidden md:block text-text-title tracking-light text-[32px] font-bold leading-tight xs:text-4xl xs:font-black xs:leading-tight xs:tracking-[-0.033em] max-w-[720px]">
|
||||
AcaMate, 학원 운영의 새로운 기준
|
||||
</h1>
|
||||
<h1
|
||||
class="block md:hidden text-center text-text-title tracking-light text-[32px] font-bold leading-tight xs:text-4xl xs:font-black xs:leading-tight xs:tracking-[-0.033em] max-w-[720px]">
|
||||
학원 운영의 새로운 기준
|
||||
</h1>
|
||||
|
||||
<p class="hidden md:block text-text-title text-base font-normal leading-normal max-w-[720px]">
|
||||
AcaMate는 학원 운영에 꼭 필요한 모든 기능을 하나의 플랫폼에 담아,
|
||||
사용자 관리부터 수업 일정, 출결 및 성과 추적까지 완벽하게 통합되어 있습니다.<br/>
|
||||
<br/>
|
||||
지
|
||||
금, 더 빠르고 스마트한 학원 운영을 경험해보세요.
|
||||
</p>
|
||||
<p class="block md:hidden text-text-title text-center text-base font-normal leading-normal max-w-[720px]">
|
||||
필요한 모든 기능을 하나로<br/>
|
||||
수업, 출결, 소통, 성과까지 모두 AcaMate에서<br/>
|
||||
지금, 스마트한 운영을 시작하세요.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-[repeat(auto-fit,minmax(158px,1fr))] gap-3 p-0">
|
||||
<div class="flex flex-1 gap-3 rounded-lg border border-[#dbe0e6] bg-white p-4 flex-col">
|
||||
<img src="Resources/Images/Icon/Person.png" alt="Person" width="24px" height="24px">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h2 class="text-[#111418] text-base font-bold leading-tight">사용자 통합 관리</h2>
|
||||
<p class="text-[#60758a] text-sm font-normal leading-normal">
|
||||
학생, 교직원, 학부모를 한 곳에서 쉽고 빠르게 관리할 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-1 gap-3 rounded-lg border border-[#dbe0e6] bg-white p-4 flex-col">
|
||||
<img src="Resources/Images/Icon/Calendar.png" alt="Calendar" width="24px" height="24px">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h2 class="text-[#111418] text-base font-bold leading-tight">수업 & 일정 관리</h2>
|
||||
<p class="text-[#60758a] text-sm font-normal leading-normal">
|
||||
드래그 앤 드롭 기반의 캘린더로 수업과 일정을 손쉽게 설정하세요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-1 gap-3 rounded-lg border border-[#dbe0e6] bg-white p-4 flex-col">
|
||||
<img src="Resources/Images/Icon/Management.png" alt="Management" width="24px" height="24px">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h2 class="text-[#111418] text-base font-bold leading-tight">출결 및 학습 추적</h2>
|
||||
<p class="text-[#60758a] text-sm font-normal leading-normal">
|
||||
출결, 과제, 성과를 한눈에 확인하고 학생의 성장을 체계적으로 관리하세요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-1 gap-3 rounded-lg border border-[#dbe0e6] bg-white p-4 flex-col">
|
||||
<img src="Resources/Images/Icon/Talk.png" alt="Talk" width="24px" height="24px">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h2 class="text-[#111418] text-base font-bold leading-tight">통합 커뮤니케이션</h2>
|
||||
<p class="text-[#60758a] text-sm font-normal leading-normal">
|
||||
공지, 상담, 실시간 알림은 물론
|
||||
1:1 및 그룹 채팅까지 모든 소통이 가능합니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- 플랫폼 설명 영역 끝 -->
|
||||
|
||||
|
||||
<div class="flex justify-center my-2">
|
||||
<span class="text-text-border text-6xl tracking-widest select-none">···</span>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 플랫폼 특징 영역 -->
|
||||
<div class="flex flex-col gap-10 px-4 py-10 container">
|
||||
<div class="flex flex-col gap-4">
|
||||
|
||||
<div class="flex flex-col md:flex-row items-center gap-6">
|
||||
<div class="w-full md:w-1/2 h-64 bg-gray-200 rounded-xl flex items-center justify-center">
|
||||
<span class="text-gray-500">이미지 자리</span>
|
||||
</div>
|
||||
@* <img src="/images/feature1.png" alt="기능 이미지" class="w-full md:w-1/2 rounded-xl shadow"/> *@
|
||||
<div class="text-left md:w-1/2 space-y-2">
|
||||
<h3 class="text-xl font-semibold">사용자 통합 관리</h3>
|
||||
<p class="text-gray-600">학생, 교사, 학부모까지 한 화면에서 직관적으로 관리할 수 있습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col md:flex-row-reverse items-center gap-6">
|
||||
<div class="w-full md:w-1/2 h-64 bg-gray-200 rounded-xl flex items-center justify-center">
|
||||
<span class="text-gray-500">이미지 자리</span>
|
||||
</div>
|
||||
@* <img src="/images/feature2.png" alt="기능 이미지" class="w-full md:w-1/2 rounded-xl shadow"/> *@
|
||||
<div class="text-left md:w-1/2 space-y-2">
|
||||
<h3 class="text-xl font-semibold">수업 & 일정 관리</h3>
|
||||
<p class="text-gray-600">드래그 앤 드롭 기반의 캘린더로 수업과 일정을 손쉽게 설정하세요.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col md:flex-row items-center gap-6">
|
||||
<div class="w-full md:w-1/2 h-64 bg-gray-200 rounded-xl flex items-center justify-center">
|
||||
<span class="text-gray-500">이미지 자리</span>
|
||||
</div>
|
||||
@* <img src="/images/feature1.png" alt="기능 이미지" class="w-full md:w-1/2 rounded-xl shadow"/> *@
|
||||
<div class="text-left md:w-1/2 space-y-2">
|
||||
<h3 class="text-xl font-semibold">출결 및 학습 추적</h3>
|
||||
<p class="text-gray-600">출결, 과제, 성과를 한눈에 확인하고 학생의 성장을 체계적으로 관리하세요.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col md:flex-row-reverse items-center gap-6">
|
||||
<div class="w-full md:w-1/2 h-64 bg-gray-200 rounded-xl flex items-center justify-center">
|
||||
<span class="text-gray-500">이미지 자리</span>
|
||||
</div>
|
||||
@* <img src="/images/feature2.png" alt="기능 이미지" class="w-full md:w-1/2 rounded-xl shadow"/> *@
|
||||
<div class="text-left md:w-1/2 space-y-2">
|
||||
<h3 class="text-xl font-semibold">통합 커뮤니케이션</h3>
|
||||
<p class="text-gray-600">공지, 상담, 실시간 알림은 물론 1:1 및 그룹 채팅까지 모든 소통이 가능합니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- 플랫폼 특징 영역 끝 -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<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>
|
||||
</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>
|
||||
|
|
|
@ -1,15 +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!;
|
||||
NavigationManager Navigation { get; set; } = default!;
|
||||
|
||||
private void OnClickEvent()
|
||||
[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()
|
||||
{
|
||||
// NavigationManager.NavigateTo("/redirectpage");
|
||||
Console.WriteLine("Redirecting to redirect page");
|
||||
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);
|
||||
LoggerService.Write($"리다이렉트된 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);
|
||||
LoggerService.Write($"리다이렉트할 URI: {baseUri}");
|
||||
await InvokeAsync(StateHasChanged); // StateHasChanged를 호출하여 UI 업데이트
|
||||
Navigation.NavigateTo(baseUri, forceLoad: false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerService.Write($"Error in HandleLocationChanged: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isProcessing = false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +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">
|
||||
<input type="text" placeholder="아이디" class="mb-4 p-2 border border-gray-300 rounded w-full" />
|
||||
<input type="password" placeholder="비밀번호" class="mb-4 p-2 border border-gray-300 rounded w-full" />
|
||||
<button class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 w-full">로그인</button>
|
||||
</div>
|
||||
</div>
|
|
@ -1,8 +0,0 @@
|
|||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Front.Program.Views.Project;
|
||||
|
||||
public partial class Auth : ComponentBase
|
||||
{
|
||||
|
||||
}
|
5
Program/Views/Project/Common/PageIndicator.razor
Normal 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>
|
7
Program/Views/Project/Common/PageIndicator.razor.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
using Front.Program.Models;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Front.Program.Views.Project.Common;
|
||||
|
||||
public partial class PageIndicator : ComponentBase
|
||||
{ }
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -1,8 +1,5 @@
|
|||
@* <div class="flex items-center p-4 bg-second-darker shadow"> *@
|
||||
@* $1$ 이동하는 거 말고 그냥 일반 텍스트 넣을거야 #1# *@
|
||||
@* <img src="/logo.png" alt="Icon" class="w-8 h-8 mr-0 absolute left-4 top-1/2 transform -translate-y-1/2"> *@
|
||||
@* <h3 class="flex-1 text-center text-normal-normal pl-12">일반 텍스트 작성</h3> *@
|
||||
@* </div> *@
|
||||
|
||||
|
||||
|
||||
<div class="relative bg-second-darker shadow p-4 flex items-center justify-center">
|
||||
<!-- 왼쪽 아이콘 -->
|
|
@ -1,6 +1,6 @@
|
|||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Front.Program.Views.Project;
|
||||
namespace Front.Program.Views.Project.Common;
|
||||
|
||||
public partial class TopBanner : ComponentBase
|
||||
{
|
67
Program/Views/Project/Common/TopProjectNav.razor
Normal file
|
@ -0,0 +1,67 @@
|
|||
<div class="flex items-center justify-between whitespace-nowrap border-b border-solid border-b-[#f0f2f5] py-4 bg-white">
|
||||
<div class="flex items-center gap-4 text-[#111418] ml-4" @onclick="OnClickRedirect">
|
||||
<img src="/logo.png" alt="Icon" class="w-8 h-8">
|
||||
<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="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" @onclick="() => isOpen = !isOpen">About</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>
|
||||
</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"
|
||||
@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"
|
||||
d="M4 6h16M4 12h16M4 18h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
@if (isOpen)
|
||||
{
|
||||
<div class="md:hidden absolute top-16 left-0 w-full bg-white shadow z-50 transition-all duration-300">
|
||||
<div class="flex flex-col items-start gap-4 p-4 text-center">
|
||||
<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 (!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>
|
||||
}
|
||||
</div>
|
57
Program/Views/Project/Common/TopProjectNav.razor.cs
Normal 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()
|
||||
{
|
||||
LoggerService.Write("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.ClearUserStateAsnyc();
|
||||
//
|
||||
// if (await UserViewModel.ClearUserData())
|
||||
// {
|
||||
// isLogin = false;
|
||||
// }
|
||||
}
|
||||
}
|
26
Program/Views/Project/ConnectUser/Auth.razor
Normal 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>
|
57
Program/Views/Project/ConnectUser/Auth.razor.cs
Normal 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)
|
||||
{
|
||||
// 페이지 이동이 발생했을 때 로딩 상태 해제
|
||||
LoggerService.Write($"페이지 이동 감지: {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)
|
||||
{
|
||||
LoggerService.Write($"카카오 로그인 오류: {ex.Message}");
|
||||
LoadingService.HideLoading();
|
||||
}
|
||||
}
|
||||
}
|
154
Program/Views/Project/ConnectUser/Register.razor
Normal file
|
@ -0,0 +1,154 @@
|
|||
@page "/auth/register"
|
||||
@inject IJSRuntime JSRuntime
|
||||
|
||||
<div class="relative flex size-full min-h-screen flex-col bg-white 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="flex flex-1 justify-center py-5 md:px-40">
|
||||
<div class="layout-content-container flex flex-col w-full max-w-[480px] py-5 flex-1">
|
||||
|
||||
<h2 class="text-text-title tracking-light text-[28px] font-bold leading-tight px-4 text-center pb-3 pt-5">
|
||||
회원가입
|
||||
</h2>
|
||||
<p class="text-red-600 font-normal text-xs text-right px-4">* 는 필수 사항입니다.</p>
|
||||
|
||||
<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">
|
||||
이름
|
||||
<span class="text-red-600">*</span>
|
||||
</p>
|
||||
<input
|
||||
pattern="\d{16}"
|
||||
maxlength="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"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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">E-Mail
|
||||
<span class="text-red-600">*</span>
|
||||
</p>
|
||||
<input
|
||||
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>
|
||||
</div>
|
||||
<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>
|
||||
<div class="flex w-full max-w-[480px] items-center gap-2">
|
||||
<input
|
||||
maxlength="3"
|
||||
pattern="\d{3}"
|
||||
placeholder="010"
|
||||
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="phonePart1"
|
||||
inputmode="numeric"
|
||||
autocomplete="off"
|
||||
oninput="this.value = this.value.replace(/[^0-9]/g, '')"
|
||||
/>
|
||||
<span class="self-center">-</span>
|
||||
<input
|
||||
maxlength="4"
|
||||
pattern="\d{4}"
|
||||
placeholder="1234"
|
||||
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="phonePart2"
|
||||
inputmode="numeric"
|
||||
autocomplete="off"
|
||||
oninput="this.value = this.value.replace(/[^0-9]/g, '')"
|
||||
/>
|
||||
<span class="self-center">-</span>
|
||||
<input
|
||||
maxlength="4"
|
||||
pattern="\d{4}"
|
||||
placeholder="5678"
|
||||
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="phonePart3"
|
||||
inputmode="numeric"
|
||||
autocomplete="off"
|
||||
oninput="this.value = this.value.replace(/[^0-9]/g, '')"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col w-full gap-4 px-4 py-3">
|
||||
<label class="flex flex-col w-full text-text-title text-base font-medium leading-normal">
|
||||
주소
|
||||
</label>
|
||||
<input
|
||||
placeholder="눌러서 주소를 선택하세요."
|
||||
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-12 placeholder:text-[#6a7581] p-[12px] text-base font-normal leading-normal"
|
||||
@onclick="OnClickAddress"
|
||||
@bind-value="address"
|
||||
readonly
|
||||
/>
|
||||
@if (address != "")
|
||||
{
|
||||
<input
|
||||
placeholder="상세주소를 입력하세요."
|
||||
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-12 placeholder:text-[#6a7581] p-[12px] text-base font-normal leading-normal"
|
||||
@bind-value="detailAddress"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="flex px-4 py-3">
|
||||
<button
|
||||
@onclick="ConfirmData"
|
||||
class="flex min-w-[84px] max-w-[480px] cursor-pointer items-center justify-center overflow-hidden rounded-xl h-10 px-4 flex-1 bg-second-normal hover:bg-second-dark text-white text-sm font-bold leading-normal tracking-[0.015em]">
|
||||
<span class="truncate">회원가입</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
293
Program/Views/Project/ConnectUser/Register.razor.cs
Normal file
|
@ -0,0 +1,293 @@
|
|||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
using System.Text.Json;
|
||||
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.ConnectUser;
|
||||
|
||||
public partial class Register : ComponentBase
|
||||
{
|
||||
[Inject] IJSRuntime JS { get; set; }
|
||||
[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 = "";
|
||||
private string address = "";
|
||||
private string detailAddress = "";
|
||||
private string snsId = "";
|
||||
|
||||
private string phonePart1 = "";
|
||||
private string phonePart2 = "";
|
||||
private string phonePart3 = "";
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
objRef = DotNetObjectReference.Create(this);
|
||||
try
|
||||
{
|
||||
var token = await CookieService.GetItemAsync("Web-AM-Connect-Key");
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
await JS.InvokeVoidAsync("alert", "인증 정보가 없습니다.");
|
||||
NavigationManager.NavigateTo("/");
|
||||
return;
|
||||
}
|
||||
|
||||
// AppController를 통해 세션에서 snsId 가져오기
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, $"/api/v1/in/app/session/get?key=snsId");
|
||||
request.Headers.Add("Web-AM-Connect-Key", token);
|
||||
|
||||
var response = await Http.SendAsync(request);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var content = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
LoggerService.Write($"세션 응답: {content}");
|
||||
|
||||
if (content.TryGetProperty("status", out var statusElement) &&
|
||||
statusElement.TryGetProperty("code", out var codeElement) &&
|
||||
codeElement.GetString() == "000")
|
||||
{
|
||||
if (content.TryGetProperty("data", out var dataElement) &&
|
||||
dataElement.TryGetProperty("data", out var snsIdElement))
|
||||
{
|
||||
snsId = snsIdElement.GetString() ?? "";
|
||||
LoggerService.Write($"서버 세션에서 가져온 SNS ID: {snsId}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LoggerService.Write($"세션 데이터 가져오기 실패: {content}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LoggerService.Write($"세션 API 호출 실패: {response.StatusCode}");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(snsId))
|
||||
{
|
||||
LoggerService.Write("SNS ID가 없습니다.");
|
||||
await JS.InvokeVoidAsync("alert", "잘못된 접근입니다.");
|
||||
NavigationManager.NavigateTo("/");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerService.Write($"SNS ID 가져오기 실패: {ex.Message}");
|
||||
await JS.InvokeVoidAsync("alert", "세션 정보를 가져오는데 실패했습니다.");
|
||||
NavigationManager.NavigateTo("/");
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
await JS.InvokeVoidAsync("alert", "이름을 입력해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(email))
|
||||
{
|
||||
await JS.InvokeVoidAsync("alert", "이메일을 입력해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 생일 업데이트
|
||||
UpdateBirthDate();
|
||||
|
||||
// 전화번호 조합
|
||||
if (phonePart1.Length == 3 && phonePart2.Length == 4 && phonePart3.Length == 4)
|
||||
{
|
||||
phone = $"{phonePart1}-{phonePart2}-{phonePart3}";
|
||||
}
|
||||
else
|
||||
{
|
||||
await JS.InvokeVoidAsync("alert", "전화번호를 올바르게 입력해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
var registerData = new
|
||||
{
|
||||
name = name,
|
||||
birth = birth,
|
||||
email = email,
|
||||
phone = phone,
|
||||
address = address,
|
||||
sns_id = snsId,
|
||||
sns_type = "ST01",
|
||||
type = "UT05",
|
||||
device_id = "",
|
||||
auto_login_yn = false,
|
||||
login_date = DateTime.Now,
|
||||
push_token = "",
|
||||
location_yn = false,
|
||||
camera_yn = false,
|
||||
photo_yn = false,
|
||||
push_yn = false,
|
||||
market_app_yn = false,
|
||||
market_sms_yn = false,
|
||||
market_email_yn = false
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
LoadingService.ShowLoading();
|
||||
var token = await CookieService.GetItemAsync("Web-AM-Connect-Key");
|
||||
LoggerService.Write($"쿠키에서 가져온 토큰: '{token}'");
|
||||
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
await JS.InvokeVoidAsync("alert", "인증 정보가 없습니다.");
|
||||
NavigationManager.NavigateTo("/");
|
||||
|
||||
LoadingService.HideLoading();
|
||||
return;
|
||||
}
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "/api/v1/in/user/register");
|
||||
request.Headers.Add("Web-AM-Connect_Key", token);
|
||||
LoggerService.Write($"요청 헤더: {string.Join(", ", request.Headers.Select(h => $"{h.Key}: {string.Join(", ", h.Value)}"))}");
|
||||
|
||||
request.Content = JsonContent.Create(registerData);
|
||||
|
||||
var response = await Http.SendAsync(request);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var result = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
var status = result.GetProperty("status");
|
||||
var code = status.GetProperty("code").GetString();
|
||||
var message = status.GetProperty("message").GetString();
|
||||
|
||||
if (code == "000")
|
||||
{
|
||||
var data = result.GetProperty("data");
|
||||
var newToken = data.GetProperty("token").GetString();
|
||||
var refresh = data.GetProperty("refresh").GetString();
|
||||
|
||||
// 서버 세션에 토큰 저장
|
||||
var sessionReq = new HttpRequestMessage(HttpMethod.Post, "/api/v1/in/app/session/set");
|
||||
sessionReq.Headers.Add("Web-AM-Connect-Key", token);
|
||||
sessionReq.Content = JsonContent.Create(new[] {
|
||||
new { key = "token", value = newToken },
|
||||
new { key = "refresh", value = refresh }
|
||||
});
|
||||
var sessionResponse = await Http.SendAsync(sessionReq);
|
||||
|
||||
// 세션 스토리지 정리
|
||||
await JS.InvokeVoidAsync("sessionStorage.removeItem", "snsId");
|
||||
|
||||
LoadingService.HideLoading();
|
||||
await JS.InvokeVoidAsync("alert", "회원가입이 완료되었습니다.");
|
||||
NavigationManager.NavigateTo("/");
|
||||
}
|
||||
else
|
||||
{
|
||||
LoadingService.HideLoading();
|
||||
await JS.InvokeVoidAsync("alert", $"회원가입에 실패하였습니다.\n잠시 후 다시 시도해주세요.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LoadingService.HideLoading();
|
||||
await JS.InvokeVoidAsync("alert", "회원가입 중 오류가 발생했습니다.\n잠시 후 다시 시도해주세요.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoadingService.HideLoading();
|
||||
LoggerService.Write($"예외 발생: {ex.Message}");
|
||||
await JS.InvokeVoidAsync("alert", "회원가입 중 오류가 발생했습니다.\n잠시 후 다시 시도해주세요.");
|
||||
}
|
||||
}
|
||||
|
||||
private void SubmitPhone()
|
||||
{
|
||||
if (phonePart1.Length == 3 && phonePart2.Length == 4 && phonePart3.Length == 4)
|
||||
{
|
||||
var fullPhone = $"{phonePart1}-{phonePart2}-{phonePart3}";
|
||||
LoggerService.Write($"입력된 전화번호: {fullPhone}");
|
||||
}
|
||||
else
|
||||
{
|
||||
LoggerService.Write("전화번호를 올바르게 입력해주세요.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenDatePicker()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (birth == null) birth = DateTime.Now;
|
||||
// showPicker 대신 click() 이벤트를 발생시켜 달력을 표시
|
||||
await JS.InvokeVoidAsync("eval", $"document.querySelector('input[type=\"date\"]').click()");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerService.Write($"달력 표시 오류: {ex.Message}");
|
||||
}
|
||||
}
|
||||
private void OnBirthChanged(ChangeEventArgs e)
|
||||
{
|
||||
LoggerService.Write($"선택된 생일: {birth}");
|
||||
|
||||
}
|
||||
|
||||
|
||||
private DotNetObjectReference<Register> objRef;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
objRef = DotNetObjectReference.Create(this);
|
||||
}
|
||||
|
||||
protected async Task OnClickAddress()
|
||||
{
|
||||
await JS.InvokeVoidAsync("openKakaoPostcodePopup", objRef);
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public void SetAddress(string roadAddress, string jibunAddress, string zonecode)
|
||||
{
|
||||
address = roadAddress;
|
||||
LoggerService.Write($"SetAddress 호출됨: {roadAddress}, {jibunAddress}, {zonecode}");
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
<div class="flex justify-end items-center p-4 bg-white shadow">
|
||||
<a href="#about" class="mx-2 text-gray-700 hover:text-blue-600">About</a>
|
||||
<a href="#join" class="mx-2 text-gray-700 hover:text-blue-600">Join</a>
|
||||
<a href="#notice" class="mx-2 text-gray-700 hover:text-blue-600">What’s New</a>
|
||||
|
||||
<button class="ml-4 px-4 py-2 bg-red-600 text-white rounded hover:bg-blue-700"
|
||||
@onclick="OnClickLogin">
|
||||
Login
|
||||
</button>
|
||||
|
||||
</div>
|
|
@ -1,17 +0,0 @@
|
|||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Front.Program.Views.Project;
|
||||
|
||||
public partial class TopNav : ComponentBase
|
||||
{
|
||||
//로그인버튼을 누르면 페이지를 이동할거야
|
||||
[Inject]
|
||||
NavigationManager NavigationManager { get; set; } = default!;
|
||||
|
||||
public void OnClickLogin()
|
||||
{
|
||||
NavigationManager.NavigateTo("/auth");
|
||||
// Console.WriteLine("Redirecting to redirect page");
|
||||
}
|
||||
|
||||
}
|
|
@ -5,11 +5,16 @@
|
|||
@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
|
||||
@using Front.Program.Views.Academy.Common
|
||||
|
||||
|
||||
|
|
13
appsettings.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Security": {
|
||||
"EncryptionKey": "AcaMate2025SecureKeySeanForEncryption19940509",
|
||||
"EncryptionIV": "AcaMate2025IV9459"
|
||||
}
|
||||
}
|
11
package.json
|
@ -2,12 +2,21 @@
|
|||
"name": "tailwind-blazor-template",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"watch:css": "tailwindcss -i ./wwwroot/css/app.css -o ./wwwroot/css/tailwind.css --watch",
|
||||
"build:css": "tailwindcss -i ./wwwroot/css/app.css -o ./wwwroot/css/tailwind.css --minify",
|
||||
"watch:css": "tailwindcss -i ./wwwroot/css/app.css -o ./wwwroot/css/tailwind.css --watch"
|
||||
"build:publish": "rm -rf ./publish && /Users/tanine/.dotnet/dotnet publish -c Debug -o ./publish",
|
||||
"build:test": "rm -rf ../AcaMate_API/publish/debug/ && /Users/tanine/.dotnet/dotnet publish -c Debug -o ../AcaMate_API/publish/debug",
|
||||
"build:copy": "mkdir -p ../AcaMate_API/publish/debug/wwwroot && cp -r ./publish/wwwroot/* ../AcaMate_API/publish/debug/wwwroot"
|
||||
},
|
||||
|
||||
"devDependencies": {
|
||||
"tailwindcss": "^3.4.1",
|
||||
"postcss": "^8.4.21",
|
||||
"autoprefixer": "^10.4.14"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ module.exports = {
|
|||
},
|
||||
fontFamily: {},
|
||||
screens: {
|
||||
'xs': '400px',
|
||||
'xs': '480px',
|
||||
'sm': '640px',
|
||||
'md': '768px',
|
||||
'lg': '1024px',
|
||||
|
|
BIN
wwwroot/Resources/Images/Icon/DOT3.png
Normal file
After Width: | Height: | Size: 450 B |
BIN
wwwroot/Resources/Images/Icon/Down.png
Normal file
After Width: | Height: | Size: 468 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
BIN
wwwroot/Resources/Images/Icon/Link.png
Normal file
After Width: | Height: | Size: 912 B |
BIN
wwwroot/Resources/Images/Icon/Logout.png
Normal file
After Width: | Height: | Size: 636 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 945 B After Width: | Height: | Size: 945 B |
BIN
wwwroot/Resources/Images/Icon/Up.png
Normal file
After Width: | Height: | Size: 473 B |
|
@ -1,3 +1,7 @@
|
|||
{
|
||||
"ApiBaseUrl": "https://devacamate.ipstein.myds.me/"
|
||||
"ApiBaseUrl": "https://devacamate.ipstein.myds.me/",
|
||||
"Security": {
|
||||
"EncryptionKey": "AcaMate2025SecureKeyForEncryptionSean1994",
|
||||
"EncryptionIV": "AcaMate2025IV9459"
|
||||
}
|
||||
}
|
|
@ -1,3 +1,7 @@
|
|||
{
|
||||
"ApiBaseUrl": "https://acamate.ipstein.myds.me/"
|
||||
"ApiBaseUrl": "https://acamate.ipstein.myds.me/",
|
||||
"Security": {
|
||||
"EncryptionKey": "AcaMate2025SecureKeyForEncryptionSean1994",
|
||||
"EncryptionIV": "AcaMate2025IV9459"
|
||||
}
|
||||
}
|
|
@ -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">
|
||||
|
@ -31,9 +31,27 @@
|
|||
<a href="" class="underline text-blue-600 ml-2">Reload</a>
|
||||
<button class="ml-4 text-red-600 font-bold dismiss">✕</button>
|
||||
</div>
|
||||
<!-- 개발모드 설정 -->
|
||||
|
||||
<script>
|
||||
window.appConfig = {
|
||||
isDev: location.hostname.includes('devacamate.ipstein.myds.me') ||
|
||||
location.hostname === '0.0.0.0' ||
|
||||
location.port === '5144'
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
<script src="scripts/logger.js"></script>
|
||||
<script src="scripts/scroll.js"></script>
|
||||
<script src="scripts/apiSender.js"></script>
|
||||
<script src="scripts/jsCommonFunc.js"></script>
|
||||
<script src="scripts/kakao-postcode.js"></script>
|
||||
|
||||
|
||||
<!-- Blazor WASM 로딩 스크립트 -->
|
||||
<script src="_framework/blazor.webassembly.js" autostart="false"></script>
|
||||
|
||||
<script>
|
||||
const MIN_LOADING_MS = 2700;
|
||||
const loadingStart = performance.now();
|
||||
|
@ -41,13 +59,45 @@
|
|||
Blazor.start().then(() => {
|
||||
const elapsed = performance.now() - loadingStart;
|
||||
const remaining = Math.max(0, MIN_LOADING_MS - elapsed);
|
||||
setTimeout(() => {
|
||||
|
||||
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>
|
||||
|
||||
<script>
|
||||
window.getCookie = function (name) {
|
||||
const v = document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)');
|
||||
return v ? v.pop() : '';
|
||||
};
|
||||
|
||||
window.setCookie = function (name, value, days) {
|
||||
let expires = "";
|
||||
if (days) {
|
||||
const date = new Date();
|
||||
date.setTime(date.getTime() + (days*24*60*60*1000));
|
||||
expires = "; expires=" + date.toUTCString();
|
||||
}
|
||||
document.cookie = name + "=" + (value || "") + expires + "; path=/";
|
||||
};
|
||||
window.deleteCookie = function (name) {
|
||||
document.cookie = name + '=; Max-Age=-99999999;';
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
39
wwwroot/kakao-postcode.html
Normal file
|
@ -0,0 +1,39 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>주소검색</title>
|
||||
<script src="https://spi.maps.daum.net/imap/map_js_init/postcode.v2.js"></script>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
var layer = document.getElementById('layer');
|
||||
new daum.Postcode({
|
||||
oncomplete: function(data) {
|
||||
var jibunAddress = data.jibunAddress || data.autoJibunAddress || "";
|
||||
var postData = {
|
||||
roadAddress: data.roadAddress,
|
||||
jibunAddress: jibunAddress,
|
||||
zonecode: data.zonecode,
|
||||
};
|
||||
window.opener.postMessage({
|
||||
roadAddress: data.roadAddress,
|
||||
jibunAddress: data.jibunAddress,
|
||||
zonecode: data.zonecode
|
||||
}, window.location.origin);
|
||||
window.close();
|
||||
},
|
||||
width: "100%",
|
||||
height: "100%"
|
||||
}).embed(layer);
|
||||
layer.style.display = "block";
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
html, body { margin:0; padding:0; width:100vw; height:100vh; background:#fff; }
|
||||
#layer { width:100vw; height:100vh; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="layer"></div>
|
||||
</body>
|
||||
</html>
|
111
wwwroot/scripts/apiSender.js
Normal file
|
@ -0,0 +1,111 @@
|
|||
window.postWithHeader = function(url, method, headerKey, headerValue) {
|
||||
fetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
[headerKey] : headerValue
|
||||
}
|
||||
}).then(res => {
|
||||
if (res.redirected) {
|
||||
window.location.href = res.url;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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: args.method,
|
||||
headers: {
|
||||
[args.headerKey]: args.headerValue
|
||||
}
|
||||
});
|
||||
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (!response.ok) {
|
||||
window.appLogger.error('API 호출 실패:', response.status, response.statusText);
|
||||
return null;
|
||||
}
|
||||
if (!contentType || !contentType.includes('application/json')) {
|
||||
const text = await response.text();
|
||||
window.appLogger.error('JSON이 아닌 응답:', text);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
window.appLogger.log('API 응답 데이터:', data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
window.appLogger.error('API 호출 중 오류 발생:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
window.loadConfig = async function(configFile) {
|
||||
try {
|
||||
window.appLogger.log('설정 파일 로드 시도:', configFile);
|
||||
const response = await fetch(configFile);
|
||||
if (!response.ok) {
|
||||
window.appLogger.error('설정 파일 로드 실패:', response.status, response.statusText);
|
||||
return null;
|
||||
}
|
||||
const config = await response.json();
|
||||
window.appLogger.log('설정 파일 로드 성공:', configFile);
|
||||
return config;
|
||||
} catch (error) {
|
||||
window.appLogger.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) {
|
||||
window.appLogger.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) {
|
||||
window.appLogger.error('복호화 중 오류 발생:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
8
wwwroot/scripts/jsCommonFunc.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
window.openDatePicker = function (element) {
|
||||
if (element) {
|
||||
const wasReadOnly = element.readOnly;
|
||||
if (wasReadOnly) element.readOnly = false;
|
||||
element.showPicker();
|
||||
if (wasReadOnly) setTimeout(() => element.readOnly = true, 0);
|
||||
}
|
||||
};
|
28
wwwroot/scripts/kakao-postcode.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
|
||||
window.addEventListener("message", function (event) {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
if (event.data && event.data.roadAddress) {
|
||||
if (window.kakaoPostcodeDotNetRef) {
|
||||
window.kakaoPostcodeDotNetRef.invokeMethodAsync(
|
||||
"SetAddress",
|
||||
event.data.roadAddress,
|
||||
event.data.jibunAddress,
|
||||
event.data.zonecode
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.getKakaoPostcodeResult = function () {
|
||||
return window.kakaoPostcodeResult;
|
||||
};
|
||||
|
||||
window.openKakaoPostcodePopup = function (dotNetObjRef) {
|
||||
window.kakaoPostcodeResult = null;
|
||||
window.kakaoPostcodeDotNetRef = dotNetObjRef;
|
||||
window.open(
|
||||
"/kakao-postcode.html",
|
||||
"kakaoPostcodePopup",
|
||||
"width=500,height=600,scrollbars=no,resizable=no"
|
||||
);
|
||||
};
|
19
wwwroot/scripts/logger.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
window.appLogger = (function() {
|
||||
const isDev = window.appConfig?.isDev === true;
|
||||
|
||||
return {
|
||||
log: function(message, data) {
|
||||
if (isDev) console.log(`[LOG] ${message}`, data || '');
|
||||
},
|
||||
error: function(message, error) {
|
||||
if (isDev) console.error(`[ERROR] ${message}`, error || '');
|
||||
},
|
||||
warn: function(message, data) {
|
||||
if (isDev) console.warn(`[WARN] ${message}`, data || '');
|
||||
},
|
||||
// 운영 환경에서도 기록해야 하는 중요 오류
|
||||
critical: function(message, error) {
|
||||
console.error(`[CRITICAL] ${message}`, error || '');
|
||||
}
|
||||
};
|
||||
})();
|
3
wwwroot/scripts/scroll.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
window.scrollToDown = function (y = 0, behavior = "smooth") {
|
||||
window.scrollTo({ top: y, behavior: behavior });
|
||||
};
|