Compare commits

...

39 Commits
main ... main

Author SHA1 Message Date
92feb9bbe2 [] Top에 모바일일 경우 더보기 버튼 활성화 2025-07-04 17:39:17 +09:00
6342324108 [] 사이드 뷰 개발 2025-07-03 21:16:55 +09:00
3cbe24216d [🐛] 화면 크기에 따른 컴포넌트 크기 오류
1. 중앙 로고 이미지가 화면의 특정 사이즈에서 크기에 문제가 생기는 오류를 수정
2025-06-27 16:03:21 +09:00
e8b942a633 [] 로거 관리 서비스 추가
1. Console에 바로 나오던 메세지들 개발 환경에 따라 나오게 필터링 하는 서비스 개발
2. script  쪽에도 추가 하여 js 에서도 필터링 되게 구현
2025-06-27 16:01:54 +09:00
def25d2206 [] URI 파라미터 로직 수정 및 로거 기능 추가 중 2025-06-26 17:52:46 +09:00
c04152dac8 [] TopNav 관련 로직 추가 및 변경
1. 모바일에서 화면에 제대로 그려지지 않는 오류 수정
2. 화살표 그림 icon으로 직접 이미지 추가
3. 드롭다운 활성화시 뒷 배경 누르면 드롭다운 사라지는 기능 추가
2025-06-24 17:55:26 +09:00
250aac4b29 [♻️] 로그인 저장데이터 문제 발생시 로직
1. / 로 보내 버리기
2025-06-23 18:04:38 +09:00
1e563eb576 [♻️] 세션 스토리지 저장 로직 변경
1. API 호출 후 받아지는 데이터 반환하는 로직 추가
2. 스토리지 저장시 사용하는 변수 이름 변경
2025-06-20 17:56:58 +09:00
d89db8c890 [] 인트로 아카데미 리스트 로직 추가 및 API 로직 변경 2025-06-19 18:00:00 +09:00
e8580650cc [🐛] 헤더 키 이름 변경 2025-06-18 13:21:34 +09:00
3e31f9f46b [] am/Intro 화면 동작 구현
1. 로그인 연동
2. 화면 변환 연동
2.1. 아직 Academy 테이블 연동은 안되어있는 상황
2025-06-17 16:00:56 +09:00
2665dcbf64 [♻️] 로그인 동작 로직 개편
1. 로그인 동작을 위해서 viewmodel 로 관련 뷰에서 동작할 모든 로직을 viewmodel에서 관리
1.1. view 와 viewmodel의 관계는 1:N으로 동작하는것을 기반으로 두고 있음
2. API 접근하는 방식도 웹만의 접근 방법에서 수정
3. 로그인 동작 정보 받는 로직 수정
2025-06-16 17:47:35 +09:00
c371700e78 [] 학원용 입장페이지 작성
1. /am/...  도메인 생성
2. /am/intro 페이지 생성
3. 메인 레이아웃 상황 따라 변경
4. 로그인 정보 받아오는 로직 변경 중
4.1. 유저 정보에 대한 JSON은 받아왔으나 이를 저장하는 로직 구현 중
4.2. 로그인 정보를 받아오는 로직을 어디서 구현해야 할 지 고민 하는 중
2025-06-13 17:53:58 +09:00
05e8fcd0b0 [] 로그인 후 레이아웃 변경 2025-06-12 17:55:50 +09:00
9621169d57 [] 로그인 동작 및 Repository 동작
1. 쿠키만 사용하던 저장 동작을 다른 저장소도 사용하게 변경
1.1. 쿠키 대신 세션 레포지토리를 기본적으로 사용하게 Service 코드 구현
2. 로그인 되었을 경우 화면 표기 변경
2.1. 시작하기 버튼 hidden 처리
2.2. 사용자 이름 불러오기
2.3. 로그인 동작 관련 변수 스토리지 저장
2.4. 서버에서 직접적인 크라이언트 쿠키 저장이 아닌 서버는 뒤의 값으로 간섭하게 변경
2025-06-11 15:12:22 +09:00
3ffec93958 [] 로그인 및 화면 구조 변경
1. 회원가입 후 자동 로그인
2. 로그인 후 페이지 처리
3. 로딩 인디케이터 동작 구조 변경
2025-06-09 17:45:53 +09:00
1649818434 [] 로딩 인디케이터 추가 2025-06-05 16:24:16 +09:00
0698f65ddf [] 회원가입 동작
1. UI 수정
2. 전화번호 유효성 검사
3. 회원가입 로직 진행
4. 데이터 세션 저장 로직 추가
2025-06-04 17:53:51 +09:00
f3fee47c28 [] 회원가입 페이지
1. 주소 (카카오 주소 검색) 팝업 연동 및 데이터 받아오기
2. 생일 Date Picker 띄우고 데이터 받아오기
3. 전화번호 양식 만들기
4. TopNav 의 버튼들 색상 변경
2025-06-02 17:58:41 +09:00
c178a17c04 [] 회원가입 기능
1. sns 로그인 작업 후 회원 없는 경우 회원가입 로직으로 전환
2. 디자인 반영
3. 사용자에게 받을 정보 input box로 관리
2025-05-30 17:53:24 +09:00
a76e5fd574 [] 카카오 로그인 버튼 구현 2025-05-29 17:51:54 +09:00
e207e246cd [] 카카오 로그인 구현 중 2025-05-28 17:59:26 +09:00
a03aabc8fb [🐛] url 변경
1. API 가 바라보는 url 변경
2025-05-28 15:17:13 +09:00
f4d0138fec [] 페이지 개발
1. about 페이지 개발
2. 반응형 적용
3. 버튼 동작 적용
2025-05-28 15:05:52 +09:00
070e15ae70 [♻️] 내부 문구 수정
1. 내부 설명 문구 수정
2025-05-27 17:49:14 +09:00
9e2151cbd7 [🐛] 화면 반응형
1. 화면 반응형 동작 가능하게 수정
2025-05-27 16:34:56 +09:00
c1fdf2773d [] 서버 반영
1. 쿠키 delete 추가
2. about 페이지  v.0.1 추가
2025-05-27 15:40:20 +09:00
899a563aac [] 쿠키 동작
1. 쿠키 추가, 불러오기
2. API 읽어오기
3. Header 값 쿠키 저장 하기
2025-05-27 13:24:47 +09:00
9167e2a9d6 [] 동작 로직 수정
1. 퍼블리시 동작 로직 수정
2. 쿠키 설정 동작 로직 추가
2025-05-26 17:44:13 +09:00
1f3b6b3217 [♻️] Jenkins 동작 로직 수정 7 2025-05-22 09:26:03 +09:00
d51449aa36 [♻️] Jenkins 동작 로직 수정 6 2025-05-21 18:02:57 +09:00
8da46338e5 Merge remote-tracking branch 'origin/main' 2025-05-21 18:01:55 +09:00
02d5797223 [♻️] Jenkins 동작 로직 수정 5 2025-05-21 18:01:03 +09:00
9ca480a134 [♻️] Jenkins 동작 로직 수정 4 2025-05-21 18:00:38 +09:00
7a34c9f95c [♻️] Jenkins 동작 로직 수정 4 2025-05-21 17:50:53 +09:00
1d2f8db794 [♻️] Jenkins 동작 로직 수정 3 2025-05-21 17:29:49 +09:00
9447bc3053 [♻️] Jenkins 동작 로직 수정 2 2025-05-21 17:24:32 +09:00
fc5e072697 [♻️] 젠킨스 동작 로직 수정 2025-05-21 17:14:01 +09:00
96b4937653 [📝] 이상한 코드 삭제 2025-05-20 14:22:26 +09:00
68 changed files with 2582 additions and 1077 deletions

