- UserRoleHistoryService 추가: 역할 변경 감사 이력 서비스 구현

- `ROLE_API_KEY_REQUEST` → `ROLE_WEBHOOK` 변경: Webhook 관리 권한 분리 및 강화
- "이용자" → "개발자" 용어 통일: 템플릿, 컨트롤러 및 예외 메시지 수정
This commit is contained in:
Rinjae
2026-07-27 20:11:09 +09:00
parent 0f58f043a8
commit 2106142d93
28 changed files with 276 additions and 90 deletions
+5 -2
View File
@@ -224,12 +224,15 @@ portal:
- ROLE_INQUIRY
- ROLE_ACCOUNT
ROLE_CORP_USER:
- ROLE_API_KEY_REQUEST
- ROLE_API_KEY_REQUEST_VIEW
- ROLE_INQUIRY
- ROLE_APP
- ROLE_ACCOUNT
ROLE_CORP_MANAGER:
- ROLE_API_KEY_REQUEST
- ROLE_API_KEY_REQUEST_VIEW
- ROLE_WEBHOOK
- ROLE_INQUIRY
- ROLE_APP
- ROLE_ACCOUNT
@@ -355,10 +358,10 @@ page:
name: "이메일 인증"
path: "/mypage/verification-email"
user_list:
name: "이용자 관리"
name: "개발자 관리"
path: "/users"
user_detail:
name: "이용자 정보"
name: "개발자 정보"
path: "/users/detail"
apikey:
name: "앱 관리"
@@ -82,6 +82,6 @@ status=Status
table.reger=Registrar
zip=Zip Code
ROLE_CORP_MANAGER=Manager
ROLE_CORP_USER=User
ROLE_CORP_USER=Developer
ROLE_USER=Individual
login.passLengthShort=Password must be over 8 characters.
@@ -82,6 +82,6 @@ status=\uC0C1\uD0DC
table.reger=\uB4F1\uB85D\uC790
zip=\uC6B0\uD3B8\uBC88\uD638
ROLE_CORP_MANAGER=\uAD00\uB9AC\uC790
ROLE_CORP_USER=\uC774\uC6A9\uC790
ROLE_CORP_USER=\uAC1C\uBC1C\uC790
ROLE_USER=\uAC1C\uC778\uC0AC\uC6A9\uC790
ConcurrentSessionControlAuthenticationStrategy.exceededAllowed=\uCD5C\uB300 \uB85C\uADF8\uC778 \uD5C8\uC6A9 \uC5F0\uACB0\uC744 \uCD08\uACFC\uD558\uC600\uC2B5\uB2C8\uB2E4.
@@ -27,6 +27,10 @@
<i class="fas fa-info-circle"></i>
<span>다른 기기 또는 브라우저에서 로그인되어 현재 세션이 종료되었습니다.</span>
</div>
<div th:if="${param.reason == 'auth'}" class="login-alert alert-info">
<i class="fas fa-info-circle"></i>
<span>인증된 사용자만 접근 가능한 페이지입니다. 로그인 후 이용해 주세요.</span>
</div>
<!-- Login Form -->
<form id="loginForm" role="form" name="loginForm" th:action="@{/actionLogin.do}" method="post" class="login-form">
@@ -37,7 +37,7 @@
<div class="org-info-notice">
<ul>
<li>법인 관리자는 API 신청 및 이용자 초대 등 모든 권한을 보유합니다.</li>
<li>법인 관리자는 API 신청 및 개발자 초대 등 모든 권한을 보유합니다.</li>
<li>법인회원 전환은 반드시 법인이메일 주소로 입력하시기 바랍니다. 개인이메일로 전환 신청시, 법인 승인이 거절 될 수 있습니다</li>
</ul>
</div>
@@ -49,7 +49,7 @@
<div class="org-info-notice">
<ul>
<li>법인관리자는 API 신청 및 이용자 초대 등 모든 권한을 보유합니다.</li>
<li>법인관리자는 API 신청 및 개발자 초대 등 모든 권한을 보유합니다.</li>
</ul>
</div>
@@ -5,14 +5,14 @@
<section layout:fragment="title">
<div class="page-title-banner">
<img th:src="@{/img/img_title_bg.png}" class="title-image">
<h1>이용자 관리</h1>
<h1>개발자 관리</h1>
</div>
</section>
<section layout:fragment="contentFragment">
<div class="org-register-container">
<div class="common-title-bar">
<h2 class="common-title">이용자 정보</h2>
<h2 class="common-title">개발자 정보</h2>
</div>
<div class="register-form-container">
@@ -23,7 +23,7 @@
<div class="search-box">
<button type="button" class="btn btn-primary" id="addUserBtn">
<i class="fas fa-plus"></i>
<span>이용자 추가</span>
<span>개발자 추가</span>
</button>
</div>
</div>
@@ -78,7 +78,7 @@
th:if="${user.userStatus.toString() == 'ACTIVE' && user.roleCode.toString() == 'ROLE_CORP_MANAGER'}"
th:data-user-id="${user.id}"
th:data-user-name="${user.maskedUserName}">
이용자로 변경
개발자로 변경
</button>
<button type="button" class="list-table-btn list-table-btn--danger remove_from_org"
th:if="${user.userStatus.toString() == 'ACTIVE' && (user.roleCode.toString() == 'ROLE_CORP_USER' || user.roleCode.toString() == 'ROLE_CORP_MANAGER')}"
@@ -121,7 +121,7 @@
th:if="${user.userStatus.toString() == 'ACTIVE' && user.roleCode.toString() == 'ROLE_CORP_MANAGER'}"
th:data-user-id="${user.id}"
th:data-user-name="${user.maskedUserName}">
이용자로 변경
개발자로 변경
</button>
<button type="button" class="dropdown-item dropdown-item--danger remove_from_org"
th:if="${user.userStatus.toString() == 'ACTIVE' && (user.roleCode.toString() == 'ROLE_CORP_USER' || user.roleCode.toString() == 'ROLE_CORP_MANAGER')}"
@@ -155,7 +155,7 @@
<div class="row-cell" data-label="휴대폰" th:text="${user.maskedMobileNumber}">
010-****-****
</div>
<div class="row-cell" data-label="권한">이용</div>
<div class="row-cell" data-label="권한">개발</div>
<div class="row-cell" data-label="계정상태">초대중</div>
<div class="row-cell row-actions">
<!-- Desktop -->
@@ -193,8 +193,8 @@
<!-- 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>
<h3>등록된 개발자가 없습니다</h3>
<p>새로운 개발자를 추가하여 시작하세요.</p>
</div>
<!-- Pagination -->
@@ -281,9 +281,9 @@
let errorMessage;
try {
const errorResponse = JSON.parse(jqXHR.responseText);
errorMessage = errorResponse.msg || '이용자 초대 중 오류가 발생했습니다';
errorMessage = errorResponse.msg || '개발자 초대 중 오류가 발생했습니다';
} catch (e) {
errorMessage = '이용자 초대 중 오류가 발생했습니다: ' + errorThrown;
errorMessage = '개발자 초대 중 오류가 발생했습니다: ' + errorThrown;
}
customPopups.showUserInviteError(errorMessage);
});
@@ -294,7 +294,7 @@
});
}
// Add event listeners to the "이용자 추가" buttons
// Add event listeners to the "개발자 추가" buttons
const addUserBtn = document.getElementById('addUserBtn');
if (addUserBtn) {
@@ -334,14 +334,14 @@
});
});
// 관리자 -> 이용자 변경 (본인 제외)
// 관리자 -> 개발자 변경 (본인 제외)
document.querySelectorAll('.revoke_manager').forEach(button => {
button.addEventListener('click', function(event) {
const btn = event.currentTarget;
const userId = btn.dataset.userId;
const userName = btn.dataset.userName;
customPopups.showConfirm(`<strong>${userName}</strong>님을 이용자로 변경하시겠습니까?`, function(selection) {
customPopups.showConfirm(`<strong>${userName}</strong>님을 개발자로 변경하시겠습니까?`, function(selection) {
if (!selection) return;
$.ajax({
url: '/users/revoke-manager',
@@ -33,14 +33,14 @@
<h3>등록된 Webhook이 없습니다</h3>
<p>API 서비스의 점검·지연·장애 알림을 받을 Webhook을 신청해보세요.</p>
<p class="field-help" sec:authorize="!hasRole('ROLE_API_KEY_REQUEST')">
<p class="field-help" sec:authorize="!hasRole('ROLE_WEBHOOK')">
Webhook 신청은 법인 관리자만 가능합니다. 기관 관리자에게 문의하세요.
</p>
</div>
</div>
<!-- 액션 -->
<div class="s1-actions" sec:authorize="hasRole('ROLE_API_KEY_REQUEST')">
<div class="s1-actions" sec:authorize="hasRole('ROLE_WEBHOOK')">
<button type="button" class="s1-btn-next" id="requestWebhook">Webhook 신청</button>
</div>
@@ -80,7 +80,7 @@
<span class="group-label">Secret Key</span>
<div class="secret-action-row">
<div class="secret-box" id="secretMasked" th:text="*{secretMasked}">************</div>
<th:block sec:authorize="hasRole('ROLE_API_KEY_REQUEST')">
<th:block sec:authorize="hasRole('ROLE_WEBHOOK')">
<button type="button" class="btn-reveal-action" id="btnRevealSecret">조회</button>
<button type="button" class="btn-regen-action" id="btnRegenSecret">재발급</button>
</th:block>
@@ -91,7 +91,7 @@
</div>
<!-- 액션 -->
<div class="s1-actions" sec:authorize="hasRole('ROLE_API_KEY_REQUEST')">
<div class="s1-actions" sec:authorize="hasRole('ROLE_WEBHOOK')">
<button type="button" class="btn-webhook-danger-figma" id="btnDeleteWebhook">삭제</button>
<a class="s1-btn-next-figma" th:href="@{/webhook/modify/step1}">수정</a>
</div>
@@ -88,12 +88,12 @@
<button class="header-link mypage-toggle" type="button">마이페이지</button>
<div class="mypage-dropdown-menu">
<ul class="mypage-menu-list">
<li sec:authorize="hasRole('ROLE_USER_MANAGER')">
<a th:href="@{/users}"><i class="fas fa-users"></i>이용자 관리</a>
<li sec:authorize="hasRole('ROLE_CORP_MANAGER')">
<a th:href="@{/users}"><i class="fas fa-users"></i>개발자 관리</a>
</li>
<li sec:authorize="hasRole('ROLE_APP')">
<a th:href="@{/myapikey}"><i class="fas fa-key"></i>앱 관리</a>
<a th:href="@{/webhook}" sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"><i class="fas fa-bell"></i>Webhook 관리</a>
<a th:href="@{/webhook}" sec:authorize="hasRole('ROLE_WEBHOOK')"><i class="fas fa-bell"></i>Webhook 관리</a>
<a th:href="@{/statistics/api}"><i class="fas fa-chart-bar"></i>이용 통계</a>
</li>
<li><a th:href="@{/mypage}"><i class="fas fa-user-circle"></i>내 정보 관리</a></li>
@@ -248,9 +248,9 @@
</svg>
</button>
<ul class="drawer-submenu">
<li sec:authorize="hasRole('ROLE_USER_MANAGER')"><a th:href="@{/users}">이용자 관리</a></li>
<li sec:authorize="hasRole('ROLE_CORP_MANAGER')"><a th:href="@{/users}">개발자 관리</a></li>
<li sec:authorize="hasRole('ROLE_APP')"><a th:href="@{/myapikey}">앱 관리</a></li>
<li sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"><a th:href="@{/webhook}">Webhook 관리</a></li>
<li sec:authorize="hasRole('ROLE_WEBHOOK')"><a th:href="@{/webhook}">Webhook 관리</a></li>
<li sec:authorize="hasRole('ROLE_APP')"><a th:href="@{/statistics/api}">이용 통계</a></li>
<li><a th:href="@{/mypage}">내 정보 관리</a></li>
<li><a th:href="@{/password/verify}">비밀번호 변경</a></li>
@@ -53,7 +53,7 @@
<a th:href="@{/users}"
th:classappend="${activeMenu == 'users'} ? 'service-nav__item--active' : ''"
class="service-nav__item"
sec:authorize="hasRole('ROLE_CORP_MANAGER')">이용자 관리</a>
sec:authorize="hasRole('ROLE_CORP_MANAGER')">개발자 관리</a>
<a th:href="@{/myapikey}"
th:classappend="${activeMenu == 'apiKey'} ? 'service-nav__item--active' : ''"
@@ -62,7 +62,7 @@
<a th:href="@{/webhook}"
th:classappend="${activeMenu == 'webhook'} ? 'service-nav__item--active' : ''"
class="service-nav__item"
sec:authorize="hasRole('ROLE_API_KEY_REQUEST')">Webhook 관리</a>
sec:authorize="hasRole('ROLE_WEBHOOK')">Webhook 관리</a>
<a th:href="@{/statistics/api}"
th:classappend="${activeMenu == 'statistics'} ? 'service-nav__item--active' : ''"
@@ -21,19 +21,6 @@
</script>
</section>
<section sec:authorize="hasAuthority('ROLE_CORP_USER')" class="content pop" th:fragment="apiKeyRequestPopup">
<script>
$(document).ready(function() {
(function($) {
$('#requestApiKey').on('click', function(event) {
event.preventDefault();
customPopups.showAlert('법인 관리자만 신청 가능합니다.');
});
})(jQuery);
});
</script>
</section>
<section class="content pop" th:fragment="apiKeyRequestPopup" th:if="${#authorization.expression('!isAuthenticated()')}">
<script>
$(document).ready(function() {
@@ -9,7 +9,7 @@
<!-- Modal Header -->
<div class="modal-header">
<h3 class="modal-title">이용자 추가</h3>
<h3 class="modal-title">개발자 추가</h3>
<button type="button" class="modal-close" id="userInvitePopupCloseButton">
<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>
@@ -21,7 +21,7 @@
<!-- Modal Body -->
<div class="modal-body">
<p id="userInvitePopupMessage" style="text-align: center; margin-bottom: 24px; color: #64748B;">
추가할 이용자 휴대폰 번호를 입력해 주세요.
추가할 개발자 휴대폰 번호를 입력해 주세요.
</p>
<!-- Mobile Input Field -->