feat: 마이페이지 퍼블리싱
This commit is contained in:
+3876
-686
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+1
-1
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: 2.9 KiB |
@@ -10,7 +10,7 @@
|
||||
.container {
|
||||
max-width: $container-max-width;
|
||||
margin: 0 auto;
|
||||
padding: 0 26px;
|
||||
padding: 0 20px;
|
||||
|
||||
@include respond-to('sm') {
|
||||
padding: 0 $spacing-md;
|
||||
@@ -109,8 +109,7 @@
|
||||
font-size: $font-size-2xl;
|
||||
}
|
||||
|
||||
.title-highlight {
|
||||
}
|
||||
.title-highlight {}
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
@@ -149,4 +148,4 @@
|
||||
.inner-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
@@ -616,6 +616,7 @@
|
||||
// ====================================
|
||||
|
||||
@include respond-to('sm') {
|
||||
|
||||
// Container mobile padding
|
||||
.apikey-detail-container {
|
||||
padding: 0 !important;
|
||||
@@ -878,3 +879,438 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// NEW FIGMA DETAIL WRAP STYLES (.detail-wrap)
|
||||
// =============================================================================
|
||||
.detail-wrap {
|
||||
width: 100%;
|
||||
font-family: 'Spoqa Han Sans Neo', 'Noto Sans KR', sans-serif;
|
||||
|
||||
// Title
|
||||
.dt-title {
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
color: #000000;
|
||||
margin: 0 0 28px 0;
|
||||
text-align: left;
|
||||
font-family: 'Spoqa Han Sans Neo', 'Noto Sans KR', sans-serif;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
// App Card Header (Figma 240:642)
|
||||
.dt-app-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #dfdfdf;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0px 4px 13.45px rgba(192, 192, 192, 0.25);
|
||||
padding: 20px 24px;
|
||||
margin-bottom: 24px;
|
||||
box-sizing: border-box;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.dt-app-icon-box {
|
||||
width: 68px;
|
||||
height: 67px;
|
||||
background: #f9f9f9;
|
||||
border: 1px solid #dee3e7;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
img,
|
||||
svg {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.dt-app-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dt-app-header-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.dt-app-name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #000;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.dt-status-badge {
|
||||
height: 20px;
|
||||
border-radius: 5px;
|
||||
padding: 0 8px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
|
||||
&.status-approved,
|
||||
&.status-completed {
|
||||
background: #1b4ab7;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
&.status-pending {
|
||||
background: #fdf4d4;
|
||||
color: #ef9546;
|
||||
}
|
||||
|
||||
&.status-rejected {
|
||||
background: #fef2f2;
|
||||
color: #ef4444;
|
||||
border: 1px solid #fee2e2;
|
||||
}
|
||||
}
|
||||
|
||||
.dt-app-desc {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
// Main details card wrapper
|
||||
.dt-info-container {
|
||||
background: #ffffff;
|
||||
border: 1px solid #dfdfdf;
|
||||
border-radius: 30px;
|
||||
box-shadow: 0px 4px 26.9px 2px rgba(192, 192, 192, 0.25);
|
||||
padding: 40px;
|
||||
margin-bottom: 35px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 28px;
|
||||
box-sizing: border-box;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 24px 20px;
|
||||
border-radius: 20px;
|
||||
gap: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
// Row of details
|
||||
.dt-row {
|
||||
display: flex;
|
||||
gap: 28px;
|
||||
width: 100%;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.dt-col {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dt-label {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #000000;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dt-input-group {
|
||||
display: flex;
|
||||
gap: 7px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dt-input-box {
|
||||
flex: 1;
|
||||
height: 47px;
|
||||
background: #ededed;
|
||||
border: 1px solid #dfdfdf;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
font-size: 16px;
|
||||
font-weight: 300;
|
||||
color: #0d0e11;
|
||||
box-sizing: border-box;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&.text-secret {
|
||||
letter-spacing: 2px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 0 14px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
// Action Buttons inside input-group (Copy, view, etc.)
|
||||
.dt-btn-copy {
|
||||
height: 47px;
|
||||
background: #d5efff;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 0 20px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #2a69de;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
background: darken(#d5efff, 5%);
|
||||
}
|
||||
|
||||
svg {
|
||||
color: #2a69de;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.dt-btn-lookup {
|
||||
width: 100%;
|
||||
height: 47px;
|
||||
background: #2a69de;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: #1b4ab7;
|
||||
}
|
||||
|
||||
svg {
|
||||
color: #ffffff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// API List scroll box
|
||||
.dt-api-list-box {
|
||||
width: 100%;
|
||||
background: #ffffff;
|
||||
border: 1px solid #dfdfdf;
|
||||
border-radius: 10px;
|
||||
padding: 0 20px;
|
||||
box-sizing: border-box;
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
|
||||
// Custom scrollbar
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
.dt-api-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid #efefef;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.dt-api-name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #0d0e11;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-right: 20px;
|
||||
flex: 1;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.dt-api-status-badge {
|
||||
height: 18px;
|
||||
background: #ecf0fa;
|
||||
border: 1px solid #4685ef;
|
||||
border-radius: 17px;
|
||||
padding: 0 10px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: #2a69de;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
|
||||
&.status-pending {
|
||||
background: #eceff4;
|
||||
border: 1px solid #dee3e7;
|
||||
color: #515151;
|
||||
}
|
||||
}
|
||||
|
||||
.dt-empty-message {
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// Footer Buttons layout
|
||||
.dt-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 10px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
.dt-btn-gray,
|
||||
.dt-btn-red,
|
||||
.dt-btn-blue {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dt-btn-gray {
|
||||
width: 156px;
|
||||
height: 45px;
|
||||
background: #bdc7cf;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: background 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: darken(#bdc7cf, 8%);
|
||||
}
|
||||
}
|
||||
|
||||
.dt-btn-red {
|
||||
width: 156px;
|
||||
height: 45px;
|
||||
background: #ff615c;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: darken(#ff615c, 8%);
|
||||
}
|
||||
}
|
||||
|
||||
.dt-btn-blue {
|
||||
width: 156px;
|
||||
height: 45px;
|
||||
background: #2a69de;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: background 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: #1b4ab7;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -247,3 +247,213 @@
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* App Card Figma Styles */
|
||||
.app-management-layout {
|
||||
display: flex;
|
||||
gap: 36px;
|
||||
max-width: 1200px;
|
||||
margin: 40px auto 100px;
|
||||
font-family: 'Spoqa Han Sans Neo', 'Noto Sans CJK KR', sans-serif;
|
||||
}
|
||||
|
||||
.app-management-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-management-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #000;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-app-create-figma {
|
||||
background-color: #2a69de;
|
||||
color: #ffffff;
|
||||
border-radius: 7px;
|
||||
padding: 0 20px;
|
||||
width: 122px;
|
||||
height: 35px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: #1b4ab7;
|
||||
}
|
||||
}
|
||||
|
||||
.app-list-container-figma {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.app-card-figma {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
height: 107px;
|
||||
padding: 20px 31px 20px 17px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #dfdfdf;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0px 4px 5.45px rgba(192, 192, 192, 0.25);
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0px 6px 15px rgba(192, 192, 192, 0.35);
|
||||
}
|
||||
}
|
||||
|
||||
.app-card-icon-box {
|
||||
width: 68px;
|
||||
height: 67px;
|
||||
background: #f9f9f9;
|
||||
border: 1px solid #dee3e7;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
img,
|
||||
svg {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.app-card-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.app-card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #000;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.app-card-badge-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.app-card-badge {
|
||||
height: 20px;
|
||||
border-radius: 5px;
|
||||
padding: 0 8px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
|
||||
&.badge-pending {
|
||||
background: #fdf4d4;
|
||||
color: #ef9546;
|
||||
}
|
||||
|
||||
&.badge-approved {
|
||||
background: #1b4ab7;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
&.badge-inactive {
|
||||
background: #f3f4f6;
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
|
||||
.app-card-desc {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-card-footer-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-card-expected-date {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
margin-left: 16px;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.app-card-expected-date {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.app-list-empty-figma {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 0;
|
||||
background: #ffffff;
|
||||
border: 1px solid #dfdfdf;
|
||||
border-radius: 20px;
|
||||
text-align: center;
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
color: #000;
|
||||
margin: 16px 0 8px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,3 +22,291 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// Withdrawal Link styles moved to components/_forms.scss (.form-actions--with-withdrawal)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Corporate Manager Profile Form Styles
|
||||
// -----------------------------------------------------------------------------
|
||||
.corp-manager-wrapper, .password-change-wrapper {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 40px 0;
|
||||
|
||||
.page-outer-title {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
color: #000;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.register-form-container {
|
||||
background: #fff;
|
||||
border: 1px solid #dfdfdf;
|
||||
border-radius: 30px;
|
||||
box-shadow: 0px 4px 26.9px 2px rgba(192, 192, 192, 0.25);
|
||||
padding: 40px 48px;
|
||||
}
|
||||
|
||||
.common-title-bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.inner-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
border-bottom: 1px solid #000;
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 32px;
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #0d0e11;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 13px;
|
||||
margin-bottom: 26px;
|
||||
|
||||
&.two-columns {
|
||||
flex-direction: row;
|
||||
gap: 34px;
|
||||
|
||||
> div {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-label-wrapper {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.form-field-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#authNumberContainer .auth-input-group {
|
||||
flex: none !important;
|
||||
width: 440px !important;
|
||||
}
|
||||
|
||||
#authNumberContainer .input-with-button {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#authNumber {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.form-label-text {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.required-badge {
|
||||
visibility: hidden;
|
||||
position: relative;
|
||||
width: 10px;
|
||||
|
||||
&::after {
|
||||
content: '*';
|
||||
visibility: visible;
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
top: 0;
|
||||
color: #f4253c;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-select,
|
||||
.input-readonly,
|
||||
.info-value-box {
|
||||
height: 47px;
|
||||
background: #fcfcfc;
|
||||
border: 1px solid #dfdfdf;
|
||||
border-radius: 10px;
|
||||
font-size: 16px;
|
||||
color: #0d0e11;
|
||||
padding: 0 20px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-input:disabled,
|
||||
.input-readonly {
|
||||
background: #ededed;
|
||||
}
|
||||
|
||||
.compound-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
width: 100%;
|
||||
|
||||
.form-input,
|
||||
.form-select {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
font-size: 16px;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.input-with-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.btn-change,
|
||||
.btn-auth {
|
||||
height: 47px;
|
||||
background: #d5efff;
|
||||
color: #2a69de;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 0 20px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.auth-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
|
||||
.form-input {
|
||||
padding-right: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-timer {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
color: #f4253c;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.btn-withdrawal {
|
||||
height: 45px;
|
||||
background: #ff615c;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 0 20px;
|
||||
cursor: pointer;
|
||||
width: 156px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.right-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
height: 45px;
|
||||
background: #bdc7cf;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 0 20px;
|
||||
cursor: pointer;
|
||||
width: 156px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-apply {
|
||||
height: 45px;
|
||||
background: #2a69de;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 0 20px;
|
||||
cursor: pointer;
|
||||
width: 156px;
|
||||
}
|
||||
|
||||
.info-notice-box {
|
||||
background: #ecf0fa;
|
||||
border-radius: 7px;
|
||||
padding: 13px 14px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 13px;
|
||||
margin-bottom: 32px;
|
||||
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #4685ef;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.password-rules-list {
|
||||
list-style-type: disc;
|
||||
padding-left: 24px;
|
||||
margin: 20px 0 0 0;
|
||||
|
||||
li {
|
||||
color: #888;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
margin-bottom: 5px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,153 +1,364 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/djbank_title_layout}">
|
||||
xmlns:sec="http://www.thymeleaf.org/extras/spring-security" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
layout:decorate="~{layout/djbank_title_layout}">
|
||||
|
||||
<body>
|
||||
<section layout:fragment="title">
|
||||
<div class="page-title-banner">
|
||||
<img th:src="@{/img/img_title_bg.png}" class="title-image">
|
||||
<h1>앱관리</h1>
|
||||
</div>
|
||||
</section>
|
||||
<th:block layout:fragment="contentFragment">
|
||||
<section class="app-management-container">
|
||||
|
||||
<!-- App List Container -->
|
||||
<div class="app-list-container">
|
||||
|
||||
<!-- App Requests (Pending) -->
|
||||
<th:block th:if="${appRequests != null and !appRequests.isEmpty()}">
|
||||
<a class="app-list-item"
|
||||
th:each="request : ${appRequests}"
|
||||
th:href="@{/myapikey/app_request_detail(id=${request.id})}">
|
||||
|
||||
<!-- App Icon -->
|
||||
<div class="app-list-icon">
|
||||
<img th:if="${request.appIconFileId != null}"
|
||||
th:src="@{/file/download(fileSn=1,fileId=${request.appIconFileId})}"
|
||||
alt="App Icon"
|
||||
class="app-icon-image">
|
||||
|
||||
<div th:unless="${request.appIconFileId != null}" class="app-icon-placeholder">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
||||
<polyline points="21 15 16 10 5 21"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- App Info -->
|
||||
<div class="app-list-info">
|
||||
<div class="app-list-header">
|
||||
<!-- Status Badge -->
|
||||
<span class="app-status-badge status-pending"
|
||||
th:text="${request.approval != null and request.approval.approvalStatus != null ? request.approval.approvalStatus.description : '승인대기'}">
|
||||
승인대기
|
||||
</span>
|
||||
<!-- App Name -->
|
||||
<h3 class="app-list-name" th:text="${request.clientName}">앱 이름</h3>
|
||||
</div>
|
||||
<!-- App Description -->
|
||||
<p class="app-list-description"
|
||||
th:text="${request.appDescription != null ? request.appDescription : '설명 없음'}">
|
||||
앱 설명이 여기에 표시됩니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</a>
|
||||
</th:block>
|
||||
|
||||
<!-- API Keys (Approved) -->
|
||||
<th:block th:if="${apiKeys != null and !apiKeys.isEmpty()}">
|
||||
<a class="app-list-item"
|
||||
th:each="apikey : ${apiKeys}"
|
||||
th:href="@{/myapikey/credential_detail(id=${apikey.clientid})}">
|
||||
|
||||
<!-- App Icon -->
|
||||
<div class="app-list-icon">
|
||||
<img th:if="${apikey.appIconFileId != null}"
|
||||
th:src="@{/file/download(fileSn=1,fileId=${apikey.appIconFileId})}"
|
||||
alt="App Icon"
|
||||
class="app-icon-image">
|
||||
<div th:unless="${apikey.appIconFileId != null}" class="app-icon-placeholder">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
||||
<polyline points="21 15 16 10 5 21"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- App Info -->
|
||||
<div class="app-list-info">
|
||||
<div class="app-list-header">
|
||||
<!-- Status Badge -->
|
||||
<span class="app-status-badge"
|
||||
th:classappend="${apikey.appstatus == '1' ? 'status-approved' : apikey.appstatus == '0' ? 'status-inactive' : 'status-pending'}"
|
||||
th:text="${apikey.appstatus == '1' ? '승인' : apikey.appstatus == '0' ? '비활성화' : '승인대기'}">
|
||||
승인
|
||||
</span>
|
||||
<!-- App Name -->
|
||||
<h3 class="app-list-name" th:text="${apikey.clientname}">앱 이름</h3>
|
||||
</div>
|
||||
<!-- App Description -->
|
||||
<p class="app-list-description"
|
||||
th:text="${apikey.appDescription != null ? apikey.appDescription : '설명 없음'}">
|
||||
앱 설명이 여기에 표시됩니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</a>
|
||||
</th:block>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div class="app-list-empty" th:if="${(appRequests == null or appRequests.isEmpty()) and (apiKeys == null or apiKeys.isEmpty())}">
|
||||
<div class="empty-icon">🔑</div>
|
||||
<h3>등록된 앱이 없습니다</h3>
|
||||
<p>새로운 앱을 생성하여 API를 사용해보세요.</p>
|
||||
</div>
|
||||
|
||||
<section layout:fragment="title">
|
||||
<div class="page-title-banner">
|
||||
<img th:src="@{/img/img_title_bg.png}" class="title-image">
|
||||
<h1>앱관리</h1>
|
||||
</div>
|
||||
|
||||
<!-- Create App Button -->
|
||||
<div class="app-create-button-wrapper" sec:authorize="hasRole('ROLE_API_KEY_REQUEST')">
|
||||
<button type="button" class="btn-app-create" id="requestApiKey">
|
||||
앱 생성
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</th:block>
|
||||
<th:block layout:fragment="contentFragment">
|
||||
<style>
|
||||
/* App Card Figma Styles */
|
||||
.app-management-layout {
|
||||
display: flex;
|
||||
gap: 36px;
|
||||
max-width: 1200px;
|
||||
margin: 40px auto 100px;
|
||||
font-family: 'Spoqa Han Sans Neo', 'Noto Sans CJK KR', sans-serif;
|
||||
}
|
||||
|
||||
<th:block layout:fragment="contentScript">
|
||||
<script th:inline="javascript">
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
.app-management-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
// App creation button handler
|
||||
const requestApiKeyBtn = document.getElementById('requestApiKey');
|
||||
.app-management-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
function handleApiKeyRequest() {
|
||||
// Clear any existing registration data from sessionStorage
|
||||
const keysToRemove = [];
|
||||
for (let i = 0; i < sessionStorage.length; i++) {
|
||||
const key = sessionStorage.key(i);
|
||||
if (key && key.startsWith('registration_')) {
|
||||
keysToRemove.push(key);
|
||||
.app-management-header h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #000;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.btn-app-create-figma {
|
||||
background-color: #2a69de;
|
||||
color: #ffffff;
|
||||
border-radius: 7px;
|
||||
padding: 0 20px;
|
||||
width: 122px;
|
||||
height: 35px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.btn-app-create-figma:hover {
|
||||
background-color: #1b4ab7;
|
||||
}
|
||||
|
||||
.app-list-container-figma {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.app-card-figma {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
height: 107px;
|
||||
padding: 20px 31px 20px 17px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #dfdfdf;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0px 4px 5.45px rgba(192, 192, 192, 0.25);
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.app-card-figma:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0px 6px 15px rgba(192, 192, 192, 0.35);
|
||||
}
|
||||
|
||||
.app-card-icon-box {
|
||||
width: 68px;
|
||||
height: 67px;
|
||||
background: #f9f9f9;
|
||||
border: 1px solid #dee3e7;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.app-card-icon-box img,
|
||||
.app-card-icon-box svg {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.app-card-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.app-card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #000;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.app-card-badge {
|
||||
height: 20px;
|
||||
border-radius: 5px;
|
||||
padding: 0 8px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.app-card-badge.badge-pending {
|
||||
background: #fdf4d4;
|
||||
color: #ef9546;
|
||||
}
|
||||
|
||||
.app-card-badge.badge-approved {
|
||||
background: #1b4ab7;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.app-card-badge.badge-inactive {
|
||||
background: #f3f4f6;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.app-card-desc {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-card-footer-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-card-expected-date {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
margin-left: 16px;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.app-list-empty-figma {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 0;
|
||||
background: #ffffff;
|
||||
border: 1px solid #dfdfdf;
|
||||
border-radius: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.app-list-empty-figma h3 {
|
||||
font-size: 18px;
|
||||
color: #000;
|
||||
margin: 16px 0 8px;
|
||||
}
|
||||
|
||||
.app-list-empty-figma p {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="signup-guide-v2">
|
||||
<div class="service-main app-management-layout">
|
||||
|
||||
<!-- Sidebar -->
|
||||
<th:block th:replace="~{fragment/djbank/service_sidebar :: sidebar('apiKey')}"></th:block>
|
||||
|
||||
<!-- Content Area -->
|
||||
<div class="app-management-content">
|
||||
<!-- Header -->
|
||||
<div class="app-management-header">
|
||||
<h2>인증 키 관리</h2>
|
||||
<div sec:authorize="hasRole('ROLE_API_KEY_REQUEST')">
|
||||
<button type="button" class="btn-app-create-figma" id="requestApiKey">
|
||||
앱 생성
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- App List Container -->
|
||||
<div class="app-list-container-figma">
|
||||
|
||||
<!-- App Requests (Pending) -->
|
||||
<th:block th:if="${appRequests != null and !appRequests.isEmpty()}">
|
||||
<a class="app-card-figma" th:each="request : ${appRequests}"
|
||||
th:href="@{/myapikey/app_request_detail(id=${request.id})}">
|
||||
|
||||
<!-- App Icon -->
|
||||
<div class="app-card-icon-box">
|
||||
<img th:if="${request.appIconFileId != null}"
|
||||
th:src="@{/file/download(fileSn=1,fileId=${request.appIconFileId})}" alt="App Icon">
|
||||
<svg th:unless="${request.appIconFileId != null}" width="46" height="46" viewBox="0 0 24 24"
|
||||
fill="none" stroke="#64748b" 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>
|
||||
|
||||
<!-- App Info -->
|
||||
<div class="app-card-info">
|
||||
<div class="app-card-header">
|
||||
<!-- App Name -->
|
||||
<h3 class="app-card-title" th:text="${request.clientName}">앱 이름</h3>
|
||||
<!-- Status Badge -->
|
||||
<span class="app-card-badge badge-pending"
|
||||
th:text="${request.approval != null and request.approval.approvalStatus != null ? request.approval.approvalStatus.description : '승인대기'}">
|
||||
승인대기
|
||||
</span>
|
||||
</div>
|
||||
<!-- App Description -->
|
||||
<div class="app-card-footer-row">
|
||||
<p class="app-card-desc"
|
||||
th:text="${request.appDescription != null ? request.appDescription : '설명 없음'}">
|
||||
앱 설명이 여기에 표시됩니다.
|
||||
</p>
|
||||
<span class="app-card-expected-date"
|
||||
th:if="${request.approval != null and request.approval.approvalStatus != null and (request.approval.approvalStatus.toString() == 'REQUESTED' or request.approval.approvalStatus.description == '진행중')}"
|
||||
th:text="${request.createdDate != null ? '예상 완료일 : ' + #temporals.format(request.createdDate.plusDays(14), 'yyyy.MM.dd') + ' (' + #temporals.format(request.createdDate.plusDays(14), 'E') + ')' : '예상 완료일 : 산정 중'}">
|
||||
예상 완료일 : 2026.07.14 (화)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</th:block>
|
||||
|
||||
<!-- API Keys (Approved/Inactive) -->
|
||||
<th:block th:if="${apiKeys != null and !apiKeys.isEmpty()}">
|
||||
<a class="app-card-figma" th:each="apikey : ${apiKeys}"
|
||||
th:href="@{/myapikey/credential_detail(id=${apikey.clientid})}">
|
||||
|
||||
<!-- App Icon -->
|
||||
<div class="app-card-icon-box">
|
||||
<img th:if="${apikey.appIconFileId != null}"
|
||||
th:src="@{/file/download(fileSn=1,fileId=${apikey.appIconFileId})}" alt="App Icon">
|
||||
<svg th:unless="${apikey.appIconFileId != null}" width="46" height="46" viewBox="0 0 24 24"
|
||||
fill="none" stroke="#64748b" 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>
|
||||
|
||||
<!-- App Info -->
|
||||
<div class="app-card-info">
|
||||
<div class="app-card-header">
|
||||
<!-- App Name -->
|
||||
<h3 class="app-card-title" th:text="${apikey.clientname}">앱 이름</h3>
|
||||
<!-- Status Badge -->
|
||||
<span class="app-card-badge"
|
||||
th:classappend="${apikey.appstatus == '1' ? 'badge-approved' : apikey.appstatus == '0' ? 'badge-inactive' : 'badge-pending'}"
|
||||
th:text="${apikey.appstatus == '1' ? '승인' : apikey.appstatus == '0' ? '비활성화' : '승인대기'}">
|
||||
승인
|
||||
</span>
|
||||
</div>
|
||||
<!-- App Description -->
|
||||
<p class="app-card-desc" th:text="${apikey.appDescription != null ? apikey.appDescription : '설명 없음'}">
|
||||
앱 설명이 여기에 표시됩니다.
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</th:block>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div class="app-list-empty-figma"
|
||||
th:if="${(appRequests == null or appRequests.isEmpty()) and (apiKeys == null or apiKeys.isEmpty())}">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#dee3e7" stroke-width="2">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
||||
</svg>
|
||||
<h3>등록된 앱이 없습니다</h3>
|
||||
<p>새로운 앱을 생성하여 API를 사용해보세요.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</th:block>
|
||||
|
||||
<th:block layout:fragment="contentScript">
|
||||
<script th:inline="javascript">
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
// App creation button handler
|
||||
const requestApiKeyBtn = document.getElementById('requestApiKey');
|
||||
|
||||
function handleApiKeyRequest() {
|
||||
// Clear any existing registration data from sessionStorage
|
||||
const keysToRemove = [];
|
||||
for (let i = 0; i < sessionStorage.length; i++) {
|
||||
const key = sessionStorage.key(i);
|
||||
if (key && key.startsWith('registration_')) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
keysToRemove.forEach(key => sessionStorage.removeItem(key));
|
||||
|
||||
// Redirect to the API key registration wizard with clear parameter
|
||||
window.location.href = '/myapikey/register/step1?clear=true';
|
||||
}
|
||||
keysToRemove.forEach(key => sessionStorage.removeItem(key));
|
||||
|
||||
// Redirect to the API key registration wizard with clear parameter
|
||||
window.location.href = '/myapikey/register/step1?clear=true';
|
||||
}
|
||||
|
||||
if (requestApiKeyBtn) {
|
||||
requestApiKeyBtn.addEventListener('click', handleApiKeyRequest);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</th:block>
|
||||
if (requestApiKeyBtn) {
|
||||
requestApiKeyBtn.addEventListener('click', handleApiKeyRequest);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</th:block>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
||||
@@ -11,460 +11,379 @@
|
||||
</div>
|
||||
</section>
|
||||
<th:block layout:fragment="contentFragment">
|
||||
<section class="apikey-register-container">
|
||||
<div class="signup-guide-v2 figma-register-wrapper">
|
||||
<div class="service-main app-management-layout">
|
||||
|
||||
<!-- Title Bar -->
|
||||
<div class="common-title-bar">
|
||||
<h2 class="common-title">앱 수정</h2>
|
||||
<span class="common-subtitle">서비스 중인 앱 정보를 수정합니다.</span>
|
||||
</div>
|
||||
<!-- Sidebar -->
|
||||
<th:block th:replace="~{fragment/djbank/service_sidebar :: sidebar('apiKey')}"></th:block>
|
||||
|
||||
<!-- Content Area -->
|
||||
<div class="app-management-content">
|
||||
<div class="step1-wrap">
|
||||
|
||||
<!-- Title -->
|
||||
<h2 class="s1-title">앱 수정</h2>
|
||||
|
||||
<!-- Progress Steps Card -->
|
||||
<div class="s1-progress-card">
|
||||
<div class="s1-steps">
|
||||
<!-- Step 1 Active: 앱 정보수정 (form/input icon) -->
|
||||
<div class="s1-step s1-step--active">
|
||||
<div class="s1-step-circle">
|
||||
<svg class="s1-step-svg" width="26" height="26" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="3" y="5" width="18" height="14" rx="2" stroke="currentColor" stroke-width="1.8" />
|
||||
<line x1="7" y1="9" x2="17" y2="9" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
|
||||
<line x1="7" y1="12" x2="14" y2="12" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
|
||||
<line x1="7" y1="15" x2="11" y2="15" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="s1-step-num">1단계</span>
|
||||
<span class="s1-step-name">앱 정보수정</span>
|
||||
</div>
|
||||
|
||||
<div class="s1-step-line"></div>
|
||||
|
||||
<!-- Step 2: API 선택 (chain link icon) -->
|
||||
<div class="s1-step">
|
||||
<div class="s1-step-circle">
|
||||
<svg class="s1-step-svg" width="26" height="26" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="s1-step-num">2단계</span>
|
||||
<span class="s1-step-name">API 선택</span>
|
||||
</div>
|
||||
|
||||
<div class="s1-step-line"></div>
|
||||
|
||||
<!-- Step 3: 완료 (rocket icon) -->
|
||||
<div class="s1-step">
|
||||
<div class="s1-step-circle">
|
||||
<svg class="s1-step-svg" width="26" height="26" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M12 15l-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="s1-step-num">3단계</span>
|
||||
<span class="s1-step-name">완료</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Indicator -->
|
||||
<div class="register-progress">
|
||||
<div class="progress-steps">
|
||||
<!-- Step 1: 앱 정보수정 (Active) -->
|
||||
<div class="progress-step-item active">
|
||||
<div class="step-icon-wrapper">
|
||||
<div class="step-icon">
|
||||
<img th:src="@{/img/apikey_step1.png}" alt="앱 정보수정" width="36" height="36">
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-label">앱 정보수정</div>
|
||||
</div>
|
||||
|
||||
<!-- Dots -->
|
||||
<div class="step-dots">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: API 선택 -->
|
||||
<div class="progress-step-item">
|
||||
<div class="step-icon-wrapper">
|
||||
<div class="step-icon">
|
||||
<img th:src="@{/img/apikey_step2.png}" alt="API 선택" width="36" height="36">
|
||||
<!-- Error -->
|
||||
<div th:if="${error}" class="s1-alert">
|
||||
<span th:text="${error}"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-label">API 선택</div>
|
||||
</div>
|
||||
|
||||
<!-- Dots -->
|
||||
<div class="step-dots">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<!-- Form Card -->
|
||||
<div class="s1-form-card">
|
||||
<form id="modifyStep1Form" method="post" th:action="@{/myapikey/modify/step1}"
|
||||
th:object="${apiKeyModification}" enctype="multipart/form-data">
|
||||
|
||||
<!-- Step 3: 완료 -->
|
||||
<div class="progress-step-item">
|
||||
<div class="step-icon-wrapper">
|
||||
<div class="step-icon">
|
||||
<img th:src="@{/img/apikey_step3.png}" alt="완료" width="36" height="36">
|
||||
<!-- Hidden clientId -->
|
||||
<input type="hidden" th:field="*{clientId}"/>
|
||||
|
||||
<!-- 앱 아이콘 -->
|
||||
<div class="s1-field">
|
||||
<label class="s1-label">앱 아이콘</label>
|
||||
<div class="s1-upload-box" id="iconDropZone">
|
||||
|
||||
<!-- 기존 파일 ID로 이미지 노출 -->
|
||||
<img class="s1-preview-img" id="iconPreviewImage1"
|
||||
th:if="${apiKeyModification.appIconFileId != null}"
|
||||
th:src="@{/file/download(fileSn=1,fileId=${apiKeyModification.appIconFileId})}"
|
||||
alt="미리보기" style="display: block;">
|
||||
|
||||
<!-- 데이터에서 이미지 노출 -->
|
||||
<img class="s1-preview-img" id="iconPreviewImage2"
|
||||
th:if="${apiKeyModification.appIconData != null and apiKeyModification.appIconFileId == null}"
|
||||
th:src="'data:' + ${apiKeyModification.appIconContentType} + ';base64,' + ${T(java.util.Base64).getEncoder().encodeToString(apiKeyModification.appIconData)}"
|
||||
alt="미리보기" style="display: block;">
|
||||
|
||||
<!-- JS용 이미지 타겟 -->
|
||||
<img id="iconPreviewImageJS" class="s1-preview-img" style="display: none;" alt="미리보기">
|
||||
|
||||
<div id="iconPlaceholder" class="s1-upload-inner" th:style="${(apiKeyModification.appIconData != null or apiKeyModification.appIconFileId != null) ? 'display: none;' : 'display: flex;'}">
|
||||
<img th:src="@{/img/icon/img_icon.png}">
|
||||
<p class="s1-upload-title">앱 아이콘 이미지 파일을 업로드 하세요</p>
|
||||
<p class="s1-upload-hint">권장 사이즈는 1280 * 12 픽셀이며 JPG,PNG,GIF 파일만 등록할 수 있습니다.</p>
|
||||
<button type="button" class="s1-btn-upload"
|
||||
onclick="document.getElementById('appIconFile').click()">
|
||||
이미지 파일 업로드
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="s1-btn-remove-icon" id="btnRemoveIcon"
|
||||
th:style="${(apiKeyModification.appIconData != null or apiKeyModification.appIconFileId != null) ? 'display: flex;' : 'display: none;'}"
|
||||
onclick="removeAppIcon()">✕</button>
|
||||
<input type="file" id="appIconFile" name="appIcon" accept="image/png,image/jpeg,image/jpg,image/gif" style="display:none;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 앱 이름 -->
|
||||
<div class="s1-field">
|
||||
<label class="s1-label">앱 이름 <span class="s1-required">*</span></label>
|
||||
<input type="text" id="appName" name="appName" th:field="*{appName}" class="s1-input"
|
||||
placeholder="앱 이름을 입력하세요." required maxlength="100">
|
||||
</div>
|
||||
|
||||
<!-- 앱 설명 -->
|
||||
<div class="s1-field">
|
||||
<label class="s1-label">앱 설명 <span class="s1-required">*</span></label>
|
||||
<textarea id="appDescription" name="appDescription" th:field="*{appDescription}" class="s1-textarea"
|
||||
placeholder="앱 설명을 입력하세요." required maxlength="500"></textarea>
|
||||
<div class="s1-counter-row">
|
||||
<span id="charCounter" class="s1-counter">0 / 500</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Call Back URL -->
|
||||
<div class="s1-field">
|
||||
<label class="s1-label">Call Back URL</label>
|
||||
<input type="text" id="callbackUrl" name="callbackUrl" th:field="*{callbackUrl}" class="s1-input"
|
||||
placeholder="URL을 입력해 주세요.">
|
||||
</div>
|
||||
|
||||
<!-- 화이트 리스트 -->
|
||||
<div class="s1-field">
|
||||
<label class="s1-label">화이트 리스트 <span class="s1-required">*</span></label>
|
||||
|
||||
<!-- 입력행 -->
|
||||
<div class="s1-ip-row">
|
||||
<input type="text" id="ipWhitelistInput" class="s1-input" placeholder="서버 IP(111.111.111.111)">
|
||||
<button type="button" class="s1-ip-btn s1-ip-btn--add" onclick="addIpAddress()">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2.5">
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
추가
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 추가된 IP 목록 -->
|
||||
<div id="ipList" style="display:none;">
|
||||
<div id="ipItems"></div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" id="ipWhitelist" name="ipWhitelist" value="">
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-label">완료</div>
|
||||
|
||||
<!-- 다음 버튼 -->
|
||||
<div class="s1-actions" style="justify-content: flex-end; gap: 10px;">
|
||||
<a th:href="@{/myapikey/modify/cancel(clientId=${apiKeyModification.clientId})}" class="s1-btn-next" style="background: #bdc7cf; flex: none; width: 120px; text-decoration: none; display: flex; justify-content: center; align-items: center;">취소</a>
|
||||
<button type="submit" form="modifyStep1Form" class="s1-btn-next" style="flex: none; width: 120px;">다음</button>
|
||||
</div>
|
||||
|
||||
</div><!-- /step1-wrap -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</th:block>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div th:if="${error}" class="alert alert-error">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="12" y1="8" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
||||
</svg>
|
||||
<span th:text="${error}"></span>
|
||||
</div>
|
||||
<th:block layout:fragment="contentScript">
|
||||
<script th:inline="javascript">
|
||||
let ipAddresses = [];
|
||||
|
||||
<!-- Form Content -->
|
||||
<div class="register-form-container">
|
||||
<form id="modifyStep1Form" method="post" th:action="@{/myapikey/modify/step1}" th:object="${apiKeyModification}" class="register-form" enctype="multipart/form-data">
|
||||
function removeAppIcon() {
|
||||
const fileInput = document.getElementById('appIconFile');
|
||||
const previewJS = document.getElementById('iconPreviewImageJS');
|
||||
const preview1 = document.getElementById('iconPreviewImage1');
|
||||
const preview2 = document.getElementById('iconPreviewImage2');
|
||||
const placeholder = document.getElementById('iconPlaceholder');
|
||||
const removeBtn = document.getElementById('btnRemoveIcon');
|
||||
|
||||
<!-- Hidden field for clientId -->
|
||||
<input type="hidden" th:field="*{clientId}"/>
|
||||
|
||||
<!-- App Icon -->
|
||||
<div class="form-row">
|
||||
<div class="form-label-wrapper">
|
||||
<span class="form-label-text">앱 아이콘</span>
|
||||
</div>
|
||||
<div class="form-field-wrapper">
|
||||
<div class="icon-upload-box">
|
||||
<div class="icon-preview-area" id="iconPreview">
|
||||
<div class="icon-placeholder" id="iconPlaceholder"
|
||||
th:style="${(apiKeyModification.appIconData != null or apiKeyModification.appIconFileId != null) ? 'display: none;' : 'display: flex;'}">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="17 8 12 3 7 8"></polyline>
|
||||
<line x1="12" y1="3" x2="12" y2="15"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- Show image from file service if available -->
|
||||
<img id="iconPreviewImage"
|
||||
th:if="${apiKeyModification.appIconFileId != null}"
|
||||
th:src="@{/file/download(fileSn=1,fileId=${apiKeyModification.appIconFileId})}"
|
||||
alt="Icon Preview"
|
||||
style="display: block;">
|
||||
<!-- Show image from data if available -->
|
||||
<img id="iconPreviewImage"
|
||||
th:if="${apiKeyModification.appIconData != null and apiKeyModification.appIconFileId == null}"
|
||||
th:src="'data:' + ${apiKeyModification.appIconContentType} + ';base64,' + ${T(java.util.Base64).getEncoder().encodeToString(apiKeyModification.appIconData)}"
|
||||
alt="Icon Preview"
|
||||
style="display: block;">
|
||||
<!-- JS Empty Image Target -->
|
||||
<img id="iconPreviewImageJS" style="display: none;" alt="Icon Preview">
|
||||
|
||||
<button type="button" class="btn-remove-icon" id="btnRemoveIcon"
|
||||
th:style="${(apiKeyModification.appIconData != null or apiKeyModification.appIconFileId != null) ? 'display: flex;' : 'display: none;'}"
|
||||
onclick="removeAppIcon()" title="아이콘 삭제">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="icon-upload-info">
|
||||
<p class="upload-title">앱 아이콘 이미지 파일을 업로드하세요</p>
|
||||
<p class="upload-hint">권장사이즈는 <strong>128X128픽셀</strong>이며 <strong>JPG, PNG, GIF</strong>파일만 등록할 수 있습니다.</p>
|
||||
</div>
|
||||
<button type="button" class="btn-upload" onclick="document.getElementById('appIcon').click()">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="17 8 12 3 7 8"></polyline>
|
||||
<line x1="12" y1="3" x2="12" y2="15"></line>
|
||||
</svg>
|
||||
이미지 파일 업로드
|
||||
</button>
|
||||
<input type="file"
|
||||
id="appIcon"
|
||||
name="appIcon"
|
||||
class="icon-input"
|
||||
accept="image/png,image/jpeg,image/jpg,image/gif"
|
||||
style="display: none;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- App Name -->
|
||||
<div class="form-row">
|
||||
<div class="form-label-wrapper label-offset">
|
||||
<span class="form-label-text">앱 이름</span>
|
||||
<span class="required-badge">필수</span>
|
||||
</div>
|
||||
<div class="form-field-wrapper">
|
||||
<input type="text"
|
||||
id="appName"
|
||||
th:field="*{appName}"
|
||||
class="form-input"
|
||||
placeholder="앱 이름을 입력하세요."
|
||||
required
|
||||
maxlength="100">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- App Description -->
|
||||
<div class="form-row">
|
||||
<div class="form-label-wrapper">
|
||||
<span class="form-label-text">앱 설명</span>
|
||||
<span class="required-badge">필수</span>
|
||||
</div>
|
||||
<div class="form-field-wrapper">
|
||||
<textarea id="appDescription"
|
||||
th:field="*{appDescription}"
|
||||
class="form-textarea"
|
||||
placeholder="앱 설명을 입력하세요."
|
||||
rows="5"
|
||||
required
|
||||
maxlength="500"></textarea>
|
||||
<small class="char-counter-wrapper">
|
||||
<span class="char-counter">0 / 500</span>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Callback URL -->
|
||||
<div class="form-row">
|
||||
<div class="form-label-wrapper label-offset">
|
||||
<span class="form-label-text">Call Back URL</span>
|
||||
</div>
|
||||
<div class="form-field-wrapper">
|
||||
<input type="text"
|
||||
id="callbackUrl"
|
||||
th:field="*{callbackUrl}"
|
||||
class="form-input"
|
||||
placeholder="URL을 입력해 주세요.">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IP Whitelist -->
|
||||
<div class="form-row">
|
||||
<div class="form-label-wrapper label-offset">
|
||||
<span class="form-label-text">화이트리스트</span>
|
||||
</div>
|
||||
<div class="form-field-wrapper">
|
||||
<div class="ip-input-row">
|
||||
<input type="text"
|
||||
id="ipWhitelistInput"
|
||||
class="form-input ip-input"
|
||||
placeholder="서버 IP (111.111.111.111)">
|
||||
<button type="button" class="btn-add-ip" onclick="addIpAddress()">
|
||||
+ 추가
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- IP List Display -->
|
||||
<div class="ip-list" id="ipList" style="display: none;">
|
||||
<div class="ip-items" id="ipItems">
|
||||
<!-- Dynamically added IP items -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden input to store IP list -->
|
||||
<input type="hidden" id="ipWhitelist" name="ipWhitelist" value="">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="form-actions">
|
||||
<a th:href="@{/myapikey/modify/cancel(clientId=${apiKeyModification.clientId})}" class="btn btn-submit btn-secondary">
|
||||
취소
|
||||
</a>
|
||||
<button type="submit" form="modifyStep1Form" class="btn-primary btn-submit">
|
||||
다음
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</th:block>
|
||||
|
||||
<th:block layout:fragment="contentScript">
|
||||
<script th:inline="javascript">
|
||||
// IP address list management
|
||||
let ipAddresses = [];
|
||||
|
||||
// Remove app icon
|
||||
function removeAppIcon() {
|
||||
const appIconInput = document.getElementById('appIcon');
|
||||
const iconPreviewImages = document.querySelectorAll('img[id^="iconPreviewImage"]'); // Select all preview images
|
||||
const iconPlaceholder = document.getElementById('iconPlaceholder');
|
||||
const btnRemoveIcon = document.getElementById('btnRemoveIcon');
|
||||
|
||||
// Clear file input
|
||||
appIconInput.value = '';
|
||||
|
||||
// Hide all preview images
|
||||
iconPreviewImages.forEach(img => {
|
||||
img.style.display = 'none';
|
||||
if (img.id === 'iconPreviewImageJS') img.src = '';
|
||||
});
|
||||
|
||||
// Show placeholder
|
||||
iconPlaceholder.style.display = 'flex';
|
||||
|
||||
// Hide delete button
|
||||
btnRemoveIcon.style.display = 'none';
|
||||
}
|
||||
|
||||
// Add IP address to the list
|
||||
function addIpAddress() {
|
||||
const ipInput = document.getElementById('ipWhitelistInput');
|
||||
const ipValue = ipInput.value.trim();
|
||||
|
||||
// Validate IP format
|
||||
if (!ipValue) {
|
||||
customPopups.showAlert('IP 주소를 입력해주세요.');
|
||||
return;
|
||||
fileInput.value = '';
|
||||
if(preview1) preview1.style.display = 'none';
|
||||
if(preview2) preview2.style.display = 'none';
|
||||
previewJS.style.display = 'none';
|
||||
previewJS.src = '';
|
||||
placeholder.style.display = 'flex';
|
||||
removeBtn.style.display = 'none';
|
||||
}
|
||||
|
||||
// Basic IP validation (supports IP and CIDR notation)
|
||||
const ipPattern = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/;
|
||||
if (!ipPattern.test(ipValue)) {
|
||||
customPopups.showAlert('올바른 IP 주소 형식이 아닙니다.\n예: 192.168.1.1 또는 10.0.0.0/24');
|
||||
return;
|
||||
function addIpAddress() {
|
||||
const ipInput = document.getElementById('ipWhitelistInput');
|
||||
const ipValue = ipInput.value.trim();
|
||||
|
||||
if (!ipValue) {
|
||||
customPopups.showAlert('IP 주소를 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
const ipPattern = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/;
|
||||
if (!ipPattern.test(ipValue)) {
|
||||
customPopups.showAlert('올바른 IP 주소 형식이 아닙니다.\n예: 192.168.1.1 또는 10.0.0.0/24');
|
||||
return;
|
||||
}
|
||||
|
||||
if (ipAddresses.includes(ipValue)) {
|
||||
customPopups.showAlert('이미 추가된 IP 주소입니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
ipAddresses.push(ipValue);
|
||||
renderIpList();
|
||||
ipInput.value = '';
|
||||
ipInput.focus();
|
||||
}
|
||||
|
||||
// Check for duplicates
|
||||
if (ipAddresses.includes(ipValue)) {
|
||||
customPopups.showAlert('이미 추가된 IP 주소입니다.');
|
||||
return;
|
||||
function removeIpAddress(ip) {
|
||||
ipAddresses = ipAddresses.filter(function (i) { return i !== ip; });
|
||||
renderIpList();
|
||||
}
|
||||
|
||||
// Add to array
|
||||
ipAddresses.push(ipValue);
|
||||
function renderIpList() {
|
||||
const ipList = document.getElementById('ipList');
|
||||
const ipItems = document.getElementById('ipItems');
|
||||
const hiddenInput = document.getElementById('ipWhitelist');
|
||||
|
||||
// Update UI
|
||||
updateIpList();
|
||||
if (ipAddresses.length > 0) {
|
||||
ipList.style.display = 'block';
|
||||
ipItems.innerHTML = ipAddresses.map(function (ip) {
|
||||
return '<div class="s1-ip-item">' +
|
||||
'<div class="s1-ip-display">' + ip + '</div>' +
|
||||
'<button type="button" class="s1-ip-btn s1-ip-btn--remove" onclick="removeIpAddress(\'' + ip + '\')">' +
|
||||
'<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M19 11H5v2h14z"/></svg>' +
|
||||
'제거</button>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
} else {
|
||||
ipList.style.display = 'none';
|
||||
ipItems.innerHTML = '';
|
||||
}
|
||||
|
||||
// Clear input
|
||||
ipInput.value = '';
|
||||
ipInput.focus();
|
||||
}
|
||||
|
||||
// Remove IP address from the list
|
||||
function removeIpAddress(ip) {
|
||||
const index = ipAddresses.indexOf(ip);
|
||||
if (index > -1) {
|
||||
ipAddresses.splice(index, 1);
|
||||
updateIpList();
|
||||
}
|
||||
}
|
||||
|
||||
// Update IP list display
|
||||
function updateIpList() {
|
||||
const ipList = document.getElementById('ipList');
|
||||
const ipItems = document.getElementById('ipItems');
|
||||
const ipWhitelistHidden = document.getElementById('ipWhitelist');
|
||||
|
||||
// Show/hide list container
|
||||
if (ipAddresses.length > 0) {
|
||||
ipList.style.display = 'block';
|
||||
|
||||
// Build IP items HTML
|
||||
let itemsHTML = '';
|
||||
ipAddresses.forEach(function(ip) {
|
||||
itemsHTML += `
|
||||
<div class="ip-item">
|
||||
<div class="ip-display">${ip}</div>
|
||||
<button type="button" class="btn-remove-ip" onclick="removeIpAddress('${ip}')">
|
||||
- 제거
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
ipItems.innerHTML = itemsHTML;
|
||||
} else {
|
||||
ipList.style.display = 'none';
|
||||
ipItems.innerHTML = '';
|
||||
hiddenInput.value = ipAddresses.join(',');
|
||||
}
|
||||
|
||||
// Update hidden input with comma-separated values
|
||||
ipWhitelistHidden.value = ipAddresses.join(',');
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const form = document.getElementById('modifyStep1Form');
|
||||
const textarea = document.getElementById('appDescription');
|
||||
const counter = document.getElementById('charCounter');
|
||||
const fileInput = document.getElementById('appIconFile');
|
||||
const previewJS = document.getElementById('iconPreviewImageJS');
|
||||
const preview1 = document.getElementById('iconPreviewImage1');
|
||||
const preview2 = document.getElementById('iconPreviewImage2');
|
||||
const placeholder = document.getElementById('iconPlaceholder');
|
||||
const ipInput = document.getElementById('ipWhitelistInput');
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.getElementById('modifyStep1Form');
|
||||
const appDescTextarea = document.getElementById('appDescription');
|
||||
const charCounter = document.querySelector('.char-counter');
|
||||
const appIconInput = document.getElementById('appIcon');
|
||||
const iconPreviewImageJS = document.getElementById('iconPreviewImageJS');
|
||||
const iconPlaceholder = document.getElementById('iconPlaceholder');
|
||||
const ipWhitelistInput = document.getElementById('ipWhitelistInput');
|
||||
// Thymeleaf: Restore IP whitelist
|
||||
/*<![CDATA[*/
|
||||
const sessionIpWhitelist = /*[[${apiKeyModification.ipWhitelistAsString}]]*/ '';
|
||||
if (sessionIpWhitelist && sessionIpWhitelist.trim() !== '') {
|
||||
sessionIpWhitelist.split(',').forEach(function (ip) {
|
||||
const t = ip.trim();
|
||||
if (t && !ipAddresses.includes(t)) ipAddresses.push(t);
|
||||
});
|
||||
renderIpList();
|
||||
}
|
||||
/*]]>*/
|
||||
|
||||
// Restore existing IP whitelist
|
||||
/*<![CDATA[*/
|
||||
const sessionIpWhitelist = /*[[${apiKeyModification.ipWhitelistAsString}]]*/ '';
|
||||
if (sessionIpWhitelist && sessionIpWhitelist.trim() !== '') {
|
||||
const ips = sessionIpWhitelist.split(',');
|
||||
ips.forEach(function(ip) {
|
||||
const trimmedIp = ip.trim();
|
||||
if (trimmedIp && !ipAddresses.includes(trimmedIp)) {
|
||||
ipAddresses.push(trimmedIp);
|
||||
}
|
||||
});
|
||||
updateIpList();
|
||||
}
|
||||
/*]]>*/
|
||||
// Char counter init
|
||||
if (textarea && counter) {
|
||||
const max = textarea.getAttribute('maxlength') || 500;
|
||||
counter.textContent = textarea.value.length + ' / ' + max;
|
||||
|
||||
// Initialize character counter
|
||||
if (appDescTextarea && charCounter) {
|
||||
const currentLength = appDescTextarea.value.length;
|
||||
const maxLength = appDescTextarea.getAttribute('maxlength');
|
||||
charCounter.textContent = currentLength + ' / ' + maxLength;
|
||||
textarea.addEventListener('input', function () {
|
||||
counter.textContent = this.value.length + ' / ' + max;
|
||||
counter.style.color = this.value.length > max * 0.9 ? '#f4253c' : '#94a3b8';
|
||||
});
|
||||
}
|
||||
|
||||
appDescTextarea.addEventListener('input', function() {
|
||||
const currentLength = this.value.length;
|
||||
const maxLength = this.getAttribute('maxlength');
|
||||
charCounter.textContent = currentLength + ' / ' + maxLength;
|
||||
|
||||
if (currentLength > maxLength * 0.9) {
|
||||
charCounter.style.color = '#FF6B6B';
|
||||
} else {
|
||||
charCounter.style.color = '#94A3B8';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// App icon preview
|
||||
if (appIconInput) {
|
||||
appIconInput.addEventListener('change', function(e) {
|
||||
// File input change
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
const file = e.target.files[0];
|
||||
const btnRemoveIcon = document.getElementById('btnRemoveIcon');
|
||||
if (!file) return;
|
||||
|
||||
if (file) {
|
||||
if (file.size > 2 * 1024 * 1024) {
|
||||
customPopups.showAlert('파일 크기는 2MB를 초과할 수 없습니다.');
|
||||
this.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif'];
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
customPopups.showAlert('PNG, JPG, GIF 형식만 업로드 가능합니다.');
|
||||
this.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(event) {
|
||||
// Hide other previews
|
||||
document.querySelectorAll('img[id^="iconPreviewImage"]').forEach(img => img.style.display = 'none');
|
||||
|
||||
iconPreviewImageJS.src = event.target.result;
|
||||
iconPreviewImageJS.style.display = 'block';
|
||||
iconPlaceholder.style.display = 'none';
|
||||
btnRemoveIcon.style.display = 'flex';
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
if (file.size > 2 * 1024 * 1024) {
|
||||
customPopups.showAlert('파일 크기는 2MB를 초과할 수 없습니다.');
|
||||
this.value = '';
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// IP input Enter key
|
||||
if (ipWhitelistInput) {
|
||||
ipWhitelistInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
if (!['image/png', 'image/jpeg', 'image/jpg', 'image/gif'].includes(file.type)) {
|
||||
customPopups.showAlert('PNG, JPG, GIF 형식만 업로드 가능합니다.');
|
||||
this.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (ev) {
|
||||
if(preview1) preview1.style.display = 'none';
|
||||
if(preview2) preview2.style.display = 'none';
|
||||
previewJS.src = ev.target.result;
|
||||
previewJS.style.display = 'block';
|
||||
placeholder.style.display = 'none';
|
||||
document.getElementById('btnRemoveIcon').style.display = 'flex';
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
// Drag & drop
|
||||
const dropZone = document.getElementById('iconDropZone');
|
||||
if (dropZone) {
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
addIpAddress();
|
||||
this.classList.add('drag-over');
|
||||
});
|
||||
dropZone.addEventListener('dragleave', function () {
|
||||
this.classList.remove('drag-over');
|
||||
});
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
this.classList.remove('drag-over');
|
||||
if (e.dataTransfer.files.length > 0) {
|
||||
fileInput.files = e.dataTransfer.files;
|
||||
fileInput.dispatchEvent(new Event('change'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// IP input Enter key
|
||||
ipInput.addEventListener('keypress', function (e) {
|
||||
if (e.key === 'Enter') { e.preventDefault(); addIpAddress(); }
|
||||
});
|
||||
|
||||
// Form submit validation
|
||||
form.addEventListener('submit', function (e) {
|
||||
const name = document.getElementById('appName').value.trim();
|
||||
const desc = textarea.value.trim();
|
||||
const url = document.getElementById('callbackUrl').value.trim();
|
||||
|
||||
if (!name) {
|
||||
e.preventDefault();
|
||||
customPopups.showAlert('앱 이름을 입력해주세요.');
|
||||
document.getElementById('appName').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!desc) {
|
||||
e.preventDefault();
|
||||
customPopups.showAlert('앱 설명을 입력해주세요.');
|
||||
textarea.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (url) {
|
||||
try { new URL(url); } catch (_) {
|
||||
e.preventDefault();
|
||||
customPopups.showAlert('올바른 URL 형식이 아닙니다.\n예: https://example.com/callback');
|
||||
document.getElementById('callbackUrl').focus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Form validation
|
||||
form.addEventListener('submit', function(e) {
|
||||
const appName = document.getElementById('appName').value.trim();
|
||||
const appDescription = document.getElementById('appDescription').value.trim();
|
||||
const callbackUrl = document.getElementById('callbackUrl').value.trim();
|
||||
|
||||
if (!appName) {
|
||||
e.preventDefault();
|
||||
customPopups.showAlert('앱 이름을 입력해주세요.');
|
||||
document.getElementById('appName').focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!appDescription) {
|
||||
e.preventDefault();
|
||||
customPopups.showAlert('앱 설명을 입력해주세요.');
|
||||
document.getElementById('appDescription').focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (callbackUrl && callbackUrl.trim() !== '') {
|
||||
try {
|
||||
new URL(callbackUrl);
|
||||
} catch (error) {
|
||||
e.preventDefault();
|
||||
customPopups.showAlert('올바른 URL 형식이 아닙니다.');
|
||||
document.getElementById('callbackUrl').focus();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</th:block>
|
||||
|
||||
</script>
|
||||
</th:block>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -6,201 +6,201 @@
|
||||
<body>
|
||||
<section layout:fragment="title">
|
||||
<div class="page-title-banner">
|
||||
<img th:src="@{/img/img_title_bg.png}" class="title-image">
|
||||
<img th:src="@{/img/img_title_bg.png}" class="title-image">
|
||||
<h1>앱관리</h1>
|
||||
</div>
|
||||
</section>
|
||||
<th:block layout:fragment="contentFragment">
|
||||
<section class="apikey-register-container">
|
||||
<div class="signup-guide-v2 figma-register-wrapper">
|
||||
<div class="service-main app-management-layout">
|
||||
|
||||
<!-- Title Bar -->
|
||||
<div class="common-title-bar">
|
||||
<h2 class="common-title">앱 수정</h2>
|
||||
<span class="common-subtitle">서비스 중인 앱 정보를 수정합니다.</span>
|
||||
</div>
|
||||
<!-- Left Sidebar (Service Sidebar) -->
|
||||
<th:block th:replace="~{fragment/djbank/service_sidebar :: sidebar('apiKey')}"></th:block>
|
||||
|
||||
<!-- Progress Indicator -->
|
||||
<div class="register-progress">
|
||||
<div class="progress-steps">
|
||||
<!-- Step 1: 앱 정보입력 (Completed) -->
|
||||
<div class="progress-step-item">
|
||||
<div class="step-icon-wrapper">
|
||||
<div class="step-icon">
|
||||
<img th:src="@{/img/apikey_step1.png}" alt="앱 정보입력" width="36" height="36">
|
||||
<!-- Content Area -->
|
||||
<div class="app-management-content">
|
||||
<div class="step2-wrap">
|
||||
|
||||
<!-- Title -->
|
||||
<h2 class="s2-title">앱 수정</h2>
|
||||
|
||||
<!-- Progress Steps Card -->
|
||||
<div class="s1-progress-card">
|
||||
<div class="s1-steps">
|
||||
<!-- Step 1: 앱 정보입력 (completed) -->
|
||||
<div class="s1-step">
|
||||
<div class="s1-step-circle">
|
||||
<!-- 입력폼 아이콘 -->
|
||||
<svg class="s1-step-svg" width="26" height="26" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="3" y="5" width="18" height="14" rx="2" stroke="currentColor" stroke-width="1.8" />
|
||||
<line x1="7" y1="9" x2="17" y2="9" stroke="currentColor" stroke-width="1.8"
|
||||
stroke-linecap="round" />
|
||||
<line x1="7" y1="12" x2="14" y2="12" stroke="currentColor" stroke-width="1.8"
|
||||
stroke-linecap="round" />
|
||||
<line x1="7" y1="15" x2="11" y2="15" stroke="currentColor" stroke-width="1.8"
|
||||
stroke-linecap="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="s1-step-num">1단계</span>
|
||||
<span class="s1-step-name">앱 정보수정</span>
|
||||
</div>
|
||||
|
||||
<div class="s1-step-line"></div>
|
||||
|
||||
<!-- Step 2 Active: API 선택 (chain link icon) -->
|
||||
<div class="s1-step s1-step--active">
|
||||
<div class="s1-step-circle">
|
||||
<!-- 체인링크/API 아이콘 -->
|
||||
<svg class="s1-step-svg" width="26" height="26" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" stroke="currentColor"
|
||||
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" stroke="currentColor"
|
||||
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="s1-step-num">2단계</span>
|
||||
<span class="s1-step-name">API 선택</span>
|
||||
</div>
|
||||
|
||||
<div class="s1-step-line"></div>
|
||||
|
||||
<!-- Step 3: 앱 생성 완료 (rocket icon) -->
|
||||
<div class="s1-step">
|
||||
<div class="s1-step-circle">
|
||||
<!-- 로켓 아이콘 -->
|
||||
<svg class="s1-step-svg" width="26" height="26" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"
|
||||
stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path
|
||||
d="M12 15l-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"
|
||||
stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" stroke="currentColor" stroke-width="1.6"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" stroke="currentColor" stroke-width="1.6"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="s1-step-num">3단계</span>
|
||||
<span class="s1-step-name">앱 수정 완료</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-label">앱 정보입력</div>
|
||||
</div>
|
||||
|
||||
<!-- Dots between Step 1 and 2 -->
|
||||
<div class="step-dots">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: API 선택 (Active) -->
|
||||
<div class="progress-step-item active">
|
||||
<div class="step-icon-wrapper">
|
||||
<div class="step-icon">
|
||||
<img th:src="@{/img/apikey_step2.png}" alt="API 선택" width="36" height="36">
|
||||
<!-- Category Carousel Tab Container -->
|
||||
<div class="s2-category-carousel-container">
|
||||
<button type="button" class="s2-carousel-btn s2-carousel-btn--prev" id="btnPrevCategory" aria-label="이전 카테고리">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="15 18 9 12 15 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="s2-category-list-wrapper" id="categoryListWrapper">
|
||||
<div class="s2-category-list" id="categoryList">
|
||||
<button type="button" class="s2-category-tab active" data-group="">
|
||||
전체
|
||||
</button>
|
||||
<button type="button" class="s2-category-tab" th:each="service : ${apiServices}" th:data-group="${service.id}" th:text="${service.groupName}">
|
||||
서비스명
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="s2-carousel-btn s2-carousel-btn--next" id="btnNextCategory" aria-label="다음 카테고리">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="step-label">API 선택</div>
|
||||
</div>
|
||||
|
||||
<!-- Dots between Step 2 and 3 -->
|
||||
<div class="step-dots">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<!-- Main Content Area -->
|
||||
<div class="s2-selection-container">
|
||||
<main class="s2-content-area">
|
||||
<form id="modifyStep2Form" method="post" th:action="@{/myapikey/modify/step2}" th:object="${apiKeyModification}" class="s2-form">
|
||||
|
||||
<!-- Step 3: 앱 수정 완료 -->
|
||||
<div class="progress-step-item">
|
||||
<div class="step-icon-wrapper">
|
||||
<div class="step-icon">
|
||||
<img th:src="@{/img/apikey_step3.png}" alt="앱 수정 완료" width="36" height="36">
|
||||
</div>
|
||||
<!-- Hidden field for clientId -->
|
||||
<input type="hidden" th:field="*{clientId}"/>
|
||||
|
||||
<!-- Header filter: Search and Select All -->
|
||||
<div class="s2-filter-header">
|
||||
<div class="s2-select-all" id="selectAllWrapper" style="display: none;">
|
||||
<label class="s2-select-all-label">
|
||||
<input type="checkbox" id="selectAllCheckbox" class="visually-hidden">
|
||||
<span class="s2-checkbox-custom"></span>
|
||||
<span id="selectAllText">전체 선택</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="s2-result-count">
|
||||
총 <strong id="apiResultCount">0</strong>건
|
||||
</div>
|
||||
<div class="s2-search-box">
|
||||
<input type="text" id="apiSearch" class="s2-search-input" placeholder="API 검색...">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.23047 0C14.3285 0 18.4618 4.0539 18.4619 9.05469C18.4619 11.1083 17.7634 13.0014 16.5889 14.5205C16.5913 14.5229 16.5942 14.5249 16.5967 14.5273L19.5498 17.4238C20.1502 18.0131 20.1501 18.9683 19.5498 19.5576C18.949 20.147 17.9748 20.147 17.374 19.5576L14.4209 16.6621C14.3966 16.6383 14.3749 16.6119 14.3525 16.5869C12.8868 17.5478 11.1257 18.1094 9.23047 18.1094C4.13258 18.1092 0 14.0554 0 9.05469C0.000110268 4.05404 4.13265 0.000222701 9.23047 0ZM9.23047 3.01855C5.83201 3.01878 3.07726 5.721 3.07715 9.05469C3.07715 12.3885 5.83194 15.0916 9.23047 15.0918C12.6292 15.0918 15.3848 12.3886 15.3848 9.05469C15.3847 5.72086 12.6291 3.01855 9.23047 3.01855Z" fill="#515961"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Cards Grid -->
|
||||
<div class="s2-cards-grid" id="apiCardGrid">
|
||||
<!-- Loading state -->
|
||||
<div class="s2-loading" id="loadingState" style="grid-column: 1 / -1;">
|
||||
<div class="s2-spinner"></div>
|
||||
<p>API 목록을 불러오는 중...</p>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div class="s2-empty" id="emptyState" style="display: none; grid-column: 1 / -1;">
|
||||
<div class="s2-empty-icon">📦</div>
|
||||
<h3>API를 선택해주세요</h3>
|
||||
<p>상단 카테고리에서 서비스를 선택하면 해당 API 목록이 표시됩니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
<div class="step-label">앱 수정 완료</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Navigation Actions -->
|
||||
<div class="s2-actions">
|
||||
<button type="button" id="btnPrevStep" class="s2-btn-prev">
|
||||
이전
|
||||
</button>
|
||||
<button type="submit" form="modifyStep2Form" class="s2-btn-save">
|
||||
다음
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div><!-- /step2-wrap -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div th:if="${error}" class="alert alert-error">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="12" y1="8" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
||||
</svg>
|
||||
<span th:text="${error}"></span>
|
||||
</div>
|
||||
|
||||
<!-- Form Content with Sidebar Layout -->
|
||||
<div class="register-form-container with-sidebar">
|
||||
|
||||
<!-- Sidebar Navigation -->
|
||||
<aside class="apikey-register-sidebar" id="apiSidebar">
|
||||
<nav class="apikey-sidebar-nav">
|
||||
<!-- Sidebar Title -->
|
||||
<div class="sidebar-title">API 서비스 목록</div>
|
||||
|
||||
<!-- All APIs -->
|
||||
<div class="menu-section">
|
||||
<div class="menu-title active" data-group="">
|
||||
<span class="menu-text">전체</span>
|
||||
<span class="api-count"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Service Categories -->
|
||||
<div class="menu-section" th:each="service : ${apiServices}">
|
||||
<div class="menu-title"
|
||||
th:data-group="${service.id}">
|
||||
<span class="menu-text" th:text="${service.groupName}">서비스명</span>
|
||||
<span class="api-count"></span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Mobile Menu Toggle -->
|
||||
<button class="apikey-mobile-toggle" id="mobileToggle" aria-label="메뉴 열기">
|
||||
☰
|
||||
</button>
|
||||
|
||||
<!-- Mobile Overlay -->
|
||||
<div class="apikey-mobile-overlay" id="mobileOverlay"></div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="apikey-register-content">
|
||||
<form id="modifyStep2Form" method="post" th:action="@{/myapikey/modify/step2}" th:object="${apiKeyModification}" class="register-form">
|
||||
|
||||
<!-- Hidden field for clientId -->
|
||||
<input type="hidden" th:field="*{clientId}"/>
|
||||
|
||||
<!-- Search Box with Select All -->
|
||||
<div class="api-filter-header">
|
||||
<div class="select-all-wrapper" id="selectAllWrapper" style="display: none;">
|
||||
<label class="select-all-label">
|
||||
<input type="checkbox" id="selectAllCheckbox" class="select-all-checkbox visually-hidden">
|
||||
<span class="custom-checkbox"></span>
|
||||
<span id="selectAllText">전체 선택</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="api-search-box">
|
||||
<input type="text"
|
||||
id="apiSearch"
|
||||
class="search-input"
|
||||
placeholder="API 검색...">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.23047 0C14.3285 0 18.4618 4.0539 18.4619 9.05469C18.4619 11.1083 17.7634 13.0014 16.5889 14.5205C16.5913 14.5229 16.5942 14.5249 16.5967 14.5273L19.5498 17.4238C20.1502 18.0131 20.1501 18.9683 19.5498 19.5576C18.949 20.147 17.9748 20.147 17.374 19.5576L14.4209 16.6621C14.3966 16.6383 14.3749 16.6119 14.3525 16.5869C12.8868 17.5478 11.1257 18.1094 9.23047 18.1094C4.13258 18.1092 0 14.0554 0 9.05469C0.000110268 4.05404 4.13265 0.000222701 9.23047 0ZM9.23047 3.01855C5.83201 3.01878 3.07726 5.721 3.07715 9.05469C3.07715 12.3885 5.83194 15.0916 9.23047 15.0918C12.6292 15.0918 15.3848 12.3886 15.3848 9.05469C15.3847 5.72086 12.6291 3.01855 9.23047 3.01855Z" fill="#515961"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Cards Grid -->
|
||||
<div class="api-selection-card-grid" id="apiCardGrid">
|
||||
<!-- Loading state -->
|
||||
<div class="loading-state" id="loadingState" style="grid-column: 1 / -1;">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>API 목록을 불러오는 중...</p>
|
||||
</div>
|
||||
|
||||
<!-- Empty state (initially hidden) -->
|
||||
<div class="empty-api-state" id="emptyState" style="display: none; grid-column: 1 / -1;">
|
||||
<div class="empty-icon">📦</div>
|
||||
<h3>API를 선택해주세요</h3>
|
||||
<p>왼쪽 메뉴에서 서비스를 선택하면 해당 API 목록이 표시됩니다.</p>
|
||||
</div>
|
||||
|
||||
<!-- API cards will be dynamically loaded here -->
|
||||
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="form-actions">
|
||||
<button type="button" id="btnPrevStep" class="btn-submit btn-secondary">
|
||||
이전
|
||||
</button>
|
||||
<button type="submit" form="modifyStep2Form" class="btn-submit btn-primary">
|
||||
저장
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
<!-- Floating Cart Button -->
|
||||
<button type="button" class="floating-cart-btn" id="floatingCartBtn" style="display: none;">
|
||||
<span class="cart-label">선택된 API</span>
|
||||
<span class="cart-badge" id="cartBadge">
|
||||
<span class="cart-count">0</span>
|
||||
<span class="cart-unit">개</span>
|
||||
<button type="button" class="s2-floating-cart" id="floatingCartBtn" style="display: none;">
|
||||
<span class="s2-cart-label">선택된 API</span>
|
||||
<span class="s2-cart-badge" id="cartBadge">
|
||||
<span class="s2-cart-count">0</span>
|
||||
<span class="s2-cart-unit">개</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<!-- Selected APIs Modal - Uses base modal component -->
|
||||
<div class="modal" id="selectedApisModal" style="display: none;">
|
||||
<div class="modal-backdrop" id="modalOverlay"></div>
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">선택된 API 목록</h3>
|
||||
<button type="button" class="modal-close" id="modalCloseBtn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Selected APIs Modal -->
|
||||
<div class="s2-modal" id="selectedApisModal" style="display: none;">
|
||||
<div class="s2-modal-backdrop" id="modalOverlay"></div>
|
||||
<div class="s2-modal-dialog">
|
||||
<div class="s2-modal-header">
|
||||
<h3 class="s2-modal-title">선택된 API 목록</h3>
|
||||
<button type="button" class="s2-modal-close" id="modalCloseBtn">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="selected-apis-list" id="modalSelectedList">
|
||||
<div class="s2-modal-body">
|
||||
<div class="s2-selected-list" id="modalSelectedList">
|
||||
<!-- Dynamically populated -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" id="modalCancelBtn">닫기</button>
|
||||
<div class="s2-modal-footer">
|
||||
<button type="button" class="s2-btn-close-modal" id="modalCancelBtn">닫기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -212,10 +212,7 @@
|
||||
// DOM Elements
|
||||
const form = document.getElementById('modifyStep2Form');
|
||||
const searchInput = document.getElementById('apiSearch');
|
||||
const sidebar = document.getElementById('apiSidebar');
|
||||
const mobileToggle = document.getElementById('mobileToggle');
|
||||
const mobileOverlay = document.getElementById('mobileOverlay');
|
||||
const menuTitles = document.querySelectorAll('.menu-title');
|
||||
const menuTitles = document.querySelectorAll('.s2-category-tab');
|
||||
const apiCardGrid = document.getElementById('apiCardGrid');
|
||||
const loadingState = document.getElementById('loadingState');
|
||||
const emptyState = document.getElementById('emptyState');
|
||||
@@ -226,7 +223,7 @@
|
||||
let allLoadedApis = []; // Store ALL APIs from server for modal display
|
||||
let selectedApis = new Set(); // Track selected API IDs
|
||||
|
||||
// Restore selected APIs from session (for modification)
|
||||
// Restore selected APIs from session
|
||||
const sessionSelectedApis = /*[[${apiKeyModification.selectedApis}]]*/ [];
|
||||
if (sessionSelectedApis && Array.isArray(sessionSelectedApis)) {
|
||||
sessionSelectedApis.forEach(function(apiId) {
|
||||
@@ -236,11 +233,11 @@
|
||||
|
||||
// Load all APIs for modal display (used when APIs from different categories are selected)
|
||||
function loadAllApisForModal() {
|
||||
fetch('/apis/for_request')
|
||||
const baseUrl = /*[[@{/apis/for_request}]]*/ '/apis/for_request';
|
||||
fetch(baseUrl)
|
||||
.then(response => response.json())
|
||||
.then(apis => {
|
||||
allLoadedApis = apis;
|
||||
console.log('Loaded', allLoadedApis.length, 'APIs for modal display');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Failed to load all APIs for modal:', error);
|
||||
@@ -254,10 +251,11 @@
|
||||
emptyState.style.display = 'none';
|
||||
|
||||
// Remove existing API cards
|
||||
document.querySelectorAll('.api-selection-card').forEach(card => card.remove());
|
||||
document.querySelectorAll('.s2-api-card').forEach(card => card.remove());
|
||||
|
||||
// Build URL with optional groupId filter
|
||||
let url = '/apis/for_request';
|
||||
// Build URL with optional groupId filter (Thymeleaf contextPath safe)
|
||||
const baseUrl = /*[[@{/apis/for_request}]]*/ '/apis/for_request';
|
||||
let url = baseUrl;
|
||||
if (groupId) {
|
||||
url += '?groupIds=' + encodeURIComponent(groupId);
|
||||
}
|
||||
@@ -268,9 +266,11 @@
|
||||
|
||||
if (apis.length === 0) {
|
||||
emptyState.style.display = 'block';
|
||||
document.getElementById('apiResultCount').textContent = '0';
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('apiResultCount').textContent = apis.length;
|
||||
renderApiCards(apis);
|
||||
updateSelectAllUI();
|
||||
}).catch(error => {
|
||||
@@ -279,6 +279,7 @@
|
||||
emptyState.querySelector('h3').textContent = 'API 로드 실패';
|
||||
emptyState.querySelector('p').textContent = '다시 시도해주세요.';
|
||||
emptyState.style.display = 'block';
|
||||
document.getElementById('apiResultCount').textContent = '0';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -303,7 +304,7 @@
|
||||
// Create API card element
|
||||
function createApiCard(api) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'api-selection-card';
|
||||
card.className = 's2-api-card';
|
||||
card.setAttribute('data-group', api.apiGroupId || '');
|
||||
card.setAttribute('data-name', (api.apiName || '').toLowerCase());
|
||||
card.setAttribute('data-desc', (api.apiSimpleDescription || '').toLowerCase());
|
||||
@@ -319,26 +320,25 @@
|
||||
: `<i class="fas fa-cube"></i>`;
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="api-card-content">
|
||||
<div class="api-card-header">
|
||||
<span class="api-card-category">${api.service || '카테고리'}</span>
|
||||
<div class="checkbox-wrapper">
|
||||
<input type="checkbox"
|
||||
name="selectedApis"
|
||||
value="${api.apiId}"
|
||||
id="api-${api.apiId}"
|
||||
class="api-checkbox visually-hidden"
|
||||
${isSelected ? 'checked' : ''}>
|
||||
<span class="custom-checkbox"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="s2-api-card-badge">
|
||||
<span>${api.apiGroupName || '카테고리'}</span>
|
||||
</div>
|
||||
<!-- Checkbox container with visible custom design -->
|
||||
<label class="s2-checkbox-wrapper">
|
||||
<input type="checkbox"
|
||||
name="selectedApis"
|
||||
value="${api.apiId}"
|
||||
id="api-${api.apiId}"
|
||||
class="s2-api-checkbox visually-hidden"
|
||||
${isSelected ? 'checked' : ''}>
|
||||
<span class="s2-checkbox-custom"></span>
|
||||
</label>
|
||||
|
||||
<h3 class="api-name">${api.apiName || 'API 이름'}</h3>
|
||||
<p class="api-description">${api.apiSimpleDescription || 'API 설명이 없습니다.'}</p>
|
||||
<h3 class="s2-api-card-title">${api.apiName || 'API 이름'}</h3>
|
||||
<p class="s2-api-card-desc">${api.apiSimpleDescription || 'API 설명이 없습니다.'}</p>
|
||||
|
||||
<div class="api-card-icon">
|
||||
${mainIconHtml}
|
||||
</div>
|
||||
<div class="s2-api-card-image">
|
||||
${mainIconHtml}
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -347,23 +347,11 @@
|
||||
|
||||
// Attach event listeners to cards
|
||||
function attachCardEventListeners() {
|
||||
const checkboxes = document.querySelectorAll('.api-checkbox');
|
||||
const apiCards = document.querySelectorAll('.api-selection-card');
|
||||
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', function() {
|
||||
updateCardSelection(this);
|
||||
updateSelectedCount();
|
||||
});
|
||||
});
|
||||
const apiCards = document.querySelectorAll('.s2-api-card');
|
||||
|
||||
apiCards.forEach(card => {
|
||||
card.addEventListener('click', function(e) {
|
||||
// Prevent double toggle if clicking directly on checkbox
|
||||
if (e.target.classList.contains('api-checkbox')) {
|
||||
return;
|
||||
}
|
||||
const checkbox = card.querySelector('.api-checkbox');
|
||||
const checkbox = card.querySelector('.s2-api-checkbox');
|
||||
if (checkbox) {
|
||||
checkbox.checked = !checkbox.checked;
|
||||
updateCardSelection(checkbox);
|
||||
@@ -371,11 +359,28 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Prevent double toggle when clicking the checkbox wrapper
|
||||
const checkboxWrappers = document.querySelectorAll('.s2-checkbox-wrapper');
|
||||
checkboxWrappers.forEach(wrapper => {
|
||||
wrapper.addEventListener('click', function(e) {
|
||||
e.stopPropagation(); // Stop click from bubbling to card!
|
||||
});
|
||||
|
||||
// Listen to checkbox change event inside it
|
||||
const checkbox = wrapper.querySelector('.s2-api-checkbox');
|
||||
if (checkbox) {
|
||||
checkbox.addEventListener('change', function() {
|
||||
updateCardSelection(this);
|
||||
updateSelectedCount();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update card visual state
|
||||
function updateCardSelection(checkbox) {
|
||||
const card = checkbox.closest('.api-selection-card');
|
||||
const card = checkbox.closest('.s2-api-card');
|
||||
if (checkbox.checked) {
|
||||
card.classList.add('selected');
|
||||
selectedApis.add(checkbox.value);
|
||||
@@ -389,7 +394,7 @@
|
||||
function updateSelectedCount() {
|
||||
// Update floating cart button
|
||||
const floatingCartBtn = document.getElementById('floatingCartBtn');
|
||||
const cartCount = document.querySelector('.cart-count');
|
||||
const cartCount = document.querySelector('.s2-cart-count');
|
||||
|
||||
if (selectedApis.size > 0) {
|
||||
floatingCartBtn.style.display = 'flex';
|
||||
@@ -425,7 +430,7 @@
|
||||
// Update select all checkbox state based on visible cards
|
||||
function updateSelectAllCheckboxState() {
|
||||
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||
const visibleCards = Array.from(document.querySelectorAll('.api-selection-card')).filter(card => card.style.display !== 'none');
|
||||
const visibleCards = Array.from(document.querySelectorAll('.s2-api-card')).filter(card => card.style.display !== 'none');
|
||||
|
||||
if (visibleCards.length === 0) {
|
||||
selectAllCheckbox.checked = false;
|
||||
@@ -433,7 +438,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const visibleCheckboxes = visibleCards.map(card => card.querySelector('.api-checkbox'));
|
||||
const visibleCheckboxes = visibleCards.map(card => card.querySelector('.s2-api-checkbox'));
|
||||
const checkedCount = visibleCheckboxes.filter(cb => cb.checked).length;
|
||||
|
||||
if (checkedCount === 0) {
|
||||
@@ -454,21 +459,21 @@
|
||||
modalSelectedList.innerHTML = '';
|
||||
|
||||
if (selectedApis.size === 0) {
|
||||
modalSelectedList.innerHTML = '<p class="empty-message">선택된 API가 없습니다.</p>';
|
||||
modalSelectedList.innerHTML = '<p class="s2-empty-message">선택된 API가 없습니다.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Build list using selectedApis Set for consistency
|
||||
selectedApis.forEach(function(apiId) {
|
||||
// Try to find the checkbox in the DOM first
|
||||
const checkbox = document.querySelector('.api-checkbox[value="' + apiId + '"]');
|
||||
const checkbox = document.querySelector('.s2-api-checkbox[value="' + apiId + '"]');
|
||||
let apiName = apiId; // Default to ID if we can't find the name
|
||||
|
||||
if (checkbox) {
|
||||
// If checkbox exists in DOM, get the name from the card
|
||||
const card = checkbox.closest('.api-selection-card');
|
||||
const card = checkbox.closest('.s2-api-card');
|
||||
if (card) {
|
||||
const nameElement = card.querySelector('.api-name');
|
||||
const nameElement = card.querySelector('.s2-api-card-title');
|
||||
if (nameElement) {
|
||||
apiName = nameElement.textContent;
|
||||
}
|
||||
@@ -477,7 +482,7 @@
|
||||
// If checkbox not in DOM (different category is showing),
|
||||
// try to find the API in our loaded data
|
||||
let api = allApis.find(a => a.apiId === apiId);
|
||||
if (!api && allLoadedApis.length > 0) {
|
||||
if (!api && allLoadedApis && allLoadedApis.length > 0) {
|
||||
api = allLoadedApis.find(a => a.apiId === apiId);
|
||||
}
|
||||
if (api && api.apiName) {
|
||||
@@ -486,24 +491,19 @@
|
||||
}
|
||||
|
||||
const apiPill = document.createElement('div');
|
||||
apiPill.className = 'api-pill';
|
||||
apiPill.className = 's2-api-pill';
|
||||
apiPill.innerHTML = `
|
||||
<span class="api-pill-name">${apiName}</span>
|
||||
<button type="button" class="api-pill-remove" data-value="${apiId}" aria-label="Remove ${apiName}">
|
||||
<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">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<span class="s2-api-pill-name">${apiName}</span>
|
||||
<button type="button" class="s2-api-pill-remove" data-value="${apiId}" aria-label="Remove ${apiName}">✕</button>
|
||||
`;
|
||||
modalSelectedList.appendChild(apiPill);
|
||||
});
|
||||
|
||||
// Add remove handlers
|
||||
document.querySelectorAll('.api-pill-remove').forEach(function(btn) {
|
||||
document.querySelectorAll('.s2-api-pill-remove').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
const value = this.getAttribute('data-value');
|
||||
const checkbox = document.querySelector('.api-checkbox[value="' + value + '"]');
|
||||
const checkbox = document.querySelector('.s2-api-checkbox[value="' + value + '"]');
|
||||
if (checkbox) {
|
||||
checkbox.checked = false;
|
||||
updateCardSelection(checkbox);
|
||||
@@ -523,7 +523,8 @@
|
||||
searchInput.addEventListener('input', function() {
|
||||
const searchTerm = this.value.toLowerCase();
|
||||
|
||||
const apiCards = document.querySelectorAll('.api-selection-card');
|
||||
const apiCards = document.querySelectorAll('.s2-api-card');
|
||||
let visibleCount = 0;
|
||||
apiCards.forEach(function(card) {
|
||||
const apiName = card.getAttribute('data-name');
|
||||
const apiDesc = card.getAttribute('data-desc');
|
||||
@@ -533,17 +534,20 @@
|
||||
|
||||
if (matchesSearch) {
|
||||
card.style.display = '';
|
||||
visibleCount++;
|
||||
} else {
|
||||
card.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('apiResultCount').textContent = visibleCount;
|
||||
|
||||
// Update select all checkbox state after search
|
||||
updateSelectAllCheckboxState();
|
||||
});
|
||||
}
|
||||
|
||||
// Sidebar category selection
|
||||
// Category tab selection
|
||||
menuTitles.forEach(function(title) {
|
||||
title.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
@@ -557,7 +561,7 @@
|
||||
currentFilter = groupId;
|
||||
|
||||
// Store service name for select all label
|
||||
currentServiceName = this.textContent.trim().split('\n')[0].trim();
|
||||
currentServiceName = this.textContent.trim();
|
||||
|
||||
// Load APIs for selected service
|
||||
loadApis(groupId);
|
||||
@@ -566,51 +570,40 @@
|
||||
if (searchInput) {
|
||||
searchInput.value = '';
|
||||
}
|
||||
|
||||
// Close mobile menu if open
|
||||
if (window.innerWidth <= 768 && sidebar.classList.contains('mobile-open')) {
|
||||
closeMobileMenu();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Mobile menu toggle
|
||||
function closeMobileMenu() {
|
||||
sidebar.classList.remove('mobile-open');
|
||||
mobileOverlay.classList.remove('active');
|
||||
mobileToggle.innerHTML = '☰';
|
||||
mobileToggle.setAttribute('aria-label', '메뉴 열기');
|
||||
}
|
||||
// Category Carousel Scroll
|
||||
const categoryListWrapper = document.getElementById('categoryListWrapper');
|
||||
const btnPrevCategory = document.getElementById('btnPrevCategory');
|
||||
const btnNextCategory = document.getElementById('btnNextCategory');
|
||||
|
||||
if (mobileToggle && sidebar && mobileOverlay) {
|
||||
mobileToggle.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (categoryListWrapper && btnPrevCategory && btnNextCategory) {
|
||||
const scrollAmount = 200;
|
||||
|
||||
sidebar.classList.toggle('mobile-open');
|
||||
mobileOverlay.classList.toggle('active');
|
||||
|
||||
if (sidebar.classList.contains('mobile-open')) {
|
||||
this.innerHTML = '✕';
|
||||
this.setAttribute('aria-label', '메뉴 닫기');
|
||||
} else {
|
||||
this.innerHTML = '☰';
|
||||
this.setAttribute('aria-label', '메뉴 열기');
|
||||
}
|
||||
btnPrevCategory.addEventListener('click', function() {
|
||||
categoryListWrapper.scrollBy({ left: -scrollAmount, behavior: 'smooth' });
|
||||
});
|
||||
|
||||
// Close menu on overlay click
|
||||
mobileOverlay.addEventListener('click', function() {
|
||||
closeMobileMenu();
|
||||
btnNextCategory.addEventListener('click', function() {
|
||||
categoryListWrapper.scrollBy({ left: scrollAmount, behavior: 'smooth' });
|
||||
});
|
||||
}
|
||||
|
||||
// Close mobile menu on resize to desktop
|
||||
window.addEventListener('resize', function() {
|
||||
if (window.innerWidth > 768 && sidebar.classList.contains('mobile-open')) {
|
||||
closeMobileMenu();
|
||||
// Toggle buttons visibility/disabled state based on scroll position
|
||||
function updateCarouselButtons() {
|
||||
const scrollLeft = categoryListWrapper.scrollLeft;
|
||||
const maxScrollLeft = categoryListWrapper.scrollWidth - categoryListWrapper.clientWidth;
|
||||
|
||||
btnPrevCategory.disabled = scrollLeft <= 0;
|
||||
btnNextCategory.disabled = scrollLeft >= maxScrollLeft - 1;
|
||||
}
|
||||
});
|
||||
|
||||
categoryListWrapper.addEventListener('scroll', updateCarouselButtons);
|
||||
window.addEventListener('resize', updateCarouselButtons);
|
||||
|
||||
// Initial check after loading categories
|
||||
setTimeout(updateCarouselButtons, 150);
|
||||
}
|
||||
|
||||
// Modal control
|
||||
const floatingCartBtn = document.getElementById('floatingCartBtn');
|
||||
@@ -680,10 +673,10 @@
|
||||
if (selectAllCheckbox) {
|
||||
selectAllCheckbox.addEventListener('change', function() {
|
||||
const isChecked = this.checked;
|
||||
const visibleCards = Array.from(document.querySelectorAll('.api-selection-card')).filter(card => card.style.display !== 'none');
|
||||
const visibleCards = Array.from(document.querySelectorAll('.s2-api-card')).filter(card => card.style.display !== 'none');
|
||||
|
||||
visibleCards.forEach(function(card) {
|
||||
const checkbox = card.querySelector('.api-checkbox');
|
||||
const checkbox = card.querySelector('.s2-api-checkbox');
|
||||
if (checkbox) {
|
||||
checkbox.checked = isChecked;
|
||||
updateCardSelection(checkbox);
|
||||
@@ -700,9 +693,8 @@
|
||||
btnPrevStep.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Change form action to save endpoint
|
||||
const originalAction = form.action;
|
||||
form.action = '/myapikey/modify/step2/save';
|
||||
// Change form action to save endpoint (Thymeleaf contextPath safe)
|
||||
form.action = /*[[@{/myapikey/modify/step2/save}]]*/ '/myapikey/modify/step2/save';
|
||||
|
||||
// Submit the form to save selections
|
||||
form.submit();
|
||||
@@ -711,7 +703,6 @@
|
||||
|
||||
// Form validation
|
||||
form.addEventListener('submit', function(e) {
|
||||
// API 선택 필수 검증
|
||||
if (selectedApis.size === 0) {
|
||||
e.preventDefault();
|
||||
customPopups.showAlert('최소 1개 이상의 API를 선택해주세요.');
|
||||
@@ -720,18 +711,12 @@
|
||||
});
|
||||
|
||||
// Initialize
|
||||
// Load all APIs for modal display FIRST (async)
|
||||
loadAllApisForModal();
|
||||
|
||||
updateSelectedCount(); // This will show count from restored session data
|
||||
|
||||
// Load all APIs automatically on page load (empty string = all APIs)
|
||||
loadApis('');
|
||||
|
||||
// Log restored selections for debugging
|
||||
if (selectedApis.size > 0) {
|
||||
console.log('Restored ' + selectedApis.size + ' selected APIs from session:', Array.from(selectedApis));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</th:block>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/djbank_title_layout}">
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
layout:decorate="~{layout/djbank_title_layout}">
|
||||
|
||||
<body>
|
||||
<section layout:fragment="title">
|
||||
@@ -10,113 +11,187 @@
|
||||
<h1>앱관리</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<th:block layout:fragment="contentFragment">
|
||||
<section class="apikey-register-container">
|
||||
<div class="signup-guide-v2 figma-register-wrapper">
|
||||
<div class="service-main app-management-layout">
|
||||
|
||||
<!-- Title Bar -->
|
||||
<div class="common-title-bar">
|
||||
<h2 class="common-title">앱 수정</h2>
|
||||
</div>
|
||||
<!-- Sidebar -->
|
||||
<th:block th:replace="~{fragment/djbank/service_sidebar :: sidebar('apiKey')}"></th:block>
|
||||
|
||||
<div class="register-progress">
|
||||
<div class="progress-steps">
|
||||
<!-- Step 1: 앱 정보입력 -->
|
||||
<div class="progress-step-item">
|
||||
<div class="step-icon-wrapper">
|
||||
<div class="step-icon">
|
||||
<img th:src="@{/img/apikey_step1.png}" alt="앱 정보입력" class="step-icon-img">
|
||||
<!-- Content Area -->
|
||||
<div class="app-management-content">
|
||||
<div class="step3-wrap">
|
||||
|
||||
<!-- Title -->
|
||||
<h2 class="s3-title">앱 수정</h2>
|
||||
|
||||
<!-- Progress Steps Card -->
|
||||
<div class="s1-progress-card">
|
||||
<div class="s1-steps">
|
||||
<!-- Step 1: 앱 정보수정 (form/input icon) -->
|
||||
<div class="s1-step">
|
||||
<div class="s1-step-circle">
|
||||
<!-- 입력폼 아이콘 -->
|
||||
<svg class="s1-step-svg" width="26" height="26" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="3" y="5" width="18" height="14" rx="2" stroke="currentColor" stroke-width="1.8" />
|
||||
<line x1="7" y1="9" x2="17" y2="9" stroke="currentColor" stroke-width="1.8"
|
||||
stroke-linecap="round" />
|
||||
<line x1="7" y1="12" x2="14" y2="12" stroke="currentColor" stroke-width="1.8"
|
||||
stroke-linecap="round" />
|
||||
<line x1="7" y1="15" x2="11" y2="15" stroke="currentColor" stroke-width="1.8"
|
||||
stroke-linecap="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="s1-step-num">1단계</span>
|
||||
<span class="s1-step-name">앱 정보수정</span>
|
||||
</div>
|
||||
|
||||
<div class="s1-step-line"></div>
|
||||
|
||||
<!-- Step 2: API 선택 (chain link icon) -->
|
||||
<div class="s1-step">
|
||||
<div class="s1-step-circle">
|
||||
<!-- 체인링크/API 아이콘 -->
|
||||
<svg class="s1-step-svg" width="26" height="26" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" stroke="currentColor"
|
||||
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" stroke="currentColor"
|
||||
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="s1-step-num">2단계</span>
|
||||
<span class="s1-step-name">API 선택</span>
|
||||
</div>
|
||||
|
||||
<div class="s1-step-line"></div>
|
||||
|
||||
<!-- Step 3 Active: 앱 수정 완료 (rocket icon) -->
|
||||
<div class="s1-step s1-step--active">
|
||||
<div class="s1-step-circle">
|
||||
<!-- 로켓 아이콘 -->
|
||||
<svg class="s1-step-svg" width="26" height="26" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"
|
||||
stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path
|
||||
d="M12 15l-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"
|
||||
stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" stroke="currentColor" stroke-width="1.6"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" stroke="currentColor" stroke-width="1.6"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="s1-step-num">3단계</span>
|
||||
<span class="s1-step-name">앱 수정 완료</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-label">앱정보입력</div>
|
||||
</div>
|
||||
|
||||
<!-- Dots between Step 1 and 2 -->
|
||||
<div class="step-dots">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<!-- Success Result Section -->
|
||||
<div class="s3-result-container" th:if="${modificationComplete}">
|
||||
<div class="s3-graphic-wrapper">
|
||||
<!-- Custom premium isometric server check success SVG design -->
|
||||
<svg class="s3-illustration" viewBox="0 0 500 400" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="glow-grad" x1="250" y1="40" x2="250" y2="360" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3ba4ed" stop-opacity="0.3"/>
|
||||
<stop offset="1" stop-color="#2a69de" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="cube-grad-1" x1="180" y1="120" x2="250" y2="280" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#50caff" />
|
||||
<stop offset="1" stop-color="#2a69de" />
|
||||
</linearGradient>
|
||||
<linearGradient id="cube-grad-2" x1="250" y1="120" x2="320" y2="280" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#2a69de" />
|
||||
<stop offset="1" stop-color="#1b4ab7" />
|
||||
</linearGradient>
|
||||
<filter id="drop-shadow" x="100" y="80" width="300" height="260" filterUnits="userSpaceOnUse">
|
||||
<feDropShadow dx="0" dy="12" stdDeviation="16" flood-color="#2a69de" flood-opacity="0.25"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Outer Glow -->
|
||||
<circle cx="250" cy="200" r="150" fill="url(#glow-grad)" />
|
||||
|
||||
<!-- Main Isometric Success Concept Group -->
|
||||
<g filter="url(#drop-shadow)" class="floating-slow">
|
||||
<!-- Server/App isometric block -->
|
||||
<!-- Top Face -->
|
||||
<path d="M250 110L340 155L250 200L160 155Z" fill="#E9F5FF" stroke="#3ba4ed" stroke-width="3" stroke-linejoin="round"/>
|
||||
<!-- Left Face -->
|
||||
<path d="M160 155V245L250 290V200Z" fill="url(#cube-grad-1)" stroke="#2a69de" stroke-width="3" stroke-linejoin="round"/>
|
||||
<!-- Right Face -->
|
||||
<path d="M250 200V290L340 245V155Z" fill="url(#cube-grad-2)" stroke="#1b4ab7" stroke-width="3" stroke-linejoin="round"/>
|
||||
|
||||
<!-- Isometric Grid lines / connection details on Top Face -->
|
||||
<path d="M205 132.5L250 155L295 132.5" stroke="#3ba4ed" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M250 155V200" stroke="#3ba4ed" stroke-width="2" stroke-linecap="round"/>
|
||||
|
||||
<!-- Glowing Code/Key lines on Isometric block -->
|
||||
<path d="M190 190L220 205" stroke="#ffffff" stroke-width="4" stroke-linecap="round" opacity="0.8"/>
|
||||
<path d="M190 215L220 230" stroke="#ffffff" stroke-width="4" stroke-linecap="round" opacity="0.8"/>
|
||||
<path d="M280 205L310 190" stroke="#ffffff" stroke-width="4" stroke-linecap="round" opacity="0.6"/>
|
||||
<path d="M280 230L310 215" stroke="#ffffff" stroke-width="4" stroke-linecap="round" opacity="0.6"/>
|
||||
</g>
|
||||
|
||||
<!-- Success Badge orbiting the isometric app block -->
|
||||
<g class="floating-fast">
|
||||
<!-- Circle badge outer border -->
|
||||
<circle cx="250" cy="180" r="42" fill="#ffffff" stroke="#10b981" stroke-width="5" filter="drop-shadow(0px 8px 16px rgba(16, 185, 129, 0.2))" />
|
||||
<circle cx="250" cy="180" r="32" fill="#10b981" />
|
||||
<!-- Checkmark path -->
|
||||
<path d="M238 180L246 188L262 172" stroke="#ffffff" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: API 선택 -->
|
||||
<div class="progress-step-item">
|
||||
<div class="step-icon-wrapper">
|
||||
<div class="step-icon">
|
||||
<img th:src="@{/img/apikey_step2.png}" alt="API 선택" class="step-icon-img">
|
||||
<div class="s3-message-wrapper">
|
||||
<h1 class="s3-success-title">앱 수정이 완료되었습니다.</h1>
|
||||
<p class="s3-success-desc">
|
||||
<span class="s3-highlight">담당자 승인 후 변경 사항이 적용됩니다.</span>
|
||||
<br>
|
||||
<span class="s3-highlight">[앱 정보]</span> 화면에서 수정 내역을 확인할 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Actions -->
|
||||
<div class="s3-actions">
|
||||
<a href="/myapikey" class="s3-btn-complete">
|
||||
완료
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-label">API 선택</div>
|
||||
</div>
|
||||
|
||||
<!-- Dots between Step 2 and 3 -->
|
||||
<div class="step-dots">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: 앱 수정 완료 (Active) -->
|
||||
<div class="progress-step-item active">
|
||||
<div class="step-icon-wrapper">
|
||||
<div class="step-icon">
|
||||
<img th:src="@{/img/apikey_step3.png}" alt="앱 수정 완료" class="step-icon-img">
|
||||
<!-- Error Result Section (Fallback) -->
|
||||
<div class="s3-result-container s3-result-container--error" th:unless="${modificationComplete}">
|
||||
<div class="s3-graphic-wrapper">
|
||||
<div class="s3-error-icon">❌</div>
|
||||
</div>
|
||||
<div class="s3-message-wrapper">
|
||||
<h1 class="s3-error-title">앱 수정 실패</h1>
|
||||
<p class="s3-error-desc">
|
||||
앱 수정 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.
|
||||
</p>
|
||||
</div>
|
||||
<div class="s3-actions">
|
||||
<a href="/myapikey" class="s3-btn-retry">
|
||||
다시 시도
|
||||
</a>
|
||||
<a href="/myapikey" class="s3-btn-list">
|
||||
목록으로
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-label">앱 수정 완료</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /step3-wrap -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success Result -->
|
||||
<div class="register-result success" th:if="${modificationComplete}">
|
||||
<!-- Result Box (모바일에서 배경 박스) -->
|
||||
<div class="result-box">
|
||||
<!-- Success Icon -->
|
||||
<div class="result-icon-wrapper">
|
||||
<div class="result-icon success-icon">
|
||||
<img th:src="@{/img/img_app_complete.png}" alt="완료">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success Message -->
|
||||
<div class="result-header">
|
||||
<h1>앱 수정이 완료되었습니다.</h1>
|
||||
<p class="result-description">
|
||||
<span class="text-gray">담당자 승인 후 변경 사항이 적용됩니다.</span>
|
||||
<br>
|
||||
<span class="text-dark">[앱 정보]</span>
|
||||
<span class="text-gray">화면에서 수정 내역을 확인할 수 있습니다.</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="result-actions">
|
||||
<a href="/myapikey" class="btn btn-primary">
|
||||
완료
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error Result (if needed) -->
|
||||
<div class="register-result error" th:unless="${modificationComplete}" style="display: none;">
|
||||
<div class="result-icon-wrapper">
|
||||
<div class="result-icon error-icon">
|
||||
<span>❌</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-header">
|
||||
<h1>앱 수정 실패</h1>
|
||||
<p class="result-description">
|
||||
앱 수정 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.
|
||||
</p>
|
||||
</div>
|
||||
<div class="result-actions">
|
||||
<a href="/myapikey" class="btn btn-secondary">
|
||||
다시 시도
|
||||
</a>
|
||||
<a href="/myapikey" class="btn btn-primary">
|
||||
목록으로
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</div>
|
||||
</th:block>
|
||||
|
||||
<th:block layout:fragment="contentScript">
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -11,183 +11,194 @@
|
||||
</div>
|
||||
</section>
|
||||
<th:block layout:fragment="contentFragment">
|
||||
<section class="apikey-register-container">
|
||||
<div class="signup-guide-v2 figma-register-wrapper">
|
||||
<div class="service-main app-management-layout">
|
||||
|
||||
<!-- Title Bar -->
|
||||
<div class="common-title-bar">
|
||||
<h2 class="common-title">앱 생성</h2>
|
||||
<span class="common-subtitle">API를 이용하여 서비스할 앱 정보를 등록해주세요.</span>
|
||||
</div>
|
||||
<!-- Left Sidebar (Service Sidebar) -->
|
||||
<th:block th:replace="~{fragment/djbank/service_sidebar :: sidebar('apiKey')}"></th:block>
|
||||
|
||||
<!-- Progress Indicator -->
|
||||
<div class="register-progress">
|
||||
<div class="progress-steps">
|
||||
<!-- Step 1: 앱 정보입력 (Active) -->
|
||||
<div class="progress-step-item">
|
||||
<div class="step-icon-wrapper">
|
||||
<div class="step-icon">
|
||||
<img th:src="@{/img/apikey_step1.png}" alt="앱 정보입력" width="36" height="36">
|
||||
<!-- Content Area -->
|
||||
<div class="app-management-content">
|
||||
<!-- 모든 스타일은 step2-wrap 안에서만 적용 -->
|
||||
<div class="step2-wrap">
|
||||
|
||||
<!-- Title -->
|
||||
<h2 class="s2-title">앱생성</h2>
|
||||
|
||||
<!-- Progress Steps Card -->
|
||||
<div class="s1-progress-card">
|
||||
<div class="s1-steps">
|
||||
<!-- Step 1: 앱 정보 입력 (completed) -->
|
||||
<div class="s1-step">
|
||||
<div class="s1-step-circle">
|
||||
<!-- 입력폼 아이콘 -->
|
||||
<svg class="s1-step-svg" width="26" height="26" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="3" y="5" width="18" height="14" rx="2" stroke="currentColor" stroke-width="1.8" />
|
||||
<line x1="7" y1="9" x2="17" y2="9" stroke="currentColor" stroke-width="1.8"
|
||||
stroke-linecap="round" />
|
||||
<line x1="7" y1="12" x2="14" y2="12" stroke="currentColor" stroke-width="1.8"
|
||||
stroke-linecap="round" />
|
||||
<line x1="7" y1="15" x2="11" y2="15" stroke="currentColor" stroke-width="1.8"
|
||||
stroke-linecap="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="s1-step-num">1단계</span>
|
||||
<span class="s1-step-name">앱 정보 입력</span>
|
||||
</div>
|
||||
|
||||
<div class="s1-step-line"></div>
|
||||
|
||||
<!-- Step 2 Active: API 선택 (chain link icon) -->
|
||||
<div class="s1-step s1-step--active">
|
||||
<div class="s1-step-circle">
|
||||
<!-- 체인링크/API 아이콘 -->
|
||||
<svg class="s1-step-svg" width="26" height="26" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" stroke="currentColor"
|
||||
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" stroke="currentColor"
|
||||
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="s1-step-num">2단계</span>
|
||||
<span class="s1-step-name">API 선택</span>
|
||||
</div>
|
||||
|
||||
<div class="s1-step-line"></div>
|
||||
|
||||
<!-- Step 3: 앱 생성 완료 (rocket icon) -->
|
||||
<div class="s1-step">
|
||||
<div class="s1-step-circle">
|
||||
<!-- 로켓 아이콘 -->
|
||||
<svg class="s1-step-svg" width="26" height="26" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"
|
||||
stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path
|
||||
d="M12 15l-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"
|
||||
stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" stroke="currentColor" stroke-width="1.6"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" stroke="currentColor" stroke-width="1.6"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="s1-step-num">3단계</span>
|
||||
<span class="s1-step-name">앱 생성 완료</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-label">앱 정보입력</div>
|
||||
</div>
|
||||
|
||||
<!-- Dots between Step 1 and 2 -->
|
||||
<div class="step-dots">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: API 선택 -->
|
||||
<div class="progress-step-item active">
|
||||
<div class="step-icon-wrapper">
|
||||
<div class="step-icon">
|
||||
<img th:src="@{/img/apikey_step2.png}" alt="API 선택" width="36" height="36">
|
||||
<!-- Category Carousel Tab Container -->
|
||||
<div class="s2-category-carousel-container">
|
||||
<button type="button" class="s2-carousel-btn s2-carousel-btn--prev" id="btnPrevCategory" aria-label="이전 카테고리">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="15 18 9 12 15 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="s2-category-list-wrapper" id="categoryListWrapper">
|
||||
<div class="s2-category-list" id="categoryList">
|
||||
<button type="button" class="s2-category-tab active" data-group="">
|
||||
전체
|
||||
</button>
|
||||
<button type="button" class="s2-category-tab" th:each="service : ${apiServices}" th:data-group="${service.id}" th:text="${service.groupName}">
|
||||
서비스명
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="s2-carousel-btn s2-carousel-btn--next" id="btnNextCategory" aria-label="다음 카테고리">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="step-label">API 선택</div>
|
||||
</div>
|
||||
|
||||
<!-- Dots between Step 2 and 3 -->
|
||||
<div class="step-dots">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<!-- Main Content Area -->
|
||||
<div class="s2-selection-container">
|
||||
<main class="s2-content-area">
|
||||
<form id="registerStep2Form" method="post" th:action="@{/myapikey/register/step2}" class="s2-form">
|
||||
|
||||
<!-- Step 3: 앱 생성 완료 -->
|
||||
<div class="progress-step-item">
|
||||
<div class="step-icon-wrapper">
|
||||
<div class="step-icon">
|
||||
<img th:src="@{/img/apikey_step3.png}" alt="앱 생성 완료" width="36" height="36">
|
||||
</div>
|
||||
<!-- Header filter: Search and Select All -->
|
||||
<div class="s2-filter-header">
|
||||
<div class="s2-select-all" id="selectAllWrapper" style="display: none;">
|
||||
<label class="s2-select-all-label">
|
||||
<input type="checkbox" id="selectAllCheckbox" class="visually-hidden">
|
||||
<span class="s2-checkbox-custom"></span>
|
||||
<span id="selectAllText">전체 선택</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="s2-result-count">
|
||||
총 <strong id="apiResultCount">0</strong>건
|
||||
</div>
|
||||
<div class="s2-search-box">
|
||||
<input type="text" id="apiSearch" class="s2-search-input" placeholder="API 검색...">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.23047 0C14.3285 0 18.4618 4.0539 18.4619 9.05469C18.4619 11.1083 17.7634 13.0014 16.5889 14.5205C16.5913 14.5229 16.5942 14.5249 16.5967 14.5273L19.5498 17.4238C20.1502 18.0131 20.1501 18.9683 19.5498 19.5576C18.949 20.147 17.9748 20.147 17.374 19.5576L14.4209 16.6621C14.3966 16.6383 14.3749 16.6119 14.3525 16.5869C12.8868 17.5478 11.1257 18.1094 9.23047 18.1094C4.13258 18.1092 0 14.0554 0 9.05469C0.000110268 4.05404 4.13265 0.000222701 9.23047 0ZM9.23047 3.01855C5.83201 3.01878 3.07726 5.721 3.07715 9.05469C3.07715 12.3885 5.83194 15.0916 9.23047 15.0918C12.6292 15.0918 15.3848 12.3886 15.3848 9.05469C15.3847 5.72086 12.6291 3.01855 9.23047 3.01855Z" fill="#515961"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Cards Grid -->
|
||||
<div class="s2-cards-grid" id="apiCardGrid">
|
||||
<!-- Loading state -->
|
||||
<div class="s2-loading" id="loadingState" style="grid-column: 1 / -1;">
|
||||
<div class="s2-spinner"></div>
|
||||
<p>API 목록을 불러오는 중...</p>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div class="s2-empty" id="emptyState" style="display: none; grid-column: 1 / -1;">
|
||||
<div class="s2-empty-icon">📦</div>
|
||||
<h3>API를 선택해주세요</h3>
|
||||
<p>상단 카테고리에서 서비스를 선택하면 해당 API 목록이 표시됩니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
<div class="step-label">앱 생성 완료</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Navigation Actions -->
|
||||
<div class="s2-actions">
|
||||
<button type="button" id="btnPrevStep" class="s2-btn-prev">
|
||||
이전
|
||||
</button>
|
||||
<button type="submit" form="registerStep2Form" class="s2-btn-save">
|
||||
다음
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div><!-- /step2-wrap -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Content with Sidebar Layout -->
|
||||
<div class="register-form-container with-sidebar">
|
||||
|
||||
<!-- Sidebar Navigation -->
|
||||
<aside class="apikey-register-sidebar" id="apiSidebar">
|
||||
<nav class="apikey-sidebar-nav">
|
||||
<!-- Sidebar Title -->
|
||||
<div class="sidebar-title">API 서비스 목록</div>
|
||||
|
||||
<!-- All APIs -->
|
||||
<div class="menu-section">
|
||||
<div class="menu-title active" data-group="">
|
||||
<span class="menu-text">전체</span>
|
||||
<span class="api-count"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Service Categories -->
|
||||
<div class="menu-section" th:each="service : ${apiServices}">
|
||||
<div class="menu-title"
|
||||
th:data-group="${service.id}">
|
||||
<span class="menu-text" th:text="${service.groupName}">서비스명</span>
|
||||
<span class="api-count"></span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Mobile Menu Toggle -->
|
||||
<button class="apikey-mobile-toggle" id="mobileToggle" aria-label="메뉴 열기">
|
||||
☰
|
||||
</button>
|
||||
|
||||
<!-- Mobile Overlay -->
|
||||
<div class="apikey-mobile-overlay" id="mobileOverlay"></div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="apikey-register-content">
|
||||
<form id="registerStep2Form" method="post" th:action="@{/myapikey/register/step2}" class="register-form">
|
||||
|
||||
<!-- Search Box with Select All -->
|
||||
<div class="api-filter-header">
|
||||
<div class="select-all-wrapper" id="selectAllWrapper" style="display: none;">
|
||||
<label class="select-all-label">
|
||||
<input type="checkbox" id="selectAllCheckbox" class="select-all-checkbox visually-hidden">
|
||||
<span class="custom-checkbox"></span>
|
||||
<span id="selectAllText">전체 선택</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="api-search-box">
|
||||
<input type="text"
|
||||
id="apiSearch"
|
||||
class="search-input"
|
||||
placeholder="API 검색...">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.23047 0C14.3285 0 18.4618 4.0539 18.4619 9.05469C18.4619 11.1083 17.7634 13.0014 16.5889 14.5205C16.5913 14.5229 16.5942 14.5249 16.5967 14.5273L19.5498 17.4238C20.1502 18.0131 20.1501 18.9683 19.5498 19.5576C18.949 20.147 17.9748 20.147 17.374 19.5576L14.4209 16.6621C14.3966 16.6383 14.3749 16.6119 14.3525 16.5869C12.8868 17.5478 11.1257 18.1094 9.23047 18.1094C4.13258 18.1092 0 14.0554 0 9.05469C0.000110268 4.05404 4.13265 0.000222701 9.23047 0ZM9.23047 3.01855C5.83201 3.01878 3.07726 5.721 3.07715 9.05469C3.07715 12.3885 5.83194 15.0916 9.23047 15.0918C12.6292 15.0918 15.3848 12.3886 15.3848 9.05469C15.3847 5.72086 12.6291 3.01855 9.23047 3.01855Z" fill="#515961"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Cards Grid -->
|
||||
<div class="api-selection-card-grid" id="apiCardGrid">
|
||||
<!-- Loading state -->
|
||||
<div class="loading-state" id="loadingState" style="grid-column: 1 / -1;">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>API 목록을 불러오는 중...</p>
|
||||
</div>
|
||||
|
||||
<!-- Empty state (initially hidden) -->
|
||||
<div class="empty-api-state" id="emptyState" style="display: none; grid-column: 1 / -1;">
|
||||
<div class="empty-icon">📦</div>
|
||||
<h3>API를 선택해주세요</h3>
|
||||
<p>왼쪽 메뉴에서 서비스를 선택하면 해당 API 목록이 표시됩니다.</p>
|
||||
</div>
|
||||
|
||||
<!-- API cards will be dynamically loaded here -->
|
||||
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="form-actions">
|
||||
<button type="button" id="btnPrevStep" class="btn-submit btn-secondary">
|
||||
이전
|
||||
</button>
|
||||
<button type="submit" form="registerStep2Form" class="btn-submit btn-primary">
|
||||
저장
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
<!-- Floating Cart Button -->
|
||||
<button type="button" class="floating-cart-btn" id="floatingCartBtn" style="display: none;">
|
||||
<span class="cart-label">선택된 API</span>
|
||||
<span class="cart-badge" id="cartBadge">
|
||||
<span class="cart-count">0</span>
|
||||
<span class="cart-unit">개</span>
|
||||
<button type="button" class="s2-floating-cart" id="floatingCartBtn" style="display: none;">
|
||||
<span class="s2-cart-label">선택된 API</span>
|
||||
<span class="s2-cart-badge" id="cartBadge">
|
||||
<span class="s2-cart-count">0</span>
|
||||
<span class="s2-cart-unit">개</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<!-- Selected APIs Modal - Uses base modal component -->
|
||||
<div class="modal" id="selectedApisModal" style="display: none;">
|
||||
<div class="modal-backdrop" id="modalOverlay"></div>
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">선택된 API 목록</h3>
|
||||
<button type="button" class="modal-close" id="modalCloseBtn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Selected APIs Modal -->
|
||||
<div class="s2-modal" id="selectedApisModal" style="display: none;">
|
||||
<div class="s2-modal-backdrop" id="modalOverlay"></div>
|
||||
<div class="s2-modal-dialog">
|
||||
<div class="s2-modal-header">
|
||||
<h3 class="s2-modal-title">선택된 API 목록</h3>
|
||||
<button type="button" class="s2-modal-close" id="modalCloseBtn">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="selected-apis-list" id="modalSelectedList">
|
||||
<div class="s2-modal-body">
|
||||
<div class="s2-selected-list" id="modalSelectedList">
|
||||
<!-- Dynamically populated -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" id="modalCancelBtn">닫기</button>
|
||||
<div class="s2-modal-footer">
|
||||
<button type="button" class="s2-btn-close-modal" id="modalCancelBtn">닫기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -199,10 +210,7 @@
|
||||
// DOM Elements
|
||||
const form = document.getElementById('registerStep2Form');
|
||||
const searchInput = document.getElementById('apiSearch');
|
||||
const sidebar = document.getElementById('apiSidebar');
|
||||
const mobileToggle = document.getElementById('mobileToggle');
|
||||
const mobileOverlay = document.getElementById('mobileOverlay');
|
||||
const menuTitles = document.querySelectorAll('.menu-title');
|
||||
const menuTitles = document.querySelectorAll('.s2-category-tab');
|
||||
const apiCardGrid = document.getElementById('apiCardGrid');
|
||||
const loadingState = document.getElementById('loadingState');
|
||||
const emptyState = document.getElementById('emptyState');
|
||||
@@ -227,10 +235,11 @@
|
||||
emptyState.style.display = 'none';
|
||||
|
||||
// Remove existing API cards
|
||||
document.querySelectorAll('.api-selection-card').forEach(card => card.remove());
|
||||
document.querySelectorAll('.s2-api-card').forEach(card => card.remove());
|
||||
|
||||
// Build URL with optional groupId filter
|
||||
let url = '/apis/for_request';
|
||||
// Build URL with optional groupId filter (Thymeleaf contextPath safe)
|
||||
const baseUrl = /*[[@{/apis/for_request}]]*/ '/apis/for_request';
|
||||
let url = baseUrl;
|
||||
if (groupId) {
|
||||
url += '?groupIds=' + encodeURIComponent(groupId);
|
||||
}
|
||||
@@ -241,9 +250,11 @@
|
||||
|
||||
if (apis.length === 0) {
|
||||
emptyState.style.display = 'block';
|
||||
document.getElementById('apiResultCount').textContent = '0';
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('apiResultCount').textContent = apis.length;
|
||||
renderApiCards(apis);
|
||||
updateSelectAllUI();
|
||||
}).catch(error => {
|
||||
@@ -252,6 +263,7 @@
|
||||
emptyState.querySelector('h3').textContent = 'API 로드 실패';
|
||||
emptyState.querySelector('p').textContent = '다시 시도해주세요.';
|
||||
emptyState.style.display = 'block';
|
||||
document.getElementById('apiResultCount').textContent = '0';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -270,10 +282,10 @@
|
||||
attachCardEventListeners();
|
||||
}
|
||||
|
||||
// Create API card element
|
||||
// Create API card element (Matches mainApiList.html exactly, card clicks toggle selection)
|
||||
function createApiCard(api) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'api-selection-card';
|
||||
card.className = 's2-api-card';
|
||||
card.setAttribute('data-group', api.apiGroupId || '');
|
||||
card.setAttribute('data-name', (api.apiName || '').toLowerCase());
|
||||
card.setAttribute('data-desc', (api.apiSimpleDescription || '').toLowerCase());
|
||||
@@ -289,26 +301,25 @@
|
||||
: `<i class="fas fa-cube"></i>`;
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="api-card-content">
|
||||
<div class="api-card-header">
|
||||
<span class="api-card-category">${api.service || '카테고리'}</span>
|
||||
<div class="checkbox-wrapper">
|
||||
<input type="checkbox"
|
||||
name="selectedApis"
|
||||
value="${api.apiId}"
|
||||
id="api-${api.apiId}"
|
||||
class="api-checkbox visually-hidden"
|
||||
${isSelected ? 'checked' : ''}>
|
||||
<span class="custom-checkbox"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="s2-api-card-badge">
|
||||
<span>${api.apiGroupName || '카테고리'}</span>
|
||||
</div>
|
||||
<!-- Checkbox container with visible custom design -->
|
||||
<label class="s2-checkbox-wrapper">
|
||||
<input type="checkbox"
|
||||
name="selectedApis"
|
||||
value="${api.apiId}"
|
||||
id="api-${api.apiId}"
|
||||
class="s2-api-checkbox visually-hidden"
|
||||
${isSelected ? 'checked' : ''}>
|
||||
<span class="s2-checkbox-custom"></span>
|
||||
</label>
|
||||
|
||||
<h3 class="api-name">${api.apiName || 'API 이름'}</h3>
|
||||
<p class="api-description">${api.apiSimpleDescription || 'API 설명이 없습니다.'}</p>
|
||||
<h3 class="s2-api-card-title">${api.apiName || 'API 이름'}</h3>
|
||||
<p class="s2-api-card-desc">${api.apiSimpleDescription || 'API 설명이 없습니다.'}</p>
|
||||
|
||||
<div class="api-card-icon">
|
||||
${mainIconHtml}
|
||||
</div>
|
||||
<div class="s2-api-card-image">
|
||||
${mainIconHtml}
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -317,23 +328,11 @@
|
||||
|
||||
// Attach event listeners to cards
|
||||
function attachCardEventListeners() {
|
||||
const checkboxes = document.querySelectorAll('.api-checkbox');
|
||||
const apiCards = document.querySelectorAll('.api-selection-card');
|
||||
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', function() {
|
||||
updateCardSelection(this);
|
||||
updateSelectedCount();
|
||||
});
|
||||
});
|
||||
const apiCards = document.querySelectorAll('.s2-api-card');
|
||||
|
||||
apiCards.forEach(card => {
|
||||
card.addEventListener('click', function(e) {
|
||||
// Prevent double toggle if clicking directly on checkbox
|
||||
if (e.target.classList.contains('api-checkbox')) {
|
||||
return;
|
||||
}
|
||||
const checkbox = card.querySelector('.api-checkbox');
|
||||
const checkbox = card.querySelector('.s2-api-checkbox');
|
||||
if (checkbox) {
|
||||
checkbox.checked = !checkbox.checked;
|
||||
updateCardSelection(checkbox);
|
||||
@@ -341,11 +340,28 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Prevent double toggle when clicking the checkbox wrapper
|
||||
const checkboxWrappers = document.querySelectorAll('.s2-checkbox-wrapper');
|
||||
checkboxWrappers.forEach(wrapper => {
|
||||
wrapper.addEventListener('click', function(e) {
|
||||
e.stopPropagation(); // Stop click from bubbling to card!
|
||||
});
|
||||
|
||||
// Listen to checkbox change event inside it
|
||||
const checkbox = wrapper.querySelector('.s2-api-checkbox');
|
||||
if (checkbox) {
|
||||
checkbox.addEventListener('change', function() {
|
||||
updateCardSelection(this);
|
||||
updateSelectedCount();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update card visual state
|
||||
function updateCardSelection(checkbox) {
|
||||
const card = checkbox.closest('.api-selection-card');
|
||||
const card = checkbox.closest('.s2-api-card');
|
||||
if (checkbox.checked) {
|
||||
card.classList.add('selected');
|
||||
selectedApis.add(checkbox.value);
|
||||
@@ -359,7 +375,7 @@
|
||||
function updateSelectedCount() {
|
||||
// Update floating cart button
|
||||
const floatingCartBtn = document.getElementById('floatingCartBtn');
|
||||
const cartCount = document.querySelector('.cart-count');
|
||||
const cartCount = document.querySelector('.s2-cart-count');
|
||||
|
||||
if (selectedApis.size > 0) {
|
||||
floatingCartBtn.style.display = 'flex';
|
||||
@@ -395,7 +411,7 @@
|
||||
// Update select all checkbox state based on visible cards
|
||||
function updateSelectAllCheckboxState() {
|
||||
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||
const visibleCards = Array.from(document.querySelectorAll('.api-selection-card')).filter(card => card.style.display !== 'none');
|
||||
const visibleCards = Array.from(document.querySelectorAll('.s2-api-card')).filter(card => card.style.display !== 'none');
|
||||
|
||||
if (visibleCards.length === 0) {
|
||||
selectAllCheckbox.checked = false;
|
||||
@@ -403,7 +419,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const visibleCheckboxes = visibleCards.map(card => card.querySelector('.api-checkbox'));
|
||||
const visibleCheckboxes = visibleCards.map(card => card.querySelector('.s2-api-checkbox'));
|
||||
const checkedCount = visibleCheckboxes.filter(cb => cb.checked).length;
|
||||
|
||||
if (checkedCount === 0) {
|
||||
@@ -424,35 +440,30 @@
|
||||
modalSelectedList.innerHTML = '';
|
||||
|
||||
if (selectedApis.size === 0) {
|
||||
modalSelectedList.innerHTML = '<p class="empty-message">선택된 API가 없습니다.</p>';
|
||||
modalSelectedList.innerHTML = '<p class="s2-empty-message">선택된 API가 없습니다.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all checked checkboxes
|
||||
const checkedBoxes = document.querySelectorAll('.api-checkbox:checked');
|
||||
const checkedBoxes = document.querySelectorAll('.s2-api-checkbox:checked');
|
||||
checkedBoxes.forEach(function(checkbox) {
|
||||
const card = checkbox.closest('.api-selection-card');
|
||||
const apiName = card.querySelector('.api-name').textContent;
|
||||
const card = checkbox.closest('.s2-api-card');
|
||||
const apiName = card.querySelector('.s2-api-card-title').textContent;
|
||||
|
||||
const apiPill = document.createElement('div');
|
||||
apiPill.className = 'api-pill';
|
||||
apiPill.className = 's2-api-pill';
|
||||
apiPill.innerHTML = `
|
||||
<span class="api-pill-name">${apiName}</span>
|
||||
<button type="button" class="api-pill-remove" data-value="${checkbox.value}" aria-label="Remove ${apiName}">
|
||||
<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">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<span class="s2-api-pill-name">${apiName}</span>
|
||||
<button type="button" class="s2-api-pill-remove" data-value="${checkbox.value}" aria-label="Remove ${apiName}">✕</button>
|
||||
`;
|
||||
modalSelectedList.appendChild(apiPill);
|
||||
});
|
||||
|
||||
// Add remove handlers
|
||||
document.querySelectorAll('.api-pill-remove').forEach(function(btn) {
|
||||
document.querySelectorAll('.s2-api-pill-remove').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
const value = this.getAttribute('data-value');
|
||||
const checkbox = document.querySelector('.api-checkbox[value="' + value + '"]');
|
||||
const checkbox = document.querySelector('.s2-api-checkbox[value="' + value + '"]');
|
||||
if (checkbox) {
|
||||
checkbox.checked = false;
|
||||
updateCardSelection(checkbox);
|
||||
@@ -467,7 +478,8 @@
|
||||
searchInput.addEventListener('input', function() {
|
||||
const searchTerm = this.value.toLowerCase();
|
||||
|
||||
const apiCards = document.querySelectorAll('.api-selection-card');
|
||||
const apiCards = document.querySelectorAll('.s2-api-card');
|
||||
let visibleCount = 0;
|
||||
apiCards.forEach(function(card) {
|
||||
const apiName = card.getAttribute('data-name');
|
||||
const apiDesc = card.getAttribute('data-desc');
|
||||
@@ -477,17 +489,20 @@
|
||||
|
||||
if (matchesSearch) {
|
||||
card.style.display = '';
|
||||
visibleCount++;
|
||||
} else {
|
||||
card.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('apiResultCount').textContent = visibleCount;
|
||||
|
||||
// Update select all checkbox state after search
|
||||
updateSelectAllCheckboxState();
|
||||
});
|
||||
}
|
||||
|
||||
// Sidebar category selection
|
||||
// Category tab selection
|
||||
menuTitles.forEach(function(title) {
|
||||
title.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
@@ -501,7 +516,7 @@
|
||||
currentFilter = groupId;
|
||||
|
||||
// Store service name for select all label
|
||||
currentServiceName = this.textContent.trim().split('\n')[0].trim();
|
||||
currentServiceName = this.textContent.trim();
|
||||
|
||||
// Load APIs for selected service
|
||||
loadApis(groupId);
|
||||
@@ -510,51 +525,40 @@
|
||||
if (searchInput) {
|
||||
searchInput.value = '';
|
||||
}
|
||||
|
||||
// Close mobile menu if open
|
||||
if (window.innerWidth <= 768 && sidebar.classList.contains('mobile-open')) {
|
||||
closeMobileMenu();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Mobile menu toggle
|
||||
function closeMobileMenu() {
|
||||
sidebar.classList.remove('mobile-open');
|
||||
mobileOverlay.classList.remove('active');
|
||||
mobileToggle.innerHTML = '☰';
|
||||
mobileToggle.setAttribute('aria-label', '메뉴 열기');
|
||||
}
|
||||
// Category Carousel Scroll
|
||||
const categoryListWrapper = document.getElementById('categoryListWrapper');
|
||||
const btnPrevCategory = document.getElementById('btnPrevCategory');
|
||||
const btnNextCategory = document.getElementById('btnNextCategory');
|
||||
|
||||
if (mobileToggle && sidebar && mobileOverlay) {
|
||||
mobileToggle.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (categoryListWrapper && btnPrevCategory && btnNextCategory) {
|
||||
const scrollAmount = 200;
|
||||
|
||||
sidebar.classList.toggle('mobile-open');
|
||||
mobileOverlay.classList.toggle('active');
|
||||
|
||||
if (sidebar.classList.contains('mobile-open')) {
|
||||
this.innerHTML = '✕';
|
||||
this.setAttribute('aria-label', '메뉴 닫기');
|
||||
} else {
|
||||
this.innerHTML = '☰';
|
||||
this.setAttribute('aria-label', '메뉴 열기');
|
||||
}
|
||||
btnPrevCategory.addEventListener('click', function() {
|
||||
categoryListWrapper.scrollBy({ left: -scrollAmount, behavior: 'smooth' });
|
||||
});
|
||||
|
||||
// Close menu on overlay click
|
||||
mobileOverlay.addEventListener('click', function() {
|
||||
closeMobileMenu();
|
||||
btnNextCategory.addEventListener('click', function() {
|
||||
categoryListWrapper.scrollBy({ left: scrollAmount, behavior: 'smooth' });
|
||||
});
|
||||
}
|
||||
|
||||
// Close mobile menu on resize to desktop
|
||||
window.addEventListener('resize', function() {
|
||||
if (window.innerWidth > 768 && sidebar.classList.contains('mobile-open')) {
|
||||
closeMobileMenu();
|
||||
// Toggle buttons visibility/disabled state based on scroll position
|
||||
function updateCarouselButtons() {
|
||||
const scrollLeft = categoryListWrapper.scrollLeft;
|
||||
const maxScrollLeft = categoryListWrapper.scrollWidth - categoryListWrapper.clientWidth;
|
||||
|
||||
btnPrevCategory.disabled = scrollLeft <= 0;
|
||||
btnNextCategory.disabled = scrollLeft >= maxScrollLeft - 1;
|
||||
}
|
||||
});
|
||||
|
||||
categoryListWrapper.addEventListener('scroll', updateCarouselButtons);
|
||||
window.addEventListener('resize', updateCarouselButtons);
|
||||
|
||||
// Initial check after loading categories
|
||||
setTimeout(updateCarouselButtons, 150);
|
||||
}
|
||||
|
||||
// Modal control
|
||||
const floatingCartBtn = document.getElementById('floatingCartBtn');
|
||||
@@ -624,10 +628,10 @@
|
||||
if (selectAllCheckbox) {
|
||||
selectAllCheckbox.addEventListener('change', function() {
|
||||
const isChecked = this.checked;
|
||||
const visibleCards = Array.from(document.querySelectorAll('.api-selection-card')).filter(card => card.style.display !== 'none');
|
||||
const visibleCards = Array.from(document.querySelectorAll('.s2-api-card')).filter(card => card.style.display !== 'none');
|
||||
|
||||
visibleCards.forEach(function(card) {
|
||||
const checkbox = card.querySelector('.api-checkbox');
|
||||
const checkbox = card.querySelector('.s2-api-checkbox');
|
||||
if (checkbox) {
|
||||
checkbox.checked = isChecked;
|
||||
updateCardSelection(checkbox);
|
||||
@@ -644,9 +648,8 @@
|
||||
btnPrevStep.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Change form action to save endpoint
|
||||
const originalAction = form.action;
|
||||
form.action = '/myapikey/register/step2/save';
|
||||
// Change form action to save endpoint (Thymeleaf contextPath safe)
|
||||
form.action = /*[[@{/myapikey/register/step2/save}]]*/ '/myapikey/register/step2/save';
|
||||
|
||||
// Submit the form to save selections
|
||||
form.submit();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/djbank_title_layout}">
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
layout:decorate="~{layout/djbank_title_layout}">
|
||||
|
||||
<body>
|
||||
<section layout:fragment="title">
|
||||
@@ -10,121 +11,192 @@
|
||||
<h1>앱관리</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<th:block layout:fragment="contentFragment">
|
||||
<section class="apikey-register-container">
|
||||
<div class="signup-guide-v2 figma-register-wrapper">
|
||||
<div class="service-main app-management-layout">
|
||||
|
||||
<!-- Title Bar -->
|
||||
<div class="common-title-bar">
|
||||
<h2 class="common-title">앱 생성</h2>
|
||||
</div>
|
||||
<!-- Sidebar -->
|
||||
<th:block th:replace="~{fragment/djbank/service_sidebar :: sidebar('apiKey')}"></th:block>
|
||||
|
||||
<div class="register-progress">
|
||||
<div class="progress-steps">
|
||||
<!-- Step 1: 앱 정보입력 -->
|
||||
<div class="progress-step-item">
|
||||
<div class="step-icon-wrapper">
|
||||
<div class="step-icon">
|
||||
<img th:src="@{/img/apikey_step1.png}" alt="앱 정보입력" class="step-icon-img">
|
||||
<!-- Content Area -->
|
||||
<div class="app-management-content">
|
||||
<div class="step3-wrap">
|
||||
|
||||
<!-- Title -->
|
||||
<h2 class="s3-title">앱생성</h2>
|
||||
|
||||
<!-- Progress Steps Card -->
|
||||
<div class="s1-progress-card">
|
||||
<div class="s1-steps">
|
||||
<!-- Step 1: 앱 정보 입력 (form/input icon) -->
|
||||
<div class="s1-step">
|
||||
<div class="s1-step-circle">
|
||||
<!-- 입력폼 아이콘 -->
|
||||
<svg class="s1-step-svg" width="26" height="26" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="3" y="5" width="18" height="14" rx="2" stroke="currentColor" stroke-width="1.8" />
|
||||
<line x1="7" y1="9" x2="17" y2="9" stroke="currentColor" stroke-width="1.8"
|
||||
stroke-linecap="round" />
|
||||
<line x1="7" y1="12" x2="14" y2="12" stroke="currentColor" stroke-width="1.8"
|
||||
stroke-linecap="round" />
|
||||
<line x1="7" y1="15" x2="11" y2="15" stroke="currentColor" stroke-width="1.8"
|
||||
stroke-linecap="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="s1-step-num">1단계</span>
|
||||
<span class="s1-step-name">앱 정보 입력</span>
|
||||
</div>
|
||||
|
||||
<div class="s1-step-line"></div>
|
||||
|
||||
<!-- Step 2: API 선택 (chain link icon) -->
|
||||
<div class="s1-step">
|
||||
<div class="s1-step-circle">
|
||||
<!-- 체인링크/API 아이콘 -->
|
||||
<svg class="s1-step-svg" width="26" height="26" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" stroke="currentColor"
|
||||
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" stroke="currentColor"
|
||||
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="s1-step-num">2단계</span>
|
||||
<span class="s1-step-name">API 선택</span>
|
||||
</div>
|
||||
|
||||
<div class="s1-step-line"></div>
|
||||
|
||||
<!-- Step 3 Active: 앱 생성 완료 (rocket icon) -->
|
||||
<div class="s1-step s1-step--active">
|
||||
<div class="s1-step-circle">
|
||||
<!-- 로켓 아이콘 -->
|
||||
<svg class="s1-step-svg" width="26" height="26" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"
|
||||
stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path
|
||||
d="M12 15l-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"
|
||||
stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" stroke="currentColor" stroke-width="1.6"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" stroke="currentColor" stroke-width="1.6"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="s1-step-num">3단계</span>
|
||||
<span class="s1-step-name">앱 생성 완료</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-label">앱정보입력</div>
|
||||
</div>
|
||||
|
||||
<!-- Dots between Step 1 and 2 -->
|
||||
<div class="step-dots">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<!-- Success Result Section -->
|
||||
<div class="s3-result-container" th:if="${registrationComplete}">
|
||||
<div class="s3-graphic-wrapper">
|
||||
<!-- Custom premium isometric server check success SVG design -->
|
||||
<svg class="s3-illustration" viewBox="0 0 500 400" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="glow-grad" x1="250" y1="40" x2="250" y2="360" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3ba4ed" stop-opacity="0.3"/>
|
||||
<stop offset="1" stop-color="#2a69de" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="cube-grad-1" x1="180" y1="120" x2="250" y2="280" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#50caff" />
|
||||
<stop offset="1" stop-color="#2a69de" />
|
||||
</linearGradient>
|
||||
<linearGradient id="cube-grad-2" x1="250" y1="120" x2="320" y2="280" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#2a69de" />
|
||||
<stop offset="1" stop-color="#1b4ab7" />
|
||||
</linearGradient>
|
||||
<filter id="drop-shadow" x="100" y="80" width="300" height="260" filterUnits="userSpaceOnUse">
|
||||
<feDropShadow dx="0" dy="12" stdDeviation="16" flood-color="#2a69de" flood-opacity="0.25"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Outer Glow -->
|
||||
<circle cx="250" cy="200" r="150" fill="url(#glow-grad)" />
|
||||
|
||||
<!-- Main Isometric Success Concept Group -->
|
||||
<g filter="url(#drop-shadow)" class="floating-slow">
|
||||
<!-- Server/App isometric block -->
|
||||
<!-- Top Face -->
|
||||
<path d="M250 110L340 155L250 200L160 155Z" fill="#E9F5FF" stroke="#3ba4ed" stroke-width="3" stroke-linejoin="round"/>
|
||||
<!-- Left Face -->
|
||||
<path d="M160 155V245L250 290V200Z" fill="url(#cube-grad-1)" stroke="#2a69de" stroke-width="3" stroke-linejoin="round"/>
|
||||
<!-- Right Face -->
|
||||
<path d="M250 200V290L340 245V155Z" fill="url(#cube-grad-2)" stroke="#1b4ab7" stroke-width="3" stroke-linejoin="round"/>
|
||||
|
||||
<!-- Isometric Grid lines / connection details on Top Face -->
|
||||
<path d="M205 132.5L250 155L295 132.5" stroke="#3ba4ed" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M250 155V200" stroke="#3ba4ed" stroke-width="2" stroke-linecap="round"/>
|
||||
|
||||
<!-- Glowing Code/Key lines on Isometric block -->
|
||||
<path d="M190 190L220 205" stroke="#ffffff" stroke-width="4" stroke-linecap="round" opacity="0.8"/>
|
||||
<path d="M190 215L220 230" stroke="#ffffff" stroke-width="4" stroke-linecap="round" opacity="0.8"/>
|
||||
<path d="M280 205L310 190" stroke="#ffffff" stroke-width="4" stroke-linecap="round" opacity="0.6"/>
|
||||
<path d="M280 230L310 215" stroke="#ffffff" stroke-width="4" stroke-linecap="round" opacity="0.6"/>
|
||||
</g>
|
||||
|
||||
<!-- Success Badge orbiting the isometric app block -->
|
||||
<g class="floating-fast">
|
||||
<!-- Circle badge outer border -->
|
||||
<circle cx="250" cy="180" r="42" fill="#ffffff" stroke="#10b981" stroke-width="5" filter="drop-shadow(0px 8px 16px rgba(16, 185, 129, 0.2))" />
|
||||
<circle cx="250" cy="180" r="32" fill="#10b981" />
|
||||
<!-- Checkmark path -->
|
||||
<path d="M238 180L246 188L262 172" stroke="#ffffff" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: API 선택 -->
|
||||
<div class="progress-step-item">
|
||||
<div class="step-icon-wrapper">
|
||||
<div class="step-icon">
|
||||
<img th:src="@{/img/apikey_step2.png}" alt="API 선택" class="step-icon-img">
|
||||
<div class="s3-message-wrapper">
|
||||
<h1 class="s3-success-title">앱 생성이 완료되었습니다.</h1>
|
||||
<p class="s3-success-desc">
|
||||
담당자 앱 승인 후 <span class="s3-highlight">[앱 정보]</span> 화면에서
|
||||
<br>
|
||||
<span class="s3-highlight">Client ID와 Client Secret을 확인</span>할 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Actions -->
|
||||
<div class="s3-actions">
|
||||
<a href="/myapikey" class="s3-btn-complete">
|
||||
완료
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-label">API 선택</div>
|
||||
</div>
|
||||
|
||||
<!-- Dots between Step 2 and 3 -->
|
||||
<div class="step-dots">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: 앱 생성 완료 (Active) -->
|
||||
<div class="progress-step-item active">
|
||||
<div class="step-icon-wrapper">
|
||||
<div class="step-icon">
|
||||
<img th:src="@{/img/apikey_step3.png}" alt="앱 생성 완료" class="step-icon-img">
|
||||
<!-- Error Result Section (Fallback) -->
|
||||
<div class="s3-result-container s3-result-container--error" th:unless="${registrationComplete}">
|
||||
<div class="s3-graphic-wrapper">
|
||||
<div class="s3-error-icon">❌</div>
|
||||
</div>
|
||||
<div class="s3-message-wrapper">
|
||||
<h1 class="s3-error-title">앱 생성 실패</h1>
|
||||
<p class="s3-error-desc">
|
||||
앱 생성 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.
|
||||
</p>
|
||||
</div>
|
||||
<div class="s3-actions">
|
||||
<a href="/myapikey/register/step1" class="s3-btn-retry">
|
||||
다시 시도
|
||||
</a>
|
||||
<a href="/myapikey" class="s3-btn-list">
|
||||
목록으로
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-label">앱 생성 완료</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /step3-wrap -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success Result -->
|
||||
<div class="register-result success" th:if="${registrationComplete}">
|
||||
<!-- Result Box (모바일에서 배경 박스) -->
|
||||
<div class="result-box">
|
||||
<!-- Success Icon -->
|
||||
<div class="result-icon-wrapper">
|
||||
<div class="result-icon success-icon">
|
||||
<img th:src="@{/img/img_app_complete.png}" alt="완료">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success Message -->
|
||||
<div class="result-header">
|
||||
<h1>앱 생성이 완료되었습니다.</h1>
|
||||
<p class="result-description">
|
||||
<span class="text-gray">담당자 앱 승인 후</span>
|
||||
<span class="text-dark">[앱 정보]</span>
|
||||
<span class="text-gray">화면에서</span>
|
||||
<br>
|
||||
<span class="text-dark">Client ID와 Client Secret을 확인</span>
|
||||
<span class="text-gray">할 수 있습니다.</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="result-actions">
|
||||
<a href="/myapikey" class="btn btn-primary">
|
||||
완료
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error Result (if needed) -->
|
||||
<div class="register-result error" th:unless="${registrationComplete}" style="display: none;">
|
||||
<div class="result-icon-wrapper">
|
||||
<div class="result-icon error-icon">
|
||||
<span>❌</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-header">
|
||||
<h1>API Key 생성 실패</h1>
|
||||
<p class="result-description">
|
||||
API Key 생성 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.
|
||||
</p>
|
||||
</div>
|
||||
<div class="result-actions">
|
||||
<a href="/myapikey/register/step1" class="btn btn-secondary">
|
||||
다시 시도
|
||||
</a>
|
||||
<a href="/myapikey" class="btn btn-primary">
|
||||
목록으로
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</div>
|
||||
</th:block>
|
||||
|
||||
<th:block layout:fragment="contentScript">
|
||||
<script th:inline="javascript">
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// Clear session storage after successful registration
|
||||
if (/*[[${registrationComplete}]]*/ false) {
|
||||
sessionStorage.removeItem('registration_appName');
|
||||
|
||||
@@ -1,239 +1,294 @@
|
||||
<!DOCTYPE html>
|
||||
<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/djbank_title_layout}">
|
||||
<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/djbank_title_layout}">
|
||||
|
||||
<head>
|
||||
<title>API 키 신청 상세</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<section layout:fragment="title">
|
||||
<div class="page-title-banner">
|
||||
<img th:src="@{/img/img_title_bg.png}" class="title-image">
|
||||
<h1>앱관리</h1>
|
||||
</div>
|
||||
</section>
|
||||
<th:block layout:fragment="contentFragment">
|
||||
<section class="apikey-register-container">
|
||||
|
||||
<!-- Title Bar -->
|
||||
<div class="common-title-bar">
|
||||
<h2 class="common-title">신청 상세</h2>
|
||||
<span class="common-subtitle">신청하신 API Key의 상세 정보를 확인할 수 있습니다.</span>
|
||||
<section layout:fragment="title">
|
||||
<div class="page-title-banner">
|
||||
<img th:src="@{/img/img_title_bg.png}" class="title-image">
|
||||
<h1>앱관리</h1>
|
||||
</div>
|
||||
|
||||
<!-- App Basic Info Card -->
|
||||
<div class="app-info-card">
|
||||
<!-- App Icon -->
|
||||
<div class="app-info-icon">
|
||||
<img th:if="${appRequest.appIconFileId != null}"
|
||||
th:src="@{/file/download(fileSn=1,fileId=${appRequest.appIconFileId})}"
|
||||
alt="앱 아이콘"
|
||||
class="app-icon-image">
|
||||
<div th:unless="${appRequest.appIconFileId != null}" class="app-icon-placeholder">
|
||||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
||||
<polyline points="21 15 16 10 5 21"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 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>
|
||||
|
||||
<!-- Request Info Section -->
|
||||
<div class="apikey-info-section">
|
||||
|
||||
<!-- Client ID (if exists) -->
|
||||
<div class="info-row info-row-with-action" th:if="${appRequest.clientId != null}">
|
||||
<label class="info-label">Client ID</label>
|
||||
<div class="info-value">
|
||||
<div class="info-value-box with-copy" th:text="${appRequest.clientId}">K00000001</div>
|
||||
<button type="button" class="btn-copy-action" th:data-secret="${appRequest.clientId}" onclick="copyToClipboardFromButton(this)">
|
||||
+ 복사
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Callback URL -->
|
||||
<div class="info-row">
|
||||
<label class="info-label">Callback URL</label>
|
||||
<div class="info-value">
|
||||
<div class="info-value-box" th:text="${appRequest.callbackUrl != null ? appRequest.callbackUrl : '설정되지 않음'}">
|
||||
http://www.abcd.co.kr
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IP Whitelist -->
|
||||
<div class="info-row">
|
||||
<label class="info-label">화이트리스트</label>
|
||||
<div class="info-value">
|
||||
<div class="info-value-box" th:text="${appRequest.ipWhitelist != null and !appRequest.ipWhitelist.isEmpty() ? appRequest.ipWhitelist : '설정되지 않음'}">
|
||||
10.0.0.1, 10.0.0.2, 192.168.123.1
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selected APIs -->
|
||||
<div class="info-row info-row-vertical">
|
||||
<label class="info-label">API 목록</label>
|
||||
<div class="info-value">
|
||||
<div class="api-list-box">
|
||||
<div class="api-list-item" th:each="api : ${appRequest.apiSpecList}">
|
||||
<span class="api-item-name" th:text="${api.apiName}">추가된 API 1</span>
|
||||
<span class="api-item-status">승인대기</span>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div class="empty-state" th:if="${appRequest.apiSpecList == null or appRequest.apiSpecList.isEmpty()}" style="padding: 24px;">
|
||||
<p>선택된 API가 없습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Created Date -->
|
||||
<div class="info-row">
|
||||
<label class="info-label">신청일시</label>
|
||||
<div class="info-value">
|
||||
<div class="info-value-box" th:text="${#temporals.format(appRequest.createdDate, 'yyyy.MM.dd HH:mm')}">
|
||||
2025.11.22 12:00
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Approval Date (if approved) -->
|
||||
<div class="info-row" th:if="${appRequest.approval != null and appRequest.approval.approvalDate != null}">
|
||||
<label class="info-label">승인일시</label>
|
||||
<div class="info-value">
|
||||
<div class="info-value-box" th:text="${#temporals.format(appRequest.approval.approvalDate, 'yyyy.MM.dd HH:mm')}">
|
||||
2025.11.22 12:00
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reason (if exists) -->
|
||||
<div class="info-row" th:if="${appRequest.approval != null and appRequest.reason != null and !appRequest.reason.isEmpty()}">
|
||||
<label class="info-label">사유</label>
|
||||
<div class="info-value">
|
||||
<div class="info-value-box" th:text="${appRequest.reason}">
|
||||
Approval reason
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="form-actions-center">
|
||||
<button type="button"
|
||||
sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"
|
||||
class="btn btn-submit btn-danger"
|
||||
th:if="${appRequest.approval != null and appRequest.approval.approvalStatus != null and
|
||||
(appRequest.approval.approvalStatus.toString() == 'PENDING' or appRequest.approval.approvalStatus.toString() == 'REQUESTED')}"
|
||||
th:data-request-id="${appRequest.id}"
|
||||
onclick="cancelRequestById(this)">
|
||||
신청 취소
|
||||
</button>
|
||||
<a th:href="@{/myapikey}" class="btn btn-submit btn-secondary">
|
||||
목록
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
<th:block layout:fragment="contentFragment">
|
||||
<div class="signup-guide-v2 figma-register-wrapper">
|
||||
<div class="service-main app-management-layout">
|
||||
|
||||
</th:block>
|
||||
<!-- Left Sidebar (Service Sidebar) -->
|
||||
<th:block th:replace="~{fragment/djbank/service_sidebar :: sidebar('apiKey')}"></th:block>
|
||||
|
||||
<th:block layout:fragment="contentScript">
|
||||
<script th:inline="javascript">
|
||||
/*<![CDATA[*/
|
||||
<!-- Content Area -->
|
||||
<div class="app-management-content">
|
||||
<!-- 모든 스타일은 detail-wrap 안에서만 적용 -->
|
||||
<div class="detail-wrap">
|
||||
<!-- Title Bar (Figma 및 스크린샷 대응) -->
|
||||
<div class="common-title-bar">
|
||||
<h2 class="common-title" th:text="${appRequest.approval != null and appRequest.approval.approvalStatus != null and appRequest.approval.approvalStatus.toString() == 'APPROVED' ? '인증키 정보' : '신청 상세'}">신청 상세</h2>
|
||||
<span class="common-subtitle" th:text="${appRequest.approval != null and appRequest.approval.approvalStatus != null and appRequest.approval.approvalStatus.toString() == 'APPROVED' ? '등록된 앱의 상세 정보를 확인할 수 있습니다.' : '신청하신 API Key의 상세 정보를 확인할 수 있습니다.'}">신청하신 API Key의 상세 정보를 확인할 수 있습니다.</span>
|
||||
</div>
|
||||
|
||||
// Copy to clipboard function
|
||||
function copyToClipboardFromButton(button) {
|
||||
var text = $(button).data('secret');
|
||||
<!-- App Basic Info Card -->
|
||||
<div class="dt-app-card">
|
||||
<!-- App Icon -->
|
||||
<div class="dt-app-icon-box">
|
||||
<img th:if="${appRequest.appIconFileId != null}"
|
||||
th:src="@{/file/download(fileSn=1,fileId=${appRequest.appIconFileId})}" alt="앱 아이콘">
|
||||
<svg th:unless="${appRequest.appIconFileId != null}" width="46" height="46"
|
||||
viewBox="0 0 24 24" fill="none" stroke="#64748b" 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>
|
||||
<!-- App Info -->
|
||||
<div class="dt-app-info">
|
||||
<!-- Status Badge line (Only for Approved or In Progress states) -->
|
||||
<th:block th:if="${appRequest.approval != null and appRequest.approval.approvalStatus != null and (appRequest.approval.approvalStatus.toString() == 'APPROVED' or appRequest.approval.approvalStatus.toString() == 'REQUESTED' or appRequest.approval.approvalStatus.description == '진행중')}">
|
||||
<div class="dt-app-status-row">
|
||||
<span th:if="${appRequest.approval.approvalStatus.toString() == 'APPROVED'}" class="dt-status-badge status-approved">답변완료</span>
|
||||
<span th:if="${appRequest.approval.approvalStatus.toString() == 'REQUESTED' or appRequest.approval.approvalStatus.description == '진행중'}" class="dt-status-badge status-pending">진행중</span>
|
||||
</div>
|
||||
</th:block>
|
||||
<h3 class="dt-app-name" th:text="${appRequest.clientName}">앱 이름</h3>
|
||||
<!-- App Description -->
|
||||
<p class="dt-app-desc"
|
||||
th:text="${appRequest.appDescription != null ? appRequest.appDescription : '앱 설명이 없습니다.'}">
|
||||
여기에 입력한 앱 설명이 표시됩니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
<!-- Request Info Section Card -->
|
||||
<div class="dt-info-container">
|
||||
<!-- Row 1: Client ID & Client Secret (only if approved) -->
|
||||
<div class="dt-row" th:if="${appRequest.clientId != null}">
|
||||
<div class="dt-col">
|
||||
<label class="dt-label">Client ID</label>
|
||||
<div class="dt-input-group">
|
||||
<div class="dt-input-box" th:text="${appRequest.clientId}">K00000001</div>
|
||||
<button type="button" class="dt-btn-copy"
|
||||
th:data-secret="${appRequest.clientId}"
|
||||
onclick="copyToClipboardFromButton(this)">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<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>
|
||||
<span>복사</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
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);
|
||||
}
|
||||
<div class="dt-col">
|
||||
<label class="dt-label">Client Secret</label>
|
||||
<div class="dt-input-group">
|
||||
<div class="dt-input-box text-secret">••••••••••••••••</div>
|
||||
<button type="button" class="dt-btn-copy"
|
||||
onclick="alert('보안을 위해 Client Secret은 승인 완료된 인증키 상세 메뉴에서 본인 비밀번호 확인 후 조회가 가능합니다.')">
|
||||
<span>안내</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// Cancel request function - called from button with data-request-id attribute
|
||||
function cancelRequestById(button) {
|
||||
var requestId = $(button).data('request-id');
|
||||
<!-- Callback URL -->
|
||||
<div class="dt-row" th:if="${appRequest.clientId != null}">
|
||||
<div class="dt-col">
|
||||
<label class="dt-label">Callback URL</label>
|
||||
<div class="dt-input-group">
|
||||
<div class="dt-input-box"
|
||||
th:text="${appRequest.callbackUrl != null ? appRequest.callbackUrl : '설정되지 않음'}">
|
||||
http://www.abcd.co.kr
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
if (!confirm('정말로 이 신청을 취소하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
<!-- 화이트리스트 -->
|
||||
<div class="dt-row">
|
||||
<div class="dt-col">
|
||||
<label class="dt-label">화이트리스트</label>
|
||||
<div class="dt-input-group">
|
||||
<div class="dt-input-box"
|
||||
th:text="${appRequest.ipWhitelist != null and !appRequest.ipWhitelist.isEmpty() ? appRequest.ipWhitelist : '설정되지 않음'}">
|
||||
설정되지 않음
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
$('.loading-overlay').show();
|
||||
<!-- API 목록 -->
|
||||
<div class="dt-row">
|
||||
<div class="dt-col">
|
||||
<label class="dt-label">API 목록</label>
|
||||
<div class="dt-api-list-box">
|
||||
<div class="dt-api-item" th:each="api : ${appRequest.apiSpecList}">
|
||||
<span class="dt-api-name" th:text="${api.apiName}">추가된 API 1</span>
|
||||
<span class="dt-api-status-badge"
|
||||
th:classappend="${appRequest.approval != null and appRequest.approval.approvalStatus.toString() == 'APPROVED' ? '' : 'status-pending'}"
|
||||
th:text="${appRequest.approval != null and appRequest.approval.approvalStatus.toString() == 'APPROVED' ? '승인' : '승인대기'}">
|
||||
승인대기
|
||||
</span>
|
||||
</div>
|
||||
<div class="dt-empty-message"
|
||||
th:if="${appRequest.apiSpecList == null or appRequest.apiSpecList.isEmpty()}">
|
||||
선택된 API가 없습니다.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
$.ajax({
|
||||
url: '/myapikey/api_key_request/cancel',
|
||||
type: 'POST',
|
||||
data: {id: requestId},
|
||||
dataType: 'json',
|
||||
headers: {
|
||||
'X-XSRF-TOKEN': /*[[${_csrf.token}]]*/ 'token'
|
||||
}
|
||||
}).done(function(response) {
|
||||
if (response.success) {
|
||||
alert(response.message || '신청이 취소되었습니다.');
|
||||
window.location.href = /*[[@{/myapikey}]]*/ '/myapikey';
|
||||
<!-- 신청일시 -->
|
||||
<div class="dt-row">
|
||||
<div class="dt-col">
|
||||
<label class="dt-label">신청일시</label>
|
||||
<div class="dt-input-group">
|
||||
<div class="dt-input-box"
|
||||
th:text="${#temporals.format(appRequest.createdDate, 'yyyy.MM.dd HH:mm')}">
|
||||
2025.11.22 12:00
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 예상 완료일 (Only if REQUESTED or 진행중) -->
|
||||
<div class="dt-row" th:if="${appRequest.approval != null and appRequest.approval.approvalStatus != null and (appRequest.approval.approvalStatus.toString() == 'REQUESTED' or appRequest.approval.approvalStatus.description == '진행중')}">
|
||||
<div class="dt-col">
|
||||
<label class="dt-label">예상 완료일</label>
|
||||
<div class="dt-input-group">
|
||||
<div class="dt-input-box"
|
||||
th:text="${appRequest.createdDate != null ? #temporals.format(appRequest.createdDate.plusDays(14), 'yyyy.MM.dd') + ' (' + #temporals.format(appRequest.createdDate.plusDays(14), 'E') + ')' : 'N/A'}">
|
||||
2026.07.14 (화)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 승인일시 (Only if APPROVED) -->
|
||||
<div class="dt-row" th:if="${appRequest.approval != null and appRequest.approval.approvalStatus != null and appRequest.approval.approvalStatus.toString() == 'APPROVED' and appRequest.approval.approvalDate != null}">
|
||||
<div class="dt-col">
|
||||
<label class="dt-label">승인일시</label>
|
||||
<div class="dt-input-group">
|
||||
<div class="dt-input-box"
|
||||
th:text="${#temporals.format(appRequest.approval.approvalDate, 'yyyy.MM.dd HH:mm')}">
|
||||
2025.11.22 12:00
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 사유 (if exists) -->
|
||||
<div class="dt-row"
|
||||
th:if="${appRequest.approval != null and appRequest.reason != null and !appRequest.reason.isEmpty()}">
|
||||
<div class="dt-col">
|
||||
<label class="dt-label">사유</label>
|
||||
<div class="dt-input-group">
|
||||
<div class="dt-input-box" th:text="${appRequest.reason}">
|
||||
사유 내용
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Bottom Navigation Actions -->
|
||||
<div class="dt-actions">
|
||||
<!-- Cancel Request Button (danger red) -->
|
||||
<button type="button" sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" class="dt-btn-red"
|
||||
th:if="${appRequest.approval != null and appRequest.approval.approvalStatus != null and
|
||||
(appRequest.approval.approvalStatus.toString() == 'PENDING' or appRequest.approval.approvalStatus.toString() == 'REQUESTED' or appRequest.approval.approvalStatus.description == '진행중')}"
|
||||
th:data-request-id="${appRequest.id}" onclick="cancelRequestById(this)">
|
||||
신청 취소
|
||||
</button>
|
||||
<!-- List Button (gray) -->
|
||||
<a th:href="@{/myapikey}" class="dt-btn-gray">
|
||||
목록
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div><!-- /detail-wrap -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</th:block>
|
||||
|
||||
<th:block layout:fragment="contentScript">
|
||||
<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 {
|
||||
alert(response.message || '신청 취소 중 오류가 발생했습니다.');
|
||||
$('.loading-overlay').hide();
|
||||
fallbackCopy(text);
|
||||
}
|
||||
}).fail(function(xhr, status, error) {
|
||||
alert('신청 취소 중 오류가 발생했습니다.');
|
||||
console.error('Cancel error:', error);
|
||||
$('.loading-overlay').hide();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/*]]>*/
|
||||
</script>
|
||||
</th:block>
|
||||
function fallbackCopy(text) {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.left = '-999999px';
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
alert('클립보드에 복사되었습니다.');
|
||||
} catch (err) {
|
||||
alert('복사에 실패했습니다.');
|
||||
}
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
|
||||
// Cancel request function - called from button with data-request-id attribute
|
||||
function cancelRequestById(button) {
|
||||
var requestId = $(button).data('request-id');
|
||||
|
||||
if (!confirm('정말로 이 신청을 취소하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$('.loading-overlay').show();
|
||||
|
||||
$.ajax({
|
||||
url: '/myapikey/api_key_request/cancel',
|
||||
type: 'POST',
|
||||
data: { id: requestId },
|
||||
dataType: 'json',
|
||||
headers: {
|
||||
'X-XSRF-TOKEN': /*[[${_csrf.token}]]*/ 'token'
|
||||
}
|
||||
}).done(function (response) {
|
||||
if (response.success) {
|
||||
alert(response.message || '신청이 취소되었습니다.');
|
||||
window.location.href = /*[[@{/myapikey}]]*/ '/myapikey';
|
||||
} else {
|
||||
alert(response.message || '신청 취소 중 오류가 발생했습니다.');
|
||||
$('.loading-overlay').hide();
|
||||
}
|
||||
}).fail(function (xhr, status, error) {
|
||||
alert('신청 취소 중 오류가 발생했습니다.');
|
||||
console.error('Cancel error:', error);
|
||||
$('.loading-overlay').hide();
|
||||
});
|
||||
}
|
||||
|
||||
/*]]>*/
|
||||
</script>
|
||||
</th:block>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
||||
@@ -14,158 +14,149 @@
|
||||
</div>
|
||||
</section>
|
||||
<th:block layout:fragment="contentFragment">
|
||||
<section class="apikey-register-container">
|
||||
<div class="signup-guide-v2 figma-register-wrapper">
|
||||
<div class="service-main app-management-layout">
|
||||
|
||||
<!-- Title Bar -->
|
||||
<div class="common-title-bar">
|
||||
<h2 class="common-title">인증키 정보</h2>
|
||||
<span class="common-subtitle">등록된 앱의 상세 정보를 확인할 수 있습니다.</span>
|
||||
</div>
|
||||
<!-- Left Sidebar (Service Sidebar) -->
|
||||
<th:block th:replace="~{fragment/djbank/service_sidebar :: sidebar('apiKey')}"></th:block>
|
||||
|
||||
<!-- App Basic Info Card -->
|
||||
<div class="app-info-card">
|
||||
<!-- App Icon -->
|
||||
<div class="app-info-icon">
|
||||
<img th:if="${apiKey.appIconFileId != null}"
|
||||
th:src="@{/file/download(fileSn=1,fileId=${apiKey.appIconFileId})}"
|
||||
alt="앱 아이콘"
|
||||
class="app-icon-image">
|
||||
<div th:unless="${apiKey.appIconFileId != null}" class="app-icon-placeholder">
|
||||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
||||
<polyline points="21 15 16 10 5 21"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<!-- App Info -->
|
||||
<div class="app-info-content">
|
||||
<!-- Status Badge -->
|
||||
<span class="app-status-badge"
|
||||
th:classappend="${apiKey.appstatus == '1' ? 'status-approved' : 'status-inactive'}"
|
||||
th:text="${apiKey.appstatus == '1' ? '승인' : '비활성화'}">
|
||||
승인
|
||||
</span>
|
||||
<!-- App Name -->
|
||||
<h3 class="app-info-name" th:text="${apiKey.clientname}">앱 이름</h3>
|
||||
<!-- App Description -->
|
||||
<p class="app-info-description"
|
||||
th:text="${apiKey.appDescription != null ? apiKey.appDescription : '앱 설명이 없습니다.'}">
|
||||
여기에 입력한 앱 설명이 표시됩니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Content Area -->
|
||||
<div class="app-management-content">
|
||||
<div class="detail-wrap">
|
||||
<!-- Title Bar -->
|
||||
<div class="common-title-bar">
|
||||
<h2 class="common-title">인증키 정보</h2>
|
||||
<span class="common-subtitle">등록된 앱의 상세 정보를 확인할 수 있습니다.</span>
|
||||
</div>
|
||||
|
||||
<!-- Credential Info Section -->
|
||||
<div class="apikey-info-section">
|
||||
<!-- App Basic Info Card -->
|
||||
<div class="dt-app-card">
|
||||
<div class="dt-app-icon-box">
|
||||
<img th:if="${apiKey.appIconFileId != null}" th:src="@{/file/download(fileSn=1,fileId=${apiKey.appIconFileId})}" alt="앱 아이콘">
|
||||
<svg th:unless="${apiKey.appIconFileId != null}" width="46" height="46" viewBox="0 0 24 24" fill="none" stroke="#64748b" 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 class="dt-app-info">
|
||||
<div class="dt-app-status-row">
|
||||
<span th:classappend="${apiKey.appstatus == '1' ? 'status-approved' : 'status-inactive'}" class="dt-status-badge" th:text="${apiKey.appstatus == '1' ? '답변완료' : '비활성화'}">답변완료</span>
|
||||
</div>
|
||||
<h3 class="dt-app-name" th:text="${apiKey.clientname}">앱 이름</h3>
|
||||
<p class="dt-app-desc" th:text="${apiKey.appDescription != null ? apiKey.appDescription : '앱 설명이 없습니다.'}">앱 설명이 표시됩니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Client ID -->
|
||||
<div class="info-row info-row-with-action">
|
||||
<label class="info-label">Client ID</label>
|
||||
<div class="info-value">
|
||||
<div class="info-value-box with-copy" th:text="${apiKey.clientid}">K00000001</div>
|
||||
<button type="button" class="btn-copy-action" th:data-secret="${apiKey.clientid}" onclick="copyToClipboardFromButton(this)">
|
||||
+ 복사
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Credential Info Section Card -->
|
||||
<div class="dt-info-container">
|
||||
<!-- Client ID -->
|
||||
<div class="dt-row">
|
||||
<div class="dt-col">
|
||||
<label class="dt-label">Client ID</label>
|
||||
<div class="dt-input-group">
|
||||
<div class="dt-input-box" th:text="${apiKey.clientid}">K00000001</div>
|
||||
<button type="button" class="dt-btn-copy" th:data-secret="${apiKey.clientid}" onclick="copyToClipboardFromButton(this)">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<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>
|
||||
<span>복사</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Client Secret -->
|
||||
<div class="info-row">
|
||||
<label class="info-label">Client Secret</label>
|
||||
<div class="info-value">
|
||||
<!-- View Button (shown initially) -->
|
||||
<button type="button" class="btn-view-secret" id="hiddenSecretBox" onclick="showPasswordPrompt()">
|
||||
<span>조회</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<path d="m21 21-4.35-4.35"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Actual Secret (hidden initially) -->
|
||||
<div id="revealedSecretBox" style="display: none;">
|
||||
<div class="info-row info-row-with-action" style="margin-bottom: 0;">
|
||||
<div class="info-value" style="display: flex; gap: 16px; align-items: center;">
|
||||
<div class="info-value-box with-copy" th:text="${apiKey.clientsecret}">secret-key-67890</div>
|
||||
<button type="button" class="btn-copy-action" th:data-secret="${apiKey.clientsecret}" onclick="copyToClipboardFromButton(this)">
|
||||
+ 복사
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Client Secret -->
|
||||
<div class="dt-row">
|
||||
<div class="dt-col">
|
||||
<label class="dt-label">Client Secret</label>
|
||||
|
||||
<!-- View Button (shown initially) -->
|
||||
<div class="dt-input-group" id="hiddenSecretBox">
|
||||
<button type="button" style="width: 100%; border-radius: 10px; background-color: #2a69de; color: #fff; height: 47px; display: flex; justify-content: center; align-items: center; border: none; font-size: 16px; font-weight: 500; cursor: pointer; transition: background-color 0.2s;" onmouseover="this.style.backgroundColor='#1d56c0'" onmouseout="this.style.backgroundColor='#2a69de'" onclick="showPasswordPrompt()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 8px;">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<path d="m21 21-4.35-4.35"></path>
|
||||
</svg>
|
||||
<span>조회</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Callback URL -->
|
||||
<div class="info-row">
|
||||
<label class="info-label">Callback URL</label>
|
||||
<div class="info-value">
|
||||
<div class="info-value-box" th:text="${apiKey.redirecturi != null ? apiKey.redirecturi : '설정되지 않음'}">
|
||||
http://www.abcd.co.kr
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Actual Secret (hidden initially) -->
|
||||
<div id="revealedSecretBox" style="display: none; width: 100%;">
|
||||
<div class="dt-input-group">
|
||||
<div class="dt-input-box" th:text="${apiKey.clientsecret}" style="flex: 1;">secret-key</div>
|
||||
<button type="button" class="dt-btn-copy" th:data-secret="${apiKey.clientsecret}" onclick="copyToClipboardFromButton(this)">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<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>
|
||||
<span>복사</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IP Whitelist -->
|
||||
<div class="info-row">
|
||||
<label class="info-label">화이트리스트</label>
|
||||
<div class="info-value">
|
||||
<div class="info-value-box" th:text="${apiKey.allowedips != null ? apiKey.allowedips : '설정되지 않음'}">
|
||||
10.0.0.1, 10.0.0.2, 192.168.123.1
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Callback URL -->
|
||||
<div class="dt-row">
|
||||
<div class="dt-col">
|
||||
<label class="dt-label">Callback URL</label>
|
||||
<div class="dt-input-group">
|
||||
<div class="dt-input-box" th:text="${apiKey.redirecturi != null and !apiKey.redirecturi.isEmpty() ? apiKey.redirecturi : '설정되지않음'}">http://www.abcd.co.kr</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Registered APIs -->
|
||||
<div class="info-row info-row-vertical">
|
||||
<label class="info-label">API 목록</label>
|
||||
<div class="info-value">
|
||||
<div class="api-list-box">
|
||||
<div class="api-list-item" th:each="api : ${apiKey.apiList}">
|
||||
<span class="api-item-name" th:text="${api.apiDesc}">추가된 API 1</span>
|
||||
<span class="api-item-status">승인</span>
|
||||
</div>
|
||||
<!-- 화이트 리스트 -->
|
||||
<div class="dt-row">
|
||||
<div class="dt-col">
|
||||
<label class="dt-label">화이트 리스트</label>
|
||||
<div class="dt-input-group">
|
||||
<div class="dt-input-box" th:text="${apiKey.allowedips != null and !apiKey.allowedips.isEmpty() ? apiKey.allowedips : '설정되지않음'}">설정되지않음</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div class="empty-state" th:if="${apiKey.apiList == null or apiKey.apiList.isEmpty()}" style="padding: 24px;">
|
||||
<p>등록된 API가 없습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- API 목록 -->
|
||||
<div class="dt-row">
|
||||
<div class="dt-col">
|
||||
<label class="dt-label">API 목록</label>
|
||||
<div class="dt-api-list-box">
|
||||
<div class="dt-api-item" th:each="api : ${apiKey.apiList}">
|
||||
<span class="dt-api-name" th:text="${api.apiDesc}">추가된 API 1</span>
|
||||
<span class="dt-api-status-badge">승인</span>
|
||||
</div>
|
||||
<div class="dt-empty-message" th:if="${apiKey.apiList == null or apiKey.apiList.isEmpty()}">
|
||||
등록된 API가 없습니다.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modified Date -->
|
||||
<div class="info-row">
|
||||
<label class="info-label">최종 수정일자</label>
|
||||
<div class="info-value">
|
||||
<div class="info-value-box" th:text="${#temporals.format(apiKey.modifiedon, 'yyyy.MM.dd')}">
|
||||
2025.11.22
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 최종 수정일자 -->
|
||||
<div class="dt-row">
|
||||
<div class="dt-col">
|
||||
<label class="dt-label">최종 수정일자</label>
|
||||
<div class="dt-input-group">
|
||||
<div class="dt-input-box" th:text="${apiKey.modifiedon != null ? #temporals.format(apiKey.modifiedon, 'yyyy.MM.dd') : 'N/A'}">2025.11.22</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- Bottom Navigation Actions -->
|
||||
<div class="dt-actions" style="justify-content: flex-end; gap: 11px; margin-top: 30px;">
|
||||
<a th:href="@{/myapikey}" class="dt-btn-gray">목록</a>
|
||||
<button type="button" sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" class="dt-btn-red" th:data-client-id="${apiKey.clientid}" onclick="deleteApiKeyFromButton(this)">인증키 삭제</button>
|
||||
<a sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" class="dt-btn-blue" th:href="@{/myapikey/modify/step1(clientId=${apiKey.clientid})}">수정</a>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="form-actions-center">
|
||||
<a th:href="@{/myapikey}" class="btn btn-submit btn-secondary">
|
||||
목록
|
||||
</a>
|
||||
<button type="button"
|
||||
sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"
|
||||
class="btn btn-submit btn-danger"
|
||||
th:data-client-id="${apiKey.clientid}"
|
||||
onclick="deleteApiKeyFromButton(this)">
|
||||
인증키 삭제
|
||||
</button>
|
||||
<a sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"
|
||||
class="btn btn-submit btn-primary"
|
||||
th:href="@{/myapikey/modify/step1(clientId=${apiKey.clientid})}">
|
||||
수정
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</div><!-- /detail-wrap -->
|
||||
</div><!-- /app-management-content -->
|
||||
</div><!-- /service-main -->
|
||||
</div><!-- /signup-guide-v2 -->
|
||||
|
||||
</th:block>
|
||||
|
||||
|
||||
@@ -12,49 +12,61 @@
|
||||
<h1>비밀번호 변경</h1>
|
||||
</div>
|
||||
</section>
|
||||
<section layout:fragment="contentFragment" class="content">
|
||||
<div class="org-register-container">
|
||||
<div class="org-section-header">
|
||||
<h3>소중한 계정 보호를 위해 비밀번호를 변경해 주세요!</h3>
|
||||
<section layout:fragment="contentFragment">
|
||||
<div class="service-main app-management-layout">
|
||||
<th:block th:replace="~{fragment/djbank/service_sidebar :: sidebar('password')}"></th:block>
|
||||
<div class="app-management-content">
|
||||
<div class="password-change-wrapper">
|
||||
<h2 class="page-outer-title">비밀번호 변경</h2>
|
||||
|
||||
<form id="passwordChangeForm" th:action="@{/mypage/change_new_password}" method="post">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
|
||||
|
||||
<div class="register-form-container">
|
||||
<div class="info-notice-box">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#4685ef" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<path d="M12 16v-4"></path>
|
||||
<path d="M12 8h.01"></path>
|
||||
</svg>
|
||||
<p>소중한 계정 보호를 위해 비밀번호를 변경해 주세요!</p>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-label-wrapper">
|
||||
<span class="form-label-text">새 비밀번호</span>
|
||||
</div>
|
||||
<div class="form-field-wrapper">
|
||||
<input type="password" name="newPassword" class="form-input"
|
||||
placeholder="새 비밀번호 입력" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-label-wrapper">
|
||||
<span class="form-label-text">새 비밀번호 확인</span>
|
||||
</div>
|
||||
<div class="form-field-wrapper">
|
||||
<input type="password" name="confirmPassword" class="form-input"
|
||||
placeholder="새 비밀번호 확인 입력" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="password-rules-list">
|
||||
<li>영문 대문자, 소문자, 숫자, 특수문자 8~20자</li>
|
||||
<li>아이디, 휴대전화, 3자리 이상연속, 반복문자 사용불가</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="form-actions" style="justify-content: flex-end;">
|
||||
<div class="right-buttons">
|
||||
<button type="button" class="btn-cancel" onclick="history.back()">취소</button>
|
||||
<button type="submit" class="btn-apply btn-primary">변경</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="passwordChangeForm" th:action="@{/mypage/change_new_password}" method="post">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
|
||||
|
||||
<div class="org-form-container">
|
||||
<div class="org-form-group">
|
||||
<label class="org-form-label">
|
||||
<span class="required">새 비밀번호</span>
|
||||
</label>
|
||||
<div class="org-form-input-wrapper">
|
||||
<input type="password" name="newPassword" class="org-form-input"
|
||||
placeholder="새 비밀번호 입력" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="org-form-group">
|
||||
<label class="org-form-label">
|
||||
<span class="required">새 비밀번호 확인</span>
|
||||
</label>
|
||||
<div class="org-form-input-wrapper">
|
||||
<input type="password" name="confirmPassword" class="org-form-input"
|
||||
placeholder="새 비밀번호 확인 입력" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="org-info-notice">
|
||||
<ul>
|
||||
<li>영문 대문자, 소문자, 숫자, 특수문자 포함 8~20자</li>
|
||||
<li>아이디, 휴대전화, 3자리 이상 연속, 반복문자 사용 불가</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="org-action-buttons">
|
||||
<button type="button" class="btn btn-secondary" onclick="history.back()">취소</button>
|
||||
<button type="submit" class="btn btn-primary">변경</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<script th:if="${error}" th:inline="javascript">
|
||||
$(document).ready(function() {
|
||||
|
||||
@@ -9,32 +9,46 @@
|
||||
<h1>비밀번호 변경</h1>
|
||||
</div>
|
||||
</section>
|
||||
<section layout:fragment="contentFragment" class="content">
|
||||
<div class="org-register-container">
|
||||
<div class="org-section-header">
|
||||
<h3>비밀번호 변경을 위하여 현재 비밀번호를 입력해 주세요</h3>
|
||||
</div>
|
||||
<section layout:fragment="contentFragment">
|
||||
<div class="service-main app-management-layout">
|
||||
<th:block th:replace="~{fragment/djbank/service_sidebar :: sidebar('password')}"></th:block>
|
||||
<div class="app-management-content">
|
||||
<div class="password-change-wrapper">
|
||||
<h2 class="page-outer-title">비밀번호 변경</h2>
|
||||
|
||||
<form th:action="@{/verify_current_password}" method="post">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
|
||||
<form th:action="@{/verify_current_password}" method="post">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
|
||||
|
||||
<div class="org-form-container">
|
||||
<div class="org-form-group">
|
||||
<label class="org-form-label">
|
||||
<span class="required">현재 비밀번호</span>
|
||||
</label>
|
||||
<div class="org-form-input-wrapper">
|
||||
<input type="password" name="currentPassword" class="org-form-input"
|
||||
placeholder="비밀번호 입력" required>
|
||||
<div class="register-form-container">
|
||||
<div class="info-notice-box">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#4685ef" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<path d="M12 16v-4"></path>
|
||||
<path d="M12 8h.01"></path>
|
||||
</svg>
|
||||
<p>비밀번호 변경을 위하여 현재 비밀번호를 입력해 주세요</p>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-label-wrapper">
|
||||
<span class="form-label-text">현재 비밀번호</span>
|
||||
</div>
|
||||
<div class="form-field-wrapper">
|
||||
<input type="password" name="currentPassword" class="form-input"
|
||||
placeholder="비밀번호 입력" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="org-action-buttons">
|
||||
<button type="button" class="btn btn-secondary" th:onclick="|location.href='@{/}'|">취소</button>
|
||||
<button type="submit" class="btn btn-primary">확인</button>
|
||||
<div class="form-actions" style="justify-content: flex-end;">
|
||||
<div class="right-buttons">
|
||||
<button type="button" class="btn-cancel" th:onclick="|location.href='@{/}'|">취소</button>
|
||||
<button type="submit" class="btn-apply btn-primary">확인</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script th:if="${error}" th:inline="javascript">
|
||||
$(document).ready(function() {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -113,10 +113,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Statistics Table -->
|
||||
<div class="statistics-table-card">
|
||||
<div class="table-header">
|
||||
<h3 class="table-title">
|
||||
<!-- API Statistics Table Section -->
|
||||
<div class="statistics-table-section" style="margin-top: 40px;">
|
||||
<div class="table-controls" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; background: none; border: none; padding: 0;">
|
||||
<h3 class="table-title" style="font-size: 20px; font-weight: 700; color: #1e293b; display: flex; align-items: center; gap: 8px; margin: 0;">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 3v18h18"/>
|
||||
<path d="M18 9l-5 5-4-4-4 4"/>
|
||||
@@ -124,7 +124,7 @@
|
||||
API별 통계
|
||||
</h3>
|
||||
<a th:href="@{/statistics/api/download(startDate=${searchDto.startDate},endDate=${searchDto.endDate},clientId=${searchDto.clientId})}"
|
||||
class="btn-download" id="btnDownload">
|
||||
class="btn-download" id="btnDownload" style="display: inline-flex; align-items: center; gap: 6px; padding: 8px 16px; border: 1px solid #e2e8f0; border-radius: 8px; background: #fff; color: #475569; font-size: 14px; font-weight: 500; text-decoration: none; transition: all 0.2s;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="7 10 12 15 17 10"/>
|
||||
@@ -134,65 +134,63 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table class="statistics-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>API명</th>
|
||||
<th>API호출수</th>
|
||||
<th>성공</th>
|
||||
<th>실패</th>
|
||||
<th>통계</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="statisticsTableBody">
|
||||
<tr th:each="detail : ${details}">
|
||||
<td class="api-name">
|
||||
<span class="api-icon">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span th:text="${detail.apiName}">API 이름</span>
|
||||
</td>
|
||||
<td th:text="${#numbers.formatInteger(detail.attemptedCount, 0, 'COMMA')}">0</td>
|
||||
<td th:text="${#numbers.formatInteger(detail.completedCount, 0, 'COMMA')}">0</td>
|
||||
<td th:text="${#numbers.formatInteger(detail.failedCount, 0, 'COMMA')}">0</td>
|
||||
<td class="progress-cell">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-success" th:style="'width:' + ${detail.successRate} + '%'"></div>
|
||||
<div class="progress-failure" th:style="'width:' + ${detail.failureRate} + '%'"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Empty State -->
|
||||
<tr th:if="${details == null or details.isEmpty()}" class="empty-row">
|
||||
<td colspan="5">데이터가 없습니다.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot id="statisticsTableFoot" th:if="${details != null and !details.isEmpty()}">
|
||||
<tr class="total-row">
|
||||
<td class="api-name">
|
||||
<span class="api-icon total">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 3v18h18"/>
|
||||
<path d="M18 9l-5 5-4-4-4 4"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>전체</span>
|
||||
</td>
|
||||
<td th:text="${#numbers.formatInteger(summary.totalAttempted, 0, 'COMMA')}">0</td>
|
||||
<td th:text="${#numbers.formatInteger(summary.totalCompleted, 0, 'COMMA')}">0</td>
|
||||
<td th:text="${#numbers.formatInteger(summary.totalFailed, 0, 'COMMA')}">0</td>
|
||||
<td class="progress-cell">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-success" th:style="'width:' + ${summary.successRate} + '%'"></div>
|
||||
<div class="progress-failure" th:style="'width:' + ${summary.failureRate} + '%'"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
<div class="board-table">
|
||||
<div class="board-table-header">
|
||||
<div class="header-cell" style="flex: 2; min-width: 200px; text-align: left;">API명</div>
|
||||
<div class="header-cell" style="width: 120px; justify-content: flex-end;">API호출수</div>
|
||||
<div class="header-cell" style="width: 120px; justify-content: flex-end;">성공</div>
|
||||
<div class="header-cell" style="width: 120px; justify-content: flex-end;">실패</div>
|
||||
<div class="header-cell" style="flex: 1; min-width: 150px; justify-content: center;">통계</div>
|
||||
</div>
|
||||
|
||||
<div class="board-table-body" id="statisticsTableBody">
|
||||
<div class="board-table-row" th:each="detail : ${details}">
|
||||
<div class="row-cell" style="flex: 2; min-width: 200px; text-align: left; display: flex; align-items: center; gap: 8px;" data-label="API명">
|
||||
<span class="api-icon">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span th:text="${detail.apiName}">API 이름</span>
|
||||
</div>
|
||||
<div class="row-cell" style="width: 120px; justify-content: flex-end;" data-label="API호출수" th:text="${#numbers.formatInteger(detail.attemptedCount, 0, 'COMMA')}">0</div>
|
||||
<div class="row-cell" style="width: 120px; justify-content: flex-end;" data-label="성공" th:text="${#numbers.formatInteger(detail.completedCount, 0, 'COMMA')}">0</div>
|
||||
<div class="row-cell" style="width: 120px; justify-content: flex-end;" data-label="실패" th:text="${#numbers.formatInteger(detail.failedCount, 0, 'COMMA')}">0</div>
|
||||
<div class="row-cell" style="flex: 1; min-width: 150px; justify-content: center;" data-label="통계">
|
||||
<div class="progress-bar" style="width: 100%; height: 8px; background: #f1f5f9; border-radius: 4px; display: flex; overflow: hidden;">
|
||||
<div class="progress-success" th:style="'width:' + ${detail.successRate} + '%; background: #4A90D9;'"></div>
|
||||
<div class="progress-failure" th:style="'width:' + ${detail.failureRate} + '%; background: #F5A3B5;'"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Empty State -->
|
||||
<div th:if="${details == null or details.isEmpty()}" class="user-empty-state" style="border-bottom: 1px solid #e2e8f0;">
|
||||
<h3 style="text-align: center; padding: 40px 0; color: #64748b; font-weight: 400; font-size: 16px;">데이터가 없습니다.</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="board-table-body total-row-container" id="statisticsTableFoot" th:if="${details != null and !details.isEmpty()}" style="border-top: none; background-color: #f8fafc; border-bottom: 1px solid #e2e8f0;">
|
||||
<div class="board-table-row total-row" style="font-weight: 600;">
|
||||
<div class="row-cell" style="flex: 2; min-width: 200px; text-align: left; display: flex; align-items: center; gap: 8px;">
|
||||
<span class="api-icon total">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 3v18h18"/>
|
||||
<path d="M18 9l-5 5-4-4-4 4"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>전체</span>
|
||||
</div>
|
||||
<div class="row-cell" style="width: 120px; justify-content: flex-end;" th:text="${#numbers.formatInteger(summary.totalAttempted, 0, 'COMMA')}">0</div>
|
||||
<div class="row-cell" style="width: 120px; justify-content: flex-end;" th:text="${#numbers.formatInteger(summary.totalCompleted, 0, 'COMMA')}">0</div>
|
||||
<div class="row-cell" style="width: 120px; justify-content: flex-end;" th:text="${#numbers.formatInteger(summary.totalFailed, 0, 'COMMA')}">0</div>
|
||||
<div class="row-cell progress-cell" style="flex: 1; min-width: 150px; justify-content: center;">
|
||||
<div class="progress-bar" style="width: 100%; height: 8px; background: #f1f5f9; border-radius: 4px; display: flex; overflow: hidden;">
|
||||
<div class="progress-success" th:style="'width:' + ${summary.successRate} + '%; background: #4A90D9;'"></div>
|
||||
<div class="progress-failure" th:style="'width:' + ${summary.failureRate} + '%; background: #F5A3B5;'"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -243,41 +241,40 @@
|
||||
// Update Table
|
||||
function updateTable() {
|
||||
const tbody = $('#statisticsTableBody');
|
||||
const tfoot = $('#statisticsTableFoot');
|
||||
tbody.empty();
|
||||
|
||||
if (currentData.details && currentData.details.length > 0) {
|
||||
currentData.details.forEach(function(detail) {
|
||||
const row = `
|
||||
<tr>
|
||||
<td class="api-name">
|
||||
<div class="board-table-row">
|
||||
<div class="row-cell" style="flex: 2; min-width: 200px; text-align: left; display: flex; align-items: center; gap: 8px;" data-label="API명">
|
||||
<span class="api-icon">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>${detail.apiName || '-'}</span>
|
||||
</td>
|
||||
<td>${numberWithCommas(detail.attemptedCount)}</td>
|
||||
<td>${numberWithCommas(detail.completedCount)}</td>
|
||||
<td>${numberWithCommas(detail.failedCount)}</td>
|
||||
<td class="progress-cell">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-success" style="width:${detail.successRate}%"></div>
|
||||
<div class="progress-failure" style="width:${detail.failureRate}%"></div>
|
||||
</div>
|
||||
<div class="row-cell" style="width: 120px; justify-content: flex-end;" data-label="API호출수">${numberWithCommas(detail.attemptedCount)}</div>
|
||||
<div class="row-cell" style="width: 120px; justify-content: flex-end;" data-label="성공">${numberWithCommas(detail.completedCount)}</div>
|
||||
<div class="row-cell" style="width: 120px; justify-content: flex-end;" data-label="실패">${numberWithCommas(detail.failedCount)}</div>
|
||||
<div class="row-cell" style="flex: 1; min-width: 150px; justify-content: center;" data-label="통계">
|
||||
<div class="progress-bar" style="width: 100%; height: 8px; background: #f1f5f9; border-radius: 4px; display: flex; overflow: hidden;">
|
||||
<div class="progress-success" style="width:${detail.successRate}%; background: #4A90D9;"></div>
|
||||
<div class="progress-failure" style="width:${detail.failureRate}%; background: #F5A3B5;"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
tbody.append(row);
|
||||
});
|
||||
|
||||
// Update footer
|
||||
if (tfoot.length === 0) {
|
||||
if ($('#statisticsTableFoot').length === 0) {
|
||||
const footerHtml = `
|
||||
<tfoot id="statisticsTableFoot">
|
||||
<tr class="total-row">
|
||||
<td class="api-name">
|
||||
<div class="board-table-body total-row-container" id="statisticsTableFoot" style="border-top: none; background-color: #f8fafc; border-bottom: 1px solid #e2e8f0;">
|
||||
<div class="board-table-row total-row" style="font-weight: 600;">
|
||||
<div class="row-cell" style="flex: 2; min-width: 200px; text-align: left; display: flex; align-items: center; gap: 8px;">
|
||||
<span class="api-icon total">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 3v18h18"/>
|
||||
@@ -285,30 +282,31 @@
|
||||
</svg>
|
||||
</span>
|
||||
<span>전체</span>
|
||||
</td>
|
||||
<td>${numberWithCommas(currentData.summary.totalAttempted)}</td>
|
||||
<td>${numberWithCommas(currentData.summary.totalCompleted)}</td>
|
||||
<td>${numberWithCommas(currentData.summary.totalFailed)}</td>
|
||||
<td class="progress-cell">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-success" style="width:${currentData.summary.successRate}%"></div>
|
||||
<div class="progress-failure" style="width:${currentData.summary.failureRate}%"></div>
|
||||
</div>
|
||||
<div class="row-cell" style="width: 120px; justify-content: flex-end;">${numberWithCommas(currentData.summary.totalAttempted)}</div>
|
||||
<div class="row-cell" style="width: 120px; justify-content: flex-end;">${numberWithCommas(currentData.summary.totalCompleted)}</div>
|
||||
<div class="row-cell" style="width: 120px; justify-content: flex-end;">${numberWithCommas(currentData.summary.totalFailed)}</div>
|
||||
<div class="row-cell progress-cell" style="flex: 1; min-width: 150px; justify-content: center;">
|
||||
<div class="progress-bar" style="width: 100%; height: 8px; background: #f1f5f9; border-radius: 4px; display: flex; overflow: hidden;">
|
||||
<div class="progress-success" style="width:${currentData.summary.successRate}%; background: #4A90D9;"></div>
|
||||
<div class="progress-failure" style="width:${currentData.summary.failureRate}%; background: #F5A3B5;"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
tbody.after(footerHtml);
|
||||
$('.board-table').append(footerHtml);
|
||||
} else {
|
||||
tfoot.find('td:eq(1)').text(numberWithCommas(currentData.summary.totalAttempted));
|
||||
tfoot.find('td:eq(2)').text(numberWithCommas(currentData.summary.totalCompleted));
|
||||
tfoot.find('td:eq(3)').text(numberWithCommas(currentData.summary.totalFailed));
|
||||
const tfoot = $('#statisticsTableFoot');
|
||||
tfoot.find('.row-cell:eq(1)').text(numberWithCommas(currentData.summary.totalAttempted));
|
||||
tfoot.find('.row-cell:eq(2)').text(numberWithCommas(currentData.summary.totalCompleted));
|
||||
tfoot.find('.row-cell:eq(3)').text(numberWithCommas(currentData.summary.totalFailed));
|
||||
tfoot.find('.progress-success').css('width', currentData.summary.successRate + '%');
|
||||
tfoot.find('.progress-failure').css('width', currentData.summary.failureRate + '%');
|
||||
}
|
||||
} else {
|
||||
tbody.html('<tr class="empty-row"><td colspan="5">데이터가 없습니다.</td></tr>');
|
||||
tfoot.remove();
|
||||
tbody.html('<div class="user-empty-state" style="border-bottom: 1px solid #e2e8f0;"><h3 style="text-align: center; padding: 40px 0; color: #64748b; font-weight: 400; font-size: 16px;">데이터가 없습니다.</h3></div>');
|
||||
$('#statisticsTableFoot').remove();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user