Compare commits

..

45 Commits
main ... debug

Author SHA1 Message Date
0b54ade1ea Merge pull request '[🐛] 헤더 키 이름 변경' (#22) from seonkyu.kim/AcaMate_Web:main into debug
All checks were successful
AcaMate_FO/pipeline/head This commit looks good
Reviewed-on: https://git.ipstein.myds.me/AcaMate/AcaMate_Web/pulls/22
2025-06-18 04:27:27 +00:00
e8580650cc [🐛] 헤더 키 이름 변경 2025-06-18 13:21:34 +09:00
ab1efceb62 Merge pull request 'main' (#21) from seonkyu.kim/AcaMate_Web:main into debug
All checks were successful
AcaMate_FO/pipeline/head This commit looks good
Reviewed-on: https://git.ipstein.myds.me/AcaMate/AcaMate_Web/pulls/21
2025-06-17 07:19:16 +00:00
3e31f9f46b [] am/Intro 화면 동작 구현
1. 로그인 연동
2. 화면 변환 연동
2.1. 아직 Academy 테이블 연동은 안되어있는 상황
2025-06-17 16:00:56 +09:00
2665dcbf64 [♻️] 로그인 동작 로직 개편
1. 로그인 동작을 위해서 viewmodel 로 관련 뷰에서 동작할 모든 로직을 viewmodel에서 관리
1.1. view 와 viewmodel의 관계는 1:N으로 동작하는것을 기반으로 두고 있음
2. API 접근하는 방식도 웹만의 접근 방법에서 수정
3. 로그인 동작 정보 받는 로직 수정
2025-06-16 17:47:35 +09:00
c371700e78 [] 학원용 입장페이지 작성
1. /am/...  도메인 생성
2. /am/intro 페이지 생성
3. 메인 레이아웃 상황 따라 변경
4. 로그인 정보 받아오는 로직 변경 중
4.1. 유저 정보에 대한 JSON은 받아왔으나 이를 저장하는 로직 구현 중
4.2. 로그인 정보를 받아오는 로직을 어디서 구현해야 할 지 고민 하는 중
2025-06-13 17:53:58 +09:00
05e8fcd0b0 [] 로그인 후 레이아웃 변경 2025-06-12 17:55:50 +09:00
9621169d57 [] 로그인 동작 및 Repository 동작
1. 쿠키만 사용하던 저장 동작을 다른 저장소도 사용하게 변경
1.1. 쿠키 대신 세션 레포지토리를 기본적으로 사용하게 Service 코드 구현
2. 로그인 되었을 경우 화면 표기 변경
2.1. 시작하기 버튼 hidden 처리
2.2. 사용자 이름 불러오기
2.3. 로그인 동작 관련 변수 스토리지 저장
2.4. 서버에서 직접적인 크라이언트 쿠키 저장이 아닌 서버는 뒤의 값으로 간섭하게 변경
2025-06-11 15:12:22 +09:00
3ffec93958 [] 로그인 및 화면 구조 변경
1. 회원가입 후 자동 로그인
2. 로그인 후 페이지 처리
3. 로딩 인디케이터 동작 구조 변경
2025-06-09 17:45:53 +09:00
6bb994d2dc Merge pull request 'main' (#20) from seonkyu.kim/AcaMate_Web:main into debug
All checks were successful
AcaMate_FO/pipeline/head This commit looks good
Reviewed-on: https://git.ipstein.myds.me/AcaMate/AcaMate_Web/pulls/20
2025-06-05 08:08:20 +00: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
06e77505cc Merge pull request '[🐛] url 변경' (#19) from seonkyu.kim/AcaMate_Web:main into debug
All checks were successful
AcaMate_FO/pipeline/head This commit looks good
Reviewed-on: https://git.ipstein.myds.me/AcaMate/AcaMate_Web/pulls/19
2025-05-28 06:17:32 +00:00
a03aabc8fb [🐛] url 변경
1. API 가 바라보는 url 변경
2025-05-28 15:17:13 +09:00
4e18ec1cf3 Merge pull request '[] 페이지 개발' (#18) from seonkyu.kim/AcaMate_Web:main into debug
All checks were successful
AcaMate_FO/pipeline/head This commit looks good
Reviewed-on: https://git.ipstein.myds.me/AcaMate/AcaMate_Web/pulls/18
2025-05-28 06:07:33 +00:00
f4d0138fec [] 페이지 개발
1. about 페이지 개발
2. 반응형 적용
3. 버튼 동작 적용
2025-05-28 15:05:52 +09:00
15c9f604e9 Merge pull request '[♻️] 내부 문구 수정' (#17) from seonkyu.kim/AcaMate_Web:main into debug
All checks were successful
AcaMate_FO/pipeline/head This commit looks good
Reviewed-on: https://git.ipstein.myds.me/AcaMate/AcaMate_Web/pulls/17
2025-05-27 08:49:43 +00:00
070e15ae70 [♻️] 내부 문구 수정
1. 내부 설명 문구 수정
2025-05-27 17:49:14 +09:00
78471e2e16 Merge pull request '[🐛] 화면 반응형' (#16) from seonkyu.kim/AcaMate_Web:main into debug
All checks were successful
AcaMate_FO/pipeline/head This commit looks good
Reviewed-on: https://git.ipstein.myds.me/AcaMate/AcaMate_Web/pulls/16
2025-05-27 07:35:19 +00:00
9e2151cbd7 [🐛] 화면 반응형
1. 화면 반응형 동작 가능하게 수정
2025-05-27 16:34:56 +09:00
8875930067 Merge pull request 'main' (#15) from seonkyu.kim/AcaMate_Web:main into debug
All checks were successful
AcaMate_FO/pipeline/head This commit looks good
Reviewed-on: https://git.ipstein.myds.me/AcaMate/AcaMate_Web/pulls/15
2025-05-27 06:41:20 +00: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
e61ae09f67 Merge pull request '[♻️] Jenkins 동작 로직 수정 7' (#14) from seonkyu.kim/AcaMate_Web:main into debug
All checks were successful
AcaMate_FO/pipeline/head This commit looks good
Reviewed-on: https://git.ipstein.myds.me/AcaMate/AcaMate_Web/pulls/14
2025-05-22 00:26:39 +00:00
1f3b6b3217 [♻️] Jenkins 동작 로직 수정 7 2025-05-22 09:26:03 +09:00
631300b9c2 Merge pull request 'main' (#13) from seonkyu.kim/AcaMate_Web:main into debug
Some checks failed
AcaMate_FO/pipeline/head There was a failure building this commit
Reviewed-on: https://git.ipstein.myds.me/AcaMate/AcaMate_Web/pulls/13
2025-05-21 09:04:13 +00: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
1f84af9fb2 Merge pull request '[♻️] Jenkins 동작 로직 수정 4' (#11) from seonkyu.kim/AcaMate_Web:main into debug
Some checks failed
AcaMate_FO/pipeline/head There was a failure building this commit
Reviewed-on: https://git.ipstein.myds.me/AcaMate/AcaMate_Web/pulls/11
2025-05-21 08:51:44 +00:00
7a34c9f95c [♻️] Jenkins 동작 로직 수정 4 2025-05-21 17:50:53 +09:00
d1151eb3d8 Merge pull request '[♻️] Jenkins 동작 로직 수정 3' (#10) from seonkyu.kim/AcaMate_Web:main into debug
Some checks failed
AcaMate_FO/pipeline/head There was a failure building this commit
Reviewed-on: https://git.ipstein.myds.me/AcaMate/AcaMate_Web/pulls/10
2025-05-21 08:30:27 +00:00
1d2f8db794 [♻️] Jenkins 동작 로직 수정 3 2025-05-21 17:29:49 +09:00
d0ce42b245 Merge pull request '[♻️] Jenkins 동작 로직 수정 2' (#9) from seonkyu.kim/AcaMate_Web:main into debug
Some checks failed
AcaMate_FO/pipeline/head There was a failure building this commit
Reviewed-on: https://git.ipstein.myds.me/AcaMate/AcaMate_Web/pulls/9
2025-05-21 08:25:01 +00:00
9447bc3053 [♻️] Jenkins 동작 로직 수정 2 2025-05-21 17:24:32 +09:00
96378dccb8 Merge pull request '[♻️] 젠킨스 동작 로직 수정' (#8) from seonkyu.kim/AcaMate_Web:main into debug
Some checks failed
AcaMate_FO/pipeline/head There was a failure building this commit
Reviewed-on: https://git.ipstein.myds.me/AcaMate/AcaMate_Web/pulls/8
2025-05-21 08:15:30 +00:00
fc5e072697 [♻️] 젠킨스 동작 로직 수정 2025-05-21 17:14:01 +09:00
3fea9254d2 Merge pull request '[📝] 이상한 코드 삭제' (#7) from seonkyu.kim/AcaMate_Web:main into debug
All checks were successful
AcaMate_FO/pipeline/head This commit looks good
Reviewed-on: https://git.ipstein.myds.me/AcaMate/AcaMate_Web/pulls/7
2025-05-20 05:23:02 +00:00
96b4937653 [📝] 이상한 코드 삭제 2025-05-20 14:22:26 +09:00
55 changed files with 2228 additions and 1072 deletions

View File

@ -27,9 +27,11 @@ AppAssembly: 현재 애플리케이션의 어셈블리를 지정
role="alert": 스크린 리더와 같은 보조 기술에 의해 읽히는 경고 메시지
-->
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
@* <PageTitle>Not found</PageTitle> *@
@* <LayoutView Layout="@typeof(MainLayout)"> *@
@* <p role="alert">Sorry, there's nothing at this address.</p> *@
@* *@
@* </LayoutView> *@
<RedirectPage />
</NotFound>
</Router>

38
App.razor.cs Normal file
View File

@ -0,0 +1,38 @@
using Microsoft.AspNetCore.Components;
using Front.Program.Models;
using Front.Program.Services;
namespace Front;
public partial class App : ComponentBase
{
[Inject] private APIService API { get; set; } = default!;
[Inject] private StorageService StorageService { get; set; } = default!;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var headerValue = await StorageService.GetItemAsync("Web-AM-Connect-Key");
// 값 없으면 API 호출
if (string.IsNullOrEmpty(headerValue))
{
var response = await API.GetJsonAsync<APIHeader, AppHeader>(
"/api/v1/in/app",
new AppHeader()
{
type = "W",
specific = "Web_Connect",
project = "AcaMate"
});
if (!string.IsNullOrEmpty(response.data.header))
{
await StorageService.SetItemAsync("Web-AM-Connect-Key", response.data.header);
}
}
}
}
}

View File

@ -11,6 +11,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.8"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.8" PrivateAssets="all"/>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.10" />
</ItemGroup>
<ItemGroup>
@ -26,15 +27,10 @@
</ItemGroup>
<ItemGroup>
<Folder Include="Program\Models\" />
<Folder Include="Program\ViewModels\" />
<Folder Include="Program\Views\Academy\" />
<Folder Include="Program\Views\Project\FooterLink\" />
<Folder Include="wwwroot\Resources\" />
</ItemGroup>
<ItemGroup>
<Content Include="wwwroot\Resources\Images\.DS_Store" />
</ItemGroup>
</Project>

16
Jenkinsfile vendored
View File

@ -5,8 +5,22 @@ pipeline {
DOCKER_DEBUG_CONTAINER = 'acamate-front-build-debug'
APP_VOLUME = '/src'
}
stages {
stage('Clear Repository') {
steps {
script {
sh """
echo 'Clearing Front directory'
docker run --rm -v /volume1/AcaMate/PROJECT/Application/Front:/front alpine \
sh -c "find /front -mindepth 1 -maxdepth 1 \\
! -name 'privacy' \\
! -name 'publish' \\
-exec rm -rf {} +"
echo 'Clean complete'
"""
}
}
}
stage('Clone Repository') {
steps {
git url: 'https://git.ipstein.myds.me/AcaMate/AcaMate_Web.git', branch: env.GIT_BRANCH

View File

@ -4,22 +4,41 @@ using Microsoft.Extensions.Configuration;
using Front;
using Front.Program.Services;
using Front.Program.ViewModels;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
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.json", optional: false);
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 = new Uri("http://0.0.0.0:5144")
BaseAddress = new Uri("https://devacamate.ipstein.myds.me")
// BaseAddress = builder.HostEnvironment.IsDevelopment()
// ? new Uri("https://devacamate.ipstein.myds.me")
// : new Uri("https://acamate.ipstein.myds.me")
};
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.AddScoped<UserStateService>();
await builder.Build().RunAsync();

View File

@ -1,45 +1,47 @@
@* @inherits LayoutComponentBase *@
@* *@
@* *@
@* <div class="min-h-screen bg-gray-50 text-gray-900"> *@
@* <TopBanner /> *@
@* <TopNav /> *@
@* *@
@* <div class="flex flex-1"> *@
@* <SideNav /> *@
@* *@
@* <main class="flex-1 p-6"> *@
@* $1$ <!-- Body는 URL 뒤에 입력할 페이지에 따라서 그거에 맞는 @page를 찾아서 열어준다. --> #1# *@
@* @Body *@
@* </main> *@
@* </div> *@
@* *@
@* <FloatingButton /> *@
@* <BottomNav /> *@
@* <Footer/> *@
@* </div> *@
@inherits LayoutComponentBase
@inherits LayoutComponentBase
@implements IDisposable
<div class="min-h-screen flex flex-col bg-gray-50 text-gray-900">
<!-- Top 영역 -->
<TopBanner />
<TopNav />
@* <TopBanner /> *@
<!-- 본문 영역 -->
@if(isAcademy)
{
<div class="flex flex-1 flex-col md:flex-row">
@* <!-- 사이드 메뉴 --> *@
@* <div class="hidden md:block md:w-64 bg-white shadow"> *@
@* <SideNav /> *@
@* </div> *@
<!-- 본문 컨텐츠 -->
<main class="flex-1 p-4 sm:p-6 md:p-8">
@if (!isIntro && UserStateService.isLogin)
{
<div class="hidden md:block w-64 bg-white shadow-lg border-r border-gray-200 fixed top-0 bottom-0">
<LeftSideAcademy/>
</div>
<div class="flex flex-1 flex-col md:ml-64">
<div class="fixed top-0 right-0 left-64 z-10">
<TopNavAcademy/>
</div>
<div class="flex-1 mt-16">
@Body
</div>
</div>
}
else
{
<main class="flex-1 w-full w-max-960 mx-auto">
@Body
</main>
}
</div>
}
else
{
@if (!isHidePrjTop)
{
<TopProjectNav />
}
<!-- 본문 컨텐츠 -->
<main class="flex-1 w-full w-max-960 mx-auto">
@Body
</main>
}
<!-- 플로팅 버튼 -->
<FloatingButton />
@ -47,4 +49,13 @@
<!-- 하단 메뉴 -->
<BottomNav />
<Footer />
@if (LoadingService.IsLoading)
{
<div class="fixed inset-0 bg-black/80 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,44 @@
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!;
// 경로의 시작 부분
// protected bool isHidePrjTop => Navigation.ToBaseRelativePath(Navigation.Uri).StartsWith("auth", StringComparison.OrdinalIgnoreCase);
// 경로의 끝 부분
protected bool isHidePrjTop => Navigation.ToBaseRelativePath(Navigation.Uri).EndsWith("auth", StringComparison.OrdinalIgnoreCase);
protected bool isAcademy => Navigation.ToBaseRelativePath(Navigation.Uri).StartsWith("am", StringComparison.OrdinalIgnoreCase);
// 경로 일치
protected bool isIntro => Navigation.ToBaseRelativePath(Navigation.Uri).Equals("am/intro", StringComparison.OrdinalIgnoreCase);
protected override void OnInitialized()
{
LoadingService.OnChange += StateHasChanged;
Navigation.LocationChanged += HandleLocationChanged;
HandleLocationChanged(this, new LocationChangedEventArgs(Navigation.Uri, false));
}
// 페이지의 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 business_name { get; set; } = string.Empty;
}

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,43 @@
using System.Net.Http.Json;
using Front.Program.Models;
namespace Front.Program.Services;
public class APIService
{
private readonly HttpClient _http;
public APIService(HttpClient http)
{
_http = http;
}
private string ChangeToString<T>(T data)
{
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;
}
}
/*
dynamic :
Register.razor.cs에서 JsonElement를 . :
JSON (TryGetProperty )
*/

View File

@ -0,0 +1,43 @@
// using Front.Program.Views.Project.Common;
using Front.Program.Models;
using Microsoft.AspNetCore.Components;
namespace Front.Program.Services;
// 뷰를 참조 안하고도 로딩 상태를 알 수 있게 바꾸기
public class LoadingService
{
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();
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,167 @@
using System.Text.Json;
using Front.Program.Services;
using Front.Program.Models;
using Microsoft.JSInterop;
namespace Front.Program.ViewModels;
public class UserStateService(StorageService _storageService,SecureService _secureService, IJSRuntime _js)
{
public UserData UserData { get; set; } = new UserData();
public bool isFirstCheck { get; set; } = false;
public bool isLogin { get; set; } = false;
public async Task<(bool success, UserData? userData)> GetUserDataFromStorageAsync()
{
try
{
var encUserData = await _storageService.GetItemAsync("USER_DATA");
if (!string.IsNullOrEmpty(encUserData))
{
var decUserData = await _secureService.DecryptAsync(encUserData);
var userData = JsonSerializer.Deserialize<UserData>(decUserData,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
Console.WriteLine($"UserData: {userData.Name}, {userData.Type}");
return (true, userData);
}
else
{
return (false, null);
}
}
catch (Exception ex)
{
Console.WriteLine($"Error [GetUserDataFormAsync] : {ex.Message}");
return (false, null);
}
}
public async Task<(bool success, UserData? userData)> GetUserDataFromServerAsync()
{
var headerValue = await _storageService.GetItemAsync("Web-AM-Connect-Key");
if (string.IsNullOrEmpty(headerValue)) return (false, null);
var args = new {
url = "/api/v1/in/user",
method = "GET",
headerKey = "Web-AM-Connect-Key",
headerValue = headerValue,
token = "VO00"
};
var response = await _js.InvokeAsync<JsonElement>(
"fetchWithHeaderAndReturnUrl",
args
);
// var response = await _js.InvokeAsync<JsonElement>("fetchWithHeaderAndReturnUrl",
// "/api/v1/in/user/",
// "GET",
// "Web_AM_Connect_Key",
// headerValue);
Console.WriteLine($"JSON 응답: {response.ToString()}");
if (response.TryGetProperty("data", out var dataElement))
{
try
{
// 전체 데이터 암호화 저장
var userDataJson = dataElement.ToString();
var userData = JsonSerializer.Deserialize<UserData>(userDataJson,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (userData != null && !string.IsNullOrEmpty(userData.Name))
{
var encryptedData = await _secureService.EncryptAsync(userDataJson);
await _storageService.SetItemAsync("USER_DATA", encryptedData);
return (true, userData);
}
else
{
Console.WriteLine("사용자 데이터에 필수 정보가 없습니다");
}
}
catch (Exception ex)
{
Console.WriteLine($"사용자 데이터 처리 중 오류: {ex.Message}");
}
}
Console.WriteLine("사용자 데이터를 찾을 수 없습니다");
return (false, null);
}
public async Task<bool> GetUserDataAsync()
{
try
{
Console.WriteLine("GetUserDataAsync 호출됨");
// 로그인 상태가 아니라면 애초에 할 필요 없음
if (await _storageService.GetItemAsync("IsLogin") != "true")
{
isLogin = false;
return false;
}
var userDataForm = await GetUserDataFromStorageAsync();
if (userDataForm.success && userDataForm.userData != null)
{
// 사용자 데이터가 성공적으로 로드되었을 때의 로직
UserData = userDataForm.userData;
isLogin = true;
return true;
}
else
{
var userDataFromServer = await GetUserDataFromServerAsync();
if (userDataFromServer.success && userDataFromServer.userData != null)
{
// 서버에서 사용자 데이터를 성공적으로 로드했을 때의 로직
UserData = userDataFromServer.userData;
isLogin = true;
return true;
}
else
{
// 사용자 데이터를 로드하지 못했을 때의 로직
Console.WriteLine("사용자 데이터를 로드하지 못했습니다.");
isLogin = false;
return false;
}
}
}
finally
{
if (!isFirstCheck) isFirstCheck = true;
}
}
public async Task ClearUserData()
{
try
{
await _storageService.RemoveItemAsync("USER_DATA");
await _storageService.RemoveItemAsync("IsLogin");
Console.WriteLine("사용자 데이터 삭제 성공");
isLogin = false;
// return true;
}
catch (Exception ex)
{
Console.WriteLine($"사용자 데이터 삭제 중 오류: {ex.Message}");
// return false;
}
}
}

View File

@ -0,0 +1,68 @@
@page "/am/intro"
<div class="relative flex size-full min-h-screen flex-col bg-normal-normal group/design-root overflow-x-hidden" style='font-family: "Public Sans", "Noto Sans", sans-serif;'>
<div class="layout-container flex h-full grow flex-col">
<div class="px-4 sm:px-6 md:px-10 lg:px-24 xl:px-40 flex flex-1 justify-center py-5">
<div class="layout-content-container flex flex-col max-w-[960px] flex-1">
<div class="container pt-24">
<div class="px-4 md:px-8 py-4">
<div class="w-full bg-center bg-no-repeat bg-contain md:bg-contain flex flex-col justify-center items-center overflow-hidden bg-white/35 rounded-xl min-h-[218px] md:h-[300px]"
style="background-image: url('/Resources/Images/Logo/Crystal_Icon.png');">
</div>
</div>
</div>
@if (!UserStateService.isLogin)
{
<h3 class="text-[#111518] tracking-light text-2xl font-bold leading-tight px-4 text-center pb-2 pt-3">
학원을 위한 통합 플랫폼
</h3>
<div class="flex px-4 py-2 justify-center">
<button class="bg-second-normal hover:bg-second-hover transition-all duration-200 shadow-md text-white font-bold py-3 px-6 rounded-xl"
@onclick="OnClickLogin">
<span class="truncate">시작하기</span>
</button>
</div>
}
else
{
<div class="flex flex-col px-4 py-2 gap-2">
<h3 class="text-[#111518] tracking-light text-xl font-bold leading-tight text-center py-12">
@UserStateService.UserData.Name 님, 안녕하세요!<br />
학원을 선택해주세요.<br />
</h3>
<div class="max-h-[180px] overflow-y-auto rounded-xl bg-second-normal/10 border-2 border-text-detail">
@foreach (var academy in academyItems)
{
<a href="/am/@academy.bid"
class="block w-full px-4 py-3 hover:bg-second-dark border-b border-gray-200 last:border-b-0 group">
<div class="flex items-center justify-between">
<span class="text-text-black group-hover:text-text-white font-medium">@academy.business_name</span>
<svg class="w-5 h-5 text-text-detail group-hover:text-text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</div>
</a>
}
</div>
</div>
}
@* <div class="w-full" style="height: 100px;"></div> *@
<div class="border-b border-text-disabled my-4 pt-12"></div>
<div class="flex flex-col md:flex-row justify-center items-center gap-4 px-4">
<a href="/terms" class="text-text-detail text-sm font-normal leading-normal hover:text-text-black hover:font-bold">이용약관</a>
<span class="hidden md:inline text-text-black">·</span>
<a href="/privacy" class="text-text-detail text-sm font-normal leading-normal hover:text-text-black hover:font-bold">개인정보처리방침</a>
<span class="hidden md:inline text-text-black">·</span>
<a href="/contact" class="text-text-detail text-sm font-normal leading-normal hover:text-text-black hover:font-bold">문의하기</a>
</div>
<p class="text-text-detail text-sm font-normal leading-normal py-4 px-4 text-center">
© 2024 AcaMate. All rights reserved.
</p>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,88 @@
using Front.Program.Models;
using Front.Program.Services;
using Front.Program.ViewModels;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
namespace Front.Program.Views.Academy;
public partial class AcademyIntro : ComponentBase, IDisposable
{
[Inject] NavigationManager Navigation { get; set; } = default!;
[Inject] StorageService StorageService { get; set; } = default!;
[Inject] QueryParamService QueryParamService { get; set; } = default!;
[Inject] UserStateService UserStateService { get; set; } = default!;
private bool _isProcessing = false;
protected Models.SimpleAcademy[] academyItems = Array.Empty<Models.SimpleAcademy>();
protected override async void OnInitialized()
{
Navigation.LocationChanged += HandleLocationChanged;
HandleLocationChanged(this, new LocationChangedEventArgs(Navigation.Uri, false));
if (!UserStateService.isFirstCheck) await UserStateService.GetUserDataAsync();
academyItems = new[]
{
new SimpleAcademy{ bid = "AA0000", business_name = "테스트 학원1"},
new SimpleAcademy{ bid = "AA0001", business_name = "테스트 학원2"},
new SimpleAcademy{ bid = "AA0002", business_name = "테스트 학원3"},
new SimpleAcademy{ bid = "AA0003", business_name = "테스트 학원4"},
new SimpleAcademy{ bid = "AA0004", business_name = "테스트 학원5"},
new SimpleAcademy{ bid = "AA0005", business_name = "테스트 학원6"},
new SimpleAcademy{ bid = "AA0006", business_name = "테스트 학원7"},
};
await InvokeAsync(StateHasChanged);
}
public void Dispose()
{
Navigation.LocationChanged -= HandleLocationChanged;
}
private async void HandleLocationChanged(object? sender, LocationChangedEventArgs e)
{
try
{
// 다중 실행 방지
if (_isProcessing) return;
_isProcessing = true;
var uri = Navigation.ToAbsoluteUri(Navigation.Uri);
Console.WriteLine($"리다이렉트된 URI: {uri}");
// 쿼리 파라미터가 있는 경우에만 처리
if (!string.IsNullOrEmpty(uri.Query))
{
var queryParam = QueryParamService.ParseQueryParam(uri);
await QueryParamService.AuthCheck(queryParam, StorageService);
// 유저 정보 확인하는거 (로그인 했으니 값 가져와야지)
await UserStateService.GetUserDataAsync();
// 쿼리 파라미터를 제거한 기본 URI로 리다이렉트
var baseUri = uri.GetLeftPart(UriPartial.Path);
Console.WriteLine($"리다이렉트할 URI: {baseUri}");
await InvokeAsync(StateHasChanged); // StateHasChanged를 호출하여 UI 업데이트
Navigation.NavigateTo(baseUri, forceLoad: false);
}
}
catch (Exception ex)
{
Console.WriteLine($"Error in HandleLocationChanged: {ex.Message}");
}
finally
{
_isProcessing = false;
}
}
protected void OnClickLogin()
{
Navigation.NavigateTo("/am/auth");
}
}

View File

@ -0,0 +1,70 @@
<div class="h-[72px] p-4 border-b border-gray-200 flex items-center">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-gray-200 overflow-hidden">
<img src="Resources/Images/Logo/Crystal_Icon.png" alt="프로필" class="w-full h-full object-cover" />
</div>
<div>
<!-- 회원 이름이 오는 곳 -->
<div class="text-gray-900 text-base font-medium">AcaMate</div>
<!-- 회원 유형이 올 곳 -->
<p class="text-gray-600 text-sm">Parent</p>
</div>
</div>
</div>
<!-- 메뉴 섹션 -->
<nav class="flex-1 p-4 space-y-2 ">
<a href="/academy/children" class="flex items-center gap-3 px-3 py-2 rounded-lg bg-gray-100">
<div class="text-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256">
<path d="M164.47,195.63a8,8,0,0,1-6.7,12.37H10.23a8,8,0,0,1-6.7-12.37,95.83,95.83,0,0,1,47.22-37.71,60,60,0,1,1,66.5,0A95.83,95.83,0,0,1,164.47,195.63Z"></path>
</svg>
</div>
<span class="text-gray-700 text-sm font-medium">My Children</span>
</a>
<a href="/academy/attendance" class="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-gray-100">
<div class="text-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256">
<path d="M208,32H184V24a8,8,0,0,0-16,0v8H88V24a8,8,0,0,0-16,0v8H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32Z"></path>
</svg>
</div>
<span class="text-gray-700 text-sm font-medium">Attendance</span>
</a>
<a href="/academy/announcements" class="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-gray-100">
<div class="text-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256">
<path d="M240,120a48.05,48.05,0,0,0-48-48H152.2c-2.91-.17-53.62-3.74-101.91-44.24A16,16,0,0,0,24,40V200a16,16,0,0,0,26.29,12.25c37.77-31.68,77-40.76,93.71-43.3v31.72A16,16,0,0,0,151.12,214l11,7.33A16,16,0,0,0,186.5,212l11.77-44.36A48.07,48.07,0,0,0,240,120Z"></path>
</svg>
</div>
<span class="text-gray-700 text-sm font-medium">Announcements</span>
</a>
<a href="/academy/messages" class="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-gray-100">
<div class="text-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256">
<path d="M140,128a12,12,0,1,1-12-12A12,12,0,0,1,140,128ZM84,116a12,12,0,1,0,12,12A12,12,0,0,0,84,116Zm88,0a12,12,0,1,0,12,12A12,12,0,0,0,172,116Z"></path>
</svg>
</div>
<span class="text-gray-700 text-sm font-medium">Messages</span>
</a>
<a href="/academy/bus-tracking" class="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-gray-100">
<div class="text-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256">
<path d="M184,32H72A32,32,0,0,0,40,64V208a16,16,0,0,0,16,16H80a16,16,0,0,0,16-16V192h64v16a16,16,0,0,0,16,16h24a16,16,0,0,0,16-16V64A32,32,0,0,0,184,32Z"></path>
</svg>
</div>
<span class="text-gray-700 text-sm font-medium">Bus Tracking</span>
</a>
<a href="/academy/settings" class="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-gray-100">
<div class="text-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256">
<path d="M128,80a48,48,0,1,0,48,48A48.05,48.05,0,0,0,128,80Zm0,80a32,32,0,1,1,32-32A32,32,0,0,1,128,160Z"></path>
</svg>
</div>
<span class="text-gray-700 text-sm font-medium">Settings</span>
</a>
</nav>

View File

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

View File

@ -0,0 +1,36 @@
<div class="flex items-center justify-between whitespace-nowrap border-b border-solid border-b-[#f0f2f5] h-[72px] bg-white">
<div id="AcademyDrop" class="relative flex items-center gap-4 text-[#111418] ml-4">
<button class="flex items-center gap-2 hover:text-blue-600" @onclick="ToggleAcademyDropdown">
<h2 class="hidden md:block text-text-title text-lg font-bold leading-tight tracking-[-0.015em]">AcaMate</h2>
<svg class="w-4 h-4 transition-transform duration-200 @(isAcademyDropdownOpen ? "rotate-180" : "")"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
@if (isAcademyDropdownOpen)
{
<div class="absolute top-full left-0 mt-2 w-64 bg-white rounded-lg shadow-lg py-2 z-50">
<div class="px-4 py-2 border-b border-gray-200">
<h3 class="font-semibold text-gray-900">학원 정보</h3>
</div>
<div class="max-h-60 overflow-y-auto">
@foreach (var academy in academyItems)
{
<a href="/am/@academy.bid" class="block px-4 py-2 text-text-title hover:bg-gray-100">
@academy.business_name
</a>
}
</div>
</div>
}
</div>
<div class="hidden md:flex flex-1 justify-end gap-8">
<div class="flex flex-1 justify-end gap-8">
<div class="flex items-center gap-9">
<a class="text-text-title font-medium leading-normal hover:text-blue-600" href="/about">About</a>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,27 @@
using Front.Program.ViewModels;
using Front.Program.Services;
using Microsoft.AspNetCore.Components;
using System.Net.Http.Json;
using System.Runtime.InteropServices.JavaScript;
using System.Text.Json;
using Front.Program.Models;
namespace Front.Program.Views.Academy;
public partial class TopNavAcademy : ComponentBase
{
[Inject] UserStateService UserStateService { get; set; } = default!;
protected bool isOpen = false;
protected bool isAcademyDropdownOpen = false;
protected Models.SimpleAcademy[] academyItems = Array.Empty<Models.SimpleAcademy>();
protected override async Task OnInitializedAsync()
{
Console.WriteLine("TOPNAV_OnInitializedAsync");
}
protected void ToggleAcademyDropdown() {
isAcademyDropdownOpen = !isAcademyDropdownOpen;
}
}

View File

@ -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);
Console.WriteLine($"리다이렉트된 URI: {uri}");
// 쿼리 파라미터가 있는 경우에만 처리
if (!string.IsNullOrEmpty(uri.Query))
{
var queryParam = QueryParamService.ParseQueryParam(uri);
await QueryParamService.AuthCheck(queryParam, StorageService);
// 유저 정보 확인하는거 (로그인 했으니 값 가져와야지)
await UserStateService.GetUserDataAsync();
// 쿼리 파라미터를 제거한 기본 URI로 리다이렉트
var baseUri = uri.GetLeftPart(UriPartial.Path);
Console.WriteLine($"리다이렉트할 URI: {baseUri}");
await InvokeAsync(StateHasChanged); // StateHasChanged를 호출하여 UI 업데이트
Navigation.NavigateTo(baseUri, forceLoad: false);
}
}
catch (Exception ex)
{
Console.WriteLine($"Error in HandleLocationChanged: {ex.Message}");
}
finally
{
_isProcessing = false;
}
}
}

View File

@ -1,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()
{
Console.WriteLine("TOPNAV_OnInitializedAsync");
if (!UserStateService.isFirstCheck) await UserStateService.GetUserDataAsync();
}
public void OnClickMenuDown()
{
isOpen = !isOpen;
}
public void OnClickRedirect()
{
if (isOpen) isOpen = !isOpen;
NavigationManager.NavigateTo("/about");
}
public void OnClickLogin()
{
if (isOpen) isOpen = !isOpen;
NavigationManager.NavigateTo("/auth");
}
public async Task OnClickLogout()
{
await UserStateService.ClearUserData();
//
// if (await UserViewModel.ClearUserData())
// {
// isLogin = false;
// }
}
}

View File

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

View File

@ -0,0 +1,57 @@
using System.Net.Http.Json;
using System.Text.Json;
using Front.Program.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
namespace Front.Program.Views.Project.ConnectUser;
public partial class Auth : ComponentBase, IDisposable
{
[Inject] NavigationManager NavigationManager { get; set; } = default!;
[Inject] LoadingService LoadingService { get; set; } = default!;
[Inject] HttpClient Http { get; set; } = default!;
[Inject] IJSRuntime JS { get; set; } = default!;
protected override void OnInitialized()
{
// LocationChanged 이벤트 구독
NavigationManager.LocationChanged += HandleLocationChanged;
}
private void HandleLocationChanged(object? sender, Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs e)
{
// 페이지 이동이 발생했을 때 로딩 상태 해제
Console.WriteLine($"페이지 이동 감지: {NavigationManager.Uri}");
LoadingService.HideLoading();
}
public void Dispose()
{
// 이벤트 구독 해제
NavigationManager.LocationChanged -= HandleLocationChanged;
}
public async Task KakaoLogin(string? path = null)
{
try
{
LoadingService.ShowLoading();
var url = $"/api/v1/out/user/kakao/auth?redirectPath={Uri.EscapeDataString(path ?? "/about")}";
var response = await Http.GetFromJsonAsync<JsonElement>(url);
var kakaoUrl = response.GetProperty("url").GetString();
if (!string.IsNullOrEmpty(kakaoUrl))
{
// JavaScript를 통해 페이지 이동
await JS.InvokeVoidAsync("eval", $"window.location.replace('{kakaoUrl}')");
}
}
catch (Exception ex)
{
Console.WriteLine($"카카오 로그인 오류: {ex.Message}");
LoadingService.HideLoading();
}
}
}

View File

@ -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>();
Console.WriteLine($"세션 응답: {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() ?? "";
Console.WriteLine($"서버 세션에서 가져온 SNS ID: {snsId}");
}
}
else
{
Console.WriteLine($"세션 데이터 가져오기 실패: {content}");
}
}
else
{
Console.WriteLine($"세션 API 호출 실패: {response.StatusCode}");
}
if (string.IsNullOrEmpty(snsId))
{
Console.WriteLine("SNS ID가 없습니다.");
await JS.InvokeVoidAsync("alert", "잘못된 접근입니다.");
NavigationManager.NavigateTo("/");
}
}
catch (Exception ex)
{
Console.WriteLine($"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");
Console.WriteLine($"쿠키에서 가져온 토큰: '{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);
Console.WriteLine($"요청 헤더: {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();
Console.WriteLine($"예외 발생: {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}";
Console.WriteLine($"입력된 전화번호: {fullPhone}");
}
else
{
Console.WriteLine("전화번호를 올바르게 입력해주세요.");
}
}
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)
{
Console.WriteLine($"달력 표시 오류: {ex.Message}");
}
}
private void OnBirthChanged(ChangeEventArgs e)
{
Console.WriteLine($"선택된 생일: {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;
Console.WriteLine($"SetAddress 호출됨: {roadAddress}, {jibunAddress}, {zonecode}");
StateHasChanged();
}
}

View File

@ -1,11 +0,0 @@
<div class="flex justify-end items-center p-4 bg-white shadow">
<a href="#about" class="mx-2 text-gray-700 hover:text-blue-600">About</a>
<a href="#join" class="mx-2 text-gray-700 hover:text-blue-600">Join</a>
<a href="#notice" class="mx-2 text-gray-700 hover:text-blue-600">Whats New</a>
<button class="ml-4 px-4 py-2 bg-red-600 text-white rounded hover:bg-blue-700"
@onclick="OnClickLogin">
Login
</button>
</div>

View File

@ -1,17 +0,0 @@
using Microsoft.AspNetCore.Components;
namespace Front.Program.Views.Project;
public partial class TopNav : ComponentBase
{
//로그인버튼을 누르면 페이지를 이동할거야
[Inject]
NavigationManager NavigationManager { get; set; } = default!;
public void OnClickLogin()
{
NavigationManager.NavigateTo("/auth");
// Console.WriteLine("Redirecting to redirect page");
}
}

View File

@ -5,11 +5,15 @@
@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

13
appsettings.json Normal file
View File

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

View File

@ -2,12 +2,21 @@
"name": "tailwind-blazor-template",
"version": "1.0.0",
"scripts": {
"watch:css": "tailwindcss -i ./wwwroot/css/app.css -o ./wwwroot/css/tailwind.css --watch",
"build:css": "tailwindcss -i ./wwwroot/css/app.css -o ./wwwroot/css/tailwind.css --minify",
"watch:css": "tailwindcss -i ./wwwroot/css/app.css -o ./wwwroot/css/tailwind.css --watch"
"build:publish": "rm -rf ./publish && /Users/tanine/.dotnet/dotnet publish -c Debug -o ./publish",
"build:test": "rm -rf ../AcaMate_API/publish/debug/ && /Users/tanine/.dotnet/dotnet publish -c Debug -o ../AcaMate_API/publish/debug",
"build:copy": "mkdir -p ../AcaMate_API/publish/debug/wwwroot && cp -r ./publish/wwwroot/* ../AcaMate_API/publish/debug/wwwroot"
},
"devDependencies": {
"tailwindcss": "^3.4.1",
"postcss": "^8.4.21",
"autoprefixer": "^10.4.14"
}
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@ -12,7 +12,7 @@
<body class="bg-gray-50 text-gray-900 font-sans">
<!-- 로딩 오버레이 -->
<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">
@ -32,8 +32,15 @@
<button class="ml-4 text-red-600 font-bold dismiss"></button>
</div>
<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 +48,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) {
console.error('API 호출 실패:', response.status, response.statusText);
return null;
}
if (!contentType || !contentType.includes('application/json')) {
const text = await response.text();
console.error('JSON이 아닌 응답:', text);
return null;
}
const data = await response.json();
console.log('API 응답 데이터:', data);
return data;
} catch (error) {
console.error('API 호출 중 오류 발생:', error);
return null;
}
};
window.loadConfig = async function(configFile) {
try {
console.log('설정 파일 로드 시도:', configFile);
const response = await fetch(configFile);
if (!response.ok) {
console.error('설정 파일 로드 실패:', response.status, response.statusText);
return null;
}
const config = await response.json();
console.log('설정 파일 로드 성공:', configFile);
return config;
} catch (error) {
console.error('설정 파일 로드 중 오류 발생:', error);
return null;
}
};
window.encryptText = async function(text, key, iv) {
try {
// XOR 암호화 구현
let result = '';
for (let i = 0; i < text.length; i++) {
const charCode = text.charCodeAt(i) ^ key.charCodeAt(i % key.length);
result += String.fromCharCode(charCode);
}
// UTF-8로 인코딩 후 Base64로 변환
const utf8Encoder = new TextEncoder();
const bytes = utf8Encoder.encode(result);
const base64 = btoa(String.fromCharCode.apply(null, bytes));
return base64;
} catch (error) {
console.error('암호화 중 오류 발생:', error);
throw error;
}
};
window.decryptText = async function(encryptedText, key, iv) {
try {
// Base64 디코딩 후 UTF-8 디코딩
const binary = atob(encryptedText);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
const utf8Decoder = new TextDecoder();
const text = utf8Decoder.decode(bytes);
// XOR 복호화
let result = '';
for (let i = 0; i < text.length; i++) {
const charCode = text.charCodeAt(i) ^ key.charCodeAt(i % key.length);
result += String.fromCharCode(charCode);
}
return result;
} catch (error) {
console.error('복호화 중 오류 발생:', error);
throw error;
}
};

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

View File

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