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

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;
width: 100%;
height: 220px;
@@ -10638,7 +10638,7 @@ select.form-control {
justify-content: center;
overflow: hidden;
}
.org-page-title-banner::before {
.page-title-banner::before {
content: "";
position: absolute;
top: 0;
@@ -10649,7 +10649,7 @@ select.form-control {
background-size: 50px 50px;
opacity: 0.5;
}
.org-page-title-banner .title-image {
.page-title-banner .title-image {
position: absolute;
top: 50%;
left: 50%;
@@ -10658,7 +10658,7 @@ select.form-control {
width: auto;
z-index: 1;
}
.org-page-title-banner h1 {
.page-title-banner h1 {
position: relative;
font-size: 40px;
font-weight: 700;
@@ -10668,14 +10668,14 @@ select.form-control {
letter-spacing: -0.5px;
}
@media (max-width: 768px) {
.org-page-title-banner {
.page-title-banner {
height: 120px;
margin-bottom: 32px;
}
.org-page-title-banner .title-image {
.page-title-banner .title-image {
height: 120px;
}
.org-page-title-banner h1 {
.page-title-banner h1 {
font-size: 24px;
}
}
@@ -5,7 +5,7 @@
@import '../abstracts/mixins';
// Page Title Banner
.org-page-title-banner {
.page-title-banner {
position: relative;
width: 100%;
height: 220px;
@@ -6,7 +6,7 @@
<body>
<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>
</div>
</th:block>
@@ -1,138 +1,123 @@
<!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_base_layout}">
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">
<h1>앱관리</h1>
</div>
</section>
<th:block layout:fragment="contentFragment">
<section class="apikey-container">
<section class="app-management-container">
<!-- Header Section -->
<div class="apikey-header">
<div class="apikey-title">
<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>
<!-- App List Title Section -->
<div class="app-list-title-section">
<h2 class="app-list-title">앱 목록</h2>
</div>
<!-- Result Count -->
<div class="apikey-result-count">
<strong th:text="${(appRequests != null ? appRequests.size() : 0) + (apiKeys != null ? apiKeys.size() : 0)}">0</strong>개의 인증키
</div>
<!-- App List Container -->
<div class="app-list-container">
<!-- App Requests Cards Grid (Pending) -->
<div class="apikey-card-grid" th:if="${appRequests != null and !appRequests.isEmpty()}">
<!-- 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})}">
<a class="apikey-card"
th:each="request : ${appRequests}"
th:href="@{/myapikey/app_request_detail(id=${request.id})}">
<!-- App Icon -->
<div class="apikey-card-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>
<!-- 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>
</div>
<!-- App Content -->
<div class="apikey-card-content">
<!-- App Name -->
<h3 class="apikey-card-title" th:text="${request.clientName}">앱 이름</h3>
<!-- App Description -->
<p class="apikey-card-description"
th:text="${request.appDescription != null ? request.appDescription : '설명 없음'}">
앱 설명이 여기에 표시됩니다.
</p>
</div>
<!-- App Status Badge (Using Approval State) -->
<div class="apikey-card-status-wrapper">
<span class="apikey-card-status status-pending"
th:text="${request.approval != null and request.approval.approvalStatus != null ? request.approval.approvalStatus.description : '상태 없음'}">
승인 대기
</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>
<!-- 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>
</div>
<!-- App Content -->
<div class="apikey-card-content">
<!-- App Name -->
<h3 class="apikey-card-title" th:text="${apikey.clientname}">앱 이름</h3>
</a>
</th:block>
<!-- App Description -->
<p class="apikey-card-description"
th:text="${apikey.appDescription != null ? apikey.appDescription : '설명 없음'}">
앱 설명이 여기에 표시됩니다.
</p>
</div>
<!-- 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 Status Badge -->
<div class="apikey-card-status-wrapper">
<span class="apikey-card-status"
th:classappend="${apikey.appstatus == '1' ? 'status-active' : apikey.appstatus == '0' ? 'status-inactive' : 'status-pending'}"
th:text="${apikey.appstatus == '1' ? '활성화' : apikey.appstatus == '0' ? '비활성화' : '승인 대기'}">
활성화
</span>
</div>
<!-- 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>
</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>
<!-- Empty State -->
<div class="apikey-empty-state" th:if="${(appRequests == null or appRequests.isEmpty()) and (apiKeys == null or apiKeys.isEmpty())}">
<div class="empty-icon">🔑</div>
<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>
<!-- 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>
@@ -140,83 +125,278 @@
</th:block>
<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">
document.addEventListener('DOMContentLoaded', function() {
// Card click handlers
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
// App creation button handler
const requestApiKeyBtn = document.getElementById('requestApiKey');
const requestApiKeyEmptyBtn = document.getElementById('requestApiKeyEmpty');
function handleApiKeyRequest() {
// Clear any existing registration data from sessionStorage
@@ -230,17 +410,12 @@
keysToRemove.forEach(key => sessionStorage.removeItem(key));
// 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';
}
if (requestApiKeyBtn) {
requestApiKeyBtn.addEventListener('click', handleApiKeyRequest);
}
if (requestApiKeyEmptyBtn) {
requestApiKeyEmptyBtn.addEventListener('click', handleApiKeyRequest);
}
});
</script>
</th:block>
@@ -4,7 +4,7 @@
layout:decorate="~{layout/kbank_signup_layout}">
<body>
<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">
<h1>법인회원 전환</h1>
</div>
@@ -7,7 +7,7 @@
</head>
<body>
<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">
<h1>비밀번호 변경</h1>
</div>
@@ -4,7 +4,7 @@
layout:decorate="~{layout/kjbank_title_layout}">
<body>
<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">
<h1>비밀번호 변경</h1>
</div>
@@ -3,7 +3,7 @@
layout:decorate="~{layout/kjbank_title_layout}">
<body>
<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">
<h1>내 정보 관리</h1>
</div>
@@ -3,7 +3,7 @@
layout:decorate="~{layout/kjbank_title_layout}">
<body>
<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">
<h1>내 정보 관리</h1>
</div>
@@ -3,7 +3,7 @@
layout:decorate="~{layout/kjbank_title_layout}">
<body>
<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">
<h1>내 정보 관리</h1>
</div>
@@ -4,7 +4,7 @@
layout:decorate="~{layout/kjbank_title_layout}">
<body>
<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">
<h1>법인회원가입</h1>
</div>
@@ -4,7 +4,7 @@
layout:decorate="~{layout/kjbank_title_layout}">
<body>
<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">
<h1 th:text="${!isInvited ? '개인회원가입' : '법인회원가입'}">개인회원가입</h1>
</div>
@@ -3,7 +3,7 @@
layout:decorate="~{layout/kjbank_title_layout}">
<body>
<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">
<h1 th:text="${agreementTitle}">비밀번호 변경</h1>