feat: 마이페이지 퍼블리싱

This commit is contained in:
hong
2026-07-14 10:34:31 +09:00
parent 045f354e5c
commit 28a9ff1a7f
23 changed files with 10500 additions and 3829 deletions
@@ -6,201 +6,201 @@
<body>
<section layout:fragment="title">
<div class="page-title-banner">
<img th:src="@{/img/img_title_bg.png}" class="title-image">
<img th:src="@{/img/img_title_bg.png}" class="title-image">
<h1>앱관리</h1>
</div>
</section>
<th:block layout:fragment="contentFragment">
<section class="apikey-register-container">
<div class="signup-guide-v2 figma-register-wrapper">
<div class="service-main app-management-layout">
<!-- Title Bar -->
<div class="common-title-bar">
<h2 class="common-title">앱 수정</h2>
<span class="common-subtitle">서비스 중인 앱 정보를 수정합니다.</span>
</div>
<!-- Left Sidebar (Service Sidebar) -->
<th:block th:replace="~{fragment/djbank/service_sidebar :: sidebar('apiKey')}"></th:block>
<!-- Progress Indicator -->
<div class="register-progress">
<div class="progress-steps">
<!-- Step 1: 앱 정보입력 (Completed) -->
<div class="progress-step-item">
<div class="step-icon-wrapper">
<div class="step-icon">
<img th:src="@{/img/apikey_step1.png}" alt="앱 정보입력" width="36" height="36">
<!-- Content Area -->
<div class="app-management-content">
<div class="step2-wrap">
<!-- Title -->
<h2 class="s2-title">앱 수정</h2>
<!-- Progress Steps Card -->
<div class="s1-progress-card">
<div class="s1-steps">
<!-- Step 1: 앱 정보입력 (completed) -->
<div class="s1-step">
<div class="s1-step-circle">
<!-- 입력폼 아이콘 -->
<svg class="s1-step-svg" width="26" height="26" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<rect x="3" y="5" width="18" height="14" rx="2" stroke="currentColor" stroke-width="1.8" />
<line x1="7" y1="9" x2="17" y2="9" stroke="currentColor" stroke-width="1.8"
stroke-linecap="round" />
<line x1="7" y1="12" x2="14" y2="12" stroke="currentColor" stroke-width="1.8"
stroke-linecap="round" />
<line x1="7" y1="15" x2="11" y2="15" stroke="currentColor" stroke-width="1.8"
stroke-linecap="round" />
</svg>
</div>
<span class="s1-step-num">1단계</span>
<span class="s1-step-name">앱 정보수정</span>
</div>
<div class="s1-step-line"></div>
<!-- Step 2 Active: API 선택 (chain link icon) -->
<div class="s1-step s1-step--active">
<div class="s1-step-circle">
<!-- 체인링크/API 아이콘 -->
<svg class="s1-step-svg" width="26" height="26" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" stroke="currentColor"
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" stroke="currentColor"
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</div>
<span class="s1-step-num">2단계</span>
<span class="s1-step-name">API 선택</span>
</div>
<div class="s1-step-line"></div>
<!-- Step 3: 앱 생성 완료 (rocket icon) -->
<div class="s1-step">
<div class="s1-step-circle">
<!-- 로켓 아이콘 -->
<svg class="s1-step-svg" width="26" height="26" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"
stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
<path
d="M12 15l-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"
stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
<path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" stroke="currentColor" stroke-width="1.6"
stroke-linecap="round" stroke-linejoin="round" />
<path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" stroke="currentColor" stroke-width="1.6"
stroke-linecap="round" stroke-linejoin="round" />
</svg>
</div>
<span class="s1-step-num">3단계</span>
<span class="s1-step-name">앱 수정 완료</span>
</div>
</div>
</div>
<div class="step-label">앱 정보입력</div>
</div>
<!-- Dots between Step 1 and 2 -->
<div class="step-dots">
<span></span><span></span><span></span>
</div>
<!-- Step 2: API 선택 (Active) -->
<div class="progress-step-item active">
<div class="step-icon-wrapper">
<div class="step-icon">
<img th:src="@{/img/apikey_step2.png}" alt="API 선택" width="36" height="36">
<!-- 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>
<div class="step-label">API 선택</div>
</div>
<!-- Dots between Step 2 and 3 -->
<div class="step-dots">
<span></span><span></span><span></span>
</div>
<!-- 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">
<!-- Step 3: 앱 수정 완료 -->
<div class="progress-step-item">
<div class="step-icon-wrapper">
<div class="step-icon">
<img th:src="@{/img/apikey_step3.png}" alt="앱 수정 완료" width="36" height="36">
</div>
<!-- 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>
<div class="step-label">앱 수정 완료</div>
</div>
<!-- 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>
</div>
</div><!-- /step2-wrap -->
</div>
</div>
</div>
<!-- Error Message -->
<div th:if="${error}" class="alert alert-error">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
<span th:text="${error}"></span>
</div>
<!-- Form Content with Sidebar Layout -->
<div class="register-form-container with-sidebar">
<!-- Sidebar Navigation -->
<aside class="apikey-register-sidebar" id="apiSidebar">
<nav class="apikey-sidebar-nav">
<!-- Sidebar Title -->
<div class="sidebar-title">API 서비스 목록</div>
<!-- All APIs -->
<div class="menu-section">
<div class="menu-title active" data-group="">
<span class="menu-text">전체</span>
<span class="api-count"></span>
</div>
</div>
<!-- Service Categories -->
<div class="menu-section" th:each="service : ${apiServices}">
<div class="menu-title"
th:data-group="${service.id}">
<span class="menu-text" th:text="${service.groupName}">서비스명</span>
<span class="api-count"></span>
</div>
</div>
</nav>
</aside>
<!-- Mobile Menu Toggle -->
<button class="apikey-mobile-toggle" id="mobileToggle" aria-label="메뉴 열기">
</button>
<!-- Mobile Overlay -->
<div class="apikey-mobile-overlay" id="mobileOverlay"></div>
<!-- Main Content -->
<main class="apikey-register-content">
<form id="modifyStep2Form" method="post" th:action="@{/myapikey/modify/step2}" th:object="${apiKeyModification}" class="register-form">
<!-- Hidden field for clientId -->
<input type="hidden" th:field="*{clientId}"/>
<!-- Search Box with Select All -->
<div class="api-filter-header">
<div class="select-all-wrapper" id="selectAllWrapper" style="display: none;">
<label class="select-all-label">
<input type="checkbox" id="selectAllCheckbox" class="select-all-checkbox visually-hidden">
<span class="custom-checkbox"></span>
<span id="selectAllText">전체 선택</span>
</label>
</div>
<div class="api-search-box">
<input type="text"
id="apiSearch"
class="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="api-selection-card-grid" id="apiCardGrid">
<!-- Loading state -->
<div class="loading-state" id="loadingState" style="grid-column: 1 / -1;">
<div class="loading-spinner"></div>
<p>API 목록을 불러오는 중...</p>
</div>
<!-- Empty state (initially hidden) -->
<div class="empty-api-state" id="emptyState" style="display: none; grid-column: 1 / -1;">
<div class="empty-icon">📦</div>
<h3>API를 선택해주세요</h3>
<p>왼쪽 메뉴에서 서비스를 선택하면 해당 API 목록이 표시됩니다.</p>
</div>
<!-- API cards will be dynamically loaded here -->
</div>
</form>
</main>
</div>
<!-- Form Actions -->
<div class="form-actions">
<button type="button" id="btnPrevStep" class="btn-submit btn-secondary">
이전
</button>
<button type="submit" form="modifyStep2Form" class="btn-submit btn-primary">
저장
</button>
</div>
</section>
<!-- Floating Cart Button -->
<button type="button" class="floating-cart-btn" id="floatingCartBtn" style="display: none;">
<span class="cart-label">선택된 API</span>
<span class="cart-badge" id="cartBadge">
<span class="cart-count">0</span>
<span class="cart-unit"></span>
<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 - Uses base modal component -->
<div class="modal" id="selectedApisModal" style="display: none;">
<div class="modal-backdrop" id="modalOverlay"></div>
<div class="modal-dialog">
<div class="modal-header">
<h3 class="modal-title">선택된 API 목록</h3>
<button type="button" class="modal-close" id="modalCloseBtn">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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>
<!-- 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="modal-body">
<div class="selected-apis-list" id="modalSelectedList">
<div class="s2-modal-body">
<div class="s2-selected-list" id="modalSelectedList">
<!-- Dynamically populated -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" id="modalCancelBtn">닫기</button>
<div class="s2-modal-footer">
<button type="button" class="s2-btn-close-modal" id="modalCancelBtn">닫기</button>
</div>
</div>
</div>
@@ -212,10 +212,7 @@
// DOM Elements
const form = document.getElementById('modifyStep2Form');
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');
@@ -226,7 +223,7 @@
let allLoadedApis = []; // Store ALL APIs from server for modal display
let selectedApis = new Set(); // Track selected API IDs
// Restore selected APIs from session (for modification)
// Restore selected APIs from session
const sessionSelectedApis = /*[[${apiKeyModification.selectedApis}]]*/ [];
if (sessionSelectedApis && Array.isArray(sessionSelectedApis)) {
sessionSelectedApis.forEach(function(apiId) {
@@ -236,11 +233,11 @@
// Load all APIs for modal display (used when APIs from different categories are selected)
function loadAllApisForModal() {
fetch('/apis/for_request')
const baseUrl = /*[[@{/apis/for_request}]]*/ '/apis/for_request';
fetch(baseUrl)
.then(response => response.json())
.then(apis => {
allLoadedApis = apis;
console.log('Loaded', allLoadedApis.length, 'APIs for modal display');
})
.catch(error => {
console.error('Failed to load all APIs for modal:', error);
@@ -254,10 +251,11 @@
emptyState.style.display = 'none';
// Remove existing API cards
document.querySelectorAll('.api-selection-card').forEach(card => card.remove());
document.querySelectorAll('.s2-api-card').forEach(card => card.remove());
// Build URL with optional groupId filter
let url = '/apis/for_request';
// 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);
}
@@ -268,9 +266,11 @@
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 => {
@@ -279,6 +279,7 @@
emptyState.querySelector('h3').textContent = 'API 로드 실패';
emptyState.querySelector('p').textContent = '다시 시도해주세요.';
emptyState.style.display = 'block';
document.getElementById('apiResultCount').textContent = '0';
});
}
@@ -303,7 +304,7 @@
// Create API card element
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());
@@ -319,26 +320,25 @@
: `<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 || '카테고리'}</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>
`;
@@ -347,23 +347,11 @@
// 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) {
// Prevent double toggle if clicking directly on checkbox
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);
@@ -371,11 +359,28 @@
}
});
});
// 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('.api-selection-card');
const card = checkbox.closest('.s2-api-card');
if (checkbox.checked) {
card.classList.add('selected');
selectedApis.add(checkbox.value);
@@ -389,7 +394,7 @@
function updateSelectedCount() {
// Update floating cart button
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';
@@ -425,7 +430,7 @@
// 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;
@@ -433,7 +438,7 @@
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) {
@@ -454,21 +459,21 @@
modalSelectedList.innerHTML = '';
if (selectedApis.size === 0) {
modalSelectedList.innerHTML = '<p class="empty-message">선택된 API가 없습니다.</p>';
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('.api-checkbox[value="' + apiId + '"]');
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('.api-selection-card');
const card = checkbox.closest('.s2-api-card');
if (card) {
const nameElement = card.querySelector('.api-name');
const nameElement = card.querySelector('.s2-api-card-title');
if (nameElement) {
apiName = nameElement.textContent;
}
@@ -477,7 +482,7 @@
// 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.length > 0) {
if (!api && allLoadedApis && allLoadedApis.length > 0) {
api = allLoadedApis.find(a => a.apiId === apiId);
}
if (api && api.apiName) {
@@ -486,24 +491,19 @@
}
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);
});
// Add remove handlers
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');
const checkbox = document.querySelector('.api-checkbox[value="' + value + '"]');
const checkbox = document.querySelector('.s2-api-checkbox[value="' + value + '"]');
if (checkbox) {
checkbox.checked = false;
updateCardSelection(checkbox);
@@ -523,7 +523,8 @@
searchInput.addEventListener('input', function() {
const searchTerm = this.value.toLowerCase();
const apiCards = document.querySelectorAll('.api-selection-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');
@@ -533,17 +534,20 @@
if (matchesSearch) {
card.style.display = '';
visibleCount++;
} else {
card.style.display = 'none';
}
});
document.getElementById('apiResultCount').textContent = visibleCount;
// Update select all checkbox state after search
updateSelectAllCheckboxState();
});
}
// Sidebar category selection
// Category tab selection
menuTitles.forEach(function(title) {
title.addEventListener('click', function(e) {
e.preventDefault();
@@ -557,7 +561,7 @@
currentFilter = groupId;
// Store service name for select all label
currentServiceName = this.textContent.trim().split('\n')[0].trim();
currentServiceName = this.textContent.trim();
// Load APIs for selected service
loadApis(groupId);
@@ -566,51 +570,40 @@
if (searchInput) {
searchInput.value = '';
}
// Close mobile menu if open
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' });
});
// Close menu on overlay click
mobileOverlay.addEventListener('click', function() {
closeMobileMenu();
btnNextCategory.addEventListener('click', function() {
categoryListWrapper.scrollBy({ left: scrollAmount, behavior: 'smooth' });
});
}
// Close mobile menu on resize to desktop
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');
@@ -680,10 +673,10 @@
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);
@@ -700,9 +693,8 @@
btnPrevStep.addEventListener('click', function(e) {
e.preventDefault();
// Change form action to save endpoint
const originalAction = form.action;
form.action = '/myapikey/modify/step2/save';
// 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();
@@ -711,7 +703,6 @@
// Form validation
form.addEventListener('submit', function(e) {
// API 선택 필수 검증
if (selectedApis.size === 0) {
e.preventDefault();
customPopups.showAlert('최소 1개 이상의 API를 선택해주세요.');
@@ -720,18 +711,12 @@
});
// Initialize
// Load all APIs for modal display FIRST (async)
loadAllApisForModal();
updateSelectedCount(); // This will show count from restored session data
// Load all APIs automatically on page load (empty string = all APIs)
loadApis('');
// Log restored selections for debugging
if (selectedApis.size > 0) {
console.log('Restored ' + selectedApis.size + ' selected APIs from session:', Array.from(selectedApis));
}
});
</script>
</th:block>