kbank 소스 일부 복원

This commit is contained in:
Rinjae
2025-11-06 14:43:42 +09:00
parent e3f579a9ab
commit fce34fd98b
3 changed files with 506 additions and 413 deletions
@@ -7,6 +7,7 @@ import org.springframework.stereotype.Component;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.sql.SQLException;
@Component
@Aspect
@@ -16,9 +17,9 @@ public class JpaErrorLoggingAspect {
private EntityManager em;
@AfterThrowing(pointcut = "@annotation(org.springframework.transaction.annotation.Transactional)", throwing = "ex")
public void onError(Exception ex) {
public void onError(SQLException ex) {
try {
String user = (String) em.createNativeQuery("select user from user").getSingleResult();
String user = (String) em.createNativeQuery("select user from dual").getSingleResult();
log.error("DB USER = {}, SQL ERROR = {}", user, ex.getMessage() );
} catch (Exception e) {
log.error("Failed to fetch DB User", e);
@@ -1,90 +1,104 @@
<!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_base_layout}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kbank_base_layout}">
<body>
<th:block layout:fragment="contentFragment">
<div class="login-page">
<div class="login-container">
<div class="login-card">
<!-- Login Header with Logo -->
<div class="login-header">
<h1 class="login-title">로그인</h1>
<p class="login-subtitle">광주은행 개발자 포털에 오신 것을 환영합니다</p>
</div>
<!-- Alert Messages -->
<div th:if="${param.logout}" class="login-alert alert-info">
<i class="fas fa-info-circle"></i>
<span>로그아웃되었습니다.</span>
</div>
<!-- Login Form -->
<form id="loginForm" role="form" name="loginForm" th:action="@{/actionLogin.do}" method="post" class="login-form">
<section layout:fragment="contentFragment" class="content">
<div class="content_wrap">
<div class="sub_title2">
<h2 class="title add7">로그인</h2>
</div>
<div class="inner i_cs9 h_inner9">
<div class="form_type">
<form id="loginForm" role="form" name="loginForm" th:action="@{/actionLogin.do}" method="post">
<input type="hidden" id="message" name="message" th:value="${loginMessage}">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<input type="hidden" id="loginId" th:value="${loginId}">
<div class="form_type_box">
<div class="info1 form_top">
<p class="title">
<span>아이디</span>
</p>
<div class="info_line info_add">
<div class="info_box1 add add2">
<input type="text" id="id" name="id" th:value="${loginId}"
class="common_input_type_1 h_inp"
placeholder="이메일 아이디를 입력해 주세요." required>
<input type="hidden" id="loginId" th:value="${loginId}">
</div>
</div>
</div>
<!-- Email Field -->
<div class="form-group">
<label for="id" class="form-label">이메일</label>
<input type="text"
id="id"
name="id"
th:value="${loginId}"
class="form-input"
placeholder="이메일 아이디를 입력해 주세요"
required>
<div class="info1">
<p class="title">
<span>비밀번호</span>
</p>
<div class="info_line info_add">
<div class="info_box1 add add2">
<input type="password" id="password" name="password"
class="common_input_type_1 h_inp"
placeholder="비밀번호를 입력해 주세요." required>
</div>
</div>
</div>
</div>
<!-- Password Field -->
<div class="form-group">
<label for="password" class="form-label">비밀번호</label>
<input type="password"
id="password"
name="password"
class="form-input"
placeholder="비밀번호를 입력해 주세요"
required>
</div>
<!-- Remember Me Checkbox -->
<div class="form-checkbox">
<input id="checkId" name="checkId" type="checkbox">
<label for="checkId">아이디 저장</label>
<div class="box_inp">
<span class="inp_check f_size">
<input id="checkId" name="checkId" type="checkbox">
<label for="checkId">아이디 저장</label>
</span>
</div>
</form>
<!-- Login Button -->
<button type="button" class="login-button" id="actionLogin">
로그인
</button>
<!-- Additional Links -->
<div class="login-links">
<a th:href="@{/account_recovery(tab='findId')}">
<i class="fas fa-search"></i>
<span>아이디 찾기</span>
</a>
<span class="link-separator">|</span>
<a th:href="@{/account_recovery(tab='resetPassword')}">
<i class="fas fa-lock"></i>
<span>비밀번호 초기화</span>
</a>
<span class="link-separator">|</span>
<a th:href="@{/signup}">
<i class="fas fa-user-plus"></i>
<span>회원가입</span>
</a>
<div class="btn_login">
<a href="#" class="common_btn_type_1 h_btn" id="actionLogin"><span>로그인</span></a>
</div>
<!-- Loading State -->
<div class="login-loading" id="loginLoading">
<div class="spinner"></div>
<div class="login_list pc-only">
<ul>
<li class="icon_search">
<img th:src="@{/img/icon/icon_login_search.png}" alt="">
<a th:href="@{/account_recovery(tab='findId')}">아이디 찾기</a>
</li>
<li>
<span>|</span>
</li>
<li class="icon_lock">
<img th:src="@{/img/icon/icon_login_lock.png}" alt="">
<a th:href="@{/account_recovery(tab='resetPassword')}">비밀번호 초기화</a>
</li>
<li>
<span>|</span>
</li>
<li class="icon_join">
<img th:src="@{/img/icon/icon_login_join.png}" alt="">
<a th:href="@{/signup}">회원가입</a>
</li>
</ul>
</div>
<div class="login_list m-only">
<ul>
<li class="icon_search">
<a th:href="@{/account_recovery(tab='findId')}">아이디 찾기</a>
</li>
<li>
<span>|</span>
</li>
<li class="icon_lock">
<a th:href="@{/account_recovery(tab='resetPassword')}">비밀번호 초기화</a>
</li>
<li>
<span>|</span>
</li>
<li class="icon_join">
<a th:href="@{/signup}">회원가입</a>
</li>
</ul>
</div>
<div th:if="${param.logout}" class="alert alert-info mt-3">
<p>로그아웃되었습니다.</p>
</div>
</div>
</div>
</div>
</th:block>
</section>
</body>
<th:block layout:fragment="contentScript">
<script th:inline="javascript">
@@ -165,10 +179,6 @@
let actionLogin = function (event) {
let form = document.loginForm;
// Show loading state
$('#loginLoading').addClass('active');
if ($('#checkId').is(':checked')) {
setCookie('saveid', $('#id').val(), new Date(new Date().getTime() + 1000 * 3600 * 24 * 30));
} else {
@@ -178,11 +188,9 @@
if (!form.checkValidity()) {
event.preventDefault();
event.stopPropagation();
$('#loginLoading').removeClass('active');
customPopups.showAlert('입력 정보 확인');
} else {
if (form.password.value.length < 8) {
$('#loginLoading').removeClass('active');
customPopups.showAlert('[[#{login.passLengthShort}]]');
} else {
const originalPassword = form.password.value;
@@ -193,10 +201,7 @@
form.classList.add('was-validated');
};
$('#actionLogin').on('click', function(event) {
event.preventDefault();
actionLogin(event);
});
$('#actionLogin').on('click', actionLogin);
$('#password').on('keydown', function (event) {
if (event.key === 'Enter' || event.keyCode === 13 || event.code === 'Enter') {
event.preventDefault();
@@ -1,372 +1,459 @@
<!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 dormantAccount = [[${session.dormantAccount}]];
if (dormantAccount) {
const dormantMsg = /*[[${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('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;
}
});
});
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>