인증키 수정 화면 스타일 적용
This commit is contained in:
@@ -1,89 +1,120 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
|
||||||
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
|
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
|
||||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
|
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_title_layout}">
|
||||||
|
|
||||||
<body>
|
<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">
|
<th:block layout:fragment="contentFragment">
|
||||||
<section class="apikey-register-container">
|
<section class="apikey-register-container">
|
||||||
|
|
||||||
|
<!-- Title Bar -->
|
||||||
|
<div class="register-title-bar">
|
||||||
|
<h2 class="register-title">앱 수정</h2>
|
||||||
|
<span class="register-subtitle">서비스 중인 앱 정보를 수정합니다.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Progress Indicator -->
|
<!-- Progress Indicator -->
|
||||||
<div class="register-progress">
|
<div class="register-progress">
|
||||||
<div class="progress-steps">
|
<div class="progress-steps">
|
||||||
<div class="progress-step active">
|
<!-- Step 1: 앱 정보수정 (Active) -->
|
||||||
<div class="step-number">1</div>
|
<div class="progress-step-item active">
|
||||||
<div class="step-label">기본 정보</div>
|
<div class="step-icon-wrapper">
|
||||||
|
<div class="step-icon">
|
||||||
|
<img th:src="@{/img/apikey_step1.png}" alt="앱 정보수정" width="36" height="36">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="step-label">앱 정보수정</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-line"></div>
|
|
||||||
<div class="progress-step">
|
<!-- Dots -->
|
||||||
<div class="step-number">2</div>
|
<div class="step-dots">
|
||||||
|
<span></span><span></span><span></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 2: API 선택 -->
|
||||||
|
<div class="progress-step-item">
|
||||||
|
<div class="step-icon-wrapper">
|
||||||
|
<div class="step-icon">
|
||||||
|
<img th:src="@{/img/apikey_step2.png}" alt="API 선택" width="36" height="36">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="step-label">API 선택</div>
|
<div class="step-label">API 선택</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-line"></div>
|
|
||||||
<div class="progress-step">
|
<!-- Dots -->
|
||||||
<div class="step-number">3</div>
|
<div class="step-dots">
|
||||||
|
<span></span><span></span><span></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 3: 완료 -->
|
||||||
|
<div class="progress-step-item">
|
||||||
|
<div class="step-icon-wrapper">
|
||||||
|
<div class="step-icon">
|
||||||
|
<img th:src="@{/img/apikey_step3.png}" alt="완료" width="36" height="36">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="step-label">완료</div>
|
<div class="step-label">완료</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Header -->
|
<!-- Error Message -->
|
||||||
<div class="register-header">
|
<div th:if="${error}" class="alert alert-error">
|
||||||
<h1>API Key 수정</h1>
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<h2>Step 1: 기본 정보 수정</h2>
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
<p class="header-description">
|
<line x1="12" y1="8" x2="12" y2="12"></line>
|
||||||
앱의 기본 정보를 수정해주세요. 변경 사항은 승인 후 적용됩니다.
|
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
||||||
</p>
|
</svg>
|
||||||
|
<span th:text="${error}"></span>
|
||||||
<!-- 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>
|
</div>
|
||||||
|
|
||||||
<!-- Form Content -->
|
<!-- Form Content -->
|
||||||
<div class="register-content">
|
<div class="register-form-container">
|
||||||
<form id="modifyStep1Form" method="post" th:action="@{/myapikey/modify/step1}" th:object="${apiKeyModification}" class="register-form" enctype="multipart/form-data">
|
<form id="modifyStep1Form" method="post" th:action="@{/myapikey/modify/step1}" th:object="${apiKeyModification}" class="register-form" enctype="multipart/form-data">
|
||||||
|
|
||||||
<!-- Hidden field for clientId -->
|
<!-- Hidden field for clientId -->
|
||||||
<input type="hidden" th:field="*{clientId}"/>
|
<input type="hidden" th:field="*{clientId}"/>
|
||||||
|
|
||||||
<div class="form-card">
|
<!-- App Icon -->
|
||||||
|
<div class="form-row">
|
||||||
<!-- App Icon -->
|
<div class="form-label-wrapper">
|
||||||
<div class="form-group">
|
<span class="form-label-text">앱 아이콘</span>
|
||||||
<label for="appIcon" class="form-label required">
|
<span class="required-badge">필수</span>
|
||||||
앱 아이콘
|
</div>
|
||||||
<span class="required-mark">*</span>
|
<div class="form-field-wrapper">
|
||||||
</label>
|
<div class="icon-upload-box">
|
||||||
<div class="icon-upload-wrapper">
|
<div class="icon-preview-area" id="iconPreview">
|
||||||
<div class="icon-preview" id="iconPreview">
|
<div class="icon-placeholder" id="iconPlaceholder"
|
||||||
<div class="icon-placeholder"
|
th:style="${(apiKeyModification.appIconData != null or apiKeyModification.appIconFileId != null) ? 'display: none;' : 'display: flex;'}">
|
||||||
th:if="${apiKeyModification.appIconData == null and apiKeyModification.appIconFileId == null}">
|
|
||||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<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>
|
<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>
|
<polyline points="17 8 12 3 7 8"></polyline>
|
||||||
<line x1="12" y1="3" x2="12" y2="15"></line>
|
<line x1="12" y1="3" x2="12" y2="15"></line>
|
||||||
</svg>
|
</svg>
|
||||||
<span>이미지 선택</span>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- Show image from file service if available -->
|
<!-- Show image from file service if available -->
|
||||||
<img id="iconPreviewImage"
|
<img id="iconPreviewImage"
|
||||||
th:if="${apiKeyModification.appIconFileId != null}"
|
th:if="${apiKeyModification.appIconFileId != null}"
|
||||||
th:src="@{/file/download(fileSn=1,fileId=${apiKeyModification.appIconFileId})}"
|
th:src="@{/file/download(fileSn=1,fileId=${apiKeyModification.appIconFileId})}"
|
||||||
alt="Icon Preview">
|
alt="Icon Preview"
|
||||||
<!-- Show image from data if available (newly uploaded) -->
|
style="display: block;">
|
||||||
|
<!-- Show image from data if available -->
|
||||||
<img id="iconPreviewImage"
|
<img id="iconPreviewImage"
|
||||||
th:if="${apiKeyModification.appIconData != null and apiKeyModification.appIconFileId == null}"
|
th:if="${apiKeyModification.appIconData != null and apiKeyModification.appIconFileId == null}"
|
||||||
th:src="'data:' + ${apiKeyModification.appIconContentType} + ';base64,' + ${T(java.util.Base64).getEncoder().encodeToString(apiKeyModification.appIconData)}"
|
th:src="'data:' + ${apiKeyModification.appIconContentType} + ';base64,' + ${T(java.util.Base64).getEncoder().encodeToString(apiKeyModification.appIconData)}"
|
||||||
alt="Icon Preview">
|
alt="Icon Preview"
|
||||||
|
style="display: block;">
|
||||||
|
<!-- JS Empty Image Target -->
|
||||||
|
<img id="iconPreviewImageJS" style="display: none;" alt="Icon Preview">
|
||||||
|
|
||||||
<button type="button" class="btn-remove-icon" id="btnRemoveIcon"
|
<button type="button" class="btn-remove-icon" id="btnRemoveIcon"
|
||||||
th:style="${(apiKeyModification.appIconData != null or apiKeyModification.appIconFileId != null) ? 'display: block;' : 'display: none;'}"
|
th:style="${(apiKeyModification.appIconData != null or apiKeyModification.appIconFileId != null) ? 'display: flex;' : 'display: none;'}"
|
||||||
onclick="removeAppIcon()" title="아이콘 삭제">
|
onclick="removeAppIcon()" title="아이콘 삭제">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<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="18" y1="6" x2="6" y2="18"></line>
|
||||||
@@ -91,82 +122,97 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="icon-upload-info">
|
||||||
|
<p class="upload-title">앱 아이콘 이미지 파일을 업로드하세요</p>
|
||||||
|
<p class="upload-hint">권장사이즈는 <strong>128X128픽셀</strong>이며 <strong>JPG, PNG, GIF</strong>파일만 등록할 수 있습니다.</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-upload" onclick="document.getElementById('appIcon').click()">
|
||||||
|
<svg width="24" height="24" 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>
|
||||||
|
이미지 파일 업로드
|
||||||
|
</button>
|
||||||
<input type="file"
|
<input type="file"
|
||||||
id="appIcon"
|
id="appIcon"
|
||||||
name="appIcon"
|
name="appIcon"
|
||||||
class="icon-input"
|
class="icon-input"
|
||||||
accept="image/png,image/jpeg,image/jpg">
|
accept="image/png,image/jpeg,image/jpg,image/gif"
|
||||||
<button type="button" class="btn-icon-select" onclick="document.getElementById('appIcon').click()">
|
style="display: none;">
|
||||||
파일 선택
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<small class="form-hint">PNG, JPG 형식 (최대 2MB, 권장 크기: 512x512px)</small>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- App Name -->
|
<!-- App Name -->
|
||||||
<div class="form-group">
|
<div class="form-row">
|
||||||
<label for="appName" class="form-label required">
|
<div class="form-label-wrapper label-offset">
|
||||||
앱 이름
|
<span class="form-label-text">앱 이름</span>
|
||||||
<span class="required-mark">*</span>
|
<span class="required-badge">필수</span>
|
||||||
</label>
|
</div>
|
||||||
|
<div class="form-field-wrapper">
|
||||||
<input type="text"
|
<input type="text"
|
||||||
id="appName"
|
id="appName"
|
||||||
th:field="*{appName}"
|
th:field="*{appName}"
|
||||||
class="form-control"
|
class="form-input"
|
||||||
placeholder="예: My Application"
|
placeholder="앱 이름을 입력하세요."
|
||||||
maxlength="50"
|
required
|
||||||
required>
|
maxlength="100">
|
||||||
<small class="form-hint">앱을 구별할 수 있는 고유한 이름을 입력해주세요. (최대 50자)</small>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- App Description -->
|
<!-- App Description -->
|
||||||
<div class="form-group">
|
<div class="form-row">
|
||||||
<label for="appDescription" class="form-label">앱 설명</label>
|
<div class="form-label-wrapper">
|
||||||
|
<span class="form-label-text">앱 설명</span>
|
||||||
|
<span class="required-badge">필수</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-field-wrapper">
|
||||||
<textarea id="appDescription"
|
<textarea id="appDescription"
|
||||||
th:field="*{appDescription}"
|
th:field="*{appDescription}"
|
||||||
class="form-control"
|
class="form-textarea"
|
||||||
rows="4"
|
placeholder="앱 설명을 입력하세요."
|
||||||
placeholder="앱의 주요 기능과 용도를 간단히 설명해주세요."
|
rows="5"
|
||||||
|
required
|
||||||
maxlength="500"></textarea>
|
maxlength="500"></textarea>
|
||||||
<small class="form-hint">앱의 용도와 주요 기능을 설명해주세요. (최대 500자)</small>
|
<small class="char-counter-wrapper">
|
||||||
|
<span class="char-counter">0 / 500</span>
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Callback URL -->
|
<!-- Callback URL -->
|
||||||
<div class="form-group">
|
<div class="form-row">
|
||||||
<label for="callbackUrl" class="form-label">Callback URL</label>
|
<div class="form-label-wrapper label-offset">
|
||||||
<input type="url"
|
<span class="form-label-text">Call Back URL</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-field-wrapper">
|
||||||
|
<input type="text"
|
||||||
id="callbackUrl"
|
id="callbackUrl"
|
||||||
th:field="*{callbackUrl}"
|
th:field="*{callbackUrl}"
|
||||||
class="form-control"
|
class="form-input"
|
||||||
placeholder="https://example.com/callback">
|
placeholder="URL을 입력해 주세요.">
|
||||||
<small class="form-hint">OAuth 2.0 인증 시 사용할 콜백 URL을 입력해주세요.</small>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- IP Whitelist -->
|
<!-- IP Whitelist -->
|
||||||
<div class="form-group">
|
<div class="form-row">
|
||||||
<label for="ipWhitelist" class="form-label">
|
<div class="form-label-wrapper label-offset">
|
||||||
IP 화이트리스트
|
<span class="form-label-text">화이트리스트</span>
|
||||||
</label>
|
</div>
|
||||||
<div class="ip-input-group">
|
<div class="form-field-wrapper">
|
||||||
|
<div class="ip-input-row">
|
||||||
<input type="text"
|
<input type="text"
|
||||||
id="ipWhitelistInput"
|
id="ipWhitelistInput"
|
||||||
class="form-input"
|
class="form-input ip-input"
|
||||||
placeholder="예: 192.168.1.1 또는 10.0.0.0/24">
|
placeholder="서버 IP (111.111.111.111)">
|
||||||
<button type="button" class="btn-add-ip" onclick="addIpAddress()">
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<small class="form-hint">허용할 IP 주소 또는 CIDR 대역을 입력하세요. (선택사항)</small>
|
|
||||||
|
|
||||||
<!-- IP List Display -->
|
<!-- IP List Display -->
|
||||||
<div class="ip-list" id="ipList" style="display: none;">
|
<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">
|
<div class="ip-items" id="ipItems">
|
||||||
<!-- Dynamically added IP items -->
|
<!-- Dynamically added IP items -->
|
||||||
</div>
|
</div>
|
||||||
@@ -175,47 +221,46 @@
|
|||||||
<!-- Hidden input to store IP list -->
|
<!-- Hidden input to store IP list -->
|
||||||
<input type="hidden" id="ipWhitelist" name="ipWhitelist" value="">
|
<input type="hidden" id="ipWhitelist" name="ipWhitelist" value="">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
|
||||||
<div class="form-actions">
|
|
||||||
<a th:href="@{/myapikey/modify/cancel(clientId=${apiKeyModification.clientId})}" class="btn btn-cancel">
|
|
||||||
취소
|
|
||||||
</a>
|
|
||||||
<button type="submit" class="btn btn-primary">
|
|
||||||
다음 단계
|
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<polyline points="9 18 15 12 9 6"></polyline>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Form Actions -->
|
||||||
|
<div class="form-actions">
|
||||||
|
<a th:href="@{/myapikey/modify/cancel(clientId=${apiKeyModification.clientId})}" class="btn btn-submit btn-secondary">
|
||||||
|
취소
|
||||||
|
</a>
|
||||||
|
<button type="submit" form="modifyStep1Form" class="btn-primary btn-submit">
|
||||||
|
다음
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
</th:block>
|
</th:block>
|
||||||
|
|
||||||
<th:block layout:fragment="contentScript">
|
<th:block layout:fragment="contentScript">
|
||||||
<script th:inline="javascript">
|
<script th:inline="javascript">
|
||||||
/*<![CDATA[*/
|
|
||||||
// IP address list management
|
// IP address list management
|
||||||
let ipAddresses = [];
|
let ipAddresses = [];
|
||||||
|
|
||||||
// Remove app icon
|
// Remove app icon
|
||||||
function removeAppIcon() {
|
function removeAppIcon() {
|
||||||
const appIconInput = document.getElementById('appIcon');
|
const appIconInput = document.getElementById('appIcon');
|
||||||
const iconPreviewImage = document.getElementById('iconPreviewImage');
|
const iconPreviewImages = document.querySelectorAll('img[id^="iconPreviewImage"]'); // Select all preview images
|
||||||
const iconPlaceholder = document.querySelector('.icon-placeholder');
|
const iconPlaceholder = document.getElementById('iconPlaceholder');
|
||||||
const btnRemoveIcon = document.getElementById('btnRemoveIcon');
|
const btnRemoveIcon = document.getElementById('btnRemoveIcon');
|
||||||
|
|
||||||
// Clear file input
|
// Clear file input
|
||||||
appIconInput.value = '';
|
appIconInput.value = '';
|
||||||
|
|
||||||
// Hide preview image and show placeholder
|
// Hide all preview images
|
||||||
iconPreviewImage.style.display = 'none';
|
iconPreviewImages.forEach(img => {
|
||||||
iconPreviewImage.src = '';
|
img.style.display = 'none';
|
||||||
|
if (img.id === 'iconPreviewImageJS') img.src = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show placeholder
|
||||||
iconPlaceholder.style.display = 'flex';
|
iconPlaceholder.style.display = 'flex';
|
||||||
|
|
||||||
// Hide delete button
|
// Hide delete button
|
||||||
@@ -229,20 +274,20 @@
|
|||||||
|
|
||||||
// Validate IP format
|
// Validate IP format
|
||||||
if (!ipValue) {
|
if (!ipValue) {
|
||||||
alert('IP 주소를 입력해주세요.');
|
customPopups.showAlert('IP 주소를 입력해주세요.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Basic IP validation (supports IP and CIDR notation)
|
// Basic IP validation (supports IP and CIDR notation)
|
||||||
const ipPattern = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/;
|
const ipPattern = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/;
|
||||||
if (!ipPattern.test(ipValue)) {
|
if (!ipPattern.test(ipValue)) {
|
||||||
alert('올바른 IP 주소 형식이 아닙니다.\n예: 192.168.1.1 또는 10.0.0.0/24');
|
customPopups.showAlert('올바른 IP 주소 형식이 아닙니다.\n예: 192.168.1.1 또는 10.0.0.0/24');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for duplicates
|
// Check for duplicates
|
||||||
if (ipAddresses.includes(ipValue)) {
|
if (ipAddresses.includes(ipValue)) {
|
||||||
alert('이미 추가된 IP 주소입니다.');
|
customPopups.showAlert('이미 추가된 IP 주소입니다.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,12 +315,8 @@
|
|||||||
function updateIpList() {
|
function updateIpList() {
|
||||||
const ipList = document.getElementById('ipList');
|
const ipList = document.getElementById('ipList');
|
||||||
const ipItems = document.getElementById('ipItems');
|
const ipItems = document.getElementById('ipItems');
|
||||||
const ipCount = document.getElementById('ipCount');
|
|
||||||
const ipWhitelistHidden = document.getElementById('ipWhitelist');
|
const ipWhitelistHidden = document.getElementById('ipWhitelist');
|
||||||
|
|
||||||
// Update count
|
|
||||||
ipCount.textContent = ipAddresses.length;
|
|
||||||
|
|
||||||
// Show/hide list container
|
// Show/hide list container
|
||||||
if (ipAddresses.length > 0) {
|
if (ipAddresses.length > 0) {
|
||||||
ipList.style.display = 'block';
|
ipList.style.display = 'block';
|
||||||
@@ -285,12 +326,9 @@
|
|||||||
ipAddresses.forEach(function(ip) {
|
ipAddresses.forEach(function(ip) {
|
||||||
itemsHTML += `
|
itemsHTML += `
|
||||||
<div class="ip-item">
|
<div class="ip-item">
|
||||||
<span class="ip-address">${ip}</span>
|
<div class="ip-display">${ip}</div>
|
||||||
<button type="button" class="btn-remove-ip" onclick="removeIpAddress('${ip}')" title="제거">
|
<button type="button" class="btn-remove-ip" onclick="removeIpAddress('${ip}')">
|
||||||
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -306,19 +344,17 @@
|
|||||||
ipWhitelistHidden.value = ipAddresses.join(',');
|
ipWhitelistHidden.value = ipAddresses.join(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Initialize with existing icon data
|
const form = document.getElementById('modifyStep1Form');
|
||||||
var hasExistingIcon = /*[[${apiKeyModification.appIconData != null}]]*/ false;
|
const appDescTextarea = document.getElementById('appDescription');
|
||||||
var hasExistingIconFile = /*[[${apiKeyModification.appIconFileId != null}]]*/ false;
|
const charCounter = document.querySelector('.char-counter');
|
||||||
|
const appIconInput = document.getElementById('appIcon');
|
||||||
// If we have existing icon data, it's already shown by Thymeleaf
|
const iconPreviewImageJS = document.getElementById('iconPreviewImageJS');
|
||||||
// But we need to ensure the UI state is correct
|
const iconPlaceholder = document.getElementById('iconPlaceholder');
|
||||||
if (hasExistingIcon || hasExistingIconFile) {
|
const ipWhitelistInput = document.getElementById('ipWhitelistInput');
|
||||||
$('.icon-placeholder').hide();
|
|
||||||
$('#btnRemoveIcon').show();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore existing IP whitelist
|
// Restore existing IP whitelist
|
||||||
|
/*<![CDATA[*/
|
||||||
const sessionIpWhitelist = /*[[${apiKeyModification.ipWhitelistAsString}]]*/ '';
|
const sessionIpWhitelist = /*[[${apiKeyModification.ipWhitelistAsString}]]*/ '';
|
||||||
if (sessionIpWhitelist && sessionIpWhitelist.trim() !== '') {
|
if (sessionIpWhitelist && sessionIpWhitelist.trim() !== '') {
|
||||||
const ips = sessionIpWhitelist.split(',');
|
const ips = sessionIpWhitelist.split(',');
|
||||||
@@ -330,46 +366,110 @@
|
|||||||
});
|
});
|
||||||
updateIpList();
|
updateIpList();
|
||||||
}
|
}
|
||||||
|
/*]]>*/
|
||||||
|
|
||||||
// File upload preview
|
// Initialize character counter
|
||||||
$('#appIcon').on('change', function(e) {
|
if (appDescTextarea && charCounter) {
|
||||||
var file = e.target.files[0];
|
const currentLength = appDescTextarea.value.length;
|
||||||
const btnRemoveIcon = document.getElementById('btnRemoveIcon');
|
const maxLength = appDescTextarea.getAttribute('maxlength');
|
||||||
const iconPreviewImage = document.getElementById('iconPreviewImage');
|
charCounter.textContent = currentLength + ' / ' + maxLength;
|
||||||
const iconPlaceholder = document.querySelector('.icon-placeholder');
|
|
||||||
|
|
||||||
if (file) {
|
appDescTextarea.addEventListener('input', function() {
|
||||||
if (file.size > 2 * 1024 * 1024) {
|
const currentLength = this.value.length;
|
||||||
alert('파일 크기는 2MB를 초과할 수 없습니다.');
|
const maxLength = this.getAttribute('maxlength');
|
||||||
$(this).val('');
|
charCounter.textContent = currentLength + ' / ' + maxLength;
|
||||||
return;
|
|
||||||
|
if (currentLength > maxLength * 0.9) {
|
||||||
|
charCounter.style.color = '#FF6B6B';
|
||||||
|
} else {
|
||||||
|
charCounter.style.color = '#94A3B8';
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
var reader = new FileReader();
|
// App icon preview
|
||||||
reader.onload = function(e) {
|
if (appIconInput) {
|
||||||
iconPreviewImage.src = e.target.result;
|
appIconInput.addEventListener('change', function(e) {
|
||||||
iconPreviewImage.style.display = 'block';
|
const file = e.target.files[0];
|
||||||
iconPlaceholder.style.display = 'none';
|
const btnRemoveIcon = document.getElementById('btnRemoveIcon');
|
||||||
btnRemoveIcon.style.display = 'flex';
|
|
||||||
};
|
if (file) {
|
||||||
reader.readAsDataURL(file);
|
if (file.size > 2 * 1024 * 1024) {
|
||||||
|
customPopups.showAlert('파일 크기는 2MB를 초과할 수 없습니다.');
|
||||||
|
this.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif'];
|
||||||
|
if (!allowedTypes.includes(file.type)) {
|
||||||
|
customPopups.showAlert('PNG, JPG, GIF 형식만 업로드 가능합니다.');
|
||||||
|
this.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = function(event) {
|
||||||
|
// Hide other previews
|
||||||
|
document.querySelectorAll('img[id^="iconPreviewImage"]').forEach(img => img.style.display = 'none');
|
||||||
|
|
||||||
|
iconPreviewImageJS.src = event.target.result;
|
||||||
|
iconPreviewImageJS.style.display = 'block';
|
||||||
|
iconPlaceholder.style.display = 'none';
|
||||||
|
btnRemoveIcon.style.display = 'flex';
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// IP input Enter key
|
||||||
|
if (ipWhitelistInput) {
|
||||||
|
ipWhitelistInput.addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
addIpAddress();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form validation
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
const appName = document.getElementById('appName').value.trim();
|
||||||
|
const appDescription = document.getElementById('appDescription').value.trim();
|
||||||
|
const callbackUrl = document.getElementById('callbackUrl').value.trim();
|
||||||
|
|
||||||
|
if (document.getElementById('iconPlaceholder').style.display !== 'none') {
|
||||||
|
e.preventDefault();
|
||||||
|
customPopups.showAlert('앱 아이콘을 등록해주세요.');
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// IP whitelist input - Enter key handler
|
if (!appName) {
|
||||||
$('#ipWhitelistInput').on('keypress', function(e) {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
addIpAddress();
|
customPopups.showAlert('앱 이름을 입력해주세요.');
|
||||||
|
document.getElementById('appName').focus();
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Form submission - ensure IP list is updated
|
if (!appDescription) {
|
||||||
$('#modifyStep1Form').on('submit', function(e) {
|
e.preventDefault();
|
||||||
// IP whitelist is already updated via updateIpList()
|
customPopups.showAlert('앱 설명을 입력해주세요.');
|
||||||
|
document.getElementById('appDescription').focus();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callbackUrl && callbackUrl.trim() !== '') {
|
||||||
|
try {
|
||||||
|
new URL(callbackUrl);
|
||||||
|
} catch (error) {
|
||||||
|
e.preventDefault();
|
||||||
|
customPopups.showAlert('올바른 URL 형식이 아닙니다.');
|
||||||
|
document.getElementById('callbackUrl').focus();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
/*]]>*/
|
|
||||||
</script>
|
</script>
|
||||||
</th:block>
|
</th:block>
|
||||||
|
|
||||||
|
|||||||
@@ -1,41 +1,71 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
|
||||||
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
|
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
|
||||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
|
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_title_layout}">
|
||||||
|
|
||||||
<body>
|
<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">
|
<th:block layout:fragment="contentFragment">
|
||||||
|
<section class="apikey-register-container">
|
||||||
|
|
||||||
<!-- Progress Indicator -->
|
<!-- Title Bar -->
|
||||||
<div class="register-progress">
|
<div class="register-title-bar">
|
||||||
<div class="progress-steps">
|
<h2 class="register-title">앱 수정</h2>
|
||||||
<div class="progress-step completed">
|
<span class="register-subtitle">API를 이용하여 서비스할 앱 정보를 수정해주세요.</span>
|
||||||
<div class="step-number">✓</div>
|
</div>
|
||||||
<div class="step-label">기본 정보</div>
|
|
||||||
</div>
|
<!-- Progress Indicator -->
|
||||||
<div class="progress-line filled"></div>
|
<div class="register-progress">
|
||||||
<div class="progress-step active">
|
<div class="progress-steps">
|
||||||
<div class="step-number">2</div>
|
<!-- Step 1: 앱 정보입력 (Completed) -->
|
||||||
<div class="step-label">API 선택</div>
|
<div class="progress-step-item">
|
||||||
</div>
|
<div class="step-icon-wrapper">
|
||||||
<div class="progress-line"></div>
|
<div class="step-icon">
|
||||||
<div class="progress-step">
|
<img th:src="@{/img/apikey_step1.png}" alt="앱 정보입력" width="36" height="36">
|
||||||
<div class="step-number">3</div>
|
</div>
|
||||||
<div class="step-label">완료</div>
|
</div>
|
||||||
|
<div class="step-label">앱 정보입력</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dots between Step 1 and 2 -->
|
||||||
|
<div class="step-dots">
|
||||||
|
<span></span><span></span><span></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 2: API 선택 (Active) -->
|
||||||
|
<div class="progress-step-item active">
|
||||||
|
<div class="step-icon-wrapper">
|
||||||
|
<div class="step-icon">
|
||||||
|
<img th:src="@{/img/apikey_step2.png}" alt="API 선택" width="36" height="36">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="step-label">API 선택</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dots between Step 2 and 3 -->
|
||||||
|
<div class="step-dots">
|
||||||
|
<span></span><span></span><span></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 3: 앱 수정 완료 -->
|
||||||
|
<div class="progress-step-item">
|
||||||
|
<div class="step-icon-wrapper">
|
||||||
|
<div class="step-icon">
|
||||||
|
<img th:src="@{/img/apikey_step3.png}" alt="앱 수정 완료" width="36" height="36">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="step-label">앱 수정 완료</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="register-header">
|
|
||||||
<h1>API Key 수정</h1>
|
|
||||||
<h2>Step 2: API 선택</h2>
|
|
||||||
<p class="header-description">
|
|
||||||
사용할 API를 선택해주세요. 여러 개의 API를 선택할 수 있습니다.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Error Message -->
|
<!-- Error Message -->
|
||||||
<div th:if="${error}" class="alert alert-error" style="margin-top: 20px;">
|
<div th:if="${error}" class="alert alert-error">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<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>
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
<line x1="12" y1="8" x2="12" y2="12"></line>
|
<line x1="12" y1="8" x2="12" y2="12"></line>
|
||||||
@@ -43,120 +73,116 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<span th:text="${error}"></span>
|
<span th:text="${error}"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main Container with Sidebar Layout -->
|
<!-- Form Content with Sidebar Layout -->
|
||||||
<section class="apikey-register-container with-sidebar">
|
<div class="register-form-container with-sidebar">
|
||||||
|
|
||||||
<!-- Sidebar Navigation -->
|
<!-- Sidebar Navigation -->
|
||||||
<aside class="apikey-register-sidebar" id="apiSidebar">
|
<aside class="apikey-register-sidebar" id="apiSidebar">
|
||||||
<nav class="apikey-sidebar-nav">
|
<nav class="apikey-sidebar-nav">
|
||||||
<!-- All APIs -->
|
<!-- Sidebar Title -->
|
||||||
<div class="menu-section">
|
<div class="sidebar-title">API 서비스 목록</div>
|
||||||
<div class="menu-title active" data-group="">
|
|
||||||
전체
|
|
||||||
<span class="api-count" id="totalApiCount">0</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Service Categories -->
|
<!-- All APIs -->
|
||||||
<div class="menu-section" th:each="service : ${apiServices}">
|
<div class="menu-section">
|
||||||
<div class="menu-title"
|
<div class="menu-title active" data-group="">
|
||||||
th:data-group="${service.id}">
|
<span class="menu-text">전체</span>
|
||||||
[[${service.groupName}]]
|
<span class="api-count"></span>
|
||||||
<span class="api-count" th:text="${service.apiGroupApiList.size()}">0</span>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<!-- Mobile Menu Toggle -->
|
|
||||||
<button class="apikey-mobile-toggle" id="mobileToggle" aria-label="메뉴 열기">
|
|
||||||
☰
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Mobile Overlay -->
|
|
||||||
<div class="apikey-mobile-overlay" id="mobileOverlay"></div>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<main class="apikey-register-content">
|
|
||||||
<form id="modifyStep2Form" method="post" th:action="@{/myapikey/modify/step2}" th:object="${apiKeyModification}" class="register-form">
|
|
||||||
|
|
||||||
<!-- Hidden field for clientId -->
|
|
||||||
<input type="hidden" th:field="*{clientId}"/>
|
|
||||||
|
|
||||||
<!-- Search Box with Select All -->
|
|
||||||
<div class="api-filter-header">
|
|
||||||
<div class="select-all-wrapper" id="selectAllWrapper" style="display: none;">
|
|
||||||
<label class="select-all-label">
|
|
||||||
<input type="checkbox" id="selectAllCheckbox" class="select-all-checkbox">
|
|
||||||
<span id="selectAllText">전체 선택</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="api-search-box">
|
|
||||||
<input type="text"
|
|
||||||
id="apiSearch"
|
|
||||||
class="search-input"
|
|
||||||
placeholder="API 검색...">
|
|
||||||
<span class="search-icon">🔍</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Result Count -->
|
|
||||||
<div class="api-result-count">
|
|
||||||
총 <strong id="visibleCount">0</strong>건
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- API Cards Grid -->
|
|
||||||
<div class="api-selection-card-grid" id="apiCardGrid">
|
|
||||||
<!-- Loading state -->
|
|
||||||
<div class="loading-state" id="loadingState" style="grid-column: 1 / -1;">
|
|
||||||
<div class="loading-spinner"></div>
|
|
||||||
<p>API 목록을 불러오는 중...</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Empty state (initially hidden) -->
|
<!-- Service Categories -->
|
||||||
<div class="empty-api-state" id="emptyState" style="display: none; grid-column: 1 / -1;">
|
<div class="menu-section" th:each="service : ${apiServices}">
|
||||||
<div class="empty-icon">📦</div>
|
<div class="menu-title"
|
||||||
<h3>API를 선택해주세요</h3>
|
th:data-group="${service.id}">
|
||||||
<p>왼쪽 메뉴에서 서비스를 선택하면 해당 API 목록이 표시됩니다.</p>
|
<span class="menu-text" th:text="${service.groupName}">서비스명</span>
|
||||||
|
<span class="api-count"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Mobile Menu Toggle -->
|
||||||
|
<button class="apikey-mobile-toggle" id="mobileToggle" aria-label="메뉴 열기">
|
||||||
|
☰
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Mobile Overlay -->
|
||||||
|
<div class="apikey-mobile-overlay" id="mobileOverlay"></div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="apikey-register-content">
|
||||||
|
<form id="modifyStep2Form" method="post" th:action="@{/myapikey/modify/step2}" th:object="${apiKeyModification}" class="register-form">
|
||||||
|
|
||||||
|
<!-- Hidden field for clientId -->
|
||||||
|
<input type="hidden" th:field="*{clientId}"/>
|
||||||
|
|
||||||
|
<!-- Search Box with Select All -->
|
||||||
|
<div class="api-filter-header">
|
||||||
|
<div class="select-all-wrapper" id="selectAllWrapper" style="display: none;">
|
||||||
|
<label class="select-all-label">
|
||||||
|
<input type="checkbox" id="selectAllCheckbox" class="select-all-checkbox visually-hidden">
|
||||||
|
<span class="custom-checkbox"></span>
|
||||||
|
<span id="selectAllText">전체 선택</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="api-search-box">
|
||||||
|
<input type="text"
|
||||||
|
id="apiSearch"
|
||||||
|
class="search-input"
|
||||||
|
placeholder="API 검색...">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M9.23047 0C14.3285 0 18.4618 4.0539 18.4619 9.05469C18.4619 11.1083 17.7634 13.0014 16.5889 14.5205C16.5913 14.5229 16.5942 14.5249 16.5967 14.5273L19.5498 17.4238C20.1502 18.0131 20.1501 18.9683 19.5498 19.5576C18.949 20.147 17.9748 20.147 17.374 19.5576L14.4209 16.6621C14.3966 16.6383 14.3749 16.6119 14.3525 16.5869C12.8868 17.5478 11.1257 18.1094 9.23047 18.1094C4.13258 18.1092 0 14.0554 0 9.05469C0.000110268 4.05404 4.13265 0.000222701 9.23047 0ZM9.23047 3.01855C5.83201 3.01878 3.07726 5.721 3.07715 9.05469C3.07715 12.3885 5.83194 15.0916 9.23047 15.0918C12.6292 15.0918 15.3848 12.3886 15.3848 9.05469C15.3847 5.72086 12.6291 3.01855 9.23047 3.01855Z" fill="#515961"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- API cards will be dynamically loaded here -->
|
<!-- API Cards Grid -->
|
||||||
|
<div class="api-selection-card-grid" id="apiCardGrid">
|
||||||
|
<!-- Loading state -->
|
||||||
|
<div class="loading-state" id="loadingState" style="grid-column: 1 / -1;">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<p>API 목록을 불러오는 중...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
<!-- Empty state (initially hidden) -->
|
||||||
|
<div class="empty-api-state" id="emptyState" style="display: none; grid-column: 1 / -1;">
|
||||||
|
<div class="empty-icon">📦</div>
|
||||||
|
<h3>API를 선택해주세요</h3>
|
||||||
|
<p>왼쪽 메뉴에서 서비스를 선택하면 해당 API 목록이 표시됩니다.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Form Actions -->
|
<!-- API cards will be dynamically loaded here -->
|
||||||
<div class="form-actions">
|
|
||||||
<!-- Save and go back button -->
|
|
||||||
<button type="submit" formaction="/myapikey/modify/step2/save" class="btn btn-secondary" id="btnPrevStep">
|
|
||||||
<span class="icon">←</span>
|
|
||||||
이전 단계
|
|
||||||
</button>
|
|
||||||
<button type="submit" class="btn btn-primary" id="btnNext">
|
|
||||||
저장
|
|
||||||
<span class="icon">→</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</form>
|
</div>
|
||||||
</main>
|
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form Actions -->
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="button" id="btnPrevStep" class="btn-submit btn-secondary">
|
||||||
|
이전
|
||||||
|
</button>
|
||||||
|
<button type="submit" form="modifyStep2Form" class="btn-submit btn-primary">
|
||||||
|
저장
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
<!-- Floating Cart Button (Figma Design) -->
|
||||||
<!-- Floating Cart Button -->
|
|
||||||
<button type="button" class="floating-cart-btn" id="floatingCartBtn" style="display: none;">
|
<button type="button" class="floating-cart-btn" id="floatingCartBtn" style="display: none;">
|
||||||
<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">
|
<span class="cart-label">선택된 API</span>
|
||||||
<circle cx="9" cy="21" r="1"></circle>
|
<span class="cart-badge" id="cartBadge">
|
||||||
<circle cx="20" cy="21" r="1"></circle>
|
<span class="cart-count">0</span>
|
||||||
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
|
<span class="cart-unit">개</span>
|
||||||
</svg>
|
</span>
|
||||||
<span class="cart-text">선택된 API:</span>
|
|
||||||
<span class="cart-badge" id="cartBadge">0</span>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Selected APIs Modal -->
|
<!-- Selected APIs Modal - Uses base modal component -->
|
||||||
<div class="selected-apis-modal" id="selectedApisModal" style="display: none;">
|
<div class="modal" id="selectedApisModal" style="display: none;">
|
||||||
<div class="modal-backdrop" id="modalOverlay"></div>
|
<div class="modal-backdrop" id="modalOverlay"></div>
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@@ -190,11 +216,9 @@
|
|||||||
const mobileToggle = document.getElementById('mobileToggle');
|
const mobileToggle = document.getElementById('mobileToggle');
|
||||||
const mobileOverlay = document.getElementById('mobileOverlay');
|
const mobileOverlay = document.getElementById('mobileOverlay');
|
||||||
const menuTitles = document.querySelectorAll('.menu-title');
|
const menuTitles = document.querySelectorAll('.menu-title');
|
||||||
const visibleCount = document.getElementById('visibleCount');
|
|
||||||
const apiCardGrid = document.getElementById('apiCardGrid');
|
const apiCardGrid = document.getElementById('apiCardGrid');
|
||||||
const loadingState = document.getElementById('loadingState');
|
const loadingState = document.getElementById('loadingState');
|
||||||
const emptyState = document.getElementById('emptyState');
|
const emptyState = document.getElementById('emptyState');
|
||||||
const totalApiCount = document.getElementById('totalApiCount');
|
|
||||||
|
|
||||||
let currentFilter = ''; // Empty string means "all"
|
let currentFilter = ''; // Empty string means "all"
|
||||||
let currentServiceName = '전체'; // Store current service name
|
let currentServiceName = '전체'; // Store current service name
|
||||||
@@ -238,80 +262,24 @@
|
|||||||
url += '?groupIds=' + encodeURIComponent(groupId);
|
url += '?groupIds=' + encodeURIComponent(groupId);
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch(url)
|
fetch(url).then(response => response.json()).then(apis => {
|
||||||
.then(response => response.json())
|
allApis = apis;
|
||||||
.then(apis => {
|
loadingState.style.display = 'none';
|
||||||
allApis = apis;
|
|
||||||
loadingState.style.display = 'none';
|
|
||||||
|
|
||||||
if (apis.length === 0) {
|
if (apis.length === 0) {
|
||||||
emptyState.style.display = 'block';
|
|
||||||
visibleCount.textContent = '0';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderApiCards(apis);
|
|
||||||
visibleCount.textContent = apis.length;
|
|
||||||
|
|
||||||
// Update total count
|
|
||||||
if (!groupId || groupId === '') {
|
|
||||||
totalApiCount.textContent = apis.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update select all UI
|
|
||||||
updateSelectAllUI();
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Failed to load APIs:', error);
|
|
||||||
loadingState.style.display = 'none';
|
|
||||||
emptyState.querySelector('h3').textContent = 'API 로드 실패';
|
|
||||||
emptyState.querySelector('p').textContent = '다시 시도해주세요.';
|
|
||||||
emptyState.style.display = 'block';
|
emptyState.style.display = 'block';
|
||||||
});
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update select all UI visibility and label
|
renderApiCards(apis);
|
||||||
function updateSelectAllUI() {
|
updateSelectAllUI();
|
||||||
const selectAllWrapper = document.getElementById('selectAllWrapper');
|
}).catch(error => {
|
||||||
const selectAllText = document.getElementById('selectAllText');
|
console.error('Failed to load APIs:', error);
|
||||||
|
loadingState.style.display = 'none';
|
||||||
if (currentFilter === '') {
|
emptyState.querySelector('h3').textContent = 'API 로드 실패';
|
||||||
// Hide select all when "전체" is selected
|
emptyState.querySelector('p').textContent = '다시 시도해주세요.';
|
||||||
selectAllWrapper.style.display = 'none';
|
emptyState.style.display = 'block';
|
||||||
} else {
|
});
|
||||||
// Show select all with service name
|
|
||||||
selectAllWrapper.style.display = 'flex';
|
|
||||||
selectAllText.textContent = currentServiceName + ' API 전체 선택';
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSelectAllCheckboxState();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update select all checkbox state (checked/indeterminate/unchecked)
|
|
||||||
function updateSelectAllCheckboxState() {
|
|
||||||
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
|
||||||
const visibleCards = Array.from(document.querySelectorAll('.api-selection-card'))
|
|
||||||
.filter(card => card.style.display !== 'none');
|
|
||||||
|
|
||||||
if (visibleCards.length === 0) {
|
|
||||||
selectAllCheckbox.checked = false;
|
|
||||||
selectAllCheckbox.indeterminate = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const visibleCheckboxes = visibleCards.map(card => card.querySelector('.api-checkbox'));
|
|
||||||
const checkedCount = visibleCheckboxes.filter(cb => cb.checked).length;
|
|
||||||
|
|
||||||
if (checkedCount === 0) {
|
|
||||||
selectAllCheckbox.checked = false;
|
|
||||||
selectAllCheckbox.indeterminate = false;
|
|
||||||
} else if (checkedCount === visibleCheckboxes.length) {
|
|
||||||
selectAllCheckbox.checked = true;
|
|
||||||
selectAllCheckbox.indeterminate = false;
|
|
||||||
} else {
|
|
||||||
selectAllCheckbox.checked = false;
|
|
||||||
selectAllCheckbox.indeterminate = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render API cards
|
// Render API cards
|
||||||
@@ -336,8 +304,7 @@
|
|||||||
function createApiCard(api) {
|
function createApiCard(api) {
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'api-selection-card';
|
card.className = 'api-selection-card';
|
||||||
// Note: AJAX response doesn't have apiGroupId, use apiGroupName or service for grouping
|
card.setAttribute('data-group', api.apiGroupId || '');
|
||||||
card.setAttribute('data-group', api.apiGroupName || '');
|
|
||||||
card.setAttribute('data-name', (api.apiName || '').toLowerCase());
|
card.setAttribute('data-name', (api.apiName || '').toLowerCase());
|
||||||
card.setAttribute('data-desc', (api.apiSimpleDescription || '').toLowerCase());
|
card.setAttribute('data-desc', (api.apiSimpleDescription || '').toLowerCase());
|
||||||
card.setAttribute('data-api-id', api.apiId);
|
card.setAttribute('data-api-id', api.apiId);
|
||||||
@@ -347,29 +314,32 @@
|
|||||||
card.classList.add('selected');
|
card.classList.add('selected');
|
||||||
}
|
}
|
||||||
|
|
||||||
card.innerHTML = `
|
const mainIconHtml = api.mainIcon
|
||||||
<div class="checkbox-wrapper">
|
? `<img src="${api.mainIcon}" alt="${api.apiName}" onerror="this.style.display='none'; this.nextElementSibling.style.display='block'"><i class="fas fa-cube" style="display:none"></i>`
|
||||||
<input type="checkbox"
|
: `<i class="fas fa-cube"></i>`;
|
||||||
name="selectedApis"
|
|
||||||
value="${api.apiId}"
|
|
||||||
id="api-${api.apiId}"
|
|
||||||
class="api-checkbox"
|
|
||||||
${isSelected ? 'checked' : ''}>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label class="api-card-content" for="api-${api.apiId}">
|
card.innerHTML = `
|
||||||
|
<div class="api-card-content">
|
||||||
<div class="api-card-header">
|
<div class="api-card-header">
|
||||||
<span class="api-card-badge">${api.service || '카테고리'}</span>
|
<span class="api-card-category">${api.service || '카테고리'}</span>
|
||||||
<span class="api-method">${api.apiMethod || 'GET'}</span>
|
<div class="checkbox-wrapper">
|
||||||
|
<input type="checkbox"
|
||||||
|
name="selectedApis"
|
||||||
|
value="${api.apiId}"
|
||||||
|
id="api-${api.apiId}"
|
||||||
|
class="api-checkbox visually-hidden"
|
||||||
|
${isSelected ? 'checked' : ''}>
|
||||||
|
<span class="custom-checkbox"></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="api-name">${api.apiName || 'API 이름'}</h3>
|
<h3 class="api-name">${api.apiName || 'API 이름'}</h3>
|
||||||
<p class="api-description">${api.apiSimpleDescription || 'API 설명이 없습니다.'}</p>
|
<p class="api-description">${api.apiSimpleDescription || 'API 설명이 없습니다.'}</p>
|
||||||
|
|
||||||
<div class="api-endpoint">
|
<div class="api-card-icon">
|
||||||
<code>${api.apiUrl || '/api/v1/example'}</code>
|
${mainIconHtml}
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return card;
|
return card;
|
||||||
@@ -384,13 +354,13 @@
|
|||||||
checkbox.addEventListener('change', function() {
|
checkbox.addEventListener('change', function() {
|
||||||
updateCardSelection(this);
|
updateCardSelection(this);
|
||||||
updateSelectedCount();
|
updateSelectedCount();
|
||||||
updateSelectAllCheckboxState();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
apiCards.forEach(card => {
|
apiCards.forEach(card => {
|
||||||
card.addEventListener('click', function(e) {
|
card.addEventListener('click', function(e) {
|
||||||
if (e.target.classList.contains('api-checkbox') || e.target.tagName === 'LABEL') {
|
// Prevent double toggle if clicking directly on checkbox
|
||||||
|
if (e.target.classList.contains('api-checkbox')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const checkbox = card.querySelector('.api-checkbox');
|
const checkbox = card.querySelector('.api-checkbox');
|
||||||
@@ -398,7 +368,6 @@
|
|||||||
checkbox.checked = !checkbox.checked;
|
checkbox.checked = !checkbox.checked;
|
||||||
updateCardSelection(checkbox);
|
updateCardSelection(checkbox);
|
||||||
updateSelectedCount();
|
updateSelectedCount();
|
||||||
updateSelectAllCheckboxState();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -420,17 +389,63 @@
|
|||||||
function updateSelectedCount() {
|
function updateSelectedCount() {
|
||||||
// Update floating cart button
|
// Update floating cart button
|
||||||
const floatingCartBtn = document.getElementById('floatingCartBtn');
|
const floatingCartBtn = document.getElementById('floatingCartBtn');
|
||||||
const cartBadge = document.getElementById('cartBadge');
|
const cartCount = document.querySelector('.cart-count');
|
||||||
|
|
||||||
if (selectedApis.size > 0) {
|
if (selectedApis.size > 0) {
|
||||||
floatingCartBtn.style.display = 'flex';
|
floatingCartBtn.style.display = 'flex';
|
||||||
cartBadge.textContent = selectedApis.size;
|
cartCount.textContent = selectedApis.size;
|
||||||
} else {
|
} else {
|
||||||
floatingCartBtn.style.display = 'none';
|
floatingCartBtn.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update modal list
|
// Update modal list
|
||||||
updateModalList();
|
updateModalList();
|
||||||
|
|
||||||
|
// Update select all checkbox state
|
||||||
|
updateSelectAllCheckboxState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update select all UI visibility and text
|
||||||
|
function updateSelectAllUI() {
|
||||||
|
const selectAllWrapper = document.getElementById('selectAllWrapper');
|
||||||
|
const selectAllText = document.getElementById('selectAllText');
|
||||||
|
|
||||||
|
if (currentFilter === '') {
|
||||||
|
// Hide select all when "전체" is selected
|
||||||
|
selectAllWrapper.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
// Show select all with service name
|
||||||
|
selectAllWrapper.style.display = 'flex';
|
||||||
|
selectAllText.textContent = currentServiceName + ' API 전체 선택';
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSelectAllCheckboxState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update select all checkbox state based on visible cards
|
||||||
|
function updateSelectAllCheckboxState() {
|
||||||
|
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||||
|
const visibleCards = Array.from(document.querySelectorAll('.api-selection-card')).filter(card => card.style.display !== 'none');
|
||||||
|
|
||||||
|
if (visibleCards.length === 0) {
|
||||||
|
selectAllCheckbox.checked = false;
|
||||||
|
selectAllCheckbox.indeterminate = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleCheckboxes = visibleCards.map(card => card.querySelector('.api-checkbox'));
|
||||||
|
const checkedCount = visibleCheckboxes.filter(cb => cb.checked).length;
|
||||||
|
|
||||||
|
if (checkedCount === 0) {
|
||||||
|
selectAllCheckbox.checked = false;
|
||||||
|
selectAllCheckbox.indeterminate = false;
|
||||||
|
} else if (checkedCount === visibleCheckboxes.length) {
|
||||||
|
selectAllCheckbox.checked = true;
|
||||||
|
selectAllCheckbox.indeterminate = false;
|
||||||
|
} else {
|
||||||
|
selectAllCheckbox.checked = false;
|
||||||
|
selectAllCheckbox.indeterminate = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update modal selected APIs list
|
// Update modal selected APIs list
|
||||||
@@ -444,8 +459,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build list using selectedApis Set for consistency
|
// Build list using selectedApis Set for consistency
|
||||||
// This ensures we show all selected APIs regardless of current filter/view
|
|
||||||
console.log('updateModalList: Building modal for', selectedApis.size, 'selected APIs');
|
|
||||||
selectedApis.forEach(function(apiId) {
|
selectedApis.forEach(function(apiId) {
|
||||||
// Try to find the checkbox in the DOM first
|
// Try to find the checkbox in the DOM first
|
||||||
const checkbox = document.querySelector('.api-checkbox[value="' + apiId + '"]');
|
const checkbox = document.querySelector('.api-checkbox[value="' + apiId + '"]');
|
||||||
@@ -483,12 +496,11 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
console.log('Adding pill for:', apiId, '→', apiName);
|
|
||||||
modalSelectedList.appendChild(apiPill);
|
modalSelectedList.appendChild(apiPill);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add remove handlers to newly created elements
|
// Add remove handlers
|
||||||
modalSelectedList.querySelectorAll('.api-pill-remove').forEach(function(btn) {
|
document.querySelectorAll('.api-pill-remove').forEach(function(btn) {
|
||||||
btn.addEventListener('click', function() {
|
btn.addEventListener('click', function() {
|
||||||
const value = this.getAttribute('data-value');
|
const value = this.getAttribute('data-value');
|
||||||
const checkbox = document.querySelector('.api-checkbox[value="' + value + '"]');
|
const checkbox = document.querySelector('.api-checkbox[value="' + value + '"]');
|
||||||
@@ -510,7 +522,6 @@
|
|||||||
if (searchInput) {
|
if (searchInput) {
|
||||||
searchInput.addEventListener('input', function() {
|
searchInput.addEventListener('input', function() {
|
||||||
const searchTerm = this.value.toLowerCase();
|
const searchTerm = this.value.toLowerCase();
|
||||||
let visibleCountNum = 0;
|
|
||||||
|
|
||||||
const apiCards = document.querySelectorAll('.api-selection-card');
|
const apiCards = document.querySelectorAll('.api-selection-card');
|
||||||
apiCards.forEach(function(card) {
|
apiCards.forEach(function(card) {
|
||||||
@@ -522,13 +533,12 @@
|
|||||||
|
|
||||||
if (matchesSearch) {
|
if (matchesSearch) {
|
||||||
card.style.display = '';
|
card.style.display = '';
|
||||||
visibleCountNum++;
|
|
||||||
} else {
|
} else {
|
||||||
card.style.display = 'none';
|
card.style.display = 'none';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
visibleCount.textContent = visibleCountNum;
|
// Update select all checkbox state after search
|
||||||
updateSelectAllCheckboxState();
|
updateSelectAllCheckboxState();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -546,7 +556,7 @@
|
|||||||
const groupId = this.getAttribute('data-group');
|
const groupId = this.getAttribute('data-group');
|
||||||
currentFilter = groupId;
|
currentFilter = groupId;
|
||||||
|
|
||||||
// Store current service name (extract text before the count)
|
// Store service name for select all label
|
||||||
currentServiceName = this.textContent.trim().split('\n')[0].trim();
|
currentServiceName = this.textContent.trim().split('\n')[0].trim();
|
||||||
|
|
||||||
// Load APIs for selected service
|
// Load APIs for selected service
|
||||||
@@ -665,13 +675,12 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Select all checkbox event handler
|
// Select All checkbox event
|
||||||
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||||
if (selectAllCheckbox) {
|
if (selectAllCheckbox) {
|
||||||
selectAllCheckbox.addEventListener('change', function() {
|
selectAllCheckbox.addEventListener('change', function() {
|
||||||
const isChecked = this.checked;
|
const isChecked = this.checked;
|
||||||
const visibleCards = Array.from(document.querySelectorAll('.api-selection-card'))
|
const visibleCards = Array.from(document.querySelectorAll('.api-selection-card')).filter(card => card.style.display !== 'none');
|
||||||
.filter(card => card.style.display !== 'none');
|
|
||||||
|
|
||||||
visibleCards.forEach(function(card) {
|
visibleCards.forEach(function(card) {
|
||||||
const checkbox = card.querySelector('.api-checkbox');
|
const checkbox = card.querySelector('.api-checkbox');
|
||||||
@@ -685,6 +694,21 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Previous step button - save current selections before going back
|
||||||
|
const btnPrevStep = document.getElementById('btnPrevStep');
|
||||||
|
if (btnPrevStep) {
|
||||||
|
btnPrevStep.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Change form action to save endpoint
|
||||||
|
const originalAction = form.action;
|
||||||
|
form.action = '/myapikey/modify/step2/save';
|
||||||
|
|
||||||
|
// Submit the form to save selections
|
||||||
|
form.submit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Form validation
|
// Form validation
|
||||||
form.addEventListener('submit', function(e) {
|
form.addEventListener('submit', function(e) {
|
||||||
// Allow submission even without selected APIs (user might want to remove all)
|
// Allow submission even without selected APIs (user might want to remove all)
|
||||||
@@ -701,11 +725,10 @@
|
|||||||
// Load all APIs for modal display FIRST (async)
|
// Load all APIs for modal display FIRST (async)
|
||||||
loadAllApisForModal();
|
loadAllApisForModal();
|
||||||
|
|
||||||
// Load all APIs automatically on page load
|
updateSelectedCount(); // This will show count from restored session data
|
||||||
loadApis('');
|
|
||||||
|
|
||||||
// Update count after APIs are loaded (this will show count from restored session data)
|
// Load all APIs automatically on page load (empty string = all APIs)
|
||||||
updateSelectedCount();
|
loadApis('');
|
||||||
|
|
||||||
// Log restored selections for debugging
|
// Log restored selections for debugging
|
||||||
if (selectedApis.size > 0) {
|
if (selectedApis.size > 0) {
|
||||||
|
|||||||
@@ -1,86 +1,116 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
|
||||||
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
|
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
|
||||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
|
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_title_layout}">
|
||||||
|
|
||||||
<body>
|
<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">
|
<th:block layout:fragment="contentFragment">
|
||||||
<section class="apikey-register-container">
|
<section class="apikey-register-container">
|
||||||
|
|
||||||
<!-- Progress Indicator -->
|
<!-- Title Bar -->
|
||||||
|
<div class="register-title-bar">
|
||||||
|
<h2 class="register-title">앱 수정</h2>
|
||||||
|
<span class="register-subtitle">API를 이용하여 서비스할 앱 정보를 수정해주세요.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="register-progress">
|
<div class="register-progress">
|
||||||
<div class="progress-steps">
|
<div class="progress-steps">
|
||||||
<div class="progress-step completed">
|
<!-- Step 1: 앱 정보입력 -->
|
||||||
<div class="step-number">✓</div>
|
<div class="progress-step-item">
|
||||||
<div class="step-label">기본 정보</div>
|
<div class="step-icon-wrapper">
|
||||||
|
<div class="step-icon">
|
||||||
|
<img th:src="@{/img/apikey_step1.png}" alt="앱 정보입력" width="36" height="36">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="step-label">앱 정보입력</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-line filled"></div>
|
|
||||||
<div class="progress-step completed">
|
<!-- Dots between Step 1 and 2 -->
|
||||||
<div class="step-number">✓</div>
|
<div class="step-dots">
|
||||||
|
<span></span><span></span><span></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 2: API 선택 -->
|
||||||
|
<div class="progress-step-item">
|
||||||
|
<div class="step-icon-wrapper">
|
||||||
|
<div class="step-icon">
|
||||||
|
<img th:src="@{/img/apikey_step2.png}" alt="API 선택" width="36" height="36">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="step-label">API 선택</div>
|
<div class="step-label">API 선택</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-line filled"></div>
|
|
||||||
<div class="progress-step active completed">
|
<!-- Dots between Step 2 and 3 -->
|
||||||
<div class="step-number">✓</div>
|
<div class="step-dots">
|
||||||
<div class="step-label">완료</div>
|
<span></span><span></span><span></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 3: 앱 수정 완료 (Active) -->
|
||||||
|
<div class="progress-step-item active">
|
||||||
|
<div class="step-icon-wrapper">
|
||||||
|
<div class="step-icon">
|
||||||
|
<img th:src="@{/img/apikey_step3.png}" alt="앱 수정 완료" width="36" height="36">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="step-label">앱 수정 완료</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Success Result -->
|
<!-- Success Result -->
|
||||||
<div class="register-result success" th:if="${modificationComplete}">
|
<div class="register-result success" th:if="${modificationComplete}">
|
||||||
|
|
||||||
<!-- Success Icon Animation -->
|
<!-- Success Icon Animation -->
|
||||||
<div class="result-icon-wrapper">
|
<div class="result-icon-wrapper">
|
||||||
<div class="result-icon success-icon">
|
<div class="result-icon success-icon">
|
||||||
<svg class="checkmark" viewBox="0 0 52 52">
|
<img th:src="@{/img/img_app_complete.png}">
|
||||||
<circle class="checkmark-circle" cx="26" cy="26" r="25" fill="none"/>
|
|
||||||
<path class="checkmark-check" fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Success Message -->
|
<!-- Success Message -->
|
||||||
<div class="result-header">
|
<div class="result-header">
|
||||||
<h1>API Key 수정 요청 완료!</h1>
|
<h1>앱 수정 요청이 완료되었습니다.</h1>
|
||||||
<p class="result-description">
|
<p class="result-description">
|
||||||
API Key 수정 요청이 성공적으로 접수되었습니다. 관리자 승인 후 변경 사항이 적용됩니다.
|
담당자 승인 후 변경 사항이 적용됩니다. [앱 정보]화면에서 수정 내역을 확인할 수 있습니다.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Result Content -->
|
<!-- API Key Display -->
|
||||||
<div class="result-content">
|
<div class="result-content">
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
<!-- Action Buttons -->
|
||||||
<div class="result-actions">
|
<div class="result-actions">
|
||||||
<a th:href="@{/myapikey}" class="btn btn-secondary">
|
<a href="/myapikey" class="btn btn-primary">
|
||||||
<span class="icon">📋</span>
|
인증키 관리
|
||||||
API Key 목록으로
|
|
||||||
</a>
|
|
||||||
<a th:href="@{/myapikey/api_key_request/history}" class="btn btn-primary">
|
|
||||||
<span class="icon">📄</span>
|
|
||||||
요청 내역 확인
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Error Result (if needed) -->
|
<!-- Error Result (if needed) -->
|
||||||
<div class="register-result error" th:unless="${modificationComplete}">
|
<div class="register-result error" th:unless="${modificationComplete}" style="display: none;">
|
||||||
<div class="result-icon-wrapper">
|
<div class="result-icon-wrapper">
|
||||||
<div class="result-icon error-icon">
|
<div class="result-icon error-icon">
|
||||||
<span>❌</span>
|
<span>❌</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="result-header">
|
<div class="result-header">
|
||||||
<h1>잘못된 접근입니다</h1>
|
<h1>앱 수정 실패</h1>
|
||||||
<p class="result-description">
|
<p class="result-description">
|
||||||
수정 프로세스를 완료하지 않았거나 잘못된 경로로 접근하셨습니다.
|
앱 수정 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="result-actions">
|
<div class="result-actions">
|
||||||
<a th:href="@{/myapikey}" class="btn btn-primary">
|
<a href="/myapikey" class="btn btn-secondary">
|
||||||
목록으로 돌아가기
|
다시 시도
|
||||||
|
</a>
|
||||||
|
<a href="/myapikey" class="btn btn-primary">
|
||||||
|
목록으로
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,64 +119,6 @@
|
|||||||
</th:block>
|
</th:block>
|
||||||
|
|
||||||
<th:block layout:fragment="contentScript">
|
<th:block layout:fragment="contentScript">
|
||||||
<script th:inline="javascript">
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
|
|
||||||
// Success animation
|
|
||||||
setTimeout(function() {
|
|
||||||
const successIcon = document.querySelector('.success-icon');
|
|
||||||
if (successIcon) {
|
|
||||||
successIcon.classList.add('animate');
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Success checkmark animation */
|
|
||||||
.checkmark {
|
|
||||||
width: 52px;
|
|
||||||
height: 52px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: block;
|
|
||||||
stroke-width: 2;
|
|
||||||
stroke: #6BCF7F;
|
|
||||||
stroke-miterlimit: 10;
|
|
||||||
animation: rotate 1s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkmark-circle {
|
|
||||||
stroke-dasharray: 166;
|
|
||||||
stroke-dashoffset: 166;
|
|
||||||
stroke-width: 2;
|
|
||||||
stroke-miterlimit: 10;
|
|
||||||
stroke: #6BCF7F;
|
|
||||||
animation: stroke 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkmark-check {
|
|
||||||
transform-origin: 50% 50%;
|
|
||||||
stroke-dasharray: 48;
|
|
||||||
stroke-dashoffset: 48;
|
|
||||||
animation: stroke 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.6s forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes stroke {
|
|
||||||
100% {
|
|
||||||
stroke-dashoffset: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes rotate {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</th:block>
|
</th:block>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -122,86 +122,6 @@
|
|||||||
<script th:inline="javascript">
|
<script th:inline="javascript">
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|
||||||
// Load data from sessionStorage
|
|
||||||
const appName = sessionStorage.getItem('registration_appName');
|
|
||||||
const selectedApis = JSON.parse(sessionStorage.getItem('registration_selectedApis') || '[]');
|
|
||||||
|
|
||||||
// Update display with actual data
|
|
||||||
if (appName) {
|
|
||||||
document.getElementById('resultAppName').textContent = appName;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy to clipboard functionality
|
|
||||||
document.querySelectorAll('.btn-copy').forEach(function(btn) {
|
|
||||||
btn.addEventListener('click', function() {
|
|
||||||
const targetId = this.getAttribute('data-copy');
|
|
||||||
const targetElement = document.getElementById(targetId);
|
|
||||||
const textToCopy = targetElement.textContent;
|
|
||||||
|
|
||||||
// Create temporary input
|
|
||||||
const tempInput = document.createElement('input');
|
|
||||||
tempInput.value = textToCopy;
|
|
||||||
document.body.appendChild(tempInput);
|
|
||||||
tempInput.select();
|
|
||||||
|
|
||||||
try {
|
|
||||||
document.execCommand('copy');
|
|
||||||
|
|
||||||
// Visual feedback
|
|
||||||
const originalText = this.innerHTML;
|
|
||||||
this.innerHTML = '<span class="copy-icon">✓</span> 복사됨';
|
|
||||||
this.classList.add('copied');
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.innerHTML = originalText;
|
|
||||||
this.classList.remove('copied');
|
|
||||||
}, 2000);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to copy:', err);
|
|
||||||
alert('복사에 실패했습니다. 수동으로 복사해주세요.');
|
|
||||||
}
|
|
||||||
|
|
||||||
document.body.removeChild(tempInput);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Toggle visibility for secret key
|
|
||||||
document.querySelectorAll('.btn-toggle-visibility').forEach(function(btn) {
|
|
||||||
btn.addEventListener('click', function() {
|
|
||||||
const targetId = this.getAttribute('data-target');
|
|
||||||
const targetElement = document.getElementById(targetId);
|
|
||||||
|
|
||||||
if (targetElement.classList.contains('blurred')) {
|
|
||||||
targetElement.classList.remove('blurred');
|
|
||||||
this.innerHTML = '<span class="visibility-icon">🙈</span>';
|
|
||||||
} else {
|
|
||||||
targetElement.classList.add('blurred');
|
|
||||||
this.innerHTML = '<span class="visibility-icon">👁️</span>';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Download key information
|
|
||||||
document.getElementById('downloadKeyBtn')?.addEventListener('click', function() {
|
|
||||||
const keyInfo = {
|
|
||||||
appName: document.getElementById('resultAppName').textContent,
|
|
||||||
clientId: document.getElementById('clientId').textContent,
|
|
||||||
clientSecret: document.getElementById('clientSecret').textContent,
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
selectedApis: selectedApis
|
|
||||||
};
|
|
||||||
|
|
||||||
const dataStr = JSON.stringify(keyInfo, null, 2);
|
|
||||||
const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr);
|
|
||||||
|
|
||||||
const exportFileDefaultName = 'apikey_' + keyInfo.appName.replace(/\s+/g, '_') + '.json';
|
|
||||||
|
|
||||||
const linkElement = document.createElement('a');
|
|
||||||
linkElement.setAttribute('href', dataUri);
|
|
||||||
linkElement.setAttribute('download', exportFileDefaultName);
|
|
||||||
linkElement.click();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear session storage after successful registration
|
// Clear session storage after successful registration
|
||||||
if (/*[[${registrationComplete}]]*/ false) {
|
if (/*[[${registrationComplete}]]*/ false) {
|
||||||
sessionStorage.removeItem('registration_appName');
|
sessionStorage.removeItem('registration_appName');
|
||||||
@@ -210,73 +130,8 @@
|
|||||||
sessionStorage.removeItem('registration_environment');
|
sessionStorage.removeItem('registration_environment');
|
||||||
sessionStorage.removeItem('registration_selectedApis');
|
sessionStorage.removeItem('registration_selectedApis');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success animation
|
|
||||||
setTimeout(function() {
|
|
||||||
const successIcon = document.querySelector('.success-icon');
|
|
||||||
if (successIcon) {
|
|
||||||
successIcon.classList.add('animate');
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Blur effect for secret key */
|
|
||||||
.key-value.blurred {
|
|
||||||
filter: blur(5px);
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Copy button feedback */
|
|
||||||
.btn-copy.copied {
|
|
||||||
background-color: #6BCF7F !important;
|
|
||||||
color: white !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Success checkmark animation */
|
|
||||||
.checkmark {
|
|
||||||
width: 52px;
|
|
||||||
height: 52px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: block;
|
|
||||||
stroke-width: 2;
|
|
||||||
stroke: #6BCF7F;
|
|
||||||
stroke-miterlimit: 10;
|
|
||||||
animation: rotate 1s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkmark-circle {
|
|
||||||
stroke-dasharray: 166;
|
|
||||||
stroke-dashoffset: 166;
|
|
||||||
stroke-width: 2;
|
|
||||||
stroke-miterlimit: 10;
|
|
||||||
stroke: #6BCF7F;
|
|
||||||
animation: stroke 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkmark-check {
|
|
||||||
transform-origin: 50% 50%;
|
|
||||||
stroke-dasharray: 48;
|
|
||||||
stroke-dashoffset: 48;
|
|
||||||
animation: stroke 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.6s forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes stroke {
|
|
||||||
100% {
|
|
||||||
stroke-dashoffset: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes rotate {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</th:block>
|
</th:block>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user