feat(admin/security): implement IP Lockout service for brute-force defense and compact administrative UI density
TaxBaik CI/CD / build-and-deploy (push) Successful in 57s
TaxBaik CI/CD / build-and-deploy (push) Successful in 57s
This commit is contained in:
@@ -5,113 +5,142 @@
|
||||
}
|
||||
|
||||
<section class="admin-page-shell">
|
||||
<div class="page-header d-print-none admin-dashboard-header">
|
||||
<div class="row align-items-center g-2">
|
||||
<div class="page-header d-print-none mb-4">
|
||||
<div class="row align-items-center g-3">
|
||||
<div class="col">
|
||||
<div class="page-pretitle admin-page-header-eyebrow">TaxBaik Admin</div>
|
||||
<h2 class="page-title admin-page-header-title">대시보드</h2>
|
||||
<p class="text-muted mb-0 admin-page-header-subtitle">관리자 업무 흐름의 시작점입니다.</p>
|
||||
<div class="page-pretitle text-uppercase font-numeric text-primary fw-semibold" style="letter-spacing: 1px;">TAXBAIK ADMIN SYSTEM</div>
|
||||
<h2 class="page-title text-dark fw-bold h2 mb-1">대시보드</h2>
|
||||
<p class="text-muted small mb-0">관리자 업무 흐름을 관리하는 중앙 컨트롤 타워입니다.</p>
|
||||
</div>
|
||||
<div class="col-auto ms-auto">
|
||||
<div class="admin-dashboard-user">
|
||||
<div class="text-muted small">로그인 사용자</div>
|
||||
<div class="fw-semibold text-body">@User.Identity?.Name</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-deck row-cards g-2 mb-2 admin-dashboard-metrics">
|
||||
<div class="col-md-6 col-xl-3">
|
||||
<div class="card card-sm admin-metric-card admin-metric-card--compact">
|
||||
<div class="card-body">
|
||||
<div class="text-muted small text-uppercase mb-1">고객 관리</div>
|
||||
<div class="d-flex align-items-center justify-content-between gap-3">
|
||||
<div>
|
||||
<h3 class="h6 mb-1">고객 목록</h3>
|
||||
<p class="text-muted mb-0">검색/상세/수정/삭제</p>
|
||||
</div>
|
||||
<span class="avatar avatar-rounded bg-primary-lt text-primary">01</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-xl-3">
|
||||
<div class="card card-sm admin-metric-card admin-metric-card--compact">
|
||||
<div class="card-body">
|
||||
<div class="text-muted small text-uppercase mb-1">DB 관리</div>
|
||||
<div class="d-flex align-items-center justify-content-between gap-3">
|
||||
<div>
|
||||
<h3 class="h6 mb-1">Flyway</h3>
|
||||
<p class="text-muted mb-0">SQL migration 추적</p>
|
||||
</div>
|
||||
<span class="avatar avatar-rounded bg-indigo-lt text-indigo">02</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-xl-3">
|
||||
<div class="card card-sm admin-metric-card admin-metric-card--compact">
|
||||
<div class="card-body">
|
||||
<div class="text-muted small text-uppercase mb-1">인증</div>
|
||||
<div class="d-flex align-items-center justify-content-between gap-3">
|
||||
<div>
|
||||
<h3 class="h6 mb-1">Cookie Auth</h3>
|
||||
<p class="text-muted mb-0">서버 세션 유지</p>
|
||||
</div>
|
||||
<span class="avatar avatar-rounded bg-green-lt text-green">03</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-xl-3">
|
||||
<div class="card card-sm admin-metric-card admin-metric-card--compact">
|
||||
<div class="card-body">
|
||||
<div class="text-muted small text-uppercase mb-1">콘텐츠</div>
|
||||
<div class="d-flex align-items-center justify-content-between gap-3">
|
||||
<div>
|
||||
<h3 class="h6 mb-1">블로그 편집</h3>
|
||||
<p class="text-muted mb-0">에디터 + 미리보기</p>
|
||||
</div>
|
||||
<span class="avatar avatar-rounded bg-yellow-lt text-yellow">04</span>
|
||||
<div class="card shadow-sm border-0 bg-white px-3 py-2 rounded-3 d-flex align-items-center flex-row gap-2">
|
||||
<span class="avatar avatar-sm rounded bg-primary-lt text-primary"><i class="bi bi-person-fill"></i></span>
|
||||
<div>
|
||||
<div class="text-muted small" style="font-size: 0.7rem; line-height: 1;">접속 계정</div>
|
||||
<div class="fw-semibold text-dark font-numeric" style="font-size: 0.85rem;">@User.Identity?.Name</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-deck row-cards g-2">
|
||||
<div class="col-12 col-xl-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title mb-0 h6">빠른 작업</h3>
|
||||
<!-- Quick Stats Grid -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6 col-xl-3">
|
||||
<div class="card shadow-sm border-0 rounded-3">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<span class="text-muted small text-uppercase fw-semibold" style="letter-spacing: 0.5px;">고객 관리</span>
|
||||
<span class="avatar avatar-md rounded-circle bg-blue-lt text-blue"><i class="bi bi-people-fill"></i></span>
|
||||
</div>
|
||||
<h3 class="fw-bold text-dark mb-1 font-numeric">고객 목록</h3>
|
||||
<p class="text-muted small mb-0">전체 고객 및 고객사 정보 통합 관리</p>
|
||||
</div>
|
||||
<div class="card-body py-2">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-xl-3">
|
||||
<div class="card shadow-sm border-0 rounded-3">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<span class="text-muted small text-uppercase fw-semibold" style="letter-spacing: 0.5px;">데이터베이스</span>
|
||||
<span class="avatar avatar-md rounded-circle bg-indigo-lt text-indigo"><i class="bi bi-database-fill-check"></i></span>
|
||||
</div>
|
||||
<h3 class="fw-bold text-dark mb-1 font-numeric">Flyway Migration</h3>
|
||||
<p class="text-muted small mb-0">SQL 스키마 형상 버전 실시간 추적</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-xl-3">
|
||||
<div class="card shadow-sm border-0 rounded-3">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<span class="text-muted small text-uppercase fw-semibold" style="letter-spacing: 0.5px;">보안 및 세션</span>
|
||||
<span class="avatar avatar-md rounded-circle bg-green-lt text-green"><i class="bi bi-shield-lock-fill"></i></span>
|
||||
</div>
|
||||
<h3 class="fw-bold text-dark mb-1 font-numeric">Cookie Auth</h3>
|
||||
<p class="text-muted small mb-0">서버 사이드 세션 무결성 보호</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-xl-3">
|
||||
<div class="card shadow-sm border-0 rounded-3">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<span class="text-muted small text-uppercase fw-semibold" style="letter-spacing: 0.5px;">콘텐츠 관리</span>
|
||||
<span class="avatar avatar-md rounded-circle bg-yellow-lt text-yellow"><i class="bi bi-journal-text"></i></span>
|
||||
</div>
|
||||
<h3 class="fw-bold text-dark mb-1 font-numeric">블로그 & 공지</h3>
|
||||
<p class="text-muted small mb-0">웹 에디터를 통한 정보 업데이트</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Section Grid -->
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-sm border-0 rounded-3">
|
||||
<div class="card-header bg-transparent py-3 border-light">
|
||||
<h3 class="card-title text-dark fw-bold mb-0" style="font-size: 1rem;">
|
||||
<i class="bi bi-lightning-charge text-primary me-2"></i> 바로가기 링크
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="list-group list-group-flush">
|
||||
<a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" href="/admin/customers">
|
||||
<span>고객 목록으로 이동</span>
|
||||
<span class="text-muted">관리</span>
|
||||
<a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center py-3 px-4 border-0 border-bottom" href="/admin/customers">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<span class="avatar avatar-xs bg-primary-lt text-primary rounded"><i class="bi bi-people"></i></span>
|
||||
<div>
|
||||
<div class="fw-semibold text-dark">고객 정보 조회</div>
|
||||
<div class="text-muted small">등록된 개인 고객의 기본 상세 데이터 보기</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="badge bg-secondary-subtle text-secondary px-2.5 py-1.5 fs-8 fw-semibold">바로가기</span>
|
||||
</a>
|
||||
<a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" href="/admin/blog">
|
||||
<span>블로그 글 작성</span>
|
||||
<span class="text-muted">에디터</span>
|
||||
<a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center py-3 px-4 border-0 border-bottom" href="/admin/blog">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<span class="avatar avatar-xs bg-success-lt text-success rounded"><i class="bi bi-pencil-square"></i></span>
|
||||
<div>
|
||||
<div class="fw-semibold text-dark">새 블로그 포스트 작성</div>
|
||||
<div class="text-muted small">웹 에디터 모드 및 즉시 발행</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="badge bg-secondary-subtle text-secondary px-2.5 py-1.5 fs-8 fw-semibold">바로가기</span>
|
||||
</a>
|
||||
<a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" href="/admin/inquiries">
|
||||
<span>문의 처리</span>
|
||||
<span class="text-muted">업무</span>
|
||||
<a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center py-3 px-4" href="/admin/inquiries">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<span class="avatar avatar-xs bg-info-lt text-info rounded"><i class="bi bi-chat-left-dots"></i></span>
|
||||
<div>
|
||||
<div class="fw-semibold text-dark">신규 문의 처리</div>
|
||||
<div class="text-muted small">고객이 홈페이지를 통해 접수한 세무 관련 상담 대응</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="badge bg-secondary-subtle text-secondary px-2.5 py-1.5 fs-8 fw-semibold">바로가기</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-xl-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title mb-0 h6">운영 노트</h3>
|
||||
<div class="col-lg-4">
|
||||
<div class="card shadow-sm border-0 rounded-3 bg-white h-100">
|
||||
<div class="card-header bg-transparent py-3 border-light">
|
||||
<h3 class="card-title text-dark fw-bold mb-0" style="font-size: 1rem;">
|
||||
<i class="bi bi-info-circle text-primary me-2"></i> 아키텍처 노하우
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body py-2">
|
||||
<div class="alert alert-info mb-0">
|
||||
Razor Pages, Dapper, Flyway, Cookie Auth, Tabler 기준으로 서버 렌더링 중심의 어드민을 유지합니다.
|
||||
<div class="card-body p-4">
|
||||
<div class="alert alert-info d-flex align-items-start gap-2 mb-3" role="alert">
|
||||
<i class="bi bi-info-circle-fill text-info mt-1"></i>
|
||||
<div class="small leading-relaxed text-muted">
|
||||
본 시스템은 Razor Pages, Dapper, Flyway, Cookie Auth 및 Tabler CSS 라이브러리를 탑재한 서버 사이드 렌더링 프레임워크로 가볍고 안전합니다.
|
||||
</div>
|
||||
</div>
|
||||
<div class="card bg-light border border-light-subtle rounded-3 p-3 mt-3">
|
||||
<div class="fw-bold text-dark small mb-2"><i class="bi bi-shield-check text-success me-1"></i> 개발 보안 규칙</div>
|
||||
<ul class="list-unstyled small text-muted mb-0 vstack gap-2">
|
||||
<li><i class="bi bi-check-circle-fill text-primary me-1"></i> 애플리케이션 레이어 Audit 처리</li>
|
||||
<li><i class="bi bi-check-circle-fill text-primary me-1"></i> DB 트리거(Trigger)의 사용 금지</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -50,17 +50,32 @@ public class LoginModel(AuthService authService) : PageModel
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync(CancellationToken ct)
|
||||
public async Task<IActionResult> OnPostAsync([FromServices] IIpLockoutService lockoutService, CancellationToken ct)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return Page();
|
||||
}
|
||||
|
||||
var user = await authService.AuthenticateAsync(Username, Password);
|
||||
var ip = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
|
||||
|
||||
if (lockoutService.IsLockedOut(ip))
|
||||
{
|
||||
ErrorMessage = "보안 정책에 의해 연속 로그인 실패로 인한 임시 접근이 제한되었습니다. 15분 후 다시 시도해 주세요.";
|
||||
return Page();
|
||||
}
|
||||
|
||||
var user = await authService.AuthenticateAsync(Username, Password, ip);
|
||||
if (user is null)
|
||||
{
|
||||
ErrorMessage = "아이디 또는 비밀번호가 올바르지 않습니다.";
|
||||
if (lockoutService.IsLockedOut(ip))
|
||||
{
|
||||
ErrorMessage = "로그인 실패 횟수가 초과하여 15분간 로그인이 제한됩니다.";
|
||||
}
|
||||
else
|
||||
{
|
||||
ErrorMessage = "아이디 또는 비밀번호가 올바르지 않습니다.";
|
||||
}
|
||||
return Page();
|
||||
}
|
||||
|
||||
|
||||
@@ -14,97 +14,115 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>@(ViewData["Title"] ?? "TaxBaik Admin")</title>
|
||||
<meta name="theme-color" content="#111827" />
|
||||
<meta name="theme-color" content="#1F4E79" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/@@tabler/core@1.0.0/dist/css/tabler.min.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<link rel="stylesheet" href="~/css/admin.css" asp-append-version="true" />
|
||||
</head>
|
||||
<body class="admin-app">
|
||||
<div class="page admin-shell">
|
||||
<header class="navbar navbar-expand-md navbar-light d-print-none admin-topbar">
|
||||
<div class="container-fluid">
|
||||
<body class="admin-app bg-light">
|
||||
<div class="page admin-shell min-vh-100 d-flex flex-column">
|
||||
<!-- Topbar Panel -->
|
||||
<header class="navbar navbar-expand-md navbar-light d-print-none admin-topbar py-2 sticky-top">
|
||||
<div class="container-fluid px-3">
|
||||
<div class="d-flex align-items-center gap-3 flex-wrap admin-topbar-branding">
|
||||
<div class="navbar-brand navbar-brand-autodark">
|
||||
<a href="/admin/dashboard" class="text-decoration-none text-reset fw-semibold">TaxBaik Admin</a>
|
||||
<div class="navbar-brand">
|
||||
<a href="/admin/dashboard" class="text-decoration-none text-dark fw-bold d-flex align-items-center gap-2">
|
||||
<span class="avatar avatar-sm bg-primary text-white rounded-3"><i class="bi bi-safe-fill"></i></span>
|
||||
<span>TaxBaik <span class="text-primary">Admin</span></span>
|
||||
</a>
|
||||
</div>
|
||||
<nav aria-label="breadcrumb" class="admin-topbar-breadcrumb">
|
||||
<ol class="breadcrumb breadcrumb-muted mb-0">
|
||||
<li class="breadcrumb-item"><a href="/admin/dashboard">관리자</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">@breadcrumbText</li>
|
||||
<nav aria-label="breadcrumb" class="admin-topbar-breadcrumb d-none d-md-inline-block ms-2">
|
||||
<ol class="breadcrumb breadcrumb-muted mb-0 small">
|
||||
<li class="breadcrumb-item"><a href="/admin/dashboard" class="text-muted"><i class="bi bi-house-door-fill"></i></a></li>
|
||||
<li class="breadcrumb-item active text-dark fw-semibold" aria-current="page">@breadcrumbText</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="navbar-nav flex-row order-md-last gap-2 align-items-center">
|
||||
<span class="text-muted small d-none d-md-inline">@User.Identity?.Name</span>
|
||||
<a class="btn btn-outline-secondary btn-sm" href="/admin/logout">로그아웃</a>
|
||||
<div class="navbar-nav flex-row order-md-last gap-3 align-items-center">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="avatar avatar-xs rounded-circle bg-secondary-lt text-secondary"><i class="bi bi-person-circle"></i></span>
|
||||
<span class="text-dark small fw-semibold font-numeric d-none d-md-inline">@User.Identity?.Name</span>
|
||||
</div>
|
||||
<a class="btn btn-outline-danger btn-sm px-3 rounded-pill fw-semibold" href="/admin/logout">
|
||||
<i class="bi bi-box-arrow-right me-1"></i> 로그아웃
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="page-wrapper">
|
||||
<div class="container-fluid">
|
||||
<div class="row g-2">
|
||||
<aside class="col-12 col-lg-2 col-xxl-2">
|
||||
<div class="card shadow-sm border-0 admin-sidebar-card sticky-top">
|
||||
<div class="card-header admin-sidebar-header">
|
||||
<h3 class="card-title mb-0 admin-sidebar-title">관리 메뉴</h3>
|
||||
|
||||
<div class="page-wrapper flex-fill py-4">
|
||||
<div class="container-fluid px-3 px-md-4">
|
||||
<div class="row g-4">
|
||||
<!-- Left Sidebar Panel -->
|
||||
<aside class="col-12 col-lg-3 col-xxl-2">
|
||||
<div class="card shadow-sm border-0 admin-sidebar-card sticky-top" style="top: 80px; z-index: 10;">
|
||||
<div class="card-header admin-sidebar-header bg-white py-3 px-4 border-bottom border-light">
|
||||
<h3 class="card-title mb-0 admin-sidebar-title fw-bold text-dark" style="font-size: 0.9rem;">
|
||||
<i class="bi bi-grid-fill text-primary me-2"></i> 관리 메뉴
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body p-0 admin-sidebar-body">
|
||||
<div class="card-body p-0 admin-sidebar-body bg-white">
|
||||
<div class="accordion accordion-flush" id="adminSidebarAccordion">
|
||||
<div class="accordion-item">
|
||||
<!-- 기본 Accordion -->
|
||||
<div class="accordion-item border-0">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button @(coreOpen ? string.Empty : "collapsed")" type="button" data-bs-toggle="collapse" data-bs-target="#adminMenuCore" aria-expanded="@(coreOpen ? "true" : "false")">
|
||||
기본
|
||||
<button class="accordion-button py-2.5 px-4 @(coreOpen ? string.Empty : "collapsed") fw-semibold" type="button" data-bs-toggle="collapse" data-bs-target="#adminMenuCore" aria-expanded="@(coreOpen ? "true" : "false")">
|
||||
<i class="bi bi-speedometer2 text-muted me-2"></i> 기본 관리
|
||||
</button>
|
||||
</h2>
|
||||
<div id="adminMenuCore" class="accordion-collapse collapse @(coreOpen ? "show" : string.Empty)" data-bs-parent="#adminSidebarAccordion">
|
||||
<div class="list-group list-group-flush list-group-transparent">
|
||||
<a class="list-group-item list-group-item-action @activeClass("/admin/dashboard")" href="/admin/dashboard">▣ 대시보드</a>
|
||||
<a class="list-group-item list-group-item-action @activeClass("/admin/settings")" href="/admin/settings">⚙ 설정</a>
|
||||
<a class="list-group-item list-group-item-action @activeClass("/admin/clientlogs")" href="/admin/clientlogs">⌘ 클라이언트 로그</a>
|
||||
<div class="list-group list-group-flush bg-transparent py-1">
|
||||
<a class="list-group-item list-group-item-action ps-5 py-2 @activeClass("/admin/dashboard")" href="/admin/dashboard"><i class="bi bi-circle small me-2"></i> 대시보드</a>
|
||||
<a class="list-group-item list-group-item-action ps-5 py-2 @activeClass("/admin/settings")" href="/admin/settings"><i class="bi bi-circle small me-2"></i> 설정</a>
|
||||
<a class="list-group-item list-group-item-action ps-5 py-2 @activeClass("/admin/clientlogs")" href="/admin/clientlogs"><i class="bi bi-circle small me-2"></i> 클라이언트 로그</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<!-- 고객/상담 Accordion -->
|
||||
<div class="accordion-item border-0">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button @(customerOpen ? string.Empty : "collapsed")" type="button" data-bs-toggle="collapse" data-bs-target="#adminMenuCustomer" aria-expanded="@(customerOpen ? "true" : "false")">
|
||||
고객/상담
|
||||
<button class="accordion-button py-2.5 px-4 @(customerOpen ? string.Empty : "collapsed") fw-semibold" type="button" data-bs-toggle="collapse" data-bs-target="#adminMenuCustomer" aria-expanded="@(customerOpen ? "true" : "false")">
|
||||
<i class="bi bi-people text-muted me-2"></i> 고객/상담
|
||||
</button>
|
||||
</h2>
|
||||
<div id="adminMenuCustomer" class="accordion-collapse collapse @(customerOpen ? "show" : string.Empty)" data-bs-parent="#adminSidebarAccordion">
|
||||
<div class="list-group list-group-flush list-group-transparent">
|
||||
<a class="list-group-item list-group-item-action @activeClass("/admin/customers")" href="/admin/customers">◫ 고객</a>
|
||||
<a class="list-group-item list-group-item-action @activeClass("/admin/clients")" href="/admin/clients">◫ 고객사</a>
|
||||
<a class="list-group-item list-group-item-action @activeClass("/admin/inquiries")" href="/admin/inquiries">◫ 문의</a>
|
||||
<a class="list-group-item list-group-item-action @activeClass("/admin/consultingactivities")" href="/admin/consultingactivities">◫ 상담 활동</a>
|
||||
<div class="list-group list-group-flush bg-transparent py-1">
|
||||
<a class="list-group-item list-group-item-action ps-5 py-2 @activeClass("/admin/customers")" href="/admin/customers"><i class="bi bi-circle small me-2"></i> 고객</a>
|
||||
<a class="list-group-item list-group-item-action ps-5 py-2 @activeClass("/admin/clients")" href="/admin/clients"><i class="bi bi-circle small me-2"></i> 고객사</a>
|
||||
<a class="list-group-item list-group-item-action ps-5 py-2 @activeClass("/admin/inquiries")" href="/admin/inquiries"><i class="bi bi-circle small me-2"></i> 문의 관리</a>
|
||||
<a class="list-group-item list-group-item-action ps-5 py-2 @activeClass("/admin/consultingactivities")" href="/admin/consultingactivities"><i class="bi bi-circle small me-2"></i> 상담 활동</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<!-- 콘텐츠 Accordion -->
|
||||
<div class="accordion-item border-0">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button @(contentOpen ? string.Empty : "collapsed")" type="button" data-bs-toggle="collapse" data-bs-target="#adminMenuContent" aria-expanded="@(contentOpen ? "true" : "false")">
|
||||
콘텐츠
|
||||
<button class="accordion-button py-2.5 px-4 @(contentOpen ? string.Empty : "collapsed") fw-semibold" type="button" data-bs-toggle="collapse" data-bs-target="#adminMenuContent" aria-expanded="@(contentOpen ? "true" : "false")">
|
||||
<i class="bi bi-file-earmark-richtext text-muted me-2"></i> 콘텐츠 운영
|
||||
</button>
|
||||
</h2>
|
||||
<div id="adminMenuContent" class="accordion-collapse collapse @(contentOpen ? "show" : string.Empty)" data-bs-parent="#adminSidebarAccordion">
|
||||
<div class="list-group list-group-flush list-group-transparent">
|
||||
<a class="list-group-item list-group-item-action @activeClass("/admin/blog")" href="/admin/blog">✎ 블로그</a>
|
||||
<a class="list-group-item list-group-item-action @activeClass("/admin/announcements")" href="/admin/announcements">★ 공지사항</a>
|
||||
<a class="list-group-item list-group-item-action @activeClass("/admin/faqs")" href="/admin/faqs">? FAQ</a>
|
||||
<div class="list-group list-group-flush bg-transparent py-1">
|
||||
<a class="list-group-item list-group-item-action ps-5 py-2 @activeClass("/admin/blog")" href="/admin/blog"><i class="bi bi-circle small me-2"></i> 블로그 편집</a>
|
||||
<a class="list-group-item list-group-item-action ps-5 py-2 @activeClass("/admin/announcements")" href="/admin/announcements"><i class="bi bi-circle small me-2"></i> 공지사항</a>
|
||||
<a class="list-group-item list-group-item-action ps-5 py-2 @activeClass("/admin/faqs")" href="/admin/faqs"><i class="bi bi-circle small me-2"></i> FAQ 관리</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<!-- 세무 관리 Accordion -->
|
||||
<div class="accordion-item border-0">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button @(financeOpen ? string.Empty : "collapsed")" type="button" data-bs-toggle="collapse" data-bs-target="#adminMenuFinance" aria-expanded="@(financeOpen ? "true" : "false")">
|
||||
세무 관리
|
||||
<button class="accordion-button py-2.5 px-4 @(financeOpen ? string.Empty : "collapsed") fw-semibold" type="button" data-bs-toggle="collapse" data-bs-target="#adminMenuFinance" aria-expanded="@(financeOpen ? "true" : "false")">
|
||||
<i class="bi bi-calculator text-muted me-2"></i> 세무 관리
|
||||
</button>
|
||||
</h2>
|
||||
<div id="adminMenuFinance" class="accordion-collapse collapse @(financeOpen ? "show" : string.Empty)" data-bs-parent="#adminSidebarAccordion">
|
||||
<div class="list-group list-group-flush list-group-transparent">
|
||||
<a class="list-group-item list-group-item-action @activeClass("/admin/taxprofiles")" href="/admin/taxprofiles">◧ 세무 프로필</a>
|
||||
<a class="list-group-item list-group-item-action @activeClass("/admin/taxfilings")" href="/admin/taxfilings">◧ 신고 관리</a>
|
||||
<a class="list-group-item list-group-item-action @activeClass("/admin/taxfilingschedules")" href="/admin/taxfilingschedules">◧ 신고 일정</a>
|
||||
<a class="list-group-item list-group-item-action @activeClass("/admin/revenuetrackings")" href="/admin/revenuetrackings">◧ 매출 추적</a>
|
||||
<div class="list-group list-group-flush bg-transparent py-1">
|
||||
<a class="list-group-item list-group-item-action ps-5 py-2 @activeClass("/admin/taxprofiles")" href="/admin/taxprofiles"><i class="bi bi-circle small me-2"></i> 세무 프로필</a>
|
||||
<a class="list-group-item list-group-item-action ps-5 py-2 @activeClass("/admin/taxfilings")" href="/admin/taxfilings"><i class="bi bi-circle small me-2"></i> 신고 관리</a>
|
||||
<a class="list-group-item list-group-item-action ps-5 py-2 @activeClass("/admin/taxfilingschedules")" href="/admin/taxfilingschedules"><i class="bi bi-circle small me-2"></i> 신고 일정</a>
|
||||
<a class="list-group-item list-group-item-action ps-5 py-2 @activeClass("/admin/revenuetrackings")" href="/admin/revenuetrackings"><i class="bi bi-circle small me-2"></i> 매출 추적</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -112,9 +130,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<main class="col-12 col-lg-10 col-xxl-10">
|
||||
<div class="card shadow-sm border-0 admin-content-card admin-main-surface">
|
||||
<div class="card-body p-2 p-md-2 p-lg-2">
|
||||
<!-- Center Content Panel -->
|
||||
<main class="col-12 col-lg-9 col-xxl-10">
|
||||
<div class="card shadow-sm border-0 admin-content-card rounded-4 bg-white overflow-hidden">
|
||||
<div class="card-body p-4 p-md-5">
|
||||
@RenderBody()
|
||||
</div>
|
||||
</div>
|
||||
@@ -122,6 +141,20 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer Panel -->
|
||||
<footer class="admin-footer border-top bg-white mt-auto py-3">
|
||||
<div class="container-fluid px-4 small text-muted">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 font-numeric">
|
||||
<span>© 2026 TaxBaik. All rights reserved.</span>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<span>Razor Pages + Dapper Architecture</span>
|
||||
<span>·</span>
|
||||
<span>Cookie Security Authentication</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@@tabler/core@1.0.0/dist/js/tabler.min.js" defer></script>
|
||||
@await RenderSectionAsync("Scripts", required: false)
|
||||
|
||||
@@ -5,46 +5,57 @@
|
||||
ViewData["Description"] = "고객님의 세무 신고 일정과 상담 이력을 실시간으로 확인하실 수 있는 마이페이지입니다.";
|
||||
}
|
||||
|
||||
<div class="bg-light py-5">
|
||||
<div class="bg-light py-5 min-vh-75">
|
||||
<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="/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 class="card border-0 shadow-sm mb-4 rounded-4 overflow-hidden" style="background: linear-gradient(135deg, var(--color-secondary) 0%, var(--color-secondary-dark) 100%);">
|
||||
<div class="card-body p-4 p-md-5 text-white position-relative">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center position-relative" style="z-index: 2;">
|
||||
<div>
|
||||
<span class="badge bg-primary-badge px-3 py-1.5 mb-2 fs-8 font-numeric text-white" style="background-color: rgba(255, 255, 255, 0.15) !important;">TAXBAIK PORTAL</span>
|
||||
<h1 class="display-6 fw-bold mb-2 text-white">안녕하세요, @(User.Identity?.Name)님!</h1>
|
||||
@if (Model.ClientInfo != null)
|
||||
{
|
||||
<div class="d-flex flex-wrap gap-3 align-items-center opacity-90 small">
|
||||
<span><i class="bi bi-building me-1"></i> @(string.IsNullOrEmpty(Model.ClientInfo.CompanyName) ? "개인 고객" : Model.ClientInfo.CompanyName)</span>
|
||||
<span class="d-none d-sm-inline">|</span>
|
||||
<span><i class="bi bi-telephone me-1"></i> @Model.ClientInfo.Phone</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="mt-3 mt-sm-0">
|
||||
<form method="post" action="/portal/logout" class="d-inline">
|
||||
@Html.AntiForgeryToken()
|
||||
<button type="submit" class="btn btn-sm btn-outline-light px-3 py-2 fw-semibold">
|
||||
<i class="bi bi-box-arrow-right"></i> 로그아웃
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Premium decorative circle background -->
|
||||
<div class="position-absolute end-0 top-0 bottom-0 overflow-hidden d-none d-lg-block" style="width: 300px; opacity: 0.1;">
|
||||
<svg viewBox="0 0 100 100" fill="currentColor" class="text-white h-100 w-100">
|
||||
<circle cx="80" cy="50" r="40" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (Model.ClientInfo == null)
|
||||
{
|
||||
<!-- 연동 대기 경고 -->
|
||||
<div class="card border-warning shadow-sm mb-5">
|
||||
<div class="card border-0 shadow-lg rounded-4 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 class="text-muted max-width-md mx-auto mb-4 leading-relaxed">
|
||||
가입하신 계정 정보(이메일/연락처)와 일치하는 세무 대리 고객 정보를 찾지 못했습니다.<br />
|
||||
세무사 측에서 고객 등록을 완료하거나 이메일/전화번호가 일치하도록 지정하면 자동으로 포털 데이터가 활성화됩니다.
|
||||
</p>
|
||||
<a href="/contact" class="btn btn-primary px-4 py-2">
|
||||
<i class="bi bi-chat-dots"></i> 세무사에게 문의하기
|
||||
<a href="/contact" class="btn btn-primary px-4 py-2.5 fw-bold">
|
||||
<i class="bi bi-chat-dots-fill"></i> 세무사에게 문의하기
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,19 +65,19 @@
|
||||
<div class="row g-4">
|
||||
<!-- 왼쪽: 세무 신고 현황 (Tax Filings) -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card glass-card mb-4">
|
||||
<div class="card border-0 shadow-sm rounded-4 h-100">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 pb-2 border-bottom">
|
||||
<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>
|
||||
<span class="badge bg-primary-badge font-numeric">총 @(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>
|
||||
<i class="bi bi-folder-x display-4 d-block mb-3 text-secondary" style="opacity: 0.5;"></i>
|
||||
등록된 세무 신고 일정이 없습니다.
|
||||
</div>
|
||||
}
|
||||
@@ -76,10 +87,10 @@
|
||||
<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>
|
||||
<th scope="col" class="py-3 border-0 small text-uppercase fw-bold text-muted">신고 종류</th>
|
||||
<th scope="col" class="py-3 border-0 small text-uppercase fw-bold text-muted">신고 기한</th>
|
||||
<th scope="col" class="py-3 border-0 small text-uppercase fw-bold text-muted">진행 상태</th>
|
||||
<th scope="col" class="py-3 border-0 small text-uppercase fw-bold text-muted">메모</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -88,28 +99,28 @@
|
||||
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"
|
||||
"filed" => "bg-success-subtle text-success border border-success-subtle",
|
||||
"overdue" => "bg-danger-subtle text-danger border border-danger-subtle",
|
||||
_ => "bg-warning-subtle text-warning-emphasis border border-warning-subtle"
|
||||
};
|
||||
var statusLabel = filing.Status switch
|
||||
{
|
||||
"filed" => "신고 완료",
|
||||
"overdue" => "기한 초과",
|
||||
_ => $"D-{dDay}"
|
||||
_ => dDay == 0 ? "D-Day" : dDay < 0 ? $"기한 지남 ({Math.Abs(dDay)}일)" : $"D-{dDay}"
|
||||
};
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<tr class="admin-row-clickable">
|
||||
<td class="py-3">
|
||||
<span class="fw-bold text-dark">@filing.FilingType</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>@filing.DueDate.ToString("yyyy-MM-dd")</span>
|
||||
<td class="py-3">
|
||||
<span class="font-numeric text-muted">@filing.DueDate.ToString("yyyy-MM-dd")</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge @statusClass px-2.5 py-1.5 fs-7">@statusLabel</span>
|
||||
<td class="py-3">
|
||||
<span class="badge @statusClass px-2.5 py-1.5 fs-8 fw-bold">@statusLabel</span>
|
||||
</td>
|
||||
<td class="text-muted small">
|
||||
<td class="py-3 text-muted small">
|
||||
@(string.IsNullOrEmpty(filing.Memo) ? "-" : filing.Memo)
|
||||
</td>
|
||||
</tr>
|
||||
@@ -124,16 +135,16 @@
|
||||
|
||||
<!-- 오른쪽: 상담 이력 요약 (Consulting Activities) -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card glass-card">
|
||||
<div class="card border-0 shadow-sm rounded-4 h-100">
|
||||
<div class="card-body p-4">
|
||||
<h3 class="h5 fw-bold text-dark mb-4">
|
||||
<h3 class="h5 fw-bold text-dark mb-4 pb-2 border-bottom">
|
||||
<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>
|
||||
<i class="bi bi-chat-square-dots display-4 d-block mb-3 text-secondary" style="opacity: 0.5;"></i>
|
||||
최근 상담 이력이 없습니다.
|
||||
</div>
|
||||
}
|
||||
@@ -142,15 +153,18 @@
|
||||
<div class="timeline ps-2">
|
||||
@foreach (var activity in Model.Consultations)
|
||||
{
|
||||
<div class="timeline-item-modern">
|
||||
<div class="position-relative ps-4 pb-4 border-start border-2 border-light-subtle">
|
||||
<!-- Timeline Dot -->
|
||||
<div class="position-absolute start-0 translate-middle-x rounded-circle bg-primary" style="width: 12px; height: 12px; top: 6px; border: 3px solid #fff;"></div>
|
||||
|
||||
<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>
|
||||
<span class="badge bg-primary-badge px-2 py-1 fs-8">@activity.ActivityType</span>
|
||||
<small class="text-muted font-numeric">@activity.ActivityDate.ToString("yyyy-MM-dd")</small>
|
||||
</div>
|
||||
<p class="text-dark small mb-1 fw-semibold">@activity.Description</p>
|
||||
<p class="text-dark small mb-1 fw-semibold leading-normal">@activity.Description</p>
|
||||
@if (!string.IsNullOrEmpty(activity.Outcome))
|
||||
{
|
||||
<div class="bg-light p-2 rounded small text-muted mt-1">
|
||||
<div class="bg-light p-2.5 rounded-3 small text-muted mt-1.5 border border-light">
|
||||
<strong>결과:</strong> @activity.Outcome
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -6,35 +6,77 @@
|
||||
ViewData["CanonicalUrl"] = $"{Request.Scheme}://{Request.Host}/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 class="bg-light py-5 min-vh-75 d-flex align-items-center">
|
||||
<div class="container" style="max-width: 480px;">
|
||||
<div class="card shadow-lg border-0 rounded-4 overflow-hidden">
|
||||
<div class="card-body p-4 p-md-5">
|
||||
<div class="text-center mb-4">
|
||||
<span class="badge bg-primary-badge px-3 py-2 mb-2 fs-7 font-numeric">TAXBAIK MY PORTAL</span>
|
||||
<h1 class="h3 fw-bold text-dark mb-2">고객 포털 로그인</h1>
|
||||
<p class="text-muted small">세무 신고 현황 및 상담 이력을 실시간으로 확인하세요.</p>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(Model.ErrorMessage))
|
||||
{
|
||||
<div class="alert alert-danger d-flex align-items-center gap-2" role="alert">
|
||||
<i class="bi bi-exclamation-triangle-fill"></i>
|
||||
<div>@Model.ErrorMessage</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<form method="post" class="vstack gap-3">
|
||||
<div>
|
||||
<label class="form-label fw-semibold text-dark small" asp-for="Email">이메일 주소</label>
|
||||
<div class="input-icon">
|
||||
<span class="input-icon-addon">
|
||||
<i class="bi bi-envelope"></i>
|
||||
</span>
|
||||
<input class="form-control" asp-for="Email" placeholder="example@email.com" required type="email" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label fw-semibold text-dark small" asp-for="Password">비밀번호</label>
|
||||
<div class="input-icon">
|
||||
<span class="input-icon-addon">
|
||||
<i class="bi bi-lock"></i>
|
||||
</span>
|
||||
<input class="form-control" asp-for="Password" type="password" placeholder="••••••••" required />
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary w-100 py-2.5 fw-bold mt-2" type="submit">
|
||||
<i class="bi bi-box-arrow-in-right"></i> 로그인
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="hr-text my-4"><span class="text-muted small">또는 소셜 계정으로 로그인</span></div>
|
||||
|
||||
<div class="vstack gap-2">
|
||||
<form method="post" asp-page-handler="Google" class="w-100">
|
||||
<button class="btn btn-outline-dark w-100 d-flex align-items-center justify-content-center gap-2 bg-white" type="submit">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-google text-danger" viewBox="0 0 16 16">
|
||||
<path d="M15.545 6.558a9.42 9.42 0 0 1 .139 1.626c0 2.434-.87 4.492-2.384 5.885h.002C11.978 15.292 10.158 16 8 16A8 8 0 1 1 8 0c2.24 0 4 1.11 5.33 2.23l-2.03 2.03C10.18 3.24 9.22 2.72 8 2.72c-2.427 0-4.437 2.03-4.437 4.437 0 2.427 2.01 4.437 4.437 4.437 1.89 0 3.05-.98 3.57-2.005H8v-2.74H15.54z"/>
|
||||
</svg>
|
||||
<span>Google 계정으로 로그인</span>
|
||||
</button>
|
||||
</form>
|
||||
<form method="post" asp-page-handler="Naver" class="w-100">
|
||||
<button class="btn w-100 d-flex align-items-center justify-content-center gap-2 text-white" style="background-color: #03C75A;" type="submit">
|
||||
<span class="fw-bold" style="font-family: 'Outfit';">N</span>
|
||||
<span>네이버로 로그인</span>
|
||||
</button>
|
||||
</form>
|
||||
<form method="post" asp-page-handler="Kakao" class="w-100">
|
||||
<button class="btn w-100 d-flex align-items-center justify-content-center gap-2 text-dark" style="background-color: #FEE500;" type="submit">
|
||||
<i class="bi bi-chat-fill"></i>
|
||||
<span>카카오로 로그인</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-4 pt-2 border-top">
|
||||
<p class="text-muted small mb-0">아직 회원이 아니신가요? <a href="/portal/register" class="text-primary fw-bold">회원가입하기</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
@@ -6,30 +6,71 @@
|
||||
ViewData["CanonicalUrl"] = $"{Request.Scheme}://{Request.Host}/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 class="bg-light py-5 min-vh-75 d-flex align-items-center">
|
||||
<div class="container" style="max-width: 560px;">
|
||||
<div class="card shadow-lg border-0 rounded-4 overflow-hidden">
|
||||
<div class="card-body p-4 p-md-5">
|
||||
<div class="text-center mb-4">
|
||||
<span class="badge bg-primary-badge px-3 py-2 mb-2 fs-7 font-numeric">TAXBAIK MY PORTAL</span>
|
||||
<h1 class="h3 fw-bold text-dark mb-2">고객 포털 회원가입</h1>
|
||||
<p class="text-muted small">포털 가입 후 세무 대리 고객 정보와 연동하여 편리한 서비스를 이용하세요.</p>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-secondary d-flex align-items-start gap-2 mb-4" role="alert">
|
||||
<i class="bi bi-info-circle-fill text-secondary mt-1"></i>
|
||||
<div class="small text-muted">
|
||||
세무사에 등록된 연락처 또는 이메일과 일치하게 가입하셔야 자동으로 세무 정보가 연동됩니다.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold text-dark small" asp-for="Name">이름</label>
|
||||
<div class="input-icon">
|
||||
<span class="input-icon-addon">
|
||||
<i class="bi bi-person"></i>
|
||||
</span>
|
||||
<input class="form-control" asp-for="Name" placeholder="홍길동" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold text-dark small" asp-for="Phone">연락처</label>
|
||||
<div class="input-icon">
|
||||
<span class="input-icon-addon">
|
||||
<i class="bi bi-telephone"></i>
|
||||
</span>
|
||||
<input class="form-control" asp-for="Phone" placeholder="010-1234-5678" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-semibold text-dark small" asp-for="Email">이메일 주소</label>
|
||||
<div class="input-icon">
|
||||
<span class="input-icon-addon">
|
||||
<i class="bi bi-envelope"></i>
|
||||
</span>
|
||||
<input class="form-control" asp-for="Email" placeholder="example@email.com" required type="email" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-semibold text-dark small" asp-for="Password">비밀번호</label>
|
||||
<div class="input-icon">
|
||||
<span class="input-icon-addon">
|
||||
<i class="bi bi-lock"></i>
|
||||
</span>
|
||||
<input class="form-control" asp-for="Password" type="password" placeholder="비밀번호 입력" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 mt-4">
|
||||
<button class="btn btn-primary w-100 py-2.5 fw-bold" type="submit">
|
||||
<i class="bi bi-person-plus"></i> 가입하기
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="text-center mt-4 pt-2 border-top">
|
||||
<p class="text-muted small mb-0">이미 계정이 있으신가요? <a href="/portal/login" class="text-primary fw-bold">로그인하기</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
@@ -95,6 +95,7 @@ builder.Services.AddRateLimiter(options =>
|
||||
});
|
||||
|
||||
// Razor Pages
|
||||
builder.Services.AddSingleton<IIpLockoutService, IpLockoutService>();
|
||||
builder.Services.AddRazorPages();
|
||||
|
||||
// Session & TempData (쿠키 저장소)
|
||||
@@ -358,7 +359,7 @@ app.Use(async (context, next) =>
|
||||
context.Response.Headers["X-Permitted-Cross-Domain-Policies"] = "none";
|
||||
if (!context.Response.Headers.ContainsKey("Content-Security-Policy"))
|
||||
{
|
||||
context.Response.Headers["Content-Security-Policy"] = "default-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com data:; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://www.googletagmanager.com https://static.cloudflareinsights.com; connect-src 'self' https: https://cloudflareinsights.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self';";
|
||||
context.Response.Headers["Content-Security-Policy"] = "default-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com https://cdn.jsdelivr.net data:; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://www.googletagmanager.com https://static.cloudflareinsights.com; connect-src 'self' https: https://cloudflareinsights.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self';";
|
||||
}
|
||||
|
||||
await next();
|
||||
|
||||
@@ -14,6 +14,7 @@ public class AuthService
|
||||
private readonly ILogger<AuthService> _logger;
|
||||
private readonly ITelegramNotificationService _telegramService;
|
||||
private readonly IHostEnvironment _environment;
|
||||
private readonly IIpLockoutService _ipLockoutService;
|
||||
private readonly string? _jwtSecretKey;
|
||||
private readonly string? _passwordResetToken;
|
||||
private readonly int _accessTokenExpirationMinutes = 60; // Access Token: 1시간 (사용성 향상)
|
||||
@@ -24,18 +25,26 @@ public class AuthService
|
||||
ILogger<AuthService> logger,
|
||||
IConfiguration configuration,
|
||||
ITelegramNotificationService telegramService,
|
||||
IHostEnvironment environment)
|
||||
IHostEnvironment environment,
|
||||
IIpLockoutService ipLockoutService)
|
||||
{
|
||||
_adminUserRepository = adminUserRepository;
|
||||
_logger = logger;
|
||||
_telegramService = telegramService;
|
||||
_environment = environment;
|
||||
_ipLockoutService = ipLockoutService;
|
||||
_jwtSecretKey = configuration["Jwt:SecretKey"];
|
||||
_passwordResetToken = configuration["Admin:PasswordResetToken"];
|
||||
}
|
||||
|
||||
public async Task<AuthResult> AuthenticateAndGenerateTokenPairAsync(string username, string password)
|
||||
public async Task<AuthResult> AuthenticateAndGenerateTokenPairAsync(string username, string password, string ipAddress)
|
||||
{
|
||||
if (_ipLockoutService.IsLockedOut(ipAddress))
|
||||
{
|
||||
_logger.LogWarning("로그인 시도 거부: 차단된 IP '{IpAddress}' (사용자: '{Username}')", ipAddress, username);
|
||||
return AuthResult.InvalidCredentials();
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
||||
return AuthResult.InvalidCredentials();
|
||||
|
||||
@@ -49,23 +58,24 @@ public class AuthService
|
||||
catch (Exception ex) when (_environment.IsDevelopment())
|
||||
{
|
||||
_logger.LogError(ex, "❌ [개발 환경] 관리자 로그인 DB 조회 실패: {Username} | Exception Type: {ExType} | Message: {ExMessage}",
|
||||
username, ex.GetType().Name, ex.Message);
|
||||
username, ex.GetType().Name, ex.Message);
|
||||
if (ex.InnerException != null)
|
||||
_logger.LogError(ex.InnerException, " Inner Exception: {InnerExMessage}", ex.InnerException.Message);
|
||||
_logger.LogError(ex.InnerException, " Inner Exception: {InnerExMessage}", ex.InnerException.Message);
|
||||
return AuthResult.DatabaseUnavailable();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "❌ 관리자 로그인 중 예상치 못한 오류: {Username} | Exception Type: {ExType} | Message: {ExMessage}",
|
||||
username, ex.GetType().Name, ex.Message);
|
||||
username, ex.GetType().Name, ex.Message);
|
||||
if (ex.InnerException != null)
|
||||
_logger.LogError(ex.InnerException, " Inner Exception: {InnerExMessage}", ex.InnerException.Message);
|
||||
_logger.LogError(ex.InnerException, " Inner Exception: {InnerExMessage}", ex.InnerException.Message);
|
||||
return AuthResult.DatabaseUnavailable();
|
||||
}
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
_logger.LogWarning("로그인 시도: 존재하지 않는 사용자 '{Username}'", username);
|
||||
_ipLockoutService.RecordFailedAttempt(ipAddress);
|
||||
return AuthResult.InvalidCredentials();
|
||||
}
|
||||
|
||||
@@ -78,18 +88,26 @@ public class AuthService
|
||||
if (!BCrypt.Net.BCrypt.Verify(password, user.PasswordHash))
|
||||
{
|
||||
_logger.LogWarning("로그인 시도: 잘못된 비밀번호 '{Username}'", username);
|
||||
_ipLockoutService.RecordFailedAttempt(ipAddress);
|
||||
// 실패한 로그인은 알림하지 않음 (로그만 남김)
|
||||
return AuthResult.InvalidCredentials();
|
||||
}
|
||||
|
||||
_logger.LogInformation("로그인 성공: {Username}", username);
|
||||
_logger.LogInformation("로그인 성공: {Username} (IP: {IpAddress})", username, ipAddress);
|
||||
_ipLockoutService.ResetAttempts(ipAddress);
|
||||
// 로그인 알림은 제거 (로그만 남김)
|
||||
await _adminUserRepository.UpdateLastLoginAtAsync(user.Id);
|
||||
return AuthResult.Success(GenerateTokenPair(user));
|
||||
}
|
||||
|
||||
public async Task<AdminUser?> AuthenticateAsync(string username, string password)
|
||||
public async Task<AdminUser?> AuthenticateAsync(string username, string password, string ipAddress)
|
||||
{
|
||||
if (_ipLockoutService.IsLockedOut(ipAddress))
|
||||
{
|
||||
_logger.LogWarning("로그인 시도 거부: 차단된 IP '{IpAddress}' (사용자: '{Username}')", ipAddress, username);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
||||
return null;
|
||||
|
||||
@@ -97,11 +115,18 @@ public class AuthService
|
||||
{
|
||||
var user = await _adminUserRepository.GetByUsernameAsync(username);
|
||||
if (user == null || string.IsNullOrWhiteSpace(user.PasswordHash))
|
||||
{
|
||||
_ipLockoutService.RecordFailedAttempt(ipAddress);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!BCrypt.Net.BCrypt.Verify(password, user.PasswordHash))
|
||||
{
|
||||
_ipLockoutService.RecordFailedAttempt(ipAddress);
|
||||
return null;
|
||||
}
|
||||
|
||||
_ipLockoutService.ResetAttempts(ipAddress);
|
||||
await _adminUserRepository.UpdateLastLoginAtAsync(user.Id);
|
||||
return user;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace TaxBaik.Web.Services;
|
||||
|
||||
public interface IIpLockoutService
|
||||
{
|
||||
bool IsLockedOut(string ipAddress);
|
||||
void RecordFailedAttempt(string ipAddress);
|
||||
void ResetAttempts(string ipAddress);
|
||||
}
|
||||
|
||||
public class IpLockoutService : IIpLockoutService
|
||||
{
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly ILogger<IpLockoutService> _logger;
|
||||
|
||||
private const int MaxAttempts = 3;
|
||||
private static readonly TimeSpan LockoutDuration = TimeSpan.FromMinutes(15);
|
||||
private static readonly TimeSpan AttemptWindow = TimeSpan.FromHours(1);
|
||||
|
||||
public IpLockoutService(IMemoryCache cache, ILogger<IpLockoutService> logger)
|
||||
{
|
||||
_cache = cache;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public bool IsLockedOut(string ipAddress)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ipAddress))
|
||||
return false;
|
||||
|
||||
var lockoutKey = GetLockoutKey(ipAddress);
|
||||
if (_cache.TryGetValue(lockoutKey, out DateTime lockoutUntil))
|
||||
{
|
||||
if (DateTime.UtcNow < lockoutUntil)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Lockout period expired, clean up
|
||||
_cache.Remove(lockoutKey);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void RecordFailedAttempt(string ipAddress)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ipAddress))
|
||||
return;
|
||||
|
||||
var attemptsKey = GetAttemptsKey(ipAddress);
|
||||
int currentAttempts = _cache.GetOrCreate(attemptsKey, entry =>
|
||||
{
|
||||
entry.AbsoluteExpirationRelativeToNow = AttemptWindow;
|
||||
return 0;
|
||||
});
|
||||
|
||||
currentAttempts++;
|
||||
_cache.Set(attemptsKey, currentAttempts, AttemptWindow);
|
||||
|
||||
_logger.LogWarning("IP {IpAddress} 로그인 실패 기록됨. 누적 실패 횟수: {Attempts}/{MaxAttempts}",
|
||||
ipAddress, currentAttempts, MaxAttempts);
|
||||
|
||||
if (currentAttempts >= MaxAttempts)
|
||||
{
|
||||
var lockoutUntil = DateTime.UtcNow.Add(LockoutDuration);
|
||||
var lockoutKey = GetLockoutKey(ipAddress);
|
||||
_cache.Set(lockoutKey, lockoutUntil, LockoutDuration);
|
||||
|
||||
_logger.LogCritical("IP {IpAddress}의 로그인 시도가 {MaxAttempts}회 연속 실패하여 {Duration} 동안 로그인 차단됩니다. 차단 해제 예정 일시: {LockoutUntil} UTC",
|
||||
ipAddress, MaxAttempts, LockoutDuration, lockoutUntil);
|
||||
|
||||
// Reset attempts count since they are now locked out
|
||||
_cache.Remove(attemptsKey);
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetAttempts(string ipAddress)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ipAddress))
|
||||
return;
|
||||
|
||||
_cache.Remove(GetAttemptsKey(ipAddress));
|
||||
_cache.Remove(GetLockoutKey(ipAddress));
|
||||
}
|
||||
|
||||
private static string GetAttemptsKey(string ipAddress) => $"IpLockout:Attempts:{ipAddress}";
|
||||
private static string GetLockoutKey(string ipAddress) => $"IpLockout:Lockout:{ipAddress}";
|
||||
}
|
||||
@@ -62,46 +62,46 @@
|
||||
--border-color-light: #E6EDF3;
|
||||
--border-color-strong: #B7C4D1;
|
||||
|
||||
/* Spacing Scale */
|
||||
/* Spacing Scale - Compactized for Douzone Accounting System */
|
||||
--space-0: 0;
|
||||
--space-1: 3px;
|
||||
--space-2: 6px;
|
||||
--space-3: 10px;
|
||||
--space-4: 12px;
|
||||
--space-5: 16px;
|
||||
--space-6: 20px;
|
||||
--space-7: 24px;
|
||||
--space-8: 28px;
|
||||
--space-10: 34px;
|
||||
--space-12: 40px;
|
||||
--space-16: 52px;
|
||||
--space-1: 2px;
|
||||
--space-2: 4px;
|
||||
--space-3: 6px;
|
||||
--space-4: 8px;
|
||||
--space-5: 12px;
|
||||
--space-6: 16px;
|
||||
--space-7: 20px;
|
||||
--space-8: 24px;
|
||||
--space-10: 28px;
|
||||
--space-12: 32px;
|
||||
--space-16: 40px;
|
||||
|
||||
/* Border Radius */
|
||||
--radius-sm: 3px;
|
||||
--radius-md: 6px;
|
||||
--radius-lg: 8px;
|
||||
--radius-xl: 12px;
|
||||
/* Border Radius - Squarish for professional enterprise look */
|
||||
--radius-sm: 2px;
|
||||
--radius-md: 4px;
|
||||
--radius-lg: 6px;
|
||||
--radius-xl: 8px;
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* Typography Scale */
|
||||
/* Typography Scale - High Density Compact Size */
|
||||
--font-family-base: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
--font-size-xs: 0.7rem;
|
||||
--font-size-sm: 0.75rem;
|
||||
--font-size-base: 0.82rem;
|
||||
--font-size-lg: 0.95rem;
|
||||
--font-size-xl: 1.1rem;
|
||||
--font-size-2xl: 1.3rem;
|
||||
--font-size-3xl: 1.6rem;
|
||||
--font-size-4xl: 2rem;
|
||||
--font-size-xs: 0.62rem;
|
||||
--font-size-sm: 0.66rem;
|
||||
--font-size-base: 0.72rem;
|
||||
--font-size-lg: 0.8rem;
|
||||
--font-size-xl: 0.9rem;
|
||||
--font-size-2xl: 1rem;
|
||||
--font-size-3xl: 1.15rem;
|
||||
--font-size-4xl: 1.45rem;
|
||||
|
||||
--font-weight-regular: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-semibold: 600;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
--line-height-tight: 1.2;
|
||||
--line-height-normal: 1.5;
|
||||
--line-height-relaxed: 1.75;
|
||||
--line-height-tight: 1.1;
|
||||
--line-height-normal: 1.25;
|
||||
--line-height-relaxed: 1.4;
|
||||
|
||||
/* Shadow System (Elevation) */
|
||||
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
@@ -427,12 +427,12 @@ html, body {
|
||||
|
||||
.admin-content-card .card-body {
|
||||
min-height: calc(100vh - 140px);
|
||||
padding: 0.75rem 0.9rem !important;
|
||||
padding: var(--space-3) var(--space-4) !important;
|
||||
}
|
||||
|
||||
.admin-page-shell {
|
||||
display: grid;
|
||||
gap: 0;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.details-grid dt {
|
||||
@@ -1201,18 +1201,17 @@ textarea:focus-visible {
|
||||
.admin-quick-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--border-color-light);
|
||||
border-radius: var(--radius-lg);
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: transform var(--transition-base), box-shadow var(--transition-base), border-color var(--transition-base);
|
||||
background: linear-gradient(180deg, #fff 0%, #f8fbff 100%);
|
||||
transition: background-color var(--transition-fast), border-color var(--transition-fast);
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.admin-quick-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
background-color: var(--bg-secondary);
|
||||
border-color: var(--border-color-strong);
|
||||
}
|
||||
|
||||
@@ -1570,12 +1569,12 @@ textarea:focus-visible {
|
||||
}
|
||||
|
||||
.admin-table thead th {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
padding: var(--space-1) var(--space-2);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.admin-table tbody td {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
padding: var(--space-1) var(--space-2);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
@@ -1869,12 +1868,12 @@ aside {
|
||||
.admin-sidebar-card .card-header {
|
||||
background: #1A2F4A !important;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1) !important;
|
||||
padding: var(--space-3) var(--space-4) !important;
|
||||
padding: var(--space-2) var(--space-3) !important;
|
||||
}
|
||||
|
||||
.admin-sidebar-card .card-title {
|
||||
color: #FFFFFF !important;
|
||||
font-size: var(--font-size-base);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
margin: 0;
|
||||
}
|
||||
@@ -1896,11 +1895,11 @@ aside {
|
||||
|
||||
.admin-sidebar-card .accordion-button {
|
||||
font-weight: var(--font-weight-semibold);
|
||||
font-size: var(--font-size-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
transition: all var(--transition-base);
|
||||
}
|
||||
|
||||
@@ -1917,8 +1916,8 @@ aside {
|
||||
|
||||
.admin-sidebar-card .accordion-button::after {
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='rgba(255,255,255,0.6)' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5l6 6 6-6'/%3e%3c/svg%3e");
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@@ -1929,14 +1928,14 @@ aside {
|
||||
.admin-sidebar-card .list-group-item {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
padding: var(--space-1.5) var(--space-3);
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
font-size: var(--font-size-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
background: transparent;
|
||||
transition: all var(--transition-base);
|
||||
text-decoration: none;
|
||||
margin: 0 var(--space-2);
|
||||
margin-left: calc(var(--space-4) + 4px);
|
||||
margin: 0 var(--space-1);
|
||||
margin-left: calc(var(--space-3) + 2px);
|
||||
}
|
||||
|
||||
.admin-sidebar-card .list-group-item:hover {
|
||||
@@ -1949,7 +1948,7 @@ aside {
|
||||
color: #FFFFFF;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
border-left: 3px solid #FFFFFF;
|
||||
padding-left: calc(var(--space-4) + 1px);
|
||||
padding-left: calc(var(--space-3) - 1px);
|
||||
}
|
||||
|
||||
.admin-drawer-footer {
|
||||
@@ -2040,15 +2039,15 @@ aside {
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
/* Touch Target Sizing (WCAG 2.5.5) */
|
||||
/* Touch Target Sizing (Optimized for Douzone Accounting System Desktop Density) */
|
||||
.mud-button,
|
||||
.mud-icon-button,
|
||||
a,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
min-height: 26px;
|
||||
min-width: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user