Compare commits

..

40 Commits
main ... main

Author SHA1 Message Date
7284dca6f2 Record
정리전 로컬 데이터 푸시
2025-10-28 20:47:58 +09:00
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
95 changed files with 3038 additions and 1092 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

@ -65,7 +65,15 @@ namespace Front.Program.Pages;
- [C# 코드 스타일 권장 사항 (Microsoft Docs)]("https://learn.microsoft.com/ko-kr/dotnet/csharp/fundamentals/coding-style/coding-conventions")을 따른다.
- 클래스, 인터페이스, 메서드, 변수, 상수 등 모든 명명 및 스타일은 위 문서를 기준으로 작성한다.
- **필요**시 `.editorconfig` 파일을 통해 프로젝트 전체에 스타일 자동 적용 도구를 사용할 수 있다.
- 작업자 이외의 제 3자에게 유출되어야 하는 데이터의 이름을 작성시에는 그 의미를 쉽게 유추 할 수 없게 영단어를 줄여서 쓴다.
1. 단어의 조합에서 각 단어의 앞 글자는 `대문자로 시작`하고 `_ , - , 공백` 등은 사용하지 않는다.
- 예) `username`, `user-name`, `user name``UserName`
2. 단어를 줄일때는 자음에 해당하는 영문자만 사용한다. (A, E, I, O, U 는 제거)
- 예) `password``Pswd`, `phone number``PhnNmbr`
3. 단어의 시작이 모음일 경우에는 첫 글자에 한해서만 모음을 허용한다.
- 예) `email``Emil`, `address``Adrss`
4. 단어를 줄일때는 2글자로 하며, 단어의 길이가 2글자 이하인 경우에는 줄이지 않는다.
- 예) `user``Us`, `id``Id`, `username``UsNm`
---
## 3⃣ Git 규칙

View File

@ -0,0 +1,24 @@
# Front 웹 개발 후 서버 배포 방법 (Local)
## 1. .NET 실행 설정 프로파일 동작
- 단순하게 run 하는 방식으로 `./bin/Debug/net8.0/` 밑에 프로젝트 결과가 생성된다.
- `./wwwroot` 내부에 있는 정적리소스들을 활용하여 웹 페이지를 보여준다.
- 해당 방법은 단순하게 서버에도 연결하지 않고 그냥 Front 로컬로 사용하는 방법이다.
## 2. package.json 에서 npm 스크립트 실행
- `dotnet publish -c Release -o ./publish` 를 실행해서 ./publish 밑에 결과물을 생성한다.
- 그런데 npm에서는 dotnet이 실행 할 수 없기에 직접 dotnet의 경로를 지정해서 해야한다.
- ./publish 폴더를 퍼블리시 동작마다 삭제를 안해주면 계속 중첩해서 추가가 되기에 일단 삭제를 하는 동작을 해줘야 한다.
- Front 프로젝트의 퍼블리시를 위해서는 Tailwind CSS 를 사용하기에 `build:css`를 먼저 실행해준다.
- 그리고 `build:publish` 를 실행하면서 `./publish` 폴더를 삭제하고 다시 퍼블리시를 한다.
- 마지막으로 생성된 `./publish/wwwroot` 폴더 안에 내용 전부를 `../AcaMate_API/publish/debug/wwwroot` 폴더에 복사한다.
# Front 웹 개발 후 서버 배포 방법 (SERVER)
## 1. Development, Production 공통
- `build:css` 실행을 먼저 진행을 해주고 나서 Local로 테스트를 진행한다.
- 이상 없음이 판단 되면 gitea 에 push 를 해준다.
- Jenkins 동작을 하게 될 경우 서버 저장소에 클론을 하게 된다.
- 기존에 있는 데이터는 삭제하지 않기에 만약에 충돌이 있는 경우가 있다.
- 기존 디렉터리와 비교해서 맞지 않는 코드의 경우에는 수동으로 삭제를 진행한다.
- `acamate-front-build-debug` 또는 `acamate-front-build-relase` 컨테이너를 수동으로 실행시켜 준다.
- private repository 나 따로 직접 서버에 반영을 해야 하는 리소스가 있는 경우에는 직접 넣어준다.
- `acamate-front-build-debug` 또는 `acamate-front-build-relase` 컨테이너를 수동으로 실행시켜 준다.