View File

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

View File

@ -11,6 +11,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.8"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.8" PrivateAssets="all"/>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.10" />
</ItemGroup>
<ItemGroup>
@ -26,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
View File

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

View File

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

View File

@ -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 />
<!-- 본문 영역 -->
<div class="flex flex-1 flex-col md:flex-row">
@* <!-- 사이드 메뉴 --> *@
@* <div class="hidden md:block md:w-64 bg-white shadow"> *@
@* <SideNav /> *@
@* </div> *@
@* <TopBanner /> *@
@if(isAcademy)
{
<div class="flex flex-1 flex-col md:flex-row">
@if (!isIntro && UserStateService.isLogin)
{
<div class="hidden md:block w-64 bg-white shadow-lg border-r border-gray-200 fixed top-0 bottom-0">
<LeftSideAcademy/>
</div>
<div class="flex flex-1 flex-col md:ml-64">
<div class="fixed top-0 right-0 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 p-4 sm:p-6 md:p-8">
<main class="flex-1 w-full w-max-960 mx-auto">
@Body
</main>
</div>
}
<!-- 플로팅 버튼 -->
<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>

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

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

View File

@ -0,0 +1,22 @@
using System.ComponentModel.DataAnnotations;
namespace Front.Program.Models;
public class Academy
{
public required string bid { get; set; }
public string business_name { get; set; } = string.Empty;
public string business_owner { get; set; } = string.Empty;
public string businessOwnerUID { get; set; } = string.Empty;
public string business_number { get; set; } = string.Empty;
public DateTime business_date { get; set; }
public string business_address { get; set; } = string.Empty;
public string business_contact { get; set; } = string.Empty;
public string uid { get; set; } = string.Empty;
}
public class SimpleAcademy
{
public required string bid { get; set; }
public string name { get; set; }
}

View File

@ -0,0 +1,6 @@
namespace Front.Program.Models;
public class APIHeader
{
public string header { get; set; } = string.Empty;
}

View File

@ -0,0 +1,13 @@
namespace Front.Program.Models;
public class UserData
{
public string Uid { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public DateTime? Birth { get; set; }
public string Type { get; set; } = string.Empty;
public string? DeviceId { get; set; }
public bool AutoLoginYn { get; set; }
public DateTime LoginDate { get; set; }
public string? PushToken { get; set; }
}

View File

@ -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 )
*/

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

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

View File

@ -0,0 +1,42 @@
using Microsoft.AspNetCore.Components;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
namespace Front.Program.Services;
public class QueryParamService
{
public Dictionary<string, string> ParseQueryParam(System.Uri uri)
{
var result = new Dictionary<string, string>();
if (!string.IsNullOrEmpty(uri.Query))
{
var query = uri.Query.TrimStart('?');
var parameters = query.Split('&')
.Select(p => p.Split('='))
.Where(p => p.Length == 2)
.ToDictionary(p => p[0], p => p[1]);
result = parameters;
}
return result;
}
public async Task AuthCheck(Dictionary<string, string> parameters, StorageService storageService)
{
if (parameters.TryGetValue("auth", out var auth))
{
LoggerService.Write($"auth 파라미터 값: {auth}");
if (auth == "true")
{
await storageService.SetItemAsync("IsLogin", "true");
LoggerService.Write("로그인 상태를 true로 설정했습니다.");
}
else
{
await storageService.RemoveItemAsync("IsLogin");
LoggerService.Write("로그인 상태를 제거했습니다.");
}
}
}
}

View File

@ -0,0 +1,120 @@
using System.Security.Cryptography;
using System.Text;
using Microsoft.JSInterop;
using System.Text.Json;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
namespace Front.Program.Services;
public class SecureService
{
private readonly ILogger<SecureService> _logger;
private readonly IJSRuntime _jsRuntime;
private readonly IWebAssemblyHostEnvironment _environment;
private string? _key;
private string? _iv;
private Task? _initializationTask;
public SecureService(
ILogger<SecureService> logger,
IJSRuntime jsRuntime,
IWebAssemblyHostEnvironment environment)
{
_logger = logger;
_jsRuntime = jsRuntime;
_environment = environment;
_initializationTask = InitializeAsync();
}
private async Task InitializeAsync()
{
try
{
var configFile = $"appsettings.{_environment.Environment}.json";
_logger.LogInformation($"설정 파일 로드: {configFile}");
var config = await _jsRuntime.InvokeAsync<JsonElement>("loadConfig", configFile);
if (config.ValueKind == JsonValueKind.Null)
{
throw new InvalidOperationException($"설정 파일을 로드할 수 없습니다: {configFile}");
}
var security = config.GetProperty("Security");
_key = security.GetProperty("EncryptionKey").GetString()
?? throw new ArgumentNullException("Security:EncryptionKey");
_iv = security.GetProperty("EncryptionIV").GetString()
?? throw new ArgumentNullException("Security:EncryptionIV");
}
catch (Exception ex)
{
_logger.LogError($"설정 로드 중 오류 발생: {ex.Message}");
throw;
}
}
private async Task EnsureInitializedAsync()
{
if (_initializationTask != null)
{
await _initializationTask;
_initializationTask = null;
}
}
private byte[] GetKeyBytes(string key)
{
using (var sha256 = SHA256.Create())
{
return sha256.ComputeHash(Encoding.UTF8.GetBytes(key));
}
}
private byte[] GetIVBytes(string iv)
{
using (var sha256 = SHA256.Create())
{
var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(iv));
return hash.Take(16).ToArray(); // IV는 16바이트만 필요
}
}
public async Task<string> EncryptAsync(string plainText)
{
await EnsureInitializedAsync();
if (_key == null || _iv == null)
throw new InvalidOperationException("암호화 키가 초기화되지 않았습니다.");
try
{
return await _jsRuntime.InvokeAsync<string>("encryptText", plainText, _key, _iv);
}
catch (Exception ex)
{
_logger.LogError($"암호화 중 오류 발생: {ex.Message}");
throw;
}
}
public async Task<string> DecryptAsync(string cipherText)
{
await EnsureInitializedAsync();
if (_key == null || _iv == null)
throw new InvalidOperationException("암호화 키가 초기화되지 않았습니다.");
try
{
return await _jsRuntime.InvokeAsync<string>("decryptText", cipherText, _key, _iv);
}
catch (Exception ex)
{
_logger.LogError($"복호화 중 오류 발생: {ex.Message}");
throw;
}
}
// 동기 메서드는 비동기 메서드를 호출하도록 수정
public string Encrypt(string plainText) => EncryptAsync(plainText).GetAwaiter().GetResult();
public string Decrypt(string cipherText) => DecryptAsync(cipherText).GetAwaiter().GetResult();
}

