- Webhook 신청 및 관리 UI 전면 개편 - 신규 가이드 반영
- Webhook 수정/삭제 기능 UX 개선 - 단계별 디자인 업데이트 - Webhook 등록/수정 CSS 및 JS 로직 최적화 및 재사용 코드 추가
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
|
||||
<!-- Content Area -->
|
||||
<div class="app-management-content">
|
||||
<!-- 모든 스타일은 step2-wrap 안에서만 적용 -->
|
||||
<div class="step2-wrap">
|
||||
|
||||
<!-- Title -->
|
||||
@@ -27,7 +28,7 @@
|
||||
<!-- Progress Steps Card -->
|
||||
<div class="s1-progress-card">
|
||||
<div class="s1-steps">
|
||||
<!-- Step 1: 앱 정보입력 (completed) -->
|
||||
<!-- Step 1: 앱 정보수정 (completed) -->
|
||||
<div class="s1-step">
|
||||
<div class="s1-step-circle">
|
||||
<!-- 입력폼 아이콘 -->
|
||||
@@ -66,7 +67,7 @@
|
||||
|
||||
<div class="s1-step-line"></div>
|
||||
|
||||
<!-- Step 3: 앱 생성 완료 (rocket icon) -->
|
||||
<!-- Step 3: 앱 수정 완료 (rocket icon) -->
|
||||
<div class="s1-step">
|
||||
<div class="s1-step-circle">
|
||||
<!-- 로켓 아이콘 -->
|
||||
@@ -90,85 +91,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category Carousel Tab Container -->
|
||||
<div class="s2-category-carousel-container">
|
||||
<button type="button" class="s2-carousel-btn s2-carousel-btn--prev" id="btnPrevCategory" aria-label="이전 카테고리">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="15 18 9 12 15 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="s2-category-list-wrapper" id="categoryListWrapper">
|
||||
<div class="s2-category-list" id="categoryList">
|
||||
<button type="button" class="s2-category-tab active" data-group="">
|
||||
전체
|
||||
</button>
|
||||
<button type="button" class="s2-category-tab" th:each="service : ${apiServices}" th:data-group="${service.id}" th:text="${service.groupName}">
|
||||
서비스명
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="s2-carousel-btn s2-carousel-btn--next" id="btnNextCategory" aria-label="다음 카테고리">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<!-- API 선택 공용 모듈 -->
|
||||
<th:block th:replace="~{fragment/api_selector :: apiSelector(${apiServices}, ${apiKeyModification.selectedApis}, '/myapikey/modify/step2', '/myapikey/modify/step2/save')}"/>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<div class="s2-selection-container">
|
||||
<main class="s2-content-area">
|
||||
<form id="modifyStep2Form" method="post" th:action="@{/myapikey/modify/step2}" th:object="${apiKeyModification}" class="s2-form">
|
||||
|
||||
<!-- Hidden field for clientId -->
|
||||
<input type="hidden" th:field="*{clientId}"/>
|
||||
|
||||
<!-- Header filter: Search and Select All -->
|
||||
<div class="s2-filter-header">
|
||||
<div class="s2-select-all" id="selectAllWrapper" style="display: none;">
|
||||
<label class="s2-select-all-label">
|
||||
<input type="checkbox" id="selectAllCheckbox" class="visually-hidden">
|
||||
<span class="s2-checkbox-custom"></span>
|
||||
<span id="selectAllText">전체 선택</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="s2-result-count">
|
||||
총 <strong id="apiResultCount">0</strong>건
|
||||
</div>
|
||||
<div class="s2-search-box">
|
||||
<input type="text" id="apiSearch" class="s2-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>
|
||||
|
||||
<!-- API Cards Grid -->
|
||||
<div class="s2-cards-grid" id="apiCardGrid">
|
||||
<!-- Loading state -->
|
||||
<div class="s2-loading" id="loadingState" style="grid-column: 1 / -1;">
|
||||
<div class="s2-spinner"></div>
|
||||
<p>API 목록을 불러오는 중...</p>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div class="s2-empty" id="emptyState" style="display: none; grid-column: 1 / -1;">
|
||||
<div class="s2-empty-icon">📦</div>
|
||||
<h3>API를 선택해주세요</h3>
|
||||
<p>상단 카테고리에서 서비스를 선택하면 해당 API 목록이 표시됩니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
<!-- clientId 는 모듈 폼에 form 속성으로 주입 -->
|
||||
<input type="hidden" name="clientId" th:value="${apiKeyModification.clientId}" form="apiSelectorForm"/>
|
||||
|
||||
<!-- Bottom Navigation Actions -->
|
||||
<div class="s2-actions">
|
||||
<button type="button" id="btnPrevStep" class="s2-btn-prev">
|
||||
이전
|
||||
</button>
|
||||
<button type="submit" form="modifyStep2Form" class="s2-btn-save">
|
||||
다음
|
||||
<button type="submit" form="apiSelectorForm" class="s2-btn-save">
|
||||
저장
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -180,548 +115,7 @@
|
||||
|
||||
<!-- 화면 전체 오버레이/플로팅은 body 직속(pagePopups)으로 렌더 → wrapper transform·overflow 영향 없이 뷰포트 기준 중앙 정렬 -->
|
||||
<section layout:fragment="pagePopups">
|
||||
<!-- Floating Cart Button -->
|
||||
<button type="button" class="s2-floating-cart" id="floatingCartBtn" style="display: none;">
|
||||
<span class="s2-cart-label">선택된 API</span>
|
||||
<span class="s2-cart-badge" id="cartBadge">
|
||||
<span class="s2-cart-count">0</span>
|
||||
<span class="s2-cart-unit">개</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<!-- Selected APIs Modal -->
|
||||
<div class="s2-modal" id="selectedApisModal" style="display: none;">
|
||||
<div class="s2-modal-backdrop" id="modalOverlay"></div>
|
||||
<div class="s2-modal-dialog">
|
||||
<div class="s2-modal-header">
|
||||
<h3 class="s2-modal-title">선택된 API 목록</h3>
|
||||
<button type="button" class="s2-modal-close" id="modalCloseBtn">✕</button>
|
||||
</div>
|
||||
<div class="s2-modal-body">
|
||||
<div class="s2-selected-list" id="modalSelectedList">
|
||||
<!-- Dynamically populated -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="s2-modal-footer">
|
||||
<button type="button" class="s2-btn-close-modal" id="modalCancelBtn">닫기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:replace="~{fragment/api_selector :: apiSelectorPopups}"/>
|
||||
</section>
|
||||
|
||||
<th:block layout:fragment="contentScript">
|
||||
<script th:inline="javascript">
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// DOM Elements
|
||||
const form = document.getElementById('modifyStep2Form');
|
||||
const searchInput = document.getElementById('apiSearch');
|
||||
const menuTitles = document.querySelectorAll('.s2-category-tab');
|
||||
const apiCardGrid = document.getElementById('apiCardGrid');
|
||||
const loadingState = document.getElementById('loadingState');
|
||||
const emptyState = document.getElementById('emptyState');
|
||||
|
||||
let currentFilter = ''; // Empty string means "all"
|
||||
let currentServiceName = '전체'; // Store current service name
|
||||
let allApis = []; // Store loaded APIs
|
||||
let allLoadedApis = []; // Store ALL APIs from server for modal display
|
||||
let selectedApis = new Set(); // Track selected API IDs
|
||||
|
||||
// Restore selected APIs from session
|
||||
const sessionSelectedApis = /*[[${apiKeyModification.selectedApis}]]*/ [];
|
||||
if (sessionSelectedApis && Array.isArray(sessionSelectedApis)) {
|
||||
sessionSelectedApis.forEach(function(apiId) {
|
||||
selectedApis.add(apiId);
|
||||
});
|
||||
}
|
||||
|
||||
// Load all APIs for modal display (used when APIs from different categories are selected)
|
||||
function loadAllApisForModal() {
|
||||
const baseUrl = /*[[@{/apis/for_request}]]*/ '/apis/for_request';
|
||||
fetch(baseUrl)
|
||||
.then(response => response.json())
|
||||
.then(apis => {
|
||||
allLoadedApis = apis;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Failed to load all APIs for modal:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Load APIs via AJAX
|
||||
function loadApis(groupId) {
|
||||
// Show loading state
|
||||
loadingState.style.display = 'block';
|
||||
emptyState.style.display = 'none';
|
||||
|
||||
// Remove existing API cards
|
||||
document.querySelectorAll('.s2-api-card').forEach(card => card.remove());
|
||||
|
||||
// Build URL with optional groupId filter (Thymeleaf contextPath safe)
|
||||
const baseUrl = /*[[@{/apis/for_request}]]*/ '/apis/for_request';
|
||||
let url = baseUrl;
|
||||
if (groupId) {
|
||||
url += '?groupIds=' + encodeURIComponent(groupId);
|
||||
}
|
||||
|
||||
fetch(url).then(response => response.json()).then(apis => {
|
||||
allApis = apis;
|
||||
loadingState.style.display = 'none';
|
||||
|
||||
if (apis.length === 0) {
|
||||
emptyState.style.display = 'block';
|
||||
document.getElementById('apiResultCount').textContent = '0';
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('apiResultCount').textContent = apis.length;
|
||||
renderApiCards(apis);
|
||||
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';
|
||||
document.getElementById('apiResultCount').textContent = '0';
|
||||
});
|
||||
}
|
||||
|
||||
// Render API cards
|
||||
function renderApiCards(apis) {
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
apis.forEach(api => {
|
||||
const card = createApiCard(api);
|
||||
fragment.appendChild(card);
|
||||
});
|
||||
|
||||
apiCardGrid.appendChild(fragment);
|
||||
|
||||
// Re-attach event listeners
|
||||
attachCardEventListeners();
|
||||
|
||||
// Update modal list after cards are rendered
|
||||
updateModalList();
|
||||
}
|
||||
|
||||
// Create API card element
|
||||
function createApiCard(api) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 's2-api-card';
|
||||
card.setAttribute('data-group', api.apiGroupId || '');
|
||||
card.setAttribute('data-name', (api.apiName || '').toLowerCase());
|
||||
card.setAttribute('data-desc', (api.apiSimpleDescription || '').toLowerCase());
|
||||
card.setAttribute('data-api-id', api.apiId);
|
||||
|
||||
const isSelected = selectedApis.has(api.apiId);
|
||||
if (isSelected) {
|
||||
card.classList.add('selected');
|
||||
}
|
||||
|
||||
const mainIconHtml = api.mainIcon
|
||||
? `<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>`
|
||||
: `<i class="fas fa-cube"></i>`;
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="s2-api-card-badge">
|
||||
<span>${api.apiGroupName || '카테고리'}</span>
|
||||
</div>
|
||||
<!-- Checkbox container with visible custom design -->
|
||||
<label class="s2-checkbox-wrapper">
|
||||
<input type="checkbox"
|
||||
name="selectedApis"
|
||||
value="${api.apiId}"
|
||||
id="api-${api.apiId}"
|
||||
class="s2-api-checkbox visually-hidden"
|
||||
${isSelected ? 'checked' : ''}>
|
||||
<span class="s2-checkbox-custom"></span>
|
||||
</label>
|
||||
|
||||
<h3 class="s2-api-card-title">${api.apiName || 'API 이름'}</h3>
|
||||
<p class="s2-api-card-desc">${api.apiSimpleDescription || 'API 설명이 없습니다.'}</p>
|
||||
|
||||
<div class="s2-api-card-image">
|
||||
${mainIconHtml}
|
||||
</div>
|
||||
`;
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
// Attach event listeners to cards
|
||||
function attachCardEventListeners() {
|
||||
const apiCards = document.querySelectorAll('.s2-api-card');
|
||||
|
||||
apiCards.forEach(card => {
|
||||
card.addEventListener('click', function(e) {
|
||||
const checkbox = card.querySelector('.s2-api-checkbox');
|
||||
if (checkbox) {
|
||||
checkbox.checked = !checkbox.checked;
|
||||
updateCardSelection(checkbox);
|
||||
updateSelectedCount();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Prevent double toggle when clicking the checkbox wrapper
|
||||
const checkboxWrappers = document.querySelectorAll('.s2-checkbox-wrapper');
|
||||
checkboxWrappers.forEach(wrapper => {
|
||||
wrapper.addEventListener('click', function(e) {
|
||||
e.stopPropagation(); // Stop click from bubbling to card!
|
||||
});
|
||||
|
||||
// Listen to checkbox change event inside it
|
||||
const checkbox = wrapper.querySelector('.s2-api-checkbox');
|
||||
if (checkbox) {
|
||||
checkbox.addEventListener('change', function() {
|
||||
updateCardSelection(this);
|
||||
updateSelectedCount();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update card visual state
|
||||
function updateCardSelection(checkbox) {
|
||||
const card = checkbox.closest('.s2-api-card');
|
||||
if (checkbox.checked) {
|
||||
card.classList.add('selected');
|
||||
selectedApis.add(checkbox.value);
|
||||
} else {
|
||||
card.classList.remove('selected');
|
||||
selectedApis.delete(checkbox.value);
|
||||
}
|
||||
}
|
||||
|
||||
// Update selected count
|
||||
function updateSelectedCount() {
|
||||
// Update floating cart button
|
||||
const floatingCartBtn = document.getElementById('floatingCartBtn');
|
||||
const cartCount = document.querySelector('.s2-cart-count');
|
||||
|
||||
if (selectedApis.size > 0) {
|
||||
floatingCartBtn.style.display = 'flex';
|
||||
cartCount.textContent = selectedApis.size;
|
||||
} else {
|
||||
floatingCartBtn.style.display = 'none';
|
||||
}
|
||||
|
||||
// Update modal list
|
||||
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('.s2-api-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('.s2-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
|
||||
function updateModalList() {
|
||||
const modalSelectedList = document.getElementById('modalSelectedList');
|
||||
modalSelectedList.innerHTML = '';
|
||||
|
||||
if (selectedApis.size === 0) {
|
||||
modalSelectedList.innerHTML = '<p class="s2-empty-message">선택된 API가 없습니다.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Build list using selectedApis Set for consistency
|
||||
selectedApis.forEach(function(apiId) {
|
||||
// Try to find the checkbox in the DOM first
|
||||
const checkbox = document.querySelector('.s2-api-checkbox[value="' + apiId + '"]');
|
||||
let apiName = apiId; // Default to ID if we can't find the name
|
||||
|
||||
if (checkbox) {
|
||||
// If checkbox exists in DOM, get the name from the card
|
||||
const card = checkbox.closest('.s2-api-card');
|
||||
if (card) {
|
||||
const nameElement = card.querySelector('.s2-api-card-title');
|
||||
if (nameElement) {
|
||||
apiName = nameElement.textContent;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If checkbox not in DOM (different category is showing),
|
||||
// try to find the API in our loaded data
|
||||
let api = allApis.find(a => a.apiId === apiId);
|
||||
if (!api && allLoadedApis && allLoadedApis.length > 0) {
|
||||
api = allLoadedApis.find(a => a.apiId === apiId);
|
||||
}
|
||||
if (api && api.apiName) {
|
||||
apiName = api.apiName;
|
||||
}
|
||||
}
|
||||
|
||||
const apiPill = document.createElement('div');
|
||||
apiPill.className = 's2-api-pill';
|
||||
apiPill.innerHTML = `
|
||||
<span class="s2-api-pill-name">${apiName}</span>
|
||||
<button type="button" class="s2-api-pill-remove" data-value="${apiId}" aria-label="Remove ${apiName}">✕</button>
|
||||
`;
|
||||
modalSelectedList.appendChild(apiPill);
|
||||
});
|
||||
|
||||
// Add remove handlers
|
||||
document.querySelectorAll('.s2-api-pill-remove').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
const value = this.getAttribute('data-value');
|
||||
const checkbox = document.querySelector('.s2-api-checkbox[value="' + value + '"]');
|
||||
if (checkbox) {
|
||||
checkbox.checked = false;
|
||||
updateCardSelection(checkbox);
|
||||
updateSelectedCount();
|
||||
} else {
|
||||
// If checkbox not found (maybe different category is showing),
|
||||
// still remove from selection
|
||||
selectedApis.delete(value);
|
||||
updateSelectedCount();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Search functionality
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', function() {
|
||||
const searchTerm = this.value.toLowerCase();
|
||||
|
||||
const apiCards = document.querySelectorAll('.s2-api-card');
|
||||
let visibleCount = 0;
|
||||
apiCards.forEach(function(card) {
|
||||
const apiName = card.getAttribute('data-name');
|
||||
const apiDesc = card.getAttribute('data-desc');
|
||||
|
||||
// Check if matches search
|
||||
const matchesSearch = apiName.includes(searchTerm) || apiDesc.includes(searchTerm);
|
||||
|
||||
if (matchesSearch) {
|
||||
card.style.display = '';
|
||||
visibleCount++;
|
||||
} else {
|
||||
card.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('apiResultCount').textContent = visibleCount;
|
||||
|
||||
// Update select all checkbox state after search
|
||||
updateSelectAllCheckboxState();
|
||||
});
|
||||
}
|
||||
|
||||
// Category tab selection
|
||||
menuTitles.forEach(function(title) {
|
||||
title.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Remove active from all
|
||||
menuTitles.forEach(t => t.classList.remove('active'));
|
||||
// Add active to clicked
|
||||
this.classList.add('active');
|
||||
|
||||
const groupId = this.getAttribute('data-group');
|
||||
currentFilter = groupId;
|
||||
|
||||
// Store service name for select all label
|
||||
currentServiceName = this.textContent.trim();
|
||||
|
||||
// Load APIs for selected service
|
||||
loadApis(groupId);
|
||||
|
||||
// Clear search when changing category
|
||||
if (searchInput) {
|
||||
searchInput.value = '';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Category Carousel Scroll
|
||||
const categoryListWrapper = document.getElementById('categoryListWrapper');
|
||||
const btnPrevCategory = document.getElementById('btnPrevCategory');
|
||||
const btnNextCategory = document.getElementById('btnNextCategory');
|
||||
|
||||
if (categoryListWrapper && btnPrevCategory && btnNextCategory) {
|
||||
const scrollAmount = 200;
|
||||
|
||||
btnPrevCategory.addEventListener('click', function() {
|
||||
categoryListWrapper.scrollBy({ left: -scrollAmount, behavior: 'smooth' });
|
||||
});
|
||||
|
||||
btnNextCategory.addEventListener('click', function() {
|
||||
categoryListWrapper.scrollBy({ left: scrollAmount, behavior: 'smooth' });
|
||||
});
|
||||
|
||||
// Toggle buttons visibility/disabled state based on scroll position
|
||||
function updateCarouselButtons() {
|
||||
const scrollLeft = categoryListWrapper.scrollLeft;
|
||||
const maxScrollLeft = categoryListWrapper.scrollWidth - categoryListWrapper.clientWidth;
|
||||
|
||||
btnPrevCategory.disabled = scrollLeft <= 0;
|
||||
btnNextCategory.disabled = scrollLeft >= maxScrollLeft - 1;
|
||||
}
|
||||
|
||||
categoryListWrapper.addEventListener('scroll', updateCarouselButtons);
|
||||
window.addEventListener('resize', updateCarouselButtons);
|
||||
|
||||
// Initial check after loading categories
|
||||
setTimeout(updateCarouselButtons, 150);
|
||||
}
|
||||
|
||||
// Modal control
|
||||
const floatingCartBtn = document.getElementById('floatingCartBtn');
|
||||
const selectedApisModal = document.getElementById('selectedApisModal');
|
||||
const modalOverlay = document.getElementById('modalOverlay');
|
||||
const modalCloseBtn = document.getElementById('modalCloseBtn');
|
||||
const modalCancelBtn = document.getElementById('modalCancelBtn');
|
||||
|
||||
// Open modal
|
||||
function openModal() {
|
||||
selectedApisModal.style.display = 'flex'; // .s2-modal은 flex 중앙정렬 → block 금지
|
||||
// Add show class to backdrop and modal for visibility
|
||||
setTimeout(function() {
|
||||
if (modalOverlay) {
|
||||
modalOverlay.classList.add('show');
|
||||
}
|
||||
selectedApisModal.classList.add('show');
|
||||
}, 10);
|
||||
document.body.style.overflow = 'hidden'; // Prevent background scroll
|
||||
}
|
||||
|
||||
// Close modal
|
||||
function closeModal() {
|
||||
// Remove show class first for transition
|
||||
if (modalOverlay) {
|
||||
modalOverlay.classList.remove('show');
|
||||
}
|
||||
selectedApisModal.classList.remove('show');
|
||||
|
||||
// Hide modal after transition
|
||||
setTimeout(function() {
|
||||
selectedApisModal.style.display = 'none';
|
||||
}, 300);
|
||||
|
||||
document.body.style.overflow = ''; // Restore scroll
|
||||
}
|
||||
|
||||
// Floating cart button click
|
||||
if (floatingCartBtn) {
|
||||
floatingCartBtn.addEventListener('click', openModal);
|
||||
}
|
||||
|
||||
// Modal overlay click
|
||||
if (modalOverlay) {
|
||||
modalOverlay.addEventListener('click', closeModal);
|
||||
}
|
||||
|
||||
// Modal close button click
|
||||
if (modalCloseBtn) {
|
||||
modalCloseBtn.addEventListener('click', closeModal);
|
||||
}
|
||||
|
||||
// Modal cancel button click
|
||||
if (modalCancelBtn) {
|
||||
modalCancelBtn.addEventListener('click', closeModal);
|
||||
}
|
||||
|
||||
// Close modal on ESC key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape' && selectedApisModal.style.display === 'flex') {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Select All checkbox event
|
||||
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||
if (selectAllCheckbox) {
|
||||
selectAllCheckbox.addEventListener('change', function() {
|
||||
const isChecked = this.checked;
|
||||
const visibleCards = Array.from(document.querySelectorAll('.s2-api-card')).filter(card => card.style.display !== 'none');
|
||||
|
||||
visibleCards.forEach(function(card) {
|
||||
const checkbox = card.querySelector('.s2-api-checkbox');
|
||||
if (checkbox) {
|
||||
checkbox.checked = isChecked;
|
||||
updateCardSelection(checkbox);
|
||||
}
|
||||
});
|
||||
|
||||
updateSelectedCount();
|
||||
});
|
||||
}
|
||||
|
||||
// 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 (Thymeleaf contextPath safe)
|
||||
form.action = /*[[@{/myapikey/modify/step2/save}]]*/ '/myapikey/modify/step2/save';
|
||||
|
||||
// Submit the form to save selections
|
||||
form.submit();
|
||||
});
|
||||
}
|
||||
|
||||
// Form validation
|
||||
form.addEventListener('submit', function(e) {
|
||||
if (selectedApis.size === 0) {
|
||||
e.preventDefault();
|
||||
customPopups.showAlert('최소 1개 이상의 API를 선택해주세요.');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize
|
||||
loadAllApisForModal();
|
||||
updateSelectedCount(); // This will show count from restored session data
|
||||
|
||||
// Load all APIs automatically on page load (empty string = all APIs)
|
||||
loadApis('');
|
||||
|
||||
});
|
||||
</script>
|
||||
</th:block>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user