9
Documents/Tip.md Normal file
View File

@ -0,0 +1,9 @@
# Tip
## JSON 파싱시
- JSON 파싱시에 소문자로 들어오는데 이를 C#의 클래스에 맞게 넣어주기 위해서는 다음과 같은 구문을 넣어줘야지 안그러면 제대로 파싱이 되지 않는다.
- 둘 다 소문자이면 아무 상관 없을건데 여기서 소문자로 쓰면 경고가 나오니까
```c#
var userData = JsonSerializer.Deserialize<UserData>(userDataJson,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
```

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,58 @@ 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<NavigationService>(); // 추가
// builder.Services.AddSingleton<LoggerService>(sp
await builder.Build().RunAsync();

View File

@ -1,50 +1,74 @@
@* @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
@implements IDisposable
<div class="min-h-screen flex flex-col bg-gray-50 text-gray-900">
<div class="min-h-screen flex flex-col 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 /> *@
@if (!isIntro && UserStateService.isLogin)
{
@* <div class="fixed flex flex-1 flex-col min-w-0"> *@
<div class="hidden md:block w-64 bg-normal-light shadow-lg fixed top-0 bottom-0 overflow-y-auto">
<SympleProfile/>
<LeftSideAcademy/>
</div>
@* </div> *@
<div class="flex flex-1 flex-col bg-white md:ml-64 min-w-0 min-h-screen">
<!-- TopNav 고정 -->
<div class="h-18 top-0 right-0 md:left-64 left-0 z-20 fixed bg-normal-light">
<TopNavAcademy/>
</div>
<!-- 본문 컨텐츠 -->
<main class="flex-1 p-4 sm:p-6 md:p-8">
<!-- 빨간 그림자 컨테이너 - TopNav 바로 아래 고정 -->
<div class="fixed bottom-0 right-0 md:left-64 left-0 z-10 bg-red
rounded-tl-2xl rounded-tr-2xl
shadow-[inset_4px_4px_4px_0px_rgba(0,0,0,0.25)] md:p-10 px-4 py-6"
style="top: 4.5rem;">
<!-- Body 컨텐츠 - 스크롤 영역 -->
<div class="h-full overflow-y-auto">
@Body
</div>
</div>
</div>
}
else
{
<main class="flex-1 w-full max-w-[960px] mx-auto overflow-y-auto">
@Body
</main>
}
</div>
}
else
{
@if (!isHidePrjTop)
{
<TopProjectNav />
}
<!-- 본문 컨텐츠 -->
<main class="flex-1 w-full max-w-[960px] mx-auto overflow-y-auto">
@Body
</main>
}
<!-- 플로팅 버튼 -->
<FloatingButton />
@* <!-- 플로팅 버튼 --> *@
@* <FloatingButton /> *@
@* *@
@* <!-- 하단 메뉴 --> *@
@* <BottomNav /> *@
@* <Footer /> *@
<!-- 하단 메뉴 -->
<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,53 @@
using Microsoft.JSInterop;
namespace Front.Program.Services;
public class NavigationService
{
private readonly IJSRuntime _jsRuntime;
public NavigationService(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
/// <summary>
/// 뒤로가기를 완전히 막습니다
/// </summary>
public async Task PreventBackNavigationAsync()
{
await _jsRuntime.InvokeVoidAsync("eval", @"
history.pushState(null, null, location.href);
window.addEventListener('popstate', function(event) {
history.pushState(null, null, location.href);
});
");
}
/// <summary>
/// 확인 대화상자와 함께 뒤로가기를 막습니다
/// </summary>
public async Task PreventBackNavigationWithConfirmAsync(string message = "정말로 페이지를 떠나시겠습니까?")
{
await _jsRuntime.InvokeVoidAsync("eval", $@"
history.pushState(null, null, location.href);
window.addEventListener('popstate', function(event) {{
if (confirm('{message}')) {{
history.back();
}} else {{
history.pushState(null, null, location.href);
}}
}});
");
}
/// <summary>
/// 뒤로가기 방지를 해제합니다
/// </summary>
public async Task AllowBackNavigationAsync()
{
await _jsRuntime.InvokeVoidAsync("eval", @"
window.removeEventListener('popstate', arguments.callee);
");
}
}

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 w-full min-h-screen flex-col bg-normal-normal group/design-root" 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,13 @@
@page "/am/main"
<!-- 스크롤 가능한 컨텐츠 영역 -->
<div class="h-full overflow-y-auto overflow-x-hidden gap-14 flex flex-col">
<BookMark />
@* <BookMark /> *@
@* <BookMark /> *@
@* <BookMark /> *@
@* <BookMark /> *@
@* <BookMark /> *@
@* <BookMark /> *@
</div>

View File

@ -0,0 +1,144 @@
using Front.Program.Services;
using Microsoft.AspNetCore.Components;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Front.Program.ViewModels;
using Front.Program.Views.Academy.Main;
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] NavigationService NavigationService { get; set; } = default!;
private List<FavoriteItem> Favorites { get; set; } = new();
private AttendanceInfo Attendance { get; set; } = new();
private List<LearningClass> LearningClasses { get; set; } = new();
private List<ScheduleItem> ScheduleItems { get; set; } = new();
private AcademyNewsItem News { get; set; } = new();
private List<UpcomingClassItem> UpcomingClasses { get; set; } = new();
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// 뒤로가기를 완전히 막기
await NavigationService.PreventBackNavigationAsync();
// 또는 확인 대화상자와 함께 막기
// await NavigationService.PreventBackNavigationWithConfirmAsync("정말로 이 페이지를 떠나시겠습니까?");
}
}
protected override void OnInitialized()
{
// Sample Data Initialization
Favorites = new List<FavoriteItem>
{
new() { Name = "학습 > 수업" },
new() { Name = "학습 > 과제" },
new() { Name = "커뮤니티 > 공지" },
new() { Name = "커뮤니티 > Q&A" },
new() { Name = "마이페이지" }
};
Attendance = new AttendanceInfo
{
Month = 7,
MonthlyPercentage = 10,
MonthlyAttendedDays = 11,
MonthlyTotalDays = 31,
DailyPercentage = 75,
DailyAttendedHours = 3,
DailyTotalHours = 4
};
LearningClasses = new List<LearningClass>
{
new() { Day = "월", Teacher = "선생님1", ClassName = "선생님1 의 수업", Progress = 20, Homework = "O" },
new() { Day = "월", Teacher = "선생님2", ClassName = "선생님2 의 수업", Progress = 30, Homework = "X" },
new() { Day = "화", Teacher = "선생님3", ClassName = "선생님3 의 수업", Progress = 45, Homework = "O" }
};
ScheduleItems = new List<ScheduleItem>
{
new() { IsImportant = true, Description = "중간고사", Date = new System.DateTime(2025, 12, 31) },
new() { IsImportant = false, Description = "보강수업", Date = new System.DateTime(2025, 12, 31) },
new() { IsImportant = true, Description = "학부모 상담", Date = new System.DateTime(2025, 12, 31) }
};
News = new AcademyNewsItem
{
Title = "School Closure on Friday",
Content = "Due to the upcoming storm, the school will be closed on Friday, March 15th. Please stay safe and monitor your email for updates.",
Date = new System.DateTime(2025, 12, 31),
ImageUrl = "" // or a path to an image
};
UpcomingClasses = new List<UpcomingClassItem>
{
new() { ClassName = "수업 이름이 얼마나 길어질지는 모르겠지만 이정도는?", Date = "2025-07-14 (월)", Time = "09:00 ~ 11:00", Teacher = "이름이다섯 선생님" },
new() { ClassName = "수학", Date = "2025-07-14 (월)", Time = "11:00 ~ 13:00", Teacher = "김수학 선생님" },
new() { ClassName = "영어", Date = "2025-07-15 (화)", Time = "09:00 ~ 11:00", Teacher = "박영어 선생님" },
new() { ClassName = "과학", Date = "2025-07-15 (화)", Time = "11:00 ~ 13:00", Teacher = "최과학 선생님" },
new() { ClassName = "코딩", Date = "2025-07-16 (수)", Time = "14:00 ~ 16:00", Teacher = "이코딩 선생님" }
};
}
// Data Models
public class FavoriteItem
{
public string Name { get; set; } = "";
}
public class AttendanceInfo
{
public int Month { get; set; }
public int MonthlyPercentage { get; set; }
public int MonthlyAttendedDays { get; set; }
public int MonthlyTotalDays { get; set; }
public int DailyPercentage { get; set; }
public int DailyAttendedHours { get; set; }
public int DailyTotalHours { get; set; }
}
public class LearningClass
{
public string Day { get; set; } = "";
public string Teacher { get; set; } = "";
public string ClassName { get; set; } = "";
public int Progress { get; set; }
public string Homework { get; set; } = "";
}
public class ScheduleItem
{
public bool IsImportant { get; set; }
public string Description { get; set; } = "";
public System.DateTime Date { get; set; }
}
public class AcademyNewsItem
{
public string Title { get; set; } = "";
public string Content { get; set; } = "";
public System.DateTime Date { get; set; }
public string? ImageUrl { get; set; }
}
public class UpcomingClassItem
{
public string ClassName { get; set; } = "";
public string Date { get; set; } = "";
public string Time { get; set; } = "";
public string Teacher { get; set; } = "";
}
}
}

