refactor: move buildable .NET source into src/, update CI/doc paths
TaxBaik CI/CD / build-and-deploy (push) Successful in 2m7s
TaxBaik CI/CD / build-and-deploy (push) Successful in 2m7s
Groups the repo root into src (buildable source), docs (already existed), and everything else (db/, scripts/, tests/, deploy/ - deployment/ops/test assets that aren't compiled, already organized as their own folders). CI now only needs src/ to build: dotnet restore/build/test/publish all point at src/TaxBaik.sln, src/TaxBaik.Web/, src/TaxBaik.Proxy/. - git mv every project (Domain, Infrastructure, Application, Application.Tests, Web, Web.Client, Proxy) and TaxBaik.sln into src/ as a unit, so relative ProjectReference/.sln paths stay valid unchanged. - .gitea/workflows/deploy.yml: 6 dotnet restore/clean/build/test/publish invocations now point at src/. db/migrations and scripts/ stay at root (deploy_gb.sh and browser-e2e.yml only touch published output and the deployed URL, not source paths - verified, no changes needed there). - scripts/validate_admin_render.sh: admin render-mode file paths now src/TaxBaik.Web.Client/... - scripts/validate_kst_timestamps.sh: dropped deploy.sh from its target list - that script was removed in the prior cleanup commit (dead, no CI workflow referenced it) but this validator still expected it to exist. - CLAUDE.md, docs/ENGINEERING_HARNESS.md, docs/ADMIN_PATTERN_CRITIQUE_WBS.md: updated project-structure diagram, dotnet run/build commands, and grep targets to the new src/ paths (also fixed a pre-existing stale path in ADMIN_PATTERN_CRITIQUE_WBS.md that still said TaxBaik.Web/Components/Admin from before that ever moved to TaxBaik.Web.Client). - Added a Repo Root harness rule + Architecture Guardrail entries: new files belong under src/docs/tests/scripts/db/deploy, not loose at root; temp work stays outside the repo (or under a gitignored .scratch/) and is never committed. Verified locally: dotnet build/test src/TaxBaik.sln (26/26 tests), and all three scripts/validate_*.sh pass against the new layout. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
@page "/portal/external-callback"
|
||||
@model TaxBaik.Web.Pages.Portal.ExternalCallbackModel
|
||||
@{
|
||||
ViewData["Title"] = "포털 인증 처리";
|
||||
}
|
||||
|
||||
<section class="container py-5">
|
||||
<div class="alert alert-info">인증을 처리하는 중입니다...</div>
|
||||
</section>
|
||||
@@ -0,0 +1,97 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using TaxBaik.Application.Services;
|
||||
using TaxBaik.Web.Services;
|
||||
|
||||
namespace TaxBaik.Web.Pages.Portal;
|
||||
|
||||
public class ExternalCallbackModel : PageModel
|
||||
{
|
||||
private readonly PortalUserService _portalUserService;
|
||||
private readonly ClientService _clientService;
|
||||
|
||||
public ExternalCallbackModel(PortalUserService portalUserService, ClientService clientService)
|
||||
{
|
||||
_portalUserService = portalUserService;
|
||||
_clientService = clientService;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGetAsync(string provider)
|
||||
{
|
||||
var external = await HttpContext.AuthenticateAsync(PortalOAuthDefaults.ExternalScheme);
|
||||
if (external?.Principal is null)
|
||||
return RedirectToPage("/Portal/Login");
|
||||
|
||||
var email = external.Principal.FindFirstValue(ClaimTypes.Email);
|
||||
var name = external.Principal.FindFirstValue(ClaimTypes.Name) ?? "고객";
|
||||
var providerId = external.Principal.FindFirstValue(ClaimTypes.NameIdentifier) ?? "";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(providerId))
|
||||
return RedirectToPage("/Portal/Login");
|
||||
|
||||
var existing = await _portalUserService.GetByProviderAsync(provider, providerId);
|
||||
if (existing is null && !string.IsNullOrWhiteSpace(email))
|
||||
{
|
||||
existing = await _portalUserService.GetByEmailAsync(email);
|
||||
if (existing is null)
|
||||
{
|
||||
int? clientId = null;
|
||||
var linkedClient = await _clientService.GetByEmailAsync(email);
|
||||
if (linkedClient is null && !string.IsNullOrWhiteSpace(external.Principal.FindFirstValue("phone")))
|
||||
linkedClient = await _clientService.GetByPhoneAsync(external.Principal.FindFirstValue("phone")!);
|
||||
if (linkedClient is not null)
|
||||
clientId = linkedClient.Id;
|
||||
|
||||
await _portalUserService.RegisterOAuthAsync(
|
||||
name,
|
||||
email,
|
||||
external.Principal.FindFirstValue("phone") ?? "",
|
||||
provider,
|
||||
providerId,
|
||||
clientId);
|
||||
existing = await _portalUserService.GetByEmailAsync(email);
|
||||
}
|
||||
else if (!string.Equals(existing.Provider, provider, StringComparison.OrdinalIgnoreCase) ||
|
||||
!string.Equals(existing.ProviderId, providerId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await _portalUserService.LinkOAuthAsync(existing, provider, providerId, name, email);
|
||||
}
|
||||
}
|
||||
|
||||
if (existing is not null && !existing.ClientId.HasValue && !string.IsNullOrWhiteSpace(email))
|
||||
{
|
||||
var linkedClient = await _clientService.GetByEmailAsync(email);
|
||||
if (linkedClient is null && !string.IsNullOrWhiteSpace(external.Principal.FindFirstValue("phone")))
|
||||
linkedClient = await _clientService.GetByPhoneAsync(external.Principal.FindFirstValue("phone")!);
|
||||
if (linkedClient is not null)
|
||||
{
|
||||
await _portalUserService.AttachClientAsync(existing, linkedClient.Id);
|
||||
existing.ClientId = linkedClient.Id;
|
||||
}
|
||||
}
|
||||
|
||||
if (existing is null)
|
||||
return RedirectToPage("/Portal/Login");
|
||||
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(ClaimTypes.NameIdentifier, existing.Id.ToString()),
|
||||
new(ClaimTypes.Name, existing.Name),
|
||||
new(ClaimTypes.Email, existing.Email),
|
||||
new("portal_user_id", existing.Id.ToString())
|
||||
};
|
||||
|
||||
if (existing.ClientId.HasValue)
|
||||
claims.Add(new("client_id", existing.ClientId.Value.ToString()));
|
||||
|
||||
await HttpContext.SignInAsync(
|
||||
PortalAuthDefaults.Scheme,
|
||||
new ClaimsPrincipal(new ClaimsIdentity(claims, PortalAuthDefaults.Scheme)),
|
||||
new AuthenticationProperties { IsPersistent = true });
|
||||
|
||||
await HttpContext.SignOutAsync(PortalOAuthDefaults.ExternalScheme);
|
||||
return RedirectToPage("/Portal/Index");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
@page "/portal"
|
||||
@model TaxBaik.Web.Pages.Portal.IndexModel
|
||||
@{
|
||||
ViewData["Title"] = "마이 포털 - 세무사 백원숙";
|
||||
ViewData["Description"] = "고객님의 세무 신고 일정과 상담 이력을 실시간으로 확인하실 수 있는 마이페이지입니다.";
|
||||
}
|
||||
|
||||
<div class="bg-light py-5">
|
||||
<div class="container">
|
||||
<!-- 상단 헤더 & 환영 문구 -->
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center mb-5 pb-4 border-bottom">
|
||||
<div>
|
||||
<p class="text-primary fw-bold mb-1">TaxBaik My Portal</p>
|
||||
<h1 class="display-6 fw-bold text-dark">안녕하세요, @(User.Identity?.Name)님!</h1>
|
||||
@if (Model.ClientInfo != null)
|
||||
{
|
||||
<p class="text-muted mb-0">
|
||||
<i class="bi bi-building"></i> @(string.IsNullOrEmpty(Model.ClientInfo.CompanyName) ? "개인 고객" : Model.ClientInfo.CompanyName)
|
||||
| <i class="bi bi-telephone"></i> @Model.ClientInfo.Phone
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
<div class="mt-3 mt-sm-0">
|
||||
<form method="post" action="/taxbaik/portal/logout" class="d-inline">
|
||||
@Html.AntiForgeryToken()
|
||||
<button type="submit" class="btn btn-outline-danger btn-sm">
|
||||
<i class="bi bi-box-arrow-right"></i> 로그아웃
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (Model.ClientInfo == null)
|
||||
{
|
||||
<!-- 연동 대기 경고 -->
|
||||
<div class="card border-warning shadow-sm mb-5">
|
||||
<div class="card-body p-5 text-center">
|
||||
<div class="mb-4">
|
||||
<span class="display-1 text-warning"><i class="bi bi-exclamation-triangle-fill"></i></span>
|
||||
</div>
|
||||
<h3 class="fw-bold text-dark mb-3">고객 정보 연동 대기 중</h3>
|
||||
<p class="text-muted max-width-md mx-auto mb-4">
|
||||
가입하신 계정 정보(이메일/연락처)와 일치하는 세무 대리 고객 레코드를 찾지 못했습니다.<br />
|
||||
세무사 측에서 고객 등록을 완료하거나 관리자 백오피스에서 이메일/전화번호가 일치하도록 지정하면 자동으로 포털 데이터가 활성화됩니다.
|
||||
</p>
|
||||
<a href="/taxbaik/contact" class="btn btn-primary px-4 py-2">
|
||||
<i class="bi bi-chat-dots"></i> 세무사에게 문의하기
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="row g-4">
|
||||
<!-- 왼쪽: 세무 신고 현황 (Tax Filings) -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card glass-card mb-4">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h3 class="h5 fw-bold text-dark mb-0">
|
||||
<i class="bi bi-calendar-check text-primary me-2"></i> 나의 세무 신고 현황
|
||||
</h3>
|
||||
<span class="badge bg-secondary">총 @(Model.Filings.Count)건</span>
|
||||
</div>
|
||||
|
||||
@if (!Model.Filings.Any())
|
||||
{
|
||||
<div class="text-center py-5 text-muted">
|
||||
<i class="bi bi-folder-x display-4 d-block mb-3 text-secondary"></i>
|
||||
등록된 세무 신고 일정이 없습니다.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th scope="col">신고 종류</th>
|
||||
<th scope="col">신고 기한</th>
|
||||
<th scope="col">진행 상태</th>
|
||||
<th scope="col">메모</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var filing in Model.Filings)
|
||||
{
|
||||
var dDay = (filing.DueDate - DateTime.Today).Days;
|
||||
var statusClass = filing.Status switch
|
||||
{
|
||||
"filed" => "bg-success-subtle text-success",
|
||||
"overdue" => "bg-danger-subtle text-danger",
|
||||
_ => "bg-warning-subtle text-warning-emphasis"
|
||||
};
|
||||
var statusLabel = filing.Status switch
|
||||
{
|
||||
"filed" => "신고 완료",
|
||||
"overdue" => "기한 초과",
|
||||
_ => $"D-{dDay}"
|
||||
};
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<span class="fw-bold text-dark">@filing.FilingType</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>@filing.DueDate.ToString("yyyy-MM-dd")</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge @statusClass px-2.5 py-1.5 fs-7">@statusLabel</span>
|
||||
</td>
|
||||
<td class="text-muted small">
|
||||
@(string.IsNullOrEmpty(filing.Memo) ? "-" : filing.Memo)
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 오른쪽: 상담 이력 요약 (Consulting Activities) -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card glass-card">
|
||||
<div class="card-body p-4">
|
||||
<h3 class="h5 fw-bold text-dark mb-4">
|
||||
<i class="bi bi-chat-text text-primary me-2"></i> 최근 상담 및 지원 이력
|
||||
</h3>
|
||||
|
||||
@if (!Model.Consultations.Any())
|
||||
{
|
||||
<div class="text-center py-5 text-muted">
|
||||
<i class="bi bi-chat-square-dots display-4 d-block mb-3 text-secondary"></i>
|
||||
최근 상담 이력이 없습니다.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="timeline ps-2">
|
||||
@foreach (var activity in Model.Consultations)
|
||||
{
|
||||
<div class="timeline-item-modern">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<span class="badge bg-primary-subtle text-primary small">@activity.ActivityType</span>
|
||||
<small class="text-muted">@activity.ActivityDate.ToString("yyyy-MM-dd")</small>
|
||||
</div>
|
||||
<p class="text-dark small mb-1 fw-semibold">@activity.Description</p>
|
||||
@if (!string.IsNullOrEmpty(activity.Outcome))
|
||||
{
|
||||
<div class="bg-light p-2 rounded small text-muted mt-1">
|
||||
<strong>결과:</strong> @activity.Outcome
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,49 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using TaxBaik.Application.Services;
|
||||
using TaxBaik.Domain.Entities;
|
||||
using TaxBaik.Web.Services;
|
||||
|
||||
namespace TaxBaik.Web.Pages.Portal;
|
||||
|
||||
[Authorize(AuthenticationSchemes = PortalAuthDefaults.Scheme)]
|
||||
public class IndexModel : PageModel
|
||||
{
|
||||
private readonly TaxFilingService _taxFilingService;
|
||||
private readonly ConsultingActivityService _consultingActivityService;
|
||||
private readonly ClientService _clientService;
|
||||
|
||||
public IndexModel(
|
||||
TaxFilingService taxFilingService,
|
||||
ConsultingActivityService consultingActivityService,
|
||||
ClientService clientService)
|
||||
{
|
||||
_taxFilingService = taxFilingService;
|
||||
_consultingActivityService = consultingActivityService;
|
||||
_clientService = clientService;
|
||||
}
|
||||
|
||||
public Client? ClientInfo { get; private set; }
|
||||
public List<TaxFiling> Filings { get; private set; } = new();
|
||||
public List<ConsultingActivity> Consultations { get; private set; } = new();
|
||||
|
||||
public async Task<IActionResult> OnGetAsync()
|
||||
{
|
||||
var clientIdClaim = User.FindFirst("client_id");
|
||||
if (clientIdClaim != null && int.TryParse(clientIdClaim.Value, out var clientId))
|
||||
{
|
||||
ClientInfo = await _clientService.GetByIdAsync(clientId);
|
||||
if (ClientInfo != null)
|
||||
{
|
||||
var filingsData = await _taxFilingService.GetByClientIdAsync(clientId);
|
||||
Filings = filingsData.OrderBy(f => f.DueDate).ToList();
|
||||
|
||||
var consultationsData = await _consultingActivityService.GetByClientIdAsync(clientId);
|
||||
Consultations = consultationsData.OrderByDescending(c => c.ActivityDate).ToList();
|
||||
}
|
||||
}
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
@page "/portal/login"
|
||||
@model TaxBaik.Web.Pages.Portal.LoginModel
|
||||
@{
|
||||
ViewData["Title"] = "고객 포털 로그인";
|
||||
ViewData["Description"] = "고객 포털 로그인 페이지입니다.";
|
||||
ViewData["CanonicalUrl"] = $"{Request.Scheme}://{Request.Host}/taxbaik/portal/login";
|
||||
}
|
||||
|
||||
<section class="container py-5" style="max-width: 560px;">
|
||||
<h1 class="h3 fw-bold mb-4">고객 포털 로그인</h1>
|
||||
<div class="alert alert-secondary">
|
||||
포털 인증은 다음 단계에서 이메일/비밀번호와 소셜 로그인으로 연결됩니다.
|
||||
</div>
|
||||
@if (!string.IsNullOrWhiteSpace(Model.ErrorMessage))
|
||||
{
|
||||
<div class="alert alert-danger">@Model.ErrorMessage</div>
|
||||
}
|
||||
<form method="post" class="vstack gap-3">
|
||||
<div>
|
||||
<label class="form-label">이메일</label>
|
||||
<input class="form-control" asp-for="Email" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">비밀번호</label>
|
||||
<input class="form-control" asp-for="Password" type="password" />
|
||||
</div>
|
||||
<button class="btn btn-dark" type="submit">로그인</button>
|
||||
</form>
|
||||
<div class="d-grid gap-2 mt-4">
|
||||
<form method="post" asp-page-handler="Google">
|
||||
<button class="btn btn-outline-dark w-100" type="submit">Google로 로그인</button>
|
||||
</form>
|
||||
<form method="post" asp-page-handler="Naver">
|
||||
<button class="btn btn-outline-success w-100" type="submit">Naver로 로그인</button>
|
||||
</form>
|
||||
<form method="post" asp-page-handler="Kakao">
|
||||
<button class="btn btn-outline-warning w-100" type="submit">Kakao로 로그인</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,56 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using TaxBaik.Web.Services;
|
||||
|
||||
namespace TaxBaik.Web.Pages.Portal;
|
||||
|
||||
public class LoginModel : PageModel
|
||||
{
|
||||
private readonly PortalAuthService _portalAuthService;
|
||||
|
||||
[BindProperty]
|
||||
public string Email { get; set; } = "";
|
||||
|
||||
[BindProperty]
|
||||
public string Password { get; set; } = "";
|
||||
|
||||
[BindProperty]
|
||||
public string? ErrorMessage { get; set; }
|
||||
|
||||
public LoginModel(PortalAuthService portalAuthService)
|
||||
{
|
||||
_portalAuthService = portalAuthService;
|
||||
}
|
||||
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Email) || string.IsNullOrWhiteSpace(Password))
|
||||
{
|
||||
ErrorMessage = "이메일과 비밀번호를 입력하세요.";
|
||||
return Page();
|
||||
}
|
||||
|
||||
var signedIn = await _portalAuthService.SignInAsync(Email, Password);
|
||||
if (!signedIn)
|
||||
{
|
||||
ErrorMessage = "로그인 정보를 확인할 수 없습니다.";
|
||||
return Page();
|
||||
}
|
||||
|
||||
return RedirectToPage("/Portal/Index");
|
||||
}
|
||||
|
||||
public IActionResult OnPostGoogle() => Challenge(BuildProps("google"), PortalOAuthDefaults.GoogleScheme);
|
||||
|
||||
public IActionResult OnPostNaver() => Challenge(BuildProps("naver"), PortalOAuthDefaults.NaverScheme);
|
||||
|
||||
public IActionResult OnPostKakao() => Challenge(BuildProps("kakao"), PortalOAuthDefaults.KakaoScheme);
|
||||
|
||||
private static AuthenticationProperties BuildProps(string provider) =>
|
||||
new() { RedirectUri = $"/taxbaik/portal/external-callback?provider={provider}" };
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
@page "/portal/register"
|
||||
@model TaxBaik.Web.Pages.Portal.RegisterModel
|
||||
@{
|
||||
ViewData["Title"] = "고객 포털 회원가입";
|
||||
ViewData["Description"] = "고객 포털 회원가입 페이지입니다.";
|
||||
ViewData["CanonicalUrl"] = $"{Request.Scheme}://{Request.Host}/taxbaik/portal/register";
|
||||
}
|
||||
|
||||
<section class="container py-5" style="max-width: 640px;">
|
||||
<h1 class="h3 fw-bold mb-4">고객 포털 회원가입</h1>
|
||||
<div class="alert alert-secondary">
|
||||
가입 흐름은 다음 단계에서 이메일/전화번호 검증과 소셜 로그인으로 확장합니다.
|
||||
</div>
|
||||
<form method="post" class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">이름</label>
|
||||
<input class="form-control" asp-for="Name" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">연락처</label>
|
||||
<input class="form-control" asp-for="Phone" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">이메일</label>
|
||||
<input class="form-control" asp-for="Email" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">비밀번호</label>
|
||||
<input class="form-control" asp-for="Password" type="password" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button class="btn btn-dark" type="submit">가입하기</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
@@ -0,0 +1,75 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using TaxBaik.Application.Services;
|
||||
using TaxBaik.Web.Services;
|
||||
|
||||
namespace TaxBaik.Web.Pages.Portal;
|
||||
|
||||
public class RegisterModel : PageModel
|
||||
{
|
||||
private readonly PortalUserService _portalUserService;
|
||||
private readonly ClientService _clientService;
|
||||
|
||||
[BindProperty]
|
||||
public string Name { get; set; } = "";
|
||||
|
||||
[BindProperty]
|
||||
public string Phone { get; set; } = "";
|
||||
|
||||
[BindProperty]
|
||||
public string Email { get; set; } = "";
|
||||
|
||||
[BindProperty]
|
||||
public string Password { get; set; } = "";
|
||||
|
||||
[BindProperty]
|
||||
public string? ErrorMessage { get; set; }
|
||||
|
||||
public RegisterModel(PortalUserService portalUserService, ClientService clientService)
|
||||
{
|
||||
_portalUserService = portalUserService;
|
||||
_clientService = clientService;
|
||||
}
|
||||
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Name) || string.IsNullOrWhiteSpace(Email))
|
||||
{
|
||||
ErrorMessage = "이름과 이메일을 입력하세요.";
|
||||
return Page();
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Password) || Password.Length < 8)
|
||||
{
|
||||
ErrorMessage = "비밀번호는 8자 이상이어야 합니다.";
|
||||
return Page();
|
||||
}
|
||||
|
||||
var existing = await _portalUserService.GetByEmailAsync(Email);
|
||||
if (existing is not null)
|
||||
{
|
||||
ErrorMessage = "이미 등록된 이메일입니다.";
|
||||
return Page();
|
||||
}
|
||||
|
||||
int? clientId = null;
|
||||
var linkedClient = await _clientService.GetByEmailAsync(Email);
|
||||
if (linkedClient is null && !string.IsNullOrWhiteSpace(Phone))
|
||||
linkedClient = await _clientService.GetByPhoneAsync(Phone);
|
||||
if (linkedClient is not null)
|
||||
clientId = linkedClient.Id;
|
||||
|
||||
await _portalUserService.RegisterLocalAsync(
|
||||
Name,
|
||||
Email,
|
||||
Phone,
|
||||
PortalAuthService.HashPassword(Password),
|
||||
clientId: clientId);
|
||||
|
||||
return RedirectToPage("/Portal/Login");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user