이용자 목록, 이용자 상세 스타일 정리

This commit is contained in:
현성필
2025-11-06 13:12:16 +09:00
parent f2d895b333
commit 65311d1639
14 changed files with 1353 additions and 1282 deletions
+603
View File
@@ -9857,6 +9857,609 @@ select.form-control {
}
}
.user-management-container {
max-width: 1400px;
margin: 0 auto;
padding: 48px 40px;
background-color: #F8FAFC;
min-height: 100vh;
}
@media (max-width: 768px) {
.user-management-container {
padding: 24px 16px;
}
}
.user-management-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 40px;
}
@media (max-width: 768px) {
.user-management-header {
flex-direction: column;
align-items: flex-start;
gap: 24px;
}
}
.user-management-title h1 {
font-size: 14px;
color: #64748B;
font-weight: 400;
margin-bottom: 4px;
}
.user-management-title h2 {
font-size: 32px;
font-weight: 700;
color: #1A1A2E;
}
.user-management-actions {
display: flex;
gap: 16px;
}
.btn-create-user,
.btn-create-user-large {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 32px;
background: linear-gradient(135deg, #4B9BFF 0%, #2E7FF7 100%);
color: #FFFFFF;
font-size: 16px;
font-weight: 600;
border: none;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(75, 155, 255, 0.1);
}
.btn-create-user:hover,
.btn-create-user-large:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(75, 155, 255, 0.15);
}
.btn-create-user:active,
.btn-create-user-large:active {
transform: translateY(0);
}
.btn-create-user .btn-icon,
.btn-create-user-large .btn-icon {
font-size: 20px;
}
.btn-create-user-large {
padding: 16px 40px;
font-size: 18px;
margin-top: 24px;
}
.table-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding: 0 8px;
}
@media (max-width: 768px) {
.table-controls {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
}
.total-count {
font-size: 16px;
color: #64748B;
}
.total-count strong {
color: #4B9BFF;
font-weight: 600;
font-size: 20px;
}
.user-list-wrapper {
background: #FFFFFF;
border-radius: 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
overflow: hidden;
margin-bottom: 40px;
}
.user-table {
display: flex;
flex-direction: column;
width: 100%;
}
.table-header {
display: grid;
grid-template-columns: 60px 120px 1fr 150px 120px 200px;
gap: 16px;
padding: 16px 24px;
background: #EFF6FF;
border-bottom: 2px solid #E2E8F0;
font-size: 14px;
font-weight: 600;
color: #1A1A2E;
}
@media (max-width: 1280px) {
.table-header {
grid-template-columns: 50px 100px 1fr 120px 100px 180px;
font-size: 12px;
padding: 8px 16px;
}
}
@media (max-width: 1024px) {
.table-header {
display: none;
}
}
.table-row {
display: grid;
grid-template-columns: 60px 120px 1fr 150px 120px 200px;
gap: 16px;
padding: 24px;
border-bottom: 1px solid #E2E8F0;
align-items: center;
transition: all 0.3s ease;
cursor: pointer;
}
.table-row:hover {
background-color: rgba(75, 155, 255, 0.02);
}
.table-row:last-child {
border-bottom: none;
}
.table-row.row-pending {
background-color: rgba(255, 217, 61, 0.03);
border-left: 3px solid #FFD93D;
}
@media (max-width: 1280px) {
.table-row {
grid-template-columns: 50px 100px 1fr 120px 100px 180px;
padding: 16px;
font-size: 14px;
}
}
@media (max-width: 1024px) {
.table-row {
grid-template-columns: 1fr;
gap: 8px;
padding: 16px;
}
.table-row::before {
content: attr(data-label);
font-weight: 600;
color: #64748B;
}
}
.col-num {
font-size: 14px;
color: #64748B;
text-align: center;
}
@media (max-width: 1024px) {
.col-num {
display: none;
}
}
.col-name {
font-weight: 500;
}
.col-name a {
color: #1A1A2E;
text-decoration: none;
transition: all 0.3s ease;
}
.col-name a:hover {
color: #4B9BFF;
}
.col-name .pending-label {
display: inline-flex;
align-items: center;
gap: 4px;
color: #64748B;
font-style: italic;
}
.col-name .pending-label i {
color: #FFD93D;
}
.col-email {
color: #64748B;
font-size: 14px;
word-break: break-all;
}
.col-role {
display: flex;
align-items: center;
}
.user-role-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background-color: rgba(75, 155, 255, 0.1);
color: #4B9BFF;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
}
.user-role-badge i {
font-size: 12px;
}
.col-status {
display: flex;
align-items: center;
justify-content: center;
}
.col-actions {
display: flex;
align-items: center;
justify-content: flex-start;
}
.user-status-badge {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 6px 12px;
border-radius: 50px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
white-space: nowrap;
}
.user-status-badge.status-active {
background-color: rgba(107, 207, 127, 0.1);
color: rgb(68.4897959184, 194.5102040816, 93.693877551);
}
.user-status-badge.status-inactive {
background-color: rgba(100, 116, 139, 0.1);
color: #64748B;
}
.user-status-badge.status-pending {
background-color: rgba(255, 217, 61, 0.15);
color: rgb(163, 131.0721649485, 0);
}
.user-current-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background-color: rgba(167, 139, 250, 0.1);
color: #A78BFA;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
}
.user-action-buttons {
display: flex;
flex-wrap: wrap;
gap: 4px;
align-items: center;
}
.user-action-buttons .btn-action {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
font-size: 12px;
font-weight: 500;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
white-space: nowrap;
}
.user-action-buttons .btn-action.btn-sm {
padding: 4px 10px;
font-size: 11px;
}
.user-action-buttons .btn-action.btn-primary {
background-color: #4B9BFF;
color: #FFFFFF;
}
.user-action-buttons .btn-action.btn-primary:hover {
background-color: rgb(24, 126.6666666667, 255);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.user-action-buttons .btn-action.btn-secondary {
background-color: #64748B;
color: #FFFFFF;
}
.user-action-buttons .btn-action.btn-secondary:hover {
background-color: rgb(78.6610878661, 91.2468619247, 109.3389121339);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.user-action-buttons .btn-action.btn-danger {
background-color: #FF6B6B;
color: #FFFFFF;
}
.user-action-buttons .btn-action.btn-danger:hover {
background-color: #ff3838;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.user-action-buttons .btn-action.btn-warning {
background-color: #FFD93D;
color: #1F2937;
}
.user-action-buttons .btn-action.btn-warning:hover {
background-color: rgb(255, 207.0103092784, 10);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.user-action-buttons .btn-action:active {
transform: scale(0.98);
}
.user-empty-state {
text-align: center;
padding: 80px 24px;
color: #64748B;
}
.user-empty-state .empty-icon {
font-size: 80px;
margin-bottom: 24px;
opacity: 0.5;
}
.user-empty-state h3 {
font-size: 24px;
font-weight: 600;
color: #1A1A2E;
margin-bottom: 16px;
}
.user-empty-state p {
font-size: 16px;
color: #64748B;
}
.user-detail-container {
max-width: 900px;
margin: 0 auto;
padding: 40px;
}
@media (max-width: 768px) {
.user-detail-container {
padding: 24px 16px;
}
}
.user-profile-card {
background: linear-gradient(135deg, #4B9BFF 0%, #2E7FF7 100%);
color: #FFFFFF;
margin-bottom: 32px;
position: relative;
overflow: hidden;
}
.user-profile-card::before {
content: "";
position: absolute;
top: -50%;
right: -20%;
width: 400px;
height: 400px;
background: rgba(255, 255, 255, 0.05);
border-radius: 50%;
}
.user-profile-card .detail-grid {
border: none;
}
.user-profile-header {
display: flex;
align-items: center;
gap: 32px;
position: relative;
z-index: 1;
}
@media (max-width: 768px) {
.user-profile-header {
flex-direction: column;
text-align: center;
}
}
.user-profile-avatar {
flex-shrink: 0;
}
.avatar-circle-large {
width: 100px;
height: 100px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 40px;
font-weight: 700;
background: rgba(255, 255, 255, 0.2);
color: #FFFFFF;
border: 4px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 8px 24px rgba(75, 155, 255, 0.15);
}
.user-profile-info {
flex: 1;
}
.user-profile-info h2 {
font-size: 32px;
font-weight: 700;
margin-bottom: 8px;
}
.user-profile-info .user-profile-email {
font-size: 16px;
opacity: 0.9;
margin-bottom: 16px;
}
.user-profile-badges {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
@media (max-width: 768px) {
.user-profile-badges {
justify-content: center;
}
}
.user-profile-badges .user-status-badge,
.user-profile-badges .user-role-badge {
background: rgba(255, 255, 255, 0.2);
color: #FFFFFF;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
}
.detail-section {
background: #FFFFFF;
border-radius: 12px;
padding: 32px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
margin-bottom: 24px;
}
.detail-section h2 {
font-size: 20px;
font-weight: 600;
color: #1A1A2E;
margin-bottom: 24px;
padding-bottom: 8px;
border-bottom: 2px solid #EFF6FF;
}
.detail-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
}
@media (max-width: 768px) {
.detail-grid {
grid-template-columns: 1fr;
}
}
.detail-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.detail-item.full-width {
grid-column: 1/-1;
}
.detail-label {
font-size: 14px;
font-weight: 600;
color: #64748B;
display: flex;
align-items: center;
gap: 4px;
}
.detail-label i {
color: #4B9BFF;
font-size: 14px;
}
.detail-value {
font-size: 16px;
color: #1A1A2E;
padding: 8px 0;
}
.status-indicator {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
}
.status-indicator.status-active {
background-color: rgba(107, 207, 127, 0.1);
color: rgb(68.4897959184, 194.5102040816, 93.693877551);
}
.status-indicator.status-active .status-dot {
background-color: #6BCF7F;
}
.status-indicator.status-inactive {
background-color: rgba(100, 116, 139, 0.1);
color: #64748B;
}
.status-indicator.status-inactive .status-dot {
background-color: #64748B;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.detail-actions {
display: flex;
gap: 16px;
justify-content: center;
padding-top: 24px;
}
@media (max-width: 768px) {
.detail-actions {
flex-direction: column;
}
}
.btn-secondary {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 32px;
background-color: #FFFFFF;
color: #1A1A2E;
font-size: 16px;
font-weight: 500;
border: 2px solid #E2E8F0;
border-radius: 12px;
text-decoration: none;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-secondary:hover {
background-color: #F8FAFC;
border-color: #4B9BFF;
color: #4B9BFF;
transform: translateY(-2px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.btn-secondary:active {
transform: translateY(0);
}
.btn-secondary i {
font-size: 16px;
}
.container {
max-width: 1280px;
margin: 0 auto;
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,30 +1,86 @@
const customPopups = {
/**
* 알림 팝업 표시
* @param {string} message - 알림 메시지
* @param {Function} callback - 확인 버튼 클릭 호출되는 콜백 (선택사항)
*/
showAlert: function (message, callback) {
// 메시지 설정
$('#customAlertMessage').text(message);
$('#customAlert').css('display', 'flex');
// Enter 키 이벤트 리스너 추가
$(document).on('keydown.customAlert', function (e) {
// 팝업 표시 (modal 구조 사용)
$('#customAlert').show();
setTimeout(function() {
$('#customAlertBackdrop').addClass('show');
$('#customAlertModal').addClass('show');
}, 10);
// Body 스크롤 방지
$('body').css('overflow', 'hidden');
// 확인 버튼에 포커스
setTimeout(function() {
$('#customAlertOkButton').focus();
}, 350);
// 확인 버튼 클릭 이벤트 (기존 이벤트 제거 후 재등록)
$('#customAlertOkButton').off('click').on('click', function () {
customPopups.hideAlert();
if (typeof callback === 'function') {
callback();
}
});
// 닫기 버튼 클릭 이벤트
$('#customAlertCloseButton').off('click').on('click', function () {
customPopups.hideAlert();
if (typeof callback === 'function') {
callback();
}
});
// Enter 키 이벤트
$(document).off('keydown.customAlert').on('keydown.customAlert', function (e) {
if (e.key === 'Enter') {
customPopups.hideAlert('#customAlert');
customPopups.hideAlert();
if (typeof callback === 'function') {
callback();
}
}
});
// 확인 버튼 클릭 시 콜백 호출
$('#customAlertOkButton').off('click').on('click', function () {
customPopups.hideAlert('#customAlert');
if (typeof callback === 'function') {
callback();
// Escape 키 이벤트
$(document).off('keydown.customAlertEsc').on('keydown.customAlertEsc', function (e) {
if (e.key === 'Escape') {
customPopups.hideAlert();
if (typeof callback === 'function') {
callback();
}
}
});
},
hideAlert: function (id) {
$(id).hide();
/**
* 알림 팝업 숨기기
*/
hideAlert: function () {
// Modal 숨김 애니메이션
$('#customAlertBackdrop').removeClass('show');
$('#customAlertModal').removeClass('show');
// 애니메이션 완료 후 숨김
setTimeout(function() {
$('#customAlert').hide();
}, 300);
// Body 스크롤 복원
$('body').css('overflow', '');
// 이벤트 리스너 제거
$('#customAlertOkButton').off('click');
$('#customAlertCloseButton').off('click');
$(document).off('keydown.customAlert');
$(document).off('keydown.customAlertEsc');
},
/**
@@ -153,17 +209,97 @@ const customPopups = {
$('#passwordPopupError').removeClass('show');
$('#passwordPopupInput').removeClass('error');
},
/**
* 확인 팝업 표시
* @param {string} message - 확인 메시지
* @param {Function} callback - 버튼 클릭 호출되는 콜백 (파라미터: boolean - true는 확인, false는 취소)
*/
showConfirm: function (message, callback) {
// 메시지 설정
$('#customConfirmMessage').text(message);
$('#customConfirm').css('display', 'flex');
$('#customConfirmYesButton').one('click', function () {
$('#customConfirm').hide();
if (callback) callback(true);
// 팝업 표시 (modal 구조 사용)
$('#customConfirm').show();
setTimeout(function() {
$('#customConfirmBackdrop').addClass('show');
$('#customConfirmModal').addClass('show');
}, 10);
// Body 스크롤 방지
$('body').css('overflow', 'hidden');
// 확인 버튼에 포커스
setTimeout(function() {
$('#customConfirmYesButton').focus();
}, 350);
// 확인 버튼 클릭 이벤트 (기존 이벤트 제거 후 재등록)
$('#customConfirmYesButton').off('click').on('click', function () {
customPopups.hideConfirm();
if (typeof callback === 'function') {
callback(true);
}
});
$('#customConfirmNoButton').one('click', function () {
$('#customConfirm').hide();
if (callback) callback(false);
// 취소 버튼 클릭 이벤트
$('#customConfirmNoButton').off('click').on('click', function () {
customPopups.hideConfirm();
if (typeof callback === 'function') {
callback(false);
}
});
// 닫기 버튼 클릭 이벤트
$('#customConfirmCloseButton').off('click').on('click', function () {
customPopups.hideConfirm();
if (typeof callback === 'function') {
callback(false);
}
});
// Enter 키 이벤트 (확인 버튼 실행)
$(document).off('keydown.customConfirm').on('keydown.customConfirm', function (e) {
if (e.key === 'Enter') {
customPopups.hideConfirm();
if (typeof callback === 'function') {
callback(true);
}
}
});
// Escape 키 이벤트 (취소)
$(document).off('keydown.customConfirmEsc').on('keydown.customConfirmEsc', function (e) {
if (e.key === 'Escape') {
customPopups.hideConfirm();
if (typeof callback === 'function') {
callback(false);
}
}
});
},
/**
* 확인 팝업 숨기기
*/
hideConfirm: function () {
// Modal 숨김 애니메이션
$('#customConfirmBackdrop').removeClass('show');
$('#customConfirmModal').removeClass('show');
// 애니메이션 완료 후 숨김
setTimeout(function() {
$('#customConfirm').hide();
}, 300);
// Body 스크롤 복원
$('body').css('overflow', '');
// 이벤트 리스너 제거
$('#customConfirmYesButton').off('click');
$('#customConfirmNoButton').off('click');
$('#customConfirmCloseButton').off('click');
$(document).off('keydown.customConfirm');
$(document).off('keydown.customConfirmEsc');
},
showEmailValidationPopup: function (message, onConvertCallback, onChangeEmailCallback) {
$('#emailValidationPopupMessage').text(message);
@@ -188,17 +324,223 @@ const customPopups = {
$('#userLoginId').val(loginId);
$('#emailResend').show();
},
/**
* 사용자 초대 팝업 표시
* @param {Object} options - 팝업 옵션
* @param {Function} options.onConfirm - 확인 버튼 클릭 호출되는 콜백 (파라미터: email)
* @param {Function} options.onCancel - 취소 버튼 클릭 호출되는 콜백 (선택사항)
*/
showUserInvite: function (options) {
options = options || {};
const onConfirm = options.onConfirm;
const onCancel = options.onCancel;
// 입력 필드 및 에러 초기화
$('#userInviteEmailInput').val('').removeClass('error');
$('#userInvitePopupError').removeClass('show').text('');
// 팝업 표시 (modal 구조 사용)
$('#userInvitePopup').show();
setTimeout(function() {
$('#userInviteModalBackdrop').addClass('show');
$('#userInviteModal').addClass('show');
}, 10);
// Body 스크롤 방지
$('body').css('overflow', 'hidden');
// 입력 필드에 포커스
setTimeout(function() {
$('#userInviteEmailInput').focus();
}, 350);
// 확인 버튼 이벤트 (기존 이벤트 제거 후 재등록)
$('#userInvitePopupConfirmButton').off('click').on('click', function () {
const email = $('#userInviteEmailInput').val().trim();
// 이메일 유효성 검사
if (!email) {
customPopups.showUserInviteError('이메일 주소를 입력해주세요.');
return;
}
// 이메일 형식 검증
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailPattern.test(email)) {
customPopups.showUserInviteError('올바른 이메일 형식이 아닙니다.');
return;
}
if (typeof onConfirm === 'function') {
onConfirm(email);
}
});
// 취소 버튼 이벤트
$('#userInvitePopupCancelButton').off('click').on('click', function () {
customPopups.hideUserInvite();
if (typeof onCancel === 'function') {
onCancel();
}
});
// 닫기 버튼 이벤트
$('#userInvitePopupCloseButton').off('click').on('click', function () {
customPopups.hideUserInvite();
if (typeof onCancel === 'function') {
onCancel();
}
});
// Enter 키 이벤트
$('#userInviteEmailInput').off('keypress').on('keypress', function (e) {
if (e.which === 13) {
$('#userInvitePopupConfirmButton').click();
}
});
// 입력 시 에러 초기화
$('#userInviteEmailInput').off('input').on('input', function () {
customPopups.clearUserInviteError();
});
},
/**
* 사용자 초대 팝업 숨기기
*/
hideUserInvite: function () {
// Modal 숨김 애니메이션
$('#userInviteModalBackdrop').removeClass('show');
$('#userInviteModal').removeClass('show');
// 애니메이션 완료 후 숨김
setTimeout(function() {
$('#userInvitePopup').hide();
}, 300);
// Body 스크롤 복원
$('body').css('overflow', '');
// 입력 필드 초기화
$('#userInviteEmailInput').val('').removeClass('error');
$('#userInvitePopupError').removeClass('show').text('');
// 이벤트 리스너 제거
$('#userInvitePopupConfirmButton').off('click');
$('#userInvitePopupCancelButton').off('click');
$('#userInvitePopupCloseButton').off('click');
$('#userInviteEmailInput').off('keypress').off('input');
},
/**
* 사용자 초대 에러 메시지 표시
* @param {string} message - 에러 메시지
*/
showUserInviteError: function (message) {
$('#userInvitePopupError').text(message).addClass('show');
$('#userInviteEmailInput').addClass('error');
},
/**
* 사용자 초대 에러 메시지 제거
*/
clearUserInviteError: function () {
$('#userInvitePopupError').removeClass('show');
$('#userInviteEmailInput').removeClass('error');
},
/**
* 권한 변경 팝업 표시
* @param {Object} options - 팝업 옵션
* @param {string} options.userName - 사용자 이름
* @param {string} options.userEmail - 사용자 이메일
* @param {string} options.userId - 사용자 ID
* @param {Function} options.onConfirm - 확인 버튼 클릭 호출되는 콜백 (파라미터: userId)
* @param {Function} options.onCancel - 취소 버튼 클릭 호출되는 콜백 (선택사항)
*/
showChangeRole: function (options) {
options = options || {};
const userName = options.userName || '';
const userEmail = options.userEmail || '';
const userId = options.userId;
const onConfirm = options.onConfirm;
const onCancel = options.onCancel;
// 사용자 정보 설정
$('#changeRoleUserName').text(userName);
$('#changeRoleUserEmail').text(userEmail ? `(${userEmail})` : '');
// 팝업 표시 (modal 구조 사용)
$('#changeRolePopup').show();
setTimeout(function() {
$('#changeRoleModalBackdrop').addClass('show');
$('#changeRoleModal').addClass('show');
}, 10);
// Body 스크롤 방지
$('body').css('overflow', 'hidden');
// 확인 버튼 이벤트 (기존 이벤트 제거 후 재등록)
$('#changeRolePopupConfirmButton').off('click').on('click', function () {
if (typeof onConfirm === 'function') {
onConfirm(userId);
}
});
// 취소 버튼 이벤트
$('#changeRolePopupCancelButton').off('click').on('click', function () {
customPopups.hideChangeRole();
if (typeof onCancel === 'function') {
onCancel();
}
});
// 닫기 버튼 이벤트
$('#changeRolePopupCloseButton').off('click').on('click', function () {
customPopups.hideChangeRole();
if (typeof onCancel === 'function') {
onCancel();
}
});
// Escape 키 이벤트
$(document).off('keydown.changeRole').on('keydown.changeRole', function (e) {
if (e.key === 'Escape') {
customPopups.hideChangeRole();
if (typeof onCancel === 'function') {
onCancel();
}
}
});
},
/**
* 권한 변경 팝업 숨기기
*/
hideChangeRole: function () {
// Modal 숨김 애니메이션
$('#changeRoleModalBackdrop').removeClass('show');
$('#changeRoleModal').removeClass('show');
// 애니메이션 완료 후 숨김
setTimeout(function() {
$('#changeRolePopup').hide();
}, 300);
// Body 스크롤 복원
$('body').css('overflow', '');
// 이벤트 리스너 제거
$('#changeRolePopupConfirmButton').off('click');
$('#changeRolePopupCancelButton').off('click');
$('#changeRolePopupCloseButton').off('click');
$(document).off('keydown.changeRole');
},
init: function () {
// customAlert 팝업의 닫기 버튼에 이벤트 리스너 추가
$('#customAlertOkButton, #customAlert .btn_close').on('click', function () {
customPopups.hideAlert('#customAlert');
});
// customConfirm 팝업의 닫기 버튼에 이벤트 리스너 추가
$('#customConfirmNoButton').on('click', function () {
customPopups.hideAlert('#customConfirm');
});
// emailValidationPopup 팝업의 닫기 버튼에 이벤트 리스너 추가
$('#emailValidationPopupCloseButton').on('click', function () {
$('#emailValidationPopup').hide();
@@ -208,11 +550,13 @@ const customPopups = {
$('#emailResend').hide();
});
// 다른 팝업의 닫기 버튼에 이벤트 리스너 추가
$('.btn_close:not(#customAlert .btn_close)').on('click', function () {
$(this).closest('.popup_total').hide();
// 다른 팝업의 닫기 버튼에 이벤트 리스너 추가 (modernized popups 제외)
$('.btn_close').on('click', function () {
const $popup = $(this).closest('.popup_total');
if ($popup.length) {
$popup.hide();
}
});
}
};
+1
View File
@@ -47,6 +47,7 @@
@import 'pages/apikey-detail';
@import 'pages/notice';
@import 'pages/inquiry';
@import 'pages/user-management';
// 6. Themes
@import 'themes/dark';
@@ -1,272 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
layout:decorate="~{layout/kbank_apikey_layout}">
<head>
<title>API 키 상세</title>
</head>
<body>
<section layout:fragment="contentFragment" class="content">
<div class="content_wrap">
<div class="sub_title3">
<h2 class="title">인증키 정보</h2>
</div>
<div class="inner i_cs h_inner11">
<div class="form_type">
<div class="info1 info_mypage">
<p class="title">
<span>인증키 이름</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp6">
<input type="text" id="clientName" th:value="${apiKey.clientname}"
class="common_input_type_1" disabled="disabled">
</div>
</div>
</div>
<div class="info1 info_mypage">
<p class="title">
<span>Client Id</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp6">
<input type="text" id="clientId" th:value="${apiKey.clientid}" class="common_input_type_1"
disabled="disabled">
</div>
</div>
</div>
<div class="info1 p_top1">
<p class="title">
<span>Client Secret</span>
</p>
<div class="info_line flex_w">
<div class="info_box1 w_inp6">
<div class="key_box">
<p th:text="${apiKey.clientsecret}">
client_key
</p>
</div>
</div>
</div>
</div>
<div class="info1 p_top5">
<p class="title"></p>
<div class="info_line line_add">
<div class="info_box1 m_top0 w_inp6">
<div class="btn_keydeltype">
<button sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" type="button"
class="btn_keydel">인증키 삭제
</button>
</div>
</div>
</div>
</div>
<div class="info1 info_mypage">
<p class="title">
<span>변경일</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp6">
<input type="text" th:value="${#temporals.format(apiKey.modifiedon, 'yyyy.MM.dd')}"
name="text" class="common_input_type_1" placeholder="2024.06.01" disabled="disabled">
</div>
</div>
</div>
<div class="info1 info_mypage">
<p class="title">
<span>인증키 상태</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp6">
<input type="text" th:value="${apiKey.appstatus == '1' ? '활성화' : '비활성화'}" name="text"
class="common_input_type_1" placeholder="활성화" disabled="disabled">
</div>
</div>
</div>
</div>
<div class="terms_box title">
<p>API 정보</p>
</div>
<div class="table_box">
<div class="pc-only">
<table class="table tb_key m_top" cellspacing="0" cellpadding="0" width="100%" id="apiList">
<colgroup>
<col width="60px">
<col width="166px">
<col width="230px">
<col width="120px">
</colgroup>
<thead>
<tr>
<th>No</th>
<th>서비스</th>
<th>API명</th>
<th>비고</th>
</tr>
</thead>
<tbody>
<tr th:each="api, apiStat : ${apiKey.apiList}">
<td th:text="${apiStat.count}">14</td>
<td th:text="${api.service}">공통</td>
<td th:text="${api.apiDesc}">회원여부조회</td>
<td><input type="hidden" name="apiId" th:value="${api.apiId}"/>
</td>
</tr>
</tbody>
</table>
</div>
<div class="m-only">
<table class="table tb_key m_top" cellspacing="0" cellpadding="0" width="100%">
<colgroup>
<col width="46px">
<col width="90px">
<col width="140px">
<col width="60px">
</colgroup>
<thead>
<tr>
<th>No</th>
<th>서비스</th>
<th>API명</th>
<th>비고</th>
</tr>
</thead>
<tbody>
<tr th:each="api, apiStat : ${apiKey.apiList}">
<td th:text="${apiStat.count}">14</td>
<td th:text="${api.apiGroupId}">공통</td>
<td th:text="${api.apiDesc}">회원여부조회</td>
<td>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="terms_box title">
<p>운영 전환 신청 내역</p>
</div>
<div class="table_box">
<div class="pc-only">
<table class="table tb_key m_top" cellspacing="0" cellpadding="0" width="100%">
<colgroup>
<col width="180px">
<col width="180px">
<col width="556px">
</colgroup>
<thead>
<tr>
<th>상태</th>
<th>일시</th>
<th>상세내용</th>
</tr>
</thead>
<tbody>
<tr th:each="history : ${prodhistory}">
<td th:text="${history.approval.approvalStatus.description}">요청</td>
<td th:text="${#temporals.format(history.createdDate, 'yyyy.MM.dd HH:mm:ss')}">2024.06.01
12:12:12
</td>
<td th:text="${history.approval.approvalSubject}+ '(' + ${history.reason} + ')'">운영 전환 요청</td>
</tr>
</tbody>
</table>
</div>
<div class="m-only">
<table class="table tb_key m_top" cellspacing="0" cellpadding="0" width="100%">
<colgroup>
<col width="80px">
<col width="180px">
<col width="240px">
</colgroup>
<thead>
<tr>
<th>상태</th>
<th>일시</th>
<th>상세내용</th>
</tr>
</thead>
<tbody>
<tr th:each="history : ${prodhistory}">
<td th:text="${history.approval.approvalStatus.description}">요청</td>
<td th:text="${#temporals.format(history.createdDate, 'yyyy.MM.dd HH:mm:ss')}">2024.06.01
12:12:12
</td>
<td th:text="${history.approval.approvalSubject}+ '(' + ${history.reason} + ')'">운영 전환 요청</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="btn_wrap bt_location">
<button type="button" sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" class="btn_add btn_request_api"
id="requestApiKey">API 변경
</button>
<button type="button" sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"
class="btn_refresh btn_request_prod_api">운영 전환 신청
</button>
</div>
<div class="btn_inventory btn_mtop">
<a th:href="@{/myapikey}" class="common_btn_type_1">목록</a>
</div>
<!-- // 기본 정보 -->
</div>
</div>
</section>
<section sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" layout:fragment="pagePopups" class="content pop">
<div th:insert="~{fragment/apikey-request :: apiKeyRequest}" class="popup_total" id="apiKeyRequestPopup"
style="display:none; z-index: 1000;"></div>
<th:block th:replace="~{fragment/apikey-request :: apiKeyRequestScript}"></th:block>
<div th:insert="~{fragment/apikey-request-prod :: prodApiKeyRequest}" class="popup_total"
id="apiKeyProdRequestPopup" style="display:none; z-index: 1000;"></div>
<th:block th:replace="~{fragment/apikey-request-prod :: prodApiKeyRequestScript}"></th:block>
</section>
<th:block layout:fragment="contentScript">
<script>
$(document).ready(function () {
activateSelect();
$('.btn_keydel').on('click', function (event) {
event.preventDefault();
$('.loading-overlay').show();
const requestData = {
clientId: document.getElementById('clientId').value,
type: 'DELETE'
};
$.ajax({
url: '/myapikey/api_key_request',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(requestData)
}).done(function (response) {
customPopups.showAlert(response.msg, function () {
window.location = '[[@{/myapikey/api_key_request/history}]]';
});
}).fail(function (jqXHR, textStatus, errorThrown) {
customPopups.showAlert('API 삭제 요청 중 오류가 발생했습니다: ' + errorThrown);
}).always(function () {
$('.loading-overlay').hide();
});
});
});
</script>
</th:block>
</body>
</html>
@@ -1,173 +0,0 @@
<!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/kbank_base_layout}">
<body>
<section class="content" layout:fragment="contentFragment">
<div class="content_wrap">
<div class="dashboard_content h_inner2">
<p class="month_date"><span th:text="${#temporals.format(#temporals.createToday().minusDays(1), 'yyyy.MM.dd')}"></span> 기준</p>
<div class="dashboard_box bd_box">
<div class="icon1">
<img th:src="@{/img/icon/icon_board04.png}" alt="API키">
<p class="tit">API 키 현황</p>
</div>
<div class="api_use">
<ul>
<li>
<div class="use_box">
<p class="tit_m">테스트 키 승인 대기</p>
<p class="num_m"><a th:href="@{/myapikey/api_key_request/history}"><span th:text="${pendingKeyCount}">5</span></a></p>
</div>
</li>
<li>
<div class="use_box">
<p class="tit_m">테스트 키 보유 건수</p>
<p class="num_m"><a th:href="@{/myapikey/}"><span th:text="${testKeyCount}">1</span></a></p>
</div>
</li>
<li>
<div class="use_box">
<p class="tit_m">운영 승인 대기</p>
<p class="num_m"><a th:href="@{/myapikey/api_key_request/prod_history}"><span th:text="${pendingProdCount}">1</span></a></p>
</div>
</li>
<li>
<div class="use_box">
<p class="tit_m">운영 키 보유 건수</p>
<p class="num_m"><a th:href="@{/myapikey/api_key_prod_page}"><span th:text="${prodKeyCount}">1</span></a></p>
</div>
</li>
</ul>
</div>
</div>
<div class="dashboard_box bd_box1">
<div class="icon1">
<img th:src="@{/img/icon/icon_board01.png}" alt="이용API">
<p class="tit">이용 API</p>
</div>
<p class="num"><span th:text="${totalApi}">1000</span></p>
</div>
<div class="dashboard_box bd_box2">
<div class="icon1">
<img th:src="@{/img/icon/icon_board02.png}" alt="API사용량">
<p class="tit">API 사용량</p>
</div>
<div class="api_use">
<ul>
<li>
<div class="use_box">
<img th:src="@{/img/icon/icon_calendar_month.png}" alt="월간 사용량">
<p class="tit_m">월간 사용량</p>
<p class="num_m"><span th:text="${monthlyTotal}">999,999,999</span></p>
</div>
</li>
<li>
<div class="use_box">
<img th:src="@{/img/icon/icon_calendar_week.png}" alt="주간 사용량">
<p class="tit_m">주간 사용량</p>
<p class="num_m"><span th:text="${weeklyTotal}">999,999</span></p>
</div>
</li>
<li>
<div class="use_box">
<img th:src="@{/img/icon/icon_calendar_day.png}" alt="일간 사용량">
<p class="tit_m">일간 사용량</p>
<p class="num_m"><span th:text="${dailyTotal}">999,999</span></p>
</div>
</li>
</ul>
</div>
</div>
<div class="dashboard_box bd_box3">
<div class="icon1">
<img th:src="@{/img/icon/icon_board03.png}" alt="이용API">
<p class="tit">주 사용 API</p>
</div>
<div class="api_use_num">
<ul>
<li>
<img th:src="@{/img/icon/icon_circle_num01_1.png}" alt="1번">
<span th:if="${topApi != null && topApi.size() > 0}" th:text="${topApi[0].apiName}">API1</span>
</li>
<li>
<img th:src="@{/img/icon/icon_circle_num02_2.png}" alt="2번">
<span th:if="${topApi != null && topApi.size() > 0}" th:text="${topApi[1].apiName}">API2</span>
</li>
<li>
<img th:src="@{/img/icon/icon_circle_num03_3.png}" alt="3번">
<span th:if="${topApi != null && topApi.size() > 0}" th:text="${topApi[2].apiName}">API3</span>
</li>
</ul>
</div>
</div>
<div class="dashboard_box2 i_cs">
<h3>일별 API 이용현황</h3>
<div class="form_type" style="display: block; height: 64px;">
</div>
<!-- 달력 -->
<div class="datepicker_wrapper">
<input type="text" class="datepicker_input" name="daterange" id="daily_usage_from_to"
th:value="${#temporals.format(#temporals.createToday().withDayOfMonth(1), 'yyyy.MM.dd')} + ' - ' + ${#temporals.format(#temporals.createToday(), 'yyyy.MM.dd')}"/>
<img th:src="@{/img/icon/icon_datepicker.png}" class="datepicker_icon" alt="달력">
</div>
<!-- // 달력 -->
<div class="table_type api_tb" id="dailyUsage">
</div>
</div>
</div>
</div>
</section>
</body>
<th:block layout:fragment="contentScript">
<script th:src="@{/js/moment.min.js}"></script>
<script th:src="@{/js/daterangepicker.js}"></script>
<script>
$(function () {
let dateRange = $('#daily_usage_from_to').val().split(' - ');
let fromDate = dateRange[0];
let toDate = dateRange[1];
// Initial load with date range
loadDailyUsage(fromDate, toDate);
$('#daily_usage_from_to').on('apply.daterangepicker', function(ev, picker) {
loadDailyUsage(
picker.startDate.format('YYYY.MM.DD'),
picker.endDate.format('YYYY.MM.DD')
);
});
// Function to load daily usage data
function loadDailyUsage(fromDate, toDate) {
fromDate = fromDate.replace(/\./g, '');
toDate = toDate.replace(/\./g, '');
$.ajax({
url: '/dashboard/daily_usage',
method: 'GET',
data: {
fromDate: fromDate,
toDate: toDate
},
success: function(response) {
$('#dailyUsage').html(response);
},
error: function(xhr, status, error) {
$('#dailyUsage').html('<p class="error">Failed to load daily usage data. Please try again later.</p>');
console.error('Error loading daily usage:', error);
}
});
}
});
</script>
</th:block>
@@ -1,284 +0,0 @@
<!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/base_layout}">
<body>
<section layout:fragment="contentFragment">
<div class="container mt-2">
<div class="text-center">
<h3>API 키 관리</h3>
</div>
<div class="card mt-2">
<div class="card-body">
<h5 class="card-title">API 키 정보 등록</h5>
<form name="apiKey" id="apiKey" th:object="${apiKey}" th:action="${#strings.isEmpty(apiKey.id) == true}?@{/myapikey/register}:@{/myapikey/update}" method="post">
<th:block th:if="${#strings.isEmpty(apiKey.id) == false}">
<input type="hidden" name="id" id="apiKeyId" th:value="${apiKey.id}"/>
<input type="hidden" name="owner" id="owner" th:value="${apiKey.owner}"/>
</th:block>
<div class="form-floating">
<input type="text" name="name" class="form-control mb-2" id="name" th:value="${apiKey.name}" maxlength="80" required>
<label for="name">이용 용도</label>
<th:block th:each="err : ${#fields.errors('name')}">
<div th:text="${err}" class="invalid-feedback"></div>
</th:block>
</div>
<div class="form-floating">
<input type="text" name="description" class="form-control mb-2" id="description" maxlength="600" th:value="${apiKey.description}"/>
<label for="description">이용 목적</label>
</div>
<th:block th:if="${#strings.isEmpty(apiKey.id) == false}">
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="form-floating col-10 mt-2 me-2">
<input type="text" name="clientId" class="form-control" id="clientId" th:value="${apiKey.clientId}" readonly>
<label for="clientId">Client ID</label>
</div>
<button class="btn btn-outline-secondary col-2" type="button" id="button-copy">Copy</button>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="form-floating col-10 me-2">
<input type="text" name="clientSecret" class="form-control" id="clientSecret" th:value="${apiKey.clientSecret}" readonly>
<label for="clientSecret">Client Secret</label>
</div>
<button class="btn btn-outline-secondary col-2" type="button" id="button-copy2">Copy</button>
</div>
</th:block>
<input type="hidden" name="apiIds" id="apiIds"/>
</form>
</div>
<div class="card-body">
<div class="row">
<div class="col-6">
<div class="card">
<div class="card-header">
전체 API
</div>
<div class="card-body">
<table class="table" id="sourceTable">
<thead>
<tr>
<th>이름</th>
<th></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-6">
<div class="card">
<div class="card-header">
선택한 API
</div>
<div class="card-body">
<table class="table" id="targetTable">
<thead>
<tr>
<th scope="col">이름</th>
<th></th>
</tr>
</thead>
<tbody>
<tr th:each="api, status: ${apiKey.apiList}">
<td>
<div class="d-flex justify-content-between">
<span th:text="${api.name}" th:data-id="${api.id}" th:data-name="${api.name}"></span>
<button type="button" class="btn btn-primary btn-sm">제거</button>
</div>
</td>
<td></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="card-footer">
<button type="button" class="btn btn-primary btn_list" id="back"><i class="fa-solid fa-list"></i> 목록</button>
<button type="button" class="btn btn-primary btn_save" id="save"><i class="fa-sharp fa-solid fa-floppy-disk"></i> 저장</button>
</div>
</div>
</div>
</section>
</body>
<th:block layout:fragment="contentScript">
<script>
$(function () {
let url = '[[@{/api/routes/list}]]';
let model = {
sourceItems: [],
targetItems: []
};
let view = {
init: function () {
this.apiIds = $('#apiIds');
this.sourceTable = $('#sourceTable').DataTable({
data: model.sourceItems,
fixedColumns: true,
autoWidth: false,
columns: [
{data: 'name'},
{
data: null, width: '60px',
className : 'text-center',
render: function (data, type, row) {
let disabled = row.selected === true ? 'disabled="true" ' : '';
return '<button type="button" class="btn btn-primary btn-sm" data-id="' + row.id + '"' + disabled + '>추가</button>';
}
}
],
columnDefs: [
{ targets: 1, orderable: false }
],
language : {
paginate: {
first: '«',
previous: '',
next: '',
last: '»'
},
lengthMenu : "페이지 당 _MENU_ 개씩 보기",
info: '[총 _TOTAL_ 건 중 _START_ ~ _END_ 표시]'
}
});
this.targetTable = $('#targetTable').DataTable({
data: model.targetItems,
paging:false,
searching:false,
columns: [
{data: 'name'},
{
data: null, width: '60px',
className : 'text-center',
render: function (data, type, row) {
return '<button type="button" class="btn btn-primary btn-sm" data-id="' + row.id + '">제거</button>';
}
}
],
columnDefs: [
{ targets: 1, orderable: false }
],
language : {
info: '',
infoEmpty: '',
emptyTable : '선택된 API가 없습니다.'
}
});
$('#sourceTable').on('click', 'button', function () {
let id = $(this).data('id');
controller.addToSelected(id);
});
$('#targetTable').on('click', 'button', function () {
let id = $(this).data('id');
controller.removeFromSelected(id);
});
this.render();
},
render: function () {
this.sourceTable.rows().invalidate();
this.targetTable.clear();
this.targetTable.rows.add(model.targetItems);
this.targetTable.draw();
// this.targetTable.rows().invalidate();
let idString = model.targetItems.map(function (obj) {
return obj.id;
}).join(',');
view.apiIds.val(idString);
}
};
let controller = {
init: function () {
let data = {};
$('#targetTable tbody tr').each(function () {
let row = $(this);
let rowData = {
id: row.find('span').data('id'),
name: row.find('span').data('name')
};
model.targetItems.push(rowData);
});
$.ajax({
url: url,
method: 'POST',
contentType: 'application/json',
data: JSON.stringify(data),
success: function (data) {
model.sourceItems = data;
model.sourceItems.forEach(function (item) {
let foundItem = model.targetItems.find(function (obj) {
if (obj.id === item.id) {
return true;
}
});
item.selected = (foundItem !== undefined);
});
view.init();
},
error: function (jqXHR, textStatus, errorThrown) {
console.error('POST request failed: ' + textStatus + ', ' + errorThrown);
}
});
},
addToSelected: function (id) {
let foundObject = model.sourceItems.find(function (obj) {
return obj.id === id;
});
foundObject.selected = true;
model.targetItems.push(foundObject);
view.render();
},
removeFromSelected: function (id) {
let sourceFound = model.sourceItems.find(function (obj) {
return obj.id === id;
});
sourceFound.selected = false;
let foundObject = model.targetItems.find(function (obj) {
return obj.id === id;
});
let index = model.targetItems.indexOf(foundObject);
model.targetItems.splice(index, 1);
view.render();
}
};
$(".btn_save").on("click", function (event) {
let form = document.forms.apiKey;
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
} else {
form.submit();
}
form.classList.add('was-validated');
});
$(".btn_list").on("click", function (e) {
window.location.href = '[[@{/myapikey/list_view}]]';
});
controller.init();
})
</script>
</th:block>
</html>
@@ -1,132 +0,0 @@
<!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/base_layout}">
<body>
<section layout:fragment="contentFragment">
<div class="container mt-2">
<div class="text-center">
<h3>API 키 관리</h3>
</div>
<div class="card mt-2">
<div class="card-body" th:if="${apiKeys.size() > 0}">
<form name="deleteForm" th:action="@{/myapikey/delete}" method="POST">
<input type="hidden" value="" name="id" />
<input id="csrf" type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
</form>
<th:block th:each="apikey, status : ${apiKeys}">
<div class="card p-0 me-2 mt-2">
<div class="card-header">
<span>API 키 정보</span>
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item" th:text="'이름: ' + ${apikey.clientname}"></li>
</ul>
<div class="accordion accordion-flush" id="apiKeySecret" th:if="${apikey.state.toString() =='ACTIVE'}" >
<div class="accordion-item">
<h2 class="accordion-header" id="flush-headingOne">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#flush-collapseOne" aria-expanded="false" aria-controls="flush-collapseOne">
Client ID
</button>
</h2>
<div id="flush-collapseOne" class="accordion-collapse collapse" aria-labelledby="flush-headingOne" data-bs-parent="#apiKeySecret">
<div class="accordion-body">
<div class="d-flex justify-content-between mb-2">
<div class="form-floating" style="flex-grow: 1;">
<input type="text" name="clientid" class="form-control" id="clientid" th:value="${apikey.clientid}" readonly>
<label for="clientid">Client ID</label>
</div>
<button class="btn btn-primary ms-2" type="button" id="button-copy" data-target="#clientid">Copy</button>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="flush-headingTwo">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#flush-collapseTwo" aria-expanded="false" aria-controls="flush-collapseTwo">
Client Secret
</button>
</h2>
<div id="flush-collapseTwo" class="accordion-collapse collapse" aria-labelledby="flush-headingTwo" data-bs-parent="#apiKeySecret">
<div class="accordion-body">
<div class="d-flex justify-content-between mb-2">
<div class="form-floating" style="flex-grow: 1;">
<input type="text" name="clientsecret" class="form-control" id="clientsecret" th:value="${apikey.clientsecret}" readonly>
<label for="clientsecret">Client Secret</label>
</div>
<button class="btn btn-primary ms-2" type="button" data-target="#clientsecret">Copy</button>
</div>
</div>
</div>
</div>
</div>
<div class="card-footer">
<button class="btn btn-primary btn-sm" th:onclick="fnDeleteItem([[${apikey.clientid}]])">삭제</button>
</div>
</div>
</th:block>
<div class="toast" id="copyToast" style="position: absolute; top: 0; right: 0;">
<div class="toast-header">
<strong class="me-auto">Notification</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
복사되었습니다
</div>
</div>
</div>
<div class="card-body" th:if="${apiKeys.size() == 0}">
<h5 class="card-title">개발 키 추가</h5>
<p class="card-text">승인 후 개발환경에서 사용가능한 키가 발급됩니다.</p>
<h3><a th:href="@{/myapikey/create_view}" class="btn btn-primary">발급하기</a></h3>
</div>
</div>
</div>
</section>
</body>
<th:block layout:fragment="contentScript">
<script>
function fnDeleteItem(id) {
if (confirm("[[#{common.delete.msg}]]")) {
document.deleteForm.id.value = id;
document.deleteForm.submit();
}
}
$(document).ready(function(){
// Listen for click events on buttons with data-target attribute
$("button[data-target]").click(function(){
// Get the value of the data-target attribute
var targetId = $(this).attr("data-target");
// Select the input element by its ID and get its value
var textToCopy = $(targetId).val();
// Create a temporary textarea to hold the text to be copied
var $temp = $("<textarea>");
$("body").append($temp);
$temp.val(textToCopy).select();
// Execute the copy command
document.execCommand("copy");
// Remove the temporary textarea
$temp.remove();
// Show the Bootstrap Toast
var toast = new bootstrap.Toast(document.getElementById("copyToast"));
toast.show();
});
});
</script>
</th:block>
</html>
@@ -1,108 +1,114 @@
<!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/kbank_base_layout}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
<body>
<section class="content" layout:fragment="contentFragment">
<div class="content_wrap">
<div class="sub_title3">
<h2 class="title">이용자 정보</h2>
<th:block layout:fragment="contentFragment">
<!-- Header -->
<div class="register-header">
<h1>이용자 상세</h1>
<p class="header-description">
이용자의 상세 정보를 확인할 수 있습니다.
</p>
</div>
<div class="inner i_cs h_inner8 h_inner12">
<section class="user-detail-container">
<!-- 기본 정보 -->
<div class="terms_box user_top">
<p>기본정보</p>
</div>
<!-- User Information -->
<div class="detail-section">
<h2 class="section-title">기본 정보</h2>
<div class="detail-grid">
<div class="form_type">
<!-- Email -->
<div class="detail-item full-width">
<label class="detail-label">
<i class="fas fa-envelope"></i> 이메일 아이디
</label>
<div class="detail-value" th:text="${user.maskedEmailAddr}">
user@example.com
</div>
</div>
<!-- Name -->
<div class="detail-item">
<label class="detail-label">
<i class="fas fa-user"></i> 성명
</label>
<div class="detail-value" th:text="${user.maskedUserName}">
홍길동
</div>
</div>
<!-- Mobile -->
<div class="detail-item">
<label class="detail-label">
<i class="fas fa-mobile-alt"></i> 휴대폰 번호
</label>
<div class="detail-value" th:text="${user.maskedMobileNumber}">
010-****-****
</div>
</div>
<!-- Role -->
<div class="detail-item">
<label class="detail-label">
<i class="fas fa-shield-alt"></i> 권한
</label>
<div class="detail-value">
<span class="user-role-badge" th:if="${user.roleCode != null}">
<span th:text="#{${user.roleCode}}">권한</span>
</span>
</div>
</div>
<!-- Status -->
<div class="detail-item">
<label class="detail-label">
<i class="fas fa-info-circle"></i> 계정상태
</label>
<div class="detail-value">
<span class="status-indicator"
th:classappend="${user.userStatus.toString() == 'ACTIVE'} ? 'status-active' : 'status-inactive'">
<span class="status-dot"></span>
<span th:text="${user.userStatus.getDescription()}">활성화</span>
</span>
</div>
</div>
<!-- Created Date -->
<div class="detail-item">
<label class="detail-label">
<i class="fas fa-calendar-plus"></i> 가입일
</label>
<div class="detail-value" th:text="${#temporals.format(user.createdDate, 'yyyy.MM.dd')}">
2024.01.01
</div>
</div>
<!-- Last Login -->
<div class="detail-item">
<label class="detail-label">
<i class="fas fa-clock"></i> 마지막 로그인
</label>
<div class="detail-value" th:text="${#temporals.format(user.lastLoginDate, 'yyyy.MM.dd HH:mm:ss')}">
2024.01.01 12:00:00
</div>
</div>
<div class="info1">
<p class="title">
<span>이메일 아이디</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp4">
<input type="text" class="common_input_type_1" th:value="${#strings.substringBefore(user.maskedEmailAddr, '@')}" disabled="disabled">
<span class="special_t">@</span>
<input type="text" class="common_input_type_1" th:value="${#strings.substringAfter(user.maskedEmailAddr, '@')}" disabled="disabled">
</div>
</div>
</div>
<div class="info1 info_mypage">
<p class="title">
<span>성명</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp4">
<input type="text" th:value="${user.maskedUserName}" name="text" class="common_input_type_1" disabled="disabled" style="background:#F7F9FD;">
</div>
</div>
<!-- Action Buttons -->
<div class="detail-actions">
<a th:href="@{/users}" class="btn-secondary">
<i class="fas fa-list"></i> 목록으로
</a>
</div>
<div class="info1 info_mypage">
<p class="title">
<span>휴대폰 번호</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp4">
<input type="text" th:value="${user.maskedMobileNumber}" name="text" class="common_input_type_1" disabled="disabled" style="background:#F7F9FD;">
</div>
</div>
</div>
</section>
<div class="info1 info_mypage">
<p class="title">
<span>권한</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp4">
<input type="text" th:value="#{${user.roleCode}}" name="text" class="common_input_type_1" disabled="disabled" style="background:#F7F9FD;">
</div>
</div>
</div>
<div class="info1 info_mypage">
<p class="title">
<span>계정상태</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp4">
<input type="text" th:value="${user.userStatus.getDescription()}" name="text" class="common_input_type_1" disabled="disabled" style="background:#F7F9FD;">
</div>
</div>
</div>
<div class="info1 info_mypage">
<p class="title">
<span>가입일</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp4">
<input type="text" th:value="${#temporals.format(user.createdDate, 'yyyy.MM.dd')}" name="text" class="common_input_type_1" disabled="disabled" style="background:#F7F9FD;">
</div>
</div>
</div>
<div class="info1 info_mypage">
<p class="title">
<span>마지막 로그인</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp4">
<input type="text" th:value="${#temporals.format(user.lastLoginDate, 'yyyy.MM.dd HH:mm:ss')}" name="text" class="common_input_type_1" disabled="disabled" style="background:#F7F9FD;">
</div>
</div>
</div>
</div>
<div class="btn_inventory btn_mtop">
<a th:href="@{/users}" class="common_btn_type_1">목록</a>
</div>
<!-- // 기본 정보 -->
</div>
</div>
</section>
</th:block>
</body>
@@ -1,158 +1,159 @@
<!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/kbank_base_layout}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
<body>
<section class="content" layout:fragment="contentFragment">
<div class="content_wrap">
<div class="sub_title3">
<h2 class="title">이용자 목록</h2>
<th:block layout:fragment="contentFragment">
<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-create-user" id="addUserBtn">
<span class="btn-icon"></span>
이용자 추가
</button>
</div>
</div>
<div class="inner i_cs h_inner8 h_inner12">
<!-- 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>
<div class="tb_board_list">
<div class="table_box">
<table class="table table_min bdt2" cellspacing="0" cellpadding="0" width="100%">
<colgroup>
<col width="60px">
<col width="86px">
<col width="210px">
<col width="80px">
<col width="180px">
<col>
</colgroup>
<thead>
<tr>
<th>No</th>
<th>성명</th>
<th>이메일 아이디</th>
<th>권한</th>
<th>계정상태</th>
<th>설정</th>
</tr>
</thead>
<tbody>
<tr th:each="user, status : ${users}">
<td th:text="${status.index + 1}">No</td>
<td>
<a th:href="@{/users/detail(id=${user.id})}" th:text="${user.maskedUserName}">성명</a>
</td>
<td th:text="${user.maskedEmailAddr}">이메일 아이디</td>
<td>
<span th:if="${user.roleCode != null}" th:text="#{${user.roleCode}}">권한</span>
</td>
<td th:text="${user.userStatus.getDescription()}">계정상태</td>
<td>
<div th:if="${user.id == currentUserId}">
현재 사용자
</div>
<div class="btn_wrap btn_set"
th:if="${user.roleCode != null and user.id != currentUserId}">
<a href="#" class="common_btn_type_4 inactivate_user"
th:if="${user.userStatus.toString() == 'ACTIVE'}"><span>비활성화</span></a>
<a href="#" class="common_btn_type_4 activate_user"
th:if="${user.userStatus.toString() != 'ACTIVE'}"><span>활성화</span></a>
<a href="#" class="common_btn_type_4 cancel_invitation"
th:if="${user.userStatus.toString() == 'PENDING'}"><span>초대취소</span></a>
<a href="#" class="common_btn_type_4 assign_manager"
th:if="${user.userStatus.toString() == 'ACTIVE' && user.roleCode.toString() == 'ROLE_CORP_USER'}"><span>관리자 권한부여</span></a>
</div>
</td>
</tr>
<tr th:each="user : ${pendingUsers}">
<td th:data-id="${user.id}"></td>
<td>
</td>
<td th:text="${user.maskedEmailAddr}">이메일 아이디</td>
<td>
</td>
<td></td>
<td>
<div class="btn_wrap btn_set">
<a href="#none" class="common_btn_type_4 cancel_invitation"><span>초대취소</span></a>
</div>
</td>
</tr>
</tbody>
</table>
<!-- 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>
</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}">
<i class="fas fa-user-check"></i> 현재 사용자
</div>
<div class="btn_wrap bt_location">
<button type="button" class="btn_add">이용자 추가</button>
<!-- Action Buttons -->
<div class="user-action-buttons" th:if="${user.roleCode != null and user.id != currentUserId}">
<button type="button" class="btn-action 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-action 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-action btn-sm btn-danger 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-action 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-action btn-sm btn-danger cancel_invitation"
th:data-user-id="${user.id}"
th:data-user-email="${user.maskedEmailAddr}">
초대취소
</button>
</div>
</div>
</div>
</div>
<div class="pagination page_top"
th:replace="~{fragment/pagination :: pagination(jsFunction='fn_select_page')}"></div>
</div>
</div>
</section>
<!-- 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-create-user-large" id="addUserBtnEmpty">
<span class="btn-icon"></span>
첫 이용자 추가하기
</button>
</div>
<!-- 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">
<div class="popup_wrap pop01" id="user_register" style="display: none; z-index: 1000">
<div class="pop_dim">
<div class="inner">
<div class="pop_body">
<div class="box-txt">
<div class="inner_txt">
<div class="title">
<h4>이용자 추가</h4>
</div>
</div>
<div class="contents">
<p class="con_txt2">추가할 이용자 e-mail 주소를 입력해 주세요.</p>
<div class="inp_box">
<input type="text" class="common_input_type_1 w_input" id="new_user_email"
placeholder="이용자 e-mail 주소">
</div>
</div>
</div>
<div class="pop_btn p_btn">
<a href="#" class="common_btn_type_1 gray btn_cancel"><span>취소</span></a>
<a href="#" class="common_btn_type_1 blue btn_register"><span>확인</span></a>
</div>
<button class="btn_close" style="margin-top:0">
<img th:src="@{/img/icon/icon_close.png}">
</button>
</div>
</div>
</div>
</div>
<div class="popup_wrap pop01" id="change_role" style="display: none; z-index: 1000">
<div class="pop_dim">
<div class="inner inner2">
<div class="pop_body">
<div class="box-txt">
<div class="inner_txt">
<div class="title">
<h4>권한변경</h4>
</div>
</div>
<div class="contents">
<p class="con_txt">
<span class="txt_bold" id="change_role_name"></span><span class="txt_mail"
id="change_role_email"></span>
에게<br>
법인 관리자 권한을 부여 하시겠습니까?
</p>
<div class="con_info_box">
<p>법인 관리자는 1개의 기관에 1명만 지정 가능 합니다.</p>
<p>본 계정의 관리자 권한은 해제 됩니다.</p>
</div>
</div>
</div>
<div class="pop_btn">
<a href="#" class="common_btn_type_1 gray btn_cancel_role"><span>취소</span></a>
<a href="#" class="common_btn_type_1 blue btn_confirm_role"><span>확인</span></a>
</div>
<button class="btn_close" style="margin-top:0">
<img th:src="@{/img/icon/icon_close.png}" alt="">
</button>
</div>
</div>
</div>
</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>
@@ -165,129 +166,80 @@
});
document.addEventListener('DOMContentLoaded', function () {
// Function to open the popup
function openPopup() {
document.getElementById('user_register').style.display = '';
}
// Function to close the popup
function closePopup() {
document.getElementById('user_register').style.display = 'none';
}
// Add event listener to the "이용자 추가" button to open the popup
const addUserBtn = document.querySelector('.btn_add');
if (addUserBtn) {
addUserBtn.addEventListener('click', openPopup);
}
// Add event listener to the close button
const closeBtn = document.querySelector('button.btn_close');
if (closeBtn) {
closeBtn.addEventListener('click', closePopup);
}
// Add event listener to the cancel button
const cancelBtn = document.querySelector('.btn_cancel');
if (cancelBtn) {
cancelBtn.addEventListener('click', closePopup);
}
// Add event listener to the confirm button
const confirmBtn = document.querySelector('.btn_register');
if (confirmBtn) {
confirmBtn.addEventListener('click', sendInvitation);
}
function sendInvitation() {
const emailInput = document.getElementById('new_user_email');
const email = emailInput.value.trim();
if (!email) {
customPopups.showAlert('이메일 주소를 입력해주세요.');
return;
}
$.ajax({
url: '/users/invite',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({emailAddr: email})
}).done(function (response) {
closePopup();
emailInput.value = '';
customPopups.showAlert(response.msg, function () {
location.reload(); // Reload the page to reflect changes
});
}).fail(function (jqXHR, textStatus, errorThrown) {
let errorMessage;
try {
// Parse the error response body
const errorResponse = JSON.parse(jqXHR.responseText);
errorMessage = errorResponse.msg || '이용자 초대 중 오류가 발생했습니다';
} catch (e) {
// Fallback if response isn't valid JSON
errorMessage = '이용자 초대 중 오류가 발생했습니다: ' + errorThrown;
// 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
}
customPopups.showAlert(errorMessage);
});
}
// Optional: Close the popup when clicking outside of it
window.addEventListener('click', function (event) {
const popup = document.getElementById('user_register');
if (event.target === popup) {
closePopup();
}
});
// 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', openChangeRolePopup);
});
button.addEventListener('click', function(event) {
const button = event.currentTarget;
const userId = button.dataset.userId;
const userName = button.dataset.userName;
const userEmail = button.dataset.userEmail;
function openChangeRolePopup(event) {
const row = event.target.closest('tr');
const name = row.querySelector('td:nth-child(2)').textContent;
const email = row.querySelector('td:nth-child(3)').textContent;
const userId = row.querySelector('a').getAttribute('href').split('=')[1];
document.getElementById('change_role_name').textContent = name;
document.getElementById('change_role_email').textContent = `(${email})`;
document.getElementById('change_role').style.display = 'block';
// Store userId for later use
document.getElementById('change_role').dataset.userId = userId;
}
function closeChangeRolePopup() {
document.getElementById('change_role').style.display = 'none';
}
document.querySelector('#change_role .btn_close').addEventListener('click', closeChangeRolePopup);
document.querySelector('#change_role .btn_cancel_role').addEventListener('click', closeChangeRolePopup);
document.querySelector('#change_role .btn_confirm_role').addEventListener('click', confirmRoleChange);
function confirmRoleChange() {
const userId = document.getElementById('change_role').dataset.userId;
$.ajax({
url: '/users/change-role',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({id: userId})
}).done(function (response) {
closeChangeRolePopup();
customPopups.showAlert(response.msg, function () {
window.location.href = '/actionLogout.do';
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);
});
}
});
}).fail(function (jqXHR, textStatus, errorThrown) {
customPopups.showAlert('권한 변경 중 오류가 발생했습니다: ' + errorThrown);
});
}
});
const activateButtons = document.querySelectorAll('.activate_user');
const inactivateButtons = document.querySelectorAll('.inactivate_user');
@@ -306,17 +258,14 @@
});
function handleUserStatusChange(event) {
const row = event.target.closest('tr');
const userId = row.querySelector('a').getAttribute('href').split('=')[1];
const userName = row.querySelector('td:nth-child(2)').textContent;
console.log(event.target.classList);
const isActivating = event.target.classList.contains('activate_user');
console.log({isActivating});
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) {
console.log({isActivating});
$.ajax({
url: `/users/${isActivating ? 'activate' : 'inactivate'}`,
type: 'POST',
@@ -336,12 +285,11 @@
}
function handleCancelInvitation(event) {
const row = event.target.closest('tr');
const userId = row.querySelector('td[data-id]').getAttribute('data-id');
console.log(userId);
const userName = row.querySelector('td:nth-child(3)').textContent;
const button = event.currentTarget;
const userId = button.dataset.userId;
const userEmail = button.dataset.userEmail;
customPopups.showConfirm(`${userName}에 대한 초대를 취소하시겠습니까?`, function (selection) {
customPopups.showConfirm(`${userEmail}에 대한 초대를 취소하시겠습니까?`, function (selection) {
if (selection) {
$.ajax({
url: '/users/cancel-invitation',
@@ -1,21 +1,35 @@
<!-- views/fragment/custom-popup.html -->
<div th:fragment="customAlert" class="popup_total" id="customAlert" style="display:none; z-index:1001;">
<div class="pop_dim">
<div class="inner">
<div class="popup-content">
<div class="inner_txt">
<div class="title">
<h2>알림</h2>
</div>
</div>
<p class="pop_text mo_text" id="customAlertMessage"></p>
<div class="pop_btnbox">
<button class="popup_button_blue inner_button" id="customAlertOkButton">확인</button>
</div>
<a class="btn_close" style="margin-top:0">
<img th:src="@{/img/icon/icon_close.png}" alt="닫기">
</a>
<div th:fragment="customAlert" id="customAlert" style="display: none;">
<!-- Modal Backdrop -->
<div class="modal-backdrop" id="customAlertBackdrop"></div>
<!-- Modal Wrapper -->
<div class="modal" id="customAlertModal">
<div class="modal-dialog">
<!-- Modal Header -->
<div class="modal-header">
<h3 class="modal-title" id="customAlertTitle">알림</h3>
<button type="button" class="modal-close" id="customAlertCloseButton">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<!-- Modal Body -->
<div class="modal-body">
<p id="customAlertMessage" style="text-align: center; margin-bottom: 24px; color: #64748B;">
<!-- 메시지가 여기에 표시됩니다 -->
</p>
</div>
<!-- Modal Footer -->
<div class="modal-footer">
<button type="button" class="btn btn-primary" id="customAlertOkButton">확인</button>
</div>
</div>
</div>
</div>
@@ -1,20 +1,36 @@
<!-- views/fragment/custom-popup2.html -->
<div th:fragment="customAlert2" class="popup_total" id="customConfirm" style="display:none; z-index:1001;">
<div class="pop_dim">
<div class="popup-content">
<div class="inner_txt">
<div class="title">
<h2 id="customAlertTitle2">알림</h2>
</div>
<div th:fragment="customConfirm" id="customConfirm" style="display: none;">
<!-- Modal Backdrop -->
<div class="modal-backdrop" id="customConfirmBackdrop"></div>
<!-- Modal Wrapper -->
<div class="modal" id="customConfirmModal">
<div class="modal-dialog">
<!-- Modal Header -->
<div class="modal-header">
<h3 class="modal-title" id="customConfirmTitle">확인</h3>
<button type="button" class="modal-close" id="customConfirmCloseButton">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<p class="pop_text" id="customConfirmMessage"></p>
<div class="pop_btnbox">
<button class="popup_button_gray" id="customConfirmNoButton">취소</button>
<button class="popup_button_blue" id="customConfirmYesButton">확인</button>
<!-- Modal Body -->
<div class="modal-body">
<p id="customConfirmMessage" style="text-align: center; margin-bottom: 24px; color: #64748B;">
<!-- 메시지가 여기에 표시됩니다 -->
</p>
</div>
<a class="btn_close" style="margin-top:0" id="customAlertCloseButton2">
<img th:src="@{/img/icon/icon_close.png}" alt="닫기">
</a>
<!-- Modal Footer -->
<div class="modal-footer">
<button type="button" class="btn btn-secondary" id="customConfirmNoButton">취소</button>
<button type="button" class="btn btn-primary" id="customConfirmYesButton">확인</button>
</div>
</div>
</div>
</div>