View File

@ -0,0 +1,83 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
namespace Front.Program.Services;
public enum STORAGE_TYPE
{
Cookie,
Local,
Session
}
public class StorageService
{
private readonly IJSRuntime _js;
public StorageService(IJSRuntime js)
{
_js = js;
}
public async Task SetItemAsync(string key, string value, STORAGE_TYPE type = STORAGE_TYPE.Session, int expire_time = 1)
{
await (type switch
{
STORAGE_TYPE.Cookie => _js.InvokeVoidAsync("setCookie", key, value, expire_time),
STORAGE_TYPE.Local => _js.InvokeVoidAsync("localStorage.setItem", key, value),
STORAGE_TYPE.Session => _js.InvokeVoidAsync("sessionStorage.setItem", key, value),
_ => throw new ArgumentException("Invalid storage type")
});
}
public async Task<string?> GetItemAsync(string key, STORAGE_TYPE type = STORAGE_TYPE.Session)
{
return type switch
{
STORAGE_TYPE.Cookie => await _js.InvokeAsync<string>("getCookie", key),
STORAGE_TYPE.Local => await _js.InvokeAsync<string>("localStorage.getItem", key),
STORAGE_TYPE.Session => await _js.InvokeAsync<string>("sessionStorage.getItem", key),
_ => null
};
}
public async Task<bool> RemoveItemAsync(string key, STORAGE_TYPE type = STORAGE_TYPE.Session)
{
try
{
await (type switch
{
STORAGE_TYPE.Cookie => _js.InvokeVoidAsync("deleteCookie", key),
STORAGE_TYPE.Local => _js.InvokeVoidAsync("localStorage.removeItem", key),
STORAGE_TYPE.Session => _js.InvokeVoidAsync("sessionStorage.removeItem", key),
_ => throw new ArgumentException("Invalid storage type")
});
return true;
}
catch
{
return false;
}
}
}
/*
1. Cookie
: ( )
:
1. HTTP / .
2. HttpOnly나 secure .
2. Local
:
: JS
1. .
2. .
3. Sesson
: / ( )
: JS
1. / .
2. .
*/

