앱 관리 - 앱목록 디자인 반영

This commit is contained in:
현성필
2025-11-25 13:47:52 +09:00
parent b05819e90d
commit b6141512b6
13 changed files with 385 additions and 210 deletions
+7 -7
View File
@@ -10628,7 +10628,7 @@ select.form-control {
} }
} }
.org-page-title-banner { .page-title-banner {
position: relative; position: relative;
width: 100%; width: 100%;
height: 220px; height: 220px;
@@ -10638,7 +10638,7 @@ select.form-control {
justify-content: center; justify-content: center;
overflow: hidden; overflow: hidden;
} }
.org-page-title-banner::before { .page-title-banner::before {
content: ""; content: "";
position: absolute; position: absolute;
top: 0; top: 0;
@@ -10649,7 +10649,7 @@ select.form-control {
background-size: 50px 50px; background-size: 50px 50px;
opacity: 0.5; opacity: 0.5;
} }
.org-page-title-banner .title-image { .page-title-banner .title-image {
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
@@ -10658,7 +10658,7 @@ select.form-control {
width: auto; width: auto;
z-index: 1; z-index: 1;
} }
.org-page-title-banner h1 { .page-title-banner h1 {
position: relative; position: relative;
font-size: 40px; font-size: 40px;
font-weight: 700; font-weight: 700;
@@ -10668,14 +10668,14 @@ select.form-control {
letter-spacing: -0.5px; letter-spacing: -0.5px;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.org-page-title-banner { .page-title-banner {
height: 120px; height: 120px;
margin-bottom: 32px; margin-bottom: 32px;
} }
.org-page-title-banner .title-image { .page-title-banner .title-image {
height: 120px; height: 120px;
} }
.org-page-title-banner h1 { .page-title-banner h1 {
font-size: 24px; font-size: 24px;
} }
} }
@@ -5,7 +5,7 @@
@import '../abstracts/mixins'; @import '../abstracts/mixins';
// Page Title Banner // Page Title Banner
.org-page-title-banner { .page-title-banner {
position: relative; position: relative;
width: 100%; width: 100%;
height: 220px; height: 220px;
@@ -6,7 +6,7 @@
<body> <body>
<th:block layout:fragment="title"> <th:block layout:fragment="title">
<div class="org-page-title-banner" style="margin-bottom: 40px;"> <div class="page-title-banner" style="margin-bottom: 40px;">
<h1>API 마켓</h1> <h1>API 마켓</h1>
</div> </div>
</th:block> </th:block>
@@ -1,138 +1,123 @@
<!DOCTYPE html> <!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security" xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}"> xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_title_layout}">
<body> <body>
<section layout:fragment="title">
<div class="page-title-banner">
<h1>앱관리</h1>
</div>
</section>
<th:block layout:fragment="contentFragment"> <th:block layout:fragment="contentFragment">
<section class="apikey-container"> <section class="app-management-container">
<!-- Header Section --> <!-- App List Title Section -->
<div class="apikey-header"> <div class="app-list-title-section">
<div class="apikey-title"> <h2 class="app-list-title">앱 목록</h2>
<h1>마이페이지</h1>
<h2>인증키 목록</h2>
</div>
<div class="apikey-actions">
<button sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"
type="button"
class="btn btn-primary"
id="requestApiKey">
<i class="fas fa-plus"></i>
<span>앱 생성하기</span>
</button>
</div>
</div> </div>
<!-- Result Count --> <!-- App List Container -->
<div class="apikey-result-count"> <div class="app-list-container">
<strong th:text="${(appRequests != null ? appRequests.size() : 0) + (apiKeys != null ? apiKeys.size() : 0)}">0</strong>개의 인증키
</div>
<!-- App Requests Cards Grid (Pending) --> <!-- App Requests (Pending) -->
<div class="apikey-card-grid" th:if="${appRequests != null and !appRequests.isEmpty()}"> <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})}">
<a class="apikey-card" <!-- App Icon -->
th:each="request : ${appRequests}" <div class="app-list-icon">
th:href="@{/myapikey/app_request_detail(id=${request.id})}"> <img th:if="${request.appIconFileId != null}"
th:src="@{/file/download(fileSn=1,fileId=${request.appIconFileId})}"
<!-- App Icon --> alt="App Icon"
<div class="apikey-card-icon"> class="app-icon-image">
<img th:if="${request.appIconFileId != null}" <div th:unless="${request.appIconFileId != null}" class="app-icon-placeholder">
th:src="@{/file/download(fileSn=1,fileId=${request.appIconFileId})}" <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
alt="App Icon" <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
class="app-icon-image"> <circle cx="8.5" cy="8.5" r="1.5"></circle>
<div th:unless="${request.appIconFileId != null}" class="app-icon-placeholder"> <polyline points="21 15 16 10 5 21"></polyline>
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> </svg>
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect> </div>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21 15 16 10 5 21"></polyline>
</svg>
</div> </div>
</div>
<!-- App Content --> <!-- App Info -->
<div class="apikey-card-content"> <div class="app-list-info">
<!-- App Name --> <div class="app-list-header">
<h3 class="apikey-card-title" th:text="${request.clientName}">앱 이름</h3> <!-- Status Badge -->
<span class="app-status-badge status-pending"
<!-- App Description --> th:text="${request.approval != null and request.approval.approvalStatus != null ? request.approval.approvalStatus.description : '승인대기'}">
<p class="apikey-card-description" 승인대기
th:text="${request.appDescription != null ? request.appDescription : '설명 없음'}"> </span>
앱 설명이 여기에 표시됩니다. <!-- App Name -->
</p> <h3 class="app-list-name" th:text="${request.clientName}">앱 이름</h3>
</div> </div>
<!-- App Description -->
<!-- App Status Badge (Using Approval State) --> <p class="app-list-description"
<div class="apikey-card-status-wrapper"> th:text="${request.appDescription != null ? request.appDescription : '설명 없음'}">
<span class="apikey-card-status status-pending" 앱 설명이 여기에 표시됩니다.
th:text="${request.approval != null and request.approval.approvalStatus != null ? request.approval.approvalStatus.description : '상태 없음'}"> </p>
승인 대기
</span>
</div>
</a>
</div>
<!-- API Key Cards Grid (Approved) -->
<div class="apikey-card-grid" th:if="${apiKeys != null and !apiKeys.isEmpty()}">
<a class="apikey-card"
th:each="apikey, status : ${apiKeys}"
th:href="@{/myapikey/credential_detail(id=${apikey.clientid})}">
<!-- App Icon -->
<div class="apikey-card-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>
</div>
<!-- App Content --> </a>
<div class="apikey-card-content"> </th:block>
<!-- App Name -->
<h3 class="apikey-card-title" th:text="${apikey.clientname}">앱 이름</h3>
<!-- App Description --> <!-- API Keys (Approved) -->
<p class="apikey-card-description" <th:block th:if="${apiKeys != null and !apiKeys.isEmpty()}">
th:text="${apikey.appDescription != null ? apikey.appDescription : '설명 없음'}"> <a class="app-list-item"
앱 설명이 여기에 표시됩니다. th:each="apikey : ${apiKeys}"
</p> th:href="@{/myapikey/credential_detail(id=${apikey.clientid})}">
</div>
<!-- App Status Badge --> <!-- App Icon -->
<div class="apikey-card-status-wrapper"> <div class="app-list-icon">
<span class="apikey-card-status" <img th:if="${apikey.appIconFileId != null}"
th:classappend="${apikey.appstatus == '1' ? 'status-active' : apikey.appstatus == '0' ? 'status-inactive' : 'status-pending'}" th:src="@{/file/download(fileSn=1,fileId=${apikey.appIconFileId})}"
th:text="${apikey.appstatus == '1' ? '활성화' : apikey.appstatus == '0' ? '비활성화' : '승인 대기'}"> alt="App Icon"
활성화 class="app-icon-image">
</span> <div th:unless="${apikey.appIconFileId != null}" class="app-icon-placeholder">
</div> <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>
</a> <!-- 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> </div>
<!-- Empty State --> <!-- Create App Button -->
<div class="apikey-empty-state" th:if="${(appRequests == null or appRequests.isEmpty()) and (apiKeys == null or apiKeys.isEmpty())}"> <div class="app-create-button-wrapper" sec:authorize="hasRole('ROLE_API_KEY_REQUEST')">
<div class="empty-icon">🔑</div> <button type="button" class="btn-app-create" id="requestApiKey">
<h3>등록된 인증키가 없습니다</h3> 앱 생성
<p>새로운 앱을 생성하여 API를 사용해보세요.</p>
<button sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"
type="button"
class="btn btn-primary"
id="requestApiKeyEmpty">
<i class="fas fa-plus"></i>
<span>첫 앱 생성하기</span>
</button> </button>
</div> </div>
@@ -140,83 +125,278 @@
</th:block> </th:block>
<th:block layout:fragment="contentScript"> <th:block layout:fragment="contentScript">
<style>
/* App Management Container */
.app-management-container {
max-width: 1228px;
margin: 0 auto;
padding: 60px 20px 80px;
}
/* App List Title Section */
.app-list-title-section {
background-color: #f6f9fb;
border-radius: 12px;
padding: 14px 45px;
margin-bottom: 30px;
}
.app-list-title {
font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif;
font-size: 28px;
font-weight: 700;
color: #212529;
margin: 0;
}
/* App List Container */
.app-list-container {
background-color: #f6f9fb;
border-radius: 12px;
padding: 30px 45px;
min-height: 400px;
}
/* App List Item */
.app-list-item {
display: flex;
align-items: center;
gap: 20px;
padding: 40px 0;
border-bottom: 1px solid #e0e0e0;
text-decoration: none;
color: inherit;
transition: background-color 0.2s ease;
}
.app-list-item:last-child {
border-bottom: none;
}
.app-list-item:hover {
background-color: rgba(255, 255, 255, 0.5);
margin: 0 -20px;
padding-left: 20px;
padding-right: 20px;
border-radius: 8px;
}
/* App Icon */
.app-list-icon {
width: 70px;
height: 70px;
flex-shrink: 0;
}
.app-list-icon .app-icon-image {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 12px;
}
.app-list-icon .app-icon-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: #e9ecef;
border-radius: 12px;
color: #868e96;
}
/* App Info */
.app-list-info {
display: flex;
flex-direction: column;
gap: 14px;
flex: 1;
}
.app-list-header {
display: flex;
align-items: center;
gap: 12px;
}
/* Status Badge */
.app-status-badge {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 5px 14px;
border-radius: 40px;
font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif;
font-size: 13px;
font-weight: 700;
color: #ffffff;
white-space: nowrap;
}
.app-status-badge.status-pending {
background-color: #8c959f;
}
.app-status-badge.status-approved {
background-color: #0049b4;
}
.app-status-badge.status-changing {
background-color: #88a5f3;
}
.app-status-badge.status-inactive {
background-color: #dc3545;
}
/* App Name */
.app-list-name {
font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif;
font-size: 22px;
font-weight: 700;
color: #212529;
margin: 0;
}
/* App Description */
.app-list-description {
font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif;
font-size: 16px;
font-weight: 400;
color: #6e7780;
margin: 0;
line-height: 1.5;
}
/* Empty State */
.app-list-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
text-align: center;
}
.app-list-empty .empty-icon {
font-size: 64px;
margin-bottom: 20px;
}
.app-list-empty h3 {
font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif;
font-size: 24px;
font-weight: 700;
color: #212529;
margin: 0 0 12px;
}
.app-list-empty p {
font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif;
font-size: 16px;
color: #6e7780;
margin: 0;
}
/* Create App Button Wrapper */
.app-create-button-wrapper {
display: flex;
justify-content: center;
margin-top: 60px;
}
/* Create App Button */
.btn-app-create {
width: 220px;
height: 60px;
background-color: #0049b4;
color: #ffffff;
border: none;
border-radius: 12px;
font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif;
font-size: 18px;
font-weight: 700;
cursor: pointer;
transition: background-color 0.2s ease, transform 0.2s ease;
}
.btn-app-create:hover {
background-color: #003a91;
transform: translateY(-2px);
}
.btn-app-create:active {
transform: translateY(0);
}
/* Responsive */
@media screen and (max-width: 768px) {
.app-management-container {
padding: 40px 16px 60px;
}
.app-list-title-section {
padding: 12px 20px;
}
.app-list-title {
font-size: 22px;
}
.app-list-container {
padding: 20px;
}
.app-list-item {
flex-direction: column;
align-items: flex-start;
padding: 30px 0;
gap: 16px;
}
.app-list-item:hover {
margin: 0 -10px;
padding-left: 10px;
padding-right: 10px;
}
.app-list-icon {
width: 60px;
height: 60px;
}
.app-list-header {
flex-wrap: wrap;
gap: 8px;
}
.app-list-name {
font-size: 18px;
}
.app-list-description {
font-size: 14px;
}
.app-create-button-wrapper {
margin-top: 40px;
}
.btn-app-create {
width: 100%;
max-width: 300px;
height: 54px;
font-size: 16px;
}
}
</style>
<script th:inline="javascript"> <script th:inline="javascript">
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Card click handlers // App creation button handler
const apikeyCards = document.querySelectorAll('.apikey-card');
apikeyCards.forEach(function(card) {
// Prevent navigation when clicking on specific elements
const preventNavElements = card.querySelectorAll('.btn-copy-key, .btn-view-detail, a');
preventNavElements.forEach(function(element) {
element.addEventListener('click', function(e) {
e.stopPropagation();
});
});
// Card click handler
card.addEventListener('click', function(e) {
// Don't navigate if clicking on interactive elements
if (e.target.closest('.btn-copy-key') || e.target.closest('.btn-view-detail')) {
return;
}
const href = this.getAttribute('data-href');
if (href) {
window.location.href = href;
}
});
// Keyboard accessibility
card.addEventListener('keypress', function(e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
const href = this.getAttribute('data-href');
if (href) {
window.location.href = href;
}
}
});
});
// Copy to clipboard functionality
const copyButtons = document.querySelectorAll('.btn-copy-key');
copyButtons.forEach(function(button) {
button.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
const textToCopy = this.getAttribute('data-clipboard-text');
// Create temporary input element
const tempInput = document.createElement('input');
tempInput.value = textToCopy;
document.body.appendChild(tempInput);
tempInput.select();
try {
document.execCommand('copy');
// Visual feedback
const originalContent = this.innerHTML;
this.innerHTML = '<span class="copy-icon">✓</span>';
this.classList.add('copied');
setTimeout(() => {
this.innerHTML = originalContent;
this.classList.remove('copied');
}, 2000);
} catch (err) {
console.error('Failed to copy text: ', err);
}
document.body.removeChild(tempInput);
});
});
// App creation button handlers
const requestApiKeyBtn = document.getElementById('requestApiKey'); const requestApiKeyBtn = document.getElementById('requestApiKey');
const requestApiKeyEmptyBtn = document.getElementById('requestApiKeyEmpty');
function handleApiKeyRequest() { function handleApiKeyRequest() {
// Clear any existing registration data from sessionStorage // Clear any existing registration data from sessionStorage
@@ -230,17 +410,12 @@
keysToRemove.forEach(key => sessionStorage.removeItem(key)); keysToRemove.forEach(key => sessionStorage.removeItem(key));
// Redirect to the API key registration wizard with clear parameter // Redirect to the API key registration wizard with clear parameter
// This tells the server to clear the session and start fresh
window.location.href = '/myapikey/register/step1?clear=true'; window.location.href = '/myapikey/register/step1?clear=true';
} }
if (requestApiKeyBtn) { if (requestApiKeyBtn) {
requestApiKeyBtn.addEventListener('click', handleApiKeyRequest); requestApiKeyBtn.addEventListener('click', handleApiKeyRequest);
} }
if (requestApiKeyEmptyBtn) {
requestApiKeyEmptyBtn.addEventListener('click', handleApiKeyRequest);
}
}); });
</script> </script>
</th:block> </th:block>
@@ -4,7 +4,7 @@
layout:decorate="~{layout/kbank_signup_layout}"> layout:decorate="~{layout/kbank_signup_layout}">
<body> <body>
<section layout:fragment="title"> <section layout:fragment="title">
<div class="org-page-title-banner"> <div class="page-title-banner">
<img th:src="@{/img/user_register_title.png}" class="title-image"> <img th:src="@{/img/user_register_title.png}" class="title-image">
<h1>법인회원 전환</h1> <h1>법인회원 전환</h1>
</div> </div>
@@ -7,7 +7,7 @@
</head> </head>
<body> <body>
<section layout:fragment="title"> <section layout:fragment="title">
<div class="org-page-title-banner"> <div class="page-title-banner">
<img th:src="@{/img/user_register_title.png}" class="title-image"> <img th:src="@{/img/user_register_title.png}" class="title-image">
<h1>비밀번호 변경</h1> <h1>비밀번호 변경</h1>
</div> </div>
@@ -4,7 +4,7 @@
layout:decorate="~{layout/kjbank_title_layout}"> layout:decorate="~{layout/kjbank_title_layout}">
<body> <body>
<section layout:fragment="title"> <section layout:fragment="title">
<div class="org-page-title-banner"> <div class="page-title-banner">
<img th:src="@{/img/user_register_title.png}" class="title-image"> <img th:src="@{/img/user_register_title.png}" class="title-image">
<h1>비밀번호 변경</h1> <h1>비밀번호 변경</h1>
</div> </div>
@@ -3,7 +3,7 @@
layout:decorate="~{layout/kjbank_title_layout}"> layout:decorate="~{layout/kjbank_title_layout}">
<body> <body>
<section layout:fragment="title"> <section layout:fragment="title">
<div class="org-page-title-banner"> <div class="page-title-banner">
<img th:src="@{/img/user_register_title.png}" class="title-image"> <img th:src="@{/img/user_register_title.png}" class="title-image">
<h1>내 정보 관리</h1> <h1>내 정보 관리</h1>
</div> </div>
@@ -3,7 +3,7 @@
layout:decorate="~{layout/kjbank_title_layout}"> layout:decorate="~{layout/kjbank_title_layout}">
<body> <body>
<section layout:fragment="title"> <section layout:fragment="title">
<div class="org-page-title-banner"> <div class="page-title-banner">
<img th:src="@{/img/user_register_title.png}" class="title-image"> <img th:src="@{/img/user_register_title.png}" class="title-image">
<h1>내 정보 관리</h1> <h1>내 정보 관리</h1>
</div> </div>
@@ -3,7 +3,7 @@
layout:decorate="~{layout/kjbank_title_layout}"> layout:decorate="~{layout/kjbank_title_layout}">
<body> <body>
<section layout:fragment="title"> <section layout:fragment="title">
<div class="org-page-title-banner"> <div class="page-title-banner">
<img th:src="@{/img/user_register_title.png}" class="title-image"> <img th:src="@{/img/user_register_title.png}" class="title-image">
<h1>내 정보 관리</h1> <h1>내 정보 관리</h1>
</div> </div>
@@ -4,7 +4,7 @@
layout:decorate="~{layout/kjbank_title_layout}"> layout:decorate="~{layout/kjbank_title_layout}">
<body> <body>
<section layout:fragment="title"> <section layout:fragment="title">
<div class="org-page-title-banner"> <div class="page-title-banner">
<img th:src="@{/img/user_register_title.png}" alt="법인회원가입" class="title-image"> <img th:src="@{/img/user_register_title.png}" alt="법인회원가입" class="title-image">
<h1>법인회원가입</h1> <h1>법인회원가입</h1>
</div> </div>
@@ -4,7 +4,7 @@
layout:decorate="~{layout/kjbank_title_layout}"> layout:decorate="~{layout/kjbank_title_layout}">
<body> <body>
<section layout:fragment="title"> <section layout:fragment="title">
<div class="org-page-title-banner"> <div class="page-title-banner">
<img th:src="@{/img/user_register_title.png}" alt="개인회원가입" class="title-image"> <img th:src="@{/img/user_register_title.png}" alt="개인회원가입" class="title-image">
<h1 th:text="${!isInvited ? '개인회원가입' : '법인회원가입'}">개인회원가입</h1> <h1 th:text="${!isInvited ? '개인회원가입' : '법인회원가입'}">개인회원가입</h1>
</div> </div>
@@ -3,7 +3,7 @@
layout:decorate="~{layout/kjbank_title_layout}"> layout:decorate="~{layout/kjbank_title_layout}">
<body> <body>
<section layout:fragment="title"> <section layout:fragment="title">
<div class="org-page-title-banner"> <div class="page-title-banner">
<img th:src="@{/img/user_register_title.png}" class="title-image"> <img th:src="@{/img/user_register_title.png}" class="title-image">
<h1 th:text="${agreementTitle}">비밀번호 변경</h1> <h1 th:text="${agreementTitle}">비밀번호 변경</h1>