모니터링 코드 엔티티 제거 및 API 선택 모듈 추가:
- MonitoringCode 엔티티, 리포지토리, 복합키 클래스 삭제 - API 선택 공용 모듈 JS 및 HTML 파일 추가 (api_selector) - 웹훅 개발 가이드 페이지 신규 추가 (webhook-dev-guide)
This commit is contained in:
@@ -0,0 +1,464 @@
|
||||
/**
|
||||
* API 선택 공용 모듈 (fragment/api_selector.html 전용)
|
||||
*
|
||||
* 사용처: 앱(API Key) 신청/수정 step2, Webhook 신청/수정 step2
|
||||
*
|
||||
* 계약:
|
||||
* - 폼: #apiSelectorForm (data-save-action = "이전" 저장 POST 경로)
|
||||
* - 기선택: window.API_SELECTOR_SELECTED (fragment 인라인 스크립트가 주입)
|
||||
* - "이전" 버튼: 호출 페이지의 #btnPrevStep (없으면 스킵)
|
||||
* - API 목록: GET /apis/for_request[?groupIds=] (ROLE_API_KEY_REQUEST)
|
||||
*
|
||||
* 원본(apiKeyRegisterStep2 인라인 스크립트) 대비 패치 3건:
|
||||
* 1) 모달 열 때마다 updateModalList() 재빌드 — 세션 복원 직후(카드 렌더 전) 빈 모달 방지
|
||||
* 2) 모달 리스트를 DOM 체크박스가 아닌 selectedApis Set 기준으로 생성 — 미렌더/타 카테고리 누락 방지
|
||||
* 3) 제출/이전 시 DOM에 없는 선택분을 hidden input으로 주입 — 카테고리 필터 상태 전송 유실 방지
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.getElementById('apiSelectorForm');
|
||||
if (!form) {
|
||||
return; // 모듈 미사용 페이지
|
||||
}
|
||||
|
||||
// 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 apiCardGrid = document.getElementById('apiCardGrid');
|
||||
const loadingState = document.getElementById('loadingState');
|
||||
const emptyState = document.getElementById('emptyState');
|
||||
|
||||
let currentFilter = ''; // Empty string means "all"
|
||||
let currentServiceName = '전체';
|
||||
let allApis = [];
|
||||
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';
|
||||
|
||||
document.querySelectorAll('.api-selection-card').forEach(card => card.remove());
|
||||
|
||||
let url = '/apis/for_request';
|
||||
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';
|
||||
return;
|
||||
}
|
||||
|
||||
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';
|
||||
});
|
||||
}
|
||||
|
||||
// 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
|
||||
function createApiCard(api) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'api-selection-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="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>
|
||||
|
||||
<h3 class="api-name">${api.apiName || 'API 이름'}</h3>
|
||||
<p class="api-description">${api.apiSimpleDescription || 'API 설명이 없습니다.'}</p>
|
||||
|
||||
<div class="api-card-icon">
|
||||
${mainIconHtml}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
|
||||
apiCards.forEach(card => {
|
||||
card.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('api-checkbox')) {
|
||||
return;
|
||||
}
|
||||
const checkbox = card.querySelector('.api-checkbox');
|
||||
if (checkbox) {
|
||||
checkbox.checked = !checkbox.checked;
|
||||
updateCardSelection(checkbox);
|
||||
updateSelectedCount();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Update card visual state
|
||||
function updateCardSelection(checkbox) {
|
||||
const card = checkbox.closest('.api-selection-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('.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 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');
|
||||
|
||||
if (visibleCards.length === 0) {
|
||||
selectAllCheckbox.checked = false;
|
||||
selectAllCheckbox.indeterminate = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const visibleCheckboxes = visibleCards.map(card => card.querySelector('.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 — selectedApis Set 기준 (패치 2)
|
||||
function updateModalList() {
|
||||
const modalSelectedList = document.getElementById('modalSelectedList');
|
||||
modalSelectedList.innerHTML = '';
|
||||
|
||||
if (selectedApis.size === 0) {
|
||||
modalSelectedList.innerHTML = '<p class="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 apiPill = document.createElement('div');
|
||||
apiPill.className = '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>
|
||||
`;
|
||||
modalSelectedList.appendChild(apiPill);
|
||||
});
|
||||
|
||||
document.querySelectorAll('.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 + '"]');
|
||||
if (checkbox) {
|
||||
checkbox.checked = false;
|
||||
updateCardSelection(checkbox);
|
||||
}
|
||||
updateSelectedCount();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Search functionality
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', function() {
|
||||
const searchTerm = this.value.toLowerCase();
|
||||
|
||||
document.querySelectorAll('.api-selection-card').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';
|
||||
});
|
||||
|
||||
updateSelectAllCheckboxState();
|
||||
});
|
||||
}
|
||||
|
||||
// Sidebar category 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().split('\n')[0].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', '메뉴 열기');
|
||||
}
|
||||
|
||||
if (mobileToggle && sidebar && mobileOverlay) {
|
||||
mobileToggle.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
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', '메뉴 열기');
|
||||
}
|
||||
});
|
||||
|
||||
mobileOverlay.addEventListener('click', closeMobileMenu);
|
||||
}
|
||||
|
||||
window.addEventListener('resize', function() {
|
||||
if (window.innerWidth > 768 && sidebar.classList.contains('mobile-open')) {
|
||||
closeMobileMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// 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 = '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 === 'block') {
|
||||
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('.api-selection-card')).filter(card => card.style.display !== 'none');
|
||||
|
||||
visibleCards.forEach(function(card) {
|
||||
const checkbox = card.querySelector('.api-checkbox');
|
||||
if (checkbox) {
|
||||
checkbox.checked = isChecked;
|
||||
updateCardSelection(checkbox);
|
||||
}
|
||||
});
|
||||
|
||||
updateSelectedCount();
|
||||
});
|
||||
}
|
||||
|
||||
// DOM에 렌더되지 않은 선택분을 hidden input으로 주입 — 전송 유실 방지 (패치 3)
|
||||
function syncHiddenSelected() {
|
||||
document.querySelectorAll('input.hidden-selected-api').forEach(el => el.remove());
|
||||
selectedApis.forEach(function(apiId) {
|
||||
if (!document.querySelector('.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 validation
|
||||
form.addEventListener('submit', function(e) {
|
||||
if (selectedApis.size === 0) {
|
||||
e.preventDefault();
|
||||
if (window.customPopups && customPopups.showAlert) {
|
||||
customPopups.showAlert('최소 1개 이상의 API를 선택해주세요.');
|
||||
} else {
|
||||
alert('최소 1개 이상의 API를 선택해주세요.');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
syncHiddenSelected();
|
||||
});
|
||||
|
||||
// Initialize
|
||||
updateSelectedCount();
|
||||
loadApis('');
|
||||
});
|
||||
Reference in New Issue
Block a user