View File

@ -0,0 +1,56 @@
<!-- 메뉴 섹션 -->
<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,8 @@
using Front.Program.ViewModels;
using Microsoft.AspNetCore.Components;
namespace Front.Program.Views.Academy.Common;
public partial class LeftSideAcademy : ComponentBase
{
}

View File

@ -0,0 +1,13 @@
<div class="w-64 h-16 p-4 bg-white rounded-tr-2xl rounded-bl-2xl rounded-br-2xl shadow-[inset_-4px_-4px_4px_0px_rgba(0,0,0,0.25)] inline-flex justify-start items-center gap-3 overflow-hidden">
<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>

View File

@ -0,0 +1,13 @@
using Front.Program.ViewModels;
using Microsoft.AspNetCore.Components;
namespace Front.Program.Views.Academy.Common;
public partial class SympleProfile : 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,56 @@
<div class="flex items-center justify-between whitespace-nowrap 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

@ -0,0 +1,19 @@
<div class="self-stretch flex flex-col justify-start items-start gap-4 scrollbar-hide">
<div class="flex flex-col justify-center items-start gap-1">
<div class="justify-center text-text-title text-3xl font-bold font-['Noto_Sans_KR']">즐겨 찾기</div>
<div class="justify-center">
<span class="text-text-detail text-base font-normal font-['Noto_Sans_KR']">즐겨찾기는 최대 </span>
<span class="text-text-detail text-base font-bold font-['Noto_Sans_KR']">5개</span>
<span class="text-text-detail text-base font-normal font-['Noto_Sans_KR']"> 까지 가능합니다.</span>
</div>
</div>
<div class="self-stretch flex justify-start items-center gap-6 overflow-x-auto overflow-y-hidden scrollbar-hide"style="-ms-overflow-style: none; scrollbar-width: none;">
@foreach (var favorite in Favorites)
{
<div class="p-2 rounded-lg outline outline-2 outline-offset-[-2px] outline-text-disabled flex justify-start items-center gap-3 overflow-hidden flex-shrink-0">
<img src="Resources/Images/Icon/Bookmark.png" alt="Home" class="w-8 h-8">
<div class="justify-center text-text-title text-[20px] font-medium font-['Noto_Sans_KR'] whitespace-nowrap">@favorite.Name</div>
</div>
}
</div>
</div>