View File

@ -0,0 +1,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);
}
}

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

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

View File

@ -0,0 +1,2 @@
@page "/am/main"
<h3>AcademyMain</h3>

View 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("쿼리 검사");
}
}

View File

@ -0,0 +1,70 @@
<div class="h-[72px] p-4 border-b border-gray-200 flex items-center">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-gray-200 overflow-hidden">
<img src="Resources/Images/Logo/Crystal_Icon.png" alt="프로필" class="w-full h-full object-cover" />
</div>
<div>
<!-- 회원 이름이 오는 곳 -->
<div class="text-gray-900 text-base font-medium">@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>

View 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";
}

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

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

View File

@ -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">
<!-- 상단 배너 영역 -->
<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>
<!-- 상단 배너 영역 끝 -->
<!-- 플랫폼 설명 영역 -->
<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>
@* 클릭시 onClick 이벤트 발생 *@
<button class="bg-blue-700 text-white px-4 py-2 rounded hover:bg-blue-800"
@onclick="OnClickEvent">
Click Me
</button>
<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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Components;
namespace Front.Program.Views.Project;
namespace Front.Program.Views.Project.Common;
public partial class RedirectPage : ComponentBase
{
@ -9,6 +9,6 @@ public partial class RedirectPage : ComponentBase
protected override void OnInitialized()
{
NavigationManager.NavigateTo("/about",true);
NavigationManager.NavigateTo("/am/intro",true);
}
}

