580 lines
21 KiB
JavaScript
580 lines
21 KiB
JavaScript
/**
|
|
* API 선택 공용 모듈 (fragment/api_selector.html 전용, figma s2 디자인)
|
|
*
|
|
* 사용처: 앱(API Key) 신청/수정 step2, Webhook 신청/수정 step2
|
|
*
|
|
* 계약:
|
|
* - 폼: #apiSelectorForm (data-save-action = "이전" 저장 POST 경로)
|
|
* - 기선택: window.API_SELECTOR_SELECTED / 목록 URL: window.API_SELECTOR_LIST_URL (fragment 인라인 주입)
|
|
* - "이전" 버튼: 호출 페이지의 #btnPrevStep (없으면 스킵)
|
|
* - 카트/모달: fragment `apiSelectorPopups` 를 pagePopups 슬롯에서 호출(body 직속)
|
|
* - 페이징: #apiPagination (window.API_SELECTOR_PAGE_SIZE 건/페이지, 기본 15) — 카테고리/검색은 클라이언트에서 처리
|
|
*
|
|
* design(figma s2) 인라인 스크립트 대비 패치 4건:
|
|
* 1) 모달 열 때마다 updateModalList() 재빌드 — 세션 복원 직후(카드 렌더 전) 빈 모달 방지
|
|
* 2) 모달 리스트를 DOM 체크박스가 아닌 selectedApis Set 기준으로 생성 — 미렌더/타 카테고리 누락 방지
|
|
* 3) 제출/이전 시 DOM에 없는 선택분을 hidden input으로 주입 — 카테고리 필터 상태 전송 유실 방지
|
|
* 4) 클라이언트 페이징 — 카드는 현재 페이지분만 DOM 렌더, 검색/전체선택/모달은 필터된 전체 목록 기준으로 동작
|
|
*/
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const form = document.getElementById('apiSelectorForm');
|
|
if (!form) {
|
|
return; // 모듈 미사용 페이지
|
|
}
|
|
|
|
const configuredPageSize = Number(globalThis.API_SELECTOR_PAGE_SIZE);
|
|
const PAGE_SIZE = Number.isInteger(configuredPageSize) && configuredPageSize > 0 ? configuredPageSize : 15;
|
|
|
|
// DOM Elements
|
|
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');
|
|
const paginationEl = document.getElementById('apiPagination');
|
|
|
|
let currentFilter = ''; // Empty string means "all"
|
|
let currentServiceName = '전체';
|
|
let allApis = []; // 현재 카테고리 조회 결과 전체
|
|
let filteredApis = []; // allApis 에 검색어까지 적용한 결과(페이징 대상)
|
|
let currentPage = 1;
|
|
let selectedApis = new Set();
|
|
|
|
// Restore selected APIs from session (fragment 인라인 주입)
|
|
const sessionSelectedApis = window.API_SELECTOR_SELECTED;
|
|
if (sessionSelectedApis && Array.isArray(sessionSelectedApis)) {
|
|
sessionSelectedApis.forEach(function(apiId) {
|
|
selectedApis.add(apiId);
|
|
});
|
|
}
|
|
|
|
// Load APIs via AJAX
|
|
function loadApis(groupId) {
|
|
loadingState.style.display = 'block';
|
|
emptyState.style.display = 'none';
|
|
clearCards();
|
|
|
|
const baseUrl = window.API_SELECTOR_LIST_URL || '/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';
|
|
applySearch();
|
|
}).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';
|
|
renderPagination(0);
|
|
});
|
|
}
|
|
|
|
// 렌더된 카드만 제거(로딩/빈 상태 엘리먼트는 유지)
|
|
function clearCards() {
|
|
document.querySelectorAll('.s2-api-card').forEach(card => card.remove());
|
|
}
|
|
|
|
// 검색어 기준으로 allApis → filteredApis 재계산 후 1페이지부터 렌더
|
|
function applySearch() {
|
|
const term = searchInput ? searchInput.value.toLowerCase().trim() : '';
|
|
filteredApis = !term ? allApis : allApis.filter(function(api) {
|
|
const name = (api.apiName || '').toLowerCase();
|
|
const desc = (api.apiSimpleDescription || '').toLowerCase();
|
|
return name.includes(term) || desc.includes(term);
|
|
});
|
|
goToPage(1);
|
|
}
|
|
|
|
// 지정 페이지로 이동 — 해당 페이지분만 렌더(재조회 없음)
|
|
function goToPage(page) {
|
|
const totalPages = Math.max(1, Math.ceil(filteredApis.length / PAGE_SIZE));
|
|
currentPage = Math.min(Math.max(1, page), totalPages);
|
|
|
|
clearCards();
|
|
document.getElementById('apiResultCount').textContent = filteredApis.length;
|
|
|
|
if (filteredApis.length === 0) {
|
|
emptyState.style.display = 'block';
|
|
renderPagination(0);
|
|
updateSelectAllUI();
|
|
return;
|
|
}
|
|
emptyState.style.display = 'none';
|
|
|
|
const start = (currentPage - 1) * PAGE_SIZE;
|
|
renderApiCards(filteredApis.slice(start, start + PAGE_SIZE));
|
|
renderPagination(filteredApis.length);
|
|
updateSelectAllUI();
|
|
}
|
|
|
|
// Render API cards (현재 페이지분)
|
|
function renderApiCards(apis) {
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
apis.forEach(api => {
|
|
fragment.appendChild(createApiCard(api));
|
|
});
|
|
|
|
apiCardGrid.appendChild(fragment);
|
|
attachCardEventListeners();
|
|
}
|
|
|
|
// Create API card element (figma s2 card)
|
|
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 || api.service || '카테고리'}</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!
|
|
});
|
|
|
|
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() {
|
|
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';
|
|
}
|
|
|
|
updateModalList();
|
|
updateSelectAllCheckboxState();
|
|
}
|
|
|
|
// Update select all UI visibility and text
|
|
function updateSelectAllUI() {
|
|
const selectAllWrapper = document.getElementById('selectAllWrapper');
|
|
const selectAllText = document.getElementById('selectAllText');
|
|
|
|
if (currentFilter === '') {
|
|
selectAllWrapper.style.display = 'none';
|
|
} else {
|
|
selectAllWrapper.style.display = 'flex';
|
|
selectAllText.textContent = currentServiceName + ' API 전체 선택';
|
|
}
|
|
|
|
updateSelectAllCheckboxState();
|
|
}
|
|
|
|
// Update select all checkbox state — 현재 페이지가 아닌 필터된 전체 목록 기준(페이징 무관)
|
|
function updateSelectAllCheckboxState() {
|
|
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
|
if (!selectAllCheckbox) {
|
|
return;
|
|
}
|
|
|
|
if (filteredApis.length === 0) {
|
|
selectAllCheckbox.checked = false;
|
|
selectAllCheckbox.indeterminate = false;
|
|
return;
|
|
}
|
|
|
|
const checkedCount = filteredApis.filter(api => selectedApis.has(api.apiId)).length;
|
|
|
|
if (checkedCount === 0) {
|
|
selectAllCheckbox.checked = false;
|
|
selectAllCheckbox.indeterminate = false;
|
|
} else if (checkedCount === filteredApis.length) {
|
|
selectAllCheckbox.checked = true;
|
|
selectAllCheckbox.indeterminate = false;
|
|
} else {
|
|
selectAllCheckbox.checked = false;
|
|
selectAllCheckbox.indeterminate = true;
|
|
}
|
|
}
|
|
|
|
// Update modal selected APIs list — selectedApis Set 기준(패치 2), 이름은 allApis 우선 조회(패치 4)
|
|
function updateModalList() {
|
|
const modalSelectedList = document.getElementById('modalSelectedList');
|
|
modalSelectedList.innerHTML = '';
|
|
|
|
if (selectedApis.size === 0) {
|
|
modalSelectedList.innerHTML = '<p class="s2-empty-message">선택된 API가 없습니다.</p>';
|
|
return;
|
|
}
|
|
|
|
selectedApis.forEach(function(apiId) {
|
|
const apiData = allApis.find(a => a.apiId === apiId);
|
|
const card = document.querySelector('.s2-api-card[data-api-id="' + apiId + '"]');
|
|
const apiName = apiData ? apiData.apiName
|
|
: (card ? card.querySelector('.s2-api-card-title').textContent : apiId);
|
|
|
|
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);
|
|
});
|
|
|
|
document.querySelectorAll('.s2-api-pill-remove').forEach(function(btn) {
|
|
btn.addEventListener('click', function() {
|
|
const value = this.getAttribute('data-value');
|
|
selectedApis.delete(value);
|
|
const checkbox = document.querySelector('.s2-api-checkbox[value="' + value + '"]');
|
|
if (checkbox) {
|
|
checkbox.checked = false;
|
|
updateCardSelection(checkbox);
|
|
}
|
|
updateSelectedCount();
|
|
});
|
|
});
|
|
}
|
|
|
|
// Pagination 컨트롤 렌더 — fragment/pagination.html 과 동일 마크업/클래스 재사용(전역 _pagination.scss 적용)
|
|
function renderPagination(totalItems) {
|
|
if (!paginationEl) {
|
|
return;
|
|
}
|
|
paginationEl.innerHTML = '';
|
|
|
|
const totalPages = Math.ceil(totalItems / PAGE_SIZE);
|
|
if (totalPages <= 1) {
|
|
return;
|
|
}
|
|
|
|
const ICON_FIRST = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M0 19V5H2.30769V19H0ZM15 19L4.61538 12L15 5V19Z" fill="currentColor"/><path d="M24 19L15 12L24 5V19Z" fill="currentColor"/></svg><span class="blind">처음 페이지</span>';
|
|
const ICON_PREV = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M16 5L16 19L5 12L16 5Z" fill="currentColor"/></svg><span class="blind">이전 페이지</span>';
|
|
const ICON_NEXT = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M8 19V5L19 12L8 19Z" fill="currentColor"/></svg><span class="blind">다음 페이지</span>';
|
|
const ICON_LAST = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M24 5L24 19L21.6923 19L21.6923 5L24 5ZM9 5L19.3846 12L9 19L9 5Z" fill="currentColor"/><path d="M1.22392e-06 5L9 12L0 19L1.22392e-06 5Z" fill="currentColor"/></svg><span class="blind">마지막 페이지</span>';
|
|
|
|
function navLink(cls, iconHtml, targetPage, disabled) {
|
|
const a = document.createElement('a');
|
|
a.href = '#';
|
|
a.className = cls + (disabled ? ' disabled' : '');
|
|
a.innerHTML = iconHtml;
|
|
if (!disabled) {
|
|
a.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
goToPage(targetPage);
|
|
});
|
|
}
|
|
return a;
|
|
}
|
|
|
|
function numLink(p) {
|
|
const a = document.createElement('a');
|
|
a.href = '#';
|
|
const isCurrent = p === currentPage;
|
|
a.className = 'page-num' + (isCurrent ? ' page-current' : '');
|
|
a.textContent = String(p);
|
|
if (!isCurrent) {
|
|
a.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
goToPage(p);
|
|
});
|
|
}
|
|
return a;
|
|
}
|
|
|
|
paginationEl.appendChild(navLink('page-first', ICON_FIRST, 1, currentPage === 1));
|
|
paginationEl.appendChild(navLink('page-prev', ICON_PREV, currentPage - 1, currentPage === 1));
|
|
|
|
const windowStart = Math.max(1, Math.min(currentPage - 2, totalPages - 4));
|
|
const windowEnd = Math.min(totalPages, windowStart + 4);
|
|
for (let p = Math.max(1, windowStart); p <= windowEnd; p++) {
|
|
paginationEl.appendChild(numLink(p));
|
|
}
|
|
|
|
paginationEl.appendChild(navLink('page-next', ICON_NEXT, currentPage + 1, currentPage === totalPages));
|
|
paginationEl.appendChild(navLink('page-last', ICON_LAST, totalPages, currentPage === totalPages));
|
|
}
|
|
|
|
// Search functionality — 클라이언트 필터(재조회 없음), 필터 변경 시 1페이지로 리셋
|
|
if (searchInput) {
|
|
searchInput.addEventListener('input', function() {
|
|
applySearch();
|
|
});
|
|
}
|
|
|
|
// Category tab selection
|
|
menuTitles.forEach(function(title) {
|
|
title.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
|
|
menuTitles.forEach(t => t.classList.remove('active'));
|
|
this.classList.add('active');
|
|
|
|
const groupId = this.getAttribute('data-group');
|
|
currentFilter = groupId;
|
|
currentServiceName = this.textContent.trim();
|
|
|
|
if (searchInput) {
|
|
searchInput.value = '';
|
|
}
|
|
loadApis(groupId);
|
|
});
|
|
});
|
|
|
|
// 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');
|
|
|
|
function openModal() {
|
|
updateModalList(); // 열 때마다 최신 선택 상태로 재빌드 (패치 1)
|
|
selectedApisModal.style.display = 'flex'; // .s2-modal은 flex 중앙정렬 → block 금지
|
|
setTimeout(function() {
|
|
if (modalOverlay) {
|
|
modalOverlay.classList.add('show');
|
|
}
|
|
selectedApisModal.classList.add('show');
|
|
}, 10);
|
|
document.body.style.overflow = 'hidden';
|
|
}
|
|
|
|
function closeModal() {
|
|
if (modalOverlay) {
|
|
modalOverlay.classList.remove('show');
|
|
}
|
|
selectedApisModal.classList.remove('show');
|
|
|
|
setTimeout(function() {
|
|
selectedApisModal.style.display = 'none';
|
|
}, 300);
|
|
|
|
document.body.style.overflow = '';
|
|
}
|
|
|
|
if (floatingCartBtn) {
|
|
floatingCartBtn.addEventListener('click', openModal);
|
|
}
|
|
if (modalOverlay) {
|
|
modalOverlay.addEventListener('click', closeModal);
|
|
}
|
|
if (modalCloseBtn) {
|
|
modalCloseBtn.addEventListener('click', closeModal);
|
|
}
|
|
if (modalCancelBtn) {
|
|
modalCancelBtn.addEventListener('click', closeModal);
|
|
}
|
|
|
|
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;
|
|
|
|
filteredApis.forEach(function(api) {
|
|
if (isChecked) {
|
|
selectedApis.add(api.apiId);
|
|
} else {
|
|
selectedApis.delete(api.apiId);
|
|
}
|
|
});
|
|
|
|
// 현재 페이지에 실제 렌더된 카드만 체크 상태 동기화
|
|
document.querySelectorAll('.s2-api-card').forEach(function(card) {
|
|
const checkbox = card.querySelector('.s2-api-checkbox');
|
|
if (checkbox) {
|
|
checkbox.checked = isChecked;
|
|
card.classList.toggle('selected', isChecked);
|
|
}
|
|
});
|
|
|
|
updateSelectedCount();
|
|
});
|
|
}
|
|
|
|
// DOM에 렌더되지 않은 선택분을 hidden input으로 주입 — 전송 유실 방지 (패치 3)
|
|
function syncHiddenSelected() {
|
|
document.querySelectorAll('input.hidden-selected-api').forEach(el => el.remove());
|
|
selectedApis.forEach(function(apiId) {
|
|
if (!document.querySelector('.s2-api-checkbox[value="' + apiId + '"]')) {
|
|
const input = document.createElement('input');
|
|
input.type = 'hidden';
|
|
input.name = 'selectedApis';
|
|
input.value = apiId;
|
|
input.className = 'hidden-selected-api';
|
|
form.appendChild(input);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Previous step button — 선택 저장 후 step1 복귀
|
|
const btnPrevStep = document.getElementById('btnPrevStep');
|
|
if (btnPrevStep) {
|
|
btnPrevStep.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
|
|
const saveAction = form.getAttribute('data-save-action');
|
|
if (saveAction) {
|
|
form.action = saveAction;
|
|
}
|
|
syncHiddenSelected();
|
|
form.submit();
|
|
});
|
|
}
|
|
|
|
// Form submit
|
|
// - Webhook/앱 변경은 API 가 필수(form data-api-required=true)이므로 미선택 시 차단한다.
|
|
// - 앱 신청(clients register)은 선택 사항이라, 미선택 시 확인 팝업으로 한 번 더 묻고 진행한다.
|
|
form.addEventListener('submit', function(e) {
|
|
if (selectedApis.size !== 0) {
|
|
syncHiddenSelected();
|
|
return;
|
|
}
|
|
|
|
var apiRequired = form.dataset.apiRequired === 'true';
|
|
if (apiRequired) {
|
|
e.preventDefault();
|
|
if (typeof customPopups !== 'undefined' && customPopups.showAlert) {
|
|
customPopups.showAlert('최소 1개 이상의 API를 선택해주세요.');
|
|
} else {
|
|
alert('최소 1개 이상의 API를 선택해주세요.');
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// 미선택 상태로 앱 신청 → 확인 후 진행
|
|
e.preventDefault();
|
|
var message = '선택한 API가 없습니다. API 없이 앱을 신청하시겠습니까?';
|
|
// custom-popups.js 는 top-level const 라 window.customPopups 가 아닌 렉시컬 전역이다.
|
|
if (typeof customPopups !== 'undefined' && customPopups.showConfirm) {
|
|
customPopups.showConfirm(message, function (ok) {
|
|
if (ok) {
|
|
syncHiddenSelected();
|
|
form.submit(); // submit() 은 submit 이벤트를 재발생시키지 않음 → 재확인 루프 없음
|
|
}
|
|
});
|
|
} else if (window.confirm(message)) {
|
|
syncHiddenSelected();
|
|
form.submit();
|
|
}
|
|
return false;
|
|
});
|
|
|
|
// Initialize
|
|
updateSelectedCount();
|
|
loadApis('');
|
|
});
|