이용자 관리 - 목록 스타일 적용

This commit is contained in:
현성필
2025-12-04 14:34:57 +09:00
parent 4f7aa0327e
commit 2a6c40b3f6
7 changed files with 564 additions and 586 deletions
@@ -1,314 +1,288 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_title_layout}">
<body>
<section layout:fragment="title">
<div class="page-title-banner">
<img th:src="@{/img/img_title_bg.png}" class="title-image">
<h1>이용자 관리</h1>
</div>
</section>
<th:block layout:fragment="contentFragment">
<section class="user-management-container">
<section class="user-management-container">
<!-- Header Section -->
<div class="user-management-header">
<div class="user-management-title">
<h1>사용자 관리</h1>
<h2>이용자 목록</h2>
</div>
<div class="user-management-actions">
<button type="button" class="btn btn-primary" id="addUserBtn">
<i class="fas fa-plus"></i>
<span>이용자 추가</span>
</button>
</div>
</div>
<!-- App List Title Section -->
<div class="table-controls">
<h2 class="total-count">
<strong th:text="${(users != null ? users.size() : 0) + (pendingUsers != null ? pendingUsers.size() : 0)}">0</strong>
</h2>
<div class="search-box">
<button type="button" class="btn btn-primary" id="addUserBtn">
<i class="fas fa-plus"></i>
<span>이용자 추가</span>
</button>
</div>
</div>
<!-- Table Controls -->
<div class="table-controls">
<div class="total-count">
<strong th:text="${(users != null ? users.size() : 0) + (pendingUsers != null ? pendingUsers.size() : 0)}">0</strong>
</div>
</div>
<!-- User Table -->
<div class="list-table" th:if="${(users != null and !users.isEmpty()) or (pendingUsers != null and !pendingUsers.isEmpty())}">
<!-- Table Header -->
<div class="list-table-header">
<div class="header-cell" style="width: 90px;">NO.</div>
<div class="header-cell" style="width: 90px;">성명</div>
<div class="header-cell" style="flex: 1; min-width: 200px;">이메일 아이디</div>
<div class="header-cell" style="width: 120px;">권한</div>
<div class="header-cell" style="width: 120px;">계정상태</div>
<div class="header-cell" style="width: 240px;">설정</div>
</div>
<!-- User Table -->
<div class="user-list-wrapper" th:if="${(users != null and !users.isEmpty()) or (pendingUsers != null and !pendingUsers.isEmpty())}">
<div class="user-table">
<!-- Table Header -->
<div class="table-header">
<div class="col-num">NO</div>
<div class="col-name">성명</div>
<div class="col-email">이메일 아이디</div>
<div class="col-role">권한</div>
<div class="col-status">계정상태</div>
<div class="col-actions">설정</div>
</div>
<!-- Active/Inactive Users -->
<div class="table-row" th:each="user, status : ${users}">
<div class="col-num" th:text="${status.index + 1}">1</div>
<div class="col-name">
<a th:href="@{/users/detail(id=${user.id})}" th:text="${user.maskedUserName}">
사용자 이름
</a>
</div>
<div class="col-email" th:text="${user.maskedEmailAddr}">
user@example.com
</div>
<div class="col-role">
<span class="user-role-badge" th:if="${user.roleCode != null}">
<i class="fas fa-shield-alt"></i>
<span th:text="#{${user.roleCode}}">권한</span>
</span>
</div>
<div class="col-status">
<span class="user-status-badge"
th:classappend="${user.userStatus.toString() == 'ACTIVE'} ? 'status-active' : (${user.userStatus.toString() == 'PENDING'} ? 'status-pending' : 'status-inactive')"
th:text="${user.userStatus.getDescription()}">
활성
</span>
</div>
<div class="col-actions">
<!-- Current User -->
<div class="user-current-badge" th:if="${user.id == currentUserId}">
<!-- Table Body -->
<div class="list-table-body">
<!-- Active/Inactive Users -->
<div class="list-table-row" th:each="user, status : ${users}">
<div class="row-cell" style="width: 90px;" data-label="NO" th:text="${status.index + 1}">1</div>
<div class="row-cell" style="width: 90px;" data-label="성명">
<a th:href="@{/users/detail(id=${user.id})}" th:text="${user.maskedUserName}">
사용자 이름
</a>
</div>
<div class="row-cell" style="flex: 1; min-width: 200px;" data-label="이메일" th:text="${user.maskedEmailAddr}">
user@example.com
</div>
<div class="row-cell" style="width: 120px;" data-label="권한">
<span th:if="${user.roleCode != null}" th:text="#{${user.roleCode}}">권한</span>
</div>
<div class="row-cell" style="width: 120px;" data-label="계정상태" th:text="${user.userStatus.getDescription()}">
활성
</div>
<div class="row-cell row-actions" style="width: 240px;">
<!-- Current User -->
<span class="user-current-badge" th:if="${user.id == currentUserId}">
<i class="fas fa-user-check"></i> 현재 사용자
</div>
<!-- Action Buttons -->
<div class="user-action-buttons" th:if="${user.roleCode != null and user.id != currentUserId}">
<button type="button" class="btn btn-sm btn-secondary inactivate_user"
th:if="${user.userStatus.toString() == 'ACTIVE'}"
th:data-user-id="${user.id}"
th:data-user-name="${user.maskedUserName}">
비활성화
</button>
<button type="button" class="btn btn-sm btn-primary activate_user"
th:if="${user.userStatus.toString() != 'ACTIVE'}"
th:data-user-id="${user.id}"
th:data-user-name="${user.maskedUserName}">
활성화
</button>
<button type="button" class="btn btn-sm btn-secondary cancel_invitation"
th:if="${user.userStatus.toString() == 'PENDING'}"
th:data-user-id="${user.id}"
th:data-user-email="${user.maskedEmailAddr}">
초대취소
</button>
<button type="button" class="btn btn-sm btn-warning assign_manager"
th:if="${user.userStatus.toString() == 'ACTIVE' && user.roleCode.toString() == 'ROLE_CORP_USER'}"
th:data-user-id="${user.id}"
th:data-user-name="${user.maskedUserName}"
th:data-user-email="${user.maskedEmailAddr}">
관리자 권한
</button>
</div>
</div>
</div>
<!-- Pending Invitation Users -->
<div class="table-row row-pending" th:each="user : ${pendingUsers}">
<div class="col-num"></div>
<div class="col-name">
<span class="pending-label">
<i class="fas fa-envelope"></i> 초대 대기중
</span>
</div>
<div class="col-email" th:text="${user.maskedEmailAddr}">
pending@example.com
</div>
<div class="col-role">-</div>
<div class="col-status">
<span class="user-status-badge status-pending">
초대중
</span>
</div>
<div class="col-actions">
<div class="user-action-buttons">
<button type="button" class="btn btn-sm btn-secondary cancel_invitation"
th:data-user-id="${user.id}"
th:data-user-email="${user.maskedEmailAddr}">
초대취소
</button>
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<th:block th:if="${user.roleCode != null and user.id != currentUserId}">
<button type="button" class="list-table-btn list-table-btn--default assign_manager"
th:if="${user.userStatus.toString() == 'ACTIVE' && user.roleCode.toString() == 'ROLE_CORP_USER'}"
th:data-user-id="${user.id}"
th:data-user-name="${user.maskedUserName}"
th:data-user-email="${user.maskedEmailAddr}">
관리자 권한부여
</button>
<button type="button" class="list-table-btn list-table-btn--primary activate_user"
th:if="${user.userStatus.toString() != 'ACTIVE'}"
th:data-user-id="${user.id}"
th:data-user-name="${user.maskedUserName}">
활성화
</button>
<button type="button" class="list-table-btn list-table-btn--primary inactivate_user"
th:if="${user.userStatus.toString() == 'ACTIVE'}"
th:data-user-id="${user.id}"
th:data-user-name="${user.maskedUserName}">
비활성화
</button>
<button type="button" class="list-table-btn list-table-btn--secondary cancel_invitation"
th:if="${user.userStatus.toString() == 'PENDING'}"
th:data-user-id="${user.id}"
th:data-user-email="${user.maskedEmailAddr}">
초대취소
</button>
</th:block>
</div>
</div>
<!-- Empty State -->
<div class="user-empty-state" th:if="${(users == null or users.isEmpty()) and (pendingUsers == null or pendingUsers.isEmpty())}">
<div class="empty-icon">👥</div>
<h3>등록된 이용자가 없습니다</h3>
<p>새로운 이용자를 추가하여 시작하세요.</p>
<button type="button" class="btn btn-primary" id="addUserBtnEmpty">
<i class="fas fa-plus"></i>
<span>이용자 추가하기</span>
<!-- Pending Invitation Users -->
<div class="list-table-row" th:each="user : ${pendingUsers}">
<div class="row-cell" style="width: 90px;" data-label="NO">-</div>
<div class="row-cell" style="width: 90px;" data-label="성명">-</div>
<div class="row-cell" style="flex: 1; min-width: 200px;" data-label="이메일" th:text="${user.maskedEmailAddr}">
pending@example.com
</div>
<div class="row-cell" style="width: 120px;" data-label="권한">이용자</div>
<div class="row-cell" style="width: 120px;" data-label="계정상태">초대중</div>
<div class="row-cell row-actions" style="width: 240px;">
<button type="button" class="list-table-btn list-table-btn--secondary cancel_invitation"
th:data-user-id="${user.id}"
th:data-user-email="${user.maskedEmailAddr}">
초대취소
</button>
</div>
</div>
</div>
</div>
<!-- Pagination -->
<div class="pagination page_top"
th:replace="~{fragment/pagination :: pagination(jsFunction='fn_select_page')}"></div>
<!-- Empty State -->
<div class="user-empty-state" th:if="${(users == null or users.isEmpty()) and (pendingUsers == null or pendingUsers.isEmpty())}">
<div class="empty-icon">👥</div>
<h3>등록된 이용자가 없습니다</h3>
<p>새로운 이용자를 추가하여 시작하세요.</p>
</div>
</section>
<!-- Pagination -->
<div class="pagination page_top"
th:replace="~{fragment/pagination :: pagination(jsFunction='fn_select_page')}"></div>
</section>
</th:block>
<section layout:fragment="pagePopups" class="content pop">
<!-- User Invite Modal -->
<div th:replace="~{fragment/popup/userInvitePopup :: userInvitePopup}"></div>
<!-- Change Role Modal -->
<div th:replace="~{fragment/popup/changeRolePopup :: changeRolePopup}"></div>
<!-- User Invite Modal -->
<div th:replace="~{fragment/popup/userInvitePopup :: userInvitePopup}"></div>
<!-- Change Role Modal -->
<div th:replace="~{fragment/popup/changeRolePopup :: changeRolePopup}"></div>
</section>
</body>
<th:block layout:fragment="contentScript">
<script>
$.ajaxSetup({
beforeSend: function (xhr) {
xhr.setRequestHeader("X-XSRF-TOKEN", '[[${_csrf.token}]]');
}
<script>
$.ajaxSetup({
beforeSend: function(xhr) {
xhr.setRequestHeader('X-XSRF-TOKEN', '[[${_csrf.token}]]');
}
});
document.addEventListener('DOMContentLoaded', function() {
// Function to open the user invite popup
function openUserInvitePopup() {
customPopups.showUserInvite({
onConfirm: function(email) {
$.ajax({
url: '/users/invite',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({emailAddr: email})
}).done(function(response) {
customPopups.hideUserInvite();
customPopups.showAlert(response.msg, function() {
location.reload();
});
}).fail(function(jqXHR, textStatus, errorThrown) {
let errorMessage;
try {
const errorResponse = JSON.parse(jqXHR.responseText);
errorMessage = errorResponse.msg || '이용자 초대 중 오류가 발생했습니다';
} catch (e) {
errorMessage = '이용자 초대 중 오류가 발생했습니다: ' + errorThrown;
}
customPopups.showUserInviteError(errorMessage);
});
},
onCancel: function() {
// Optional: handle cancel action
}
});
}
document.addEventListener('DOMContentLoaded', function () {
// Function to open the user invite popup
function openUserInvitePopup() {
customPopups.showUserInvite({
onConfirm: function(email) {
$.ajax({
url: '/users/invite',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({emailAddr: email})
}).done(function (response) {
customPopups.hideUserInvite();
customPopups.showAlert(response.msg, function () {
location.reload();
});
}).fail(function (jqXHR, textStatus, errorThrown) {
let errorMessage;
try {
const errorResponse = JSON.parse(jqXHR.responseText);
errorMessage = errorResponse.msg || '이용자 초대 중 오류가 발생했습니다';
} catch (e) {
errorMessage = '이용자 초대 중 오류가 발생했습니다: ' + errorThrown;
}
customPopups.showUserInviteError(errorMessage);
});
},
onCancel: function() {
// Optional: handle cancel action
}
// Add event listeners to the "이용자 추가" buttons
const addUserBtn = document.getElementById('addUserBtn');
if (addUserBtn) {
addUserBtn.addEventListener('click', openUserInvitePopup);
}
// Change role button handlers
const adminPrivilegeButtons = document.querySelectorAll('.assign_manager');
adminPrivilegeButtons.forEach(button => {
button.addEventListener('click', function(event) {
const button = event.currentTarget;
const userId = button.dataset.userId;
const userName = button.dataset.userName;
const userEmail = button.dataset.userEmail;
customPopups.showChangeRole({
userName: userName,
userEmail: userEmail,
userId: userId,
onConfirm: function(userId) {
$.ajax({
url: '/users/change-role',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({id: userId})
}).done(function(response) {
customPopups.hideChangeRole();
customPopups.showAlert(response.msg, function() {
window.location.href = '/actionLogout.do';
});
}).fail(function(jqXHR, textStatus, errorThrown) {
customPopups.hideChangeRole();
customPopups.showAlert('권한 변경 중 오류가 발생했습니다: ' + errorThrown);
});
}
// Add event listeners to the "이용자 추가" buttons
const addUserBtn = document.getElementById('addUserBtn');
const addUserBtnEmpty = document.getElementById('addUserBtnEmpty');
if (addUserBtn) {
addUserBtn.addEventListener('click', openUserInvitePopup);
}
if (addUserBtnEmpty) {
addUserBtnEmpty.addEventListener('click', openUserInvitePopup);
}
// Change role button handlers
const adminPrivilegeButtons = document.querySelectorAll('.assign_manager');
adminPrivilegeButtons.forEach(button => {
button.addEventListener('click', function(event) {
const button = event.currentTarget;
const userId = button.dataset.userId;
const userName = button.dataset.userName;
const userEmail = button.dataset.userEmail;
customPopups.showChangeRole({
userName: userName,
userEmail: userEmail,
userId: userId,
onConfirm: function(userId) {
$.ajax({
url: '/users/change-role',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({id: userId})
}).done(function (response) {
customPopups.hideChangeRole();
customPopups.showAlert(response.msg, function () {
window.location.href = '/actionLogout.do';
});
}).fail(function (jqXHR, textStatus, errorThrown) {
customPopups.hideChangeRole();
customPopups.showAlert('권한 변경 중 오류가 발생했습니다: ' + errorThrown);
});
}
});
});
});
const activateButtons = document.querySelectorAll('.activate_user');
const inactivateButtons = document.querySelectorAll('.inactivate_user');
const cancelInvitationButtons = document.querySelectorAll('.cancel_invitation');
activateButtons.forEach(button => {
button.addEventListener('click', handleUserStatusChange);
});
inactivateButtons.forEach(button => {
button.addEventListener('click', handleUserStatusChange);
});
cancelInvitationButtons.forEach(button => {
button.addEventListener('click', handleCancelInvitation);
});
function handleUserStatusChange(event) {
const button = event.currentTarget;
const userId = button.dataset.userId;
const userName = button.dataset.userName;
const isActivating = button.classList.contains('activate_user');
const actionText = isActivating ? '활성화' : '비활성화';
customPopups.showConfirm(`${userName} 사용자를 ${actionText}하시겠습니까?`, function (selection) {
if (selection) {
$.ajax({
url: `/users/${isActivating ? 'activate' : 'inactivate'}`,
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({id: userId})
}).done(function (response) {
customPopups.showAlert(response.msg, function () {
location.reload(); // Reload the page to reflect changes
});
}).fail(function (jqXHR, textStatus, errorThrown) {
customPopups.showAlert(`사용자 ${actionText} 중 오류가 발생했습니다: ` + errorThrown);
}).always(function () {
$('#customConfirm').hide();
});
}
});
}
function handleCancelInvitation(event) {
const button = event.currentTarget;
const userId = button.dataset.userId;
const userEmail = button.dataset.userEmail;
customPopups.showConfirm(`${userEmail}에 대한 초대를 취소하시겠습니까?`, function (selection) {
if (selection) {
$.ajax({
url: '/users/cancel-invitation',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({id: userId})
}).done(function (response) {
customPopups.showAlert(response.msg, function () {
location.reload(); // Reload the page to reflect changes
});
}).fail(function (jqXHR, textStatus, errorThrown) {
customPopups.showAlert('초대 취소 중 오류가 발생했습니다: ' + errorThrown);
}).always(function () {
$('#customConfirm').hide();
});
}
});
}
});
});
</script>
});
const activateButtons = document.querySelectorAll('.activate_user');
const inactivateButtons = document.querySelectorAll('.inactivate_user');
const cancelInvitationButtons = document.querySelectorAll('.cancel_invitation');
activateButtons.forEach(button => {
button.addEventListener('click', handleUserStatusChange);
});
inactivateButtons.forEach(button => {
button.addEventListener('click', handleUserStatusChange);
});
cancelInvitationButtons.forEach(button => {
button.addEventListener('click', handleCancelInvitation);
});
function handleUserStatusChange(event) {
const button = event.currentTarget;
const userId = button.dataset.userId;
const userName = button.dataset.userName;
const isActivating = button.classList.contains('activate_user');
const actionText = isActivating ? '활성화' : '비활성화';
customPopups.showConfirm(`${userName} 사용자를 ${actionText}하시겠습니까?`, function(selection) {
if (selection) {
$.ajax({
url: `/users/${isActivating ? 'activate' : 'inactivate'}`,
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({id: userId})
}).done(function(response) {
customPopups.showAlert(response.msg, function() {
location.reload(); // Reload the page to reflect changes
});
}).fail(function(jqXHR, textStatus, errorThrown) {
customPopups.showAlert(`사용자 ${actionText} 중 오류가 발생했습니다: ` + errorThrown);
}).always(function() {
$('#customConfirm').hide();
});
}
});
}
function handleCancelInvitation(event) {
const button = event.currentTarget;
const userId = button.dataset.userId;
const userEmail = button.dataset.userEmail;
customPopups.showConfirm(`${userEmail}에 대한 초대를 취소하시겠습니까?`, function(selection) {
if (selection) {
$.ajax({
url: '/users/cancel-invitation',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({id: userId})
}).done(function(response) {
customPopups.showAlert(response.msg, function() {
location.reload(); // Reload the page to reflect changes
});
}).fail(function(jqXHR, textStatus, errorThrown) {
customPopups.showAlert('초대 취소 중 오류가 발생했습니다: ' + errorThrown);
}).always(function() {
$('#customConfirm').hide();
});
}
});
}
});
</script>
</th:block>