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

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;
@extend .apikey-btn-copy;
}
@media (max-width: $breakpoint-sm) {
align-self: stretch;
justify-content: center;
}
}
// 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,160 +2,159 @@
<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>
</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: 기본 정보 (From Step 1) -->
<div class="detail-section">
<h2 class="section-title">기본 정보</h2>
<div class="detail-grid">
<!-- Title Bar -->
<div class="register-title-bar">
<h2 class="register-title">API Key 신청 상세</h2>
<span class="register-subtitle">신청하신 API Key의 상세 정보를 확인할 수 있습니다.</span>
</div>
<!-- App Basic Info Card (Figma Design) -->
<div class="app-info-card">
<!-- App Icon -->
<div class="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})}"
<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>
<div class="app-icon-display" th:unless="${appRequest.appIconFileId != null}">
<div class="app-icon-placeholder">
<span>📱</span>
<div th:unless="${appRequest.appIconFileId != null}" class="app-icon-placeholder">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21 15 16 10 5 21"></polyline>
</svg>
</div>
</div>
<!-- App Info -->
<div class="app-info-content">
<!-- Status Badge -->
<span class="app-status-badge"
th:if="${appRequest.approval != null}"
th:classappend="${appRequest.approval.approvalStatus.toString() == 'APPROVED' ? 'status-approved' :
(appRequest.approval.approvalStatus.toString() == 'REJECTED' ? 'status-rejected' : 'status-pending')}"
th:text="${appRequest.approval.approvalStatus.description}">
대기중
</span>
<!-- App Name -->
<h3 class="app-info-name" th:text="${appRequest.clientName}">앱 이름</h3>
<!-- App Description -->
<p class="app-info-description"
th:text="${appRequest.appDescription != null ? appRequest.appDescription : '앱 설명이 없습니다.'}">
여기에 입력한 앱 설명이 표시됩니다.
</p>
</div>
</div>
<!-- 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 class="info-row info-row-with-action" th:if="${appRequest.clientId != null}">
<label class="info-label">Client ID</label>
<div class="info-value">
<div class="info-value-box with-copy" th:text="${appRequest.clientId}">K00000001</div>
<button type="button" class="btn-copy-action" th:data-secret="${appRequest.clientId}" onclick="copyToClipboardFromButton(this)">
+ 복사
</button>
</div>
</div>
<!-- Callback URL -->
<div class="detail-item full-width">
<label class="detail-label">Callback URL</label>
<div class="detail-value" th:text="${appRequest.callbackUrl}">
https://example.com/callback
<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="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>
<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="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 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>
<div class="detail-item" th:if="${appRequest.approval != null}">
<label class="detail-label">승인 상태</label>
<div class="detail-value" th:text="${appRequest.approval.approvalStatus.description}">
승인 대기
<!-- 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>
<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}">
<!-- 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>
<!-- 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">
<div class="form-actions-center">
<button type="button"
sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"
class="btn btn-secondary"
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 th:href="@{/myapikey}" class="btn btn-submit btn-secondary">
목록
</a>
</div>
@@ -167,6 +166,38 @@
<script th:inline="javascript">
/*<![CDATA[*/
// Copy to clipboard function
function copyToClipboardFromButton(button) {
var text = $(button).data('secret');
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function() {
alert('클립보드에 복사되었습니다.');
}).catch(function(err) {
console.error('Failed to copy:', err);
fallbackCopy(text);
});
} else {
fallbackCopy(text);
}
}
function fallbackCopy(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
alert('클립보드에 복사되었습니다.');
} catch (err) {
alert('복사에 실패했습니다.');
}
document.body.removeChild(textArea);
}
// Cancel request function - called from button with data-request-id attribute
function cancelRequestById(button) {
var requestId = $(button).data('request-id');
@@ -2,164 +2,132 @@
<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>
</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 1: 앱 기본 정보 -->
<div class="detail-section">
<h2 class="section-title">앱 정보</h2>
<div class="detail-grid">
<!-- Title Bar -->
<div class="register-title-bar">
<h2 class="register-title">앱 정보</h2>
<span class="register-subtitle">등록된 앱의 상세 정보를 확인할 수 있습니다.</span>
</div>
<!-- App Basic Info Card (Figma Design) -->
<div class="app-info-card">
<!-- App Icon -->
<div class="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})}"
<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>
<div class="app-icon-display" th:unless="${apiKey.appIconFileId != null}">
<div class="app-icon-placeholder">
<span>📱</span>
<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>
</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>
<!-- 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>
</div>
</div>
<!-- App Name -->
<h3 class="app-info-name" th:text="${apiKey.clientname}">앱 이름</h3>
<!-- 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.
<p class="app-info-description"
th:text="${apiKey.appDescription != null ? apiKey.appDescription : '앱 설명이 없습니다.'}">
여기에 입력한 앱 설명이 표시됩니다.
</p>
</div>
</div>
</div>
</div>
<!-- Section 2: 인증 상세 정보 -->
<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">
<!-- 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>
복사
<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>
</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>
<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>
<!-- 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>
</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 class="info-row">
<label class="info-label">Callback URL</label>
<div class="info-value">
<div class="info-value-box" th:text="${apiKey.redirecturi != null ? apiKey.redirecturi : '설정되지 않음'}">
http://www.abcd.co.kr
</div>
</div>
</div>
<!-- IP Whitelist -->
<div class="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 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>
<!-- 등록된 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>
<!-- Registered APIs -->
<div class="info-row info-row-vertical">
<label class="info-label">API 목록</label>
<div class="info-value">
<div class="api-list-box">
<div class="api-list-item-figma" th:each="api : ${apiKey.apiList}">
<span class="api-item-name" th:text="${api.apiDesc}">추가된 API 1</span>
<span class="api-item-status">승인</span>
</div>
<!-- Empty State -->
<div class="empty-state" th:if="${apiKey.apiList == null or apiKey.apiList.isEmpty()}">
<div class="empty-icon">📦</div>
<div class="empty-state" th:if="${apiKey.apiList == null or apiKey.apiList.isEmpty()}" style="padding: 24px;">
<p>등록된 API가 없습니다.</p>
</div>
</div>
@@ -167,32 +135,33 @@
</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 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>
</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})}">
수정
<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-danger"
class="btn btn-submit btn-danger"
th:data-client-id="${apiKey.clientid}"
onclick="deleteApiKeyFromButton(this)">
인증키 삭제
</button>
<a th:href="@{/myapikey}" class="btn btn-secondary">
목록으로
<a sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"
class="btn btn-submit btn-primary"
th:href="@{/myapikey/modify/step1(clientId=${apiKey.clientid})}">
수정
</a>
</div>
@@ -306,7 +275,6 @@
});
}
/*]]>*/
</script>
@@ -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">