Webhook 신청/관리 및 EventType 제공 기능 추가:
- Webhook 신청/수정 3단계 UI 및 관련 서비스 구현 - TSEAIRM28 공통코드 기반 EventType 조회 기능 추가 - 신규/수정 세션 관리 및 비밀번호 재인증 로직 포함
This commit is contained in:
@@ -388,6 +388,27 @@ page:
|
||||
api_statistics:
|
||||
name: "이용 통계"
|
||||
path: "/statistics/api"
|
||||
webhook:
|
||||
name: "Webhook 관리"
|
||||
path: "/webhook"
|
||||
webhook_register_step1:
|
||||
name: "Webhook 신청 (기본 정보)"
|
||||
path: "/webhook/register/step1"
|
||||
webhook_register_step2:
|
||||
name: "Webhook 신청 (API 선택)"
|
||||
path: "/webhook/register/step2"
|
||||
webhook_register_step3:
|
||||
name: "Webhook 신청 완료"
|
||||
path: "/webhook/register/step3"
|
||||
webhook_modify_step1:
|
||||
name: "Webhook 수정 (기본 정보)"
|
||||
path: "/webhook/modify/step1"
|
||||
webhook_modify_step2:
|
||||
name: "Webhook 수정 (API 선택)"
|
||||
path: "/webhook/modify/step2"
|
||||
webhook_modify_step3:
|
||||
name: "Webhook 수정 완료"
|
||||
path: "/webhook/modify/step3"
|
||||
|
||||
# 에디터 이미지 설정 (약관 이미지 표시용)
|
||||
editor:
|
||||
|
||||
@@ -19622,6 +19622,421 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
.webhook-container {
|
||||
max-width: 880px;
|
||||
margin: 0 auto;
|
||||
padding: 24px 16px 60px;
|
||||
}
|
||||
.webhook-container .field-help {
|
||||
color: #6b7280;
|
||||
font-size: 13px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.webhook-container .field-error,
|
||||
.webhook-container .webhook-field-error {
|
||||
color: #e03131;
|
||||
font-size: 13px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.webhook-container .req {
|
||||
color: #e03131;
|
||||
}
|
||||
|
||||
.webhook-empty {
|
||||
text-align: center;
|
||||
padding: 64px 20px;
|
||||
background: #f5f7fb;
|
||||
border: 1px solid #e2e6ee;
|
||||
border-radius: 14px;
|
||||
}
|
||||
.webhook-empty .empty-icon {
|
||||
font-size: 48px;
|
||||
}
|
||||
.webhook-empty h3 {
|
||||
margin: 16px 0 8px;
|
||||
font-size: 20px;
|
||||
}
|
||||
.webhook-empty p {
|
||||
color: #6b7280;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.webhook-steps {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.webhook-steps .webhook-step {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
background: #f5f7fb;
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
.webhook-steps .webhook-step .step-no {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: #d5dbe6;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.webhook-steps .webhook-step.active {
|
||||
background: rgba(11, 95, 255, 0.1);
|
||||
color: #0b5fff;
|
||||
}
|
||||
.webhook-steps .webhook-step.active .step-no {
|
||||
background: #0b5fff;
|
||||
}
|
||||
|
||||
.webhook-field {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.webhook-field label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.webhook-field input[type=text],
|
||||
.webhook-field input[type=password] {
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #e2e6ee;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.webhook-field input[type=text]:focus,
|
||||
.webhook-field input[type=password]:focus {
|
||||
outline: none;
|
||||
border-color: #0b5fff;
|
||||
}
|
||||
|
||||
.webhook-eventtype-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
.webhook-eventtype-list .eventtype-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #e2e6ee;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.webhook-eventtype-list .eventtype-item input {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
.webhook-eventtype-list .eventtype-item .eventtype-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
.webhook-eventtype-list .eventtype-item .eventtype-code {
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
margin-left: auto;
|
||||
}
|
||||
.webhook-eventtype-list .eventtype-item:hover {
|
||||
border-color: #0b5fff;
|
||||
}
|
||||
|
||||
.webhook-api-groups {
|
||||
margin: 16px 0 8px;
|
||||
}
|
||||
|
||||
.webhook-api-group {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.webhook-api-group .group-name {
|
||||
font-size: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.webhook-api-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.webhook-api-list .webhook-api-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #e2e6ee;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.webhook-api-list .webhook-api-card input {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
.webhook-api-list .webhook-api-card .api-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
.webhook-api-list .webhook-api-card .api-id {
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
margin-left: auto;
|
||||
}
|
||||
.webhook-api-list .webhook-api-card:hover {
|
||||
border-color: #0b5fff;
|
||||
}
|
||||
|
||||
.webhook-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-top: 32px;
|
||||
}
|
||||
.webhook-actions.center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-webhook-next,
|
||||
.btn-webhook-create {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px 28px;
|
||||
background: #0b5fff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
.btn-webhook-next:hover,
|
||||
.btn-webhook-create:hover {
|
||||
background: #0847c4;
|
||||
}
|
||||
|
||||
.btn-webhook-cancel {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px 28px;
|
||||
background: #fff;
|
||||
color: #6b7280;
|
||||
border: 1px solid #e2e6ee;
|
||||
border-radius: 10px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
.btn-webhook-cancel:hover {
|
||||
border-color: #6b7280;
|
||||
}
|
||||
|
||||
.btn-webhook-danger {
|
||||
padding: 12px 28px;
|
||||
background: #fff;
|
||||
color: #e03131;
|
||||
border: 1px solid #e03131;
|
||||
border-radius: 10px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-webhook-danger:hover {
|
||||
background: #e03131;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-mini {
|
||||
padding: 4px 12px;
|
||||
background: #f5f7fb;
|
||||
color: #0b5fff;
|
||||
border: 1px solid #e2e6ee;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-mini:hover {
|
||||
background: rgba(11, 95, 255, 0.1);
|
||||
}
|
||||
|
||||
.btn-copy {
|
||||
padding: 10px 18px;
|
||||
background: #0b5fff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-copy:hover {
|
||||
background: #0847c4;
|
||||
}
|
||||
|
||||
.webhook-card {
|
||||
border: 1px solid #e2e6ee;
|
||||
border-radius: 14px;
|
||||
padding: 24px;
|
||||
}
|
||||
.webhook-card .webhook-card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.webhook-card .webhook-card-head h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
.webhook-card .webhook-card-head .webhook-created {
|
||||
color: #6b7280;
|
||||
font-size: 13px;
|
||||
}
|
||||
.webhook-card .webhook-card-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 14px 0;
|
||||
border-top: 1px solid #e2e6ee;
|
||||
}
|
||||
.webhook-card .webhook-card-row .row-label {
|
||||
width: 110px;
|
||||
color: #6b7280;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.webhook-card .webhook-card-row .row-value {
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
}
|
||||
.webhook-card .webhook-card-row.secret-row, .webhook-card .webhook-card-row .secret-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.webhook-card .webhook-card-row code {
|
||||
background: #f5f7fb;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.webhook-card .eventtype-badge,
|
||||
.webhook-card .api-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
margin: 2px 4px 2px 0;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
background: rgba(11, 95, 255, 0.1);
|
||||
color: #0b5fff;
|
||||
}
|
||||
.webhook-card .api-badge {
|
||||
background: #f5f7fb;
|
||||
color: #6b7280;
|
||||
}
|
||||
.webhook-card .webhook-card-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.webhook-complete {
|
||||
text-align: center;
|
||||
padding: 24px;
|
||||
}
|
||||
.webhook-complete .complete-icon {
|
||||
font-size: 48px;
|
||||
}
|
||||
.webhook-complete h3 {
|
||||
margin: 16px 0;
|
||||
}
|
||||
.webhook-complete .webhook-secret-box {
|
||||
text-align: left;
|
||||
max-width: 640px;
|
||||
margin: 24px auto;
|
||||
padding: 20px;
|
||||
background: #f5f7fb;
|
||||
border: 1px solid #e2e6ee;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.webhook-complete .webhook-secret-box .secret-warning {
|
||||
color: #92400e;
|
||||
background: #fef3c7;
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.webhook-complete .webhook-secret-box label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.webhook-complete .webhook-secret-box .secret-value-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.webhook-complete .webhook-secret-box .secret-value-row input {
|
||||
flex: 1;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #e2e6ee;
|
||||
border-radius: 10px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.webhook-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.webhook-modal .webhook-modal-box {
|
||||
background: #fff;
|
||||
border-radius: 14px;
|
||||
padding: 28px;
|
||||
width: 360px;
|
||||
max-width: 90vw;
|
||||
}
|
||||
.webhook-modal .webhook-modal-box h4 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.webhook-modal .webhook-modal-box p {
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.webhook-modal .webhook-modal-box input {
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #e2e6ee;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.webhook-modal .webhook-modal-box input:focus {
|
||||
outline: none;
|
||||
border-color: #0b5fff;
|
||||
}
|
||||
.webhook-modal .webhook-modal-box .webhook-modal-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.webhook-modal .webhook-modal-box .webhook-modal-actions .btn-webhook-next, .webhook-modal .webhook-modal-box .webhook-modal-actions .btn-webhook-cancel {
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.d-none {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
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
@@ -66,6 +66,7 @@
|
||||
@use 'pages/terms-agreements' as *;
|
||||
@use 'pages/service' as *;
|
||||
@use 'pages/api-statistics' as *;
|
||||
@use 'pages/webhook' as *;
|
||||
|
||||
// 6. Themes
|
||||
@use 'themes/dark' as *;
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
// Webhook 신청/관리 (DJPGPT0004)
|
||||
// App API Key 신청 3-step UI 톤을 답습한 자체 완결 스타일.
|
||||
|
||||
$wh-primary: #0b5fff;
|
||||
$wh-primary-dark: #0847c4;
|
||||
$wh-danger: #e03131;
|
||||
$wh-border: #e2e6ee;
|
||||
$wh-muted: #6b7280;
|
||||
$wh-bg-soft: #f5f7fb;
|
||||
|
||||
.webhook-container {
|
||||
max-width: 880px;
|
||||
margin: 0 auto;
|
||||
padding: 24px 16px 60px;
|
||||
|
||||
.field-help { color: $wh-muted; font-size: 13px; margin-top: 6px; }
|
||||
.field-error,
|
||||
.webhook-field-error { color: $wh-danger; font-size: 13px; margin-top: 6px; }
|
||||
.req { color: $wh-danger; }
|
||||
}
|
||||
|
||||
// ---------- 빈 상태 ----------
|
||||
.webhook-empty {
|
||||
text-align: center;
|
||||
padding: 64px 20px;
|
||||
background: $wh-bg-soft;
|
||||
border: 1px solid $wh-border;
|
||||
border-radius: 14px;
|
||||
|
||||
.empty-icon { font-size: 48px; }
|
||||
h3 { margin: 16px 0 8px; font-size: 20px; }
|
||||
p { color: $wh-muted; margin-bottom: 24px; }
|
||||
}
|
||||
|
||||
// ---------- 진행 표시 ----------
|
||||
.webhook-steps {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 28px;
|
||||
|
||||
.webhook-step {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
background: $wh-bg-soft;
|
||||
color: $wh-muted;
|
||||
font-size: 14px;
|
||||
|
||||
.step-no {
|
||||
width: 22px; height: 22px;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: #d5dbe6; color: #fff; font-size: 12px; font-weight: 700;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: rgba($wh-primary, 0.1);
|
||||
color: $wh-primary;
|
||||
.step-no { background: $wh-primary; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 폼 ----------
|
||||
.webhook-field {
|
||||
margin-bottom: 24px;
|
||||
|
||||
label { display: block; font-weight: 600; margin-bottom: 8px; }
|
||||
|
||||
input[type="text"],
|
||||
input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid $wh-border;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
&:focus { outline: none; border-color: $wh-primary; }
|
||||
}
|
||||
}
|
||||
|
||||
.webhook-eventtype-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
|
||||
.eventtype-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid $wh-border;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
input { width: 18px; height: 18px; }
|
||||
.eventtype-name { font-weight: 600; }
|
||||
.eventtype-code { color: $wh-muted; font-size: 12px; margin-left: auto; }
|
||||
&:hover { border-color: $wh-primary; }
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- API 선택 ----------
|
||||
.webhook-api-groups { margin: 16px 0 8px; }
|
||||
.webhook-api-group {
|
||||
margin-bottom: 24px;
|
||||
.group-name { font-size: 16px; margin-bottom: 12px; }
|
||||
}
|
||||
.webhook-api-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 10px;
|
||||
|
||||
.webhook-api-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid $wh-border;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
input { width: 18px; height: 18px; }
|
||||
.api-name { font-weight: 600; }
|
||||
.api-id { color: $wh-muted; font-size: 12px; margin-left: auto; }
|
||||
&:hover { border-color: $wh-primary; }
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 액션 버튼 ----------
|
||||
.webhook-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-top: 32px;
|
||||
&.center { justify-content: center; }
|
||||
}
|
||||
|
||||
.btn-webhook-next,
|
||||
.btn-webhook-create {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
padding: 12px 28px;
|
||||
background: $wh-primary; color: #fff;
|
||||
border: none; border-radius: 10px;
|
||||
font-size: 15px; font-weight: 600; cursor: pointer; text-decoration: none;
|
||||
&:hover { background: $wh-primary-dark; }
|
||||
}
|
||||
.btn-webhook-cancel {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
padding: 12px 28px;
|
||||
background: #fff; color: $wh-muted;
|
||||
border: 1px solid $wh-border; border-radius: 10px;
|
||||
font-size: 15px; font-weight: 600; cursor: pointer; text-decoration: none;
|
||||
&:hover { border-color: $wh-muted; }
|
||||
}
|
||||
.btn-webhook-danger {
|
||||
padding: 12px 28px;
|
||||
background: #fff; color: $wh-danger;
|
||||
border: 1px solid $wh-danger; border-radius: 10px;
|
||||
font-size: 15px; font-weight: 600; cursor: pointer;
|
||||
&:hover { background: $wh-danger; color: #fff; }
|
||||
}
|
||||
.btn-mini {
|
||||
padding: 4px 12px;
|
||||
background: $wh-bg-soft; color: $wh-primary;
|
||||
border: 1px solid $wh-border; border-radius: 8px;
|
||||
font-size: 12px; cursor: pointer;
|
||||
&:hover { background: rgba($wh-primary, 0.1); }
|
||||
}
|
||||
.btn-copy {
|
||||
padding: 10px 18px;
|
||||
background: $wh-primary; color: #fff;
|
||||
border: none; border-radius: 8px; cursor: pointer;
|
||||
&:hover { background: $wh-primary-dark; }
|
||||
}
|
||||
|
||||
// ---------- 등록 카드 ----------
|
||||
.webhook-card {
|
||||
border: 1px solid $wh-border;
|
||||
border-radius: 14px;
|
||||
padding: 24px;
|
||||
|
||||
.webhook-card-head {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
h3 { font-size: 18px; }
|
||||
.webhook-created { color: $wh-muted; font-size: 13px; }
|
||||
}
|
||||
|
||||
.webhook-card-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 14px 0;
|
||||
border-top: 1px solid $wh-border;
|
||||
|
||||
.row-label { width: 110px; color: $wh-muted; font-weight: 600; flex-shrink: 0; }
|
||||
.row-value { flex: 1; word-break: break-all; }
|
||||
&.secret-row, .secret-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
code { background: $wh-bg-soft; padding: 4px 8px; border-radius: 6px; }
|
||||
}
|
||||
|
||||
.eventtype-badge,
|
||||
.api-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 10px; margin: 2px 4px 2px 0;
|
||||
border-radius: 999px; font-size: 12px;
|
||||
background: rgba($wh-primary, 0.1); color: $wh-primary;
|
||||
}
|
||||
.api-badge { background: $wh-bg-soft; color: $wh-muted; }
|
||||
|
||||
.webhook-card-actions {
|
||||
display: flex; gap: 12px; justify-content: flex-end;
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 완료 화면 ----------
|
||||
.webhook-complete {
|
||||
text-align: center;
|
||||
padding: 24px;
|
||||
|
||||
.complete-icon { font-size: 48px; }
|
||||
h3 { margin: 16px 0; }
|
||||
|
||||
.webhook-secret-box {
|
||||
text-align: left;
|
||||
max-width: 640px; margin: 24px auto;
|
||||
padding: 20px;
|
||||
background: $wh-bg-soft; border: 1px solid $wh-border; border-radius: 12px;
|
||||
|
||||
.secret-warning {
|
||||
color: #92400e; background: #fef3c7;
|
||||
border-radius: 8px; padding: 12px 14px; margin-bottom: 16px; font-size: 13px;
|
||||
}
|
||||
label { display: block; font-weight: 600; margin-bottom: 8px; }
|
||||
.secret-value-row {
|
||||
display: flex; gap: 8px;
|
||||
input { flex: 1; padding: 12px 14px; border: 1px solid $wh-border; border-radius: 10px; font-family: monospace; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 비밀번호 모달 ----------
|
||||
.webhook-modal {
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
z-index: 1000;
|
||||
|
||||
.webhook-modal-box {
|
||||
background: #fff; border-radius: 14px;
|
||||
padding: 28px; width: 360px; max-width: 90vw;
|
||||
|
||||
h4 { font-size: 18px; margin-bottom: 8px; }
|
||||
p { color: $wh-muted; font-size: 14px; margin-bottom: 16px; }
|
||||
input {
|
||||
width: 100%; padding: 12px 14px;
|
||||
border: 1px solid $wh-border; border-radius: 10px; margin-bottom: 8px;
|
||||
&:focus { outline: none; border-color: $wh-primary; }
|
||||
}
|
||||
.webhook-modal-actions {
|
||||
display: flex; gap: 12px; justify-content: flex-end; margin-top: 12px;
|
||||
.btn-webhook-next, .btn-webhook-cancel { padding: 10px 20px; font-size: 14px; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<!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}">
|
||||
|
||||
<body>
|
||||
<section layout:fragment="title">
|
||||
<div class="page-title-banner">
|
||||
<img th:src="@{/img/img_title_bg.png}" class="title-image">
|
||||
<h1>Webhook 관리</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<th:block layout:fragment="contentFragment">
|
||||
<section class="webhook-container">
|
||||
<div class="webhook-empty">
|
||||
<div class="empty-icon">🔔</div>
|
||||
<h3>등록된 Webhook이 없습니다</h3>
|
||||
<p>API 서비스의 점검·지연·장애 알림을 받을 Webhook을 신청해보세요.</p>
|
||||
|
||||
<div class="webhook-empty-action" sec:authorize="hasRole('ROLE_APP')">
|
||||
<button type="button" class="btn-webhook-create" id="requestWebhook">
|
||||
Webhook 신청
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</th:block>
|
||||
|
||||
<th:block layout:fragment="contentScript">
|
||||
<script th:inline="javascript">
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var btn = document.getElementById('requestWebhook');
|
||||
if (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
window.location.href = '/webhook/register/step1?clear=true';
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</th:block>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,158 @@
|
||||
<!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}">
|
||||
|
||||
<body>
|
||||
<section layout:fragment="title">
|
||||
<div class="page-title-banner">
|
||||
<img th:src="@{/img/img_title_bg.png}" class="title-image">
|
||||
<h1>Webhook 관리</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<th:block layout:fragment="contentFragment">
|
||||
<section class="webhook-container">
|
||||
<div class="webhook-card" th:object="${webhook}">
|
||||
|
||||
<div class="webhook-card-head">
|
||||
<h3>등록된 Webhook</h3>
|
||||
<span class="webhook-created" th:if="*{createdDate != null and #strings.length(createdDate) >= 8}"
|
||||
th:text="|등록일 ${#strings.substring(webhook.createdDate,0,4)}-${#strings.substring(webhook.createdDate,4,6)}-${#strings.substring(webhook.createdDate,6,8)}|">등록일</span>
|
||||
</div>
|
||||
|
||||
<div class="webhook-card-row">
|
||||
<span class="row-label">수신 URL</span>
|
||||
<span class="row-value" th:text="*{targetUrl}">https://...</span>
|
||||
</div>
|
||||
|
||||
<div class="webhook-card-row">
|
||||
<span class="row-label">알림 이벤트</span>
|
||||
<span class="row-value">
|
||||
<span class="eventtype-badge" th:each="et : *{eventTypes}" th:text="${et.name}">이벤트</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="webhook-card-row">
|
||||
<span class="row-label">대상 API</span>
|
||||
<span class="row-value">
|
||||
<span class="api-badge" th:each="apiId : *{apiIds}" th:text="${apiId}">API</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="webhook-card-row">
|
||||
<span class="row-label">Secret Key</span>
|
||||
<span class="row-value secret-row">
|
||||
<code id="secretMasked" th:text="*{secretMasked}">••••••••</code>
|
||||
<button type="button" class="btn-mini" id="btnRevealSecret">조회</button>
|
||||
<button type="button" class="btn-mini" id="btnRegenSecret">재발급</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="webhook-card-actions">
|
||||
<a class="btn-webhook-next" th:href="@{/webhook/modify/step1}">수정</a>
|
||||
<button type="button" class="btn-webhook-danger" id="btnDeleteWebhook">삭제</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 비밀번호 재인증 모달 -->
|
||||
<div class="webhook-modal" id="pwModal" style="display:none;">
|
||||
<div class="webhook-modal-box">
|
||||
<h4 id="pwModalTitle">비밀번호 확인</h4>
|
||||
<p id="pwModalDesc">계속하려면 비밀번호를 입력하세요.</p>
|
||||
<input type="password" id="pwModalInput" placeholder="비밀번호" autocomplete="current-password">
|
||||
<p class="field-error" id="pwModalError" style="display:none;"></p>
|
||||
<div class="webhook-modal-actions">
|
||||
<button type="button" class="btn-webhook-cancel" id="pwModalCancel">취소</button>
|
||||
<button type="button" class="btn-webhook-next" id="pwModalConfirm">확인</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</th:block>
|
||||
|
||||
<th:block layout:fragment="contentScript">
|
||||
<script th:inline="javascript">
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var csrfToken = document.querySelector('meta[name="_csrf"]');
|
||||
var csrfHeaderMeta = document.querySelector('meta[name="_csrf_header"]');
|
||||
var CSRF_TOKEN = csrfToken ? csrfToken.getAttribute('content') : '';
|
||||
var CSRF_HEADER = csrfHeaderMeta ? csrfHeaderMeta.getAttribute('content') : 'X-XSRF-TOKEN';
|
||||
|
||||
var modal = document.getElementById('pwModal');
|
||||
var input = document.getElementById('pwModalInput');
|
||||
var errorEl = document.getElementById('pwModalError');
|
||||
var titleEl = document.getElementById('pwModalTitle');
|
||||
var descEl = document.getElementById('pwModalDesc');
|
||||
var pendingAction = null;
|
||||
|
||||
function openModal(title, desc, action) {
|
||||
titleEl.textContent = title;
|
||||
descEl.textContent = desc;
|
||||
input.value = '';
|
||||
errorEl.style.display = 'none';
|
||||
pendingAction = action;
|
||||
modal.style.display = 'flex';
|
||||
input.focus();
|
||||
}
|
||||
function closeModal() { modal.style.display = 'none'; pendingAction = null; }
|
||||
|
||||
document.getElementById('pwModalCancel').addEventListener('click', closeModal);
|
||||
document.getElementById('pwModalConfirm').addEventListener('click', function () {
|
||||
if (pendingAction) { pendingAction(input.value); }
|
||||
});
|
||||
input.addEventListener('keyup', function (e) {
|
||||
if (e.key === 'Enter' && pendingAction) { pendingAction(input.value); }
|
||||
});
|
||||
|
||||
function post(url, password) {
|
||||
var headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
|
||||
headers[CSRF_HEADER] = CSRF_TOKEN;
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: 'password=' + encodeURIComponent(password)
|
||||
}).then(function (r) { return r.json(); });
|
||||
}
|
||||
|
||||
function showError(msg) { errorEl.textContent = msg; errorEl.style.display = 'block'; }
|
||||
|
||||
// Secret 조회
|
||||
document.getElementById('btnRevealSecret').addEventListener('click', function () {
|
||||
openModal('Secret 조회', '비밀번호 확인 후 Secret Key를 표시합니다.', function (pw) {
|
||||
post('/webhook/verify-secret', pw).then(function (res) {
|
||||
if (res.success) {
|
||||
document.getElementById('secretMasked').textContent = res.secret;
|
||||
closeModal();
|
||||
} else { showError(res.message || '실패했습니다.'); }
|
||||
}).catch(function () { showError('요청 처리 중 오류가 발생했습니다.'); });
|
||||
});
|
||||
});
|
||||
|
||||
// Secret 재발급
|
||||
document.getElementById('btnRegenSecret').addEventListener('click', function () {
|
||||
openModal('Secret 재발급', '재발급 시 기존 Secret은 무효화됩니다. 계속하려면 비밀번호를 입력하세요.', function (pw) {
|
||||
post('/webhook/regenerate-secret', pw).then(function (res) {
|
||||
if (res.success) {
|
||||
document.getElementById('secretMasked').textContent = res.secret;
|
||||
closeModal();
|
||||
alert('Secret이 재발급되었습니다. 새 값을 안전하게 보관하세요.');
|
||||
} else { showError(res.message || '실패했습니다.'); }
|
||||
}).catch(function () { showError('요청 처리 중 오류가 발생했습니다.'); });
|
||||
});
|
||||
});
|
||||
|
||||
// 삭제
|
||||
document.getElementById('btnDeleteWebhook').addEventListener('click', function () {
|
||||
openModal('Webhook 삭제', '삭제하면 복구할 수 없습니다. 계속하려면 비밀번호를 입력하세요.', function (pw) {
|
||||
post('/webhook/delete', pw).then(function (res) {
|
||||
if (res.success) { window.location.href = '/webhook'; }
|
||||
else { showError(res.message || '실패했습니다.'); }
|
||||
}).catch(function () { showError('요청 처리 중 오류가 발생했습니다.'); });
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</th:block>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,59 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/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>Webhook 수정</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<th:block layout:fragment="contentFragment">
|
||||
<section class="webhook-container webhook-register">
|
||||
|
||||
<div class="webhook-steps" th:with="cur=${currentStep}">
|
||||
<div class="webhook-step" th:classappend="${cur >= 1} ? 'active'">
|
||||
<span class="step-no">1</span><span class="step-label">기본 정보</span>
|
||||
</div>
|
||||
<div class="webhook-step" th:classappend="${cur >= 2} ? 'active'">
|
||||
<span class="step-no">2</span><span class="step-label">API 선택</span>
|
||||
</div>
|
||||
<div class="webhook-step" th:classappend="${cur >= 3} ? 'active'">
|
||||
<span class="step-no">3</span><span class="step-label">완료</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="modifyStep1Form" method="post" th:action="@{/webhook/modify/step1}" th:object="${webhookModification}">
|
||||
|
||||
<div class="webhook-field">
|
||||
<label for="targetUrl">Webhook 수신 URL <span class="req">*</span></label>
|
||||
<input type="text" id="targetUrl" th:field="*{targetUrl}"
|
||||
placeholder="https://example.com/webhook" autocomplete="off">
|
||||
<p class="field-error" th:if="${#fields.hasErrors('targetUrl')}" th:errors="*{targetUrl}">URL 오류</p>
|
||||
<p class="field-help">이벤트 발생 시 이 URL로 서명된 POST 요청이 전송됩니다.</p>
|
||||
</div>
|
||||
|
||||
<div class="webhook-field">
|
||||
<label>알림 받을 이벤트 <span class="req">*</span></label>
|
||||
<div class="webhook-eventtype-list">
|
||||
<label class="eventtype-item" th:each="et : ${eventTypes}">
|
||||
<input type="checkbox" name="eventTypes" th:value="${et.code}"
|
||||
th:checked="${#lists.contains(webhookModification.eventTypes, et.code)}">
|
||||
<span class="eventtype-name" th:text="${et.name}">이벤트명</span>
|
||||
<span class="eventtype-code" th:text="${et.code}">CODE</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="field-error" th:if="${#fields.hasErrors('eventTypes')}" th:errors="*{eventTypes}">이벤트 오류</p>
|
||||
</div>
|
||||
|
||||
<div class="webhook-actions">
|
||||
<a class="btn-webhook-cancel" th:href="@{/webhook/modify/cancel}">취소</a>
|
||||
<button type="submit" class="btn-webhook-next">다음</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</th:block>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,55 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/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>Webhook 수정</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<th:block layout:fragment="contentFragment">
|
||||
<section class="webhook-container webhook-register">
|
||||
|
||||
<div class="webhook-steps" th:with="cur=${currentStep}">
|
||||
<div class="webhook-step" th:classappend="${cur >= 1} ? 'active'">
|
||||
<span class="step-no">1</span><span class="step-label">기본 정보</span>
|
||||
</div>
|
||||
<div class="webhook-step" th:classappend="${cur >= 2} ? 'active'">
|
||||
<span class="step-no">2</span><span class="step-label">API 선택</span>
|
||||
</div>
|
||||
<div class="webhook-step" th:classappend="${cur >= 3} ? 'active'">
|
||||
<span class="step-no">3</span><span class="step-label">완료</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="webhook-field-error" th:if="${error}" th:text="${error}"></p>
|
||||
|
||||
<form id="modifyStep2Form" method="post" th:action="@{/webhook/modify/step2}">
|
||||
<p class="field-help">이 Webhook으로 상태 알림을 받을 API를 선택하세요. (1개 이상)</p>
|
||||
|
||||
<div class="webhook-api-groups">
|
||||
<div class="webhook-api-group" th:each="group : ${apiGroups}">
|
||||
<h3 class="group-name" th:text="${group.groupName}">그룹명</h3>
|
||||
<div class="webhook-api-list">
|
||||
<label class="webhook-api-card" th:each="api : ${group.apiGroupApiList}">
|
||||
<input type="checkbox" name="selectedApis" th:value="${api.apiId}"
|
||||
th:checked="${selectedApis != null and #lists.contains(selectedApis, api.apiId)}">
|
||||
<span class="api-name" th:text="${api.apiName}">API 이름</span>
|
||||
<span class="api-id" th:text="${api.apiId}">API ID</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="webhook-actions">
|
||||
<a class="btn-webhook-cancel" th:href="@{/webhook/modify/step1}">이전</a>
|
||||
<button type="submit" class="btn-webhook-next">수정 완료</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</th:block>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/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>Webhook 수정 완료</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<th:block layout:fragment="contentFragment">
|
||||
<section class="webhook-container webhook-register">
|
||||
|
||||
<div class="webhook-steps">
|
||||
<div class="webhook-step active"><span class="step-no">1</span><span class="step-label">기본 정보</span></div>
|
||||
<div class="webhook-step active"><span class="step-no">2</span><span class="step-label">API 선택</span></div>
|
||||
<div class="webhook-step active"><span class="step-no">3</span><span class="step-label">완료</span></div>
|
||||
</div>
|
||||
|
||||
<div class="webhook-complete">
|
||||
<div class="complete-icon">✅</div>
|
||||
<h3>Webhook 설정이 수정되었습니다</h3>
|
||||
<p class="field-help">Secret Key는 변경되지 않았습니다.</p>
|
||||
|
||||
<div class="webhook-actions center">
|
||||
<a class="btn-webhook-next" th:href="@{/webhook}">완료</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</th:block>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,60 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/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>Webhook 신청</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<th:block layout:fragment="contentFragment">
|
||||
<section class="webhook-container webhook-register">
|
||||
|
||||
<!-- 진행 표시 -->
|
||||
<div class="webhook-steps" th:with="cur=${currentStep}">
|
||||
<div class="webhook-step" th:classappend="${cur >= 1} ? 'active'">
|
||||
<span class="step-no">1</span><span class="step-label">기본 정보</span>
|
||||
</div>
|
||||
<div class="webhook-step" th:classappend="${cur >= 2} ? 'active'">
|
||||
<span class="step-no">2</span><span class="step-label">API 선택</span>
|
||||
</div>
|
||||
<div class="webhook-step" th:classappend="${cur >= 3} ? 'active'">
|
||||
<span class="step-no">3</span><span class="step-label">완료</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="registerStep1Form" method="post" th:action="@{/webhook/register/step1}" th:object="${webhookRegistration}">
|
||||
|
||||
<div class="webhook-field">
|
||||
<label for="targetUrl">Webhook 수신 URL <span class="req">*</span></label>
|
||||
<input type="text" id="targetUrl" th:field="*{targetUrl}"
|
||||
placeholder="https://example.com/webhook" autocomplete="off">
|
||||
<p class="field-error" th:if="${#fields.hasErrors('targetUrl')}" th:errors="*{targetUrl}">URL 오류</p>
|
||||
<p class="field-help">이벤트 발생 시 이 URL로 서명된 POST 요청이 전송됩니다.</p>
|
||||
</div>
|
||||
|
||||
<div class="webhook-field">
|
||||
<label>알림 받을 이벤트 <span class="req">*</span></label>
|
||||
<div class="webhook-eventtype-list">
|
||||
<label class="eventtype-item" th:each="et : ${eventTypes}">
|
||||
<input type="checkbox" name="eventTypes" th:value="${et.code}"
|
||||
th:checked="${#lists.contains(webhookRegistration.eventTypes, et.code)}">
|
||||
<span class="eventtype-name" th:text="${et.name}">이벤트명</span>
|
||||
<span class="eventtype-code" th:text="${et.code}">CODE</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="field-error" th:if="${#fields.hasErrors('eventTypes')}" th:errors="*{eventTypes}">이벤트 오류</p>
|
||||
</div>
|
||||
|
||||
<div class="webhook-actions">
|
||||
<a class="btn-webhook-cancel" th:href="@{/webhook/register/cancel}">취소</a>
|
||||
<button type="submit" class="btn-webhook-next">다음</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</th:block>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,56 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/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>Webhook 신청</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<th:block layout:fragment="contentFragment">
|
||||
<section class="webhook-container webhook-register">
|
||||
|
||||
<div class="webhook-steps" th:with="cur=${currentStep}">
|
||||
<div class="webhook-step" th:classappend="${cur >= 1} ? 'active'">
|
||||
<span class="step-no">1</span><span class="step-label">기본 정보</span>
|
||||
</div>
|
||||
<div class="webhook-step" th:classappend="${cur >= 2} ? 'active'">
|
||||
<span class="step-no">2</span><span class="step-label">API 선택</span>
|
||||
</div>
|
||||
<div class="webhook-step" th:classappend="${cur >= 3} ? 'active'">
|
||||
<span class="step-no">3</span><span class="step-label">완료</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="webhook-field-error" th:if="${error}" th:text="${error}"></p>
|
||||
|
||||
<form id="registerStep2Form" method="post" th:action="@{/webhook/register/step2}">
|
||||
<p class="field-help">이 Webhook으로 상태 알림을 받을 API를 선택하세요. (1개 이상)</p>
|
||||
|
||||
<div class="webhook-api-groups">
|
||||
<div class="webhook-api-group" th:each="group : ${apiGroups}">
|
||||
<h3 class="group-name" th:text="${group.groupName}">그룹명</h3>
|
||||
<div class="webhook-api-list">
|
||||
<label class="webhook-api-card"
|
||||
th:each="api : ${group.apiGroupApiList}">
|
||||
<input type="checkbox" name="selectedApis" th:value="${api.apiId}"
|
||||
th:checked="${selectedApis != null and #lists.contains(selectedApis, api.apiId)}">
|
||||
<span class="api-name" th:text="${api.apiName}">API 이름</span>
|
||||
<span class="api-id" th:text="${api.apiId}">API ID</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="webhook-actions">
|
||||
<a class="btn-webhook-cancel" th:href="@{/webhook/register/step1}">이전</a>
|
||||
<button type="submit" class="btn-webhook-next">신청 완료</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</th:block>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,67 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/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>Webhook 신청 완료</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<th:block layout:fragment="contentFragment">
|
||||
<section class="webhook-container webhook-register">
|
||||
|
||||
<div class="webhook-steps">
|
||||
<div class="webhook-step active"><span class="step-no">1</span><span class="step-label">기본 정보</span></div>
|
||||
<div class="webhook-step active"><span class="step-no">2</span><span class="step-label">API 선택</span></div>
|
||||
<div class="webhook-step active"><span class="step-no">3</span><span class="step-label">완료</span></div>
|
||||
</div>
|
||||
|
||||
<div class="webhook-complete">
|
||||
<div class="complete-icon">✅</div>
|
||||
<h3>Webhook이 등록되었습니다</h3>
|
||||
|
||||
<div class="webhook-secret-box">
|
||||
<div class="secret-warning">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
아래 Secret Key는 <strong>지금 한 번만</strong> 전체가 표시됩니다. 안전한 곳에 보관하세요.
|
||||
(분실 시 재발급 필요)
|
||||
</div>
|
||||
<label>Secret Key</label>
|
||||
<div class="secret-value-row">
|
||||
<input type="text" id="secretValue" th:value="${secret}" readonly>
|
||||
<button type="button" class="btn-copy" id="copySecret">복사</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="webhook-actions center">
|
||||
<a class="btn-webhook-next" th:href="@{/webhook}">완료</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</th:block>
|
||||
|
||||
<th:block layout:fragment="contentScript">
|
||||
<script th:inline="javascript">
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var btn = document.getElementById('copySecret');
|
||||
var input = document.getElementById('secretValue');
|
||||
if (btn && input) {
|
||||
btn.addEventListener('click', function () {
|
||||
input.select();
|
||||
input.setSelectionRange(0, 99999);
|
||||
navigator.clipboard.writeText(input.value).then(function () {
|
||||
btn.textContent = '복사됨';
|
||||
setTimeout(function () { btn.textContent = '복사'; }, 1500);
|
||||
}).catch(function () {
|
||||
document.execCommand('copy');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</th:block>
|
||||
</body>
|
||||
</html>
|
||||
@@ -71,6 +71,7 @@
|
||||
<li sec:authorize="hasRole('ROLE_APP')">
|
||||
<a th:href="@{/myapikey}"><i class="fas fa-key"></i>인증 키 관리</a>
|
||||
<a th:href="@{/statistics/api}"><i class="fas fa-chart-bar"></i>이용 통계</a>
|
||||
<a th:href="@{/webhook}"><i class="fas fa-bell"></i>Webhook 관리</a>
|
||||
</li>
|
||||
<li><a th:href="@{/mypage}"><i class="fas fa-user-circle"></i>내 정보 관리</a></li>
|
||||
<li><a th:href="@{/change_password}"><i class="fas fa-lock"></i>비밀번호 변경</a></li>
|
||||
@@ -222,6 +223,7 @@
|
||||
<li sec:authorize="hasRole('ROLE_USER_MANAGER')"><a th:href="@{/users}">이용자 관리</a></li>
|
||||
<li sec:authorize="hasRole('ROLE_APP')"><a th:href="@{/myapikey}">인증 키 관리</a></li>
|
||||
<li sec:authorize="hasRole('ROLE_APP')"><a th:href="@{/statistics/api}">이용 통계</a></li>
|
||||
<li sec:authorize="hasRole('ROLE_APP')"><a th:href="@{/webhook}">Webhook 관리</a></li>
|
||||
<li><a th:href="@{/mypage}">내 정보 관리</a></li>
|
||||
<li><a th:href="@{/change_password}">비밀번호 변경</a></li>
|
||||
</ul>
|
||||
|
||||
Reference in New Issue
Block a user