feat: 마이페이지 퍼블리싱
This commit is contained in:
@@ -11,460 +11,379 @@
|
||||
</div>
|
||||
</section>
|
||||
<th:block layout:fragment="contentFragment">
|
||||
<section class="apikey-register-container">
|
||||
<div class="signup-guide-v2 figma-register-wrapper">
|
||||
<div class="service-main app-management-layout">
|
||||
|
||||
<!-- Title Bar -->
|
||||
<div class="common-title-bar">
|
||||
<h2 class="common-title">앱 수정</h2>
|
||||
<span class="common-subtitle">서비스 중인 앱 정보를 수정합니다.</span>
|
||||
</div>
|
||||
<!-- Sidebar -->
|
||||
<th:block th:replace="~{fragment/djbank/service_sidebar :: sidebar('apiKey')}"></th:block>
|
||||
|
||||
<!-- Content Area -->
|
||||
<div class="app-management-content">
|
||||
<div class="step1-wrap">
|
||||
|
||||
<!-- Title -->
|
||||
<h2 class="s1-title">앱 수정</h2>
|
||||
|
||||
<!-- Progress Steps Card -->
|
||||
<div class="s1-progress-card">
|
||||
<div class="s1-steps">
|
||||
<!-- Step 1 Active: 앱 정보수정 (form/input icon) -->
|
||||
<div class="s1-step s1-step--active">
|
||||
<div class="s1-step-circle">
|
||||
<svg class="s1-step-svg" width="26" height="26" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="3" y="5" width="18" height="14" rx="2" stroke="currentColor" stroke-width="1.8" />
|
||||
<line x1="7" y1="9" x2="17" y2="9" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
|
||||
<line x1="7" y1="12" x2="14" y2="12" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
|
||||
<line x1="7" y1="15" x2="11" y2="15" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="s1-step-num">1단계</span>
|
||||
<span class="s1-step-name">앱 정보수정</span>
|
||||
</div>
|
||||
|
||||
<div class="s1-step-line"></div>
|
||||
|
||||
<!-- Step 2: API 선택 (chain link icon) -->
|
||||
<div class="s1-step">
|
||||
<div class="s1-step-circle">
|
||||
<svg class="s1-step-svg" width="26" height="26" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="s1-step-num">2단계</span>
|
||||
<span class="s1-step-name">API 선택</span>
|
||||
</div>
|
||||
|
||||
<div class="s1-step-line"></div>
|
||||
|
||||
<!-- Step 3: 완료 (rocket icon) -->
|
||||
<div class="s1-step">
|
||||
<div class="s1-step-circle">
|
||||
<svg class="s1-step-svg" width="26" height="26" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M12 15l-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="s1-step-num">3단계</span>
|
||||
<span class="s1-step-name">완료</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Indicator -->
|
||||
<div class="register-progress">
|
||||
<div class="progress-steps">
|
||||
<!-- Step 1: 앱 정보수정 (Active) -->
|
||||
<div class="progress-step-item active">
|
||||
<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>
|
||||
|
||||
<!-- Dots -->
|
||||
<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">
|
||||
<!-- Error -->
|
||||
<div th:if="${error}" class="s1-alert">
|
||||
<span th:text="${error}"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-label">API 선택</div>
|
||||
</div>
|
||||
|
||||
<!-- Dots -->
|
||||
<div class="step-dots">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<!-- Form Card -->
|
||||
<div class="s1-form-card">
|
||||
<form id="modifyStep1Form" method="post" th:action="@{/myapikey/modify/step1}"
|
||||
th:object="${apiKeyModification}" enctype="multipart/form-data">
|
||||
|
||||
<!-- 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">
|
||||
<!-- Hidden clientId -->
|
||||
<input type="hidden" th:field="*{clientId}"/>
|
||||
|
||||
<!-- 앱 아이콘 -->
|
||||
<div class="s1-field">
|
||||
<label class="s1-label">앱 아이콘</label>
|
||||
<div class="s1-upload-box" id="iconDropZone">
|
||||
|
||||
<!-- 기존 파일 ID로 이미지 노출 -->
|
||||
<img class="s1-preview-img" id="iconPreviewImage1"
|
||||
th:if="${apiKeyModification.appIconFileId != null}"
|
||||
th:src="@{/file/download(fileSn=1,fileId=${apiKeyModification.appIconFileId})}"
|
||||
alt="미리보기" style="display: block;">
|
||||
|
||||
<!-- 데이터에서 이미지 노출 -->
|
||||
<img class="s1-preview-img" id="iconPreviewImage2"
|
||||
th:if="${apiKeyModification.appIconData != null and apiKeyModification.appIconFileId == null}"
|
||||
th:src="'data:' + ${apiKeyModification.appIconContentType} + ';base64,' + ${T(java.util.Base64).getEncoder().encodeToString(apiKeyModification.appIconData)}"
|
||||
alt="미리보기" style="display: block;">
|
||||
|
||||
<!-- JS용 이미지 타겟 -->
|
||||
<img id="iconPreviewImageJS" class="s1-preview-img" style="display: none;" alt="미리보기">
|
||||
|
||||
<div id="iconPlaceholder" class="s1-upload-inner" th:style="${(apiKeyModification.appIconData != null or apiKeyModification.appIconFileId != null) ? 'display: none;' : 'display: flex;'}">
|
||||
<img th:src="@{/img/icon/img_icon.png}">
|
||||
<p class="s1-upload-title">앱 아이콘 이미지 파일을 업로드 하세요</p>
|
||||
<p class="s1-upload-hint">권장 사이즈는 1280 * 12 픽셀이며 JPG,PNG,GIF 파일만 등록할 수 있습니다.</p>
|
||||
<button type="button" class="s1-btn-upload"
|
||||
onclick="document.getElementById('appIconFile').click()">
|
||||
이미지 파일 업로드
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="s1-btn-remove-icon" id="btnRemoveIcon"
|
||||
th:style="${(apiKeyModification.appIconData != null or apiKeyModification.appIconFileId != null) ? 'display: flex;' : 'display: none;'}"
|
||||
onclick="removeAppIcon()">✕</button>
|
||||
<input type="file" id="appIconFile" name="appIcon" accept="image/png,image/jpeg,image/jpg,image/gif" style="display:none;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 앱 이름 -->
|
||||
<div class="s1-field">
|
||||
<label class="s1-label">앱 이름 <span class="s1-required">*</span></label>
|
||||
<input type="text" id="appName" name="appName" th:field="*{appName}" class="s1-input"
|
||||
placeholder="앱 이름을 입력하세요." required maxlength="100">
|
||||
</div>
|
||||
|
||||
<!-- 앱 설명 -->
|
||||
<div class="s1-field">
|
||||
<label class="s1-label">앱 설명 <span class="s1-required">*</span></label>
|
||||
<textarea id="appDescription" name="appDescription" th:field="*{appDescription}" class="s1-textarea"
|
||||
placeholder="앱 설명을 입력하세요." required maxlength="500"></textarea>
|
||||
<div class="s1-counter-row">
|
||||
<span id="charCounter" class="s1-counter">0 / 500</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Call Back URL -->
|
||||
<div class="s1-field">
|
||||
<label class="s1-label">Call Back URL</label>
|
||||
<input type="text" id="callbackUrl" name="callbackUrl" th:field="*{callbackUrl}" class="s1-input"
|
||||
placeholder="URL을 입력해 주세요.">
|
||||
</div>
|
||||
|
||||
<!-- 화이트 리스트 -->
|
||||
<div class="s1-field">
|
||||
<label class="s1-label">화이트 리스트 <span class="s1-required">*</span></label>
|
||||
|
||||
<!-- 입력행 -->
|
||||
<div class="s1-ip-row">
|
||||
<input type="text" id="ipWhitelistInput" class="s1-input" placeholder="서버 IP(111.111.111.111)">
|
||||
<button type="button" class="s1-ip-btn s1-ip-btn--add" onclick="addIpAddress()">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2.5">
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
추가
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 추가된 IP 목록 -->
|
||||
<div id="ipList" style="display:none;">
|
||||
<div id="ipItems"></div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" id="ipWhitelist" name="ipWhitelist" value="">
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-label">완료</div>
|
||||
|
||||
<!-- 다음 버튼 -->
|
||||
<div class="s1-actions" style="justify-content: flex-end; gap: 10px;">
|
||||
<a th:href="@{/myapikey/modify/cancel(clientId=${apiKeyModification.clientId})}" class="s1-btn-next" style="background: #bdc7cf; flex: none; width: 120px; text-decoration: none; display: flex; justify-content: center; align-items: center;">취소</a>
|
||||
<button type="submit" form="modifyStep1Form" class="s1-btn-next" style="flex: none; width: 120px;">다음</button>
|
||||
</div>
|
||||
|
||||
</div><!-- /step1-wrap -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</th:block>
|
||||
|
||||
<!-- Error Message -->
|
||||
<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">
|
||||
<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>
|
||||
<th:block layout:fragment="contentScript">
|
||||
<script th:inline="javascript">
|
||||
let ipAddresses = [];
|
||||
|
||||
<!-- Form 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">
|
||||
function removeAppIcon() {
|
||||
const fileInput = document.getElementById('appIconFile');
|
||||
const previewJS = document.getElementById('iconPreviewImageJS');
|
||||
const preview1 = document.getElementById('iconPreviewImage1');
|
||||
const preview2 = document.getElementById('iconPreviewImage2');
|
||||
const placeholder = document.getElementById('iconPlaceholder');
|
||||
const removeBtn = document.getElementById('btnRemoveIcon');
|
||||
|
||||
<!-- Hidden field for clientId -->
|
||||
<input type="hidden" th:field="*{clientId}"/>
|
||||
|
||||
<!-- App Icon -->
|
||||
<div class="form-row">
|
||||
<div class="form-label-wrapper">
|
||||
<span class="form-label-text">앱 아이콘</span>
|
||||
</div>
|
||||
<div class="form-field-wrapper">
|
||||
<div class="icon-upload-box">
|
||||
<div class="icon-preview-area" id="iconPreview">
|
||||
<div class="icon-placeholder" id="iconPlaceholder"
|
||||
th:style="${(apiKeyModification.appIconData != null or apiKeyModification.appIconFileId != null) ? 'display: none;' : 'display: flex;'}">
|
||||
<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>
|
||||
</div>
|
||||
<!-- Show image from file service if available -->
|
||||
<img id="iconPreviewImage"
|
||||
th:if="${apiKeyModification.appIconFileId != null}"
|
||||
th:src="@{/file/download(fileSn=1,fileId=${apiKeyModification.appIconFileId})}"
|
||||
alt="Icon Preview"
|
||||
style="display: block;">
|
||||
<!-- Show image from data if available -->
|
||||
<img id="iconPreviewImage"
|
||||
th:if="${apiKeyModification.appIconData != null and apiKeyModification.appIconFileId == null}"
|
||||
th:src="'data:' + ${apiKeyModification.appIconContentType} + ';base64,' + ${T(java.util.Base64).getEncoder().encodeToString(apiKeyModification.appIconData)}"
|
||||
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"
|
||||
th:style="${(apiKeyModification.appIconData != null or apiKeyModification.appIconFileId != null) ? 'display: flex;' : '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>
|
||||
<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"
|
||||
id="appIcon"
|
||||
name="appIcon"
|
||||
class="icon-input"
|
||||
accept="image/png,image/jpeg,image/jpg,image/gif"
|
||||
style="display: none;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- App Name -->
|
||||
<div class="form-row">
|
||||
<div class="form-label-wrapper label-offset">
|
||||
<span class="form-label-text">앱 이름</span>
|
||||
<span class="required-badge">필수</span>
|
||||
</div>
|
||||
<div class="form-field-wrapper">
|
||||
<input type="text"
|
||||
id="appName"
|
||||
th:field="*{appName}"
|
||||
class="form-input"
|
||||
placeholder="앱 이름을 입력하세요."
|
||||
required
|
||||
maxlength="100">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- App Description -->
|
||||
<div class="form-row">
|
||||
<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"
|
||||
th:field="*{appDescription}"
|
||||
class="form-textarea"
|
||||
placeholder="앱 설명을 입력하세요."
|
||||
rows="5"
|
||||
required
|
||||
maxlength="500"></textarea>
|
||||
<small class="char-counter-wrapper">
|
||||
<span class="char-counter">0 / 500</span>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Callback URL -->
|
||||
<div class="form-row">
|
||||
<div class="form-label-wrapper label-offset">
|
||||
<span class="form-label-text">Call Back URL</span>
|
||||
</div>
|
||||
<div class="form-field-wrapper">
|
||||
<input type="text"
|
||||
id="callbackUrl"
|
||||
th:field="*{callbackUrl}"
|
||||
class="form-input"
|
||||
placeholder="URL을 입력해 주세요.">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IP Whitelist -->
|
||||
<div class="form-row">
|
||||
<div class="form-label-wrapper label-offset">
|
||||
<span class="form-label-text">화이트리스트</span>
|
||||
</div>
|
||||
<div class="form-field-wrapper">
|
||||
<div class="ip-input-row">
|
||||
<input type="text"
|
||||
id="ipWhitelistInput"
|
||||
class="form-input ip-input"
|
||||
placeholder="서버 IP (111.111.111.111)">
|
||||
<button type="button" class="btn-add-ip" onclick="addIpAddress()">
|
||||
+ 추가
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- IP List Display -->
|
||||
<div class="ip-list" id="ipList" style="display: none;">
|
||||
<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>
|
||||
</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>
|
||||
</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 iconPreviewImages = document.querySelectorAll('img[id^="iconPreviewImage"]'); // Select all preview images
|
||||
const iconPlaceholder = document.getElementById('iconPlaceholder');
|
||||
const btnRemoveIcon = document.getElementById('btnRemoveIcon');
|
||||
|
||||
// Clear file input
|
||||
appIconInput.value = '';
|
||||
|
||||
// Hide all preview images
|
||||
iconPreviewImages.forEach(img => {
|
||||
img.style.display = 'none';
|
||||
if (img.id === 'iconPreviewImageJS') img.src = '';
|
||||
});
|
||||
|
||||
// Show placeholder
|
||||
iconPlaceholder.style.display = 'flex';
|
||||
|
||||
// Hide delete button
|
||||
btnRemoveIcon.style.display = 'none';
|
||||
}
|
||||
|
||||
// Add IP address to the list
|
||||
function addIpAddress() {
|
||||
const ipInput = document.getElementById('ipWhitelistInput');
|
||||
const ipValue = ipInput.value.trim();
|
||||
|
||||
// Validate IP format
|
||||
if (!ipValue) {
|
||||
customPopups.showAlert('IP 주소를 입력해주세요.');
|
||||
return;
|
||||
fileInput.value = '';
|
||||
if(preview1) preview1.style.display = 'none';
|
||||
if(preview2) preview2.style.display = 'none';
|
||||
previewJS.style.display = 'none';
|
||||
previewJS.src = '';
|
||||
placeholder.style.display = 'flex';
|
||||
removeBtn.style.display = 'none';
|
||||
}
|
||||
|
||||
// Basic IP validation (supports IP and CIDR notation)
|
||||
const ipPattern = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/;
|
||||
if (!ipPattern.test(ipValue)) {
|
||||
customPopups.showAlert('올바른 IP 주소 형식이 아닙니다.\n예: 192.168.1.1 또는 10.0.0.0/24');
|
||||
return;
|
||||
function addIpAddress() {
|
||||
const ipInput = document.getElementById('ipWhitelistInput');
|
||||
const ipValue = ipInput.value.trim();
|
||||
|
||||
if (!ipValue) {
|
||||
customPopups.showAlert('IP 주소를 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
const ipPattern = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/;
|
||||
if (!ipPattern.test(ipValue)) {
|
||||
customPopups.showAlert('올바른 IP 주소 형식이 아닙니다.\n예: 192.168.1.1 또는 10.0.0.0/24');
|
||||
return;
|
||||
}
|
||||
|
||||
if (ipAddresses.includes(ipValue)) {
|
||||
customPopups.showAlert('이미 추가된 IP 주소입니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
ipAddresses.push(ipValue);
|
||||
renderIpList();
|
||||
ipInput.value = '';
|
||||
ipInput.focus();
|
||||
}
|
||||
|
||||
// Check for duplicates
|
||||
if (ipAddresses.includes(ipValue)) {
|
||||
customPopups.showAlert('이미 추가된 IP 주소입니다.');
|
||||
return;
|
||||
function removeIpAddress(ip) {
|
||||
ipAddresses = ipAddresses.filter(function (i) { return i !== ip; });
|
||||
renderIpList();
|
||||
}
|
||||
|
||||
// Add to array
|
||||
ipAddresses.push(ipValue);
|
||||
function renderIpList() {
|
||||
const ipList = document.getElementById('ipList');
|
||||
const ipItems = document.getElementById('ipItems');
|
||||
const hiddenInput = document.getElementById('ipWhitelist');
|
||||
|
||||
// Update UI
|
||||
updateIpList();
|
||||
if (ipAddresses.length > 0) {
|
||||
ipList.style.display = 'block';
|
||||
ipItems.innerHTML = ipAddresses.map(function (ip) {
|
||||
return '<div class="s1-ip-item">' +
|
||||
'<div class="s1-ip-display">' + ip + '</div>' +
|
||||
'<button type="button" class="s1-ip-btn s1-ip-btn--remove" onclick="removeIpAddress(\'' + ip + '\')">' +
|
||||
'<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M19 11H5v2h14z"/></svg>' +
|
||||
'제거</button>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
} else {
|
||||
ipList.style.display = 'none';
|
||||
ipItems.innerHTML = '';
|
||||
}
|
||||
|
||||
// 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 ipWhitelistHidden = document.getElementById('ipWhitelist');
|
||||
|
||||
// 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">
|
||||
<div class="ip-display">${ip}</div>
|
||||
<button type="button" class="btn-remove-ip" onclick="removeIpAddress('${ip}')">
|
||||
- 제거
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
ipItems.innerHTML = itemsHTML;
|
||||
} else {
|
||||
ipList.style.display = 'none';
|
||||
ipItems.innerHTML = '';
|
||||
hiddenInput.value = ipAddresses.join(',');
|
||||
}
|
||||
|
||||
// Update hidden input with comma-separated values
|
||||
ipWhitelistHidden.value = ipAddresses.join(',');
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const form = document.getElementById('modifyStep1Form');
|
||||
const textarea = document.getElementById('appDescription');
|
||||
const counter = document.getElementById('charCounter');
|
||||
const fileInput = document.getElementById('appIconFile');
|
||||
const previewJS = document.getElementById('iconPreviewImageJS');
|
||||
const preview1 = document.getElementById('iconPreviewImage1');
|
||||
const preview2 = document.getElementById('iconPreviewImage2');
|
||||
const placeholder = document.getElementById('iconPlaceholder');
|
||||
const ipInput = document.getElementById('ipWhitelistInput');
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.getElementById('modifyStep1Form');
|
||||
const appDescTextarea = document.getElementById('appDescription');
|
||||
const charCounter = document.querySelector('.char-counter');
|
||||
const appIconInput = document.getElementById('appIcon');
|
||||
const iconPreviewImageJS = document.getElementById('iconPreviewImageJS');
|
||||
const iconPlaceholder = document.getElementById('iconPlaceholder');
|
||||
const ipWhitelistInput = document.getElementById('ipWhitelistInput');
|
||||
// Thymeleaf: Restore IP whitelist
|
||||
/*<![CDATA[*/
|
||||
const sessionIpWhitelist = /*[[${apiKeyModification.ipWhitelistAsString}]]*/ '';
|
||||
if (sessionIpWhitelist && sessionIpWhitelist.trim() !== '') {
|
||||
sessionIpWhitelist.split(',').forEach(function (ip) {
|
||||
const t = ip.trim();
|
||||
if (t && !ipAddresses.includes(t)) ipAddresses.push(t);
|
||||
});
|
||||
renderIpList();
|
||||
}
|
||||
/*]]>*/
|
||||
|
||||
// Restore existing IP whitelist
|
||||
/*<![CDATA[*/
|
||||
const sessionIpWhitelist = /*[[${apiKeyModification.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();
|
||||
}
|
||||
/*]]>*/
|
||||
// Char counter init
|
||||
if (textarea && counter) {
|
||||
const max = textarea.getAttribute('maxlength') || 500;
|
||||
counter.textContent = textarea.value.length + ' / ' + max;
|
||||
|
||||
// Initialize character counter
|
||||
if (appDescTextarea && charCounter) {
|
||||
const currentLength = appDescTextarea.value.length;
|
||||
const maxLength = appDescTextarea.getAttribute('maxlength');
|
||||
charCounter.textContent = currentLength + ' / ' + maxLength;
|
||||
textarea.addEventListener('input', function () {
|
||||
counter.textContent = this.value.length + ' / ' + max;
|
||||
counter.style.color = this.value.length > max * 0.9 ? '#f4253c' : '#94a3b8';
|
||||
});
|
||||
}
|
||||
|
||||
appDescTextarea.addEventListener('input', function() {
|
||||
const currentLength = this.value.length;
|
||||
const maxLength = this.getAttribute('maxlength');
|
||||
charCounter.textContent = currentLength + ' / ' + maxLength;
|
||||
|
||||
if (currentLength > maxLength * 0.9) {
|
||||
charCounter.style.color = '#FF6B6B';
|
||||
} else {
|
||||
charCounter.style.color = '#94A3B8';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// App icon preview
|
||||
if (appIconInput) {
|
||||
appIconInput.addEventListener('change', function(e) {
|
||||
// File input change
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
const file = e.target.files[0];
|
||||
const btnRemoveIcon = document.getElementById('btnRemoveIcon');
|
||||
if (!file) return;
|
||||
|
||||
if (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);
|
||||
if (file.size > 2 * 1024 * 1024) {
|
||||
customPopups.showAlert('파일 크기는 2MB를 초과할 수 없습니다.');
|
||||
this.value = '';
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// IP input Enter key
|
||||
if (ipWhitelistInput) {
|
||||
ipWhitelistInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
if (!['image/png', 'image/jpeg', 'image/jpg', 'image/gif'].includes(file.type)) {
|
||||
customPopups.showAlert('PNG, JPG, GIF 형식만 업로드 가능합니다.');
|
||||
this.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (ev) {
|
||||
if(preview1) preview1.style.display = 'none';
|
||||
if(preview2) preview2.style.display = 'none';
|
||||
previewJS.src = ev.target.result;
|
||||
previewJS.style.display = 'block';
|
||||
placeholder.style.display = 'none';
|
||||
document.getElementById('btnRemoveIcon').style.display = 'flex';
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
// Drag & drop
|
||||
const dropZone = document.getElementById('iconDropZone');
|
||||
if (dropZone) {
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
addIpAddress();
|
||||
this.classList.add('drag-over');
|
||||
});
|
||||
dropZone.addEventListener('dragleave', function () {
|
||||
this.classList.remove('drag-over');
|
||||
});
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
this.classList.remove('drag-over');
|
||||
if (e.dataTransfer.files.length > 0) {
|
||||
fileInput.files = e.dataTransfer.files;
|
||||
fileInput.dispatchEvent(new Event('change'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// IP input Enter key
|
||||
ipInput.addEventListener('keypress', function (e) {
|
||||
if (e.key === 'Enter') { e.preventDefault(); addIpAddress(); }
|
||||
});
|
||||
|
||||
// Form submit validation
|
||||
form.addEventListener('submit', function (e) {
|
||||
const name = document.getElementById('appName').value.trim();
|
||||
const desc = textarea.value.trim();
|
||||
const url = document.getElementById('callbackUrl').value.trim();
|
||||
|
||||
if (!name) {
|
||||
e.preventDefault();
|
||||
customPopups.showAlert('앱 이름을 입력해주세요.');
|
||||
document.getElementById('appName').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!desc) {
|
||||
e.preventDefault();
|
||||
customPopups.showAlert('앱 설명을 입력해주세요.');
|
||||
textarea.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (url) {
|
||||
try { new URL(url); } catch (_) {
|
||||
e.preventDefault();
|
||||
customPopups.showAlert('올바른 URL 형식이 아닙니다.\n예: https://example.com/callback');
|
||||
document.getElementById('callbackUrl').focus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 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 (!appName) {
|
||||
e.preventDefault();
|
||||
customPopups.showAlert('앱 이름을 입력해주세요.');
|
||||
document.getElementById('appName').focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!appDescription) {
|
||||
e.preventDefault();
|
||||
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>
|
||||
</th:block>
|
||||
|
||||
</script>
|
||||
</th:block>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user