View File

@ -0,0 +1,26 @@
using Microsoft.AspNetCore.Components;
namespace Front.Program.Views.Academy.Main;
public partial class BookMark : ComponentBase
{
public List<FavoriteItem> Favorites { get; set; } = new();
protected override void OnInitialized()
{
Favorites = new List<FavoriteItem>
{
new() { Name = "학습 > 수업" },
new() { Name = "학습 > 과제" },
new() { Name = "커뮤니티 > 공지" },
new() { Name = "커뮤니티 > Q&A" },
new() { Name = "마이페이지" }
};
}
public class FavoriteItem
{
public string Name { get; set; } = "";
}
}

View File

@ -0,0 +1,62 @@
<div class="flex-shrink-0 w-[600px] h-72 p-6 rounded-lg outline outline-2 outline-offset-[-2px] outline-neutral-400 inline-flex flex-col justify-between items-start overflow-hidden">
<div class="self-stretch inline-flex justify-between items-start">
<div class="w-72 inline-flex flex-col justify-start items-start gap-2">
<div class="justify-center text-stone-900 text-2xl font-bold font-['Noto_Sans_KR']">출석 확인</div>
<div class="self-stretch inline-flex justify-start items-start gap-1">
<div class="justify-center text-neutral-600 text-base font-normal font-['Noto_Sans_KR']">최근</div>
<div class="justify-center text-stone-900 text-base font-medium font-['Noto_Sans_KR']">@Attendance.Month 월</div>
<div class="justify-center text-neutral-600 text-base font-normal font-['Noto_Sans_KR']">의 출석을 확인할 수 있습니다.</div>
</div>
</div>
<div class="w-6 h-6 relative overflow-hidden">
<div class="w-2 h-3 left-[8px] top-[6px] absolute bg-stone-500"></div>
</div>
</div>
<div class="self-stretch inline-flex justify-between items-center">
<div class="inline-flex flex-col justify-start items-start gap-2">
<div class="w-20 py-1 inline-flex justify-center items-center gap-1">
<div class="text-center justify-center text-stone-900 text-base font-medium font-['Noto_Sans_KR'] leading-normal">월간</div>
<div class="text-center justify-center text-neutral-600 text-base font-normal font-['Noto_Sans_KR'] leading-normal">출석</div>
</div>
<div class="self-stretch inline-flex justify-start items-center gap-4">
<div class="w-20 h-20 relative">
<div class="w-14 h-14 left-[70px] top-[10px] absolute origin-top-left rotate-90 rounded-full outline outline-[12px] outline-offset-[-6px] outline-stone-400"></div>
<div class="w-14 h-14 left-[10px] top-[10px] absolute rounded-full outline outline-[12px] outline-offset-[-6px] outline-red-200"></div>
<div class="left-[23px] top-[31px] absolute justify-start text-Colors-Red text-base font-bold font-['Noto_Sans_KR']">@Attendance.MonthlyPercentage%</div>
</div>
<div class="min-w-24 flex justify-start items-center gap-2.5">
<div class="w-24 self-stretch py-1 flex justify-between items-center">
<div class="w-8 h-4 text-center justify-center text-Colors-Red text-xl font-medium font-['Noto_Sans_KR'] leading-loose">@Attendance.MonthlyAttendedDays</div>
<div class="w-2 h-4 text-center justify-center text-neutral-600 text-xs font-medium font-['Noto_Sans_KR'] leading-none">/</div>
<div class="w-8 h-4 text-center justify-center text-neutral-600 text-xl font-normal font-['Noto_Sans_KR'] leading-loose">@Attendance.MonthlyTotalDays</div>
<div class="text-center justify-center text-neutral-600 text-base font-normal font-['Noto_Sans_KR'] leading-normal">일</div>
</div>
</div>
</div>
</div>
<div class="w-0.5 h-20 bg-neutral-400 rounded-xs"></div>
<div class="inline-flex flex-col justify-start items-start gap-2">
<div class="w-20 py-1 inline-flex justify-center items-center gap-1">
<div class="text-center justify-center text-stone-900 text-base font-medium font-['Noto_Sans_KR'] leading-normal">일일</div>
<div class="text-center justify-center text-neutral-600 text-base font-normal font-['Noto_Sans_KR'] leading-normal">출석</div>
</div>
<div class="inline-flex justify-start items-center gap-4">
<div class="w-20 h-20 relative">
<div class="w-14 h-14 left-[70px] top-[10px] absolute origin-top-left rotate-90 rounded-full outline outline-[12px] outline-offset-[-6px] outline-slate-400"></div>
<div class="w-14 h-14 left-[10px] top-[10px] absolute rounded-full outline outline-[12px] outline-offset-[-6px] outline-blue-200"></div>
<div class="left-[23px] top-[31px] absolute justify-start text-Colors-Blue text-base font-bold font-['Noto_Sans_KR']">@Attendance.DailyPercentage%</div>
</div>
<div class="min-w-24 flex justify-start items-center gap-2.5">
<div class="self-stretch min-w-24 py-1 flex justify-start items-center gap-0.5">
<div class="w-6 h-4 text-center justify-center text-Colors-Blue text-xl font-medium font-['Noto_Sans_KR'] leading-loose">@Attendance.DailyAttendedHours</div>
<div class="w-2 h-4 text-center justify-center text-neutral-600 text-xs font-medium font-['Noto_Sans_KR'] leading-none">/</div>
<div class="w-6 h-4 text-center justify-center text-neutral-600 text-xl font-normal font-['Noto_Sans_KR'] leading-loose">@Attendance.DailyTotalHours</div>
<div class="text-center justify-center text-neutral-600 text-base font-normal font-['Noto_Sans_KR'] leading-normal">시간</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,35 @@
using Microsoft.AspNetCore.Components;
namespace Front.Program.Views.Academy.Main;
public partial class CheckAttendance : ComponentBase
{
private AttendanceInfo Attendance { get; set; } = new();
protected override void OnInitialized()
{
Attendance = new AttendanceInfo
{
Month = 7,
MonthlyPercentage = 10,
MonthlyAttendedDays = 11,
MonthlyTotalDays = 31,
DailyPercentage = 75,
DailyAttendedHours = 3,
DailyTotalHours = 4
};
}
}
public class AttendanceInfo
{
public int Month { get; set; }
public int MonthlyPercentage { get; set; }
public int MonthlyAttendedDays { get; set; }
public int MonthlyTotalDays { get; set; }
public int DailyPercentage { get; set; }
public int DailyAttendedHours { get; set; }
public int DailyTotalHours { get; set; }
}

