인증키 요청 조회, 인증키 상세 조회 디자인 반영

This commit is contained in:
현성필
2025-12-01 15:24:55 +09:00
parent 00c014c73d
commit 1da9b48dd5
18 changed files with 2101 additions and 1293 deletions
@@ -126,273 +126,6 @@
</th:block>
<th:block layout:fragment="contentScript">
<style>
/* App Management Container */
.app-management-container {
max-width: 1228px;
margin: 0 auto;
padding: 60px 20px 80px;
}
/* App List Title Section */
.app-list-title-section {
background-color: #f6f9fb;
border-radius: 12px;
padding: 14px 45px;
margin-bottom: 30px;
}
.app-list-title {
font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif;
font-size: 28px;
font-weight: 700;
color: #212529;
margin: 0;
}
/* App List Container */
.app-list-container {
background-color: #f6f9fb;
border-radius: 12px;
padding: 30px 45px;
min-height: 400px;
}
/* App List Item */
.app-list-item {
display: flex;
align-items: center;
gap: 20px;
padding: 40px 0;
border-bottom: 1px solid #e0e0e0;
text-decoration: none;
color: inherit;
transition: background-color 0.2s ease;
}
.app-list-item:last-child {
border-bottom: none;
}
.app-list-item:hover {
background-color: rgba(255, 255, 255, 0.5);
margin: 0 -20px;
padding-left: 20px;
padding-right: 20px;
border-radius: 8px;
}
/* App Icon */
.app-list-icon {
width: 70px;
height: 70px;
flex-shrink: 0;
}
.app-list-icon .app-icon-image {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 12px;
}
.app-list-icon .app-icon-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: #e9ecef;
border-radius: 12px;
color: #868e96;
}
/* App Info */
.app-list-info {
display: flex;
flex-direction: column;
gap: 14px;
flex: 1;
}
.app-list-header {
display: flex;
align-items: center;
gap: 12px;
}
/* Status Badge */
.app-status-badge {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 5px 14px;
border-radius: 40px;
font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif;
font-size: 13px;
font-weight: 700;
color: #ffffff;
white-space: nowrap;
}
.app-status-badge.status-pending {
background-color: #8c959f;
}
.app-status-badge.status-approved {
background-color: #0049b4;
}
.app-status-badge.status-changing {
background-color: #88a5f3;
}
.app-status-badge.status-inactive {
background-color: #dc3545;
}
/* App Name */
.app-list-name {
font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif;
font-size: 22px;
font-weight: 700;
color: #212529;
margin: 0;
}
/* App Description */
.app-list-description {
font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif;
font-size: 16px;
font-weight: 400;
color: #6e7780;
margin: 0;
line-height: 1.5;
}
/* Empty State */
.app-list-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
text-align: center;
}
.app-list-empty .empty-icon {
font-size: 64px;
margin-bottom: 20px;
}
.app-list-empty h3 {
font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif;
font-size: 24px;
font-weight: 700;
color: #212529;
margin: 0 0 12px;
}
.app-list-empty p {
font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif;
font-size: 16px;
color: #6e7780;
margin: 0;
}
/* Create App Button Wrapper */
.app-create-button-wrapper {
display: flex;
justify-content: center;
margin-top: 60px;
}
/* Create App Button */
.btn-app-create {
width: 220px;
height: 60px;
background-color: #0049b4;
color: #ffffff;
border: none;
border-radius: 12px;
font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif;
font-size: 18px;
font-weight: 700;
cursor: pointer;
transition: background-color 0.2s ease, transform 0.2s ease;
}
.btn-app-create:hover {
background-color: #003a91;
transform: translateY(-2px);
}
.btn-app-create:active {
transform: translateY(0);
}
/* Responsive */
@media screen and (max-width: 768px) {
.app-management-container {
padding: 40px 16px 60px;
}
.app-list-title-section {
padding: 12px 20px;
}
.app-list-title {
font-size: 22px;
}
.app-list-container {
padding: 20px;
}
.app-list-item {
flex-direction: column;
align-items: flex-start;
padding: 30px 0;
gap: 16px;
}
.app-list-item:hover {
margin: 0 -10px;
padding-left: 10px;
padding-right: 10px;
}
.app-list-icon {
width: 60px;
height: 60px;
}
.app-list-header {
flex-wrap: wrap;
gap: 8px;
}
.app-list-name {
font-size: 18px;
}
.app-list-description {
font-size: 14px;
}
.app-create-button-wrapper {
margin-top: 40px;
}
.btn-app-create {
width: 100%;
max-width: 300px;
height: 54px;
font-size: 16px;
}
}
</style>
<script th:inline="javascript">
document.addEventListener('DOMContentLoaded', function() {
@@ -2,206 +2,237 @@
<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/kjbank_base_layout}">
layout:decorate="~{layout/kjbank_title_layout}">
<head>
<title>API 키 신청 상세</title>
<title>API 키 신청 상세</title>
</head>
<body>
<th:block layout:fragment="contentFragment">
<!-- Header -->
<div class="register-header">
<h1>API Key 신청 상세</h1>
<p class="header-description">
신청하신 API Key의 상세 정보를 확인할 수 있습니다.
</p>
<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="apikey-detail-container">
<section class="apikey-detail-container">
<!-- Title Bar -->
<div class="register-title-bar">
<h2 class="register-title">API Key 신청 상세</h2>
<span class="register-subtitle">신청하신 API Key의 상세 정보를 확인할 수 있습니다.</span>
</div>
<!-- Section: 기본 정보 (From Step 1) -->
<div class="detail-section">
<h2 class="section-title">기본 정보</h2>
<div class="detail-grid">
<!-- App Icon -->
<div class="detail-item full-width">
<label class="detail-label">앱 아이콘</label>
<div class="detail-value">
<div class="app-icon-display" th:if="${appRequest.appIconFileId != null}">
<img th:src="@{/file/download(fileSn=1,fileId=${appRequest.appIconFileId})}"
alt="앱 아이콘"
class="app-icon-image">
<!-- App Basic Info Card (Figma Design) -->
<div class="app-info-card">
<!-- App Icon -->
<div class="app-info-icon">
<img th:if="${appRequest.appIconFileId != null}"
th:src="@{/file/download(fileSn=1,fileId=${appRequest.appIconFileId})}"
alt="앱 아이콘"
class="app-icon-image">
<div th:unless="${appRequest.appIconFileId != null}" class="app-icon-placeholder">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21 15 16 10 5 21"></polyline>
</svg>
</div>
</div>
<div class="app-icon-display" th:unless="${appRequest.appIconFileId != null}">
<div class="app-icon-placeholder">
<span>📱</span>
</div>
<!-- App Info -->
<div class="app-info-content">
<!-- Status Badge -->
<span class="app-status-badge"
th:if="${appRequest.approval != null}"
th:classappend="${appRequest.approval.approvalStatus.toString() == 'APPROVED' ? 'status-approved' :
(appRequest.approval.approvalStatus.toString() == 'REJECTED' ? 'status-rejected' : 'status-pending')}"
th:text="${appRequest.approval.approvalStatus.description}">
대기중
</span>
<!-- App Name -->
<h3 class="app-info-name" th:text="${appRequest.clientName}">앱 이름</h3>
<!-- App Description -->
<p class="app-info-description"
th:text="${appRequest.appDescription != null ? appRequest.appDescription : '앱 설명이 없습니다.'}">
여기에 입력한 앱 설명이 표시됩니다.
</p>
</div>
</div>
</div>
<!-- App Name -->
<div class="detail-item">
<label class="detail-label">앱 이름</label>
<div class="detail-value" th:text="${appRequest.clientName}">
My Application
</div>
</div>
<!-- Request Info Section (Figma Design: node 985-1365) -->
<div class="apikey-info-section">
<!-- Client ID (if exists) -->
<div class="detail-item" th:if="${appRequest.clientId != null}">
<label class="detail-label">Client ID</label>
<div class="detail-value" th:text="${appRequest.clientId}">
client-id-12345
</div>
</div>
<!-- App Description -->
<div class="detail-item full-width">
<label class="detail-label">앱 설명</label>
<div class="detail-value" th:text="${appRequest.appDescription}">
This is a sample app description.
</div>
</div>
<!-- Callback URL -->
<div class="detail-item full-width">
<label class="detail-label">Callback URL</label>
<div class="detail-value" th:text="${appRequest.callbackUrl}">
https://example.com/callback
</div>
</div>
<!-- IP Whitelist -->
<div class="detail-item full-width" th:if="${appRequest.ipWhitelist != null and !appRequest.ipWhitelist.isEmpty()}">
<label class="detail-label">IP 화이트리스트</label>
<div class="detail-value">
<div class="ip-list">
<span class="ip-tag" th:each="ip : ${#strings.arraySplit(appRequest.ipWhitelist, ',')}">
[[${ip.trim()}]]
</span>
<!-- Client ID (if exists) -->
<div class="info-row info-row-with-action" th:if="${appRequest.clientId != null}">
<label class="info-label">Client ID</label>
<div class="info-value">
<div class="info-value-box with-copy" th:text="${appRequest.clientId}">K00000001</div>
<button type="button" class="btn-copy-action" th:data-secret="${appRequest.clientId}" onclick="copyToClipboardFromButton(this)">
+ 복사
</button>
</div>
</div>
</div>
<!-- Callback URL -->
<div class="info-row">
<label class="info-label">Callback URL</label>
<div class="info-value">
<div class="info-value-box" th:text="${appRequest.callbackUrl != null ? appRequest.callbackUrl : '설정되지 않음'}">
http://www.abcd.co.kr
</div>
</div>
</div>
<!-- IP Whitelist -->
<div class="info-row">
<label class="info-label">화이트리스트</label>
<div class="info-value">
<div class="info-value-box" th:text="${appRequest.ipWhitelist != null and !appRequest.ipWhitelist.isEmpty() ? appRequest.ipWhitelist : '설정되지 않음'}">
10.0.0.1, 10.0.0.2, 192.168.123.1
</div>
</div>
</div>
<!-- Selected APIs -->
<div class="info-row info-row-vertical">
<label class="info-label">API 목록</label>
<div class="info-value">
<div class="api-list-box">
<div class="api-list-item-figma" th:each="api : ${appRequest.apiSpecList}">
<span class="api-item-name" th:text="${api.apiName}">추가된 API 1</span>
<span class="api-item-status">승인대기</span>
</div>
<!-- Empty State -->
<div class="empty-state" th:if="${appRequest.apiSpecList == null or appRequest.apiSpecList.isEmpty()}" style="padding: 24px;">
<p>선택된 API가 없습니다.</p>
</div>
</div>
</div>
</div>
<!-- Created Date -->
<div class="info-row">
<label class="info-label">신청일시</label>
<div class="info-value">
<div class="info-value-box" th:text="${#temporals.format(appRequest.createdDate, 'yyyy.MM.dd HH:mm')}">
2025.11.22 12:00
</div>
</div>
</div>
<!-- Approval Date (if approved) -->
<div class="info-row" th:if="${appRequest.approval != null and appRequest.approval.approvalDate != null}">
<label class="info-label">승인일시</label>
<div class="info-value">
<div class="info-value-box" th:text="${#temporals.format(appRequest.approval.approvalDate, 'yyyy.MM.dd HH:mm')}">
2025.11.22 12:00
</div>
</div>
</div>
<!-- Reason (if exists) -->
<div class="info-row" th:if="${appRequest.approval != null and appRequest.reason != null and !appRequest.reason.isEmpty()}">
<label class="info-label">사유</label>
<div class="info-value">
<div class="info-value-box" th:text="${appRequest.reason}">
Approval reason
</div>
</div>
</div>
</div>
<!-- Created Date -->
<div class="detail-item">
<label class="detail-label">신청일시</label>
<div class="detail-value" th:text="${#temporals.format(appRequest.createdDate, 'yyyy.MM.dd HH:mm')}">
2024.01.01 12:00
</div>
</div>
<div class="detail-item" th:if="${appRequest.approval != null}">
<label class="detail-label">승인 상태</label>
<div class="detail-value" th:text="${appRequest.approval.approvalStatus.description}">
승인 대기
</div>
</div>
<div class="detail-item" th:if="${appRequest.approval != null and appRequest.approval.approvalDate != null}">
<label class="detail-label">승인일시</label>
<div class="detail-value" th:text="${#temporals.format(appRequest.approval.approvalDate, 'yyyy.MM.dd HH:mm')}">
2024.01.01 12:00
</div>
</div>
<div class="detail-item full-width" th:if="${appRequest.approval != null and appRequest.reason != null and !appRequest.reason.isEmpty()}">
<label class="detail-label">사유</label>
<div class="detail-value" th:text="${appRequest.reason}">
Approval reason
</div>
</div>
</div>
</div>
<!-- Section: API 정보 (From Step 2) -->
<div class="detail-section">
<h2 class="section-title">선택된 API</h2>
<!-- API List -->
<div class="api-detail-list">
<div class="api-list-item" th:each="api : ${appRequest.apiSpecList}">
<span class="api-method"
th:classappend="${api.apiMethod == 'GET' ? 'method-get' :
(api.apiMethod == 'POST' ? 'method-post' :
(api.apiMethod == 'PUT' ? 'method-put' :
(api.apiMethod == 'DELETE' ? 'method-delete' : 'method-other')))}">
[[${api.apiMethod}]]
</span>
<span class="api-name" th:text="${api.apiName}">API Name</span>
<span class="api-status status-pending">대기중</span>
</div>
<!-- Empty State -->
<div class="empty-state" th:if="${appRequest.apiSpecList == null or appRequest.apiSpecList.isEmpty()}">
<div class="empty-icon">📦</div>
<p>선택된 API가 없습니다.</p>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="detail-actions">
<button type="button"
sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"
class="btn btn-secondary"
th:if="${appRequest.approval != null and appRequest.approval.approvalStatus != null and
<!-- Action Buttons -->
<div class="form-actions-center">
<button type="button"
sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"
class="btn btn-submit btn-danger"
th:if="${appRequest.approval != null and appRequest.approval.approvalStatus != null and
(appRequest.approval.approvalStatus.toString() == 'PENDING' or appRequest.approval.approvalStatus.toString() == 'REQUESTED')}"
th:data-request-id="${appRequest.id}"
onclick="cancelRequestById(this)">
신청 취소
</button>
<a th:href="@{/myapikey}" class="btn btn-primary">
목록으로
</a>
</div>
th:data-request-id="${appRequest.id}"
onclick="cancelRequestById(this)">
신청 취소
</button>
<a th:href="@{/myapikey}" class="btn btn-submit btn-secondary">
목록
</a>
</div>
</section>
</section>
</th:block>
<th:block layout:fragment="contentScript">
<script th:inline="javascript">
/*<![CDATA[*/
<script th:inline="javascript">
/*<![CDATA[*/
// Cancel request function - called from button with data-request-id attribute
function cancelRequestById(button) {
var requestId = $(button).data('request-id');
// Copy to clipboard function
function copyToClipboardFromButton(button) {
var text = $(button).data('secret');
if (!confirm('정말로 이 신청을 취소하시겠습니까?')) {
return;
}
$('.loading-overlay').show();
$.ajax({
url: '/myapikey/api_key_request/cancel',
type: 'POST',
data: {id: requestId},
dataType: 'json',
headers: {
'X-XSRF-TOKEN': /*[[${_csrf.token}]]*/ 'token'
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function() {
alert('클립보드에 복사되었습니다.');
}).catch(function(err) {
console.error('Failed to copy:', err);
fallbackCopy(text);
});
} else {
fallbackCopy(text);
}
}
}).done(function(response) {
if (response.success) {
alert(response.message || '신청이 취소되었습니다.');
window.location.href = /*[[@{/myapikey}]]*/ '/myapikey';
} else {
alert(response.message || '신청 취소 중 오류가 발생했습니다.');
$('.loading-overlay').hide();
}
}).fail(function(xhr, status, error) {
alert('신청 취소 중 오류가 발생했습니다.');
console.error('Cancel error:', error);
$('.loading-overlay').hide();
});
}
/*]]>*/
</script>
function fallbackCopy(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
alert('클립보드에 복사되었습니다.');
} catch (err) {
alert('복사에 실패했습니다.');
}
document.body.removeChild(textArea);
}
// Cancel request function - called from button with data-request-id attribute
function cancelRequestById(button) {
var requestId = $(button).data('request-id');
if (!confirm('정말로 이 신청을 취소하시겠습니까?')) {
return;
}
$('.loading-overlay').show();
$.ajax({
url: '/myapikey/api_key_request/cancel',
type: 'POST',
data: {id: requestId},
dataType: 'json',
headers: {
'X-XSRF-TOKEN': /*[[${_csrf.token}]]*/ 'token'
}
}).done(function(response) {
if (response.success) {
alert(response.message || '신청이 취소되었습니다.');
window.location.href = /*[[@{/myapikey}]]*/ '/myapikey';
} else {
alert(response.message || '신청 취소 중 오류가 발생했습니다.');
$('.loading-overlay').hide();
}
}).fail(function(xhr, status, error) {
alert('신청 취소 중 오류가 발생했습니다.');
console.error('Cancel error:', error);
$('.loading-overlay').hide();
});
}
/*]]>*/
</script>
</th:block>
</body>
@@ -2,313 +2,281 @@
<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/kjbank_base_layout}">
layout:decorate="~{layout/kjbank_title_layout}">
<head>
<title>API 키 상세</title>
<title>API 키 상세</title>
</head>
<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="apikey-detail-container">
<!-- Header -->
<div class="register-header">
<h1>API Key 상세</h1>
<p class="header-description">
발급된 API Key의 상세 정보를 확인할 수 있습니다.
</p>
<!-- Title Bar -->
<div class="register-title-bar">
<h2 class="register-title">앱 정보</h2>
<span class="register-subtitle">등록된 앱의 상세 정보를 확인할 수 있습니다.</span>
</div>
<section class="apikey-detail-container">
<!-- App Basic Info Card (Figma Design) -->
<div class="app-info-card">
<!-- App Icon -->
<div class="app-info-icon">
<img th:if="${apiKey.appIconFileId != null}"
th:src="@{/file/download(fileSn=1,fileId=${apiKey.appIconFileId})}"
alt="앱 아이콘"
class="app-icon-image">
<div th:unless="${apiKey.appIconFileId != null}" class="app-icon-placeholder">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21 15 16 10 5 21"></polyline>
</svg>
</div>
</div>
<!-- App Info -->
<div class="app-info-content">
<!-- Status Badge -->
<span class="app-status-badge"
th:classappend="${apiKey.appstatus == '1' ? 'status-approved' : 'status-inactive'}"
th:text="${apiKey.appstatus == '1' ? '승인' : '비활성화'}">
승인
</span>
<!-- App Name -->
<h3 class="app-info-name" th:text="${apiKey.clientname}">앱 이름</h3>
<!-- App Description -->
<p class="app-info-description"
th:text="${apiKey.appDescription != null ? apiKey.appDescription : '앱 설명이 없습니다.'}">
여기에 입력한 앱 설명이 표시됩니다.
</p>
</div>
</div>
<!-- Section 1: 앱 기본 정보 -->
<div class="detail-section">
<h2 class="section-title">앱 정보</h2>
<div class="detail-grid">
<!-- Credential Info Section (Figma Design: node 985-1365) -->
<div class="apikey-info-section">
<!-- App Icon -->
<div class="detail-item full-width">
<label class="detail-label">앱 아이콘</label>
<div class="detail-value">
<div class="app-icon-display" th:if="${apiKey.appIconFileId != null}">
<img th:src="@{/file/download(fileSn=1,fileId=${apiKey.appIconFileId})}"
alt="앱 아이콘"
class="app-icon-image">
</div>
<div class="app-icon-display" th:unless="${apiKey.appIconFileId != null}">
<div class="app-icon-placeholder">
<span>📱</span>
</div>
</div>
</div>
</div>
<!-- App Name -->
<div class="detail-item">
<label class="detail-label">앱 이름</label>
<div class="detail-value" th:text="${apiKey.clientname}">
My Application
</div>
</div>
<!-- App Status -->
<div class="detail-item">
<label class="detail-label">앱 상태</label>
<div class="detail-value">
<span class="status-indicator"
th:classappend="${apiKey.appstatus == '1' ? 'status-active' : 'status-inactive'}">
<span class="status-dot"></span>
<span th:text="${apiKey.appstatus == '1' ? '활성화' : '비활성화'}">활성화</span>
</span>
</div>
</div>
<!-- App Description -->
<div class="detail-item full-width">
<label class="detail-label">앱 설명</label>
<div class="detail-value" th:text="${apiKey.appDescription != null ? apiKey.appDescription : '설명이 없습니다.'}">
This is a sample app description.
</div>
</div>
<!-- Client ID -->
<div class="info-row info-row-with-action">
<label class="info-label">Client ID</label>
<div class="info-value">
<div class="info-value-box with-copy" th:text="${apiKey.clientid}">K00000001</div>
<button type="button" class="btn-copy-action" th:data-secret="${apiKey.clientid}" onclick="copyToClipboardFromButton(this)">
+ 복사
</button>
</div>
</div>
<!-- Client Secret -->
<div class="info-row">
<label class="info-label">Client Secret</label>
<div class="info-value">
<!-- View Button (shown initially) -->
<button type="button" class="btn-view-secret" id="hiddenSecretBox" onclick="showPasswordPrompt()">
<span>조회</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.35-4.35"></path>
</svg>
</button>
<!-- Actual Secret (hidden initially) -->
<div id="revealedSecretBox" style="display: none;">
<div class="info-row info-row-with-action" style="margin-bottom: 0;">
<div class="info-value" style="display: flex; gap: 16px; align-items: center;">
<div class="info-value-box with-copy" th:text="${apiKey.clientsecret}">secret-key-67890</div>
<button type="button" class="btn-copy-action" th:data-secret="${apiKey.clientsecret}" onclick="copyToClipboardFromButton(this)">
+ 복사
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Section 2: 인증 상세 정보 -->
<div class="detail-section">
<h2 class="section-title">인증 정보</h2>
<div class="detail-grid">
<!-- Callback URL -->
<div class="info-row">
<label class="info-label">Callback URL</label>
<div class="info-value">
<div class="info-value-box" th:text="${apiKey.redirecturi != null ? apiKey.redirecturi : '설정되지 않음'}">
http://www.abcd.co.kr
</div>
</div>
</div>
<!-- Client ID -->
<div class="detail-item">
<label class="detail-label">Client ID</label>
<div class="detail-value">
<div class="secret-box">
<code class="credential-code" th:text="${apiKey.clientid}">client-id-12345</code>
<button type="button" class="btn-copy" th:data-secret="${apiKey.clientid}" onclick="copyToClipboardFromButton(this)">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
복사
</button>
</div>
</div>
</div>
<!-- Client Secret -->
<div class="detail-item">
<label class="detail-label">Client Secret</label>
<div class="detail-value">
<!-- Hidden Secret (shown initially) -->
<div class="secret-box" id="hiddenSecretBox">
<code class="credential-code">••••••••••••••••••••</code>
<button type="button" class="btn-copy" onclick="showPasswordPrompt()">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
보기
</button>
</div>
<!-- Actual Secret (hidden initially) -->
<div class="secret-box" id="revealedSecretBox" style="display: none;">
<code class="credential-code" th:text="${apiKey.clientsecret}">secret-key-67890</code>
<button type="button" class="btn-copy" th:data-secret="${apiKey.clientsecret}" onclick="copyToClipboardFromButton(this)">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
복사
</button>
</div>
</div>
</div>
<!-- Callback URL -->
<div class="detail-item full-width">
<label class="detail-label">Callback URL</label>
<div class="detail-value" th:text="${apiKey.redirecturi != null ? apiKey.redirecturi : '설정되지 않음'}">
https://example.com/callback
</div>
</div>
<!-- IP Whitelist -->
<div class="detail-item full-width" th:if="${apiKey.allowedips != null}">
<label class="detail-label">IP 화이트리스트</label>
<div class="detail-value">
<div class="ip-list">
<span class="ip-tag" th:each="ip : ${#strings.arraySplit(apiKey.allowedips, ',')}">
[[${ip.trim()}]]
</span>
</div>
</div>
</div>
<!-- 등록된 API -->
<div class="detail-item full-width">
<label class="detail-label">등록된 API</label>
<div class="detail-value">
<!-- API List -->
<div class="api-detail-list">
<div class="api-list-item" th:each="api : ${apiKey.apiList}">
<span class="api-method method-other">
API
</span>
<span class="api-name" th:text="${api.apiDesc}">API Name</span>
<span class="api-status status-active">활성</span>
</div>
<!-- Empty State -->
<div class="empty-state" th:if="${apiKey.apiList == null or apiKey.apiList.isEmpty()}">
<div class="empty-icon">📦</div>
<p>등록된 API가 없습니다.</p>
</div>
</div>
</div>
</div>
<!-- Modified Date -->
<div class="detail-item">
<label class="detail-label">최종 수정일자</label>
<div class="detail-value" th:text="${#temporals.format(apiKey.modifiedon, 'yyyy.MM.dd HH:mm')}">
2024.01.01 12:00
</div>
</div>
<!-- IP Whitelist -->
<div class="info-row">
<label class="info-label">화이트리스트</label>
<div class="info-value">
<div class="info-value-box" th:text="${apiKey.allowedips != null ? apiKey.allowedips : '설정되지 않음'}">
10.0.0.1, 10.0.0.2, 192.168.123.1
</div>
</div>
</div>
<!-- Registered APIs -->
<div class="info-row info-row-vertical">
<label class="info-label">API 목록</label>
<div class="info-value">
<div class="api-list-box">
<div class="api-list-item-figma" th:each="api : ${apiKey.apiList}">
<span class="api-item-name" th:text="${api.apiDesc}">추가된 API 1</span>
<span class="api-item-status">승인</span>
</div>
</div>
<!-- Action Buttons -->
<div class="form-actions">
<a sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"
class="btn btn-primary"
th:href="@{/myapikey/modify/step1(clientId=${apiKey.clientid})}">
수정
</a>
<button type="button"
sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"
class="btn btn-danger"
th:data-client-id="${apiKey.clientid}"
onclick="deleteApiKeyFromButton(this)">
인증키 삭제
</button>
<a th:href="@{/myapikey}" class="btn btn-secondary">
목록으로
</a>
<!-- Empty State -->
<div class="empty-state" th:if="${apiKey.apiList == null or apiKey.apiList.isEmpty()}" style="padding: 24px;">
<p>등록된 API가 없습니다.</p>
</div>
</div>
</div>
</div>
</section>
<!-- Modified Date -->
<div class="info-row">
<label class="info-label">최종 수정일자</label>
<div class="info-value">
<div class="info-value-box" th:text="${#temporals.format(apiKey.modifiedon, 'yyyy.MM.dd')}">
2025.11.22
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="form-actions-center">
<a th:href="@{/myapikey}" class="btn btn-submit btn-secondary">
목록
</a>
<button type="button"
sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"
class="btn btn-submit btn-danger"
th:data-client-id="${apiKey.clientid}"
onclick="deleteApiKeyFromButton(this)">
인증키 삭제
</button>
<a sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"
class="btn btn-submit btn-primary"
th:href="@{/myapikey/modify/step1(clientId=${apiKey.clientid})}">
수정
</a>
</div>
</section>
</th:block>
<th:block layout:fragment="contentScript">
<script th:inline="javascript">
/*<![CDATA[*/
<script th:inline="javascript">
/*<![CDATA[*/
// Show password prompt for viewing client secret
function showPasswordPrompt() {
customPopups.showPasswordInput({
title: 'Client Secret 조회',
message: '보안을 위해 비밀번호를 입력해주세요.',
onConfirm: function(password) {
// Verify password via AJAX
$.ajax({
url: /*[[@{/myapikey/verify-password}]]*/ '/myapikey/verify-password',
type: 'POST',
data: { password: password },
headers: {
'X-XSRF-TOKEN': /*[[${_csrf.token}]]*/ 'token'
},
success: function(response) {
if (response.success) {
// Hide password popup
customPopups.hidePasswordInput();
// Show password prompt for viewing client secret
function showPasswordPrompt() {
customPopups.showPasswordInput({
title: 'Client Secret 조회',
message: '보안을 위해 비밀번호를 입력해주세요.',
onConfirm: function(password) {
// Verify password via AJAX
$.ajax({
url: /*[[@{/myapikey/verify-password}]]*/ '/myapikey/verify-password',
type: 'POST',
data: {password: password},
headers: {
'X-XSRF-TOKEN': /*[[${_csrf.token}]]*/ 'token'
},
success: function(response) {
if (response.success) {
// Hide password popup
customPopups.hidePasswordInput();
// Reveal the client secret
$('#hiddenSecretBox').hide();
$('#revealedSecretBox').fadeIn(300);
} else {
// Show error message
customPopups.showPasswordError(response.message || '비밀번호가 일치하지 않습니다.');
}
},
error: function() {
customPopups.showPasswordError('오류가 발생했습니다. 다시 시도해주세요.');
}
});
},
onCancel: function() {
// User cancelled - do nothing
}
});
}
// Copy to clipboard function - called from button with data-secret attribute
function copyToClipboardFromButton(button) {
var text = $(button).data('secret');
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function() {
alert('클립보드에 복사되었습니다.');
}).catch(function(err) {
console.error('Failed to copy:', err);
fallbackCopy(text);
});
} else {
fallbackCopy(text);
// Reveal the client secret
$('#hiddenSecretBox').hide();
$('#revealedSecretBox').fadeIn(300);
} else {
// Show error message
customPopups.showPasswordError(response.message || '비밀번호가 일치하지 않습니다.');
}
},
error: function() {
customPopups.showPasswordError('오류가 발생했습니다. 다시 시도해주세요.');
}
});
},
onCancel: function() {
// User cancelled - do nothing
}
});
}
function fallbackCopy(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
alert('클립보드에 복사되었습니다.');
} catch (err) {
alert('복사에 실패했습니다.');
}
document.body.removeChild(textArea);
// Copy to clipboard function - called from button with data-secret attribute
function copyToClipboardFromButton(button) {
var text = $(button).data('secret');
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function() {
alert('클립보드에 복사되었습니다.');
}).catch(function(err) {
console.error('Failed to copy:', err);
fallbackCopy(text);
});
} else {
fallbackCopy(text);
}
}
function fallbackCopy(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
alert('클립보드에 복사되었습니다.');
} catch (err) {
alert('복사에 실패했습니다.');
}
document.body.removeChild(textArea);
}
// Delete API Key function - called from button with data-client-id attribute
function deleteApiKeyFromButton(button) {
var clientId = $(button).data('client-id');
if (!confirm('정말로 이 인증키를 삭제하시겠습니까?')) {
return;
}
$('.loading-overlay').show();
const requestData = {
clientId: clientId
};
$.ajax({
url: '/myapikey/api_key_delete',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(requestData),
headers: {
'X-XSRF-TOKEN': /*[[${_csrf.token}]]*/ 'token'
}
}).done(function(response) {
alert(response.msg || 'API Key가 삭제되었습니다.');
window.location.href = /*[[@{/myapikey}]]*/ '/myapikey';
}).fail(function(jqXHR, textStatus, errorThrown) {
alert('API 삭제 요청 중 오류가 발생했습니다: ' + errorThrown);
}).always(function() {
$('.loading-overlay').hide();
});
}
// Delete API Key function - called from button with data-client-id attribute
function deleteApiKeyFromButton(button) {
var clientId = $(button).data('client-id');
if (!confirm('정말로 이 인증키를 삭제하시겠습니까?')) {
return;
}
$('.loading-overlay').show();
const requestData = {
clientId: clientId
};
$.ajax({
url: '/myapikey/api_key_delete',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(requestData),
headers: {
'X-XSRF-TOKEN': /*[[${_csrf.token}]]*/ 'token'
}
}).done(function (response) {
alert(response.msg || 'API Key가 삭제되었습니다.');
window.location.href = /*[[@{/myapikey}]]*/ '/myapikey';
}).fail(function (jqXHR, textStatus, errorThrown) {
alert('API 삭제 요청 중 오류가 발생했습니다: ' + errorThrown);
}).always(function () {
$('.loading-overlay').hide();
});
}
/*]]>*/
</script>
/*]]>*/
</script>
</th:block>