512 lines
19 KiB
HTML
512 lines
19 KiB
HTML
<!DOCTYPE html>
|
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
|
|
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
|
|
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
|
|
|
|
<body>
|
|
<th:block layout:fragment="contentFragment">
|
|
<section class="apikey-register-container">
|
|
|
|
<!-- Progress Indicator -->
|
|
<div class="register-progress">
|
|
<div class="progress-steps">
|
|
<div class="progress-step active">
|
|
<div class="step-number">1</div>
|
|
<div class="step-label">기본 정보</div>
|
|
</div>
|
|
<div class="progress-line"></div>
|
|
<div class="progress-step">
|
|
<div class="step-number">2</div>
|
|
<div class="step-label">API 선택</div>
|
|
</div>
|
|
<div class="progress-line"></div>
|
|
<div class="progress-step">
|
|
<div class="step-number">3</div>
|
|
<div class="step-label">완료</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Header -->
|
|
<div class="register-header">
|
|
<h1>API Key 생성</h1>
|
|
<h2>Step 1: 기본 정보 입력</h2>
|
|
<p class="header-description">
|
|
앱의 기본 정보를 입력해주세요. 이 정보는 API 관리 및 모니터링에 사용됩니다.
|
|
</p>
|
|
|
|
<!-- Error Message -->
|
|
<div th:if="${error}" class="alert alert-error" style="margin-top: 20px;">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<circle cx="12" cy="12" r="10"></circle>
|
|
<line x1="12" y1="8" x2="12" y2="12"></line>
|
|
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
|
</svg>
|
|
<span th:text="${error}"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Form Content -->
|
|
<div class="register-content">
|
|
<form id="registerStep1Form" method="post" th:action="@{/myapikey/register/step1}" th:object="${apiKeyRegistration}" class="register-form" enctype="multipart/form-data">
|
|
|
|
<div class="form-card">
|
|
|
|
<!-- App Icon -->
|
|
<div class="form-group">
|
|
<label for="appIcon" class="form-label required">
|
|
앱 아이콘
|
|
<span class="required-mark">*</span>
|
|
</label>
|
|
<div class="icon-upload-wrapper">
|
|
<div class="icon-preview" id="iconPreview">
|
|
<div class="icon-placeholder">
|
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
|
<polyline points="17 8 12 3 7 8"></polyline>
|
|
<line x1="12" y1="3" x2="12" y2="15"></line>
|
|
</svg>
|
|
<span>이미지 선택</span>
|
|
</div>
|
|
<img id="iconPreviewImage" style="display: none;" alt="Icon Preview">
|
|
<button type="button" class="btn-remove-icon" id="btnRemoveIcon" style="display: none;" onclick="removeAppIcon()" title="아이콘 삭제">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<input type="file"
|
|
id="appIcon"
|
|
name="appIcon"
|
|
class="icon-input"
|
|
accept="image/png,image/jpeg,image/jpg"
|
|
required>
|
|
<button type="button" class="btn-icon-select" onclick="document.getElementById('appIcon').click()">
|
|
파일 선택
|
|
</button>
|
|
</div>
|
|
<small class="form-hint">PNG, JPG 형식 (최대 2MB, 권장 크기: 512x512px)</small>
|
|
</div>
|
|
|
|
<!-- App Name -->
|
|
<div class="form-group">
|
|
<label for="appName" class="form-label required">
|
|
앱 이름
|
|
<span class="required-mark">*</span>
|
|
</label>
|
|
<input type="text"
|
|
id="appName"
|
|
name="appName"
|
|
th:field="*{appName}"
|
|
class="form-input"
|
|
placeholder="예: My Service API Client"
|
|
required
|
|
maxlength="100">
|
|
<small class="form-hint">API Key를 식별할 수 있는 고유한 이름을 입력하세요.</small>
|
|
</div>
|
|
|
|
<!-- App Description -->
|
|
<div class="form-group">
|
|
<label for="appDescription" class="form-label required">
|
|
앱 설명
|
|
<span class="required-mark">*</span>
|
|
</label>
|
|
<textarea id="appDescription"
|
|
name="appDescription"
|
|
th:field="*{appDescription}"
|
|
class="form-textarea"
|
|
placeholder="이 앱이 어떤 용도로 사용되는지 간단히 설명해주세요."
|
|
rows="4"
|
|
required
|
|
maxlength="500"></textarea>
|
|
<small class="form-hint">
|
|
<span class="char-counter">0 / 500</span>
|
|
</small>
|
|
</div>
|
|
|
|
<!-- Callback URL -->
|
|
<div class="form-group">
|
|
<label for="callbackUrl" class="form-label">
|
|
Callback URL
|
|
</label>
|
|
<input type="text"
|
|
id="callbackUrl"
|
|
name="callbackUrl"
|
|
th:field="*{callbackUrl}"
|
|
class="form-input"
|
|
placeholder="https://example.com/api/callback">
|
|
<small class="form-hint">OAuth 인증 후 리다이렉트될 URL을 입력하세요. (선택사항)</small>
|
|
</div>
|
|
|
|
<!-- IP Whitelist -->
|
|
<div class="form-group">
|
|
<label for="ipWhitelist" class="form-label">
|
|
IP 화이트리스트
|
|
</label>
|
|
<div class="ip-input-group">
|
|
<input type="text"
|
|
id="ipWhitelistInput"
|
|
class="form-input"
|
|
placeholder="예: 192.168.1.1 또는 10.0.0.0/24">
|
|
<button type="button" class="btn-add-ip" onclick="addIpAddress()">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
</svg>
|
|
추가
|
|
</button>
|
|
</div>
|
|
<small class="form-hint">허용할 IP 주소 또는 CIDR 대역을 입력하세요. (선택사항)</small>
|
|
|
|
<!-- IP List Display -->
|
|
<div class="ip-list" id="ipList" style="display: none;">
|
|
<div class="ip-list-header">
|
|
<span class="ip-count">등록된 IP: <strong id="ipCount">0</strong>개</span>
|
|
</div>
|
|
<div class="ip-items" id="ipItems">
|
|
<!-- Dynamically added IP items -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Hidden input to store IP list -->
|
|
<input type="hidden" id="ipWhitelist" name="ipWhitelist" value="">
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Form Actions -->
|
|
<div class="form-actions">
|
|
<a href="/myapikey" class="btn-secondary" id="btnCancel">
|
|
<span class="icon">←</span>
|
|
취소
|
|
</a>
|
|
<button type="submit" class="btn-primary">
|
|
다음 단계
|
|
<span class="icon">→</span>
|
|
</button>
|
|
</div>
|
|
|
|
</form>
|
|
</div>
|
|
|
|
</section>
|
|
</th:block>
|
|
|
|
<th:block layout:fragment="contentScript">
|
|
<script th:inline="javascript">
|
|
// IP address list management
|
|
let ipAddresses = [];
|
|
|
|
// Remove app icon
|
|
function removeAppIcon() {
|
|
const appIconInput = document.getElementById('appIcon');
|
|
const iconPreviewImage = document.getElementById('iconPreviewImage');
|
|
const iconPlaceholder = document.querySelector('.icon-placeholder');
|
|
const btnRemoveIcon = document.getElementById('btnRemoveIcon');
|
|
|
|
// Clear file input
|
|
appIconInput.value = '';
|
|
|
|
// Hide preview image and show placeholder
|
|
iconPreviewImage.style.display = 'none';
|
|
iconPreviewImage.src = '';
|
|
iconPlaceholder.style.display = 'flex';
|
|
|
|
// Hide delete button
|
|
btnRemoveIcon.style.display = 'none';
|
|
|
|
// Add required attribute back since no icon is selected
|
|
appIconInput.setAttribute('required', 'required');
|
|
}
|
|
|
|
// Add IP address to the list
|
|
function addIpAddress() {
|
|
const ipInput = document.getElementById('ipWhitelistInput');
|
|
const ipValue = ipInput.value.trim();
|
|
|
|
// Validate IP format
|
|
if (!ipValue) {
|
|
alert('IP 주소를 입력해주세요.');
|
|
return;
|
|
}
|
|
|
|
// Basic IP validation (supports IP and CIDR notation)
|
|
const ipPattern = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/;
|
|
if (!ipPattern.test(ipValue)) {
|
|
alert('올바른 IP 주소 형식이 아닙니다.\n예: 192.168.1.1 또는 10.0.0.0/24');
|
|
return;
|
|
}
|
|
|
|
// Check for duplicates
|
|
if (ipAddresses.includes(ipValue)) {
|
|
alert('이미 추가된 IP 주소입니다.');
|
|
return;
|
|
}
|
|
|
|
// Add to array
|
|
ipAddresses.push(ipValue);
|
|
|
|
// Update UI
|
|
updateIpList();
|
|
|
|
// Clear input
|
|
ipInput.value = '';
|
|
ipInput.focus();
|
|
}
|
|
|
|
// Remove IP address from the list
|
|
function removeIpAddress(ip) {
|
|
const index = ipAddresses.indexOf(ip);
|
|
if (index > -1) {
|
|
ipAddresses.splice(index, 1);
|
|
updateIpList();
|
|
}
|
|
}
|
|
|
|
// Update IP list display
|
|
function updateIpList() {
|
|
const ipList = document.getElementById('ipList');
|
|
const ipItems = document.getElementById('ipItems');
|
|
const ipCount = document.getElementById('ipCount');
|
|
const ipWhitelistHidden = document.getElementById('ipWhitelist');
|
|
|
|
// Update count
|
|
ipCount.textContent = ipAddresses.length;
|
|
|
|
// Show/hide list container
|
|
if (ipAddresses.length > 0) {
|
|
ipList.style.display = 'block';
|
|
|
|
// Build IP items HTML
|
|
let itemsHTML = '';
|
|
ipAddresses.forEach(function(ip) {
|
|
itemsHTML += `
|
|
<div class="ip-item">
|
|
<span class="ip-address">${ip}</span>
|
|
<button type="button" class="btn-remove-ip" onclick="removeIpAddress('${ip}')" title="제거">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
ipItems.innerHTML = itemsHTML;
|
|
} else {
|
|
ipList.style.display = 'none';
|
|
ipItems.innerHTML = '';
|
|
}
|
|
|
|
// Update hidden input with comma-separated values
|
|
ipWhitelistHidden.value = ipAddresses.join(',');
|
|
}
|
|
|
|
// Allow Enter key to add IP
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Only clear sessionStorage if starting a new registration (clear=true parameter)
|
|
// This preserves data during page refresh or back navigation from Step 2
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const shouldClear = urlParams.get('clear') === 'true';
|
|
|
|
if (shouldClear) {
|
|
const keysToRemove = [];
|
|
for (let i = 0; i < sessionStorage.length; i++) {
|
|
const key = sessionStorage.key(i);
|
|
if (key && key.startsWith('registration_')) {
|
|
keysToRemove.push(key);
|
|
}
|
|
}
|
|
keysToRemove.forEach(key => sessionStorage.removeItem(key));
|
|
}
|
|
|
|
const form = document.getElementById('registerStep1Form');
|
|
const appDescTextarea = document.getElementById('appDescription');
|
|
const charCounter = document.querySelector('.char-counter');
|
|
const appIconInput = document.getElementById('appIcon');
|
|
const iconPreviewImage = document.getElementById('iconPreviewImage');
|
|
const iconPlaceholder = document.querySelector('.icon-placeholder');
|
|
const ipWhitelistInput = document.getElementById('ipWhitelistInput');
|
|
|
|
// Restore IP whitelist from session (Thymeleaf provides this)
|
|
/*<![CDATA[*/
|
|
const sessionIpWhitelist = /*[[${apiKeyRegistration.ipWhitelistAsString}]]*/ '';
|
|
if (sessionIpWhitelist && sessionIpWhitelist.trim() !== '') {
|
|
const ips = sessionIpWhitelist.split(',');
|
|
ips.forEach(function(ip) {
|
|
const trimmedIp = ip.trim();
|
|
if (trimmedIp && !ipAddresses.includes(trimmedIp)) {
|
|
ipAddresses.push(trimmedIp);
|
|
}
|
|
});
|
|
updateIpList();
|
|
}
|
|
|
|
// Restore app icon preview from session if exists
|
|
const appIconFileName = /*[[${apiKeyRegistration.appIconFileName}]]*/ '';
|
|
const appIconBase64 = /*[[${apiKeyRegistration.appIconBase64}]]*/ '';
|
|
|
|
if (appIconFileName && appIconFileName.trim() !== '' && appIconBase64) {
|
|
// Display the actual uploaded image from session
|
|
iconPreviewImage.src = appIconBase64;
|
|
iconPreviewImage.style.display = 'block';
|
|
iconPlaceholder.style.display = 'none';
|
|
|
|
// Show delete button
|
|
const btnRemoveIcon = document.getElementById('btnRemoveIcon');
|
|
btnRemoveIcon.style.display = 'flex';
|
|
|
|
// Remove required attribute since we already have an image in session
|
|
appIconInput.removeAttribute('required');
|
|
}
|
|
/*]]>*/
|
|
|
|
// Initialize character counter for existing description
|
|
if (appDescTextarea && charCounter) {
|
|
const currentLength = appDescTextarea.value.length;
|
|
const maxLength = appDescTextarea.getAttribute('maxlength');
|
|
charCounter.textContent = currentLength + ' / ' + maxLength;
|
|
}
|
|
|
|
// Character counter for description
|
|
if (appDescTextarea && charCounter) {
|
|
appDescTextarea.addEventListener('input', function() {
|
|
const currentLength = this.value.length;
|
|
const maxLength = this.getAttribute('maxlength');
|
|
charCounter.textContent = currentLength + ' / ' + maxLength;
|
|
|
|
// Change color when near limit
|
|
if (currentLength > maxLength * 0.9) {
|
|
charCounter.style.color = '#FF6B6B';
|
|
} else {
|
|
charCounter.style.color = '#94A3B8';
|
|
}
|
|
});
|
|
}
|
|
|
|
// App icon preview
|
|
if (appIconInput) {
|
|
appIconInput.addEventListener('change', function(e) {
|
|
const file = e.target.files[0];
|
|
const btnRemoveIcon = document.getElementById('btnRemoveIcon');
|
|
|
|
if (file) {
|
|
// Validate file size (max 2MB)
|
|
if (file.size > 2 * 1024 * 1024) {
|
|
alert('파일 크기는 2MB를 초과할 수 없습니다.');
|
|
this.value = '';
|
|
return;
|
|
}
|
|
|
|
// Validate file type - Only PNG and JPG allowed
|
|
const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg'];
|
|
if (!allowedTypes.includes(file.type)) {
|
|
alert('PNG, JPG 형식만 업로드 가능합니다.');
|
|
this.value = '';
|
|
return;
|
|
}
|
|
|
|
// Show preview
|
|
const reader = new FileReader();
|
|
reader.onload = function(event) {
|
|
iconPreviewImage.src = event.target.result;
|
|
iconPreviewImage.style.display = 'block';
|
|
iconPlaceholder.style.display = 'none';
|
|
|
|
// Show delete button
|
|
btnRemoveIcon.style.display = 'flex';
|
|
};
|
|
reader.readAsDataURL(file);
|
|
|
|
// Remove required attribute since a file is now selected
|
|
appIconInput.removeAttribute('required');
|
|
}
|
|
});
|
|
}
|
|
|
|
// IP whitelist input - Enter key handler
|
|
if (ipWhitelistInput) {
|
|
ipWhitelistInput.addEventListener('keypress', function(e) {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
addIpAddress();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Cancel button handler - clear sessionStorage and notify server
|
|
const btnCancel = document.getElementById('btnCancel');
|
|
if (btnCancel) {
|
|
btnCancel.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
|
|
// Clear all registration-related data from sessionStorage
|
|
const keysToRemove = [];
|
|
for (let i = 0; i < sessionStorage.length; i++) {
|
|
const key = sessionStorage.key(i);
|
|
if (key && key.startsWith('registration_')) {
|
|
keysToRemove.push(key);
|
|
}
|
|
}
|
|
keysToRemove.forEach(key => sessionStorage.removeItem(key));
|
|
|
|
// Navigate to cancel endpoint to clear server session
|
|
window.location.href = '/myapikey/register/cancel';
|
|
});
|
|
}
|
|
|
|
// Form validation
|
|
form.addEventListener('submit', function(e) {
|
|
const appIcon = document.getElementById('appIcon').files[0];
|
|
const appName = document.getElementById('appName').value.trim();
|
|
const appDescription = document.getElementById('appDescription').value.trim();
|
|
const callbackUrl = document.getElementById('callbackUrl').value.trim();
|
|
|
|
// Check if app icon exists (either newly uploaded or from session)
|
|
const hasSessionIcon = appIconFileName && appIconFileName.trim() !== '';
|
|
if (!appIcon && !hasSessionIcon) {
|
|
e.preventDefault();
|
|
alert('앱 아이콘을 선택해주세요.');
|
|
return false;
|
|
}
|
|
|
|
if (!appName) {
|
|
e.preventDefault();
|
|
alert('앱 이름을 입력해주세요.');
|
|
document.getElementById('appName').focus();
|
|
return false;
|
|
}
|
|
|
|
if (!appDescription) {
|
|
e.preventDefault();
|
|
alert('앱 설명을 입력해주세요.');
|
|
document.getElementById('appDescription').focus();
|
|
return false;
|
|
}
|
|
|
|
// Validate URL format only if callbackUrl is provided (optional field)
|
|
if (callbackUrl && callbackUrl.trim() !== '') {
|
|
try {
|
|
new URL(callbackUrl);
|
|
} catch (error) {
|
|
e.preventDefault();
|
|
alert('올바른 URL 형식이 아닙니다.\n예: https://example.com/api/callback');
|
|
document.getElementById('callbackUrl').focus();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Store data in sessionStorage for later steps
|
|
sessionStorage.setItem('registration_appName', appName);
|
|
sessionStorage.setItem('registration_appDescription', appDescription);
|
|
sessionStorage.setItem('registration_callbackUrl', callbackUrl);
|
|
sessionStorage.setItem('registration_ipWhitelist', ipAddresses.join(','));
|
|
});
|
|
});
|
|
</script>
|
|
</th:block>
|
|
</body>
|
|
</html>
|