View File

@ -0,0 +1,5 @@
<h3>ClassInfo</h3>
@code {
}

View File

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

View File

@ -0,0 +1,5 @@
<h3>RecentCalendar</h3>
@code {
}

View File

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

View File

@ -0,0 +1,5 @@
<h3>RecentNews</h3>
@code {
}

View File

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

View File

@ -0,0 +1,2 @@
<h3>ScheduledClass</h3>

View File

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

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">
@* 클릭시 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>

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

@ -13,10 +13,7 @@
- HTML & CSS
### IDE
- JetBrains Rider
<<<<<<< HEAD
=======
---
>>>>>>> seonkyu.kim-main

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
@using Front.Program.Views.Academy.Main

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,18 @@
"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/seankim/.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",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.21",
"autoprefixer": "^10.4.14"
"tailwindcss": "^3.4.1"
},
"dependencies": {
"tailwind-scrollbar-hide": "^4.0.0"
}
}

View File

@ -47,13 +47,15 @@ module.exports = {
detail: '#545454',
disabled: '#8E8E8E',
white: '#FFFFFF',
black: '#000000',
back: '#D8D8D8',
border: '#C6C6C6'
}
},
fontFamily: {},
fontFamily: {
sans: ['"Noto Sans KR"', 'ui-sans-serif', 'system-ui', '-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', 'Roboto', '"Helvetica Neue"', 'Arial', 'sans-serif'],
},
screens: {
'xs': '400px',
'xs': '480px',
'sm': '640px',
'md': '768px',
'lg': '1024px',
@ -62,5 +64,7 @@ module.exports = {
}
},
},
plugins: [],
plugins: [
require('tailwind-scrollbar-hide')
],
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 749 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 740 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 939 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

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: 468 B

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');
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,33 @@
window.navigationHelper = {
preventBackNavigation: function() {
// 현재 상태를 히스토리에 추가
history.pushState(null, null, location.href);
// popstate 이벤트 리스너 추가
window.addEventListener('popstate', function(event) {
// 뒤로가기 시도 시 다시 현재 페이지로 이동
history.pushState(null, null, location.href);
});
},
allowBackNavigation: function() {
// popstate 이벤트 리스너 제거
window.removeEventListener('popstate', this.popstateHandler);
},
// 확인 대화상자와 함께 뒤로가기 막기
preventBackNavigationWithConfirm: function(message) {
history.pushState(null, null, location.href);
window.addEventListener('popstate', function(event) {
if (confirm(message || '정말로 페이지를 떠나시겠습니까?')) {
// 사용자가 확인을 누르면 뒤로가기 허용
history.back();
} else {
// 취소를 누르면 현재 페이지 유지
history.pushState(null, null, location.href);
}
});
}
};

View File

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