View File

@ -1,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">
<!-- 왼쪽 아이콘 -->

View File

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

View File

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

View File

@ -0,0 +1,57 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using System;
using System.Net.Http.Json;
using System.Text.Json;
using Front.Program.Models;
using Front.Program.Services;
using Front.Program.ViewModels;
namespace Front.Program.Views.Project.Common;
public partial class TopProjectNav : ComponentBase
{
[Inject] NavigationManager NavigationManager { get; set; } = default!;
[Inject] UserStateService UserStateService { get; set; } = default!;
[Inject] IJSRuntime JS { get; set; } = default!;
[Inject] HttpClient Http { get; set; } = default!;
protected bool isOpen = false;
protected override async Task OnInitializedAsync()
{
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;
// }
}
}

View File

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

View File

@ -0,0 +1,57 @@
using System.Net.Http.Json;
using System.Text.Json;
using Front.Program.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
namespace Front.Program.Views.Project.ConnectUser;
public partial class Auth : ComponentBase, IDisposable
{
[Inject] NavigationManager NavigationManager { get; set; } = default!;
[Inject] LoadingService LoadingService { get; set; } = default!;
[Inject] HttpClient Http { get; set; } = default!;
[Inject] IJSRuntime JS { get; set; } = default!;
protected override void OnInitialized()
{
// LocationChanged 이벤트 구독
NavigationManager.LocationChanged += HandleLocationChanged;
}
private void HandleLocationChanged(object? sender, Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs e)
{
// 페이지 이동이 발생했을 때 로딩 상태 해제
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();
}
}
}

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

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -53,7 +53,7 @@ module.exports = {
},
fontFamily: {},
screens: {
'xs': '400px',
'xs': '480px',
'sm': '640px',
'md': '768px',
'lg': '1024px',

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 912 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 945 B

After

Width:  |  Height:  |  Size: 945 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 B

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@ -12,7 +12,7 @@
<body class="bg-gray-50 text-gray-900 font-sans">
<!-- 로딩 오버레이 -->
<div class="fixed inset-0 z-50 flex items-center justify-center bg-gray-100" id="loading-overlay">
<div class="fixed inset-0 z-50 flex items-center justify-center bg-gray-100 hidden" id="loading-overlay" data-show-on="/about">
<!-- 로딩 오버레이 내부 -->
<div class="flex flex-col items-center">
<div class="relative w-48 h-48 overflow-hidden">
@ -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');
if (overlay) overlay.remove();
}, remaining);
const overlay = document.getElementById('loading-overlay');
const showOnPath = overlay?.getAttribute('data-show-on');
if (overlay && showOnPath && window.location.pathname === showOnPath) {
overlay.classList.remove('hidden');
setTimeout(() => {
if (overlay) overlay.remove();
}, remaining);
} else if (overlay) {
overlay.remove();
}
}).catch(err => {
console.error("❌ Blazor 로딩 실패", err);
console.error("Blazor 로딩 실패", err);
});
</script>
<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>

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

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

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

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

View File

@ -0,0 +1,3 @@
window.scrollToDown = function (y = 0, behavior = "smooth") {
window.scrollTo({ top: y, behavior: behavior });
};