API 목록, API 상세 페이지 적용

This commit is contained in:
현성필
2025-10-02 14:12:47 +09:00
parent 02cfbf54f3
commit 46657f1744
15 changed files with 2122 additions and 309 deletions
+688
View File
@@ -4060,6 +4060,694 @@ select.form-control {
animation: countUp 0.6s ease-out forwards;
}
.api-market-container {
display: flex;
min-height: 100vh;
background-color: #F8FAFC;
position: relative;
}
.api-market-sidebar {
width: 280px;
background-color: #FFFFFF;
border-right: 1px solid #E2E8F0;
padding: 40px 24px;
position: sticky;
top: 0;
height: 100vh;
overflow-y: auto;
flex-shrink: 0;
}
@media (max-width: 1024px) {
.api-market-sidebar {
width: 240px;
}
}
@media (max-width: 768px) {
.api-market-sidebar {
position: fixed;
left: 0;
top: 60px;
transform: translateX(-100%);
transition: transform 0.3s ease;
box-shadow: 0 8px 24px rgba(75, 155, 255, 0.15);
z-index: 500;
height: calc(100vh - 60px);
}
.api-market-sidebar.mobile-open {
transform: translateX(0);
}
}
.api-sidebar-nav .menu-section {
margin-bottom: 4px;
}
.api-sidebar-nav .menu-title {
padding: 12px 16px;
font-size: 14px;
font-weight: 600;
color: #2E7FF7;
cursor: pointer;
border-radius: 8px;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: space-between;
}
.api-sidebar-nav .menu-title:hover {
background-color: #F8FAFC;
}
.api-sidebar-nav .menu-title.active {
background-color: #EFF6FF;
color: #2E7FF7;
}
.api-sidebar-nav .menu-title .api-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 24px;
height: 24px;
padding: 0 8px;
background-color: rgba(75, 155, 255, 0.1);
color: #4B9BFF;
font-size: 12px;
font-weight: 600;
border-radius: 50px;
transition: all 0.3s ease;
}
.api-sidebar-nav .menu-title.active .api-count {
background-color: #4B9BFF;
color: #FFFFFF;
}
.api-sidebar-nav .api-list {
padding-left: 16px;
margin-top: 4px;
margin-bottom: 8px;
}
.api-sidebar-nav .api-item {
padding: 8px 16px;
font-size: 14px;
color: #64748B;
cursor: pointer;
border-radius: 6px;
transition: all 0.3s ease;
display: flex;
align-items: center;
position: relative;
margin-bottom: 4px;
}
.api-sidebar-nav .api-item::before {
content: "•";
margin-right: 8px;
color: #4B9BFF;
opacity: 0.5;
}
.api-sidebar-nav .api-item:hover {
background-color: rgba(75, 155, 255, 0.05);
color: #1A1A2E;
padding-left: 20px;
}
.api-sidebar-nav .api-item:hover::before {
opacity: 1;
}
.api-sidebar-nav .api-item.active {
background-color: rgba(75, 155, 255, 0.1);
color: #4B9BFF;
font-weight: 500;
}
.api-sidebar-nav .api-item.active::before {
opacity: 1;
}
.api-sidebar-nav .api-item .api-name {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.api-market-content {
flex: 1;
padding: 40px 48px;
min-height: 100vh;
display: flex;
flex-direction: column;
}
@media (max-width: 768px) {
.api-market-content {
padding: 24px 16px;
}
}
.api-mobile-toggle {
display: none;
position: fixed;
bottom: 24px;
right: 24px;
width: 56px;
height: 56px;
background: linear-gradient(135deg, #4B9BFF 0%, #2E7FF7 100%);
border-radius: 50%;
border: none;
color: #FFFFFF;
font-size: 24px;
cursor: pointer;
box-shadow: 0 8px 24px rgba(75, 155, 255, 0.15);
z-index: 501;
transition: all 0.3s ease;
}
.api-mobile-toggle:hover {
transform: scale(1.05);
box-shadow: 0 16px 40px rgba(75, 155, 255, 0.2);
}
.api-mobile-toggle:active {
transform: scale(0.95);
}
@media (max-width: 768px) {
.api-mobile-toggle {
display: flex;
align-items: center;
justify-content: center;
}
}
.api-market-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 40px;
}
@media (max-width: 768px) {
.api-market-header {
flex-direction: column;
align-items: flex-start;
gap: 24px;
}
}
.api-market-title h1 {
font-size: 14px;
color: #64748B;
font-weight: 400;
margin-bottom: 4px;
}
.api-market-title h2 {
font-size: 32px;
font-weight: 700;
color: #1A1A2E;
}
.api-market-search {
position: relative;
width: 300px;
}
@media (max-width: 768px) {
.api-market-search {
width: 100%;
}
}
.api-market-search form {
position: relative;
display: flex;
align-items: center;
}
.api-market-search input {
flex: 1;
padding: 12px 48px 12px 16px;
border: 1px solid #E2E8F0;
border-radius: 12px;
font-size: 14px;
outline: none;
transition: all 0.3s ease;
}
.api-market-search input:focus {
border-color: #4B9BFF;
box-shadow: 0 0 0 3px rgba(75, 155, 255, 0.1);
}
.api-market-search input::placeholder {
color: #94A3B8;
}
.api-market-search .search-submit-btn {
position: absolute;
right: 4px;
top: 50%;
transform: translateY(-50%);
width: 36px;
height: 36px;
background: transparent;
border: none;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
color: #64748B;
}
.api-market-search .search-submit-btn:hover {
background: #EFF6FF;
color: #4B9BFF;
transform: translateY(-50%) scale(1.05);
}
.api-market-search .search-submit-btn:active {
transform: translateY(-50%) scale(0.95);
}
.api-market-search .search-submit-btn .search-icon {
font-size: 18px;
display: block;
}
.api-result-count {
font-size: 16px;
color: #64748B;
margin-bottom: 32px;
}
.api-result-count strong {
color: #4B9BFF;
font-weight: 600;
}
.api-card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 24px;
}
@media (max-width: 768px) {
.api-card-grid {
grid-template-columns: 1fr;
}
}
.api-card {
background: #FFFFFF;
border-radius: 12px;
padding: 32px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
cursor: pointer;
display: flex;
flex-direction: column;
justify-content: space-between;
min-height: 220px;
position: relative;
overflow: hidden;
}
.api-card::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(135deg, #4B9BFF 0%, #2E7FF7 100%);
transform: scaleX(0);
transform-origin: left;
transition: transform 0.3s ease;
}
.api-card:hover {
box-shadow: 0 8px 24px rgba(75, 155, 255, 0.15);
transform: translateY(-5px);
}
.api-card:hover::before {
transform: scaleX(1);
}
.api-card:hover .api-card-icon {
transform: scale(1.1);
}
.api-card:active {
transform: translateY(-3px);
}
.api-card-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
}
.api-card-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background-color: #F8FAFC;
border-radius: 6px;
font-size: 12px;
color: #64748B;
font-weight: 500;
}
.api-card-badge::before {
content: "◉";
color: #4B9BFF;
font-size: 10px;
}
.api-card-title {
font-size: 18px;
font-weight: 600;
color: #1A1A2E;
margin-bottom: 12px;
line-height: 1.2;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.api-card-description {
font-size: 14px;
color: #64748B;
line-height: 1.6;
flex-grow: 1;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.api-card-icon {
text-align: center;
margin-top: 24px;
font-size: 48px;
opacity: 0.8;
transition: transform 0.3s ease;
}
.api-card-icon img {
max-width: 60px;
max-height: 60px;
object-fit: contain;
}
.api-card:nth-child(3n+1) .api-card-icon {
color: #60a5fa;
}
.api-card:nth-child(3n+2) .api-card-icon {
color: #34d399;
}
.api-card:nth-child(3n+3) .api-card-icon {
color: #818cf8;
}
.api-empty-state {
text-align: center;
padding: 80px 24px;
color: #64748B;
}
.api-empty-state .empty-icon {
font-size: 80px;
margin-bottom: 24px;
opacity: 0.5;
}
.api-empty-state h3 {
font-size: 24px;
font-weight: 600;
color: #1A1A2E;
margin-bottom: 16px;
}
.api-empty-state p {
font-size: 16px;
color: #64748B;
}
.api-loading {
display: flex;
justify-content: center;
align-items: center;
padding: 80px;
}
.api-loading .spinner {
width: 48px;
height: 48px;
border: 4px solid #E2E8F0;
border-top-color: #4B9BFF;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.api-mobile-overlay {
display: none;
position: fixed;
top: 60px;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 499;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}
@media (max-width: 768px) {
.api-mobile-overlay {
display: block;
}
}
.api-mobile-overlay.active {
opacity: 1;
pointer-events: auto;
}
.api-detail-content {
display: flex;
flex-direction: column;
gap: 32px;
}
.api-detail-tabs {
display: flex;
gap: 8px;
border-bottom: 2px solid #E2E8F0;
margin-bottom: 32px;
}
.api-detail-tabs .tab-button {
padding: 16px 24px;
background: transparent;
border: none;
border-bottom: 3px solid transparent;
font-size: 16px;
font-weight: 500;
color: #64748B;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
bottom: -2px;
}
.api-detail-tabs .tab-button:hover {
color: #4B9BFF;
background-color: rgba(75, 155, 255, 0.05);
}
.api-detail-tabs .tab-button.active {
color: #4B9BFF;
font-weight: 600;
border-bottom-color: #4B9BFF;
}
.tab-content {
display: none;
flex-direction: column;
gap: 32px;
min-height: 200px;
}
.tab-content.active {
display: flex;
}
.api-overview-card {
background: #FFFFFF;
border-radius: 12px;
padding: 32px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
gap: 24px;
}
.api-overview-card .api-overview-header {
display: flex;
flex-direction: row;
align-items: center;
gap: 16px;
}
@media (max-width: 768px) {
.api-overview-card .api-overview-header {
flex-direction: column;
align-items: flex-start;
}
}
.api-overview-card .api-method-badge {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
padding: 8px 16px;
background: linear-gradient(135deg, #4B9BFF 0%, #2E7FF7 100%);
color: #FFFFFF;
font-size: 14px;
font-weight: 600;
border-radius: 6px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.api-overview-card .api-endpoint {
flex: 1;
}
.api-overview-card .api-endpoint code {
display: block;
padding: 8px 16px;
background-color: #F8FAFC;
border: 1px solid #E2E8F0;
border-radius: 8px;
font-size: 18px;
font-family: "Courier New", monospace;
color: #1A1A2E;
word-break: break-all;
}
.api-overview-card .api-simple-description {
padding: 16px 0;
border-bottom: 1px solid #E2E8F0;
}
.api-overview-card .api-simple-description p {
font-size: 16px;
color: #64748B;
line-height: 1.6;
margin: 0;
}
.api-overview-card .api-overview-info h4 {
font-size: 18px;
font-weight: 600;
color: #1A1A2E;
margin-bottom: 16px;
}
.api-details-grid {
display: flex;
flex-direction: column;
gap: 24px;
}
.api-detail-card {
background: #FFFFFF;
border-radius: 12px;
padding: 32px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.api-detail-card:hover {
box-shadow: 0 4px 12px rgba(75, 155, 255, 0.1);
}
.api-detail-card h3 {
font-size: 20px;
font-weight: 600;
color: #1A1A2E;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 2px solid #EFF6FF;
}
.api-detail-card .detail-content {
font-size: 14px;
color: #64748B;
line-height: 1.6;
}
.api-detail-card .detail-content pre {
background-color: #F8FAFC;
border: 1px solid #E2E8F0;
border-radius: 8px;
padding: 16px;
overflow-x: auto;
margin: 0;
}
.api-detail-card .detail-content pre code {
font-family: "Courier New", monospace;
font-size: 14px;
color: #1A1A2E;
white-space: pre-wrap;
word-break: break-word;
}
.api-detail-card .detail-content table {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
background-color: #FFFFFF;
border: 1px solid #E2E8F0;
border-radius: 8px;
overflow: hidden;
}
.api-detail-card .detail-content table thead {
background-color: #EFF6FF;
}
.api-detail-card .detail-content table th {
text-align: left;
padding: 8px 16px;
font-size: 14px;
font-weight: 600;
color: #1A1A2E;
border-bottom: 2px solid #E2E8F0;
background-color: rgba(75, 155, 255, 0.05);
}
.api-detail-card .detail-content table th:not(:last-child) {
border-right: 1px solid #E2E8F0;
}
.api-detail-card .detail-content table td {
padding: 8px 16px;
font-size: 14px;
color: #64748B;
border-bottom: 1px solid #E2E8F0;
}
.api-detail-card .detail-content table td:not(:last-child) {
border-right: 1px solid #E2E8F0;
}
.api-detail-card .detail-content table tr {
transition: all 0.3s ease;
}
.api-detail-card .detail-content table tr:last-child td {
border-bottom: none;
}
.api-detail-card .detail-content table tr:hover {
background-color: rgba(75, 155, 255, 0.02);
}
.api-detail-card .detail-content table tbody tr:nth-child(even) {
background-color: rgba(248, 250, 252, 0.3);
}
.api-info-table {
width: 100%;
border-collapse: collapse;
}
.api-info-table tr {
border-bottom: 1px solid #E2E8F0;
}
.api-info-table tr:last-child {
border-bottom: none;
}
.api-info-table th {
text-align: left;
padding: 8px 16px;
font-size: 14px;
font-weight: 600;
color: #1A1A2E;
width: 30%;
background-color: #F8FAFC;
}
.api-info-table td {
padding: 8px 16px;
font-size: 14px;
color: #64748B;
}
.api-info-table td code {
display: inline-block;
padding: 2px 8px;
background-color: #F8FAFC;
border-radius: 6px;
font-family: "Courier New", monospace;
font-size: 12px;
color: #4B9BFF;
}
.login-page {
min-height: calc(100vh - 140px);
display: flex;
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: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

+1
View File
@@ -33,6 +33,7 @@
@import 'pages/index';
@import 'pages/home';
@import 'pages/api-portal';
@import 'pages/api-market';
@import 'pages/documentation';
@import 'pages/login';
@@ -0,0 +1,780 @@
// -----------------------------------------------------------------------------
// API Market Page - Modern Sidebar + Card Grid Layout
// Reference: design/api-market.html
// -----------------------------------------------------------------------------
.api-market-container {
display: flex;
min-height: 100vh;
background-color: $gray-bg;
position: relative;
}
// Sidebar Navigation
.api-market-sidebar {
width: 280px;
background-color: $white;
border-right: 1px solid $border-gray;
padding: $spacing-2xl $spacing-lg;
position: sticky;
top: 0;
height: 100vh;
overflow-y: auto;
flex-shrink: 0;
@media (max-width: $breakpoint-md) {
width: 240px;
}
@media (max-width: $breakpoint-sm) {
position: fixed;
left: 0;
top: 60px; // Below mobile header
transform: translateX(-100%);
transition: transform 0.3s ease;
box-shadow: $shadow-lg;
z-index: $z-index-modal;
height: calc(100vh - 60px); // Full height minus header
&.mobile-open {
transform: translateX(0);
}
}
}
.api-sidebar-nav {
.menu-section {
margin-bottom: $spacing-xs;
}
.menu-title {
padding: 12px $spacing-md;
font-size: $font-size-sm;
font-weight: $font-weight-semibold;
color: $secondary-blue;
cursor: pointer;
border-radius: $border-radius-md;
transition: $transition-base;
display: flex;
align-items: center;
justify-content: space-between;
&:hover {
background-color: $gray-bg;
}
&.active {
background-color: $light-bg;
color: $secondary-blue;
}
.api-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 24px;
height: 24px;
padding: 0 $spacing-sm;
background-color: rgba($primary-blue, 0.1);
color: $primary-blue;
font-size: $font-size-xs;
font-weight: $font-weight-semibold;
border-radius: $border-radius-full;
transition: $transition-base;
}
&.active .api-count {
background-color: $primary-blue;
color: $white;
}
}
// Level 2: API List
.api-list {
padding-left: $spacing-md;
margin-top: $spacing-xs;
margin-bottom: $spacing-sm;
}
.api-item {
padding: 8px $spacing-md;
font-size: $font-size-sm;
color: $text-gray;
cursor: pointer;
border-radius: $border-radius-sm;
transition: $transition-base;
display: flex;
align-items: center;
position: relative;
margin-bottom: 4px;
&::before {
content: '';
margin-right: $spacing-sm;
color: $primary-blue;
opacity: 0.5;
}
&:hover {
background-color: rgba($primary-blue, 0.05);
color: $text-dark;
padding-left: calc($spacing-md + 4px);
&::before {
opacity: 1;
}
}
&.active {
background-color: rgba($primary-blue, 0.1);
color: $primary-blue;
font-weight: $font-weight-medium;
&::before {
opacity: 1;
}
}
.api-name {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
// Main Content Area
.api-market-content {
flex: 1;
padding: $spacing-2xl $spacing-3xl;
min-height: 100vh;
display: flex;
flex-direction: column;
@media (max-width: $breakpoint-sm) {
padding: $spacing-lg $spacing-md;
}
}
// Mobile Menu Toggle
.api-mobile-toggle {
display: none;
position: fixed;
bottom: $spacing-lg;
right: $spacing-lg;
width: 56px;
height: 56px;
background: $gradient-primary;
border-radius: $border-radius-circle;
border: none;
color: $white;
font-size: 24px;
cursor: pointer;
box-shadow: $shadow-lg;
z-index: $z-index-modal + 1;
transition: $transition-base;
&:hover {
transform: scale(1.05);
box-shadow: $shadow-xl;
}
&:active {
transform: scale(0.95);
}
@media (max-width: $breakpoint-sm) {
display: flex;
align-items: center;
justify-content: center;
}
}
// Header Section
.api-market-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: $spacing-2xl;
@media (max-width: $breakpoint-sm) {
flex-direction: column;
align-items: flex-start;
gap: $spacing-lg;
}
}
.api-market-title {
h1 {
font-size: $font-size-sm;
color: $text-gray;
font-weight: $font-weight-regular;
margin-bottom: $spacing-xs;
}
h2 {
font-size: $font-size-2xl;
font-weight: $font-weight-bold;
color: $text-dark;
}
}
// Search Box
.api-market-search {
position: relative;
width: 300px;
@media (max-width: $breakpoint-sm) {
width: 100%;
}
form {
position: relative;
display: flex;
align-items: center;
}
input {
flex: 1;
padding: 12px 48px 12px $spacing-md;
border: 1px solid $border-gray;
border-radius: $border-radius-lg;
font-size: $font-size-sm;
outline: none;
transition: $transition-base;
&:focus {
border-color: $primary-blue;
box-shadow: 0 0 0 3px rgba($primary-blue, 0.1);
}
&::placeholder {
color: $text-light;
}
}
.search-submit-btn {
position: absolute;
right: 4px;
top: 50%;
transform: translateY(-50%);
width: 36px;
height: 36px;
background: transparent;
border: none;
border-radius: $border-radius-md;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: $transition-base;
color: $text-gray;
&:hover {
background: $light-bg;
color: $primary-blue;
transform: translateY(-50%) scale(1.05);
}
&:active {
transform: translateY(-50%) scale(0.95);
}
.search-icon {
font-size: 18px;
display: block;
}
}
}
// Result Count
.api-result-count {
font-size: $font-size-base;
color: $text-gray;
margin-bottom: $spacing-xl;
strong {
color: $primary-blue;
font-weight: $font-weight-semibold;
}
}
// Card Grid
.api-card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: $spacing-lg;
@media (max-width: $breakpoint-sm) {
grid-template-columns: 1fr;
}
}
// API Card
.api-card {
background: $white;
border-radius: $border-radius-lg;
padding: $spacing-xl;
box-shadow: $shadow-sm;
transition: $transition-base;
cursor: pointer;
display: flex;
flex-direction: column;
justify-content: space-between;
min-height: 220px;
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: $gradient-primary;
transform: scaleX(0);
transform-origin: left;
transition: transform 0.3s ease;
}
&:hover {
box-shadow: $shadow-lg;
transform: translateY(-5px);
&::before {
transform: scaleX(1);
}
.api-card-icon {
transform: scale(1.1);
}
}
&:active {
transform: translateY(-3px);
}
}
.api-card-header {
display: flex;
align-items: center;
gap: $spacing-sm;
margin-bottom: $spacing-md;
}
.api-card-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background-color: $gray-bg;
border-radius: $border-radius-sm;
font-size: $font-size-xs;
color: $text-gray;
font-weight: $font-weight-medium;
&::before {
content: "";
color: $primary-blue;
font-size: 10px;
}
}
.api-card-title {
font-size: $font-size-md;
font-weight: $font-weight-semibold;
color: $text-dark;
margin-bottom: 12px;
line-height: $line-height-tight;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.api-card-description {
font-size: $font-size-sm;
color: $text-gray;
line-height: $line-height-normal;
flex-grow: 1;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.api-card-icon {
text-align: center;
margin-top: $spacing-lg;
font-size: 48px;
opacity: 0.8;
transition: transform 0.3s ease;
img {
max-width: 60px;
max-height: 60px;
object-fit: contain;
}
}
// Icon color variations
.api-card:nth-child(3n+1) .api-card-icon {
color: #60a5fa;
}
.api-card:nth-child(3n+2) .api-card-icon {
color: #34d399;
}
.api-card:nth-child(3n+3) .api-card-icon {
color: #818cf8;
}
// Empty State
.api-empty-state {
text-align: center;
padding: $spacing-5xl $spacing-lg;
color: $text-gray;
.empty-icon {
font-size: 80px;
margin-bottom: $spacing-lg;
opacity: 0.5;
}
h3 {
font-size: $font-size-xl;
font-weight: $font-weight-semibold;
color: $text-dark;
margin-bottom: $spacing-md;
}
p {
font-size: $font-size-base;
color: $text-gray;
}
}
// Loading State
.api-loading {
display: flex;
justify-content: center;
align-items: center;
padding: $spacing-5xl;
.spinner {
width: 48px;
height: 48px;
border: 4px solid $border-gray;
border-top-color: $primary-blue;
border-radius: $border-radius-circle;
animation: spin 1s linear infinite;
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
// Mobile Overlay
.api-mobile-overlay {
display: none;
position: fixed;
top: 60px; // Below mobile header
left: 0;
right: 0;
bottom: 0;
background-color: rgba($black, 0.5);
z-index: $z-index-modal - 1;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
@media (max-width: $breakpoint-sm) {
display: block;
}
&.active {
opacity: 1;
pointer-events: auto;
}
}
// API Detail Content
.api-detail-content {
display: flex;
flex-direction: column;
gap: $spacing-xl;
}
// Tab Navigation
.api-detail-tabs {
display: flex;
gap: $spacing-sm;
border-bottom: 2px solid $border-gray;
margin-bottom: $spacing-xl;
.tab-button {
padding: $spacing-md $spacing-lg;
background: transparent;
border: none;
border-bottom: 3px solid transparent;
font-size: $font-size-base;
font-weight: $font-weight-medium;
color: $text-gray;
cursor: pointer;
transition: $transition-base;
position: relative;
bottom: -2px;
&:hover {
color: $primary-blue;
background-color: rgba($primary-blue, 0.05);
}
&.active {
color: $primary-blue;
font-weight: $font-weight-semibold;
border-bottom-color: $primary-blue;
}
}
}
// Tab Content
.tab-content {
display: none;
flex-direction: column;
gap: $spacing-xl;
min-height: 200px;
&.active {
display: flex;
}
}
// API Overview Card
.api-overview-card {
background: $white;
border-radius: $border-radius-lg;
padding: $spacing-xl;
box-shadow: $shadow-sm;
display: flex;
flex-direction: column;
gap: $spacing-lg;
.api-overview-header {
display: flex;
flex-direction: row;
align-items: center;
gap: $spacing-md;
@media (max-width: $breakpoint-sm) {
flex-direction: column;
align-items: flex-start;
}
}
.api-method-badge {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
padding: 8px 16px;
background: $gradient-primary;
color: $white;
font-size: $font-size-sm;
font-weight: $font-weight-semibold;
border-radius: $border-radius-sm;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.api-endpoint {
flex: 1;
code {
display: block;
padding: $spacing-sm $spacing-md;
background-color: $gray-bg;
border: 1px solid $border-gray;
border-radius: $border-radius-md;
font-size: $font-size-md;
font-family: 'Courier New', monospace;
color: $text-dark;
word-break: break-all;
}
}
.api-simple-description {
padding: $spacing-md 0;
border-bottom: 1px solid $border-gray;
p {
font-size: $font-size-base;
color: $text-gray;
line-height: $line-height-normal;
margin: 0;
}
}
.api-overview-info {
h4 {
font-size: $font-size-md;
font-weight: $font-weight-semibold;
color: $text-dark;
margin-bottom: $spacing-md;
}
}
}
// API Details Grid
.api-details-grid {
display: flex;
flex-direction: column;
gap: $spacing-lg;
}
// API Detail Card
.api-detail-card {
background: $white;
border-radius: $border-radius-lg;
padding: $spacing-xl;
box-shadow: $shadow-sm;
transition: $transition-base;
&:hover {
box-shadow: $shadow-md;
}
h3 {
font-size: $font-size-lg;
font-weight: $font-weight-semibold;
color: $text-dark;
margin-bottom: $spacing-md;
padding-bottom: $spacing-sm;
border-bottom: 2px solid $light-bg;
}
.detail-content {
font-size: $font-size-sm;
color: $text-gray;
line-height: $line-height-normal;
pre {
background-color: $gray-bg;
border: 1px solid $border-gray;
border-radius: $border-radius-md;
padding: $spacing-md;
overflow-x: auto;
margin: 0;
code {
font-family: 'Courier New', monospace;
font-size: $font-size-sm;
color: $text-dark;
white-space: pre-wrap;
word-break: break-word;
}
}
// Table styling for HTML tables in specifications
table {
width: 100%;
border-collapse: collapse;
margin: $spacing-md 0;
background-color: $white;
border: 1px solid $border-gray;
border-radius: $border-radius-md;
overflow: hidden;
thead {
background-color: $light-bg;
}
th {
text-align: left;
padding: $spacing-sm $spacing-md;
font-size: $font-size-sm;
font-weight: $font-weight-semibold;
color: $text-dark;
border-bottom: 2px solid $border-gray;
background-color: rgba($primary-blue, 0.05);
&:not(:last-child) {
border-right: 1px solid $border-gray;
}
}
td {
padding: $spacing-sm $spacing-md;
font-size: $font-size-sm;
color: $text-gray;
border-bottom: 1px solid $border-gray;
&:not(:last-child) {
border-right: 1px solid $border-gray;
}
}
tr {
transition: $transition-base;
&:last-child td {
border-bottom: none;
}
&:hover {
background-color: rgba($primary-blue, 0.02);
}
}
tbody tr:nth-child(even) {
background-color: rgba($gray-bg, 0.3);
}
}
}
}
// API Info Table
.api-info-table {
width: 100%;
border-collapse: collapse;
tr {
border-bottom: 1px solid $border-gray;
&:last-child {
border-bottom: none;
}
}
th {
text-align: left;
padding: $spacing-sm $spacing-md;
font-size: $font-size-sm;
font-weight: $font-weight-semibold;
color: $text-dark;
width: 30%;
background-color: $gray-bg;
}
td {
padding: $spacing-sm $spacing-md;
font-size: $font-size-sm;
color: $text-gray;
code {
display: inline-block;
padding: 2px 8px;
background-color: $gray-bg;
border-radius: $border-radius-sm;
font-family: 'Courier New', monospace;
font-size: $font-size-xs;
color: $primary-blue;
}
}
}
@@ -1,204 +1,454 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kbank_api_layout}">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/kjbank_base_layout}">
<body>
<section layout:fragment="contentFragment" class="content api_con">
<style>
/* Custom dark theme for CodeMirror */
.cm-s-custom-dark.CodeMirror {
background-color: #252B37;
color: white;
}
<th:block layout:fragment="contentFragment">
<section class="api-market-container">
.cm-s-custom-dark .CodeMirror-gutters {
background-color: #1a1a1a;
border-right: 1px solid #3a3a3a;
}
.cm-s-custom-dark .CodeMirror-linenumber {
color: #777;
}
.cm-s-custom-dark .CodeMirror-cursor {
border-left: 1px solid white;
}
.cm-s-custom-dark .cm-string {
color: #a6e22e;
}
.cm-s-custom-dark .cm-number {
color: #ae81ff;
}
.cm-s-custom-dark .cm-atom {
color: #f92672;
}
.cm-s-custom-dark .cm-keyword {
color: #66d9ef;
}
.cm-s-custom-dark .cm-property {
color: #ccffee;
}
.cm-s-custom-dark .cm-punctuation {
color: #f8f8f2;
}
</style>
<div class="content_wrap w_api">
<div class="sub_title4">
<h2 class="title" th:text="${apiSpecInfo.apiName}"></h2>
<p class="infotxt" th:text="${apiSpecInfo.apiSimpleDescription}"></p>
</div>
<div class="pc-only">
<div class="inner i_cs h_inner2">
<div class="api_stit">
<table class="api_table" cellspacing="0" cellpadding="0" width="100%">
<colgroup>
<col width="200px">
<col width="auto">
</colgroup>
<tbody>
<tr>
<th>API 명 (URI)</th>
<td th:text="${apiSpecInfo.apiUrl}"></td>
</tr>
<tr>
<th>Method</th>
<td>[[${apiSpecInfo.apiMethod}]]</td>
</tr>
<tr>
<th>Content-Type</th>
<td>[[${apiSpecInfo.apiContentType}]]</td>
</tr>
</tbody>
</table>
<div class="api_stit api_dot" th:utext="${apiSpecInfo.description}">
<!-- Sidebar Navigation -->
<aside class="api-market-sidebar" id="apiSidebar">
<nav class="api-sidebar-nav">
<!-- All APIs -->
<div class="menu-section">
<div class="menu-title" th:classappend="${selected == '-1'} ? 'active'" data-group="">
전체
<span class="api-count" th:text="${totalApiCount}">0</span>
</div>
</div>
</div>
</div>
<div class="inner i_cs h_inner2">
<div class="api_stit m-only">
<table class="api_table" cellspacing="0" cellpadding="0" width="100%">
<colgroup>
<col width="110px">
<col width="auto">
</colgroup>
<tbody>
<tr>
<th>API 명 (URI)</th>
<td th:text="${apiSpecInfo.apiUrl}"></td>
</tr>
<tr>
<th>Method</th>
<td>[[${apiSpecInfo.apiMethod}]]</td>
</tr>
<tr>
<th>Content-Type</th>
<td>[[${apiSpecInfo.apiContentType}]]</td>
</tr>
</tbody>
</table>
<div class="api_stit api_dot" th:utext="${apiSpecInfo.description}">
<!-- Service Categories -->
<div class="menu-section" th:each="service : ${services}">
<div class="menu-title"
th:classappend="${selected == service.id} ? 'active'"
th:data-group="${service.id}">
[[${service.groupName}]]
<span class="api-count" th:text="${service.apiGroupApiList.size()}">0</span>
</div>
<!-- Level 2: API List -->
<div class="api-list" th:if="${service.apiGroupApiList != null and !service.apiGroupApiList.isEmpty()}">
<div class="api-item"
th:each="api : ${service.apiGroupApiList}"
th:data-api-id="${api.apiId}"
th:attr="data-href=@{/apis/detail(apiId=${api.apiId})}">
<span class="api-name" th:text="${api.apiDesc}">API Name</span>
</div>
</div>
</div>
</nav>
</aside>
<!-- Mobile Menu Toggle -->
<button class="api-mobile-toggle" id="mobileToggle" aria-label="메뉴 열기">
</button>
<!-- Mobile Overlay -->
<div class="api-mobile-overlay" id="mobileOverlay"></div>
<!-- Main Content -->
<main class="api-market-content">
<!-- Header -->
<div class="api-market-header">
<div class="api-market-title">
<h1>서비스</h1>
<h2 th:text="${apiSpecInfo.apiName}">API 이름</h2>
</div>
</div>
<!-- API Content -->
<div class="api-detail-content">
<div class="api_stit api_sc api_tb_type api_sc2">
<h3>요청 메시지</h3>
<div class="pc-only" th:utext="${apiSpecInfo.apiRequestSpec}">
<!-- Tab Navigation -->
<div class="api-detail-tabs">
<button class="tab-button active" data-tab="api-info">API 정보</button>
<button class="tab-button" data-tab="testbed">테스트베드</button>
</div>
<div class="m-only" th:utext="${apiSpecInfo.apiRequestSpec}">
<!-- Tab Content -->
<div class="tab-content active" id="api-info-tab">
<!-- API Overview Card (Merged with Additional Information) -->
<div class="api-overview-card">
<div class="api-overview-header">
<div class="api-method-badge" th:text="${apiSpecInfo.apiMethod}">GET</div>
<div class="api-endpoint">
<code th:text="${apiSpecInfo.apiUrl}">/api/v1/example</code>
</div>
</div>
<!-- API Information Table -->
<div class="api-overview-info">
<table class="api-info-table">
<tr th:if="${apiSpecInfo.apiContentType != null}">
<th>Content Type</th>
<td th:text="${apiSpecInfo.apiContentType}">application/json</td>
</tr>
</table>
</div>
<div class="api-simple-description" th:if="${apiSpecInfo.apiSimpleDescription != null}">
<p th:text="${apiSpecInfo.apiSimpleDescription}">Simple API description</p>
</div>
<div class="detail-content" th:utext="${apiSpecInfo.description}">
Detailed API description
</div>
</div>
</div>
<div class="api_stit api_sc">
<h3>요청 메시지 예제</h3>
<ul>
<li>
<p class="tit">
<span>Header</span>
</p>
<div class="api_sauce">
<p>
Content-Type: application/json
</p>
<!-- API Details Grid -->
<div class="api-details-grid">
<!-- Request Specification -->
<div class="api-detail-card" th:if="${apiSpecInfo.apiRequestSpec != null}">
<h3>Request Specification</h3>
<div class="detail-content" th:utext="${apiSpecInfo.apiRequestSpec}">
Request spec
</div>
</li>
</ul>
</div>
<div class="api_stit api_sc api_sc3">
<ul>
<li>
<p class="tit">
<span>Body</span>
</p>
<div class="api_sauce">
<textarea id="sampleRequest" th:text="${apiSpecInfo.sampleRequest}"></textarea>
</div>
<!-- Sample Request -->
<div class="api-detail-card" th:if="${apiSpecInfo.sampleRequest != null}">
<h3>샘플 요청</h3>
<div class="detail-content">
<pre><code th:text="${apiSpecInfo.sampleRequest}">Sample request</code></pre>
</div>
</li>
</ul>
</div>
<!-- Response Specification -->
<div class="api-detail-card" th:if="${apiSpecInfo.apiResponseSpec != null}">
<h3>Response Specification</h3>
<div class="detail-content" th:utext="${apiSpecInfo.apiResponseSpec}">
Response spec
</div>
</div>
<!-- Sample Response -->
<div class="api-detail-card" th:if="${apiSpecInfo.sampleResponse != null}">
<h3>샘플 응답</h3>
<div class="detail-content">
<pre><code th:text="${apiSpecInfo.sampleResponse}">Sample response</code></pre>
</div>
</div>
</div>
</div>
<!-- Testbed Tab Content -->
<div class="tab-content" id="testbed-tab">
<link rel="stylesheet" type="text/css" th:href="@{/plugins/swaggerUI/swagger-ui.css}"/>
<link rel="stylesheet" type="text/css" th:href="@{/plugins/swaggerUI/index.css}"/>
<div class="api-overview-card">
<div id="swagger-ui"></div>
</div>
</div>
</div>
<div class="api_line"></div>
</main>
</section>
</th:block>
<div class="api_stit api_sc api_sc2 api_tb_type">
<h3>응답 메시지</h3>
<div class="pc-only" th:utext="${apiSpecInfo.apiResponseSpec}"></div>
<div class="m-only" th:utext="${apiSpecInfo.apiResponseSpec}"></div>
</div>
<div class="api_stit api_sc">
<h3>응답 메시지 예제</h3>
<ul>
<li>
<p class="tit">
<span>Body</span>
</p>
<div class="api_sauce">
<textarea id="sampleResponse" th:text="${apiSpecInfo.sampleResponse}"></textarea>
</div>
</li>
</ul>
</div>
<div class="api_btn">
<a th:href="@{/apis/testbed/api(id=${apiSpecInfo.apiId})}" class="common_btn_type_1 gray">테스트</a>
<div class="btn_gap"></div>
<a href="#none" class="common_btn_type_1" id="requestApiKey">API 이용 신청</a>
</div>
</div>
</div>
</section>
</body>
<th:block layout:fragment="contentScript">
<script>
document.addEventListener('DOMContentLoaded', (event) => {
const commonOptions = {
lineNumbers: false,
mode: {name: 'javascript', json: true},
theme: 'custom-dark',
readOnly: true,
autoCloseBrackets: true,
matchBrackets: true,
foldGutter: true
};
const sampleRequestEditor = CodeMirror.fromTextArea(document.getElementById('sampleRequest'), commonOptions);
<script th:inline="javascript">
document.addEventListener('DOMContentLoaded', function() {
// DOM Elements
const sidebar = document.getElementById('apiSidebar');
const mobileToggle = document.getElementById('mobileToggle');
const mobileOverlay = document.getElementById('mobileOverlay');
const sampleResponseEditor = CodeMirror.fromTextArea(document.getElementById('sampleResponse'), commonOptions);
// API Card Click Handlers
const apiCards = document.querySelectorAll('.api-card');
apiCards.forEach(function(card) {
// Click handler
card.addEventListener('click', function() {
const href = this.getAttribute('data-href');
if (href) {
window.location.href = href;
}
});
// Refresh CodeMirror instances to ensure proper rendering
sampleRequestEditor.refresh();
sampleResponseEditor.refresh();
// Keyboard accessibility
card.addEventListener('keypress', function(e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
const href = this.getAttribute('data-href');
if (href) {
window.location.href = href;
}
}
});
});
// Level 2 API Item Click Handlers
const apiItems = document.querySelectorAll('.api-item');
apiItems.forEach(function(item) {
// Click handler
item.addEventListener('click', function(e) {
e.stopPropagation(); // Prevent parent menu-title click
const href = this.getAttribute('data-href');
if (href) {
window.location.href = href;
}
});
// Keyboard accessibility
item.addEventListener('keypress', function(e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
e.stopPropagation();
const href = this.getAttribute('data-href');
if (href) {
window.location.href = href;
}
}
});
});
// Category/Service Selection - removed as not needed in detail page
// Mobile Menu Toggle
if (mobileToggle && sidebar && mobileOverlay) {
mobileToggle.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
sidebar.classList.toggle('mobile-open');
mobileOverlay.classList.toggle('active');
// Update button text
if (sidebar.classList.contains('mobile-open')) {
this.innerHTML = '✕';
this.setAttribute('aria-label', '메뉴 닫기');
} else {
this.innerHTML = '☰';
this.setAttribute('aria-label', '메뉴 열기');
}
});
// Add touch event for better mobile support
mobileToggle.addEventListener('touchend', function(e) {
e.preventDefault();
e.stopPropagation();
this.click();
});
// Close menu on overlay click
mobileOverlay.addEventListener('click', function() {
sidebar.classList.remove('mobile-open');
this.classList.remove('active');
mobileToggle.innerHTML = '☰';
mobileToggle.setAttribute('aria-label', '메뉴 열기');
});
} else {
console.warn('Mobile toggle elements not found:', {
mobileToggle: !!mobileToggle,
sidebar: !!sidebar,
mobileOverlay: !!mobileOverlay
});
}
// Close mobile menu on resize to desktop
window.addEventListener('resize', function() {
if (window.innerWidth > 768 && sidebar.classList.contains('mobile-open')) {
sidebar.classList.remove('mobile-open');
mobileOverlay.classList.remove('active');
mobileToggle.innerHTML = '☰';
mobileToggle.setAttribute('aria-label', '메뉴 열기');
}
});
// Tab functionality
const tabButtons = document.querySelectorAll('.tab-button');
const tabContents = document.querySelectorAll('.tab-content');
tabButtons.forEach(function(button) {
button.addEventListener('click', function() {
const targetTab = this.getAttribute('data-tab');
// Remove active class from all buttons and contents
tabButtons.forEach(function(btn) {
btn.classList.remove('active');
});
tabContents.forEach(function(content) {
content.classList.remove('active');
});
// Add active class to clicked button and corresponding content
this.classList.add('active');
document.getElementById(targetTab + '-tab').classList.add('active');
});
});
// Swagger UI functionality for testbed tab
let swaggerInitialized = false;
const currentApiId = /*[[${apiSpecInfo.apiId}]]*/ 'default';
// Initialize Swagger UI when testbed tab is clicked
tabButtons.forEach(function(button) {
button.addEventListener('click', function() {
const targetTab = this.getAttribute('data-tab');
if (targetTab === 'testbed' && !swaggerInitialized) {
initializeSwagger(currentApiId);
swaggerInitialized = true;
}
});
});
function initializeSwagger(apiId) {
if (!apiId || apiId === 'default') {
document.getElementById('swagger-ui').innerHTML = '<p>API 정보를 불러올 수 없습니다.</p>';
return;
}
const swaggerUrl = `/api/apis/${apiId}/swagger.json`;
// Load Swagger UI scripts dynamically
const loadScript = (src) => {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = src;
script.onload = resolve;
script.onerror = reject;
document.body.appendChild(script);
});
};
// Load required scripts
Promise.all([
loadScript(/*[[@{/plugins/swaggerUI/swagger-ui-bundle.js}]]*/ ''),
loadScript(/*[[@{/plugins/swaggerUI/swagger-ui-standalone-preset.js}]]*/ '')
]).then(() => {
// Load code template script
return loadScript(/*[[@{/js/code_template.js}]]*/ '');
}).then(() => {
// Initialize Swagger UI
const generateSnippet = (parsedRequest, language) => {
let result;
try {
$.ajax({
url: /*[[@{/api/sample_code/}]]*/ '' + language,
type: 'POST',
async: false,
contentType: 'application/json',
headers: {
'X-XSRF-TOKEN': /*[[${_csrf.token}]]*/ ''
},
data: JSON.stringify(parsedRequest),
success: function(data) {
result = data;
},
error: function(jqXHR, textStatus, errorThrown) {
console.error('Error:', errorThrown);
}
});
} catch (error) {
console.error('Error:', error);
}
return result;
};
const SnippetGeneratorPlugin = {
fn: {
requestSnippetGenerator_csharp: (request) => {
var parsedRequest = parseSwaggerRequest(request);
return generateSnippet(parsedRequest, 'csharp');
},
requestSnippetGenerator_ecma5: (request) => {
var parsedRequest = parseSwaggerRequest(request);
return generateSnippet(parsedRequest, 'ecma5');
},
requestSnippetGenerator_java: (request) => {
var parsedRequest = parseSwaggerRequest(request);
return generateSnippet(parsedRequest, 'java');
},
requestSnippetGenerator_python: (request) => {
var parsedRequest = parseSwaggerRequest(request);
return generateSnippet(parsedRequest, 'python');
}
}
};
// Clear existing Swagger UI
document.getElementById('swagger-ui').innerHTML = '';
// Initialize Swagger UI
const ui = SwaggerUIBundle({
url: swaggerUrl,
dom_id: '#swagger-ui',
requestInterceptor: function(req) {
if (!req.loadSpec) {
const urlPath = new URL(req.url).pathname;
const method = req.method.toLowerCase();
const spec = ui.spec();
const specString = JSON.stringify(spec);
const specData = JSON.parse(specString);
if (specData && specData.json && specData.json.paths && specData.json.paths[urlPath]) {
const operation = specData.json.paths[urlPath][method];
if (operation && operation['x-original-api-id']) {
req.headers['original-api-id'] = operation['x-original-api-id'];
}
}
req.headers['original-url'] = req.url;
req.headers['X-XSRF-TOKEN'] = /*[[${_csrf.token}]]*/ '';
if ($('#apps').val() != null) {
req.headers['client-id'] = $('#apps').val();
}
req.url = window.location.origin + /*[[@{/api/call-api}]]*/ '';
}
return req;
},
showMutatedRequest: false,
validatorUrl: '',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset.slice(1)
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl,
SnippetGeneratorPlugin
],
layout: 'StandaloneLayout',
requestSnippetsEnabled: true,
requestSnippets: {
generators: {
'csharp': {
title: 'C#',
syntax: 'csharp'
},
'ecma5': {
title: 'ECMA5',
syntax: 'javascript'
},
'java': {
title: 'Java',
syntax: 'java'
},
'python': {
title: 'Python',
syntax: 'python'
}
},
defaultExpanded: true,
languages: null
}
});
window.ui = ui;
}).catch(error => {
console.error('Failed to load Swagger UI:', error);
document.getElementById('swagger-ui').innerHTML = '<p>Swagger UI를 불러오는 중 오류가 발생했습니다.</p>';
});
}
});
</script>
</th:block>
</html>
@@ -1,153 +1,250 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kbank_base_layout}">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/kjbank_base_layout}">
<body>
<section layout:fragment="contentFragment" class="content">
<th:block layout:fragment="contentFragment">
<section class="api-market-container">
<div class="content_wrap">
<div class="api_service_type m-only">
<img th:src="@{/img/bg_api_visual03.png}" alt="Kbank API 서비스">
<div class="service_txt">
<p>Kbank API 서비스</p>
<span>
API를 활용해 당신의 비즈니스 모델을<br/>
혁신하여 창의적이고 차별화된 서비스를<br/>
제작하세요.
</span>
</div>
</div>
<div class="api_service_type pc-only">
<img th:src="@{/img/bg_api_visual02.png}" alt="Kbank API 서비스">
<div class="service_txt">
<p>Kbank API 서비스</p>
<span>
Kbank가 제공하는 다양한 API를 소개합니다.<br/>
API를 활용해 당신의 비즈니스 모델을 혁신하여 창의적이고<br/>
차별화된 서비스를 제작하세요.
</span>
</div>
</div>
<div class="api_inner api_pd">
<div class="main_keyword">
<div class="search_type2">
<form id="searchForm" th:action="@{/apis}" method="get">
<div class="search-container2">
<input type="hidden" name="groupIds" th:value="${#strings.replace(search.groupIds, '[', '').replace(']', '')}"/>
<input type="text" class="search-box2" name="keyword" th:value="${search.keyword}" placeholder="어떤 API를 찾고 계신가요?">
<button class="search-button2" type="submit"></button>
<!-- Sidebar Navigation -->
<aside class="api-market-sidebar" id="apiSidebar">
<nav class="api-sidebar-nav">
<!-- All APIs -->
<div class="menu-section">
<div class="menu-title" th:classappend="${selected == '-1'} ? 'active'" data-group="">
전체
<span class="api-count" th:text="${totalApiCount}">0</span>
</div>
</form>
</div>
<!-- Service Categories -->
<div class="menu-section" th:each="service : ${services}">
<div class="menu-title"
th:classappend="${selected == service.id} ? 'active'"
th:data-group="${service.id}">
[[${service.groupName}]]
<span class="api-count" th:text="${service.apiGroupApiList.size()}">0</span>
</div>
</div>
</nav>
</aside>
<!-- Mobile Menu Toggle -->
<button class="api-mobile-toggle" id="mobileToggle" aria-label="메뉴 열기">
</button>
<!-- Mobile Overlay -->
<div class="api-mobile-overlay" id="mobileOverlay"></div>
<!-- Main Content -->
<main class="api-market-content">
<!-- Header -->
<div class="api-market-header">
<div class="api-market-title">
<h1>오픈API</h1>
<h2>API 마켓</h2>
</div>
<div class="api-market-search">
<form id="searchForm" th:action="@{/apis}" method="get">
<input type="hidden" name="groupIds" th:value="${search.groupIds != null and !search.groupIds.isEmpty() ? search.groupIds[0] : ''}"/>
<input type="text"
class="search-input"
name="keyword"
th:value="${search.keyword}"
placeholder="어떤 API를 찾고 계신가요?">
<button type="submit" class="search-submit-btn" aria-label="검색">
<span class="search-icon">🔍</span>
</button>
</form>
</div>
</div>
<div class="api_keyword">
<ul class="keyword_list tab_list grt">
<li>
<a href="#">전체<span class="number" th:text="${selectedApiCount}" th:classppend="${selected == '-1'}?'active'"></span></a>
</li>
<li th:each="item : ${services}">
<a href="#" th:data-group="${item.id}" th:classappend="${selected == item.id}?'active'">
[[${item.groupName}]]<span class="number" th:text="${item.apiGroupApiList.size()}"></span>
</a>
</li>
</ul>
<!-- Result Count -->
<div class="api-result-count">
<strong th:text="${selectedApiCount}">0</strong>
</div>
<div class="m-only">
<div class="api_info_container card_top">
<div class="api_box" th:each="api : ${apis}" th:data-href="@{/apis/detail(id=${api.apiId})}">
<div class="api_info_box">
<div class="api_list" th:data-href="@{/apis/detail(id=${api.apiId})}">
<p class="txt1" th:text="${api.apiName}"></p>
<p class="txt2" th:text="${api.apiSimpleDescription}"></p>
</div>
<div class="api_img" style="height: 48px;">
<img th:if="${#strings.isEmpty(api.mainIcon) == false}" th:src="${api.mainIcon}" alt="">
</div>
<!-- API Cards Grid -->
<div class="api-card-grid" th:if="${apis != null and !apis.isEmpty()}">
<div class="api-card"
th:each="api : ${apis}"
th:data-href="@{/apis/detail(id=${api.apiId})}"
role="button"
tabindex="0">
<div>
<!-- Card Header with Badge -->
<div class="api-card-header">
<span class="api-card-badge" th:if="${api.apiGroupName}" th:text="${api.apiGroupName}">
카테고리
</span>
</div>
<!-- Card Title -->
<h3 class="api-card-title" th:text="${api.apiName}">
API 이름
</h3>
<!-- Card Description -->
<p class="api-card-description" th:text="${api.apiSimpleDescription ?: 'API 설명이 없습니다.'}">
API 설명
</p>
</div>
<!-- Card Icon -->
<div class="api-card-icon">
<img th:if="${api.mainIcon}"
th:src="${api.mainIcon}"
th:alt="${api.apiName}"
onerror="this.style.display='none'; this.parentElement.innerHTML='📄'">
<span th:if="${#strings.isEmpty(api.mainIcon)}">📄</span>
</div>
</div>
</div>
<div class="pc-only">
<div class="ser_result" ><span>[[${selectedApiCount}]]건</span> 검색</div>
<div class="api_info_container box_list mt_0">
<div class="api_box" th:each="api : ${apis}" th:data-href="@{/apis/detail(id=${api.apiId})}">
<div class="api_info_box pc-only">
<div class="api_list" th:data-href="@{/apis/detail(id=${api.apiId})}">
<p class="txt1" th:text="${api.apiName}"></p>
<p class="txt2" th:text="${api.apiSimpleDescription}"></p>
</div>
<div class="api_img">
<a th:href="@{/apis/detail(id=${api.apiId})}" class="btn btn-primary"><img th:if="${#strings.isEmpty(api.mainIcon) == false}" th:src="${api.mainIcon}" alt=""></a>
</div>
</div>
</div>
</div>
<!-- Empty State -->
<div class="api-empty-state" th:if="${apis == null or apis.isEmpty()}">
<div class="empty-icon">🔍</div>
<h3>검색 결과가 없습니다</h3>
<p>다른 키워드로 검색하거나 카테고리를 변경해 보세요.</p>
</div>
</div>
</div>
</div>
</section>
</main>
</section>
</th:block>
</body>
<th:block layout:fragment="contentScript">
<script>
document.addEventListener('DOMContentLoaded', (event) => {
const apiListItems = document.querySelectorAll('.api_list');
apiListItems.forEach(function(item) {
item.addEventListener('click', function() {
var href = this.getAttribute('data-href');
if (href) {
window.location.href = href;
}
});
});
<script th:inline="javascript">
document.addEventListener('DOMContentLoaded', function() {
// DOM Elements
const sidebar = document.getElementById('apiSidebar');
const mobileToggle = document.getElementById('mobileToggle');
const mobileOverlay = document.getElementById('mobileOverlay');
const searchForm = document.getElementById('searchForm');
const groupIdsInput = searchForm.querySelector('input[name="groupIds"]');
const searchInput = searchForm.querySelector('input[name="keyword"]');
const apiItems = document.querySelectorAll('.api_box');
apiItems.forEach(function(item) {
item.removeEventListener('click',null);
item.addEventListener('click', function() {
// API Card Click Handlers
const apiCards = document.querySelectorAll('.api-card');
apiCards.forEach(function(card) {
// Click handler
card.addEventListener('click', function() {
const href = this.getAttribute('data-href');
if (href) {
window.location.href = href;
}
});
// Keyboard accessibility
card.addEventListener('keypress', function(e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
const href = this.getAttribute('data-href');
if (href) {
window.location.href = href;
}
}
});
});
var keywordListItems = document.querySelectorAll('.keyword_list li');
var form = document.getElementById('searchForm');
var groupIdsInput = form.querySelector('input[name="groupIds"]');
// Category/Service Selection
const menuTitles = document.querySelectorAll('.menu-title');
menuTitles.forEach(function(title) {
title.addEventListener('click', function(e) {
e.preventDefault();
keywordListItems.forEach(function(item) {
item.addEventListener('click', function(e) {
e.preventDefault(); // Prevent default anchor behavior
var anchor = this.querySelector('a');
var groupId = anchor.getAttribute('data-group');
const groupId = this.getAttribute('data-group');
// Update groupIds input
if (groupId) {
groupId = groupId.replace(/[\[\]]/g, '');
}
if (groupId) {
// If specific group is selected, create/set input value
if (!groupIdsInput) {
groupIdsInput = document.createElement('input');
groupIdsInput.type = 'hidden';
groupIdsInput.name = 'groupIds';
form.appendChild(groupIdsInput);
}
groupIdsInput.value = groupId;
} else {
if (groupIdsInput) {
groupIdsInput.remove();
}
groupIdsInput.value = '';
}
form.submit();
// Submit form
searchForm.submit();
});
});
// Search on Enter key
searchInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
searchForm.submit();
}
});
// Mobile Menu Toggle
if (mobileToggle && sidebar && mobileOverlay) {
mobileToggle.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
sidebar.classList.toggle('mobile-open');
mobileOverlay.classList.toggle('active');
// Update button text
if (sidebar.classList.contains('mobile-open')) {
this.innerHTML = '✕';
this.setAttribute('aria-label', '메뉴 닫기');
} else {
this.innerHTML = '☰';
this.setAttribute('aria-label', '메뉴 열기');
}
});
// Add touch event for better mobile support
mobileToggle.addEventListener('touchend', function(e) {
e.preventDefault();
e.stopPropagation();
this.click();
});
// Close menu on overlay click
mobileOverlay.addEventListener('click', function() {
sidebar.classList.remove('mobile-open');
this.classList.remove('active');
mobileToggle.innerHTML = '☰';
mobileToggle.setAttribute('aria-label', '메뉴 열기');
});
// Close menu on category selection (mobile)
menuTitles.forEach(function(title) {
title.addEventListener('click', function() {
if (window.innerWidth <= 768) {
sidebar.classList.remove('mobile-open');
mobileOverlay.classList.remove('active');
mobileToggle.innerHTML = '☰';
mobileToggle.setAttribute('aria-label', '메뉴 열기');
}
});
});
} else {
console.warn('Mobile toggle elements not found:', {
mobileToggle: !!mobileToggle,
sidebar: !!sidebar,
mobileOverlay: !!mobileOverlay
});
}
// Close mobile menu on resize to desktop
window.addEventListener('resize', function() {
if (window.innerWidth > 768 && sidebar.classList.contains('mobile-open')) {
sidebar.classList.remove('mobile-open');
mobileOverlay.classList.remove('active');
mobileToggle.innerHTML = '☰';
mobileToggle.setAttribute('aria-label', '메뉴 열기');
}
});
});
</script>
</th:block>
</html>
@@ -15,9 +15,8 @@
<meta content="max-age=0, public" http-equiv="Cache-Control"/>
<meta content="index, follow" name="robots"/>
<meta content="https://www.eactive.co.kr/" property="og:url"/>
<link rel="icon" type="image/ico" th:href="@{/favicon_16px.png}">
<link rel="stylesheet" type="text/css" th:href="@{/css/style2.css}">
<link rel="stylesheet" type="text/css" th:href="@{/css/common2.css}">
<link rel="icon" type="image/ico" th:href="@{/favicon.ico}">
<link rel="stylesheet" type="text/css" th:href="@{/css/main.css}">
</head>
<body>
<div id="wrap" class="wrap_top">
@@ -15,9 +15,8 @@
<meta content="max-age=0, public" http-equiv="Cache-Control"/>
<meta content="index, follow" name="robots"/>
<meta content="https://www.eactive.co.kr/" property="og:url"/>
<link rel="icon" type="image/ico" th:href="@{/favicon_16px.png}">
<link rel="stylesheet" type="text/css" th:href="@{/css/style2.css}">
<link rel="stylesheet" type="text/css" th:href="@{/css/common2.css}">
<link rel="icon" type="image/ico" th:href="@{/favicon.ico}">
<link rel="stylesheet" type="text/css" th:href="@{/css/main.css}">
</head>
<body>
<div id="wrap" class="wrap_top">
@@ -15,9 +15,8 @@
<meta content="max-age=0, public" http-equiv="Cache-Control"/>
<meta content="index, follow" name="robots"/>
<meta content="https://www.eactive.co.kr/" property="og:url"/>
<link rel="icon" type="image/ico" th:href="@{/favicon_16px.png}">
<link rel="stylesheet" type="text/css" th:href="@{/css/style2.css}">
<link rel="stylesheet" type="text/css" th:href="@{/css/common2.css}">
<link rel="icon" type="image/ico" th:href="@{/favicon.ico}">
<link rel="stylesheet" type="text/css" th:href="@{/css/main.css}">
</head>
<body>
<div id="wrap" class="wrap_top">
@@ -24,7 +24,7 @@
<link rel="stylesheet" th:href="@{/plugins/codemirror/theme/monokai.css}" type="text/css"/>
<link rel="stylesheet" th:href="@{/plugins/summernote/summernote-lite.css}" type="text/css" />
<link rel="stylesheet" th:href="@{/plugins/jquery-ui/jquery-ui.min.css}" />
<link rel="icon" type="image/ico" th:href="@{/favicon_16px.png}">
<link rel="icon" type="image/ico" th:href="@{/favicon.ico}">
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
@@ -22,7 +22,7 @@
<nav class="header-center">
<ul class="nav-menu">
<li><a href="#docs" class="nav-link">서비스 소개</a></li>
<li><a href="#api-market" class="nav-link">오픈 API</a></li>
<li><a href="/apis" class="nav-link">오픈 API</a></li>
<li><a href="#playground" class="nav-link">고객지원</a></li>
<li><a href="#community" class="nav-link">MY</a></li>
</ul>
@@ -82,7 +82,7 @@
<nav class="drawer-nav">
<ul class="drawer-menu">
<li><a href="#docs" class="drawer-link">서비스 소개</a></li>
<li><a href="#api-market" class="drawer-link">오픈 API</a></li>
<li><a href="/apis" class="drawer-link">오픈 API</a></li>
<li><a href="#playground" class="drawer-link">고객지원</a></li>
<li><a href="#community" class="drawer-link">MY</a></li>
</ul>