- Webhook 신청 및 관리 UI 전면 개편 - 신규 가이드 반영

- Webhook 수정/삭제 기능 UX 개선 - 단계별 디자인 업데이트
- Webhook 등록/수정 CSS 및 JS 로직 최적화 및 재사용 코드 추가
This commit is contained in:
Rinjae
2026-07-20 19:53:54 +09:00
parent 9f4874fe44
commit 41391c8df0
21 changed files with 1107 additions and 1923 deletions
+103 -100
View File
@@ -1,15 +1,15 @@
/**
* API 선택 공용 모듈 (fragment/api_selector.html 전용)
* API 선택 공용 모듈 (fragment/api_selector.html 전용, figma s2 디자인)
*
* 사용처: 앱(API Key) 신청/수정 step2, Webhook 신청/수정 step2
*
* 계약:
* - 폼: #apiSelectorForm (data-save-action = "이전" 저장 POST 경로)
* - 기선택: window.API_SELECTOR_SELECTED (fragment 인라인 스크립트가 주입)
* - 기선택: window.API_SELECTOR_SELECTED / 목록 URL: window.API_SELECTOR_LIST_URL (fragment 인라인 주입)
* - "이전" 버튼: 호출 페이지의 #btnPrevStep (없으면 스킵)
* - API 목록: GET /apis/for_request[?groupIds=] (ROLE_API_KEY_REQUEST)
* - 카트/모달: fragment `apiSelectorPopups` 를 pagePopups 슬롯에서 호출(body 직속)
*
* 원본(apiKeyRegisterStep2 인라인 스크립트) 대비 패치 3건:
* design(figma s2) 인라인 스크립트 대비 패치 3건:
* 1) 모달 열 때마다 updateModalList() 재빌드 — 세션 복원 직후(카드 렌더 전) 빈 모달 방지
* 2) 모달 리스트를 DOM 체크박스가 아닌 selectedApis Set 기준으로 생성 — 미렌더/타 카테고리 누락 방지
* 3) 제출/이전 시 DOM에 없는 선택분을 hidden input으로 주입 — 카테고리 필터 상태 전송 유실 방지
@@ -22,10 +22,7 @@ document.addEventListener('DOMContentLoaded', function() {
// DOM Elements
const searchInput = document.getElementById('apiSearch');
const sidebar = document.getElementById('apiSidebar');
const mobileToggle = document.getElementById('mobileToggle');
const mobileOverlay = document.getElementById('mobileOverlay');
const menuTitles = document.querySelectorAll('.menu-title');
const menuTitles = document.querySelectorAll('.s2-category-tab');
const apiCardGrid = document.getElementById('apiCardGrid');
const loadingState = document.getElementById('loadingState');
const emptyState = document.getElementById('emptyState');
@@ -48,9 +45,10 @@ document.addEventListener('DOMContentLoaded', function() {
loadingState.style.display = 'block';
emptyState.style.display = 'none';
document.querySelectorAll('.api-selection-card').forEach(card => card.remove());
document.querySelectorAll('.s2-api-card').forEach(card => card.remove());
let url = '/apis/for_request';
const baseUrl = window.API_SELECTOR_LIST_URL || '/apis/for_request';
let url = baseUrl;
if (groupId) {
url += '?groupIds=' + encodeURIComponent(groupId);
}
@@ -61,9 +59,11 @@ document.addEventListener('DOMContentLoaded', function() {
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 => {
@@ -72,6 +72,7 @@ document.addEventListener('DOMContentLoaded', function() {
emptyState.querySelector('h3').textContent = 'API 로드 실패';
emptyState.querySelector('p').textContent = '다시 시도해주세요.';
emptyState.style.display = 'block';
document.getElementById('apiResultCount').textContent = '0';
});
}
@@ -87,10 +88,10 @@ document.addEventListener('DOMContentLoaded', function() {
attachCardEventListeners();
}
// Create API card element
// Create API card element (figma s2 card)
function createApiCard(api) {
const card = document.createElement('div');
card.className = 'api-selection-card';
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());
@@ -106,26 +107,25 @@ document.addEventListener('DOMContentLoaded', function() {
: `<i class="fas fa-cube"></i>`;
card.innerHTML = `
<div class="api-card-content">
<div class="api-card-header">
<span class="api-card-category">${api.service || '카테고리'}</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 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="api-name">${api.apiName || 'API 이름'}</h3>
<p class="api-description">${api.apiSimpleDescription || 'API 설명이 없습니다.'}</p>
<h3 class="s2-api-card-title">${api.apiName || 'API 이름'}</h3>
<p class="s2-api-card-desc">${api.apiSimpleDescription || 'API 설명이 없습니다.'}</p>
<div class="api-card-icon">
${mainIconHtml}
</div>
<div class="s2-api-card-image">
${mainIconHtml}
</div>
`;
@@ -134,22 +134,11 @@ document.addEventListener('DOMContentLoaded', function() {
// Attach event listeners to cards
function attachCardEventListeners() {
const checkboxes = document.querySelectorAll('.api-checkbox');
const apiCards = document.querySelectorAll('.api-selection-card');
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', function() {
updateCardSelection(this);
updateSelectedCount();
});
});
const apiCards = document.querySelectorAll('.s2-api-card');
apiCards.forEach(card => {
card.addEventListener('click', function(e) {
if (e.target.classList.contains('api-checkbox')) {
return;
}
const checkbox = card.querySelector('.api-checkbox');
const checkbox = card.querySelector('.s2-api-checkbox');
if (checkbox) {
checkbox.checked = !checkbox.checked;
updateCardSelection(checkbox);
@@ -157,11 +146,27 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
});
// 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('.api-selection-card');
const card = checkbox.closest('.s2-api-card');
if (checkbox.checked) {
card.classList.add('selected');
selectedApis.add(checkbox.value);
@@ -174,7 +179,7 @@ document.addEventListener('DOMContentLoaded', function() {
// Update selected count
function updateSelectedCount() {
const floatingCartBtn = document.getElementById('floatingCartBtn');
const cartCount = document.querySelector('.cart-count');
const cartCount = document.querySelector('.s2-cart-count');
if (selectedApis.size > 0) {
floatingCartBtn.style.display = 'flex';
@@ -205,7 +210,7 @@ document.addEventListener('DOMContentLoaded', function() {
// 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');
const visibleCards = Array.from(document.querySelectorAll('.s2-api-card')).filter(card => card.style.display !== 'none');
if (visibleCards.length === 0) {
selectAllCheckbox.checked = false;
@@ -213,7 +218,7 @@ document.addEventListener('DOMContentLoaded', function() {
return;
}
const visibleCheckboxes = visibleCards.map(card => card.querySelector('.api-checkbox'));
const visibleCheckboxes = visibleCards.map(card => card.querySelector('.s2-api-checkbox'));
const checkedCount = visibleCheckboxes.filter(cb => cb.checked).length;
if (checkedCount === 0) {
@@ -234,33 +239,28 @@ document.addEventListener('DOMContentLoaded', function() {
modalSelectedList.innerHTML = '';
if (selectedApis.size === 0) {
modalSelectedList.innerHTML = '<p class="empty-message">선택된 API가 없습니다.</p>';
modalSelectedList.innerHTML = '<p class="s2-empty-message">선택된 API가 없습니다.</p>';
return;
}
selectedApis.forEach(function(apiId) {
const card = document.querySelector('.api-selection-card[data-api-id="' + apiId + '"]');
const apiName = card ? card.querySelector('.api-name').textContent : apiId;
const card = document.querySelector('.s2-api-card[data-api-id="' + apiId + '"]');
const apiName = card ? card.querySelector('.s2-api-card-title').textContent : apiId;
const apiPill = document.createElement('div');
apiPill.className = 'api-pill';
apiPill.className = 's2-api-pill';
apiPill.innerHTML = `
<span class="api-pill-name">${apiName}</span>
<button type="button" class="api-pill-remove" data-value="${apiId}" aria-label="Remove ${apiName}">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
<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('.api-pill-remove').forEach(function(btn) {
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('.api-checkbox[value="' + value + '"]');
const checkbox = document.querySelector('.s2-api-checkbox[value="' + value + '"]');
if (checkbox) {
checkbox.checked = false;
updateCardSelection(checkbox);
@@ -275,18 +275,27 @@ document.addEventListener('DOMContentLoaded', function() {
searchInput.addEventListener('input', function() {
const searchTerm = this.value.toLowerCase();
document.querySelectorAll('.api-selection-card').forEach(function(card) {
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');
const matchesSearch = apiName.includes(searchTerm) || apiDesc.includes(searchTerm);
card.style.display = matchesSearch ? '' : 'none';
if (matchesSearch) {
card.style.display = '';
visibleCount++;
} else {
card.style.display = 'none';
}
});
document.getElementById('apiResultCount').textContent = visibleCount;
updateSelectAllCheckboxState();
});
}
// Sidebar category selection
// Category tab selection
menuTitles.forEach(function(title) {
title.addEventListener('click', function(e) {
e.preventDefault();
@@ -296,53 +305,47 @@ document.addEventListener('DOMContentLoaded', function() {
const groupId = this.getAttribute('data-group');
currentFilter = groupId;
currentServiceName = this.textContent.trim().split('\n')[0].trim();
currentServiceName = this.textContent.trim();
loadApis(groupId);
if (searchInput) {
searchInput.value = '';
}
if (window.innerWidth <= 768 && sidebar.classList.contains('mobile-open')) {
closeMobileMenu();
}
});
});
// Mobile menu toggle
function closeMobileMenu() {
sidebar.classList.remove('mobile-open');
mobileOverlay.classList.remove('active');
mobileToggle.innerHTML = '☰';
mobileToggle.setAttribute('aria-label', '메뉴 열기');
}
// Category Carousel Scroll
const categoryListWrapper = document.getElementById('categoryListWrapper');
const btnPrevCategory = document.getElementById('btnPrevCategory');
const btnNextCategory = document.getElementById('btnNextCategory');
if (mobileToggle && sidebar && mobileOverlay) {
mobileToggle.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
if (categoryListWrapper && btnPrevCategory && btnNextCategory) {
const scrollAmount = 200;
sidebar.classList.toggle('mobile-open');
mobileOverlay.classList.toggle('active');
if (sidebar.classList.contains('mobile-open')) {
this.innerHTML = '✕';
this.setAttribute('aria-label', '메뉴 닫기');
} else {
this.innerHTML = '☰';
this.setAttribute('aria-label', '메뉴 열기');
}
btnPrevCategory.addEventListener('click', function() {
categoryListWrapper.scrollBy({ left: -scrollAmount, behavior: 'smooth' });
});
mobileOverlay.addEventListener('click', closeMobileMenu);
}
btnNextCategory.addEventListener('click', function() {
categoryListWrapper.scrollBy({ left: scrollAmount, behavior: 'smooth' });
});
window.addEventListener('resize', function() {
if (window.innerWidth > 768 && sidebar.classList.contains('mobile-open')) {
closeMobileMenu();
// 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');
@@ -353,7 +356,7 @@ document.addEventListener('DOMContentLoaded', function() {
function openModal() {
updateModalList(); // 열 때마다 최신 선택 상태로 재빌드 (패치 1)
selectedApisModal.style.display = 'block';
selectedApisModal.style.display = 'flex'; // .s2-modal은 flex 중앙정렬 → block 금지
setTimeout(function() {
if (modalOverlay) {
modalOverlay.classList.add('show');
@@ -390,7 +393,7 @@ document.addEventListener('DOMContentLoaded', function() {
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && selectedApisModal.style.display === 'block') {
if (e.key === 'Escape' && selectedApisModal.style.display === 'flex') {
closeModal();
}
});
@@ -400,10 +403,10 @@ document.addEventListener('DOMContentLoaded', function() {
if (selectAllCheckbox) {
selectAllCheckbox.addEventListener('change', function() {
const isChecked = this.checked;
const visibleCards = Array.from(document.querySelectorAll('.api-selection-card')).filter(card => card.style.display !== 'none');
const visibleCards = Array.from(document.querySelectorAll('.s2-api-card')).filter(card => card.style.display !== 'none');
visibleCards.forEach(function(card) {
const checkbox = card.querySelector('.api-checkbox');
const checkbox = card.querySelector('.s2-api-checkbox');
if (checkbox) {
checkbox.checked = isChecked;
updateCardSelection(checkbox);
@@ -418,7 +421,7 @@ document.addEventListener('DOMContentLoaded', function() {
function syncHiddenSelected() {
document.querySelectorAll('input.hidden-selected-api').forEach(el => el.remove());
selectedApis.forEach(function(apiId) {
if (!document.querySelector('.api-checkbox[value="' + apiId + '"]')) {
if (!document.querySelector('.s2-api-checkbox[value="' + apiId + '"]')) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'selectedApis';