클라이언트 생성/수정 흐름 및 관련 UI 개선:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled

- 클라이언트 등록/수정 URL 구조 리팩토링 (myapikey → clients)
- 로그인 직후 클라이언트 미보유 사용자 대상 추가 안내 팝업
- 클라이언트 Secret 조회 및 관련 2FA 단계 로직 개선
This commit is contained in:
Rinjae
2026-07-29 19:25:10 +09:00
parent c02a5ff047
commit 02eb81af67
37 changed files with 535 additions and 197 deletions
@@ -25,7 +25,7 @@
<div class="detail-wrap">
<!-- Title Bar -->
<div class="board-header">
<h2 class="board-title">인증키 정보</h2>
<h2 class="board-title">클라이언트 정보</h2>
<span class="board-desc">등록된 앱의 상세 정보를 확인할 수 있습니다.</span>
</div>
@@ -188,11 +188,11 @@
<!-- Bottom Navigation Actions -->
<div class="dt-actions" style="justify-content: flex-end; gap: 11px; margin-top: 30px;">
<a th:href="@{/myapikey}" class="dt-btn-gray">목록</a>
<a th:href="@{/clients}" class="dt-btn-gray">목록</a>
<button type="button" sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" class="dt-btn-red"
th:data-client-id="${apiKey.clientid}" onclick="deleteApiKeyFromButton(this)">API 이용 해지</button>
<a sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" class="dt-btn-blue"
th:href="@{/myapikey/modify/step1(clientId=${apiKey.clientid})}">수정</a>
th:href="@{/clients/modify/step1(clientId=${apiKey.clientid})}">변경 신청</a>
</div>
</div><!-- /detail-wrap -->
@@ -218,43 +218,48 @@
);
}
// Show password prompt for viewing client secret (최초 1회 노출 + 서버측 물리 삭제)
// Client Secret 조회 진입 — 확인 후 조회 (본인 확인은 step-up 2FA)
function showPasswordPrompt() {
customPopups.showPasswordInput({
title: 'Client Secret 조회',
message: '보안을 위해 비밀번호를 입력해주세요.<br>조회 즉시 값은 영구 삭제됩니다.',
onConfirm: function (password) {
$.ajax({
url: /*[[@{/myapikey/credential/reveal-secret}]]*/ '/myapikey/credential/reveal-secret',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ clientId: CREDENTIAL_CLIENT_ID, password: password }),
headers: {
'X-XSRF-TOKEN': /*[[${_csrf.token}]]*/ 'token'
}
}).done(function (response) {
if (response.success) {
customPopups.hidePasswordInput();
// 서버가 반환한 secret을 화면에 1회 주입
$('#revealedSecretValue').text(response.secret);
$('#revealedSecretCopyBtn').attr('data-secret', response.secret).data('secret', response.secret);
$('#hiddenSecretBox').hide();
$('#revealedSecretBox').fadeIn(300);
} else if (response.alreadyRevealed) {
customPopups.hidePasswordInput();
showLostKeyGuide();
} else {
customPopups.showPasswordError(response.message || '비밀번호가 일치하지 않습니다.');
}
}).fail(function () {
customPopups.showPasswordError('오류가 발생했습니다. 다시 시도해주세요.');
});
},
onCancel: function () {
// User cancelled - do nothing
customPopups.showConfirm(
'Client Secret은 <strong>지금 한 번만</strong> 조회할 수 있으며,<br>조회 즉시 값은 <strong>영구 삭제</strong>됩니다.<br><br>조회하시겠습니까?',
function (confirmed) {
if (!confirmed) {
return;
}
doRevealSecret();
}
);
}
// Secret 조회 요청 (step-up 필요 시 2FA 후 동일 요청 재시도)
function doRevealSecret() {
$.ajax({
url: /*[[@{/clients/credential/reveal-secret}]]*/ '/clients/credential/reveal-secret',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ clientId: CREDENTIAL_CLIENT_ID }),
headers: {
'X-XSRF-TOKEN': /*[[${_csrf.token}]]*/ 'token'
}
}).done(function (response) {
if (response.success) {
// 서버가 반환한 secret을 화면에 1회 주입
$('#revealedSecretValue').text(response.secret);
$('#revealedSecretCopyBtn').attr('data-secret', response.secret).data('secret', response.secret);
$('#hiddenSecretBox').hide();
$('#revealedSecretBox').fadeIn(300);
} else if (response.alreadyRevealed) {
showLostKeyGuide();
} else {
customPopups.showAlert(response.message || 'Client Secret 조회에 실패했습니다.');
}
}).fail(function (jqXHR) {
if (isStepUpRequired(jqXHR)) {
requireStepUp('/clients/credential/reveal-secret', function () { doRevealSecret(); });
return;
}
customPopups.showAlert('오류가 발생했습니다. 다시 시도해주세요.');
});
}
@@ -307,7 +312,7 @@
$('.loading-overlay').show();
$.ajax({
url: /*[[@{/myapikey/api_key_delete}]]*/ '/myapikey/api_key_delete',
url: /*[[@{/clients/api_key_delete}]]*/ '/clients/api_key_delete',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ clientId: clientId }),
@@ -320,11 +325,11 @@
return;
}
customPopups.showAlert(response.msg || 'API Key가 삭제되었습니다.', function () {
window.location.href = /*[[@{/myapikey}]]*/ '/myapikey';
window.location.href = /*[[@{/clients}]]*/ '/clients';
});
}).fail(function (jqXHR, textStatus, errorThrown) {
if (isStepUpRequired(jqXHR)) {
requireStepUp('/myapikey/api_key_delete', function () { doDeleteApiKey(clientId); });
requireStepUp('/clients/api_key_delete', function () { doDeleteApiKey(clientId); });
return;
}
customPopups.showAlert('API 삭제 요청 중 오류가 발생했습니다: ' + errorThrown);