/** * 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 ? `${api.apiName}` : ``; card.innerHTML = `
${api.apiGroupName || api.service || '카테고리'}

${api.apiName || 'API 이름'}

${api.apiSimpleDescription || 'API 설명이 없습니다.'}

${mainIconHtml}
`; 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 = '

선택된 API가 없습니다.

'; 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 = ` ${apiName} `; 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 = '처음 페이지'; const ICON_PREV = '이전 페이지'; const ICON_NEXT = '다음 페이지'; const ICON_LAST = '마지막 페이지'; 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(''); });