feat: WTM 멀티프로젝트 플랫폼 구축 (BE + FE 전체 구현)
Phase 0: wbx-spring-core 라이브러리 전환 - java-library 플러그인, WbxAutoConfiguration, Admin 조건부 활성화 - 루트 settings.gradle + build.gradle (멀티모듈) Phase 1: wtm-api 모듈 생성 - 23개 JPA Entity, 14개 Controller, 79개 API 엔드포인트 - Flyway V100~V107 MySQL 마이그레이션 - TimesheetRuleEngine, TimesheetApprovalHandler, P6WbsParser Phase 2: wtm-frontend (Vue 3 + PrimeVue 4) - 10개 도메인 모듈, 17개 View, 5개 서브컴포넌트 - 반응형 레이아웃 (AppLayout, AppSidebar, AppTopbar) - BaseCrudTable, BaseFormDialog, BasePageHeader 표준 컴포넌트 - JWT 인터셉터, 역할 기반 메뉴 필터링 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
이 Commit은 다음에 포함되어 있습니다:
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head><meta charset="UTF-8"><title>WBX Admin - Audit Logs</title><link rel="stylesheet" th:href="@{/admin/css/admin.css}"></head>
|
||||
<body>
|
||||
<div class="admin-layout">
|
||||
<div th:replace="~{admin/fragments :: sidebar('audit-logs')}"></div>
|
||||
<div class="admin-content">
|
||||
<div class="page-header"><h1>감사 로그</h1><p>최근 100건</p></div>
|
||||
<div class="data-table">
|
||||
<table>
|
||||
<thead><tr><th>시간</th><th>사용자ID</th><th>액션</th><th>리소스</th><th>리소스ID</th><th>IP</th><th>상세</th></tr></thead>
|
||||
<tbody>
|
||||
<tr th:each="log : ${logs}">
|
||||
<td th:text="${log.createdAt != null ? #temporals.format(log.createdAt, 'yy-MM-dd HH:mm:ss') : ''}"></td>
|
||||
<td th:text="${log.userId}"></td>
|
||||
<td><span class="badge" th:text="${log.action}"></span></td>
|
||||
<td th:text="${log.resource}"></td>
|
||||
<td th:text="${log.resourceId}"></td>
|
||||
<td th:text="${log.ipAddress}"></td>
|
||||
<td th:text="${log.detail != null ? (#strings.abbreviate(log.detail, 80)) : ''}"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head><meta charset="UTF-8"><title>WBX Admin - System Config</title><link rel="stylesheet" th:href="@{/admin/css/admin.css}"></head>
|
||||
<body>
|
||||
<div class="admin-layout">
|
||||
<div th:replace="~{admin/fragments :: sidebar('config')}"></div>
|
||||
<div class="admin-content">
|
||||
<div class="page-header"><h1>시스템 설정</h1><p>Key-Value 설정 관리</p></div>
|
||||
<div th:if="${message}" class="alert alert-success" th:text="${message}"></div>
|
||||
|
||||
<!-- 설정 추가 폼 -->
|
||||
<div class="card" style="margin-bottom:20px; padding:16px;">
|
||||
<h3>설정 추가/수정</h3>
|
||||
<form th:action="@{/admin/config/save}" method="post" style="display:flex; gap:10px; align-items:end; flex-wrap:wrap;">
|
||||
<div><label>Key</label><input name="configKey" required style="display:block; padding:6px; border:1px solid #ddd; border-radius:4px;"></div>
|
||||
<div><label>Value</label><input name="configValue" required style="display:block; padding:6px; border:1px solid #ddd; border-radius:4px; min-width:200px;"></div>
|
||||
<div><label>설명</label><input name="description" style="display:block; padding:6px; border:1px solid #ddd; border-radius:4px;"></div>
|
||||
<button type="submit" class="btn btn-primary" style="padding:6px 16px;">저장</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 기존 설정 목록 -->
|
||||
<div class="data-table">
|
||||
<table>
|
||||
<thead><tr><th>Key</th><th>Value</th><th>설명</th><th>수정일</th></tr></thead>
|
||||
<tbody>
|
||||
<tr th:each="cfg : ${configs}">
|
||||
<td><code th:text="${cfg.configKey}"></code></td>
|
||||
<td th:text="${cfg.configValue}"></td>
|
||||
<td th:text="${cfg.description}"></td>
|
||||
<td th:text="${cfg.updatedAt != null ? #temporals.format(cfg.updatedAt, 'yy-MM-dd HH:mm') : ''}"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>WBX Admin - Dashboard</title>
|
||||
<link rel="stylesheet" th:href="@{/admin/css/admin.css}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-layout">
|
||||
<div th:replace="~{admin/fragments :: sidebar('dashboard')}"></div>
|
||||
<div class="admin-content">
|
||||
<div class="page-header">
|
||||
<h1>대시보드</h1>
|
||||
<p>WBX Spring Framework 관리 콘솔</p>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="label">활성 사용자</div>
|
||||
<div class="value" th:text="${userCount}">0</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="label">전체 사용자</div>
|
||||
<div class="value" th:text="${totalUsers}">0</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="label">로그인 성공</div>
|
||||
<div class="value green" th:text="${loginCount}">0</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="label">등록 역할</div>
|
||||
<div class="value" th:text="${roleCount}">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-header"><h1>빠른 링크</h1></div>
|
||||
<a th:href="@{/admin/users}" class="btn btn-primary" style="margin-right:8px">사용자 관리</a>
|
||||
<a th:href="@{/admin/roles}" class="btn btn-outline" style="margin-right:8px">역할 관리</a>
|
||||
<a th:href="@{/admin/login-history}" class="btn btn-outline">로그인 이력</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<body>
|
||||
<div th:fragment="sidebar(active)" class="admin-sidebar">
|
||||
<div class="logo">WBX Admin</div>
|
||||
<nav>
|
||||
<a th:href="@{/admin}" th:classappend="${active == 'dashboard'} ? 'active'">📊 대시보드</a>
|
||||
<a th:href="@{/admin/users}" th:classappend="${active == 'users'} ? 'active'">👥 사용자 관리</a>
|
||||
<a th:href="@{/admin/roles}" th:classappend="${active == 'roles'} ? 'active'">🔑 역할/권한</a>
|
||||
<a th:href="@{/admin/login-history}" th:classappend="${active == 'login-history'} ? 'active'">📋 로그인 이력</a>
|
||||
<a th:href="@{/admin/audit-logs}" th:classappend="${active == 'audit-logs'} ? 'active'">📝 감사 로그</a>
|
||||
<a th:href="@{/admin/permissions}" th:classappend="${active == 'permissions'} ? 'active'">🛡 권한 매트릭스</a>
|
||||
<a th:href="@{/admin/config}" th:classappend="${active == 'config'} ? 'active'">⚙ 시스템 설정</a>
|
||||
<a th:href="@{/admin/system-health}" th:classappend="${active == 'system-health'} ? 'active'">💻 시스템 상태</a>
|
||||
<a th:href="@{/admin/logout}" style="margin-top:auto; border-top:1px solid rgba(255,255,255,0.1); padding-top:16px;">🚪 로그아웃</a>
|
||||
</nav>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head><meta charset="UTF-8"><title>WBX Admin - Login History</title><link rel="stylesheet" th:href="@{/admin/css/admin.css}"></head>
|
||||
<body>
|
||||
<div class="admin-layout">
|
||||
<div th:replace="~{admin/fragments :: sidebar('login-history')}"></div>
|
||||
<div class="admin-content">
|
||||
<div class="page-header"><h1>로그인 이력</h1><p>최근 50건</p></div>
|
||||
<div class="data-table">
|
||||
<table>
|
||||
<thead><tr><th>시간</th><th>이메일</th><th>액션</th><th>IP</th><th>인증방법</th><th>사유</th></tr></thead>
|
||||
<tbody>
|
||||
<tr th:each="log : ${logs}">
|
||||
<td th:text="${#temporals.format(log.createdAt, 'yy-MM-dd HH:mm:ss')}"></td>
|
||||
<td th:text="${log.email}"></td>
|
||||
<td>
|
||||
<span th:if="${log.action == 'LOGIN_SUCCESS'}" class="badge badge-success">성공</span>
|
||||
<span th:if="${log.action == 'LOGIN_FAILURE'}" class="badge badge-danger">실패</span>
|
||||
<span th:if="${log.action == 'LOGOUT'}" class="badge badge-info">로그아웃</span>
|
||||
</td>
|
||||
<td th:text="${log.ipAddress}"></td>
|
||||
<td th:text="${log.authMethod}"></td>
|
||||
<td th:text="${log.failureReason}"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>WBX Admin - Login</title>
|
||||
<link rel="stylesheet" th:href="@{/admin/css/admin.css}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<div class="login-box">
|
||||
<h2>WBX Admin</h2>
|
||||
<div th:if="${param.error}" class="error">이메일 또는 비밀번호가 올바르지 않습니다.</div>
|
||||
<div th:if="${param.logout}" class="alert alert-info">로그아웃되었습니다.</div>
|
||||
<form th:action="@{/admin/login}" method="post">
|
||||
<input type="email" name="username" placeholder="이메일" required autofocus>
|
||||
<input type="password" name="password" placeholder="비밀번호" required>
|
||||
<button type="submit">로그인</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head><meta charset="UTF-8"><title>WBX Admin - Permissions</title><link rel="stylesheet" th:href="@{/admin/css/admin.css}"></head>
|
||||
<body>
|
||||
<div class="admin-layout">
|
||||
<div th:replace="~{admin/fragments :: sidebar('permissions')}"></div>
|
||||
<div class="admin-content">
|
||||
<div class="page-header">
|
||||
<h1>권한 매트릭스</h1>
|
||||
<p>전체 역할-모듈-액션 권한 현황. 개별 역할 편집은 <a th:href="@{/admin/roles}" style="color:#1e3c78;">역할 관리</a>에서 가능합니다.</p>
|
||||
</div>
|
||||
<div class="data-table">
|
||||
<table>
|
||||
<thead><tr><th>역할</th><th>모듈</th><th>액션</th><th>범위</th><th>관리</th></tr></thead>
|
||||
<tbody>
|
||||
<tr th:each="perm : ${permissions}">
|
||||
<td>
|
||||
<span th:each="role : ${roles}" th:if="${role.id == perm.roleId}">
|
||||
<a th:href="@{/admin/roles/{id}(id=${role.id})}" class="badge badge-info" style="text-decoration:none;" th:text="${role.code}"></a>
|
||||
</span>
|
||||
</td>
|
||||
<td th:text="${perm.module}"></td>
|
||||
<td th:text="${perm.action}"></td>
|
||||
<td>
|
||||
<span class="badge" th:classappend="${perm.deptScope?.name() == 'COMPANY'} ? 'badge-success' : (${perm.deptScope?.name() == 'DEPT'} ? 'badge-warning' : '')"
|
||||
th:text="${perm.deptScope?.name()}"></span>
|
||||
</td>
|
||||
<td>
|
||||
<form th:action="@{/admin/permissions/{id}/delete(id=${perm.id})}" method="post" style="display:inline;" onsubmit="return confirm('삭제하시겠습니까?')">
|
||||
<button type="submit" class="btn btn-danger" style="padding:2px 8px;font-size:11px;">삭제</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(permissions)}"><td colspan="5" style="text-align:center;color:#888;padding:20px;">등록된 권한이 없습니다. 역할 관리에서 권한을 추가하세요.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head><meta charset="UTF-8"><title>WBX Admin - Role Detail</title><link rel="stylesheet" th:href="@{/admin/css/admin.css}"></head>
|
||||
<body>
|
||||
<div class="admin-layout">
|
||||
<div th:replace="~{admin/fragments :: sidebar('roles')}"></div>
|
||||
<div class="admin-content" th:if="${role}">
|
||||
<div class="page-header">
|
||||
<h1>역할 상세: <span th:text="${role.code}"></span></h1>
|
||||
<p><a th:href="@{/admin/roles}" style="color:#1e3c78;">← 역할 목록으로</a></p>
|
||||
</div>
|
||||
<div th:if="${message}" class="alert alert-success" th:text="${message}"></div>
|
||||
<div th:if="${error}" class="alert" style="background:#ffebee;color:#c62828;border:1px solid #ffcdd2;padding:12px 16px;border-radius:6px;margin-bottom:16px;font-size:13px;" th:text="${error}"></div>
|
||||
|
||||
<!-- 역할 정보 수정 -->
|
||||
<div style="background:#fff;border-radius:8px;padding:20px;margin-bottom:20px;box-shadow:0 1px 3px rgba(0,0,0,0.08);">
|
||||
<h3 style="margin-bottom:12px;font-size:15px;color:#1e3c78;">역할 정보</h3>
|
||||
<form th:action="@{/admin/roles/{id}/update(id=${role.id})}" method="post" style="display:flex;gap:10px;align-items:end;flex-wrap:wrap;">
|
||||
<div>
|
||||
<label style="font-size:12px;color:#888;display:block;margin-bottom:4px;">코드 (변경 불가)</label>
|
||||
<input th:value="${role.code}" disabled style="padding:6px 10px;border:1px solid #eee;border-radius:4px;font-size:13px;background:#f5f5f5;width:140px;">
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:12px;color:#888;display:block;margin-bottom:4px;">이름</label>
|
||||
<input name="name" th:value="${role.name}" required style="padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;width:160px;">
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:12px;color:#888;display:block;margin-bottom:4px;">설명</label>
|
||||
<input name="description" th:value="${role.description}" style="padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;width:280px;">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">수정</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 권한 추가 -->
|
||||
<div style="background:#fff;border-radius:8px;padding:20px;margin-bottom:20px;box-shadow:0 1px 3px rgba(0,0,0,0.08);">
|
||||
<h3 style="margin-bottom:12px;font-size:15px;color:#1e3c78;">권한 추가</h3>
|
||||
<form th:action="@{/admin/roles/{id}/permissions/add(id=${role.id})}" method="post" style="display:flex;gap:10px;align-items:end;flex-wrap:wrap;">
|
||||
<div>
|
||||
<label style="font-size:12px;color:#888;display:block;margin-bottom:4px;">모듈</label>
|
||||
<input name="module" required placeholder="예: TIMESHEET" style="padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;width:160px;">
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:12px;color:#888;display:block;margin-bottom:4px;">액션</label>
|
||||
<select name="action" style="padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;">
|
||||
<option value="VIEW">VIEW (조회)</option>
|
||||
<option value="CREATE">CREATE (생성)</option>
|
||||
<option value="UPDATE">UPDATE (수정)</option>
|
||||
<option value="DELETE">DELETE (삭제)</option>
|
||||
<option value="APPROVE">APPROVE (결재)</option>
|
||||
<option value="EXPORT">EXPORT (내보내기)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:12px;color:#888;display:block;margin-bottom:4px;">데이터 범위</label>
|
||||
<select name="deptScope" style="padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;">
|
||||
<option th:each="s : ${deptScopes}" th:value="${s.name()}" th:text="${s.name() == 'OWN' ? 'OWN (본인)' : (s.name() == 'DEPT' ? 'DEPT (부서)' : 'COMPANY (전사)')}"></option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">추가</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 현재 권한 목록 -->
|
||||
<div class="data-table">
|
||||
<table>
|
||||
<thead><tr><th>모듈</th><th>액션</th><th>범위</th><th>관리</th></tr></thead>
|
||||
<tbody>
|
||||
<tr th:each="perm : ${permissions}">
|
||||
<td th:text="${perm.module}"></td>
|
||||
<td><span class="badge badge-info" th:text="${perm.action}"></span></td>
|
||||
<td>
|
||||
<span class="badge" th:classappend="${perm.deptScope?.name() == 'COMPANY'} ? 'badge-success' : (${perm.deptScope?.name() == 'DEPT'} ? 'badge-warning' : '')"
|
||||
th:text="${perm.deptScope?.name()}"></span>
|
||||
</td>
|
||||
<td>
|
||||
<form th:action="@{/admin/permissions/{id}/delete(id=${perm.id})}" method="post" style="display:inline;" onsubmit="return confirm('이 권한을 삭제하시겠습니까?')">
|
||||
<button type="submit" class="btn btn-danger" style="padding:3px 10px;font-size:12px;">삭제</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(permissions)}"><td colspan="4" style="text-align:center;color:#888;padding:20px;">설정된 권한이 없습니다. 위 폼에서 권한을 추가하세요.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head><meta charset="UTF-8"><title>WBX Admin - Roles</title><link rel="stylesheet" th:href="@{/admin/css/admin.css}"></head>
|
||||
<body>
|
||||
<div class="admin-layout">
|
||||
<div th:replace="~{admin/fragments :: sidebar('roles')}"></div>
|
||||
<div class="admin-content">
|
||||
<div class="page-header"><h1>역할 관리</h1><p>역할 추가/수정/삭제 및 권한 설정</p></div>
|
||||
<div th:if="${message}" class="alert alert-success" th:text="${message}"></div>
|
||||
<div th:if="${error}" class="alert" style="background:#ffebee;color:#c62828;border:1px solid #ffcdd2;padding:12px 16px;border-radius:6px;margin-bottom:16px;font-size:13px;" th:text="${error}"></div>
|
||||
|
||||
<!-- 역할 추가 폼 -->
|
||||
<div style="background:#fff;border-radius:8px;padding:16px;margin-bottom:20px;box-shadow:0 1px 3px rgba(0,0,0,0.08);">
|
||||
<h3 style="margin-bottom:12px;font-size:15px;color:#1e3c78;">새 역할 추가</h3>
|
||||
<form th:action="@{/admin/roles/add}" method="post" style="display:flex;gap:10px;align-items:end;flex-wrap:wrap;">
|
||||
<div>
|
||||
<label style="font-size:12px;color:#888;display:block;margin-bottom:4px;">코드</label>
|
||||
<input name="code" required placeholder="예: MANAGER" style="padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;width:140px;">
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:12px;color:#888;display:block;margin-bottom:4px;">이름</label>
|
||||
<input name="name" required placeholder="예: 관리자" style="padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;width:160px;">
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:12px;color:#888;display:block;margin-bottom:4px;">설명</label>
|
||||
<input name="description" placeholder="역할 설명" style="padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;width:240px;">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">추가</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 역할 목록 -->
|
||||
<div class="data-table">
|
||||
<table>
|
||||
<thead><tr><th>ID</th><th>코드</th><th>이름</th><th>설명</th><th>시스템</th><th>관리</th></tr></thead>
|
||||
<tbody>
|
||||
<tr th:each="role : ${roles}">
|
||||
<td th:text="${role.id}"></td>
|
||||
<td><span class="badge badge-info" th:text="${role.code}"></span></td>
|
||||
<td th:text="${role.name}"></td>
|
||||
<td th:text="${role.description}"></td>
|
||||
<td th:text="${role.system} ? 'Y' : ''"></td>
|
||||
<td>
|
||||
<a th:href="@{/admin/roles/{id}(id=${role.id})}" class="btn btn-outline" style="padding:3px 10px;font-size:12px;">상세/권한</a>
|
||||
<form th:unless="${role.system}" th:action="@{/admin/roles/{id}/delete(id=${role.id})}" method="post" style="display:inline;" onsubmit="return confirm('삭제하시겠습니까?')">
|
||||
<button type="submit" class="btn btn-danger" style="padding:3px 10px;font-size:12px;">삭제</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(roles)}"><td colspan="6" style="text-align:center;color:#888">등록된 역할이 없습니다.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head><meta charset="UTF-8"><title>WBX Admin - System Health</title><link rel="stylesheet" th:href="@{/admin/css/admin.css}"></head>
|
||||
<body>
|
||||
<div class="admin-layout">
|
||||
<div th:replace="~{admin/fragments :: sidebar('system-health')}"></div>
|
||||
<div class="admin-content">
|
||||
<div class="page-header"><h1>시스템 상태</h1><p>서버 런타임 정보</p></div>
|
||||
<div class="data-table">
|
||||
<table>
|
||||
<thead><tr><th>항목</th><th>값</th></tr></thead>
|
||||
<tbody>
|
||||
<tr th:each="entry : ${health}">
|
||||
<td th:text="${entry.key}"></td>
|
||||
<td th:text="${entry.value}"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,154 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head><meta charset="UTF-8"><title>WBX Admin - User Detail</title><link rel="stylesheet" th:href="@{/admin/css/admin.css}"></head>
|
||||
<body>
|
||||
<div class="admin-layout">
|
||||
<div th:replace="~{admin/fragments :: sidebar('users')}"></div>
|
||||
<div class="admin-content" th:if="${user}">
|
||||
<div class="page-header">
|
||||
<h1>사용자 상세: <span th:text="${user.email}"></span></h1>
|
||||
<p><a th:href="@{/admin/users}" style="color:#1e3c78;">← 사용자 목록으로</a></p>
|
||||
</div>
|
||||
<div th:if="${message}" class="alert alert-success" th:text="${message}"></div>
|
||||
<div th:if="${error}" class="alert" style="background:#ffebee;color:#c62828;border:1px solid #ffcdd2;padding:12px 16px;border-radius:6px;margin-bottom:16px;font-size:13px;" th:text="${error}"></div>
|
||||
|
||||
<!-- 사용자 정보 수정 폼 -->
|
||||
<div style="background:#fff;border-radius:8px;padding:20px;margin-bottom:20px;box-shadow:0 1px 3px rgba(0,0,0,0.08);">
|
||||
<h3 style="margin-bottom:12px;font-size:15px;color:#1e3c78;">기본 정보</h3>
|
||||
<form th:action="@{/admin/users/{id}/update(id=${user.id})}" method="post">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:16px;">
|
||||
<div>
|
||||
<label style="font-size:12px;color:#888;display:block;margin-bottom:4px;">이메일 (변경 불가)</label>
|
||||
<input th:value="${user.email}" disabled style="width:100%;padding:6px 10px;border:1px solid #eee;border-radius:4px;font-size:13px;background:#f5f5f5;">
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:12px;color:#888;display:block;margin-bottom:4px;">사용자명 (변경 불가)</label>
|
||||
<input th:value="${user.username}" disabled style="width:100%;padding:6px 10px;border:1px solid #eee;border-radius:4px;font-size:13px;background:#f5f5f5;">
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:12px;color:#888;display:block;margin-bottom:4px;">이름</label>
|
||||
<input name="fullName" th:value="${user.fullName}" style="width:100%;padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;">
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:12px;color:#888;display:block;margin-bottom:4px;">전화번호</label>
|
||||
<input name="phone" th:value="${user.phone}" style="width:100%;padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;">
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:12px;color:#888;display:block;margin-bottom:4px;">직위</label>
|
||||
<input name="positionTitle" th:value="${user.positionTitle}" style="width:100%;padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;">
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:12px;color:#888;display:block;margin-bottom:4px;">사번</label>
|
||||
<input name="employeeNumber" th:value="${user.employeeNumber}" style="width:100%;padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;">
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:16px;">
|
||||
<label style="display:flex;align-items:center;gap:4px;font-size:13px;">
|
||||
<input name="isAdmin" type="checkbox" value="true" th:checked="${user.admin}"> 관리자 (SA)
|
||||
</label>
|
||||
<button type="submit" class="btn btn-primary">정보 수정</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 상태 정보 + 액션 버튼 -->
|
||||
<div style="background:#fff;border-radius:8px;padding:20px;margin-bottom:20px;box-shadow:0 1px 3px rgba(0,0,0,0.08);">
|
||||
<h3 style="margin-bottom:12px;font-size:15px;color:#1e3c78;">계정 상태</h3>
|
||||
<div class="detail-grid" style="box-shadow:none;padding:0;margin-bottom:12px;">
|
||||
<div class="label">상태</div>
|
||||
<div class="value">
|
||||
<span th:if="${user.active}" class="badge badge-success">활성</span>
|
||||
<span th:unless="${user.active}" class="badge badge-danger">비활성</span>
|
||||
</div>
|
||||
<div class="label">잠금</div>
|
||||
<div class="value">
|
||||
<span th:if="${user.locked}" class="badge badge-danger">잠금 (실패 <span th:text="${user.failedLoginAttempts}"></span>회)</span>
|
||||
<span th:unless="${user.locked}">정상</span>
|
||||
</div>
|
||||
<div class="label">MFA</div>
|
||||
<div class="value" th:text="${user.mfaEnabled} ? '활성화' : '미설정'"></div>
|
||||
<div class="label">SSO</div>
|
||||
<div class="value" th:text="${user.ssoProvider != null} ? ${user.ssoProvider} : '미연동'"></div>
|
||||
<div class="label">최종 로그인</div>
|
||||
<div class="value" th:text="${user.lastLoginAt != null} ? ${#temporals.format(user.lastLoginAt, 'yyyy-MM-dd HH:mm:ss')} : '없음'"></div>
|
||||
<div class="label">생성일</div>
|
||||
<div class="value" th:text="${user.createdAt != null} ? ${#temporals.format(user.createdAt, 'yyyy-MM-dd HH:mm')} : ''"></div>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;">
|
||||
<form th:action="@{/admin/users/{id}/unlock(id=${user.id})}" method="post" style="display:inline">
|
||||
<button class="btn btn-primary" type="submit">잠금 해제</button>
|
||||
</form>
|
||||
<form th:action="@{/admin/users/{id}/reset-password(id=${user.id})}" method="post" style="display:inline">
|
||||
<button class="btn btn-outline" type="submit">비밀번호 초기화</button>
|
||||
</form>
|
||||
<form th:action="@{/admin/users/{id}/toggle-status(id=${user.id})}" method="post" style="display:inline">
|
||||
<button class="btn" th:classappend="${user.active} ? 'btn-danger' : 'btn-primary'" type="submit" th:text="${user.active} ? '비활성화' : '활성화'"></button>
|
||||
</form>
|
||||
<form th:action="@{/admin/users/{id}/delete(id=${user.id})}" method="post" style="display:inline;margin-left:auto;" onsubmit="return confirm('이 사용자를 삭제하시겠습니까? 복구할 수 없습니다.')">
|
||||
<button class="btn btn-danger" type="submit">사용자 삭제</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 역할 할당 -->
|
||||
<div style="background:#fff;border-radius:8px;padding:20px;margin-bottom:20px;box-shadow:0 1px 3px rgba(0,0,0,0.08);">
|
||||
<h3 style="margin-bottom:12px;font-size:15px;color:#1e3c78;">역할 할당</h3>
|
||||
<form th:action="@{/admin/users/{id}/roles/add(id=${user.id})}" method="post" style="display:flex;gap:10px;align-items:end;margin-bottom:16px;">
|
||||
<div>
|
||||
<label style="font-size:12px;color:#888;display:block;margin-bottom:4px;">역할 선택</label>
|
||||
<select name="roleId" style="padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;">
|
||||
<option th:each="role : ${allRoles}" th:value="${role.id}" th:text="${role.code + ' — ' + role.name}"></option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">역할 추가</button>
|
||||
</form>
|
||||
<div class="data-table" th:if="${!userRoles.isEmpty()}">
|
||||
<table>
|
||||
<thead><tr><th>역할 코드</th><th>역할 이름</th><th>할당일</th><th>관리</th></tr></thead>
|
||||
<tbody>
|
||||
<tr th:each="ur : ${userRoles}">
|
||||
<td>
|
||||
<span th:each="role : ${allRoles}" th:if="${role.id == ur.roleId}" class="badge badge-info" th:text="${role.code}"></span>
|
||||
</td>
|
||||
<td>
|
||||
<span th:each="role : ${allRoles}" th:if="${role.id == ur.roleId}" th:text="${role.name}"></span>
|
||||
</td>
|
||||
<td th:text="${ur.grantedAt != null} ? ${#temporals.format(ur.grantedAt, 'yy-MM-dd HH:mm')} : ''"></td>
|
||||
<td>
|
||||
<form th:action="@{/admin/users/{uid}/roles/{urid}/delete(uid=${user.id},urid=${ur.id})}" method="post" style="display:inline;" onsubmit="return confirm('이 역할을 해제하시겠습니까?')">
|
||||
<button type="submit" class="btn btn-danger" style="padding:3px 10px;font-size:12px;">해제</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p th:if="${userRoles.isEmpty()}" style="color:#888;font-size:13px;">할당된 역할이 없습니다.</p>
|
||||
</div>
|
||||
|
||||
<!-- 로그인 이력 -->
|
||||
<div style="background:#fff;border-radius:8px;padding:20px;box-shadow:0 1px 3px rgba(0,0,0,0.08);">
|
||||
<h3 style="margin-bottom:12px;font-size:15px;color:#1e3c78;">로그인 이력 (최근 10건)</h3>
|
||||
<div class="data-table">
|
||||
<table>
|
||||
<thead><tr><th>시간</th><th>액션</th><th>IP</th><th>인증방법</th></tr></thead>
|
||||
<tbody>
|
||||
<tr th:each="log : ${loginHistory}">
|
||||
<td th:text="${#temporals.format(log.createdAt, 'yy-MM-dd HH:mm')}"></td>
|
||||
<td>
|
||||
<span th:if="${log.action == 'LOGIN_SUCCESS'}" class="badge badge-success">성공</span>
|
||||
<span th:if="${log.action == 'LOGIN_FAILURE'}" class="badge badge-danger">실패</span>
|
||||
<span th:if="${log.action == 'LOGOUT'}" class="badge badge-info">로그아웃</span>
|
||||
</td>
|
||||
<td th:text="${log.ipAddress}"></td>
|
||||
<td th:text="${log.authMethod}"></td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(loginHistory)}"><td colspan="4" style="text-align:center;color:#888;">이력이 없습니다.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,73 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head><meta charset="UTF-8"><title>WBX Admin - Users</title><link rel="stylesheet" th:href="@{/admin/css/admin.css}"></head>
|
||||
<body>
|
||||
<div class="admin-layout">
|
||||
<div th:replace="~{admin/fragments :: sidebar('users')}"></div>
|
||||
<div class="admin-content">
|
||||
<div class="page-header"><h1>사용자 관리</h1><p>사용자 추가/수정/삭제 및 역할 할당</p></div>
|
||||
<div th:if="${message}" class="alert alert-success" th:text="${message}"></div>
|
||||
<div th:if="${error}" class="alert" style="background:#ffebee;color:#c62828;border:1px solid #ffcdd2;padding:12px 16px;border-radius:6px;margin-bottom:16px;font-size:13px;" th:text="${error}"></div>
|
||||
|
||||
<!-- 사용자 추가 폼 -->
|
||||
<div style="background:#fff;border-radius:8px;padding:16px;margin-bottom:20px;box-shadow:0 1px 3px rgba(0,0,0,0.08);">
|
||||
<h3 style="margin-bottom:12px;font-size:15px;color:#1e3c78;">새 사용자 추가</h3>
|
||||
<form th:action="@{/admin/users/add}" method="post" style="display:flex;gap:8px;align-items:end;flex-wrap:wrap;">
|
||||
<div>
|
||||
<label style="font-size:12px;color:#888;display:block;margin-bottom:4px;">이메일 *</label>
|
||||
<input name="email" type="email" required placeholder="user@company.com" style="padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;width:180px;">
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:12px;color:#888;display:block;margin-bottom:4px;">사용자명 *</label>
|
||||
<input name="username" required placeholder="hong" style="padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;width:120px;">
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:12px;color:#888;display:block;margin-bottom:4px;">비밀번호 *</label>
|
||||
<input name="password" type="password" required placeholder="********" style="padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;width:130px;">
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:12px;color:#888;display:block;margin-bottom:4px;">이름</label>
|
||||
<input name="fullName" placeholder="홍길동" style="padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;width:100px;">
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:12px;color:#888;display:block;margin-bottom:4px;">전화</label>
|
||||
<input name="phone" placeholder="010-0000-0000" style="padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;width:120px;">
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:4px;padding-bottom:2px;">
|
||||
<input name="isAdmin" type="checkbox" value="true" id="chkAdmin">
|
||||
<label for="chkAdmin" style="font-size:12px;color:#888;">관리자</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">추가</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 사용자 목록 -->
|
||||
<div class="data-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>ID</th><th>이메일</th><th>사용자명</th><th>이름</th><th>관리자</th><th>상태</th><th>잠금</th><th>최종 로그인</th><th>관리</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="user : ${users}">
|
||||
<td th:text="${user.id}"></td>
|
||||
<td th:text="${user.email}"></td>
|
||||
<td th:text="${user.username}"></td>
|
||||
<td th:text="${user.fullName}"></td>
|
||||
<td><span th:if="${user.admin}" class="badge badge-info">SA</span></td>
|
||||
<td>
|
||||
<span th:if="${user.active}" class="badge badge-success">활성</span>
|
||||
<span th:unless="${user.active}" class="badge badge-danger">비활성</span>
|
||||
</td>
|
||||
<td><span th:if="${user.locked}" class="badge badge-danger">잠금</span></td>
|
||||
<td th:text="${user.lastLoginAt != null} ? ${#temporals.format(user.lastLoginAt, 'yy-MM-dd HH:mm')} : '-'"></td>
|
||||
<td style="white-space:nowrap;">
|
||||
<a th:href="@{/admin/users/{id}(id=${user.id})}" class="btn btn-outline" style="padding:3px 10px;font-size:12px;">상세</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
새 Issue에서 참조
사용자 차단