Merge remote-tracking branch 'origin/devs/ums' into main-design-layout

# Conflicts:
#	src/main/resources/templates/views/apps/login/login.html
This commit is contained in:
현성필
2025-11-14 11:13:46 +09:00
46 changed files with 2258 additions and 426 deletions
@@ -1,372 +1,476 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_index_layout}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kbank_index_layout}">
<body>
<th:block layout:fragment="contentFragment">
<section class="hero-section">
<div class="hero-patterns">
<div class="pattern-circle pattern-1"></div>
<div class="pattern-circle pattern-2"></div>
<div class="pattern-dots"></div>
</div>
<div class="container">
<div class="hero-content">
<div class="hero-badge">
<span class="badge-icon">🚀</span>
<span>Wa bank 생태계에 오신 것을 환영합니다!</span>
<section layout:fragment="contentFragment" class="content main">
<script th:src="@{/js/htmx.min.js}"></script>
<!-- main slick -->
<div class="slider-container">
<div class="slider m-only">
<div class="slider_box">
<img th:src="@{/img/img_mainslick_m.png}" alt="슬라이드 1">
<div class="slick_txt">
<p>디지털 금융 리더 <span>케이뱅크</span><br/>새로운 비즈니스 기회를<br/>제공합니다.</p>
</div>
<h1 class="main-headline">
<span class="headline-wa">Wa한</span> 금융 혁신,<br>
여러분의 아이디어로 시작됩니다
</h1>
<h2 class="sub-headline">
광주은행 오픈 API로 새로운 금융 서비스를 만들어보세요.<br>
안전하고 빠르게, 그리고 쉽게 연동할 수 있습니다.
</h2>
<div class="hero-buttons">
<a href="/apis" class="btn btn-primary">
<i class="fas fa-code"></i> API 둘러보기
</a>
<a href="#get-started" class="btn btn-secondary">
<i class="fas fa-play"></i> 5분 만에 시작하기
</a>
</div>
<div class="slider_box">
<img th:src="@{/img/img_mainslick2_m.png}" alt="슬라이드 2">
<div class="slick_txt">
<p>차별화된 <span>API와<br> 편리한 테스트 환경</span><br> 지원합니다</p>
</div>
</div>
<div class="slider_box">
<img th:src="@{/img/img_mainslick3_m.png}" alt="슬라이드 3">
<div class="slick_txt">
<p><span>디지털 금융 혁신</span><br> 케이뱅크가<br> 함께합니다</p>
</div>
</div>
</div>
</section>
<!-- API 검색 섹션 -->
<section class="api-search-section">
<div class="container">
<div class="search-container">
<form th:action="@{/apis}" method="get" class="search-form">
<div class="search-wrapper">
<input type="text" class="search-input" placeholder="원하는 API를 검색해보세요" id="apiSearchInput" name="keyword">
<button class="search-button" type="submit">
<i class="fas fa-search"></i>
<span>검색하기</span>
</button>
</div>
</form>
<div class="hashtag-suggestions" th:if="${hashtags != null and not #lists.isEmpty(hashtags)}">
<span class="hashtag-label">추천 검색어:</span>
<a href="#" class="hashtag" th:each="tag : ${hashtags}"
th:data-search="${tag}"
th:text="${'#' + tag}">
</a>
<div class="slider pc-only">
<div class="slider_box">
<img th:src="@{/img/img_mainslick_pc.png}" alt="슬라이드 1">
<div class="slick_txt">
<p><span>케이뱅크</span>가 새로운 비즈니스<br> 기회를 제공합니다</p>
</div>
<!-- Fallback hashtags if no hashtags from controller -->
<div class="hashtag-suggestions" th:unless="${hashtags != null and not #lists.isEmpty(hashtags)}">
<span class="hashtag-label">추천 검색어:</span>
<a href="#" class="hashtag" data-search="계좌조회">#계좌조회</a>
<a href="#" class="hashtag" data-search="송금API">#송금API</a>
<a href="#" class="hashtag" data-search="실시간결제">#실시간결제</a>
<a href="#" class="hashtag" data-search="오픈뱅킹">#오픈뱅킹</a>
</div>
<div class="slider_box">
<img th:src="@{/img/img_mainslick2_pc.png}" alt="슬라이드 2">
<div class="slick_txt">
<p>차별화된 <span>API</span><br/>
<span>편리한 테스트 환경</span><br/>지원합니다</p>
</div>
</div>
<div class="slider_box">
<img th:src="@{/img/img_mainslick3_pc.png}" alt="슬라이드 3">
<div class="slick_txt">
<p><span>디지털 금융 혁신</span><br>케이뱅크가<br>함께합니다</p>
</div>
</div>
</div>
</section>
<button class="slick-prev" aria-label="Previous" type="button">이전</button>
<button class="slick-next" aria-label="Next" type="button">다음</button>
<ul class="slick-dots">
<li>
<button type="button">1</button>
</li>
<li>
<button type="button">2</button>
</li>
<li>
<button type="button">3</button>
</li>
</ul>
</div>
<!-- // main slick -->
<section class="api-showcase">
<div class="container">
<div class="section-header">
<h2 class="section-title">
<span class="title-highlight">인기 API</span>로 바로 시작하세요
</h2>
<p class="section-subtitle">가장 많이 사용되는 API를 확인하고 바로 테스트해보세요</p>
<div class="main-container">
<div class="search_type">
<form th:action="@{/apis}" id="searchForm" method="get">
<div class="search-container">
<input type="text" name="keyword" class="search-box" id="search-box" placeholder="어떤 API를 찾고 계신가요?">
<button class="search-button" type="submit"></button>
</div>
</form>
</div>
<div class="hashtag">
<ul>
<li th:each="tag: ${hashtags}"><a href="#" th:text="${tag}">#인증</a></li>
</ul>
</div>
<div class="main_keyword">
<div class="api_tit">
<h2>가장 많이 찾는 API</h2>
<p class="total_view">
<a href="/apis">전체보기</a>
</p>
</div>
<div class="api-grid">
<!-- Dynamic API Cards from Controller (max 4) -->
<div th:each="api, iterStat : ${apis}"
th:if="${iterStat.index < 4}"
class="api-card"
th:classappend="${iterStat.index == 0} ? 'api-card-popular' : (${iterStat.index == 3} ? 'api-card-new' : '')">
<!-- Show badge for first (인기) and last (NEW) items -->
<div th:if="${iterStat.index == 0}" class="api-badge">인기</div>
<div th:if="${iterStat.index == 3}" class="api-badge new">NEW</div>
<!-- API Icon - using different icons based on index or API type -->
<div class="api-icon">
<!-- Icon selection based on API name or service type -->
<i th:if="${api.apiName != null}"
th:class="${#strings.containsIgnoreCase(api.apiName, '계좌') or #strings.containsIgnoreCase(api.apiName, '조회')} ? 'fas fa-wallet' :
(${#strings.containsIgnoreCase(api.apiName, '송금') or #strings.containsIgnoreCase(api.apiName, '이체')} ? 'fas fa-exchange-alt' :
(${#strings.containsIgnoreCase(api.apiName, '카드') or #strings.containsIgnoreCase(api.apiName, '결제')} ? 'fas fa-credit-card' :
(${#strings.containsIgnoreCase(api.apiName, 'AI') or #strings.containsIgnoreCase(api.apiName, '분석')} ? 'fas fa-chart-line' :
(${iterStat.index == 0} ? 'fas fa-wallet' :
(${iterStat.index == 1} ? 'fas fa-exchange-alt' :
(${iterStat.index == 2} ? 'fas fa-credit-card' : 'fas fa-chart-line'))))))"></i>
</div>
<!-- API Name -->
<h3 class="api-name" th:text="${api.apiName}">API 이름</h3>
<!-- API Description -->
<p class="api-description" th:text="${api.apiSimpleDescription != null ? api.apiSimpleDescription : api.description}">API 설명</p>
<!-- API Features/Method Info -->
<div class="api-features">
<span class="feature-tag" th:if="${api.apiMethod != null}" th:text="${api.apiMethod}">GET</span>
<span class="feature-tag" th:if="${api.service != null}" th:text="${api.service}">서비스</span>
</div>
<!-- Test Link -->
<a th:href="@{/apis/detail(id=${api.apiId})}" class="api-link">
테스트하기 <i class="fas fa-arrow-right"></i>
</a>
<div class="api_keyword">
<ul class="keyword_list tab_list">
<li>
<a href="#"
hx-get="/api_fragments"
hx-target="#api_spec_list"
hx-trigger="click"
onclick="setActive(this)">전체<span class="number" th:text="${totalApiCount}"></span></a>
</li>
<li th:each="item : ${services}">
<a href="#"
th:hx-get="@{/api_fragments(id=${item.id})}"
hx-target="#api_spec_list"
hx-trigger="click"
onclick="setActive(this)">
[[${item.groupName}]]<span class="number" th:text="${item.apiGroupApiList.size()}"></span>
</a>
</li>
</ul>
<div class="more_type m-only">
<button class="showmore" type="button">
<img th:src="@{/img/icon/icon_more_btn.png}" alt="더보기 버튼">
</button>
</div>
<div class="more_type pc-only">
<button class="showmore" type="button">
<img th:src="@{/img/icon/icon_more_btn2.png}" alt="더보기 버튼">
</button>
</div>
</div>
<!-- Fallback: Show static cards if no APIs from controller -->
<div th:if="${apis == null or #lists.isEmpty(apis)}" class="api-card api-card-popular">
<div class="api-badge">인기</div>
<div class="api-icon">
<i class="fas fa-wallet"></i>
<div class="api_info_container box_list" id="api_spec_list">
<div class="api_info_box" th:each="api : ${apis}" th:data-href="@{/apis/detail(id=${api.apiId})}">
<div class="api_list" th:data-href="@{/apis/detail(id=${api.apiId})}">
<p class="txt1" th:text="${api.apiName}"></p>
<p class="txt2" th:text="${api.apiSimpleDescription}"></p>
</div>
<h3 class="api-name">계좌 조회 API</h3>
<p class="api-description">실시간 잔액 조회와 거래내역을 안전하게 확인</p>
<div class="api-features">
<span class="feature-tag">실시간</span>
<span class="feature-tag">OAuth 2.0</span>
<div class="api_img" style="height: 48px;">
<a th:href="@{/apis/detail(id=${api.apiId})}" class="btn btn-primary">
<img th:if="${#strings.isEmpty(api.mainIcon) == false}" th:src="${api.mainIcon}" alt="">
</a>
</div>
<a href="#" class="api-link">
테스트하기 <i class="fas fa-arrow-right"></i>
</a>
</div>
<div th:if="${apis == null or #lists.isEmpty(apis)}" class="api-card">
<div class="api-icon">
<i class="fas fa-exchange-alt"></i>
</div>
<h3 class="api-name">송금 API</h3>
<p class="api-description">간편하고 안전한 송금 서비스 구현</p>
<div class="api-features">
<span class="feature-tag">간편송금</span>
<span class="feature-tag">실명확인</span>
</div>
<a href="#" class="api-link">
테스트하기 <i class="fas fa-arrow-right"></i>
</a>
</div>
<div th:if="${apis == null or #lists.isEmpty(apis)}" class="api-card">
<div class="api-icon">
<i class="fas fa-credit-card"></i>
</div>
<h3 class="api-name">카드 결제 API</h3>
<p class="api-description">온/오프라인 결제 솔루션 통합</p>
<div class="api-features">
<span class="feature-tag">PG연동</span>
<span class="feature-tag">간편결제</span>
</div>
<a href="#" class="api-link">
테스트하기 <i class="fas fa-arrow-right"></i>
</a>
</div>
<div th:if="${apis == null or #lists.isEmpty(apis)}" class="api-card api-card-new">
<div class="api-badge new">NEW</div>
<div class="api-icon">
<i class="fas fa-chart-line"></i>
</div>
<h3 class="api-name">AI 신용평가 API</h3>
<p class="api-description">머신러닝 기반 실시간 신용 분석</p>
<div class="api-features">
<span class="feature-tag">AI/ML</span>
<span class="feature-tag">실시간</span>
</div>
<a href="#" class="api-link">
테스트하기 <i class="fas fa-arrow-right"></i>
</a>
</div>
</div>
</div>
</section>
<!-- 정보 섹션 -->
<section class="info-section">
<div class="container">
<div class="info-wrapper">
<div class="image-container">
<div class="image-placeholder">
<i class="fas fa-chart-line"></i>
<span>금융 혁신</span>
</div>
</div>
<div class="main_con">
<p class="con_tit m-only"><img th:src="@{/img/img_con_tit.png}" alt="Kbank API PORTAL"></p>
<p class="con_tit pc-only"><img th:src="@{/img/img_con_tit2.png}" alt="Kbank API PORTAL"></p>
<p class="con_img m-only"><img th:src="@{/img/img_mask_group.png}" alt="Kbank API PORTAL"></p>
<p class="con_img pc-only"><img th:src="@{/img/img_mask_group2.png}" alt="Kbank API PORTAL"></p>
<p class="con_txt m-only">
<span>Kbank API Portal</span>은 기업이 금융서비스를<br>
편리하게 개발할 수 있도록 차별화된<br>
API와 테스트환경을 제공합니다.<br>
</p>
<p class="con_txt pc-only">
<span>Kbank API Portal</span>은 기업이 금융서비스를 편리하게 개발할 수 있도록 차별화된 API와 테스트환경을 제공합니다.
</p>
<div class="main_btn">
<a th:href="@{/intro}" class="common_btn_type_5"><span>처음 만나는 케이뱅크 API</span></a>
<a th:href="@{/partnership}" class="common_btn_type_5 m_btn"><span>사업 제휴 문의</span></a>
</div>
</div>
</div>
<div class="info-content">
<p class="highlight-text">
광주은행 <span class="text-accent">API Portal</span>은 기업이 금융서비스를 편리하게 개발할 수 있도록<br>
차별화된 API와 테스트환경을 제공합니다.
<div class="tiding_box">
<div class="api_type">
<div class="api_tiding">
<div class="api_tit">
<h2>새로운 케이뱅크 API 소식을<br> 만나보세요</h2>
<p class="total_view">
<a href="/portalnotice">전체보기</a>
</p>
<div class="action-buttons">
<a href="#" class="action-btn">
<span class="btn-number">1</span>
<span class="btn-text">처음 만나는 광주뱅크 API</span>
<i class="fas fa-arrow-right"></i>
</div>
<div class="api_tiding_list">
<ul>
<li th:each="notice : ${portalNotices}">
<a th:href="@{/portalnotice/detail(id=${notice.id})}">
<p th:text="${notice.noticeSubject}"></p>
</a>
<span th:text="${#temporals.format(notice.createdDate, 'yyyy.MM.dd')}"></span>
</li>
</ul>
</div>
</div>
<div class="api_info">
<ul>
<li class="img1">
<a th:href="@{/faq_list}">
<p>FAQ</p>
<span>자주 묻는 질문과 답변을<br> 확인해보세요</span>
<img th:src="@{/img/icon/icon_petals01.png}" alt="faq 이미지">
</a>
<a href="#" class="action-btn">
<span class="btn-number">2</span>
<span class="btn-text">사업 제휴 문의</span>
<i class="fas fa-arrow-right"></i>
</li>
<li class="img2">
<a th:href="@{/inquiry}">
<p>Q&amp;A</p>
<span>문의사항에<br> 친절하게 답변 드리겠습니다</span>
<img th:src="@{/img/icon/icon_petals02.png}" alt="Q&A 이미지">
</a>
</div>
</div>
</li>
<li class="img3">
<a th:href="@{/partnership}">
<p>사업제휴</p>
<span>온라인 비즈니스 혁신을 위한<br> 사업 제안을 환영합니다</span>
<img th:src="@{/img/icon/icon_petals03.png}" alt="사업제휴 이미지">
</a>
</li>
</ul>
</div>
</div>
</section>
</div>
<!-- 지원 센터 섹션 -->
<section class="support-center">
<div class="container">
<div class="section-header">
<h2 class="section-title">
도움이 <span class="title-highlight">필요하신가요?</span>
</h2>
<p class="section-subtitle">다양한 방법으로 필요한 정보를 찾아보세요</p>
</div>
<div class="support-grid">
<a th:href="@{/portalnotice}" class="support-card">
<div class="card-icon">
<i class="fas fa-bullhorn"></i>
</div>
<h3>공지사항</h3>
<p>최신 업데이트와 중요한 소식을 확인하세요</p>
<div class="card-arrow">
<i class="fas fa-arrow-right"></i>
</div>
</a>
<a th:href="@{/faq_list}" class="support-card">
<div class="card-icon">
<i class="fas fa-question-circle"></i>
</div>
<h3>자주 묻는 질문</h3>
<p>자주 묻는 질문과 답변을 빠르게 찾아보세요</p>
<div class="card-arrow">
<i class="fas fa-arrow-right"></i>
</div>
</a>
<a th:href="@{/inquiry}" class="support-card">
<div class="card-icon">
<i class="fas fa-comments"></i>
</div>
<h3>문의하기</h3>
<p>추가 도움이 필요하시면 직접 문의해주세요</p>
<div class="card-arrow">
<i class="fas fa-arrow-right"></i>
</div>
</a>
<div class="api_count">
<div class="api_total_box">
<p class="txt m-only">우리곁의 수많은 서비스들이<br> <em>케이뱅크API</em><br> 함께하고 있습니다</p>
<p class="txt pc-only">우리곁의 수많은 서비스들이<br> <em>케이뱅크API와 함께</em>하고 있습니다</p>
<div class="api_total">
<ul>
<li>
<img th:src="@{/img/icon/icon_api01.png}" alt="서비스 이용 수">
<div class="api_num">
<p th:text="${serviceCount}"></p>
<span>서비스 이용</span>
</div>
</li>
<li>
<img th:src="@{/img/icon/icon_api02.png}" alt="API 이용건수">
<div class="api_num">
<p th:text="${selectedApiCount}"></p>
<span>API 이용</span>
</div>
</li>
<li>
<img th:src="@{/img/icon/icon_api03.png}" alt="API 활용기업">
<div class="api_num">
<p th:text="${approvedOrgCount}"></p>
<span>API 활용 기업</span>
</div>
</li>
</ul>
</div>
</div>
</section>
</div>
<!-- API 활용 현황 섹션 -->
<section class="api-stats-section">
<div class="container">
<div class="section-header">
<h2 class="section-title">
우리곁의 수많은 서비스들이<br>
<span class="title-highlight">광주은행 API</span>와 함께하고 있습니다
</h2>
</div>
<div class="stats-cards">
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-store"></i>
</div>
<div class="stat-content">
<div class="stat-number" th:attr="data-target=${serviceCount != null ? serviceCount : 0}" th:text="${serviceCount != null ? serviceCount : 0}">0</div>
<div class="stat-label">서비스 이용 수</div>
<div class="stat-description">다양한 분야의 서비스가 활용중</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-exchange-alt"></i>
</div>
<div class="stat-content">
<div class="stat-number" th:attr="data-target=${totalApiCount != null ? totalApiCount : 0}" th:text="${totalApiCount != null ? totalApiCount : 0}">0</div>
<div class="stat-label">API 이용 건수</div>
<div class="stat-description">일일 평균 API 호출 수</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-building"></i>
</div>
<div class="stat-content">
<div class="stat-number" th:attr="data-target=${approvedOrgCount != null ? approvedOrgCount : 0}" th:text="${approvedOrgCount != null ? approvedOrgCount : 0}">0</div>
<div class="stat-label">API 활용 기업</div>
<div class="stat-description">혁신적인 파트너사와 함께</div>
</div>
</div>
</div>
</div>
</section>
</th:block>
<div id="main_bg"></div>
</section>
</body>
<th:block layout:fragment="contentScript">
<script>
// 숫자 카운트업 애니메이션
document.addEventListener('DOMContentLoaded', function() {
const observerOptions = {
threshold: 0.5,
rootMargin: '0px 0px -100px 0px'
};
const animateNumbers = (entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const numbers = entry.target.querySelectorAll('.stat-number');
numbers.forEach(number => {
const target = parseInt(number.getAttribute('data-target'));
const duration = 2000; // 2초
const increment = target / (duration / 16);
let current = 0;
const updateNumber = () => {
current += increment;
if (current < target) {
number.textContent = Math.floor(current).toLocaleString('ko-KR');
requestAnimationFrame(updateNumber);
} else {
number.textContent = target.toLocaleString('ko-KR');
}
};
updateNumber();
});
observer.unobserve(entry.target);
}
});
};
const observer = new IntersectionObserver(animateNumbers, observerOptions);
const statsSection = document.querySelector('.api-stats-section');
if (statsSection) {
observer.observe(statsSection);
<script th:inline="javascript">
$(document).ready(function() {
// 기존 success 메시지 처리
var successMsg = [[${success}]];
console.log("Success message:", successMsg);
if (successMsg) {
console.log("Showing alert");
customPopups.showAlert(successMsg);
}
// API 검색 기능
const searchInput = document.getElementById('apiSearchInput');
const searchForm = document.querySelector('.search-form');
const hashtags = document.querySelectorAll('.hashtag');
// 이메일 인증 체크
const emailVerificationRequired = [[${session.emailVerificationRequired}]];
if (emailVerificationRequired) {
const emailVerifyMsg = /*[[${session.success}]]*/ '';
const redirectUrl = /*[[${session.redirectUrl}]]*/ '';
// 해시태그 클릭 이벤트
hashtags.forEach(tag => {
tag.addEventListener('click', function(e) {
e.preventDefault();
const searchTerm = this.getAttribute('data-search');
searchInput.value = searchTerm;
// 폼 제출
if (searchForm) {
searchForm.submit();
console.log('emailVerifyMsg:', emailVerifyMsg);
console.log('redirectUrl:', redirectUrl);
customPopups.showAlert(emailVerifyMsg);
$('#customAlertOkButton').off('click.custom').on('click.custom', function () {
customPopups.hideAlert('#customAlert');
window.location.href = redirectUrl;
});
return; // 다른 체크 중단
}
// 장기미사용 계정 체크
const dormantAccount = [[${session.dormantAccount}]];
if (dormantAccount) {
const dormantMsg = /*[[${session.success}]]*/ '';
const redirectUrl = /*[[${session.redirectUrl}]]*/ '';
console.log('dormantMsg:', dormantMsg);
console.log('redirectUrl:', redirectUrl);
customPopups.showAlert(dormantMsg);
$('#customAlertOkButton').off('click.custom').on('click.custom', function () {
customPopups.hideAlert('#customAlert');
window.location.href = redirectUrl;
});
return; // 다른 체크 중단
}
// 비밀번호 만료 체크 추가
const passwordExpired = [[${session.passwordExpired}]];
if (passwordExpired) {
const pwdExpiredMsg = /*[[${session.success}]]*/ '';
const formattedMsg = pwdExpiredMsg.replace(/\n/g, '<br>');
const redirectUrl = /*[[${session.redirectUrl}]]*/ '';
console.log('pwdExpiredMsg:', pwdExpiredMsg);
console.log('redirectUrl:', redirectUrl);
$('#customAlertMessage').html(formattedMsg);
$('#customAlert').css('display','flex');
$('#customAlertOkButton').off('click.custom').on('click.custom', function () {
customPopups.hideAlert('#customAlert');
window.location.href = redirectUrl;
});
}
});
</script>
<script>
function setActive(element) {
// Remove 'active' class from all links
document.querySelectorAll('.keyword_list a').forEach(el => {
el.classList.remove('active');
});
// Add 'active' class to clicked link
element.classList.add('active');
}
document.addEventListener('DOMContentLoaded', (event) => {
const ITEMS_PER_PAGE = 4;
let isExpanded = false;
// 더보기 버튼 초기화
function initMoreButton() {
const mobileMoreBtn = document.querySelector('.more_type.m-only .showmore');
const pcMoreBtn = document.querySelector('.more_type.pc-only .showmore');
const apiInfoBoxes = document.querySelectorAll('.api_info_box');
// 5개 미만이면 더보기 버튼 숨기기
const mobileMoreContainer = document.querySelector('.more_type.m-only');
const pcMoreContainer = document.querySelector('.more_type.pc-only');
console.log('더보기 버튼 초기화:', {
항목개수: apiInfoBoxes.length,
보여질상태: apiInfoBoxes.length >= 5 ? 'block' : 'none'
});
// 더보기 버튼 표시 여부 설정
if (apiInfoBoxes.length < 5) {
mobileMoreContainer.style.setProperty('display', 'none', 'important');
pcMoreContainer.style.setProperty('display', 'none', 'important');
} else {
mobileMoreContainer.style.setProperty('display', 'block', 'important');
pcMoreContainer.style.setProperty('display', 'block', 'important');
}
// 초기 상태 설정
updateApiListVisibility();
// 더보기 버튼 이벤트 리스너 설정
[mobileMoreBtn, pcMoreBtn].forEach(btn => {
if (btn) {
btn.removeEventListener('click', handleMoreClick);
btn.addEventListener('click', handleMoreClick);
}
});
}
function handleMoreClick(e) {
e.preventDefault();
console.log('더보기 버튼 클릭');
isExpanded = !isExpanded; // 상태 토글
// 버튼 이미지/텍스트 변경
const mobileMoreBtn = document.querySelector('.more_type.m-only .showmore img');
const pcMoreBtn = document.querySelector('.more_type.pc-only .showmore img');
if (mobileMoreBtn) {
mobileMoreBtn.alt = isExpanded ? "접기 버튼" : "더보기 버튼";
}
if (pcMoreBtn) {
pcMoreBtn.alt = isExpanded ? "접기 버튼" : "더보기 버튼";
}
updateApiListVisibility();
}
// API 리스트 표시 상태 업데이트
function updateApiListVisibility() {
const apiInfoBoxes = document.querySelectorAll('.api_info_box');
const endIndex = isExpanded ? apiInfoBoxes.length : ITEMS_PER_PAGE;
console.log('업데이트 - API 목록:', {
총개수: apiInfoBoxes.length,
표시상태: isExpanded ? '전체표시' : '일부표시',
표시할개수: endIndex
});
apiInfoBoxes.forEach((box, index) => {
box.style.display = index < endIndex ? 'block' : 'none';
});
}
// HTMX 로드 완료 이벤트 리스너
document.body.addEventListener('htmx:afterSwap', function(evt) {
if (evt.detail.target.id === 'api_spec_list') {
console.log('HTMX 컨텐츠 로드됨 - 페이지 초기화');
isExpanded = false;
initMoreButton();
}
});
document.body.addEventListener('htmx:afterSettle', function(evt) {
const apiListItems = document.querySelectorAll('.api_list');
apiListItems.forEach(function(item) {
item.removeEventListener('click',null);
item.addEventListener('click', function() {
const href = this.getAttribute('data-href');
if (href) {
window.location.href = href;
}
});
});
const apiItems = document.querySelectorAll('.api_info_box');
apiItems.forEach(function(item) {
item.removeEventListener('click',null);
item.addEventListener('click', function() {
const href = this.getAttribute('data-href');
if (href) {
window.location.href = href;
}
});
});
});
// 초기 실행
console.log('초기 실행');
initMoreButton();
// All 카테고리를 기본으로 active 설정
document.querySelector('.keyword_list li:first-child a').classList.add('active');
const apiListItems = document.querySelectorAll('.api_list');
apiListItems.forEach(function(item) {
item.addEventListener('click', function() {
const href = this.getAttribute('data-href');
if (href) {
window.location.href = href;
}
});
});
const apiItems = document.querySelectorAll('.api_info_box');
apiItems.forEach(function(item) {
item.removeEventListener('click',null);
item.addEventListener('click', function() {
const href = this.getAttribute('data-href');
if (href) {
window.location.href = href;
}
});
});
G
const hashtagItems = document.querySelectorAll('.hashtag ul li a');
const searchBox = document.getElementById('search-box');
hashtagItems.forEach(function(item) {
item.addEventListener('click', function(e) {
e.preventDefault();
// Remove the # symbol if present and set the text in search box
const searchText = this.textContent.replace('#', '').trim();
searchBox.value = searchText;
document.getElementById("searchForm").submit();
});
});
const infoItems = document.querySelectorAll('.api_info ul li');
infoItems.forEach(function (item) {
item.style.cursor = 'pointer';
item.addEventListener('click', function() {
const link = this.querySelector('a');
if (link) {
location.href = link.getAttribute('href');
}
});
});
});
</script>
</th:block>
@@ -0,0 +1,232 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kbank_base_layout}">
<body>
<section layout:fragment="contentFragment" class="content">
<div class="content_wrap">
<!-- 타이틀 -->
<div class="sub_title2" id="title-verification-email">
<h2 class="title add5">이메일 인증</h2>
</div>
<div class="inner i_cs10 h_inner2">
<div class="form_type">
<form id="verificationEmailForm" role="form" name="verificationEmailForm" method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<input type="hidden" id="userId" name="userId" th:value="${userId}">
<!-- 이메일 주소 표시 (readonly) -->
<div class="info1 p_top2">
<p class="title w_tit">
<span class="dot">이메일 주소</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp4">
<input type="email" id="email" name="email" class="common_input_type_1"
th:value="${email}" readonly style="background-color: #f5f5f5;">
</div>
</div>
<p class="info_txt" style="margin-top: 10px; color: #666;">
위 이메일 주소로 인증코드가 발송됩니다.
</p>
</div>
<!-- 인증코드 받기 버튼 -->
<div class="info1 pt28">
<div class="btn_check" style="width: 100%;">
<a class="common_btn_type_2 cbt btn_send_code" style="width: 100%;"><span>인증코드 받기</span></a>
</div>
</div>
<!-- 인증번호 입력 -->
<div class="info1 pt28 auth-code-container" id="authCodeContainer" style="display: none;">
<p class="title w_tit">
<span class="dot">인증코드 입력</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp5">
<div class="certify_box">
<input type="text" id="authCode" class="common_input_type_1 w_input"
placeholder="이메일로 받은 인증코드를 입력하세요" maxlength="6" required>
<span class="certify_time" id="certify_time">05:00</span>
</div>
</div>
<div class="btn_check">
<a class="common_btn_type_2 cbt btn_verify_code"><span>인증코드 확인</span></a>
</div>
</div>
</div>
<!-- 제출 버튼 -->
<div class="btn_application m_top">
<a type="button" class="common_btn_type_1 gray btn_cancel"><span>취소</span></a>
<!-- <div class="btn_gap"></div>-->
<!-- <button type="button" class="common_btn_type_1 btn_complete" disabled><span>인증 완료</span></button>-->
</div>
</form>
</div>
</div>
</div>
</section>
</body>
<th:block layout:fragment="contentScript">
<script th:inline="javascript">
$(document).ready(function() {
let timerInterval;
let resendTimerInterval;
let isVerified = false;
// CSRF 토큰 설정
const csrfHeaderName = /*[[${_csrf.headerName}]]*/ 'X-CSRF-TOKEN';
const csrfToken = /*[[${_csrf.token}]]*/ '';
// 서버 설정값
const resendLimitSeconds = /*[[${@environment.getProperty('portal.auth.resend_limit_seconds', '60')}]]*/ 60;
// 취소 버튼 클릭 시 메인 페이지로 이동
$('.btn_cancel').on('click', function() {
if (confirm('이메일 인증을 취소하고 메인 페이지로 이동하시겠습니까?')) {
location.href = '/';
}
});
// 타이머 시작 함수 (인증 코드 유효 시간)
function startTimer(duration) {
let timer = duration;
clearInterval(timerInterval);
timerInterval = setInterval(function () {
let minutes = parseInt(timer / 60, 10);
let seconds = parseInt(timer % 60, 10);
minutes = minutes < 10 ? "0" + minutes : minutes;
seconds = seconds < 10 ? "0" + seconds : seconds;
$('#certify_time').text(minutes + ":" + seconds);
if (--timer < 0) {
clearInterval(timerInterval);
$('#certify_time').text("00:00").css('color', 'red');
customPopups.showAlert("인증 시간이 만료되었습니다. 인증코드를 다시 받아주세요.");
}
}, 1000);
}
// 재발송 타이머 시작 함수
function startResendTimer() {
let resendTimer = resendLimitSeconds;
const $button = $('.btn_send_code');
clearInterval(resendTimerInterval);
$button.prop('disabled', true);
resendTimerInterval = setInterval(function () {
if (resendTimer > 0) {
$button.find('span').text('재발송 가능 (' + resendTimer + '초)');
resendTimer--;
} else {
clearInterval(resendTimerInterval);
$button.prop('disabled', false).find('span').text('인증코드 재발송');
}
}, 1000);
}
// 인증코드 받기 버튼 클릭
$('.btn_send_code').on('click', function(e) {
e.preventDefault();
const email = $('#email').val();
const $button = $(this);
$button.prop('disabled', true).find('span').text('발송 중...');
$.ajax({
url: '/mypage/send-verification-code',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ email: email }),
headers: {
[csrfHeaderName]: csrfToken
},
success: function(response) {
customPopups.showAlert('인증코드가 이메일로 발송되었습니다.');
$('#authCodeContainer').show();
startTimer(300); // 5분 타이머
startResendTimer(); // 재발송 타이머 시작 (1분)
},
error: function(xhr) {
const errorMsg = xhr.responseJSON?.error || '인증코드 발송에 실패했습니다.';
customPopups.showAlert(errorMsg);
$button.prop('disabled', false).find('span').text('인증코드 받기');
}
});
});
// 인증코드 확인 버튼 클릭
$('.btn_verify_code').on('click', function(e) {
e.preventDefault();
const authCode = $('#authCode').val().trim();
const email = $('#email').val();
if (!authCode) {
customPopups.showAlert('인증코드를 입력해주세요.');
return;
}
if (authCode.length !== 6) {
customPopups.showAlert('6자리 인증코드를 입력해주세요.');
return;
}
$.ajax({
url: '/mypage/verify-email-code',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
email: email,
code: authCode
}),
headers: {
[csrfHeaderName]: csrfToken
},
success: function(response) {
clearInterval(timerInterval);
isVerified = true;
customPopups.showAlert('이메일 인증이 완료되었습니다!');
// 알림 확인 후 메인 페이지로 이동
$('#customAlertOkButton').off('click.emailVerify').on('click.emailVerify', function () {
customPopups.hideAlert('#customAlert');
location.href = '/';
});
},
error: function(xhr) {
const errorMsg = xhr.responseJSON?.error || '인증코드가 일치하지 않습니다.';
customPopups.showAlert(errorMsg);
}
});
});
// 인증 완료 버튼 클릭
$('.btn_complete').on('click', function(e) {
e.preventDefault();
if (!isVerified) {
customPopups.showAlert('이메일 인증을 먼저 완료해주세요.');
return;
}
// 메인 페이지로 이동
customPopups.showAlert('이메일 인증이 완료되었습니다. 메인 페이지로 이동합니다.');
setTimeout(function() {
location.href = '/';
}, 1500);
});
});
</script>
</th:block>
</html>