feat: 마이페이지 퍼블리싱
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user