인증키 요청 조회, 인증키 상세 조회 디자인 반영

This commit is contained in:
현성필
2025-12-01 15:24:55 +09:00
parent 00c014c73d
commit 1da9b48dd5
18 changed files with 2101 additions and 1293 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

@@ -7,25 +7,6 @@
@include card;
position: relative;
overflow: hidden;
// Card hover effect
&::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: #3BA4ED;
transform: rotate(45deg);
transition: $transition-slow;
opacity: 0;
}
&:hover::before {
animation: shimmer 0.6s ease;
opacity: 1;
}
}
// API showcase cards
@@ -51,7 +51,7 @@
left: 0;
right: 0;
//background-color: #C3DFEA;
background-color: #E5F8FF;
background-color: #ffffff;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
z-index: 1000;
+2
View File
@@ -24,6 +24,7 @@
@import 'components/buttons';
@import 'components/badges';
@import 'components/cards';
@import 'components/apikey-common';
@import 'components/forms';
@import 'components/modals';
@import 'components/navigation';
@@ -47,6 +48,7 @@
@import 'pages/mypage';
@import 'pages/apikey-register';
@import 'pages/apikey-detail';
@import 'pages/apikey-list';
@import 'pages/notice';
@import 'pages/inquiry';
@import 'pages/org-register';
@@ -3,17 +3,16 @@
// ====================================
// API Key Detail Pages Styles
// For appRequestDetail.html and credentialDetail.html
// Uses common styles from components/_apikey-common.scss
// ====================================
// Container Styles
// Container Styles - extends common
.apikey-detail-container {
max-width: 1200px;
margin: 0 auto;
padding: $spacing-4xl;
padding-top: 0;
@extend .apikey-container;
padding: $spacing-2xl $spacing-lg;
@media (max-width: $breakpoint-sm) {
padding: $spacing-md;
padding: $spacing-lg $spacing-md;
}
}
@@ -40,12 +39,9 @@
margin-bottom: $spacing-4xl;
}
// Status Badge - extends common with light style
.status-badge {
display: inline-block;
padding: $spacing-sm $spacing-lg;
border-radius: $border-radius-full;
font-weight: $font-weight-semibold;
font-size: $font-size-sm;
@extend .apikey-status-badge;
&.badge-pending {
background-color: #FFF3CD;
@@ -63,17 +59,9 @@
}
}
// Detail Section
// Detail Section - extends common
.detail-section {
background: $white;
border-radius: $border-radius-lg;
padding: $spacing-4xl;
margin-bottom: $spacing-lg;
box-shadow: $shadow-md;
@media (max-width: $breakpoint-sm) {
padding: $spacing-xl;
}
@extend .apikey-detail-section;
}
.section-title {
@@ -82,28 +70,15 @@
color: $text-dark;
margin-bottom: $spacing-lg;
padding-bottom: $spacing-md;
//border-bottom: 2px solid $border-gray;
}
// Detail Grid
// Detail Grid - extends common
.detail-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: $spacing-lg;
@media (max-width: $breakpoint-sm) {
grid-template-columns: 1fr;
}
@extend .apikey-detail-grid;
}
.detail-item {
display: flex;
flex-direction: column;
gap: $spacing-sm;
&.full-width {
grid-column: 1 / -1;
}
@extend .apikey-detail-item;
}
.detail-label {
@@ -118,106 +93,46 @@
word-break: break-word;
}
// App Icon
// App Icon - extends common
.app-icon-display {
width: 120px;
height: 120px;
@extend .apikey-icon-display;
}
.app-icon-image {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: $border-radius-lg;
border: 2px solid $border-gray;
@extend .apikey-icon-image;
}
.app-icon-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: $gray-bg;
border-radius: $border-radius-lg;
border: 2px dashed $text-light;
font-size: 48px;
@extend .apikey-icon-placeholder;
}
// IP List
// IP List - extends common
.ip-list {
display: flex;
flex-wrap: wrap;
gap: $spacing-sm;
@extend .apikey-ip-list;
}
.ip-tag {
display: inline-block;
padding: $spacing-xs $spacing-md;
background-color: $light-bg;
color: $secondary-blue;
border-radius: $border-radius-sm;
font-size: $font-size-sm;
font-family: $font-family-mono;
@extend .apikey-ip-tag;
}
// Credential Code
// Credential Code - extends common
.credential-code {
display: inline-block;
padding: $spacing-sm $spacing-md;
background-color: $gray-bg;
border: 1px solid $border-gray;
border-radius: $border-radius-sm;
font-family: $font-family-mono;
font-size: $font-size-sm;
color: $secondary-blue;
@extend .apikey-credential-code;
}
// Secret Box
// Secret Box - extends common
.secret-box {
display: flex;
align-items: center;
gap: $spacing-md;
@media (max-width: $breakpoint-sm) {
flex-direction: column;
align-items: flex-start;
}
@extend .apikey-secret-box;
}
// Note: .btn-copy styles now in components/_buttons.scss
// Keeping this for page-specific override if needed
// Copy Button - extends common
.btn-copy {
svg {
width: 16px;
height: 16px;
}
@media (max-width: $breakpoint-sm) {
align-self: stretch;
justify-content: center;
}
@extend .apikey-btn-copy;
}
// Status Indicator
// Status Indicator - extends common
.status-indicator {
display: inline-flex;
align-items: center;
gap: $spacing-sm;
padding: $spacing-xs $spacing-md;
border-radius: $border-radius-xl;
font-size: $font-size-sm;
font-weight: $font-weight-semibold;
&.status-active {
background-color: #D4EDDA;
color: #155724;
}
&.status-inactive {
background-color: #F8D7DA;
color: #721C24;
}
@extend .apikey-status-indicator;
}
.status-dot {
@@ -227,31 +142,20 @@
background-color: currentColor;
}
// API Detail List
// API Detail List - extends common
.api-detail-list {
display: flex;
flex-direction: column;
gap: $spacing-sm;
@extend .apikey-api-list;
}
.api-list-item {
display: flex;
align-items: center;
gap: $spacing-md;
padding: $spacing-md $spacing-lg;
background: $white;
border: 1px solid $border-gray;
border-radius: $border-radius-md;
transition: all 0.2s;
@extend .apikey-api-item;
&:hover {
border-color: $primary-blue;
box-shadow: $shadow-sm;
}
@media (max-width: $breakpoint-sm) {
padding: $spacing-md;
gap: $spacing-sm;
.api-name {
flex: 1;
font-size: $font-size-base;
font-weight: $font-weight-medium;
color: $text-dark;
margin: 0;
}
}
@@ -291,14 +195,6 @@
}
}
.api-list-item .api-name {
flex: 1;
font-size: $font-size-base;
font-weight: $font-weight-medium;
color: $text-dark;
margin: 0;
}
.api-status {
padding: $spacing-xs $spacing-md;
border-radius: $border-radius-lg;
@@ -350,36 +246,16 @@
font-weight: $font-weight-semibold;
}
// Empty State
// Empty State - extends common
.empty-state {
@extend .apikey-empty-state;
grid-column: 1 / -1;
text-align: center;
padding: 48px $spacing-lg;
.empty-icon {
font-size: 64px;
margin-bottom: $spacing-md;
}
p {
font-size: $font-size-base;
color: $text-gray;
}
}
// Action Buttons
// Action Buttons - extends common
.detail-actions {
display: flex;
justify-content: center;
gap: $spacing-md;
flex-wrap: wrap;
margin-top: $spacing-4xl;
padding-top: $spacing-4xl;
border-top: 1px solid $border-gray;
@media (max-width: $breakpoint-sm) {
flex-direction: column;
}
@extend .apikey-form-actions;
}
// Note: Button styles now in components/_buttons.scss
@@ -391,3 +267,353 @@
width: 100%;
}
}
// ====================================
// App Info Card (Figma Design)
// For credentialDetail.html and appRequestDetail.html
// ====================================
.app-info-card {
display: flex;
align-items: flex-start;
gap: $spacing-xl;
padding: 0;
padding-top: 27px;
margin-bottom: $spacing-4xl;
@media (max-width: $breakpoint-sm) {
flex-direction: column;
align-items: center;
text-align: center;
padding: $spacing-lg 0;
gap: $spacing-lg;
}
}
.app-info-icon {
flex-shrink: 0;
width: 160px;
height: 160px;
border-radius: $border-radius-xl;
overflow: hidden;
background: $gray-bg;
display: flex;
align-items: center;
justify-content: center;
margin-left: 87px;
@media (max-width: $breakpoint-sm) {
width: 120px;
height: 120px;
}
.app-icon-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.app-icon-placeholder {
color: $text-light;
display: flex;
align-items: center;
justify-content: center;
}
}
.app-info-content {
flex: 1;
display: flex;
flex-direction: column;
gap: $spacing-md;
}
.app-status-badge {
display: inline-block;
padding: $spacing-xs $spacing-lg;
border-radius: $border-radius-full;
font-size: $font-size-sm;
font-weight: $font-weight-semibold;
width: fit-content;
&.status-approved {
background-color: #0049b4;
color: $white;
}
&.status-pending {
background-color: #FFF3CD;
color: #856404;
}
&.status-inactive {
background-color: $border-gray;
color: $text-gray;
}
&.status-rejected {
background-color: #F8D7DA;
color: #721C24;
}
}
.app-info-name {
font-size: 24px;
font-weight: $font-weight-bold;
color: $text-dark;
margin: 0;
@media (max-width: $breakpoint-sm) {
font-size: 20px;
}
}
.app-info-description {
font-size: 18px;
color: #515151;
margin: 0;
line-height: 1.5;
@media (max-width: $breakpoint-sm) {
font-size: $font-size-base;
}
}
// ====================================
// API Key Info Section
// For credential details display
// Figma Design: node 985-1365
// ====================================
.apikey-info-section {
background: #F6F9FB;
border-radius: $border-radius-lg;
padding: $spacing-3xl $spacing-2xl;
margin-bottom: $spacing-2xl;
@media (max-width: $breakpoint-sm) {
padding: $spacing-lg;
}
}
.info-row {
display: flex;
align-items: center;
gap: $spacing-xl;
margin-bottom: $spacing-xl;
&:last-child {
margin-bottom: 0;
}
@media (max-width: $breakpoint-sm) {
flex-direction: column;
align-items: flex-start;
gap: $spacing-sm;
}
}
.info-row-vertical {
align-items: flex-start;
.info-value {
width: 100%;
@media (max-width: $breakpoint-sm) {
max-width: 100%;
}
}
}
.info-label {
flex-shrink: 0;
width: 140px;
font-size: 20px;
font-weight: $font-weight-bold;
color: #212529;
@media (max-width: $breakpoint-sm) {
width: 100%;
font-size: $font-size-base;
}
}
.info-value {
flex: 1;
font-size: $font-size-base;
color: $text-dark;
word-break: break-word;
@media (max-width: $breakpoint-sm) {
width: 100%;
}
}
// Info Value Box Styles (Figma)
.info-value-box {
display: flex;
align-items: center;
justify-content: center;
height: 60px;
padding: 0 $spacing-lg;
background: #DDDDDD;
border-radius: $border-radius-lg;
font-size: 20px;
color: #515151;
text-align: center;
@media (max-width: $breakpoint-sm) {
height: 50px;
font-size: $font-size-base;
padding: 0 $spacing-md;
}
&.with-copy {
flex: 1;
}
}
// Client ID Row with Copy Button
.info-row-with-action {
.info-value {
display: flex;
gap: $spacing-md;
align-items: center;
}
}
.btn-copy-action {
display: flex;
align-items: center;
justify-content: center;
gap: $spacing-xs;
height: 60px;
width: 158px;
padding: 0 $spacing-lg;
background: #A4D6EA;
border: none;
border-radius: $border-radius-lg;
font-size: 20px;
font-weight: $font-weight-bold;
color: $white;
cursor: pointer;
white-space: nowrap;
transition: background 0.2s ease;
&:hover {
background: darken(#A4D6EA, 10%);
}
@media (max-width: $breakpoint-sm) {
height: 50px;
font-size: $font-size-base;
padding: 0 $spacing-md;
}
}
// Client Secret View Button
.btn-view-secret {
display: flex;
align-items: center;
justify-content: center;
gap: $spacing-sm;
width: 100%;
height: 60px;
background: #3BA4ED;
border: none;
border-radius: $border-radius-lg;
font-size: 20px;
font-weight: $font-weight-bold;
color: $white;
cursor: pointer;
transition: background 0.2s ease;
&:hover {
background: darken(#3BA4ED, 10%);
}
svg {
width: 20px;
height: 20px;
}
@media (max-width: $breakpoint-sm) {
max-width: 100%;
height: 50px;
font-size: $font-size-base;
}
}
// API List Box (White background)
.api-list-box {
width: 100%;
background: $white;
border: 1px solid #DADADA;
border-radius: $border-radius-lg;
padding: $spacing-lg;
@media (max-width: $breakpoint-sm) {
max-width: 100%;
padding: $spacing-md;
}
}
// API List Item (Figma style)
.api-list-item-figma {
display: flex;
align-items: center;
justify-content: space-between;
padding: $spacing-md 0;
border-bottom: 1px solid #DADADA;
&:last-child {
border-bottom: none;
}
.api-item-name {
font-size: 18px;
color: #000;
@media (max-width: $breakpoint-sm) {
font-size: $font-size-base;
}
}
.api-item-status {
display: flex;
align-items: center;
justify-content: center;
min-width: 100px;
height: 30px;
padding: 0 $spacing-lg;
background: #ECEFF4;
border-radius: $border-radius-full;
font-size: $font-size-base;
color: #515151;
@media (max-width: $breakpoint-sm) {
min-width: 80px;
font-size: $font-size-sm;
padding: 0 $spacing-md;
}
}
}
// ====================================
// Form Actions Center
// ====================================
.form-actions-center {
display: flex;
justify-content: center;
gap: $spacing-md;
margin-top: $spacing-2xl;
@media (max-width: $breakpoint-sm) {
flex-direction: column;
.btn {
width: 100%;
}
}
}
@@ -1,18 +1,17 @@
// -----------------------------------------------------------------------------
// API Key Registration Wizard Styles
// Reference: apps/mypage/apiKeyRegisterStep1.html, Step2, Step3
// Uses common styles from components/_apikey-common.scss
// -----------------------------------------------------------------------------
// =============================================================================
// STEP 1: App Information Input (New Figma Design)
// =============================================================================
// Title Bar
// Title Bar - extends common with flex layout
.register-title-bar {
background-color: #f6f9fb;
border-radius: $border-radius-lg;
@extend .apikey-title-bar;
padding: 18px 45px;
margin-bottom: 30px;
display: flex;
align-items: baseline;
gap: 16px;
@@ -116,10 +115,9 @@
}
}
// Form Container (Step 1)
// Form Container (Step 1) - extends common
.register-form-container {
background-color: #f6f9fb;
border-radius: $border-radius-lg;
@extend .apikey-section-box;
padding: 50px 45px;
@include respond-to('md') {
@@ -483,39 +481,18 @@
}
}
// Submit Button - Step 1 Style
// Submit Button - Step 1 Style - extends common
.btn-submit {
width: 220px;
height: 60px;
border: none;
border-radius: $border-radius-lg;
font-size: 18px;
font-weight: $font-weight-bold;
cursor: pointer;
transition: background-color 0.2s ease, transform 0.2s ease;
&:hover {
transform: translateY(-2px);
}
&:active {
transform: translateY(0);
}
@include respond-to('md') {
width: 100%;
max-width: 300px;
}
@extend .apikey-action-btn;
}
// =============================================================================
// STEP 2 & STEP 3: Original Styles
// =============================================================================
// Container with sidebar layout (Step 2)
// Container with sidebar layout (Step 2) - extends common
.apikey-register-container {
max-width: $container-max-width;
margin: 0 auto;
@extend .apikey-container;
padding: $spacing-2xl $spacing-lg;
@media (max-width: $breakpoint-sm) {
@@ -1167,29 +1144,10 @@ input[type="checkbox"]:checked + .custom-checkbox {
}
}
// Empty state for API selection
// Empty state for API selection - extends common
.empty-api-state {
@extend .apikey-empty-state;
padding: $spacing-3xl;
text-align: center;
color: $text-gray;
.empty-icon {
font-size: 60px;
margin-bottom: $spacing-lg;
opacity: 0.5;
}
h3 {
font-size: $font-size-lg;
font-weight: $font-weight-semibold;
color: $text-dark;
margin-bottom: $spacing-sm;
}
p {
font-size: $font-size-base;
color: $text-gray;
}
}
// Loading state for API selection
@@ -1563,17 +1521,16 @@ input[type="checkbox"]:checked + .custom-checkbox {
}
}
// Form Actions
// Form Actions - extends common
.form-actions {
display: flex;
@extend .apikey-form-actions;
justify-content: space-between;
align-items: center;
margin-top: $spacing-2xl;
padding: 0 $spacing-lg $spacing-lg;
gap: $spacing-md;
margin-top: $spacing-2xl;
border-top: none;
padding-top: 0;
@media (max-width: $breakpoint-sm) {
flex-direction: column;
padding: 0 $spacing-md $spacing-md;
gap: $spacing-sm;
}
@@ -1602,16 +1559,21 @@ input[type="checkbox"]:checked + .custom-checkbox {
}
}
// Result Page (Step 3)
// Result Page (Step 3) - extends common section box
.register-result {
@extend .apikey-section-box;
text-align: center;
max-width: 800px;
margin: 0 auto;
height: 1045px;
padding-top: 262px;
&.success {
.result-icon {
background: rgba($accent-green, 0.1);
color: $accent-green;
img {
width: 250px;
height: 250px;
}
}
}
@@ -1626,9 +1588,6 @@ input[type="checkbox"]:checked + .custom-checkbox {
margin-bottom: $spacing-xl;
.result-icon {
width: 80px;
height: 80px;
border-radius: $border-radius-circle;
margin: 0 auto;
display: flex;
align-items: center;
@@ -1641,16 +1600,15 @@ input[type="checkbox"]:checked + .custom-checkbox {
margin-bottom: $spacing-2xl;
h1 {
font-size: $font-size-2xl;
font-size: $font-size-xl;
font-weight: $font-weight-bold;
color: $text-dark;
color: $primary-blue;
margin-bottom: $spacing-md;
}
.result-description {
font-size: $font-size-base;
color: $text-gray;
max-width: 500px;
margin: 0 auto;
}
}
@@ -241,21 +241,6 @@
}
}
// Animation for form elements
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.partnership-card {
animation: fadeInUp 0.6s ease-out;
}
// Focus states for better accessibility
.partnership-card {
@@ -126,273 +126,6 @@
</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() {
@@ -2,206 +2,237 @@
<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_base_layout}">
layout:decorate="~{layout/kjbank_title_layout}">
<head>
<title>API 키 신청 상세</title>
<title>API 키 신청 상세</title>
</head>
<body>
<th:block layout:fragment="contentFragment">
<!-- Header -->
<div class="register-header">
<h1>API Key 신청 상세</h1>
<p class="header-description">
신청하신 API Key의 상세 정보를 확인할 수 있습니다.
</p>
<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">
<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>
<!-- Section: 기본 정보 (From Step 1) -->
<div class="detail-section">
<h2 class="section-title">기본 정보</h2>
<div class="detail-grid">
<!-- App Icon -->
<div class="detail-item full-width">
<label class="detail-label">앱 아이콘</label>
<div class="detail-value">
<div class="app-icon-display" th:if="${appRequest.appIconFileId != null}">
<img th:src="@{/file/download(fileSn=1,fileId=${appRequest.appIconFileId})}"
alt="앱 아이콘"
class="app-icon-image">
<!-- 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>
<div class="app-icon-display" th:unless="${appRequest.appIconFileId != null}">
<div class="app-icon-placeholder">
<span>📱</span>
</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>
</div>
<!-- App Name -->
<div class="detail-item">
<label class="detail-label">앱 이름</label>
<div class="detail-value" th:text="${appRequest.clientName}">
My Application
</div>
</div>
<!-- Request Info Section (Figma Design: node 985-1365) -->
<div class="apikey-info-section">
<!-- Client ID (if exists) -->
<div class="detail-item" th:if="${appRequest.clientId != null}">
<label class="detail-label">Client ID</label>
<div class="detail-value" th:text="${appRequest.clientId}">
client-id-12345
</div>
</div>
<!-- App Description -->
<div class="detail-item full-width">
<label class="detail-label">앱 설명</label>
<div class="detail-value" th:text="${appRequest.appDescription}">
This is a sample app description.
</div>
</div>
<!-- Callback URL -->
<div class="detail-item full-width">
<label class="detail-label">Callback URL</label>
<div class="detail-value" th:text="${appRequest.callbackUrl}">
https://example.com/callback
</div>
</div>
<!-- IP Whitelist -->
<div class="detail-item full-width" th:if="${appRequest.ipWhitelist != null and !appRequest.ipWhitelist.isEmpty()}">
<label class="detail-label">IP 화이트리스트</label>
<div class="detail-value">
<div class="ip-list">
<span class="ip-tag" th:each="ip : ${#strings.arraySplit(appRequest.ipWhitelist, ',')}">
[[${ip.trim()}]]
</span>
<!-- 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>
</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>
<!-- Created Date -->
<div class="detail-item">
<label class="detail-label">신청일시</label>
<div class="detail-value" th:text="${#temporals.format(appRequest.createdDate, 'yyyy.MM.dd HH:mm')}">
2024.01.01 12:00
</div>
</div>
<div class="detail-item" th:if="${appRequest.approval != null}">
<label class="detail-label">승인 상태</label>
<div class="detail-value" th:text="${appRequest.approval.approvalStatus.description}">
승인 대기
</div>
</div>
<div class="detail-item" th:if="${appRequest.approval != null and appRequest.approval.approvalDate != null}">
<label class="detail-label">승인일시</label>
<div class="detail-value" th:text="${#temporals.format(appRequest.approval.approvalDate, 'yyyy.MM.dd HH:mm')}">
2024.01.01 12:00
</div>
</div>
<div class="detail-item full-width" th:if="${appRequest.approval != null and appRequest.reason != null and !appRequest.reason.isEmpty()}">
<label class="detail-label">사유</label>
<div class="detail-value" th:text="${appRequest.reason}">
Approval reason
</div>
</div>
</div>
</div>
<!-- Section: API 정보 (From Step 2) -->
<div class="detail-section">
<h2 class="section-title">선택된 API</h2>
<!-- API List -->
<div class="api-detail-list">
<div class="api-list-item" th:each="api : ${appRequest.apiSpecList}">
<span class="api-method"
th:classappend="${api.apiMethod == 'GET' ? 'method-get' :
(api.apiMethod == 'POST' ? 'method-post' :
(api.apiMethod == 'PUT' ? 'method-put' :
(api.apiMethod == 'DELETE' ? 'method-delete' : 'method-other')))}">
[[${api.apiMethod}]]
</span>
<span class="api-name" th:text="${api.apiName}">API Name</span>
<span class="api-status status-pending">대기중</span>
</div>
<!-- Empty State -->
<div class="empty-state" th:if="${appRequest.apiSpecList == null or appRequest.apiSpecList.isEmpty()}">
<div class="empty-icon">📦</div>
<p>선택된 API가 없습니다.</p>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="detail-actions">
<button type="button"
sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"
class="btn btn-secondary"
th:if="${appRequest.approval != null and appRequest.approval.approvalStatus != null and
<!-- 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-primary">
목록으로
</a>
</div>
th:data-request-id="${appRequest.id}"
onclick="cancelRequestById(this)">
신청 취소
</button>
<a th:href="@{/myapikey}" class="btn btn-submit btn-secondary">
목록
</a>
</div>
</section>
</section>
</th:block>
<th:block layout:fragment="contentScript">
<script th:inline="javascript">
/*<![CDATA[*/
<script th:inline="javascript">
/*<![CDATA[*/
// Cancel request function - called from button with data-request-id attribute
function cancelRequestById(button) {
var requestId = $(button).data('request-id');
// Copy to clipboard function
function copyToClipboardFromButton(button) {
var text = $(button).data('secret');
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'
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);
}
}
}).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>
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>
@@ -2,313 +2,281 @@
<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_base_layout}">
layout:decorate="~{layout/kjbank_title_layout}">
<head>
<title>API 키 상세</title>
<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">
<!-- Header -->
<div class="register-header">
<h1>API Key 상세</h1>
<p class="header-description">
발급된 API Key의 상세 정보를 확인할 수 있습니다.
</p>
<!-- Title Bar -->
<div class="register-title-bar">
<h2 class="register-title">앱 정보</h2>
<span class="register-subtitle">등록된 앱의 상세 정보를 확인할 수 있습니다.</span>
</div>
<section class="apikey-detail-container">
<!-- 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>
<!-- Section 1: 앱 기본 정보 -->
<div class="detail-section">
<h2 class="section-title">앱 정보</h2>
<div class="detail-grid">
<!-- Credential Info Section (Figma Design: node 985-1365) -->
<div class="apikey-info-section">
<!-- App Icon -->
<div class="detail-item full-width">
<label class="detail-label">앱 아이콘</label>
<div class="detail-value">
<div class="app-icon-display" th:if="${apiKey.appIconFileId != null}">
<img th:src="@{/file/download(fileSn=1,fileId=${apiKey.appIconFileId})}"
alt="앱 아이콘"
class="app-icon-image">
</div>
<div class="app-icon-display" th:unless="${apiKey.appIconFileId != null}">
<div class="app-icon-placeholder">
<span>📱</span>
</div>
</div>
</div>
</div>
<!-- App Name -->
<div class="detail-item">
<label class="detail-label">앱 이름</label>
<div class="detail-value" th:text="${apiKey.clientname}">
My Application
</div>
</div>
<!-- App Status -->
<div class="detail-item">
<label class="detail-label">앱 상태</label>
<div class="detail-value">
<span class="status-indicator"
th:classappend="${apiKey.appstatus == '1' ? 'status-active' : 'status-inactive'}">
<span class="status-dot"></span>
<span th:text="${apiKey.appstatus == '1' ? '활성화' : '비활성화'}">활성화</span>
</span>
</div>
</div>
<!-- App Description -->
<div class="detail-item full-width">
<label class="detail-label">앱 설명</label>
<div class="detail-value" th:text="${apiKey.appDescription != null ? apiKey.appDescription : '설명이 없습니다.'}">
This is a sample app description.
</div>
</div>
<!-- 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>
<!-- Section 2: 인증 상세 정보 -->
<div class="detail-section">
<h2 class="section-title">인증 정보</h2>
<div class="detail-grid">
<!-- 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>
<!-- Client ID -->
<div class="detail-item">
<label class="detail-label">Client ID</label>
<div class="detail-value">
<div class="secret-box">
<code class="credential-code" th:text="${apiKey.clientid}">client-id-12345</code>
<button type="button" class="btn-copy" th:data-secret="${apiKey.clientid}" onclick="copyToClipboardFromButton(this)">
<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">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
복사
</button>
</div>
</div>
</div>
<!-- Client Secret -->
<div class="detail-item">
<label class="detail-label">Client Secret</label>
<div class="detail-value">
<!-- Hidden Secret (shown initially) -->
<div class="secret-box" id="hiddenSecretBox">
<code class="credential-code">••••••••••••••••••••</code>
<button type="button" class="btn-copy" onclick="showPasswordPrompt()">
<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">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
보기
</button>
</div>
<!-- Actual Secret (hidden initially) -->
<div class="secret-box" id="revealedSecretBox" style="display: none;">
<code class="credential-code" th:text="${apiKey.clientsecret}">secret-key-67890</code>
<button type="button" class="btn-copy" th:data-secret="${apiKey.clientsecret}" onclick="copyToClipboardFromButton(this)">
<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">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
복사
</button>
</div>
</div>
</div>
<!-- Callback URL -->
<div class="detail-item full-width">
<label class="detail-label">Callback URL</label>
<div class="detail-value" th:text="${apiKey.redirecturi != null ? apiKey.redirecturi : '설정되지 않음'}">
https://example.com/callback
</div>
</div>
<!-- IP Whitelist -->
<div class="detail-item full-width" th:if="${apiKey.allowedips != null}">
<label class="detail-label">IP 화이트리스트</label>
<div class="detail-value">
<div class="ip-list">
<span class="ip-tag" th:each="ip : ${#strings.arraySplit(apiKey.allowedips, ',')}">
[[${ip.trim()}]]
</span>
</div>
</div>
</div>
<!-- 등록된 API -->
<div class="detail-item full-width">
<label class="detail-label">등록된 API</label>
<div class="detail-value">
<!-- API List -->
<div class="api-detail-list">
<div class="api-list-item" th:each="api : ${apiKey.apiList}">
<span class="api-method method-other">
API
</span>
<span class="api-name" th:text="${api.apiDesc}">API Name</span>
<span class="api-status status-active">활성</span>
</div>
<!-- Empty State -->
<div class="empty-state" th:if="${apiKey.apiList == null or apiKey.apiList.isEmpty()}">
<div class="empty-icon">📦</div>
<p>등록된 API가 없습니다.</p>
</div>
</div>
</div>
</div>
<!-- Modified Date -->
<div class="detail-item">
<label class="detail-label">최종 수정일자</label>
<div class="detail-value" th:text="${#temporals.format(apiKey.modifiedon, 'yyyy.MM.dd HH:mm')}">
2024.01.01 12:00
</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>
</div>
<!-- Action Buttons -->
<div class="form-actions">
<a sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"
class="btn btn-primary"
th:href="@{/myapikey/modify/step1(clientId=${apiKey.clientid})}">
수정
</a>
<button type="button"
sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"
class="btn btn-danger"
th:data-client-id="${apiKey.clientid}"
onclick="deleteApiKeyFromButton(this)">
인증키 삭제
</button>
<a th:href="@{/myapikey}" class="btn btn-secondary">
목록으로
</a>
<!-- 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>
</section>
<!-- 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[*/
<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();
// 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);
// 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
}
});
}
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);
// 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();
});
}
// 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>
/*]]>*/
</script>
</th:block>
@@ -0,0 +1,278 @@
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<body>
<th:block th:fragment="headerFragment">
<!-- Global Header Container -->
<header class="global-header" style="background-color: #E5F8FF;">
<div class="container">
<div class="header-content">
<!-- Desktop Header Layout -->
<div class="desktop-header">
<div class="header-left">
<div class="logo-wrapper">
<a th:href="@{/}" class="logo-link">
<img src="/img/logo/logo.png" alt="광주은행" class="logo" width="114" height="22">
</a>
<a th:href="@{/}" class="logo-text">API Portal</a>
</div>
</div>
<nav class="header-center">
<ul class="nav-menu">
<li><a href="#docs" class="nav-link">서비스 소개</a></li>
<li><a href="/apis" class="nav-link">오픈 API</a></li>
<li>
<a href="#playground" class="nav-link">고객지원</a>
<ul class="sub-menu">
<li><a th:href="@{/portalnotice}">공지사항</a></li>
<li><a th:href="@{/faq_list}">FAQ</a></li>
<li><a th:href="@{/inquiry}">Q&amp;A</a></li>
<li><a th:href="@{/partnership}">사업 제휴 문의</a></li>
</ul>
</li>
</ul>
</nav>
<div class="header-right">
<!-- Login State (Anonymous) -->
<div class="auth-group" sec:authorize="isAnonymous()">
<a th:href="@{/login}" class="login-btn login-btn-box">
<img th:src="@{/img/user_icon.svg}" alt="User" class="user-icon">로그인</a>
</div>
<!-- Logout State (Authenticated) -->
<div class="auth-group authenticated" sec:authorize="isAuthenticated()">
<div class="header-user-info">
<div class="user-identity">
<img th:src="@{/img/user_icon.svg}" alt="User" class="user-icon">
<span class="user-name">[[${#authentication.principal.userName}]]님</span>
</div>
<span class="divider"></span>
<a th:href="@{/actionLogout.do}" class="header-link">로그아웃</a>
<span class="divider"></span>
<div class="mypage-dropdown">
<button class="header-link mypage-toggle" type="button">마이페이지</button>
<div class="mypage-dropdown-menu">
<ul class="mypage-menu-list">
<li sec:authorize="hasRole('ROLE_USER_MANAGER')">
<a th:href="@{/users}"><i class="fas fa-users"></i>이용자 관리</a>
</li>
<li sec:authorize="hasRole('ROLE_APP')">
<a th:href="@{/myapikey}"><i class="fas fa-key"></i>인증 키 관리</a>
<a th:href="@{/commission/manage}"><i class="fas fa-money-bill"></i>과금 관리</a>
</li>
<li><a th:href="@{/mypage}"><i class="fas fa-user-circle"></i>내 정보 관리</a></li>
<li><a th:href="@{/change_password}"><i class="fas fa-lock"></i>비밀번호 변경</a></li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Mobile Header Layout -->
<div class="mobile-header">
<div class="mobile-left">
<a th:href="@{/}" class="mobile-logo-link">
<img src="/img/logo/logo.png" alt="광주은행" class="mobile-logo">
</a>
</div>
<div class="mobile-center">
<h1 class="mobile-title">개발자 포털</h1>
</div>
<div class="mobile-right">
<button class="mobile-menu-btn" id="mobileMenuToggle">
<span class="hamburger-line"></span>
<span class="hamburger-line"></span>
<span class="hamburger-line"></span>
</button>
</div>
</div>
</div>
</div>
</header>
<!-- Mobile Drawer/Modal -->
<div class="mobile-drawer" id="mobileDrawer">
<div class="drawer-overlay" id="drawerOverlay"></div>
<div class="drawer-content">
<div class="drawer-header">
<div class="drawer-logo">
<a th:href="@{/}" class="drawer-logo-link">
<img src="/img/logo/logo.png" alt="광주은행" class="logo">
</a>
<a th:href="@{/}" class="logo-text">개발자 포털</a>
</div>
<button class="drawer-close" id="drawerClose">
<i class="fas fa-times"></i>
</button>
</div>
<!-- Authenticated User Info (Mobile) -->
<div class="drawer-user-info" sec:authorize="isAuthenticated()">
<div class="drawer-profile">
<div class="profile-avatar">
<i class="fas fa-user-circle"></i>
</div>
<div class="profile-info">
<p class="profile-name">[[${#authentication.principal.userName}]]님</p>
<span class="profile-greeting">즐거운 하루 되세요!</span>
</div>
</div>
</div>
<nav class="drawer-nav">
<ul class="drawer-menu">
<li><a href="#docs" class="drawer-link">서비스 소개</a></li>
<li><a href="/apis" class="drawer-link">오픈 API</a></li>
<li><a href="#playground" class="drawer-link">고객지원</a></li>
<!-- Authenticated Menu Items -->
<li sec:authorize="isAuthenticated()">
<a href="#community" class="drawer-link">커뮤니티</a>
</li>
<li sec:authorize="hasRole('ROLE_USER_MANAGER')">
<a th:href="@{/users}" class="drawer-link">
<i class="fas fa-users"></i> 이용자 관리
</a>
</li>
<li sec:authorize="hasRole('ROLE_APP')">
<a th:href="@{/myapikey}" class="drawer-link">
<i class="fas fa-key"></i> 인증 키 관리
</a>
</li>
<li sec:authorize="isAuthenticated()">
<a th:href="@{/mypage}" class="drawer-link">
<i class="fas fa-user-circle"></i> 내 정보 관리
</a>
</li>
<li sec:authorize="isAuthenticated()">
<a th:href="@{/change_password}" class="drawer-link">
<i class="fas fa-lock"></i> 비밀번호 변경
</a>
</li>
</ul>
</nav>
<div class="drawer-actions">
<!-- Anonymous User Actions -->
<div sec:authorize="isAnonymous()">
<a th:href="@{/login}" class="drawer-login-btn">로그인</a>
</div>
<!-- Authenticated User Actions -->
<div sec:authorize="isAuthenticated()">
<a th:href="@{/actionLogout.do}" class="drawer-logout-btn">
<i class="fas fa-sign-out-alt"></i>
로그아웃
</a>
</div>
</div>
</div>
</div>
</th:block>
<!-- Mobile Menu & Dropdown JavaScript -->
<th:block th:fragment="headerScript">
<script>
document.addEventListener('DOMContentLoaded', function() {
// Mobile Drawer
const menuToggle = document.getElementById('mobileMenuToggle');
const drawer = document.getElementById('mobileDrawer');
const drawerClose = document.getElementById('drawerClose');
const drawerOverlay = document.getElementById('drawerOverlay');
function openDrawer() {
drawer.classList.add('active');
document.body.style.overflow = 'hidden';
}
function closeDrawer() {
drawer.classList.remove('active');
document.body.style.overflow = '';
}
menuToggle?.addEventListener('click', openDrawer);
drawerClose?.addEventListener('click', closeDrawer);
drawerOverlay?.addEventListener('click', closeDrawer);
// Close drawer when clicking on nav links
document.querySelectorAll('.drawer-link').forEach(link => {
link.addEventListener('click', closeDrawer);
});
// Mypage Dropdown
const mypageToggle = document.querySelector('.mypage-toggle');
const mypageDropdown = document.querySelector('.mypage-dropdown');
if (mypageToggle && mypageDropdown) {
mypageToggle.addEventListener('click', function(e) {
e.stopPropagation();
mypageDropdown.classList.toggle('active');
});
// Close dropdown when clicking outside
document.addEventListener('click', function(e) {
if (!mypageDropdown.contains(e.target)) {
mypageDropdown.classList.remove('active');
}
});
// Prevent dropdown from closing when clicking inside
mypageDropdown.addEventListener('click', function(e) {
e.stopPropagation();
});
}
// Desktop Navigation Submenu Toggle (Click-based for mobile-like behavior on desktop)
const navItems = document.querySelectorAll('.header-center .nav-menu > li');
navItems.forEach(item => {
const link = item.querySelector(':scope > a');
const submenu = item.querySelector('.sub-menu');
if (submenu) {
// Add click handler for toggle
link.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
// Close other submenus
navItems.forEach(otherItem => {
if (otherItem !== item && otherItem.classList.contains('submenu-open')) {
otherItem.classList.remove('submenu-open');
}
});
// Toggle current submenu
item.classList.toggle('submenu-open');
});
// Close submenu when clicking outside
document.addEventListener('click', function(e) {
if (!item.contains(e.target)) {
item.classList.remove('submenu-open');
}
});
// Prevent submenu from closing when clicking inside
submenu.addEventListener('click', function(e) {
e.stopPropagation();
});
}
});
});
</script>
</th:block>
</body>
</html>
@@ -4,7 +4,7 @@
<body>
<section layout:fragment="title">
<div class="page-title-banner">
<img th:src="@{/img/user_register_title.png}" class="title-image">
<img th:src="@{/img/img_title_bg.png}" class="title-image">
<h1 th:text="${agreementTitle}">비밀번호 변경</h1>
</div>
@@ -9,7 +9,7 @@
<body>
<!-- header -->
<th:block th:replace="fragment/kjbank/header_container :: headerFragment"></th:block>
<th:block th:replace="fragment/kjbank/index_header_container :: headerFragment"></th:block>
<th:block th:replace="fragment/kjbank/header_container :: headerScript"></th:block>
<th:block layout:fragment="contentFragment">