This commit is contained in:
Rinjae
2025-12-03 15:49:02 +09:00
285 changed files with 35316 additions and 12320 deletions
@@ -1,6 +1,6 @@
<!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_api_layout}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
<body>
@@ -1,6 +1,6 @@
<!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_api_layout}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
<body>
@@ -1,6 +1,6 @@
<!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_api_layout}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
<body>
@@ -1,6 +1,6 @@
<!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_api_layout}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
<body>
@@ -1,6 +1,6 @@
<!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_api_layout}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
<body>
@@ -1,6 +1,6 @@
<!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_api_layout}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
<body>
@@ -1,6 +1,6 @@
<!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_api_layout}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
<body>
@@ -1,6 +1,6 @@
<!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_api_layout}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
<body>
@@ -1,6 +1,6 @@
<!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_api_layout}"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
@@ -2,39 +2,53 @@
<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}">
layout:decorate="~{layout/kjbank_title_layout}">
<body>
<th:block layout:fragment="title">
<div class="page-title-banner" style="margin-bottom: 24px;">
<h1>API 마켓</h1>
</div>
</th:block>
<th:block layout:fragment="contentFragment">
<section class="api-market-container">
<!-- Sidebar Navigation -->
<aside class="api-market-sidebar" id="apiSidebar">
<div class="api-sidebar-header">
<img th:src="@{/img/api_sidebar.png}" alt="API">
</div>
<nav class="api-sidebar-nav">
<!-- All APIs -->
<div class="menu-section">
<div class="menu-title" th:classappend="${selected == '-1'} ? 'active'" data-group="">
<a class="menu-title" th:classappend="${selected == '-1'} ? 'active'" th:href="@{/apis}">
전체
<span class="api-count" th:text="${totalApiCount}">0</span>
</div>
</a>
</div>
<!-- Service Categories -->
<div class="menu-section" th:each="service : ${services}">
<div class="menu-title"
th:classappend="${selected == service.id} ? 'active'"
<!-- Service Categories (Accordion) -->
<div class="menu-section accordion-section" th:each="service : ${services}">
<div class="menu-title accordion-trigger"
th:classappend="${selected == service.id} ? 'active expanded'"
th:data-group="${service.id}">
[[${service.groupName}]]
<span class="api-count" th:text="${service.apiGroupApiList.size()}">0</span>
<span class="menu-title-text">[[${service.groupName}]]</span>
<span class="accordion-icon">
<i class="fas fa-chevron-down"></i>
</span>
</div>
<!-- Level 2: API List -->
<div class="api-list" th:if="${service.apiGroupApiList != null and !service.apiGroupApiList.isEmpty()}">
<!-- Level 2: API List (Accordion Content) -->
<div class="api-list accordion-content"
th:if="${service.apiGroupApiList != null and !service.apiGroupApiList.isEmpty()}"
th:classappend="${selected == service.id} ? 'expanded'">
<div class="api-item"
th:each="api : ${service.apiGroupApiList}"
th:data-api-id="${api.apiId}"
th:attr="data-href=@{/apis/detail(apiId=${api.apiId})}">
th:classappend="${apiSpecInfo != null and apiSpecInfo.apiId == api.apiId} ? 'active'"
th:attr="data-href=@{/apis/detail(id=${api.apiId})}">
<span class="api-name" th:text="${api.apiDesc}">API Name</span>
</div>
</div>
@@ -73,74 +87,83 @@
<div class="tab-content active" id="api-info-tab">
<!-- API Overview Card (Merged with Additional Information) -->
<div class="api-overview-card">
<div class="api-overview-header">
<div class="api-method-badge" th:text="${apiSpecInfo.apiMethod}">GET</div>
<div class="api-endpoint">
<code th:text="${apiSpecInfo.apiUrl}">/api/v1/example</code>
<div class="org-section-header org-section-header--agreement">
<h3>기본 정보</h3>
</div>
</div>
<!-- API Information Table -->
<div class="api-overview-info">
<table class="api-info-table">
<tr th:if="${apiSpecInfo.apiContentType != null}">
<th>Content Type</th>
<td th:text="${apiSpecInfo.apiContentType}">application/json</td>
</tr>
</table>
</div>
<div class="api-simple-description" th:if="${apiSpecInfo.apiSimpleDescription != null}">
<p th:text="${apiSpecInfo.apiSimpleDescription}">Simple API description</p>
</div>
<div class="detail-content" th:utext="${apiSpecInfo.description}">
Detailed API description
</div>
</div>
<!-- API Details Grid -->
<div class="api-details-grid">
<!-- Request Specification -->
<div class="api-detail-card" th:if="${apiSpecInfo.apiRequestSpec != null}">
<h3>Request Specification</h3>
<div class="detail-content" th:utext="${apiSpecInfo.apiRequestSpec}">
Request spec
<div class="api-overview-header">
<div class="api-method-badge" th:text="${apiSpecInfo.apiMethod}">GET</div>
<div class="api-endpoint">
<code th:text="${apiSpecInfo.apiUrl}">/api/v1/example</code>
</div>
</div>
<!-- API Information Table -->
<div class="api-overview-info">
<table class="api-info-table">
<tr th:if="${apiSpecInfo.apiContentType != null}">
<th>Content Type</th>
<td th:text="${apiSpecInfo.apiContentType}">application/json</td>
</tr>
</table>
</div>
<div class="api-simple-description" th:if="${apiSpecInfo.apiSimpleDescription != null}">
<p th:text="${apiSpecInfo.apiSimpleDescription}">Simple API description</p>
</div>
<div class="detail-content" th:utext="${apiSpecInfo.description}">
Detailed API description
</div>
</div>
<!-- Sample Request -->
<div class="api-detail-card" th:if="${apiSpecInfo.sampleRequest != null}">
<h3>샘플 요청</h3>
<div class="detail-content">
<pre><code th:text="${apiSpecInfo.sampleRequest}">Sample request</code></pre>
</div>
</div>
<!-- API Details Grid -->
<div class="api-details-grid">
<!-- Response Specification -->
<div class="api-detail-card" th:if="${apiSpecInfo.apiResponseSpec != null}">
<h3>Response Specification</h3>
<div class="detail-content" th:utext="${apiSpecInfo.apiResponseSpec}">
Response spec
<!-- Request Specification -->
<div class="api-detail-card" th:if="${apiSpecInfo.apiRequestSpec != null}">
<div class="org-section-header org-section-header--agreement">
<h3>Request Specification</h3>
</div>
<div class="detail-content" th:utext="${apiSpecInfo.apiRequestSpec}">
Request spec
</div>
</div>
</div>
<!-- Sample Response -->
<div class="api-detail-card" th:if="${apiSpecInfo.sampleResponse != null}">
<h3>샘플 응답</h3>
<div class="detail-content">
<pre><code th:text="${apiSpecInfo.sampleResponse}">Sample response</code></pre>
<!-- Sample Request -->
<div class="api-detail-card" th:if="${apiSpecInfo.sampleRequest != null}">
<div class="org-section-header org-section-header--agreement">
<h3>샘플 요청</h3>
</div>
<div class="detail-content">
<pre><code th:text="${apiSpecInfo.sampleRequest}">Sample request</code></pre>
</div>
</div>
<!-- Response Specification -->
<div class="api-detail-card" th:if="${apiSpecInfo.apiResponseSpec != null}">
<div class="org-section-header org-section-header--agreement">
<h3>Response Specification</h3>
</div>
<div class="detail-content" th:utext="${apiSpecInfo.apiResponseSpec}">
Response spec
</div>
</div>
<!-- Sample Response -->
<div class="api-detail-card" th:if="${apiSpecInfo.sampleResponse != null}">
<div class="org-section-header org-section-header--agreement">
<h3>샘플 응답</h3>
</div>
<div class="detail-content">
<pre><code th:text="${apiSpecInfo.sampleResponse}">Sample response</code></pre>
</div>
</div>
</div>
</div>
</div>
<!-- Testbed Tab Content -->
<div class="tab-content" id="testbed-tab">
<link rel="stylesheet" type="text/css" th:href="@{/plugins/swaggerUI/swagger-ui.css}"/>
<link rel="stylesheet" type="text/css" th:href="@{/plugins/swaggerUI/index.css}"/>
<div class="api-overview-card">
<div id="swagger-ui"></div>
</div>
<div id="swagger-ui"></div>
</div>
</div>
@@ -207,7 +230,31 @@
});
});
// Category/Service Selection - removed as not needed in detail page
// Accordion Toggle Functionality
const accordionTriggers = document.querySelectorAll('.accordion-trigger');
accordionTriggers.forEach(function(trigger) {
trigger.addEventListener('click', function(e) {
e.preventDefault();
const menuSection = this.closest('.accordion-section');
const accordionContent = menuSection.querySelector('.accordion-content');
if (accordionContent) {
// Toggle expanded state
const isExpanded = this.classList.contains('expanded');
if (isExpanded) {
// Close this accordion
this.classList.remove('expanded');
accordionContent.classList.remove('expanded');
} else {
// Open this accordion
this.classList.add('expanded');
accordionContent.classList.add('expanded');
}
}
});
});
// Mobile Menu Toggle
if (mobileToggle && sidebar && mobileOverlay) {
@@ -1,141 +0,0 @@
<!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_apikey_layout}">
<body>
<section layout:fragment="contentFragment" class="content">
<div class="content_wrap">
<div class="sub_title3">
<h2 class="title">API키 신청</h2>
</div>
<div class="inner i_cs h_inner5">
<div class="form_type">
<div class="info1 form_top">
<p class="title w_tit">
<span class="dot">API키 이름</span>
</p>
<div class="info_line info_add">
<div class="info_box1 w_inp4">
<input type="text" name="name" style="max-width: 300px;" id="appRequestClientName" class="common_input_type_1 h_inp2" placeholder="API키 이름 입력">
</div>
</div>
</div>
<div class="infor api_dot top" style="margin-top: 10px;">
<ul>
<li>
<p>인증키가 없을 경우, 승인 시, 인증키는 신규로 발급 됩니다.</p>
</li>
</ul>
</div>
</div>
<p class="pop_text l_text m-only" style="margin-top: 10px; font-weight: 700;">API</p>
<div class="board_search sh_box top">
<p class="pop_text l_text pc-only">API</p>
<div class="search_txt ser_api w_inp2">
<input type="text" class="common_input_type_1 h_inp2" id="api_keyword" placeholder="API 검색">
</div>
<div class="btn_wrap search_btn">
<button type="button" class="btn_search" id="api_search">검색</button>
<i class="i_search"></i>
</div>
<p class="pop_text l_text pop_text2 pc-only" style="margin-left:76px;">신청 API</p>
</div>
<div class="tb_conbox top">
<div class="box table_type2 top_type">
<table class="table_col table_min tb_bdtop" id="sourceTable" cellspacing="0" cellpadding="0" width="100%">
<colgroup>
<col width="60px">
<col width="140px">
<col width="230px">
</colgroup>
<thead>
<tr>
<th>
<div class="total_wrap">
<span class="inp_check b_tit">
<input type="checkbox" class="agree_all" id="selectAllSourceTable" name="selectall" data-group="total">
<label for="selectAllSourceTable"></label>
</span>
</div>
</th>
<th>서비스</th>
<th>API</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<div class="box pop_arrow m-only" style="text-align:center;">
<a href="#none" class="add_apis"><img th:src="@{/img/icon/Icon_arrow_btn_down.png}" alt="올리기"></a>
<a href="#none" class="remove_apis"><img th:src="@{/img/icon/Icon_arrow_btn_up.png}" alt="내리기"></a>
</div>
<div class="box pop_arrow pc-only" style="text-align:center;">
<p>
<a href="#none" class="add_apis"><img th:src="@{/img/icon/icon_arrow_btn_r.png}" alt="넣기"></a>
</p>
<p>
<a href="#none" class="remove_apis"><img th:src="@{/img/icon/icon_arrow_btn_l.png}" alt="빼기"></a>
</p>
</div>
<div class="box table_type2 top_type">
<table class="table_col table_min tb_bdtop" id="targetTable" cellspacing="0" cellpadding="0" width="100%">
<colgroup>
<col width="60px">
<col width="140px">
<col width="230px">
</colgroup>
<thead>
<tr>
<th>
<div class="total_wrap">
<span class="inp_check b_tit">
<input type="checkbox" class="agree_all" id="selectAllTargetTable" name="selectall2" data-group="total2">
<label for="selectAllTargetTable"></label>
</span>
</div>
</th>
<th>서비스</th>
<th>API</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
<p class="pop_text l_text2">신청사유</p>
<div class="pop_textbox">
<textarea name="reason" rows="5" cols="30" class="common_textareaType_1" placeholder="신청사유를 입력해 주세요."></textarea>
</div>
<div class="form_type">
<div class="btn_application btn_top3">
<a th:href="@{/}" class="common_btn_type_1 gray"><span>취소</span></a>
<div class="btn_gap"></div>
<a href="#none" class="common_btn_type_1 request_apis"><span>확인</span></a>
</div>
</div>
</div>
</div>
</section>
<section layout:fragment="pagePopups" class="content pop">
<th:block th:replace="~{fragment/apikey-request :: apiKeyRequestScript}"></th:block>
</section>
</body>
<th:block layout:fragment="contentScript">
<script th:inline="javascript">
document.addEventListener('DOMContentLoaded', function () {
activateSelect();
$('.btn_cancel').on('click', function(){
window.location.href = '/apis';
})
});
</script>
</th:block>
</html>
@@ -1,162 +0,0 @@
<!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_apikey_layout}">
<body>
<section layout:fragment="contentFragment" class="content">
<div class="content_wrap">
<div class="sub_title3">
<h2 class="title">인증키 운영 전환 신청</h2>
</div>
<div class="inner i_cs h_inner5">
<div class="form_type">
<div class="info1 form_top">
<p class="title w_tit">
<span class="dot">API키 이름</span>
</p>
<div class="info_line info_add">
<div class="info_box1 w_inp4">
<input type="text" name="name" id="appRequestProdClientName" class="common_input_type_1 h_inp2" style="width: 300px;" placeholder="API키 이름 입력">
</div>
</div>
</div>
<div class="infor api_dot top" style="margin-top: 10px;">
<ul>
<li>
<p>인증키가 없을 경우, 승인 시, 인증키는 신규로 발급 됩니다.</p>
</li>
</ul>
</div>
</div>
<div class="form_type">
<div class="info1 form_top" style="margin-top: 10px;">
<p class="title w_tit">
<span class="">API키 선택</span>
</p>
<div class="info_line info_add">
<div class="info_box1 w_inp4">
<div class="custom_select select_w h_inp" style="width: 300px;">
<input type="hidden" id="clientId" />
<div class="select_selected h_inp" id="selectedDevApiKey">API키 선택</div>
<ul class="common_selecttype select_items select_hide" id="devApiKeyList">
</ul>
</div>
</div>
</div>
</div>
</div>
<p class="pop_text l_text m-only" style="margin-top: 10px; font-weight: 700;">API</p>
<div class="board_search sh_box top2">
<p class="pop_text l_text pc-only">API</p>
<div class="search_txt ser_api w_inp">
<input type="text" class="common_input_type_1 h_inp2" id="prod_api_keyword" placeholder="API 검색">
</div>
<div class="btn_wrap search_btn">
<button type="button" class="btn_search" id="prod_api_search">검색</button>
<i class="i_search"></i>
</div>
<p class="pop_text l_text pop_text2 pc-only" style="margin-left: 145px;">운영 이관 신청 API</p>
</div>
<div class="tb_conbox top2">
<div class="box table_type2 top_type">
<table class="table_col table_min tb_bdtop" id="prodSourceTable" cellspacing="0" cellpadding="0" width="100%">
<colgroup>
<col width="60px">
<col width="140px">
<col width="230px">
</colgroup>
<thead>
<tr>
<th>
<div class="total_wrap">
<span class="inp_check b_tit">
<input type="checkbox" class="agree_all" id="selectAllProdSourceTable" name="selectall" data-group="total">
<label for="selectAllProdSourceTable"></label>
</span>
</div>
</th>
<th>서비스</th>
<th>API</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<div class="box pop_arrow m-only" style="text-align:center;">
<a href="#none" class="add_apis"><img th:src="@{/img/icon/Icon_arrow_btn_down.png}" alt="올리기"></a>
<a href="#none" class="remove_apis"><img th:src="@{/img/icon/Icon_arrow_btn_up.png}" alt="내리기"></a>
</div>
<div class="box pop_arrow pc-only" style="text-align:center;">
<p>
<a href="#none" class="add_apis"><img th:src="@{/img/icon/icon_arrow_btn_r.png}" alt="넣기"></a>
</p>
<p>
<a href="#none" class="remove_apis"><img th:src="@{/img/icon/icon_arrow_btn_l.png}" alt="빼기"></a>
</p>
</div>
<p class="pop_text l_text2 pt_top m-only">운영 이관 신청 API</p>
<div class="box table_type2 top_type">
<table class="table_col table_min tb_bdtop" id="prodTargetTable" cellspacing="0" cellpadding="0" width="100%">
<colgroup>
<col width="60px">
<col width="140px">
<col width="230px">
</colgroup>
<thead>
<tr>
<th>
<div class="total_wrap">
<span class="inp_check b_tit">
<input type="checkbox" class="agree_all" id="selectAllProdTargetTable" name="selectall2" data-group="total2">
<label for="selectAllProdTargetTable"></label>
</span>
</div>
</th>
<th>서비스</th>
<th>API</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
<p class="pop_text l_text2">신청사유</p>
<div class="pop_textbox">
<textarea name="reason" id="prod_reason" rows="5" cols="30" class="common_textareaType_1" placeholder="신청사유를 입력해 주세요."></textarea>
</div>
<div class="form_type">
<div class="btn_application btn_top3">
<button class="common_btn_type_1 gray btn_cancel">취소</button>
<div class="btn_gap"></div>
<button class="common_btn_type_1 request_prod_apis">운영 전환 신청</button>
</div>
</div>
</div>
</div>
</section>
<section layout:fragment="pagePopups" class="content pop">
<th:block th:replace="~{fragment/apikey-request-prod :: prodApiKeyRequestScript}"></th:block>
</section>
</body>
<th:block layout:fragment="contentScript">
<script th:inline="javascript">
document.addEventListener('DOMContentLoaded', function () {
activateSelect();
$('.btn_cancel').on('click', function(){
window.location.href = '/apis';
});
});
</script>
</th:block>
</html>
@@ -2,21 +2,30 @@
<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}">
layout:decorate="~{layout/kjbank_title_layout}">
<body>
<th:block layout:fragment="title">
<div class="page-title-banner" style="margin-bottom: 24px;">
<h1>API 마켓</h1>
</div>
</th:block>
<th:block layout:fragment="contentFragment">
<section class="api-market-container">
<!-- Sidebar Navigation -->
<aside class="api-market-sidebar" id="apiSidebar">
<!-- Sidebar Header Image -->
<div class="api-sidebar-header">
<img th:src="@{/img/api_sidebar.png}" alt="API">
</div>
<nav class="api-sidebar-nav">
<!-- All APIs -->
<div class="menu-section">
<div class="menu-title" th:classappend="${selected == '-1'} ? 'active'" data-group="">
전체
<span class="api-count" th:text="${totalApiCount}">0</span>
</div>
</div>
@@ -26,7 +35,6 @@
th:classappend="${selected == service.id} ? 'active'"
th:data-group="${service.id}">
[[${service.groupName}]]
<span class="api-count" th:text="${service.apiGroupApiList.size()}">0</span>
</div>
</div>
</nav>
@@ -43,11 +51,10 @@
<!-- Main Content -->
<main class="api-market-content">
<!-- Header -->
<!-- Header: Result Count + Search -->
<div class="api-market-header">
<div class="api-market-title">
<h1>오픈API</h1>
<h2>API 마켓</h2>
<div class="api-result-count">
<strong th:text="${selectedApiCount}">0</strong>
</div>
<div class="api-market-search">
<form id="searchForm" th:action="@{/apis}" method="get">
@@ -58,17 +65,14 @@
th:value="${search.keyword}"
placeholder="어떤 API를 찾고 계신가요?">
<button type="submit" class="search-submit-btn" aria-label="검색">
<span class="search-icon">🔍</span>
<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>
</button>
</form>
</div>
</div>
<!-- Result Count -->
<div class="api-result-count">
<strong th:text="${selectedApiCount}">0</strong>
</div>
<!-- API Cards Grid -->
<div class="api-card-grid" th:if="${apis != null and !apis.isEmpty()}">
<div class="api-card"
@@ -76,33 +80,29 @@
th:data-href="@{/apis/detail(id=${api.apiId})}"
role="button"
tabindex="0">
<div>
<!-- Card Header with Badge -->
<div class="api-card-header">
<span class="api-card-badge" th:if="${api.apiGroupName}" th:text="${api.apiGroupName}">
카테고리
</span>
<!-- Group Info -->
<div class="api-card-group">
<div class="api-card-group-icon">
<img th:if="${api.mainIcon}"
th:src="${api.mainIcon}"
th:alt="${api.apiGroupName}"
onerror="this.style.display='none'; this.parentElement.innerHTML='<i class=\'fas fa-cube\'></i>'">
<i th:if="${#strings.isEmpty(api.mainIcon)}" class="fas fa-cube"></i>
</div>
<!-- Card Title -->
<h3 class="api-card-title" th:text="${api.apiName}">
API 이름
</h3>
<!-- Card Description -->
<p class="api-card-description" th:text="${api.apiSimpleDescription ?: 'API 설명이 없습니다.'}">
API 설명
</p>
<span class="api-card-group-name" th:text="${api.apiGroupName}">
그룹 이름
</span>
</div>
<!-- Card Icon -->
<div class="api-card-icon">
<img th:if="${api.mainIcon}"
th:src="${api.mainIcon}"
th:alt="${api.apiName}"
onerror="this.style.display='none'; this.parentElement.innerHTML='📄'">
<span th:if="${#strings.isEmpty(api.mainIcon)}">📄</span>
</div>
<!-- API Name -->
<h3 class="api-card-name" th:text="${api.apiName}">
API 이름
</h3>
<!-- API Description -->
<p class="api-card-description" th:text="${api.apiSimpleDescription ?: 'API 설명이 없습니다.'}">
API 설명
</p>
</div>
</div>
@@ -1,6 +1,6 @@
<!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_api_layout}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
<body>
<section layout:fragment="contentFragment" class="content api_con">
@@ -379,4 +379,4 @@ grant_type=client_credentials&client_id=your_client_id&client_secret=your_client
</script>
</th:block>
</body>
</html>
</html>
@@ -0,0 +1,418 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/kjbank_title_layout}">
<head>
<title>수수료 관리</title>
</head>
<body>
<section layout:fragment="title">
<div class="page-title-banner">
<img th:src="@{/img/img_title_bg.png}" class="title-image">
<h1>기관 수수료 관리</h1>
</div>
</section>
<section layout:fragment="contentFragment" class="content">
<div class="searchBox row-two">
<span class="form-inline">
<label for="providers">제공기관</label>
<select id="providers" class="form-control">
<option value="">- 선택 -</option>
<option value="1" selected>광주은행</option>
<option value="2">한국은행</option>
</select>
</span>
<span class="form-inline">
<label for="organizations">이용기관</label>
<select id="organizations" class="form-control">
<option value="">- 선택 -</option>
<option value="1" selected th:text="${organization?.orgName ?: 'ABC 핀테크'}"></option>
</select>
</span>
<span class="form-inline">
<label for="forYear">조회기간</label>
<select id="forYear" class="form-control">
<option value="">- 선택 -</option>
<option value="2023">2023</option>
<option value="2024">2024</option>
<option value="2025" selected>2025</option>
</select> <span></span>&nbsp;&nbsp;
<select id="forMonth" class="form-control">
<option value="">- 선택 -</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3" selected>3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
<option value="8">8</option>
<option value="9">9</option>
<option value="10">10</option>
<option value="11">11</option>
<option value="12">12</option>
</select> <span></span>
</span>
<span class="btnBox">
<button type="button" class="btn btn-primary" onclick="loadData()">
<span class="fa fa-search"></span><span>조회</span>
</button>
<button type="button" id="printBtn" class="btn btn-secondary" onclick="printCommission()">
<span>청구서 출력</span>
</button>
</span>
</div>
<div class="commission-container">
<div class="col-md-12">
<p class="commission-notice">
이용기관은 매월 1~5일 전월 수수료 조회 가능하며, 10일 확정금액으로 정산됩니다. <br/>
금액이상시 담당자 연락 및 조정요청 하시기 바랍니다. (매월 1~5일 수수료 조회, 6~9일 수수료 조정 및 확정, 10일 정산)
</p>
<!-- API 수수료 테이블 -->
<div class="table-responsive">
<table class="table table-bordered border-type">
<colgroup>
<col style="width:6%"/>
<col style="width:16%"/>
<col style="width:8%"/>
<col style="width:8%"/>
<col style="width:8%"/>
<col style="width:8%"/>
<col style="width:8%"/>
<col style="width:8%"/>
<col style="width:8%"/>
<col style="width:8%"/>
<col/>
</colgroup>
<thead>
<tr>
<th rowspan="2">구분</th>
<th rowspan="2">API 명</th>
<th colspan="4">조정 전</th>
<th colspan="4">조정 후</th>
<th rowspan="2">조정사유</th>
</tr>
<tr>
<th>처리<br/>건수</th>
<th>적용<br/>수수료(원)</th>
<th>최저<br/>수수료</th>
<th>출금<br/>예상금액</th>
<th>조정<br/>건수</th>
<th>조정<br/>수수료(원)</th>
<th>최저<br/>수수료</th>
<th>출금<br/>예상금액</th>
</tr>
</thead>
<tbody id="apiTableBody">
<!-- 동적 생성 -->
</tbody>
</table>
<!-- 판매 수수료 테이블 -->
<table class="table table-bordered border-type voffset3">
<colgroup>
<col style="width: 6%;">
<col style="width: 8%;">
<col style="width: 8%;">
<col style="width: 5%;">
<col style="width: 5%;">
<col style="width: 7%;">
<col style="width: 6%;">
<col style="width: 6%;">
<col style="width: 7%;">
<col style="width: 7%;">
<col style="width: 6%;">
<col style="width: 6%;">
<col style="width: 7%;">
<col style="width: 7%;">
<col>
</colgroup>
<thead>
<tr>
<th rowspan="2">구분</th>
<th rowspan="2">과금대상<br/>업무</th>
<th rowspan="2">수수료<br/>유형</th>
<th rowspan="2">과금<br/>기준</th>
<th rowspan="2">값구분</th>
<th rowspan="2">판매액</th>
<th colspan="4">조정 전</th>
<th colspan="4">조정 후</th>
<th rowspan="2">조정사유</th>
</tr>
<tr>
<th>적용<br/>수수료</th>
<th>처리<br/>건수</th>
<th>최저<br/>수수료</th>
<th>출금<br/>예상수수료</th>
<th>적용<br/>수수료</th>
<th>처리<br/>건수</th>
<th>최저<br/>수수료</th>
<th>출금<br/>예상수수료</th>
</tr>
</thead>
<tbody id="saleTableBody">
<!-- 동적 생성 -->
</tbody>
</table>
<!-- 합계 테이블 -->
<table class="table table-bordered border-type voffset3">
<colgroup>
<col/>
<col style="width: 13%"/>
<col style="width: 13%"/>
<col style="width: 13%"/>
<col style="width: 13%"/>
<col style="width: 13%"/>
<col style="width: 13%"/>
</colgroup>
<thead>
<tr>
<th rowspan="2">구분</th>
<th colspan="3">조정 전</th>
<th colspan="3">조정 후</th>
</tr>
<tr>
<th>판매액</th>
<th>처리건수(건)</th>
<th>출금예상금액</th>
<th>판매액</th>
<th>처리건수(건)</th>
<th>출금예상금액</th>
</tr>
</thead>
<tbody>
<tr class="summary-row">
<td class="text-center"><strong id="totalTitle">합계</strong></td>
<td class="text-right"><strong id="totalSalesAmount1">-</strong></td>
<td class="text-right"><strong id="totalCount1">-</strong></td>
<td class="text-right"><strong class="summary-amount" id="totalAmount1">-</strong></td>
<td class="text-right"><strong id="totalSalesAmount2">-</strong></td>
<td class="text-right"><strong id="totalCount2">-</strong></td>
<td class="text-right"><strong class="summary-amount" id="totalAmount2">-</strong></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>
<th:block layout:fragment="contentScript">
<script th:inline="javascript">
function formatNumber(num) {
return new Intl.NumberFormat('ko-KR').format(num);
}
function loadData() {
const year = document.getElementById('forYear').value;
const month = document.getElementById('forMonth').value;
if (!year || !month) {
customPopups.showAlert('조회 기간을 선택해주세요.');
return;
}
// 서버에서 데이터 조회 API 호출
fetch('/commission', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': /*[[${_csrf.token}]]*/ ''
},
body: JSON.stringify({
year: year,
month: month
})
})
.then(response => {
if (!response.ok) {
throw new Error('데이터 조회에 실패했습니다.');
}
return response.json();
})
.then(data => renderCommissionData(data, year, month))
.catch(error => {
console.error('Error:', error);
customPopups.showAlert('데이터 조회 중 오류가 발생했습니다.');
});
}
function renderCommissionData(data, year, month) {
// API 수수료 항목 필터링
const apiCommissions = data.list.filter(item => item.paymentTargetType === '01');
const saleCommissions = data.list.filter(item => item.paymentTargetType !== '01');
// API 수수료 테이블 렌더링
const apiTableBody = document.getElementById('apiTableBody');
apiTableBody.innerHTML = '';
let apiTotal1 = 0;
let apiTotal2 = 0;
let apiCount1 = 0;
let apiCount2 = 0;
apiCommissions.forEach((item, index) => {
if (index === 0) {
apiTableBody.innerHTML += `
<tr>
<td rowspan="${apiCommissions.length}" class="text-center">API<br/>수수료</td>
<td class="text-left">${item.paymentTargetName}</td>
<td class="text-right">${formatNumber(item.totalCount || 0)}</td>
<td class="text-right">${formatNumber(item.value || 0)}</td>
<td class="text-right">${formatNumber(item.minimumAmount || 0)}</td>
<td class="text-right"><span class="color_darkRed">${formatNumber(item.sum || 0)}</span></td>
<td class="text-right"><strong class="color_blue">${formatNumber(item.totalCount2 || 0)}</strong></td>
<td class="text-right">${formatNumber(item.value2 || 0)}</td>
<td class="text-right">${formatNumber(item.minimumAmount2 || 0)}</td>
<td class="text-right"><span class="color_darkRed">${formatNumber(item.sum2 || 0)}</span></td>
<td>${item.changeReason2 || ''}</td>
</tr>
`;
} else {
apiTableBody.innerHTML += `
<tr>
<td class="text-left">${item.paymentTargetName}</td>
<td class="text-right">${formatNumber(item.totalCount || 0)}</td>
<td class="text-right">${formatNumber(item.value || 0)}</td>
<td class="text-right">${formatNumber(item.minimumAmount || 0)}</td>
<td class="text-right"><span class="color_darkRed">${formatNumber(item.sum || 0)}</span></td>
<td class="text-right"><strong class="color_blue">${formatNumber(item.totalCount2 || 0)}</strong></td>
<td class="text-right">${formatNumber(item.value2 || 0)}</td>
<td class="text-right">${formatNumber(item.minimumAmount2 || 0)}</td>
<td class="text-right"><span class="color_darkRed">${formatNumber(item.sum2 || 0)}</span></td>
<td>${item.changeReason2 || ''}</td>
</tr>
`;
}
apiTotal1 += parseFloat(item.sum || 0);
apiTotal2 += parseFloat(item.sum2 || 0);
apiCount1 += parseInt(item.totalCount || 0);
apiCount2 += parseInt(item.totalCount2 || 0);
});
// API 소계
apiTableBody.innerHTML += `
<tr>
<td colspan="2" class="text-center">${year}${month}월 API 수수료 소계</td>
<td class="text-right">${formatNumber(apiCount1)}</td>
<td class="text-right"></td>
<td class="text-right"></td>
<td class="text-right"><strong class="color_blue">${formatNumber(apiTotal1)}</strong></td>
<td class="text-right">${formatNumber(apiCount2)}</td>
<td class="text-right"></td>
<td class="text-right"></td>
<td class="text-right"><strong class="color_blue">${formatNumber(apiTotal2)}</strong></td>
<td></td>
</tr>
`;
// 판매 수수료 테이블 렌더링
const saleTableBody = document.getElementById('saleTableBody');
saleTableBody.innerHTML = '';
let saleTotal1 = 0;
let saleTotal2 = 0;
let saleCount1 = 0;
let saleCount2 = 0;
let saleSalesAmount = 0;
saleCommissions.forEach((item, index) => {
const isPercentageFee = item.paymentBaseName === '금액';
if (index === 0) {
saleTableBody.innerHTML += `
<tr>
<td rowspan="${saleCommissions.length}" class="text-center">판매<br/>수수료</td>
<td class="text-left">${item.paymentTargetName}</td>
<td>${item.detailFeeTypeName || ''}</td>
<td>${item.paymentBaseName || ''}</td>
<td>${item.valueDivisionName || ''}</td>
<td class="text-right">${item.totalValue ? formatNumber(item.totalValue) : ''}</td>
<td class="text-right">${isPercentageFee ? (parseFloat(item.value || 0) * 100) + '%' : formatNumber(item.value || 0)}</td>
<td class="text-right">${formatNumber(item.totalCount || 0)}</td>
<td class="text-right">${formatNumber(item.minimumAmount || 0)}</td>
<td class="text-right"><span class="color_darkRed">${formatNumber(item.sum || 0)}</span></td>
<td class="text-right">${isPercentageFee ? (parseFloat(item.value2 || 0) * 100) + '%' : formatNumber(item.value2 || 0)}</td>
<td class="text-right">${formatNumber(item.totalCount2 || 0)}</td>
<td class="text-right">${formatNumber(item.minimumAmount2 || 0)}</td>
<td class="text-right"><span class="color_darkRed">${formatNumber(item.sum2 || 0)}</span></td>
<td>${item.changeReason2 || ''}</td>
</tr>
`;
} else {
saleTableBody.innerHTML += `
<tr>
<td class="text-left">${item.paymentTargetName}</td>
<td>${item.detailFeeTypeName || ''}</td>
<td>${item.paymentBaseName || ''}</td>
<td>${item.valueDivisionName || ''}</td>
<td class="text-right">${item.totalValue ? formatNumber(item.totalValue) : ''}</td>
<td class="text-right">${isPercentageFee ? (parseFloat(item.value || 0) * 100) + '%' : formatNumber(item.value || 0)}</td>
<td class="text-right">${formatNumber(item.totalCount || 0)}</td>
<td class="text-right">${formatNumber(item.minimumAmount || 0)}</td>
<td class="text-right"><span class="color_darkRed">${formatNumber(item.sum || 0)}</span></td>
<td class="text-right">${isPercentageFee ? (parseFloat(item.value2 || 0) * 100) + '%' : formatNumber(item.value2 || 0)}</td>
<td class="text-right">${formatNumber(item.totalCount2 || 0)}</td>
<td class="text-right">${formatNumber(item.minimumAmount2 || 0)}</td>
<td class="text-right"><span class="color_darkRed">${formatNumber(item.sum2 || 0)}</span></td>
<td>${item.changeReason2 || ''}</td>
</tr>
`;
}
saleTotal1 += parseFloat(item.sum || 0);
saleTotal2 += parseFloat(item.sum2 || 0);
saleCount1 += parseInt(item.totalCount || 0);
saleCount2 += parseInt(item.totalCount2 || 0);
saleSalesAmount += parseFloat(item.totalValue || 0);
});
// 판매 소계
saleTableBody.innerHTML += `
<tr>
<td colspan="5" class="text-center">${year}${month}월 판매 수수료 소계</td>
<td class="text-right">${formatNumber(saleSalesAmount)}</td>
<td class="text-right"></td>
<td class="text-right">${formatNumber(saleCount1)}</td>
<td></td>
<td class="text-right"><strong class="color_blue">${formatNumber(saleTotal1)}</strong></td>
<td class="text-right"></td>
<td class="text-right">${formatNumber(saleCount2)}</td>
<td></td>
<td class="text-right"><strong class="color_blue">${formatNumber(saleTotal2)}</strong></td>
<td></td>
</tr>
`;
// 합계 업데이트
document.getElementById('totalTitle').textContent = `${year}${month}월 합계`;
document.getElementById('totalSalesAmount1').textContent = formatNumber(saleSalesAmount);
document.getElementById('totalCount1').textContent = formatNumber(apiCount1 + saleCount1);
document.getElementById('totalAmount1').textContent = formatNumber(apiTotal1 + saleTotal1);
document.getElementById('totalSalesAmount2').textContent = formatNumber(saleSalesAmount);
document.getElementById('totalCount2').textContent = formatNumber(apiCount2 + saleCount2);
document.getElementById('totalAmount2').textContent = formatNumber(apiTotal2 + saleTotal2);
// 출력 버튼 표시
document.getElementById('printBtn').style.display = 'inline-block';
}
function printCommission() {
const year = document.getElementById('forYear').value;
const month = document.getElementById('forMonth').value;
if (!year || !month) {
customPopups.showAlert('조회 기간을 선택해주세요.');
return;
}
// 별도 창으로 청구서 출력 페이지 열기
const printUrl = '/commission/print/' + year + '/' + month;
window.open(printUrl, '_blank', 'width=800,height=900,scrollbars=yes,resizable=yes');
}
</script>
</th:block>
</body>
</html>
@@ -0,0 +1,188 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<title>수수료 청구서</title>
<!-- <link rel="stylesheet" th:href="@{/content/css/vendor.css}">-->
<!-- <link rel="stylesheet" th:href="@{/css/main.css}">-->
<style>
html,body{font-size:12px;}
body{width:600px; margin:90px}
table{border-collapse: collapse;}
table th, td{padding:5px 10px;}
table tr{font-size:12px;}
.info-table{ border-top: 10px solid gray; width: 100%; border-bottom: 2px solid #333;}
.info-table th{text-align:left;}
.commission-table{width:100%;}
.commission-table th,.commission-table td{border:1px solid #999 !important}
.commission-table td{text-align:right}
.bg_ygreen{background-color:aquamarine; text-align:center}
.bg_skyBlue{background-color:skyblue; text-align:center}
.bg_yellow{background-color:yellow; text-align:center}
DIV { position: relative; }
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5a67d8;
}
@media print {
@page {
size: auto;
margin: 0 -0cm;
}
html {
margin: 0 0cm;
}
.control {
display: none !important;
}
}
</style>
</head>
<body class="commission-print-page">
<div class="control">
<input id="printButton" type="button" class="btn btn-primary" value="청구서 인쇄" onclick="window.print()"/>
</div>
<div id="PrintDIV">
<h1 style="text-align:center">
<img style="height:30px;" th:src="@{/img/logo/logo.png}" alt="광주은행 오픈뱅킹" />
</h1>
<p style="text-align:center">우61470 광주광역시 동구 제봉로 225 전화 (062)239-6722 FAX (062)239-6719 오픈뱅크 플랫폼 담당자</p>
<table class="info-table">
<colgroup>
<col style="width:15%;" />
<col/>
</colgroup>
<tbody>
<tr>
<th>문서번호</th>
<td><span th:text="|전자금융(OBP) ${year}-${month}-001호|">전자금융(OBP) 2024-03-001호</span></td>
</tr>
<tr>
<th>발 급 일</th>
<td><span th:text="${toDay}">2024년 3월 15일</span></td>
</tr>
<tr>
<th>수 신</th>
<td><span th:text="${organization}">ABC 핀테크 주식회사</span></td>
</tr>
<tr>
<th>참 조</th>
<td></td>
</tr>
<tr>
<th>제 목</th>
<td><span th:text="|${year}년 ${month}월 오픈뱅크플랫폼 API 서비스 이용수수료 청구|">2024년 3월 오픈뱅크플랫폼 API 서비스 이용수수료 청구</span></td>
</tr>
</tbody>
</table>
<div>
<p>1. 귀 사의 무궁한 발전을 기원합니다.</p>
<p>2. 귀 사와 체결된 서비스 이용 계약에 따라 동 업무처리와 관련하여 발생된 수수료를 다음과 같이 청구하오니 <br/>협조하여 주시기 바랍니다.</p>
</div>
<h2 style="float:left">1. 청구내역</h2>
<p style="float: right;margin-top: 25px;">(단위:건수/원)</p>
<table class="commission-table">
<colgroup>
<col style="width:35%"/>
<col style="width:20%"/>
<col style="width:15%"/>
<col style="width:15%"/>
<col style="width:15%"/>
</colgroup>
<thead>
<tr>
<th class="bg_ygreen">청구내용</th>
<th class="bg_ygreen">대출금액</th>
<th class="bg_ygreen">처리건수</th>
<th class="bg_ygreen">수수료</th>
<th class="bg_ygreen">수수료합계</th>
</tr>
</thead>
<tbody>
<th:block th:if="${commission != null and commission.list != null}">
<!-- API 수수료 -->
<tr th:each="item : ${commission.list}" th:if="${item.paymentTargetType == '01'}">
<td style="text-align:left" th:text="${item.paymentTargetName}">계좌잔액조회</td>
<td></td>
<td th:text="${#numbers.formatInteger(item.totalValue2, 0, 'COMMA')}">20,543</td>
<td th:text="|₩${#numbers.formatInteger(item.value2, 0, 'COMMA')}|">₩50</td>
<td th:text="|₩${#numbers.formatInteger(item.sum2, 0, 'COMMA')}|">₩1,027,150</td>
</tr>
<!-- API 소계 -->
<th:block th:if="${commission.apiSum2 != null}">
<tr>
<th class="bg_skyBlue" colspan="4">소계</th>
<td class="bg_skyBlue" th:text="|₩${#numbers.formatInteger(commission.apiSum2, 0, 'COMMA')}|">₩3,306,780</td>
</tr>
</th:block>
<!-- 판매 수수료 -->
<tr th:each="item : ${commission.list}" th:if="${item.paymentTargetType != '01'}">
<td style="text-align:left" th:text="${item.paymentTargetName}">개인대출 상품 A</td>
<td th:text="${item.paymentBaseName == '금액' ? '₩' + #numbers.formatInteger(item.totalValue2, 0, 'COMMA') : ''}">₩1,050,000,000</td>
<td th:text="${item.paymentBaseName == '건' ? #numbers.formatInteger(item.totalCount2, 0, 'COMMA') : ''}">55</td>
<td th:text="|₩${#numbers.formatInteger(item.value2, 0, 'COMMA')}|">₩5,000</td>
<td th:text="|₩${#numbers.formatInteger(item.sum2, 0, 'COMMA')}|">₩275,000</td>
</tr>
<!-- 판매 소계 -->
<th:block th:if="${commission.productSum2 != null}">
<tr>
<th class="bg_skyBlue" colspan="4">소계</th>
<td class="bg_skyBlue" th:text="|₩${#numbers.formatInteger(commission.productSum2, 0, 'COMMA')}|">₩533,000</td>
</tr>
</th:block>
<!-- 합계 -->
<th:block th:if="${commission.sum2 != null}">
<tr>
<th class="bg_yellow" colspan="4">합계</th>
<td class="bg_yellow" th:text="|₩${#numbers.formatInteger(commission.sum2, 0, 'COMMA')}|">₩3,839,780</td>
</tr>
</th:block>
</th:block>
</tbody>
</table>
<p>* 수수료는 Max(처리건수*금액, 월최저수수료) 금액으로 출금됩니다</p>
<br>
<h2>2. 수수료정산</h2>
<p>① 정산방법 : 수수료 결제계좌에서 자동출금</p>
<p>② 처리일자 : 매월10일 (휴,공휴일인 경우 익영업일). 끝.</p>
<div style="text-align:center;padding:20px 0">
<h1>
<span style="letter-spacing:0.8;">주식회사 광주은행</span><br/>
<span style="letter-spacing:6.5;">디지털</span> <span style="letter-spacing:6.5;">사업부장</span>
<small style="position: absolute; bottom: 39px; margin-left: 12px; font-weight: normal; font-size: 13px;">(직인생략)</small>
</h1>
</div>
</div>
<script th:if="${error}" th:inline="javascript">
$(document).ready(function() {
customPopups.showAlert([[${error}]]);
});
</script>
</body>
</html>
@@ -1,121 +1,125 @@
<!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}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
<body>
<section class="content" layout:fragment="contentFragment">
<div class="content_wrap">
<div class="sub_title3">
<h2 class="title">FAQ</h2>
</div>
<div class="inner i_cs h_inner8">
<!-- 검색영역 -->
<th:block layout:fragment="contentFragment">
<!-- FAQ Page -->
<div class="faq-page">
<div class="faq-container">
<!-- Page Header -->
<div class="faq-header">
<h1 class="page-title">FAQ</h1>
<p class="page-description">자주 묻는 질문과 답변을 확인하세요.</p>
</div>
<!-- Search and Filter Controls -->
<form name="articleForm" th:action="@{/faq_list}" method="post" th:object="${search}" onSubmit="fn_search_article(); return false;">
<div class="board_search">
<div class="total-post-wrap">
<span class="total-post pc-only">
<strong th:text="${page.totalElements}"></strong>
</span>
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<input name="page" type="hidden" value="">
<div class="table-controls">
<div class="total-count">
<strong th:text="${page.totalElements}">0</strong>
</div>
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<input name="page" type="hidden" value="">
<div class="search_txt ">
<input type="text" class="common_input_type_1 h_inp2" th:field="*{searchWrd}" placeholder="제목/내용 검색">
</div>
<div class="btn_wrap search_btn">
<button type="button" class="btn_search" onclick="fn_search_article()">검색</button>
<i class="i_search"></i>
<div class="search-box">
<input type="text"
th:field="*{searchWrd}"
placeholder="질문 또는 답변 내용으로 검색">
<button type="button" class="btn btn-primary" onclick="fn_search_article()">
<i class="fas fa-search"></i>
<span>검색</span>
</button>
</div>
</div>
</form>
<div class="total-post-wrap">
<span class="total-post m-only">
<strong th:text="${page.totalElements}"></strong>
</span>
</div>
<!-- // 검색영역 -->
<div class="board-list-wrap notice">
<div class="box-brandInfo gapType-1 faq">
<ul>
<li th:each="faq : ${page.content}">
<div class="bd_txt">
<a href="#none" th:text="${faq.faqQuestion}"></a>
</div>
<div class="box-ct">
<div id="faqAnswer" class="list_txt" th:utext="${faq.faqAnswer}"></div>
</div>
</li>
<li class="no-result-wrap" th:if="${#lists.isEmpty(page.content)}">
<div class="no_data">
<div class="no_result_text m-only">
<img th:src="@{/img/img_nodata.png}" alt="">
<p>조회된 검색 결과가 없습니다.</p>
</div>
<div class="no_result_text pc-only">
<img th:src="@{/img/img_nodata2.png}" alt="">
<p>조회된 검색 결과가 없습니다.</p>
</div>
</div>
</li>
</ul>
<!-- FAQ Accordion -->
<div class="faq-accordion" th:if="${!#lists.isEmpty(page.content)}">
<div class="faq-item" th:each="faq : ${page.content}">
<div class="faq-question">
<div class="faq-question-text" th:text="${faq.faqQuestion}">
질문 내용이 여기에 표시됩니다.
</div>
<i class="faq-icon fas fa-chevron-down"></i>
</div>
<div class="faq-answer">
<div class="faq-answer-content" th:utext="${faq.faqAnswer}">
답변 내용이 여기에 표시됩니다.
</div>
</div>
</div>
</div>
<!-- pagination -->
<!-- Empty State -->
<div class="table-empty" th:if="${#lists.isEmpty(page.content)}">
<div class="empty-icon">
<img th:src="@{/img/img_nodata.png}" alt="데이터 없음">
</div>
<p class="empty-text">조회된 검색 결과가 없습니다.</p>
</div>
<!-- Pagination -->
<div class="pagination" th:replace="~{fragment/pagination :: pagination(jsFunction='fn_select_page')}">
</div>
<!-- // pagination -->
</div>
</div>
</section>
</th:block>
</body>
<th:block layout:fragment="contentScript">
<script>
function fn_init() {
document.inquiryForm.searchWrd.focus();
}
function fn_select_page(pageNo) {
document.articleForm.page.value = pageNo;
document.articleForm.action = '[[@{/faq_list}]]';
document.articleForm.submit();
}
/*********************************************************
* 조회 처리 함수
******************************************************** */
function fn_search_article() {
document.articleForm.page.value = 1;
document.articleForm.submit();
}
function decodeHTMLEntities(text) {
var textArea = document.createElement('textarea');
textArea.innerHTML = text;
return textArea.value;
var textArea = document.createElement('textarea');
textArea.innerHTML = text;
return textArea.value;
}
$(function() {
fn_init();
document.addEventListener('DOMContentLoaded', function() {
// Decode HTML entities in FAQ answers
document.querySelectorAll('.faq-answer-content').forEach(function(element) {
element.innerHTML = decodeHTMLEntities(element.innerHTML);
});
// FAQ 아코디언 기능 추가
$('.box-brandInfo.faq .bd_txt a').click(function(e) {
e.preventDefault();
$(this).closest('li').toggleClass('sel');
$(this).closest('li').find('.box-ct').slideToggle();
// FAQ Accordion functionality
document.querySelectorAll('.faq-question').forEach(function(question) {
question.addEventListener('click', function(e) {
e.preventDefault();
const faqItem = this.closest('.faq-item');
const faqAnswer = faqItem.querySelector('.faq-answer');
const isActive = faqItem.classList.contains('active');
// Toggle active state
faqItem.classList.toggle('active');
// Toggle answer visibility with animation
if (isActive) {
faqAnswer.classList.remove('show');
} else {
faqAnswer.classList.add('show');
}
});
});
});
$('[id="faqAnswer"]').each(function() {
$(this).html(decodeHTMLEntities($(this).html()));
});
</script>
</th:block>
@@ -1,72 +1,110 @@
<!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}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
<body>
<section layout:fragment="contentFragment" class="content">
<th:block layout:fragment="contentFragment">
<div class="content_wrap">
<div class="sub_title3">
<h2 class="title">Q&amp;A</h2>
</div>
<div class="inner i_cs h_inner8 h_inner10">
<!-- Inquiry Detail Page -->
<div class="inquiry-page">
<div class="inquiry-container">
<div class="board-view">
<div class="board-view-head qna">
<div class="complete_box">
<span th:class="${inquiry.inquiryStatus == 'RESPONDED' ? 'complete' : 'question'}" th:text="${inquiry.inquiryStatus =='RESPONDED'?'답변완료': '문의중'}"></span>
</div>
<div class="detail_title">
<strong class="title" th:text="${inquiry.inquirySubject}"></strong>
</div>
<div class="write-info">
<p class="date-info">
<span class="date_stxt">등록일 : <em>[[${#temporals.format(inquiry.createdDate, 'yyyy-MM-dd HH:mm')}]]</em></span>
<span>작성자 : [[${inquiry.maskedInquirerName}]]</span>
</p>
</div>
</div>
<div class="board_file" th:if="${!#strings.isEmpty(inquiry.attachFile)}">
<p class="file_group" th:with="fileInfo=${@fileService.findById(inquiry.attachFile)}">
<span th:each="fileDetail, status : ${fileInfo.getFileDetails()}">
<a th:href="'javascript:fn_downloadFile(\'' + ${fileDetail.fileId} + '\',\''+ ${fileDetail.fileSn} +'\')'">[[${fileDetail.originalFileName}]].[[${fileDetail.fileExtension}]]</a>
<!-- Page Header -->
<div class="inquiry-header">
<h1 class="page-title">Q&A</h1>
<p class="page-description">문의 내용을 확인하세요.</p>
</div>
<!-- Inquiry Detail Card -->
<div class="inquiry-detail-card">
<div class="detail-header">
<div class="status-wrapper">
<span class="status-badge-header"
th:classappend="${inquiry.inquiryStatus == 'RESPONDED' ? 'status-completed' : 'status-pending'}"
th:text="${inquiry.inquiryStatus == 'RESPONDED' ? '답변완료' : '문의중'}">
문의중
</span>
</p>
</div>
<h2 class="inquiry-title" th:text="${inquiry.inquirySubject}">질문 제목</h2>
<div class="inquiry-meta">
<div class="meta-item">
<i class="fas fa-calendar-alt"></i>
<span>등록일</span>
<strong th:text="${#temporals.format(inquiry.createdDate, 'yyyy-MM-dd HH:mm')}">2025.01.01 10:00</strong>
</div>
<div class="meta-item">
<i class="fas fa-user"></i>
<span>작성자</span>
<strong th:text="${inquiry.maskedInquirerName}">홍**</strong>
</div>
</div>
</div>
<div class="board-view-body" style="white-space: pre-line;">
<!-- Attachment Section -->
<div class="detail-attachment" th:if="${!#strings.isEmpty(inquiry.attachFile)}">
<div class="attachment-icon">
<i class="fas fa-paperclip"></i>
</div>
<div class="attachment-list" th:with="fileInfo=${@fileService.findById(inquiry.attachFile)}">
<a class="attachment-item"
th:each="fileDetail : ${fileInfo.getFileDetails()}"
th:href="'javascript:fn_downloadFile(\\'' + ${fileDetail.fileId} + '\\',\\''+ ${fileDetail.fileSn} +'\\')'">
<i class="fas fa-file"></i>
<span>[[${fileDetail.originalFileName}]].[[${fileDetail.fileExtension}]]</span>
</a>
</div>
</div>
<!-- Inquiry Content -->
<div class="detail-content" style="white-space: pre-line;">
<th:block th:utext="${inquiry.inquiryDetail}"></th:block>
</div>
</div>
<div class="comment_wrap" th:if="${inquiry.inquiryStatus == 'RESPONDED'}">
<div class="comment_wrap_head">
<strong class="title">
[[${inquiry.responderName}]] <em>(등록일 : [[${#temporals.format(inquiry.responseDate, 'yyyy-MM-dd HH:mm')}]])</em>
</strong>
<div class="m-only">
<th:block class="responseDetail" th:utext="${inquiry.responseDetail}"></th:block>
<!-- Admin Response Card -->
<div class="inquiry-response-card" th:if="${inquiry.inquiryStatus == 'RESPONDED'}">
<div class="response-header">
<div class="response-title">
<i class="fas fa-comment-dots"></i>
<span>관리자 답변</span>
</div>
<div class="pc-only">
<th:block class="responseDetail pc-only" th:utext="${inquiry.responseDetail}"></th:block>
<div class="response-meta">
<span th:text="${inquiry.responderName}">관리자</span>
<span class="meta-separator"></span>
<span th:text="${#temporals.format(inquiry.responseDate, 'yyyy-MM-dd HH:mm')}">2025.01.02 14:30</span>
</div>
</div>
<div class="response-content">
<th:block class="responseDetail" th:utext="${inquiry.responseDetail}"></th:block>
</div>
</div>
<div class="qna_btn_group">
<th:block th:if="${inquiry.inquiryStatus != 'RESPONDED' && inquiry.inquirer.id == #authentication.principal.id}">
<form id="deleteForm" th:action="@{/inquiry/{id}/delete(id=${inquiry.id})}" method="post" style="display: inline;"></form>
<a th:href="@{/inquiry/edit(id=${inquiry.id})}" class="common_btn_type_1 btn_edit">수정</a>
<a href="#" class="common_btn_type_1 btn_delete" onclick="confirmDelete(); return false;">삭제</a>
</th:block>
</div>
<div class="btn_board btn_top" >
<a href="/inquiry" class="common_btn_type_1">목록</a>
</div>
</div>
<!-- Action Buttons -->
<div class="inquiry-actions">
<div class="actions-left">
<a th:href="@{/inquiry}" class="action-btn-secondary">
<i class="fas fa-list"></i>
<span>목록</span>
</a>
</div>
<div class="actions-right" th:if="${inquiry.inquiryStatus != 'RESPONDED' && inquiry.inquirer.id == #authentication.principal.id}">
<form id="deleteForm" th:action="@{/inquiry/{id}/delete(id=${inquiry.id})}" method="post" style="display: inline;"></form>
<a th:href="@{/inquiry/edit(id=${inquiry.id})}" class="action-btn-edit">
<i class="fas fa-edit"></i>
<span>수정</span>
</a>
<a href="#" class="action-btn-delete" onclick="confirmDelete(); return false;">
<i class="fas fa-trash-alt"></i>
<span>삭제</span>
</a>
</div>
</div>
</div>
</div>
</section>
</th:block>
</body>
<th:block layout:fragment="contentScript">
<script th:inline="javascript">
@@ -1,153 +1,196 @@
<!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}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
<body>
<section layout:fragment="contentFragment" th:with="isNew=${inquiry.id == null}" class="content">
<th:block layout:fragment="contentFragment" th:with="isNew=${inquiry.id == null}">
<div class="content_wrap">
<div class="sub_title3">
<h2 class="title" th:text="${isNew ? 'Q&amp;A 질문하기' : 'Q&amp;A 질문 수정'}"></h2>
</div>
<div class="inner i_cs h_inner5">
<form name="inquiryForm" id="inquiryForm" th:action="${isNew}? @{/inquiry} : @{/inquiry/edit}"
th:object="${inquiry}" enctype="multipart/form-data" method="post">
<input type="hidden" th:field="*{id}" th:if="${!isNew}">
<div class="form_type">
<div class="info1 form_top">
<p class="title w_tit">
<span class="dot">제목</span>
</p>
<div class="info_line info_add">
<div class="info_box1 w_inp4">
<input type="text" class="common_input_type_1" placeholder="제목" id="inquirySubject"
th:field="*{inquirySubject}" required>
</div>
</div>
</div>
<!-- Inquiry Form Page -->
<div class="inquiry-page">
<div class="inquiry-container">
<div class="info1">
<p class="title w_tit">
<span class="dot">내용</span>
</p>
<div class="info_line info_add">
<div class="info_box1 qna_text w_inp4">
<textarea name="text" id="inquiryDetail" rows="5" cols="30" class="common_textareaType_1 f_inp"
th:field="*{inquiryDetail}" placeholder="내용"></textarea>
<!-- <span>158/1,000 byte</span> -->
</div>
</div>
</div>
<!-- Page Header -->
<div class="inquiry-header">
<h1 class="page-title" th:text="${isNew ? 'Q&A 질문하기' : 'Q&A 질문 수정'}">Q&A 질문하기</h1>
<p class="page-description">궁금하신 사항을 작성해 주세요.</p>
</div>
<div class="info1 pt28">
<p class="title tit_add w_tit"> 첨부파일 </p>
<div class="info_line info_add">
<div class="info_box1 info_add2">
<!-- Inquiry Form Card -->
<div class="inquiry-form-card">
<form name="inquiryForm" id="inquiryForm" th:action="${isNew}? @{/inquiry} : @{/inquiry/edit}"
th:object="${inquiry}" enctype="multipart/form-data" method="post">
<input type="hidden" th:field="*{id}" th:if="${!isNew}">
<div class="filebox">
<input type="text" class="upload-name before"
th:class="${!#strings.isEmpty(inquiry.attachFile)} ? 'upload-name i_trash after' : 'upload-name before'"
th:value="${!#strings.isEmpty(inquiry.attachFile)} ? ${@fileService.findById(inquiry.attachFile).fileDetails[0].originalFileName + '.' + @fileService.findById(inquiry.attachFile).fileDetails[0].fileExtension}"
readonly="readonly">
<input type="hidden" th:field="*{attachFile}">
</div>
<div class="btn_check">
<div class="file_btn">
<label for="file">파일선택</label>
<input type="file" id="file" name="files"
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.hwp,.gif,.jpg,.png">
</div>
</div>
</div>
<!-- Subject Field -->
<div class="form-group">
<label for="inquirySubject" class="form-label required">
<span>제목</span>
</label>
<div class="form-input-wrapper">
<input type="text"
id="inquirySubject"
th:field="*{inquirySubject}"
class="form-input"
placeholder="제목을 입력하세요"
required>
</div>
</div>
<div class="list-dot">
<p th:text="'첨부파일은 ' + ${@portalProperties.file.maxSize} + ' 이내로 등록해 주세요.'">첨부파일은 8MB 이내로 등록해 주세요.</p>
<p>문서파일과 이미지 파일만 등록 가능합니다. (pdf, doc, docx, xls, xlsx, ppt, pptx, hwp, gif, jpg,
png)</p>
</div>
</div>
</div>
<!-- Content Field -->
<div class="form-group">
<label for="inquiryDetail" class="form-label required">
<span>내용</span>
</label>
<div class="form-input-wrapper">
<textarea id="inquiryDetail"
th:field="*{inquiryDetail}"
class="form-textarea"
rows="10"
placeholder="내용을 입력하세요"
required></textarea>
</div>
</div>
<!-- File Upload Field -->
<div class="form-group">
<label for="file" class="form-label">
<span>첨부파일</span>
</label>
<div class="form-input-wrapper">
<div class="file-upload-wrapper">
<input type="text"
class="form-input file-name-display"
th:class="${!#strings.isEmpty(inquiry.attachFile)} ? 'form-input file-name-display has-file' : 'form-input file-name-display'"
th:value="${!#strings.isEmpty(inquiry.attachFile)} ? ${@fileService.findById(inquiry.attachFile).fileDetails[0].originalFileName + '.' + @fileService.findById(inquiry.attachFile).fileDetails[0].fileExtension} : ''"
placeholder="파일을 선택하세요"
readonly>
<input type="hidden" th:field="*{attachFile}">
<label for="file" class="file-upload-btn">
<i class="fas fa-paperclip"></i>
<span>파일 선택</span>
</label>
<input type="file"
id="file"
name="files"
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.hwp,.gif,.jpg,.png"
style="display: none;">
<button type="button" class="file-remove-btn" style="display: none;">
<i class="fas fa-times"></i>
</button>
</div>
<div class="form-help-text">
<!-- <p><i class="fas fa-info-circle"></i> 첨부파일은 10MB 이내로 등록해 주세요.</p>-->
<!-- <p><i class="fas fa-info-circle"></i> 문서파일과 이미지 파일만 등록 가능합니다. (pdf, doc, docx, xls, xlsx, ppt, pptx, hwp, gif, jpg, png)</p>-->
<p><i class="fas fa-check-circle"></i>
<span th:text="'첨부파일은 ' + ${@portalProperties.file.maxSize} + ' 이내로 등록해 주세요.'">첨부파일은 8MB 이내로 등록해 주세요.</span>
</p>
<p>
<i class="fas fa-check-circle"></i>
문서파일과 이미지 파일만 등록 가능합니다. (pdf, doc, docx, xls, xlsx, ppt, pptx, hwp, gif, jpg, png)
</p>
</div>
</div>
</div>
<!-- Form Actions -->
<div class="inquiry-form-actions">
<a th:href="@{/inquiry}" class="action-btn-secondary">
<i class="fas fa-times"></i>
<span>취소</span>
</a>
<button type="button" class="action-btn-primary btn-submit">
<i class="fas fa-check"></i>
<span>확인</span>
</button>
</div>
</form>
</div>
<div class="btn_application btn_top3">
<a th:href="@{/inquiry}" class="common_btn_type_1 gray"><span>취소</span></a>
<div class="btn_gap"></div>
<a href="#none" class="common_btn_type_1 btn_register"><span>확인</span></a>
</div>
</div>
</form>
</div>
</div>
</section>
</div>
</th:block>
</body>
<th:block layout:fragment="contentScript">
<script th:if="${error}" th:inline="javascript">
$(document).ready(function () {
const errorMsg = [[${error}]];
const formattedMsg = errorMsg.replace(/\n/g,'<br>');
$('#customAlertMessage').html(formattedMsg);
$('#customAlert').css('display','flex');
});
</script>
<script th:inline="javascript">
$(function () {
let fnRegister = document.querySelector('.btn_register');
<script th:if="${error}" th:inline="javascript">
$(document).ready(function () {
const errorMsg = [[${error}]];
const formattedMsg = errorMsg.replace(/\n/g,'<br>');
$('#customAlertMessage').html(formattedMsg);
$('#customAlert').css('display','flex');
});
</script>
<script th:inline="javascript">
$(function () {
let btnSubmit = document.querySelector('.btn-submit');
let fileInput = document.getElementById('file');
let fileNameDisplay = document.querySelector('.file-name-display');
let fileRemoveBtn = document.querySelector('.file-remove-btn');
fnRegister.addEventListener('click', function (event) {
// Form submission handler
btnSubmit.addEventListener('click', function (event) {
event.preventDefault();
let form = document.getElementById('inquiryForm');
let inquirySubject = document.getElementById('inquirySubject').value.trim();
let inquiryDetail = document.getElementById('inquiryDetail').value;
let form = document.getElementById('inquiryForm');
let inquirySubject = document.getElementById('inquirySubject').value.trim();
let inquiryDetail = document.getElementById('inquiryDetail').value;
// validation 체크
if (!inquirySubject) {
customPopups.showAlert('제목을 작성해 주세요.');
document.getElementById('inquirySubject').focus();
return;
}
if (!inquiryDetail.trim()) {
customPopups.showAlert('내용을 작성해 주세요.');
document.getElementById('inquiryDetail').focus();
return false;
}
// Validation
if (!inquirySubject) {
customPopups.showAlert('제목을 작성해 주세요.');
document.getElementById('inquirySubject').focus();
return;
}
if (!inquiryDetail.trim()) {
customPopups.showAlert('내용을 작성해 주세요.');
document.getElementById('inquiryDetail').focus();
return false;
}
form.submit();
});
form.submit();
});
document.getElementById('inquiryForm').inquirySubject.focus();
const fileInput = document.getElementById('file');
const fileNameInput = document.querySelector('.upload-name');
// File input change handler
fileInput.addEventListener('change', function () {
const file = fileInput.files[0];
fileInput.addEventListener('change', function () {
const file = fileInput.files[0];
if (file) {
const allowedExtensions = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.hwp', '.gif', '.jpg', '.jpeg', '.png'];
const fileExt = '.' + file.name.split('.').pop().toLowerCase();
if (file) {
// FileValidator를 사용한 파일 검증
if (!FileValidator.validateFile(file)) {
fileInput.value = '';
fileNameInput.value = '';
fileNameInput.classList.remove('i_trash', 'after');
fileNameInput.classList.add('before');
return;
}
if (!allowedExtensions.includes(fileExt)) {
customPopups.showAlert("허용된 파일 형식이 아닙니다.\n문서파일(pdf, doc, docx, xls, xlsx, ppt, pptx, hwp)과 이미지 파일(gif, jpg, png)만 등록 가능합니다.");
fileInput.value = '';
fileNameDisplay.value = '';
fileNameDisplay.classList.remove('has-file');
fileRemoveBtn.style.display = 'none';
return;
}
fileNameInput.value = file.name; // Set the file name
fileNameInput.classList.remove('before'); // Remove the 'before' class
fileNameInput.classList.add('i_trash', 'after'); // Add 'i_trash' and 'after' classes
}
});
fileNameDisplay.value = file.name;
fileNameDisplay.classList.add('has-file');
fileRemoveBtn.style.display = 'flex';
}
});
// Handle file removal when upload-name is clicked
fileNameInput.addEventListener('click', function () {
if (fileNameInput.classList.contains('i_trash')) {
fileInput.value = ''; // Clear the file input
fileNameInput.value = ''; // Reset the file name input
fileNameInput.classList.remove('i_trash', 'after'); // Remove the 'i_trash' and 'after' classes
fileNameInput.classList.add('before'); // Add the 'before' class back
}
});
});
</script>
// File remove handler
fileRemoveBtn.addEventListener('click', function () {
fileInput.value = '';
fileNameDisplay.value = '';
fileNameDisplay.classList.remove('has-file');
fileRemoveBtn.style.display = 'none';
});
// Show remove button if file already exists
if (fileNameDisplay.value) {
fileRemoveBtn.style.display = 'flex';
}
// Focus on subject field
document.getElementById('inquirySubject').focus();
});
</script>
</th:block>
</html>
@@ -1,101 +1,106 @@
<!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}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
<body>
<section class="content" layout:fragment="contentFragment">
<div class="content_wrap">
<div class="sub_title3">
<h2 class="title">Q&amp;A</h2>
</div>
<div class="inner i_cs h_inner8">
<th:block layout:fragment="contentFragment">
<!-- Inquiry List Page -->
<div class="inquiry-page">
<div class="inquiry-container">
<!-- 검색영역 -->
<!-- Page Header -->
<div class="inquiry-header">
<h1 class="page-title">Q&A</h1>
<p class="page-description">궁금하신 사항을 문의하고 답변을 확인하세요.</p>
</div>
<!-- Search and Filter Controls -->
<form name="inquiryForm" th:action="@{/inquiry}" method="get" onsubmit="return false;">
<div class="board_search">
<div class="total-post-wrap">
<span class="total-post pc-only">
<strong th:text="${page.totalElements}"></strong>
</span>
<input type="hidden" name="page" value="1">
<input name="searchCnd" type="hidden" value="0">
<div class="table-controls">
<div class="total-count">
<strong th:text="${page.totalElements}">0</strong>
</div>
<input type="hidden" name="page" value="1">
<input name="searchCnd" type="hidden" value="0">
<div class="search_txt ">
<input type="text" class="common_input_type_1 h_inp2" name="searchWrd"
th:value="${search.searchWrd}" placeholder="제목/내용 검색">
</div>
<div class="btn_wrap search_btn">
<button type="button" class="btn_search" onclick="fn_search_inquiry()">검색</button>
<i class="i_search"></i>
<div class="search-box">
<input type="text"
name="searchWrd"
th:value="${search.searchWrd}"
placeholder="제목 또는 내용으로 검색">
<button type="button" class="btn btn-primary" onclick="fn_search_inquiry()">
<i class="fas fa-search"></i>
<span>검색</span>
</button>
</div>
</div>
</form>
<div class="total-post-wrap">
<span class="total-post m-only">
<strong th:text="${page.totalElements}"></strong>
</span>
</div>
<!-- // 검색영역 -->
<div class="board-list-wrap notice">
<ul class="board-list qna_list">
<li class="th-head">
<div>
<span class="col-num">No</span>
<span class="col-title">제목</span>
<span class="col-name">작성자</span>
<span class="col-comp">처리상태</span>
<span class="col-date">등록일</span>
</div>
</li>
<li th:each="inquiry, status : ${inquiries}">
<div class="td-body">
<span class="col-num"
th:text="${page.totalElements - (page.number * page.size) - status.index}"></span>
<span class="col-title">
<em class="qna-type">
<a th:href="@{/inquiry/detail(id=${inquiry.id})}">[[${inquiry.inquirySubject}]]<span
th:if="${#strings.isEmpty(inquiry.attachFile) == false}" class="icon_file">
<img th:src="@{/img/icon/icon_file.png}" alt=""></span></a>
</em>
</span>
<span class="col-name" th:text="${inquiry.maskedInquirerName}"></span>
<span class="col-comp">
<span th:class="${inquiry.inquiryStatus == 'RESPONDED' ? 'complete' : 'question'}"
th:text="${inquiry.inquiryStatus =='RESPONDED'?'답변완료': '문의중'}"></span>
</span>
<span class="col-date"
th:text="${#temporals.format(inquiry.createdDate, 'yyyy.MM.dd')}"></span>
</div>
</li>
<li class="no-result-wrap" th:if="${inquiries.isEmpty()}">
<div class="no_data">
<div class="no_result_text m-only">
<img th:src="@{/img/img_nodata.png}" alt="">
<p>조회된 검색 결과가 없습니다.</p>
</div>
<div class="no_result_text pc-only">
<img th:src="@{/img/img_nodata2.png}" alt="">
<p>조회된 검색 결과가 없습니다.</p>
</div>
</div>
</li>
</ul>
</div>
<!-- Inquiry Table -->
<div class="inquiry-list-wrapper" th:if="${!inquiries.isEmpty()}">
<div class="inquiry-table">
<!-- Table Header -->
<div class="table-header">
<div class="col-num">NO</div>
<div class="col-title">제목</div>
<div class="col-name">작성자</div>
<div class="col-status">처리상태</div>
<div class="col-date">등록일</div>
</div>
<div class="pg_btn">
<!-- pagination -->
<div class="pagination" th:replace="~{fragment/pagination :: pagination(jsFunction='fn_select_page')}">
</div>
<!-- // pagination -->
<div class="btn_board btn_top">
<a th:href="@{/inquiry/new}" class="common_btn_type_1 btn_qna">질문하기</a>
<!-- Table Rows -->
<div class="table-row"
th:each="inquiry, status : ${inquiries}"
th:onclick="'location.href=\'' + @{/inquiry/detail(id=${inquiry.id})} + '\''">
<div class="col-num"
th:text="${page.totalElements - (page.number * page.size) - status.index}">1</div>
<div class="col-title">
<a th:href="@{/inquiry/detail(id=${inquiry.id})}">
<span th:text="${inquiry.inquirySubject}">질문 제목</span>
<span class="file-icon" th:if="${!#strings.isEmpty(inquiry.attachFile)}">
<img th:src="@{/img/icon/icon_file.png}" alt="첨부파일">
</span>
</a>
</div>
<div class="col-name" th:text="${inquiry.maskedInquirerName}">홍**</div>
<div class="col-status">
<span class="status-badge"
th:classappend="${inquiry.inquiryStatus == 'RESPONDED' ? 'status-completed' : 'status-pending'}"
th:text="${inquiry.inquiryStatus == 'RESPONDED' ? '답변완료' : '문의중'}">
문의중
</span>
</div>
<div class="col-date"
th:text="${#temporals.format(inquiry.createdDate, 'yyyy.MM.dd')}">2025.01.01</div>
</div>
</div>
</div>
<!-- Empty State -->
<div class="table-empty" th:if="${inquiries.isEmpty()}">
<div class="empty-icon">
<img th:src="@{/img/img_nodata.png}" alt="데이터 없음">
</div>
<p class="empty-text">조회된 검색 결과가 없습니다.</p>
</div>
<!-- Pagination -->
<div class="pagination" th:replace="~{fragment/pagination :: pagination(jsFunction='fn_select_page')}">
</div>
<!-- New Inquiry Button -->
<div class="inquiry-actions">
<a th:href="@{/inquiry/new}" class="btn btn-primary">
<i class="fas fa-pen"></i>
<span>질문하기</span>
</a>
</div>
</div>
</div>
</section>
</th:block>
</body>
@@ -1,42 +1,64 @@
<!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}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
<body>
<section class="content" layout:fragment="contentFragment">
<div class="content_wrap">
<div class="sub_title3">
<h2 class="title">공지사항</h2>
</div>
<div class="inner i_cs h_inner8">
<div class="board-view">
<div class="board-view-head">
<strong class="title" th:text="${portalNotice.noticeSubject}"></strong>
<div class="write-info">
<p class="date-info">
<span>등록일 <em th:text="${#temporals.format(portalNotice.createdDate, 'yyyy.MM.dd')}"></em></span>
</p>
<th:block layout:fragment="contentFragment">
<!-- Notice Detail Page -->
<div class="notice-detail-page">
<div class="notice-detail-container">
<!-- Page Header -->
<div class="notice-header">
<h1 class="page-title">공지사항</h1>
</div>
<!-- Notice Detail Card -->
<div class="notice-detail-card">
<!-- Header Section -->
<div class="detail-header">
<h2 class="notice-title" th:text="${portalNotice.noticeSubject}">공지사항 제목</h2>
<div class="notice-meta">
<div class="meta-item">
<i class="fas fa-calendar-alt"></i>
<span>등록일</span>
<strong th:text="${#temporals.format(portalNotice.createdDate, 'yyyy.MM.dd')}">2025.01.01</strong>
</div>
</div>
</div>
<div class="board_file" th:if="${!#strings.isEmpty(portalNotice.fileId)}">
<p class="file_group" th:with="fileInfo=${@fileService.findById(portalNotice.fileId)}">
<span th:each="fileDetail, status : ${fileInfo.getFileDetails()}">
<a th:href="'javascript:fn_downloadFile(\'' + ${fileDetail.fileId} + '\',\''+ ${fileDetail.fileSn} +'\')'">첨부파일 : [[${fileDetail.originalFileName}]].[[${fileDetail.fileExtension}]]</a>
</span>
</p>
<!-- Attachments Section -->
<div class="detail-attachments" th:if="${!#strings.isEmpty(portalNotice.fileId)}">
<div class="attachment-list" th:with="fileInfo=${@fileService.findById(portalNotice.fileId)}">
<div class="attachment-item" th:each="fileDetail, status : ${fileInfo.getFileDetails()}">
<a th:href="'javascript:fn_downloadFile(\'' + ${fileDetail.fileId} + '\',\''+ ${fileDetail.fileSn} +'\')'">
<i class="fas fa-paperclip"></i>
<span>[[${fileDetail.originalFileName}]].[[${fileDetail.fileExtension}]]</span>
</a>
</div>
</div>
</div>
<div class="board-view-body">
<div id="noticeDetail" th:utext="${portalNotice.noticeDetail}"></div>
<!-- Content Section -->
<div class="detail-content">
<div id="noticeDetail" class="content-body" th:utext="${portalNotice.noticeDetail}">
공지사항 내용이 여기에 표시됩니다.
</div>
</div>
</div>
<div class="btn_board btn_top">
<a href="#" class="common_btn_type_1" th:onclick="|window.location.href='@{/portalnotice}';|">목록</a>
<!-- Action Buttons -->
<div class="notice-actions">
<a href="#" class="action-btn btn-list" th:onclick="|window.location.href='@{/portalnotice}';|">
<i class="fas fa-list"></i>
<span>목록으로</span>
</a>
</div>
</div>
</div>
</section>
</th:block>
</body>
@@ -1,90 +1,86 @@
<!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}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
<body>
<section class="content" layout:fragment="contentFragment">
<div class="content_wrap">
<div class="sub_title3">
<h2 class="title">공지사항</h2>
</div>
<div class="inner i_cs4 h_inner8 h_inner10">
<th:block layout:fragment="contentFragment">
<!-- Notice List Page -->
<div class="notice-page">
<div class="notice-container">
<!-- 검색영역 -->
<!-- Page Header -->
<div class="notice-header">
<h1 class="page-title">공지사항</h1>
<p class="page-description">광주은행 API Portal의 새로운 소식과 공지사항을 확인하세요.</p>
</div>
<!-- Search and Filter Controls -->
<form name="noticeForm" th:action="@{/portalnotice}" method="get" onsubmit="return false;">
<div class="board_search">
<div class="total-post-wrap">
<span class="total-post pc-only">
<strong th:text="${page.totalElements}"></strong>
</span>
<input type="hidden" name="page" value="1">
<div class="table-controls">
<div class="total-count">
<strong th:text="${page.totalElements}">0</strong>
</div>
<input type="hidden" name="page" value="1">
<div class="search_txt">
<input type="text" class="common_input_type_1 h_inp2" name="searchSubjectOrDetail" th:value="${search.searchSubjectOrDetail}" placeholder="제목/내용 검색">
</div>
<div class="btn_wrap search_btn">
<button type="button" class="btn_search" onclick="fn_search_notice()">검색</button>
<i class="i_search"></i>
<div class="search-box">
<input type="text"
name="searchSubjectOrDetail"
th:value="${search.searchSubjectOrDetail}"
placeholder="제목 또는 내용으로 검색">
<button type="button" class="btn btn-primary" onclick="fn_search_notice()">
<i class="fas fa-search"></i>
<span>검색</span>
</button>
</div>
</div>
</form>
<div class="total-post-wrap">
<span class="total-post m-only">
<strong th:text="${page.totalElements}"></strong>
</span>
</div>
<!-- // 검색영역 -->
<div class="board-list-wrap notice">
<ul class="board-list notice-type">
<li class="th-head">
<div>
<span class="col-num">NO</span>
<span class="col-title">제목</span>
<span class="col-date">등록일</span>
<!-- Notice Table -->
<div class="notice-list-wrapper" th:if="${!portalNotices.isEmpty()}">
<div class="notice-table">
<!-- Table Header -->
<div class="table-header">
<div class="col-num">NO</div>
<div class="col-title">제목</div>
<div class="col-date">등록일</div>
</div>
<!-- Table Rows -->
<div class="table-row"
th:each="notice, status : ${portalNotices}"
th:onclick="'location.href=\'' + @{/portalnotice/detail(id=${notice.id})} + '\''">
<div class="col-num"
th:text="${page.totalElements - (page.number * page.size) - status.index}">1</div>
<div class="col-title">
<a th:href="@{/portalnotice/detail(id=${notice.id})}">
<span th:text="${notice.noticeSubject}">공지사항 제목</span>
<span class="file-icon" th:if="${notice.fileId != null and !notice.fileId.isEmpty()}">
<img th:src="@{/img/icon/icon_file.png}" alt="첨부파일">
</span>
</a>
</div>
</li>
<li th:each="notice, status : ${portalNotices}">
<div>
<span class="col-num" th:text="${page.totalElements - (page.number * page.size) - status.index}"></span>
<span class="col-title icon_file">
<em>
<a th:href="@{/portalnotice/detail(id=${notice.id})}">
<span th:text="${notice.noticeSubject}"></span>
<span class="icon_file" th:if="${notice.fileId != null and !notice.fileId.isEmpty()}">
<img th:src="@{/img/icon/icon_file.png}" alt="첨부파일">
</span>
</a>
</em>
</span>
<span class="col-date" th:text="${#temporals.format(notice.createdDate, 'yyyy.MM.dd')}"></span>
</div>
</li>
<li class="no-result-wrap" th:if="${portalNotices.isEmpty()}">
<div class="no_data">
<div class="no_result_text">
<div class="no_result_text m-only">
<img th:src="@{/img/img_nodata.png}" alt="">
<p>조회된 데이터가 없습니다.</p>
</div>
<div class="no_result_text pc-only">
<img th:src="@{/img/img_nodata2.png}" alt="">
<p>조회된 데이터가 없습니다.</p>
</div>
</div>
</div>
</li>
</ul>
<div class="col-date"
th:text="${#temporals.format(notice.createdDate, 'yyyy.MM.dd')}">2025.01.01</div>
</div>
</div>
</div>
<!-- pagination -->
<!-- Empty State -->
<div class="table-empty" th:if="${portalNotices.isEmpty()}">
<div class="empty-icon">
<img th:src="@{/img/img_nodata.png}" alt="데이터 없음">
</div>
<p class="empty-text">조회된 데이터가 없습니다.</p>
</div>
<!-- Pagination -->
<div class="pagination" th:replace="~{fragment/pagination :: pagination(jsFunction='fn_select_page')}">
</div>
<!-- // pagination -->
</div>
</div>
</section>
</th:block>
</body>
@@ -1,77 +1,119 @@
<!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}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
<body>
<section class="content" layout:fragment="contentFragment">
<div class="content_wrap">
<div class="sub_title3">
<h2 class="title">사업 제휴 신청</h2>
</div>
<div class="inner i_cs h_inner6">
<div class="con_title f_tit">
<p>Kbank는 온라인 비즈니스 혁신을 위한 <span>사업 제안을 환영</span>합니다.</p>
<!-- Page Header -->
<div class="page-header">
<div class="page-header-content">
<h1 class="page-title">
<i class="fas fa-handshake"></i>
사업 제휴 신청
</h1>
<p class="page-description">
광주은행은 온라인 비즈니스 혁신을 위한 <span class="highlight">사업 제안을 환영</span>합니다.
</p>
</div>
</div>
<div class="form_type">
<!-- Partnership Form Card -->
<div class="partnership-form-container">
<div class="card partnership-card">
<form name="partnershipForm" id="partnershipForm" th:action="@{/partnership}" th:object="${partnership}"
enctype="multipart/form-data" method="post">
<div class="info1">
<p class="title w_tit">
<span class="dot">사업 제목</span>
</p>
<div class="info_line info_add">
<div class="info_box1 w_inp4">
<input type="text" class="common_input_type_1" id="bizSubject" th:field="*{bizSubject}"
placeholder="제안하고자 하시는 사업제목" required>
</div>
</div>
<div class="invalid-feedback" th:if="${#fields.hasErrors('bizSubject')}"
th:errors="*{bizSubject}"></div>
</div>
<div class="info1">
<p class="title w_tit">
<span class="dot">사업 내용</span>
</p>
<div class="info_line info_add">
<div class="info_box1 qna_text w_inp4">
<textarea class="common_textareaType_1 f_inp" id="bizDetail" th:field="*{bizDetail}"
placeholder="제안하고자 하시는 사업내용" required></textarea>
</div>
</div>
<div class="invalid-feedback" th:if="${#fields.hasErrors('bizDetail')}"
th:errors="*{bizDetail}"></div>
</div>
<div class="info1">
<p class="title tit_add w_tit">첨부파일</p>
<div class="info_line info_add">
<div class="info_box1 info_add2">
<div class="filebox">
<input class="upload-name" value="첨부파일을 등록 해 주세요" disabled="disabled">
</div>
<div class="btn_check">
<div class="file_btn">
<label for="file">파일선택</label>
<input type="file" id="file" name="files" accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.hwp,.gif,.jpg,.png">
</div>
</div>
</div>
<div class="list-dot">
<p th:text="'첨부파일은 ' + ${@portalProperties.file.maxSize} + ' 이내로 등록해 주세요.'">첨부파일은 8MB 이내로 등록해 주세요.</p>
<p>문서파일과 이미지 파일만 등록 가능합니다. (pdf, doc, docx, xls, xlsx, ppt, pptx, hwp, gif, jpg,
png)</p>
</div>
<!-- 사업 제목 -->
<div class="form-group">
<label for="bizSubject">
<i class="fas fa-lightbulb"></i>
사업 제목
<span class="required">*</span>
</label>
<input type="text"
class="form-control"
id="bizSubject"
th:field="*{bizSubject}"
placeholder="제안하고자 하시는 사업 제목을 입력해 주세요"
required>
<div class="form-error" th:if="${#fields.hasErrors('bizSubject')}" th:errors="*{bizSubject}">
<i class="fas fa-exclamation-circle"></i>
<span></span>
</div>
</div>
<div class="btn_application m_top">
<a th:href="@{/}" id="cancelButton" class="common_btn_type_1 gray"><span>취소</span></a>
<div class="btn_gap"></div>
<button type="button" class="common_btn_type_1 btn_register"><span>제휴신청</span></button>
<!-- 사업 내용 -->
<div class="form-group">
<label for="bizDetail">
<i class="fas fa-file-alt"></i>
사업 내용
<span class="required">*</span>
</label>
<textarea class="form-control"
id="bizDetail"
th:field="*{bizDetail}"
rows="8"
placeholder="제안하고자 하시는 사업 내용을 상세히 입력해 주세요"
required></textarea>
<div class="form-hint">
<i class="fas fa-info-circle"></i>
사업의 목적, 내용, 기대 효과 등을 구체적으로 작성해 주세요
</div>
<div class="form-error" th:if="${#fields.hasErrors('bizDetail')}" th:errors="*{bizDetail}">
<i class="fas fa-exclamation-circle"></i>
<span></span>
</div>
</div>
<!-- 첨부파일 -->
<div class="form-group">
<label>
<i class="fas fa-paperclip"></i>
첨부파일
</label>
<div class="file-upload-wrapper">
<input type="text"
class="file-name-display"
value="첨부파일을 등록해 주세요"
readonly>
<label for="file" class="btn btn-primary">
<i class="fas fa-upload"></i>
파일 선택
</label>
<button type="button" class="file-remove-btn" style="display: none;">
<i class="fas fa-times"></i>
</button>
<input type="file"
id="file"
name="files"
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.hwp,.gif,.jpg,.png">
</div>
<div class="form-help-text">
<!-- <p><i class="fas fa-check-circle"></i> 첨부파일은 10MB 이내로 등록해 주세요</p>-->
<!-- <p><i class="fas fa-check-circle"></i> 문서파일과 이미지 파일만 등록 가능합니다 (pdf, doc, docx, xls, xlsx, ppt, pptx, hwp, gif, jpg, png)</p>-->
<p><i class="fas fa-check-circle"></i>
<span th:text="'첨부파일은 ' + ${@portalProperties.file.maxSize} + ' 이내로 등록해 주세요.'">첨부파일은 8MB 이내로 등록해 주세요.</span>
</p>
<p>
<i class="fas fa-check-circle"></i>
문서파일과 이미지 파일만 등록 가능합니다. (pdf, doc, docx, xls, xlsx, ppt, pptx, hwp, gif, jpg, png)
</p>
</div>
</div>
<!-- 버튼 영역 -->
<div class="form-actions">
<a th:href="@{/}" id="cancelButton" class="action-btn-secondary">
<i class="fas fa-times"></i>
취소
</a>
<button type="button" class="btn btn-primary btn_register">
<i class="fas fa-paper-plane"></i>
제휴 신청
</button>
</div>
</form>
</div>
@@ -97,17 +139,21 @@
</script>
<script th:inline="javascript">
$(function () {
let fnRegister = document.querySelector('.btn_register');
let cancelButton = document.getElementById('cancelButton');
const fnRegister = document.querySelector('.btn_register');
const fileInput = document.getElementById('file');
const fileNameInput = document.querySelector('.file-name-display');
const fileRemoveBtn = document.querySelector('.file-remove-btn');
const allowFileExtention = ['pdf','doc','docx','xls','xlsx','ppt','pptx','hwp','gif','jpg','png'];
// Form submission
fnRegister.addEventListener('click', function (event) {
event.preventDefault();
let form = document.getElementById('partnershipForm');
let bizSubject = document.getElementById('bizSubject').value.trim();
let bizDetail = document.getElementById('bizDetail').value.trim();
const form = document.getElementById('partnershipForm');
const bizSubject = document.getElementById('bizSubject').value.trim();
const bizDetail = document.getElementById('bizDetail').value.trim();
// validation 체크
// Validation
if (!bizSubject) {
customPopups.showAlert('사업 제목을 작성해 주세요.');
document.getElementById('bizSubject').focus();
@@ -123,34 +169,54 @@
form.submit();
});
document.getElementById('partnershipForm').bizSubject.focus();
const fileInput = document.getElementById('file');
const fileNameInput = document.querySelector('.upload-name');
// Auto focus on first field
document.getElementById('bizSubject').focus();
// File validation functions
function isValidFileExtention(filename) {
const extention = filename.split('.').pop().toLowerCase();
return allowFileExtention.includes(extention);
}
function isValidFileSize(filesize) {
const maxSize = 10 * 1024 * 1024; // 10MB
return filesize <= maxSize;
}
// File input change event
fileInput.addEventListener('change', function () {
const file = fileInput.files[0];
if (file) {
// FileValidator를 사용한 파일 검증
if (!FileValidator.validateFile(file)) {
// Validate file extension
if (!isValidFileExtention(file.name)) {
const errorMsg = '허용된 파일 형식이 아닙니다.<br>문서파일과 이미지 파일만 등록 가능합니다.<br>(pdf, doc, docx, xls, xlsx, ppt, pptx, hwp, gif, jpg, png)';
$('#customAlertMessage').html(errorMsg);
$('#customAlert').css('display','flex');
fileInput.value = '';
fileNameInput.value = '첨부파일을 등록 해 주세요';
fileNameInput.classList.remove('i_trash', 'after');
fileNameInput.classList.add('before');
return;
}
// Validate file size
if (!isValidFileSize(file.size)) {
customPopups.showAlert('첨부파일은 10MB 이내로 등록해 주세요.');
fileInput.value = '';
return;
}
// Update UI
fileNameInput.value = file.name;
fileNameInput.classList.remove('before');
fileNameInput.classList.add('i_trash', 'after');
fileNameInput.classList.add('has-file');
fileRemoveBtn.style.display = 'inline-flex';
}
});
fileNameInput.addEventListener('click', function () {
if (fileNameInput.classList.contains('i_trash')) {
fileInput.value = '';
fileNameInput.value = '';
fileNameInput.classList.remove('i_trash', 'after');
fileNameInput.classList.add('before');
}
// File remove button click event
fileRemoveBtn.addEventListener('click', function () {
fileInput.value = '';
fileNameInput.value = '첨부파일을 등록해 주세요';
fileNameInput.classList.remove('has-file');
fileRemoveBtn.style.display = 'none';
});
});
</script>
@@ -1,415 +1,341 @@
<!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}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
<body>
<section layout:fragment="contentFragment" class="content">
<div class="content_wrap">
<!-- 타이틀 -->
<div class="sub_title2" id="title-find-id">
<h2 class="title add5">아이디 찾기</h2>
<th:block layout:fragment="contentFragment">
<div class="account-recovery-page">
<div class="account-recovery-container">
<div class="account-recovery-card">
<!-- Logo -->
<div class="account-recovery-logo">
<img th:src="@{/img/logo/logo_box.png}" alt="광주은행">
</div>
<div class="inner i_cs10 h_inner2">
<!-- 탭 메뉴 -->
<div class="tab-wrap">
<div class="tabs">
<ul class="tab_nav tabm">
<li class="active"><a href="">아이디 찾기</a></li>
<li><a th:href="@{/account_recovery(tab='resetPassword')}">비밀번호 초기화</a></li>
</ul>
<!-- Header Title -->
<h2 class="account-recovery-title">아이디 찾기</h2>
<!-- 아이디 찾기 폼 -->
<div class="tab active">
<div class="form_type">
<form id="accountForm" role="form" name="accountForm" th:action="@{/find_id}" method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<input type="hidden" id="mobileNumber" name="mobileNumber" th:value="${mobileNumber}">
<!-- Tab Navigation -->
<div class="account-recovery-tabs">
<a href="#" class="tab-link active">아이디 찾기</a>
<a th:href="@{/account_recovery(tab='resetPassword')}" class="tab-link">비밀번호 초기화</a>
</div>
<!-- 성명 입력 -->
<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="text" name="userName"
th:value="${userName}" class="common_input_type_1" placeholder="성명"
required>
</div>
</div>
</div>
<!-- Alert Messages -->
<div id="alertContainer"></div>
<!-- 휴대폰 번호 입력 -->
<div class="info1 pt28">
<p class="title w_tit">
<span class="dot">휴대폰 번호</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp5">
<span class="input">
<div class="custom_select ct_sc w_inp10 h_inp2 select_mobile_prefix" id="prefix_select">
<div class="select_selected active">선택</div>
<ul class="common_selecttype select_hide select_items ct_sc2">
<li>010</li>
<li>011</li>
<li>016</li>
<li>017</li>
<li>018</li>
</ul>
</div>
</span>
<span class="dash">-</span>
<span class="input">
<input type="tel" name="phoneMiddle" maxlength="4" class="common_input_type_1 w_inp11"
placeholder="앞자리" required>
</span>
<span class="dash">-</span>
<span class="input">
<input type="tel" name="phoneLast" maxlength="4" class="common_input_type_1 w_inp11"
placeholder="뒷자리" required>
</span>
</div>
<div class="btn_check">
<a class="common_btn_type_2 cbt btn_auth"
th:hx-post="@{/auth/request_auth_number}"
hx-include="#mobileNumber"
hx-target="#authNumberContainer"
hx-swap="none">
<span>인증번호 받기</span>
</a>
</div>
</div>
</div>
<!-- 인증번호 입력 -->
<div class="info1 pt28 auth-number-container" id="authNumberContainer">
<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" name="authNumber" id="authNumber" maxlength="6"
pattern="[0-9]*"
inputmode="numeric"
class="common_input_type_1 w_input auth-number-input"
placeholder="SMS 인증번호" required>
<span class="certify_time" id="certify_time">03:00</span>
</div>
</div>
<div class="btn_check">
<a class="common_btn_type_2 cbt btn_verify_auth"
th:hx-post="@{/auth/verify_auth_number}"
hx-include="#mobileNumber, #authNumber"
hx-target="#authNumberValidation"
hx-swap="none">
<span>인증번호 확인</span>
</a>
</div>
</div>
</div>
<div id="authNumberValidation"></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" onclick="validateAndSubmit()"><span>아이디 찾기</span></button>
</div>
</form>
</div>
</div>
</div>
<!-- Account Recovery Form -->
<form id="accountForm" role="form" name="accountForm" th:action="@{/find_id}" method="post" class="account-recovery-form">
<input type="hidden" id="mobileNumber" name="mobileNumber" th:value="${mobileNumber}">
<!-- Name Field -->
<div class="form-group">
<label for="userName" class="form-label">성명</label>
<input type="text"
id="userName"
name="userName"
th:value="${userName}"
class="form-input"
placeholder="성명"
required>
</div>
<!-- Phone Number Fields -->
<div class="form-group">
<label class="form-label">휴대폰 번호</label>
<div class="phone-input-group">
<select id="phonePrefix" class="form-select phone-prefix" required>
<option value="">선택</option>
<option value="010">010</option>
<option value="011">011</option>
<option value="016">016</option>
<option value="017">017</option>
<option value="018">018</option>
</select>
<span class="phone-separator">-</span>
<input type="tel"
id="phoneMiddle"
name="phoneMiddle"
maxlength="4"
class="form-input phone-middle"
placeholder="0000"
pattern="[0-9]*"
inputmode="numeric"
required>
<span class="phone-separator">-</span>
<input type="tel"
id="phoneLast"
name="phoneLast"
maxlength="4"
class="form-input phone-last"
placeholder="0000"
pattern="[0-9]*"
inputmode="numeric"
required>
</div>
</div>
<!-- Auth Number Request Button -->
<button type="button" class="auth-request-button" id="requestAuthButton">
인증번호 받기
</button>
<!-- Auth Number Input -->
<div class="form-group auth-number-group" id="authNumberGroup" style="display: none;">
<label for="authNumber" class="form-label">인증번호 입력</label>
<div class="auth-input-group">
<input type="text"
id="authNumber"
name="authNumber"
maxlength="6"
class="form-input auth-input"
placeholder="인증번호 6자리"
pattern="[0-9]*"
inputmode="numeric"
required>
<span class="auth-timer" id="authTimer">03:00</span>
</div>
</div>
<!-- Auth Number Verify Button -->
<button type="button" class="auth-verify-button" id="verifyAuthButton" style="display: none;">
인증번호 확인
</button>
<!-- Submit Buttons -->
<div class="form-actions">
<button type="button" class="cancel-button" id="cancelButton">취소</button>
<button type="submit" class="submit-button" id="submitButton">아이디 찾기</button>
</div>
</form>
<!-- Loading State -->
<div class="loading-overlay" id="loadingOverlay">
<div class="spinner"></div>
</div>
</div>
</div>
</section>
</div>
</th:block>
</body>
<th:block layout:fragment="contentScript">
<script th:inline="javascript">
$(document).ready(function () {
const errorMsg = [[${error}]];
<script th:inline="javascript">
$(document).ready(function() {
const errorMsg = [[${error}]];
if (errorMsg) {
showAlert(errorMsg, 'error');
}
if (errorMsg) {
customPopups.showAlert(errorMsg);
}
const paramError = new URL(window.location.href).searchParams.get('error');
if (paramError) {
showAlert(decodeURIComponent(paramError), 'error');
}
});
// Flash Attribute로 전달되는 에러 메시지 처리
const paramError = new URL(window.location.href).searchParams.get('error');
if (paramError) {
customPopups.showAlert(decodeURIComponent(paramError));
}
});
</script>
<script th:inline="javascript">
function combineMobileNumber(evt) {
let form = $('#accountForm');
let prefix = form.find('.select_mobile_prefix .select_selected').text().trim();
let middle = form.find('input[name="phoneMiddle"]').val().trim();
let last = form.find('input[name="phoneLast"]').val().trim();
// Alert 표시 함수
function showAlert(message, type = 'info') {
const alertContainer = $('#alertContainer');
const alertClass = type === 'error' ? 'alert-error' : type === 'success' ? 'alert-success' : 'alert-info';
const iconClass = type === 'error' ? 'fa-exclamation-circle' : type === 'success' ? 'fa-check-circle' : 'fa-info-circle';
if (prefix === '선택' || !middle || !last) {
customPopups.showAlert('휴대폰 번호를 올바르게 입력해주세요.');
evt.preventDefault();
return;
}
const alertHtml = `
<div class="account-alert ${alertClass}">
<i class="fas ${iconClass}"></i>
<span>${message}</span>
</div>
`;
let combinedNumber = `${prefix}-${middle}-${last}`;
let mobileNumberInput = document.getElementById('mobileNumber');
if (mobileNumberInput) {
mobileNumberInput.value = combinedNumber;
}
return combinedNumber;
alertContainer.html(alertHtml);
setTimeout(() => alertContainer.empty(), 5000);
}
// 휴대폰 번호 결합 함수
function combineMobileNumber() {
const prefix = $('#phonePrefix').val();
const middle = $('#phoneMiddle').val().trim();
const last = $('#phoneLast').val().trim();
if (!prefix || prefix === '' || !middle || !last) {
return null;
}
return `${prefix}-${middle}-${last}`;
}
// 타이머 변수
let countdownInterval;
let isAuthNumberRequested = false;
let isAuthVerified = false;
// 인증번호 타이머 시작
function startAuthTimer() {
clearInterval(countdownInterval);
let remainingTime = 180; // 3분
function updateTimer() {
if (remainingTime <= 0) {
clearInterval(countdownInterval);
$('#authTimer').text('시간 초과').css('color', '#FF6B6B');
showAlert('입력 시간이 초과되었습니다. 인증번호를 다시 발송해주세요.', 'error');
// 필드 활성화
$('#phonePrefix, #phoneMiddle, #phoneLast').prop('disabled', false);
$('#requestAuthButton').prop('disabled', false);
isAuthNumberRequested = false;
} else {
const minutes = Math.floor(remainingTime / 60);
const seconds = remainingTime % 60;
$('#authTimer').text(`${minutes}:${seconds.toString().padStart(2, '0')}`);
remainingTime--;
}
}
// 폼 전체 유효성 검증 함수
function validateAndSubmit() {
const form = document.getElementById('accountForm');
updateTimer();
countdownInterval = setInterval(updateTimer, 1000);
}
// 성명 검증
const userName = form.querySelector('input[name="userName"]').value.trim();
if (!userName) {
customPopups.showAlert('성명을 입력해주세요.');
return false;
}
// 인증번호 요청 버튼
$('#requestAuthButton').on('click', function() {
const mobileNumber = combineMobileNumber();
// 휴대폰 번호 검증
const prefix = form.querySelector('.select_mobile_prefix .select_selected').textContent.trim();
const phoneMiddle = form.querySelector('input[name="phoneMiddle"]').value.trim();
const phoneLast = form.querySelector('input[name="phoneLast"]').value.trim();
if (!mobileNumber) {
showAlert('휴대폰 번호를 올바르게 입력해주세요.', 'error');
return;
}
if (prefix === '선택' || !phoneMiddle || !phoneLast) {
customPopups.showAlert('휴대폰 번호를 올바르게 입력해주세요.');
return false;
}
$('#mobileNumber').val(mobileNumber);
$('#loadingOverlay').fadeIn(200);
// 인증번호 요청 여부 검증
if (!window.isAuthNumberRequested) {
customPopups.showAlert('휴대폰 인증을 먼저 진행해주세요.');
return false;
}
$.ajax({
url: '/auth/request_auth_number',
type: 'POST',
data: {
mobileNumber: mobileNumber,
_csrf: $('input[name="_csrf"]').val()
},
success: function(response) {
$('#loadingOverlay').fadeOut(200);
// 인증 완료 여부 검증
const authNumberInput = document.getElementById('authNumber');
if (!authNumberInput?.disabled) {
customPopups.showAlert('휴대폰 인증을 완료해주세요.');
return false;
}
if (response.valid) {
showAlert(response.message || '인증번호가 발송되었습니다.', 'success');
// 모든 검증을 통과하면 휴대폰 번호 결합 후 form submit
combineMobileNumber({
preventDefault: () => {
}
});
// 휴대폰 번호 필드 비활성화
$('#phonePrefix, #phoneMiddle, #phoneLast').prop('disabled', true);
$('#requestAuthButton').prop('disabled', true);
// 세션 검증을 위한 파라미터 추가
const currentAction = form.getAttribute('action');
form.setAttribute('action', currentAction + (currentAction.includes('?') ? '&' : '?') + 'showError=true');
// 인증번호 입력 영역 표시
$('#authNumberGroup').slideDown(300);
$('#verifyAuthButton').slideDown(300);
form.submit();
isAuthNumberRequested = true;
startAuthTimer();
} else {
showAlert(response.message || '인증번호 발송에 실패했습니다.', 'error');
}
},
error: function() {
$('#loadingOverlay').fadeOut(200);
showAlert('처리 중 오류가 발생했습니다. 다시 시도해주세요.', 'error');
}
});
});
document.body.addEventListener('htmx:beforeRequest', function (evt) {
combineMobileNumber(evt);
});
// 인증번호 확인 버튼
$('#verifyAuthButton').on('click', function() {
if (!isAuthNumberRequested) {
showAlert('휴대폰 번호 인증을 먼저 진행해주세요.', 'error');
return;
}
document.body.addEventListener('htmx:configRequest', function (evt) {
evt.detail.parameters['mobileNumber'] = combineMobileNumber(evt);
});
const authNumber = $('#authNumber').val().trim();
document.addEventListener('DOMContentLoaded', function () {
// 기존 휴대폰 번호 복원
const mobileNumber = document.getElementById('mobileNumber')?.value;
if (mobileNumber) {
const mobileParts = mobileNumber.split('-');
if (mobileParts.length === 3) {
const PrefixSelect = document.querySelector('#prefix_select .select_selected');
if (PrefixSelect) {
PrefixSelect.textContent = mobileParts[0];
}
const orgMiddleInput = document.querySelector('[name="phoneMiddle"]');
if (orgMiddleInput) {
orgMiddleInput.value = mobileParts[1];
}
const orgLastInput = document.querySelector('[name="phoneLast"]');
if (orgLastInput) {
orgLastInput.value = mobileParts[2];
}
}
}
if (authNumber.length !== 6) {
showAlert('인증번호 6자리를 입력해주세요.', 'error');
return;
}
activateSelect();
let countdownInterval;
window.isAuthNumberRequested = false;
const mobileNumber = $('#mobileNumber').val();
$('#loadingOverlay').fadeIn(200);
function startAuthNumberTimer() {
clearInterval(countdownInterval);
let remainingTime = 180; // 3분
$.ajax({
url: '/auth/verify_auth_number',
type: 'POST',
data: {
mobileNumber: mobileNumber,
authNumber: authNumber,
_csrf: $('input[name="_csrf"]').val()
},
success: function(response) {
$('#loadingOverlay').fadeOut(200);
function updateRemainingTime() {
if (remainingTime <= 0) {
clearInterval(countdownInterval);
let timerElement = document.getElementById('certify_time');
if (timerElement) {
timerElement.textContent = '입력 시간이 초과되었습니다.';
}
customPopups.showAlert('입력 시간이 초과되었습니다. 인증번호를 다시 발송해주세요.');
if (response.valid) {
showAlert(response.message || '인증이 완료되었습니다.', 'success');
let form = $('#accountForm');
let mobilePrefix = form.find('.select_mobile_prefix');
let phoneMiddle = form.find('input[name="phoneMiddle"]');
let phoneLast = form.find('input[name="phoneLast"]');
let authButton = document.querySelector('.btn_auth');
let verifyAuthButton = document.querySelector('.btn_verify_auth');
let authNumberInput = document.getElementById('authNumber');
// 인증번호 입력 필드 비활성화
$('#authNumber').prop('disabled', true);
$('#verifyAuthButton').prop('disabled', true);
// 입력 필드 활성화
mobilePrefix.removeClass('disabled');
phoneMiddle.prop('readonly', false);
phoneLast.prop('readonly', false);
if (authButton) {
authButton.disabled = false;
}
if (verifyAuthButton) {
verifyAuthButton.disabled = true;
}
if (authNumberInput) {
authNumberInput.readOnly = true;
}
window.isAuthNumberRequested = false;
} else {
let minutes = Math.floor(remainingTime / 60);
let seconds = remainingTime % 60;
let timerElement = document.getElementById('certify_time');
if (timerElement) {
timerElement.textContent = `남은 시간: ${minutes}:${seconds.toString().padStart(2, '0')}`;
}
remainingTime--;
}
}
clearInterval(countdownInterval);
$('#authTimer').text('인증 완료').css('color', '#6BCF7F');
updateRemainingTime();
countdownInterval = setInterval(updateRemainingTime, 1000);
}
isAuthVerified = true;
} else {
showAlert(response.message || '인증번호가 일치하지 않습니다.', 'error');
}
},
error: function() {
$('#loadingOverlay').fadeOut(200);
showAlert('처리 중 오류가 발생했습니다. 다시 시도해주세요.', 'error');
}
});
});
function handleAuthNumberRequest(response) {
if (response.valid) {
let form = $('#accountForm');
let mobilePrefix = form.find('.select_mobile_prefix');
let phoneMiddle = form.find('input[name="phoneMiddle"]');
let phoneLast = form.find('input[name="phoneLast"]');
let authButton = document.querySelector('.btn_auth');
// 폼 제출
$('#accountForm').on('submit', function(e) {
e.preventDefault();
mobilePrefix.addClass('disabled');
phoneMiddle.prop('disabled', true);
phoneLast.prop('disabled', true);
if (authButton) {
authButton.disabled = true;
}
const userName = $('#userName').val().trim();
if (!userName) {
showAlert('성명을 입력해주세요.', 'error');
return;
}
window.isAuthNumberRequested = true;
startAuthNumberTimer();
customPopups.showAlert(response.message || '인증번호가 발송되었습니다.');
} else {
customPopups.showAlert(response.message || '알 수 없는 오류가 발생했습니다.');
let authButton = document.querySelector('.btn_auth');
if (authButton) {
authButton.disabled = false;
}
}
}
const mobileNumber = combineMobileNumber();
if (!mobileNumber) {
showAlert('휴대폰 번호를 올바르게 입력해주세요.', 'error');
return;
}
function handleAuthNumberVerification(response) {
customPopups.showAlert(response.message);
if (response.valid) {
let authNumberInput = document.getElementById('authNumber');
let verifyAuthButton = document.querySelector('.btn_verify_auth');
let timerElement = document.getElementById('certify_time');
if (!isAuthNumberRequested) {
showAlert('휴대폰 인증을 먼저 진행해주세요.', 'error');
return;
}
if (authNumberInput) {
authNumberInput.readOnly = true;
authNumberInput.disabled = true;
}
if (verifyAuthButton) {
verifyAuthButton.disabled = true;
}
if (!isAuthVerified) {
showAlert('휴대폰 인증을 완료해주세요.', 'error');
return;
}
clearInterval(countdownInterval);
if (timerElement) {
timerElement.textContent = '인증 완료';
}
window.isAuthVerified = true;
document.dispatchEvent(new Event('authVerified'));
}
}
$('#mobileNumber').val(mobileNumber);
this.submit();
});
// HTMX 응답 처리
document.body.addEventListener('htmx:afterSwap', function (event) {
let targetId = event.detail.target ? event.detail.target.id : null;
if (targetId && ['authNumberContainer', 'authNumberValidation'].includes(targetId)) {
let response;
try {
response = typeof event.detail.xhr.response === 'string'
? JSON.parse(event.detail.xhr.response)
: event.detail.xhr.response;
} catch (e) {
console.error('Failed to parse response as JSON:', event.detail.xhr.response);
customPopups.showAlert('서버에서 예상치 못한 응답을 받았습니다.');
return;
}
// 취소 버튼
$('#cancelButton').on('click', function() {
window.location.href = '/login';
});
switch (targetId) {
case 'authNumberContainer':
handleAuthNumberRequest(response);
break;
case 'authNumberValidation':
handleAuthNumberVerification(response);
break;
}
}
});
document.body.addEventListener('htmx:responseError', function (event) {
customPopups.showAlert('처리 중 오류가 발생했습니다. 다시 시도해주세요.');
});
// 인증번호 확인 버튼 이벤트
let verifyAuthButton = document.querySelector('.btn_verify_auth');
if (verifyAuthButton) {
verifyAuthButton.addEventListener('click', function (e) {
if (!window.isAuthNumberRequested) {
e.preventDefault();
customPopups.showAlert('휴대폰 번호 인증을 먼저 진행해주세요.');
return;
}
let form = $('#accountForm');
let authNumberInput = form.find('.auth-number-input');
let authNumber = authNumberInput.val().trim();
if (authNumber.length !== 6) {
e.preventDefault();
customPopups.showAlert('입력된 인증번호가 유효하지 않습니다.');
return;
}
});
}
// 취소 버튼 이벤트
let cancelButton = document.querySelector('.btn_cancel');
if (cancelButton) {
cancelButton.addEventListener('click', function () {
location.href = '/login';
});
}
// 폼 제출 이벤트 처리
const accountForm = document.getElementById('accountForm');
if (accountForm) {
accountForm.addEventListener('submit', function (e) {
e.preventDefault();
validateAndSubmit();
});
}
});
</script>
// 기존 휴대폰 번호 복원
$(document).ready(function() {
const savedMobileNumber = $('#mobileNumber').val();
if (savedMobileNumber) {
const parts = savedMobileNumber.split('-');
if (parts.length === 3) {
$('#phonePrefix').val(parts[0]);
$('#phoneMiddle').val(parts[1]);
$('#phoneLast').val(parts[2]);
}
}
});
</script>
</th:block>
</html>
@@ -167,7 +167,6 @@
});
document.addEventListener('DOMContentLoaded', function () {
activateSelect();
let countdownInterval;
let isAuthVerified = false;
@@ -1,110 +1,81 @@
<!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}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
<body>
<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">
<th:block layout:fragment="contentFragment">
<div class="login-page">
<div class="login-container">
<div class="login-card">
<!-- Logo -->
<div class="login-logo">
<img th:src="@{/img/logo/logo_box.png}" alt="광주은행">
</div>
<!-- Header Message -->
<p class="login-message">로그인 하시면 더 많은 서비스를 이용하실 수 있습니다.</p>
<!-- 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">
<input type="hidden" id="message" name="message" th:value="${loginMessage}">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<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>
<input type="hidden" id="loginId" th:value="${loginId}">
<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>
<!-- ID Field -->
<div class="form-group">
<input type="text"
id="id"
name="id"
th:value="${loginId}"
class="form-input"
placeholder="아이디"
required>
</div>
<div class="box_inp">
<span class="inp_check f_size">
<input id="checkId" name="checkId" type="checkbox">
<label for="checkId">아이디 저장</label>
</span>
<!-- Password Field -->
<div class="form-group">
<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>
</form>
<div class="btn_login">
<a href="#" class="common_btn_type_1 h_btn" id="actionLogin"><span>로그인</span></a>
<!-- Login Button -->
<button type="button" class="login-button" id="actionLogin">
로그인
</button>
<!-- Additional Links -->
<div class="login-links">
<a th:href="@{/account_recovery(tab='findId')}">아이디 찾기</a>
<span class="link-separator">|</span>
<a th:href="@{/account_recovery(tab='resetPassword')}">비밀번호 초기화</a>
<span class="link-separator">|</span>
<a th:href="@{/signup}">회원가입</a>
</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>
<li>
<span>|</span>
</li>
<li class="icon_join">
<a th:href="@{/signup/invitation-code}">초대를 받으셨나요?</a>
</li>
</ul>
</div>
<div th:if="${param.logout}" class="alert alert-info mt-3">
<p>로그아웃되었습니다.</p>
<!-- Loading State -->
<div class="login-loading" id="loginLoading">
<div class="spinner"></div>
</div>
</div>
</div>
</div>
</section>
</th:block>
</body>
<th:block layout:fragment="contentScript">
<script th:inline="javascript">
@@ -151,10 +122,10 @@
}
if (document.loginForm.message && document.loginForm.message.value) {
if (document.loginForm.message.value.includes("이메일 인증이 완료되지 않았습니다")) {
console.log("이메일 인증 페이지 이동");
location.href = '/mypage/verification-email'
// console.log("Retrieved loginId:", loginId);
// customPopups.showEmailResendPopup(document.loginForm.message.value, loginId);
console.log("이메일 재발송 팝업 트리거");
let loginId = $('#loginId').val();
console.log("Retrieved loginId:", loginId);
customPopups.showEmailResendPopup(document.loginForm.message.value, loginId);
} else {
customPopups.showAlert(document.loginForm.message.value);
}
@@ -185,6 +156,10 @@
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 {
@@ -194,9 +169,11 @@
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;
@@ -207,7 +184,10 @@
form.classList.add('was-validated');
};
$('#actionLogin').on('click', actionLogin);
$('#actionLogin').on('click', function(event) {
event.preventDefault();
actionLogin(event);
});
$('#password').on('keydown', function (event) {
if (event.key === 'Enter' || event.keyCode === 13 || event.code === 'Enter') {
event.preventDefault();
@@ -1,462 +1,383 @@
<!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}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
<body>
<section layout:fragment="contentFragment" class="content">
<div class="content_wrap">
<!-- 타이틀 -->
<div class="sub_title2" id="title-reset-password">
<h2 class="title add8">비밀번호 초기화</h2>
<th:block layout:fragment="contentFragment">
<div class="account-recovery-page">
<div class="account-recovery-container">
<div class="account-recovery-card">
<!-- Logo -->
<div class="account-recovery-logo">
<img th:src="@{/img/logo/logo_box.png}" alt="광주은행">
</div>
<div class="inner i_cs10 h_inner2">
<!-- 탭 메뉴 -->
<div class="tab-wrap">
<div class="tabs">
<ul class="tab_nav tabm">
<li><a th:href="@{/account_recovery(tab='findId')}">아이디 찾기</a></li>
<li class="active"><a href="">비밀번호 초기화</a></li>
</ul>
<!-- Header Title -->
<h2 class="account-recovery-title">비밀번호 초기화</h2>
<!-- 비밀번호 초기화 폼 -->
<div class="tab active">
<div class="form_type">
<form id="accountForm" role="form" name="accountForm" th:action="@{/reset_password}"
method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<input type="hidden" id="mobileNumber" name="mobileNumber" th:value="${mobileNumber}">
<!-- Tab Navigation -->
<div class="account-recovery-tabs">
<a th:href="@{/account_recovery(tab='findId')}" class="tab-link">아이디 찾기</a>
<a href="#" class="tab-link active">비밀번호 초기화</a>
</div>
<!-- 성명 입력 -->
<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="text" name="userName"
th:value="${userName}" class="common_input_type_1" placeholder="성명"
required>
</div>
</div>
</div>
<!-- Alert Messages -->
<div id="alertContainer"></div>
<!-- 아이디 입력 (이메일) -->
<div class="info1 pt28">
<p class="title w_tit">
<span class="dot">이메일 아이디</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp4">
<input type="text" name="loginId" th:value="${loginId}"
class="common_input_type_1" placeholder="이메일 아이디">
</div>
</div>
</div>
<!-- Account Recovery Form -->
<form id="accountForm" role="form" name="accountForm" th:action="@{/reset_password}" method="post" class="account-recovery-form">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<input type="hidden" id="mobileNumber" name="mobileNumber" th:value="${mobileNumber}">
<!-- 휴대폰 번호 입력 -->
<div class="info1 pt28">
<p class="title w_tit">
<span class="dot">휴대폰 번호</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp5">
<span class="input">
<div class="custom_select ct_sc w_inp10 h_inp2 select_mobile_prefix" id="prefix_select">
<div class="select_selected active">선택</div>
<ul class="common_selecttype select_hide select_items ct_sc2">
<li>010</li>
<li>011</li>
<li>016</li>
<li>017</li>
<li>018</li>
</ul>
</div>
</span>
<span class="dash">-</span>
<span class="input">
<input type="tel" maxlength="4" name="phoneMiddle" class="common_input_type_1 w_inp11"
placeholder="앞자리" required>
</span>
<span class="dash">-</span>
<span class="input">
<input type="tel" maxlength="4" name="phoneLast" class="common_input_type_1 w_inp11"
placeholder="뒷자리" required>
</span>
</div>
<div class="btn_check">
<a class="common_btn_type_2 cbt btn_auth"
th:hx-post="@{/auth/request_auth_number}"
hx-include="#mobileNumber"
hx-target="#authNumberContainer"
hx-swap="none">
<span>인증번호 받기</span>
</a>
</div>
</div>
</div>
<!-- 인증번호 입력 -->
<div class="info1 pt28 auth-number-container" id="authNumberContainer">
<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" name="authNumber" id="authNumber" maxlength="6"
pattern="[0-9]*"
inputmode="numeric"
class="common_input_type_1 w_input auth-number-input"
placeholder="SMS 인증번호" required>
<span class="certify_time" id="certify_time">03:00</span>
</div>
</div>
<div class="btn_check">
<a class="common_btn_type_2 cbt btn_verify_auth"
th:hx-post="@{/auth/verify_auth_number}"
hx-include="#mobileNumber, #authNumber"
hx-target="#authNumberValidation"
hx-swap="none">
<span>인증번호 확인</span>
</a>
</div>
</div>
</div>
<div id="authNumberValidation"></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" onclick="validateAndSubmit()"><span>비밀번호 초기화</span>
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Name Field -->
<div class="form-group">
<label for="userName" class="form-label">성명</label>
<input type="text"
id="userName"
name="userName"
th:value="${userName}"
class="form-input"
placeholder="성명"
required>
</div>
<!-- Email ID Field -->
<div class="form-group">
<label for="loginId" class="form-label">이메일 아이디</label>
<input type="email"
id="loginId"
name="loginId"
th:value="${loginId}"
class="form-input"
placeholder="이메일 아이디"
required>
</div>
<!-- Phone Number Fields -->
<div class="form-group">
<label class="form-label">휴대폰 번호</label>
<div class="phone-input-group">
<select id="phonePrefix" class="form-select phone-prefix" required>
<option value="">선택</option>
<option value="010">010</option>
<option value="011">011</option>
<option value="016">016</option>
<option value="017">017</option>
<option value="018">018</option>
</select>
<span class="phone-separator">-</span>
<input type="tel"
id="phoneMiddle"
name="phoneMiddle"
maxlength="4"
class="form-input phone-middle"
placeholder="0000"
pattern="[0-9]*"
inputmode="numeric"
required>
<span class="phone-separator">-</span>
<input type="tel"
id="phoneLast"
name="phoneLast"
maxlength="4"
class="form-input phone-last"
placeholder="0000"
pattern="[0-9]*"
inputmode="numeric"
required>
</div>
</div>
<!-- Auth Number Request Button -->
<button type="button" class="auth-request-button" id="requestAuthButton">
인증번호 받기
</button>
<!-- Auth Number Input -->
<div class="form-group auth-number-group" id="authNumberGroup" style="display: none;">
<label for="authNumber" class="form-label">인증번호 입력</label>
<div class="auth-input-group">
<input type="text"
id="authNumber"
name="authNumber"
maxlength="6"
class="form-input auth-input"
placeholder="인증번호 6자리"
pattern="[0-9]*"
inputmode="numeric"
required>
<span class="auth-timer" id="authTimer">03:00</span>
</div>
</div>
<!-- Auth Number Verify Button -->
<button type="button" class="auth-verify-button" id="verifyAuthButton" style="display: none;">
인증번호 확인
</button>
<!-- Submit Buttons -->
<div class="form-actions">
<button type="button" class="cancel-button" id="cancelButton">취소</button>
<button type="submit" class="submit-button" id="submitButton">비밀번호 초기화</button>
</div>
</form>
<!-- Loading State -->
<div class="loading-overlay" id="loadingOverlay">
<div class="spinner"></div>
</div>
</div>
</div>
</section>
</div>
</th:block>
</body>
<th:block layout:fragment="contentScript">
<script th:inline="javascript">
$(document).ready(function() {
// 모든 종류의 에러 메시지 처리를 통합
const errorMessage = [[${errorMessage}]];
const error = [[${error}]];
const success = [[${success}]];
<script th:inline="javascript">
$(document).ready(function() {
const errorMsg = [[${error}]];
if (errorMsg) {
showAlert(errorMsg, 'error');
}
if (errorMessage) {
customPopups.showAlert(errorMessage);
}
if (error) {
customPopups.showAlert(error);
}
if (success) {
customPopups.showAlert(success);
}
});
</script>
<script th:inline="javascript">
// 휴대폰 번호 결합 함수
function combineMobileNumber(evt) {
let form = $('#accountForm');
let prefix = form.find('.select_mobile_prefix .select_selected').text().trim();
let middle = form.find('input[name="phoneMiddle"]').val().trim();
let last = form.find('input[name="phoneLast"]').val().trim();
const paramError = new URL(window.location.href).searchParams.get('error');
if (paramError) {
showAlert(decodeURIComponent(paramError), 'error');
}
});
if (prefix === '선택' || !middle || !last) {
customPopups.showAlert('휴대폰 번호를 올바르게 입력해주세요.');
evt.preventDefault();
return;
}
// Alert 표시 함수
function showAlert(message, type = 'info') {
const alertContainer = $('#alertContainer');
const alertClass = type === 'error' ? 'alert-error' : type === 'success' ? 'alert-success' : 'alert-info';
const iconClass = type === 'error' ? 'fa-exclamation-circle' : type === 'success' ? 'fa-check-circle' : 'fa-info-circle';
let combinedNumber = `${prefix}-${middle}-${last}`;
const alertHtml = `
<div class="account-alert ${alertClass}">
<i class="fas ${iconClass}"></i>
<span>${message}</span>
</div>
`;
if (combinedNumber.trim().length === 0) {
customPopups.showAlert('휴대폰 번호를 올바르게 입력해주세요.');
evt.preventDefault();
return;
}
$('#mobileNumber').val(combinedNumber);
alertContainer.html(alertHtml);
setTimeout(() => alertContainer.empty(), 5000);
}
return combinedNumber;
// 휴대폰 번호 결합 함수
function combineMobileNumber() {
const prefix = $('#phonePrefix').val();
const middle = $('#phoneMiddle').val().trim();
const last = $('#phoneLast').val().trim();
if (!prefix || prefix === '' || !middle || !last) {
return null;
}
return `${prefix}-${middle}-${last}`;
}
// 타이머 변수
let countdownInterval;
let isAuthNumberRequested = false;
let isAuthVerified = false;
// 인증번호 타이머 시작
function startAuthTimer() {
clearInterval(countdownInterval);
let remainingTime = 180; // 3분
function updateTimer() {
if (remainingTime <= 0) {
clearInterval(countdownInterval);
$('#authTimer').text('시간 초과').css('color', '#FF6B6B');
showAlert('입력 시간이 초과되었습니다. 인증번호를 다시 발송해주세요.', 'error');
// 필드 활성화
$('#phonePrefix, #phoneMiddle, #phoneLast').prop('disabled', false);
$('#requestAuthButton').prop('disabled', false);
isAuthNumberRequested = false;
} else {
const minutes = Math.floor(remainingTime / 60);
const seconds = remainingTime % 60;
$('#authTimer').text(`${minutes}:${seconds.toString().padStart(2, '0')}`);
remainingTime--;
}
}
// 폼 전체 유효성 검증 함수
function validateAndSubmit() {
const form = document.getElementById('accountForm');
updateTimer();
countdownInterval = setInterval(updateTimer, 1000);
}
// 성명 검증
const userName = form.querySelector('input[name="userName"]').value.trim();
if (!userName) {
customPopups.showAlert('성명을 입력해주세요.');
return false;
}
// 인증번호 요청 버튼
$('#requestAuthButton').on('click', function() {
const userName = $('#userName').val().trim();
if (!userName) {
showAlert('성명을 입력해주세요.', 'error');
return;
}
// 이메일 아이디 검증
const loginId = form.querySelector('input[name="loginId"]').value.trim();
if (!loginId) {
customPopups.showAlert('이메일 아이디를 입력해주세요.');
return false;
}
const loginId = $('#loginId').val().trim();
if (!loginId) {
showAlert('이메일 아이디를 입력해주세요.', 'error');
return;
}
// 이메일 형식 검증
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(loginId)) {
customPopups.showAlert('올바른 이메일 형식을 입력해주세요.');
return false;
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(loginId)) {
showAlert('올바른 이메일 형식을 입력해주세요.', 'error');
return;
}
// 휴대폰 번호 검증
const prefix = form.querySelector('.select_mobile_prefix .select_selected').textContent.trim();
const phoneMiddle = form.querySelector('input[name="phoneMiddle"]').value.trim();
const phoneLast = form.querySelector('input[name="phoneLast"]').value.trim();
const mobileNumber = combineMobileNumber();
if (!mobileNumber) {
showAlert('휴대폰 번호를 올바르게 입력해주세요.', 'error');
return;
}
if (prefix === '선택' || !phoneMiddle || !phoneLast) {
customPopups.showAlert('휴대폰 번호를 올바르게 입력해주세요.');
return false;
}
$('#mobileNumber').val(mobileNumber);
$('#loadingOverlay').fadeIn(200);
// 인증번호 요청 여부 검증
if (!window.isAuthNumberRequested) {
customPopups.showAlert('휴대폰 인증을 먼저 진행해주세요.');
return false;
}
$.ajax({
url: '/auth/request_auth_number',
type: 'POST',
data: {
mobileNumber: mobileNumber,
_csrf: $('input[name="_csrf"]').val()
},
success: function(response) {
$('#loadingOverlay').fadeOut(200);
// 인증 완료 여부 검증
const authNumberInput = document.getElementById('authNumber');
if (!authNumberInput?.disabled) {
customPopups.showAlert('휴대폰 인증을 완료해주세요.');
return false;
}
if (response.valid) {
showAlert(response.message || '인증번호가 발송되었습니다.', 'success');
// 모든 검증을 통과하면 휴대폰 번호 결합 후 form submit
combineMobileNumber({
preventDefault: () => {
}
});
// 휴대폰 번호 필드 비활성화
$('#phonePrefix, #phoneMiddle, #phoneLast').prop('disabled', true);
$('#requestAuthButton').prop('disabled', true);
// 세션 검증을 위한 파라미터 추가
const currentAction = form.getAttribute('action');
form.setAttribute('action', currentAction + (currentAction.includes('?') ? '&' : '?') + 'showError=true');
// 인증번호 입력 영역 표시
$('#authNumberGroup').slideDown(300);
$('#verifyAuthButton').slideDown(300);
form.submit();
isAuthNumberRequested = true;
startAuthTimer();
} else {
showAlert(response.message || '인증번호 발송에 실패했습니다.', 'error');
}
},
error: function() {
$('#loadingOverlay').fadeOut(200);
showAlert('처리 중 오류가 발생했습니다. 다시 시도해주세요.', 'error');
}
});
});
// HTMX 이벤트 리스너
document.body.addEventListener('htmx:beforeRequest', function (evt) {
combineMobileNumber(evt);
});
// 인증번호 확인 버튼
$('#verifyAuthButton').on('click', function() {
if (!isAuthNumberRequested) {
showAlert('휴대폰 번호 인증을 먼저 진행해주세요.', 'error');
return;
}
document.body.addEventListener('htmx:configRequest', function (evt) {
evt.detail.parameters['mobileNumber'] = combineMobileNumber(evt);
});
const authNumber = $('#authNumber').val().trim();
// 메인 초기화 및 이벤트 바인딩
document.addEventListener('DOMContentLoaded', function () {
// 기존 휴대폰 번호 복원
const mobileNumber = document.getElementById('mobileNumber')?.value;
if (mobileNumber) {
const mobileParts = mobileNumber.split('-');
if (mobileParts.length === 3) {
const PrefixSelect = document.querySelector('#prefix_select .select_selected');
if (PrefixSelect) {
PrefixSelect.textContent = mobileParts[0];
}
const orgMiddleInput = document.querySelector('[name="phoneMiddle"]');
if (orgMiddleInput) {
orgMiddleInput.value = mobileParts[1];
}
const orgLastInput = document.querySelector('[name="phoneLast"]');
if (orgLastInput) {
orgLastInput.value = mobileParts[2];
}
}
}
if (authNumber.length !== 6) {
showAlert('인증번호 6자리를 입력해주세요.', 'error');
return;
}
// 셀렉트 박스 활성화
activateSelect();
const mobileNumber = $('#mobileNumber').val();
$('#loadingOverlay').fadeIn(200);
// 인증 관련 변수 초기화
let countdownInterval;
window.isAuthNumberRequested = false;
window.isAuthVerified = false;
$.ajax({
url: '/auth/verify_auth_number',
type: 'POST',
data: {
mobileNumber: mobileNumber,
authNumber: authNumber,
_csrf: $('input[name="_csrf"]').val()
},
success: function(response) {
$('#loadingOverlay').fadeOut(200);
// 인증번호 타이머 함수
function startAuthNumberTimer() {
clearInterval(countdownInterval);
let remainingTime = 180; // 3분
if (response.valid) {
showAlert(response.message || '인증이 완료되었습니다.', 'success');
function updateRemainingTime() {
if (remainingTime <= 0) {
clearInterval(countdownInterval);
let timerElement = document.getElementById('certify_time');
if (timerElement) {
timerElement.textContent = '입력 시간이 초과되었습니다.';
}
customPopups.showAlert('입력 시간이 초과되었습니다. 인증번호를 다시 발송해주세요.');
// 인증번호 입력 필드 비활성화
$('#authNumber').prop('disabled', true);
$('#verifyAuthButton').prop('disabled', true);
let form = $('#accountForm');
let mobilePrefix = form.find('.select_mobile_prefix');
let phoneMiddle = form.find('input[name="phoneMiddle"]');
let phoneLast = form.find('input[name="phoneLast"]');
let authButton = document.querySelector('.btn_auth');
let verifyAuthButton = document.querySelector('.btn_verify_auth');
let authNumberInput = document.getElementById('authNumber');
clearInterval(countdownInterval);
$('#authTimer').text('인증 완료').css('color', '#6BCF7F');
// 입력 필드 활성화
mobilePrefix.removeClass('disabled');
phoneMiddle.prop('readonly', false);
phoneLast.prop('readonly', false);
if (authButton) {
authButton.disabled = false;
}
if (verifyAuthButton) {
verifyAuthButton.disabled = true;
}
if (authNumberInput) {
authNumberInput.readOnly = true;
}
window.isAuthNumberRequested = false;
} else {
let minutes = Math.floor(remainingTime / 60);
let seconds = remainingTime % 60;
let timerElement = document.getElementById('certify_time');
if (timerElement) {
timerElement.textContent = `남은 시간: ${minutes}:${seconds.toString().padStart(2, '0')}`;
}
remainingTime--;
}
}
isAuthVerified = true;
} else {
showAlert(response.message || '인증번호가 일치하지 않습니다.', 'error');
}
},
error: function() {
$('#loadingOverlay').fadeOut(200);
showAlert('처리 중 오류가 발생했습니다. 다시 시도해주세요.', 'error');
}
});
});
updateRemainingTime();
countdownInterval = setInterval(updateRemainingTime, 1000);
}
// 폼 제출
$('#accountForm').on('submit', function(e) {
e.preventDefault();
// 인증번호 요청 처리 함수
function handleAuthNumberRequest(response) {
if (response.valid) {
let form = $('#accountForm');
let mobilePrefix = form.find('.select_mobile_prefix');
let phoneMiddle = form.find('input[name="phoneMiddle"]');
let phoneLast = form.find('input[name="phoneLast"]');
let authButton = document.querySelector('.btn_auth');
const userName = $('#userName').val().trim();
if (!userName) {
showAlert('성명을 입력해주세요.', 'error');
return;
}
mobilePrefix.addClass('disabled');
phoneMiddle.prop('disabled', true);
phoneLast.prop('disabled', true);
if (authButton) {
authButton.disabled = true;
}
const loginId = $('#loginId').val().trim();
if (!loginId) {
showAlert('이메일 아이디를 입력해주세요.', 'error');
return;
}
window.isAuthNumberRequested = true;
startAuthNumberTimer();
customPopups.showAlert(response.message || '인증번호가 발송되었습니다.');
} else {
customPopups.showAlert(response.message || '알 수 없는 오류가 발생했습니다.');
let authButton = document.querySelector('.btn_auth');
if (authButton) {
authButton.disabled = false;
}
}
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(loginId)) {
showAlert('올바른 이메일 형식을 입력해주세요.', 'error');
return;
}
// 인증번호 확인 처리 함수
function handleAuthNumberVerification(response) {
customPopups.showAlert(response.message);
if (response.valid) {
let authNumberInput = document.getElementById('authNumber');
let verifyAuthButton = document.querySelector('.btn_verify_auth');
let timerElement = document.getElementById('certify_time');
const mobileNumber = combineMobileNumber();
if (!mobileNumber) {
showAlert('휴대폰 번호를 올바르게 입력해주세요.', 'error');
return;
}
if (authNumberInput) {
authNumberInput.readOnly = true;
authNumberInput.disabled = true;
}
if (verifyAuthButton) {
verifyAuthButton.disabled = true;
}
if (!isAuthNumberRequested) {
showAlert('휴대폰 인증을 먼저 진행해주세요.', 'error');
return;
}
clearInterval(countdownInterval);
if (timerElement) {
timerElement.textContent = '인증 완료';
}
window.isAuthVerified = true;
document.dispatchEvent(new Event('authVerified'));
}
}
if (!isAuthVerified) {
showAlert('휴대폰 인증을 완료해주세요.', 'error');
return;
}
// HTMX 응답 처리
document.body.addEventListener('htmx:afterSwap', function (event) {
let targetId = event.detail.target ? event.detail.target.id : null;
if (targetId && ['authNumberContainer', 'authNumberValidation'].includes(targetId)) {
let response;
try {
response = typeof event.detail.xhr.response === 'string'
? JSON.parse(event.detail.xhr.response)
: event.detail.xhr.response;
} catch (e) {
console.error('Failed to parse response as JSON:', event.detail.xhr.response);
customPopups.showAlert('서버에서 예상치 못한 응답을 받았습니다.');
return;
}
$('#mobileNumber').val(mobileNumber);
this.submit();
});
switch (targetId) {
case 'authNumberContainer':
handleAuthNumberRequest(response);
break;
case 'authNumberValidation':
handleAuthNumberVerification(response);
break;
}
}
});
// 취소 버튼
$('#cancelButton').on('click', function() {
window.location.href = '/login';
});
// HTMX 에러 처리
document.body.addEventListener('htmx:responseError', function (event) {
customPopups.showAlert('처리 중 오류가 발생했습니다. 다시 시도해주세요.');
});
// 인증번호 확인 버튼 이벤트
let verifyAuthButton = document.querySelector('.btn_verify_auth');
if (verifyAuthButton) {
verifyAuthButton.addEventListener('click', function (e) {
if (!window.isAuthNumberRequested) {
e.preventDefault();
customPopups.showAlert('휴대폰 번호 인증을 먼저 진행해주세요.');
return;
}
let form = $('#accountForm');
let authNumberInput = form.find('.auth-number-input');
let authNumber = authNumberInput.val().trim();
if (authNumber.length !== 6) {
e.preventDefault();
customPopups.showAlert('입력된 인증번호가 유효하지 않습니다.');
return;
}
});
}
// 취소 버튼 이벤트
let cancelButton = document.querySelector('.btn_cancel');
if (cancelButton) {
cancelButton.addEventListener('click', function () {
location.href = '/login';
});
}
// 폼 제출 이벤트 처리
const accountForm = document.getElementById('accountForm');
if (accountForm) {
accountForm.addEventListener('submit', function (e) {
e.preventDefault();
validateAndSubmit();
});
}
});
</script>
// 기존 휴대폰 번호 복원
$(document).ready(function() {
const savedMobileNumber = $('#mobileNumber').val();
if (savedMobileNumber) {
const parts = savedMobileNumber.split('-');
if (parts.length === 3) {
$('#phonePrefix').val(parts[0]);
$('#phoneMiddle').val(parts[1]);
$('#phoneLast').val(parts[2]);
}
}
});
</script>
</th:block>
</html>
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,6 @@
<!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}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
<body>
<section layout:fragment="contentFragment">
@@ -1,272 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
layout:decorate="~{layout/kbank_apikey_layout}">
<head>
<title>API 키 상세</title>
</head>
<body>
<section layout:fragment="contentFragment" class="content">
<div class="content_wrap">
<div class="sub_title3">
<h2 class="title">인증키 정보</h2>
</div>
<div class="inner i_cs h_inner11">
<div class="form_type">
<div class="info1 info_mypage">
<p class="title">
<span>인증키 이름</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp6">
<input type="text" id="clientName" th:value="${apiKey.clientname}"
class="common_input_type_1" disabled="disabled">
</div>
</div>
</div>
<div class="info1 info_mypage">
<p class="title">
<span>Client Id</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp6">
<input type="text" id="clientId" th:value="${apiKey.clientid}" class="common_input_type_1"
disabled="disabled">
</div>
</div>
</div>
<div class="info1 p_top1">
<p class="title">
<span>Client Secret</span>
</p>
<div class="info_line flex_w">
<div class="info_box1 w_inp6">
<div class="key_box">
<p th:text="${apiKey.clientsecret}">
client_key
</p>
</div>
</div>
</div>
</div>
<div class="info1 p_top5">
<p class="title"></p>
<div class="info_line line_add">
<div class="info_box1 m_top0 w_inp6">
<div class="btn_keydeltype">
<button sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" type="button"
class="btn_keydel">인증키 삭제
</button>
</div>
</div>
</div>
</div>
<div class="info1 info_mypage">
<p class="title">
<span>변경일</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp6">
<input type="text" th:value="${#temporals.format(apiKey.modifiedon, 'yyyy.MM.dd')}"
name="text" class="common_input_type_1" placeholder="2024.06.01" disabled="disabled">
</div>
</div>
</div>
<div class="info1 info_mypage">
<p class="title">
<span>인증키 상태</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp6">
<input type="text" th:value="${apiKey.appstatus == '1' ? '활성화' : '비활성화'}" name="text"
class="common_input_type_1" placeholder="활성화" disabled="disabled">
</div>
</div>
</div>
</div>
<div class="terms_box title">
<p>API 정보</p>
</div>
<div class="table_box">
<div class="pc-only">
<table class="table tb_key m_top" cellspacing="0" cellpadding="0" width="100%" id="apiList">
<colgroup>
<col width="60px">
<col width="166px">
<col width="230px">
<col width="120px">
</colgroup>
<thead>
<tr>
<th>No</th>
<th>서비스</th>
<th>API명</th>
<th>비고</th>
</tr>
</thead>
<tbody>
<tr th:each="api, apiStat : ${apiKey.apiList}">
<td th:text="${apiStat.count}">14</td>
<td th:text="${api.service}">공통</td>
<td th:text="${api.apiDesc}">회원여부조회</td>
<td><input type="hidden" name="apiId" th:value="${api.apiId}"/>
</td>
</tr>
</tbody>
</table>
</div>
<div class="m-only">
<table class="table tb_key m_top" cellspacing="0" cellpadding="0" width="100%">
<colgroup>
<col width="46px">
<col width="90px">
<col width="140px">
<col width="60px">
</colgroup>
<thead>
<tr>
<th>No</th>
<th>서비스</th>
<th>API명</th>
<th>비고</th>
</tr>
</thead>
<tbody>
<tr th:each="api, apiStat : ${apiKey.apiList}">
<td th:text="${apiStat.count}">14</td>
<td th:text="${api.apiGroupId}">공통</td>
<td th:text="${api.apiDesc}">회원여부조회</td>
<td>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="terms_box title">
<p>운영 전환 신청 내역</p>
</div>
<div class="table_box">
<div class="pc-only">
<table class="table tb_key m_top" cellspacing="0" cellpadding="0" width="100%">
<colgroup>
<col width="180px">
<col width="180px">
<col width="556px">
</colgroup>
<thead>
<tr>
<th>상태</th>
<th>일시</th>
<th>상세내용</th>
</tr>
</thead>
<tbody>
<tr th:each="history : ${prodhistory}">
<td th:text="${history.approval.approvalStatus.description}">요청</td>
<td th:text="${#temporals.format(history.createdDate, 'yyyy.MM.dd HH:mm:ss')}">2024.06.01
12:12:12
</td>
<td th:text="${history.approval.approvalSubject}+ '(' + ${history.reason} + ')'">운영 전환 요청</td>
</tr>
</tbody>
</table>
</div>
<div class="m-only">
<table class="table tb_key m_top" cellspacing="0" cellpadding="0" width="100%">
<colgroup>
<col width="80px">
<col width="180px">
<col width="240px">
</colgroup>
<thead>
<tr>
<th>상태</th>
<th>일시</th>
<th>상세내용</th>
</tr>
</thead>
<tbody>
<tr th:each="history : ${prodhistory}">
<td th:text="${history.approval.approvalStatus.description}">요청</td>
<td th:text="${#temporals.format(history.createdDate, 'yyyy.MM.dd HH:mm:ss')}">2024.06.01
12:12:12
</td>
<td th:text="${history.approval.approvalSubject}+ '(' + ${history.reason} + ')'">운영 전환 요청</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="btn_wrap bt_location">
<button type="button" sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" class="btn_add btn_request_api"
id="requestApiKey">API 변경
</button>
<button type="button" sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"
class="btn_refresh btn_request_prod_api">운영 전환 신청
</button>
</div>
<div class="btn_inventory btn_mtop">
<a th:href="@{/myapikey}" class="common_btn_type_1">목록</a>
</div>
<!-- // 기본 정보 -->
</div>
</div>
</section>
<section sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" layout:fragment="pagePopups" class="content pop">
<div th:insert="~{fragment/apikey-request :: apiKeyRequest}" class="popup_total" id="apiKeyRequestPopup"
style="display:none; z-index: 1000;"></div>
<th:block th:replace="~{fragment/apikey-request :: apiKeyRequestScript}"></th:block>
<div th:insert="~{fragment/apikey-request-prod :: prodApiKeyRequest}" class="popup_total"
id="apiKeyProdRequestPopup" style="display:none; z-index: 1000;"></div>
<th:block th:replace="~{fragment/apikey-request-prod :: prodApiKeyRequestScript}"></th:block>
</section>
<th:block layout:fragment="contentScript">
<script>
$(document).ready(function () {
activateSelect();
$('.btn_keydel').on('click', function (event) {
event.preventDefault();
$('.loading-overlay').show();
const requestData = {
clientId: document.getElementById('clientId').value,
type: 'DELETE'
};
$.ajax({
url: '/myapikey/api_key_request',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(requestData)
}).done(function (response) {
customPopups.showAlert(response.msg, function () {
window.location = '[[@{/myapikey/api_key_request/history}]]';
});
}).fail(function (jqXHR, textStatus, errorThrown) {
customPopups.showAlert('API 삭제 요청 중 오류가 발생했습니다: ' + errorThrown);
}).always(function () {
$('.loading-overlay').hide();
});
});
});
</script>
</th:block>
</body>
</html>
@@ -1,92 +1,156 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kbank_apikey_layout}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_title_layout}">
<body>
<section layout:fragment="contentFragment" class="content">
<div class="content_wrap">
<div class="sub_title3">
<h2 class="title">인증키 목록</h2>
</div>
<div class="inner i_cs h_inner8">
<div class="tab-wrap">
<div class="tabs">
<ul class="tab_nav tab_bt">
<li class="active">
<a th:href="@{/myapikey}">개발</a>
</li>
<li>
<a th:href="@{/myapikey/api_key_prod_page}">운영</a>
</li>
</ul>
<div class="tab active">
<div class="tb_board_list">
<div class="table_box">
<table class="table table_min" cellspacing="0" cellpadding="0" width="100%">
<colgroup>
<col width="60px">
<col width="86px">
<col width="230px">
<col width="120px">
<col width="100px">
<col width="120px">
</colgroup>
<thead>
<tr>
<th>No</th>
<th>이름</th>
<th>인증키</th>
<th>인증키상태</th>
<th>사용 API 수</th>
<th>마지막수정일</th>
</tr>
</thead>
<tbody>
<tr th:each="apikey, status : ${apiKeys}">
<td th:text="${status.index + 1}">1</td>
<td>
<a th:href="@{/myapikey/api_key_detail(id=${apikey.clientid})}" th:text="${apikey.clientname}">수신</a>
</td>
<td>
<a th:href="@{/myapikey/api_key_detail(id=${apikey.clientid})}" th:text="${apikey.clientid}"></a>
</td>
<td th:text="${apikey.appstatus == '1' ? '활성화' : '비활성화'}">활성화</td>
<td th:text="${apikey.apiList.size()}">1</td>
<td th:text="${#temporals.format(apikey.modifiedon, 'yyyy.MM.dd')}"></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="btn_wrap bt_location">
<button sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" type="button" class="btn_key" id="requestApiKey">인증키 신청</button>
</div>
<div class="pagination page_top" th:replace="~{fragment/pagination :: pagination(jsFunction='fn_select_page')}"></div>
<form name="listForm" th:action="@{/myapikey}" method="get">
<input type="hidden" id="page" name="page"/>
</form>
</div>
</div>
</div>
</div>
<section layout:fragment="title">
<div class="page-title-banner">
<img th:src="@{/img/img_title_bg.png}" class="title-image">
<h1>앱관리</h1>
</div>
</section>
<section sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" layout:fragment="pagePopups" class="content pop">
<div th:insert="~{fragment/apikey-request :: apiKeyRequest}" class="popup_total" id="apiKeyRequestPopup" style="display:none; z-index: 1000;"></div>
<th:block th:replace="~{fragment/apikey-request :: apiKeyRequestScript}"></th:block>
</section>
<th:block layout:fragment="contentScript">
<script>
document.addEventListener('DOMContentLoaded', function() {
activateSelect();
});
<th:block layout:fragment="contentFragment">
<section class="app-management-container">
function fn_select_page(pageNo) {
document.listForm.page.value = pageNo;
document.listForm.action = '[[@{/myapikey}]]';
document.listForm.submit();
}
<!-- App List Title Section -->
<div class="app-list-title-section">
<h2 class="app-list-title">앱 목록</h2>
</div>
<!-- App List Container -->
<div class="app-list-container">
<!-- App Requests (Pending) -->
<th:block th:if="${appRequests != null and !appRequests.isEmpty()}">
<a class="app-list-item"
th:each="request : ${appRequests}"
th:href="@{/myapikey/app_request_detail(id=${request.id})}">
<!-- App Icon -->
<div class="app-list-icon">
<img th:if="${request.appIconFileId != null}"
th:src="@{/file/download(fileSn=1,fileId=${request.appIconFileId})}"
alt="App Icon"
class="app-icon-image">
<div th:unless="${request.appIconFileId != null}" class="app-icon-placeholder">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21 15 16 10 5 21"></polyline>
</svg>
</div>
</div>
<!-- App Info -->
<div class="app-list-info">
<div class="app-list-header">
<!-- Status Badge -->
<span class="app-status-badge status-pending"
th:text="${request.approval != null and request.approval.approvalStatus != null ? request.approval.approvalStatus.description : '승인대기'}">
승인대기
</span>
<!-- App Name -->
<h3 class="app-list-name" th:text="${request.clientName}">앱 이름</h3>
</div>
<!-- App Description -->
<p class="app-list-description"
th:text="${request.appDescription != null ? request.appDescription : '설명 없음'}">
앱 설명이 여기에 표시됩니다.
</p>
</div>
</a>
</th:block>
<!-- API Keys (Approved) -->
<th:block th:if="${apiKeys != null and !apiKeys.isEmpty()}">
<a class="app-list-item"
th:each="apikey : ${apiKeys}"
th:href="@{/myapikey/credential_detail(id=${apikey.clientid})}">
<!-- App Icon -->
<div class="app-list-icon">
<img th:if="${apikey.appIconFileId != null}"
th:src="@{/file/download(fileSn=1,fileId=${apikey.appIconFileId})}"
alt="App Icon"
class="app-icon-image">
<div th:unless="${apikey.appIconFileId != null}" class="app-icon-placeholder">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21 15 16 10 5 21"></polyline>
</svg>
</div>
</div>
<!-- App Info -->
<div class="app-list-info">
<div class="app-list-header">
<!-- Status Badge -->
<span class="app-status-badge"
th:classappend="${apikey.appstatus == '1' ? 'status-approved' : apikey.appstatus == '0' ? 'status-inactive' : 'status-pending'}"
th:text="${apikey.appstatus == '1' ? '승인' : apikey.appstatus == '0' ? '비활성화' : '승인대기'}">
승인
</span>
<!-- App Name -->
<h3 class="app-list-name" th:text="${apikey.clientname}">앱 이름</h3>
</div>
<!-- App Description -->
<p class="app-list-description"
th:text="${apikey.appDescription != null ? apikey.appDescription : '설명 없음'}">
앱 설명이 여기에 표시됩니다.
</p>
</div>
</a>
</th:block>
<!-- Empty State -->
<div class="app-list-empty" th:if="${(appRequests == null or appRequests.isEmpty()) and (apiKeys == null or apiKeys.isEmpty())}">
<div class="empty-icon">🔑</div>
<h3>등록된 앱이 없습니다</h3>
<p>새로운 앱을 생성하여 API를 사용해보세요.</p>
</div>
</div>
<!-- Create App Button -->
<div class="app-create-button-wrapper" sec:authorize="hasRole('ROLE_API_KEY_REQUEST')">
<button type="button" class="btn-app-create" id="requestApiKey">
앱 생성
</button>
</div>
</section>
</th:block>
<th:block layout:fragment="contentScript">
<script th:inline="javascript">
document.addEventListener('DOMContentLoaded', function() {
// App creation button handler
const requestApiKeyBtn = document.getElementById('requestApiKey');
function handleApiKeyRequest() {
// Clear any existing registration data from sessionStorage
const keysToRemove = [];
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
if (key && key.startsWith('registration_')) {
keysToRemove.push(key);
}
}
keysToRemove.forEach(key => sessionStorage.removeItem(key));
// Redirect to the API key registration wizard with clear parameter
window.location.href = '/myapikey/register/step1?clear=true';
}
if (requestApiKeyBtn) {
requestApiKeyBtn.addEventListener('click', handleApiKeyRequest);
}
});
</script>
</th:block>
</body>
@@ -0,0 +1,477 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_title_layout}">
<body>
<section layout:fragment="title">
<div class="page-title-banner">
<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">
<!-- Title Bar -->
<div class="register-title-bar">
<h2 class="register-title">앱 수정</h2>
<span class="register-subtitle">서비스 중인 앱 정보를 수정합니다.</span>
</div>
<!-- Progress Indicator -->
<div class="register-progress">
<div class="progress-steps">
<!-- Step 1: 앱 정보수정 (Active) -->
<div class="progress-step-item active">
<div class="step-icon-wrapper">
<div class="step-icon">
<img th:src="@{/img/apikey_step1.png}" alt="앱 정보수정" width="36" height="36">
</div>
</div>
<div class="step-label">앱 정보수정</div>
</div>
<!-- Dots -->
<div class="step-dots">
<span></span><span></span><span></span>
</div>
<!-- Step 2: API 선택 -->
<div class="progress-step-item">
<div class="step-icon-wrapper">
<div class="step-icon">
<img th:src="@{/img/apikey_step2.png}" alt="API 선택" width="36" height="36">
</div>
</div>
<div class="step-label">API 선택</div>
</div>
<!-- Dots -->
<div class="step-dots">
<span></span><span></span><span></span>
</div>
<!-- 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>
</div>
<div class="step-label">완료</div>
</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 -->
<div class="register-form-container">
<form id="modifyStep1Form" method="post" th:action="@{/myapikey/modify/step1}" th:object="${apiKeyModification}" class="register-form" enctype="multipart/form-data">
<!-- Hidden field for clientId -->
<input type="hidden" th:field="*{clientId}"/>
<!-- App Icon -->
<div class="form-row">
<div class="form-label-wrapper">
<span class="form-label-text">앱 아이콘</span>
<span class="required-badge">필수</span>
</div>
<div class="form-field-wrapper">
<div class="icon-upload-box">
<div class="icon-preview-area" id="iconPreview">
<div class="icon-placeholder" id="iconPlaceholder"
th:style="${(apiKeyModification.appIconData != null or apiKeyModification.appIconFileId != null) ? 'display: none;' : 'display: flex;'}">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>
</div>
<!-- Show image from file service if available -->
<img id="iconPreviewImage"
th:if="${apiKeyModification.appIconFileId != null}"
th:src="@{/file/download(fileSn=1,fileId=${apiKeyModification.appIconFileId})}"
alt="Icon Preview"
style="display: block;">
<!-- Show image from data if available -->
<img id="iconPreviewImage"
th:if="${apiKeyModification.appIconData != null and apiKeyModification.appIconFileId == null}"
th:src="'data:' + ${apiKeyModification.appIconContentType} + ';base64,' + ${T(java.util.Base64).getEncoder().encodeToString(apiKeyModification.appIconData)}"
alt="Icon Preview"
style="display: block;">
<!-- JS Empty Image Target -->
<img id="iconPreviewImageJS" style="display: none;" alt="Icon Preview">
<button type="button" class="btn-remove-icon" id="btnRemoveIcon"
th:style="${(apiKeyModification.appIconData != null or apiKeyModification.appIconFileId != null) ? 'display: flex;' : 'display: none;'}"
onclick="removeAppIcon()" title="아이콘 삭제">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="icon-upload-info">
<p class="upload-title">앱 아이콘 이미지 파일을 업로드하세요</p>
<p class="upload-hint">권장사이즈는 <strong>128X128픽셀</strong>이며 <strong>JPG, PNG, GIF</strong>파일만 등록할 수 있습니다.</p>
</div>
<button type="button" class="btn-upload" onclick="document.getElementById('appIcon').click()">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>
이미지 파일 업로드
</button>
<input type="file"
id="appIcon"
name="appIcon"
class="icon-input"
accept="image/png,image/jpeg,image/jpg,image/gif"
style="display: none;">
</div>
</div>
</div>
<!-- App Name -->
<div class="form-row">
<div class="form-label-wrapper label-offset">
<span class="form-label-text">앱 이름</span>
<span class="required-badge">필수</span>
</div>
<div class="form-field-wrapper">
<input type="text"
id="appName"
th:field="*{appName}"
class="form-input"
placeholder="앱 이름을 입력하세요."
required
maxlength="100">
</div>
</div>
<!-- App Description -->
<div class="form-row">
<div class="form-label-wrapper">
<span class="form-label-text">앱 설명</span>
<span class="required-badge">필수</span>
</div>
<div class="form-field-wrapper">
<textarea id="appDescription"
th:field="*{appDescription}"
class="form-textarea"
placeholder="앱 설명을 입력하세요."
rows="5"
required
maxlength="500"></textarea>
<small class="char-counter-wrapper">
<span class="char-counter">0 / 500</span>
</small>
</div>
</div>
<!-- Callback URL -->
<div class="form-row">
<div class="form-label-wrapper label-offset">
<span class="form-label-text">Call Back URL</span>
</div>
<div class="form-field-wrapper">
<input type="text"
id="callbackUrl"
th:field="*{callbackUrl}"
class="form-input"
placeholder="URL을 입력해 주세요.">
</div>
</div>
<!-- IP Whitelist -->
<div class="form-row">
<div class="form-label-wrapper label-offset">
<span class="form-label-text">화이트리스트</span>
</div>
<div class="form-field-wrapper">
<div class="ip-input-row">
<input type="text"
id="ipWhitelistInput"
class="form-input ip-input"
placeholder="서버 IP (111.111.111.111)">
<button type="button" class="btn-add-ip" onclick="addIpAddress()">
+ 추가
</button>
</div>
<!-- IP List Display -->
<div class="ip-list" id="ipList" style="display: none;">
<div class="ip-items" id="ipItems">
<!-- Dynamically added IP items -->
</div>
</div>
<!-- Hidden input to store IP list -->
<input type="hidden" id="ipWhitelist" name="ipWhitelist" value="">
</div>
</div>
</form>
</div>
<!-- Form Actions -->
<div class="form-actions">
<a th:href="@{/myapikey/modify/cancel(clientId=${apiKeyModification.clientId})}" class="btn btn-submit btn-secondary">
취소
</a>
<button type="submit" form="modifyStep1Form" class="btn-primary btn-submit">
다음
</button>
</div>
</section>
</th:block>
<th:block layout:fragment="contentScript">
<script th:inline="javascript">
// IP address list management
let ipAddresses = [];
// Remove app icon
function removeAppIcon() {
const appIconInput = document.getElementById('appIcon');
const iconPreviewImages = document.querySelectorAll('img[id^="iconPreviewImage"]'); // Select all preview images
const iconPlaceholder = document.getElementById('iconPlaceholder');
const btnRemoveIcon = document.getElementById('btnRemoveIcon');
// Clear file input
appIconInput.value = '';
// Hide all preview images
iconPreviewImages.forEach(img => {
img.style.display = 'none';
if (img.id === 'iconPreviewImageJS') img.src = '';
});
// Show placeholder
iconPlaceholder.style.display = 'flex';
// Hide delete button
btnRemoveIcon.style.display = 'none';
}
// Add IP address to the list
function addIpAddress() {
const ipInput = document.getElementById('ipWhitelistInput');
const ipValue = ipInput.value.trim();
// Validate IP format
if (!ipValue) {
customPopups.showAlert('IP 주소를 입력해주세요.');
return;
}
// Basic IP validation (supports IP and CIDR notation)
const ipPattern = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/;
if (!ipPattern.test(ipValue)) {
customPopups.showAlert('올바른 IP 주소 형식이 아닙니다.\n예: 192.168.1.1 또는 10.0.0.0/24');
return;
}
// Check for duplicates
if (ipAddresses.includes(ipValue)) {
customPopups.showAlert('이미 추가된 IP 주소입니다.');
return;
}
// Add to array
ipAddresses.push(ipValue);
// Update UI
updateIpList();
// Clear input
ipInput.value = '';
ipInput.focus();
}
// Remove IP address from the list
function removeIpAddress(ip) {
const index = ipAddresses.indexOf(ip);
if (index > -1) {
ipAddresses.splice(index, 1);
updateIpList();
}
}
// Update IP list display
function updateIpList() {
const ipList = document.getElementById('ipList');
const ipItems = document.getElementById('ipItems');
const ipWhitelistHidden = document.getElementById('ipWhitelist');
// Show/hide list container
if (ipAddresses.length > 0) {
ipList.style.display = 'block';
// Build IP items HTML
let itemsHTML = '';
ipAddresses.forEach(function(ip) {
itemsHTML += `
<div class="ip-item">
<div class="ip-display">${ip}</div>
<button type="button" class="btn-remove-ip" onclick="removeIpAddress('${ip}')">
- 제거
</button>
</div>
`;
});
ipItems.innerHTML = itemsHTML;
} else {
ipList.style.display = 'none';
ipItems.innerHTML = '';
}
// Update hidden input with comma-separated values
ipWhitelistHidden.value = ipAddresses.join(',');
}
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('modifyStep1Form');
const appDescTextarea = document.getElementById('appDescription');
const charCounter = document.querySelector('.char-counter');
const appIconInput = document.getElementById('appIcon');
const iconPreviewImageJS = document.getElementById('iconPreviewImageJS');
const iconPlaceholder = document.getElementById('iconPlaceholder');
const ipWhitelistInput = document.getElementById('ipWhitelistInput');
// Restore existing IP whitelist
/*<![CDATA[*/
const sessionIpWhitelist = /*[[${apiKeyModification.ipWhitelistAsString}]]*/ '';
if (sessionIpWhitelist && sessionIpWhitelist.trim() !== '') {
const ips = sessionIpWhitelist.split(',');
ips.forEach(function(ip) {
const trimmedIp = ip.trim();
if (trimmedIp && !ipAddresses.includes(trimmedIp)) {
ipAddresses.push(trimmedIp);
}
});
updateIpList();
}
/*]]>*/
// Initialize character counter
if (appDescTextarea && charCounter) {
const currentLength = appDescTextarea.value.length;
const maxLength = appDescTextarea.getAttribute('maxlength');
charCounter.textContent = currentLength + ' / ' + maxLength;
appDescTextarea.addEventListener('input', function() {
const currentLength = this.value.length;
const maxLength = this.getAttribute('maxlength');
charCounter.textContent = currentLength + ' / ' + maxLength;
if (currentLength > maxLength * 0.9) {
charCounter.style.color = '#FF6B6B';
} else {
charCounter.style.color = '#94A3B8';
}
});
}
// App icon preview
if (appIconInput) {
appIconInput.addEventListener('change', function(e) {
const file = e.target.files[0];
const btnRemoveIcon = document.getElementById('btnRemoveIcon');
if (file) {
if (file.size > 2 * 1024 * 1024) {
customPopups.showAlert('파일 크기는 2MB를 초과할 수 없습니다.');
this.value = '';
return;
}
const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif'];
if (!allowedTypes.includes(file.type)) {
customPopups.showAlert('PNG, JPG, GIF 형식만 업로드 가능합니다.');
this.value = '';
return;
}
const reader = new FileReader();
reader.onload = function(event) {
// Hide other previews
document.querySelectorAll('img[id^="iconPreviewImage"]').forEach(img => img.style.display = 'none');
iconPreviewImageJS.src = event.target.result;
iconPreviewImageJS.style.display = 'block';
iconPlaceholder.style.display = 'none';
btnRemoveIcon.style.display = 'flex';
};
reader.readAsDataURL(file);
}
});
}
// IP input Enter key
if (ipWhitelistInput) {
ipWhitelistInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
addIpAddress();
}
});
}
// Form validation
form.addEventListener('submit', function(e) {
const appName = document.getElementById('appName').value.trim();
const appDescription = document.getElementById('appDescription').value.trim();
const callbackUrl = document.getElementById('callbackUrl').value.trim();
if (document.getElementById('iconPlaceholder').style.display !== 'none') {
e.preventDefault();
customPopups.showAlert('앱 아이콘을 등록해주세요.');
return false;
}
if (!appName) {
e.preventDefault();
customPopups.showAlert('앱 이름을 입력해주세요.');
document.getElementById('appName').focus();
return false;
}
if (!appDescription) {
e.preventDefault();
customPopups.showAlert('앱 설명을 입력해주세요.');
document.getElementById('appDescription').focus();
return false;
}
if (callbackUrl && callbackUrl.trim() !== '') {
try {
new URL(callbackUrl);
} catch (error) {
e.preventDefault();
customPopups.showAlert('올바른 URL 형식이 아닙니다.');
document.getElementById('callbackUrl').focus();
return false;
}
}
});
});
</script>
</th:block>
</body>
</html>
@@ -0,0 +1,741 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_title_layout}">
<body>
<section layout:fragment="title">
<div class="page-title-banner">
<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">
<!-- Title Bar -->
<div class="register-title-bar">
<h2 class="register-title">앱 수정</h2>
<span class="register-subtitle">API를 이용하여 서비스할 앱 정보를 수정해주세요.</span>
</div>
<!-- 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">
</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">
</div>
</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>
<!-- 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>
</div>
<div class="step-label">앱 수정 완료</div>
</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 (Figma Design) -->
<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>
</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>
</div>
<div class="modal-body">
<div class="selected-apis-list" id="modalSelectedList">
<!-- Dynamically populated -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" id="modalCancelBtn">닫기</button>
</div>
</div>
</div>
</th:block>
<th:block layout:fragment="contentScript">
<script th:inline="javascript">
document.addEventListener('DOMContentLoaded', function() {
// 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 apiCardGrid = document.getElementById('apiCardGrid');
const loadingState = document.getElementById('loadingState');
const emptyState = document.getElementById('emptyState');
let currentFilter = ''; // Empty string means "all"
let currentServiceName = '전체'; // Store current service name
let allApis = []; // Store loaded APIs
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)
const sessionSelectedApis = /*[[${apiKeyModification.selectedApis}]]*/ [];
if (sessionSelectedApis && Array.isArray(sessionSelectedApis)) {
sessionSelectedApis.forEach(function(apiId) {
selectedApis.add(apiId);
});
}
// Load all APIs for modal display (used when APIs from different categories are selected)
function loadAllApisForModal() {
fetch('/apis/for_request')
.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);
});
}
// Load APIs via AJAX
function loadApis(groupId) {
// Show loading state
loadingState.style.display = 'block';
emptyState.style.display = 'none';
// Remove existing API cards
document.querySelectorAll('.api-selection-card').forEach(card => card.remove());
// Build URL with optional groupId filter
let url = '/apis/for_request';
if (groupId) {
url += '?groupIds=' + encodeURIComponent(groupId);
}
fetch(url).then(response => response.json()).then(apis => {
allApis = apis;
loadingState.style.display = 'none';
if (apis.length === 0) {
emptyState.style.display = 'block';
return;
}
renderApiCards(apis);
updateSelectAllUI();
}).catch(error => {
console.error('Failed to load APIs:', error);
loadingState.style.display = 'none';
emptyState.querySelector('h3').textContent = 'API 로드 실패';
emptyState.querySelector('p').textContent = '다시 시도해주세요.';
emptyState.style.display = 'block';
});
}
// Render API cards
function renderApiCards(apis) {
const fragment = document.createDocumentFragment();
apis.forEach(api => {
const card = createApiCard(api);
fragment.appendChild(card);
});
apiCardGrid.appendChild(fragment);
// Re-attach event listeners
attachCardEventListeners();
// Update modal list after cards are rendered
updateModalList();
}
// Create API card element
function createApiCard(api) {
const card = document.createElement('div');
card.className = 'api-selection-card';
card.setAttribute('data-group', api.apiGroupId || '');
card.setAttribute('data-name', (api.apiName || '').toLowerCase());
card.setAttribute('data-desc', (api.apiSimpleDescription || '').toLowerCase());
card.setAttribute('data-api-id', api.apiId);
const isSelected = selectedApis.has(api.apiId);
if (isSelected) {
card.classList.add('selected');
}
const mainIconHtml = api.mainIcon
? `<img src="${api.mainIcon}" alt="${api.apiName}" onerror="this.style.display='none'; this.nextElementSibling.style.display='block'"><i class="fas fa-cube" style="display:none"></i>`
: `<i class="fas fa-cube"></i>`;
card.innerHTML = `
<div class="api-card-content">
<div class="api-card-header">
<span class="api-card-category">${api.service || '카테고리'}</span>
<div class="checkbox-wrapper">
<input type="checkbox"
name="selectedApis"
value="${api.apiId}"
id="api-${api.apiId}"
class="api-checkbox visually-hidden"
${isSelected ? 'checked' : ''}>
<span class="custom-checkbox"></span>
</div>
</div>
<h3 class="api-name">${api.apiName || 'API 이름'}</h3>
<p class="api-description">${api.apiSimpleDescription || 'API 설명이 없습니다.'}</p>
<div class="api-card-icon">
${mainIconHtml}
</div>
</div>
`;
return card;
}
// Attach event listeners to cards
function attachCardEventListeners() {
const checkboxes = document.querySelectorAll('.api-checkbox');
const apiCards = document.querySelectorAll('.api-selection-card');
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', function() {
updateCardSelection(this);
updateSelectedCount();
});
});
apiCards.forEach(card => {
card.addEventListener('click', function(e) {
// Prevent double toggle if clicking directly on checkbox
if (e.target.classList.contains('api-checkbox')) {
return;
}
const checkbox = card.querySelector('.api-checkbox');
if (checkbox) {
checkbox.checked = !checkbox.checked;
updateCardSelection(checkbox);
updateSelectedCount();
}
});
});
}
// Update card visual state
function updateCardSelection(checkbox) {
const card = checkbox.closest('.api-selection-card');
if (checkbox.checked) {
card.classList.add('selected');
selectedApis.add(checkbox.value);
} else {
card.classList.remove('selected');
selectedApis.delete(checkbox.value);
}
}
// Update selected count
function updateSelectedCount() {
// Update floating cart button
const floatingCartBtn = document.getElementById('floatingCartBtn');
const cartCount = document.querySelector('.cart-count');
if (selectedApis.size > 0) {
floatingCartBtn.style.display = 'flex';
cartCount.textContent = selectedApis.size;
} else {
floatingCartBtn.style.display = 'none';
}
// Update modal list
updateModalList();
// Update select all checkbox state
updateSelectAllCheckboxState();
}
// Update select all UI visibility and text
function updateSelectAllUI() {
const selectAllWrapper = document.getElementById('selectAllWrapper');
const selectAllText = document.getElementById('selectAllText');
if (currentFilter === '') {
// Hide select all when "전체" is selected
selectAllWrapper.style.display = 'none';
} else {
// Show select all with service name
selectAllWrapper.style.display = 'flex';
selectAllText.textContent = currentServiceName + ' API 전체 선택';
}
updateSelectAllCheckboxState();
}
// Update select all checkbox state based on visible cards
function updateSelectAllCheckboxState() {
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
const visibleCards = Array.from(document.querySelectorAll('.api-selection-card')).filter(card => card.style.display !== 'none');
if (visibleCards.length === 0) {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
return;
}
const visibleCheckboxes = visibleCards.map(card => card.querySelector('.api-checkbox'));
const checkedCount = visibleCheckboxes.filter(cb => cb.checked).length;
if (checkedCount === 0) {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
} else if (checkedCount === visibleCheckboxes.length) {
selectAllCheckbox.checked = true;
selectAllCheckbox.indeterminate = false;
} else {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = true;
}
}
// Update modal selected APIs list
function updateModalList() {
const modalSelectedList = document.getElementById('modalSelectedList');
modalSelectedList.innerHTML = '';
if (selectedApis.size === 0) {
modalSelectedList.innerHTML = '<p class="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 + '"]');
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');
if (card) {
const nameElement = card.querySelector('.api-name');
if (nameElement) {
apiName = nameElement.textContent;
}
}
} else {
// 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) {
api = allLoadedApis.find(a => a.apiId === apiId);
}
if (api && api.apiName) {
apiName = api.apiName;
}
}
const apiPill = document.createElement('div');
apiPill.className = 'api-pill';
apiPill.innerHTML = `
<span class="api-pill-name">${apiName}</span>
<button type="button" class="api-pill-remove" data-value="${apiId}" aria-label="Remove ${apiName}">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
`;
modalSelectedList.appendChild(apiPill);
});
// Add remove handlers
document.querySelectorAll('.api-pill-remove').forEach(function(btn) {
btn.addEventListener('click', function() {
const value = this.getAttribute('data-value');
const checkbox = document.querySelector('.api-checkbox[value="' + value + '"]');
if (checkbox) {
checkbox.checked = false;
updateCardSelection(checkbox);
updateSelectedCount();
} else {
// If checkbox not found (maybe different category is showing),
// still remove from selection
selectedApis.delete(value);
updateSelectedCount();
}
});
});
}
// Search functionality
if (searchInput) {
searchInput.addEventListener('input', function() {
const searchTerm = this.value.toLowerCase();
const apiCards = document.querySelectorAll('.api-selection-card');
apiCards.forEach(function(card) {
const apiName = card.getAttribute('data-name');
const apiDesc = card.getAttribute('data-desc');
// Check if matches search
const matchesSearch = apiName.includes(searchTerm) || apiDesc.includes(searchTerm);
if (matchesSearch) {
card.style.display = '';
} else {
card.style.display = 'none';
}
});
// Update select all checkbox state after search
updateSelectAllCheckboxState();
});
}
// Sidebar category selection
menuTitles.forEach(function(title) {
title.addEventListener('click', function(e) {
e.preventDefault();
// Remove active from all
menuTitles.forEach(t => t.classList.remove('active'));
// Add active to clicked
this.classList.add('active');
const groupId = this.getAttribute('data-group');
currentFilter = groupId;
// Store service name for select all label
currentServiceName = this.textContent.trim().split('\n')[0].trim();
// Load APIs for selected service
loadApis(groupId);
// Clear search when changing category
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', '메뉴 열기');
}
if (mobileToggle && sidebar && mobileOverlay) {
mobileToggle.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
sidebar.classList.toggle('mobile-open');
mobileOverlay.classList.toggle('active');
if (sidebar.classList.contains('mobile-open')) {
this.innerHTML = '✕';
this.setAttribute('aria-label', '메뉴 닫기');
} else {
this.innerHTML = '☰';
this.setAttribute('aria-label', '메뉴 열기');
}
});
// Close menu on overlay click
mobileOverlay.addEventListener('click', function() {
closeMobileMenu();
});
}
// Close mobile menu on resize to desktop
window.addEventListener('resize', function() {
if (window.innerWidth > 768 && sidebar.classList.contains('mobile-open')) {
closeMobileMenu();
}
});
// Modal control
const floatingCartBtn = document.getElementById('floatingCartBtn');
const selectedApisModal = document.getElementById('selectedApisModal');
const modalOverlay = document.getElementById('modalOverlay');
const modalCloseBtn = document.getElementById('modalCloseBtn');
const modalCancelBtn = document.getElementById('modalCancelBtn');
// Open modal
function openModal() {
selectedApisModal.style.display = 'block';
// Add show class to backdrop and modal for visibility
setTimeout(function() {
if (modalOverlay) {
modalOverlay.classList.add('show');
}
selectedApisModal.classList.add('show');
}, 10);
document.body.style.overflow = 'hidden'; // Prevent background scroll
}
// Close modal
function closeModal() {
// Remove show class first for transition
if (modalOverlay) {
modalOverlay.classList.remove('show');
}
selectedApisModal.classList.remove('show');
// Hide modal after transition
setTimeout(function() {
selectedApisModal.style.display = 'none';
}, 300);
document.body.style.overflow = ''; // Restore scroll
}
// Floating cart button click
if (floatingCartBtn) {
floatingCartBtn.addEventListener('click', openModal);
}
// Modal overlay click
if (modalOverlay) {
modalOverlay.addEventListener('click', closeModal);
}
// Modal close button click
if (modalCloseBtn) {
modalCloseBtn.addEventListener('click', closeModal);
}
// Modal cancel button click
if (modalCancelBtn) {
modalCancelBtn.addEventListener('click', closeModal);
}
// Close modal on ESC key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && selectedApisModal.style.display === 'block') {
closeModal();
}
});
// Select All checkbox event
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
if (selectAllCheckbox) {
selectAllCheckbox.addEventListener('change', function() {
const isChecked = this.checked;
const visibleCards = Array.from(document.querySelectorAll('.api-selection-card')).filter(card => card.style.display !== 'none');
visibleCards.forEach(function(card) {
const checkbox = card.querySelector('.api-checkbox');
if (checkbox) {
checkbox.checked = isChecked;
updateCardSelection(checkbox);
}
});
updateSelectedCount();
});
}
// Previous step button - save current selections before going back
const btnPrevStep = document.getElementById('btnPrevStep');
if (btnPrevStep) {
btnPrevStep.addEventListener('click', function(e) {
e.preventDefault();
// Change form action to save endpoint
const originalAction = form.action;
form.action = '/myapikey/modify/step2/save';
// Submit the form to save selections
form.submit();
});
}
// Form validation
form.addEventListener('submit', function(e) {
// Allow submission even without selected APIs (user might want to remove all)
// But show confirmation
if (selectedApis.size === 0) {
if (!confirm('API를 선택하지 않으셨습니다. 계속하시겠습니까?')) {
e.preventDefault();
return false;
}
}
});
// 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>
</body>
</html>
@@ -0,0 +1,124 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_title_layout}">
<body>
<section layout:fragment="title">
<div class="page-title-banner">
<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">
<!-- Title Bar -->
<div class="register-title-bar">
<h2 class="register-title">앱 수정</h2>
<span class="register-subtitle">API를 이용하여 서비스할 앱 정보를 수정해주세요.</span>
</div>
<div class="register-progress">
<div class="progress-steps">
<!-- Step 1: 앱 정보입력 -->
<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">
</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 선택 -->
<div class="progress-step-item">
<div class="step-icon-wrapper">
<div class="step-icon">
<img th:src="@{/img/apikey_step2.png}" alt="API 선택" width="36" height="36">
</div>
</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>
<!-- Step 3: 앱 수정 완료 (Active) -->
<div class="progress-step-item active">
<div class="step-icon-wrapper">
<div class="step-icon">
<img th:src="@{/img/apikey_step3.png}" alt="앱 수정 완료" width="36" height="36">
</div>
</div>
<div class="step-label">앱 수정 완료</div>
</div>
</div>
</div>
<!-- Success Result -->
<div class="register-result success" th:if="${modificationComplete}">
<!-- Success Icon Animation -->
<div class="result-icon-wrapper">
<div class="result-icon success-icon">
<img th:src="@{/img/img_app_complete.png}">
</div>
</div>
<!-- Success Message -->
<div class="result-header">
<h1>앱 수정 요청이 완료되었습니다.</h1>
<p class="result-description">
담당자 승인 후 변경 사항이 적용됩니다. [앱 정보]화면에서 수정 내역을 확인할 수 있습니다.
</p>
</div>
<!-- API Key Display -->
<div class="result-content">
<!-- Action Buttons -->
<div class="result-actions">
<a href="/myapikey" class="btn btn-primary">
인증키 관리
</a>
</div>
</div>
</div>
<!-- Error Result (if needed) -->
<div class="register-result error" th:unless="${modificationComplete}" style="display: none;">
<div class="result-icon-wrapper">
<div class="result-icon error-icon">
<span></span>
</div>
</div>
<div class="result-header">
<h1>앱 수정 실패</h1>
<p class="result-description">
앱 수정 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.
</p>
</div>
<div class="result-actions">
<a href="/myapikey" class="btn btn-secondary">
다시 시도
</a>
<a href="/myapikey" class="btn btn-primary">
목록으로
</a>
</div>
</div>
</section>
</th:block>
<th:block layout:fragment="contentScript">
</th:block>
</body>
</html>
@@ -1,165 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
layout:decorate="~{layout/kbank_apikey_layout}">
<head>
<title>API 키 상세</title>
</head>
<body>
<section layout:fragment="contentFragment" class="content">
<div class="content_wrap">
<div class="sub_title3">
<h2 class="title">인증키 정보</h2>
</div>
<div class="inner i_cs h_inner11">
<div class="form_type">
<div class="info1 info_mypage">
<p class="title">
<span>인증키 이름</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp6">
<input type="hidden" id="prodClientId" th:value="${@encryptionUtil.encrypt(apiKey.clientid)}" class="common_input_type_1" disabled="disabled">
<input type="text" id="prodClientName" th:value="${apiKey.clientname}" class="common_input_type_1" disabled="disabled">
</div>
</div>
</div>
<div class="info1 info_mypage">
<p class="title">
<span>변경일</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp6">
<input type="text" th:value="${#temporals.format(apiKey.modifiedon, 'yyyy.MM.dd')}" name="text" class="common_input_type_1" placeholder="2024.06.01" disabled="disabled">
</div>
</div>
</div>
<div class="info1 info_mypage">
<p class="title">
<span>인증키 상태</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp6">
<input type="text" th:value="${apiKey.appstatus == '1' ? '활성화' : '비활성화'}" name="text" class="common_input_type_1" placeholder="활성화" disabled="disabled">
</div>
</div>
</div>
</div>
<div class="terms_box title">
<p>API 정보</p>
</div>
<div class="table_box">
<div class="pc-only">
<table class="table tb_key m_top" cellspacing="0" cellpadding="0" width="100%">
<colgroup>
<col width="60px">
<col width="166px">
<col width="230px">
<col width="120px">
</colgroup>
<thead>
<tr>
<th>No</th>
<th>서비스</th>
<th>API명</th>
<th>비고</th>
</tr>
</thead>
<tbody>
<tr th:each="api, apiStat : ${apiKey.apiList}">
<td th:text="${apiStat.count}">14</td>
<td th:text="${api.service}">공통</td>
<td th:text="${api.apiDesc}">회원여부조회</td>
<td>
</td>
</tr>
</tbody>
</table>
</div>
<div class="m-only">
<table class="table tb_key m_top" cellspacing="0" cellpadding="0" width="100%">
<colgroup>
<col width="46px">
<col width="90px">
<col width="140px">
<col width="60px">
</colgroup>
<thead>
<tr>
<th>No</th>
<th>서비스</th>
<th>API명</th>
<th>비고</th>
</tr>
</thead>
<tbody>
<tr th:each="api, apiStat : ${apiKey.apiList}">
<td th:text="${apiStat.count}">14</td>
<td th:text="${api.apiGroupId}">공통</td>
<td th:text="${api.apiDesc}">회원여부조회</td>
<td>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="btn_wrap bt_location">
<button type="button" sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" class="btn_add btn_change_prod_api">API 변경</button>
</div>
<div class="btn_inventory btn_mtop">
<a th:href="@{/myapikey/api_key_prod_page}" class="common_btn_type_1">목록</a>
</div>
<!-- // 기본 정보 -->
</div>
</div>
</section>
<section sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" layout:fragment="pagePopups" class="content pop">
<div th:insert="~{fragment/apikey-request :: apiKeyRequest}" class="popup_total" id="apiKeyRequestPopup" style="display:none; z-index: 1000;"></div>
<th:block th:replace="~{fragment/apikey-request :: apiKeyRequestScript}"></th:block>
<div th:insert="~{fragment/apikey-request-prod :: prodApiKeyRequest}" class="popup_total" id="apiKeyProdRequestPopup" style="display:none; z-index: 1000;"></div>
<th:block th:replace="~{fragment/apikey-request-prod :: prodApiKeyRequestScript}"></th:block>
</section>
<th:block layout:fragment="contentScript">
<script>
$(document).ready(function() {
activateSelect();
$('.btn_keydel').on('click', function(event) {
event.preventDefault();
$('.loading-overlay').show();
const requestData = {
clientId: document.getElementById("prodClientId").value,
type: 'DELETE'
};
$.ajax({
url: '/myapikey/api_key_request',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(requestData)
}).done(function(response) {
customPopups.showConfirm(response.msg, function() {
window.location = '[[@{/myapikey/api_key_request/history}]]';
});
}).fail(function(jqXHR, textStatus, errorThrown) {
customPopups.showAlert('API 삭제 요청 중 오류가 발생했습니다: ' + errorThrown);
}).always(function() {
$('.loading-overlay').hide();
});
});
});
</script>
</th:block>
</body>
</html>
@@ -1,89 +0,0 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kbank_apikey_layout}">
<body>
<section layout:fragment="contentFragment" class="content">
<div class="content_wrap">
<div class="sub_title3">
<h2 class="title">인증키 목록</h2>
</div>
<div class="inner i_cs h_inner8">
<div class="tab-wrap">
<div class="tabs">
<ul class="tab_nav tab_bt">
<li>
<a th:href="@{/myapikey}">개발</a>
</li>
<li class="active">
<a th:href="@{/myapikey/api_key_prod_page}">운영</a>
</li>
</ul>
<div class="tab active">
<div class="tb_board_list">
<div class="table_box">
<table class="table table_min" cellspacing="0" cellpadding="0" width="100%">
<colgroup>
<col width="60px">
<col width="230px">
<col width="120px">
<col width="100px">
<col width="120px">
</colgroup>
<thead>
<tr>
<th>No</th>
<th>이름</th>
<th>인증키상태</th>
<th>사용 API 수</th>
<th>마지막수정일</th>
</tr>
</thead>
<tbody>
<tr th:each="apikey, status : ${apiKeys}">
<td th:text="${status.index + 1}">1</td>
<td>
<a th:href="@{/myapikey/api_key_prod_detail(id=${@encryptionUtil.encrypt(apikey.clientid)})}" th:text="${apikey.clientname}">수신</a>
</td>
<td th:text="${apikey.appstatus == '1' ? '활성화' : '비활성화'}">활성화</td>
<td th:text="${apikey.apiList.size()}">1</td>
<td th:text="${#temporals.format(apikey.modifiedon, 'yyyy.MM.dd')}"></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="btn_wrap bt_location">
<button sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" type="button" class="btn_key btn_request_prod_api" id="requestApiKey">운영 인증키 신청</button>
</div>
<div class="pagination page_top" th:replace="~{fragment/pagination :: pagination(jsFunction='fn_select_page')}"></div>
<form name="listForm" th:action="@{/myapikey/api_key_prod_page}" method="get">
<input type="hidden" id="page" name="page"/>
</form>
</div>
</div>
</div>
</div>
</div>
</section>
<section sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" layout:fragment="pagePopups" class="content pop">
<div th:insert="~{fragment/apikey-request-prod :: prodApiKeyRequest}" id="apiKeyProdRequestPopup" class="popup_total" style="display: none"></div>
<th:block th:replace="~{fragment/apikey-request-prod :: prodApiKeyRequestScript}"></th:block>
</section>
<th:block layout:fragment="contentScript">
<script>
document.addEventListener('DOMContentLoaded', function() {
activateSelect();
});
function fn_select_page(pageNo) {
document.listForm.page.value = pageNo;
document.listForm.action = '[[@{/myapikey/api_key_prod_page}]]';
document.listForm.submit();
}
</script>
</th:block>
</body>
</html>
File diff suppressed because one or more lines are too long
@@ -0,0 +1,691 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_title_layout}">
<body>
<section layout:fragment="title">
<div class="page-title-banner">
<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">
<!-- Title Bar -->
<div class="register-title-bar">
<h2 class="register-title">앱 생성</h2>
<span class="register-subtitle">API를 이용하여 서비스할 앱 정보를 등록해주세요.</span>
</div>
<!-- Progress Indicator -->
<div class="register-progress">
<div class="progress-steps">
<!-- Step 1: 앱 정보입력 (Active) -->
<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">
</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 선택 -->
<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">
</div>
</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>
<!-- 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>
</div>
<div class="step-label">앱 생성 완료</div>
</div>
</div>
</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="registerStep2Form" method="post" th:action="@{/myapikey/register/step2}" class="register-form">
<!-- 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="registerStep2Form" class="btn-submit btn-primary">
저장
</button>
</div>
</section>
<!-- Floating Cart Button (Figma Design) -->
<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>
</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>
</div>
<div class="modal-body">
<div class="selected-apis-list" id="modalSelectedList">
<!-- Dynamically populated -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" id="modalCancelBtn">닫기</button>
</div>
</div>
</div>
</th:block>
<th:block layout:fragment="contentScript">
<script th:inline="javascript">
document.addEventListener('DOMContentLoaded', function() {
// DOM Elements
const form = document.getElementById('registerStep2Form');
const searchInput = document.getElementById('apiSearch');
const sidebar = document.getElementById('apiSidebar');
const mobileToggle = document.getElementById('mobileToggle');
const mobileOverlay = document.getElementById('mobileOverlay');
const menuTitles = document.querySelectorAll('.menu-title');
const apiCardGrid = document.getElementById('apiCardGrid');
const loadingState = document.getElementById('loadingState');
const emptyState = document.getElementById('emptyState');
let currentFilter = ''; // Empty string means "all"
let currentServiceName = '전체'; // Store current service name
let allApis = []; // Store loaded APIs
let selectedApis = new Set(); // Track selected API IDs
// Restore selected APIs from session
const sessionSelectedApis = /*[[${apiKeyRegistration.selectedApis}]]*/ [];
if (sessionSelectedApis && Array.isArray(sessionSelectedApis)) {
sessionSelectedApis.forEach(function(apiId) {
selectedApis.add(apiId);
});
}
// Load APIs via AJAX
function loadApis(groupId) {
// Show loading state
loadingState.style.display = 'block';
emptyState.style.display = 'none';
// Remove existing API cards
document.querySelectorAll('.api-selection-card').forEach(card => card.remove());
// Build URL with optional groupId filter
let url = '/apis/for_request';
if (groupId) {
url += '?groupIds=' + encodeURIComponent(groupId);
}
fetch(url).then(response => response.json()).then(apis => {
allApis = apis;
loadingState.style.display = 'none';
if (apis.length === 0) {
emptyState.style.display = 'block';
return;
}
renderApiCards(apis);
updateSelectAllUI();
}).catch(error => {
console.error('Failed to load APIs:', error);
loadingState.style.display = 'none';
emptyState.querySelector('h3').textContent = 'API 로드 실패';
emptyState.querySelector('p').textContent = '다시 시도해주세요.';
emptyState.style.display = 'block';
});
}
// Render API cards
function renderApiCards(apis) {
const fragment = document.createDocumentFragment();
apis.forEach(api => {
const card = createApiCard(api);
fragment.appendChild(card);
});
apiCardGrid.appendChild(fragment);
// Re-attach event listeners
attachCardEventListeners();
}
// Create API card element
function createApiCard(api) {
const card = document.createElement('div');
card.className = 'api-selection-card';
card.setAttribute('data-group', api.apiGroupId || '');
card.setAttribute('data-name', (api.apiName || '').toLowerCase());
card.setAttribute('data-desc', (api.apiSimpleDescription || '').toLowerCase());
card.setAttribute('data-api-id', api.apiId);
const isSelected = selectedApis.has(api.apiId);
if (isSelected) {
card.classList.add('selected');
}
const mainIconHtml = api.mainIcon
? `<img src="${api.mainIcon}" alt="${api.apiName}" onerror="this.style.display='none'; this.nextElementSibling.style.display='block'"><i class="fas fa-cube" style="display:none"></i>`
: `<i class="fas fa-cube"></i>`;
card.innerHTML = `
<div class="api-card-content">
<div class="api-card-header">
<span class="api-card-category">${api.service || '카테고리'}</span>
<div class="checkbox-wrapper">
<input type="checkbox"
name="selectedApis"
value="${api.apiId}"
id="api-${api.apiId}"
class="api-checkbox visually-hidden"
${isSelected ? 'checked' : ''}>
<span class="custom-checkbox"></span>
</div>
</div>
<h3 class="api-name">${api.apiName || 'API 이름'}</h3>
<p class="api-description">${api.apiSimpleDescription || 'API 설명이 없습니다.'}</p>
<div class="api-card-icon">
${mainIconHtml}
</div>
</div>
`;
return card;
}
// Attach event listeners to cards
function attachCardEventListeners() {
const checkboxes = document.querySelectorAll('.api-checkbox');
const apiCards = document.querySelectorAll('.api-selection-card');
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', function() {
updateCardSelection(this);
updateSelectedCount();
});
});
apiCards.forEach(card => {
card.addEventListener('click', function(e) {
// Prevent double toggle if clicking directly on checkbox
if (e.target.classList.contains('api-checkbox')) {
return;
}
const checkbox = card.querySelector('.api-checkbox');
if (checkbox) {
checkbox.checked = !checkbox.checked;
updateCardSelection(checkbox);
updateSelectedCount();
}
});
});
}
// Update card visual state
function updateCardSelection(checkbox) {
const card = checkbox.closest('.api-selection-card');
if (checkbox.checked) {
card.classList.add('selected');
selectedApis.add(checkbox.value);
} else {
card.classList.remove('selected');
selectedApis.delete(checkbox.value);
}
}
// Update selected count
function updateSelectedCount() {
// Update floating cart button
const floatingCartBtn = document.getElementById('floatingCartBtn');
const cartCount = document.querySelector('.cart-count');
if (selectedApis.size > 0) {
floatingCartBtn.style.display = 'flex';
cartCount.textContent = selectedApis.size;
} else {
floatingCartBtn.style.display = 'none';
}
// Update modal list
updateModalList();
// Update select all checkbox state
updateSelectAllCheckboxState();
}
// Update select all UI visibility and text
function updateSelectAllUI() {
const selectAllWrapper = document.getElementById('selectAllWrapper');
const selectAllText = document.getElementById('selectAllText');
if (currentFilter === '') {
// Hide select all when "전체" is selected
selectAllWrapper.style.display = 'none';
} else {
// Show select all with service name
selectAllWrapper.style.display = 'flex';
selectAllText.textContent = currentServiceName + ' API 전체 선택';
}
updateSelectAllCheckboxState();
}
// Update select all checkbox state based on visible cards
function updateSelectAllCheckboxState() {
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
const visibleCards = Array.from(document.querySelectorAll('.api-selection-card')).filter(card => card.style.display !== 'none');
if (visibleCards.length === 0) {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
return;
}
const visibleCheckboxes = visibleCards.map(card => card.querySelector('.api-checkbox'));
const checkedCount = visibleCheckboxes.filter(cb => cb.checked).length;
if (checkedCount === 0) {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
} else if (checkedCount === visibleCheckboxes.length) {
selectAllCheckbox.checked = true;
selectAllCheckbox.indeterminate = false;
} else {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = true;
}
}
// Update modal selected APIs list
function updateModalList() {
const modalSelectedList = document.getElementById('modalSelectedList');
modalSelectedList.innerHTML = '';
if (selectedApis.size === 0) {
modalSelectedList.innerHTML = '<p class="empty-message">선택된 API가 없습니다.</p>';
return;
}
// Get all checked checkboxes
const checkedBoxes = document.querySelectorAll('.api-checkbox:checked');
checkedBoxes.forEach(function(checkbox) {
const card = checkbox.closest('.api-selection-card');
const apiName = card.querySelector('.api-name').textContent;
const apiPill = document.createElement('div');
apiPill.className = 'api-pill';
apiPill.innerHTML = `
<span class="api-pill-name">${apiName}</span>
<button type="button" class="api-pill-remove" data-value="${checkbox.value}" aria-label="Remove ${apiName}">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
`;
modalSelectedList.appendChild(apiPill);
});
// Add remove handlers
document.querySelectorAll('.api-pill-remove').forEach(function(btn) {
btn.addEventListener('click', function() {
const value = this.getAttribute('data-value');
const checkbox = document.querySelector('.api-checkbox[value="' + value + '"]');
if (checkbox) {
checkbox.checked = false;
updateCardSelection(checkbox);
updateSelectedCount();
}
});
});
}
// Search functionality
if (searchInput) {
searchInput.addEventListener('input', function() {
const searchTerm = this.value.toLowerCase();
const apiCards = document.querySelectorAll('.api-selection-card');
apiCards.forEach(function(card) {
const apiName = card.getAttribute('data-name');
const apiDesc = card.getAttribute('data-desc');
// Check if matches search
const matchesSearch = apiName.includes(searchTerm) || apiDesc.includes(searchTerm);
if (matchesSearch) {
card.style.display = '';
} else {
card.style.display = 'none';
}
});
// Update select all checkbox state after search
updateSelectAllCheckboxState();
});
}
// Sidebar category selection
menuTitles.forEach(function(title) {
title.addEventListener('click', function(e) {
e.preventDefault();
// Remove active from all
menuTitles.forEach(t => t.classList.remove('active'));
// Add active to clicked
this.classList.add('active');
const groupId = this.getAttribute('data-group');
currentFilter = groupId;
// Store service name for select all label
currentServiceName = this.textContent.trim().split('\n')[0].trim();
// Load APIs for selected service
loadApis(groupId);
// Clear search when changing category
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', '메뉴 열기');
}
if (mobileToggle && sidebar && mobileOverlay) {
mobileToggle.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
sidebar.classList.toggle('mobile-open');
mobileOverlay.classList.toggle('active');
if (sidebar.classList.contains('mobile-open')) {
this.innerHTML = '✕';
this.setAttribute('aria-label', '메뉴 닫기');
} else {
this.innerHTML = '☰';
this.setAttribute('aria-label', '메뉴 열기');
}
});
// Close menu on overlay click
mobileOverlay.addEventListener('click', function() {
closeMobileMenu();
});
}
// Close mobile menu on resize to desktop
window.addEventListener('resize', function() {
if (window.innerWidth > 768 && sidebar.classList.contains('mobile-open')) {
closeMobileMenu();
}
});
// Modal control
const floatingCartBtn = document.getElementById('floatingCartBtn');
const selectedApisModal = document.getElementById('selectedApisModal');
const modalOverlay = document.getElementById('modalOverlay');
const modalCloseBtn = document.getElementById('modalCloseBtn');
const modalCancelBtn = document.getElementById('modalCancelBtn');
// Open modal
function openModal() {
selectedApisModal.style.display = 'block';
// Add show class to backdrop and modal for visibility
setTimeout(function() {
if (modalOverlay) {
modalOverlay.classList.add('show');
}
selectedApisModal.classList.add('show');
}, 10);
document.body.style.overflow = 'hidden'; // Prevent background scroll
}
// Close modal
function closeModal() {
// Remove show class first for transition
if (modalOverlay) {
modalOverlay.classList.remove('show');
}
selectedApisModal.classList.remove('show');
// Hide modal after transition
setTimeout(function() {
selectedApisModal.style.display = 'none';
}, 300);
document.body.style.overflow = ''; // Restore scroll
}
// Floating cart button click
if (floatingCartBtn) {
floatingCartBtn.addEventListener('click', openModal);
}
// Modal overlay click
if (modalOverlay) {
modalOverlay.addEventListener('click', closeModal);
}
// Modal close button click
if (modalCloseBtn) {
modalCloseBtn.addEventListener('click', closeModal);
}
// Modal cancel button click
if (modalCancelBtn) {
modalCancelBtn.addEventListener('click', closeModal);
}
// Close modal on ESC key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && selectedApisModal.style.display === 'block') {
closeModal();
}
});
// Select All checkbox event
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
if (selectAllCheckbox) {
selectAllCheckbox.addEventListener('change', function() {
const isChecked = this.checked;
const visibleCards = Array.from(document.querySelectorAll('.api-selection-card')).filter(card => card.style.display !== 'none');
visibleCards.forEach(function(card) {
const checkbox = card.querySelector('.api-checkbox');
if (checkbox) {
checkbox.checked = isChecked;
updateCardSelection(checkbox);
}
});
updateSelectedCount();
});
}
// Previous step button - save current selections before going back
const btnPrevStep = document.getElementById('btnPrevStep');
if (btnPrevStep) {
btnPrevStep.addEventListener('click', function(e) {
e.preventDefault();
// Change form action to save endpoint
const originalAction = form.action;
form.action = '/myapikey/register/step2/save';
// Submit the form to save selections
form.submit();
});
}
// Form validation
form.addEventListener('submit', function(e) {
if (selectedApis.size === 0) {
e.preventDefault();
if (confirm('API를 선택하지 않으셨습니다. 나중에 추가하시겠습니까?')) {
// Allow form submission without selected APIs
form.submit();
}
return false;
}
// Store selected APIs in sessionStorage
sessionStorage.setItem('registration_selectedApis', JSON.stringify(Array.from(selectedApis)));
});
// Initialize
updateSelectedCount(); // This will show count from restored session data
// Load all APIs automatically on page load (empty string = all APIs)
loadApis('');
// Load data from step 1 (if needed for display)
const appName = sessionStorage.getItem('registration_appName');
if (appName) {
console.log('App Name from Step 1:', appName);
}
// Log restored selections for debugging
if (selectedApis.size > 0) {
console.log('Restored ' + selectedApis.size + ' selected APIs from session:', Array.from(selectedApis));
}
});
</script>
</th:block>
</body>
</html>
@@ -0,0 +1,137 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_title_layout}">
<body>
<section layout:fragment="title">
<div class="page-title-banner">
<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">
<!-- Title Bar -->
<div class="register-title-bar">
<h2 class="register-title">앱 생성</h2>
<span class="register-subtitle">API를 이용하여 서비스할 앱 정보를 등록해주세요.</span>
</div>
<div class="register-progress">
<div class="progress-steps">
<!-- Step 1: 앱 정보입력 (Active) -->
<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">
</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 선택 -->
<div class="progress-step-item">
<div class="step-icon-wrapper">
<div class="step-icon">
<img th:src="@{/img/apikey_step2.png}" alt="API 선택" width="36" height="36">
</div>
</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>
<!-- Step 3: 앱 생성 완료 -->
<div class="progress-step-item active">
<div class="step-icon-wrapper">
<div class="step-icon">
<img th:src="@{/img/apikey_step3.png}" alt="앱 생성 완료" width="36" height="36">
</div>
</div>
<div class="step-label">앱 생성 완료</div>
</div>
</div>
</div>
<!-- Success Result -->
<div class="register-result success" th:if="${registrationComplete}">
<!-- Success Icon Animation -->
<div class="result-icon-wrapper">
<div class="result-icon success-icon">
<img th:src="@{/img/img_app_complete.png}">
</div>
</div>
<!-- Success Message -->
<div class="result-header">
<h1>앱 생성 요청이 완료되었습니다.</h1>
<p class="result-description">
담당자 앱 승인 후 [앱 정보]화면에서 Client ID와 Client Secret을 확인 할 수 있습니다.
</p>
</div>
<!-- API Key Display -->
<div class="result-content">
<!-- Action Buttons -->
<div class="result-actions">
<a href="/myapikey" class="btn btn-primary">
인증키 관리
</a>
</div>
</div>
</div>
<!-- Error Result (if needed) -->
<div class="register-result error" th:unless="${registrationComplete}" style="display: none;">
<div class="result-icon-wrapper">
<div class="result-icon error-icon">
<span></span>
</div>
</div>
<div class="result-header">
<h1>API Key 생성 실패</h1>
<p class="result-description">
API Key 생성 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.
</p>
</div>
<div class="result-actions">
<a href="/myapikey/register/step1" class="btn btn-secondary">
다시 시도
</a>
<a href="/myapikey" class="btn btn-primary">
목록으로
</a>
</div>
</div>
</section>
</th:block>
<th:block layout:fragment="contentScript">
<script th:inline="javascript">
document.addEventListener('DOMContentLoaded', function() {
// Clear session storage after successful registration
if (/*[[${registrationComplete}]]*/ false) {
sessionStorage.removeItem('registration_appName');
sessionStorage.removeItem('registration_appDescription');
sessionStorage.removeItem('registration_appType');
sessionStorage.removeItem('registration_environment');
sessionStorage.removeItem('registration_selectedApis');
}
});
</script>
</th:block>
</body>
</html>
@@ -1,191 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kbank_base_layout}">
<head>
<title>API 요청 상세</title>
</head>
<body>
<section class="content" layout:fragment="contentFragment">
<div class="content_wrap">
<div class="sub_title3">
<h2 class="title">운영 인증키 요청 정보</h2>
</div>
<div class="inner i_cs h_inner11">
<div class="form_type">
<div class="info1 info_mypage">
<p class="title">
<span>이름</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp6">
<input type="text" name="text" class="common_input_type_1" disabled="disabled" th:value="${appRequest.clientName}">
</div>
</div>
</div>
<div class="info1 info_mypage">
<p class="title">
<span>유형</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp6">
<input type="text" name="text" class="common_input_type_1" disabled="disabled" th:value="${appRequest.type.description}">
</div>
</div>
</div>
<div class="info1 info_mypage">
<p class="title">
<span>상태</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp6">
<input type="text" name="text" class="common_input_type_1" disabled="disabled" th:value="${appRequest.approval.approvalStatus.getDescription()}">
</div>
</div>
</div>
<div class="info1 info_mypage">
<p class="title">
<span>사유</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp6">
<input type="text" name="text" class="common_input_type_1" disabled="disabled" th:value="${appRequest.reason}">
</div>
</div>
</div>
<div class="info1 info_mypage">
<p class="title">
<span>요청일시</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp6">
<input type="text" name="text" class="common_input_type_1" disabled="disabled" th:value="${#temporals.format(appRequest.approval.createdDate, 'yyyy-MM-dd HH:mm')}">
</div>
</div>
</div>
</div>
<div class="terms_box title">
<p>API 정보</p>
</div>
<div class="table_box">
<div class="pc-only">
<table class="table tb_key m_top" cellspacing="0" cellpadding="0" width="100%">
<colgroup>
<col width="60px">
<col width="166px">
<col width="230px">
<col width="80px">
</colgroup>
<thead>
<tr>
<th>No</th>
<th>서비스</th>
<th>API명</th>
<th></th>
</tr>
</thead>
<tbody>
<tr th:each="api,status : ${appRequest.apiSpecList}"
th:class="${appRequest.type != null ?
((appRequest.type.toString() == 'MODIFY' or appRequest.type.toString() == 'PROD_MODIFY') ?
(#strings.isEmpty(appRequest.prevApiList) ? '' :
(!#strings.contains(appRequest.prevApiList, api.apiId) ? 'highlight_new' : '')) :
((appRequest.type.toString() == 'DELETE' or appRequest.type.toString() == 'PROD_DELETE') ? 'highlight_delete' : '')) : ''}">
<td th:text="${status.index + 1}"></td>
<td th:text="${api.service}"></td>
<td th:text="${api.apiName}"></td>
<td th:class="status"
th:text="${(appRequest.type.toString() =='MODIFY' or appRequest.type.toString() == 'PROD_MODIFY') ?
(#strings.isEmpty(appRequest.prevApiList) ? '' : (#strings.contains(appRequest.prevApiList, api.apiId) ? '' : '추가')) : ''}"></td>
</tr>
<tr class="highlight_delete"
th:if="${!#strings.isEmpty(appRequest.prevApiList) and (appRequest.type != null and
(appRequest.type.toString() == 'MODIFY' or appRequest.type.toString() == 'PROD_MODIFY'))}"
th:each="apiId : ${#strings.arraySplit(appRequest.prevApiList,',')}"
th:with="api=${@apiService.selectDetail(apiId)},
serviceDto=${@apiServiceService.findApiServiceByApiId(apiId)}"
th:unless="${#strings.contains(appRequest.apiList, apiId)}">
<td></td>
<td th:text="${serviceDto != null ? serviceDto.getGroupName() : ''}"></td>
<td th:text="${api.apiName}"></td>
<td th:class="status">삭제</td>
</tr>
</tbody>
</table>
</div>
<div class="m-only">
<table class="table tb_key m_top" cellspacing="0" cellpadding="0" width="100%">
<colgroup>
<col width="46px">
<col width="166px">
<col width="140px">
<col width="40px">
</colgroup>
<thead>
<tr>
<th>No</th>
<th>서비스</th>
<th>API명</th>
<th></th>
</tr>
</thead>
<tbody>
<tr th:each="api,status : ${appRequest.apiSpecList}"
th:class="${appRequest.type != null ?
((appRequest.type.toString() == 'MODIFY' or appRequest.type.toString() == 'PROD_MODIFY') ?
(#strings.isEmpty(appRequest.prevApiList) ? '' :
(!#strings.contains(appRequest.prevApiList, api.apiId) ? 'highlight_new' : '')) :
((appRequest.type.toString() == 'DELETE' or appRequest.type.toString() == 'PROD_DELETE') ? 'highlight_delete' : '')) : ''}">
<td th:text="${status.index + 1}"></td>
<td th:text="${api.service}"></td>
<td th:text="${api.apiName}"></td>
<td th:class="status"
th:text="${(appRequest.type.toString() =='MODIFY' or appRequest.type.toString() == 'PROD_MODIFY') ?
(#strings.isEmpty(appRequest.prevApiList) ? '' : (#strings.contains(appRequest.prevApiList, api.apiId) ? '' : '추가')) : ''}"></td>
</tr>
<tr class="highlight_delete"
th:if="${!#strings.isEmpty(appRequest.prevApiList) and (appRequest.type != null and
(appRequest.type.toString() == 'MODIFY' or appRequest.type.toString() == 'PROD_MODIFY'))}"
th:each="apiId : ${#strings.arraySplit(appRequest.prevApiList,',')}"
th:with="api=${@apiService.selectDetail(apiId)},
serviceDto=${@apiServiceService.findApiServiceByApiId(apiId)}"
th:unless="${#strings.contains(appRequest.apiList, apiId)}">
<td></td>
<td th:text="${serviceDto != null ? serviceDto.getGroupName() : ''}"></td>
<td th:text="${api.apiName}"></td>
<td th:class="status">삭제</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="btn_wrap bt_location" th:if="${appRequest.approval.approvalStatus.toString() == 'REQUESTED'}">
<form th:action="@{/myapikey/api_key_request/cancel}" method="post">
<input type="hidden" name="id" th:value="${appRequest.id}">
<button type="submit" class="btn_del">취소</button>
</form>
</div>
<div class="btn_inventory btn_mtop">
<a th:href="@{/myapikey/api_key_request/prod_history}" class="common_btn_type_1">목록</a>
</div>
</div>
</div>
<div id="copyToast" class="toast" style="position: absolute; top: 20px; right: 20px;" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header">
<strong class="me-auto">알림</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
복사되었습니다
</div>
</div>
</section>
<script layout:fragment="script">
</script>
</body>
</html>
@@ -1,114 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kbank_apikey_layout}">
<body>
<section class="content" layout:fragment="contentFragment">
<div class="content_wrap">
<div class="sub_title3">
<h2 class="title">인증키 신청 목록</h2>
</div>
<div class="inner i_cs h_inner8">
<div class="tab-wrap">
<div class="tabs">
<ul class="tab_nav tab_bt">
<li>
<a th:href="@{/myapikey/api_key_request/history}">개발</a>
</li>
<li class="active">
<a th:href="@{/myapikey/api_key_request/prod_history}">운영</a>
</li>
</ul>
<div class="tab active">
<div class="tb_board_list">
<div class="table_box">
<div class="pc-only">
<table class="table table_min" cellspacing="0" cellpadding="0" width="100%">
<colgroup>
<col width="60px">
<col width="150px">
<col width="90px">
<col width="90px">
<col width="90px">
<col width="250px">
<col width="100px">
</colgroup>
<thead>
<tr>
<th>No</th>
<th>신청명</th>
<th>신청상태</th>
<th>신청일</th>
<th>처리일자</th>
<th>신청사유</th>
<th>사용 API 수</th>
</tr>
</thead>
<tbody>
<tr th:each="request, status : ${requests}">
<td th:text="${status.index + 1}">1</td>
<td><a th:href="@{/myapikey/api_key_request/prod_detail(id=${request.id})}" class="btn btn-sm btn-primary" th:text="'[' + ${request.clientName} + '] ' + ${request.type.description}"></a></td>
<td><span th:if="${request.approval != null}" th:text="${request.approval.approvalStatus.description}"></span></td>
<td><span th:if="${request.approval != null}" th:text="${#temporals.format(request.approval.createdDate, 'yyyy-MM-dd')}"></span></td>
<td><span th:if="${request.approval != null}" th:text="${#temporals.format(request.approval.approvalDate, 'yyyy-MM-dd')}"></span></td>
<td th:text="${request.reason}">1</td>
<td th:text="${#lists.size(#strings.arraySplit(request.apiList, ',').?[!#strings.isEmpty(#this)])}">0</td>
</tr>
</tbody>
</table>
</div>
<div class="m-only">
<table class="table table_min" cellspacing="0" cellpadding="0" width="100%">
<colgroup>
<col width="60px">
<col width="110px">
<col width="90px">
<col width="90px">
<col width="250px">
<col width="100px">
</colgroup>
<thead>
<tr>
<th>No</th>
<th>신청명</th>
<th>신청상태</th>
<th>신청일</th>
<th>신청사유</th>
<th>사용 API 수</th>
</tr>
</thead>
<tbody>
<tr th:each="request, status : ${requests}">
<td th:text="${status.index + 1}">1</td>
<td><a th:href="@{/myapikey/api_key_request/prod_detail(id=${request.id})}" class="btn btn-sm btn-primary" th:text="${request.clientName}"></a></td>
<td th:text="${request.status}">1</td>
<td th:text="${#temporals.format(request.createdDate, 'yyyy-MM-dd')}"></td>
<td th:text="${request.reason}">1</td>
<td th:text="${#lists.size(#strings.arraySplit(request.apiList, ',').?[!#strings.isEmpty(#this)])}">0</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="pagination" th:replace="~{fragment/pagination :: pagination(jsFunction='fn_select_page')}"></div>
<form name="listForm" th:action="@{/myapikey/api_key_request/history}" method="get">
<input type="hidden" id="page" name="page"/>
</form>
</div>
</div>
</div>
</div>
</div>
</section>
<th:block layout:fragment="contentScript">
<script>
function fn_select_page(pageNo) {
document.listForm.page.value = pageNo;
document.listForm.action = '[[@{/myapikey/api_key_request/history}]]';
document.listForm.submit();
}
</script>
</th:block>
</body>
</html>
@@ -0,0 +1,239 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
layout:decorate="~{layout/kjbank_title_layout}">
<head>
<title>API 키 신청 상세</title>
</head>
<body>
<section layout:fragment="title">
<div class="page-title-banner">
<img th:src="@{/img/img_title_bg.png}" class="title-image">
<h1>앱관리</h1>
</div>
</section>
<th:block layout:fragment="contentFragment">
<section class="apikey-detail-container">
<!-- Title Bar -->
<div class="register-title-bar">
<h2 class="register-title">API Key 신청 상세</h2>
<span class="register-subtitle">신청하신 API Key의 상세 정보를 확인할 수 있습니다.</span>
</div>
<!-- App Basic Info Card (Figma Design) -->
<div class="app-info-card">
<!-- App Icon -->
<div class="app-info-icon">
<img th:if="${appRequest.appIconFileId != null}"
th:src="@{/file/download(fileSn=1,fileId=${appRequest.appIconFileId})}"
alt="앱 아이콘"
class="app-icon-image">
<div th:unless="${appRequest.appIconFileId != null}" class="app-icon-placeholder">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21 15 16 10 5 21"></polyline>
</svg>
</div>
</div>
<!-- App Info -->
<div class="app-info-content">
<!-- Status Badge -->
<span class="app-status-badge"
th:if="${appRequest.approval != null}"
th:classappend="${appRequest.approval.approvalStatus.toString() == 'APPROVED' ? 'status-approved' :
(appRequest.approval.approvalStatus.toString() == 'REJECTED' ? 'status-rejected' : 'status-pending')}"
th:text="${appRequest.approval.approvalStatus.description}">
대기중
</span>
<!-- App Name -->
<h3 class="app-info-name" th:text="${appRequest.clientName}">앱 이름</h3>
<!-- App Description -->
<p class="app-info-description"
th:text="${appRequest.appDescription != null ? appRequest.appDescription : '앱 설명이 없습니다.'}">
여기에 입력한 앱 설명이 표시됩니다.
</p>
</div>
</div>
<!-- Request Info Section (Figma Design: node 985-1365) -->
<div class="apikey-info-section">
<!-- Client ID (if exists) -->
<div class="info-row info-row-with-action" th:if="${appRequest.clientId != null}">
<label class="info-label">Client ID</label>
<div class="info-value">
<div class="info-value-box with-copy" th:text="${appRequest.clientId}">K00000001</div>
<button type="button" class="btn-copy-action" th:data-secret="${appRequest.clientId}" onclick="copyToClipboardFromButton(this)">
+ 복사
</button>
</div>
</div>
<!-- Callback URL -->
<div class="info-row">
<label class="info-label">Callback URL</label>
<div class="info-value">
<div class="info-value-box" th:text="${appRequest.callbackUrl != null ? appRequest.callbackUrl : '설정되지 않음'}">
http://www.abcd.co.kr
</div>
</div>
</div>
<!-- IP Whitelist -->
<div class="info-row">
<label class="info-label">화이트리스트</label>
<div class="info-value">
<div class="info-value-box" th:text="${appRequest.ipWhitelist != null and !appRequest.ipWhitelist.isEmpty() ? appRequest.ipWhitelist : '설정되지 않음'}">
10.0.0.1, 10.0.0.2, 192.168.123.1
</div>
</div>
</div>
<!-- Selected APIs -->
<div class="info-row info-row-vertical">
<label class="info-label">API 목록</label>
<div class="info-value">
<div class="api-list-box">
<div class="api-list-item-figma" th:each="api : ${appRequest.apiSpecList}">
<span class="api-item-name" th:text="${api.apiName}">추가된 API 1</span>
<span class="api-item-status">승인대기</span>
</div>
<!-- Empty State -->
<div class="empty-state" th:if="${appRequest.apiSpecList == null or appRequest.apiSpecList.isEmpty()}" style="padding: 24px;">
<p>선택된 API가 없습니다.</p>
</div>
</div>
</div>
</div>
<!-- Created Date -->
<div class="info-row">
<label class="info-label">신청일시</label>
<div class="info-value">
<div class="info-value-box" th:text="${#temporals.format(appRequest.createdDate, 'yyyy.MM.dd HH:mm')}">
2025.11.22 12:00
</div>
</div>
</div>
<!-- Approval Date (if approved) -->
<div class="info-row" th:if="${appRequest.approval != null and appRequest.approval.approvalDate != null}">
<label class="info-label">승인일시</label>
<div class="info-value">
<div class="info-value-box" th:text="${#temporals.format(appRequest.approval.approvalDate, 'yyyy.MM.dd HH:mm')}">
2025.11.22 12:00
</div>
</div>
</div>
<!-- Reason (if exists) -->
<div class="info-row" th:if="${appRequest.approval != null and appRequest.reason != null and !appRequest.reason.isEmpty()}">
<label class="info-label">사유</label>
<div class="info-value">
<div class="info-value-box" th:text="${appRequest.reason}">
Approval reason
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="form-actions-center">
<button type="button"
sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"
class="btn btn-submit btn-danger"
th:if="${appRequest.approval != null and appRequest.approval.approvalStatus != null and
(appRequest.approval.approvalStatus.toString() == 'PENDING' or appRequest.approval.approvalStatus.toString() == 'REQUESTED')}"
th:data-request-id="${appRequest.id}"
onclick="cancelRequestById(this)">
신청 취소
</button>
<a th:href="@{/myapikey}" class="btn btn-submit btn-secondary">
목록
</a>
</div>
</section>
</th:block>
<th:block layout:fragment="contentScript">
<script th:inline="javascript">
/*<![CDATA[*/
// Copy to clipboard function
function copyToClipboardFromButton(button) {
var text = $(button).data('secret');
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function() {
alert('클립보드에 복사되었습니다.');
}).catch(function(err) {
console.error('Failed to copy:', err);
fallbackCopy(text);
});
} else {
fallbackCopy(text);
}
}
function fallbackCopy(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
alert('클립보드에 복사되었습니다.');
} catch (err) {
alert('복사에 실패했습니다.');
}
document.body.removeChild(textArea);
}
// Cancel request function - called from button with data-request-id attribute
function cancelRequestById(button) {
var requestId = $(button).data('request-id');
if (!confirm('정말로 이 신청을 취소하시겠습니까?')) {
return;
}
$('.loading-overlay').show();
$.ajax({
url: '/myapikey/api_key_request/cancel',
type: 'POST',
data: {id: requestId},
dataType: 'json',
headers: {
'X-XSRF-TOKEN': /*[[${_csrf.token}]]*/ 'token'
}
}).done(function(response) {
if (response.success) {
alert(response.message || '신청이 취소되었습니다.');
window.location.href = /*[[@{/myapikey}]]*/ '/myapikey';
} else {
alert(response.message || '신청 취소 중 오류가 발생했습니다.');
$('.loading-overlay').hide();
}
}).fail(function(xhr, status, error) {
alert('신청 취소 중 오류가 발생했습니다.');
console.error('Cancel error:', error);
$('.loading-overlay').hide();
});
}
/*]]>*/
</script>
</th:block>
</body>
</html>
@@ -2,133 +2,98 @@
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<body>
<th:block th:fragment="commonUserInfo">
<div class="info1 info_mypage">
<p class="title w_tit">
<span>이메일 아이디</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp4">
<input type="text" th:value="${loginId}" name="loginId" class="common_input_type_1"
placeholder="이메일" disabled="disabled">
<input type="hidden" th:value="${loginId}" name="loginId">
</div>
<div class="form-row">
<div class="form-label-wrapper label-offset">
<span class="form-label-text">이메일 아이디</span>
</div>
<div class="form-field-wrapper">
<input type="text" th:value="${loginId}" name="loginId" class="form-input input-readonly"
placeholder="이메일" disabled="disabled">
<input type="hidden" th:value="${loginId}" name="loginId">
</div>
</div>
<div class="info1 info_mypage">
<p class="title w_tit">
<span class="dot">성명</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp4 c_inp">
<input type="text" th:value="${userName}" id="userName" name="userName" class="common_input_type_1"
placeholder="성명">
</div>
<div class="form-row">
<div class="form-label-wrapper label-offset">
<span class="form-label-text">성명</span> <span class="required-badge">필수</span>
</div>
<div class="form-field-wrapper">
<input type="text" th:value="${userName}" id="userName" name="userName" class="form-input"
placeholder="성명">
</div>
</div>
<div class="info1 info_mypage">
<p class="title">
<span class="dot">휴대폰 번호</span>
</p>
<div class="info_line">
<div class="info_box1">
<span class="input">
<div class="form-row">
<div class="form-label-wrapper label-offset">
<span class="form-label-text">휴대폰 번호</span> <span class="required-badge">필수</span>
</div>
<div class="form-field-wrapper input-with-button" th:with="parts=${#strings.arraySplit(mobileNumber, '-')}">
<div class="compound-input">
<input type="text"
th:with="parts=${#strings.arraySplit(mobileNumber, '-')}"
th:value="${parts != null and parts.length > 0 ? parts[0] : ''}"
name="mobilePrefix"
class="common_input_type_1"
class="form-input input-readonly"
maxlength="3"
disabled="disabled">
</span>
<span class="dash">-</span>
<span class="input">
<span class="separator">-</span>
<input type="text"
th:with="parts=${#strings.arraySplit(mobileNumber, '-')}"
th:value="${parts != null and parts.length > 1 ? parts[1] : ''}"
name="mobileMiddle"
class="common_input_type_1"
class="form-input input-readonly"
maxlength="4"
disabled="disabled">
</span>
<span class="dash">-</span>
<span class="input">
<span class="separator">-</span>
<input type="text"
th:with="parts=${#strings.arraySplit(mobileNumber, '-')}"
th:value="${parts != null and parts.length > 2 ? parts[2] : ''}"
name="mobileLast"
class="common_input_type_1"
class="form-input input-readonly"
maxlength="4"
disabled="disabled">
</span>
</div>
<div class="btn_check" id="changePhoneBtn">
<a href="#" class="common_btn_type_2 cbt change_mobile_phone"><span>변경</span></a>
</div>
<button type="button" class="btn-input-action btn-change change_mobile_phone" id="changePhoneBtn">변경</button>
</div>
</div>
<div class="info1 info_mypage" id="newPhoneNumberContainer" style="display: none;">
<p class="title">
<span class="dot">새 휴대폰 번호</span>
</p>
<div class="info_line">
<div class="info_box1">
<span class="input">
<div class="custom_select ct_sc h_inp2" id="phoneMobile">
<div class="select_selected">선택</div>
<ul class="common_selecttype select_hide select_items ct_sc2" id="newMobilePrefix">
<li>010</li>
<li>011</li>
<li>016</li>
<li>017</li>
<li>018</li>
<li>019</li>
</ul>
</div>
</span>
<span class="dash">-</span>
<span class="input">
<input type="text" id="newMobileMiddle" name="newMobileMiddle" class="common_input_type_1" maxlength="4"
placeholder="앞자리"/>
</span>
<span class="dash">-</span>
<span class="input">
<input type="text" id="newMobileLast" name="newMobileLast" class="common_input_type_1" maxlength="4"
placeholder="뒷자리"/>
</span>
<div class="form-row" id="newPhoneNumberContainer" style="display: none;">
<div class="form-label-wrapper label-offset">
<span class="form-label-text">새 휴대폰 번호</span> <span class="required-badge">필수</span>
</div>
<div class="form-field-wrapper input-with-button">
<div class="compound-input">
<select class="form-select phone-number" id="phoneMobile" name="newMobilePrefix">
<option value="">선택</option>
<option value="010">010</option>
<option value="011">011</option>
<option value="016">016</option>
<option value="017">017</option>
<option value="018">018</option>
<option value="019">019</option>
</select>
<span class="separator">-</span>
<input type="text" id="newMobileMiddle" name="newMobileMiddle" class="form-input phone-number"
maxlength="4" pattern="\d*" inputmode="numeric" placeholder="앞자리"/>
<span class="separator">-</span>
<input type="text" id="newMobileLast" name="newMobileLast" class="form-input phone-number"
maxlength="4" pattern="\d*" inputmode="numeric" placeholder="뒷자리"/>
<input type="hidden" value="" name="mobileNumber" id="mobileNumber">
</div>
<div class="btn_check">
<a class="common_btn_type_2 cbt btn_auth"
th:hx-post="@{/auth/request_auth_number}"
hx-include="#mobileNumber,input[name='_csrf']"
hx-target="#authNumberContainer"
hx-swap="none">
<span>인증번호 받기</span>
</a>
</div>
<button type="button" class="btn-input-action btn-auth btn_auth">
인증번호 받기
</button>
</div>
</div>
<div class="info1 info_mypage" id="authNumberContainer" style="display: none;">
<p class="title">
<span class="dot">인증번호 입력</span>
</p>
<div class="info_line">
<div class="info_box1">
<div class="certify_box">
<input type="text" id="authNumber" name="authNumber" maxlength="6" pattern="[0-9]*"
inputmode="numeric" class="common_input_type_1 w_input"
placeholder="SMS 인증번호">
<span class="certify_time" id="certify_time">03:00</span>
</div>
</div>
<div class="btn_check">
<a href="#none" class="common_btn_type_2 cbt btn_verify_auth"
th:hx-post="@{/auth/verify_auth_number}"
hx-include="#mobileNumber, #authNumber,input[name='_csrf']"
hx-target="#authNumber-validation"
hx-swap="none"><span>인증번호 확인</span></a>
<div class="form-row" id="authNumberContainer" style="display: none;">
<div class="form-label-wrapper label-offset">
<span class="form-label-text">인증번호 입력</span> <span class="required-badge">필수</span>
</div>
<div class="form-field-wrapper input-with-button">
<div class="auth-input-group">
<input type="text" id="authNumber" name="authNumber" maxlength="6" pattern="[0-9]*"
inputmode="numeric" class="form-input"
placeholder="SMS 인증번호 6자리">
<span class="auth-timer" id="certify_time">03:00</span>
</div>
<button type="button" class="btn-input-action btn-auth btn_verify_auth">
인증번호 확인
</button>
</div>
</div>
<div id="authNumber-validation"></div>
@@ -137,19 +102,19 @@
<script>
// 휴대폰 번호 조합 함수
function combineMobileNumber() {
let mobileElement = document.querySelector('#phoneMobile .select_selected');
if (!mobileElement) {
console.error('Mobile element not found');
let mobileSelect = document.getElementById('phoneMobile');
if (!mobileSelect) {
console.error('Mobile select element not found');
return null;
}
let prefix = mobileElement.textContent.trim();
let prefix = mobileSelect.value;
let middle = document.getElementById('newMobileMiddle')?.value.trim() || '';
let last = document.getElementById('newMobileLast')?.value.trim() || '';
console.log('values:', {prefix, middle, last});
if (prefix === '선택' || !middle || !last) {
if (!prefix || !middle || !last) {
return null;
}
@@ -167,12 +132,7 @@
// 전역 변수로 설정
window.isAuthVerified = false;
document.body.addEventListener('htmx:configRequest', function(evt) {
evt.detail.parameters['mobileNumber'] = combineMobileNumber(evt);
});
document.addEventListener('DOMContentLoaded', function () {
activateSelect();
let countdownInterval;
let isAuthVerified = false;
@@ -216,33 +176,15 @@
}
}
// 전화번호 입력 필드 숫자만 입력 가능
$('#newMobileMiddle, #newMobileLast, #authNumber').on('input', function () {
$(this).val($(this).val().replace(/[^0-9]/g, ''));
});
// select box 클릭 이벤트 처리
const selectBox = document.querySelector('#phoneMobile');
if (selectBox) {
selectBox.addEventListener('click', function (e) {
console.log('select box clicked', e.target.tagName);
if (e.target.tagName === 'LI') {
let selectedText = e.target.textContent;
console.log('selected value:', selectedText);
let selectedDiv = this.querySelector('.select_selected');
selectedDiv.textContent = selectedText;
this.querySelector('ul').classList.add('select_hide');
let combinedNumber = combineMobileNumber();
console.log('combined number after selection:', combinedNumber);
} else if (e.target.classList.contains('select_selected')) {
this.querySelector('ul').classList.toggle('select_hide');
}
});
}
// select 변경 이벤트
$('#phoneMobile').on('change', function () {
combineMobileNumber();
});
// 인증번호 검증 결과 처리 함수
function handleAuthNumberVerification(response) {
@@ -306,7 +248,7 @@
}
// 이벤트 리스너 등록
document.querySelectorAll('#phoneMobile .select_selected, #newMobileMiddle, #newMobileLast')
document.querySelectorAll('#newMobileMiddle, #newMobileLast')
.forEach(el => el.addEventListener('input', combineMobileNumber));
let authNumberInput = document.getElementById("authNumber");
@@ -341,35 +283,32 @@
const originalLast = document.querySelector('[name="mobileLast"]').value;
const originalMobileNumber = `${originalPrefix}-${originalMiddle}-${originalLast}`;
console.log('Original Mobile:', originalMobileNumber);
console.log('New Mobile:', newMobileNumber);
});
}
// 번호 동일성 체크 함수
function isSameMobileNumber() {
let newMobileNumber = combineMobileNumber();
if (!newMobileNumber) return false;
const originalPrefix = document.querySelector('[name="mobilePrefix"]').value;
const originalMiddle = document.querySelector('[name="mobileMiddle"]').value;
const originalLast = document.querySelector('[name="mobileLast"]').value;
const originalMobileNumber = `${originalPrefix}-${originalMiddle}-${originalLast}`;
return newMobileNumber === originalMobileNumber;
}
// HTMX beforeRequest 이벤트 핸들러
document.body.addEventListener('htmx:beforeRequest', function (evt) {
if (evt.target.classList.contains('btn_auth')) {
if (isSameMobileNumber()) {
evt.preventDefault();
// 동일한 번호인지 체크
if (newMobileNumber === originalMobileNumber) {
customPopups.showAlert("동일한 휴대폰 번호입니다.");
return;
}
}
});
console.log('Original Mobile:', originalMobileNumber);
console.log('New Mobile:', newMobileNumber);
// jQuery AJAX 호출
$.ajax({
url: /*[[@{/auth/request_auth_number}]]*/ '/auth/request_auth_number',
type: 'POST',
data: {
mobileNumber: newMobileNumber,
_csrf: $('input[name="_csrf"]').val()
},
success: function(response) {
handleAuthNumberRequest(response);
},
error: function(xhr, status, error) {
customPopups.showAlert("처리 중 오류가 발생했습니다. 다시 시도해주세요.");
}
});
});
}
// 인증번호 확인 버튼 이벤트 리스너
let verifyAuthButton = document.querySelector(".btn_verify_auth");
@@ -380,63 +319,43 @@
customPopups.showAlert("6자리 인증번호를 입력해주세요.");
return;
}
let mobileNumber = document.getElementById('mobileNumber').value;
// jQuery AJAX 호출
$.ajax({
url: /*[[@{/auth/verify_auth_number}]]*/ '/auth/verify_auth_number',
type: 'POST',
data: {
mobileNumber: mobileNumber,
authNumber: authNumber,
_csrf: $('input[name="_csrf"]').val()
},
success: function(response) {
handleAuthNumberVerification(response);
},
error: function(xhr, status, error) {
customPopups.showAlert("처리 중 오류가 발생했습니다. 다시 시도해주세요.");
}
});
});
}
// HTMX 이벤트 리스너
document.body.addEventListener('htmx:afterSwap', function (event) {
let targetId = event.detail.target ? event.detail.target.id : null;
if (targetId && ['authNumberContainer', 'authNumber-validation'].includes(targetId)) {
let response;
try {
response = typeof event.detail.xhr.response === 'string'
? JSON.parse(event.detail.xhr.response)
: event.detail.xhr.response;
} catch (e) {
console.error('Failed to parse response as JSON:', event.detail.xhr.response);
customPopups.showAlert("서버에서 예상치 못한 응답을 받았습니다.");
return;
}
switch (targetId) {
case 'authNumberContainer':
handleAuthNumberRequest(response);
break;
case 'authNumber-validation':
handleAuthNumberVerification(response);
break;
}
}
});
// 휴대폰 번호 변경 버튼 이벤트 리스너
const changeMobilePhoneBtn = document.querySelector('.change_mobile_phone');
const newPhoneNumberContainer = document.getElementById('newPhoneNumberContainer');
const authNumberContainer = document.getElementById('authNumberContainer');
const changePhoneBtn = document.getElementById('changePhoneBtn');
if (changeMobilePhoneBtn && newPhoneNumberContainer && authNumberContainer && changePhoneBtn) {
if (changeMobilePhoneBtn && newPhoneNumberContainer && authNumberContainer) {
changeMobilePhoneBtn.addEventListener('click', function (e) {
e.preventDefault();
newPhoneNumberContainer.style.display = '';
authNumberContainer.style.display = '';
changePhoneBtn.style.visibility = 'hidden';
changeMobilePhoneBtn.style.display = 'none';
});
}
document.body.addEventListener('htmx:responseError', function (event) {
customPopups.showAlert("처리 중 오류가 발생했습니다. 다시 시도해주세요.");
});
// HTMX 요청 전 파라미터 설정 추가
document.body.addEventListener('htmx:configRequest', function(evt) {
const mobileNumber = combineMobileNumber();
if (mobileNumber) {
evt.detail.parameters['mobileNumber'] = mobileNumber;
}
});
// 초기화 실행
initializeMobileNumber();
});
@@ -0,0 +1,284 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
layout:decorate="~{layout/kjbank_title_layout}">
<head>
<title>API 키 상세</title>
</head>
<body>
<section layout:fragment="title">
<div class="page-title-banner">
<img th:src="@{/img/img_title_bg.png}" class="title-image">
<h1>앱관리</h1>
</div>
</section>
<th:block layout:fragment="contentFragment">
<section class="apikey-detail-container">
<!-- Title Bar -->
<div class="register-title-bar">
<h2 class="register-title">앱 정보</h2>
<span class="register-subtitle">등록된 앱의 상세 정보를 확인할 수 있습니다.</span>
</div>
<!-- App Basic Info Card (Figma Design) -->
<div class="app-info-card">
<!-- App Icon -->
<div class="app-info-icon">
<img th:if="${apiKey.appIconFileId != null}"
th:src="@{/file/download(fileSn=1,fileId=${apiKey.appIconFileId})}"
alt="앱 아이콘"
class="app-icon-image">
<div th:unless="${apiKey.appIconFileId != null}" class="app-icon-placeholder">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21 15 16 10 5 21"></polyline>
</svg>
</div>
</div>
<!-- App Info -->
<div class="app-info-content">
<!-- Status Badge -->
<span class="app-status-badge"
th:classappend="${apiKey.appstatus == '1' ? 'status-approved' : 'status-inactive'}"
th:text="${apiKey.appstatus == '1' ? '승인' : '비활성화'}">
승인
</span>
<!-- App Name -->
<h3 class="app-info-name" th:text="${apiKey.clientname}">앱 이름</h3>
<!-- App Description -->
<p class="app-info-description"
th:text="${apiKey.appDescription != null ? apiKey.appDescription : '앱 설명이 없습니다.'}">
여기에 입력한 앱 설명이 표시됩니다.
</p>
</div>
</div>
<!-- Credential Info Section (Figma Design: node 985-1365) -->
<div class="apikey-info-section">
<!-- Client ID -->
<div class="info-row info-row-with-action">
<label class="info-label">Client ID</label>
<div class="info-value">
<div class="info-value-box with-copy" th:text="${apiKey.clientid}">K00000001</div>
<button type="button" class="btn-copy-action" th:data-secret="${apiKey.clientid}" onclick="copyToClipboardFromButton(this)">
+ 복사
</button>
</div>
</div>
<!-- Client Secret -->
<div class="info-row">
<label class="info-label">Client Secret</label>
<div class="info-value">
<!-- View Button (shown initially) -->
<button type="button" class="btn-view-secret" id="hiddenSecretBox" onclick="showPasswordPrompt()">
<span>조회</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.35-4.35"></path>
</svg>
</button>
<!-- Actual Secret (hidden initially) -->
<div id="revealedSecretBox" style="display: none;">
<div class="info-row info-row-with-action" style="margin-bottom: 0;">
<div class="info-value" style="display: flex; gap: 16px; align-items: center;">
<div class="info-value-box with-copy" th:text="${apiKey.clientsecret}">secret-key-67890</div>
<button type="button" class="btn-copy-action" th:data-secret="${apiKey.clientsecret}" onclick="copyToClipboardFromButton(this)">
+ 복사
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Callback URL -->
<div class="info-row">
<label class="info-label">Callback URL</label>
<div class="info-value">
<div class="info-value-box" th:text="${apiKey.redirecturi != null ? apiKey.redirecturi : '설정되지 않음'}">
http://www.abcd.co.kr
</div>
</div>
</div>
<!-- IP Whitelist -->
<div class="info-row">
<label class="info-label">화이트리스트</label>
<div class="info-value">
<div class="info-value-box" th:text="${apiKey.allowedips != null ? apiKey.allowedips : '설정되지 않음'}">
10.0.0.1, 10.0.0.2, 192.168.123.1
</div>
</div>
</div>
<!-- Registered APIs -->
<div class="info-row info-row-vertical">
<label class="info-label">API 목록</label>
<div class="info-value">
<div class="api-list-box">
<div class="api-list-item-figma" th:each="api : ${apiKey.apiList}">
<span class="api-item-name" th:text="${api.apiDesc}">추가된 API 1</span>
<span class="api-item-status">승인</span>
</div>
<!-- Empty State -->
<div class="empty-state" th:if="${apiKey.apiList == null or apiKey.apiList.isEmpty()}" style="padding: 24px;">
<p>등록된 API가 없습니다.</p>
</div>
</div>
</div>
</div>
<!-- Modified Date -->
<div class="info-row">
<label class="info-label">최종 수정일자</label>
<div class="info-value">
<div class="info-value-box" th:text="${#temporals.format(apiKey.modifiedon, 'yyyy.MM.dd')}">
2025.11.22
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="form-actions-center">
<a th:href="@{/myapikey}" class="btn btn-submit btn-secondary">
목록
</a>
<button type="button"
sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"
class="btn btn-submit btn-danger"
th:data-client-id="${apiKey.clientid}"
onclick="deleteApiKeyFromButton(this)">
인증키 삭제
</button>
<a sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"
class="btn btn-submit btn-primary"
th:href="@{/myapikey/modify/step1(clientId=${apiKey.clientid})}">
수정
</a>
</div>
</section>
</th:block>
<th:block layout:fragment="contentScript">
<script th:inline="javascript">
/*<![CDATA[*/
// Show password prompt for viewing client secret
function showPasswordPrompt() {
customPopups.showPasswordInput({
title: 'Client Secret 조회',
message: '보안을 위해 비밀번호를 입력해주세요.',
onConfirm: function(password) {
// Verify password via AJAX
$.ajax({
url: /*[[@{/myapikey/verify-password}]]*/ '/myapikey/verify-password',
type: 'POST',
data: {password: password},
headers: {
'X-XSRF-TOKEN': /*[[${_csrf.token}]]*/ 'token'
},
success: function(response) {
if (response.success) {
// Hide password popup
customPopups.hidePasswordInput();
// Reveal the client secret
$('#hiddenSecretBox').hide();
$('#revealedSecretBox').fadeIn(300);
} else {
// Show error message
customPopups.showPasswordError(response.message || '비밀번호가 일치하지 않습니다.');
}
},
error: function() {
customPopups.showPasswordError('오류가 발생했습니다. 다시 시도해주세요.');
}
});
},
onCancel: function() {
// User cancelled - do nothing
}
});
}
// Copy to clipboard function - called from button with data-secret attribute
function copyToClipboardFromButton(button) {
var text = $(button).data('secret');
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function() {
alert('클립보드에 복사되었습니다.');
}).catch(function(err) {
console.error('Failed to copy:', err);
fallbackCopy(text);
});
} else {
fallbackCopy(text);
}
}
function fallbackCopy(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
alert('클립보드에 복사되었습니다.');
} catch (err) {
alert('복사에 실패했습니다.');
}
document.body.removeChild(textArea);
}
// Delete API Key function - called from button with data-client-id attribute
function deleteApiKeyFromButton(button) {
var clientId = $(button).data('client-id');
if (!confirm('정말로 이 인증키를 삭제하시겠습니까?')) {
return;
}
$('.loading-overlay').show();
const requestData = {
clientId: clientId
};
$.ajax({
url: '/myapikey/api_key_delete',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(requestData),
headers: {
'X-XSRF-TOKEN': /*[[${_csrf.token}]]*/ 'token'
}
}).done(function(response) {
alert(response.msg || 'API Key가 삭제되었습니다.');
window.location.href = /*[[@{/myapikey}]]*/ '/myapikey';
}).fail(function(jqXHR, textStatus, errorThrown) {
alert('API 삭제 요청 중 오류가 발생했습니다: ' + errorThrown);
}).always(function() {
$('.loading-overlay').hide();
});
}
/*]]>*/
</script>
</th:block>
</body>
</html>
@@ -1,173 +0,0 @@
<!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 class="content" layout:fragment="contentFragment">
<div class="content_wrap">
<div class="dashboard_content h_inner2">
<p class="month_date"><span th:text="${#temporals.format(#temporals.createToday().minusDays(1), 'yyyy.MM.dd')}"></span> 기준</p>
<div class="dashboard_box bd_box">
<div class="icon1">
<img th:src="@{/img/icon/icon_board04.png}" alt="API키">
<p class="tit">API 키 현황</p>
</div>
<div class="api_use">
<ul>
<li>
<div class="use_box">
<p class="tit_m">테스트 키 승인 대기</p>
<p class="num_m"><a th:href="@{/myapikey/api_key_request/history}"><span th:text="${pendingKeyCount}">5</span></a></p>
</div>
</li>
<li>
<div class="use_box">
<p class="tit_m">테스트 키 보유 건수</p>
<p class="num_m"><a th:href="@{/myapikey/}"><span th:text="${testKeyCount}">1</span></a></p>
</div>
</li>
<li>
<div class="use_box">
<p class="tit_m">운영 승인 대기</p>
<p class="num_m"><a th:href="@{/myapikey/api_key_request/prod_history}"><span th:text="${pendingProdCount}">1</span></a></p>
</div>
</li>
<li>
<div class="use_box">
<p class="tit_m">운영 키 보유 건수</p>
<p class="num_m"><a th:href="@{/myapikey/api_key_prod_page}"><span th:text="${prodKeyCount}">1</span></a></p>
</div>
</li>
</ul>
</div>
</div>
<div class="dashboard_box bd_box1">
<div class="icon1">
<img th:src="@{/img/icon/icon_board01.png}" alt="이용API">
<p class="tit">이용 API</p>
</div>
<p class="num"><span th:text="${totalApi}">1000</span></p>
</div>
<div class="dashboard_box bd_box2">
<div class="icon1">
<img th:src="@{/img/icon/icon_board02.png}" alt="API사용량">
<p class="tit">API 사용량</p>
</div>
<div class="api_use">
<ul>
<li>
<div class="use_box">
<img th:src="@{/img/icon/icon_calendar_month.png}" alt="월간 사용량">
<p class="tit_m">월간 사용량</p>
<p class="num_m"><span th:text="${monthlyTotal}">999,999,999</span></p>
</div>
</li>
<li>
<div class="use_box">
<img th:src="@{/img/icon/icon_calendar_week.png}" alt="주간 사용량">
<p class="tit_m">주간 사용량</p>
<p class="num_m"><span th:text="${weeklyTotal}">999,999</span></p>
</div>
</li>
<li>
<div class="use_box">
<img th:src="@{/img/icon/icon_calendar_day.png}" alt="일간 사용량">
<p class="tit_m">일간 사용량</p>
<p class="num_m"><span th:text="${dailyTotal}">999,999</span></p>
</div>
</li>
</ul>
</div>
</div>
<div class="dashboard_box bd_box3">
<div class="icon1">
<img th:src="@{/img/icon/icon_board03.png}" alt="이용API">
<p class="tit">주 사용 API</p>
</div>
<div class="api_use_num">
<ul>
<li>
<img th:src="@{/img/icon/icon_circle_num01_1.png}" alt="1번">
<span th:if="${topApi != null && topApi.size() > 0}" th:text="${topApi[0].apiName}">API1</span>
</li>
<li>
<img th:src="@{/img/icon/icon_circle_num02_2.png}" alt="2번">
<span th:if="${topApi != null && topApi.size() > 0}" th:text="${topApi[1].apiName}">API2</span>
</li>
<li>
<img th:src="@{/img/icon/icon_circle_num03_3.png}" alt="3번">
<span th:if="${topApi != null && topApi.size() > 0}" th:text="${topApi[2].apiName}">API3</span>
</li>
</ul>
</div>
</div>
<div class="dashboard_box2 i_cs">
<h3>일별 API 이용현황</h3>
<div class="form_type" style="display: block; height: 64px;">
</div>
<!-- 달력 -->
<div class="datepicker_wrapper">
<input type="text" class="datepicker_input" name="daterange" id="daily_usage_from_to"
th:value="${#temporals.format(#temporals.createToday().withDayOfMonth(1), 'yyyy.MM.dd')} + ' - ' + ${#temporals.format(#temporals.createToday(), 'yyyy.MM.dd')}"/>
<img th:src="@{/img/icon/icon_datepicker.png}" class="datepicker_icon" alt="달력">
</div>
<!-- // 달력 -->
<div class="table_type api_tb" id="dailyUsage">
</div>
</div>
</div>
</div>
</section>
</body>
<th:block layout:fragment="contentScript">
<script th:src="@{/js/moment.min.js}"></script>
<script th:src="@{/js/daterangepicker.js}"></script>
<script>
$(function () {
let dateRange = $('#daily_usage_from_to').val().split(' - ');
let fromDate = dateRange[0];
let toDate = dateRange[1];
// Initial load with date range
loadDailyUsage(fromDate, toDate);
$('#daily_usage_from_to').on('apply.daterangepicker', function(ev, picker) {
loadDailyUsage(
picker.startDate.format('YYYY.MM.DD'),
picker.endDate.format('YYYY.MM.DD')
);
});
// Function to load daily usage data
function loadDailyUsage(fromDate, toDate) {
fromDate = fromDate.replace(/\./g, '');
toDate = toDate.replace(/\./g, '');
$.ajax({
url: '/dashboard/daily_usage',
method: 'GET',
data: {
fromDate: fromDate,
toDate: toDate
},
success: function(response) {
$('#dailyUsage').html(response);
},
error: function(xhr, status, error) {
$('#dailyUsage').html('<p class="error">Failed to load daily usage data. Please try again later.</p>');
console.error('Error loading daily usage:', error);
}
});
}
});
</script>
</th:block>
@@ -1,284 +0,0 @@
<!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/base_layout}">
<body>
<section layout:fragment="contentFragment">
<div class="container mt-2">
<div class="text-center">
<h3>API 키 관리</h3>
</div>
<div class="card mt-2">
<div class="card-body">
<h5 class="card-title">API 키 정보 등록</h5>
<form name="apiKey" id="apiKey" th:object="${apiKey}" th:action="${#strings.isEmpty(apiKey.id) == true}?@{/myapikey/register}:@{/myapikey/update}" method="post">
<th:block th:if="${#strings.isEmpty(apiKey.id) == false}">
<input type="hidden" name="id" id="apiKeyId" th:value="${apiKey.id}"/>
<input type="hidden" name="owner" id="owner" th:value="${apiKey.owner}"/>
</th:block>
<div class="form-floating">
<input type="text" name="name" class="form-control mb-2" id="name" th:value="${apiKey.name}" maxlength="80" required>
<label for="name">이용 용도</label>
<th:block th:each="err : ${#fields.errors('name')}">
<div th:text="${err}" class="invalid-feedback"></div>
</th:block>
</div>
<div class="form-floating">
<input type="text" name="description" class="form-control mb-2" id="description" maxlength="600" th:value="${apiKey.description}"/>
<label for="description">이용 목적</label>
</div>
<th:block th:if="${#strings.isEmpty(apiKey.id) == false}">
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="form-floating col-10 mt-2 me-2">
<input type="text" name="clientId" class="form-control" id="clientId" th:value="${apiKey.clientId}" readonly>
<label for="clientId">Client ID</label>
</div>
<button class="btn btn-outline-secondary col-2" type="button" id="button-copy">Copy</button>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="form-floating col-10 me-2">
<input type="text" name="clientSecret" class="form-control" id="clientSecret" th:value="${apiKey.clientSecret}" readonly>
<label for="clientSecret">Client Secret</label>
</div>
<button class="btn btn-outline-secondary col-2" type="button" id="button-copy2">Copy</button>
</div>
</th:block>
<input type="hidden" name="apiIds" id="apiIds"/>
</form>
</div>
<div class="card-body">
<div class="row">
<div class="col-6">
<div class="card">
<div class="card-header">
전체 API
</div>
<div class="card-body">
<table class="table" id="sourceTable">
<thead>
<tr>
<th>이름</th>
<th></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-6">
<div class="card">
<div class="card-header">
선택한 API
</div>
<div class="card-body">
<table class="table" id="targetTable">
<thead>
<tr>
<th scope="col">이름</th>
<th></th>
</tr>
</thead>
<tbody>
<tr th:each="api, status: ${apiKey.apiList}">
<td>
<div class="d-flex justify-content-between">
<span th:text="${api.name}" th:data-id="${api.id}" th:data-name="${api.name}"></span>
<button type="button" class="btn btn-primary btn-sm">제거</button>
</div>
</td>
<td></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="card-footer">
<button type="button" class="btn btn-primary btn_list" id="back"><i class="fa-solid fa-list"></i> 목록</button>
<button type="button" class="btn btn-primary btn_save" id="save"><i class="fa-sharp fa-solid fa-floppy-disk"></i> 저장</button>
</div>
</div>
</div>
</section>
</body>
<th:block layout:fragment="contentScript">
<script>
$(function () {
let url = '[[@{/api/routes/list}]]';
let model = {
sourceItems: [],
targetItems: []
};
let view = {
init: function () {
this.apiIds = $('#apiIds');
this.sourceTable = $('#sourceTable').DataTable({
data: model.sourceItems,
fixedColumns: true,
autoWidth: false,
columns: [
{data: 'name'},
{
data: null, width: '60px',
className : 'text-center',
render: function (data, type, row) {
let disabled = row.selected === true ? 'disabled="true" ' : '';
return '<button type="button" class="btn btn-primary btn-sm" data-id="' + row.id + '"' + disabled + '>추가</button>';
}
}
],
columnDefs: [
{ targets: 1, orderable: false }
],
language : {
paginate: {
first: '«',
previous: '',
next: '',
last: '»'
},
lengthMenu : "페이지 당 _MENU_ 개씩 보기",
info: '[총 _TOTAL_ 건 중 _START_ ~ _END_ 표시]'
}
});
this.targetTable = $('#targetTable').DataTable({
data: model.targetItems,
paging:false,
searching:false,
columns: [
{data: 'name'},
{
data: null, width: '60px',
className : 'text-center',
render: function (data, type, row) {
return '<button type="button" class="btn btn-primary btn-sm" data-id="' + row.id + '">제거</button>';
}
}
],
columnDefs: [
{ targets: 1, orderable: false }
],
language : {
info: '',
infoEmpty: '',
emptyTable : '선택된 API가 없습니다.'
}
});
$('#sourceTable').on('click', 'button', function () {
let id = $(this).data('id');
controller.addToSelected(id);
});
$('#targetTable').on('click', 'button', function () {
let id = $(this).data('id');
controller.removeFromSelected(id);
});
this.render();
},
render: function () {
this.sourceTable.rows().invalidate();
this.targetTable.clear();
this.targetTable.rows.add(model.targetItems);
this.targetTable.draw();
// this.targetTable.rows().invalidate();
let idString = model.targetItems.map(function (obj) {
return obj.id;
}).join(',');
view.apiIds.val(idString);
}
};
let controller = {
init: function () {
let data = {};
$('#targetTable tbody tr').each(function () {
let row = $(this);
let rowData = {
id: row.find('span').data('id'),
name: row.find('span').data('name')
};
model.targetItems.push(rowData);
});
$.ajax({
url: url,
method: 'POST',
contentType: 'application/json',
data: JSON.stringify(data),
success: function (data) {
model.sourceItems = data;
model.sourceItems.forEach(function (item) {
let foundItem = model.targetItems.find(function (obj) {
if (obj.id === item.id) {
return true;
}
});
item.selected = (foundItem !== undefined);
});
view.init();
},
error: function (jqXHR, textStatus, errorThrown) {
console.error('POST request failed: ' + textStatus + ', ' + errorThrown);
}
});
},
addToSelected: function (id) {
let foundObject = model.sourceItems.find(function (obj) {
return obj.id === id;
});
foundObject.selected = true;
model.targetItems.push(foundObject);
view.render();
},
removeFromSelected: function (id) {
let sourceFound = model.sourceItems.find(function (obj) {
return obj.id === id;
});
sourceFound.selected = false;
let foundObject = model.targetItems.find(function (obj) {
return obj.id === id;
});
let index = model.targetItems.indexOf(foundObject);
model.targetItems.splice(index, 1);
view.render();
}
};
$(".btn_save").on("click", function (event) {
let form = document.forms.apiKey;
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
} else {
form.submit();
}
form.classList.add('was-validated');
});
$(".btn_list").on("click", function (e) {
window.location.href = '[[@{/myapikey/list_view}]]';
});
controller.init();
})
</script>
</th:block>
</html>
@@ -1,132 +0,0 @@
<!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/base_layout}">
<body>
<section layout:fragment="contentFragment">
<div class="container mt-2">
<div class="text-center">
<h3>API 키 관리</h3>
</div>
<div class="card mt-2">
<div class="card-body" th:if="${apiKeys.size() > 0}">
<form name="deleteForm" th:action="@{/myapikey/delete}" method="POST">
<input type="hidden" value="" name="id" />
<input id="csrf" type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
</form>
<th:block th:each="apikey, status : ${apiKeys}">
<div class="card p-0 me-2 mt-2">
<div class="card-header">
<span>API 키 정보</span>
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item" th:text="'이름: ' + ${apikey.clientname}"></li>
</ul>
<div class="accordion accordion-flush" id="apiKeySecret" th:if="${apikey.state.toString() =='ACTIVE'}" >
<div class="accordion-item">
<h2 class="accordion-header" id="flush-headingOne">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#flush-collapseOne" aria-expanded="false" aria-controls="flush-collapseOne">
Client ID
</button>
</h2>
<div id="flush-collapseOne" class="accordion-collapse collapse" aria-labelledby="flush-headingOne" data-bs-parent="#apiKeySecret">
<div class="accordion-body">
<div class="d-flex justify-content-between mb-2">
<div class="form-floating" style="flex-grow: 1;">
<input type="text" name="clientid" class="form-control" id="clientid" th:value="${apikey.clientid}" readonly>
<label for="clientid">Client ID</label>
</div>
<button class="btn btn-primary ms-2" type="button" id="button-copy" data-target="#clientid">Copy</button>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="flush-headingTwo">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#flush-collapseTwo" aria-expanded="false" aria-controls="flush-collapseTwo">
Client Secret
</button>
</h2>
<div id="flush-collapseTwo" class="accordion-collapse collapse" aria-labelledby="flush-headingTwo" data-bs-parent="#apiKeySecret">
<div class="accordion-body">
<div class="d-flex justify-content-between mb-2">
<div class="form-floating" style="flex-grow: 1;">
<input type="text" name="clientsecret" class="form-control" id="clientsecret" th:value="${apikey.clientsecret}" readonly>
<label for="clientsecret">Client Secret</label>
</div>
<button class="btn btn-primary ms-2" type="button" data-target="#clientsecret">Copy</button>
</div>
</div>
</div>
</div>
</div>
<div class="card-footer">
<button class="btn btn-primary btn-sm" th:onclick="fnDeleteItem([[${apikey.clientid}]])">삭제</button>
</div>
</div>
</th:block>
<div class="toast" id="copyToast" style="position: absolute; top: 0; right: 0;">
<div class="toast-header">
<strong class="me-auto">Notification</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
복사되었습니다
</div>
</div>
</div>
<div class="card-body" th:if="${apiKeys.size() == 0}">
<h5 class="card-title">개발 키 추가</h5>
<p class="card-text">승인 후 개발환경에서 사용가능한 키가 발급됩니다.</p>
<h3><a th:href="@{/myapikey/create_view}" class="btn btn-primary">발급하기</a></h3>
</div>
</div>
</div>
</section>
</body>
<th:block layout:fragment="contentScript">
<script>
function fnDeleteItem(id) {
if (confirm("[[#{common.delete.msg}]]")) {
document.deleteForm.id.value = id;
document.deleteForm.submit();
}
}
$(document).ready(function(){
// Listen for click events on buttons with data-target attribute
$("button[data-target]").click(function(){
// Get the value of the data-target attribute
var targetId = $(this).attr("data-target");
// Select the input element by its ID and get its value
var textToCopy = $(targetId).val();
// Create a temporary textarea to hold the text to be copied
var $temp = $("<textarea>");
$("body").append($temp);
$temp.val(textToCopy).select();
// Execute the copy command
document.execCommand("copy");
// Remove the temporary textarea
$temp.remove();
// Show the Bootstrap Toast
var toast = new bootstrap.Toast(document.getElementById("copyToast"));
toast.show();
});
});
</script>
</th:block>
</html>
@@ -1,73 +1,67 @@
<!DOCTYPE html>
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:th="http://www.thymeleaf.org"
layout:decorate="~{layout/kbank_mypage_layout}">
layout:decorate="~{layout/kbank_signup_layout}">
<body>
<section layout:fragment="title">
<div class="page-title-banner">
<img th:src="@{/img/user_register_title.png}" class="title-image">
<h1>법인회원 전환</h1>
</div>
</section>
<section layout:fragment="contentFragment" class="content">
<script th:src="@{/js/htmx.min.js}"></script>
<div class="content_wrap">
<div class="sub_title3">
<h2 class="title">법인회원 전환</h2>
</div>
<div class="org-register-container">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<input type="hidden" name="registrationType" th:value="${registrationType}"/>
<div class="inner i_cs h_inner8">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<input type="hidden" name="registrationType" th:value="${registrationType}"/>
<!-- 약관동의 섹션 -->
<th:block
th:replace="apps/register/userAgreementContent :: agreementContent(${termsOfUse}, ${privacyCollect})"></th:block>
<!-- 약관동의 섹션 -->
<th:block
th:replace="apps/register/userAgreementContent :: agreementContent(${termsOfUse}, ${privacyCollect})"></th:block>
<!-- 법인 기본 정보 폼 -->
<form name="portalOrg" id="businessTransferForm" method="post" th:action="@{/mypage/org-transfer}"
th:object="${portalOrg}" enctype="multipart/form-data">
<!-- 법인 기본 정보 -->
<div class="terms_box">
<p>법인 기본 정보</p>
<span class="dot">필수 입력</span>
<div class="org-section-header">
<h3>법인 기본 정보</h3>
<span class="required-badge">필수 입력</span>
</div>
<!-- 법인 기본 정보 폼 -->
<form name="portalOrg" id="businessTransferForm" method="post" th:action="@{/mypage/org-transfer}"
th:object="${portalOrg}" enctype="multipart/form-data">
<th:block th:replace="~{apps/register/components/orgBasicInfoForm :: orgInfoForm}"></th:block>
<th:block th:replace="~{apps/register/components/orgBasicInfoForm :: orgInfoForm}"></th:block>
<!-- 법인 관리자 정보 -->
<div class="form_type">
<div class="terms_box">
<p>법인 관리자 정보</p>
<span class="dot">필수 입력</span>
</div>
<div class="top_info m_tbom">
<ul>
<li>법인 관리자는 API 신청 및 이용자 초대 등 모든 권한을 보유합니다.</li>
<li class="top">법인회원 전환은 반드시 법인이메일 주소로 입력하시기 바랍니다. 개인이메일로 전환 신청시, 법인 승인이 거절 될 수 있습니다</li>
</ul>
</div>
<div class="form_type">
<div class="info1 pt28">
<p class="title">
<span>이메일 아이디</span>
</p>
<div class="info_line">
<div class="info_box1">
<input type="text" th:value="${user.loginId}" name="loginId"
class="common_input_type_1 input-readonly"
placeholder="이메일" disabled="disabled">
<input type="hidden" th:value="${user.loginId}" name="loginId">
</div>
<div class="btn_check pc-only"></div>
</div>
</div>
<!-- 법인 관리자 정보 -->
<div class="org-section-header">
<h3>법인 관리자 정보</h3>
<span class="required-badge">필수 입력</span>
</div>
<div class="org-info-notice">
<ul>
<li>법인 관리자는 API 신청 및 이용자 초대 등 모든 권한을 보유합니다.</li>
<li>법인회원 전환은 반드시 법인이메일 주소로 입력하시기 바랍니다. 개인이메일로 전환 신청시, 법인 승인이 거절 될 수 있습니다</li>
</ul>
</div>
<div class="org-form-container">
<div class="org-form-group">
<label class="org-form-label">
<span>이메일 아이디</span>
</label>
<div class="org-form-input-wrapper">
<input type="text" th:value="${user.loginId}" name="loginId"
class="org-form-input input-readonly"
placeholder="이메일" disabled="disabled">
<input type="hidden" th:value="${user.loginId}" name="loginId">
</div>
</div>
</div>
<div class="form_type">
<div class="btn_application m_top">
<a class="common_btn_type_1 gray" id="cancelButton" th:href="@{/mypage}"><span>취소</span></a>
<div class="btn_gap"></div>
<a class="common_btn_type_1" id="submitButton"><span>법인회원 전환</span></a>
</div>
</div>
</form>
</div>
<!-- Action Buttons -->
<div class="org-action-buttons">
<button type="button" class="btn btn-secondary" id="cancelButton" th:onclick="|location.href='@{/mypage}'|">취소</button>
<button type="button" class="btn btn-primary" id="submitButton">법인회원 전환</button>
</div>
</form>
</div>
</section>
<th:block layout:fragment="contentScript">
@@ -128,29 +122,6 @@
return true;
}
// HTMX 이벤트 리스너 - 모든 HTMX 응답을 중앙에서 처리
document.body.addEventListener('htmx:afterRequest', function (event) {
const targetId = event.detail.target.id;
let response;
try {
response = JSON.parse(event.detail.xhr.response);
} catch (e) {
console.error('응답 JSON 파싱 실패:', e);
customPopups.showAlert('서버 응답을 처리할 수 없습니다.');
return;
}
// 응답 처리
if (response && response.message) {
if (response.status === 'SUCCESS') {
customPopups.showSuccess(response.message);
} else {
customPopups.showAlert(response.message);
}
}
});
// 제출 버튼 처리
document.querySelector('#submitButton').addEventListener('click', function (e) {
e.preventDefault();
@@ -162,26 +133,49 @@
return;
}
// FormData 생성
const formData = new FormData(form);
// 약관 동의 값 추가
const termsInput = document.createElement('input');
termsInput.type = 'hidden';
termsInput.name = 'termsOfUse';
termsInput.value = $('#termsOfUse').prop('checked');
form.appendChild(termsInput);
formData.append('termsOfUse', $('#termsOfUse').prop('checked'));
formData.append('privacyCollect', $('#privacyCollect').prop('checked'));
const privacyCollectInput = document.createElement('input');
privacyCollectInput.type = 'hidden';
privacyCollectInput.name = 'privacyCollect';
privacyCollectInput.value = $('#privacyCollect').prop('checked');
form.appendChild(privacyCollectInput);
// jQuery AJAX로 폼 제출
$.ajax({
url: /*[[@{/mypage/org-transfer}]]*/ '/mypage/org-transfer',
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(response) {
console.log('폼 제출 성공:', response);
// 폼 제출
try {
form.submit();
console.log('폼 제출 완료');
} catch (error) {
console.error('폼 제출 중 오류:', error);
}
// 응답이 JSON인 경우
if (typeof response === 'object') {
if (response.status === 'SUCCESS') {
customPopups.showSuccess(response.message || '법인회원 전환 신청이 완료되었습니다.', function() {
window.location.href = /*[[@{/mypage}]]*/ '/mypage';
});
} else {
customPopups.showAlert(response.message || '처리 중 오류가 발생했습니다.');
}
} else {
// 응답이 HTML인 경우 (페이지 리다이렉트)
window.location.href = /*[[@{/mypage}]]*/ '/mypage';
}
},
error: function(xhr, status, error) {
console.error('폼 제출 중 오류:', error);
// 서버에서 반환한 에러 메시지가 있으면 표시
let errorMessage = '처리 중 오류가 발생했습니다. 다시 시도해주세요.';
if (xhr.responseJSON && xhr.responseJSON.message) {
errorMessage = xhr.responseJSON.message;
}
customPopups.showAlert(errorMessage);
}
});
});
// 인증 완료 이벤트 리스너
@@ -1,60 +1,60 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/kbank_mypage_layout}">
layout:decorate="~{layout/kjbank_title_layout}">
<head>
<title>비밀번호 변경</title>
</head>
<body>
<section layout:fragment="title">
<div class="page-title-banner">
<img th:src="@{/img/user_register_title.png}" class="title-image">
<h1>비밀번호 변경</h1>
</div>
</section>
<section layout:fragment="contentFragment" class="content">
<div class="content_wrap">
<div class="sub_title3">
<h2 class="title">비밀번호 변경</h2>
<div class="org-register-container">
<div class="org-section-header">
<h3>소중한 계정 보호를 위해 비밀번호 변경해 주세요!</h3>
</div>
<div class="inner i_cs11 h_inner8">
<div class="con_title m_btm0">
<p class="tit_txt pc-only">소중한 계정 보호를 위해 <span>비밀번호를 변경</span>해 주세요!</p>
<p class="tit_txt m-only">소중한 계정 보호를 위해<br> <span>비밀번호를 변경</span>해 주세요!</p>
<form id="passwordChangeForm" th:action="@{/mypage/change_new_password}" method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<div class="org-form-container">
<div class="org-form-group">
<label class="org-form-label">
<span class="required">새 비밀번호</span>
</label>
<div class="org-form-input-wrapper">
<input type="password" name="newPassword" class="org-form-input"
placeholder="새 비밀번호 입력" required>
</div>
</div>
<div class="org-form-group">
<label class="org-form-label">
<span class="required">새 비밀번호 확인</span>
</label>
<div class="org-form-input-wrapper">
<input type="password" name="confirmPassword" class="org-form-input"
placeholder="새 비밀번호 확인 입력" required>
</div>
</div>
</div>
<form id="passwordChangeForm" th:action="@{/mypage/change_new_password}" method="post" class="form_type">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<div class="form_type_box">
<div class="info1 p_top8">
<p class="title">
<span class="dot">새 비밀번호</span>
</p>
<div class="info_line info_add">
<div class="info_box1 add add2">
<input type="password" name="newPassword" class="common_input_type_1"
placeholder="새 비밀번호 입력" required>
</div>
</div>
</div>
<div class="info1 info_mypage">
<p class="title">
<span class="dot">새 비밀번호 확인</span>
</p>
<div class="info_line info_add">
<div class="info_box1 add add2">
<input type="password" name="confirmPassword" class="common_input_type_1"
placeholder="새 비밀번호 확인 입력" required>
</div>
</div>
</div>
<div class="org-info-notice">
<ul>
<li>영문 대문자, 소문자, 숫자, 특수문자 포함 8~20자</li>
<li>아이디, 휴대전화, 3자리 이상 연속, 반복문자 사용 불가</li>
</ul>
</div>
<div class="list-dot info_text">
<p>영문 대문자, 소문자, 숫자, 특수문자 포함 8~20자 (아이디, 휴대전화, 3자리 이상 연속,반복문자 사용 불가)</p>
</div>
</div>
<div class="btn_application m_top">
<a href="#" onclick="history.back()" class="common_btn_type_1 gray"><span>취소</span></a>
<div class="btn_gap"></div>
<button type="submit" class="common_btn_type_1"><span>변경</span></button>
</div>
</form>
</div>
<div class="org-action-buttons">
<button type="button" class="btn btn-secondary" onclick="history.back()">취소</button>
<button type="submit" class="btn btn-primary">변경</button>
</div>
</form>
</div>
<script th:if="${error}" th:inline="javascript">
$(document).ready(function() {
@@ -63,4 +63,4 @@
</script>
</section>
</body>
</html>
</html>
@@ -1,38 +1,40 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/kbank_mypage_layout}">
layout:decorate="~{layout/kjbank_title_layout}">
<body>
<section layout:fragment="title">
<div class="page-title-banner">
<img th:src="@{/img/user_register_title.png}" class="title-image">
<h1>비밀번호 변경</h1>
</div>
</section>
<section layout:fragment="contentFragment" class="content">
<div class="content_wrap">
<div class="sub_title3">
<h2 class="title">비밀번호 변경</h2>
<div class="org-register-container">
<div class="org-section-header">
<h3>비밀번호 변경을 위하여 현재 비밀번호를 입력해 주세요</h3>
</div>
<div class="inner i_cs11 h_inner8">
<div class="con_title m_btm0">
<p class="tit_txt">비밀번호 변경을 위하여 <span>현재 비밀번호</span>를 입력해 주세요</p>
</div>
<form th:action="@{/verify_current_password}" method="post" class="form_type">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<div class="form_type_box">
<div class="info1 p_top8">
<p class="title">
<span class="dot">현재 비밀번호</span>
</p>
<div class="info_line info_add">
<div class="info_box1 add add2">
<input type="password" name="currentPassword" class="common_input_type_1" placeholder="비밀번호 입력" required>
</div>
</div>
<form th:action="@{/verify_current_password}" method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<div class="org-form-container">
<div class="org-form-group">
<label class="org-form-label">
<span class="required">현재 비밀번호</span>
</label>
<div class="org-form-input-wrapper">
<input type="password" name="currentPassword" class="org-form-input"
placeholder="비밀번호 입력" required>
</div>
</div>
<div class="btn_application m_top">
<a th:href="@{/}" class="common_btn_type_1 gray"><span>취소</span></a>
<div class="btn_gap"></div>
<button type="submit" class="common_btn_type_1"><span>확인</span></button>
</div>
</form>
</div>
</div>
<div class="org-action-buttons">
<button type="button" class="btn btn-secondary" th:onclick="|location.href='@{/}'|">취소</button>
<button type="submit" class="btn btn-primary">확인</button>
</div>
</form>
</div>
<script th:if="${error}" th:inline="javascript">
$(document).ready(function() {
@@ -41,4 +43,4 @@
</script>
</section>
</body>
</html>
</html>
@@ -1,314 +1,228 @@
<!DOCTYPE html>
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/kbank_mypage_layout}">
layout:decorate="~{layout/kjbank_title_layout}">
<body>
<section layout:fragment="contentFragment" class="content">
<div class="content_wrap">
<div class="sub_title3">
<h2 class="title">내 정보 관리</h2>
</div>
<div class="inner i_cs h_inner11">
<form id="updateForm" method="post" th:action="@{/mypage/update}" th:object="${user}">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<!-- 법인 기본 정보 -->
<div class="terms_box user_top">
<p>법인 기본 정보</p>
</div>
<div class="form_type">
<div class="info1">
<p class="title w_tit">
<span>제휴기관 ID</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp4">
<input type="text" name="organizationId" class="common_input_type_1"
th:value="${user.portalOrg.orgCode}"
placeholder="k000000001"
disabled="disabled" style="background:#F7F9FD;">
</div>
</div>
</div>
<div class="info1 info_mypage">
<p class="title w_tit">
<span>사업자등록정보</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp4 c_inp">
<input type="text" th:field="*{portalOrg.compRegNo}" id="compRegNo"
class="common_input_type_1"
style="display:none;">
<input type="text" pattern="\d*" inputmode="numeric"
th:value="${#strings.substring(user.portalOrg.compRegNo,0,3)}"
name="compRegNo1" class="common_input_type_1" maxlength="3">
<span class="dash">-</span>
<input type="text" pattern="\d*" inputmode="numeric"
th:value="${#strings.substring(user.portalOrg.compRegNo,3,5)}"
name="compRegNo2" class="common_input_type_1" maxlength="2">
<span class="dash">-</span>
<input type="text" pattern="\d*" inputmode="numeric"
th:value="${#strings.substring(user.portalOrg.compRegNo,5,10)}"
name="compRegNo3" class="common_input_type_1" maxlength="5">
</div>
</div>
</div>
<div class="info1 info_mypage">
<p class="title w_tit">
<span>법인등록번호</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp4 c_inp">
<input type="text" th:field="*{portalOrg.corpRegNo}" id="corpRegNo"
class="common_input_type_1"
style="display:none;">
<input type="text" pattern="\d*" inputmode="numeric"
th:value="${#strings.substring(user.portalOrg.corpRegNo,0,6)}"
name="corpRegNo1" class="common_input_type_1" maxlength="6">
<span class="dash">-</span>
<input type="text" pattern="\d*" inputmode="numeric"
th:value="${#strings.substring(user.portalOrg.corpRegNo,6,13)}"
name="corpRegNo2" class="common_input_type_1" maxlength="7">
</div>
</div>
</div>
<div class="info1 info_mypage">
<p class="title w_tit">
<span>법인명</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp4 c_inp">
<input type="text" th:field="*{portalOrg.orgName}" class="common_input_type_1">
</div>
</div>
</div>
<div class="info1 info_mypage">
<p class="title w_tit">
<span>대표자 성명</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp4 c_inp">
<input type="text" th:field="*{portalOrg.ceoName}" class="common_input_type_1">
</div>
</div>
</div>
<div class="info1 info_mypage">
<p class="title w_tit">
<span>사업장 소재지</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp4 c_inp">
<input type="text" th:field="*{portalOrg.orgAddr}" class="common_input_type_1">
</div>
</div>
</div>
<div class="info1 info_mypage">
<p class="title w_tit">
<span>업태</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp4 c_inp">
<input type="text" th:field="*{portalOrg.orgSectors}" class="common_input_type_1">
</div>
</div>
</div>
<div class="info1 info_mypage">
<p class="title w_tit">
<span>업종</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp4 c_inp">
<input type="text" th:field="*{portalOrg.orgIndustryType}" class="common_input_type_1">
</div>
</div>
</div>
<div class="info1 info_mypage">
<p class="title w_tit">
<span>회사 전화번호</span>
</p>
<div class="info_line info_add">
<div class="info_box1 w_inp4 c_inp" th:with="orgPhoneParts=${user.portalOrg.orgPhoneNumber != null ? user.portalOrg.orgPhoneNumber.split('-') : {}}">
<input type="text" th:field="*{portalOrg.orgPhoneNumber}" id="orgPhoneNumber"
class="common_input_type_1" style="display:none;">
<span class="input">
<div class="custom_select ct_sc h_inp2" id="orgPhonePrefix">
<div class="select_selected"
th:text="${orgPhoneParts[0] != null ? orgPhoneParts[0] : ''}"></div>
<ul class="common_selecttype select_items ct_sc2">
<li>국번없음</li>
<li>02</li>
<li>031</li>
<li>032</li>
<li>033</li>
<li>041</li>
<li>042</li>
<li>043</li>
<li>044</li>
<li>051</li>
<li>052</li>
<li>053</li>
<li>054</li>
<li>055</li>
<li>061</li>
<li>062</li>
<li>063</li>
<li>064</li>
<li>070</li>
</ul>
</div>
</span>
<span class="dash">-</span>
<span class="input">
<input type="text" pattern="\d*" inputmode="numeric"
th:value="${orgPhoneParts[1] != null ? orgPhoneParts[1] : ''}"
name="orgPhone1"
class="common_input_type_1" maxlength="4">
</span>
<span class="dash">-</span>
<span class="input">
<input type="text" pattern="\d*" inputmode="numeric"
th:value="${orgPhoneParts[2] != null ? orgPhoneParts[2] : ''}"
name="orgPhone2"
class="common_input_type_1" maxlength="4">
</span>
</div>
</div>
</div>
<div class="info1 info_mypage">
<p class="title w_tit">
<span>서비스명</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp4 c_inp">
<input type="text" th:field="*{portalOrg.serviceName}" class="common_input_type_1">
</div>
</div>
</div>
<div class="info1 info_mypage">
<p class="title w_tit">
<span>고객센터 전화번호</span>
</p>
<div class="info_line info_add">
<div class="info_box1 w_inp4 c_inp" th:with="scPhoneParts=${user.portalOrg.scPhoneNumber != null ? user.portalOrg.scPhoneNumber.split('-') : {}}">
<input type="text" th:field="*{portalOrg.scPhoneNumber}" id="scPhoneNumber"
class="common_input_type_1" style="display:none;">
<span class="input">
<div class="custom_select ct_sc h_inp2" id="scPhonePrefix">
<div class="select_selected"
th:text="${scPhoneParts[0] != null ? scPhoneParts[0] : ''}"></div>
<ul class="common_selecttype select_items ct_sc2">
<li>국번없음</li>
<li>02</li>
<li>031</li>
<li>032</li>
<li>033</li>
<li>041</li>
<li>042</li>
<li>043</li>
<li>044</li>
<li>051</li>
<li>052</li>
<li>053</li>
<li>054</li>
<li>055</li>
<li>061</li>
<li>062</li>
<li>063</li>
<li>064</li>
<li>070</li>
</ul>
</div>
</span>
<span class="dash">-</span>
<span class="input">
<input type="text" pattern="\d*" inputmode="numeric"
th:value="${scPhoneParts[1] != null ? scPhoneParts[1] : ''}"
name="scPhone1"
class="common_input_type_1" maxlength="4">
</span>
<span class="dash">-</span>
<span class="input">
<input type="text" pattern="\d*" inputmode="numeric"
th:value="${scPhoneParts[2] != null ? scPhoneParts[2] : ''}"
name="scPhone2"
class="common_input_type_1" maxlength="4">
</span>
</div>
</div>
</div>
</div>
<!-- 법인 관리자 정보 -->
<div class="terms_box user_top2">
<p>법인 관리자 정보</p>
<span class="dot">필수 입력</span>
</div>
<div class="form_type">
<th:block th:replace="~{apps/mypage/components/commonUserInfo :: commonUserInfo}"></th:block>
</div>
<!-- 사용 서버 IP 대역(화이트리스트) -->
<div class="terms_box user_top2">
<p>사용 서버 IP 목록(화이트리스트)</p>
</div>
<div class="tb_board_list ip_list">
<input type="hidden" id="ipWhitelist" th:field="*{portalOrg.ipWhitelist}"/>
<div class="table_box">
<table class="table table_min" cellspacing="0" cellpadding="0" width="100%">
<colgroup>
<col width="60px">
<col width="556px">
<col width="120px">
</colgroup>
<thead>
<tr>
<th>No</th>
<th>IP</th>
<th>삭제</th>
</tr>
</thead>
<tbody>
<tr th:each="ip, status : ${ipList}">
<td th:text="${status.count}">1</td>
<td th:text="${ip}">128.255.255.254</td>
<td>
<button type="button" class="btn_del">삭제</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="terms_box2">
<div class="form_type">
<div class="info1 info2">
<div class="btn_wrap bt_location2">
<p class="title w_tit2">
<span>사용 서버 IP 추가</span>
</p>
</div>
<div class="info_line info_w">
<div class="info_box1 info_box2">
<input type="text" pattern="^(\d{1,3}\.){0,3}\d{1,3}$" inputmode="numeric"
id="ipRangeStart" class="common_input_type_1 inp_wd1" maxlength="20"
placeholder="XXX.XXX.XXX.XXX">
</div>
</div>
<div class="api_box">
<button type="button" class="btn_add2">IP 추가</button>
</div>
</div>
</div>
</div>
<div class="form_type">
<div class="btn_application m_top">
<a class="common_btn_type_1 gray" th:href="@{/}"><span>취소</span></a>
<div class="btn_gap"></div>
<a class="common_btn_type_1"><span>수정 적용</span></a>
</div>
</div>
</form>
<div class="withdrawal">
<a>회원탈퇴</a>
</div>
</div>
<section layout:fragment="title">
<div class="page-title-banner">
<img th:src="@{/img/img_title_bg.png}" class="title-image">
<h1>내 정보 관리</h1>
</div>
</section>
<section layout:fragment="contentFragment">
<div class="org-register-container">
<div class="common-title-bar">
<h2 class="common-title">법인 기본 정보</h2>
</div>
<div class="register-form-container">
<form id="updateForm" method="post" th:action="@{/mypage/update}" th:object="${user}">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<!-- 법인 기본 정보 -->
<div class="register-form">
<div class="form-row">
<div class="form-label-wrapper label-offset">
<span class="form-label-text">제휴기관 ID</span>
</div>
<div class="form-field-wrapper">
<input type="text" name="organizationId" class="form-input input-readonly info-value-box"
th:value="${user.portalOrg.orgCode}"
placeholder="XXXXXX-01"
disabled="disabled">
</div>
</div>
<div class="form-row">
<div class="form-label-wrapper label-offset">
<span class="form-label-text">사업자등록정보</span>
</div>
<div class="form-field-wrapper">
<div class="compound-input">
<input type="text" th:field="*{portalOrg.compRegNo}" id="compRegNo"
class="form-input"
style="display:none;">
<input type="text" pattern="\d*" inputmode="numeric"
th:value="${#strings.substring(user.portalOrg.compRegNo,0,3)}"
name="compRegNo1" class="form-input" maxlength="3">
<span class="separator">-</span>
<input type="text" pattern="\d*" inputmode="numeric"
th:value="${#strings.substring(user.portalOrg.compRegNo,3,5)}"
name="compRegNo2" class="form-input" maxlength="2">
<span class="separator">-</span>
<input type="text" pattern="\d*" inputmode="numeric"
th:value="${#strings.substring(user.portalOrg.compRegNo,5,10)}"
name="compRegNo3" class="form-input" maxlength="5">
</div>
</div>
</div>
<div class="form-row">
<div class="form-label-wrapper label-offset">
<span class="form-label-text">법인등록번호</span>
</div>
<div class="form-field-wrapper">
<div class="compound-input">
<input type="text" th:field="*{portalOrg.corpRegNo}" id="corpRegNo"
class="form-input"
style="display:none;">
<input type="text" pattern="\d*" inputmode="numeric"
th:value="${#strings.substring(user.portalOrg.corpRegNo,0,6)}"
name="corpRegNo1" class="form-input" maxlength="6">
<span class="separator">-</span>
<input type="text" pattern="\d*" inputmode="numeric"
th:value="${#strings.substring(user.portalOrg.corpRegNo,6,13)}"
name="corpRegNo2" class="form-input" maxlength="7">
</div>
</div>
</div>
<div class="form-row">
<div class="form-label-wrapper label-offset">
<span class="form-label-text">법인명</span>
</div>
<div class="form-field-wrapper">
<input type="text" th:field="*{portalOrg.orgName}" class="form-input">
</div>
</div>
<div class="form-row">
<div class="form-label-wrapper label-offset">
<span class="form-label-text">대표자 성명</span>
</div>
<div class="form-field-wrapper">
<input type="text" th:field="*{portalOrg.ceoName}" class="form-input">
</div>
</div>
<div class="form-row">
<div class="form-label-wrapper label-offset">
<span class="form-label-text">사업장 소재지</span>
</div>
<div class="form-field-wrapper">
<input type="text" th:field="*{portalOrg.orgAddr}" class="form-input">
</div>
</div>
<div class="form-row">
<div class="form-label-wrapper label-offset">
<span class="form-label-text">업태</span>
</div>
<div class="form-field-wrapper">
<input type="text" th:field="*{portalOrg.orgSectors}" class="form-input">
</div>
</div>
<div class="form-row">
<div class="form-label-wrapper label-offset">
<span class="form-label-text">업종</span>
</div>
<div class="form-field-wrapper">
<input type="text" th:field="*{portalOrg.orgIndustryType}" class="form-input">
</div>
</div>
<div class="form-row">
<div class="form-label-wrapper label-offset">
<span class="form-label-text">회사 전화번호</span>
</div>
<div class="form-field-wrapper" th:with="orgPhoneParts=${user.portalOrg.orgPhoneNumber != null ? user.portalOrg.orgPhoneNumber.split('-') : {}}">
<div class="compound-input">
<input type="text" th:field="*{portalOrg.orgPhoneNumber}" id="orgPhoneNumber"
class="form-input" style="display:none;">
<select class="form-select phone-number" id="orgPhonePrefix">
<option value="">선택</option>
<option value="02" th:selected="${#lists.size(orgPhoneParts) > 0 && orgPhoneParts[0] == '02'}">02</option>
<option value="031" th:selected="${#lists.size(orgPhoneParts) > 0 && orgPhoneParts[0] == '031'}">031</option>
<option value="032" th:selected="${#lists.size(orgPhoneParts) > 0 && orgPhoneParts[0] == '032'}">032</option>
<option value="033" th:selected="${#lists.size(orgPhoneParts) > 0 && orgPhoneParts[0] == '033'}">033</option>
<option value="041" th:selected="${#lists.size(orgPhoneParts) > 0 && orgPhoneParts[0] == '041'}">041</option>
<option value="042" th:selected="${#lists.size(orgPhoneParts) > 0 && orgPhoneParts[0] == '042'}">042</option>
<option value="043" th:selected="${#lists.size(orgPhoneParts) > 0 && orgPhoneParts[0] == '043'}">043</option>
<option value="044" th:selected="${#lists.size(orgPhoneParts) > 0 && orgPhoneParts[0] == '044'}">044</option>
<option value="051" th:selected="${#lists.size(orgPhoneParts) > 0 && orgPhoneParts[0] == '051'}">051</option>
<option value="052" th:selected="${#lists.size(orgPhoneParts) > 0 && orgPhoneParts[0] == '052'}">052</option>
<option value="053" th:selected="${#lists.size(orgPhoneParts) > 0 && orgPhoneParts[0] == '053'}">053</option>
<option value="054" th:selected="${#lists.size(orgPhoneParts) > 0 && orgPhoneParts[0] == '054'}">054</option>
<option value="055" th:selected="${#lists.size(orgPhoneParts) > 0 && orgPhoneParts[0] == '055'}">055</option>
<option value="061" th:selected="${#lists.size(orgPhoneParts) > 0 && orgPhoneParts[0] == '061'}">061</option>
<option value="062" th:selected="${#lists.size(orgPhoneParts) > 0 && orgPhoneParts[0] == '062'}">062</option>
<option value="063" th:selected="${#lists.size(orgPhoneParts) > 0 && orgPhoneParts[0] == '063'}">063</option>
<option value="064" th:selected="${#lists.size(orgPhoneParts) > 0 && orgPhoneParts[0] == '064'}">064</option>
<option value="070" th:selected="${#lists.size(orgPhoneParts) > 0 && orgPhoneParts[0] == '070'}">070</option>
</select>
<span class="separator">-</span>
<input type="text" pattern="\d*" inputmode="numeric"
th:value="${#lists.size(orgPhoneParts) > 1 ? orgPhoneParts[1] : ''}"
name="orgPhone1"
class="form-input phone-number" maxlength="4">
<span class="separator">-</span>
<input type="text" pattern="\d*" inputmode="numeric"
th:value="${#lists.size(orgPhoneParts) > 2 ? orgPhoneParts[2] : ''}"
name="orgPhone2"
class="form-input phone-number" maxlength="4">
</div>
</div>
</div>
<div class="form-row">
<div class="form-label-wrapper label-offset">
<span class="form-label-text">서비스명</span>
</div>
<div class="form-field-wrapper">
<input type="text" th:field="*{portalOrg.serviceName}" class="form-input">
</div>
</div>
<div class="form-row">
<div class="form-label-wrapper label-offset">
<span class="form-label-text">고객센터 전화번호</span>
</div>
<div class="form-field-wrapper" th:with="scPhoneParts=${user.portalOrg.scPhoneNumber != null ? user.portalOrg.scPhoneNumber.split('-') : {}}">
<div class="compound-input">
<input type="text" th:field="*{portalOrg.scPhoneNumber}" id="scPhoneNumber"
class="form-input" style="display:none;">
<select class="form-select phone-number" id="scPhonePrefix">
<option value="">선택</option>
<option value="02" th:selected="${#lists.size(scPhoneParts) > 0 && scPhoneParts[0] == '02'}">02</option>
<option value="031" th:selected="${#lists.size(scPhoneParts) > 0 && scPhoneParts[0] == '031'}">031</option>
<option value="032" th:selected="${#lists.size(scPhoneParts) > 0 && scPhoneParts[0] == '032'}">032</option>
<option value="033" th:selected="${#lists.size(scPhoneParts) > 0 && scPhoneParts[0] == '033'}">033</option>
<option value="041" th:selected="${#lists.size(scPhoneParts) > 0 && scPhoneParts[0] == '041'}">041</option>
<option value="042" th:selected="${#lists.size(scPhoneParts) > 0 && scPhoneParts[0] == '042'}">042</option>
<option value="043" th:selected="${#lists.size(scPhoneParts) > 0 && scPhoneParts[0] == '043'}">043</option>
<option value="044" th:selected="${#lists.size(scPhoneParts) > 0 && scPhoneParts[0] == '044'}">044</option>
<option value="051" th:selected="${#lists.size(scPhoneParts) > 0 && scPhoneParts[0] == '051'}">051</option>
<option value="052" th:selected="${#lists.size(scPhoneParts) > 0 && scPhoneParts[0] == '052'}">052</option>
<option value="053" th:selected="${#lists.size(scPhoneParts) > 0 && scPhoneParts[0] == '053'}">053</option>
<option value="054" th:selected="${#lists.size(scPhoneParts) > 0 && scPhoneParts[0] == '054'}">054</option>
<option value="055" th:selected="${#lists.size(scPhoneParts) > 0 && scPhoneParts[0] == '055'}">055</option>
<option value="061" th:selected="${#lists.size(scPhoneParts) > 0 && scPhoneParts[0] == '061'}">061</option>
<option value="062" th:selected="${#lists.size(scPhoneParts) > 0 && scPhoneParts[0] == '062'}">062</option>
<option value="063" th:selected="${#lists.size(scPhoneParts) > 0 && scPhoneParts[0] == '063'}">063</option>
<option value="064" th:selected="${#lists.size(scPhoneParts) > 0 && scPhoneParts[0] == '064'}">064</option>
<option value="070" th:selected="${#lists.size(scPhoneParts) > 0 && scPhoneParts[0] == '070'}">070</option>
</select>
<span class="separator">-</span>
<input type="text" pattern="\d*" inputmode="numeric"
th:value="${#lists.size(scPhoneParts) > 1 ? scPhoneParts[1] : ''}"
name="scPhone1"
class="form-input phone-number" maxlength="4">
<span class="separator">-</span>
<input type="text" pattern="\d*" inputmode="numeric"
th:value="${#lists.size(scPhoneParts) > 2 ? scPhoneParts[2] : ''}"
name="scPhone2"
class="form-input phone-number" maxlength="4">
</div>
</div>
</div>
</div>
<!-- 법인 관리자 정보 -->
<div class="register-title-bar" style="padding-left: 0px;">
<h2 class="register-title">법인 관리자 정보</h2>
</div>
<div class="register-form">
<th:block th:replace="~{apps/mypage/components/commonUserInfo :: commonUserInfo}"></th:block>
</div>
<!-- Action Buttons -->
</form>
</div>
<div class="form-actions form-actions--with-withdrawal">
<a class="withdrawal-link"><img th:src="@{/img/btn_withdrawal.png}" alt="회원탈퇴">회원탈퇴</a>
<div class="form-actions-buttons">
<button type="button" class="btn btn-submit btn-secondary" th:onclick="|location.href='@{/}'|">취소</button>
<button type="button" class="btn btn-submit btn-primary">수정 적용</button>
</div>
</div>
</div>
</section>
<th:block layout:fragment="contentScript">
<script th:if="${error}" th:inline="javascript">
@@ -344,7 +258,6 @@
orgPhoneNumber: document.querySelector('#orgPhoneNumber').value,
serviceName: document.querySelector('input[name="portalOrg.serviceName"]').value,
scPhoneNumber: document.querySelector('#scPhoneNumber').value,
ipWhitelist: document.querySelector('#ipWhitelist').value
}
// 전화번호 및 입력 처리 유틸리티
@@ -518,42 +431,9 @@
scPhone1: 4,
scPhone2: 4
}
},
ipAddress: {
selectors: ['#ipRangeStart'],
pattern: /[^\d.]/g,
validate: (value) => {
const parts = value.split('.');
if (parts.length > 4) return false;
return parts.every(part => {
const num = parseInt(part);
return !isNaN(num) && num >= 0 && num <= 255;
});
}
}
};
// IP 목록 필드 업데이트 함수
function updateIpListField() {
// hidden input 필드 가져오기 또는 생성
let ipListInput = document.getElementById('ipWhitelist');
if (!ipListInput) {
ipListInput = document.createElement('input');
ipListInput.type = 'hidden';
ipListInput.id = 'ipWhitelist';
ipListInput.name = 'portalOrg.ipWhitelist';
document.getElementById('updateForm').appendChild(ipListInput);
}
// 테이블에서 IP 목록 수집
const ipList = Array.from(document.querySelectorAll('.table_min tbody tr td:nth-child(2)'))
.map(td => td.textContent.trim())
.join(',');
// hidden 필드에 값 설정
ipListInput.value = ipList;
}
// Hidden 필드 업데이트 함수
function updateHiddenFields() {
const form = document.getElementById('updateForm');
@@ -590,16 +470,16 @@
document.querySelector('[id="corpRegNo"]').value = corpRegNo;
}
// 전화번호 결합 (회사, 고객센터)
// 전화번호 결합 (회사, 고객센터) - org-form-select 사용
['org', 'sc'].forEach(type => {
const prefix = document.querySelector(`#${type}PhonePrefix .select_selected`);
const prefix = document.querySelector(`#${type}PhonePrefix`);
const phone1 = document.querySelector(`[name="${type}Phone1"]`);
const phone2 = document.querySelector(`[name="${type}Phone2"]`);
if (prefix && phone1 && phone2) {
// 하이픈을 명시적으로 추가
const phoneNumber = `${prefix.textContent}-${phone1.value}-${phone2.value}`;
if (phoneNumber) {
// select의 value 사용
const phoneNumber = `${prefix.value}-${phone1.value}-${phone2.value}`;
if (phoneNumber && prefix.value) {
document.querySelector(`[id="${type}PhoneNumber"]`).value = phoneNumber;
}
}
@@ -633,8 +513,6 @@
mobileNumberInput.value = existingNumber;
}
}
updateIpListField();
}
// 폼 정제 함수
@@ -662,89 +540,7 @@
return form;
}
// IP 추가 함수
function addIpRange() {
const ipInput = document.getElementById('ipRangeStart');
const ipValue = ipInput.value.trim();
// IP 주소 유효성 검사
const ipPattern = /^(\d{1,3}\.){3}\d{1,3}$/;
if (!ipPattern.test(ipValue)) {
customPopups.showAlert('올바른 IP 주소 형식이 아닙니다.');
return;
}
// IP 주소 범위 검사
const parts = ipValue.split('.');
const isValidRange = parts.every(part => {
const num = parseInt(part);
return num >= 0 && num <= 255;
});
if (!isValidRange) {
customPopups.showAlert('IP 주소는 0-255 범위의 숫자여야 합니다.');
return;
}
// IP 목록에 이미 있는지 확인
const existingIps = Array.from(document.querySelectorAll('.table_min tbody tr td:nth-child(2)'))
.map(td => td.textContent.trim());
if (existingIps.includes(ipValue)) {
customPopups.showAlert('이미 추가된 IP 주소입니다.');
return;
}
// 테이블에 새로운 행 추가
const tbody = document.querySelector('.table_min tbody');
const newRow = document.createElement('tr');
const currentRows = tbody.getElementsByTagName('tr');
const newRowNumber = currentRows.length + 1;
newRow.innerHTML = `
<td>${newRowNumber}</td>
<td>${ipValue}</td>
<td>
<button type="button" class="btn_del">삭제</button>
</td>
`;
tbody.appendChild(newRow);
// 새로 추가된 삭제 버튼에 이벤트 리스너 설정
newRow.querySelector('.btn_del').addEventListener('click', function () {
deleteIpRange(this);
});
// IP 입력 필드 초기화
ipInput.value = '';
// hidden 필드 업데이트
updateIpListField();
}
// IP 삭제 함수
function deleteIpRange(button) {
const row = button.closest('tr');
row.remove();
// 번호 재정렬
const rows = document.querySelectorAll('.table_min tbody tr');
rows.forEach((row, index) => {
row.querySelector('td:first-child').textContent = index + 1;
});
// hidden 필드 업데이트
updateIpListField();
}
// 삭제 버튼들에 이벤트 리스너 설정
function setupDeleteButtons() {
document.querySelectorAll('.btn_del').forEach(button => {
button.addEventListener('click', function() {
deleteIpRange(this);
});
});
}
// 숫자 입력 필드 이벤트 핸들러 설정
inputConfig.numericOnly.selectors.forEach(selector => {
document.querySelectorAll(selector).forEach(input => {
@@ -767,55 +563,16 @@
});
// IP 주소 입력 필드 이벤트 설정
const ipInput = document.getElementById('ipRangeStart');
if (ipInput) {
// IP 입력 필드 이벤트
ipInput.addEventListener('input', function(e) {
let value = e.target.value.replace(/[^\d.]/g, '');
const parts = value.split('.').slice(0, 4);
const validParts = parts.map(part => {
const num = parseInt(part);
return (!isNaN(num) && num <= 255) ? num : '';
});
e.target.value = validParts.join('.');
});
// Enter 키 입력 시 IP 추가
ipInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
addIpRange();
}
});
}
// IP 추가 버튼에 이벤트 리스너 설정
const addButton = document.querySelector('.btn_add2');
if (addButton) {
addButton.addEventListener('click', addIpRange);
}
// 전화번호 선택 이벤트
// 전화번호 선택 이벤트 (org-form-select에 대한 change 이벤트)
['org', 'sc'].forEach(type => {
const prefixSelect = document.querySelector(`#${type}PhonePrefix`);
if (prefixSelect) {
prefixSelect.addEventListener('click', updateHiddenFields);
const selectItems = prefixSelect.querySelectorAll('.select_items li');
selectItems.forEach(item => {
item.addEventListener('click', () => {
prefixSelect.querySelector('.select_selected').textContent = item.textContent;
updateHiddenFields();
});
});
prefixSelect.addEventListener('change', updateHiddenFields);
}
});
// 수정 버튼 이벤트
const submitButton = document.querySelector('.common_btn_type_1:not(.gray)');
const submitButton = document.querySelector('.btn-primary:not(.btn_add2)');
if (submitButton) {
submitButton.addEventListener('click', function(e) {
e.preventDefault();
@@ -833,8 +590,7 @@
orgIndustryType: document.querySelector('input[name="portalOrg.orgIndustryType"]').value,
orgPhoneNumber: document.querySelector('#orgPhoneNumber').value,
serviceName: document.querySelector('input[name="portalOrg.serviceName"]').value,
scPhoneNumber: document.querySelector('#scPhoneNumber').value,
ipWhitelist: document.querySelector('#ipWhitelist').value
scPhoneNumber: document.querySelector('#scPhoneNumber').value
};
// 변경사항 체크
@@ -911,9 +667,6 @@
return;
}
// IP 목록 업데이트
updateIpListField();
// 모든 hidden 필드 업데이트
updateHiddenFields();
@@ -934,8 +687,6 @@
});
}
// 초기 설정 실행
setupDeleteButtons();
updateHiddenFields();
initializePhoneNumbers();
});
@@ -1,45 +1,45 @@
<!DOCTYPE html>
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/kbank_mypage_layout}">
layout:decorate="~{layout/kjbank_title_layout}">
<body>
<section layout:fragment="contentFragment" class="content">
<div class="content_wrap">
<div class="sub_title3">
<h2 class="title">내 정보 관리</h2>
</div>
<div class="inner i_cs h_inner8">
<form id="updateForm" method="post" th:action="@{/mypage/update}">
<div class="terms_box user_top">
<p>기본 정보</p>
<span class="dot">필수 입력란입니다</span>
</div>
<div class="form_type">
<div class="info1 p_top">
<p class="title w_tit">
<span>소속기관</span>
</p>
<div class="info_line p_top">
<div class="info_box1 w_inp4">
<input type="text" th:value="${user.portalOrg.orgName}" name="organization"
class="common_input_type_1" placeholder="소속기관" disabled="disabled">
</div>
</div>
</div>
<th:block th:replace="~{apps/mypage/components/commonUserInfo :: commonUserInfo}"></th:block>
</div>
<div class="form_type">
<div class="btn_application m_top">
<a class="common_btn_type_1 gray" th:href="@{/}"><span>취소</span></a>
<div class="btn_gap"></div>
<a class="common_btn_type_1 submit-btn"><span>수정 적용</span></a>
</div>
</div>
<div class="withdrawal">
<a>회원탈퇴</a>
</div>
</form>
</div>
<section layout:fragment="title">
<div class="page-title-banner">
<img th:src="@{/img/img_title_bg.png}" class="title-image">
<h1>내 정보 관리</h1>
</div>
</section>
<section layout:fragment="contentFragment">
<div class="org-register-container">
<div class="common-title-bar">
<h2 class="common-title">기본 정보</h2>
</div>
<div class="register-form-container">
<form id="updateForm" method="post" th:action="@{/mypage/update}">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<div class="register-form">
<div class="form-row">
<div class="form-label-wrapper label-offset">
<span class="form-label-text">소속기관</span>
</div>
<div class="form-field-wrapper">
<input type="text" th:value="${user.portalOrg.orgName}" name="organization"
class="form-input input-readonly info-value-box" placeholder="소속기관" disabled="disabled">
</div>
</div>
<th:block th:replace="~{apps/mypage/components/commonUserInfo :: commonUserInfo}"></th:block>
</div>
</form>
</div>
<!-- Action Buttons -->
<div class="form-actions form-actions--with-withdrawal">
<a class="withdrawal-link"><img th:src="@{/img/btn_withdrawal.png}" alt="회원탈퇴">회원탈퇴</a>
<div class="form-actions-buttons">
<button type="button" class="btn btn-submit btn-secondary" th:onclick="|location.href='@{/}'|">취소</button>
<button type="button" class="btn btn-submit btn-primary submit-btn">수정 적용</button>
</div>
</div>
</div>
</section>
<th:block layout:fragment="contentScript">
<script th:if="${error}" th:inline="javascript">
@@ -64,7 +64,7 @@
const form = document.getElementById('updateForm');
// 수정 버튼 이벤트
const submitButton = document.querySelector('.common_btn_type_1.submit-btn');
const submitButton = document.querySelector('.btn-primary.submit-btn');
if (submitButton) {
submitButton.addEventListener('click', function (e) {
e.preventDefault();
@@ -145,7 +145,7 @@
}
// 회원탈퇴 버튼 이벤트
const withdrawalBtn = document.querySelector('.withdrawal a');
const withdrawalBtn = document.querySelector('.withdrawal-link');
if (withdrawalBtn) {
withdrawalBtn.addEventListener('click', (e) => {
e.preventDefault();
@@ -1,64 +1,61 @@
<!DOCTYPE html>
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/kbank_mypage_layout}">
layout:decorate="~{layout/kjbank_title_layout}">
<body>
<section layout:fragment="contentFragment" class="content">
<script th:src="@{/js/htmx.min.js}"></script>
<div class="content_wrap">
<div class="sub_title3">
<h2 class="title">내 정보 관리</h2>
</div>
<section layout:fragment="title">
<div class="page-title-banner">
<img th:src="@{/img/img_title_bg.png}" class="title-image">
<h1>내 정보 관리</h1>
</div>
</section>
<section layout:fragment="contentFragment">
<div class="org-register-container">
<!-- 초대 알림 배너 -->
<div th:if="${hasPendingInvitation}" class="invitation_alert_banner" style="background-color: #f0f8ff; border: 1px solid #2196F3; border-radius: 4px; padding: 15px 20px; margin: 20px 0;">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div>
<strong style="color: #1976D2;">법인 회원 초대가 대기 중입니다</strong>
<p style="margin: 5px 0 0 0; color: #666;">기관에서 귀하를 법인 회원으로 초대했습니다. 초대를 수락하면 해당 기관의 법인 회원으로 전환됩니다.</p>
</div>
<div>
<a th:href="@{/signup/decision(invitation=${invitationToken})}" class="common_btn_type_1 blue" style="white-space: nowrap;">
<span>초대 확인</span>
</a>
</div>
</div>
</div>
<!-- 초대 알림 배너 -->
<div th:if="${hasPendingInvitation}" class="invitation_alert_banner" style="background-color: #f0f8ff; border: 1px solid #2196F3; border-radius: 4px; padding: 15px 20px; margin: 20px 0;">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div>
<strong style="color: #1976D2;">법인 회원 초대가 대기 중입니다</strong>
<p style="margin: 5px 0 0 0; color: #666;">기관에서 귀하를 법인 회원으로 초대했습니다. 초대를 수락하면 해당 기관의 법인 회원으로 전환됩니다.</p>
</div>
<div>
<a th:href="@{/signup/decision(invitation=${invitationToken})}" class="common_btn_type_1 blue" style="white-space: nowrap;">
<span>초대 확인</span>
</a>
</div>
</div>
</div>
<div class="inner i_cs h_inner8">
<form id="updateForm" method="post" th:action="@{/mypage/update}" th:object="${user}">
<div class="terms_box user_top">
<p>기본 정보</p>
<span class="dot">필수 입력</span>
</div>
<div class="form_type">
<th:block th:replace="~{apps/mypage/components/commonUserInfo :: commonUserInfo}"></th:block>
</div>
</form>
<div class="pc-only">
<div class="btn_wrap bt_location">
<button type="button" class="btn_business">법인회원 전환</button>
</div>
</div>
<div class="m-only">
<div class="btn_wrap bt_location mt16">
<button type="button" class="btn_business business">법인회원 전환</button>
</div>
</div>
<div class="form_type">
<div class="btn_application m_top">
<a href="#" class="common_btn_type_1 gray" th:href="@{/}"><span>취소</span></a>
<div class="btn_gap"></div>
<a href="#" class="common_btn_type_1 submit-btn"><span>수정</span></a>
</div>
</div>
<input type="hidden" name="finalMobileNumber"/>
<div class="withdrawal">
<a>회원탈퇴</a>
</div>
</div>
<div class="common-title-bar">
<h2 class="common-title">기본 정보</h2>
</div>
<div class="register-form-container">
<form id="updateForm" method="post" th:action="@{/mypage/update}" th:object="${user}">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<div class="register-form">
<th:block th:replace="~{apps/mypage/components/commonUserInfo :: commonUserInfo}"></th:block>
</div>
<!-- Corporate Transfer Button -->
<div class="corporate-transfer-section">
<button type="button" class="btn btn-secondary btn_business btn-block">
법인회원 전환
</button>
</div>
</form>
</div>
<input type="hidden" name="finalMobileNumber"/>
<!-- Action Buttons -->
<div class="form-actions form-actions--with-withdrawal">
<a class="withdrawal-link"><img th:src="@{/img/btn_withdrawal.png}" alt="회원탈퇴">회원탈퇴</a>
<div class="form-actions-buttons">
<button type="button" class="btn btn-submit btn-secondary" th:onclick="|location.href='@{/}'|">취소</button>
<button type="button" class="btn btn-submit btn-primary submit-btn">수정</button>
</div>
</div>
</div>
</section>
<th:block layout:fragment="contentScript">
<script th:if="${error}" th:inline="javascript">
@@ -83,7 +80,7 @@
const form = document.getElementById('updateForm');
// 수정 버튼 이벤트
const submitButton = document.querySelector('.common_btn_type_1.submit-btn');
const submitButton = document.querySelector('.btn-primary.submit-btn');
if (submitButton) {
submitButton.addEventListener('click', function (e) {
e.preventDefault();
@@ -164,26 +161,24 @@
}
// 법인전환 버튼 이벤트
const businessTransferBtns = document.querySelectorAll('.btn_business');
if (businessTransferBtns.length > 0) {
businessTransferBtns.forEach(btn => {
btn.addEventListener('click', () => {
customPopups.showConfirm(
'법인 회원으로 전환하시겠습니까?',
() => {
const transferForm = document.createElement('form');
transferForm.method = 'GET';
transferForm.action = '/mypage/org-transfer';
document.body.appendChild(transferForm);
transferForm.submit();
}
);
});
const businessTransferBtn = document.querySelector('.btn_business');
if (businessTransferBtn) {
businessTransferBtn.addEventListener('click', () => {
customPopups.showConfirm(
'법인 회원으로 전환하시겠습니까?',
() => {
const transferForm = document.createElement('form');
transferForm.method = 'GET';
transferForm.action = '/mypage/org-transfer';
document.body.appendChild(transferForm);
transferForm.submit();
}
);
});
}
// 회원탈퇴 버튼 이벤트
const withdrawalBtn = document.querySelector('.withdrawal a');
const withdrawalBtn = document.querySelector('.withdrawal-link');
if (withdrawalBtn) {
withdrawalBtn.addEventListener('click', (e) => {
e.preventDefault();
@@ -194,4 +189,4 @@
</script>
</th:block>
</body>
</html>
</html>
@@ -1,102 +1,84 @@
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kbank_base_layout}"
xmlns:hx-on="http://www.w3.org/1999/xhtml">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kbank_base_layout}">
<body>
<th:block th:fragment="commonUserInfo">
<div class="info1 pt28">
<p class="title">
<span class="dot">이메일 아이디</span>
</p>
<div class="info_line">
<div class="info_box1">
<input type="text" id="userId" class="common_input_type_1 w_inp"
th:classappend="${isInvited ? 'input-readonly' : ''}"
maxlength="30"
th:value="${emailId}"
th:readonly="${isInvited}"
th:placeholder="#{portalUser.Register.email}">
<span class="special_t">@</span>
<input type="text" id="domain" class="common_input_type_1 w_inp2"
th:classappend="${isInvited ? 'input-readonly' : ''}"
maxlength="50"
th:value="${domain}"
th:readonly="${isInvited}"
th:placeholder="#{portalUser.Register.domain}">
<input type="hidden" id="loginId" name="loginId" th:value="${loginId}">
<input type="hidden" name="registrationType" th:value="${registrationType}"/>
<input type="hidden" id="registrationScenario" name="registrationScenario" value="new"/>
</div>
<div class="btn_check">
<a class="common_btn_type_2 cbt btn_check_email"
th:classappend="${isInvited ? 'hidden' : ''}"
th:hx-post="@{/check_id}"
hx-include="#csrf, #loginId, [name='registrationType']"
hx-target="#email-validation"
hx-swap="none">
<span>중복체크</span>
</a>
<div class="org-form-group">
<label class="org-form-label">
이메일 아이디 <span class="required-badge">필수</span>
</label>
<div class="org-form-input-wrapper">
<div class="org-input-row">
<div class="org-compound-input">
<input type="text" id="userId" class="org-form-input"
th:classappend="${isInvited ? 'input-readonly' : ''}"
maxlength="30"
th:value="${emailId}"
th:readonly="${isInvited}"
th:placeholder="#{portalUser.Register.email}">
<span class="separator">@</span>
<input type="text" id="domain" class="org-form-input"
th:classappend="${isInvited ? 'input-readonly' : ''}"
maxlength="50"
th:value="${domain}"
th:readonly="${isInvited}"
th:placeholder="#{portalUser.Register.domain}">
</div>
<button type="button" class="btn org-btn-check btn_check_email"
th:classappend="${isInvited ? 'hidden' : ''}">
중복체크
</button>
</div>
<input type="hidden" id="loginId" name="loginId" th:value="${loginId}">
<input type="hidden" name="registrationType" th:value="${registrationType}"/>
<input type="hidden" id="registrationScenario" name="registrationScenario" value="new"/>
<div id="email-validation" class="org-validation-message"></div>
</div>
<div id="email-validation"></div>
</div>
</th:block>
</body>
<th:block>
<script th:fragment="commonUserInfoScript">
<script th:fragment="commonUserInfoScript" th:inline="javascript">
// 이메일 조합 함수
function combineEmail() {
let emailId = document.getElementById('userId').value;
let domain = document.getElementById('domain').value;
let emailId = $('#userId').val();
let domain = $('#domain').val();
let combined = emailId + '@' + domain;
let loginIdInput = document.getElementById('loginId');
if (loginIdInput) {
loginIdInput.value = combined;
}
$('#loginId').val(combined);
}
function handleEmailValidationResponse(response) {
const individualConversionForm = document.getElementById('individualConversionForm');
const emailChangeForm = document.getElementById('emailChangeForm');
const newUserForm = document.getElementById('newUserForm');
const userIdInput = document.getElementById('userId');
const domainInput = document.getElementById('domain');
const checkButton = document.querySelector('.btn_check_email');
const loginIdInput = document.getElementById('loginId');
const scenarioInput = document.getElementById('registrationScenario');
const individualConversionForm = $('#individualConversionForm');
const emailChangeForm = $('#emailChangeForm');
const newUserForm = $('#newUserForm, #newUserFields');
const userIdInput = $('#userId');
const domainInput = $('#domain');
const checkButton = $('.btn_check_email');
const loginIdInput = $('#loginId');
const scenarioInput = $('#registrationScenario');
if (response && response.showPopup) {
switch (response.popupType) {
case "validEmail":
case "newUser":
// 시나리오 값 설정
if (scenarioInput) {
scenarioInput.value = 'new';
// hidden input이 form의 일부인지 확인
if (!scenarioInput.form) {
document.getElementById('registerForm').appendChild(scenarioInput);
}
}
scenarioInput.val('new');
customPopups.showAlert(response.message);
if (checkButton) {
checkButton.style.display = 'none';
}
checkButton.hide();
if (userIdInput && domainInput && loginIdInput) {
userIdInput.disabled = true;
domainInput.disabled = true;
userIdInput.style.backgroundColor = '#e0e0e0';
domainInput.style.backgroundColor = '#e0e0e0';
userIdInput.prop('disabled', true);
domainInput.prop('disabled', true);
userIdInput.css('backgroundColor', '#e0e0e0');
domainInput.css('backgroundColor', '#e0e0e0');
const combinedEmail = `${userIdInput.value.trim()}@${domainInput.value.trim()}`;
loginIdInput.value = combinedEmail;
}
const combinedEmail = `${userIdInput.val().trim()}@${domainInput.val().trim()}`;
loginIdInput.val(combinedEmail);
if (newUserForm) newUserForm.style.display = 'block';
if (individualConversionForm) individualConversionForm.style.display = 'none';
if (emailChangeForm) emailChangeForm.style.display = 'none';
newUserForm.show();
individualConversionForm.hide();
emailChangeForm.hide();
break;
case "conversionOrChange":
@@ -104,51 +86,33 @@
response.message,
function () {
// "이메일 유지" 선택 시 동작
if (scenarioInput) {
scenarioInput.value = 'retain';
if (!scenarioInput.form) {
document.getElementById('registerForm').appendChild(scenarioInput);
}
}
scenarioInput.val('retain');
if (individualConversionForm) individualConversionForm.style.display = 'block';
if (emailChangeForm) emailChangeForm.style.display = 'none';
if (newUserForm) newUserForm.style.display = 'none';
individualConversionForm.show();
emailChangeForm.hide();
newUserForm.hide();
if (userIdInput && domainInput) {
userIdInput.disabled = true;
domainInput.disabled = true;
userIdInput.style.backgroundColor = '#e0e0e0';
domainInput.style.backgroundColor = '#e0e0e0';
}
userIdInput.prop('disabled', true);
domainInput.prop('disabled', true);
userIdInput.css('backgroundColor', '#e0e0e0');
domainInput.css('backgroundColor', '#e0e0e0');
if (checkButton) {
checkButton.style.display = 'none';
}
checkButton.hide();
},
function () {
// "이메일 전환" 선택 시 동작
if (scenarioInput) {
scenarioInput.value = 'change';
if (!scenarioInput.form) {
document.getElementById('registerForm').appendChild(scenarioInput);
}
}
scenarioInput.val('change');
if (emailChangeForm) emailChangeForm.style.display = 'block';
if (individualConversionForm) individualConversionForm.style.display = 'none';
if (newUserForm) newUserForm.style.display = 'none';
emailChangeForm.show();
individualConversionForm.hide();
newUserForm.hide();
if (userIdInput && domainInput) {
userIdInput.disabled = true;
domainInput.disabled = true;
userIdInput.style.backgroundColor = '#e0e0e0';
domainInput.style.backgroundColor = '#e0e0e0';
}
userIdInput.prop('disabled', true);
domainInput.prop('disabled', true);
userIdInput.css('backgroundColor', '#e0e0e0');
domainInput.css('backgroundColor', '#e0e0e0');
if (checkButton) {
checkButton.style.display = 'none';
}
checkButton.hide();
}
);
break;
@@ -169,11 +133,35 @@
}
// 이메일 입력 필드에 이벤트 리스너 추가
document.addEventListener('DOMContentLoaded', function () {
let userIdInput = document.getElementById("userId");
let domainInput = document.getElementById("domain");
if (userIdInput) userIdInput.addEventListener("input", combineEmail);
if (domainInput) domainInput.addEventListener("input", combineEmail);
$(document).ready(function () {
$('#userId, #domain').on('input', combineEmail);
// 이메일 중복 체크 버튼 클릭
$('.btn_check_email').on('click', function () {
const loginId = $('#loginId').val();
const registrationType = $('input[name="registrationType"]').val();
if (!loginId || loginId.indexOf('@') === -1) {
customPopups.showAlert('이메일 주소를 올바르게 입력해주세요.');
return;
}
$.ajax({
url: /*[[@{/check_id}]]*/ '/check_id',
type: 'POST',
data: {
loginId: loginId,
registrationType: registrationType,
_csrf: $('input[name="_csrf"]').val()
},
success: function (response) {
handleEmailValidationResponse(response);
},
error: function () {
customPopups.showAlert('처리 중 오류가 발생했습니다. 다시 시도해주세요.');
}
});
});
});
</script>
@@ -3,350 +3,373 @@
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kbank_base_layout}">
<body>
<th:block th:fragment="newUserForm">
<div id="newUserFields">
<!-- 성명 -->
<div class="info1 pt28">
<p class="title">
<span class="dot">성명</span>
</p>
<div class="info_line">
<div class="info_box1">
<input type="text" th:field="*{userName}" id="userName" class="common_input_type_1"
th:placeholder="#{portalUser.Register.userName}">
</div>
<div class="btn_check pc-only"></div>
</div>
</div>
<!-- 휴대폰 번호 입력 -->
<div class="info1 pt28">
<p class="title">
<span class="dot">휴대폰 번호</span>
</p>
<div class="info_line">
<div class="info_box1">
<span class="input">
<div class="custom_select ct_sc h_inp2" id="phoneMobile">
<div class="select_selected">선택</div>
<ul class="common_selecttype select_items select_hide ct_sc2">
<li>010</li>
<li>011</li>
<li>017</li>
<li>016</li>
<li>018</li>
</ul>
</div>
</span>
<span class="dash">-</span>
<span class="input">
<input type="text" id="cellPhone1" maxlength="4" class="common_input_type_1 w_inp3"
th:placeholder="#{portal.Register.phone1}">
</span>
<span class="dash">-</span>
<span class="input">
<input type="text" id="cellPhone2" maxlength="4" class="common_input_type_1 w_inp3"
th:placeholder="#{portal.Register.phone2}">
</span>
<input type="hidden" th:field="*{mobileNumber}" id="mobileNumber">
</div>
<div class="btn_check">
<a class="common_btn_type_2 cbt btn_auth"
th:hx-post="@{/auth/request_auth_number}"
hx-include="#csrf, #mobileNumber"
hx-target="#authNumberContainer"
hx-swap="none">
<span>인증번호 받기</span>
</a>
</div>
</div>
</div>
<div id="mobile-validation"></div>
<!-- 인증번호 입력 -->
<div class="info1 pt28" id="authNumberContainer">
<p class="title">
<span class="dot">인증번호 입력</span>
</p>
<div class="info_line">
<div class="info_box1">
<div class="certify_box">
<input type="text" th:field="*{authNumber}" id="authNumber" maxlength="6" pattern="[0-9]*"
inputmode="numeric" class="common_input_type_1 w_input" th:placeholder="#{ncrdRegister.validate.authNumber}" required>
<span class="certify_time" id="certify_time">03:00</span>
</div>
</div>
<div class="btn_check">
<a class="common_btn_type_2 cbt btn_verify_auth"
th:hx-post="@{/auth/verify_auth_number}"
hx-include="#mobileNumber,#authNumber"
hx-target="#authNumber-validation"
hx-swap="none">
<span>인증번호 확인</span>
</a>
</div>
</div>
</div>
<div id="authNumber-validation"></div>
<div class="info1 pt28">
<p class="title">
<span class="dot">비밀번호</span>
</p>
<div class="info_line">
<div class="info_box1">
<input type="password" name="password" id="password" class="common_input_type_1 w_input"
th:placeholder="#{portalUser.Register.pass}"
th:hx-post="@{/check_password}"
hx-trigger="blur"
hx-include="#mobileNumber, #loginId"
hx-target="#password-validation"
hx-swap="innerHTML">
<input type="hidden" name="isPasswordValid" id="isPasswordValid"/>
</div>
<div class="btn_check pc-only"></div>
</div>
</div>
<div id="password-validation" class="validation-message"></div>
<!-- 비밀번호 확인 -->
<div class="info1 pt28">
<p class="title">
<span class="dot">비밀번호 확인</span>
</p>
<div class="info_line">
<div class="info_box1">
<input type="password" name="password2" id="password2" class="common_input_type_1 w_input"
th:placeholder="#{portalUser.Register.passConfirm}"
th:hx-post="@{/check_password_match}"
hx-trigger="blur"
hx-target="#password-match-validation"
hx-include="#password"
hx-swap="innerHTML">
<input type="hidden" name="isPasswordMatch" id="isPasswordMatch"/>
</div>
<div class="btn_check pc-only"></div>
</div>
</div>
<div id="password-match-validation" class="validation-message"></div>
<div id="newUserFields">
<!-- 성명 -->
<div class="org-form-group">
<label class="org-form-label">
성명 <span class="required-badge">필수</span>
</label>
<div class="org-form-input-wrapper">
<input type="text" th:field="*{userName}" id="userName" class="org-form-input"
th:placeholder="#{portalUser.Register.userName}">
<div id="userName-validation" class="org-validation-message"></div>
</div>
</div>
<!-- 휴대폰 번호 입력 -->
<div class="org-form-group">
<label class="org-form-label">
휴대폰 번호 <span class="required-badge">필수</span>
</label>
<div class="org-form-input-wrapper">
<div class="org-input-row">
<div class="org-compound-input">
<select class="org-form-select phone-number" id="phoneMobile">
<option value="">선택</option>
<option value="010">010</option>
<option value="011">011</option>
<option value="017">017</option>
<option value="016">016</option>
<option value="018">018</option>
</select>
<span class="separator">-</span>
<input type="text" id="cellPhone1" maxlength="4" class="org-form-input phone-number"
th:placeholder="#{portal.Register.phone1}">
<span class="separator">-</span>
<input type="text" id="cellPhone2" maxlength="4" class="org-form-input phone-number"
th:placeholder="#{portal.Register.phone2}">
</div>
<button type="button" class="btn org-btn-check btn_auth">
인증번호 받기
</button>
</div>
<input type="hidden" th:field="*{mobileNumber}" id="mobileNumber">
<div id="mobile-validation" class="org-validation-message"></div>
</div>
</div>
<!-- 인증번호 입력 -->
<div class="org-form-group" id="authNumberContainer" style="display: none;">
<label class="org-form-label">
인증번호 입력 <span class="required-badge">필수</span>
</label>
<div class="org-form-input-wrapper">
<div class="org-input-row">
<div class="org-auth-input-group">
<input type="text" th:field="*{authNumber}" id="authNumber" maxlength="6" pattern="[0-9]*"
inputmode="numeric" class="org-form-input" th:placeholder="#{ncrdRegister.validate.authNumber}" required>
<span class="org-timer" id="certify_time">03:00</span>
</div>
<button type="button" class="btn org-btn-check btn_verify_auth">
인증번호 확인
</button>
</div>
<div id="authNumber-validation" class="org-validation-message"></div>
</div>
</div>
<!-- 비밀번호 -->
<div class="org-form-group">
<label class="org-form-label">
비밀번호 <span class="required-badge">필수</span>
</label>
<div class="org-form-input-wrapper">
<input type="password" name="password" id="password" class="org-form-input"
th:placeholder="#{portalUser.Register.pass}">
<input type="hidden" name="isPasswordValid" id="isPasswordValid"/>
<div id="password-validation" class="org-validation-message"></div>
</div>
</div>
<!-- 비밀번호 확인 -->
<div class="org-form-group">
<label class="org-form-label">
비밀번호 확인 <span class="required-badge">필수</span>
</label>
<div class="org-form-input-wrapper">
<input type="password" name="password2" id="password2" class="org-form-input"
th:placeholder="#{portalUser.Register.passConfirm}">
<input type="hidden" name="isPasswordMatch" id="isPasswordMatch"/>
<div id="password-match-validation" class="org-validation-message"></div>
</div>
</div>
</div>
</th:block>
</body>
<th:block>
<script th:fragment="newUserScript">
// 휴대폰 번호 조합 함수
function combineMobileNumber() {
let mobileElement = document.querySelector('#phoneMobile .select_selected');
if (!mobileElement) {
console.error('Mobile element not found');
return null;
}
let prefix = mobileElement.textContent.trim();
let middle = document.getElementById('cellPhone1').value.trim();
let last = document.getElementById('cellPhone2').value.trim();
<script th:fragment="newUserScript">
// 휴대폰 번호 조합 함수
function combineMobileNumber() {
let mobileSelect = document.getElementById('phoneMobile');
if (!mobileSelect) {
console.error('Mobile select element not found');
return null;
}
let prefix = mobileSelect.value;
let middle = document.getElementById('cellPhone1').value.trim();
let last = document.getElementById('cellPhone2').value.trim();
if (prefix === '선택' || !middle || !last) {
return null;
}
if (!prefix || !middle || !last) {
return null;
}
// 형식화된 번호 (xxx-xxxx-xxxx)
let formattedNumber = `${prefix}-${middle}-${last}`;
// 형식화된 번호 (xxx-xxxx-xxxx)
let formattedNumber = `${prefix}-${middle}-${last}`;
// hidden input에 형식화된 번호 설정
let mobileNumberInput = document.getElementById('mobileNumber');
if (mobileNumberInput) {
mobileNumberInput.value = formattedNumber;
// hidden input에 형식화된 번호 설정
let mobileNumberInput = document.getElementById('mobileNumber');
if (mobileNumberInput) {
mobileNumberInput.value = formattedNumber;
}
return formattedNumber;
}
document.addEventListener('DOMContentLoaded', function() {
let countdownInterval;
let isAuthVerified = false;
// 휴대폰 번호 중간, 끝자리 입력 제한 및 형식화
$('#cellPhone1').on('input', function() {
let value = $(this).val().replace(/[^0-9]/g, '');
if (value.length > 4) {
value = value.slice(0, 4);
}
$(this).val(value);
combineMobileNumber();
});
$('#cellPhone2').on('input', function() {
let value = $(this).val().replace(/[^0-9]/g, '');
if (value.length > 4) {
value = value.slice(0, 4);
}
$(this).val(value);
combineMobileNumber();
});
// select 변경 감지
$('#phoneMobile').on('change', function() {
combineMobileNumber();
});
// 인증번호 입력 제한
$('#authNumber').on('input', function() {
$(this).val($(this).val().replace(/[^0-9]/g, ''));
});
// 인증번호 검증 결과 처리 함수
function handleAuthNumberVerification(response) {
customPopups.showAlert(response.message);
if (response.valid) {
let authNumberInput = document.getElementById('authNumber');
let verifyAuthButton = document.querySelector('.btn_verify_auth');
let timerElement = document.getElementById('certify_time');
if (authNumberInput) {
authNumberInput.readonly = true;
}
if (verifyAuthButton) {
verifyAuthButton.disabled = true;
}
clearInterval(countdownInterval);
if (timerElement) {
timerElement.textContent = '인증 완료';
}
isAuthVerified = true;
document.dispatchEvent(new Event('authVerified'));
}
}
// 인증번호 요청 후 처리 함수
function handleAuthNumberRequest(response) {
if (response.valid) {
let authButton = document.querySelector('.btn_auth');
let authContainer = document.getElementById('authNumberContainer');
if (authButton) {
authButton.disabled = true;
}
if (authContainer) {
authContainer.style.display = 'flex';
}
startAuthNumberTimer();
customPopups.showAlert(response.message || '인증번호가 발송되었습니다.');
} else {
customPopups.showAlert(response.message || '알 수 없는 오류가 발생했습니다.');
let authButton = document.querySelector('.btn_auth');
if (authButton) {
authButton.disabled = false;
}
}
}
// 인증번호 타이머 시작 함수
function startAuthNumberTimer() {
clearInterval(countdownInterval);
let remainingTime = 180; // 3분
let timerElement = document.getElementById('certify_time');
let authButton = document.querySelector('.btn_auth');
let verifyAuthButton = document.querySelector('.btn_verify_auth');
let authNumberInput = document.getElementById('authNumber');
function updateRemainingTime() {
if (remainingTime <= 0) {
clearInterval(countdownInterval);
if (timerElement) {
timerElement.textContent = '입력 시간이 초과되었습니다.';
}
return formattedNumber;
customPopups.showAlert('입력 시간이 초과되었습니다. 인증번호를 다시 발송해주세요.');
if (authButton) {
authButton.disabled = false;
}
if (verifyAuthButton) {
verifyAuthButton.disabled = true;
}
if (authNumberInput) {
authNumberInput.readOnly = true;
}
} else {
let minutes = Math.floor(remainingTime / 60);
let seconds = remainingTime % 60;
if (timerElement) {
timerElement.textContent = `남은 시간: ${minutes}:${seconds.toString().padStart(2, '0')}`;
}
remainingTime--;
}
}
document.addEventListener('DOMContentLoaded', function () {
activateSelect();
let countdownInterval;
let isAuthVerified = false;
updateRemainingTime();
countdownInterval = setInterval(updateRemainingTime, 1000);
}
// 휴대폰 번호 중간, 끝자리 입력 제한 및 형식화
$('#cellPhone1').on('input', function () {
let value = $(this).val().replace(/[^0-9]/g, '');
if (value.length > 4) {
value = value.slice(0, 4);
}
$(this).val(value);
combineMobileNumber();
});
// 이벤트 리스너 등록
document.getElementById('phoneMobile').addEventListener('change', combineMobileNumber);
document.querySelectorAll('#cellPhone1, #cellPhone2').forEach(el => el.addEventListener('input', combineMobileNumber));
$('#cellPhone2').on('input', function () {
let value = $(this).val().replace(/[^0-9]/g, '');
if (value.length > 4) {
value = value.slice(0, 4);
}
$(this).val(value);
combineMobileNumber();
});
// select_selected 변경 감지
$('#phoneMobile .select_items li').on('click', function () {
setTimeout(() => {
combineMobileNumber();
}, 100);
});
// 인증번호 입력 제한
$('#authNumber').on('input', function () {
$(this).val($(this).val().replace(/[^0-9]/g, ''));
});
// 인증번호 검증 결과 처리 함수
function handleAuthNumberVerification(response) {
customPopups.showAlert(response.message);
if (response.valid) {
let authNumberInput = document.getElementById('authNumber');
let verifyAuthButton = document.querySelector(".btn_verify_auth");
let timerElement = document.getElementById('certify_time');
if (authNumberInput) authNumberInput.readonly = true;
if (verifyAuthButton) verifyAuthButton.disabled = true;
clearInterval(countdownInterval);
if (timerElement) timerElement.textContent = '인증 완료';
isAuthVerified = true;
document.dispatchEvent(new Event('authVerified'));
}
}
// 인증번호 요청 후 처리 함수
function handleAuthNumberRequest(response) {
if (response.valid) {
let authButton = document.querySelector(".btn_auth");
if (authButton) authButton.disabled = true;
startAuthNumberTimer();
customPopups.showAlert(response.message || "인증번호가 발송되었습니다.");
} else {
customPopups.showAlert(response.message || '알 수 없는 오류가 발생했습니다.');
let authButton = document.querySelector(".btn_auth");
if (authButton) authButton.disabled = false;
}
}
// 인증번호 타이머 시작 함수
function startAuthNumberTimer() {
clearInterval(countdownInterval);
let remainingTime = 180; // 3분
let timerElement = document.getElementById("certify_time");
let authButton = document.querySelector(".btn_auth");
let verifyAuthButton = document.querySelector(".btn_verify_auth");
let authNumberInput = document.getElementById('authNumber');
function updateRemainingTime() {
if (remainingTime <= 0) {
clearInterval(countdownInterval);
if (timerElement) timerElement.textContent = "입력 시간이 초과되었습니다.";
customPopups.showAlert("입력 시간이 초과되었습니다. 인증번호를 다시 발송해주세요.");
if (authButton) authButton.disabled = false;
if (verifyAuthButton) verifyAuthButton.disabled = true;
if (authNumberInput) authNumberInput.readOnly = true;
} else {
let minutes = Math.floor(remainingTime / 60);
let seconds = remainingTime % 60;
if (timerElement) timerElement.textContent = `남은 시간: ${minutes}:${seconds.toString().padStart(2, '0')}`;
remainingTime--;
}
}
updateRemainingTime();
countdownInterval = setInterval(updateRemainingTime, 1000);
}
// 이벤트 리스너 등록
document.querySelectorAll('#phoneMobile .select_selected, #cellPhone1, #cellPhone2')
.forEach(el => el.addEventListener('input', combineMobileNumber));
let authNumberInput = document.getElementById("authNumber");
if (authNumberInput) {
authNumberInput.addEventListener("input", function () {
this.value = this.value.replace(/\D/g, '');
this.classList.toggle('is-invalid', this.value.length !== 6);
});
}
let userNameInput = document.getElementById("userName");
if (userNameInput) {
userNameInput.addEventListener("input", function () {
this.classList.toggle('is-invalid', this.value.trim().length === 0);
});
}
// 인증번호 요청 버튼 이벤트 리스너
let authButton = document.querySelector(".btn_auth");
if (authButton) {
authButton.addEventListener("click", function (event) {
let mobileNumber = combineMobileNumber();
if (!mobileNumber) {
event.preventDefault(); // htmx 요청 중지
customPopups.showAlert("휴대폰 번호를 올바르게 입력해주세요.");
return;
}
document.getElementById('mobileNumber').value = mobileNumber;
// htmx 요청이 자동으로 실행됨
});
}
// 인증번호 확인 버튼 이벤트 리스너
let verifyAuthButton = document.querySelector(".btn_verify_auth");
if (verifyAuthButton) {
verifyAuthButton.addEventListener("click", function () {
let authNumber = document.getElementById('authNumber').value;
if (authNumber.length !== 6) {
customPopups.showAlert("6자리 인증번호를 입력해주세요.");
return;
}
});
}
// HTMX 이벤트 리스너
document.body.addEventListener('htmx:afterSwap', function (event) {
let targetId = event.detail.target ? event.detail.target.id : null;
if (targetId && ['password-validation', 'password-match-validation', 'authNumberContainer', 'authNumber-validation', 'email-validation'].includes(targetId)) {
let response;
try {
response = typeof event.detail.xhr.response === 'string'
? JSON.parse(event.detail.xhr.response)
: event.detail.xhr.response;
} catch (e) {
console.error('Failed to parse response as JSON:', event.detail.xhr.response);
customPopups.showAlert("서버에서 예상치 못한 응답을 받았습니다.");
return;
}
switch (targetId) {
case 'email-validation':
// handleEmailValidationResponse(response);
break;
case 'password-validation':
case 'password-match-validation':
let targetElement = document.getElementById(targetId);
targetElement.className = 'validation-message ' + (response.valid ? 'success' : 'error');
targetElement.textContent = response.message;
// 유효성 상태 저장
if (targetId === 'password-validation') {
document.getElementById('isPasswordValid').value = response.valid;
} else if (targetId === 'password-match-validation') {
document.getElementById('isPasswordMatch').value = response.valid;
}
break;
case 'authNumberContainer':
handleAuthNumberRequest(response);
break;
case 'authNumber-validation':
handleAuthNumberVerification(response);
break;
}
}
});
document.body.addEventListener('htmx:responseError', function (event) {
customPopups.showAlert("처리 중 오류가 발생했습니다. 다시 시도해주세요.");
});
let authNumberInput = document.getElementById('authNumber');
if (authNumberInput) {
authNumberInput.addEventListener('input', function() {
this.value = this.value.replace(/\D/g, '');
this.classList.toggle('is-invalid', this.value.length !== 6);
});
</script>
}
let userNameInput = document.getElementById('userName');
if (userNameInput) {
userNameInput.addEventListener('input', function() {
this.classList.toggle('is-invalid', this.value.trim().length === 0);
});
}
// 인증번호 요청 버튼 이벤트 리스너
$('.btn_auth').on('click', function() {
let mobileNumber = combineMobileNumber();
if (!mobileNumber) {
customPopups.showAlert('휴대폰 번호를 올바르게 입력해주세요.');
return;
}
$('#mobileNumber').val(mobileNumber);
$.ajax({
url: /*[[@{/auth/request_auth_number}]]*/ '/auth/request_auth_number',
type: 'POST',
data: {
mobileNumber: mobileNumber,
_csrf: $('input[name="_csrf"]').val()
},
success: function(response) {
handleAuthNumberRequest(response);
},
error: function() {
customPopups.showAlert('처리 중 오류가 발생했습니다. 다시 시도해주세요.');
}
});
});
// 인증번호 확인 버튼 이벤트 리스너
$('.btn_verify_auth').on('click', function() {
let authNumber = $('#authNumber').val();
if (authNumber.length !== 6) {
customPopups.showAlert('6자리 인증번호를 입력해주세요.');
return;
}
$.ajax({
url: /*[[@{/auth/verify_auth_number}]]*/ '/auth/verify_auth_number',
type: 'POST',
data: {
mobileNumber: $('#mobileNumber').val(),
authNumber: authNumber,
_csrf: $('input[name="_csrf"]').val()
},
success: function(response) {
handleAuthNumberVerification(response);
},
error: function() {
customPopups.showAlert('처리 중 오류가 발생했습니다. 다시 시도해주세요.');
}
});
});
// 비밀번호 검증
$('#password').on('blur', function() {
let password = $(this).val();
if (!password) {
return;
}
$.ajax({
url: /*[[@{/check_password}]]*/ '/check_password',
type: 'POST',
data: {
password: password,
mobileNumber: $('#mobileNumber').val(),
loginId: $('#loginId').val(),
_csrf: $('input[name="_csrf"]').val()
},
success: function(response) {
let targetElement = $('#password-validation');
targetElement.removeClass('success error');
targetElement.addClass(response.valid ? 'success' : 'error');
targetElement.text(response.message);
$('#isPasswordValid').val(response.valid);
},
error: function() {
customPopups.showAlert('처리 중 오류가 발생했습니다. 다시 시도해주세요.');
}
});
});
// 비밀번호 확인 검증
$('#password2').on('blur', function() {
let password2 = $(this).val();
if (!password2) {
return;
}
$.ajax({
url: /*[[@{/check_password_match}]]*/ '/check_password_match',
type: 'POST',
data: {
password: $('#password').val(),
password2: password2,
_csrf: $('input[name="_csrf"]').val()
},
success: function(response) {
let targetElement = $('#password-match-validation');
targetElement.removeClass('success error');
targetElement.addClass(response.valid ? 'success' : 'error');
targetElement.text(response.message);
$('#isPasswordMatch').val(response.valid);
},
error: function() {
customPopups.showAlert("처리 중 오류가 발생했습니다. 다시 시도해주세요.");
}
});
});
});
</script>
</th:block>
</html>
@@ -1,287 +1,272 @@
<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:hx-on="http://www.w3.org/1999/xhtml">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<body>
<th:block th:fragment="orgInfoForm">
<div class="form_type">
<div class="info1">
<p class="title">
<span class="dot">사업자등록번호</span>
</p>
<div class="info_line">
<div class="info_box1">
<input type="text" class="common_input_type_1 w_inp5 business-number" id="compRegNo1" maxlength="3"
th:placeholder="#{portalOrg.Register.compRegNo1}" required>
<span class="dash">-</span>
<input type="text" class="common_input_type_1 w_inp6 business-number" id="compRegNo2" maxlength="2"
th:placeholder="#{portalOrg.Register.compRegNo2}" required>
<span class="dash">-</span>
<input type="text" class="common_input_type_1 w_inp7 business-number" id="compRegNo3" maxlength="5"
th:placeholder="#{portalOrg.Register.compRegNo3}" required>
</div>
<div class="btn_check pc-only"></div>
<input type="hidden" id="compRegNo" th:field="*{compRegNo}" hx-trigger="change delay:1s"/>
<!-- Business Registration Number -->
<div class="org-form-group">
<label class="org-form-label">
사업자등록번호 <span class="required-badge">필수</span>
</label>
<div class="org-form-input-wrapper">
<div class="org-compound-input">
<input type="text" class="org-form-input business-number" id="compRegNo1" maxlength="3"
placeholder="000" required>
<span class="separator">-</span>
<input type="text" class="org-form-input business-number" id="compRegNo2" maxlength="2"
placeholder="00" required>
<span class="separator">-</span>
<input type="text" class="org-form-input business-number" id="compRegNo3" maxlength="5"
placeholder="00000" required>
</div>
<input type="hidden" id="compRegNo" th:field="*{compRegNo}"/>
<div id="compRegNo-validation" class="org-validation-message"></div>
</div>
</div>
<div class="info1 pt28">
<p class="title">
<span class="dot">법인등록번호</span>
</p>
<div class="info_line">
<div class="info_box1">
<input type="text" class="common_input_type_1 w_inp8 corp-reg-no" id="corpRegNo1" maxlength="6"
th:placeholder="#{portalOrg.Register.corpRegNo1}" required>
<span class="dash">-</span>
<input type="text" class="common_input_type_1 w_inp9 corp-reg-no" id="corpRegNo2" maxlength="7"
th:placeholder="#{portalOrg.Register.corpRegNo2}" required>
</div>
<div class="btn_check pc-only"></div>
<input type="hidden" id="corpRegNo" th:field="*{corpRegNo}" hx-trigger="change delay:1s"/>
<!-- Corporate Registration Number -->
<div class="org-form-group">
<label class="org-form-label">
법인등록번호 <span class="required-badge">필수</span>
</label>
<div class="org-form-input-wrapper">
<div class="org-compound-input">
<input type="text" class="org-form-input corp-reg-no" id="corpRegNo1" maxlength="6"
placeholder="000000" required>
<span class="separator">-</span>
<input type="text" class="org-form-input corp-reg-no" id="corpRegNo2" maxlength="7"
placeholder="0000000" required>
</div>
<input type="hidden" id="corpRegNo" th:field="*{corpRegNo}"/>
<div id="corpRegNo-validation" class="org-validation-message"></div>
</div>
</div>
<div class="info1 pt28">
<p class="title">
<span class="dot">법인명</span>
</p>
<div class="info_line">
<div class="info_box1">
<input type="text" class="common_input_type_1" id="orgName" th:field="*{orgName}"
th:placeholder="#{portalOrg.Register.orgName}"
th:classappend="${#fields.hasErrors('orgName')}? 'is-invalid'" required>
</div>
<div class="btn_check pc-only"></div>
<div th:if="${#fields.hasErrors('orgName')}" class="invalid-feedback">
<span th:errors="*{orgName}"></span>
</div>
<!-- Company Name -->
<div class="org-form-group">
<label class="org-form-label" for="orgName">
법인명 <span class="required-badge">필수</span>
</label>
<div class="org-form-input-wrapper">
<input type="text" class="org-form-input" id="orgName" th:field="*{orgName}"
placeholder="법인명을 입력하세요"
th:classappend="${#fields.hasErrors('orgName')}? 'is-invalid'" required>
<div th:if="${#fields.hasErrors('orgName')}" class="org-validation-message error">
<span th:errors="*{orgName}"></span>
</div>
</div>
</div>
<div class="info1 pt28">
<p class="title">
<span class="dot">대표자 성명</span>
</p>
<div class="info_line">
<div class="info_box1">
<input type="text" class="common_input_type_1" id="ceoName" th:field="*{ceoName}"
th:placeholder="#{portalOrg.Register.ceoName}"
th:classappend="${#fields.hasErrors('ceoName')}? 'is-invalid'" required>
</div>
<div class="btn_check pc-only"></div>
<div th:if="${#fields.hasErrors('ceoName')}" class="invalid-feedback">
<span th:errors="*{ceoName}"></span>
</div>
<!-- CEO Name -->
<div class="org-form-group">
<label class="org-form-label" for="ceoName">
대표자 성명 <span class="required-badge">필수</span>
</label>
<div class="org-form-input-wrapper">
<input type="text" class="org-form-input" id="ceoName" th:field="*{ceoName}"
placeholder="대표자 성명을 입력하세요"
th:classappend="${#fields.hasErrors('ceoName')}? 'is-invalid'" required>
<div th:if="${#fields.hasErrors('ceoName')}" class="org-validation-message error">
<span th:errors="*{ceoName}"></span>
</div>
</div>
</div>
<div class="info1 pt28">
<p class="title">
<span class="dot">사업장 소재지</span>
</p>
<div class="info_line">
<div class="info_box1">
<input type="text" class="common_input_type_1" id="orgAddr" th:field="*{orgAddr}"
th:placeholder="#{portalOrg.Register.orgAddr}"
th:classappend="${#fields.hasErrors('orgAddr')}? 'is-invalid'" required>
</div>
<div class="btn_check pc-only"></div>
<div th:if="${#fields.hasErrors('orgAddr')}" class="invalid-feedback">
<span th:errors="*{orgAddr}"></span>
</div>
<!-- Business Address -->
<div class="org-form-group">
<label class="org-form-label" for="orgAddr">
사업장 소재지 <span class="required-badge">필수</span>
</label>
<div class="org-form-input-wrapper">
<input type="text" class="org-form-input" id="orgAddr" th:field="*{orgAddr}"
placeholder="사업장 소재지를 입력하세요"
th:classappend="${#fields.hasErrors('orgAddr')}? 'is-invalid'" required>
<div th:if="${#fields.hasErrors('orgAddr')}" class="org-validation-message error">
<span th:errors="*{orgAddr}"></span>
</div>
</div>
</div>
<div class="info1 pt28">
<p class="title">
<span class="dot">업태</span>
</p>
<div class="info_line">
<div class="info_box1">
<input type="text" class="common_input_type_1 w_input" id="orgSectors" th:field="*{orgSectors}"
th:placeholder="#{portalOrg.Register.orgSectors}">
</div>
<div class="btn_check pc-only"></div>
</div>
<!-- Business Type -->
<div class="org-form-group">
<label class="org-form-label" for="orgSectors">
업태 <span class="required-badge">필수</span>
</label>
<div class="org-form-input-wrapper">
<input type="text" class="org-form-input" id="orgSectors" th:field="*{orgSectors}"
placeholder="업태를 입력하세요" required>
</div>
</div>
<div class="info1 pt28">
<p class="title">
<span class="dot">업종</span>
</p>
<div class="info_line">
<div class="info_box1">
<input type="text" class="common_input_type_1" id="orgIndustryType" th:field="*{orgIndustryType}"
th:placeholder="#{portalOrg.Register.orgIndustryType}">
</div>
<div class="btn_check pc-only"></div>
</div>
<!-- Industry Type -->
<div class="org-form-group">
<label class="org-form-label" for="orgIndustryType">
업종 <span class="required-badge">필수</span>
</label>
<div class="org-form-input-wrapper">
<input type="text" class="org-form-input" id="orgIndustryType" th:field="*{orgIndustryType}"
placeholder="업종을 입력하세요" required>
</div>
</div>
<div class="info1 pt28">
<p class="title">
<span class="dot">회사 전화번호</span>
</p>
<div class="info_line">
<div class="info_box1">
<span class="input">
<div class="custom_select ct_sc h_inp2" id="orgAreaCode">
<div class="select_selected active">선택</div>
<ul class="common_selecttype select_hide select_items ct_sc2">
<li>국번없음</li>
<li>02</li>
<li>031</li>
<li>032</li>
<li>033</li>
<li>041</li>
<li>042</li>
<li>043</li>
<li>044</li>
<li>051</li>
<li>052</li>
<li>053</li>
<li>054</li>
<li>055</li>
<li>061</li>
<li>062</li>
<li>063</li>
<li>064</li>
<li>060</li>
<li>070</li>
<li>080</li>
<li>010</li>
<li>011</li>
<li>016</li>
<li>017</li>
<li>018</li>
<li>019</li>
</ul>
</div>
</span>
<span class="dash">-</span>
<span class="input">
<input type="text" id="orgPhoneNumber1" class="common_input_type_1 w_inp3" maxlength="4"
th:placeholder="#{portal.Register.phone1}">
</span>
<span class="dash">-</span>
<span class="input">
<input type="text" id="orgPhoneNumber2" class="common_input_type_1 w_inp3" maxlength="4"
th:placeholder="#{portal.Register.phone2}">
</span>
</div>
<input type="hidden" id="orgPhoneNumber" th:field="*{orgPhoneNumber}">
<div class="btn_check pc-only"></div>
<!-- Company Phone Number -->
<div class="org-form-group">
<label class="org-form-label">
회사 전화번호 <span class="required-badge">필수</span>
</label>
<div class="org-form-input-wrapper">
<div class="org-compound-input">
<select class="org-form-select" id="orgAreaCode" required>
<option value="">선택</option>
<option value="국번없음">국번없음</option>
<option value="02">02</option>
<option value="031">031</option>
<option value="032">032</option>
<option value="033">033</option>
<option value="041">041</option>
<option value="042">042</option>
<option value="043">043</option>
<option value="044">044</option>
<option value="051">051</option>
<option value="052">052</option>
<option value="053">053</option>
<option value="054">054</option>
<option value="055">055</option>
<option value="061">061</option>
<option value="062">062</option>
<option value="063">063</option>
<option value="064">064</option>
<option value="070">070</option>
<option value="080">080</option>
<option value="010">010</option>
<option value="011">011</option>
<option value="016">016</option>
<option value="017">017</option>
<option value="018">018</option>
<option value="019">019</option>
</select>
<span class="separator">-</span>
<input type="text" id="orgPhoneNumber1" class="org-form-input" maxlength="4"
placeholder="0000">
<span class="separator">-</span>
<input type="text" id="orgPhoneNumber2" class="org-form-input" maxlength="4"
placeholder="0000">
</div>
<input type="hidden" id="orgPhoneNumber" th:field="*{orgPhoneNumber}">
</div>
</div>
<!-- Service Name -->
<div class="org-form-group">
<label class="org-form-label" for="serviceName">
서비스명 <span class="required-badge">필수</span>
</label>
<div class="org-form-input-wrapper">
<input type="text" class="org-form-input" id="serviceName" th:field="*{serviceName}"
placeholder="서비스명을 입력하세요"
th:classappend="${#fields.hasErrors('serviceName')}? 'is-invalid'" required>
<div th:if="${#fields.hasErrors('serviceName')}" class="org-validation-message error">
<span th:errors="*{serviceName}"></span>
</div>
</div>
<div class="info1 pt28">
<p class="title">
<span class="dot">서비스명</span>
</p>
<div class="info_line">
<div class="info_box1">
<input type="text" class="common_input_type_1" id="serviceName" th:field="*{serviceName}"
th:placeholder="#{portalOrg.Register.serviceName}"
th:classappend="${#fields.hasErrors('serviceName')}? 'is-invalid'" required>
</div>
<div class="btn_check pc-only"></div>
<div th:if="${#fields.hasErrors('serviceName')}" class="invalid-feedback">
<span th:errors="*{serviceName}"></span>
</div>
</div>
<!-- Customer Service Phone Number -->
<div class="org-form-group">
<label class="org-form-label">
고객센터 전화번호 <span class="required-badge">필수</span>
</label>
<div class="org-form-input-wrapper">
<div class="org-compound-input">
<select class="org-form-select" id="scAreaCode" required>
<option value="">선택</option>
<option value="국번없음">국번없음</option>
<option value="02">02</option>
<option value="031">031</option>
<option value="032">032</option>
<option value="033">033</option>
<option value="041">041</option>
<option value="042">042</option>
<option value="043">043</option>
<option value="044">044</option>
<option value="051">051</option>
<option value="052">052</option>
<option value="053">053</option>
<option value="054">054</option>
<option value="055">055</option>
<option value="061">061</option>
<option value="062">062</option>
<option value="063">063</option>
<option value="064">064</option>
<option value="070">070</option>
<option value="080">080</option>
<option value="010">010</option>
<option value="011">011</option>
<option value="016">016</option>
<option value="017">017</option>
<option value="018">018</option>
<option value="019">019</option>
</select>
<span class="separator">-</span>
<input type="text" id="scPhoneNumber1" class="org-form-input" maxlength="4"
placeholder="0000">
<span class="separator">-</span>
<input type="text" id="scPhoneNumber2" class="org-form-input" maxlength="4"
placeholder="0000">
</div>
<input type="hidden" id="scPhoneNumber" th:field="*{scPhoneNumber}">
</div>
<div class="info1 pt28">
<p class="title">
<span class="dot">고객센터 전화번호</span>
</p>
<div class="info_line">
<div class="info_box1">
<span class="input">
<div class="custom_select ct_sc h_inp2" id="scAreaCode">
<div class="select_selected active">선택</div>
<ul class="common_selecttype select_hide select_items ct_sc2">
<li>국번없음</li>
<li>02</li>
<li>031</li>
<li>032</li>
<li>033</li>
<li>041</li>
<li>042</li>
<li>043</li>
<li>044</li>
<li>051</li>
<li>052</li>
<li>053</li>
<li>054</li>
<li>055</li>
<li>061</li>
<li>062</li>
<li>063</li>
<li>064</li>
<li>060</li>
<li>070</li>
<li>080</li>
<li>010</li>
<li>011</li>
<li>016</li>
<li>017</li>
<li>018</li>
<li>019</li>
</ul>
</div>
</span>
<span class="dash">-</span>
<span class="input">
<input type="text" id="scPhoneNumber1" class="common_input_type_1 w_inp3" maxlength="4"
th:placeholder="#{portal.Register.phone1}">
</span>
<span class="dash">-</span>
<span class="input">
<input type="text" id="scPhoneNumber2" class="common_input_type_1 w_inp3" maxlength="4"
th:placeholder="#{portal.Register.phone2}">
</span>
</div>
<input type="hidden" id="scPhoneNumber" th:field="*{scPhoneNumber}">
<div class="btn_check pc-only"></div>
</div>
<!-- Business Registration Certificate Upload -->
<div class="org-form-group">
<label class="org-form-label">
사업자 등록증 <span class="required-badge">필수</span>
</label>
<div class="org-form-input-wrapper">
<div class="org-file-upload">
<input type="text" class="org-file-display" id="fileNameDisplay" readonly placeholder="첨부파일을 선택하세요">
<input type="hidden" id="compRegFile" th:field="*{compRegFile}">
<label class="org-file-label" for="files">
<svg width="24" height="22" viewBox="0 0 24 22" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect width="23.9494" height="22" fill="url(#pattern0_3001_1395)"/>
<defs>
<pattern id="pattern0_3001_1395" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_3001_1395" transform="matrix(0.0217391 0 0 0.0236654 0 -0.0443038)"/>
</pattern>
<image id="image0_3001_1395" width="46" height="46" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC4AAAAuCAYAAABXuSs3AAAACXBIWXMAABcSAAAXEgFnn9JSAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAeRJREFUeNrsme1tgzAQhiHqAGxQRiATlGxAJ6i7QTpByQTtBrBBRiCdADYInaB0AvcsnaUTNR82GPzDr2Q5ODh6cO7LJgwWiHMeQZfgZReGYRO4LAEMreD/dYeWuQodQ/vh4ypcBK8JYC4eBMczXHGpzCXojICdB0xIwtcugX9IWx65h8kns8nyMAOWQRfjZUbG84Epj+QeYevfA/eJKPRpK3LU3K4KG+AV30ZG8OEAtEgq1Llu0L5m/qYwFYafS4WpvBDTkyrBbF7XWG1GVqTWnJuSuanGP6m18oeBcboi3UZBi+nAH3aOsCKqNCbwe4P/QjuZwB8MQiTDqnANPUE7o/Mbmw0FzInTVGT8OuVIC5xTJbaWqUQK59WVjrPHxinfgi69RegrGfluP3DcJT2PZWzoUtejirE8uAe3BN5ORIR+CGtcAZelbaJKDphR3yU0RJDOCXAAKckqFphhI5kxoavIil/22BQrU77cZNg8V+mVBPlqzokJ5KgojGQ6f1tlN2MjcwKYcNITHgbJ1sL4bSurWJTy8QFaH8c9uAf34B7cg3twD74xOK0/4hWP3OYonlUnDdTEYvK9V642G0FT8KP222ryhm0vVUt2QnvBX6fMM5wBHwfLDjl11WKdP6o/AQYA6KS7NOFJR28AAAAASUVORK5CYII="/>
</defs>
</svg>
파일선택
<input type="file" id="files" name="files"
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.hwp,.gif,.jpg,.png">
</label>
<button type="button" class="org-file-remove" id="fileRemoveBtn" style="display: none;">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<div class="info1 pt28">
<p class="title tit_add w_tit">
<span class="dot">사업자 등록증</span>
</p>
<div class="info_line info_add">
<div class="info_box1 info_add2">
<div class="filebox">
<input type="text" class="upload-name before" readonly="readonly" placeholder="첨부파일">
<input type="hidden" id="compRegFile" th:field="*{compRegFile}">
</div>
<div class="btn_check">
<div class="file_btn">
<label for="files">파일선택</label>
<input type="file" id="files" name="files" accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.hwp,.gif,.jpg,.png">
</div>
</div>
</div>
<div th:if="${#fields.hasErrors('compRegFile')}" class="invalid-feedback">
<span th:errors="*{compRegFile}"></span>
</div>
<div class="list-dot">
<p th:text="'첨부파일은 ' + ${@portalProperties.file.maxSize} + ' 이내로 등록해 주세요.'">첨부파일은 8MB 이내로 등록해 주세요.</p>
<p>문서파일과 이미지 파일만 등록 가능합니다. (pdf, doc, docx, xls, xlsx, ppt, pptx, hwp, gif, jpg, png)</p>
</div>
<div class="org-file-notice">
<p th:text="'첨부파일은 ' + ${@portalProperties.file.maxSize} + ' 이내로 등록해 주세요.'">첨부파일은 8MB 이내로 등록해 주세요.</p>
<p>문서파일과 이미지 파일만 등록 가능합니다. (pdf, doc, docx, xls, xlsx, ppt, pptx, hwp, gif, jpg, png)</p>
</div>
<div th:if="${#fields.hasErrors('compRegFile')}" class="org-validation-message error">
<span th:errors="*{compRegFile}"></span>
</div>
</div>
</div>
</th:block>
</body>
<th:block layout:fragment="contentScript">
<script th:fragment="orgInfoScript">
function validatePhoneNumber(prefix) {
const areaCode = document.getElementById(`${prefix}AreaCode`).value;
const part1 = document.getElementById(`${prefix}PhoneNumber1`).value.trim();
const part2 = document.getElementById(`${prefix}PhoneNumber2`).value.trim();
// 회사 전화번호 검증
if (!part1 || !part2) {
if (!areaCode || areaCode === '' || !part1 || !part2) {
customPopups.showAlert(`${prefix === 'org' ? '회사' : '고객센터'} 전화번호를 모두 입력해주세요.`);
return false;
}
@@ -289,7 +274,6 @@
}
document.addEventListener('DOMContentLoaded', function() {
activateSelect();
function checkAndSendData(parts, expectedLengths, hiddenInputId, url, paramName) {
let isValid = true;
@@ -305,26 +289,37 @@
if (isValid) {
document.getElementById(hiddenInputId).value = fullValue;
let values = {};
values[paramName] = fullValue;
// CSRF 토큰을 정확하게 가져오기
const csrfToken = document.querySelector('input[name="_csrf"]')?.value ||
document.querySelector('meta[name="_csrf"]')?.content;
document.querySelector('meta[name="_csrf"]')?.content;
htmx.ajax('POST', url, {
values: values,
// AJAX로 검증 요청
$.ajax({
url: url,
type: 'POST',
data: {
[paramName]: fullValue,
_csrf: csrfToken
},
headers: {
'X-XSRF-TOKEN': csrfToken,
'X-Custom-Handler': 'registration-validation'
},
swap: 'none',
target: '#' + hiddenInputId // 명시적 타겟 지정
}).then(function() {
// HTMX 이벤트가 제대로 발생하는지 확인
console.log('HTMX request completed');
}).catch(function(error) {
console.error('HTMX request failed:', error);
success: function(response) {
const validationDiv = document.getElementById(hiddenInputId + '-validation');
if (validationDiv) {
if (response.valid) {
validationDiv.className = 'org-validation-message success';
validationDiv.textContent = response.message || '유효한 번호입니다.';
} else {
validationDiv.className = 'org-validation-message error';
validationDiv.textContent = response.message || '유효하지 않은 번호입니다.';
}
}
},
error: function() {
console.error('Validation request failed');
}
});
}
}
@@ -346,10 +341,11 @@
checkAndSendData(parts, [6, 7], 'corpRegNo', '/check-corp-reg-no', 'corpRegNo');
}
// 파일 선택 관련 코드
// File upload handling
const fileInput = document.getElementById('files');
const fileNameInput = document.querySelector('.upload-name');
const fileNameDisplay = document.getElementById('fileNameDisplay');
const compRegFileInput = document.getElementById('compRegFile');
const fileRemoveBtn = document.getElementById('fileRemoveBtn');
fileInput.addEventListener('change', function() {
const files = fileInput.files;
@@ -368,22 +364,20 @@
fileNames.push(files[i].name);
}
fileNameInput.value = fileNames.join(', ');
fileNameInput.classList.remove('before');
fileNameInput.classList.add('i_trash', 'after');
fileNameDisplay.value = fileNames.join(', ');
fileNameDisplay.classList.add('has-file');
compRegFileInput.value = fileNames.join(', '); // hidden input에 파일 이름들 설정
fileRemoveBtn.style.display = 'block';
}
});
// 파일명 입력란 클릭 시 파일 제거
fileNameInput.addEventListener('click', function() {
if (fileNameInput.classList.contains('i_trash')) {
fileInput.value = '';
fileNameInput.value = '';
compRegFileInput.value = ''; // hidden input 값도 제거
fileNameInput.classList.remove('i_trash', 'after');
fileNameInput.classList.add('before');
}
// File remove button
fileRemoveBtn.addEventListener('click', function() {
fileInput.value = '';
fileNameDisplay.value = '';
compRegFileInput.value = '';
fileNameDisplay.classList.remove('has-file');
fileRemoveBtn.style.display = 'none';
});
function addListeners() {
@@ -394,21 +388,15 @@
document.getElementById('corpRegNo1').addEventListener('input', checkCorporateNumber);
document.getElementById('corpRegNo2').addEventListener('input', checkCorporateNumber);
// 전화번호 조합 함수 추가
// 전화번호 조합 함수
function combinePhoneNumber(prefix) {
const areaCode = document.querySelector(`#${prefix}AreaCode .select_selected`).textContent;
const areaCode = document.getElementById(`${prefix}AreaCode`).value;
const part1 = document.getElementById(`${prefix}PhoneNumber1`).value;
const part2 = document.getElementById(`${prefix}PhoneNumber2`).value;
if (areaCode === '선택' || !part1 || !part2) return;
if (!areaCode || areaCode === '' || !part1 || !part2) return;
let fullNumber = '';
// 지역번호가 '없음'인 경우에도 접두어로 저장
if (areaCode === '국번없음') {
fullNumber = `국번없음-${part1}-${part2}`;
} else {
fullNumber = `${areaCode}-${part1}-${part2}`;
}
const fullNumber = `${areaCode}-${part1}-${part2}`;
// hidden input에 하이픈이 포함된 값 설정
const hiddenInput = document.getElementById(`${prefix}PhoneNumber`);
@@ -433,12 +421,11 @@
});
// 지역번호 선택 시에도 전화번호 조합
document.querySelector('#orgAreaCode').addEventListener('click', () => combinePhoneNumber('org'));
document.querySelector('#scAreaCode').addEventListener('click', () => combinePhoneNumber('sc'));
document.getElementById('orgAreaCode').addEventListener('change', () => combinePhoneNumber('org'));
document.getElementById('scAreaCode').addEventListener('change', () => combinePhoneNumber('sc'));
}
addListeners();
});
</script>
</th:block>
@@ -1,181 +1,219 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.w3.org/1999/xhtml">
<html xmlns:th="http://www.thymeleaf.org">
<body>
<th:block th:fragment="orgUserInfoForm">
<div class="info1 pt28">
<p class="title">
<span class="dot">이메일 아이디</span>
</p>
<div class="info_line">
<div class="info_box1">
<input type="text" id="userId" class="common_input_type_1 w_inp"
th:placeholder="#{portalUser.Register.email}">
<span class="special_t">@</span>
<input type="text" id="domain" class="common_input_type_1 w_inp2"
th:placeholder="#{portalUser.Register.domain}">
<input type="hidden" id="loginId" name="loginId" th:value="${loginId}">
<input type="hidden" name="registrationType" th:value="${registrationType}"/>
<input type="hidden" id="registrationScenario" name="registrationScenario" value="new"/>
</div>
<div class="btn_check">
<a class="common_btn_type_2 cbt btn_check_email"
th:hx-post="@{/check_id}"
hx-include="#csrf, #loginId, [name='registrationType']"
hx-target="#email-validation"
hx-swap="none">
<span>중복체크</span>
</a>
</div>
</div>
<div id="email-validation"></div>
</div>
<div id="dynamicFormContainer">
<div id="individualConversionForm" style="display: none;">
<div th:include="~{apps/register/components/individualConversionForm :: individualConversionForm}"></div>
</div>
<div id="emailChangeForm" style="display: none;">
<div th:include="~{apps/register/components/emailChangeForm :: emailChangeForm}"></div>
</div>
<div id="newUserForm" style="display: none;">
<div th:include="~{apps/register/components/newUserInfoForm :: newUserForm}"></div>
<!-- Email ID -->
<div class="org-form-group">
<label class="org-form-label">
이메일 아이디 <span class="required-badge">필수</span>
</label>
<div class="org-form-input-wrapper">
<div class="org-input-row">
<div class="org-compound-input">
<input type="text" id="userId" class="org-form-input"
placeholder="이메일 아이디">
<span class="separator">@</span>
<input type="text" id="domain" class="org-form-input"
placeholder="도메인">
</div>
<button type="button" class="btn org-btn-check btn_check_email">
중복체크
</button>
</div>
<input type="hidden" id="loginId" name="loginId" th:value="${loginId}">
<input type="hidden" name="registrationType" th:value="${registrationType}"/>
<input type="hidden" id="registrationScenario" name="registrationScenario" value="new"/>
<div id="email-validation" class="org-validation-message"></div>
</div>
</div>
<!-- Dynamic Form Container -->
<div id="dynamicFormContainer">
<div id="individualConversionForm" style="display: none;">
<div th:include="~{apps/register/components/individualConversionForm :: individualConversionForm}"></div>
</div>
<div id="emailChangeForm" style="display: none;">
<div th:include="~{apps/register/components/emailChangeForm :: emailChangeForm}"></div>
</div>
<div id="newUserForm" style="display: none;">
<div th:include="~{apps/register/components/newUserInfoForm :: newUserForm}"></div>
</div>
</div>
</th:block>
</body>
<th:block layout:fragment="contentScript">
<script th:fragment="orgUserInfoScript">
// 이메일 조합 함수
function combineEmail() {
let emailId = document.getElementById('userId').value;
let domain = document.getElementById('domain').value;
let combined = emailId + '@' + domain;
let loginIdInput = document.getElementById('loginId');
if (loginIdInput) {
loginIdInput.value = combined;
<script th:fragment="orgUserInfoScript">
// 이메일 조합 함수
function combineEmail() {
let emailId = document.getElementById('userId').value;
let domain = document.getElementById('domain').value;
let combined = emailId + '@' + domain;
let loginIdInput = document.getElementById('loginId');
if (loginIdInput) {
loginIdInput.value = combined;
}
}
function handleEmailValidationResponse(response) {
const individualConversionForm = document.getElementById('individualConversionForm');
const emailChangeForm = document.getElementById('emailChangeForm');
const newUserForm = document.getElementById('newUserForm');
const userIdInput = document.getElementById('userId');
const domainInput = document.getElementById('domain');
const checkButton = document.querySelector('.btn_check_email');
const loginIdInput = document.getElementById('loginId');
const scenarioInput = document.getElementById('registrationScenario');
if (response && response.showPopup) {
switch (response.popupType) {
case "validEmail":
case "newUser":
// 시나리오 값 설정
if (scenarioInput) {
scenarioInput.value = 'new';
// hidden input이 form의 일부인지 확인
if (!scenarioInput.form) {
document.getElementById('registerForm').appendChild(scenarioInput);
}
}
}
function handleEmailValidationResponse(response) {
const individualConversionForm = document.getElementById('individualConversionForm');
const emailChangeForm = document.getElementById('emailChangeForm');
const newUserForm = document.getElementById('newUserForm');
const userIdInput = document.getElementById('userId');
const domainInput = document.getElementById('domain');
const checkButton = document.querySelector('.btn_check_email');
const loginIdInput = document.getElementById('loginId');
const scenarioInput = document.getElementById('registrationScenario');
customPopups.showAlert(response.message);
if (response && response.showPopup) {
switch (response.popupType) {
case "validEmail":
case "newUser":
// 시나리오 값 설정
if (scenarioInput) {
scenarioInput.value = 'new';
// hidden input이 form의 일부인지 확인
if (!scenarioInput.form) {
document.getElementById('registerForm').appendChild(scenarioInput);
}
}
if (checkButton) {
checkButton.style.display = 'none';
}
customPopups.showAlert(response.message);
if (userIdInput && domainInput && loginIdInput) {
userIdInput.disabled = true;
domainInput.disabled = true;
userIdInput.style.backgroundColor = '#e0e0e0';
domainInput.style.backgroundColor = '#e0e0e0';
if (checkButton) {
checkButton.style.display = 'none';
}
const combinedEmail = `${userIdInput.value.trim()}@${domainInput.value.trim()}`;
loginIdInput.value = combinedEmail;
}
if (userIdInput && domainInput && loginIdInput) {
userIdInput.disabled = true;
domainInput.disabled = true;
userIdInput.style.backgroundColor = '#e0e0e0';
domainInput.style.backgroundColor = '#e0e0e0';
if (newUserForm) newUserForm.style.display = 'block';
if (individualConversionForm) individualConversionForm.style.display = 'none';
if (emailChangeForm) emailChangeForm.style.display = 'none';
break;
const combinedEmail = `${userIdInput.value.trim()}@${domainInput.value.trim()}`;
loginIdInput.value = combinedEmail;
}
if (newUserForm) newUserForm.style.display = 'block';
if (individualConversionForm) individualConversionForm.style.display = 'none';
if (emailChangeForm) emailChangeForm.style.display = 'none';
break;
case "conversionOrChange":
customPopups.showEmailValidationPopup(
response.message,
function () {
// "이메일 유지" 선택 시 동작
if (scenarioInput) {
scenarioInput.value = 'retain';
if (!scenarioInput.form) {
document.getElementById('registerForm').appendChild(scenarioInput);
}
}
if (individualConversionForm) individualConversionForm.style.display = 'block';
if (emailChangeForm) emailChangeForm.style.display = 'none';
if (newUserForm) newUserForm.style.display = 'none';
if (userIdInput && domainInput) {
userIdInput.disabled = true;
domainInput.disabled = true;
userIdInput.style.backgroundColor = '#e0e0e0';
domainInput.style.backgroundColor = '#e0e0e0';
}
if (checkButton) {
checkButton.style.display = 'none';
}
},
function () {
// "이메일 전환" 선택 시 동작
if (scenarioInput) {
scenarioInput.value = 'change';
if (!scenarioInput.form) {
document.getElementById('registerForm').appendChild(scenarioInput);
}
}
if (emailChangeForm) emailChangeForm.style.display = 'block';
if (individualConversionForm) individualConversionForm.style.display = 'none';
if (newUserForm) newUserForm.style.display = 'none';
if (userIdInput && domainInput) {
userIdInput.disabled = true;
domainInput.disabled = true;
userIdInput.style.backgroundColor = '#e0e0e0';
domainInput.style.backgroundColor = '#e0e0e0';
}
if (checkButton) {
checkButton.style.display = 'none';
}
}
);
break;
case "error":
customPopups.showAlert(response.message);
break;
default:
console.error('알 수 없는 팝업 유형:', response.popupType);
break;
case "conversionOrChange":
customPopups.showEmailValidationPopup(
response.message,
function () {
// "이메일 유지" 선택 시 동작
if (scenarioInput) {
scenarioInput.value = 'retain';
if (!scenarioInput.form) {
document.getElementById('registerForm').appendChild(scenarioInput);
}
}
} else if (response && response.message) {
customPopups.showAlert(response.message);
} else {
customPopups.showAlert('알 수 없는 오류가 발생했습니다.');
}
if (individualConversionForm) individualConversionForm.style.display = 'block';
if (emailChangeForm) emailChangeForm.style.display = 'none';
if (newUserForm) newUserForm.style.display = 'none';
if (userIdInput && domainInput) {
userIdInput.disabled = true;
domainInput.disabled = true;
userIdInput.style.backgroundColor = '#e0e0e0';
domainInput.style.backgroundColor = '#e0e0e0';
}
if (checkButton) {
checkButton.style.display = 'none';
}
},
function () {
// "이메일 전환" 선택 시 동작
if (scenarioInput) {
scenarioInput.value = 'change';
if (!scenarioInput.form) {
document.getElementById('registerForm').appendChild(scenarioInput);
}
}
if (emailChangeForm) emailChangeForm.style.display = 'block';
if (individualConversionForm) individualConversionForm.style.display = 'none';
if (newUserForm) newUserForm.style.display = 'none';
if (userIdInput && domainInput) {
userIdInput.disabled = true;
domainInput.disabled = true;
userIdInput.style.backgroundColor = '#e0e0e0';
domainInput.style.backgroundColor = '#e0e0e0';
}
if (checkButton) {
checkButton.style.display = 'none';
}
}
);
break;
case "error":
customPopups.showAlert(response.message);
break;
default:
console.error('알 수 없는 팝업 유형:', response.popupType);
break;
}
} else if (response && response.message) {
customPopups.showAlert(response.message);
} else {
customPopups.showAlert('알 수 없는 오류가 발생했습니다.');
}
}
// 이메일 입력 필드에 이벤트 리스너 추가
document.addEventListener('DOMContentLoaded', function () {
let userIdInput = document.getElementById("userId");
let domainInput = document.getElementById("domain");
if (userIdInput) userIdInput.addEventListener("input", combineEmail);
if (domainInput) domainInput.addEventListener("input", combineEmail);
// Email check button click handler
document.addEventListener('DOMContentLoaded', function () {
let userIdInput = document.getElementById("userId");
let domainInput = document.getElementById("domain");
const checkButton = document.querySelector('.btn_check_email');
if (userIdInput) userIdInput.addEventListener("input", combineEmail);
if (domainInput) domainInput.addEventListener("input", combineEmail);
// Email check button with AJAX
if (checkButton) {
checkButton.addEventListener('click', function() {
const loginId = document.getElementById('loginId').value;
const registrationType = document.querySelector('input[name="registrationType"]').value;
if (!loginId || loginId.indexOf('@') === -1) {
customPopups.showAlert('이메일 주소를 입력해주세요.');
return;
}
const csrfToken = document.querySelector('input[name="_csrf"]')?.value;
// Show loading
document.getElementById('loadingOverlay')?.classList.add('active');
$.ajax({
url: '/check_id',
type: 'POST',
data: {
loginId: loginId,
registrationType: registrationType,
_csrf: csrfToken
},
success: function(response) {
document.getElementById('loadingOverlay')?.classList.remove('active');
handleEmailValidationResponse(response);
},
error: function() {
document.getElementById('loadingOverlay')?.classList.remove('active');
customPopups.showAlert('서버 오류가 발생했습니다. 다시 시도해주세요.');
}
});
});
}
});
</script>
</script>
</th:block>
</html>
</html>
@@ -1,501 +1,463 @@
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/kbank_base_layout}">
layout:decorate="~{layout/kjbank_title_layout}">
<body>
<section layout:fragment="contentFragment" class="content">
<script th:src="@{/js/htmx.min.js}"></script>
<div class="content_wrap">
<div class="sub_title2">
<h2 class="title add">법인회원가입</h2>
</div>
<div class="inner i_cs h_inner2">
<!-- 약관동의 섹션 -->
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<input type="hidden" name="registrationType" th:value="${registrationType}"/>
<th:block
th:replace="~{apps/register/userAgreementContent :: agreementContent(${termsOfUse}, ${privacyCollect})}"></th:block>
<form name="portalOrg" id="registerForm" role="form" th:action="@{/signup/portalOrg}"
th:object="${portalOrg}" method="post">
<!-- 법인 기본 정보 -->
<div class="terms_box">
<p>법인 기본 정보</p>
<span class="dot">필수 입력</span>
</div>
<div class="form_type">
<th:block th:replace="~{apps/register/components/orgBasicInfoForm :: orgInfoForm}"></th:block>
</div>
<!-- 기본 관리자 정보 -->
<div class="terms_box">
<p>법인 관리자 정보</p>
<span class="dot">필수 입력</span>
</div>
<div class="top_info mtb12">
<ul>
<li>법인관리자는 API 신청 및 이용자 초대 등 모든 권한을 보유합니다.</li>
</ul>
</div>
<div class="form_type" th:if="${registrationType == 'corporate'}">
<th:block th:replace="~{apps/register/components/orgUserInfoForm :: orgUserInfoForm}"></th:block>
</div>
<!-- 가입 버튼 -->
<div class="form_type">
<div class="btn_application">
<a type="button" class="common_btn_type_1 gray" id="cancelButton"><span>취소</span></a>
<div class="btn_gap"></div>
<a type="button" class="common_btn_type_1 btn_register"><span>가입신청</span></a>
</div>
</div>
</form>
</div>
</div>
<section layout:fragment="title">
<div class="page-title-banner">
<img th:src="@{/img/user_register_title.png}" alt="법인회원가입" class="title-image">
<h1>법인회원가입</h1>
</div>
</section>
<section layout:fragment="contentFragment">
<div class="org-register-page">
<!-- Page Title Banner -->
<div class="org-register-container">
<!-- Hidden Fields -->
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<input type="hidden" name="registrationType" th:value="${registrationType}"/>
<!-- Alert Container -->
<div id="alertContainer"></div>
<div class="org-section-header" style="margin-top: 60px;margin-bottom: 60px;">
<h3>법인 회원 가입 후 서비스 또는 API 이용을 하실 수 있습니다.</h3>
</div>
<!-- Agreement Section -->
<th:block th:replace="~{apps/register/userAgreementContent :: agreementContent(${termsOfUse}, ${privacyCollect})}"></th:block>
<form name="portalOrg" id="registerForm" role="form" th:action="@{/signup/portalOrg}"
th:object="${portalOrg}" method="post">
<!-- Corporate Basic Information Section -->
<div class="org-section-header org-section-header--agreement">
<h3>법인 기본 정보</h3>
<span class="required-badge">필수 입력</span>
</div>
<th:block th:replace="~{apps/register/components/orgBasicInfoForm :: orgInfoForm}"></th:block>
<!-- Corporate Admin Information Section -->
<div class="org-section-header org-section-header--agreement" style="margin-top: 48px;">
<h3>법인 관리자 정보</h3>
<span class="required-badge">필수 입력</span>
</div>
<div class="org-info-notice">
<ul>
<li>법인관리자는 API 신청 및 이용자 초대 등 모든 권한을 보유합니다.</li>
</ul>
</div>
<th:block th:if="${registrationType == 'corporate'}">
<th:block th:replace="~{apps/register/components/orgUserInfoForm :: orgUserInfoForm}"></th:block>
</th:block>
<!-- Action Buttons -->
<div class="org-action-buttons">
<button type="button" class="btn btn-secondary" id="cancelButton">취소</button>
<button type="button" class="btn btn-primary btn_register">가입신청</button>
</div>
</form>
<!-- Loading Overlay -->
<div class="org-loading-overlay" id="loadingOverlay">
<div class="spinner"></div>
</div>
</div>
</div>
</section>
<th:block layout:fragment="contentScript">
<script th:if="${error}" th:inline="javascript">
$(document).ready(function () {
customPopups.showAlert([[${error}]]);
<script th:if="${error}" th:inline="javascript">
$(document).ready(function () {
customPopups.showAlert([[${error}]]);
});
</script>
<script th:replace="~{apps/register/userAgreementContent :: agreementScript}"></script>
<script th:replace="~{apps/register/components/orgBasicInfoForm :: orgInfoScript}"></script>
<script th:replace="~{apps/register/components/orgUserInfoForm :: orgUserInfoScript}"></script>
<script th:replace="~{apps/register/components/emailChangeForm :: emailChangeScript}"></script>
<script th:replace="~{apps/register/components/newUserInfoForm :: newUserScript}"></script>
<script th:inline="javascript">
document.addEventListener('DOMContentLoaded', function () {
let form = document.getElementById('registerForm');
let agreementForm = document.getElementById('agreementForm');
let btn_register = document.querySelector('.btn_register');
let isAuthVerified = false;
// registrationScenario를 hidden input에서 가져오도록 수정
let registrationScenario = document.getElementById('registrationScenario')?.value || 'new';
// hidden input의 값이 변경되면 registrationScenario 변수도 업데이트
const scenarioInput = document.getElementById('registrationScenario');
if (scenarioInput) {
scenarioInput.addEventListener('change', function () {
registrationScenario = this.value;
console.log('Registration Scenario Updated:', registrationScenario);
});
</script>
<script th:replace="~{apps/register/userAgreementContent :: agreementScript}"></script>
<script th:replace="~{apps/register/components/orgBasicInfoForm :: orgInfoScript}"></script>
<script th:replace="~{apps/register/components/orgUserInfoForm :: orgUserInfoScript}"></script>
<script th:replace="~{apps/register/components/emailChangeForm :: emailChangeScript}"></script>
<script th:replace="~{apps/register/components/newUserInfoForm :: newUserScript}"></script>
<script th:inline="javascript">
document.addEventListener('DOMContentLoaded', function () {
activateSelect();
let form = document.getElementById('registerForm');
let agreementForm = document.getElementById('agreementForm');
let btn_register = document.querySelector('.btn_register');
let isAuthVerified = false;
// registrationScenario를 hidden input에서 가져오도록 수정
let registrationScenario = document.getElementById('registrationScenario')?.value || 'new';
// hidden input의 값이 변경되면 registrationScenario 변수도 업데이트
const scenarioInput = document.getElementById('registrationScenario');
if (scenarioInput) {
scenarioInput.addEventListener('change', function () {
registrationScenario = this.value;
console.log('Registration Scenario Updated:', registrationScenario);
});
// MutationObserver를 사용하여 값 변경 감지
const observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (mutation.type === 'attributes' && mutation.attributeName === 'value') {
registrationScenario = scenarioInput.value;
console.log('Registration Scenario Updated (from mutation):', registrationScenario);
}
});
});
observer.observe(scenarioInput, {attributes: true});
// MutationObserver를 사용하여 값 변경 감지
const observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (mutation.type === 'attributes' && mutation.attributeName === 'value') {
registrationScenario = scenarioInput.value;
console.log('Registration Scenario Updated (from mutation):', registrationScenario);
}
});
});
observer.observe(scenarioInput, {attributes: true});
}
// 시나리오별 필수 필드 정의
const requiredFieldsByScenario = {
new: {
user: ['loginId', 'userName', 'password', 'password2', 'mobileNumber', 'authNumber'],
org: ['compRegNo', 'corpRegNo', 'orgName', 'ceoName', 'orgAddr', 'orgSectors', 'orgIndustryType', 'orgPhoneNumber', 'scPhoneNumber', 'serviceName', 'compRegFile', 'files']
},
retain: {
user: ['loginId', 'passwordConfirmIndividual'],
org: ['compRegNo', 'corpRegNo', 'orgName', 'ceoName', 'orgAddr', 'orgSectors', 'orgIndustryType', 'orgPhoneNumber', 'scPhoneNumber', 'serviceName', 'compRegFile', 'files']
},
change: {
user: ['loginId', 'passwordConfirmEmailChange', 'newLoginId'],
org: ['compRegNo', 'corpRegNo', 'orgName', 'ceoName', 'orgAddr', 'orgSectors', 'orgIndustryType', 'orgPhoneNumber', 'scPhoneNumber', 'serviceName', 'compRegFile', 'files']
}
};
// 시나리오별 필수 필드 정의
const requiredFieldsByScenario = {
new: {
user: ['loginId', 'userName', 'password', 'password2', 'mobileNumber', 'authNumber'],
org: ['compRegNo', 'corpRegNo', 'orgName', 'ceoName', 'orgAddr', 'orgSectors', 'orgIndustryType', 'orgPhoneNumber', 'scPhoneNumber', 'serviceName', 'compRegFile', 'files']
},
retain: {
user: ['loginId', 'passwordConfirmIndividual'],
org: ['compRegNo', 'corpRegNo', 'orgName', 'ceoName', 'orgAddr', 'orgSectors', 'orgIndustryType', 'orgPhoneNumber', 'scPhoneNumber', 'serviceName', 'compRegFile', 'files']
},
change: {
user: ['loginId', 'passwordConfirmEmailChange', 'newLoginId'],
org: ['compRegNo', 'corpRegNo', 'orgName', 'ceoName', 'orgAddr', 'orgSectors', 'orgIndustryType', 'orgPhoneNumber', 'scPhoneNumber', 'serviceName', 'compRegFile', 'files']
}
};
// 이메일 유효성 검사 함수
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
// 이메일 유효성 검사 함수
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
function collectFormData() {
const form = document.createElement('form');
form.enctype = 'multipart/form-data';
form.method = 'POST';
form.action = '/signup/portalOrg';
form.style.display = 'none';
const fields = requiredFieldsByScenario[registrationScenario];
if (!fields) {
console.error('Unknown scenario');
return form;
}
// 시나리오 값 추가
const scenarioInput = document.getElementById('registrationScenario');
if (scenarioInput) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'registrationScenario';
input.value = scenarioInput.value;
form.appendChild(input);
} else {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'registrationScenario';
input.value = registrationScenario;
form.appendChild(input);
}
// user와 org 필드를 따로 처리
fields.user.forEach(fieldId => {
const element = document.getElementById(fieldId);
if (element) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = fieldId;
input.value = element.value;
form.appendChild(input);
}
});
fields.org.forEach(fieldId => {
const element = document.getElementById(fieldId);
if (element) {
if (element.type === 'file') {
const files = element.files;
if (files.length > 0) {
const input = document.createElement('input');
input.type = 'file';
input.name = fieldId;
input.files = files;
form.appendChild(input);
}
} else {
const input = document.createElement('input');
input.type = 'hidden';
input.name = fieldId;
input.value = element.value;
form.appendChild(input);
}
}
});
function collectFormData() {
const form = document.createElement('form');
form.enctype = 'multipart/form-data';
form.method = 'POST';
form.action = '/signup/portalOrg';
form.style.display = 'none';
// Add confirmPassword manually based on the scenario
if (registrationScenario === 'retain') {
const passwordConfirmIndividual = document.getElementById('passwordConfirmIndividual');
if (passwordConfirmIndividual) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'confirmPassword';
input.value = passwordConfirmIndividual.value;
form.appendChild(input);
}
} else if (registrationScenario === 'change') {
const passwordConfirmEmailChange = document.getElementById('passwordConfirmEmailChange');
if (passwordConfirmEmailChange) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'confirmPassword';
input.value = passwordConfirmEmailChange.value;
form.appendChild(input);
}
}
const fields = requiredFieldsByScenario[registrationScenario];
// 공통 필드 추가
if (document.getElementById('termsOfUse')) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'termsOfUse';
input.value = document.getElementById('termsOfUse').checked;
form.appendChild(input);
}
if (!fields) {
console.error('Unknown scenario');
return form;
}
if (document.getElementById('privacyCollect')) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'privacyCollect';
input.value = document.getElementById('privacyCollect').checked;
form.appendChild(input);
}
// 시나리오 값 추가
const scenarioInput = document.getElementById('registrationScenario');
if (scenarioInput) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'registrationScenario';
input.value = scenarioInput.value;
form.appendChild(input);
} else {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'registrationScenario';
input.value = registrationScenario;
form.appendChild(input);
}
// 시스템 필드 추가
const csrfToken = document.querySelector('input[name="_csrf"]');
if (csrfToken) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = '_csrf';
input.value = csrfToken.value;
form.appendChild(input);
}
// user와 org 필드를 따로 처리
fields.user.forEach(fieldId => {
const element = document.getElementById(fieldId);
if (element) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = fieldId;
input.value = element.value;
form.appendChild(input);
}
});
const registrationType = document.querySelector('input[name="registrationType"]');
if (registrationType) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'registrationType';
input.value = registrationType.value;
form.appendChild(input);
}
fields.org.forEach(fieldId => {
const element = document.getElementById(fieldId);
if (element) {
if (element.type === 'file') {
const files = element.files;
if (files.length > 0) {
const input = document.createElement('input');
input.type = 'file';
input.name = fieldId;
input.files = files;
form.appendChild(input);
}
} else {
const input = document.createElement('input');
input.type = 'hidden';
input.name = fieldId;
input.value = element.value;
form.appendChild(input);
}
}
});
console.log('Collected Form Data for scenario:',
document.getElementById('registrationScenario')?.value || registrationScenario);
return form;
}
// Add confirmPassword manually based on the scenario
if (registrationScenario === 'retain') {
const passwordConfirmIndividual = document.getElementById('passwordConfirmIndividual');
if (passwordConfirmIndividual) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'confirmPassword';
input.value = passwordConfirmIndividual.value;
form.appendChild(input);
}
} else if (registrationScenario === 'change') {
const passwordConfirmEmailChange = document.getElementById('passwordConfirmEmailChange');
if (passwordConfirmEmailChange) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'confirmPassword';
input.value = passwordConfirmEmailChange.value;
form.appendChild(input);
}
}
// 시나리오별 유효성 검사
function validateFormByScenario() {
// 공통 필드 추가
if (document.getElementById('termsOfUse')) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'termsOfUse';
input.value = document.getElementById('termsOfUse').checked;
form.appendChild(input);
}
const currentScenario = document.getElementById('registrationScenario')?.value || 'new';
console.log('Registration Scenario:', currentScenario);
if (document.getElementById('privacyCollect')) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'privacyCollect';
input.value = document.getElementById('privacyCollect').checked;
form.appendChild(input);
}
if (!currentScenario) {
console.log('Unknown scenario');
return false;
}
// 시스템 필드 추가
const csrfToken = document.querySelector('input[name="_csrf"]');
if (csrfToken) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = '_csrf';
input.value = csrfToken.value;
form.appendChild(input);
}
switch (registrationScenario) {
case 'new':
const loginIdElement = document.getElementById('loginId');
const userNameElement = document.getElementById('userName');
const registrationType = document.querySelector('input[name="registrationType"]');
if (registrationType) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'registrationType';
input.value = registrationType.value;
form.appendChild(input);
}
const isEmailValid = loginIdElement ? isValidEmail(loginIdElement.value) : false;
const hasUserName = userNameElement ? userNameElement.value.trim() !== '' : false;
const isPasswordValid = document.getElementById('isPasswordValid').value.trim() === 'true';
const isPasswordMatch = document.getElementById('isPasswordMatch').value.trim() === 'true';
console.log('Collected Form Data for scenario:',
document.getElementById('registrationScenario')?.value || registrationScenario);
return form;
}
console.log('New User Validation:', {
email: loginIdElement?.value,
isEmailValid,
isPasswordValid,
isPasswordMatch,
userName: userNameElement?.value,
hasUserName,
isAuthVerified,
requiredFields: currentScenario
});
return isEmailValid && isPasswordValid && isPasswordMatch && hasUserName && isAuthVerified;
// 시나리오별 유효성 검사
function validateFormByScenario() {
case 'retain':
const passwordConfirmElement = document.getElementById('passwordConfirmIndividual');
const hasPassword = passwordConfirmElement ? passwordConfirmElement.value.length >= 8 : false;
const retainLoginIdElement = document.getElementById('loginId');
const currentScenario = document.getElementById('registrationScenario')?.value || 'new';
console.log('Registration Scenario:', currentScenario);
console.log('Retain User Validation:', {
email: retainLoginIdElement?.value,
isEmailValid: retainLoginIdElement ? isValidEmail(retainLoginIdElement.value) : false,
passwordLength: passwordConfirmElement?.value.length,
hasPassword,
requiredFields: currentScenario
});
return retainLoginIdElement && isValidEmail(retainLoginIdElement.value) && hasPassword;
if (!currentScenario) {
console.log('Unknown scenario');
return false;
}
case 'change':
const changeLoginIdElement = document.getElementById('loginId');
const newLoginIdElement = document.getElementById('newLoginId');
const changePasswordElement = document.getElementById('passwordConfirmEmailChange');
const hasChangePassword = changePasswordElement ? changePasswordElement.value.length >= 8 : false;
switch (registrationScenario) {
case 'new':
const loginIdElement = document.getElementById('loginId');
const userNameElement = document.getElementById('userName');
console.log('Change Email Validation:', {
currentEmail: changeLoginIdElement?.value,
newEmail: newLoginIdElement?.value,
hasPassword: hasChangePassword,
requiredFields: currentScenario
});
return changeLoginIdElement && newLoginIdElement &&
isValidEmail(changeLoginIdElement.value) &&
hasChangePassword &&
isValidEmail(newLoginIdElement.value);
const isEmailValid = loginIdElement ? isValidEmail(loginIdElement.value) : false;
const hasUserName = userNameElement ? userNameElement.value.trim() !== '' : false;
const isPasswordValid = document.getElementById('isPasswordValid').value.trim() === 'true';
const isPasswordMatch = document.getElementById('isPasswordMatch').value.trim() === 'true';
default:
console.log('Unknown scenario');
return false;
}
}
console.log('New User Validation:', {
email: loginIdElement?.value,
isEmailValid,
isPasswordValid,
isPasswordMatch,
userName: userNameElement?.value,
hasUserName,
isAuthVerified,
requiredFields: currentScenario
});
return isEmailValid && isPasswordValid && isPasswordMatch && hasUserName && isAuthVerified;
// 공통 필드 유효성 검사
function validateCommonFields() {
case 'retain':
const passwordConfirmElement = document.getElementById('passwordConfirmIndividual');
const hasPassword = passwordConfirmElement ? passwordConfirmElement.value.length >= 8 : false;
const retainLoginIdElement = document.getElementById('loginId');
if(!validatePhoneNumber('org') || !validatePhoneNumber('sc')) {
return false;
}
console.log('Retain User Validation:', {
email: retainLoginIdElement?.value,
isEmailValid: retainLoginIdElement ? isValidEmail(retainLoginIdElement.value) : false,
passwordLength: passwordConfirmElement?.value.length,
hasPassword,
requiredFields: currentScenario
});
return retainLoginIdElement && isValidEmail(retainLoginIdElement.value) && hasPassword;
const validations = {
termsOfUse: $('#termsOfUse').prop('checked'),
privacyPolicy: $('#privacyCollect').prop('checked'),
compRegNo: $('#compRegNo').val().trim() !== '',
corpRegNo: $('#corpRegNo').val().trim() !== '',
orgName: $('#orgName').val().trim() !== '',
ceoName: $('#ceoName').val().trim() !== '',
orgAddr: $('#orgAddr').val().trim() !== '',
orgPhoneNumber: $('#orgPhoneNumber').val().trim() !== '',
scPhoneNumber: $('#scPhoneNumber').val().trim() !== '',
serviceName: $('#serviceName').val().trim() !== '',
compRegFile: $('#compRegFile').val().trim() !== ''
};
case 'change':
const changeLoginIdElement = document.getElementById('loginId');
const newLoginIdElement = document.getElementById('newLoginId');
const changePasswordElement = document.getElementById('passwordConfirmEmailChange');
const hasChangePassword = changePasswordElement ? changePasswordElement.value.length >= 8 : false;
console.log('Common Fields Validation:', validations);
return Object.values(validations).every(value => value === true);
}
console.log('Change Email Validation:', {
currentEmail: changeLoginIdElement?.value,
newEmail: newLoginIdElement?.value,
hasPassword: hasChangePassword,
requiredFields: currentScenario
});
return changeLoginIdElement && newLoginIdElement &&
isValidEmail(changeLoginIdElement.value) &&
hasChangePassword &&
isValidEmail(newLoginIdElement.value);
// 전체 폼 유효성 검사
function validateForm() {
const scenarioValidation = validateFormByScenario();
const commonValidation = validateCommonFields();
default:
console.log('Unknown scenario');
return false;
}
}
console.log('Form validation result:', {
scenarioValidation,
commonValidation,
registrationScenario
});
// 공통 필드 유효성 검사
function validateCommonFields() {
return scenarioValidation && commonValidation;
}
if(!validatePhoneNumber('org') || !validatePhoneNumber('sc')) {
return false;
}
let isSubmitting = false;
const validations = {
termsOfUse: $('#termsOfUse').prop('checked'),
privacyPolicy: $('#privacyCollect').prop('checked'),
compRegNo: $('#compRegNo').val().trim() !== '',
corpRegNo: $('#corpRegNo').val().trim() !== '',
orgName: $('#orgName').val().trim() !== '',
ceoName: $('#ceoName').val().trim() !== '',
orgAddr: $('#orgAddr').val().trim() !== '',
orgPhoneNumber: $('#orgPhoneNumber').val().trim() !== '',
scPhoneNumber: $('#scPhoneNumber').val().trim() !== '',
serviceName: $('#serviceName').val().trim() !== '',
compRegFile: $('#compRegFile').val().trim() !== ''
};
// 등록 버튼 클릭 이벤트
btn_register.addEventListener('click', function (event) {
event.preventDefault();
event.stopPropagation();
console.log('Common Fields Validation:', validations);
return Object.values(validations).every(value => value === true);
}
// Prevent resubmission
if (isSubmitting) {
console.log('Form is already being submitted...');
return;
}
// 전체 폼 유효성 검사
function validateForm() {
const scenarioValidation = validateFormByScenario();
const commonValidation = validateCommonFields();
isSubmitting = true;
btn_register.disabled = true;
console.log('Form validation result:', {
scenarioValidation,
commonValidation,
registrationScenario
});
// Show loading overlay
document.getElementById('loadingOverlay').classList.add('active');
return scenarioValidation && commonValidation;
}
try {
form.classList.add('was-validated');
if (agreementForm) {
agreementForm.classList.add('was-validated');
}
let isSubmitting = false;
console.log('Validating form...');
// 등록 버튼 클릭 이벤트
btn_register.addEventListener('click', function (event) {
event.preventDefault();
event.stopPropagation();
if (validateForm()) {
console.log('Form validation passed, collecting form data...');
const form = collectFormData();
document.body.appendChild(form);
form.submit();
} else {
console.log('Form validation failed');
// Prevent resubmission
if (isSubmitting) {
console.log('Form is already being submitted...');
return;
}
// 현재 상태 출력
const scenarioValidation = validateFormByScenario();
const commonValidation = validateCommonFields();
isSubmitting = true;
btn_register.disabled = true;
try {
form.classList.add('was-validated');
if (agreementForm) {
agreementForm.classList.add('was-validated');
}
const isPasswordValid = document.getElementById('isPasswordValid')?.value.trim() === 'true';
const isPasswordMatch = document.getElementById('isPasswordMatch')?.value.trim() === 'true';
console.log('Validating form...');
console.log('Validation details:', {
scenarioValidation,
commonValidation,
registrationScenario,
isAuthVerified,
isPasswordValid,
isPasswordMatch
});
if (validateForm()) {
console.log('Form validation passed, collecting form data...');
const form = collectFormData();
document.body.appendChild(form);
form.submit();
} else {
console.log('Form validation failed');
// 현재 상태 출력
const scenarioValidation = validateFormByScenario();
const commonValidation = validateCommonFields();
const isPasswordValid = document.getElementById('isPasswordValid').value.trim() === 'true';
const isPasswordMatch = document.getElementById('isPasswordMatch').value.trim() === 'true';
console.log('Validation details:', {
scenarioValidation,
commonValidation,
registrationScenario,
isAuthVerified,
isPasswordValid,
isPasswordMatch
});
let errorMessage = '모든 필수 항목을 입력하고 약관에 동의해주세요.'
if (!scenarioValidation) {
customPopups.showAlert(errorMessage);
}
if (!commonValidation) {
customPopups.showAlert(errorMessage);
}
customPopups.showAlert(errorMessage);
// Reset submission state since validation failed
isSubmitting = false;
btn_register.disabled = false;
}
} catch (error) {
console.error('Form submission error:', error);
// Reset submission state on error
isSubmitting = false;
btn_register.disabled = false;
}
let errorMessage = '모든 필수 항목을 입력하고 약관에 동의해주세요.'
customPopups.showAlert(errorMessage);
// Reset submission state since validation failed
isSubmitting = false;
btn_register.disabled = false;
document.getElementById('loadingOverlay').classList.remove('active');
}
} catch (error) {
console.error('Form submission error:', error);
// Reset submission state on error
isSubmitting = false;
btn_register.disabled = false;
document.getElementById('loadingOverlay').classList.remove('active');
}
});
// 신규 이메일 중복 체크 응답 처리 함수
function handleNewEmailValidationResponse(response) {
if (response.valid) {
customPopups.showAlert(response.message || '신규 이메일 정보입니다.');
} else {
customPopups.showAlert(response.message || '이미 사용 중인 이메일입니다.');
}
}
// 신규 이메일 중복 체크 응답 처리 함수
function handleNewEmailValidationResponse(response) {
if (response.valid) {
customPopups.showAlert(response.message || '신규 이메일 정보입니다.');
} else {
customPopups.showAlert(response.message || '이미 사용 중인 이메일입니다.');
}
}
// HTMX 이벤트 리스너 - 모든 HTMX 응답을 중앙에서 처리
document.body.addEventListener('htmx:afterRequest', function (event) {
const targetId = event.detail.target.id;
let response;
// 인증 완료 이벤트 리스너
document.addEventListener('authVerified', function () {
isAuthVerified = true;
});
try {
response = JSON.parse(event.detail.xhr.response);
} catch (e) {
console.error('응답 JSON 파싱 실패:', e);
customPopups.showAlert('서버 응답을 처리할 수 없습니다.');
return;
}
// 비밀번호 관련 검증은 컴포넌트의 로직을 따르도록 return
if (targetId === 'password-validation' || targetId === 'password-match-validation') {
return;
}
// 나머지 요청들에 대해서만 처리
if (event.detail.xhr.getResponseHeader('X-Custom-Handler') === 'registration-validation') {
if (response && response.message) {
customPopups.showAlert(response.message);
}
return;
}
// 기존 응답 처리 로직
if (targetId === 'email-validation') {
handleEmailValidationResponse(response);
} else if (targetId === 'new-email-validation') {
handleNewEmailValidationResponse(response);
}
// 사업자등록번호와 법인등록번호에 대한 커스텀 이벤트 디스패치
if (targetId === 'compRegNo' || targetId === 'corpRegNo') {
const customEvent = new CustomEvent('htmxResponse', {
detail: {
targetId: targetId,
response: response
}
});
document.dispatchEvent(customEvent);
}
});
// 커스텀 이벤트 리스너
document.addEventListener('htmxResponse', function (event) {
const {targetId, response} = event.detail;
if (targetId === 'compRegNo') {
customPopups.showAlert(response.message || (response.valid ? '유효한 사업자등록번호입니다.' : '유효하지 않은 사업자등록번호입니다.'));
} else if (targetId === 'corpRegNo') {
customPopups.showAlert(response.message || (response.valid ? '유효한 법인등록번호입니다.' : '유효하지 않은 법인등록번호입니다.'));
}
});
// 인증 완료 이벤트 리스너
document.addEventListener('authVerified', function () {
isAuthVerified = true;
});
// 취소 버튼 클릭 시
document.getElementById('cancelButton').addEventListener('click', function () {
location.href = '/login';
});
});
</script>
// 취소 버튼 클릭 시
document.getElementById('cancelButton').addEventListener('click', function () {
location.href = '/login';
});
});
</script>
</th:block>
</body>
</html>
@@ -2,66 +2,49 @@
<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}">
layout:decorate="~{layout/kjbank_base_layout}">
<body>
<section layout:fragment="contentFragment" class="content">
<div class="content_wrap">
<div class="sub_title2">
<h2 class="title add3">회원가입</h2>
<th:block layout:fragment="contentFragment">
<div class="signup-selection-page">
<div class="signup-selection-container">
<div class="signup-selection-card">
<!-- Logo -->
<div class="signup-logo">
<img th:src="@{/img/logo/logo_box.png}" alt="광주은행">
</div>
<div class="inner i_cs2">
<div class="con_title">
<p>Kbank API Portal을 사용하기 위해<br>
<span class="bold">회원가입</span>해 주세요.
</p>
</div>
<div class="member_list">
<div class="mb_group1">
<a th:href="@{/signup/portalUser}" class="signup-btn">
<span>개인회원가입</span>
</a>
</div>
<div class="mb_group2">
<a th:href="@{/signup/portalOrg}" class="signup-btn">
<span>법인회원가입</span>
</a>
</div>
</div>
<!-- Message -->
<p class="signup-message">광주은행 API Portal 사용을 위해 회원 가입해 주세요.</p>
<div class="login_list m-only">
<ul>
<li>
<a th:href="@{/}">
<img th:src="@{/img/icon/icon_home3.png}" alt=""> 홈으로
</a>
</li>
<li class="line">|</li>
<li>
<a th:href="@{/login}">
<img th:src="@{/img/icon/icon_login2.png}" alt=""> 로그인 하기
</a>
</li>
</ul>
</div>
<div class="login_list pc-only">
<ul>
<li>
<a th:href="@{/}">
<img th:src="@{/img/icon/icon_home.png}" alt=""> 홈으로
</a>
</li>
<li class="line">|</li>
<li>
<a th:href="@{/login}">
<img th:src="@{/img/icon/icon_login.png}" alt=""> 로그인 하기
</a>
</li>
</ul>
</div>
<!-- Signup Buttons -->
<div class="signup-buttons">
<!-- Individual Signup Button -->
<a th:href="@{/signup/portalUser}" class="signup-btn signup-btn-individual">
<svg width="22" height="30" viewBox="0 0 22 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.4287 12.748C19.0578 12.7481 21.9998 15.7302 22 19.4092V29.3447C21.9998 29.6617 21.7783 29.9263 21.4834 29.9873L21.3535 30H0.646484C0.289425 29.9998 0.00024463 29.7067 0 29.3447V18.2607C0 15.2164 2.43446 12.748 5.4375 12.748C6.67211 12.7481 7.86991 13.1742 8.83398 13.9561L9.13965 14.2031C9.4187 14.4294 9.46443 14.8431 9.24121 15.126C9.01802 15.4088 8.61011 15.4546 8.33105 15.2285L8.02539 14.9805C7.29081 14.3848 6.37819 14.0596 5.4375 14.0596C3.1492 14.0596 1.29395 15.9409 1.29395 18.2607V28.6885H20.7061V19.4092C20.7058 16.4548 18.3431 14.0596 15.4287 14.0596C13.2292 14.0598 11.2606 15.4434 10.4883 17.5312L9.98828 18.8818C9.86275 19.2209 9.48978 19.3927 9.15527 19.2656C8.82079 19.1383 8.65091 18.7601 8.77637 18.4209L9.27637 17.0703C10.2382 14.4705 12.6898 12.7482 15.4287 12.748ZM11 0C14.2162 8.65396e-06 16.8231 2.64285 16.8232 5.90332C16.8232 9.16394 14.2163 11.8076 11 11.8076C7.78365 11.8076 5.17676 9.16394 5.17676 5.90332C5.17693 2.64284 7.78375 0 11 0ZM11 1.31152C8.4985 1.31152 6.47087 3.36743 6.4707 5.90332C6.4707 8.43936 8.49839 10.4951 11 10.4951C13.5016 10.4951 15.5293 8.43935 15.5293 5.90332C15.5291 3.36743 13.5015 1.31153 11 1.31152Z" fill="white"/>
</svg>
<span>개인회원 가입</span>
</a>
<!-- Organization Signup Button -->
<a th:href="@{/signup/portalOrg}" class="signup-btn signup-btn-organization">
<svg width="26" height="30" viewBox="0 0 26 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.001 7.2002C14.3062 7.20046 15.3652 8.27529 15.3652 9.60059V27.5996L15.3623 27.7236C15.3009 28.9505 14.3313 29.9346 13.123 29.9971L13.001 30H2.36426L2.24316 29.9971C1.03469 29.9348 0.0653104 28.9506 0.00390625 27.7236L0 27.5996V9.60059C1.90336e-07 8.27527 1.05901 7.20043 2.36426 7.2002H13.001ZM23.0469 0C24.6785 0.00011158 26.0009 1.3433 26.001 3V27C26.0009 28.6567 24.6785 29.9999 23.0469 30H16.5469C16.2205 30 15.9551 29.7308 15.9551 29.3994C15.9553 29.0682 16.2206 28.7998 16.5469 28.7998H23.0469C24.0258 28.7997 24.8192 27.9939 24.8193 27V3C24.8193 2.00604 24.0258 1.20031 23.0469 1.2002H13.5918C12.6128 1.20022 11.8194 2.00598 11.8193 3V6C11.8193 6.33124 11.5547 6.5994 11.2285 6.59961C10.9022 6.59961 10.6377 6.33137 10.6377 6V3C10.6378 1.34325 11.9601 2.44626e-05 13.5918 0H23.0469ZM2.36426 8.40039C1.71173 8.40062 1.18262 8.938 1.18262 9.60059V27.5996C1.18265 28.2622 1.71175 28.7996 2.36426 28.7998H4.1377V25.2002C4.1377 23.5435 5.46013 22.2004 7.0918 22.2002H8.86523C10.4966 22.2007 11.8193 23.5437 11.8193 25.2002V28.7998H13.001C13.6535 28.7995 14.1836 28.2621 14.1836 27.5996V9.60059C14.1836 8.93802 13.6535 8.40066 13.001 8.40039H2.36426ZM7.0918 23.4004C6.11285 23.4005 5.31934 24.2062 5.31934 25.2002V28.7998H10.6377V25.2002C10.6377 24.2064 9.84392 23.4009 8.86523 23.4004H7.0918ZM18.3193 24.5996H17.1377V22.2002H18.3193V24.5996ZM21.8652 24.5996H20.6836V22.2002H21.8652V24.5996ZM18.3193 20.4004H17.1377V18H18.3193V20.4004ZM21.8652 20.4004H20.6836V18H21.8652V20.4004ZM4.72852 18.5996H3.54688V16.2002H4.72852V18.5996ZM8.27441 18.5996H7.09277V16.2002H8.27441V18.5996ZM11.8193 18.5996H10.6377V16.2002H11.8193V18.5996ZM18.3193 16.2002H17.1377V13.7998H18.3193V16.2002ZM21.8652 16.2002H20.6836V13.7998H21.8652V16.2002ZM4.72852 14.4004H3.54688V12H4.72852V14.4004ZM8.27441 14.4004H7.09277V12H8.27441V14.4004ZM11.8193 14.4004H10.6377V12H11.8193V14.4004ZM18.3193 12H17.1377V9.60059H18.3193V12ZM21.8652 12H20.6836V9.60059H21.8652V12ZM18.3193 7.7998H17.1377V5.40039H18.3193V7.7998ZM21.8652 7.7998H20.6836V5.40039H21.8652V7.7998Z" fill="white"/>
</svg>
<span>법인회원 가입</span>
</a>
</div>
<!-- Navigation Links -->
<div class="signup-navigation">
<a th:href="@{/}" class="signup-nav-link">홈으로</a>
<span class="signup-nav-separator">|</span>
<a th:href="@{/login}" class="signup-nav-link">로그인 하기</a>
</div>
</div>
</div>
</section>
</div>
</th:block>
<th:block layout:fragment="script">
<script th:src="@{/js/jquery-3.7.1.min.js}"></script>
<script th:src="@{/js/daterangepicker.js}"></script>
@@ -70,11 +53,8 @@
</th:block>
<th:block layout:fragment="contentScript">
<script>
$(function () {
$('.mb_group1, .mb_group2').on('click', function () {
window.location.href = $(this).find('a').attr('href');
});
});
// Modern signup selection page - no custom JS needed
// Card interactions handled by CSS and standard anchor tags
</script>
</th:block>
</body>
@@ -5,53 +5,70 @@
layout:decorate="~{layout/base_layout}">
<body>
<th:block th:fragment="agreementContent(termsOfUse, privacyCollect)">
<div class="terms_box t_top">
<p>약관 동의</p>
<!-- Agreement Section Header -->
<div class="org-section-header org-section-header--agreement">
<h3>약관 동의</h3>
<span class="required-badge">필수 동의</span>
</div>
<form id="agreementForm">
<div class="total_wrap">
<span class="inp_check b_tit">
<input type='checkbox' value='selectall' id="agree_all"/>
<label for="agree_all">전체동의</label>
</span>
<div class="total_type">
<div class="suv_ck">
<ul>
<li>
<span class="inp_check s_tit">
<input type='checkbox' name='termsOfUse' id="termsOfUse" required/>
<label for="termsOfUse">
<span>[필수]</span>「API Portal」 서비스 이용약관
</label>
</span>
<a class="toggle-btn" data-target="termsOfUseContent"></a>
<div id="termsOfUseContent" class="txt_wrap scrollbar" style="display:none;">
<div th:utext="${termsOfUse?.contents ?: '이용약관 내용을 불러올 수 없습니다.'}"></div>
</div>
</li>
</ul>
<form id="agreementForm" class="agreement-form">
<!-- Agree All Checkbox -->
<div class="agreement-all-section">
<label class="agreement-checkbox-label">
<input type="checkbox" id="agree_all" class="agreement-checkbox-input">
<span class="agreement-checkbox-custom"></span>
<span class="agreement-checkbox-text agreement-all-text">전체동의</span>
</label>
</div>
<!-- Divider -->
<div class="agreement-divider"></div>
<!-- Individual Agreements -->
<div class="agreement-items-list">
<!-- Terms of Use -->
<div class="agreement-item-row">
<label class="agreement-checkbox-label">
<input type="checkbox" name="termsOfUse" id="termsOfUse" class="agreement-checkbox-input" required>
<span class="agreement-checkbox-custom"></span>
<span class="agreement-checkbox-text">
<span class="agreement-required">[필수]</span>
<span class="agreement-title">이용약관 동의</span>
</span>
</label>
<button type="button" class="agreement-toggle-icon" data-target="termsOfUseContent">
<i class="fas fa-chevron-down"></i>
</button>
</div>
<div id="termsOfUseContent" class="agreement-content" style="display:none;">
<div class="agreement-scroll">
<div th:utext="${termsOfUse?.contents ?: '이용약관 내용을 불러올 수 없습니다.'}"></div>
</div>
<!-- 개인정보수집동의서 추가 -->
<div class="suv_ck">
<ul>
<li>
<span class="inp_check s_tit">
<input type='checkbox' name='privacyCollect' id="privacyCollect" required/>
<th:block th:switch="${registrationType}">
<label for="privacyCollect" th:case="'personal'">
<span>[필수]</span> Kbank API Portal 회원가입을 위한 개인정보 수집•이용 동의서 (개인회원용)
</label>
<label for="privacyCollect" th:case="'corporate'">
<span>[필수]</span> Kbank API Portal 회원가입을 위한 개인정보 수집•이용 동의서 (법인회원용)
</label>
</th:block>
</span>
<a class="toggle-btn" data-target="privacyCollectContent"></a>
<div id="privacyCollectContent" class="txt_wrap scrollbar" style="display:none;">
<div th:utext="${privacyCollect?.contents ?: '개인정보수집동의서 내용을 불러올 수 없습니다.'}"></div>
</div>
</li>
</ul>
</div>
<!-- Privacy Policy -->
<div class="agreement-item-row">
<label class="agreement-checkbox-label">
<input type="checkbox" name="privacyCollect" id="privacyCollect" class="agreement-checkbox-input" required>
<span class="agreement-checkbox-custom"></span>
<th:block th:switch="${registrationType}">
<span class="agreement-checkbox-text" th:case="'personal'">
<span class="agreement-required">[필수]</span>
<span class="agreement-title">개인정보 수집 및 이용 동의</span>
</span>
<span class="agreement-checkbox-text" th:case="'corporate'">
<span class="agreement-required">[필수]</span>
<span class="agreement-title">개인정보 수집 및 이용 동의</span>
</span>
</th:block>
</label>
<button type="button" class="agreement-toggle-icon" data-target="privacyCollectContent">
<i class="fas fa-chevron-down"></i>
</button>
</div>
<div id="privacyCollectContent" class="agreement-content" style="display:none;">
<div class="agreement-scroll">
<div th:utext="${privacyCollect?.contents ?: '개인정보수집동의서 내용을 불러올 수 없습니다.'}"></div>
</div>
</div>
</div>
@@ -61,49 +78,56 @@
<script th:fragment="agreementScript">
document.addEventListener('DOMContentLoaded', function () {
const agreeAll = document.getElementById('agree_all');
const agreementChecks = document.querySelectorAll('.inp_check.s_tit input[type="checkbox"]');
const toggleBtns = document.querySelectorAll('.toggle-btn');
const agreementChecks = document.querySelectorAll('.agreement-checkbox-input[required]');
const toggleIcons = document.querySelectorAll('.agreement-toggle-icon');
// 전체 동의 체크박스 이벤트
agreeAll.addEventListener('change', function () {
agreementChecks.forEach(check => {
check.checked = this.checked;
if (agreeAll) {
agreeAll.addEventListener('change', function () {
agreementChecks.forEach(check => {
check.checked = this.checked;
});
});
});
}
// 개별 체크박스 이벤트
agreementChecks.forEach(check => {
check.addEventListener('change', function () {
agreeAll.checked = Array.from(agreementChecks).every(c => c.checked);
if (agreeAll) {
agreeAll.checked = Array.from(agreementChecks).every(c => c.checked);
}
});
});
// 토글 버튼 이벤트
toggleBtns.forEach(btn => {
btn.addEventListener('click', function (e) {
// 토글 아이콘 이벤트
toggleIcons.forEach(icon => {
icon.addEventListener('click', function (e) {
e.preventDefault();
const targetId = this.getAttribute('data-target');
const targetContent = document.getElementById(targetId);
const parentSection = this.closest('.suv_ck');
const parentRow = this.closest('.agreement-item-row');
// 현재 섹션이 이미 열려있는지 확인
const isCurrentlyOpen = targetContent.style.display === 'block';
document.querySelectorAll('.txt_wrap').forEach(content => {
// 모든 섹션 닫기
document.querySelectorAll('.agreement-content').forEach(content => {
content.style.display = 'none';
});
document.querySelectorAll('.toggle-btn').forEach(button => {
document.querySelectorAll('.agreement-toggle-icon').forEach(button => {
button.classList.remove('active');
});
document.querySelectorAll('.suv_ck').forEach(section => {
section.classList.remove('active');
document.querySelectorAll('.agreement-item-row').forEach(item => {
item.classList.remove('active');
});
// 현재 섹션이 닫혀있었다면 열기
if (!isCurrentlyOpen) {
targetContent.style.display = 'block';
this.classList.add('active');
parentSection.classList.add('active');
if (parentRow) {
parentRow.classList.add('active');
}
setTimeout(() => {
targetContent.scrollIntoView({
@@ -1,65 +1,76 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
<html lang="ko" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/kbank_base_layout}">
layout:decorate="~{layout/kjbank_title_layout}">
<body>
<section layout:fragment="contentFragment" class="content">
<script th:src="@{/js/htmx.min.js}"></script>
<div class="content_wrap">
<div class="sub_title2">
<h2 class="title add" th:if="${!isInvited}">개인회원가입</h2>
<h2 class="title add" th:if="${isInvited}">법인회원가입</h2>
</div>
<div class="inner i_cs h_inner2">
<div class="top_info" th:if="${!isInvited}">
<ul>
<li>서비스 또는 API 사용을 원하실 경우 법인회원 승인 후 이용하실 수 있습니다.</li>
</ul>
</div>
<!-- 약관동의 섹션 -->
<input type="hidden" id="csrf" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<th:block th:replace="~{apps/register/userAgreementContent :: agreementContent(${termsOfUse}, ${privacyCollect})}"></th:block>
<!-- 기본 정보 입력 섹션 -->
<form name="portalUser" id="registerForm" role="form" th:action="@{/signup/portalUser}"
th:object="${portalUser}" method="post">
<!-- 법인 정보 섹션, 초대받은 경우에만 -->
<div class="terms_box" th:if="${isInvited}">
<p>법인 정보</p>
</div>
<section layout:fragment="title">
<div class="page-title-banner">
<img th:src="@{/img/user_register_title.png}" alt="개인회원가입" class="title-image">
<h1 th:text="${!isInvited ? '개인회원가입' : '법인회원가입'}">개인회원가입</h1>
</div>
</section>
<div class="form_type" th:if="${isInvited}">
<div class="info1">
<p class="title w_tit">
<span>법인명</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp4">
<input type="text" name="companyName" class="common_input_type_1"
th:classappend="${isInvited ? 'input-readonly' : ''}" th:value="${orgName}" readonly>
</div>
</div>
</div>
</div>
<!-- end 법인 정보 섹션, 초대받은 경우에만 -->
<div class="terms_box">
<p>기본 정보</p>
<span class="dot">필수 입력란입니다</span>
</div>
<div class="form_type" th:if="${registrationType == isInvited ? 'corporate' :'personal'}">
<th:block th:replace="~{apps/register/components/commonUserInfoForm :: commonUserInfo}"></th:block>
<th:block th:replace="~{apps/register/components/newUserInfoForm :: newUserForm}"></th:block>
</div>
<div class="form_type">
<div class="btn_application">
<a type="button" class="common_btn_type_1 gray" id="cancelButton"><span>취소</span></a>
<div class="btn_gap"></div>
<a type="button" class="common_btn_type_1 btn_register"><span>가입신청</span></a>
</div>
</div>
</form>
<section layout:fragment="contentFragment">
<div class="org-register-page">
<div class="org-register-container">
<!-- Hidden Fields -->
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<input type="hidden" name="registrationType" th:value="${registrationType}"/>
<!-- Alert Container -->
<div id="alertContainer"></div>
<!-- Info Notice -->
<div class="org-info-notice" th:if="${!isInvited}">
<ul>
<li>서비스 또는 API 사용을 원하실 경우 법인회원 승인 후 이용하실 수 있습니다.</li>
</ul>
</div>
<!-- Agreement Section -->
<th:block th:replace="~{apps/register/userAgreementContent :: agreementContent(${termsOfUse}, ${privacyCollect})}"></th:block>
<form name="portalUser" id="registerForm" role="form" th:action="@{/signup/portalUser}"
th:object="${portalUser}" method="post">
<!-- Corporate Information Section (invited users only) -->
<div th:if="${isInvited}">
<div class="org-section-header org-section-header--agreement">
<h3>법인 정보</h3>
</div>
<div class="org-form-group">
<label class="org-form-label">법인명</label>
<div class="org-form-input-wrapper">
<input type="text" name="companyName" class="org-form-input" th:value="${orgName}" readonly>
</div>
</div>
</div>
<!-- Basic Information Section -->
<div class="org-section-header org-section-header--agreement" th:style="${isInvited ? 'margin-top: 48px;' : ''}">
<h3>기본 정보</h3>
<span class="required-badge">필수 입력</span>
</div>
<div th:if="${registrationType == (isInvited ? 'corporate' : 'personal')}">
<th:block th:replace="~{apps/register/components/commonUserInfoForm :: commonUserInfo}"></th:block>
<th:block th:replace="~{apps/register/components/newUserInfoForm :: newUserForm}"></th:block>
</div>
<!-- Action Buttons -->
<div class="org-action-buttons">
<button type="button" class="btn btn-secondary" id="cancelButton">취소</button>
<button type="button" class="btn btn-primary btn_register">가입신청</button>
</div>
</form>
<!-- Loading Overlay -->
<div class="org-loading-overlay" id="loadingOverlay">
<div class="spinner"></div>
</div>
</div>
</div>
</section>
<th:block layout:fragment="contentScript">
<script th:if="${error}" th:inline="javascript">
@@ -70,128 +81,119 @@
<script th:replace="~{apps/register/userAgreementContent :: agreementScript}"></script>
<script th:replace="~{apps/register/components/commonUserInfoForm :: commonUserInfoScript}"></script>
<script th:replace="~{apps/register/components/newUserInfoForm :: newUserScript}"></script>
<script>
let form = document.getElementById('registerForm');
let agreementForm = document.getElementById('agreementForm');
let btn_register = document.querySelector('.btn_register');
let isAuthVerified = false;
<script th:inline="javascript">
document.addEventListener('DOMContentLoaded', function () {
let form = document.getElementById('registerForm');
let agreementForm = document.getElementById('agreementForm');
let btn_register = document.querySelector('.btn_register');
let isAuthVerified = false;
// 인증 완료 이벤트 리스너
document.addEventListener('authVerified', function() {
isAuthVerified = true;
});
// 인증 완료 이벤트 리스너
document.addEventListener('authVerified', function() {
isAuthVerified = true;
});
// 이메일 유효성 검사 함수
function isValidEmail(email) {
const safeEmailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
return safeEmailRegex.test(email);
}
// 폼 유효성 검사 함수
function checkFormValidity() {
return isValidEmail($('#loginId').val()) &&
($('#password').val() === $('#password2').val()) &&
($('#userName').val().trim() !== '') &&
$('#termsOfUse').prop('checked') &&
$('#privacyCollect').prop('checked') &&
(combineMobileNumber('#mobileNumber') !== null) &&
isAuthVerified; // 인증 여부 확인
}
let isSubmitting = false;
// 등록 버튼 클릭 이벤트
btn_register.addEventListener('click', function(event) {
event.preventDefault();
event.stopPropagation();
// Prevent resubmission
if (isSubmitting) {
console.log('Form is already being submitted...');
return;
// 이메일 유효성 검사 함수
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
isSubmitting = true;
btn_register.disabled = true;
// 폼 유효성 검사 함수
function checkFormValidity() {
const loginIdElement = $('#loginId');
const userNameElement = $('#userName');
const termsOfUseElement = $('#termsOfUse');
const privacyCollectElement = $('#privacyCollect');
const mobileNumberElement = $('#mobileNumber');
const isPasswordValid = $('#isPasswordValid').val() === 'true';
const isPasswordMatch = $('#isPasswordMatch').val() === 'true';
form.classList.add('was-validated');
agreementForm.classList.add('was-validated');
return isValidEmail(loginIdElement.val()) &&
isPasswordValid &&
isPasswordMatch &&
userNameElement.val().trim() !== '' &&
termsOfUseElement.prop('checked') &&
privacyCollectElement.prop('checked') &&
mobileNumberElement.val().trim() !== '' &&
isAuthVerified;
}
if (checkFormValidity() && form.checkValidity() && agreementForm.checkValidity()) {
// 약관 동의 폼의 데이터를 hidden input으로 추가
let agreementFormData = new FormData(agreementForm);
for (let pair of agreementFormData.entries()) {
if (!form.querySelector('input[name="' + pair[0] + '"]')) {
let input = document.createElement('input');
input.type = 'hidden';
input.name = pair[0];
input.value = pair[1];
form.appendChild(input);
}
let isSubmitting = false;
// 등록 버튼 클릭 이벤트
btn_register.addEventListener('click', function(event) {
event.preventDefault();
event.stopPropagation();
// Prevent resubmission
if (isSubmitting) {
console.log('Form is already being submitted...');
return;
}
// 불필요한 hidden input 제거
const unnecessaryFields = ['isPasswordValid', 'isPasswordMatch'];
unnecessaryFields.forEach(fieldName => {
const field = form.querySelector(`input[name="${fieldName}"]`);
if (field) {
field.remove();
isSubmitting = true;
btn_register.disabled = true;
// Show loading overlay
document.getElementById('loadingOverlay').classList.add('active');
try {
form.classList.add('was-validated');
if (agreementForm) {
agreementForm.classList.add('was-validated');
}
});
// authCompletedYn 값 설정
const authCompletedField = document.createElement('input');
authCompletedField.type = 'hidden';
authCompletedField.name = 'authCompletedYn';
authCompletedField.value = isAuthVerified ? 'Y' : 'N';
form.appendChild(authCompletedField);
if (checkFormValidity() && form.checkValidity() && (agreementForm ? agreementForm.checkValidity() : true)) {
// 약관 동의 폼의 데이터를 hidden input으로 추가
if (agreementForm) {
let agreementFormData = new FormData(agreementForm);
for (let pair of agreementFormData.entries()) {
if (!form.querySelector('input[name="' + pair[0] + '"]')) {
let input = document.createElement('input');
input.type = 'hidden';
input.name = pair[0];
input.value = pair[1];
form.appendChild(input);
}
}
}
form.submit();
} else {
customPopups.showAlert('모든 필수 항목을 입력하고 약관에 동의해주세요.');
isSubmitting = false;
btn_register.disabled = false;
}
});
// 불필요한 hidden input 제거
const unnecessaryFields = ['isPasswordValid', 'isPasswordMatch'];
unnecessaryFields.forEach(fieldName => {
const field = form.querySelector(`input[name="${fieldName}"]`);
if (field) {
field.remove();
}
});
// 취소버튼 클릭시
document.getElementById('cancelButton').addEventListener('click', function() {
location.href = '/login';
});
// authCompletedYn 값 설정
const authCompletedField = document.createElement('input');
authCompletedField.type = 'hidden';
authCompletedField.name = 'authCompletedYn';
authCompletedField.value = isAuthVerified ? 'Y' : 'N';
form.appendChild(authCompletedField);
// HTMX 이벤트 리스너 - 모든 HTMX 응답을 중앙에서 처리
document.body.addEventListener('htmx:afterRequest', function(event) {
const targetId = event.detail.target.id;
let response;
try {
response = JSON.parse(event.detail.xhr.response);
} catch (e) {
console.error('응답 JSON 파싱 실패:', e);
customPopups.showAlert('서버 응답을 처리할 수 없습니다.');
return;
}
// 커스텀 헤더를 체크하여 등록 검증 요청 처리
if (event.detail.xhr.getResponseHeader('X-Custom-Handler') === 'registration-validation') {
if (response && response.message) {
if (response.status === 'SUCCESS') {
customPopups.showSuccess(response.message);
form.submit();
} else {
customPopups.showAlert(response.message);
customPopups.showAlert('모든 필수 항목을 입력하고 약관에 동의해주세요.');
isSubmitting = false;
btn_register.disabled = false;
document.getElementById('loadingOverlay').classList.remove('active');
}
} catch (error) {
console.error('Form submission error:', error);
isSubmitting = false;
btn_register.disabled = false;
document.getElementById('loadingOverlay').classList.remove('active');
}
return;
}
});
// 응답 처리 로직
if (targetId === 'email-validation') {
// commonUserInfoForm의 함수 호출
handleEmailValidationResponse(response);
} else if (targetId === 'password-validation' || targetId === 'password-match-validation') {
// 비밀번호 검증은 컴포넌트에서 처리하도록 함
return;
}
// 취소 버튼 클릭 시
document.getElementById('cancelButton').addEventListener('click', function() {
location.href = '/login';
});
});
</script>
</th:block>
@@ -1,108 +1,114 @@
<!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}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
<body>
<section class="content" layout:fragment="contentFragment">
<div class="content_wrap">
<div class="sub_title3">
<h2 class="title">이용자 정보</h2>
<th:block layout:fragment="contentFragment">
<!-- Header -->
<div class="register-header">
<h1>이용자 상세</h1>
<p class="header-description">
이용자의 상세 정보를 확인할 수 있습니다.
</p>
</div>
<div class="inner i_cs h_inner8 h_inner12">
<section class="user-detail-container">
<!-- 기본 정보 -->
<div class="terms_box user_top">
<p>기본정보</p>
</div>
<!-- User Information -->
<div class="detail-section">
<h2 class="section-title">기본 정보</h2>
<div class="detail-grid">
<div class="form_type">
<!-- Email -->
<div class="detail-item full-width">
<label class="detail-label">
<i class="fas fa-envelope"></i> 이메일 아이디
</label>
<div class="detail-value" th:text="${user.maskedEmailAddr}">
user@example.com
</div>
</div>
<!-- Name -->
<div class="detail-item">
<label class="detail-label">
<i class="fas fa-user"></i> 성명
</label>
<div class="detail-value" th:text="${user.maskedUserName}">
홍길동
</div>
</div>
<!-- Mobile -->
<div class="detail-item">
<label class="detail-label">
<i class="fas fa-mobile-alt"></i> 휴대폰 번호
</label>
<div class="detail-value" th:text="${user.maskedMobileNumber}">
010-****-****
</div>
</div>
<!-- Role -->
<div class="detail-item">
<label class="detail-label">
<i class="fas fa-shield-alt"></i> 권한
</label>
<div class="detail-value">
<span class="user-role-badge" th:if="${user.roleCode != null}">
<span th:text="#{${user.roleCode}}">권한</span>
</span>
</div>
</div>
<!-- Status -->
<div class="detail-item">
<label class="detail-label">
<i class="fas fa-info-circle"></i> 계정상태
</label>
<div class="detail-value">
<span class="status-indicator"
th:classappend="${user.userStatus.toString() == 'ACTIVE'} ? 'status-active' : 'status-inactive'">
<span class="status-dot"></span>
<span th:text="${user.userStatus.getDescription()}">활성화</span>
</span>
</div>
</div>
<!-- Created Date -->
<div class="detail-item">
<label class="detail-label">
<i class="fas fa-calendar-plus"></i> 가입일
</label>
<div class="detail-value" th:text="${#temporals.format(user.createdDate, 'yyyy.MM.dd')}">
2024.01.01
</div>
</div>
<!-- Last Login -->
<div class="detail-item">
<label class="detail-label">
<i class="fas fa-clock"></i> 마지막 로그인
</label>
<div class="detail-value" th:text="${#temporals.format(user.lastLoginDate, 'yyyy.MM.dd HH:mm:ss')}">
2024.01.01 12:00:00
</div>
</div>
<div class="info1">
<p class="title">
<span>이메일 아이디</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp4">
<input type="text" class="common_input_type_1" th:value="${#strings.substringBefore(user.maskedEmailAddr, '@')}" disabled="disabled">
<span class="special_t">@</span>
<input type="text" class="common_input_type_1" th:value="${#strings.substringAfter(user.maskedEmailAddr, '@')}" disabled="disabled">
</div>
</div>
</div>
<div class="info1 info_mypage">
<p class="title">
<span>성명</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp4">
<input type="text" th:value="${user.maskedUserName}" name="text" class="common_input_type_1" disabled="disabled" style="background:#F7F9FD;">
</div>
</div>
<!-- Action Buttons -->
<div class="detail-actions">
<a th:href="@{/users}" class="btn btn-secondary">
<i class="fas fa-list"></i> 목록으로
</a>
</div>
<div class="info1 info_mypage">
<p class="title">
<span>휴대폰 번호</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp4">
<input type="text" th:value="${user.maskedMobileNumber}" name="text" class="common_input_type_1" disabled="disabled" style="background:#F7F9FD;">
</div>
</div>
</div>
</section>
<div class="info1 info_mypage">
<p class="title">
<span>권한</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp4">
<input type="text" th:value="#{${user.roleCode}}" name="text" class="common_input_type_1" disabled="disabled" style="background:#F7F9FD;">
</div>
</div>
</div>
<div class="info1 info_mypage">
<p class="title">
<span>계정상태</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp4">
<input type="text" th:value="${user.userStatus.getDescription()}" name="text" class="common_input_type_1" disabled="disabled" style="background:#F7F9FD;">
</div>
</div>
</div>
<div class="info1 info_mypage">
<p class="title">
<span>가입일</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp4">
<input type="text" th:value="${#temporals.format(user.createdDate, 'yyyy.MM.dd')}" name="text" class="common_input_type_1" disabled="disabled" style="background:#F7F9FD;">
</div>
</div>
</div>
<div class="info1 info_mypage">
<p class="title">
<span>마지막 로그인</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp4">
<input type="text" th:value="${#temporals.format(user.lastLoginDate, 'yyyy.MM.dd HH:mm:ss')}" name="text" class="common_input_type_1" disabled="disabled" style="background:#F7F9FD;">
</div>
</div>
</div>
</div>
<div class="btn_inventory btn_mtop">
<a th:href="@{/users}" class="common_btn_type_1">목록</a>
</div>
<!-- // 기본 정보 -->
</div>
</div>
</section>
</th:block>
</body>
@@ -1,161 +1,159 @@
<!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}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
<body>
<section class="content" layout:fragment="contentFragment">
<div class="content_wrap">
<div class="sub_title3">
<h2 class="title">이용자 목록</h2>
<th:block layout:fragment="contentFragment">
<section class="user-management-container">
<!-- Header Section -->
<div class="user-management-header">
<div class="user-management-title">
<h1>사용자 관리</h1>
<h2>이용자 목록</h2>
</div>
<div class="user-management-actions">
<button type="button" class="btn btn-primary" id="addUserBtn">
<i class="fas fa-plus"></i>
<span>이용자 추가</span>
</button>
</div>
</div>
<div class="inner i_cs h_inner8 h_inner12">
<!-- Table Controls -->
<div class="table-controls">
<div class="total-count">
<strong th:text="${(users != null ? users.size() : 0) + (pendingUsers != null ? pendingUsers.size() : 0)}">0</strong>
</div>
</div>
<div class="tb_board_list">
<div class="table_box">
<table class="table table_min bdt2" cellspacing="0" cellpadding="0" width="100%">
<colgroup>
<col width="60px">
<col width="86px">
<col width="210px">
<col width="80px">
<col width="180px">
<col>
</colgroup>
<thead>
<tr>
<th>No</th>
<th>성명</th>
<th>이메일 아이디</th>
<th>권한</th>
<th>계정상태</th>
<th>설정</th>
</tr>
</thead>
<tbody>
<tr th:each="user, status : ${users}">
<td th:text="${status.index + 1}">No</td>
<td>
<a th:href="@{/users/detail(id=${user.id})}" th:text="${user.maskedUserName}">성명</a>
</td>
<td th:text="${user.maskedEmailAddr}">이메일 아이디</td>
<td>
<span th:if="${user.roleCode != null}" th:text="#{${user.roleCode}}">권한</span>
</td>
<td th:text="${user.userStatus.getDescription()}">계정상태</td>
<td>
<div th:if="${user.id == currentUserId}">
현재 사용자
</div>
<div class="btn_wrap btn_set"
th:if="${user.roleCode != null and user.id != currentUserId}">
<a href="#" class="common_btn_type_4 inactivate_user"
th:if="${user.userStatus.toString() == 'ACTIVE'}"><span>비활성화</span></a>
<a href="#" class="common_btn_type_4 activate_user"
th:if="${user.userStatus.toString() != 'ACTIVE'}"><span>활성화</span></a>
<a href="#" class="common_btn_type_4 cancel_invitation"
th:if="${user.userStatus.toString() == 'PENDING'}"><span>초대취소</span></a>
<a href="#" class="common_btn_type_4 assign_manager"
th:if="${user.userStatus.toString() == 'ACTIVE' && user.roleCode.toString() == 'ROLE_CORP_USER'}"><span>관리자 권한부여</span></a>
</div>
</td>
</tr>
<tr th:each="user : ${pendingUsers}">
<td th:data-id="${user.id}"></td>
<td>
</td>
<td th:text="${user.maskedEmailAddr}">이메일 아이디</td>
<td>
</td>
<td>초대 대기 중</td>
<td>
<div class="btn_wrap btn_set">
<a href="#none" class="common_btn_type_4 resend_invitation"><span>재발송</span></a>
<a href="#none" class="common_btn_type_4 cancel_invitation"><span>초대취소</span></a>
</div>
</td>
</tr>
</tbody>
</table>
<!-- User Table -->
<div class="user-list-wrapper" th:if="${(users != null and !users.isEmpty()) or (pendingUsers != null and !pendingUsers.isEmpty())}">
<div class="user-table">
<!-- Table Header -->
<div class="table-header">
<div class="col-num">NO</div>
<div class="col-name">성명</div>
<div class="col-email">이메일 아이디</div>
<div class="col-role">권한</div>
<div class="col-status">계정상태</div>
<div class="col-actions">설정</div>
</div>
</div>
<!-- Active/Inactive Users -->
<div class="table-row" th:each="user, status : ${users}">
<div class="col-num" th:text="${status.index + 1}">1</div>
<div class="col-name">
<a th:href="@{/users/detail(id=${user.id})}" th:text="${user.maskedUserName}">
사용자 이름
</a>
</div>
<div class="col-email" th:text="${user.maskedEmailAddr}">
user@example.com
</div>
<div class="col-role">
<span class="user-role-badge" th:if="${user.roleCode != null}">
<i class="fas fa-shield-alt"></i>
<span th:text="#{${user.roleCode}}">권한</span>
</span>
</div>
<div class="col-status">
<span class="user-status-badge"
th:classappend="${user.userStatus.toString() == 'ACTIVE'} ? 'status-active' : (${user.userStatus.toString() == 'PENDING'} ? 'status-pending' : 'status-inactive')"
th:text="${user.userStatus.getDescription()}">
활성
</span>
</div>
<div class="col-actions">
<!-- Current User -->
<div class="user-current-badge" th:if="${user.id == currentUserId}">
<i class="fas fa-user-check"></i> 현재 사용자
</div>
<div class="btn_wrap bt_location">
<button type="button" class="btn_add">이용자 추가</button>
<!-- Action Buttons -->
<div class="user-action-buttons" th:if="${user.roleCode != null and user.id != currentUserId}">
<button type="button" class="btn btn-sm btn-secondary inactivate_user"
th:if="${user.userStatus.toString() == 'ACTIVE'}"
th:data-user-id="${user.id}"
th:data-user-name="${user.maskedUserName}">
비활성화
</button>
<button type="button" class="btn btn-sm btn-primary activate_user"
th:if="${user.userStatus.toString() != 'ACTIVE'}"
th:data-user-id="${user.id}"
th:data-user-name="${user.maskedUserName}">
활성화
</button>
<button type="button" class="btn btn-sm btn-secondary cancel_invitation"
th:if="${user.userStatus.toString() == 'PENDING'}"
th:data-user-id="${user.id}"
th:data-user-email="${user.maskedEmailAddr}">
초대취소
</button>
<button type="button" class="btn btn-sm btn-warning assign_manager"
th:if="${user.userStatus.toString() == 'ACTIVE' && user.roleCode.toString() == 'ROLE_CORP_USER'}"
th:data-user-id="${user.id}"
th:data-user-name="${user.maskedUserName}"
th:data-user-email="${user.maskedEmailAddr}">
관리자 권한
</button>
</div>
</div>
</div>
<!-- Pending Invitation Users -->
<div class="table-row row-pending" th:each="user : ${pendingUsers}">
<div class="col-num"></div>
<div class="col-name">
<span class="pending-label">
<i class="fas fa-envelope"></i> 초대 대기중
</span>
</div>
<div class="col-email" th:text="${user.maskedEmailAddr}">
pending@example.com
</div>
<div class="col-role">-</div>
<div class="col-status">
<span class="user-status-badge status-pending">
초대중
</span>
</div>
<div class="col-actions">
<div class="user-action-buttons">
<button type="button" class="btn btn-sm btn-secondary cancel_invitation"
th:data-user-id="${user.id}"
th:data-user-email="${user.maskedEmailAddr}">
초대취소
</button>
</div>
</div>
</div>
</div>
<div class="pagination page_top"
th:replace="~{fragment/pagination :: pagination(jsFunction='fn_select_page')}"></div>
</div>
</div>
</section>
<!-- Empty State -->
<div class="user-empty-state" th:if="${(users == null or users.isEmpty()) and (pendingUsers == null or pendingUsers.isEmpty())}">
<div class="empty-icon">👥</div>
<h3>등록된 이용자가 없습니다</h3>
<p>새로운 이용자를 추가하여 시작하세요.</p>
<button type="button" class="btn btn-primary" id="addUserBtnEmpty">
<i class="fas fa-plus"></i>
<span>첫 이용자 추가하기</span>
</button>
</div>
<!-- Pagination -->
<div class="pagination page_top"
th:replace="~{fragment/pagination :: pagination(jsFunction='fn_select_page')}"></div>
</section>
</th:block>
<section layout:fragment="pagePopups" class="content pop">
<div class="popup_wrap pop01" id="user_register" style="display: none; z-index: 1000">
<div class="pop_dim">
<div class="inner">
<div class="pop_body">
<div class="box-txt">
<div class="inner_txt">
<div class="title">
<h4>이용자 추가</h4>
</div>
</div>
<div class="contents">
<p class="con_txt2">추가할 이용자 e-mail 주소를 입력해 주세요.</p>
<div class="inp_box">
<input type="email" class="common_input_type_1 w_input" id="new_user_email"
placeholder="이용자 e-mail 주소"
pattern="[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}"
title="올바른 이메일 형식을 입력해주세요 (예: user@example.com)">
</div>
</div>
</div>
<div class="pop_btn p_btn">
<a href="#" class="common_btn_type_1 gray btn_cancel"><span>취소</span></a>
<a href="#" class="common_btn_type_1 blue btn_register"><span>확인</span></a>
</div>
<button class="btn_close" style="margin-top:0">
<img th:src="@{/img/icon/icon_close.png}">
</button>
</div>
</div>
</div>
</div>
<div class="popup_wrap pop01" id="change_role" style="display: none; z-index: 1000">
<div class="pop_dim">
<div class="inner inner2">
<div class="pop_body">
<div class="box-txt">
<div class="inner_txt">
<div class="title">
<h4>권한변경</h4>
</div>
</div>
<div class="contents">
<p class="con_txt">
<span class="txt_bold" id="change_role_name"></span><span class="txt_mail"
id="change_role_email"></span>
에게<br>
법인 관리자 권한을 부여 하시겠습니까?
</p>
<div class="con_info_box">
<p>법인 관리자는 1개의 기관에 1명만 지정 가능 합니다.</p>
<p>본 계정의 관리자 권한은 해제 됩니다.</p>
</div>
</div>
</div>
<div class="pop_btn">
<a href="#" class="common_btn_type_1 gray btn_cancel_role"><span>취소</span></a>
<a href="#" class="common_btn_type_1 blue btn_confirm_role"><span>확인</span></a>
</div>
<button class="btn_close" style="margin-top:0">
<img th:src="@{/img/icon/icon_close.png}" alt="">
</button>
</div>
</div>
</div>
</div>
<!-- User Invite Modal -->
<div th:replace="~{fragment/popup/userInvitePopup :: userInvitePopup}"></div>
<!-- Change Role Modal -->
<div th:replace="~{fragment/popup/changeRolePopup :: changeRolePopup}"></div>
</section>
</body>
@@ -168,141 +166,84 @@
});
document.addEventListener('DOMContentLoaded', function () {
// Function to open the popup
function openPopup() {
document.getElementById('user_register').style.display = '';
}
// Function to close the popup
function closePopup() {
document.getElementById('user_register').style.display = 'none';
}
// Add event listener to the "이용자 추가" button to open the popup
const addUserBtn = document.querySelector('.btn_add');
if (addUserBtn) {
addUserBtn.addEventListener('click', openPopup);
}
// Add event listener to the close button
const closeBtn = document.querySelector('button.btn_close');
if (closeBtn) {
closeBtn.addEventListener('click', closePopup);
}
// Add event listener to the cancel button
const cancelBtn = document.querySelector('.btn_cancel');
if (cancelBtn) {
cancelBtn.addEventListener('click', closePopup);
}
// Add event listener to the confirm button
const confirmBtn = document.querySelector('.btn_register');
if (confirmBtn) {
confirmBtn.addEventListener('click', sendInvitation);
}
function sendInvitation() {
const emailInput = document.getElementById('new_user_email');
const email = emailInput.value.trim();
if (!email) {
customPopups.showAlert('이메일 주소를 입력해주세요.');
return;
}
// 이메일 형식 검증
const emailPattern = /^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/;
if (!emailPattern.test(email)) {
customPopups.showAlert('올바른 이메일 형식을 입력해주세요. (예: user@example.com)');
return;
}
$.ajax({
url: '/users/invite',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({emailAddr: email})
}).done(function (response) {
closePopup();
emailInput.value = '';
customPopups.showAlert(response.msg, function () {
location.reload(); // Reload the page to reflect changes
});
}).fail(function (jqXHR, textStatus, errorThrown) {
let errorMessage;
try {
// Parse the error response body
const errorResponse = JSON.parse(jqXHR.responseText);
errorMessage = errorResponse.msg || '이용자 초대 중 오류가 발생했습니다';
} catch (e) {
// Fallback if response isn't valid JSON
errorMessage = '이용자 초대 중 오류가 발생했습니다: ' + errorThrown;
// Function to open the user invite popup
function openUserInvitePopup() {
customPopups.showUserInvite({
onConfirm: function(email) {
$.ajax({
url: '/users/invite',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({emailAddr: email})
}).done(function (response) {
customPopups.hideUserInvite();
customPopups.showAlert(response.msg, function () {
location.reload();
});
}).fail(function (jqXHR, textStatus, errorThrown) {
let errorMessage;
try {
const errorResponse = JSON.parse(jqXHR.responseText);
errorMessage = errorResponse.msg || '이용자 초대 중 오류가 발생했습니다';
} catch (e) {
errorMessage = '이용자 초대 중 오류가 발생했습니다: ' + errorThrown;
}
customPopups.showUserInviteError(errorMessage);
});
},
onCancel: function() {
// Optional: handle cancel action
}
customPopups.showAlert(errorMessage);
});
}
// Optional: Close the popup when clicking outside of it
window.addEventListener('click', function (event) {
const popup = document.getElementById('user_register');
if (event.target === popup) {
closePopup();
}
});
// Add event listeners to the "이용자 추가" buttons
const addUserBtn = document.getElementById('addUserBtn');
const addUserBtnEmpty = document.getElementById('addUserBtnEmpty');
if (addUserBtn) {
addUserBtn.addEventListener('click', openUserInvitePopup);
}
if (addUserBtnEmpty) {
addUserBtnEmpty.addEventListener('click', openUserInvitePopup);
}
// Change role button handlers
const adminPrivilegeButtons = document.querySelectorAll('.assign_manager');
adminPrivilegeButtons.forEach(button => {
button.addEventListener('click', openChangeRolePopup);
});
button.addEventListener('click', function(event) {
const button = event.currentTarget;
const userId = button.dataset.userId;
const userName = button.dataset.userName;
const userEmail = button.dataset.userEmail;
function openChangeRolePopup(event) {
const row = event.target.closest('tr');
const name = row.querySelector('td:nth-child(2)').textContent;
const email = row.querySelector('td:nth-child(3)').textContent;
const userId = row.querySelector('a').getAttribute('href').split('=')[1];
document.getElementById('change_role_name').textContent = name;
document.getElementById('change_role_email').textContent = `(${email})`;
document.getElementById('change_role').style.display = 'block';
// Store userId for later use
document.getElementById('change_role').dataset.userId = userId;
}
function closeChangeRolePopup() {
document.getElementById('change_role').style.display = 'none';
}
document.querySelector('#change_role .btn_close').addEventListener('click', closeChangeRolePopup);
document.querySelector('#change_role .btn_cancel_role').addEventListener('click', closeChangeRolePopup);
document.querySelector('#change_role .btn_confirm_role').addEventListener('click', confirmRoleChange);
function confirmRoleChange() {
const userId = document.getElementById('change_role').dataset.userId;
$.ajax({
url: '/users/change-role',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({id: userId})
}).done(function (response) {
closeChangeRolePopup();
customPopups.showAlert(response.msg, function () {
window.location.href = '/actionLogout.do';
customPopups.showChangeRole({
userName: userName,
userEmail: userEmail,
userId: userId,
onConfirm: function(userId) {
$.ajax({
url: '/users/change-role',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({id: userId})
}).done(function (response) {
customPopups.hideChangeRole();
customPopups.showAlert(response.msg, function () {
window.location.href = '/actionLogout.do';
});
}).fail(function (jqXHR, textStatus, errorThrown) {
customPopups.hideChangeRole();
customPopups.showAlert('권한 변경 중 오류가 발생했습니다: ' + errorThrown);
});
}
});
}).fail(function (jqXHR, textStatus, errorThrown) {
customPopups.showAlert('권한 변경 중 오류가 발생했습니다: ' + errorThrown);
});
}
});
const activateButtons = document.querySelectorAll('.activate_user');
const inactivateButtons = document.querySelectorAll('.inactivate_user');
const cancelInvitationButtons = document.querySelectorAll('.cancel_invitation');
const resendInvitationButtons = document.querySelectorAll('.resend_invitation');
activateButtons.forEach(button => {
button.addEventListener('click', handleUserStatusChange);
@@ -316,22 +257,15 @@
button.addEventListener('click', handleCancelInvitation);
});
resendInvitationButtons.forEach(button => {
button.addEventListener('click', handleResendInvitation);
});
function handleUserStatusChange(event) {
const row = event.target.closest('tr');
const userId = row.querySelector('a').getAttribute('href').split('=')[1];
const userName = row.querySelector('td:nth-child(2)').textContent;
console.log(event.target.classList);
const isActivating = event.target.classList.contains('activate_user');
console.log({isActivating});
const button = event.currentTarget;
const userId = button.dataset.userId;
const userName = button.dataset.userName;
const isActivating = button.classList.contains('activate_user');
const actionText = isActivating ? '활성화' : '비활성화';
customPopups.showConfirm(`${userName} 사용자를 ${actionText}하시겠습니까?`, function (selection) {
if (selection) {
console.log({isActivating});
$.ajax({
url: `/users/${isActivating ? 'activate' : 'inactivate'}`,
type: 'POST',
@@ -351,12 +285,11 @@
}
function handleCancelInvitation(event) {
const row = event.target.closest('tr');
const userId = row.querySelector('td[data-id]').getAttribute('data-id');
console.log(userId);
const userName = row.querySelector('td:nth-child(3)').textContent;
const button = event.currentTarget;
const userId = button.dataset.userId;
const userEmail = button.dataset.userEmail;
customPopups.showConfirm(`${userName}에 대한 초대를 취소하시겠습니까?`, function (selection) {
customPopups.showConfirm(`${userEmail}에 대한 초대를 취소하시겠습니까?`, function (selection) {
if (selection) {
$.ajax({
url: '/users/cancel-invitation',
@@ -376,38 +309,6 @@
});
}
function handleResendInvitation(event) {
const row = event.target.closest('tr');
const userId = row.querySelector('td[data-id]').getAttribute('data-id');
const userName = row.querySelector('td:nth-child(3)').textContent;
customPopups.showConfirm(`${userName}에게 초대를 재발송하시겠습니까?`, function (selection) {
if (selection) {
$.ajax({
url: '/users/resend-invitation',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({id: userId})
}).done(function (response) {
customPopups.showAlert(response.msg, function () {
location.reload(); // Reload the page to reflect changes
});
}).fail(function (jqXHR, textStatus, errorThrown) {
let errorMessage;
try {
const errorResponse = JSON.parse(jqXHR.responseText);
errorMessage = errorResponse.msg || '초대 재발송 중 오류가 발생했습니다';
} catch (e) {
errorMessage = '초대 재발송 중 오류가 발생했습니다: ' + errorThrown;
}
customPopups.showAlert(errorMessage);
}).always(function () {
$('#customConfirm').hide();
});
}
});
}
});
</script>
</th:block>